summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /widget
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget')
-rw-r--r--widget/BasicEvents.h1335
-rw-r--r--widget/ClipboardReadRequestChild.h34
-rw-r--r--widget/ClipboardReadRequestParent.cpp115
-rw-r--r--widget/ClipboardReadRequestParent.h39
-rw-r--r--widget/ClipboardWriteRequestChild.cpp73
-rw-r--r--widget/ClipboardWriteRequestChild.h40
-rw-r--r--widget/ClipboardWriteRequestParent.cpp113
-rw-r--r--widget/ClipboardWriteRequestParent.h46
-rw-r--r--widget/ColorScheme.h30
-rw-r--r--widget/CommandList.h171
-rw-r--r--widget/CompositorWidget.cpp78
-rw-r--r--widget/CompositorWidget.h298
-rw-r--r--widget/ContentCache.cpp2011
-rw-r--r--widget/ContentCache.h645
-rw-r--r--widget/ContentData.cpp29
-rw-r--r--widget/ContentData.h84
-rw-r--r--widget/ContentEvents.h313
-rw-r--r--widget/DimensionRequest.cpp120
-rw-r--r--widget/DimensionRequest.h71
-rw-r--r--widget/EventClassList.h58
-rw-r--r--widget/EventForwards.h497
-rw-r--r--widget/EventMessageList.h459
-rw-r--r--widget/FontRange.h24
-rw-r--r--widget/GfxDriverInfo.cpp777
-rw-r--r--widget/GfxDriverInfo.h501
-rw-r--r--widget/GfxInfoBase.cpp2219
-rw-r--r--widget/GfxInfoBase.h182
-rw-r--r--widget/GfxInfoCollector.cpp44
-rw-r--r--widget/GfxInfoCollector.h88
-rw-r--r--widget/IMEData.cpp418
-rw-r--r--widget/IMEData.h1085
-rw-r--r--widget/IconLoader.cpp132
-rw-r--r--widget/IconLoader.h69
-rw-r--r--widget/InProcessCompositorWidget.cpp136
-rw-r--r--widget/InProcessCompositorWidget.h55
-rw-r--r--widget/InitData.h113
-rw-r--r--widget/InputData.cpp924
-rw-r--r--widget/InputData.h835
-rw-r--r--widget/LSBUtils.cpp142
-rw-r--r--widget/LSBUtils.h25
-rw-r--r--widget/LookAndFeel.h585
-rw-r--r--widget/LookAndFeelTypes.ipdlh59
-rw-r--r--widget/MediaKeysEventSourceFactory.h24
-rw-r--r--widget/MiscEvents.h150
-rw-r--r--widget/MouseEvents.h812
-rw-r--r--widget/NativeKeyBindingsType.h19
-rw-r--r--widget/NativeKeyToDOMCodeName.h821
-rw-r--r--widget/NativeKeyToDOMKeyName.h1286
-rw-r--r--widget/NativeMenu.h101
-rw-r--r--widget/NativeMenuSupport.h47
-rw-r--r--widget/PClipboardReadRequest.ipdl25
-rw-r--r--widget/PClipboardWriteRequest.ipdl25
-rw-r--r--widget/PrintBackgroundTask.h121
-rw-r--r--widget/PuppetBidiKeyboard.cpp45
-rw-r--r--widget/PuppetBidiKeyboard.h35
-rw-r--r--widget/PuppetWidget.cpp1179
-rw-r--r--widget/PuppetWidget.h406
-rw-r--r--widget/RemoteLookAndFeel.cpp236
-rw-r--r--widget/RemoteLookAndFeel.h63
-rw-r--r--widget/Screen.cpp191
-rw-r--r--widget/Screen.h84
-rw-r--r--widget/ScreenManager.cpp261
-rw-r--r--widget/ScreenManager.h66
-rw-r--r--widget/ScrollbarDrawing.cpp424
-rw-r--r--widget/ScrollbarDrawing.h172
-rw-r--r--widget/ScrollbarDrawingAndroid.cpp84
-rw-r--r--widget/ScrollbarDrawingAndroid.h50
-rw-r--r--widget/ScrollbarDrawingCocoa.cpp476
-rw-r--r--widget/ScrollbarDrawingCocoa.h76
-rw-r--r--widget/ScrollbarDrawingGTK.cpp133
-rw-r--r--widget/ScrollbarDrawingGTK.h52
-rw-r--r--widget/ScrollbarDrawingWin.cpp173
-rw-r--r--widget/ScrollbarDrawingWin.h55
-rw-r--r--widget/ScrollbarDrawingWin11.cpp364
-rw-r--r--widget/ScrollbarDrawingWin11.h68
-rw-r--r--widget/SharedWidgetUtils.cpp273
-rw-r--r--widget/SwipeTracker.cpp272
-rw-r--r--widget/SwipeTracker.h112
-rw-r--r--widget/SystemTimeConverter.h233
-rw-r--r--widget/TextEventDispatcher.cpp1043
-rw-r--r--widget/TextEventDispatcher.h568
-rw-r--r--widget/TextEventDispatcherListener.h95
-rw-r--r--widget/TextEvents.h1511
-rw-r--r--widget/TextRange.h287
-rw-r--r--widget/TextRecognition.cpp128
-rw-r--r--widget/TextRecognition.h54
-rw-r--r--widget/Theme.cpp1718
-rw-r--r--widget/Theme.h208
-rw-r--r--widget/ThemeChangeKind.h35
-rw-r--r--widget/ThemeCocoa.cpp63
-rw-r--r--widget/ThemeCocoa.h42
-rw-r--r--widget/ThemeColors.cpp272
-rw-r--r--widget/ThemeColors.h116
-rw-r--r--widget/ThemeDrawing.cpp183
-rw-r--r--widget/ThemeDrawing.h79
-rw-r--r--widget/TouchEvents.h220
-rw-r--r--widget/TouchResampler.cpp377
-rw-r--r--widget/TouchResampler.h191
-rw-r--r--widget/VsyncDispatcher.cpp261
-rw-r--r--widget/VsyncDispatcher.h157
-rw-r--r--widget/WidgetEventImpl.cpp1970
-rw-r--r--widget/WidgetMessageUtils.h76
-rw-r--r--widget/WidgetTraceEvent.h27
-rw-r--r--widget/WidgetUtils.cpp134
-rw-r--r--widget/WidgetUtils.h96
-rw-r--r--widget/WindowButtonType.h22
-rw-r--r--widget/WindowOcclusionState.h34
-rw-r--r--widget/android/AndroidAlerts.cpp159
-rw-r--r--widget/android/AndroidAlerts.h46
-rw-r--r--widget/android/AndroidBridge.cpp431
-rw-r--r--widget/android/AndroidBridge.h275
-rw-r--r--widget/android/AndroidBridgeUtilities.h19
-rw-r--r--widget/android/AndroidCompositorWidget.cpp105
-rw-r--r--widget/android/AndroidCompositorWidget.h70
-rw-r--r--widget/android/AndroidContentController.cpp70
-rw-r--r--widget/android/AndroidContentController.h52
-rw-r--r--widget/android/AndroidUiThread.cpp373
-rw-r--r--widget/android/AndroidUiThread.h25
-rw-r--r--widget/android/AndroidView.h35
-rw-r--r--widget/android/AndroidVsync.cpp150
-rw-r--r--widget/android/AndroidVsync.h79
-rw-r--r--widget/android/AndroidWidgetUtils.cpp48
-rw-r--r--widget/android/AndroidWidgetUtils.h37
-rw-r--r--widget/android/Base64UtilsSupport.h52
-rw-r--r--widget/android/CompositorWidgetChild.cpp42
-rw-r--r--widget/android/CompositorWidgetChild.h41
-rw-r--r--widget/android/CompositorWidgetParent.cpp54
-rw-r--r--widget/android/CompositorWidgetParent.h42
-rw-r--r--widget/android/EventDispatcher.cpp776
-rw-r--r--widget/android/EventDispatcher.h104
-rw-r--r--widget/android/GeckoBatteryManager.h28
-rw-r--r--widget/android/GeckoEditableSupport.cpp1684
-rw-r--r--widget/android/GeckoEditableSupport.h286
-rw-r--r--widget/android/GeckoNetworkManager.h45
-rw-r--r--widget/android/GeckoProcessManager.cpp77
-rw-r--r--widget/android/GeckoProcessManager.h84
-rw-r--r--widget/android/GeckoSystemStateListener.h31
-rw-r--r--widget/android/GeckoTelemetryDelegate.h100
-rw-r--r--widget/android/GeckoVRManager.h24
-rw-r--r--widget/android/GeckoViewSupport.h126
-rw-r--r--widget/android/GfxInfo.cpp856
-rw-r--r--widget/android/GfxInfo.h109
-rw-r--r--widget/android/ImageDecoderSupport.cpp185
-rw-r--r--widget/android/ImageDecoderSupport.h30
-rw-r--r--widget/android/InProcessAndroidCompositorWidget.cpp57
-rw-r--r--widget/android/InProcessAndroidCompositorWidget.h45
-rw-r--r--widget/android/MediaKeysEventSourceFactory.cpp17
-rw-r--r--widget/android/PCompositorWidget.ipdl30
-rw-r--r--widget/android/PlatformWidgetTypes.ipdlh29
-rw-r--r--widget/android/ScreenHelperAndroid.cpp70
-rw-r--r--widget/android/ScreenHelperAndroid.h29
-rw-r--r--widget/android/Telemetry.h39
-rw-r--r--widget/android/WebExecutorSupport.cpp472
-rw-r--r--widget/android/WebExecutorSupport.h32
-rw-r--r--widget/android/WindowEvent.h57
-rw-r--r--widget/android/bindings/AccessibilityEvent-classes.txt3
-rw-r--r--widget/android/bindings/AndroidBuild-classes.txt5
-rw-r--r--widget/android/bindings/AndroidDragEvent-classes.txt3
-rw-r--r--widget/android/bindings/AndroidGraphics-classes.txt10
-rw-r--r--widget/android/bindings/AndroidInputType-classes.txt3
-rw-r--r--widget/android/bindings/AndroidProcess-classes.txt5
-rw-r--r--widget/android/bindings/AndroidRect-classes.txt2
-rw-r--r--widget/android/bindings/InetAddress-classes.txt6
-rw-r--r--widget/android/bindings/JavaBuiltins-classes.txt25
-rw-r--r--widget/android/bindings/JavaExceptions-classes.txt8
-rw-r--r--widget/android/bindings/KeyEvent-classes.txt3
-rw-r--r--widget/android/bindings/MediaCodec-classes.txt15
-rw-r--r--widget/android/bindings/MotionEvent-classes.txt3
-rw-r--r--widget/android/bindings/SurfaceTexture-classes.txt5
-rw-r--r--widget/android/bindings/ViewConfiguration-classes.txt1
-rw-r--r--widget/android/bindings/moz.build55
-rw-r--r--widget/android/components.conf109
-rw-r--r--widget/android/jni/Accessors.h251
-rw-r--r--widget/android/jni/Conversions.cpp115
-rw-r--r--widget/android/jni/Conversions.h23
-rw-r--r--widget/android/jni/GeckoBundleUtils.cpp309
-rw-r--r--widget/android/jni/GeckoBundleUtils.h46
-rw-r--r--widget/android/jni/GeckoResultUtils.h54
-rw-r--r--widget/android/jni/Natives.h1540
-rw-r--r--widget/android/jni/NativesInlines.h116
-rw-r--r--widget/android/jni/Refs.h1135
-rw-r--r--widget/android/jni/TypeAdapter.h71
-rw-r--r--widget/android/jni/Types.h123
-rw-r--r--widget/android/jni/Utils.cpp348
-rw-r--r--widget/android/jni/Utils.h150
-rw-r--r--widget/android/jni/moz.build36
-rw-r--r--widget/android/moz.build207
-rw-r--r--widget/android/nsAppShell.cpp755
-rw-r--r--widget/android/nsAppShell.h217
-rw-r--r--widget/android/nsClipboard.cpp214
-rw-r--r--widget/android/nsClipboard.h36
-rw-r--r--widget/android/nsDeviceContextAndroid.cpp100
-rw-r--r--widget/android/nsDeviceContextAndroid.h36
-rw-r--r--widget/android/nsDragService.cpp272
-rw-r--r--widget/android/nsDragService.h57
-rw-r--r--widget/android/nsIAndroidBridge.idl58
-rw-r--r--widget/android/nsLookAndFeel.cpp460
-rw-r--r--widget/android/nsLookAndFeel.h57
-rw-r--r--widget/android/nsPrintSettingsServiceAndroid.cpp33
-rw-r--r--widget/android/nsPrintSettingsServiceAndroid.h18
-rw-r--r--widget/android/nsUserIdleServiceAndroid.cpp12
-rw-r--r--widget/android/nsUserIdleServiceAndroid.h40
-rw-r--r--widget/android/nsWidgetFactory.cpp21
-rw-r--r--widget/android/nsWidgetFactory.h21
-rw-r--r--widget/android/nsWindow.cpp3381
-rw-r--r--widget/android/nsWindow.h299
-rw-r--r--widget/cocoa/AppearanceOverride.h19
-rw-r--r--widget/cocoa/AppearanceOverride.mm90
-rw-r--r--widget/cocoa/CFTypeRefPtr.h194
-rw-r--r--widget/cocoa/CustomCocoaEvents.h18
-rw-r--r--widget/cocoa/DesktopBackgroundImage.h19
-rw-r--r--widget/cocoa/DesktopBackgroundImage.mm69
-rw-r--r--widget/cocoa/GfxInfo.h98
-rw-r--r--widget/cocoa/GfxInfo.mm562
-rw-r--r--widget/cocoa/MOZIconHelper.h35
-rw-r--r--widget/cocoa/MOZIconHelper.mm65
-rw-r--r--widget/cocoa/MOZMenuOpeningCoordinator.h57
-rw-r--r--widget/cocoa/MOZMenuOpeningCoordinator.mm224
-rw-r--r--widget/cocoa/MacThemeGeometryType.h17
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMac.h47
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMac.mm191
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h62
-rw-r--r--widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm187
-rw-r--r--widget/cocoa/MediaKeysEventSourceFactory.cpp18
-rw-r--r--widget/cocoa/NativeKeyBindings.h67
-rw-r--r--widget/cocoa/NativeKeyBindings.mm655
-rw-r--r--widget/cocoa/NativeMenuMac.h94
-rw-r--r--widget/cocoa/NativeMenuMac.mm410
-rw-r--r--widget/cocoa/NativeMenuSupport.mm37
-rw-r--r--widget/cocoa/OSXNotificationCenter.h57
-rw-r--r--widget/cocoa/OSXNotificationCenter.mm591
-rw-r--r--widget/cocoa/ScreenHelperCocoa.h34
-rw-r--r--widget/cocoa/ScreenHelperCocoa.mm181
-rw-r--r--widget/cocoa/TextInputHandler.h1352
-rw-r--r--widget/cocoa/TextInputHandler.mm5389
-rw-r--r--widget/cocoa/TextRecognition.mm121
-rw-r--r--widget/cocoa/VibrancyManager.h92
-rw-r--r--widget/cocoa/VibrancyManager.mm110
-rw-r--r--widget/cocoa/ViewRegion.h55
-rw-r--r--widget/cocoa/ViewRegion.mm68
-rw-r--r--widget/cocoa/WidgetTraceEvent.mm79
-rw-r--r--widget/cocoa/components.conf168
-rw-r--r--widget/cocoa/crashtests/373122-1-inner.html39
-rw-r--r--widget/cocoa/crashtests/373122-1.html9
-rw-r--r--widget/cocoa/crashtests/397209-1.html7
-rw-r--r--widget/cocoa/crashtests/403296-1.xhtml10
-rw-r--r--widget/cocoa/crashtests/419737-1.html8
-rw-r--r--widget/cocoa/crashtests/435223-1.html8
-rw-r--r--widget/cocoa/crashtests/444260-1.xhtml3
-rw-r--r--widget/cocoa/crashtests/444864-1.html6
-rw-r--r--widget/cocoa/crashtests/449111-1.html4
-rw-r--r--widget/cocoa/crashtests/460349-1.xhtml4
-rw-r--r--widget/cocoa/crashtests/460387-1.html2
-rw-r--r--widget/cocoa/crashtests/464589-1.html20
-rw-r--r--widget/cocoa/crashtests/crashtests.list11
-rw-r--r--widget/cocoa/cursors/arrowN.pngbin0 -> 253 bytes
-rw-r--r--widget/cocoa/cursors/arrowN@2x.pngbin0 -> 614 bytes
-rw-r--r--widget/cocoa/cursors/arrowS.pngbin0 -> 250 bytes
-rw-r--r--widget/cocoa/cursors/arrowS@2x.pngbin0 -> 609 bytes
-rw-r--r--widget/cocoa/cursors/cell.pngbin0 -> 264 bytes
-rw-r--r--widget/cocoa/cursors/cell@2x.pngbin0 -> 639 bytes
-rw-r--r--widget/cocoa/cursors/colResize.pngbin0 -> 320 bytes
-rw-r--r--widget/cocoa/cursors/colResize@2x.pngbin0 -> 825 bytes
-rw-r--r--widget/cocoa/cursors/help.pngbin0 -> 713 bytes
-rw-r--r--widget/cocoa/cursors/help@2x.pngbin0 -> 1679 bytes
-rw-r--r--widget/cocoa/cursors/move.pngbin0 -> 281 bytes
-rw-r--r--widget/cocoa/cursors/move@2x.pngbin0 -> 619 bytes
-rw-r--r--widget/cocoa/cursors/rowResize.pngbin0 -> 329 bytes
-rw-r--r--widget/cocoa/cursors/rowResize@2x.pngbin0 -> 843 bytes
-rw-r--r--widget/cocoa/cursors/sizeNE.pngbin0 -> 274 bytes
-rw-r--r--widget/cocoa/cursors/sizeNE@2x.pngbin0 -> 775 bytes
-rw-r--r--widget/cocoa/cursors/sizeNESW.pngbin0 -> 295 bytes
-rw-r--r--widget/cocoa/cursors/sizeNESW@2x.pngbin0 -> 948 bytes
-rw-r--r--widget/cocoa/cursors/sizeNS.pngbin0 -> 279 bytes
-rw-r--r--widget/cocoa/cursors/sizeNS@2x.pngbin0 -> 658 bytes
-rw-r--r--widget/cocoa/cursors/sizeNW.pngbin0 -> 274 bytes
-rw-r--r--widget/cocoa/cursors/sizeNW@2x.pngbin0 -> 771 bytes
-rw-r--r--widget/cocoa/cursors/sizeNWSE.pngbin0 -> 288 bytes
-rw-r--r--widget/cocoa/cursors/sizeNWSE@2x.pngbin0 -> 947 bytes
-rw-r--r--widget/cocoa/cursors/sizeSE.pngbin0 -> 264 bytes
-rw-r--r--widget/cocoa/cursors/sizeSE@2x.pngbin0 -> 783 bytes
-rw-r--r--widget/cocoa/cursors/sizeSW.pngbin0 -> 268 bytes
-rw-r--r--widget/cocoa/cursors/sizeSW@2x.pngbin0 -> 783 bytes
-rw-r--r--widget/cocoa/cursors/vtIBeam.pngbin0 -> 104 bytes
-rw-r--r--widget/cocoa/cursors/vtIBeam@2x.pngbin0 -> 331 bytes
-rw-r--r--widget/cocoa/cursors/zoomIn.pngbin0 -> 648 bytes
-rw-r--r--widget/cocoa/cursors/zoomIn@2x.pngbin0 -> 1702 bytes
-rw-r--r--widget/cocoa/cursors/zoomOut.pngbin0 -> 641 bytes
-rw-r--r--widget/cocoa/cursors/zoomOut@2x.pngbin0 -> 1693 bytes
-rw-r--r--widget/cocoa/docs/index.md9
-rw-r--r--widget/cocoa/docs/macos-apis.md188
-rw-r--r--widget/cocoa/docs/sdks.md227
-rw-r--r--widget/cocoa/metrics.yaml11
-rw-r--r--widget/cocoa/moz.build181
-rw-r--r--widget/cocoa/mozView.h62
-rw-r--r--widget/cocoa/nsAppShell.h96
-rw-r--r--widget/cocoa/nsAppShell.mm1154
-rw-r--r--widget/cocoa/nsBidiKeyboard.h23
-rw-r--r--widget/cocoa/nsBidiKeyboard.mm38
-rw-r--r--widget/cocoa/nsChangeObserver.h71
-rw-r--r--widget/cocoa/nsChildView.h621
-rw-r--r--widget/cocoa/nsChildView.mm5159
-rw-r--r--widget/cocoa/nsClipboard.h65
-rw-r--r--widget/cocoa/nsClipboard.mm875
-rw-r--r--widget/cocoa/nsCocoaFeatures.h48
-rw-r--r--widget/cocoa/nsCocoaFeatures.mm206
-rw-r--r--widget/cocoa/nsCocoaUtils.h595
-rw-r--r--widget/cocoa/nsCocoaUtils.mm1857
-rw-r--r--widget/cocoa/nsCocoaWindow.h515
-rw-r--r--widget/cocoa/nsCocoaWindow.mm4460
-rw-r--r--widget/cocoa/nsColorPicker.h41
-rw-r--r--widget/cocoa/nsColorPicker.mm165
-rw-r--r--widget/cocoa/nsCursorManager.h63
-rw-r--r--widget/cocoa/nsCursorManager.mm363
-rw-r--r--widget/cocoa/nsDeviceContextSpecX.h53
-rw-r--r--widget/cocoa/nsDeviceContextSpecX.mm318
-rw-r--r--widget/cocoa/nsDragService.h62
-rw-r--r--widget/cocoa/nsDragService.mm507
-rw-r--r--widget/cocoa/nsFilePicker.h74
-rw-r--r--widget/cocoa/nsFilePicker.mm679
-rw-r--r--widget/cocoa/nsLookAndFeel.h43
-rw-r--r--widget/cocoa/nsLookAndFeel.mm679
-rw-r--r--widget/cocoa/nsMacCursor.h129
-rw-r--r--widget/cocoa/nsMacCursor.mm393
-rw-r--r--widget/cocoa/nsMacDockSupport.h35
-rw-r--r--widget/cocoa/nsMacDockSupport.mm436
-rw-r--r--widget/cocoa/nsMacFinderProgress.h24
-rw-r--r--widget/cocoa/nsMacFinderProgress.mm93
-rw-r--r--widget/cocoa/nsMacSharingService.h22
-rw-r--r--widget/cocoa/nsMacSharingService.mm218
-rw-r--r--widget/cocoa/nsMacUserActivityUpdater.h23
-rw-r--r--widget/cocoa/nsMacUserActivityUpdater.mm65
-rw-r--r--widget/cocoa/nsMacWebAppUtils.h22
-rw-r--r--widget/cocoa/nsMacWebAppUtils.mm98
-rw-r--r--widget/cocoa/nsMenuBarX.h161
-rw-r--r--widget/cocoa/nsMenuBarX.mm1169
-rw-r--r--widget/cocoa/nsMenuGroupOwnerX.h102
-rw-r--r--widget/cocoa/nsMenuGroupOwnerX.mm238
-rw-r--r--widget/cocoa/nsMenuItemIconX.h70
-rw-r--r--widget/cocoa/nsMenuItemIconX.mm173
-rw-r--r--widget/cocoa/nsMenuItemX.h105
-rw-r--r--widget/cocoa/nsMenuItemX.mm449
-rw-r--r--widget/cocoa/nsMenuParentX.h31
-rw-r--r--widget/cocoa/nsMenuUtilsX.h53
-rw-r--r--widget/cocoa/nsMenuUtilsX.mm316
-rw-r--r--widget/cocoa/nsMenuX.h313
-rw-r--r--widget/cocoa/nsMenuX.mm1490
-rw-r--r--widget/cocoa/nsNativeThemeCocoa.h408
-rw-r--r--widget/cocoa/nsNativeThemeCocoa.mm3328
-rw-r--r--widget/cocoa/nsNativeThemeColors.h57
-rw-r--r--widget/cocoa/nsPIWidgetCocoa.idl37
-rw-r--r--widget/cocoa/nsPrintDialogX.h62
-rw-r--r--widget/cocoa/nsPrintDialogX.mm636
-rw-r--r--widget/cocoa/nsPrintSettingsServiceX.h33
-rw-r--r--widget/cocoa/nsPrintSettingsServiceX.mm78
-rw-r--r--widget/cocoa/nsPrintSettingsX.h106
-rw-r--r--widget/cocoa/nsPrintSettingsX.mm372
-rw-r--r--widget/cocoa/nsSandboxViolationSink.h36
-rw-r--r--widget/cocoa/nsSandboxViolationSink.mm112
-rw-r--r--widget/cocoa/nsSound.h25
-rw-r--r--widget/cocoa/nsSound.mm71
-rw-r--r--widget/cocoa/nsStandaloneNativeMenu.h27
-rw-r--r--widget/cocoa/nsStandaloneNativeMenu.mm81
-rw-r--r--widget/cocoa/nsSystemStatusBarCocoa.h40
-rw-r--r--widget/cocoa/nsSystemStatusBarCocoa.mm70
-rw-r--r--widget/cocoa/nsToolkit.h49
-rw-r--r--widget/cocoa/nsToolkit.mm265
-rw-r--r--widget/cocoa/nsTouchBar.h141
-rw-r--r--widget/cocoa/nsTouchBar.mm650
-rw-r--r--widget/cocoa/nsTouchBarInput.h91
-rw-r--r--widget/cocoa/nsTouchBarInput.mm251
-rw-r--r--widget/cocoa/nsTouchBarInputIcon.h70
-rw-r--r--widget/cocoa/nsTouchBarInputIcon.mm141
-rw-r--r--widget/cocoa/nsTouchBarUpdater.h23
-rw-r--r--widget/cocoa/nsTouchBarUpdater.mm116
-rw-r--r--widget/cocoa/nsUserIdleServiceX.h36
-rw-r--r--widget/cocoa/nsUserIdleServiceX.mm60
-rw-r--r--widget/cocoa/nsWidgetFactory.h44
-rw-r--r--widget/cocoa/nsWidgetFactory.mm124
-rw-r--r--widget/cocoa/nsWindowMap.h61
-rw-r--r--widget/cocoa/nsWindowMap.mm291
-rw-r--r--widget/cocoa/resources/MainMenu.nib/classes.nib4
-rw-r--r--widget/cocoa/resources/MainMenu.nib/info.nib21
-rw-r--r--widget/cocoa/resources/MainMenu.nib/keyedobjects.nibbin0 -> 1877 bytes
-rw-r--r--widget/components.conf83
-rw-r--r--widget/crashtests/1128214.html19
-rw-r--r--widget/crashtests/303901-1.html29
-rw-r--r--widget/crashtests/303901-2.html20
-rw-r--r--widget/crashtests/380359-1.xhtml8
-rw-r--r--widget/crashtests/crashtests.list4
-rw-r--r--widget/generic/PCompositorWidget.ipdl29
-rw-r--r--widget/generic/PlatformWidgetTypes.ipdlh21
-rw-r--r--widget/gtk/AsyncDBus.cpp90
-rw-r--r--widget/gtk/AsyncDBus.h38
-rw-r--r--widget/gtk/AsyncGtkClipboardRequest.cpp120
-rw-r--r--widget/gtk/AsyncGtkClipboardRequest.h61
-rw-r--r--widget/gtk/CompositorWidgetChild.cpp51
-rw-r--r--widget/gtk/CompositorWidgetChild.h41
-rw-r--r--widget/gtk/CompositorWidgetParent.cpp54
-rw-r--r--widget/gtk/CompositorWidgetParent.h42
-rw-r--r--widget/gtk/DMABufLibWrapper.cpp346
-rw-r--r--widget/gtk/DMABufLibWrapper.h231
-rw-r--r--widget/gtk/DMABufSurface.cpp1729
-rw-r--r--widget/gtk/DMABufSurface.h413
-rw-r--r--widget/gtk/GRefPtr.h71
-rw-r--r--widget/gtk/GUniquePtr.h35
-rw-r--r--widget/gtk/GfxInfo.cpp1556
-rw-r--r--widget/gtk/GfxInfo.h137
-rw-r--r--widget/gtk/GfxInfoUtils.h98
-rw-r--r--widget/gtk/GtkCompositorWidget.cpp247
-rw-r--r--widget/gtk/GtkCompositorWidget.h140
-rw-r--r--widget/gtk/IMContextWrapper.cpp3358
-rw-r--r--widget/gtk/IMContextWrapper.h692
-rw-r--r--widget/gtk/InProcessGtkCompositorWidget.cpp44
-rw-r--r--widget/gtk/InProcessGtkCompositorWidget.h30
-rw-r--r--widget/gtk/MPRISInterfaceDescription.h91
-rw-r--r--widget/gtk/MPRISServiceHandler.cpp909
-rw-r--r--widget/gtk/MPRISServiceHandler.h195
-rw-r--r--widget/gtk/MediaKeysEventSourceFactory.cpp14
-rw-r--r--widget/gtk/MozContainer.cpp402
-rw-r--r--widget/gtk/MozContainer.h97
-rw-r--r--widget/gtk/MozContainerWayland.cpp824
-rw-r--r--widget/gtk/MozContainerWayland.h110
-rw-r--r--widget/gtk/NativeKeyBindings.cpp527
-rw-r--r--widget/gtk/NativeKeyBindings.h62
-rw-r--r--widget/gtk/NativeMenuGtk.cpp424
-rw-r--r--widget/gtk/NativeMenuGtk.h64
-rw-r--r--widget/gtk/NativeMenuSupport.cpp29
-rw-r--r--widget/gtk/PCompositorWidget.ipdl35
-rw-r--r--widget/gtk/PlatformWidgetTypes.ipdlh33
-rw-r--r--widget/gtk/ScreenHelperGTK.cpp306
-rw-r--r--widget/gtk/ScreenHelperGTK.h30
-rw-r--r--widget/gtk/TaskbarProgress.cpp106
-rw-r--r--widget/gtk/TaskbarProgress.h33
-rw-r--r--widget/gtk/WakeLockListener.cpp913
-rw-r--r--widget/gtk/WakeLockListener.h38
-rw-r--r--widget/gtk/WaylandBuffer.cpp224
-rw-r--r--widget/gtk/WaylandBuffer.h140
-rw-r--r--widget/gtk/WaylandVsyncSource.cpp431
-rw-r--r--widget/gtk/WaylandVsyncSource.h99
-rw-r--r--widget/gtk/WidgetStyleCache.cpp1434
-rw-r--r--widget/gtk/WidgetStyleCache.h63
-rw-r--r--widget/gtk/WidgetTraceEvent.cpp68
-rw-r--r--widget/gtk/WidgetUtilsGtk.cpp500
-rw-r--r--widget/gtk/WidgetUtilsGtk.h83
-rw-r--r--widget/gtk/WindowSurface.h41
-rw-r--r--widget/gtk/WindowSurfaceProvider.cpp212
-rw-r--r--widget/gtk/WindowSurfaceProvider.h101
-rw-r--r--widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp418
-rw-r--r--widget/gtk/WindowSurfaceWaylandMultiBuffer.h84
-rw-r--r--widget/gtk/WindowSurfaceX11.cpp47
-rw-r--r--widget/gtk/WindowSurfaceX11.h39
-rw-r--r--widget/gtk/WindowSurfaceX11Image.cpp272
-rw-r--r--widget/gtk/WindowSurfaceX11Image.h49
-rw-r--r--widget/gtk/WindowSurfaceX11SHM.cpp27
-rw-r--r--widget/gtk/WindowSurfaceX11SHM.h36
-rw-r--r--widget/gtk/compat/gdk/gdkdnd.h29
-rw-r--r--widget/gtk/compat/gdk/gdkkeysyms.h266
-rw-r--r--widget/gtk/compat/gdk/gdkvisual.h15
-rw-r--r--widget/gtk/compat/gdk/gdkwindow.h27
-rw-r--r--widget/gtk/compat/gdk/gdkx.h42
-rw-r--r--widget/gtk/compat/glib/gmem.h48
-rw-r--r--widget/gtk/compat/gtk/gtkwidget.h43
-rw-r--r--widget/gtk/compat/gtk/gtkwindow.h26
-rw-r--r--widget/gtk/components.conf151
-rw-r--r--widget/gtk/crashtests/540078-1.xhtml1
-rw-r--r--widget/gtk/crashtests/673390-1.html1
-rw-r--r--widget/gtk/crashtests/crashtests.list2
-rw-r--r--widget/gtk/gbm.h480
-rw-r--r--widget/gtk/gtk3drawing.cpp2179
-rw-r--r--widget/gtk/gtkdrawing.h524
-rw-r--r--widget/gtk/moz.build179
-rw-r--r--widget/gtk/mozgtk/moz.build37
-rw-r--r--widget/gtk/mozgtk/mozgtk.c30
-rw-r--r--widget/gtk/mozwayland/moz.build16
-rw-r--r--widget/gtk/mozwayland/mozwayland.c230
-rw-r--r--widget/gtk/mozwayland/mozwayland.h134
-rw-r--r--widget/gtk/nsAppShell.cpp494
-rw-r--r--widget/gtk/nsAppShell.h69
-rw-r--r--widget/gtk/nsApplicationChooser.cpp132
-rw-r--r--widget/gtk/nsApplicationChooser.h32
-rw-r--r--widget/gtk/nsBidiKeyboard.cpp52
-rw-r--r--widget/gtk/nsBidiKeyboard.h25
-rw-r--r--widget/gtk/nsClipboard.cpp1425
-rw-r--r--widget/gtk/nsClipboard.h177
-rw-r--r--widget/gtk/nsClipboardWayland.cpp75
-rw-r--r--widget/gtk/nsClipboardWayland.h28
-rw-r--r--widget/gtk/nsClipboardX11.cpp173
-rw-r--r--widget/gtk/nsClipboardX11.h32
-rw-r--r--widget/gtk/nsColorPicker.cpp253
-rw-r--r--widget/gtk/nsColorPicker.h71
-rw-r--r--widget/gtk/nsDeviceContextSpecG.cpp422
-rw-r--r--widget/gtk/nsDeviceContextSpecG.h62
-rw-r--r--widget/gtk/nsDragService.cpp2756
-rw-r--r--widget/gtk/nsDragService.h268
-rw-r--r--widget/gtk/nsFilePicker.cpp785
-rw-r--r--widget/gtk/nsFilePicker.h96
-rw-r--r--widget/gtk/nsGTKToolkit.h52
-rw-r--r--widget/gtk/nsGtkCursors.h416
-rw-r--r--widget/gtk/nsGtkKeyUtils.cpp2541
-rw-r--r--widget/gtk/nsGtkKeyUtils.h509
-rw-r--r--widget/gtk/nsGtkUtils.h59
-rw-r--r--widget/gtk/nsImageToPixbuf.cpp121
-rw-r--r--widget/gtk/nsImageToPixbuf.h37
-rw-r--r--widget/gtk/nsLookAndFeel.cpp2329
-rw-r--r--widget/gtk/nsLookAndFeel.h211
-rw-r--r--widget/gtk/nsNativeThemeGTK.cpp1369
-rw-r--r--widget/gtk/nsNativeThemeGTK.h119
-rw-r--r--widget/gtk/nsPrintDialogGTK.cpp621
-rw-r--r--widget/gtk/nsPrintDialogGTK.h35
-rw-r--r--widget/gtk/nsPrintSettingsGTK.cpp681
-rw-r--r--widget/gtk/nsPrintSettingsGTK.h147
-rw-r--r--widget/gtk/nsPrintSettingsServiceGTK.cpp80
-rw-r--r--widget/gtk/nsPrintSettingsServiceGTK.h33
-rw-r--r--widget/gtk/nsShmImage.cpp326
-rw-r--r--widget/gtk/nsShmImage.h75
-rw-r--r--widget/gtk/nsSound.cpp397
-rw-r--r--widget/gtk/nsSound.h33
-rw-r--r--widget/gtk/nsToolkit.cpp24
-rw-r--r--widget/gtk/nsUserIdleServiceGTK.cpp317
-rw-r--r--widget/gtk/nsUserIdleServiceGTK.h78
-rw-r--r--widget/gtk/nsWaylandDisplay.cpp204
-rw-r--r--widget/gtk/nsWaylandDisplay.h120
-rw-r--r--widget/gtk/nsWidgetFactory.cpp67
-rw-r--r--widget/gtk/nsWidgetFactory.h21
-rw-r--r--widget/gtk/nsWindow.cpp10028
-rw-r--r--widget/gtk/nsWindow.h1016
-rw-r--r--widget/gtk/v4l2test/moz.build17
-rw-r--r--widget/gtk/v4l2test/v4l2test.cpp188
-rw-r--r--widget/gtk/va_drmcommon.h156
-rw-r--r--widget/gtk/vaapitest/moz.build20
-rw-r--r--widget/gtk/vaapitest/vaapitest.cpp255
-rw-r--r--widget/gtk/wayland/fractional-scale-v1-client-protocol.h268
-rw-r--r--widget/gtk/wayland/fractional-scale-v1-protocol.c73
-rw-r--r--widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h228
-rw-r--r--widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c60
-rw-r--r--widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h650
-rw-r--r--widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c81
-rw-r--r--widget/gtk/wayland/moz.build37
-rw-r--r--widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h650
-rw-r--r--widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c97
-rw-r--r--widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h293
-rw-r--r--widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c69
-rw-r--r--widget/gtk/wayland/viewporter-client-protocol.h392
-rw-r--r--widget/gtk/wayland/viewporter-protocol.c56
-rw-r--r--widget/gtk/wayland/xdg-activation-v1-client-protocol.h409
-rw-r--r--widget/gtk/wayland/xdg-activation-v1-protocol.c82
-rw-r--r--widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h392
-rw-r--r--widget/gtk/wayland/xdg-output-unstable-v1-protocol.c74
-rw-r--r--widget/headless/HeadlessClipboard.cpp154
-rw-r--r--widget/headless/HeadlessClipboard.h45
-rw-r--r--widget/headless/HeadlessClipboardData.cpp35
-rw-r--r--widget/headless/HeadlessClipboardData.h44
-rw-r--r--widget/headless/HeadlessCompositorWidget.cpp46
-rw-r--r--widget/headless/HeadlessCompositorWidget.h54
-rw-r--r--widget/headless/HeadlessKeyBindings.cpp37
-rw-r--r--widget/headless/HeadlessKeyBindings.h42
-rw-r--r--widget/headless/HeadlessKeyBindingsCocoa.mm53
-rw-r--r--widget/headless/HeadlessLookAndFeel.h58
-rw-r--r--widget/headless/HeadlessLookAndFeelGTK.cpp223
-rw-r--r--widget/headless/HeadlessScreenHelper.cpp43
-rw-r--r--widget/headless/HeadlessScreenHelper.h27
-rw-r--r--widget/headless/HeadlessSound.cpp36
-rw-r--r--widget/headless/HeadlessSound.h31
-rw-r--r--widget/headless/HeadlessWidget.cpp625
-rw-r--r--widget/headless/HeadlessWidget.h182
-rw-r--r--widget/headless/HeadlessWidgetTypes.ipdlh20
-rw-r--r--widget/headless/moz.build48
-rw-r--r--widget/headless/tests/headless.html6
-rw-r--r--widget/headless/tests/headless_button.html6
-rw-r--r--widget/headless/tests/moz.build7
-rw-r--r--widget/headless/tests/test_headless.js224
-rw-r--r--widget/headless/tests/test_headless_clipboard.js45
-rw-r--r--widget/headless/tests/xpcshell.toml14
-rw-r--r--widget/moz.build396
-rw-r--r--widget/nsAppShellSingleton.h60
-rw-r--r--widget/nsAutoRollup.cpp50
-rw-r--r--widget/nsAutoRollup.h54
-rw-r--r--widget/nsBaseAppShell.cpp313
-rw-r--r--widget/nsBaseAppShell.h140
-rw-r--r--widget/nsBaseClipboard.cpp1313
-rw-r--r--widget/nsBaseClipboard.h220
-rw-r--r--widget/nsBaseDragService.cpp1044
-rw-r--r--widget/nsBaseDragService.h221
-rw-r--r--widget/nsBaseFilePicker.cpp496
-rw-r--r--widget/nsBaseFilePicker.h82
-rw-r--r--widget/nsBaseWidget.cpp3513
-rw-r--r--widget/nsBaseWidget.h777
-rw-r--r--widget/nsCUPSShim.cpp76
-rw-r--r--widget/nsCUPSShim.h96
-rw-r--r--widget/nsClipboardHelper.cpp123
-rw-r--r--widget/nsClipboardHelper.h30
-rw-r--r--widget/nsClipboardProxy.cpp271
-rw-r--r--widget/nsClipboardProxy.h48
-rw-r--r--widget/nsColorPickerProxy.cpp54
-rw-r--r--widget/nsColorPickerProxy.h33
-rw-r--r--widget/nsContentProcessWidgetFactory.h57
-rw-r--r--widget/nsDeviceContextSpecProxy.cpp164
-rw-r--r--widget/nsDeviceContextSpecProxy.h59
-rw-r--r--widget/nsDragServiceProxy.cpp96
-rw-r--r--widget/nsDragServiceProxy.h27
-rw-r--r--widget/nsFilePickerProxy.cpp296
-rw-r--r--widget/nsFilePickerProxy.h89
-rw-r--r--widget/nsGUIEventIPC.h1406
-rw-r--r--widget/nsHTMLFormatConverter.cpp184
-rw-r--r--widget/nsHTMLFormatConverter.h32
-rw-r--r--widget/nsIAppShell.idl71
-rw-r--r--widget/nsIApplicationChooser.idl39
-rw-r--r--widget/nsIBaseWindow.cpp14
-rw-r--r--widget/nsIBaseWindow.idl296
-rw-r--r--widget/nsIBidiKeyboard.idl31
-rw-r--r--widget/nsIClipboard.idl215
-rw-r--r--widget/nsIClipboardHelper.idl45
-rw-r--r--widget/nsIClipboardOwner.idl29
-rw-r--r--widget/nsIColorPicker.idl72
-rw-r--r--widget/nsIDeviceContextSpec.cpp87
-rw-r--r--widget/nsIDeviceContextSpec.h111
-rw-r--r--widget/nsIDisplayInfo.idl14
-rw-r--r--widget/nsIDragService.idl213
-rw-r--r--widget/nsIDragSession.idl151
-rw-r--r--widget/nsIFilePicker.idl255
-rw-r--r--widget/nsIFormatConverter.idl50
-rw-r--r--widget/nsIGfxInfo.idl366
-rw-r--r--widget/nsIGfxInfoDebug.idl19
-rw-r--r--widget/nsIGtkTaskbarProgress.idl22
-rw-r--r--widget/nsIJumpListBuilder.idl119
-rw-r--r--widget/nsILegacyJumpListBuilder.idl161
-rw-r--r--widget/nsILegacyJumpListItem.idl120
-rw-r--r--widget/nsIMacDockSupport.idl71
-rw-r--r--widget/nsIMacFinderProgress.idl43
-rw-r--r--widget/nsIMacSharingService.idl30
-rw-r--r--widget/nsIMacUserActivityUpdater.idl24
-rw-r--r--widget/nsIMacWebAppUtils.idl35
-rw-r--r--widget/nsIPaper.idl42
-rw-r--r--widget/nsIPaperMargin.idl16
-rw-r--r--widget/nsIPrintDialogService.idl66
-rw-r--r--widget/nsIPrintSettings.idl405
-rw-r--r--widget/nsIPrintSettingsService.idl151
-rw-r--r--widget/nsIPrintSettingsWin.idl56
-rw-r--r--widget/nsIPrinter.idl83
-rw-r--r--widget/nsIPrinterList.idl61
-rw-r--r--widget/nsIRollupListener.h81
-rw-r--r--widget/nsIScreen.idl127
-rw-r--r--widget/nsIScreenManager.idl36
-rw-r--r--widget/nsISharePicker.idl32
-rw-r--r--widget/nsISound.idl40
-rw-r--r--widget/nsIStandaloneNativeMenu.idl55
-rw-r--r--widget/nsISystemStatusBar.idl36
-rw-r--r--widget/nsITaskbarOverlayIconController.idl39
-rw-r--r--widget/nsITaskbarPreview.idl70
-rw-r--r--widget/nsITaskbarPreviewButton.idl62
-rw-r--r--widget/nsITaskbarPreviewController.idl103
-rw-r--r--widget/nsITaskbarProgress.idl58
-rw-r--r--widget/nsITaskbarTabPreview.idl62
-rw-r--r--widget/nsITaskbarWindowPreview.idl69
-rw-r--r--widget/nsITouchBarHelper.idl65
-rw-r--r--widget/nsITouchBarInput.idl78
-rw-r--r--widget/nsITouchBarUpdater.idl42
-rw-r--r--widget/nsITransferable.idl225
-rw-r--r--widget/nsIUserIdleService.idl86
-rw-r--r--widget/nsIUserIdleServiceInternal.idl17
-rw-r--r--widget/nsIWidget.h2144
-rw-r--r--widget/nsIWidgetListener.cpp89
-rw-r--r--widget/nsIWidgetListener.h194
-rw-r--r--widget/nsIWinTaskbar.idl196
-rw-r--r--widget/nsIWindowsUIUtils.idl34
-rw-r--r--widget/nsNativeTheme.cpp580
-rw-r--r--widget/nsNativeTheme.h170
-rw-r--r--widget/nsPaper.cpp87
-rw-r--r--widget/nsPaper.h128
-rw-r--r--widget/nsPaperMargin.cpp32
-rw-r--r--widget/nsPaperMargin.h29
-rw-r--r--widget/nsPrimitiveHelpers.cpp193
-rw-r--r--widget/nsPrimitiveHelpers.h54
-rw-r--r--widget/nsPrintSettingsImpl.cpp941
-rw-r--r--widget/nsPrintSettingsImpl.h134
-rw-r--r--widget/nsPrintSettingsService.cpp1090
-rw-r--r--widget/nsPrintSettingsService.h87
-rw-r--r--widget/nsPrinterBase.cpp247
-rw-r--r--widget/nsPrinterBase.h114
-rw-r--r--widget/nsPrinterCUPS.cpp476
-rw-r--r--widget/nsPrinterCUPS.h181
-rw-r--r--widget/nsPrinterListBase.cpp168
-rw-r--r--widget/nsPrinterListBase.h92
-rw-r--r--widget/nsPrinterListCUPS.cpp233
-rw-r--r--widget/nsPrinterListCUPS.h32
-rw-r--r--widget/nsTransferable.cpp561
-rw-r--r--widget/nsTransferable.h92
-rw-r--r--widget/nsUserIdleService.cpp900
-rw-r--r--widget/nsUserIdleService.h218
-rw-r--r--widget/nsWidgetsCID.h332
-rw-r--r--widget/nsXPLookAndFeel.cpp1547
-rw-r--r--widget/nsXPLookAndFeel.h96
-rw-r--r--widget/reftests/664925.xhtml1
-rw-r--r--widget/reftests/meter-fallback-default-style-ref.html57
-rw-r--r--widget/reftests/meter-fallback-default-style.html21
-rw-r--r--widget/reftests/meter-native-style-ref.html19
-rw-r--r--widget/reftests/meter-native-style.html18
-rw-r--r--widget/reftests/meter-vertical-native-style-ref.html14
-rw-r--r--widget/reftests/meter-vertical-native-style.html13
-rw-r--r--widget/reftests/progressbar-fallback-default-style-ref.html33
-rw-r--r--widget/reftests/progressbar-fallback-default-style.html20
-rw-r--r--widget/reftests/reftest.list9
-rw-r--r--widget/reftests/scaled-scrollbar.html6
-rw-r--r--widget/reftests/scrollbar-buttons.html4
-rw-r--r--widget/tests/TestChromeMargin.cpp130
-rw-r--r--widget/tests/browser/browser.toml90
-rw-r--r--widget/tests/browser/browser_test_AZERTY_digit_shortcut.js84
-rw-r--r--widget/tests/browser/browser_test_ContentCache.js296
-rw-r--r--widget/tests/browser/browser_test_InputContextURI.js156
-rw-r--r--widget/tests/browser/browser_test_clipboard_contextmenu.js127
-rw-r--r--widget/tests/browser/browser_test_clipboardcache.js137
-rw-r--r--widget/tests/browser/browser_test_fullscreen_size.js66
-rw-r--r--widget/tests/browser/browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js122
-rw-r--r--widget/tests/browser/browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js261
-rw-r--r--widget/tests/browser/browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js116
-rw-r--r--widget/tests/browser/browser_test_ime_state_in_plugin_in_remote_content.js120
-rw-r--r--widget/tests/browser/browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js78
-rw-r--r--widget/tests/browser/browser_test_ime_state_on_editable_state_change_in_remote_content.js297
-rw-r--r--widget/tests/browser/browser_test_ime_state_on_focus_move_in_remote_content.js128
-rw-r--r--widget/tests/browser/browser_test_ime_state_on_input_type_change_in_remote_content.js70
-rw-r--r--widget/tests/browser/browser_test_ime_state_on_readonly_change_in_remote_content.js68
-rw-r--r--widget/tests/browser/browser_test_scrollbar_colors.js146
-rw-r--r--widget/tests/browser/browser_test_swipe_gesture.js1274
-rw-r--r--widget/tests/browser/file_ime_state_tests.html48
-rw-r--r--widget/tests/browser/helper_scrollbar_colors.html22
-rw-r--r--widget/tests/browser/helper_swipe_gesture.html20
-rw-r--r--widget/tests/bug586713_window.xhtml50
-rw-r--r--widget/tests/chrome.toml200
-rw-r--r--widget/tests/clipboard_helper.js230
-rw-r--r--widget/tests/empty_window.xhtml4
-rw-r--r--widget/tests/file_bug596600.html4
-rw-r--r--widget/tests/file_ime_state_test_helper.js197
-rw-r--r--widget/tests/file_input_events_on_deactive_window.html5
-rw-r--r--widget/tests/file_secure_input.html1
-rw-r--r--widget/tests/file_test_clipboard.js153
-rw-r--r--widget/tests/file_test_clipboard_asyncGetData.js170
-rw-r--r--widget/tests/file_test_clipboard_asyncSetData.js179
-rw-r--r--widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js616
-rw-r--r--widget/tests/file_test_ime_state_in_text_control_on_reframe.js190
-rw-r--r--widget/tests/file_test_ime_state_on_focus_move.js1588
-rw-r--r--widget/tests/file_test_ime_state_on_input_type_change.js315
-rw-r--r--widget/tests/file_test_ime_state_on_readonly_change.js242
-rw-r--r--widget/tests/gtest/MockWinWidget.cpp77
-rw-r--r--widget/tests/gtest/MockWinWidget.h85
-rw-r--r--widget/tests/gtest/TestTimeConverter.cpp265
-rw-r--r--widget/tests/gtest/TestTouchResampler.cpp941
-rw-r--r--widget/tests/gtest/TestWinHeaderOnlyUtils.cpp37
-rw-r--r--widget/tests/gtest/TestWinMessageLoggingUtils.cpp101
-rw-r--r--widget/tests/gtest/TestWinWindowOcclusionTracker.cpp162
-rw-r--r--widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp402
-rw-r--r--widget/tests/gtest/moz.build27
-rw-r--r--widget/tests/mochitest.toml53
-rw-r--r--widget/tests/moz.build125
-rw-r--r--widget/tests/native_menus_window.xhtml282
-rw-r--r--widget/tests/standalone_native_menu_window.xhtml374
-rw-r--r--widget/tests/system_font_changes.xhtml63
-rw-r--r--widget/tests/taskbar_previews.xhtml116
-rw-r--r--widget/tests/test_AltGr_key_events_in_web_content_on_windows.html106
-rw-r--r--widget/tests/test_actionhint.html114
-rw-r--r--widget/tests/test_alwaysontop_focus.xhtml38
-rw-r--r--widget/tests/test_assign_event_data.html708
-rw-r--r--widget/tests/test_autocapitalize.html62
-rw-r--r--widget/tests/test_bug1123480.xhtml155
-rw-r--r--widget/tests/test_bug343416.xhtml191
-rw-r--r--widget/tests/test_bug428405.xhtml168
-rw-r--r--widget/tests/test_bug429954.xhtml42
-rw-r--r--widget/tests/test_bug444800.xhtml97
-rw-r--r--widget/tests/test_bug466599.xhtml102
-rw-r--r--widget/tests/test_bug478536.xhtml33
-rw-r--r--widget/tests/test_bug485118.xhtml72
-rw-r--r--widget/tests/test_bug517396.xhtml53
-rw-r--r--widget/tests/test_bug522217.xhtml35
-rw-r--r--widget/tests/test_bug538242.xhtml55
-rw-r--r--widget/tests/test_bug565392.html62
-rw-r--r--widget/tests/test_bug586713.xhtml29
-rw-r--r--widget/tests/test_bug593307.xhtml40
-rw-r--r--widget/tests/test_bug596600.xhtml190
-rw-r--r--widget/tests/test_bug673301.xhtml33
-rw-r--r--widget/tests/test_bug760802.xhtml81
-rw-r--r--widget/tests/test_clipboard.html16
-rw-r--r--widget/tests/test_clipboard_asyncGetData.html19
-rw-r--r--widget/tests/test_clipboard_asyncGetData_chrome.html19
-rw-r--r--widget/tests/test_clipboard_asyncSetData.html19
-rw-r--r--widget/tests/test_clipboard_asyncSetData_chrome.html19
-rw-r--r--widget/tests/test_clipboard_cache_chrome.html231
-rw-r--r--widget/tests/test_clipboard_chrome.html16
-rw-r--r--widget/tests/test_clipboard_owner_chrome.html75
-rw-r--r--widget/tests/test_composition_text_querycontent.xhtml34
-rw-r--r--widget/tests/test_contextmenu_by_mouse_on_unix.html105
-rw-r--r--widget/tests/test_ime_state_in_contenteditable_on_readonly_change_in_parent.html72
-rw-r--r--widget/tests/test_ime_state_in_plugin_in_parent.html92
-rw-r--r--widget/tests/test_ime_state_in_text_control_on_reframe_in_parent.html42
-rw-r--r--widget/tests/test_ime_state_on_editable_state_change_in_parent.html263
-rw-r--r--widget/tests/test_ime_state_on_focus_move_in_parent.html88
-rw-r--r--widget/tests/test_ime_state_on_input_type_change_in_parent.html39
-rw-r--r--widget/tests/test_ime_state_on_readonly_change_in_parent.html31
-rw-r--r--widget/tests/test_ime_state_others_in_parent.html153
-rw-r--r--widget/tests/test_input_events_on_deactive_window.xhtml233
-rw-r--r--widget/tests/test_key_event_counts.xhtml90
-rw-r--r--widget/tests/test_keycodes.xhtml5647
-rw-r--r--widget/tests/test_keypress_event_with_alt_on_mac.html106
-rw-r--r--widget/tests/test_mouse_event_with_control_on_mac.html116
-rw-r--r--widget/tests/test_mouse_scroll.xhtml35
-rw-r--r--widget/tests/test_native_key_bindings_mac.html336
-rw-r--r--widget/tests/test_native_menus.xhtml29
-rw-r--r--widget/tests/test_panel_mouse_coords.xhtml78
-rw-r--r--widget/tests/test_picker_no_crash.html30
-rw-r--r--widget/tests/test_platform_colors.xhtml95
-rw-r--r--widget/tests/test_position_on_resize.xhtml90
-rw-r--r--widget/tests/test_secure_input.html141
-rw-r--r--widget/tests/test_sizemode_events.xhtml148
-rw-r--r--widget/tests/test_standalone_native_menu.xhtml29
-rw-r--r--widget/tests/test_surrogate_pair_native_key_handling.xhtml178
-rw-r--r--widget/tests/test_system_font_changes.xhtml28
-rw-r--r--widget/tests/test_system_status_bar.xhtml53
-rw-r--r--widget/tests/test_taskbar_progress.xhtml117
-rw-r--r--widget/tests/test_textScaleFactor_system_font.html139
-rw-r--r--widget/tests/test_transferable_overflow.xhtml155
-rw-r--r--widget/tests/test_wheeltransaction.xhtml27
-rw-r--r--widget/tests/unit/test_macsharingservice.js61
-rw-r--r--widget/tests/unit/test_macwebapputils.js34
-rw-r--r--widget/tests/unit/test_taskbar_legacyjumplistitems.js229
-rw-r--r--widget/tests/unit/xpcshell.toml11
-rw-r--r--widget/tests/window_bug429954.xhtml44
-rw-r--r--widget/tests/window_bug478536.xhtml211
-rw-r--r--widget/tests/window_bug522217.xhtml80
-rw-r--r--widget/tests/window_bug538242.xhtml3
-rw-r--r--widget/tests/window_bug593307_centerscreen.xhtml26
-rw-r--r--widget/tests/window_bug593307_offscreen.xhtml35
-rw-r--r--widget/tests/window_composition_text_querycontent.xhtml10923
-rw-r--r--widget/tests/window_imestate_iframes.html358
-rw-r--r--widget/tests/window_mouse_scroll_win.html1516
-rw-r--r--widget/tests/window_mouse_scroll_win_2.html6
-rw-r--r--widget/tests/window_picker_no_crash_child.html6
-rw-r--r--widget/tests/window_state_windows.xhtml80
-rw-r--r--widget/tests/window_wheeltransaction.xhtml1569
-rw-r--r--widget/uikit/GfxInfo.cpp174
-rw-r--r--widget/uikit/GfxInfo.h75
-rw-r--r--widget/uikit/moz.build24
-rw-r--r--widget/uikit/nsAppShell.h56
-rw-r--r--widget/uikit/nsAppShell.mm243
-rw-r--r--widget/uikit/nsLookAndFeel.h39
-rw-r--r--widget/uikit/nsLookAndFeel.mm361
-rw-r--r--widget/uikit/nsScreenManager.h61
-rw-r--r--widget/uikit/nsScreenManager.mm104
-rw-r--r--widget/uikit/nsWidgetFactory.mm53
-rw-r--r--widget/uikit/nsWindow.h116
-rw-r--r--widget/uikit/nsWindow.mm767
-rw-r--r--widget/windows/AudioSession.cpp346
-rw-r--r--widget/windows/AudioSession.h19
-rw-r--r--widget/windows/CheckInvariantWrapper.h78
-rw-r--r--widget/windows/CompositorWidgetChild.cpp114
-rw-r--r--widget/windows/CompositorWidgetChild.h62
-rw-r--r--widget/windows/CompositorWidgetParent.cpp232
-rw-r--r--widget/windows/CompositorWidgetParent.h92
-rw-r--r--widget/windows/DirectManipulationOwner.cpp722
-rw-r--r--widget/windows/DirectManipulationOwner.h54
-rw-r--r--widget/windows/GfxInfo.cpp2082
-rw-r--r--widget/windows/GfxInfo.h105
-rw-r--r--widget/windows/IEnumFE.cpp139
-rw-r--r--widget/windows/IEnumFE.h88
-rw-r--r--widget/windows/IMMHandler.cpp2408
-rw-r--r--widget/windows/IMMHandler.h427
-rw-r--r--widget/windows/InProcessWinCompositorWidget.cpp368
-rw-r--r--widget/windows/InProcessWinCompositorWidget.h111
-rw-r--r--widget/windows/InputDeviceUtils.cpp61
-rw-r--r--widget/windows/InputDeviceUtils.h26
-rw-r--r--widget/windows/JumpListBuilder.cpp818
-rw-r--r--widget/windows/JumpListBuilder.h102
-rw-r--r--widget/windows/KeyboardLayout.cpp5442
-rw-r--r--widget/windows/KeyboardLayout.h1156
-rw-r--r--widget/windows/LSPAnnotator.cpp135
-rw-r--r--widget/windows/LegacyJumpListBuilder.cpp647
-rw-r--r--widget/windows/LegacyJumpListBuilder.h71
-rw-r--r--widget/windows/LegacyJumpListItem.cpp559
-rw-r--r--widget/windows/LegacyJumpListItem.h133
-rw-r--r--widget/windows/MediaKeysEventSourceFactory.cpp20
-rw-r--r--widget/windows/OSKInputPaneManager.cpp101
-rw-r--r--widget/windows/OSKInputPaneManager.h24
-rw-r--r--widget/windows/OSKTabTipManager.cpp112
-rw-r--r--widget/windows/OSKTabTipManager.h22
-rw-r--r--widget/windows/OSKVRManager.cpp35
-rw-r--r--widget/windows/OSKVRManager.h22
-rw-r--r--widget/windows/PCompositorWidget.ipdl49
-rw-r--r--widget/windows/PlatformWidgetTypes.ipdlh34
-rw-r--r--widget/windows/RemoteBackbuffer.cpp713
-rw-r--r--widget/windows/RemoteBackbuffer.h95
-rw-r--r--widget/windows/ScreenHelperWin.cpp157
-rw-r--r--widget/windows/ScreenHelperWin.h26
-rw-r--r--widget/windows/ShellHeaderOnlyUtils.h183
-rw-r--r--widget/windows/SystemStatusBar.cpp339
-rw-r--r--widget/windows/SystemStatusBar.h33
-rw-r--r--widget/windows/TSFTextStore.cpp7513
-rw-r--r--widget/windows/TSFTextStore.h1161
-rw-r--r--widget/windows/TaskbarPreview.cpp413
-rw-r--r--widget/windows/TaskbarPreview.h132
-rw-r--r--widget/windows/TaskbarPreviewButton.cpp137
-rw-r--r--widget/windows/TaskbarPreviewButton.h47
-rw-r--r--widget/windows/TaskbarTabPreview.cpp344
-rw-r--r--widget/windows/TaskbarTabPreview.h70
-rw-r--r--widget/windows/TaskbarWindowPreview.cpp326
-rw-r--r--widget/windows/TaskbarWindowPreview.h85
-rw-r--r--widget/windows/ToastNotification.cpp915
-rw-r--r--widget/windows/ToastNotification.h83
-rw-r--r--widget/windows/ToastNotificationHandler.cpp1167
-rw-r--r--widget/windows/ToastNotificationHandler.h162
-rw-r--r--widget/windows/ToastNotificationHeaderOnlyUtils.h155
-rw-r--r--widget/windows/UrlmonHeaderOnlyUtils.h76
-rw-r--r--widget/windows/WidgetTraceEvent.cpp121
-rw-r--r--widget/windows/WinCompositorWidget.cpp105
-rw-r--r--widget/windows/WinCompositorWidget.h103
-rw-r--r--widget/windows/WinCompositorWindowThread.cpp294
-rw-r--r--widget/windows/WinCompositorWindowThread.h67
-rw-r--r--widget/windows/WinEventObserver.cpp223
-rw-r--r--widget/windows/WinEventObserver.h115
-rw-r--r--widget/windows/WinHeaderOnlyUtils.h820
-rw-r--r--widget/windows/WinIMEHandler.cpp1081
-rw-r--r--widget/windows/WinIMEHandler.h247
-rw-r--r--widget/windows/WinMessages.h93
-rw-r--r--widget/windows/WinModifierKeyState.h59
-rw-r--r--widget/windows/WinMouseScrollHandler.cpp1634
-rw-r--r--widget/windows/WinMouseScrollHandler.h567
-rw-r--r--widget/windows/WinPointerEvents.cpp181
-rw-r--r--widget/windows/WinPointerEvents.h75
-rw-r--r--widget/windows/WinRegistry.cpp325
-rw-r--r--widget/windows/WinRegistry.h235
-rw-r--r--widget/windows/WinTaskbar.cpp493
-rw-r--r--widget/windows/WinTaskbar.h46
-rw-r--r--widget/windows/WinTextEventDispatcherListener.cpp68
-rw-r--r--widget/windows/WinTextEventDispatcherListener.h50
-rw-r--r--widget/windows/WinUtils.cpp2107
-rw-r--r--widget/windows/WinUtils.h684
-rw-r--r--widget/windows/WinWindowOcclusionTracker.cpp1471
-rw-r--r--widget/windows/WinWindowOcclusionTracker.h333
-rw-r--r--widget/windows/WindowHook.cpp113
-rw-r--r--widget/windows/WindowHook.h76
-rw-r--r--widget/windows/WindowsConsole.cpp53
-rw-r--r--widget/windows/WindowsConsole.h16
-rw-r--r--widget/windows/WindowsEMF.cpp94
-rw-r--r--widget/windows/WindowsEMF.h106
-rw-r--r--widget/windows/WindowsEventLog.h99
-rw-r--r--widget/windows/WindowsSMTCProvider.cpp716
-rw-r--r--widget/windows/WindowsSMTCProvider.h128
-rw-r--r--widget/windows/WindowsUIUtils.cpp809
-rw-r--r--widget/windows/WindowsUIUtils.h50
-rw-r--r--widget/windows/components.conf220
-rw-r--r--widget/windows/docs/blocklist.rst347
-rw-r--r--widget/windows/docs/index.rst9
-rw-r--r--widget/windows/docs/windows-pointing-device/apple_vision.jpgbin0 -> 8013 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/apple_vision_user.webpbin0 -> 18126 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/index.rst1384
-rw-r--r--widget/windows/docs/windows-pointing-device/mouse.jpgbin0 -> 74248 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/touch_media_queries.pngbin0 -> 45966 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/touchpad.jpgbin0 -> 8055 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/touchscreen.jpgbin0 -> 14494 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/trackpoint.jpgbin0 -> 7494 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/vrcontroller.jpgbin0 -> 79483 bytes
-rw-r--r--widget/windows/docs/windows-pointing-device/wacom_tablet.pngbin0 -> 509107 bytes
-rw-r--r--widget/windows/filedialog/PWinFileDialog.ipdl32
-rw-r--r--widget/windows/filedialog/WinFileDialogChild.cpp110
-rw-r--r--widget/windows/filedialog/WinFileDialogChild.h52
-rw-r--r--widget/windows/filedialog/WinFileDialogCommands.cpp460
-rw-r--r--widget/windows/filedialog/WinFileDialogCommands.h74
-rw-r--r--widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh49
-rw-r--r--widget/windows/filedialog/WinFileDialogParent.cpp94
-rw-r--r--widget/windows/filedialog/WinFileDialogParent.h90
-rw-r--r--widget/windows/filedialog/moz.build27
-rw-r--r--widget/windows/metrics.yaml58
-rw-r--r--widget/windows/moz.build213
-rw-r--r--widget/windows/nsAppShell.cpp1000
-rw-r--r--widget/windows/nsAppShell.h64
-rw-r--r--widget/windows/nsBidiKeyboard.cpp169
-rw-r--r--widget/windows/nsBidiKeyboard.h34
-rw-r--r--widget/windows/nsClipboard.cpp1472
-rw-r--r--widget/windows/nsClipboard.h113
-rw-r--r--widget/windows/nsColorPicker.cpp202
-rw-r--r--widget/windows/nsColorPicker.h55
-rw-r--r--widget/windows/nsDataObj.cpp2276
-rw-r--r--widget/windows/nsDataObj.h315
-rw-r--r--widget/windows/nsDataObjCollection.cpp370
-rw-r--r--widget/windows/nsDataObjCollection.h92
-rw-r--r--widget/windows/nsDeviceContextSpecWin.cpp680
-rw-r--r--widget/windows/nsDeviceContextSpecWin.h98
-rw-r--r--widget/windows/nsDragService.cpp664
-rw-r--r--widget/windows/nsDragService.h67
-rw-r--r--widget/windows/nsFilePicker.cpp1078
-rw-r--r--widget/windows/nsFilePicker.h140
-rw-r--r--widget/windows/nsLookAndFeel.cpp916
-rw-r--r--widget/windows/nsLookAndFeel.h124
-rw-r--r--widget/windows/nsNativeDragSource.cpp98
-rw-r--r--widget/windows/nsNativeDragSource.h68
-rw-r--r--widget/windows/nsNativeDragTarget.cpp472
-rw-r--r--widget/windows/nsNativeDragTarget.h103
-rw-r--r--widget/windows/nsNativeThemeWin.cpp1975
-rw-r--r--widget/windows/nsNativeThemeWin.h166
-rw-r--r--widget/windows/nsPrintDialogUtil.cpp360
-rw-r--r--widget/windows/nsPrintDialogUtil.h11
-rw-r--r--widget/windows/nsPrintDialogWin.cpp147
-rw-r--r--widget/windows/nsPrintDialogWin.h39
-rw-r--r--widget/windows/nsPrintSettingsServiceWin.cpp127
-rw-r--r--widget/windows/nsPrintSettingsServiceWin.h29
-rw-r--r--widget/windows/nsPrintSettingsWin.cpp477
-rw-r--r--widget/windows/nsPrintSettingsWin.h62
-rw-r--r--widget/windows/nsPrinterWin.cpp521
-rw-r--r--widget/windows/nsPrinterWin.h53
-rw-r--r--widget/windows/nsSharePicker.cpp81
-rw-r--r--widget/windows/nsSharePicker.h29
-rw-r--r--widget/windows/nsSound.cpp331
-rw-r--r--widget/windows/nsSound.h47
-rw-r--r--widget/windows/nsToolkit.cpp69
-rw-r--r--widget/windows/nsToolkit.h47
-rw-r--r--widget/windows/nsUXThemeConstants.h256
-rw-r--r--widget/windows/nsUXThemeData.cpp98
-rw-r--r--widget/windows/nsUXThemeData.h69
-rw-r--r--widget/windows/nsUserIdleServiceWin.cpp20
-rw-r--r--widget/windows/nsUserIdleServiceWin.h48
-rw-r--r--widget/windows/nsWidgetFactory.cpp60
-rw-r--r--widget/windows/nsWidgetFactory.h21
-rw-r--r--widget/windows/nsWinGesture.cpp388
-rw-r--r--widget/windows/nsWinGesture.h91
-rw-r--r--widget/windows/nsWindow.cpp9000
-rw-r--r--widget/windows/nsWindow.h905
-rw-r--r--widget/windows/nsWindowDbg.cpp1612
-rw-r--r--widget/windows/nsWindowDbg.h153
-rw-r--r--widget/windows/nsWindowDefs.h119
-rw-r--r--widget/windows/nsWindowGfx.cpp718
-rw-r--r--widget/windows/nsWindowGfx.h35
-rw-r--r--widget/windows/nsWindowLoggedMessages.cpp307
-rw-r--r--widget/windows/nsWindowLoggedMessages.h26
-rw-r--r--widget/windows/nsWindowTaskbarConcealer.cpp384
-rw-r--r--widget/windows/nsWindowTaskbarConcealer.h53
-rw-r--r--widget/windows/nsdefs.h58
-rw-r--r--widget/windows/res/aliasb.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/cell.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/col_resize.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/copy.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/grab.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/grabbing.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/none.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/row_resize.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/select.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/vertical_text.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/zoom_in.curbin0 -> 326 bytes
-rw-r--r--widget/windows/res/zoom_out.curbin0 -> 326 bytes
-rw-r--r--widget/windows/resource.h16
-rw-r--r--widget/windows/tests/TestUriValidation.cpp135
-rw-r--r--widget/windows/tests/TestUrisToValidate.h471
-rw-r--r--widget/windows/tests/gtest/TestJumpListBuilder.cpp823
-rw-r--r--widget/windows/tests/gtest/TestWinDND.cpp728
-rw-r--r--widget/windows/tests/gtest/moz.build19
-rw-r--r--widget/windows/tests/moz.build33
-rw-r--r--widget/windows/tests/unit/test_windows_alert_service.js667
-rw-r--r--widget/windows/tests/unit/xpcshell.toml3
-rw-r--r--widget/windows/touchinjection_sdk80.h171
-rw-r--r--widget/windows/widget.rc30
-rw-r--r--widget/x11/keysym2ucs.c866
-rw-r--r--widget/x11/keysym2ucs.h28
-rw-r--r--widget/x11/moz.build16
1059 files changed, 308658 insertions, 0 deletions
diff --git a/widget/BasicEvents.h b/widget/BasicEvents.h
new file mode 100644
index 0000000000..b3707f1cf4
--- /dev/null
+++ b/widget/BasicEvents.h
@@ -0,0 +1,1335 @@
+/* -*- 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/. */
+
+#ifndef mozilla_BasicEvents_h__
+#define mozilla_BasicEvents_h__
+
+#include <stdint.h>
+#include <type_traits>
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "nsCOMPtr.h"
+#include "nsAtom.h"
+#include "nsISupportsImpl.h"
+#include "nsIWidget.h"
+#include "nsString.h"
+#include "Units.h"
+
+#ifdef DEBUG
+# include "nsXULAppAPI.h"
+#endif // #ifdef DEBUG
+
+class nsIPrincipal;
+
+namespace IPC {
+template <typename T>
+struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+
+class EventTargetChainItem;
+
+enum class CrossProcessForwarding {
+ // eStop prevents the event to be sent to remote process.
+ eStop,
+ // eAllow keeps current state of the event whether it's sent to remote
+ // process. In other words, eAllow does NOT mean that making the event
+ // sent to remote process when IsCrossProcessForwardingStopped() returns
+ // true.
+ eAllow,
+};
+
+/******************************************************************************
+ * mozilla::BaseEventFlags
+ *
+ * BaseEventFlags must be a POD struct for safe to use memcpy (including
+ * in ParamTraits<BaseEventFlags>). So don't make virtual methods, constructor,
+ * destructor and operators.
+ * This is necessary for VC which is NOT C++0x compiler.
+ ******************************************************************************/
+
+struct BaseEventFlags {
+ public:
+ // If mIsTrusted is true, the event is a trusted event. Otherwise, it's
+ // an untrusted event.
+ bool mIsTrusted : 1;
+ // If mInBubblingPhase is true, the event is in bubbling phase or target
+ // phase.
+ bool mInBubblingPhase : 1;
+ // If mInCapturePhase is true, the event is in capture phase or target phase.
+ bool mInCapturePhase : 1;
+ // If mInTargetPhase is true, the event is in target phase.
+ bool mInTargetPhase : 1;
+ // If mInSystemGroup is true, the event is being dispatched in system group.
+ bool mInSystemGroup : 1;
+ // If mCancelable is true, the event can be consumed. I.e., calling
+ // dom::Event::PreventDefault() can prevent the default action.
+ bool mCancelable : 1;
+ // If mBubbles is true, the event can bubble. Otherwise, cannot be handled
+ // in bubbling phase.
+ bool mBubbles : 1;
+ // If mPropagationStopped is true, dom::Event::StopPropagation() or
+ // dom::Event::StopImmediatePropagation() has been called.
+ bool mPropagationStopped : 1;
+ // If mImmediatePropagationStopped is true,
+ // dom::Event::StopImmediatePropagation() has been called.
+ // Note that mPropagationStopped must be true when this is true.
+ bool mImmediatePropagationStopped : 1;
+ // If mDefaultPrevented is true, the event has been consumed.
+ // E.g., dom::Event::PreventDefault() has been called or
+ // the default action has been performed.
+ bool mDefaultPrevented : 1;
+ // If mDefaultPreventedByContent is true, the event has been
+ // consumed by content.
+ // Note that mDefaultPrevented must be true when this is true.
+ bool mDefaultPreventedByContent : 1;
+ // If mDefaultPreventedByChrome is true, the event has been
+ // consumed by chrome.
+ // Note that mDefaultPrevented must be true when this is true.
+ bool mDefaultPreventedByChrome : 1;
+ // mMultipleActionsPrevented may be used when default handling don't want to
+ // be prevented, but only one of the event targets should handle the event.
+ // For example, when a <label> element is in another <label> element and
+ // the first <label> element is clicked, that one may set this true.
+ // Then, the second <label> element won't handle the event.
+ bool mMultipleActionsPrevented : 1;
+ // If mIsBeingDispatched is true, the DOM event created from the event is
+ // dispatching into the DOM tree and not completed.
+ bool mIsBeingDispatched : 1;
+ // If mDispatchedAtLeastOnce is true, the event has been dispatched
+ // as a DOM event and the dispatch has been completed in the process.
+ // So, this is false even if the event has already been dispatched
+ // in another process.
+ bool mDispatchedAtLeastOnce : 1;
+ // If mIsSynthesizedForTests is true, the event has been synthesized for
+ // automated tests or something hacky approach of an add-on.
+ bool mIsSynthesizedForTests : 1;
+ // If mExceptionWasRaised is true, one of the event handlers has raised an
+ // exception.
+ bool mExceptionWasRaised : 1;
+ // If mRetargetToNonNativeAnonymous is true and the target is in a non-native
+ // native anonymous subtree, the event target is set to mOriginalTarget.
+ bool mRetargetToNonNativeAnonymous : 1;
+ // If mNoContentDispatch is true, the event is never dispatched to the
+ // event handlers which are added to the contents, onfoo attributes and
+ // properties. Note that this flag is ignored when
+ // EventChainPreVisitor::mForceContentDispatch is set true. For exapmle,
+ // window and document object sets it true. Therefore, web applications
+ // can handle the event if they add event listeners to the window or the
+ // document.
+ // XXX This is an ancient and broken feature, don't use this for new bug
+ // as far as possible.
+ bool mNoContentDispatch : 1;
+ // If mOnlyChromeDispatch is true, the event is dispatched to only chrome.
+ bool mOnlyChromeDispatch : 1;
+ // Indicates if the key combination is reserved by chrome. This is set by
+ // MarkAsReservedByChrome().
+ bool mIsReservedByChrome : 1;
+ // If mOnlySystemGroupDispatchInContent is true, event listeners added to
+ // the default group for non-chrome EventTarget won't be called.
+ // Be aware, if this is true, EventDispatcher needs to check if each event
+ // listener is added to chrome node, so, don't set this to true for the
+ // events which are fired a lot of times like eMouseMove.
+ bool mOnlySystemGroupDispatchInContent : 1;
+ // If mOnlySystemGroupDispatch is true, the event will be dispatched only to
+ // event listeners added in the system group.
+ bool mOnlySystemGroupDispatch : 1;
+ // The event's action will be handled by APZ. The main thread should not
+ // perform its associated action.
+ bool mHandledByAPZ : 1;
+ // True if the event is currently being handled by an event listener that
+ // was registered as a passive listener.
+ bool mInPassiveListener : 1;
+ // If mComposed is true, the event fired by nodes in shadow DOM can cross the
+ // boundary of shadow DOM and light DOM.
+ bool mComposed : 1;
+ // Similar to mComposed. Set it to true to allow events cross the boundary
+ // between native non-anonymous content and native anonymouse content
+ bool mComposedInNativeAnonymousContent : 1;
+ // Set to true for events which are suppressed or delayed so that later a
+ // DelayedEvent of it is dispatched. This is used when parent side process
+ // the key event after content side, and may drop the event if the event
+ // was suppressed or delayed in contents side.
+ // It is also set to true for the events (in a DelayedInputEvent), which will
+ // be dispatched afterwards.
+ bool mIsSuppressedOrDelayed : 1;
+ // Certain mouse events can be marked as positionless to return 0 from
+ // coordinate related getters.
+ bool mIsPositionless : 1;
+
+ // Flags managing state of propagation between processes.
+ // Note the the following flags shouldn't be referred directly. Use utility
+ // methods instead.
+
+ // If mNoRemoteProcessDispatch is true, the event is not allowed to be sent
+ // to remote process.
+ bool mNoRemoteProcessDispatch : 1;
+ // If mWantReplyFromContentProcess is true, the event will be redispatched
+ // in the parent process after the content process has handled it. Useful
+ // for when the parent process need the know first how the event was used
+ // by content before handling it itself.
+ bool mWantReplyFromContentProcess : 1;
+ // If mPostedToRemoteProcess is true, the event has been posted to the
+ // remote process (but it's not handled yet if it's not a duplicated event
+ // instance).
+ bool mPostedToRemoteProcess : 1;
+ // If mCameFromAnotherProcess is true, the event came from another process.
+ bool mCameFromAnotherProcess : 1;
+
+ /**
+ * Helper methods for methods of DOM Event.
+ */
+ inline void StopPropagation() { mPropagationStopped = true; }
+ inline void StopImmediatePropagation() {
+ StopPropagation();
+ mImmediatePropagationStopped = true;
+ }
+ inline void PreventDefault(bool aCalledByDefaultHandler = true) {
+ if (!mCancelable) {
+ return;
+ }
+ mDefaultPrevented = true;
+ // Note that even if preventDefault() has already been called by chrome,
+ // a call of preventDefault() by content needs to overwrite
+ // mDefaultPreventedByContent to true because in such case, defaultPrevented
+ // must be true when web apps check it after they call preventDefault().
+ if (aCalledByDefaultHandler) {
+ StopCrossProcessForwarding();
+ mDefaultPreventedByChrome = true;
+ } else {
+ mDefaultPreventedByContent = true;
+ }
+ }
+ // This should be used only before dispatching events into the DOM tree.
+ inline void PreventDefaultBeforeDispatch(
+ CrossProcessForwarding aCrossProcessForwarding) {
+ if (!mCancelable) {
+ return;
+ }
+ mDefaultPrevented = true;
+ if (aCrossProcessForwarding == CrossProcessForwarding::eStop) {
+ StopCrossProcessForwarding();
+ }
+ }
+ inline bool DefaultPrevented() const { return mDefaultPrevented; }
+ inline bool DefaultPreventedByContent() const {
+ MOZ_ASSERT(!mDefaultPreventedByContent || DefaultPrevented());
+ return mDefaultPreventedByContent;
+ }
+ inline bool IsTrusted() const { return mIsTrusted; }
+ inline bool PropagationStopped() const { return mPropagationStopped; }
+
+ // Helper methods to access flags managing state of propagation between
+ // processes.
+
+ /**
+ * Prevent to be dispatched to remote process.
+ */
+ inline void StopCrossProcessForwarding() {
+ MOZ_ASSERT(!mPostedToRemoteProcess);
+ mNoRemoteProcessDispatch = true;
+ mWantReplyFromContentProcess = false;
+ }
+ /**
+ * Return true if the event shouldn't be dispatched to remote process.
+ */
+ inline bool IsCrossProcessForwardingStopped() const {
+ return mNoRemoteProcessDispatch;
+ }
+ /**
+ * Mark the event as waiting reply from remote process.
+ * If the caller needs to win other keyboard event handlers in chrome,
+ * the caller should call StopPropagation() too.
+ * Otherwise, if the caller just needs to know if the event is consumed by
+ * either content or chrome, it should just call this because the event
+ * may be reserved by chrome and it needs to be dispatched into the DOM
+ * tree in chrome for checking if it's reserved before being sent to any
+ * remote processes.
+ */
+ inline void MarkAsWaitingReplyFromRemoteProcess() {
+ MOZ_ASSERT(!mPostedToRemoteProcess);
+ mNoRemoteProcessDispatch = false;
+ mWantReplyFromContentProcess = true;
+ }
+ /**
+ * Reset "waiting reply from remote process" state. This is useful when
+ * you dispatch a copy of an event coming from different process.
+ */
+ inline void ResetWaitingReplyFromRemoteProcessState() {
+ if (IsWaitingReplyFromRemoteProcess()) {
+ // FYI: mWantReplyFromContentProcess is also used for indicating
+ // "handled in remote process" state. Therefore, only when
+ // IsWaitingReplyFromRemoteProcess() returns true, this should
+ // reset the flag.
+ mWantReplyFromContentProcess = false;
+ }
+ }
+ /**
+ * Return true if the event handler should wait reply event. I.e., if this
+ * returns true, any event handler should do nothing with the event.
+ */
+ inline bool IsWaitingReplyFromRemoteProcess() const {
+ return !mNoRemoteProcessDispatch && mWantReplyFromContentProcess;
+ }
+ /**
+ * Mark the event as already handled in the remote process. This should be
+ * called when initializing reply events.
+ */
+ inline void MarkAsHandledInRemoteProcess() {
+ mNoRemoteProcessDispatch = true;
+ mWantReplyFromContentProcess = true;
+ mPostedToRemoteProcess = false;
+ }
+ /**
+ * Return true if the event has already been handled in the remote process.
+ */
+ inline bool IsHandledInRemoteProcess() const {
+ return mNoRemoteProcessDispatch && mWantReplyFromContentProcess;
+ }
+ /**
+ * Return true if the event should be sent back to its parent process.
+ */
+ inline bool WantReplyFromContentProcess() const {
+ MOZ_ASSERT(!XRE_IsParentProcess());
+ return IsWaitingReplyFromRemoteProcess();
+ }
+ /**
+ * Mark the event has already posted to a remote process.
+ */
+ inline void MarkAsPostedToRemoteProcess() {
+ MOZ_ASSERT(!IsCrossProcessForwardingStopped());
+ mPostedToRemoteProcess = true;
+ }
+ /**
+ * Reset the cross process dispatching state. This should be used when a
+ * process receives the event because the state is in the sender.
+ */
+ inline void ResetCrossProcessDispatchingState() {
+ MOZ_ASSERT(!IsCrossProcessForwardingStopped());
+ mPostedToRemoteProcess = false;
+ // Ignore propagation state in the remote process if it's marked as
+ // "waiting reply from remote process" because the process needs to
+ // stop propagation in the process until receiving a reply event.
+ // Note that the propagation stopped flag is important for the reply event
+ // handler in the main process because it's used for making whether it's
+ // ignored by the remote process or not.
+ if (!XRE_IsParentProcess() && IsWaitingReplyFromRemoteProcess()) {
+ mPropagationStopped = mImmediatePropagationStopped = false;
+ }
+ // mDispatchedAtLeastOnce indicates the state in current process.
+ mDispatchedAtLeastOnce = false;
+ }
+ /**
+ * Return true if the event has been posted to a remote process.
+ * Note that MarkAsPostedToRemoteProcess() is called by
+ * ParamTraits<mozilla::WidgetEvent>. Therefore, it *might* be possible
+ * that posting the event failed even if this returns true. But that must
+ * really rare. If that'd be problem for you, you should unmark this in
+ * BrowserParent or somewhere.
+ */
+ inline bool HasBeenPostedToRemoteProcess() const {
+ return mPostedToRemoteProcess;
+ }
+ /**
+ * Return true if the event came from another process.
+ */
+ inline bool CameFromAnotherProcess() const { return mCameFromAnotherProcess; }
+ /**
+ * Mark the event as coming from another process.
+ */
+ inline void MarkAsComingFromAnotherProcess() {
+ mCameFromAnotherProcess = true;
+ }
+ /**
+ * Mark the event is reserved by chrome. I.e., shouldn't be dispatched to
+ * content because it shouldn't be cancelable.
+ */
+ inline void MarkAsReservedByChrome() {
+ MOZ_ASSERT(!mPostedToRemoteProcess);
+ mIsReservedByChrome = true;
+ // For reserved commands (such as Open New Tab), we don't need to wait for
+ // the content to answer, neither to give a chance for content to override
+ // its behavior.
+ StopCrossProcessForwarding();
+ // If the event is reserved by chrome, we shouldn't expose the event to
+ // web contents because such events shouldn't be cancelable. So, it's not
+ // good behavior to fire such events but to ignore the defaultPrevented
+ // attribute value in chrome.
+ mOnlySystemGroupDispatchInContent = true;
+ }
+ /**
+ * Return true if the event is reserved by chrome.
+ */
+ inline bool IsReservedByChrome() const {
+ MOZ_ASSERT(!mIsReservedByChrome || (IsCrossProcessForwardingStopped() &&
+ mOnlySystemGroupDispatchInContent));
+ return mIsReservedByChrome;
+ }
+
+ inline void Clear() { SetRawFlags(0); }
+ // Get if either the instance's bit or the aOther's bit is true, the
+ // instance's bit becomes true. In other words, this works like:
+ // eventFlags |= aOther;
+ inline void Union(const BaseEventFlags& aOther) {
+ RawFlags rawFlags = GetRawFlags() | aOther.GetRawFlags();
+ SetRawFlags(rawFlags);
+ }
+
+ private:
+ typedef uint64_t RawFlags;
+
+ inline void SetRawFlags(RawFlags aRawFlags) {
+ static_assert(sizeof(BaseEventFlags) <= sizeof(RawFlags),
+ "mozilla::EventFlags must not be bigger than the RawFlags");
+ memcpy(this, &aRawFlags, sizeof(BaseEventFlags));
+ }
+ inline RawFlags GetRawFlags() const {
+ RawFlags result = 0;
+ memcpy(&result, this, sizeof(BaseEventFlags));
+ return result;
+ }
+};
+
+/******************************************************************************
+ * mozilla::EventFlags
+ ******************************************************************************/
+
+struct EventFlags : public BaseEventFlags {
+ EventFlags() { Clear(); }
+};
+
+/******************************************************************************
+ * mozilla::WidgetEventTime
+ ******************************************************************************/
+
+class WidgetEventTime {
+ public:
+ // Timestamp when the message was created.
+ TimeStamp mTimeStamp;
+
+ WidgetEventTime() : mTimeStamp(TimeStamp::Now()) {}
+
+ explicit WidgetEventTime(const WidgetEventTime* aTime)
+ : mTimeStamp(aTime ? aTime->mTimeStamp : TimeStamp::Now()) {
+ MOZ_ASSERT(aTime != this);
+ MOZ_ASSERT_IF(aTime, !aTime->mTimeStamp.IsNull());
+ }
+
+ explicit WidgetEventTime(TimeStamp aTimeStamp) : mTimeStamp(aTimeStamp) {}
+
+ void AssignEventTime(const WidgetEventTime& aOther) {
+ mTimeStamp = aOther.mTimeStamp;
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetEvent
+ ******************************************************************************/
+
+class WidgetEvent : public WidgetEventTime {
+ private:
+ void SetDefaultCancelableAndBubbles() {
+ switch (mClass) {
+ case eEditorInputEventClass:
+ mFlags.mCancelable = false;
+ mFlags.mBubbles = mFlags.mIsTrusted;
+ break;
+ case eMouseEventClass:
+ mFlags.mCancelable =
+ (mMessage != eMouseEnter && mMessage != eMouseLeave);
+ mFlags.mBubbles = (mMessage != eMouseEnter && mMessage != eMouseLeave);
+ break;
+ case ePointerEventClass:
+ mFlags.mCancelable =
+ (mMessage != ePointerEnter && mMessage != ePointerLeave &&
+ mMessage != ePointerCancel && mMessage != ePointerGotCapture &&
+ mMessage != ePointerLostCapture);
+ mFlags.mBubbles =
+ (mMessage != ePointerEnter && mMessage != ePointerLeave);
+ break;
+ case eDragEventClass:
+ mFlags.mCancelable = (mMessage != eDragExit && mMessage != eDragLeave &&
+ mMessage != eDragEnd);
+ mFlags.mBubbles = true;
+ break;
+ case eSMILTimeEventClass:
+ mFlags.mCancelable = false;
+ mFlags.mBubbles = false;
+ break;
+ case eTransitionEventClass:
+ case eAnimationEventClass:
+ mFlags.mCancelable = false;
+ mFlags.mBubbles = true;
+ break;
+ case eCompositionEventClass:
+ // XXX compositionstart is cancelable in draft of DOM3 Events.
+ // However, it doesn't make sense for us, we cannot cancel
+ // composition when we send compositionstart event.
+ mFlags.mCancelable = false;
+ mFlags.mBubbles = true;
+ break;
+ default:
+ if (mMessage == eResize || mMessage == eMozVisualResize ||
+ mMessage == eMozVisualScroll || mMessage == eEditorInput ||
+ mMessage == eFormSelect) {
+ mFlags.mCancelable = false;
+ } else {
+ mFlags.mCancelable = true;
+ }
+ mFlags.mBubbles = true;
+ break;
+ }
+ }
+
+ protected:
+ WidgetEvent(bool aIsTrusted, EventMessage aMessage,
+ EventClassID aEventClassID,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEventTime(aTime),
+ mClass(aEventClassID),
+ mMessage(aMessage),
+ mRefPoint(0, 0),
+ mLastRefPoint(0, 0),
+ mFocusSequenceNumber(0),
+ mSpecifiedEventType(nullptr),
+ mPath(nullptr),
+ mLayersId(layers::LayersId{0}) {
+ MOZ_COUNT_CTOR(WidgetEvent);
+ mFlags.Clear();
+ mFlags.mIsTrusted = aIsTrusted;
+ SetDefaultCancelableAndBubbles();
+ SetDefaultComposed();
+ SetDefaultComposedInNativeAnonymousContent();
+ }
+
+ WidgetEvent() : mPath(nullptr) { MOZ_COUNT_CTOR(WidgetEvent); }
+
+ public:
+ WidgetEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEvent(aIsTrusted, aMessage, eBasicEventClass, aTime) {}
+
+ MOZ_COUNTED_DTOR_VIRTUAL(WidgetEvent)
+
+ WidgetEvent(const WidgetEvent& aOther) : WidgetEventTime(aOther) {
+ MOZ_COUNT_CTOR(WidgetEvent);
+ *this = aOther;
+ }
+ WidgetEvent& operator=(const WidgetEvent& aOther) = default;
+
+ WidgetEvent(WidgetEvent&& aOther)
+ : WidgetEventTime(std::move(aOther)),
+ mClass(aOther.mClass),
+ mMessage(aOther.mMessage),
+ mRefPoint(std::move(aOther.mRefPoint)),
+ mLastRefPoint(std::move(aOther.mLastRefPoint)),
+ mFocusSequenceNumber(aOther.mFocusSequenceNumber),
+ mFlags(std::move(aOther.mFlags)),
+ mSpecifiedEventType(std::move(aOther.mSpecifiedEventType)),
+ mSpecifiedEventTypeString(std::move(aOther.mSpecifiedEventTypeString)),
+ mTarget(std::move(aOther.mTarget)),
+ mCurrentTarget(std::move(aOther.mCurrentTarget)),
+ mOriginalTarget(std::move(aOther.mOriginalTarget)),
+ mRelatedTarget(std::move(aOther.mRelatedTarget)),
+ mOriginalRelatedTarget(std::move(aOther.mOriginalRelatedTarget)),
+ mPath(std::move(aOther.mPath)) {
+ MOZ_COUNT_CTOR(WidgetEvent);
+ }
+ WidgetEvent& operator=(WidgetEvent&& aOther) = default;
+
+ virtual WidgetEvent* Duplicate() const {
+ MOZ_ASSERT(mClass == eBasicEventClass,
+ "Duplicate() must be overridden by sub class");
+ WidgetEvent* result = new WidgetEvent(false, mMessage, this);
+ result->AssignEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ EventClassID mClass;
+ EventMessage mMessage;
+ // Relative to the widget of the event, or if there is no widget then it is
+ // in screen coordinates. Not modified by layout code.
+ // This is in visual coordinates, i.e. the correct RelativeTo value that
+ // expresses what this is relative to is `{viewportFrame, Visual}`, where
+ // `viewportFrame` is the viewport frame of the widget's root document.
+ LayoutDeviceIntPoint mRefPoint;
+ // The previous mRefPoint, if known, used to calculate mouse movement deltas.
+ LayoutDeviceIntPoint mLastRefPoint;
+ // The sequence number of the last potentially focus changing event handled
+ // by APZ. This is used to track when that event has been processed by
+ // content, and focus can be reconfirmed for async keyboard scrolling.
+ uint64_t mFocusSequenceNumber;
+ // See BaseEventFlags definition for the detail.
+ BaseEventFlags mFlags;
+
+ // If JS creates an event with unknown event type or known event type but
+ // for different event interface, the event type is stored to this.
+ // NOTE: This is always used if the instance is a WidgetCommandEvent instance
+ // or "input" event is dispatched with dom::Event class.
+ RefPtr<nsAtom> mSpecifiedEventType;
+
+ // nsAtom isn't available on non-main thread due to unsafe. Therefore,
+ // mSpecifiedEventTypeString is used instead of mSpecifiedEventType if
+ // the event is created in non-main thread.
+ nsString mSpecifiedEventTypeString;
+
+ // Event targets, needed by DOM Events
+ // Note that when you need event target for DOM event, you should use
+ // Get*DOMEventTarget() instead of accessing these members directly.
+ nsCOMPtr<dom::EventTarget> mTarget;
+ nsCOMPtr<dom::EventTarget> mCurrentTarget;
+ nsCOMPtr<dom::EventTarget> mOriginalTarget;
+
+ /// The possible related target
+ nsCOMPtr<dom::EventTarget> mRelatedTarget;
+ nsCOMPtr<dom::EventTarget> mOriginalRelatedTarget;
+
+ nsTArray<EventTargetChainItem>* mPath;
+
+ // The LayersId of the content process that this event should be
+ // dispatched to. This field is only used in the chrome process
+ // and doesn't get remoted to child processes.
+ layers::LayersId mLayersId;
+
+ dom::EventTarget* GetDOMEventTarget() const;
+ dom::EventTarget* GetCurrentDOMEventTarget() const;
+ dom::EventTarget* GetOriginalDOMEventTarget() const;
+
+ void AssignEventData(const WidgetEvent& aEvent, bool aCopyTargets) {
+ // mClass should be initialized with the constructor.
+ // mMessage should be initialized with the constructor.
+ mRefPoint = aEvent.mRefPoint;
+ // mLastRefPoint doesn't need to be copied.
+ mFocusSequenceNumber = aEvent.mFocusSequenceNumber;
+ // mLayersId intentionally not copied, since it's not used within content
+ AssignEventTime(aEvent);
+ // mFlags should be copied manually if it's necessary.
+ mSpecifiedEventType = aEvent.mSpecifiedEventType;
+ // mSpecifiedEventTypeString should be copied manually if it's necessary.
+ mTarget = aCopyTargets ? aEvent.mTarget : nullptr;
+ mCurrentTarget = aCopyTargets ? aEvent.mCurrentTarget : nullptr;
+ mOriginalTarget = aCopyTargets ? aEvent.mOriginalTarget : nullptr;
+ mRelatedTarget = aCopyTargets ? aEvent.mRelatedTarget : nullptr;
+ mOriginalRelatedTarget =
+ aCopyTargets ? aEvent.mOriginalRelatedTarget : nullptr;
+ }
+
+ /**
+ * Helper methods for methods of DOM Event.
+ */
+ void StopPropagation() { mFlags.StopPropagation(); }
+ void StopImmediatePropagation() { mFlags.StopImmediatePropagation(); }
+ void PreventDefault(bool aCalledByDefaultHandler = true,
+ nsIPrincipal* aPrincipal = nullptr);
+
+ void PreventDefaultBeforeDispatch(
+ CrossProcessForwarding aCrossProcessForwarding) {
+ mFlags.PreventDefaultBeforeDispatch(aCrossProcessForwarding);
+ }
+ bool DefaultPrevented() const { return mFlags.DefaultPrevented(); }
+ bool DefaultPreventedByContent() const {
+ return mFlags.DefaultPreventedByContent();
+ }
+ bool IsTrusted() const { return mFlags.IsTrusted(); }
+ bool PropagationStopped() const { return mFlags.PropagationStopped(); }
+
+ /**
+ * Prevent to be dispatched to remote process.
+ */
+ inline void StopCrossProcessForwarding() {
+ mFlags.StopCrossProcessForwarding();
+ }
+ /**
+ * Return true if the event shouldn't be dispatched to remote process.
+ */
+ inline bool IsCrossProcessForwardingStopped() const {
+ return mFlags.IsCrossProcessForwardingStopped();
+ }
+ /**
+ * Mark the event as waiting reply from remote process.
+ * Note that this also stops immediate propagation in current process.
+ */
+ inline void MarkAsWaitingReplyFromRemoteProcess() {
+ mFlags.MarkAsWaitingReplyFromRemoteProcess();
+ }
+ /**
+ * Reset "waiting reply from remote process" state. This is useful when
+ * you dispatch a copy of an event coming from different process.
+ */
+ inline void ResetWaitingReplyFromRemoteProcessState() {
+ mFlags.ResetWaitingReplyFromRemoteProcessState();
+ }
+ /**
+ * Return true if the event handler should wait reply event. I.e., if this
+ * returns true, any event handler should do nothing with the event.
+ */
+ inline bool IsWaitingReplyFromRemoteProcess() const {
+ return mFlags.IsWaitingReplyFromRemoteProcess();
+ }
+ /**
+ * Mark the event as already handled in the remote process. This should be
+ * called when initializing reply events.
+ */
+ inline void MarkAsHandledInRemoteProcess() {
+ mFlags.MarkAsHandledInRemoteProcess();
+ }
+ /**
+ * Return true if the event has already been handled in the remote process.
+ * I.e., if this returns true, the event is a reply event.
+ */
+ inline bool IsHandledInRemoteProcess() const {
+ return mFlags.IsHandledInRemoteProcess();
+ }
+ /**
+ * Return true if the event should be sent back to its parent process.
+ * So, usual event handlers shouldn't call this.
+ */
+ inline bool WantReplyFromContentProcess() const {
+ return mFlags.WantReplyFromContentProcess();
+ }
+ /**
+ * Mark the event has already posted to a remote process.
+ */
+ inline void MarkAsPostedToRemoteProcess() {
+ mFlags.MarkAsPostedToRemoteProcess();
+ }
+ /**
+ * Reset the cross process dispatching state. This should be used when a
+ * process receives the event because the state is in the sender.
+ */
+ inline void ResetCrossProcessDispatchingState() {
+ mFlags.ResetCrossProcessDispatchingState();
+ }
+ /**
+ * Return true if the event has been posted to a remote process.
+ */
+ inline bool HasBeenPostedToRemoteProcess() const {
+ return mFlags.HasBeenPostedToRemoteProcess();
+ }
+ /**
+ * Return true if the event came from another process.
+ */
+ inline bool CameFromAnotherProcess() const {
+ return mFlags.CameFromAnotherProcess();
+ }
+ /**
+ * Mark the event as coming from another process.
+ */
+ inline void MarkAsComingFromAnotherProcess() {
+ mFlags.MarkAsComingFromAnotherProcess();
+ }
+ /**
+ * Mark the event is reserved by chrome. I.e., shouldn't be dispatched to
+ * content because it shouldn't be cancelable.
+ */
+ inline void MarkAsReservedByChrome() { mFlags.MarkAsReservedByChrome(); }
+ /**
+ * Return true if the event is reserved by chrome.
+ */
+ inline bool IsReservedByChrome() const { return mFlags.IsReservedByChrome(); }
+
+ /**
+ * Utils for checking event types
+ */
+
+ /**
+ * As*Event() returns the pointer of the instance only when the instance is
+ * the class or one of its derived class.
+ */
+#define NS_ROOT_EVENT_CLASS(aPrefix, aName)
+#define NS_EVENT_CLASS(aPrefix, aName) \
+ virtual aPrefix##aName* As##aName(); \
+ const aPrefix##aName* As##aName() const;
+
+#include "mozilla/EventClassList.h"
+
+#undef NS_EVENT_CLASS
+#undef NS_ROOT_EVENT_CLASS
+
+ /**
+ * Returns true if the event is a query content event.
+ */
+ bool IsQueryContentEvent() const;
+ /**
+ * Returns true if the event is a selection event.
+ */
+ bool IsSelectionEvent() const;
+ /**
+ * Returns true if the event is a content command event.
+ */
+ bool IsContentCommandEvent() const;
+
+ /**
+ * Returns true if the event mMessage is one of mouse events.
+ */
+ bool HasMouseEventMessage() const;
+ /**
+ * Returns true if the event mMessage is one of drag events.
+ */
+ bool HasDragEventMessage() const;
+ /**
+ * Returns true if aMessage or mMessage is one of key events.
+ */
+ static bool IsKeyEventMessage(EventMessage aMessage);
+ bool HasKeyEventMessage() const { return IsKeyEventMessage(mMessage); }
+ /**
+ * Returns true if the event mMessage is one of composition events or text
+ * event.
+ */
+ bool HasIMEEventMessage() const;
+
+ /**
+ * Returns true if the event can be sent to remote process.
+ */
+ bool CanBeSentToRemoteProcess() const;
+ /**
+ * Returns true if the original target is a remote process and the event
+ * will be posted to the remote process later.
+ */
+ bool WillBeSentToRemoteProcess() const;
+ /**
+ * Returns true if the event is related to IME handling. It includes
+ * IME events, query content events and selection events.
+ * Be careful when you use this.
+ */
+ bool IsIMERelatedEvent() const;
+
+ /**
+ * Whether the event should be handled by the frame of the mouse cursor
+ * position or not. When it should be handled there (e.g., the mouse events),
+ * this returns true.
+ */
+ bool IsUsingCoordinates() const;
+ /**
+ * Whether the event should be handled by the focused DOM window in the
+ * same top level window's or not. E.g., key events, IME related events
+ * (including the query content events, they are used in IME transaction)
+ * should be handled by the (last) focused window rather than the dispatched
+ * window.
+ *
+ * NOTE: Even if this returns true, the event isn't going to be handled by the
+ * application level active DOM window which is on another top level window.
+ * So, when the event is fired on a deactive window, the event is going to be
+ * handled by the last focused DOM window in the last focused window.
+ */
+ bool IsTargetedAtFocusedWindow() const;
+ /**
+ * Whether the event should be handled by the focused content or not. E.g.,
+ * key events, IME related events and other input events which are not handled
+ * by the frame of the mouse cursor position.
+ *
+ * NOTE: Even if this returns true, the event isn't going to be handled by the
+ * application level active DOM window which is on another top level window.
+ * So, when the event is fired on a deactive window, the event is going to be
+ * handled by the last focused DOM element of the last focused DOM window in
+ * the last focused window.
+ */
+ bool IsTargetedAtFocusedContent() const;
+ /**
+ * Whether the event should cause a DOM event.
+ */
+ bool IsAllowedToDispatchDOMEvent() const;
+ /**
+ * Whether the event should be dispatched in system group.
+ */
+ bool IsAllowedToDispatchInSystemGroup() const;
+ /**
+ * Whether the event should be blocked for fingerprinting resistance.
+ */
+ bool IsBlockedForFingerprintingResistance() const;
+ /**
+ * Whether the event handler can flush pending notifications or not.
+ */
+ bool AllowFlushingPendingNotifications() const;
+ /**
+ * Initialize mComposed
+ */
+ void SetDefaultComposed() {
+ switch (mClass) {
+ case eClipboardEventClass:
+ mFlags.mComposed = true;
+ break;
+ case eCompositionEventClass:
+ mFlags.mComposed =
+ mMessage == eCompositionStart || mMessage == eCompositionUpdate ||
+ mMessage == eCompositionChange || mMessage == eCompositionEnd;
+ break;
+ case eDragEventClass:
+ // All drag & drop events are composed
+ mFlags.mComposed = mMessage == eDrag || mMessage == eDragEnd ||
+ mMessage == eDragEnter || mMessage == eDragExit ||
+ mMessage == eDragLeave || mMessage == eDragOver ||
+ mMessage == eDragStart || mMessage == eDrop;
+ break;
+ case eEditorInputEventClass:
+ mFlags.mComposed =
+ mMessage == eEditorInput || mMessage == eEditorBeforeInput;
+ break;
+ case eFocusEventClass:
+ mFlags.mComposed = mMessage == eBlur || mMessage == eFocus ||
+ mMessage == eFocusOut || mMessage == eFocusIn;
+ break;
+ case eKeyboardEventClass:
+ mFlags.mComposed =
+ mMessage == eKeyDown || mMessage == eKeyUp || mMessage == eKeyPress;
+ break;
+ case eMouseEventClass:
+ mFlags.mComposed =
+ mMessage == eMouseClick || mMessage == eMouseDoubleClick ||
+ mMessage == eMouseAuxClick || mMessage == eMouseDown ||
+ mMessage == eMouseUp || mMessage == eMouseOver ||
+ mMessage == eMouseOut || mMessage == eMouseMove ||
+ mMessage == eContextMenu || mMessage == eXULPopupShowing ||
+ mMessage == eXULPopupHiding || mMessage == eXULPopupShown ||
+ mMessage == eXULPopupHidden;
+ break;
+ case ePointerEventClass:
+ // All pointer events are composed
+ mFlags.mComposed =
+ mMessage == ePointerDown || mMessage == ePointerMove ||
+ mMessage == ePointerUp || mMessage == ePointerCancel ||
+ mMessage == ePointerOver || mMessage == ePointerOut ||
+ mMessage == ePointerGotCapture || mMessage == ePointerLostCapture;
+ break;
+ case eTouchEventClass:
+ // All touch events are composed
+ mFlags.mComposed = mMessage == eTouchStart || mMessage == eTouchEnd ||
+ mMessage == eTouchMove || mMessage == eTouchCancel;
+ break;
+ case eUIEventClass:
+ mFlags.mComposed = mMessage == eLegacyDOMFocusIn ||
+ mMessage == eLegacyDOMFocusOut ||
+ mMessage == eLegacyDOMActivate;
+ break;
+ case eWheelEventClass:
+ // All wheel events are composed
+ mFlags.mComposed = mMessage == eWheel;
+ break;
+ case eMouseScrollEventClass:
+ // Legacy mouse scroll events are composed too, for consistency with
+ // wheel.
+ mFlags.mComposed = mMessage == eLegacyMouseLineOrPageScroll ||
+ mMessage == eLegacyMousePixelScroll;
+ break;
+ default:
+ mFlags.mComposed = false;
+ break;
+ }
+ }
+
+ void SetComposed(const nsAString& aEventTypeArg) {
+ mFlags.mComposed = // composition events
+ aEventTypeArg.EqualsLiteral("compositionstart") ||
+ aEventTypeArg.EqualsLiteral("compositionupdate") ||
+ aEventTypeArg.EqualsLiteral("compositionend") ||
+ aEventTypeArg.EqualsLiteral("text") ||
+ // drag and drop events
+ aEventTypeArg.EqualsLiteral("dragstart") ||
+ aEventTypeArg.EqualsLiteral("drag") ||
+ aEventTypeArg.EqualsLiteral("dragenter") ||
+ aEventTypeArg.EqualsLiteral("dragexit") ||
+ aEventTypeArg.EqualsLiteral("dragleave") ||
+ aEventTypeArg.EqualsLiteral("dragover") ||
+ aEventTypeArg.EqualsLiteral("drop") ||
+ aEventTypeArg.EqualsLiteral("dropend") ||
+ // editor input events
+ aEventTypeArg.EqualsLiteral("input") ||
+ aEventTypeArg.EqualsLiteral("beforeinput") ||
+ // focus events
+ aEventTypeArg.EqualsLiteral("blur") ||
+ aEventTypeArg.EqualsLiteral("focus") ||
+ aEventTypeArg.EqualsLiteral("focusin") ||
+ aEventTypeArg.EqualsLiteral("focusout") ||
+ // keyboard events
+ aEventTypeArg.EqualsLiteral("keydown") ||
+ aEventTypeArg.EqualsLiteral("keyup") ||
+ aEventTypeArg.EqualsLiteral("keypress") ||
+ // mouse events
+ aEventTypeArg.EqualsLiteral("click") ||
+ aEventTypeArg.EqualsLiteral("dblclick") ||
+ aEventTypeArg.EqualsLiteral("mousedown") ||
+ aEventTypeArg.EqualsLiteral("mouseup") ||
+ aEventTypeArg.EqualsLiteral("mouseenter") ||
+ aEventTypeArg.EqualsLiteral("mouseleave") ||
+ aEventTypeArg.EqualsLiteral("mouseover") ||
+ aEventTypeArg.EqualsLiteral("mouseout") ||
+ aEventTypeArg.EqualsLiteral("mousemove") ||
+ aEventTypeArg.EqualsLiteral("contextmenu") ||
+ // pointer events
+ aEventTypeArg.EqualsLiteral("pointerdown") ||
+ aEventTypeArg.EqualsLiteral("pointermove") ||
+ aEventTypeArg.EqualsLiteral("pointerup") ||
+ aEventTypeArg.EqualsLiteral("pointercancel") ||
+ aEventTypeArg.EqualsLiteral("pointerover") ||
+ aEventTypeArg.EqualsLiteral("pointerout") ||
+ aEventTypeArg.EqualsLiteral("pointerenter") ||
+ aEventTypeArg.EqualsLiteral("pointerleave") ||
+ aEventTypeArg.EqualsLiteral("gotpointercapture") ||
+ aEventTypeArg.EqualsLiteral("lostpointercapture") ||
+ // touch events
+ aEventTypeArg.EqualsLiteral("touchstart") ||
+ aEventTypeArg.EqualsLiteral("touchend") ||
+ aEventTypeArg.EqualsLiteral("touchmove") ||
+ aEventTypeArg.EqualsLiteral("touchcancel") ||
+ // UI legacy events
+ aEventTypeArg.EqualsLiteral("DOMFocusIn") ||
+ aEventTypeArg.EqualsLiteral("DOMFocusOut") ||
+ aEventTypeArg.EqualsLiteral("DOMActivate") ||
+ // wheel events
+ aEventTypeArg.EqualsLiteral("wheel");
+ }
+
+ void SetComposed(bool aComposed) { mFlags.mComposed = aComposed; }
+
+ void SetDefaultComposedInNativeAnonymousContent() {
+ // For compatibility concerns, we set mComposedInNativeAnonymousContent to
+ // false for those events we want to stop propagation.
+ //
+ // nsVideoFrame may create anonymous image element which fires eLoad,
+ // eLoadStart, eLoadEnd, eLoadError. We don't want these events cross
+ // the boundary of NAC
+ mFlags.mComposedInNativeAnonymousContent =
+ mMessage != eLoad && mMessage != eLoadStart && mMessage != eLoadEnd &&
+ mMessage != eLoadError;
+ }
+
+ bool IsUserAction() const;
+};
+
+/******************************************************************************
+ * mozilla::WidgetGUIEvent
+ ******************************************************************************/
+
+class WidgetGUIEvent : public WidgetEvent {
+ protected:
+ WidgetGUIEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ EventClassID aEventClassID,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEvent(aIsTrusted, aMessage, aEventClassID, aTime),
+ mWidget(aWidget) {}
+
+ WidgetGUIEvent() = default;
+
+ public:
+ virtual WidgetGUIEvent* AsGUIEvent() override { return this; }
+
+ WidgetGUIEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEvent(aIsTrusted, aMessage, eGUIEventClass, aTime),
+ mWidget(aWidget) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eGUIEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetGUIEvent* result = new WidgetGUIEvent(false, mMessage, nullptr, this);
+ result->AssignGUIEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ // Originator of the event
+ nsCOMPtr<nsIWidget> mWidget;
+
+ void AssignGUIEventData(const WidgetGUIEvent& aEvent, bool aCopyTargets) {
+ AssignEventData(aEvent, aCopyTargets);
+ // widget should be initialized with the constructor.
+ }
+};
+
+/******************************************************************************
+ * mozilla::Modifier
+ *
+ * All modifier keys should be defined here. This is used for managing
+ * modifier states for DOM Level 3 or later.
+ ******************************************************************************/
+
+enum Modifier {
+ MODIFIER_NONE = 0x0000,
+ MODIFIER_ALT = 0x0001,
+ MODIFIER_ALTGRAPH = 0x0002,
+ MODIFIER_CAPSLOCK = 0x0004,
+ MODIFIER_CONTROL = 0x0008,
+ MODIFIER_FN = 0x0010,
+ MODIFIER_FNLOCK = 0x0020,
+ MODIFIER_META = 0x0040,
+ MODIFIER_NUMLOCK = 0x0080,
+ MODIFIER_SCROLLLOCK = 0x0100,
+ MODIFIER_SHIFT = 0x0200,
+ MODIFIER_SYMBOL = 0x0400,
+ MODIFIER_SYMBOLLOCK = 0x0800,
+};
+
+/******************************************************************************
+ * Modifier key names.
+ ******************************************************************************/
+
+#define NS_DOM_KEYNAME_ALT "Alt"
+#define NS_DOM_KEYNAME_ALTGRAPH "AltGraph"
+#define NS_DOM_KEYNAME_CAPSLOCK "CapsLock"
+#define NS_DOM_KEYNAME_CONTROL "Control"
+#define NS_DOM_KEYNAME_FN "Fn"
+#define NS_DOM_KEYNAME_FNLOCK "FnLock"
+#define NS_DOM_KEYNAME_META "Meta"
+#define NS_DOM_KEYNAME_NUMLOCK "NumLock"
+#define NS_DOM_KEYNAME_SCROLLLOCK "ScrollLock"
+#define NS_DOM_KEYNAME_SHIFT "Shift"
+#define NS_DOM_KEYNAME_SYMBOL "Symbol"
+#define NS_DOM_KEYNAME_SYMBOLLOCK "SymbolLock"
+#define NS_DOM_KEYNAME_OS "OS"
+
+/******************************************************************************
+ * mozilla::Modifiers
+ ******************************************************************************/
+
+typedef uint16_t Modifiers;
+
+class MOZ_STACK_CLASS GetModifiersName final : public nsAutoCString {
+ public:
+ explicit GetModifiersName(Modifiers aModifiers) {
+ if (aModifiers & MODIFIER_ALT) {
+ AssignLiteral(NS_DOM_KEYNAME_ALT);
+ }
+ if (aModifiers & MODIFIER_ALTGRAPH) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_ALTGRAPH);
+ }
+ if (aModifiers & MODIFIER_CAPSLOCK) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_CAPSLOCK);
+ }
+ if (aModifiers & MODIFIER_CONTROL) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_CONTROL);
+ }
+ if (aModifiers & MODIFIER_FN) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_FN);
+ }
+ if (aModifiers & MODIFIER_FNLOCK) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_FNLOCK);
+ }
+ if (aModifiers & MODIFIER_META) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_META);
+ }
+ if (aModifiers & MODIFIER_NUMLOCK) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_NUMLOCK);
+ }
+ if (aModifiers & MODIFIER_SCROLLLOCK) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_SCROLLLOCK);
+ }
+ if (aModifiers & MODIFIER_SHIFT) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_SHIFT);
+ }
+ if (aModifiers & MODIFIER_SYMBOL) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_SYMBOL);
+ }
+ if (aModifiers & MODIFIER_SYMBOLLOCK) {
+ MaybeAppendSeparator();
+ AppendLiteral(NS_DOM_KEYNAME_SYMBOLLOCK);
+ }
+ if (IsEmpty()) {
+ AssignLiteral("none");
+ }
+ }
+
+ private:
+ void MaybeAppendSeparator() {
+ if (!IsEmpty()) {
+ AppendLiteral(" | ");
+ }
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetInputEvent
+ ******************************************************************************/
+
+class WidgetInputEvent : public WidgetGUIEvent {
+ protected:
+ WidgetInputEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ EventClassID aEventClassID,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, aEventClassID, aTime),
+ mModifiers(0) {}
+
+ WidgetInputEvent() : mModifiers(0) {}
+
+ public:
+ virtual WidgetInputEvent* AsInputEvent() override { return this; }
+
+ WidgetInputEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, eInputEventClass, aTime),
+ mModifiers(0) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eInputEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetInputEvent* result =
+ new WidgetInputEvent(false, mMessage, nullptr, this);
+ result->AssignInputEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ /**
+ * Returns a modifier of "Accel" virtual modifier which is used for shortcut
+ * key.
+ */
+ static Modifier AccelModifier();
+
+ /**
+ * GetModifier() returns a modifier flag which is activated by aDOMKeyName.
+ */
+ static Modifier GetModifier(const nsAString& aDOMKeyName);
+
+ // true indicates the accel key on the environment is down
+ bool IsAccel() const { return ((mModifiers & AccelModifier()) != 0); }
+
+ // true indicates the shift key is down
+ bool IsShift() const { return ((mModifiers & MODIFIER_SHIFT) != 0); }
+ // true indicates the control key is down
+ bool IsControl() const { return ((mModifiers & MODIFIER_CONTROL) != 0); }
+ // true indicates the alt key is down
+ bool IsAlt() const { return ((mModifiers & MODIFIER_ALT) != 0); }
+ // true indicates the meta key is down (Command key on macOS, Windows logo key
+ // on Windows, Super/Hyper key on Linux, Meta key on Android).
+ bool IsMeta() const { return ((mModifiers & MODIFIER_META) != 0); }
+ // true indicates the alt graph key is down
+ // NOTE: on Mac, the option key press causes both IsAlt() and IsAltGrpah()
+ // return true.
+ bool IsAltGraph() const { return ((mModifiers & MODIFIER_ALTGRAPH) != 0); }
+ // true indicates the CapLock LED is turn on.
+ bool IsCapsLocked() const { return ((mModifiers & MODIFIER_CAPSLOCK) != 0); }
+ // true indicates the NumLock LED is turn on.
+ bool IsNumLocked() const { return ((mModifiers & MODIFIER_NUMLOCK) != 0); }
+ // true indicates the ScrollLock LED is turn on.
+ bool IsScrollLocked() const {
+ return ((mModifiers & MODIFIER_SCROLLLOCK) != 0);
+ }
+
+ // true indicates the Fn key is down, but this is not supported by native
+ // key event on any platform.
+ bool IsFn() const { return ((mModifiers & MODIFIER_FN) != 0); }
+ // true indicates the FnLock LED is turn on, but we don't know such
+ // keyboards nor platforms.
+ bool IsFnLocked() const { return ((mModifiers & MODIFIER_FNLOCK) != 0); }
+ // true indicates the Symbol is down, but this is not supported by native
+ // key event on any platforms.
+ bool IsSymbol() const { return ((mModifiers & MODIFIER_SYMBOL) != 0); }
+ // true indicates the SymbolLock LED is turn on, but we don't know such
+ // keyboards nor platforms.
+ bool IsSymbolLocked() const {
+ return ((mModifiers & MODIFIER_SYMBOLLOCK) != 0);
+ }
+
+ void InitBasicModifiers(bool aCtrlKey, bool aAltKey, bool aShiftKey,
+ bool aMetaKey) {
+ mModifiers = 0;
+ if (aCtrlKey) {
+ mModifiers |= MODIFIER_CONTROL;
+ }
+ if (aAltKey) {
+ mModifiers |= MODIFIER_ALT;
+ }
+ if (aShiftKey) {
+ mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aMetaKey) {
+ mModifiers |= MODIFIER_META;
+ }
+ }
+
+ Modifiers mModifiers;
+
+ void AssignInputEventData(const WidgetInputEvent& aEvent, bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ mModifiers = aEvent.mModifiers;
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalUIEvent
+ *
+ * XXX Why this inherits WidgetGUIEvent rather than WidgetEvent?
+ ******************************************************************************/
+
+class InternalUIEvent : public WidgetGUIEvent {
+ protected:
+ InternalUIEvent() : mDetail(0), mCausedByUntrustedEvent(false) {}
+
+ InternalUIEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ EventClassID aEventClassID,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, aEventClassID, aTime),
+ mDetail(0),
+ mCausedByUntrustedEvent(false) {}
+
+ InternalUIEvent(bool aIsTrusted, EventMessage aMessage,
+ EventClassID aEventClassID,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, nullptr, aEventClassID, aTime),
+ mDetail(0),
+ mCausedByUntrustedEvent(false) {}
+
+ public:
+ virtual InternalUIEvent* AsUIEvent() override { return this; }
+
+ /**
+ * If the UIEvent is caused by another event (e.g., click event),
+ * aEventCausesThisEvent should be the event. If there is no such event,
+ * this should be nullptr.
+ */
+ InternalUIEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEvent* aEventCausesThisEvent,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, nullptr, eUIEventClass, aTime),
+ mDetail(0),
+ mCausedByUntrustedEvent(aEventCausesThisEvent &&
+ !aEventCausesThisEvent->IsTrusted()) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eUIEventClass,
+ "Duplicate() must be overridden by sub class");
+ InternalUIEvent* result =
+ new InternalUIEvent(false, mMessage, nullptr, this);
+ result->AssignUIEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ int32_t mDetail;
+ // mCausedByUntrustedEvent is true if the event is caused by untrusted event.
+ bool mCausedByUntrustedEvent;
+
+ // If you check the event is a trusted event and NOT caused by an untrusted
+ // event, IsTrustable() returns what you expected.
+ bool IsTrustable() const { return IsTrusted() && !mCausedByUntrustedEvent; }
+
+ void AssignUIEventData(const InternalUIEvent& aEvent, bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ mDetail = aEvent.mDetail;
+ mCausedByUntrustedEvent = aEvent.mCausedByUntrustedEvent;
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_BasicEvents_h__
diff --git a/widget/ClipboardReadRequestChild.h b/widget/ClipboardReadRequestChild.h
new file mode 100644
index 0000000000..81d82e6296
--- /dev/null
+++ b/widget/ClipboardReadRequestChild.h
@@ -0,0 +1,34 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ClipboardReadRequestChild_h
+#define mozilla_ClipboardReadRequestChild_h
+
+#include "mozilla/PClipboardReadRequestChild.h"
+
+class nsITransferable;
+
+namespace mozilla {
+
+class ClipboardReadRequestChild final : public PClipboardReadRequestChild {
+ public:
+ explicit ClipboardReadRequestChild(const nsTArray<nsCString>& aFlavorList) {
+ mFlavorList.AppendElements(aFlavorList);
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(ClipboardReadRequestChild)
+
+ const nsTArray<nsCString>& FlavorList() const { return mFlavorList; }
+
+ protected:
+ virtual ~ClipboardReadRequestChild() = default;
+
+ private:
+ nsTArray<nsCString> mFlavorList;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ClipboardReadRequestChild_h
diff --git a/widget/ClipboardReadRequestParent.cpp b/widget/ClipboardReadRequestParent.cpp
new file mode 100644
index 0000000000..3f045a800a
--- /dev/null
+++ b/widget/ClipboardReadRequestParent.cpp
@@ -0,0 +1,115 @@
+/* -*- 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 "mozilla/ClipboardReadRequestParent.h"
+
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIClipboard.h"
+#include "nsITransferable.h"
+#include "nsWidgetsCID.h"
+
+using mozilla::dom::ContentParent;
+using mozilla::ipc::IPCResult;
+
+namespace mozilla {
+
+namespace {
+
+class ClipboardGetDataCallback final : public nsIAsyncClipboardRequestCallback {
+ public:
+ explicit ClipboardGetDataCallback(std::function<void(nsresult)>&& aCallback)
+ : mCallback(std::move(aCallback)) {}
+
+ // This object will never be held by a cycle-collected object, so it doesn't
+ // need to be cycle-collected despite holding alive cycle-collected objects.
+ NS_DECL_ISUPPORTS
+
+ // nsIAsyncClipboardRequestCallback
+ NS_IMETHOD OnComplete(nsresult aResult) override {
+ mCallback(aResult);
+ return NS_OK;
+ }
+
+ protected:
+ ~ClipboardGetDataCallback() = default;
+
+ std::function<void(nsresult)> mCallback;
+};
+
+NS_IMPL_ISUPPORTS(ClipboardGetDataCallback, nsIAsyncClipboardRequestCallback)
+
+static Result<nsCOMPtr<nsITransferable>, nsresult> CreateTransferable(
+ const nsTArray<nsCString>& aTypes) {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> trans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+
+ MOZ_TRY(trans->Init(nullptr));
+ // The private flag is only used to prevent the data from being cached to the
+ // disk. The flag is not exported to the IPCDataTransfer object.
+ // The flag is set because we are not sure whether the clipboard data is used
+ // in a private browsing context. The transferable is only used in this scope,
+ // so the cache would not reduce memory consumption anyway.
+ trans->SetIsPrivateData(true);
+ // Fill out flavors for transferable
+ for (uint32_t t = 0; t < aTypes.Length(); t++) {
+ MOZ_TRY(trans->AddDataFlavor(aTypes[t].get()));
+ }
+
+ return std::move(trans);
+}
+
+} // namespace
+
+IPCResult ClipboardReadRequestParent::RecvGetData(
+ const nsTArray<nsCString>& aFlavors, GetDataResolver&& aResolver) {
+ bool valid = false;
+ if (NS_FAILED(mAsyncGetClipboardData->GetValid(&valid)) || !valid) {
+ Unused << PClipboardReadRequestParent::Send__delete__(this);
+ aResolver(NS_ERROR_FAILURE);
+ return IPC_OK();
+ }
+
+ // Create transferable
+ auto result = CreateTransferable(aFlavors);
+ if (result.isErr()) {
+ aResolver(result.unwrapErr());
+ return IPC_OK();
+ }
+
+ nsCOMPtr<nsITransferable> trans = result.unwrap();
+ RefPtr<ClipboardGetDataCallback> callback =
+ MakeRefPtr<ClipboardGetDataCallback>([self = RefPtr{this},
+ resolver = std::move(aResolver),
+ trans,
+ manager = mManager](nsresult aRv) {
+ if (NS_FAILED(aRv)) {
+ bool valid = false;
+ if (NS_FAILED(self->mAsyncGetClipboardData->GetValid(&valid)) ||
+ !valid) {
+ Unused << PClipboardReadRequestParent::Send__delete__(self);
+ }
+ resolver(aRv);
+ return;
+ }
+
+ dom::IPCTransferableData ipcTransferableData;
+ nsContentUtils::TransferableToIPCTransferableData(
+ trans, &ipcTransferableData, false /* aInSyncMessage */, manager);
+ resolver(std::move(ipcTransferableData));
+ });
+ nsresult rv = mAsyncGetClipboardData->GetData(trans, callback);
+ if (NS_FAILED(rv)) {
+ callback->OnComplete(rv);
+ }
+ return IPC_OK();
+}
+
+} // namespace mozilla
diff --git a/widget/ClipboardReadRequestParent.h b/widget/ClipboardReadRequestParent.h
new file mode 100644
index 0000000000..cd51c4e300
--- /dev/null
+++ b/widget/ClipboardReadRequestParent.h
@@ -0,0 +1,39 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ClipboardReadRequestParent_h
+#define mozilla_ClipboardReadRequestParent_h
+
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/PClipboardReadRequestParent.h"
+#include "nsIClipboard.h"
+
+namespace mozilla {
+
+class ClipboardReadRequestParent final : public PClipboardReadRequestParent {
+ using IPCResult = mozilla::ipc::IPCResult;
+ using ContentParent = mozilla::dom::ContentParent;
+
+ public:
+ ClipboardReadRequestParent(ContentParent* aManager,
+ nsIAsyncGetClipboardData* aAsyncGetClipboardData)
+ : mManager(aManager), mAsyncGetClipboardData(aAsyncGetClipboardData) {}
+
+ NS_INLINE_DECL_REFCOUNTING(ClipboardReadRequestParent, override)
+
+ // PClipboardReadRequestParent
+ IPCResult RecvGetData(const nsTArray<nsCString>& aFlavors,
+ GetDataResolver&& aResolver);
+
+ private:
+ ~ClipboardReadRequestParent() = default;
+
+ RefPtr<ContentParent> mManager;
+ nsCOMPtr<nsIAsyncGetClipboardData> mAsyncGetClipboardData;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ClipboardReadRequestParent_h
diff --git a/widget/ClipboardWriteRequestChild.cpp b/widget/ClipboardWriteRequestChild.cpp
new file mode 100644
index 0000000000..0c42f7380d
--- /dev/null
+++ b/widget/ClipboardWriteRequestChild.cpp
@@ -0,0 +1,73 @@
+/* -*- 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 "mozilla/ClipboardWriteRequestChild.h"
+
+#if defined(ACCESSIBILITY) && defined(XP_WIN)
+# include "mozilla/a11y/Compatibility.h"
+#endif
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "nsITransferable.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(ClipboardWriteRequestChild, nsIAsyncSetClipboardData)
+
+NS_IMETHODIMP
+ClipboardWriteRequestChild::SetData(nsITransferable* aTransferable,
+ nsIClipboardOwner* aOwner) {
+ MOZ_ASSERT(aTransferable);
+ // Callback should be notified if actor is destroyed.
+ MOZ_ASSERT_IF(!CanSend(), !mIsValid && !mCallback);
+
+ if (!mIsValid) {
+ return NS_ERROR_FAILURE;
+ }
+
+#if defined(ACCESSIBILITY) && defined(XP_WIN)
+ a11y::Compatibility::SuppressA11yForClipboardCopy();
+#endif
+
+ mIsValid = false;
+ IPCTransferable ipcTransferable;
+ nsContentUtils::TransferableToIPCTransferable(aTransferable, &ipcTransferable,
+ false, nullptr);
+ SendSetData(std::move(ipcTransferable));
+ return NS_OK;
+}
+
+NS_IMETHODIMP ClipboardWriteRequestChild::Abort(nsresult aReason) {
+ // Callback should be notified if actor is destroyed.
+ MOZ_ASSERT_IF(!CanSend(), !mIsValid && !mCallback);
+
+ if (!mIsValid || !NS_FAILED(aReason)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Need to notify callback first to propagate reason properly.
+ MaybeNotifyCallback(aReason);
+ Unused << PClipboardWriteRequestChild::Send__delete__(this, aReason);
+ return NS_OK;
+}
+
+ipc::IPCResult ClipboardWriteRequestChild::Recv__delete__(nsresult aResult) {
+ MaybeNotifyCallback(aResult);
+ return IPC_OK();
+}
+
+void ClipboardWriteRequestChild::ActorDestroy(ActorDestroyReason aReason) {
+ MaybeNotifyCallback(NS_ERROR_ABORT);
+}
+
+void ClipboardWriteRequestChild::MaybeNotifyCallback(nsresult aResult) {
+ mIsValid = false;
+ if (nsCOMPtr<nsIAsyncClipboardRequestCallback> callback =
+ mCallback.forget()) {
+ callback->OnComplete(aResult);
+ }
+}
+
+} // namespace mozilla
diff --git a/widget/ClipboardWriteRequestChild.h b/widget/ClipboardWriteRequestChild.h
new file mode 100644
index 0000000000..078cfa07de
--- /dev/null
+++ b/widget/ClipboardWriteRequestChild.h
@@ -0,0 +1,40 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ClipboardWriteRequestChild_h
+#define mozilla_ClipboardWriteRequestChild_h
+
+#include "mozilla/PClipboardWriteRequestChild.h"
+#include "nsIClipboard.h"
+
+class nsITransferable;
+
+namespace mozilla {
+
+class ClipboardWriteRequestChild : public PClipboardWriteRequestChild,
+ public nsIAsyncSetClipboardData {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCSETCLIPBOARDDATA
+
+ explicit ClipboardWriteRequestChild(
+ nsIAsyncClipboardRequestCallback* aCallback)
+ : mCallback(aCallback) {}
+
+ ipc::IPCResult Recv__delete__(nsresult aResult);
+ void ActorDestroy(ActorDestroyReason aReason) override final;
+
+ protected:
+ virtual ~ClipboardWriteRequestChild() = default;
+
+ void MaybeNotifyCallback(nsresult aResult);
+
+ bool mIsValid = true;
+ nsCOMPtr<nsIAsyncClipboardRequestCallback> mCallback;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ClipboardWriteRequestChild_h
diff --git a/widget/ClipboardWriteRequestParent.cpp b/widget/ClipboardWriteRequestParent.cpp
new file mode 100644
index 0000000000..751befbe05
--- /dev/null
+++ b/widget/ClipboardWriteRequestParent.cpp
@@ -0,0 +1,113 @@
+/* -*- 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 "mozilla/ClipboardWriteRequestParent.h"
+
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIClipboard.h"
+#include "nsITransferable.h"
+#include "nsWidgetsCID.h"
+
+static NS_DEFINE_CID(kCClipboardCID, NS_CLIPBOARD_CID);
+
+using mozilla::dom::ContentParent;
+using mozilla::ipc::IPCResult;
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(ClipboardWriteRequestParent, nsIAsyncClipboardRequestCallback)
+
+ClipboardWriteRequestParent::ClipboardWriteRequestParent(
+ ContentParent* aManager)
+ : mManager(aManager) {}
+
+ClipboardWriteRequestParent::~ClipboardWriteRequestParent() = default;
+
+nsresult ClipboardWriteRequestParent::Init(const int32_t& aClipboardType) {
+ nsCOMPtr<nsIClipboard> clipboard(do_GetService(kCClipboardCID));
+ if (!clipboard) {
+ Unused << PClipboardWriteRequestParent::Send__delete__(this,
+ NS_ERROR_FAILURE);
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = clipboard->AsyncSetData(aClipboardType, this,
+ getter_AddRefs(mAsyncSetClipboardData));
+ if (NS_FAILED(rv)) {
+ Unused << PClipboardWriteRequestParent::Send__delete__(this, rv);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP ClipboardWriteRequestParent::OnComplete(nsresult aResult) {
+ nsCOMPtr<nsIAsyncSetClipboardData> clipboardData =
+ std::move(mAsyncSetClipboardData);
+ if (clipboardData) {
+ Unused << PClipboardWriteRequestParent::Send__delete__(this, aResult);
+ }
+ return NS_OK;
+}
+
+IPCResult ClipboardWriteRequestParent::RecvSetData(
+ const IPCTransferable& aTransferable) {
+ if (!mManager->ValidatePrincipal(
+ aTransferable.requestingPrincipal(),
+ {ContentParent::ValidatePrincipalOptions::AllowNullPtr,
+ ContentParent::ValidatePrincipalOptions::AllowExpanded,
+ ContentParent::ValidatePrincipalOptions::AllowSystem})) {
+ ContentParent::LogAndAssertFailedPrincipalValidationInfo(
+ aTransferable.requestingPrincipal(), __func__);
+ }
+
+ if (!mAsyncSetClipboardData) {
+ return IPC_OK();
+ }
+
+ nsresult rv = NS_ERROR_FAILURE;
+ nsCOMPtr<nsITransferable> trans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) {
+ mAsyncSetClipboardData->Abort(rv);
+ return IPC_OK();
+ }
+
+ trans->Init(nullptr);
+ rv = nsContentUtils::IPCTransferableToTransferable(
+ aTransferable, true /* aAddDataFlavor */, trans,
+ true /* aFilterUnknownFlavors */);
+ if (NS_FAILED(rv)) {
+ mAsyncSetClipboardData->Abort(rv);
+ return IPC_OK();
+ }
+
+ mAsyncSetClipboardData->SetData(trans, nullptr);
+ return IPC_OK();
+}
+
+IPCResult ClipboardWriteRequestParent::Recv__delete__(nsresult aReason) {
+#ifndef FUZZING_SNAPSHOT
+ MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(aReason));
+#endif
+ nsCOMPtr<nsIAsyncSetClipboardData> clipboardData =
+ std::move(mAsyncSetClipboardData);
+ if (clipboardData) {
+ clipboardData->Abort(aReason);
+ }
+ return IPC_OK();
+}
+
+void ClipboardWriteRequestParent::ActorDestroy(ActorDestroyReason aReason) {
+ nsCOMPtr<nsIAsyncSetClipboardData> clipboardData =
+ std::move(mAsyncSetClipboardData);
+ if (clipboardData) {
+ clipboardData->Abort(NS_ERROR_ABORT);
+ }
+}
+
+} // namespace mozilla
diff --git a/widget/ClipboardWriteRequestParent.h b/widget/ClipboardWriteRequestParent.h
new file mode 100644
index 0000000000..6027661bdb
--- /dev/null
+++ b/widget/ClipboardWriteRequestParent.h
@@ -0,0 +1,46 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ClipboardWriteRequestParent_h
+#define mozilla_ClipboardWriteRequestParent_h
+
+#include "mozilla/PClipboardWriteRequestParent.h"
+#include "nsIClipboard.h"
+
+namespace mozilla {
+
+namespace dom {
+class ContentParent;
+}
+
+class ClipboardWriteRequestParent final
+ : public PClipboardWriteRequestParent,
+ public nsIAsyncClipboardRequestCallback {
+ using IPCResult = mozilla::ipc::IPCResult;
+ using ContentParent = mozilla::dom::ContentParent;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCCLIPBOARDREQUESTCALLBACK
+
+ explicit ClipboardWriteRequestParent(ContentParent* aManager);
+
+ nsresult Init(const int32_t& aClipboardType);
+
+ IPCResult RecvSetData(const IPCTransferable& aTransferable);
+ IPCResult Recv__delete__(nsresult aReason);
+
+ void ActorDestroy(ActorDestroyReason aReason) override final;
+
+ private:
+ ~ClipboardWriteRequestParent();
+
+ RefPtr<ContentParent> mManager;
+ nsCOMPtr<nsIAsyncSetClipboardData> mAsyncSetClipboardData;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ClipboardWriteRequestParent_h
diff --git a/widget/ColorScheme.h b/widget/ColorScheme.h
new file mode 100644
index 0000000000..5a8b1a6cdd
--- /dev/null
+++ b/widget/ColorScheme.h
@@ -0,0 +1,30 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ColorScheme_h
+#define mozilla_ColorScheme_h
+
+#include <cstdint>
+
+namespace mozilla {
+
+// Whether we should use a light or dark appearance.
+enum class ColorScheme : uint8_t { Light, Dark };
+
+// The color-scheme we should get back in case it's not explicit. The "used"
+// mode is the mode that most of layout should use (defaults to light if the
+// page doesn't have a color-scheme property / meta-tag indicating otherwise).
+//
+// The "preferred" mode returns the color-scheme for purposes of
+// prefers-color-scheme propagation (that is, it defaults to the current
+// preferred color-scheme, rather than light).
+enum class ColorSchemeMode : uint8_t {
+ Used,
+ Preferred,
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/widget/CommandList.h b/widget/CommandList.h
new file mode 100644
index 0000000000..243656bb01
--- /dev/null
+++ b/widget/CommandList.h
@@ -0,0 +1,171 @@
+/* -*- 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/. */
+
+/**
+ * Define NS_DEFINE_COMMAND(aName, aCommandStr) before including this.
+ * @param aName The name useful in C++ of the command.
+ * @param aCommandStr The command string in JS.
+ *
+ * Define NS_DEFINE_COMMAND_WITH_PARAM(aName, aCommandStr, aParam) before
+ * including this.
+ * @param aName The name useful in C++ of the command.
+ * @param aCommandStr The command string in JS, but this may be shared with
+ * other aName values. I.e., cannot map aName and
+ * aCommandStr 1:1.
+ * @param aParam Additional param value. When aCommandStr is executed,
+ * this value is also specified. I.e., aName becomes
+ * unique when you look for with both aCommandStr and
+ * aParam.
+ *
+ * Define NS_DEFINE_COMMAND_NO_EXEC_COMMAND(aName) before including this.
+ * @param aName The name useful in C++ of the command.
+ */
+
+// Mapped from commands of some platforms
+NS_DEFINE_COMMAND(BeginLine, cmd_beginLine)
+NS_DEFINE_COMMAND(CharNext, cmd_charNext)
+NS_DEFINE_COMMAND(CharPrevious, cmd_charPrevious)
+NS_DEFINE_COMMAND(Copy, cmd_copy)
+NS_DEFINE_COMMAND(Cut, cmd_cut)
+NS_DEFINE_COMMAND(Delete, cmd_delete)
+NS_DEFINE_COMMAND(DeleteCharBackward, cmd_deleteCharBackward)
+NS_DEFINE_COMMAND(DeleteCharForward, cmd_deleteCharForward)
+NS_DEFINE_COMMAND(DeleteToBeginningOfLine, cmd_deleteToBeginningOfLine)
+NS_DEFINE_COMMAND(DeleteToEndOfLine, cmd_deleteToEndOfLine)
+NS_DEFINE_COMMAND(DeleteWordBackward, cmd_deleteWordBackward)
+NS_DEFINE_COMMAND(DeleteWordForward, cmd_deleteWordForward)
+NS_DEFINE_COMMAND(EndLine, cmd_endLine)
+NS_DEFINE_COMMAND(InsertParagraph, cmd_insertParagraph)
+NS_DEFINE_COMMAND(InsertLineBreak, cmd_insertLineBreak)
+NS_DEFINE_COMMAND(LineNext, cmd_lineNext)
+NS_DEFINE_COMMAND(LinePrevious, cmd_linePrevious)
+NS_DEFINE_COMMAND(MoveBottom, cmd_moveBottom)
+NS_DEFINE_COMMAND(MovePageDown, cmd_movePageDown)
+NS_DEFINE_COMMAND(MovePageUp, cmd_movePageUp)
+NS_DEFINE_COMMAND(MoveTop, cmd_moveTop)
+NS_DEFINE_COMMAND(Paste, cmd_paste)
+NS_DEFINE_COMMAND(ScrollBottom, cmd_scrollBottom)
+NS_DEFINE_COMMAND(ScrollLeft, cmd_scrollLeft)
+NS_DEFINE_COMMAND(ScrollLineDown, cmd_scrollLineDown)
+NS_DEFINE_COMMAND(ScrollLineUp, cmd_scrollLineUp)
+NS_DEFINE_COMMAND(ScrollPageDown, cmd_scrollPageDown)
+NS_DEFINE_COMMAND(ScrollPageUp, cmd_scrollPageUp)
+NS_DEFINE_COMMAND(ScrollRight, cmd_scrollRight)
+NS_DEFINE_COMMAND(ScrollTop, cmd_scrollTop)
+NS_DEFINE_COMMAND(SelectAll, cmd_selectAll)
+NS_DEFINE_COMMAND(SelectBeginLine, cmd_selectBeginLine)
+NS_DEFINE_COMMAND(SelectBottom, cmd_selectBottom)
+NS_DEFINE_COMMAND(SelectCharNext, cmd_selectCharNext)
+NS_DEFINE_COMMAND(SelectCharPrevious, cmd_selectCharPrevious)
+NS_DEFINE_COMMAND(SelectEndLine, cmd_selectEndLine)
+NS_DEFINE_COMMAND(SelectLineNext, cmd_selectLineNext)
+NS_DEFINE_COMMAND(SelectLinePrevious, cmd_selectLinePrevious)
+NS_DEFINE_COMMAND(SelectPageDown, cmd_selectPageDown)
+NS_DEFINE_COMMAND(SelectPageUp, cmd_selectPageUp)
+NS_DEFINE_COMMAND(SelectTop, cmd_selectTop)
+NS_DEFINE_COMMAND(SelectWordNext, cmd_selectWordNext)
+NS_DEFINE_COMMAND(SelectWordPrevious, cmd_selectWordPrevious)
+NS_DEFINE_COMMAND(WordNext, cmd_wordNext)
+NS_DEFINE_COMMAND(WordPrevious, cmd_wordPrevious)
+
+// We don't have corresponding commands for them, but some platforms have them.
+NS_DEFINE_COMMAND_NO_EXEC_COMMAND(CancelOperation)
+NS_DEFINE_COMMAND_NO_EXEC_COMMAND(Complete)
+NS_DEFINE_COMMAND_NO_EXEC_COMMAND(InsertBacktab)
+NS_DEFINE_COMMAND_NO_EXEC_COMMAND(InsertTab)
+
+// Commands mapped from HTMLDocument.execCommand()
+NS_DEFINE_COMMAND(FormatBold, cmd_bold)
+NS_DEFINE_COMMAND(FormatItalic, cmd_italic)
+NS_DEFINE_COMMAND(FormatUnderline, cmd_underline)
+NS_DEFINE_COMMAND(FormatStrikeThrough, cmd_strikethrough)
+NS_DEFINE_COMMAND(FormatSubscript, cmd_subscript)
+NS_DEFINE_COMMAND(FormatSuperscript, cmd_superscript)
+NS_DEFINE_COMMAND(HistoryUndo, cmd_undo)
+NS_DEFINE_COMMAND(HistoryRedo, cmd_redo)
+NS_DEFINE_COMMAND(FormatBlock, cmd_formatBlock)
+NS_DEFINE_COMMAND(FormatIndent, cmd_indent)
+NS_DEFINE_COMMAND(FormatOutdent, cmd_outdent)
+NS_DEFINE_COMMAND_WITH_PARAM(FormatJustifyLeft, cmd_align, left)
+NS_DEFINE_COMMAND_WITH_PARAM(FormatJustifyRight, cmd_align, right)
+NS_DEFINE_COMMAND_WITH_PARAM(FormatJustifyCenter, cmd_align, center)
+NS_DEFINE_COMMAND_WITH_PARAM(FormatJustifyFull, cmd_align, justify)
+NS_DEFINE_COMMAND(FormatBackColor, cmd_highlight)
+NS_DEFINE_COMMAND(FormatFontColor, cmd_fontColor)
+NS_DEFINE_COMMAND(FormatFontName, cmd_fontFace)
+NS_DEFINE_COMMAND(FormatFontSize, cmd_fontSize)
+NS_DEFINE_COMMAND(FormatIncreaseFontSize, cmd_increaseFont)
+NS_DEFINE_COMMAND(FormatDecreaseFontSize, cmd_decreaseFont)
+NS_DEFINE_COMMAND(InsertHorizontalRule, cmd_insertHR)
+NS_DEFINE_COMMAND(InsertLink, cmd_insertLinkNoUI)
+NS_DEFINE_COMMAND(InsertImage, cmd_insertImageNoUI)
+NS_DEFINE_COMMAND(InsertHTML, cmd_insertHTML)
+NS_DEFINE_COMMAND(InsertText, cmd_insertText)
+NS_DEFINE_COMMAND(InsertOrderedList, cmd_ol)
+NS_DEFINE_COMMAND(InsertUnorderedList, cmd_ul)
+NS_DEFINE_COMMAND(FormatRemove, cmd_removeStyles)
+NS_DEFINE_COMMAND(FormatRemoveLink, cmd_removeLinks)
+NS_DEFINE_COMMAND(SetDocumentUseCSS, cmd_setDocumentUseCSS)
+NS_DEFINE_COMMAND(SetDocumentReadOnly, cmd_setDocumentReadOnly)
+NS_DEFINE_COMMAND(SetDocumentInsertBROnEnterKeyPress, cmd_insertBrOnReturn)
+NS_DEFINE_COMMAND(SetDocumentDefaultParagraphSeparator,
+ cmd_defaultParagraphSeparator)
+NS_DEFINE_COMMAND(ToggleObjectResizers, cmd_enableObjectResizing)
+NS_DEFINE_COMMAND(ToggleInlineTableEditor, cmd_enableInlineTableEditing)
+NS_DEFINE_COMMAND(ToggleAbsolutePositionEditor,
+ cmd_enableAbsolutePositionEditing)
+NS_DEFINE_COMMAND(EnableCompatibleJoinSplitNodeDirection,
+ cmd_enableCompatibleJoinSplitNodeDirection)
+
+// Commands not mapped from HTMLDocument.execCommand() but available with
+// command dispatcher and handled in editor.
+NS_DEFINE_COMMAND(CutOrDelete, cmd_cutOrDelete)
+NS_DEFINE_COMMAND(CopyOrDelete, cmd_copyOrDelete)
+NS_DEFINE_COMMAND(EditorObserverDocumentCreated, obs_documentCreated)
+NS_DEFINE_COMMAND(EditorObserverDocumentLocationChanged,
+ obs_documentLocationChanged)
+NS_DEFINE_COMMAND(EditorObserverDocumentWillBeDestroyed,
+ obs_documentWillBeDestroyed)
+NS_DEFINE_COMMAND(FormatAbbreviation, cmd_abbr)
+NS_DEFINE_COMMAND(FormatAbsolutePosition, cmd_absPos)
+NS_DEFINE_COMMAND(FormatAcronym, cmd_acronym)
+NS_DEFINE_COMMAND(FormatCitation, cmd_cite)
+NS_DEFINE_COMMAND(FormatCode, cmd_code)
+NS_DEFINE_COMMAND(FormatDecreaseZIndex, cmd_decreaseZIndex)
+NS_DEFINE_COMMAND(FormatDocumentBackgroundColor, cmd_backgroundColor)
+NS_DEFINE_COMMAND(FormatEmphasis, cmd_em)
+NS_DEFINE_COMMAND(FormatIncreaseZIndex, cmd_increaseZIndex)
+NS_DEFINE_COMMAND(FormatJustify, cmd_align) // Only for getting enabled/state
+NS_DEFINE_COMMAND(FormatJustifyNone, cmd_align) // with empty string or params
+NS_DEFINE_COMMAND(FormatNoBreak, cmd_nobreak)
+NS_DEFINE_COMMAND(FormatRemoveList, cmd_removeList)
+NS_DEFINE_COMMAND(FormatSample, cmd_samp)
+NS_DEFINE_COMMAND(FormatSetBlockTextDirection, cmd_switchTextDirection)
+NS_DEFINE_COMMAND(FormatStrong, cmd_strong)
+NS_DEFINE_COMMAND(FormatTeletypeText, cmd_tt)
+NS_DEFINE_COMMAND(FormatVariable, cmd_var)
+NS_DEFINE_COMMAND(InsertDefinitionDetails, cmd_dd)
+NS_DEFINE_COMMAND(InsertDefinitionTerm, cmd_dt)
+NS_DEFINE_COMMAND(MoveDown, cmd_moveDown)
+NS_DEFINE_COMMAND(MoveDown2, cmd_moveDown2)
+NS_DEFINE_COMMAND(MoveLeft, cmd_moveLeft)
+NS_DEFINE_COMMAND(MoveLeft2, cmd_moveLeft2)
+NS_DEFINE_COMMAND(MoveRight, cmd_moveRight)
+NS_DEFINE_COMMAND(MoveRight2, cmd_moveRight2)
+NS_DEFINE_COMMAND(MoveUp, cmd_moveUp)
+NS_DEFINE_COMMAND(MoveUp2, cmd_moveUp2)
+NS_DEFINE_COMMAND(ParagraphState, cmd_paragraphState)
+NS_DEFINE_COMMAND(PasteAsQuotation, cmd_pasteQuote)
+NS_DEFINE_COMMAND(PasteTransferable, cmd_pasteTransferable)
+NS_DEFINE_COMMAND(PasteWithoutFormat, cmd_pasteNoFormatting)
+NS_DEFINE_COMMAND(SelectDown, cmd_selectDown)
+NS_DEFINE_COMMAND(SelectDown2, cmd_selectDown2)
+NS_DEFINE_COMMAND(SelectLeft, cmd_selectLeft)
+NS_DEFINE_COMMAND(SelectLeft2, cmd_selectLeft2)
+NS_DEFINE_COMMAND(SelectRight, cmd_selectRight)
+NS_DEFINE_COMMAND(SelectRight2, cmd_selectRight2)
+NS_DEFINE_COMMAND(SelectUp, cmd_selectUp)
+NS_DEFINE_COMMAND(SelectUp2, cmd_selectUp2)
+NS_DEFINE_COMMAND(SetDocumentModified, cmd_setDocumentModified)
diff --git a/widget/CompositorWidget.cpp b/widget/CompositorWidget.cpp
new file mode 100644
index 0000000000..6eddecff4a
--- /dev/null
+++ b/widget/CompositorWidget.cpp
@@ -0,0 +1,78 @@
+/* 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 "CompositorWidget.h"
+#include "GLConsts.h"
+#include "nsBaseWidget.h"
+#include "VsyncDispatcher.h"
+
+namespace mozilla {
+namespace widget {
+
+CompositorWidget::CompositorWidget(const layers::CompositorOptions& aOptions)
+ : mOptions(aOptions) {}
+
+CompositorWidget::~CompositorWidget() = default;
+
+already_AddRefed<gfx::DrawTarget> CompositorWidget::StartRemoteDrawing() {
+ return nullptr;
+}
+
+void CompositorWidget::CleanupRemoteDrawing() { mLastBackBuffer = nullptr; }
+
+already_AddRefed<gfx::DrawTarget> CompositorWidget::GetBackBufferDrawTarget(
+ gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect,
+ bool* aOutIsCleared) {
+ MOZ_ASSERT(aScreenTarget);
+ gfx::SurfaceFormat format =
+ aScreenTarget->GetFormat() == gfx::SurfaceFormat::B8G8R8X8
+ ? gfx::SurfaceFormat::B8G8R8X8
+ : gfx::SurfaceFormat::B8G8R8A8;
+ gfx::IntSize size = aRect.Size();
+ gfx::IntSize clientSize = Max(size, GetClientSize().ToUnknownSize());
+
+ *aOutIsCleared = false;
+ // Re-use back buffer if possible
+ if (!mLastBackBuffer ||
+ mLastBackBuffer->GetBackendType() != aScreenTarget->GetBackendType() ||
+ mLastBackBuffer->GetFormat() != format ||
+ mLastBackBuffer->GetSize() != clientSize) {
+ mLastBackBuffer =
+ aScreenTarget->CreateSimilarDrawTarget(clientSize, format);
+ *aOutIsCleared = true;
+ }
+ return do_AddRef(mLastBackBuffer);
+}
+
+already_AddRefed<gfx::SourceSurface> CompositorWidget::EndBackBufferDrawing() {
+ RefPtr<gfx::SourceSurface> surface =
+ mLastBackBuffer ? mLastBackBuffer->Snapshot() : nullptr;
+ return surface.forget();
+}
+
+uint32_t CompositorWidget::GetGLFrameBufferFormat() { return LOCAL_GL_RGBA; }
+
+RefPtr<VsyncObserver> CompositorWidget::GetVsyncObserver() const {
+ // This should only used when the widget is in the GPU process, and should be
+ // implemented by IPDL-enabled CompositorWidgets.
+ // GPU process does not have a CompositorVsyncDispatcher.
+ MOZ_ASSERT_UNREACHABLE("Must be implemented by derived class");
+ return nullptr;
+}
+
+LayoutDeviceIntRegion CompositorWidget::GetTransparentRegion() {
+ // By default, we check the transparency mode to determine if the widget is
+ // transparent, and if so, designate the entire widget drawing area as
+ // transparent. Widgets wanting more complex transparency region determination
+ // should override this method.
+ auto* widget = RealWidget();
+ if (!widget || widget->GetTransparencyMode() != TransparencyMode::Opaque ||
+ widget->WidgetPaintsBackground()) {
+ return LayoutDeviceIntRect(LayoutDeviceIntPoint(0, 0), GetClientSize());
+ }
+ return LayoutDeviceIntRegion();
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/CompositorWidget.h b/widget/CompositorWidget.h
new file mode 100644
index 0000000000..d5e2fb664b
--- /dev/null
+++ b/widget/CompositorWidget.h
@@ -0,0 +1,298 @@
+/* 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/. */
+
+#ifndef mozilla_widget_CompositorWidget_h__
+#define mozilla_widget_CompositorWidget_h__
+
+#include "nsISupports.h"
+#include "mozilla/RefPtr.h"
+#include "Units.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/layers/CompositorOptions.h"
+#include "mozilla/layers/LayersTypes.h"
+
+#ifdef MOZ_IS_GCC
+# include "mozilla/layers/NativeLayer.h"
+#endif
+
+class nsIWidget;
+class nsBaseWidget;
+
+namespace mozilla {
+class VsyncObserver;
+namespace gl {
+class GLContext;
+} // namespace gl
+namespace layers {
+class Compositor;
+class LayerManager;
+class NativeLayerRoot;
+} // namespace layers
+namespace gfx {
+class DrawTarget;
+class SourceSurface;
+} // namespace gfx
+namespace widget {
+
+class WinCompositorWidget;
+class GtkCompositorWidget;
+class AndroidCompositorWidget;
+class CompositorWidgetInitData;
+
+// Gecko widgets usually need to communicate with the CompositorWidget with
+// platform-specific messages (for example to update the window size or
+// transparency). This functionality is controlled through a "host". Since
+// this functionality is platform-dependent, it is only forward declared
+// here.
+class PlatformCompositorWidgetDelegate;
+
+// Headless mode uses its own, singular CompositorWidget implementation.
+class HeadlessCompositorWidget;
+
+class CompositorWidgetDelegate {
+ public:
+ virtual PlatformCompositorWidgetDelegate* AsPlatformSpecificDelegate() {
+ return nullptr;
+ }
+
+ virtual HeadlessCompositorWidget* AsHeadlessCompositorWidget() {
+ return nullptr;
+ }
+};
+
+// Platforms that support out-of-process widgets.
+#if defined(XP_WIN) || defined(MOZ_X11) || defined(MOZ_WIDGET_ANDROID) || \
+ defined(MOZ_WAYLAND)
+// CompositorWidgetParent should implement CompositorWidget and
+// PCompositorWidgetParent.
+class CompositorWidgetParent;
+
+// CompositorWidgetChild should implement CompositorWidgetDelegate and
+// PCompositorWidgetChild.
+class CompositorWidgetChild;
+
+# define MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING
+#endif
+
+class WidgetRenderingContext {
+ public:
+#if defined(XP_MACOSX)
+ gl::GLContext* mGL = nullptr;
+#endif
+};
+
+/**
+ * Access to a widget from the compositor is restricted to these methods.
+ */
+class CompositorWidget {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::widget::CompositorWidget)
+
+ /**
+ * Create an in-process compositor widget. aWidget may be ignored if the
+ * platform does not require it.
+ */
+ static RefPtr<CompositorWidget> CreateLocal(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsIWidget* aWidget);
+
+ /**
+ * Called before rendering using OMTC. Returns false when the widget is
+ * not ready to be rendered (for example while the window is closed).
+ *
+ * Always called from the compositing thread, which may be the main-thread if
+ * OMTC is not enabled.
+ */
+ virtual bool PreRender(WidgetRenderingContext* aContext) { return true; }
+
+ /**
+ * Called after rendering using OMTC. Not called when rendering was
+ * cancelled by a negative return value from PreRender.
+ *
+ * Always called from the compositing thread, which may be the main-thread if
+ * OMTC is not enabled.
+ */
+ virtual void PostRender(WidgetRenderingContext* aContext) {}
+
+ /**
+ * Called before the first composite. If the result is non-null, one or more
+ * native layers will be placed on the window and used for compositing.
+ * When native layers are used, StartRemoteDrawing(InRegion) and
+ * EndRemoteDrawing(InRegion) will not be called.
+ */
+ virtual RefPtr<layers::NativeLayerRoot> GetNativeLayerRoot() {
+ return nullptr;
+ }
+
+ /**
+ * Return a DrawTarget for the window which can be composited into.
+ *
+ * Only called if GetNativeLayerRoot() returns nullptr.
+ * Called by BasicCompositor on the compositor thread for OMTC drawing
+ * before each composition (unless there's a native layer root).
+ *
+ * The window may specify its buffer mode. If unspecified, it is assumed
+ * to require double-buffering.
+ */
+ virtual already_AddRefed<gfx::DrawTarget> StartRemoteDrawing();
+ virtual already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) {
+ return StartRemoteDrawing();
+ }
+
+ /**
+ * Ensure that what was painted into the DrawTarget returned from
+ * StartRemoteDrawing reaches the screen.
+ *
+ * Called by BasicCompositor on the compositor thread for OMTC drawing
+ * after each composition for which StartRemoteDrawing(InRegion) was called.
+ */
+ virtual void EndRemoteDrawing() {}
+ virtual void EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget,
+ const LayoutDeviceIntRegion& aInvalidRegion) {
+ EndRemoteDrawing();
+ }
+
+ /**
+ * Return true when it is better to defer EndRemoteDrawing().
+ *
+ * Called by BasicCompositor on the compositor thread for OMTC drawing
+ * after each composition.
+ */
+ virtual bool NeedsToDeferEndRemoteDrawing() { return false; }
+
+ /**
+ * Some widgets (namely Gtk) may need clean up underlying surface
+ * before painting to draw transparent objects correctly. Return
+ * the transparent region where this clearing is required.
+ */
+ virtual LayoutDeviceIntRegion GetTransparentRegion();
+
+ /**
+ * Called when shutting down the LayerManager to clean-up any cached
+ * resources.
+ *
+ * Always called from the compositing thread.
+ */
+ virtual void CleanupWindowEffects() {}
+
+ /**
+ * A hook for the widget to prepare a Compositor, during the latter's
+ * initialization.
+ *
+ * If this method returns true, it means that the widget will be able to
+ * present frames from the compoositor.
+ *
+ * Returning false will cause the compositor's initialization to fail, and
+ * a different compositor backend will be used (if any).
+ */
+ virtual bool InitCompositor(layers::Compositor* aCompositor) { return true; }
+
+ /**
+ * A hook that is ran whenever composition is resumed.
+ *
+ * This is called from CompositorBridgeParent::ResumeComposition,
+ * immediately prior to webrender being resumed.
+ *
+ * Returns true if composition can be successfully resumed, else false.
+ */
+ virtual bool OnResumeComposition() { return true; }
+
+ /**
+ * Return the size of the drawable area of the widget.
+ */
+ virtual LayoutDeviceIntSize GetClientSize() = 0;
+
+ /**
+ * Return the internal format of the default framebuffer for this
+ * widget.
+ */
+ virtual uint32_t GetGLFrameBufferFormat();
+
+ /*
+ * Access the underlying nsIWidget. This method will be removed when the
+ * compositor no longer depends on nsIWidget on any platform.
+ */
+ virtual nsIWidget* RealWidget() = 0;
+
+ /**
+ * Clean up any resources used by Start/EndRemoteDrawing.
+ *
+ * Called by BasicCompositor on the compositor thread for OMTC drawing
+ * when the compositor is destroyed.
+ */
+ virtual void CleanupRemoteDrawing();
+
+ /**
+ * Return a key that can represent the widget object round-trip across the
+ * CompositorBridge channel. This only needs to be implemented on GTK and
+ * Windows.
+ *
+ * The key must be the nsIWidget pointer cast to a uintptr_t. See
+ * CompositorBridgeChild::RecvHideAllPlugins and
+ * CompositorBridgeParent::SendHideAllPlugins.
+ */
+ virtual uintptr_t GetWidgetKey() { return 0; }
+
+ /**
+ * Create a backbuffer for the software compositor.
+ */
+ virtual already_AddRefed<gfx::DrawTarget> GetBackBufferDrawTarget(
+ gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect,
+ bool* aOutIsCleared);
+
+ /**
+ * Ensure end of composition to back buffer.
+ *
+ * Called by BasicCompositor on the compositor thread for OMTC drawing
+ * after each composition to back buffer.
+ */
+ virtual already_AddRefed<gfx::SourceSurface> EndBackBufferDrawing();
+
+ /**
+ * Observe or unobserve vsync.
+ */
+ virtual void ObserveVsync(VsyncObserver* aObserver) = 0;
+
+ /**
+ * Get the compositor options for the compositor associated with this
+ * CompositorWidget.
+ */
+ const layers::CompositorOptions& GetCompositorOptions() { return mOptions; }
+
+ /**
+ * Return true if the window is hidden and should not be composited.
+ */
+ virtual bool IsHidden() const { return false; }
+
+ /**
+ * This is only used by out-of-process compositors.
+ */
+ virtual RefPtr<VsyncObserver> GetVsyncObserver() const;
+
+ virtual WinCompositorWidget* AsWindows() { return nullptr; }
+ virtual GtkCompositorWidget* AsGTK() { return nullptr; }
+ virtual AndroidCompositorWidget* AsAndroid() { return nullptr; }
+
+ /**
+ * Return the platform-specific delegate for the widget, if any.
+ */
+ virtual CompositorWidgetDelegate* AsDelegate() { return nullptr; }
+
+ protected:
+ explicit CompositorWidget(const layers::CompositorOptions& aOptions);
+ virtual ~CompositorWidget();
+
+ // Back buffer of BasicCompositor
+ RefPtr<gfx::DrawTarget> mLastBackBuffer;
+
+ layers::CompositorOptions mOptions;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/ContentCache.cpp b/widget/ContentCache.cpp
new file mode 100644
index 0000000000..b6093fb3f7
--- /dev/null
+++ b/widget/ContentCache.cpp
@@ -0,0 +1,2011 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 "ContentCache.h"
+
+#include <utility>
+
+#include "IMEData.h"
+#include "TextEvents.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Logging.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "nsExceptionHandler.h"
+#include "nsIWidget.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+using namespace dom;
+using namespace widget;
+
+static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; }
+
+static const char* GetNotificationName(const IMENotification* aNotification) {
+ if (!aNotification) {
+ return "Not notification";
+ }
+ return ToChar(aNotification->mMessage);
+}
+
+/*****************************************************************************
+ * mozilla::ContentCache
+ *****************************************************************************/
+
+LazyLogModule sContentCacheLog("ContentCacheWidgets");
+
+bool ContentCache::IsValid() const {
+ if (mText.isNothing()) {
+ // mSelection and mCaret depend on mText.
+ if (NS_WARN_IF(mSelection.isSome()) || NS_WARN_IF(mCaret.isSome())) {
+ return false;
+ }
+ } else {
+ // mSelection depends on mText.
+ if (mSelection.isSome() && NS_WARN_IF(!mSelection->IsValidIn(*mText))) {
+ return false;
+ }
+
+ // mCaret depends on mSelection.
+ if (mCaret.isSome() &&
+ (NS_WARN_IF(mSelection.isNothing()) ||
+ NS_WARN_IF(!mSelection->mHasRange) ||
+ NS_WARN_IF(mSelection->StartOffset() != mCaret->Offset()))) {
+ return false;
+ }
+ }
+
+ // mTextRectArray stores character rects around composition string.
+ // Note that even if we fail to collect the rects, we may keep storing
+ // mCompositionStart.
+ if (mTextRectArray.isSome()) {
+ if (NS_WARN_IF(mCompositionStart.isNothing())) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void ContentCache::AssertIfInvalid() const {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (IsValid()) {
+ return;
+ }
+
+ // This text will appear in the crash reports without any permissions.
+ // Do not use `ToString` here to avoid to expose unexpected data with
+ // changing the type or `operator<<()`.
+ nsPrintfCString info(
+ "ContentCache={ mText=%s, mSelection=%s, mCaret=%s, mTextRectArray=%s, "
+ "mCompositionStart=%s }\n",
+ // Don't expose mText.ref() value for protecting the user's privacy.
+ mText.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ Length()=%zu }", mText->Length()).get(),
+ mSelection.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ mAnchor=%u, mFocus=%u }", mSelection->mAnchor,
+ mSelection->mFocus)
+ .get(),
+ mCaret.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ mOffset=%u }", mCaret->mOffset).get(),
+ mTextRectArray.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ Length()=%u }", mTextRectArray->Length()).get(),
+ mCompositionStart.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("%u", mCompositionStart.value()).get());
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(false, "Invalid ContentCache data");
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+}
+
+/*****************************************************************************
+ * mozilla::ContentCacheInChild
+ *****************************************************************************/
+
+void ContentCacheInChild::Clear() {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info, ("0x%p Clear()", this));
+
+ mCompositionStart.reset();
+ mLastCommit.reset();
+ mText.reset();
+ mSelection.reset();
+ mFirstCharRect.SetEmpty();
+ mCaret.reset();
+ mTextRectArray.reset();
+ mLastCommitStringTextRectArray.reset();
+ mEditorRect.SetEmpty();
+}
+
+void ContentCacheInChild::OnCompositionEvent(
+ const WidgetCompositionEvent& aCompositionEvent) {
+ if (aCompositionEvent.CausesDOMCompositionEndEvent()) {
+ RefPtr<TextComposition> composition =
+ IMEStateManager::GetTextCompositionFor(aCompositionEvent.mWidget);
+ if (composition) {
+ nsAutoString lastCommitString;
+ if (aCompositionEvent.mMessage == eCompositionCommitAsIs) {
+ lastCommitString = composition->CommitStringIfCommittedAsIs();
+ } else {
+ lastCommitString = aCompositionEvent.mData;
+ }
+ // We don't need to store canceling information because this is required
+ // by undoing of last commit (Kakutei-Undo of Japanese IME).
+ if (!lastCommitString.IsEmpty()) {
+ mLastCommit = Some(OffsetAndData<uint32_t>(
+ composition->NativeOffsetOfStartComposition(), lastCommitString));
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p OnCompositionEvent(), stored last composition string data "
+ "(aCompositionEvent={ mMessage=%s, mData=\"%s\"}, mLastCommit=%s)",
+ this, ToChar(aCompositionEvent.mMessage),
+ PrintStringDetail(
+ aCompositionEvent.mData,
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get(),
+ ToString(mLastCommit).c_str()));
+ return;
+ }
+ }
+ }
+ if (mLastCommit.isSome()) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p OnCompositionEvent(), resetting the last composition string "
+ "data (aCompositionEvent={ mMessage=%s, mData=\"%s\"}, "
+ "mLastCommit=%s)",
+ this, ToChar(aCompositionEvent.mMessage),
+ PrintStringDetail(aCompositionEvent.mData,
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get(),
+ ToString(mLastCommit).c_str()));
+ mLastCommit.reset();
+ }
+}
+
+bool ContentCacheInChild::CacheAll(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheAll(aWidget=0x%p, aNotification=%s)", this, aWidget,
+ GetNotificationName(aNotification)));
+
+ const bool textCached = CacheText(aWidget, aNotification);
+ const bool editorRectCached = CacheEditorRect(aWidget, aNotification);
+ AssertIfInvalid();
+ return (textCached || editorRectCached) && IsValid();
+}
+
+bool ContentCacheInChild::CacheSelection(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheSelection(aWidget=0x%p, aNotification=%s), mText=%s", this,
+ aWidget, GetNotificationName(aNotification),
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get()));
+
+ mSelection.reset();
+ mCaret.reset();
+
+ if (mText.isNothing()) {
+ return false;
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ aWidget);
+ aWidget->DispatchEvent(&querySelectedTextEvent, status);
+ if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheSelection(), FAILED, couldn't retrieve the selected text",
+ this));
+ // XXX Allowing selection-independent character rects makes things
+ // complicated in the parent...
+ }
+ // ContentCache should store only editable content. Therefore, if current
+ // selection root is not editable, we don't need to store the selection, i.e.,
+ // let's treat it as there is no selection. However, if we already have
+ // previously editable text, let's store the selection even if it becomes
+ // uneditable because not doing so would create odd situation. E.g., IME may
+ // fail only querying selection after succeeded querying text.
+ else if (NS_WARN_IF(!querySelectedTextEvent.mReply->mIsEditableContent)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheSelection(), FAILED, editable content had already been "
+ "blurred",
+ this));
+ AssertIfInvalid();
+ return false;
+ } else {
+ mSelection.emplace(querySelectedTextEvent);
+ }
+
+ return (CacheCaretAndTextRects(aWidget, aNotification) ||
+ querySelectedTextEvent.Succeeded()) &&
+ IsValid();
+}
+
+bool ContentCacheInChild::CacheCaret(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ mCaret.reset();
+
+ if (mSelection.isNothing()) {
+ return false;
+ }
+
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheCaret(aWidget=0x%p, aNotification=%s)", this, aWidget,
+ GetNotificationName(aNotification)));
+
+ if (mSelection->mHasRange) {
+ // XXX Should be mSelection.mFocus?
+ const uint32_t offset = mSelection->StartOffset();
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWidget);
+ queryCaretRectEvent.InitForQueryCaretRect(offset);
+ aWidget->DispatchEvent(&queryCaretRectEvent, status);
+ if (NS_WARN_IF(queryCaretRectEvent.Failed())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheCaret(), FAILED, couldn't retrieve the caret rect "
+ "at offset=%u",
+ this, offset));
+ return false;
+ }
+ mCaret.emplace(offset, queryCaretRectEvent.mReply->mRect);
+ }
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheCaret(), Succeeded, mSelection=%s, mCaret=%s", this,
+ ToString(mSelection).c_str(), ToString(mCaret).c_str()));
+ AssertIfInvalid();
+ return IsValid();
+}
+
+bool ContentCacheInChild::CacheEditorRect(
+ nsIWidget* aWidget, const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheEditorRect(aWidget=0x%p, aNotification=%s)", this,
+ aWidget, GetNotificationName(aNotification)));
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, aWidget);
+ aWidget->DispatchEvent(&queryEditorRectEvent, status);
+ if (NS_WARN_IF(queryEditorRectEvent.Failed())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheEditorRect(), FAILED, couldn't retrieve the editor rect",
+ this));
+ return false;
+ }
+ // ContentCache should store only editable content. Therefore, if current
+ // selection root is not editable, we don't need to store the editor rect,
+ // i.e., let's treat it as there is no focused editor.
+ if (NS_WARN_IF(!queryEditorRectEvent.mReply->mIsEditableContent)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheText(), FAILED, editable content had already been "
+ "blurred",
+ this));
+ return false;
+ }
+ mEditorRect = queryEditorRectEvent.mReply->mRect;
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheEditorRect(), Succeeded, mEditorRect=%s", this,
+ ToString(mEditorRect).c_str()));
+ return true;
+}
+
+bool ContentCacheInChild::CacheCaretAndTextRects(
+ nsIWidget* aWidget, const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheCaretAndTextRects(aWidget=0x%p, aNotification=%s)", this,
+ aWidget, GetNotificationName(aNotification)));
+
+ const bool caretCached = CacheCaret(aWidget, aNotification);
+ const bool textRectsCached = CacheTextRects(aWidget, aNotification);
+ AssertIfInvalid();
+ return (caretCached || textRectsCached) && IsValid();
+}
+
+bool ContentCacheInChild::CacheText(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheText(aWidget=0x%p, aNotification=%s)", this, aWidget,
+ GetNotificationName(aNotification)));
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ aWidget);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ aWidget->DispatchEvent(&queryTextContentEvent, status);
+ if (NS_WARN_IF(queryTextContentEvent.Failed())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheText(), FAILED, couldn't retrieve whole text", this));
+ mText.reset();
+ }
+ // ContentCache should store only editable content. Therefore, if current
+ // selection root is not editable, we don't need to store the text, i.e.,
+ // let's treat it as there is no editable text.
+ else if (NS_WARN_IF(!queryTextContentEvent.mReply->mIsEditableContent)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheText(), FAILED, editable content had already been "
+ "blurred",
+ this));
+ mText.reset();
+ } else {
+ mText = Some(nsString(queryTextContentEvent.mReply->DataRef()));
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheText(), Succeeded, mText=%s", this,
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor)
+ .get()));
+ }
+
+ // Forget last commit range if string in the range is different from the
+ // last commit string.
+ if (mLastCommit.isSome() &&
+ (mText.isNothing() ||
+ nsDependentSubstring(mText.ref(), mLastCommit->StartOffset(),
+ mLastCommit->Length()) != mLastCommit->DataRef())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Debug,
+ ("0x%p CacheText(), resetting the last composition string data "
+ "(mLastCommit=%s, current string=\"%s\")",
+ this, ToString(mLastCommit).c_str(),
+ PrintStringDetail(
+ nsDependentSubstring(mText.ref(), mLastCommit->StartOffset(),
+ mLastCommit->Length()),
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get()));
+ mLastCommit.reset();
+ }
+
+ // If we fail to get editable text content, it must mean that there is no
+ // focused element anymore or focused element is not editable. In this case,
+ // we should not get selection of non-editable content
+ if (MOZ_UNLIKELY(mText.isNothing())) {
+ mSelection.reset();
+ mCaret.reset();
+ mTextRectArray.reset();
+ AssertIfInvalid();
+ return false;
+ }
+
+ return CacheSelection(aWidget, aNotification);
+}
+
+bool ContentCacheInChild::QueryCharRect(nsIWidget* aWidget, uint32_t aOffset,
+ LayoutDeviceIntRect& aCharRect) const {
+ aCharRect.SetEmpty();
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, aWidget);
+ queryTextRectEvent.InitForQueryTextRect(aOffset, 1);
+ aWidget->DispatchEvent(&queryTextRectEvent, status);
+ if (NS_WARN_IF(queryTextRectEvent.Failed())) {
+ return false;
+ }
+ aCharRect = queryTextRectEvent.mReply->mRect;
+
+ // Guarantee the rect is not empty.
+ if (NS_WARN_IF(!aCharRect.Height())) {
+ aCharRect.SetHeight(1);
+ }
+ if (NS_WARN_IF(!aCharRect.Width())) {
+ aCharRect.SetWidth(1);
+ }
+ return true;
+}
+
+bool ContentCacheInChild::QueryCharRectArray(nsIWidget* aWidget,
+ uint32_t aOffset, uint32_t aLength,
+ RectArray& aCharRectArray) const {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextRectsEvent(true, eQueryTextRectArray,
+ aWidget);
+ queryTextRectsEvent.InitForQueryTextRectArray(aOffset, aLength);
+ aWidget->DispatchEvent(&queryTextRectsEvent, status);
+ if (NS_WARN_IF(queryTextRectsEvent.Failed())) {
+ aCharRectArray.Clear();
+ return false;
+ }
+ aCharRectArray = std::move(queryTextRectsEvent.mReply->mRectArray);
+ return true;
+}
+
+bool ContentCacheInChild::CacheTextRects(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheTextRects(aWidget=0x%p, aNotification=%s), mCaret=%s", this,
+ aWidget, GetNotificationName(aNotification), ToString(mCaret).c_str()));
+
+ if (mSelection.isSome()) {
+ mSelection->ClearRects();
+ }
+
+ // Retrieve text rects in composition string if there is.
+ RefPtr<TextComposition> textComposition =
+ IMEStateManager::GetTextCompositionFor(aWidget);
+ if (textComposition) {
+ // mCompositionStart may be updated by some composition event handlers.
+ // So, let's update it with the latest information.
+ mCompositionStart = Some(textComposition->NativeOffsetOfStartComposition());
+ // Note that TextComposition::String() may not be modified here because
+ // it's modified after all edit action listeners are performed but this
+ // is called while some of them are performed.
+ // FYI: For supporting IME which commits composition and restart new
+ // composition immediately, we should cache next character of current
+ // composition too.
+ uint32_t length = textComposition->LastData().Length() + 1;
+ mTextRectArray = Some(TextRectArray(mCompositionStart.value()));
+ if (NS_WARN_IF(!QueryCharRectArray(aWidget, mTextRectArray->mStart, length,
+ mTextRectArray->mRects))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve text rect array of the composition string",
+ this));
+ mTextRectArray.reset();
+ }
+ } else {
+ mCompositionStart.reset();
+ mTextRectArray.reset();
+ }
+
+ if (mSelection.isSome()) {
+ // Set mSelection->mAnchorCharRects
+ // If we've already have the rect in mTextRectArray, save the query cost.
+ if (mSelection->mHasRange && mTextRectArray.isSome() &&
+ mTextRectArray->IsOffsetInRange(mSelection->mAnchor) &&
+ (!mSelection->mAnchor ||
+ mTextRectArray->IsOffsetInRange(mSelection->mAnchor - 1))) {
+ mSelection->mAnchorCharRects[eNextCharRect] =
+ mTextRectArray->GetRect(mSelection->mAnchor);
+ if (mSelection->mAnchor) {
+ mSelection->mAnchorCharRects[ePrevCharRect] =
+ mTextRectArray->GetRect(mSelection->mAnchor - 1);
+ }
+ }
+ // Otherwise, get it from content even if there is no selection ranges.
+ else {
+ RectArray rects;
+ const uint32_t startOffset = mSelection->mHasRange && mSelection->mAnchor
+ ? mSelection->mAnchor - 1u
+ : 0u;
+ const uint32_t length =
+ mSelection->mHasRange && mSelection->mAnchor ? 2u : 1u;
+ if (NS_WARN_IF(
+ !QueryCharRectArray(aWidget, startOffset, length, rects))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, couldn't retrieve text rect "
+ "array around the selection anchor (%s)",
+ this,
+ mSelection ? ToString(mSelection->mAnchor).c_str() : "Nothing"));
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty());
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mAnchorCharRects[eNextCharRect].IsEmpty());
+ } else if (rects.Length()) {
+ if (rects.Length() > 1) {
+ mSelection->mAnchorCharRects[ePrevCharRect] = rects[0];
+ mSelection->mAnchorCharRects[eNextCharRect] = rects[1];
+ } else {
+ mSelection->mAnchorCharRects[eNextCharRect] = rects[0];
+ MOZ_ASSERT(mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty());
+ }
+ }
+ }
+
+ // Set mSelection->mFocusCharRects
+ // If selection is collapsed (including no selection case), the focus char
+ // rects are same as the anchor char rects so that we can just copy them.
+ if (mSelection->IsCollapsed()) {
+ mSelection->mFocusCharRects[0] = mSelection->mAnchorCharRects[0];
+ mSelection->mFocusCharRects[1] = mSelection->mAnchorCharRects[1];
+ }
+ // If the selection range is in mTextRectArray, save the query cost.
+ else if (mTextRectArray.isSome() &&
+ mTextRectArray->IsOffsetInRange(mSelection->mFocus) &&
+ (!mSelection->mFocus ||
+ mTextRectArray->IsOffsetInRange(mSelection->mFocus - 1))) {
+ MOZ_ASSERT(mSelection->mHasRange);
+ mSelection->mFocusCharRects[eNextCharRect] =
+ mTextRectArray->GetRect(mSelection->mFocus);
+ if (mSelection->mFocus) {
+ mSelection->mFocusCharRects[ePrevCharRect] =
+ mTextRectArray->GetRect(mSelection->mFocus - 1);
+ }
+ }
+ // Otherwise, including no selection range cases, need to query the rects.
+ else {
+ MOZ_ASSERT(mSelection->mHasRange);
+ RectArray rects;
+ const uint32_t startOffset =
+ mSelection->mFocus ? mSelection->mFocus - 1u : 0u;
+ const uint32_t length = mSelection->mFocus ? 2u : 1u;
+ if (NS_WARN_IF(
+ !QueryCharRectArray(aWidget, startOffset, length, rects))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, couldn't retrieve text rect "
+ "array around the selection focus (%s)",
+ this,
+ mSelection ? ToString(mSelection->mFocus).c_str() : "Nothing"));
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mFocusCharRects[ePrevCharRect].IsEmpty());
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mFocusCharRects[eNextCharRect].IsEmpty());
+ } else if (NS_WARN_IF(mSelection.isNothing())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, mSelection was reset during "
+ "the call of QueryCharRectArray",
+ this));
+ } else {
+ if (rects.Length() > 1) {
+ mSelection->mFocusCharRects[ePrevCharRect] = rects[0];
+ mSelection->mFocusCharRects[eNextCharRect] = rects[1];
+ } else if (rects.Length()) {
+ mSelection->mFocusCharRects[eNextCharRect] = rects[0];
+ MOZ_ASSERT(mSelection->mFocusCharRects[ePrevCharRect].IsEmpty());
+ }
+ }
+ }
+ }
+
+ // If there is a non-collapsed selection range, let's query the whole selected
+ // text rect. Note that the result cannot be computed from first character
+ // rect and last character rect of the selection because they both may be in
+ // middle of different line.
+ if (mSelection.isSome() && mSelection->mHasRange &&
+ !mSelection->IsCollapsed()) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, aWidget);
+ queryTextRectEvent.InitForQueryTextRect(mSelection->StartOffset(),
+ mSelection->Length());
+ aWidget->DispatchEvent(&queryTextRectEvent, status);
+ if (NS_WARN_IF(queryTextRectEvent.Failed())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve text rect of whole selected text",
+ this));
+ } else {
+ mSelection->mRect = queryTextRectEvent.mReply->mRect;
+ }
+ }
+
+ // Even if there is no selection range, we should have the first character
+ // rect for the last resort of suggesting position of IME UI.
+ if (mSelection.isSome() && mSelection->mHasRange && !mSelection->mFocus) {
+ mFirstCharRect = mSelection->mFocusCharRects[eNextCharRect];
+ } else if (mSelection.isSome() && mSelection->mHasRange &&
+ mSelection->mFocus == 1) {
+ mFirstCharRect = mSelection->mFocusCharRects[ePrevCharRect];
+ } else if (mSelection.isSome() && mSelection->mHasRange &&
+ !mSelection->mAnchor) {
+ mFirstCharRect = mSelection->mAnchorCharRects[eNextCharRect];
+ } else if (mSelection.isSome() && mSelection->mHasRange &&
+ mSelection->mAnchor == 1) {
+ mFirstCharRect = mSelection->mFocusCharRects[ePrevCharRect];
+ } else if (mTextRectArray.isSome() && mTextRectArray->IsOffsetInRange(0u)) {
+ mFirstCharRect = mTextRectArray->GetRect(0u);
+ } else {
+ LayoutDeviceIntRect charRect;
+ if (MOZ_UNLIKELY(NS_WARN_IF(!QueryCharRect(aWidget, 0, charRect)))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve first char rect",
+ this));
+ mFirstCharRect.SetEmpty();
+ } else {
+ mFirstCharRect = charRect;
+ }
+ }
+
+ // Finally, let's cache the last commit string's character rects until
+ // selection change or something other editing because user may reconvert
+ // or undo the last commit. Then, IME requires the character rects for
+ // positioning their UI.
+ if (mLastCommit.isSome()) {
+ mLastCommitStringTextRectArray =
+ Some(TextRectArray(mLastCommit->StartOffset()));
+ if (mLastCommit->Length() == 1 && mSelection.isSome() &&
+ mSelection->mHasRange &&
+ mSelection->mAnchor - 1 == mLastCommit->StartOffset() &&
+ !mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty()) {
+ mLastCommitStringTextRectArray->mRects.AppendElement(
+ mSelection->mAnchorCharRects[ePrevCharRect]);
+ } else if (NS_WARN_IF(!QueryCharRectArray(
+ aWidget, mLastCommit->StartOffset(), mLastCommit->Length(),
+ mLastCommitStringTextRectArray->mRects))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve text rect array of the last commit string",
+ this));
+ mLastCommitStringTextRectArray.reset();
+ mLastCommit.reset();
+ }
+ MOZ_ASSERT((mLastCommitStringTextRectArray.isSome()
+ ? mLastCommitStringTextRectArray->mRects.Length()
+ : 0) == (mLastCommit.isSome() ? mLastCommit->Length() : 0));
+ } else {
+ mLastCommitStringTextRectArray.reset();
+ }
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheTextRects(), Succeeded, "
+ "mText=%s, mTextRectArray=%s, mSelection=%s, "
+ "mFirstCharRect=%s, mLastCommitStringTextRectArray=%s",
+ this,
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get(),
+ ToString(mTextRectArray).c_str(), ToString(mSelection).c_str(),
+ ToString(mFirstCharRect).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+ AssertIfInvalid();
+ return IsValid();
+}
+
+bool ContentCacheInChild::SetSelection(
+ nsIWidget* aWidget,
+ const IMENotification::SelectionChangeDataBase& aSelectionChangeData) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p SetSelection(aSelectionChangeData=%s), mText=%s", this,
+ ToString(aSelectionChangeData).c_str(),
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get()));
+
+ if (MOZ_UNLIKELY(mText.isNothing())) {
+ return false;
+ }
+
+ mSelection = Some(Selection(aSelectionChangeData));
+
+ if (mLastCommit.isSome()) {
+ // Forget last commit string range if selection is not collapsed
+ // at end of the last commit string.
+ if (!mSelection->mHasRange || !mSelection->IsCollapsed() ||
+ mSelection->mAnchor != mLastCommit->EndOffset()) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p SetSelection(), forgetting last commit composition data "
+ "(mSelection=%s, mLastCommit=%s)",
+ this, ToString(mSelection).c_str(), ToString(mLastCommit).c_str()));
+ mLastCommit.reset();
+ }
+ }
+
+ CacheCaret(aWidget);
+ CacheTextRects(aWidget);
+
+ return mSelection.isSome() && IsValid();
+}
+
+/*****************************************************************************
+ * mozilla::ContentCacheInParent
+ *****************************************************************************/
+
+ContentCacheInParent::ContentCacheInParent(BrowserParent& aBrowserParent)
+ : mBrowserParent(aBrowserParent),
+ mCommitStringByRequest(nullptr),
+ mPendingCommitLength(0),
+ mIsChildIgnoringCompositionEvents(false) {}
+
+void ContentCacheInParent::AssignContent(const ContentCache& aOther,
+ nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_DIAGNOSTIC_ASSERT(aOther.IsValid());
+
+ mText = aOther.mText;
+ mSelection = aOther.mSelection;
+ mFirstCharRect = aOther.mFirstCharRect;
+ mCaret = aOther.mCaret;
+ mTextRectArray = aOther.mTextRectArray;
+ mLastCommitStringTextRectArray = aOther.mLastCommitStringTextRectArray;
+ mEditorRect = aOther.mEditorRect;
+
+ // Only when there is one composition, the TextComposition instance in this
+ // process is managing the composition in the remote process. Therefore,
+ // we shouldn't update composition start offset of TextComposition with
+ // old composition which is still being handled by the child process.
+ if (WidgetHasComposition() && mHandlingCompositions.Length() == 1 &&
+ mCompositionStart.isSome()) {
+ IMEStateManager::MaybeStartOffsetUpdatedInChild(aWidget,
+ mCompositionStart.value());
+ }
+
+ // When this instance allows to query content relative to composition string,
+ // we should modify mCompositionStart with the latest information in the
+ // remote process because now we have the information around the composition
+ // string.
+ mCompositionStartInChild = aOther.mCompositionStart;
+ if (WidgetHasComposition() || HasPendingCommit()) {
+ if (mCompositionStartInChild.isSome()) {
+ if (mCompositionStart.valueOr(UINT32_MAX) !=
+ mCompositionStartInChild.value()) {
+ mCompositionStart = mCompositionStartInChild;
+ mPendingCommitLength = 0;
+ }
+ } else if (mCompositionStart.isSome() && mSelection.isSome() &&
+ mSelection->mHasRange &&
+ mCompositionStart.value() != mSelection->StartOffset()) {
+ mCompositionStart = Some(mSelection->StartOffset());
+ mPendingCommitLength = 0;
+ }
+ }
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p AssignContent(aNotification=%s), "
+ "Succeeded, mText=%s, mSelection=%s, mFirstCharRect=%s, "
+ "mCaret=%s, mTextRectArray=%s, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, mCompositionStart=%s, "
+ "mPendingCommitLength=%u, mEditorRect=%s, "
+ "mLastCommitStringTextRectArray=%s",
+ this, GetNotificationName(aNotification),
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get(),
+ ToString(mSelection).c_str(), ToString(mFirstCharRect).c_str(),
+ ToString(mCaret).c_str(), ToString(mTextRectArray).c_str(),
+ GetBoolName(WidgetHasComposition()), mHandlingCompositions.Length(),
+ ToString(mCompositionStart).c_str(), mPendingCommitLength,
+ ToString(mEditorRect).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+}
+
+bool ContentCacheInParent::HandleQueryContentEvent(
+ WidgetQueryContentEvent& aEvent, nsIWidget* aWidget) const {
+ MOZ_ASSERT(aWidget);
+
+ // ContentCache doesn't store offset of its start with XP linebreaks.
+ // So, we don't support to query contents relative to composition start
+ // offset with XP linebreaks.
+ if (NS_WARN_IF(!aEvent.mUseNativeLineBreak)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to query with XP "
+ "linebreaks",
+ this));
+ return false;
+ }
+
+ if (NS_WARN_IF(!aEvent.mInput.IsValidOffset())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to invalid offset", this));
+ return false;
+ }
+
+ if (NS_WARN_IF(!aEvent.mInput.IsValidEventMessage(aEvent.mMessage))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to invalid event message",
+ this));
+ return false;
+ }
+
+ bool isRelativeToInsertionPoint = aEvent.mInput.mRelativeToInsertionPoint;
+ if (isRelativeToInsertionPoint) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p HandleQueryContentEvent(), "
+ "making offset absolute... aEvent={ mMessage=%s, mInput={ "
+ "mOffset=%" PRId64 ", mLength=%" PRIu32 " } }, "
+ "WidgetHasComposition()=%s, HasPendingCommit()=%s, "
+ "mCompositionStart=%" PRIu32 ", "
+ "mPendingCommitLength=%" PRIu32 ", mSelection=%s",
+ this, ToChar(aEvent.mMessage), aEvent.mInput.mOffset,
+ aEvent.mInput.mLength, GetBoolName(WidgetHasComposition()),
+ GetBoolName(HasPendingCommit()), mCompositionStart.valueOr(UINT32_MAX),
+ mPendingCommitLength, ToString(mSelection).c_str()));
+ if (WidgetHasComposition() || HasPendingCommit()) {
+ if (NS_WARN_IF(mCompositionStart.isNothing()) ||
+ NS_WARN_IF(!aEvent.mInput.MakeOffsetAbsolute(
+ mCompositionStart.value() + mPendingCommitLength))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to "
+ "aEvent.mInput.MakeOffsetAbsolute(mCompositionStart + "
+ "mPendingCommitLength) failure, "
+ "mCompositionStart=%" PRIu32 ", mPendingCommitLength=%" PRIu32 ", "
+ "aEvent={ mMessage=%s, mInput={ mOffset=%" PRId64
+ ", mLength=%" PRIu32 " } }",
+ this, mCompositionStart.valueOr(UINT32_MAX), mPendingCommitLength,
+ ToChar(aEvent.mMessage), aEvent.mInput.mOffset,
+ aEvent.mInput.mLength));
+ return false;
+ }
+ } else if (NS_WARN_IF(mSelection.isNothing())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to mSelection is "
+ "Nothing",
+ this));
+ return false;
+ } else if (NS_WARN_IF(mSelection->mHasRange)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to there is no "
+ "selection range, but the query requested with relative offset "
+ "from selection",
+ this));
+ return false;
+ } else if (NS_WARN_IF(!aEvent.mInput.MakeOffsetAbsolute(
+ mSelection->StartOffset() + mPendingCommitLength))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to "
+ "aEvent.mInput.MakeOffsetAbsolute(mSelection->StartOffset() + "
+ "mPendingCommitLength) failure, mSelection=%s, "
+ "mPendingCommitLength=%" PRIu32 ", aEvent={ mMessage=%s, "
+ "mInput={ mOffset=%" PRId64 ", mLength=%" PRIu32 " } }",
+ this, ToString(mSelection).c_str(), mPendingCommitLength,
+ ToChar(aEvent.mMessage), aEvent.mInput.mOffset,
+ aEvent.mInput.mLength));
+ return false;
+ }
+ }
+
+ switch (aEvent.mMessage) {
+ case eQuerySelectedText:
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ "
+ "mMessage=eQuerySelectedText }, aWidget=0x%p)",
+ this, aWidget));
+ if (MOZ_UNLIKELY(NS_WARN_IF(mSelection.isNothing()))) {
+ // If content cache hasn't been initialized properly, make the query
+ // failed.
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED because mSelection "
+ "is Nothing",
+ this));
+ return false;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mText.isSome());
+ MOZ_DIAGNOSTIC_ASSERT(mSelection->IsValidIn(*mText));
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ if (mSelection->mHasRange) {
+ if (MOZ_LIKELY(mText.isSome())) {
+ aEvent.mReply->mOffsetAndData.emplace(
+ mSelection->StartOffset(),
+ Substring(mText.ref(), mSelection->StartOffset(),
+ mSelection->Length()),
+ OffsetAndDataFor::SelectedString);
+ } else {
+ // TODO: Investigate this case. I find this during
+ // test_mousecapture.xhtml on Linux.
+ aEvent.mReply->mOffsetAndData.emplace(
+ 0u, EmptyString(), OffsetAndDataFor::SelectedString);
+ }
+ }
+ aEvent.mReply->mWritingMode = mSelection->mWritingMode;
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQuerySelectedText, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ case eQueryTextContent: {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ "
+ "mMessage=eQueryTextContent, mInput={ mOffset=%" PRId64
+ ", mLength=%u } }, aWidget=0x%p), mText->Length()=%zu",
+ this, aEvent.mInput.mOffset, aEvent.mInput.mLength, aWidget,
+ mText.isSome() ? mText->Length() : 0u));
+ if (MOZ_UNLIKELY(NS_WARN_IF(mText.isNothing()))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED because "
+ "there is no text data",
+ this));
+ return false;
+ }
+ const uint32_t inputOffset = aEvent.mInput.mOffset;
+ const uint32_t inputEndOffset = std::min<uint32_t>(
+ aEvent.mInput.EndOffset(), mText.isSome() ? mText->Length() : 0u);
+ if (MOZ_UNLIKELY(NS_WARN_IF(inputEndOffset < inputOffset))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED because "
+ "inputOffset=%u is larger than inputEndOffset=%u",
+ this, inputOffset, inputEndOffset));
+ return false;
+ }
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ const nsAString& textInQueriedRange =
+ inputEndOffset > inputOffset
+ ? static_cast<const nsAString&>(Substring(
+ mText.ref(), inputOffset, inputEndOffset - inputOffset))
+ : static_cast<const nsAString&>(EmptyString());
+ aEvent.mReply->mOffsetAndData.emplace(inputOffset, textInQueriedRange,
+ OffsetAndDataFor::EditorString);
+ // TODO: Support font ranges
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryTextContent, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+ case eQueryTextRect: {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent("
+ "aEvent={ mMessage=eQueryTextRect, mInput={ mOffset=%" PRId64
+ ", mLength=%u } }, aWidget=0x%p), mText->Length()=%zu",
+ this, aEvent.mInput.mOffset, aEvent.mInput.mLength, aWidget,
+ mText.isSome() ? mText->Length() : 0u));
+ // Note that if the query is relative to insertion point, the query was
+ // probably requested by native IME. In such case, we should return
+ // non-empty rect since returning failure causes IME showing its window
+ // at odd position.
+ LayoutDeviceIntRect textRect;
+ if (aEvent.mInput.mLength) {
+ if (MOZ_UNLIKELY(NS_WARN_IF(
+ !GetUnionTextRects(aEvent.mInput.mOffset, aEvent.mInput.mLength,
+ isRelativeToInsertionPoint, textRect)))) {
+ // XXX We don't have cache for this request.
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED to get union rect",
+ this));
+ return false;
+ }
+ } else {
+ // If the length is 0, we should return caret rect instead.
+ if (NS_WARN_IF(!GetCaretRect(aEvent.mInput.mOffset,
+ isRelativeToInsertionPoint, textRect))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED to get caret rect",
+ this));
+ return false;
+ }
+ }
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ aEvent.mReply->mRect = textRect;
+ const nsAString& textInQueriedRange =
+ mText.isSome() && aEvent.mInput.mOffset <
+ static_cast<int64_t>(
+ mText.isSome() ? mText->Length() : 0u)
+ ? static_cast<const nsAString&>(
+ Substring(mText.ref(), aEvent.mInput.mOffset,
+ mText->Length() >= aEvent.mInput.EndOffset()
+ ? aEvent.mInput.mLength
+ : UINT32_MAX))
+ : static_cast<const nsAString&>(EmptyString());
+ aEvent.mReply->mOffsetAndData.emplace(aEvent.mInput.mOffset,
+ textInQueriedRange,
+ OffsetAndDataFor::EditorString);
+ // XXX This may be wrong if storing range isn't in the selection range.
+ aEvent.mReply->mWritingMode =
+ mSelection.isSome() ? mSelection->mWritingMode : WritingMode();
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryTextRect mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+ case eQueryCaretRect: {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ mMessage=eQueryCaretRect, "
+ "mInput={ mOffset=%" PRId64
+ " } }, aWidget=0x%p), mText->Length()=%zu",
+ this, aEvent.mInput.mOffset, aWidget,
+ mText.isSome() ? mText->Length() : 0u));
+ // Note that if the query is relative to insertion point, the query was
+ // probably requested by native IME. In such case, we should return
+ // non-empty rect since returning failure causes IME showing its window
+ // at odd position.
+ LayoutDeviceIntRect caretRect;
+ if (NS_WARN_IF(!GetCaretRect(aEvent.mInput.mOffset,
+ isRelativeToInsertionPoint, caretRect))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(),FAILED to get caret rect",
+ this));
+ return false;
+ }
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ aEvent.mReply->mRect = caretRect;
+ aEvent.mReply->mOffsetAndData.emplace(aEvent.mInput.mOffset,
+ EmptyString(),
+ OffsetAndDataFor::SelectedString);
+ // TODO: Set mWritingMode here
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryCaretRect, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+ case eQueryEditorRect:
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ "
+ "mMessage=eQueryEditorRect }, aWidget=0x%p)",
+ this, aWidget));
+ // XXX This query should fail if no editable elmenet has focus. Or,
+ // perhaps, should return rect of the window instead.
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ aEvent.mReply->mRect = mEditorRect;
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryEditorRect, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ default:
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ if (NS_WARN_IF(aEvent.Failed())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to not set enough "
+ "data, aEvent={ mMessage=%s, mReply=%s }",
+ this, ToChar(aEvent.mMessage), ToString(aEvent.mReply).c_str()));
+ return false;
+ }
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=%s, mReply=%s }",
+ this, ToChar(aEvent.mMessage), ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+}
+
+bool ContentCacheInParent::GetTextRect(uint32_t aOffset,
+ bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aTextRect) const {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p GetTextRect(aOffset=%u, aRoundToExistingOffset=%s), "
+ "mTextRectArray=%s, mSelection=%s, mLastCommitStringTextRectArray=%s",
+ this, aOffset, GetBoolName(aRoundToExistingOffset),
+ ToString(mTextRectArray).c_str(), ToString(mSelection).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+
+ if (!aOffset) {
+ NS_WARNING_ASSERTION(!mFirstCharRect.IsEmpty(), "empty rect");
+ aTextRect = mFirstCharRect;
+ return !aTextRect.IsEmpty();
+ }
+ if (mSelection.isSome() && mSelection->mHasRange) {
+ if (aOffset == mSelection->mAnchor) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[eNextCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mAnchorCharRects[eNextCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ if (mSelection->mAnchor && aOffset == mSelection->mAnchor - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mAnchorCharRects[ePrevCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ if (aOffset == mSelection->mFocus) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[eNextCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mFocusCharRects[eNextCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ if (mSelection->mFocus && aOffset == mSelection->mFocus - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[ePrevCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mFocusCharRects[ePrevCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ }
+
+ if (mTextRectArray.isSome() && mTextRectArray->IsOffsetInRange(aOffset)) {
+ aTextRect = mTextRectArray->GetRect(aOffset);
+ return !aTextRect.IsEmpty();
+ }
+
+ if (mLastCommitStringTextRectArray.isSome() &&
+ mLastCommitStringTextRectArray->IsOffsetInRange(aOffset)) {
+ aTextRect = mLastCommitStringTextRectArray->GetRect(aOffset);
+ return !aTextRect.IsEmpty();
+ }
+
+ if (!aRoundToExistingOffset) {
+ aTextRect.SetEmpty();
+ return false;
+ }
+
+ if (mTextRectArray.isNothing() || !mTextRectArray->HasRects()) {
+ // If there are no rects in mTextRectArray, we should refer the start of
+ // the selection if there is because IME must query a char rect around it if
+ // there is no composition.
+ if (mSelection.isNothing()) {
+ // Unfortunately, there is no data about text rect...
+ aTextRect.SetEmpty();
+ return false;
+ }
+ aTextRect = mSelection->StartCharRect();
+ return !aTextRect.IsEmpty();
+ }
+
+ // Although we may have mLastCommitStringTextRectArray here and it must have
+ // previous character rects at selection. However, we should stop using it
+ // because it's stored really short time after commiting a composition.
+ // So, multiple query may return different rect and it may cause flickerling
+ // the IME UI.
+ uint32_t offset = aOffset;
+ if (offset < mTextRectArray->StartOffset()) {
+ offset = mTextRectArray->StartOffset();
+ } else {
+ offset = mTextRectArray->EndOffset() - 1;
+ }
+ aTextRect = mTextRectArray->GetRect(offset);
+ return !aTextRect.IsEmpty();
+}
+
+bool ContentCacheInParent::GetUnionTextRects(
+ uint32_t aOffset, uint32_t aLength, bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aUnionTextRect) const {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p GetUnionTextRects(aOffset=%u, "
+ "aLength=%u, aRoundToExistingOffset=%s), mTextRectArray=%s, "
+ "mSelection=%s, mLastCommitStringTextRectArray=%s",
+ this, aOffset, aLength, GetBoolName(aRoundToExistingOffset),
+ ToString(mTextRectArray).c_str(), ToString(mSelection).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+
+ CheckedInt<uint32_t> endOffset = CheckedInt<uint32_t>(aOffset) + aLength;
+ if (!endOffset.isValid()) {
+ return false;
+ }
+
+ if (mSelection.isSome() && !mSelection->IsCollapsed() &&
+ aOffset == mSelection->StartOffset() && aLength == mSelection->Length()) {
+ NS_WARNING_ASSERTION(!mSelection->mRect.IsEmpty(), "empty rect");
+ aUnionTextRect = mSelection->mRect;
+ return !aUnionTextRect.IsEmpty();
+ }
+
+ if (aLength == 1) {
+ if (!aOffset) {
+ NS_WARNING_ASSERTION(!mFirstCharRect.IsEmpty(), "empty rect");
+ aUnionTextRect = mFirstCharRect;
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (mSelection.isSome() && mSelection->mHasRange) {
+ if (aOffset == mSelection->mAnchor) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[eNextCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mAnchorCharRects[eNextCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (mSelection->mAnchor && aOffset == mSelection->mAnchor - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mAnchorCharRects[ePrevCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (aOffset == mSelection->mFocus) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[eNextCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mFocusCharRects[eNextCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (mSelection->mFocus && aOffset == mSelection->mFocus - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[ePrevCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mFocusCharRects[ePrevCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ }
+ }
+
+ // Even if some text rects are not cached of the queried range,
+ // we should return union rect when the first character's rect is cached
+ // since the first character rect is important and the others are not so
+ // in most cases.
+
+ if (!aOffset && mSelection.isSome() && mSelection->mHasRange &&
+ aOffset != mSelection->mAnchor && aOffset != mSelection->mFocus &&
+ (mTextRectArray.isNothing() ||
+ !mTextRectArray->IsOffsetInRange(aOffset)) &&
+ (mLastCommitStringTextRectArray.isNothing() ||
+ !mLastCommitStringTextRectArray->IsOffsetInRange(aOffset))) {
+ // The first character rect isn't cached.
+ return false;
+ }
+
+ // Use mLastCommitStringTextRectArray only when it overlaps with aOffset
+ // even if aROundToExistingOffset is true for avoiding flickerling IME UI.
+ // See the last comment in GetTextRect() for the detail.
+ if (mLastCommitStringTextRectArray.isSome() &&
+ mLastCommitStringTextRectArray->IsOverlappingWith(aOffset, aLength)) {
+ aUnionTextRect =
+ mLastCommitStringTextRectArray->GetUnionRectAsFarAsPossible(
+ aOffset, aLength, aRoundToExistingOffset);
+ } else {
+ aUnionTextRect.SetEmpty();
+ }
+
+ if (mTextRectArray.isSome() &&
+ ((aRoundToExistingOffset && mTextRectArray->HasRects()) ||
+ mTextRectArray->IsOverlappingWith(aOffset, aLength))) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mTextRectArray->GetUnionRectAsFarAsPossible(
+ aOffset, aLength, aRoundToExistingOffset));
+ }
+
+ if (!aOffset) {
+ aUnionTextRect = aUnionTextRect.Union(mFirstCharRect);
+ }
+ if (mSelection.isSome() && mSelection->mHasRange) {
+ if (aOffset <= mSelection->mAnchor &&
+ mSelection->mAnchor < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mAnchorCharRects[eNextCharRect]);
+ }
+ if (mSelection->mAnchor && aOffset <= mSelection->mAnchor - 1 &&
+ mSelection->mAnchor - 1 < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mAnchorCharRects[ePrevCharRect]);
+ }
+ if (aOffset <= mSelection->mFocus &&
+ mSelection->mFocus < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mFocusCharRects[eNextCharRect]);
+ }
+ if (mSelection->mFocus && aOffset <= mSelection->mFocus - 1 &&
+ mSelection->mFocus - 1 < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mFocusCharRects[ePrevCharRect]);
+ }
+ }
+
+ return !aUnionTextRect.IsEmpty();
+}
+
+bool ContentCacheInParent::GetCaretRect(uint32_t aOffset,
+ bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aCaretRect) const {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p GetCaretRect(aOffset=%u, aRoundToExistingOffset=%s), "
+ "mCaret=%s, mTextRectArray=%s, mSelection=%s, mFirstCharRect=%s",
+ this, aOffset, GetBoolName(aRoundToExistingOffset),
+ ToString(mCaret).c_str(), ToString(mTextRectArray).c_str(),
+ ToString(mSelection).c_str(), ToString(mFirstCharRect).c_str()));
+
+ if (mCaret.isSome() && mCaret->mOffset == aOffset) {
+ aCaretRect = mCaret->mRect;
+ return true;
+ }
+
+ // Guess caret rect from the text rect if it's stored.
+ if (!GetTextRect(aOffset, aRoundToExistingOffset, aCaretRect)) {
+ // There might be previous character rect in the cache. If so, we can
+ // guess the caret rect with it.
+ if (!aOffset ||
+ !GetTextRect(aOffset - 1, aRoundToExistingOffset, aCaretRect)) {
+ aCaretRect.SetEmpty();
+ return false;
+ }
+
+ if (mSelection.isSome() && mSelection->mWritingMode.IsVertical()) {
+ aCaretRect.MoveToY(aCaretRect.YMost());
+ } else {
+ // XXX bidi-unaware.
+ aCaretRect.MoveToX(aCaretRect.XMost());
+ }
+ }
+
+ // XXX This is not bidi aware because we don't cache each character's
+ // direction. However, this is usually used by IME, so, assuming the
+ // character is in LRT context must not cause any problem.
+ if (mSelection.isSome() && mSelection->mWritingMode.IsVertical()) {
+ aCaretRect.SetHeight(mCaret.isSome() ? mCaret->mRect.Height() : 1);
+ } else {
+ aCaretRect.SetWidth(mCaret.isSome() ? mCaret->mRect.Width() : 1);
+ }
+ return true;
+}
+
+bool ContentCacheInParent::OnCompositionEvent(
+ const WidgetCompositionEvent& aCompositionEvent) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p OnCompositionEvent(aCompositionEvent={ "
+ "mMessage=%s, mData=\"%s\", mRanges->Length()=%zu }), "
+ "PendingEventsNeedingAck()=%u, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s, mCommitStringByRequest=0x%p",
+ this, ToChar(aCompositionEvent.mMessage),
+ PrintStringDetail(aCompositionEvent.mData,
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get(),
+ aCompositionEvent.mRanges ? aCompositionEvent.mRanges->Length() : 0,
+ PendingEventsNeedingAck(), GetBoolName(WidgetHasComposition()),
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents), mCommitStringByRequest));
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mDispatchedEventMessages.AppendElement(aCompositionEvent.mMessage);
+#endif // #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ // We must be able to simulate the selection because
+ // we might not receive selection updates in time
+ if (!WidgetHasComposition()) {
+ if (mCompositionStartInChild.isSome()) {
+ // If there is pending composition in the remote process, let's use
+ // its start offset temporarily because this stores a lot of information
+ // around it and the user must look around there, so, showing some UI
+ // around it must make sense.
+ mCompositionStart = mCompositionStartInChild;
+ } else {
+ mCompositionStart = Some(mSelection.isSome() && mSelection->mHasRange
+ ? mSelection->StartOffset()
+ : 0u);
+ }
+ MOZ_ASSERT(aCompositionEvent.mMessage == eCompositionStart);
+ mHandlingCompositions.AppendElement(
+ HandlingCompositionData(aCompositionEvent.mCompositionId));
+ }
+
+ mHandlingCompositions.LastElement().mSentCommitEvent =
+ aCompositionEvent.CausesDOMCompositionEndEvent();
+ MOZ_ASSERT(mHandlingCompositions.LastElement().mCompositionId ==
+ aCompositionEvent.mCompositionId);
+
+ if (!WidgetHasComposition()) {
+ // mCompositionStart will be reset when commit event is completely handled
+ // in the remote process.
+ if (mHandlingCompositions.Length() == 1u) {
+ mPendingCommitLength = aCompositionEvent.mData.Length();
+ }
+ MOZ_ASSERT(HasPendingCommit());
+ } else if (aCompositionEvent.mMessage != eCompositionStart) {
+ mHandlingCompositions.LastElement().mCompositionString =
+ aCompositionEvent.mData;
+ }
+
+ // During REQUEST_TO_COMMIT_COMPOSITION or REQUEST_TO_CANCEL_COMPOSITION,
+ // widget usually sends a eCompositionChange and/or eCompositionCommit event
+ // to finalize or clear the composition, respectively. In this time,
+ // we need to intercept all composition events here and pass the commit
+ // string for returning to the remote process as a result of
+ // RequestIMEToCommitComposition(). Then, eCommitComposition event will
+ // be dispatched with the committed string in the remote process internally.
+ if (mCommitStringByRequest) {
+ if (aCompositionEvent.mMessage == eCompositionCommitAsIs) {
+ *mCommitStringByRequest =
+ mHandlingCompositions.LastElement().mCompositionString;
+ } else {
+ MOZ_ASSERT(aCompositionEvent.mMessage == eCompositionChange ||
+ aCompositionEvent.mMessage == eCompositionCommit);
+ *mCommitStringByRequest = aCompositionEvent.mData;
+ }
+ // We need to wait eCompositionCommitRequestHandled from the remote process
+ // in this case. Therefore, mPendingEventsNeedingAck needs to be
+ // incremented here.
+ if (!WidgetHasComposition()) {
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck++;
+ }
+ return false;
+ }
+
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck++;
+ return true;
+}
+
+void ContentCacheInParent::OnSelectionEvent(
+ const WidgetSelectionEvent& aSelectionEvent) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p OnSelectionEvent(aEvent={ "
+ "mMessage=%s, mOffset=%u, mLength=%u, mReversed=%s, "
+ "mExpandToClusterBoundary=%s, mUseNativeLineBreak=%s }), "
+ "PendingEventsNeedingAck()=%u, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s",
+ this, ToChar(aSelectionEvent.mMessage), aSelectionEvent.mOffset,
+ aSelectionEvent.mLength, GetBoolName(aSelectionEvent.mReversed),
+ GetBoolName(aSelectionEvent.mExpandToClusterBoundary),
+ GetBoolName(aSelectionEvent.mUseNativeLineBreak),
+ PendingEventsNeedingAck(), GetBoolName(WidgetHasComposition()),
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents)));
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ mDispatchedEventMessages.AppendElement(aSelectionEvent.mMessage);
+#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ mPendingSetSelectionEventNeedingAck++;
+}
+
+void ContentCacheInParent::OnEventNeedingAckHandled(nsIWidget* aWidget,
+ EventMessage aMessage,
+ uint32_t aCompositionId) {
+ // This is called when the child process receives WidgetCompositionEvent or
+ // WidgetSelectionEvent.
+
+ HandlingCompositionData* handlingCompositionData =
+ aMessage != eSetSelection ? GetHandlingCompositionData(aCompositionId)
+ : nullptr;
+
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p OnEventNeedingAckHandled(aWidget=0x%p, aMessage=%s, "
+ "aCompositionId=%" PRIu32
+ "), PendingEventsNeedingAck()=%u, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s, handlingCompositionData=0x%p",
+ this, aWidget, ToChar(aMessage), aCompositionId,
+ PendingEventsNeedingAck(), GetBoolName(WidgetHasComposition()),
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents),
+ handlingCompositionData));
+
+ // If we receive composition event messages for older one or invalid one,
+ // we should ignore them.
+ if (NS_WARN_IF(aMessage != eSetSelection && !handlingCompositionData)) {
+ return;
+ }
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ mReceivedEventMessages.AppendElement(aMessage);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ const bool isCommittedInChild =
+ // Commit requester in the remote process has committed the composition.
+ aMessage == eCompositionCommitRequestHandled ||
+ // The commit event has been handled normally in the remote process.
+ (!mIsChildIgnoringCompositionEvents &&
+ WidgetCompositionEvent::IsFollowedByCompositionEnd(aMessage));
+ const bool hasPendingCommit = HasPendingCommit();
+
+ if (isCommittedInChild) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ if (mHandlingCompositions.Length() == 1u) {
+ RemoveUnnecessaryEventMessageLog();
+ }
+
+ if (NS_WARN_IF(aMessage != eCompositionCommitRequestHandled &&
+ !handlingCompositionData->mSentCommitEvent)) {
+ nsPrintfCString info(
+ "\nReceived unexpected commit event message (%s) which we've "
+ "not sent yet\n\n",
+ ToChar(aMessage));
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false,
+ "Received unexpected commit event which has not been sent yet");
+ }
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ // This should not occur, though. If we receive a commit notification for
+ // not the oldest composition, we should forget all older compositions.
+ size_t numberOfOutdatedCompositions = 1u;
+ for (auto& data : mHandlingCompositions) {
+ if (&data == handlingCompositionData) {
+ if (
+ // Don't put the info into the log when we've already sent commit
+ // event because it may be just inserting a character without
+ // composing state, but the remote process may move focus at
+ // eCompositionStart. This may happen with UI of IME to put only
+ // one character, e.g., the default Emoji picker of Windows.
+ !data.mSentCommitEvent &&
+ // In the normal case, only one message should remain, however,
+ // remaining 2 or more messages is also valid, for example, the
+ // remote process may have a composition update listener which
+ // takes a while. Then, we can have multiple pending messages.
+ data.mPendingEventsNeedingAck >= 1u) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ (" NOTE: BrowserParent has %" PRIu32
+ " pending composition messages for the handling composition, "
+ "but before they are handled in the remote process, the active "
+ "composition is commited by a request. "
+ "OnEventNeedingAckHandled() calls for them will be ignored",
+ data.mPendingEventsNeedingAck));
+ }
+ break;
+ }
+ if (MOZ_UNLIKELY(data.mPendingEventsNeedingAck)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Warning,
+ (" BrowserParent has %" PRIu32
+ " pending composition messages for an older composition than "
+ "the handling composition, but it'll be removed because newer "
+ "composition gets comitted in the remote process",
+ data.mPendingEventsNeedingAck));
+ }
+ numberOfOutdatedCompositions++;
+ }
+ mHandlingCompositions.RemoveElementsAt(0u, numberOfOutdatedCompositions);
+ handlingCompositionData = nullptr;
+
+ // Forget pending commit string length if it's handled in the remote
+ // process. Note that this doesn't care too old composition's commit
+ // string because in such case, we cannot return proper information
+ // to IME synchronously.
+ mPendingCommitLength = 0;
+ }
+
+ if (WidgetCompositionEvent::IsFollowedByCompositionEnd(aMessage)) {
+ // After the remote process receives eCompositionCommit(AsIs) event,
+ // it'll restart to handle composition events.
+ mIsChildIgnoringCompositionEvents = false;
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ if (NS_WARN_IF(!hasPendingCommit)) {
+ nsPrintfCString info(
+ "\nThere is no pending comment events but received "
+ "%s message from the remote child\n\n",
+ ToChar(aMessage));
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false,
+ "No pending commit events but received unexpected commit event");
+ }
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ } else if (aMessage == eCompositionCommitRequestHandled && hasPendingCommit) {
+ // If the remote process commits composition synchronously after
+ // requesting commit composition and we've already sent commit composition,
+ // it starts to ignore following composition events until receiving
+ // eCompositionStart event.
+ mIsChildIgnoringCompositionEvents = true;
+ }
+
+ // If neither widget (i.e., IME) nor the remote process has composition,
+ // now, we can forget composition string informations.
+ if (mHandlingCompositions.IsEmpty()) {
+ mCompositionStart.reset();
+ }
+
+ if (handlingCompositionData) {
+ if (NS_WARN_IF(!handlingCompositionData->mPendingEventsNeedingAck)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ nsPrintfCString info(
+ "\nThere is no pending events but received %s "
+ "message from the remote child\n\n",
+ ToChar(aMessage));
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "No pending event message but received unexpected event");
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ } else {
+ handlingCompositionData->mPendingEventsNeedingAck--;
+ }
+ } else if (aMessage == eSetSelection) {
+ if (NS_WARN_IF(!mPendingSetSelectionEventNeedingAck)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ nsAutoCString info(
+ "\nThere is no pending set selection events but received from the "
+ "remote child\n\n");
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "No pending event message but received unexpected event");
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ } else {
+ mPendingSetSelectionEventNeedingAck--;
+ }
+ }
+
+ if (!PendingEventsNeedingAck()) {
+ FlushPendingNotifications(aWidget);
+ }
+}
+
+bool ContentCacheInParent::RequestIMEToCommitComposition(
+ nsIWidget* aWidget, bool aCancel, uint32_t aCompositionId,
+ nsAString& aCommittedString) {
+ HandlingCompositionData* const handlingCompositionData =
+ GetHandlingCompositionData(aCompositionId);
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p RequestToCommitComposition(aWidget=%p, "
+ "aCancel=%s, aCompositionId=%" PRIu32
+ "), mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s, "
+ "IMEStateManager::DoesBrowserParentHaveIMEFocus(&mBrowserParent)=%s, "
+ "WidgetHasComposition()=%s, mCommitStringByRequest=%p, "
+ "handlingCompositionData=0x%p",
+ this, aWidget, GetBoolName(aCancel), aCompositionId,
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents),
+ GetBoolName(
+ IMEStateManager::DoesBrowserParentHaveIMEFocus(&mBrowserParent)),
+ GetBoolName(WidgetHasComposition()), mCommitStringByRequest,
+ handlingCompositionData));
+
+ MOZ_ASSERT(!mCommitStringByRequest);
+
+ // If we don't know the composition ID, it must have already been committed
+ // in this process. In the case, we should do nothing here.
+ if (NS_WARN_IF(!handlingCompositionData)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eToUnknownCompositionReceived);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If we receive a commit result for not latest composition, this request is
+ // too late for IME. The remote process should wait following composition
+ // events for cleaning up TextComposition and handle the request as it's
+ // handled asynchronously.
+ if (handlingCompositionData != &mHandlingCompositions.LastElement()) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eToOldCompositionReceived);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If the composition has already been commit, th remote process will receive
+ // composition events and clean up TextComposition. So, this should do
+ // nothing and TextComposition should handle the request as it's handled
+ // asynchronously.
+ // XXX Perhaps, this is wrong because TextComposition in child process
+ // may commit the composition with current composition string in the
+ // remote process. I.e., it may be different from actual commit string
+ // which user typed. So, perhaps, we should return true and the commit
+ // string.
+ if (handlingCompositionData->mSentCommitEvent) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eToCommittedCompositionReceived);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If BrowserParent which has IME focus was already changed to different one,
+ // the request shouldn't be sent to IME because it's too late.
+ if (!IMEStateManager::DoesBrowserParentHaveIMEFocus(&mBrowserParent)) {
+ // Use the latest composition string which may not be handled in the
+ // remote process for avoiding data loss.
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eReceivedAfterBrowserParentBlur);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ aCommittedString = handlingCompositionData->mCompositionString;
+ // After we return true from here, i.e., without actually requesting IME
+ // to commit composition, we will receive eCompositionCommitRequestHandled
+ // pseudo event message from the remote process. So, we need to increment
+ // mPendingEventsNeedingAck here.
+ handlingCompositionData->mPendingEventsNeedingAck++;
+ return true;
+ }
+
+ RefPtr<TextComposition> composition =
+ IMEStateManager::GetTextCompositionFor(aWidget);
+ if (NS_WARN_IF(!composition)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Warning,
+ (" 0x%p RequestToCommitComposition(), "
+ "does nothing due to no composition",
+ this));
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eReceivedButNoTextComposition);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If we receive a request for different composition, we must have already
+ // sent a commit event. So the remote process should handle it.
+ // XXX I think that this should never happen because we already checked
+ // whether handlingCompositionData is the latest composition or not.
+ // However, we don't want to commit different composition for the users.
+ // Therefore, let's handle the odd case here.
+ if (NS_WARN_IF(composition->Id() != aCompositionId)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::
+ eReceivedButForDifferentTextComposition);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ mCommitStringByRequest = &aCommittedString;
+
+ // Request commit or cancel composition with TextComposition because we may
+ // have already requested to commit or cancel the composition or we may
+ // have already received eCompositionCommit(AsIs) event. Those status are
+ // managed by composition. So, if we don't request commit composition,
+ // we should do nothing with native IME here.
+ composition->RequestToCommit(aWidget, aCancel);
+
+ mCommitStringByRequest = nullptr;
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ (" 0x%p RequestToCommitComposition(), "
+ "WidgetHasComposition()=%s, the composition %s committed synchronously",
+ this, GetBoolName(WidgetHasComposition()),
+ composition->Destroyed() ? "WAS" : "has NOT been"));
+
+ if (!composition->Destroyed()) {
+ // When the composition isn't committed synchronously, the remote process's
+ // TextComposition instance will synthesize commit events and wait to
+ // receive delayed composition events. When TextComposition instances both
+ // in this process and the remote process will be destroyed when delayed
+ // composition events received. TextComposition instance in the parent
+ // process will dispatch following composition events and be destroyed
+ // normally. On the other hand, TextComposition instance in the remote
+ // process won't dispatch following composition events and will be
+ // destroyed by IMEStateManager::DispatchCompositionEvent().
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eHandledAsynchronously);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // When the composition is committed synchronously, the commit string will be
+ // returned to the remote process. Then, PuppetWidget will dispatch
+ // eCompositionCommit event with the returned commit string (i.e., the value
+ // is aCommittedString of this method) and that causes destroying
+ // TextComposition instance in the remote process (Note that TextComposition
+ // instance in this process was already destroyed).
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eHandledSynchronously);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return true;
+}
+
+void ContentCacheInParent::MaybeNotifyIME(
+ nsIWidget* aWidget, const IMENotification& aNotification) {
+ if (!PendingEventsNeedingAck()) {
+ IMEStateManager::NotifyIME(aNotification, aWidget, &mBrowserParent);
+ return;
+ }
+
+ switch (aNotification.mMessage) {
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ mPendingSelectionChange.MergeWith(aNotification);
+ break;
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ mPendingTextChange.MergeWith(aNotification);
+ break;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ mPendingLayoutChange.MergeWith(aNotification);
+ break;
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ mPendingCompositionUpdate.MergeWith(aNotification);
+ break;
+ default:
+ MOZ_CRASH("Unsupported notification");
+ break;
+ }
+}
+
+void ContentCacheInParent::FlushPendingNotifications(nsIWidget* aWidget) {
+ MOZ_ASSERT(!PendingEventsNeedingAck());
+
+ // If the BrowserParent's widget has already gone, this can do nothing since
+ // widget is necessary to notify IME of something.
+ if (!aWidget) {
+ return;
+ }
+
+ // New notifications which are notified during flushing pending notifications
+ // should be merged again.
+ const bool pendingEventNeedingAckIncremented =
+ !mHandlingCompositions.IsEmpty();
+ if (pendingEventNeedingAckIncremented) {
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck++;
+ }
+
+ nsCOMPtr<nsIWidget> widget = aWidget;
+
+ // First, text change notification should be sent because selection change
+ // notification notifies IME of current selection range in the latest content.
+ // So, IME may need the latest content before that.
+ if (mPendingTextChange.HasNotification()) {
+ IMENotification notification(mPendingTextChange);
+ if (!widget->Destroyed()) {
+ mPendingTextChange.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ if (mPendingSelectionChange.HasNotification()) {
+ IMENotification notification(mPendingSelectionChange);
+ if (!widget->Destroyed()) {
+ mPendingSelectionChange.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ // Layout change notification should be notified after selection change
+ // notification because IME may want to query position of new caret position.
+ if (mPendingLayoutChange.HasNotification()) {
+ IMENotification notification(mPendingLayoutChange);
+ if (!widget->Destroyed()) {
+ mPendingLayoutChange.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ // Finally, send composition update notification because it notifies IME of
+ // finishing handling whole sending events.
+ if (mPendingCompositionUpdate.HasNotification()) {
+ IMENotification notification(mPendingCompositionUpdate);
+ if (!widget->Destroyed()) {
+ mPendingCompositionUpdate.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ // Decrement it which was incremented above.
+ if (!mHandlingCompositions.IsEmpty() && pendingEventNeedingAckIncremented &&
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck) {
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck--;
+ }
+
+ if (!PendingEventsNeedingAck() && !widget->Destroyed() &&
+ (mPendingTextChange.HasNotification() ||
+ mPendingSelectionChange.HasNotification() ||
+ mPendingLayoutChange.HasNotification() ||
+ mPendingCompositionUpdate.HasNotification())) {
+ FlushPendingNotifications(widget);
+ }
+}
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+void ContentCacheInParent::RemoveUnnecessaryEventMessageLog() {
+ bool foundLastCompositionStart = false;
+ for (size_t i = mDispatchedEventMessages.Length(); i > 1; i--) {
+ if (mDispatchedEventMessages[i - 1] != eCompositionStart) {
+ continue;
+ }
+ if (!foundLastCompositionStart) {
+ // Find previous eCompositionStart of the latest eCompositionStart.
+ foundLastCompositionStart = true;
+ continue;
+ }
+ // Remove the messages before the last 2 sets of composition events.
+ mDispatchedEventMessages.RemoveElementsAt(0, i - 1);
+ break;
+ }
+ uint32_t numberOfCompositionCommitRequestHandled = 0;
+ foundLastCompositionStart = false;
+ for (size_t i = mReceivedEventMessages.Length(); i > 1; i--) {
+ if (mReceivedEventMessages[i - 1] == eCompositionCommitRequestHandled) {
+ numberOfCompositionCommitRequestHandled++;
+ }
+ if (mReceivedEventMessages[i - 1] != eCompositionStart) {
+ continue;
+ }
+ if (!foundLastCompositionStart) {
+ // Find previous eCompositionStart of the latest eCompositionStart.
+ foundLastCompositionStart = true;
+ continue;
+ }
+ // Remove the messages before the last 2 sets of composition events.
+ mReceivedEventMessages.RemoveElementsAt(0, i - 1);
+ break;
+ }
+
+ if (!numberOfCompositionCommitRequestHandled) {
+ // If there is no eCompositionCommitRequestHandled in
+ // mReceivedEventMessages, we don't need to store log of
+ // RequestIMEToCommmitComposition().
+ mRequestIMEToCommitCompositionResults.Clear();
+ } else {
+ // We need to keep all reason of eCompositionCommitRequestHandled, which
+ // is sent when mRequestIMEToCommitComposition() returns true.
+ // So, we can discard older log than the first
+ // eCompositionCommitRequestHandled in mReceivedEventMessages.
+ for (size_t i = mRequestIMEToCommitCompositionResults.Length(); i > 1;
+ i--) {
+ if (mRequestIMEToCommitCompositionResults[i - 1] ==
+ RequestIMEToCommitCompositionResult::
+ eReceivedAfterBrowserParentBlur ||
+ mRequestIMEToCommitCompositionResults[i - 1] ==
+ RequestIMEToCommitCompositionResult::eHandledSynchronously) {
+ --numberOfCompositionCommitRequestHandled;
+ if (!numberOfCompositionCommitRequestHandled) {
+ mRequestIMEToCommitCompositionResults.RemoveElementsAt(0, i - 1);
+ break;
+ }
+ }
+ }
+ }
+}
+
+void ContentCacheInParent::AppendEventMessageLog(nsACString& aLog) const {
+ aLog.AppendLiteral("Dispatched Event Message Log:\n");
+ for (EventMessage message : mDispatchedEventMessages) {
+ aLog.AppendLiteral(" ");
+ aLog.Append(ToChar(message));
+ aLog.AppendLiteral("\n");
+ }
+ aLog.AppendLiteral("\nReceived Event Message Log:\n");
+ for (EventMessage message : mReceivedEventMessages) {
+ aLog.AppendLiteral(" ");
+ aLog.Append(ToChar(message));
+ aLog.AppendLiteral("\n");
+ }
+ aLog.AppendLiteral("\nResult of RequestIMEToCommitComposition():\n");
+ for (RequestIMEToCommitCompositionResult result :
+ mRequestIMEToCommitCompositionResults) {
+ aLog.AppendLiteral(" ");
+ aLog.Append(ToReadableText(result));
+ aLog.AppendLiteral("\n");
+ }
+ aLog.AppendLiteral("\n");
+}
+
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+/*****************************************************************************
+ * mozilla::ContentCache::Selection
+ *****************************************************************************/
+
+ContentCache::Selection::Selection(
+ const WidgetQueryContentEvent& aQuerySelectedTextEvent)
+ : mAnchor(UINT32_MAX),
+ mFocus(UINT32_MAX),
+ mWritingMode(aQuerySelectedTextEvent.mReply->WritingModeRef()),
+ mHasRange(aQuerySelectedTextEvent.mReply->mOffsetAndData.isSome()) {
+ MOZ_ASSERT(aQuerySelectedTextEvent.mMessage == eQuerySelectedText);
+ MOZ_ASSERT(aQuerySelectedTextEvent.Succeeded());
+ if (mHasRange) {
+ mAnchor = aQuerySelectedTextEvent.mReply->AnchorOffset();
+ mFocus = aQuerySelectedTextEvent.mReply->FocusOffset();
+ }
+}
+
+/*****************************************************************************
+ * mozilla::ContentCache::TextRectArray
+ *****************************************************************************/
+
+LayoutDeviceIntRect ContentCache::TextRectArray::GetRect(
+ uint32_t aOffset) const {
+ LayoutDeviceIntRect rect;
+ if (IsOffsetInRange(aOffset)) {
+ rect = mRects[aOffset - mStart];
+ }
+ return rect;
+}
+
+LayoutDeviceIntRect ContentCache::TextRectArray::GetUnionRect(
+ uint32_t aOffset, uint32_t aLength) const {
+ LayoutDeviceIntRect rect;
+ if (!IsRangeCompletelyInRange(aOffset, aLength)) {
+ return rect;
+ }
+ for (uint32_t i = 0; i < aLength; i++) {
+ rect = rect.Union(mRects[aOffset - mStart + i]);
+ }
+ return rect;
+}
+
+LayoutDeviceIntRect ContentCache::TextRectArray::GetUnionRectAsFarAsPossible(
+ uint32_t aOffset, uint32_t aLength, bool aRoundToExistingOffset) const {
+ LayoutDeviceIntRect rect;
+ if (!HasRects() ||
+ (!aRoundToExistingOffset && !IsOverlappingWith(aOffset, aLength))) {
+ return rect;
+ }
+ uint32_t startOffset = std::max(aOffset, mStart);
+ if (aRoundToExistingOffset && startOffset >= EndOffset()) {
+ startOffset = EndOffset() - 1;
+ }
+ uint32_t endOffset = std::min(aOffset + aLength, EndOffset());
+ if (aRoundToExistingOffset && endOffset < mStart + 1) {
+ endOffset = mStart + 1;
+ }
+ if (NS_WARN_IF(endOffset < startOffset)) {
+ return rect;
+ }
+ for (uint32_t i = 0; i < endOffset - startOffset; i++) {
+ rect = rect.Union(mRects[startOffset - mStart + i]);
+ }
+ return rect;
+}
+
+} // namespace mozilla
diff --git a/widget/ContentCache.h b/widget/ContentCache.h
new file mode 100644
index 0000000000..3f8b48425e
--- /dev/null
+++ b/widget/ContentCache.h
@@ -0,0 +1,645 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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/. */
+
+#ifndef mozilla_ContentCache_h
+#define mozilla_ContentCache_h
+
+#include <stdint.h>
+
+#include "mozilla/widget/IMEData.h"
+#include "mozilla/ipc/IPCForwards.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ToString.h"
+#include "mozilla/WritingModes.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "Units.h"
+
+class nsIWidget;
+
+namespace mozilla {
+
+class ContentCacheInParent;
+
+namespace dom {
+class BrowserParent;
+} // namespace dom
+
+/**
+ * ContentCache stores various information of the child content.
+ * This class has members which are necessary both in parent process and
+ * content process.
+ */
+
+class ContentCache {
+ public:
+ using RectArray = CopyableTArray<LayoutDeviceIntRect>;
+ using IMENotification = widget::IMENotification;
+
+ ContentCache() = default;
+
+ [[nodiscard]] bool IsValid() const;
+
+ protected:
+ void AssertIfInvalid() const;
+
+ // Whole text in the target
+ Maybe<nsString> mText;
+
+ // Start offset of the composition string.
+ Maybe<uint32_t> mCompositionStart;
+
+ enum { ePrevCharRect = 1, eNextCharRect = 0 };
+
+ struct Selection final {
+ // Following values are offset in "flat text".
+ uint32_t mAnchor;
+ uint32_t mFocus;
+
+ WritingMode mWritingMode;
+
+ bool mHasRange;
+
+ // Character rects at previous and next character of mAnchor and mFocus.
+ // The reason why ContentCache needs to store each previous character of
+ // them is IME may query character rect of the last character of a line
+ // when caret is at the end of the line.
+ // Note that use ePrevCharRect and eNextCharRect for accessing each item.
+ LayoutDeviceIntRect mAnchorCharRects[2];
+ LayoutDeviceIntRect mFocusCharRects[2];
+
+ // Whole rect of selected text. This is empty if the selection is collapsed.
+ LayoutDeviceIntRect mRect;
+
+ Selection() : mAnchor(UINT32_MAX), mFocus(UINT32_MAX), mHasRange(false) {
+ ClearRects();
+ };
+
+ explicit Selection(
+ const IMENotification::SelectionChangeDataBase& aSelectionChangeData)
+ : mAnchor(UINT32_MAX),
+ mFocus(UINT32_MAX),
+ mWritingMode(aSelectionChangeData.GetWritingMode()),
+ mHasRange(aSelectionChangeData.HasRange()) {
+ if (mHasRange) {
+ mAnchor = aSelectionChangeData.AnchorOffset();
+ mFocus = aSelectionChangeData.FocusOffset();
+ }
+ }
+
+ [[nodiscard]] bool IsValidIn(const nsAString& aText) const {
+ return !mHasRange ||
+ (mAnchor <= aText.Length() && mFocus <= aText.Length());
+ }
+
+ explicit Selection(const WidgetQueryContentEvent& aQuerySelectedTextEvent);
+
+ void ClearRects() {
+ for (auto& rect : mAnchorCharRects) {
+ rect.SetEmpty();
+ }
+ for (auto& rect : mFocusCharRects) {
+ rect.SetEmpty();
+ }
+ mRect.SetEmpty();
+ }
+ bool HasRects() const {
+ for (const auto& rect : mAnchorCharRects) {
+ if (!rect.IsEmpty()) {
+ return true;
+ }
+ }
+ for (const auto& rect : mFocusCharRects) {
+ if (!rect.IsEmpty()) {
+ return true;
+ }
+ }
+ return !mRect.IsEmpty();
+ }
+
+ bool IsCollapsed() const { return !mHasRange || mFocus == mAnchor; }
+ bool Reversed() const {
+ MOZ_ASSERT(mHasRange);
+ return mFocus < mAnchor;
+ }
+ uint32_t StartOffset() const {
+ MOZ_ASSERT(mHasRange);
+ return Reversed() ? mFocus : mAnchor;
+ }
+ uint32_t EndOffset() const {
+ MOZ_ASSERT(mHasRange);
+ return Reversed() ? mAnchor : mFocus;
+ }
+ uint32_t Length() const {
+ MOZ_ASSERT(mHasRange);
+ return Reversed() ? mAnchor - mFocus : mFocus - mAnchor;
+ }
+ LayoutDeviceIntRect StartCharRect() const {
+ return Reversed() ? mFocusCharRects[eNextCharRect]
+ : mAnchorCharRects[eNextCharRect];
+ }
+ LayoutDeviceIntRect EndCharRect() const {
+ return Reversed() ? mAnchorCharRects[eNextCharRect]
+ : mFocusCharRects[eNextCharRect];
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Selection& aSelection) {
+ aStream << "{ ";
+ if (!aSelection.mHasRange) {
+ aStream << "HasRange()=false";
+ } else {
+ aStream << "mAnchor=" << aSelection.mAnchor
+ << ", mFocus=" << aSelection.mFocus << ", mWritingMode="
+ << ToString(aSelection.mWritingMode).c_str();
+ }
+ if (aSelection.HasRects()) {
+ if (aSelection.mAnchor > 0) {
+ aStream << ", mAnchorCharRects[ePrevCharRect]="
+ << aSelection.mAnchorCharRects[ContentCache::ePrevCharRect];
+ }
+ aStream << ", mAnchorCharRects[eNextCharRect]="
+ << aSelection.mAnchorCharRects[ContentCache::eNextCharRect];
+ if (aSelection.mFocus > 0) {
+ aStream << ", mFocusCharRects[ePrevCharRect]="
+ << aSelection.mFocusCharRects[ContentCache::ePrevCharRect];
+ }
+ aStream << ", mFocusCharRects[eNextCharRect]="
+ << aSelection.mFocusCharRects[ContentCache::eNextCharRect]
+ << ", mRect=" << aSelection.mRect;
+ }
+ if (aSelection.mHasRange) {
+ aStream << ", Reversed()=" << (aSelection.Reversed() ? "true" : "false")
+ << ", StartOffset()=" << aSelection.StartOffset()
+ << ", EndOffset()=" << aSelection.EndOffset()
+ << ", IsCollapsed()="
+ << (aSelection.IsCollapsed() ? "true" : "false")
+ << ", Length()=" << aSelection.Length();
+ }
+ aStream << " }";
+ return aStream;
+ }
+ };
+ Maybe<Selection> mSelection;
+
+ // Stores first char rect because Yosemite's Japanese IME sometimes tries
+ // to query it. If there is no text, this is caret rect.
+ LayoutDeviceIntRect mFirstCharRect;
+
+ struct Caret final {
+ uint32_t mOffset = 0u;
+ LayoutDeviceIntRect mRect;
+
+ explicit Caret(uint32_t aOffset, LayoutDeviceIntRect aCaretRect)
+ : mOffset(aOffset), mRect(aCaretRect) {}
+
+ uint32_t Offset() const { return mOffset; }
+ bool HasRect() const { return !mRect.IsEmpty(); }
+
+ [[nodiscard]] bool IsValidIn(const nsAString& aText) const {
+ return mOffset <= aText.Length();
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Caret& aCaret) {
+ aStream << "{ mOffset=" << aCaret.mOffset;
+ if (aCaret.HasRect()) {
+ aStream << ", mRect=" << aCaret.mRect;
+ }
+ return aStream << " }";
+ }
+
+ private:
+ // For ParamTraits<Caret>
+ Caret() = default;
+
+ friend struct IPC::ParamTraits<ContentCache::Caret>;
+ ALLOW_DEPRECATED_READPARAM
+ };
+ Maybe<Caret> mCaret;
+
+ struct TextRectArray final {
+ uint32_t mStart = 0u;
+ RectArray mRects;
+
+ explicit TextRectArray(uint32_t aStartOffset) : mStart(aStartOffset) {}
+
+ bool HasRects() const { return Length() > 0; }
+ uint32_t StartOffset() const { return mStart; }
+ uint32_t EndOffset() const {
+ CheckedInt<uint32_t> endOffset =
+ CheckedInt<uint32_t>(mStart) + mRects.Length();
+ return endOffset.isValid() ? endOffset.value() : UINT32_MAX;
+ }
+ uint32_t Length() const { return EndOffset() - mStart; }
+ bool IsOffsetInRange(uint32_t aOffset) const {
+ return StartOffset() <= aOffset && aOffset < EndOffset();
+ }
+ bool IsRangeCompletelyInRange(uint32_t aOffset, uint32_t aLength) const {
+ CheckedInt<uint32_t> endOffset = CheckedInt<uint32_t>(aOffset) + aLength;
+ if (NS_WARN_IF(!endOffset.isValid())) {
+ return false;
+ }
+ return IsOffsetInRange(aOffset) && aOffset + aLength <= EndOffset();
+ }
+ bool IsOverlappingWith(uint32_t aOffset, uint32_t aLength) const {
+ if (!HasRects() || aOffset == UINT32_MAX || !aLength) {
+ return false;
+ }
+ CheckedInt<uint32_t> endOffset = CheckedInt<uint32_t>(aOffset) + aLength;
+ if (NS_WARN_IF(!endOffset.isValid())) {
+ return false;
+ }
+ return aOffset < EndOffset() && endOffset.value() > mStart;
+ }
+ LayoutDeviceIntRect GetRect(uint32_t aOffset) const;
+ LayoutDeviceIntRect GetUnionRect(uint32_t aOffset, uint32_t aLength) const;
+ LayoutDeviceIntRect GetUnionRectAsFarAsPossible(
+ uint32_t aOffset, uint32_t aLength, bool aRoundToExistingOffset) const;
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const TextRectArray& aTextRectArray) {
+ aStream << "{ mStart=" << aTextRectArray.mStart
+ << ", mRects={ Length()=" << aTextRectArray.Length();
+ if (aTextRectArray.HasRects()) {
+ aStream << ", Elements()=[ ";
+ static constexpr uint32_t kMaxPrintRects = 4;
+ const uint32_t kFirstHalf = aTextRectArray.Length() <= kMaxPrintRects
+ ? UINT32_MAX
+ : (kMaxPrintRects + 1) / 2;
+ const uint32_t kSecondHalf =
+ aTextRectArray.Length() <= kMaxPrintRects ? 0 : kMaxPrintRects / 2;
+ for (uint32_t i = 0; i < aTextRectArray.Length(); i++) {
+ if (i > 0) {
+ aStream << ", ";
+ }
+ aStream << ToString(aTextRectArray.mRects[i]).c_str();
+ if (i + 1 == kFirstHalf) {
+ aStream << " ...";
+ i = aTextRectArray.Length() - kSecondHalf - 1;
+ }
+ }
+ }
+ return aStream << " ] } }";
+ }
+
+ private:
+ // For ParamTraits<TextRectArray>
+ TextRectArray() = default;
+
+ friend struct IPC::ParamTraits<ContentCache::TextRectArray>;
+ ALLOW_DEPRECATED_READPARAM
+ };
+ Maybe<TextRectArray> mTextRectArray;
+ Maybe<TextRectArray> mLastCommitStringTextRectArray;
+
+ LayoutDeviceIntRect mEditorRect;
+
+ friend class ContentCacheInParent;
+ friend struct IPC::ParamTraits<ContentCache>;
+ friend struct IPC::ParamTraits<ContentCache::Selection>;
+ friend struct IPC::ParamTraits<ContentCache::Caret>;
+ friend struct IPC::ParamTraits<ContentCache::TextRectArray>;
+ friend std::ostream& operator<<(
+ std::ostream& aStream,
+ const Selection& aSelection); // For e(Prev|Next)CharRect
+ ALLOW_DEPRECATED_READPARAM
+};
+
+class ContentCacheInChild final : public ContentCache {
+ public:
+ ContentCacheInChild() = default;
+
+ /**
+ * Called when composition event will be dispatched in this process from
+ * PuppetWidget.
+ */
+ void OnCompositionEvent(const WidgetCompositionEvent& aCompositionEvent);
+
+ /**
+ * When IME loses focus, this should be called and making this forget the
+ * content for reducing footprint.
+ */
+ void Clear();
+
+ /**
+ * Cache*() retrieves the latest content information and store them.
+ * Be aware, CacheSelection() calls CacheCaretAndTextRects(),
+ * CacheCaretAndTextRects() calls CacheCaret() and CacheTextRects(), and
+ * CacheText() calls CacheSelection(). So, related data is also retrieved
+ * automatically.
+ */
+ bool CacheEditorRect(nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+ bool CacheCaretAndTextRects(nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+ bool CacheText(nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+
+ bool CacheAll(nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+
+ /**
+ * SetSelection() modifies selection with specified raw data. And also this
+ * tries to retrieve text rects too.
+ *
+ * @return true if the selection is cached. Otherwise, false.
+ */
+ [[nodiscard]] bool SetSelection(
+ nsIWidget* aWidget,
+ const IMENotification::SelectionChangeDataBase& aSelectionChangeData);
+
+ private:
+ bool QueryCharRect(nsIWidget* aWidget, uint32_t aOffset,
+ LayoutDeviceIntRect& aCharRect) const;
+ bool QueryCharRectArray(nsIWidget* aWidget, uint32_t aOffset,
+ uint32_t aLength, RectArray& aCharRectArray) const;
+ bool CacheSelection(nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+ bool CacheCaret(nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+ bool CacheTextRects(nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+
+ // Once composition is committed, all of the commit string may be composed
+ // again by Kakutei-Undo of Japanese IME. Therefore, we need to keep
+ // storing the last composition start to cache all character rects of the
+ // last commit string.
+ Maybe<OffsetAndData<uint32_t>> mLastCommit;
+};
+
+class ContentCacheInParent final : public ContentCache {
+ public:
+ ContentCacheInParent() = delete;
+ explicit ContentCacheInParent(dom::BrowserParent& aBrowserParent);
+
+ /**
+ * AssignContent() is called when BrowserParent receives ContentCache from
+ * the content process. This doesn't copy composition information because
+ * it's managed by BrowserParent itself.
+ */
+ void AssignContent(const ContentCache& aOther, nsIWidget* aWidget,
+ const IMENotification* aNotification = nullptr);
+
+ /**
+ * HandleQueryContentEvent() sets content data to aEvent.mReply.
+ *
+ * For eQuerySelectedText, fail if the cache doesn't contain the whole
+ * selected range. (This shouldn't happen because PuppetWidget should have
+ * already sent the whole selection.)
+ *
+ * For eQueryTextContent, fail only if the cache doesn't overlap with
+ * the queried range. Note the difference from above. We use
+ * this behavior because a normal eQueryTextContent event is allowed to
+ * have out-of-bounds offsets, so that widget can request content without
+ * knowing the exact length of text. It's up to widget to handle cases when
+ * the returned offset/length are different from the queried offset/length.
+ *
+ * For eQueryTextRect, fail if cached offset/length aren't equals to input.
+ * Cocoa widget always queries selected offset, so it works on it.
+ *
+ * For eQueryCaretRect, fail if cached offset isn't equals to input
+ *
+ * For eQueryEditorRect, always success
+ */
+ bool HandleQueryContentEvent(WidgetQueryContentEvent& aEvent,
+ nsIWidget* aWidget) const;
+
+ /**
+ * OnCompositionEvent() should be called before sending composition string.
+ * This returns true if the event should be sent. Otherwise, false.
+ */
+ bool OnCompositionEvent(const WidgetCompositionEvent& aCompositionEvent);
+
+ /**
+ * OnSelectionEvent() should be called before sending selection event.
+ */
+ void OnSelectionEvent(const WidgetSelectionEvent& aSelectionEvent);
+
+ /**
+ * OnEventNeedingAckHandled() should be called after the child process
+ * handles a sent event which needs acknowledging.
+ *
+ * WARNING: This may send notifications to IME. That might cause destroying
+ * BrowserParent or aWidget. Therefore, the caller must not destroy
+ * this instance during a call of this method.
+ */
+ void OnEventNeedingAckHandled(nsIWidget* aWidget, EventMessage aMessage,
+ uint32_t aCompositionId);
+
+ /**
+ * RequestIMEToCommitComposition() requests aWidget to commit or cancel
+ * composition. If it's handled synchronously, this returns true.
+ *
+ * @param aWidget The widget to be requested to commit or cancel
+ * the composition.
+ * @param aCancel When the caller tries to cancel the composition, true.
+ * Otherwise, i.e., tries to commit the composition, false.
+ * @param aCompositionId
+ * The composition ID which should be committed or
+ * canceled.
+ * @param aCommittedString The committed string (i.e., the last data of
+ * dispatched composition events during requesting
+ * IME to commit composition.
+ * @return Whether the composition is actually committed
+ * synchronously.
+ */
+ bool RequestIMEToCommitComposition(nsIWidget* aWidget, bool aCancel,
+ uint32_t aCompositionId,
+ nsAString& aCommittedString);
+
+ /**
+ * MaybeNotifyIME() may notify IME of the notification. If child process
+ * hasn't been handled all sending events yet, this stores the notification
+ * and flush it later.
+ */
+ void MaybeNotifyIME(nsIWidget* aWidget, const IMENotification& aNotification);
+
+ private:
+ struct HandlingCompositionData;
+
+ // Return true when the widget in this process thinks that IME has
+ // composition. So, this returns true when there is at least one handling
+ // composition data and the last handling composition has not dispatched
+ // composition commit event to the remote process yet.
+ [[nodiscard]] bool WidgetHasComposition() const {
+ return !mHandlingCompositions.IsEmpty() &&
+ !mHandlingCompositions.LastElement().mSentCommitEvent;
+ }
+
+ // Return true if there is a pending composition which has already sent
+ // a commit event to the remote process, but not yet handled by it.
+ [[nodiscard]] bool HasPendingCommit() const {
+ for (const HandlingCompositionData& data : mHandlingCompositions) {
+ if (data.mSentCommitEvent) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Return the number of composition events and set selection events which were
+ // sent to the remote process, but we've not verified that the remote process
+ // finished handling it.
+ [[nodiscard]] uint32_t PendingEventsNeedingAck() const {
+ uint32_t ret = mPendingSetSelectionEventNeedingAck;
+ for (const HandlingCompositionData& data : mHandlingCompositions) {
+ ret += data.mPendingEventsNeedingAck;
+ }
+ return ret;
+ }
+
+ [[nodiscard]] HandlingCompositionData* GetHandlingCompositionData(
+ uint32_t aCompositionId) {
+ for (HandlingCompositionData& data : mHandlingCompositions) {
+ if (data.mCompositionId == aCompositionId) {
+ return &data;
+ }
+ }
+ return nullptr;
+ }
+ [[nodiscard]] const HandlingCompositionData* GetHandlingCompositionData(
+ uint32_t aCompositionId) const {
+ return const_cast<ContentCacheInParent*>(this)->GetHandlingCompositionData(
+ aCompositionId);
+ }
+
+ IMENotification mPendingSelectionChange;
+ IMENotification mPendingTextChange;
+ IMENotification mPendingLayoutChange;
+ IMENotification mPendingCompositionUpdate;
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ // Log of event messages to be output to crash report.
+ nsTArray<EventMessage> mDispatchedEventMessages;
+ nsTArray<EventMessage> mReceivedEventMessages;
+ // Log of RequestIMEToCommitComposition() in the last 2 compositions.
+ enum class RequestIMEToCommitCompositionResult : uint8_t {
+ eToOldCompositionReceived,
+ eToUnknownCompositionReceived,
+ eToCommittedCompositionReceived,
+ eReceivedAfterBrowserParentBlur,
+ eReceivedButNoTextComposition,
+ eReceivedButForDifferentTextComposition,
+ eHandledAsynchronously,
+ eHandledSynchronously,
+ };
+ const char* ToReadableText(
+ RequestIMEToCommitCompositionResult aResult) const {
+ switch (aResult) {
+ case RequestIMEToCommitCompositionResult::eToOldCompositionReceived:
+ return "Commit request is not handled because it's for "
+ "older composition";
+ case RequestIMEToCommitCompositionResult::eToUnknownCompositionReceived:
+ return "Commit request is not handled because it's for "
+ "unknown composition";
+ case RequestIMEToCommitCompositionResult::eToCommittedCompositionReceived:
+ return "Commit request is not handled because BrowserParent has "
+ "already "
+ "sent commit event for the composition";
+ case RequestIMEToCommitCompositionResult::eReceivedAfterBrowserParentBlur:
+ return "Commit request is handled with stored composition string "
+ "because BrowserParent has already lost focus";
+ case RequestIMEToCommitCompositionResult::eReceivedButNoTextComposition:
+ return "Commit request is not handled because there is no "
+ "TextComposition instance";
+ case RequestIMEToCommitCompositionResult::
+ eReceivedButForDifferentTextComposition:
+ return "Commit request is handled with stored composition string "
+ "because new TextComposition is active";
+ case RequestIMEToCommitCompositionResult::eHandledAsynchronously:
+ return "Commit request is handled but IME doesn't commit current "
+ "composition synchronously";
+ case RequestIMEToCommitCompositionResult::eHandledSynchronously:
+ return "Commit request is handled synchronously";
+ default:
+ return "Unknown reason";
+ }
+ }
+ nsTArray<RequestIMEToCommitCompositionResult>
+ mRequestIMEToCommitCompositionResults;
+#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ // Stores pending compositions (meaning eCompositionStart was dispatched, but
+ // eCompositionCommit(AsIs) has not been handled by the remote process yet).
+ struct HandlingCompositionData {
+ // The lasted composition string which was sent to the remote process.
+ nsString mCompositionString;
+ // The composition ID of a handling composition with the instance.
+ uint32_t mCompositionId;
+ // Increased when sending composition events and decreased when the
+ // remote process finished handling the events.
+ uint32_t mPendingEventsNeedingAck = 0u;
+ // true if eCompositionCommit(AsIs) has already been sent to the remote
+ // process.
+ bool mSentCommitEvent = false;
+
+ explicit HandlingCompositionData(uint32_t aCompositionId)
+ : mCompositionId(aCompositionId) {}
+ };
+ AutoTArray<HandlingCompositionData, 2> mHandlingCompositions;
+
+ // mBrowserParent is owner of the instance.
+ dom::BrowserParent& MOZ_NON_OWNING_REF mBrowserParent;
+ // This is not nullptr only while the instance is requesting IME to
+ // composition. Then, data value of dispatched composition events should
+ // be stored into the instance.
+ nsAString* mCommitStringByRequest;
+ // mCompositionStartInChild stores current composition start offset in the
+ // remote process.
+ Maybe<uint32_t> mCompositionStartInChild;
+ // Increased when sending eSetSelection events and decreased when the remote
+ // process finished handling the events. Note that eSetSelection may be
+ // dispatched without composition. Therefore, we need to count it with this.
+ uint32_t mPendingSetSelectionEventNeedingAck = 0u;
+ // mPendingCommitLength is commit string length of the first pending
+ // composition. This is used by relative offset query events when querying
+ // new composition start offset.
+ // Note that when mHandlingCompositions has 2 or more elements, i.e., there
+ // are 2 or more pending compositions, this cache won't be used because in
+ // such case, anyway ContentCacheInParent cannot return proper character rect.
+ uint32_t mPendingCommitLength;
+ // mIsChildIgnoringCompositionEvents is set to true if the child process
+ // requests commit composition whose commit has already been sent to it.
+ // Then, set to false when the child process ignores the commit event.
+ bool mIsChildIgnoringCompositionEvents;
+
+ /**
+ * When following methods' aRoundToExistingOffset is true, even if specified
+ * offset or range is out of bounds, the result is computed with the existing
+ * cache forcibly.
+ */
+ bool GetCaretRect(uint32_t aOffset, bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aCaretRect) const;
+ bool GetTextRect(uint32_t aOffset, bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aTextRect) const;
+ bool GetUnionTextRects(uint32_t aOffset, uint32_t aLength,
+ bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aUnionTextRect) const;
+
+ void FlushPendingNotifications(nsIWidget* aWidget);
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ /**
+ * Remove unnecessary messages from mDispatchedEventMessages and
+ * mReceivedEventMessages.
+ */
+ void RemoveUnnecessaryEventMessageLog();
+
+ /**
+ * Append event message log to aLog.
+ */
+ void AppendEventMessageLog(nsACString& aLog) const;
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ContentCache_h
diff --git a/widget/ContentData.cpp b/widget/ContentData.cpp
new file mode 100644
index 0000000000..529ccc5c0f
--- /dev/null
+++ b/widget/ContentData.cpp
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 40; 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 "ContentData.h"
+
+#include "TextEvents.h"
+
+namespace mozilla {
+
+/******************************************************************************
+ * ContentSelection
+ ******************************************************************************/
+
+ContentSelection::ContentSelection(
+ const WidgetQueryContentEvent& aSelectedTextEvent)
+ : mWritingMode(aSelectedTextEvent.mReply->WritingModeRef()) {
+ MOZ_ASSERT(aSelectedTextEvent.mMessage == eQuerySelectedText);
+ MOZ_ASSERT(aSelectedTextEvent.Succeeded());
+ if (aSelectedTextEvent.mReply->mOffsetAndData.isSome()) {
+ mOffsetAndData =
+ Some(OffsetAndData<uint32_t>(aSelectedTextEvent.mReply->StartOffset(),
+ aSelectedTextEvent.mReply->DataRef(),
+ OffsetAndDataFor::SelectedString));
+ }
+}
+
+} // namespace mozilla
diff --git a/widget/ContentData.h b/widget/ContentData.h
new file mode 100644
index 0000000000..3e641a2684
--- /dev/null
+++ b/widget/ContentData.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_ContentData_h
+#define mozilla_ContentData_h
+
+#include <sstream>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Debug.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/widget/IMEData.h"
+
+/**
+ * This file is intended for declaring classes which store DOM content data for
+ * widget classes. Those data should be retrived by `WidgetQueryContentEvent`,
+ * notified with `IMENotification`, or set assumed data as result of dispatching
+ * widget events such as `WidgetKeyboardEvent`, `WidgetCompositionEvent`,
+ * `WidgetSelectionEvent` etc.
+ */
+
+namespace mozilla {
+
+/**
+ * ContentSelection stores DOM selection in flattend text by
+ * ContentEventHandler. It should be retrieved with `eQuerySelectedText` event,
+ * notified with `NOTIFY_IME_OF_SELECTION_CHANGE` or set by widget itself.
+ */
+class ContentSelection {
+ public:
+ using SelectionChangeDataBase =
+ widget::IMENotification::SelectionChangeDataBase;
+ ContentSelection() = default;
+ explicit ContentSelection(const SelectionChangeDataBase& aSelectionChangeData)
+ : mOffsetAndData(Some(aSelectionChangeData.ToUint32OffsetAndData())),
+ mWritingMode(aSelectionChangeData.GetWritingMode()) {}
+ explicit ContentSelection(const WidgetQueryContentEvent& aSelectedTextEvent);
+ ContentSelection(uint32_t aOffset, const WritingMode& aWritingMode)
+ : mOffsetAndData(Some(OffsetAndData<uint32_t>(
+ aOffset, EmptyString(), OffsetAndDataFor::SelectedString))),
+ mWritingMode(aWritingMode) {}
+
+ const OffsetAndData<uint32_t>& OffsetAndDataRef() const {
+ return mOffsetAndData.ref();
+ }
+
+ void Collapse(uint32_t aOffset) {
+ if (mOffsetAndData.isSome()) {
+ mOffsetAndData->Collapse(aOffset);
+ } else {
+ mOffsetAndData.emplace(aOffset, EmptyString(),
+ OffsetAndDataFor::SelectedString);
+ }
+ }
+ void Clear() {
+ mOffsetAndData.reset();
+ mWritingMode = WritingMode();
+ }
+
+ bool HasRange() const { return mOffsetAndData.isSome(); }
+ const WritingMode& WritingModeRef() const { return mWritingMode; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const ContentSelection& aContentSelection) {
+ if (aContentSelection.HasRange()) {
+ return aStream << "{ HasRange()=false }";
+ }
+ aStream << "{ mOffsetAndData=" << aContentSelection.mOffsetAndData
+ << ", mWritingMode=" << aContentSelection.mWritingMode << " }";
+ return aStream;
+ }
+
+ private:
+ Maybe<OffsetAndData<uint32_t>> mOffsetAndData;
+ WritingMode mWritingMode;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_ContentData_h
diff --git a/widget/ContentEvents.h b/widget/ContentEvents.h
new file mode 100644
index 0000000000..e43de98c9f
--- /dev/null
+++ b/widget/ContentEvents.h
@@ -0,0 +1,313 @@
+/* -*- 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/. */
+
+#ifndef mozilla_ContentEvents_h__
+#define mozilla_ContentEvents_h__
+
+#include <stdint.h>
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/EventTarget.h"
+#include "nsCOMPtr.h"
+#include "nsRect.h"
+#include "nsString.h"
+
+class nsIContent;
+
+namespace mozilla {
+
+/******************************************************************************
+ * mozilla::InternalScrollPortEvent
+ ******************************************************************************/
+
+class InternalScrollPortEvent : public WidgetGUIEvent {
+ public:
+ virtual InternalScrollPortEvent* AsScrollPortEvent() override { return this; }
+
+ enum OrientType { eVertical, eHorizontal, eBoth };
+
+ InternalScrollPortEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, eScrollPortEventClass,
+ aTime),
+ mOrient(eVertical) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eScrollPortEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ InternalScrollPortEvent* result =
+ new InternalScrollPortEvent(false, mMessage, nullptr, this);
+ result->AssignScrollPortEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ OrientType mOrient;
+
+ void AssignScrollPortEventData(const InternalScrollPortEvent& aEvent,
+ bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ mOrient = aEvent.mOrient;
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalScrollPortEvent
+ ******************************************************************************/
+
+class InternalScrollAreaEvent : public WidgetGUIEvent {
+ public:
+ virtual InternalScrollAreaEvent* AsScrollAreaEvent() override { return this; }
+
+ InternalScrollAreaEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, eScrollAreaEventClass,
+ aTime) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eScrollAreaEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ InternalScrollAreaEvent* result =
+ new InternalScrollAreaEvent(false, mMessage, nullptr, this);
+ result->AssignScrollAreaEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ nsRect mArea;
+
+ void AssignScrollAreaEventData(const InternalScrollAreaEvent& aEvent,
+ bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ mArea = aEvent.mArea;
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalFormEvent
+ *
+ * We hold the originating form control for form submit and reset events.
+ * mOriginator is a weak pointer (does not hold a strong reference).
+ ******************************************************************************/
+
+class InternalFormEvent : public WidgetEvent {
+ public:
+ virtual InternalFormEvent* AsFormEvent() override { return this; }
+
+ InternalFormEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEvent(aIsTrusted, aMessage, eFormEventClass, aTime),
+ mOriginator(nullptr) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eFormEventClass,
+ "Duplicate() must be overridden by sub class");
+ InternalFormEvent* result = new InternalFormEvent(false, mMessage, this);
+ result->AssignFormEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ nsIContent* mOriginator;
+
+ void AssignFormEventData(const InternalFormEvent& aEvent, bool aCopyTargets) {
+ AssignEventData(aEvent, aCopyTargets);
+
+ // Don't copy mOriginator due to a weak pointer.
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalClipboardEvent
+ ******************************************************************************/
+
+class InternalClipboardEvent : public WidgetEvent {
+ public:
+ virtual InternalClipboardEvent* AsClipboardEvent() override { return this; }
+
+ InternalClipboardEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEvent(aIsTrusted, aMessage, eClipboardEventClass, aTime) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eClipboardEventClass,
+ "Duplicate() must be overridden by sub class");
+ InternalClipboardEvent* result =
+ new InternalClipboardEvent(false, mMessage, this);
+ result->AssignClipboardEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ nsCOMPtr<dom::DataTransfer> mClipboardData;
+
+ void AssignClipboardEventData(const InternalClipboardEvent& aEvent,
+ bool aCopyTargets) {
+ AssignEventData(aEvent, aCopyTargets);
+
+ mClipboardData = aEvent.mClipboardData;
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalFocusEvent
+ ******************************************************************************/
+
+class InternalFocusEvent : public InternalUIEvent {
+ public:
+ virtual InternalFocusEvent* AsFocusEvent() override { return this; }
+
+ InternalFocusEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEventTime* aTime = nullptr)
+ : InternalUIEvent(aIsTrusted, aMessage, eFocusEventClass, aTime),
+ mFromRaise(false),
+ mIsRefocus(false) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eFocusEventClass,
+ "Duplicate() must be overridden by sub class");
+ InternalFocusEvent* result = new InternalFocusEvent(false, mMessage, this);
+ result->AssignFocusEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ bool mFromRaise;
+ bool mIsRefocus;
+
+ void AssignFocusEventData(const InternalFocusEvent& aEvent,
+ bool aCopyTargets) {
+ AssignUIEventData(aEvent, aCopyTargets);
+
+ mFromRaise = aEvent.mFromRaise;
+ mIsRefocus = aEvent.mIsRefocus;
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalTransitionEvent
+ ******************************************************************************/
+
+class InternalTransitionEvent : public WidgetEvent {
+ public:
+ virtual InternalTransitionEvent* AsTransitionEvent() override { return this; }
+
+ InternalTransitionEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEvent(aIsTrusted, aMessage, eTransitionEventClass, aTime),
+ mElapsedTime(0.0) {}
+
+ InternalTransitionEvent(const InternalTransitionEvent& aOther) = delete;
+ InternalTransitionEvent& operator=(const InternalTransitionEvent& aOther) =
+ delete;
+ InternalTransitionEvent(InternalTransitionEvent&& aOther) = default;
+ InternalTransitionEvent& operator=(InternalTransitionEvent&& aOther) =
+ default;
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eTransitionEventClass,
+ "Duplicate() must be overridden by sub class");
+ InternalTransitionEvent* result =
+ new InternalTransitionEvent(false, mMessage, this);
+ result->AssignTransitionEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ nsString mPropertyName;
+ nsString mPseudoElement;
+ float mElapsedTime;
+
+ void AssignTransitionEventData(const InternalTransitionEvent& aEvent,
+ bool aCopyTargets) {
+ AssignEventData(aEvent, aCopyTargets);
+
+ mPropertyName = aEvent.mPropertyName;
+ mElapsedTime = aEvent.mElapsedTime;
+ mPseudoElement = aEvent.mPseudoElement;
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalAnimationEvent
+ ******************************************************************************/
+
+class InternalAnimationEvent : public WidgetEvent {
+ public:
+ virtual InternalAnimationEvent* AsAnimationEvent() override { return this; }
+
+ InternalAnimationEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetEvent(aIsTrusted, aMessage, eAnimationEventClass, aTime),
+ mElapsedTime(0.0) {}
+
+ InternalAnimationEvent(const InternalAnimationEvent& aOther) = delete;
+ InternalAnimationEvent& operator=(const InternalAnimationEvent& aOther) =
+ delete;
+ InternalAnimationEvent(InternalAnimationEvent&& aOther) = default;
+ InternalAnimationEvent& operator=(InternalAnimationEvent&& aOther) = default;
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eAnimationEventClass,
+ "Duplicate() must be overridden by sub class");
+ InternalAnimationEvent* result =
+ new InternalAnimationEvent(false, mMessage, this);
+ result->AssignAnimationEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ nsString mAnimationName;
+ nsString mPseudoElement;
+ float mElapsedTime;
+
+ void AssignAnimationEventData(const InternalAnimationEvent& aEvent,
+ bool aCopyTargets) {
+ AssignEventData(aEvent, aCopyTargets);
+
+ mAnimationName = aEvent.mAnimationName;
+ mElapsedTime = aEvent.mElapsedTime;
+ mPseudoElement = aEvent.mPseudoElement;
+ }
+};
+
+/******************************************************************************
+ * mozilla::InternalSMILTimeEvent
+ ******************************************************************************/
+
+class InternalSMILTimeEvent : public InternalUIEvent {
+ public:
+ virtual InternalSMILTimeEvent* AsSMILTimeEvent() override { return this; }
+
+ InternalSMILTimeEvent(bool aIsTrusted, EventMessage aMessage,
+ const WidgetEventTime* aTime = nullptr)
+ : InternalUIEvent(aIsTrusted, aMessage, eSMILTimeEventClass, aTime) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eSMILTimeEventClass,
+ "Duplicate() must be overridden by sub class");
+ InternalSMILTimeEvent* result =
+ new InternalSMILTimeEvent(false, mMessage, this);
+ result->AssignSMILTimeEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ void AssignSMILTimeEventData(const InternalSMILTimeEvent& aEvent,
+ bool aCopyTargets) {
+ AssignUIEventData(aEvent, aCopyTargets);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ContentEvents_h__
diff --git a/widget/DimensionRequest.cpp b/widget/DimensionRequest.cpp
new file mode 100644
index 0000000000..4f2f667643
--- /dev/null
+++ b/widget/DimensionRequest.cpp
@@ -0,0 +1,120 @@
+/* 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 "DimensionRequest.h"
+
+#include "nsIBaseWindow.h"
+#include "nsIDocShellTreeOwner.h"
+#include "mozilla/Try.h"
+
+namespace mozilla {
+
+nsresult DimensionRequest::SupplementFrom(nsIBaseWindow* aSource) {
+ NS_ENSURE_ARG_POINTER(aSource);
+ int32_t x = 0, y = 0, width = 0, height = 0;
+
+ bool needsPosition = mX.isSome() != mY.isSome();
+ bool needsSize = mWidth.isSome() != mHeight.isSome();
+
+ if (!needsPosition && !needsSize) {
+ return NS_OK;
+ }
+
+ MOZ_TRY(aSource->GetDimensions(mDimensionKind, needsPosition ? &x : nullptr,
+ needsPosition ? &y : nullptr,
+ needsSize ? &width : nullptr,
+ needsSize ? &height : nullptr));
+
+ if (needsPosition) {
+ if (mX.isNothing()) {
+ mX.emplace(x);
+ }
+ if (mY.isNothing()) {
+ mY.emplace(y);
+ }
+ }
+ if (needsSize) {
+ if (mWidth.isNothing()) {
+ mWidth.emplace(width);
+ }
+ if (mHeight.isNothing()) {
+ mHeight.emplace(height);
+ }
+ }
+
+ MOZ_ASSERT(mX.isSome() == mY.isSome());
+ MOZ_ASSERT(mWidth.isSome() == mHeight.isSome());
+ return NS_OK;
+}
+
+nsresult DimensionRequest::ApplyOuterTo(nsIBaseWindow* aTarget) {
+ NS_ENSURE_ARG_POINTER(aTarget);
+ MOZ_ASSERT(mX.isSome() == mY.isSome(),
+ "Missing dimensions should have been completed.");
+ MOZ_ASSERT(mWidth.isSome() == mHeight.isSome(),
+ "Missing dimensions should have been completed.");
+ if (mDimensionKind != DimensionKind::Outer) {
+ MOZ_ASSERT_UNREACHABLE("Expected outer dimensions.");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ bool havePosition = mX.isSome() && mY.isSome();
+ bool haveSize = mWidth.isSome() && mHeight.isSome();
+
+ if (!havePosition && !haveSize) {
+ return NS_OK;
+ }
+
+ if (havePosition && haveSize) {
+ return aTarget->SetPositionAndSize(*mX, *mY, *mWidth, *mHeight, true);
+ }
+
+ if (havePosition) {
+ return aTarget->SetPosition(*mX, *mY);
+ }
+
+ if (haveSize) {
+ return aTarget->SetSize(*mWidth, *mHeight, true);
+ }
+
+ MOZ_ASSERT_UNREACHABLE();
+ return NS_ERROR_UNEXPECTED;
+}
+
+nsresult DimensionRequest::ApplyInnerTo(nsIDocShellTreeOwner* aTarget,
+ bool aAsRootShell) {
+ NS_ENSURE_ARG_POINTER(aTarget);
+ MOZ_ASSERT(mX.isSome() == mY.isSome(),
+ "Missing dimensions should have been completed.");
+ MOZ_ASSERT(mWidth.isSome() == mHeight.isSome(),
+ "Missing dimensions should have been completed.");
+ if (mDimensionKind != DimensionKind::Inner) {
+ MOZ_ASSERT_UNREACHABLE("Expected inner dimensions.");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ bool havePosition = mX.isSome() && mY.isSome();
+ bool haveSize = mWidth.isSome() && mHeight.isSome();
+
+ if (!havePosition && !haveSize) {
+ return NS_OK;
+ }
+
+ if (havePosition) {
+ MOZ_ASSERT_UNREACHABLE("Inner position is not implemented.");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ if (haveSize) {
+ if (aAsRootShell) {
+ return aTarget->SetRootShellSize(*mWidth, *mHeight);
+ }
+ return aTarget->SetPrimaryContentSize(*mWidth, *mHeight);
+ }
+
+ MOZ_ASSERT_UNREACHABLE();
+ return NS_ERROR_UNEXPECTED;
+}
+
+} // namespace mozilla
diff --git a/widget/DimensionRequest.h b/widget/DimensionRequest.h
new file mode 100644
index 0000000000..75f5c96b45
--- /dev/null
+++ b/widget/DimensionRequest.h
@@ -0,0 +1,71 @@
+/* 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/. */
+
+#ifndef mozilla_DimensionRequest_h
+#define mozilla_DimensionRequest_h
+
+#include "Units.h"
+#include "mozilla/Maybe.h"
+
+class nsIBaseWindow;
+class nsIDocShellTreeOwner;
+
+namespace mozilla {
+
+enum class DimensionKind { Inner, Outer };
+
+/**
+ * DimensionRequest allows to request the change of some dimensions without
+ * having to specify the unchanged dimensions. This is specifically necessary
+ * when a change is initiated from a child process, which might not have an
+ * up-to-date view of its latest dimensions. Having to specify the missing
+ * dimensions with an outdated view can revert a previous change.
+ *
+ * The following series of changes `window.screenX = 10; window.screenY = 10;`
+ * essentially translates into two moveTo() calls. For the second call we want
+ * to account for changes made by the first call. From a child process we would
+ * end up crafting the second call without knowing the results of the first
+ * call. In the parent process we have access to results of the first call
+ * before crafting the second one. Although on Linux even the parent process
+ * doesn't have immediate access to the results of the last change.
+ *
+ * Note: The concept of an inner position is not present on
+ * nsIDocShellTreeOwner and nsIBaseWindow. A request specifying an inner
+ * position will return an NS_ERROR_NOT_IMPLEMENTED.
+ */
+struct DimensionRequest {
+ DimensionKind mDimensionKind;
+ Maybe<LayoutDeviceIntCoord> mX;
+ Maybe<LayoutDeviceIntCoord> mY;
+ Maybe<LayoutDeviceIntCoord> mWidth;
+ Maybe<LayoutDeviceIntCoord> mHeight;
+
+ /**
+ * Fills the missing dimensions with values obtained from `aSource`. Whether
+ * inner dimensions are supported depends on the implementation of
+ * `nsIBaseWindow::GetDimensions` for `aSource`.
+ *
+ * @param aSource The source for the missing dimensions.
+ */
+ nsresult SupplementFrom(nsIBaseWindow* aSource);
+
+ /**
+ * Changes the outer size and or position of `aTarget`. Only outer dimensions
+ * are supported.
+ *
+ * @param aTarget The target whose size or position we want to change.
+ */
+ nsresult ApplyOuterTo(nsIBaseWindow* aTarget);
+
+ /**
+ * Changes the inner size of `aTarget`. Only inner dimensions are supported.
+ *
+ * @param aTarget The target whose size we want to change.
+ */
+ nsresult ApplyInnerTo(nsIDocShellTreeOwner* aTarget, bool aAsRootShell);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_DimensionRequest_h
diff --git a/widget/EventClassList.h b/widget/EventClassList.h
new file mode 100644
index 0000000000..c8870d84e5
--- /dev/null
+++ b/widget/EventClassList.h
@@ -0,0 +1,58 @@
+/* -*- 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/. */
+
+/**
+ * This header file lists up all event classes and related structs.
+ * Define NS_EVENT_CLASS(aPrefix, aName) and NS_ROOT_EVENT_CLASS(aPrefix, aName)
+ * before including this.
+ * If an event name is WidgetInputEvent, aPrefix is "Widget" and aName is
+ * "InputEvent". NS_ROOT_EVENT_CLASS() is only used for WidgetEvent for
+ * allowing special handling for it. If you don't need such special handling,
+ * you can define it as:
+ * #define NS_ROOT_EVENT_CLASS(aPrefix, aName) NS_EVENT_CLASS(aPrefix, aName)
+ */
+
+// BasicEvents.h
+NS_ROOT_EVENT_CLASS(Widget, Event)
+NS_EVENT_CLASS(Widget, GUIEvent)
+NS_EVENT_CLASS(Widget, InputEvent)
+NS_EVENT_CLASS(Internal, UIEvent)
+
+// TextEvents.h
+NS_EVENT_CLASS(Widget, KeyboardEvent)
+NS_EVENT_CLASS(Widget, CompositionEvent)
+NS_EVENT_CLASS(Widget, QueryContentEvent)
+NS_EVENT_CLASS(Widget, SelectionEvent)
+NS_EVENT_CLASS(Internal, EditorInputEvent)
+
+// MouseEvents.h
+NS_EVENT_CLASS(Widget, MouseEventBase)
+NS_EVENT_CLASS(Widget, MouseEvent)
+NS_EVENT_CLASS(Widget, DragEvent)
+NS_EVENT_CLASS(Widget, MouseScrollEvent)
+NS_EVENT_CLASS(Widget, WheelEvent)
+NS_EVENT_CLASS(Widget, PointerEvent)
+
+// TouchEvents.h
+NS_EVENT_CLASS(Widget, GestureNotifyEvent)
+NS_EVENT_CLASS(Widget, SimpleGestureEvent)
+NS_EVENT_CLASS(Widget, TouchEvent)
+
+// ContentEvents.h
+NS_EVENT_CLASS(Internal, ScrollPortEvent)
+NS_EVENT_CLASS(Internal, ScrollAreaEvent)
+NS_EVENT_CLASS(Internal, FormEvent)
+NS_EVENT_CLASS(Internal, ClipboardEvent)
+NS_EVENT_CLASS(Internal, FocusEvent)
+NS_EVENT_CLASS(Internal, TransitionEvent)
+NS_EVENT_CLASS(Internal, AnimationEvent)
+NS_EVENT_CLASS(Internal, SMILTimeEvent)
+
+// MiscEvents.h
+NS_EVENT_CLASS(Widget, CommandEvent)
+NS_EVENT_CLASS(Widget, ContentCommandEvent)
+
+// InternalMutationEvent.h (dom/events)
+NS_EVENT_CLASS(Internal, MutationEvent)
diff --git a/widget/EventForwards.h b/widget/EventForwards.h
new file mode 100644
index 0000000000..e6a1ad6274
--- /dev/null
+++ b/widget/EventForwards.h
@@ -0,0 +1,497 @@
+/* -*- 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/. */
+
+#ifndef mozilla_EventForwards_h__
+#define mozilla_EventForwards_h__
+
+#include <stdint.h>
+
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+#ifdef DEBUG
+# include "mozilla/StaticPrefs_dom.h"
+#endif // #ifdef DEBUG
+
+class nsCommandParams;
+
+/**
+ * XXX Following enums should be in BasicEvents.h. However, currently, it's
+ * impossible to use foward delearation for enum.
+ */
+
+/**
+ * Return status for event processors.
+ */
+enum nsEventStatus {
+ // The event is ignored, do default processing
+ nsEventStatus_eIgnore,
+ // The event is consumed, don't do default processing
+ nsEventStatus_eConsumeNoDefault,
+ // The event is consumed, but do default processing
+ nsEventStatus_eConsumeDoDefault,
+ // Value is not for use, only for serialization
+ nsEventStatus_eSentinel
+};
+
+namespace mozilla {
+
+enum class CanBubble { eYes, eNo };
+
+enum class Cancelable { eYes, eNo };
+
+enum class ChromeOnlyDispatch { eYes, eNo };
+
+enum class Trusted { eYes, eNo };
+
+enum class Composed { eYes, eNo, eDefault };
+
+/**
+ * Event messages
+ */
+
+typedef uint16_t EventMessageType;
+
+enum EventMessage : EventMessageType {
+
+#define NS_EVENT_MESSAGE(aMessage) aMessage,
+#define NS_EVENT_MESSAGE_FIRST_LAST(aMessage, aFirst, aLast) \
+ aMessage##First = aFirst, aMessage##Last = aLast,
+
+#include "mozilla/EventMessageList.h"
+
+#undef NS_EVENT_MESSAGE
+#undef NS_EVENT_MESSAGE_FIRST_LAST
+
+ // For preventing bustage due to "," after the last item.
+ eEventMessage_MaxValue
+};
+
+const char* ToChar(EventMessage aEventMessage);
+
+/**
+ * Event class IDs
+ */
+
+typedef uint8_t EventClassIDType;
+
+enum EventClassID : EventClassIDType {
+// The event class name will be:
+// eBasicEventClass for WidgetEvent
+// eFooEventClass for WidgetFooEvent or InternalFooEvent
+#define NS_ROOT_EVENT_CLASS(aPrefix, aName) eBasic##aName##Class
+#define NS_EVENT_CLASS(aPrefix, aName) , e##aName##Class
+
+#include "mozilla/EventClassList.h"
+
+#undef NS_EVENT_CLASS
+#undef NS_ROOT_EVENT_CLASS
+};
+
+const char* ToChar(EventClassID aEventClassID);
+
+typedef uint16_t Modifiers;
+
+#define NS_DEFINE_KEYNAME(aCPPName, aDOMKeyName) KEY_NAME_INDEX_##aCPPName,
+
+typedef uint16_t KeyNameIndexType;
+enum KeyNameIndex : KeyNameIndexType {
+#include "mozilla/KeyNameList.h"
+ // If a DOM keyboard event is synthesized by script, this is used. Then,
+ // specified key name should be stored and use it as .key value.
+ KEY_NAME_INDEX_USE_STRING
+};
+
+#undef NS_DEFINE_KEYNAME
+
+const nsCString ToString(KeyNameIndex aKeyNameIndex);
+
+#define NS_DEFINE_PHYSICAL_KEY_CODE_NAME(aCPPName, aDOMCodeName) \
+ CODE_NAME_INDEX_##aCPPName,
+
+typedef uint8_t CodeNameIndexType;
+enum CodeNameIndex : CodeNameIndexType {
+#include "mozilla/PhysicalKeyCodeNameList.h"
+ // If a DOM keyboard event is synthesized by script, this is used. Then,
+ // specified code name should be stored and use it as .code value.
+ CODE_NAME_INDEX_USE_STRING
+};
+
+#undef NS_DEFINE_PHYSICAL_KEY_CODE_NAME
+
+const nsCString ToString(CodeNameIndex aCodeNameIndex);
+
+#define NS_DEFINE_INPUTTYPE(aCPPName, aDOMName) e##aCPPName,
+
+typedef uint8_t EditorInputTypeType;
+enum class EditorInputType : EditorInputTypeType {
+#include "mozilla/InputTypeList.h"
+ // If a DOM input event is synthesized by script, this is used. Then,
+ // specified input type should be stored as string and use it as .inputType
+ // value.
+ eUnknown,
+};
+
+#undef NS_DEFINE_INPUTTYPE
+
+inline bool ExposesClipboardDataOrDataTransfer(EditorInputType aInputType) {
+ switch (aInputType) {
+ case EditorInputType::eInsertFromPaste:
+ case EditorInputType::eInsertFromPasteAsQuotation:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * IsDataAvailableOnTextEditor() returns true if aInputType on TextEditor
+ * should have non-null InputEvent.data value.
+ */
+inline bool IsDataAvailableOnTextEditor(EditorInputType aInputType) {
+ switch (aInputType) {
+ case EditorInputType::eInsertText:
+ case EditorInputType::eInsertCompositionText:
+ case EditorInputType::eInsertFromComposition: // Only level 2
+ case EditorInputType::eInsertFromPaste:
+ case EditorInputType::eInsertFromPasteAsQuotation:
+ case EditorInputType::eInsertTranspose:
+ case EditorInputType::eInsertFromDrop:
+ case EditorInputType::eInsertReplacementText:
+ case EditorInputType::eInsertFromYank:
+ case EditorInputType::eFormatSetBlockTextDirection:
+ case EditorInputType::eFormatSetInlineTextDirection:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * IsDataAvailableOnHTMLEditor() returns true if aInputType on HTMLEditor
+ * should have non-null InputEvent.data value.
+ */
+inline bool IsDataAvailableOnHTMLEditor(EditorInputType aInputType) {
+ switch (aInputType) {
+ case EditorInputType::eInsertText:
+ case EditorInputType::eInsertCompositionText:
+ case EditorInputType::eInsertFromComposition: // Only level 2
+ case EditorInputType::eFormatSetBlockTextDirection:
+ case EditorInputType::eFormatSetInlineTextDirection:
+ case EditorInputType::eInsertLink:
+ case EditorInputType::eFormatBackColor:
+ case EditorInputType::eFormatFontColor:
+ case EditorInputType::eFormatFontName:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * IsDataTransferAvailableOnHTMLEditor() returns true if aInputType on
+ * HTMLEditor should have non-null InputEvent.dataTransfer value.
+ */
+inline bool IsDataTransferAvailableOnHTMLEditor(EditorInputType aInputType) {
+ switch (aInputType) {
+ case EditorInputType::eInsertFromPaste:
+ case EditorInputType::eInsertFromPasteAsQuotation:
+ case EditorInputType::eInsertFromDrop:
+ case EditorInputType::eInsertTranspose:
+ case EditorInputType::eInsertReplacementText:
+ case EditorInputType::eInsertFromYank:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * MayHaveTargetRangesOnHTMLEditor() returns true if "beforeinput" event whose
+ * whose inputType is aInputType on HTMLEditor may return non-empty static
+ * range array from getTargetRanges().
+ * Note that TextEditor always sets empty array. Therefore, there is no
+ * method for TextEditor.
+ */
+inline bool MayHaveTargetRangesOnHTMLEditor(EditorInputType aInputType) {
+ switch (aInputType) {
+ // Explicitly documented by the specs.
+ case EditorInputType::eHistoryRedo:
+ case EditorInputType::eHistoryUndo:
+ // Not documented, but other browsers use empty array.
+ case EditorInputType::eFormatSetBlockTextDirection:
+ return false;
+ default:
+ return true;
+ }
+}
+
+/**
+ * IsCancelableBeforeInputEvent() returns true if `beforeinput` event for
+ * aInputType should be cancelable.
+ *
+ * Input Events Level 1:
+ * https://rawgit.com/w3c/input-events/v1/index.html#x5-1-2-attributes
+ * Input Events Level 2:
+ * https://w3c.github.io/input-events/#interface-InputEvent-Attributes
+ */
+inline bool IsCancelableBeforeInputEvent(EditorInputType aInputType) {
+ switch (aInputType) {
+ case EditorInputType::eInsertText:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eInsertReplacementText:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eInsertLineBreak:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eInsertParagraph:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eInsertOrderedList:
+ return true;
+ case EditorInputType::eInsertUnorderedList:
+ return true;
+ case EditorInputType::eInsertHorizontalRule:
+ return true;
+ case EditorInputType::eInsertFromYank:
+ return true;
+ case EditorInputType::eInsertFromDrop:
+ return true;
+ case EditorInputType::eInsertFromPaste:
+ return true;
+ case EditorInputType::eInsertFromPasteAsQuotation:
+ return true;
+ case EditorInputType::eInsertTranspose:
+ return true;
+ case EditorInputType::eInsertCompositionText:
+ return false;
+ case EditorInputType::eInsertFromComposition:
+ MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
+ return true;
+ case EditorInputType::eInsertLink:
+ return true;
+ case EditorInputType::eDeleteByComposition:
+ MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
+ return true;
+ case EditorInputType::eDeleteCompositionText:
+ MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
+ return false;
+ case EditorInputType::eDeleteWordBackward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteWordForward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteSoftLineBackward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteSoftLineForward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteEntireSoftLine:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteHardLineBackward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteHardLineForward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteByDrag:
+ return true;
+ case EditorInputType::eDeleteByCut:
+ return true;
+ case EditorInputType::eDeleteContent:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteContentBackward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eDeleteContentForward:
+ return true; // In Level 1, undefined.
+ case EditorInputType::eHistoryUndo:
+ return true;
+ case EditorInputType::eHistoryRedo:
+ return true;
+ case EditorInputType::eFormatBold:
+ return true;
+ case EditorInputType::eFormatItalic:
+ return true;
+ case EditorInputType::eFormatUnderline:
+ return true;
+ case EditorInputType::eFormatStrikeThrough:
+ return true;
+ case EditorInputType::eFormatSuperscript:
+ return true;
+ case EditorInputType::eFormatSubscript:
+ return true;
+ case EditorInputType::eFormatJustifyFull:
+ return true;
+ case EditorInputType::eFormatJustifyCenter:
+ return true;
+ case EditorInputType::eFormatJustifyRight:
+ return true;
+ case EditorInputType::eFormatJustifyLeft:
+ return true;
+ case EditorInputType::eFormatIndent:
+ return true;
+ case EditorInputType::eFormatOutdent:
+ return true;
+ case EditorInputType::eFormatRemove:
+ return true;
+ case EditorInputType::eFormatSetBlockTextDirection:
+ return true;
+ case EditorInputType::eFormatSetInlineTextDirection:
+ return true;
+ case EditorInputType::eFormatBackColor:
+ return true;
+ case EditorInputType::eFormatFontColor:
+ return true;
+ case EditorInputType::eFormatFontName:
+ return true;
+ case EditorInputType::eUnknown:
+ // This is not declared by Input Events, but it does not make sense to
+ // allow web apps to cancel default action without inputType value check.
+ // If some our specific edit actions should be cancelable, new inputType
+ // value for them should be declared by the spec.
+ return false;
+ default:
+ MOZ_ASSERT_UNREACHABLE("The new input type is not handled");
+ return false;
+ }
+}
+
+#define NS_DEFINE_COMMAND(aName, aCommandStr) , aName
+#define NS_DEFINE_COMMAND_WITH_PARAM(aName, aCommandStr, aParam) , aName
+#define NS_DEFINE_COMMAND_NO_EXEC_COMMAND(aName) , aName
+
+typedef uint8_t CommandInt;
+enum class Command : CommandInt {
+ DoNothing
+
+#include "mozilla/CommandList.h"
+};
+#undef NS_DEFINE_COMMAND
+#undef NS_DEFINE_COMMAND_WITH_PARAM
+#undef NS_DEFINE_COMMAND_NO_EXEC_COMMAND
+
+const char* ToChar(Command aCommand);
+
+/**
+ * Return a command value for aCommandName.
+ * XXX: Is there a better place to put `Command` related methods instead of
+ * global scope in `mozilla` namespace?
+ *
+ * @param aCommandName Should be a XUL command name like "cmd_bold"
+ * (case sensitive).
+ * @param aCommandparams Additional parameter value of aCommandName.
+ * Can be nullptr, but if aCommandName requires
+ * additional parameter and sets this to nullptr,
+ * will return Command::DoNothing with warning.
+ */
+Command GetInternalCommand(const char* aCommandName,
+ const nsCommandParams* aCommandParams = nullptr);
+
+} // namespace mozilla
+
+/**
+ * All header files should include this header instead of *Events.h.
+ */
+
+namespace mozilla {
+
+template <class T>
+class OwningNonNull;
+
+namespace dom {
+class StaticRange;
+}
+
+#define NS_EVENT_CLASS(aPrefix, aName) class aPrefix##aName;
+#define NS_ROOT_EVENT_CLASS(aPrefix, aName) NS_EVENT_CLASS(aPrefix, aName)
+
+#include "mozilla/EventClassList.h"
+
+#undef NS_EVENT_CLASS
+#undef NS_ROOT_EVENT_CLASS
+
+// BasicEvents.h
+struct BaseEventFlags;
+struct EventFlags;
+
+class WidgetEventTime;
+
+// TextEvents.h
+enum class AccessKeyType;
+
+struct AlternativeCharCode;
+struct ShortcutKeyCandidate;
+
+typedef nsTArray<ShortcutKeyCandidate> ShortcutKeyCandidateArray;
+typedef AutoTArray<ShortcutKeyCandidate, 10> AutoShortcutKeyCandidateArray;
+
+// TextRange.h
+typedef uint8_t RawTextRangeType;
+enum class TextRangeType : RawTextRangeType;
+
+struct TextRangeStyle;
+struct TextRange;
+
+class EditCommands;
+class TextRangeArray;
+
+typedef nsTArray<OwningNonNull<dom::StaticRange>> OwningNonNullStaticRangeArray;
+
+// FontRange.h
+struct FontRange;
+
+enum MouseButton : int16_t {
+ eNotPressed = -1,
+ ePrimary = 0,
+ eMiddle = 1,
+ eSecondary = 2,
+ eX1 = 3, // Typically, "back" button
+ eX2 = 4, // Typically, "forward" button
+ eEraser = 5
+};
+
+enum MouseButtonsFlag {
+ eNoButtons = 0x00,
+ ePrimaryFlag = 0x01,
+ eSecondaryFlag = 0x02,
+ eMiddleFlag = 0x04,
+ // typicall, "back" button being left side of 5-button
+ // mice, see "buttons" attribute document of DOM3 Events.
+ e4thFlag = 0x08,
+ // typicall, "forward" button being right side of 5-button
+ // mice, see "buttons" attribute document of DOM3 Events.
+ e5thFlag = 0x10,
+ eEraserFlag = 0x20
+};
+
+/**
+ * Returns a MouseButtonsFlag value which is changed by a button state change
+ * event whose mButton is aMouseButton.
+ */
+inline MouseButtonsFlag MouseButtonsFlagToChange(MouseButton aMouseButton) {
+ switch (aMouseButton) {
+ case MouseButton::ePrimary:
+ return MouseButtonsFlag::ePrimaryFlag;
+ case MouseButton::eMiddle:
+ return MouseButtonsFlag::eMiddleFlag;
+ case MouseButton::eSecondary:
+ return MouseButtonsFlag::eSecondaryFlag;
+ case MouseButton::eX1:
+ return MouseButtonsFlag::e4thFlag;
+ case MouseButton::eX2:
+ return MouseButtonsFlag::e5thFlag;
+ case MouseButton::eEraser:
+ return MouseButtonsFlag::eEraserFlag;
+ default:
+ return MouseButtonsFlag::eNoButtons;
+ }
+}
+
+enum class TextRangeType : RawTextRangeType;
+
+// IMEData.h
+
+template <typename IntType>
+class StartAndEndOffsets;
+template <typename IntType>
+class OffsetAndData;
+
+} // namespace mozilla
+
+#endif // mozilla_EventForwards_h__
diff --git a/widget/EventMessageList.h b/widget/EventMessageList.h
new file mode 100644
index 0000000000..ae02d04676
--- /dev/null
+++ b/widget/EventMessageList.h
@@ -0,0 +1,459 @@
+/* -*- 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/. */
+
+/**
+ * This header file lists up all event messages.
+ * Before including this header file, you should define:
+ * NS_EVENT_MESSAGE(aMessage)
+ *
+ * Additionally, you can specify following macro for e*First and e*Last.
+ * NS_EVENT_MESSAGE_FIRST_LAST(aMessage, aFirst, aLast)
+ * This is optional, if you need only actual event messages, you don't need
+ * to define this macro.
+ *
+ * Naming rules of the event messages:
+ * 0. Starting with "e" prefix and use camelcase.
+ * 1. Basically, use same name as the DOM name which is fired at dispatching
+ * the event.
+ * 2. If the event message name becomes too generic, e.g., "eInvalid", that may
+ * conflict with another enum's item name, append something after the "e"
+ * prefix, e.g., "eFormInvalid".
+ */
+
+#ifndef NS_EVENT_MESSAGE_FIRST_LAST
+# define UNDEF_NS_EVENT_MESSAGE_FIRST_LAST 1
+# define NS_EVENT_MESSAGE_FIRST_LAST(aMessage, aFirst, aLast)
+#endif
+
+NS_EVENT_MESSAGE(eVoidEvent)
+
+// This is a dummy event message for all event listener implementation in
+// EventListenerManager.
+NS_EVENT_MESSAGE(eAllEvents)
+
+NS_EVENT_MESSAGE(eKeyPress)
+NS_EVENT_MESSAGE(eKeyUp)
+NS_EVENT_MESSAGE(eKeyDown)
+
+// This message is sent after a content process handles a key event or accesskey
+// to indicate that an potential accesskey was not found. The parent process may
+// then respond by, for example, opening menus and processing other shortcuts.
+// It inherits its properties from a keypress event.
+NS_EVENT_MESSAGE(eAccessKeyNotFound)
+
+NS_EVENT_MESSAGE(eResize)
+NS_EVENT_MESSAGE(eScroll)
+NS_EVENT_MESSAGE(eMozVisualResize)
+NS_EVENT_MESSAGE(eMozVisualScroll)
+
+NS_EVENT_MESSAGE(eOffline)
+NS_EVENT_MESSAGE(eOnline)
+
+NS_EVENT_MESSAGE(eLanguageChange)
+
+NS_EVENT_MESSAGE(eMouseMove)
+NS_EVENT_MESSAGE(eMouseUp)
+NS_EVENT_MESSAGE(eMouseDown)
+NS_EVENT_MESSAGE(eMouseEnterIntoWidget)
+NS_EVENT_MESSAGE(eMouseExitFromWidget)
+NS_EVENT_MESSAGE(eMouseDoubleClick)
+NS_EVENT_MESSAGE(eMouseClick)
+NS_EVENT_MESSAGE(eMouseAuxClick)
+// eMouseActivate is fired when the widget is activated by a click.
+NS_EVENT_MESSAGE(eMouseActivate)
+NS_EVENT_MESSAGE(eMouseOver)
+NS_EVENT_MESSAGE(eMouseOut)
+NS_EVENT_MESSAGE(eMouseHitTest)
+NS_EVENT_MESSAGE(eMouseEnter)
+NS_EVENT_MESSAGE(eMouseLeave)
+NS_EVENT_MESSAGE(eMouseTouchDrag)
+NS_EVENT_MESSAGE(eMouseLongTap)
+NS_EVENT_MESSAGE(eMouseExploreByTouch)
+NS_EVENT_MESSAGE_FIRST_LAST(eMouseEvent, eMouseMove, eMouseExploreByTouch)
+
+// Pointer spec events
+NS_EVENT_MESSAGE(ePointerMove)
+NS_EVENT_MESSAGE(ePointerUp)
+NS_EVENT_MESSAGE(ePointerDown)
+NS_EVENT_MESSAGE(ePointerOver)
+NS_EVENT_MESSAGE(ePointerOut)
+NS_EVENT_MESSAGE(ePointerEnter)
+NS_EVENT_MESSAGE(ePointerLeave)
+NS_EVENT_MESSAGE(ePointerCancel)
+NS_EVENT_MESSAGE(ePointerGotCapture)
+NS_EVENT_MESSAGE(ePointerLostCapture)
+NS_EVENT_MESSAGE_FIRST_LAST(ePointerEvent, ePointerMove, ePointerLostCapture)
+
+NS_EVENT_MESSAGE(eContextMenu)
+
+NS_EVENT_MESSAGE(eCueChange)
+
+NS_EVENT_MESSAGE(eBeforeToggle)
+
+NS_EVENT_MESSAGE(eLoad)
+NS_EVENT_MESSAGE(eUnload)
+NS_EVENT_MESSAGE(eHashChange)
+NS_EVENT_MESSAGE(eImageAbort)
+NS_EVENT_MESSAGE(eLoadError)
+NS_EVENT_MESSAGE(eLoadEnd)
+NS_EVENT_MESSAGE(ePopState)
+NS_EVENT_MESSAGE(eRejectionHandled)
+NS_EVENT_MESSAGE(eStorage)
+NS_EVENT_MESSAGE(eUnhandledRejection)
+NS_EVENT_MESSAGE(eBeforeUnload)
+NS_EVENT_MESSAGE(eReadyStateChange)
+
+NS_EVENT_MESSAGE(eFormSubmit)
+NS_EVENT_MESSAGE(eFormReset)
+NS_EVENT_MESSAGE(eFormChange)
+NS_EVENT_MESSAGE(eFormSelect)
+NS_EVENT_MESSAGE(eFormInvalid)
+NS_EVENT_MESSAGE(eFormCheckboxStateChange)
+NS_EVENT_MESSAGE(eFormRadioStateChange)
+NS_EVENT_MESSAGE(eFormData)
+
+// Need separate focus/blur notifications for non-native widgets
+NS_EVENT_MESSAGE(eFocus)
+NS_EVENT_MESSAGE(eBlur)
+NS_EVENT_MESSAGE(eFocusIn)
+NS_EVENT_MESSAGE(eFocusOut)
+
+NS_EVENT_MESSAGE(eDragEnter)
+NS_EVENT_MESSAGE(eDragOver)
+NS_EVENT_MESSAGE(eDragExit)
+NS_EVENT_MESSAGE(eDrag)
+NS_EVENT_MESSAGE(eDragEnd)
+NS_EVENT_MESSAGE(eDragStart)
+NS_EVENT_MESSAGE(eDrop)
+NS_EVENT_MESSAGE(eDragLeave)
+NS_EVENT_MESSAGE_FIRST_LAST(eDragDropEvent, eDragEnter, eDragLeave)
+
+// XUL specific events
+NS_EVENT_MESSAGE(eXULPopupShowing)
+NS_EVENT_MESSAGE(eXULPopupShown)
+NS_EVENT_MESSAGE(eXULPopupHiding)
+NS_EVENT_MESSAGE(eXULPopupHidden)
+NS_EVENT_MESSAGE(eXULBroadcast)
+NS_EVENT_MESSAGE(eXULCommandUpdate)
+NS_EVENT_MESSAGE(eXULSystemStatusBarClick)
+
+// Legacy mouse scroll (wheel) events
+NS_EVENT_MESSAGE(eLegacyMouseLineOrPageScroll)
+NS_EVENT_MESSAGE(eLegacyMousePixelScroll)
+
+NS_EVENT_MESSAGE(eScrollPortUnderflow)
+NS_EVENT_MESSAGE(eScrollPortOverflow)
+
+NS_EVENT_MESSAGE(eLegacySubtreeModified)
+NS_EVENT_MESSAGE(eLegacyNodeInserted)
+NS_EVENT_MESSAGE(eLegacyNodeRemoved)
+NS_EVENT_MESSAGE(eLegacyNodeRemovedFromDocument)
+NS_EVENT_MESSAGE(eLegacyNodeInsertedIntoDocument)
+NS_EVENT_MESSAGE(eLegacyAttrModified)
+NS_EVENT_MESSAGE(eLegacyCharacterDataModified)
+NS_EVENT_MESSAGE_FIRST_LAST(eLegacyMutationEvent, eLegacySubtreeModified,
+ eLegacyCharacterDataModified)
+
+NS_EVENT_MESSAGE(eUnidentifiedEvent)
+
+// composition events
+NS_EVENT_MESSAGE(eCompositionStart)
+// eCompositionEnd is the message for DOM compositionend event.
+// This event should NOT be dispatched from widget if eCompositionCommit
+// is available.
+NS_EVENT_MESSAGE(eCompositionEnd)
+// eCompositionUpdate is the message for DOM compositionupdate event.
+// This event should NOT be dispatched from widget since it will be dispatched
+// by mozilla::TextComposition automatically if eCompositionChange event
+// will change composition string.
+NS_EVENT_MESSAGE(eCompositionUpdate)
+// eCompositionChange is the message for representing a change of
+// composition string. This should be dispatched from widget even if
+// composition string isn't changed but the ranges are changed. This causes
+// a DOM "text" event which is a non-standard DOM event.
+NS_EVENT_MESSAGE(eCompositionChange)
+// eCompositionCommitAsIs is the message for representing a commit of
+// composition string. TextComposition will commit composition with the
+// last data. TextComposition will dispatch this event to the DOM tree as
+// eCompositionChange without clause information. After that,
+// eCompositionEnd will be dispatched automatically.
+// Its mData and mRanges should be empty and nullptr.
+NS_EVENT_MESSAGE(eCompositionCommitAsIs)
+// eCompositionCommit is the message for representing a commit of
+// composition string with its mData value. TextComposition will dispatch this
+// event to the DOM tree as eCompositionChange without clause information.
+// After that, eCompositionEnd will be dispatched automatically.
+// Its mRanges should be nullptr.
+NS_EVENT_MESSAGE(eCompositionCommit)
+// eCompositionCommitRequestHandled is NOT used with any Widget*Event.
+// This is used only by PBrowser.OnEventNeedingAckHandled(). If active IME
+// commits composition synchronously, BrowserParent returns the commit string
+// to the remote process synchronously. Then, BrowserChild dispatches
+// eCompositionCommit in the remote process. Finally, this message is sent
+// to BrowserParent. (If IME commits composition asynchronously, this message
+// is not used.)
+NS_EVENT_MESSAGE(eCompositionCommitRequestHandled)
+
+// Following events are defined for deprecated DOM events which are using
+// InternalUIEvent class.
+// DOMActivate (mapped with the DOM event and used internally)
+NS_EVENT_MESSAGE(eLegacyDOMActivate)
+// DOMFocusIn (only mapped with the DOM event)
+NS_EVENT_MESSAGE(eLegacyDOMFocusIn)
+// DOMFocusOut (only mapped with the DOM event)
+NS_EVENT_MESSAGE(eLegacyDOMFocusOut)
+
+// pagetransition events
+NS_EVENT_MESSAGE(ePageShow)
+NS_EVENT_MESSAGE(ePageHide)
+
+// SVG events
+NS_EVENT_MESSAGE(eSVGLoad)
+NS_EVENT_MESSAGE(eSVGScroll)
+
+// XUL command events
+NS_EVENT_MESSAGE(eXULCommand)
+
+// Cut, copy, paste events
+NS_EVENT_MESSAGE(eCopy)
+NS_EVENT_MESSAGE(eCut)
+NS_EVENT_MESSAGE(ePaste)
+NS_EVENT_MESSAGE(ePasteNoFormatting)
+
+// Query for the selected text information, it return the selection offset,
+// selection length and selected text.
+NS_EVENT_MESSAGE(eQuerySelectedText)
+// Query for the text content of specified range, it returns actual lengh (if
+// the specified range is too long) and the text of the specified range.
+// Returns the entire text if requested length > actual length.
+NS_EVENT_MESSAGE(eQueryTextContent)
+// Query for the caret rect of nth insertion point. The offset of the result is
+// relative position from the top level widget.
+NS_EVENT_MESSAGE(eQueryCaretRect)
+// Query for the bounding rect of a range of characters. This works on any
+// valid character range given offset and length. Result is relative to top
+// level widget coordinates
+NS_EVENT_MESSAGE(eQueryTextRect)
+// Query for the bounding rect array of a range of characters.
+// Thiis similar event of eQueryTextRect.
+NS_EVENT_MESSAGE(eQueryTextRectArray)
+// Query for the bounding rect of the current focused frame. Result is relative
+// to top level widget coordinates
+NS_EVENT_MESSAGE(eQueryEditorRect)
+// Query for the current state of the content. The particular members of
+// mReply that are set for each query content event will be valid on success.
+NS_EVENT_MESSAGE(eQueryContentState)
+// Query for the selection in the form of a nsITransferable.
+NS_EVENT_MESSAGE(eQuerySelectionAsTransferable)
+// Query for character at a point. This returns the character offset, its
+// rect and also tentative caret point if the point is clicked. The point is
+// specified by Event::mRefPoint.
+NS_EVENT_MESSAGE(eQueryCharacterAtPoint)
+// Query if the DOM element under Event::mRefPoint belongs to our widget
+// or not.
+NS_EVENT_MESSAGE(eQueryDOMWidgetHittest)
+
+// Video events
+NS_EVENT_MESSAGE(eLoadStart)
+NS_EVENT_MESSAGE(eProgress)
+NS_EVENT_MESSAGE(eSuspend)
+NS_EVENT_MESSAGE(eEmptied)
+NS_EVENT_MESSAGE(eStalled)
+NS_EVENT_MESSAGE(ePlay)
+NS_EVENT_MESSAGE(ePause)
+NS_EVENT_MESSAGE(eLoadedMetaData)
+NS_EVENT_MESSAGE(eLoadedData)
+NS_EVENT_MESSAGE(eWaiting)
+NS_EVENT_MESSAGE(ePlaying)
+NS_EVENT_MESSAGE(eCanPlay)
+NS_EVENT_MESSAGE(eCanPlayThrough)
+NS_EVENT_MESSAGE(eSeeking)
+NS_EVENT_MESSAGE(eSeeked)
+NS_EVENT_MESSAGE(eTimeUpdate)
+NS_EVENT_MESSAGE(eEnded)
+NS_EVENT_MESSAGE(eRateChange)
+NS_EVENT_MESSAGE(eDurationChange)
+NS_EVENT_MESSAGE(eVolumeChange)
+
+// paint notification events
+NS_EVENT_MESSAGE(eAfterPaint)
+
+// Simple gesture events
+NS_EVENT_MESSAGE(eSwipeGestureMayStart)
+NS_EVENT_MESSAGE(eSwipeGestureStart)
+NS_EVENT_MESSAGE(eSwipeGestureUpdate)
+NS_EVENT_MESSAGE(eSwipeGestureEnd)
+NS_EVENT_MESSAGE(eSwipeGesture)
+NS_EVENT_MESSAGE(eMagnifyGestureStart)
+NS_EVENT_MESSAGE(eMagnifyGestureUpdate)
+NS_EVENT_MESSAGE(eMagnifyGesture)
+NS_EVENT_MESSAGE(eRotateGestureStart)
+NS_EVENT_MESSAGE(eRotateGestureUpdate)
+NS_EVENT_MESSAGE(eRotateGesture)
+NS_EVENT_MESSAGE(eTapGesture)
+NS_EVENT_MESSAGE(ePressTapGesture)
+NS_EVENT_MESSAGE(eEdgeUIStarted)
+NS_EVENT_MESSAGE(eEdgeUICanceled)
+NS_EVENT_MESSAGE(eEdgeUICompleted)
+
+// Events to manipulate selection (WidgetSelectionEvent)
+// Clear any previous selection and set the given range as the selection
+NS_EVENT_MESSAGE(eSetSelection)
+
+// Events of commands for the contents
+NS_EVENT_MESSAGE(eContentCommandCut)
+NS_EVENT_MESSAGE(eContentCommandCopy)
+NS_EVENT_MESSAGE(eContentCommandPaste)
+NS_EVENT_MESSAGE(eContentCommandDelete)
+NS_EVENT_MESSAGE(eContentCommandUndo)
+NS_EVENT_MESSAGE(eContentCommandRedo)
+// eContentCommandInsertText tries to insert text with replacing selection
+// in focused editor.
+NS_EVENT_MESSAGE(eContentCommandInsertText)
+NS_EVENT_MESSAGE(eContentCommandPasteTransferable)
+NS_EVENT_MESSAGE(eContentCommandLookUpDictionary)
+// eContentCommandScroll scrolls the nearest scrollable element to the
+// currently focused content or latest DOM selection. This would normally be
+// the same element scrolled by keyboard scroll commands, except that this event
+// will scroll an element scrollable in either direction. I.e., if the nearest
+// scrollable ancestor element can only be scrolled vertically, and horizontal
+// scrolling is requested using this event, no scrolling will occur.
+NS_EVENT_MESSAGE(eContentCommandScroll)
+
+// Event to gesture notification
+NS_EVENT_MESSAGE(eGestureNotify)
+
+NS_EVENT_MESSAGE(eScrolledAreaChanged)
+
+// CSS Transition & Animation events:
+NS_EVENT_MESSAGE(eTransitionStart)
+NS_EVENT_MESSAGE(eTransitionRun)
+NS_EVENT_MESSAGE(eTransitionEnd)
+NS_EVENT_MESSAGE(eTransitionCancel)
+NS_EVENT_MESSAGE(eAnimationStart)
+NS_EVENT_MESSAGE(eAnimationEnd)
+NS_EVENT_MESSAGE(eAnimationIteration)
+NS_EVENT_MESSAGE(eAnimationCancel)
+
+// Webkit-prefixed versions of Transition & Animation events, for web compat:
+NS_EVENT_MESSAGE(eWebkitTransitionEnd)
+NS_EVENT_MESSAGE(eWebkitAnimationStart)
+NS_EVENT_MESSAGE(eWebkitAnimationEnd)
+NS_EVENT_MESSAGE(eWebkitAnimationIteration)
+
+NS_EVENT_MESSAGE(eSMILBeginEvent)
+NS_EVENT_MESSAGE(eSMILEndEvent)
+NS_EVENT_MESSAGE(eSMILRepeatEvent)
+
+NS_EVENT_MESSAGE(eAudioProcess)
+NS_EVENT_MESSAGE(eAudioComplete)
+
+// script notification events
+NS_EVENT_MESSAGE(eBeforeScriptExecute)
+NS_EVENT_MESSAGE(eAfterScriptExecute)
+
+NS_EVENT_MESSAGE(eBeforePrint)
+NS_EVENT_MESSAGE(eAfterPrint)
+
+NS_EVENT_MESSAGE(eMessage)
+NS_EVENT_MESSAGE(eMessageError)
+NS_EVENT_MESSAGE(eRTCTransform)
+
+// Menu open event
+NS_EVENT_MESSAGE(eOpen)
+
+// Device motion and orientation
+NS_EVENT_MESSAGE(eDeviceOrientation)
+NS_EVENT_MESSAGE(eDeviceOrientationAbsolute)
+NS_EVENT_MESSAGE(eDeviceMotion)
+NS_EVENT_MESSAGE(eUserProximity)
+NS_EVENT_MESSAGE(eDeviceLight)
+#if defined(MOZ_WIDGET_ANDROID)
+NS_EVENT_MESSAGE(eOrientationChange)
+#endif
+
+// WebVR events
+NS_EVENT_MESSAGE(eVRDisplayActivate)
+NS_EVENT_MESSAGE(eVRDisplayDeactivate)
+NS_EVENT_MESSAGE(eVRDisplayConnect)
+NS_EVENT_MESSAGE(eVRDisplayDisconnect)
+NS_EVENT_MESSAGE(eVRDisplayPresentChange)
+
+// Fullscreen DOM API
+NS_EVENT_MESSAGE(eFullscreenChange)
+NS_EVENT_MESSAGE(eFullscreenError)
+NS_EVENT_MESSAGE(eMozFullscreenChange)
+NS_EVENT_MESSAGE(eMozFullscreenError)
+
+NS_EVENT_MESSAGE(eTouchStart)
+NS_EVENT_MESSAGE(eTouchMove)
+NS_EVENT_MESSAGE(eTouchEnd)
+NS_EVENT_MESSAGE(eTouchCancel)
+NS_EVENT_MESSAGE(eTouchPointerCancel)
+
+// Pointerlock DOM API
+NS_EVENT_MESSAGE(ePointerLockChange)
+NS_EVENT_MESSAGE(ePointerLockError)
+NS_EVENT_MESSAGE(eMozPointerLockChange)
+NS_EVENT_MESSAGE(eMozPointerLockError)
+
+// eWheel is the event message of DOM wheel event.
+NS_EVENT_MESSAGE(eWheel)
+// eWheelOperationStart may be dispatched when user starts to operate mouse
+// wheel. This won't be fired on some platforms which don't have corresponding
+// native event.
+NS_EVENT_MESSAGE(eWheelOperationStart)
+// eWheelOperationEnd may be dispatched when user ends or cancels operating
+// mouse wheel. This won't be fired on some platforms which don't have
+// corresponding native event.
+NS_EVENT_MESSAGE(eWheelOperationEnd)
+
+// MediaRecorder events.
+NS_EVENT_MESSAGE(eMediaRecorderDataAvailable)
+NS_EVENT_MESSAGE(eMediaRecorderWarning)
+NS_EVENT_MESSAGE(eMediaRecorderStop)
+
+// Gamepad input events
+NS_EVENT_MESSAGE(eGamepadButtonDown)
+NS_EVENT_MESSAGE(eGamepadButtonUp)
+NS_EVENT_MESSAGE(eGamepadAxisMove)
+NS_EVENT_MESSAGE(eGamepadConnected)
+NS_EVENT_MESSAGE(eGamepadDisconnected)
+NS_EVENT_MESSAGE_FIRST_LAST(eGamepadEvent, eGamepadButtonDown,
+ eGamepadDisconnected)
+
+// input and beforeinput events.
+NS_EVENT_MESSAGE(eEditorInput)
+NS_EVENT_MESSAGE(eEditorBeforeInput)
+
+// selection events
+NS_EVENT_MESSAGE(eSelectStart)
+NS_EVENT_MESSAGE(eSelectionChange)
+NS_EVENT_MESSAGE(eSlotChange)
+
+// visibility change
+NS_EVENT_MESSAGE(eVisibilityChange)
+
+// security policy events
+NS_EVENT_MESSAGE(eSecurityPolicyViolation)
+
+// Details element events.
+NS_EVENT_MESSAGE(eToggle)
+
+// Dialog element events.
+NS_EVENT_MESSAGE(eClose)
+NS_EVENT_MESSAGE(eCancel)
+
+// Marquee element events.
+NS_EVENT_MESSAGE(eMarqueeBounce)
+NS_EVENT_MESSAGE(eMarqueeStart)
+NS_EVENT_MESSAGE(eMarqueeFinish)
+
+NS_EVENT_MESSAGE(eScrollend)
+
+#ifdef UNDEF_NS_EVENT_MESSAGE_FIRST_LAST
+# undef UNDEF_NS_EVENT_MESSAGE_FIRST_LAST
+# undef NS_EVENT_MESSAGE_FIRST_LAST
+#endif
diff --git a/widget/FontRange.h b/widget/FontRange.h
new file mode 100644
index 0000000000..e5ee079a62
--- /dev/null
+++ b/widget/FontRange.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_FontRange_h_
+#define mozilla_FontRange_h_
+
+#include "gfxTypes.h" // for gfxFloat
+
+namespace mozilla {
+
+struct FontRange {
+ FontRange() : mStartOffset(0), mFontSize(0) {}
+
+ int32_t mStartOffset;
+ nsString mFontName;
+ gfxFloat mFontSize; // in device pixels
+};
+
+} // namespace mozilla
+
+#endif // mozilla_FontRange_h_
diff --git a/widget/GfxDriverInfo.cpp b/widget/GfxDriverInfo.cpp
new file mode 100644
index 0000000000..d60b023ddc
--- /dev/null
+++ b/widget/GfxDriverInfo.cpp
@@ -0,0 +1,777 @@
+/* -*- 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 "GfxDriverInfo.h"
+
+#include "nsIGfxInfo.h"
+#include "nsTArray.h"
+#include "nsUnicharUtils.h"
+
+using namespace mozilla::widget;
+
+GfxDeviceFamily*
+ GfxDriverInfo::sDeviceFamilies[static_cast<size_t>(DeviceFamily::Max)];
+nsString*
+ GfxDriverInfo::sWindowProtocol[static_cast<size_t>(WindowProtocol::Max)];
+nsString* GfxDriverInfo::sDeviceVendors[static_cast<size_t>(DeviceVendor::Max)];
+nsString* GfxDriverInfo::sDriverVendors[static_cast<size_t>(DriverVendor::Max)];
+
+GfxDriverInfo::GfxDriverInfo()
+ : mOperatingSystem(OperatingSystem::Unknown),
+ mOperatingSystemVersion(0),
+ mScreen(ScreenSizeStatus::All),
+ mBattery(BatteryStatus::All),
+ mWindowProtocol(GfxDriverInfo::GetWindowProtocol(WindowProtocol::All)),
+ mAdapterVendor(GfxDriverInfo::GetDeviceVendor(DeviceFamily::All)),
+ mDriverVendor(GfxDriverInfo::GetDriverVendor(DriverVendor::All)),
+ mDevices(GfxDriverInfo::GetDeviceFamily(DeviceFamily::All)),
+ mDeleteDevices(false),
+ mFeature(optionalFeatures),
+ mFeatureStatus(nsIGfxInfo::FEATURE_STATUS_OK),
+ mComparisonOp(DRIVER_COMPARISON_IGNORED),
+ mDriverVersion(0),
+ mDriverVersionMax(0),
+ mSuggestedVersion(nullptr),
+ mRuleId(nullptr),
+ mGpu2(false) {}
+
+GfxDriverInfo::GfxDriverInfo(
+ OperatingSystem os, ScreenSizeStatus screen, BatteryStatus battery,
+ const nsAString& windowProtocol, const nsAString& vendor,
+ const nsAString& driverVendor, GfxDeviceFamily* devices, int32_t feature,
+ int32_t featureStatus, VersionComparisonOp op, uint64_t driverVersion,
+ const char* ruleId, const char* suggestedVersion /* = nullptr */,
+ bool ownDevices /* = false */, bool gpu2 /* = false */)
+ : mOperatingSystem(os),
+ mOperatingSystemVersion(0),
+ mScreen(screen),
+ mBattery(battery),
+ mWindowProtocol(windowProtocol),
+ mAdapterVendor(vendor),
+ mDriverVendor(driverVendor),
+ mDevices(devices),
+ mDeleteDevices(ownDevices),
+ mFeature(feature),
+ mFeatureStatus(featureStatus),
+ mComparisonOp(op),
+ mDriverVersion(driverVersion),
+ mDriverVersionMax(0),
+ mSuggestedVersion(suggestedVersion),
+ mRuleId(ruleId),
+ mGpu2(gpu2) {}
+
+GfxDriverInfo::GfxDriverInfo(const GfxDriverInfo& aOrig)
+ : mOperatingSystem(aOrig.mOperatingSystem),
+ mOperatingSystemVersion(aOrig.mOperatingSystemVersion),
+ mScreen(aOrig.mScreen),
+ mBattery(aOrig.mBattery),
+ mWindowProtocol(aOrig.mWindowProtocol),
+ mAdapterVendor(aOrig.mAdapterVendor),
+ mDriverVendor(aOrig.mDriverVendor),
+ mFeature(aOrig.mFeature),
+ mFeatureStatus(aOrig.mFeatureStatus),
+ mComparisonOp(aOrig.mComparisonOp),
+ mDriverVersion(aOrig.mDriverVersion),
+ mDriverVersionMax(aOrig.mDriverVersionMax),
+ mSuggestedVersion(aOrig.mSuggestedVersion),
+ mRuleId(aOrig.mRuleId),
+ mGpu2(aOrig.mGpu2) {
+ // If we're managing the lifetime of the device family, we have to make a
+ // copy of the original's device family.
+ if (aOrig.mDeleteDevices && aOrig.mDevices) {
+ GfxDeviceFamily* devices = new GfxDeviceFamily;
+ *devices = *aOrig.mDevices;
+ mDevices = devices;
+ } else {
+ mDevices = aOrig.mDevices;
+ }
+
+ mDeleteDevices = aOrig.mDeleteDevices;
+}
+
+GfxDriverInfo::~GfxDriverInfo() {
+ if (mDeleteDevices) {
+ delete mDevices;
+ }
+}
+
+void GfxDeviceFamily::Append(const nsAString& aDeviceId) {
+ mIds.AppendElement(aDeviceId);
+}
+
+void GfxDeviceFamily::AppendRange(int32_t aBeginDeviceId,
+ int32_t aEndDeviceId) {
+ mRanges.AppendElement(
+ GfxDeviceFamily::DeviceRange{aBeginDeviceId, aEndDeviceId});
+}
+
+nsresult GfxDeviceFamily::Contains(nsAString& aDeviceId) const {
+ for (const auto& id : mIds) {
+ if (id.Equals(aDeviceId, nsCaseInsensitiveStringComparator)) {
+ return NS_OK;
+ }
+ }
+
+ if (mRanges.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult valid = NS_OK;
+ int32_t deviceId = aDeviceId.ToInteger(&valid, 16);
+ if (valid != NS_OK) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (const auto& range : mRanges) {
+ if (deviceId >= range.mBegin && deviceId <= range.mEnd) {
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+// Macros for appending a device to the DeviceFamily.
+#define APPEND_DEVICE(device) APPEND_DEVICE2(#device)
+#define APPEND_DEVICE2(device) \
+ deviceFamily->Append(NS_LITERAL_STRING_FROM_CSTRING(device))
+#define APPEND_RANGE(start, end) deviceFamily->AppendRange(start, end)
+
+const GfxDeviceFamily* GfxDriverInfo::GetDeviceFamily(DeviceFamily id) {
+ if (id >= DeviceFamily::Max) {
+ MOZ_ASSERT_UNREACHABLE("DeviceFamily id is out of range");
+ return nullptr;
+ }
+
+ // All of these have no specific device ID filtering.
+ switch (id) {
+ case DeviceFamily::All:
+ case DeviceFamily::IntelAll:
+ case DeviceFamily::NvidiaAll:
+ case DeviceFamily::AtiAll:
+ case DeviceFamily::MicrosoftAll:
+ case DeviceFamily::ParallelsAll:
+ case DeviceFamily::QualcommAll:
+ case DeviceFamily::AppleAll:
+ case DeviceFamily::AmazonAll:
+ return nullptr;
+ default:
+ break;
+ }
+
+ // If it already exists, we must have processed it once, so return it now.
+ auto idx = static_cast<size_t>(id);
+ if (sDeviceFamilies[idx]) {
+ return sDeviceFamilies[idx];
+ }
+
+ sDeviceFamilies[idx] = new GfxDeviceFamily;
+ GfxDeviceFamily* deviceFamily = sDeviceFamilies[idx];
+
+ switch (id) {
+ case DeviceFamily::IntelGMA500:
+ APPEND_DEVICE(0x8108); /* IntelGMA500_1 */
+ APPEND_DEVICE(0x8109); /* IntelGMA500_2 */
+ break;
+ case DeviceFamily::IntelGMA900:
+ APPEND_DEVICE(0x2582); /* IntelGMA900_1 */
+ APPEND_DEVICE(0x2782); /* IntelGMA900_2 */
+ APPEND_DEVICE(0x2592); /* IntelGMA900_3 */
+ APPEND_DEVICE(0x2792); /* IntelGMA900_4 */
+ break;
+ case DeviceFamily::IntelGMA950:
+ APPEND_DEVICE(0x2772); /* Intel945G_1 */
+ APPEND_DEVICE(0x2776); /* Intel945G_2 */
+ APPEND_DEVICE(0x27a2); /* Intel945_1 */
+ APPEND_DEVICE(0x27a6); /* Intel945_2 */
+ APPEND_DEVICE(0x27ae); /* Intel945_3 */
+ break;
+ case DeviceFamily::IntelGMA3150:
+ APPEND_DEVICE(0xa001); /* IntelGMA3150_Nettop_1 */
+ APPEND_DEVICE(0xa002); /* IntelGMA3150_Nettop_2 */
+ APPEND_DEVICE(0xa011); /* IntelGMA3150_Netbook_1 */
+ APPEND_DEVICE(0xa012); /* IntelGMA3150_Netbook_2 */
+ break;
+ case DeviceFamily::IntelGMAX3000:
+ APPEND_DEVICE(0x2972); /* Intel946GZ_1 */
+ APPEND_DEVICE(0x2973); /* Intel946GZ_2 */
+ APPEND_DEVICE(0x2982); /* IntelG35_1 */
+ APPEND_DEVICE(0x2983); /* IntelG35_2 */
+ APPEND_DEVICE(0x2992); /* IntelQ965_1 */
+ APPEND_DEVICE(0x2993); /* IntelQ965_2 */
+ APPEND_DEVICE(0x29a2); /* IntelG965_1 */
+ APPEND_DEVICE(0x29a3); /* IntelG965_2 */
+ APPEND_DEVICE(0x29b2); /* IntelQ35_1 */
+ APPEND_DEVICE(0x29b3); /* IntelQ35_2 */
+ APPEND_DEVICE(0x29c2); /* IntelG33_1 */
+ APPEND_DEVICE(0x29c3); /* IntelG33_2 */
+ APPEND_DEVICE(0x29d2); /* IntelQ33_1 */
+ APPEND_DEVICE(0x29d3); /* IntelQ33_2 */
+ APPEND_DEVICE(0x2a02); /* IntelGL960_1 */
+ APPEND_DEVICE(0x2a03); /* IntelGL960_2 */
+ APPEND_DEVICE(0x2a12); /* IntelGM965_1 */
+ APPEND_DEVICE(0x2a13); /* IntelGM965_2 */
+ break;
+ case DeviceFamily::IntelGMAX4500HD:
+ APPEND_DEVICE(0x2a42); /* IntelGMA4500MHD_1 */
+ APPEND_DEVICE(0x2a43); /* IntelGMA4500MHD_2 */
+ APPEND_DEVICE(0x2e42); /* IntelB43_1 */
+ APPEND_DEVICE(0x2e43); /* IntelB43_2 */
+ APPEND_DEVICE(0x2e92); /* IntelB43_3 */
+ APPEND_DEVICE(0x2e93); /* IntelB43_4 */
+ APPEND_DEVICE(0x2e32); /* IntelG41_1 */
+ APPEND_DEVICE(0x2e33); /* IntelG41_2 */
+ APPEND_DEVICE(0x2e22); /* IntelG45_1 */
+ APPEND_DEVICE(0x2e23); /* IntelG45_2 */
+ APPEND_DEVICE(0x2e12); /* IntelQ45_1 */
+ APPEND_DEVICE(0x2e13); /* IntelQ45_2 */
+ break;
+ case DeviceFamily::IntelHDGraphicsToIvyBridge:
+ APPEND_DEVICE(0x015A); /* IntelIvyBridge_GT1_1 (HD Graphics) */
+ // clang-format off
+ APPEND_DEVICE(0x0152); /* IntelIvyBridge_GT1_2 (HD Graphics 2500, desktop) */
+ APPEND_DEVICE(0x0162); /* IntelIvyBridge_GT2_1 (HD Graphics 4000, desktop) */
+ APPEND_DEVICE(0x0166); /* IntelIvyBridge_GT2_2 (HD Graphics 4000, mobile) */
+ APPEND_DEVICE(0x016A); /* IntelIvyBridge_GT2_3 (HD Graphics P4000, workstation) */
+ // clang-format on
+ [[fallthrough]];
+ case DeviceFamily::IntelHDGraphicsToSandyBridge:
+ APPEND_DEVICE(0x0042); /* IntelHDGraphics */
+ APPEND_DEVICE(0x0046); /* IntelMobileHDGraphics */
+ APPEND_DEVICE(0x0102); /* IntelSandyBridge_1 */
+ APPEND_DEVICE(0x0106); /* IntelSandyBridge_2 */
+ APPEND_DEVICE(0x0112); /* IntelSandyBridge_3 */
+ APPEND_DEVICE(0x0116); /* IntelSandyBridge_4 */
+ APPEND_DEVICE(0x0122); /* IntelSandyBridge_5 */
+ APPEND_DEVICE(0x0126); /* IntelSandyBridge_6 */
+ APPEND_DEVICE(0x010a); /* IntelSandyBridge_7 */
+ break;
+ case DeviceFamily::IntelHaswell:
+ APPEND_DEVICE(0x0402); /* IntelHaswell_GT1_1 */
+ APPEND_DEVICE(0x0406); /* IntelHaswell_GT1_2 */
+ APPEND_DEVICE(0x040A); /* IntelHaswell_GT1_3 */
+ APPEND_DEVICE(0x040B); /* IntelHaswell_GT1_4 */
+ APPEND_DEVICE(0x040E); /* IntelHaswell_GT1_5 */
+ APPEND_DEVICE(0x0A02); /* IntelHaswell_GT1_6 */
+ APPEND_DEVICE(0x0A06); /* IntelHaswell_GT1_7 */
+ APPEND_DEVICE(0x0A0A); /* IntelHaswell_GT1_8 */
+ APPEND_DEVICE(0x0A0B); /* IntelHaswell_GT1_9 */
+ APPEND_DEVICE(0x0A0E); /* IntelHaswell_GT1_10 */
+ APPEND_DEVICE(0x0412); /* IntelHaswell_GT2_1 */
+ APPEND_DEVICE(0x0416); /* IntelHaswell_GT2_2 */
+ APPEND_DEVICE(0x041A); /* IntelHaswell_GT2_3 */
+ APPEND_DEVICE(0x041B); /* IntelHaswell_GT2_4 */
+ APPEND_DEVICE(0x041E); /* IntelHaswell_GT2_5 */
+ APPEND_DEVICE(0x0A12); /* IntelHaswell_GT2_6 */
+ APPEND_DEVICE(0x0A16); /* IntelHaswell_GT2_7 */
+ APPEND_DEVICE(0x0A1A); /* IntelHaswell_GT2_8 */
+ APPEND_DEVICE(0x0A1B); /* IntelHaswell_GT2_9 */
+ APPEND_DEVICE(0x0A1E); /* IntelHaswell_GT2_10 */
+ APPEND_DEVICE(0x0422); /* IntelHaswell_GT3_1 */
+ APPEND_DEVICE(0x0426); /* IntelHaswell_GT3_2 */
+ APPEND_DEVICE(0x042A); /* IntelHaswell_GT3_3 */
+ APPEND_DEVICE(0x042B); /* IntelHaswell_GT3_4 */
+ APPEND_DEVICE(0x042E); /* IntelHaswell_GT3_5 */
+ APPEND_DEVICE(0x0A22); /* IntelHaswell_GT3_6 */
+ APPEND_DEVICE(0x0A26); /* IntelHaswell_GT3_7 */
+ APPEND_DEVICE(0x0A2A); /* IntelHaswell_GT3_8 */
+ APPEND_DEVICE(0x0A2B); /* IntelHaswell_GT3_9 */
+ APPEND_DEVICE(0x0A2E); /* IntelHaswell_GT3_10 */
+ APPEND_DEVICE(0x0D22); /* IntelHaswell_GT3e_1 */
+ APPEND_DEVICE(0x0D26); /* IntelHaswell_GT3e_2 */
+ APPEND_DEVICE(0x0D2A); /* IntelHaswell_GT3e_3 */
+ APPEND_DEVICE(0x0D2B); /* IntelHaswell_GT3e_4 */
+ APPEND_DEVICE(0x0D2E); /* IntelHaswell_GT3e_5 */
+ break;
+ case DeviceFamily::IntelSandyBridge:
+ APPEND_DEVICE(0x0102);
+ APPEND_DEVICE(0x0106);
+ APPEND_DEVICE(0x010a);
+ APPEND_DEVICE(0x0112);
+ APPEND_DEVICE(0x0116);
+ APPEND_DEVICE(0x0122);
+ APPEND_DEVICE(0x0126);
+ break;
+ case DeviceFamily::IntelGen7Baytrail:
+ APPEND_DEVICE(0x0f30);
+ APPEND_DEVICE(0x0f31);
+ APPEND_DEVICE(0x0f33);
+ APPEND_DEVICE(0x0155);
+ APPEND_DEVICE(0x0157);
+ break;
+ case DeviceFamily::IntelSkylake:
+ APPEND_DEVICE(0x1902);
+ APPEND_DEVICE(0x1906);
+ APPEND_DEVICE(0x190a);
+ APPEND_DEVICE(0x190B);
+ APPEND_DEVICE(0x190e);
+ APPEND_DEVICE(0x1912);
+ APPEND_DEVICE(0x1913);
+ APPEND_DEVICE(0x1915);
+ APPEND_DEVICE(0x1916);
+ APPEND_DEVICE(0x1917);
+ APPEND_DEVICE(0x191a);
+ APPEND_DEVICE(0x191b);
+ APPEND_DEVICE(0x191d);
+ APPEND_DEVICE(0x191e);
+ APPEND_DEVICE(0x1921);
+ APPEND_DEVICE(0x1923);
+ APPEND_DEVICE(0x1926);
+ APPEND_DEVICE(0x1927);
+ APPEND_DEVICE(0x192a);
+ APPEND_DEVICE(0x192b);
+ APPEND_DEVICE(0x192d);
+ APPEND_DEVICE(0x1932);
+ APPEND_DEVICE(0x193a);
+ APPEND_DEVICE(0x193b);
+ APPEND_DEVICE(0x193d);
+ break;
+ case DeviceFamily::IntelKabyLake:
+ APPEND_DEVICE(0x5902);
+ APPEND_DEVICE(0x5906);
+ APPEND_DEVICE(0x5908);
+ APPEND_DEVICE(0x590A);
+ APPEND_DEVICE(0x590B);
+ APPEND_DEVICE(0x590E);
+ APPEND_DEVICE(0x5913);
+ APPEND_DEVICE(0x5915);
+ APPEND_DEVICE(0x5912);
+ APPEND_DEVICE(0x5916);
+ APPEND_DEVICE(0x5917);
+ APPEND_DEVICE(0x591A);
+ APPEND_DEVICE(0x591B);
+ APPEND_DEVICE(0x591D);
+ APPEND_DEVICE(0x591E);
+ APPEND_DEVICE(0x5921);
+ APPEND_DEVICE(0x5923);
+ APPEND_DEVICE(0x5926);
+ APPEND_DEVICE(0x5927);
+ APPEND_DEVICE(0x593B);
+ APPEND_DEVICE(0x591C);
+ APPEND_DEVICE(0x87C0);
+ break;
+ case DeviceFamily::IntelHD520:
+ APPEND_DEVICE(0x1916);
+ break;
+ case DeviceFamily::IntelMobileHDGraphics:
+ APPEND_DEVICE(0x0046); /* IntelMobileHDGraphics */
+ break;
+ case DeviceFamily::NvidiaBlockD3D9Layers:
+ // Glitches whilst scrolling (see bugs 612007, 644787, 645872)
+ APPEND_DEVICE(0x00f3); /* NV43 [GeForce 6200 (TM)] */
+ APPEND_DEVICE(0x0146); /* NV43 [Geforce Go 6600TE/6200TE (TM)] */
+ APPEND_DEVICE(0x014f); /* NV43 [GeForce 6200 (TM)] */
+ APPEND_DEVICE(0x0161); /* NV44 [GeForce 6200 TurboCache (TM)] */
+ APPEND_DEVICE(0x0162); /* NV44 [GeForce 6200SE TurboCache (TM)] */
+ APPEND_DEVICE(0x0163); /* NV44 [GeForce 6200 LE (TM)] */
+ APPEND_DEVICE(0x0164); /* NV44 [GeForce Go 6200 (TM)] */
+ APPEND_DEVICE(0x0167); /* NV43 [GeForce Go 6200/6400 (TM)] */
+ APPEND_DEVICE(0x0168); /* NV43 [GeForce Go 6200/6400 (TM)] */
+ APPEND_DEVICE(0x0169); /* NV44 [GeForce 6250 (TM)] */
+ APPEND_DEVICE(0x0222); /* NV44 [GeForce 6200 A-LE (TM)] */
+ APPEND_DEVICE(0x0240); /* C51PV [GeForce 6150 (TM)] */
+ APPEND_DEVICE(0x0241); /* C51 [GeForce 6150 LE (TM)] */
+ APPEND_DEVICE(0x0244); /* C51 [Geforce Go 6150 (TM)] */
+ APPEND_DEVICE(0x0245); /* C51 [Quadro NVS 210S/GeForce 6150LE (TM)] */
+ APPEND_DEVICE(0x0247); /* C51 [GeForce Go 6100 (TM)] */
+ APPEND_DEVICE(0x03d0); /* C61 [GeForce 6150SE nForce 430 (TM)] */
+ APPEND_DEVICE(0x03d1); /* C61 [GeForce 6100 nForce 405 (TM)] */
+ APPEND_DEVICE(0x03d2); /* C61 [GeForce 6100 nForce 400 (TM)] */
+ APPEND_DEVICE(0x03d5); /* C61 [GeForce 6100 nForce 420 (TM)] */
+ break;
+ case DeviceFamily::RadeonX1000:
+ // This list is from the ATIRadeonX1000.kext Info.plist
+ APPEND_DEVICE(0x7187);
+ APPEND_DEVICE(0x7210);
+ APPEND_DEVICE(0x71de);
+ APPEND_DEVICE(0x7146);
+ APPEND_DEVICE(0x7142);
+ APPEND_DEVICE(0x7109);
+ APPEND_DEVICE(0x71c5);
+ APPEND_DEVICE(0x71c0);
+ APPEND_DEVICE(0x7240);
+ APPEND_DEVICE(0x7249);
+ APPEND_DEVICE(0x7291);
+ break;
+ case DeviceFamily::RadeonCaicos:
+ APPEND_DEVICE(0x6766);
+ APPEND_DEVICE(0x6767);
+ APPEND_DEVICE(0x6768);
+ APPEND_DEVICE(0x6770);
+ APPEND_DEVICE(0x6771);
+ APPEND_DEVICE(0x6772);
+ APPEND_DEVICE(0x6778);
+ APPEND_DEVICE(0x6779);
+ APPEND_DEVICE(0x677b);
+ break;
+ case DeviceFamily::RadeonBlockZeroVideoCopy:
+ // Stoney
+ APPEND_DEVICE(0x98e4);
+ // Carrizo
+ APPEND_RANGE(0x9870, 0x9877);
+ break;
+ case DeviceFamily::Geforce7300GT:
+ APPEND_DEVICE(0x0393);
+ break;
+ case DeviceFamily::Nvidia310M:
+ APPEND_DEVICE(0x0A70);
+ break;
+ case DeviceFamily::Nvidia8800GTS:
+ APPEND_DEVICE(0x0193);
+ break;
+ case DeviceFamily::Bug1137716:
+ APPEND_DEVICE(0x0a29);
+ APPEND_DEVICE(0x0a2b);
+ APPEND_DEVICE(0x0a2d);
+ APPEND_DEVICE(0x0a35);
+ APPEND_DEVICE(0x0a6c);
+ APPEND_DEVICE(0x0a70);
+ APPEND_DEVICE(0x0a72);
+ APPEND_DEVICE(0x0a7a);
+ APPEND_DEVICE(0x0caf);
+ APPEND_DEVICE(0x0dd2);
+ APPEND_DEVICE(0x0dd3);
+ // GF180M ids
+ APPEND_DEVICE(0x0de3);
+ APPEND_DEVICE(0x0de8);
+ APPEND_DEVICE(0x0de9);
+ APPEND_DEVICE(0x0dea);
+ APPEND_DEVICE(0x0deb);
+ APPEND_DEVICE(0x0dec);
+ APPEND_DEVICE(0x0ded);
+ APPEND_DEVICE(0x0dee);
+ APPEND_DEVICE(0x0def);
+ APPEND_DEVICE(0x0df0);
+ APPEND_DEVICE(0x0df1);
+ APPEND_DEVICE(0x0df2);
+ APPEND_DEVICE(0x0df3);
+ APPEND_DEVICE(0x0df4);
+ APPEND_DEVICE(0x0df5);
+ APPEND_DEVICE(0x0df6);
+ APPEND_DEVICE(0x0df7);
+ APPEND_DEVICE(0x1050);
+ APPEND_DEVICE(0x1051);
+ APPEND_DEVICE(0x1052);
+ APPEND_DEVICE(0x1054);
+ APPEND_DEVICE(0x1055);
+ break;
+ case DeviceFamily::Bug1116812:
+ APPEND_DEVICE(0x2e32);
+ APPEND_DEVICE(0x2a02);
+ break;
+ case DeviceFamily::Bug1155608:
+ APPEND_DEVICE(0x2e22); /* IntelG45_1 */
+ break;
+ case DeviceFamily::Bug1447141:
+ APPEND_DEVICE(0x9991);
+ APPEND_DEVICE(0x9993);
+ APPEND_DEVICE(0x9996);
+ APPEND_DEVICE(0x9998);
+ APPEND_DEVICE(0x9901);
+ APPEND_DEVICE(0x990b);
+ break;
+ case DeviceFamily::Bug1207665:
+ APPEND_DEVICE(0xa001); /* Intel Media Accelerator 3150 */
+ APPEND_DEVICE(0xa002);
+ APPEND_DEVICE(0xa011);
+ APPEND_DEVICE(0xa012);
+ break;
+ case DeviceFamily::AmdR600:
+ // AMD R600 generation GPUs
+ // R600
+ APPEND_RANGE(0x9400, 0x9403);
+ APPEND_DEVICE(0x9405);
+ APPEND_RANGE(0x940a, 0x940b);
+ APPEND_DEVICE(0x940f);
+ // RV610
+ APPEND_RANGE(0x94c0, 0x94c1);
+ APPEND_RANGE(0x94c3, 0x94c9);
+ APPEND_RANGE(0x94cb, 0x94cd);
+ // RV630
+ APPEND_RANGE(0x9580, 0x9581);
+ APPEND_DEVICE(0x9583);
+ APPEND_RANGE(0x9586, 0x958f);
+ // RV670
+ APPEND_RANGE(0x9500, 0x9501);
+ APPEND_RANGE(0x9504, 0x9509);
+ APPEND_DEVICE(0x950f);
+ APPEND_DEVICE(0x9511);
+ APPEND_DEVICE(0x9515);
+ APPEND_DEVICE(0x9517);
+ APPEND_DEVICE(0x9519);
+ // RV620
+ APPEND_DEVICE(0x95c0);
+ APPEND_DEVICE(0x95c2);
+ APPEND_RANGE(0x95c4, 0x95c7);
+ APPEND_DEVICE(0x95c9);
+ APPEND_RANGE(0x95cc, 0x95cf);
+ // RV635
+ APPEND_RANGE(0x9590, 0x9591);
+ APPEND_DEVICE(0x9593);
+ APPEND_RANGE(0x9595, 0x9599);
+ APPEND_DEVICE(0x959b);
+ // RS780
+ APPEND_RANGE(0x9610, 0x9616);
+ // RS880
+ APPEND_RANGE(0x9710, 0x9715);
+ break;
+ case DeviceFamily::NvidiaWebRenderBlocked:
+ APPEND_RANGE(0x0190, 0x019e); // early tesla
+ APPEND_RANGE(0x0500, 0x05df); // C67-C68
+ break;
+ case DeviceFamily::IntelWebRenderBlocked:
+ // powervr
+ // sgx535
+ APPEND_DEVICE(0x2e5b);
+ APPEND_DEVICE(0x8108);
+ APPEND_DEVICE(0x8109);
+ APPEND_DEVICE(0x4102);
+ // sgx545
+ APPEND_DEVICE(0x0be0);
+ APPEND_DEVICE(0x0be1);
+ APPEND_DEVICE(0x0be3);
+ APPEND_RANGE(0x08c7, 0x08cf);
+
+ // gen4
+ APPEND_DEVICE(0x2972);
+ APPEND_DEVICE(0x2973);
+ APPEND_DEVICE(0x2992);
+ APPEND_DEVICE(0x2993);
+ APPEND_DEVICE(0x29a2);
+ APPEND_DEVICE(0x29a3);
+
+ APPEND_DEVICE(0x2982);
+ APPEND_DEVICE(0x2983);
+
+ APPEND_DEVICE(0x2a02);
+ APPEND_DEVICE(0x2a03);
+ APPEND_DEVICE(0x2a12);
+ APPEND_DEVICE(0x2a13);
+
+ // gen4.5
+ APPEND_DEVICE(0x2e02);
+ APPEND_DEVICE(0x2e42); /* IntelB43_1 */
+ APPEND_DEVICE(0x2e43); /* IntelB43_2 */
+ APPEND_DEVICE(0x2e92); /* IntelB43_3 */
+ APPEND_DEVICE(0x2e93); /* IntelB43_4 */
+ APPEND_DEVICE(0x2e12); /* IntelQ45_1 */
+ APPEND_DEVICE(0x2e13); /* IntelQ45_2 */
+ APPEND_DEVICE(0x2e32); /* IntelG41_1 */
+ APPEND_DEVICE(0x2e33); /* IntelG41_2 */
+ APPEND_DEVICE(0x2e22); /* IntelG45_1 */
+
+ APPEND_DEVICE(0x2e23); /* IntelG45_2 */
+ APPEND_DEVICE(0x2a42); /* IntelGMA4500MHD_1 */
+ APPEND_DEVICE(0x2a43); /* IntelGMA4500MHD_2 */
+
+ // gen5 (ironlake)
+ APPEND_DEVICE(0x0042);
+ APPEND_DEVICE(0x0046);
+ break;
+ // This should never happen, but we get a warning if we don't handle this.
+ case DeviceFamily::Max:
+ case DeviceFamily::All:
+ case DeviceFamily::IntelAll:
+ case DeviceFamily::NvidiaAll:
+ case DeviceFamily::AtiAll:
+ case DeviceFamily::MicrosoftAll:
+ case DeviceFamily::ParallelsAll:
+ case DeviceFamily::QualcommAll:
+ case DeviceFamily::AppleAll:
+ case DeviceFamily::AmazonAll:
+ NS_WARNING("Invalid DeviceFamily id");
+ break;
+ }
+
+ return deviceFamily;
+}
+
+// Macro for assigning a window protocol id to a string.
+#define DECLARE_WINDOW_PROTOCOL_ID(name, windowProtocolId) \
+ case WindowProtocol::name: \
+ sWindowProtocol[idx]->AssignLiteral(windowProtocolId); \
+ break;
+
+const nsAString& GfxDriverInfo::GetWindowProtocol(WindowProtocol id) {
+ if (id >= WindowProtocol::Max) {
+ MOZ_ASSERT_UNREACHABLE("WindowProtocol id is out of range");
+ id = WindowProtocol::All;
+ }
+
+ auto idx = static_cast<size_t>(id);
+ if (sWindowProtocol[idx]) {
+ return *sWindowProtocol[idx];
+ }
+
+ sWindowProtocol[idx] = new nsString();
+
+ switch (id) {
+ DECLARE_WINDOW_PROTOCOL_ID(X11, "x11");
+ DECLARE_WINDOW_PROTOCOL_ID(XWayland, "xwayland");
+ DECLARE_WINDOW_PROTOCOL_ID(Wayland, "wayland");
+ DECLARE_WINDOW_PROTOCOL_ID(WaylandDRM, "wayland/drm");
+ DECLARE_WINDOW_PROTOCOL_ID(WaylandAll, "wayland/all");
+ DECLARE_WINDOW_PROTOCOL_ID(X11All, "x11/all");
+ case WindowProtocol::Max: // Suppress a warning.
+ DECLARE_WINDOW_PROTOCOL_ID(All, "");
+ }
+
+ return *sWindowProtocol[idx];
+}
+
+// Macro for assigning a device vendor id to a string.
+#define DECLARE_VENDOR_ID(name, deviceId) \
+ case DeviceVendor::name: \
+ sDeviceVendors[idx]->AssignLiteral(deviceId); \
+ break;
+
+const nsAString& GfxDriverInfo::GetDeviceVendor(DeviceFamily id) {
+ if (id >= DeviceFamily::Max) {
+ MOZ_ASSERT_UNREACHABLE("DeviceVendor id is out of range");
+ id = DeviceFamily::All;
+ }
+
+ DeviceVendor vendor = DeviceVendor::All;
+ switch (id) {
+ case DeviceFamily::IntelAll:
+ case DeviceFamily::IntelGMA500:
+ case DeviceFamily::IntelGMA900:
+ case DeviceFamily::IntelGMA950:
+ case DeviceFamily::IntelGMA3150:
+ case DeviceFamily::IntelGMAX3000:
+ case DeviceFamily::IntelGMAX4500HD:
+ case DeviceFamily::IntelHDGraphicsToIvyBridge:
+ case DeviceFamily::IntelHDGraphicsToSandyBridge:
+ case DeviceFamily::IntelHaswell:
+ case DeviceFamily::IntelSandyBridge:
+ case DeviceFamily::IntelGen7Baytrail:
+ case DeviceFamily::IntelSkylake:
+ case DeviceFamily::IntelKabyLake:
+ case DeviceFamily::IntelHD520:
+ case DeviceFamily::IntelMobileHDGraphics:
+ case DeviceFamily::IntelWebRenderBlocked:
+ case DeviceFamily::Bug1116812:
+ case DeviceFamily::Bug1155608:
+ case DeviceFamily::Bug1207665:
+ vendor = DeviceVendor::Intel;
+ break;
+ case DeviceFamily::NvidiaAll:
+ case DeviceFamily::NvidiaBlockD3D9Layers:
+ case DeviceFamily::NvidiaWebRenderBlocked:
+ case DeviceFamily::Geforce7300GT:
+ case DeviceFamily::Nvidia310M:
+ case DeviceFamily::Nvidia8800GTS:
+ case DeviceFamily::Bug1137716:
+ vendor = DeviceVendor::NVIDIA;
+ break;
+ case DeviceFamily::AtiAll:
+ case DeviceFamily::RadeonBlockZeroVideoCopy:
+ case DeviceFamily::RadeonCaicos:
+ case DeviceFamily::RadeonX1000:
+ case DeviceFamily::Bug1447141:
+ case DeviceFamily::AmdR600:
+ vendor = DeviceVendor::ATI;
+ break;
+ case DeviceFamily::MicrosoftAll:
+ vendor = DeviceVendor::Microsoft;
+ break;
+ case DeviceFamily::ParallelsAll:
+ vendor = DeviceVendor::Parallels;
+ break;
+ case DeviceFamily::AppleAll:
+ vendor = DeviceVendor::Apple;
+ break;
+ case DeviceFamily::AmazonAll:
+ vendor = DeviceVendor::Amazon;
+ break;
+ case DeviceFamily::QualcommAll:
+ // Choose an arbitrary Qualcomm PCI VENdor ID for now.
+ // TODO: This should be "QCOM" when Windows device ID parsing is reworked.
+ vendor = DeviceVendor::Qualcomm;
+ break;
+ case DeviceFamily::All:
+ case DeviceFamily::Max:
+ break;
+ }
+
+ return GetDeviceVendor(vendor);
+}
+
+const nsAString& GfxDriverInfo::GetDeviceVendor(DeviceVendor id) {
+ if (id >= DeviceVendor::Max) {
+ MOZ_ASSERT_UNREACHABLE("DeviceVendor id is out of range");
+ id = DeviceVendor::All;
+ }
+
+ auto idx = static_cast<size_t>(id);
+ if (sDeviceVendors[idx]) {
+ return *sDeviceVendors[idx];
+ }
+
+ sDeviceVendors[idx] = new nsString();
+
+ switch (id) {
+ DECLARE_VENDOR_ID(Intel, "0x8086");
+ DECLARE_VENDOR_ID(NVIDIA, "0x10de");
+ DECLARE_VENDOR_ID(ATI, "0x1002");
+ // AMD has 0x1022 but continues to release GPU hardware under ATI.
+ DECLARE_VENDOR_ID(Microsoft, "0x1414");
+ DECLARE_VENDOR_ID(MicrosoftBasic, "0x00ba");
+ DECLARE_VENDOR_ID(MicrosoftHyperV, "0x000b");
+ DECLARE_VENDOR_ID(Parallels, "0x1ab8");
+ DECLARE_VENDOR_ID(VMWare, "0x15ad");
+ DECLARE_VENDOR_ID(VirtualBox, "0x80ee");
+ DECLARE_VENDOR_ID(Apple, "0x106b");
+ DECLARE_VENDOR_ID(Amazon, "0x1d0f");
+ // Choose an arbitrary Qualcomm PCI VENdor ID for now.
+ // TODO: This should be "QCOM" when Windows device ID parsing is reworked.
+ DECLARE_VENDOR_ID(Qualcomm, "0x5143");
+ case DeviceVendor::Max: // Suppress a warning.
+ DECLARE_VENDOR_ID(All, "");
+ }
+
+ return *sDeviceVendors[idx];
+}
+
+// Macro for assigning a driver vendor id to a string.
+#define DECLARE_DRIVER_VENDOR_ID(name, driverVendorId) \
+ case DriverVendor::name: \
+ sDriverVendors[idx]->AssignLiteral(driverVendorId); \
+ break;
+
+const nsAString& GfxDriverInfo::GetDriverVendor(DriverVendor id) {
+ if (id >= DriverVendor::Max) {
+ MOZ_ASSERT_UNREACHABLE("DriverVendor id is out of range");
+ id = DriverVendor::All;
+ }
+
+ auto idx = static_cast<size_t>(id);
+ if (sDriverVendors[idx]) {
+ return *sDriverVendors[idx];
+ }
+
+ sDriverVendors[idx] = new nsString();
+
+ switch (id) {
+ DECLARE_DRIVER_VENDOR_ID(MesaAll, "mesa/all");
+ DECLARE_DRIVER_VENDOR_ID(MesaLLVMPipe, "mesa/llvmpipe");
+ DECLARE_DRIVER_VENDOR_ID(MesaSoftPipe, "mesa/softpipe");
+ DECLARE_DRIVER_VENDOR_ID(MesaSWRast, "mesa/swrast");
+ DECLARE_DRIVER_VENDOR_ID(MesaSWUnknown, "mesa/software-unknown");
+ DECLARE_DRIVER_VENDOR_ID(MesaUnknown, "mesa/unknown");
+ DECLARE_DRIVER_VENDOR_ID(MesaR600, "mesa/r600");
+ DECLARE_DRIVER_VENDOR_ID(MesaNouveau, "mesa/nouveau");
+ DECLARE_DRIVER_VENDOR_ID(NonMesaAll, "non-mesa/all");
+ DECLARE_DRIVER_VENDOR_ID(HardwareMesaAll, "mesa/hw-all");
+ DECLARE_DRIVER_VENDOR_ID(SoftwareMesaAll, "mesa/sw-all");
+ DECLARE_DRIVER_VENDOR_ID(MesaNonIntelNvidiaAtiAll,
+ "mesa/non-intel-nvidia-ati-all");
+ DECLARE_DRIVER_VENDOR_ID(MesaVM, "mesa/vmwgfx");
+ case DriverVendor::Max: // Suppress a warning.
+ DECLARE_DRIVER_VENDOR_ID(All, "");
+ }
+
+ return *sDriverVendors[idx];
+}
diff --git a/widget/GfxDriverInfo.h b/widget/GfxDriverInfo.h
new file mode 100644
index 0000000000..a584c7ac3f
--- /dev/null
+++ b/widget/GfxDriverInfo.h
@@ -0,0 +1,501 @@
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_GfxDriverInfo_h__
+#define __mozilla_widget_GfxDriverInfo_h__
+
+#include "nsString.h"
+#include "nsTArray.h"
+
+// Macros for adding a blocklist item to the static list. _EXT variants
+// allow one to specify all available parameters, including those available
+// only on specific platforms (e.g. desktop environment and driver vendor
+// for Linux.)
+
+#define APPEND_TO_DRIVER_BLOCKLIST_EXT( \
+ os, screen, battery, windowProtocol, driverVendor, devices, feature, \
+ featureStatus, driverComparator, driverVersion, ruleId, suggestedVersion) \
+ sDriverInfo->AppendElement(GfxDriverInfo( \
+ os, screen, battery, \
+ (nsAString&)GfxDriverInfo::GetWindowProtocol(windowProtocol), \
+ (nsAString&)GfxDriverInfo::GetDeviceVendor(devices), \
+ (nsAString&)GfxDriverInfo::GetDriverVendor(driverVendor), \
+ (GfxDeviceFamily*)GfxDriverInfo::GetDeviceFamily(devices), feature, \
+ featureStatus, driverComparator, driverVersion, ruleId, \
+ suggestedVersion))
+
+#define APPEND_TO_DRIVER_BLOCKLIST(os, devices, feature, featureStatus, \
+ driverComparator, driverVersion, ruleId, \
+ suggestedVersion) \
+ APPEND_TO_DRIVER_BLOCKLIST_EXT( \
+ os, ScreenSizeStatus::All, BatteryStatus::All, WindowProtocol::All, \
+ DriverVendor::All, devices, feature, featureStatus, driverComparator, \
+ driverVersion, ruleId, suggestedVersion)
+
+#define APPEND_TO_DRIVER_BLOCKLIST2_EXT( \
+ os, screen, battery, windowProtocol, driverVendor, devices, feature, \
+ featureStatus, driverComparator, driverVersion, ruleId) \
+ sDriverInfo->AppendElement(GfxDriverInfo( \
+ os, screen, battery, \
+ (nsAString&)GfxDriverInfo::GetWindowProtocol(windowProtocol), \
+ (nsAString&)GfxDriverInfo::GetDeviceVendor(devices), \
+ (nsAString&)GfxDriverInfo::GetDriverVendor(driverVendor), \
+ (GfxDeviceFamily*)GfxDriverInfo::GetDeviceFamily(devices), feature, \
+ featureStatus, driverComparator, driverVersion, ruleId))
+
+#define APPEND_TO_DRIVER_BLOCKLIST2(os, devices, feature, featureStatus, \
+ driverComparator, driverVersion, ruleId) \
+ APPEND_TO_DRIVER_BLOCKLIST2_EXT( \
+ os, ScreenSizeStatus::All, BatteryStatus::All, WindowProtocol::All, \
+ DriverVendor::All, devices, feature, featureStatus, driverComparator, \
+ driverVersion, ruleId)
+
+#define APPEND_TO_DRIVER_BLOCKLIST_RANGE_EXT( \
+ os, screen, battery, windowProtocol, driverVendor, devices, feature, \
+ featureStatus, driverComparator, driverVersion, driverVersionMax, ruleId, \
+ suggestedVersion) \
+ do { \
+ MOZ_ASSERT((driverComparator) == DRIVER_BETWEEN_EXCLUSIVE || \
+ (driverComparator) == DRIVER_BETWEEN_INCLUSIVE || \
+ (driverComparator) == DRIVER_BETWEEN_INCLUSIVE_START); \
+ GfxDriverInfo info( \
+ os, screen, battery, \
+ (nsAString&)GfxDriverInfo::GetWindowProtocol(windowProtocol), \
+ (nsAString&)GfxDriverInfo::GetDeviceVendor(devices), \
+ (nsAString&)GfxDriverInfo::GetDriverVendor(driverVendor), \
+ (GfxDeviceFamily*)GfxDriverInfo::GetDeviceFamily(devices), feature, \
+ featureStatus, driverComparator, driverVersion, ruleId, \
+ suggestedVersion); \
+ info.mDriverVersionMax = driverVersionMax; \
+ sDriverInfo->AppendElement(info); \
+ } while (false)
+
+#define APPEND_TO_DRIVER_BLOCKLIST_RANGE( \
+ os, devices, feature, featureStatus, driverComparator, driverVersion, \
+ driverVersionMax, ruleId, suggestedVersion) \
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE_EXT( \
+ os, ScreenSizeStatus::All, BatteryStatus::All, WindowProtocol::All, \
+ DriverVendor::All, devices, feature, featureStatus, driverComparator, \
+ driverVersion, driverVersionMax, ruleId, suggestedVersion)
+
+#define APPEND_TO_DRIVER_BLOCKLIST_RANGE_GPU2_EXT( \
+ os, screen, battery, windowProtocol, driverVendor, devices, feature, \
+ featureStatus, driverComparator, driverVersion, driverVersionMax, ruleId, \
+ suggestedVersion) \
+ do { \
+ MOZ_ASSERT((driverComparator) == DRIVER_BETWEEN_EXCLUSIVE || \
+ (driverComparator) == DRIVER_BETWEEN_INCLUSIVE || \
+ (driverComparator) == DRIVER_BETWEEN_INCLUSIVE_START); \
+ GfxDriverInfo info( \
+ os, screen, battery, \
+ (nsAString&)GfxDriverInfo::GetWindowProtocol(windowProtocol), \
+ (nsAString&)GfxDriverInfo::GetDeviceVendor(devices), \
+ (nsAString&)GfxDriverInfo::GetDriverVendor(driverVendor), \
+ (GfxDeviceFamily*)GfxDriverInfo::GetDeviceFamily(devices), feature, \
+ featureStatus, driverComparator, driverVersion, ruleId, \
+ suggestedVersion, false, true); \
+ info.mDriverVersionMax = driverVersionMax; \
+ sDriverInfo->AppendElement(info); \
+ } while (false)
+
+#define APPEND_TO_DRIVER_BLOCKLIST_RANGE_GPU2( \
+ os, devices, feature, featureStatus, driverComparator, driverVersion, \
+ driverVersionMax, ruleId, suggestedVersion) \
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE_GPU2_EXT( \
+ os, ScreenSizeStatus::All, BatteryStatus::All, WindowProtocol::All, \
+ DriverVendor::All, devices, feature, featureStatus, driverComparator, \
+ driverVersion, driverVersionMax, ruleId, suggestedVersion)
+
+namespace mozilla {
+namespace widget {
+
+enum class OperatingSystem : uint8_t {
+ Unknown,
+ Windows,
+ WindowsXP,
+ WindowsServer2003,
+ WindowsVista,
+ Windows7,
+ Windows8,
+ Windows8_1,
+ Windows10,
+ RecentWindows10,
+ NotRecentWindows10,
+ Linux,
+ OSX,
+ OSX10_5,
+ OSX10_6,
+ OSX10_7,
+ OSX10_8,
+ OSX10_9,
+ OSX10_10,
+ OSX10_11,
+ OSX10_12,
+ OSX10_13,
+ OSX10_14,
+ OSX10_15,
+ OSX11_0,
+ Android,
+ Ios
+};
+
+enum VersionComparisonOp {
+ DRIVER_LESS_THAN, // driver < version
+ DRIVER_BUILD_ID_LESS_THAN, // driver build id < version
+ DRIVER_LESS_THAN_OR_EQUAL, // driver <= version
+ DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, // driver build id <= version
+ DRIVER_GREATER_THAN, // driver > version
+ DRIVER_GREATER_THAN_OR_EQUAL, // driver >= version
+ DRIVER_EQUAL, // driver == version
+ DRIVER_NOT_EQUAL, // driver != version
+ DRIVER_BETWEEN_EXCLUSIVE, // driver > version && driver < versionMax
+ DRIVER_BETWEEN_INCLUSIVE, // driver >= version && driver <= versionMax
+ DRIVER_BETWEEN_INCLUSIVE_START, // driver >= version && driver < versionMax
+ DRIVER_COMPARISON_IGNORED
+};
+
+enum class DeviceFamily : uint8_t {
+ All,
+ IntelAll,
+ NvidiaAll,
+ AtiAll,
+ MicrosoftAll,
+ ParallelsAll,
+ QualcommAll,
+ AppleAll,
+ AmazonAll,
+ IntelGMA500,
+ IntelGMA900,
+ IntelGMA950,
+ IntelGMA3150,
+ IntelGMAX3000,
+ IntelGMAX4500HD,
+ IntelHDGraphicsToIvyBridge,
+ IntelHDGraphicsToSandyBridge,
+ IntelHaswell,
+ IntelSandyBridge,
+ IntelGen7Baytrail,
+ IntelSkylake,
+ IntelKabyLake,
+ IntelHD520,
+ IntelMobileHDGraphics,
+ NvidiaBlockD3D9Layers,
+ RadeonX1000,
+ RadeonCaicos,
+ RadeonBlockZeroVideoCopy,
+ Geforce7300GT,
+ Nvidia310M,
+ Nvidia8800GTS,
+ Bug1137716,
+ Bug1116812,
+ Bug1155608,
+ Bug1207665,
+ Bug1447141,
+ AmdR600,
+ IntelWebRenderBlocked,
+ NvidiaWebRenderBlocked,
+
+ Max
+};
+
+enum class DeviceVendor : uint8_t {
+ All, // There is an assumption that this is the first enum
+ Intel,
+ NVIDIA,
+ ATI,
+ Microsoft,
+ Parallels,
+ VMWare,
+ VirtualBox,
+ Qualcomm,
+ MicrosoftBasic,
+ MicrosoftHyperV,
+ Apple,
+ Amazon,
+
+ Max
+};
+
+enum DriverVendor : uint8_t {
+ All, // There is an assumption that this is the first enum
+ // Wildcard for all Mesa drivers.
+ MesaAll,
+ // Note that the following list of Mesa drivers is not comprehensive; we pull
+ // the DRI driver at runtime. These drivers are provided for convenience when
+ // populating the local blocklist.
+ MesaLLVMPipe,
+ MesaSoftPipe,
+ MesaSWRast,
+ MesaSWUnknown,
+ // AMD
+ MesaR600,
+ // Nouveau: Open-source nvidia
+ MesaNouveau,
+ // A generic ID to be provided when we can't determine the DRI driver on Mesa.
+ MesaUnknown,
+ // Wildcard for all non-Mesa drivers.
+ NonMesaAll,
+ // Wildcard for all hardware Mesa drivers.
+ HardwareMesaAll,
+ // Wildcard for all software Mesa drivers.
+ SoftwareMesaAll,
+ // Wildcard for all non-Intel/NVIDIA/ATI Mesa drivers.
+ MesaNonIntelNvidiaAtiAll,
+ // Running in VM.
+ MesaVM,
+
+ Max
+};
+
+enum class WindowProtocol : uint8_t {
+ All, // There is an assumption that this is the first enum
+ X11,
+ XWayland,
+ Wayland,
+ WaylandDRM,
+ // Wildcard for all Wayland variants, excluding XWayland.
+ WaylandAll,
+ // Wildcard for all X11 variants, including XWayland.
+ X11All,
+ Max
+};
+
+enum class BatteryStatus : uint8_t { All, Present, None };
+
+enum class ScreenSizeStatus : uint8_t {
+ All,
+ Small, // <= 1900x1200
+ SmallAndMedium, // <= 3440x1440
+ Medium, // <= 3440x1440 && > 1900x1200
+ MediumAndLarge, // >1900x1200
+ Large // > 3440x1440
+};
+
+/* Array of devices to match, or an empty array for all devices */
+class GfxDeviceFamily final {
+ public:
+ GfxDeviceFamily() = default;
+
+ void Append(const nsAString& aDeviceId);
+ void AppendRange(int32_t aBeginDeviceId, int32_t aEndDeviceId);
+
+ bool IsEmpty() const { return mIds.IsEmpty() && mRanges.IsEmpty(); }
+
+ nsresult Contains(nsAString& aDeviceId) const;
+
+ private:
+ struct DeviceRange {
+ int32_t mBegin;
+ int32_t mEnd;
+ };
+
+ CopyableTArray<nsString> mIds;
+ CopyableTArray<DeviceRange> mRanges;
+};
+
+struct GfxDriverInfo {
+ // If |ownDevices| is true, you are transferring ownership of the devices
+ // array, and it will be deleted when this GfxDriverInfo is destroyed.
+ GfxDriverInfo(OperatingSystem os, ScreenSizeStatus aScreen,
+ BatteryStatus aBattery, const nsAString& windowProtocol,
+ const nsAString& vendor, const nsAString& driverVendor,
+ GfxDeviceFamily* devices, int32_t feature,
+ int32_t featureStatus, VersionComparisonOp op,
+ uint64_t driverVersion, const char* ruleId,
+ const char* suggestedVersion = nullptr, bool ownDevices = false,
+ bool gpu2 = false);
+
+ GfxDriverInfo();
+ GfxDriverInfo(const GfxDriverInfo&);
+ ~GfxDriverInfo();
+
+ OperatingSystem mOperatingSystem;
+ uint32_t mOperatingSystemVersion;
+ ScreenSizeStatus mScreen;
+ BatteryStatus mBattery;
+ nsString mWindowProtocol;
+
+ nsString mAdapterVendor;
+ nsString mDriverVendor;
+
+ const GfxDeviceFamily* mDevices;
+
+ // Whether the mDevices array should be deleted when this structure is
+ // deallocated. False by default.
+ bool mDeleteDevices;
+
+ /* A feature from nsIGfxInfo, or a wildcard set of features */
+ int32_t mFeature;
+ /* Block all features */
+ static constexpr int32_t allFeatures = -1;
+ /* Block all features not permitted by OnlyAllowFeatureOnKnownConfig */
+ static constexpr int32_t optionalFeatures = -2;
+
+ /* A feature status from nsIGfxInfo */
+ int32_t mFeatureStatus;
+
+ VersionComparisonOp mComparisonOp;
+
+ /* versions are assumed to be A.B.C.D packed as 0xAAAABBBBCCCCDDDD */
+ uint64_t mDriverVersion;
+ uint64_t mDriverVersionMax;
+ static constexpr uint64_t allDriverVersions = ~(uint64_t(0));
+
+ const char* mSuggestedVersion;
+ nsCString mRuleId;
+
+ static const GfxDeviceFamily* GetDeviceFamily(DeviceFamily id);
+ static GfxDeviceFamily*
+ sDeviceFamilies[static_cast<size_t>(DeviceFamily::Max)];
+
+ static const nsAString& GetWindowProtocol(WindowProtocol id);
+ static nsString* sWindowProtocol[static_cast<size_t>(WindowProtocol::Max)];
+
+ static const nsAString& GetDeviceVendor(DeviceVendor id);
+ static const nsAString& GetDeviceVendor(DeviceFamily id);
+ static nsString* sDeviceVendors[static_cast<size_t>(DeviceVendor::Max)];
+
+ static const nsAString& GetDriverVendor(DriverVendor id);
+ static nsString* sDriverVendors[static_cast<size_t>(DriverVendor::Max)];
+
+ nsString mModel, mHardware, mProduct, mManufacturer;
+
+ bool mGpu2;
+};
+
+inline uint64_t DriverVersion(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
+ return (uint64_t(a) << 48) | (uint64_t(b) << 32) | (uint64_t(c) << 16) |
+ uint64_t(d);
+}
+
+inline uint64_t V(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
+#ifdef XP_WIN
+ // We make sure every driver number is padded by 0s, this will allow us the
+ // easiest 'compare as if decimals' approach. See ParseDriverVersion for a
+ // more extensive explanation of this approach.
+ while (b > 0 && b < 1000) {
+ b *= 10;
+ }
+ while (c > 0 && c < 1000) {
+ c *= 10;
+ }
+ while (d > 0 && d < 1000) {
+ d *= 10;
+ }
+#endif
+ return DriverVersion(a, b, c, d);
+}
+
+// All destination string storage needs to have at least 5 bytes available.
+inline bool SplitDriverVersion(const char* aSource, char* aAStr, char* aBStr,
+ char* aCStr, char* aDStr) {
+ // sscanf doesn't do what we want here to we parse this manually.
+ int len = strlen(aSource);
+
+ // This "4" is hardcoded in a few places, including once as a 3.
+ char* dest[4] = {aAStr, aBStr, aCStr, aDStr};
+ unsigned destIdx = 0;
+ unsigned destPos = 0;
+
+ for (int i = 0; i < len; i++) {
+ if (destIdx >= 4) {
+ // Invalid format found. Ensure we don't access dest beyond bounds.
+ return false;
+ }
+
+ if (aSource[i] == '.') {
+ MOZ_ASSERT(destIdx < 4 && destPos <= 4);
+ dest[destIdx++][destPos] = 0;
+ destPos = 0;
+ continue;
+ }
+
+ if (destPos > 3) {
+ // Ignore more than 4 chars. Ensure we never access dest[destIdx]
+ // beyond its bounds.
+ continue;
+ }
+
+ MOZ_ASSERT(destIdx < 4 && destPos < 4);
+ dest[destIdx][destPos++] = aSource[i];
+ }
+
+ // Take care of the trailing period
+ if (destIdx >= 4) {
+ return false;
+ }
+
+ // Add last terminator.
+ MOZ_ASSERT(destIdx < 4 && destPos <= 4);
+ dest[destIdx][destPos] = 0;
+ for (int unusedDestIdx = destIdx + 1; unusedDestIdx < 4; unusedDestIdx++) {
+ dest[unusedDestIdx][0] = 0;
+ }
+
+ if (destIdx != 3) {
+ return false;
+ }
+ return true;
+}
+
+// This allows us to pad driver version 'substrings' with 0s, this
+// effectively allows us to treat the version numbers as 'decimals'. This is
+// a little strange but this method seems to do the right thing for all
+// different vendor's driver strings. i.e. .98 will become 9800, which is
+// larger than .978 which would become 9780.
+inline void PadDriverDecimal(char* aString) {
+ for (int i = 0; i < 4; i++) {
+ if (!aString[i]) {
+ for (int c = i; c < 4; c++) {
+ aString[c] = '0';
+ }
+ break;
+ }
+ }
+ aString[4] = 0;
+}
+
+inline bool ParseDriverVersion(const nsAString& aVersion,
+ uint64_t* aNumericVersion) {
+ *aNumericVersion = 0;
+
+#ifndef ANDROID
+ int a, b, c, d;
+ char aStr[8], bStr[8], cStr[8], dStr[8];
+ /* honestly, why do I even bother */
+ if (!SplitDriverVersion(NS_LossyConvertUTF16toASCII(aVersion).get(), aStr,
+ bStr, cStr, dStr))
+ return false;
+
+# ifdef XP_WIN
+ PadDriverDecimal(bStr);
+ PadDriverDecimal(cStr);
+ PadDriverDecimal(dStr);
+# endif
+
+ a = atoi(aStr);
+ b = atoi(bStr);
+ c = atoi(cStr);
+ d = atoi(dStr);
+
+ if (a < 0 || a > 0xffff) return false;
+ if (b < 0 || b > 0xffff) return false;
+ if (c < 0 || c > 0xffff) return false;
+ if (d < 0 || d > 0xffff) return false;
+
+ *aNumericVersion = DriverVersion(a, b, c, d);
+#else
+ // Can't use aVersion.ToInteger() because that's not compiled into our code
+ // unless we have XPCOM_GLUE_AVOID_NSPR disabled.
+ *aNumericVersion = atoi(NS_LossyConvertUTF16toASCII(aVersion).get());
+#endif
+ MOZ_ASSERT(*aNumericVersion != GfxDriverInfo::allDriverVersions);
+ return true;
+}
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /*__mozilla_widget_GfxDriverInfo_h__ */
diff --git a/widget/GfxInfoBase.cpp b/widget/GfxInfoBase.cpp
new file mode 100644
index 0000000000..3e32ffbe15
--- /dev/null
+++ b/widget/GfxInfoBase.cpp
@@ -0,0 +1,2219 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "mozilla/ArrayUtils.h"
+
+#include "GfxInfoBase.h"
+
+#include <mutex> // std::call_once
+
+#include "GfxDriverInfo.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsVersionComparator.h"
+#include "mozilla/Services.h"
+#include "mozilla/Observer.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsTArray.h"
+#include "nsXULAppAPI.h"
+#include "nsIXULAppInfo.h"
+#include "mozilla/BinarySearch.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/BuildConstants.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "mozilla/widget/Screen.h"
+
+#include "jsapi.h"
+
+#include "gfxPlatform.h"
+#include "gfxConfig.h"
+#include "DriverCrashGuard.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include <set>
+# include "AndroidBuild.h"
+#endif
+
+using namespace mozilla::widget;
+using namespace mozilla;
+using mozilla::MutexAutoLock;
+
+nsTArray<GfxDriverInfo>* GfxInfoBase::sDriverInfo;
+StaticAutoPtr<nsTArray<gfx::GfxInfoFeatureStatus>> GfxInfoBase::sFeatureStatus;
+bool GfxInfoBase::sDriverInfoObserverInitialized;
+bool GfxInfoBase::sShutdownOccurred;
+
+// Call this when setting sFeatureStatus to a non-null pointer to
+// ensure destruction even if the GfxInfo component is never instantiated.
+static void InitFeatureStatus(nsTArray<gfx::GfxInfoFeatureStatus>* aPtr) {
+ static std::once_flag sOnce;
+ std::call_once(sOnce, [] { ClearOnShutdown(&GfxInfoBase::sFeatureStatus); });
+ GfxInfoBase::sFeatureStatus = aPtr;
+}
+
+// Observes for shutdown so that the child GfxDriverInfo list is freed.
+class ShutdownObserver : public nsIObserver {
+ virtual ~ShutdownObserver() = default;
+
+ public:
+ ShutdownObserver() = default;
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Observe(nsISupports* subject, const char* aTopic,
+ const char16_t* aData) override {
+ MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0);
+
+ delete GfxInfoBase::sDriverInfo;
+ GfxInfoBase::sDriverInfo = nullptr;
+
+ for (auto& deviceFamily : GfxDriverInfo::sDeviceFamilies) {
+ delete deviceFamily;
+ deviceFamily = nullptr;
+ }
+
+ for (auto& windowProtocol : GfxDriverInfo::sWindowProtocol) {
+ delete windowProtocol;
+ windowProtocol = nullptr;
+ }
+
+ for (auto& deviceVendor : GfxDriverInfo::sDeviceVendors) {
+ delete deviceVendor;
+ deviceVendor = nullptr;
+ }
+
+ for (auto& driverVendor : GfxDriverInfo::sDriverVendors) {
+ delete driverVendor;
+ driverVendor = nullptr;
+ }
+
+ GfxInfoBase::sShutdownOccurred = true;
+
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(ShutdownObserver, nsIObserver)
+
+static void InitGfxDriverInfoShutdownObserver() {
+ if (GfxInfoBase::sDriverInfoObserverInitialized) return;
+
+ GfxInfoBase::sDriverInfoObserverInitialized = true;
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (!observerService) {
+ NS_WARNING("Could not get observer service!");
+ return;
+ }
+
+ ShutdownObserver* obs = new ShutdownObserver();
+ observerService->AddObserver(obs, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+}
+
+using namespace mozilla::widget;
+using namespace mozilla::gfx;
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(GfxInfoBase, nsIGfxInfo, nsIObserver,
+ nsISupportsWeakReference)
+
+#define BLOCKLIST_PREF_BRANCH "gfx.blacklist."
+#define SUGGESTED_VERSION_PREF BLOCKLIST_PREF_BRANCH "suggested-driver-version"
+
+static const char* GetPrefNameForFeature(int32_t aFeature) {
+ const char* name = nullptr;
+ switch (aFeature) {
+ case nsIGfxInfo::FEATURE_DIRECT2D:
+ name = BLOCKLIST_PREF_BRANCH "direct2d";
+ break;
+ case nsIGfxInfo::FEATURE_DIRECT3D_9_LAYERS:
+ name = BLOCKLIST_PREF_BRANCH "layers.direct3d9";
+ break;
+ case nsIGfxInfo::FEATURE_DIRECT3D_10_LAYERS:
+ name = BLOCKLIST_PREF_BRANCH "layers.direct3d10";
+ break;
+ case nsIGfxInfo::FEATURE_DIRECT3D_10_1_LAYERS:
+ name = BLOCKLIST_PREF_BRANCH "layers.direct3d10-1";
+ break;
+ case nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS:
+ name = BLOCKLIST_PREF_BRANCH "layers.direct3d11";
+ break;
+ case nsIGfxInfo::FEATURE_DIRECT3D_11_ANGLE:
+ name = BLOCKLIST_PREF_BRANCH "direct3d11angle";
+ break;
+ case nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING:
+ name = BLOCKLIST_PREF_BRANCH "hardwarevideodecoding";
+ break;
+ case nsIGfxInfo::FEATURE_OPENGL_LAYERS:
+ name = BLOCKLIST_PREF_BRANCH "layers.opengl";
+ break;
+ case nsIGfxInfo::FEATURE_WEBGL_OPENGL:
+ name = BLOCKLIST_PREF_BRANCH "webgl.opengl";
+ break;
+ case nsIGfxInfo::FEATURE_WEBGL_ANGLE:
+ name = BLOCKLIST_PREF_BRANCH "webgl.angle";
+ break;
+ case nsIGfxInfo::UNUSED_FEATURE_WEBGL_MSAA:
+ name = BLOCKLIST_PREF_BRANCH "webgl.msaa";
+ break;
+ case nsIGfxInfo::FEATURE_STAGEFRIGHT:
+ name = BLOCKLIST_PREF_BRANCH "stagefright";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_H264:
+ name = BLOCKLIST_PREF_BRANCH "webrtc.hw.acceleration.h264";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE:
+ name = BLOCKLIST_PREF_BRANCH "webrtc.hw.acceleration.encode";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE:
+ name = BLOCKLIST_PREF_BRANCH "webrtc.hw.acceleration.decode";
+ break;
+ case nsIGfxInfo::FEATURE_CANVAS2D_ACCELERATION:
+ name = BLOCKLIST_PREF_BRANCH "canvas2d.acceleration";
+ break;
+ case nsIGfxInfo::FEATURE_DX_INTEROP2:
+ name = BLOCKLIST_PREF_BRANCH "dx.interop2";
+ break;
+ case nsIGfxInfo::FEATURE_GPU_PROCESS:
+ name = BLOCKLIST_PREF_BRANCH "gpu.process";
+ break;
+ case nsIGfxInfo::FEATURE_WEBGL2:
+ name = BLOCKLIST_PREF_BRANCH "webgl2";
+ break;
+ case nsIGfxInfo::FEATURE_D3D11_KEYED_MUTEX:
+ name = BLOCKLIST_PREF_BRANCH "d3d11.keyed.mutex";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRENDER:
+ name = BLOCKLIST_PREF_BRANCH "webrender";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR:
+ name = BLOCKLIST_PREF_BRANCH "webrender.compositor";
+ break;
+ case nsIGfxInfo::FEATURE_DX_NV12:
+ name = BLOCKLIST_PREF_BRANCH "dx.nv12";
+ break;
+ case nsIGfxInfo::FEATURE_DX_P010:
+ name = BLOCKLIST_PREF_BRANCH "dx.p010";
+ break;
+ case nsIGfxInfo::FEATURE_DX_P016:
+ name = BLOCKLIST_PREF_BRANCH "dx.p016";
+ break;
+ case nsIGfxInfo::FEATURE_VP8_HW_DECODE:
+ name = BLOCKLIST_PREF_BRANCH "vp8.hw-decode";
+ break;
+ case nsIGfxInfo::FEATURE_VP9_HW_DECODE:
+ name = BLOCKLIST_PREF_BRANCH "vp9.hw-decode";
+ break;
+ case nsIGfxInfo::FEATURE_GL_SWIZZLE:
+ name = BLOCKLIST_PREF_BRANCH "gl.swizzle";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRENDER_SCISSORED_CACHE_CLEARS:
+ name = BLOCKLIST_PREF_BRANCH "webrender.scissored_cache_clears";
+ break;
+ case nsIGfxInfo::FEATURE_ALLOW_WEBGL_OUT_OF_PROCESS:
+ name = BLOCKLIST_PREF_BRANCH "webgl.allow-oop";
+ break;
+ case nsIGfxInfo::FEATURE_THREADSAFE_GL:
+ name = BLOCKLIST_PREF_BRANCH "gl.threadsafe";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRENDER_OPTIMIZED_SHADERS:
+ name = BLOCKLIST_PREF_BRANCH "webrender.optimized-shaders";
+ break;
+ case nsIGfxInfo::FEATURE_X11_EGL:
+ name = BLOCKLIST_PREF_BRANCH "x11.egl";
+ break;
+ case nsIGfxInfo::FEATURE_DMABUF:
+ name = BLOCKLIST_PREF_BRANCH "dmabuf";
+ break;
+ case nsIGfxInfo::FEATURE_WEBGPU:
+ name = BLOCKLIST_PREF_BRANCH "webgpu";
+ break;
+ case nsIGfxInfo::FEATURE_VIDEO_OVERLAY:
+ name = BLOCKLIST_PREF_BRANCH "video-overlay";
+ break;
+ case nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY:
+ name = BLOCKLIST_PREF_BRANCH "hw-video-zero-copy";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRENDER_SHADER_CACHE:
+ name = BLOCKLIST_PREF_BRANCH "webrender.program-binary-disk";
+ break;
+ case nsIGfxInfo::FEATURE_WEBRENDER_PARTIAL_PRESENT:
+ name = BLOCKLIST_PREF_BRANCH "webrender.partial-present";
+ break;
+ case nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT:
+ name = BLOCKLIST_PREF_BRANCH "dmabuf.surface-export";
+ break;
+ case nsIGfxInfo::FEATURE_REUSE_DECODER_DEVICE:
+ name = BLOCKLIST_PREF_BRANCH "reuse-decoder-device";
+ break;
+ case nsIGfxInfo::FEATURE_BACKDROP_FILTER:
+ name = BLOCKLIST_PREF_BRANCH "backdrop.filter";
+ break;
+ case nsIGfxInfo::FEATURE_ACCELERATED_CANVAS2D:
+ name = BLOCKLIST_PREF_BRANCH "accelerated-canvas2d";
+ break;
+ case nsIGfxInfo::FEATURE_H264_HW_DECODE:
+ name = BLOCKLIST_PREF_BRANCH "h264.hw-decode";
+ break;
+ case nsIGfxInfo::FEATURE_AV1_HW_DECODE:
+ name = BLOCKLIST_PREF_BRANCH "av1.hw-decode";
+ break;
+ case nsIGfxInfo::FEATURE_VIDEO_SOFTWARE_OVERLAY:
+ name = BLOCKLIST_PREF_BRANCH "video-software-overlay";
+ break;
+ case nsIGfxInfo::FEATURE_WEBGL_USE_HARDWARE:
+ name = BLOCKLIST_PREF_BRANCH "webgl-use-hardware";
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected nsIGfxInfo feature?!");
+ break;
+ }
+
+ return name;
+}
+
+// Returns the value of the pref for the relevant feature in aValue.
+// If the pref doesn't exist, aValue is not touched, and returns false.
+static bool GetPrefValueForFeature(int32_t aFeature, int32_t& aValue,
+ nsACString& aFailureId) {
+ const char* prefname = GetPrefNameForFeature(aFeature);
+ if (!prefname) return false;
+
+ aValue = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ if (!NS_SUCCEEDED(Preferences::GetInt(prefname, &aValue))) {
+ return false;
+ }
+
+ if (aValue == nsIGfxInfo::FEATURE_DENIED) {
+ // We should never see the DENIED status with the downloadable blocklist.
+ return false;
+ }
+
+ nsCString failureprefname(prefname);
+ failureprefname += ".failureid";
+ nsAutoCString failureValue;
+ nsresult rv = Preferences::GetCString(failureprefname.get(), failureValue);
+ if (NS_SUCCEEDED(rv)) {
+ aFailureId = failureValue.get();
+ } else {
+ aFailureId = "FEATURE_FAILURE_BLOCKLIST_PREF";
+ }
+
+ return true;
+}
+
+static void SetPrefValueForFeature(int32_t aFeature, int32_t aValue,
+ const nsACString& aFailureId) {
+ const char* prefname = GetPrefNameForFeature(aFeature);
+ if (!prefname) return;
+ if (XRE_IsParentProcess()) {
+ GfxInfoBase::sFeatureStatus = nullptr;
+ }
+
+ Preferences::SetInt(prefname, aValue);
+ if (!aFailureId.IsEmpty()) {
+ nsAutoCString failureprefname(prefname);
+ failureprefname += ".failureid";
+ Preferences::SetCString(failureprefname.get(), aFailureId);
+ }
+}
+
+static void RemovePrefForFeature(int32_t aFeature) {
+ const char* prefname = GetPrefNameForFeature(aFeature);
+ if (!prefname) return;
+
+ if (XRE_IsParentProcess()) {
+ GfxInfoBase::sFeatureStatus = nullptr;
+ }
+ Preferences::ClearUser(prefname);
+}
+
+static bool GetPrefValueForDriverVersion(nsCString& aVersion) {
+ return NS_SUCCEEDED(
+ Preferences::GetCString(SUGGESTED_VERSION_PREF, aVersion));
+}
+
+static void SetPrefValueForDriverVersion(const nsAString& aVersion) {
+ Preferences::SetString(SUGGESTED_VERSION_PREF, aVersion);
+}
+
+static void RemovePrefForDriverVersion() {
+ Preferences::ClearUser(SUGGESTED_VERSION_PREF);
+}
+
+static OperatingSystem BlocklistOSToOperatingSystem(const nsAString& os) {
+ if (os.EqualsLiteral("WINNT 6.1")) {
+ return OperatingSystem::Windows7;
+ }
+ if (os.EqualsLiteral("WINNT 6.2")) {
+ return OperatingSystem::Windows8;
+ }
+ if (os.EqualsLiteral("WINNT 6.3")) {
+ return OperatingSystem::Windows8_1;
+ }
+ if (os.EqualsLiteral("WINNT 10.0")) {
+ return OperatingSystem::Windows10;
+ }
+ if (os.EqualsLiteral("Linux")) {
+ return OperatingSystem::Linux;
+ }
+ if (os.EqualsLiteral("Darwin 9")) {
+ return OperatingSystem::OSX10_5;
+ }
+ if (os.EqualsLiteral("Darwin 10")) {
+ return OperatingSystem::OSX10_6;
+ }
+ if (os.EqualsLiteral("Darwin 11")) {
+ return OperatingSystem::OSX10_7;
+ }
+ if (os.EqualsLiteral("Darwin 12")) {
+ return OperatingSystem::OSX10_8;
+ }
+ if (os.EqualsLiteral("Darwin 13")) {
+ return OperatingSystem::OSX10_9;
+ }
+ if (os.EqualsLiteral("Darwin 14")) {
+ return OperatingSystem::OSX10_10;
+ }
+ if (os.EqualsLiteral("Darwin 15")) {
+ return OperatingSystem::OSX10_11;
+ }
+ if (os.EqualsLiteral("Darwin 16")) {
+ return OperatingSystem::OSX10_12;
+ }
+ if (os.EqualsLiteral("Darwin 17")) {
+ return OperatingSystem::OSX10_13;
+ }
+ if (os.EqualsLiteral("Darwin 18")) {
+ return OperatingSystem::OSX10_14;
+ }
+ if (os.EqualsLiteral("Darwin 19")) {
+ return OperatingSystem::OSX10_15;
+ }
+ if (os.EqualsLiteral("Darwin 20")) {
+ return OperatingSystem::OSX11_0;
+ }
+ if (os.EqualsLiteral("Android")) {
+ return OperatingSystem::Android;
+ // For historical reasons, "All" in blocklist means "All Windows"
+ }
+ if (os.EqualsLiteral("All")) {
+ return OperatingSystem::Windows;
+ }
+ if (os.EqualsLiteral("Darwin")) {
+ return OperatingSystem::OSX;
+ }
+
+ return OperatingSystem::Unknown;
+}
+
+static GfxDeviceFamily* BlocklistDevicesToDeviceFamily(
+ nsTArray<nsCString>& devices) {
+ if (devices.Length() == 0) return nullptr;
+
+ // For each device, get its device ID, and return a freshly-allocated
+ // GfxDeviceFamily with the contents of that array.
+ GfxDeviceFamily* deviceIds = new GfxDeviceFamily;
+
+ for (uint32_t i = 0; i < devices.Length(); ++i) {
+ // We make sure we don't add any "empty" device entries to the array, so
+ // we don't need to check if devices[i] is empty.
+ deviceIds->Append(NS_ConvertUTF8toUTF16(devices[i]));
+ }
+
+ return deviceIds;
+}
+
+static int32_t BlocklistFeatureToGfxFeature(const nsAString& aFeature) {
+ MOZ_ASSERT(!aFeature.IsEmpty());
+ if (aFeature.EqualsLiteral("DIRECT2D")) {
+ return nsIGfxInfo::FEATURE_DIRECT2D;
+ }
+ if (aFeature.EqualsLiteral("DIRECT3D_9_LAYERS")) {
+ return nsIGfxInfo::FEATURE_DIRECT3D_9_LAYERS;
+ }
+ if (aFeature.EqualsLiteral("DIRECT3D_10_LAYERS")) {
+ return nsIGfxInfo::FEATURE_DIRECT3D_10_LAYERS;
+ }
+ if (aFeature.EqualsLiteral("DIRECT3D_10_1_LAYERS")) {
+ return nsIGfxInfo::FEATURE_DIRECT3D_10_1_LAYERS;
+ }
+ if (aFeature.EqualsLiteral("DIRECT3D_11_LAYERS")) {
+ return nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS;
+ }
+ if (aFeature.EqualsLiteral("DIRECT3D_11_ANGLE")) {
+ return nsIGfxInfo::FEATURE_DIRECT3D_11_ANGLE;
+ }
+ if (aFeature.EqualsLiteral("HARDWARE_VIDEO_DECODING")) {
+ return nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING;
+ }
+ if (aFeature.EqualsLiteral("OPENGL_LAYERS")) {
+ return nsIGfxInfo::FEATURE_OPENGL_LAYERS;
+ }
+ if (aFeature.EqualsLiteral("WEBGL_OPENGL")) {
+ return nsIGfxInfo::FEATURE_WEBGL_OPENGL;
+ }
+ if (aFeature.EqualsLiteral("WEBGL_ANGLE")) {
+ return nsIGfxInfo::FEATURE_WEBGL_ANGLE;
+ }
+ if (aFeature.EqualsLiteral("WEBGL_MSAA")) {
+ return nsIGfxInfo::UNUSED_FEATURE_WEBGL_MSAA;
+ }
+ if (aFeature.EqualsLiteral("STAGEFRIGHT")) {
+ return nsIGfxInfo::FEATURE_STAGEFRIGHT;
+ }
+ if (aFeature.EqualsLiteral("WEBRTC_HW_ACCELERATION_ENCODE")) {
+ return nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE;
+ }
+ if (aFeature.EqualsLiteral("WEBRTC_HW_ACCELERATION_DECODE")) {
+ return nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE;
+ }
+ if (aFeature.EqualsLiteral("WEBRTC_HW_ACCELERATION_H264")) {
+ return nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_H264;
+ }
+ if (aFeature.EqualsLiteral("CANVAS2D_ACCELERATION")) {
+ return nsIGfxInfo::FEATURE_CANVAS2D_ACCELERATION;
+ }
+ if (aFeature.EqualsLiteral("DX_INTEROP2")) {
+ return nsIGfxInfo::FEATURE_DX_INTEROP2;
+ }
+ if (aFeature.EqualsLiteral("GPU_PROCESS")) {
+ return nsIGfxInfo::FEATURE_GPU_PROCESS;
+ }
+ if (aFeature.EqualsLiteral("WEBGL2")) {
+ return nsIGfxInfo::FEATURE_WEBGL2;
+ }
+ if (aFeature.EqualsLiteral("D3D11_KEYED_MUTEX")) {
+ return nsIGfxInfo::FEATURE_D3D11_KEYED_MUTEX;
+ }
+ if (aFeature.EqualsLiteral("WEBRENDER")) {
+ return nsIGfxInfo::FEATURE_WEBRENDER;
+ }
+ if (aFeature.EqualsLiteral("WEBRENDER_COMPOSITOR")) {
+ return nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR;
+ }
+ if (aFeature.EqualsLiteral("DX_NV12")) {
+ return nsIGfxInfo::FEATURE_DX_NV12;
+ }
+ if (aFeature.EqualsLiteral("VP8_HW_DECODE")) {
+ return nsIGfxInfo::FEATURE_VP8_HW_DECODE;
+ }
+ if (aFeature.EqualsLiteral("VP9_HW_DECODE")) {
+ return nsIGfxInfo::FEATURE_VP9_HW_DECODE;
+ }
+ if (aFeature.EqualsLiteral("GL_SWIZZLE")) {
+ return nsIGfxInfo::FEATURE_GL_SWIZZLE;
+ }
+ if (aFeature.EqualsLiteral("WEBRENDER_SCISSORED_CACHE_CLEARS")) {
+ return nsIGfxInfo::FEATURE_WEBRENDER_SCISSORED_CACHE_CLEARS;
+ }
+ if (aFeature.EqualsLiteral("ALLOW_WEBGL_OUT_OF_PROCESS")) {
+ return nsIGfxInfo::FEATURE_ALLOW_WEBGL_OUT_OF_PROCESS;
+ }
+ if (aFeature.EqualsLiteral("THREADSAFE_GL")) {
+ return nsIGfxInfo::FEATURE_THREADSAFE_GL;
+ }
+ if (aFeature.EqualsLiteral("X11_EGL")) {
+ return nsIGfxInfo::FEATURE_X11_EGL;
+ }
+ if (aFeature.EqualsLiteral("DMABUF")) {
+ return nsIGfxInfo::FEATURE_DMABUF;
+ }
+ if (aFeature.EqualsLiteral("WEBGPU")) {
+ return nsIGfxInfo::FEATURE_WEBGPU;
+ }
+ if (aFeature.EqualsLiteral("VIDEO_OVERLAY")) {
+ return nsIGfxInfo::FEATURE_VIDEO_OVERLAY;
+ }
+ if (aFeature.EqualsLiteral("HW_DECODED_VIDEO_ZERO_COPY")) {
+ return nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY;
+ }
+ if (aFeature.EqualsLiteral("REUSE_DECODER_DEVICE")) {
+ return nsIGfxInfo::FEATURE_REUSE_DECODER_DEVICE;
+ }
+ if (aFeature.EqualsLiteral("WEBRENDER_PARTIAL_PRESENT")) {
+ return nsIGfxInfo::FEATURE_WEBRENDER_PARTIAL_PRESENT;
+ }
+ if (aFeature.EqualsLiteral("BACKDROP_FILTER")) {
+ return nsIGfxInfo::FEATURE_BACKDROP_FILTER;
+ }
+ if (aFeature.EqualsLiteral("ACCELERATED_CANVAS2D")) {
+ return nsIGfxInfo::FEATURE_ACCELERATED_CANVAS2D;
+ }
+ if (aFeature.EqualsLiteral("ALL")) {
+ return GfxDriverInfo::allFeatures;
+ }
+ if (aFeature.EqualsLiteral("OPTIONAL")) {
+ return GfxDriverInfo::optionalFeatures;
+ }
+
+ // If we don't recognize the feature, it may be new, and something
+ // this version doesn't understand. So, nothing to do. This is
+ // different from feature not being specified at all, in which case
+ // this method should not get called and we should continue with the
+ // "optional features" blocklisting.
+ return 0;
+}
+
+static int32_t BlocklistFeatureStatusToGfxFeatureStatus(
+ const nsAString& aStatus) {
+ if (aStatus.EqualsLiteral("STATUS_OK")) {
+ return nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ if (aStatus.EqualsLiteral("BLOCKED_DRIVER_VERSION")) {
+ return nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION;
+ }
+ if (aStatus.EqualsLiteral("BLOCKED_DEVICE")) {
+ return nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ }
+ if (aStatus.EqualsLiteral("DISCOURAGED")) {
+ return nsIGfxInfo::FEATURE_DISCOURAGED;
+ }
+ if (aStatus.EqualsLiteral("BLOCKED_OS_VERSION")) {
+ return nsIGfxInfo::FEATURE_BLOCKED_OS_VERSION;
+ }
+ if (aStatus.EqualsLiteral("DENIED")) {
+ return nsIGfxInfo::FEATURE_DENIED;
+ }
+ if (aStatus.EqualsLiteral("ALLOW_QUALIFIED")) {
+ return nsIGfxInfo::FEATURE_ALLOW_QUALIFIED;
+ }
+ if (aStatus.EqualsLiteral("ALLOW_ALWAYS")) {
+ return nsIGfxInfo::FEATURE_ALLOW_ALWAYS;
+ }
+
+ // Do not allow it to set STATUS_UNKNOWN. Also, we are not
+ // expecting the "mismatch" status showing up here.
+
+ return nsIGfxInfo::FEATURE_STATUS_OK;
+}
+
+static VersionComparisonOp BlocklistComparatorToComparisonOp(
+ const nsAString& op) {
+ if (op.EqualsLiteral("LESS_THAN")) {
+ return DRIVER_LESS_THAN;
+ }
+ if (op.EqualsLiteral("BUILD_ID_LESS_THAN")) {
+ return DRIVER_BUILD_ID_LESS_THAN;
+ }
+ if (op.EqualsLiteral("LESS_THAN_OR_EQUAL")) {
+ return DRIVER_LESS_THAN_OR_EQUAL;
+ }
+ if (op.EqualsLiteral("BUILD_ID_LESS_THAN_OR_EQUAL")) {
+ return DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL;
+ }
+ if (op.EqualsLiteral("GREATER_THAN")) {
+ return DRIVER_GREATER_THAN;
+ }
+ if (op.EqualsLiteral("GREATER_THAN_OR_EQUAL")) {
+ return DRIVER_GREATER_THAN_OR_EQUAL;
+ }
+ if (op.EqualsLiteral("EQUAL")) {
+ return DRIVER_EQUAL;
+ }
+ if (op.EqualsLiteral("NOT_EQUAL")) {
+ return DRIVER_NOT_EQUAL;
+ }
+ if (op.EqualsLiteral("BETWEEN_EXCLUSIVE")) {
+ return DRIVER_BETWEEN_EXCLUSIVE;
+ }
+ if (op.EqualsLiteral("BETWEEN_INCLUSIVE")) {
+ return DRIVER_BETWEEN_INCLUSIVE;
+ }
+ if (op.EqualsLiteral("BETWEEN_INCLUSIVE_START")) {
+ return DRIVER_BETWEEN_INCLUSIVE_START;
+ }
+
+ return DRIVER_COMPARISON_IGNORED;
+}
+
+/*
+ Deserialize Blocklist entries from string.
+ e.g:
+ os:WINNT 6.0\tvendor:0x8086\tdevices:0x2582,0x2782\tfeature:DIRECT3D_10_LAYERS\tfeatureStatus:BLOCKED_DRIVER_VERSION\tdriverVersion:8.52.322.2202\tdriverVersionComparator:LESS_THAN_OR_EQUAL
+*/
+static bool BlocklistEntryToDriverInfo(const nsACString& aBlocklistEntry,
+ GfxDriverInfo& aDriverInfo) {
+ // If we get an application version to be zero, something is not working
+ // and we are not going to bother checking the blocklist versions.
+ // See TestGfxWidgets.cpp for how version comparison works.
+ // <versionRange minVersion="42.0a1" maxVersion="45.0"></versionRange>
+ static mozilla::Version zeroV("0");
+ static mozilla::Version appV(GfxInfoBase::GetApplicationVersion().get());
+ if (appV <= zeroV) {
+ gfxCriticalErrorOnce(gfxCriticalError::DefaultOptions(false))
+ << "Invalid application version "
+ << GfxInfoBase::GetApplicationVersion().get();
+ }
+
+ aDriverInfo.mRuleId = "FEATURE_FAILURE_DL_BLOCKLIST_NO_ID"_ns;
+
+ for (const auto& keyValue : aBlocklistEntry.Split('\t')) {
+ nsTArray<nsCString> splitted;
+ ParseString(keyValue, ':', splitted);
+ if (splitted.Length() != 2) {
+ // If we don't recognize the input data, we do not want to proceed.
+ gfxCriticalErrorOnce(CriticalLog::DefaultOptions(false))
+ << "Unrecognized data " << nsCString(keyValue).get();
+ return false;
+ }
+ const nsCString& key = splitted[0];
+ const nsCString& value = splitted[1];
+ NS_ConvertUTF8toUTF16 dataValue(value);
+
+ if (value.Length() == 0) {
+ // Safety check for empty values.
+ gfxCriticalErrorOnce(CriticalLog::DefaultOptions(false))
+ << "Empty value for " << key.get();
+ return false;
+ }
+
+ if (key.EqualsLiteral("blockID")) {
+ nsCString blockIdStr = "FEATURE_FAILURE_DL_BLOCKLIST_"_ns + value;
+ aDriverInfo.mRuleId = blockIdStr.get();
+ } else if (key.EqualsLiteral("os")) {
+ aDriverInfo.mOperatingSystem = BlocklistOSToOperatingSystem(dataValue);
+ } else if (key.EqualsLiteral("osversion")) {
+ aDriverInfo.mOperatingSystemVersion = strtoul(value.get(), nullptr, 10);
+ } else if (key.EqualsLiteral("windowProtocol")) {
+ aDriverInfo.mWindowProtocol = dataValue;
+ } else if (key.EqualsLiteral("vendor")) {
+ aDriverInfo.mAdapterVendor = dataValue;
+ } else if (key.EqualsLiteral("driverVendor")) {
+ aDriverInfo.mDriverVendor = dataValue;
+ } else if (key.EqualsLiteral("feature")) {
+ aDriverInfo.mFeature = BlocklistFeatureToGfxFeature(dataValue);
+ if (aDriverInfo.mFeature == 0) {
+ // If we don't recognize the feature, we do not want to proceed.
+ gfxWarning() << "Unrecognized feature " << value.get();
+ return false;
+ }
+ } else if (key.EqualsLiteral("featureStatus")) {
+ aDriverInfo.mFeatureStatus =
+ BlocklistFeatureStatusToGfxFeatureStatus(dataValue);
+ } else if (key.EqualsLiteral("driverVersion")) {
+ uint64_t version;
+ if (ParseDriverVersion(dataValue, &version))
+ aDriverInfo.mDriverVersion = version;
+ } else if (key.EqualsLiteral("driverVersionMax")) {
+ uint64_t version;
+ if (ParseDriverVersion(dataValue, &version))
+ aDriverInfo.mDriverVersionMax = version;
+ } else if (key.EqualsLiteral("driverVersionComparator")) {
+ aDriverInfo.mComparisonOp = BlocklistComparatorToComparisonOp(dataValue);
+ } else if (key.EqualsLiteral("model")) {
+ aDriverInfo.mModel = dataValue;
+ } else if (key.EqualsLiteral("product")) {
+ aDriverInfo.mProduct = dataValue;
+ } else if (key.EqualsLiteral("manufacturer")) {
+ aDriverInfo.mManufacturer = dataValue;
+ } else if (key.EqualsLiteral("hardware")) {
+ aDriverInfo.mHardware = dataValue;
+ } else if (key.EqualsLiteral("versionRange")) {
+ nsTArray<nsCString> versionRange;
+ ParseString(value, ',', versionRange);
+ if (versionRange.Length() != 2) {
+ gfxCriticalErrorOnce(CriticalLog::DefaultOptions(false))
+ << "Unrecognized versionRange " << value.get();
+ return false;
+ }
+ const nsCString& minValue = versionRange[0];
+ const nsCString& maxValue = versionRange[1];
+
+ mozilla::Version minV(minValue.get());
+ mozilla::Version maxV(maxValue.get());
+
+ if (minV > zeroV && !(appV >= minV)) {
+ // The version of the application is less than the minimal version
+ // this blocklist entry applies to, so we can just ignore it by
+ // returning false and letting the caller deal with it.
+ return false;
+ }
+ if (maxV > zeroV && !(appV <= maxV)) {
+ // The version of the application is more than the maximal version
+ // this blocklist entry applies to, so we can just ignore it by
+ // returning false and letting the caller deal with it.
+ return false;
+ }
+ } else if (key.EqualsLiteral("devices")) {
+ nsTArray<nsCString> devices;
+ ParseString(value, ',', devices);
+ GfxDeviceFamily* deviceIds = BlocklistDevicesToDeviceFamily(devices);
+ if (deviceIds) {
+ // Get GfxDriverInfo to adopt the devices array we created.
+ aDriverInfo.mDeleteDevices = true;
+ aDriverInfo.mDevices = deviceIds;
+ }
+ }
+ // We explicitly ignore unknown elements.
+ }
+
+ return true;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (strcmp(aTopic, "blocklist-data-gfxItems") == 0) {
+ nsTArray<GfxDriverInfo> driverInfo;
+ NS_ConvertUTF16toUTF8 utf8Data(aData);
+
+ for (const auto& blocklistEntry : utf8Data.Split('\n')) {
+ GfxDriverInfo di;
+ if (BlocklistEntryToDriverInfo(blocklistEntry, di)) {
+ // XXX Changing this to driverInfo.AppendElement(di) causes leaks.
+ // Probably some non-standard semantics of the copy/move operations?
+ *driverInfo.AppendElement() = di;
+ // Prevent di falling out of scope from destroying the devices.
+ di.mDeleteDevices = false;
+ } else {
+ driverInfo.AppendElement();
+ }
+ }
+
+ EvaluateDownloadedBlocklist(driverInfo);
+ }
+
+ return NS_OK;
+}
+
+GfxInfoBase::GfxInfoBase() : mScreenPixels(INT64_MAX), mMutex("GfxInfoBase") {}
+
+GfxInfoBase::~GfxInfoBase() = default;
+
+nsresult GfxInfoBase::Init() {
+ InitGfxDriverInfoShutdownObserver();
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->AddObserver(this, "blocklist-data-gfxItems", true);
+ }
+
+ return NS_OK;
+}
+
+void GfxInfoBase::GetData() {
+ if (mScreenPixels != INT64_MAX) {
+ // Already initialized.
+ return;
+ }
+
+ ScreenManager::GetSingleton().GetTotalScreenPixels(&mScreenPixels);
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetFeatureStatus(int32_t aFeature, nsACString& aFailureId,
+ int32_t* aStatus) {
+ // Ignore the gfx.blocklist.all pref on release and beta.
+#if defined(RELEASE_OR_BETA)
+ int32_t blocklistAll = 0;
+#else
+ int32_t blocklistAll = StaticPrefs::gfx_blocklist_all_AtStartup();
+#endif
+ if (blocklistAll > 0) {
+ gfxCriticalErrorOnce(gfxCriticalError::DefaultOptions(false))
+ << "Forcing blocklisting all features";
+ *aStatus = FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_BLOCK_ALL";
+ return NS_OK;
+ }
+
+ if (blocklistAll < 0) {
+ gfxCriticalErrorOnce(gfxCriticalError::DefaultOptions(false))
+ << "Ignoring any feature blocklisting.";
+ *aStatus = FEATURE_STATUS_OK;
+ return NS_OK;
+ }
+
+ // This is how we evaluate the downloadable blocklist. If there is no pref,
+ // then we will fallback to checking the static blocklist.
+ if (GetPrefValueForFeature(aFeature, *aStatus, aFailureId)) {
+ return NS_OK;
+ }
+
+ if (XRE_IsContentProcess() || XRE_IsGPUProcess()) {
+ // Use the cached data received from the parent process.
+ MOZ_ASSERT(sFeatureStatus);
+ bool success = false;
+ for (const auto& fs : *sFeatureStatus) {
+ if (fs.feature() == aFeature) {
+ aFailureId = fs.failureId();
+ *aStatus = fs.status();
+ success = true;
+ break;
+ }
+ }
+ return success ? NS_OK : NS_ERROR_FAILURE;
+ }
+
+ nsString version;
+ nsTArray<GfxDriverInfo> driverInfo;
+ nsresult rv =
+ GetFeatureStatusImpl(aFeature, aStatus, version, driverInfo, aFailureId);
+ return rv;
+}
+
+nsTArray<gfx::GfxInfoFeatureStatus> GfxInfoBase::GetAllFeatures() {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ if (!sFeatureStatus) {
+ InitFeatureStatus(new nsTArray<gfx::GfxInfoFeatureStatus>());
+ for (int32_t i = 1; i <= nsIGfxInfo::FEATURE_MAX_VALUE; ++i) {
+ int32_t status = 0;
+ nsAutoCString failureId;
+ GetFeatureStatus(i, failureId, &status);
+ gfx::GfxInfoFeatureStatus gfxFeatureStatus;
+ gfxFeatureStatus.feature() = i;
+ gfxFeatureStatus.status() = status;
+ gfxFeatureStatus.failureId() = failureId;
+ sFeatureStatus->AppendElement(gfxFeatureStatus);
+ }
+ }
+
+ nsTArray<gfx::GfxInfoFeatureStatus> features;
+ for (const auto& status : *sFeatureStatus) {
+ gfx::GfxInfoFeatureStatus copy = status;
+ features.AppendElement(copy);
+ }
+ return features;
+}
+
+inline bool MatchingAllowStatus(int32_t aStatus) {
+ switch (aStatus) {
+ case nsIGfxInfo::FEATURE_ALLOW_ALWAYS:
+ case nsIGfxInfo::FEATURE_ALLOW_QUALIFIED:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Matching OS go somewhat beyond the simple equality check because of the
+// "All Windows" and "All OS X" variations.
+//
+// aBlockedOS is describing the system(s) we are trying to block.
+// aSystemOS is describing the system we are running on.
+//
+// aSystemOS should not be "Windows" or "OSX" - it should be set to
+// a particular version instead.
+// However, it is valid for aBlockedOS to be one of those generic values,
+// as we could be blocking all of the versions.
+inline bool MatchingOperatingSystems(OperatingSystem aBlockedOS,
+ OperatingSystem aSystemOS,
+ uint32_t aSystemOSBuild) {
+ MOZ_ASSERT(aSystemOS != OperatingSystem::Windows &&
+ aSystemOS != OperatingSystem::OSX);
+
+ // If the block entry OS is unknown, it doesn't match
+ if (aBlockedOS == OperatingSystem::Unknown) {
+ return false;
+ }
+
+#if defined(XP_WIN)
+ if (aBlockedOS == OperatingSystem::Windows) {
+ // We do want even "unknown" aSystemOS to fall under "all windows"
+ return true;
+ }
+
+ constexpr uint32_t kMinWin10BuildNumber = 18362;
+ if (aBlockedOS == OperatingSystem::RecentWindows10 &&
+ aSystemOS == OperatingSystem::Windows10) {
+ // For allowlist purposes, we sometimes want to restrict to only recent
+ // versions of Windows 10. This is a bit of a kludge but easier than adding
+ // complicated blocklist infrastructure for build ID comparisons like driver
+ // versions.
+ return aSystemOSBuild >= kMinWin10BuildNumber;
+ }
+
+ if (aBlockedOS == OperatingSystem::NotRecentWindows10) {
+ if (aSystemOS == OperatingSystem::Windows10) {
+ return aSystemOSBuild < kMinWin10BuildNumber;
+ } else {
+ return true;
+ }
+ }
+#endif
+
+#if defined(XP_MACOSX)
+ if (aBlockedOS == OperatingSystem::OSX) {
+ // We do want even "unknown" aSystemOS to fall under "all OS X"
+ return true;
+ }
+#endif
+
+ return aSystemOS == aBlockedOS;
+}
+
+inline bool MatchingBattery(BatteryStatus aBatteryStatus, bool aHasBattery) {
+ switch (aBatteryStatus) {
+ case BatteryStatus::All:
+ return true;
+ case BatteryStatus::None:
+ return !aHasBattery;
+ case BatteryStatus::Present:
+ return aHasBattery;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("bad battery status");
+ return false;
+}
+
+inline bool MatchingScreenSize(ScreenSizeStatus aScreenStatus,
+ int64_t aScreenPixels) {
+ constexpr int64_t kMaxSmallPixels = 2304000; // 1920x1200
+ constexpr int64_t kMaxMediumPixels = 4953600; // 3440x1440
+
+ switch (aScreenStatus) {
+ case ScreenSizeStatus::All:
+ return true;
+ case ScreenSizeStatus::Small:
+ return aScreenPixels <= kMaxSmallPixels;
+ case ScreenSizeStatus::SmallAndMedium:
+ return aScreenPixels <= kMaxMediumPixels;
+ case ScreenSizeStatus::Medium:
+ return aScreenPixels > kMaxSmallPixels &&
+ aScreenPixels <= kMaxMediumPixels;
+ case ScreenSizeStatus::MediumAndLarge:
+ return aScreenPixels > kMaxSmallPixels;
+ case ScreenSizeStatus::Large:
+ return aScreenPixels > kMaxMediumPixels;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("bad screen status");
+ return false;
+}
+
+int32_t GfxInfoBase::FindBlocklistedDeviceInList(
+ const nsTArray<GfxDriverInfo>& info, nsAString& aSuggestedVersion,
+ int32_t aFeature, nsACString& aFailureId, OperatingSystem os,
+ bool aForAllowing) {
+ int32_t status = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+
+ // Some properties are not available on all platforms.
+ nsAutoString windowProtocol;
+ nsresult rv = GetWindowProtocol(windowProtocol);
+ if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) {
+ return 0;
+ }
+
+ bool hasBattery = false;
+ rv = GetHasBattery(&hasBattery);
+ if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) {
+ return 0;
+ }
+
+ uint32_t osBuild = OperatingSystemBuild();
+
+ // Get the adapters once then reuse below
+ nsAutoString adapterVendorID[2];
+ nsAutoString adapterDeviceID[2];
+ nsAutoString adapterDriverVendor[2];
+ nsAutoString adapterDriverVersionString[2];
+ bool adapterInfoFailed[2];
+
+ adapterInfoFailed[0] =
+ (NS_FAILED(GetAdapterVendorID(adapterVendorID[0])) ||
+ NS_FAILED(GetAdapterDeviceID(adapterDeviceID[0])) ||
+ NS_FAILED(GetAdapterDriverVendor(adapterDriverVendor[0])) ||
+ NS_FAILED(GetAdapterDriverVersion(adapterDriverVersionString[0])));
+ adapterInfoFailed[1] =
+ (NS_FAILED(GetAdapterVendorID2(adapterVendorID[1])) ||
+ NS_FAILED(GetAdapterDeviceID2(adapterDeviceID[1])) ||
+ NS_FAILED(GetAdapterDriverVendor2(adapterDriverVendor[1])) ||
+ NS_FAILED(GetAdapterDriverVersion2(adapterDriverVersionString[1])));
+ // No point in going on if we don't have adapter info
+ if (adapterInfoFailed[0] && adapterInfoFailed[1]) {
+ return 0;
+ }
+
+#if defined(XP_WIN) || defined(ANDROID) || defined(MOZ_WIDGET_GTK)
+ uint64_t driverVersion[2] = {0, 0};
+ if (!adapterInfoFailed[0]) {
+ ParseDriverVersion(adapterDriverVersionString[0], &driverVersion[0]);
+ }
+ if (!adapterInfoFailed[1]) {
+ ParseDriverVersion(adapterDriverVersionString[1], &driverVersion[1]);
+ }
+#endif
+
+ uint32_t i = 0;
+ for (; i < info.Length(); i++) {
+ // If the status is FEATURE_ALLOW_*, then it is for the allowlist, not
+ // blocklisting. Only consider entries for our search mode.
+ if (MatchingAllowStatus(info[i].mFeatureStatus) != aForAllowing) {
+ continue;
+ }
+
+ // If we don't have the info for this GPU, no need to check further.
+ // It is unclear that we would ever have a mixture of 1st and 2nd
+ // GPU, but leaving the code in for that possibility for now.
+ // (Actually, currently mGpu2 will never be true, so this can
+ // be optimized out.)
+ uint32_t infoIndex = info[i].mGpu2 ? 1 : 0;
+ if (adapterInfoFailed[infoIndex]) {
+ continue;
+ }
+
+ // Do the operating system check first, no point in getting the driver
+ // info if we won't need to use it.
+ if (!MatchingOperatingSystems(info[i].mOperatingSystem, os, osBuild)) {
+ continue;
+ }
+
+ if (info[i].mOperatingSystemVersion &&
+ info[i].mOperatingSystemVersion != OperatingSystemVersion()) {
+ continue;
+ }
+
+ if (!MatchingBattery(info[i].mBattery, hasBattery)) {
+ continue;
+ }
+
+ if (!MatchingScreenSize(info[i].mScreen, mScreenPixels)) {
+ continue;
+ }
+
+ if (!DoesWindowProtocolMatch(info[i].mWindowProtocol, windowProtocol)) {
+ continue;
+ }
+
+ if (!DoesVendorMatch(info[i].mAdapterVendor, adapterVendorID[infoIndex])) {
+ continue;
+ }
+
+ if (!DoesDriverVendorMatch(info[i].mDriverVendor,
+ adapterDriverVendor[infoIndex])) {
+ continue;
+ }
+
+ if (info[i].mDevices && !info[i].mDevices->IsEmpty()) {
+ nsresult rv = info[i].mDevices->Contains(adapterDeviceID[infoIndex]);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ // Not found
+ continue;
+ }
+ if (rv != NS_OK) {
+ // Failed to search, allowlist should not match, blocklist should match
+ // for safety reasons
+ if (aForAllowing) {
+ continue;
+ }
+ break;
+ }
+ }
+
+ bool match = false;
+
+ if (!info[i].mHardware.IsEmpty() && !info[i].mHardware.Equals(Hardware())) {
+ continue;
+ }
+ if (!info[i].mModel.IsEmpty() && !info[i].mModel.Equals(Model())) {
+ continue;
+ }
+ if (!info[i].mProduct.IsEmpty() && !info[i].mProduct.Equals(Product())) {
+ continue;
+ }
+ if (!info[i].mManufacturer.IsEmpty() &&
+ !info[i].mManufacturer.Equals(Manufacturer())) {
+ continue;
+ }
+
+#if defined(XP_WIN) || defined(ANDROID) || defined(MOZ_WIDGET_GTK)
+ switch (info[i].mComparisonOp) {
+ case DRIVER_LESS_THAN:
+ match = driverVersion[infoIndex] < info[i].mDriverVersion;
+ break;
+ case DRIVER_BUILD_ID_LESS_THAN:
+ match = (driverVersion[infoIndex] & 0xFFFF) < info[i].mDriverVersion;
+ break;
+ case DRIVER_LESS_THAN_OR_EQUAL:
+ match = driverVersion[infoIndex] <= info[i].mDriverVersion;
+ break;
+ case DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL:
+ match = (driverVersion[infoIndex] & 0xFFFF) <= info[i].mDriverVersion;
+ break;
+ case DRIVER_GREATER_THAN:
+ match = driverVersion[infoIndex] > info[i].mDriverVersion;
+ break;
+ case DRIVER_GREATER_THAN_OR_EQUAL:
+ match = driverVersion[infoIndex] >= info[i].mDriverVersion;
+ break;
+ case DRIVER_EQUAL:
+ match = driverVersion[infoIndex] == info[i].mDriverVersion;
+ break;
+ case DRIVER_NOT_EQUAL:
+ match = driverVersion[infoIndex] != info[i].mDriverVersion;
+ break;
+ case DRIVER_BETWEEN_EXCLUSIVE:
+ match = driverVersion[infoIndex] > info[i].mDriverVersion &&
+ driverVersion[infoIndex] < info[i].mDriverVersionMax;
+ break;
+ case DRIVER_BETWEEN_INCLUSIVE:
+ match = driverVersion[infoIndex] >= info[i].mDriverVersion &&
+ driverVersion[infoIndex] <= info[i].mDriverVersionMax;
+ break;
+ case DRIVER_BETWEEN_INCLUSIVE_START:
+ match = driverVersion[infoIndex] >= info[i].mDriverVersion &&
+ driverVersion[infoIndex] < info[i].mDriverVersionMax;
+ break;
+ case DRIVER_COMPARISON_IGNORED:
+ // We don't have a comparison op, so we match everything.
+ match = true;
+ break;
+ default:
+ NS_WARNING("Bogus op in GfxDriverInfo");
+ break;
+ }
+#else
+ // We don't care what driver version it was. We only check OS version and if
+ // the device matches.
+ match = true;
+#endif
+
+ if (match || info[i].mDriverVersion == GfxDriverInfo::allDriverVersions) {
+ if (info[i].mFeature == GfxDriverInfo::allFeatures ||
+ info[i].mFeature == aFeature ||
+ (info[i].mFeature == GfxDriverInfo::optionalFeatures &&
+ OnlyAllowFeatureOnKnownConfig(aFeature))) {
+ status = info[i].mFeatureStatus;
+ if (!info[i].mRuleId.IsEmpty()) {
+ aFailureId = info[i].mRuleId.get();
+ } else {
+ aFailureId = "FEATURE_FAILURE_DL_BLOCKLIST_NO_ID";
+ }
+ break;
+ }
+ }
+ }
+
+#if defined(XP_WIN)
+ // As a very special case, we block D2D on machines with an NVidia 310M GPU
+ // as either the primary or secondary adapter. D2D is also blocked when the
+ // NV 310M is the primary adapter (using the standard blocklisting mechanism).
+ // If the primary GPU already matched something in the blocklist then we
+ // ignore this special rule. See bug 1008759.
+ if (status == nsIGfxInfo::FEATURE_STATUS_UNKNOWN &&
+ (aFeature == nsIGfxInfo::FEATURE_DIRECT2D)) {
+ if (!adapterInfoFailed[1]) {
+ nsAString& nvVendorID =
+ (nsAString&)GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA);
+ const nsString nv310mDeviceId = u"0x0A70"_ns;
+ if (nvVendorID.Equals(adapterVendorID[1],
+ nsCaseInsensitiveStringComparator) &&
+ nv310mDeviceId.Equals(adapterDeviceID[1],
+ nsCaseInsensitiveStringComparator)) {
+ status = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_D2D_NV310M_BLOCK";
+ }
+ }
+ }
+
+ // Depends on Windows driver versioning. We don't pass a GfxDriverInfo object
+ // back to the Windows handler, so we must handle this here.
+ if (status == FEATURE_BLOCKED_DRIVER_VERSION) {
+ if (info[i].mSuggestedVersion) {
+ aSuggestedVersion.AppendPrintf("%s", info[i].mSuggestedVersion);
+ } else if (info[i].mComparisonOp == DRIVER_LESS_THAN &&
+ info[i].mDriverVersion != GfxDriverInfo::allDriverVersions) {
+ aSuggestedVersion.AppendPrintf(
+ "%lld.%lld.%lld.%lld",
+ (info[i].mDriverVersion & 0xffff000000000000) >> 48,
+ (info[i].mDriverVersion & 0x0000ffff00000000) >> 32,
+ (info[i].mDriverVersion & 0x00000000ffff0000) >> 16,
+ (info[i].mDriverVersion & 0x000000000000ffff));
+ }
+ }
+#endif
+
+ return status;
+}
+
+void GfxInfoBase::SetFeatureStatus(nsTArray<gfx::GfxInfoFeatureStatus>&& aFS) {
+ MOZ_ASSERT(!sFeatureStatus);
+ InitFeatureStatus(new nsTArray<gfx::GfxInfoFeatureStatus>(std::move(aFS)));
+}
+
+bool GfxInfoBase::DoesWindowProtocolMatch(
+ const nsAString& aBlocklistWindowProtocol,
+ const nsAString& aWindowProtocol) {
+ return aBlocklistWindowProtocol.Equals(aWindowProtocol,
+ nsCaseInsensitiveStringComparator) ||
+ aBlocklistWindowProtocol.Equals(
+ GfxDriverInfo::GetWindowProtocol(WindowProtocol::All),
+ nsCaseInsensitiveStringComparator);
+}
+
+bool GfxInfoBase::DoesVendorMatch(const nsAString& aBlocklistVendor,
+ const nsAString& aAdapterVendor) {
+ return aBlocklistVendor.Equals(aAdapterVendor,
+ nsCaseInsensitiveStringComparator) ||
+ aBlocklistVendor.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::All),
+ nsCaseInsensitiveStringComparator);
+}
+
+bool GfxInfoBase::DoesDriverVendorMatch(const nsAString& aBlocklistVendor,
+ const nsAString& aDriverVendor) {
+ return aBlocklistVendor.Equals(aDriverVendor,
+ nsCaseInsensitiveStringComparator) ||
+ aBlocklistVendor.Equals(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::All),
+ nsCaseInsensitiveStringComparator);
+}
+
+bool GfxInfoBase::IsFeatureAllowlisted(int32_t aFeature) const {
+ return aFeature == nsIGfxInfo::FEATURE_VIDEO_OVERLAY ||
+ aFeature == nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY;
+}
+
+nsresult GfxInfoBase::GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS /* = nullptr */) {
+ if (aFeature <= 0) {
+ gfxWarning() << "Invalid feature <= 0";
+ return NS_OK;
+ }
+
+ if (*aStatus != nsIGfxInfo::FEATURE_STATUS_UNKNOWN) {
+ // Terminate now with the status determined by the derived type (OS-specific
+ // code).
+ return NS_OK;
+ }
+
+ if (sShutdownOccurred) {
+ // This is futile; we've already commenced shutdown and our blocklists have
+ // been deleted. We may want to look into resurrecting the blocklist instead
+ // but for now, just don't even go there.
+ return NS_OK;
+ }
+
+ // Ensure any additional initialization required is complete.
+ GetData();
+
+ // If an operating system was provided by the derived GetFeatureStatusImpl,
+ // grab it here. Otherwise, the OS is unknown.
+ OperatingSystem os = (aOS ? *aOS : OperatingSystem::Unknown);
+
+ nsAutoString adapterVendorID;
+ nsAutoString adapterDeviceID;
+ nsAutoString adapterDriverVersionString;
+ if (NS_FAILED(GetAdapterVendorID(adapterVendorID)) ||
+ NS_FAILED(GetAdapterDeviceID(adapterDeviceID)) ||
+ NS_FAILED(GetAdapterDriverVersion(adapterDriverVersionString))) {
+ if (OnlyAllowFeatureOnKnownConfig(aFeature)) {
+ aFailureId = "FEATURE_FAILURE_CANT_RESOLVE_ADAPTER";
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ // We only check either the given blocklist, or the static list, as given.
+ int32_t status;
+ if (aDriverInfo.Length()) {
+ status =
+ FindBlocklistedDeviceInList(aDriverInfo, aSuggestedVersion, aFeature,
+ aFailureId, os, /* aForAllowing */ false);
+ } else {
+ if (!sDriverInfo) {
+ sDriverInfo = new nsTArray<GfxDriverInfo>();
+ }
+ status = FindBlocklistedDeviceInList(GetGfxDriverInfo(), aSuggestedVersion,
+ aFeature, aFailureId, os,
+ /* aForAllowing */ false);
+ }
+
+ if (status == nsIGfxInfo::FEATURE_STATUS_UNKNOWN) {
+ if (IsFeatureAllowlisted(aFeature)) {
+ // This feature is actually using the allowlist; that means after we pass
+ // the blocklist to prevent us explicitly from getting the feature, we now
+ // need to check the allowlist to ensure we are allowed to get it in the
+ // first place.
+ if (aDriverInfo.Length()) {
+ status = FindBlocklistedDeviceInList(aDriverInfo, aSuggestedVersion,
+ aFeature, aFailureId, os,
+ /* aForAllowing */ true);
+ } else {
+ status = FindBlocklistedDeviceInList(
+ GetGfxDriverInfo(), aSuggestedVersion, aFeature, aFailureId, os,
+ /* aForAllowing */ true);
+ }
+
+ if (status == nsIGfxInfo::FEATURE_STATUS_UNKNOWN) {
+ status = nsIGfxInfo::FEATURE_DENIED;
+ }
+ } else {
+ // It's now done being processed. It's safe to set the status to
+ // STATUS_OK.
+ status = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ }
+
+ *aStatus = status;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetFeatureSuggestedDriverVersion(int32_t aFeature,
+ nsAString& aVersion) {
+ nsCString version;
+ if (GetPrefValueForDriverVersion(version)) {
+ aVersion = NS_ConvertASCIItoUTF16(version);
+ return NS_OK;
+ }
+
+ int32_t status;
+ nsCString discardFailureId;
+ nsTArray<GfxDriverInfo> driverInfo;
+ return GetFeatureStatusImpl(aFeature, &status, aVersion, driverInfo,
+ discardFailureId);
+}
+
+void GfxInfoBase::EvaluateDownloadedBlocklist(
+ nsTArray<GfxDriverInfo>& aDriverInfo) {
+ // If the list is empty, then we don't actually want to call
+ // GetFeatureStatusImpl since we will use the static list instead. In that
+ // case, all we want to do is make sure the pref is removed.
+ if (aDriverInfo.IsEmpty()) {
+ gfxCriticalNoteOnce << "Evaluate empty downloaded blocklist";
+ return;
+ }
+
+ OperatingSystem os = GetOperatingSystem();
+
+ // For every feature we know about, we evaluate whether this blocklist has a
+ // non-STATUS_OK status. If it does, we set the pref we evaluate in
+ // GetFeatureStatus above, so we don't need to hold on to this blocklist
+ // anywhere permanent.
+ for (int feature = 1; feature <= nsIGfxInfo::FEATURE_MAX_VALUE; ++feature) {
+ int32_t status = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ nsCString failureId;
+ nsAutoString suggestedVersion;
+
+ // Note that we are careful to call the base class method since we only want
+ // to evaluate the downloadable blocklist for these prefs.
+ MOZ_ALWAYS_TRUE(NS_SUCCEEDED(GfxInfoBase::GetFeatureStatusImpl(
+ feature, &status, suggestedVersion, aDriverInfo, failureId, &os)));
+
+ switch (status) {
+ default:
+ MOZ_FALLTHROUGH_ASSERT("Unhandled feature status!");
+ case nsIGfxInfo::FEATURE_STATUS_UNKNOWN:
+ // This may be returned during shutdown or for invalid features.
+ case nsIGfxInfo::FEATURE_ALLOW_ALWAYS:
+ case nsIGfxInfo::FEATURE_ALLOW_QUALIFIED:
+ case nsIGfxInfo::FEATURE_DENIED:
+ // We cannot use the downloadable blocklist to control the allowlist.
+ // If a feature is allowlisted, then we should also ignore DENIED
+ // statuses from GetFeatureStatusImpl because we don't check the
+ // static list when and this is an expected value. If we wish to
+ // override the allowlist, it is as simple as creating a normal
+ // blocklist rule with a BLOCKED* status code.
+ case nsIGfxInfo::FEATURE_STATUS_OK:
+ RemovePrefForFeature(feature);
+ break;
+
+ case nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION:
+ if (!suggestedVersion.IsEmpty()) {
+ SetPrefValueForDriverVersion(suggestedVersion);
+ } else {
+ RemovePrefForDriverVersion();
+ }
+ [[fallthrough]];
+
+ case nsIGfxInfo::FEATURE_BLOCKED_MISMATCHED_VERSION:
+ case nsIGfxInfo::FEATURE_BLOCKED_DEVICE:
+ case nsIGfxInfo::FEATURE_DISCOURAGED:
+ case nsIGfxInfo::FEATURE_BLOCKED_OS_VERSION:
+ case nsIGfxInfo::FEATURE_BLOCKED_PLATFORM_TEST:
+ SetPrefValueForFeature(feature, status, failureId);
+ break;
+ }
+ }
+}
+
+NS_IMETHODIMP_(void)
+GfxInfoBase::LogFailure(const nsACString& failure) {
+ // gfxCriticalError has a mutex lock of its own, so we may not actually
+ // need this lock. ::GetFailures() accesses the data but the LogForwarder
+ // will not return the copy of the logs unless it can get the same lock
+ // that gfxCriticalError uses. Still, that is so much of an implementation
+ // detail that it's nicer to just add an extra lock here and in
+ // ::GetFailures()
+ MutexAutoLock lock(mMutex);
+
+ // By default, gfxCriticalError asserts; make it not assert in this case.
+ gfxCriticalError(CriticalLog::DefaultOptions(false))
+ << "(LF) " << failure.BeginReading();
+}
+
+NS_IMETHODIMP GfxInfoBase::GetFailures(nsTArray<int32_t>& indices,
+ nsTArray<nsCString>& failures) {
+ MutexAutoLock lock(mMutex);
+
+ LogForwarder* logForwarder = Factory::GetLogForwarder();
+ if (!logForwarder) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // There are two string copies in this method, starting with this one. We are
+ // assuming this is not a big deal, as the size of the array should be small
+ // and the strings in it should be small as well (the error messages in the
+ // code.) The second copy happens with the AppendElement() calls.
+ // Technically, we don't need the mutex lock after the StringVectorCopy()
+ // call.
+ LoggingRecord loggedStrings = logForwarder->LoggingRecordCopy();
+ LoggingRecord::const_iterator it;
+ for (it = loggedStrings.begin(); it != loggedStrings.end(); ++it) {
+ failures.AppendElement(nsDependentCSubstring(std::get<1>(*it).c_str(),
+ std::get<1>(*it).size()));
+ indices.AppendElement(std::get<0>(*it));
+ }
+
+ return NS_OK;
+}
+
+nsTArray<GfxInfoCollectorBase*>* sCollectors;
+
+static void InitCollectors() {
+ if (!sCollectors) sCollectors = new nsTArray<GfxInfoCollectorBase*>;
+}
+
+nsresult GfxInfoBase::GetInfo(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ InitCollectors();
+ InfoObject obj(aCx);
+
+ for (uint32_t i = 0; i < sCollectors->Length(); i++) {
+ (*sCollectors)[i]->GetInfo(obj);
+ }
+
+ // Some example property definitions
+ // obj.DefineProperty("wordCacheSize", gfxTextRunWordCache::Count());
+ // obj.DefineProperty("renderer", mRendererIDsString);
+ // obj.DefineProperty("five", 5);
+
+ if (!obj.mOk) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aResult.setObject(*obj.mObj);
+ return NS_OK;
+}
+
+nsAutoCString gBaseAppVersion;
+
+const nsCString& GfxInfoBase::GetApplicationVersion() {
+ static bool versionInitialized = false;
+ if (!versionInitialized) {
+ // If we fail to get the version, we will not try again.
+ versionInitialized = true;
+
+ // Get the version from xpcom/system/nsIXULAppInfo.idl
+ nsCOMPtr<nsIXULAppInfo> app = do_GetService("@mozilla.org/xre/app-info;1");
+ if (app) {
+ app->GetVersion(gBaseAppVersion);
+ }
+ }
+ return gBaseAppVersion;
+}
+
+/* static */ bool GfxInfoBase::OnlyAllowFeatureOnKnownConfig(int32_t aFeature) {
+ switch (aFeature) {
+ // The GPU process doesn't need hardware acceleration and can run on
+ // devices that we normally block from not being on our whitelist.
+ case nsIGfxInfo::FEATURE_GPU_PROCESS:
+ return kIsAndroid;
+ // We can mostly assume that ANGLE will work
+ case nsIGfxInfo::FEATURE_DIRECT3D_11_ANGLE:
+ // Remote WebGL is needed for Win32k Lockdown, so it should be enabled
+ // regardless of HW support or not
+ case nsIGfxInfo::FEATURE_ALLOW_WEBGL_OUT_OF_PROCESS:
+ // Backdrop filter should generally work, especially if we fall back to
+ // Software WebRender because of an unknown vendor.
+ case nsIGfxInfo::FEATURE_BACKDROP_FILTER:
+ return false;
+ default:
+ return true;
+ }
+}
+
+void GfxInfoBase::AddCollector(GfxInfoCollectorBase* collector) {
+ InitCollectors();
+ sCollectors->AppendElement(collector);
+}
+
+void GfxInfoBase::RemoveCollector(GfxInfoCollectorBase* collector) {
+ InitCollectors();
+ for (uint32_t i = 0; i < sCollectors->Length(); i++) {
+ if ((*sCollectors)[i] == collector) {
+ sCollectors->RemoveElementAt(i);
+ break;
+ }
+ }
+ if (sCollectors->IsEmpty()) {
+ delete sCollectors;
+ sCollectors = nullptr;
+ }
+}
+
+static void AppendMonitor(JSContext* aCx, widget::Screen& aScreen,
+ JS::Handle<JSObject*> aOutArray, int32_t aIndex) {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+
+ auto screenSize = aScreen.GetRect().Size();
+
+ JS::Rooted<JS::Value> screenWidth(aCx, JS::Int32Value(screenSize.width));
+ JS_SetProperty(aCx, obj, "screenWidth", screenWidth);
+
+ JS::Rooted<JS::Value> screenHeight(aCx, JS::Int32Value(screenSize.height));
+ JS_SetProperty(aCx, obj, "screenHeight", screenHeight);
+
+ // XXX Just preserving behavior since this is exposed to telemetry, but we
+ // could consider including this everywhere.
+#ifdef XP_MACOSX
+ JS::Rooted<JS::Value> scale(
+ aCx, JS::NumberValue(aScreen.GetContentsScaleFactor()));
+ JS_SetProperty(aCx, obj, "scale", scale);
+#endif
+
+#ifdef XP_WIN
+ JS::Rooted<JS::Value> refreshRate(aCx,
+ JS::Int32Value(aScreen.GetRefreshRate()));
+ JS_SetProperty(aCx, obj, "refreshRate", refreshRate);
+
+ JS::Rooted<JS::Value> pseudoDisplay(
+ aCx, JS::BooleanValue(aScreen.GetIsPseudoDisplay()));
+ JS_SetProperty(aCx, obj, "pseudoDisplay", pseudoDisplay);
+#endif
+
+ JS::Rooted<JS::Value> element(aCx, JS::ObjectValue(*obj));
+ JS_SetElement(aCx, aOutArray, aIndex, element);
+}
+
+nsresult GfxInfoBase::FindMonitors(JSContext* aCx,
+ JS::Handle<JSObject*> aOutArray) {
+ int32_t index = 0;
+ auto& sm = ScreenManager::GetSingleton();
+ for (auto& screen : sm.CurrentScreenList()) {
+ AppendMonitor(aCx, *screen, aOutArray, index++);
+ }
+
+ if (index == 0) {
+ // Ensure we return at least one monitor, this is needed for xpcshell.
+ RefPtr<Screen> screen = sm.GetPrimaryScreen();
+ AppendMonitor(aCx, *screen, aOutArray, index++);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetMonitors(JSContext* aCx, JS::MutableHandle<JS::Value> aResult) {
+ JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, 0));
+
+ nsresult rv = FindMonitors(aCx, array);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aResult.setObject(*array);
+ return NS_OK;
+}
+
+static inline bool SetJSPropertyString(JSContext* aCx,
+ JS::Handle<JSObject*> aObj,
+ const char* aProp, const char* aString) {
+ JS::Rooted<JSString*> str(aCx, JS_NewStringCopyZ(aCx, aString));
+ if (!str) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> val(aCx, JS::StringValue(str));
+ return JS_SetProperty(aCx, aObj, aProp, val);
+}
+
+template <typename T>
+static inline bool AppendJSElement(JSContext* aCx, JS::Handle<JSObject*> aObj,
+ const T& aValue) {
+ uint32_t index;
+ if (!JS::GetArrayLength(aCx, aObj, &index)) {
+ return false;
+ }
+ return JS_SetElement(aCx, aObj, index, aValue);
+}
+
+nsresult GfxInfoBase::GetFeatures(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aOut) {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ aOut.setObject(*obj);
+
+ layers::LayersBackend backend =
+ gfxPlatform::Initialized()
+ ? gfxPlatform::GetPlatform()->GetCompositorBackend()
+ : layers::LayersBackend::LAYERS_NONE;
+ const char* backendName = layers::GetLayersBackendName(backend);
+ SetJSPropertyString(aCx, obj, "compositor", backendName);
+
+ // If graphics isn't initialized yet, just stop now.
+ if (!gfxPlatform::Initialized()) {
+ return NS_OK;
+ }
+
+ DescribeFeatures(aCx, obj);
+ return NS_OK;
+}
+
+nsresult GfxInfoBase::GetFeatureLog(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aOut) {
+ JS::Rooted<JSObject*> containerObj(aCx, JS_NewPlainObject(aCx));
+ if (!containerObj) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ aOut.setObject(*containerObj);
+
+ JS::Rooted<JSObject*> featureArray(aCx, JS::NewArrayObject(aCx, 0));
+ if (!featureArray) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Collect features.
+ gfxConfig::ForEachFeature([&](const char* aName, const char* aDescription,
+ FeatureState& aFeature) -> void {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return;
+ }
+ if (!SetJSPropertyString(aCx, obj, "name", aName) ||
+ !SetJSPropertyString(aCx, obj, "description", aDescription) ||
+ !SetJSPropertyString(aCx, obj, "status",
+ FeatureStatusToString(aFeature.GetValue()))) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> log(aCx);
+ if (!BuildFeatureStateLog(aCx, aFeature, &log)) {
+ return;
+ }
+ if (!JS_SetProperty(aCx, obj, "log", log)) {
+ return;
+ }
+
+ if (!AppendJSElement(aCx, featureArray, obj)) {
+ return;
+ }
+ });
+
+ JS::Rooted<JSObject*> fallbackArray(aCx, JS::NewArrayObject(aCx, 0));
+ if (!fallbackArray) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Collect fallbacks.
+ gfxConfig::ForEachFallback(
+ [&](const char* aName, const char* aMessage) -> void {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return;
+ }
+
+ if (!SetJSPropertyString(aCx, obj, "name", aName) ||
+ !SetJSPropertyString(aCx, obj, "message", aMessage)) {
+ return;
+ }
+
+ if (!AppendJSElement(aCx, fallbackArray, obj)) {
+ return;
+ }
+ });
+
+ JS::Rooted<JS::Value> val(aCx);
+
+ val = JS::ObjectValue(*featureArray);
+ JS_SetProperty(aCx, containerObj, "features", val);
+
+ val = JS::ObjectValue(*fallbackArray);
+ JS_SetProperty(aCx, containerObj, "fallbacks", val);
+
+ return NS_OK;
+}
+
+bool GfxInfoBase::BuildFeatureStateLog(JSContext* aCx,
+ const FeatureState& aFeature,
+ JS::MutableHandle<JS::Value> aOut) {
+ JS::Rooted<JSObject*> log(aCx, JS::NewArrayObject(aCx, 0));
+ if (!log) {
+ return false;
+ }
+ aOut.setObject(*log);
+
+ aFeature.ForEachStatusChange([&](const char* aType, FeatureStatus aStatus,
+ const char* aMessage,
+ const nsCString& aFailureId) -> void {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return;
+ }
+
+ if (!SetJSPropertyString(aCx, obj, "type", aType) ||
+ !SetJSPropertyString(aCx, obj, "status",
+ FeatureStatusToString(aStatus)) ||
+ (!aFailureId.IsEmpty() &&
+ !SetJSPropertyString(aCx, obj, "failureId", aFailureId.get())) ||
+ (aMessage && !SetJSPropertyString(aCx, obj, "message", aMessage))) {
+ return;
+ }
+
+ if (!AppendJSElement(aCx, log, obj)) {
+ return;
+ }
+ });
+
+ return true;
+}
+
+void GfxInfoBase::DescribeFeatures(JSContext* aCx, JS::Handle<JSObject*> aObj) {
+ JS::Rooted<JSObject*> obj(aCx);
+
+ gfx::FeatureState& hwCompositing =
+ gfxConfig::GetFeature(gfx::Feature::HW_COMPOSITING);
+ InitFeatureObject(aCx, aObj, "hwCompositing", hwCompositing, &obj);
+
+ gfx::FeatureState& gpuProcess =
+ gfxConfig::GetFeature(gfx::Feature::GPU_PROCESS);
+ InitFeatureObject(aCx, aObj, "gpuProcess", gpuProcess, &obj);
+
+ gfx::FeatureState& webrender = gfxConfig::GetFeature(gfx::Feature::WEBRENDER);
+ InitFeatureObject(aCx, aObj, "webrender", webrender, &obj);
+
+ gfx::FeatureState& wrCompositor =
+ gfxConfig::GetFeature(gfx::Feature::WEBRENDER_COMPOSITOR);
+ InitFeatureObject(aCx, aObj, "wrCompositor", wrCompositor, &obj);
+
+ gfx::FeatureState& openglCompositing =
+ gfxConfig::GetFeature(gfx::Feature::OPENGL_COMPOSITING);
+ InitFeatureObject(aCx, aObj, "openglCompositing", openglCompositing, &obj);
+
+ gfx::FeatureState& omtp = gfxConfig::GetFeature(gfx::Feature::OMTP);
+ InitFeatureObject(aCx, aObj, "omtp", omtp, &obj);
+}
+
+bool GfxInfoBase::InitFeatureObject(JSContext* aCx,
+ JS::Handle<JSObject*> aContainer,
+ const char* aName,
+ mozilla::gfx::FeatureState& aFeatureState,
+ JS::MutableHandle<JSObject*> aOutObj) {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return false;
+ }
+
+ nsCString status = aFeatureState.GetStatusAndFailureIdString();
+
+ JS::Rooted<JSString*> str(aCx, JS_NewStringCopyZ(aCx, status.get()));
+ JS::Rooted<JS::Value> val(aCx, JS::StringValue(str));
+ JS_SetProperty(aCx, obj, "status", val);
+
+ // Add the feature object to the container.
+ {
+ JS::Rooted<JS::Value> val(aCx, JS::ObjectValue(*obj));
+ JS_SetProperty(aCx, aContainer, aName, val);
+ }
+
+ aOutObj.set(obj);
+ return true;
+}
+
+nsresult GfxInfoBase::GetActiveCrashGuards(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aOut) {
+ JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, 0));
+ if (!array) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ aOut.setObject(*array);
+
+ DriverCrashGuard::ForEachActiveCrashGuard(
+ [&](const char* aName, const char* aPrefName) -> void {
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return;
+ }
+ if (!SetJSPropertyString(aCx, obj, "type", aName)) {
+ return;
+ }
+ if (!SetJSPropertyString(aCx, obj, "prefName", aPrefName)) {
+ return;
+ }
+ if (!AppendJSElement(aCx, array, obj)) {
+ return;
+ }
+ });
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetTargetFrameRate(uint32_t* aTargetFrameRate) {
+ *aTargetFrameRate = gfxPlatform::TargetFrameRate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetCodecSupportInfo(nsACString& aCodecSupportInfo) {
+ aCodecSupportInfo.Assign(gfx::gfxVars::CodecSupportInfo());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetIsHeadless(bool* aIsHeadless) {
+ *aIsHeadless = gfxPlatform::IsHeadless();
+ return NS_OK;
+}
+
+#if defined(MOZ_WIDGET_ANDROID)
+
+const char* chromebookProductList[] = {
+ "asuka", "asurada", "atlas", "auron", "banjo", "banon",
+ "bob", "brask", "brya", "buddy", "butterfly", "candy",
+ "caroline", "cave", "celes", "chell", "cherry", "clapper",
+ "coral", "corsola", "cyan", "daisy", "dedede", "drallion",
+ "edgar", "elm", "enguarde", "eve", "expresso", "falco",
+ "fizz", "gandof", "glimmer", "gnawty", "grunt", "guado",
+ "guybrush", "hana", "hatch", "heli", "jacuzzi", "kalista",
+ "kefka", "kevin", "kip", "kukui", "lars", "leon",
+ "link", "lulu", "lumpy", "mccloud", "monroe", "nami",
+ "nautilus", "ninja", "nissa", "nocturne", "nyan", "octopus",
+ "orco", "panther", "parrot", "peach", "peppy", "puff",
+ "pyro", "quawks", "rammus", "reef", "reks", "relm",
+ "rikku", "samus", "sand", "sarien", "scarlet", "sentry",
+ "setzer", "skyrim", "snappy", "soraka", "squawks", "staryu",
+ "stout", "strongbad", "stumpy", "sumo", "swanky", "terra",
+ "tidus", "tricky", "trogdor", "ultima", "veyron", "volteer",
+ "winky", "wizpig", "wolf", "x86", "zako", "zork"};
+
+bool ProductIsChromebook(nsCString product) {
+ size_t result;
+ return BinarySearchIf(
+ chromebookProductList, 0, ArrayLength(chromebookProductList),
+ [&](const char* const aValue) -> int {
+ return strcmp(product.get(), aValue);
+ },
+ &result);
+}
+#endif
+
+using Device = nsIGfxInfo::FontVisibilityDeviceDetermination;
+static StaticAutoPtr<std::pair<Device, nsString>> ret;
+
+std::pair<Device, nsString>* GfxInfoBase::GetFontVisibilityDeterminationPair() {
+ if(!ret) {
+ ret = new std::pair<Device, nsString>();
+ ret->first = Device::Unassigned;
+ ret->second = u""_ns;
+ ClearOnShutdown(&ret);
+ }
+
+ if (ret->first != Device::Unassigned) {
+ return ret;
+ }
+
+#if defined(MOZ_WIDGET_ANDROID)
+ auto androidReleaseVersion = strtol(
+ java::sdk::Build::VERSION::RELEASE()->ToCString().get(), nullptr, 10);
+
+ auto androidManufacturer = java::sdk::Build::MANUFACTURER()->ToCString();
+ nsContentUtils::ASCIIToLower(androidManufacturer);
+
+ auto androidBrand = java::sdk::Build::BRAND()->ToCString();
+ nsContentUtils::ASCIIToLower(androidBrand);
+
+ auto androidModel = java::sdk::Build::MODEL()->ToCString();
+ nsContentUtils::ASCIIToLower(androidModel);
+
+ auto androidProduct = java::sdk::Build::PRODUCT()->ToCString();
+ nsContentUtils::ASCIIToLower(androidProduct);
+
+ auto androidProductIsChromebook = ProductIsChromebook(androidProduct);
+
+ if (androidReleaseVersion < 4 || androidReleaseVersion > 20) {
+ // Something is screwy, oh well.
+ ret->second.AppendASCII("Unknown Release Version - ");
+ ret->first = Device::Android_Unknown_Release_Version;
+ } else if (androidReleaseVersion <= 8) {
+ ret->second.AppendASCII("Android <9 - ");
+ ret->first = Device::Android_sub_9;
+ } else if (androidReleaseVersion <= 11) {
+ ret->second.AppendASCII("Android 9-11 - ");
+ ret->first = Device::Android_9_11;
+ } else if (androidReleaseVersion > 11) {
+ ret->second.AppendASCII("Android 12+ - ");
+ ret->first = Device::Android_12_plus;
+ } else {
+ MOZ_CRASH_UNSAFE_PRINTF(
+ "Somehow wound up in GetFontVisibilityDeterminationPair with a release "
+ "version of %li",
+ androidReleaseVersion);
+ }
+
+ if (androidManufacturer == "google" && androidModel == androidProduct &&
+ androidProductIsChromebook) {
+ // Chromebook font set coming later
+ ret->second.AppendASCII("Chromebook - ");
+ ret->first = Device::Android_Chromebook;
+ }
+ if (androidBrand == "amazon") {
+ // Amazon Fire font set coming later
+ ret->second.AppendASCII("Amazon - ");
+ ret->first = Device::Android_Amazon;
+ }
+ if (androidBrand == "peloton") {
+ // We don't know how to categorize fonts on this system
+ ret->second.AppendASCII("Peloton - ");
+ ret->first = Device::Android_Unknown_Peloton;
+ }
+ if (androidProduct == "vbox86p") {
+ ret->second.AppendASCII("vbox - ");
+ // We can't categorize fonts when running in an emulator on a Desktop
+ ret->first = Device::Android_Unknown_vbox;
+ }
+ if (androidModel.Find("mitv"_ns) != kNotFound && androidBrand == "xiaomi") {
+ // We don't know how to categorize fonts on this system
+ ret->second.AppendASCII("mitv - ");
+ ret->first = Device::Android_Unknown_mitv;
+ }
+
+ ret->second.AppendPrintf(
+ "release_version_str=%s, release_version=%li",
+ java::sdk::Build::VERSION::RELEASE()->ToCString().get(),
+ androidReleaseVersion);
+ ret->second.AppendPrintf(
+ ", manufacturer=%s, brand=%s, model=%s, product=%s, chromebook=%s",
+ androidManufacturer.get(), androidBrand.get(), androidModel.get(),
+ androidProduct.get(), androidProductIsChromebook ? "yes" : "no");
+
+#elif defined(XP_LINUX)
+ ret->first = Device::Linux_Unknown;
+
+ long versionMajor = 0;
+ FILE* fp = fopen("/etc/os-release", "r");
+ if (fp) {
+ char buf[512];
+ while (fgets(buf, sizeof(buf), fp)) {
+ if (strncmp(buf, "VERSION_ID=\"", 12) == 0) {
+ ret->second.AppendPrintf("VERSION_ID=%.11s", buf + 11);
+ versionMajor = strtol(buf + 12, nullptr, 10);
+ if (ret->first != Device::Linux_Unknown) {
+ break;
+ }
+ }
+
+ if (strncmp(buf, "ID=", 3) == 0) {
+ ret->second.AppendPrintf("ID=%.6s", buf + 3);
+ if (strncmp(buf + 3, "ubuntu", 6) == 0) {
+ ret->first = Device::Linux_Ubuntu_any;
+ } else if (strncmp(buf + 3, "fedora", 6) == 0) {
+ ret->first = Device::Linux_Fedora_any;
+ }
+
+ if (versionMajor) {
+ break;
+ }
+ }
+ }
+ fclose(fp);
+ }
+ if (ret->first == Device::Linux_Ubuntu_any) {
+ if (versionMajor == 20) {
+ ret->first = Device::Linux_Ubuntu_20;
+ ret->second.Insert(u"Ubuntu 20 - ", 0);
+ } else if (versionMajor == 22) {
+ ret->first = Device::Linux_Ubuntu_22;
+ ret->second.Insert(u"Ubuntu 22 - ", 0);
+ } else {
+ ret->second.Insert(u"Ubuntu Unknown - ", 0);
+ }
+ } else if (ret->first == Device::Linux_Fedora_any) {
+ if (versionMajor == 38) {
+ ret->first = Device::Linux_Fedora_38;
+ ret->second.Insert(u"Fedora 38 - ", 0);
+ } else if (versionMajor == 39) {
+ ret->first = Device::Linux_Fedora_39;
+ ret->second.Insert(u"Fedora 39 - ", 0);
+ } else {
+ ret->second.Insert(u"Fedora Unknown - ", 0);
+ }
+ } else {
+ ret->second.Insert(u"Linux Unknown - ", 0);
+ }
+
+#elif defined(XP_MACOSX)
+ ret->first = Device::MacOS_Platform;
+ ret->second.AppendASCII("macOS Platform");
+#elif defined(XP_WIN)
+ ret->first = Device::Windows_Platform;
+ ret->second.AppendASCII("Windows Platform");
+#else
+ ret->first = Device::Unknown_Platform;
+ ret->second.AppendASCII("Unknown Platform");
+#endif
+
+ return ret;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetFontVisibilityDetermination(
+ Device* aFontVisibilityDetermination) {
+ auto ret = GetFontVisibilityDeterminationPair();
+
+ *aFontVisibilityDetermination = ret->first;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetFontVisibilityDeterminationStr(
+ nsAString& aFontVisibilityDeterminationStr) {
+ auto ret = GetFontVisibilityDeterminationPair();
+ aFontVisibilityDeterminationStr.Assign(ret->second);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetContentBackend(nsAString& aContentBackend) {
+ BackendType backend = gfxPlatform::GetPlatform()->GetDefaultContentBackend();
+ nsString outStr;
+
+ switch (backend) {
+ case BackendType::DIRECT2D1_1: {
+ outStr.AppendPrintf("Direct2D 1.1");
+ break;
+ }
+ case BackendType::SKIA: {
+ outStr.AppendPrintf("Skia");
+ break;
+ }
+ case BackendType::CAIRO: {
+ outStr.AppendPrintf("Cairo");
+ break;
+ }
+ default:
+ return NS_ERROR_FAILURE;
+ }
+
+ aContentBackend.Assign(outStr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetAzureCanvasBackend(nsAString& aBackend) {
+ CopyASCIItoUTF16(mozilla::MakeStringSpan(
+ gfxPlatform::GetPlatform()->GetAzureCanvasBackend()),
+ aBackend);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetAzureContentBackend(nsAString& aBackend) {
+ CopyASCIItoUTF16(mozilla::MakeStringSpan(
+ gfxPlatform::GetPlatform()->GetAzureContentBackend()),
+ aBackend);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::GetUsingGPUProcess(bool* aOutValue) {
+ GPUProcessManager* gpu = GPUProcessManager::Get();
+ if (!gpu) {
+ // Not supported in content processes.
+ return NS_ERROR_FAILURE;
+ }
+
+ *aOutValue = !!gpu->GetGPUChild();
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(int32_t)
+GfxInfoBase::GetMaxRefreshRate(bool* aMixed) {
+ if (aMixed) {
+ *aMixed = false;
+ }
+
+ int32_t maxRefreshRate = 0;
+ for (auto& screen : ScreenManager::GetSingleton().CurrentScreenList()) {
+ int32_t refreshRate = screen->GetRefreshRate();
+ if (aMixed && maxRefreshRate > 0 && maxRefreshRate != refreshRate) {
+ *aMixed = true;
+ }
+ maxRefreshRate = std::max(maxRefreshRate, refreshRate);
+ }
+
+ return maxRefreshRate > 0 ? maxRefreshRate : -1;
+}
+
+NS_IMETHODIMP
+GfxInfoBase::ControlGPUProcessForXPCShell(bool aEnable, bool* _retval) {
+ gfxPlatform::GetPlatform();
+
+ GPUProcessManager* gpm = GPUProcessManager::Get();
+ if (aEnable) {
+ if (!gfxConfig::IsEnabled(gfx::Feature::GPU_PROCESS)) {
+ gfxConfig::UserForceEnable(gfx::Feature::GPU_PROCESS, "xpcshell-test");
+ }
+ DebugOnly<nsresult> rv = gpm->EnsureGPUReady();
+ MOZ_ASSERT(rv != NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+ } else {
+ gfxConfig::UserDisable(gfx::Feature::GPU_PROCESS, "xpcshell-test");
+ gpm->KillProcess();
+ }
+
+ *_retval = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfoBase::KillGPUProcessForTests() {
+ GPUProcessManager* gpm = GPUProcessManager::Get();
+ if (!gpm) {
+ // gfxPlatform has not been initialized.
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ gpm->KillProcess();
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfoBase::CrashGPUProcessForTests() {
+ GPUProcessManager* gpm = GPUProcessManager::Get();
+ if (!gpm) {
+ // gfxPlatform has not been initialized.
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ gpm->CrashProcess();
+ return NS_OK;
+}
+
+GfxInfoCollectorBase::GfxInfoCollectorBase() {
+ GfxInfoBase::AddCollector(this);
+}
+
+GfxInfoCollectorBase::~GfxInfoCollectorBase() {
+ GfxInfoBase::RemoveCollector(this);
+}
diff --git a/widget/GfxInfoBase.h b/widget/GfxInfoBase.h
new file mode 100644
index 0000000000..4d4b1a2def
--- /dev/null
+++ b/widget/GfxInfoBase.h
@@ -0,0 +1,182 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_GfxInfoBase_h__
+#define __mozilla_widget_GfxInfoBase_h__
+
+#include "GfxDriverInfo.h"
+#include "GfxInfoCollector.h"
+#include "gfxFeature.h"
+#include "gfxTelemetry.h"
+#include "js/Value.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/gfx/GraphicsMessages.h"
+#include "nsCOMPtr.h"
+#include "nsIGfxInfo.h"
+#include "nsIGfxInfoDebug.h"
+#include "nsIObserver.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace widget {
+
+class GfxInfoBase : public nsIGfxInfo,
+ public nsIObserver,
+ public nsSupportsWeakReference
+#ifdef DEBUG
+ ,
+ public nsIGfxInfoDebug
+#endif
+{
+ public:
+ GfxInfoBase();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ // We only declare a subset of the nsIGfxInfo interface. It's up to derived
+ // classes to implement the rest of the interface.
+ // Derived classes need to use
+ // using GfxInfoBase::GetFeatureStatus;
+ // using GfxInfoBase::GetFeatureSuggestedDriverVersion;
+ // to import the relevant methods into their namespace.
+ NS_IMETHOD GetFeatureStatus(int32_t aFeature, nsACString& aFailureId,
+ int32_t* _retval) override;
+ NS_IMETHOD GetFeatureSuggestedDriverVersion(int32_t aFeature,
+ nsAString& _retval) override;
+
+ NS_IMETHOD GetMonitors(JSContext* cx,
+ JS::MutableHandle<JS::Value> _retval) override;
+ NS_IMETHOD GetFailures(nsTArray<int32_t>& indices,
+ nsTArray<nsCString>& failures) override;
+ NS_IMETHOD_(void) LogFailure(const nsACString& failure) override;
+ NS_IMETHOD GetInfo(JSContext*, JS::MutableHandle<JS::Value>) override;
+ NS_IMETHOD GetFeatures(JSContext*, JS::MutableHandle<JS::Value>) override;
+ NS_IMETHOD GetFeatureLog(JSContext*, JS::MutableHandle<JS::Value>) override;
+ NS_IMETHOD GetActiveCrashGuards(JSContext*,
+ JS::MutableHandle<JS::Value>) override;
+ NS_IMETHOD GetFontVisibilityDetermination(
+ nsIGfxInfo::FontVisibilityDeviceDetermination*
+ aFontVisibilityDetermination) override;
+ NS_IMETHOD GetFontVisibilityDeterminationStr(
+ nsAString& aFontVisibilityDeterminationStr) override;
+ NS_IMETHOD GetContentBackend(nsAString& aContentBackend) override;
+ NS_IMETHOD GetAzureCanvasBackend(nsAString& aBackend) override;
+ NS_IMETHOD GetAzureContentBackend(nsAString& aBackend) override;
+ NS_IMETHOD GetUsingGPUProcess(bool* aOutValue) override;
+ NS_IMETHOD GetIsHeadless(bool* aIsHeadless) override;
+ NS_IMETHOD GetTargetFrameRate(uint32_t* aTargetFrameRate) override;
+ NS_IMETHOD GetCodecSupportInfo(nsACString& aCodecSupportInfo) override;
+
+ // Non-XPCOM method to get IPC data:
+ nsTArray<mozilla::gfx::GfxInfoFeatureStatus> GetAllFeatures();
+
+ // Initialization function. If you override this, you must call this class's
+ // version of Init first.
+ // We need Init to be called separately from the constructor so we can
+ // register as an observer after all derived classes have been constructed
+ // and we know we have a non-zero refcount.
+ // Ideally, Init() would be void-return, but the rules of
+ // NS_GENERIC_FACTORY_CONSTRUCTOR_INIT require it be nsresult return.
+ virtual nsresult Init();
+
+ NS_IMETHOD_(void) GetData() override;
+ NS_IMETHOD_(int32_t) GetMaxRefreshRate(bool* aMixed) override;
+
+ static void AddCollector(GfxInfoCollectorBase* collector);
+ static void RemoveCollector(GfxInfoCollectorBase* collector);
+
+ static nsTArray<GfxDriverInfo>* sDriverInfo;
+ static StaticAutoPtr<nsTArray<mozilla::gfx::GfxInfoFeatureStatus>>
+ sFeatureStatus;
+ static bool sDriverInfoObserverInitialized;
+ static bool sShutdownOccurred;
+
+ virtual nsString Model() { return u""_ns; }
+ virtual nsString Hardware() { return u""_ns; }
+ virtual nsString Product() { return u""_ns; }
+ virtual nsString Manufacturer() { return u""_ns; }
+ virtual uint32_t OperatingSystemVersion() { return 0; }
+ virtual uint32_t OperatingSystemBuild() { return 0; }
+
+ // Convenience to get the application version
+ static const nsCString& GetApplicationVersion();
+
+ virtual nsresult FindMonitors(JSContext* cx, JS::Handle<JSObject*> array);
+
+ static void SetFeatureStatus(
+ nsTArray<mozilla::gfx::GfxInfoFeatureStatus>&& aFS);
+
+ static bool OnlyAllowFeatureOnKnownConfig(int32_t aFeature);
+
+ protected:
+ virtual ~GfxInfoBase();
+
+ virtual OperatingSystem GetOperatingSystem() = 0;
+
+ virtual nsresult GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS = nullptr);
+
+ // Gets the driver info table. Used by GfxInfoBase to check for general cases
+ // (while subclasses check for more specific ones).
+ virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() = 0;
+
+ virtual void DescribeFeatures(JSContext* aCx, JS::Handle<JSObject*> obj);
+
+ virtual bool DoesWindowProtocolMatch(
+ const nsAString& aBlocklistWindowProtocol,
+ const nsAString& aWindowProtocol);
+
+ bool DoesVendorMatch(const nsAString& aBlocklistVendor,
+ const nsAString& aAdapterVendor);
+
+ virtual bool DoesDriverVendorMatch(const nsAString& aBlocklistVendor,
+ const nsAString& aDriverVendor);
+
+ bool InitFeatureObject(JSContext* aCx, JS::Handle<JSObject*> aContainer,
+ const char* aName,
+ mozilla::gfx::FeatureState& aFeatureState,
+ JS::MutableHandle<JSObject*> aOutObj);
+
+ NS_IMETHOD ControlGPUProcessForXPCShell(bool aEnable, bool* _retval) override;
+
+ NS_IMETHOD KillGPUProcessForTests() override;
+ NS_IMETHOD CrashGPUProcessForTests() override;
+
+ // Total number of pixels for all detected screens at startup.
+ int64_t mScreenPixels;
+
+ private:
+ virtual int32_t FindBlocklistedDeviceInList(
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsAString& aSuggestedVersion,
+ int32_t aFeature, nsACString& aFailureId, OperatingSystem os,
+ bool aForAllowing);
+
+ std::pair<nsIGfxInfo::FontVisibilityDeviceDetermination, nsString>*
+ GetFontVisibilityDeterminationPair();
+
+ bool IsFeatureAllowlisted(int32_t aFeature) const;
+
+ void EvaluateDownloadedBlocklist(nsTArray<GfxDriverInfo>& aDriverInfo);
+
+ bool BuildFeatureStateLog(JSContext* aCx, const gfx::FeatureState& aFeature,
+ JS::MutableHandle<JS::Value> aOut);
+
+ Mutex mMutex MOZ_UNANNOTATED;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_GfxInfoBase_h__ */
diff --git a/widget/GfxInfoCollector.cpp b/widget/GfxInfoCollector.cpp
new file mode 100644
index 0000000000..6ad8494586
--- /dev/null
+++ b/widget/GfxInfoCollector.cpp
@@ -0,0 +1,44 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "GfxInfoCollector.h"
+#include "jsapi.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+#include "nsString.h"
+
+using namespace mozilla;
+using namespace widget;
+
+void InfoObject::DefineProperty(const char* name, int value) {
+ if (!mOk) return;
+
+ mOk = JS_DefineProperty(mCx, mObj, name, value, JSPROP_ENUMERATE);
+}
+
+void InfoObject::DefineProperty(const char* name, const nsAString& value) {
+ if (!mOk) return;
+
+ const nsString& flat = PromiseFlatString(value);
+ JS::Rooted<JSString*> string(
+ mCx, JS_NewUCStringCopyN(mCx, static_cast<const char16_t*>(flat.get()),
+ flat.Length()));
+ if (!string) mOk = false;
+
+ if (!mOk) return;
+
+ mOk = JS_DefineProperty(mCx, mObj, name, string, JSPROP_ENUMERATE);
+}
+
+void InfoObject::DefineProperty(const char* name, const char* value) {
+ nsAutoString string = NS_ConvertASCIItoUTF16(value);
+ DefineProperty(name, string);
+}
+
+InfoObject::InfoObject(JSContext* aCx) : mCx(aCx), mObj(aCx), mOk(true) {
+ mObj = JS_NewPlainObject(aCx);
+ if (!mObj) mOk = false;
+}
diff --git a/widget/GfxInfoCollector.h b/widget/GfxInfoCollector.h
new file mode 100644
index 0000000000..7108a7fe2d
--- /dev/null
+++ b/widget/GfxInfoCollector.h
@@ -0,0 +1,88 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_GfxInfoCollector_h__
+#define __mozilla_widget_GfxInfoCollector_h__
+
+#include "mozilla/Attributes.h"
+#include "nsStringFwd.h"
+#include "js/RootingAPI.h"
+
+namespace mozilla {
+namespace widget {
+
+/* this is handy wrapper around JSAPI to make it more pleasant to use.
+ * We collect the JSAPI errors and so that callers don't need to */
+class MOZ_STACK_CLASS InfoObject {
+ friend class GfxInfoBase;
+
+ public:
+ void DefineProperty(const char* name, int value);
+ void DefineProperty(const char* name, const nsAString& value);
+ void DefineProperty(const char* name, const char* value);
+
+ private:
+ // We need to ensure that this object lives on the stack so that GC sees it
+ // properly
+ explicit InfoObject(JSContext* aCx);
+ InfoObject(InfoObject&);
+
+ JSContext* mCx;
+ JS::Rooted<JSObject*> mObj;
+ bool mOk;
+};
+
+/*
+
+ Here's an example usage:
+
+ class Foo {
+ Foo::Foo() : mInfoCollector(this, &Foo::GetAweseomeness) {}
+
+ void GetAwesomeness(InfoObject &obj) {
+ obj.DefineProperty("awesome", mAwesome);
+ }
+
+ int mAwesome;
+
+ GfxInfoCollector<Foo> mInfoCollector;
+ }
+
+ This will define a property on the object
+ returned from calling getInfo() on a
+ GfxInfo object. e.g.
+
+ gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ info = gfxInfo.getInfo();
+ if (info.awesome)
+ alert(info.awesome);
+
+*/
+
+class GfxInfoCollectorBase {
+ public:
+ GfxInfoCollectorBase();
+ virtual void GetInfo(InfoObject& obj) = 0;
+ virtual ~GfxInfoCollectorBase();
+};
+
+template <class T>
+class GfxInfoCollector : public GfxInfoCollectorBase {
+ public:
+ GfxInfoCollector(T* aPointer, void (T::*aFunc)(InfoObject& obj))
+ : mPointer(aPointer), mFunc(aFunc) {}
+ virtual void GetInfo(InfoObject& obj) override { (mPointer->*mFunc)(obj); }
+
+ protected:
+ T* mPointer;
+ void (T::*mFunc)(InfoObject& obj);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/IMEData.cpp b/widget/IMEData.cpp
new file mode 100644
index 0000000000..f9497a648d
--- /dev/null
+++ b/widget/IMEData.cpp
@@ -0,0 +1,418 @@
+/* -*- Mode: C++; tab-width: 40; 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 "IMEData.h"
+
+#include <sstream>
+
+#include "ContentData.h"
+#include "gfxFontUtils.h"
+#include "TextEvents.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/ToString.h"
+#include "mozilla/WritingModes.h"
+
+#include "nsPrintfCString.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+template PrintStringDetail::PrintStringDetail(
+ const Maybe<nsString>& aMaybeString, uint32_t aMaxLength);
+
+template <typename StringType>
+PrintStringDetail::PrintStringDetail(const Maybe<StringType>& aMaybeString,
+ uint32_t aMaxLength /* = UINT32_MAX */)
+ : PrintStringDetail(aMaybeString.refOr(EmptyString()), aMaxLength) {
+ if (aMaybeString.isNothing()) {
+ AssignASCII(ToString(aMaybeString).c_str());
+ }
+}
+
+PrintStringDetail::PrintStringDetail(const nsAString& aString,
+ uint32_t aMaxLength /* = UINT32_MAX */) {
+ Assign("\"");
+ const uint32_t kFirstHalf =
+ aString.Length() <= aMaxLength ? UINT32_MAX : (aMaxLength + 1) / 2;
+ const uint32_t kSecondHalf =
+ aString.Length() <= aMaxLength ? 0 : aMaxLength / 2;
+ for (uint32_t i = 0; i < aString.Length(); i++) {
+ if (i > 0) {
+ AppendLiteral(" ");
+ }
+ char32_t ch = aString.CharAt(i);
+ if (NS_IS_HIGH_SURROGATE(ch) && i + 1 < aString.Length() &&
+ NS_IS_LOW_SURROGATE(aString.CharAt(i + 1))) {
+ ch = SURROGATE_TO_UCS4(ch, aString.CharAt(i + 1));
+ }
+ Append(PrintCharData(ch));
+ if (i + 1 == kFirstHalf) {
+ AppendLiteral(" ...");
+ i = aString.Length() - kSecondHalf - 1;
+ if (NS_IS_LOW_SURROGATE(aString.CharAt(i)) &&
+ NS_IS_HIGH_SURROGATE(aString.CharAt(i - 1))) {
+ if (i - 1 <= kFirstHalf) {
+ i++;
+ } else {
+ i--;
+ }
+ }
+ } else if (!IS_IN_BMP(ch)) {
+ i++;
+ }
+ }
+ AppendLiteral("\" (Length()=");
+ AppendInt(static_cast<uint32_t>(aString.Length()));
+ AppendLiteral(")");
+}
+
+// static
+nsCString PrintStringDetail::PrintCharData(char32_t aChar) {
+ switch (aChar) {
+ case 0x0000:
+ return "NULL (0x0000)"_ns;
+ case 0x0008:
+ return "BACKSPACE (0x0008)"_ns;
+ case 0x0009:
+ return "CHARACTER TABULATION (0x0009)"_ns;
+ case 0x000A:
+ return "LINE FEED (0x000A)"_ns;
+ case 0x000B:
+ return "LINE TABULATION (0x000B)"_ns;
+ case 0x000C:
+ return "FORM FEED (0x000C)"_ns;
+ case 0x000D:
+ return "CARRIAGE RETURN (0x000D)"_ns;
+ case 0x0018:
+ return "CANCEL (0x0018)"_ns;
+ case 0x001B:
+ return "ESCAPE (0x001B)"_ns;
+ case 0x0020:
+ return "SPACE (0x0020)"_ns;
+ case 0x007F:
+ return "DELETE (0x007F)"_ns;
+ case 0x00A0:
+ return "NO-BREAK SPACE (0x00A0)"_ns;
+ case 0x00AD:
+ return "SOFT HYPHEN (0x00AD)"_ns;
+ case 0x2000:
+ return "EN QUAD (0x2000)"_ns;
+ case 0x2001:
+ return "EM QUAD (0x2001)"_ns;
+ case 0x2002:
+ return "EN SPACE (0x2002)"_ns;
+ case 0x2003:
+ return "EM SPACE (0x2003)"_ns;
+ case 0x2004:
+ return "THREE-PER-EM SPACE (0x2004)"_ns;
+ case 0x2005:
+ return "FOUR-PER-EM SPACE (0x2005)"_ns;
+ case 0x2006:
+ return "SIX-PER-EM SPACE (0x2006)"_ns;
+ case 0x2007:
+ return "FIGURE SPACE (0x2007)"_ns;
+ case 0x2008:
+ return "PUNCTUATION SPACE (0x2008)"_ns;
+ case 0x2009:
+ return "THIN SPACE (0x2009)"_ns;
+ case 0x200A:
+ return "HAIR SPACE (0x200A)"_ns;
+ case 0x200B:
+ return "ZERO WIDTH SPACE (0x200B)"_ns;
+ case 0x200C:
+ return "ZERO WIDTH NON-JOINER (0x200C)"_ns;
+ case 0x200D:
+ return "ZERO WIDTH JOINER (0x200D)"_ns;
+ case 0x200E:
+ return "LEFT-TO-RIGHT MARK (0x200E)"_ns;
+ case 0x200F:
+ return "RIGHT-TO-LEFT MARK (0x200F)"_ns;
+ case 0x2029:
+ return "PARAGRAPH SEPARATOR (0x2029)"_ns;
+ case 0x202A:
+ return "LEFT-TO-RIGHT EMBEDDING (0x202A)"_ns;
+ case 0x202B:
+ return "RIGHT-TO-LEFT EMBEDDING (0x202B)"_ns;
+ case 0x202D:
+ return "LEFT-TO-RIGHT OVERRIDE (0x202D)"_ns;
+ case 0x202E:
+ return "RIGHT-TO-LEFT OVERRIDE (0x202E)"_ns;
+ case 0x202F:
+ return "NARROW NO-BREAK SPACE (0x202F)"_ns;
+ case 0x205F:
+ return "MEDIUM MATHEMATICAL SPACE (0x205F)"_ns;
+ case 0x2060:
+ return "WORD JOINER (0x2060)"_ns;
+ case 0x2066:
+ return "LEFT-TO-RIGHT ISOLATE (0x2066)"_ns;
+ case 0x2067:
+ return "RIGHT-TO-LEFT ISOLATE (0x2067)"_ns;
+ case 0x3000:
+ return "IDEOGRAPHIC SPACE (0x3000)"_ns;
+ case 0xFEFF:
+ return "ZERO WIDTH NO-BREAK SPACE (0xFEFF)"_ns;
+ default: {
+ if (aChar < ' ' || (aChar >= 0x80 && aChar < 0xA0)) {
+ return nsPrintfCString("Control (0x%04X)", aChar);
+ }
+ if (NS_IS_HIGH_SURROGATE(aChar)) {
+ return nsPrintfCString("High Surrogate (0x%04X)", aChar);
+ }
+ if (NS_IS_LOW_SURROGATE(aChar)) {
+ return nsPrintfCString("Low Surrogate (0x%04X)", aChar);
+ }
+ if (gfxFontUtils::IsVarSelector(aChar)) {
+ return IS_IN_BMP(aChar)
+ ? nsPrintfCString("Variant Selector (0x%04X)", aChar)
+ : nsPrintfCString("Variant Selector (0x%08X)", aChar);
+ }
+ nsAutoString utf16Str;
+ AppendUCS4ToUTF16(aChar, utf16Str);
+ return IS_IN_BMP(aChar)
+ ? nsPrintfCString("'%s' (0x%04X)",
+ NS_ConvertUTF16toUTF8(utf16Str).get(), aChar)
+ : nsPrintfCString("'%s' (0x%08X)",
+ NS_ConvertUTF16toUTF8(utf16Str).get(),
+ aChar);
+ }
+ }
+}
+
+namespace widget {
+
+std::ostream& operator<<(std::ostream& aStream, const IMEEnabled& aEnabled) {
+ switch (aEnabled) {
+ case IMEEnabled::Disabled:
+ return aStream << "DISABLED";
+ case IMEEnabled::Enabled:
+ return aStream << "ENABLED";
+ case IMEEnabled::Password:
+ return aStream << "PASSWORD";
+ case IMEEnabled::Unknown:
+ return aStream << "illegal value";
+ }
+ MOZ_ASSERT_UNREACHABLE("Add a case to handle your new IMEEnabled value");
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream, const IMEState::Open& aOpen) {
+ switch (aOpen) {
+ case IMEState::DONT_CHANGE_OPEN_STATE:
+ aStream << "DONT_CHANGE_OPEN_STATE";
+ break;
+ case IMEState::OPEN:
+ aStream << "OPEN";
+ break;
+ case IMEState::CLOSED:
+ aStream << "CLOSED";
+ break;
+ default:
+ aStream << "illegal value";
+ break;
+ }
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream, const IMEState& aState) {
+ aStream << "{ mEnabled=" << aState.mEnabled << ", mOpen=" << aState.mOpen
+ << " }";
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const InputContext::Origin& aOrigin) {
+ switch (aOrigin) {
+ case InputContext::ORIGIN_MAIN:
+ aStream << "ORIGIN_MAIN";
+ break;
+ case InputContext::ORIGIN_CONTENT:
+ aStream << "ORIGIN_CONTENT";
+ break;
+ default:
+ aStream << "illegal value";
+ break;
+ }
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream, const InputContext& aContext) {
+ aStream << "{ mIMEState=" << aContext.mIMEState
+ << ", mOrigin=" << aContext.mOrigin << ", mHTMLInputType=\""
+ << aContext.mHTMLInputType << "\", mHTMLInputMode=\""
+ << aContext.mHTMLInputMode << "\", mActionHint=\""
+ << aContext.mActionHint << "\", mAutocapitalize=\""
+ << aContext.mAutocapitalize << "\", mIsPrivateBrowsing="
+ << (aContext.mInPrivateBrowsing ? "true" : "false") << " }";
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const InputContextAction::Cause& aCause) {
+ switch (aCause) {
+ case InputContextAction::CAUSE_UNKNOWN:
+ aStream << "CAUSE_UNKNOWN";
+ break;
+ case InputContextAction::CAUSE_UNKNOWN_CHROME:
+ aStream << "CAUSE_UNKNOWN_CHROME";
+ break;
+ case InputContextAction::CAUSE_KEY:
+ aStream << "CAUSE_KEY";
+ break;
+ case InputContextAction::CAUSE_MOUSE:
+ aStream << "CAUSE_MOUSE";
+ break;
+ case InputContextAction::CAUSE_TOUCH:
+ aStream << "CAUSE_TOUCH";
+ break;
+ case InputContextAction::CAUSE_LONGPRESS:
+ aStream << "CAUSE_LONGPRESS";
+ break;
+ case InputContextAction::CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT:
+ aStream << "CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT";
+ break;
+ case InputContextAction::CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT:
+ aStream << "CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT";
+ break;
+ default:
+ aStream << "illegal value";
+ break;
+ }
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const InputContextAction::FocusChange& aFocusChange) {
+ switch (aFocusChange) {
+ case InputContextAction::FOCUS_NOT_CHANGED:
+ aStream << "FOCUS_NOT_CHANGED";
+ break;
+ case InputContextAction::GOT_FOCUS:
+ aStream << "GOT_FOCUS";
+ break;
+ case InputContextAction::LOST_FOCUS:
+ aStream << "LOST_FOCUS";
+ break;
+ case InputContextAction::MENU_GOT_PSEUDO_FOCUS:
+ aStream << "MENU_GOT_PSEUDO_FOCUS";
+ break;
+ case InputContextAction::MENU_LOST_PSEUDO_FOCUS:
+ aStream << "MENU_LOST_PSEUDO_FOCUS";
+ break;
+ case InputContextAction::WIDGET_CREATED:
+ aStream << "WIDGET_CREATED";
+ break;
+ default:
+ aStream << "illegal value";
+ break;
+ }
+ return aStream;
+}
+
+std::ostream& operator<<(
+ std::ostream& aStream,
+ const IMENotification::SelectionChangeDataBase& aData) {
+ if (!aData.IsInitialized()) {
+ aStream << "{ IsInitialized()=false }";
+ return aStream;
+ }
+ if (!aData.HasRange()) {
+ aStream << "{ HasRange()=false }";
+ return aStream;
+ }
+ aStream << "{ mOffset=" << aData.mOffset;
+ if (aData.mString->Length() > 20) {
+ aStream << ", mString.Length()=" << aData.mString->Length();
+ } else {
+ aStream << ", mString=\"" << NS_ConvertUTF16toUTF8(*aData.mString)
+ << "\" (Length()=" << aData.mString->Length() << ")";
+ }
+
+ aStream << ", GetWritingMode()=" << aData.GetWritingMode()
+ << ", mReversed=" << (aData.mReversed ? "true" : "false")
+ << ", mCausedByComposition="
+ << (aData.mCausedByComposition ? "true" : "false")
+ << ", mCausedBySelectionEvent="
+ << (aData.mCausedBySelectionEvent ? "true" : "false")
+ << ", mOccurredDuringComposition="
+ << (aData.mOccurredDuringComposition ? "true" : "false") << " }";
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const IMENotification::TextChangeDataBase& aData) {
+ if (!aData.IsValid()) {
+ aStream << "{ IsValid()=false }";
+ return aStream;
+ }
+ aStream << "{ mStartOffset=" << aData.mStartOffset
+ << ", mRemoveEndOffset=" << aData.mRemovedEndOffset
+ << ", mAddedEndOffset=" << aData.mAddedEndOffset
+ << ", mCausedOnlyByComposition="
+ << (aData.mCausedOnlyByComposition ? "true" : "false")
+ << ", mIncludingChangesDuringComposition="
+ << (aData.mIncludingChangesDuringComposition ? "true" : "false")
+ << ", mIncludingChangesWithoutComposition="
+ << (aData.mIncludingChangesWithoutComposition ? "true" : "false")
+ << " }";
+ return aStream;
+}
+
+/******************************************************************************
+ * IMENotification::SelectionChangeDataBase
+ ******************************************************************************/
+
+void IMENotification::SelectionChangeDataBase::Assign(
+ const WidgetQueryContentEvent& aQuerySelectedTextEvent) {
+ MOZ_ASSERT(aQuerySelectedTextEvent.mMessage == eQuerySelectedText);
+ MOZ_ASSERT(aQuerySelectedTextEvent.Succeeded());
+
+ if (!(mIsInitialized = aQuerySelectedTextEvent.Succeeded())) {
+ ClearSelectionData();
+ return;
+ }
+ if ((mHasRange = aQuerySelectedTextEvent.FoundSelection())) {
+ mOffset = aQuerySelectedTextEvent.mReply->StartOffset();
+ *mString = aQuerySelectedTextEvent.mReply->DataRef();
+ mReversed = aQuerySelectedTextEvent.mReply->mReversed;
+ SetWritingMode(aQuerySelectedTextEvent.mReply->WritingModeRef());
+ } else {
+ mOffset = UINT32_MAX;
+ mString->Truncate();
+ mReversed = false;
+ // Let's keep the writing mode for avoiding temporarily changing the
+ // writing mode at no selection range.
+ }
+}
+
+void IMENotification::SelectionChangeDataBase::SetWritingMode(
+ const WritingMode& aWritingMode) {
+ mWritingModeBits = aWritingMode.GetBits();
+}
+
+WritingMode IMENotification::SelectionChangeDataBase::GetWritingMode() const {
+ return WritingMode(mWritingModeBits);
+}
+
+bool IMENotification::SelectionChangeDataBase::EqualsRange(
+ const ContentSelection& aContentSelection) const {
+ if (aContentSelection.HasRange() != HasRange()) {
+ return false;
+ }
+ if (!HasRange()) {
+ return true;
+ }
+ return mOffset == aContentSelection.OffsetAndDataRef().StartOffset() &&
+ *mString == aContentSelection.OffsetAndDataRef().DataRef();
+}
+
+bool IMENotification::SelectionChangeDataBase::EqualsRangeAndWritingMode(
+ const ContentSelection& aContentSelection) const {
+ return EqualsRange(aContentSelection) &&
+ mWritingModeBits == aContentSelection.WritingModeRef().GetBits();
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/IMEData.h b/widget/IMEData.h
new file mode 100644
index 0000000000..7fb1241333
--- /dev/null
+++ b/widget/IMEData.h
@@ -0,0 +1,1085 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_widget_IMEData_h_
+#define mozilla_widget_IMEData_h_
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/ToString.h"
+
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+#include "nsPoint.h"
+#include "nsRect.h"
+#include "nsString.h"
+#include "nsXULAppAPI.h"
+#include "Units.h"
+
+class nsIWidget;
+
+namespace mozilla {
+
+class ContentSelection;
+class WritingMode;
+
+template <class T>
+class Maybe;
+
+// Helper class to logging string which may contain various Unicode characters
+// and/or may be too long string for logging.
+class MOZ_STACK_CLASS PrintStringDetail : public nsAutoCString {
+ public:
+ static constexpr uint32_t kMaxLengthForCompositionString = 8;
+ static constexpr uint32_t kMaxLengthForSelectedString = 12;
+ static constexpr uint32_t kMaxLengthForEditor = 20;
+
+ PrintStringDetail() = delete;
+ explicit PrintStringDetail(const nsAString& aString,
+ uint32_t aMaxLength = UINT32_MAX);
+ template <typename StringType>
+ explicit PrintStringDetail(const Maybe<StringType>& aMaybeString,
+ uint32_t aMaxLength = UINT32_MAX);
+
+ private:
+ static nsCString PrintCharData(char32_t aChar);
+};
+
+// StartAndEndOffsets represents a range in flat-text.
+template <typename IntType>
+class StartAndEndOffsets {
+ protected:
+ static IntType MaxOffset() { return std::numeric_limits<IntType>::max(); }
+
+ public:
+ StartAndEndOffsets() = delete;
+ explicit StartAndEndOffsets(IntType aStartOffset, IntType aEndOffset)
+ : mStartOffset(aStartOffset),
+ mEndOffset(aStartOffset <= aEndOffset ? aEndOffset : aStartOffset) {
+ MOZ_ASSERT(aStartOffset <= mEndOffset);
+ }
+
+ IntType StartOffset() const { return mStartOffset; }
+ IntType Length() const { return mEndOffset - mStartOffset; }
+ IntType EndOffset() const { return mEndOffset; }
+
+ bool IsOffsetInRange(IntType aOffset) const {
+ return aOffset >= mStartOffset && aOffset < mEndOffset;
+ }
+ bool IsOffsetInRangeOrEndOffset(IntType aOffset) const {
+ return aOffset >= mStartOffset && aOffset <= mEndOffset;
+ }
+
+ void MoveTo(IntType aNewStartOffset) {
+ auto delta = static_cast<int64_t>(mStartOffset) - aNewStartOffset;
+ mStartOffset += delta;
+ mEndOffset += delta;
+ }
+ void SetOffsetAndLength(IntType aNewOffset, IntType aNewLength) {
+ mStartOffset = aNewOffset;
+ CheckedInt<IntType> endOffset(aNewOffset + aNewLength);
+ mEndOffset = endOffset.isValid() ? endOffset.value() : MaxOffset();
+ }
+ void SetEndOffset(IntType aEndOffset) {
+ MOZ_ASSERT(mStartOffset <= aEndOffset);
+ mEndOffset = std::max(aEndOffset, mStartOffset);
+ }
+ void SetStartAndEndOffsets(IntType aStartOffset, IntType aEndOffset) {
+ MOZ_ASSERT(aStartOffset <= aEndOffset);
+ mStartOffset = aStartOffset;
+ mEndOffset = aStartOffset <= aEndOffset ? aEndOffset : aStartOffset;
+ }
+ void SetLength(IntType aNewLength) {
+ CheckedInt<IntType> endOffset(mStartOffset + aNewLength);
+ mEndOffset = endOffset.isValid() ? endOffset.value() : MaxOffset();
+ }
+
+ friend std::ostream& operator<<(
+ std::ostream& aStream,
+ const StartAndEndOffsets<IntType>& aStartAndEndOffsets) {
+ aStream << "{ mStartOffset=" << aStartAndEndOffsets.mStartOffset
+ << ", mEndOffset=" << aStartAndEndOffsets.mEndOffset
+ << ", Length()=" << aStartAndEndOffsets.Length() << " }";
+ return aStream;
+ }
+
+ private:
+ IntType mStartOffset;
+ IntType mEndOffset;
+};
+
+// OffsetAndData class is designed for storing composition string and its
+// start offset. Length() and EndOffset() return only valid length or
+// offset. I.e., if the string is too long for inserting at the offset,
+// the length is shrunken. However, the string itself is not shrunken.
+// Therefore, moving it to where all of the string can be contained,
+// they will return longer/bigger value.
+enum class OffsetAndDataFor {
+ CompositionString,
+ SelectedString,
+ EditorString,
+};
+template <typename IntType>
+class OffsetAndData {
+ protected:
+ static IntType MaxOffset() { return std::numeric_limits<IntType>::max(); }
+
+ public:
+ OffsetAndData() = delete;
+ explicit OffsetAndData(
+ IntType aStartOffset, const nsAString& aData,
+ OffsetAndDataFor aFor = OffsetAndDataFor::CompositionString)
+ : mData(aData), mOffset(aStartOffset), mFor(aFor) {}
+
+ bool IsValid() const {
+ CheckedInt<IntType> offset(mOffset);
+ offset += mData.Length();
+ return offset.isValid();
+ }
+ IntType StartOffset() const { return mOffset; }
+ IntType Length() const {
+ CheckedInt<IntType> endOffset(CheckedInt<IntType>(mOffset) +
+ mData.Length());
+ return endOffset.isValid() ? mData.Length() : MaxOffset() - mOffset;
+ }
+ IntType EndOffset() const { return mOffset + Length(); }
+ StartAndEndOffsets<IntType> CreateStartAndEndOffsets() const {
+ return StartAndEndOffsets<IntType>(StartOffset(), EndOffset());
+ }
+ const nsString& DataRef() const {
+ // In strictly speaking, we should return substring which may be shrunken
+ // for rounding to the max offset. However, it's unrealistic edge case,
+ // and creating new string is not so cheap job in a hot path. Therefore,
+ // this just returns the data as-is.
+ return mData;
+ }
+ bool IsDataEmpty() const { return mData.IsEmpty(); }
+
+ bool IsOffsetInRange(IntType aOffset) const {
+ return aOffset >= mOffset && aOffset < EndOffset();
+ }
+ bool IsOffsetInRangeOrEndOffset(IntType aOffset) const {
+ return aOffset >= mOffset && aOffset <= EndOffset();
+ }
+
+ void Collapse(IntType aOffset) {
+ mOffset = aOffset;
+ mData.Truncate();
+ }
+ void MoveTo(IntType aNewOffset) { mOffset = aNewOffset; }
+ void SetOffsetAndData(IntType aStartOffset, const nsAString& aData) {
+ mOffset = aStartOffset;
+ mData = aData;
+ }
+ void SetData(const nsAString& aData) { mData = aData; }
+ void TruncateData(uint32_t aLength = 0) { mData.Truncate(aLength); }
+ void ReplaceData(nsAString::size_type aCutStart,
+ nsAString::size_type aCutLength,
+ const nsAString& aNewString) {
+ mData.Replace(aCutStart, aCutLength, aNewString);
+ }
+
+ friend std::ostream& operator<<(
+ std::ostream& aStream, const OffsetAndData<IntType>& aOffsetAndData) {
+ const auto maxDataLength =
+ aOffsetAndData.mFor == OffsetAndDataFor::CompositionString
+ ? PrintStringDetail::kMaxLengthForCompositionString
+ : (aOffsetAndData.mFor == OffsetAndDataFor::SelectedString
+ ? PrintStringDetail::kMaxLengthForSelectedString
+ : PrintStringDetail::kMaxLengthForEditor);
+ aStream << "{ mOffset=" << aOffsetAndData.mOffset << ", mData="
+ << PrintStringDetail(aOffsetAndData.mData, maxDataLength).get()
+ << ", Length()=" << aOffsetAndData.Length()
+ << ", EndOffset()=" << aOffsetAndData.EndOffset() << " }";
+ return aStream;
+ }
+
+ private:
+ nsString mData;
+ IntType mOffset;
+ OffsetAndDataFor mFor;
+};
+
+namespace widget {
+
+/**
+ * Preference for receiving IME updates
+ *
+ * If mWantUpdates is not NOTIFY_NOTHING, nsTextStateManager will observe text
+ * change and/or selection change and call nsIWidget::NotifyIME() with
+ * NOTIFY_IME_OF_SELECTION_CHANGE and/or NOTIFY_IME_OF_TEXT_CHANGE.
+ * Please note that the text change observing cost is very expensive especially
+ * on an HTML editor has focus.
+ * If the IME implementation on a particular platform doesn't care about
+ * NOTIFY_IME_OF_SELECTION_CHANGE and/or NOTIFY_IME_OF_TEXT_CHANGE,
+ * they should set mWantUpdates to NOTIFY_NOTHING to avoid the cost.
+ * If the IME implementation needs notifications even while our process is
+ * deactive, it should also set NOTIFY_DURING_DEACTIVE.
+ */
+struct IMENotificationRequests final {
+ typedef uint8_t Notifications;
+
+ enum : Notifications {
+ NOTIFY_NOTHING = 0,
+ NOTIFY_TEXT_CHANGE = 1 << 1,
+ NOTIFY_POSITION_CHANGE = 1 << 2,
+ // NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR is used when mouse button is pressed
+ // or released on a character in the focused editor. The notification is
+ // notified to IME as a mouse event. If it's consumed by IME, NotifyIME()
+ // returns NS_SUCCESS_EVENT_CONSUMED. Otherwise, it returns NS_OK if it's
+ // handled without any error.
+ NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR = 1 << 3,
+ // NOTE: NOTIFY_DURING_DEACTIVE isn't supported in environments where two
+ // or more compositions are possible. E.g., Mac and Linux (GTK).
+ NOTIFY_DURING_DEACTIVE = 1 << 7,
+
+ NOTIFY_ALL = NOTIFY_TEXT_CHANGE | NOTIFY_POSITION_CHANGE |
+ NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR,
+ };
+
+ IMENotificationRequests() : mWantUpdates(NOTIFY_NOTHING) {}
+
+ explicit IMENotificationRequests(Notifications aWantUpdates)
+ : mWantUpdates(aWantUpdates) {}
+
+ IMENotificationRequests operator|(
+ const IMENotificationRequests& aOther) const {
+ return IMENotificationRequests(aOther.mWantUpdates | mWantUpdates);
+ }
+ IMENotificationRequests& operator|=(const IMENotificationRequests& aOther) {
+ mWantUpdates |= aOther.mWantUpdates;
+ return *this;
+ }
+ bool operator==(const IMENotificationRequests& aOther) const {
+ return mWantUpdates == aOther.mWantUpdates;
+ }
+
+ bool WantTextChange() const { return !!(mWantUpdates & NOTIFY_TEXT_CHANGE); }
+
+ bool WantPositionChanged() const {
+ return !!(mWantUpdates & NOTIFY_POSITION_CHANGE);
+ }
+
+ bool WantChanges() const { return WantTextChange(); }
+
+ bool WantMouseButtonEventOnChar() const {
+ return !!(mWantUpdates & NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR);
+ }
+
+ bool WantDuringDeactive() const {
+ return !!(mWantUpdates & NOTIFY_DURING_DEACTIVE);
+ }
+
+ Notifications mWantUpdates;
+};
+
+/**
+ * IME enabled states.
+ *
+ * WARNING: If you change these values, you also need to edit:
+ * nsIDOMWindowUtils.idl
+ */
+enum class IMEEnabled {
+ /**
+ * 'Disabled' means the user cannot use IME. So, the IME open state should
+ * be 'closed' during 'disabled'.
+ */
+ Disabled,
+ /**
+ * 'Enabled' means the user can use IME.
+ */
+ Enabled,
+ /**
+ * 'Password' state is a special case for the password editors.
+ * E.g., on mac, the password editors should disable the non-Roman
+ * keyboard layouts at getting focus. Thus, the password editor may have
+ * special rules on some platforms.
+ */
+ Password,
+ /**
+ * 'Unknown' is useful when you cache this enum. So, this shouldn't be
+ * used with nsIWidget::SetInputContext().
+ */
+ Unknown,
+};
+
+/**
+ * Contains IMEStatus plus information about the current
+ * input context that the IME can use as hints if desired.
+ */
+
+struct IMEState final {
+ IMEEnabled mEnabled;
+
+ /**
+ * IME open states the mOpen value of SetInputContext() should be one value of
+ * OPEN, CLOSE or DONT_CHANGE_OPEN_STATE. GetInputContext() should return
+ * OPEN, CLOSE or OPEN_STATE_NOT_SUPPORTED.
+ */
+ enum Open {
+ /**
+ * 'Unsupported' means the platform cannot return actual IME open state.
+ * This value is used only by GetInputContext().
+ */
+ OPEN_STATE_NOT_SUPPORTED,
+ /**
+ * 'Don't change' means the widget shouldn't change IME open state when
+ * SetInputContext() is called.
+ */
+ DONT_CHANGE_OPEN_STATE = OPEN_STATE_NOT_SUPPORTED,
+ /**
+ * 'Open' means that IME should compose in its primary language (or latest
+ * input mode except direct ASCII character input mode). Even if IME is
+ * opened by this value, users should be able to close IME by theirselves.
+ * Web contents can specify this value by |ime-mode: active;|.
+ */
+ OPEN,
+ /**
+ * 'Closed' means that IME shouldn't handle key events (or should handle
+ * as ASCII character inputs on mobile device). Even if IME is closed by
+ * this value, users should be able to open IME by theirselves.
+ * Web contents can specify this value by |ime-mode: inactive;|.
+ */
+ CLOSED
+ };
+ Open mOpen;
+
+ IMEState() : mEnabled(IMEEnabled::Enabled), mOpen(DONT_CHANGE_OPEN_STATE) {}
+
+ explicit IMEState(IMEEnabled aEnabled, Open aOpen = DONT_CHANGE_OPEN_STATE)
+ : mEnabled(aEnabled), mOpen(aOpen) {}
+
+ // Returns true if the user can input characters.
+ // This means that a plain text editor, an HTML editor, a password editor or
+ // a plain text editor whose ime-mode is "disabled".
+ bool IsEditable() const {
+ return mEnabled == IMEEnabled::Enabled || mEnabled == IMEEnabled::Password;
+ }
+};
+
+// NS_ONLY_ONE_NATIVE_IME_CONTEXT is a special value of native IME context.
+// If there can be only one IME composition in a process, this can be used.
+#define NS_ONLY_ONE_NATIVE_IME_CONTEXT \
+ (reinterpret_cast<void*>(static_cast<intptr_t>(-1)))
+
+struct NativeIMEContext final {
+ // Pointer to native IME context. Typically this is the result of
+ // nsIWidget::GetNativeData(NS_RAW_NATIVE_IME_CONTEXT) in the parent process.
+ // See also NS_ONLY_ONE_NATIVE_IME_CONTEXT.
+ uintptr_t mRawNativeIMEContext;
+ // Process ID of the origin of mNativeIMEContext.
+ // static_cast<uint64_t>(-1) if the instance is not initialized properly.
+ // 0 if the instance is originated in the parent process.
+ // 1 or greater if the instance is originated in a content process.
+ uint64_t mOriginProcessID;
+
+ NativeIMEContext() : mRawNativeIMEContext(0), mOriginProcessID(0) {
+ Init(nullptr);
+ }
+
+ explicit NativeIMEContext(nsIWidget* aWidget)
+ : mRawNativeIMEContext(0), mOriginProcessID(0) {
+ Init(aWidget);
+ }
+
+ bool IsValid() const {
+ return mRawNativeIMEContext &&
+ mOriginProcessID != static_cast<uint64_t>(-1);
+ }
+
+ bool IsOriginatedInParentProcess() const {
+ return mOriginProcessID != 0 &&
+ mOriginProcessID != static_cast<uint64_t>(-1);
+ }
+
+ void Init(nsIWidget* aWidget);
+ void InitWithRawNativeIMEContext(const void* aRawNativeIMEContext) {
+ InitWithRawNativeIMEContext(const_cast<void*>(aRawNativeIMEContext));
+ }
+ void InitWithRawNativeIMEContext(void* aRawNativeIMEContext);
+
+ bool operator==(const NativeIMEContext& aOther) const {
+ return mRawNativeIMEContext == aOther.mRawNativeIMEContext &&
+ mOriginProcessID == aOther.mOriginProcessID;
+ }
+ bool operator!=(const NativeIMEContext& aOther) const {
+ return !(*this == aOther);
+ }
+};
+
+struct InputContext final {
+ InputContext()
+ : mOrigin(XRE_IsParentProcess() ? ORIGIN_MAIN : ORIGIN_CONTENT),
+ mHasHandledUserInput(false),
+ mInPrivateBrowsing(false) {}
+
+ // If InputContext instance is a static variable, any heap allocated stuff
+ // of its members need to be deleted at XPCOM shutdown. Otherwise, it's
+ // detected as memory leak.
+ void ShutDown() {
+ mURI = nullptr;
+ mHTMLInputType.Truncate();
+ mHTMLInputMode.Truncate();
+ mActionHint.Truncate();
+ mAutocapitalize.Truncate();
+ }
+
+ bool IsPasswordEditor() const {
+ return mHTMLInputType.LowerCaseEqualsLiteral("password");
+ }
+
+ NativeKeyBindingsType GetNativeKeyBindingsType() const {
+ MOZ_DIAGNOSTIC_ASSERT(mIMEState.IsEditable());
+ // See GetInputType in IMEStateManager.cpp
+ if (mHTMLInputType.IsEmpty()) {
+ return NativeKeyBindingsType::RichTextEditor;
+ }
+ return mHTMLInputType.EqualsLiteral("textarea")
+ ? NativeKeyBindingsType::MultiLineEditor
+ : NativeKeyBindingsType::SingleLineEditor;
+ }
+
+ // https://html.spec.whatwg.org/dev/interaction.html#autocapitalization
+ bool IsAutocapitalizeSupported() const {
+ return !mHTMLInputType.EqualsLiteral("password") &&
+ !mHTMLInputType.EqualsLiteral("url") &&
+ !mHTMLInputType.EqualsLiteral("email");
+ }
+
+ bool IsInputAttributeChanged(const InputContext& aOldContext) const {
+ return mIMEState.mEnabled != aOldContext.mIMEState.mEnabled ||
+#if defined(ANDROID) || defined(MOZ_WIDGET_GTK) || defined(XP_WIN)
+ // input type and inputmode are supported by Windows IME API, GTK
+ // IME API and Android IME API
+ mHTMLInputType != aOldContext.mHTMLInputType ||
+ mHTMLInputMode != aOldContext.mHTMLInputMode ||
+#endif
+#if defined(ANDROID) || defined(MOZ_WIDGET_GTK)
+ // autocapitalize is supported by Android IME API and GTK IME API
+ mAutocapitalize != aOldContext.mAutocapitalize ||
+#endif
+#if defined(ANDROID)
+ // enterkeyhint is only supported by Android IME API.
+ mActionHint != aOldContext.mActionHint ||
+#endif
+ false;
+ }
+
+ IMEState mIMEState;
+
+ // The URI of the document which has the editable element.
+ nsCOMPtr<nsIURI> mURI;
+
+ /* The type of the input if the input is a html input field */
+ nsString mHTMLInputType;
+
+ // The value of the inputmode
+ nsString mHTMLInputMode;
+
+ /* A hint for the action that is performed when the input is submitted */
+ nsString mActionHint;
+
+ /* A hint for autocapitalize */
+ nsString mAutocapitalize;
+
+ /**
+ * mOrigin indicates whether this focus event refers to main or remote
+ * content.
+ */
+ enum Origin {
+ // Adjusting focus of content on the main process
+ ORIGIN_MAIN,
+ // Adjusting focus of content in a remote process
+ ORIGIN_CONTENT
+ };
+ Origin mOrigin;
+
+ /**
+ * True if the document has ever received user input
+ */
+ bool mHasHandledUserInput;
+
+ /* Whether the owning document of the input element has been loaded
+ * in private browsing mode. */
+ bool mInPrivateBrowsing;
+
+ bool IsOriginMainProcess() const { return mOrigin == ORIGIN_MAIN; }
+
+ bool IsOriginContentProcess() const { return mOrigin == ORIGIN_CONTENT; }
+
+ bool IsOriginCurrentProcess() const {
+ if (XRE_IsParentProcess()) {
+ return IsOriginMainProcess();
+ }
+ return IsOriginContentProcess();
+ }
+};
+
+// FYI: Implemented in nsBaseWidget.cpp
+const char* ToChar(InputContext::Origin aOrigin);
+
+struct InputContextAction final {
+ /**
+ * mCause indicates what action causes calling nsIWidget::SetInputContext().
+ * It must be one of following values.
+ */
+ enum Cause {
+ // The cause is unknown but originated from content. Focus might have been
+ // changed by content script.
+ CAUSE_UNKNOWN,
+ // The cause is unknown but originated from chrome. Focus might have been
+ // changed by chrome script.
+ CAUSE_UNKNOWN_CHROME,
+ // The cause is user's keyboard operation.
+ CAUSE_KEY,
+ // The cause is user's mouse operation.
+ CAUSE_MOUSE,
+ // The cause is user's touch operation (implies mouse)
+ CAUSE_TOUCH,
+ // The cause is users' long press operation.
+ CAUSE_LONGPRESS,
+ // The cause is unknown but it occurs during user input except keyboard
+ // input. E.g., an event handler of a user input event moves focus.
+ CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT,
+ // The cause is unknown but it occurs during keyboard input.
+ CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT,
+ };
+ Cause mCause;
+
+ /**
+ * mFocusChange indicates what happened for focus.
+ */
+ enum FocusChange {
+ FOCUS_NOT_CHANGED,
+ // A content got focus.
+ GOT_FOCUS,
+ // Focused content lost focus.
+ LOST_FOCUS,
+ // Menu got pseudo focus that means focused content isn't changed but
+ // keyboard events will be handled by menu.
+ MENU_GOT_PSEUDO_FOCUS,
+ // Menu lost pseudo focus that means focused content will handle keyboard
+ // events.
+ MENU_LOST_PSEUDO_FOCUS,
+ // The widget is created. When a widget is crated, it may need to notify
+ // IME module to initialize its native IME context. In such case, this is
+ // used. I.e., this isn't used by IMEStateManager.
+ WIDGET_CREATED
+ };
+ FocusChange mFocusChange;
+
+ bool ContentGotFocusByTrustedCause() const {
+ return (mFocusChange == GOT_FOCUS && mCause != CAUSE_UNKNOWN);
+ }
+
+ bool UserMightRequestOpenVKB() const {
+ // If focus is changed, user must not request to open VKB.
+ if (mFocusChange != FOCUS_NOT_CHANGED) {
+ return false;
+ }
+ switch (mCause) {
+ // If user clicks or touches focused editor, user must request to open
+ // VKB.
+ case CAUSE_MOUSE:
+ case CAUSE_TOUCH:
+ // If script does something during a user input and that causes changing
+ // input context, user might request to open VKB. E.g., user clicks
+ // dummy editor and JS moves focus to an actual editable node. However,
+ // this should return false if the user input is a keyboard event since
+ // physical keyboard operation shouldn't cause opening VKB.
+ case CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * IsHandlingUserInput() returns true if it's caused by a user action directly
+ * or it's caused by script or something but it occurred while we're handling
+ * a user action. E.g., when it's caused by Element.focus() in an event
+ * handler of a user input, this returns true.
+ */
+ static bool IsHandlingUserInput(Cause aCause) {
+ switch (aCause) {
+ case CAUSE_KEY:
+ case CAUSE_MOUSE:
+ case CAUSE_TOUCH:
+ case CAUSE_LONGPRESS:
+ case CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT:
+ case CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ bool IsHandlingUserInput() const { return IsHandlingUserInput(mCause); }
+
+ InputContextAction()
+ : mCause(CAUSE_UNKNOWN), mFocusChange(FOCUS_NOT_CHANGED) {}
+
+ explicit InputContextAction(Cause aCause,
+ FocusChange aFocusChange = FOCUS_NOT_CHANGED)
+ : mCause(aCause), mFocusChange(aFocusChange) {}
+};
+
+// IMEMessage is shared by IMEStateManager and TextComposition.
+// Update values in GeckoEditable.java if you make changes here.
+// XXX Negative values are used in Android...
+typedef int8_t IMEMessageType;
+enum IMEMessage : IMEMessageType {
+ // This is used by IMENotification internally. This means that the instance
+ // hasn't been initialized yet.
+ NOTIFY_IME_OF_NOTHING,
+ // An editable content is getting focus
+ NOTIFY_IME_OF_FOCUS,
+ // An editable content is losing focus
+ NOTIFY_IME_OF_BLUR,
+ // Selection in the focused editable content is changed
+ NOTIFY_IME_OF_SELECTION_CHANGE,
+ // Text in the focused editable content is changed
+ NOTIFY_IME_OF_TEXT_CHANGE,
+ // Notified when a dispatched composition event is handled by the
+ // contents. This must be notified after the other notifications.
+ // Note that if a remote process has focus, this is notified only once when
+ // all dispatched events are handled completely. So, the receiver shouldn't
+ // count number of received this notification for comparing with the number
+ // of dispatched events.
+ // NOTE: If a composition event causes moving focus from the focused editor,
+ // this notification may not be notified as usual. Even in such case,
+ // NOTIFY_IME_OF_BLUR is always sent. So, notification listeners
+ // should tread the blur notification as including this if there is
+ // pending composition events.
+ NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED,
+ // Position or size of focused element may be changed.
+ NOTIFY_IME_OF_POSITION_CHANGE,
+ // Mouse button event is fired on a character in focused editor
+ NOTIFY_IME_OF_MOUSE_BUTTON_EVENT,
+ // Request to commit current composition to IME
+ // (some platforms may not support)
+ REQUEST_TO_COMMIT_COMPOSITION,
+ // Request to cancel current composition to IME
+ // (some platforms may not support)
+ REQUEST_TO_CANCEL_COMPOSITION
+};
+
+// FYI: Implemented in nsBaseWidget.cpp
+const char* ToChar(IMEMessage aIMEMessage);
+
+struct IMENotification final {
+ IMENotification() : mMessage(NOTIFY_IME_OF_NOTHING), mSelectionChangeData() {}
+
+ IMENotification(const IMENotification& aOther)
+ : mMessage(NOTIFY_IME_OF_NOTHING) {
+ Assign(aOther);
+ }
+
+ ~IMENotification() { Clear(); }
+
+ MOZ_IMPLICIT IMENotification(IMEMessage aMessage)
+ : mMessage(aMessage), mSelectionChangeData() {
+ switch (aMessage) {
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ mSelectionChangeData.mString = new nsString();
+ mSelectionChangeData.Clear();
+ break;
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ mTextChangeData.Clear();
+ break;
+ case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ mMouseButtonEventData.mEventMessage = eVoidEvent;
+ mMouseButtonEventData.mOffset = UINT32_MAX;
+ mMouseButtonEventData.mCursorPos.MoveTo(0, 0);
+ mMouseButtonEventData.mCharRect.SetRect(0, 0, 0, 0);
+ mMouseButtonEventData.mButton = -1;
+ mMouseButtonEventData.mButtons = 0;
+ mMouseButtonEventData.mModifiers = 0;
+ break;
+ default:
+ break;
+ }
+ }
+
+ void Assign(const IMENotification& aOther) {
+ bool changingMessage = mMessage != aOther.mMessage;
+ if (changingMessage) {
+ Clear();
+ mMessage = aOther.mMessage;
+ }
+ switch (mMessage) {
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ if (changingMessage) {
+ mSelectionChangeData.mString = new nsString();
+ }
+ mSelectionChangeData.Assign(aOther.mSelectionChangeData);
+ break;
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ mTextChangeData = aOther.mTextChangeData;
+ break;
+ case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ mMouseButtonEventData = aOther.mMouseButtonEventData;
+ break;
+ default:
+ break;
+ }
+ }
+
+ IMENotification& operator=(const IMENotification& aOther) {
+ Assign(aOther);
+ return *this;
+ }
+
+ void Clear() {
+ if (mMessage == NOTIFY_IME_OF_SELECTION_CHANGE) {
+ MOZ_ASSERT(mSelectionChangeData.mString);
+ delete mSelectionChangeData.mString;
+ mSelectionChangeData.mString = nullptr;
+ }
+ mMessage = NOTIFY_IME_OF_NOTHING;
+ }
+
+ bool HasNotification() const { return mMessage != NOTIFY_IME_OF_NOTHING; }
+
+ void MergeWith(const IMENotification& aNotification) {
+ switch (mMessage) {
+ case NOTIFY_IME_OF_NOTHING:
+ MOZ_ASSERT(aNotification.mMessage != NOTIFY_IME_OF_NOTHING);
+ Assign(aNotification);
+ break;
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ MOZ_ASSERT(aNotification.mMessage == NOTIFY_IME_OF_SELECTION_CHANGE);
+ mSelectionChangeData.Assign(aNotification.mSelectionChangeData);
+ break;
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ MOZ_ASSERT(aNotification.mMessage == NOTIFY_IME_OF_TEXT_CHANGE);
+ mTextChangeData += aNotification.mTextChangeData;
+ break;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ MOZ_ASSERT(aNotification.mMessage == mMessage);
+ break;
+ default:
+ MOZ_CRASH("Merging notification isn't supported");
+ break;
+ }
+ }
+
+ IMEMessage mMessage;
+
+ // NOTIFY_IME_OF_SELECTION_CHANGE specific data
+ struct SelectionChangeDataBase {
+ // Selection range.
+ uint32_t mOffset;
+
+ // Selected string
+ nsString* mString;
+
+ // Writing mode at the selection.
+ uint8_t mWritingModeBits;
+
+ bool mIsInitialized;
+ bool mHasRange;
+ bool mReversed;
+ bool mCausedByComposition;
+ bool mCausedBySelectionEvent;
+ bool mOccurredDuringComposition;
+
+ void SetWritingMode(const WritingMode& aWritingMode);
+ WritingMode GetWritingMode() const;
+
+ uint32_t StartOffset() const {
+ MOZ_ASSERT(mHasRange);
+ return mOffset;
+ }
+ uint32_t EndOffset() const {
+ MOZ_ASSERT(mHasRange);
+ return mOffset + Length();
+ }
+ uint32_t AnchorOffset() const {
+ MOZ_ASSERT(mHasRange);
+ return mOffset + (mReversed ? Length() : 0);
+ }
+ uint32_t FocusOffset() const {
+ MOZ_ASSERT(mHasRange);
+ return mOffset + (mReversed ? 0 : Length());
+ }
+ const nsString& String() const {
+ MOZ_ASSERT(mHasRange);
+ return *mString;
+ }
+ uint32_t Length() const {
+ MOZ_ASSERT(mHasRange);
+ return mString->Length();
+ }
+ bool IsInInt32Range() const {
+ return mHasRange && mOffset <= INT32_MAX && Length() <= INT32_MAX &&
+ mOffset + Length() <= INT32_MAX;
+ }
+ bool HasRange() const { return mIsInitialized && mHasRange; }
+ bool IsCollapsed() const { return !mHasRange || mString->IsEmpty(); }
+ void ClearSelectionData() {
+ mIsInitialized = false;
+ mHasRange = false;
+ mOffset = UINT32_MAX;
+ mString->Truncate();
+ mWritingModeBits = 0;
+ mReversed = false;
+ }
+ void Clear() {
+ ClearSelectionData();
+ mCausedByComposition = false;
+ mCausedBySelectionEvent = false;
+ mOccurredDuringComposition = false;
+ }
+ bool IsInitialized() const { return mIsInitialized; }
+ void Assign(const SelectionChangeDataBase& aOther) {
+ mIsInitialized = aOther.mIsInitialized;
+ mHasRange = aOther.mHasRange;
+ if (mIsInitialized && mHasRange) {
+ mOffset = aOther.mOffset;
+ *mString = aOther.String();
+ mReversed = aOther.mReversed;
+ mWritingModeBits = aOther.mWritingModeBits;
+ } else {
+ mOffset = UINT32_MAX;
+ mString->Truncate();
+ mReversed = false;
+ // Let's keep the writing mode for avoiding temporarily changing the
+ // writing mode at no selection range.
+ }
+ AssignReason(aOther.mCausedByComposition, aOther.mCausedBySelectionEvent,
+ aOther.mOccurredDuringComposition);
+ }
+ void Assign(const WidgetQueryContentEvent& aQuerySelectedTextEvent);
+ void AssignReason(bool aCausedByComposition, bool aCausedBySelectionEvent,
+ bool aOccurredDuringComposition) {
+ mCausedByComposition = aCausedByComposition;
+ mCausedBySelectionEvent = aCausedBySelectionEvent;
+ mOccurredDuringComposition = aOccurredDuringComposition;
+ }
+
+ bool EqualsRange(const SelectionChangeDataBase& aOther) const {
+ if (HasRange() != aOther.HasRange()) {
+ return false;
+ }
+ if (!HasRange()) {
+ return true;
+ }
+ return mOffset == aOther.mOffset && mString->Equals(*aOther.mString);
+ }
+ bool EqualsRangeAndDirection(const SelectionChangeDataBase& aOther) const {
+ return EqualsRange(aOther) &&
+ (!HasRange() || mReversed == aOther.mReversed);
+ }
+ bool EqualsRangeAndDirectionAndWritingMode(
+ const SelectionChangeDataBase& aOther) const {
+ return EqualsRangeAndDirection(aOther) &&
+ mWritingModeBits == aOther.mWritingModeBits;
+ }
+
+ bool EqualsRange(const ContentSelection& aContentSelection) const;
+ bool EqualsRangeAndWritingMode(
+ const ContentSelection& aContentSelection) const;
+
+ OffsetAndData<uint32_t> ToUint32OffsetAndData() const {
+ return OffsetAndData<uint32_t>(mOffset, *mString,
+ OffsetAndDataFor::SelectedString);
+ }
+ };
+
+ // SelectionChangeDataBase cannot have constructors because it's used in
+ // the union. Therefore, SelectionChangeData should only implement
+ // constructors. In other words, add other members to
+ // SelectionChangeDataBase.
+ struct SelectionChangeData final : public SelectionChangeDataBase {
+ SelectionChangeData() {
+ mString = &mStringInstance;
+ Clear();
+ }
+ explicit SelectionChangeData(const SelectionChangeDataBase& aOther) {
+ mString = &mStringInstance;
+ Assign(aOther);
+ }
+ SelectionChangeData(const SelectionChangeData& aOther) {
+ mString = &mStringInstance;
+ Assign(aOther);
+ }
+ SelectionChangeData& operator=(const SelectionChangeDataBase& aOther) {
+ mString = &mStringInstance;
+ Assign(aOther);
+ return *this;
+ }
+ SelectionChangeData& operator=(const SelectionChangeData& aOther) {
+ mString = &mStringInstance;
+ Assign(aOther);
+ return *this;
+ }
+
+ private:
+ // When SelectionChangeData is used outside of union, it shouldn't create
+ // nsString instance in the heap as far as possible.
+ nsString mStringInstance;
+ };
+
+ struct TextChangeDataBase {
+ // mStartOffset is the start offset of modified or removed text in
+ // original content and inserted text in new content.
+ uint32_t mStartOffset;
+ // mRemovalEndOffset is the end offset of modified or removed text in
+ // original content. If the value is same as mStartOffset, no text hasn't
+ // been removed yet.
+ uint32_t mRemovedEndOffset;
+ // mAddedEndOffset is the end offset of inserted text or same as
+ // mStartOffset if just removed. The vlaue is offset in the new content.
+ uint32_t mAddedEndOffset;
+
+ // Note that TextChangeDataBase may be the result of merging two or more
+ // changes especially in e10s mode.
+
+ // mCausedOnlyByComposition is true only when *all* merged changes are
+ // caused by composition.
+ bool mCausedOnlyByComposition;
+ // mIncludingChangesDuringComposition is true if at least one change which
+ // is not caused by composition occurred during the last composition.
+ // Note that if after the last composition is finished and there are some
+ // changes not caused by composition, this is set to false.
+ bool mIncludingChangesDuringComposition;
+ // mIncludingChangesWithoutComposition is true if there is at least one
+ // change which did occur when there wasn't a composition ongoing.
+ bool mIncludingChangesWithoutComposition;
+
+ uint32_t OldLength() const {
+ MOZ_ASSERT(IsValid());
+ return mRemovedEndOffset - mStartOffset;
+ }
+ uint32_t NewLength() const {
+ MOZ_ASSERT(IsValid());
+ return mAddedEndOffset - mStartOffset;
+ }
+
+ // Positive if text is added. Negative if text is removed.
+ int64_t Difference() const { return mAddedEndOffset - mRemovedEndOffset; }
+
+ bool IsInInt32Range() const {
+ MOZ_ASSERT(IsValid());
+ return mStartOffset <= INT32_MAX && mRemovedEndOffset <= INT32_MAX &&
+ mAddedEndOffset <= INT32_MAX;
+ }
+
+ bool IsValid() const {
+ return !(mStartOffset == UINT32_MAX && !mRemovedEndOffset &&
+ !mAddedEndOffset);
+ }
+
+ void Clear() {
+ mStartOffset = UINT32_MAX;
+ mRemovedEndOffset = mAddedEndOffset = 0;
+ }
+
+ void MergeWith(const TextChangeDataBase& aOther);
+ TextChangeDataBase& operator+=(const TextChangeDataBase& aOther) {
+ MergeWith(aOther);
+ return *this;
+ }
+
+#ifdef DEBUG
+ void Test();
+#endif // #ifdef DEBUG
+ };
+
+ // TextChangeDataBase cannot have constructors because they are used in union.
+ // Therefore, TextChangeData should only implement constructor. In other
+ // words, add other members to TextChangeDataBase.
+ struct TextChangeData : public TextChangeDataBase {
+ TextChangeData() { Clear(); }
+
+ TextChangeData(uint32_t aStartOffset, uint32_t aRemovedEndOffset,
+ uint32_t aAddedEndOffset, bool aCausedByComposition,
+ bool aOccurredDuringComposition) {
+ MOZ_ASSERT(aRemovedEndOffset >= aStartOffset,
+ "removed end offset must not be smaller than start offset");
+ MOZ_ASSERT(aAddedEndOffset >= aStartOffset,
+ "added end offset must not be smaller than start offset");
+ mStartOffset = aStartOffset;
+ mRemovedEndOffset = aRemovedEndOffset;
+ mAddedEndOffset = aAddedEndOffset;
+ mCausedOnlyByComposition = aCausedByComposition;
+ mIncludingChangesDuringComposition =
+ !aCausedByComposition && aOccurredDuringComposition;
+ mIncludingChangesWithoutComposition =
+ !aCausedByComposition && !aOccurredDuringComposition;
+ }
+ };
+
+ struct MouseButtonEventData {
+ // The value of WidgetEvent::mMessage
+ EventMessage mEventMessage;
+ // Character offset from the start of the focused editor under the cursor
+ uint32_t mOffset;
+ // Cursor position in pixels relative to the widget
+ LayoutDeviceIntPoint mCursorPos;
+ // Character rect in pixels under the cursor relative to the widget
+ LayoutDeviceIntRect mCharRect;
+ // The value of WidgetMouseEventBase::button and buttons
+ int16_t mButton;
+ int16_t mButtons;
+ // The value of WidgetInputEvent::modifiers
+ Modifiers mModifiers;
+ };
+
+ union {
+ // NOTIFY_IME_OF_SELECTION_CHANGE specific data
+ SelectionChangeDataBase mSelectionChangeData;
+
+ // NOTIFY_IME_OF_TEXT_CHANGE specific data
+ TextChangeDataBase mTextChangeData;
+
+ // NOTIFY_IME_OF_MOUSE_BUTTON_EVENT specific data
+ MouseButtonEventData mMouseButtonEventData;
+ };
+
+ void SetData(const SelectionChangeDataBase& aSelectionChangeData) {
+ MOZ_RELEASE_ASSERT(mMessage == NOTIFY_IME_OF_SELECTION_CHANGE);
+ mSelectionChangeData.Assign(aSelectionChangeData);
+ }
+
+ void SetData(const TextChangeDataBase& aTextChangeData) {
+ MOZ_RELEASE_ASSERT(mMessage == NOTIFY_IME_OF_TEXT_CHANGE);
+ mTextChangeData = aTextChangeData;
+ }
+};
+
+struct CandidateWindowPosition {
+ // Upper left corner of the candidate window if mExcludeRect is false.
+ // Otherwise, the position currently interested. E.g., caret position.
+ LayoutDeviceIntPoint mPoint;
+ // Rect which shouldn't be overlapped with the candidate window.
+ // This is valid only when mExcludeRect is true.
+ LayoutDeviceIntRect mRect;
+ // See explanation of mPoint and mRect.
+ bool mExcludeRect;
+};
+
+std::ostream& operator<<(std::ostream& aStream, const IMEEnabled& aEnabled);
+std::ostream& operator<<(std::ostream& aStream, const IMEState::Open& aOpen);
+std::ostream& operator<<(std::ostream& aStream, const IMEState& aState);
+std::ostream& operator<<(std::ostream& aStream,
+ const InputContext::Origin& aOrigin);
+std::ostream& operator<<(std::ostream& aStream, const InputContext& aContext);
+std::ostream& operator<<(std::ostream& aStream,
+ const InputContextAction::Cause& aCause);
+std::ostream& operator<<(std::ostream& aStream,
+ const InputContextAction::FocusChange& aFocusChange);
+std::ostream& operator<<(std::ostream& aStream,
+ const IMENotification::SelectionChangeDataBase& aData);
+std::ostream& operator<<(std::ostream& aStream,
+ const IMENotification::TextChangeDataBase& aData);
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef mozilla_widget_IMEData_h_
diff --git a/widget/IconLoader.cpp b/widget/IconLoader.cpp
new file mode 100644
index 0000000000..5c0488e3e2
--- /dev/null
+++ b/widget/IconLoader.cpp
@@ -0,0 +1,132 @@
+/* -*- 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 "mozilla/widget/IconLoader.h"
+#include "gfxPlatform.h"
+#include "imgIContainer.h"
+#include "imgLoader.h"
+#include "imgRequestProxy.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "nsIContent.h"
+#include "nsIContentPolicy.h"
+
+using namespace mozilla;
+
+namespace mozilla::widget {
+
+NS_IMPL_ISUPPORTS(IconLoader, imgINotificationObserver)
+
+IconLoader::IconLoader(Listener* aListener) : mListener(aListener) {}
+
+IconLoader::~IconLoader() { Destroy(); }
+
+void IconLoader::Destroy() {
+ if (mIconRequest) {
+ mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
+ mIconRequest = nullptr;
+ }
+ mListener = nullptr;
+}
+
+nsresult IconLoader::LoadIcon(nsIURI* aIconURI, nsINode* aNode,
+ bool aIsInternalIcon) {
+ if (mIconRequest) {
+ // Another icon request is already in flight. Kill it.
+ mIconRequest->CancelWithReason(
+ NS_BINDING_ABORTED, "Another icon request is already in flight"_ns);
+ mIconRequest = nullptr;
+ }
+
+ if (!aNode) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<mozilla::dom::Document> document = aNode->OwnerDoc();
+
+ nsCOMPtr<nsILoadGroup> loadGroup = document->GetDocumentLoadGroup();
+ if (!loadGroup) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<imgLoader> loader = nsContentUtils::GetImgLoaderForDocument(document);
+ if (!loader) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ if (aIsInternalIcon) {
+ rv = loader->LoadImage(
+ aIconURI, nullptr, nullptr, nullptr, 0, loadGroup, this, nullptr,
+ nullptr, nsIRequest::LOAD_NORMAL, nullptr,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE, u""_ns,
+ /* aUseUrgentStartForChannel */ false, /* aLinkPreload */ false, 0,
+ getter_AddRefs(mIconRequest));
+ } else {
+ // TODO: nsIContentPolicy::TYPE_INTERNAL_IMAGE may not be the correct
+ // policy. See bug 1691868 for more details.
+ rv = loader->LoadImage(
+ aIconURI, nullptr, nullptr, aNode->NodePrincipal(), 0, loadGroup, this,
+ aNode, document, nsIRequest::LOAD_NORMAL, nullptr,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE, u""_ns,
+ /* aUseUrgentStartForChannel */ false,
+ /* aLinkPreload */ false, 0, getter_AddRefs(mIconRequest));
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+//
+// imgINotificationObserver
+//
+
+void IconLoader::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) {
+ if (aType == imgINotificationObserver::LOAD_COMPLETE) {
+ // Make sure the image loaded successfully.
+ uint32_t status = imgIRequest::STATUS_ERROR;
+ if (NS_FAILED(aRequest->GetImageStatus(&status)) ||
+ (status & imgIRequest::STATUS_ERROR)) {
+ mIconRequest->CancelWithReason(NS_BINDING_ABORTED,
+ "GetImageStatus failed"_ns);
+ mIconRequest = nullptr;
+ return;
+ }
+
+ nsCOMPtr<imgIContainer> image;
+ aRequest->GetImage(getter_AddRefs(image));
+ MOZ_ASSERT(image);
+
+ // Ask the image to decode at its intrinsic size.
+ int32_t width = 0, height = 0;
+ image->GetWidth(&width);
+ image->GetHeight(&height);
+ image->RequestDecodeForSize(nsIntSize(width, height),
+ imgIContainer::FLAG_HIGH_QUALITY_SCALING);
+ }
+
+ if (aType == imgINotificationObserver::FRAME_COMPLETE) {
+ nsCOMPtr<imgIContainer> image;
+ aRequest->GetImage(getter_AddRefs(image));
+ MOZ_ASSERT(image);
+
+ if (mListener) {
+ mListener->OnComplete(image);
+ }
+ return;
+ }
+
+ if (aType == imgINotificationObserver::DECODE_COMPLETE) {
+ if (mIconRequest && mIconRequest == aRequest) {
+ mIconRequest->CancelWithReason(NS_BINDING_ABORTED, "DECODE_COMPLETE"_ns);
+ mIconRequest = nullptr;
+ }
+ }
+}
+
+} // namespace mozilla::widget
diff --git a/widget/IconLoader.h b/widget/IconLoader.h
new file mode 100644
index 0000000000..f665df6fc5
--- /dev/null
+++ b/widget/IconLoader.h
@@ -0,0 +1,69 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_IconLoader_h_
+#define mozilla_widget_IconLoader_h_
+
+#include "imgINotificationObserver.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+
+class nsIURI;
+class nsINode;
+class imgRequestProxy;
+class imgIContainer;
+
+namespace mozilla::widget {
+
+/**
+ * IconLoader is a utility for loading icons from either the disk or over
+ * the network, and then using them in native OS widgetry like the macOS
+ * global menu bar or the Windows notification area.
+ */
+
+class IconLoader : public imgINotificationObserver {
+ public:
+ // This is the interface that our listeners need to implement so that they can
+ // be notified when the icon is loaded.
+ class Listener {
+ public:
+ virtual nsresult OnComplete(imgIContainer* aContainer) = 0;
+ };
+
+ // Create the loader.
+ // aListener will be notified when the load is complete.
+ // The loader does not keep an owning reference to the listener. Call Destroy
+ // before the listener goes away.
+ explicit IconLoader(Listener* aListener);
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+
+ // LoadIcon will start a load request for the icon.
+ // The request may not complete until after LoadIcon returns.
+ // If aIsInternalIcon is true, the document and principal will not be
+ // used when loading.
+ nsresult LoadIcon(nsIURI* aIconURI, nsINode* aNode,
+ bool aIsInternalIcon = false);
+
+ void Destroy();
+
+ protected:
+ virtual ~IconLoader();
+
+ private:
+ RefPtr<imgRequestProxy> mIconRequest;
+
+ // The listener, which is notified when loading completes.
+ // Can be null, after a call to Destroy.
+ // This is a non-owning reference and needs to be cleared with a call to
+ // Destroy before the listener goes away.
+ Listener* mListener;
+};
+
+} // namespace mozilla::widget
+#endif // mozilla_widget_IconLoader_h_
diff --git a/widget/InProcessCompositorWidget.cpp b/widget/InProcessCompositorWidget.cpp
new file mode 100644
index 0000000000..1c25e9d9a1
--- /dev/null
+++ b/widget/InProcessCompositorWidget.cpp
@@ -0,0 +1,136 @@
+/* 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 "InProcessCompositorWidget.h"
+
+#include "mozilla/VsyncDispatcher.h"
+#include "nsBaseWidget.h"
+
+namespace mozilla {
+namespace widget {
+
+// Platforms with no OOP compositor process support use
+// InProcessCompositorWidget by default.
+#if !defined(MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING)
+/* static */
+RefPtr<CompositorWidget> CompositorWidget::CreateLocal(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsIWidget* aWidget) {
+ // We're getting crashes from storing a NULL mWidget, and this is the
+ // only remaining explanation that doesn't involve memory corruption,
+ // so placing a release assert here. For even more sanity-checking, we
+ // do it after the static_cast.
+ nsBaseWidget* widget = static_cast<nsBaseWidget*>(aWidget);
+ MOZ_RELEASE_ASSERT(widget);
+ return new InProcessCompositorWidget(aOptions, widget);
+}
+#endif
+
+InProcessCompositorWidget::InProcessCompositorWidget(
+ const layers::CompositorOptions& aOptions, nsBaseWidget* aWidget)
+ : CompositorWidget(aOptions),
+ mWidget(aWidget),
+ mCanary(CANARY_VALUE),
+ mWidgetSanity(aWidget) {
+ // The only method of construction that is used outside of unit tests is
+ // ::CreateLocal, above. That method of construction asserts that mWidget
+ // is not assigned a NULL value. And yet mWidget is NULL in some crash
+ // reports that involve other class methods. Adding a release assert here
+ // will give us the earliest possible notification that we're headed for
+ // a crash.
+ MOZ_RELEASE_ASSERT(mWidget);
+}
+
+bool InProcessCompositorWidget::PreRender(WidgetRenderingContext* aContext) {
+ CheckWidgetSanity();
+ return mWidget->PreRender(aContext);
+}
+
+void InProcessCompositorWidget::PostRender(WidgetRenderingContext* aContext) {
+ CheckWidgetSanity();
+ mWidget->PostRender(aContext);
+}
+
+RefPtr<layers::NativeLayerRoot>
+InProcessCompositorWidget::GetNativeLayerRoot() {
+ CheckWidgetSanity();
+ return mWidget->GetNativeLayerRoot();
+}
+
+already_AddRefed<gfx::DrawTarget>
+InProcessCompositorWidget::StartRemoteDrawing() {
+ CheckWidgetSanity();
+ return mWidget->StartRemoteDrawing();
+}
+
+already_AddRefed<gfx::DrawTarget>
+InProcessCompositorWidget::StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) {
+ CheckWidgetSanity();
+ return mWidget->StartRemoteDrawingInRegion(aInvalidRegion, aBufferMode);
+}
+
+void InProcessCompositorWidget::EndRemoteDrawing() {
+ CheckWidgetSanity();
+ mWidget->EndRemoteDrawing();
+}
+
+void InProcessCompositorWidget::EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
+ CheckWidgetSanity();
+ mWidget->EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion);
+}
+
+void InProcessCompositorWidget::CleanupRemoteDrawing() {
+ CheckWidgetSanity();
+ mWidget->CleanupRemoteDrawing();
+}
+
+void InProcessCompositorWidget::CleanupWindowEffects() {
+ CheckWidgetSanity();
+ mWidget->CleanupWindowEffects();
+}
+
+bool InProcessCompositorWidget::InitCompositor(
+ layers::Compositor* aCompositor) {
+ CheckWidgetSanity();
+ return mWidget->InitCompositor(aCompositor);
+}
+
+LayoutDeviceIntSize InProcessCompositorWidget::GetClientSize() {
+ CheckWidgetSanity();
+ return mWidget->GetClientSize();
+}
+
+uint32_t InProcessCompositorWidget::GetGLFrameBufferFormat() {
+ CheckWidgetSanity();
+ return mWidget->GetGLFrameBufferFormat();
+}
+
+uintptr_t InProcessCompositorWidget::GetWidgetKey() {
+ CheckWidgetSanity();
+ return reinterpret_cast<uintptr_t>(mWidget);
+}
+
+nsIWidget* InProcessCompositorWidget::RealWidget() { return mWidget; }
+
+void InProcessCompositorWidget::ObserveVsync(VsyncObserver* aObserver) {
+ CheckWidgetSanity();
+ if (RefPtr<CompositorVsyncDispatcher> cvd =
+ mWidget->GetCompositorVsyncDispatcher()) {
+ cvd->SetCompositorVsyncObserver(aObserver);
+ }
+}
+
+const char* InProcessCompositorWidget::CANARY_VALUE =
+ reinterpret_cast<char*>(0x1a1a1a1a);
+
+void InProcessCompositorWidget::CheckWidgetSanity() {
+ MOZ_RELEASE_ASSERT(mWidgetSanity == mWidget);
+ MOZ_RELEASE_ASSERT(mCanary == CANARY_VALUE);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/InProcessCompositorWidget.h b/widget/InProcessCompositorWidget.h
new file mode 100644
index 0000000000..eb44668de9
--- /dev/null
+++ b/widget/InProcessCompositorWidget.h
@@ -0,0 +1,55 @@
+/* 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/. */
+
+#ifndef mozilla_widget_InProcessCompositorWidget_h__
+#define mozilla_widget_InProcessCompositorWidget_h__
+
+#include "CompositorWidget.h"
+
+namespace mozilla {
+namespace widget {
+
+// This version of CompositorWidget implements a wrapper around
+// nsBaseWidget.
+class InProcessCompositorWidget : public CompositorWidget {
+ public:
+ explicit InProcessCompositorWidget(const layers::CompositorOptions& aOptions,
+ nsBaseWidget* aWidget);
+
+ virtual bool PreRender(WidgetRenderingContext* aManager) override;
+ virtual void PostRender(WidgetRenderingContext* aManager) override;
+ virtual RefPtr<layers::NativeLayerRoot> GetNativeLayerRoot() override;
+ virtual already_AddRefed<gfx::DrawTarget> StartRemoteDrawing() override;
+ virtual already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) override;
+ virtual void EndRemoteDrawing() override;
+ virtual void EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget,
+ const LayoutDeviceIntRegion& aInvalidRegion) override;
+ virtual void CleanupRemoteDrawing() override;
+ virtual void CleanupWindowEffects() override;
+ virtual bool InitCompositor(layers::Compositor* aCompositor) override;
+ virtual LayoutDeviceIntSize GetClientSize() override;
+ virtual uint32_t GetGLFrameBufferFormat() override;
+ virtual void ObserveVsync(VsyncObserver* aObserver) override;
+ virtual uintptr_t GetWidgetKey() override;
+
+ // If you can override this method, inherit from CompositorWidget instead.
+ nsIWidget* RealWidget() override;
+
+ protected:
+ nsBaseWidget* mWidget;
+ // Bug 1679368: Maintain an additional widget pointer, constant, and
+ // function for sanity checking while we chase a crash.
+ static const char* CANARY_VALUE;
+ const char* mCanary;
+ nsBaseWidget* mWidgetSanity;
+ void CheckWidgetSanity();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/InitData.h b/widget/InitData.h
new file mode 100644
index 0000000000..1498feed9a
--- /dev/null
+++ b/widget/InitData.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_widget_InitData_h__
+#define mozilla_widget_InitData_h__
+
+#include <cstdint>
+#include "mozilla/TypedEnumBits.h"
+#include "X11UndefineNone.h"
+
+namespace mozilla::widget {
+
+// Window types
+enum class WindowType : uint8_t {
+ TopLevel, // default top level window
+ Dialog, // top level window but usually handled differently
+ // by the OS
+ Sheet, // MacOSX sheet (special dialog class)
+ Popup, // used for combo boxes, etc
+ Child, // child windows (contained inside a window on the
+ // desktop (has no border))
+ Invisible, // windows that are invisible or offscreen
+};
+
+// Popup types for WindowType::Popup
+enum class PopupType : uint8_t {
+ Panel,
+ Menu,
+ Tooltip,
+ Any, // used only to pass to nsXULPopupManager::GetTopPopup
+};
+
+// Popup levels specify the window ordering behaviour.
+enum class PopupLevel : uint8_t {
+ // The popup appears just above its parent and maintains its position
+ // relative to the parent.
+ Parent,
+ // The popup is a floating popup used for tool palettes. A parent window must
+ // be specified, but a platform implementation need not use this. On Windows,
+ // floating is generally equivalent to parent. On Mac, floating puts the
+ // popup at toplevel, but it will hide when the application is deactivated.
+ Floating,
+ // The popup appears on top of other windows, including those of other
+ // applications.
+ Top,
+};
+
+// Border styles
+enum class BorderStyle : int16_t {
+ None = 0, // no border, titlebar, etc.. opposite of all
+ All = 1 << 0, // all window decorations
+ Border = 1 << 1, // enables the border on the window. these
+ // are only for decoration and are not
+ // resize handles
+ ResizeH = 1 << 2, // enables the resize handles for the
+ // window. if this is set, border is
+ // implied to also be set
+ Title = 1 << 3, // enables the titlebar for the window
+ Menu = 1 << 4, // enables the window menu button on the
+ // title bar. this being on should force
+ // the title bar to display
+ Minimize = 1 << 5, // enables the minimize button so the user
+ // can minimize the window. turned off for
+ // tranient windows since they can not be
+ // minimized separate from their parent
+ Maximize = 1 << 6, // enables the maxmize button so the user
+ // can maximize the window
+ Close = 1 << 7, // show the close button
+ Default = -1 // whatever the OS wants... i.e. don't do anything
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(BorderStyle)
+
+enum class TransparencyMode : uint8_t {
+ Opaque = 0, // Fully opaque
+ Transparent, // Parts of the window may be transparent
+ // If you add to the end here, you must update the serialization code in
+ // WidgetMessageUtils.h
+};
+
+// Basic struct for widget initialization data.
+// @see Create member function of nsIWidget
+struct InitData {
+ WindowType mWindowType = WindowType::Child;
+ BorderStyle mBorderStyle = BorderStyle::Default;
+ PopupType mPopupHint = PopupType::Panel;
+ PopupLevel mPopupLevel = PopupLevel::Top;
+ TransparencyMode mTransparencyMode = TransparencyMode::Opaque;
+ // when painting exclude area occupied by child windows and sibling windows
+ bool mClipChildren = false;
+ bool mClipSiblings = false;
+ bool mRTL = false;
+ bool mNoAutoHide = false; // true for noautohide panels
+ bool mIsDragPopup = false; // true for drag feedback panels
+ // true if window creation animation is suppressed, e.g. for session restore
+ bool mIsAnimationSuppressed = false;
+ // true if the window should support an alpha channel, if available.
+ bool mHasRemoteContent = false;
+ bool mAlwaysOnTop = false;
+ // Whether we're a PictureInPicture window
+ bool mPIPWindow = false;
+ // True if the window is user-resizable.
+ bool mResizable = false;
+ bool mIsPrivate = false;
+ // True if the window is an alert / notification.
+ bool mIsAlert = false;
+};
+
+} // namespace mozilla::widget
+
+#endif // mozilla_widget_InitData
diff --git a/widget/InputData.cpp b/widget/InputData.cpp
new file mode 100644
index 0000000000..3d1695dd8e
--- /dev/null
+++ b/widget/InputData.cpp
@@ -0,0 +1,924 @@
+/* -*- 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 "InputData.h"
+
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/Touch.h"
+#include "mozilla/dom/WheelEventBinding.h"
+#include "mozilla/TextEvents.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsThreadUtils.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/SwipeTracker.h"
+#include "UnitTransforms.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+InputData::~InputData() = default;
+
+InputData::InputData(InputType aInputType)
+ : mInputType(aInputType),
+ mFocusSequenceNumber(0),
+ mLayersId{0},
+ modifiers(0) {}
+
+InputData::InputData(InputType aInputType, TimeStamp aTimeStamp,
+ Modifiers aModifiers)
+ : mInputType(aInputType),
+ mTimeStamp(aTimeStamp),
+ mFocusSequenceNumber(0),
+ mLayersId{0},
+ modifiers(aModifiers) {}
+
+SingleTouchData::SingleTouchData(int32_t aIdentifier,
+ ScreenIntPoint aScreenPoint,
+ ScreenSize aRadius, float aRotationAngle,
+ float aForce)
+ : mIdentifier(aIdentifier),
+ mScreenPoint(aScreenPoint),
+ mRadius(aRadius),
+ mRotationAngle(aRotationAngle),
+ mForce(aForce) {}
+
+SingleTouchData::SingleTouchData(int32_t aIdentifier,
+ ParentLayerPoint aLocalScreenPoint,
+ ScreenSize aRadius, float aRotationAngle,
+ float aForce)
+ : mIdentifier(aIdentifier),
+ mLocalScreenPoint(aLocalScreenPoint),
+ mRadius(aRadius),
+ mRotationAngle(aRotationAngle),
+ mForce(aForce) {}
+
+SingleTouchData::SingleTouchData()
+ : mIdentifier(0), mRotationAngle(0.0), mForce(0.0) {}
+
+already_AddRefed<Touch> SingleTouchData::ToNewDOMTouch() const {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Can only create dom::Touch instances on main thread");
+ RefPtr<Touch> touch =
+ new Touch(mIdentifier,
+ LayoutDeviceIntPoint::Truncate(mScreenPoint.x, mScreenPoint.y),
+ LayoutDeviceIntPoint::Truncate(mRadius.width, mRadius.height),
+ mRotationAngle, mForce);
+ touch->tiltX = mTiltX;
+ touch->tiltY = mTiltY;
+ touch->twist = mTwist;
+ return touch.forget();
+}
+
+MultiTouchInput::MultiTouchInput(MultiTouchType aType, uint32_t aTime,
+ TimeStamp aTimeStamp, Modifiers aModifiers)
+ : InputData(MULTITOUCH_INPUT, aTimeStamp, aModifiers),
+ mType(aType),
+ mHandledByAPZ(false) {}
+
+MultiTouchInput::MultiTouchInput()
+ : InputData(MULTITOUCH_INPUT),
+ mType(MULTITOUCH_START),
+ mHandledByAPZ(false) {}
+
+MultiTouchInput::MultiTouchInput(const WidgetTouchEvent& aTouchEvent)
+ : InputData(MULTITOUCH_INPUT, aTouchEvent.mTimeStamp,
+ aTouchEvent.mModifiers),
+ mHandledByAPZ(aTouchEvent.mFlags.mHandledByAPZ),
+ mButton(aTouchEvent.mButton),
+ mButtons(aTouchEvent.mButtons) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Can only copy from WidgetTouchEvent on main thread");
+
+ switch (aTouchEvent.mMessage) {
+ case eTouchStart:
+ mType = MULTITOUCH_START;
+ break;
+ case eTouchMove:
+ mType = MULTITOUCH_MOVE;
+ break;
+ case eTouchEnd:
+ mType = MULTITOUCH_END;
+ break;
+ case eTouchCancel:
+ mType = MULTITOUCH_CANCEL;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Did not assign a type to a MultiTouchInput");
+ break;
+ }
+
+ mScreenOffset = ViewAs<ExternalPixel>(
+ aTouchEvent.mWidget->WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ for (size_t i = 0; i < aTouchEvent.mTouches.Length(); i++) {
+ const Touch* domTouch = aTouchEvent.mTouches[i];
+
+ // Extract data from weird interfaces.
+ int32_t identifier = domTouch->Identifier();
+ int32_t radiusX = domTouch->RadiusX(CallerType::System);
+ int32_t radiusY = domTouch->RadiusY(CallerType::System);
+ float rotationAngle = domTouch->RotationAngle(CallerType::System);
+ float force = domTouch->Force(CallerType::System);
+
+ SingleTouchData data(
+ identifier,
+ ViewAs<ScreenPixel>(
+ domTouch->mRefPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent),
+ ScreenSize((float)radiusX, (float)radiusY), rotationAngle, force);
+
+ mTouches.AppendElement(data);
+ }
+}
+
+void MultiTouchInput::Translate(const ScreenPoint& aTranslation) {
+ ScreenIntPoint translation = RoundedToInt(aTranslation);
+
+ for (auto& touchData : mTouches) {
+ for (auto& historicalData : touchData.mHistoricalData) {
+ historicalData.mScreenPoint.MoveBy(translation.x, translation.y);
+ }
+ touchData.mScreenPoint.MoveBy(translation.x, translation.y);
+ }
+}
+
+WidgetTouchEvent MultiTouchInput::ToWidgetEvent(nsIWidget* aWidget,
+ uint16_t aInputSource) const {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Can only convert To WidgetTouchEvent on main thread");
+ MOZ_ASSERT(aInputSource ==
+ mozilla::dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH ||
+ aInputSource == mozilla::dom::MouseEvent_Binding::MOZ_SOURCE_PEN);
+
+ EventMessage touchEventMessage = eVoidEvent;
+ switch (mType) {
+ case MULTITOUCH_START:
+ touchEventMessage = eTouchStart;
+ break;
+ case MULTITOUCH_MOVE:
+ touchEventMessage = eTouchMove;
+ break;
+ case MULTITOUCH_END:
+ touchEventMessage = eTouchEnd;
+ break;
+ case MULTITOUCH_CANCEL:
+ touchEventMessage = eTouchCancel;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Did not assign a type to WidgetTouchEvent in MultiTouchInput");
+ break;
+ }
+
+ WidgetTouchEvent event(true, touchEventMessage, aWidget);
+ if (touchEventMessage == eVoidEvent) {
+ return event;
+ }
+
+ event.mModifiers = this->modifiers;
+ event.mTimeStamp = this->mTimeStamp;
+ event.mFlags.mHandledByAPZ = mHandledByAPZ;
+ event.mFocusSequenceNumber = mFocusSequenceNumber;
+ event.mLayersId = mLayersId;
+ event.mInputSource = aInputSource;
+ event.mButton = mButton;
+ event.mButtons = mButtons;
+
+ for (size_t i = 0; i < mTouches.Length(); i++) {
+ *event.mTouches.AppendElement() = mTouches[i].ToNewDOMTouch();
+ }
+
+ return event;
+}
+
+int32_t MultiTouchInput::IndexOfTouch(int32_t aTouchIdentifier) {
+ for (size_t i = 0; i < mTouches.Length(); i++) {
+ if (mTouches[i].mIdentifier == aTouchIdentifier) {
+ return (int32_t)i;
+ }
+ }
+ return -1;
+}
+
+bool MultiTouchInput::TransformToLocal(
+ const ScreenToParentLayerMatrix4x4& aTransform) {
+ for (auto& touchData : mTouches) {
+ for (auto& historicalData : touchData.mHistoricalData) {
+ Maybe<ParentLayerIntPoint> historicalPoint =
+ UntransformBy(aTransform, historicalData.mScreenPoint);
+ if (!historicalPoint) {
+ return false;
+ }
+ historicalData.mLocalScreenPoint = *historicalPoint;
+ }
+ Maybe<ParentLayerIntPoint> point =
+ UntransformBy(aTransform, touchData.mScreenPoint);
+ if (!point) {
+ return false;
+ }
+ touchData.mLocalScreenPoint = *point;
+ }
+ return true;
+}
+
+MouseInput::MouseInput()
+ : InputData(MOUSE_INPUT),
+ mType(MOUSE_NONE),
+ mButtonType(NONE),
+ mInputSource(0),
+ mButtons(0),
+ mHandledByAPZ(false),
+ mPreventClickEvent(false) {}
+
+MouseInput::MouseInput(MouseType aType, ButtonType aButtonType,
+ uint16_t aInputSource, int16_t aButtons,
+ const ScreenPoint& aPoint, TimeStamp aTimeStamp,
+ Modifiers aModifiers)
+ : InputData(MOUSE_INPUT, aTimeStamp, aModifiers),
+ mType(aType),
+ mButtonType(aButtonType),
+ mInputSource(aInputSource),
+ mButtons(aButtons),
+ mOrigin(aPoint),
+ mHandledByAPZ(false),
+ mPreventClickEvent(false) {}
+
+MouseInput::MouseInput(const WidgetMouseEventBase& aMouseEvent)
+ : InputData(MOUSE_INPUT, aMouseEvent.mTimeStamp, aMouseEvent.mModifiers),
+ mType(MOUSE_NONE),
+ mButtonType(NONE),
+ mInputSource(aMouseEvent.mInputSource),
+ mButtons(aMouseEvent.mButtons),
+ mHandledByAPZ(aMouseEvent.mFlags.mHandledByAPZ),
+ mPreventClickEvent(aMouseEvent.mClass == eMouseEventClass &&
+ aMouseEvent.AsMouseEvent()->mClickEventPrevented) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Can only copy from WidgetTouchEvent on main thread");
+
+ mButtonType = NONE;
+
+ switch (aMouseEvent.mButton) {
+ case MouseButton::ePrimary:
+ mButtonType = MouseInput::PRIMARY_BUTTON;
+ break;
+ case MouseButton::eMiddle:
+ mButtonType = MouseInput::MIDDLE_BUTTON;
+ break;
+ case MouseButton::eSecondary:
+ mButtonType = MouseInput::SECONDARY_BUTTON;
+ break;
+ }
+
+ switch (aMouseEvent.mMessage) {
+ case eMouseMove:
+ mType = MOUSE_MOVE;
+ break;
+ case eMouseUp:
+ mType = MOUSE_UP;
+ break;
+ case eMouseDown:
+ mType = MOUSE_DOWN;
+ break;
+ case eDragStart:
+ mType = MOUSE_DRAG_START;
+ break;
+ case eDragEnd:
+ mType = MOUSE_DRAG_END;
+ break;
+ case eMouseEnterIntoWidget:
+ mType = MOUSE_WIDGET_ENTER;
+ break;
+ case eMouseExitFromWidget:
+ mType = MOUSE_WIDGET_EXIT;
+ break;
+ case eMouseExploreByTouch:
+ mType = MOUSE_EXPLORE_BY_TOUCH;
+ break;
+ case eMouseHitTest:
+ mType = MOUSE_HITTEST;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Mouse event type not supported");
+ break;
+ }
+
+ mOrigin = ScreenPoint(ViewAs<ScreenPixel>(
+ aMouseEvent.mRefPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+}
+
+bool MouseInput::IsLeftButton() const { return mButtonType == PRIMARY_BUTTON; }
+
+bool MouseInput::TransformToLocal(
+ const ScreenToParentLayerMatrix4x4& aTransform) {
+ Maybe<ParentLayerPoint> point = UntransformBy(aTransform, mOrigin);
+ if (!point) {
+ return false;
+ }
+ mLocalOrigin = *point;
+
+ return true;
+}
+
+WidgetMouseEvent MouseInput::ToWidgetEvent(nsIWidget* aWidget) const {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Can only convert To WidgetTouchEvent on main thread");
+
+ EventMessage msg = eVoidEvent;
+ uint32_t clickCount = 0;
+ Maybe<WidgetMouseEvent::ExitFrom> exitFrom;
+ switch (mType) {
+ case MOUSE_MOVE:
+ msg = eMouseMove;
+ break;
+ case MOUSE_UP:
+ msg = eMouseUp;
+ clickCount = 1;
+ break;
+ case MOUSE_DOWN:
+ msg = eMouseDown;
+ clickCount = 1;
+ break;
+ case MOUSE_DRAG_START:
+ msg = eDragStart;
+ break;
+ case MOUSE_DRAG_END:
+ msg = eDragEnd;
+ break;
+ case MOUSE_WIDGET_ENTER:
+ msg = eMouseEnterIntoWidget;
+ break;
+ case MOUSE_WIDGET_EXIT:
+ msg = eMouseExitFromWidget;
+ exitFrom = Some(WidgetMouseEvent::ePlatformChild);
+ break;
+ case MOUSE_EXPLORE_BY_TOUCH:
+ msg = eMouseExploreByTouch;
+ break;
+ case MOUSE_HITTEST:
+ msg = eMouseHitTest;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Did not assign a type to WidgetMouseEvent in MouseInput");
+ break;
+ }
+
+ WidgetMouseEvent event(true, msg, aWidget, WidgetMouseEvent::eReal,
+ WidgetMouseEvent::eNormal);
+
+ if (msg == eVoidEvent) {
+ return event;
+ }
+
+ switch (mButtonType) {
+ case MouseInput::PRIMARY_BUTTON:
+ event.mButton = MouseButton::ePrimary;
+ break;
+ case MouseInput::MIDDLE_BUTTON:
+ event.mButton = MouseButton::eMiddle;
+ break;
+ case MouseInput::SECONDARY_BUTTON:
+ event.mButton = MouseButton::eSecondary;
+ break;
+ case MouseInput::NONE:
+ default:
+ break;
+ }
+
+ event.mButtons = mButtons;
+ event.mModifiers = modifiers;
+ event.mTimeStamp = mTimeStamp;
+ event.mLayersId = mLayersId;
+ event.mFlags.mHandledByAPZ = mHandledByAPZ;
+ event.mRefPoint = RoundedToInt(ViewAs<LayoutDevicePixel>(
+ mOrigin,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+ event.mClickCount = clickCount;
+ event.mInputSource = mInputSource;
+ event.mFocusSequenceNumber = mFocusSequenceNumber;
+ event.mExitFrom = exitFrom;
+ event.mClickEventPrevented = mPreventClickEvent;
+
+ return event;
+}
+
+PanGestureInput::PanGestureInput()
+ : InputData(PANGESTURE_INPUT),
+ mType(PANGESTURE_MAYSTART),
+ mLineOrPageDeltaX(0),
+ mLineOrPageDeltaY(0),
+ mUserDeltaMultiplierX(1.0),
+ mUserDeltaMultiplierY(1.0),
+ mHandledByAPZ(false),
+ mOverscrollBehaviorAllowsSwipe(false),
+ mSimulateMomentum(false),
+ mIsNoLineOrPageDelta(true),
+ mMayTriggerSwipe(false) {}
+
+PanGestureInput::PanGestureInput(PanGestureType aType, TimeStamp aTimeStamp,
+ const ScreenPoint& aPanStartPoint,
+ const ScreenPoint& aPanDisplacement,
+ Modifiers aModifiers)
+ : InputData(PANGESTURE_INPUT, aTimeStamp, aModifiers),
+ mType(aType),
+ mPanStartPoint(aPanStartPoint),
+ mPanDisplacement(aPanDisplacement),
+ mLineOrPageDeltaX(0),
+ mLineOrPageDeltaY(0),
+ mUserDeltaMultiplierX(1.0),
+ mUserDeltaMultiplierY(1.0),
+ mHandledByAPZ(false),
+ mOverscrollBehaviorAllowsSwipe(false),
+ mSimulateMomentum(false),
+ mIsNoLineOrPageDelta(true) {
+ mMayTriggerSwipe = SwipeTracker::CanTriggerSwipe(*this);
+}
+
+PanGestureInput::PanGestureInput(PanGestureType aType, TimeStamp aTimeStamp,
+ const ScreenPoint& aPanStartPoint,
+ const ScreenPoint& aPanDisplacement,
+ Modifiers aModifiers,
+ IsEligibleForSwipe aIsEligibleForSwipe)
+ : PanGestureInput(aType, aTimeStamp, aPanStartPoint, aPanDisplacement,
+ aModifiers) {
+ mMayTriggerSwipe &= bool(aIsEligibleForSwipe);
+}
+
+void PanGestureInput::SetLineOrPageDeltas(int32_t aLineOrPageDeltaX,
+ int32_t aLineOrPageDeltaY) {
+ mLineOrPageDeltaX = aLineOrPageDeltaX;
+ mLineOrPageDeltaY = aLineOrPageDeltaY;
+ mIsNoLineOrPageDelta = false;
+}
+
+bool PanGestureInput::IsMomentum() const {
+ switch (mType) {
+ case PanGestureInput::PANGESTURE_MOMENTUMSTART:
+ case PanGestureInput::PANGESTURE_MOMENTUMPAN:
+ case PanGestureInput::PANGESTURE_MOMENTUMEND:
+ return true;
+ default:
+ return false;
+ }
+}
+
+WidgetWheelEvent PanGestureInput::ToWidgetEvent(nsIWidget* aWidget) const {
+ WidgetWheelEvent wheelEvent(true, eWheel, aWidget);
+ wheelEvent.mModifiers = this->modifiers;
+ wheelEvent.mTimeStamp = mTimeStamp;
+ wheelEvent.mLayersId = mLayersId;
+ wheelEvent.mRefPoint = RoundedToInt(ViewAs<LayoutDevicePixel>(
+ mPanStartPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+ wheelEvent.mButtons = 0;
+ wheelEvent.mMayHaveMomentum = true; // pan inputs may have momentum
+ wheelEvent.mIsMomentum = IsMomentum();
+ wheelEvent.mLineOrPageDeltaX = mLineOrPageDeltaX;
+ wheelEvent.mLineOrPageDeltaY = mLineOrPageDeltaY;
+ wheelEvent.mDeltaX = mPanDisplacement.x;
+ wheelEvent.mDeltaY = mPanDisplacement.y;
+ wheelEvent.mFlags.mHandledByAPZ = mHandledByAPZ;
+ wheelEvent.mFocusSequenceNumber = mFocusSequenceNumber;
+ wheelEvent.mIsNoLineOrPageDelta = mIsNoLineOrPageDelta;
+ if (mDeltaType == PanGestureInput::PANDELTA_PAGE) {
+ // widget/gtk is currently the only consumer that uses delta type
+ // PANDELTA_PAGE
+ // Emulate legacy widget/gtk behavior
+ wheelEvent.mDeltaMode = WheelEvent_Binding::DOM_DELTA_LINE;
+ wheelEvent.mScrollType = WidgetWheelEvent::SCROLL_ASYNCHRONOUSLY;
+ wheelEvent.mDeltaX *= 3;
+ wheelEvent.mDeltaY *= 3;
+ } else {
+ wheelEvent.mDeltaMode = WheelEvent_Binding::DOM_DELTA_PIXEL;
+ }
+ return wheelEvent;
+}
+
+bool PanGestureInput::TransformToLocal(
+ const ScreenToParentLayerMatrix4x4& aTransform) {
+ Maybe<ParentLayerPoint> panStartPoint =
+ UntransformBy(aTransform, mPanStartPoint);
+ if (!panStartPoint) {
+ return false;
+ }
+ mLocalPanStartPoint = *panStartPoint;
+
+ if (mDeltaType == PanGestureInput::PANDELTA_PAGE) {
+ // Skip transforming the pan displacement because we want
+ // raw page proportion counts.
+ mLocalPanDisplacement = ViewAs<ParentLayerPixel>(
+ mPanDisplacement, PixelCastJustification::DeltaIsPageProportion);
+ return true;
+ }
+
+ Maybe<ParentLayerPoint> panDisplacement =
+ UntransformVector(aTransform, mPanDisplacement, mPanStartPoint);
+ if (!panDisplacement) {
+ return false;
+ }
+ mLocalPanDisplacement = *panDisplacement;
+ return true;
+}
+
+ScreenPoint PanGestureInput::UserMultipliedPanDisplacement() const {
+ return ScreenPoint(mPanDisplacement.x * mUserDeltaMultiplierX,
+ mPanDisplacement.y * mUserDeltaMultiplierY);
+}
+
+ParentLayerPoint PanGestureInput::UserMultipliedLocalPanDisplacement() const {
+ return ParentLayerPoint(mLocalPanDisplacement.x * mUserDeltaMultiplierX,
+ mLocalPanDisplacement.y * mUserDeltaMultiplierY);
+}
+
+static int32_t TakeLargestInt(gfx::Coord* aCoord) {
+ int32_t result(aCoord->value); // truncate towards zero
+ aCoord->value -= result;
+ return result;
+}
+
+/* static */ gfx::IntPoint PanGestureInput::GetIntegerDeltaForEvent(
+ bool aIsStart, float x, float y) {
+ static gfx::Point sAccumulator(0.0f, 0.0f);
+ if (aIsStart) {
+ sAccumulator = gfx::Point(0.0f, 0.0f);
+ }
+ sAccumulator.x += x;
+ sAccumulator.y += y;
+ return gfx::IntPoint(TakeLargestInt(&sAccumulator.x),
+ TakeLargestInt(&sAccumulator.y));
+}
+
+PinchGestureInput::PinchGestureInput()
+ : InputData(PINCHGESTURE_INPUT),
+ mType(PINCHGESTURE_START),
+ mSource(UNKNOWN),
+ mHandledByAPZ(false) {}
+
+PinchGestureInput::PinchGestureInput(
+ PinchGestureType aType, PinchGestureSource aSource, TimeStamp aTimeStamp,
+ const ExternalPoint& aScreenOffset, const ScreenPoint& aFocusPoint,
+ ScreenCoord aCurrentSpan, ScreenCoord aPreviousSpan, Modifiers aModifiers)
+ : InputData(PINCHGESTURE_INPUT, aTimeStamp, aModifiers),
+ mType(aType),
+ mSource(aSource),
+ mFocusPoint(aFocusPoint),
+ mScreenOffset(aScreenOffset),
+ mCurrentSpan(aCurrentSpan),
+ mPreviousSpan(aPreviousSpan),
+ mLineOrPageDeltaY(0),
+ mHandledByAPZ(false) {}
+
+bool PinchGestureInput::TransformToLocal(
+ const ScreenToParentLayerMatrix4x4& aTransform) {
+ Maybe<ParentLayerPoint> point = UntransformBy(aTransform, mFocusPoint);
+ if (!point) {
+ return false;
+ }
+ mLocalFocusPoint = *point;
+ return true;
+}
+
+WidgetWheelEvent PinchGestureInput::ToWidgetEvent(nsIWidget* aWidget) const {
+ WidgetWheelEvent wheelEvent(true, eWheel, aWidget);
+ wheelEvent.mModifiers = this->modifiers | MODIFIER_CONTROL;
+ wheelEvent.mTimeStamp = mTimeStamp;
+ wheelEvent.mLayersId = mLayersId;
+ wheelEvent.mRefPoint = RoundedToInt(ViewAs<LayoutDevicePixel>(
+ mFocusPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+ wheelEvent.mButtons = 0;
+ wheelEvent.mFlags.mHandledByAPZ = mHandledByAPZ;
+ wheelEvent.mDeltaMode = WheelEvent_Binding::DOM_DELTA_PIXEL;
+
+ wheelEvent.mDeltaY = ComputeDeltaY(aWidget);
+
+ wheelEvent.mLineOrPageDeltaY = mLineOrPageDeltaY;
+
+ MOZ_ASSERT(mType == PINCHGESTURE_END || wheelEvent.mDeltaY != 0.0);
+
+ return wheelEvent;
+}
+
+double PinchGestureInput::ComputeDeltaY(nsIWidget* aWidget) const {
+#if defined(XP_DARWIN)
+ // This converts the pinch gesture value to a fake wheel event that has the
+ // control key pressed so that pages can implement custom pinch gesture
+ // handling. It may seem strange that this doesn't use a wheel event with
+ // the deltaZ property set, but this matches Chrome's behavior as described
+ // at https://code.google.com/p/chromium/issues/detail?id=289887
+ //
+ // The intent of the formula below is to produce numbers similar to Chrome's
+ // implementation of this feature. Chrome implements deltaY using the formula
+ // "-100 * log(1 + [event magnification])" which is unfortunately incorrect.
+ // All deltas for a single pinch gesture should sum to 0 if the start and end
+ // of a pinch gesture end up in the same place. This doesn't happen in Chrome
+ // because they followed Apple's misleading documentation, which implies that
+ // "1 + [event magnification]" is the scale factor. The scale factor is
+ // instead "pow(ratio, [event magnification])" so "[event magnification]" is
+ // already in log space.
+ //
+ // The multiplication by the backing scale factor below counteracts the
+ // division by the backing scale factor in WheelEvent.
+
+ // We want to set deltaY to |-100.0 * M * GetDefaultScaleInternal()| where M
+ // is [event magnification] but [event magnification] is only available in the
+ // macOS widget code so we have to reverse engineer from mCurrentSpan and
+ // mPreviousSpan (which are derived from [event magnification]) to get it.
+ // Specifically, we know |mCurrentSpan == 100.0| and |mPreviousSpan == 100.0 *
+ // (1.0 - M)|. We can calculate deltaY by solving the mPreviousSpan equation
+ // for M in terms of mPreviousSpan and plugging that into to the formula for
+ // deltaY.
+ return (mPreviousSpan - 100.0) *
+ (aWidget ? aWidget->GetDefaultScaleInternal() : 1.f);
+#else
+ // This calculation is based on what the Windows and Linux widget code does.
+ // Specifically, it creates a PinchGestureInput with |mCurrentSpan == 100.0 *
+ // currentScale| and |mPreviousSpan == 100.0 * lastScale| where currentScale
+ // is the scale from the current OS event and lastScale is the scale when the
+ // previous OS event happened. On macOS [event magnification] is a relative
+ // change in scale factor, ie if the scale factor changed from 1 to 1.1 it
+ // will be 0.1, similarly if it changed from 1 to 0.9 it will be -0.1. To
+ // calculate the relative scale change on Windows we would calculate |M =
+ // currentScale - lastScale = (mCurrentSpan-mPreviousSpan)/100| and use the
+ // same formula as the macOS code
+ // (|-100.0 * M * GetDefaultScaleInternal()|).
+
+ return (mPreviousSpan - mCurrentSpan) *
+ (aWidget ? aWidget->GetDefaultScaleInternal() : 1.f);
+#endif
+}
+
+bool PinchGestureInput::SetLineOrPageDeltaY(nsIWidget* aWidget) {
+ double deltaY = ComputeDeltaY(aWidget);
+ if (deltaY == 0 && mType != PINCHGESTURE_END) {
+ return false;
+ }
+ gfx::IntPoint lineOrPageDelta = PinchGestureInput::GetIntegerDeltaForEvent(
+ (mType == PINCHGESTURE_START), 0, deltaY);
+ mLineOrPageDeltaY = lineOrPageDelta.y;
+ if (mLineOrPageDeltaY == 0) {
+ // For PINCHGESTURE_SCALE events, don't dispatch them. Note that the delta
+ // isn't lost; it remains in the accumulator in GetIntegerDeltaForEvent().
+ if (mType == PINCHGESTURE_SCALE) {
+ return false;
+ }
+ // On Windows, drop PINCHGESTURE_START as well (the Windows widget code will
+ // defer the START event until we accumulate enough delta).
+ // The Linux widget code doesn't support this, so instead set the event's
+ // mLineOrPageDeltaY to the smallest nonzero amount in the relevant
+ // direction.
+ if (mType == PINCHGESTURE_START) {
+#ifdef XP_WIN
+ return false;
+#else
+ mLineOrPageDeltaY = (deltaY >= 0) ? 1 : -1;
+#endif
+ }
+ // For PINCHGESTURE_END events, not dispatching a DOMMouseScroll for them is
+ // fine.
+ }
+ return true;
+}
+
+/* static */ gfx::IntPoint PinchGestureInput::GetIntegerDeltaForEvent(
+ bool aIsStart, float x, float y) {
+ static gfx::Point sAccumulator(0.0f, 0.0f);
+ if (aIsStart) {
+ sAccumulator = gfx::Point(0.0f, 0.0f);
+ }
+ sAccumulator.x += x;
+ sAccumulator.y += y;
+ return gfx::IntPoint(TakeLargestInt(&sAccumulator.x),
+ TakeLargestInt(&sAccumulator.y));
+}
+
+TapGestureInput::TapGestureInput()
+ : InputData(TAPGESTURE_INPUT), mType(TAPGESTURE_LONG) {}
+
+TapGestureInput::TapGestureInput(TapGestureType aType, TimeStamp aTimeStamp,
+ const ScreenIntPoint& aPoint,
+ Modifiers aModifiers)
+ : InputData(TAPGESTURE_INPUT, aTimeStamp, aModifiers),
+ mType(aType),
+ mPoint(aPoint) {}
+
+TapGestureInput::TapGestureInput(TapGestureType aType, TimeStamp aTimeStamp,
+ const ParentLayerPoint& aLocalPoint,
+ Modifiers aModifiers)
+ : InputData(TAPGESTURE_INPUT, aTimeStamp, aModifiers),
+ mType(aType),
+ mLocalPoint(aLocalPoint) {}
+
+bool TapGestureInput::TransformToLocal(
+ const ScreenToParentLayerMatrix4x4& aTransform) {
+ Maybe<ParentLayerIntPoint> point = UntransformBy(aTransform, mPoint);
+ if (!point) {
+ return false;
+ }
+ mLocalPoint = *point;
+ return true;
+}
+
+WidgetSimpleGestureEvent TapGestureInput::ToWidgetEvent(
+ nsIWidget* aWidget) const {
+ WidgetSimpleGestureEvent event(true, eTapGesture, aWidget);
+
+ event.mTimeStamp = mTimeStamp;
+ event.mLayersId = mLayersId;
+ event.mRefPoint = ViewAs<LayoutDevicePixel>(
+ mPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+ event.mButtons = 0;
+ event.mClickCount = 1;
+ event.mModifiers = modifiers;
+
+ return event;
+}
+
+ScrollWheelInput::ScrollWheelInput()
+ : InputData(SCROLLWHEEL_INPUT),
+ mDeltaType(SCROLLDELTA_LINE),
+ mScrollMode(SCROLLMODE_INSTANT),
+ mHandledByAPZ(false),
+ mDeltaX(0.0),
+ mDeltaY(0.0),
+ mLineOrPageDeltaX(0),
+ mLineOrPageDeltaY(0),
+ mScrollSeriesNumber(0),
+ mUserDeltaMultiplierX(1.0),
+ mUserDeltaMultiplierY(1.0),
+ mMayHaveMomentum(false),
+ mIsMomentum(false),
+ mAPZAction(APZWheelAction::Scroll) {}
+
+ScrollWheelInput::ScrollWheelInput(
+ TimeStamp aTimeStamp, Modifiers aModifiers, ScrollMode aScrollMode,
+ ScrollDeltaType aDeltaType, const ScreenPoint& aOrigin, double aDeltaX,
+ double aDeltaY, bool aAllowToOverrideSystemScrollSpeed,
+ WheelDeltaAdjustmentStrategy aWheelDeltaAdjustmentStrategy)
+ : InputData(SCROLLWHEEL_INPUT, aTimeStamp, aModifiers),
+ mDeltaType(aDeltaType),
+ mScrollMode(aScrollMode),
+ mOrigin(aOrigin),
+ mHandledByAPZ(false),
+ mDeltaX(aDeltaX),
+ mDeltaY(aDeltaY),
+ mLineOrPageDeltaX(0),
+ mLineOrPageDeltaY(0),
+ mScrollSeriesNumber(0),
+ mUserDeltaMultiplierX(1.0),
+ mUserDeltaMultiplierY(1.0),
+ mMayHaveMomentum(false),
+ mIsMomentum(false),
+ mAllowToOverrideSystemScrollSpeed(aAllowToOverrideSystemScrollSpeed),
+ mWheelDeltaAdjustmentStrategy(aWheelDeltaAdjustmentStrategy),
+ mAPZAction(APZWheelAction::Scroll) {}
+
+ScrollWheelInput::ScrollWheelInput(const WidgetWheelEvent& aWheelEvent)
+ : InputData(SCROLLWHEEL_INPUT, aWheelEvent.mTimeStamp,
+ aWheelEvent.mModifiers),
+ mDeltaType(DeltaTypeForDeltaMode(aWheelEvent.mDeltaMode)),
+ mScrollMode(SCROLLMODE_INSTANT),
+ mHandledByAPZ(aWheelEvent.mFlags.mHandledByAPZ),
+ mDeltaX(aWheelEvent.mDeltaX),
+ mDeltaY(aWheelEvent.mDeltaY),
+ mWheelTicksX(aWheelEvent.mWheelTicksX),
+ mWheelTicksY(aWheelEvent.mWheelTicksX),
+ mLineOrPageDeltaX(aWheelEvent.mLineOrPageDeltaX),
+ mLineOrPageDeltaY(aWheelEvent.mLineOrPageDeltaY),
+ mScrollSeriesNumber(0),
+ mUserDeltaMultiplierX(1.0),
+ mUserDeltaMultiplierY(1.0),
+ mMayHaveMomentum(aWheelEvent.mMayHaveMomentum),
+ mIsMomentum(aWheelEvent.mIsMomentum),
+ mAllowToOverrideSystemScrollSpeed(
+ aWheelEvent.mAllowToOverrideSystemScrollSpeed),
+ mWheelDeltaAdjustmentStrategy(WheelDeltaAdjustmentStrategy::eNone),
+ mAPZAction(APZWheelAction::Scroll) {
+ mOrigin = ScreenPoint(ViewAs<ScreenPixel>(
+ aWheelEvent.mRefPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+}
+
+ScrollWheelInput::ScrollDeltaType ScrollWheelInput::DeltaTypeForDeltaMode(
+ uint32_t aDeltaMode) {
+ switch (aDeltaMode) {
+ case WheelEvent_Binding::DOM_DELTA_LINE:
+ return SCROLLDELTA_LINE;
+ case WheelEvent_Binding::DOM_DELTA_PAGE:
+ return SCROLLDELTA_PAGE;
+ case WheelEvent_Binding::DOM_DELTA_PIXEL:
+ return SCROLLDELTA_PIXEL;
+ default:
+ MOZ_CRASH();
+ }
+ return SCROLLDELTA_LINE;
+}
+
+uint32_t ScrollWheelInput::DeltaModeForDeltaType(ScrollDeltaType aDeltaType) {
+ switch (aDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_LINE:
+ return WheelEvent_Binding::DOM_DELTA_LINE;
+ case ScrollWheelInput::SCROLLDELTA_PAGE:
+ return WheelEvent_Binding::DOM_DELTA_PAGE;
+ case ScrollWheelInput::SCROLLDELTA_PIXEL:
+ default:
+ return WheelEvent_Binding::DOM_DELTA_PIXEL;
+ }
+}
+
+ScrollUnit ScrollWheelInput::ScrollUnitForDeltaType(
+ ScrollDeltaType aDeltaType) {
+ switch (aDeltaType) {
+ case SCROLLDELTA_LINE:
+ return ScrollUnit::LINES;
+ case SCROLLDELTA_PAGE:
+ return ScrollUnit::PAGES;
+ case SCROLLDELTA_PIXEL:
+ return ScrollUnit::DEVICE_PIXELS;
+ default:
+ MOZ_CRASH();
+ }
+ return ScrollUnit::LINES;
+}
+
+WidgetWheelEvent ScrollWheelInput::ToWidgetEvent(nsIWidget* aWidget) const {
+ WidgetWheelEvent wheelEvent(true, eWheel, aWidget);
+ wheelEvent.mModifiers = this->modifiers;
+ wheelEvent.mTimeStamp = mTimeStamp;
+ wheelEvent.mLayersId = mLayersId;
+ wheelEvent.mRefPoint = RoundedToInt(ViewAs<LayoutDevicePixel>(
+ mOrigin,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+ wheelEvent.mButtons = 0;
+ wheelEvent.mDeltaMode = DeltaModeForDeltaType(mDeltaType);
+ wheelEvent.mMayHaveMomentum = mMayHaveMomentum;
+ wheelEvent.mIsMomentum = mIsMomentum;
+ wheelEvent.mDeltaX = mDeltaX;
+ wheelEvent.mDeltaY = mDeltaY;
+ wheelEvent.mWheelTicksX = mWheelTicksX;
+ wheelEvent.mWheelTicksY = mWheelTicksY;
+ wheelEvent.mLineOrPageDeltaX = mLineOrPageDeltaX;
+ wheelEvent.mLineOrPageDeltaY = mLineOrPageDeltaY;
+ wheelEvent.mAllowToOverrideSystemScrollSpeed =
+ mAllowToOverrideSystemScrollSpeed;
+ wheelEvent.mFlags.mHandledByAPZ = mHandledByAPZ;
+ wheelEvent.mFocusSequenceNumber = mFocusSequenceNumber;
+ return wheelEvent;
+}
+
+bool ScrollWheelInput::TransformToLocal(
+ const ScreenToParentLayerMatrix4x4& aTransform) {
+ Maybe<ParentLayerPoint> point = UntransformBy(aTransform, mOrigin);
+ if (!point) {
+ return false;
+ }
+ mLocalOrigin = *point;
+ return true;
+}
+
+bool ScrollWheelInput::IsCustomizedByUserPrefs() const {
+ return mUserDeltaMultiplierX != 1.0 || mUserDeltaMultiplierY != 1.0;
+}
+
+KeyboardInput::KeyboardInput(const WidgetKeyboardEvent& aEvent)
+ : InputData(KEYBOARD_INPUT, aEvent.mTimeStamp, aEvent.mModifiers),
+ mKeyCode(aEvent.mKeyCode),
+ mCharCode(aEvent.mCharCode),
+ mHandledByAPZ(false) {
+ switch (aEvent.mMessage) {
+ case eKeyPress: {
+ mType = KeyboardInput::KEY_PRESS;
+ break;
+ }
+ case eKeyUp: {
+ mType = KeyboardInput::KEY_UP;
+ break;
+ }
+ case eKeyDown: {
+ mType = KeyboardInput::KEY_DOWN;
+ break;
+ }
+ default:
+ mType = KeyboardInput::KEY_OTHER;
+ break;
+ }
+
+ aEvent.GetShortcutKeyCandidates(mShortcutCandidates);
+}
+
+KeyboardInput::KeyboardInput()
+ : InputData(KEYBOARD_INPUT),
+ mType(KEY_DOWN),
+ mKeyCode(0),
+ mCharCode(0),
+ mHandledByAPZ(false) {}
+
+} // namespace mozilla
diff --git a/widget/InputData.h b/widget/InputData.h
new file mode 100644
index 0000000000..855cfcd178
--- /dev/null
+++ b/widget/InputData.h
@@ -0,0 +1,835 @@
+/* -*- 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/. */
+
+#ifndef InputData_h__
+#define InputData_h__
+
+#include "nsDebug.h"
+#include "nsPoint.h"
+#include "nsTArray.h"
+#include "Units.h"
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaAdjustmentStrategy
+#include "mozilla/gfx/MatrixFwd.h"
+#include "mozilla/layers/APZPublicUtils.h"
+#include "mozilla/layers/KeyboardScrollAction.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ipc/IPCForwards.h"
+
+template <class E>
+struct already_AddRefed;
+class nsIWidget;
+
+namespace mozilla {
+
+namespace layers {
+class APZInputBridgeChild;
+class PAPZInputBridgeParent;
+} // namespace layers
+
+namespace dom {
+class Touch;
+} // namespace dom
+
+// clang-format off
+MOZ_DEFINE_ENUM(
+ InputType, (
+ MULTITOUCH_INPUT,
+ MOUSE_INPUT,
+ PANGESTURE_INPUT,
+ PINCHGESTURE_INPUT,
+ TAPGESTURE_INPUT,
+ SCROLLWHEEL_INPUT,
+ KEYBOARD_INPUT
+));
+// clang-format on
+
+class MultiTouchInput;
+class MouseInput;
+class PanGestureInput;
+class PinchGestureInput;
+class TapGestureInput;
+class ScrollWheelInput;
+class KeyboardInput;
+
+// This looks unnecessary now, but as we add more and more classes that derive
+// from InputType (eventually probably almost as many as *Events.h has), it
+// will be more and more clear what's going on with a macro that shortens the
+// definition of the RTTI functions.
+#define INPUTDATA_AS_CHILD_TYPE(type, enumID) \
+ const type& As##type() const { \
+ MOZ_ASSERT(mInputType == enumID, "Invalid cast of InputData."); \
+ return (const type&)*this; \
+ } \
+ type& As##type() { \
+ MOZ_ASSERT(mInputType == enumID, "Invalid cast of InputData."); \
+ return (type&)*this; \
+ }
+
+/** Base input data class. Should never be instantiated. */
+class InputData {
+ public:
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+ InputType mInputType;
+ // Time that this data is relevant to. This only really matters when this data
+ // is used as an event.
+ TimeStamp mTimeStamp;
+ // The sequence number of the last potentially focus changing event handled
+ // by APZ. This is used to track when that event has been processed by
+ // content, and focus can be reconfirmed for async keyboard scrolling.
+ uint64_t mFocusSequenceNumber;
+
+ // The LayersId of the content process that the corresponding WidgetEvent
+ // should be dispatched to.
+ layers::LayersId mLayersId;
+
+ Modifiers modifiers;
+
+ INPUTDATA_AS_CHILD_TYPE(MultiTouchInput, MULTITOUCH_INPUT)
+ INPUTDATA_AS_CHILD_TYPE(MouseInput, MOUSE_INPUT)
+ INPUTDATA_AS_CHILD_TYPE(PanGestureInput, PANGESTURE_INPUT)
+ INPUTDATA_AS_CHILD_TYPE(PinchGestureInput, PINCHGESTURE_INPUT)
+ INPUTDATA_AS_CHILD_TYPE(TapGestureInput, TAPGESTURE_INPUT)
+ INPUTDATA_AS_CHILD_TYPE(ScrollWheelInput, SCROLLWHEEL_INPUT)
+ INPUTDATA_AS_CHILD_TYPE(KeyboardInput, KEYBOARD_INPUT)
+
+ virtual ~InputData();
+ explicit InputData(InputType aInputType);
+
+ protected:
+ InputData(InputType aInputType, TimeStamp aTimeStamp, Modifiers aModifiers);
+};
+
+/**
+ * Data container for a single touch input. Similar to dom::Touch, but used in
+ * off-main-thread situations. This is more for just storing touch data, whereas
+ * dom::Touch is more useful for dispatching through the DOM (which can only
+ * happen on the main thread). dom::Touch also bears the problem of storing
+ * pointers to nsIWidget instances which can only be used on the main thread,
+ * so if instead we used dom::Touch and ever set these pointers
+ * off-main-thread, Bad Things Can Happen(tm).
+ *
+ * Note that this doesn't inherit from InputData because this itself is not an
+ * event. It is only a container/struct that should have any number of instances
+ * within a MultiTouchInput.
+ *
+ * fixme/bug 775746: Make dom::Touch inherit from this class.
+ */
+class SingleTouchData {
+ public:
+ // Construct a SingleTouchData from a Screen point.
+ // mLocalScreenPoint remains (0,0) unless it's set later.
+ SingleTouchData(int32_t aIdentifier, ScreenIntPoint aScreenPoint,
+ ScreenSize aRadius, float aRotationAngle, float aForce);
+
+ // Construct a SingleTouchData from a ParentLayer point.
+ // mScreenPoint remains (0,0) unless it's set later.
+ // Note: if APZ starts using the radius for anything, we should add a local
+ // version of that too, and have this constructor take it as a
+ // ParentLayerSize.
+ SingleTouchData(int32_t aIdentifier, ParentLayerPoint aLocalScreenPoint,
+ ScreenSize aRadius, float aRotationAngle, float aForce);
+
+ SingleTouchData();
+
+ already_AddRefed<dom::Touch> ToNewDOMTouch() const;
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+
+ // Historical data of this touch, which was coalesced into this event.
+ // Touch event coalescing can happen at the system level when the touch
+ // screen's sampling frequency is higher than the vsync rate, or when the
+ // UI thread is busy. When multiple "samples" of touch data are coalesced into
+ // one touch event, the touch event's regular position information is the
+ // information from the last sample. And the previous, "coalesced-away"
+ // samples are stored in mHistoricalData.
+
+ struct HistoricalTouchData {
+ // The timestamp at which the information in this "sample" was originally
+ // sampled.
+ TimeStamp mTimeStamp;
+
+ // The touch data of this historical sample.
+ ScreenIntPoint mScreenPoint;
+ ParentLayerPoint mLocalScreenPoint;
+ ScreenSize mRadius;
+ float mRotationAngle = 0.0f;
+ float mForce = 0.0f;
+ };
+ CopyableTArray<HistoricalTouchData> mHistoricalData;
+
+ // A unique number assigned to each SingleTouchData within a MultiTouchInput
+ // so that they can be easily distinguished when handling a touch
+ // start/move/end.
+ int32_t mIdentifier;
+
+ // Point on the screen that the touch hit, in device pixels. They are
+ // coordinates on the screen.
+ ScreenIntPoint mScreenPoint;
+
+ // |mScreenPoint| transformed to the local coordinates of the APZC targeted
+ // by the hit. This is set and used by APZ.
+ ParentLayerPoint mLocalScreenPoint;
+
+ // Radius that the touch covers, i.e. if you're using your thumb it will
+ // probably be larger than using your pinky, even with the same force.
+ // Radius can be different along x and y. For example, if you press down with
+ // your entire finger vertically, the y radius will be much larger than the x
+ // radius.
+ ScreenSize mRadius;
+
+ float mRotationAngle;
+
+ // How hard the screen is being pressed.
+ float mForce;
+
+ int32_t mTiltX = 0;
+ int32_t mTiltY = 0;
+ int32_t mTwist = 0;
+};
+
+/**
+ * Similar to WidgetTouchEvent, but for use off-main-thread. Also only stores a
+ * screen touch point instead of the many different coordinate spaces
+ * WidgetTouchEvent stores its touch point in. This includes a way to initialize
+ * itself from a WidgetTouchEvent by copying all relevant data over. Note that
+ * this copying from WidgetTouchEvent functionality can only be used on the main
+ * thread.
+ *
+ * Stores an array of SingleTouchData.
+ */
+class MultiTouchInput : public InputData {
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ MultiTouchType, (
+ MULTITOUCH_START,
+ MULTITOUCH_MOVE,
+ MULTITOUCH_END,
+ MULTITOUCH_CANCEL
+ ));
+ // clang-format on
+
+ MultiTouchInput(MultiTouchType aType, uint32_t aTime, TimeStamp aTimeStamp,
+ Modifiers aModifiers);
+ MultiTouchInput();
+ MultiTouchInput(MultiTouchInput&&) = default;
+ MultiTouchInput(const MultiTouchInput&) = default;
+ explicit MultiTouchInput(const WidgetTouchEvent& aTouchEvent);
+
+ MultiTouchInput& operator=(MultiTouchInput&&) = default;
+ MultiTouchInput& operator=(const MultiTouchInput&) = default;
+
+ void Translate(const ScreenPoint& aTranslation);
+
+ WidgetTouchEvent ToWidgetEvent(
+ nsIWidget* aWidget,
+ uint16_t aInputSource =
+ /* MouseEvent_Binding::MOZ_SOURCE_TOUCH = */ 5) const;
+
+ // Return the index into mTouches of the SingleTouchData with the given
+ // identifier, or -1 if there is no such SingleTouchData.
+ int32_t IndexOfTouch(int32_t aTouchIdentifier);
+
+ bool TransformToLocal(const ScreenToParentLayerMatrix4x4& aTransform);
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+ MultiTouchType mType;
+ CopyableTArray<SingleTouchData> mTouches;
+ // The screen offset of the root widget. This can be changing along with
+ // the touch interaction, so we sstore it in the event.
+ ExternalPoint mScreenOffset;
+ bool mHandledByAPZ;
+ // These button fields match to the corresponding fields in
+ // WidgetMouseEventBase, except mButton defaults to -1 to follow PointerEvent.
+ int16_t mButton = eNotPressed;
+ int16_t mButtons = 0;
+};
+
+class MouseInput : public InputData {
+ protected:
+ friend mozilla::layers::APZInputBridgeChild;
+ friend mozilla::layers::PAPZInputBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ MouseInput();
+
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ MouseType, (
+ MOUSE_NONE,
+ MOUSE_MOVE,
+ MOUSE_DOWN,
+ MOUSE_UP,
+ MOUSE_DRAG_START,
+ MOUSE_DRAG_END,
+ MOUSE_WIDGET_ENTER,
+ MOUSE_WIDGET_EXIT,
+ MOUSE_HITTEST,
+ MOUSE_EXPLORE_BY_TOUCH
+ ));
+
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ ButtonType, (
+ PRIMARY_BUTTON,
+ MIDDLE_BUTTON,
+ SECONDARY_BUTTON,
+ NONE
+ ));
+ // clang-format on
+
+ MouseInput(MouseType aType, ButtonType aButtonType, uint16_t aInputSource,
+ int16_t aButtons, const ScreenPoint& aPoint, TimeStamp aTimeStamp,
+ Modifiers aModifiers);
+ explicit MouseInput(const WidgetMouseEventBase& aMouseEvent);
+
+ bool IsLeftButton() const;
+
+ bool TransformToLocal(const ScreenToParentLayerMatrix4x4& aTransform);
+ WidgetMouseEvent ToWidgetEvent(nsIWidget* aWidget) const;
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+ MouseType mType;
+ ButtonType mButtonType;
+ uint16_t mInputSource;
+ int16_t mButtons;
+ ScreenPoint mOrigin;
+ ParentLayerPoint mLocalOrigin;
+ bool mHandledByAPZ;
+ /**
+ * If click event should not be fired in the content after the "mousedown"
+ * event or following "mouseup", set to true.
+ */
+ bool mPreventClickEvent;
+};
+
+/**
+ * Encapsulation class for pan events, can be used off-main-thread.
+ * These events are currently only used for scrolling on desktop.
+ */
+class PanGestureInput : public InputData {
+ friend struct IPC::ParamTraits<PanGestureInput>;
+
+ protected:
+ friend mozilla::layers::APZInputBridgeChild;
+ friend mozilla::layers::PAPZInputBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ PanGestureInput();
+
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ PanGestureType, (
+ // MayStart: Dispatched before any actual panning has occurred but when a
+ // pan gesture is probably about to start, for example when the user
+ // starts touching the touchpad. Should interrupt any ongoing APZ
+ // animation and can be used to trigger scrollability indicators (e.g.
+ // flashing overlay scrollbars).
+ PANGESTURE_MAYSTART,
+
+ // Cancelled: Dispatched after MayStart when no pan gesture is going to
+ // happen after all, for example when the user lifts their fingers from a
+ // touchpad without having done any scrolling.
+ PANGESTURE_CANCELLED,
+
+ // Start: A pan gesture is starting.
+ // For devices that do not support the MayStart event type, this event can
+ // be used to interrupt ongoing APZ animations.
+ PANGESTURE_START,
+
+ // Pan: The actual pan motion by mPanDisplacement.
+ PANGESTURE_PAN,
+
+ // End: The pan gesture has ended, for example because the user has lifted
+ // their fingers from a touchpad after scrolling.
+ // Any potential momentum events fire after this event.
+ PANGESTURE_END,
+
+ // The following momentum event types are used in order to control the pan
+ // momentum animation. Using these instead of our own animation ensures
+ // that the animation curve is OS native and that the animation stops
+ // reliably if it is cancelled by the user.
+
+ // MomentumStart: Dispatched between the End event of the actual
+ // user-controlled pan, and the first MomentumPan event of the momentum
+ // animation.
+ PANGESTURE_MOMENTUMSTART,
+
+ // MomentumPan: The actual momentum motion by mPanDisplacement.
+ PANGESTURE_MOMENTUMPAN,
+
+ // MomentumEnd: The momentum animation has ended, for example because the
+ // momentum velocity has gone below the stopping threshold, or because the
+ // user has stopped the animation by putting their fingers on a touchpad.
+ PANGESTURE_MOMENTUMEND,
+
+ // Interrupted:: A pan gesture started being handled by an APZC but
+ // subsequent pan events might have been consumed by other operations
+ // which haven't been handled by the APZC (e.g. full zoom).
+ PANGESTURE_INTERRUPTED
+ ));
+
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ PanDeltaType, (
+ // There are three kinds of scroll delta modes in Gecko: "page", "line"
+ // and "pixel". Touchpad pan gestures only support "page" and "pixel".
+ //
+ // NOTE: PANDELTA_PAGE currently replicates Gtk behavior
+ // (see AsyncPanZoomController::OnPan).
+ PANDELTA_PAGE,
+ PANDELTA_PIXEL
+ ));
+ // clang-format on
+
+ PanGestureInput(PanGestureType aType, TimeStamp aTimeStamp,
+ const ScreenPoint& aPanStartPoint,
+ const ScreenPoint& aPanDisplacement, Modifiers aModifiers);
+
+ enum class IsEligibleForSwipe : bool { No, Yes };
+ PanGestureInput(PanGestureType aType, TimeStamp aTimeStamp,
+ const ScreenPoint& aPanStartPoint,
+ const ScreenPoint& aPanDisplacement, Modifiers aModifiers,
+ IsEligibleForSwipe aIsEligibleForSwipe);
+
+ void SetLineOrPageDeltas(int32_t aLineOrPageDeltaX,
+ int32_t aLineOrPageDeltaY);
+
+ bool IsMomentum() const;
+
+ WidgetWheelEvent ToWidgetEvent(nsIWidget* aWidget) const;
+
+ bool TransformToLocal(const ScreenToParentLayerMatrix4x4& aTransform);
+
+ ScreenPoint UserMultipliedPanDisplacement() const;
+ ParentLayerPoint UserMultipliedLocalPanDisplacement() const;
+
+ void SetHandledByAPZ(bool aHandled) { mHandledByAPZ = aHandled; }
+ void SetOverscrollBehaviorAllowsSwipe(bool aAllows) {
+ mOverscrollBehaviorAllowsSwipe = aAllows;
+ }
+ void SetSimulateMomentum(bool aSimulate) { mSimulateMomentum = aSimulate; }
+ void SetIsNoLineOrPageDelta(bool aIsNoLineOrPageDelta) {
+ mIsNoLineOrPageDelta = aIsNoLineOrPageDelta;
+ }
+
+ // Returns true if this pan gesture event is elligible for browser swipe
+ // gesture considering the overscroll-behavior property of the target
+ // scroll container.
+ bool AllowsSwipe() const {
+ MOZ_ASSERT(mHandledByAPZ);
+ return mMayTriggerSwipe && mOverscrollBehaviorAllowsSwipe;
+ }
+
+ // Similar to above AllowsSwipe() but this doesn't care the
+ // overscroll-behavior property, this function should be only used for cases
+ // where APZ isn't involved.
+ bool MayTriggerSwipe() const { return mMayTriggerSwipe; }
+ bool RequiresContentResponseIfCannotScrollHorizontallyInStartDirection();
+
+ static gfx::IntPoint GetIntegerDeltaForEvent(bool aIsStart, float x, float y);
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+ PanGestureType mType;
+ ScreenPoint mPanStartPoint;
+
+ // The delta. This can be non-zero on any type of event.
+ ScreenPoint mPanDisplacement;
+
+ // Versions of |mPanStartPoint| and |mPanDisplacement| in the local
+ // coordinates of the APZC receiving the pan. These are set and used by APZ.
+ ParentLayerPoint mLocalPanStartPoint;
+ ParentLayerPoint mLocalPanDisplacement;
+
+ // See lineOrPageDeltaX/Y on WidgetWheelEvent.
+ int32_t mLineOrPageDeltaX;
+ int32_t mLineOrPageDeltaY;
+
+ // User-set delta multipliers.
+ double mUserDeltaMultiplierX;
+ double mUserDeltaMultiplierY;
+
+ PanDeltaType mDeltaType = PANDELTA_PIXEL;
+
+ bool mHandledByAPZ : 1;
+
+ // This is used by APZ to communicate to widget code whether the
+ // overscroll-behavior of the scroll frame handling this swipe allows
+ // non-local overscroll behaviors in the horizontal direction (such as
+ // swipe navigation).
+ bool mOverscrollBehaviorAllowsSwipe : 1;
+
+ // true if APZ should do a fling animation after this pan ends, like
+ // it would with touchscreens. (For platforms that don't emit momentum
+ // events.)
+ bool mSimulateMomentum : 1;
+
+ // true if the creator of this object does not set the mLineOrPageDeltaX/Y
+ // fields and when/if WidgetWheelEvent's are generated from this object wants
+ // the corresponding mLineOrPageDeltaX/Y fields in the WidgetWheelEvent to be
+ // automatically calculated (upon event dispatch by the EventStateManager
+ // code).
+ bool mIsNoLineOrPageDelta : 1;
+
+ private:
+ // If this is true, and this event started a new input block that couldn't
+ // find a scrollable target which is scrollable in the horizontal component
+ // of the scroll start direction, then this input block needs to be put on
+ // hold until a content response has arrived, even if the block has a
+ // confirmed target.
+ // This is used by events that can result in a swipe instead of a scroll.
+ bool mMayTriggerSwipe : 1;
+ void SetMayTriggerSwipe(bool aValue) { mMayTriggerSwipe = aValue; }
+};
+
+/**
+ * Encapsulation class for pinch events. In general, these will be generated by
+ * a gesture listener by looking at SingleTouchData/MultiTouchInput instances
+ * and determining whether or not the user was trying to do a gesture.
+ */
+class PinchGestureInput : public InputData {
+ protected:
+ friend mozilla::layers::APZInputBridgeChild;
+ friend mozilla::layers::PAPZInputBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ PinchGestureInput();
+
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ PinchGestureType, (
+ PINCHGESTURE_START,
+ PINCHGESTURE_SCALE,
+ // The FINGERLIFTED state is used when a touch-based pinch gesture is
+ // terminated by lifting one of the two fingers. The position of the
+ // finger that's still down is populated as the focus point.
+ PINCHGESTURE_FINGERLIFTED,
+ // The END state is used when the pinch gesture is completely terminated.
+ // In this state, the focus point should not be relied upon for having
+ // meaningful data.
+ PINCHGESTURE_END
+ ));
+
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ PinchGestureSource, (
+ UNKNOWN, // Default initialization value. Should never actually be used.
+ TOUCH, // From two-finger pinch gesture
+ ONE_TOUCH, // From one-finger pinch gesture
+ TRACKPAD, // From trackpad pinch gesture
+ MOUSEWHEEL // Synthesized from modifier+mousewheel
+
+ // If adding more items here, increase n_values for the
+ // APZ_ZOOM_PINCHSOURCE Telemetry metric.
+ ));
+ // clang-format on
+
+ // Construct a pinch gesture from a Screen point.
+ PinchGestureInput(PinchGestureType aType, PinchGestureSource aSource,
+ TimeStamp aTimeStamp, const ExternalPoint& aScreenOffset,
+ const ScreenPoint& aFocusPoint, ScreenCoord aCurrentSpan,
+ ScreenCoord aPreviousSpan, Modifiers aModifiers);
+
+ bool TransformToLocal(const ScreenToParentLayerMatrix4x4& aTransform);
+
+ WidgetWheelEvent ToWidgetEvent(nsIWidget* aWidget) const;
+
+ double ComputeDeltaY(nsIWidget* aWidget) const;
+
+ // Set mLineOrPageDeltaY based on ComputeDeltaY().
+ // Return false if the caller should drop this event to ensure
+ // that preventDefault() is respected. (More specifically, this will be
+ // true for event types other than PINCHGESTURE_END if the computed
+ // mLineOrPageDeltaY is zero. In such cases, the resulting DOMMouseScroll
+ // event will not be dispatched, which is a problem if the page is relying
+ // on DOMMouseScroll to prevent browser zooming).
+ // Note that even if the function returns false, the delta from the event
+ // is accumulated and available to be sent in a later event.
+ bool SetLineOrPageDeltaY(nsIWidget* aWidget);
+
+ static gfx::IntPoint GetIntegerDeltaForEvent(bool aIsStart, float x, float y);
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+ PinchGestureType mType;
+
+ // Some indication of the input device that generated this pinch gesture.
+ PinchGestureSource mSource;
+
+ // Center point of the pinch gesture. That is, if there are two fingers on the
+ // screen, it is their midpoint. In the case of more than two fingers, the
+ // point is implementation-specific, but can for example be the midpoint
+ // between the very first and very last touch. This is in device pixels and
+ // are the coordinates on the screen of this midpoint.
+ // For PINCHGESTURE_END events, this may hold the last known focus point or
+ // just be empty; in any case for END events it should not be relied upon.
+ // For PINCHGESTURE_FINGERLIFTED events, this holds the point of the finger
+ // that is still down.
+ ScreenPoint mFocusPoint;
+
+ // The screen offset of the root widget. This can be changing along with
+ // the touch interaction, so we sstore it in the event.
+ ExternalPoint mScreenOffset;
+
+ // |mFocusPoint| transformed to the local coordinates of the APZC targeted
+ // by the hit. This is set and used by APZ.
+ ParentLayerPoint mLocalFocusPoint;
+
+ // The distance between the touches responsible for the pinch gesture.
+ ScreenCoord mCurrentSpan;
+
+ // The previous |mCurrentSpan| in the PinchGestureInput preceding this one.
+ // This is only really relevant during a PINCHGESTURE_SCALE because when it is
+ // of this type then there must have been a history of spans.
+ ScreenCoord mPreviousSpan;
+
+ // We accumulate (via GetIntegerDeltaForEvent) the deltaY that would be
+ // computed by ToWidgetEvent, and then whenever we get a whole integer
+ // value we put it in mLineOrPageDeltaY. Since we only ever use deltaY we
+ // don't need a mLineOrPageDeltaX. This field is used to dispatch legacy mouse
+ // events which are only dispatched when the corresponding field on
+ // WidgetWheelEvent is non-zero.
+ int32_t mLineOrPageDeltaY;
+
+ bool mHandledByAPZ;
+};
+
+/**
+ * Encapsulation class for tap events. In general, these will be generated by
+ * a gesture listener by looking at SingleTouchData/MultiTouchInput instances
+ * and determining whether or not the user was trying to do a gesture.
+ */
+class TapGestureInput : public InputData {
+ protected:
+ friend mozilla::layers::APZInputBridgeChild;
+ friend mozilla::layers::PAPZInputBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ TapGestureInput();
+
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ TapGestureType, (
+ TAPGESTURE_LONG,
+ TAPGESTURE_LONG_UP,
+ TAPGESTURE_UP,
+ TAPGESTURE_CONFIRMED,
+ TAPGESTURE_DOUBLE,
+ TAPGESTURE_SECOND, // See GeckoContentController::TapType::eSecondTap
+ TAPGESTURE_CANCEL
+ ));
+ // clang-format on
+
+ // Construct a tap gesture from a Screen point.
+ // mLocalPoint remains (0,0) unless it's set later.
+ TapGestureInput(TapGestureType aType, TimeStamp aTimeStamp,
+ const ScreenIntPoint& aPoint, Modifiers aModifiers);
+
+ // Construct a tap gesture from a ParentLayer point.
+ // mPoint remains (0,0) unless it's set later.
+ TapGestureInput(TapGestureType aType, TimeStamp aTimeStamp,
+ const ParentLayerPoint& aLocalPoint, Modifiers aModifiers);
+
+ bool TransformToLocal(const ScreenToParentLayerMatrix4x4& aTransform);
+
+ WidgetSimpleGestureEvent ToWidgetEvent(nsIWidget* aWidget) const;
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+ TapGestureType mType;
+
+ // The location of the tap in screen pixels.
+ ScreenIntPoint mPoint;
+
+ // The location of the tap in the local coordinates of the APZC receiving it.
+ // This is set and used by APZ.
+ ParentLayerPoint mLocalPoint;
+};
+
+// Encapsulation class for scroll-wheel events. These are generated by mice
+// with physical scroll wheels, and on Windows by most touchpads when using
+// scroll gestures.
+class ScrollWheelInput : public InputData {
+ protected:
+ friend mozilla::layers::APZInputBridgeChild;
+ friend mozilla::layers::PAPZInputBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ typedef mozilla::layers::APZWheelAction APZWheelAction;
+
+ ScrollWheelInput();
+
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ ScrollDeltaType, (
+ // There are three kinds of scroll delta modes in Gecko: "page", "line"
+ // and "pixel".
+ SCROLLDELTA_LINE,
+ SCROLLDELTA_PAGE,
+ SCROLLDELTA_PIXEL
+ ));
+
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ ScrollMode, (
+ SCROLLMODE_INSTANT,
+ SCROLLMODE_SMOOTH
+ )
+ );
+ // clang-format on
+
+ ScrollWheelInput(TimeStamp aTimeStamp, Modifiers aModifiers,
+ ScrollMode aScrollMode, ScrollDeltaType aDeltaType,
+ const ScreenPoint& aOrigin, double aDeltaX, double aDeltaY,
+ bool aAllowToOverrideSystemScrollSpeed,
+ WheelDeltaAdjustmentStrategy aWheelDeltaAdjustmentStrategy);
+ explicit ScrollWheelInput(const WidgetWheelEvent& aEvent);
+
+ static ScrollDeltaType DeltaTypeForDeltaMode(uint32_t aDeltaMode);
+ static uint32_t DeltaModeForDeltaType(ScrollDeltaType aDeltaType);
+ static mozilla::ScrollUnit ScrollUnitForDeltaType(ScrollDeltaType aDeltaType);
+
+ WidgetWheelEvent ToWidgetEvent(nsIWidget* aWidget) const;
+ bool TransformToLocal(const ScreenToParentLayerMatrix4x4& aTransform);
+
+ bool IsCustomizedByUserPrefs() const;
+
+ // The following two functions are for auto-dir scrolling. For detailed
+ // information on auto-dir, @see mozilla::WheelDeltaAdjustmentStrategy
+ bool IsAutoDir(bool aForce = false) const {
+ if (aForce) {
+ return true;
+ }
+
+ switch (mWheelDeltaAdjustmentStrategy) {
+ case WheelDeltaAdjustmentStrategy::eAutoDir:
+ case WheelDeltaAdjustmentStrategy::eAutoDirWithRootHonour:
+ return true;
+ default:
+ // Prevent compilation errors generated by -Werror=switch
+ break;
+ }
+ return false;
+ }
+ // Indicates which element this scroll honours if it's an auto-dir scroll.
+ // If true, honour the root element; otherwise, honour the currently scrolling
+ // target.
+ // Note that if IsAutoDir() returns false, then this function also returns
+ // false, but false in this case is meaningless as IsAutoDir() indicates it's
+ // not an auto-dir scroll.
+ // For detailed information on auto-dir,
+ // @see mozilla::WheelDeltaAdjustmentStrategy
+ bool HonoursRoot(bool aForce = false) const {
+ return WheelDeltaAdjustmentStrategy::eAutoDirWithRootHonour ==
+ mWheelDeltaAdjustmentStrategy ||
+ aForce;
+ }
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+ ScrollDeltaType mDeltaType;
+ ScrollMode mScrollMode;
+ ScreenPoint mOrigin;
+
+ bool mHandledByAPZ;
+
+ // Deltas are in units corresponding to the delta type. For line deltas, they
+ // are the number of line units to scroll. The number of device pixels for a
+ // horizontal and vertical line unit are in FrameMetrics::mLineScrollAmount.
+ // For pixel deltas, these values are in ScreenCoords.
+ //
+ // The horizontal (X) delta is > 0 for scrolling right and < 0 for scrolling
+ // left. The vertical (Y) delta is < 0 for scrolling up and > 0 for
+ // scrolling down.
+ double mDeltaX;
+ double mDeltaY;
+
+ // The number of scroll wheel ticks.
+ double mWheelTicksX = 0.0;
+ double mWheelTicksY = 0.0;
+
+ // The location of the scroll in local coordinates. This is set and used by
+ // APZ.
+ ParentLayerPoint mLocalOrigin;
+
+ // See lineOrPageDeltaX/Y on WidgetWheelEvent.
+ int32_t mLineOrPageDeltaX;
+ int32_t mLineOrPageDeltaY;
+
+ // Indicates the order in which this event was added to a transaction. The
+ // first event is 1; if not a member of a transaction, this is 0.
+ uint32_t mScrollSeriesNumber;
+
+ // User-set delta multipliers.
+ double mUserDeltaMultiplierX;
+ double mUserDeltaMultiplierY;
+
+ bool mMayHaveMomentum;
+ bool mIsMomentum;
+ bool mAllowToOverrideSystemScrollSpeed;
+
+ // Sometimes a wheel event input's wheel delta should be adjusted. This member
+ // specifies how to adjust the wheel delta.
+ WheelDeltaAdjustmentStrategy mWheelDeltaAdjustmentStrategy;
+
+ APZWheelAction mAPZAction;
+};
+
+class KeyboardInput : public InputData {
+ public:
+ typedef mozilla::layers::KeyboardScrollAction KeyboardScrollAction;
+
+ // Note that if you change the first member in this enum(I.e. KEY_DOWN) to one
+ // other member, don't forget to update the minimum value in
+ // ContiguousEnumSerializer for KeyboardEventType in widget/nsGUIEventIPC
+ // accordingly.
+ enum KeyboardEventType {
+ KEY_DOWN,
+ KEY_PRESS,
+ KEY_UP,
+ // Any other key event such as eAccessKeyNotFound
+ KEY_OTHER,
+
+ // Used as an upper bound for ContiguousEnumSerializer
+ KEY_SENTINEL,
+ };
+
+ explicit KeyboardInput(const WidgetKeyboardEvent& aEvent);
+
+ // Warning, this class is serialized and sent over IPC. Any change to its
+ // fields must be reflected in its ParamTraits<>, in nsGUIEventIPC.h
+
+ KeyboardEventType mType;
+ uint32_t mKeyCode;
+ uint32_t mCharCode;
+ CopyableTArray<ShortcutKeyCandidate> mShortcutCandidates;
+
+ bool mHandledByAPZ;
+
+ // The scroll action to perform on a layer for this keyboard input. This is
+ // only used in APZ and is NOT serialized over IPC.
+ KeyboardScrollAction mAction;
+
+ protected:
+ friend mozilla::layers::APZInputBridgeChild;
+ friend mozilla::layers::PAPZInputBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ KeyboardInput();
+};
+
+} // namespace mozilla
+
+#endif // InputData_h__
diff --git a/widget/LSBUtils.cpp b/widget/LSBUtils.cpp
new file mode 100644
index 0000000000..9ebffacd7e
--- /dev/null
+++ b/widget/LSBUtils.cpp
@@ -0,0 +1,142 @@
+/* -*- 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 "LSBUtils.h"
+
+#include <fstream>
+#include <string>
+#include <string_view>
+#include <unistd.h>
+#include "base/process_util.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/ipc/LaunchError.h"
+
+namespace mozilla::widget::lsb {
+
+static const char gLsbReleasePath[] = "/usr/bin/lsb_release";
+static const char gEtcOsReleasePath[] = "/etc/os-release";
+static const char gUsrOsReleasePath[] = "/usr/lib/os-release";
+
+// See https://www.freedesktop.org/software/systemd/man/latest/os-release.html
+bool ExtractAndSetValue(nsACString& aContainer, std::string_view& aValue) {
+ // We assume the value is well formed and doesn't contain escape characters.
+ if (aValue.size() > 1 && (aValue.front() == '"' || aValue.front() == '\'')) {
+ // We assume the quote is properly balanced.
+ aValue = aValue.substr(1, aValue.size() - 2);
+ }
+ aContainer.Assign(aValue.data(), aValue.size());
+ return !aValue.empty();
+}
+
+bool GetOSRelease(nsACString& aDistributor, nsACString& aDescription,
+ nsACString& aRelease, nsACString& aCodename) {
+ std::ifstream stream(gEtcOsReleasePath);
+ if (stream.fail()) {
+ stream.open(gUsrOsReleasePath);
+ if (stream.fail()) {
+ return false;
+ }
+ }
+ bool seen_id = false, seen_pretty_name = false, seen_version_id = false;
+ std::string rawline;
+ nsAutoCString name;
+ while (std::getline(stream, rawline)) {
+ std::string_view line(rawline);
+ size_t pos = line.find('=');
+ if (pos != std::string_view::npos) {
+ auto key = line.substr(0, pos);
+ auto value = line.substr(pos + 1);
+ if (key == "ID") {
+ if (ExtractAndSetValue(aDistributor, value)) {
+ // Capitalize the first letter of the id. This mimics what
+ // lsb_release does on Debian and derivatives. On RH derivatives,
+ // ID tends to be capitalized already.
+ char* c = aDistributor.BeginWriting();
+ if (*c >= 'a' && *c <= 'z') {
+ *c -= ('a' - 'A');
+ }
+ seen_id = true;
+ }
+ } else if (key == "NAME") {
+ ExtractAndSetValue(name, value);
+ } else if (key == "PRETTY_NAME") {
+ if (ExtractAndSetValue(aDescription, value)) seen_pretty_name = true;
+ } else if (key == "VERSION_ID") {
+ if (ExtractAndSetValue(aRelease, value)) seen_version_id = true;
+ } else if (key == "VERSION_CODENAME") {
+ ExtractAndSetValue(aCodename, value);
+ }
+ }
+ }
+ // If NAME is set and only differs from ID in case, use NAME.
+ if (seen_id && !name.IsEmpty() && name.EqualsIgnoreCase(aDistributor)) {
+ aDistributor = name;
+ }
+ // Only consider our work done if we've seen at least ID, PRETTY_NAME and
+ // VERSION_ID.
+ return seen_id && seen_pretty_name && seen_version_id;
+}
+
+bool GetLSBRelease(nsACString& aDistributor, nsACString& aDescription,
+ nsACString& aRelease, nsACString& aCodename) {
+ // Nowadays, /etc/os-release is more likely to be available than
+ // /usr/bin/lsb_release. Relying on the former also avoids forking.
+ if (GetOSRelease(aDistributor, aDescription, aRelease, aCodename)) {
+ return true;
+ }
+
+ if (access(gLsbReleasePath, R_OK) != 0) return false;
+
+ int pipefd[2];
+ if (pipe(pipefd) == -1) {
+ NS_WARNING("pipe() failed!");
+ return false;
+ }
+
+ std::vector<std::string> argv = {gLsbReleasePath, "-idrc"};
+
+ base::LaunchOptions options;
+ options.fds_to_remap.push_back({pipefd[1], STDOUT_FILENO});
+ options.wait = true;
+
+ base::ProcessHandle process;
+ Result<Ok, ipc::LaunchError> err =
+ base::LaunchApp(argv, std::move(options), &process);
+ close(pipefd[1]);
+ if (err.isErr()) {
+ NS_WARNING("Failed to spawn lsb_release!");
+ close(pipefd[0]);
+ return false;
+ }
+
+ ScopedCloseFile stream(fdopen(pipefd[0], "r"));
+ if (!stream) {
+ NS_WARNING("Could not wrap fd!");
+ close(pipefd[0]);
+ return false;
+ }
+
+ char dist[256], desc[256], release[256], codename[256];
+ if (fscanf(stream.get(),
+ "Distributor ID:\t%255[^\n]\n"
+ "Description:\t%255[^\n]\n"
+ "Release:\t%255[^\n]\n"
+ "Codename:\t%255[^\n]\n",
+ dist, desc, release, codename) != 4) {
+ NS_WARNING("Failed to parse lsb_release!");
+ return false;
+ }
+
+ aDistributor.Assign(dist);
+ aDescription.Assign(desc);
+ aRelease.Assign(release);
+ aCodename.Assign(codename);
+ return true;
+}
+
+} // namespace mozilla::widget::lsb
diff --git a/widget/LSBUtils.h b/widget/LSBUtils.h
new file mode 100644
index 0000000000..37d4c400ad
--- /dev/null
+++ b/widget/LSBUtils.h
@@ -0,0 +1,25 @@
+/* -*- 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/. */
+
+#ifndef _MOZILLA_WIDGET_LSB_UTILS_H
+#define _MOZILLA_WIDGET_LSB_UTILS_H
+
+#include "nsString.h"
+
+namespace mozilla {
+namespace widget {
+namespace lsb {
+
+// Fetches the LSB release data by parsing the lsb_release command.
+// Returns false if the lsb_release command was not found, or parsing failed.
+bool GetLSBRelease(nsACString& aDistributor, nsACString& aDescription,
+ nsACString& aRelease, nsACString& aCodename);
+
+} // namespace lsb
+} // namespace widget
+} // namespace mozilla
+
+#endif // _MOZILLA_WIDGET_LSB_UTILS_H
diff --git a/widget/LookAndFeel.h b/widget/LookAndFeel.h
new file mode 100644
index 0000000000..2ab61df304
--- /dev/null
+++ b/widget/LookAndFeel.h
@@ -0,0 +1,585 @@
+/* -*- 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/. */
+
+#ifndef __LookAndFeel
+#define __LookAndFeel
+
+#ifndef MOZILLA_INTERNAL_API
+# error "This header is only usable from within libxul (MOZILLA_INTERNAL_API)."
+#endif
+
+#include "nsDebug.h"
+#include "nsColor.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/widget/ThemeChangeKind.h"
+#include "mozilla/ColorScheme.h"
+
+struct gfxFontStyle;
+
+class nsIFrame;
+
+namespace mozilla {
+
+using Modifiers = uint16_t;
+struct StyleColorSchemeFlags;
+
+namespace dom {
+class Document;
+}
+
+namespace widget {
+class FullLookAndFeel;
+} // namespace widget
+
+enum class StyleSystemColor : uint8_t;
+enum class StyleSystemColorScheme : uint8_t;
+enum class StyleSystemFont : uint8_t;
+
+class LookAndFeel {
+ public:
+ using ColorID = StyleSystemColor;
+ using ColorScheme = mozilla::ColorScheme;
+
+ // When modifying this list, also modify nsXPLookAndFeel::sIntPrefs
+ // in widget/xpwidgts/nsXPLookAndFeel.cpp.
+ enum class IntID {
+ // default, may be overriden by OS
+ CaretBlinkTime,
+ // Amount of blinks that happen before the caret stops blinking.
+ CaretBlinkCount,
+ // pixel width of caret
+ CaretWidth,
+ // show the caret when text is selected?
+ ShowCaretDuringSelection,
+ // select textfields when focused via tab/accesskey?
+ SelectTextfieldsOnKeyFocus,
+ // delay before submenus open
+ SubmenuDelay,
+ // can popups overlap menu/task bar?
+ MenusCanOverlapOSBar,
+ // should overlay scrollbars be used?
+ UseOverlayScrollbars,
+ // allow H and V overlay scrollbars to overlap?
+ AllowOverlayScrollbarsOverlap,
+ // skip navigating to disabled menu item?
+ SkipNavigatingDisabledMenuItem,
+ // begin a drag if the mouse is moved further than the threshold while the
+ // button is down
+ DragThresholdX,
+ DragThresholdY,
+ // Accessibility theme being used?
+ UseAccessibilityTheme,
+
+ // position of scroll arrows in a scrollbar
+ ScrollArrowStyle,
+
+ // each button can take one of four values:
+ ScrollButtonLeftMouseButtonAction,
+ // 0 - scrolls one line, 1 - scrolls one page
+ ScrollButtonMiddleMouseButtonAction,
+ // 2 - scrolls to end, 3 - button ignored
+ ScrollButtonRightMouseButtonAction,
+
+ // delay for opening spring loaded folders
+ TreeOpenDelay,
+ // delay for closing spring loaded folders
+ TreeCloseDelay,
+ // delay for triggering the tree scrolling
+ TreeLazyScrollDelay,
+ // delay for scrolling the tree
+ TreeScrollDelay,
+ // the maximum number of lines to be scrolled at ones
+ TreeScrollLinesMax,
+ // What type of tab-order to use
+ TabFocusModel,
+ // Should menu items blink when they're chosen?
+ ChosenMenuItemsShouldBlink,
+
+ /*
+ * A Boolean value to determine whether the Windows accent color
+ * should be applied to the title bar.
+ *
+ * The value of this metric is not used on other platforms. These platforms
+ * should return NS_ERROR_NOT_IMPLEMENTED when queried for this metric.
+ */
+ WindowsAccentColorInTitlebar,
+
+ /*
+ * A Boolean value to determine whether the macOS Big Sur-specific
+ * theming should be used.
+ */
+ MacBigSurTheme,
+
+ /*
+ * A Boolean value to determine whether macOS is in RTL mode or not.
+ */
+ MacRTL,
+
+ /*
+ * AlertNotificationOrigin indicates from which corner of the
+ * screen alerts slide in, and from which direction (horizontal/vertical).
+ * 0, the default, represents bottom right, sliding vertically.
+ * Use any bitwise combination of the following constants:
+ * NS_ALERT_HORIZONTAL (1), NS_ALERT_LEFT (2), NS_ALERT_TOP (4).
+ *
+ * 6 4
+ * +-----------+
+ * 7| |5
+ * | |
+ * 3| |1
+ * +-----------+
+ * 2 0
+ */
+ AlertNotificationOrigin,
+
+ /**
+ * If true, clicking on a scrollbar (not as in dragging the thumb) defaults
+ * to scrolling the view corresponding to the clicked point. Otherwise, we
+ * only do so if the scrollbar is clicked using the middle mouse button or
+ * if shift is pressed when the scrollbar is clicked.
+ */
+ ScrollToClick,
+
+ /**
+ * IME and spell checker underline styles, the values should be
+ * NS_DECORATION_LINE_STYLE_*. They are defined below.
+ */
+ IMERawInputUnderlineStyle,
+ IMESelectedRawTextUnderlineStyle,
+ IMEConvertedTextUnderlineStyle,
+ IMESelectedConvertedTextUnderline,
+ SpellCheckerUnderlineStyle,
+
+ /**
+ * If this metric != 0, support window dragging on the menubar.
+ */
+ MenuBarDrag,
+ /**
+ * 0: scrollbar button repeats to scroll only when cursor is on the button.
+ * 1: scrollbar button repeats to scroll even if cursor is outside of it.
+ */
+ ScrollbarButtonAutoRepeatBehavior,
+ /**
+ * Delay before showing a tooltip.
+ */
+ TooltipDelay,
+ /*
+ * A Boolean value to determine whether swipe animations should be used.
+ */
+ SwipeAnimationEnabled,
+
+ /*
+ * Controls whether overlay scrollbars display when the user moves
+ * the mouse in a scrollable frame.
+ */
+ ScrollbarDisplayOnMouseMove,
+
+ /*
+ * Overlay scrollbar animation constants.
+ */
+ ScrollbarFadeBeginDelay,
+ ScrollbarFadeDuration,
+
+ /**
+ * Distance in pixels to offset the context menu from the cursor
+ * on open.
+ */
+ ContextMenuOffsetVertical,
+ ContextMenuOffsetHorizontal,
+
+ /*
+ * A boolean value indicating whether client-side decorations are
+ * supported by the user's GTK version.
+ */
+ GTKCSDAvailable,
+
+ /*
+ * A boolean value indicating whether client-side decorations should
+ * contain a minimize button.
+ */
+ GTKCSDMinimizeButton,
+
+ /*
+ * A boolean value indicating whether client-side decorations should
+ * contain a maximize button.
+ */
+ GTKCSDMaximizeButton,
+
+ /*
+ * A boolean value indicating whether client-side decorations should
+ * contain a close button.
+ */
+ GTKCSDCloseButton,
+
+ /**
+ * An Integer value that will represent the position of the Minimize button
+ * in GTK Client side decoration header.
+ */
+ GTKCSDMinimizeButtonPosition,
+
+ /**
+ * An Integer value that will represent the position of the Maximize button
+ * in GTK Client side decoration header.
+ */
+ GTKCSDMaximizeButtonPosition,
+
+ /**
+ * An Integer value that will represent the position of the Close button
+ * in GTK Client side decoration header.
+ */
+ GTKCSDCloseButtonPosition,
+
+ /*
+ * A boolean value indicating whether titlebar buttons are located
+ * in left titlebar corner.
+ */
+ GTKCSDReversedPlacement,
+
+ /*
+ * A boolean value indicating whether or not the OS is using a dark theme,
+ * which we may want to switch to as well if not overridden by the user.
+ */
+ SystemUsesDarkTheme,
+
+ /**
+ * Corresponding to prefers-reduced-motion.
+ * https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion
+ * 0: no-preference
+ * 1: reduce
+ */
+ PrefersReducedMotion,
+
+ /**
+ * Corresponding to prefers-reduced-transparency.
+ * https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-transparency
+ * 0: no-preference
+ * 1: reduce
+ */
+ PrefersReducedTransparency,
+
+ /**
+ * Corresponding to inverted-colors.
+ * https://drafts.csswg.org/mediaqueries-5/#inverted
+ * 0: none
+ * 1: inverted
+ */
+ InvertedColors,
+
+ /**
+ * Corresponding to PointerCapabilities in ServoTypes.h
+ * 0: None
+ * 1: Coarse
+ * 2: Fine
+ * 4: Hover
+ */
+ PrimaryPointerCapabilities,
+ /**
+ * Corresponding to union of PointerCapabilities values in ServoTypes.h
+ * E.g. if there is a mouse and a digitizer, the value will be
+ * 'Coarse | Fine | Hover'.
+ */
+ AllPointerCapabilities,
+
+ /** The scrollbar size, in CSS pixels. */
+ SystemScrollbarSize,
+
+ /** A boolean value to determine whether a touch device is present */
+ TouchDeviceSupportPresent,
+
+ /** GTK titlebar radius */
+ TitlebarRadius,
+
+ /**
+ * Corresponding to dynamic-range.
+ * https://drafts.csswg.org/mediaqueries-5/#dynamic-range
+ * 0: Standard
+ * 1: High
+ */
+ DynamicRange,
+ VideoDynamicRange,
+
+ /** Whether XUL panel animations are enabled. */
+ PanelAnimations,
+
+ /* Whether we should hide the cursor while typing */
+ HideCursorWhileTyping,
+
+ /* The StyleGtkThemeFamily of the current GTK theme. */
+ GTKThemeFamily,
+
+ /*
+ * Not an ID; used to define the range of valid IDs. Must be last.
+ */
+ End,
+ };
+
+ // This is a common enough integer that seems worth the shortcut.
+ static bool UseOverlayScrollbars() {
+ return GetInt(IntID::UseOverlayScrollbars);
+ }
+
+ // Returns keyCode value of a modifier key which is used for accesskey.
+ // Returns 0 if the platform doesn't support access key.
+ static uint32_t GetMenuAccessKey();
+ // Modifier mask for the menu accesskey.
+ static Modifiers GetMenuAccessKeyModifiers();
+
+ enum {
+ eScrollArrow_None = 0,
+ eScrollArrow_StartBackward = 0x1000,
+ eScrollArrow_StartForward = 0x0100,
+ eScrollArrow_EndBackward = 0x0010,
+ eScrollArrow_EndForward = 0x0001
+ };
+
+ enum {
+ // single arrow at each end
+ eScrollArrowStyle_Single =
+ eScrollArrow_StartBackward | eScrollArrow_EndForward,
+ // both arrows at bottom/right, none at top/left
+ eScrollArrowStyle_BothAtBottom =
+ eScrollArrow_EndBackward | eScrollArrow_EndForward,
+ // both arrows at both ends
+ eScrollArrowStyle_BothAtEachEnd =
+ eScrollArrow_EndBackward | eScrollArrow_EndForward |
+ eScrollArrow_StartBackward | eScrollArrow_StartForward,
+ // both arrows at top/left, none at bottom/right
+ eScrollArrowStyle_BothAtTop =
+ eScrollArrow_StartBackward | eScrollArrow_StartForward
+ };
+
+ // When modifying this list, also modify nsXPLookAndFeel::sFloatPrefs
+ // in widget/nsXPLookAndFeel.cpp.
+ enum class FloatID {
+ IMEUnderlineRelativeSize,
+ SpellCheckerUnderlineRelativeSize,
+
+ // The width/height ratio of the cursor. If used, the CaretWidth int metric
+ // should be added to the calculated caret width.
+ CaretAspectRatio,
+
+ // GTK text scale factor.
+ TextScaleFactor,
+
+ // Mouse pointer scaling factor.
+ CursorScale,
+
+ // Not an ID; used to define the range of valid IDs. Must be last.
+ End,
+ };
+
+ using FontID = mozilla::StyleSystemFont;
+
+ static ColorScheme SystemColorScheme() {
+ return GetInt(IntID::SystemUsesDarkTheme) ? ColorScheme::Dark
+ : ColorScheme::Light;
+ }
+
+ static bool IsDarkColor(nscolor);
+
+ static ColorScheme ColorSchemeForStyle(
+ const dom::Document&, const StyleColorSchemeFlags&,
+ ColorSchemeMode = ColorSchemeMode::Used);
+ static ColorScheme ColorSchemeForFrame(
+ const nsIFrame*, ColorSchemeMode = ColorSchemeMode::Used);
+
+ // Whether standins for native colors should be used (that is, colors faked,
+ // taken from win7, mostly). This forces light appearance, effectively.
+ enum class UseStandins : bool { No, Yes };
+ static UseStandins ShouldUseStandins(const dom::Document&, ColorID);
+
+ // Returns a native color value (might be overwritten by prefs) for a given
+ // color id.
+ //
+ // NOTE:
+ // ColorID::TextSelectForeground might return NS_SAME_AS_FOREGROUND_COLOR.
+ // ColorID::IME* might return NS_TRANSPARENT, NS_SAME_AS_FOREGROUND_COLOR or
+ // NS_40PERCENT_FOREGROUND_COLOR.
+ // These values have particular meaning. Then, they are not an actual
+ // color value.
+ static Maybe<nscolor> GetColor(ColorID, ColorScheme, UseStandins);
+
+ // Gets the color with appropriate defaults for UseStandins, ColorScheme etc
+ // for a given frame.
+ static Maybe<nscolor> GetColor(ColorID, const nsIFrame*);
+
+ // Versions of the above which returns the color if found, or a default (which
+ // defaults to opaque black) otherwise.
+ static nscolor Color(ColorID aId, ColorScheme aScheme,
+ UseStandins aUseStandins,
+ nscolor aDefault = NS_RGB(0, 0, 0)) {
+ return GetColor(aId, aScheme, aUseStandins).valueOr(aDefault);
+ }
+
+ static nscolor Color(ColorID aId, nsIFrame* aFrame,
+ nscolor aDefault = NS_RGB(0, 0, 0)) {
+ return GetColor(aId, aFrame).valueOr(aDefault);
+ }
+
+ static float GetTextScaleFactor() {
+ float f = GetFloat(FloatID::TextScaleFactor, 1.0f);
+ if (MOZ_UNLIKELY(f <= 0.0f)) {
+ return 1.0f;
+ }
+ return f;
+ }
+
+ struct ZoomSettings {
+ float mFullZoom = 1.0f;
+ float mTextZoom = 1.0f;
+ };
+
+ static ZoomSettings SystemZoomSettings();
+
+ /**
+ * GetInt() and GetFloat() return a int or float value for aID. The result
+ * might be distance, time, some flags or a int value which has particular
+ * meaning. See each document at definition of each ID for the detail.
+ * The result is always 0 when they return error. Therefore, if you want to
+ * use a value for the default value, you should use the other method which
+ * returns int or float directly.
+ */
+ static nsresult GetInt(IntID, int32_t* aResult);
+ static nsresult GetFloat(FloatID aID, float* aResult);
+
+ static int32_t GetInt(IntID aID, int32_t aDefault = 0) {
+ int32_t result;
+ if (NS_FAILED(GetInt(aID, &result))) {
+ return aDefault;
+ }
+ return result;
+ }
+
+ static float GetFloat(FloatID aID, float aDefault = 0.0f) {
+ float result;
+ if (NS_FAILED(GetFloat(aID, &result))) {
+ return aDefault;
+ }
+ return result;
+ }
+
+ /**
+ * Retrieve the name and style of a system-theme font. Returns true
+ * if the system theme specifies this font, false if a default should
+ * be used. In the latter case neither aName nor aStyle is modified.
+ *
+ * Size of the font should be in CSS pixels, not device pixels.
+ *
+ * @param aID Which system-theme font is wanted.
+ * @param aName The name of the font to use.
+ * @param aStyle Styling to apply to the font.
+ */
+ static bool GetFont(FontID aID, nsString& aName, gfxFontStyle& aStyle);
+
+ /**
+ * GetPasswordCharacter() returns a unicode character which should be used
+ * for a masked character in password editor. E.g., '*'.
+ */
+ static char16_t GetPasswordCharacter();
+
+ /**
+ * If the latest character in password field shouldn't be hidden by the
+ * result of GetPasswordCharacter(), GetEchoPassword() returns TRUE.
+ * Otherwise, FALSE.
+ */
+ static bool GetEchoPassword();
+
+ /**
+ * Whether we should be drawing in the titlebar by default.
+ */
+ static bool DrawInTitlebar();
+
+ enum class TitlebarAction {
+ None,
+ WindowLower,
+ WindowMenu,
+ WindowMinimize,
+ WindowMaximize,
+ WindowMaximizeToggle,
+ // We don't support more actions (maximize-horizontal, maximize-vertical,..)
+ // as they're implemented as part of Wayland gtk_surface1 protocol
+ // which is not accessible to us.
+ };
+
+ enum class TitlebarEvent {
+ Double_Click,
+ Middle_Click,
+ };
+
+ /**
+ * Get system defined action for titlebar events.
+ */
+ static TitlebarAction GetTitlebarAction(TitlebarEvent aEvent);
+
+ /**
+ * The millisecond to mask password value.
+ * This value is only valid when GetEchoPassword() returns true.
+ */
+ static uint32_t GetPasswordMaskDelay();
+
+ /** Gets theme information for about:support */
+ static void GetThemeInfo(nsACString&);
+
+ /**
+ * When system look and feel is changed, Refresh() must be called. Then,
+ * cached data would be released.
+ */
+ static void Refresh();
+
+ /**
+ * GTK's initialization code can't be run off main thread, call this
+ * if you plan on using LookAndFeel off main thread later.
+ *
+ * This initialized state may get reset due to theme changes, so it
+ * must be called prior to each potential off-main-thread LookAndFeel
+ * call, not just once.
+ */
+ static void NativeInit();
+
+ static void SetData(widget::FullLookAndFeel&& aTables);
+ static void NotifyChangedAllWindows(widget::ThemeChangeKind);
+ static bool HasPendingGlobalThemeChange() { return sGlobalThemeChanged; }
+ static void HandleGlobalThemeChange() {
+ if (MOZ_UNLIKELY(HasPendingGlobalThemeChange())) {
+ DoHandleGlobalThemeChange();
+ }
+ }
+
+ protected:
+ static void DoHandleGlobalThemeChange();
+ // Set to true when ThemeChanged needs to be called on mTheme (and other
+ // global LookAndFeel. This is used because mTheme is a service, so there's
+ // no need to notify it from more than one prescontext.
+ static bool sGlobalThemeChanged;
+};
+
+} // namespace mozilla
+
+// ---------------------------------------------------------------------
+// Special colors for ColorID::IME* and ColorID::SpellCheckerUnderline
+// ---------------------------------------------------------------------
+
+// For background color only.
+constexpr nscolor NS_TRANSPARENT = NS_RGBA(0x01, 0x00, 0x00, 0x00);
+// For foreground color only.
+constexpr nscolor NS_SAME_AS_FOREGROUND_COLOR = NS_RGBA(0x02, 0x00, 0x00, 0x00);
+constexpr nscolor NS_40PERCENT_FOREGROUND_COLOR =
+ NS_RGBA(0x03, 0x00, 0x00, 0x00);
+
+#define NS_IS_SELECTION_SPECIAL_COLOR(c) \
+ ((c) == NS_TRANSPARENT || (c) == NS_SAME_AS_FOREGROUND_COLOR || \
+ (c) == NS_40PERCENT_FOREGROUND_COLOR)
+
+// ------------------------------------------
+// Bits for IntID::AlertNotificationOrigin
+// ------------------------------------------
+
+#define NS_ALERT_HORIZONTAL 1
+#define NS_ALERT_LEFT 2
+#define NS_ALERT_TOP 4
+
+#endif /* __LookAndFeel */
diff --git a/widget/LookAndFeelTypes.ipdlh b/widget/LookAndFeelTypes.ipdlh
new file mode 100644
index 0000000000..73ad01e16a
--- /dev/null
+++ b/widget/LookAndFeelTypes.ipdlh
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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/. */
+
+using nscolor from "nsColor.h";
+
+namespace mozilla {
+namespace widget {
+
+[Comparable] struct LookAndFeelFont {
+ bool haveFont;
+ nsString name;
+ float size;
+ float weight;
+ bool italic;
+};
+
+/**
+ * The format allows for some compression compared with having fixed
+ * length arrays for each value type and some indication of whether
+ * a value is present. This is because not all values are present on
+ * a given platform, and because there is also substantial repetition
+ * of specific values.
+ *
+ * Each of ints, floats, colors, and fonts is an array that stores the
+ * unique values that occur in the LookAndFeel. intMap, floatMap,
+ * colorMap, and fontMap map from value IDs (LookAndFeel::IntID, etc.)
+ * to indexes into the value arrays. The map arrays are of fixed
+ * length, determined by the maximum ID value. If a value for a
+ * particular ID is not present, the entry in the map is set to -1.
+ */
+struct LookAndFeelTables {
+ int32_t[] ints;
+ float[] floats;
+ LookAndFeelFont[] fonts;
+ nscolor[] lightColors;
+ nscolor[] darkColors;
+
+ uint8_t[] intMap;
+ uint8_t[] floatMap;
+ uint8_t[] fontMap;
+ uint8_t[] lightColorMap;
+ uint8_t[] darkColorMap;
+
+ uint16_t passwordChar;
+ bool passwordEcho;
+};
+
+/**
+ * Stores the entirety of a LookAndFeel's data.
+ */
+struct FullLookAndFeel {
+ LookAndFeelTables tables;
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/MediaKeysEventSourceFactory.h b/widget/MediaKeysEventSourceFactory.h
new file mode 100644
index 0000000000..6e913dde22
--- /dev/null
+++ b/widget/MediaKeysEventSourceFactory.h
@@ -0,0 +1,24 @@
+/* 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/. */
+
+#ifndef WIDGET_MEDIAKEYSEVENTSOURCEFACTORY_H_
+#define WIDGET_MEDIAKEYSEVENTSOURCEFACTORY_H_
+
+namespace mozilla {
+namespace dom {
+class MediaControlKeySource;
+} // namespace dom
+} // namespace mozilla
+
+namespace mozilla {
+namespace widget {
+
+// This function declaration is used to create a media keys event source on
+// different platforms, each platform should have their own implementation.
+extern mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource();
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/MiscEvents.h b/widget/MiscEvents.h
new file mode 100644
index 0000000000..44c9d771e2
--- /dev/null
+++ b/widget/MiscEvents.h
@@ -0,0 +1,150 @@
+/* -*- 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/. */
+
+#ifndef mozilla_MiscEvents_h__
+#define mozilla_MiscEvents_h__
+
+#include <stdint.h>
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/Maybe.h"
+#include "nsCOMPtr.h"
+#include "nsAtom.h"
+#include "nsGkAtoms.h"
+#include "nsITransferable.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+namespace dom {
+class PBrowserParent;
+class PBrowserChild;
+} // namespace dom
+
+/******************************************************************************
+ * mozilla::WidgetContentCommandEvent
+ ******************************************************************************/
+
+class WidgetContentCommandEvent : public WidgetGUIEvent {
+ public:
+ virtual WidgetContentCommandEvent* AsContentCommandEvent() override {
+ return this;
+ }
+
+ WidgetContentCommandEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget, bool aOnlyEnabledCheck = false)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget,
+ eContentCommandEventClass),
+ mOnlyEnabledCheck(aOnlyEnabledCheck),
+ mSucceeded(false),
+ mIsEnabled(false) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ // This event isn't an internal event of any DOM event.
+ NS_ASSERTION(!IsAllowedToDispatchDOMEvent(),
+ "WidgetQueryContentEvent needs to support Duplicate()");
+ MOZ_CRASH("WidgetQueryContentEvent doesn't support Duplicate()");
+ return nullptr;
+ }
+
+ // eContentCommandInsertText
+ mozilla::Maybe<nsString> mString; // [in]
+
+ // eContentCommandPasteTransferable
+ nsCOMPtr<nsITransferable> mTransferable; // [in]
+
+ // eContentCommandScroll
+ // for mScroll.mUnit
+ enum { eCmdScrollUnit_Line, eCmdScrollUnit_Page, eCmdScrollUnit_Whole };
+
+ struct ScrollInfo {
+ ScrollInfo()
+ : mAmount(0), mUnit(eCmdScrollUnit_Line), mIsHorizontal(false) {}
+
+ int32_t mAmount; // [in]
+ uint8_t mUnit; // [in]
+ bool mIsHorizontal; // [in]
+ } mScroll;
+
+ bool mOnlyEnabledCheck; // [in]
+
+ bool mSucceeded; // [out]
+ bool mIsEnabled; // [out]
+
+ void AssignContentCommandEventData(const WidgetContentCommandEvent& aEvent,
+ bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ mString = aEvent.mString;
+ mScroll = aEvent.mScroll;
+ mOnlyEnabledCheck = aEvent.mOnlyEnabledCheck;
+ mSucceeded = aEvent.mSucceeded;
+ mIsEnabled = aEvent.mIsEnabled;
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetCommandEvent
+ *
+ * This sends a command to chrome. If you want to request what is performed
+ * in focused content, you should use WidgetContentCommandEvent instead.
+ *
+ * XXX Should be |WidgetChromeCommandEvent|?
+ ******************************************************************************/
+
+class WidgetCommandEvent : public WidgetGUIEvent {
+ public:
+ virtual WidgetCommandEvent* AsCommandEvent() override { return this; }
+
+ protected:
+ WidgetCommandEvent(bool aIsTrusted, nsAtom* aEventType, nsAtom* aCommand,
+ nsIWidget* aWidget, const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, eUnidentifiedEvent, aWidget,
+ eCommandEventClass, aTime),
+ mCommand(aCommand) {
+ mSpecifiedEventType = aEventType;
+ }
+
+ public:
+ /**
+ * Constructor to initialize an app command. This is the only case to
+ * initialize this class as a command in C++ stack.
+ */
+ WidgetCommandEvent(bool aIsTrusted, nsAtom* aCommand, nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetCommandEvent(aIsTrusted, nsGkAtoms::onAppCommand, aCommand,
+ aWidget, aTime) {}
+
+ /**
+ * Constructor to initialize as internal event of dom::CommandEvent.
+ */
+ WidgetCommandEvent()
+ : WidgetCommandEvent(false, nullptr, nullptr, nullptr, nullptr) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eCommandEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetCommandEvent* result = new WidgetCommandEvent(
+ false, mSpecifiedEventType, mCommand, nullptr, this);
+ result->AssignCommandEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ RefPtr<nsAtom> mCommand;
+
+ // XXX Not tested by test_assign_event_data.html
+ void AssignCommandEventData(const WidgetCommandEvent& aEvent,
+ bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ // mCommand must have been initialized with the constructor.
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_MiscEvents_h__
diff --git a/widget/MouseEvents.h b/widget/MouseEvents.h
new file mode 100644
index 0000000000..d29b406524
--- /dev/null
+++ b/widget/MouseEvents.h
@@ -0,0 +1,812 @@
+/* -*- 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/. */
+
+#ifndef mozilla_MouseEvents_h__
+#define mozilla_MouseEvents_h__
+
+#include <stdint.h>
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/ipc/IPCForwards.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+
+namespace dom {
+class PBrowserParent;
+class PBrowserChild;
+class PBrowserBridgeParent;
+} // namespace dom
+
+class WidgetPointerEvent;
+} // namespace mozilla
+
+namespace mozilla {
+class WidgetPointerEventHolder final {
+ public:
+ nsTArray<WidgetPointerEvent> mEvents;
+ NS_INLINE_DECL_REFCOUNTING(WidgetPointerEventHolder)
+
+ private:
+ virtual ~WidgetPointerEventHolder() = default;
+};
+
+/******************************************************************************
+ * mozilla::WidgetPointerHelper
+ ******************************************************************************/
+
+class WidgetPointerHelper {
+ public:
+ uint32_t pointerId;
+ int32_t tiltX;
+ int32_t tiltY;
+ int32_t twist;
+ float tangentialPressure;
+ bool convertToPointer;
+ RefPtr<WidgetPointerEventHolder> mCoalescedWidgetEvents;
+
+ WidgetPointerHelper()
+ : pointerId(0),
+ tiltX(0),
+ tiltY(0),
+ twist(0),
+ tangentialPressure(0),
+ convertToPointer(true) {}
+
+ WidgetPointerHelper(uint32_t aPointerId, uint32_t aTiltX, uint32_t aTiltY,
+ uint32_t aTwist = 0, float aTangentialPressure = 0)
+ : pointerId(aPointerId),
+ tiltX(aTiltX),
+ tiltY(aTiltY),
+ twist(aTwist),
+ tangentialPressure(aTangentialPressure),
+ convertToPointer(true) {}
+
+ explicit WidgetPointerHelper(const WidgetPointerHelper& aHelper) = default;
+
+ void AssignPointerHelperData(const WidgetPointerHelper& aEvent,
+ bool aCopyCoalescedEvents = false) {
+ pointerId = aEvent.pointerId;
+ tiltX = aEvent.tiltX;
+ tiltY = aEvent.tiltY;
+ twist = aEvent.twist;
+ tangentialPressure = aEvent.tangentialPressure;
+ convertToPointer = aEvent.convertToPointer;
+ if (aCopyCoalescedEvents) {
+ mCoalescedWidgetEvents = aEvent.mCoalescedWidgetEvents;
+ }
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetMouseEventBase
+ ******************************************************************************/
+
+class WidgetMouseEventBase : public WidgetInputEvent {
+ private:
+ friend class dom::PBrowserParent;
+ friend class dom::PBrowserChild;
+ friend class dom::PBrowserBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ protected:
+ WidgetMouseEventBase()
+ : mPressure(0),
+ mButton(0),
+ mButtons(0),
+ mInputSource(/* MouseEvent_Binding::MOZ_SOURCE_MOUSE = */ 1) {}
+ // Including MouseEventBinding.h here leads to an include loop, so
+ // we have to hardcode MouseEvent_Binding::MOZ_SOURCE_MOUSE.
+
+ WidgetMouseEventBase(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget, EventClassID aEventClassID,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetInputEvent(aIsTrusted, aMessage, aWidget, aEventClassID, aTime),
+ mPressure(0),
+ mButton(0),
+ mButtons(0),
+ mInputSource(/* MouseEvent_Binding::MOZ_SOURCE_MOUSE = */ 1) {}
+ // Including MouseEventBinding.h here leads to an include loop, so
+ // we have to hardcode MouseEvent_Binding::MOZ_SOURCE_MOUSE.
+
+ public:
+ virtual WidgetMouseEventBase* AsMouseEventBase() override { return this; }
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_CRASH("WidgetMouseEventBase must not be most-subclass");
+ }
+
+ // Finger or touch pressure of event. It ranges between 0.0 and 1.0.
+ float mPressure;
+
+ // Pressed button ID of mousedown or mouseup event.
+ // This is set only when pressing a button causes the event.
+ int16_t mButton;
+
+ // Flags of all pressed buttons at the event fired.
+ // This is set at any mouse event, don't be confused with |mButton|.
+ int16_t mButtons;
+
+ // Possible values a in MouseEvent
+ uint16_t mInputSource;
+
+ bool IsLeftButtonPressed() const {
+ return !!(mButtons & MouseButtonsFlag::ePrimaryFlag);
+ }
+ bool IsRightButtonPressed() const {
+ return !!(mButtons & MouseButtonsFlag::eSecondaryFlag);
+ }
+ bool IsMiddleButtonPressed() const {
+ return !!(mButtons & MouseButtonsFlag::eMiddleFlag);
+ }
+ bool Is4thButtonPressed() const {
+ return !!(mButtons & MouseButtonsFlag::e4thFlag);
+ }
+ bool Is5thButtonPressed() const {
+ return !!(mButtons & MouseButtonsFlag::e5thFlag);
+ }
+
+ void AssignMouseEventBaseData(const WidgetMouseEventBase& aEvent,
+ bool aCopyTargets) {
+ AssignInputEventData(aEvent, aCopyTargets);
+
+ mButton = aEvent.mButton;
+ mButtons = aEvent.mButtons;
+ mPressure = aEvent.mPressure;
+ mInputSource = aEvent.mInputSource;
+ }
+
+ /**
+ * Returns true if left click event.
+ */
+ bool IsLeftClickEvent() const {
+ return mMessage == eMouseClick && mButton == MouseButton::ePrimary;
+ }
+
+ /**
+ * Returns true if this event changes a button state to "pressed".
+ */
+ [[nodiscard]] bool IsPressingButton() const {
+ MOZ_ASSERT(IsTrusted());
+ if (mClass == eMouseEventClass) {
+ return mMessage == eMouseDown;
+ }
+ if (mButton == MouseButton::eNotPressed) {
+ return false;
+ }
+ // If this is an ePointerDown event whose mButton is not "not pressed", this
+ // is a button pressing event.
+ if (mMessage == ePointerDown) {
+ return true;
+ }
+ // If 2 or more buttons are pressed at same time, they are sent with
+ // pointermove rather than pointerdown. Therefore, let's check whether
+ // mButtons contains the proper flag for the pressing button.
+ const bool buttonsContainButton = !!(
+ mButtons & MouseButtonsFlagToChange(static_cast<MouseButton>(mButton)));
+ return mMessage == ePointerMove && buttonsContainButton;
+ }
+
+ /**
+ * Returns true if this event changes a button state to "released".
+ */
+ [[nodiscard]] bool IsReleasingButton() const {
+ MOZ_ASSERT(IsTrusted());
+ if (mClass == eMouseEventClass) {
+ return mMessage == eMouseUp;
+ }
+ if (mButton == MouseButton::eNotPressed) {
+ return false;
+ }
+ // If this is an ePointerUp event whose mButton is not "not pressed", this
+ // is a button release event.
+ if (mMessage == ePointerUp) {
+ return true;
+ }
+ // If the releasing button is not the last button of pressing buttons, web
+ // apps notified by pointermove rather than pointerup. Therefore, let's
+ // check whether mButtons loses the proper flag for the releasing button.
+ const bool buttonsLoseTheButton = !(
+ mButtons & MouseButtonsFlagToChange(static_cast<MouseButton>(mButton)));
+ return mMessage == ePointerMove && buttonsLoseTheButton;
+ }
+
+ /**
+ * Returns true if the input source supports hover state like a mouse.
+ */
+ [[nodiscard]] bool InputSourceSupportsHover() const;
+};
+
+/******************************************************************************
+ * mozilla::WidgetMouseEvent
+ ******************************************************************************/
+
+class WidgetMouseEvent : public WidgetMouseEventBase,
+ public WidgetPointerHelper {
+ private:
+ friend class dom::PBrowserParent;
+ friend class dom::PBrowserChild;
+ friend class dom::PBrowserBridgeParent;
+ ALLOW_DEPRECATED_READPARAM
+
+ public:
+ typedef bool ReasonType;
+ enum Reason : ReasonType { eReal, eSynthesized };
+
+ typedef uint8_t ContextMenuTriggerType;
+ enum ContextMenuTrigger : ContextMenuTriggerType {
+ eNormal,
+ eContextMenuKey,
+ eControlClick
+ };
+
+ typedef uint8_t ExitFromType;
+ enum ExitFrom : ExitFromType {
+ ePlatformChild,
+ ePlatformTopLevel,
+ ePuppet,
+ ePuppetParentToPuppetChild
+ };
+
+ protected:
+ WidgetMouseEvent()
+ : mReason(eReal),
+ mContextMenuTrigger(eNormal),
+ mClickCount(0),
+ mIgnoreRootScrollFrame(false),
+ mClickEventPrevented(false) {}
+
+ WidgetMouseEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ EventClassID aEventClassID, Reason aReason,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget, aEventClassID,
+ aTime),
+ mReason(aReason),
+ mContextMenuTrigger(eNormal),
+ mClickCount(0),
+ mIgnoreRootScrollFrame(false),
+ mClickEventPrevented(false) {}
+
+#ifdef DEBUG
+ void AssertContextMenuEventButtonConsistency() const;
+#endif
+
+ public:
+ virtual WidgetMouseEvent* AsMouseEvent() override { return this; }
+
+ WidgetMouseEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ Reason aReason,
+ ContextMenuTrigger aContextMenuTrigger = eNormal,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget, eMouseEventClass,
+ aTime),
+ mReason(aReason),
+ mContextMenuTrigger(aContextMenuTrigger),
+ mClickCount(0),
+ mIgnoreRootScrollFrame(false),
+ mClickEventPrevented(false) {
+ if (aMessage == eContextMenu) {
+ mButton = (mContextMenuTrigger == eNormal) ? MouseButton::eSecondary
+ : MouseButton::ePrimary;
+ }
+ }
+
+#ifdef DEBUG
+ virtual ~WidgetMouseEvent() { AssertContextMenuEventButtonConsistency(); }
+#endif
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eMouseEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetMouseEvent* result = new WidgetMouseEvent(
+ false, mMessage, nullptr, mReason, mContextMenuTrigger, this);
+ result->AssignMouseEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ // If during mouseup handling we detect that click event might need to be
+ // dispatched, this is setup to be the target of the click event.
+ nsCOMPtr<dom::EventTarget> mClickTarget;
+
+ // mReason indicates the reason why the event is fired:
+ // - Representing mouse operation.
+ // - Synthesized for emulating mousemove event when the content under the
+ // mouse cursor is scrolled.
+ Reason mReason;
+
+ // mContextMenuTrigger is valid only when mMessage is eContextMenu.
+ // This indicates if the context menu event is caused by context menu key or
+ // other reasons (typically, a click of right mouse button).
+ ContextMenuTrigger mContextMenuTrigger;
+
+ // mExitFrom contains a value only when mMessage is eMouseExitFromWidget.
+ // This indicates if the mouse cursor exits from a top level platform widget,
+ // a child widget or a puppet widget.
+ Maybe<ExitFrom> mExitFrom;
+
+ // mClickCount may be non-zero value when mMessage is eMouseDown, eMouseUp,
+ // eMouseClick or eMouseDoubleClick. The number is count of mouse clicks.
+ // Otherwise, this must be 0.
+ uint32_t mClickCount;
+
+ // Whether the event should ignore scroll frame bounds during dispatch.
+ bool mIgnoreRootScrollFrame;
+
+ // Whether the event shouldn't cause click event.
+ bool mClickEventPrevented;
+
+ void AssignMouseEventData(const WidgetMouseEvent& aEvent, bool aCopyTargets) {
+ AssignMouseEventBaseData(aEvent, aCopyTargets);
+ AssignPointerHelperData(aEvent, /* aCopyCoalescedEvents */ true);
+
+ mExitFrom = aEvent.mExitFrom;
+ mClickCount = aEvent.mClickCount;
+ mIgnoreRootScrollFrame = aEvent.mIgnoreRootScrollFrame;
+ mClickEventPrevented = aEvent.mClickEventPrevented;
+ }
+
+ /**
+ * Returns true if the event is a context menu event caused by key.
+ */
+ bool IsContextMenuKeyEvent() const {
+ return mMessage == eContextMenu && mContextMenuTrigger == eContextMenuKey;
+ }
+
+ /**
+ * Returns true if the event is a real mouse event. Otherwise, i.e., it's
+ * a synthesized event by scroll or something, returns false.
+ */
+ bool IsReal() const { return mReason == eReal; }
+
+ /**
+ * Returns true if middle click paste is enabled.
+ */
+ static bool IsMiddleClickPasteEnabled();
+};
+
+/******************************************************************************
+ * mozilla::WidgetDragEvent
+ ******************************************************************************/
+
+class WidgetDragEvent : public WidgetMouseEvent {
+ private:
+ friend class mozilla::dom::PBrowserParent;
+ friend class mozilla::dom::PBrowserChild;
+ ALLOW_DEPRECATED_READPARAM
+
+ protected:
+ WidgetDragEvent()
+ : mUserCancelled(false),
+ mDefaultPreventedOnContent(false),
+ mInHTMLEditorEventListener(false) {}
+
+ public:
+ virtual WidgetDragEvent* AsDragEvent() override { return this; }
+
+ WidgetDragEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetMouseEvent(aIsTrusted, aMessage, aWidget, eDragEventClass, eReal,
+ aTime),
+ mUserCancelled(false),
+ mDefaultPreventedOnContent(false),
+ mInHTMLEditorEventListener(false) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eDragEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetDragEvent* result =
+ new WidgetDragEvent(false, mMessage, nullptr, this);
+ result->AssignDragEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ // The dragging data.
+ nsCOMPtr<dom::DataTransfer> mDataTransfer;
+
+ // If this is true, user has cancelled the drag operation.
+ bool mUserCancelled;
+ // If this is true, the drag event's preventDefault() is called on content.
+ bool mDefaultPreventedOnContent;
+ // If this event is currently being handled by HTMLEditorEventListener.
+ bool mInHTMLEditorEventListener;
+
+ // XXX Not tested by test_assign_event_data.html
+ void AssignDragEventData(const WidgetDragEvent& aEvent, bool aCopyTargets) {
+ AssignMouseEventData(aEvent, aCopyTargets);
+
+ mDataTransfer = aEvent.mDataTransfer;
+ // XXX mUserCancelled isn't copied, is this intentionally?
+ mUserCancelled = false;
+ mDefaultPreventedOnContent = aEvent.mDefaultPreventedOnContent;
+ // XXX mInHTMLEditorEventListener isn't copied, is this intentionally?
+ mInHTMLEditorEventListener = false;
+ }
+
+ bool CanConvertToInputData() const {
+ return mMessage == eDragStart || mMessage == eDragEnd;
+ }
+
+ /**
+ * Should be called before dispatching the DOM tree if this event is
+ * synthesized for tests because drop effect is initialized before
+ * dispatching from widget if it's not synthesized event, but synthesized
+ * events are not initialized in the path.
+ */
+ void InitDropEffectForTests();
+};
+
+/******************************************************************************
+ * mozilla::WidgetMouseScrollEvent
+ *
+ * This is used for legacy DOM mouse scroll events, i.e.,
+ * DOMMouseScroll and MozMousePixelScroll event. These events are NOT hanbled
+ * by ESM even if widget dispatches them. Use new WidgetWheelEvent instead.
+ ******************************************************************************/
+
+class WidgetMouseScrollEvent : public WidgetMouseEventBase {
+ private:
+ WidgetMouseScrollEvent() : mDelta(0), mIsHorizontal(false) {}
+
+ public:
+ virtual WidgetMouseScrollEvent* AsMouseScrollEvent() override { return this; }
+
+ WidgetMouseScrollEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget,
+ eMouseScrollEventClass, aTime),
+ mDelta(0),
+ mIsHorizontal(false) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eMouseScrollEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetMouseScrollEvent* result =
+ new WidgetMouseScrollEvent(false, mMessage, nullptr, this);
+ result->AssignMouseScrollEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ // The delta value of mouse scroll event.
+ // If the event message is eLegacyMouseLineOrPageScroll, the value indicates
+ // scroll amount in lines. However, if the value is
+ // UIEvent::SCROLL_PAGE_UP or UIEvent::SCROLL_PAGE_DOWN, the
+ // value inducates one page scroll. If the event message is
+ // eLegacyMousePixelScroll, the value indicates scroll amount in pixels.
+ int32_t mDelta;
+
+ // If this is true, it may cause to scroll horizontally.
+ // Otherwise, vertically.
+ bool mIsHorizontal;
+
+ void AssignMouseScrollEventData(const WidgetMouseScrollEvent& aEvent,
+ bool aCopyTargets) {
+ AssignMouseEventBaseData(aEvent, aCopyTargets);
+
+ mDelta = aEvent.mDelta;
+ mIsHorizontal = aEvent.mIsHorizontal;
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetWheelEvent
+ ******************************************************************************/
+
+class WidgetWheelEvent : public WidgetMouseEventBase {
+ private:
+ friend class mozilla::dom::PBrowserParent;
+ friend class mozilla::dom::PBrowserChild;
+ ALLOW_DEPRECATED_READPARAM
+
+ WidgetWheelEvent()
+ : mDeltaX(0.0),
+ mDeltaY(0.0),
+ mDeltaZ(0.0),
+ mOverflowDeltaX(0.0),
+ mOverflowDeltaY(0.0)
+ // Including WheelEventBinding.h here leads to an include loop, so
+ // we have to hardcode WheelEvent_Binding::DOM_DELTA_PIXEL.
+ ,
+ mDeltaMode(/* WheelEvent_Binding::DOM_DELTA_PIXEL = */ 0),
+ mLineOrPageDeltaX(0),
+ mLineOrPageDeltaY(0),
+ mScrollType(SCROLL_DEFAULT),
+ mCustomizedByUserPrefs(false),
+ mMayHaveMomentum(false),
+ mIsMomentum(false),
+ mIsNoLineOrPageDelta(false),
+ mViewPortIsOverscrolled(false),
+ mCanTriggerSwipe(false),
+ mAllowToOverrideSystemScrollSpeed(false),
+ mDeltaValuesHorizontalizedForDefaultHandler(false) {}
+
+ public:
+ virtual WidgetWheelEvent* AsWheelEvent() override { return this; }
+
+ WidgetWheelEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget, eWheelEventClass,
+ aTime),
+ mDeltaX(0.0),
+ mDeltaY(0.0),
+ mDeltaZ(0.0),
+ mOverflowDeltaX(0.0),
+ mOverflowDeltaY(0.0)
+ // Including WheelEventBinding.h here leads to an include loop, so
+ // we have to hardcode WheelEvent_Binding::DOM_DELTA_PIXEL.
+ ,
+ mDeltaMode(/* WheelEvent_Binding::DOM_DELTA_PIXEL = */ 0),
+ mLineOrPageDeltaX(0),
+ mLineOrPageDeltaY(0),
+ mScrollType(SCROLL_DEFAULT),
+ mCustomizedByUserPrefs(false),
+ mMayHaveMomentum(false),
+ mIsMomentum(false),
+ mIsNoLineOrPageDelta(false),
+ mViewPortIsOverscrolled(false),
+ mCanTriggerSwipe(false),
+ mAllowToOverrideSystemScrollSpeed(true),
+ mDeltaValuesHorizontalizedForDefaultHandler(false) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eWheelEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetWheelEvent* result =
+ new WidgetWheelEvent(false, mMessage, nullptr, this);
+ result->AssignWheelEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ // Scroll gestures that start at the edge of the scrollable range can result
+ // in a swipe gesture. For the first wheel event of such a gesture, call
+ // TriggersSwipe() after the event has been processed in order to find out
+ // whether a swipe should be started.
+ bool TriggersSwipe() const {
+ return mCanTriggerSwipe && mViewPortIsOverscrolled &&
+ this->mOverflowDeltaX != 0.0;
+ }
+
+ // NOTE: mDeltaX, mDeltaY and mDeltaZ may be customized by
+ // mousewheel.*.delta_multiplier_* prefs which are applied by
+ // EventStateManager. So, after widget dispatches this event,
+ // these delta values may have different values than before.
+ double mDeltaX;
+ double mDeltaY;
+ double mDeltaZ;
+
+ // The mousewheel tick counts.
+ double mWheelTicksX = 0.0;
+ double mWheelTicksY = 0.0;
+
+ enum class DeltaModeCheckingState : uint8_t {
+ // Neither deltaMode nor the delta values have been accessed.
+ Unknown,
+ // The delta values have been accessed, without checking deltaMode first.
+ Unchecked,
+ // The deltaMode has been checked.
+ Checked,
+ };
+
+ // For compat reasons, we might expose a DOM_DELTA_LINE event as
+ // DOM_DELTA_PIXEL instead. Whether we do that depends on whether the event
+ // has been asked for the deltaMode before the deltas. If it has, we assume
+ // that the page will correctly handle DOM_DELTA_LINE. This variable tracks
+ // that state. See bug 1392460.
+ DeltaModeCheckingState mDeltaModeCheckingState =
+ DeltaModeCheckingState::Unknown;
+
+ // The amount of scrolling per line or page, without accounting for mouse
+ // wheel transactions etc.
+ //
+ // Computed by EventStateManager::DeltaAccumulator::InitLineOrPageDelta.
+ nsSize mScrollAmount;
+
+ // overflowed delta values for scroll, these values are set by
+ // EventStateManger. If the default action of the wheel event isn't scroll,
+ // these values are always zero. Otherwise, remaining delta values which are
+ // not used by scroll are set.
+ // NOTE: mDeltaX, mDeltaY and mDeltaZ may be modified by EventStateManager.
+ // However, mOverflowDeltaX and mOverflowDeltaY indicate unused original
+ // delta values which are not applied the delta_multiplier prefs.
+ // So, if widget wanted to know the actual direction to be scrolled,
+ // it would need to check the mDeltaX and mDeltaY.
+ double mOverflowDeltaX;
+ double mOverflowDeltaY;
+
+ // Should be one of WheelEvent_Binding::DOM_DELTA_*
+ uint32_t mDeltaMode;
+
+ // If widget sets mLineOrPageDelta, EventStateManager will dispatch
+ // eLegacyMouseLineOrPageScroll event for compatibility. Note that the delta
+ // value means pages if the mDeltaMode is DOM_DELTA_PAGE, otherwise, lines.
+ int32_t mLineOrPageDeltaX;
+ int32_t mLineOrPageDeltaY;
+
+ // When the default action for an wheel event is moving history or zooming,
+ // need to chose a delta value for doing it.
+ int32_t GetPreferredIntDelta() {
+ if (!mLineOrPageDeltaX && !mLineOrPageDeltaY) {
+ return 0;
+ }
+ if (mLineOrPageDeltaY && !mLineOrPageDeltaX) {
+ return mLineOrPageDeltaY;
+ }
+ if (mLineOrPageDeltaX && !mLineOrPageDeltaY) {
+ return mLineOrPageDeltaX;
+ }
+ if ((mLineOrPageDeltaX < 0 && mLineOrPageDeltaY > 0) ||
+ (mLineOrPageDeltaX > 0 && mLineOrPageDeltaY < 0)) {
+ return 0; // We cannot guess the answer in this case.
+ }
+ return (Abs(mLineOrPageDeltaX) > Abs(mLineOrPageDeltaY))
+ ? mLineOrPageDeltaX
+ : mLineOrPageDeltaY;
+ }
+
+ // Scroll type
+ // The default value is SCROLL_DEFAULT, which means EventStateManager will
+ // select preferred scroll type automatically.
+ enum ScrollType : uint8_t {
+ SCROLL_DEFAULT,
+ SCROLL_SYNCHRONOUSLY,
+ SCROLL_ASYNCHRONOUSLY,
+ SCROLL_SMOOTHLY
+ };
+ ScrollType mScrollType;
+
+ // If the delta values are computed from prefs, this value is true.
+ // Otherwise, i.e., they are computed from native events, false.
+ bool mCustomizedByUserPrefs;
+
+ // true if the momentum events directly tied to this event may follow it.
+ bool mMayHaveMomentum;
+ // true if the event is caused by momentum.
+ bool mIsMomentum;
+
+ // If device event handlers don't know when they should set mLineOrPageDeltaX
+ // and mLineOrPageDeltaY, this is true. Otherwise, false.
+ // If mIsNoLineOrPageDelta is true, ESM will generate
+ // eLegacyMouseLineOrPageScroll events when accumulated delta values reach
+ // a line height.
+ bool mIsNoLineOrPageDelta;
+
+ // Whether or not the parent of the currently overscrolled frame is the
+ // ViewPort. This is false in situations when an element on the page is being
+ // overscrolled (such as a text field), but true when the 'page' is being
+ // overscrolled.
+ bool mViewPortIsOverscrolled;
+
+ // The wheel event can trigger a swipe to start if it's overscrolling the
+ // viewport.
+ bool mCanTriggerSwipe;
+
+ // If mAllowToOverrideSystemScrollSpeed is true, the scroll speed may be
+ // overridden. Otherwise, the scroll speed won't be overridden even if
+ // it's enabled by the pref.
+ bool mAllowToOverrideSystemScrollSpeed;
+
+ // After the event's default action handler has adjusted its delta's values
+ // for horizontalizing a vertical wheel scroll, this variable will be set to
+ // true.
+ bool mDeltaValuesHorizontalizedForDefaultHandler;
+
+ void AssignWheelEventData(const WidgetWheelEvent& aEvent, bool aCopyTargets) {
+ AssignMouseEventBaseData(aEvent, aCopyTargets);
+
+ mDeltaX = aEvent.mDeltaX;
+ mDeltaY = aEvent.mDeltaY;
+ mDeltaZ = aEvent.mDeltaZ;
+ mDeltaMode = aEvent.mDeltaMode;
+ mScrollAmount = aEvent.mScrollAmount;
+ mCustomizedByUserPrefs = aEvent.mCustomizedByUserPrefs;
+ mMayHaveMomentum = aEvent.mMayHaveMomentum;
+ mIsMomentum = aEvent.mIsMomentum;
+ mIsNoLineOrPageDelta = aEvent.mIsNoLineOrPageDelta;
+ mLineOrPageDeltaX = aEvent.mLineOrPageDeltaX;
+ mLineOrPageDeltaY = aEvent.mLineOrPageDeltaY;
+ mScrollType = aEvent.mScrollType;
+ mOverflowDeltaX = aEvent.mOverflowDeltaX;
+ mOverflowDeltaY = aEvent.mOverflowDeltaY;
+ mViewPortIsOverscrolled = aEvent.mViewPortIsOverscrolled;
+ mCanTriggerSwipe = aEvent.mCanTriggerSwipe;
+ mAllowToOverrideSystemScrollSpeed =
+ aEvent.mAllowToOverrideSystemScrollSpeed;
+ mDeltaValuesHorizontalizedForDefaultHandler =
+ aEvent.mDeltaValuesHorizontalizedForDefaultHandler;
+ }
+
+ // System scroll speed settings may be too slow at using Gecko. In such
+ // case, we should override the scroll speed computed with system settings.
+ // Following methods return preferred delta values which are multiplied by
+ // factors specified by prefs. If system scroll speed shouldn't be
+ // overridden (e.g., this feature is disabled by pref), they return raw
+ // delta values.
+ double OverriddenDeltaX() const;
+ double OverriddenDeltaY() const;
+
+ // Compute the overridden delta value. This may be useful for suppressing
+ // too fast scroll by system scroll speed overriding when widget sets
+ // mAllowToOverrideSystemScrollSpeed.
+ static double ComputeOverriddenDelta(double aDelta, bool aIsForVertical);
+
+ private:
+ static bool sInitialized;
+ static bool sIsSystemScrollSpeedOverrideEnabled;
+ static int32_t sOverrideFactorX;
+ static int32_t sOverrideFactorY;
+ static void Initialize();
+};
+
+/******************************************************************************
+ * mozilla::WidgetPointerEvent
+ ******************************************************************************/
+
+class WidgetPointerEvent : public WidgetMouseEvent {
+ friend class mozilla::dom::PBrowserParent;
+ friend class mozilla::dom::PBrowserChild;
+ ALLOW_DEPRECATED_READPARAM
+
+ public:
+ virtual WidgetPointerEvent* AsPointerEvent() override { return this; }
+
+ WidgetPointerEvent(bool aIsTrusted, EventMessage aMsg, nsIWidget* w,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetMouseEvent(aIsTrusted, aMsg, w, ePointerEventClass, eReal, aTime),
+ mWidth(1),
+ mHeight(1),
+ mIsPrimary(true),
+ mFromTouchEvent(false) {}
+
+ explicit WidgetPointerEvent(const WidgetMouseEvent& aEvent)
+ : WidgetMouseEvent(aEvent),
+ mWidth(1),
+ mHeight(1),
+ mIsPrimary(true),
+ mFromTouchEvent(false) {
+ mClass = ePointerEventClass;
+ }
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == ePointerEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetPointerEvent* result =
+ new WidgetPointerEvent(false, mMessage, nullptr, this);
+ result->AssignPointerEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ int32_t mWidth;
+ int32_t mHeight;
+ bool mIsPrimary;
+ bool mFromTouchEvent;
+
+ // XXX Not tested by test_assign_event_data.html
+ void AssignPointerEventData(const WidgetPointerEvent& aEvent,
+ bool aCopyTargets) {
+ AssignMouseEventData(aEvent, aCopyTargets);
+
+ mWidth = aEvent.mWidth;
+ mHeight = aEvent.mHeight;
+ mIsPrimary = aEvent.mIsPrimary;
+ mFromTouchEvent = aEvent.mFromTouchEvent;
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_MouseEvents_h__
diff --git a/widget/NativeKeyBindingsType.h b/widget/NativeKeyBindingsType.h
new file mode 100644
index 0000000000..a5885a91ef
--- /dev/null
+++ b/widget/NativeKeyBindingsType.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_NativeKeyBindingsType_h
+#define mozilla_NativeKeyBindingsType_h
+
+namespace mozilla {
+
+enum class NativeKeyBindingsType : uint8_t {
+ SingleLineEditor, // <input type="text"> etc
+ MultiLineEditor, // <textarea>
+ RichTextEditor, // contenteditable or designMode
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_NativeKeyBindings_h
diff --git a/widget/NativeKeyToDOMCodeName.h b/widget/NativeKeyToDOMCodeName.h
new file mode 100644
index 0000000000..7a819a9f3c
--- /dev/null
+++ b/widget/NativeKeyToDOMCodeName.h
@@ -0,0 +1,821 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/**
+ * This header file defines simple code mapping between native scancode or
+ * something and DOM code name index.
+ * You must define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX macro before include
+ * this.
+ *
+ * It must have two arguments, (aNativeKey, aCodeNameIndex).
+ * aNativeKey is a scancode value or something (depends on the platform).
+ * aCodeNameIndex is the widget::CodeNameIndex value.
+ */
+
+// Windows
+#define CODE_MAP_WIN(aCPPCodeName, aNativeKey)
+// Mac OS X
+#define CODE_MAP_MAC(aCPPCodeName, aNativeKey)
+// GTK and Qt on Linux
+#define CODE_MAP_X11(aCPPCodeName, aNativeKey)
+// Android
+#define CODE_MAP_ANDROID(aCPPCodeName, aNativeKey)
+
+#if defined(XP_WIN)
+# undef CODE_MAP_WIN
+// aNativeKey is scan code
+# define CODE_MAP_WIN(aCPPCodeName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, \
+ CODE_NAME_INDEX_##aCPPCodeName)
+#elif defined(XP_MACOSX)
+# undef CODE_MAP_MAC
+// aNativeKey is key code starting with kVK_.
+# define CODE_MAP_MAC(aCPPCodeName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, \
+ CODE_NAME_INDEX_##aCPPCodeName)
+#elif defined(MOZ_WIDGET_GTK)
+# undef CODE_MAP_X11
+// aNativeKey is hardware_keycode of GDKEvent or nativeScanCode of QKeyEvent.
+# define CODE_MAP_X11(aCPPCodeName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, \
+ CODE_NAME_INDEX_##aCPPCodeName)
+#elif defined(ANDROID)
+# undef CODE_MAP_ANDROID
+// aNativeKey is scan code
+# define CODE_MAP_ANDROID(aCPPCodeName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, \
+ CODE_NAME_INDEX_##aCPPCodeName)
+#endif
+
+// Writing system keys
+CODE_MAP_WIN(Backquote, 0x0029)
+CODE_MAP_MAC(Backquote, kVK_ANSI_Grave)
+CODE_MAP_X11(Backquote, 0x0031)
+CODE_MAP_ANDROID(Backquote, 0x0029)
+
+CODE_MAP_WIN(Backslash, 0x002B)
+CODE_MAP_MAC(Backslash, kVK_ANSI_Backslash)
+CODE_MAP_X11(Backslash, 0x0033)
+CODE_MAP_ANDROID(Backslash, 0x002B)
+
+CODE_MAP_WIN(Backspace, 0x000E)
+CODE_MAP_MAC(Backspace, kVK_Delete)
+CODE_MAP_X11(Backspace, 0x0016)
+CODE_MAP_ANDROID(Backspace, 0x000E)
+
+CODE_MAP_WIN(BracketLeft, 0x001A)
+CODE_MAP_MAC(BracketLeft, kVK_ANSI_LeftBracket)
+CODE_MAP_X11(BracketLeft, 0x0022)
+CODE_MAP_ANDROID(BracketLeft, 0x001A)
+
+CODE_MAP_WIN(BracketRight, 0x001B)
+CODE_MAP_MAC(BracketRight, kVK_ANSI_RightBracket)
+CODE_MAP_X11(BracketRight, 0x0023)
+CODE_MAP_ANDROID(BracketRight, 0x001B)
+
+CODE_MAP_WIN(Comma, 0x0033)
+CODE_MAP_MAC(Comma, kVK_ANSI_Comma)
+CODE_MAP_X11(Comma, 0x003B)
+CODE_MAP_ANDROID(Comma, 0x00033)
+
+CODE_MAP_WIN(Digit0, 0x000B)
+CODE_MAP_MAC(Digit0, kVK_ANSI_0)
+CODE_MAP_X11(Digit0, 0x0013)
+CODE_MAP_ANDROID(Digit0, 0x000B)
+
+CODE_MAP_WIN(Digit1, 0x0002)
+CODE_MAP_MAC(Digit1, kVK_ANSI_1)
+CODE_MAP_X11(Digit1, 0x000A)
+CODE_MAP_ANDROID(Digit1, 0x0002)
+
+CODE_MAP_WIN(Digit2, 0x0003)
+CODE_MAP_MAC(Digit2, kVK_ANSI_2)
+CODE_MAP_X11(Digit2, 0x000B)
+CODE_MAP_ANDROID(Digit2, 0x0003)
+
+CODE_MAP_WIN(Digit3, 0x0004)
+CODE_MAP_MAC(Digit3, kVK_ANSI_3)
+CODE_MAP_X11(Digit3, 0x000C)
+CODE_MAP_ANDROID(Digit3, 0x0004)
+
+CODE_MAP_WIN(Digit4, 0x0005)
+CODE_MAP_MAC(Digit4, kVK_ANSI_4)
+CODE_MAP_X11(Digit4, 0x000D)
+CODE_MAP_ANDROID(Digit4, 0x0005)
+
+CODE_MAP_WIN(Digit5, 0x0006)
+CODE_MAP_MAC(Digit5, kVK_ANSI_5)
+CODE_MAP_X11(Digit5, 0x000E)
+CODE_MAP_ANDROID(Digit5, 0x0006)
+
+CODE_MAP_WIN(Digit6, 0x0007)
+CODE_MAP_MAC(Digit6, kVK_ANSI_6)
+CODE_MAP_X11(Digit6, 0x000F)
+CODE_MAP_ANDROID(Digit6, 0x0007)
+
+CODE_MAP_WIN(Digit7, 0x0008)
+CODE_MAP_MAC(Digit7, kVK_ANSI_7)
+CODE_MAP_X11(Digit7, 0x0010)
+CODE_MAP_ANDROID(Digit7, 0x0008)
+
+CODE_MAP_WIN(Digit8, 0x0009)
+CODE_MAP_MAC(Digit8, kVK_ANSI_8)
+CODE_MAP_X11(Digit8, 0x0011)
+CODE_MAP_ANDROID(Digit8, 0x0009)
+
+CODE_MAP_WIN(Digit9, 0x000A)
+CODE_MAP_MAC(Digit9, kVK_ANSI_9)
+CODE_MAP_X11(Digit9, 0x0012)
+CODE_MAP_ANDROID(Digit9, 0x000A)
+
+CODE_MAP_WIN(Equal, 0x000D)
+CODE_MAP_MAC(Equal, kVK_ANSI_Equal)
+CODE_MAP_X11(Equal, 0x0015)
+CODE_MAP_ANDROID(Equal, 0x000D)
+
+CODE_MAP_WIN(IntlBackslash, 0x0056)
+CODE_MAP_MAC(IntlBackslash, kVK_ISO_Section)
+CODE_MAP_X11(IntlBackslash, 0x005E)
+CODE_MAP_ANDROID(IntlBackslash, 0x0056)
+
+// Win: IntlHash's scan code is shared with "Backslash" key.
+// Mac: IntlHash's virtual key code is shared with "Backslash" key.
+// X11: IntlHash's scan code is shared with "Backslash" key.
+// Android: IntlHash's scan code is shared with "Backslash" key.
+
+CODE_MAP_WIN(IntlRo, 0x0073)
+CODE_MAP_MAC(IntlRo, kVK_JIS_Underscore)
+CODE_MAP_X11(IntlRo, 0x0061)
+CODE_MAP_ANDROID(IntlRo, 0x0059)
+
+CODE_MAP_WIN(IntlYen, 0x007D)
+CODE_MAP_MAC(IntlYen, kVK_JIS_Yen)
+CODE_MAP_X11(IntlYen, 0x0084)
+CODE_MAP_ANDROID(IntlYen, 0x007C)
+
+CODE_MAP_WIN(KeyA, 0x001E)
+CODE_MAP_MAC(KeyA, kVK_ANSI_A)
+CODE_MAP_X11(KeyA, 0x0026)
+CODE_MAP_ANDROID(KeyA, 0x001E)
+
+CODE_MAP_WIN(KeyB, 0x0030)
+CODE_MAP_MAC(KeyB, kVK_ANSI_B)
+CODE_MAP_X11(KeyB, 0x0038)
+CODE_MAP_ANDROID(KeyB, 0x0030)
+
+CODE_MAP_WIN(KeyC, 0x002E)
+CODE_MAP_MAC(KeyC, kVK_ANSI_C)
+CODE_MAP_X11(KeyC, 0x0036)
+CODE_MAP_ANDROID(KeyC, 0x002E)
+
+CODE_MAP_WIN(KeyD, 0x0020)
+CODE_MAP_MAC(KeyD, kVK_ANSI_D)
+CODE_MAP_X11(KeyD, 0x0028)
+CODE_MAP_ANDROID(KeyD, 0x0020)
+
+CODE_MAP_WIN(KeyE, 0x0012)
+CODE_MAP_MAC(KeyE, kVK_ANSI_E)
+CODE_MAP_X11(KeyE, 0x001A)
+CODE_MAP_ANDROID(KeyE, 0x0012)
+
+CODE_MAP_WIN(KeyF, 0x0021)
+CODE_MAP_MAC(KeyF, kVK_ANSI_F)
+CODE_MAP_X11(KeyF, 0x0029)
+CODE_MAP_ANDROID(KeyF, 0x0021)
+
+CODE_MAP_WIN(KeyG, 0x0022)
+CODE_MAP_MAC(KeyG, kVK_ANSI_G)
+CODE_MAP_X11(KeyG, 0x002A)
+CODE_MAP_ANDROID(KeyG, 0x0022)
+
+CODE_MAP_WIN(KeyH, 0x0023)
+CODE_MAP_MAC(KeyH, kVK_ANSI_H)
+CODE_MAP_X11(KeyH, 0x002B)
+CODE_MAP_ANDROID(KeyH, 0x0023)
+
+CODE_MAP_WIN(KeyI, 0x0017)
+CODE_MAP_MAC(KeyI, kVK_ANSI_I)
+CODE_MAP_X11(KeyI, 0x001F)
+CODE_MAP_ANDROID(KeyI, 0x0017)
+
+CODE_MAP_WIN(KeyJ, 0x0024)
+CODE_MAP_MAC(KeyJ, kVK_ANSI_J)
+CODE_MAP_X11(KeyJ, 0x002C)
+CODE_MAP_ANDROID(KeyJ, 0x0024)
+
+CODE_MAP_WIN(KeyK, 0x0025)
+CODE_MAP_MAC(KeyK, kVK_ANSI_K)
+CODE_MAP_X11(KeyK, 0x002D)
+CODE_MAP_ANDROID(KeyK, 0x0025)
+
+CODE_MAP_WIN(KeyL, 0x0026)
+CODE_MAP_MAC(KeyL, kVK_ANSI_L)
+CODE_MAP_X11(KeyL, 0x002E)
+CODE_MAP_ANDROID(KeyL, 0x0026)
+
+CODE_MAP_WIN(KeyM, 0x0032)
+CODE_MAP_MAC(KeyM, kVK_ANSI_M)
+CODE_MAP_X11(KeyM, 0x003A)
+CODE_MAP_ANDROID(KeyM, 0x0032)
+
+CODE_MAP_WIN(KeyN, 0x0031)
+CODE_MAP_MAC(KeyN, kVK_ANSI_N)
+CODE_MAP_X11(KeyN, 0x0039)
+CODE_MAP_ANDROID(KeyN, 0x0031)
+
+CODE_MAP_WIN(KeyO, 0x0018)
+CODE_MAP_MAC(KeyO, kVK_ANSI_O)
+CODE_MAP_X11(KeyO, 0x0020)
+CODE_MAP_ANDROID(KeyO, 0x0018)
+
+CODE_MAP_WIN(KeyP, 0x0019)
+CODE_MAP_MAC(KeyP, kVK_ANSI_P)
+CODE_MAP_X11(KeyP, 0x0021)
+CODE_MAP_ANDROID(KeyP, 0x0019)
+
+CODE_MAP_WIN(KeyQ, 0x0010)
+CODE_MAP_MAC(KeyQ, kVK_ANSI_Q)
+CODE_MAP_X11(KeyQ, 0x0018)
+CODE_MAP_ANDROID(KeyQ, 0x0010)
+
+CODE_MAP_WIN(KeyR, 0x0013)
+CODE_MAP_MAC(KeyR, kVK_ANSI_R)
+CODE_MAP_X11(KeyR, 0x001B)
+CODE_MAP_ANDROID(KeyR, 0x0013)
+
+CODE_MAP_WIN(KeyS, 0x001F)
+CODE_MAP_MAC(KeyS, kVK_ANSI_S)
+CODE_MAP_X11(KeyS, 0x0027)
+CODE_MAP_ANDROID(KeyS, 0x001F)
+
+CODE_MAP_WIN(KeyT, 0x0014)
+CODE_MAP_MAC(KeyT, kVK_ANSI_T)
+CODE_MAP_X11(KeyT, 0x001C)
+CODE_MAP_ANDROID(KeyT, 0x0014)
+
+CODE_MAP_WIN(KeyU, 0x0016)
+CODE_MAP_MAC(KeyU, kVK_ANSI_U)
+CODE_MAP_X11(KeyU, 0x001E)
+CODE_MAP_ANDROID(KeyU, 0x0016)
+
+CODE_MAP_WIN(KeyV, 0x002F)
+CODE_MAP_MAC(KeyV, kVK_ANSI_V)
+CODE_MAP_X11(KeyV, 0x0037)
+CODE_MAP_ANDROID(KeyV, 0x002F)
+
+CODE_MAP_WIN(KeyW, 0x0011)
+CODE_MAP_MAC(KeyW, kVK_ANSI_W)
+CODE_MAP_X11(KeyW, 0x0019)
+CODE_MAP_ANDROID(KeyW, 0x0011)
+
+CODE_MAP_WIN(KeyX, 0x002D)
+CODE_MAP_MAC(KeyX, kVK_ANSI_X)
+CODE_MAP_X11(KeyX, 0x0035)
+CODE_MAP_ANDROID(KeyX, 0x002D)
+
+CODE_MAP_WIN(KeyY, 0x0015)
+CODE_MAP_MAC(KeyY, kVK_ANSI_Y)
+CODE_MAP_X11(KeyY, 0x001D)
+CODE_MAP_ANDROID(KeyY, 0x0015)
+
+CODE_MAP_WIN(KeyZ, 0x002C)
+CODE_MAP_MAC(KeyZ, kVK_ANSI_Z)
+CODE_MAP_X11(KeyZ, 0x0034)
+CODE_MAP_ANDROID(KeyZ, 0x002C)
+
+CODE_MAP_WIN(Minus, 0x000C)
+CODE_MAP_MAC(Minus, kVK_ANSI_Minus)
+CODE_MAP_X11(Minus, 0x0014)
+CODE_MAP_ANDROID(Minus, 0x000C)
+
+CODE_MAP_WIN(Period, 0x0034)
+CODE_MAP_MAC(Period, kVK_ANSI_Period)
+CODE_MAP_X11(Period, 0x003C)
+CODE_MAP_ANDROID(Period, 0x0034)
+
+CODE_MAP_WIN(Quote, 0x0028)
+CODE_MAP_MAC(Quote, kVK_ANSI_Quote)
+CODE_MAP_X11(Quote, 0x0030)
+CODE_MAP_ANDROID(Quote, 0x0028)
+
+CODE_MAP_WIN(Semicolon, 0x0027)
+CODE_MAP_MAC(Semicolon, kVK_ANSI_Semicolon)
+CODE_MAP_X11(Semicolon, 0x002F)
+CODE_MAP_ANDROID(Semicolon, 0x0027)
+
+CODE_MAP_WIN(Slash, 0x0035)
+CODE_MAP_MAC(Slash, kVK_ANSI_Slash)
+CODE_MAP_X11(Slash, 0x003D)
+CODE_MAP_ANDROID(Slash, 0x0035)
+
+// Functional keys
+CODE_MAP_WIN(AltLeft, 0x0038)
+CODE_MAP_MAC(AltLeft, kVK_Option)
+CODE_MAP_X11(AltLeft, 0x0040)
+CODE_MAP_ANDROID(AltLeft, 0x0038)
+
+CODE_MAP_WIN(AltRight, 0xE038)
+CODE_MAP_MAC(AltRight, kVK_RightOption)
+CODE_MAP_X11(AltRight, 0x006C)
+CODE_MAP_ANDROID(AltRight, 0x0064)
+
+CODE_MAP_WIN(CapsLock, 0x003A)
+CODE_MAP_MAC(CapsLock, kVK_CapsLock)
+CODE_MAP_X11(CapsLock, 0x0042)
+CODE_MAP_ANDROID(CapsLock, 0x003A)
+
+CODE_MAP_WIN(ContextMenu, 0xE05D)
+CODE_MAP_MAC(ContextMenu, kVK_PC_ContextMenu)
+CODE_MAP_X11(ContextMenu, 0x0087)
+CODE_MAP_ANDROID(ContextMenu, 0x007F)
+
+CODE_MAP_WIN(ControlLeft, 0x001D)
+CODE_MAP_MAC(ControlLeft, kVK_Control)
+CODE_MAP_X11(ControlLeft, 0x0025)
+CODE_MAP_ANDROID(ControlLeft, 0x001D)
+
+CODE_MAP_WIN(ControlRight, 0xE01D)
+CODE_MAP_MAC(ControlRight, kVK_RightControl)
+CODE_MAP_X11(ControlRight, 0x0069)
+CODE_MAP_ANDROID(ControlRight, 0x0061)
+
+CODE_MAP_WIN(Enter, 0x001C)
+CODE_MAP_MAC(Enter, kVK_Return)
+CODE_MAP_X11(Enter, 0x0024)
+CODE_MAP_ANDROID(Enter, 0x001C)
+
+CODE_MAP_WIN(MetaLeft, 0xE05B)
+CODE_MAP_MAC(MetaLeft, kVK_Command)
+CODE_MAP_X11(MetaLeft, 0x0085)
+CODE_MAP_ANDROID(MetaLeft, 0x007D)
+
+CODE_MAP_WIN(MetaRight, 0xE05C)
+CODE_MAP_MAC(MetaRight, kVK_RightCommand)
+CODE_MAP_X11(MetaRight, 0x0086)
+CODE_MAP_ANDROID(MetaRight, 0x007E)
+
+CODE_MAP_WIN(ShiftLeft, 0x002A)
+CODE_MAP_MAC(ShiftLeft, kVK_Shift)
+CODE_MAP_X11(ShiftLeft, 0x0032)
+CODE_MAP_ANDROID(ShiftLeft, 0x002A)
+
+CODE_MAP_WIN(ShiftRight, 0x0036)
+CODE_MAP_MAC(ShiftRight, kVK_RightShift)
+CODE_MAP_X11(ShiftRight, 0x003E)
+CODE_MAP_ANDROID(ShiftRight, 0x0036)
+
+CODE_MAP_WIN(Space, 0x0039)
+CODE_MAP_MAC(Space, kVK_Space)
+CODE_MAP_X11(Space, 0x0041)
+CODE_MAP_ANDROID(Space, 0x0039)
+
+CODE_MAP_WIN(Tab, 0x000F)
+CODE_MAP_MAC(Tab, kVK_Tab)
+CODE_MAP_X11(Tab, 0x0017)
+CODE_MAP_ANDROID(Tab, 0x000F)
+
+// IME keys
+CODE_MAP_WIN(Convert, 0x0079)
+CODE_MAP_X11(Convert, 0x0064)
+CODE_MAP_ANDROID(Convert, 0x005C)
+
+CODE_MAP_WIN(Lang1, 0x0072) // for non-Korean layout
+CODE_MAP_WIN(Lang1, 0xE0F2) // for Korean layout
+CODE_MAP_MAC(Lang1, kVK_JIS_Kana)
+CODE_MAP_X11(Lang1, 0x0082)
+CODE_MAP_ANDROID(Lang1, 0x007A)
+
+CODE_MAP_WIN(Lang2, 0x0071) // for non-Korean layout
+CODE_MAP_WIN(Lang2, 0xE0F1) // for Korean layout
+CODE_MAP_MAC(Lang2, kVK_JIS_Eisu)
+CODE_MAP_X11(Lang2, 0x0083)
+CODE_MAP_ANDROID(Lang2, 0x007B)
+
+CODE_MAP_WIN(KanaMode, 0x0070)
+CODE_MAP_X11(KanaMode, 0x0065)
+CODE_MAP_ANDROID(KanaMode, 0x005D)
+
+CODE_MAP_WIN(NonConvert, 0x007B)
+CODE_MAP_X11(NonConvert, 0x0066)
+CODE_MAP_ANDROID(NonConvert, 0x005E)
+
+// Control pad section
+CODE_MAP_WIN(Delete, 0xE053)
+CODE_MAP_MAC(Delete, kVK_ForwardDelete)
+CODE_MAP_X11(Delete, 0x0077)
+CODE_MAP_ANDROID(Delete, 0x006F)
+
+CODE_MAP_WIN(End, 0xE04F)
+CODE_MAP_MAC(End, kVK_End)
+CODE_MAP_X11(End, 0x0073)
+CODE_MAP_ANDROID(End, 0x006B)
+
+CODE_MAP_MAC(Help, kVK_Help) // Insert key on PC keyboard
+CODE_MAP_X11(Help, 0x0092) // Help key on Sun keyboard
+CODE_MAP_ANDROID(Help, 0x008A) // Help key on Sun keyboard
+
+CODE_MAP_WIN(Home, 0xE047)
+CODE_MAP_MAC(Home, kVK_Home)
+CODE_MAP_X11(Home, 0x006E)
+CODE_MAP_ANDROID(Home, 0x0066)
+
+CODE_MAP_WIN(Insert, 0xE052)
+CODE_MAP_X11(Insert, 0x0076)
+CODE_MAP_ANDROID(Insert, 0x006E)
+
+CODE_MAP_WIN(PageDown, 0xE051)
+CODE_MAP_MAC(PageDown, kVK_PageDown)
+CODE_MAP_X11(PageDown, 0x0075)
+CODE_MAP_ANDROID(PageDown, 0x006D)
+
+CODE_MAP_WIN(PageUp, 0xE049)
+CODE_MAP_MAC(PageUp, kVK_PageUp)
+CODE_MAP_X11(PageUp, 0x0070)
+CODE_MAP_ANDROID(PageUp, 0x0068)
+
+// Arrow pad section
+CODE_MAP_WIN(ArrowDown, 0xE050)
+CODE_MAP_MAC(ArrowDown, kVK_DownArrow)
+CODE_MAP_X11(ArrowDown, 0x0074)
+CODE_MAP_ANDROID(ArrowDown, 0x006C)
+
+CODE_MAP_WIN(ArrowLeft, 0xE04B)
+CODE_MAP_MAC(ArrowLeft, kVK_LeftArrow)
+CODE_MAP_X11(ArrowLeft, 0x0071)
+CODE_MAP_ANDROID(ArrowLeft, 0x0069)
+
+CODE_MAP_WIN(ArrowRight, 0xE04D)
+CODE_MAP_MAC(ArrowRight, kVK_RightArrow)
+CODE_MAP_X11(ArrowRight, 0x0072)
+CODE_MAP_ANDROID(ArrowRight, 0x006A)
+
+CODE_MAP_WIN(ArrowUp, 0xE048)
+CODE_MAP_MAC(ArrowUp, kVK_UpArrow)
+CODE_MAP_X11(ArrowUp, 0x006F)
+CODE_MAP_ANDROID(ArrowUp, 0x0067)
+
+// Numpad section
+CODE_MAP_WIN(NumLock, 0xE045) // MSDN says 0x0045, though...
+CODE_MAP_MAC(NumLock, kVK_ANSI_KeypadClear)
+CODE_MAP_X11(NumLock, 0x004D)
+CODE_MAP_ANDROID(NumLock, 0x0045)
+
+CODE_MAP_WIN(Numpad0, 0x0052)
+CODE_MAP_MAC(Numpad0, kVK_ANSI_Keypad0)
+CODE_MAP_X11(Numpad0, 0x005A)
+CODE_MAP_ANDROID(Numpad0, 0x0052)
+
+CODE_MAP_WIN(Numpad1, 0x004F)
+CODE_MAP_MAC(Numpad1, kVK_ANSI_Keypad1)
+CODE_MAP_X11(Numpad1, 0x0057)
+CODE_MAP_ANDROID(Numpad1, 0x004F)
+
+CODE_MAP_WIN(Numpad2, 0x0050)
+CODE_MAP_MAC(Numpad2, kVK_ANSI_Keypad2)
+CODE_MAP_X11(Numpad2, 0x0058)
+CODE_MAP_ANDROID(Numpad2, 0x0050)
+
+CODE_MAP_WIN(Numpad3, 0x0051)
+CODE_MAP_MAC(Numpad3, kVK_ANSI_Keypad3)
+CODE_MAP_X11(Numpad3, 0x0059)
+CODE_MAP_ANDROID(Numpad3, 0x0051)
+
+CODE_MAP_WIN(Numpad4, 0x004B)
+CODE_MAP_MAC(Numpad4, kVK_ANSI_Keypad4)
+CODE_MAP_X11(Numpad4, 0x0053)
+CODE_MAP_ANDROID(Numpad4, 0x004B)
+
+CODE_MAP_WIN(Numpad5, 0x004C)
+CODE_MAP_MAC(Numpad5, kVK_ANSI_Keypad5)
+CODE_MAP_X11(Numpad5, 0x0054)
+CODE_MAP_ANDROID(Numpad5, 0x004C)
+
+CODE_MAP_WIN(Numpad6, 0x004D)
+CODE_MAP_MAC(Numpad6, kVK_ANSI_Keypad6)
+CODE_MAP_X11(Numpad6, 0x0055)
+CODE_MAP_ANDROID(Numpad6, 0x004D)
+
+CODE_MAP_WIN(Numpad7, 0x0047)
+CODE_MAP_MAC(Numpad7, kVK_ANSI_Keypad7)
+CODE_MAP_X11(Numpad7, 0x004F)
+CODE_MAP_ANDROID(Numpad7, 0x0047)
+
+CODE_MAP_WIN(Numpad8, 0x0048)
+CODE_MAP_MAC(Numpad8, kVK_ANSI_Keypad8)
+CODE_MAP_X11(Numpad8, 0x0050)
+CODE_MAP_ANDROID(Numpad8, 0x0048)
+
+CODE_MAP_WIN(Numpad9, 0x0049)
+CODE_MAP_MAC(Numpad9, kVK_ANSI_Keypad9)
+CODE_MAP_X11(Numpad9, 0x0051)
+CODE_MAP_ANDROID(Numpad9, 0x0049)
+
+CODE_MAP_WIN(NumpadAdd, 0x004E)
+CODE_MAP_MAC(NumpadAdd, kVK_ANSI_KeypadPlus)
+CODE_MAP_X11(NumpadAdd, 0x0056)
+CODE_MAP_ANDROID(NumpadAdd, 0x004E)
+
+CODE_MAP_WIN(NumpadComma, 0x007E)
+CODE_MAP_MAC(NumpadComma, kVK_JIS_KeypadComma)
+CODE_MAP_X11(NumpadComma, 0x0081)
+CODE_MAP_ANDROID(NumpadComma, 0x0079)
+
+CODE_MAP_WIN(NumpadDecimal, 0x0053)
+CODE_MAP_MAC(NumpadDecimal, kVK_ANSI_KeypadDecimal)
+CODE_MAP_X11(NumpadDecimal, 0x005B)
+CODE_MAP_ANDROID(NumpadDecimal, 0x0053)
+
+CODE_MAP_WIN(NumpadDivide, 0xE035)
+CODE_MAP_MAC(NumpadDivide, kVK_ANSI_KeypadDivide)
+CODE_MAP_X11(NumpadDivide, 0x006A)
+CODE_MAP_ANDROID(NumpadDivide, 0x0062)
+
+CODE_MAP_WIN(NumpadEnter, 0xE01C)
+CODE_MAP_MAC(NumpadEnter, kVK_ANSI_KeypadEnter)
+CODE_MAP_MAC(NumpadEnter, kVK_Powerbook_KeypadEnter)
+CODE_MAP_X11(NumpadEnter, 0x0068)
+CODE_MAP_ANDROID(NumpadEnter, 0x0060)
+
+CODE_MAP_WIN(NumpadEqual, 0x0059)
+CODE_MAP_MAC(NumpadEqual, kVK_ANSI_KeypadEquals)
+CODE_MAP_X11(NumpadEqual, 0x007D)
+CODE_MAP_ANDROID(NumpadEqual, 0x0075)
+
+CODE_MAP_WIN(NumpadMultiply, 0x0037)
+CODE_MAP_MAC(NumpadMultiply, kVK_ANSI_KeypadMultiply)
+CODE_MAP_X11(NumpadMultiply, 0x003F)
+CODE_MAP_ANDROID(NumpadMultiply, 0x0037)
+
+CODE_MAP_WIN(NumpadSubtract, 0x004A)
+CODE_MAP_MAC(NumpadSubtract, kVK_ANSI_KeypadMinus)
+CODE_MAP_X11(NumpadSubtract, 0x0052)
+CODE_MAP_ANDROID(NumpadSubtract, 0x004A)
+
+// Function section
+CODE_MAP_WIN(Escape, 0x0001)
+CODE_MAP_MAC(Escape, kVK_Escape)
+CODE_MAP_X11(Escape, 0x0009)
+CODE_MAP_ANDROID(Escape, 0x0001)
+
+CODE_MAP_WIN(F1, 0x003B)
+CODE_MAP_MAC(F1, kVK_F1)
+CODE_MAP_X11(F1, 0x0043)
+CODE_MAP_ANDROID(F1, 0x003B)
+
+CODE_MAP_WIN(F2, 0x003C)
+CODE_MAP_MAC(F2, kVK_F2)
+CODE_MAP_X11(F2, 0x0044)
+CODE_MAP_ANDROID(F2, 0x003C)
+
+CODE_MAP_WIN(F3, 0x003D)
+CODE_MAP_MAC(F3, kVK_F3)
+CODE_MAP_X11(F3, 0x0045)
+CODE_MAP_ANDROID(F3, 0x003D)
+
+CODE_MAP_WIN(F4, 0x003E)
+CODE_MAP_MAC(F4, kVK_F4)
+CODE_MAP_X11(F4, 0x0046)
+CODE_MAP_ANDROID(F4, 0x003E)
+
+CODE_MAP_WIN(F5, 0x003F)
+CODE_MAP_MAC(F5, kVK_F5)
+CODE_MAP_X11(F5, 0x0047)
+CODE_MAP_ANDROID(F5, 0x003F)
+
+CODE_MAP_WIN(F6, 0x0040)
+CODE_MAP_MAC(F6, kVK_F6)
+CODE_MAP_X11(F6, 0x0048)
+CODE_MAP_ANDROID(F6, 0x0040)
+
+CODE_MAP_WIN(F7, 0x0041)
+CODE_MAP_MAC(F7, kVK_F7)
+CODE_MAP_X11(F7, 0x0049)
+CODE_MAP_ANDROID(F7, 0x0041)
+
+CODE_MAP_WIN(F8, 0x0042)
+CODE_MAP_MAC(F8, kVK_F8)
+CODE_MAP_X11(F8, 0x004A)
+CODE_MAP_ANDROID(F8, 0x0042)
+
+CODE_MAP_WIN(F9, 0x0043)
+CODE_MAP_MAC(F9, kVK_F9)
+CODE_MAP_X11(F9, 0x004B)
+CODE_MAP_ANDROID(F9, 0x0043)
+
+CODE_MAP_WIN(F10, 0x0044)
+CODE_MAP_MAC(F10, kVK_F10)
+CODE_MAP_X11(F10, 0x004C)
+CODE_MAP_ANDROID(F10, 0x0044)
+
+CODE_MAP_WIN(F11, 0x0057)
+CODE_MAP_MAC(F11, kVK_F11)
+CODE_MAP_X11(F11, 0x005F)
+CODE_MAP_ANDROID(F11, 0x0057)
+
+CODE_MAP_WIN(F12, 0x0058)
+CODE_MAP_MAC(F12, kVK_F12)
+CODE_MAP_X11(F12, 0x0060)
+CODE_MAP_ANDROID(F12, 0x0058)
+
+CODE_MAP_WIN(F13, 0x0064)
+CODE_MAP_MAC(F13, kVK_F13) // PrintScreen on PC keyboard
+CODE_MAP_X11(F13, 0x00BF)
+CODE_MAP_ANDROID(F13, 0x00B7)
+
+CODE_MAP_WIN(F14, 0x0065)
+CODE_MAP_MAC(F14, kVK_F14) // ScrollLock on PC keyboard
+CODE_MAP_X11(F14, 0x00C0)
+CODE_MAP_ANDROID(F14, 0x00B8)
+
+CODE_MAP_WIN(F15, 0x0066)
+CODE_MAP_MAC(F15, kVK_F15) // Pause on PC keyboard
+CODE_MAP_X11(F15, 0x00C1)
+CODE_MAP_ANDROID(F15, 0x00B9)
+
+CODE_MAP_WIN(F16, 0x0067)
+CODE_MAP_MAC(F16, kVK_F16)
+CODE_MAP_X11(F16, 0x00C2)
+CODE_MAP_ANDROID(F16, 0x00BA)
+
+CODE_MAP_WIN(F17, 0x0068)
+CODE_MAP_MAC(F17, kVK_F17)
+CODE_MAP_X11(F17, 0x00C3)
+CODE_MAP_ANDROID(F17, 0x00BB)
+
+CODE_MAP_WIN(F18, 0x0069)
+CODE_MAP_MAC(F18, kVK_F18)
+CODE_MAP_X11(F18, 0x00C4)
+CODE_MAP_ANDROID(F18, 0x00BC)
+
+CODE_MAP_WIN(F19, 0x006A)
+CODE_MAP_MAC(F19, kVK_F19)
+CODE_MAP_X11(F19, 0x00C5)
+CODE_MAP_ANDROID(F19, 0x00BD)
+
+CODE_MAP_WIN(F20, 0x006B)
+CODE_MAP_MAC(F20, kVK_F20)
+CODE_MAP_X11(F20, 0x00C6)
+CODE_MAP_ANDROID(F20, 0x00BE)
+
+CODE_MAP_WIN(F21, 0x006C)
+CODE_MAP_X11(F21, 0x00C7)
+CODE_MAP_ANDROID(F21, 0x00BF)
+
+CODE_MAP_WIN(F22, 0x006D)
+CODE_MAP_X11(F22, 0x00C8)
+CODE_MAP_ANDROID(F22, 0x00C0)
+
+CODE_MAP_WIN(F23, 0x006E)
+CODE_MAP_X11(F23, 0x00C9)
+CODE_MAP_ANDROID(F23, 0x00C1)
+
+CODE_MAP_WIN(F24, 0x0076)
+CODE_MAP_X11(F24, 0x00CA)
+CODE_MAP_ANDROID(F24, 0x00C2)
+
+CODE_MAP_MAC(Fn, kVK_Function) // not available?
+CODE_MAP_ANDROID(Fn, 0x01D0)
+
+CODE_MAP_WIN(PrintScreen, 0xE037)
+CODE_MAP_WIN(PrintScreen, 0x0054) // Alt + PrintScreen
+CODE_MAP_X11(PrintScreen, 0x006B)
+CODE_MAP_ANDROID(PrintScreen, 0x0063)
+
+CODE_MAP_WIN(ScrollLock, 0x0046)
+CODE_MAP_X11(ScrollLock, 0x004E)
+CODE_MAP_ANDROID(ScrollLock, 0x0046)
+
+CODE_MAP_WIN(Pause, 0x0045)
+CODE_MAP_WIN(Pause, 0xE046) // Ctrl + Pause
+CODE_MAP_X11(Pause, 0x007F)
+CODE_MAP_ANDROID(Pause, 0x0077)
+
+// Media keys
+CODE_MAP_WIN(BrowserBack, 0xE06A)
+CODE_MAP_X11(BrowserBack, 0x00A6)
+CODE_MAP_ANDROID(BrowserBack, 0x009E)
+
+CODE_MAP_WIN(BrowserFavorites, 0xE066)
+CODE_MAP_X11(BrowserFavorites, 0x00A4)
+CODE_MAP_ANDROID(BrowserFavorites, 0x009C)
+
+CODE_MAP_WIN(BrowserForward, 0xE069)
+CODE_MAP_X11(BrowserForward, 0x00A7)
+CODE_MAP_ANDROID(BrowserForward, 0x009F)
+
+CODE_MAP_WIN(BrowserHome, 0xE032)
+CODE_MAP_X11(BrowserHome, 0x00B4)
+// CODE_MAP_ANDROID(BrowserHome) // not available? works as Home key.
+
+CODE_MAP_WIN(BrowserRefresh, 0xE067)
+CODE_MAP_X11(BrowserRefresh, 0x00B5)
+CODE_MAP_ANDROID(BrowserRefresh, 0x00AD)
+
+CODE_MAP_WIN(BrowserSearch, 0xE065)
+CODE_MAP_X11(BrowserSearch, 0x00E1)
+CODE_MAP_ANDROID(BrowserSearch, 0x00D9)
+
+CODE_MAP_WIN(BrowserStop, 0xE068)
+CODE_MAP_X11(BrowserStop, 0x0088)
+CODE_MAP_ANDROID(BrowserStop, 0x0080)
+
+// CODE_MAP_WIN(Eject) // not available?
+// CODE_MAP_MAC(Eject) // not available?
+CODE_MAP_X11(Eject, 0x00A9)
+CODE_MAP_ANDROID(Eject, 0x00A1)
+
+CODE_MAP_WIN(LaunchApp1, 0xE06B)
+CODE_MAP_X11(LaunchApp1, 0x0098)
+CODE_MAP_ANDROID(LaunchApp1, 0x0090)
+
+CODE_MAP_WIN(LaunchApp2, 0xE021)
+CODE_MAP_X11(LaunchApp2, 0x0094)
+// CODE_MAP_ANDROID(LaunchApp2) // not available?
+
+CODE_MAP_WIN(LaunchMail, 0xE06C)
+CODE_MAP_X11(LaunchMail, 0x00A3)
+// CODE_MAP_ANDROID(LaunchMail) // not available?
+
+CODE_MAP_WIN(MediaPlayPause, 0xE022)
+CODE_MAP_X11(MediaPlayPause, 0x00AC)
+CODE_MAP_ANDROID(MediaPlayPause, 0x00A4)
+
+CODE_MAP_WIN(MediaSelect, 0xE06D)
+CODE_MAP_X11(MediaSelect, 0x00B3)
+// CODE_MAP_ANDROID(MediaSelect) // not available?
+
+CODE_MAP_WIN(MediaStop, 0xE024)
+CODE_MAP_X11(MediaStop, 0x00AE)
+CODE_MAP_ANDROID(MediaStop, 0x00A6)
+
+CODE_MAP_WIN(MediaTrackNext, 0xE019)
+CODE_MAP_X11(MediaTrackNext, 0x00AB)
+CODE_MAP_ANDROID(MediaTrackNext, 0x00A3)
+
+CODE_MAP_WIN(MediaTrackPrevious, 0xE010)
+CODE_MAP_X11(MediaTrackPrevious, 0x00AD)
+CODE_MAP_ANDROID(MediaTrackPrevious, 0x00A5)
+
+CODE_MAP_WIN(Power, 0xE05E)
+CODE_MAP_MAC(Power, 0x007F) // On 10.7 and 10.8 only
+// CODE_MAP_X11(Power) // not available?
+CODE_MAP_ANDROID(Power, 0x0074)
+
+// CODE_MAP_WIN(Sleep) // not available?
+// CODE_MAP_X11(Sleep) // not available?
+CODE_MAP_ANDROID(Sleep, 0x008E)
+
+CODE_MAP_WIN(VolumeDown, 0xE02E)
+CODE_MAP_MAC(VolumeDown, kVK_VolumeDown) // not available?
+CODE_MAP_X11(VolumeDown, 0x007A)
+CODE_MAP_ANDROID(VolumeDown, 0x0072)
+
+CODE_MAP_WIN(VolumeMute, 0xE020)
+CODE_MAP_MAC(VolumeMute, kVK_Mute) // not available?
+CODE_MAP_X11(VolumeMute, 0x0079)
+CODE_MAP_ANDROID(VolumeMute, 0x0071)
+
+CODE_MAP_WIN(VolumeUp, 0xE030)
+CODE_MAP_MAC(VolumeUp, kVK_VolumeUp) // not available?
+CODE_MAP_X11(VolumeUp, 0x007B)
+CODE_MAP_ANDROID(VolumeUp, 0x0073) // side of body, not on keyboard
+
+// CODE_MAP_WIN(WakeUp) // not available?
+CODE_MAP_X11(WakeUp, 0x0097)
+CODE_MAP_ANDROID(WakeUp, 0x008F)
+
+// Legacy editing keys
+CODE_MAP_X11(Again, 0x0089) // Again key on Sun keyboard
+CODE_MAP_ANDROID(Again, 0x0081) // Again key on Sun keyboard
+
+CODE_MAP_X11(Copy, 0x008D) // Copy key on Sun keyboard
+CODE_MAP_ANDROID(Copy, 0x0085) // Copy key on Sun keyboard
+
+CODE_MAP_X11(Cut, 0x0091) // Cut key on Sun keyboard
+CODE_MAP_ANDROID(Cut, 0x0089) // Cut key on Sun keyboard
+
+CODE_MAP_X11(Find, 0x0090) // Find key on Sun keyboard
+CODE_MAP_ANDROID(Find, 0x0088) // Find key on Sun keyboard
+
+CODE_MAP_X11(Open, 0x008E) // Open key on Sun keyboard
+CODE_MAP_ANDROID(Open, 0x0086) // Open key on Sun keyboard
+
+CODE_MAP_X11(Paste, 0x008F) // Paste key on Sun keyboard
+CODE_MAP_ANDROID(Paste, 0x0087) // Paste key on Sun keyboard
+
+CODE_MAP_X11(Props, 0x008A) // Props key on Sun keyboard
+CODE_MAP_ANDROID(Props, 0x0082) // Props key on Sun keyboard
+
+CODE_MAP_X11(Select, 0x008C) // Front key on Sun keyboard
+CODE_MAP_ANDROID(Select, 0x0084) // Front key on Sun keyboard
+
+CODE_MAP_X11(Undo, 0x008B) // Undo key on Sun keyboard
+CODE_MAP_ANDROID(Undo, 0x0083) // Undo key on Sun keyboard
+
+#undef CODE_MAP_WIN
+#undef CODE_MAP_MAC
+#undef CODE_MAP_X11
+#undef CODE_MAP_ANDROID
diff --git a/widget/NativeKeyToDOMKeyName.h b/widget/NativeKeyToDOMKeyName.h
new file mode 100644
index 0000000000..af079bef5b
--- /dev/null
+++ b/widget/NativeKeyToDOMKeyName.h
@@ -0,0 +1,1286 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/**
+ * This header file defines simple key mapping between native keycode value and
+ * DOM key name index.
+ * You must define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX macro before include
+ * this.
+ *
+ * It must have two arguments, (aNativeKey, aKeyNameIndex).
+ * aNativeKey is a native keycode value.
+ * aKeyNameIndex is the widget::KeyNameIndex value.
+ */
+
+// Windows
+#define KEY_MAP_WIN(aCPPKeyName, aNativeKey)
+#define KEY_MAP_WIN_JPN(aCPPKeyName, aNativeKey)
+#define KEY_MAP_WIN_KOR(aCPPKeyName, aNativeKey)
+#define KEY_MAP_WIN_OTH(aCPPKeyName, aNativeKey)
+#define KEY_MAP_WIN_CMD(aCPPKeyName, aAppCommand)
+// Mac OS X
+#define KEY_MAP_COCOA(aCPPKeyName, aNativeKey)
+// GTK
+#define KEY_MAP_GTK(aCPPKeyName, aNativeKey)
+// Only for Android
+#define KEY_MAP_ANDROID(aCPPKeyName, aNativeKey)
+
+#if defined(XP_WIN)
+# if defined(NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX)
+// KEY_MAP_WIN() defines the mapping not depending on keyboard layout.
+# undef KEY_MAP_WIN
+# define KEY_MAP_WIN(aCPPKeyName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \
+ KEY_NAME_INDEX_##aCPPKeyName)
+# elif defined(NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX)
+// KEY_MAP_WIN_JPN() defines the mapping which is valid only with Japanese
+// keyboard layout.
+# undef KEY_MAP_WIN_JPN
+# define KEY_MAP_WIN_JPN(aCPPKeyName, aNativeKey) \
+ NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX( \
+ aNativeKey, KEY_NAME_INDEX_##aCPPKeyName)
+# elif defined(NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX)
+// KEY_MAP_WIN_KOR() defines the mapping which is valid only with Korean
+// keyboard layout.
+# undef KEY_MAP_WIN_KOR
+# define KEY_MAP_WIN_KOR(aCPPKeyName, aNativeKey) \
+ NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \
+ KEY_NAME_INDEX_##aCPPKeyName)
+# elif defined(NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX)
+// KEY_MAP_WIN_OTH() defines the mapping which is valid with neither
+// Japanese keyboard layout nor Korean keyboard layout.
+# undef KEY_MAP_WIN_OTH
+# define KEY_MAP_WIN_OTH(aCPPKeyName, aNativeKey) \
+ NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \
+ KEY_NAME_INDEX_##aCPPKeyName)
+# elif defined(NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX)
+// KEY_MAP_WIN_CMD() defines the mapping from APPCOMMAND_* of WM_APPCOMMAND.
+# undef KEY_MAP_WIN_CMD
+# define KEY_MAP_WIN_CMD(aCPPKeyName, aAppCommand) \
+ NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX(aAppCommand, \
+ KEY_NAME_INDEX_##aCPPKeyName)
+# else
+# error Any NS_*_TO_DOM_KEY_NAME_INDEX() is not defined.
+# endif // #if defined(NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX) ...
+#elif defined(XP_MACOSX)
+# undef KEY_MAP_COCOA
+# define KEY_MAP_COCOA(aCPPKeyName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \
+ KEY_NAME_INDEX_##aCPPKeyName)
+#elif defined(MOZ_WIDGET_GTK)
+# undef KEY_MAP_GTK
+# define KEY_MAP_GTK(aCPPKeyName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \
+ KEY_NAME_INDEX_##aCPPKeyName)
+#elif defined(ANDROID)
+# undef KEY_MAP_ANDROID
+# define KEY_MAP_ANDROID(aCPPKeyName, aNativeKey) \
+ NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \
+ KEY_NAME_INDEX_##aCPPKeyName)
+#endif
+
+/******************************************************************************
+ * Modifier Keys
+ ******************************************************************************/
+// Alt
+KEY_MAP_WIN(Alt, VK_MENU)
+KEY_MAP_WIN(Alt, VK_LMENU)
+KEY_MAP_WIN(Alt, VK_RMENU) // This is ignored if active keyboard layout
+ // has AltGr. In such case, AltGraph is mapped.
+KEY_MAP_COCOA(Alt, kVK_Option)
+KEY_MAP_COCOA(Alt, kVK_RightOption)
+KEY_MAP_GTK(Alt, GDK_Alt_L)
+KEY_MAP_GTK(Alt, GDK_Alt_R)
+KEY_MAP_GTK(Alt, GDK_Meta_L)
+KEY_MAP_GTK(Alt, GDK_Meta_R)
+KEY_MAP_ANDROID(Alt, AKEYCODE_ALT_LEFT)
+KEY_MAP_ANDROID(Alt, AKEYCODE_ALT_RIGHT)
+
+// AltGraph
+KEY_MAP_GTK (AltGraph, GDK_Mode_switch /* same as GDK_kana_switch,
+ GDK_ISO_Group_Shift and
+ GDK_script_switch */)
+// Let's treat both Level 3 shift and Level 5 shift as AltGr.
+// And also, let's treat Latch key and Lock key as AltGr key too like
+// GDK_Shift_Lock.
+KEY_MAP_GTK(AltGraph, GDK_ISO_Level3_Shift)
+KEY_MAP_GTK(AltGraph, GDK_ISO_Level3_Latch)
+KEY_MAP_GTK(AltGraph, GDK_ISO_Level3_Lock)
+KEY_MAP_GTK(AltGraph, GDK_ISO_Level5_Shift)
+KEY_MAP_GTK(AltGraph, GDK_ISO_Level5_Latch)
+KEY_MAP_GTK(AltGraph, GDK_ISO_Level5_Lock)
+
+// CapsLock
+KEY_MAP_WIN(CapsLock, VK_CAPITAL)
+KEY_MAP_COCOA(CapsLock, kVK_CapsLock)
+KEY_MAP_GTK(CapsLock, GDK_Caps_Lock)
+KEY_MAP_ANDROID(CapsLock, AKEYCODE_CAPS_LOCK)
+
+// Control
+KEY_MAP_WIN(Control, VK_CONTROL)
+KEY_MAP_WIN(Control, VK_LCONTROL)
+KEY_MAP_WIN(Control, VK_RCONTROL)
+KEY_MAP_COCOA(Control, kVK_Control)
+KEY_MAP_COCOA(Control, kVK_RightControl)
+KEY_MAP_GTK(Control, GDK_Control_L)
+KEY_MAP_GTK(Control, GDK_Control_R)
+KEY_MAP_ANDROID(Control, AKEYCODE_CTRL_LEFT)
+KEY_MAP_ANDROID(Control, AKEYCODE_CTRL_RIGHT)
+
+// Fn
+KEY_MAP_COCOA(Fn, kVK_Function)
+KEY_MAP_ANDROID(Fn, AKEYCODE_FUNCTION)
+
+// Meta
+KEY_MAP_WIN(Meta, VK_LWIN)
+KEY_MAP_WIN(Meta, VK_RWIN)
+KEY_MAP_COCOA(Meta, kVK_Command)
+KEY_MAP_COCOA(Meta, kVK_RightCommand)
+KEY_MAP_GTK(Meta, GDK_Super_L)
+KEY_MAP_GTK(Meta, GDK_Super_R)
+KEY_MAP_GTK(Meta, GDK_Hyper_L)
+KEY_MAP_GTK(Meta, GDK_Hyper_R)
+KEY_MAP_ANDROID(Meta, AKEYCODE_META_LEFT)
+KEY_MAP_ANDROID(Meta, AKEYCODE_META_RIGHT)
+
+// NumLock
+KEY_MAP_WIN(NumLock, VK_NUMLOCK)
+KEY_MAP_GTK(NumLock, GDK_Num_Lock)
+KEY_MAP_ANDROID(NumLock, AKEYCODE_NUM_LOCK)
+
+// ScrollLock
+KEY_MAP_WIN(ScrollLock, VK_SCROLL)
+KEY_MAP_GTK(ScrollLock, GDK_Scroll_Lock)
+KEY_MAP_ANDROID(ScrollLock, AKEYCODE_SCROLL_LOCK)
+
+// Shift
+KEY_MAP_WIN(Shift, VK_SHIFT)
+KEY_MAP_WIN(Shift, VK_LSHIFT)
+KEY_MAP_WIN(Shift, VK_RSHIFT)
+KEY_MAP_COCOA(Shift, kVK_Shift)
+KEY_MAP_COCOA(Shift, kVK_RightShift)
+KEY_MAP_GTK(Shift, GDK_Shift_L)
+KEY_MAP_GTK(Shift, GDK_Shift_R)
+KEY_MAP_GTK(Shift, GDK_Shift_Lock) // Let's treat as Shift key (bug 769159)
+KEY_MAP_ANDROID(Shift, AKEYCODE_SHIFT_LEFT)
+KEY_MAP_ANDROID(Shift, AKEYCODE_SHIFT_RIGHT)
+
+// Symbol
+KEY_MAP_ANDROID(Symbol, AKEYCODE_SYM)
+
+/******************************************************************************
+ * Whitespace Keys
+ ******************************************************************************/
+// Enter
+KEY_MAP_WIN(Enter, VK_RETURN)
+KEY_MAP_COCOA(Enter, kVK_Return)
+KEY_MAP_COCOA(Enter, kVK_ANSI_KeypadEnter)
+KEY_MAP_COCOA(Enter, kVK_Powerbook_KeypadEnter)
+KEY_MAP_GTK(Enter, GDK_Return)
+KEY_MAP_GTK(Enter, GDK_KP_Enter)
+KEY_MAP_GTK(Enter, GDK_ISO_Enter)
+KEY_MAP_GTK(Enter, GDK_3270_Enter)
+KEY_MAP_ANDROID(Enter, AKEYCODE_DPAD_CENTER)
+KEY_MAP_ANDROID(Enter, AKEYCODE_ENTER)
+KEY_MAP_ANDROID(Enter, AKEYCODE_NUMPAD_ENTER)
+
+// Tab
+KEY_MAP_WIN(Tab, VK_TAB)
+KEY_MAP_COCOA(Tab, kVK_Tab)
+KEY_MAP_GTK(Tab, GDK_Tab)
+KEY_MAP_GTK(Tab, GDK_ISO_Left_Tab) // Shift+Tab
+KEY_MAP_GTK(Tab, GDK_KP_Tab)
+KEY_MAP_ANDROID(Tab, AKEYCODE_TAB)
+
+/******************************************************************************
+ * Navigation Keys
+ ******************************************************************************/
+// ArrowDown
+KEY_MAP_WIN(ArrowDown, VK_DOWN)
+KEY_MAP_COCOA(ArrowDown, kVK_DownArrow)
+KEY_MAP_GTK(ArrowDown, GDK_Down)
+KEY_MAP_GTK(ArrowDown, GDK_KP_Down)
+KEY_MAP_ANDROID(ArrowDown, AKEYCODE_DPAD_DOWN)
+
+// ArrowLeft
+KEY_MAP_WIN(ArrowLeft, VK_LEFT)
+KEY_MAP_COCOA(ArrowLeft, kVK_LeftArrow)
+KEY_MAP_GTK(ArrowLeft, GDK_Left)
+KEY_MAP_GTK(ArrowLeft, GDK_KP_Left)
+KEY_MAP_ANDROID(ArrowLeft, AKEYCODE_DPAD_LEFT)
+
+// ArrowRight
+KEY_MAP_WIN(ArrowRight, VK_RIGHT)
+KEY_MAP_COCOA(ArrowRight, kVK_RightArrow)
+KEY_MAP_GTK(ArrowRight, GDK_Right)
+KEY_MAP_GTK(ArrowRight, GDK_KP_Right)
+KEY_MAP_ANDROID(ArrowRight, AKEYCODE_DPAD_RIGHT)
+
+// ArrowUp
+KEY_MAP_WIN(ArrowUp, VK_UP)
+KEY_MAP_COCOA(ArrowUp, kVK_UpArrow)
+KEY_MAP_GTK(ArrowUp, GDK_Up)
+KEY_MAP_GTK(ArrowUp, GDK_KP_Up)
+KEY_MAP_ANDROID(ArrowUp, AKEYCODE_DPAD_UP)
+
+// End
+KEY_MAP_WIN(End, VK_END)
+KEY_MAP_COCOA(End, kVK_End)
+KEY_MAP_GTK(End, GDK_End)
+KEY_MAP_GTK(End, GDK_KP_End)
+KEY_MAP_ANDROID(End, AKEYCODE_MOVE_END)
+
+// Home
+KEY_MAP_WIN(Home, VK_HOME)
+KEY_MAP_COCOA(Home, kVK_Home)
+KEY_MAP_GTK(Home, GDK_Home)
+KEY_MAP_GTK(Home, GDK_KP_Home)
+KEY_MAP_ANDROID(Home, AKEYCODE_MOVE_HOME)
+
+// PageDown
+KEY_MAP_WIN(PageDown, VK_NEXT)
+KEY_MAP_COCOA(PageDown, kVK_PageDown)
+KEY_MAP_GTK(PageDown, GDK_Page_Down /* same as GDK_Next */)
+KEY_MAP_GTK(PageDown, GDK_KP_Page_Down /* same as GDK_KP_Next */)
+KEY_MAP_ANDROID(PageDown, AKEYCODE_PAGE_DOWN)
+
+// PageUp
+KEY_MAP_WIN(PageUp, VK_PRIOR)
+KEY_MAP_COCOA(PageUp, kVK_PageUp)
+KEY_MAP_GTK(PageUp, GDK_Page_Up /* same as GDK_Prior */)
+KEY_MAP_GTK(PageUp, GDK_KP_Page_Up /* same as GDK_KP_Prior */)
+KEY_MAP_ANDROID(PageUp, AKEYCODE_PAGE_UP)
+
+/******************************************************************************
+ * Editing Keys
+ ******************************************************************************/
+// Backspace
+KEY_MAP_WIN(Backspace, VK_BACK)
+KEY_MAP_COCOA(Backspace, kVK_PC_Backspace)
+KEY_MAP_GTK(Backspace, GDK_BackSpace)
+KEY_MAP_ANDROID(Backspace, AKEYCODE_DEL)
+
+// Clear
+KEY_MAP_WIN(Clear, VK_CLEAR)
+KEY_MAP_WIN(Clear, VK_OEM_CLEAR)
+KEY_MAP_COCOA(Clear, kVK_ANSI_KeypadClear)
+KEY_MAP_GTK(Clear, GDK_Clear)
+KEY_MAP_ANDROID(Clear, AKEYCODE_CLEAR)
+
+// Copy
+KEY_MAP_WIN_CMD(Copy, APPCOMMAND_COPY)
+KEY_MAP_GTK(Copy, GDK_Copy)
+KEY_MAP_ANDROID(Copy, AKEYCODE_COPY)
+
+// CrSel
+KEY_MAP_WIN(CrSel, VK_CRSEL)
+KEY_MAP_GTK(CrSel, GDK_3270_CursorSelect) // legacy IBM keyboard layout
+
+// Cut
+KEY_MAP_WIN_CMD(Cut, APPCOMMAND_CUT)
+KEY_MAP_GTK(Cut, GDK_Cut)
+KEY_MAP_ANDROID(Cut, AKEYCODE_CUT)
+
+// Delete
+KEY_MAP_WIN(Delete, VK_DELETE)
+KEY_MAP_COCOA(Delete, kVK_PC_Delete)
+KEY_MAP_GTK(Delete, GDK_Delete)
+KEY_MAP_GTK(Delete, GDK_KP_Delete)
+KEY_MAP_ANDROID(Delete, AKEYCODE_FORWARD_DEL)
+
+// EraseEof
+KEY_MAP_WIN(EraseEof, VK_EREOF)
+KEY_MAP_GTK(EraseEof, GDK_3270_EraseEOF) // legacy IBM keyboard layout
+
+// ExSel
+KEY_MAP_WIN(ExSel, VK_EXSEL)
+KEY_MAP_GTK(ExSel, GDK_3270_ExSelect) // legacy IBM keyboard layout
+
+// Insert
+KEY_MAP_WIN(Insert, VK_INSERT)
+KEY_MAP_GTK(Insert, GDK_Insert)
+KEY_MAP_GTK(Insert, GDK_KP_Insert)
+KEY_MAP_ANDROID(Insert, AKEYCODE_INSERT)
+
+// Paste
+KEY_MAP_WIN_CMD(Paste, APPCOMMAND_PASTE)
+KEY_MAP_GTK(Paste, GDK_Paste)
+KEY_MAP_ANDROID(Paste, AKEYCODE_PASTE)
+
+// Redo
+KEY_MAP_WIN_CMD(Redo, APPCOMMAND_REDO)
+KEY_MAP_GTK(Redo, GDK_Redo)
+
+// Undo
+KEY_MAP_WIN_CMD(Undo, APPCOMMAND_UNDO)
+KEY_MAP_GTK(Undo, GDK_Undo)
+
+/******************************************************************************
+ * UI Keys
+ ******************************************************************************/
+// Accept
+KEY_MAP_WIN(Accept, VK_ACCEPT)
+
+// Attn
+KEY_MAP_WIN_OTH(Attn, VK_ATTN) // not valid with Japanese keyboard layout
+KEY_MAP_GTK(Attn, GDK_3270_Attn) // legacy IBM keyboard layout
+
+// Cancel
+KEY_MAP_WIN(Cancel, VK_CANCEL)
+KEY_MAP_GTK(Cancel, GDK_Cancel)
+
+// ContextMenu
+KEY_MAP_WIN(ContextMenu, VK_APPS)
+KEY_MAP_COCOA(ContextMenu, kVK_PC_ContextMenu)
+KEY_MAP_GTK(ContextMenu, GDK_Menu)
+KEY_MAP_ANDROID(ContextMenu, AKEYCODE_MENU)
+
+// Escape
+KEY_MAP_WIN(Escape, VK_ESCAPE)
+KEY_MAP_COCOA(Escape, kVK_Escape)
+KEY_MAP_GTK(Escape, GDK_Escape)
+KEY_MAP_ANDROID(Escape, AKEYCODE_ESCAPE)
+
+// Execute
+KEY_MAP_WIN(Execute, VK_EXECUTE)
+KEY_MAP_GTK(Execute, GDK_Execute)
+
+// Find
+KEY_MAP_WIN_CMD(Find, APPCOMMAND_FIND)
+KEY_MAP_GTK(Find, GDK_Find)
+
+// Help
+KEY_MAP_WIN(Help, VK_HELP)
+KEY_MAP_WIN_CMD(Help, APPCOMMAND_HELP)
+KEY_MAP_COCOA(Help, kVK_Help)
+KEY_MAP_GTK(Help, GDK_Help)
+KEY_MAP_ANDROID(Help, AKEYCODE_HELP)
+
+// Pause
+KEY_MAP_WIN(Pause, VK_PAUSE)
+KEY_MAP_GTK(Pause, GDK_Pause)
+// Break is typically mapped to Alt+Pause or Ctrl+Pause on GTK.
+KEY_MAP_GTK(Pause, GDK_Break)
+KEY_MAP_ANDROID(Pause, AKEYCODE_BREAK)
+
+// Play
+KEY_MAP_WIN(Play, VK_PLAY)
+KEY_MAP_GTK(Play, GDK_3270_Play) // legacy IBM keyboard layout
+
+// Select
+KEY_MAP_WIN(Select, VK_SELECT)
+KEY_MAP_GTK(Select, GDK_Select)
+
+// ZoomIn
+KEY_MAP_GTK(ZoomIn, GDK_ZoomIn)
+KEY_MAP_ANDROID(ZoomIn, AKEYCODE_ZOOM_IN)
+
+// ZoomOut
+KEY_MAP_GTK(ZoomOut, GDK_ZoomOut)
+KEY_MAP_ANDROID(ZoomOut, AKEYCODE_ZOOM_OUT)
+
+/******************************************************************************
+ * Device Keys
+ ******************************************************************************/
+// BrightnessDown
+KEY_MAP_GTK(BrightnessDown, GDK_MonBrightnessDown)
+KEY_MAP_ANDROID(BrightnessDown, AKEYCODE_BRIGHTNESS_DOWN)
+
+// BrightnessUp
+KEY_MAP_GTK(BrightnessUp, GDK_MonBrightnessUp)
+KEY_MAP_ANDROID(BrightnessUp, AKEYCODE_BRIGHTNESS_UP)
+
+// Eject
+KEY_MAP_GTK(Eject, GDK_Eject)
+KEY_MAP_ANDROID(Eject, AKEYCODE_MEDIA_EJECT)
+
+// LogOff
+KEY_MAP_GTK(LogOff, GDK_LogOff)
+
+// Power
+KEY_MAP_ANDROID(Power, AKEYCODE_POWER)
+
+// PowerOff
+KEY_MAP_GTK(PowerOff, GDK_PowerDown)
+KEY_MAP_GTK(PowerOff, GDK_PowerOff)
+
+// PrintScreen
+KEY_MAP_WIN(PrintScreen, VK_SNAPSHOT)
+KEY_MAP_GTK(PrintScreen, GDK_3270_PrintScreen)
+KEY_MAP_GTK(PrintScreen, GDK_Print)
+KEY_MAP_GTK(PrintScreen, GDK_Sys_Req)
+KEY_MAP_ANDROID(PrintScreen, AKEYCODE_SYSRQ)
+
+// Hibernate
+KEY_MAP_GTK(Hibernate, GDK_Hibernate)
+
+// Standby
+KEY_MAP_WIN(Standby, VK_SLEEP)
+KEY_MAP_GTK(Standby, GDK_Standby)
+KEY_MAP_GTK(Standby, GDK_Suspend)
+KEY_MAP_GTK(Standby, GDK_Sleep)
+KEY_MAP_ANDROID(Standby, AKEYCODE_SLEEP)
+
+// WakeUp
+KEY_MAP_GTK(WakeUp, GDK_WakeUp)
+KEY_MAP_ANDROID(WakeUp, AKEYCODE_WAKEUP)
+
+/******************************************************************************
+ * IME and Composition Keys
+ ******************************************************************************/
+// AllCandidates
+KEY_MAP_GTK(AllCandidates, GDK_MultipleCandidate) // OADG 109, Zen Koho
+
+// Alphanumeric
+KEY_MAP_WIN_JPN(Alphanumeric, VK_OEM_ATTN)
+KEY_MAP_GTK(Alphanumeric, GDK_Eisu_Shift)
+KEY_MAP_GTK(Alphanumeric, GDK_Eisu_toggle)
+
+// CodeInput
+KEY_MAP_GTK(CodeInput, GDK_Codeinput) // OADG 109, Kanji Bangou
+
+// Compose
+KEY_MAP_GTK(Compose, GDK_Multi_key) // "Multi Key" is "Compose key" on X
+
+// Convert
+KEY_MAP_WIN(Convert, VK_CONVERT)
+KEY_MAP_GTK(Convert, GDK_Henkan)
+KEY_MAP_ANDROID(Convert, AKEYCODE_HENKAN)
+
+// Dead
+KEY_MAP_GTK(Dead, GDK_dead_grave)
+KEY_MAP_GTK(Dead, GDK_dead_acute)
+KEY_MAP_GTK(Dead, GDK_dead_circumflex)
+KEY_MAP_GTK(Dead, GDK_dead_tilde) // Same as GDK_dead_perispomeni
+KEY_MAP_GTK(Dead, GDK_dead_macron)
+KEY_MAP_GTK(Dead, GDK_dead_breve)
+KEY_MAP_GTK(Dead, GDK_dead_abovedot)
+KEY_MAP_GTK(Dead, GDK_dead_diaeresis)
+KEY_MAP_GTK(Dead, GDK_dead_abovering)
+KEY_MAP_GTK(Dead, GDK_dead_doubleacute)
+KEY_MAP_GTK(Dead, GDK_dead_caron)
+KEY_MAP_GTK(Dead, GDK_dead_cedilla)
+KEY_MAP_GTK(Dead, GDK_dead_ogonek)
+KEY_MAP_GTK(Dead, GDK_dead_iota)
+KEY_MAP_GTK(Dead, GDK_dead_voiced_sound)
+KEY_MAP_GTK(Dead, GDK_dead_semivoiced_sound)
+KEY_MAP_GTK(Dead, GDK_dead_belowdot)
+KEY_MAP_GTK(Dead, GDK_dead_hook)
+KEY_MAP_GTK(Dead, GDK_dead_horn)
+KEY_MAP_GTK(Dead, GDK_dead_stroke)
+KEY_MAP_GTK(Dead, GDK_dead_abovecomma) // Same as GDK_dead_psili
+KEY_MAP_GTK(Dead, GDK_dead_abovereversedcomma) // Same as GDK_dead_dasia
+KEY_MAP_GTK(Dead, GDK_dead_doublegrave)
+KEY_MAP_GTK(Dead, GDK_dead_belowring)
+KEY_MAP_GTK(Dead, GDK_dead_belowmacron)
+KEY_MAP_GTK(Dead, GDK_dead_belowcircumflex)
+KEY_MAP_GTK(Dead, GDK_dead_belowtilde)
+KEY_MAP_GTK(Dead, GDK_dead_belowbreve)
+KEY_MAP_GTK(Dead, GDK_dead_belowdiaeresis)
+KEY_MAP_GTK(Dead, GDK_dead_invertedbreve)
+KEY_MAP_GTK(Dead, GDK_dead_belowcomma)
+KEY_MAP_GTK(Dead, GDK_dead_currency)
+KEY_MAP_GTK(Dead, GDK_dead_a)
+KEY_MAP_GTK(Dead, GDK_dead_A)
+KEY_MAP_GTK(Dead, GDK_dead_e)
+KEY_MAP_GTK(Dead, GDK_dead_E)
+KEY_MAP_GTK(Dead, GDK_dead_i)
+KEY_MAP_GTK(Dead, GDK_dead_I)
+KEY_MAP_GTK(Dead, GDK_dead_o)
+KEY_MAP_GTK(Dead, GDK_dead_O)
+KEY_MAP_GTK(Dead, GDK_dead_u)
+KEY_MAP_GTK(Dead, GDK_dead_U)
+KEY_MAP_GTK(Dead, GDK_dead_small_schwa)
+KEY_MAP_GTK(Dead, GDK_dead_capital_schwa)
+KEY_MAP_GTK(Dead, GDK_dead_greek)
+
+// FinalMode
+KEY_MAP_WIN(FinalMode, VK_FINAL)
+
+// GroupFirst
+KEY_MAP_GTK(GroupFirst, GDK_ISO_First_Group)
+
+// GroupLast
+KEY_MAP_GTK(GroupLast, GDK_ISO_Last_Group)
+
+// GroupNext
+KEY_MAP_GTK(GroupNext, GDK_ISO_Next_Group)
+KEY_MAP_ANDROID(GroupNext, AKEYCODE_LANGUAGE_SWITCH)
+
+// GroupPrevious
+KEY_MAP_GTK(GroupPrevious, GDK_ISO_Prev_Group)
+
+// ModeChange
+KEY_MAP_WIN(ModeChange, VK_MODECHANGE)
+KEY_MAP_ANDROID(ModeChange, AKEYCODE_SWITCH_CHARSET)
+
+// NonConvert
+KEY_MAP_WIN(NonConvert, VK_NONCONVERT)
+KEY_MAP_GTK(NonConvert, GDK_Muhenkan)
+KEY_MAP_ANDROID(NonConvert, AKEYCODE_MUHENKAN)
+
+// PreviousCandidate
+KEY_MAP_GTK(PreviousCandidate, GDK_PreviousCandidate) // OADG 109, Mae Koho
+
+// Process
+KEY_MAP_WIN(Process, VK_PROCESSKEY)
+
+// SingleCandidate
+KEY_MAP_GTK(SingleCandidate, GDK_SingleCandidate)
+
+/******************************************************************************
+ * Keys specific to Korean keyboards
+ ******************************************************************************/
+// HangulMode
+KEY_MAP_WIN_KOR(HangulMode, VK_HANGUL /* same as VK_KANA */)
+
+// HanjaMode
+KEY_MAP_WIN_KOR(HanjaMode, VK_HANJA /* same as VK_KANJI */)
+
+// JunjaMode
+KEY_MAP_WIN(JunjaMode, VK_JUNJA)
+
+/******************************************************************************
+ * Keys specific to Japanese keyboards
+ ******************************************************************************/
+// Eisu
+KEY_MAP_COCOA(Eisu, kVK_JIS_Eisu)
+KEY_MAP_ANDROID(Eisu, AKEYCODE_EISU)
+
+// Hankaku
+KEY_MAP_WIN_JPN(Hankaku, VK_OEM_AUTO)
+KEY_MAP_GTK(Hankaku, GDK_Hankaku)
+
+// Hiragana
+KEY_MAP_WIN_JPN(Hiragana, VK_OEM_COPY)
+KEY_MAP_GTK(Hiragana, GDK_Hiragana)
+
+// HiraganaKatakana
+KEY_MAP_GTK(HiraganaKatakana, GDK_Hiragana_Katakana)
+KEY_MAP_ANDROID(HiraganaKatakana, AKEYCODE_KATAKANA_HIRAGANA)
+
+// KanaMode
+// VK_KANA is never used with modern Japanese keyboard, however, IE maps it to
+// KanaMode, therefore, we should use same map for it.
+KEY_MAP_WIN_JPN(KanaMode, VK_KANA /* same as VK_HANGUL */)
+KEY_MAP_WIN_JPN(KanaMode, VK_ATTN)
+KEY_MAP_GTK(KanaMode, GDK_Kana_Lock)
+KEY_MAP_GTK(KanaMode, GDK_Kana_Shift)
+
+// KanjiMode
+KEY_MAP_WIN_JPN(KanjiMode, VK_KANJI /* same as VK_HANJA */)
+KEY_MAP_COCOA(KanjiMode, kVK_JIS_Kana) // Kana key opens IME
+KEY_MAP_GTK(KanjiMode, GDK_Kanji) // Typically, Alt + Hankaku/Zenkaku key
+// Assuming that KANA key of Android is the Kana key on Mac keyboard.
+KEY_MAP_ANDROID(KanjiMode, AKEYCODE_KANA)
+
+// Katakana
+KEY_MAP_WIN_JPN(Katakana, VK_OEM_FINISH)
+KEY_MAP_GTK(Katakana, GDK_Katakana)
+
+// Romaji
+KEY_MAP_WIN_JPN(Romaji, VK_OEM_BACKTAB)
+KEY_MAP_GTK(Romaji, GDK_Romaji)
+
+// Zenkaku
+KEY_MAP_WIN_JPN(Zenkaku, VK_OEM_ENLW)
+KEY_MAP_GTK(Zenkaku, GDK_Zenkaku)
+
+// ZenkakuHankaku
+KEY_MAP_GTK(ZenkakuHankaku, GDK_Zenkaku_Hankaku)
+KEY_MAP_ANDROID(ZenkakuHankaku, AKEYCODE_ZENKAKU_HANKAKU)
+
+/******************************************************************************
+ * General-Purpose Function Keys
+ ******************************************************************************/
+// F1
+KEY_MAP_WIN(F1, VK_F1)
+KEY_MAP_COCOA(F1, kVK_F1)
+KEY_MAP_GTK(F1, GDK_F1)
+KEY_MAP_GTK(F1, GDK_KP_F1)
+KEY_MAP_ANDROID(F1, AKEYCODE_F1)
+
+// F2
+KEY_MAP_WIN(F2, VK_F2)
+KEY_MAP_COCOA(F2, kVK_F2)
+KEY_MAP_GTK(F2, GDK_F2)
+KEY_MAP_GTK(F2, GDK_KP_F2)
+KEY_MAP_ANDROID(F2, AKEYCODE_F2)
+
+// F3
+KEY_MAP_WIN(F3, VK_F3)
+KEY_MAP_COCOA(F3, kVK_F3)
+KEY_MAP_GTK(F3, GDK_F3)
+KEY_MAP_GTK(F3, GDK_KP_F3)
+KEY_MAP_ANDROID(F3, AKEYCODE_F3)
+
+// F4
+KEY_MAP_WIN(F4, VK_F4)
+KEY_MAP_COCOA(F4, kVK_F4)
+KEY_MAP_GTK(F4, GDK_F4)
+KEY_MAP_GTK(F4, GDK_KP_F4)
+KEY_MAP_ANDROID(F4, AKEYCODE_F4)
+
+// F5
+KEY_MAP_WIN(F5, VK_F5)
+KEY_MAP_COCOA(F5, kVK_F5)
+KEY_MAP_GTK(F5, GDK_F5)
+KEY_MAP_ANDROID(F5, AKEYCODE_F5)
+
+// F6
+KEY_MAP_WIN(F6, VK_F6)
+KEY_MAP_COCOA(F6, kVK_F6)
+KEY_MAP_GTK(F6, GDK_F6)
+KEY_MAP_ANDROID(F6, AKEYCODE_F6)
+
+// F7
+KEY_MAP_WIN(F7, VK_F7)
+KEY_MAP_COCOA(F7, kVK_F7)
+KEY_MAP_GTK(F7, GDK_F7)
+KEY_MAP_ANDROID(F7, AKEYCODE_F7)
+
+// F8
+KEY_MAP_WIN(F8, VK_F8)
+KEY_MAP_COCOA(F8, kVK_F8)
+KEY_MAP_GTK(F8, GDK_F8)
+KEY_MAP_ANDROID(F8, AKEYCODE_F8)
+
+// F9
+KEY_MAP_WIN(F9, VK_F9)
+KEY_MAP_COCOA(F9, kVK_F9)
+KEY_MAP_GTK(F9, GDK_F9)
+KEY_MAP_ANDROID(F9, AKEYCODE_F9)
+
+// F10
+KEY_MAP_WIN(F10, VK_F10)
+KEY_MAP_COCOA(F10, kVK_F10)
+KEY_MAP_GTK(F10, GDK_F10)
+KEY_MAP_ANDROID(F10, AKEYCODE_F10)
+
+// F11
+KEY_MAP_WIN(F11, VK_F11)
+KEY_MAP_COCOA(F11, kVK_F11)
+KEY_MAP_GTK(F11, GDK_F11 /* same as GDK_L1 */)
+KEY_MAP_ANDROID(F11, AKEYCODE_F11)
+
+// F12
+KEY_MAP_WIN(F12, VK_F12)
+KEY_MAP_COCOA(F12, kVK_F12)
+KEY_MAP_GTK(F12, GDK_F12 /* same as GDK_L2 */)
+KEY_MAP_ANDROID(F12, AKEYCODE_F12)
+
+// F13
+KEY_MAP_WIN(F13, VK_F13)
+KEY_MAP_COCOA(F13, kVK_F13)
+KEY_MAP_GTK(F13, GDK_F13 /* same as GDK_L3 */)
+
+// F14
+KEY_MAP_WIN(F14, VK_F14)
+KEY_MAP_COCOA(F14, kVK_F14)
+KEY_MAP_GTK(F14, GDK_F14 /* same as GDK_L4 */)
+
+// F15
+KEY_MAP_WIN(F15, VK_F15)
+KEY_MAP_COCOA(F15, kVK_F15)
+KEY_MAP_GTK(F15, GDK_F15 /* same as GDK_L5 */)
+
+// F16
+KEY_MAP_WIN(F16, VK_F16)
+KEY_MAP_COCOA(F16, kVK_F16)
+KEY_MAP_GTK(F16, GDK_F16 /* same as GDK_L6 */)
+
+// F17
+KEY_MAP_WIN(F17, VK_F17)
+KEY_MAP_COCOA(F17, kVK_F17)
+KEY_MAP_GTK(F17, GDK_F17 /* same as GDK_L7 */)
+
+// F18
+KEY_MAP_WIN(F18, VK_F18)
+KEY_MAP_COCOA(F18, kVK_F18)
+KEY_MAP_GTK(F18, GDK_F18 /* same as GDK_L8 */)
+
+// F19
+KEY_MAP_WIN(F19, VK_F19)
+KEY_MAP_COCOA(F19, kVK_F19)
+KEY_MAP_GTK(F19, GDK_F19 /* same as GDK_L9 */)
+
+// F20
+KEY_MAP_WIN(F20, VK_F20)
+KEY_MAP_GTK(F20, GDK_F20 /* same as GDK_L10 */)
+
+// F21
+KEY_MAP_WIN(F21, VK_F21)
+KEY_MAP_GTK(F21, GDK_F21 /* same as GDK_R1 */)
+
+// F22
+KEY_MAP_WIN(F22, VK_F22)
+KEY_MAP_GTK(F22, GDK_F22 /* same as GDK_R2 */)
+
+// F23
+KEY_MAP_WIN(F23, VK_F23)
+KEY_MAP_GTK(F23, GDK_F23 /* same as GDK_R3 */)
+
+// F24
+KEY_MAP_WIN(F24, VK_F24)
+KEY_MAP_GTK(F24, GDK_F24 /* same as GDK_R4 */)
+
+// F25
+KEY_MAP_GTK(F25, GDK_F25 /* same as GDK_R5 */)
+
+// F26
+KEY_MAP_GTK(F26, GDK_F26 /* same as GDK_R6 */)
+
+// F27
+KEY_MAP_GTK(F27, GDK_F27 /* same as GDK_R7 */)
+
+// F28
+KEY_MAP_GTK(F28, GDK_F28 /* same as GDK_R8 */)
+
+// F29
+KEY_MAP_GTK(F29, GDK_F29 /* same as GDK_R9 */)
+
+// F30
+KEY_MAP_GTK(F30, GDK_F30 /* same as GDK_R10 */)
+
+// F31
+KEY_MAP_GTK(F31, GDK_F31 /* same as GDK_R11 */)
+
+// F32
+KEY_MAP_GTK(F32, GDK_F32 /* same as GDK_R12 */)
+
+// F33
+KEY_MAP_GTK(F33, GDK_F33 /* same as GDK_R13 */)
+
+// F34
+KEY_MAP_GTK(F34, GDK_F34 /* same as GDK_R14 */)
+
+// F35
+KEY_MAP_GTK(F35, GDK_F35 /* same as GDK_R15 */)
+
+/******************************************************************************
+ * Multimedia Keys
+ ******************************************************************************/
+// ChannelDown
+KEY_MAP_WIN_CMD(ChannelDown, APPCOMMAND_MEDIA_CHANNEL_DOWN)
+KEY_MAP_ANDROID(ChannelDown, AKEYCODE_CHANNEL_DOWN)
+
+// ChannelUp
+KEY_MAP_WIN_CMD(ChannelUp, APPCOMMAND_MEDIA_CHANNEL_UP)
+KEY_MAP_ANDROID(ChannelUp, AKEYCODE_CHANNEL_UP)
+
+// Close
+// NOTE: This is not a key to close disk tray, this is a key to close document
+// or window.
+KEY_MAP_WIN_CMD(Close, APPCOMMAND_CLOSE)
+KEY_MAP_GTK(Close, GDK_Close)
+
+// MailForward
+KEY_MAP_WIN_CMD(MailForward, APPCOMMAND_FORWARD_MAIL)
+KEY_MAP_GTK(MailForward, GDK_MailForward)
+
+// MailReply
+KEY_MAP_WIN_CMD(MailReply, APPCOMMAND_REPLY_TO_MAIL)
+KEY_MAP_GTK(MailReply, GDK_Reply)
+
+// MailSend
+KEY_MAP_WIN_CMD(MailSend, APPCOMMAND_SEND_MAIL)
+KEY_MAP_GTK(MailSend, GDK_Send)
+
+// MediaFastForward
+KEY_MAP_WIN_CMD(MediaFastForward, APPCOMMAND_MEDIA_FAST_FORWARD)
+KEY_MAP_GTK(MediaFastForward, GDK_AudioForward)
+KEY_MAP_ANDROID(MediaFastForward, AKEYCODE_MEDIA_FAST_FORWARD)
+
+// MediaPause
+KEY_MAP_WIN_CMD(MediaPause, APPCOMMAND_MEDIA_PAUSE)
+KEY_MAP_GTK(MediaPause, GDK_AudioPause)
+KEY_MAP_ANDROID(MediaPause, AKEYCODE_MEDIA_PAUSE)
+
+// MediaPlay
+KEY_MAP_WIN_CMD(MediaPlay, APPCOMMAND_MEDIA_PLAY)
+KEY_MAP_GTK(MediaPlay, GDK_AudioPlay)
+KEY_MAP_ANDROID(MediaPlay, AKEYCODE_MEDIA_PLAY)
+
+// MediaPlayPause
+KEY_MAP_WIN(MediaPlayPause, VK_MEDIA_PLAY_PAUSE)
+KEY_MAP_WIN_CMD(MediaPlayPause, APPCOMMAND_MEDIA_PLAY_PAUSE)
+KEY_MAP_ANDROID(MediaPlayPause, AKEYCODE_MEDIA_PLAY_PAUSE)
+
+// MediaRecord
+KEY_MAP_WIN_CMD(MediaRecord, APPCOMMAND_MEDIA_RECORD)
+KEY_MAP_GTK(MediaRecord, GDK_AudioRecord)
+KEY_MAP_ANDROID(MediaRecord, AKEYCODE_MEDIA_RECORD)
+
+// MediaRewind
+KEY_MAP_WIN_CMD(MediaRewind, APPCOMMAND_MEDIA_REWIND)
+KEY_MAP_GTK(MediaRewind, GDK_AudioRewind)
+KEY_MAP_ANDROID(MediaRewind, AKEYCODE_MEDIA_REWIND)
+
+// MediaStop
+KEY_MAP_WIN(MediaStop, VK_MEDIA_STOP)
+KEY_MAP_WIN_CMD(MediaStop, APPCOMMAND_MEDIA_STOP)
+KEY_MAP_GTK(MediaStop, GDK_AudioStop)
+KEY_MAP_ANDROID(MediaStop, AKEYCODE_MEDIA_STOP)
+
+// MediaTrackNext
+KEY_MAP_WIN(MediaTrackNext, VK_MEDIA_NEXT_TRACK)
+KEY_MAP_WIN_CMD(MediaTrackNext, APPCOMMAND_MEDIA_NEXTTRACK)
+KEY_MAP_GTK(MediaTrackNext, GDK_AudioNext)
+KEY_MAP_ANDROID(MediaTrackNext, AKEYCODE_MEDIA_NEXT)
+
+// MediaTrackPrevious
+KEY_MAP_WIN(MediaTrackPrevious, VK_MEDIA_PREV_TRACK)
+KEY_MAP_WIN_CMD(MediaTrackPrevious, APPCOMMAND_MEDIA_PREVIOUSTRACK)
+KEY_MAP_GTK(MediaTrackPrevious, GDK_AudioPrev)
+KEY_MAP_ANDROID(MediaTrackPrevious, AKEYCODE_MEDIA_PREVIOUS)
+
+// New
+KEY_MAP_WIN_CMD(New, APPCOMMAND_NEW)
+KEY_MAP_GTK(New, GDK_New)
+
+// Open
+KEY_MAP_WIN_CMD(Open, APPCOMMAND_OPEN)
+KEY_MAP_GTK(Open, GDK_Open)
+
+// Print
+KEY_MAP_WIN_CMD(Print, APPCOMMAND_PRINT)
+
+// Save
+KEY_MAP_WIN_CMD(Save, APPCOMMAND_SAVE)
+KEY_MAP_GTK(Save, GDK_Save)
+
+// SpellCheck
+KEY_MAP_WIN_CMD(SpellCheck, APPCOMMAND_SPELL_CHECK)
+KEY_MAP_GTK(SpellCheck, GDK_Spell)
+
+/******************************************************************************
+ * Audio Keys
+ *****************************************************************************/
+// AudioBassBoostDown
+KEY_MAP_WIN_CMD(AudioBassBoostDown, APPCOMMAND_BASS_DOWN)
+
+// AudioBassBoostUp
+KEY_MAP_WIN_CMD(AudioBassBoostUp, APPCOMMAND_BASS_UP)
+
+// AudioVolumeDown
+KEY_MAP_WIN(AudioVolumeDown, VK_VOLUME_DOWN)
+KEY_MAP_WIN_CMD(AudioVolumeDown, APPCOMMAND_VOLUME_DOWN)
+KEY_MAP_COCOA(AudioVolumeDown, kVK_VolumeDown)
+KEY_MAP_GTK(AudioVolumeDown, GDK_AudioLowerVolume)
+KEY_MAP_ANDROID(AudioVolumeDown, AKEYCODE_VOLUME_DOWN)
+
+// AudioVolumeUp
+KEY_MAP_WIN(AudioVolumeUp, VK_VOLUME_UP)
+KEY_MAP_WIN_CMD(AudioVolumeUp, APPCOMMAND_VOLUME_UP)
+KEY_MAP_COCOA(AudioVolumeUp, kVK_VolumeUp)
+KEY_MAP_GTK(AudioVolumeUp, GDK_AudioRaiseVolume)
+KEY_MAP_ANDROID(AudioVolumeUp, AKEYCODE_VOLUME_UP)
+
+// AudioVolumeMute
+KEY_MAP_WIN(AudioVolumeMute, VK_VOLUME_MUTE)
+KEY_MAP_WIN_CMD(AudioVolumeMute, APPCOMMAND_VOLUME_MUTE)
+KEY_MAP_COCOA(AudioVolumeMute, kVK_Mute)
+KEY_MAP_GTK(AudioVolumeMute, GDK_AudioMute)
+KEY_MAP_ANDROID(AudioVolumeMute, AKEYCODE_VOLUME_MUTE)
+
+// MicrophoneVolumeMute
+KEY_MAP_ANDROID(MicrophoneVolumeMute, AKEYCODE_MUTE)
+
+/******************************************************************************
+ * Application Keys
+ ******************************************************************************/
+// LaunchCalculator
+KEY_MAP_GTK(LaunchCalculator, GDK_Calculator)
+KEY_MAP_ANDROID(LaunchCalculator, AKEYCODE_CALCULATOR)
+
+// LaunchCalendar
+KEY_MAP_GTK(LaunchCalendar, GDK_Calendar)
+KEY_MAP_ANDROID(LaunchCalendar, AKEYCODE_CALENDAR)
+
+// LaunchContacts
+KEY_MAP_ANDROID(LaunchContacts, AKEYCODE_CONTACTS)
+
+// LaunchMail
+KEY_MAP_WIN(LaunchMail, VK_LAUNCH_MAIL)
+KEY_MAP_WIN_CMD(LaunchMail, APPCOMMAND_LAUNCH_MAIL)
+KEY_MAP_GTK(LaunchMail, GDK_Mail)
+KEY_MAP_ANDROID(LaunchMail, AKEYCODE_ENVELOPE)
+
+// LaunchMediaPlayer
+KEY_MAP_WIN(LaunchMediaPlayer, VK_LAUNCH_MEDIA_SELECT)
+KEY_MAP_WIN_CMD(LaunchMediaPlayer, APPCOMMAND_LAUNCH_MEDIA_SELECT)
+// GDK_CD is defined as "Launch CD/DVD player" in XF86keysym.h.
+// Therefore, let's map it to media player rather than music player.
+KEY_MAP_GTK(LaunchMediaPlayer, GDK_CD)
+KEY_MAP_GTK(LaunchMediaPlayer, GDK_Video)
+KEY_MAP_GTK(LaunchMediaPlayer, GDK_AudioMedia)
+
+// LaunchMusicPlayer
+KEY_MAP_GTK(LaunchMusicPlayer, GDK_Music)
+KEY_MAP_ANDROID(LaunchMusicPlayer, AKEYCODE_MUSIC)
+
+// LaunchMyComputer
+KEY_MAP_GTK(LaunchMyComputer, GDK_MyComputer)
+KEY_MAP_GTK(LaunchMyComputer, GDK_Explorer)
+
+// LaunchScreenSaver
+KEY_MAP_GTK(LaunchScreenSaver, GDK_ScreenSaver)
+
+// LaunchSpreadsheet
+KEY_MAP_GTK(LaunchSpreadsheet, GDK_Excel)
+
+// LaunchWebBrowser
+KEY_MAP_GTK(LaunchWebBrowser, GDK_WWW)
+KEY_MAP_ANDROID(LaunchWebBrowser, AKEYCODE_EXPLORER)
+
+// LaunchWebCam
+KEY_MAP_GTK(LaunchWebCam, GDK_WebCam)
+
+// LaunchWordProcessor
+KEY_MAP_GTK(LaunchWordProcessor, GDK_Word)
+
+// LaunchApplication1
+KEY_MAP_WIN(LaunchApplication1, VK_LAUNCH_APP1)
+KEY_MAP_WIN_CMD(LaunchApplication1, APPCOMMAND_LAUNCH_APP1)
+KEY_MAP_GTK(LaunchApplication1, GDK_Launch0)
+
+// LaunchApplication2
+KEY_MAP_WIN(LaunchApplication2, VK_LAUNCH_APP2)
+KEY_MAP_WIN_CMD(LaunchApplication2, APPCOMMAND_LAUNCH_APP2)
+KEY_MAP_GTK(LaunchApplication2, GDK_Launch1)
+
+// LaunchApplication3
+KEY_MAP_GTK(LaunchApplication3, GDK_Launch2)
+
+// LaunchApplication4
+KEY_MAP_GTK(LaunchApplication4, GDK_Launch3)
+
+// LaunchApplication5
+KEY_MAP_GTK(LaunchApplication5, GDK_Launch4)
+
+// LaunchApplication6
+KEY_MAP_GTK(LaunchApplication6, GDK_Launch5)
+
+// LaunchApplication7
+KEY_MAP_GTK(LaunchApplication7, GDK_Launch6)
+
+// LaunchApplication8
+KEY_MAP_GTK(LaunchApplication8, GDK_Launch7)
+
+// LaunchApplication9
+KEY_MAP_GTK(LaunchApplication9, GDK_Launch8)
+
+// LaunchApplication10
+KEY_MAP_GTK(LaunchApplication10, GDK_Launch9)
+
+// LaunchApplication11
+KEY_MAP_GTK(LaunchApplication11, GDK_LaunchA)
+
+// LaunchApplication12
+KEY_MAP_GTK(LaunchApplication12, GDK_LaunchB)
+
+// LaunchApplication13
+KEY_MAP_GTK(LaunchApplication13, GDK_LaunchC)
+
+// LaunchApplication14
+KEY_MAP_GTK(LaunchApplication14, GDK_LaunchD)
+
+// LaunchApplication15
+KEY_MAP_GTK(LaunchApplication15, GDK_LaunchE)
+
+// LaunchApplication16
+KEY_MAP_GTK(LaunchApplication16, GDK_LaunchF)
+
+// LaunchApplication17
+
+// LaunchApplication18
+
+/******************************************************************************
+ * Browser Keys
+ ******************************************************************************/
+// BrowserBack
+KEY_MAP_WIN(BrowserBack, VK_BROWSER_BACK)
+KEY_MAP_WIN_CMD(BrowserBack, APPCOMMAND_BROWSER_BACKWARD)
+KEY_MAP_GTK(BrowserBack, GDK_Back)
+
+// BrowserFavorites
+KEY_MAP_WIN(BrowserFavorites, VK_BROWSER_FAVORITES)
+KEY_MAP_WIN_CMD(BrowserFavorites, APPCOMMAND_BROWSER_FAVORITES)
+KEY_MAP_ANDROID(BrowserFavorites, AKEYCODE_BOOKMARK)
+
+// BrowserForward
+KEY_MAP_WIN(BrowserForward, VK_BROWSER_FORWARD)
+KEY_MAP_WIN_CMD(BrowserForward, APPCOMMAND_BROWSER_FORWARD)
+KEY_MAP_GTK(BrowserForward, GDK_Forward)
+KEY_MAP_ANDROID(BrowserForward, AKEYCODE_FORWARD)
+
+// BrowserHome
+KEY_MAP_WIN(BrowserHome, VK_BROWSER_HOME)
+KEY_MAP_WIN_CMD(BrowserHome, APPCOMMAND_BROWSER_HOME)
+KEY_MAP_GTK(BrowserHome, GDK_HomePage)
+
+// BrowserRefresh
+KEY_MAP_WIN(BrowserRefresh, VK_BROWSER_REFRESH)
+KEY_MAP_WIN_CMD(BrowserRefresh, APPCOMMAND_BROWSER_REFRESH)
+KEY_MAP_GTK(BrowserRefresh, GDK_Refresh)
+KEY_MAP_GTK(BrowserRefresh, GDK_Reload)
+
+// BrowserSearch
+KEY_MAP_WIN(BrowserSearch, VK_BROWSER_SEARCH)
+KEY_MAP_WIN_CMD(BrowserSearch, APPCOMMAND_BROWSER_SEARCH)
+KEY_MAP_GTK(BrowserSearch, GDK_Search)
+KEY_MAP_ANDROID(BrowserSearch, AKEYCODE_SEARCH)
+
+// BrowserStop
+KEY_MAP_WIN(BrowserStop, VK_BROWSER_STOP)
+KEY_MAP_WIN_CMD(BrowserStop, APPCOMMAND_BROWSER_STOP)
+KEY_MAP_GTK(BrowserStop, GDK_Stop)
+
+/******************************************************************************
+ * Mobile Phone Keys
+ ******************************************************************************/
+// AppSwitch
+KEY_MAP_ANDROID(AppSwitch, AKEYCODE_APP_SWITCH)
+
+// Call
+KEY_MAP_ANDROID(Call, AKEYCODE_CALL)
+
+// Camera
+KEY_MAP_ANDROID(Camera, AKEYCODE_CAMERA)
+
+// CameraFocus
+KEY_MAP_ANDROID(CameraFocus, AKEYCODE_FOCUS)
+
+// EndCall
+KEY_MAP_ANDROID(EndCall, AKEYCODE_ENDCALL)
+
+// GoBack
+KEY_MAP_ANDROID(GoBack, AKEYCODE_BACK)
+
+// GoHome
+KEY_MAP_ANDROID(GoHome, AKEYCODE_HOME)
+
+// HeadsetHook
+KEY_MAP_ANDROID(HeadsetHook, AKEYCODE_HEADSETHOOK)
+
+// Notification
+KEY_MAP_ANDROID(Notification, AKEYCODE_NOTIFICATION)
+
+// MannerMode
+KEY_MAP_ANDROID(MannerMode, AKEYCODE_MANNER_MODE)
+
+/******************************************************************************
+ * TV Keys
+ ******************************************************************************/
+// TV
+KEY_MAP_ANDROID(TV, AKEYCODE_TV)
+
+// TV3DMode
+KEY_MAP_ANDROID(TV3DMode, AKEYCODE_3D_MODE)
+
+// TVAntennaCable
+KEY_MAP_ANDROID(TVAntennaCable, AKEYCODE_TV_ANTENNA_CABLE)
+
+// TVAudioDescription
+KEY_MAP_ANDROID(TVAudioDescription, AKEYCODE_TV_AUDIO_DESCRIPTION)
+
+// TVAudioDescriptionMixDown
+KEY_MAP_ANDROID(TVAudioDescriptionMixDown,
+ AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN)
+
+// TVAudioDescriptionMixUp
+KEY_MAP_ANDROID(TVAudioDescriptionMixUp, AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP)
+
+// TVContentsMenu
+KEY_MAP_ANDROID(TVContentsMenu, AKEYCODE_TV_CONTENTS_MENU)
+
+// TVDataService
+KEY_MAP_ANDROID(TVDataService, AKEYCODE_TV_DATA_SERVICE)
+
+// TVInput
+KEY_MAP_ANDROID(TVInput, AKEYCODE_TV_INPUT)
+
+// TVInputComponent1
+KEY_MAP_ANDROID(TVInputComponent1, AKEYCODE_TV_INPUT_COMPONENT_1)
+
+// TVInputComponent2
+KEY_MAP_ANDROID(TVInputComponent2, AKEYCODE_TV_INPUT_COMPONENT_2)
+
+// TVInputComposite1
+KEY_MAP_ANDROID(TVInputComposite1, AKEYCODE_TV_INPUT_COMPOSITE_1)
+
+// TVInputComposite2
+KEY_MAP_ANDROID(TVInputComposite2, AKEYCODE_TV_INPUT_COMPOSITE_2)
+
+// TVInputHDMI1
+KEY_MAP_ANDROID(TVInputHDMI1, AKEYCODE_TV_INPUT_HDMI_1)
+
+// TVInputHDMI2
+KEY_MAP_ANDROID(TVInputHDMI2, AKEYCODE_TV_INPUT_HDMI_2)
+
+// TVInputHDMI3
+KEY_MAP_ANDROID(TVInputHDMI3, AKEYCODE_TV_INPUT_HDMI_3)
+
+// TVInputHDMI4
+KEY_MAP_ANDROID(TVInputHDMI4, AKEYCODE_TV_INPUT_HDMI_4)
+
+// TVInputVGA1
+KEY_MAP_ANDROID(TVInputVGA1, AKEYCODE_TV_INPUT_VGA_1)
+
+// TVNetwork
+KEY_MAP_ANDROID(TVNetwork, AKEYCODE_TV_NETWORK)
+
+// TVNumberEntry
+KEY_MAP_ANDROID(TVNumberEntry, AKEYCODE_TV_NUMBER_ENTRY)
+
+// TVPower
+KEY_MAP_ANDROID(TVPower, AKEYCODE_TV_POWER)
+
+// TVRadioService
+KEY_MAP_ANDROID(TVRadioService, AKEYCODE_TV_RADIO_SERVICE)
+
+// TVSatellite
+KEY_MAP_ANDROID(TVSatellite, AKEYCODE_TV_SATELLITE)
+
+// TVSatelliteBS
+KEY_MAP_ANDROID(TVSatelliteBS, AKEYCODE_TV_SATELLITE_BS)
+
+// TVSatelliteCS
+KEY_MAP_ANDROID(TVSatelliteCS, AKEYCODE_TV_SATELLITE_CS)
+
+// TVSatelliteToggle
+KEY_MAP_ANDROID(TVSatelliteToggle, AKEYCODE_TV_SATELLITE_SERVICE)
+
+// TVTerrestrialAnalog
+KEY_MAP_ANDROID(TVTerrestrialAnalog, AKEYCODE_TV_TERRESTRIAL_ANALOG)
+
+// TVTerrestrialDigital
+KEY_MAP_ANDROID(TVTerrestrialDigital, AKEYCODE_TV_TERRESTRIAL_DIGITAL)
+
+// TVTimer
+KEY_MAP_ANDROID(TVTimer, AKEYCODE_TV_TIMER_PROGRAMMING)
+
+/******************************************************************************
+ * Media Controller Keys
+ ******************************************************************************/
+// AVRInput
+KEY_MAP_ANDROID(AVRInput, AKEYCODE_AVR_INPUT)
+
+// AVRPower
+KEY_MAP_ANDROID(AVRPower, AKEYCODE_AVR_POWER)
+
+// ColorF0Red
+KEY_MAP_GTK(ColorF0Red, GDK_Red)
+KEY_MAP_ANDROID(ColorF0Red, AKEYCODE_PROG_RED)
+
+// ColorF1Green
+KEY_MAP_GTK(ColorF1Green, GDK_Green)
+KEY_MAP_ANDROID(ColorF1Green, AKEYCODE_PROG_GREEN)
+
+// ColorF2Yellow
+KEY_MAP_GTK(ColorF2Yellow, GDK_Yellow)
+KEY_MAP_ANDROID(ColorF2Yellow, AKEYCODE_PROG_YELLOW)
+
+// ColorF3Blue
+KEY_MAP_GTK(ColorF3Blue, GDK_Blue)
+KEY_MAP_ANDROID(ColorF3Blue, AKEYCODE_PROG_BLUE)
+
+// ClosedCaptionToggle
+KEY_MAP_ANDROID(ClosedCaptionToggle, AKEYCODE_CAPTIONS)
+
+// Dimmer
+KEY_MAP_GTK(Dimmer, GDK_BrightnessAdjust)
+
+// DVR
+KEY_MAP_ANDROID(DVR, AKEYCODE_DVR)
+
+// Guide
+KEY_MAP_ANDROID(Guide, AKEYCODE_GUIDE)
+
+// Info
+KEY_MAP_ANDROID(Info, AKEYCODE_INFO)
+
+// MediaAudioTrack
+KEY_MAP_ANDROID(MediaAudioTrack, AKEYCODE_MEDIA_AUDIO_TRACK)
+
+// MediaLast
+KEY_MAP_ANDROID(MediaLast, AKEYCODE_LAST_CHANNEL)
+
+// MediaTopMenu
+KEY_MAP_ANDROID(MediaTopMenu, AKEYCODE_MEDIA_TOP_MENU)
+
+// MediaSkipBackward
+KEY_MAP_ANDROID(MediaSkipBackward, AKEYCODE_MEDIA_SKIP_BACKWARD)
+
+// MediaSkipForward
+KEY_MAP_ANDROID(MediaSkipForward, AKEYCODE_MEDIA_SKIP_FORWARD)
+
+// MediaStepBackward
+KEY_MAP_ANDROID(MediaStepBackward, AKEYCODE_MEDIA_STEP_BACKWARD)
+
+// MediaStepForward
+KEY_MAP_ANDROID(MediaStepForward, AKEYCODE_MEDIA_STEP_FORWARD)
+
+// NavigateIn
+KEY_MAP_ANDROID(NavigateIn, AKEYCODE_NAVIGATE_IN)
+
+// NavigateNext
+KEY_MAP_ANDROID(NavigateNext, AKEYCODE_NAVIGATE_NEXT)
+
+// NavigateOut
+KEY_MAP_ANDROID(NavigateOut, AKEYCODE_NAVIGATE_OUT)
+
+// NavigatePrevious
+KEY_MAP_ANDROID(NavigatePrevious, AKEYCODE_NAVIGATE_PREVIOUS)
+
+// Pairing
+KEY_MAP_ANDROID(Pairing, AKEYCODE_PAIRING)
+
+// PinPToggle
+KEY_MAP_ANDROID(PinPToggle, AKEYCODE_WINDOW)
+
+// RandomToggle
+KEY_MAP_GTK(RandomToggle, GDK_AudioRandomPlay)
+
+// Settings
+KEY_MAP_ANDROID(Settings, AKEYCODE_SETTINGS)
+
+// STBInput
+KEY_MAP_ANDROID(STBInput, AKEYCODE_STB_INPUT)
+
+// STBPower
+KEY_MAP_ANDROID(STBPower, AKEYCODE_STB_POWER)
+
+// Subtitle
+KEY_MAP_GTK(Subtitle, GDK_Subtitle)
+
+// Teletext
+KEY_MAP_ANDROID(Teletext, AKEYCODE_TV_TELETEXT)
+
+// VideoModeNext
+KEY_MAP_GTK(VideoModeNext, GDK_Next_VMode)
+
+// ZoomToggle
+KEY_MAP_WIN(ZoomToggle, VK_ZOOM)
+KEY_MAP_ANDROID(ZoomToggle, AKEYCODE_TV_ZOOM_MODE)
+
+/******************************************************************************
+ * Keys not defined by any standards
+ ******************************************************************************/
+// SoftLeft
+KEY_MAP_ANDROID(SoftLeft, AKEYCODE_SOFT_LEFT)
+
+// SoftRight
+KEY_MAP_ANDROID(SoftRight, AKEYCODE_SOFT_RIGHT)
+
+#undef KEY_MAP_WIN
+#undef KEY_MAP_WIN_JPN
+#undef KEY_MAP_WIN_KOR
+#undef KEY_MAP_WIN_OTH
+#undef KEY_MAP_WIN_CMD
+#undef KEY_MAP_COCOA
+#undef KEY_MAP_GTK
+#undef KEY_MAP_ANDROID
diff --git a/widget/NativeMenu.h b/widget/NativeMenu.h
new file mode 100644
index 0000000000..ef1808c7f9
--- /dev/null
+++ b/widget/NativeMenu.h
@@ -0,0 +1,101 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_NativeMenu_h
+#define mozilla_widget_NativeMenu_h
+
+#include "nsISupportsImpl.h"
+#include "Units.h"
+
+class nsIFrame;
+class nsPresContext;
+
+namespace mozilla {
+using Modifiers = uint16_t;
+class ErrorResult;
+} // namespace mozilla
+
+namespace mozilla::dom {
+class Element;
+}
+
+namespace mozilla::widget {
+
+class NativeMenu {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(NativeMenu)
+
+ // Show this menu as a context menu at the specified position.
+ // This call assumes that the popupshowing event for the root popup has
+ // already been sent and "approved", i.e. preventDefault() was not called.
+ virtual void ShowAsContextMenu(nsIFrame* aClickedFrame,
+ const CSSIntPoint& aPosition,
+ bool aIsContextMenu) = 0;
+
+ // Close the menu and synchronously fire popuphiding / popuphidden events.
+ // Returns false if the menu wasn't open.
+ virtual bool Close() = 0;
+
+ // Activate aItemElement and close this menu.
+ // aItemElement can be nested arbitrarily deeply within submenus inside this
+ // menu. Only works while this menu (and any submenus on the path to the
+ // item) is open, otherwise aRv reports an error.
+ virtual void ActivateItem(dom::Element* aItemElement, Modifiers aModifiers,
+ int16_t aButton, ErrorResult& aRv) = 0;
+
+ // Open, or simulate the opening of, a submenu.
+ // aMenuElement can be nested arbitrarily deeply within submenus inside this
+ // menu. Only works while this menu (and any submenus on the path to the
+ // submenu) is open.
+ virtual void OpenSubmenu(dom::Element* aMenuElement) = 0;
+
+ // Closing, or simulate the closing of, a submenu.
+ // aMenuElement can be nested arbitrarily deeply within submenus inside this
+ // menu. Only works while this menu (and any submenus on the path to the
+ // submenu) is open.
+ virtual void CloseSubmenu(dom::Element* aMenuElement) = 0;
+
+ // Return this NativeMenu's DOM element.
+ virtual RefPtr<dom::Element> Element() = 0;
+
+ class Observer {
+ public:
+ // Called when the menu opened, after popupshown.
+ // No strong reference is held to the observer during the call.
+ virtual void OnNativeMenuOpened() = 0;
+
+ // Called when the menu closed, after popuphidden.
+ // No strong reference is held to the observer during the call.
+ virtual void OnNativeMenuClosed() = 0;
+
+ // Called before the popupshowing event of a submenu fires.
+ virtual void OnNativeSubMenuWillOpen(dom::Element* aPopupElement) = 0;
+
+ // Called after the popupshown event of a submenu fired.
+ virtual void OnNativeSubMenuDidOpen(dom::Element* aPopupElement) = 0;
+
+ // Called after the popuphidden event of a submenu fired.
+ virtual void OnNativeSubMenuClosed(dom::Element* aPopupElement) = 0;
+
+ // Called before the command event of an activated menu item fires.
+ virtual void OnNativeMenuWillActivateItem(
+ dom::Element* aMenuItemElement) = 0;
+ };
+
+ // Add an observer that gets notified of menu opening and closing.
+ // The menu does not keep a strong reference the observer. The observer must
+ // remove itself before it is destroyed.
+ virtual void AddObserver(Observer* aObserver) = 0;
+
+ // Remove an observer that was previously added with AddObserver.
+ virtual void RemoveObserver(Observer* aObserver) = 0;
+
+ protected:
+ virtual ~NativeMenu() = default;
+};
+
+} // namespace mozilla::widget
+
+#endif // mozilla_widget_NativeMenu_h
diff --git a/widget/NativeMenuSupport.h b/widget/NativeMenuSupport.h
new file mode 100644
index 0000000000..c6429b5d4d
--- /dev/null
+++ b/widget/NativeMenuSupport.h
@@ -0,0 +1,47 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_NativeMenuSupport_h
+#define mozilla_widget_NativeMenuSupport_h
+
+#include "mozilla/RefPtr.h"
+
+class nsIWidget;
+
+#if defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
+# define HAS_NATIVE_MENU_SUPPORT 1
+#endif
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+}
+
+namespace widget {
+
+class NativeMenu;
+
+class NativeMenuSupport final {
+ public:
+ // Given a top-level window widget and a menu bar DOM node, sets up native
+ // menus. Once created, native menus are controlled via the DOM, including
+ // destruction.
+ static void CreateNativeMenuBar(nsIWidget* aParent,
+ dom::Element* aMenuBarElement);
+
+ // Given a menupopup DOM node, create a NativeMenu instance that can be shown
+ // as a native context menu.
+ static already_AddRefed<NativeMenu> CreateNativeContextMenu(
+ dom::Element* aPopup);
+
+ // Whether or not native context menus are enabled.
+ static bool ShouldUseNativeContextMenus();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_NativeMenuSupport_h
diff --git a/widget/PClipboardReadRequest.ipdl b/widget/PClipboardReadRequest.ipdl
new file mode 100644
index 0000000000..39ae8ed38e
--- /dev/null
+++ b/widget/PClipboardReadRequest.ipdl
@@ -0,0 +1,25 @@
+/* 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 protocol PContent;
+
+include DOMTypes;
+include IPCTransferable;
+include NeckoChannelParams;
+
+using nsContentPolicyType from "nsIContentPolicy.h";
+
+namespace mozilla {
+
+protocol PClipboardReadRequest {
+ manager PContent;
+
+ parent:
+ async GetData(nsCString[] aFlavors) returns (IPCTransferableDataOrError aTransferableData);
+
+ both:
+ async __delete__();
+};
+
+} // namespace mozilla
diff --git a/widget/PClipboardWriteRequest.ipdl b/widget/PClipboardWriteRequest.ipdl
new file mode 100644
index 0000000000..54b7c9a136
--- /dev/null
+++ b/widget/PClipboardWriteRequest.ipdl
@@ -0,0 +1,25 @@
+/* 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 protocol PContent;
+
+include DOMTypes;
+include IPCTransferable;
+include NeckoChannelParams;
+
+using nsContentPolicyType from "nsIContentPolicy.h";
+
+namespace mozilla {
+
+protocol PClipboardWriteRequest {
+ manager PContent;
+
+ parent:
+ async SetData(IPCTransferable aTransferable);
+
+ both:
+ async __delete__(nsresult aResult);
+};
+
+} // namespace mozilla
diff --git a/widget/PrintBackgroundTask.h b/widget/PrintBackgroundTask.h
new file mode 100644
index 0000000000..8b18c8a879
--- /dev/null
+++ b/widget/PrintBackgroundTask.h
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef mozilla_PrintBackgroundTask_h_
+#define mozilla_PrintBackgroundTask_h_
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Telemetry.h"
+
+#include <tuple>
+#include <utility>
+
+// A helper to resolve a DOM Promise with the result of a const method, executed
+// in another thread.
+//
+// Once in the main thread, the caller can turn the result of the method into a
+// JSValue by specializing ResolveOrReject.
+namespace mozilla {
+
+template <typename T, typename Result>
+void ResolveOrReject(dom::Promise& aPromise, T&, Result& aResult) {
+ aPromise.MaybeResolve(std::forward<Result>(aResult));
+}
+
+template <typename T, typename Result, typename... Args>
+using PrintBackgroundTask = Result (T::*)(Args...) const;
+
+template <typename T, typename Result, typename... Args>
+void SpawnPrintBackgroundTask(
+ T& aReceiver, dom::Promise& aPromise, const nsCString& aTelemetryKey,
+ PrintBackgroundTask<T, Result, Args...> aBackgroundTask, Args... aArgs) {
+ auto promiseHolder = MakeRefPtr<nsMainThreadPtrHolder<dom::Promise>>(
+ "nsPrinterBase::SpawnBackgroundTaskPromise", &aPromise);
+ // We actually want to allow to access the printer data from the callback, so
+ // disable strict checking. They should of course only access immutable
+ // members.
+ auto holder = MakeRefPtr<nsMainThreadPtrHolder<T>>(
+ "nsPrinterBase::SpawnBackgroundTaskPrinter", &aReceiver,
+ /* strict = */ false);
+ // See
+ // https://stackoverflow.com/questions/47496358/c-lambdas-how-to-capture-variadic-parameter-pack-from-the-upper-scope
+ // about the tuple shenanigans. It could be improved with C++20
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "SpawnPrintBackgroundTask",
+ [holder = std::move(holder), promiseHolder = std::move(promiseHolder),
+ aTelemetryKey, startRoundTrip = TimeStamp::Now(),
+ backgroundTask = aBackgroundTask,
+ aArgs = std::make_tuple(std::forward<Args>(aArgs)...)] {
+ auto start = TimeStamp::Now();
+ Result result = std::apply(
+ [&](auto&&... args) {
+ return (holder->get()->*backgroundTask)(args...);
+ },
+ std::move(aArgs));
+ Telemetry::AccumulateTimeDelta(
+ Telemetry::PRINT_BACKGROUND_TASK_TIME_MS, aTelemetryKey, start);
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "SpawnPrintBackgroundTaskResolution",
+ [holder = std::move(holder),
+ promiseHolder = std::move(promiseHolder),
+ telemetryKey = std::move(aTelemetryKey), startRoundTrip,
+ result = std::move(result)] {
+ Telemetry::AccumulateTimeDelta(
+ Telemetry::PRINT_BACKGROUND_TASK_ROUND_TRIP_TIME_MS,
+ telemetryKey, startRoundTrip);
+ ResolveOrReject(*promiseHolder->get(), *holder->get(),
+ result);
+ }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+}
+
+// Gets a fresh promise into aResultPromise, that resolves whenever the print
+// background task finishes.
+template <typename T, typename Result, typename... Args>
+nsresult PrintBackgroundTaskPromise(
+ T& aReceiver, JSContext* aCx, dom::Promise** aResultPromise,
+ const nsCString& aTelemetryKey,
+ PrintBackgroundTask<T, Result, Args...> aTask, Args... aArgs) {
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ SpawnPrintBackgroundTask(aReceiver, *promise, aTelemetryKey, aTask,
+ std::forward<Args>(aArgs)...);
+
+ promise.forget(aResultPromise);
+ return NS_OK;
+}
+
+// Resolves an async attribute via a background task, creating and storing a
+// promise as needed in aPromiseSlot.
+template <typename T, typename Result, typename... Args>
+nsresult AsyncPromiseAttributeGetter(
+ T& aReceiver, RefPtr<dom::Promise>& aPromiseSlot, JSContext* aCx,
+ dom::Promise** aResultPromise, const nsCString& aTelemetryKey,
+ PrintBackgroundTask<T, Result, Args...> aTask, Args... aArgs) {
+ if (RefPtr<dom::Promise> existing = aPromiseSlot) {
+ existing.forget(aResultPromise);
+ return NS_OK;
+ }
+
+ nsresult rv =
+ PrintBackgroundTaskPromise(aReceiver, aCx, aResultPromise, aTelemetryKey,
+ aTask, std::forward<Args>(aArgs)...);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aPromiseSlot = *aResultPromise;
+ return NS_OK;
+}
+
+} // namespace mozilla
+
+#endif
diff --git a/widget/PuppetBidiKeyboard.cpp b/widget/PuppetBidiKeyboard.cpp
new file mode 100644
index 0000000000..99ea6f6ca9
--- /dev/null
+++ b/widget/PuppetBidiKeyboard.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 "PuppetBidiKeyboard.h"
+#include "nsIWidget.h"
+
+using namespace mozilla::widget;
+
+NS_IMPL_ISUPPORTS(PuppetBidiKeyboard, nsIBidiKeyboard)
+
+PuppetBidiKeyboard::PuppetBidiKeyboard()
+ : nsIBidiKeyboard(), mIsLangRTL(false), mHaveBidiKeyboards(false) {}
+
+PuppetBidiKeyboard::~PuppetBidiKeyboard() = default;
+
+NS_IMETHODIMP
+PuppetBidiKeyboard::Reset() { return NS_OK; }
+
+NS_IMETHODIMP
+PuppetBidiKeyboard::IsLangRTL(bool* aIsRTL) {
+ *aIsRTL = mIsLangRTL;
+ return NS_OK;
+}
+
+void PuppetBidiKeyboard::SetBidiKeyboardInfo(bool aIsLangRTL,
+ bool aHaveBidiKeyboards) {
+ mIsLangRTL = aIsLangRTL;
+ mHaveBidiKeyboards = aHaveBidiKeyboards;
+}
+
+NS_IMETHODIMP
+PuppetBidiKeyboard::GetHaveBidiKeyboards(bool* aResult) {
+ *aResult = mHaveBidiKeyboards;
+ return NS_OK;
+}
+
+// static
+already_AddRefed<nsIBidiKeyboard>
+nsIWidget::CreateBidiKeyboardContentProcess() {
+ return do_AddRef(new PuppetBidiKeyboard());
+}
diff --git a/widget/PuppetBidiKeyboard.h b/widget/PuppetBidiKeyboard.h
new file mode 100644
index 0000000000..82250cc36e
--- /dev/null
+++ b/widget/PuppetBidiKeyboard.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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/. */
+
+#ifndef mozilla_widget_PuppetBidiKeyboard_h_
+#define mozilla_widget_PuppetBidiKeyboard_h_
+
+#include "nsIBidiKeyboard.h"
+
+namespace mozilla {
+namespace widget {
+
+class PuppetBidiKeyboard final : public nsIBidiKeyboard {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIBIDIKEYBOARD
+
+ PuppetBidiKeyboard();
+
+ void SetBidiKeyboardInfo(bool aIsLangRTL, bool aHaveBidiKeyboards);
+
+ private:
+ ~PuppetBidiKeyboard();
+
+ bool mIsLangRTL;
+ bool mHaveBidiKeyboards;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_PuppetBidiKeyboard_h_
diff --git a/widget/PuppetWidget.cpp b/widget/PuppetWidget.cpp
new file mode 100644
index 0000000000..07b22d9a2b
--- /dev/null
+++ b/widget/PuppetWidget.cpp
@@ -0,0 +1,1179 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 "base/basictypes.h"
+
+#include "gfxPlatform.h"
+#include "nsRefreshDriver.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/layers/APZChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/Unused.h"
+#include "PuppetWidget.h"
+#include "nsContentUtils.h"
+#include "nsIWidgetListener.h"
+#include "imgIContainer.h"
+#include "nsView.h"
+#include "nsXPLookAndFeel.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+
+static void InvalidateRegion(nsIWidget* aWidget,
+ const LayoutDeviceIntRegion& aRegion) {
+ for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) {
+ aWidget->Invalidate(iter.Get());
+ }
+}
+
+/*static*/
+already_AddRefed<nsIWidget> nsIWidget::CreatePuppetWidget(
+ BrowserChild* aBrowserChild) {
+ MOZ_ASSERT(!aBrowserChild || nsIWidget::UsePuppetWidgets(),
+ "PuppetWidgets not allowed in this configuration");
+
+ nsCOMPtr<nsIWidget> widget = new PuppetWidget(aBrowserChild);
+ return widget.forget();
+}
+
+namespace mozilla {
+namespace widget {
+
+static bool IsPopup(const widget::InitData* aInitData) {
+ return aInitData && aInitData->mWindowType == WindowType::Popup;
+}
+
+static bool MightNeedIMEFocus(const widget::InitData* aInitData) {
+ // In the puppet-widget world, popup widgets are just dummies and
+ // shouldn't try to mess with IME state.
+#ifdef MOZ_CROSS_PROCESS_IME
+ return !IsPopup(aInitData);
+#else
+ return false;
+#endif
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(PuppetWidget, nsBaseWidget,
+ TextEventDispatcherListener)
+
+PuppetWidget::PuppetWidget(BrowserChild* aBrowserChild)
+ : mBrowserChild(aBrowserChild),
+ mMemoryPressureObserver(nullptr),
+ mEnabled(false),
+ mVisible(false),
+ mSizeMode(nsSizeMode_Normal),
+ mNeedIMEStateInit(false),
+ mIgnoreCompositionEvents(false) {
+ // Setting 'Unknown' means "not yet cached".
+ mInputContext.mIMEState.mEnabled = IMEEnabled::Unknown;
+}
+
+PuppetWidget::~PuppetWidget() { Destroy(); }
+
+void PuppetWidget::InfallibleCreate(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ MOZ_ASSERT(!aNativeParent, "got a non-Puppet native parent");
+
+ BaseCreate(nullptr, aInitData);
+
+ mBounds = aRect;
+ mEnabled = true;
+ mVisible = true;
+
+ mDrawTarget = gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
+ IntSize(1, 1), SurfaceFormat::B8G8R8A8);
+
+ mNeedIMEStateInit = MightNeedIMEFocus(aInitData);
+
+ PuppetWidget* parent = static_cast<PuppetWidget*>(aParent);
+ if (parent) {
+ parent->SetChild(this);
+ mWindowRenderer = parent->GetWindowRenderer();
+ } else {
+ Resize(mBounds.X(), mBounds.Y(), mBounds.Width(), mBounds.Height(), false);
+ }
+ mMemoryPressureObserver = MemoryPressureObserver::Create(this);
+}
+
+nsresult PuppetWidget::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ InfallibleCreate(aParent, aNativeParent, aRect, aInitData);
+ return NS_OK;
+}
+
+void PuppetWidget::InitIMEState() {
+ MOZ_ASSERT(mBrowserChild);
+ if (mNeedIMEStateInit) {
+ mContentCache.Clear();
+ mBrowserChild->SendUpdateContentCache(mContentCache);
+ mIMENotificationRequestsOfParent = IMENotificationRequests();
+ mNeedIMEStateInit = false;
+ }
+}
+
+already_AddRefed<nsIWidget> PuppetWidget::CreateChild(
+ const LayoutDeviceIntRect& aRect, widget::InitData* aInitData,
+ bool aForceUseIWidgetParent) {
+ bool isPopup = IsPopup(aInitData);
+ nsCOMPtr<nsIWidget> widget = nsIWidget::CreatePuppetWidget(mBrowserChild);
+ return ((widget && NS_SUCCEEDED(widget->Create(isPopup ? nullptr : this,
+ nullptr, aRect, aInitData)))
+ ? widget.forget()
+ : nullptr);
+}
+
+void PuppetWidget::Destroy() {
+ if (mOnDestroyCalled) {
+ return;
+ }
+ mOnDestroyCalled = true;
+
+ Base::OnDestroy();
+ Base::Destroy();
+ if (mMemoryPressureObserver) {
+ mMemoryPressureObserver->Unregister();
+ mMemoryPressureObserver = nullptr;
+ }
+ mChild = nullptr;
+ if (mWindowRenderer) {
+ mWindowRenderer->Destroy();
+ }
+ mWindowRenderer = nullptr;
+ mBrowserChild = nullptr;
+}
+
+void PuppetWidget::Show(bool aState) {
+ NS_ASSERTION(mEnabled,
+ "does it make sense to Show()/Hide() a disabled widget?");
+
+ bool wasVisible = mVisible;
+ mVisible = aState;
+
+ if (mChild) {
+ mChild->mVisible = aState;
+ }
+
+ if (!wasVisible && mVisible) {
+ // The previously attached widget listener is handy if
+ // we're transitioning from page to page without dropping
+ // layers (since we'll continue to show the old layers
+ // associated with that old widget listener). If the
+ // PuppetWidget was hidden, those layers are dropped,
+ // so the previously attached widget listener is really
+ // of no use anymore (and is actually actively harmful - see
+ // bug 1323586).
+ mPreviouslyAttachedWidgetListener = nullptr;
+ Resize(mBounds.Width(), mBounds.Height(), false);
+ Invalidate(mBounds);
+ }
+}
+
+void PuppetWidget::Resize(double aWidth, double aHeight, bool aRepaint) {
+ LayoutDeviceIntRect oldBounds = mBounds;
+ mBounds.SizeTo(
+ LayoutDeviceIntSize(NSToIntRound(aWidth), NSToIntRound(aHeight)));
+
+ if (mChild) {
+ mChild->Resize(aWidth, aHeight, aRepaint);
+ return;
+ }
+
+ // XXX: roc says that |aRepaint| dictates whether or not to
+ // invalidate the expanded area
+ if (oldBounds.Size() < mBounds.Size() && aRepaint) {
+ LayoutDeviceIntRegion dirty(mBounds);
+ dirty.Sub(dirty, oldBounds);
+ InvalidateRegion(this, dirty);
+ }
+
+ // call WindowResized() on both the current listener, and possibly
+ // also the previous one if we're in a state where we're drawing that one
+ // because the current one is paint suppressed
+ if (!oldBounds.IsEqualEdges(mBounds) && mAttachedWidgetListener) {
+ if (GetCurrentWidgetListener() &&
+ GetCurrentWidgetListener() != mAttachedWidgetListener) {
+ GetCurrentWidgetListener()->WindowResized(this, mBounds.Width(),
+ mBounds.Height());
+ }
+ mAttachedWidgetListener->WindowResized(this, mBounds.Width(),
+ mBounds.Height());
+ }
+}
+
+void PuppetWidget::SetFocus(Raise aRaise, CallerType aCallerType) {
+ if (aRaise == Raise::Yes && mBrowserChild) {
+ mBrowserChild->SendRequestFocus(true, aCallerType);
+ }
+}
+
+void PuppetWidget::Invalidate(const LayoutDeviceIntRect& aRect) {
+#ifdef DEBUG
+ debug_DumpInvalidate(stderr, this, &aRect, "PuppetWidget", 0);
+#endif
+
+ if (mChild) {
+ mChild->Invalidate(aRect);
+ return;
+ }
+
+ if (mBrowserChild && !aRect.IsEmpty() && !mWidgetPaintTask.IsPending()) {
+ mWidgetPaintTask = new WidgetPaintTask(this);
+ nsCOMPtr<nsIRunnable> event(mWidgetPaintTask.get());
+ SchedulerGroup::Dispatch(event.forget());
+ }
+}
+
+mozilla::LayoutDeviceToLayoutDeviceMatrix4x4
+PuppetWidget::WidgetToTopLevelWidgetTransform() {
+ if (!GetOwningBrowserChild()) {
+ NS_WARNING("PuppetWidget without Tab does not have transform information.");
+ return mozilla::LayoutDeviceToLayoutDeviceMatrix4x4();
+ }
+ return GetOwningBrowserChild()->GetChildToParentConversionMatrix();
+}
+
+void PuppetWidget::InitEvent(WidgetGUIEvent& aEvent,
+ LayoutDeviceIntPoint* aPoint) {
+ if (nullptr == aPoint) {
+ aEvent.mRefPoint = LayoutDeviceIntPoint(0, 0);
+ } else {
+ // use the point override if provided
+ aEvent.mRefPoint = *aPoint;
+ }
+}
+
+nsresult PuppetWidget::DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) {
+#ifdef DEBUG
+ debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "PuppetWidget", 0);
+#endif
+
+ MOZ_ASSERT(!mChild || mChild->mWindowType == WindowType::Popup,
+ "Unexpected event dispatch!");
+
+ MOZ_ASSERT(!aEvent->AsKeyboardEvent() ||
+ aEvent->mFlags.mIsSynthesizedForTests ||
+ aEvent->AsKeyboardEvent()->AreAllEditCommandsInitialized(),
+ "Non-sysnthesized keyboard events should have edit commands for "
+ "all types "
+ "before dispatched");
+
+ if (aEvent->mClass == eCompositionEventClass) {
+ // If we've already requested to commit/cancel the latest composition,
+ // TextComposition for the old composition has been destroyed. Then,
+ // the DOM tree needs to listen to next eCompositionStart and its
+ // following events. So, until we meet new eCompositionStart, let's
+ // discard all unnecessary composition events here.
+ if (mIgnoreCompositionEvents) {
+ if (aEvent->mMessage != eCompositionStart) {
+ aStatus = nsEventStatus_eIgnore;
+ return NS_OK;
+ }
+ // Now, we receive new eCompositionStart. Let's restart to handle
+ // composition in this process.
+ mIgnoreCompositionEvents = false;
+ }
+ // Store the latest native IME context of parent process's widget or
+ // TextEventDispatcher if it's in this process.
+ WidgetCompositionEvent* compositionEvent = aEvent->AsCompositionEvent();
+#ifdef DEBUG
+ if (mNativeIMEContext.IsValid() &&
+ mNativeIMEContext != compositionEvent->mNativeIMEContext) {
+ RefPtr<TextComposition> composition =
+ IMEStateManager::GetTextCompositionFor(this);
+ MOZ_ASSERT(
+ !composition,
+ "When there is composition caused by old native IME context, "
+ "composition events caused by different native IME context are not "
+ "allowed");
+ }
+#endif // #ifdef DEBUG
+ mNativeIMEContext = compositionEvent->mNativeIMEContext;
+ mContentCache.OnCompositionEvent(*compositionEvent);
+ }
+
+ // If the event is a composition event or a keyboard event, it should be
+ // dispatched with TextEventDispatcher if we could do that with current
+ // design. However, we cannot do that without big changes and the behavior
+ // is not so complicated for now. Therefore, we should just notify it
+ // of dispatching events and TextEventDispatcher should emulate the state
+ // with events here.
+ if (aEvent->mClass == eCompositionEventClass ||
+ aEvent->mClass == eKeyboardEventClass) {
+ TextEventDispatcher* dispatcher = GetTextEventDispatcher();
+ // However, if the event is being dispatched by the text event dispatcher
+ // or, there is native text event dispatcher listener, that means that
+ // native text input event handler is in this process like on Android,
+ // and the event is not synthesized for tests, the event is coming from
+ // the TextEventDispatcher. In these cases, we shouldn't notify
+ // TextEventDispatcher of dispatching the event.
+ if (!dispatcher->IsDispatchingEvent() &&
+ !(mNativeTextEventDispatcherListener &&
+ !aEvent->mFlags.mIsSynthesizedForTests)) {
+ DebugOnly<nsresult> rv =
+ dispatcher->BeginInputTransactionFor(aEvent, this);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "The text event dispatcher should always succeed to start input "
+ "transaction for the event");
+ }
+ }
+
+ aStatus = nsEventStatus_eIgnore;
+
+ if (GetCurrentWidgetListener()) {
+ aStatus =
+ GetCurrentWidgetListener()->HandleEvent(aEvent, mUseAttachedEvents);
+ }
+
+ return NS_OK;
+}
+
+nsIWidget::ContentAndAPZEventStatus PuppetWidget::DispatchInputEvent(
+ WidgetInputEvent* aEvent) {
+ ContentAndAPZEventStatus status;
+ if (!AsyncPanZoomEnabled()) {
+ DispatchEvent(aEvent, status.mContentStatus);
+ return status;
+ }
+
+ if (!mBrowserChild) {
+ return status;
+ }
+
+ switch (aEvent->mClass) {
+ case eWheelEventClass:
+ Unused << mBrowserChild->SendDispatchWheelEvent(*aEvent->AsWheelEvent());
+ break;
+ case eMouseEventClass:
+ Unused << mBrowserChild->SendDispatchMouseEvent(*aEvent->AsMouseEvent());
+ break;
+ case eKeyboardEventClass:
+ Unused << mBrowserChild->SendDispatchKeyboardEvent(
+ *aEvent->AsKeyboardEvent());
+ break;
+ case eTouchEventClass:
+ Unused << mBrowserChild->SendDispatchTouchEvent(*aEvent->AsTouchEvent());
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unsupported event type");
+ }
+
+ return status;
+}
+
+nsresult PuppetWidget::SynthesizeNativeKeyEvent(
+ int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "keyevent");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeKeyEvent(
+ aNativeKeyboardLayout, aNativeKeyCode, aModifierFlags, aCharacters,
+ aUnmodifiedCharacters, notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeMouseEvent(
+ mozilla::LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeMouseEvent(
+ aPoint, static_cast<uint32_t>(aNativeMessage),
+ static_cast<int16_t>(aButton), static_cast<uint32_t>(aModifierFlags),
+ notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeMouseMove(
+ mozilla::LayoutDeviceIntPoint aPoint, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mousemove");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeMouseMove(aPoint, notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeMouseScrollEvent(
+ mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage,
+ double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeMouseScrollEvent(
+ aPoint, aNativeMessage, aDeltaX, aDeltaY, aDeltaZ, aModifierFlags,
+ aAdditionalFlags, notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeTouchPoint(
+ uint32_t aPointerId, TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPointerPressure,
+ uint32_t aPointerOrientation, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpoint");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeTouchPoint(
+ aPointerId, aPointerState, aPoint, aPointerPressure, aPointerOrientation,
+ notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint,
+ int32_t aModifierFlags) {
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeTouchPadPinch(aEventPhase, aScale, aPoint,
+ aModifierFlags);
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeTouchTap(LayoutDeviceIntPoint aPoint,
+ bool aLongTap,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchtap");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeTouchTap(aPoint, aLongTap,
+ notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::ClearNativeTouchSequence(nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "cleartouch");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendClearNativeTouchSequence(notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativePenInput(
+ uint32_t aPointerId, TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPressure, uint32_t aRotation,
+ int32_t aTiltX, int32_t aTiltY, int32_t aButton, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "peninput");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativePenInput(
+ aPointerId, aPointerState, aPoint, aPressure, aRotation, aTiltX, aTiltY,
+ aButton, notifier.SaveObserver());
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeTouchpadDoubleTap(
+ LayoutDeviceIntPoint aPoint, uint32_t aModifierFlags) {
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeTouchpadDoubleTap(aPoint, aModifierFlags);
+ return NS_OK;
+}
+
+nsresult PuppetWidget::SynthesizeNativeTouchpadPan(
+ TouchpadGesturePhase aEventPhase, LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY, int32_t aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpadpanevent");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendSynthesizeNativeTouchpadPan(aEventPhase, aPoint, aDeltaX,
+ aDeltaY, aModifierFlags,
+ notifier.SaveObserver());
+ return NS_OK;
+}
+
+void PuppetWidget::LockNativePointer() {
+ if (!mBrowserChild) {
+ return;
+ }
+ mBrowserChild->SendLockNativePointer();
+}
+
+void PuppetWidget::UnlockNativePointer() {
+ if (!mBrowserChild) {
+ return;
+ }
+ mBrowserChild->SendUnlockNativePointer();
+}
+
+void PuppetWidget::SetConfirmedTargetAPZC(
+ uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) const {
+ if (mBrowserChild) {
+ mBrowserChild->SetTargetAPZC(aInputBlockId, aTargets);
+ }
+}
+
+void PuppetWidget::UpdateZoomConstraints(
+ const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
+ const Maybe<ZoomConstraints>& aConstraints) {
+ if (mBrowserChild) {
+ mBrowserChild->DoUpdateZoomConstraints(aPresShellId, aViewId, aConstraints);
+ }
+}
+
+bool PuppetWidget::AsyncPanZoomEnabled() const {
+ return mBrowserChild && mBrowserChild->AsyncPanZoomEnabled();
+}
+
+bool PuppetWidget::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+ if (NS_WARN_IF(!mBrowserChild)) {
+ return false;
+ }
+ mBrowserChild->RequestEditCommands(aType, aEvent, aCommands);
+ return true;
+}
+
+WindowRenderer* PuppetWidget::GetWindowRenderer() {
+ if (!mWindowRenderer) {
+ if (XRE_IsParentProcess()) {
+ // On the parent process there is no CompositorBridgeChild which confuses
+ // some layers code, so we use basic layers instead. Note that we create
+ mWindowRenderer = new FallbackRenderer;
+ return mWindowRenderer;
+ }
+
+ // If we know for sure that the parent side of this BrowserChild is not
+ // connected to the compositor, we don't want to use a "remote" layer
+ // manager like WebRender or Client. Instead we use a Basic one which
+ // can do drawing in this process.
+ MOZ_ASSERT(!mBrowserChild ||
+ mBrowserChild->IsLayersConnected() != Some(true));
+ mWindowRenderer = CreateFallbackRenderer();
+ }
+
+ return mWindowRenderer;
+}
+
+bool PuppetWidget::CreateRemoteLayerManager(
+ const std::function<bool(WebRenderLayerManager*)>& aInitializeFunc) {
+ RefPtr<WebRenderLayerManager> lm = new WebRenderLayerManager(this);
+ MOZ_ASSERT(mBrowserChild);
+
+ if (!aInitializeFunc(lm)) {
+ return false;
+ }
+
+ // Force the old LM to self destruct, otherwise if the reference dangles we
+ // could fail to revoke the most recent transaction. We only want to replace
+ // it if we successfully create its successor because a partially initialized
+ // layer manager is worse than a fully initialized but shutdown layer manager.
+ DestroyLayerManager();
+ mWindowRenderer = std::move(lm);
+ return true;
+}
+
+nsresult PuppetWidget::RequestIMEToCommitComposition(bool aCancel) {
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(!Destroyed());
+
+ // There must not be composition which is caused by the PuppetWidget instance.
+ if (NS_WARN_IF(!mNativeIMEContext.IsValid())) {
+ return NS_OK;
+ }
+
+ // We've already requested to commit/cancel composition.
+ if (NS_WARN_IF(mIgnoreCompositionEvents)) {
+#ifdef DEBUG
+ RefPtr<TextComposition> composition =
+ IMEStateManager::GetTextCompositionFor(this);
+ MOZ_ASSERT(!composition);
+#endif // #ifdef DEBUG
+ return NS_OK;
+ }
+
+ RefPtr<TextComposition> composition =
+ IMEStateManager::GetTextCompositionFor(this);
+ // This method shouldn't be called when there is no text composition instance.
+ if (NS_WARN_IF(!composition)) {
+ return NS_OK;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ composition->IsRequestingCommitOrCancelComposition(),
+ "Requesting commit or cancel composition should be requested via "
+ "TextComposition instance");
+
+ bool isCommitted = false;
+ nsAutoString committedString;
+ if (NS_WARN_IF(!mBrowserChild->SendRequestIMEToCommitComposition(
+ aCancel, composition->Id(), &isCommitted, &committedString))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If the composition wasn't committed synchronously, we need to wait async
+ // composition events for destroying the TextComposition instance.
+ if (!isCommitted) {
+ return NS_OK;
+ }
+
+ // Dispatch eCompositionCommit event.
+ WidgetCompositionEvent compositionCommitEvent(true, eCompositionCommit, this);
+ InitEvent(compositionCommitEvent, nullptr);
+ compositionCommitEvent.mData = committedString;
+ nsEventStatus status = nsEventStatus_eIgnore;
+ DispatchEvent(&compositionCommitEvent, status);
+
+#ifdef DEBUG
+ RefPtr<TextComposition> currentComposition =
+ IMEStateManager::GetTextCompositionFor(this);
+ MOZ_ASSERT(!currentComposition);
+#endif // #ifdef DEBUG
+
+ // Ignore the following composition events until we receive new
+ // eCompositionStart event.
+ mIgnoreCompositionEvents = true;
+
+ Unused << mBrowserChild->SendOnEventNeedingAckHandled(
+ eCompositionCommitRequestHandled, composition->Id());
+
+ // NOTE: PuppetWidget might be destroyed already.
+ return NS_OK;
+}
+
+// When this widget caches input context and currently managed by
+// IMEStateManager, the cache is valid.
+bool PuppetWidget::HaveValidInputContextCache() const {
+ return (mInputContext.mIMEState.mEnabled != IMEEnabled::Unknown &&
+ IMEStateManager::GetWidgetForActiveInputContext() == this);
+}
+
+nsRefreshDriver* PuppetWidget::GetTopLevelRefreshDriver() const {
+ if (!mBrowserChild) {
+ return nullptr;
+ }
+
+ if (PresShell* presShell = mBrowserChild->GetTopLevelPresShell()) {
+ return presShell->GetRefreshDriver();
+ }
+
+ return nullptr;
+}
+
+void PuppetWidget::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ mInputContext = aContext;
+ // Any widget instances cannot cache IME open state because IME open state
+ // can be changed by user but native IME may not notify us of changing the
+ // open state on some platforms.
+ mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
+ if (!mBrowserChild) {
+ return;
+ }
+ mBrowserChild->SendSetInputContext(aContext, aAction);
+}
+
+InputContext PuppetWidget::GetInputContext() {
+ // XXX Currently, we don't support retrieving IME open state from child
+ // process.
+
+ // If the cache of input context is valid, we can avoid to use synchronous
+ // IPC.
+ if (HaveValidInputContextCache()) {
+ return mInputContext;
+ }
+
+ NS_WARNING("PuppetWidget::GetInputContext() needs to retrieve it with IPC");
+
+ // Don't cache InputContext here because this process isn't managing IME
+ // state of the chrome widget. So, we cannot modify mInputContext when
+ // chrome widget is set to new context.
+ InputContext context;
+ if (mBrowserChild) {
+ mBrowserChild->SendGetInputContext(&context.mIMEState);
+ }
+ return context;
+}
+
+NativeIMEContext PuppetWidget::GetNativeIMEContext() {
+ return mNativeIMEContext;
+}
+
+nsresult PuppetWidget::NotifyIMEOfFocusChange(
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(IMEStateManager::CanSendNotificationToWidget());
+
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool gotFocus = aIMENotification.mMessage == NOTIFY_IME_OF_FOCUS;
+ if (gotFocus) {
+ // When IME gets focus, we should initialize all information of the
+ // content, however, it may fail to get it because the editor may have
+ // already been blurred.
+ if (NS_WARN_IF(!mContentCache.CacheAll(this, &aIMENotification))) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ // When IME loses focus, we don't need to store anything.
+ mContentCache.Clear();
+ }
+
+ mIMENotificationRequestsOfParent =
+ IMENotificationRequests(IMENotificationRequests::NOTIFY_ALL);
+ RefPtr<PuppetWidget> self = this;
+ mBrowserChild->SendNotifyIMEFocus(mContentCache, aIMENotification)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self](IMENotificationRequests&& aRequests) {
+ self->mIMENotificationRequestsOfParent = aRequests;
+ if (TextEventDispatcher* dispatcher =
+ self->GetTextEventDispatcher()) {
+ dispatcher->OnWidgetChangeIMENotificationRequests(self);
+ }
+ },
+ [self](mozilla::ipc::ResponseRejectReason&& aReason) {
+ NS_WARNING("SendNotifyIMEFocus got rejected.");
+ });
+
+ return NS_OK;
+}
+
+nsresult PuppetWidget::NotifyIMEOfCompositionUpdate(
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(IMEStateManager::CanSendNotificationToWidget());
+
+ if (NS_WARN_IF(!mBrowserChild)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(
+ !mContentCache.CacheCaretAndTextRects(this, &aIMENotification))) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendNotifyIMECompositionUpdate(mContentCache,
+ aIMENotification);
+ return NS_OK;
+}
+
+nsresult PuppetWidget::NotifyIMEOfTextChange(
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(IMEStateManager::CanSendNotificationToWidget());
+ MOZ_ASSERT(aIMENotification.mMessage == NOTIFY_IME_OF_TEXT_CHANGE,
+ "Passed wrong notification");
+
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // FYI: text change notification is the first notification after
+ // a user operation changes the content. So, we need to modify
+ // the cache as far as possible here.
+
+ if (NS_WARN_IF(!mContentCache.CacheText(this, &aIMENotification))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // BrowserParent doesn't this this to cache. we don't send the notification
+ // if parent process doesn't request NOTIFY_TEXT_CHANGE.
+ if (mIMENotificationRequestsOfParent.WantTextChange()) {
+ mBrowserChild->SendNotifyIMETextChange(mContentCache, aIMENotification);
+ } else {
+ mBrowserChild->SendUpdateContentCache(mContentCache);
+ }
+ return NS_OK;
+}
+
+nsresult PuppetWidget::NotifyIMEOfSelectionChange(
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(IMEStateManager::CanSendNotificationToWidget());
+ MOZ_ASSERT(aIMENotification.mMessage == NOTIFY_IME_OF_SELECTION_CHANGE,
+ "Passed wrong notification");
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Note that selection change must be notified after text change if it occurs.
+ // Therefore, we don't need to query text content again here.
+ if (MOZ_UNLIKELY(!mContentCache.SetSelection(
+ this, aIMENotification.mSelectionChangeData))) {
+ // If there is no text cache yet, caching text will cache selection too.
+ // Therefore, in the case, we don't need to notify IME of selection change
+ // right now.
+ return NS_OK;
+ }
+
+ mBrowserChild->SendNotifyIMESelection(mContentCache, aIMENotification);
+
+ return NS_OK;
+}
+
+nsresult PuppetWidget::NotifyIMEOfMouseButtonEvent(
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(IMEStateManager::CanSendNotificationToWidget());
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool consumedByIME = false;
+ if (!mBrowserChild->SendNotifyIMEMouseButtonEvent(aIMENotification,
+ &consumedByIME)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return consumedByIME ? NS_SUCCESS_EVENT_CONSUMED : NS_OK;
+}
+
+nsresult PuppetWidget::NotifyIMEOfPositionChange(
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(IMEStateManager::CanSendNotificationToWidget());
+ if (NS_WARN_IF(!mBrowserChild)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!mContentCache.CacheEditorRect(this, &aIMENotification))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_WARN_IF(
+ !mContentCache.CacheCaretAndTextRects(this, &aIMENotification))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (mIMENotificationRequestsOfParent.WantPositionChanged()) {
+ mBrowserChild->SendNotifyIMEPositionChange(mContentCache, aIMENotification);
+ } else {
+ mBrowserChild->SendUpdateContentCache(mContentCache);
+ }
+ return NS_OK;
+}
+
+struct CursorSurface {
+ UniquePtr<char[]> mData;
+ IntSize mSize;
+};
+
+void PuppetWidget::SetCursor(const Cursor& aCursor) {
+ if (!mBrowserChild) {
+ return;
+ }
+
+ const bool force = mUpdateCursor;
+ if (!force && mCursor == aCursor) {
+ return;
+ }
+
+ bool hasCustomCursor = false;
+ Maybe<mozilla::ipc::BigBuffer> customCursorData;
+ size_t length = 0;
+ IntSize customCursorSize;
+ int32_t stride = 0;
+ auto format = SurfaceFormat::B8G8R8A8;
+ ImageResolution resolution = aCursor.mResolution;
+ if (aCursor.IsCustom()) {
+ int32_t width = 0, height = 0;
+ aCursor.mContainer->GetWidth(&width);
+ aCursor.mContainer->GetHeight(&height);
+ const int32_t flags =
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY;
+ RefPtr<SourceSurface> surface;
+ if (width && height &&
+ aCursor.mContainer->GetType() == imgIContainer::TYPE_VECTOR) {
+ // For vector images, scale to device pixels.
+ resolution.ScaleBy(GetDefaultScale().scale);
+ resolution.ApplyInverseTo(width, height);
+ surface = aCursor.mContainer->GetFrameAtSize(
+ {width, height}, imgIContainer::FRAME_CURRENT, flags);
+ } else {
+ // NOTE(emilio): We get the frame at the full size, ignoring resolution,
+ // because we're going to rasterize it, and we'd effectively lose the
+ // extra pixels if we rasterized to CustomCursorSize.
+ surface =
+ aCursor.mContainer->GetFrame(imgIContainer::FRAME_CURRENT, flags);
+ }
+ if (surface) {
+ if (RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface()) {
+ hasCustomCursor = true;
+ customCursorData =
+ nsContentUtils::GetSurfaceData(*dataSurface, &length, &stride);
+ customCursorSize = dataSurface->GetSize();
+ format = dataSurface->GetFormat();
+ }
+ }
+ }
+
+ if (!mBrowserChild->SendSetCursor(
+ aCursor.mDefaultCursor, hasCustomCursor, std::move(customCursorData),
+ customCursorSize.width, customCursorSize.height, resolution.mX,
+ resolution.mY, stride, format, aCursor.mHotspotX, aCursor.mHotspotY,
+ force)) {
+ return;
+ }
+ mCursor = aCursor;
+ mUpdateCursor = false;
+}
+
+void PuppetWidget::SetChild(PuppetWidget* aChild) {
+ MOZ_ASSERT(this != aChild, "can't parent a widget to itself");
+ MOZ_ASSERT(!aChild->mChild,
+ "fake widget 'hierarchy' only expected to have one level");
+
+ mChild = aChild;
+}
+
+NS_IMETHODIMP
+PuppetWidget::WidgetPaintTask::Run() {
+ if (mWidget) {
+ mWidget->Paint();
+ }
+ return NS_OK;
+}
+
+void PuppetWidget::Paint() {
+ if (!GetCurrentWidgetListener()) return;
+
+ mWidgetPaintTask.Revoke();
+
+ RefPtr<PuppetWidget> strongThis(this);
+
+ GetCurrentWidgetListener()->WillPaintWindow(this);
+
+ if (GetCurrentWidgetListener()) {
+ GetCurrentWidgetListener()->DidPaintWindow();
+ }
+}
+
+void PuppetWidget::PaintNowIfNeeded() {
+ if (IsVisible() && mWidgetPaintTask.IsPending()) {
+ Paint();
+ }
+}
+
+void PuppetWidget::OnMemoryPressure(layers::MemoryPressureReason aWhy) {
+ if (aWhy != MemoryPressureReason::LOW_MEMORY_ONGOING && !mVisible &&
+ mWindowRenderer && mWindowRenderer->AsWebRender() &&
+ XRE_IsContentProcess()) {
+ mWindowRenderer->AsWebRender()->ClearCachedResources();
+ }
+}
+
+bool PuppetWidget::NeedsPaint() {
+ // e10s popups are handled by the parent process, so never should be painted
+ // here
+ return mVisible;
+}
+
+LayoutDeviceIntPoint PuppetWidget::GetChromeOffset() {
+ if (!GetOwningBrowserChild()) {
+ NS_WARNING("PuppetWidget without Tab does not have chrome information.");
+ return LayoutDeviceIntPoint();
+ }
+ return GetOwningBrowserChild()->GetChromeOffset();
+}
+
+LayoutDeviceIntPoint PuppetWidget::WidgetToScreenOffset() {
+ return GetWindowPosition() + WidgetToTopLevelWidgetOffset();
+}
+
+LayoutDeviceIntPoint PuppetWidget::GetWindowPosition() {
+ if (!GetOwningBrowserChild()) {
+ return LayoutDeviceIntPoint();
+ }
+
+ int32_t winX, winY, winW, winH;
+ NS_ENSURE_SUCCESS(GetOwningBrowserChild()->GetDimensions(
+ DimensionKind::Outer, &winX, &winY, &winW, &winH),
+ LayoutDeviceIntPoint());
+ return LayoutDeviceIntPoint(winX, winY) +
+ GetOwningBrowserChild()->GetClientOffset();
+}
+
+LayoutDeviceIntRect PuppetWidget::GetScreenBounds() {
+ return LayoutDeviceIntRect(WidgetToScreenOffset(), mBounds.Size());
+}
+
+uint32_t PuppetWidget::GetMaxTouchPoints() const {
+ return mBrowserChild ? mBrowserChild->MaxTouchPoints() : 0;
+}
+
+void PuppetWidget::StartAsyncScrollbarDrag(
+ const AsyncDragMetrics& aDragMetrics) {
+ mBrowserChild->StartScrollbarDrag(aDragMetrics);
+}
+
+ScreenIntMargin PuppetWidget::GetSafeAreaInsets() const {
+ return mSafeAreaInsets;
+}
+
+void PuppetWidget::UpdateSafeAreaInsets(
+ const ScreenIntMargin& aSafeAreaInsets) {
+ mSafeAreaInsets = aSafeAreaInsets;
+}
+
+nsIWidgetListener* PuppetWidget::GetCurrentWidgetListener() {
+ if (!mPreviouslyAttachedWidgetListener || !mAttachedWidgetListener) {
+ return mAttachedWidgetListener;
+ }
+
+ if (mAttachedWidgetListener->GetView()->IsPrimaryFramePaintSuppressed()) {
+ return mPreviouslyAttachedWidgetListener;
+ }
+
+ return mAttachedWidgetListener;
+}
+
+void PuppetWidget::ZoomToRect(const uint32_t& aPresShellId,
+ const ScrollableLayerGuid::ViewID& aViewId,
+ const CSSRect& aRect, const uint32_t& aFlags) {
+ if (!mBrowserChild) {
+ return;
+ }
+
+ mBrowserChild->ZoomToRect(aPresShellId, aViewId, aRect, aFlags);
+}
+
+void PuppetWidget::LookUpDictionary(
+ const nsAString& aText, const nsTArray<mozilla::FontRange>& aFontRangeArray,
+ const bool aIsVertical, const LayoutDeviceIntPoint& aPoint) {
+ if (!mBrowserChild) {
+ return;
+ }
+
+ mBrowserChild->SendLookUpDictionary(aText, aFontRangeArray, aIsVertical,
+ aPoint);
+}
+
+bool PuppetWidget::HasPendingInputEvent() {
+ if (!mBrowserChild) {
+ return false;
+ }
+
+ bool ret = false;
+
+ mBrowserChild->GetIPCChannel()->PeekMessages(
+ [&ret](const IPC::Message& aMsg) -> bool {
+ if (nsContentUtils::IsMessageInputEvent(aMsg)) {
+ ret = true;
+ return false; // Stop peeking.
+ }
+ return true;
+ });
+
+ return ret;
+}
+
+// TextEventDispatcherListener
+
+NS_IMETHODIMP
+PuppetWidget::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(aTextEventDispatcher == mTextEventDispatcher);
+
+ // If there is different text event dispatcher listener for handling
+ // text event dispatcher, that means that native keyboard events and
+ // IME events are handled in this process. Therefore, we don't need
+ // to send any requests and notifications to the parent process.
+ if (mNativeTextEventDispatcherListener) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ switch (aIMENotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ return RequestIMEToCommitComposition(false);
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ return RequestIMEToCommitComposition(true);
+ case NOTIFY_IME_OF_FOCUS:
+ case NOTIFY_IME_OF_BLUR:
+ return NotifyIMEOfFocusChange(aIMENotification);
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ return NotifyIMEOfSelectionChange(aIMENotification);
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ return NotifyIMEOfTextChange(aIMENotification);
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ return NotifyIMEOfCompositionUpdate(aIMENotification);
+ case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ return NotifyIMEOfMouseButtonEvent(aIMENotification);
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ return NotifyIMEOfPositionChange(aIMENotification);
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+NS_IMETHODIMP_(IMENotificationRequests)
+PuppetWidget::GetIMENotificationRequests() {
+ return IMENotificationRequests(
+ mIMENotificationRequestsOfParent.mWantUpdates |
+ IMENotificationRequests::NOTIFY_TEXT_CHANGE |
+ IMENotificationRequests::NOTIFY_POSITION_CHANGE);
+}
+
+NS_IMETHODIMP_(void)
+PuppetWidget::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
+ MOZ_ASSERT(aTextEventDispatcher == mTextEventDispatcher);
+}
+
+NS_IMETHODIMP_(void)
+PuppetWidget::WillDispatchKeyboardEvent(
+ TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
+ void* aData) {
+ MOZ_ASSERT(aTextEventDispatcher == mTextEventDispatcher);
+}
+
+nsresult PuppetWidget::SetSystemFont(const nsCString& aFontName) {
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mBrowserChild->SendSetSystemFont(aFontName);
+ return NS_OK;
+}
+
+nsresult PuppetWidget::GetSystemFont(nsCString& aFontName) {
+ if (!mBrowserChild) {
+ return NS_ERROR_FAILURE;
+ }
+ mBrowserChild->SendGetSystemFont(&aFontName);
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/PuppetWidget.h b/widget/PuppetWidget.h
new file mode 100644
index 0000000000..65050bb455
--- /dev/null
+++ b/widget/PuppetWidget.h
@@ -0,0 +1,406 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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/. */
+
+/**
+ * This "puppet widget" isn't really a platform widget. It's intended
+ * to be used in widgetless rendering contexts, such as sandboxed
+ * content processes. If any "real" widgetry is needed, the request
+ * is forwarded to and/or data received from elsewhere.
+ */
+
+#ifndef mozilla_widget_PuppetWidget_h__
+#define mozilla_widget_PuppetWidget_h__
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/RefPtr.h"
+#include "nsBaseWidget.h"
+#include "nsCOMArray.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ContentCache.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "mozilla/layers/MemoryPressureObserver.h"
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+namespace dom {
+class BrowserChild;
+} // namespace dom
+
+namespace layers {
+class WebRenderLayerManager;
+} // namespace layers
+
+namespace widget {
+
+struct AutoCacheNativeKeyCommands;
+
+class PuppetWidget : public nsBaseWidget,
+ public TextEventDispatcherListener,
+ public layers::MemoryPressureListener {
+ typedef mozilla::CSSRect CSSRect;
+ typedef mozilla::dom::BrowserChild BrowserChild;
+ typedef mozilla::gfx::DrawTarget DrawTarget;
+ typedef mozilla::layers::WebRenderLayerManager WebRenderLayerManager;
+
+ // Avoiding to make compiler confused between mozilla::widget and nsIWidget.
+ typedef mozilla::widget::TextEventDispatcher TextEventDispatcher;
+ typedef mozilla::widget::TextEventDispatcherListener
+ TextEventDispatcherListener;
+
+ typedef nsBaseWidget Base;
+
+ // The width and height of the "widget" are clamped to this.
+ public:
+ explicit PuppetWidget(BrowserChild* aBrowserChild);
+
+ protected:
+ virtual ~PuppetWidget();
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // PuppetWidget creation is infallible, hence InfallibleCreate(), which
+ // Create() calls.
+ using nsBaseWidget::Create; // for Create signature not overridden here
+ virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData = nullptr) override;
+ void InfallibleCreate(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData = nullptr);
+
+ void InitIMEState();
+
+ virtual already_AddRefed<nsIWidget> CreateChild(
+ const LayoutDeviceIntRect& aRect, widget::InitData* aInitData = nullptr,
+ bool aForceUseIWidgetParent = false) override;
+
+ virtual void Destroy() override;
+
+ virtual void Show(bool aState) override;
+
+ virtual bool IsVisible() const override { return mVisible; }
+
+ // Widget position is controlled by the parent process via BrowserChild.
+ virtual void Move(double aX, double aY) override {}
+
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override {
+ if (!mBounds.IsEqualXY(aX, aY)) {
+ NotifyWindowMoved(aX, aY);
+ }
+ mBounds.MoveTo(aX, aY);
+ return Resize(aWidth, aHeight, aRepaint);
+ }
+
+ // XXX/cjones: copying gtk behavior here; unclear what disabling a
+ // widget is supposed to entail
+ virtual void Enable(bool aState) override { mEnabled = aState; }
+ virtual bool IsEnabled() const override { return mEnabled; }
+
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override { mSizeMode = aMode; }
+
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override;
+
+ // PuppetWidgets don't have native data, as they're purely nonnative.
+ virtual void* GetNativeData(uint32_t aDataType) override { return nullptr; }
+
+ // PuppetWidgets don't have any concept of titles.
+ virtual nsresult SetTitle(const nsAString& aTitle) override {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ virtual mozilla::LayoutDeviceToLayoutDeviceMatrix4x4
+ WidgetToTopLevelWidgetTransform() override;
+
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+
+ virtual LayoutDeviceIntPoint TopLevelWidgetToScreenOffset() override {
+ return GetWindowPosition();
+ }
+
+ int32_t RoundsWidgetCoordinatesTo() override { return mRounding; }
+
+ void InitEvent(WidgetGUIEvent& aEvent,
+ LayoutDeviceIntPoint* aPoint = nullptr);
+
+ virtual nsresult DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+ ContentAndAPZEventStatus DispatchInputEvent(
+ WidgetInputEvent* aEvent) override;
+ void SetConfirmedTargetAPZC(
+ uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) const override;
+ void UpdateZoomConstraints(
+ const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
+ const mozilla::Maybe<ZoomConstraints>& aConstraints) override;
+ bool AsyncPanZoomEnabled() const override;
+
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
+ nsTArray<mozilla::CommandInt>& aCommands) override;
+
+ friend struct AutoCacheNativeKeyCommands;
+
+ //
+ // nsBaseWidget methods we override
+ //
+
+ // Documents loaded in child processes are always subdocuments of
+ // other docs in an ancestor process. To ensure that the
+ // backgrounds of those documents are painted like those of
+ // same-process subdocuments, we force the widget here to be
+ // transparent, which in turn will cause layout to use a transparent
+ // backstop background color.
+ virtual TransparencyMode GetTransparencyMode() override {
+ return TransparencyMode::Transparent;
+ }
+
+ virtual WindowRenderer* GetWindowRenderer() override;
+
+ // This is used for creating remote layer managers and for re-creating
+ // them after a compositor reset. The lambda aInitializeFunc is used to
+ // perform any caller-required initialization for the newly created layer
+ // manager; in the event of a failure, return false and it will destroy the
+ // new layer manager without changing the state of the widget.
+ bool CreateRemoteLayerManager(
+ const std::function<bool(WebRenderLayerManager*)>& aInitializeFunc);
+
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ virtual InputContext GetInputContext() override;
+ virtual NativeIMEContext GetNativeIMEContext() override;
+ TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override {
+ return mNativeTextEventDispatcherListener
+ ? mNativeTextEventDispatcherListener.get()
+ : this;
+ }
+ void SetNativeTextEventDispatcherListener(
+ TextEventDispatcherListener* aListener) {
+ mNativeTextEventDispatcherListener = aListener;
+ }
+
+ virtual void SetCursor(const Cursor&) override;
+
+ float GetDPI() override { return mDPI; }
+ double GetDefaultScaleInternal() override { return mDefaultScale; }
+
+ virtual bool NeedsPaint() override;
+
+ // Paint the widget immediately if any paints are queued up.
+ void PaintNowIfNeeded();
+
+ virtual BrowserChild* GetOwningBrowserChild() override {
+ return mBrowserChild;
+ }
+
+ void UpdateBackingScaleCache(float aDpi, int32_t aRounding, double aScale) {
+ mDPI = aDpi;
+ mRounding = aRounding;
+ mDefaultScale = aScale;
+ }
+
+ // safe area insets support
+ virtual ScreenIntMargin GetSafeAreaInsets() const override;
+ void UpdateSafeAreaInsets(const ScreenIntMargin& aSafeAreaInsets);
+
+ // Get the offset to the chrome of the window that this tab belongs to.
+ //
+ // NOTE: In OOP iframes this value is zero. You should use
+ // WidgetToTopLevelWidgetTransform instead which is already including the
+ // chrome offset.
+ LayoutDeviceIntPoint GetChromeOffset();
+
+ // Get the screen position of the application window.
+ LayoutDeviceIntPoint GetWindowPosition();
+
+ virtual LayoutDeviceIntRect GetScreenBounds() override;
+
+ virtual nsresult SynthesizeNativeKeyEvent(
+ int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters, nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale,
+ LayoutDeviceIntPoint aPoint, int32_t aModifierFlags) override;
+ virtual nsresult SynthesizeNativeTouchTap(LayoutDeviceIntPoint aPoint,
+ bool aLongTap,
+ nsIObserver* aObserver) override;
+ virtual nsresult ClearNativeTouchSequence(nsIObserver* aObserver) override;
+ virtual uint32_t GetMaxTouchPoints() const override;
+ virtual nsresult SynthesizeNativePenInput(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPressure,
+ uint32_t aRotation, int32_t aTiltX,
+ int32_t aTiltY, int32_t aButton,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeTouchpadDoubleTap(
+ LayoutDeviceIntPoint aPoint, uint32_t aModifierFlags) override;
+
+ virtual nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ virtual void LockNativePointer() override;
+ virtual void UnlockNativePointer() override;
+
+ virtual void StartAsyncScrollbarDrag(
+ const AsyncDragMetrics& aDragMetrics) override;
+
+ virtual void ZoomToRect(const uint32_t& aPresShellId,
+ const ScrollableLayerGuid::ViewID& aViewId,
+ const CSSRect& aRect,
+ const uint32_t& aFlags) override;
+
+ virtual bool HasPendingInputEvent() override;
+
+ virtual void LookUpDictionary(
+ const nsAString& aText,
+ const nsTArray<mozilla::FontRange>& aFontRangeArray,
+ const bool aIsVertical, const LayoutDeviceIntPoint& aPoint) override;
+
+ nsresult SetSystemFont(const nsCString& aFontName) override;
+ nsresult GetSystemFont(nsCString& aFontName) override;
+
+ // TextEventDispatcherListener
+ using nsBaseWidget::NotifyIME;
+ NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) override;
+ NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
+ NS_IMETHOD_(void)
+ OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;
+ NS_IMETHOD_(void)
+ WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) override;
+
+ virtual void OnMemoryPressure(layers::MemoryPressureReason aWhy) override;
+
+ private:
+ void Paint();
+
+ void SetChild(PuppetWidget* aChild);
+
+ nsresult RequestIMEToCommitComposition(bool aCancel);
+ nsresult NotifyIMEOfFocusChange(const IMENotification& aIMENotification);
+ nsresult NotifyIMEOfSelectionChange(const IMENotification& aIMENotification);
+ nsresult NotifyIMEOfCompositionUpdate(
+ const IMENotification& aIMENotification);
+ nsresult NotifyIMEOfTextChange(const IMENotification& aIMENotification);
+ nsresult NotifyIMEOfMouseButtonEvent(const IMENotification& aIMENotification);
+ nsresult NotifyIMEOfPositionChange(const IMENotification& aIMENotification);
+
+ bool CacheEditorRect();
+ bool CacheCompositionRects(uint32_t& aStartOffset,
+ nsTArray<LayoutDeviceIntRect>& aRectArray,
+ uint32_t& aTargetCauseOffset);
+ bool GetCaretRect(LayoutDeviceIntRect& aCaretRect, uint32_t aCaretOffset);
+ uint32_t GetCaretOffset();
+
+ nsIWidgetListener* GetCurrentWidgetListener();
+
+ // When this widget caches input context and currently managed by
+ // IMEStateManager, the cache is valid.
+ bool HaveValidInputContextCache() const;
+
+ class WidgetPaintTask : public Runnable {
+ public:
+ NS_DECL_NSIRUNNABLE
+ explicit WidgetPaintTask(PuppetWidget* widget)
+ : Runnable("PuppetWidget::WidgetPaintTask"), mWidget(widget) {}
+ void Revoke() { mWidget = nullptr; }
+
+ private:
+ PuppetWidget* mWidget;
+ };
+
+ nsRefreshDriver* GetTopLevelRefreshDriver() const;
+
+ // BrowserChild normally holds a strong reference to this PuppetWidget
+ // or its root ancestor, but each PuppetWidget also needs a
+ // reference back to BrowserChild (e.g. to delegate nsIWidget IME calls
+ // to chrome) So we hold a weak reference to BrowserChild here. Since
+ // it's possible for BrowserChild to outlive the PuppetWidget, we clear
+ // this weak reference in Destroy()
+ BrowserChild* mBrowserChild;
+ // The "widget" to which we delegate events if we don't have an
+ // event handler.
+ RefPtr<PuppetWidget> mChild;
+ nsRevocableEventPtr<WidgetPaintTask> mWidgetPaintTask;
+ RefPtr<layers::MemoryPressureObserver> mMemoryPressureObserver;
+ // XXX/cjones: keeping this around until we teach LayerManager to do
+ // retained-content-only transactions
+ RefPtr<DrawTarget> mDrawTarget;
+ // IME
+ IMENotificationRequests mIMENotificationRequestsOfParent;
+ InputContext mInputContext;
+ // mNativeIMEContext is initialized when this dispatches every composition
+ // event both from parent process's widget and TextEventDispatcher in same
+ // process. If it hasn't been started composition yet, this isn't necessary
+ // for XP code since there is no TextComposition instance which is caused by
+ // the PuppetWidget instance.
+ NativeIMEContext mNativeIMEContext;
+ ContentCacheInChild mContentCache;
+
+ // The DPI of the parent widget containing this widget.
+ float mDPI = GetFallbackDPI();
+ int32_t mRounding = 1;
+ double mDefaultScale = GetFallbackDefaultScale().scale;
+
+ ScreenIntMargin mSafeAreaInsets;
+
+ RefPtr<TextEventDispatcherListener> mNativeTextEventDispatcherListener;
+
+ protected:
+ bool mEnabled;
+ bool mVisible;
+
+ private:
+ nsSizeMode mSizeMode;
+
+ bool mNeedIMEStateInit;
+ // When remote process requests to commit/cancel a composition, the
+ // composition may have already been committed in the main process. In such
+ // case, this will receive remaining composition events for the old
+ // composition even after requesting to commit/cancel the old composition
+ // but the TextComposition for the old composition has already been
+ // destroyed. So, until this meets new eCompositionStart, following
+ // composition events should be ignored if this is set to true.
+ bool mIgnoreCompositionEvents;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_PuppetWidget_h__
diff --git a/widget/RemoteLookAndFeel.cpp b/widget/RemoteLookAndFeel.cpp
new file mode 100644
index 0000000000..5eea54496b
--- /dev/null
+++ b/widget/RemoteLookAndFeel.cpp
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 "RemoteLookAndFeel.h"
+
+#include "gfxFont.h"
+#include "MainThreadUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/Try.h"
+#include "nsXULAppAPI.h"
+
+#include <limits>
+#include <type_traits>
+#include <utility>
+
+namespace mozilla::widget {
+
+// A cached copy of the data extracted by ExtractData.
+//
+// Storing this lets us avoid doing most of the work of ExtractData each
+// time we create a new content process.
+//
+// Only used in the parent process.
+static StaticAutoPtr<FullLookAndFeel> sCachedLookAndFeelData;
+
+RemoteLookAndFeel::RemoteLookAndFeel(FullLookAndFeel&& aData)
+ : mTables(std::move(aData.tables())) {
+ MOZ_ASSERT(XRE_IsContentProcess(),
+ "Only content processes should be using a RemoteLookAndFeel");
+}
+
+RemoteLookAndFeel::~RemoteLookAndFeel() = default;
+
+void RemoteLookAndFeel::SetDataImpl(FullLookAndFeel&& aData) {
+ MOZ_ASSERT(XRE_IsContentProcess(),
+ "Only content processes should be using a RemoteLookAndFeel");
+ MOZ_ASSERT(NS_IsMainThread());
+ mTables = std::move(aData.tables());
+}
+
+namespace {
+
+// Some lnf values are somewhat expensive to get, and are not needed in child
+// processes, so we can avoid querying them.
+bool IsNeededInChildProcess(LookAndFeel::IntID aId) {
+ switch (aId) {
+ case LookAndFeel::IntID::AlertNotificationOrigin:
+ return false; // see bug 1703205
+ default:
+ return true;
+ }
+}
+
+template <typename Item, typename UInt, typename ID>
+Result<const Item*, nsresult> MapLookup(const nsTArray<Item>& aItems,
+ const nsTArray<UInt>& aMap, ID aID) {
+ UInt mapped = aMap[static_cast<size_t>(aID)];
+
+ if (mapped == std::numeric_limits<UInt>::max()) {
+ return Err(NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ return &aItems[static_cast<size_t>(mapped)];
+}
+
+template <typename Item, typename UInt, typename Id>
+void AddToMap(nsTArray<Item>& aItems, nsTArray<UInt>& aMap, Id aId,
+ Maybe<Item>&& aNewItem) {
+ auto mapIndex = size_t(aId);
+ aMap.EnsureLengthAtLeast(mapIndex + 1);
+ if (aNewItem.isNothing()) {
+ aMap[mapIndex] = std::numeric_limits<UInt>::max();
+ return;
+ }
+
+ size_t newIndex = aItems.Length();
+ MOZ_ASSERT(newIndex < std::numeric_limits<UInt>::max());
+
+ // Check if there is an existing value in aItems that we can point to.
+ //
+ // The arrays should be small enough and contain few enough unique
+ // values that sequential search here is reasonable.
+ for (size_t i = 0; i < newIndex; ++i) {
+ if (aItems[i] == aNewItem.ref()) {
+ aMap[mapIndex] = static_cast<UInt>(i);
+ return;
+ }
+ }
+
+ aItems.AppendElement(aNewItem.extract());
+ aMap[mapIndex] = static_cast<UInt>(newIndex);
+}
+
+} // namespace
+
+nsresult RemoteLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
+ nscolor& aResult) {
+ const nscolor* result;
+ const bool dark = aScheme == ColorScheme::Dark;
+ MOZ_TRY_VAR(
+ result,
+ MapLookup(dark ? mTables.darkColors() : mTables.lightColors(),
+ dark ? mTables.darkColorMap() : mTables.lightColorMap(), aID));
+ aResult = *result;
+ return NS_OK;
+}
+
+nsresult RemoteLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ IsNeededInChildProcess(aID),
+ "Querying value that we didn't bother getting from the parent process!");
+
+ const int32_t* result;
+ MOZ_TRY_VAR(result, MapLookup(mTables.ints(), mTables.intMap(), aID));
+ aResult = *result;
+ return NS_OK;
+}
+
+nsresult RemoteLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ const float* result;
+ MOZ_TRY_VAR(result, MapLookup(mTables.floats(), mTables.floatMap(), aID));
+ aResult = *result;
+ return NS_OK;
+}
+
+bool RemoteLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ auto result = MapLookup(mTables.fonts(), mTables.fontMap(), aID);
+ if (result.isErr()) {
+ return false;
+ }
+ const LookAndFeelFont& font = *result.unwrap();
+ return LookAndFeelFontToStyle(font, aFontName, aFontStyle);
+}
+
+char16_t RemoteLookAndFeel::GetPasswordCharacterImpl() {
+ return static_cast<char16_t>(mTables.passwordChar());
+}
+
+bool RemoteLookAndFeel::GetEchoPasswordImpl() { return mTables.passwordEcho(); }
+
+static bool AddIDsToMap(nsXPLookAndFeel* aImpl, FullLookAndFeel* aLf) {
+ using IntID = LookAndFeel::IntID;
+ using FontID = LookAndFeel::FontID;
+ using FloatID = LookAndFeel::FloatID;
+ using ColorID = LookAndFeel::ColorID;
+ using ColorScheme = LookAndFeel::ColorScheme;
+
+ bool anyFromOtherTheme = false;
+ for (auto id : MakeEnumeratedRange(IntID::End)) {
+ if (!IsNeededInChildProcess(id)) {
+ continue;
+ }
+ int32_t theInt;
+ nsresult rv = aImpl->NativeGetInt(id, theInt);
+ AddToMap(aLf->tables().ints(), aLf->tables().intMap(), id,
+ NS_SUCCEEDED(rv) ? Some(theInt) : Nothing{});
+ }
+
+ for (auto id : MakeEnumeratedRange(ColorID::End)) {
+ nscolor theColor;
+ nsresult rv = aImpl->NativeGetColor(id, ColorScheme::Light, theColor);
+ AddToMap(aLf->tables().lightColors(), aLf->tables().lightColorMap(), id,
+ NS_SUCCEEDED(rv) ? Some(theColor) : Nothing{});
+ rv = aImpl->NativeGetColor(id, ColorScheme::Dark, theColor);
+ AddToMap(aLf->tables().darkColors(), aLf->tables().darkColorMap(), id,
+ NS_SUCCEEDED(rv) ? Some(theColor) : Nothing{});
+ }
+
+ for (auto id : MakeEnumeratedRange(FloatID::End)) {
+ float theFloat;
+ nsresult rv = aImpl->NativeGetFloat(id, theFloat);
+ AddToMap(aLf->tables().floats(), aLf->tables().floatMap(), id,
+ NS_SUCCEEDED(rv) ? Some(theFloat) : Nothing{});
+ }
+
+ for (auto id : MakeEnumeratedRange(FontID::End)) {
+ gfxFontStyle fontStyle{};
+
+ nsString name;
+ bool rv = aImpl->NativeGetFont(id, name, fontStyle);
+ Maybe<LookAndFeelFont> maybeFont;
+ if (rv) {
+ maybeFont.emplace(
+ nsXPLookAndFeel::StyleToLookAndFeelFont(name, fontStyle));
+ }
+ AddToMap(aLf->tables().fonts(), aLf->tables().fontMap(), id,
+ std::move(maybeFont));
+ }
+
+ return anyFromOtherTheme;
+}
+
+// static
+const FullLookAndFeel* RemoteLookAndFeel::ExtractData() {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only parent processes should be extracting LookAndFeel data");
+
+ if (sCachedLookAndFeelData) {
+ return sCachedLookAndFeelData;
+ }
+
+ static bool sInitialized = false;
+ if (!sInitialized) {
+ sInitialized = true;
+ ClearOnShutdown(&sCachedLookAndFeelData);
+ }
+
+ FullLookAndFeel* lf = new FullLookAndFeel{};
+ nsXPLookAndFeel* impl = nsXPLookAndFeel::GetInstance();
+
+ lf->tables().passwordChar() = impl->GetPasswordCharacterImpl();
+ lf->tables().passwordEcho() = impl->GetEchoPasswordImpl();
+
+ AddIDsToMap(impl, lf);
+
+ // This assignment to sCachedLookAndFeelData must be done after the
+ // WithThemeConfiguredForContent call, since it can end up calling RefreshImpl
+ // on the LookAndFeel, which will clear out sCachedTables.
+ sCachedLookAndFeelData = lf;
+ return sCachedLookAndFeelData;
+}
+
+void RemoteLookAndFeel::ClearCachedData() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ sCachedLookAndFeelData = nullptr;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/RemoteLookAndFeel.h b/widget/RemoteLookAndFeel.h
new file mode 100644
index 0000000000..918f3392ec
--- /dev/null
+++ b/widget/RemoteLookAndFeel.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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/. */
+
+#ifndef mozilla_widget_RemoteLookAndFeel_h__
+#define mozilla_widget_RemoteLookAndFeel_h__
+
+#include "mozilla/widget/nsXPLookAndFeel.h"
+#include "mozilla/widget/LookAndFeelTypes.h"
+
+namespace mozilla::widget {
+
+/**
+ * A LookAndFeel implementation whose native values are provided by the
+ * parent process.
+ */
+class RemoteLookAndFeel final : public nsXPLookAndFeel {
+ public:
+ explicit RemoteLookAndFeel(FullLookAndFeel&& aTables);
+
+ virtual ~RemoteLookAndFeel();
+
+ void NativeInit() override {}
+
+ nsresult NativeGetInt(IntID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID, float& aResult) override;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override;
+ bool NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) override;
+
+ char16_t GetPasswordCharacterImpl() override;
+ bool GetEchoPasswordImpl() override;
+
+ // Sets the LookAndFeel data to be used by this content process' singleton
+ // RemoteLookAndFeel object.
+ void SetDataImpl(FullLookAndFeel&& aTables) override;
+
+ // Extracts the data from the platform's default LookAndFeel implementation.
+ //
+ // This is called in the parent process to obtain the data to send down to
+ // content processes when they are created (and when the OS theme changes).
+ //
+ // Note that the pointer returned from here is only valid until the next time
+ // ClearCachedData is called.
+ static const FullLookAndFeel* ExtractData();
+
+ // Clears any cached extracted data from the platform's default LookAndFeel
+ // implementation.
+ //
+ // This is called in the parent process when the default LookAndFeel is
+ // refreshed, to invalidate sCachedLookAndFeelData.
+ static void ClearCachedData();
+
+ private:
+ LookAndFeelTables mTables;
+};
+
+} // namespace mozilla::widget
+
+#endif // mozilla_widget_RemoteLookAndFeel_h__
diff --git a/widget/Screen.cpp b/widget/Screen.cpp
new file mode 100644
index 0000000000..71a1624624
--- /dev/null
+++ b/widget/Screen.cpp
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* 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 "Screen.h"
+
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/dom/ScreenBinding.h"
+#include "mozilla/Hal.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/StaticPrefs_layout.h"
+
+namespace mozilla::widget {
+
+NS_IMPL_ISUPPORTS(Screen, nsIScreen)
+
+static hal::ScreenOrientation EffectiveOrientation(
+ hal::ScreenOrientation aOrientation, const LayoutDeviceIntRect& aRect) {
+ if (aOrientation == hal::ScreenOrientation::None) {
+ return aRect.Width() >= aRect.Height()
+ ? hal::ScreenOrientation::LandscapePrimary
+ : hal::ScreenOrientation::PortraitPrimary;
+ }
+ return aOrientation;
+}
+
+Screen::Screen(LayoutDeviceIntRect aRect, LayoutDeviceIntRect aAvailRect,
+ uint32_t aPixelDepth, uint32_t aColorDepth,
+ uint32_t aRefreshRate, DesktopToLayoutDeviceScale aContentsScale,
+ CSSToLayoutDeviceScale aDefaultCssScale, float aDPI,
+ IsPseudoDisplay aIsPseudoDisplay,
+ hal::ScreenOrientation aOrientation,
+ OrientationAngle aOrientationAngle)
+ : mRect(aRect),
+ mAvailRect(aAvailRect),
+ mRectDisplayPix(RoundedToInt(aRect / aContentsScale)),
+ mAvailRectDisplayPix(RoundedToInt(aAvailRect / aContentsScale)),
+ mPixelDepth(aPixelDepth),
+ mColorDepth(aColorDepth),
+ mRefreshRate(aRefreshRate),
+ mContentsScale(aContentsScale),
+ mDefaultCssScale(aDefaultCssScale),
+ mDPI(aDPI),
+ mScreenOrientation(EffectiveOrientation(aOrientation, aRect)),
+ mOrientationAngle(aOrientationAngle),
+ mIsPseudoDisplay(aIsPseudoDisplay == IsPseudoDisplay::Yes) {}
+
+Screen::Screen(const dom::ScreenDetails& aScreen)
+ : mRect(aScreen.rect()),
+ mAvailRect(aScreen.availRect()),
+ mRectDisplayPix(aScreen.rectDisplayPix()),
+ mAvailRectDisplayPix(aScreen.availRectDisplayPix()),
+ mPixelDepth(aScreen.pixelDepth()),
+ mColorDepth(aScreen.colorDepth()),
+ mRefreshRate(aScreen.refreshRate()),
+ mContentsScale(aScreen.contentsScaleFactor()),
+ mDefaultCssScale(aScreen.defaultCSSScaleFactor()),
+ mDPI(aScreen.dpi()),
+ mScreenOrientation(aScreen.orientation()),
+ mOrientationAngle(aScreen.orientationAngle()),
+ mIsPseudoDisplay(aScreen.isPseudoDisplay()) {}
+
+Screen::Screen(const Screen& aOther)
+ : mRect(aOther.mRect),
+ mAvailRect(aOther.mAvailRect),
+ mRectDisplayPix(aOther.mRectDisplayPix),
+ mAvailRectDisplayPix(aOther.mAvailRectDisplayPix),
+ mPixelDepth(aOther.mPixelDepth),
+ mColorDepth(aOther.mColorDepth),
+ mRefreshRate(aOther.mRefreshRate),
+ mContentsScale(aOther.mContentsScale),
+ mDefaultCssScale(aOther.mDefaultCssScale),
+ mDPI(aOther.mDPI),
+ mScreenOrientation(aOther.mScreenOrientation),
+ mOrientationAngle(aOther.mOrientationAngle),
+ mIsPseudoDisplay(aOther.mIsPseudoDisplay) {}
+
+dom::ScreenDetails Screen::ToScreenDetails() const {
+ return dom::ScreenDetails(
+ mRect, mRectDisplayPix, mAvailRect, mAvailRectDisplayPix, mPixelDepth,
+ mColorDepth, mRefreshRate, mContentsScale, mDefaultCssScale, mDPI,
+ mScreenOrientation, mOrientationAngle, mIsPseudoDisplay);
+}
+
+NS_IMETHODIMP
+Screen::GetRect(int32_t* aOutLeft, int32_t* aOutTop, int32_t* aOutWidth,
+ int32_t* aOutHeight) {
+ mRect.GetRect(aOutLeft, aOutTop, aOutWidth, aOutHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetRectDisplayPix(int32_t* aOutLeft, int32_t* aOutTop,
+ int32_t* aOutWidth, int32_t* aOutHeight) {
+ mRectDisplayPix.GetRect(aOutLeft, aOutTop, aOutWidth, aOutHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetAvailRect(int32_t* aOutLeft, int32_t* aOutTop, int32_t* aOutWidth,
+ int32_t* aOutHeight) {
+ mAvailRect.GetRect(aOutLeft, aOutTop, aOutWidth, aOutHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetAvailRectDisplayPix(int32_t* aOutLeft, int32_t* aOutTop,
+ int32_t* aOutWidth, int32_t* aOutHeight) {
+ mAvailRectDisplayPix.GetRect(aOutLeft, aOutTop, aOutWidth, aOutHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetColorGamut(dom::ScreenColorGamut* aScreenColorGamut) {
+ // TODO(zrhoffman, bug 1771373): Return a wider color gamut when one is
+ // available
+ *aScreenColorGamut = dom::ScreenColorGamut::Srgb;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetPixelDepth(int32_t* aPixelDepth) {
+ *aPixelDepth = mPixelDepth;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetColorDepth(int32_t* aColorDepth) {
+ *aColorDepth = mColorDepth;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetContentsScaleFactor(double* aOutScale) {
+ *aOutScale = mContentsScale.scale;
+ return NS_OK;
+}
+
+CSSToLayoutDeviceScale Screen::GetCSSToLayoutDeviceScale(
+ IncludeOSZoom aIncludeOSZoom) const {
+ auto scale = CSSToLayoutDeviceScale(StaticPrefs::layout_css_devPixelsPerPx());
+ if (scale.scale <= 0.0) {
+ scale = mDefaultCssScale;
+ }
+ if (bool(aIncludeOSZoom)) {
+ scale.scale *= LookAndFeel::SystemZoomSettings().mFullZoom;
+ }
+ return scale;
+}
+
+NS_IMETHODIMP
+Screen::GetDefaultCSSScaleFactor(double* aOutScale) {
+ *aOutScale = GetCSSToLayoutDeviceScale(IncludeOSZoom::Yes).scale;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetDpi(float* aDPI) {
+ *aDPI = mDPI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetRefreshRate(int32_t* aRefreshRate) {
+ *aRefreshRate = mRefreshRate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Screen::GetIsPseudoDisplay(bool* aIsPseudoDisplay) {
+ *aIsPseudoDisplay = mIsPseudoDisplay;
+ return NS_OK;
+}
+
+hal::ScreenOrientation Screen::GetDefaultOrientationType() const {
+ if (mRect.Width() >= mRect.Height()) {
+ if (mOrientationAngle == 0 || mOrientationAngle == 180) {
+ return hal::ScreenOrientation::LandscapePrimary;
+ }
+ return hal::ScreenOrientation::PortraitPrimary;
+ }
+
+ if (mOrientationAngle == 0 || mOrientationAngle == 180) {
+ return hal::ScreenOrientation::PortraitPrimary;
+ }
+ return hal::ScreenOrientation::LandscapePrimary;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/Screen.h b/widget/Screen.h
new file mode 100644
index 0000000000..f3f8b4a628
--- /dev/null
+++ b/widget/Screen.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_Screen_h
+#define mozilla_widget_Screen_h
+
+#include "nsIScreen.h"
+
+#include "Units.h"
+#include "mozilla/HalScreenConfiguration.h" // For hal::ScreenOrientation
+
+namespace mozilla {
+namespace dom {
+class ScreenDetails;
+} // namespace dom
+
+namespace widget {
+
+class Screen final : public nsIScreen {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISCREEN
+
+ using OrientationAngle = uint16_t;
+ enum class IsPseudoDisplay : bool { No, Yes };
+
+ Screen(LayoutDeviceIntRect aRect, LayoutDeviceIntRect aAvailRect,
+ uint32_t aPixelDepth, uint32_t aColorDepth, uint32_t aRefreshRate,
+ DesktopToLayoutDeviceScale aContentsScale,
+ CSSToLayoutDeviceScale aDefaultCssScale, float aDpi, IsPseudoDisplay,
+ hal::ScreenOrientation = hal::ScreenOrientation::None,
+ OrientationAngle = 0);
+ explicit Screen(const dom::ScreenDetails& aScreenDetails);
+ Screen(const Screen& aOther);
+
+ dom::ScreenDetails ToScreenDetails() const;
+
+ OrientationAngle GetOrientationAngle() const { return mOrientationAngle; }
+ hal::ScreenOrientation GetOrientationType() const {
+ return mScreenOrientation;
+ }
+
+ /**
+ * Return default orientation type that angle is 0.
+ * This returns LandscapePrimary or PortraitPrimary.
+ */
+ hal::ScreenOrientation GetDefaultOrientationType() const;
+
+ float GetDPI() const { return mDPI; }
+
+ const LayoutDeviceIntRect& GetRect() const { return mRect; }
+ const LayoutDeviceIntRect& GetAvailRect() const { return mAvailRect; }
+ const DesktopToLayoutDeviceScale& GetContentsScaleFactor() const {
+ return mContentsScale;
+ }
+
+ enum class IncludeOSZoom : bool { No, Yes };
+ CSSToLayoutDeviceScale GetCSSToLayoutDeviceScale(IncludeOSZoom) const;
+
+ private:
+ virtual ~Screen() = default;
+
+ const LayoutDeviceIntRect mRect;
+ const LayoutDeviceIntRect mAvailRect;
+ const DesktopIntRect mRectDisplayPix;
+ const DesktopIntRect mAvailRectDisplayPix;
+ const uint32_t mPixelDepth;
+ const uint32_t mColorDepth;
+ const uint32_t mRefreshRate;
+ const DesktopToLayoutDeviceScale mContentsScale;
+ const CSSToLayoutDeviceScale mDefaultCssScale;
+ const float mDPI;
+ const hal::ScreenOrientation mScreenOrientation;
+ const OrientationAngle mOrientationAngle;
+ const bool mIsPseudoDisplay;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/ScreenManager.cpp b/widget/ScreenManager.cpp
new file mode 100644
index 0000000000..58e20806eb
--- /dev/null
+++ b/widget/ScreenManager.cpp
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ScreenManager.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPtr.h"
+#ifdef MOZ_WAYLAND
+# include "mozilla/WidgetUtilsGtk.h"
+#endif /* MOZ_WAYLAND */
+
+static mozilla::LazyLogModule sScreenLog("WidgetScreen");
+
+namespace mozilla::widget {
+
+NS_IMPL_ISUPPORTS(ScreenManager, nsIScreenManager)
+
+ScreenManager::ScreenManager() = default;
+
+ScreenManager::~ScreenManager() = default;
+
+static StaticRefPtr<ScreenManager> sSingleton;
+
+ScreenManager& ScreenManager::GetSingleton() {
+ if (!sSingleton) {
+ sSingleton = new ScreenManager();
+ ClearOnShutdown(&sSingleton);
+ }
+ return *sSingleton;
+}
+
+already_AddRefed<ScreenManager> ScreenManager::GetAddRefedSingleton() {
+ RefPtr<ScreenManager> sm = &GetSingleton();
+ return sm.forget();
+}
+
+void ScreenManager::SetHelper(UniquePtr<Helper> aHelper) {
+ mHelper = std::move(aHelper);
+}
+
+// static
+void ScreenManager::Refresh(nsTArray<RefPtr<Screen>>&& aScreens) {
+ if (PastShutdownPhase(ShutdownPhase::XPCOMShutdown)) {
+ // We don't refresh screen data if starting XPCOM shutdown path.
+ // GetSingleton returns invalid data since it is freed.
+ return;
+ }
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refresh screens"));
+ GetSingleton().RefreshInternal(std::move(aScreens));
+}
+
+void ScreenManager::Refresh(nsTArray<mozilla::dom::ScreenDetails>&& aScreens) {
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refresh screens from IPC"));
+
+ AutoTArray<RefPtr<Screen>, 4> screens;
+ for (auto& screen : aScreens) {
+ screens.AppendElement(new Screen(screen));
+ }
+ RefreshInternal(std::move(screens));
+}
+
+void ScreenManager::RefreshInternal(nsTArray<RefPtr<Screen>>&& aScreens) {
+ mScreenList = std::move(aScreens);
+
+ CopyScreensToAllRemotesIfIsParent();
+ if (nsCOMPtr<nsIObserverService> s = services::GetObserverService()) {
+ s->NotifyObservers(nullptr, "screen-information-changed", nullptr);
+ }
+}
+
+template <class Range>
+void ScreenManager::CopyScreensToRemoteRange(Range aRemoteRange) {
+ AutoTArray<dom::ScreenDetails, 4> screens;
+ for (auto& screen : mScreenList) {
+ screens.AppendElement(screen->ToScreenDetails());
+ }
+ for (auto cp : aRemoteRange) {
+ MOZ_LOG(sScreenLog, LogLevel::Debug,
+ ("Send screens to [Pid %d]", cp->Pid()));
+ if (!cp->SendRefreshScreens(screens)) {
+ MOZ_LOG(sScreenLog, LogLevel::Error,
+ ("SendRefreshScreens to [Pid %d] failed", cp->Pid()));
+ }
+ }
+}
+
+void ScreenManager::CopyScreensToRemote(dom::ContentParent* aContentParent) {
+ MOZ_ASSERT(aContentParent);
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ auto range = {aContentParent};
+ CopyScreensToRemoteRange(range);
+}
+
+void ScreenManager::CopyScreensToAllRemotesIfIsParent() {
+ if (XRE_IsContentProcess()) {
+ return;
+ }
+
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refreshing all ContentParents"));
+
+ CopyScreensToRemoteRange(
+ dom::ContentParent::AllProcesses(dom::ContentParent::eLive));
+}
+
+// Returns the screen that contains the rectangle. If the rect overlaps
+// multiple screens, it picks the screen with the greatest area of intersection.
+//
+// The coordinates are in desktop pixels.
+//
+NS_IMETHODIMP
+ScreenManager::ScreenForRect(int32_t aX, int32_t aY, int32_t aWidth,
+ int32_t aHeight, nsIScreen** aOutScreen) {
+ DesktopIntRect rect(aX, aY, aWidth, aHeight);
+ nsCOMPtr<nsIScreen> screen = ScreenForRect(rect);
+ screen.forget(aOutScreen);
+ return NS_OK;
+}
+
+already_AddRefed<Screen> ScreenManager::ScreenForRect(
+ const DesktopIntRect& aRect) {
+#if defined(MOZ_WAYLAND) && defined(MOZ_LOGGING)
+ static bool inWayland = GdkIsWaylandDisplay();
+ if (inWayland) {
+ MOZ_LOG(sScreenLog, LogLevel::Warning,
+ ("Getting screen in wayland, primary display will be returned."));
+ }
+#endif
+
+ if (mScreenList.IsEmpty()) {
+ MOZ_LOG(sScreenLog, LogLevel::Warning,
+ ("No screen available. This can happen in xpcshell."));
+ auto screen = MakeRefPtr<Screen>(
+ LayoutDeviceIntRect(), LayoutDeviceIntRect(), 0, 0, 0,
+ DesktopToLayoutDeviceScale(), CSSToLayoutDeviceScale(), 96 /* dpi */,
+ Screen::IsPseudoDisplay::No, hal::ScreenOrientation::None, 0);
+ return screen.forget();
+ }
+
+ // Optimize for the common case. If the number of screens is only
+ // one then just return the primary screen.
+ if (mScreenList.Length() == 1) {
+ return GetPrimaryScreen();
+ }
+
+ // which screen should we return?
+ Screen* which = mScreenList[0].get();
+
+ // walk the list of screens and find the one that has the most
+ // surface area.
+ uint32_t area = 0;
+ for (auto& screen : mScreenList) {
+ int32_t x, y, width, height;
+ x = y = width = height = 0;
+ screen->GetRectDisplayPix(&x, &y, &width, &height);
+ // calculate the surface area
+ DesktopIntRect screenRect(x, y, width, height);
+ screenRect.IntersectRect(screenRect, aRect);
+ uint32_t tempArea = screenRect.Area();
+ if (tempArea > area) {
+ which = screen.get();
+ area = tempArea;
+ }
+ }
+
+ // If the rect intersects one or more screen,
+ // return the screen that has the largest intersection.
+ if (area > 0) {
+ RefPtr<Screen> ret = which;
+ return ret.forget();
+ }
+
+ // If the rect does not intersect a screen, find
+ // a screen that is nearest to the rect.
+ uint32_t distance = UINT32_MAX;
+ for (auto& screen : mScreenList) {
+ int32_t x, y, width, height;
+ x = y = width = height = 0;
+ screen->GetRectDisplayPix(&x, &y, &width, &height);
+
+ uint32_t distanceX = 0;
+ if (aRect.x > (x + width)) {
+ distanceX = aRect.x - (x + width);
+ } else if (aRect.XMost() < x) {
+ distanceX = x - aRect.XMost();
+ }
+
+ uint32_t distanceY = 0;
+ if (aRect.y > (y + height)) {
+ distanceY = aRect.y - (y + height);
+ } else if (aRect.YMost() < y) {
+ distanceY = y - aRect.YMost();
+ }
+
+ uint32_t tempDistance = distanceX * distanceX + distanceY * distanceY;
+ if (tempDistance < distance) {
+ which = screen.get();
+ distance = tempDistance;
+ if (distance == 0) {
+ break;
+ }
+ }
+ }
+
+ RefPtr<Screen> ret = which;
+ return ret.forget();
+}
+
+// The screen with the menubar/taskbar. This shouldn't be needed very
+// often.
+//
+already_AddRefed<Screen> ScreenManager::GetPrimaryScreen() {
+ if (mScreenList.IsEmpty()) {
+ MOZ_LOG(sScreenLog, LogLevel::Warning,
+ ("No screen available. This can happen in xpcshell."));
+ return MakeAndAddRef<Screen>(
+ LayoutDeviceIntRect(), LayoutDeviceIntRect(), 0, 0, 0,
+ DesktopToLayoutDeviceScale(), CSSToLayoutDeviceScale(), 96 /* dpi */,
+ Screen::IsPseudoDisplay::No, hal::ScreenOrientation::None, 0);
+ }
+
+ return do_AddRef(mScreenList[0]);
+}
+
+NS_IMETHODIMP
+ScreenManager::GetPrimaryScreen(nsIScreen** aPrimaryScreen) {
+ nsCOMPtr<nsIScreen> screen = GetPrimaryScreen();
+ screen.forget(aPrimaryScreen);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ScreenManager::GetTotalScreenPixels(int64_t* aTotalScreenPixels) {
+ MOZ_ASSERT(aTotalScreenPixels);
+
+ if (mScreenList.IsEmpty()) {
+ MOZ_LOG(sScreenLog, LogLevel::Warning,
+ ("No screen available. This can happen in xpcshell."));
+ *aTotalScreenPixels = 0;
+ return NS_OK;
+ }
+
+ int64_t pixels = 0;
+ for (auto& screen : mScreenList) {
+ int32_t x, y, width, height;
+ x = y = width = height = 0;
+ screen->GetRect(&x, &y, &width, &height);
+ pixels += width * height;
+ }
+
+ *aTotalScreenPixels = pixels;
+ return NS_OK;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ScreenManager.h b/widget/ScreenManager.h
new file mode 100644
index 0000000000..7c40b9fcdb
--- /dev/null
+++ b/widget/ScreenManager.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_ScreenManager_h
+#define mozilla_widget_ScreenManager_h
+
+#include "nsIScreenManager.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/widget/Screen.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+class ContentParent;
+class ScreenDetails;
+} // namespace dom
+} // namespace mozilla
+
+namespace mozilla::widget {
+
+class ScreenManager final : public nsIScreenManager {
+ public:
+ class Helper {
+ public:
+ virtual ~Helper() = default;
+ };
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISCREENMANAGER
+
+ static ScreenManager& GetSingleton();
+ static already_AddRefed<ScreenManager> GetAddRefedSingleton();
+
+ void SetHelper(UniquePtr<Helper> aHelper);
+ static void Refresh(nsTArray<RefPtr<Screen>>&& aScreens);
+ void Refresh(nsTArray<mozilla::dom::ScreenDetails>&& aScreens);
+ void CopyScreensToRemote(mozilla::dom::ContentParent* aContentParent);
+ already_AddRefed<Screen> GetPrimaryScreen();
+ already_AddRefed<Screen> ScreenForRect(const DesktopIntRect& aRect);
+
+ const nsTArray<RefPtr<Screen>>& CurrentScreenList() const {
+ return mScreenList;
+ }
+
+ private:
+ ScreenManager();
+ virtual ~ScreenManager();
+
+ void RefreshInternal(nsTArray<RefPtr<Screen>>&& aScreens);
+ template <class Range>
+ void CopyScreensToRemoteRange(Range aRemoteRange);
+ void CopyScreensToAllRemotesIfIsParent();
+
+ AutoTArray<RefPtr<Screen>, 4> mScreenList;
+ UniquePtr<Helper> mHelper;
+};
+
+} // namespace mozilla::widget
+
+#endif // mozilla_widget_ScreenManager_h
diff --git a/widget/ScrollbarDrawing.cpp b/widget/ScrollbarDrawing.cpp
new file mode 100644
index 0000000000..43bda00411
--- /dev/null
+++ b/widget/ScrollbarDrawing.cpp
@@ -0,0 +1,424 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* 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 "ScrollbarDrawing.h"
+
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsContainerFrame.h"
+#include "nsDeviceContext.h"
+#include "nsIFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsLookAndFeel.h"
+#include "nsNativeTheme.h"
+
+using namespace mozilla::gfx;
+
+namespace mozilla::widget {
+
+using mozilla::RelativeLuminanceUtils;
+
+/* static */
+auto ScrollbarDrawing::GetDPIRatioForScrollbarPart(const nsPresContext* aPc)
+ -> DPIRatio {
+ DPIRatio ratio(
+ float(AppUnitsPerCSSPixel()) /
+ float(aPc->DeviceContext()->AppUnitsPerDevPixelAtUnitFullZoom()));
+ if (aPc->IsPrintPreview()) {
+ ratio.scale *= aPc->GetPrintPreviewScaleForSequenceFrameOrScrollbars();
+ }
+ if (mKind == Kind::Cocoa) {
+ return DPIRatio(ratio.scale >= 2.0f ? 2.0f : 1.0f);
+ }
+ return ratio;
+}
+
+/*static*/
+nsIFrame* ScrollbarDrawing::GetParentScrollbarFrame(nsIFrame* aFrame) {
+ // Walk our parents to find a scrollbar frame
+ nsIFrame* scrollbarFrame = aFrame;
+ do {
+ if (scrollbarFrame->IsScrollbarFrame()) {
+ break;
+ }
+ } while ((scrollbarFrame = scrollbarFrame->GetParent()));
+
+ // We return null if we can't find a parent scrollbar frame
+ return scrollbarFrame;
+}
+
+/*static*/
+bool ScrollbarDrawing::IsParentScrollbarRolledOver(nsIFrame* aFrame) {
+ nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame);
+ return aFrame->PresContext()->UseOverlayScrollbars()
+ ? nsNativeTheme::CheckBooleanAttr(scrollbarFrame, nsGkAtoms::hover)
+ : nsNativeTheme::GetContentState(scrollbarFrame,
+ StyleAppearance::None)
+ .HasState(ElementState::HOVER);
+}
+
+/*static*/
+bool ScrollbarDrawing::IsParentScrollbarHoveredOrActive(nsIFrame* aFrame) {
+ nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame);
+ return scrollbarFrame &&
+ scrollbarFrame->GetContent()
+ ->AsElement()
+ ->State()
+ .HasAtLeastOneOfStates(ElementState::HOVER | ElementState::ACTIVE);
+}
+
+/*static*/
+bool ScrollbarDrawing::IsScrollbarWidthThin(const ComputedStyle& aStyle) {
+ auto scrollbarWidth = aStyle.StyleUIReset()->ScrollbarWidth();
+ return scrollbarWidth == StyleScrollbarWidth::Thin;
+}
+
+/*static*/
+bool ScrollbarDrawing::IsScrollbarWidthThin(nsIFrame* aFrame) {
+ ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame);
+ return IsScrollbarWidthThin(*style);
+}
+
+CSSIntCoord ScrollbarDrawing::GetCSSScrollbarSize(StyleScrollbarWidth aWidth,
+ Overlay aOverlay) const {
+ return mScrollbarSize[aWidth == StyleScrollbarWidth::Thin]
+ [aOverlay == Overlay::Yes];
+}
+
+void ScrollbarDrawing::ConfigureScrollbarSize(StyleScrollbarWidth aWidth,
+ Overlay aOverlay,
+ CSSIntCoord aSize) {
+ mScrollbarSize[aWidth == StyleScrollbarWidth::Thin]
+ [aOverlay == Overlay::Yes] = aSize;
+}
+
+void ScrollbarDrawing::ConfigureScrollbarSize(CSSIntCoord aSize) {
+ ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::No, aSize);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes, aSize);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::No, aSize / 2);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes, aSize / 2);
+}
+
+LayoutDeviceIntCoord ScrollbarDrawing::GetScrollbarSize(
+ const nsPresContext* aPresContext, StyleScrollbarWidth aWidth,
+ Overlay aOverlay) {
+ return (CSSCoord(GetCSSScrollbarSize(aWidth, aOverlay)) *
+ GetDPIRatioForScrollbarPart(aPresContext))
+ .Rounded();
+}
+
+LayoutDeviceIntCoord ScrollbarDrawing::GetScrollbarSize(
+ const nsPresContext* aPresContext, nsIFrame* aFrame) {
+ auto* style = nsLayoutUtils::StyleForScrollbar(aFrame);
+ auto width = style->StyleUIReset()->ScrollbarWidth();
+ auto overlay =
+ aPresContext->UseOverlayScrollbars() ? Overlay::Yes : Overlay::No;
+ return GetScrollbarSize(aPresContext, width, overlay);
+}
+
+bool ScrollbarDrawing::IsScrollbarTrackOpaque(nsIFrame* aFrame) {
+ auto trackColor = ComputeScrollbarTrackColor(
+ aFrame, *nsLayoutUtils::StyleForScrollbar(aFrame),
+ aFrame->PresContext()->Document()->State(),
+ Colors(aFrame, StyleAppearance::ScrollbartrackVertical));
+ return trackColor.a == 1.0f;
+}
+
+sRGBColor ScrollbarDrawing::ComputeScrollbarTrackColor(
+ nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return aColors.System(StyleSystemColor::Window);
+ }
+ const nsStyleUI* ui = aStyle.StyleUI();
+ if (ui->mScrollbarColor.IsColors()) {
+ return sRGBColor::FromABGR(
+ ui->mScrollbarColor.AsColors().track.CalcColor(aStyle));
+ }
+ static constexpr sRGBColor sDefaultDarkTrackColor =
+ sRGBColor::FromU8(20, 20, 25, 77);
+ static constexpr sRGBColor sDefaultTrackColor(
+ gfx::sRGBColor::UnusualFromARGB(0xfff0f0f0));
+
+ auto systemColor = aDocumentState.HasAllStates(DocumentState::WINDOW_INACTIVE)
+ ? StyleSystemColor::ThemedScrollbarInactive
+ : StyleSystemColor::ThemedScrollbar;
+ return aColors.SystemOrElse(systemColor, [&] {
+ return aColors.IsDark() ? sDefaultDarkTrackColor : sDefaultTrackColor;
+ });
+}
+
+// Don't use the theme color for dark scrollbars if it's not a color (if it's
+// grey-ish), as that'd either lack enough contrast, or be close to what we'd do
+// by default anyways.
+sRGBColor ScrollbarDrawing::ComputeScrollbarThumbColor(
+ nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors) {
+ const nsStyleUI* ui = aStyle.StyleUI();
+ if (ui->mScrollbarColor.IsColors()) {
+ return sRGBColor::FromABGR(ThemeColors::AdjustUnthemedScrollbarThumbColor(
+ ui->mScrollbarColor.AsColors().thumb.CalcColor(aStyle), aElementState));
+ }
+
+ auto systemColor = [&] {
+ if (aDocumentState.HasState(DocumentState::WINDOW_INACTIVE)) {
+ return StyleSystemColor::ThemedScrollbarThumbInactive;
+ }
+ if (aElementState.HasState(ElementState::ACTIVE)) {
+ if (aColors.HighContrast()) {
+ return StyleSystemColor::Selecteditem;
+ }
+ return StyleSystemColor::ThemedScrollbarThumbActive;
+ }
+ if (aElementState.HasState(ElementState::HOVER)) {
+ if (aColors.HighContrast()) {
+ return StyleSystemColor::Selecteditem;
+ }
+ return StyleSystemColor::ThemedScrollbarThumbHover;
+ }
+ if (aColors.HighContrast()) {
+ return StyleSystemColor::Windowtext;
+ }
+ return StyleSystemColor::ThemedScrollbarThumb;
+ }();
+
+ return aColors.SystemOrElse(systemColor, [&] {
+ const nscolor unthemedColor = aColors.IsDark() ? NS_RGBA(249, 249, 250, 102)
+ : NS_RGB(0xcd, 0xcd, 0xcd);
+
+ return sRGBColor::FromABGR(ThemeColors::AdjustUnthemedScrollbarThumbColor(
+ unthemedColor, aElementState));
+ });
+}
+
+template <typename PaintBackendData>
+bool ScrollbarDrawing::DoPaintDefaultScrollbar(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ const bool overlay = aFrame->PresContext()->UseOverlayScrollbars();
+ if (overlay && !aElementState.HasAtLeastOneOfStates(ElementState::HOVER |
+ ElementState::ACTIVE)) {
+ return true;
+ }
+ const auto color =
+ ComputeScrollbarTrackColor(aFrame, aStyle, aDocumentState, aColors);
+ if (overlay && mKind == Kind::Win11) {
+ LayoutDeviceCoord radius =
+ (aScrollbarKind == ScrollbarKind::Horizontal ? aRect.height
+ : aRect.width) /
+ 2.0f;
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, color,
+ sRGBColor(), 0, radius / aDpiRatio,
+ aDpiRatio);
+ } else {
+ ThemeDrawing::FillRect(aPaintData, aRect, color);
+ }
+ return true;
+}
+
+bool ScrollbarDrawing::PaintScrollbar(
+ DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintDefaultScrollbar(aDrawTarget, aRect, aScrollbarKind, aFrame,
+ aStyle, aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+bool ScrollbarDrawing::PaintScrollbar(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintDefaultScrollbar(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+template <typename PaintBackendData>
+bool ScrollbarDrawing::DoPaintDefaultScrollCorner(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ auto scrollbarColor =
+ ComputeScrollbarTrackColor(aFrame, aStyle, aDocumentState, aColors);
+ ThemeDrawing::FillRect(aPaintData, aRect, scrollbarColor);
+ return true;
+}
+
+bool ScrollbarDrawing::PaintScrollCorner(
+ DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ return DoPaintDefaultScrollCorner(aDrawTarget, aRect, aScrollbarKind, aFrame,
+ aStyle, aDocumentState, aColors, aDpiRatio);
+}
+
+bool ScrollbarDrawing::PaintScrollCorner(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ return DoPaintDefaultScrollCorner(aWrData, aRect, aScrollbarKind, aFrame,
+ aStyle, aDocumentState, aColors, aDpiRatio);
+}
+
+nscolor ScrollbarDrawing::GetScrollbarButtonColor(nscolor aTrackColor,
+ ElementState aStates) {
+ // See numbers in GetScrollbarArrowColor.
+ // This function is written based on ratios between values listed there.
+
+ bool isActive = aStates.HasState(ElementState::ACTIVE);
+ bool isHover = aStates.HasState(ElementState::HOVER);
+ if (!isActive && !isHover) {
+ return aTrackColor;
+ }
+ float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
+ if (isActive) {
+ if (luminance >= 0.18f) {
+ luminance *= 0.134f;
+ } else {
+ luminance /= 0.134f;
+ luminance = std::min(luminance, 1.0f);
+ }
+ } else {
+ if (luminance >= 0.18f) {
+ luminance *= 0.805f;
+ } else {
+ luminance /= 0.805f;
+ }
+ }
+ return RelativeLuminanceUtils::Adjust(aTrackColor, luminance);
+}
+
+Maybe<nscolor> ScrollbarDrawing::GetScrollbarArrowColor(nscolor aButtonColor) {
+ // In Windows 10 scrollbar, there are several gray colors used:
+ //
+ // State | Background (lum) | Arrow | Contrast
+ // -------+------------------+---------+---------
+ // Normal | Gray 240 (87.1%) | Gray 96 | 5.5
+ // Hover | Gray 218 (70.1%) | Black | 15.0
+ // Active | Gray 96 (11.7%) | White | 6.3
+ //
+ // Contrast value is computed based on the definition in
+ // https://www.w3.org/TR/WCAG20/#contrast-ratiodef
+ //
+ // This function is written based on these values.
+
+ if (NS_GET_A(aButtonColor) == 0) {
+ // If the button color is transparent, because of e.g.
+ // scrollbar-color: <something> transparent, then use
+ // the thumb color, which is expected to have enough
+ // contrast.
+ return Nothing();
+ }
+
+ float luminance = RelativeLuminanceUtils::Compute(aButtonColor);
+ // Color with luminance larger than 0.72 has contrast ratio over 4.6
+ // to color with luminance of gray 96, so this value is chosen for
+ // this range. It is the luminance of gray 221.
+ if (luminance >= 0.72) {
+ // ComputeRelativeLuminanceFromComponents(96). That function cannot
+ // be constexpr because of std::pow.
+ const float GRAY96_LUMINANCE = 0.117f;
+ return Some(RelativeLuminanceUtils::Adjust(aButtonColor, GRAY96_LUMINANCE));
+ }
+ // The contrast ratio of a color to black equals that to white when its
+ // luminance is around 0.18, with a contrast ratio ~4.6 to both sides,
+ // thus the value below. It's the lumanince of gray 118.
+ //
+ // TODO(emilio): Maybe the button alpha is not the best thing to use here and
+ // we should use the thumb alpha? It seems weird that the color of the arrow
+ // depends on the opacity of the scrollbar thumb...
+ if (luminance >= 0.18) {
+ return Some(NS_RGBA(0, 0, 0, NS_GET_A(aButtonColor)));
+ }
+ return Some(NS_RGBA(255, 255, 255, NS_GET_A(aButtonColor)));
+}
+
+std::pair<sRGBColor, sRGBColor> ScrollbarDrawing::ComputeScrollbarButtonColors(
+ nsIFrame* aFrame, StyleAppearance aAppearance, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ if (aElementState.HasAtLeastOneOfStates(ElementState::ACTIVE |
+ ElementState::HOVER)) {
+ return aColors.SystemPair(StyleSystemColor::Selecteditem,
+ StyleSystemColor::Buttonface);
+ }
+ return aColors.SystemPair(StyleSystemColor::Window,
+ StyleSystemColor::Windowtext);
+ }
+
+ auto trackColor =
+ ComputeScrollbarTrackColor(aFrame, aStyle, aDocumentState, aColors);
+ nscolor buttonColor =
+ GetScrollbarButtonColor(trackColor.ToABGR(), aElementState);
+ auto arrowColor =
+ GetScrollbarArrowColor(buttonColor)
+ .map(sRGBColor::FromABGR)
+ .valueOrFrom([&] {
+ return ComputeScrollbarThumbColor(aFrame, aStyle, aElementState,
+ aDocumentState, aColors);
+ });
+ return {sRGBColor::FromABGR(buttonColor), arrowColor};
+}
+
+bool ScrollbarDrawing::PaintScrollbarButton(
+ DrawTarget& aDrawTarget, StyleAppearance aAppearance,
+ const LayoutDeviceRect& aRect, ScrollbarKind aScrollbarKind,
+ nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio&) {
+ auto [buttonColor, arrowColor] = ComputeScrollbarButtonColors(
+ aFrame, aAppearance, aStyle, aElementState, aDocumentState, aColors);
+ aDrawTarget.FillRect(aRect.ToUnknownRect(),
+ ColorPattern(ToDeviceColor(buttonColor)));
+
+ // Start with Up arrow.
+ float arrowPolygonX[] = {-4.0f, 0.0f, 4.0f, 4.0f, 0.0f, -4.0f};
+ float arrowPolygonY[] = {0.0f, -4.0f, 0.0f, 3.0f, -1.0f, 3.0f};
+
+ const float kPolygonSize = 17;
+
+ const int32_t arrowNumPoints = ArrayLength(arrowPolygonX);
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarbuttonUp:
+ break;
+ case StyleAppearance::ScrollbarbuttonDown:
+ for (int32_t i = 0; i < arrowNumPoints; i++) {
+ arrowPolygonY[i] *= -1;
+ }
+ break;
+ case StyleAppearance::ScrollbarbuttonLeft:
+ for (int32_t i = 0; i < arrowNumPoints; i++) {
+ float temp = arrowPolygonX[i];
+ arrowPolygonX[i] = arrowPolygonY[i];
+ arrowPolygonY[i] = temp;
+ }
+ break;
+ case StyleAppearance::ScrollbarbuttonRight:
+ for (int32_t i = 0; i < arrowNumPoints; i++) {
+ float temp = arrowPolygonX[i];
+ arrowPolygonX[i] = arrowPolygonY[i] * -1;
+ arrowPolygonY[i] = temp;
+ }
+ break;
+ default:
+ return false;
+ }
+ ThemeDrawing::PaintArrow(aDrawTarget, aRect, arrowPolygonX, arrowPolygonY,
+ kPolygonSize, arrowNumPoints, arrowColor);
+ return true;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ScrollbarDrawing.h b/widget/ScrollbarDrawing.h
new file mode 100644
index 0000000000..656822aeed
--- /dev/null
+++ b/widget/ScrollbarDrawing.h
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_ScrollbarDrawing_h
+#define mozilla_widget_ScrollbarDrawing_h
+
+#include "mozilla/dom/RustTypes.h"
+#include "mozilla/gfx/2D.h"
+#include "nsColor.h"
+#include "nsITheme.h"
+#include "ThemeColors.h"
+#include "ThemeDrawing.h"
+#include "Units.h"
+
+namespace mozilla::widget {
+
+class ScrollbarDrawing {
+ protected:
+ using DPIRatio = mozilla::CSSToLayoutDeviceScale;
+ using ElementState = dom::ElementState;
+ using DocumentState = dom::DocumentState;
+ using DrawTarget = mozilla::gfx::DrawTarget;
+ using sRGBColor = mozilla::gfx::sRGBColor;
+ using Colors = ThemeColors;
+ using Overlay = nsITheme::Overlay;
+ using WebRenderBackendData = mozilla::widget::WebRenderBackendData;
+
+ enum class Kind : uint8_t {
+ Android,
+ Cocoa,
+ Gtk,
+ Win10,
+ Win11,
+ };
+
+ explicit ScrollbarDrawing(Kind aKind) : mKind(aKind) {}
+
+ public:
+ virtual ~ScrollbarDrawing() = default;
+
+ enum class ScrollbarKind : uint8_t {
+ Horizontal,
+ VerticalLeft,
+ VerticalRight,
+ };
+
+ DPIRatio GetDPIRatioForScrollbarPart(const nsPresContext*);
+
+ static nsIFrame* GetParentScrollbarFrame(nsIFrame* aFrame);
+ static bool IsParentScrollbarRolledOver(nsIFrame* aFrame);
+ static bool IsParentScrollbarHoveredOrActive(nsIFrame* aFrame);
+
+ static bool IsScrollbarWidthThin(const ComputedStyle& aStyle);
+ static bool IsScrollbarWidthThin(nsIFrame* aFrame);
+
+ CSSIntCoord GetCSSScrollbarSize(StyleScrollbarWidth, Overlay) const;
+ LayoutDeviceIntCoord GetScrollbarSize(const nsPresContext*,
+ StyleScrollbarWidth, Overlay);
+ LayoutDeviceIntCoord GetScrollbarSize(const nsPresContext*, nsIFrame*);
+
+ virtual LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*,
+ StyleAppearance aAppearance,
+ nsIFrame* aFrame) = 0;
+ virtual Maybe<nsITheme::Transparency> GetScrollbarPartTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ return Nothing();
+ }
+
+ bool IsScrollbarTrackOpaque(nsIFrame*);
+ virtual sRGBColor ComputeScrollbarTrackColor(nsIFrame*, const ComputedStyle&,
+ const DocumentState&,
+ const Colors&);
+ virtual sRGBColor ComputeScrollbarThumbColor(nsIFrame*, const ComputedStyle&,
+ const ElementState&,
+ const DocumentState&,
+ const Colors&);
+
+ nscolor GetScrollbarButtonColor(nscolor aTrackColor, ElementState);
+ Maybe<nscolor> GetScrollbarArrowColor(nscolor aButtonColor);
+
+ // Returned colors are button, arrow.
+ virtual std::pair<sRGBColor, sRGBColor> ComputeScrollbarButtonColors(
+ nsIFrame*, StyleAppearance, const ComputedStyle&, const ElementState&,
+ const DocumentState&, const Colors&);
+
+ virtual bool PaintScrollbarButton(DrawTarget&, StyleAppearance,
+ const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&,
+ const ElementState&, const DocumentState&,
+ const Colors&, const DPIRatio&);
+
+ virtual bool PaintScrollbarThumb(DrawTarget&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*,
+ const ComputedStyle&, const ElementState&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&) = 0;
+ virtual bool PaintScrollbarThumb(WebRenderBackendData&,
+ const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&,
+ const ElementState&, const DocumentState&,
+ const Colors&, const DPIRatio&) = 0;
+
+ template <typename PaintBackendData>
+ bool DoPaintDefaultScrollbar(PaintBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState&, const DocumentState&,
+ const Colors&, const DPIRatio&);
+ bool PaintScrollbar(DrawTarget&, const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&, const ElementState&,
+ const DocumentState&, const Colors&, const DPIRatio&);
+ bool PaintScrollbar(WebRenderBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState&, const DocumentState&, const Colors&,
+ const DPIRatio&);
+
+ virtual bool PaintScrollbarTrack(DrawTarget&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*,
+ const ComputedStyle&, const DocumentState&,
+ const Colors&, const DPIRatio&) {
+ // Draw nothing by default. Subclasses can override this.
+ return true;
+ }
+ virtual bool PaintScrollbarTrack(WebRenderBackendData&,
+ const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&) {
+ // Draw nothing by default. Subclasses can override this.
+ return true;
+ }
+
+ template <typename PaintBackendData>
+ bool DoPaintDefaultScrollCorner(PaintBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*,
+ const ComputedStyle&, const DocumentState&,
+ const Colors&, const DPIRatio&);
+ virtual bool PaintScrollCorner(DrawTarget&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&);
+ virtual bool PaintScrollCorner(WebRenderBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&);
+
+ virtual void RecomputeScrollbarParams() = 0;
+
+ virtual bool ShouldDrawScrollbarButtons() { return true; }
+
+ private:
+ // The scrollbar sizes for all our scrollbars. Indices are overlay or not,
+ // then thin or not. Should be configured via ConfigureScrollbarSize.
+ CSSIntCoord mScrollbarSize[2][2]{};
+
+ protected:
+ // For some kind of style differences a full virtual method is overkill, so we
+ // store the kind here so we can branch on it if necessary.
+ Kind mKind;
+
+ // Configures the scrollbar sizes based on a single size.
+ void ConfigureScrollbarSize(CSSIntCoord);
+
+ // Configures a particular scrollbar size.
+ void ConfigureScrollbarSize(StyleScrollbarWidth, Overlay, CSSIntCoord);
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ScrollbarDrawingAndroid.cpp b/widget/ScrollbarDrawingAndroid.cpp
new file mode 100644
index 0000000000..29ad917a91
--- /dev/null
+++ b/widget/ScrollbarDrawingAndroid.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 40; 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 "ScrollbarDrawingAndroid.h"
+
+#include "nsIFrame.h"
+#include "nsNativeTheme.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+LayoutDeviceIntSize ScrollbarDrawingAndroid::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, StyleAppearance aAppearance,
+ nsIFrame* aFrame) {
+ MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
+ auto size =
+ GetScrollbarSize(aPresContext, StyleScrollbarWidth::Auto, Overlay::Yes);
+ return LayoutDeviceIntSize{size, size};
+}
+
+template <typename PaintBackendData>
+void ScrollbarDrawingAndroid::DoPaintScrollbarThumb(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ // TODO(emilio): Maybe do like macOS and draw a stroke?
+ const auto color = ComputeScrollbarThumbColor(aFrame, aStyle, aElementState,
+ aDocumentState, aColors);
+ const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal;
+
+ // Draw the thumb rect centered in the scrollbar.
+ LayoutDeviceRect thumbRect(aRect);
+ if (horizontal) {
+ thumbRect.height *= 0.5f;
+ thumbRect.y += thumbRect.height * 0.5f;
+ } else {
+ thumbRect.width *= 0.5f;
+ thumbRect.x += thumbRect.width * 0.5f;
+ }
+
+ const LayoutDeviceCoord radius =
+ (horizontal ? thumbRect.height : thumbRect.width) / 2.0f;
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, color,
+ sRGBColor::White(0.0f), 0.0f,
+ radius / aDpiRatio, aDpiRatio);
+}
+
+bool ScrollbarDrawingAndroid::PaintScrollbarThumb(
+ DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarThumb(aDt, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingAndroid::PaintScrollbarThumb(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+void ScrollbarDrawingAndroid::RecomputeScrollbarParams() {
+ uint32_t defaultSize = 6;
+ uint32_t overrideSize =
+ StaticPrefs::widget_non_native_theme_scrollbar_size_override();
+ if (overrideSize > 0) {
+ defaultSize = overrideSize;
+ }
+ ConfigureScrollbarSize(defaultSize);
+ // We make thin scrollbars as wide as auto ones because auto scrollbars on
+ // android are already thin enough.
+ ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes, defaultSize);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::No, defaultSize);
+}
diff --git a/widget/ScrollbarDrawingAndroid.h b/widget/ScrollbarDrawingAndroid.h
new file mode 100644
index 0000000000..8986a6d70a
--- /dev/null
+++ b/widget/ScrollbarDrawingAndroid.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_widget_ScrollbarDrawingAndroid_h
+#define mozilla_widget_ScrollbarDrawingAndroid_h
+
+#include "nsITheme.h"
+#include "ScrollbarDrawing.h"
+
+namespace mozilla::widget {
+
+class ScrollbarDrawingAndroid final : public ScrollbarDrawing {
+ public:
+ ScrollbarDrawingAndroid() : ScrollbarDrawing(Kind::Android) {}
+ virtual ~ScrollbarDrawingAndroid() = default;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*,
+ StyleAppearance aAppearance,
+ nsIFrame* aFrame) override;
+
+ template <typename PaintBackendData>
+ void DoPaintScrollbarThumb(PaintBackendData&, const LayoutDeviceRect& aRect,
+ ScrollbarKind, nsIFrame* aFrame,
+ const ComputedStyle& aStyle,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&);
+ bool PaintScrollbarThumb(DrawTarget&, const LayoutDeviceRect& aRect,
+ ScrollbarKind, nsIFrame* aFrame,
+ const ComputedStyle& aStyle,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+ bool PaintScrollbarThumb(WebRenderBackendData&, const LayoutDeviceRect& aRect,
+ ScrollbarKind, nsIFrame* aFrame,
+ const ComputedStyle& aStyle,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+
+ void RecomputeScrollbarParams() override;
+
+ bool ShouldDrawScrollbarButtons() override { return false; }
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ScrollbarDrawingCocoa.cpp b/widget/ScrollbarDrawingCocoa.cpp
new file mode 100644
index 0000000000..cf9f45d892
--- /dev/null
+++ b/widget/ScrollbarDrawingCocoa.cpp
@@ -0,0 +1,476 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* 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 "ScrollbarDrawingCocoa.h"
+
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsContainerFrame.h"
+#include "nsAlgorithm.h"
+#include "nsIFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsLookAndFeel.h"
+#include "nsNativeTheme.h"
+
+using namespace mozilla::gfx;
+namespace mozilla::widget {
+
+using ScrollbarKind = ScrollbarDrawing::ScrollbarKind;
+
+struct ColoredRect {
+ LayoutDeviceRect mRect;
+ nscolor mColor = 0;
+};
+
+// The caller can draw this rectangle with rounded corners as appropriate.
+struct ThumbRect {
+ LayoutDeviceRect mRect;
+ nscolor mFillColor = 0;
+ nscolor mStrokeColor = 0;
+ float mStrokeWidth = 0.0f;
+ float mStrokeOutset = 0.0f;
+};
+
+using ScrollbarTrackRects = Array<ColoredRect, 4>;
+using ScrollCornerRects = Array<ColoredRect, 7>;
+
+struct ScrollbarParams {
+ bool isOverlay = false;
+ bool isRolledOver = false;
+ bool isSmall = false;
+ bool isHorizontal = false;
+ bool isRtl = false;
+ bool isDark = false;
+ bool isCustom = false;
+ // Two colors only used when custom is true.
+ nscolor trackColor = NS_RGBA(0, 0, 0, 0);
+ nscolor faceColor = NS_RGBA(0, 0, 0, 0);
+};
+
+static ScrollbarParams ComputeScrollbarParams(nsIFrame* aFrame,
+ const ComputedStyle& aStyle,
+ const ThemeColors& aColors,
+ ScrollbarKind aScrollbarKind) {
+ ScrollbarParams params;
+ params.isOverlay =
+ nsLookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0;
+ params.isRolledOver = ScrollbarDrawing::IsParentScrollbarRolledOver(aFrame);
+ params.isSmall =
+ aStyle.StyleUIReset()->ScrollbarWidth() == StyleScrollbarWidth::Thin;
+ params.isRtl = aScrollbarKind == ScrollbarKind::VerticalLeft;
+ params.isHorizontal = aScrollbarKind == ScrollbarKind::Horizontal;
+ params.isDark = aColors.IsDark();
+
+ const nsStyleUI* ui = aStyle.StyleUI();
+ if (ui->HasCustomScrollbars()) {
+ const auto& colors = ui->mScrollbarColor.AsColors();
+ params.isCustom = true;
+ params.trackColor = colors.track.CalcColor(aStyle);
+ params.faceColor = colors.thumb.CalcColor(aStyle);
+ }
+
+ return params;
+}
+
+LayoutDeviceIntSize ScrollbarDrawingCocoa::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, StyleAppearance aAppearance,
+ nsIFrame* aFrame) {
+ MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
+
+ auto minSize = [&]() -> CSSIntSize {
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ return {26, 0};
+ case StyleAppearance::ScrollbarthumbVertical:
+ return {0, 26};
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbartrackVertical:
+ case StyleAppearance::ScrollbartrackHorizontal: {
+ ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame);
+ auto scrollbarWidth = style->StyleUIReset()->ScrollbarWidth();
+ auto size = GetCSSScrollbarSize(
+ scrollbarWidth, Overlay(aPresContext->UseOverlayScrollbars()));
+ return {size, size};
+ }
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ return {15, 16};
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ return {16, 15};
+ default:
+ return {};
+ }
+ }();
+
+ auto dpi = GetDPIRatioForScrollbarPart(aPresContext);
+ return LayoutDeviceIntSize::Round(CSSSize(minSize) * dpi);
+}
+
+static ThumbRect GetThumbRect(const LayoutDeviceRect& aRect,
+ const ScrollbarParams& aParams, float aScale) {
+ // Compute the thumb thickness. This varies based on aParams.small,
+ // aParams.overlay and aParams.rolledOver. non-overlay: 6 / 8, overlay
+ // non-hovered: 5 / 7, overlay hovered: 9 / 11
+ float thickness = aParams.isSmall ? 6.0f : 8.0f;
+ if (aParams.isOverlay) {
+ thickness -= 1.0f;
+ if (aParams.isRolledOver) {
+ thickness += 4.0f;
+ }
+ }
+ thickness *= aScale;
+
+ // Compute the thumb rect.
+ const float outerSpacing =
+ ((aParams.isOverlay || aParams.isSmall) ? 1.0f : 2.0f) * aScale;
+ LayoutDeviceRect thumbRect = aRect;
+ thumbRect.Deflate(1.0f * aScale);
+ if (aParams.isHorizontal) {
+ float bottomEdge = thumbRect.YMost() - outerSpacing;
+ thumbRect.SetBoxY(bottomEdge - thickness, bottomEdge);
+ } else {
+ if (aParams.isRtl) {
+ float leftEdge = thumbRect.X() + outerSpacing;
+ thumbRect.SetBoxX(leftEdge, leftEdge + thickness);
+ } else {
+ float rightEdge = thumbRect.XMost() - outerSpacing;
+ thumbRect.SetBoxX(rightEdge - thickness, rightEdge);
+ }
+ }
+
+ // Compute the thumb fill color.
+ nscolor faceColor;
+ if (aParams.isCustom) {
+ faceColor = aParams.faceColor;
+ } else {
+ if (aParams.isOverlay) {
+ faceColor =
+ aParams.isDark ? NS_RGBA(255, 255, 255, 128) : NS_RGBA(0, 0, 0, 128);
+ } else if (aParams.isDark) {
+ faceColor = aParams.isRolledOver ? NS_RGBA(158, 158, 158, 255)
+ : NS_RGBA(117, 117, 117, 255);
+ } else {
+ faceColor = aParams.isRolledOver ? NS_RGBA(125, 125, 125, 255)
+ : NS_RGBA(194, 194, 194, 255);
+ }
+ }
+
+ nscolor strokeColor = 0;
+ float strokeOutset = 0.0f;
+ float strokeWidth = 0.0f;
+
+ // Overlay scrollbars have an additional stroke around the fill.
+ if (aParams.isOverlay) {
+ // For the default alpha of 128 we want to end up with 48 in the outline.
+ constexpr float kAlphaScaling = 48.0f / 128.0f;
+ const uint8_t strokeAlpha =
+ uint8_t(clamped(NS_GET_A(faceColor) * kAlphaScaling, 0.0f, 48.0f));
+ if (strokeAlpha) {
+ strokeOutset = (aParams.isDark ? 0.3f : 0.5f) * aScale;
+ strokeWidth = (aParams.isDark ? 0.6f : 0.8f) * aScale;
+
+ strokeColor = aParams.isDark ? NS_RGBA(0, 0, 0, strokeAlpha)
+ : NS_RGBA(255, 255, 255, strokeAlpha);
+ }
+ }
+
+ return {thumbRect, faceColor, strokeColor, strokeWidth, strokeOutset};
+}
+
+struct ScrollbarTrackDecorationColors {
+ nscolor mInnerColor = 0;
+ nscolor mShadowColor = 0;
+ nscolor mOuterColor = 0;
+};
+
+static ScrollbarTrackDecorationColors ComputeScrollbarTrackDecorationColors(
+ nscolor aTrackColor) {
+ ScrollbarTrackDecorationColors result;
+ float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
+ if (luminance >= 0.5f) {
+ result.mInnerColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.836f);
+ result.mShadowColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.982f);
+ result.mOuterColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.886f);
+ } else {
+ result.mInnerColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.196f);
+ result.mShadowColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.018f);
+ result.mOuterColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.129f);
+ }
+ return result;
+}
+
+static bool GetScrollbarTrackRects(const LayoutDeviceRect& aRect,
+ const ScrollbarParams& aParams, float aScale,
+ ScrollbarTrackRects& aRects) {
+ if (aParams.isOverlay && !aParams.isRolledOver) {
+ // Non-hovered overlay scrollbars don't have a track. Draw nothing.
+ return false;
+ }
+
+ nscolor trackColor;
+ if (aParams.isCustom) {
+ trackColor = aParams.trackColor;
+ } else {
+ if (aParams.isOverlay) {
+ trackColor = aParams.isDark ? NS_RGBA(201, 201, 201, 38)
+ : NS_RGBA(250, 250, 250, 191);
+ } else {
+ trackColor = aParams.isDark ? NS_RGBA(46, 46, 46, 255)
+ : NS_RGBA(250, 250, 250, 255);
+ }
+ }
+
+ float thickness = aParams.isHorizontal ? aRect.height : aRect.width;
+
+ // The scrollbar track is drawn as multiple non-overlapping segments, which
+ // make up lines of different widths and with slightly different shading.
+ ScrollbarTrackDecorationColors colors =
+ ComputeScrollbarTrackDecorationColors(trackColor);
+ struct {
+ nscolor color;
+ float thickness;
+ } segments[] = {
+ {colors.mInnerColor, 1.0f * aScale},
+ {colors.mShadowColor, 1.0f * aScale},
+ {trackColor, thickness - 3.0f * aScale},
+ {colors.mOuterColor, 1.0f * aScale},
+ };
+
+ // Iterate over the segments "from inside to outside" and fill each segment.
+ // For horizontal scrollbars, iterate top to bottom.
+ // For vertical scrollbars, iterate left to right or right to left based on
+ // aParams.isRtl.
+ auto current = aRects.begin();
+ float accumulatedThickness = 0.0f;
+ for (const auto& segment : segments) {
+ LayoutDeviceRect segmentRect = aRect;
+ float startThickness = accumulatedThickness;
+ float endThickness = startThickness + segment.thickness;
+ if (aParams.isHorizontal) {
+ segmentRect.SetBoxY(aRect.Y() + startThickness, aRect.Y() + endThickness);
+ } else {
+ if (aParams.isRtl) {
+ segmentRect.SetBoxX(aRect.XMost() - endThickness,
+ aRect.XMost() - startThickness);
+ } else {
+ segmentRect.SetBoxX(aRect.X() + startThickness,
+ aRect.X() + endThickness);
+ }
+ }
+ accumulatedThickness = endThickness;
+ *current++ = {segmentRect, segment.color};
+ }
+
+ return true;
+}
+
+static bool GetScrollCornerRects(const LayoutDeviceRect& aRect,
+ const ScrollbarParams& aParams, float aScale,
+ ScrollCornerRects& aRects) {
+ if (aParams.isOverlay && !aParams.isRolledOver) {
+ // Non-hovered overlay scrollbars don't have a corner. Draw nothing.
+ return false;
+ }
+
+ // Draw the following scroll corner.
+ //
+ // Output: Rectangles:
+ // +---+---+----------+---+ +---+---+----------+---+
+ // | I | S | T ... T | O | | I | S | T ... T | O |
+ // +---+ | | | +---+---+ | |
+ // | S S | T ... T | | | S S | T ... T | . |
+ // +-------+ | . | +-------+----------+ . |
+ // | T ... T | . | | T ... T | . |
+ // | . . | . | | . . | |
+ // | T ... T | | | T ... T | O |
+ // +------------------+ | +------------------+---+
+ // | O ... O | | O ... O |
+ // +----------------------+ +----------------------+
+
+ float width = aRect.width;
+ float height = aRect.height;
+ nscolor trackColor;
+ if (aParams.isCustom) {
+ trackColor = aParams.trackColor;
+ } else {
+ trackColor =
+ aParams.isDark ? NS_RGBA(46, 46, 46, 255) : NS_RGBA(250, 250, 250, 255);
+ }
+ ScrollbarTrackDecorationColors colors =
+ ComputeScrollbarTrackDecorationColors(trackColor);
+ struct {
+ nscolor color;
+ LayoutDeviceRect relativeRect;
+ } pieces[] = {
+ {colors.mInnerColor, {0.0f, 0.0f, 1.0f * aScale, 1.0f * aScale}},
+ {colors.mShadowColor,
+ {1.0f * aScale, 0.0f, 1.0f * aScale, 1.0f * aScale}},
+ {colors.mShadowColor,
+ {0.0f, 1.0f * aScale, 2.0f * aScale, 1.0f * aScale}},
+ {trackColor, {2.0f * aScale, 0.0f, width - 3.0f * aScale, 2.0f * aScale}},
+ {trackColor,
+ {0.0f, 2.0f * aScale, width - 1.0f * aScale, height - 3.0f * aScale}},
+ {colors.mOuterColor,
+ {width - 1.0f * aScale, 0.0f, 1.0f * aScale, height - 1.0f * aScale}},
+ {colors.mOuterColor,
+ {0.0f, height - 1.0f * aScale, width, 1.0f * aScale}},
+ };
+
+ auto current = aRects.begin();
+ for (const auto& piece : pieces) {
+ LayoutDeviceRect pieceRect = piece.relativeRect + aRect.TopLeft();
+ if (aParams.isRtl) {
+ pieceRect.x = aRect.XMost() - piece.relativeRect.XMost();
+ }
+ *current++ = {pieceRect, piece.color};
+ }
+ return true;
+}
+
+template <typename PaintBackendData>
+void ScrollbarDrawingCocoa::DoPaintScrollbarThumb(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ ScrollbarParams params =
+ ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
+ auto thumb = GetThumbRect(aRect, params, aDpiRatio.scale);
+ LayoutDeviceCoord radius =
+ (params.isHorizontal ? thumb.mRect.Height() : thumb.mRect.Width()) / 2.0f;
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aPaintData, thumb.mRect, thumb.mRect,
+ sRGBColor::FromABGR(thumb.mFillColor), sRGBColor::White(0.0f), 0.0f,
+ radius / aDpiRatio, aDpiRatio);
+ if (!thumb.mStrokeColor) {
+ return;
+ }
+
+ // Paint the stroke if needed.
+ auto strokeRect = thumb.mRect;
+ strokeRect.Inflate(thumb.mStrokeOutset + thumb.mStrokeWidth);
+ radius =
+ (params.isHorizontal ? strokeRect.Height() : strokeRect.Width()) / 2.0f;
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aPaintData, strokeRect, sRGBColor::White(0.0f),
+ sRGBColor::FromABGR(thumb.mStrokeColor), thumb.mStrokeWidth,
+ radius / aDpiRatio, aDpiRatio);
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
+ DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarThumb(aDt, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+template <typename PaintBackendData>
+void ScrollbarDrawingCocoa::DoPaintScrollbarTrack(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ ScrollbarParams params =
+ ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
+ ScrollbarTrackRects rects;
+ if (GetScrollbarTrackRects(aRect, params, aDpiRatio.scale, rects)) {
+ for (const auto& rect : rects) {
+ ThemeDrawing::FillRect(aPaintData, rect.mRect,
+ sRGBColor::FromABGR(rect.mColor));
+ }
+ }
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
+ DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarTrack(aDt, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarTrack(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+template <typename PaintBackendData>
+void ScrollbarDrawingCocoa::DoPaintScrollCorner(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ ScrollbarParams params =
+ ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
+ ScrollCornerRects rects;
+ if (GetScrollCornerRects(aRect, params, aDpiRatio.scale, rects)) {
+ for (const auto& rect : rects) {
+ ThemeDrawing::FillRect(aPaintData, rect.mRect,
+ sRGBColor::FromABGR(rect.mColor));
+ }
+ }
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollCorner(
+ DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollCorner(aDt, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollCorner(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollCorner(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+void ScrollbarDrawingCocoa::RecomputeScrollbarParams() {
+ // FIXME(emilio): This doesn't respect the
+ // StaticPrefs::widget_non_native_theme_scrollbar_size_override() pref;
+ ConfigureScrollbarSize(15); // Just in case, for future-proofing
+ ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::No, 15);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::No, 11);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes, 16);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes, 14);
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ScrollbarDrawingCocoa.h b/widget/ScrollbarDrawingCocoa.h
new file mode 100644
index 0000000000..840da6c232
--- /dev/null
+++ b/widget/ScrollbarDrawingCocoa.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_ScrollbarDrawingCocoa_h
+#define mozilla_widget_ScrollbarDrawingCocoa_h
+
+#include "ScrollbarDrawing.h"
+
+#include "mozilla/Array.h"
+
+namespace mozilla::widget {
+
+class ScrollbarDrawingCocoa final : public ScrollbarDrawing {
+ public:
+ ScrollbarDrawingCocoa() : ScrollbarDrawing(Kind::Cocoa) {}
+ virtual ~ScrollbarDrawingCocoa() = default;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*,
+ StyleAppearance aAppearance,
+ nsIFrame* aFrame) override;
+
+ static CSSIntCoord GetScrollbarSize(StyleScrollbarWidth, bool aOverlay);
+
+ template <typename PaintBackendData>
+ void DoPaintScrollbarThumb(PaintBackendData&, const LayoutDeviceRect& aRect,
+ ScrollbarKind, nsIFrame* aFrame,
+ const ComputedStyle& aStyle, const ElementState&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&);
+ bool PaintScrollbarThumb(DrawTarget&, const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&, const ElementState&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&) override;
+ bool PaintScrollbarThumb(WebRenderBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState&, const DocumentState&,
+ const Colors&, const DPIRatio&) override;
+
+ template <typename PaintBackendData>
+ void DoPaintScrollbarTrack(PaintBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&);
+ bool PaintScrollbarTrack(DrawTarget&, const LayoutDeviceRect& aRect,
+ ScrollbarKind, nsIFrame* aFrame,
+ const ComputedStyle& aStyle, const DocumentState&,
+ const Colors&, const DPIRatio&) override;
+ bool PaintScrollbarTrack(WebRenderBackendData&, const LayoutDeviceRect& aRect,
+ ScrollbarKind, nsIFrame* aFrame,
+ const ComputedStyle& aStyle, const DocumentState&,
+ const Colors&, const DPIRatio&) override;
+
+ template <typename PaintBackendData>
+ void DoPaintScrollCorner(PaintBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&);
+ bool PaintScrollCorner(DrawTarget&, const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&, const DocumentState&,
+ const Colors&, const DPIRatio&) override;
+ bool PaintScrollCorner(WebRenderBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const DocumentState&, const Colors&,
+ const DPIRatio&) override;
+
+ void RecomputeScrollbarParams() override;
+
+ bool ShouldDrawScrollbarButtons() override { return false; }
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ScrollbarDrawingGTK.cpp b/widget/ScrollbarDrawingGTK.cpp
new file mode 100644
index 0000000000..3e0cad9179
--- /dev/null
+++ b/widget/ScrollbarDrawingGTK.cpp
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 40; 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 "ScrollbarDrawingGTK.h"
+
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsLayoutUtils.h"
+#include "nsNativeTheme.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+
+LayoutDeviceIntSize ScrollbarDrawingGTK::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, StyleAppearance aAppearance,
+ nsIFrame* aFrame) {
+ MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
+ auto scrollbarSize = GetScrollbarSize(aPresContext, aFrame);
+ LayoutDeviceIntSize size{scrollbarSize, scrollbarSize};
+ if (aAppearance == StyleAppearance::ScrollbarHorizontal ||
+ aAppearance == StyleAppearance::ScrollbarVertical ||
+ aAppearance == StyleAppearance::ScrollbarthumbHorizontal ||
+ aAppearance == StyleAppearance::ScrollbarthumbVertical) {
+ CSSCoord thumbSize(
+ StaticPrefs::widget_non_native_theme_gtk_scrollbar_thumb_cross_size());
+ const bool isVertical =
+ aAppearance == StyleAppearance::ScrollbarVertical ||
+ aAppearance == StyleAppearance::ScrollbarthumbVertical;
+ auto dpi = GetDPIRatioForScrollbarPart(aPresContext);
+ if (isVertical) {
+ size.height = thumbSize * dpi;
+ } else {
+ size.width = thumbSize * dpi;
+ }
+ }
+ return size;
+}
+
+Maybe<nsITheme::Transparency> ScrollbarDrawingGTK::GetScrollbarPartTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (!aFrame->PresContext()->UseOverlayScrollbars() &&
+ (aAppearance == StyleAppearance::ScrollbarVertical ||
+ aAppearance == StyleAppearance::ScrollbarHorizontal) &&
+ IsScrollbarTrackOpaque(aFrame)) {
+ return Some(nsITheme::eOpaque);
+ }
+
+ return Nothing();
+}
+
+template <typename PaintBackendData>
+bool ScrollbarDrawingGTK::DoPaintScrollbarThumb(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ sRGBColor thumbColor = ComputeScrollbarThumbColor(
+ aFrame, aStyle, aElementState, aDocumentState, aColors);
+
+ LayoutDeviceRect thumbRect(aRect);
+
+ const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal;
+ if (aFrame->PresContext()->UseOverlayScrollbars() &&
+ !ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame)) {
+ if (horizontal) {
+ thumbRect.height *= 0.5;
+ thumbRect.y += thumbRect.height;
+ } else {
+ thumbRect.width *= 0.5;
+ if (aScrollbarKind == ScrollbarKind::VerticalRight) {
+ thumbRect.x += thumbRect.width;
+ }
+ }
+ }
+
+ {
+ float factor = std::max(
+ 0.0f,
+ 1.0f - StaticPrefs::widget_non_native_theme_gtk_scrollbar_thumb_size());
+ thumbRect.Deflate((horizontal ? thumbRect.height : thumbRect.width) *
+ factor);
+ }
+
+ LayoutDeviceCoord radius =
+ StaticPrefs::widget_non_native_theme_gtk_scrollbar_round_thumb()
+ ? (horizontal ? thumbRect.height : thumbRect.width) / 2.0f
+ : 0.0f;
+
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, thumbColor,
+ sRGBColor(), 0, radius / aDpiRatio,
+ aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingGTK::PaintScrollbarThumb(
+ DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintScrollbarThumb(aDrawTarget, aRect, aScrollbarKind, aFrame,
+ aStyle, aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+bool ScrollbarDrawingGTK::PaintScrollbarThumb(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+bool ScrollbarDrawingGTK::ShouldDrawScrollbarButtons() {
+ if (StaticPrefs::widget_non_native_theme_enabled()) {
+ return StaticPrefs::widget_non_native_theme_gtk_scrollbar_allow_buttons();
+ }
+ return true;
+}
+
+void ScrollbarDrawingGTK::RecomputeScrollbarParams() {
+ uint32_t defaultSize = 12;
+ uint32_t overrideSize =
+ StaticPrefs::widget_non_native_theme_scrollbar_size_override();
+ if (overrideSize > 0) {
+ defaultSize = overrideSize;
+ }
+ ConfigureScrollbarSize(defaultSize);
+}
diff --git a/widget/ScrollbarDrawingGTK.h b/widget/ScrollbarDrawingGTK.h
new file mode 100644
index 0000000000..162997e127
--- /dev/null
+++ b/widget/ScrollbarDrawingGTK.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_widget_ScrollbarDrawingGTK_h
+#define mozilla_widget_ScrollbarDrawingGTK_h
+
+#include "nsITheme.h"
+#include "nsNativeTheme.h"
+#include "ScrollbarDrawing.h"
+
+namespace mozilla::widget {
+
+class ScrollbarDrawingGTK final : public ScrollbarDrawing {
+ public:
+ ScrollbarDrawingGTK() : ScrollbarDrawing(Kind::Gtk) {}
+ virtual ~ScrollbarDrawingGTK() = default;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*,
+ StyleAppearance aAppearance,
+ nsIFrame* aFrame) override;
+
+ Maybe<nsITheme::Transparency> GetScrollbarPartTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) override;
+
+ template <typename PaintBackendData>
+ bool DoPaintScrollbarThumb(PaintBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&);
+ bool PaintScrollbarThumb(DrawTarget&, const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle& aStyle,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+ bool PaintScrollbarThumb(WebRenderBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*,
+ const ComputedStyle& aStyle,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+
+ void RecomputeScrollbarParams() override;
+
+ bool ShouldDrawScrollbarButtons() override;
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ScrollbarDrawingWin.cpp b/widget/ScrollbarDrawingWin.cpp
new file mode 100644
index 0000000000..c2d85ae671
--- /dev/null
+++ b/widget/ScrollbarDrawingWin.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 40; 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 "ScrollbarDrawingWin.h"
+
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsLayoutUtils.h"
+#include "Theme.h"
+#include "nsNativeTheme.h"
+
+namespace mozilla::widget {
+
+LayoutDeviceIntSize ScrollbarDrawingWin::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, StyleAppearance aAppearance,
+ nsIFrame* aFrame) {
+ MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
+
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ // For scrollbar-width:thin, we don't display the buttons.
+ if (IsScrollbarWidthThin(aFrame)) {
+ return LayoutDeviceIntSize{};
+ }
+ [[fallthrough]];
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbarthumbVertical:
+ case StyleAppearance::ScrollbarthumbHorizontal: {
+ // TODO: for short scrollbars it could be nice if the thumb could shrink
+ // under this size.
+ auto relevantSize = GetScrollbarSize(aPresContext, aFrame);
+ const bool isHorizontal =
+ aAppearance == StyleAppearance::ScrollbarHorizontal ||
+ aAppearance == StyleAppearance::ScrollbarthumbHorizontal ||
+ aAppearance == StyleAppearance::ScrollbarbuttonLeft ||
+ aAppearance == StyleAppearance::ScrollbarbuttonRight;
+ auto size = LayoutDeviceIntSize{relevantSize, relevantSize};
+ if (aAppearance == StyleAppearance::ScrollbarHorizontal ||
+ aAppearance == StyleAppearance::ScrollbarVertical) {
+ // Always reserve some space in the right direction. Historically we've
+ // reserved 2 times the size in the other axis (for the buttons).
+ // We do this even when painting thin scrollbars just for consistency,
+ // though there just one would probably do there.
+ if (isHorizontal) {
+ size.width *= 2;
+ } else {
+ size.height *= 2;
+ }
+ }
+ return size;
+ }
+ default:
+ return LayoutDeviceIntSize{};
+ }
+}
+
+// Returns the style for custom scrollbar if the scrollbar part frame should
+// use the custom drawing path, nullptr otherwise.
+const ComputedStyle* GetCustomScrollbarStyle(nsIFrame* aFrame) {
+ const ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame);
+ if (style->StyleUI()->HasCustomScrollbars() ||
+ ScrollbarDrawing::IsScrollbarWidthThin(*style)) {
+ return style;
+ }
+ bool useDarkScrollbar = !StaticPrefs::widget_disable_dark_scrollbar() &&
+ nsNativeTheme::IsDarkBackgroundForScrollbar(aFrame);
+ if (useDarkScrollbar) {
+ return style;
+ }
+ return nullptr;
+}
+
+Maybe<nsITheme::Transparency> ScrollbarDrawingWin::GetScrollbarPartTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (nsNativeTheme::IsWidgetScrollbarPart(aAppearance)) {
+ if (const ComputedStyle* style = GetCustomScrollbarStyle(aFrame)) {
+ auto* ui = style->StyleUI();
+ if (ui->mScrollbarColor.IsAuto() ||
+ ui->mScrollbarColor.AsColors().track.MaybeTransparent()) {
+ return Some(nsITheme::eTransparent);
+ }
+ // These widgets may be thinner than the track, so we need to return
+ // transparent for them to make the track visible.
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ case StyleAppearance::ScrollbarthumbVertical:
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ return Some(nsITheme::eTransparent);
+ default:
+ break;
+ }
+ }
+ if (aFrame->PresContext()->UseOverlayScrollbars()) {
+ return Some(nsITheme::eTransparent);
+ }
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::Scrollcorner:
+ // Knowing that scrollbars and statusbars are opaque improves
+ // performance, because we create layers for them. This better be
+ // true across all Windows themes! If it's not true, we should
+ // paint an opaque background for them to make it true!
+ // TODO(emilio): Unclear how much this optimization matters in practice
+ // now we're in a WR-only world.
+ return Some(nsITheme::eOpaque);
+ default:
+ break;
+ }
+
+ return Nothing();
+}
+
+template <typename PaintBackendData>
+bool ScrollbarDrawingWin::DoPaintScrollbarThumb(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ sRGBColor thumbColor = ComputeScrollbarThumbColor(
+ aFrame, aStyle, aElementState, aDocumentState, aColors);
+ ThemeDrawing::FillRect(aPaintData, aRect, thumbColor);
+ return true;
+}
+
+bool ScrollbarDrawingWin::PaintScrollbarThumb(
+ DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintScrollbarThumb(aDrawTarget, aRect, aScrollbarKind, aFrame,
+ aStyle, aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+bool ScrollbarDrawingWin::PaintScrollbarThumb(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+void ScrollbarDrawingWin::RecomputeScrollbarParams() {
+ uint32_t defaultSize = kDefaultWinScrollbarSize;
+ uint32_t overrideSize =
+ StaticPrefs::widget_non_native_theme_scrollbar_size_override();
+ if (overrideSize > 0) {
+ defaultSize = overrideSize;
+ }
+ ConfigureScrollbarSize(defaultSize);
+
+ if (StaticPrefs::widget_non_native_theme_win_scrollbar_use_system_size()) {
+ ConfigureScrollbarSize(LookAndFeel::GetInt(
+ LookAndFeel::IntID::SystemScrollbarSize, defaultSize));
+ }
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ScrollbarDrawingWin.h b/widget/ScrollbarDrawingWin.h
new file mode 100644
index 0000000000..adc79035c2
--- /dev/null
+++ b/widget/ScrollbarDrawingWin.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_widget_ScrollbarDrawingWin_h
+#define mozilla_widget_ScrollbarDrawingWin_h
+
+#include "nsITheme.h"
+#include "nsNativeTheme.h"
+#include "ScrollbarDrawing.h"
+
+namespace mozilla::widget {
+
+class ScrollbarDrawingWin : public ScrollbarDrawing {
+ protected:
+ explicit ScrollbarDrawingWin(Kind aKind) : ScrollbarDrawing(aKind) {}
+
+ public:
+ ScrollbarDrawingWin() : ScrollbarDrawingWin(Kind::Win10) {}
+
+ virtual ~ScrollbarDrawingWin() = default;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*,
+ StyleAppearance aAppearance,
+ nsIFrame* aFrame) override;
+
+ Maybe<nsITheme::Transparency> GetScrollbarPartTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) override;
+
+ template <typename PaintBackendData>
+ bool DoPaintScrollbarThumb(PaintBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&);
+ bool PaintScrollbarThumb(DrawTarget&, const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+ bool PaintScrollbarThumb(WebRenderBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+
+ void RecomputeScrollbarParams() override;
+};
+
+static constexpr uint32_t kDefaultWinScrollbarSize = 17;
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ScrollbarDrawingWin11.cpp b/widget/ScrollbarDrawingWin11.cpp
new file mode 100644
index 0000000000..8517b60baa
--- /dev/null
+++ b/widget/ScrollbarDrawingWin11.cpp
@@ -0,0 +1,364 @@
+/* -*- Mode: C++; tab-width: 40; 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 "ScrollbarDrawingWin11.h"
+
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsLayoutUtils.h"
+#include "Theme.h"
+#include "nsNativeTheme.h"
+
+using mozilla::gfx::sRGBColor;
+
+namespace mozilla::widget {
+
+// There are effectively three kinds of scrollbars in Windows 11:
+//
+// * Overlay scrollbars (the ones where the scrollbar disappears automatically
+// and doesn't take space)
+// * Non-overlay scrollbar with thin (overlay-like) thumb.
+// * Non-overlay scrollbar with thick thumb.
+//
+// See bug 1755193 for some discussion on non-overlay scrollbar styles.
+enum class Style {
+ Overlay,
+ ThinThumb,
+ ThickThumb,
+};
+
+static Style ScrollbarStyle(nsPresContext* aPresContext) {
+ if (aPresContext->UseOverlayScrollbars()) {
+ return Style::Overlay;
+ }
+ if (StaticPrefs::
+ widget_non_native_theme_win11_scrollbar_force_overlay_style()) {
+ return Style::ThinThumb;
+ }
+ return Style::ThickThumb;
+}
+
+static constexpr CSSIntCoord kDefaultWinOverlayScrollbarSize = CSSIntCoord(12);
+static constexpr CSSIntCoord kDefaultWinOverlayThinScrollbarSize =
+ CSSIntCoord(10);
+
+LayoutDeviceIntSize ScrollbarDrawingWin11::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, StyleAppearance aAppearance,
+ nsIFrame* aFrame) {
+ MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
+ if (ScrollbarStyle(aPresContext) != Style::ThinThumb) {
+ return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext, aAppearance,
+ aFrame);
+ }
+ constexpr float kArrowRatio = 14.0f / kDefaultWinScrollbarSize;
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight: {
+ if (IsScrollbarWidthThin(aFrame)) {
+ return {};
+ }
+ const LayoutDeviceIntCoord size =
+ ScrollbarDrawing::GetScrollbarSize(aPresContext, aFrame);
+ return LayoutDeviceIntSize{
+ size, (kArrowRatio * LayoutDeviceCoord(size)).Rounded()};
+ }
+ default:
+ return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext,
+ aAppearance, aFrame);
+ }
+}
+
+sRGBColor ScrollbarDrawingWin11::ComputeScrollbarTrackColor(
+ nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return ScrollbarDrawingWin::ComputeScrollbarTrackColor(
+ aFrame, aStyle, aDocumentState, aColors);
+ }
+ const nsStyleUI* ui = aStyle.StyleUI();
+ if (ui->mScrollbarColor.IsColors()) {
+ return sRGBColor::FromABGR(
+ ui->mScrollbarColor.AsColors().track.CalcColor(aStyle));
+ }
+ return aColors.IsDark() ? sRGBColor::FromU8(23, 23, 23, 255)
+ : sRGBColor::FromU8(240, 240, 240, 255);
+}
+
+sRGBColor ScrollbarDrawingWin11::ComputeScrollbarThumbColor(
+ nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return ScrollbarDrawingWin::ComputeScrollbarThumbColor(
+ aFrame, aStyle, aElementState, aDocumentState, aColors);
+ }
+ const nscolor baseColor = [&] {
+ const nsStyleUI* ui = aStyle.StyleUI();
+ if (ui->mScrollbarColor.IsColors()) {
+ return ui->mScrollbarColor.AsColors().thumb.CalcColor(aStyle);
+ }
+ return aColors.IsDark() ? NS_RGBA(149, 149, 149, 255)
+ : NS_RGBA(133, 133, 133, 255);
+ }();
+ ElementState state = aElementState;
+ if (!IsScrollbarWidthThin(aStyle)) {
+ // non-thin scrollbars get hover feedback by changing thumb shape, so we
+ // only provide active feedback (and we use the hover state for that as it's
+ // more subtle).
+ state &= ~ElementState::HOVER;
+ if (state.HasState(ElementState::ACTIVE)) {
+ state &= ~ElementState::ACTIVE;
+ state |= ElementState::HOVER;
+ }
+ }
+ return sRGBColor::FromABGR(
+ ThemeColors::AdjustUnthemedScrollbarThumbColor(baseColor, state));
+}
+
+std::pair<sRGBColor, sRGBColor>
+ScrollbarDrawingWin11::ComputeScrollbarButtonColors(
+ nsIFrame* aFrame, StyleAppearance aAppearance, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return ScrollbarDrawingWin::ComputeScrollbarButtonColors(
+ aFrame, aAppearance, aStyle, aElementState, aDocumentState, aColors);
+ }
+ // The button always looks transparent (the track behind it is visible), so we
+ // can hardcode it.
+ sRGBColor arrowColor = ComputeScrollbarThumbColor(
+ aFrame, aStyle, aElementState, aDocumentState, aColors);
+ return {sRGBColor::White(0.0f), arrowColor};
+}
+
+bool ScrollbarDrawingWin11::PaintScrollbarButton(
+ DrawTarget& aDrawTarget, StyleAppearance aAppearance,
+ const LayoutDeviceRect& aRect, ScrollbarKind aScrollbarKind,
+ nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ if (!ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame)) {
+ return true;
+ }
+
+ const auto style = ScrollbarStyle(aFrame->PresContext());
+ auto [buttonColor, arrowColor] = ComputeScrollbarButtonColors(
+ aFrame, aAppearance, aStyle, aElementState, aDocumentState, aColors);
+ if (style != Style::Overlay) {
+ aDrawTarget.FillRect(aRect.ToUnknownRect(),
+ gfx::ColorPattern(ToDeviceColor(buttonColor)));
+ }
+
+ // Start with Up arrow.
+ float arrowPolygonX[] = {-4.5f, 4.5f, 4.5f, 0.5f, -0.5f, -4.5f, -4.5f};
+ float arrowPolygonXActive[] = {-4.0f, 4.0f, 4.0f, -0.25f,
+ -0.25f, -4.0f, -4.0f};
+ float arrowPolygonXHover[] = {-5.0f, 5.0f, 5.0f, 0.75f, -0.75f, -5.0f, -5.0f};
+ float arrowPolygonY[] = {2.5f, 2.5f, 1.0f, -4.0f, -4.0f, 1.0f, 2.5f};
+ float arrowPolygonYActive[] = {2.0f, 2.0f, 0.5f, -3.5f, -3.5f, 0.5f, 2.0f};
+ float arrowPolygonYHover[] = {3.0f, 3.0f, 1.5f, -4.5f, -4.5f, 1.5f, 3.0f};
+ float* arrowX = arrowPolygonX;
+ float* arrowY = arrowPolygonY;
+ const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal;
+
+ const float verticalOffset = [&] {
+ if (style != Style::Overlay) {
+ return 0.0f;
+ }
+ // To compensate for the scrollbar track radius we shift stuff vertically a
+ // bit. This 1px is arbitrary, but enough for the triangle not to overflow.
+ return 1.0f;
+ }();
+ const float horizontalOffset = [&] {
+ if (style != Style::ThinThumb) {
+ return 0.0f; // Always center it in the rect.
+ }
+ // Compensate for the displacement we do of the thumb position by displacing
+ // the arrow as well, see comment in DoPaintScrollbarThumb.
+ if (horizontal) {
+ return -0.5f;
+ }
+ return aScrollbarKind == ScrollbarKind::VerticalRight ? 0.5f : -0.5f;
+ }();
+ const float polygonSize = style == Style::Overlay
+ ? float(kDefaultWinOverlayScrollbarSize)
+ : float(kDefaultWinScrollbarSize);
+ const int32_t arrowNumPoints = ArrayLength(arrowPolygonX);
+
+ if (aElementState.HasState(ElementState::ACTIVE)) {
+ arrowX = arrowPolygonXActive;
+ arrowY = arrowPolygonYActive;
+ } else if (aElementState.HasState(ElementState::HOVER)) {
+ arrowX = arrowPolygonXHover;
+ arrowY = arrowPolygonYHover;
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonRight:
+ for (int32_t i = 0; i < arrowNumPoints; i++) {
+ arrowY[i] += verticalOffset;
+ arrowY[i] *= -1;
+ }
+ [[fallthrough]];
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ if (horizontalOffset != 0.0f) {
+ for (int32_t i = 0; i < arrowNumPoints; i++) {
+ arrowX[i] += horizontalOffset;
+ }
+ }
+ break;
+ default:
+ return false;
+ }
+
+ if (horizontal) {
+ std::swap(arrowX, arrowY);
+ }
+
+ LayoutDeviceRect arrowRect(aRect);
+ if (style != Style::ThinThumb) {
+ auto margin = CSSCoord(style == Style::Overlay ? 1 : 2) * aDpiRatio;
+ arrowRect.Deflate(margin, margin);
+ }
+
+ ThemeDrawing::PaintArrow(aDrawTarget, arrowRect, arrowX, arrowY, polygonSize,
+ arrowNumPoints, arrowColor);
+ return true;
+}
+
+template <typename PaintBackendData>
+bool ScrollbarDrawingWin11::DoPaintScrollbarThumb(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ sRGBColor thumbColor = ComputeScrollbarThumbColor(
+ aFrame, aStyle, aElementState, aDocumentState, aColors);
+
+ LayoutDeviceRect thumbRect(aRect);
+
+ const auto style = ScrollbarStyle(aFrame->PresContext());
+ const bool hovered =
+ ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame) ||
+ (style != Style::Overlay && IsScrollbarWidthThin(aStyle));
+ const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal;
+ if (style == Style::ThickThumb) {
+ constexpr float kHoveredThumbRatio =
+ (1.0f - (11.0f / kDefaultWinScrollbarSize)) / 2.0f;
+ constexpr float kUnhoveredThumbRatio =
+ (1.0f - (9.0f / kDefaultWinScrollbarSize)) / 2.0f;
+ const float ratio = hovered ? kHoveredThumbRatio : kUnhoveredThumbRatio;
+ if (horizontal) {
+ thumbRect.Deflate(0, thumbRect.height * ratio);
+ } else {
+ thumbRect.Deflate(thumbRect.width * ratio, 0);
+ }
+
+ auto radius = CSSCoord(hovered ? 2 : 0);
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, thumbColor,
+ sRGBColor(), 0, radius, aDpiRatio);
+ return true;
+ }
+
+ const float defaultTrackSize = style == Style::Overlay
+ ? float(kDefaultWinOverlayScrollbarSize)
+ : float(kDefaultWinScrollbarSize);
+ const float trackSize = horizontal ? thumbRect.height : thumbRect.width;
+ const float thumbSizeInPixels = hovered ? 6.0f : 2.0f;
+
+ // The thumb might be a bit off-center, depending on our scrollbar styles.
+ //
+ // Hovered shifts, if any, need to be accounted for in PaintScrollbarButton.
+ // For example, for the hovered horizontal thin scrollbar shift:
+ //
+ // Scrollbar is 17px high by default. We make the thumb 6px tall and move
+ // it 5px towards the bottom, so the center (8.5 initially) is displaced
+ // by:
+ // (5px + 6px / 2) - 8.5px = -0.5px
+ //
+ // Same calculations apply to other shifts.
+ const float shiftInPixels = [&] {
+ if (style == Style::Overlay) {
+ if (hovered) {
+ // Keep the center intact.
+ return (defaultTrackSize - thumbSizeInPixels) / 2.0f;
+ }
+ // We want logical pixels from the thumb to the edge. For LTR and
+ // horizontal scrollbars that means shifting down the scrollbar size minus
+ // the thumb.
+ constexpr float kSpaceToEdge = 3.0f;
+ if (horizontal || aScrollbarKind == ScrollbarKind::VerticalRight) {
+ return defaultTrackSize - thumbSizeInPixels - kSpaceToEdge;
+ }
+ // For rtl is simpler.
+ return kSpaceToEdge;
+ }
+ if (horizontal) {
+ return hovered ? 5.0f : 7.0f;
+ }
+ const bool ltr = aScrollbarKind == ScrollbarKind::VerticalRight;
+ return ltr ? (hovered ? 6.0f : 8.0f) : (hovered ? 5.0f : 7.0f);
+ }();
+
+ if (horizontal) {
+ thumbRect.y += shiftInPixels * trackSize / defaultTrackSize;
+ thumbRect.height *= thumbSizeInPixels / defaultTrackSize;
+ } else {
+ thumbRect.x += shiftInPixels * trackSize / defaultTrackSize;
+ thumbRect.width *= thumbSizeInPixels / defaultTrackSize;
+ }
+
+ if (style == Style::Overlay || hovered) {
+ LayoutDeviceCoord radius =
+ (horizontal ? thumbRect.height : thumbRect.width) / 2.0f;
+
+ MOZ_ASSERT(aRect.Contains(thumbRect));
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, thumbColor,
+ sRGBColor(), 0, radius / aDpiRatio,
+ aDpiRatio);
+ return true;
+ }
+
+ ThemeDrawing::FillRect(aPaintData, thumbRect, thumbColor);
+ return true;
+}
+
+bool ScrollbarDrawingWin11::PaintScrollbarThumb(
+ DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintScrollbarThumb(aDrawTarget, aRect, aScrollbarKind, aFrame,
+ aStyle, aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+bool ScrollbarDrawingWin11::PaintScrollbarThumb(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ return DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors,
+ aDpiRatio);
+}
+
+void ScrollbarDrawingWin11::RecomputeScrollbarParams() {
+ ScrollbarDrawingWin::RecomputeScrollbarParams();
+ // TODO(emilio): Maybe make this configurable? Though this doesn't respect
+ // classic Windows registry settings, and cocoa overlay scrollbars also don't
+ // respect the override it seems, so this should be fine.
+ ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes,
+ kDefaultWinOverlayThinScrollbarSize);
+ ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes,
+ kDefaultWinOverlayScrollbarSize);
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ScrollbarDrawingWin11.h b/widget/ScrollbarDrawingWin11.h
new file mode 100644
index 0000000000..67d572d966
--- /dev/null
+++ b/widget/ScrollbarDrawingWin11.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_widget_ScrollbarDrawingWin11_h
+#define mozilla_widget_ScrollbarDrawingWin11_h
+
+#include "nsITheme.h"
+#include "nsNativeTheme.h"
+#include "ScrollbarDrawing.h"
+#include "ScrollbarDrawingWin.h"
+
+namespace mozilla::widget {
+
+class ScrollbarDrawingWin11 final : public ScrollbarDrawingWin {
+ public:
+ ScrollbarDrawingWin11() : ScrollbarDrawingWin(Kind::Win11) {}
+ virtual ~ScrollbarDrawingWin11() = default;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*,
+ StyleAppearance aAppearance,
+ nsIFrame* aFrame) override;
+
+ sRGBColor ComputeScrollbarTrackColor(nsIFrame*, const ComputedStyle&,
+ const DocumentState& aDocumentState,
+ const Colors&) override;
+ sRGBColor ComputeScrollbarThumbColor(nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState,
+ const Colors&) override;
+
+ // Returned colors are button, arrow.
+ std::pair<sRGBColor, sRGBColor> ComputeScrollbarButtonColors(
+ nsIFrame*, StyleAppearance, const ComputedStyle&,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors&) override;
+
+ bool PaintScrollbarButton(DrawTarget&, StyleAppearance,
+ const LayoutDeviceRect&, ScrollbarKind, nsIFrame*,
+ const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+
+ template <typename PaintBackendData>
+ bool DoPaintScrollbarThumb(PaintBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&);
+ bool PaintScrollbarThumb(DrawTarget&, const LayoutDeviceRect&, ScrollbarKind,
+ nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+ bool PaintScrollbarThumb(WebRenderBackendData&, const LayoutDeviceRect&,
+ ScrollbarKind, nsIFrame*, const ComputedStyle&,
+ const ElementState& aElementState,
+ const DocumentState& aDocumentState, const Colors&,
+ const DPIRatio&) override;
+
+ void RecomputeScrollbarParams() override;
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/SharedWidgetUtils.cpp b/widget/SharedWidgetUtils.cpp
new file mode 100644
index 0000000000..f1c71b5c66
--- /dev/null
+++ b/widget/SharedWidgetUtils.cpp
@@ -0,0 +1,273 @@
+/* -*- 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 "WidgetUtils.h"
+
+#include "mozilla/TextEvents.h"
+
+#include "nsIBaseWindow.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIDocShell.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsPIDOMWindow.h"
+
+namespace mozilla {
+namespace widget {
+
+// static
+void WidgetUtils::Shutdown() {
+ WidgetKeyboardEvent::Shutdown();
+ InternalEditorInputEvent::Shutdown();
+}
+
+// static
+already_AddRefed<nsIWidget> WidgetUtils::DOMWindowToWidget(
+ nsPIDOMWindowOuter* aDOMWindow) {
+ nsCOMPtr<nsIWidget> widget;
+ nsCOMPtr<nsPIDOMWindowOuter> window = aDOMWindow;
+
+ if (window) {
+ nsCOMPtr<nsIBaseWindow> baseWin(do_QueryInterface(window->GetDocShell()));
+
+ while (!widget && baseWin) {
+ baseWin->GetParentWidget(getter_AddRefs(widget));
+ if (!widget) {
+ nsCOMPtr<nsIDocShellTreeItem> docShellAsItem(
+ do_QueryInterface(baseWin));
+ if (!docShellAsItem) return nullptr;
+
+ nsCOMPtr<nsIDocShellTreeItem> parent;
+ docShellAsItem->GetInProcessParent(getter_AddRefs(parent));
+
+ window = do_GetInterface(parent);
+ if (!window) return nullptr;
+
+ baseWin = do_QueryInterface(window->GetDocShell());
+ }
+ }
+ }
+
+ return widget.forget();
+}
+
+// static
+uint32_t WidgetUtils::ComputeKeyCodeFromChar(uint32_t aCharCode) {
+ if (aCharCode >= 'A' && aCharCode <= 'Z') {
+ return aCharCode - 'A' + NS_VK_A;
+ }
+ if (aCharCode >= 'a' && aCharCode <= 'z') {
+ return aCharCode - 'a' + NS_VK_A;
+ }
+ if (aCharCode >= '0' && aCharCode <= '9') {
+ return aCharCode - '0' + NS_VK_0;
+ }
+ switch (aCharCode) {
+ case ' ':
+ return NS_VK_SPACE;
+ case '\t':
+ return NS_VK_TAB;
+ case ':':
+ return NS_VK_COLON;
+ case ';':
+ return NS_VK_SEMICOLON;
+ case '<':
+ return NS_VK_LESS_THAN;
+ case '=':
+ return NS_VK_EQUALS;
+ case '>':
+ return NS_VK_GREATER_THAN;
+ case '?':
+ return NS_VK_QUESTION_MARK;
+ case '@':
+ return NS_VK_AT;
+ case '^':
+ return NS_VK_CIRCUMFLEX;
+ case '!':
+ return NS_VK_EXCLAMATION;
+ case '"':
+ return NS_VK_DOUBLE_QUOTE;
+ case '#':
+ return NS_VK_HASH;
+ case '$':
+ return NS_VK_DOLLAR;
+ case '%':
+ return NS_VK_PERCENT;
+ case '&':
+ return NS_VK_AMPERSAND;
+ case '_':
+ return NS_VK_UNDERSCORE;
+ case '(':
+ return NS_VK_OPEN_PAREN;
+ case ')':
+ return NS_VK_CLOSE_PAREN;
+ case '*':
+ return NS_VK_ASTERISK;
+ case '+':
+ return NS_VK_PLUS;
+ case '|':
+ return NS_VK_PIPE;
+ case '-':
+ return NS_VK_HYPHEN_MINUS;
+ case '{':
+ return NS_VK_OPEN_CURLY_BRACKET;
+ case '}':
+ return NS_VK_CLOSE_CURLY_BRACKET;
+ case '~':
+ return NS_VK_TILDE;
+ case ',':
+ return NS_VK_COMMA;
+ case '.':
+ return NS_VK_PERIOD;
+ case '/':
+ return NS_VK_SLASH;
+ case '`':
+ return NS_VK_BACK_QUOTE;
+ case '[':
+ return NS_VK_OPEN_BRACKET;
+ case '\\':
+ return NS_VK_BACK_SLASH;
+ case ']':
+ return NS_VK_CLOSE_BRACKET;
+ case '\'':
+ return NS_VK_QUOTE;
+ }
+ return 0;
+}
+
+// static
+void WidgetUtils::GetLatinCharCodeForKeyCode(uint32_t aKeyCode,
+ bool aIsCapsLock,
+ uint32_t* aUnshiftedCharCode,
+ uint32_t* aShiftedCharCode) {
+ MOZ_ASSERT(aUnshiftedCharCode && aShiftedCharCode,
+ "aUnshiftedCharCode and aShiftedCharCode must not be NULL");
+
+ if (aKeyCode >= NS_VK_A && aKeyCode <= NS_VK_Z) {
+ *aUnshiftedCharCode = *aShiftedCharCode = aKeyCode;
+ if (aIsCapsLock) {
+ *aShiftedCharCode += 0x20;
+ } else {
+ *aUnshiftedCharCode += 0x20;
+ }
+ return;
+ }
+
+ // aShiftedCharCode must be zero for non-alphabet keys.
+ *aShiftedCharCode = 0;
+
+ if (aKeyCode >= NS_VK_0 && aKeyCode <= NS_VK_9) {
+ *aUnshiftedCharCode = aKeyCode;
+ return;
+ }
+
+ switch (aKeyCode) {
+ case NS_VK_SPACE:
+ *aUnshiftedCharCode = ' ';
+ break;
+ case NS_VK_COLON:
+ *aUnshiftedCharCode = ':';
+ break;
+ case NS_VK_SEMICOLON:
+ *aUnshiftedCharCode = ';';
+ break;
+ case NS_VK_LESS_THAN:
+ *aUnshiftedCharCode = '<';
+ break;
+ case NS_VK_EQUALS:
+ *aUnshiftedCharCode = '=';
+ break;
+ case NS_VK_GREATER_THAN:
+ *aUnshiftedCharCode = '>';
+ break;
+ case NS_VK_QUESTION_MARK:
+ *aUnshiftedCharCode = '?';
+ break;
+ case NS_VK_AT:
+ *aUnshiftedCharCode = '@';
+ break;
+ case NS_VK_CIRCUMFLEX:
+ *aUnshiftedCharCode = '^';
+ break;
+ case NS_VK_EXCLAMATION:
+ *aUnshiftedCharCode = '!';
+ break;
+ case NS_VK_DOUBLE_QUOTE:
+ *aUnshiftedCharCode = '"';
+ break;
+ case NS_VK_HASH:
+ *aUnshiftedCharCode = '#';
+ break;
+ case NS_VK_DOLLAR:
+ *aUnshiftedCharCode = '$';
+ break;
+ case NS_VK_PERCENT:
+ *aUnshiftedCharCode = '%';
+ break;
+ case NS_VK_AMPERSAND:
+ *aUnshiftedCharCode = '&';
+ break;
+ case NS_VK_UNDERSCORE:
+ *aUnshiftedCharCode = '_';
+ break;
+ case NS_VK_OPEN_PAREN:
+ *aUnshiftedCharCode = '(';
+ break;
+ case NS_VK_CLOSE_PAREN:
+ *aUnshiftedCharCode = ')';
+ break;
+ case NS_VK_ASTERISK:
+ *aUnshiftedCharCode = '*';
+ break;
+ case NS_VK_PLUS:
+ *aUnshiftedCharCode = '+';
+ break;
+ case NS_VK_PIPE:
+ *aUnshiftedCharCode = '|';
+ break;
+ case NS_VK_HYPHEN_MINUS:
+ *aUnshiftedCharCode = '-';
+ break;
+ case NS_VK_OPEN_CURLY_BRACKET:
+ *aUnshiftedCharCode = '{';
+ break;
+ case NS_VK_CLOSE_CURLY_BRACKET:
+ *aUnshiftedCharCode = '}';
+ break;
+ case NS_VK_TILDE:
+ *aUnshiftedCharCode = '~';
+ break;
+ case NS_VK_COMMA:
+ *aUnshiftedCharCode = ',';
+ break;
+ case NS_VK_PERIOD:
+ *aUnshiftedCharCode = '.';
+ break;
+ case NS_VK_SLASH:
+ *aUnshiftedCharCode = '/';
+ break;
+ case NS_VK_BACK_QUOTE:
+ *aUnshiftedCharCode = '`';
+ break;
+ case NS_VK_OPEN_BRACKET:
+ *aUnshiftedCharCode = '[';
+ break;
+ case NS_VK_BACK_SLASH:
+ *aUnshiftedCharCode = '\\';
+ break;
+ case NS_VK_CLOSE_BRACKET:
+ *aUnshiftedCharCode = ']';
+ break;
+ case NS_VK_QUOTE:
+ *aUnshiftedCharCode = '\'';
+ break;
+ default:
+ *aUnshiftedCharCode = 0;
+ break;
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/SwipeTracker.cpp b/widget/SwipeTracker.cpp
new file mode 100644
index 0000000000..7474432e71
--- /dev/null
+++ b/widget/SwipeTracker.cpp
@@ -0,0 +1,272 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "SwipeTracker.h"
+
+#include "InputData.h"
+#include "mozilla/FlushType.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/dom/SimpleGestureEventBinding.h"
+#include "nsAlgorithm.h"
+#include "nsIWidget.h"
+#include "nsRefreshDriver.h"
+#include "UnitTransforms.h"
+
+// These values were tweaked to make the physics feel similar to the native
+// swipe.
+static const double kSpringForce = 250.0;
+static const double kSwipeSuccessThreshold = 0.25;
+
+namespace mozilla {
+
+static already_AddRefed<nsRefreshDriver> GetRefreshDriver(nsIWidget& aWidget) {
+ nsIWidgetListener* widgetListener = aWidget.GetWidgetListener();
+ PresShell* presShell =
+ widgetListener ? widgetListener->GetPresShell() : nullptr;
+ nsPresContext* presContext =
+ presShell ? presShell->GetPresContext() : nullptr;
+ RefPtr<nsRefreshDriver> refreshDriver =
+ presContext ? presContext->RefreshDriver() : nullptr;
+ return refreshDriver.forget();
+}
+
+SwipeTracker::SwipeTracker(nsIWidget& aWidget,
+ const PanGestureInput& aSwipeStartEvent,
+ uint32_t aAllowedDirections,
+ uint32_t aSwipeDirection)
+ : mWidget(aWidget),
+ mRefreshDriver(GetRefreshDriver(mWidget)),
+ mAxis(0.0, 0.0, 0.0, kSpringForce, 1.0),
+ mEventPosition(RoundedToInt(ViewAs<LayoutDevicePixel>(
+ aSwipeStartEvent.mPanStartPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent))),
+ mLastEventTimeStamp(aSwipeStartEvent.mTimeStamp),
+ mAllowedDirections(aAllowedDirections),
+ mSwipeDirection(aSwipeDirection) {
+ SendSwipeEvent(eSwipeGestureStart, 0, 0.0, aSwipeStartEvent.mTimeStamp);
+ ProcessEvent(aSwipeStartEvent, /* aProcessingFirstEvent = */ true);
+}
+
+void SwipeTracker::Destroy() { UnregisterFromRefreshDriver(); }
+
+SwipeTracker::~SwipeTracker() {
+ MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver,
+ "Destroy needs to be called before deallocating");
+}
+
+double SwipeTracker::SwipeSuccessTargetValue() const {
+ return mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
+ ? -1.0
+ : 1.0;
+}
+
+double SwipeTracker::ClampToAllowedRange(double aGestureAmount) const {
+ // gestureAmount needs to stay between -1 and 0 when swiping right and
+ // between 0 and 1 when swiping left.
+ double min =
+ mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT ? -1.0
+ : 0.0;
+ double max =
+ mSwipeDirection == dom::SimpleGestureEvent_Binding::DIRECTION_LEFT ? 1.0
+ : 0.0;
+ return clamped(aGestureAmount, min, max);
+}
+
+bool SwipeTracker::ComputeSwipeSuccess() const {
+ double targetValue = SwipeSuccessTargetValue();
+
+ // If the fingers were moving away from the target direction when they were
+ // lifted from the touchpad, abort the swipe.
+ if (mCurrentVelocity * targetValue <
+ -StaticPrefs::widget_swipe_velocity_twitch_tolerance()) {
+ return false;
+ }
+
+ return (mGestureAmount * targetValue +
+ mCurrentVelocity * targetValue *
+ StaticPrefs::widget_swipe_success_velocity_contribution()) >=
+ kSwipeSuccessThreshold;
+}
+
+nsEventStatus SwipeTracker::ProcessEvent(
+ const PanGestureInput& aEvent, bool aProcessingFirstEvent /* = false */) {
+ // If the fingers have already been lifted or the swipe direction is where
+ // navigation is impossible, don't process this event for swiping.
+ if (!mEventsAreControllingSwipe || !SwipingInAllowedDirection()) {
+ // Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture
+ // and nsEventStatus_eIgnore for events of subsequent scroll gestures.
+ if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
+ aEvent.mType == PanGestureInput::PANGESTURE_START) {
+ mEventsHaveStartedNewGesture = true;
+ }
+ return mEventsHaveStartedNewGesture ? nsEventStatus_eIgnore
+ : nsEventStatus_eConsumeNoDefault;
+ }
+
+ mDeltaTypeIsPage = aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE;
+ double delta = [&]() -> double {
+ if (mDeltaTypeIsPage) {
+ return -aEvent.mPanDisplacement.x / StaticPrefs::widget_swipe_page_size();
+ }
+ return -aEvent.mPanDisplacement.x / mWidget.GetDefaultScaleInternal() /
+ StaticPrefs::widget_swipe_pixel_size();
+ }();
+
+ mGestureAmount = ClampToAllowedRange(mGestureAmount + delta);
+ if (aEvent.mType != PanGestureInput::PANGESTURE_END) {
+ if (!aProcessingFirstEvent) {
+ double elapsedSeconds = std::max(
+ 0.008, (aEvent.mTimeStamp - mLastEventTimeStamp).ToSeconds());
+ mCurrentVelocity = delta / elapsedSeconds;
+ }
+ mLastEventTimeStamp = aEvent.mTimeStamp;
+ }
+
+ const bool computedSwipeSuccess = ComputeSwipeSuccess();
+ double eventAmount = mGestureAmount;
+ // If ComputeSwipeSuccess returned false because the users fingers were
+ // moving slightly away from the target direction then we do not want to
+ // display the UI as if we were at the success threshold as that would
+ // give a false indication that navigation would happen.
+ if (!computedSwipeSuccess && (eventAmount >= kSwipeSuccessThreshold ||
+ eventAmount <= -kSwipeSuccessThreshold)) {
+ eventAmount = 0.999 * kSwipeSuccessThreshold;
+ if (mGestureAmount < 0.f) {
+ eventAmount = -eventAmount;
+ }
+ }
+
+ SendSwipeEvent(eSwipeGestureUpdate, 0, eventAmount, aEvent.mTimeStamp);
+
+ if (aEvent.mType == PanGestureInput::PANGESTURE_END) {
+ mEventsAreControllingSwipe = false;
+ if (computedSwipeSuccess) {
+ // Let's use same timestamp as previous event because this is caused by
+ // the preceding event.
+ SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0, aEvent.mTimeStamp);
+ UnregisterFromRefreshDriver();
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("SwipeTracker::SwipeFinished",
+ [swipeTracker = RefPtr<SwipeTracker>(this),
+ timeStamp = aEvent.mTimeStamp] {
+ swipeTracker->SwipeFinished(timeStamp);
+ }));
+ } else {
+ StartAnimating(eventAmount, 0.0);
+ }
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+void SwipeTracker::StartAnimating(double aStartValue, double aTargetValue) {
+ mAxis.SetPosition(aStartValue);
+ mAxis.SetDestination(aTargetValue);
+ mAxis.SetVelocity(mCurrentVelocity);
+
+ mLastAnimationFrameTime = TimeStamp::Now();
+
+ // Add ourselves as a refresh driver observer. The refresh driver
+ // will call WillRefresh for each animation frame until we
+ // unregister ourselves.
+ MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver,
+ "We only want a single refresh driver registration");
+ if (mRefreshDriver) {
+ mRefreshDriver->AddRefreshObserver(this, FlushType::Style,
+ "Swipe animation");
+ mRegisteredWithRefreshDriver = true;
+ }
+}
+
+void SwipeTracker::WillRefresh(TimeStamp aTime) {
+ // FIXME(emilio): shouldn't we be using `aTime`?
+ TimeStamp now = TimeStamp::Now();
+ mAxis.Simulate(now - mLastAnimationFrameTime);
+ mLastAnimationFrameTime = now;
+
+ const double wholeSize = mDeltaTypeIsPage
+ ? StaticPrefs::widget_swipe_page_size()
+ : StaticPrefs::widget_swipe_pixel_size();
+ // NOTE(emilio): It's unclear this makes sense for page-based swiping, but
+ // this preserves behavior and all platforms probably will end up converging
+ // in pixel-based pan input, so...
+ const double minIncrement = 1.0 / wholeSize;
+ const bool isFinished = mAxis.IsFinished(minIncrement);
+
+ mGestureAmount = isFinished ? mAxis.GetDestination() : mAxis.GetPosition();
+ SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount, now);
+
+ if (isFinished) {
+ UnregisterFromRefreshDriver();
+ SwipeFinished(now);
+ }
+}
+
+void SwipeTracker::CancelSwipe(const TimeStamp& aTimeStamp) {
+ SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
+}
+
+void SwipeTracker::SwipeFinished(const TimeStamp& aTimeStamp) {
+ SendSwipeEvent(eSwipeGestureEnd, 0, 0.0, aTimeStamp);
+ mWidget.SwipeFinished();
+}
+
+void SwipeTracker::UnregisterFromRefreshDriver() {
+ if (mRegisteredWithRefreshDriver) {
+ MOZ_ASSERT(mRefreshDriver, "How were we able to register, then?");
+ mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
+ mRegisteredWithRefreshDriver = false;
+ }
+}
+
+/* static */ WidgetSimpleGestureEvent SwipeTracker::CreateSwipeGestureEvent(
+ EventMessage aMsg, nsIWidget* aWidget,
+ const LayoutDeviceIntPoint& aPosition, const TimeStamp& aTimeStamp) {
+ // XXX Why isn't this initialized with nsCocoaUtils::InitInputEvent()?
+ WidgetSimpleGestureEvent geckoEvent(true, aMsg, aWidget);
+ geckoEvent.mModifiers = 0;
+ // XXX How about geckoEvent.mTime?
+ geckoEvent.mTimeStamp = aTimeStamp;
+ geckoEvent.mRefPoint = aPosition;
+ geckoEvent.mButtons = 0;
+ return geckoEvent;
+}
+
+bool SwipeTracker::SendSwipeEvent(EventMessage aMsg, uint32_t aDirection,
+ double aDelta, const TimeStamp& aTimeStamp) {
+ WidgetSimpleGestureEvent geckoEvent =
+ CreateSwipeGestureEvent(aMsg, &mWidget, mEventPosition, aTimeStamp);
+ geckoEvent.mDirection = aDirection;
+ geckoEvent.mDelta = aDelta;
+ geckoEvent.mAllowedDirections = mAllowedDirections;
+ return mWidget.DispatchWindowEvent(geckoEvent);
+}
+
+// static
+bool SwipeTracker::CanTriggerSwipe(const PanGestureInput& aPanInput) {
+ if (StaticPrefs::widget_disable_swipe_tracker()) {
+ return false;
+ }
+
+ if (aPanInput.mType != PanGestureInput::PANGESTURE_START) {
+ return false;
+ }
+
+ // Only initiate horizontal tracking for events whose horizontal element is
+ // at least eight times larger than its vertical element. This minimizes
+ // performance problems with vertical scrolls (by minimizing the possibility
+ // that they'll be misinterpreted as horizontal swipes), while still
+ // tolerating a small vertical element to a true horizontal swipe. The number
+ // '8' was arrived at by trial and error.
+ return std::abs(aPanInput.mPanDisplacement.x) >
+ std::abs(aPanInput.mPanDisplacement.y) * 8;
+}
+
+} // namespace mozilla
diff --git a/widget/SwipeTracker.h b/widget/SwipeTracker.h
new file mode 100644
index 0000000000..e475cb3b29
--- /dev/null
+++ b/widget/SwipeTracker.h
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef SwipeTracker_h
+#define SwipeTracker_h
+
+#include "EventForwards.h"
+#include "mozilla/layers/AxisPhysicsMSDModel.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "nsRefreshObservers.h"
+#include "Units.h"
+
+class nsIWidget;
+class nsRefreshDriver;
+
+namespace mozilla {
+
+class PanGestureInput;
+
+/**
+ * SwipeTracker turns PanGestureInput events into swipe events
+ * (WidgetSimpleGestureEvent) and dispatches them into Gecko.
+ * The swiping behavior mirrors the behavior of the Cocoa API
+ * -[NSEvent
+ * trackSwipeEventWithOptions:dampenAmountThresholdMin:max:usingHandler:].
+ * The advantage of using this class over the Cocoa API is that this class
+ * properly supports submitting queued up events to it, and that it hopefully
+ * doesn't intermittently break scrolling the way the Cocoa API does (bug
+ * 927702).
+ *
+ * The swipe direction is either left or right. It is determined before the
+ * SwipeTracker is created and stays fixed during the swipe.
+ * During the swipe, the swipe has a current "value" which is between 0 and the
+ * target value. The target value is either 1 (swiping left) or -1 (swiping
+ * right) - see SwipeSuccessTargetValue().
+ * A swipe can either succeed or fail. If it succeeds, the swipe animation
+ * animates towards the success target value; if it fails, it animates back to
+ * a value of 0. A swipe can only succeed if the user is swiping in an allowed
+ * direction. (Since both the allowed directions and the swipe direction are
+ * known at swipe start time, it's clear from the beginning whether a swipe is
+ * doomed to fail. In that case, the purpose of the SwipeTracker is to simulate
+ * a bounce-back animation.)
+ */
+class SwipeTracker final : public nsARefreshObserver {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(SwipeTracker, override)
+
+ SwipeTracker(nsIWidget& aWidget, const PanGestureInput& aSwipeStartEvent,
+ uint32_t aAllowedDirections, uint32_t aSwipeDirection);
+
+ void Destroy();
+
+ nsEventStatus ProcessEvent(const PanGestureInput& aEvent,
+ bool aProcessingFirstEvent = false);
+ void CancelSwipe(const TimeStamp& aTimeStamp);
+
+ static WidgetSimpleGestureEvent CreateSwipeGestureEvent(
+ EventMessage aMsg, nsIWidget* aWidget,
+ const LayoutDeviceIntPoint& aPosition, const TimeStamp& aTimeStamp);
+
+ // nsARefreshObserver
+ void WillRefresh(mozilla::TimeStamp aTime) override;
+
+ static bool CanTriggerSwipe(const PanGestureInput& aPanInput);
+
+ protected:
+ ~SwipeTracker();
+
+ bool SwipingInAllowedDirection() const {
+ return mAllowedDirections & mSwipeDirection;
+ }
+ double SwipeSuccessTargetValue() const;
+ double ClampToAllowedRange(double aGestureAmount) const;
+ bool ComputeSwipeSuccess() const;
+ void StartAnimating(double aStartValue, double aTargetValue);
+ void SwipeFinished(const TimeStamp& aTimeStamp);
+ void UnregisterFromRefreshDriver();
+ bool SendSwipeEvent(EventMessage aMsg, uint32_t aDirection, double aDelta,
+ const TimeStamp& aTimeStamp);
+
+ nsIWidget& mWidget;
+ RefPtr<nsRefreshDriver> mRefreshDriver;
+ layers::AxisPhysicsMSDModel mAxis;
+ const LayoutDeviceIntPoint mEventPosition;
+ TimeStamp mLastEventTimeStamp;
+ TimeStamp mLastAnimationFrameTime;
+ const uint32_t mAllowedDirections;
+ const uint32_t mSwipeDirection;
+ double mGestureAmount = 0.0;
+ double mCurrentVelocity = 0.0;
+ bool mDeltaTypeIsPage = false;
+ bool mEventsAreControllingSwipe = true;
+ bool mEventsHaveStartedNewGesture = false;
+ bool mRegisteredWithRefreshDriver = false;
+};
+
+struct SwipeEventQueue {
+ SwipeEventQueue(uint32_t aAllowedDirections, uint64_t aInputBlockId)
+ : allowedDirections(aAllowedDirections), inputBlockId(aInputBlockId) {}
+
+ nsTArray<PanGestureInput> queuedEvents;
+ uint32_t allowedDirections;
+ uint64_t inputBlockId;
+};
+
+} // namespace mozilla
+
+#endif // SwipeTracker_h
diff --git a/widget/SystemTimeConverter.h b/widget/SystemTimeConverter.h
new file mode 100644
index 0000000000..aa2a760487
--- /dev/null
+++ b/widget/SystemTimeConverter.h
@@ -0,0 +1,233 @@
+/* -*- 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/. */
+
+#ifndef SystemTimeConverter_h
+#define SystemTimeConverter_h
+
+#include <limits>
+#include <type_traits>
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+// Utility class that converts time values represented as an unsigned integral
+// number of milliseconds from one time source (e.g. a native event time) to
+// corresponding mozilla::TimeStamp objects.
+//
+// This class handles wrapping of integer values and skew between the time
+// source and mozilla::TimeStamp values.
+//
+// It does this by using an historical reference time recorded in both time
+// scales (i.e. both as a numerical time value and as a TimeStamp).
+//
+// For performance reasons, this class is careful to minimize calls to the
+// native "current time" function (e.g. gdk_x11_server_get_time) since this can
+// be slow.
+template <typename Time, typename TimeStampNowProvider = TimeStamp>
+class SystemTimeConverter {
+ public:
+ SystemTimeConverter()
+ : mReferenceTime(Time(0)),
+ mLastBackwardsSkewCheck(Time(0)),
+ kTimeRange(std::numeric_limits<Time>::max()),
+ kTimeHalfRange(kTimeRange / 2),
+ kBackwardsSkewCheckInterval(Time(2000)) {
+ static_assert(!std::is_signed_v<Time>, "Expected Time to be unsigned");
+ }
+
+ template <typename CurrentTimeGetter>
+ mozilla::TimeStamp GetTimeStampFromSystemTime(
+ Time aTime, CurrentTimeGetter& aCurrentTimeGetter) {
+ TimeStamp roughlyNow = TimeStampNowProvider::Now();
+
+ // If the reference time is not set, use the current time value to fill
+ // it in.
+ if (mReferenceTimeStamp.IsNull()) {
+ // This sometimes happens when ::GetMessageTime returns 0 for the first
+ // message on Windows.
+ if (!aTime) return roughlyNow;
+ UpdateReferenceTime(aTime, aCurrentTimeGetter);
+ }
+
+ // Check for skew between the source of Time values and TimeStamp values.
+ // We do this by comparing two durations (both in ms):
+ //
+ // i. The duration from the reference time to the passed-in time.
+ // (timeDelta in the diagram below)
+ // ii. The duration from the reference timestamp to the current time
+ // based on TimeStamp::Now.
+ // (timeStampDelta in the diagram below)
+ //
+ // Normally, we'd expect (ii) to be slightly larger than (i) to account
+ // for the time taken between generating the event and processing it.
+ //
+ // If (ii) - (i) is negative then the source of Time values is getting
+ // "ahead" of TimeStamp. We call this "forwards" skew below.
+ //
+ // For the reverse case, if (ii) - (i) is positive (and greater than some
+ // tolerance factor), then we may have "backwards" skew. This is often
+ // the case when we have a backlog of events and by the time we process
+ // them, the time given by the system is comparatively "old".
+ //
+ // The IsNewerThanTimestamp function computes the equivalent of |aTime| in
+ // the TimeStamp scale and returns that in |timeAsTimeStamp|.
+ //
+ // Graphically:
+ //
+ // mReferenceTime aTime
+ // Time scale: ........+.......................*........
+ // |--------timeDelta------|
+ //
+ // mReferenceTimeStamp roughlyNow
+ // TimeStamp scale: ........+...........................*....
+ // |------timeStampDelta-------|
+ //
+ // |---|
+ // roughlyNow-timeAsTimeStamp
+ //
+ TimeStamp timeAsTimeStamp;
+ bool newer = IsTimeNewerThanTimestamp(aTime, roughlyNow, &timeAsTimeStamp);
+
+ // Tolerance when detecting clock skew.
+ static const TimeDuration kTolerance = TimeDuration::FromMilliseconds(30.0);
+
+ // Check for forwards skew
+ if (newer) {
+ // Make aTime correspond to roughlyNow
+ UpdateReferenceTime(aTime, roughlyNow);
+
+ // We didn't have backwards skew so don't bother checking for
+ // backwards skew again for a little while.
+ mLastBackwardsSkewCheck = aTime;
+
+ return roughlyNow;
+ }
+
+ if (roughlyNow - timeAsTimeStamp <= kTolerance) {
+ // If the time between event times and TimeStamp values is within
+ // the tolerance then assume we don't have clock skew so we can
+ // avoid checking for backwards skew for a while.
+ mLastBackwardsSkewCheck = aTime;
+ } else if (aTime - mLastBackwardsSkewCheck > kBackwardsSkewCheckInterval) {
+ aCurrentTimeGetter.GetTimeAsyncForPossibleBackwardsSkew(roughlyNow);
+ mLastBackwardsSkewCheck = aTime;
+ }
+
+ // Finally, calculate the timestamp
+ return timeAsTimeStamp;
+ }
+
+ void CompensateForBackwardsSkew(Time aReferenceTime,
+ const TimeStamp& aLowerBound) {
+ // Check if we actually have backwards skew. Backwards skew looks like
+ // the following:
+ //
+ // mReferenceTime
+ // Time: ..+...a...b...c..........................
+ //
+ // mReferenceTimeStamp
+ // TimeStamp: ..+.....a.....b.....c....................
+ //
+ // Converted
+ // time: ......a'..b'..c'.........................
+ //
+ // What we need to do is bring mReferenceTime "forwards".
+ //
+ // Suppose when we get (c), we detect possible backwards skew and trigger
+ // an async request for the current time (which is passed in here as
+ // aReferenceTime).
+ //
+ // We end up with something like the following:
+ //
+ // mReferenceTime aReferenceTime
+ // Time: ..+...a...b...c...v......................
+ //
+ // mReferenceTimeStamp
+ // TimeStamp: ..+.....a.....b.....c..........x.........
+ // ^ ^
+ // aLowerBound TimeStamp::Now()
+ //
+ // If the duration (aLowerBound - mReferenceTimeStamp) is greater than
+ // (aReferenceTime - mReferenceTime) then we know we have backwards skew.
+ //
+ // If that's not the case, then we probably just got caught behind
+ // temporarily.
+ if (IsTimeNewerThanTimestamp(aReferenceTime, aLowerBound, nullptr)) {
+ return;
+ }
+
+ // We have backwards skew; the equivalent TimeStamp for aReferenceTime lies
+ // somewhere between aLowerBound (which was the TimeStamp when we triggered
+ // the async request for the current time) and TimeStamp::Now().
+ //
+ // If aReferenceTime was waiting in the event queue for a long time, the
+ // equivalent TimeStamp might be much closer to aLowerBound than
+ // TimeStamp::Now() so for now we just set it to aLowerBound. That's
+ // guaranteed to be at least somewhat of an improvement.
+ UpdateReferenceTime(aReferenceTime, aLowerBound);
+ }
+
+ private:
+ template <typename CurrentTimeGetter>
+ void UpdateReferenceTime(Time aReferenceTime,
+ const CurrentTimeGetter& aCurrentTimeGetter) {
+ Time currentTime = aCurrentTimeGetter.GetCurrentTime();
+ TimeStamp currentTimeStamp = TimeStampNowProvider::Now();
+ Time timeSinceReference = currentTime - aReferenceTime;
+ TimeStamp referenceTimeStamp =
+ currentTimeStamp - TimeDuration::FromMilliseconds(timeSinceReference);
+ UpdateReferenceTime(aReferenceTime, referenceTimeStamp);
+ }
+
+ void UpdateReferenceTime(Time aReferenceTime,
+ const TimeStamp& aReferenceTimeStamp) {
+ mReferenceTime = aReferenceTime;
+ mReferenceTimeStamp = aReferenceTimeStamp;
+ }
+
+ bool IsTimeNewerThanTimestamp(Time aTime, TimeStamp aTimeStamp,
+ TimeStamp* aTimeAsTimeStamp) {
+ Time timeDelta = aTime - mReferenceTime;
+
+ // Cast the result to signed 64-bit integer first since that should be
+ // enough to hold the range of values returned by ToMilliseconds() and
+ // the result of converting from double to an integer-type when the value
+ // is outside the integer range is undefined.
+ // Then we do an implicit cast to Time (typically an unsigned 32-bit
+ // integer) which wraps times outside that range.
+ TimeDuration timeStampDelta = (aTimeStamp - mReferenceTimeStamp);
+ int64_t wholeMillis = static_cast<int64_t>(timeStampDelta.ToMilliseconds());
+ Time wrappedTimeStampDelta = wholeMillis; // truncate to unsigned
+
+ Time timeToTimeStamp = wrappedTimeStampDelta - timeDelta;
+ bool isNewer = false;
+ if (timeToTimeStamp == 0) {
+ // wholeMillis needs no adjustment
+ } else if (timeToTimeStamp < kTimeHalfRange) {
+ wholeMillis -= timeToTimeStamp;
+ } else {
+ isNewer = true;
+ wholeMillis += (-timeToTimeStamp);
+ }
+ if (aTimeAsTimeStamp) {
+ *aTimeAsTimeStamp =
+ mReferenceTimeStamp + TimeDuration::FromMilliseconds(wholeMillis);
+ }
+
+ return isNewer;
+ }
+
+ Time mReferenceTime;
+ TimeStamp mReferenceTimeStamp;
+ Time mLastBackwardsSkewCheck;
+
+ const Time kTimeRange;
+ const Time kTimeHalfRange;
+ const Time kBackwardsSkewCheckInterval;
+};
+
+} // namespace mozilla
+
+#endif /* SystemTimeConverter_h */
diff --git a/widget/TextEventDispatcher.cpp b/widget/TextEventDispatcher.cpp
new file mode 100644
index 0000000000..cf414a23e3
--- /dev/null
+++ b/widget/TextEventDispatcher.cpp
@@ -0,0 +1,1043 @@
+/* -*- 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 "TextEventDispatcher.h"
+
+#include "IMEData.h"
+#include "PuppetWidget.h"
+#include "TextEvents.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsCharTraits.h"
+#include "nsIFrame.h"
+#include "nsIWidget.h"
+#include "nsPIDOMWindow.h"
+#include "nsView.h"
+
+namespace mozilla {
+namespace widget {
+
+/******************************************************************************
+ * TextEventDispatcher
+ *****************************************************************************/
+TextEventDispatcher::TextEventDispatcher(nsIWidget* aWidget)
+ : mWidget(aWidget),
+ mDispatchingEvent(0),
+ mInputTransactionType(eNoInputTransaction),
+ mIsComposing(false),
+ mIsHandlingComposition(false),
+ mHasFocus(false) {
+ MOZ_RELEASE_ASSERT(mWidget, "aWidget must not be nullptr");
+
+ ClearNotificationRequests();
+}
+
+nsresult TextEventDispatcher::BeginInputTransaction(
+ TextEventDispatcherListener* aListener) {
+ return BeginInputTransactionInternal(aListener,
+ eSameProcessSyncInputTransaction);
+}
+
+nsresult TextEventDispatcher::BeginTestInputTransaction(
+ TextEventDispatcherListener* aListener, bool aIsAPZAware) {
+ return BeginInputTransactionInternal(
+ aListener, aIsAPZAware ? eAsyncTestInputTransaction
+ : eSameProcessSyncTestInputTransaction);
+}
+
+nsresult TextEventDispatcher::BeginNativeInputTransaction() {
+ if (NS_WARN_IF(!mWidget)) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<TextEventDispatcherListener> listener =
+ mWidget->GetNativeTextEventDispatcherListener();
+ if (NS_WARN_IF(!listener)) {
+ return NS_ERROR_FAILURE;
+ }
+ return BeginInputTransactionInternal(listener, eNativeInputTransaction);
+}
+
+nsresult TextEventDispatcher::BeginInputTransactionInternal(
+ TextEventDispatcherListener* aListener, InputTransactionType aType) {
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ if (listener == aListener && mInputTransactionType == aType) {
+ UpdateNotificationRequests();
+ return NS_OK;
+ }
+ // If this has composition or is dispatching an event, any other listener
+ // can steal ownership. Especially, if the latter case is allowed,
+ // nobody cannot begin input transaction with this if a modal dialog is
+ // opened during dispatching an event.
+ if (IsComposing() || IsDispatchingEvent()) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+ }
+ mListener = do_GetWeakReference(aListener);
+ mInputTransactionType = aType;
+ if (listener && listener != aListener) {
+ listener->OnRemovedFrom(this);
+ }
+ UpdateNotificationRequests();
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::BeginInputTransactionFor(
+ const WidgetGUIEvent* aEvent, PuppetWidget* aPuppetWidget) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+ MOZ_ASSERT(!IsDispatchingEvent());
+
+ switch (aEvent->mMessage) {
+ case eKeyDown:
+ case eKeyPress:
+ case eKeyUp:
+ MOZ_ASSERT(aEvent->mClass == eKeyboardEventClass);
+ break;
+ case eCompositionStart:
+ case eCompositionChange:
+ case eCompositionCommit:
+ case eCompositionCommitAsIs:
+ MOZ_ASSERT(aEvent->mClass == eCompositionEventClass);
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aEvent->mFlags.mIsSynthesizedForTests) {
+ // If the event is for an automated test and this instance dispatched
+ // an event to the parent process, we can assume that this is already
+ // initialized properly.
+ if (mInputTransactionType == eAsyncTestInputTransaction) {
+ return NS_OK;
+ }
+ // Even if the event coming from the parent process is synthesized for
+ // tests, this process should treat it as "sync" test here because
+ // it won't be go back to the parent process.
+ nsresult rv = BeginInputTransactionInternal(
+ static_cast<TextEventDispatcherListener*>(aPuppetWidget),
+ eSameProcessSyncTestInputTransaction);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsresult rv = BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // Emulate modifying members which indicate the state of composition.
+ // If we need to manage more states and/or more complexly, we should create
+ // internal methods which are called by both here and each event dispatcher
+ // method of this class.
+ switch (aEvent->mMessage) {
+ case eKeyDown:
+ case eKeyPress:
+ case eKeyUp:
+ return NS_OK;
+ case eCompositionStart:
+ MOZ_ASSERT(!mIsComposing);
+ mIsComposing = mIsHandlingComposition = true;
+ return NS_OK;
+ case eCompositionChange:
+ MOZ_ASSERT(mIsComposing);
+ MOZ_ASSERT(mIsHandlingComposition);
+ mIsComposing = mIsHandlingComposition = true;
+ return NS_OK;
+ case eCompositionCommit:
+ case eCompositionCommitAsIs:
+ MOZ_ASSERT(mIsComposing);
+ MOZ_ASSERT(mIsHandlingComposition);
+ mIsComposing = false;
+ mIsHandlingComposition = true;
+ return NS_OK;
+ default:
+ MOZ_ASSERT_UNREACHABLE("You forgot to handle the event");
+ return NS_ERROR_UNEXPECTED;
+ }
+}
+void TextEventDispatcher::EndInputTransaction(
+ TextEventDispatcherListener* aListener) {
+ if (NS_WARN_IF(IsComposing()) || NS_WARN_IF(IsDispatchingEvent())) {
+ return;
+ }
+
+ mInputTransactionType = eNoInputTransaction;
+
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (NS_WARN_IF(!listener)) {
+ return;
+ }
+
+ if (NS_WARN_IF(listener != aListener)) {
+ return;
+ }
+
+ mListener = nullptr;
+ listener->OnRemovedFrom(this);
+ UpdateNotificationRequests();
+}
+
+void TextEventDispatcher::OnDestroyWidget() {
+ mWidget = nullptr;
+ mHasFocus = false;
+ ClearNotificationRequests();
+ mPendingComposition.Clear();
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ mListener = nullptr;
+ mWritingMode.reset();
+ mInputTransactionType = eNoInputTransaction;
+ if (listener) {
+ listener->OnRemovedFrom(this);
+ }
+}
+
+nsresult TextEventDispatcher::GetState() const {
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (!listener) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ if (!mWidget || mWidget->Destroyed()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return NS_OK;
+}
+
+void TextEventDispatcher::InitEvent(WidgetGUIEvent& aEvent) const {
+ aEvent.mRefPoint = LayoutDeviceIntPoint(0, 0);
+ aEvent.mFlags.mIsSynthesizedForTests = IsForTests();
+ if (aEvent.mClass != eCompositionEventClass) {
+ return;
+ }
+ void* pseudoIMEContext = GetPseudoIMEContext();
+ if (pseudoIMEContext) {
+ aEvent.AsCompositionEvent()->mNativeIMEContext.InitWithRawNativeIMEContext(
+ pseudoIMEContext);
+ }
+#ifdef DEBUG
+ else {
+ MOZ_ASSERT(!XRE_IsContentProcess(),
+ "Why did the content process start native event transaction?");
+ MOZ_ASSERT(aEvent.AsCompositionEvent()->mNativeIMEContext.IsValid(),
+ "Native IME context shouldn't be invalid");
+ }
+#endif // #ifdef DEBUG
+}
+
+Maybe<WritingMode> TextEventDispatcher::MaybeQueryWritingModeAtSelection()
+ const {
+ if (mHasFocus || mWritingMode.isSome()) {
+ return mWritingMode;
+ }
+
+ if (NS_WARN_IF(!mWidget)) {
+ return Nothing();
+ }
+
+ // If a remote content has focus and IME does not have focus, it's going to
+ // fail eQuerySelectedText in ContentCacheParent. For avoiding to waste
+ // unnecessary runtime cost and to prevent unnecessary warnings, we should
+ // not dispatch the event in the case.
+ const InputContext inputContext = mWidget->GetInputContext();
+ if (XRE_IsE10sParentProcess() && inputContext.IsOriginContentProcess() &&
+ !inputContext.mIMEState.IsEditable()) {
+ return Nothing();
+ }
+
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ mWidget);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ const_cast<TextEventDispatcher*>(this)->DispatchEvent(
+ mWidget, querySelectedTextEvent, status);
+ if (!querySelectedTextEvent.FoundSelection()) {
+ return Nothing();
+ }
+
+ return Some(querySelectedTextEvent.mReply->mWritingMode);
+}
+
+nsresult TextEventDispatcher::DispatchEvent(nsIWidget* aWidget,
+ WidgetGUIEvent& aEvent,
+ nsEventStatus& aStatus) {
+ MOZ_ASSERT(!aEvent.AsInputEvent(), "Use DispatchInputEvent()");
+
+ RefPtr<TextEventDispatcher> kungFuDeathGrip(this);
+ nsCOMPtr<nsIWidget> widget(aWidget);
+ mDispatchingEvent++;
+ nsresult rv = widget->DispatchEvent(&aEvent, aStatus);
+ mDispatchingEvent--;
+ return rv;
+}
+
+nsresult TextEventDispatcher::DispatchInputEvent(nsIWidget* aWidget,
+ WidgetInputEvent& aEvent,
+ nsEventStatus& aStatus) {
+ RefPtr<TextEventDispatcher> kungFuDeathGrip(this);
+ nsCOMPtr<nsIWidget> widget(aWidget);
+ mDispatchingEvent++;
+
+ // If the event is dispatched via nsIWidget::DispatchInputEvent(), it
+ // sends the event to the parent process first since APZ needs to handle it
+ // first. However, some callers (e.g., keyboard apps on B2G and tests
+ // expecting synchronous dispatch) don't want this to do that.
+ nsresult rv = NS_OK;
+ if (ShouldSendInputEventToAPZ()) {
+ aStatus = widget->DispatchInputEvent(&aEvent).mContentStatus;
+ } else {
+ rv = widget->DispatchEvent(&aEvent, aStatus);
+ }
+
+ mDispatchingEvent--;
+ return rv;
+}
+
+nsresult TextEventDispatcher::StartComposition(
+ nsEventStatus& aStatus, const WidgetEventTime* aEventTime) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsresult rv = GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(mIsComposing)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // When you change some members from here, you may need same change in
+ // BeginInputTransactionFor().
+ mIsComposing = mIsHandlingComposition = true;
+ WidgetCompositionEvent compositionStartEvent(true, eCompositionStart,
+ mWidget);
+ InitEvent(compositionStartEvent);
+ if (aEventTime) {
+ compositionStartEvent.AssignEventTime(*aEventTime);
+ }
+ rv = DispatchEvent(mWidget, compositionStartEvent, aStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::StartCompositionAutomaticallyIfNecessary(
+ nsEventStatus& aStatus, const WidgetEventTime* aEventTime) {
+ if (IsComposing()) {
+ return NS_OK;
+ }
+
+ nsresult rv = StartComposition(aStatus, aEventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // If started composition has already been committed, we shouldn't dispatch
+ // the compositionchange event.
+ if (!IsComposing()) {
+ aStatus = nsEventStatus_eConsumeNoDefault;
+ return NS_OK;
+ }
+
+ // Note that the widget might be destroyed during a call of
+ // StartComposition(). In such case, we shouldn't keep dispatching next
+ // event.
+ rv = GetState();
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(rv != NS_ERROR_NOT_INITIALIZED,
+ "aDispatcher must still be initialized in this case");
+ aStatus = nsEventStatus_eConsumeNoDefault;
+ return NS_OK; // Don't throw exception in this case
+ }
+
+ aStatus = nsEventStatus_eIgnore;
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::CommitComposition(
+ nsEventStatus& aStatus, const nsAString* aCommitString,
+ const WidgetEventTime* aEventTime) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsresult rv = GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // When there is no composition, caller shouldn't try to commit composition
+ // with non-existing composition string nor commit composition with empty
+ // string.
+ if (NS_WARN_IF(!IsComposing() &&
+ (!aCommitString || aCommitString->IsEmpty()))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIWidget> widget(mWidget);
+ rv = StartCompositionAutomaticallyIfNecessary(aStatus, aEventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ return NS_OK;
+ }
+
+ // When you change some members from here, you may need same change in
+ // BeginInputTransactionFor().
+
+ // End current composition and make this free for other IMEs.
+ mIsComposing = false;
+
+ EventMessage message =
+ aCommitString ? eCompositionCommit : eCompositionCommitAsIs;
+ WidgetCompositionEvent compositionCommitEvent(true, message, widget);
+ InitEvent(compositionCommitEvent);
+ if (aEventTime) {
+ compositionCommitEvent.AssignEventTime(*aEventTime);
+ }
+ if (message == eCompositionCommit) {
+ compositionCommitEvent.mData = *aCommitString;
+ // If aCommitString comes from TextInputProcessor, it may be void, but
+ // editor requires non-void string even when it's empty.
+ compositionCommitEvent.mData.SetIsVoid(false);
+ // Don't send CRLF nor CR, replace it with LF here.
+ compositionCommitEvent.mData.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ compositionCommitEvent.mData.ReplaceSubstring(u"\r"_ns, u"\n"_ns);
+ }
+ rv = DispatchEvent(widget, compositionCommitEvent, aStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::NotifyIME(
+ const IMENotification& aIMENotification) {
+ nsresult rv = NS_ERROR_NOT_IMPLEMENTED;
+
+ switch (aIMENotification.mMessage) {
+ case NOTIFY_IME_OF_FOCUS: {
+ mWritingMode = MaybeQueryWritingModeAtSelection();
+ break;
+ }
+ case NOTIFY_IME_OF_BLUR:
+ mHasFocus = false;
+ mWritingMode.reset();
+ ClearNotificationRequests();
+ break;
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ // If content handles composition events when native IME doesn't have
+ // composition, that means that we completely finished handling
+ // composition(s). Note that when focused content is in a remote
+ // process, this is sent when all dispatched composition events
+ // have been handled in the remote process.
+ if (!IsComposing()) {
+ mIsHandlingComposition = false;
+ }
+ break;
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ if (mHasFocus && aIMENotification.mSelectionChangeData.HasRange()) {
+ mWritingMode =
+ Some(aIMENotification.mSelectionChangeData.GetWritingMode());
+ }
+ break;
+ default:
+ break;
+ }
+
+ // First, send the notification to current input transaction's listener.
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ rv = listener->NotifyIME(this, aIMENotification);
+ }
+
+ if (!mWidget) {
+ return rv;
+ }
+
+ // If current input transaction isn't for native event handler, we should
+ // send the notification to the native text event dispatcher listener
+ // since native event handler may need to do something from
+ // TextEventDispatcherListener::NotifyIME() even before there is no
+ // input transaction yet. For example, native IME handler may need to
+ // create new context at receiving NOTIFY_IME_OF_FOCUS. In this case,
+ // mListener may not be initialized since input transaction should be
+ // initialized immediately before dispatching every WidgetKeyboardEvent
+ // and WidgetCompositionEvent (dispatching events always occurs after
+ // focus move).
+ nsCOMPtr<TextEventDispatcherListener> nativeListener =
+ mWidget->GetNativeTextEventDispatcherListener();
+ if (listener != nativeListener && nativeListener) {
+ switch (aIMENotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ // It's not necessary to notify native IME of requests.
+ break;
+ default: {
+ // Even if current input transaction's listener returns NS_OK or
+ // something, we need to notify native IME of notifications because
+ // when user typing after TIP does something, the changed information
+ // is necessary for them.
+ nsresult rv2 = nativeListener->NotifyIME(this, aIMENotification);
+ // But return the result from current listener except when the
+ // notification isn't handled.
+ if (rv == NS_ERROR_NOT_IMPLEMENTED) {
+ rv = rv2;
+ }
+ break;
+ }
+ }
+ }
+
+ if (aIMENotification.mMessage == NOTIFY_IME_OF_FOCUS) {
+ mHasFocus = true;
+ UpdateNotificationRequests();
+ }
+
+ return rv;
+}
+
+void TextEventDispatcher::ClearNotificationRequests() {
+ mIMENotificationRequests = IMENotificationRequests();
+}
+
+void TextEventDispatcher::UpdateNotificationRequests() {
+ ClearNotificationRequests();
+
+ // If it doesn't has focus, no notifications are available.
+ if (!mHasFocus || !mWidget) {
+ return;
+ }
+
+ // If there is a listener, its requests are necessary.
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ mIMENotificationRequests = listener->GetIMENotificationRequests();
+ }
+
+ // Even if this is in non-native input transaction, native IME needs
+ // requests. So, add native IME requests too.
+ if (!IsInNativeInputTransaction()) {
+ nsCOMPtr<TextEventDispatcherListener> nativeListener =
+ mWidget->GetNativeTextEventDispatcherListener();
+ if (nativeListener) {
+ mIMENotificationRequests |= nativeListener->GetIMENotificationRequests();
+ }
+ }
+}
+
+bool TextEventDispatcher::DispatchKeyboardEvent(
+ EventMessage aMessage, const WidgetKeyboardEvent& aKeyboardEvent,
+ nsEventStatus& aStatus, void* aData) {
+ return DispatchKeyboardEventInternal(aMessage, aKeyboardEvent, aStatus,
+ aData);
+}
+
+bool TextEventDispatcher::DispatchKeyboardEventInternal(
+ EventMessage aMessage, const WidgetKeyboardEvent& aKeyboardEvent,
+ nsEventStatus& aStatus, void* aData, uint32_t aIndexOfKeypress,
+ bool aNeedsCallback) {
+ // Note that this method is also used for dispatching key events on a plugin
+ // because key events on a plugin should be dispatched same as normal key
+ // events. Then, only some handlers which need to intercept key events
+ // before the focused plugin (e.g., reserved shortcut key handlers) can
+ // consume the events.
+ MOZ_ASSERT(
+ aMessage == eKeyDown || aMessage == eKeyUp || aMessage == eKeyPress,
+ "Invalid aMessage value");
+ nsresult rv = GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ // If the key shouldn't cause keypress events, don't this patch them.
+ if (aMessage == eKeyPress && !aKeyboardEvent.ShouldCauseKeypressEvents()) {
+ return false;
+ }
+
+ // Basically, key events shouldn't be dispatched during composition.
+ // Note that plugin process has different IME context. Therefore, we don't
+ // need to check our composition state when the key event is fired on a
+ // plugin.
+ if (IsComposing()) {
+ // However, if we need to behave like other browsers, we need the keydown
+ // and keyup events. Note that this behavior is also allowed by D3E spec.
+ // FYI: keypress events must not be fired during composition.
+ if (!StaticPrefs::dom_keyboardevent_dispatch_during_composition() ||
+ aMessage == eKeyPress) {
+ return false;
+ }
+ // XXX If there was mOnlyContentDispatch for this case, it might be useful
+ // because our chrome doesn't assume that key events are fired during
+ // composition.
+ }
+
+ WidgetKeyboardEvent keyEvent(true, aMessage, mWidget);
+ InitEvent(keyEvent);
+ keyEvent.AssignKeyEventData(aKeyboardEvent, false);
+ // Command arrays are not duplicated by AssignKeyEventData() due to
+ // both performance and footprint reasons. So, when TextInputProcessor
+ // emulates real text input or synthesizing keyboard events for tests,
+ // the arrays may be initialized all commands already. If so, we need to
+ // duplicate the arrays here, but we should do this only when we're
+ // dispatching eKeyPress events because BrowserParent::SendRealKeyEvent()
+ // does this only for eKeyPress event. Note that this is not required if
+ // we're in the main process because in the parent process, the edit commands
+ // will be initialized by `ExecuteEditCommands()` (when the event is handled
+ // by editor event listener) or `InitAllEditCommands()` (when the event is
+ // set to a content process). We should test whether these pathes work or
+ // not too.
+ if (XRE_IsContentProcess() && keyEvent.mIsSynthesizedByTIP) {
+ if (aMessage == eKeyPress) {
+ keyEvent.AssignCommands(aKeyboardEvent);
+ } else {
+ // Prevent retriving native edit commands if we're in a content process
+ // because only `eKeyPress` events coming from the main process have
+ // edit commands (See `BrowserParent::SendRealKeyEvent`). And also
+ // retriving edit commands from a content process requires synchonous
+ // IPC and that makes running tests slower. Therefore, we should mark
+ // the `eKeyPress` event does not need to retrieve edit commands anymore.
+ keyEvent.PreventNativeKeyBindings();
+ }
+ }
+
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ // If the key event should be dispatched as consumed event, marking it here.
+ // This is useful to prevent double action. This is intended to the system
+ // has already consumed the event but we need to dispatch the event for
+ // compatibility with older version and other browsers. So, we should not
+ // stop cross process forwarding of them.
+ keyEvent.PreventDefaultBeforeDispatch(CrossProcessForwarding::eAllow);
+ }
+
+ // Corrects each member for the specific key event type.
+ if (keyEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING) {
+ MOZ_ASSERT(!aIndexOfKeypress,
+ "aIndexOfKeypress must be 0 for non-printable key");
+ // If the keyboard event isn't caused by printable key, its charCode should
+ // be 0.
+ keyEvent.SetCharCode(0);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT_IF(aMessage == eKeyDown || aMessage == eKeyUp,
+ !aIndexOfKeypress);
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ aMessage == eKeyPress,
+ aIndexOfKeypress < std::max<size_t>(keyEvent.mKeyValue.Length(), 1));
+ char16_t ch =
+ keyEvent.mKeyValue.IsEmpty() ? 0 : keyEvent.mKeyValue[aIndexOfKeypress];
+ keyEvent.SetCharCode(static_cast<uint32_t>(ch));
+ if (aMessage == eKeyPress) {
+ // keyCode of eKeyPress events of printable keys should be always 0.
+ keyEvent.mKeyCode = 0;
+ // eKeyPress events are dispatched for every character.
+ // So, each key value of eKeyPress events should be a character.
+ if (ch) {
+ if (!IS_SURROGATE(ch)) {
+ keyEvent.mKeyValue.Assign(ch);
+ } else {
+ const bool isHighSurrogateFollowedByLowSurrogate =
+ aIndexOfKeypress + 1 < keyEvent.mKeyValue.Length() &&
+ NS_IS_HIGH_SURROGATE(ch) &&
+ NS_IS_LOW_SURROGATE(keyEvent.mKeyValue[aIndexOfKeypress + 1]);
+ const bool isLowSurrogateFollowingHighSurrogate =
+ !isHighSurrogateFollowedByLowSurrogate && aIndexOfKeypress > 0 &&
+ NS_IS_LOW_SURROGATE(ch) &&
+ NS_IS_HIGH_SURROGATE(keyEvent.mKeyValue[aIndexOfKeypress - 1]);
+ NS_WARNING_ASSERTION(isHighSurrogateFollowedByLowSurrogate ||
+ isLowSurrogateFollowingHighSurrogate,
+ "Lone surrogate input should not happen");
+ if (StaticPrefs::
+ dom_event_keypress_dispatch_once_per_surrogate_pair()) {
+ if (isHighSurrogateFollowedByLowSurrogate) {
+ keyEvent.mKeyValue.Assign(
+ keyEvent.mKeyValue.BeginReading() + aIndexOfKeypress, 2);
+ keyEvent.SetCharCode(
+ SURROGATE_TO_UCS4(ch, keyEvent.mKeyValue[1]));
+ } else if (isLowSurrogateFollowingHighSurrogate) {
+ // Although not dispatching eKeyPress event (because it's already
+ // dispatched for the low surrogate above), the caller should
+ // treat that this dispatched eKeyPress event normally so that
+ // return true here.
+ return true;
+ }
+ // Do not expose ill-formed UTF-16 string because it's a
+ // problematic for Rust-running-as-wasm for example.
+ else {
+ keyEvent.mKeyValue.Truncate();
+ }
+ } else if (!StaticPrefs::
+ dom_event_keypress_key_allow_lone_surrogate()) {
+ // If it's a high surrogate followed by a low surrogate, we should
+ // expose the surrogate pair with .key value.
+ if (isHighSurrogateFollowedByLowSurrogate) {
+ keyEvent.mKeyValue.Assign(
+ keyEvent.mKeyValue.BeginReading() + aIndexOfKeypress, 2);
+ }
+ // Do not expose low surrogate which should be handled by the
+ // preceding keypress event. And also do not expose ill-formed
+ // UTF-16 because it's a problematic for Rust-running-as-wasm for
+ // example.
+ else {
+ keyEvent.mKeyValue.Truncate();
+ }
+ } else {
+ // Here is a path for traditional behavior. We set `.key` to
+ // high-surrogate and low-surrogate separately.
+ keyEvent.mKeyValue.Assign(ch);
+ }
+ }
+ } else {
+ keyEvent.mKeyValue.Truncate();
+ }
+ }
+ }
+ if (aMessage == eKeyUp) {
+ // mIsRepeat of keyup event must be false.
+ keyEvent.mIsRepeat = false;
+ }
+ // mIsComposing should be initialized later.
+ keyEvent.mIsComposing = false;
+ if (mInputTransactionType == eNativeInputTransaction) {
+ // Copy mNativeKeyEvent here because for safety for other users of
+ // AssignKeyEventData(), it doesn't copy this.
+ keyEvent.mNativeKeyEvent = aKeyboardEvent.mNativeKeyEvent;
+ } else {
+ // If it's not a keyboard event for native key event, we should ensure that
+ // mNativeKeyEvent is null.
+ keyEvent.mNativeKeyEvent = nullptr;
+ }
+ // TODO: Manage mUniqueId here.
+
+ // Request the alternative char codes for the key event.
+ // eKeyDown also needs alternative char codes because nsXBLWindowKeyHandler
+ // needs to check if a following keypress event is reserved by chrome for
+ // stopping propagation of its preceding keydown event.
+ keyEvent.mAlternativeCharCodes.Clear();
+ if ((aMessage == eKeyDown || aMessage == eKeyPress) &&
+ (aNeedsCallback || keyEvent.IsControl() || keyEvent.IsAlt() ||
+ keyEvent.IsMeta())) {
+ nsCOMPtr<TextEventDispatcherListener> listener =
+ do_QueryReferent(mListener);
+ if (listener) {
+ DebugOnly<WidgetKeyboardEvent> original(keyEvent);
+ listener->WillDispatchKeyboardEvent(this, keyEvent, aIndexOfKeypress,
+ aData);
+ MOZ_ASSERT(keyEvent.mMessage ==
+ static_cast<WidgetKeyboardEvent&>(original).mMessage);
+ MOZ_ASSERT(keyEvent.mKeyCode ==
+ static_cast<WidgetKeyboardEvent&>(original).mKeyCode);
+ MOZ_ASSERT(keyEvent.mLocation ==
+ static_cast<WidgetKeyboardEvent&>(original).mLocation);
+ MOZ_ASSERT(keyEvent.mIsRepeat ==
+ static_cast<WidgetKeyboardEvent&>(original).mIsRepeat);
+ MOZ_ASSERT(keyEvent.mIsComposing ==
+ static_cast<WidgetKeyboardEvent&>(original).mIsComposing);
+ MOZ_ASSERT(keyEvent.mKeyNameIndex ==
+ static_cast<WidgetKeyboardEvent&>(original).mKeyNameIndex);
+ MOZ_ASSERT(keyEvent.mCodeNameIndex ==
+ static_cast<WidgetKeyboardEvent&>(original).mCodeNameIndex);
+ MOZ_ASSERT(keyEvent.mKeyValue ==
+ static_cast<WidgetKeyboardEvent&>(original).mKeyValue);
+ MOZ_ASSERT(keyEvent.mCodeValue ==
+ static_cast<WidgetKeyboardEvent&>(original).mCodeValue);
+ }
+ }
+
+ if (StaticPrefs::
+ dom_keyboardevent_keypress_dispatch_non_printable_keys_only_system_group_in_content() &&
+ keyEvent.mMessage == eKeyPress &&
+ !keyEvent.ShouldKeyPressEventBeFiredOnContent()) {
+ // Note that even if we set it to true, this may be overwritten by
+ // PresShell::DispatchEventToDOM().
+ keyEvent.mFlags.mOnlySystemGroupDispatchInContent = true;
+ }
+
+ // If an editable element has focus and we're in the parent process, we should
+ // retrieve native key bindings right now because even if it matches with a
+ // reserved shortcut key, it should be handled by the editor.
+ if (XRE_IsParentProcess() && mHasFocus &&
+ (aMessage == eKeyDown || aMessage == eKeyPress)) {
+ keyEvent.InitAllEditCommands(mWritingMode);
+ }
+
+ DispatchInputEvent(mWidget, keyEvent, aStatus);
+ return true;
+}
+
+bool TextEventDispatcher::MaybeDispatchKeypressEvents(
+ const WidgetKeyboardEvent& aKeyboardEvent, nsEventStatus& aStatus,
+ void* aData, bool aNeedsCallback) {
+ // If the key event was consumed, keypress event shouldn't be fired.
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ return false;
+ }
+
+ // If the key shouldn't cause keypress events, don't fire them.
+ if (!aKeyboardEvent.ShouldCauseKeypressEvents()) {
+ return false;
+ }
+
+ // If the key isn't a printable key or just inputting one character or
+ // no character, we should dispatch only one keypress. Otherwise, i.e.,
+ // if the key is a printable key and inputs multiple characters, keypress
+ // event should be dispatched the count of inputting characters times.
+ size_t keypressCount =
+ aKeyboardEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING
+ ? 1
+ : std::max(static_cast<nsAString::size_type>(1),
+ aKeyboardEvent.mKeyValue.Length());
+ bool isDispatched = false;
+ bool consumed = false;
+ for (size_t i = 0; i < keypressCount; i++) {
+ aStatus = nsEventStatus_eIgnore;
+ if (!DispatchKeyboardEventInternal(eKeyPress, aKeyboardEvent, aStatus,
+ aData, i, aNeedsCallback)) {
+ // The widget must have been gone.
+ break;
+ }
+ isDispatched = true;
+ if (!consumed) {
+ consumed = (aStatus == nsEventStatus_eConsumeNoDefault);
+ }
+ }
+
+ // If one of the keypress event was consumed, return ConsumeNoDefault.
+ if (consumed) {
+ aStatus = nsEventStatus_eConsumeNoDefault;
+ }
+
+ return isDispatched;
+}
+
+/******************************************************************************
+ * TextEventDispatcher::PendingComposition
+ *****************************************************************************/
+
+TextEventDispatcher::PendingComposition::PendingComposition() { Clear(); }
+
+void TextEventDispatcher::PendingComposition::Clear() {
+ mString.Truncate();
+ mClauses = nullptr;
+ mCaret.mRangeType = TextRangeType::eUninitialized;
+ mReplacedNativeLineBreakers = false;
+}
+
+void TextEventDispatcher::PendingComposition::EnsureClauseArray() {
+ if (mClauses) {
+ return;
+ }
+ mClauses = new TextRangeArray();
+}
+
+nsresult TextEventDispatcher::PendingComposition::SetString(
+ const nsAString& aString) {
+ MOZ_ASSERT(!mReplacedNativeLineBreakers);
+ mString = aString;
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::PendingComposition::AppendClause(
+ uint32_t aLength, TextRangeType aTextRangeType) {
+ MOZ_ASSERT(!mReplacedNativeLineBreakers);
+
+ if (NS_WARN_IF(!aLength)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ switch (aTextRangeType) {
+ case TextRangeType::eRawClause:
+ case TextRangeType::eSelectedRawClause:
+ case TextRangeType::eConvertedClause:
+ case TextRangeType::eSelectedClause: {
+ EnsureClauseArray();
+ TextRange textRange;
+ textRange.mStartOffset =
+ mClauses->IsEmpty() ? 0 : mClauses->LastElement().mEndOffset;
+ textRange.mEndOffset = textRange.mStartOffset + aLength;
+ textRange.mRangeType = aTextRangeType;
+ mClauses->AppendElement(textRange);
+ return NS_OK;
+ }
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+}
+
+nsresult TextEventDispatcher::PendingComposition::SetCaret(uint32_t aOffset,
+ uint32_t aLength) {
+ MOZ_ASSERT(!mReplacedNativeLineBreakers);
+
+ mCaret.mStartOffset = aOffset;
+ mCaret.mEndOffset = mCaret.mStartOffset + aLength;
+ mCaret.mRangeType = TextRangeType::eCaret;
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::PendingComposition::Set(
+ const nsAString& aString, const TextRangeArray* aRanges) {
+ Clear();
+
+ nsresult rv = SetString(aString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!aRanges || aRanges->IsEmpty()) {
+ // Create dummy range if mString isn't empty.
+ if (!mString.IsEmpty()) {
+ rv = AppendClause(mString.Length(), TextRangeType::eRawClause);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ ReplaceNativeLineBreakers();
+ }
+ return NS_OK;
+ }
+
+ // Adjust offsets in the ranges for XP linefeed character (only \n).
+ for (uint32_t i = 0; i < aRanges->Length(); ++i) {
+ TextRange range = aRanges->ElementAt(i);
+ if (range.mRangeType == TextRangeType::eCaret) {
+ mCaret = range;
+ } else {
+ EnsureClauseArray();
+ mClauses->AppendElement(range);
+ }
+ }
+ ReplaceNativeLineBreakers();
+ return NS_OK;
+}
+
+void TextEventDispatcher::PendingComposition::ReplaceNativeLineBreakers() {
+ mReplacedNativeLineBreakers = true;
+
+ // If the composition string is empty, we don't need to do anything.
+ if (mString.IsEmpty()) {
+ return;
+ }
+
+ nsAutoString nativeString(mString);
+ // Don't expose CRLF nor CR to web contents, instead, use LF.
+ mString.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ mString.ReplaceSubstring(u"\r"_ns, u"\n"_ns);
+
+ // If the length isn't changed, we don't need to adjust any offset and length
+ // of mClauses nor mCaret.
+ if (nativeString.Length() == mString.Length()) {
+ return;
+ }
+
+ if (mClauses) {
+ for (TextRange& clause : *mClauses) {
+ AdjustRange(clause, nativeString);
+ }
+ }
+ if (mCaret.mRangeType == TextRangeType::eCaret) {
+ AdjustRange(mCaret, nativeString);
+ }
+}
+
+// static
+void TextEventDispatcher::PendingComposition::AdjustRange(
+ TextRange& aRange, const nsAString& aNativeString) {
+ TextRange nativeRange = aRange;
+ // XXX Following code wastes runtime cost because this causes computing
+ // mStartOffset for each clause from the start of composition string.
+ // If we'd make TextRange have only its length, we don't need to do
+ // this. However, this must not be so serious problem because
+ // composition string is usually short and separated as a few clauses.
+ if (nativeRange.mStartOffset > 0) {
+ nsAutoString preText(Substring(aNativeString, 0, nativeRange.mStartOffset));
+ preText.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ aRange.mStartOffset = preText.Length();
+ }
+ if (nativeRange.Length() == 0) {
+ aRange.mEndOffset = aRange.mStartOffset;
+ } else {
+ nsAutoString clause(Substring(aNativeString, nativeRange.mStartOffset,
+ nativeRange.Length()));
+ clause.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ aRange.mEndOffset = aRange.mStartOffset + clause.Length();
+ }
+}
+
+nsresult TextEventDispatcher::PendingComposition::Flush(
+ TextEventDispatcher* aDispatcher, nsEventStatus& aStatus,
+ const WidgetEventTime* aEventTime) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsresult rv = aDispatcher->GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (mClauses && !mClauses->IsEmpty() &&
+ mClauses->LastElement().mEndOffset != mString.Length()) {
+ NS_WARNING(
+ "Sum of length of the all clauses must be same as the string "
+ "length");
+ Clear();
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ if (mCaret.mRangeType == TextRangeType::eCaret) {
+ if (mCaret.mEndOffset > mString.Length()) {
+ NS_WARNING("Caret position is out of the composition string");
+ Clear();
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ EnsureClauseArray();
+ mClauses->AppendElement(mCaret);
+ }
+
+ // If the composition string is set without Set(), we need to replace native
+ // line breakers in the composition string with XP line breaker.
+ if (!mReplacedNativeLineBreakers) {
+ ReplaceNativeLineBreakers();
+ }
+
+ RefPtr<TextEventDispatcher> kungFuDeathGrip(aDispatcher);
+ nsCOMPtr<nsIWidget> widget(aDispatcher->mWidget);
+ WidgetCompositionEvent compChangeEvent(true, eCompositionChange, widget);
+ aDispatcher->InitEvent(compChangeEvent);
+ if (aEventTime) {
+ compChangeEvent.AssignEventTime(*aEventTime);
+ }
+ compChangeEvent.mData = mString;
+ // If mString comes from TextInputProcessor, it may be void, but editor
+ // requires non-void string even when it's empty.
+ compChangeEvent.mData.SetIsVoid(false);
+ if (mClauses) {
+ MOZ_ASSERT(!mClauses->IsEmpty(),
+ "mClauses must be non-empty array when it's not nullptr");
+ compChangeEvent.mRanges = mClauses;
+ }
+
+ // While this method dispatches a composition event, some other event handler
+ // cause more clauses to be added. So, we should clear pending composition
+ // before dispatching the event.
+ Clear();
+
+ rv = aDispatcher->StartCompositionAutomaticallyIfNecessary(aStatus,
+ aEventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ return NS_OK;
+ }
+ rv = aDispatcher->DispatchEvent(widget, compChangeEvent, aStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/TextEventDispatcher.h b/widget/TextEventDispatcher.h
new file mode 100644
index 0000000000..6ab131f079
--- /dev/null
+++ b/widget/TextEventDispatcher.h
@@ -0,0 +1,568 @@
+/* -*- 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/. */
+
+#ifndef mozilla_textcompositionsynthesizer_h_
+#define mozilla_textcompositionsynthesizer_h_
+
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "mozilla/TextRange.h"
+#include "mozilla/widget/IMEData.h"
+#include "WritingModes.h"
+
+class nsIWidget;
+
+namespace mozilla {
+namespace widget {
+
+class PuppetWidget;
+
+/**
+ * TextEventDispatcher is a helper class for dispatching widget events defined
+ * in TextEvents.h. Currently, this is a helper for dispatching
+ * WidgetCompositionEvent and WidgetKeyboardEvent. This manages the behavior
+ * of them for conforming to DOM Level 3 Events.
+ * An instance of this class is created by nsIWidget instance and owned by it.
+ * This is typically created only by the top level widgets because only they
+ * handle IME.
+ */
+
+class TextEventDispatcher final {
+ ~TextEventDispatcher() = default;
+
+ NS_INLINE_DECL_REFCOUNTING(TextEventDispatcher)
+
+ public:
+ explicit TextEventDispatcher(nsIWidget* aWidget);
+
+ /**
+ * Initializes the instance for IME or automated test. Either IME or tests
+ * need to call one of them before starting composition. If they return
+ * NS_ERROR_ALREADY_INITIALIZED, it means that the listener already listens
+ * notifications from TextEventDispatcher for same purpose (for IME or tests).
+ * If this returns another error, the caller shouldn't keep starting
+ * composition.
+ *
+ * @param aListener Specify the listener to listen notifications and
+ * requests. This must not be null.
+ * NOTE: aListener is stored as weak reference in
+ * TextEventDispatcher. See mListener
+ * definition below.
+ */
+ nsresult BeginInputTransaction(TextEventDispatcherListener* aListener);
+ nsresult BeginTestInputTransaction(TextEventDispatcherListener* aListener,
+ bool aIsAPZAware);
+ nsresult BeginNativeInputTransaction();
+
+ /**
+ * BeginInputTransactionFor() should be used when aPuppetWidget dispatches
+ * a composition or keyboard event coming from its parent process.
+ */
+ nsresult BeginInputTransactionFor(const WidgetGUIEvent* aEvent,
+ PuppetWidget* aPuppetWidget);
+
+ /**
+ * EndInputTransaction() should be called when the listener stops using
+ * the TextEventDispatcher.
+ *
+ * @param aListener The listener using the TextEventDispatcher instance.
+ */
+ void EndInputTransaction(TextEventDispatcherListener* aListener);
+
+ /**
+ * OnDestroyWidget() is called when mWidget is being destroyed.
+ */
+ void OnDestroyWidget();
+
+ nsIWidget* GetWidget() const { return mWidget; }
+
+ /**
+ * Return true starting from ending handling focus notification and until
+ * receiving blur notification.
+ */
+ bool HasFocus() const { return mHasFocus; }
+
+ const IMENotificationRequests& IMENotificationRequestsRef() const {
+ return mIMENotificationRequests;
+ }
+
+ /**
+ * OnWidgetChangeIMENotificationRequests() is called when aWidget's
+ * IMENotificationRequest is maybe modified by unusual path. E.g.,
+ * modified in an async path.
+ */
+ void OnWidgetChangeIMENotificationRequests(nsIWidget* aWidget) {
+ MOZ_ASSERT(aWidget);
+ if (mWidget == aWidget) {
+ UpdateNotificationRequests();
+ }
+ }
+
+ /**
+ * GetState() returns current state of this class.
+ *
+ * @return NS_OK: Fine to compose text.
+ * NS_ERROR_NOT_INITIALIZED: BeginInputTransaction() or
+ * BeginInputTransactionForTests()
+ * should be called.
+ * NS_ERROR_NOT_AVAILABLE: The widget isn't available for
+ * composition.
+ */
+ nsresult GetState() const;
+
+ /**
+ * IsComposing() returns true after calling StartComposition() and before
+ * calling CommitComposition(). In other words, native IME has composition
+ * when this returns true.
+ */
+ bool IsComposing() const { return mIsComposing; }
+
+ /**
+ * IsHandlingComposition() returns true after calling StartComposition() and
+ * content has not handled eCompositionCommit(AsIs) event. In other words,
+ * our content has composition when this returns true.
+ */
+ bool IsHandlingComposition() const { return mIsHandlingComposition; }
+
+ /**
+ * IsInNativeInputTransaction() returns true if native IME handler began a
+ * transaction and it's not finished yet.
+ */
+ bool IsInNativeInputTransaction() const {
+ return mInputTransactionType == eNativeInputTransaction;
+ }
+
+ /**
+ * IsDispatchingEvent() returns true while this instance dispatching an event.
+ */
+ bool IsDispatchingEvent() const { return mDispatchingEvent > 0; }
+
+ /**
+ * GetPseudoIMEContext() returns pseudo native IME context if there is an
+ * input transaction whose type is not for native event handler.
+ * Otherwise, returns nullptr.
+ */
+ void* GetPseudoIMEContext() const {
+ if (mInputTransactionType == eNoInputTransaction ||
+ mInputTransactionType == eNativeInputTransaction) {
+ return nullptr;
+ }
+ return const_cast<TextEventDispatcher*>(this);
+ }
+
+ /**
+ * Return writing mode at selection while this has focus. Otherwise, or
+ * never exists selection ranges, this returns Nothing.
+ */
+ const Maybe<WritingMode>& MaybeWritingModeRefAtSelection() const {
+ return mWritingMode;
+ }
+
+ /**
+ * MaybeQueryWritingModeAtSelection() returns writing mode at current
+ * selection even if this does not have focus. If this is not focused, this
+ * queries selection. Then, chrome script can run due to flushing the layout
+ * if an element in chrome has focus (but it should not cause any problem
+ * hopefully).
+ */
+ MOZ_CAN_RUN_SCRIPT Maybe<WritingMode> MaybeQueryWritingModeAtSelection()
+ const;
+
+ /**
+ * StartComposition() starts composition explicitly.
+ *
+ * @param aEventTime If this is not nullptr, WidgetCompositionEvent will
+ * be initialized with this. Otherwise, initialized
+ * with the time at initializing.
+ */
+ nsresult StartComposition(nsEventStatus& aStatus,
+ const WidgetEventTime* aEventTime = nullptr);
+
+ /**
+ * CommitComposition() commits composition.
+ *
+ * @param aCommitString If this is null, commits with the last composition
+ * string. Otherwise, commits the composition with
+ * this value.
+ * @param aEventTime If this is not nullptr, WidgetCompositionEvent will
+ * be initialized with this. Otherwise, initialized
+ * with the time at initializing.
+ */
+ nsresult CommitComposition(nsEventStatus& aStatus,
+ const nsAString* aCommitString = nullptr,
+ const WidgetEventTime* aEventTime = nullptr);
+
+ /**
+ * SetPendingCompositionString() sets new composition string which will be
+ * dispatched with eCompositionChange event by calling Flush().
+ *
+ * @param aString New composition string.
+ */
+ nsresult SetPendingCompositionString(const nsAString& aString) {
+ return mPendingComposition.SetString(aString);
+ }
+
+ /**
+ * AppendClauseToPendingComposition() appends a clause information to
+ * the pending composition string.
+ *
+ * @param aLength Length of the clause.
+ * @param aTextRangeType One of TextRangeType::eRawClause,
+ * TextRangeType::eSelectedRawClause,
+ * TextRangeType::eConvertedClause or
+ * TextRangeType::eSelectedClause.
+ */
+ nsresult AppendClauseToPendingComposition(uint32_t aLength,
+ TextRangeType aTextRangeType) {
+ return mPendingComposition.AppendClause(aLength, aTextRangeType);
+ }
+
+ /**
+ * SetCaretInPendingComposition() sets caret position in the pending
+ * composition string and its length. This is optional. If IME doesn't
+ * want to show caret, it shouldn't need to call this.
+ *
+ * @param aOffset Offset of the caret in the pending composition
+ * string. This should not be larger than the length
+ * of the pending composition string.
+ * @param aLength Caret width. If this is 0, caret will be collapsed.
+ * Note that Gecko doesn't supported wide caret yet,
+ * therefore, this is ignored for now.
+ */
+ nsresult SetCaretInPendingComposition(uint32_t aOffset, uint32_t aLength) {
+ return mPendingComposition.SetCaret(aOffset, aLength);
+ }
+
+ /**
+ * SetPendingComposition() is useful if native IME handler already creates
+ * array of clauses and/or caret information.
+ *
+ * @param aString Composition string. This may include native line
+ * breakers since they will be replaced with XP line
+ * breakers automatically.
+ * @param aRanges This should include the ranges of clauses and/or
+ * a range of caret. Note that this method allows
+ * some ranges overlap each other and the range order
+ * is not from start to end.
+ */
+ nsresult SetPendingComposition(const nsAString& aString,
+ const TextRangeArray* aRanges) {
+ return mPendingComposition.Set(aString, aRanges);
+ }
+
+ /**
+ * FlushPendingComposition() sends the pending composition string
+ * to the widget of the store DOM window. Before calling this, IME needs to
+ * set pending composition string with SetPendingCompositionString(),
+ * AppendClauseToPendingComposition() and/or
+ * SetCaretInPendingComposition().
+ *
+ * @param aEventTime If this is not nullptr, WidgetCompositionEvent will
+ * be initialized with this. Otherwise, initialized
+ * with the time at initializing.
+ */
+ nsresult FlushPendingComposition(
+ nsEventStatus& aStatus, const WidgetEventTime* aEventTime = nullptr) {
+ return mPendingComposition.Flush(this, aStatus, aEventTime);
+ }
+
+ /**
+ * ClearPendingComposition() makes this instance forget pending composition.
+ */
+ void ClearPendingComposition() { mPendingComposition.Clear(); }
+
+ /**
+ * GetPendingCompositionClauses() returns text ranges which was appended by
+ * AppendClauseToPendingComposition() or SetPendingComposition().
+ */
+ const TextRangeArray* GetPendingCompositionClauses() const {
+ return mPendingComposition.GetClauses();
+ }
+
+ /**
+ * @see nsIWidget::NotifyIME()
+ */
+ // Calling NotifyIME may call OS's API so that everything could happen.
+ // We should mark it MOZ_CAN_RUN_SCRIPT later.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+ NotifyIME(const IMENotification& aIMENotification);
+
+ /**
+ * DispatchKeyboardEvent() maybe dispatches aKeyboardEvent.
+ *
+ * @param aMessage Must be eKeyDown or eKeyUp.
+ * Use MaybeDispatchKeypressEvents() for dispatching
+ * eKeyPress.
+ * @param aKeyboardEvent A keyboard event.
+ * @param aStatus If dispatching event should be marked as consumed,
+ * set nsEventStatus_eConsumeNoDefault. Otherwise,
+ * set nsEventStatus_eIgnore. After dispatching
+ * a event and it's consumed this returns
+ * nsEventStatus_eConsumeNoDefault.
+ * @param aData Calling this method may cause calling
+ * WillDispatchKeyboardEvent() of the listener.
+ * aData will be set to its argument.
+ * @return true if an event is dispatched. Otherwise, false.
+ */
+ bool DispatchKeyboardEvent(EventMessage aMessage,
+ const WidgetKeyboardEvent& aKeyboardEvent,
+ nsEventStatus& aStatus, void* aData = nullptr);
+
+ /**
+ * MaybeDispatchKeypressEvents() maybe dispatches a keypress event which is
+ * generated from aKeydownEvent.
+ *
+ * @param aKeyboardEvent A keyboard event.
+ * @param aStatus Sets the result when the caller dispatches
+ * aKeyboardEvent. Note that if the value is
+ * nsEventStatus_eConsumeNoDefault, this does NOT
+ * dispatch keypress events.
+ * When this method dispatches one or more keypress
+ * events and one of them is consumed, this returns
+ * nsEventStatus_eConsumeNoDefault.
+ * @param aData Calling this method may cause calling
+ * WillDispatchKeyboardEvent() of the listener.
+ * aData will be set to its argument.
+ * @param aNeedsCallback Set true when caller needs to initialize each
+ * eKeyPress event immediately before dispatch.
+ * Then, WillDispatchKeyboardEvent() is always called.
+ * @return true if one or more events are dispatched.
+ * Otherwise, false.
+ */
+ bool MaybeDispatchKeypressEvents(const WidgetKeyboardEvent& aKeyboardEvent,
+ nsEventStatus& aStatus,
+ void* aData = nullptr,
+ bool aNeedsCallback = false);
+
+ private:
+ // mWidget is owner of the instance. When this is created, this is set.
+ // And when mWidget is released, this is cleared by OnDestroyWidget().
+ // Note that mWidget may be destroyed already (i.e., mWidget->Destroyed() may
+ // return true).
+ nsIWidget* mWidget;
+ // mListener is a weak reference to TextEventDispatcherListener. That might
+ // be referred by JS. Therefore, the listener might be difficult to release
+ // itself if this is a strong reference. Additionally, it's difficult to
+ // check if a method to uninstall the listener is called by valid instance.
+ // So, using weak reference is the best way in this case.
+ nsWeakPtr mListener;
+ // mIMENotificationRequests should store current IME's notification requests.
+ // So, this may be invalid when IME doesn't have focus.
+ IMENotificationRequests mIMENotificationRequests;
+ // mWritingMode stores writing mode at current selection starting from
+ // receiving focus notification and until receiving blur notification. When
+ // selection is changed, this is updated by every selection change
+ // notification.
+ Maybe<WritingMode> mWritingMode;
+
+ // mPendingComposition stores new composition string temporarily.
+ // These values will be used for dispatching eCompositionChange event
+ // in Flush(). When Flush() is called, the members will be cleared
+ // automatically.
+ class PendingComposition {
+ public:
+ PendingComposition();
+ nsresult SetString(const nsAString& aString);
+ nsresult AppendClause(uint32_t aLength, TextRangeType aTextRangeType);
+ nsresult SetCaret(uint32_t aOffset, uint32_t aLength);
+ nsresult Set(const nsAString& aString, const TextRangeArray* aRanges);
+ nsresult Flush(TextEventDispatcher* aDispatcher, nsEventStatus& aStatus,
+ const WidgetEventTime* aEventTime);
+ const TextRangeArray* GetClauses() const { return mClauses; }
+ void Clear();
+
+ private:
+ nsString mString;
+ RefPtr<TextRangeArray> mClauses;
+ TextRange mCaret;
+ bool mReplacedNativeLineBreakers;
+
+ void EnsureClauseArray();
+
+ /**
+ * ReplaceNativeLineBreakers() replaces "\r\n" and "\r" to "\n" and adjust
+ * each clause information and the caret information.
+ */
+ void ReplaceNativeLineBreakers();
+
+ /**
+ * AdjustRange() adjusts aRange as in the string with XP line breakers.
+ *
+ * @param aRange The reference to a range in aNativeString.
+ * This will be modified.
+ * @param aNativeString The string with native line breakers.
+ * This may include "\r\n" and/or "\r".
+ */
+ static void AdjustRange(TextRange& aRange, const nsAString& aNativeString);
+ };
+ PendingComposition mPendingComposition;
+
+ // While dispatching an event, this is incremented.
+ uint16_t mDispatchingEvent;
+
+ enum InputTransactionType : uint8_t {
+ // No input transaction has been started.
+ eNoInputTransaction,
+ // Input transaction for native IME or keyboard event handler. Note that
+ // keyboard events may be dispatched via parent process if there is.
+ // In remote processes, this is also used when events come from the parent
+ // process and are not for tests because we cannot distinguish if
+ // TextEventDispatcher has which type of transaction when it dispatches
+ // (eNativeInputTransaction or eSameProcessSyncInputTransaction).
+ eNativeInputTransaction,
+ // Input transaction for automated tests which are APZ-aware. Note that
+ // keyboard events may be dispatched via parent process if there is.
+ eAsyncTestInputTransaction,
+ // Input transaction for automated tests which assume events are fired
+ // synchronously. I.e., keyboard events are always dispatched in the
+ // current process.
+ // In remote processes, this is also used when events come from the parent
+ // process and are not dispatched by the instance itself for APZ-aware
+ // tests because this instance won't dispatch the events via the parent
+ // process again.
+ eSameProcessSyncTestInputTransaction,
+ // Input transaction for others (currently, only FuzzingFunctions).
+ // Events are fired synchronously in the process.
+ // XXX Should we make this async for testing default action handlers in
+ // the main process?
+ eSameProcessSyncInputTransaction
+ };
+
+ InputTransactionType mInputTransactionType;
+
+ bool IsForTests() const {
+ return mInputTransactionType == eAsyncTestInputTransaction ||
+ mInputTransactionType == eSameProcessSyncTestInputTransaction;
+ }
+
+ // ShouldSendInputEventToAPZ() returns true when WidgetInputEvent should
+ // be dispatched via its parent process (if there is) for APZ. Otherwise,
+ // when the input transaction is for IME of B2G or automated tests which
+ // isn't APZ-aware, WidgetInputEvent should be dispatched form current
+ // process directly.
+ bool ShouldSendInputEventToAPZ() const {
+ switch (mInputTransactionType) {
+ case eNativeInputTransaction:
+ case eAsyncTestInputTransaction:
+ return true;
+ case eSameProcessSyncTestInputTransaction:
+ case eSameProcessSyncInputTransaction:
+ return false;
+ case eNoInputTransaction:
+ NS_WARNING(
+ "Why does the caller need to dispatch an event when "
+ "there is no input transaction?");
+ return true;
+ default:
+ MOZ_CRASH("Define the behavior of new InputTransactionType");
+ }
+ }
+
+ // See IsComposing().
+ bool mIsComposing;
+
+ // See IsHandlingComposition().
+ bool mIsHandlingComposition;
+
+ // true while NOTIFY_IME_OF_FOCUS is received but NOTIFY_IME_OF_BLUR has not
+ // received yet. Otherwise, false.
+ bool mHasFocus;
+
+ nsresult BeginInputTransactionInternal(TextEventDispatcherListener* aListener,
+ InputTransactionType aType);
+
+ /**
+ * InitEvent() initializes aEvent. This must be called before dispatching
+ * the event.
+ */
+ void InitEvent(WidgetGUIEvent& aEvent) const;
+
+ /**
+ * DispatchEvent() dispatches aEvent on aWidget.
+ */
+ nsresult DispatchEvent(nsIWidget* aWidget, WidgetGUIEvent& aEvent,
+ nsEventStatus& aStatus);
+
+ /**
+ * DispatchInputEvent() dispatches aEvent on aWidget.
+ */
+ nsresult DispatchInputEvent(nsIWidget* aWidget, WidgetInputEvent& aEvent,
+ nsEventStatus& aStatus);
+
+ /**
+ * StartCompositionAutomaticallyIfNecessary() starts composition if it hasn't
+ * been started it yet.
+ *
+ * @param aStatus If it succeeded to start composition normally, this
+ * returns nsEventStatus_eIgnore. Otherwise, e.g.,
+ * the composition is canceled during dispatching
+ * compositionstart event, this returns
+ * nsEventStatus_eConsumeNoDefault. In this case,
+ * the caller shouldn't keep doing its job.
+ * @param aEventTime If this is not nullptr, WidgetCompositionEvent will
+ * be initialized with this. Otherwise, initialized
+ * with the time at initializing.
+ * @return Only when something unexpected occurs, this returns
+ * an error. Otherwise, returns NS_OK even if aStatus
+ * is nsEventStatus_eConsumeNoDefault.
+ */
+ nsresult StartCompositionAutomaticallyIfNecessary(
+ nsEventStatus& aStatus, const WidgetEventTime* aEventTime);
+
+ /**
+ * DispatchKeyboardEventInternal() maybe dispatches aKeyboardEvent.
+ *
+ * @param aMessage Must be eKeyDown, eKeyUp or eKeyPress.
+ * @param aKeyboardEvent A keyboard event. If aMessage is eKeyPress and
+ * the event is for second or later character, its
+ * mKeyValue should be empty string.
+ * @param aStatus If dispatching event should be marked as consumed,
+ * set nsEventStatus_eConsumeNoDefault. Otherwise,
+ * set nsEventStatus_eIgnore. After dispatching
+ * a event and it's consumed this returns
+ * nsEventStatus_eConsumeNoDefault.
+ * @param aData Calling this method may cause calling
+ * WillDispatchKeyboardEvent() of the listener.
+ * aData will be set to its argument.
+ * @param aIndexOfKeypress This must be 0 if aMessage isn't eKeyPress or
+ * aKeyboard.mKeyNameIndex isn't
+ * KEY_NAME_INDEX_USE_STRING. Otherwise, i.e.,
+ * when an eKeyPress event causes inputting
+ * text, this must be between 0 and
+ * mKeyValue.Length() - 1 since keypress events
+ * sending only one character per event.
+ * @param aNeedsCallback Set true when caller needs to initialize each
+ * eKeyPress event immediately before dispatch.
+ * Then, WillDispatchKeyboardEvent() is always called.
+ * @return true if an event is dispatched. Otherwise, false.
+ */
+ // TODO: Mark this as MOZ_CAN_RUN_SCRIPT instead.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool DispatchKeyboardEventInternal(
+ EventMessage aMessage, const WidgetKeyboardEvent& aKeyboardEvent,
+ nsEventStatus& aStatus, void* aData, uint32_t aIndexOfKeypress = 0,
+ bool aNeedsCallback = false);
+
+ /**
+ * ClearNotificationRequests() clears mIMENotificationRequests.
+ */
+ void ClearNotificationRequests();
+
+ /**
+ * UpdateNotificationRequests() updates mIMENotificationRequests with
+ * current state. If the instance doesn't have focus, this clears
+ * mIMENotificationRequests. Otherwise, updates it with both requests of
+ * current listener and native listener.
+ */
+ void UpdateNotificationRequests();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef mozilla_widget_textcompositionsynthesizer_h_
diff --git a/widget/TextEventDispatcherListener.h b/widget/TextEventDispatcherListener.h
new file mode 100644
index 0000000000..cd846c6d38
--- /dev/null
+++ b/widget/TextEventDispatcherListener.h
@@ -0,0 +1,95 @@
+/* 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/. */
+
+#ifndef mozilla_textinputdispatcherlistener_h_
+#define mozilla_textinputdispatcherlistener_h_
+
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace widget {
+
+class TextEventDispatcher;
+struct IMENotification;
+struct IMENotificationRequests;
+
+#define NS_TEXT_INPUT_PROXY_LISTENER_IID \
+ {0xf2226f55, \
+ 0x6ddb, \
+ 0x40d5, \
+ {0x8a, 0x24, 0xce, 0x4d, 0x5b, 0x38, 0x15, 0xf0}};
+
+class TextEventDispatcherListener : public nsSupportsWeakReference {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_TEXT_INPUT_PROXY_LISTENER_IID)
+
+ /**
+ * NotifyIME() is called by TextEventDispatcher::NotifyIME(). This is a
+ * notification or request to IME. See document of nsIWidget::NotifyIME()
+ * for the detail.
+ */
+ NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) = 0;
+
+ /**
+ * Returns preference for which IME notification are received by NotifyIME().
+ */
+ NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() = 0;
+
+ /**
+ * OnRemovedFrom() is called when the TextEventDispatcher stops working and
+ * is releasing the listener.
+ */
+ NS_IMETHOD_(void)
+ OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) = 0;
+
+ /**
+ * WillDispatchKeyboardEvent() may be called immediately before
+ * TextEventDispatcher dispatching a keyboard event. This is called only
+ * during calling TextEventDispatcher::DispatchKeyboardEvent() or
+ * TextEventDispatcher::MaybeDispatchKeypressEvents(). But this may not
+ * be called if TextEventDispatcher thinks that the keyboard event doesn't
+ * need alternative char codes.
+ *
+ * This method can overwrite any members of aKeyboardEvent which is already
+ * initialized by TextEventDispatcher. If it's necessary, this method should
+ * overwrite the charCode when Control key is pressed. TextEventDispatcher
+ * computes charCode from mKeyValue. However, when Control key is pressed,
+ * charCode should be an ASCII char. In such case, this method needs to
+ * overwrite it properly.
+ *
+ * @param aTextEventDispatcher Pointer to the caller.
+ * @param aKeyboardEvent The event trying to dispatch.
+ * This is already initialized, but if it's
+ * necessary, this method should overwrite the
+ * members and set alternative char codes.
+ * @param aIndexOfKeypress When aKeyboardEvent is eKeyPress event,
+ * it may be a sequence of keypress events
+ * if the key causes multiple characters.
+ * In such case, this indicates the index from
+ * first keypress event.
+ * If aKeyboardEvent is the first eKeyPress or
+ * other events, this value is 0.
+ * @param aData The pointer which was specified at calling
+ * the method of TextEventDispatcher.
+ * For example, if you do:
+ * |TextEventDispatcher->DispatchKeyboardEvent(
+ * eKeyDown, event, status, this);|
+ * Then, aData of this method becomes |this|.
+ * Finally, you can use it like:
+ * |static_cast<NativeEventHandler*>(aData)|
+ */
+ NS_IMETHOD_(void)
+ WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(TextEventDispatcherListener,
+ NS_TEXT_INPUT_PROXY_LISTENER_IID)
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef mozilla_textinputdispatcherlistener_h_
diff --git a/widget/TextEvents.h b/widget/TextEvents.h
new file mode 100644
index 0000000000..71d2e656e2
--- /dev/null
+++ b/widget/TextEvents.h
@@ -0,0 +1,1511 @@
+/* -*- 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/. */
+
+#ifndef mozilla_TextEvents_h__
+#define mozilla_TextEvents_h__
+
+#include <stdint.h>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EventForwards.h" // for KeyNameIndex, temporarily
+#include "mozilla/FontRange.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/TextRange.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/widget/IMEData.h"
+#include "mozilla/ipc/IPCForwards.h"
+#include "nsCOMPtr.h"
+#include "nsHashtablesFwd.h"
+#include "nsISelectionListener.h"
+#include "nsITransferable.h"
+#include "nsRect.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsStringHashKey;
+
+/******************************************************************************
+ * virtual keycode values
+ ******************************************************************************/
+
+enum {
+#define NS_DEFINE_VK(aDOMKeyName, aDOMKeyCode) NS_##aDOMKeyName = aDOMKeyCode,
+#include "mozilla/VirtualKeyCodeList.h"
+#undef NS_DEFINE_VK
+ NS_VK_UNKNOWN = 0xFF
+};
+
+namespace mozilla {
+
+enum : uint32_t {
+ eKeyLocationStandard = dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_STANDARD,
+ eKeyLocationLeft = dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_LEFT,
+ eKeyLocationRight = dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_RIGHT,
+ eKeyLocationNumpad = dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_NUMPAD
+};
+
+const nsCString GetDOMKeyCodeName(uint32_t aKeyCode);
+
+namespace dom {
+class PBrowserParent;
+class PBrowserChild;
+} // namespace dom
+namespace plugins {
+class PPluginInstanceChild;
+} // namespace plugins
+
+enum class AccessKeyType {
+ // Handle access key for chrome.
+ eChrome,
+ // Handle access key for content.
+ eContent,
+ // Don't handle access key.
+ eNone
+};
+
+/******************************************************************************
+ * mozilla::AlternativeCharCode
+ *
+ * This stores alternative charCode values of a key event with some modifiers.
+ * The stored values proper for testing shortcut key or access key.
+ ******************************************************************************/
+
+struct AlternativeCharCode {
+ AlternativeCharCode() = default;
+ AlternativeCharCode(uint32_t aUnshiftedCharCode, uint32_t aShiftedCharCode)
+ : mUnshiftedCharCode(aUnshiftedCharCode),
+ mShiftedCharCode(aShiftedCharCode) {}
+
+ uint32_t mUnshiftedCharCode = 0u;
+ uint32_t mShiftedCharCode = 0u;
+
+ bool operator==(const AlternativeCharCode& aOther) const {
+ return mUnshiftedCharCode == aOther.mUnshiftedCharCode &&
+ mShiftedCharCode == aOther.mShiftedCharCode;
+ }
+ bool operator!=(const AlternativeCharCode& aOther) const {
+ return !(*this == aOther);
+ }
+};
+
+/******************************************************************************
+ * mozilla::ShortcutKeyCandidate
+ *
+ * This stores a candidate of shortcut key combination.
+ ******************************************************************************/
+
+struct ShortcutKeyCandidate {
+ enum class ShiftState : bool {
+ // Can ignore `Shift` modifier state when comparing the key combination.
+ // E.g., Ctrl + Shift + `:` in the US keyboard layout may match with
+ // Ctrl + `:` shortcut.
+ Ignorable,
+ // `Shift` modifier state should be respected. I.e., Ctrl + `;` in the US
+ // keyboard layout never matches with Ctrl + Shift + `;` shortcut.
+ MatchExactly,
+ };
+
+ enum class SkipIfEarlierHandlerDisabled : bool {
+ // Even if an earlier handler is disabled, this may match with another
+ // handler for avoiding inaccessible shortcut with the active keyboard
+ // layout.
+ No,
+ // If an earlier handler (i.e., preferred handler) is disabled, this should
+ // not try to match. E.g., Ctrl + `-` in the French keyboard layout when
+ // the zoom level is the minimum value, it should not match with Ctrl + `6`
+ // shortcut (French keyboard layout introduces `-` when pressing Digit6 key
+ // without Shift, and Shift + Digit6 introduces `6`).
+ Yes,
+ };
+
+ ShortcutKeyCandidate() = default;
+ ShortcutKeyCandidate(
+ uint32_t aCharCode, ShiftState aShiftState,
+ SkipIfEarlierHandlerDisabled aSkipIfEarlierHandlerDisabled)
+ : mCharCode(aCharCode),
+ mShiftState(aShiftState),
+ mSkipIfEarlierHandlerDisabled(aSkipIfEarlierHandlerDisabled) {}
+
+ // The mCharCode value which must match keyboard shortcut definition.
+ uint32_t mCharCode = 0;
+
+ ShiftState mShiftState = ShiftState::MatchExactly;
+ SkipIfEarlierHandlerDisabled mSkipIfEarlierHandlerDisabled =
+ SkipIfEarlierHandlerDisabled::No;
+};
+
+/******************************************************************************
+ * mozilla::IgnoreModifierState
+ *
+ * This stores flags for modifiers that should be ignored when matching
+ * XBL handlers.
+ ******************************************************************************/
+
+struct IgnoreModifierState {
+ // When mShift is true, Shift key state will be ignored.
+ bool mShift;
+ // When mMeta is true, Meta key state will be ignored.
+ bool mMeta;
+
+ IgnoreModifierState() : mShift(false), mMeta(false) {}
+};
+
+/******************************************************************************
+ * mozilla::WidgetKeyboardEvent
+ ******************************************************************************/
+
+class WidgetKeyboardEvent final : public WidgetInputEvent {
+ private:
+ friend class dom::PBrowserParent;
+ friend class dom::PBrowserChild;
+ friend struct IPC::ParamTraits<WidgetKeyboardEvent>;
+ ALLOW_DEPRECATED_READPARAM
+
+ protected:
+ WidgetKeyboardEvent()
+ : mNativeKeyEvent(nullptr),
+ mKeyCode(0),
+ mCharCode(0),
+ mPseudoCharCode(0),
+ mLocation(eKeyLocationStandard),
+ mUniqueId(0),
+ mKeyNameIndex(KEY_NAME_INDEX_Unidentified),
+ mCodeNameIndex(CODE_NAME_INDEX_UNKNOWN),
+ mIsRepeat(false),
+ mIsComposing(false),
+ mIsSynthesizedByTIP(false),
+ mMaybeSkippableInRemoteProcess(true),
+ mUseLegacyKeyCodeAndCharCodeValues(false),
+ mEditCommandsForSingleLineEditorInitialized(false),
+ mEditCommandsForMultiLineEditorInitialized(false),
+ mEditCommandsForRichTextEditorInitialized(false) {}
+
+ public:
+ WidgetKeyboardEvent* AsKeyboardEvent() override { return this; }
+
+ WidgetKeyboardEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget,
+ EventClassID aEventClassID = eKeyboardEventClass,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetInputEvent(aIsTrusted, aMessage, aWidget, aEventClassID, aTime),
+ mNativeKeyEvent(nullptr),
+ mKeyCode(0),
+ mCharCode(0),
+ mPseudoCharCode(0),
+ mLocation(eKeyLocationStandard),
+ mUniqueId(0),
+ mKeyNameIndex(KEY_NAME_INDEX_Unidentified),
+ mCodeNameIndex(CODE_NAME_INDEX_UNKNOWN),
+ mIsRepeat(false),
+ mIsComposing(false),
+ mIsSynthesizedByTIP(false),
+ mMaybeSkippableInRemoteProcess(true),
+ mUseLegacyKeyCodeAndCharCodeValues(false),
+ mEditCommandsForSingleLineEditorInitialized(false),
+ mEditCommandsForMultiLineEditorInitialized(false),
+ mEditCommandsForRichTextEditorInitialized(false) {}
+
+ // IsInputtingText() and IsInputtingLineBreak() are used to check if
+ // it should cause eKeyPress events even on web content.
+ // UI Events defines that "keypress" event should be fired "if and only if
+ // that key normally produces a character value".
+ // <https://www.w3.org/TR/uievents/#event-type-keypress>
+ // Additionally, for backward compatiblity with all existing browsers,
+ // there is a spec issue for Enter key press.
+ // <https://github.com/w3c/uievents/issues/183>
+ bool IsInputtingText() const {
+ // NOTE: On some keyboard layout, some characters are inputted with Control
+ // key or Alt key, but at that time, widget clears the modifier flag
+ // from eKeyPress event because our TextEditor won't handle eKeyPress
+ // events as inputting text (bug 1346832).
+ // NOTE: There are some complicated issues of our traditional behavior.
+ // -- On Windows, KeyboardLayout::WillDispatchKeyboardEvent() clears
+ // MODIFIER_ALT and MODIFIER_CONTROL of eKeyPress event if it
+ // should be treated as inputting a character because AltGr is
+ // represented with both Alt key and Ctrl key are pressed, and
+ // some keyboard layouts may produces a character with Ctrl key.
+ // -- On Linux, KeymapWrapper doesn't have this hack since perhaps,
+ // we don't have any bug reports that user cannot input proper
+ // character with Alt and/or Ctrl key.
+ // -- On macOS, IMEInputHandler::WillDispatchKeyboardEvent() clears
+ // MODIFIER_ALT and MDOFIEIR_CONTROL of eKeyPress event only when
+ // TextInputHandler::InsertText() has been called for the event.
+ // I.e., they are cleared only when an editor has focus (even if IME
+ // is disabled in password field or by |ime-mode: disabled;|) because
+ // TextInputHandler::InsertText() is called while
+ // TextInputHandler::HandleKeyDownEvent() calls interpretKeyEvents:
+ // to notify text input processor of Cocoa (including IME). In other
+ // words, when we need to disable IME completey when no editor has
+ // focus, we cannot call interpretKeyEvents:. So,
+ // TextInputHandler::InsertText() won't be called when no editor has
+ // focus so that neither MODIFIER_ALT nor MODIFIER_CONTROL is
+ // cleared. So, fortunately, altKey and ctrlKey values of "keypress"
+ // events are same as the other browsers only when no editor has
+ // focus.
+ // NOTE: As mentioned above, for compatibility with the other browsers on
+ // macOS, we should keep MODIFIER_ALT and MODIFIER_CONTROL flags of
+ // eKeyPress events when no editor has focus. However, Alt key,
+ // labeled "option" on keyboard for Mac, is AltGraph key on the other
+ // platforms. So, even if MODIFIER_ALT is set, we need to dispatch
+ // eKeyPress event even on web content unless mCharCode is 0.
+ // Therefore, we need to ignore MODIFIER_ALT flag here only on macOS.
+ return mMessage == eKeyPress && mCharCode &&
+ !(mModifiers & (
+#ifndef XP_MACOSX
+ // So, ignore MODIFIER_ALT only on macOS since
+ // option key is used as AltGraph key on macOS.
+ MODIFIER_ALT |
+#endif // #ifndef XP_MAXOSX
+ MODIFIER_CONTROL | MODIFIER_META));
+ }
+
+ bool IsInputtingLineBreak() const {
+ return mMessage == eKeyPress && mKeyNameIndex == KEY_NAME_INDEX_Enter &&
+ !(mModifiers & (MODIFIER_ALT | MODIFIER_CONTROL | MODIFIER_META));
+ }
+
+ /**
+ * ShouldKeyPressEventBeFiredOnContent() should be called only when the
+ * instance is eKeyPress event. This returns true when the eKeyPress
+ * event should be fired even on content in the default event group.
+ */
+ bool ShouldKeyPressEventBeFiredOnContent() const {
+ MOZ_DIAGNOSTIC_ASSERT(mMessage == eKeyPress);
+ if (IsInputtingText() || IsInputtingLineBreak()) {
+ return true;
+ }
+ // Ctrl + Enter won't cause actual input in our editor.
+ // However, the other browsers fire keypress event in any platforms.
+ // So, for compatibility with them, we should fire keypress event for
+ // Ctrl + Enter too.
+ return mMessage == eKeyPress && mKeyNameIndex == KEY_NAME_INDEX_Enter &&
+ !(mModifiers & (MODIFIER_ALT | MODIFIER_META | MODIFIER_SHIFT));
+ }
+
+ WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eKeyboardEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetKeyboardEvent* result = new WidgetKeyboardEvent(
+ false, mMessage, nullptr, eKeyboardEventClass, this);
+ result->AssignKeyEventData(*this, true);
+ result->mEditCommandsForSingleLineEditor =
+ mEditCommandsForSingleLineEditor.Clone();
+ result->mEditCommandsForMultiLineEditor =
+ mEditCommandsForMultiLineEditor.Clone();
+ result->mEditCommandsForRichTextEditor =
+ mEditCommandsForRichTextEditor.Clone();
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ bool CanUserGestureActivateTarget() const {
+ // Printable keys, 'carriage return' and 'space' are supported user gestures
+ // for activating the document. However, if supported key is being pressed
+ // combining with other operation keys, such like alt, control ..etc., we
+ // won't activate the target for them because at that time user might
+ // interact with browser or window manager which doesn't necessarily
+ // demonstrate user's intent to play media.
+ const bool isCombiningWithOperationKeys = (IsControl() && !IsAltGraph()) ||
+ (IsAlt() && !IsAltGraph()) ||
+ IsMeta();
+ const bool isEnterOrSpaceKey =
+ mKeyNameIndex == KEY_NAME_INDEX_Enter || mKeyCode == NS_VK_SPACE;
+ return (PseudoCharCode() || isEnterOrSpaceKey) &&
+ (!isCombiningWithOperationKeys ||
+ // ctrl-c/ctrl-x/ctrl-v is quite common shortcut for clipboard
+ // operation.
+ // XXXedgar, we have to find a better way to handle browser keyboard
+ // shortcut for user activation, instead of just ignoring all
+ // combinations, see bug 1641171.
+ ((mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_C ||
+ mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_V ||
+ mKeyCode == dom::KeyboardEvent_Binding::DOM_VK_X) &&
+ IsAccel()));
+ }
+
+ // Returns true if this event is likely an user activation for a link or
+ // a link-like button, where modifier keys are likely be used for controlling
+ // where the link is opened.
+ //
+ // This returns false if the keyboard event is more likely an user-defined
+ // shortcut key.
+ bool CanReflectModifiersToUserActivation() const {
+ MOZ_ASSERT(CanUserGestureActivateTarget(),
+ "Consumer should check CanUserGestureActivateTarget first");
+ // 'carriage return' and 'space' are supported user gestures for activating
+ // a link or a button.
+ // A button often behaves like a link, by calling window.open inside its
+ // event handler.
+ //
+ // Access keys can also activate links/buttons, but access keys have their
+ // own modifiers, and those modifiers are not appropriate for reflecting to
+ // the user activation nor controlling where the link is opened.
+ return mKeyNameIndex == KEY_NAME_INDEX_Enter || mKeyCode == NS_VK_SPACE;
+ }
+
+ [[nodiscard]] bool ShouldWorkAsSpaceKey() const {
+ if (mKeyCode == NS_VK_SPACE) {
+ return true;
+ }
+ // Additionally, if the code value is "Space" and the key is not mapped to
+ // a function key (i.e., not a printable key), we should treat it as space
+ // key because the active keyboard layout may input different character
+ // from the ASCII white space (U+0020). For example, NBSP (U+00A0).
+ return mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
+ mCodeNameIndex == CODE_NAME_INDEX_Space;
+ }
+
+ /**
+ * CanTreatAsUserInput() returns true if the key is pressed for perhaps
+ * doing something on the web app or our UI. This means that when this
+ * returns false, e.g., when user presses a modifier key, user is probably
+ * displeased by opening popup, entering fullscreen mode, etc. Therefore,
+ * only when this returns true, such reactions should be allowed.
+ */
+ bool CanTreatAsUserInput() const {
+ if (!IsTrusted()) {
+ return false;
+ }
+ switch (mKeyNameIndex) {
+ case KEY_NAME_INDEX_Escape:
+ // modifier keys:
+ case KEY_NAME_INDEX_Alt:
+ case KEY_NAME_INDEX_AltGraph:
+ case KEY_NAME_INDEX_CapsLock:
+ case KEY_NAME_INDEX_Control:
+ case KEY_NAME_INDEX_Fn:
+ case KEY_NAME_INDEX_FnLock:
+ case KEY_NAME_INDEX_Meta:
+ case KEY_NAME_INDEX_NumLock:
+ case KEY_NAME_INDEX_ScrollLock:
+ case KEY_NAME_INDEX_Shift:
+ case KEY_NAME_INDEX_Symbol:
+ case KEY_NAME_INDEX_SymbolLock:
+ // legacy modifier keys:
+ case KEY_NAME_INDEX_Hyper:
+ case KEY_NAME_INDEX_Super:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * ShouldInteractionTimeRecorded() returns true if the handling time of
+ * the event should be recorded with the telemetry.
+ */
+ bool ShouldInteractionTimeRecorded() const {
+ // Let's record only when we can treat the instance is a user input.
+ return CanTreatAsUserInput();
+ }
+
+ // OS translated Unicode chars which are used for accesskey and accelkey
+ // handling. The handlers will try from first character to last character.
+ CopyableTArray<AlternativeCharCode> mAlternativeCharCodes;
+ // DOM KeyboardEvent.key only when mKeyNameIndex is KEY_NAME_INDEX_USE_STRING.
+ nsString mKeyValue;
+ // DOM KeyboardEvent.code only when mCodeNameIndex is
+ // CODE_NAME_INDEX_USE_STRING.
+ nsString mCodeValue;
+
+ // OS-specific native event can optionally be preserved.
+ // This is used to retrieve editing shortcut keys in the environment.
+ void* mNativeKeyEvent;
+ // A DOM keyCode value or 0. If a keypress event whose mCharCode is 0, this
+ // should be 0.
+ uint32_t mKeyCode;
+ // If the instance is a keypress event of a printable key, this is a UTF-16
+ // value of the key. Otherwise, 0. This value must not be a control
+ // character when some modifiers are active. Then, this value should be an
+ // unmodified value except Shift and AltGr.
+ uint32_t mCharCode;
+ // mPseudoCharCode is valid only when mMessage is an eKeyDown event.
+ // This stores mCharCode value of keypress event which is fired with same
+ // key value and same modifier state.
+ uint32_t mPseudoCharCode;
+ // One of eKeyLocation*
+ uint32_t mLocation;
+ // Unique id associated with a keydown / keypress event. It's ok if this wraps
+ // over long periods.
+ uint32_t mUniqueId;
+
+ // DOM KeyboardEvent.key
+ KeyNameIndex mKeyNameIndex;
+ // DOM KeyboardEvent.code
+ CodeNameIndex mCodeNameIndex;
+
+ // Indicates whether the event is generated by auto repeat or not.
+ // if this is keyup event, always false.
+ bool mIsRepeat;
+ // Indicates whether the event is generated during IME (or deadkey)
+ // composition. This is initialized by EventStateManager. So, key event
+ // dispatchers don't need to initialize this.
+ bool mIsComposing;
+ // Indicates whether the event is synthesized from Text Input Processor
+ // or an actual event from nsAppShell.
+ bool mIsSynthesizedByTIP;
+ // Indicates whether the event is skippable in remote process.
+ // Don't refer this member directly when you need to check this.
+ // Use CanSkipInRemoteProcess() instead.
+ bool mMaybeSkippableInRemoteProcess;
+ // Indicates whether the event should return legacy keyCode value and
+ // charCode value to web apps (one of them is always 0) or not, when it's
+ // an eKeyPress event.
+ bool mUseLegacyKeyCodeAndCharCodeValues;
+
+ bool CanSkipInRemoteProcess() const {
+ // If this is a repeat event (i.e., generated by auto-repeat feature of
+ // the platform), remove process may skip to handle it because of
+ // performances reasons.. However, if it's caused by odd keyboard utils,
+ // we should not ignore any key events even marked as repeated since
+ // generated key sequence may be important to input proper text. E.g.,
+ // "SinhalaTamil IME" on Windows emulates dead key like input with
+ // generating WM_KEYDOWN for VK_PACKET (inputting any Unicode characters
+ // without keyboard layout information) and VK_BACK (Backspace) to remove
+ // previous character(s) and those messages may be marked as "repeat" by
+ // their bug.
+ return mIsRepeat && mMaybeSkippableInRemoteProcess;
+ }
+
+ /**
+ * If the key is an arrow key, and the current selection is in a vertical
+ * content, the caret should be moved to physically. However, arrow keys
+ * are mapped to logical move commands in horizontal content. Therefore,
+ * we need to check writing mode if and only if the key is an arrow key, and
+ * need to remap the command to logical command in vertical content if the
+ * writing mode at selection is vertical. These methods help to convert
+ * arrow keys in horizontal content to correspnding direction arrow keys
+ * in vertical content.
+ */
+ bool NeedsToRemapNavigationKey() const {
+ // TODO: Use mKeyNameIndex instead.
+ return mKeyCode >= NS_VK_LEFT && mKeyCode <= NS_VK_DOWN;
+ }
+
+ uint32_t GetRemappedKeyCode(const WritingMode& aWritingMode) const {
+ if (!aWritingMode.IsVertical()) {
+ return mKeyCode;
+ }
+ switch (mKeyCode) {
+ case NS_VK_LEFT:
+ return aWritingMode.IsVerticalLR() ? NS_VK_UP : NS_VK_DOWN;
+ case NS_VK_RIGHT:
+ return aWritingMode.IsVerticalLR() ? NS_VK_DOWN : NS_VK_UP;
+ case NS_VK_UP:
+ return NS_VK_LEFT;
+ case NS_VK_DOWN:
+ return NS_VK_RIGHT;
+ default:
+ return mKeyCode;
+ }
+ }
+
+ KeyNameIndex GetRemappedKeyNameIndex(const WritingMode& aWritingMode) const {
+ if (!aWritingMode.IsVertical()) {
+ return mKeyNameIndex;
+ }
+ uint32_t remappedKeyCode = GetRemappedKeyCode(aWritingMode);
+ if (remappedKeyCode == mKeyCode) {
+ return mKeyNameIndex;
+ }
+ switch (remappedKeyCode) {
+ case NS_VK_LEFT:
+ return KEY_NAME_INDEX_ArrowLeft;
+ case NS_VK_RIGHT:
+ return KEY_NAME_INDEX_ArrowRight;
+ case NS_VK_UP:
+ return KEY_NAME_INDEX_ArrowUp;
+ case NS_VK_DOWN:
+ return KEY_NAME_INDEX_ArrowDown;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
+ return mKeyNameIndex;
+ }
+ }
+
+ /**
+ * Retrieves all edit commands from mWidget. This shouldn't be called when
+ * the instance is an untrusted event, doesn't have widget or in non-chrome
+ * process.
+ *
+ * @param aWritingMode
+ * When writing mode of focused element is vertical, this
+ * will resolve some key's physical direction to logical
+ * direction. For doing it, this must be set to the
+ * writing mode at current selection. However, when there
+ * is no focused element and no selection ranges, this
+ * should be set to Nothing(). Using the result of
+ * `TextEventDispatcher::MaybeQueryWritingModeAtSelection()`
+ * is recommended.
+ */
+ MOZ_CAN_RUN_SCRIPT void InitAllEditCommands(
+ const Maybe<WritingMode>& aWritingMode);
+
+ /**
+ * Retrieves edit commands from mWidget only for aType. This shouldn't be
+ * called when the instance is an untrusted event or doesn't have widget.
+ *
+ * @param aWritingMode
+ * When writing mode of focused element is vertical, this
+ * will resolve some key's physical direction to logical
+ * direction. For doing it, this must be set to the
+ * writing mode at current selection. However, when there
+ * is no focused element and no selection ranges, this
+ * should be set to Nothing(). Using the result of
+ * `TextEventDispatcher::MaybeQueryWritingModeAtSelection()`
+ * is recommended.
+ * @return false if some resource is not available to get
+ * commands unexpectedly. Otherwise, true even if
+ * retrieved command is nothing.
+ */
+ MOZ_CAN_RUN_SCRIPT bool InitEditCommandsFor(
+ NativeKeyBindingsType aType, const Maybe<WritingMode>& aWritingMode);
+
+ /**
+ * PreventNativeKeyBindings() makes the instance to not cause any edit
+ * actions even if it matches with a native key binding.
+ */
+ void PreventNativeKeyBindings() {
+ mEditCommandsForSingleLineEditor.Clear();
+ mEditCommandsForMultiLineEditor.Clear();
+ mEditCommandsForRichTextEditor.Clear();
+ mEditCommandsForSingleLineEditorInitialized = true;
+ mEditCommandsForMultiLineEditorInitialized = true;
+ mEditCommandsForRichTextEditorInitialized = true;
+ }
+
+ /**
+ * EditCommandsConstRef() returns reference to edit commands for aType.
+ */
+ const nsTArray<CommandInt>& EditCommandsConstRef(
+ NativeKeyBindingsType aType) const {
+ return const_cast<WidgetKeyboardEvent*>(this)->EditCommandsRef(aType);
+ }
+
+ /**
+ * IsEditCommandsInitialized() returns true if edit commands for aType
+ * was already initialized. Otherwise, false.
+ */
+ bool IsEditCommandsInitialized(NativeKeyBindingsType aType) const {
+ return const_cast<WidgetKeyboardEvent*>(this)->IsEditCommandsInitializedRef(
+ aType);
+ }
+
+ /**
+ * AreAllEditCommandsInitialized() returns true if edit commands for all
+ * types were already initialized. Otherwise, false.
+ */
+ bool AreAllEditCommandsInitialized() const {
+ return mEditCommandsForSingleLineEditorInitialized &&
+ mEditCommandsForMultiLineEditorInitialized &&
+ mEditCommandsForRichTextEditorInitialized;
+ }
+
+ /**
+ * Execute edit commands for aType.
+ *
+ * @return true if the caller should do nothing anymore.
+ * false, otherwise.
+ */
+ typedef void (*DoCommandCallback)(Command, void*);
+ MOZ_CAN_RUN_SCRIPT bool ExecuteEditCommands(NativeKeyBindingsType aType,
+ DoCommandCallback aCallback,
+ void* aCallbackData);
+
+ // If the key should cause keypress events, this returns true.
+ // Otherwise, false.
+ bool ShouldCauseKeypressEvents() const;
+
+ // mCharCode value of non-eKeyPress events is always 0. However, if
+ // non-eKeyPress event has one or more alternative char code values,
+ // its first item should be the mCharCode value of following eKeyPress event.
+ // PseudoCharCode() returns mCharCode value for eKeyPress event,
+ // the first alternative char code value of non-eKeyPress event or 0.
+ uint32_t PseudoCharCode() const {
+ return mMessage == eKeyPress ? mCharCode : mPseudoCharCode;
+ }
+ void SetCharCode(uint32_t aCharCode) {
+ if (mMessage == eKeyPress) {
+ mCharCode = aCharCode;
+ } else {
+ mPseudoCharCode = aCharCode;
+ }
+ }
+
+ void GetDOMKeyName(nsAString& aKeyName) {
+ if (mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
+ aKeyName = mKeyValue;
+ return;
+ }
+ GetDOMKeyName(mKeyNameIndex, aKeyName);
+ }
+ void GetDOMCodeName(nsAString& aCodeName) {
+ if (mCodeNameIndex == CODE_NAME_INDEX_USE_STRING) {
+ aCodeName = mCodeValue;
+ return;
+ }
+ GetDOMCodeName(mCodeNameIndex, aCodeName);
+ }
+
+ /**
+ * GetFallbackKeyCodeOfPunctuationKey() returns a DOM keyCode value for
+ * aCodeNameIndex. This is keyCode value of the key when active keyboard
+ * layout is ANSI (US), JIS or ABNT keyboard layout (the latter 2 layouts
+ * are used only when ANSI doesn't have the key). The result is useful
+ * if the key doesn't produce ASCII character with active keyboard layout
+ * nor with alternative ASCII capable keyboard layout.
+ */
+ static uint32_t GetFallbackKeyCodeOfPunctuationKey(
+ CodeNameIndex aCodeNameIndex);
+
+ bool IsModifierKeyEvent() const {
+ return GetModifierForKeyName(mKeyNameIndex) != MODIFIER_NONE;
+ }
+
+ /**
+ * Get the candidates for shortcut key.
+ *
+ * @param aCandidates [out] the candidate shortcut key combination list.
+ * the first item is most preferred.
+ */
+ void GetShortcutKeyCandidates(ShortcutKeyCandidateArray& aCandidates) const;
+
+ /**
+ * Get the candidates for access key.
+ *
+ * @param aCandidates [out] the candidate access key list.
+ * the first item is most preferred.
+ */
+ void GetAccessKeyCandidates(nsTArray<uint32_t>& aCandidates) const;
+
+ /**
+ * Check whether the modifiers match with chrome access key or
+ * content access key.
+ */
+ bool ModifiersMatchWithAccessKey(AccessKeyType aType) const;
+
+ /**
+ * Return active modifiers which may match with access key.
+ * For example, even if Alt is access key modifier, then, when Control,
+ * CapseLock and NumLock are active, this returns only MODIFIER_CONTROL.
+ */
+ Modifiers ModifiersForAccessKeyMatching() const;
+
+ /**
+ * Return access key modifiers.
+ */
+ static Modifiers AccessKeyModifiers(AccessKeyType aType);
+
+ static void Shutdown();
+
+ /**
+ * ComputeLocationFromCodeValue() returns one of .mLocation value
+ * (eKeyLocation*) which is the most preferred value for the specified code
+ * value.
+ */
+ static uint32_t ComputeLocationFromCodeValue(CodeNameIndex aCodeNameIndex);
+
+ /**
+ * ComputeKeyCodeFromKeyNameIndex() return a .mKeyCode value which can be
+ * mapped from the specified key value. Note that this returns 0 if the
+ * key name index is KEY_NAME_INDEX_Unidentified or KEY_NAME_INDEX_USE_STRING.
+ * This means that this method is useful only for non-printable keys.
+ */
+ static uint32_t ComputeKeyCodeFromKeyNameIndex(KeyNameIndex aKeyNameIndex);
+
+ /**
+ * ComputeCodeNameIndexFromKeyNameIndex() returns a code name index which
+ * is typically mapped to given key name index on the platform.
+ * Note that this returns CODE_NAME_INDEX_UNKNOWN if the key name index is
+ * KEY_NAME_INDEX_Unidentified or KEY_NAME_INDEX_USE_STRING.
+ * This means that this method is useful only for non-printable keys.
+ *
+ * @param aKeyNameIndex A non-printable key name index.
+ * @param aLocation Should be one of location value. This is
+ * important when aKeyNameIndex may exist in
+ * both Numpad or Standard, or in both Left or
+ * Right. If this is nothing, this method
+ * returns Left or Standard position's code
+ * value.
+ */
+ static CodeNameIndex ComputeCodeNameIndexFromKeyNameIndex(
+ KeyNameIndex aKeyNameIndex, const Maybe<uint32_t>& aLocation);
+
+ /**
+ * GetModifierForKeyName() returns a value of Modifier which is activated
+ * by the aKeyNameIndex.
+ */
+ static Modifier GetModifierForKeyName(KeyNameIndex aKeyNameIndex);
+
+ /**
+ * IsLeftOrRightModiferKeyNameIndex() returns true if aKeyNameIndex is a
+ * modifier key which may be in Left and Right location.
+ */
+ static bool IsLeftOrRightModiferKeyNameIndex(KeyNameIndex aKeyNameIndex) {
+ switch (aKeyNameIndex) {
+ case KEY_NAME_INDEX_Alt:
+ case KEY_NAME_INDEX_Control:
+ case KEY_NAME_INDEX_Meta:
+ case KEY_NAME_INDEX_Shift:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * IsLockableModifier() returns true if aKeyNameIndex is a lockable modifier
+ * key such as CapsLock and NumLock.
+ */
+ static bool IsLockableModifier(KeyNameIndex aKeyNameIndex);
+
+ static void GetDOMKeyName(KeyNameIndex aKeyNameIndex, nsAString& aKeyName);
+ static void GetDOMCodeName(CodeNameIndex aCodeNameIndex,
+ nsAString& aCodeName);
+
+ static KeyNameIndex GetKeyNameIndex(const nsAString& aKeyValue);
+ static CodeNameIndex GetCodeNameIndex(const nsAString& aCodeValue);
+
+ static const char* GetCommandStr(Command aCommand);
+
+ void AssignKeyEventData(const WidgetKeyboardEvent& aEvent,
+ bool aCopyTargets) {
+ AssignInputEventData(aEvent, aCopyTargets);
+
+ mKeyCode = aEvent.mKeyCode;
+ mCharCode = aEvent.mCharCode;
+ mPseudoCharCode = aEvent.mPseudoCharCode;
+ mLocation = aEvent.mLocation;
+ mAlternativeCharCodes = aEvent.mAlternativeCharCodes.Clone();
+ mIsRepeat = aEvent.mIsRepeat;
+ mIsComposing = aEvent.mIsComposing;
+ mKeyNameIndex = aEvent.mKeyNameIndex;
+ mCodeNameIndex = aEvent.mCodeNameIndex;
+ mKeyValue = aEvent.mKeyValue;
+ mCodeValue = aEvent.mCodeValue;
+ // Don't copy mNativeKeyEvent because it may be referred after its instance
+ // is destroyed.
+ mNativeKeyEvent = nullptr;
+ mUniqueId = aEvent.mUniqueId;
+ mIsSynthesizedByTIP = aEvent.mIsSynthesizedByTIP;
+ mMaybeSkippableInRemoteProcess = aEvent.mMaybeSkippableInRemoteProcess;
+ mUseLegacyKeyCodeAndCharCodeValues =
+ aEvent.mUseLegacyKeyCodeAndCharCodeValues;
+
+ // Don't copy mEditCommandsFor*Editor because it may require a lot of
+ // memory space. For example, if the event is dispatched but grabbed by
+ // a JS variable, they are not necessary anymore.
+
+ mEditCommandsForSingleLineEditorInitialized =
+ aEvent.mEditCommandsForSingleLineEditorInitialized;
+ mEditCommandsForMultiLineEditorInitialized =
+ aEvent.mEditCommandsForMultiLineEditorInitialized;
+ mEditCommandsForRichTextEditorInitialized =
+ aEvent.mEditCommandsForRichTextEditorInitialized;
+ }
+
+ void AssignCommands(const WidgetKeyboardEvent& aEvent) {
+ mEditCommandsForSingleLineEditorInitialized =
+ aEvent.mEditCommandsForSingleLineEditorInitialized;
+ if (mEditCommandsForSingleLineEditorInitialized) {
+ mEditCommandsForSingleLineEditor =
+ aEvent.mEditCommandsForSingleLineEditor.Clone();
+ } else {
+ mEditCommandsForSingleLineEditor.Clear();
+ }
+ mEditCommandsForMultiLineEditorInitialized =
+ aEvent.mEditCommandsForMultiLineEditorInitialized;
+ if (mEditCommandsForMultiLineEditorInitialized) {
+ mEditCommandsForMultiLineEditor =
+ aEvent.mEditCommandsForMultiLineEditor.Clone();
+ } else {
+ mEditCommandsForMultiLineEditor.Clear();
+ }
+ mEditCommandsForRichTextEditorInitialized =
+ aEvent.mEditCommandsForRichTextEditorInitialized;
+ if (mEditCommandsForRichTextEditorInitialized) {
+ mEditCommandsForRichTextEditor =
+ aEvent.mEditCommandsForRichTextEditor.Clone();
+ } else {
+ mEditCommandsForRichTextEditor.Clear();
+ }
+ }
+
+ private:
+ static const char16_t* const kKeyNames[];
+ static const char16_t* const kCodeNames[];
+ typedef nsTHashMap<nsStringHashKey, KeyNameIndex> KeyNameIndexHashtable;
+ typedef nsTHashMap<nsStringHashKey, CodeNameIndex> CodeNameIndexHashtable;
+ static KeyNameIndexHashtable* sKeyNameIndexHashtable;
+ static CodeNameIndexHashtable* sCodeNameIndexHashtable;
+
+ // mEditCommandsFor*Editor store edit commands. This should be initialized
+ // with InitEditCommandsFor().
+ // XXX Ideally, this should be array of Command rather than CommandInt.
+ // However, ParamTraits isn't aware of enum array.
+ CopyableTArray<CommandInt> mEditCommandsForSingleLineEditor;
+ CopyableTArray<CommandInt> mEditCommandsForMultiLineEditor;
+ CopyableTArray<CommandInt> mEditCommandsForRichTextEditor;
+
+ nsTArray<CommandInt>& EditCommandsRef(NativeKeyBindingsType aType) {
+ switch (aType) {
+ case NativeKeyBindingsType::SingleLineEditor:
+ return mEditCommandsForSingleLineEditor;
+ case NativeKeyBindingsType::MultiLineEditor:
+ return mEditCommandsForMultiLineEditor;
+ case NativeKeyBindingsType::RichTextEditor:
+ return mEditCommandsForRichTextEditor;
+ default:
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE(
+ "Invalid native key binding type");
+ }
+ }
+
+ // mEditCommandsFor*EditorInitialized are set to true when
+ // InitEditCommandsFor() initializes edit commands for the type.
+ bool mEditCommandsForSingleLineEditorInitialized;
+ bool mEditCommandsForMultiLineEditorInitialized;
+ bool mEditCommandsForRichTextEditorInitialized;
+
+ bool& IsEditCommandsInitializedRef(NativeKeyBindingsType aType) {
+ switch (aType) {
+ case NativeKeyBindingsType::SingleLineEditor:
+ return mEditCommandsForSingleLineEditorInitialized;
+ case NativeKeyBindingsType::MultiLineEditor:
+ return mEditCommandsForMultiLineEditorInitialized;
+ case NativeKeyBindingsType::RichTextEditor:
+ return mEditCommandsForRichTextEditorInitialized;
+ default:
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE(
+ "Invalid native key binding type");
+ }
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetCompositionEvent
+ ******************************************************************************/
+
+class WidgetCompositionEvent : public WidgetGUIEvent {
+ private:
+ friend class mozilla::dom::PBrowserParent;
+ friend class mozilla::dom::PBrowserChild;
+ ALLOW_DEPRECATED_READPARAM
+
+ WidgetCompositionEvent() : mOriginalMessage(eVoidEvent) {}
+
+ public:
+ virtual WidgetCompositionEvent* AsCompositionEvent() override { return this; }
+
+ WidgetCompositionEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, eCompositionEventClass,
+ aTime),
+ mNativeIMEContext(aWidget),
+ mOriginalMessage(eVoidEvent) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eCompositionEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetCompositionEvent* result =
+ new WidgetCompositionEvent(false, mMessage, nullptr, this);
+ result->AssignCompositionEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ // The composition string or the commit string. If the instance is a
+ // compositionstart event, this is initialized with selected text by
+ // TextComposition automatically.
+ nsString mData;
+
+ RefPtr<TextRangeArray> mRanges;
+
+ // mNativeIMEContext stores the native IME context which causes the
+ // composition event.
+ widget::NativeIMEContext mNativeIMEContext;
+
+ // If the instance is a clone of another event, mOriginalMessage stores
+ // the another event's mMessage.
+ EventMessage mOriginalMessage;
+
+ // Composition ID considered by TextComposition. If the event has not been
+ // handled by TextComposition yet, this is 0. And also if the event is for
+ // a composition synthesized in a content process, this is always 0.
+ uint32_t mCompositionId = 0;
+
+ void AssignCompositionEventData(const WidgetCompositionEvent& aEvent,
+ bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ mData = aEvent.mData;
+ mOriginalMessage = aEvent.mOriginalMessage;
+ mRanges = aEvent.mRanges;
+
+ // Currently, we don't need to copy the other members because they are
+ // for internal use only (not available from JS).
+ }
+
+ bool IsComposing() const { return mRanges && mRanges->IsComposing(); }
+
+ uint32_t TargetClauseOffset() const {
+ return mRanges ? mRanges->TargetClauseOffset() : 0;
+ }
+
+ uint32_t TargetClauseLength() const {
+ uint32_t length = UINT32_MAX;
+ if (mRanges) {
+ length = mRanges->TargetClauseLength();
+ }
+ return length == UINT32_MAX ? mData.Length() : length;
+ }
+
+ uint32_t RangeCount() const { return mRanges ? mRanges->Length() : 0; }
+
+ bool CausesDOMTextEvent() const {
+ return mMessage == eCompositionChange || mMessage == eCompositionCommit ||
+ mMessage == eCompositionCommitAsIs;
+ }
+
+ bool CausesDOMCompositionEndEvent() const {
+ return mMessage == eCompositionEnd || mMessage == eCompositionCommit ||
+ mMessage == eCompositionCommitAsIs;
+ }
+
+ bool IsFollowedByCompositionEnd() const {
+ return IsFollowedByCompositionEnd(mOriginalMessage);
+ }
+
+ static bool IsFollowedByCompositionEnd(EventMessage aEventMessage) {
+ return aEventMessage == eCompositionCommit ||
+ aEventMessage == eCompositionCommitAsIs;
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetQueryContentEvent
+ ******************************************************************************/
+
+class WidgetQueryContentEvent : public WidgetGUIEvent {
+ private:
+ friend class dom::PBrowserParent;
+ friend class dom::PBrowserChild;
+ ALLOW_DEPRECATED_READPARAM
+
+ WidgetQueryContentEvent()
+ : mUseNativeLineBreak(true),
+ mWithFontRanges(false),
+ mNeedsToFlushLayout(true) {
+ MOZ_CRASH("WidgetQueryContentEvent is created without proper arguments");
+ }
+
+ public:
+ virtual WidgetQueryContentEvent* AsQueryContentEvent() override {
+ return this;
+ }
+
+ WidgetQueryContentEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, eQueryContentEventClass),
+ mUseNativeLineBreak(true),
+ mWithFontRanges(false),
+ mNeedsToFlushLayout(true) {}
+
+ WidgetQueryContentEvent(EventMessage aMessage,
+ const WidgetQueryContentEvent& aOtherEvent)
+ : WidgetGUIEvent(aOtherEvent.IsTrusted(), aMessage,
+ const_cast<nsIWidget*>(aOtherEvent.mWidget.get()),
+ eQueryContentEventClass),
+ mUseNativeLineBreak(aOtherEvent.mUseNativeLineBreak),
+ mWithFontRanges(false),
+ mNeedsToFlushLayout(aOtherEvent.mNeedsToFlushLayout) {}
+
+ WidgetEvent* Duplicate() const override {
+ // This event isn't an internal event of any DOM event.
+ NS_ASSERTION(!IsAllowedToDispatchDOMEvent(),
+ "WidgetQueryContentEvent needs to support Duplicate()");
+ MOZ_CRASH("WidgetQueryContentEvent doesn't support Duplicate()");
+ }
+
+ struct Options final {
+ bool mUseNativeLineBreak;
+ bool mRelativeToInsertionPoint;
+
+ explicit Options()
+ : mUseNativeLineBreak(true), mRelativeToInsertionPoint(false) {}
+
+ explicit Options(const WidgetQueryContentEvent& aEvent)
+ : mUseNativeLineBreak(aEvent.mUseNativeLineBreak),
+ mRelativeToInsertionPoint(aEvent.mInput.mRelativeToInsertionPoint) {}
+ };
+
+ void Init(const Options& aOptions) {
+ mUseNativeLineBreak = aOptions.mUseNativeLineBreak;
+ mInput.mRelativeToInsertionPoint = aOptions.mRelativeToInsertionPoint;
+ MOZ_ASSERT(mInput.IsValidEventMessage(mMessage));
+ }
+
+ void InitForQueryTextContent(int64_t aOffset, uint32_t aLength,
+ const Options& aOptions = Options()) {
+ NS_ASSERTION(mMessage == eQueryTextContent, "wrong initializer is called");
+ mInput.mOffset = aOffset;
+ mInput.mLength = aLength;
+ Init(aOptions);
+ MOZ_ASSERT(mInput.IsValidOffset());
+ }
+
+ void InitForQueryCaretRect(int64_t aOffset,
+ const Options& aOptions = Options()) {
+ NS_ASSERTION(mMessage == eQueryCaretRect, "wrong initializer is called");
+ mInput.mOffset = aOffset;
+ Init(aOptions);
+ MOZ_ASSERT(mInput.IsValidOffset());
+ }
+
+ void InitForQueryTextRect(int64_t aOffset, uint32_t aLength,
+ const Options& aOptions = Options()) {
+ NS_ASSERTION(mMessage == eQueryTextRect, "wrong initializer is called");
+ mInput.mOffset = aOffset;
+ mInput.mLength = aLength;
+ Init(aOptions);
+ MOZ_ASSERT(mInput.IsValidOffset());
+ }
+
+ void InitForQuerySelectedText(SelectionType aSelectionType,
+ const Options& aOptions = Options()) {
+ MOZ_ASSERT(mMessage == eQuerySelectedText);
+ MOZ_ASSERT(aSelectionType != SelectionType::eNone);
+ mInput.mSelectionType = aSelectionType;
+ Init(aOptions);
+ }
+
+ void InitForQueryDOMWidgetHittest(
+ const mozilla::LayoutDeviceIntPoint& aPoint) {
+ NS_ASSERTION(mMessage == eQueryDOMWidgetHittest,
+ "wrong initializer is called");
+ mRefPoint = aPoint;
+ }
+
+ void InitForQueryTextRectArray(uint32_t aOffset, uint32_t aLength,
+ const Options& aOptions = Options()) {
+ NS_ASSERTION(mMessage == eQueryTextRectArray,
+ "wrong initializer is called");
+ mInput.mOffset = aOffset;
+ mInput.mLength = aLength;
+ Init(aOptions);
+ }
+
+ void RequestFontRanges() {
+ MOZ_ASSERT(mMessage == eQueryTextContent);
+ mWithFontRanges = true;
+ }
+
+ bool Succeeded() const {
+ if (mReply.isNothing()) {
+ return false;
+ }
+ switch (mMessage) {
+ case eQueryTextContent:
+ case eQueryTextRect:
+ case eQueryCaretRect:
+ return mReply->mOffsetAndData.isSome();
+ default:
+ return true;
+ }
+ }
+
+ bool Failed() const { return !Succeeded(); }
+
+ bool FoundSelection() const {
+ MOZ_ASSERT(mMessage == eQuerySelectedText);
+ return Succeeded() && mReply->mOffsetAndData.isSome();
+ }
+
+ bool FoundChar() const {
+ MOZ_ASSERT(mMessage == eQueryCharacterAtPoint);
+ return Succeeded() && mReply->mOffsetAndData.isSome();
+ }
+
+ bool FoundTentativeCaretOffset() const {
+ MOZ_ASSERT(mMessage == eQueryCharacterAtPoint);
+ return Succeeded() && mReply->mTentativeCaretOffset.isSome();
+ }
+
+ bool DidNotFindSelection() const {
+ MOZ_ASSERT(mMessage == eQuerySelectedText);
+ return Failed() || mReply->mOffsetAndData.isNothing();
+ }
+
+ bool DidNotFindChar() const {
+ MOZ_ASSERT(mMessage == eQueryCharacterAtPoint);
+ return Failed() || mReply->mOffsetAndData.isNothing();
+ }
+
+ bool DidNotFindTentativeCaretOffset() const {
+ MOZ_ASSERT(mMessage == eQueryCharacterAtPoint);
+ return Failed() || mReply->mTentativeCaretOffset.isNothing();
+ }
+
+ bool mUseNativeLineBreak;
+ bool mWithFontRanges;
+ bool mNeedsToFlushLayout;
+ struct Input final {
+ uint32_t EndOffset() const {
+ CheckedInt<uint32_t> endOffset = CheckedInt<uint32_t>(mOffset) + mLength;
+ return NS_WARN_IF(!endOffset.isValid()) ? UINT32_MAX : endOffset.value();
+ }
+
+ int64_t mOffset;
+ uint32_t mLength;
+ SelectionType mSelectionType;
+ // If mOffset is true, mOffset is relative to the start offset of
+ // composition if there is, otherwise, the start of the first selection
+ // range.
+ bool mRelativeToInsertionPoint;
+
+ Input()
+ : mOffset(0),
+ mLength(0),
+ mSelectionType(SelectionType::eNormal),
+ mRelativeToInsertionPoint(false) {}
+
+ bool IsValidOffset() const {
+ return mRelativeToInsertionPoint || mOffset >= 0;
+ }
+ bool IsValidEventMessage(EventMessage aEventMessage) const {
+ if (!mRelativeToInsertionPoint) {
+ return true;
+ }
+ switch (aEventMessage) {
+ case eQueryTextContent:
+ case eQueryCaretRect:
+ case eQueryTextRect:
+ return true;
+ default:
+ return false;
+ }
+ }
+ bool MakeOffsetAbsolute(uint32_t aInsertionPointOffset) {
+ if (NS_WARN_IF(!mRelativeToInsertionPoint)) {
+ return true;
+ }
+ mRelativeToInsertionPoint = false;
+ // If mOffset + aInsertionPointOffset becomes negative value,
+ // we should assume the absolute offset is 0.
+ if (mOffset < 0 && -mOffset > aInsertionPointOffset) {
+ mOffset = 0;
+ return true;
+ }
+ // Otherwise, we don't allow too large offset.
+ CheckedInt<uint32_t> absOffset(mOffset + aInsertionPointOffset);
+ if (NS_WARN_IF(!absOffset.isValid())) {
+ mOffset = UINT32_MAX;
+ return false;
+ }
+ mOffset = absOffset.value();
+ return true;
+ }
+ } mInput;
+
+ struct Reply final {
+ EventMessage const mEventMessage;
+ void* mContentsRoot = nullptr;
+ Maybe<OffsetAndData<uint32_t>> mOffsetAndData;
+ // mTentativeCaretOffset is used by only eQueryCharacterAtPoint.
+ // This is the offset where caret would be if user clicked at the mRefPoint.
+ Maybe<uint32_t> mTentativeCaretOffset;
+ // mRect is used by eQueryTextRect, eQueryCaretRect, eQueryCharacterAtPoint
+ // and eQueryEditorRect. The coordinates is system coordinates relative to
+ // the top level widget of mFocusedWidget. E.g., if a <xul:panel> which
+ // is owned by a window has focused editor, the offset of mRect is relative
+ // to the owner window, not the <xul:panel>.
+ mozilla::LayoutDeviceIntRect mRect;
+ // The return widget has the caret. This is set at all query events.
+ nsIWidget* mFocusedWidget = nullptr;
+ // mozilla::WritingMode value at the end (focus) of the selection
+ mozilla::WritingMode mWritingMode;
+ // Used by eQuerySelectionAsTransferable
+ nsCOMPtr<nsITransferable> mTransferable;
+ // Used by eQueryTextContent with font ranges requested
+ CopyableAutoTArray<mozilla::FontRange, 1> mFontRanges;
+ // Used by eQueryTextRectArray
+ CopyableTArray<mozilla::LayoutDeviceIntRect> mRectArray;
+ // true if selection is reversed (end < start)
+ bool mReversed = false;
+ // true if DOM element under mouse belongs to widget
+ bool mWidgetIsHit = false;
+ // true if mContentRoot is focused editable content
+ bool mIsEditableContent = false;
+
+ Reply() = delete;
+ explicit Reply(EventMessage aEventMessage) : mEventMessage(aEventMessage) {}
+
+ // Don't allow to copy/move because of `mEventMessage`.
+ Reply(const Reply& aOther) = delete;
+ Reply(Reply&& aOther) = delete;
+ Reply& operator=(const Reply& aOther) = delete;
+ Reply& operator=(Reply&& aOther) = delete;
+
+ MOZ_NEVER_INLINE_DEBUG uint32_t StartOffset() const {
+ MOZ_ASSERT(mOffsetAndData.isSome());
+ return mOffsetAndData->StartOffset();
+ }
+ MOZ_NEVER_INLINE_DEBUG uint32_t EndOffset() const {
+ MOZ_ASSERT(mOffsetAndData.isSome());
+ return mOffsetAndData->EndOffset();
+ }
+ MOZ_NEVER_INLINE_DEBUG uint32_t DataLength() const {
+ MOZ_ASSERT(mOffsetAndData.isSome() ||
+ mEventMessage == eQuerySelectedText);
+ return mOffsetAndData.isSome() ? mOffsetAndData->Length() : 0;
+ }
+ MOZ_NEVER_INLINE_DEBUG uint32_t AnchorOffset() const {
+ MOZ_ASSERT(mEventMessage == eQuerySelectedText);
+ MOZ_ASSERT(mOffsetAndData.isSome());
+ return StartOffset() + (mReversed ? DataLength() : 0);
+ }
+
+ MOZ_NEVER_INLINE_DEBUG uint32_t FocusOffset() const {
+ MOZ_ASSERT(mEventMessage == eQuerySelectedText);
+ MOZ_ASSERT(mOffsetAndData.isSome());
+ return StartOffset() + (mReversed ? 0 : DataLength());
+ }
+
+ const WritingMode& WritingModeRef() const {
+ MOZ_ASSERT(mEventMessage == eQuerySelectedText ||
+ mEventMessage == eQueryCaretRect ||
+ mEventMessage == eQueryTextRect);
+ MOZ_ASSERT(mOffsetAndData.isSome() ||
+ mEventMessage == eQuerySelectedText);
+ return mWritingMode;
+ }
+
+ MOZ_NEVER_INLINE_DEBUG const nsString& DataRef() const {
+ MOZ_ASSERT(mOffsetAndData.isSome() ||
+ mEventMessage == eQuerySelectedText);
+ return mOffsetAndData.isSome() ? mOffsetAndData->DataRef()
+ : EmptyString();
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsDataEmpty() const {
+ MOZ_ASSERT(mOffsetAndData.isSome() ||
+ mEventMessage == eQuerySelectedText);
+ return mOffsetAndData.isSome() ? mOffsetAndData->IsDataEmpty() : true;
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsOffsetInRange(uint32_t aOffset) const {
+ MOZ_ASSERT(mOffsetAndData.isSome() ||
+ mEventMessage == eQuerySelectedText);
+ return mOffsetAndData.isSome() ? mOffsetAndData->IsOffsetInRange(aOffset)
+ : false;
+ }
+ MOZ_NEVER_INLINE_DEBUG bool IsOffsetInRangeOrEndOffset(
+ uint32_t aOffset) const {
+ MOZ_ASSERT(mOffsetAndData.isSome() ||
+ mEventMessage == eQuerySelectedText);
+ return mOffsetAndData.isSome()
+ ? mOffsetAndData->IsOffsetInRangeOrEndOffset(aOffset)
+ : false;
+ }
+ MOZ_NEVER_INLINE_DEBUG void TruncateData(uint32_t aLength = 0) {
+ MOZ_ASSERT(mOffsetAndData.isSome());
+ mOffsetAndData->TruncateData(aLength);
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Reply& aReply) {
+ aStream << "{ ";
+ if (aReply.mEventMessage == eQuerySelectedText ||
+ aReply.mEventMessage == eQueryTextContent ||
+ aReply.mEventMessage == eQueryTextRect ||
+ aReply.mEventMessage == eQueryCaretRect ||
+ aReply.mEventMessage == eQueryCharacterAtPoint) {
+ aStream << "mOffsetAndData=" << ToString(aReply.mOffsetAndData).c_str()
+ << ", ";
+ if (aReply.mEventMessage == eQueryCharacterAtPoint) {
+ aStream << "mTentativeCaretOffset="
+ << ToString(aReply.mTentativeCaretOffset).c_str() << ", ";
+ }
+ }
+ if (aReply.mOffsetAndData.isSome() && aReply.mOffsetAndData->Length()) {
+ if (aReply.mEventMessage == eQuerySelectedText) {
+ aStream << ", mReversed=" << (aReply.mReversed ? "true" : "false");
+ }
+ if (aReply.mEventMessage == eQuerySelectionAsTransferable) {
+ aStream << ", mTransferable=0x" << aReply.mTransferable;
+ }
+ }
+ if (aReply.mEventMessage == eQuerySelectedText ||
+ aReply.mEventMessage == eQueryTextRect ||
+ aReply.mEventMessage == eQueryCaretRect) {
+ aStream << ", mWritingMode=" << ToString(aReply.mWritingMode).c_str();
+ }
+ aStream << ", mContentsRoot=0x" << aReply.mContentsRoot
+ << ", mIsEditableContent="
+ << (aReply.mIsEditableContent ? "true" : "false")
+ << ", mFocusedWidget=0x" << aReply.mFocusedWidget;
+ if (aReply.mEventMessage == eQueryTextContent) {
+ aStream << ", mFontRanges={ Length()=" << aReply.mFontRanges.Length()
+ << " }";
+ } else if (aReply.mEventMessage == eQueryTextRect ||
+ aReply.mEventMessage == eQueryCaretRect ||
+ aReply.mEventMessage == eQueryCharacterAtPoint) {
+ aStream << ", mRect=" << ToString(aReply.mRect).c_str();
+ } else if (aReply.mEventMessage == eQueryTextRectArray) {
+ aStream << ", mRectArray={ Length()=" << aReply.mRectArray.Length()
+ << " }";
+ } else if (aReply.mEventMessage == eQueryDOMWidgetHittest) {
+ aStream << ", mWidgetIsHit="
+ << (aReply.mWidgetIsHit ? "true" : "false");
+ }
+ return aStream << " }";
+ }
+ };
+
+ void EmplaceReply() { mReply.emplace(mMessage); }
+ Maybe<Reply> mReply;
+
+ // values of mComputedScrollAction
+ enum { SCROLL_ACTION_NONE, SCROLL_ACTION_LINE, SCROLL_ACTION_PAGE };
+};
+
+/******************************************************************************
+ * mozilla::WidgetSelectionEvent
+ ******************************************************************************/
+
+class WidgetSelectionEvent : public WidgetGUIEvent {
+ private:
+ friend class mozilla::dom::PBrowserParent;
+ friend class mozilla::dom::PBrowserChild;
+ ALLOW_DEPRECATED_READPARAM
+
+ WidgetSelectionEvent()
+ : mOffset(0),
+ mLength(0),
+ mReversed(false),
+ mExpandToClusterBoundary(true),
+ mSucceeded(false),
+ mUseNativeLineBreak(true),
+ mReason(nsISelectionListener::NO_REASON) {}
+
+ public:
+ virtual WidgetSelectionEvent* AsSelectionEvent() override { return this; }
+
+ WidgetSelectionEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, eSelectionEventClass),
+ mOffset(0),
+ mLength(0),
+ mReversed(false),
+ mExpandToClusterBoundary(true),
+ mSucceeded(false),
+ mUseNativeLineBreak(true),
+ mReason(nsISelectionListener::NO_REASON) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ // This event isn't an internal event of any DOM event.
+ NS_ASSERTION(!IsAllowedToDispatchDOMEvent(),
+ "WidgetSelectionEvent needs to support Duplicate()");
+ MOZ_CRASH("WidgetSelectionEvent doesn't support Duplicate()");
+ return nullptr;
+ }
+
+ // Start offset of selection
+ uint32_t mOffset;
+ // Length of selection
+ uint32_t mLength;
+ // Selection "anchor" should be in front
+ bool mReversed;
+ // Cluster-based or character-based
+ bool mExpandToClusterBoundary;
+ // true if setting selection succeeded.
+ bool mSucceeded;
+ // true if native line breaks are used for mOffset and mLength
+ bool mUseNativeLineBreak;
+ // Fennec provides eSetSelection reason codes for downstream
+ // use in AccessibleCaret visibility logic.
+ int16_t mReason;
+};
+
+/******************************************************************************
+ * mozilla::InternalEditorInputEvent
+ ******************************************************************************/
+
+class InternalEditorInputEvent : public InternalUIEvent {
+ private:
+ InternalEditorInputEvent()
+ : mData(VoidString()),
+ mInputType(EditorInputType::eUnknown),
+ mIsComposing(false) {}
+
+ public:
+ virtual InternalEditorInputEvent* AsEditorInputEvent() override {
+ return this;
+ }
+
+ InternalEditorInputEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget = nullptr,
+ const WidgetEventTime* aTime = nullptr)
+ : InternalUIEvent(aIsTrusted, aMessage, aWidget, eEditorInputEventClass,
+ aTime),
+ mData(VoidString()),
+ mInputType(EditorInputType::eUnknown) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eEditorInputEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ InternalEditorInputEvent* result =
+ new InternalEditorInputEvent(false, mMessage, nullptr, this);
+ result->AssignEditorInputEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ nsString mData;
+ RefPtr<dom::DataTransfer> mDataTransfer;
+ OwningNonNullStaticRangeArray mTargetRanges;
+
+ EditorInputType mInputType;
+
+ bool mIsComposing;
+
+ void AssignEditorInputEventData(const InternalEditorInputEvent& aEvent,
+ bool aCopyTargets) {
+ AssignUIEventData(aEvent, aCopyTargets);
+
+ mData = aEvent.mData;
+ mDataTransfer = aEvent.mDataTransfer;
+ mTargetRanges = aEvent.mTargetRanges.Clone();
+ mInputType = aEvent.mInputType;
+ mIsComposing = aEvent.mIsComposing;
+ }
+
+ void GetDOMInputTypeName(nsAString& aInputTypeName) {
+ GetDOMInputTypeName(mInputType, aInputTypeName);
+ }
+ static void GetDOMInputTypeName(EditorInputType aInputType,
+ nsAString& aInputTypeName);
+ static EditorInputType GetEditorInputType(const nsAString& aInputType);
+
+ static void Shutdown();
+
+ private:
+ static const char16_t* const kInputTypeNames[];
+ typedef nsTHashMap<nsStringHashKey, EditorInputType> InputTypeHashtable;
+ static InputTypeHashtable* sInputTypeHashtable;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_TextEvents_h__
diff --git a/widget/TextRange.h b/widget/TextRange.h
new file mode 100644
index 0000000000..2ad41a6bc6
--- /dev/null
+++ b/widget/TextRange.h
@@ -0,0 +1,287 @@
+/* -*- 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/. */
+
+#ifndef mozilla_TextRage_h_
+#define mozilla_TextRage_h_
+
+#include <stdint.h>
+
+#include "mozilla/EventForwards.h"
+
+#include "nsColor.h"
+#include "nsISelectionController.h"
+#include "nsITextInputProcessor.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+/******************************************************************************
+ * mozilla::TextRangeStyle
+ ******************************************************************************/
+
+struct TextRangeStyle {
+ typedef uint8_t LineStyleType;
+ // FYI: Modify IME_RANGE_LINE_* too when you modify LineStyle.
+ enum class LineStyle : LineStyleType {
+ None,
+ Solid,
+ Dotted,
+ Dashed,
+ Double,
+ Wavy,
+ };
+ inline static LineStyle ToLineStyle(RawTextRangeType aRawLineStyle) {
+ switch (static_cast<LineStyle>(aRawLineStyle)) {
+ case LineStyle::None:
+ case LineStyle::Solid:
+ case LineStyle::Dotted:
+ case LineStyle::Dashed:
+ case LineStyle::Double:
+ case LineStyle::Wavy:
+ return static_cast<LineStyle>(aRawLineStyle);
+ }
+ MOZ_ASSERT_UNREACHABLE("aRawLineStyle value is invalid");
+ return LineStyle::None;
+ }
+
+ enum {
+ DEFINED_NONE = 0x00,
+ DEFINED_LINESTYLE = 0x01,
+ DEFINED_FOREGROUND_COLOR = 0x02,
+ DEFINED_BACKGROUND_COLOR = 0x04,
+ DEFINED_UNDERLINE_COLOR = 0x08
+ };
+
+ // Initialize all members, because TextRange instances may be compared by
+ // memcomp.
+ //
+ // FIXME(emilio): I don't think that'd be sound, as it has padding which the
+ // compiler is not guaranteed to initialize.
+ TextRangeStyle() { Clear(); }
+
+ void Clear() {
+ mDefinedStyles = DEFINED_NONE;
+ mLineStyle = LineStyle::None;
+ mIsBoldLine = false;
+ mForegroundColor = mBackgroundColor = mUnderlineColor = NS_RGBA(0, 0, 0, 0);
+ }
+
+ bool IsDefined() const { return mDefinedStyles != DEFINED_NONE; }
+
+ bool IsLineStyleDefined() const {
+ return (mDefinedStyles & DEFINED_LINESTYLE) != 0;
+ }
+
+ bool IsForegroundColorDefined() const {
+ return (mDefinedStyles & DEFINED_FOREGROUND_COLOR) != 0;
+ }
+
+ bool IsBackgroundColorDefined() const {
+ return (mDefinedStyles & DEFINED_BACKGROUND_COLOR) != 0;
+ }
+
+ bool IsUnderlineColorDefined() const {
+ return (mDefinedStyles & DEFINED_UNDERLINE_COLOR) != 0;
+ }
+
+ bool IsNoChangeStyle() const {
+ return !IsForegroundColorDefined() && !IsBackgroundColorDefined() &&
+ IsLineStyleDefined() && mLineStyle == LineStyle::None;
+ }
+
+ bool Equals(const TextRangeStyle& aOther) const {
+ if (mDefinedStyles != aOther.mDefinedStyles) return false;
+ if (IsLineStyleDefined() && (mLineStyle != aOther.mLineStyle ||
+ !mIsBoldLine != !aOther.mIsBoldLine))
+ return false;
+ if (IsForegroundColorDefined() &&
+ (mForegroundColor != aOther.mForegroundColor))
+ return false;
+ if (IsBackgroundColorDefined() &&
+ (mBackgroundColor != aOther.mBackgroundColor))
+ return false;
+ if (IsUnderlineColorDefined() &&
+ (mUnderlineColor != aOther.mUnderlineColor))
+ return false;
+ return true;
+ }
+
+ bool operator!=(const TextRangeStyle& aOther) const {
+ return !Equals(aOther);
+ }
+
+ bool operator==(const TextRangeStyle& aOther) const { return Equals(aOther); }
+
+ uint8_t mDefinedStyles;
+ LineStyle mLineStyle; // DEFINED_LINESTYLE
+
+ bool mIsBoldLine; // DEFINED_LINESTYLE
+
+ nscolor mForegroundColor; // DEFINED_FOREGROUND_COLOR
+ nscolor mBackgroundColor; // DEFINED_BACKGROUND_COLOR
+ nscolor mUnderlineColor; // DEFINED_UNDERLINE_COLOR
+};
+
+/******************************************************************************
+ * mozilla::TextRange
+ ******************************************************************************/
+
+enum class TextRangeType : RawTextRangeType {
+ eUninitialized = 0x00,
+ eCaret = 0x01,
+ eRawClause = nsITextInputProcessor::ATTR_RAW_CLAUSE,
+ eSelectedRawClause = nsITextInputProcessor::ATTR_SELECTED_RAW_CLAUSE,
+ eConvertedClause = nsITextInputProcessor::ATTR_CONVERTED_CLAUSE,
+ eSelectedClause = nsITextInputProcessor::ATTR_SELECTED_CLAUSE
+};
+
+bool IsValidRawTextRangeValue(RawTextRangeType aRawTextRangeValue);
+RawTextRangeType ToRawTextRangeType(TextRangeType aTextRangeType);
+TextRangeType ToTextRangeType(RawTextRangeType aRawTextRangeType);
+const char* ToChar(TextRangeType aTextRangeType);
+SelectionType ToSelectionType(TextRangeType aTextRangeType);
+
+struct TextRange {
+ TextRange()
+ : mStartOffset(0),
+ mEndOffset(0),
+ mRangeType(TextRangeType::eUninitialized) {}
+
+ uint32_t mStartOffset;
+ // XXX Storing end offset makes the initializing code very complicated.
+ // We should replace it with mLength.
+ uint32_t mEndOffset;
+
+ TextRangeStyle mRangeStyle;
+
+ TextRangeType mRangeType;
+
+ uint32_t Length() const { return mEndOffset - mStartOffset; }
+
+ bool IsClause() const { return mRangeType != TextRangeType::eCaret; }
+
+ bool Equals(const TextRange& aOther) const {
+ return mStartOffset == aOther.mStartOffset &&
+ mEndOffset == aOther.mEndOffset && mRangeType == aOther.mRangeType &&
+ mRangeStyle == aOther.mRangeStyle;
+ }
+
+ void RemoveCharacter(uint32_t aOffset) {
+ if (mStartOffset > aOffset) {
+ --mStartOffset;
+ --mEndOffset;
+ } else if (mEndOffset > aOffset) {
+ --mEndOffset;
+ }
+ }
+};
+
+/******************************************************************************
+ * mozilla::TextRangeArray
+ ******************************************************************************/
+class TextRangeArray final : public AutoTArray<TextRange, 10> {
+ friend class WidgetCompositionEvent;
+
+ ~TextRangeArray() = default;
+
+ NS_INLINE_DECL_REFCOUNTING(TextRangeArray)
+
+ const TextRange* GetTargetClause() const {
+ for (uint32_t i = 0; i < Length(); ++i) {
+ const TextRange& range = ElementAt(i);
+ if (range.mRangeType == TextRangeType::eSelectedRawClause ||
+ range.mRangeType == TextRangeType::eSelectedClause) {
+ return &range;
+ }
+ }
+ return nullptr;
+ }
+
+ // Returns target clause offset. If there are selected clauses, this returns
+ // the first selected clause offset. Otherwise, 0.
+ uint32_t TargetClauseOffset() const {
+ const TextRange* range = GetTargetClause();
+ return range ? range->mStartOffset : 0;
+ }
+
+ // Returns target clause length. If there are selected clauses, this returns
+ // the first selected clause length. Otherwise, UINT32_MAX.
+ uint32_t TargetClauseLength() const {
+ const TextRange* range = GetTargetClause();
+ return range ? range->Length() : UINT32_MAX;
+ }
+
+ public:
+ bool IsComposing() const {
+ for (uint32_t i = 0; i < Length(); ++i) {
+ if (ElementAt(i).IsClause()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool Equals(const TextRangeArray& aOther) const {
+ size_t len = Length();
+ if (len != aOther.Length()) {
+ return false;
+ }
+ for (size_t i = 0; i < len; i++) {
+ if (!ElementAt(i).Equals(aOther.ElementAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ void RemoveCharacter(uint32_t aOffset) {
+ for (size_t i = 0, len = Length(); i < len; i++) {
+ ElementAt(i).RemoveCharacter(aOffset);
+ }
+ }
+
+ bool HasCaret() const {
+ for (const TextRange& range : *this) {
+ if (range.mRangeType == TextRangeType::eCaret) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool HasClauses() const {
+ for (const TextRange& range : *this) {
+ if (range.IsClause()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ uint32_t GetCaretPosition() const {
+ for (const TextRange& range : *this) {
+ if (range.mRangeType == TextRangeType::eCaret) {
+ return range.mStartOffset;
+ }
+ }
+ return UINT32_MAX;
+ }
+
+ const TextRange* GetFirstClause() const {
+ for (const TextRange& range : *this) {
+ // Look for the range of a clause whose start offset is 0 because the
+ // first clause's start offset is always 0.
+ if (range.IsClause() && !range.mStartOffset) {
+ return &range;
+ }
+ }
+ MOZ_ASSERT(!HasClauses());
+ return nullptr;
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_TextRage_h_
diff --git a/widget/TextRecognition.cpp b/widget/TextRecognition.cpp
new file mode 100644
index 0000000000..aa8ff98bb9
--- /dev/null
+++ b/widget/TextRecognition.cpp
@@ -0,0 +1,128 @@
+/* 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 "TextRecognition.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/ContentChild.h"
+#include "nsTextNode.h"
+#include "imgIContainer.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla::widget {
+
+auto TextRecognition::FindText(imgIContainer& aImage,
+ const nsTArray<nsCString>& aLanguages)
+ -> RefPtr<NativePromise> {
+ // TODO: Maybe decode async.
+ RefPtr<gfx::SourceSurface> surface = aImage.GetFrame(
+ imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ if (NS_WARN_IF(!surface)) {
+ return NativePromise::CreateAndReject("Failed to get surface"_ns, __func__);
+ }
+ RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
+ if (NS_WARN_IF(!dataSurface)) {
+ return NativePromise::CreateAndReject("Failed to get data surface"_ns,
+ __func__);
+ }
+ return FindText(*dataSurface, aLanguages);
+}
+
+auto TextRecognition::FindText(gfx::DataSourceSurface& aSurface,
+ const nsTArray<nsCString>& aLanguages)
+ -> RefPtr<NativePromise> {
+ if (!IsSupported()) {
+ return NativePromise::CreateAndReject("Text recognition not available"_ns,
+ __func__);
+ }
+
+ if (XRE_IsContentProcess()) {
+ auto* contentChild = ContentChild::GetSingleton();
+ auto image = nsContentUtils::SurfaceToIPCImage(aSurface);
+ if (!image) {
+ return NativePromise::CreateAndReject("Failed to share data surface"_ns,
+ __func__);
+ }
+ auto promise = MakeRefPtr<NativePromise::Private>(__func__);
+ contentChild->SendFindImageText(std::move(*image), aLanguages)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise](TextRecognitionResultOrError&& aResultOrError) {
+ switch (aResultOrError.type()) {
+ case TextRecognitionResultOrError::Type::TTextRecognitionResult:
+ promise->Resolve(
+ std::move(aResultOrError.get_TextRecognitionResult()),
+ __func__);
+ break;
+ case TextRecognitionResultOrError::Type::TnsCString:
+ promise->Reject(std::move(aResultOrError.get_nsCString()),
+ __func__);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown result?");
+ promise->Reject("Unknown error"_ns, __func__);
+ break;
+ }
+ },
+ [promise](mozilla::ipc::ResponseRejectReason) {
+ promise->Reject("IPC rejection"_ns, __func__);
+ });
+ return promise;
+ }
+ return DoFindText(aSurface, aLanguages);
+}
+
+void TextRecognition::FillShadow(ShadowRoot& aShadow,
+ const TextRecognitionResult& aResult) {
+ auto& doc = *aShadow.OwnerDoc();
+ RefPtr<Element> div = doc.CreateHTMLElement(nsGkAtoms::div);
+ for (const auto& quad : aResult.quads()) {
+ RefPtr<Element> span = doc.CreateHTMLElement(nsGkAtoms::span);
+ // TODO: We probably want to position them here and so on. For now, expose
+ // the data as attributes so that it's easy to play with the returned values
+ // in JS.
+ {
+ nsAutoString points;
+ for (const auto& point : quad.points()) {
+ points.AppendFloat(point.x);
+ points.Append(u',');
+ points.AppendFloat(point.y);
+ points.Append(u',');
+ }
+ points.Trim(",");
+ span->SetAttribute(u"data-points"_ns, points, IgnoreErrors());
+ nsAutoString confidence;
+ confidence.AppendFloat(quad.confidence());
+ span->SetAttribute(u"data-confidence"_ns, confidence, IgnoreErrors());
+ }
+
+ {
+ RefPtr<nsTextNode> text = doc.CreateTextNode(quad.string());
+ span->AppendChildTo(text, true, IgnoreErrors());
+ }
+ div->AppendChildTo(span, true, IgnoreErrors());
+ }
+ aShadow.AppendChildTo(div, true, IgnoreErrors());
+}
+
+#ifndef XP_MACOSX
+auto TextRecognition::DoFindText(gfx::DataSourceSurface&,
+ const nsTArray<nsCString>&)
+ -> RefPtr<NativePromise> {
+ MOZ_CRASH("DoFindText is not implemented on this platform");
+}
+#endif
+
+bool TextRecognition::IsSupported() {
+#ifdef XP_MACOSX
+ return true;
+#else
+ return false;
+#endif
+}
+
+} // namespace mozilla::widget
diff --git a/widget/TextRecognition.h b/widget/TextRecognition.h
new file mode 100644
index 0000000000..246c860a88
--- /dev/null
+++ b/widget/TextRecognition.h
@@ -0,0 +1,54 @@
+/* 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/. */
+
+#ifndef mozilla_widget_nsTextRecognition__
+#define mozilla_widget_nsTextRecognition__
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/gfx/Point.h"
+#include "nsTArray.h"
+
+class imgIContainer;
+namespace mozilla {
+
+namespace dom {
+class ShadowRoot;
+class TextRecognitionResultOrError;
+class TextRecognitionResult;
+} // namespace dom
+
+namespace gfx {
+class SourceSurface;
+class DataSourceSurface;
+} // namespace gfx
+
+namespace widget {
+
+class TextRecognition final {
+ public:
+ using NativePromise = MozPromise<dom::TextRecognitionResult, nsCString,
+ /* IsExclusive = */ true>;
+
+ TextRecognition() = default;
+
+ static void FillShadow(dom::ShadowRoot&, const dom::TextRecognitionResult&);
+
+ static RefPtr<NativePromise> FindText(imgIContainer&,
+ const nsTArray<nsCString>&);
+ static RefPtr<NativePromise> FindText(gfx::DataSourceSurface&,
+ const nsTArray<nsCString>&);
+ static bool IsSupported();
+
+ protected:
+ // This should be implemented in the OS specific file.
+ static RefPtr<NativePromise> DoFindText(gfx::DataSourceSurface&,
+ const nsTArray<nsCString>&);
+
+ ~TextRecognition() = default;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/Theme.cpp b/widget/Theme.cpp
new file mode 100644
index 0000000000..35c53240e5
--- /dev/null
+++ b/widget/Theme.cpp
@@ -0,0 +1,1718 @@
+/* -*- Mode: C++; tab-width: 40; 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 "Theme.h"
+#include <utility>
+#include "ThemeCocoa.h"
+
+#include "ThemeDrawing.h"
+#include "Units.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLMeterElement.h"
+#include "mozilla/dom/HTMLProgressElement.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/gfx/Filters.h"
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsCSSColorUtils.h"
+#include "nsCSSRendering.h"
+#include "nsScrollbarFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsDeviceContext.h"
+#include "nsLayoutUtils.h"
+#include "nsRangeFrame.h"
+#include "PathHelpers.h"
+#include "ScrollbarDrawingAndroid.h"
+#include "ScrollbarDrawingCocoa.h"
+#include "ScrollbarDrawingGTK.h"
+#include "ScrollbarDrawingWin.h"
+#include "ScrollbarDrawingWin11.h"
+
+#ifdef XP_WIN
+# include "mozilla/WindowsVersion.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+
+namespace {
+
+static constexpr gfx::sRGBColor sColorGrey10(
+ gfx::sRGBColor::UnusualFromARGB(0xffe9e9ed));
+static constexpr gfx::sRGBColor sColorGrey10Alpha50(
+ gfx::sRGBColor::UnusualFromARGB(0x7fe9e9ed));
+static constexpr gfx::sRGBColor sColorGrey20(
+ gfx::sRGBColor::UnusualFromARGB(0xffd0d0d7));
+static constexpr gfx::sRGBColor sColorGrey30(
+ gfx::sRGBColor::UnusualFromARGB(0xffb1b1b9));
+static constexpr gfx::sRGBColor sColorGrey40(
+ gfx::sRGBColor::UnusualFromARGB(0xff8f8f9d));
+static constexpr gfx::sRGBColor sColorGrey40Alpha50(
+ gfx::sRGBColor::UnusualFromARGB(0x7f8f8f9d));
+static constexpr gfx::sRGBColor sColorGrey50(
+ gfx::sRGBColor::UnusualFromARGB(0xff676774));
+static constexpr gfx::sRGBColor sColorGrey60(
+ gfx::sRGBColor::UnusualFromARGB(0xff484851));
+
+static constexpr gfx::sRGBColor sColorMeterGreen10(
+ gfx::sRGBColor::UnusualFromARGB(0xff00ab60));
+static constexpr gfx::sRGBColor sColorMeterGreen20(
+ gfx::sRGBColor::UnusualFromARGB(0xff056139));
+static constexpr gfx::sRGBColor sColorMeterYellow10(
+ gfx::sRGBColor::UnusualFromARGB(0xffffbd4f));
+static constexpr gfx::sRGBColor sColorMeterYellow20(
+ gfx::sRGBColor::UnusualFromARGB(0xffd2811e));
+static constexpr gfx::sRGBColor sColorMeterRed10(
+ gfx::sRGBColor::UnusualFromARGB(0xffe22850));
+static constexpr gfx::sRGBColor sColorMeterRed20(
+ gfx::sRGBColor::UnusualFromARGB(0xff810220));
+
+static const CSSCoord kMinimumRangeThumbSize = 20.0f;
+static const CSSCoord kMinimumDropdownArrowButtonWidth = 18.0f;
+static const CSSCoord kMinimumSpinnerButtonWidth = 18.0f;
+static const CSSCoord kMinimumSpinnerButtonHeight = 9.0f;
+static const CSSCoord kButtonBorderWidth = 1.0f;
+static const CSSCoord kMenulistBorderWidth = 1.0f;
+static const CSSCoord kTextFieldBorderWidth = 1.0f;
+static const CSSCoord kRangeHeight = 6.0f;
+static const CSSCoord kProgressbarHeight = 6.0f;
+static const CSSCoord kMeterHeight = 12.0f;
+
+// nsCheckboxRadioFrame takes the bottom of the content box as the baseline.
+// This border-width makes its baseline 2px under the bottom, which is nice.
+static constexpr CSSCoord kCheckboxRadioBorderWidth = 2.0f;
+
+static constexpr sRGBColor sTransparent = sRGBColor::White(0.0);
+
+// This pushes and pops a clip rect to the draw target.
+//
+// This is done to reduce fuzz in places where we may have antialiasing,
+// because skia is not clip-invariant: given different clips, it does not
+// guarantee the same result, even if the painted content doesn't intersect
+// the clips.
+//
+// This is a bit sad, overall, but...
+struct MOZ_RAII AutoClipRect {
+ AutoClipRect(DrawTarget& aDt, const LayoutDeviceRect& aRect) : mDt(aDt) {
+ mDt.PushClipRect(aRect.ToUnknownRect());
+ }
+
+ ~AutoClipRect() { mDt.PopClip(); }
+
+ private:
+ DrawTarget& mDt;
+};
+
+static StaticRefPtr<Theme> gNativeInstance;
+static StaticRefPtr<Theme> gNonNativeInstance;
+static StaticRefPtr<Theme> gRDMInstance;
+
+} // namespace
+
+#ifdef ANDROID
+already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() {
+ // Android doesn't have a native theme.
+ return do_AddRef(new Theme(Theme::ScrollbarStyle()));
+}
+#endif
+
+already_AddRefed<nsITheme> do_GetBasicNativeThemeDoNotUseDirectly() {
+ if (MOZ_UNLIKELY(!gNonNativeInstance)) {
+ UniquePtr<ScrollbarDrawing> scrollbarDrawing = Theme::ScrollbarStyle();
+#ifdef MOZ_WIDGET_COCOA
+ gNonNativeInstance = new ThemeCocoa(std::move(scrollbarDrawing));
+#else
+ gNonNativeInstance = new Theme(std::move(scrollbarDrawing));
+#endif
+ ClearOnShutdown(&gNonNativeInstance);
+ }
+ return do_AddRef(gNonNativeInstance);
+}
+
+already_AddRefed<nsITheme> do_GetNativeThemeDoNotUseDirectly() {
+ if (MOZ_UNLIKELY(!gNativeInstance)) {
+ gNativeInstance = do_CreateNativeThemeDoNotUseDirectly();
+ ClearOnShutdown(&gNativeInstance);
+ }
+ return do_AddRef(gNativeInstance);
+}
+
+already_AddRefed<nsITheme> do_GetRDMThemeDoNotUseDirectly() {
+ if (MOZ_UNLIKELY(!gRDMInstance)) {
+ gRDMInstance = new Theme(MakeUnique<ScrollbarDrawingAndroid>());
+ ClearOnShutdown(&gRDMInstance);
+ }
+ return do_AddRef(gRDMInstance);
+}
+
+namespace mozilla::widget {
+
+NS_IMPL_ISUPPORTS_INHERITED(Theme, nsNativeTheme, nsITheme)
+
+static constexpr nsLiteralCString kPrefs[] = {
+ "widget.non-native-theme.use-theme-accent"_ns,
+ "widget.non-native-theme.win.scrollbar.use-system-size"_ns,
+ "widget.non-native-theme.scrollbar.size.override"_ns,
+ "widget.non-native-theme.scrollbar.style"_ns,
+};
+
+void Theme::Init() {
+ for (const auto& pref : kPrefs) {
+ Preferences::RegisterCallback(PrefChangedCallback, pref);
+ }
+ LookAndFeelChanged();
+}
+
+void Theme::Shutdown() {
+ for (const auto& pref : kPrefs) {
+ Preferences::UnregisterCallback(PrefChangedCallback, pref);
+ }
+}
+
+/* static */
+void Theme::LookAndFeelChanged() {
+ ThemeColors::RecomputeAccentColors();
+ if (gNonNativeInstance) {
+ gNonNativeInstance->SetScrollbarDrawing(ScrollbarStyle());
+ }
+ if (gNativeInstance) {
+ gNativeInstance->SetScrollbarDrawing(ScrollbarStyle());
+ }
+}
+
+auto Theme::GetDPIRatio(nsPresContext* aPc, StyleAppearance aAppearance)
+ -> DPIRatio {
+ // Widgets react to zoom, except scrollbars.
+ if (IsWidgetScrollbarPart(aAppearance)) {
+ return GetScrollbarDrawing().GetDPIRatioForScrollbarPart(aPc);
+ }
+ return DPIRatio(float(AppUnitsPerCSSPixel()) / aPc->AppUnitsPerDevPixel());
+}
+
+auto Theme::GetDPIRatio(nsIFrame* aFrame, StyleAppearance aAppearance)
+ -> DPIRatio {
+ return GetDPIRatio(aFrame->PresContext(), aAppearance);
+}
+
+// Checkbox and radio need to preserve aspect-ratio for compat. We also snap the
+// size to exact device pixels to avoid snapping disorting the circles.
+static LayoutDeviceRect CheckBoxRadioRect(const LayoutDeviceRect& aRect) {
+ // Place a square rect in the center of aRect.
+ auto size = std::trunc(std::min(aRect.width, aRect.height));
+ auto position = aRect.Center() - LayoutDevicePoint(size * 0.5, size * 0.5);
+ return LayoutDeviceRect(position, LayoutDeviceSize(size, size));
+}
+
+std::tuple<sRGBColor, sRGBColor, sRGBColor> Theme::ComputeCheckboxColors(
+ const ElementState& aState, StyleAppearance aAppearance,
+ const Colors& aColors) {
+ MOZ_ASSERT(aAppearance == StyleAppearance::Checkbox ||
+ aAppearance == StyleAppearance::Radio);
+
+ bool isDisabled = aState.HasState(ElementState::DISABLED);
+ bool isChecked = aState.HasState(ElementState::CHECKED);
+ bool isIndeterminate = aAppearance == StyleAppearance::Checkbox &&
+ aState.HasState(ElementState::INDETERMINATE);
+
+ if (isChecked || isIndeterminate) {
+ if (isDisabled) {
+ auto bg = ComputeBorderColor(aState, aColors, OutlineCoversBorder::No);
+ auto fg = aColors.HighContrast()
+ ? aColors.System(StyleSystemColor::Graytext)
+ : sRGBColor::White(aColors.IsDark() ? .4f : .8f);
+ return std::make_tuple(bg, bg, fg);
+ }
+
+ if (aColors.HighContrast()) {
+ auto bg = aColors.System(StyleSystemColor::Selecteditem);
+ auto fg = aColors.System(StyleSystemColor::Selecteditemtext);
+ return std::make_tuple(bg, bg, fg);
+ }
+
+ bool isActive =
+ aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
+ bool isHovered = aState.HasState(ElementState::HOVER);
+ const auto& bg = isActive ? aColors.Accent().GetDarker()
+ : isHovered ? aColors.Accent().GetDark()
+ : aColors.Accent().Get();
+ const auto& fg = aColors.Accent().GetForeground();
+ return std::make_tuple(bg, bg, fg);
+ }
+
+ auto [bg, border] =
+ ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::No);
+ // We don't paint a checkmark in this case so any color would do.
+ return std::make_tuple(bg, border, sTransparent);
+}
+
+sRGBColor Theme::ComputeBorderColor(const ElementState& aState,
+ const Colors& aColors,
+ OutlineCoversBorder aOutlineCoversBorder) {
+ bool isDisabled = aState.HasState(ElementState::DISABLED);
+ if (aColors.HighContrast()) {
+ return aColors.System(isDisabled ? StyleSystemColor::Graytext
+ : StyleSystemColor::Buttontext);
+ }
+ bool isActive =
+ aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
+ bool isHovered = aState.HasState(ElementState::HOVER);
+ bool isFocused = aState.HasState(ElementState::FOCUSRING);
+ if (isDisabled) {
+ return sColorGrey40Alpha50;
+ }
+ if (isFocused && aOutlineCoversBorder == OutlineCoversBorder::Yes) {
+ // If we draw the outline over the border, prevent issues where the border
+ // shows underneath if it snaps in the wrong direction by using a
+ // transparent border. An alternative to this is ensuring that we snap the
+ // offset in PaintRoundedFocusRect the same was a we snap border widths, so
+ // that negative offsets are guaranteed to cover the border.
+ // But this looks harder to mess up.
+ return sTransparent;
+ }
+ bool dark = aColors.IsDark();
+ if (isActive) {
+ return dark ? sColorGrey20 : sColorGrey60;
+ }
+ if (isHovered) {
+ return dark ? sColorGrey30 : sColorGrey50;
+ }
+ return sColorGrey40;
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeButtonColors(
+ const ElementState& aState, const Colors& aColors, nsIFrame* aFrame) {
+ bool isActive =
+ aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
+ bool isDisabled = aState.HasState(ElementState::DISABLED);
+ bool isHovered = aState.HasState(ElementState::HOVER);
+
+ nscolor backgroundColor = [&] {
+ if (isDisabled) {
+ return aColors.SystemNs(StyleSystemColor::MozButtondisabledface);
+ }
+ if (isActive) {
+ return aColors.SystemNs(StyleSystemColor::MozButtonactiveface);
+ }
+ if (isHovered) {
+ return aColors.SystemNs(StyleSystemColor::MozButtonhoverface);
+ }
+ return aColors.SystemNs(StyleSystemColor::Buttonface);
+ }();
+
+ if (aState.HasState(ElementState::AUTOFILL)) {
+ backgroundColor = NS_ComposeColors(
+ backgroundColor,
+ aColors.SystemNs(StyleSystemColor::MozAutofillBackground));
+ }
+
+ const sRGBColor borderColor =
+ ComputeBorderColor(aState, aColors, OutlineCoversBorder::Yes);
+ return std::make_pair(sRGBColor::FromABGR(backgroundColor), borderColor);
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeTextfieldColors(
+ const ElementState& aState, const Colors& aColors,
+ OutlineCoversBorder aOutlineCoversBorder) {
+ nscolor backgroundColor = [&] {
+ if (aState.HasState(ElementState::DISABLED)) {
+ return aColors.SystemNs(StyleSystemColor::MozDisabledfield);
+ }
+ return aColors.SystemNs(StyleSystemColor::Field);
+ }();
+
+ if (aState.HasState(ElementState::AUTOFILL)) {
+ backgroundColor = NS_ComposeColors(
+ backgroundColor,
+ aColors.SystemNs(StyleSystemColor::MozAutofillBackground));
+ }
+
+ const sRGBColor borderColor =
+ ComputeBorderColor(aState, aColors, aOutlineCoversBorder);
+ return std::make_pair(sRGBColor::FromABGR(backgroundColor), borderColor);
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeProgressColors(
+ const ElementState& aState, const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return aColors.SystemPair(StyleSystemColor::Selecteditem,
+ StyleSystemColor::Buttontext);
+ }
+
+ bool isActive =
+ aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
+ bool isDisabled = aState.HasState(ElementState::DISABLED);
+ bool isHovered = aState.HasState(ElementState::HOVER);
+
+ if (isDisabled) {
+ return std::make_pair(sColorGrey40Alpha50, sColorGrey40Alpha50);
+ }
+ if (isActive || isHovered) {
+ return std::make_pair(aColors.Accent().GetDark(),
+ aColors.Accent().GetDarker());
+ }
+ return std::make_pair(aColors.Accent().Get(), aColors.Accent().GetDark());
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeTrackColors(
+ const ElementState& aState, const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return aColors.SystemPair(StyleSystemColor::Window,
+ StyleSystemColor::Buttontext);
+ }
+ bool isActive =
+ aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
+ bool isDisabled = aState.HasState(ElementState::DISABLED);
+ bool isHovered = aState.HasState(ElementState::HOVER);
+
+ if (isDisabled) {
+ return std::make_pair(sColorGrey10Alpha50, sColorGrey40Alpha50);
+ }
+ if (isActive || isHovered) {
+ return std::make_pair(sColorGrey20, sColorGrey50);
+ }
+ return std::make_pair(sColorGrey10, sColorGrey40);
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeThumbColors(
+ const ElementState& aState, const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return aColors.SystemPair(StyleSystemColor::Selecteditemtext,
+ StyleSystemColor::Selecteditem);
+ }
+
+ bool isActive =
+ aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
+ bool isDisabled = aState.HasState(ElementState::DISABLED);
+ bool isHovered = aState.HasState(ElementState::HOVER);
+
+ const sRGBColor& backgroundColor = [&] {
+ if (isDisabled) {
+ return sColorGrey40;
+ }
+ if (isActive) {
+ return aColors.Accent().Get();
+ }
+ if (isHovered) {
+ return sColorGrey60;
+ }
+ return sColorGrey50;
+ }();
+
+ const sRGBColor borderColor = sRGBColor::OpaqueWhite();
+ return std::make_pair(backgroundColor, borderColor);
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeProgressColors(
+ const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return aColors.SystemPair(StyleSystemColor::Selecteditem,
+ StyleSystemColor::Buttontext);
+ }
+ return std::make_pair(aColors.Accent().Get(), aColors.Accent().GetDark());
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeProgressTrackColors(
+ const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return aColors.SystemPair(StyleSystemColor::Buttonface,
+ StyleSystemColor::Buttontext);
+ }
+ return std::make_pair(sColorGrey10, sColorGrey40);
+}
+
+std::pair<sRGBColor, sRGBColor> Theme::ComputeMeterchunkColors(
+ const ElementState& aMeterState, const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return ComputeProgressColors(aColors);
+ }
+ sRGBColor borderColor = sColorMeterGreen20;
+ sRGBColor chunkColor = sColorMeterGreen10;
+
+ if (aMeterState.HasState(ElementState::SUB_OPTIMUM)) {
+ borderColor = sColorMeterYellow20;
+ chunkColor = sColorMeterYellow10;
+ } else if (aMeterState.HasState(ElementState::SUB_SUB_OPTIMUM)) {
+ borderColor = sColorMeterRed20;
+ chunkColor = sColorMeterRed10;
+ }
+
+ return std::make_pair(chunkColor, borderColor);
+}
+
+std::array<sRGBColor, 3> Theme::ComputeFocusRectColors(const Colors& aColors) {
+ if (aColors.HighContrast()) {
+ return {aColors.System(StyleSystemColor::Selecteditem),
+ aColors.System(StyleSystemColor::Buttontext),
+ aColors.System(StyleSystemColor::Window)};
+ }
+ const auto& accent = aColors.Accent();
+ const sRGBColor middle =
+ aColors.IsDark() ? sRGBColor::Black(.3f) : sRGBColor::White(.3f);
+ return {accent.Get(), middle, accent.GetLight()};
+}
+
+template <typename PaintBackendData>
+void Theme::PaintRoundedFocusRect(PaintBackendData& aBackendData,
+ const LayoutDeviceRect& aRect,
+ const Colors& aColors, DPIRatio aDpiRatio,
+ CSSCoord aRadius, CSSCoord aOffset) {
+ // NOTE(emilio): If the widths or offsets here change, make sure to tweak
+ // the GetWidgetOverflow path for FocusOutline.
+ auto [innerColor, middleColor, outerColor] = ComputeFocusRectColors(aColors);
+
+ LayoutDeviceRect focusRect(aRect);
+
+ // The focus rect is painted outside of the border area (aRect), see:
+ //
+ // data:text/html,<div style="border: 1px solid; outline: 2px solid
+ // red">Foobar</div>
+ //
+ // But some controls might provide a negative offset to cover the border, if
+ // necessary.
+ CSSCoord strokeWidth = 2.0f;
+ auto strokeWidthDevPx =
+ LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
+ CSSCoord strokeRadius = aRadius;
+ focusRect.Inflate(aOffset * aDpiRatio + strokeWidthDevPx);
+
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aBackendData, focusRect, sTransparent, innerColor, strokeWidth,
+ strokeRadius, aDpiRatio);
+
+ strokeWidth = CSSCoord(1.0f);
+ strokeWidthDevPx =
+ LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
+ strokeRadius += strokeWidth;
+ focusRect.Inflate(strokeWidthDevPx);
+
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aBackendData, focusRect, sTransparent, middleColor, strokeWidth,
+ strokeRadius, aDpiRatio);
+
+ strokeWidth = CSSCoord(2.0f);
+ strokeWidthDevPx =
+ LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
+ strokeRadius += strokeWidth;
+ focusRect.Inflate(strokeWidthDevPx);
+
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aBackendData, focusRect, sTransparent, outerColor, strokeWidth,
+ strokeRadius, aDpiRatio);
+}
+
+void Theme::PaintCheckboxControl(DrawTarget& aDrawTarget,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState,
+ const Colors& aColors, DPIRatio aDpiRatio) {
+ auto [backgroundColor, borderColor, checkColor] =
+ ComputeCheckboxColors(aState, StyleAppearance::Checkbox, aColors);
+ {
+ const CSSCoord radius = 2.0f;
+ CSSCoord borderWidth = kCheckboxRadioBorderWidth;
+ if (backgroundColor == borderColor) {
+ borderWidth = 0.0f;
+ }
+ ThemeDrawing::PaintRoundedRectWithRadius(aDrawTarget, aRect,
+ backgroundColor, borderColor,
+ borderWidth, radius, aDpiRatio);
+ }
+
+ if (aState.HasState(ElementState::INDETERMINATE)) {
+ PaintIndeterminateMark(aDrawTarget, aRect, checkColor);
+ } else if (aState.HasState(ElementState::CHECKED)) {
+ PaintCheckMark(aDrawTarget, aRect, checkColor);
+ }
+
+ if (aState.HasState(ElementState::FOCUSRING)) {
+ PaintRoundedFocusRect(aDrawTarget, aRect, aColors, aDpiRatio, 5.0f, 1.0f);
+ }
+}
+
+constexpr CSSCoord kCheckboxRadioContentBoxSize = 10.0f;
+constexpr CSSCoord kCheckboxRadioBorderBoxSize =
+ kCheckboxRadioContentBoxSize + kCheckboxRadioBorderWidth * 2.0f;
+
+void Theme::PaintCheckMark(DrawTarget& aDrawTarget,
+ const LayoutDeviceRect& aRect,
+ const sRGBColor& aColor) {
+ // Points come from the coordinates on a 14X14 (kCheckboxRadioBorderBoxSize)
+ // unit box centered at 0,0
+ const float checkPolygonX[] = {-4.5f, -1.5f, -0.5f, 5.0f, 4.75f,
+ 3.5f, -0.5f, -1.5f, -3.5f};
+ const float checkPolygonY[] = {0.5f, 4.0f, 4.0f, -2.5f, -4.0f,
+ -4.0f, 1.0f, 1.25f, -1.0f};
+ const int32_t checkNumPoints = sizeof(checkPolygonX) / sizeof(float);
+ const float scale =
+ ThemeDrawing::ScaleToFillRect(aRect, kCheckboxRadioBorderBoxSize);
+ auto center = aRect.Center().ToUnknownPoint();
+
+ RefPtr<PathBuilder> builder = aDrawTarget.CreatePathBuilder();
+ Point p = center + Point(checkPolygonX[0] * scale, checkPolygonY[0] * scale);
+ builder->MoveTo(p);
+ for (int32_t i = 1; i < checkNumPoints; i++) {
+ p = center + Point(checkPolygonX[i] * scale, checkPolygonY[i] * scale);
+ builder->LineTo(p);
+ }
+ RefPtr<Path> path = builder->Finish();
+
+ aDrawTarget.Fill(path, ColorPattern(ToDeviceColor(aColor)));
+}
+
+void Theme::PaintIndeterminateMark(DrawTarget& aDrawTarget,
+ const LayoutDeviceRect& aRect,
+ const sRGBColor& aColor) {
+ const CSSCoord borderWidth = 2.0f;
+ const float scale =
+ ThemeDrawing::ScaleToFillRect(aRect, kCheckboxRadioBorderBoxSize);
+
+ Rect rect = aRect.ToUnknownRect();
+ rect.y += (rect.height / 2) - (borderWidth * scale / 2);
+ rect.height = borderWidth * scale;
+ rect.x += (borderWidth * scale) + (borderWidth * scale / 8);
+ rect.width -= ((borderWidth * scale) + (borderWidth * scale / 8)) * 2;
+
+ aDrawTarget.FillRect(rect, ColorPattern(ToDeviceColor(aColor)));
+}
+
+template <typename PaintBackendData>
+void Theme::PaintStrokedCircle(PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ const sRGBColor& aBackgroundColor,
+ const sRGBColor& aBorderColor,
+ const CSSCoord aBorderWidth,
+ DPIRatio aDpiRatio) {
+ auto radius = LayoutDeviceCoord(aRect.Size().width) / aDpiRatio;
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, aBackgroundColor,
+ aBorderColor, aBorderWidth, radius,
+ aDpiRatio);
+}
+
+void Theme::PaintCircleShadow(WebRenderBackendData& aWrData,
+ const LayoutDeviceRect& aBoxRect,
+ const LayoutDeviceRect& aClipRect,
+ float aShadowAlpha, const CSSPoint& aShadowOffset,
+ CSSCoord aShadowBlurStdDev, DPIRatio aDpiRatio) {
+ const bool kBackfaceIsVisible = true;
+ const LayoutDeviceCoord stdDev = aShadowBlurStdDev * aDpiRatio;
+ const LayoutDevicePoint shadowOffset = aShadowOffset * aDpiRatio;
+ const IntSize inflation =
+ gfxAlphaBoxBlur::CalculateBlurRadius(gfxPoint(stdDev, stdDev));
+ LayoutDeviceRect shadowRect = aBoxRect;
+ shadowRect.MoveBy(shadowOffset);
+ shadowRect.Inflate(inflation.width, inflation.height);
+ const auto boxRect = wr::ToLayoutRect(aBoxRect);
+ aWrData.mBuilder.PushBoxShadow(
+ wr::ToLayoutRect(shadowRect), wr::ToLayoutRect(aClipRect),
+ kBackfaceIsVisible, boxRect,
+ wr::ToLayoutVector2D(aShadowOffset * aDpiRatio),
+ wr::ToColorF(DeviceColor(0.0f, 0.0f, 0.0f, aShadowAlpha)), stdDev,
+ /* aSpread = */ 0.0f,
+ wr::ToBorderRadius(gfx::RectCornerRadii(aBoxRect.Size().width)),
+ wr::BoxShadowClipMode::Outset);
+}
+
+void Theme::PaintCircleShadow(DrawTarget& aDrawTarget,
+ const LayoutDeviceRect& aBoxRect,
+ const LayoutDeviceRect& aClipRect,
+ float aShadowAlpha, const CSSPoint& aShadowOffset,
+ CSSCoord aShadowBlurStdDev, DPIRatio aDpiRatio) {
+ Float stdDev = aShadowBlurStdDev * aDpiRatio;
+ Point offset = (aShadowOffset * aDpiRatio).ToUnknownPoint();
+
+ RefPtr<FilterNode> blurFilter =
+ aDrawTarget.CreateFilter(FilterType::GAUSSIAN_BLUR);
+ if (!blurFilter) {
+ return;
+ }
+
+ blurFilter->SetAttribute(ATT_GAUSSIAN_BLUR_STD_DEVIATION, stdDev);
+
+ IntSize inflation =
+ gfxAlphaBoxBlur::CalculateBlurRadius(gfxPoint(stdDev, stdDev));
+ Rect inflatedRect = aBoxRect.ToUnknownRect();
+ inflatedRect.Inflate(inflation.width, inflation.height);
+ Rect sourceRectInFilterSpace =
+ inflatedRect - aBoxRect.TopLeft().ToUnknownPoint();
+ Point destinationPointOfSourceRect = inflatedRect.TopLeft() + offset;
+
+ IntSize dtSize = RoundedToInt(aBoxRect.Size().ToUnknownSize());
+ RefPtr<DrawTarget> ellipseDT = aDrawTarget.CreateSimilarDrawTargetForFilter(
+ dtSize, SurfaceFormat::A8, blurFilter, blurFilter,
+ sourceRectInFilterSpace, destinationPointOfSourceRect);
+ if (!ellipseDT) {
+ return;
+ }
+
+ AutoClipRect clipRect(aDrawTarget, aClipRect);
+
+ RefPtr<Path> ellipse = MakePathForEllipse(
+ *ellipseDT, (aBoxRect - aBoxRect.TopLeft()).Center().ToUnknownPoint(),
+ aBoxRect.Size().ToUnknownSize());
+ ellipseDT->Fill(ellipse,
+ ColorPattern(DeviceColor(0.0f, 0.0f, 0.0f, aShadowAlpha)));
+ RefPtr<SourceSurface> ellipseSurface = ellipseDT->Snapshot();
+
+ blurFilter->SetInput(IN_GAUSSIAN_BLUR_IN, ellipseSurface);
+ aDrawTarget.DrawFilter(blurFilter, sourceRectInFilterSpace,
+ destinationPointOfSourceRect);
+}
+
+template <typename PaintBackendData>
+void Theme::PaintRadioControl(PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState, const Colors& aColors,
+ DPIRatio aDpiRatio) {
+ auto [backgroundColor, borderColor, checkColor] =
+ ComputeCheckboxColors(aState, StyleAppearance::Radio, aColors);
+ {
+ CSSCoord borderWidth = kCheckboxRadioBorderWidth;
+ if (backgroundColor == borderColor) {
+ borderWidth = 0.0f;
+ }
+ PaintStrokedCircle(aPaintData, aRect, backgroundColor, borderColor,
+ borderWidth, aDpiRatio);
+ }
+
+ if (aState.HasState(ElementState::CHECKED)) {
+ LayoutDeviceRect rect(aRect);
+ auto width = LayoutDeviceCoord(
+ ThemeDrawing::SnapBorderWidth(kCheckboxRadioBorderWidth, aDpiRatio));
+ rect.Deflate(width);
+
+ PaintStrokedCircle(aPaintData, rect, backgroundColor, checkColor,
+ kCheckboxRadioBorderWidth, aDpiRatio);
+ }
+
+ if (aState.HasState(ElementState::FOCUSRING)) {
+ PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio, 5.0f, 1.0f);
+ }
+}
+
+template <typename PaintBackendData>
+void Theme::PaintTextField(PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState, const Colors& aColors,
+ DPIRatio aDpiRatio) {
+ auto [backgroundColor, borderColor] =
+ ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::Yes);
+
+ const CSSCoord radius = 2.0f;
+
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
+ borderColor, kTextFieldBorderWidth,
+ radius, aDpiRatio);
+
+ if (aState.HasState(ElementState::FOCUSRING)) {
+ PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
+ radius + kTextFieldBorderWidth,
+ -kTextFieldBorderWidth);
+ }
+}
+
+template <typename PaintBackendData>
+void Theme::PaintListbox(PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState, const Colors& aColors,
+ DPIRatio aDpiRatio) {
+ const CSSCoord radius = 2.0f;
+ auto [backgroundColor, borderColor] =
+ ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::Yes);
+
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
+ borderColor, kMenulistBorderWidth,
+ radius, aDpiRatio);
+
+ if (aState.HasState(ElementState::FOCUSRING)) {
+ PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
+ radius + kMenulistBorderWidth, -kMenulistBorderWidth);
+ }
+}
+
+template <typename PaintBackendData>
+void Theme::PaintMenulist(PaintBackendData& aDrawTarget,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState, const Colors& aColors,
+ DPIRatio aDpiRatio) {
+ const CSSCoord radius = 4.0f;
+ auto [backgroundColor, borderColor] = ComputeButtonColors(aState, aColors);
+
+ ThemeDrawing::PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor,
+ borderColor, kMenulistBorderWidth,
+ radius, aDpiRatio);
+
+ if (aState.HasState(ElementState::FOCUSRING)) {
+ PaintRoundedFocusRect(aDrawTarget, aRect, aColors, aDpiRatio,
+ radius + kMenulistBorderWidth, -kMenulistBorderWidth);
+ }
+}
+
+enum class PhysicalArrowDirection {
+ Right,
+ Left,
+ Bottom,
+};
+
+void Theme::PaintMenulistArrow(nsIFrame* aFrame, DrawTarget& aDrawTarget,
+ const LayoutDeviceRect& aRect) {
+ // not const: these may be negated in-place below
+ float polygonX[] = {-4.0f, -0.5f, 0.5f, 4.0f, 4.0f,
+ 3.0f, 0.0f, 0.0f, -3.0f, -4.0f};
+ float polygonY[] = {-1, 3.0f, 3.0f, -1.0f, -2.0f,
+ -2.0f, 1.5f, 1.5f, -2.0f, -2.0f};
+
+ const float kPolygonSize = kMinimumDropdownArrowButtonWidth;
+ const auto direction = [&] {
+ const auto wm = aFrame->GetWritingMode();
+ switch (wm.GetBlockDir()) {
+ case WritingMode::BlockDir::eBlockLR:
+ return PhysicalArrowDirection::Right;
+ case WritingMode::BlockDir::eBlockRL:
+ return PhysicalArrowDirection::Left;
+ case WritingMode::BlockDir::eBlockTB:
+ return PhysicalArrowDirection::Bottom;
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown direction?");
+ return PhysicalArrowDirection::Bottom;
+ }();
+
+ auto const [xs, ys] = [&] {
+ using Pair = std::pair<const float*, const float*>;
+ switch (direction) {
+ case PhysicalArrowDirection::Left:
+ // rotate 90°: [[0,1],[-1,0]]
+ for (float& f : polygonY) {
+ f = -f;
+ }
+ return Pair(polygonY, polygonX);
+
+ case PhysicalArrowDirection::Right:
+ // rotate 270°: [[0,-1],[1,0]]
+ for (float& f : polygonX) {
+ f = -f;
+ }
+ return Pair(polygonY, polygonX);
+
+ case PhysicalArrowDirection::Bottom:
+ // rotate 0°: [[1,0],[0,1]]
+ return Pair(polygonX, polygonY);
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown direction?");
+ return Pair(polygonX, polygonY);
+ }();
+
+ const auto arrowColor = sRGBColor::FromABGR(
+ nsLayoutUtils::GetColor(aFrame, &nsStyleText::mWebkitTextFillColor));
+ ThemeDrawing::PaintArrow(aDrawTarget, aRect, xs, ys, kPolygonSize,
+ ArrayLength(polygonX), arrowColor);
+}
+
+void Theme::PaintSpinnerButton(nsIFrame* aFrame, DrawTarget& aDrawTarget,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState,
+ StyleAppearance aAppearance,
+ const Colors& aColors, DPIRatio aDpiRatio) {
+ auto [backgroundColor, borderColor] = ComputeButtonColors(aState, aColors);
+
+ aDrawTarget.FillRect(aRect.ToUnknownRect(),
+ ColorPattern(ToDeviceColor(backgroundColor)));
+
+ const float kPolygonX[] = {-3.5f, -0.5f, 0.5f, 3.5f, 3.5f,
+ 2.5f, 0.0f, 0.0f, -2.5f, -3.5f};
+ float polygonY[] = {-1.5f, 1.5f, 1.5f, -1.5f, -2.5f,
+ -2.5f, 0.0f, 0.0f, -2.5f, -2.5f};
+
+ const float kPolygonSize = kMinimumSpinnerButtonHeight;
+ if (aAppearance == StyleAppearance::SpinnerUpbutton) {
+ for (auto& coord : polygonY) {
+ coord = -coord;
+ }
+ }
+
+ ThemeDrawing::PaintArrow(aDrawTarget, aRect, kPolygonX, polygonY,
+ kPolygonSize, ArrayLength(kPolygonX), borderColor);
+}
+
+template <typename PaintBackendData>
+void Theme::PaintRange(nsIFrame* aFrame, PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState, const Colors& aColors,
+ DPIRatio aDpiRatio, bool aHorizontal) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(aFrame);
+ if (!rangeFrame) {
+ return;
+ }
+
+ auto tickMarks = rangeFrame->TickMarks();
+ double progress = rangeFrame->GetValueAsFractionOfRange();
+ auto rect = aRect;
+ LayoutDeviceRect thumbRect(0, 0, kMinimumRangeThumbSize * aDpiRatio,
+ kMinimumRangeThumbSize * aDpiRatio);
+ LayoutDeviceRect progressClipRect(aRect);
+ LayoutDeviceRect trackClipRect(aRect);
+ const LayoutDeviceCoord verticalSize = kRangeHeight * aDpiRatio;
+ const LayoutDeviceCoord tickMarkWidth(
+ ThemeDrawing::SnapBorderWidth(1.0f, aDpiRatio));
+ const LayoutDeviceCoord tickMarkHeight(
+ ThemeDrawing::SnapBorderWidth(5.0f, aDpiRatio));
+ LayoutDevicePoint tickMarkOrigin, tickMarkDirection;
+ LayoutDeviceSize tickMarkSize;
+ if (aHorizontal) {
+ rect.height = verticalSize;
+ rect.y = aRect.y + (aRect.height - rect.height) / 2;
+ tickMarkSize = LayoutDeviceSize(tickMarkWidth, tickMarkHeight);
+ thumbRect.y = aRect.y + (aRect.height - thumbRect.height) / 2;
+
+ if (IsFrameRTL(aFrame)) {
+ tickMarkOrigin =
+ LayoutDevicePoint(aRect.XMost() - thumbRect.width / 2, aRect.YMost());
+ tickMarkDirection = LayoutDevicePoint(-1.0f, 0.0f);
+ thumbRect.x =
+ aRect.x + (aRect.width - thumbRect.width) * (1.0 - progress);
+ float midPoint = thumbRect.Center().X();
+ trackClipRect.SetBoxX(aRect.X(), midPoint);
+ progressClipRect.SetBoxX(midPoint, aRect.XMost());
+ } else {
+ tickMarkOrigin =
+ LayoutDevicePoint(aRect.x + thumbRect.width / 2, aRect.YMost());
+ tickMarkDirection = LayoutDevicePoint(1.0, 0.0f);
+ thumbRect.x = aRect.x + (aRect.width - thumbRect.width) * progress;
+ float midPoint = thumbRect.Center().X();
+ progressClipRect.SetBoxX(aRect.X(), midPoint);
+ trackClipRect.SetBoxX(midPoint, aRect.XMost());
+ }
+ } else {
+ rect.width = verticalSize;
+ rect.x = aRect.x + (aRect.width - rect.width) / 2;
+ tickMarkOrigin = LayoutDevicePoint(aRect.XMost() - tickMarkHeight / 4,
+ aRect.YMost() - thumbRect.width / 2);
+ tickMarkDirection = LayoutDevicePoint(0.0f, -1.0f);
+ tickMarkSize = LayoutDeviceSize(tickMarkHeight, tickMarkWidth);
+ thumbRect.x = aRect.x + (aRect.width - thumbRect.width) / 2;
+
+ if (rangeFrame->IsUpwards()) {
+ thumbRect.y =
+ aRect.y + (aRect.height - thumbRect.height) * (1.0 - progress);
+ float midPoint = thumbRect.Center().Y();
+ trackClipRect.SetBoxY(aRect.Y(), midPoint);
+ progressClipRect.SetBoxY(midPoint, aRect.YMost());
+ } else {
+ thumbRect.y = aRect.y + (aRect.height - thumbRect.height) * progress;
+ float midPoint = thumbRect.Center().Y();
+ trackClipRect.SetBoxY(midPoint, aRect.YMost());
+ progressClipRect.SetBoxY(aRect.Y(), midPoint);
+ }
+ }
+
+ const CSSCoord borderWidth = 1.0f;
+ const CSSCoord radius = 3.0f;
+
+ auto [progressColor, progressBorderColor] =
+ ComputeRangeProgressColors(aState, aColors);
+ auto [trackColor, trackBorderColor] =
+ ComputeRangeTrackColors(aState, aColors);
+ auto tickMarkColor = trackBorderColor;
+
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, progressClipRect,
+ progressColor, progressBorderColor,
+ borderWidth, radius, aDpiRatio);
+
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, trackClipRect,
+ trackColor, trackBorderColor,
+ borderWidth, radius, aDpiRatio);
+
+ if (!aState.HasState(ElementState::DISABLED)) {
+ // Ensure the shadow doesn't expand outside of our overflow rect declared in
+ // GetWidgetOverflow().
+ auto overflowRect = aRect;
+ overflowRect.Inflate(CSSCoord(6.0f) * aDpiRatio);
+ // Thumb shadow
+ PaintCircleShadow(aPaintData, thumbRect, overflowRect, 0.3f,
+ CSSPoint(0.0f, 2.0f), 2.0f, aDpiRatio);
+ }
+
+ tickMarkDirection.x *= aRect.width - thumbRect.width;
+ tickMarkDirection.y *= aRect.height - thumbRect.height;
+ tickMarkOrigin -=
+ LayoutDevicePoint(tickMarkSize.width, tickMarkSize.height) / 2;
+ auto tickMarkRect = LayoutDeviceRect(tickMarkOrigin, tickMarkSize);
+ for (auto tickMark : tickMarks) {
+ auto tickMarkOffset =
+ tickMarkDirection *
+ float(rangeFrame->GetDoubleAsFractionOfRange(tickMark));
+ ThemeDrawing::FillRect(aPaintData, tickMarkRect + tickMarkOffset,
+ tickMarkColor);
+ }
+
+ // Draw the thumb on top.
+ const CSSCoord thumbBorderWidth = 2.0f;
+ auto [thumbColor, thumbBorderColor] =
+ ComputeRangeThumbColors(aState, aColors);
+
+ PaintStrokedCircle(aPaintData, thumbRect, thumbColor, thumbBorderColor,
+ thumbBorderWidth, aDpiRatio);
+
+ if (aState.HasState(ElementState::FOCUSRING)) {
+ PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio, radius, 1.0f);
+ }
+}
+
+template <typename PaintBackendData>
+void Theme::PaintProgress(nsIFrame* aFrame, PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ const ElementState& aState, const Colors& aColors,
+ DPIRatio aDpiRatio, bool aIsMeter) {
+ const CSSCoord borderWidth = 1.0f;
+ const CSSCoord radius = aIsMeter ? 6.0f : 3.0f;
+
+ LayoutDeviceRect rect(aRect);
+ const LayoutDeviceCoord thickness =
+ (aIsMeter ? kMeterHeight : kProgressbarHeight) * aDpiRatio;
+
+ const bool isHorizontal = !nsNativeTheme::IsVerticalProgress(aFrame);
+ if (isHorizontal) {
+ // Center it vertically.
+ rect.y += (rect.height - thickness) / 2;
+ rect.height = thickness;
+ } else {
+ // Center it horizontally.
+ rect.x += (rect.width - thickness) / 2;
+ rect.width = thickness;
+ }
+
+ {
+ // Paint the track, unclipped.
+ auto [backgroundColor, borderColor] = ComputeProgressTrackColors(aColors);
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, rect,
+ backgroundColor, borderColor,
+ borderWidth, radius, aDpiRatio);
+ }
+
+ // Now paint the chunk, clipped as needed.
+ LayoutDeviceRect clipRect = rect;
+ if (aState.HasState(ElementState::INDETERMINATE)) {
+ // For indeterminate progress, we paint an animated chunk of 1/3 of the
+ // progress size.
+ //
+ // Animation speed and math borrowed from GTK.
+ const LayoutDeviceCoord size = isHorizontal ? rect.width : rect.height;
+ const LayoutDeviceCoord barSize = size * 0.3333f;
+ const LayoutDeviceCoord travel = 2.0f * (size - barSize);
+
+ // Period equals to travel / pixelsPerMillisecond where pixelsPerMillisecond
+ // equals progressSize / 1000.0. This is equivalent to 1600.
+ const unsigned kPeriod = 1600;
+
+ const int t = PR_IntervalToMilliseconds(PR_IntervalNow()) % kPeriod;
+ const LayoutDeviceCoord dx = travel * float(t) / float(kPeriod);
+ if (isHorizontal) {
+ rect.width = barSize;
+ rect.x += (dx < travel * .5f) ? dx : travel - dx;
+ } else {
+ rect.height = barSize;
+ rect.y += (dx < travel * .5f) ? dx : travel - dx;
+ }
+ clipRect = rect;
+ // Queue the next frame if needed.
+ if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 60)) {
+ NS_WARNING("Couldn't refresh indeterminate <progress>");
+ }
+ } else {
+ // This is the progress chunk, clip it to the right amount.
+ double position = [&] {
+ if (aIsMeter) {
+ auto* meter = dom::HTMLMeterElement::FromNode(aFrame->GetContent());
+ if (!meter) {
+ return 0.0;
+ }
+ return meter->Position();
+ }
+ auto* progress = dom::HTMLProgressElement::FromNode(aFrame->GetContent());
+ if (!progress) {
+ return 0.0;
+ }
+ return progress->Position();
+ }();
+ if (isHorizontal) {
+ double clipWidth = rect.width * position;
+ clipRect.width = clipWidth;
+ if (IsFrameRTL(aFrame)) {
+ clipRect.x += rect.width - clipWidth;
+ }
+ } else {
+ double clipHeight = rect.height * position;
+ clipRect.height = clipHeight;
+ clipRect.y += rect.height - clipHeight;
+ }
+ }
+
+ auto [backgroundColor, borderColor] =
+ aIsMeter ? ComputeMeterchunkColors(aState, aColors)
+ : ComputeProgressColors(aColors);
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, clipRect,
+ backgroundColor, borderColor,
+ borderWidth, radius, aDpiRatio);
+}
+
+template <typename PaintBackendData>
+void Theme::PaintButton(nsIFrame* aFrame, PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ StyleAppearance aAppearance, const ElementState& aState,
+ const Colors& aColors, DPIRatio aDpiRatio) {
+ const CSSCoord radius = 4.0f;
+ auto [backgroundColor, borderColor] =
+ ComputeButtonColors(aState, aColors, aFrame);
+
+ if (aAppearance == StyleAppearance::Toolbarbutton &&
+ (!aState.HasState(ElementState::HOVER) ||
+ aState.HasState(ElementState::DISABLED))) {
+ borderColor = sTransparent;
+ }
+
+ ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
+ borderColor, kButtonBorderWidth,
+ radius, aDpiRatio);
+
+ if (aState.HasState(ElementState::FOCUSRING)) {
+ PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
+ radius + kButtonBorderWidth, -kButtonBorderWidth);
+ }
+}
+
+NS_IMETHODIMP
+Theme::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect,
+ const nsRect& /* aDirtyRect */,
+ DrawOverflow aDrawOverflow) {
+ if (!DoDrawWidgetBackground(*aContext->GetDrawTarget(), aFrame, aAppearance,
+ aRect, aDrawOverflow)) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ return NS_OK;
+}
+
+bool Theme::CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) {
+ if (!StaticPrefs::widget_non_native_theme_webrender()) {
+ return false;
+ }
+ WebRenderBackendData data{aBuilder, aResources, aSc, aManager};
+ return DoDrawWidgetBackground(data, aFrame, aAppearance, aRect,
+ DrawOverflow::Yes);
+}
+
+static LayoutDeviceRect ToSnappedRect(const nsRect& aRect,
+ nscoord aTwipsPerPixel, DrawTarget& aDt) {
+ return LayoutDeviceRect::FromUnknownRect(
+ NSRectToSnappedRect(aRect, aTwipsPerPixel, aDt));
+}
+
+static LayoutDeviceRect ToSnappedRect(const nsRect& aRect,
+ nscoord aTwipsPerPixel,
+ WebRenderBackendData& aDt) {
+ // TODO: Do we need to do any more snapping here?
+ return LayoutDeviceRect::FromAppUnits(aRect, aTwipsPerPixel);
+}
+
+static ScrollbarDrawing::ScrollbarKind ComputeScrollbarKind(
+ nsIFrame* aFrame, bool aIsHorizontal) {
+ if (aIsHorizontal) {
+ return ScrollbarDrawing::ScrollbarKind::Horizontal;
+ }
+ nsIFrame* scrollbar = ScrollbarDrawing::GetParentScrollbarFrame(aFrame);
+ if (NS_WARN_IF(!scrollbar)) {
+ return ScrollbarDrawing::ScrollbarKind::VerticalRight;
+ }
+ MOZ_ASSERT(scrollbar->IsScrollbarFrame());
+ nsIScrollbarMediator* sm =
+ static_cast<nsScrollbarFrame*>(scrollbar)->GetScrollbarMediator();
+ if (NS_WARN_IF(!sm)) {
+ return ScrollbarDrawing::ScrollbarKind::VerticalRight;
+ }
+ return sm->IsScrollbarOnRight()
+ ? ScrollbarDrawing::ScrollbarKind::VerticalRight
+ : ScrollbarDrawing::ScrollbarKind::VerticalLeft;
+}
+
+static ScrollbarDrawing::ScrollbarKind ComputeScrollbarKindForScrollCorner(
+ nsIFrame* aFrame) {
+ nsIScrollableFrame* sf = do_QueryFrame(aFrame->GetParent());
+ if (!sf) {
+ return ScrollbarDrawing::ScrollbarKind::VerticalRight;
+ }
+ return sf->IsScrollbarOnRight()
+ ? ScrollbarDrawing::ScrollbarKind::VerticalRight
+ : ScrollbarDrawing::ScrollbarKind::VerticalLeft;
+}
+
+template <typename PaintBackendData>
+bool Theme::DoDrawWidgetBackground(PaintBackendData& aPaintData,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ DrawOverflow aDrawOverflow) {
+ static_assert(std::is_same_v<PaintBackendData, DrawTarget> ||
+ std::is_same_v<PaintBackendData, WebRenderBackendData>);
+
+ const nsPresContext* pc = aFrame->PresContext();
+ const nscoord twipsPerPixel = pc->AppUnitsPerDevPixel();
+ const auto devPxRect = ToSnappedRect(aRect, twipsPerPixel, aPaintData);
+
+ const DocumentState docState = pc->Document()->State();
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ // Paint the outline iff we're asked to draw overflow and we have
+ // outline-style: auto.
+ if (aDrawOverflow == DrawOverflow::Yes &&
+ aFrame->StyleOutline()->mOutlineStyle.IsAuto()) {
+ elementState |= ElementState::FOCUSRING;
+ } else {
+ elementState &= ~ElementState::FOCUSRING;
+ }
+
+ // Hack to avoid skia fuzziness: Add a dummy clip if the widget doesn't
+ // overflow devPxRect.
+ Maybe<AutoClipRect> maybeClipRect;
+ if constexpr (std::is_same_v<PaintBackendData, DrawTarget>) {
+ if (aAppearance != StyleAppearance::FocusOutline &&
+ aAppearance != StyleAppearance::Range &&
+ !elementState.HasState(ElementState::FOCUSRING)) {
+ maybeClipRect.emplace(aPaintData, devPxRect);
+ }
+ }
+
+ const Colors colors(aFrame, aAppearance);
+ DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
+
+ switch (aAppearance) {
+ case StyleAppearance::Radio: {
+ auto rect = CheckBoxRadioRect(devPxRect);
+ PaintRadioControl(aPaintData, rect, elementState, colors, dpiRatio);
+ break;
+ }
+ case StyleAppearance::Checkbox: {
+ if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
+ // TODO: Need to figure out how to best draw this using WR.
+ return false;
+ } else {
+ auto rect = CheckBoxRadioRect(devPxRect);
+ PaintCheckboxControl(aPaintData, rect, elementState, colors, dpiRatio);
+ }
+ break;
+ }
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ PaintTextField(aPaintData, devPxRect, elementState, colors, dpiRatio);
+ break;
+ case StyleAppearance::Listbox:
+ PaintListbox(aPaintData, devPxRect, elementState, colors, dpiRatio);
+ break;
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist:
+ PaintMenulist(aPaintData, devPxRect, elementState, colors, dpiRatio);
+ break;
+ case StyleAppearance::MozMenulistArrowButton:
+ if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
+ // TODO: Need to figure out how to best draw this using WR.
+ return false;
+ } else {
+ PaintMenulistArrow(aFrame, aPaintData, devPxRect);
+ }
+ break;
+ case StyleAppearance::Tooltip: {
+ const CSSCoord strokeWidth(1.0f);
+ const CSSCoord strokeRadius(2.0f);
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aPaintData, devPxRect,
+ colors.System(StyleSystemColor::Infobackground),
+ colors.System(StyleSystemColor::Infotext), strokeWidth, strokeRadius,
+ dpiRatio);
+ break;
+ }
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
+ // TODO: Need to figure out how to best draw this using WR.
+ return false;
+ } else {
+ PaintSpinnerButton(aFrame, aPaintData, devPxRect, elementState,
+ aAppearance, colors, dpiRatio);
+ }
+ break;
+ case StyleAppearance::Range:
+ PaintRange(aFrame, aPaintData, devPxRect, elementState, colors, dpiRatio,
+ IsRangeHorizontal(aFrame));
+ break;
+ case StyleAppearance::RangeThumb:
+ // Painted as part of StyleAppearance::Range.
+ break;
+ case StyleAppearance::ProgressBar:
+ PaintProgress(aFrame, aPaintData, devPxRect, elementState, colors,
+ dpiRatio,
+ /* aIsMeter = */ false);
+ break;
+ case StyleAppearance::Progresschunk:
+ /* Painted as part of the progress bar */
+ break;
+ case StyleAppearance::Meter:
+ PaintProgress(aFrame, aPaintData, devPxRect, elementState, colors,
+ dpiRatio,
+ /* aIsMeter = */ true);
+ break;
+ case StyleAppearance::Meterchunk:
+ /* Painted as part of the meter bar */
+ break;
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ case StyleAppearance::ScrollbarthumbVertical: {
+ bool isHorizontal =
+ aAppearance == StyleAppearance::ScrollbarthumbHorizontal;
+ auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
+ return GetScrollbarDrawing().PaintScrollbarThumb(
+ aPaintData, devPxRect, kind, aFrame,
+ *nsLayoutUtils::StyleForScrollbar(aFrame), elementState, docState,
+ colors, dpiRatio);
+ }
+ case StyleAppearance::ScrollbartrackHorizontal:
+ case StyleAppearance::ScrollbartrackVertical: {
+ bool isHorizontal =
+ aAppearance == StyleAppearance::ScrollbartrackHorizontal;
+ auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
+ return GetScrollbarDrawing().PaintScrollbarTrack(
+ aPaintData, devPxRect, kind, aFrame,
+ *nsLayoutUtils::StyleForScrollbar(aFrame), docState, colors,
+ dpiRatio);
+ }
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbarVertical: {
+ bool isHorizontal = aAppearance == StyleAppearance::ScrollbarHorizontal;
+ auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
+ return GetScrollbarDrawing().PaintScrollbar(
+ aPaintData, devPxRect, kind, aFrame,
+ *nsLayoutUtils::StyleForScrollbar(aFrame), elementState, docState,
+ colors, dpiRatio);
+ }
+ case StyleAppearance::Scrollcorner: {
+ auto kind = ComputeScrollbarKindForScrollCorner(aFrame);
+ return GetScrollbarDrawing().PaintScrollCorner(
+ aPaintData, devPxRect, kind, aFrame,
+ *nsLayoutUtils::StyleForScrollbar(aFrame), docState, colors,
+ dpiRatio);
+ }
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight: {
+ // For scrollbar-width:thin, we don't display the buttons.
+ if (!ScrollbarDrawing::IsScrollbarWidthThin(aFrame)) {
+ if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
+ // TODO: Need to figure out how to best draw this using WR.
+ return false;
+ } else {
+ bool isHorizontal =
+ aAppearance == StyleAppearance::ScrollbarbuttonLeft ||
+ aAppearance == StyleAppearance::ScrollbarbuttonRight;
+ auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
+ GetScrollbarDrawing().PaintScrollbarButton(
+ aPaintData, aAppearance, devPxRect, kind, aFrame,
+ *nsLayoutUtils::StyleForScrollbar(aFrame), elementState, docState,
+ colors, dpiRatio);
+ }
+ }
+ break;
+ }
+ case StyleAppearance::Button:
+ case StyleAppearance::Toolbarbutton:
+ PaintButton(aFrame, aPaintData, devPxRect, aAppearance, elementState,
+ colors, dpiRatio);
+ break;
+ case StyleAppearance::FocusOutline:
+ PaintAutoStyleOutline(aFrame, aPaintData, devPxRect, colors, dpiRatio);
+ break;
+ default:
+ // Various appearance values are used for XUL elements. Normally these
+ // will not be available in content documents (and thus in the content
+ // processes where the native basic theme can be used), but tests are
+ // run with the remote XUL pref enabled and so we can get in here. So
+ // we just return an error rather than assert.
+ return false;
+ }
+
+ return true;
+}
+
+template <typename PaintBackendData>
+void Theme::PaintAutoStyleOutline(nsIFrame* aFrame,
+ PaintBackendData& aPaintData,
+ const LayoutDeviceRect& aRect,
+ const Colors& aColors, DPIRatio aDpiRatio) {
+ const auto& accentColor = aColors.Accent();
+ const bool solid = StaticPrefs::widget_non_native_theme_solid_outline_style();
+ LayoutDeviceCoord strokeWidth(ThemeDrawing::SnapBorderWidth(2.0f, aDpiRatio));
+
+ LayoutDeviceRect rect(aRect);
+ rect.Inflate(strokeWidth);
+
+ const nscoord a2d = aFrame->PresContext()->AppUnitsPerDevPixel();
+ nscoord cssOffset = aFrame->StyleOutline()->mOutlineOffset.ToAppUnits();
+ nscoord cssRadii[8] = {0};
+ if (!aFrame->GetBorderRadii(cssRadii)) {
+ const auto twoPixels = 2 * AppUnitsPerCSSPixel();
+ const nscoord radius =
+ cssOffset >= 0 ? twoPixels : std::max(twoPixels + cssOffset, 0);
+ cssOffset = -twoPixels;
+ for (auto& r : cssRadii) {
+ r = radius;
+ }
+ }
+
+ auto offset = LayoutDevicePixel::FromAppUnits(cssOffset, a2d);
+ RectCornerRadii innerRadii;
+ nsCSSRendering::ComputePixelRadii(cssRadii, a2d, &innerRadii);
+
+ // NOTE(emilio): This doesn't use PaintRoundedRectWithRadius because we need
+ // to support arbitrary radii.
+ auto DrawRect = [&](const sRGBColor& aColor) {
+ RectCornerRadii outerRadii;
+ if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
+ const Float widths[4] = {strokeWidth + offset, strokeWidth + offset,
+ strokeWidth + offset, strokeWidth + offset};
+ nsCSSBorderRenderer::ComputeOuterRadii(innerRadii, widths, &outerRadii);
+ const auto dest = wr::ToLayoutRect(rect);
+ const auto side =
+ wr::ToBorderSide(ToDeviceColor(aColor), StyleBorderStyle::Solid);
+ const wr::BorderSide sides[4] = {side, side, side, side};
+ const bool kBackfaceIsVisible = true;
+ const auto wrWidths = wr::ToBorderWidths(strokeWidth, strokeWidth,
+ strokeWidth, strokeWidth);
+ const auto wrRadius = wr::ToBorderRadius(outerRadii);
+ aPaintData.mBuilder.PushBorder(dest, dest, kBackfaceIsVisible, wrWidths,
+ {sides, 4}, wrRadius);
+ } else {
+ const LayoutDeviceCoord halfWidth = strokeWidth * 0.5f;
+ const Float widths[4] = {halfWidth + offset, halfWidth + offset,
+ halfWidth + offset, halfWidth + offset};
+ nsCSSBorderRenderer::ComputeOuterRadii(innerRadii, widths, &outerRadii);
+ LayoutDeviceRect dest(rect);
+ dest.Deflate(halfWidth);
+ RefPtr<Path> path =
+ MakePathForRoundedRect(aPaintData, dest.ToUnknownRect(), outerRadii);
+ aPaintData.Stroke(path, ColorPattern(ToDeviceColor(aColor)),
+ StrokeOptions(strokeWidth));
+ }
+ };
+
+ auto primaryColor = aColors.HighContrast()
+ ? aColors.System(StyleSystemColor::Selecteditem)
+ : accentColor.Get();
+ DrawRect(primaryColor);
+
+ if (solid) {
+ return;
+ }
+
+ offset += strokeWidth;
+
+ strokeWidth =
+ LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(1.0f, aDpiRatio));
+ rect.Inflate(strokeWidth);
+
+ auto secondaryColor = aColors.HighContrast()
+ ? aColors.System(StyleSystemColor::Canvastext)
+ : accentColor.GetForeground();
+ DrawRect(secondaryColor);
+}
+
+LayoutDeviceIntMargin Theme::GetWidgetBorder(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Button:
+ case StyleAppearance::Toolbarbutton:
+ // Return the border size from the UA sheet, even though what we paint
+ // doesn't actually match that. We know this is the UA sheet border
+ // because we disable native theming when different border widths are
+ // specified by authors, see Theme::IsWidgetStyled.
+ //
+ // The Rounded() bit is technically redundant, but needed to appease the
+ // type system, we should always end up with full device pixels due to
+ // round_border_to_device_pixels at style time.
+ return LayoutDeviceIntMargin::FromAppUnits(
+ aFrame->StyleBorder()->GetComputedBorder(),
+ aFrame->PresContext()->AppUnitsPerDevPixel())
+ .Rounded();
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio: {
+ DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
+ LayoutDeviceIntCoord w =
+ ThemeDrawing::SnapBorderWidth(kCheckboxRadioBorderWidth, dpiRatio);
+ return LayoutDeviceIntMargin(w, w, w, w);
+ }
+ default:
+ return LayoutDeviceIntMargin();
+ }
+}
+
+bool Theme::GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ switch (aAppearance) {
+ // Radios and checkboxes return a fixed size in GetMinimumWidgetSize
+ // and have a meaningful baseline, so they can't have
+ // author-specified padding.
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ aResult->SizeTo(0, 0, 0, 0);
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+bool Theme::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) {
+ CSSIntMargin overflow;
+ switch (aAppearance) {
+ case StyleAppearance::FocusOutline: {
+ // 2px * one segment, or 2px + 1px
+ const auto width =
+ StaticPrefs::widget_non_native_theme_solid_outline_style() ? 2 : 3;
+ overflow.SizeTo(width, width, width, width);
+ break;
+ }
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Range:
+ // 2px for each outline segment, plus 1px separation, plus we paint with a
+ // 1px extra offset, so 6px.
+ overflow.SizeTo(6, 6, 6, 6);
+ break;
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::Button:
+ case StyleAppearance::Toolbarbutton:
+ // 2px for each segment, plus 1px separation, but we paint 1px inside
+ // the border area so 4px overflow.
+ overflow.SizeTo(4, 4, 4, 4);
+ break;
+ default:
+ return false;
+ }
+
+ // TODO: This should convert from device pixels to app units, not from CSS
+ // pixels. And it should take the dpi ratio into account.
+ // Using CSS pixels can cause the overflow to be too small if the page is
+ // zoomed out.
+ aOverflowRect->Inflate(CSSPixel::ToAppUnits(overflow));
+ return true;
+}
+
+LayoutDeviceIntCoord Theme::GetScrollbarSize(const nsPresContext* aPresContext,
+ StyleScrollbarWidth aWidth,
+ Overlay aOverlay) {
+ return GetScrollbarDrawing().GetScrollbarSize(aPresContext, aWidth, aOverlay);
+}
+
+nscoord Theme::GetCheckboxRadioPrefSize() {
+ return CSSPixel::ToAppUnits(kCheckboxRadioContentBoxSize);
+}
+
+/* static */
+UniquePtr<ScrollbarDrawing> Theme::ScrollbarStyle() {
+ switch (StaticPrefs::widget_non_native_theme_scrollbar_style()) {
+ case 1:
+ return MakeUnique<ScrollbarDrawingCocoa>();
+ case 2:
+ return MakeUnique<ScrollbarDrawingGTK>();
+ case 3:
+ return MakeUnique<ScrollbarDrawingAndroid>();
+ case 4:
+ return MakeUnique<ScrollbarDrawingWin>();
+ case 5:
+ return MakeUnique<ScrollbarDrawingWin11>();
+ default:
+ break;
+ }
+ // Default to native scrollbar style for each platform.
+#ifdef XP_WIN
+ if (IsWin11OrLater()) {
+ return MakeUnique<ScrollbarDrawingWin11>();
+ }
+ return MakeUnique<ScrollbarDrawingWin>();
+#elif MOZ_WIDGET_COCOA
+ return MakeUnique<ScrollbarDrawingCocoa>();
+#elif MOZ_WIDGET_GTK
+ return MakeUnique<ScrollbarDrawingGTK>();
+#elif ANDROID
+ return MakeUnique<ScrollbarDrawingAndroid>();
+#else
+# error "Unknown platform, need scrollbar implementation."
+#endif
+}
+
+LayoutDeviceIntSize Theme::GetMinimumWidgetSize(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
+
+ if (IsWidgetScrollbarPart(aAppearance)) {
+ return GetScrollbarDrawing().GetMinimumWidgetSize(aPresContext, aAppearance,
+ aFrame);
+ }
+
+ LayoutDeviceIntSize result;
+ switch (aAppearance) {
+ case StyleAppearance::RangeThumb:
+ result.SizeTo((kMinimumRangeThumbSize * dpiRatio).Rounded(),
+ (kMinimumRangeThumbSize * dpiRatio).Rounded());
+ break;
+ case StyleAppearance::MozMenulistArrowButton:
+ result.width = (kMinimumDropdownArrowButtonWidth * dpiRatio).Rounded();
+ break;
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ result.width = (kMinimumSpinnerButtonWidth * dpiRatio).Rounded();
+ result.height = (kMinimumSpinnerButtonHeight * dpiRatio).Rounded();
+ break;
+ default:
+ break;
+ }
+ return result;
+}
+
+nsITheme::Transparency Theme::GetWidgetTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (auto scrollbar = GetScrollbarDrawing().GetScrollbarPartTransparency(
+ aFrame, aAppearance)) {
+ return *scrollbar;
+ }
+ if (aAppearance == StyleAppearance::Tooltip) {
+ // We draw a rounded rect, so we need transparency.
+ return eTransparent;
+ }
+ return eUnknownTransparency;
+}
+
+NS_IMETHODIMP
+Theme::WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) {
+ if (!aAttribute) {
+ // Hover/focus/active changed. Always repaint.
+ *aShouldRepaint = true;
+ } else {
+ // Check the attribute to see if it's relevant.
+ // disabled, checked, dlgtype, default, etc.
+ *aShouldRepaint = false;
+ if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
+ aAttribute == nsGkAtoms::selected ||
+ aAttribute == nsGkAtoms::visuallyselected ||
+ aAttribute == nsGkAtoms::menuactive ||
+ aAttribute == nsGkAtoms::sortDirection ||
+ aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::_default ||
+ aAttribute == nsGkAtoms::open || aAttribute == nsGkAtoms::hover) {
+ *aShouldRepaint = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Theme::ThemeChanged() { return NS_OK; }
+
+bool Theme::WidgetAppearanceDependsOnWindowFocus(StyleAppearance aAppearance) {
+ return IsWidgetScrollbarPart(aAppearance);
+}
+
+nsITheme::ThemeGeometryType Theme::ThemeGeometryTypeForWidget(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ return eThemeGeometryTypeUnknown;
+}
+
+bool Theme::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::FocusOutline:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Range:
+ case StyleAppearance::RangeThumb:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Meterchunk:
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ case StyleAppearance::ScrollbarthumbVertical:
+ case StyleAppearance::ScrollbartrackHorizontal:
+ case StyleAppearance::ScrollbartrackVertical:
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::Scrollcorner:
+ case StyleAppearance::Button:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::Tooltip:
+ return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+ default:
+ return false;
+ }
+}
+
+bool Theme::WidgetIsContainer(StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ return false;
+ default:
+ return true;
+ }
+}
+
+bool Theme::ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) {
+ return true;
+}
+
+bool Theme::ThemeNeedsComboboxDropmarker() { return true; }
+
+bool Theme::ThemeSupportsScrollbarButtons() {
+ return GetScrollbarDrawing().ShouldDrawScrollbarButtons();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/Theme.h b/widget/Theme.h
new file mode 100644
index 0000000000..c09c414056
--- /dev/null
+++ b/widget/Theme.h
@@ -0,0 +1,208 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_Theme_h
+#define mozilla_widget_Theme_h
+
+#include "Units.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/gfx/Types.h"
+#include "nsITheme.h"
+#include "nsNativeTheme.h"
+#include "ScrollbarDrawing.h"
+
+namespace mozilla {
+
+enum class StyleSystemColor : uint8_t;
+
+namespace widget {
+
+class Theme : protected nsNativeTheme, public nsITheme {
+ protected:
+ using sRGBColor = gfx::sRGBColor;
+ using DrawTarget = gfx::DrawTarget;
+ using Path = gfx::Path;
+ using Rect = gfx::Rect;
+ using Point = gfx::Point;
+ using RectCornerRadii = gfx::RectCornerRadii;
+ using Colors = ThemeColors;
+ using AccentColor = ThemeAccentColor;
+ using ElementState = dom::ElementState;
+
+ public:
+ explicit Theme(UniquePtr<ScrollbarDrawing>&& aScrollbarDrawing)
+ : mScrollbarDrawing(std::move(aScrollbarDrawing)) {
+ mScrollbarDrawing->RecomputeScrollbarParams();
+ }
+
+ static void Init();
+ static void Shutdown();
+ static void LookAndFeelChanged();
+
+ using DPIRatio = CSSToLayoutDeviceScale;
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // The nsITheme interface.
+ NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame*,
+ StyleAppearance, const nsRect& aRect,
+ const nsRect& aDirtyRect,
+ DrawOverflow) override;
+
+ bool CreateWebRenderCommandsForWidget(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ const layers::StackingContextHelper& aSc,
+ layers::RenderRootStateManager* aManager, nsIFrame*, StyleAppearance,
+ const nsRect& aRect) override;
+
+ // PaintBackendData will be either a DrawTarget, or a WebRenderBackendData.
+ //
+ // The return value represents whether the widget could be painted with the
+ // given back-end.
+ template <typename PaintBackendData>
+ bool DoDrawWidgetBackground(PaintBackendData&, nsIFrame*, StyleAppearance,
+ const nsRect&, DrawOverflow);
+
+ [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder(nsDeviceContext* aContext,
+ nsIFrame*,
+ StyleAppearance) override;
+ bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame*, StyleAppearance,
+ LayoutDeviceIntMargin* aResult) override;
+ bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame*, StyleAppearance,
+ nsRect* aOverflowRect) override;
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*, nsIFrame*,
+ StyleAppearance) override;
+ Transparency GetWidgetTransparency(nsIFrame*, StyleAppearance) override;
+ NS_IMETHOD WidgetStateChanged(nsIFrame*, StyleAppearance, nsAtom* aAttribute,
+ bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) override;
+ NS_IMETHOD ThemeChanged() override;
+ bool WidgetAppearanceDependsOnWindowFocus(StyleAppearance) override;
+ /*bool NeedToClearBackgroundBehindWidget(
+ nsIFrame*, StyleAppearance) override;*/
+ ThemeGeometryType ThemeGeometryTypeForWidget(nsIFrame*,
+ StyleAppearance) override;
+ bool ThemeSupportsWidget(nsPresContext*, nsIFrame*, StyleAppearance) override;
+ bool WidgetIsContainer(StyleAppearance) override;
+ bool ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) override;
+ bool ThemeNeedsComboboxDropmarker() override;
+
+ LayoutDeviceIntCoord GetScrollbarSize(const nsPresContext*,
+ StyleScrollbarWidth, Overlay) final;
+
+ nscoord GetCheckboxRadioPrefSize() override;
+
+ static UniquePtr<ScrollbarDrawing> ScrollbarStyle();
+
+ protected:
+ virtual ~Theme() = default;
+
+ DPIRatio GetDPIRatio(nsPresContext*, StyleAppearance);
+ DPIRatio GetDPIRatio(nsIFrame*, StyleAppearance);
+
+ std::tuple<sRGBColor, sRGBColor, sRGBColor> ComputeCheckboxColors(
+ const ElementState&, StyleAppearance, const Colors&);
+ enum class OutlineCoversBorder : bool { No, Yes };
+ sRGBColor ComputeBorderColor(const ElementState&, const Colors&,
+ OutlineCoversBorder);
+
+ std::pair<sRGBColor, sRGBColor> ComputeButtonColors(const ElementState&,
+ const Colors&,
+ nsIFrame* = nullptr);
+ std::pair<sRGBColor, sRGBColor> ComputeTextfieldColors(const ElementState&,
+ const Colors&,
+ OutlineCoversBorder);
+ std::pair<sRGBColor, sRGBColor> ComputeRangeProgressColors(
+ const ElementState&, const Colors&);
+ std::pair<sRGBColor, sRGBColor> ComputeRangeTrackColors(const ElementState&,
+ const Colors&);
+ std::pair<sRGBColor, sRGBColor> ComputeRangeThumbColors(const ElementState&,
+ const Colors&);
+ std::pair<sRGBColor, sRGBColor> ComputeProgressColors(const Colors&);
+ std::pair<sRGBColor, sRGBColor> ComputeProgressTrackColors(const Colors&);
+ std::pair<sRGBColor, sRGBColor> ComputeMeterchunkColors(
+ const ElementState& aMeterState, const Colors&);
+ std::array<sRGBColor, 3> ComputeFocusRectColors(const Colors&);
+
+ template <typename PaintBackendData>
+ void PaintRoundedFocusRect(PaintBackendData&, const LayoutDeviceRect&,
+ const Colors&, DPIRatio, CSSCoord aRadius,
+ CSSCoord aOffset);
+ template <typename PaintBackendData>
+ void PaintAutoStyleOutline(nsIFrame*, PaintBackendData&,
+ const LayoutDeviceRect&, const Colors&, DPIRatio);
+
+ void PaintCheckboxControl(DrawTarget& aDrawTarget, const LayoutDeviceRect&,
+ const ElementState&, const Colors&, DPIRatio);
+ void PaintCheckMark(DrawTarget&, const LayoutDeviceRect&, const sRGBColor&);
+ void PaintIndeterminateMark(DrawTarget&, const LayoutDeviceRect&,
+ const sRGBColor&);
+
+ template <typename PaintBackendData>
+ void PaintStrokedCircle(PaintBackendData&, const LayoutDeviceRect&,
+ const sRGBColor& aBackgroundColor,
+ const sRGBColor& aBorderColor,
+ const CSSCoord aBorderWidth, DPIRatio);
+ void PaintCircleShadow(DrawTarget&, const LayoutDeviceRect& aBoxRect,
+ const LayoutDeviceRect& aClipRect, float aShadowAlpha,
+ const CSSPoint& aShadowOffset,
+ CSSCoord aShadowBlurStdDev, DPIRatio);
+ void PaintCircleShadow(WebRenderBackendData&,
+ const LayoutDeviceRect& aBoxRect,
+ const LayoutDeviceRect& aClipRect, float aShadowAlpha,
+ const CSSPoint& aShadowOffset,
+ CSSCoord aShadowBlurStdDev, DPIRatio);
+ template <typename PaintBackendData>
+ void PaintRadioControl(PaintBackendData&, const LayoutDeviceRect&,
+ const ElementState&, const Colors&, DPIRatio);
+ template <typename PaintBackendData>
+ void PaintRadioCheckmark(PaintBackendData&, const LayoutDeviceRect&,
+ const ElementState&, DPIRatio);
+ template <typename PaintBackendData>
+ void PaintTextField(PaintBackendData&, const LayoutDeviceRect&,
+ const ElementState&, const Colors&, DPIRatio);
+ template <typename PaintBackendData>
+ void PaintListbox(PaintBackendData&, const LayoutDeviceRect&,
+ const ElementState&, const Colors&, DPIRatio);
+ template <typename PaintBackendData>
+ void PaintMenulist(PaintBackendData&, const LayoutDeviceRect&,
+ const ElementState&, const Colors&, DPIRatio);
+ void PaintMenulistArrow(nsIFrame*, DrawTarget&, const LayoutDeviceRect&);
+ void PaintSpinnerButton(nsIFrame*, DrawTarget&, const LayoutDeviceRect&,
+ const ElementState&, StyleAppearance, const Colors&,
+ DPIRatio);
+ template <typename PaintBackendData>
+ void PaintRange(nsIFrame*, PaintBackendData&, const LayoutDeviceRect&,
+ const ElementState&, const Colors&, DPIRatio,
+ bool aHorizontal);
+ template <typename PaintBackendData>
+ void PaintProgress(nsIFrame*, PaintBackendData&, const LayoutDeviceRect&,
+ const ElementState&, const Colors&, DPIRatio,
+ bool aIsMeter);
+ template <typename PaintBackendData>
+ void PaintButton(nsIFrame*, PaintBackendData&, const LayoutDeviceRect&,
+ StyleAppearance, const ElementState&, const Colors&,
+ DPIRatio);
+
+ static void PrefChangedCallback(const char*, void*) {
+ LookAndFeel::NotifyChangedAllWindows(ThemeChangeKind::Layout);
+ }
+
+ void SetScrollbarDrawing(UniquePtr<ScrollbarDrawing>&& aScrollbarDrawing) {
+ mScrollbarDrawing = std::move(aScrollbarDrawing);
+ mScrollbarDrawing->RecomputeScrollbarParams();
+ }
+ ScrollbarDrawing& GetScrollbarDrawing() const { return *mScrollbarDrawing; }
+ UniquePtr<ScrollbarDrawing> mScrollbarDrawing;
+
+ bool ThemeSupportsScrollbarButtons() override;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/ThemeChangeKind.h b/widget/ThemeChangeKind.h
new file mode 100644
index 0000000000..2300ea648b
--- /dev/null
+++ b/widget/ThemeChangeKind.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_ThemeChangeKind
+#define mozilla_widget_ThemeChangeKind
+
+#include "mozilla/TypedEnumBits.h"
+
+namespace mozilla::widget {
+
+enum class ThemeChangeKind : uint8_t {
+ // This is the cheapest change, no need to forcibly recompute style and/or
+ // layout.
+ MediaQueriesOnly = 0,
+ // Style needs to forcibly be recomputed because some of the stuff that may
+ // have changed, like system colors, are reflected in the computed style but
+ // not in the specified style.
+ Style = 1 << 0,
+ // Layout needs to forcibly be recomputed because some of the stuff that may
+ // have changed is layout-dependent, like system font.
+ Layout = 1 << 1,
+ // The union of the two flags above.
+ StyleAndLayout = Style | Layout,
+ // For IPC serialization purposes.
+ AllBits = Style | Layout,
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(ThemeChangeKind)
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ThemeCocoa.cpp b/widget/ThemeCocoa.cpp
new file mode 100644
index 0000000000..733c215991
--- /dev/null
+++ b/widget/ThemeCocoa.cpp
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 40; 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 "ThemeCocoa.h"
+
+#include "cocoa/MacThemeGeometryType.h"
+#include "gfxPlatform.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/ServoStyleConsts.h"
+
+namespace mozilla::widget {
+
+LayoutDeviceIntSize ThemeCocoa::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (aAppearance == StyleAppearance::MozMenulistArrowButton) {
+ auto size =
+ GetScrollbarSize(aPresContext, StyleScrollbarWidth::Auto, Overlay::No);
+ return {size, size};
+ }
+ return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance);
+}
+
+NS_IMETHODIMP
+ThemeCocoa::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect, const nsRect& aDirtyRect,
+ DrawOverflow aDrawOverflow) {
+ switch (aAppearance) {
+ case StyleAppearance::Tooltip:
+ // Cocoa tooltip background and border are already drawn by the
+ // OS window server.
+ return NS_OK;
+ default:
+ break;
+ }
+ return Theme::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect,
+ aDirtyRect, aDrawOverflow);
+}
+
+bool ThemeCocoa::CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) {
+ switch (aAppearance) {
+ case StyleAppearance::Tooltip:
+ // Cocoa tooltip background and border are already drawn by the
+ // OS window server.
+ return true;
+ default:
+ break;
+ }
+ return Theme::CreateWebRenderCommandsForWidget(
+ aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect);
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ThemeCocoa.h b/widget/ThemeCocoa.h
new file mode 100644
index 0000000000..f846766f37
--- /dev/null
+++ b/widget/ThemeCocoa.h
@@ -0,0 +1,42 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_ThemeCocoa_h
+#define mozilla_widget_ThemeCocoa_h
+
+#include "Theme.h"
+
+#include "ScrollbarDrawingCocoa.h"
+
+namespace mozilla::widget {
+
+class ThemeCocoa : public Theme {
+ public:
+ explicit ThemeCocoa(UniquePtr<ScrollbarDrawing>&& aScrollbarDrawing)
+ : Theme(std::move(aScrollbarDrawing)) {}
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame*,
+ StyleAppearance, const nsRect& aRect,
+ const nsRect& aDirtyRect,
+ DrawOverflow) override;
+
+ bool CreateWebRenderCommandsForWidget(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ const layers::StackingContextHelper& aSc,
+ layers::RenderRootStateManager* aManager, nsIFrame*, StyleAppearance,
+ const nsRect& aRect) override;
+
+ protected:
+ virtual ~ThemeCocoa() = default;
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ThemeColors.cpp b/widget/ThemeColors.cpp
new file mode 100644
index 0000000000..ebbfcd88b2
--- /dev/null
+++ b/widget/ThemeColors.cpp
@@ -0,0 +1,272 @@
+/* -*- Mode: C++; tab-width: 40; 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 "ThemeColors.h"
+
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "ThemeDrawing.h"
+#include "nsNativeTheme.h"
+
+using namespace mozilla::gfx;
+
+namespace mozilla::widget {
+
+struct ColorPalette {
+ ColorPalette(nscolor aAccent, nscolor aForeground);
+
+ constexpr ColorPalette(sRGBColor aAccent, sRGBColor aForeground,
+ sRGBColor aLight, sRGBColor aDark, sRGBColor aDarker)
+ : mAccent(aAccent),
+ mForeground(aForeground),
+ mAccentLight(aLight),
+ mAccentDark(aDark),
+ mAccentDarker(aDarker) {}
+
+ constexpr static ColorPalette Default() {
+ return ColorPalette(
+ sDefaultAccent, sDefaultAccentText,
+ sRGBColor::UnusualFromARGB(0x4d008deb), // Luminance: 25.04791%
+ sRGBColor::UnusualFromARGB(0xff0250bb), // Luminance: 9.33808%
+ sRGBColor::UnusualFromARGB(0xff054096) // Luminance: 5.90106%
+ );
+ }
+
+ // Ensure accent color is opaque by blending with white. This serves two
+ // purposes: On one hand, it avoids surprises if we overdraw. On the other, it
+ // makes our math below make more sense, as we want to match the browser
+ // style, which has an opaque accent color.
+ static nscolor EnsureOpaque(nscolor aAccent) {
+ if (NS_GET_A(aAccent) != 0xff) {
+ return NS_ComposeColors(NS_RGB(0xff, 0xff, 0xff), aAccent);
+ }
+ return aAccent;
+ }
+
+ static nscolor GetLight(nscolor aAccent) {
+ // The luminance from the light color divided by the one of the accent color
+ // in the default palette.
+ constexpr float kLightLuminanceScale = 25.048f / 13.693f;
+ const float lightLuminanceAdjust = ThemeColors::ScaleLuminanceBy(
+ RelativeLuminanceUtils::Compute(aAccent), kLightLuminanceScale);
+ nscolor lightColor =
+ RelativeLuminanceUtils::Adjust(aAccent, lightLuminanceAdjust);
+ return NS_RGBA(NS_GET_R(lightColor), NS_GET_G(lightColor),
+ NS_GET_B(lightColor), 0x4d);
+ }
+
+ static nscolor GetDark(nscolor aAccent) {
+ // Same deal as above (but without the alpha).
+ constexpr float kDarkLuminanceScale = 9.338f / 13.693f;
+ const float darkLuminanceAdjust = ThemeColors::ScaleLuminanceBy(
+ RelativeLuminanceUtils::Compute(aAccent), kDarkLuminanceScale);
+ return RelativeLuminanceUtils::Adjust(aAccent, darkLuminanceAdjust);
+ }
+
+ static nscolor GetDarker(nscolor aAccent) {
+ // Same deal as above.
+ constexpr float kDarkerLuminanceScale = 5.901f / 13.693f;
+ const float darkerLuminanceAdjust = ThemeColors::ScaleLuminanceBy(
+ RelativeLuminanceUtils::Compute(aAccent), kDarkerLuminanceScale);
+ return RelativeLuminanceUtils::Adjust(aAccent, darkerLuminanceAdjust);
+ }
+
+ sRGBColor mAccent;
+ sRGBColor mForeground;
+
+ // Note that depending on the exact accent color, lighter/darker might really
+ // be inverted.
+ sRGBColor mAccentLight;
+ sRGBColor mAccentDark;
+ sRGBColor mAccentDarker;
+};
+
+static nscolor GetAccentColor(bool aBackground, ColorScheme aScheme) {
+ auto useStandins = LookAndFeel::UseStandins(
+ !StaticPrefs::widget_non_native_theme_use_theme_accent());
+ return ColorPalette::EnsureOpaque(
+ LookAndFeel::Color(aBackground ? LookAndFeel::ColorID::Accentcolor
+ : LookAndFeel::ColorID::Accentcolortext,
+ aScheme, useStandins));
+}
+
+static ColorPalette sDefaultLightPalette = ColorPalette::Default();
+static ColorPalette sDefaultDarkPalette = ColorPalette::Default();
+
+ColorPalette::ColorPalette(nscolor aAccent, nscolor aForeground) {
+ mAccent = sRGBColor::FromABGR(aAccent);
+ mForeground = sRGBColor::FromABGR(aForeground);
+ mAccentLight = sRGBColor::FromABGR(GetLight(aAccent));
+ mAccentDark = sRGBColor::FromABGR(GetDark(aAccent));
+ mAccentDarker = sRGBColor::FromABGR(GetDarker(aAccent));
+}
+
+ThemeAccentColor::ThemeAccentColor(const ComputedStyle& aStyle,
+ ColorScheme aScheme)
+ : mDefaultPalette(aScheme == ColorScheme::Light ? &sDefaultLightPalette
+ : &sDefaultDarkPalette) {
+ const auto& color = aStyle.StyleUI()->mAccentColor;
+ if (color.IsAuto()) {
+ return;
+ }
+ MOZ_ASSERT(color.IsColor());
+ nscolor accentColor =
+ ColorPalette::EnsureOpaque(color.AsColor().CalcColor(aStyle));
+ if (sRGBColor::FromABGR(accentColor) == mDefaultPalette->mAccent) {
+ return;
+ }
+ mAccentColor.emplace(accentColor);
+}
+
+sRGBColor ThemeAccentColor::Get() const {
+ if (!mAccentColor) {
+ return mDefaultPalette->mAccent;
+ }
+ return sRGBColor::FromABGR(*mAccentColor);
+}
+
+sRGBColor ThemeAccentColor::GetForeground() const {
+ if (!mAccentColor) {
+ return mDefaultPalette->mForeground;
+ }
+ return sRGBColor::FromABGR(
+ ThemeColors::ComputeCustomAccentForeground(*mAccentColor));
+}
+
+sRGBColor ThemeAccentColor::GetLight() const {
+ if (!mAccentColor) {
+ return mDefaultPalette->mAccentLight;
+ }
+ return sRGBColor::FromABGR(ColorPalette::GetLight(*mAccentColor));
+}
+
+sRGBColor ThemeAccentColor::GetDark() const {
+ if (!mAccentColor) {
+ return mDefaultPalette->mAccentDark;
+ }
+ return sRGBColor::FromABGR(ColorPalette::GetDark(*mAccentColor));
+}
+
+sRGBColor ThemeAccentColor::GetDarker() const {
+ if (!mAccentColor) {
+ return mDefaultPalette->mAccentDarker;
+ }
+ return sRGBColor::FromABGR(ColorPalette::GetDarker(*mAccentColor));
+}
+
+auto ThemeColors::ShouldBeHighContrast(const nsPresContext& aPc)
+ -> HighContrastInfo {
+ // We make sure that we're drawing backgrounds, since otherwise layout will
+ // darken our used text colors etc anyways, and that can cause contrast issues
+ // with dark high-contrast themes.
+ if (!aPc.GetBackgroundColorDraw()) {
+ return {};
+ }
+ const auto& prefs = PreferenceSheet::PrefsFor(*aPc.Document());
+ return {prefs.NonNativeThemeShouldBeHighContrast(),
+ prefs.mMustUseLightSystemColors};
+}
+
+ColorScheme ThemeColors::ColorSchemeForWidget(const nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const HighContrastInfo& aInfo) {
+ if (aInfo.mMustUseLightSystemColors) {
+ return ColorScheme::Light;
+ }
+ if (!nsNativeTheme::IsWidgetScrollbarPart(aAppearance)) {
+ return LookAndFeel::ColorSchemeForFrame(aFrame);
+ }
+ // Scrollbars are a bit tricky. Their used color-scheme depends on whether the
+ // background they are on is light or dark.
+ //
+ // TODO(emilio): This heuristic effectively predates the color-scheme CSS
+ // property. Perhaps we should check whether the style or the document set
+ // `color-scheme` to something that isn't `normal`, and if so go through the
+ // code-path above.
+ if (StaticPrefs::widget_disable_dark_scrollbar()) {
+ return ColorScheme::Light;
+ }
+ return nsNativeTheme::IsDarkBackgroundForScrollbar(
+ const_cast<nsIFrame*>(aFrame))
+ ? ColorScheme::Dark
+ : ColorScheme::Light;
+}
+
+/*static*/
+void ThemeColors::RecomputeAccentColors() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ sDefaultLightPalette =
+ ColorPalette(GetAccentColor(true, ColorScheme::Light),
+ GetAccentColor(false, ColorScheme::Light));
+
+ sDefaultDarkPalette = ColorPalette(GetAccentColor(true, ColorScheme::Dark),
+ GetAccentColor(false, ColorScheme::Dark));
+}
+
+/*static*/
+nscolor ThemeColors::ComputeCustomAccentForeground(nscolor aColor) {
+ // Contrast ratio is defined in
+ // https://www.w3.org/TR/WCAG20/#contrast-ratiodef as:
+ //
+ // (L1 + 0.05) / (L2 + 0.05)
+ //
+ // Where L1 is the lighter color, and L2 is the darker one. So we determine
+ // whether we're dark or light and resolve the equation for the target ratio.
+ //
+ // So when lightening:
+ //
+ // L1 = k * (L2 + 0.05) - 0.05
+ //
+ // And when darkening:
+ //
+ // L2 = (L1 + 0.05) / k - 0.05
+ //
+ const float luminance = RelativeLuminanceUtils::Compute(aColor);
+
+ // We generally prefer white unless we can't because the color is really light
+ // and we can't provide reasonable contrast.
+ const float ratioWithWhite = 1.05f / (luminance + 0.05f);
+ const bool canBeWhite =
+ ratioWithWhite >=
+ StaticPrefs::layout_css_accent_color_min_contrast_ratio();
+ if (canBeWhite) {
+ return NS_RGB(0xff, 0xff, 0xff);
+ }
+ const float targetRatio =
+ StaticPrefs::layout_css_accent_color_darkening_target_contrast_ratio();
+ const float targetLuminance = (luminance + 0.05f) / targetRatio - 0.05f;
+ return RelativeLuminanceUtils::Adjust(aColor, targetLuminance);
+}
+
+nscolor ThemeColors::AdjustUnthemedScrollbarThumbColor(
+ nscolor aFaceColor, dom::ElementState aStates) {
+ // In Windows 10, scrollbar thumb has the following colors:
+ //
+ // State | Color | Luminance
+ // -------+----------+----------
+ // Normal | Gray 205 | 61.0%
+ // Hover | Gray 166 | 38.1%
+ // Active | Gray 96 | 11.7%
+ //
+ // This function is written based on the ratios between the values.
+ bool isActive = aStates.HasState(dom::ElementState::ACTIVE);
+ bool isHover = aStates.HasState(dom::ElementState::HOVER);
+ if (!isActive && !isHover) {
+ return aFaceColor;
+ }
+ float luminance = RelativeLuminanceUtils::Compute(aFaceColor);
+ if (isActive) {
+ // 11.7 / 61.0
+ luminance = ScaleLuminanceBy(luminance, 0.192f);
+ } else {
+ // 38.1 / 61.0
+ luminance = ScaleLuminanceBy(luminance, 0.625f);
+ }
+ return RelativeLuminanceUtils::Adjust(aFaceColor, luminance);
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ThemeColors.h b/widget/ThemeColors.h
new file mode 100644
index 0000000000..739a7ca0f0
--- /dev/null
+++ b/widget/ThemeColors.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 40; 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/. */
+
+#ifndef mozilla_widget_ThemeColors_h
+#define mozilla_widget_ThemeColors_h
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/LookAndFeel.h"
+#include "nsIFrame.h"
+
+namespace mozilla::widget {
+
+static constexpr gfx::sRGBColor sDefaultAccent(
+ gfx::sRGBColor::UnusualFromARGB(0xff0060df)); // Luminance: 13.69346%
+static constexpr gfx::sRGBColor sDefaultAccentText(
+ gfx::sRGBColor::OpaqueWhite());
+
+struct ColorPalette;
+
+class ThemeAccentColor {
+ protected:
+ using sRGBColor = mozilla::gfx::sRGBColor;
+ using ComputedStyle = mozilla::ComputedStyle;
+
+ Maybe<nscolor> mAccentColor;
+ const ColorPalette* mDefaultPalette = nullptr;
+
+ public:
+ explicit ThemeAccentColor(const ComputedStyle&, ColorScheme);
+ virtual ~ThemeAccentColor() = default;
+
+ sRGBColor Get() const;
+ sRGBColor GetForeground() const;
+ sRGBColor GetLight() const;
+ sRGBColor GetDark() const;
+ sRGBColor GetDarker() const;
+};
+
+// Widget color information associated to a particular frame.
+class ThemeColors {
+ protected:
+ using Document = mozilla::dom::Document;
+ using sRGBColor = mozilla::gfx::sRGBColor;
+ using LookAndFeel = mozilla::LookAndFeel;
+ using StyleSystemColor = mozilla::StyleSystemColor;
+ using AccentColor = ThemeAccentColor;
+
+ struct HighContrastInfo {
+ bool mHighContrast = false;
+ bool mMustUseLightSystemColors = false;
+ };
+
+ const Document& mDoc;
+ const HighContrastInfo mHighContrastInfo;
+ const ColorScheme mColorScheme;
+ const AccentColor mAccentColor;
+
+ public:
+ explicit ThemeColors(const nsIFrame* aFrame, StyleAppearance aAppearance)
+ : mDoc(*aFrame->PresContext()->Document()),
+ mHighContrastInfo(ShouldBeHighContrast(*aFrame->PresContext())),
+ mColorScheme(ColorSchemeForWidget(aFrame, aAppearance, mHighContrastInfo)),
+ mAccentColor(*aFrame->Style(), mColorScheme) {}
+ virtual ~ThemeColors() = default;
+
+ [[nodiscard]] static float ScaleLuminanceBy(float aLuminance, float aFactor) {
+ return aLuminance >= 0.18f ? aLuminance * aFactor : aLuminance / aFactor;
+ }
+
+ const AccentColor& Accent() const { return mAccentColor; }
+ bool HighContrast() const { return mHighContrastInfo.mHighContrast; }
+ bool IsDark() const { return mColorScheme == ColorScheme::Dark; }
+
+ nscolor SystemNs(StyleSystemColor aColor) const {
+ return LookAndFeel::Color(aColor, mColorScheme,
+ LookAndFeel::ShouldUseStandins(mDoc, aColor));
+ }
+
+ sRGBColor System(StyleSystemColor aColor) const {
+ return sRGBColor::FromABGR(SystemNs(aColor));
+ }
+
+ template <typename Compute>
+ sRGBColor SystemOrElse(StyleSystemColor aColor, Compute aCompute) const {
+ if (auto color = LookAndFeel::GetColor(
+ aColor, mColorScheme,
+ LookAndFeel::ShouldUseStandins(mDoc, aColor))) {
+ return sRGBColor::FromABGR(*color);
+ }
+ return aCompute();
+ }
+
+ std::pair<sRGBColor, sRGBColor> SystemPair(StyleSystemColor aFirst,
+ StyleSystemColor aSecond) const {
+ return std::make_pair(System(aFirst), System(aSecond));
+ }
+
+ // Whether we should use system colors (for high contrast mode).
+ static HighContrastInfo ShouldBeHighContrast(const nsPresContext&);
+ static ColorScheme ColorSchemeForWidget(const nsIFrame*, StyleAppearance,
+ const HighContrastInfo&);
+
+ static void RecomputeAccentColors();
+
+ static nscolor ComputeCustomAccentForeground(nscolor aColor);
+
+ static nscolor AdjustUnthemedScrollbarThumbColor(nscolor aFaceColor,
+ dom::ElementState aStates);
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/ThemeDrawing.cpp b/widget/ThemeDrawing.cpp
new file mode 100644
index 0000000000..93e317a41b
--- /dev/null
+++ b/widget/ThemeDrawing.cpp
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ThemeDrawing.h"
+
+namespace mozilla::widget {
+
+/*static*/
+void ThemeDrawing::FillRect(DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ const sRGBColor& aColor) {
+ aDt.FillRect(aRect.ToUnknownRect(), gfx::ColorPattern(ToDeviceColor(aColor)));
+}
+
+/*static*/
+void ThemeDrawing::FillRect(WebRenderBackendData& aWrData,
+ const LayoutDeviceRect& aRect,
+ const sRGBColor& aColor) {
+ const bool kBackfaceIsVisible = true;
+ auto dest = wr::ToLayoutRect(aRect);
+ aWrData.mBuilder.PushRect(dest, dest, kBackfaceIsVisible, false, false,
+ wr::ToColorF(ToDeviceColor(aColor)));
+}
+
+/*static*/
+LayoutDeviceIntCoord ThemeDrawing::SnapBorderWidth(const CSSCoord& aCssWidth,
+ const DPIRatio& aDpiRatio) {
+ if (aCssWidth == 0.0f) {
+ return 0;
+ }
+ return std::max(LayoutDeviceIntCoord(1), (aCssWidth * aDpiRatio).Truncated());
+}
+
+/*static*/
+void ThemeDrawing::PaintArrow(DrawTarget& aDrawTarget,
+ const LayoutDeviceRect& aRect,
+ const float aArrowPolygonX[],
+ const float aArrowPolygonY[],
+ const float aArrowPolygonSize,
+ const int32_t aArrowNumPoints,
+ const sRGBColor aFillColor) {
+ const float scale = ScaleToFillRect(aRect, aArrowPolygonSize);
+
+ auto center = aRect.Center().ToUnknownPoint();
+
+ RefPtr<gfx::PathBuilder> builder = aDrawTarget.CreatePathBuilder();
+ gfx::Point p =
+ center + gfx::Point(aArrowPolygonX[0] * scale, aArrowPolygonY[0] * scale);
+ builder->MoveTo(p);
+ for (int32_t i = 1; i < aArrowNumPoints; i++) {
+ p = center +
+ gfx::Point(aArrowPolygonX[i] * scale, aArrowPolygonY[i] * scale);
+ builder->LineTo(p);
+ }
+ RefPtr<gfx::Path> path = builder->Finish();
+
+ aDrawTarget.Fill(path, gfx::ColorPattern(ToDeviceColor(aFillColor)));
+}
+
+void ThemeDrawing::PaintRoundedRectWithRadius(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ const LayoutDeviceRect& aClipRect, const sRGBColor& aBackgroundColor,
+ const sRGBColor& aBorderColor, const CSSCoord& aBorderWidth,
+ const CSSCoord& aRadius, const DPIRatio& aDpiRatio) {
+ const bool kBackfaceIsVisible = true;
+ const LayoutDeviceCoord borderWidth(SnapBorderWidth(aBorderWidth, aDpiRatio));
+ const LayoutDeviceCoord radius(aRadius * aDpiRatio);
+ const wr::LayoutRect dest = wr::ToLayoutRect(aRect);
+ const wr::LayoutRect clip = wr::ToLayoutRect(aClipRect);
+
+ // Push the background.
+ if (aBackgroundColor.a != 0.0f) {
+ auto backgroundColor = wr::ToColorF(ToDeviceColor(aBackgroundColor));
+ wr::LayoutRect backgroundRect = [&] {
+ LayoutDeviceRect bg = aRect;
+ bg.Deflate(borderWidth);
+ return wr::ToLayoutRect(bg);
+ }();
+ if (radius == 0.0f) {
+ aWrData.mBuilder.PushRect(backgroundRect, clip, kBackfaceIsVisible, false,
+ false, backgroundColor);
+ } else {
+ // NOTE(emilio): This follows DisplayListBuilder::PushRoundedRect and
+ // draws the rounded fill as an extra thick rounded border instead of a
+ // rectangle that's clipped to a rounded clip. Refer to that method for a
+ // justification. See bug 1694269.
+ LayoutDeviceCoord backgroundRadius =
+ std::max(0.0f, float(radius) - float(borderWidth));
+ wr::BorderSide side = {backgroundColor, wr::BorderStyle::Solid};
+ const wr::BorderSide sides[4] = {side, side, side, side};
+ float h = backgroundRect.width() * 0.6f;
+ float v = backgroundRect.height() * 0.6f;
+ wr::LayoutSideOffsets widths = {v, h, v, h};
+ wr::BorderRadius radii = {{backgroundRadius, backgroundRadius},
+ {backgroundRadius, backgroundRadius},
+ {backgroundRadius, backgroundRadius},
+ {backgroundRadius, backgroundRadius}};
+ aWrData.mBuilder.PushBorder(backgroundRect, clip, kBackfaceIsVisible,
+ widths, {sides, 4}, radii);
+ }
+ }
+
+ if (borderWidth != 0.0f && aBorderColor.a != 0.0f) {
+ // Push the border.
+ const auto borderColor = ToDeviceColor(aBorderColor);
+ const auto side = wr::ToBorderSide(borderColor, StyleBorderStyle::Solid);
+ const wr::BorderSide sides[4] = {side, side, side, side};
+ const LayoutDeviceSize sideRadius(radius, radius);
+ const auto widths =
+ wr::ToBorderWidths(borderWidth, borderWidth, borderWidth, borderWidth);
+ const auto wrRadius =
+ wr::ToBorderRadius(sideRadius, sideRadius, sideRadius, sideRadius);
+ aWrData.mBuilder.PushBorder(dest, clip, kBackfaceIsVisible, widths,
+ {sides, 4}, wrRadius);
+ }
+}
+
+void ThemeDrawing::PaintRoundedRectWithRadius(
+ DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
+ const LayoutDeviceRect& aClipRect, const sRGBColor& aBackgroundColor,
+ const sRGBColor& aBorderColor, const CSSCoord& aBorderWidth,
+ const CSSCoord& aRadius, const DPIRatio& aDpiRatio) {
+ const LayoutDeviceCoord borderWidth(SnapBorderWidth(aBorderWidth, aDpiRatio));
+ const bool needsClip = !(aRect == aClipRect);
+ if (needsClip) {
+ aDrawTarget.PushClipRect(aClipRect.ToUnknownRect());
+ }
+
+ LayoutDeviceRect rect(aRect);
+ // Deflate the rect by half the border width, so that the middle of the
+ // stroke fills exactly the area we want to fill and not more.
+ rect.Deflate(borderWidth * 0.5f);
+
+ LayoutDeviceCoord radius(aRadius * aDpiRatio - borderWidth * 0.5f);
+ // Fix up the radius if it's too large with the rect we're going to paint.
+ {
+ LayoutDeviceCoord min = std::min(rect.width, rect.height);
+ if (radius * 2.0f > min) {
+ radius = min * 0.5f;
+ }
+ }
+
+ Maybe<gfx::ColorPattern> backgroundPattern;
+ if (aBackgroundColor.a != 0.0f) {
+ backgroundPattern.emplace(ToDeviceColor(aBackgroundColor));
+ }
+ Maybe<gfx::ColorPattern> borderPattern;
+ if (borderWidth != 0.0f && aBorderColor.a != 0.0f) {
+ borderPattern.emplace(ToDeviceColor(aBorderColor));
+ }
+
+ if (borderPattern || backgroundPattern) {
+ if (radius != 0.0f) {
+ gfx::RectCornerRadii radii(radius, radius, radius, radius);
+ RefPtr<gfx::Path> roundedRect =
+ MakePathForRoundedRect(aDrawTarget, rect.ToUnknownRect(), radii);
+
+ if (backgroundPattern) {
+ aDrawTarget.Fill(roundedRect, *backgroundPattern);
+ }
+ if (borderPattern) {
+ aDrawTarget.Stroke(roundedRect, *borderPattern,
+ gfx::StrokeOptions(borderWidth));
+ }
+ } else {
+ if (backgroundPattern) {
+ aDrawTarget.FillRect(rect.ToUnknownRect(), *backgroundPattern);
+ }
+ if (borderPattern) {
+ aDrawTarget.StrokeRect(rect.ToUnknownRect(), *borderPattern,
+ gfx::StrokeOptions(borderWidth));
+ }
+ }
+ }
+
+ if (needsClip) {
+ aDrawTarget.PopClip();
+ }
+}
+
+} // namespace mozilla::widget
diff --git a/widget/ThemeDrawing.h b/widget/ThemeDrawing.h
new file mode 100644
index 0000000000..469c29ea2f
--- /dev/null
+++ b/widget/ThemeDrawing.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_ThemeDrawing_h
+#define mozilla_widget_ThemeDrawing_h
+
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "RetainedDisplayListBuilder.h"
+
+namespace mozilla::widget {
+
+struct WebRenderBackendData {
+ wr::DisplayListBuilder& mBuilder;
+ wr::IpcResourceUpdateQueue& mResources;
+ const layers::StackingContextHelper& mSc;
+ layers::RenderRootStateManager* mManager;
+};
+
+class ThemeDrawing {
+ protected:
+ using DrawTarget = gfx::DrawTarget;
+ using sRGBColor = gfx::sRGBColor;
+ using DPIRatio = CSSToLayoutDeviceScale;
+
+ public:
+ virtual ~ThemeDrawing() = 0;
+
+ static void FillRect(DrawTarget&, const LayoutDeviceRect&, const sRGBColor&);
+ static void FillRect(WebRenderBackendData&, const LayoutDeviceRect&,
+ const sRGBColor&);
+
+ // Returns the right scale for points in a aSize x aSize sized box, centered
+ // at 0x0 to fill aRect in the smaller dimension.
+ static float ScaleToFillRect(const LayoutDeviceRect& aRect,
+ const float& aSize) {
+ return std::min(aRect.width, aRect.height) / aSize;
+ }
+
+ static LayoutDeviceIntCoord SnapBorderWidth(const CSSCoord& aCssWidth,
+ const DPIRatio& aDpiRatio);
+
+ static void PaintArrow(DrawTarget&, const LayoutDeviceRect&,
+ const float aArrowPolygonX[],
+ const float aArrowPolygonY[],
+ const float aArrowPolygonSize,
+ const int32_t aArrowNumPoints,
+ const sRGBColor aFillColor);
+
+ static void PaintRoundedRectWithRadius(
+ DrawTarget&, const LayoutDeviceRect& aRect,
+ const LayoutDeviceRect& aClipRect, const sRGBColor& aBackgroundColor,
+ const sRGBColor& aBorderColor, const CSSCoord& aBorderWidth,
+ const CSSCoord& aRadius, const DPIRatio&);
+ static void PaintRoundedRectWithRadius(
+ WebRenderBackendData&, const LayoutDeviceRect& aRect,
+ const LayoutDeviceRect& aClipRect, const sRGBColor& aBackgroundColor,
+ const sRGBColor& aBorderColor, const CSSCoord& aBorderWidth,
+ const CSSCoord& aRadius, const DPIRatio&);
+ template <typename PaintBackendData>
+ static void PaintRoundedRectWithRadius(PaintBackendData& aData,
+ const LayoutDeviceRect& aRect,
+ const sRGBColor& aBackgroundColor,
+ const sRGBColor& aBorderColor,
+ const CSSCoord& aBorderWidth,
+ const CSSCoord& aRadius,
+ const DPIRatio& aDpiRatio) {
+ PaintRoundedRectWithRadius(aData, aRect, aRect, aBackgroundColor,
+ aBorderColor, aBorderWidth, aRadius, aDpiRatio);
+ }
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/TouchEvents.h b/widget/TouchEvents.h
new file mode 100644
index 0000000000..5259276d52
--- /dev/null
+++ b/widget/TouchEvents.h
@@ -0,0 +1,220 @@
+/* -*- 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/. */
+
+#ifndef mozilla_TouchEvents_h__
+#define mozilla_TouchEvents_h__
+
+#include <stdint.h>
+
+#include "mozilla/dom/Touch.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/RefPtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+/******************************************************************************
+ * mozilla::WidgetGestureNotifyEvent
+ *
+ * This event is the first event generated when the user touches
+ * the screen with a finger, and it's meant to decide what kind
+ * of action we'll use for that touch interaction.
+ *
+ * The event is dispatched to the layout and based on what is underneath
+ * the initial contact point it's then decided if we should pan
+ * (finger scrolling) or drag the target element.
+ ******************************************************************************/
+
+class WidgetGestureNotifyEvent : public WidgetGUIEvent {
+ public:
+ virtual WidgetGestureNotifyEvent* AsGestureNotifyEvent() override {
+ return this;
+ }
+
+ WidgetGestureNotifyEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, eGestureNotifyEventClass,
+ aTime),
+ mPanDirection(ePanNone),
+ mDisplayPanFeedback(false) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ // XXX Looks like this event is handled only in PostHandleEvent() of
+ // EventStateManager. Therefore, it might be possible to handle this
+ // in PreHandleEvent() and not to dispatch as a DOM event into the DOM
+ // tree like ContentQueryEvent. Then, this event doesn't need to
+ // support Duplicate().
+ MOZ_ASSERT(mClass == eGestureNotifyEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetGestureNotifyEvent* result =
+ new WidgetGestureNotifyEvent(false, mMessage, nullptr, this);
+ result->AssignGestureNotifyEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ typedef int8_t PanDirectionType;
+ enum PanDirection : PanDirectionType {
+ ePanNone,
+ ePanVertical,
+ ePanHorizontal,
+ ePanBoth
+ };
+
+ PanDirection mPanDirection;
+ bool mDisplayPanFeedback;
+
+ void AssignGestureNotifyEventData(const WidgetGestureNotifyEvent& aEvent,
+ bool aCopyTargets) {
+ AssignGUIEventData(aEvent, aCopyTargets);
+
+ mPanDirection = aEvent.mPanDirection;
+ mDisplayPanFeedback = aEvent.mDisplayPanFeedback;
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetSimpleGestureEvent
+ ******************************************************************************/
+
+class WidgetSimpleGestureEvent : public WidgetMouseEventBase {
+ public:
+ virtual WidgetSimpleGestureEvent* AsSimpleGestureEvent() override {
+ return this;
+ }
+
+ WidgetSimpleGestureEvent(bool aIsTrusted, EventMessage aMessage,
+ nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget,
+ eSimpleGestureEventClass, aTime),
+ mAllowedDirections(0),
+ mDirection(0),
+ mClickCount(0),
+ mDelta(0.0) {}
+
+ WidgetSimpleGestureEvent(const WidgetSimpleGestureEvent& aOther)
+ : WidgetMouseEventBase(aOther.IsTrusted(), aOther.mMessage,
+ aOther.mWidget, eSimpleGestureEventClass),
+ mAllowedDirections(aOther.mAllowedDirections),
+ mDirection(aOther.mDirection),
+ mClickCount(0),
+ mDelta(aOther.mDelta) {}
+
+ virtual WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eSimpleGestureEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetSimpleGestureEvent* result =
+ new WidgetSimpleGestureEvent(false, mMessage, nullptr, this);
+ result->AssignSimpleGestureEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ // See SimpleGestureEvent.webidl for values
+ uint32_t mAllowedDirections;
+ // See SimpleGestureEvent.webidl for values
+ uint32_t mDirection;
+ // The number of taps for tap events
+ uint32_t mClickCount;
+ // Delta for magnify and rotate events
+ double mDelta;
+
+ // XXX Not tested by test_assign_event_data.html
+ void AssignSimpleGestureEventData(const WidgetSimpleGestureEvent& aEvent,
+ bool aCopyTargets) {
+ AssignMouseEventBaseData(aEvent, aCopyTargets);
+
+ // mAllowedDirections isn't copied
+ mDirection = aEvent.mDirection;
+ mDelta = aEvent.mDelta;
+ mClickCount = aEvent.mClickCount;
+ }
+};
+
+/******************************************************************************
+ * mozilla::WidgetTouchEvent
+ ******************************************************************************/
+
+class WidgetTouchEvent final : public WidgetInputEvent {
+ public:
+ typedef nsTArray<RefPtr<mozilla::dom::Touch>> TouchArray;
+ typedef AutoTArray<RefPtr<mozilla::dom::Touch>, 10> AutoTouchArray;
+ typedef AutoTouchArray::base_type TouchArrayBase;
+
+ WidgetTouchEvent* AsTouchEvent() override { return this; }
+
+ MOZ_COUNTED_DEFAULT_CTOR(WidgetTouchEvent)
+
+ WidgetTouchEvent(const WidgetTouchEvent& aOther)
+ : WidgetInputEvent(aOther.IsTrusted(), aOther.mMessage, aOther.mWidget,
+ eTouchEventClass) {
+ MOZ_COUNT_CTOR(WidgetTouchEvent);
+ mModifiers = aOther.mModifiers;
+ mTimeStamp = aOther.mTimeStamp;
+ mTouches.AppendElements(aOther.mTouches);
+ mInputSource = aOther.mInputSource;
+ mButton = aOther.mButton;
+ mButtons = aOther.mButtons;
+ mFlags.mCancelable = mMessage != eTouchCancel;
+ mFlags.mHandledByAPZ = aOther.mFlags.mHandledByAPZ;
+ }
+
+ WidgetTouchEvent(WidgetTouchEvent&& aOther)
+ : WidgetInputEvent(std::move(aOther)) {
+ MOZ_COUNT_CTOR(WidgetTouchEvent);
+ mModifiers = aOther.mModifiers;
+ mTimeStamp = aOther.mTimeStamp;
+ mTouches = std::move(aOther.mTouches);
+ mInputSource = aOther.mInputSource;
+ mButton = aOther.mButton;
+ mButtons = aOther.mButtons;
+ mFlags = aOther.mFlags;
+ }
+
+ WidgetTouchEvent& operator=(WidgetTouchEvent&&) = default;
+
+ WidgetTouchEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
+ const WidgetEventTime* aTime = nullptr)
+ : WidgetInputEvent(aIsTrusted, aMessage, aWidget, eTouchEventClass,
+ aTime) {
+ MOZ_COUNT_CTOR(WidgetTouchEvent);
+ mFlags.mCancelable = mMessage != eTouchCancel;
+ }
+
+ MOZ_COUNTED_DTOR_OVERRIDE(WidgetTouchEvent)
+
+ WidgetEvent* Duplicate() const override {
+ MOZ_ASSERT(mClass == eTouchEventClass,
+ "Duplicate() must be overridden by sub class");
+ // Not copying widget, it is a weak reference.
+ WidgetTouchEvent* result =
+ new WidgetTouchEvent(false, mMessage, nullptr, this);
+ result->AssignTouchEventData(*this, true);
+ result->mFlags = mFlags;
+ return result;
+ }
+
+ TouchArray mTouches;
+ uint16_t mInputSource = 5; // MouseEvent_Binding::MOZ_SOURCE_TOUCH
+ int16_t mButton = eNotPressed;
+ int16_t mButtons = 0;
+
+ void AssignTouchEventData(const WidgetTouchEvent& aEvent, bool aCopyTargets) {
+ AssignInputEventData(aEvent, aCopyTargets);
+
+ // Assign*EventData() assume that they're called only new instance.
+ MOZ_ASSERT(mTouches.IsEmpty());
+ mTouches.AppendElements(aEvent.mTouches);
+ mInputSource = aEvent.mInputSource;
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_TouchEvents_h__
diff --git a/widget/TouchResampler.cpp b/widget/TouchResampler.cpp
new file mode 100644
index 0000000000..eeed30fe0e
--- /dev/null
+++ b/widget/TouchResampler.cpp
@@ -0,0 +1,377 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "TouchResampler.h"
+
+#include "nsAlgorithm.h"
+
+/**
+ * TouchResampler implementation
+ */
+
+namespace mozilla {
+namespace widget {
+
+// The values below have been tested and found to be acceptable on a device
+// with a display refresh rate of 60Hz and touch sampling rate of 100Hz.
+// While their "ideal" values are dependent on the exact rates of each device,
+// the values we've picked below should be somewhat robust across a variation of
+// different rates. They mostly aim to avoid making predictions that are too far
+// away (in terms of distance) from the finger, and to detect pauses in the
+// finger motion without too much delay.
+
+// Maximum time between two consecutive data points to consider resampling
+// between them.
+// Values between 1x and 5x of the touch sampling interval are reasonable.
+static const double kTouchResampleWindowSize = 40.0;
+
+// These next two values constrain the sampling timestamp.
+// Our caller will usually adjust frame timestamps to be slightly in the past,
+// for example by 5ms. This means that, during normal operation, we will
+// maximally need to predict by [touch sampling rate] minus 5ms.
+// So we would like kTouchResampleMaxPredictMs to satisfy the following:
+// kTouchResampleMaxPredictMs + [frame time adjust] > [touch sampling rate]
+static const double kTouchResampleMaxPredictMs = 8.0;
+// This one is a protection against very outdated frame timestamps.
+// Values larger than the touch sampling interval and less than 3x of the vsync
+// interval are reasonable.
+static const double kTouchResampleMaxBacksampleMs = 20.0;
+
+// The maximum age of the most recent data point to consider resampling.
+// Should be between 1x and 3x of the touch sampling interval.
+static const double kTouchResampleOldTouchThresholdMs = 17.0;
+
+uint64_t TouchResampler::ProcessEvent(MultiTouchInput&& aInput) {
+ mCurrentTouches.UpdateFromEvent(aInput);
+
+ uint64_t eventId = mNextEventId;
+ mNextEventId++;
+
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_MOVE) {
+ // Touch move events are deferred until NotifyFrame.
+ mDeferredTouchMoveEvents.push({std::move(aInput), eventId});
+ } else {
+ // Non-move events are transferred to the outgoing queue unmodified.
+ // If there are pending touch move events, flush those out first, so that
+ // events are emitted in the right order.
+ FlushDeferredTouchMoveEventsUnresampled();
+ if (mInResampledState) {
+ // Return to a non-resampled state before emitting a non-move event.
+ ReturnToNonResampledState();
+ }
+ EmitEvent(std::move(aInput), eventId);
+ }
+
+ return eventId;
+}
+
+void TouchResampler::NotifyFrame(const TimeStamp& aTimeStamp) {
+ TimeStamp lastTouchTime = mCurrentTouches.LatestDataPointTime();
+ if (mDeferredTouchMoveEvents.empty() ||
+ (lastTouchTime &&
+ lastTouchTime < aTimeStamp - TimeDuration::FromMilliseconds(
+ kTouchResampleOldTouchThresholdMs))) {
+ // We haven't received a touch move event in a while, so the fingers must
+ // have stopped moving. Flush any old touch move events.
+ FlushDeferredTouchMoveEventsUnresampled();
+
+ if (mInResampledState) {
+ // Make sure we pause at the resting position that we actually observed,
+ // and not at a resampled position.
+ ReturnToNonResampledState();
+ }
+
+ // Clear touch location history so that we don't resample across a pause.
+ mCurrentTouches.ClearDataPoints();
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(lastTouchTime);
+ TimeStamp lowerBound = lastTouchTime - TimeDuration::FromMilliseconds(
+ kTouchResampleMaxBacksampleMs);
+ TimeStamp upperBound = lastTouchTime + TimeDuration::FromMilliseconds(
+ kTouchResampleMaxPredictMs);
+ TimeStamp sampleTime = clamped(aTimeStamp, lowerBound, upperBound);
+
+ if (mLastEmittedEventTime && sampleTime < mLastEmittedEventTime) {
+ // Keep emitted timestamps in order.
+ sampleTime = mLastEmittedEventTime;
+ }
+
+ // We have at least one pending touch move event. Pick one of the events from
+ // mDeferredTouchMoveEvents as the base event for the resampling adjustment.
+ // We want to produce an event stream whose timestamps are in the right order.
+ // As the base event, use the first event that's at or after sampleTime,
+ // unless there is no such event, in that case use the last one we have. We
+ // will set the timestamp on the resampled event to sampleTime later.
+ // Flush out any older events so that everything remains in the right order.
+ MultiTouchInput input;
+ uint64_t eventId;
+ while (true) {
+ MOZ_RELEASE_ASSERT(!mDeferredTouchMoveEvents.empty());
+ std::tie(input, eventId) = std::move(mDeferredTouchMoveEvents.front());
+ mDeferredTouchMoveEvents.pop();
+ if (mDeferredTouchMoveEvents.empty() || input.mTimeStamp >= sampleTime) {
+ break;
+ }
+ // Flush this event to the outgoing queue without resampling. What ends up
+ // on the screen will still be smooth because we will proceed to emit a
+ // resampled event before the paint for this frame starts.
+ PrependLeftoverHistoricalData(&input);
+ MOZ_RELEASE_ASSERT(input.mTimeStamp < sampleTime);
+ EmitEvent(std::move(input), eventId);
+ }
+
+ mOriginalOfResampledTouchMove = Nothing();
+
+ // Compute the resampled touch positions.
+ nsTArray<ScreenIntPoint> resampledPositions;
+ bool anyPositionDifferentFromOriginal = false;
+ for (const auto& touch : input.mTouches) {
+ ScreenIntPoint resampledPosition =
+ mCurrentTouches.ResampleTouchPositionAtTime(
+ touch.mIdentifier, touch.mScreenPoint, sampleTime);
+ if (resampledPosition != touch.mScreenPoint) {
+ anyPositionDifferentFromOriginal = true;
+ }
+ resampledPositions.AppendElement(resampledPosition);
+ }
+
+ if (anyPositionDifferentFromOriginal) {
+ // Store a copy of the original event, so that we can return to an
+ // non-resampled position later, if necessary.
+ mOriginalOfResampledTouchMove = Some(input);
+
+ // Add the original observed position to the historical data, as well as any
+ // leftover historical positions from the previous touch move event, and
+ // store the resampled values in the "final" position of the event.
+ PrependLeftoverHistoricalData(&input);
+ for (size_t i = 0; i < input.mTouches.Length(); i++) {
+ auto& touch = input.mTouches[i];
+ touch.mHistoricalData.AppendElement(SingleTouchData::HistoricalTouchData{
+ input.mTimeStamp,
+ touch.mScreenPoint,
+ touch.mLocalScreenPoint,
+ touch.mRadius,
+ touch.mRotationAngle,
+ touch.mForce,
+ });
+
+ // Remove any historical touch data that's in the future, compared to
+ // sampleTime. This data will be included by upcoming touch move
+ // events. This only happens if the frame timestamp can be older than the
+ // event timestamp, i.e. if interpolation occurs (rather than
+ // extrapolation).
+ auto futureDataStart = std::find_if(
+ touch.mHistoricalData.begin(), touch.mHistoricalData.end(),
+ [sampleTime](
+ const SingleTouchData::HistoricalTouchData& aHistoricalData) {
+ return aHistoricalData.mTimeStamp > sampleTime;
+ });
+ if (futureDataStart != touch.mHistoricalData.end()) {
+ nsTArray<SingleTouchData::HistoricalTouchData> futureData(
+ Span<SingleTouchData::HistoricalTouchData>(touch.mHistoricalData)
+ .From(futureDataStart.GetIndex()));
+ touch.mHistoricalData.TruncateLength(futureDataStart.GetIndex());
+ mRemainingTouchData.insert({touch.mIdentifier, std::move(futureData)});
+ }
+
+ touch.mScreenPoint = resampledPositions[i];
+ }
+ input.mTimeStamp = sampleTime;
+ }
+
+ EmitEvent(std::move(input), eventId);
+ mInResampledState = anyPositionDifferentFromOriginal;
+}
+
+void TouchResampler::PrependLeftoverHistoricalData(MultiTouchInput* aInput) {
+ for (auto& touch : aInput->mTouches) {
+ auto leftoverData = mRemainingTouchData.find(touch.mIdentifier);
+ if (leftoverData != mRemainingTouchData.end()) {
+ nsTArray<SingleTouchData::HistoricalTouchData> data =
+ std::move(leftoverData->second);
+ mRemainingTouchData.erase(leftoverData);
+ touch.mHistoricalData.InsertElementsAt(0, data);
+ }
+
+ if (TimeStamp cutoffTime = mLastEmittedEventTime) {
+ // If we received historical touch data that was further in the past than
+ // the last resampled event, discard that data so that the touch data
+ // points are emitted in order.
+ touch.mHistoricalData.RemoveElementsBy(
+ [cutoffTime](const SingleTouchData::HistoricalTouchData& aTouchData) {
+ return aTouchData.mTimeStamp < cutoffTime;
+ });
+ }
+ }
+ mRemainingTouchData.clear();
+}
+
+void TouchResampler::FlushDeferredTouchMoveEventsUnresampled() {
+ while (!mDeferredTouchMoveEvents.empty()) {
+ auto [input, eventId] = std::move(mDeferredTouchMoveEvents.front());
+ mDeferredTouchMoveEvents.pop();
+ PrependLeftoverHistoricalData(&input);
+ EmitEvent(std::move(input), eventId);
+ mInResampledState = false;
+ mOriginalOfResampledTouchMove = Nothing();
+ }
+}
+
+void TouchResampler::ReturnToNonResampledState() {
+ MOZ_RELEASE_ASSERT(mInResampledState);
+ MOZ_RELEASE_ASSERT(mDeferredTouchMoveEvents.empty(),
+ "Don't call this if there is a deferred touch move event. "
+ "We can return to the non-resampled state by sending that "
+ "event, rather than a copy of a previous event.");
+
+ // The last outgoing event was a resampled touch move event.
+ // Return to the non-resampled state, by sending a touch move event to
+ // "overwrite" any resampled positions with the original observed positions.
+ MultiTouchInput input = std::move(*mOriginalOfResampledTouchMove);
+ mOriginalOfResampledTouchMove = Nothing();
+
+ // For the event's timestamp, we want to backdate the correction as far as we
+ // can, while still preserving timestamp ordering. But we also don't want to
+ // backdate it to be older than it was originally.
+ if (mLastEmittedEventTime > input.mTimeStamp) {
+ input.mTimeStamp = mLastEmittedEventTime;
+ }
+
+ // Assemble the correct historical touch data for this event.
+ // We don't want to include data points that we've already sent out with the
+ // resampled event. And from the leftover data points, we only want those that
+ // don't duplicate the final time + position of this event.
+ for (auto& touch : input.mTouches) {
+ touch.mHistoricalData.Clear();
+ }
+ PrependLeftoverHistoricalData(&input);
+ for (auto& touch : input.mTouches) {
+ touch.mHistoricalData.RemoveElementsBy([&](const auto& histData) {
+ return histData.mTimeStamp >= input.mTimeStamp;
+ });
+ }
+
+ EmitExtraEvent(std::move(input));
+ mInResampledState = false;
+}
+
+void TouchResampler::TouchInfo::Update(const SingleTouchData& aTouch,
+ const TimeStamp& aEventTime) {
+ for (const auto& historicalData : aTouch.mHistoricalData) {
+ mBaseDataPoint = mLatestDataPoint;
+ mLatestDataPoint =
+ Some(DataPoint{historicalData.mTimeStamp, historicalData.mScreenPoint});
+ }
+ mBaseDataPoint = mLatestDataPoint;
+ mLatestDataPoint = Some(DataPoint{aEventTime, aTouch.mScreenPoint});
+}
+
+ScreenIntPoint TouchResampler::TouchInfo::ResampleAtTime(
+ const ScreenIntPoint& aLastObservedPosition, const TimeStamp& aTimeStamp) {
+ TimeStamp cutoff =
+ aTimeStamp - TimeDuration::FromMilliseconds(kTouchResampleWindowSize);
+ if (!mBaseDataPoint || !mLatestDataPoint ||
+ !(mBaseDataPoint->mTimeStamp < mLatestDataPoint->mTimeStamp) ||
+ mBaseDataPoint->mTimeStamp < cutoff) {
+ return aLastObservedPosition;
+ }
+
+ // For the actual resampling, connect the last two data points with a line and
+ // sample along that line.
+ TimeStamp t1 = mBaseDataPoint->mTimeStamp;
+ TimeStamp t2 = mLatestDataPoint->mTimeStamp;
+ double t = (aTimeStamp - t1) / (t2 - t1);
+
+ double x1 = mBaseDataPoint->mPosition.x;
+ double x2 = mLatestDataPoint->mPosition.x;
+ double y1 = mBaseDataPoint->mPosition.y;
+ double y2 = mLatestDataPoint->mPosition.y;
+
+ int32_t resampledX = round(x1 + t * (x2 - x1));
+ int32_t resampledY = round(y1 + t * (y2 - y1));
+ return ScreenIntPoint(resampledX, resampledY);
+}
+
+void TouchResampler::CurrentTouches::UpdateFromEvent(
+ const MultiTouchInput& aInput) {
+ switch (aInput.mType) {
+ case MultiTouchInput::MULTITOUCH_START: {
+ // A new touch has been added; make sure mTouches reflects the current
+ // touches in the event.
+ nsTArray<TouchInfo> newTouches;
+ for (const auto& touch : aInput.mTouches) {
+ const auto touchInfo = TouchByIdentifier(touch.mIdentifier);
+ if (touchInfo != mTouches.end()) {
+ // This is one of the existing touches.
+ newTouches.AppendElement(std::move(*touchInfo));
+ mTouches.RemoveElementAt(touchInfo);
+ } else {
+ // This is the new touch.
+ newTouches.AppendElement(TouchInfo{
+ touch.mIdentifier, Nothing(),
+ Some(DataPoint{aInput.mTimeStamp, touch.mScreenPoint})});
+ }
+ }
+ MOZ_ASSERT(mTouches.IsEmpty(), "Missing touch end before touch start?");
+ mTouches = std::move(newTouches);
+ break;
+ }
+
+ case MultiTouchInput::MULTITOUCH_MOVE: {
+ // The touches have moved.
+ // Add position information to the history data points.
+ for (const auto& touch : aInput.mTouches) {
+ const auto touchInfo = TouchByIdentifier(touch.mIdentifier);
+ MOZ_ASSERT(touchInfo != mTouches.end());
+ if (touchInfo != mTouches.end()) {
+ touchInfo->Update(touch, aInput.mTimeStamp);
+ }
+ }
+ mLatestDataPointTime = aInput.mTimeStamp;
+ break;
+ }
+
+ case MultiTouchInput::MULTITOUCH_END: {
+ // A touch has been removed.
+ MOZ_RELEASE_ASSERT(aInput.mTouches.Length() == 1);
+ const auto touchInfo = TouchByIdentifier(aInput.mTouches[0].mIdentifier);
+ MOZ_ASSERT(touchInfo != mTouches.end());
+ if (touchInfo != mTouches.end()) {
+ mTouches.RemoveElementAt(touchInfo);
+ }
+ break;
+ }
+
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ // All touches are canceled.
+ mTouches.Clear();
+ break;
+ }
+}
+
+nsTArray<TouchResampler::TouchInfo>::iterator
+TouchResampler::CurrentTouches::TouchByIdentifier(int32_t aIdentifier) {
+ return std::find_if(mTouches.begin(), mTouches.end(),
+ [aIdentifier](const TouchInfo& info) {
+ return info.mIdentifier == aIdentifier;
+ });
+}
+
+ScreenIntPoint TouchResampler::CurrentTouches::ResampleTouchPositionAtTime(
+ int32_t aIdentifier, const ScreenIntPoint& aLastObservedPosition,
+ const TimeStamp& aTimeStamp) {
+ const auto touchInfo = TouchByIdentifier(aIdentifier);
+ MOZ_ASSERT(touchInfo != mTouches.end());
+ if (touchInfo != mTouches.end()) {
+ return touchInfo->ResampleAtTime(aLastObservedPosition, aTimeStamp);
+ }
+ return aLastObservedPosition;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/TouchResampler.h b/widget/TouchResampler.h
new file mode 100644
index 0000000000..69f544b557
--- /dev/null
+++ b/widget/TouchResampler.h
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_TouchResampler_h
+#define mozilla_widget_TouchResampler_h
+
+#include <queue>
+#include <unordered_map>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h"
+#include "InputData.h"
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * De-jitters touch motions by resampling (interpolating or extrapolating) touch
+ * positions for the vsync timestamp.
+ *
+ * Touch resampling improves the touch panning experience on devices where touch
+ * positions are sampled at a rate that's not an integer multiple of the display
+ * refresh rate, for example 100Hz touch sampling on a 60Hz display: Without
+ * resampling, we would alternate between taking one touch sample or two touch
+ * samples into account each frame, creating a jittery motion ("small step, big
+ * step, small step, big step").
+ * Intended for use on Android, where both touch events and vsync notifications
+ * arrive on the same thread, the Java UI thread.
+ * This class is not thread safe.
+ *
+ * TouchResampler operates in the following way:
+ *
+ * Original events are fed into ProcessEvent().
+ * Outgoing events (potentially resampled for resampling) are added to a queue
+ * and can be consumed by calling ConsumeOutgoingEvents(). Touch events which
+ * are not touch move events are forwarded instantly and not resampled. Only
+ * touch move events are resampled. Whenever a touch move event is received, it
+ * gets delayed until NotifyFrame() is called, at which point it is resampled
+ * into a resampled version for the given frame timestamp, and added to the
+ * outgoing queue. If no touch move event is received between two consecutive
+ * frames, this is treated as a stop in the touch motion. If the last outgoing
+ * event was an resampled touch move event, we return back to the non-resampled
+ * state by emitting a copy of the last original touch move event, which has
+ * unmodified position data. Touch events which are not touch move events also
+ * force a return to the non-resampled state before they are moved to the
+ * outgoing queue.
+ */
+class TouchResampler final {
+ public:
+ // Feed a touch event into the interpolater. Returns an ID that can be used to
+ // match outgoing events to this incoming event, to track data associated with
+ // this event.
+ uint64_t ProcessEvent(MultiTouchInput&& aInput);
+
+ // Emit events, potentially resampled, for this timestamp. The events are put
+ // into the outgoing queue. May not emit any events if there's no update.
+ void NotifyFrame(const TimeStamp& aTimeStamp);
+
+ // Returns true between the start and the end of a touch gesture. During this
+ // time, the caller should keep itself registered with the system frame
+ // callback mechanism, so that NotifyFrame() can be called on every frame.
+ // (Otherwise, if we only registered the callback after receiving a touch move
+ // event, the frame callback might be delayed by a full frame.)
+ bool InTouchingState() const { return mCurrentTouches.HasTouch(); }
+
+ struct OutgoingEvent {
+ // The event, potentially modified from the original for resampling.
+ MultiTouchInput mEvent;
+
+ // Some(eventId) if this event is a modified version of an original event,
+ // Nothing() if this is an extra event.
+ Maybe<uint64_t> mEventId;
+ };
+
+ // Returns the outgoing events that were produced since the last call.
+ // No event IDs will be skipped. Returns at least one outgoing event for each
+ // incoming event (possibly after a delay), and potential extra events with
+ // no originating event ID.
+ // Outgoing events should be consumed after every call to ProcessEvent() and
+ // after every call to NotifyFrame().
+ std::queue<OutgoingEvent> ConsumeOutgoingEvents() {
+ return std::move(mOutgoingEvents);
+ }
+
+ private:
+ // Add the event to the outgoing queue.
+ void EmitEvent(MultiTouchInput&& aInput, uint64_t aEventId) {
+ mLastEmittedEventTime = aInput.mTimeStamp;
+ mOutgoingEvents.push(OutgoingEvent{std::move(aInput), Some(aEventId)});
+ }
+
+ // Emit an event that does not correspond to an incoming event.
+ void EmitExtraEvent(MultiTouchInput&& aInput) {
+ mLastEmittedEventTime = aInput.mTimeStamp;
+ mOutgoingEvents.push(OutgoingEvent{std::move(aInput), Nothing()});
+ }
+
+ // Move any touch move events that we deferred for resampling to the outgoing
+ // queue unmodified, leaving mDeferredTouchMoveEvents empty.
+ void FlushDeferredTouchMoveEventsUnresampled();
+
+ // Must only be called if mInResampledState is true and
+ // mDeferredTouchMoveEvents is empty. Emits mOriginalOfResampledTouchMove,
+ // with a potentially adjusted timestamp for correct ordering.
+ void ReturnToNonResampledState();
+
+ // Takes historical touch data from mRemainingTouchData and prepends it to the
+ // data in aInput.
+ void PrependLeftoverHistoricalData(MultiTouchInput* aInput);
+
+ struct DataPoint {
+ TimeStamp mTimeStamp;
+ ScreenIntPoint mPosition;
+ };
+
+ struct TouchInfo {
+ void Update(const SingleTouchData& aTouch, const TimeStamp& aEventTime);
+ ScreenIntPoint ResampleAtTime(const ScreenIntPoint& aLastObservedPosition,
+ const TimeStamp& aTimeStamp);
+
+ int32_t mIdentifier = 0;
+ Maybe<DataPoint> mBaseDataPoint;
+ Maybe<DataPoint> mLatestDataPoint;
+ };
+
+ struct CurrentTouches {
+ void UpdateFromEvent(const MultiTouchInput& aInput);
+ bool HasTouch() const { return !mTouches.IsEmpty(); }
+ TimeStamp LatestDataPointTime() { return mLatestDataPointTime; }
+
+ ScreenIntPoint ResampleTouchPositionAtTime(
+ int32_t aIdentifier, const ScreenIntPoint& aLastObservedPosition,
+ const TimeStamp& aTimeStamp);
+
+ void ClearDataPoints() {
+ for (auto& touch : mTouches) {
+ touch.mBaseDataPoint = Nothing();
+ touch.mLatestDataPoint = Nothing();
+ }
+ }
+
+ private:
+ nsTArray<TouchInfo>::iterator TouchByIdentifier(int32_t aIdentifier);
+
+ nsTArray<TouchInfo> mTouches;
+ TimeStamp mLatestDataPointTime;
+ };
+
+ // The current touch positions with historical data points. This data only
+ // contains original non-resampled positions from the incoming touch events.
+ CurrentTouches mCurrentTouches;
+
+ // Incoming touch move events are stored here until NotifyFrame is called.
+ std::queue<std::pair<MultiTouchInput, uint64_t>> mDeferredTouchMoveEvents;
+
+ // Stores any touch samples that were not included in the last emitted touch
+ // move event because they were in the future compared to the emitted event's
+ // timestamp. These data points should be prepended to the historical data of
+ // the next emitted touch move evnt.
+ // Can only be non-empty if mInResampledState is true.
+ std::unordered_map<int32_t, nsTArray<SingleTouchData::HistoricalTouchData>>
+ mRemainingTouchData;
+
+ // If we're in an resampled state, because the last outgoing event was a
+ // resampled touch move event, then this contains a copy of the unresampled,
+ // original touch move event.
+ // Some() iff mInResampledState is true.
+ Maybe<MultiTouchInput> mOriginalOfResampledTouchMove;
+
+ // The stream of outgoing events that can be consumed by our caller.
+ std::queue<OutgoingEvent> mOutgoingEvents;
+
+ // The timestamp of the event that was emitted most recently, or the null
+ // timestamp if no event has been emitted yet.
+ TimeStamp mLastEmittedEventTime;
+
+ uint64_t mNextEventId = 0;
+
+ // True if the last outgoing event was a touch move event with an resampled
+ // position. We only want to stay in this state as long as a continuous stream
+ // of touch move events is coming in.
+ bool mInResampledState = false;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_TouchResampler_h
diff --git a/widget/VsyncDispatcher.cpp b/widget/VsyncDispatcher.cpp
new file mode 100644
index 0000000000..acb00a368a
--- /dev/null
+++ b/widget/VsyncDispatcher.cpp
@@ -0,0 +1,261 @@
+/* -*- 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 "MainThreadUtils.h"
+#include "VsyncDispatcher.h"
+#include "VsyncSource.h"
+#include "gfxPlatform.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/StaticPrefs_gfx.h"
+
+using namespace mozilla::layers;
+
+namespace mozilla {
+
+CompositorVsyncDispatcher::CompositorVsyncDispatcher(
+ RefPtr<VsyncDispatcher> aVsyncDispatcher)
+ : mVsyncDispatcher(std::move(aVsyncDispatcher)),
+ mCompositorObserverLock("CompositorObserverLock"),
+ mDidShutdown(false) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+CompositorVsyncDispatcher::~CompositorVsyncDispatcher() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+}
+
+void CompositorVsyncDispatcher::NotifyVsync(const VsyncEvent& aVsync) {
+ // In vsync thread
+ layers::CompositorBridgeParent::PostInsertVsyncProfilerMarker(aVsync.mTime);
+
+ MutexAutoLock lock(mCompositorObserverLock);
+ if (mCompositorVsyncObserver) {
+ mCompositorVsyncObserver->NotifyVsync(aVsync);
+ }
+}
+
+void CompositorVsyncDispatcher::ObserveVsync(bool aEnable) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (mDidShutdown) {
+ return;
+ }
+
+ if (aEnable) {
+ mVsyncDispatcher->AddVsyncObserver(this);
+ } else {
+ mVsyncDispatcher->RemoveVsyncObserver(this);
+ }
+}
+
+void CompositorVsyncDispatcher::SetCompositorVsyncObserver(
+ VsyncObserver* aVsyncObserver) {
+ // When remote compositing or running gtests, vsync observation is
+ // initiated on the main thread. Otherwise, it is initiated from the
+ // compositor thread.
+ MOZ_ASSERT(NS_IsMainThread() ||
+ CompositorThreadHolder::IsInCompositorThread());
+
+ { // scope lock
+ MutexAutoLock lock(mCompositorObserverLock);
+ mCompositorVsyncObserver = aVsyncObserver;
+ }
+
+ bool observeVsync = aVsyncObserver != nullptr;
+ nsCOMPtr<nsIRunnable> vsyncControl = NewRunnableMethod<bool>(
+ "CompositorVsyncDispatcher::ObserveVsync", this,
+ &CompositorVsyncDispatcher::ObserveVsync, observeVsync);
+ NS_DispatchToMainThread(vsyncControl);
+}
+
+void CompositorVsyncDispatcher::Shutdown() {
+ // Need to explicitly remove CompositorVsyncDispatcher when the nsBaseWidget
+ // shuts down. Otherwise, we would get dead vsync notifications between when
+ // the nsBaseWidget shuts down and the CompositorBridgeParent shuts down.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mDidShutdown);
+ ObserveVsync(false);
+ mDidShutdown = true;
+ { // scope lock
+ MutexAutoLock lock(mCompositorObserverLock);
+ mCompositorVsyncObserver = nullptr;
+ }
+ mVsyncDispatcher = nullptr;
+}
+
+VsyncDispatcher::VsyncDispatcher(gfx::VsyncSource* aVsyncSource)
+ : mState(State(aVsyncSource), "VsyncDispatcher::mState") {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+VsyncDispatcher::~VsyncDispatcher() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+void VsyncDispatcher::SetVsyncSource(gfx::VsyncSource* aVsyncSource) {
+ MOZ_RELEASE_ASSERT(aVsyncSource);
+
+ auto state = mState.Lock();
+ if (aVsyncSource == state->mCurrentVsyncSource) {
+ return;
+ }
+
+ if (state->mIsObservingVsync) {
+ state->mCurrentVsyncSource->RemoveVsyncDispatcher(this);
+ aVsyncSource->AddVsyncDispatcher(this);
+ }
+ state->mCurrentVsyncSource = aVsyncSource;
+}
+
+RefPtr<gfx::VsyncSource> VsyncDispatcher::GetCurrentVsyncSource() {
+ auto state = mState.Lock();
+ return state->mCurrentVsyncSource;
+}
+
+TimeDuration VsyncDispatcher::GetVsyncRate() {
+ auto state = mState.Lock();
+ return state->mCurrentVsyncSource->GetVsyncRate();
+}
+
+static int32_t ComputeFrameRateDivisor(gfx::VsyncSource* aCurrentVsyncSource) {
+ int32_t maxRate = StaticPrefs::gfx_display_max_frame_rate();
+ if (maxRate == 0) {
+ return StaticPrefs::gfx_display_frame_rate_divisor();
+ }
+
+ // Compute the frame rate divisor based on max frame rates.
+ double frameDuration = aCurrentVsyncSource->GetVsyncRate().ToMilliseconds();
+
+ // Respect the pref gfx.display.frame-rate-divisor if larger.
+ return std::max(StaticPrefs::gfx_display_frame_rate_divisor(),
+ int32_t(floor(1000.0 / frameDuration / maxRate)));
+}
+
+void VsyncDispatcher::NotifyVsync(const VsyncEvent& aVsync) {
+ nsTArray<RefPtr<VsyncObserver>> observers;
+ bool shouldDispatchToMainThread = false;
+ {
+ auto state = mState.Lock();
+ if (++state->mVsyncSkipCounter <
+ ComputeFrameRateDivisor(state->mCurrentVsyncSource)) {
+ return;
+ }
+ state->mVsyncSkipCounter = 0;
+
+ // Copy out the observers so that we don't keep the mutex
+ // locked while notifying vsync.
+ observers = state->mObservers.Clone();
+ shouldDispatchToMainThread = !state->mMainThreadObservers.IsEmpty() &&
+ (state->mLastVsyncIdSentToMainThread ==
+ state->mLastMainThreadProcessedVsyncId);
+ }
+
+ for (const auto& observer : observers) {
+ observer->NotifyVsync(aVsync);
+ }
+
+ if (shouldDispatchToMainThread) {
+ auto state = mState.Lock();
+ state->mLastVsyncIdSentToMainThread = aVsync.mId;
+ NS_DispatchToMainThread(NewRunnableMethod<VsyncEvent>(
+ "VsyncDispatcher::NotifyMainThreadObservers", this,
+ &VsyncDispatcher::NotifyMainThreadObservers, aVsync));
+ }
+}
+
+void VsyncDispatcher::NotifyMainThreadObservers(VsyncEvent aEvent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsTArray<RefPtr<VsyncObserver>> observers;
+ {
+ // Copy out the main thread observers so that we don't keep the mutex
+ // locked while notifying vsync.
+ auto state = mState.Lock();
+ observers.AppendElements(state->mMainThreadObservers);
+ }
+
+ for (const auto& observer : observers) {
+ observer->NotifyVsync(aEvent);
+ }
+
+ { // Scope lock
+ auto state = mState.Lock();
+ state->mLastMainThreadProcessedVsyncId = aEvent.mId;
+ }
+}
+
+void VsyncDispatcher::AddVsyncObserver(VsyncObserver* aVsyncObserver) {
+ MOZ_ASSERT(aVsyncObserver);
+ { // scope lock - called on PBackground thread or main thread
+ auto state = mState.Lock();
+ if (!state->mObservers.Contains(aVsyncObserver)) {
+ state->mObservers.AppendElement(aVsyncObserver);
+ }
+ }
+
+ UpdateVsyncStatus();
+}
+
+void VsyncDispatcher::RemoveVsyncObserver(VsyncObserver* aVsyncObserver) {
+ MOZ_ASSERT(aVsyncObserver);
+ { // scope lock - called on PBackground thread or main thread
+ auto state = mState.Lock();
+ state->mObservers.RemoveElement(aVsyncObserver);
+ }
+
+ UpdateVsyncStatus();
+}
+
+void VsyncDispatcher::AddMainThreadObserver(VsyncObserver* aObserver) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aObserver);
+ {
+ auto state = mState.Lock();
+ state->mMainThreadObservers.AppendElement(aObserver);
+ }
+
+ UpdateVsyncStatus();
+}
+
+void VsyncDispatcher::RemoveMainThreadObserver(VsyncObserver* aObserver) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aObserver);
+ {
+ auto state = mState.Lock();
+ state->mMainThreadObservers.RemoveElement(aObserver);
+ }
+
+ UpdateVsyncStatus();
+}
+
+void VsyncDispatcher::UpdateVsyncStatus() {
+ bool wasObservingVsync = false;
+ bool needVsync = false;
+ RefPtr<gfx::VsyncSource> vsyncSource;
+
+ {
+ auto state = mState.Lock();
+ wasObservingVsync = state->mIsObservingVsync;
+ needVsync =
+ !state->mObservers.IsEmpty() || !state->mMainThreadObservers.IsEmpty();
+ state->mIsObservingVsync = needVsync;
+ vsyncSource = state->mCurrentVsyncSource;
+ }
+
+ // Call Add/RemoveVsyncDispatcher outside the lock, because it can re-enter
+ // into VsyncDispatcher::NotifyVsync.
+ if (needVsync && !wasObservingVsync) {
+ vsyncSource->AddVsyncDispatcher(this);
+ } else if (!needVsync && wasObservingVsync) {
+ vsyncSource->RemoveVsyncDispatcher(this);
+ }
+}
+
+} // namespace mozilla
diff --git a/widget/VsyncDispatcher.h b/widget/VsyncDispatcher.h
new file mode 100644
index 0000000000..1001a74888
--- /dev/null
+++ b/widget/VsyncDispatcher.h
@@ -0,0 +1,157 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_VsyncDispatcher_h
+#define mozilla_widget_VsyncDispatcher_h
+
+#include "mozilla/DataMutex.h"
+#include "mozilla/TimeStamp.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "mozilla/RefPtr.h"
+#include "VsyncSource.h"
+
+namespace mozilla {
+
+class VsyncObserver {
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ public:
+ // The method is called when a vsync occurs.
+ // In general, this vsync notification will occur on the hardware vsync
+ // thread from VsyncSource. But it might also be called on PVsync ipc thread
+ // if this notification is cross process. Thus all observer should check the
+ // thread model before handling the real task.
+ virtual void NotifyVsync(const VsyncEvent& aVsync) = 0;
+
+ protected:
+ VsyncObserver() = default;
+ virtual ~VsyncObserver() = default;
+}; // VsyncObserver
+
+class VsyncDispatcher;
+
+// Used to dispatch vsync events in the parent process to compositors.
+//
+// When the compositor is in-process, CompositorWidgets own a
+// CompositorVsyncDispatcher, and directly attach the compositor's observer
+// to it.
+//
+// When the compositor is out-of-process, the CompositorWidgetDelegate owns
+// the vsync dispatcher instead. The widget receives vsync observer/unobserve
+// commands via IPDL, and uses this to attach a CompositorWidgetVsyncObserver.
+// This observer forwards vsync notifications (on the vsync thread) to a
+// dedicated vsync I/O thread, which then forwards the notification to the
+// compositor thread in the compositor process.
+class CompositorVsyncDispatcher final : public VsyncObserver {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorVsyncDispatcher, override)
+
+ public:
+ explicit CompositorVsyncDispatcher(RefPtr<VsyncDispatcher> aVsyncDispatcher);
+
+ // Called on the vsync thread when a hardware vsync occurs
+ void NotifyVsync(const VsyncEvent& aVsync) override;
+
+ // Compositor vsync observers must be added/removed on the compositor thread
+ void SetCompositorVsyncObserver(VsyncObserver* aVsyncObserver);
+ void Shutdown();
+
+ private:
+ virtual ~CompositorVsyncDispatcher();
+ void ObserveVsync(bool aEnable);
+
+ RefPtr<VsyncDispatcher> mVsyncDispatcher;
+ Mutex mCompositorObserverLock MOZ_UNANNOTATED;
+ RefPtr<VsyncObserver> mCompositorVsyncObserver;
+ bool mDidShutdown;
+};
+
+// Dispatch vsync events to various observers. This is used by:
+// - CompositorVsyncDispatcher
+// - Parent process refresh driver timers
+// - IPC for content process refresh driver timers (VsyncParent <->
+// VsyncMainChild)
+// - IPC for content process worker requestAnimationFrame (VsyncParent <->
+// VsyncWorkerChild)
+//
+// This class is only used in the parent process.
+// There is one global vsync dispatcher which is managed by gfxPlatform.
+// On Linux Wayland, there is also one vsync source and vsync dispatcher per
+// widget.
+// A vsync dispatcher can swap out its underlying VsyncSource. This happens, for
+// example, when the layout.frame_rate pref is modified, causing us to switch
+// between hardware vsync and software vsync.
+class VsyncDispatcher final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VsyncDispatcher)
+
+ public:
+ explicit VsyncDispatcher(gfx::VsyncSource* aVsyncSource);
+
+ // Please check CompositorVsyncDispatcher::NotifyVsync().
+ void NotifyVsync(const VsyncEvent& aVsync);
+
+ // Swap out the underlying vsync source. Can be called on any thread.
+ // aVsyncSource must be non-null.
+ void SetVsyncSource(gfx::VsyncSource* aVsyncSource);
+
+ // Always non-null.
+ RefPtr<gfx::VsyncSource> GetCurrentVsyncSource();
+
+ TimeDuration GetVsyncRate();
+
+ // Add a vsync observer to this dispatcher. This is a no-op if the observer is
+ // already registered. Can be called from any thread.
+ void AddVsyncObserver(VsyncObserver* aVsyncObserver);
+
+ // Remove a vsync observer from this dispatcher. This is a no-op if the
+ // observer is not registered. Can be called from any thread.
+ void RemoveVsyncObserver(VsyncObserver* aVsyncObserver);
+
+ // Add and remove an observer for vsync which can only be notified on the
+ // main thread. Note that keeping an observer registered means vsync will keep
+ // firing, which may impact power usage. So this is intended only for "short
+ // term" vsync observers.
+ // These methods must be called on the parent process main thread, and the
+ // observer will likewise be notified on the parent process main thread.
+ void AddMainThreadObserver(VsyncObserver* aObserver);
+ void RemoveMainThreadObserver(VsyncObserver* aObserver);
+
+ private:
+ virtual ~VsyncDispatcher();
+
+ // Can be called on any thread.
+ void UpdateVsyncStatus();
+
+ // Can only be called on the main thread.
+ void NotifyMainThreadObservers(VsyncEvent aEvent);
+
+ struct State {
+ explicit State(gfx::VsyncSource* aVsyncSource)
+ : mCurrentVsyncSource(aVsyncSource) {}
+ State(State&&) = default;
+ ~State() = default;
+
+ nsTArray<RefPtr<VsyncObserver>> mObservers;
+ nsTArray<RefPtr<VsyncObserver>> mMainThreadObservers;
+ VsyncId mLastVsyncIdSentToMainThread;
+ VsyncId mLastMainThreadProcessedVsyncId;
+
+ // Always non-null.
+ RefPtr<gfx::VsyncSource> mCurrentVsyncSource;
+
+ // The number of skipped vsyncs since the last non-skipped vsync.
+ // Reset to zero every n vsyncs, where n is given by the pref
+ // gfx.display.frame-rate-divisor.
+ int32_t mVsyncSkipCounter = 0;
+
+ bool mIsObservingVsync = false;
+ };
+
+ DataMutex<State> mState;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_widget_VsyncDispatcher_h
diff --git a/widget/WidgetEventImpl.cpp b/widget/WidgetEventImpl.cpp
new file mode 100644
index 0000000000..121b006f8a
--- /dev/null
+++ b/widget/WidgetEventImpl.cpp
@@ -0,0 +1,1970 @@
+/* -*- 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 "BasicEvents.h"
+#include "ContentEvents.h"
+#include "MiscEvents.h"
+#include "MouseEvents.h"
+#include "NativeKeyBindingsType.h"
+#include "TextEventDispatcher.h"
+#include "TextEvents.h"
+#include "TouchEvents.h"
+
+#include "mozilla/EventStateManager.h"
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/WheelEventBinding.h"
+#include "nsCommandParams.h"
+#include "nsContentUtils.h"
+#include "nsIContent.h"
+#include "nsIDragSession.h"
+#include "nsPrintfCString.h"
+
+#if defined(XP_WIN)
+# include "windef.h"
+# include "winnetwk.h"
+# include "npapi.h"
+# include "WinUtils.h"
+#endif // #if defined (XP_WIN)
+
+#if defined(MOZ_WIDGET_GTK) || defined(XP_MACOSX)
+# include "NativeKeyBindings.h"
+#endif // #if defined(MOZ_WIDGET_GTK) || defined(XP_MACOSX)
+
+namespace mozilla {
+
+/******************************************************************************
+ * Global helper methods
+ ******************************************************************************/
+
+const char* ToChar(EventMessage aEventMessage) {
+ switch (aEventMessage) {
+#define NS_EVENT_MESSAGE(aMessage) \
+ case aMessage: \
+ return #aMessage;
+
+#include "mozilla/EventMessageList.h"
+
+#undef NS_EVENT_MESSAGE
+ default:
+ return "illegal event message";
+ }
+}
+
+const char* ToChar(EventClassID aEventClassID) {
+ switch (aEventClassID) {
+#define NS_ROOT_EVENT_CLASS(aPrefix, aName) \
+ case eBasic##aName##Class: \
+ return "eBasic" #aName "Class";
+
+#define NS_EVENT_CLASS(aPrefix, aName) \
+ case e##aName##Class: \
+ return "e" #aName "Class";
+
+#include "mozilla/EventClassList.h"
+
+#undef NS_EVENT_CLASS
+#undef NS_ROOT_EVENT_CLASS
+ default:
+ return "illegal event class ID";
+ }
+}
+
+const nsCString ToString(KeyNameIndex aKeyNameIndex) {
+ if (aKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
+ return "USE_STRING"_ns;
+ }
+ nsAutoString keyName;
+ WidgetKeyboardEvent::GetDOMKeyName(aKeyNameIndex, keyName);
+ return NS_ConvertUTF16toUTF8(keyName);
+}
+
+const nsCString ToString(CodeNameIndex aCodeNameIndex) {
+ if (aCodeNameIndex == CODE_NAME_INDEX_USE_STRING) {
+ return "USE_STRING"_ns;
+ }
+ nsAutoString codeName;
+ WidgetKeyboardEvent::GetDOMCodeName(aCodeNameIndex, codeName);
+ return NS_ConvertUTF16toUTF8(codeName);
+}
+
+const char* ToChar(Command aCommand) {
+ if (aCommand == Command::DoNothing) {
+ return "CommandDoNothing";
+ }
+
+ switch (aCommand) {
+#define NS_DEFINE_COMMAND(aName, aCommandStr) \
+ case Command::aName: \
+ return "Command::" #aName;
+#define NS_DEFINE_COMMAND_WITH_PARAM(aName, aCommandStr, aParam) \
+ case Command::aName: \
+ return "Command::" #aName;
+#define NS_DEFINE_COMMAND_NO_EXEC_COMMAND(aName) \
+ case Command::aName: \
+ return "Command::" #aName;
+
+#include "mozilla/CommandList.h"
+
+#undef NS_DEFINE_COMMAND
+#undef NS_DEFINE_COMMAND_WITH_PARAM
+#undef NS_DEFINE_COMMAND_NO_EXEC_COMMAND
+
+ default:
+ return "illegal command value";
+ }
+}
+
+const nsCString GetDOMKeyCodeName(uint32_t aKeyCode) {
+ switch (aKeyCode) {
+#define NS_DISALLOW_SAME_KEYCODE
+#define NS_DEFINE_VK(aDOMKeyName, aDOMKeyCode) \
+ case aDOMKeyCode: \
+ return nsLiteralCString(#aDOMKeyName);
+
+#include "mozilla/VirtualKeyCodeList.h"
+
+#undef NS_DEFINE_VK
+#undef NS_DISALLOW_SAME_KEYCODE
+
+ default:
+ return nsPrintfCString("Invalid DOM keyCode (0x%08X)", aKeyCode);
+ }
+}
+
+bool IsValidRawTextRangeValue(RawTextRangeType aRawTextRangeType) {
+ switch (static_cast<TextRangeType>(aRawTextRangeType)) {
+ case TextRangeType::eUninitialized:
+ case TextRangeType::eCaret:
+ case TextRangeType::eRawClause:
+ case TextRangeType::eSelectedRawClause:
+ case TextRangeType::eConvertedClause:
+ case TextRangeType::eSelectedClause:
+ return true;
+ default:
+ return false;
+ }
+}
+
+RawTextRangeType ToRawTextRangeType(TextRangeType aTextRangeType) {
+ return static_cast<RawTextRangeType>(aTextRangeType);
+}
+
+TextRangeType ToTextRangeType(RawTextRangeType aRawTextRangeType) {
+ MOZ_ASSERT(IsValidRawTextRangeValue(aRawTextRangeType));
+ return static_cast<TextRangeType>(aRawTextRangeType);
+}
+
+const char* ToChar(TextRangeType aTextRangeType) {
+ switch (aTextRangeType) {
+ case TextRangeType::eUninitialized:
+ return "TextRangeType::eUninitialized";
+ case TextRangeType::eCaret:
+ return "TextRangeType::eCaret";
+ case TextRangeType::eRawClause:
+ return "TextRangeType::eRawClause";
+ case TextRangeType::eSelectedRawClause:
+ return "TextRangeType::eSelectedRawClause";
+ case TextRangeType::eConvertedClause:
+ return "TextRangeType::eConvertedClause";
+ case TextRangeType::eSelectedClause:
+ return "TextRangeType::eSelectedClause";
+ default:
+ return "Invalid TextRangeType";
+ }
+}
+
+SelectionType ToSelectionType(TextRangeType aTextRangeType) {
+ switch (aTextRangeType) {
+ case TextRangeType::eRawClause:
+ return SelectionType::eIMERawClause;
+ case TextRangeType::eSelectedRawClause:
+ return SelectionType::eIMESelectedRawClause;
+ case TextRangeType::eConvertedClause:
+ return SelectionType::eIMEConvertedClause;
+ case TextRangeType::eSelectedClause:
+ return SelectionType::eIMESelectedClause;
+ default:
+ MOZ_CRASH("TextRangeType is invalid");
+ return SelectionType::eNormal;
+ }
+}
+
+/******************************************************************************
+ * non class method implementation
+ ******************************************************************************/
+
+static nsTHashMap<nsDepCharHashKey, Command>* sCommandHashtable = nullptr;
+
+Command GetInternalCommand(const char* aCommandName,
+ const nsCommandParams* aCommandParams) {
+ if (!aCommandName) {
+ return Command::DoNothing;
+ }
+
+ // Special cases for "cmd_align". It's mapped to multiple internal commands
+ // with additional param. Therefore, we cannot handle it with the hashtable.
+ if (!strcmp(aCommandName, "cmd_align")) {
+ if (!aCommandParams) {
+ // Note that if this is called by EditorCommand::IsCommandEnabled(),
+ // it cannot set aCommandParams. So, don't warn in this case even though
+ // this is illegal case for DoCommandParams().
+ return Command::FormatJustify;
+ }
+ nsAutoCString cValue;
+ nsresult rv = aCommandParams->GetCString("state_attribute", cValue);
+ if (NS_FAILED(rv)) {
+ nsString value; // Avoid copying the string buffer with using nsString.
+ rv = aCommandParams->GetString("state_attribute", value);
+ if (NS_FAILED(rv)) {
+ return Command::FormatJustifyNone;
+ }
+ CopyUTF16toUTF8(value, cValue);
+ }
+ if (cValue.LowerCaseEqualsASCII("left")) {
+ return Command::FormatJustifyLeft;
+ }
+ if (cValue.LowerCaseEqualsASCII("right")) {
+ return Command::FormatJustifyRight;
+ }
+ if (cValue.LowerCaseEqualsASCII("center")) {
+ return Command::FormatJustifyCenter;
+ }
+ if (cValue.LowerCaseEqualsASCII("justify")) {
+ return Command::FormatJustifyFull;
+ }
+ if (cValue.IsEmpty()) {
+ return Command::FormatJustifyNone;
+ }
+ return Command::DoNothing;
+ }
+
+ if (!sCommandHashtable) {
+ sCommandHashtable = new nsTHashMap<nsDepCharHashKey, Command>();
+#define NS_DEFINE_COMMAND(aName, aCommandStr) \
+ sCommandHashtable->InsertOrUpdate(#aCommandStr, Command::aName);
+
+#define NS_DEFINE_COMMAND_WITH_PARAM(aName, aCommandStr, aParam)
+
+#define NS_DEFINE_COMMAND_NO_EXEC_COMMAND(aName)
+
+#include "mozilla/CommandList.h"
+
+#undef NS_DEFINE_COMMAND
+#undef NS_DEFINE_COMMAND_WITH_PARAM
+#undef NS_DEFINE_COMMAND_NO_EXEC_COMMAND
+ }
+ Command command = Command::DoNothing;
+ if (!sCommandHashtable->Get(aCommandName, &command)) {
+ return Command::DoNothing;
+ }
+ return command;
+}
+
+/******************************************************************************
+ * As*Event() implementation
+ ******************************************************************************/
+
+#define NS_ROOT_EVENT_CLASS(aPrefix, aName)
+#define NS_EVENT_CLASS(aPrefix, aName) \
+ aPrefix##aName* WidgetEvent::As##aName() { return nullptr; } \
+ \
+ const aPrefix##aName* WidgetEvent::As##aName() const { \
+ return const_cast<WidgetEvent*>(this)->As##aName(); \
+ }
+
+#include "mozilla/EventClassList.h"
+
+#undef NS_EVENT_CLASS
+#undef NS_ROOT_EVENT_CLASS
+
+/******************************************************************************
+ * mozilla::WidgetEvent
+ *
+ * Event struct type checking methods.
+ ******************************************************************************/
+
+bool WidgetEvent::IsQueryContentEvent() const {
+ return mClass == eQueryContentEventClass;
+}
+
+bool WidgetEvent::IsSelectionEvent() const {
+ return mClass == eSelectionEventClass;
+}
+
+bool WidgetEvent::IsContentCommandEvent() const {
+ return mClass == eContentCommandEventClass;
+}
+
+/******************************************************************************
+ * mozilla::WidgetEvent
+ *
+ * Event message checking methods.
+ ******************************************************************************/
+
+bool WidgetEvent::HasMouseEventMessage() const {
+ switch (mMessage) {
+ case eMouseDown:
+ case eMouseUp:
+ case eMouseClick:
+ case eMouseDoubleClick:
+ case eMouseAuxClick:
+ case eMouseEnterIntoWidget:
+ case eMouseExitFromWidget:
+ case eMouseActivate:
+ case eMouseOver:
+ case eMouseOut:
+ case eMouseHitTest:
+ case eMouseMove:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool WidgetEvent::HasDragEventMessage() const {
+ switch (mMessage) {
+ case eDragEnter:
+ case eDragOver:
+ case eDragExit:
+ case eDrag:
+ case eDragEnd:
+ case eDragStart:
+ case eDrop:
+ case eDragLeave:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/* static */
+bool WidgetEvent::IsKeyEventMessage(EventMessage aMessage) {
+ switch (aMessage) {
+ case eKeyDown:
+ case eKeyPress:
+ case eKeyUp:
+ case eAccessKeyNotFound:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool WidgetEvent::HasIMEEventMessage() const {
+ switch (mMessage) {
+ case eCompositionStart:
+ case eCompositionEnd:
+ case eCompositionUpdate:
+ case eCompositionChange:
+ case eCompositionCommitAsIs:
+ case eCompositionCommit:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/******************************************************************************
+ * mozilla::WidgetEvent
+ *
+ * Specific event checking methods.
+ ******************************************************************************/
+
+bool WidgetEvent::CanBeSentToRemoteProcess() const {
+ // If this event is explicitly marked as shouldn't be sent to remote process,
+ // just return false.
+ if (IsCrossProcessForwardingStopped()) {
+ return false;
+ }
+
+ if (mClass == eKeyboardEventClass || mClass == eWheelEventClass) {
+ return true;
+ }
+
+ switch (mMessage) {
+ case eMouseDown:
+ case eMouseUp:
+ case eMouseMove:
+ case eMouseExploreByTouch:
+ case eContextMenu:
+ case eMouseEnterIntoWidget:
+ case eMouseExitFromWidget:
+ case eMouseTouchDrag:
+ case eTouchStart:
+ case eTouchMove:
+ case eTouchEnd:
+ case eTouchCancel:
+ case eDragOver:
+ case eDragExit:
+ case eDrop:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool WidgetEvent::WillBeSentToRemoteProcess() const {
+ // This event won't be posted to remote process if it's already explicitly
+ // stopped.
+ if (IsCrossProcessForwardingStopped()) {
+ return false;
+ }
+
+ // When mOriginalTarget is nullptr, this method shouldn't be used.
+ if (NS_WARN_IF(!mOriginalTarget)) {
+ return false;
+ }
+
+ return EventStateManager::IsTopLevelRemoteTarget(
+ nsIContent::FromEventTarget(mOriginalTarget));
+}
+
+bool WidgetEvent::IsIMERelatedEvent() const {
+ return HasIMEEventMessage() || IsQueryContentEvent() || IsSelectionEvent();
+}
+
+bool WidgetEvent::IsUsingCoordinates() const {
+ const WidgetMouseEvent* mouseEvent = AsMouseEvent();
+ if (mouseEvent) {
+ return !mouseEvent->IsContextMenuKeyEvent();
+ }
+ return !HasKeyEventMessage() && !IsIMERelatedEvent() &&
+ !IsContentCommandEvent();
+}
+
+bool WidgetEvent::IsTargetedAtFocusedWindow() const {
+ const WidgetMouseEvent* mouseEvent = AsMouseEvent();
+ if (mouseEvent) {
+ return mouseEvent->IsContextMenuKeyEvent();
+ }
+ return HasKeyEventMessage() || IsIMERelatedEvent() || IsContentCommandEvent();
+}
+
+bool WidgetEvent::IsTargetedAtFocusedContent() const {
+ const WidgetMouseEvent* mouseEvent = AsMouseEvent();
+ if (mouseEvent) {
+ return mouseEvent->IsContextMenuKeyEvent();
+ }
+ return HasKeyEventMessage() || IsIMERelatedEvent();
+}
+
+bool WidgetEvent::IsAllowedToDispatchDOMEvent() const {
+ switch (mClass) {
+ case eMouseEventClass:
+ if (mMessage == eMouseTouchDrag) {
+ return false;
+ }
+ [[fallthrough]];
+ case ePointerEventClass:
+ // We want synthesized mouse moves to cause mouseover and mouseout
+ // DOM events (EventStateManager::PreHandleEvent), but not mousemove
+ // DOM events.
+ // Synthesized button up events also do not cause DOM events because they
+ // do not have a reliable mRefPoint.
+ return AsMouseEvent()->mReason == WidgetMouseEvent::eReal;
+
+ case eWheelEventClass: {
+ // wheel event whose all delta values are zero by user pref applied, it
+ // shouldn't cause a DOM event.
+ const WidgetWheelEvent* wheelEvent = AsWheelEvent();
+ return wheelEvent->mDeltaX != 0.0 || wheelEvent->mDeltaY != 0.0 ||
+ wheelEvent->mDeltaZ != 0.0;
+ }
+ case eTouchEventClass:
+ return mMessage != eTouchPointerCancel;
+ // Following events are handled in EventStateManager, so, we don't need to
+ // dispatch DOM event for them into the DOM tree.
+ case eQueryContentEventClass:
+ case eSelectionEventClass:
+ case eContentCommandEventClass:
+ return false;
+
+ default:
+ return true;
+ }
+}
+
+bool WidgetEvent::IsAllowedToDispatchInSystemGroup() const {
+ // We don't expect to implement default behaviors with pointer events because
+ // if we do, prevent default on mouse events can't prevent default behaviors
+ // anymore.
+ return mClass != ePointerEventClass;
+}
+
+bool WidgetEvent::IsBlockedForFingerprintingResistance() const {
+ switch (mClass) {
+ case eKeyboardEventClass: {
+ const WidgetKeyboardEvent* keyboardEvent = AsKeyboardEvent();
+
+ return (keyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_Alt ||
+ keyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_Shift ||
+ keyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_Control ||
+ keyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_AltGraph);
+ }
+ case ePointerEventClass: {
+ const WidgetPointerEvent* pointerEvent = AsPointerEvent();
+
+ // We suppress the pointer events if it is not primary for fingerprinting
+ // resistance. It is because of that we want to spoof any pointer event
+ // into a mouse pointer event and the mouse pointer event only has
+ // isPrimary as true.
+ return !pointerEvent->mIsPrimary;
+ }
+ default:
+ return false;
+ }
+}
+
+bool WidgetEvent::AllowFlushingPendingNotifications() const {
+ if (mClass != eQueryContentEventClass) {
+ return true;
+ }
+ // If the dispatcher does not want a flush of pending notifications, it may
+ // be caused by that it's unsafe. Therefore, we should allow handlers to
+ // flush pending things only when the dispatcher requires the latest content
+ // layout.
+ return AsQueryContentEvent()->mNeedsToFlushLayout;
+}
+
+/******************************************************************************
+ * mozilla::WidgetEvent
+ *
+ * Misc methods.
+ ******************************************************************************/
+
+static dom::EventTarget* GetTargetForDOMEvent(dom::EventTarget* aTarget) {
+ return aTarget ? aTarget->GetTargetForDOMEvent() : nullptr;
+}
+
+dom::EventTarget* WidgetEvent::GetDOMEventTarget() const {
+ return GetTargetForDOMEvent(mTarget);
+}
+
+dom::EventTarget* WidgetEvent::GetCurrentDOMEventTarget() const {
+ return GetTargetForDOMEvent(mCurrentTarget);
+}
+
+dom::EventTarget* WidgetEvent::GetOriginalDOMEventTarget() const {
+ if (mOriginalTarget) {
+ return GetTargetForDOMEvent(mOriginalTarget);
+ }
+ return GetDOMEventTarget();
+}
+
+void WidgetEvent::PreventDefault(bool aCalledByDefaultHandler,
+ nsIPrincipal* aPrincipal) {
+ if (mMessage == ePointerDown) {
+ if (aCalledByDefaultHandler) {
+ // Shouldn't prevent default on pointerdown by default handlers to stop
+ // firing legacy mouse events. Use MOZ_ASSERT to catch incorrect usages
+ // in debug builds.
+ MOZ_ASSERT(false);
+ return;
+ }
+ if (aPrincipal) {
+ nsAutoString addonId;
+ Unused << NS_WARN_IF(NS_FAILED(aPrincipal->GetAddonId(addonId)));
+ if (!addonId.IsEmpty()) {
+ // Ignore the case that it's called by a web extension.
+ return;
+ }
+ }
+ }
+ mFlags.PreventDefault(aCalledByDefaultHandler);
+}
+
+bool WidgetEvent::IsUserAction() const {
+ if (!IsTrusted()) {
+ return false;
+ }
+ // FYI: eMouseScrollEventClass and ePointerEventClass represent
+ // user action but they are synthesized events.
+ switch (mClass) {
+ case eKeyboardEventClass:
+ case eCompositionEventClass:
+ case eMouseScrollEventClass:
+ case eWheelEventClass:
+ case eGestureNotifyEventClass:
+ case eSimpleGestureEventClass:
+ case eTouchEventClass:
+ case eCommandEventClass:
+ case eContentCommandEventClass:
+ return true;
+ case eMouseEventClass:
+ case eDragEventClass:
+ case ePointerEventClass:
+ return AsMouseEvent()->IsReal();
+ default:
+ return false;
+ }
+}
+
+/******************************************************************************
+ * mozilla::WidgetInputEvent
+ ******************************************************************************/
+
+/* static */
+Modifier WidgetInputEvent::GetModifier(const nsAString& aDOMKeyName) {
+ if (aDOMKeyName.EqualsLiteral("Accel")) {
+ return AccelModifier();
+ }
+ KeyNameIndex keyNameIndex = WidgetKeyboardEvent::GetKeyNameIndex(aDOMKeyName);
+ return WidgetKeyboardEvent::GetModifierForKeyName(keyNameIndex);
+}
+
+/* static */
+Modifier WidgetInputEvent::AccelModifier() {
+ static Modifier sAccelModifier = MODIFIER_NONE;
+ if (sAccelModifier == MODIFIER_NONE) {
+ switch (StaticPrefs::ui_key_accelKey()) {
+ case dom::KeyboardEvent_Binding::DOM_VK_META:
+ case dom::KeyboardEvent_Binding::DOM_VK_WIN:
+ sAccelModifier = MODIFIER_META;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_ALT:
+ sAccelModifier = MODIFIER_ALT;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
+ sAccelModifier = MODIFIER_CONTROL;
+ break;
+ default:
+#ifdef XP_MACOSX
+ sAccelModifier = MODIFIER_META;
+#else
+ sAccelModifier = MODIFIER_CONTROL;
+#endif
+ break;
+ }
+ }
+ return sAccelModifier;
+}
+
+/******************************************************************************
+ * mozilla::WidgetMouseEventBase (MouseEvents.h)
+ ******************************************************************************/
+
+bool WidgetMouseEventBase::InputSourceSupportsHover() const {
+ switch (mInputSource) {
+ case dom::MouseEvent_Binding::MOZ_SOURCE_MOUSE:
+ case dom::MouseEvent_Binding::MOZ_SOURCE_PEN:
+ case dom::MouseEvent_Binding::MOZ_SOURCE_ERASER:
+ return true;
+ case dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH:
+ case dom::MouseEvent_Binding::MOZ_SOURCE_UNKNOWN:
+ case dom::MouseEvent_Binding::MOZ_SOURCE_KEYBOARD:
+ case dom::MouseEvent_Binding::MOZ_SOURCE_CURSOR:
+ default:
+ return false;
+ }
+}
+
+/******************************************************************************
+ * mozilla::WidgetMouseEvent (MouseEvents.h)
+ ******************************************************************************/
+
+/* static */
+bool WidgetMouseEvent::IsMiddleClickPasteEnabled() {
+ return Preferences::GetBool("middlemouse.paste", false);
+}
+
+#ifdef DEBUG
+void WidgetMouseEvent::AssertContextMenuEventButtonConsistency() const {
+ if (mMessage != eContextMenu) {
+ return;
+ }
+
+ if (mContextMenuTrigger == eNormal) {
+ NS_WARNING_ASSERTION(mButton == MouseButton::eSecondary,
+ "eContextMenu events with eNormal trigger should use "
+ "secondary mouse button");
+ } else {
+ NS_WARNING_ASSERTION(mButton == MouseButton::ePrimary,
+ "eContextMenu events with non-eNormal trigger should "
+ "use primary mouse button");
+ }
+
+ if (mContextMenuTrigger == eControlClick) {
+ NS_WARNING_ASSERTION(IsControl(),
+ "eContextMenu events with eControlClick trigger "
+ "should return true from IsControl()");
+ }
+}
+#endif
+
+/******************************************************************************
+ * mozilla::WidgetDragEvent (MouseEvents.h)
+ ******************************************************************************/
+
+void WidgetDragEvent::InitDropEffectForTests() {
+ MOZ_ASSERT(mFlags.mIsSynthesizedForTests);
+
+ nsCOMPtr<nsIDragSession> session = nsContentUtils::GetDragSession();
+ if (NS_WARN_IF(!session)) {
+ return;
+ }
+
+ uint32_t effectAllowed = session->GetEffectAllowedForTests();
+ uint32_t desiredDropEffect = nsIDragService::DRAGDROP_ACTION_NONE;
+#ifdef XP_MACOSX
+ if (IsAlt()) {
+ desiredDropEffect = IsMeta() ? nsIDragService::DRAGDROP_ACTION_LINK
+ : nsIDragService::DRAGDROP_ACTION_COPY;
+ }
+#else
+ // On Linux, we know user's intention from API, but we should use
+ // same modifiers as Windows for tests because GNOME on Ubuntu use
+ // them and that makes each test simpler.
+ if (IsControl()) {
+ desiredDropEffect = IsShift() ? nsIDragService::DRAGDROP_ACTION_LINK
+ : nsIDragService::DRAGDROP_ACTION_COPY;
+ } else if (IsShift()) {
+ desiredDropEffect = nsIDragService::DRAGDROP_ACTION_MOVE;
+ }
+#endif // #ifdef XP_MACOSX #else
+ // First, use modifier state for preferring action which is explicitly
+ // specified by the synthesizer.
+ if (!(desiredDropEffect &= effectAllowed)) {
+ // Otherwise, use an action which is allowed at starting the session.
+ desiredDropEffect = effectAllowed;
+ }
+ if (desiredDropEffect & nsIDragService::DRAGDROP_ACTION_MOVE) {
+ session->SetDragAction(nsIDragService::DRAGDROP_ACTION_MOVE);
+ } else if (desiredDropEffect & nsIDragService::DRAGDROP_ACTION_COPY) {
+ session->SetDragAction(nsIDragService::DRAGDROP_ACTION_COPY);
+ } else if (desiredDropEffect & nsIDragService::DRAGDROP_ACTION_LINK) {
+ session->SetDragAction(nsIDragService::DRAGDROP_ACTION_LINK);
+ } else {
+ session->SetDragAction(nsIDragService::DRAGDROP_ACTION_NONE);
+ }
+}
+
+/******************************************************************************
+ * mozilla::WidgetWheelEvent (MouseEvents.h)
+ ******************************************************************************/
+
+/* static */
+double WidgetWheelEvent::ComputeOverriddenDelta(double aDelta,
+ bool aIsForVertical) {
+ if (!StaticPrefs::mousewheel_system_scroll_override_enabled()) {
+ return aDelta;
+ }
+ int32_t intFactor =
+ aIsForVertical
+ ? StaticPrefs::mousewheel_system_scroll_override_vertical_factor()
+ : StaticPrefs::mousewheel_system_scroll_override_horizontal_factor();
+ // Making the scroll speed slower doesn't make sense. So, ignore odd factor
+ // which is less than 1.0.
+ if (intFactor <= 100) {
+ return aDelta;
+ }
+ double factor = static_cast<double>(intFactor) / 100;
+ return aDelta * factor;
+}
+
+double WidgetWheelEvent::OverriddenDeltaX() const {
+ if (!mAllowToOverrideSystemScrollSpeed ||
+ mDeltaMode != dom::WheelEvent_Binding::DOM_DELTA_LINE ||
+ mCustomizedByUserPrefs) {
+ return mDeltaX;
+ }
+ return ComputeOverriddenDelta(mDeltaX, false);
+}
+
+double WidgetWheelEvent::OverriddenDeltaY() const {
+ if (!mAllowToOverrideSystemScrollSpeed ||
+ mDeltaMode != dom::WheelEvent_Binding::DOM_DELTA_LINE ||
+ mCustomizedByUserPrefs) {
+ return mDeltaY;
+ }
+ return ComputeOverriddenDelta(mDeltaY, true);
+}
+
+/******************************************************************************
+ * mozilla::WidgetKeyboardEvent (TextEvents.h)
+ ******************************************************************************/
+
+#define NS_DEFINE_KEYNAME(aCPPName, aDOMKeyName) (u"" aDOMKeyName),
+const char16_t* const WidgetKeyboardEvent::kKeyNames[] = {
+#include "mozilla/KeyNameList.h"
+};
+#undef NS_DEFINE_KEYNAME
+
+#define NS_DEFINE_PHYSICAL_KEY_CODE_NAME(aCPPName, aDOMCodeName) \
+ (u"" aDOMCodeName),
+const char16_t* const WidgetKeyboardEvent::kCodeNames[] = {
+#include "mozilla/PhysicalKeyCodeNameList.h"
+};
+#undef NS_DEFINE_PHYSICAL_KEY_CODE_NAME
+
+WidgetKeyboardEvent::KeyNameIndexHashtable*
+ WidgetKeyboardEvent::sKeyNameIndexHashtable = nullptr;
+WidgetKeyboardEvent::CodeNameIndexHashtable*
+ WidgetKeyboardEvent::sCodeNameIndexHashtable = nullptr;
+
+void WidgetKeyboardEvent::InitAllEditCommands(
+ const Maybe<WritingMode>& aWritingMode) {
+ // If this event is synthesized for tests, we don't need to retrieve the
+ // command via the main process. So, we don't need widget and can trust
+ // the event.
+ if (!mFlags.mIsSynthesizedForTests) {
+ // If the event was created without widget, e.g., created event in chrome
+ // script, this shouldn't execute native key bindings.
+ if (NS_WARN_IF(!mWidget)) {
+ return;
+ }
+
+ // This event should be trusted event here and we shouldn't expose native
+ // key binding information to web contents with untrusted events.
+ if (NS_WARN_IF(!IsTrusted())) {
+ return;
+ }
+
+ MOZ_ASSERT(
+ XRE_IsParentProcess(),
+ "It's too expensive to retrieve all edit commands from remote process");
+ MOZ_ASSERT(!AreAllEditCommandsInitialized(),
+ "Shouldn't be called two or more times");
+ }
+
+ DebugOnly<bool> okIgnored = InitEditCommandsFor(
+ NativeKeyBindingsType::SingleLineEditor, aWritingMode);
+ NS_WARNING_ASSERTION(okIgnored,
+ "InitEditCommandsFor(NativeKeyBindingsType::"
+ "SingleLineEditor) failed, but ignored");
+ okIgnored =
+ InitEditCommandsFor(NativeKeyBindingsType::MultiLineEditor, aWritingMode);
+ NS_WARNING_ASSERTION(okIgnored,
+ "InitEditCommandsFor(NativeKeyBindingsType::"
+ "MultiLineEditor) failed, but ignored");
+ okIgnored =
+ InitEditCommandsFor(NativeKeyBindingsType::RichTextEditor, aWritingMode);
+ NS_WARNING_ASSERTION(okIgnored,
+ "InitEditCommandsFor(NativeKeyBindingsType::"
+ "RichTextEditor) failed, but ignored");
+}
+
+bool WidgetKeyboardEvent::InitEditCommandsFor(
+ NativeKeyBindingsType aType, const Maybe<WritingMode>& aWritingMode) {
+ bool& initialized = IsEditCommandsInitializedRef(aType);
+ if (initialized) {
+ return true;
+ }
+ nsTArray<CommandInt>& commands = EditCommandsRef(aType);
+
+ // If this event is synthesized for tests, we shouldn't access customized
+ // shortcut settings of the environment. Therefore, we don't need to check
+ // whether `widget` is set or not. And we can treat synthesized events are
+ // always trusted.
+ if (mFlags.mIsSynthesizedForTests) {
+ MOZ_DIAGNOSTIC_ASSERT(IsTrusted());
+#if defined(MOZ_WIDGET_GTK) || defined(XP_MACOSX)
+ // TODO: We should implement `NativeKeyBindings` for Windows and Android
+ // too in bug 1301497 for getting rid of the #if.
+ widget::NativeKeyBindings::GetEditCommandsForTests(aType, *this,
+ aWritingMode, commands);
+#endif
+ initialized = true;
+ return true;
+ }
+
+ if (NS_WARN_IF(!mWidget) || NS_WARN_IF(!IsTrusted())) {
+ return false;
+ }
+ // `nsIWidget::GetEditCommands()` will retrieve `WritingMode` at selection
+ // again, but it should be almost zero-cost since `TextEventDispatcher`
+ // caches the value.
+ nsCOMPtr<nsIWidget> widget = mWidget;
+ initialized = widget->GetEditCommands(aType, *this, commands);
+ return initialized;
+}
+
+bool WidgetKeyboardEvent::ExecuteEditCommands(NativeKeyBindingsType aType,
+ DoCommandCallback aCallback,
+ void* aCallbackData) {
+ // If the event was created without widget, e.g., created event in chrome
+ // script, this shouldn't execute native key bindings.
+ if (NS_WARN_IF(!mWidget)) {
+ return false;
+ }
+
+ // This event should be trusted event here and we shouldn't expose native
+ // key binding information to web contents with untrusted events.
+ if (NS_WARN_IF(!IsTrusted())) {
+ return false;
+ }
+
+ if (!IsEditCommandsInitializedRef(aType)) {
+ Maybe<WritingMode> writingMode;
+ if (RefPtr<widget::TextEventDispatcher> textEventDispatcher =
+ mWidget->GetTextEventDispatcher()) {
+ writingMode = textEventDispatcher->MaybeQueryWritingModeAtSelection();
+ }
+ if (NS_WARN_IF(!InitEditCommandsFor(aType, writingMode))) {
+ return false;
+ }
+ }
+
+ const nsTArray<CommandInt>& commands = EditCommandsRef(aType);
+ if (commands.IsEmpty()) {
+ return false;
+ }
+
+ for (CommandInt command : commands) {
+ aCallback(static_cast<Command>(command), aCallbackData);
+ }
+ return true;
+}
+
+bool WidgetKeyboardEvent::ShouldCauseKeypressEvents() const {
+ // Currently, we don't dispatch keypress events of modifier keys and
+ // dead keys.
+ switch (mKeyNameIndex) {
+ case KEY_NAME_INDEX_Alt:
+ case KEY_NAME_INDEX_AltGraph:
+ case KEY_NAME_INDEX_CapsLock:
+ case KEY_NAME_INDEX_Control:
+ case KEY_NAME_INDEX_Fn:
+ case KEY_NAME_INDEX_FnLock:
+ // case KEY_NAME_INDEX_Hyper:
+ case KEY_NAME_INDEX_Meta:
+ case KEY_NAME_INDEX_NumLock:
+ case KEY_NAME_INDEX_ScrollLock:
+ case KEY_NAME_INDEX_Shift:
+ // case KEY_NAME_INDEX_Super:
+ case KEY_NAME_INDEX_Symbol:
+ case KEY_NAME_INDEX_SymbolLock:
+ case KEY_NAME_INDEX_Dead:
+ return false;
+ default:
+ return true;
+ }
+}
+
+static bool HasASCIIDigit(const ShortcutKeyCandidateArray& aCandidates) {
+ for (uint32_t i = 0; i < aCandidates.Length(); ++i) {
+ uint32_t ch = aCandidates[i].mCharCode;
+ if (ch >= '0' && ch <= '9') return true;
+ }
+ return false;
+}
+
+static bool CharsCaseInsensitiveEqual(uint32_t aChar1, uint32_t aChar2) {
+ return aChar1 == aChar2 || (IS_IN_BMP(aChar1) && IS_IN_BMP(aChar2) &&
+ ToLowerCase(static_cast<char16_t>(aChar1)) ==
+ ToLowerCase(static_cast<char16_t>(aChar2)));
+}
+
+static bool IsCaseChangeableChar(uint32_t aChar) {
+ return IS_IN_BMP(aChar) && ToLowerCase(static_cast<char16_t>(aChar)) !=
+ ToUpperCase(static_cast<char16_t>(aChar));
+}
+
+void WidgetKeyboardEvent::GetShortcutKeyCandidates(
+ ShortcutKeyCandidateArray& aCandidates) const {
+ MOZ_ASSERT(aCandidates.IsEmpty(), "aCandidates must be empty");
+
+ using ShiftState = ShortcutKeyCandidate::ShiftState;
+ using SkipIfEarlierHandlerDisabled =
+ ShortcutKeyCandidate::SkipIfEarlierHandlerDisabled;
+
+ // ShortcutKeyCandidate::mCharCode is a candidate charCode.
+ // ShortcutKeyCandidate::mShiftState means the mCharCode should be tried to
+ // execute a command with/without shift key state. If this is Ignorable,
+ // the shifted key state should be ignored. Otherwise, don't ignore the state.
+ // the priority of the charCodes are (shift key is not pressed):
+ // 0: PseudoCharCode()/ShiftState::MatchExactly,
+ // 1: unshiftedCharCodes[0]/ShiftState::MatchExactly,
+ // 2: unshiftedCharCodes[1]/ShiftState::MatchExactly...
+ // the priority of the charCodes are (shift key is pressed):
+ // 0: PseudoCharCode()/ShiftState::MatchExactly,
+ // 1: shiftedCharCodes[0]/ShiftState::MatchExactly,
+ // 2: shiftedCharCodes[0]/ShiftState::Ignorable,
+ // 3: shiftedCharCodes[1]/ShiftState::MatchExactly,
+ // 4: shiftedCharCodes[1]/ShiftState::Ignorable...
+ uint32_t pseudoCharCode = PseudoCharCode();
+ if (pseudoCharCode) {
+ ShortcutKeyCandidate key(pseudoCharCode, ShiftState::MatchExactly,
+ SkipIfEarlierHandlerDisabled::No);
+ aCandidates.AppendElement(key);
+ }
+
+ uint32_t len = mAlternativeCharCodes.Length();
+ if (!IsShift()) {
+ for (uint32_t i = 0; i < len; ++i) {
+ uint32_t ch = mAlternativeCharCodes[i].mUnshiftedCharCode;
+ if (!ch || ch == pseudoCharCode) {
+ continue;
+ }
+ ShortcutKeyCandidate key(ch, ShiftState::MatchExactly,
+ SkipIfEarlierHandlerDisabled::No);
+ aCandidates.AppendElement(key);
+ }
+ // If unshiftedCharCodes doesn't have numeric but shiftedCharCode has it,
+ // this keyboard layout is AZERTY or similar layout, probably.
+ // In this case, Accel+[0-9] should be accessible without shift key.
+ // However, the priority should be lowest.
+ if (!HasASCIIDigit(aCandidates)) {
+ for (uint32_t i = 0; i < len; ++i) {
+ uint32_t ch = mAlternativeCharCodes[i].mShiftedCharCode;
+ if (ch >= '0' && ch <= '9') {
+ ShortcutKeyCandidate key(
+ ch, ShiftState::MatchExactly,
+ // Ctrl + `-` in the French keyboard layout should not match with
+ // Ctrl + `6` shortcut when it's already fully zoomed out.
+ SkipIfEarlierHandlerDisabled::Yes);
+ aCandidates.AppendElement(key);
+ break;
+ }
+ }
+ }
+ } else {
+ for (uint32_t i = 0; i < len; ++i) {
+ uint32_t ch = mAlternativeCharCodes[i].mShiftedCharCode;
+ if (!ch) {
+ continue;
+ }
+
+ if (ch != pseudoCharCode) {
+ ShortcutKeyCandidate key(ch, ShiftState::MatchExactly,
+ SkipIfEarlierHandlerDisabled::No);
+ aCandidates.AppendElement(key);
+ }
+
+ // If the char is an alphabet, the shift key state should not be
+ // ignored. E.g., Ctrl+Shift+C should not execute Ctrl+C.
+
+ // And checking the charCode is same as unshiftedCharCode too.
+ // E.g., for Ctrl+Shift+(Plus of Numpad) should not run Ctrl+Plus.
+ uint32_t unshiftCh = mAlternativeCharCodes[i].mUnshiftedCharCode;
+ if (CharsCaseInsensitiveEqual(ch, unshiftCh)) {
+ continue;
+ }
+
+ // On the Hebrew keyboard layout on Windows, the unshifted char is a
+ // localized character but the shifted char is a Latin alphabet,
+ // then, we should not execute without the shift state. See bug 433192.
+ if (IsCaseChangeableChar(ch)) {
+ continue;
+ }
+
+ // Setting the alternative charCode candidates for retry without shift
+ // key state only when the shift key is pressed.
+ ShortcutKeyCandidate key(ch, ShiftState::Ignorable,
+ SkipIfEarlierHandlerDisabled::No);
+ aCandidates.AppendElement(key);
+ }
+ }
+
+ // Special case for "Space" key. With some keyboard layouts, "Space" with
+ // or without Shift key causes non-ASCII space. For such keyboard layouts,
+ // we should guarantee that the key press works as an ASCII white space key
+ // press. However, if the space key is assigned to a function key, it
+ // shouldn't work as a space key.
+ if (mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
+ mCodeNameIndex == CODE_NAME_INDEX_Space && pseudoCharCode != ' ') {
+ ShortcutKeyCandidate spaceKey(' ', ShiftState::MatchExactly,
+ SkipIfEarlierHandlerDisabled::No);
+ aCandidates.AppendElement(spaceKey);
+ }
+}
+
+void WidgetKeyboardEvent::GetAccessKeyCandidates(
+ nsTArray<uint32_t>& aCandidates) const {
+ MOZ_ASSERT(aCandidates.IsEmpty(), "aCandidates must be empty");
+
+ // return the lower cased charCode candidates for access keys.
+ // the priority of the charCodes are:
+ // 0: charCode, 1: unshiftedCharCodes[0], 2: shiftedCharCodes[0]
+ // 3: unshiftedCharCodes[1], 4: shiftedCharCodes[1],...
+ uint32_t pseudoCharCode = PseudoCharCode();
+ if (pseudoCharCode) {
+ uint32_t ch = pseudoCharCode;
+ if (IS_IN_BMP(ch)) {
+ ch = ToLowerCase(static_cast<char16_t>(ch));
+ }
+ aCandidates.AppendElement(ch);
+ }
+ for (uint32_t i = 0; i < mAlternativeCharCodes.Length(); ++i) {
+ uint32_t ch[2] = {mAlternativeCharCodes[i].mUnshiftedCharCode,
+ mAlternativeCharCodes[i].mShiftedCharCode};
+ for (uint32_t j = 0; j < 2; ++j) {
+ if (!ch[j]) {
+ continue;
+ }
+ if (IS_IN_BMP(ch[j])) {
+ ch[j] = ToLowerCase(static_cast<char16_t>(ch[j]));
+ }
+ // Don't append the charcode that was already appended.
+ if (aCandidates.IndexOf(ch[j]) == aCandidates.NoIndex) {
+ aCandidates.AppendElement(ch[j]);
+ }
+ }
+ }
+ // Special case for "Space" key. With some keyboard layouts, "Space" with
+ // or without Shift key causes non-ASCII space. For such keyboard layouts,
+ // we should guarantee that the key press works as an ASCII white space key
+ // press. However, if the space key is assigned to a function key, it
+ // shouldn't work as a space key.
+ if (mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
+ mCodeNameIndex == CODE_NAME_INDEX_Space && pseudoCharCode != ' ') {
+ aCandidates.AppendElement(' ');
+ }
+}
+
+// mask values for ui.key.chromeAccess and ui.key.contentAccess
+#define NS_MODIFIER_SHIFT 1
+#define NS_MODIFIER_CONTROL 2
+#define NS_MODIFIER_ALT 4
+#define NS_MODIFIER_META 8
+
+static Modifiers PrefFlagsToModifiers(int32_t aPrefFlags) {
+ Modifiers result = 0;
+ if (aPrefFlags & NS_MODIFIER_SHIFT) {
+ result |= MODIFIER_SHIFT;
+ }
+ if (aPrefFlags & NS_MODIFIER_CONTROL) {
+ result |= MODIFIER_CONTROL;
+ }
+ if (aPrefFlags & NS_MODIFIER_ALT) {
+ result |= MODIFIER_ALT;
+ }
+ if (aPrefFlags & NS_MODIFIER_META) {
+ result |= MODIFIER_META;
+ }
+ return result;
+}
+
+bool WidgetKeyboardEvent::ModifiersMatchWithAccessKey(
+ AccessKeyType aType) const {
+ if (!ModifiersForAccessKeyMatching()) {
+ return false;
+ }
+ return ModifiersForAccessKeyMatching() == AccessKeyModifiers(aType);
+}
+
+Modifiers WidgetKeyboardEvent::ModifiersForAccessKeyMatching() const {
+ static const Modifiers kModifierMask =
+ MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META;
+ return mModifiers & kModifierMask;
+}
+
+/* static */
+Modifiers WidgetKeyboardEvent::AccessKeyModifiers(AccessKeyType aType) {
+ switch (StaticPrefs::ui_key_generalAccessKey()) {
+ case -1:
+ break; // use the individual prefs
+ case NS_VK_SHIFT:
+ return MODIFIER_SHIFT;
+ case NS_VK_CONTROL:
+ return MODIFIER_CONTROL;
+ case NS_VK_ALT:
+ return MODIFIER_ALT;
+ case NS_VK_META:
+ case NS_VK_WIN:
+ return MODIFIER_META;
+ default:
+ return MODIFIER_NONE;
+ }
+
+ switch (aType) {
+ case AccessKeyType::eChrome:
+ return PrefFlagsToModifiers(StaticPrefs::ui_key_chromeAccess());
+ case AccessKeyType::eContent:
+ return PrefFlagsToModifiers(StaticPrefs::ui_key_contentAccess());
+ default:
+ return MODIFIER_NONE;
+ }
+}
+
+/* static */
+void WidgetKeyboardEvent::Shutdown() {
+ delete sKeyNameIndexHashtable;
+ sKeyNameIndexHashtable = nullptr;
+ delete sCodeNameIndexHashtable;
+ sCodeNameIndexHashtable = nullptr;
+ // Although sCommandHashtable is not a member of WidgetKeyboardEvent, but
+ // let's delete it here since we need to do it at same time.
+ delete sCommandHashtable;
+ sCommandHashtable = nullptr;
+}
+
+/* static */
+void WidgetKeyboardEvent::GetDOMKeyName(KeyNameIndex aKeyNameIndex,
+ nsAString& aKeyName) {
+ if (aKeyNameIndex >= KEY_NAME_INDEX_USE_STRING) {
+ aKeyName.Truncate();
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(
+ static_cast<size_t>(aKeyNameIndex) < ArrayLength(kKeyNames),
+ "Illegal key enumeration value");
+ aKeyName = kKeyNames[aKeyNameIndex];
+}
+
+/* static */
+void WidgetKeyboardEvent::GetDOMCodeName(CodeNameIndex aCodeNameIndex,
+ nsAString& aCodeName) {
+ if (aCodeNameIndex >= CODE_NAME_INDEX_USE_STRING) {
+ aCodeName.Truncate();
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(
+ static_cast<size_t>(aCodeNameIndex) < ArrayLength(kCodeNames),
+ "Illegal physical code enumeration value");
+
+ // Generate some continuous runs of codes, rather than looking them up.
+ if (aCodeNameIndex >= CODE_NAME_INDEX_KeyA &&
+ aCodeNameIndex <= CODE_NAME_INDEX_KeyZ) {
+ uint32_t index = aCodeNameIndex - CODE_NAME_INDEX_KeyA;
+ aCodeName.AssignLiteral(u"Key");
+ aCodeName.Append(u'A' + index);
+ return;
+ }
+ if (aCodeNameIndex >= CODE_NAME_INDEX_Digit0 &&
+ aCodeNameIndex <= CODE_NAME_INDEX_Digit9) {
+ uint32_t index = aCodeNameIndex - CODE_NAME_INDEX_Digit0;
+ aCodeName.AssignLiteral(u"Digit");
+ aCodeName.AppendInt(index);
+ return;
+ }
+ if (aCodeNameIndex >= CODE_NAME_INDEX_Numpad0 &&
+ aCodeNameIndex <= CODE_NAME_INDEX_Numpad9) {
+ uint32_t index = aCodeNameIndex - CODE_NAME_INDEX_Numpad0;
+ aCodeName.AssignLiteral(u"Numpad");
+ aCodeName.AppendInt(index);
+ return;
+ }
+ if (aCodeNameIndex >= CODE_NAME_INDEX_F1 &&
+ aCodeNameIndex <= CODE_NAME_INDEX_F24) {
+ uint32_t index = aCodeNameIndex - CODE_NAME_INDEX_F1;
+ aCodeName.Assign(u'F');
+ aCodeName.AppendInt(index + 1);
+ return;
+ }
+
+ aCodeName = kCodeNames[aCodeNameIndex];
+}
+
+/* static */
+KeyNameIndex WidgetKeyboardEvent::GetKeyNameIndex(const nsAString& aKeyValue) {
+ if (!sKeyNameIndexHashtable) {
+ sKeyNameIndexHashtable = new KeyNameIndexHashtable(ArrayLength(kKeyNames));
+ for (size_t i = 0; i < ArrayLength(kKeyNames); i++) {
+ sKeyNameIndexHashtable->InsertOrUpdate(nsDependentString(kKeyNames[i]),
+ static_cast<KeyNameIndex>(i));
+ }
+ }
+ return sKeyNameIndexHashtable->MaybeGet(aKeyValue).valueOr(
+ KEY_NAME_INDEX_USE_STRING);
+}
+
+/* static */
+CodeNameIndex WidgetKeyboardEvent::GetCodeNameIndex(
+ const nsAString& aCodeValue) {
+ if (!sCodeNameIndexHashtable) {
+ sCodeNameIndexHashtable =
+ new CodeNameIndexHashtable(ArrayLength(kCodeNames));
+ for (size_t i = 0; i < ArrayLength(kCodeNames); i++) {
+ sCodeNameIndexHashtable->InsertOrUpdate(nsDependentString(kCodeNames[i]),
+ static_cast<CodeNameIndex>(i));
+ }
+ }
+ return sCodeNameIndexHashtable->MaybeGet(aCodeValue)
+ .valueOr(CODE_NAME_INDEX_USE_STRING);
+}
+
+/* static */
+uint32_t WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(
+ CodeNameIndex aCodeNameIndex) {
+ switch (aCodeNameIndex) {
+ case CODE_NAME_INDEX_Semicolon: // VK_OEM_1 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_SEMICOLON;
+ case CODE_NAME_INDEX_Equal: // VK_OEM_PLUS on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_EQUALS;
+ case CODE_NAME_INDEX_Comma: // VK_OEM_COMMA on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_COMMA;
+ case CODE_NAME_INDEX_Minus: // VK_OEM_MINUS on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_HYPHEN_MINUS;
+ case CODE_NAME_INDEX_Period: // VK_OEM_PERIOD on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_PERIOD;
+ case CODE_NAME_INDEX_Slash: // VK_OEM_2 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_SLASH;
+ case CODE_NAME_INDEX_Backquote: // VK_OEM_3 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_BACK_QUOTE;
+ case CODE_NAME_INDEX_BracketLeft: // VK_OEM_4 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_OPEN_BRACKET;
+ case CODE_NAME_INDEX_Backslash: // VK_OEM_5 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_BACK_SLASH;
+ case CODE_NAME_INDEX_BracketRight: // VK_OEM_6 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_CLOSE_BRACKET;
+ case CODE_NAME_INDEX_Quote: // VK_OEM_7 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_QUOTE;
+ case CODE_NAME_INDEX_IntlBackslash: // VK_OEM_5 on Windows (ABNT, etc)
+ case CODE_NAME_INDEX_IntlYen: // VK_OEM_5 on Windows (JIS)
+ case CODE_NAME_INDEX_IntlRo: // VK_OEM_102 on Windows
+ return dom::KeyboardEvent_Binding::DOM_VK_BACK_SLASH;
+ default:
+ return 0;
+ }
+}
+
+/* static */ const char* WidgetKeyboardEvent::GetCommandStr(Command aCommand) {
+#define NS_DEFINE_COMMAND(aName, aCommandStr) , #aCommandStr
+#define NS_DEFINE_COMMAND_WITH_PARAM(aName, aCommandStr, aParam) , #aCommandStr
+#define NS_DEFINE_COMMAND_NO_EXEC_COMMAND(aName) , ""
+ static const char* const kCommands[] = {
+ "" // DoNothing
+#include "mozilla/CommandList.h"
+ };
+#undef NS_DEFINE_COMMAND
+#undef NS_DEFINE_COMMAND_WITH_PARAM
+#undef NS_DEFINE_COMMAND_NO_EXEC_COMMAND
+
+ MOZ_RELEASE_ASSERT(static_cast<size_t>(aCommand) < ArrayLength(kCommands),
+ "Illegal command enumeration value");
+ return kCommands[static_cast<CommandInt>(aCommand)];
+}
+
+/* static */
+uint32_t WidgetKeyboardEvent::ComputeLocationFromCodeValue(
+ CodeNameIndex aCodeNameIndex) {
+ // Following commented out cases are not defined in PhysicalKeyCodeNameList.h
+ // but are defined by D3E spec. So, they should be uncommented when the
+ // code values are defined in the header.
+ switch (aCodeNameIndex) {
+ case CODE_NAME_INDEX_AltLeft:
+ case CODE_NAME_INDEX_ControlLeft:
+ case CODE_NAME_INDEX_MetaLeft:
+ case CODE_NAME_INDEX_ShiftLeft:
+ return eKeyLocationLeft;
+ case CODE_NAME_INDEX_AltRight:
+ case CODE_NAME_INDEX_ControlRight:
+ case CODE_NAME_INDEX_MetaRight:
+ case CODE_NAME_INDEX_ShiftRight:
+ return eKeyLocationRight;
+ case CODE_NAME_INDEX_Numpad0:
+ case CODE_NAME_INDEX_Numpad1:
+ case CODE_NAME_INDEX_Numpad2:
+ case CODE_NAME_INDEX_Numpad3:
+ case CODE_NAME_INDEX_Numpad4:
+ case CODE_NAME_INDEX_Numpad5:
+ case CODE_NAME_INDEX_Numpad6:
+ case CODE_NAME_INDEX_Numpad7:
+ case CODE_NAME_INDEX_Numpad8:
+ case CODE_NAME_INDEX_Numpad9:
+ case CODE_NAME_INDEX_NumpadAdd:
+ case CODE_NAME_INDEX_NumpadBackspace:
+ case CODE_NAME_INDEX_NumpadClear:
+ case CODE_NAME_INDEX_NumpadClearEntry:
+ case CODE_NAME_INDEX_NumpadComma:
+ case CODE_NAME_INDEX_NumpadDecimal:
+ case CODE_NAME_INDEX_NumpadDivide:
+ case CODE_NAME_INDEX_NumpadEnter:
+ case CODE_NAME_INDEX_NumpadEqual:
+ case CODE_NAME_INDEX_NumpadMemoryAdd:
+ case CODE_NAME_INDEX_NumpadMemoryClear:
+ case CODE_NAME_INDEX_NumpadMemoryRecall:
+ case CODE_NAME_INDEX_NumpadMemoryStore:
+ case CODE_NAME_INDEX_NumpadMemorySubtract:
+ case CODE_NAME_INDEX_NumpadMultiply:
+ case CODE_NAME_INDEX_NumpadParenLeft:
+ case CODE_NAME_INDEX_NumpadParenRight:
+ case CODE_NAME_INDEX_NumpadSubtract:
+ return eKeyLocationNumpad;
+ default:
+ return eKeyLocationStandard;
+ }
+}
+
+/* static */
+uint32_t WidgetKeyboardEvent::ComputeKeyCodeFromKeyNameIndex(
+ KeyNameIndex aKeyNameIndex) {
+ switch (aKeyNameIndex) {
+ case KEY_NAME_INDEX_Cancel:
+ return dom::KeyboardEvent_Binding::DOM_VK_CANCEL;
+ case KEY_NAME_INDEX_Help:
+ return dom::KeyboardEvent_Binding::DOM_VK_HELP;
+ case KEY_NAME_INDEX_Backspace:
+ return dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE;
+ case KEY_NAME_INDEX_Tab:
+ return dom::KeyboardEvent_Binding::DOM_VK_TAB;
+ case KEY_NAME_INDEX_Clear:
+ return dom::KeyboardEvent_Binding::DOM_VK_CLEAR;
+ case KEY_NAME_INDEX_Enter:
+ return dom::KeyboardEvent_Binding::DOM_VK_RETURN;
+ case KEY_NAME_INDEX_Shift:
+ return dom::KeyboardEvent_Binding::DOM_VK_SHIFT;
+ case KEY_NAME_INDEX_Control:
+ return dom::KeyboardEvent_Binding::DOM_VK_CONTROL;
+ case KEY_NAME_INDEX_Alt:
+ return dom::KeyboardEvent_Binding::DOM_VK_ALT;
+ case KEY_NAME_INDEX_Pause:
+ return dom::KeyboardEvent_Binding::DOM_VK_PAUSE;
+ case KEY_NAME_INDEX_CapsLock:
+ return dom::KeyboardEvent_Binding::DOM_VK_CAPS_LOCK;
+ case KEY_NAME_INDEX_Hiragana:
+ case KEY_NAME_INDEX_Katakana:
+ case KEY_NAME_INDEX_HiraganaKatakana:
+ case KEY_NAME_INDEX_KanaMode:
+ return dom::KeyboardEvent_Binding::DOM_VK_KANA;
+ case KEY_NAME_INDEX_HangulMode:
+ return dom::KeyboardEvent_Binding::DOM_VK_HANGUL;
+ case KEY_NAME_INDEX_Eisu:
+ return dom::KeyboardEvent_Binding::DOM_VK_EISU;
+ case KEY_NAME_INDEX_JunjaMode:
+ return dom::KeyboardEvent_Binding::DOM_VK_JUNJA;
+ case KEY_NAME_INDEX_FinalMode:
+ return dom::KeyboardEvent_Binding::DOM_VK_FINAL;
+ case KEY_NAME_INDEX_HanjaMode:
+ return dom::KeyboardEvent_Binding::DOM_VK_HANJA;
+ case KEY_NAME_INDEX_KanjiMode:
+ return dom::KeyboardEvent_Binding::DOM_VK_KANJI;
+ case KEY_NAME_INDEX_Escape:
+ return dom::KeyboardEvent_Binding::DOM_VK_ESCAPE;
+ case KEY_NAME_INDEX_Convert:
+ return dom::KeyboardEvent_Binding::DOM_VK_CONVERT;
+ case KEY_NAME_INDEX_NonConvert:
+ return dom::KeyboardEvent_Binding::DOM_VK_NONCONVERT;
+ case KEY_NAME_INDEX_Accept:
+ return dom::KeyboardEvent_Binding::DOM_VK_ACCEPT;
+ case KEY_NAME_INDEX_ModeChange:
+ return dom::KeyboardEvent_Binding::DOM_VK_MODECHANGE;
+ case KEY_NAME_INDEX_PageUp:
+ return dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP;
+ case KEY_NAME_INDEX_PageDown:
+ return dom::KeyboardEvent_Binding::DOM_VK_PAGE_DOWN;
+ case KEY_NAME_INDEX_End:
+ return dom::KeyboardEvent_Binding::DOM_VK_END;
+ case KEY_NAME_INDEX_Home:
+ return dom::KeyboardEvent_Binding::DOM_VK_HOME;
+ case KEY_NAME_INDEX_ArrowLeft:
+ return dom::KeyboardEvent_Binding::DOM_VK_LEFT;
+ case KEY_NAME_INDEX_ArrowUp:
+ return dom::KeyboardEvent_Binding::DOM_VK_UP;
+ case KEY_NAME_INDEX_ArrowRight:
+ return dom::KeyboardEvent_Binding::DOM_VK_RIGHT;
+ case KEY_NAME_INDEX_ArrowDown:
+ return dom::KeyboardEvent_Binding::DOM_VK_DOWN;
+ case KEY_NAME_INDEX_Select:
+ return dom::KeyboardEvent_Binding::DOM_VK_SELECT;
+ case KEY_NAME_INDEX_Print:
+ return dom::KeyboardEvent_Binding::DOM_VK_PRINT;
+ case KEY_NAME_INDEX_Execute:
+ return dom::KeyboardEvent_Binding::DOM_VK_EXECUTE;
+ case KEY_NAME_INDEX_PrintScreen:
+ return dom::KeyboardEvent_Binding::DOM_VK_PRINTSCREEN;
+ case KEY_NAME_INDEX_Insert:
+ return dom::KeyboardEvent_Binding::DOM_VK_INSERT;
+ case KEY_NAME_INDEX_Delete:
+ return dom::KeyboardEvent_Binding::DOM_VK_DELETE;
+ case KEY_NAME_INDEX_ContextMenu:
+ return dom::KeyboardEvent_Binding::DOM_VK_CONTEXT_MENU;
+ case KEY_NAME_INDEX_Standby:
+ return dom::KeyboardEvent_Binding::DOM_VK_SLEEP;
+ case KEY_NAME_INDEX_F1:
+ return dom::KeyboardEvent_Binding::DOM_VK_F1;
+ case KEY_NAME_INDEX_F2:
+ return dom::KeyboardEvent_Binding::DOM_VK_F2;
+ case KEY_NAME_INDEX_F3:
+ return dom::KeyboardEvent_Binding::DOM_VK_F3;
+ case KEY_NAME_INDEX_F4:
+ return dom::KeyboardEvent_Binding::DOM_VK_F4;
+ case KEY_NAME_INDEX_F5:
+ return dom::KeyboardEvent_Binding::DOM_VK_F5;
+ case KEY_NAME_INDEX_F6:
+ return dom::KeyboardEvent_Binding::DOM_VK_F6;
+ case KEY_NAME_INDEX_F7:
+ return dom::KeyboardEvent_Binding::DOM_VK_F7;
+ case KEY_NAME_INDEX_F8:
+ return dom::KeyboardEvent_Binding::DOM_VK_F8;
+ case KEY_NAME_INDEX_F9:
+ return dom::KeyboardEvent_Binding::DOM_VK_F9;
+ case KEY_NAME_INDEX_F10:
+ return dom::KeyboardEvent_Binding::DOM_VK_F10;
+ case KEY_NAME_INDEX_F11:
+ return dom::KeyboardEvent_Binding::DOM_VK_F11;
+ case KEY_NAME_INDEX_F12:
+ return dom::KeyboardEvent_Binding::DOM_VK_F12;
+ case KEY_NAME_INDEX_F13:
+ return dom::KeyboardEvent_Binding::DOM_VK_F13;
+ case KEY_NAME_INDEX_F14:
+ return dom::KeyboardEvent_Binding::DOM_VK_F14;
+ case KEY_NAME_INDEX_F15:
+ return dom::KeyboardEvent_Binding::DOM_VK_F15;
+ case KEY_NAME_INDEX_F16:
+ return dom::KeyboardEvent_Binding::DOM_VK_F16;
+ case KEY_NAME_INDEX_F17:
+ return dom::KeyboardEvent_Binding::DOM_VK_F17;
+ case KEY_NAME_INDEX_F18:
+ return dom::KeyboardEvent_Binding::DOM_VK_F18;
+ case KEY_NAME_INDEX_F19:
+ return dom::KeyboardEvent_Binding::DOM_VK_F19;
+ case KEY_NAME_INDEX_F20:
+ return dom::KeyboardEvent_Binding::DOM_VK_F20;
+ case KEY_NAME_INDEX_F21:
+ return dom::KeyboardEvent_Binding::DOM_VK_F21;
+ case KEY_NAME_INDEX_F22:
+ return dom::KeyboardEvent_Binding::DOM_VK_F22;
+ case KEY_NAME_INDEX_F23:
+ return dom::KeyboardEvent_Binding::DOM_VK_F23;
+ case KEY_NAME_INDEX_F24:
+ return dom::KeyboardEvent_Binding::DOM_VK_F24;
+ case KEY_NAME_INDEX_NumLock:
+ return dom::KeyboardEvent_Binding::DOM_VK_NUM_LOCK;
+ case KEY_NAME_INDEX_ScrollLock:
+ return dom::KeyboardEvent_Binding::DOM_VK_SCROLL_LOCK;
+ case KEY_NAME_INDEX_AudioVolumeMute:
+ return dom::KeyboardEvent_Binding::DOM_VK_VOLUME_MUTE;
+ case KEY_NAME_INDEX_AudioVolumeDown:
+ return dom::KeyboardEvent_Binding::DOM_VK_VOLUME_DOWN;
+ case KEY_NAME_INDEX_AudioVolumeUp:
+ return dom::KeyboardEvent_Binding::DOM_VK_VOLUME_UP;
+ case KEY_NAME_INDEX_Meta:
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+ return dom::KeyboardEvent_Binding::DOM_VK_WIN;
+#else
+ return dom::KeyboardEvent_Binding::DOM_VK_META;
+#endif
+ case KEY_NAME_INDEX_AltGraph:
+ return dom::KeyboardEvent_Binding::DOM_VK_ALTGR;
+ case KEY_NAME_INDEX_Process:
+ return dom::KeyboardEvent_Binding::DOM_VK_PROCESSKEY;
+ case KEY_NAME_INDEX_Attn:
+ return dom::KeyboardEvent_Binding::DOM_VK_ATTN;
+ case KEY_NAME_INDEX_CrSel:
+ return dom::KeyboardEvent_Binding::DOM_VK_CRSEL;
+ case KEY_NAME_INDEX_ExSel:
+ return dom::KeyboardEvent_Binding::DOM_VK_EXSEL;
+ case KEY_NAME_INDEX_EraseEof:
+ return dom::KeyboardEvent_Binding::DOM_VK_EREOF;
+ case KEY_NAME_INDEX_Play:
+ return dom::KeyboardEvent_Binding::DOM_VK_PLAY;
+ case KEY_NAME_INDEX_ZoomToggle:
+ case KEY_NAME_INDEX_ZoomIn:
+ case KEY_NAME_INDEX_ZoomOut:
+ return dom::KeyboardEvent_Binding::DOM_VK_ZOOM;
+ default:
+ return 0;
+ }
+}
+
+/* static */
+CodeNameIndex WidgetKeyboardEvent::ComputeCodeNameIndexFromKeyNameIndex(
+ KeyNameIndex aKeyNameIndex, const Maybe<uint32_t>& aLocation) {
+ if (aLocation.isSome() &&
+ aLocation.value() ==
+ dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_NUMPAD) {
+ // On macOS, NumLock is not supported. Therefore, this handles
+ // control key values except "Enter" only on non-macOS platforms.
+ switch (aKeyNameIndex) {
+#ifndef XP_MACOSX
+ case KEY_NAME_INDEX_Insert:
+ return CODE_NAME_INDEX_Numpad0;
+ case KEY_NAME_INDEX_End:
+ return CODE_NAME_INDEX_Numpad1;
+ case KEY_NAME_INDEX_ArrowDown:
+ return CODE_NAME_INDEX_Numpad2;
+ case KEY_NAME_INDEX_PageDown:
+ return CODE_NAME_INDEX_Numpad3;
+ case KEY_NAME_INDEX_ArrowLeft:
+ return CODE_NAME_INDEX_Numpad4;
+ case KEY_NAME_INDEX_Clear:
+ // FYI: "Clear" on macOS should be DOM_KEY_LOCATION_STANDARD.
+ return CODE_NAME_INDEX_Numpad5;
+ case KEY_NAME_INDEX_ArrowRight:
+ return CODE_NAME_INDEX_Numpad6;
+ case KEY_NAME_INDEX_Home:
+ return CODE_NAME_INDEX_Numpad7;
+ case KEY_NAME_INDEX_ArrowUp:
+ return CODE_NAME_INDEX_Numpad8;
+ case KEY_NAME_INDEX_PageUp:
+ return CODE_NAME_INDEX_Numpad9;
+ case KEY_NAME_INDEX_Delete:
+ return CODE_NAME_INDEX_NumpadDecimal;
+#endif // #ifndef XP_MACOSX
+ case KEY_NAME_INDEX_Enter:
+ return CODE_NAME_INDEX_NumpadEnter;
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+ }
+
+ if (WidgetKeyboardEvent::IsLeftOrRightModiferKeyNameIndex(aKeyNameIndex)) {
+ if (aLocation.isSome() &&
+ (aLocation.value() !=
+ dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_LEFT &&
+ aLocation.value() !=
+ dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_RIGHT)) {
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+ bool isRight =
+ aLocation.isSome() &&
+ aLocation.value() == dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_RIGHT;
+ switch (aKeyNameIndex) {
+ case KEY_NAME_INDEX_Alt:
+ return isRight ? CODE_NAME_INDEX_AltRight : CODE_NAME_INDEX_AltLeft;
+ case KEY_NAME_INDEX_Control:
+ return isRight ? CODE_NAME_INDEX_ControlRight
+ : CODE_NAME_INDEX_ControlLeft;
+ case KEY_NAME_INDEX_Shift:
+ return isRight ? CODE_NAME_INDEX_ShiftRight : CODE_NAME_INDEX_ShiftLeft;
+ case KEY_NAME_INDEX_Meta:
+ return isRight ? CODE_NAME_INDEX_MetaRight : CODE_NAME_INDEX_MetaLeft;
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+ }
+
+ if (aLocation.isSome() &&
+ aLocation.value() !=
+ dom::KeyboardEvent_Binding::DOM_KEY_LOCATION_STANDARD) {
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+
+ switch (aKeyNameIndex) {
+ // Standard section:
+ case KEY_NAME_INDEX_Escape:
+ return CODE_NAME_INDEX_Escape;
+ case KEY_NAME_INDEX_Tab:
+ return CODE_NAME_INDEX_Tab;
+ case KEY_NAME_INDEX_CapsLock:
+ return CODE_NAME_INDEX_CapsLock;
+ case KEY_NAME_INDEX_ContextMenu:
+ return CODE_NAME_INDEX_ContextMenu;
+ case KEY_NAME_INDEX_Backspace:
+ return CODE_NAME_INDEX_Backspace;
+ case KEY_NAME_INDEX_Enter:
+ return CODE_NAME_INDEX_Enter;
+#ifdef XP_MACOSX
+ // Although, macOS does not fire native key event of "Fn" key, we support
+ // Fn key event if it's sent by other apps directly.
+ case KEY_NAME_INDEX_Fn:
+ return CODE_NAME_INDEX_Fn;
+#endif // #ifdef
+
+ // Arrow Pad section:
+ case KEY_NAME_INDEX_ArrowLeft:
+ return CODE_NAME_INDEX_ArrowLeft;
+ case KEY_NAME_INDEX_ArrowUp:
+ return CODE_NAME_INDEX_ArrowUp;
+ case KEY_NAME_INDEX_ArrowDown:
+ return CODE_NAME_INDEX_ArrowDown;
+ case KEY_NAME_INDEX_ArrowRight:
+ return CODE_NAME_INDEX_ArrowRight;
+
+ // Control Pad section:
+#ifndef XP_MACOSX
+ case KEY_NAME_INDEX_Insert:
+ return CODE_NAME_INDEX_Insert;
+#else
+ case KEY_NAME_INDEX_Help:
+ return CODE_NAME_INDEX_Help;
+#endif // #ifndef XP_MACOSX #else
+ case KEY_NAME_INDEX_Delete:
+ return CODE_NAME_INDEX_Delete;
+ case KEY_NAME_INDEX_Home:
+ return CODE_NAME_INDEX_Home;
+ case KEY_NAME_INDEX_End:
+ return CODE_NAME_INDEX_End;
+ case KEY_NAME_INDEX_PageUp:
+ return CODE_NAME_INDEX_PageUp;
+ case KEY_NAME_INDEX_PageDown:
+ return CODE_NAME_INDEX_PageDown;
+
+ // Function keys:
+ case KEY_NAME_INDEX_F1:
+ return CODE_NAME_INDEX_F1;
+ case KEY_NAME_INDEX_F2:
+ return CODE_NAME_INDEX_F2;
+ case KEY_NAME_INDEX_F3:
+ return CODE_NAME_INDEX_F3;
+ case KEY_NAME_INDEX_F4:
+ return CODE_NAME_INDEX_F4;
+ case KEY_NAME_INDEX_F5:
+ return CODE_NAME_INDEX_F5;
+ case KEY_NAME_INDEX_F6:
+ return CODE_NAME_INDEX_F6;
+ case KEY_NAME_INDEX_F7:
+ return CODE_NAME_INDEX_F7;
+ case KEY_NAME_INDEX_F8:
+ return CODE_NAME_INDEX_F8;
+ case KEY_NAME_INDEX_F9:
+ return CODE_NAME_INDEX_F9;
+ case KEY_NAME_INDEX_F10:
+ return CODE_NAME_INDEX_F10;
+ case KEY_NAME_INDEX_F11:
+ return CODE_NAME_INDEX_F11;
+ case KEY_NAME_INDEX_F12:
+ return CODE_NAME_INDEX_F12;
+ case KEY_NAME_INDEX_F13:
+ return CODE_NAME_INDEX_F13;
+ case KEY_NAME_INDEX_F14:
+ return CODE_NAME_INDEX_F14;
+ case KEY_NAME_INDEX_F15:
+ return CODE_NAME_INDEX_F15;
+ case KEY_NAME_INDEX_F16:
+ return CODE_NAME_INDEX_F16;
+ case KEY_NAME_INDEX_F17:
+ return CODE_NAME_INDEX_F17;
+ case KEY_NAME_INDEX_F18:
+ return CODE_NAME_INDEX_F18;
+ case KEY_NAME_INDEX_F19:
+ return CODE_NAME_INDEX_F19;
+ case KEY_NAME_INDEX_F20:
+ return CODE_NAME_INDEX_F20;
+#ifndef XP_MACOSX
+ case KEY_NAME_INDEX_F21:
+ return CODE_NAME_INDEX_F21;
+ case KEY_NAME_INDEX_F22:
+ return CODE_NAME_INDEX_F22;
+ case KEY_NAME_INDEX_F23:
+ return CODE_NAME_INDEX_F23;
+ case KEY_NAME_INDEX_F24:
+ return CODE_NAME_INDEX_F24;
+ case KEY_NAME_INDEX_Pause:
+ return CODE_NAME_INDEX_Pause;
+ case KEY_NAME_INDEX_PrintScreen:
+ return CODE_NAME_INDEX_PrintScreen;
+ case KEY_NAME_INDEX_ScrollLock:
+ return CODE_NAME_INDEX_ScrollLock;
+#endif // #ifndef XP_MACOSX
+
+ // NumLock key:
+#ifndef XP_MACOSX
+ case KEY_NAME_INDEX_NumLock:
+ return CODE_NAME_INDEX_NumLock;
+#else
+ case KEY_NAME_INDEX_Clear:
+ return CODE_NAME_INDEX_NumLock;
+#endif // #ifndef XP_MACOSX #else
+
+ // Media keys:
+ case KEY_NAME_INDEX_AudioVolumeDown:
+ return CODE_NAME_INDEX_VolumeDown;
+ case KEY_NAME_INDEX_AudioVolumeMute:
+ return CODE_NAME_INDEX_VolumeMute;
+ case KEY_NAME_INDEX_AudioVolumeUp:
+ return CODE_NAME_INDEX_VolumeUp;
+#ifndef XP_MACOSX
+ case KEY_NAME_INDEX_BrowserBack:
+ return CODE_NAME_INDEX_BrowserBack;
+ case KEY_NAME_INDEX_BrowserFavorites:
+ return CODE_NAME_INDEX_BrowserFavorites;
+ case KEY_NAME_INDEX_BrowserForward:
+ return CODE_NAME_INDEX_BrowserForward;
+ case KEY_NAME_INDEX_BrowserRefresh:
+ return CODE_NAME_INDEX_BrowserRefresh;
+ case KEY_NAME_INDEX_BrowserSearch:
+ return CODE_NAME_INDEX_BrowserSearch;
+ case KEY_NAME_INDEX_BrowserStop:
+ return CODE_NAME_INDEX_BrowserStop;
+ case KEY_NAME_INDEX_MediaPlayPause:
+ return CODE_NAME_INDEX_MediaPlayPause;
+ case KEY_NAME_INDEX_MediaStop:
+ return CODE_NAME_INDEX_MediaStop;
+ case KEY_NAME_INDEX_MediaTrackNext:
+ return CODE_NAME_INDEX_MediaTrackNext;
+ case KEY_NAME_INDEX_MediaTrackPrevious:
+ return CODE_NAME_INDEX_MediaTrackPrevious;
+ case KEY_NAME_INDEX_LaunchApplication1:
+ return CODE_NAME_INDEX_LaunchApp1;
+#endif // #ifndef XP_MACOSX
+
+ // Only Windows and GTK supports the following multimedia keys.
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+ case KEY_NAME_INDEX_BrowserHome:
+ return CODE_NAME_INDEX_BrowserHome;
+ case KEY_NAME_INDEX_LaunchApplication2:
+ return CODE_NAME_INDEX_LaunchApp2;
+#endif // #if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+
+ // Only GTK and Android supports the following multimedia keys.
+#if defined(MOZ_WIDGET_GTK) || defined(ANDROID)
+ case KEY_NAME_INDEX_Eject:
+ return CODE_NAME_INDEX_Eject;
+ case KEY_NAME_INDEX_WakeUp:
+ return CODE_NAME_INDEX_WakeUp;
+#endif // #if defined(MOZ_WIDGET_GTK) || defined(ANDROID)
+
+ // Only Windows does not support Help key (and macOS handled above).
+#if !defined(XP_WIN) && !defined(XP_MACOSX)
+ case KEY_NAME_INDEX_Help:
+ return CODE_NAME_INDEX_Help;
+#endif // #if !defined(XP_WIN) && !defined(XP_MACOSX)
+
+ // IME specific keys:
+#ifdef XP_WIN
+ case KEY_NAME_INDEX_Convert:
+ return CODE_NAME_INDEX_Convert;
+ case KEY_NAME_INDEX_NonConvert:
+ return CODE_NAME_INDEX_NonConvert;
+ case KEY_NAME_INDEX_Alphanumeric:
+ return CODE_NAME_INDEX_CapsLock;
+ case KEY_NAME_INDEX_KanaMode:
+ case KEY_NAME_INDEX_Romaji:
+ case KEY_NAME_INDEX_Katakana:
+ case KEY_NAME_INDEX_Hiragana:
+ return CODE_NAME_INDEX_KanaMode;
+ case KEY_NAME_INDEX_Hankaku:
+ case KEY_NAME_INDEX_Zenkaku:
+ case KEY_NAME_INDEX_KanjiMode:
+ return CODE_NAME_INDEX_Backquote;
+ case KEY_NAME_INDEX_HanjaMode:
+ return CODE_NAME_INDEX_Lang2;
+ case KEY_NAME_INDEX_HangulMode:
+ return CODE_NAME_INDEX_Lang1;
+#endif // #ifdef XP_WIN
+
+#ifdef MOZ_WIDGET_GTK
+ case KEY_NAME_INDEX_Convert:
+ return CODE_NAME_INDEX_Convert;
+ case KEY_NAME_INDEX_NonConvert:
+ return CODE_NAME_INDEX_NonConvert;
+ case KEY_NAME_INDEX_Alphanumeric:
+ return CODE_NAME_INDEX_CapsLock;
+ case KEY_NAME_INDEX_HiraganaKatakana:
+ return CODE_NAME_INDEX_KanaMode;
+ case KEY_NAME_INDEX_ZenkakuHankaku:
+ return CODE_NAME_INDEX_Backquote;
+#endif // #ifdef MOZ_WIDGET_GTK
+
+#ifdef ANDROID
+ case KEY_NAME_INDEX_Convert:
+ return CODE_NAME_INDEX_Convert;
+ case KEY_NAME_INDEX_NonConvert:
+ return CODE_NAME_INDEX_NonConvert;
+ case KEY_NAME_INDEX_HiraganaKatakana:
+ return CODE_NAME_INDEX_KanaMode;
+ case KEY_NAME_INDEX_ZenkakuHankaku:
+ return CODE_NAME_INDEX_Backquote;
+ case KEY_NAME_INDEX_Eisu:
+ return CODE_NAME_INDEX_Lang2;
+ case KEY_NAME_INDEX_KanjiMode:
+ return CODE_NAME_INDEX_Lang1;
+#endif // #ifdef ANDROID
+
+#ifdef XP_MACOSX
+ case KEY_NAME_INDEX_Eisu:
+ return CODE_NAME_INDEX_Lang2;
+ case KEY_NAME_INDEX_KanjiMode:
+ return CODE_NAME_INDEX_Lang1;
+#endif // #ifdef XP_MACOSX
+
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+}
+
+/* static */
+Modifier WidgetKeyboardEvent::GetModifierForKeyName(
+ KeyNameIndex aKeyNameIndex) {
+ switch (aKeyNameIndex) {
+ case KEY_NAME_INDEX_Alt:
+ return MODIFIER_ALT;
+ case KEY_NAME_INDEX_AltGraph:
+ return MODIFIER_ALTGRAPH;
+ case KEY_NAME_INDEX_CapsLock:
+ return MODIFIER_CAPSLOCK;
+ case KEY_NAME_INDEX_Control:
+ return MODIFIER_CONTROL;
+ case KEY_NAME_INDEX_Fn:
+ return MODIFIER_FN;
+ case KEY_NAME_INDEX_FnLock:
+ return MODIFIER_FNLOCK;
+ // case KEY_NAME_INDEX_Hyper:
+ case KEY_NAME_INDEX_Meta:
+ return MODIFIER_META;
+ case KEY_NAME_INDEX_NumLock:
+ return MODIFIER_NUMLOCK;
+ case KEY_NAME_INDEX_ScrollLock:
+ return MODIFIER_SCROLLLOCK;
+ case KEY_NAME_INDEX_Shift:
+ return MODIFIER_SHIFT;
+ // case KEY_NAME_INDEX_Super:
+ case KEY_NAME_INDEX_Symbol:
+ return MODIFIER_SYMBOL;
+ case KEY_NAME_INDEX_SymbolLock:
+ return MODIFIER_SYMBOLLOCK;
+ default:
+ return MODIFIER_NONE;
+ }
+}
+
+/* static */
+bool WidgetKeyboardEvent::IsLockableModifier(KeyNameIndex aKeyNameIndex) {
+ switch (aKeyNameIndex) {
+ case KEY_NAME_INDEX_CapsLock:
+ case KEY_NAME_INDEX_FnLock:
+ case KEY_NAME_INDEX_NumLock:
+ case KEY_NAME_INDEX_ScrollLock:
+ case KEY_NAME_INDEX_SymbolLock:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/******************************************************************************
+ * mozilla::InternalEditorInputEvent (TextEvents.h)
+ ******************************************************************************/
+
+#define NS_DEFINE_INPUTTYPE(aCPPName, aDOMName) (u"" aDOMName),
+const char16_t* const InternalEditorInputEvent::kInputTypeNames[] = {
+#include "mozilla/InputTypeList.h"
+};
+#undef NS_DEFINE_INPUTTYPE
+
+InternalEditorInputEvent::InputTypeHashtable*
+ InternalEditorInputEvent::sInputTypeHashtable = nullptr;
+
+/* static */
+void InternalEditorInputEvent::Shutdown() {
+ delete sInputTypeHashtable;
+ sInputTypeHashtable = nullptr;
+}
+
+/* static */
+void InternalEditorInputEvent::GetDOMInputTypeName(EditorInputType aInputType,
+ nsAString& aInputTypeName) {
+ if (static_cast<size_t>(aInputType) >=
+ static_cast<size_t>(EditorInputType::eUnknown)) {
+ aInputTypeName.Truncate();
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(
+ static_cast<size_t>(aInputType) < ArrayLength(kInputTypeNames),
+ "Illegal input type enumeration value");
+ aInputTypeName.Assign(kInputTypeNames[static_cast<size_t>(aInputType)]);
+}
+
+/* static */
+EditorInputType InternalEditorInputEvent::GetEditorInputType(
+ const nsAString& aInputType) {
+ if (aInputType.IsEmpty()) {
+ return EditorInputType::eUnknown;
+ }
+
+ if (!sInputTypeHashtable) {
+ sInputTypeHashtable = new InputTypeHashtable(ArrayLength(kInputTypeNames));
+ for (size_t i = 0; i < ArrayLength(kInputTypeNames); i++) {
+ sInputTypeHashtable->InsertOrUpdate(nsDependentString(kInputTypeNames[i]),
+ static_cast<EditorInputType>(i));
+ }
+ }
+ return sInputTypeHashtable->MaybeGet(aInputType)
+ .valueOr(EditorInputType::eUnknown);
+}
+
+} // namespace mozilla
diff --git a/widget/WidgetMessageUtils.h b/widget/WidgetMessageUtils.h
new file mode 100644
index 0000000000..5a3cd1eb7c
--- /dev/null
+++ b/widget/WidgetMessageUtils.h
@@ -0,0 +1,76 @@
+/* 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/. */
+
+#ifndef mozilla_WidgetMessageUtils_h
+#define mozilla_WidgetMessageUtils_h
+
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/DimensionRequest.h"
+#include "mozilla/GfxMessageUtils.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/widget/ThemeChangeKind.h"
+#include "nsIWidget.h"
+#include "nsStyleConsts.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::widget::ThemeChangeKind>
+ : public BitFlagsEnumSerializer<mozilla::widget::ThemeChangeKind,
+ mozilla::widget::ThemeChangeKind::AllBits> {
+};
+
+template <>
+struct ParamTraits<mozilla::LookAndFeel::IntID>
+ : ContiguousEnumSerializer<mozilla::LookAndFeel::IntID,
+ mozilla::LookAndFeel::IntID(0),
+ mozilla::LookAndFeel::IntID::End> {
+ using IdType = std::underlying_type_t<mozilla::LookAndFeel::IntID>;
+};
+
+template <>
+struct ParamTraits<mozilla::LookAndFeel::ColorID>
+ : ContiguousEnumSerializer<mozilla::LookAndFeel::ColorID,
+ mozilla::LookAndFeel::ColorID(0),
+ mozilla::LookAndFeel::ColorID::End> {
+ using IdType = std::underlying_type_t<mozilla::LookAndFeel::ColorID>;
+};
+
+template <>
+struct ParamTraits<mozilla::widget::TransparencyMode>
+ : ContiguousEnumSerializerInclusive<
+ mozilla::widget::TransparencyMode,
+ mozilla::widget::TransparencyMode::Opaque,
+ mozilla::widget::TransparencyMode::Transparent> {};
+
+template <>
+struct ParamTraits<nsCursor>
+ : ContiguousEnumSerializer<nsCursor, eCursor_standard, eCursorCount> {};
+
+template <>
+struct ParamTraits<nsIWidget::TouchpadGesturePhase>
+ : ContiguousEnumSerializerInclusive<
+ nsIWidget::TouchpadGesturePhase,
+ nsIWidget::TouchpadGesturePhase::PHASE_BEGIN,
+ nsIWidget::TouchpadGesturePhase::PHASE_END> {};
+
+template <>
+struct ParamTraits<nsIWidget::TouchPointerState>
+ : public BitFlagsEnumSerializer<nsIWidget::TouchPointerState,
+ nsIWidget::TouchPointerState::ALL_BITS> {};
+
+template <>
+struct ParamTraits<mozilla::DimensionKind>
+ : public ContiguousEnumSerializerInclusive<mozilla::DimensionKind,
+ mozilla::DimensionKind::Inner,
+ mozilla::DimensionKind::Outer> {
+};
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::DimensionRequest, mDimensionKind, mX,
+ mY, mWidth, mHeight);
+
+} // namespace IPC
+
+#endif // WidgetMessageUtils_h
diff --git a/widget/WidgetTraceEvent.h b/widget/WidgetTraceEvent.h
new file mode 100644
index 0000000000..7f85cc91c0
--- /dev/null
+++ b/widget/WidgetTraceEvent.h
@@ -0,0 +1,27 @@
+/* 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/. */
+
+#ifndef WIDGET_PUBLIC_WIDGETTRACEEVENT_H_
+#define WIDGET_PUBLIC_WIDGETTRACEEVENT_H_
+
+namespace mozilla {
+
+// Perform any required initialization in the widget backend for
+// event tracing. Return true if initialization was successful.
+bool InitWidgetTracing();
+
+// Perform any required cleanup in the widget backend for event tracing.
+void CleanUpWidgetTracing();
+
+// Fire a tracer event at the UI-thread event loop, and block until
+// the event is processed. This should only be called by
+// a thread that's not the UI thread.
+bool FireAndWaitForTracerEvent();
+
+// Signal that the event has been received by the event loop.
+void SignalTracerThread();
+
+} // namespace mozilla
+
+#endif // WIDGET_PUBLIC_WIDGETTRACEEVENT_H_
diff --git a/widget/WidgetUtils.cpp b/widget/WidgetUtils.cpp
new file mode 100644
index 0000000000..3bafdd0a4d
--- /dev/null
+++ b/widget/WidgetUtils.cpp
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 "mozilla/WidgetUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/Components.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+#include "nsIBidiKeyboard.h"
+#include "nsIStringBundle.h"
+#include "nsTArray.h"
+#include "prenv.h"
+
+namespace mozilla {
+
+gfx::Matrix ComputeTransformForRotation(const nsIntRect& aBounds,
+ ScreenRotation aRotation) {
+ gfx::Matrix transform;
+ static const gfx::Float floatPi = static_cast<gfx::Float>(M_PI);
+
+ switch (aRotation) {
+ case ROTATION_0:
+ break;
+ case ROTATION_90:
+ transform.PreTranslate(aBounds.Width(), 0);
+ transform.PreRotate(floatPi / 2);
+ break;
+ case ROTATION_180:
+ transform.PreTranslate(aBounds.Width(), aBounds.Height());
+ transform.PreRotate(floatPi);
+ break;
+ case ROTATION_270:
+ transform.PreTranslate(0, aBounds.Height());
+ transform.PreRotate(floatPi * 3 / 2);
+ break;
+ default:
+ MOZ_CRASH("Unknown rotation");
+ }
+ return transform;
+}
+
+gfx::Matrix ComputeTransformForUnRotation(const nsIntRect& aBounds,
+ ScreenRotation aRotation) {
+ gfx::Matrix transform;
+ static const gfx::Float floatPi = static_cast<gfx::Float>(M_PI);
+
+ switch (aRotation) {
+ case ROTATION_0:
+ break;
+ case ROTATION_90:
+ transform.PreTranslate(0, aBounds.Height());
+ transform.PreRotate(floatPi * 3 / 2);
+ break;
+ case ROTATION_180:
+ transform.PreTranslate(aBounds.Width(), aBounds.Height());
+ transform.PreRotate(floatPi);
+ break;
+ case ROTATION_270:
+ transform.PreTranslate(aBounds.Width(), 0);
+ transform.PreRotate(floatPi / 2);
+ break;
+ default:
+ MOZ_CRASH("Unknown rotation");
+ }
+ return transform;
+}
+
+nsIntRect RotateRect(nsIntRect aRect, const nsIntRect& aBounds,
+ ScreenRotation aRotation) {
+ switch (aRotation) {
+ case ROTATION_0:
+ return aRect;
+ case ROTATION_90:
+ return nsIntRect(aRect.Y(), aBounds.Width() - aRect.XMost(),
+ aRect.Height(), aRect.Width());
+ case ROTATION_180:
+ return nsIntRect(aBounds.Width() - aRect.XMost(),
+ aBounds.Height() - aRect.YMost(), aRect.Width(),
+ aRect.Height());
+ case ROTATION_270:
+ return nsIntRect(aBounds.Height() - aRect.YMost(), aRect.X(),
+ aRect.Height(), aRect.Width());
+ default:
+ MOZ_CRASH("Unknown rotation");
+ }
+}
+
+namespace widget {
+
+// static
+void WidgetUtils::SendBidiKeyboardInfoToContent() {
+ nsCOMPtr<nsIBidiKeyboard> bidiKeyboard = nsContentUtils::GetBidiKeyboard();
+ if (!bidiKeyboard) {
+ return;
+ }
+
+ bool rtl;
+ if (NS_FAILED(bidiKeyboard->IsLangRTL(&rtl))) {
+ return;
+ }
+ bool bidiKeyboards = false;
+ bidiKeyboard->GetHaveBidiKeyboards(&bidiKeyboards);
+
+ nsTArray<dom::ContentParent*> children;
+ dom::ContentParent::GetAll(children);
+ for (uint32_t i = 0; i < children.Length(); i++) {
+ Unused << children[i]->SendBidiKeyboardNotify(rtl, bidiKeyboards);
+ }
+}
+
+// static
+void WidgetUtils::GetBrandShortName(nsAString& aBrandName) {
+ aBrandName.Truncate();
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ if (bundleService) {
+ bundleService->CreateBundle("chrome://branding/locale/brand.properties",
+ getter_AddRefs(bundle));
+ }
+
+ if (bundle) {
+ bundle->GetStringFromName("brandShortName", aBrandName);
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/WidgetUtils.h b/widget/WidgetUtils.h
new file mode 100644
index 0000000000..1971c3854c
--- /dev/null
+++ b/widget/WidgetUtils.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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/. */
+
+#ifndef mozilla_WidgetUtils_h
+#define mozilla_WidgetUtils_h
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/gfx/Matrix.h"
+#include "nsRect.h"
+
+class nsIWidget;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+
+// NB: these must match up with pseudo-enum in nsIScreen.idl.
+enum ScreenRotation {
+ ROTATION_0 = 0,
+ ROTATION_90,
+ ROTATION_180,
+ ROTATION_270,
+
+ ROTATION_COUNT
+};
+
+gfx::Matrix ComputeTransformForRotation(const nsIntRect& aBounds,
+ ScreenRotation aRotation);
+
+gfx::Matrix ComputeTransformForUnRotation(const nsIntRect& aBounds,
+ ScreenRotation aRotation);
+
+nsIntRect RotateRect(nsIntRect aRect, const nsIntRect& aBounds,
+ ScreenRotation aRotation);
+
+namespace widget {
+
+class WidgetUtils {
+ public:
+ /**
+ * Shutdown() is called when "xpcom-will-shutdown" is notified. This is
+ * useful when you need to observe the notification in XP level code under
+ * widget.
+ */
+ static void Shutdown();
+
+ /**
+ * Starting at the docshell item for the passed in DOM window this looks up
+ * the docshell tree until it finds a docshell item that has a widget.
+ */
+ static already_AddRefed<nsIWidget> DOMWindowToWidget(
+ nsPIDOMWindowOuter* aDOMWindow);
+
+ /**
+ * Compute our keyCode value (NS_VK_*) from an ASCII character.
+ */
+ static uint32_t ComputeKeyCodeFromChar(uint32_t aCharCode);
+
+ /**
+ * Get unshifted charCode and shifted charCode for aKeyCode if the keyboad
+ * layout is a Latin keyboard layout.
+ *
+ * @param aKeyCode Our keyCode (NS_VK_*).
+ * @param aIsCapsLock TRUE if CapsLock is Locked. Otherwise, FALSE.
+ * This is used only when aKeyCode is NS_VK_[0-9].
+ * @param aUnshiftedCharCode CharCode for aKeyCode without Shift key.
+ * This may be zero if aKeyCode key doesn't input
+ * a Latin character.
+ * Note that must not be nullptr.
+ * @param aShiftedCharCode CharCode for aKeyCOde with Shift key.
+ * This is always 0 when aKeyCode isn't
+ * NS_VK_[A-Z].
+ * Note that must not be nullptr.
+ */
+ static void GetLatinCharCodeForKeyCode(uint32_t aKeyCode, bool aIsCapsLock,
+ uint32_t* aUnshiftedCharCode,
+ uint32_t* aShiftedCharCode);
+
+ /**
+ * Send bidi keyboard information to content process
+ */
+ static void SendBidiKeyboardInfoToContent();
+
+ /**
+ * Get branchShortName from string bundle
+ */
+ static void GetBrandShortName(nsAString& aBrandName);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_WidgetUtils_h
diff --git a/widget/WindowButtonType.h b/widget/WindowButtonType.h
new file mode 100644
index 0000000000..76314709df
--- /dev/null
+++ b/widget/WindowButtonType.h
@@ -0,0 +1,22 @@
+/* -*- 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/. */
+
+#ifndef mozilla_WindowButtonType_h
+#define mozilla_WindowButtonType_h
+
+#include <cstdint>
+
+namespace mozilla {
+
+enum class WindowButtonType : uint8_t {
+ Minimize,
+ Maximize, // Also covers restore.
+ Close,
+ Count,
+};
+
+}
+
+#endif
diff --git a/widget/WindowOcclusionState.h b/widget/WindowOcclusionState.h
new file mode 100644
index 0000000000..76f70d9512
--- /dev/null
+++ b/widget/WindowOcclusionState.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef widget_WindowOcclusionState_h
+#define widget_WindowOcclusionState_h
+
+namespace mozilla {
+namespace widget {
+
+// nsWindow's window occlusion state. On Windows, it is tracked by
+// WinWindowOcclusionTracker.
+enum class OcclusionState {
+ // The window's occlusion state isn't tracked (NotifyOcclusionState()) or
+ // hasn't been computed yet.
+ UNKNOWN = 0,
+ // The window IsWindowVisibleAndFullyOpaque() [1] and:
+ // - Its bounds aren't completely covered by fully opaque windows [2]
+ VISIBLE = 1,
+ // The window IsWindowVisibleAndFullyOpaque() [1], but they all:
+ // - Have bounds completely covered by fully opaque windows [2]
+ OCCLUDED = 2,
+ // The window is not IsWindowVisibleAndFullyOpaque() [1].
+ HIDDEN = 3,
+
+ kMaxValue = HIDDEN,
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_WindowOcclusionState_h
diff --git a/widget/android/AndroidAlerts.cpp b/widget/android/AndroidAlerts.cpp
new file mode 100644
index 0000000000..456dc08290
--- /dev/null
+++ b/widget/android/AndroidAlerts.cpp
@@ -0,0 +1,159 @@
+/* -*- Mode: c++; tab-width: 4; 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 "AndroidAlerts.h"
+#include "mozilla/java/GeckoRuntimeWrappers.h"
+#include "mozilla/java/WebNotificationWrappers.h"
+#include "nsIPrincipal.h"
+#include "nsIURI.h"
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(AndroidAlerts, nsIAlertsService)
+
+StaticAutoPtr<AndroidAlerts::ListenerMap> AndroidAlerts::sListenerMap;
+nsTHashMap<nsStringHashKey, java::WebNotification::GlobalRef>
+ AndroidAlerts::mNotificationsMap;
+
+NS_IMETHODIMP
+AndroidAlerts::ShowAlertNotification(
+ const nsAString& aImageUrl, const nsAString& aAlertTitle,
+ const nsAString& aAlertText, bool aAlertTextClickable,
+ const nsAString& aAlertCookie, nsIObserver* aAlertListener,
+ const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
+ const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
+ bool aRequireInteraction) {
+ MOZ_ASSERT_UNREACHABLE("Should be implemented by nsAlertsService.");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+AndroidAlerts::ShowAlert(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ return ShowPersistentNotification(u""_ns, aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
+AndroidAlerts::ShowPersistentNotification(const nsAString& aPersistentData,
+ nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ // nsAlertsService disables our alerts backend if we ever return failure
+ // here. To keep the backend enabled, we always return NS_OK even if we
+ // encounter an error here.
+ nsresult rv;
+
+ nsAutoString imageUrl;
+ rv = aAlert->GetImageURL(imageUrl);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsAutoString title;
+ rv = aAlert->GetTitle(title);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsAutoString text;
+ rv = aAlert->GetText(text);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsAutoString cookie;
+ rv = aAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsAutoString name;
+ rv = aAlert->GetName(name);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsAutoString lang;
+ rv = aAlert->GetLang(lang);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsAutoString dir;
+ rv = aAlert->GetDir(dir);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ bool requireInteraction;
+ rv = aAlert->GetRequireInteraction(&requireInteraction);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = aAlert->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsCString spec;
+ if (uri) {
+ rv = uri->GetDisplaySpec(spec);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ }
+
+ bool silent;
+ rv = aAlert->GetSilent(&silent);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ bool privateBrowsing;
+ rv = aAlert->GetInPrivateBrowsing(&privateBrowsing);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsTArray<uint32_t> vibrate;
+ rv = aAlert->GetVibrate(vibrate);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ if (aPersistentData.IsEmpty() && aAlertListener) {
+ if (!sListenerMap) {
+ sListenerMap = new ListenerMap();
+ }
+ // This will remove any observers already registered for this name.
+ sListenerMap->InsertOrUpdate(name, aAlertListener);
+ }
+
+ java::WebNotification::LocalRef notification = notification->New(
+ title, name, cookie, text, imageUrl, dir, lang, requireInteraction, spec,
+ silent, privateBrowsing, jni::IntArray::From(vibrate));
+ java::GeckoRuntime::LocalRef runtime = java::GeckoRuntime::GetInstance();
+ if (runtime != NULL) {
+ runtime->NotifyOnShow(notification);
+ }
+ mNotificationsMap.InsertOrUpdate(name, notification);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AndroidAlerts::CloseAlert(const nsAString& aAlertName, bool aContextClosed) {
+ java::WebNotification::LocalRef notification =
+ mNotificationsMap.Get(aAlertName);
+ if (!notification) {
+ return NS_OK;
+ }
+
+ java::GeckoRuntime::LocalRef runtime = java::GeckoRuntime::GetInstance();
+ if (runtime != NULL) {
+ runtime->NotifyOnClose(notification);
+ }
+ mNotificationsMap.Remove(aAlertName);
+
+ return NS_OK;
+}
+
+void AndroidAlerts::NotifyListener(const nsAString& aName, const char* aTopic,
+ const char16_t* aCookie) {
+ if (!sListenerMap) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserver> listener = sListenerMap->Get(aName);
+ if (!listener) {
+ return;
+ }
+
+ listener->Observe(nullptr, aTopic, aCookie);
+
+ if ("alertfinished"_ns.Equals(aTopic)) {
+ sListenerMap->Remove(aName);
+ mNotificationsMap.Remove(aName);
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/AndroidAlerts.h b/widget/android/AndroidAlerts.h
new file mode 100644
index 0000000000..f4a9822dc1
--- /dev/null
+++ b/widget/android/AndroidAlerts.h
@@ -0,0 +1,46 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_AndroidAlerts_h__
+#define mozilla_widget_AndroidAlerts_h__
+
+#include "nsTHashMap.h"
+#include "nsInterfaceHashtable.h"
+#include "nsCOMPtr.h"
+#include "nsHashKeys.h"
+#include "nsIAlertsService.h"
+#include "nsIObserver.h"
+
+#include "mozilla/java/WebNotificationWrappers.h"
+
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace widget {
+
+class AndroidAlerts : public nsIAlertsService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIALERTSSERVICE
+
+ AndroidAlerts() {}
+
+ static void NotifyListener(const nsAString& aName, const char* aTopic,
+ const char16_t* aCookie);
+
+ static nsTHashMap<nsStringHashKey, mozilla::java::WebNotification::GlobalRef>
+ mNotificationsMap;
+
+ protected:
+ virtual ~AndroidAlerts() { sListenerMap = nullptr; }
+
+ using ListenerMap = nsInterfaceHashtable<nsStringHashKey, nsIObserver>;
+ static StaticAutoPtr<ListenerMap> sListenerMap;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // nsAndroidAlerts_h__
diff --git a/widget/android/AndroidBridge.cpp b/widget/android/AndroidBridge.cpp
new file mode 100644
index 0000000000..024b64036d
--- /dev/null
+++ b/widget/android/AndroidBridge.cpp
@@ -0,0 +1,431 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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 <android/log.h>
+#include <dlfcn.h>
+#include <math.h>
+#include <GLES2/gl2.h>
+
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+
+#include "mozilla/Hal.h"
+#include "nsXULAppAPI.h"
+#include <prthread.h>
+#include "AndroidBridge.h"
+#include "AndroidBridgeUtilities.h"
+#include "nsAlertsUtils.h"
+#include "nsAppShell.h"
+#include "nsOSHelperAppService.h"
+#include "nsWindow.h"
+#include "mozilla/Preferences.h"
+#include "nsThreadUtils.h"
+#include "nsPresContext.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Mutex.h"
+#include "nsPrintfCString.h"
+#include "nsContentUtils.h"
+
+#include "EventDispatcher.h"
+
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "WidgetUtils.h"
+
+#include "mozilla/java/EventDispatcherWrappers.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+#include "mozilla/java/GeckoThreadWrappers.h"
+
+using namespace mozilla;
+
+AndroidBridge* AndroidBridge::sBridge = nullptr;
+static jobject sGlobalContext = nullptr;
+nsTHashMap<nsStringHashKey, nsString> AndroidBridge::sStoragePaths;
+
+jmethodID AndroidBridge::GetMethodID(JNIEnv* env, jclass jClass,
+ const char* methodName,
+ const char* methodType) {
+ jmethodID methodID = env->GetMethodID(jClass, methodName, methodType);
+ if (!methodID) {
+ ALOG(
+ ">>> FATAL JNI ERROR! GetMethodID(methodName=\"%s\", "
+ "methodType=\"%s\") failed. Did ProGuard optimize away something it "
+ "shouldn't have?",
+ methodName, methodType);
+ env->ExceptionDescribe();
+ MOZ_CRASH();
+ }
+ return methodID;
+}
+
+jmethodID AndroidBridge::GetStaticMethodID(JNIEnv* env, jclass jClass,
+ const char* methodName,
+ const char* methodType) {
+ jmethodID methodID = env->GetStaticMethodID(jClass, methodName, methodType);
+ if (!methodID) {
+ ALOG(
+ ">>> FATAL JNI ERROR! GetStaticMethodID(methodName=\"%s\", "
+ "methodType=\"%s\") failed. Did ProGuard optimize away something it "
+ "shouldn't have?",
+ methodName, methodType);
+ env->ExceptionDescribe();
+ MOZ_CRASH();
+ }
+ return methodID;
+}
+
+jfieldID AndroidBridge::GetFieldID(JNIEnv* env, jclass jClass,
+ const char* fieldName,
+ const char* fieldType) {
+ jfieldID fieldID = env->GetFieldID(jClass, fieldName, fieldType);
+ if (!fieldID) {
+ ALOG(
+ ">>> FATAL JNI ERROR! GetFieldID(fieldName=\"%s\", "
+ "fieldType=\"%s\") failed. Did ProGuard optimize away something it "
+ "shouldn't have?",
+ fieldName, fieldType);
+ env->ExceptionDescribe();
+ MOZ_CRASH();
+ }
+ return fieldID;
+}
+
+jfieldID AndroidBridge::GetStaticFieldID(JNIEnv* env, jclass jClass,
+ const char* fieldName,
+ const char* fieldType) {
+ jfieldID fieldID = env->GetStaticFieldID(jClass, fieldName, fieldType);
+ if (!fieldID) {
+ ALOG(
+ ">>> FATAL JNI ERROR! GetStaticFieldID(fieldName=\"%s\", "
+ "fieldType=\"%s\") failed. Did ProGuard optimize away something it "
+ "shouldn't have?",
+ fieldName, fieldType);
+ env->ExceptionDescribe();
+ MOZ_CRASH();
+ }
+ return fieldID;
+}
+
+void AndroidBridge::ConstructBridge() {
+ /* NSS hack -- bionic doesn't handle recursive unloads correctly,
+ * because library finalizer functions are called with the dynamic
+ * linker lock still held. This results in a deadlock when trying
+ * to call dlclose() while we're already inside dlclose().
+ * Conveniently, NSS has an env var that can prevent it from unloading.
+ */
+ putenv(const_cast<char*>("NSS_DISABLE_UNLOAD=1"));
+
+ MOZ_ASSERT(!sBridge);
+ sBridge = new AndroidBridge();
+}
+
+void AndroidBridge::DeconstructBridge() {
+ if (sBridge) {
+ delete sBridge;
+ // AndroidBridge destruction requires sBridge to still be valid,
+ // so we set sBridge to nullptr after deleting it.
+ sBridge = nullptr;
+ }
+}
+
+AndroidBridge::~AndroidBridge() {}
+
+AndroidBridge::AndroidBridge() {
+ ALOG_BRIDGE("AndroidBridge::Init");
+
+ JNIEnv* const jEnv = jni::GetGeckoThreadEnv();
+ AutoLocalJNIFrame jniFrame(jEnv);
+
+ mMessageQueue = java::GeckoThread::MsgQueue();
+ auto msgQueueClass = jni::Class::LocalRef::Adopt(
+ jEnv, jEnv->GetObjectClass(mMessageQueue.Get()));
+ // mMessageQueueNext must not be null
+ mMessageQueueNext =
+ GetMethodID(jEnv, msgQueueClass.Get(), "next", "()Landroid/os/Message;");
+ // mMessageQueueMessages may be null (e.g. due to proguard optimization)
+ mMessageQueueMessages = jEnv->GetFieldID(msgQueueClass.Get(), "mMessages",
+ "Landroid/os/Message;");
+}
+
+void AndroidBridge::Vibrate(const nsTArray<uint32_t>& aPattern) {
+ ALOG_BRIDGE("%s", __PRETTY_FUNCTION__);
+
+ uint32_t len = aPattern.Length();
+ if (!len) {
+ ALOG_BRIDGE(" invalid 0-length array");
+ return;
+ }
+
+ // It's clear if this worth special-casing, but it creates less
+ // java junk, so dodges the GC.
+ if (len == 1) {
+ jlong d = aPattern[0];
+ if (d < 0) {
+ ALOG_BRIDGE(" invalid vibration duration < 0");
+ return;
+ }
+ java::GeckoAppShell::Vibrate(d);
+ return;
+ }
+
+ // First element of the array vibrate() expects is how long to wait
+ // *before* vibrating. For us, this is always 0.
+
+ JNIEnv* const env = jni::GetGeckoThreadEnv();
+ AutoLocalJNIFrame jniFrame(env, 1);
+
+ jlongArray array = env->NewLongArray(len + 1);
+ if (!array) {
+ ALOG_BRIDGE(" failed to allocate array");
+ return;
+ }
+
+ jlong* elts = env->GetLongArrayElements(array, nullptr);
+ elts[0] = 0;
+ for (uint32_t i = 0; i < aPattern.Length(); ++i) {
+ jlong d = aPattern[i];
+ if (d < 0) {
+ ALOG_BRIDGE(" invalid vibration duration < 0");
+ env->ReleaseLongArrayElements(array, elts, JNI_ABORT);
+ return;
+ }
+ elts[i + 1] = d;
+ }
+ env->ReleaseLongArrayElements(array, elts, 0);
+
+ java::GeckoAppShell::Vibrate(jni::LongArray::Ref::From(array),
+ -1 /* don't repeat */);
+}
+
+void AndroidBridge::GetIconForExtension(const nsACString& aFileExt,
+ uint32_t aIconSize,
+ uint8_t* const aBuf) {
+ ALOG_BRIDGE("AndroidBridge::GetIconForExtension");
+ NS_ASSERTION(aBuf != nullptr,
+ "AndroidBridge::GetIconForExtension: aBuf is null!");
+ if (!aBuf) return;
+
+ auto arr = java::GeckoAppShell::GetIconForExtension(
+ NS_ConvertUTF8toUTF16(aFileExt), aIconSize);
+
+ NS_ASSERTION(
+ arr != nullptr,
+ "AndroidBridge::GetIconForExtension: Returned pixels array is null!");
+ if (!arr) return;
+
+ JNIEnv* const env = arr.Env();
+ uint32_t len = static_cast<uint32_t>(env->GetArrayLength(arr.Get()));
+ jbyte* elements = env->GetByteArrayElements(arr.Get(), 0);
+
+ uint32_t bufSize = aIconSize * aIconSize * 4;
+ NS_ASSERTION(
+ len == bufSize,
+ "AndroidBridge::GetIconForExtension: Pixels array is incomplete!");
+ if (len == bufSize) memcpy(aBuf, elements, bufSize);
+
+ env->ReleaseByteArrayElements(arr.Get(), elements, 0);
+}
+
+namespace mozilla {
+class TracerRunnable : public Runnable {
+ public:
+ TracerRunnable() : Runnable("TracerRunnable") {
+ mTracerLock = new Mutex("TracerRunnable");
+ mTracerCondVar = new CondVar(*mTracerLock, "TracerRunnable");
+ mMainThread = do_GetMainThread();
+ }
+ ~TracerRunnable() {
+ delete mTracerCondVar;
+ delete mTracerLock;
+ mTracerLock = nullptr;
+ mTracerCondVar = nullptr;
+ }
+
+ virtual nsresult Run() {
+ MutexAutoLock lock(*mTracerLock);
+ if (!AndroidBridge::Bridge()) return NS_OK;
+
+ mHasRun = true;
+ mTracerCondVar->Notify();
+ return NS_OK;
+ }
+
+ bool Fire() {
+ if (!mTracerLock || !mTracerCondVar) return false;
+ MutexAutoLock lock(*mTracerLock);
+ mHasRun = false;
+ mMainThread->Dispatch(this, NS_DISPATCH_NORMAL);
+ while (!mHasRun) mTracerCondVar->Wait();
+ return true;
+ }
+
+ void Signal() {
+ MutexAutoLock lock(*mTracerLock);
+ mHasRun = true;
+ mTracerCondVar->Notify();
+ }
+
+ private:
+ Mutex* mTracerLock;
+ CondVar* mTracerCondVar;
+ bool mHasRun;
+ nsCOMPtr<nsIThread> mMainThread;
+};
+StaticRefPtr<TracerRunnable> sTracerRunnable;
+
+bool InitWidgetTracing() {
+ if (!sTracerRunnable) sTracerRunnable = new TracerRunnable();
+ return true;
+}
+
+void CleanUpWidgetTracing() { sTracerRunnable = nullptr; }
+
+bool FireAndWaitForTracerEvent() {
+ if (sTracerRunnable) return sTracerRunnable->Fire();
+ return false;
+}
+
+void SignalTracerThread() {
+ if (sTracerRunnable) return sTracerRunnable->Signal();
+}
+
+} // namespace mozilla
+
+void AndroidBridge::GetCurrentBatteryInformation(
+ hal::BatteryInformation* aBatteryInfo) {
+ ALOG_BRIDGE("AndroidBridge::GetCurrentBatteryInformation");
+
+ // To prevent calling too many methods through JNI, the Java method returns
+ // an array of double even if we actually want a double and a boolean.
+ auto arr = java::GeckoAppShell::GetCurrentBatteryInformation();
+
+ JNIEnv* const env = arr.Env();
+ if (!arr || env->GetArrayLength(arr.Get()) != 3) {
+ return;
+ }
+
+ jdouble* info = env->GetDoubleArrayElements(arr.Get(), 0);
+
+ aBatteryInfo->level() = info[0];
+ aBatteryInfo->charging() = info[1] == 1.0f;
+ aBatteryInfo->remainingTime() = info[2];
+
+ env->ReleaseDoubleArrayElements(arr.Get(), info, 0);
+}
+
+void AndroidBridge::GetCurrentNetworkInformation(
+ hal::NetworkInformation* aNetworkInfo) {
+ ALOG_BRIDGE("AndroidBridge::GetCurrentNetworkInformation");
+
+ // To prevent calling too many methods through JNI, the Java method returns
+ // an array of double even if we actually want an integer, a boolean, and an
+ // integer.
+
+ auto arr = java::GeckoAppShell::GetCurrentNetworkInformation();
+
+ JNIEnv* const env = arr.Env();
+ if (!arr || env->GetArrayLength(arr.Get()) != 3) {
+ return;
+ }
+
+ jdouble* info = env->GetDoubleArrayElements(arr.Get(), 0);
+
+ aNetworkInfo->type() = info[0];
+ aNetworkInfo->isWifi() = info[1] == 1.0f;
+ aNetworkInfo->dhcpGateway() = info[2];
+
+ env->ReleaseDoubleArrayElements(arr.Get(), info, 0);
+}
+
+jobject AndroidBridge::GetGlobalContextRef() {
+ // The context object can change, so get a fresh copy every time.
+ auto context = java::GeckoAppShell::GetApplicationContext();
+ sGlobalContext = jni::Object::GlobalRef(context).Forget();
+ MOZ_ASSERT(sGlobalContext);
+ return sGlobalContext;
+}
+
+/* Implementation file */
+NS_IMPL_ISUPPORTS(nsAndroidBridge, nsIAndroidEventDispatcher, nsIAndroidBridge)
+
+nsAndroidBridge::nsAndroidBridge() {
+ if (jni::IsAvailable()) {
+ RefPtr<widget::EventDispatcher> dispatcher = new widget::EventDispatcher();
+ dispatcher->Attach(java::EventDispatcher::GetInstance(),
+ /* window */ nullptr);
+ mEventDispatcher = dispatcher;
+ }
+}
+
+NS_IMETHODIMP
+nsAndroidBridge::GetDispatcherByName(const char* aName,
+ nsIAndroidEventDispatcher** aResult) {
+ if (!jni::IsAvailable()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<widget::EventDispatcher> dispatcher = new widget::EventDispatcher();
+ dispatcher->Attach(java::EventDispatcher::ByName(aName),
+ /* window */ nullptr);
+ dispatcher.forget(aResult);
+ return NS_OK;
+}
+
+nsAndroidBridge::~nsAndroidBridge() {}
+
+hal::ScreenOrientation AndroidBridge::GetScreenOrientation() {
+ ALOG_BRIDGE("AndroidBridge::GetScreenOrientation");
+
+ int16_t orientation = java::GeckoAppShell::GetScreenOrientation();
+
+ return hal::ScreenOrientation(orientation);
+}
+
+uint16_t AndroidBridge::GetScreenAngle() {
+ return java::GeckoAppShell::GetScreenAngle();
+}
+
+nsresult AndroidBridge::GetProxyForURI(const nsACString& aSpec,
+ const nsACString& aScheme,
+ const nsACString& aHost,
+ const int32_t aPort,
+ nsACString& aResult) {
+ if (!jni::IsAvailable()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto jstrRet =
+ java::GeckoAppShell::GetProxyForURI(aSpec, aScheme, aHost, aPort);
+
+ if (!jstrRet) return NS_ERROR_FAILURE;
+
+ aResult = jstrRet->ToCString();
+ return NS_OK;
+}
+
+bool AndroidBridge::PumpMessageLoop() {
+ JNIEnv* const env = jni::GetGeckoThreadEnv();
+
+ if (mMessageQueueMessages) {
+ auto msg = jni::Object::LocalRef::Adopt(
+ env, env->GetObjectField(mMessageQueue.Get(), mMessageQueueMessages));
+ // if queue.mMessages is null, queue.next() will block, which we don't
+ // want. It turns out to be an order of magnitude more performant to do
+ // this extra check here and block less vs. one fewer checks here and
+ // more blocking.
+ if (!msg) {
+ return false;
+ }
+ }
+
+ auto msg = jni::Object::LocalRef::Adopt(
+ env, env->CallObjectMethod(mMessageQueue.Get(), mMessageQueueNext));
+ if (!msg) {
+ return false;
+ }
+
+ return java::GeckoThread::PumpMessageLoop(msg);
+}
diff --git a/widget/android/AndroidBridge.h b/widget/android/AndroidBridge.h
new file mode 100644
index 0000000000..f2070ef31a
--- /dev/null
+++ b/widget/android/AndroidBridge.h
@@ -0,0 +1,275 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef AndroidBridge_h__
+#define AndroidBridge_h__
+
+#include <unistd.h> // for gettid
+
+#include "nsCOMPtr.h"
+
+#include "mozilla/jni/Refs.h"
+
+#include "nsIMutableArray.h"
+#include "nsIMIMEInfo.h"
+
+#include "nsIAndroidBridge.h"
+
+#include "mozilla/jni/Utils.h"
+#include "nsTHashMap.h"
+
+// Some debug #defines
+// #define DEBUG_ANDROID_EVENTS
+// #define DEBUG_ANDROID_WIDGET
+
+namespace mozilla {
+
+class AutoLocalJNIFrame;
+
+namespace hal {
+class BatteryInformation;
+class NetworkInformation;
+enum class ScreenOrientation : uint32_t;
+} // namespace hal
+
+class AndroidBridge final {
+ public:
+ static bool IsJavaUiThread() {
+ return mozilla::jni::GetUIThreadId() == gettid();
+ }
+
+ static void ConstructBridge();
+ static void DeconstructBridge();
+
+ static AndroidBridge* Bridge() { return sBridge; }
+
+ bool GetHandlersForURL(const nsAString& aURL,
+ nsIMutableArray* handlersArray = nullptr,
+ nsIHandlerApp** aDefaultApp = nullptr,
+ const nsAString& aAction = u""_ns);
+
+ bool GetHandlersForMimeType(const nsAString& aMimeType,
+ nsIMutableArray* handlersArray = nullptr,
+ nsIHandlerApp** aDefaultApp = nullptr,
+ const nsAString& aAction = u""_ns);
+
+ void GetMimeTypeFromExtensions(const nsACString& aFileExt,
+ nsCString& aMimeType);
+ void GetExtensionFromMimeType(const nsACString& aMimeType,
+ nsACString& aFileExt);
+
+ void Vibrate(const nsTArray<uint32_t>& aPattern);
+
+ void GetIconForExtension(const nsACString& aFileExt, uint32_t aIconSize,
+ uint8_t* const aBuf);
+
+ // Returns a global reference to the Context for Fennec's Activity. The
+ // caller is responsible for ensuring this doesn't leak by calling
+ // DeleteGlobalRef() when the context is no longer needed.
+ jobject GetGlobalContextRef(void);
+
+ void GetCurrentBatteryInformation(hal::BatteryInformation* aBatteryInfo);
+
+ void GetCurrentNetworkInformation(hal::NetworkInformation* aNetworkInfo);
+
+ hal::ScreenOrientation GetScreenOrientation();
+ uint16_t GetScreenAngle();
+
+ nsresult GetProxyForURI(const nsACString& aSpec, const nsACString& aScheme,
+ const nsACString& aHost, const int32_t aPort,
+ nsACString& aResult);
+
+ bool PumpMessageLoop();
+
+ // Utility methods.
+ static jfieldID GetFieldID(JNIEnv* env, jclass jClass, const char* fieldName,
+ const char* fieldType);
+ static jfieldID GetStaticFieldID(JNIEnv* env, jclass jClass,
+ const char* fieldName,
+ const char* fieldType);
+ static jmethodID GetMethodID(JNIEnv* env, jclass jClass,
+ const char* methodName, const char* methodType);
+ static jmethodID GetStaticMethodID(JNIEnv* env, jclass jClass,
+ const char* methodName,
+ const char* methodType);
+
+ static jni::Object::LocalRef ChannelCreate(jni::Object::Param);
+
+ static void InputStreamClose(jni::Object::Param obj);
+ static uint32_t InputStreamAvailable(jni::Object::Param obj);
+ static nsresult InputStreamRead(jni::Object::Param obj, char* aBuf,
+ uint32_t aCount, uint32_t* aRead);
+
+ protected:
+ static nsTHashMap<nsStringHashKey, nsString> sStoragePaths;
+
+ static AndroidBridge* sBridge;
+
+ AndroidBridge();
+ ~AndroidBridge();
+
+ jni::Object::GlobalRef mMessageQueue;
+ jfieldID mMessageQueueMessages;
+ jmethodID mMessageQueueNext;
+};
+
+class AutoJNIClass {
+ private:
+ JNIEnv* const mEnv;
+ const jclass mClass;
+
+ public:
+ AutoJNIClass(JNIEnv* jEnv, const char* name)
+ : mEnv(jEnv), mClass(jni::GetClassRef(jEnv, name)) {}
+
+ ~AutoJNIClass() { mEnv->DeleteLocalRef(mClass); }
+
+ jclass getRawRef() const { return mClass; }
+
+ jclass getGlobalRef() const {
+ return static_cast<jclass>(mEnv->NewGlobalRef(mClass));
+ }
+
+ jfieldID getField(const char* name, const char* type) const {
+ return AndroidBridge::GetFieldID(mEnv, mClass, name, type);
+ }
+
+ jfieldID getStaticField(const char* name, const char* type) const {
+ return AndroidBridge::GetStaticFieldID(mEnv, mClass, name, type);
+ }
+
+ jmethodID getMethod(const char* name, const char* type) const {
+ return AndroidBridge::GetMethodID(mEnv, mClass, name, type);
+ }
+
+ jmethodID getStaticMethod(const char* name, const char* type) const {
+ return AndroidBridge::GetStaticMethodID(mEnv, mClass, name, type);
+ }
+};
+
+class AutoJObject {
+ public:
+ explicit AutoJObject(JNIEnv* aJNIEnv = nullptr) : mObject(nullptr) {
+ mJNIEnv = aJNIEnv ? aJNIEnv : jni::GetGeckoThreadEnv();
+ }
+
+ AutoJObject(JNIEnv* aJNIEnv, jobject aObject) {
+ mJNIEnv = aJNIEnv ? aJNIEnv : jni::GetGeckoThreadEnv();
+ mObject = aObject;
+ }
+
+ ~AutoJObject() {
+ if (mObject) mJNIEnv->DeleteLocalRef(mObject);
+ }
+
+ jobject operator=(jobject aObject) {
+ if (mObject) {
+ mJNIEnv->DeleteLocalRef(mObject);
+ }
+ return mObject = aObject;
+ }
+
+ operator jobject() { return mObject; }
+
+ private:
+ JNIEnv* mJNIEnv;
+ jobject mObject;
+};
+
+class AutoLocalJNIFrame {
+ public:
+ explicit AutoLocalJNIFrame(int nEntries = 15)
+ : mEntries(nEntries),
+ mJNIEnv(jni::GetGeckoThreadEnv()),
+ mHasFrameBeenPushed(false) {
+ MOZ_ASSERT(mJNIEnv);
+ Push();
+ }
+
+ explicit AutoLocalJNIFrame(JNIEnv* aJNIEnv, int nEntries = 15)
+ : mEntries(nEntries),
+ mJNIEnv(aJNIEnv ? aJNIEnv : jni::GetGeckoThreadEnv()),
+ mHasFrameBeenPushed(false) {
+ MOZ_ASSERT(mJNIEnv);
+ Push();
+ }
+
+ ~AutoLocalJNIFrame() {
+ if (mHasFrameBeenPushed) {
+ Pop();
+ }
+ }
+
+ JNIEnv* GetEnv() { return mJNIEnv; }
+
+ bool CheckForException() {
+ if (mJNIEnv->ExceptionCheck()) {
+ MOZ_CATCH_JNI_EXCEPTION(mJNIEnv);
+ return true;
+ }
+ return false;
+ }
+
+ // Note! Calling Purge makes all previous local refs created in
+ // the AutoLocalJNIFrame's scope INVALID; be sure that you locked down
+ // any local refs that you need to keep around in global refs!
+ void Purge() {
+ Pop();
+ Push();
+ }
+
+ template <typename ReturnType = jobject>
+ ReturnType Pop(ReturnType aResult = nullptr) {
+ MOZ_ASSERT(mHasFrameBeenPushed);
+ mHasFrameBeenPushed = false;
+ return static_cast<ReturnType>(
+ mJNIEnv->PopLocalFrame(static_cast<jobject>(aResult)));
+ }
+
+ private:
+ void Push() {
+ MOZ_ASSERT(!mHasFrameBeenPushed);
+ // Make sure there is enough space to store a local ref to the
+ // exception. I am not completely sure this is needed, but does
+ // not hurt.
+ if (mJNIEnv->PushLocalFrame(mEntries + 1) != 0) {
+ CheckForException();
+ return;
+ }
+ mHasFrameBeenPushed = true;
+ }
+
+ const int mEntries;
+ JNIEnv* const mJNIEnv;
+ bool mHasFrameBeenPushed;
+};
+
+} // namespace mozilla
+
+#define NS_ANDROIDBRIDGE_CID \
+ { \
+ 0x0FE2321D, 0xEBD9, 0x467D, { \
+ 0xA7, 0x43, 0x03, 0xA6, 0x8D, 0x40, 0x59, 0x9E \
+ } \
+ }
+
+class nsAndroidBridge final : public nsIAndroidBridge {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIANDROIDBRIDGE
+
+ NS_FORWARD_SAFE_NSIANDROIDEVENTDISPATCHER(mEventDispatcher)
+
+ nsAndroidBridge();
+
+ private:
+ ~nsAndroidBridge();
+
+ nsCOMPtr<nsIAndroidEventDispatcher> mEventDispatcher;
+
+ protected:
+};
+
+#endif /* AndroidBridge_h__ */
diff --git a/widget/android/AndroidBridgeUtilities.h b/widget/android/AndroidBridgeUtilities.h
new file mode 100644
index 0000000000..2d67c7ff46
--- /dev/null
+++ b/widget/android/AndroidBridgeUtilities.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef ALOG
+# if defined(DEBUG) || defined(FORCE_ALOG)
+# define ALOG(args...) __android_log_print(ANDROID_LOG_INFO, "Gecko", ##args)
+# else
+# define ALOG(args...) ((void)0)
+# endif
+#endif
+
+#ifdef DEBUG
+# define ALOG_BRIDGE(args...) ALOG(args)
+#else
+# define ALOG_BRIDGE(args...) ((void)0)
+#endif
diff --git a/widget/android/AndroidCompositorWidget.cpp b/widget/android/AndroidCompositorWidget.cpp
new file mode 100644
index 0000000000..a7eeb665e1
--- /dev/null
+++ b/widget/android/AndroidCompositorWidget.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=2 et tw=80 : */
+/* 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 "AndroidCompositorWidget.h"
+
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "nsWindow.h"
+
+namespace mozilla {
+namespace widget {
+
+AndroidCompositorWidget::AndroidCompositorWidget(
+ const AndroidCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions)
+ : CompositorWidget(aOptions),
+ mWidgetId(aInitData.widgetId()),
+ mNativeWindow(nullptr),
+ mFormat(WINDOW_FORMAT_RGBA_8888),
+ mClientSize(aInitData.clientSize()) {}
+
+AndroidCompositorWidget::~AndroidCompositorWidget() {
+ if (mNativeWindow) {
+ ANativeWindow_release(mNativeWindow);
+ }
+}
+
+already_AddRefed<gfx::DrawTarget>
+AndroidCompositorWidget::StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) {
+ if (!mNativeWindow) {
+ EGLNativeWindowType window = GetEGLNativeWindow();
+ JNIEnv* const env = jni::GetEnvForThread();
+ mNativeWindow =
+ ANativeWindow_fromSurface(env, reinterpret_cast<jobject>(window));
+ if (mNativeWindow) {
+ mFormat = ANativeWindow_getFormat(mNativeWindow);
+ ANativeWindow_acquire(mNativeWindow);
+ } else {
+ return nullptr;
+ }
+ }
+
+ if (mFormat != WINDOW_FORMAT_RGBA_8888 &&
+ mFormat != WINDOW_FORMAT_RGBX_8888) {
+ gfxCriticalNoteOnce << "Non supported format: " << mFormat;
+ return nullptr;
+ }
+
+ // XXX Handle inOutDirtyBounds
+ if (ANativeWindow_lock(mNativeWindow, &mBuffer, nullptr) != 0) {
+ return nullptr;
+ }
+
+ const int bpp = 4;
+ gfx::SurfaceFormat format = gfx::SurfaceFormat::R8G8B8A8;
+ if (mFormat == WINDOW_FORMAT_RGBX_8888) {
+ format = gfx::SurfaceFormat::R8G8B8X8;
+ }
+
+ RefPtr<gfx::DrawTarget> dt = gfx::Factory::CreateDrawTargetForData(
+ gfx::BackendType::SKIA, static_cast<unsigned char*>(mBuffer.bits),
+ gfx::IntSize(mBuffer.width, mBuffer.height), mBuffer.stride * bpp, format,
+ true);
+
+ return dt.forget();
+}
+
+void AndroidCompositorWidget::EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
+ ANativeWindow_unlockAndPost(mNativeWindow);
+}
+
+bool AndroidCompositorWidget::OnResumeComposition() {
+ OnCompositorSurfaceChanged();
+
+ if (!mSurface) {
+ gfxCriticalError() << "OnResumeComposition called with null Surface";
+ return false;
+ }
+
+ return true;
+}
+
+EGLNativeWindowType AndroidCompositorWidget::GetEGLNativeWindow() {
+ return (EGLNativeWindowType)mSurface.Get();
+}
+
+LayoutDeviceIntSize AndroidCompositorWidget::GetClientSize() {
+ return mClientSize;
+}
+
+void AndroidCompositorWidget::NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ mClientSize =
+ LayoutDeviceIntSize(std::min(aClientSize.width, MOZ_WIDGET_MAX_SIZE),
+ std::min(aClientSize.height, MOZ_WIDGET_MAX_SIZE));
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/AndroidCompositorWidget.h b/widget/android/AndroidCompositorWidget.h
new file mode 100644
index 0000000000..c478477b5f
--- /dev/null
+++ b/widget/android/AndroidCompositorWidget.h
@@ -0,0 +1,70 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef mozilla_widget_AndroidCompositorWidget_h
+#define mozilla_widget_AndroidCompositorWidget_h
+
+#include "CompositorWidget.h"
+#include "AndroidNativeWindow.h"
+#include "GLDefs.h"
+
+namespace mozilla {
+namespace widget {
+
+class PlatformCompositorWidgetDelegate : public CompositorWidgetDelegate {
+ public:
+ virtual void NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) = 0;
+
+ // CompositorWidgetDelegate Overrides
+ PlatformCompositorWidgetDelegate* AsPlatformSpecificDelegate() override {
+ return this;
+ }
+};
+
+class AndroidCompositorWidgetInitData;
+
+class AndroidCompositorWidget : public CompositorWidget {
+ public:
+ AndroidCompositorWidget(const AndroidCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions);
+ ~AndroidCompositorWidget() override;
+
+ EGLNativeWindowType GetEGLNativeWindow();
+
+ // CompositorWidget overrides
+
+ already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) override;
+ void EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget,
+ const LayoutDeviceIntRegion& aInvalidRegion) override;
+
+ bool OnResumeComposition() override;
+
+ AndroidCompositorWidget* AsAndroid() override { return this; }
+
+ LayoutDeviceIntSize GetClientSize() override;
+ void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize);
+
+ protected:
+ int32_t mWidgetId;
+ java::sdk::Surface::GlobalRef mSurface;
+ ANativeWindow* mNativeWindow;
+ ANativeWindow_Buffer mBuffer;
+ int32_t mFormat;
+ LayoutDeviceIntSize mClientSize;
+
+ private:
+ // Called whenever the compositor surface may have changed. The derived class
+ // should update mSurface to the new compositor surface.
+ virtual void OnCompositorSurfaceChanged() = 0;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_AndroidCompositorWidget_h
diff --git a/widget/android/AndroidContentController.cpp b/widget/android/AndroidContentController.cpp
new file mode 100644
index 0000000000..c1029fd4a2
--- /dev/null
+++ b/widget/android/AndroidContentController.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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 "AndroidContentController.h"
+
+#include "AndroidBridge.h"
+#include "base/message_loop.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "nsIObserverService.h"
+#include "nsLayoutUtils.h"
+#include "nsWindow.h"
+
+using mozilla::layers::IAPZCTreeManager;
+
+namespace mozilla {
+namespace widget {
+
+void AndroidContentController::Destroy() {
+ mAndroidWindow = nullptr;
+ ChromeProcessController::Destroy();
+}
+
+void AndroidContentController::UpdateOverscrollVelocity(
+ const ScrollableLayerGuid& aGuid, const float aX, const float aY,
+ const bool aIsRootContent) {
+ if (aIsRootContent && mAndroidWindow) {
+ mAndroidWindow->UpdateOverscrollVelocity(aX, aY);
+ }
+}
+
+void AndroidContentController::UpdateOverscrollOffset(
+ const ScrollableLayerGuid& aGuid, const float aX, const float aY,
+ const bool aIsRootContent) {
+ if (aIsRootContent && mAndroidWindow) {
+ mAndroidWindow->UpdateOverscrollOffset(aX, aY);
+ }
+}
+
+void AndroidContentController::NotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ // This function may get invoked twice, if the first invocation is not on
+ // the main thread then the ChromeProcessController version of this function
+ // will redispatch to the main thread. We want to make sure that our handling
+ // only happens on the main thread.
+ ChromeProcessController::NotifyAPZStateChange(aGuid, aChange, aArg,
+ aInputBlockId);
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (aChange ==
+ layers::GeckoContentController::APZStateChange::eTransformEnd) {
+ // This is used by tests to determine when the APZ is done doing whatever
+ // it's doing. XXX generify this as needed when writing additional tests.
+ observerService->NotifyObservers(nullptr, "APZ:TransformEnd", nullptr);
+ observerService->NotifyObservers(nullptr, "PanZoom:StateChange",
+ u"NOTHING");
+ } else if (aChange == layers::GeckoContentController::APZStateChange::
+ eTransformBegin) {
+ observerService->NotifyObservers(nullptr, "PanZoom:StateChange",
+ u"PANNING");
+ }
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/AndroidContentController.h b/widget/android/AndroidContentController.h
new file mode 100644
index 0000000000..04640deb4e
--- /dev/null
+++ b/widget/android/AndroidContentController.h
@@ -0,0 +1,52 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef AndroidContentController_h__
+#define AndroidContentController_h__
+
+#include "mozilla/layers/ChromeProcessController.h"
+#include "mozilla/EventForwards.h" // for Modifiers
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "nsTArray.h"
+#include "nsWindow.h"
+
+namespace mozilla {
+namespace layers {
+class APZEventState;
+class IAPZCTreeManager;
+} // namespace layers
+namespace widget {
+
+class AndroidContentController final
+ : public mozilla::layers::ChromeProcessController {
+ public:
+ AndroidContentController(nsWindow* aWindow,
+ mozilla::layers::APZEventState* aAPZEventState,
+ mozilla::layers::IAPZCTreeManager* aAPZCTreeManager)
+ : mozilla::layers::ChromeProcessController(aWindow, aAPZEventState,
+ aAPZCTreeManager),
+ mAndroidWindow(aWindow) {}
+
+ // ChromeProcessController methods
+ virtual void Destroy() override;
+ void UpdateOverscrollVelocity(const ScrollableLayerGuid& aGuid,
+ const float aX, const float aY,
+ const bool aIsRootContent) override;
+ void UpdateOverscrollOffset(const ScrollableLayerGuid& aGuid, const float aX,
+ const float aY,
+ const bool aIsRootContent) override;
+ void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) override;
+
+ private:
+ nsWindow* mAndroidWindow;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/android/AndroidUiThread.cpp b/widget/android/AndroidUiThread.cpp
new file mode 100644
index 0000000000..97a5105787
--- /dev/null
+++ b/widget/android/AndroidUiThread.cpp
@@ -0,0 +1,373 @@
+/* -*- Mode: C++; tab-width: 8; 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 "base/message_loop.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/EventQueue.h"
+#include "mozilla/java/GeckoThreadWrappers.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/ThreadEventQueue.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "GeckoProfiler.h"
+#include "nsThread.h"
+#include "nsThreadManager.h"
+#include "nsThreadUtils.h"
+
+#include <android/api-level.h>
+#include <pthread.h>
+
+using namespace mozilla;
+
+namespace {
+
+class AndroidUiThread;
+class AndroidUiTask;
+
+StaticAutoPtr<LinkedList<AndroidUiTask> > sTaskQueue;
+StaticAutoPtr<mozilla::Mutex> sTaskQueueLock;
+StaticRefPtr<AndroidUiThread> sThread;
+static bool sThreadDestroyed;
+static MessageLoop* sMessageLoop;
+static Atomic<Monitor*> sMessageLoopAccessMonitor;
+
+void EnqueueTask(already_AddRefed<nsIRunnable> aTask, int aDelayMs);
+
+/*
+ * The AndroidUiThread is derived from nsThread so that nsIRunnable objects that
+ * get dispatched may be intercepted. Only nsIRunnable objects that need to be
+ * synchronously executed are passed into the nsThread to be queued. All other
+ * nsIRunnable object are immediately dispatched to the Android UI thread.
+ * AndroidUiThread is derived from nsThread instead of being an nsIEventTarget
+ * wrapper that contains an nsThread object because if nsIRunnable objects with
+ * a delay were dispatch directly to an nsThread object, such as obtained from
+ * nsThreadManager::GetCurrentThread(), the nsIRunnable could get stuck in the
+ * nsThread nsIRunnable queue. This is due to the fact that Android controls the
+ * event loop in the Android UI thread and has no knowledge of when the nsThread
+ * needs to be drained.
+ */
+
+class AndroidUiThread : public nsThread {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(AndroidUiThread, nsThread)
+ AndroidUiThread()
+ : nsThread(
+ MakeNotNull<ThreadEventQueue*>(MakeUnique<mozilla::EventQueue>()),
+ nsThread::NOT_MAIN_THREAD, {.stackSize = 0}) {}
+
+ nsresult Dispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aFlags) override;
+ nsresult DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aDelayMs) override;
+
+ private:
+ ~AndroidUiThread() {}
+};
+
+NS_IMETHODIMP
+AndroidUiThread::Dispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aFlags) {
+ EnqueueTask(std::move(aEvent), 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AndroidUiThread::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aDelayMs) {
+ EnqueueTask(std::move(aEvent), aDelayMs);
+ return NS_OK;
+}
+
+static void PumpEvents() { NS_ProcessPendingEvents(sThread.get()); }
+
+class ThreadObserver : public nsIThreadObserver {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITHREADOBSERVER
+
+ ThreadObserver() {}
+
+ private:
+ virtual ~ThreadObserver() {}
+};
+
+NS_IMPL_ISUPPORTS(ThreadObserver, nsIThreadObserver)
+
+NS_IMETHODIMP
+ThreadObserver::OnDispatchedEvent() {
+ EnqueueTask(NS_NewRunnableFunction("PumpEvents", &PumpEvents), 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ThreadObserver::OnProcessNextEvent(nsIThreadInternal* thread, bool mayWait) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ThreadObserver::AfterProcessNextEvent(nsIThreadInternal* thread,
+ bool eventWasProcessed) {
+ return NS_OK;
+}
+
+class AndroidUiTask : public LinkedListElement<AndroidUiTask> {
+ using TimeStamp = mozilla::TimeStamp;
+ using TimeDuration = mozilla::TimeDuration;
+
+ public:
+ explicit AndroidUiTask(already_AddRefed<nsIRunnable> aTask)
+ : mTask(aTask),
+ mRunTime() // Null timestamp representing no delay.
+ {}
+
+ AndroidUiTask(already_AddRefed<nsIRunnable> aTask, int aDelayMs)
+ : mTask(aTask),
+ mRunTime(TimeStamp::Now() + TimeDuration::FromMilliseconds(aDelayMs)) {}
+
+ bool IsEarlierThan(const AndroidUiTask& aOther) const {
+ if (mRunTime) {
+ return aOther.mRunTime ? mRunTime < aOther.mRunTime : false;
+ }
+ // In the case of no delay, we're earlier if aOther has a delay.
+ // Otherwise, we're not earlier, to maintain task order.
+ return !!aOther.mRunTime;
+ }
+
+ int64_t MillisecondsToRunTime() const {
+ if (mRunTime) {
+ return int64_t((mRunTime - TimeStamp::Now()).ToMilliseconds());
+ }
+ return 0;
+ }
+
+ already_AddRefed<nsIRunnable> TakeTask() { return mTask.forget(); }
+
+ private:
+ nsCOMPtr<nsIRunnable> mTask;
+ const TimeStamp mRunTime;
+};
+
+class CreateOnUiThread : public Runnable {
+ public:
+ CreateOnUiThread() : Runnable("CreateOnUiThread") {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(!sThreadDestroyed);
+ MOZ_ASSERT(sMessageLoopAccessMonitor);
+ MonitorAutoLock lock(*sMessageLoopAccessMonitor);
+ sThread = new AndroidUiThread();
+ sThread->InitCurrentThread();
+ sThread->SetObserver(new ThreadObserver());
+ RegisterThreadWithProfiler();
+ sMessageLoop =
+ new MessageLoop(MessageLoop::TYPE_MOZILLA_ANDROID_UI, sThread.get());
+ lock.NotifyAll();
+ return NS_OK;
+ }
+
+ private:
+ static void RegisterThreadWithProfiler() {
+#if defined(MOZ_GECKO_PROFILER)
+ // We don't use the PROFILER_REGISTER_THREAD macro here because by this
+ // point the Android UI thread is already quite a ways into its stack;
+ // the profiler's sampler thread will ignore a lot of frames if we do not
+ // provide a better value for the stack top. We'll manually obtain that
+ // info via pthreads.
+
+ // Fallback address if any pthread calls fail
+ char fallback;
+ char* stackTop = &fallback;
+
+ auto regOnExit = MakeScopeExit(
+ [&stackTop]() { profiler_register_thread("AndroidUI", stackTop); });
+
+ pthread_attr_t attrs;
+ if (pthread_getattr_np(pthread_self(), &attrs)) {
+ return;
+ }
+
+ void* stackBase;
+ size_t stackSize;
+ if (pthread_attr_getstack(&attrs, &stackBase, &stackSize)) {
+ return;
+ }
+
+ stackTop = static_cast<char*>(stackBase) + stackSize - 1;
+#endif // defined(MOZ_GECKO_PROFILER)
+ }
+};
+
+class DestroyOnUiThread : public Runnable {
+ public:
+ DestroyOnUiThread() : Runnable("DestroyOnUiThread"), mDestroyed(false) {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(!sThreadDestroyed);
+ MOZ_ASSERT(sMessageLoopAccessMonitor);
+ MOZ_ASSERT(sTaskQueue);
+ MonitorAutoLock lock(*sMessageLoopAccessMonitor);
+ sThreadDestroyed = true;
+
+ {
+ // Flush the queue
+ MutexAutoLock lock(*sTaskQueueLock);
+ while (AndroidUiTask* task = sTaskQueue->getFirst()) {
+ delete task;
+ }
+ }
+
+ delete sMessageLoop;
+ sMessageLoop = nullptr;
+ MOZ_ASSERT(sThread);
+ PROFILER_UNREGISTER_THREAD();
+ nsThreadManager::get().UnregisterCurrentThread(*sThread);
+ sThread = nullptr;
+ mDestroyed = true;
+ lock.NotifyAll();
+ return NS_OK;
+ }
+
+ void WaitForDestruction() {
+ MOZ_ASSERT(sMessageLoopAccessMonitor);
+ MonitorAutoLock lock(*sMessageLoopAccessMonitor);
+ while (!mDestroyed) {
+ lock.Wait();
+ }
+ }
+
+ private:
+ bool mDestroyed;
+};
+
+void EnqueueTask(already_AddRefed<nsIRunnable> aTask, int aDelayMs) {
+ if (sThreadDestroyed) {
+ return;
+ }
+
+ // add the new task into the sTaskQueue, sorted with
+ // the earliest task first in the queue
+ AndroidUiTask* newTask =
+ (aDelayMs ? new AndroidUiTask(std::move(aTask), aDelayMs)
+ : new AndroidUiTask(std::move(aTask)));
+
+ bool headOfList = false;
+ {
+ MOZ_ASSERT(sTaskQueue);
+ MOZ_ASSERT(sTaskQueueLock);
+ MutexAutoLock lock(*sTaskQueueLock);
+
+ AndroidUiTask* task = sTaskQueue->getFirst();
+
+ while (task) {
+ if (newTask->IsEarlierThan(*task)) {
+ task->setPrevious(newTask);
+ break;
+ }
+ task = task->getNext();
+ }
+
+ if (!newTask->isInList()) {
+ sTaskQueue->insertBack(newTask);
+ }
+ headOfList = !newTask->getPrevious();
+ }
+
+ if (headOfList) {
+ // if we're inserting it at the head of the queue, notify Java because
+ // we need to get a callback at an earlier time than the last scheduled
+ // callback
+ java::GeckoThread::RequestUiThreadCallback(int64_t(aDelayMs));
+ }
+}
+
+} // namespace
+
+namespace mozilla {
+
+void CreateAndroidUiThread() {
+ MOZ_ASSERT(!sThread);
+ MOZ_ASSERT(!sMessageLoopAccessMonitor);
+ sTaskQueue = new LinkedList<AndroidUiTask>();
+ sTaskQueueLock = new Mutex("AndroidUiThreadTaskQueueLock");
+ sMessageLoopAccessMonitor =
+ new Monitor("AndroidUiThreadMessageLoopAccessMonitor");
+ sThreadDestroyed = false;
+ RefPtr<CreateOnUiThread> runnable = new CreateOnUiThread;
+ EnqueueTask(do_AddRef(runnable), 0);
+}
+
+void DestroyAndroidUiThread() {
+ MOZ_ASSERT(sThread);
+ RefPtr<DestroyOnUiThread> runnable = new DestroyOnUiThread;
+ EnqueueTask(do_AddRef(runnable), 0);
+ runnable->WaitForDestruction();
+ delete sMessageLoopAccessMonitor;
+ sMessageLoopAccessMonitor = nullptr;
+}
+
+MessageLoop* GetAndroidUiThreadMessageLoop() {
+ if (!sMessageLoopAccessMonitor) {
+ return nullptr;
+ }
+
+ MonitorAutoLock lock(*sMessageLoopAccessMonitor);
+ while (!sMessageLoop) {
+ lock.Wait();
+ }
+
+ return sMessageLoop;
+}
+
+RefPtr<nsThread> GetAndroidUiThread() {
+ if (!sMessageLoopAccessMonitor) {
+ return nullptr;
+ }
+
+ MonitorAutoLock lock(*sMessageLoopAccessMonitor);
+ while (!sThread) {
+ lock.Wait();
+ }
+
+ return sThread;
+}
+
+int64_t RunAndroidUiTasks() {
+ MutexAutoLock lock(*sTaskQueueLock);
+
+ if (sThreadDestroyed) {
+ return -1;
+ }
+
+ while (!sTaskQueue->isEmpty()) {
+ AndroidUiTask* task = sTaskQueue->getFirst();
+ const int64_t timeLeft = task->MillisecondsToRunTime();
+ if (timeLeft > 0) {
+ // this task (and therefore all remaining tasks)
+ // have not yet reached their runtime. return the
+ // time left until we should be called again
+ return timeLeft;
+ }
+
+ // Retrieve task before unlocking/running.
+ nsCOMPtr<nsIRunnable> runnable(task->TakeTask());
+ // LinkedListElements auto remove from list upon destruction
+ delete task;
+
+ // Unlock to allow posting new tasks reentrantly.
+ MutexAutoUnlock unlock(*sTaskQueueLock);
+ runnable->Run();
+ if (sThreadDestroyed) {
+ return -1;
+ }
+ }
+ return -1;
+}
+
+} // namespace mozilla
diff --git a/widget/android/AndroidUiThread.h b/widget/android/AndroidUiThread.h
new file mode 100644
index 0000000000..af722fb048
--- /dev/null
+++ b/widget/android/AndroidUiThread.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef AndroidUiThread_h__
+#define AndroidUiThread_h__
+
+#include <mozilla/RefPtr.h>
+#include <nsThread.h>
+
+class MessageLoop;
+
+namespace mozilla {
+
+void CreateAndroidUiThread();
+void DestroyAndroidUiThread();
+int64_t RunAndroidUiTasks();
+
+MessageLoop* GetAndroidUiThreadMessageLoop();
+RefPtr<nsThread> GetAndroidUiThread();
+
+} // namespace mozilla
+
+#endif // AndroidUiThread_h__
diff --git a/widget/android/AndroidView.h b/widget/android/AndroidView.h
new file mode 100644
index 0000000000..173db8b5c1
--- /dev/null
+++ b/widget/android/AndroidView.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_AndroidView_h
+#define mozilla_widget_AndroidView_h
+
+#include "mozilla/widget/EventDispatcher.h"
+
+namespace mozilla {
+namespace widget {
+
+class AndroidView final : public nsIAndroidView {
+ virtual ~AndroidView() {}
+
+ public:
+ const RefPtr<mozilla::widget::EventDispatcher> mEventDispatcher{
+ new mozilla::widget::EventDispatcher()};
+
+ AndroidView() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIANDROIDVIEW
+
+ NS_FORWARD_NSIANDROIDEVENTDISPATCHER(mEventDispatcher->)
+
+ mozilla::java::GeckoBundle::GlobalRef mInitData;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_AndroidView_h
diff --git a/widget/android/AndroidVsync.cpp b/widget/android/AndroidVsync.cpp
new file mode 100644
index 0000000000..6aed5f1e53
--- /dev/null
+++ b/widget/android/AndroidVsync.cpp
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "AndroidVsync.h"
+
+#include "AndroidBridge.h"
+#include "nsTArray.h"
+
+/**
+ * Implementation for the AndroidVsync class.
+ */
+
+namespace mozilla {
+namespace widget {
+
+StaticDataMutex<ThreadSafeWeakPtr<AndroidVsync>> AndroidVsync::sInstance(
+ "AndroidVsync::sInstance");
+
+/* static */ RefPtr<AndroidVsync> AndroidVsync::GetInstance() {
+ auto weakInstance = sInstance.Lock();
+ RefPtr<AndroidVsync> instance(*weakInstance);
+ if (!instance) {
+ instance = new AndroidVsync();
+ *weakInstance = instance;
+ }
+ return instance;
+}
+
+/**
+ * Owned by the Java AndroidVsync instance.
+ */
+class AndroidVsyncSupport final
+ : public java::AndroidVsync::Natives<AndroidVsyncSupport> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AndroidVsyncSupport)
+
+ using Base = java::AndroidVsync::Natives<AndroidVsyncSupport>;
+ using Base::AttachNative;
+ using Base::DisposeNative;
+
+ explicit AndroidVsyncSupport(AndroidVsync* aAndroidVsync)
+ : mAndroidVsync(std::move(aAndroidVsync),
+ "AndroidVsyncSupport::mAndroidVsync") {}
+
+ // Called by Java
+ void NotifyVsync(const java::AndroidVsync::LocalRef& aInstance,
+ int64_t aFrameTimeNanos) {
+ auto androidVsync = mAndroidVsync.Lock();
+ if (*androidVsync) {
+ (*androidVsync)->NotifyVsync(aFrameTimeNanos);
+ }
+ }
+
+ // Called by the AndroidVsync destructor
+ void Unlink() {
+ auto androidVsync = mAndroidVsync.Lock();
+ *androidVsync = nullptr;
+ }
+
+ protected:
+ ~AndroidVsyncSupport() = default;
+
+ DataMutex<AndroidVsync*> mAndroidVsync;
+};
+
+AndroidVsync::AndroidVsync() : mImpl("AndroidVsync.mImpl") {
+ AndroidVsyncSupport::Init();
+
+ auto impl = mImpl.Lock();
+ impl->mSupport = new AndroidVsyncSupport(this);
+ impl->mSupportJava = java::AndroidVsync::New();
+ AndroidVsyncSupport::AttachNative(impl->mSupportJava, impl->mSupport);
+}
+
+AndroidVsync::~AndroidVsync() {
+ auto impl = mImpl.Lock();
+ impl->mInputObservers.Clear();
+ impl->mRenderObservers.Clear();
+ impl->UpdateObservingVsync();
+ impl->mSupport->Unlink();
+}
+
+void AndroidVsync::RegisterObserver(Observer* aObserver, ObserverType aType) {
+ auto impl = mImpl.Lock();
+ if (aType == AndroidVsync::INPUT) {
+ impl->mInputObservers.AppendElement(aObserver);
+ } else {
+ impl->mRenderObservers.AppendElement(aObserver);
+ }
+ impl->UpdateObservingVsync();
+}
+
+void AndroidVsync::UnregisterObserver(Observer* aObserver, ObserverType aType) {
+ auto impl = mImpl.Lock();
+ if (aType == AndroidVsync::INPUT) {
+ impl->mInputObservers.RemoveElement(aObserver);
+ } else {
+ impl->mRenderObservers.RemoveElement(aObserver);
+ }
+ aObserver->Dispose();
+ impl->UpdateObservingVsync();
+}
+
+void AndroidVsync::Impl::UpdateObservingVsync() {
+ bool shouldObserve =
+ !mInputObservers.IsEmpty() || !mRenderObservers.IsEmpty();
+ if (shouldObserve != mObservingVsync) {
+ mObservingVsync = mSupportJava->ObserveVsync(shouldObserve);
+ }
+}
+
+// Always called on the Java UI thread.
+void AndroidVsync::NotifyVsync(int64_t aFrameTimeNanos) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ // Convert aFrameTimeNanos to a TimeStamp. The value converts trivially to
+ // the internal ticks representation of TimeStamp_posix; both use the
+ // monotonic clock and are in nanoseconds.
+ TimeStamp timeStamp = TimeStamp::FromSystemTime(aFrameTimeNanos);
+
+ // Do not keep the lock held while calling OnVsync.
+ nsTArray<Observer*> observers;
+ {
+ auto impl = mImpl.Lock();
+ observers.AppendElements(impl->mInputObservers);
+ observers.AppendElements(impl->mRenderObservers);
+ }
+ for (Observer* observer : observers) {
+ observer->OnVsync(timeStamp);
+ }
+}
+
+void AndroidVsync::OnMaybeUpdateRefreshRate() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto impl = mImpl.Lock();
+
+ nsTArray<Observer*> observers;
+ observers.AppendElements(impl->mRenderObservers);
+
+ for (Observer* observer : observers) {
+ observer->OnMaybeUpdateRefreshRate();
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/AndroidVsync.h b/widget/android/AndroidVsync.h
new file mode 100644
index 0000000000..d50e1ec71f
--- /dev/null
+++ b/widget/android/AndroidVsync.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_AndroidVsync_h
+#define mozilla_widget_AndroidVsync_h
+
+#include "mozilla/DataMutex.h"
+#include "mozilla/java/AndroidVsyncNatives.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace widget {
+
+class AndroidVsyncSupport;
+
+/**
+ * A thread-safe way to listen to vsync notifications on Android. All methods
+ * can be called on any thread.
+ * Observers must keep a strong reference to the AndroidVsync instance until
+ * they unregister themselves.
+ */
+class AndroidVsync final : public SupportsThreadSafeWeakPtr<AndroidVsync> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(AndroidVsync)
+
+ static RefPtr<AndroidVsync> GetInstance();
+
+ ~AndroidVsync();
+
+ class Observer {
+ public:
+ // Will be called on the Java UI thread.
+ virtual void OnVsync(const TimeStamp& aTimeStamp) = 0;
+ // Will be called on the Java UI thread.
+ virtual void OnMaybeUpdateRefreshRate() {}
+ // Called when the observer is unregistered, in case it wants to
+ // manage its own lifetime.
+ virtual void Dispose() {}
+ virtual ~Observer() = default;
+ };
+
+ // INPUT observers are called before RENDER observers.
+ enum ObserverType { INPUT, RENDER };
+ void RegisterObserver(Observer* aObserver, ObserverType aType);
+ void UnregisterObserver(Observer* aObserver, ObserverType aType);
+
+ void OnMaybeUpdateRefreshRate();
+
+ private:
+ friend class AndroidVsyncSupport;
+
+ AndroidVsync();
+
+ // Called by Java, via AndroidVsyncSupport
+ void NotifyVsync(int64_t aFrameTimeNanos);
+
+ struct Impl {
+ void UpdateObservingVsync();
+
+ nsTArray<Observer*> mInputObservers;
+ nsTArray<Observer*> mRenderObservers;
+ RefPtr<AndroidVsyncSupport> mSupport;
+ java::AndroidVsync::GlobalRef mSupportJava;
+ bool mObservingVsync = false;
+ };
+
+ DataMutex<Impl> mImpl;
+
+ static StaticDataMutex<ThreadSafeWeakPtr<AndroidVsync>> sInstance;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_AndroidVsync_h
diff --git a/widget/android/AndroidWidgetUtils.cpp b/widget/android/AndroidWidgetUtils.cpp
new file mode 100644
index 0000000000..c013a5b4f8
--- /dev/null
+++ b/widget/android/AndroidWidgetUtils.cpp
@@ -0,0 +1,48 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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 "AndroidWidgetUtils.h"
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Swizzle.h"
+
+using namespace mozilla::gfx;
+
+namespace mozilla::widget {
+
+// static
+already_AddRefed<DataSourceSurface>
+AndroidWidgetUtils::GetDataSourceSurfaceForAndroidBitmap(
+ gfx::SourceSurface* aSurface, const LayoutDeviceIntRect* aRect,
+ uint32_t aStride) {
+ RefPtr<DataSourceSurface> srcDataSurface = aSurface->GetDataSurface();
+ if (NS_WARN_IF(!srcDataSurface)) {
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap sourceMap(srcDataSurface,
+ DataSourceSurface::READ);
+
+ RefPtr<DataSourceSurface> destDataSurface =
+ gfx::Factory::CreateDataSourceSurfaceWithStride(
+ aRect ? IntSize(aRect->width, aRect->height)
+ : srcDataSurface->GetSize(),
+ SurfaceFormat::R8G8B8A8, aStride ? aStride : sourceMap.GetStride());
+ if (NS_WARN_IF(!destDataSurface)) {
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap destMap(destDataSurface,
+ DataSourceSurface::READ_WRITE);
+
+ SwizzleData(sourceMap.GetData(), sourceMap.GetStride(), aSurface->GetFormat(),
+ destMap.GetData(), destMap.GetStride(), SurfaceFormat::R8G8B8A8,
+ destDataSurface->GetSize());
+
+ return destDataSurface.forget();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/android/AndroidWidgetUtils.h b/widget/android/AndroidWidgetUtils.h
new file mode 100644
index 0000000000..ccf831206d
--- /dev/null
+++ b/widget/android/AndroidWidgetUtils.h
@@ -0,0 +1,37 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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/. */
+
+#ifndef mozilla_widget_AndroidWidgetUtils_h__
+#define mozilla_widget_AndroidWidgetUtils_h__
+
+#include "Units.h"
+
+namespace mozilla {
+
+namespace gfx {
+class SourceSurface;
+class DataSourceSurface;
+} // namespace gfx
+
+namespace widget {
+
+class AndroidWidgetUtils final {
+ public:
+ typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect;
+
+ /**
+ * Return Android's bitmap object compatible data surface.
+ */
+ static already_AddRefed<gfx::DataSourceSurface>
+ GetDataSourceSurfaceForAndroidBitmap(
+ gfx::SourceSurface* aSurface, const LayoutDeviceIntRect* aRect = nullptr,
+ uint32_t aStride = 0);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/android/Base64UtilsSupport.h b/widget/android/Base64UtilsSupport.h
new file mode 100644
index 0000000000..8ac4347aaa
--- /dev/null
+++ b/widget/android/Base64UtilsSupport.h
@@ -0,0 +1,52 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef Base64UtilsSupport_h__
+#define Base64UtilsSupport_h__
+
+#include "mozilla/Base64.h"
+#include "mozilla/java/Base64UtilsNatives.h"
+
+namespace mozilla {
+namespace widget {
+
+class Base64UtilsSupport final
+ : public java::Base64Utils::Natives<Base64UtilsSupport> {
+ public:
+ static jni::ByteArray::LocalRef Decode(jni::String::Param data) {
+ if (!data) {
+ return nullptr;
+ }
+
+ FallibleTArray<uint8_t> bytes;
+ if (NS_FAILED(Base64URLDecode(
+ data->ToCString(), Base64URLDecodePaddingPolicy::Ignore, bytes))) {
+ return nullptr;
+ }
+
+ return jni::ByteArray::New((const signed char*)bytes.Elements(),
+ bytes.Length());
+ }
+
+ static jni::String::LocalRef Encode(jni::ByteArray::Param data) {
+ if (!data) {
+ return nullptr;
+ }
+
+ nsTArray<int8_t> bytes = data->GetElements();
+ nsCString result;
+ if (NS_FAILED(
+ Base64URLEncode(data->Length(), (const uint8_t*)bytes.Elements(),
+ Base64URLEncodePaddingPolicy::Omit, result))) {
+ return nullptr;
+ }
+ return jni::StringParam(result);
+ }
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // Base64UtilsSupport_h__
diff --git a/widget/android/CompositorWidgetChild.cpp b/widget/android/CompositorWidgetChild.cpp
new file mode 100644
index 0000000000..ba51dda7a5
--- /dev/null
+++ b/widget/android/CompositorWidgetChild.cpp
@@ -0,0 +1,42 @@
+/* -*- 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 "CompositorWidgetChild.h"
+#include "mozilla/Unused.h"
+#include "gfxPlatform.h"
+
+namespace mozilla {
+namespace widget {
+
+CompositorWidgetChild::CompositorWidgetChild(
+ RefPtr<CompositorVsyncDispatcher> aVsyncDispatcher,
+ RefPtr<CompositorWidgetVsyncObserver> aVsyncObserver,
+ const CompositorWidgetInitData&)
+ : mVsyncDispatcher(aVsyncDispatcher), mVsyncObserver(aVsyncObserver) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(!gfxPlatform::IsHeadless());
+}
+
+CompositorWidgetChild::~CompositorWidgetChild() = default;
+
+bool CompositorWidgetChild::Initialize() { return true; }
+
+mozilla::ipc::IPCResult CompositorWidgetChild::RecvObserveVsync() {
+ mVsyncDispatcher->SetCompositorVsyncObserver(mVsyncObserver);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetChild::RecvUnobserveVsync() {
+ mVsyncDispatcher->SetCompositorVsyncObserver(nullptr);
+ return IPC_OK();
+}
+
+void CompositorWidgetChild::NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ Unused << SendNotifyClientSizeChanged(aClientSize);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/CompositorWidgetChild.h b/widget/android/CompositorWidgetChild.h
new file mode 100644
index 0000000000..88cb2913e9
--- /dev/null
+++ b/widget/android/CompositorWidgetChild.h
@@ -0,0 +1,41 @@
+/* -*- 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/. */
+
+#ifndef widget_android_CompositorWidgetChild_h
+#define widget_android_CompositorWidgetChild_h
+
+#include "AndroidCompositorWidget.h"
+#include "mozilla/widget/PCompositorWidgetChild.h"
+#include "mozilla/widget/CompositorWidgetVsyncObserver.h"
+
+namespace mozilla {
+namespace widget {
+
+class CompositorWidgetChild final : public PCompositorWidgetChild,
+ public PlatformCompositorWidgetDelegate {
+ public:
+ CompositorWidgetChild(RefPtr<CompositorVsyncDispatcher> aVsyncDispatcher,
+ RefPtr<CompositorWidgetVsyncObserver> aVsyncObserver,
+ const CompositorWidgetInitData&);
+ ~CompositorWidgetChild() override;
+
+ bool Initialize();
+
+ mozilla::ipc::IPCResult RecvObserveVsync() override;
+ mozilla::ipc::IPCResult RecvUnobserveVsync() override;
+
+ // PlatformCompositorWidgetDelegate overrides
+
+ void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override;
+
+ private:
+ RefPtr<CompositorVsyncDispatcher> mVsyncDispatcher;
+ RefPtr<CompositorWidgetVsyncObserver> mVsyncObserver;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_android_CompositorWidgetChild_h
diff --git a/widget/android/CompositorWidgetParent.cpp b/widget/android/CompositorWidgetParent.cpp
new file mode 100644
index 0000000000..93b87f3cd3
--- /dev/null
+++ b/widget/android/CompositorWidgetParent.cpp
@@ -0,0 +1,54 @@
+/* -*- 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 "CompositorWidgetParent.h"
+#include "mozilla/Unused.h"
+#include "mozilla/java/GeckoServiceGpuProcessWrappers.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+
+namespace mozilla {
+namespace widget {
+
+CompositorWidgetParent::CompositorWidgetParent(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions)
+ : AndroidCompositorWidget(aInitData.get_AndroidCompositorWidgetInitData(),
+ aOptions) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+}
+
+CompositorWidgetParent::~CompositorWidgetParent() = default;
+
+nsIWidget* CompositorWidgetParent::RealWidget() { return nullptr; }
+
+void CompositorWidgetParent::ObserveVsync(VsyncObserver* aObserver) {
+ if (aObserver) {
+ Unused << SendObserveVsync();
+ } else {
+ Unused << SendUnobserveVsync();
+ }
+ mVsyncObserver = aObserver;
+}
+
+RefPtr<VsyncObserver> CompositorWidgetParent::GetVsyncObserver() const {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+ return mVsyncObserver;
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvNotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ NotifyClientSizeChanged(aClientSize);
+ return IPC_OK();
+}
+
+void CompositorWidgetParent::OnCompositorSurfaceChanged() {
+ java::GeckoServiceGpuProcess::RemoteCompositorSurfaceManager::LocalRef
+ manager = java::GeckoServiceGpuProcess::RemoteCompositorSurfaceManager::
+ GetInstance();
+ mSurface = manager->GetCompositorSurface(mWidgetId);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/CompositorWidgetParent.h b/widget/android/CompositorWidgetParent.h
new file mode 100644
index 0000000000..cd6e4241ca
--- /dev/null
+++ b/widget/android/CompositorWidgetParent.h
@@ -0,0 +1,42 @@
+/* -*- 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/. */
+
+#ifndef widget_android_CompositorWidgetParent_h
+#define widget_android_CompositorWidgetParent_h
+
+#include "AndroidCompositorWidget.h"
+#include "mozilla/VsyncDispatcher.h"
+#include "mozilla/widget/PCompositorWidgetParent.h"
+
+namespace mozilla {
+namespace widget {
+
+class CompositorWidgetParent final : public PCompositorWidgetParent,
+ public AndroidCompositorWidget {
+ public:
+ explicit CompositorWidgetParent(const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions);
+ ~CompositorWidgetParent() override;
+
+ // CompositorWidget overrides
+
+ nsIWidget* RealWidget() override;
+ void ObserveVsync(VsyncObserver* aObserver) override;
+ RefPtr<VsyncObserver> GetVsyncObserver() const override;
+
+ mozilla::ipc::IPCResult RecvNotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) override;
+
+ private:
+ // AndroidCompositorWidget overrides
+ void OnCompositorSurfaceChanged() override;
+
+ RefPtr<VsyncObserver> mVsyncObserver;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_android_CompositorWidgetParent_h
diff --git a/widget/android/EventDispatcher.cpp b/widget/android/EventDispatcher.cpp
new file mode 100644
index 0000000000..ab876de136
--- /dev/null
+++ b/widget/android/EventDispatcher.cpp
@@ -0,0 +1,776 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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 "EventDispatcher.h"
+
+#include "JavaBuiltins.h"
+#include "nsAppShell.h"
+#include "nsJSUtils.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_SetElement, JS_SetUCProperty
+#include "js/String.h" // JS::StringHasLatin1Chars
+#include "js/Warnings.h" // JS::WarnUTF8
+#include "xpcpublic.h"
+
+#include "mozilla/fallible.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/java/EventCallbackWrappers.h"
+#include "mozilla/jni/GeckoBundleUtils.h"
+
+// Disable the C++ 2a warning. See bug #1509926
+#if defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wc++2a-compat"
+#endif
+
+namespace mozilla {
+namespace widget {
+
+namespace detail {
+
+bool CheckJS(JSContext* aCx, bool aResult) {
+ if (!aResult) {
+ JS_ClearPendingException(aCx);
+ }
+ return aResult;
+}
+
+nsresult BoxData(const nsAString& aEvent, JSContext* aCx,
+ JS::Handle<JS::Value> aData, jni::Object::LocalRef& aOut,
+ bool aObjectOnly) {
+ nsresult rv = jni::BoxData(aCx, aData, aOut, aObjectOnly);
+ if (rv != NS_ERROR_INVALID_ARG) {
+ return rv;
+ }
+
+ NS_ConvertUTF16toUTF8 event(aEvent);
+ if (JS_IsExceptionPending(aCx)) {
+ JS::WarnUTF8(aCx, "Error dispatching %s", event.get());
+ } else {
+ JS_ReportErrorUTF8(aCx, "Invalid event data for %s", event.get());
+ }
+ return NS_ERROR_INVALID_ARG;
+}
+
+nsresult UnboxString(JSContext* aCx, const jni::Object::LocalRef& aData,
+ JS::MutableHandle<JS::Value> aOut) {
+ if (!aData) {
+ aOut.setNull();
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aData.IsInstanceOf<jni::String>());
+
+ JNIEnv* const env = aData.Env();
+ const jstring jstr = jstring(aData.Get());
+ const size_t len = env->GetStringLength(jstr);
+ const jchar* const jchars = env->GetStringChars(jstr, nullptr);
+
+ if (NS_WARN_IF(!jchars)) {
+ env->ExceptionClear();
+ return NS_ERROR_FAILURE;
+ }
+
+ auto releaseStr = MakeScopeExit([env, jstr, jchars] {
+ env->ReleaseStringChars(jstr, jchars);
+ env->ExceptionClear();
+ });
+
+ JS::Rooted<JSString*> str(
+ aCx,
+ JS_NewUCStringCopyN(aCx, reinterpret_cast<const char16_t*>(jchars), len));
+ NS_ENSURE_TRUE(CheckJS(aCx, !!str), NS_ERROR_FAILURE);
+
+ aOut.setString(str);
+ return NS_OK;
+}
+
+nsresult UnboxValue(JSContext* aCx, const jni::Object::LocalRef& aData,
+ JS::MutableHandle<JS::Value> aOut);
+
+nsresult UnboxBundle(JSContext* aCx, const jni::Object::LocalRef& aData,
+ JS::MutableHandle<JS::Value> aOut) {
+ if (!aData) {
+ aOut.setNull();
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aData.IsInstanceOf<java::GeckoBundle>());
+
+ JNIEnv* const env = aData.Env();
+ const auto& bundle = java::GeckoBundle::Ref::From(aData);
+ jni::ObjectArray::LocalRef keys = bundle->Keys();
+ jni::ObjectArray::LocalRef values = bundle->Values();
+ const size_t len = keys->Length();
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+
+ NS_ENSURE_TRUE(CheckJS(aCx, !!obj), NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(values->Length() == len, NS_ERROR_FAILURE);
+
+ for (size_t i = 0; i < len; i++) {
+ jni::String::LocalRef key = keys->GetElement(i);
+ const size_t keyLen = env->GetStringLength(key.Get());
+ const jchar* const keyChars = env->GetStringChars(key.Get(), nullptr);
+ if (NS_WARN_IF(!keyChars)) {
+ env->ExceptionClear();
+ return NS_ERROR_FAILURE;
+ }
+
+ auto releaseKeyChars = MakeScopeExit([env, &key, keyChars] {
+ env->ReleaseStringChars(key.Get(), keyChars);
+ env->ExceptionClear();
+ });
+
+ JS::Rooted<JS::Value> value(aCx);
+ nsresult rv = UnboxValue(aCx, values->GetElement(i), &value);
+ if (rv == NS_ERROR_INVALID_ARG && !JS_IsExceptionPending(aCx)) {
+ JS_ReportErrorUTF8(
+ aCx, u8"Invalid event data property %s",
+ NS_ConvertUTF16toUTF8(
+ nsString(reinterpret_cast<const char16_t*>(keyChars), keyLen))
+ .get());
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(
+ CheckJS(aCx, JS_SetUCProperty(
+ aCx, obj, reinterpret_cast<const char16_t*>(keyChars),
+ keyLen, value)),
+ NS_ERROR_FAILURE);
+ }
+
+ aOut.setObject(*obj);
+ return NS_OK;
+}
+
+template <typename Type, typename JNIType, typename ArrayType,
+ JNIType* (JNIEnv::*GetElements)(ArrayType, jboolean*),
+ void (JNIEnv::*ReleaseElements)(ArrayType, JNIType*, jint),
+ JS::Value (*ToValue)(Type)>
+nsresult UnboxArrayPrimitive(JSContext* aCx, const jni::Object::LocalRef& aData,
+ JS::MutableHandle<JS::Value> aOut) {
+ JNIEnv* const env = aData.Env();
+ const ArrayType jarray = ArrayType(aData.Get());
+ JNIType* const array = (env->*GetElements)(jarray, nullptr);
+ JS::RootedVector<JS::Value> elements(aCx);
+
+ if (NS_WARN_IF(!array)) {
+ env->ExceptionClear();
+ return NS_ERROR_FAILURE;
+ }
+
+ auto releaseArray = MakeScopeExit([env, jarray, array] {
+ (env->*ReleaseElements)(jarray, array, JNI_ABORT);
+ env->ExceptionClear();
+ });
+
+ const size_t len = env->GetArrayLength(jarray);
+ NS_ENSURE_TRUE(elements.initCapacity(len), NS_ERROR_FAILURE);
+
+ for (size_t i = 0; i < len; i++) {
+ NS_ENSURE_TRUE(elements.append((*ToValue)(Type(array[i]))),
+ NS_ERROR_FAILURE);
+ }
+
+ JS::Rooted<JSObject*> obj(
+ aCx, JS::NewArrayObject(aCx, JS::HandleValueArray(elements)));
+ NS_ENSURE_TRUE(CheckJS(aCx, !!obj), NS_ERROR_FAILURE);
+
+ aOut.setObject(*obj);
+ return NS_OK;
+}
+
+struct StringArray : jni::ObjectBase<StringArray> {
+ static const char name[];
+};
+
+struct GeckoBundleArray : jni::ObjectBase<GeckoBundleArray> {
+ static const char name[];
+};
+
+const char StringArray::name[] = "[Ljava/lang/String;";
+const char GeckoBundleArray::name[] = "[Lorg/mozilla/gecko/util/GeckoBundle;";
+
+template <nsresult (*Unbox)(JSContext*, const jni::Object::LocalRef&,
+ JS::MutableHandle<JS::Value>)>
+nsresult UnboxArrayObject(JSContext* aCx, const jni::Object::LocalRef& aData,
+ JS::MutableHandle<JS::Value> aOut) {
+ jni::ObjectArray::LocalRef array(aData.Env(),
+ jni::ObjectArray::Ref::From(aData));
+ const size_t len = array->Length();
+ JS::Rooted<JSObject*> obj(aCx, JS::NewArrayObject(aCx, len));
+ NS_ENSURE_TRUE(CheckJS(aCx, !!obj), NS_ERROR_FAILURE);
+
+ for (size_t i = 0; i < len; i++) {
+ jni::Object::LocalRef element = array->GetElement(i);
+ JS::Rooted<JS::Value> value(aCx);
+ nsresult rv = (*Unbox)(aCx, element, &value);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_SetElement(aCx, obj, i, value)),
+ NS_ERROR_FAILURE);
+ }
+
+ aOut.setObject(*obj);
+ return NS_OK;
+}
+
+nsresult UnboxValue(JSContext* aCx, const jni::Object::LocalRef& aData,
+ JS::MutableHandle<JS::Value> aOut) {
+ using jni::Java2Native;
+
+ if (!aData) {
+ aOut.setNull();
+ } else if (aData.IsInstanceOf<jni::Boolean>()) {
+ aOut.setBoolean(Java2Native<bool>(aData, aData.Env()));
+ } else if (aData.IsInstanceOf<jni::Integer>()) {
+ aOut.setInt32(Java2Native<int>(aData, aData.Env()));
+ } else if (aData.IsInstanceOf<jni::Byte>() ||
+ aData.IsInstanceOf<jni::Short>()) {
+ aOut.setInt32(java::sdk::Number::Ref::From(aData)->IntValue());
+ } else if (aData.IsInstanceOf<jni::Double>()) {
+ aOut.setNumber(Java2Native<double>(aData, aData.Env()));
+ } else if (aData.IsInstanceOf<jni::Float>() ||
+ aData.IsInstanceOf<jni::Long>()) {
+ aOut.setNumber(java::sdk::Number::Ref::From(aData)->DoubleValue());
+ } else if (aData.IsInstanceOf<jni::String>()) {
+ return UnboxString(aCx, aData, aOut);
+ } else if (aData.IsInstanceOf<jni::Character>()) {
+ return UnboxString(aCx, java::sdk::String::ValueOf(aData), aOut);
+ } else if (aData.IsInstanceOf<java::GeckoBundle>()) {
+ return UnboxBundle(aCx, aData, aOut);
+
+ } else if (aData.IsInstanceOf<jni::BooleanArray>()) {
+ return UnboxArrayPrimitive<
+ bool, jboolean, jbooleanArray, &JNIEnv::GetBooleanArrayElements,
+ &JNIEnv::ReleaseBooleanArrayElements, &JS::BooleanValue>(aCx, aData,
+ aOut);
+
+ } else if (aData.IsInstanceOf<jni::IntArray>()) {
+ return UnboxArrayPrimitive<
+ int32_t, jint, jintArray, &JNIEnv::GetIntArrayElements,
+ &JNIEnv::ReleaseIntArrayElements, &JS::Int32Value>(aCx, aData, aOut);
+
+ } else if (aData.IsInstanceOf<jni::DoubleArray>()) {
+ return UnboxArrayPrimitive<
+ double, jdouble, jdoubleArray, &JNIEnv::GetDoubleArrayElements,
+ &JNIEnv::ReleaseDoubleArrayElements, &JS::DoubleValue>(aCx, aData,
+ aOut);
+
+ } else if (aData.IsInstanceOf<StringArray>()) {
+ return UnboxArrayObject<&UnboxString>(aCx, aData, aOut);
+ } else if (aData.IsInstanceOf<GeckoBundleArray>()) {
+ return UnboxArrayObject<&UnboxBundle>(aCx, aData, aOut);
+ } else {
+ NS_WARNING("Invalid type");
+ return NS_ERROR_INVALID_ARG;
+ }
+ return NS_OK;
+}
+
+nsresult UnboxData(jni::String::Param aEvent, JSContext* aCx,
+ jni::Object::Param aData, JS::MutableHandle<JS::Value> aOut,
+ bool aBundleOnly) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ jni::Object::LocalRef jniData(jni::GetGeckoThreadEnv(), aData);
+ nsresult rv = NS_ERROR_INVALID_ARG;
+
+ if (!aBundleOnly) {
+ rv = UnboxValue(aCx, jniData, aOut);
+ } else if (!jniData || jniData.IsInstanceOf<java::GeckoBundle>()) {
+ rv = UnboxBundle(aCx, jniData, aOut);
+ }
+ if (rv != NS_ERROR_INVALID_ARG || !aEvent) {
+ return rv;
+ }
+
+ nsCString event = aEvent->ToCString();
+ if (JS_IsExceptionPending(aCx)) {
+ JS::WarnUTF8(aCx, "Error dispatching %s", event.get());
+ } else {
+ JS_ReportErrorUTF8(aCx, "Invalid event data for %s", event.get());
+ }
+ return NS_ERROR_INVALID_ARG;
+}
+
+class JavaCallbackDelegate final : public nsIAndroidEventCallback {
+ const java::EventCallback::GlobalRef mCallback;
+
+ virtual ~JavaCallbackDelegate() {}
+
+ NS_IMETHOD Call(JSContext* aCx, JS::Handle<JS::Value> aData,
+ void (java::EventCallback::*aCall)(jni::Object::Param)
+ const) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ jni::Object::LocalRef data(jni::GetGeckoThreadEnv());
+ nsresult rv = BoxData(u"callback"_ns, aCx, aData, data,
+ /* ObjectOnly */ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dom::AutoNoJSAPI nojsapi;
+
+ (java::EventCallback(*mCallback).*aCall)(data);
+ return NS_OK;
+ }
+
+ public:
+ explicit JavaCallbackDelegate(java::EventCallback::Param aCallback)
+ : mCallback(jni::GetGeckoThreadEnv(), aCallback) {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override {
+ return Call(aCx, aData, &java::EventCallback::SendSuccess);
+ }
+
+ NS_IMETHOD OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override {
+ return Call(aCx, aData, &java::EventCallback::SendError);
+ }
+};
+
+NS_IMPL_ISUPPORTS(JavaCallbackDelegate, nsIAndroidEventCallback)
+
+class NativeCallbackDelegateSupport final
+ : public java::EventDispatcher::NativeCallbackDelegate ::Natives<
+ NativeCallbackDelegateSupport> {
+ using CallbackDelegate = java::EventDispatcher::NativeCallbackDelegate;
+ using Base = CallbackDelegate::Natives<NativeCallbackDelegateSupport>;
+
+ const nsCOMPtr<nsIAndroidEventCallback> mCallback;
+ const nsCOMPtr<nsIAndroidEventFinalizer> mFinalizer;
+ const nsCOMPtr<nsIGlobalObject> mGlobalObject;
+
+ void Call(jni::Object::Param aData,
+ nsresult (nsIAndroidEventCallback::*aCall)(JS::Handle<JS::Value>,
+ JSContext*)) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Use either the attached window's realm or a default realm.
+
+ dom::AutoJSAPI jsapi;
+ NS_ENSURE_TRUE_VOID(jsapi.Init(mGlobalObject));
+
+ JS::Rooted<JS::Value> data(jsapi.cx());
+ nsresult rv = UnboxData(u"callback"_ns, jsapi.cx(), aData, &data,
+ /* BundleOnly */ false);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = (mCallback->*aCall)(data, jsapi.cx());
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+
+ public:
+ using Base::AttachNative;
+
+ template <typename Functor>
+ static void OnNativeCall(Functor&& aCall) {
+ if (NS_IsMainThread()) {
+ // Invoke callbacks synchronously if we're already on Gecko thread.
+ return aCall();
+ }
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("OnNativeCall", std::move(aCall)));
+ }
+
+ static void Finalize(const CallbackDelegate::LocalRef& aInstance) {
+ DisposeNative(aInstance);
+ }
+
+ NativeCallbackDelegateSupport(nsIAndroidEventCallback* callback,
+ nsIAndroidEventFinalizer* finalizer,
+ nsIGlobalObject* globalObject)
+ : mCallback(callback),
+ mFinalizer(finalizer),
+ mGlobalObject(globalObject) {}
+
+ ~NativeCallbackDelegateSupport() {
+ if (mFinalizer) {
+ mFinalizer->OnFinalize();
+ }
+ }
+
+ void SendSuccess(jni::Object::Param aData) {
+ Call(aData, &nsIAndroidEventCallback::OnSuccess);
+ }
+
+ void SendError(jni::Object::Param aData) {
+ Call(aData, &nsIAndroidEventCallback::OnError);
+ }
+};
+
+class FinalizingCallbackDelegate final : public nsIAndroidEventCallback {
+ const nsCOMPtr<nsIAndroidEventCallback> mCallback;
+ const nsCOMPtr<nsIAndroidEventFinalizer> mFinalizer;
+
+ virtual ~FinalizingCallbackDelegate() {
+ if (mFinalizer) {
+ mFinalizer->OnFinalize();
+ }
+ }
+
+ public:
+ FinalizingCallbackDelegate(nsIAndroidEventCallback* aCallback,
+ nsIAndroidEventFinalizer* aFinalizer)
+ : mCallback(aCallback), mFinalizer(aFinalizer) {}
+
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_NSIANDROIDEVENTCALLBACK(mCallback->);
+};
+
+NS_IMPL_ISUPPORTS(FinalizingCallbackDelegate, nsIAndroidEventCallback)
+
+} // namespace detail
+
+using namespace detail;
+
+NS_IMPL_ISUPPORTS(EventDispatcher, nsIAndroidEventDispatcher)
+
+nsIGlobalObject* EventDispatcher::GetGlobalObject() {
+ if (mDOMWindow) {
+ return nsGlobalWindowInner::Cast(mDOMWindow->GetCurrentInnerWindow());
+ }
+ return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+}
+
+nsresult EventDispatcher::DispatchOnGecko(ListenersList* list,
+ const nsAString& aEvent,
+ JS::Handle<JS::Value> aData,
+ nsIAndroidEventCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ dom::AutoNoJSAPI nojsapi;
+
+ list->lockCount++;
+
+ auto iteratingScope = MakeScopeExit([list] {
+ list->lockCount--;
+ if (list->lockCount || !list->unregistering) {
+ return;
+ }
+
+ list->unregistering = false;
+ for (ssize_t i = list->listeners.Count() - 1; i >= 0; i--) {
+ if (list->listeners[i]) {
+ continue;
+ }
+ list->listeners.RemoveObjectAt(i);
+ }
+ });
+
+ const size_t count = list->listeners.Count();
+ for (size_t i = 0; i < count; i++) {
+ if (!list->listeners[i]) {
+ // Unregistered.
+ continue;
+ }
+ const nsresult rv = list->listeners[i]->OnEvent(aEvent, aData, aCallback);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+ return NS_OK;
+}
+
+java::EventDispatcher::NativeCallbackDelegate::LocalRef
+EventDispatcher::WrapCallback(nsIAndroidEventCallback* aCallback,
+ nsIAndroidEventFinalizer* aFinalizer) {
+ if (!aCallback) {
+ return java::EventDispatcher::NativeCallbackDelegate::LocalRef(
+ jni::GetGeckoThreadEnv());
+ }
+
+ java::EventDispatcher::NativeCallbackDelegate::LocalRef callback =
+ java::EventDispatcher::NativeCallbackDelegate::New();
+ NativeCallbackDelegateSupport::AttachNative(
+ callback, MakeUnique<NativeCallbackDelegateSupport>(aCallback, aFinalizer,
+ GetGlobalObject()));
+ return callback;
+}
+
+bool EventDispatcher::HasListener(const char16_t* aEvent) {
+ java::EventDispatcher::LocalRef dispatcher(mDispatcher);
+ if (!dispatcher) {
+ return false;
+ }
+
+ nsDependentString event(aEvent);
+ return dispatcher->HasListener(event);
+}
+
+NS_IMETHODIMP
+EventDispatcher::Dispatch(JS::Handle<JS::Value> aEvent,
+ JS::Handle<JS::Value> aData,
+ nsIAndroidEventCallback* aCallback,
+ nsIAndroidEventFinalizer* aFinalizer,
+ JSContext* aCx) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aEvent.isString()) {
+ NS_WARNING("Invalid event name");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoJSString event;
+ NS_ENSURE_TRUE(CheckJS(aCx, event.init(aCx, aEvent.toString())),
+ NS_ERROR_OUT_OF_MEMORY);
+
+ // Don't need to lock here because we're on the main thread, and we can't
+ // race against Register/UnregisterListener.
+
+ ListenersList* list = mListenersMap.Get(event);
+ if (list) {
+ if (!aCallback || !aFinalizer) {
+ return DispatchOnGecko(list, event, aData, aCallback);
+ }
+ nsCOMPtr<nsIAndroidEventCallback> callback(
+ new FinalizingCallbackDelegate(aCallback, aFinalizer));
+ return DispatchOnGecko(list, event, aData, callback);
+ }
+
+ java::EventDispatcher::LocalRef dispatcher(mDispatcher);
+ if (!dispatcher) {
+ return NS_OK;
+ }
+
+ jni::Object::LocalRef data(jni::GetGeckoThreadEnv());
+ nsresult rv = BoxData(event, aCx, aData, data, /* ObjectOnly */ true);
+ // Keep XPConnect from overriding the JSContext exception with one
+ // based on the nsresult.
+ //
+ // XXXbz Does xpconnect still do that? Needs to be checked/tested.
+ NS_ENSURE_SUCCESS(rv, JS_IsExceptionPending(aCx) ? NS_OK : rv);
+
+ dom::AutoNoJSAPI nojsapi;
+ dispatcher->DispatchToThreads(event, data,
+ WrapCallback(aCallback, aFinalizer));
+ return NS_OK;
+}
+
+nsresult EventDispatcher::Dispatch(const char16_t* aEvent,
+ java::GeckoBundle::Param aData,
+ nsIAndroidEventCallback* aCallback) {
+ nsDependentString event(aEvent);
+
+ ListenersList* list = mListenersMap.Get(event);
+ if (list) {
+ dom::AutoJSAPI jsapi;
+ NS_ENSURE_TRUE(jsapi.Init(GetGlobalObject()), NS_ERROR_FAILURE);
+ JS::Rooted<JS::Value> data(jsapi.cx());
+ nsresult rv = UnboxData(/* Event */ nullptr, jsapi.cx(), aData, &data,
+ /* BundleOnly */ true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return DispatchOnGecko(list, event, data, aCallback);
+ }
+
+ java::EventDispatcher::LocalRef dispatcher(mDispatcher);
+ if (!dispatcher) {
+ return NS_OK;
+ }
+
+ dispatcher->DispatchToThreads(event, aData, WrapCallback(aCallback));
+ return NS_OK;
+}
+
+nsresult EventDispatcher::IterateEvents(JSContext* aCx,
+ JS::Handle<JS::Value> aEvents,
+ IterateEventsCallback aCallback,
+ nsIAndroidEventListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(mLock);
+
+ auto processEvent = [this, aCx, aCallback,
+ aListener](JS::Handle<JS::Value> event) -> nsresult {
+ nsAutoJSString str;
+ NS_ENSURE_TRUE(CheckJS(aCx, str.init(aCx, event.toString())),
+ NS_ERROR_OUT_OF_MEMORY);
+ return (this->*aCallback)(str, aListener);
+ };
+
+ if (aEvents.isString()) {
+ return processEvent(aEvents);
+ }
+
+ bool isArray = false;
+ NS_ENSURE_TRUE(aEvents.isObject(), NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(CheckJS(aCx, JS::IsArrayObject(aCx, aEvents, &isArray)),
+ NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(isArray, NS_ERROR_INVALID_ARG);
+
+ JS::Rooted<JSObject*> events(aCx, &aEvents.toObject());
+ uint32_t length = 0;
+ NS_ENSURE_TRUE(CheckJS(aCx, JS::GetArrayLength(aCx, events, &length)),
+ NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(length, NS_ERROR_INVALID_ARG);
+
+ for (size_t i = 0; i < length; i++) {
+ JS::Rooted<JS::Value> event(aCx);
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, events, i, &event)),
+ NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(event.isString(), NS_ERROR_INVALID_ARG);
+
+ const nsresult rv = processEvent(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult EventDispatcher::RegisterEventLocked(
+ const nsAString& aEvent, nsIAndroidEventListener* aListener) {
+ ListenersList* list = mListenersMap.GetOrInsertNew(aEvent);
+
+#ifdef DEBUG
+ for (ssize_t i = 0; i < list->listeners.Count(); i++) {
+ NS_ENSURE_TRUE(list->listeners[i] != aListener,
+ NS_ERROR_ALREADY_INITIALIZED);
+ }
+#endif
+
+ list->listeners.AppendObject(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EventDispatcher::RegisterListener(nsIAndroidEventListener* aListener,
+ JS::Handle<JS::Value> aEvents,
+ JSContext* aCx) {
+ return IterateEvents(aCx, aEvents, &EventDispatcher::RegisterEventLocked,
+ aListener);
+}
+
+nsresult EventDispatcher::UnregisterEventLocked(
+ const nsAString& aEvent, nsIAndroidEventListener* aListener) {
+ ListenersList* list = mListenersMap.Get(aEvent);
+#ifdef DEBUG
+ NS_ENSURE_TRUE(list, NS_ERROR_NOT_INITIALIZED);
+#else
+ NS_ENSURE_TRUE(list, NS_OK);
+#endif
+
+ DebugOnly<bool> found = false;
+ for (ssize_t i = list->listeners.Count() - 1; i >= 0; i--) {
+ if (list->listeners[i] != aListener) {
+ continue;
+ }
+ if (list->lockCount) {
+ // Only mark for removal when list is locked.
+ list->listeners.ReplaceObjectAt(nullptr, i);
+ list->unregistering = true;
+ } else {
+ list->listeners.RemoveObjectAt(i);
+ }
+ found = true;
+ }
+#ifdef DEBUG
+ return found ? NS_OK : NS_ERROR_NOT_INITIALIZED;
+#else
+ return NS_OK;
+#endif
+}
+
+NS_IMETHODIMP
+EventDispatcher::UnregisterListener(nsIAndroidEventListener* aListener,
+ JS::Handle<JS::Value> aEvents,
+ JSContext* aCx) {
+ return IterateEvents(aCx, aEvents, &EventDispatcher::UnregisterEventLocked,
+ aListener);
+}
+
+void EventDispatcher::Attach(java::EventDispatcher::Param aDispatcher,
+ nsPIDOMWindowOuter* aDOMWindow) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDispatcher);
+
+ java::EventDispatcher::LocalRef dispatcher(mDispatcher);
+
+ if (dispatcher) {
+ if (dispatcher == aDispatcher) {
+ // Only need to update the window.
+ mDOMWindow = aDOMWindow;
+ return;
+ }
+ dispatcher->SetAttachedToGecko(java::EventDispatcher::REATTACHING);
+ }
+
+ dispatcher = java::EventDispatcher::LocalRef(aDispatcher);
+ NativesBase::AttachNative(dispatcher, this);
+ mDispatcher = dispatcher;
+ mDOMWindow = aDOMWindow;
+
+ dispatcher->SetAttachedToGecko(java::EventDispatcher::ATTACHED);
+}
+
+void EventDispatcher::Shutdown() {
+ mDispatcher = nullptr;
+ mDOMWindow = nullptr;
+}
+
+void EventDispatcher::Detach() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDispatcher);
+
+ java::EventDispatcher::GlobalRef dispatcher(mDispatcher);
+
+ // SetAttachedToGecko will call disposeNative for us later on the Gecko
+ // thread to make sure all pending dispatchToGecko calls have completed.
+ if (dispatcher) {
+ dispatcher->SetAttachedToGecko(java::EventDispatcher::DETACHED);
+ }
+
+ Shutdown();
+}
+
+bool EventDispatcher::HasGeckoListener(jni::String::Param aEvent) {
+ // Can be called from any thread.
+ MutexAutoLock lock(mLock);
+ return !!mListenersMap.Get(aEvent->ToString());
+}
+
+void EventDispatcher::DispatchToGecko(jni::String::Param aEvent,
+ jni::Object::Param aData,
+ jni::Object::Param aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Don't need to lock here because we're on the main thread, and we can't
+ // race against Register/UnregisterListener.
+
+ nsString event = aEvent->ToString();
+ ListenersList* list = mListenersMap.Get(event);
+ if (!list || list->listeners.IsEmpty()) {
+ return;
+ }
+
+ // Use the same compartment as the attached window if possible, otherwise
+ // use a default compartment.
+ dom::AutoJSAPI jsapi;
+ NS_ENSURE_TRUE_VOID(jsapi.Init(GetGlobalObject()));
+
+ JS::Rooted<JS::Value> data(jsapi.cx());
+ nsresult rv = UnboxData(aEvent, jsapi.cx(), aData, &data,
+ /* BundleOnly */ true);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIAndroidEventCallback> callback;
+ if (aCallback) {
+ callback =
+ new JavaCallbackDelegate(java::EventCallback::Ref::From(aCallback));
+ }
+
+ DispatchOnGecko(list, event, data, callback);
+}
+
+/* static */
+nsresult EventDispatcher::UnboxBundle(JSContext* aCx, jni::Object::Param aData,
+ JS::MutableHandle<JS::Value> aOut) {
+ return detail::UnboxBundle(aCx, aData, aOut);
+}
+
+} // namespace widget
+} // namespace mozilla
+
+#if defined(__clang__)
+# pragma clang diagnostic pop
+#endif
diff --git a/widget/android/EventDispatcher.h b/widget/android/EventDispatcher.h
new file mode 100644
index 0000000000..a7daa18d7d
--- /dev/null
+++ b/widget/android/EventDispatcher.h
@@ -0,0 +1,104 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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/. */
+
+#ifndef mozilla_widget_EventDispatcher_h
+#define mozilla_widget_EventDispatcher_h
+
+#include "jsapi.h"
+#include "nsClassHashtable.h"
+#include "nsCOMArray.h"
+#include "nsIAndroidBridge.h"
+#include "nsHashKeys.h"
+#include "nsPIDOMWindow.h"
+
+#include "mozilla/java/EventDispatcherNatives.h"
+#include "mozilla/java/GeckoBundleWrappers.h"
+#include "mozilla/Mutex.h"
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * EventDispatcher is the Gecko counterpart to the Java EventDispatcher class.
+ * Together, they make up a unified event bus. Events dispatched from the Java
+ * side may notify event listeners on the Gecko side, and vice versa.
+ */
+class EventDispatcher final
+ : public nsIAndroidEventDispatcher,
+ public java::EventDispatcher::Natives<EventDispatcher> {
+ using NativesBase = java::EventDispatcher::Natives<EventDispatcher>;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIANDROIDEVENTDISPATCHER
+
+ EventDispatcher() {}
+
+ void Attach(java::EventDispatcher::Param aDispatcher,
+ nsPIDOMWindowOuter* aDOMWindow);
+ void Detach();
+
+ nsresult Dispatch(const char16_t* aEvent,
+ java::GeckoBundle::Param aData = nullptr,
+ nsIAndroidEventCallback* aCallback = nullptr);
+
+ bool HasListener(const char16_t* aEvent);
+ bool HasGeckoListener(jni::String::Param aEvent);
+ void DispatchToGecko(jni::String::Param aEvent, jni::Object::Param aData,
+ jni::Object::Param aCallback);
+
+ static nsresult UnboxBundle(JSContext* aCx, jni::Object::Param aData,
+ JS::MutableHandle<JS::Value> aOut);
+
+ nsIGlobalObject* GetGlobalObject();
+
+ using NativesBase::DisposeNative;
+
+ private:
+ friend class java::EventDispatcher::Natives<EventDispatcher>;
+
+ java::EventDispatcher::WeakRef mDispatcher;
+ nsCOMPtr<nsPIDOMWindowOuter> mDOMWindow;
+
+ virtual ~EventDispatcher() {}
+
+ void Shutdown();
+
+ struct ListenersList {
+ nsCOMArray<nsIAndroidEventListener> listeners{/* count */ 1};
+ // 0 if the list can be modified
+ uint32_t lockCount{0};
+ // true if this list has a listener that is being unregistered
+ bool unregistering{false};
+ };
+
+ using ListenersMap = nsClassHashtable<nsStringHashKey, ListenersList>;
+
+ Mutex mLock MOZ_UNANNOTATED{"mozilla::widget::EventDispatcher"};
+ ListenersMap mListenersMap;
+
+ using IterateEventsCallback =
+ nsresult (EventDispatcher::*)(const nsAString&, nsIAndroidEventListener*);
+
+ nsresult IterateEvents(JSContext* aCx, JS::Handle<JS::Value> aEvents,
+ IterateEventsCallback aCallback,
+ nsIAndroidEventListener* aListener);
+ nsresult RegisterEventLocked(const nsAString&, nsIAndroidEventListener*);
+ nsresult UnregisterEventLocked(const nsAString&, nsIAndroidEventListener*);
+
+ nsresult DispatchOnGecko(ListenersList* list, const nsAString& aEvent,
+ JS::Handle<JS::Value> aData,
+ nsIAndroidEventCallback* aCallback);
+
+ java::EventDispatcher::NativeCallbackDelegate::LocalRef WrapCallback(
+ nsIAndroidEventCallback* aCallback,
+ nsIAndroidEventFinalizer* aFinalizer = nullptr);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_EventDispatcher_h
diff --git a/widget/android/GeckoBatteryManager.h b/widget/android/GeckoBatteryManager.h
new file mode 100644
index 0000000000..d9a171d0d1
--- /dev/null
+++ b/widget/android/GeckoBatteryManager.h
@@ -0,0 +1,28 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef GeckoBatteryManager_h
+#define GeckoBatteryManager_h
+
+#include "nsAppShell.h"
+
+#include "mozilla/Hal.h"
+#include "mozilla/java/GeckoBatteryManagerNatives.h"
+
+namespace mozilla {
+
+class GeckoBatteryManager final
+ : public java::GeckoBatteryManager::Natives<GeckoBatteryManager> {
+ public:
+ static void OnBatteryChange(double aLevel, bool aCharging,
+ double aRemainingTime) {
+ hal::NotifyBatteryChange(
+ hal::BatteryInformation(aLevel, aCharging, aRemainingTime));
+ }
+};
+
+} // namespace mozilla
+
+#endif // GeckoBatteryManager_h
diff --git a/widget/android/GeckoEditableSupport.cpp b/widget/android/GeckoEditableSupport.cpp
new file mode 100644
index 0000000000..fcac5d5399
--- /dev/null
+++ b/widget/android/GeckoEditableSupport.cpp
@@ -0,0 +1,1684 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 4; indent-tabs-mode: nil; -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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 "GeckoEditableSupport.h"
+
+#include "AndroidRect.h"
+#include "KeyEvent.h"
+#include "PuppetWidget.h"
+#include "nsIContent.h"
+#include "nsITransferable.h"
+#include "nsStringStream.h"
+
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/java/GeckoEditableChildWrappers.h"
+#include "mozilla/java/GeckoServiceChildProcessWrappers.h"
+#include "mozilla/jni/NativesInlines.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/widget/GeckoViewSupport.h"
+
+#include <android/api-level.h>
+#include <android/input.h>
+#include <android/log.h>
+
+#ifdef NIGHTLY_BUILD
+static mozilla::LazyLogModule sGeckoEditableSupportLog("GeckoEditableSupport");
+# define ALOGIME(...) \
+ MOZ_LOG(sGeckoEditableSupportLog, LogLevel::Debug, (__VA_ARGS__))
+#else
+# define ALOGIME(args...) \
+ do { \
+ } while (0)
+#endif
+
+static uint32_t ConvertAndroidKeyCodeToDOMKeyCode(int32_t androidKeyCode) {
+ // Special-case alphanumeric keycodes because they are most common.
+ if (androidKeyCode >= AKEYCODE_A && androidKeyCode <= AKEYCODE_Z) {
+ return androidKeyCode - AKEYCODE_A + NS_VK_A;
+ }
+
+ if (androidKeyCode >= AKEYCODE_0 && androidKeyCode <= AKEYCODE_9) {
+ return androidKeyCode - AKEYCODE_0 + NS_VK_0;
+ }
+
+ switch (androidKeyCode) {
+ // KEYCODE_UNKNOWN (0) ... KEYCODE_HOME (3)
+ case AKEYCODE_BACK:
+ return NS_VK_ESCAPE;
+ // KEYCODE_CALL (5) ... KEYCODE_POUND (18)
+ case AKEYCODE_DPAD_UP:
+ return NS_VK_UP;
+ case AKEYCODE_DPAD_DOWN:
+ return NS_VK_DOWN;
+ case AKEYCODE_DPAD_LEFT:
+ return NS_VK_LEFT;
+ case AKEYCODE_DPAD_RIGHT:
+ return NS_VK_RIGHT;
+ case AKEYCODE_DPAD_CENTER:
+ return NS_VK_RETURN;
+ case AKEYCODE_VOLUME_UP:
+ return NS_VK_VOLUME_UP;
+ case AKEYCODE_VOLUME_DOWN:
+ return NS_VK_VOLUME_DOWN;
+ // KEYCODE_VOLUME_POWER (26) ... KEYCODE_Z (54)
+ case AKEYCODE_COMMA:
+ return NS_VK_COMMA;
+ case AKEYCODE_PERIOD:
+ return NS_VK_PERIOD;
+ case AKEYCODE_ALT_LEFT:
+ return NS_VK_ALT;
+ case AKEYCODE_ALT_RIGHT:
+ return NS_VK_ALT;
+ case AKEYCODE_SHIFT_LEFT:
+ return NS_VK_SHIFT;
+ case AKEYCODE_SHIFT_RIGHT:
+ return NS_VK_SHIFT;
+ case AKEYCODE_TAB:
+ return NS_VK_TAB;
+ case AKEYCODE_SPACE:
+ return NS_VK_SPACE;
+ // KEYCODE_SYM (63) ... KEYCODE_ENVELOPE (65)
+ case AKEYCODE_ENTER:
+ return NS_VK_RETURN;
+ case AKEYCODE_DEL:
+ return NS_VK_BACK; // Backspace
+ case AKEYCODE_GRAVE:
+ return NS_VK_BACK_QUOTE;
+ // KEYCODE_MINUS (69)
+ case AKEYCODE_EQUALS:
+ return NS_VK_EQUALS;
+ case AKEYCODE_LEFT_BRACKET:
+ return NS_VK_OPEN_BRACKET;
+ case AKEYCODE_RIGHT_BRACKET:
+ return NS_VK_CLOSE_BRACKET;
+ case AKEYCODE_BACKSLASH:
+ return NS_VK_BACK_SLASH;
+ case AKEYCODE_SEMICOLON:
+ return NS_VK_SEMICOLON;
+ // KEYCODE_APOSTROPHE (75)
+ case AKEYCODE_SLASH:
+ return NS_VK_SLASH;
+ // KEYCODE_AT (77) ... KEYCODE_MEDIA_FAST_FORWARD (90)
+ case AKEYCODE_MUTE:
+ return NS_VK_VOLUME_MUTE;
+ case AKEYCODE_PAGE_UP:
+ return NS_VK_PAGE_UP;
+ case AKEYCODE_PAGE_DOWN:
+ return NS_VK_PAGE_DOWN;
+ // KEYCODE_PICTSYMBOLS (94) ... KEYCODE_BUTTON_MODE (110)
+ case AKEYCODE_ESCAPE:
+ return NS_VK_ESCAPE;
+ case AKEYCODE_FORWARD_DEL:
+ return NS_VK_DELETE;
+ case AKEYCODE_CTRL_LEFT:
+ return NS_VK_CONTROL;
+ case AKEYCODE_CTRL_RIGHT:
+ return NS_VK_CONTROL;
+ case AKEYCODE_CAPS_LOCK:
+ return NS_VK_CAPS_LOCK;
+ case AKEYCODE_SCROLL_LOCK:
+ return NS_VK_SCROLL_LOCK;
+ // KEYCODE_META_LEFT (117) ... KEYCODE_FUNCTION (119)
+ case AKEYCODE_SYSRQ:
+ return NS_VK_PRINTSCREEN;
+ case AKEYCODE_BREAK:
+ return NS_VK_PAUSE;
+ case AKEYCODE_MOVE_HOME:
+ return NS_VK_HOME;
+ case AKEYCODE_MOVE_END:
+ return NS_VK_END;
+ case AKEYCODE_INSERT:
+ return NS_VK_INSERT;
+ // KEYCODE_FORWARD (125) ... KEYCODE_MEDIA_RECORD (130)
+ case AKEYCODE_F1:
+ return NS_VK_F1;
+ case AKEYCODE_F2:
+ return NS_VK_F2;
+ case AKEYCODE_F3:
+ return NS_VK_F3;
+ case AKEYCODE_F4:
+ return NS_VK_F4;
+ case AKEYCODE_F5:
+ return NS_VK_F5;
+ case AKEYCODE_F6:
+ return NS_VK_F6;
+ case AKEYCODE_F7:
+ return NS_VK_F7;
+ case AKEYCODE_F8:
+ return NS_VK_F8;
+ case AKEYCODE_F9:
+ return NS_VK_F9;
+ case AKEYCODE_F10:
+ return NS_VK_F10;
+ case AKEYCODE_F11:
+ return NS_VK_F11;
+ case AKEYCODE_F12:
+ return NS_VK_F12;
+ case AKEYCODE_NUM_LOCK:
+ return NS_VK_NUM_LOCK;
+ case AKEYCODE_NUMPAD_0:
+ return NS_VK_NUMPAD0;
+ case AKEYCODE_NUMPAD_1:
+ return NS_VK_NUMPAD1;
+ case AKEYCODE_NUMPAD_2:
+ return NS_VK_NUMPAD2;
+ case AKEYCODE_NUMPAD_3:
+ return NS_VK_NUMPAD3;
+ case AKEYCODE_NUMPAD_4:
+ return NS_VK_NUMPAD4;
+ case AKEYCODE_NUMPAD_5:
+ return NS_VK_NUMPAD5;
+ case AKEYCODE_NUMPAD_6:
+ return NS_VK_NUMPAD6;
+ case AKEYCODE_NUMPAD_7:
+ return NS_VK_NUMPAD7;
+ case AKEYCODE_NUMPAD_8:
+ return NS_VK_NUMPAD8;
+ case AKEYCODE_NUMPAD_9:
+ return NS_VK_NUMPAD9;
+ case AKEYCODE_NUMPAD_DIVIDE:
+ return NS_VK_DIVIDE;
+ case AKEYCODE_NUMPAD_MULTIPLY:
+ return NS_VK_MULTIPLY;
+ case AKEYCODE_NUMPAD_SUBTRACT:
+ return NS_VK_SUBTRACT;
+ case AKEYCODE_NUMPAD_ADD:
+ return NS_VK_ADD;
+ case AKEYCODE_NUMPAD_DOT:
+ return NS_VK_DECIMAL;
+ case AKEYCODE_NUMPAD_COMMA:
+ return NS_VK_SEPARATOR;
+ case AKEYCODE_NUMPAD_ENTER:
+ return NS_VK_RETURN;
+ case AKEYCODE_NUMPAD_EQUALS:
+ return NS_VK_EQUALS;
+ // KEYCODE_NUMPAD_LEFT_PAREN (162) ... KEYCODE_CALCULATOR (210)
+
+ // Needs to confirm the behavior. If the key switches the open state
+ // of Japanese IME (or switches input character between Hiragana and
+ // Roman numeric characters), then, it might be better to use
+ // NS_VK_KANJI which is used for Alt+Zenkaku/Hankaku key on Windows.
+ case AKEYCODE_ZENKAKU_HANKAKU:
+ return 0;
+ case AKEYCODE_EISU:
+ return NS_VK_EISU;
+ case AKEYCODE_MUHENKAN:
+ return NS_VK_NONCONVERT;
+ case AKEYCODE_HENKAN:
+ return NS_VK_CONVERT;
+ case AKEYCODE_KATAKANA_HIRAGANA:
+ return 0;
+ case AKEYCODE_YEN:
+ return NS_VK_BACK_SLASH; // Same as other platforms.
+ case AKEYCODE_RO:
+ return NS_VK_BACK_SLASH; // Same as other platforms.
+ case AKEYCODE_KANA:
+ return NS_VK_KANA;
+ case AKEYCODE_ASSIST:
+ return NS_VK_HELP;
+
+ // the A key is the action key for gamepad devices.
+ case AKEYCODE_BUTTON_A:
+ return NS_VK_RETURN;
+
+ default:
+ ALOG(
+ "ConvertAndroidKeyCodeToDOMKeyCode: "
+ "No DOM keycode for Android keycode %d",
+ int(androidKeyCode));
+ return 0;
+ }
+}
+
+static KeyNameIndex ConvertAndroidKeyCodeToKeyNameIndex(
+ int32_t keyCode, int32_t action, int32_t domPrintableKeyValue) {
+ // Special-case alphanumeric keycodes because they are most common.
+ if (keyCode >= AKEYCODE_A && keyCode <= AKEYCODE_Z) {
+ return KEY_NAME_INDEX_USE_STRING;
+ }
+
+ if (keyCode >= AKEYCODE_0 && keyCode <= AKEYCODE_9) {
+ return KEY_NAME_INDEX_USE_STRING;
+ }
+
+ switch (keyCode) {
+#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ // KEYCODE_0 (7) ... KEYCODE_9 (16)
+ case AKEYCODE_STAR: // '*' key
+ case AKEYCODE_POUND: // '#' key
+
+ // KEYCODE_A (29) ... KEYCODE_Z (54)
+
+ case AKEYCODE_COMMA: // ',' key
+ case AKEYCODE_PERIOD: // '.' key
+ case AKEYCODE_SPACE:
+ case AKEYCODE_GRAVE: // '`' key
+ case AKEYCODE_MINUS: // '-' key
+ case AKEYCODE_EQUALS: // '=' key
+ case AKEYCODE_LEFT_BRACKET: // '[' key
+ case AKEYCODE_RIGHT_BRACKET: // ']' key
+ case AKEYCODE_BACKSLASH: // '\' key
+ case AKEYCODE_SEMICOLON: // ';' key
+ case AKEYCODE_APOSTROPHE: // ''' key
+ case AKEYCODE_SLASH: // '/' key
+ case AKEYCODE_AT: // '@' key
+ case AKEYCODE_PLUS: // '+' key
+
+ case AKEYCODE_NUMPAD_0:
+ case AKEYCODE_NUMPAD_1:
+ case AKEYCODE_NUMPAD_2:
+ case AKEYCODE_NUMPAD_3:
+ case AKEYCODE_NUMPAD_4:
+ case AKEYCODE_NUMPAD_5:
+ case AKEYCODE_NUMPAD_6:
+ case AKEYCODE_NUMPAD_7:
+ case AKEYCODE_NUMPAD_8:
+ case AKEYCODE_NUMPAD_9:
+ case AKEYCODE_NUMPAD_DIVIDE:
+ case AKEYCODE_NUMPAD_MULTIPLY:
+ case AKEYCODE_NUMPAD_SUBTRACT:
+ case AKEYCODE_NUMPAD_ADD:
+ case AKEYCODE_NUMPAD_DOT:
+ case AKEYCODE_NUMPAD_COMMA:
+ case AKEYCODE_NUMPAD_EQUALS:
+ case AKEYCODE_NUMPAD_LEFT_PAREN:
+ case AKEYCODE_NUMPAD_RIGHT_PAREN:
+
+ case AKEYCODE_YEN: // yen sign key
+ case AKEYCODE_RO: // Japanese Ro key
+ return KEY_NAME_INDEX_USE_STRING;
+
+ case AKEYCODE_NUM: // XXX Not sure
+ case AKEYCODE_PICTSYMBOLS:
+
+ case AKEYCODE_BUTTON_A:
+ case AKEYCODE_BUTTON_B:
+ case AKEYCODE_BUTTON_C:
+ case AKEYCODE_BUTTON_X:
+ case AKEYCODE_BUTTON_Y:
+ case AKEYCODE_BUTTON_Z:
+ case AKEYCODE_BUTTON_L1:
+ case AKEYCODE_BUTTON_R1:
+ case AKEYCODE_BUTTON_L2:
+ case AKEYCODE_BUTTON_R2:
+ case AKEYCODE_BUTTON_THUMBL:
+ case AKEYCODE_BUTTON_THUMBR:
+ case AKEYCODE_BUTTON_START:
+ case AKEYCODE_BUTTON_SELECT:
+ case AKEYCODE_BUTTON_MODE:
+
+ case AKEYCODE_MEDIA_CLOSE:
+
+ case AKEYCODE_BUTTON_1:
+ case AKEYCODE_BUTTON_2:
+ case AKEYCODE_BUTTON_3:
+ case AKEYCODE_BUTTON_4:
+ case AKEYCODE_BUTTON_5:
+ case AKEYCODE_BUTTON_6:
+ case AKEYCODE_BUTTON_7:
+ case AKEYCODE_BUTTON_8:
+ case AKEYCODE_BUTTON_9:
+ case AKEYCODE_BUTTON_10:
+ case AKEYCODE_BUTTON_11:
+ case AKEYCODE_BUTTON_12:
+ case AKEYCODE_BUTTON_13:
+ case AKEYCODE_BUTTON_14:
+ case AKEYCODE_BUTTON_15:
+ case AKEYCODE_BUTTON_16:
+ return KEY_NAME_INDEX_Unidentified;
+
+ case AKEYCODE_UNKNOWN:
+ MOZ_ASSERT(action != AKEY_EVENT_ACTION_MULTIPLE,
+ "Don't call this when action is AKEY_EVENT_ACTION_MULTIPLE!");
+ // It's actually an unknown key if the action isn't ACTION_MULTIPLE.
+ // However, it might cause text input. So, let's check the value.
+ return domPrintableKeyValue ? KEY_NAME_INDEX_USE_STRING
+ : KEY_NAME_INDEX_Unidentified;
+
+ default:
+ ALOG(
+ "ConvertAndroidKeyCodeToKeyNameIndex: "
+ "No DOM key name index for Android keycode %d",
+ keyCode);
+ return KEY_NAME_INDEX_Unidentified;
+ }
+}
+
+static CodeNameIndex ConvertAndroidScanCodeToCodeNameIndex(int32_t scanCode) {
+ switch (scanCode) {
+#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \
+ case aNativeKey: \
+ return aCodeNameIndex;
+
+#include "NativeKeyToDOMCodeName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX
+
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+}
+
+static void InitKeyEvent(WidgetKeyboardEvent& aEvent, int32_t aAction,
+ int32_t aKeyCode, int32_t aScanCode,
+ int32_t aMetaState, int64_t aTime,
+ int32_t aDomPrintableKeyValue, int32_t aRepeatCount,
+ int32_t aFlags) {
+ const uint32_t domKeyCode = ConvertAndroidKeyCodeToDOMKeyCode(aKeyCode);
+
+ aEvent.mModifiers = nsWindow::GetModifiers(aMetaState);
+ aEvent.mKeyCode = domKeyCode;
+
+ aEvent.mIsRepeat =
+ (aEvent.mMessage == eKeyDown || aEvent.mMessage == eKeyPress) &&
+ ((aFlags & java::sdk::KeyEvent::FLAG_LONG_PRESS) || aRepeatCount);
+
+ aEvent.mKeyNameIndex = ConvertAndroidKeyCodeToKeyNameIndex(
+ aKeyCode, aAction, aDomPrintableKeyValue);
+ aEvent.mCodeNameIndex = ConvertAndroidScanCodeToCodeNameIndex(aScanCode);
+
+ if (aEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
+ aDomPrintableKeyValue) {
+ aEvent.mKeyValue = char16_t(aDomPrintableKeyValue);
+ }
+
+ aEvent.mLocation =
+ WidgetKeyboardEvent::ComputeLocationFromCodeValue(aEvent.mCodeNameIndex);
+ aEvent.mTimeStamp = nsWindow::GetEventTimeStamp(aTime);
+}
+
+static nscolor ConvertAndroidColor(uint32_t aArgb) {
+ return NS_RGBA((aArgb & 0x00ff0000) >> 16, (aArgb & 0x0000ff00) >> 8,
+ (aArgb & 0x000000ff), (aArgb & 0xff000000) >> 24);
+}
+
+static jni::ObjectArray::LocalRef ConvertRectArrayToJavaRectFArray(
+ const nsTArray<LayoutDeviceIntRect>& aRects) {
+ const size_t length = aRects.Length();
+ auto rects = jni::ObjectArray::New<java::sdk::RectF>(length);
+
+ for (size_t i = 0; i < length; i++) {
+ const LayoutDeviceIntRect& tmp = aRects[i];
+
+ auto rect = java::sdk::RectF::New(tmp.x, tmp.y, tmp.XMost(), tmp.YMost());
+ rects->SetElement(i, rect);
+ }
+ return rects;
+}
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(GeckoEditableSupport, TextEventDispatcherListener,
+ nsISupportsWeakReference)
+
+// This is the blocker helper class whether disposing GeckoEditableChild now.
+// During JNI call from GeckoEditableChild, we shouldn't dispose it.
+class MOZ_RAII AutoGeckoEditableBlocker final {
+ public:
+ explicit AutoGeckoEditableBlocker(GeckoEditableSupport* aGeckoEditableSupport)
+ : mGeckoEditable(aGeckoEditableSupport) {
+ mGeckoEditable->AddBlocker();
+ }
+ ~AutoGeckoEditableBlocker() { mGeckoEditable->ReleaseBlocker(); }
+
+ private:
+ RefPtr<GeckoEditableSupport> mGeckoEditable;
+};
+
+RefPtr<TextComposition> GeckoEditableSupport::GetComposition() const {
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ return widget ? IMEStateManager::GetTextCompositionFor(widget) : nullptr;
+}
+
+bool GeckoEditableSupport::RemoveComposition(RemoveCompositionFlag aFlag) {
+ if (!mDispatcher || !mDispatcher->IsComposing()) {
+ return false;
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+
+ NS_ENSURE_SUCCESS(BeginInputTransaction(mDispatcher), false);
+ mDispatcher->CommitComposition(
+ status, aFlag == CANCEL_IME_COMPOSITION ? &EmptyString() : nullptr);
+ return true;
+}
+
+void GeckoEditableSupport::OnKeyEvent(int32_t aAction, int32_t aKeyCode,
+ int32_t aScanCode, int32_t aMetaState,
+ int32_t aKeyPressMetaState, int64_t aTime,
+ int32_t aDomPrintableKeyValue,
+ int32_t aRepeatCount, int32_t aFlags,
+ bool aIsSynthesizedImeKey,
+ jni::Object::Param aOriginalEvent) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ RefPtr<TextEventDispatcher> dispatcher =
+ mDispatcher ? mDispatcher.get()
+ : widget ? widget->GetTextEventDispatcher()
+ : nullptr;
+ NS_ENSURE_TRUE_VOID(dispatcher && widget);
+
+ if (!aIsSynthesizedImeKey) {
+ if (nsWindow* window = GetNsWindow()) {
+ window->UserActivity();
+ }
+ } else if (aIsSynthesizedImeKey && mIMEMaskEventsCount > 0) {
+ // Don't synthesize editor keys when not focused.
+ return;
+ }
+
+ EventMessage msg;
+ if (aAction == java::sdk::KeyEvent::ACTION_DOWN) {
+ msg = eKeyDown;
+ } else if (aAction == java::sdk::KeyEvent::ACTION_UP) {
+ msg = eKeyUp;
+ } else if (aAction == java::sdk::KeyEvent::ACTION_MULTIPLE) {
+ // Keys with multiple action are handled in Java,
+ // and we should never see one here
+ MOZ_CRASH("Cannot handle key with multiple action");
+ } else {
+ NS_WARNING("Unknown key action event");
+ return;
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetKeyboardEvent event(true, msg, widget);
+ InitKeyEvent(event, aAction, aKeyCode, aScanCode, aMetaState, aTime,
+ aDomPrintableKeyValue, aRepeatCount, aFlags);
+
+ if (nsIWidget::UsePuppetWidgets()) {
+ // Don't use native key bindings.
+ event.PreventNativeKeyBindings();
+ }
+
+ if (aIsSynthesizedImeKey) {
+ // Keys synthesized by Java IME code are saved in the mIMEKeyEvents
+ // array until the next IME_REPLACE_TEXT event, at which point
+ // these keys are dispatched in sequence.
+ mIMEKeyEvents.AppendElement(UniquePtr<WidgetEvent>(event.Duplicate()));
+ } else {
+ NS_ENSURE_SUCCESS_VOID(BeginInputTransaction(dispatcher));
+ dispatcher->DispatchKeyboardEvent(msg, event, status);
+ if (widget->Destroyed() || status == nsEventStatus_eConsumeNoDefault) {
+ // Skip default processing.
+ return;
+ }
+ mEditable->OnDefaultKeyEvent(aOriginalEvent);
+ }
+
+ // Only send keypress after keydown.
+ if (msg != eKeyDown) {
+ return;
+ }
+
+ WidgetKeyboardEvent pressEvent(true, eKeyPress, widget);
+ InitKeyEvent(pressEvent, aAction, aKeyCode, aScanCode, aKeyPressMetaState,
+ aTime, aDomPrintableKeyValue, aRepeatCount, aFlags);
+
+ if (nsIWidget::UsePuppetWidgets()) {
+ // Don't use native key bindings.
+ pressEvent.PreventNativeKeyBindings();
+ }
+
+ if (aIsSynthesizedImeKey) {
+ mIMEKeyEvents.AppendElement(UniquePtr<WidgetEvent>(pressEvent.Duplicate()));
+ } else {
+ dispatcher->MaybeDispatchKeypressEvents(pressEvent, status);
+ }
+}
+
+/*
+ * Send dummy key events for pages that are unaware of input events,
+ * to provide web compatibility for pages that depend on key events.
+ */
+void GeckoEditableSupport::SendIMEDummyKeyEvent(nsIWidget* aWidget,
+ EventMessage msg) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ MOZ_ASSERT(mDispatcher);
+
+ WidgetKeyboardEvent event(true, msg, aWidget);
+ // TODO: If we can know scan code of the key event which caused replacing
+ // composition string, we should set mCodeNameIndex here. Then,
+ // we should rename this method because it becomes not a "dummy"
+ // keyboard event.
+ event.mKeyCode = NS_VK_PROCESSKEY;
+ event.mKeyNameIndex = KEY_NAME_INDEX_Process;
+ // KeyboardEvents marked as "processed by IME" shouldn't cause any edit
+ // actions. So, we should set their native key binding to none before
+ // dispatch to avoid crash on PuppetWidget and avoid running redundant
+ // path to look for native key bindings.
+ if (nsIWidget::UsePuppetWidgets()) {
+ event.PreventNativeKeyBindings();
+ }
+ NS_ENSURE_SUCCESS_VOID(BeginInputTransaction(mDispatcher));
+ mDispatcher->DispatchKeyboardEvent(msg, event, status);
+}
+
+void GeckoEditableSupport::AddIMETextChange(
+ const IMENotification::TextChangeDataBase& aChange) {
+ mIMEPendingTextChange.MergeWith(aChange);
+
+ // We may not be in the middle of flushing,
+ // in which case this flag is meaningless.
+ mIMETextChangedDuringFlush = true;
+}
+
+void GeckoEditableSupport::PostFlushIMEChanges() {
+ if (mIMEPendingTextChange.IsValid() || mIMESelectionChanged) {
+ // Already posted
+ return;
+ }
+
+ RefPtr<GeckoEditableSupport> self(this);
+
+ nsAppShell::PostEvent([this, self] {
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (widget && !widget->Destroyed()) {
+ FlushIMEChanges();
+ }
+ });
+}
+
+void GeckoEditableSupport::FlushIMEChanges(FlushChangesFlag aFlags) {
+ // Only send change notifications if we are *not* masking events,
+ // i.e. if we have a focused editor,
+ NS_ENSURE_TRUE_VOID(!mIMEMaskEventsCount);
+
+ if (mIMEDelaySynchronizeReply && mIMEActiveCompositionCount > 0) {
+ // We are still expecting more composition events to be handled. Once
+ // that happens, FlushIMEChanges will be called again.
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ NS_ENSURE_TRUE_VOID(widget);
+
+ struct TextRecord {
+ TextRecord() : start(-1), oldEnd(-1), newEnd(-1) {}
+
+ bool IsValid() const { return start >= 0; }
+
+ nsString text;
+ int32_t start;
+ int32_t oldEnd;
+ int32_t newEnd;
+ };
+ TextRecord textTransaction;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ bool causedOnlyByComposition = mIMEPendingTextChange.IsValid() &&
+ mIMEPendingTextChange.mCausedOnlyByComposition;
+ mIMETextChangedDuringFlush = false;
+
+ auto shouldAbort = [=](bool aForce) -> bool {
+ if (!aForce && !mIMETextChangedDuringFlush) {
+ return false;
+ }
+ // A query event could have triggered more text changes to come in, as
+ // indicated by our flag. If that happens, try flushing IME changes
+ // again.
+ if (aFlags == FLUSH_FLAG_NONE) {
+ FlushIMEChanges(FLUSH_FLAG_RETRY);
+ } else {
+ // Don't retry if already retrying, to avoid infinite loops.
+ __android_log_print(ANDROID_LOG_WARN, "GeckoEditableSupport",
+ "Already retrying IME flush");
+ }
+ return true;
+ };
+
+ if (mIMEPendingTextChange.IsValid() &&
+ (mIMEPendingTextChange.mStartOffset !=
+ mIMEPendingTextChange.mRemovedEndOffset ||
+ mIMEPendingTextChange.mStartOffset !=
+ mIMEPendingTextChange.mAddedEndOffset)) {
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ widget);
+
+ if (mIMEPendingTextChange.mAddedEndOffset !=
+ mIMEPendingTextChange.mStartOffset) {
+ queryTextContentEvent.InitForQueryTextContent(
+ mIMEPendingTextChange.mStartOffset,
+ mIMEPendingTextChange.mAddedEndOffset -
+ mIMEPendingTextChange.mStartOffset);
+ widget->DispatchEvent(&queryTextContentEvent, status);
+
+ if (shouldAbort(NS_WARN_IF(queryTextContentEvent.Failed()))) {
+ return;
+ }
+
+ textTransaction.text = queryTextContentEvent.mReply->DataRef();
+ }
+
+ textTransaction.start =
+ static_cast<int32_t>(mIMEPendingTextChange.mStartOffset);
+ textTransaction.oldEnd =
+ static_cast<int32_t>(mIMEPendingTextChange.mRemovedEndOffset);
+ textTransaction.newEnd =
+ static_cast<int32_t>(mIMEPendingTextChange.mAddedEndOffset);
+ }
+
+ int32_t selStart = -1;
+ int32_t selEnd = -1;
+
+ if (mIMESelectionChanged) {
+ if (mCachedSelection.IsValid()) {
+ selStart = mCachedSelection.mStartOffset;
+ selEnd = mCachedSelection.mEndOffset;
+ } else {
+ // XXX Unfortunately we don't know current selection via selection
+ // change notification.
+ // eQuerySelectedText might be newer data than text change data.
+ // It means that GeckoEditableChild.onSelectionChange may throw
+ // IllegalArgumentException since we don't merge with newer text
+ // change.
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ widget);
+ widget->DispatchEvent(&querySelectedTextEvent, status);
+
+ if (shouldAbort(
+ NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection()))) {
+ return;
+ }
+
+ selStart =
+ static_cast<int32_t>(querySelectedTextEvent.mReply->AnchorOffset());
+ selEnd =
+ static_cast<int32_t>(querySelectedTextEvent.mReply->FocusOffset());
+ }
+
+ if (aFlags == FLUSH_FLAG_RECOVER && textTransaction.IsValid()) {
+ // Sometimes we get out-of-bounds selection during recovery.
+ // Limit the offsets so we don't crash.
+ const int32_t end = textTransaction.start + textTransaction.text.Length();
+ selStart = std::min(selStart, end);
+ selEnd = std::min(selEnd, end);
+ }
+ }
+
+ JNIEnv* const env = jni::GetGeckoThreadEnv();
+ auto flushOnException = [=]() -> bool {
+ if (!env->ExceptionCheck()) {
+ return false;
+ }
+ if (aFlags != FLUSH_FLAG_RECOVER) {
+ // First time seeing an exception; try flushing text.
+ env->ExceptionClear();
+ __android_log_print(ANDROID_LOG_WARN, "GeckoEditableSupport",
+ "Recovering from IME exception");
+ FlushIMEText(FLUSH_FLAG_RECOVER);
+ } else {
+ // Give up because we've already tried.
+#ifdef RELEASE_OR_BETA
+ env->ExceptionClear();
+#else
+ MOZ_CATCH_JNI_EXCEPTION(env);
+#endif
+ }
+ return true;
+ };
+
+ // Commit the text change and selection change transaction.
+ mIMEPendingTextChange.Clear();
+
+ if (textTransaction.IsValid()) {
+ mEditable->OnTextChange(textTransaction.text, textTransaction.start,
+ textTransaction.oldEnd, textTransaction.newEnd,
+ causedOnlyByComposition);
+ if (flushOnException()) {
+ return;
+ }
+ }
+
+ while (mIMEDelaySynchronizeReply && mIMEActiveSynchronizeCount) {
+ mIMEActiveSynchronizeCount--;
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_REPLY_EVENT);
+ }
+ mIMEDelaySynchronizeReply = false;
+ mIMEActiveSynchronizeCount = 0;
+ mIMEActiveCompositionCount = 0;
+
+ if (mIMESelectionChanged) {
+ mIMESelectionChanged = false;
+ if (mDispatcher) {
+ // mCausedOnlyByComposition may be true on committing text.
+ // So even if true, there is no composition.
+ causedOnlyByComposition &= mDispatcher->IsComposing();
+ }
+ mEditable->OnSelectionChange(selStart, selEnd, causedOnlyByComposition);
+ flushOnException();
+ }
+}
+
+void GeckoEditableSupport::FlushIMEText(FlushChangesFlag aFlags) {
+ NS_WARNING_ASSERTION(
+ !mIMEDelaySynchronizeReply || !mIMEActiveCompositionCount,
+ "Cannot synchronize Java text with Gecko text");
+
+ // Notify Java of the newly focused content
+ mIMEPendingTextChange.Clear();
+ mIMESelectionChanged = true;
+
+ // Use 'INT32_MAX / 2' here because subsequent text changes might combine
+ // with this text change, and overflow might occur if we just use
+ // INT32_MAX.
+ IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE);
+ notification.mTextChangeData.mStartOffset = 0;
+ notification.mTextChangeData.mRemovedEndOffset = INT32_MAX / 2;
+ notification.mTextChangeData.mAddedEndOffset = INT32_MAX / 2;
+ NotifyIME(mDispatcher, notification);
+
+ FlushIMEChanges(aFlags);
+}
+
+void GeckoEditableSupport::UpdateCompositionRects() {
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ RefPtr<TextComposition> composition(GetComposition());
+ NS_ENSURE_TRUE_VOID(mDispatcher && widget);
+
+ jni::ObjectArray::LocalRef rects;
+ if (composition) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ uint32_t offset = composition->NativeOffsetOfStartComposition();
+ WidgetQueryContentEvent queryTextRectsEvent(true, eQueryTextRectArray,
+ widget);
+ queryTextRectsEvent.InitForQueryTextRectArray(
+ offset, composition->String().Length());
+ widget->DispatchEvent(&queryTextRectsEvent, status);
+ rects = ConvertRectArrayToJavaRectFArray(
+ queryTextRectsEvent.Succeeded()
+ ? queryTextRectsEvent.mReply->mRectArray
+ : CopyableTArray<mozilla::LayoutDeviceIntRect>());
+ } else {
+ rects = ConvertRectArrayToJavaRectFArray(
+ CopyableTArray<mozilla::LayoutDeviceIntRect>());
+ }
+
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, widget);
+ WidgetQueryContentEvent::Options options;
+ options.mRelativeToInsertionPoint = true;
+ queryCaretRectEvent.InitForQueryCaretRect(0, options);
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ widget->DispatchEvent(&queryCaretRectEvent, status);
+ auto caretRect =
+ queryCaretRectEvent.Succeeded()
+ ? java::sdk::RectF::New(queryCaretRectEvent.mReply->mRect.x,
+ queryCaretRectEvent.mReply->mRect.y,
+ queryCaretRectEvent.mReply->mRect.XMost(),
+ queryCaretRectEvent.mReply->mRect.YMost())
+ : java::sdk::RectF::New();
+
+ mEditable->UpdateCompositionRects(rects, caretRect);
+}
+
+void GeckoEditableSupport::OnImeSynchronize() {
+ AutoGeckoEditableBlocker blocker(this);
+
+ if (mIMEDelaySynchronizeReply) {
+ // If we are waiting for other events to reply,
+ // queue this reply as well.
+ mIMEActiveSynchronizeCount++;
+ return;
+ }
+ if (!mIMEMaskEventsCount) {
+ FlushIMEChanges();
+ }
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_REPLY_EVENT);
+}
+
+void GeckoEditableSupport::OnImeReplaceText(int32_t aStart, int32_t aEnd,
+ jni::String::Param aText) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ if (DoReplaceText(aStart, aEnd, aText)) {
+ mIMEDelaySynchronizeReply = true;
+ }
+
+ OnImeSynchronize();
+}
+
+bool GeckoEditableSupport::DoReplaceText(int32_t aStart, int32_t aEnd,
+ jni::String::Param aText) {
+ ALOGIME("IME: IME_REPLACE_TEXT: text=\"%s\"",
+ NS_ConvertUTF16toUTF8(aText->ToString()).get());
+
+ // Return true if processed and we should reply to the OnImeReplaceText
+ // event later. Return false if _not_ processed and we should reply to the
+ // OnImeReplaceText event now.
+
+ if (mIMEMaskEventsCount > 0) {
+ // Not focused; still reply to events, but don't do anything else.
+ return false;
+ }
+
+ if (nsWindow* window = GetNsWindow()) {
+ window->UserActivity();
+ }
+
+ /*
+ Replace text in Gecko thread from aStart to aEnd with the string text.
+ */
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ NS_ENSURE_TRUE(mDispatcher && widget, false);
+ NS_ENSURE_SUCCESS(BeginInputTransaction(mDispatcher), false);
+
+ RefPtr<TextComposition> composition(GetComposition());
+ MOZ_ASSERT(!composition || !composition->IsEditorHandlingEvent());
+
+ nsString string(aText->ToString());
+ const bool composing = !mIMERanges->IsEmpty();
+ nsEventStatus status = nsEventStatus_eIgnore;
+ bool textChanged = composing;
+ // Whether deleting content before setting or committing composition text.
+ bool performDeletion = false;
+ // Dispatch composition start to set current composition.
+ bool needDispatchCompositionStart = false;
+
+ if (!mIMEKeyEvents.IsEmpty() || !composition || !mDispatcher->IsComposing() ||
+ uint32_t(aStart) != composition->NativeOffsetOfStartComposition() ||
+ uint32_t(aEnd) != composition->NativeOffsetOfStartComposition() +
+ composition->String().Length()) {
+ // Only start a new composition if we have key events,
+ // if we don't have an existing composition, or
+ // the replaced text does not match our composition.
+ textChanged |= RemoveComposition();
+
+#ifdef NIGHTLY_BUILD
+ {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ widget);
+ widget->DispatchEvent(&querySelectedTextEvent, status);
+ if (querySelectedTextEvent.Succeeded()) {
+ ALOGIME(
+ "IME: Current selection: %s",
+ ToString(querySelectedTextEvent.mReply->mOffsetAndData).c_str());
+ }
+ }
+#endif
+
+ // If aStart or aEnd is negative value, we use current selection instead
+ // of updating the selection.
+ if (aStart >= 0 && aEnd >= 0) {
+ // Use text selection to set target position(s) for
+ // insert, or replace, of text.
+ WidgetSelectionEvent event(true, eSetSelection, widget);
+ event.mOffset = uint32_t(aStart);
+ event.mLength = uint32_t(aEnd - aStart);
+ event.mExpandToClusterBoundary = false;
+ event.mReason = nsISelectionListener::IME_REASON;
+ widget->DispatchEvent(&event, status);
+ }
+
+ if (!mIMEKeyEvents.IsEmpty()) {
+ bool ignoreNextKeyPress = false;
+ for (uint32_t i = 0; i < mIMEKeyEvents.Length(); i++) {
+ const auto event = mIMEKeyEvents[i]->AsKeyboardEvent();
+ // widget for duplicated events is initially nullptr.
+ event->mWidget = widget;
+
+ status = nsEventStatus_eIgnore;
+ if (event->mMessage != eKeyPress) {
+ mDispatcher->DispatchKeyboardEvent(event->mMessage, *event, status);
+ // Skip default processing. It means that next key press shouldn't
+ // be dispatched.
+ ignoreNextKeyPress = event->mMessage == eKeyDown &&
+ status == nsEventStatus_eConsumeNoDefault;
+ } else {
+ if (ignoreNextKeyPress) {
+ // Don't dispatch key press since previous key down is consumed.
+ ignoreNextKeyPress = false;
+ continue;
+ }
+ mDispatcher->MaybeDispatchKeypressEvents(*event, status);
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ textChanged = true;
+ }
+ }
+ if (!mDispatcher || widget->Destroyed()) {
+ // Don't wait for any text change event.
+ textChanged = false;
+ break;
+ }
+ }
+ mIMEKeyEvents.Clear();
+ return textChanged;
+ }
+
+ if (aStart != aEnd) {
+ if (composing) {
+ // Actually Gecko doesn't start composition, so it is unnecessary to
+ // delete content before setting composition string.
+ needDispatchCompositionStart = true;
+ } else {
+ // Perform a deletion first.
+ performDeletion = true;
+ }
+ }
+ } else if (composition->String().Equals(string)) {
+ // If the new text is the same as the existing composition text,
+ // the NS_COMPOSITION_CHANGE event does not generate a text
+ // change notification. However, the Java side still expects
+ // one, so we manually generate a notification.
+ //
+ // Also, since this is IME change, we have to set mCausedOnlyByComposition.
+ IMENotification::TextChangeData dummyChange(aStart, aEnd, aEnd, true,
+ false);
+ PostFlushIMEChanges();
+ mIMESelectionChanged = true;
+ AddIMETextChange(dummyChange);
+ textChanged = true;
+ }
+
+ SendIMEDummyKeyEvent(widget, eKeyDown);
+ if (!mDispatcher || widget->Destroyed()) {
+ return false;
+ }
+
+ if (needDispatchCompositionStart) {
+ // StartComposition sets composition string from selected string.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mDispatcher->StartComposition(status);
+ if (!mDispatcher || widget->Destroyed()) {
+ return false;
+ }
+ } else if (performDeletion) {
+ WidgetContentCommandEvent event(true, eContentCommandDelete, widget);
+ widget->DispatchEvent(&event, status);
+ if (!mDispatcher || widget->Destroyed()) {
+ return false;
+ }
+ textChanged = true;
+ }
+
+ if (composing) {
+ mDispatcher->SetPendingComposition(string, mIMERanges);
+ mDispatcher->FlushPendingComposition(status);
+ mIMEActiveCompositionCount++;
+ // Ensure IME ranges are empty.
+ mIMERanges->Clear();
+ } else if (!string.IsEmpty() || mDispatcher->IsComposing()) {
+ mDispatcher->CommitComposition(status, &string);
+ mIMEActiveCompositionCount++;
+ textChanged = true;
+ }
+ if (!mDispatcher || widget->Destroyed()) {
+ return false;
+ }
+
+ SendIMEDummyKeyEvent(widget, eKeyUp);
+ // Widget may be destroyed after dispatching the above event.
+
+ return textChanged;
+}
+
+void GeckoEditableSupport::OnImeAddCompositionRange(
+ int32_t aStart, int32_t aEnd, int32_t aRangeType, int32_t aRangeStyle,
+ int32_t aRangeLineStyle, bool aRangeBoldLine, int32_t aRangeForeColor,
+ int32_t aRangeBackColor, int32_t aRangeLineColor) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ if (mIMEMaskEventsCount > 0) {
+ // Not focused.
+ return;
+ }
+
+ TextRange range;
+ range.mStartOffset = aStart;
+ range.mEndOffset = aEnd;
+ range.mRangeType = ToTextRangeType(aRangeType);
+ range.mRangeStyle.mDefinedStyles = aRangeStyle;
+ range.mRangeStyle.mLineStyle = TextRangeStyle::ToLineStyle(aRangeLineStyle);
+ range.mRangeStyle.mIsBoldLine = aRangeBoldLine;
+ range.mRangeStyle.mForegroundColor =
+ ConvertAndroidColor(uint32_t(aRangeForeColor));
+ range.mRangeStyle.mBackgroundColor =
+ ConvertAndroidColor(uint32_t(aRangeBackColor));
+ range.mRangeStyle.mUnderlineColor =
+ ConvertAndroidColor(uint32_t(aRangeLineColor));
+ mIMERanges->AppendElement(range);
+}
+
+void GeckoEditableSupport::OnImeUpdateComposition(int32_t aStart, int32_t aEnd,
+ int32_t aFlags) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ if (DoUpdateComposition(aStart, aEnd, aFlags)) {
+ mIMEDelaySynchronizeReply = true;
+ }
+}
+
+bool GeckoEditableSupport::DoUpdateComposition(int32_t aStart, int32_t aEnd,
+ int32_t aFlags) {
+ if (mIMEMaskEventsCount > 0) {
+ // Not focused.
+ return false;
+ }
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ nsEventStatus status = nsEventStatus_eIgnore;
+ NS_ENSURE_TRUE(mDispatcher && widget, false);
+
+ const bool keepCurrent =
+ !!(aFlags & java::GeckoEditableChild::FLAG_KEEP_CURRENT_COMPOSITION);
+
+ // A composition with no ranges means we want to set the selection.
+ if (mIMERanges->IsEmpty()) {
+ if (keepCurrent && mDispatcher->IsComposing()) {
+ // Don't set selection if we want to keep current composition.
+ return false;
+ }
+
+ MOZ_ASSERT(aStart >= 0 && aEnd >= 0);
+ const bool compositionChanged = RemoveComposition();
+
+ WidgetSelectionEvent selEvent(true, eSetSelection, widget);
+ selEvent.mOffset = std::min(aStart, aEnd);
+ selEvent.mLength = std::max(aStart, aEnd) - selEvent.mOffset;
+ selEvent.mReversed = aStart > aEnd;
+ selEvent.mExpandToClusterBoundary = false;
+ widget->DispatchEvent(&selEvent, status);
+ return compositionChanged;
+ }
+
+ /**
+ * Update the composition from aStart to aEnd using information from added
+ * ranges. This is only used for visual indication and does not affect the
+ * text content. Only the offsets are specified and not the text content
+ * to eliminate the possibility of this event altering the text content
+ * unintentionally.
+ */
+ nsString string;
+ RefPtr<TextComposition> composition(GetComposition());
+ MOZ_ASSERT(!composition || !composition->IsEditorHandlingEvent());
+
+ if (!composition || !mDispatcher->IsComposing() ||
+ uint32_t(aStart) != composition->NativeOffsetOfStartComposition() ||
+ uint32_t(aEnd) != composition->NativeOffsetOfStartComposition() +
+ composition->String().Length()) {
+ if (keepCurrent) {
+ // Don't start a new composition if we want to keep the current one.
+ mIMERanges->Clear();
+ return false;
+ }
+
+ // Only start new composition if we don't have an existing one,
+ // or if the existing composition doesn't match the new one.
+ RemoveComposition();
+
+ {
+ WidgetSelectionEvent event(true, eSetSelection, widget);
+ event.mOffset = uint32_t(aStart);
+ event.mLength = uint32_t(aEnd - aStart);
+ event.mExpandToClusterBoundary = false;
+ event.mReason = nsISelectionListener::IME_REASON;
+ widget->DispatchEvent(&event, status);
+ }
+
+ {
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ widget);
+ widget->DispatchEvent(&querySelectedTextEvent, status);
+ MOZ_ASSERT(querySelectedTextEvent.Succeeded());
+ if (querySelectedTextEvent.FoundSelection()) {
+ string = querySelectedTextEvent.mReply->DataRef();
+ }
+ }
+ } else {
+ // If the new composition matches the existing composition,
+ // reuse the old composition.
+ string = composition->String();
+ }
+
+ ALOGIME("IME: IME_SET_TEXT: text=\"%s\", length=%zu, range=%zu",
+ NS_ConvertUTF16toUTF8(string).get(), string.Length(),
+ mIMERanges->Length());
+
+ if (NS_WARN_IF(NS_FAILED(BeginInputTransaction(mDispatcher)))) {
+ mIMERanges->Clear();
+ return false;
+ }
+ mDispatcher->SetPendingComposition(string, mIMERanges);
+ mDispatcher->FlushPendingComposition(status);
+ mIMEActiveCompositionCount++;
+ mIMERanges->Clear();
+ return true;
+}
+
+void GeckoEditableSupport::OnImeRequestCursorUpdates(int aRequestMode) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ if (aRequestMode == EditableClient::ONE_SHOT) {
+ UpdateCompositionRects();
+ return;
+ }
+
+ mIMEMonitorCursor = (aRequestMode == EditableClient::START_MONITOR);
+}
+
+class MOZ_STACK_CLASS AutoSelectionRestore final {
+ public:
+ explicit AutoSelectionRestore(nsIWidget* widget,
+ TextEventDispatcher* dispatcher)
+ : mWidget(widget), mDispatcher(dispatcher) {
+ MOZ_ASSERT(widget);
+ if (!dispatcher || !dispatcher->IsComposing()) {
+ mOffset = UINT32_MAX;
+ mLength = UINT32_MAX;
+ return;
+ }
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ widget);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ widget->DispatchEvent(&querySelectedTextEvent, status);
+ if (querySelectedTextEvent.DidNotFindSelection()) {
+ mOffset = UINT32_MAX;
+ mLength = UINT32_MAX;
+ return;
+ }
+
+ mOffset = querySelectedTextEvent.mReply->StartOffset();
+ mLength = querySelectedTextEvent.mReply->DataLength();
+ }
+
+ ~AutoSelectionRestore() {
+ if (mWidget->Destroyed() || mOffset == UINT32_MAX) {
+ return;
+ }
+
+ WidgetSelectionEvent selection(true, eSetSelection, mWidget);
+ selection.mOffset = mOffset;
+ selection.mLength = mLength;
+ selection.mExpandToClusterBoundary = false;
+ selection.mReason = nsISelectionListener::IME_REASON;
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mWidget->DispatchEvent(&selection, status);
+ }
+
+ private:
+ nsCOMPtr<nsIWidget> mWidget;
+ RefPtr<TextEventDispatcher> mDispatcher;
+ uint32_t mOffset;
+ uint32_t mLength;
+};
+
+void GeckoEditableSupport::OnImeRequestCommit() {
+ AutoGeckoEditableBlocker blocker(this);
+
+ if (mIMEMaskEventsCount > 0) {
+ // Not focused.
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (NS_WARN_IF(!widget)) {
+ return;
+ }
+
+ AutoSelectionRestore restore(widget, mDispatcher);
+
+ RemoveComposition(COMMIT_IME_COMPOSITION);
+}
+
+void GeckoEditableSupport::AsyncNotifyIME(int32_t aNotification) {
+ RefPtr<GeckoEditableSupport> self(this);
+
+ nsAppShell::PostEvent([this, self, aNotification] {
+ if (!mIMEMaskEventsCount) {
+ mEditable->NotifyIME(aNotification);
+ }
+ });
+}
+
+nsresult GeckoEditableSupport::NotifyIME(
+ TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) {
+ MOZ_ASSERT(mEditable);
+
+ switch (aNotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION: {
+ ALOGIME("IME: REQUEST_TO_COMMIT_COMPOSITION");
+
+ RemoveComposition(COMMIT_IME_COMPOSITION);
+ AsyncNotifyIME(EditableListener::NOTIFY_IME_TO_COMMIT_COMPOSITION);
+ break;
+ }
+
+ case REQUEST_TO_CANCEL_COMPOSITION: {
+ ALOGIME("IME: REQUEST_TO_CANCEL_COMPOSITION");
+
+ RemoveComposition(CANCEL_IME_COMPOSITION);
+ AsyncNotifyIME(EditableListener::NOTIFY_IME_TO_CANCEL_COMPOSITION);
+ break;
+ }
+
+ case NOTIFY_IME_OF_FOCUS: {
+ ALOGIME("IME: NOTIFY_IME_OF_FOCUS");
+
+ mIMEFocusCount++;
+
+ RefPtr<GeckoEditableSupport> self(this);
+ RefPtr<TextEventDispatcher> dispatcher = aTextEventDispatcher;
+
+ // Post an event because we have to flush the text before sending a
+ // focus event, and we may not be able to flush text during the
+ // NotifyIME call.
+ nsAppShell::PostEvent([this, self, dispatcher] {
+ nsCOMPtr<nsIWidget> widget = dispatcher->GetWidget();
+
+ --mIMEMaskEventsCount;
+ if (!mIMEFocusCount || !widget || widget->Destroyed()) {
+ return;
+ }
+
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_TOKEN);
+
+ if (mIsRemote) {
+ if (!mEditableAttached) {
+ // Re-attach on focus; see OnRemovedFrom().
+ jni::NativeWeakPtrHolder<GeckoEditableSupport>::AttachExisting(
+ mEditable, do_AddRef(this));
+ mEditableAttached = true;
+ }
+ // Because GeckoEditableSupport in content process doesn't
+ // manage the active input context, we need to retrieve the
+ // input context from the widget, for use by
+ // OnImeReplaceText.
+ mInputContext = widget->GetInputContext();
+ }
+ mDispatcher = dispatcher;
+ mIMEKeyEvents.Clear();
+
+ mCachedSelection.Reset();
+
+ mIMEDelaySynchronizeReply = false;
+ mIMEActiveCompositionCount = 0;
+ FlushIMEText();
+
+ // IME will call requestCursorUpdates after getting context.
+ // So reset cursor update mode before getting context.
+ mIMEMonitorCursor = false;
+
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_FOCUS);
+ });
+ break;
+ }
+
+ case NOTIFY_IME_OF_BLUR: {
+ ALOGIME("IME: NOTIFY_IME_OF_BLUR");
+
+ mIMEFocusCount--;
+ MOZ_ASSERT(mIMEFocusCount >= 0);
+
+ RefPtr<GeckoEditableSupport> self(this);
+ nsAppShell::PostEvent([this, self] {
+ if (!mIMEFocusCount) {
+ mIMEDelaySynchronizeReply = false;
+ mIMEActiveSynchronizeCount = 0;
+ mIMEActiveCompositionCount = 0;
+ mInputContext.ShutDown();
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_BLUR);
+ OnRemovedFrom(mDispatcher);
+ }
+ });
+
+ // Mask events because we lost focus. Unmask on the next focus.
+ mIMEMaskEventsCount++;
+ break;
+ }
+
+ case NOTIFY_IME_OF_SELECTION_CHANGE: {
+ ALOGIME("IME: NOTIFY_IME_OF_SELECTION_CHANGE: SelectionChangeData=%s",
+ ToString(aNotification.mSelectionChangeData).c_str());
+
+ if (aNotification.mSelectionChangeData.HasRange()) {
+ mCachedSelection.mStartOffset = static_cast<int32_t>(
+ aNotification.mSelectionChangeData.AnchorOffset());
+ mCachedSelection.mEndOffset = static_cast<int32_t>(
+ aNotification.mSelectionChangeData.FocusOffset());
+ } else {
+ mCachedSelection.Reset();
+ }
+
+ PostFlushIMEChanges();
+ mIMESelectionChanged = true;
+ break;
+ }
+
+ case NOTIFY_IME_OF_TEXT_CHANGE: {
+ ALOGIME("IME: NOTIFY_IME_OF_TEXT_CHANGE: TextChangeData=%s",
+ ToString(aNotification.mTextChangeData).c_str());
+
+ /* Make sure Java's selection is up-to-date */
+ PostFlushIMEChanges();
+ mIMESelectionChanged = true;
+ AddIMETextChange(aNotification.mTextChangeData);
+ break;
+ }
+
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: {
+ ALOGIME("IME: NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED");
+
+ // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED isn't sent per IME call.
+ // Receiving this event means that Gecko has already handled all IME
+ // composing events in queue.
+ //
+ if (mIsRemote) {
+ OnNotifyIMEOfCompositionEventHandled();
+ } else {
+ // Also, when receiving this event, mIMEDelaySynchronizeReply won't
+ // update yet on non-e10s case since IME event is posted before updating
+ // it. So we have to delay handling of this event.
+ RefPtr<GeckoEditableSupport> self(this);
+ nsAppShell::PostEvent(
+ [this, self] { OnNotifyIMEOfCompositionEventHandled(); });
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ return NS_OK;
+}
+
+void GeckoEditableSupport::OnNotifyIMEOfCompositionEventHandled() {
+ // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED may be merged with multiple events,
+ // so reset count.
+ mIMEActiveCompositionCount = 0;
+ if (mIMEDelaySynchronizeReply) {
+ FlushIMEChanges();
+ }
+
+ // Hardware keyboard support requires each string rect.
+ if (mIMEMonitorCursor) {
+ UpdateCompositionRects();
+ }
+}
+
+void GeckoEditableSupport::OnRemovedFrom(
+ TextEventDispatcher* aTextEventDispatcher) {
+ mDispatcher = nullptr;
+
+ if (mIsRemote && mEditable->HasEditableParent()) {
+ // When we're remote, detach every time.
+ OnWeakNonIntrusiveDetach(NS_NewRunnableFunction(
+ "GeckoEditableSupport::OnRemovedFrom",
+ [editable = java::GeckoEditableChild::GlobalRef(mEditable)] {
+ DisposeNative(editable);
+ }));
+ }
+}
+
+void GeckoEditableSupport::WillDispatchKeyboardEvent(
+ TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
+ void* aData) {}
+
+NS_IMETHODIMP_(IMENotificationRequests)
+GeckoEditableSupport::GetIMENotificationRequests() {
+ return IMENotificationRequests(IMENotificationRequests::NOTIFY_TEXT_CHANGE);
+}
+
+static bool ShouldKeyboardDismiss(const nsAString& aInputType,
+ const nsAString& aInputMode) {
+ // Some input type uses the prompt to input value. So it is unnecessary to
+ // show software keyboard.
+ return aInputMode.EqualsLiteral("none") || aInputType.EqualsLiteral("date") ||
+ aInputType.EqualsLiteral("time") ||
+ aInputType.EqualsLiteral("month") ||
+ aInputType.EqualsLiteral("week") ||
+ aInputType.EqualsLiteral("datetime-local");
+}
+
+void GeckoEditableSupport::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ // SetInputContext is called from chrome process only
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(mEditable);
+
+ ALOGIME(
+ "IME: SetInputContext: aContext=%s, "
+ "aAction={mCause=%s, mFocusChange=%s}",
+ ToString(aContext).c_str(), ToString(aAction.mCause).c_str(),
+ ToString(aAction.mFocusChange).c_str());
+
+ mInputContext = aContext;
+
+ if (mInputContext.mIMEState.mEnabled != IMEEnabled::Disabled &&
+ !ShouldKeyboardDismiss(mInputContext.mHTMLInputType,
+ mInputContext.mHTMLInputMode) &&
+ aAction.UserMightRequestOpenVKB()) {
+ // Don't reset keyboard when we should simply open the vkb
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_OPEN_VKB);
+ return;
+ }
+
+ // Post an event to keep calls in order relative to NotifyIME.
+ nsAppShell::PostEvent([this, self = RefPtr<GeckoEditableSupport>(this),
+ context = mInputContext, action = aAction] {
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+
+ if (!widget || widget->Destroyed()) {
+ return;
+ }
+ NotifyIMEContext(context, action);
+ });
+}
+
+void GeckoEditableSupport::NotifyIMEContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ const bool inPrivateBrowsing = aContext.mInPrivateBrowsing;
+ // isUserAction is used whether opening virtual keyboard. But long press
+ // shouldn't open it.
+ const bool isUserAction =
+ aAction.mCause != InputContextAction::CAUSE_LONGPRESS &&
+ !(aAction.mCause == InputContextAction::CAUSE_UNKNOWN_CHROME &&
+ aContext.mIMEState.mEnabled == IMEEnabled::Enabled) &&
+ (aAction.IsHandlingUserInput() || aContext.mHasHandledUserInput);
+ const int32_t flags =
+ (inPrivateBrowsing ? EditableListener::IME_FLAG_PRIVATE_BROWSING : 0) |
+ (isUserAction ? EditableListener::IME_FLAG_USER_ACTION : 0) |
+ (aAction.mFocusChange == InputContextAction::FOCUS_NOT_CHANGED
+ ? EditableListener::IME_FOCUS_NOT_CHANGED
+ : 0);
+
+ mEditable->NotifyIMEContext(static_cast<int32_t>(aContext.mIMEState.mEnabled),
+ aContext.mHTMLInputType, aContext.mHTMLInputMode,
+ aContext.mActionHint, aContext.mAutocapitalize,
+ flags);
+}
+
+InputContext GeckoEditableSupport::GetInputContext() {
+ // GetInputContext is called from chrome process only
+ MOZ_ASSERT(XRE_IsParentProcess());
+ InputContext context = mInputContext;
+ context.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
+ return context;
+}
+
+void GeckoEditableSupport::TransferParent(jni::Object::Param aEditableParent) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ mEditable->SetParent(aEditableParent);
+
+ // If we are already focused, make sure the new parent has our token
+ // and focus information, so it can accept additional calls from us.
+ if (mIMEFocusCount > 0) {
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_TOKEN);
+ if (mIsRemote) {
+ // GeckoEditableSupport::SetInputContext is called on chrome process
+ // only, so mInputContext may be still invalid since it is set after
+ // we have gotton focus.
+ RefPtr<GeckoEditableSupport> self(this);
+ nsAppShell::PostEvent([self = std::move(self)] {
+ NS_WARNING_ASSERTION(
+ self->mDispatcher,
+ "Text dispatcher is still null. Why don't we get focus yet?");
+ self->NotifyIMEContext(self->mInputContext, InputContextAction());
+ });
+ } else {
+ NotifyIMEContext(mInputContext, InputContextAction());
+ }
+ mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_FOCUS);
+ // We have focus, so don't destroy editable child.
+ return;
+ }
+
+ if (mIsRemote && !mDispatcher) {
+ // Detach now if we were only attached temporarily.
+ OnRemovedFrom(/* dispatcher */ nullptr);
+ }
+}
+
+void GeckoEditableSupport::SetOnBrowserChild(dom::BrowserChild* aBrowserChild) {
+ MOZ_ASSERT(!XRE_IsParentProcess());
+ NS_ENSURE_TRUE_VOID(aBrowserChild);
+
+ const dom::ContentChild* const contentChild =
+ dom::ContentChild::GetSingleton();
+ RefPtr<widget::PuppetWidget> widget(aBrowserChild->WebWidget());
+ NS_ENSURE_TRUE_VOID(contentChild && widget);
+
+ // Get the content/tab ID in order to get the correct
+ // IGeckoEditableParent object, which GeckoEditableChild uses to
+ // communicate with the parent process.
+ const uint64_t contentId = contentChild->GetID();
+ const uint64_t tabId = aBrowserChild->GetTabId();
+ NS_ENSURE_TRUE_VOID(contentId && tabId);
+
+ RefPtr<widget::TextEventDispatcherListener> listener =
+ widget->GetNativeTextEventDispatcherListener();
+
+ if (!listener ||
+ listener.get() ==
+ static_cast<widget::TextEventDispatcherListener*>(widget)) {
+ // We need to set a new listener.
+ const auto editableChild = java::GeckoEditableChild::New(
+ /* parent */ nullptr, /* default */ false);
+
+ // Temporarily attach so we can receive the initial editable parent.
+ auto editableSupport =
+ jni::NativeWeakPtrHolder<GeckoEditableSupport>::Attach(editableChild,
+ editableChild);
+ auto accEditableSupport(editableSupport.Access());
+ MOZ_RELEASE_ASSERT(accEditableSupport);
+
+ // Tell PuppetWidget to use our listener for IME operations.
+ widget->SetNativeTextEventDispatcherListener(
+ accEditableSupport.AsRefPtr().get());
+
+ accEditableSupport->mEditableAttached = true;
+
+ // Connect the new child to a parent that corresponds to the BrowserChild.
+ java::GeckoServiceChildProcess::GetEditableParent(editableChild, contentId,
+ tabId);
+ return;
+ }
+
+ // We need to update the existing listener to use the new parent.
+
+ // We expect the existing TextEventDispatcherListener to be a
+ // GeckoEditableSupport object, so we perform a sanity check to make
+ // sure, by comparing their respective vtable pointers.
+ const RefPtr<widget::GeckoEditableSupport> dummy =
+ new widget::GeckoEditableSupport(/* child */ nullptr);
+ NS_ENSURE_TRUE_VOID(*reinterpret_cast<const uintptr_t*>(listener.get()) ==
+ *reinterpret_cast<const uintptr_t*>(dummy.get()));
+
+ const auto support =
+ static_cast<widget::GeckoEditableSupport*>(listener.get());
+ if (!support->mEditableAttached) {
+ // Temporarily attach so we can receive the initial editable parent.
+ jni::NativeWeakPtrHolder<GeckoEditableSupport>::AttachExisting(
+ support->GetJavaEditable(), do_AddRef(support));
+ support->mEditableAttached = true;
+ }
+
+ // Transfer to a new parent that corresponds to the BrowserChild.
+ java::GeckoServiceChildProcess::GetEditableParent(support->GetJavaEditable(),
+ contentId, tabId);
+}
+
+nsIWidget* GeckoEditableSupport::GetWidget() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDispatcher ? mDispatcher->GetWidget() : GetNsWindow();
+}
+
+nsWindow* GeckoEditableSupport::GetNsWindow() const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto acc(mWindow.Access());
+ if (!acc) {
+ return nullptr;
+ }
+
+ return acc->GetNsWindow();
+}
+
+void GeckoEditableSupport::OnImeInsertImage(jni::ByteArray::Param aData,
+ jni::String::Param aMimeType) {
+ AutoGeckoEditableBlocker blocker(this);
+
+ nsCString mimeType(aMimeType->ToCString());
+ nsCOMPtr<nsIInputStream> byteStream;
+
+ nsresult rv = NS_NewByteInputStream(
+ getter_AddRefs(byteStream),
+ mozilla::Span(
+ reinterpret_cast<const char*>(aData->GetElements().Elements()),
+ aData->Length()),
+ NS_ASSIGNMENT_COPY);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsITransferable> trans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ if (NS_WARN_IF(!trans)) {
+ return;
+ }
+ trans->Init(nullptr);
+ rv = trans->SetTransferData(mimeType.get(), byteStream);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (NS_WARN_IF(!widget) || NS_WARN_IF(widget->Destroyed())) {
+ return;
+ }
+
+ WidgetContentCommandEvent command(true, eContentCommandPasteTransferable,
+ widget);
+ command.mTransferable = trans.forget();
+ nsEventStatus status;
+ widget->DispatchEvent(&command, status);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/GeckoEditableSupport.h b/widget/android/GeckoEditableSupport.h
new file mode 100644
index 0000000000..90cb2710d3
--- /dev/null
+++ b/widget/android/GeckoEditableSupport.h
@@ -0,0 +1,286 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef mozilla_widget_GeckoEditableSupport_h
+#define mozilla_widget_GeckoEditableSupport_h
+
+#include "nsAppShell.h"
+#include "nsIWidget.h"
+#include "nsTArray.h"
+
+#include "mozilla/java/GeckoEditableChildNatives.h"
+#include "mozilla/java/SessionTextInputWrappers.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "mozilla/UniquePtr.h"
+
+class nsWindow;
+
+namespace mozilla {
+
+class TextComposition;
+
+namespace dom {
+class BrowserChild;
+}
+
+namespace widget {
+
+class GeckoEditableSupport final
+ : public TextEventDispatcherListener,
+ public java::GeckoEditableChild::Natives<GeckoEditableSupport> {
+ /*
+ Rules for managing IME between Gecko and Java:
+
+ * Gecko controls the text content, and Java shadows the Gecko text
+ through text updates
+ * Gecko and Java maintain separate selections, and synchronize when
+ needed through selection updates and set-selection events
+ * Java controls the composition, and Gecko shadows the Java
+ composition through update composition events
+ */
+
+ using EditableBase = java::GeckoEditableChild::Natives<GeckoEditableSupport>;
+ using EditableClient = java::SessionTextInput::EditableClient;
+ using EditableListener = java::SessionTextInput::EditableListener;
+
+ enum FlushChangesFlag {
+ // Not retrying.
+ FLUSH_FLAG_NONE,
+ // Retrying due to IME text changes during flush.
+ FLUSH_FLAG_RETRY,
+ // Retrying due to IME sync exceptions during flush.
+ FLUSH_FLAG_RECOVER
+ };
+
+ enum RemoveCompositionFlag { CANCEL_IME_COMPOSITION, COMMIT_IME_COMPOSITION };
+
+ const bool mIsRemote;
+ jni::NativeWeakPtr<GeckoViewSupport> mWindow; // Parent only
+ RefPtr<TextEventDispatcher> mDispatcher;
+ java::GeckoEditableChild::GlobalRef mEditable;
+ bool mEditableAttached;
+ InputContext mInputContext;
+ AutoTArray<UniquePtr<mozilla::WidgetEvent>, 4> mIMEKeyEvents;
+ IMENotification::TextChangeData mIMEPendingTextChange;
+ RefPtr<TextRangeArray> mIMERanges;
+ RefPtr<Runnable> mDisposeRunnable;
+ int32_t mIMEMaskEventsCount; // Mask events when > 0.
+ int32_t mIMEFocusCount; // We are focused when > 0.
+ bool mIMEDelaySynchronizeReply; // We reply asynchronously when true.
+ int32_t mIMEActiveSynchronizeCount; // The number of replies being delayed.
+ int32_t mIMEActiveCompositionCount; // The number of compositions expected.
+ uint32_t mDisposeBlockCount;
+ bool mIMESelectionChanged;
+ bool mIMETextChangedDuringFlush;
+ bool mIMEMonitorCursor;
+
+ // The cached selection data
+ struct Selection {
+ Selection() : mStartOffset(-1), mEndOffset(-1) {}
+
+ void Reset() {
+ mStartOffset = -1;
+ mEndOffset = -1;
+ }
+
+ bool IsValid() const { return mStartOffset >= 0 && mEndOffset >= 0; }
+
+ int32_t mStartOffset;
+ int32_t mEndOffset;
+ } mCachedSelection;
+
+ nsIWidget* GetWidget() const;
+ nsWindow* GetNsWindow() const;
+
+ nsresult BeginInputTransaction(TextEventDispatcher* aDispatcher) {
+ if (mIsRemote) {
+ return aDispatcher->BeginInputTransaction(this);
+ } else {
+ return aDispatcher->BeginNativeInputTransaction();
+ }
+ }
+
+ virtual ~GeckoEditableSupport() {}
+
+ RefPtr<TextComposition> GetComposition() const;
+ bool RemoveComposition(RemoveCompositionFlag aFlag = COMMIT_IME_COMPOSITION);
+ void SendIMEDummyKeyEvent(nsIWidget* aWidget, EventMessage msg);
+ void AddIMETextChange(const IMENotification::TextChangeDataBase& aChange);
+ void PostFlushIMEChanges();
+ void FlushIMEChanges(FlushChangesFlag aFlags = FLUSH_FLAG_NONE);
+ void FlushIMEText(FlushChangesFlag aFlags = FLUSH_FLAG_NONE);
+ void AsyncNotifyIME(int32_t aNotification);
+ void UpdateCompositionRects();
+ bool DoReplaceText(int32_t aStart, int32_t aEnd, jni::String::Param aText);
+ bool DoUpdateComposition(int32_t aStart, int32_t aEnd, int32_t aFlags);
+ void OnNotifyIMEOfCompositionEventHandled();
+ void NotifyIMEContext(const InputContext& aContext,
+ const InputContextAction& aAction);
+
+ public:
+ template <typename Functor>
+ static void OnNativeCall(Functor&& aCall) {
+ struct IMEEvent : nsAppShell::LambdaEvent<Functor> {
+ explicit IMEEvent(Functor&& l)
+ : nsAppShell::LambdaEvent<Functor>(std::move(l)) {}
+
+ bool IsUIEvent() const override {
+ using GES = GeckoEditableSupport;
+ if (this->lambda.IsTarget(&GES::OnKeyEvent) ||
+ this->lambda.IsTarget(&GES::OnImeReplaceText) ||
+ this->lambda.IsTarget(&GES::OnImeUpdateComposition)) {
+ return true;
+ }
+ return false;
+ }
+
+ void Run() override {
+ if (NS_WARN_IF(!this->lambda.GetNativeObject())) {
+ // Ignore stale calls after disposal.
+ jni::GetGeckoThreadEnv()->ExceptionClear();
+ return;
+ }
+ nsAppShell::LambdaEvent<Functor>::Run();
+ }
+ };
+ nsAppShell::PostEvent(mozilla::MakeUnique<IMEEvent>(std::move(aCall)));
+ }
+
+ static void SetOnBrowserChild(dom::BrowserChild* aBrowserChild);
+
+ // Constructor for main process GeckoEditableChild.
+ GeckoEditableSupport(jni::NativeWeakPtr<GeckoViewSupport> aWindow,
+ java::GeckoEditableChild::Param aEditableChild)
+ : mIsRemote(!aWindow.IsAttached()),
+ mWindow(aWindow),
+ mEditable(aEditableChild),
+ mEditableAttached(!mIsRemote),
+ mIMERanges(new TextRangeArray()),
+ mIMEMaskEventsCount(1), // Mask IME events since there's no focus yet
+ mIMEFocusCount(0),
+ mIMEDelaySynchronizeReply(false),
+ mIMEActiveSynchronizeCount(0),
+ mDisposeBlockCount(0),
+ mIMESelectionChanged(false),
+ mIMETextChangedDuringFlush(false),
+ mIMEMonitorCursor(false) {}
+
+ // Constructor for content process GeckoEditableChild.
+ explicit GeckoEditableSupport(java::GeckoEditableChild::Param aEditableChild)
+ : GeckoEditableSupport(nullptr, aEditableChild) {}
+
+ NS_DECL_ISUPPORTS
+
+ // TextEventDispatcherListener methods
+ NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) override;
+
+ NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
+
+ NS_IMETHOD_(void)
+ OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;
+
+ NS_IMETHOD_(void)
+ WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) override;
+
+ void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction);
+
+ InputContext GetInputContext();
+
+ bool HasIMEFocus() const { return mIMEFocusCount != 0; }
+
+ void AddBlocker() { mDisposeBlockCount++; }
+
+ void ReleaseBlocker() {
+ mDisposeBlockCount--;
+
+ if (!mDisposeBlockCount && mDisposeRunnable) {
+ if (HasIMEFocus()) {
+ // If we have IME focus, GeckoEditableChild is already attached again.
+ // So disposer is unnecessary.
+ mDisposeRunnable = nullptr;
+ return;
+ }
+
+ RefPtr<GeckoEditableSupport> self(this);
+ RefPtr<Runnable> disposer = std::move(mDisposeRunnable);
+
+ nsAppShell::PostEvent(
+ [self = std::move(self), disposer = std::move(disposer)] {
+ self->mEditableAttached = false;
+ disposer->Run();
+ });
+ }
+ }
+
+ bool IsGeckoEditableUsed() const { return mDisposeBlockCount != 0; }
+
+ // GeckoEditableChild methods
+ using EditableBase::AttachNative;
+ using EditableBase::DisposeNative;
+
+ const java::GeckoEditableChild::Ref& GetJavaEditable() { return mEditable; }
+
+ void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
+ RefPtr<GeckoEditableSupport> self(this);
+ nsAppShell::PostEvent(
+ [self = std::move(self), disposer = RefPtr<Runnable>(aDisposer)] {
+ if (self->IsGeckoEditableUsed()) {
+ // Current calling stack uses GeckoEditableChild, so we should
+ // not dispose it now.
+ self->mDisposeRunnable = disposer;
+ return;
+ }
+ self->mEditableAttached = false;
+ disposer->Run();
+ });
+ }
+
+ // Transfer to a new parent.
+ void TransferParent(jni::Object::Param aEditableParent);
+
+ // Handle an Android KeyEvent.
+ void OnKeyEvent(int32_t aAction, int32_t aKeyCode, int32_t aScanCode,
+ int32_t aMetaState, int32_t aKeyPressMetaState, int64_t aTime,
+ int32_t aDomPrintableKeyValue, int32_t aRepeatCount,
+ int32_t aFlags, bool aIsSynthesizedImeKey,
+ jni::Object::Param originalEvent);
+
+ // Synchronize Gecko thread with the InputConnection thread.
+ void OnImeSynchronize();
+
+ // Replace a range of text with new text.
+ void OnImeReplaceText(int32_t aStart, int32_t aEnd, jni::String::Param aText);
+
+ // Add styling for a range within the active composition.
+ void OnImeAddCompositionRange(int32_t aStart, int32_t aEnd,
+ int32_t aRangeType, int32_t aRangeStyle,
+ int32_t aRangeLineStyle, bool aRangeBoldLine,
+ int32_t aRangeForeColor,
+ int32_t aRangeBackColor,
+ int32_t aRangeLineColor);
+
+ // Update styling for the active composition using previous-added ranges.
+ void OnImeUpdateComposition(int32_t aStart, int32_t aEnd, int32_t aFlags);
+
+ // Set cursor mode whether IME requests
+ void OnImeRequestCursorUpdates(int aRequestMode);
+
+ // Commit current composition to sync Gecko text state with Java.
+ void OnImeRequestCommit();
+
+ // Insert image from software keyboard.
+ void OnImeInsertImage(jni::ByteArray::Param aData,
+ jni::String::Param aMimeType);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_GeckoEditableSupport_h
diff --git a/widget/android/GeckoNetworkManager.h b/widget/android/GeckoNetworkManager.h
new file mode 100644
index 0000000000..7240821b9e
--- /dev/null
+++ b/widget/android/GeckoNetworkManager.h
@@ -0,0 +1,45 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef GeckoNetworkManager_h
+#define GeckoNetworkManager_h
+
+#include "nsAppShell.h"
+#include "nsCOMPtr.h"
+#include "nsINetworkLinkService.h"
+
+#include "mozilla/java/GeckoNetworkManagerNatives.h"
+#include "mozilla/Services.h"
+
+namespace mozilla {
+
+class GeckoNetworkManager final
+ : public java::GeckoNetworkManager::Natives<GeckoNetworkManager> {
+ GeckoNetworkManager() = delete;
+
+ public:
+ static void OnConnectionChanged(int32_t aType, jni::String::Param aSubType,
+ bool aIsWifi, int32_t aGateway) {
+ hal::NotifyNetworkChange(hal::NetworkInformation(aType, aIsWifi, aGateway));
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(nullptr, NS_NETWORK_LINK_TYPE_TOPIC,
+ aSubType->ToString().get());
+ }
+ }
+
+ static void OnStatusChanged(jni::String::Param aStatus) {
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(nullptr, NS_NETWORK_LINK_TOPIC,
+ aStatus->ToString().get());
+ }
+ }
+};
+
+} // namespace mozilla
+
+#endif // GeckoNetworkManager_h
diff --git a/widget/android/GeckoProcessManager.cpp b/widget/android/GeckoProcessManager.cpp
new file mode 100644
index 0000000000..274e92ed9b
--- /dev/null
+++ b/widget/android/GeckoProcessManager.cpp
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "GeckoProcessManager.h"
+
+#include "nsINetworkLinkService.h"
+#include "nsISupportsImpl.h"
+#include "mozilla/Services.h"
+
+namespace mozilla {
+
+/* static */ void GeckoProcessManager::Init() {
+ BaseNatives::Init();
+ ConnectionManager::Init();
+}
+
+NS_IMPL_ISUPPORTS(GeckoProcessManager::ConnectionManager, nsIObserver)
+
+NS_IMETHODIMP GeckoProcessManager::ConnectionManager::Observe(
+ nsISupports* aSubject, const char* aTopic, const char16_t* aData) {
+ java::GeckoProcessManager::ConnectionManager::LocalRef connMgr(mJavaConnMgr);
+ if (!connMgr) {
+ return NS_OK;
+ }
+
+ if (!strcmp("application-foreground", aTopic)) {
+ connMgr->OnForeground();
+ return NS_OK;
+ }
+
+ if (!strcmp("application-background", aTopic)) {
+ connMgr->OnBackground();
+ return NS_OK;
+ }
+
+ if (!strcmp(NS_NETWORK_LINK_TOPIC, aTopic)) {
+ const nsDependentString state(aData);
+ // state can be up, down, or unknown. For the purposes of socket process
+ // prioritization, we treat unknown as being up.
+ const bool isUp = !state.EqualsLiteral(NS_NETWORK_LINK_DATA_DOWN);
+ connMgr->OnNetworkStateChange(isUp);
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+/* static */ void GeckoProcessManager::ConnectionManager::AttachTo(
+ java::GeckoProcessManager::ConnectionManager::Param aInstance) {
+ RefPtr<ConnectionManager> native(new ConnectionManager());
+ BaseNatives::AttachNative(aInstance, native);
+
+ native->mJavaConnMgr = aInstance;
+
+ nsCOMPtr<nsIObserverService> obsServ(services::GetObserverService());
+ obsServ->AddObserver(native, "application-background", false);
+ obsServ->AddObserver(native, "application-foreground", false);
+}
+
+void GeckoProcessManager::ConnectionManager::ObserveNetworkNotifications() {
+ nsCOMPtr<nsIObserverService> obsServ(services::GetObserverService());
+ obsServ->AddObserver(this, NS_NETWORK_LINK_TOPIC, false);
+
+ const bool isUp = java::GeckoAppShell::IsNetworkLinkUp();
+
+ java::GeckoProcessManager::ConnectionManager::LocalRef connMgr(mJavaConnMgr);
+ if (!connMgr) {
+ return;
+ }
+
+ connMgr->OnNetworkStateChange(isUp);
+}
+
+} // namespace mozilla
diff --git a/widget/android/GeckoProcessManager.h b/widget/android/GeckoProcessManager.h
new file mode 100644
index 0000000000..341cb5f5c5
--- /dev/null
+++ b/widget/android/GeckoProcessManager.h
@@ -0,0 +1,84 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef GeckoProcessManager_h
+#define GeckoProcessManager_h
+
+#include "WidgetUtils.h"
+#include "nsAppShell.h"
+#include "nsContentUtils.h"
+#include "nsIObserver.h"
+#include "nsWindow.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/ContentProcessManager.h"
+#include "mozilla/java/GeckoProcessManagerNatives.h"
+
+namespace mozilla {
+
+class GeckoProcessManager final
+ : public java::GeckoProcessManager::Natives<GeckoProcessManager> {
+ using BaseNatives = java::GeckoProcessManager::Natives<GeckoProcessManager>;
+
+ GeckoProcessManager() = delete;
+
+ static already_AddRefed<nsIWidget> GetWidget(int64_t aContentId,
+ int64_t aTabId) {
+ using namespace dom;
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ContentProcessManager* const cpm = ContentProcessManager::GetSingleton();
+ NS_ENSURE_TRUE(cpm, nullptr);
+
+ RefPtr<BrowserParent> tab = cpm->GetTopLevelBrowserParentByProcessAndTabId(
+ ContentParentId(aContentId), TabId(aTabId));
+ NS_ENSURE_TRUE(tab, nullptr);
+
+ nsCOMPtr<nsPIDOMWindowOuter> domWin = tab->GetParentWindowOuter();
+ NS_ENSURE_TRUE(domWin, nullptr);
+
+ return widget::WidgetUtils::DOMWindowToWidget(domWin);
+ }
+
+ class ConnectionManager final
+ : public java::GeckoProcessManager::ConnectionManager::Natives<
+ ConnectionManager>,
+ public nsIObserver {
+ using BaseNatives = java::GeckoProcessManager::ConnectionManager::Natives<
+ ConnectionManager>;
+
+ virtual ~ConnectionManager() = default;
+
+ public:
+ ConnectionManager() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ static void AttachTo(
+ java::GeckoProcessManager::ConnectionManager::Param aInstance);
+ void ObserveNetworkNotifications();
+
+ private:
+ java::GeckoProcessManager::ConnectionManager::WeakRef mJavaConnMgr;
+ };
+
+ public:
+ static void Init();
+
+ static void GetEditableParent(jni::Object::Param aEditableChild,
+ int64_t aContentId, int64_t aTabId) {
+ nsCOMPtr<nsIWidget> widget = GetWidget(aContentId, aTabId);
+ if (RefPtr<nsWindow> window = nsWindow::From(widget)) {
+ java::GeckoProcessManager::SetEditableChildParent(
+ aEditableChild, window->GetEditableParent());
+ }
+ }
+};
+
+} // namespace mozilla
+
+#endif // GeckoProcessManager_h
diff --git a/widget/android/GeckoSystemStateListener.h b/widget/android/GeckoSystemStateListener.h
new file mode 100644
index 0000000000..75cb1cd9fe
--- /dev/null
+++ b/widget/android/GeckoSystemStateListener.h
@@ -0,0 +1,31 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef GeckoSystemStateListener_h
+#define GeckoSystemStateListener_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/java/GeckoSystemStateListenerNatives.h"
+
+namespace mozilla {
+
+class GeckoSystemStateListener final
+ : public java::GeckoSystemStateListener::Natives<GeckoSystemStateListener> {
+ GeckoSystemStateListener() = delete;
+
+ public:
+ static void OnDeviceChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // TODO(emilio, bug 1673318): This could become more granular and avoid work
+ // if we get whether these are layout/style-affecting from the caller.
+ mozilla::LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::StyleAndLayout);
+ }
+};
+
+} // namespace mozilla
+
+#endif // GeckoSystemStateListener_h
diff --git a/widget/android/GeckoTelemetryDelegate.h b/widget/android/GeckoTelemetryDelegate.h
new file mode 100644
index 0000000000..43b20d4ad6
--- /dev/null
+++ b/widget/android/GeckoTelemetryDelegate.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef GeckoTelemetryDelegate_h__
+#define GeckoTelemetryDelegate_h__
+
+#include "geckoview/streaming/GeckoViewStreamingTelemetry.h"
+
+#include <jni.h>
+
+#include "mozilla/java/RuntimeTelemetryNatives.h"
+#include "mozilla/jni/Natives.h"
+
+namespace mozilla {
+namespace widget {
+
+class GeckoTelemetryDelegate final
+ : public GeckoViewStreamingTelemetry::StreamingTelemetryDelegate,
+ public mozilla::java::RuntimeTelemetry::Proxy::Natives<
+ GeckoTelemetryDelegate> {
+ public:
+ // Implement Proxy native.
+ static void RegisterDelegateProxy(
+ mozilla::java::RuntimeTelemetry::Proxy::Param aProxy) {
+ MOZ_ASSERT(aProxy);
+
+ GeckoViewStreamingTelemetry::RegisterDelegate(
+ new GeckoTelemetryDelegate(aProxy));
+ }
+
+ explicit GeckoTelemetryDelegate(
+ mozilla::java::RuntimeTelemetry::Proxy::Param aProxy)
+ : mProxy(aProxy) {}
+
+ private:
+ void DispatchHistogram(bool aIsCategorical, const nsCString& aName,
+ const nsTArray<uint32_t>& aSamples) {
+ if (!mozilla::jni::IsAvailable() || !mProxy || aSamples.Length() < 1) {
+ return;
+ }
+
+ // Convert aSamples to an array of int64_t. We know |samples| required
+ // capacity needs to match |aSamples.Length()|.
+ nsTArray<int64_t> samples(aSamples.Length());
+ for (size_t i = 0, l = aSamples.Length(); i < l; ++i) {
+ samples.AppendElement(static_cast<int64_t>(aSamples[i]));
+ }
+
+ // LongArray::From *copies* the elements
+ mProxy->DispatchHistogram(aIsCategorical, aName,
+ mozilla::jni::LongArray::From(samples));
+ }
+
+ // Implement StreamingTelemetryDelegate.
+ void ReceiveHistogramSamples(const nsCString& aName,
+ const nsTArray<uint32_t>& aSamples) override {
+ DispatchHistogram(/* isCategorical */ false, aName, aSamples);
+ }
+
+ void ReceiveCategoricalHistogramSamples(
+ const nsCString& aName, const nsTArray<uint32_t>& aSamples) override {
+ DispatchHistogram(/* isCategorical */ true, aName, aSamples);
+ }
+
+ void ReceiveBoolScalarValue(const nsCString& aName, bool aValue) override {
+ if (!mozilla::jni::IsAvailable() || !mProxy) {
+ return;
+ }
+
+ mProxy->DispatchBooleanScalar(aName, aValue);
+ }
+
+ void ReceiveStringScalarValue(const nsCString& aName,
+ const nsCString& aValue) override {
+ if (!mozilla::jni::IsAvailable() || !mProxy) {
+ return;
+ }
+
+ mProxy->DispatchStringScalar(aName, aValue);
+ }
+
+ void ReceiveUintScalarValue(const nsCString& aName,
+ uint32_t aValue) override {
+ if (!mozilla::jni::IsAvailable() || !mProxy) {
+ return;
+ }
+
+ mProxy->DispatchLongScalar(aName, static_cast<int64_t>(aValue));
+ }
+
+ mozilla::java::RuntimeTelemetry::Proxy::GlobalRef mProxy;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // GeckoTelemetryDelegate_h__
diff --git a/widget/android/GeckoVRManager.h b/widget/android/GeckoVRManager.h
new file mode 100644
index 0000000000..de28c402cd
--- /dev/null
+++ b/widget/android/GeckoVRManager.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 20; 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/. */
+
+#ifndef mozilla_GeckoVRManager_h_
+#define mozilla_GeckoVRManager_h_
+
+#include "mozilla/java/GeckoVRManagerWrappers.h"
+#include "mozilla/jni/Utils.h"
+
+namespace mozilla {
+
+class GeckoVRManager {
+ public:
+ static void* GetExternalContext() {
+ return reinterpret_cast<void*>(
+ mozilla::java::GeckoVRManager::GetExternalContext());
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_GeckoVRManager_h_
diff --git a/widget/android/GeckoViewSupport.h b/widget/android/GeckoViewSupport.h
new file mode 100644
index 0000000000..c784552824
--- /dev/null
+++ b/widget/android/GeckoViewSupport.h
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_GeckoViewSupport_h
+#define mozilla_widget_GeckoViewSupport_h
+
+#include "mozilla/java/GeckoResultWrappers.h"
+#include "mozilla/java/GeckoSessionNatives.h"
+#include "mozilla/java/WebResponseWrappers.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/widget/WindowEvent.h"
+
+class nsPIDOMWindowOuter;
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+class GeckoViewSupport final
+ : public java::GeckoSession::Window::Natives<GeckoViewSupport> {
+ RefPtr<nsWindow> mWindow;
+
+ // We hold a WeakRef because we want to allow the
+ // GeckoSession.Window to be garbage collected.
+ // Callers need to create a LocalRef from this
+ // before calling methods.
+ java::GeckoSession::Window::WeakRef mGeckoViewWindow;
+
+ public:
+ typedef java::GeckoSession::Window::Natives<GeckoViewSupport> Base;
+
+ template <typename Functor>
+ static void OnNativeCall(Functor&& aCall) {
+ NS_DispatchToMainThread(new WindowEvent<Functor>(std::move(aCall)));
+ }
+
+ GeckoViewSupport(nsWindow* aWindow,
+ const java::GeckoSession::Window::LocalRef& aInstance,
+ nsPIDOMWindowOuter* aDOMWindow)
+ : mWindow(aWindow), mGeckoViewWindow(aInstance), mDOMWindow(aDOMWindow) {}
+
+ ~GeckoViewSupport();
+
+ nsWindow* GetNsWindow() const { return mWindow; }
+
+ using Base::DisposeNative;
+
+ /**
+ * GeckoView methods
+ */
+ private:
+ nsCOMPtr<nsPIDOMWindowOuter> mDOMWindow;
+ bool mIsReady{false};
+ RefPtr<dom::CanonicalBrowsingContext> GetContentCanonicalBrowsingContext();
+ MOZ_CAN_RUN_SCRIPT void CreatePdf(
+ jni::LocalRef<mozilla::java::GeckoResult> aGeckoResult,
+ RefPtr<dom::CanonicalBrowsingContext> aCbc);
+
+ public:
+ // Create and attach a window.
+ static void Open(const jni::Class::LocalRef& aCls,
+ java::GeckoSession::Window::Param aWindow,
+ jni::Object::Param aQueue, jni::Object::Param aCompositor,
+ jni::Object::Param aDispatcher,
+ jni::Object::Param aSessionAccessibility,
+ jni::Object::Param aInitData, jni::String::Param aId,
+ jni::String::Param aChromeURI, bool aPrivateMode);
+
+ // Close and destroy the nsWindow.
+ void Close();
+
+ // Transfer this nsWindow to new GeckoSession objects.
+ void Transfer(const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aQueue, jni::Object::Param aCompositor,
+ jni::Object::Param aDispatcher,
+ jni::Object::Param aSessionAccessibility,
+ jni::Object::Param aInitData);
+
+ void AttachEditable(const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aEditableParent);
+
+ void AttachAccessibility(const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aSessionAccessibility);
+
+ void OnReady(jni::Object::Param aQueue = nullptr);
+
+ auto OnLoadRequest(mozilla::jni::String::Param aUri, int32_t aWindowType,
+ int32_t aFlags, mozilla::jni::String::Param aTriggeringUri,
+ bool aHasUserGesture, bool aIsTopLevel) const
+ -> java::GeckoResult::LocalRef;
+
+ void OnShowDynamicToolbar() const;
+
+ void OnUpdateSessionStore(mozilla::jni::Object::Param aBundle);
+
+ void PassExternalResponse(java::WebResponse::Param aResponse);
+
+ void AttachMediaSessionController(
+ const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aController, const int64_t aId);
+
+ void DetachMediaSessionController(
+ const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aController);
+
+ void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
+ RefPtr<Runnable> disposer(aDisposer);
+ disposer->Run();
+ }
+
+ MOZ_CAN_RUN_SCRIPT void PrintToPdf(
+ const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aStream, int64_t aBcId);
+
+ MOZ_CAN_RUN_SCRIPT void PrintToPdf(
+ const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aStream);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_GeckoViewSupport_h
diff --git a/widget/android/GfxInfo.cpp b/widget/android/GfxInfo.cpp
new file mode 100644
index 0000000000..0e78b187bf
--- /dev/null
+++ b/widget/android/GfxInfo.cpp
@@ -0,0 +1,856 @@
+/* -*- 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 "GfxInfo.h"
+#include "AndroidBuild.h"
+#include "GLContext.h"
+#include "GLContextProvider.h"
+#include "nsUnicharUtils.h"
+#include "prenv.h"
+#include "nsExceptionHandler.h"
+#include "nsHashKeys.h"
+#include "nsVersionComparator.h"
+#include "nsServiceManagerUtils.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+#include "mozilla/java/HardwareCodecCapabilityUtilsWrappers.h"
+
+namespace mozilla {
+namespace widget {
+
+class GfxInfo::GLStrings {
+ nsCString mVendor;
+ nsCString mRenderer;
+ nsCString mVersion;
+ nsTArray<nsCString> mExtensions;
+ bool mReady;
+
+ public:
+ GLStrings() : mReady(false) {}
+
+ const nsCString& Vendor() {
+ EnsureInitialized();
+ return mVendor;
+ }
+
+ // This spoofed value wins, even if the environment variable
+ // MOZ_GFX_SPOOF_GL_VENDOR was set.
+ void SpoofVendor(const nsCString& s) { mVendor = s; }
+
+ const nsCString& Renderer() {
+ EnsureInitialized();
+ return mRenderer;
+ }
+
+ // This spoofed value wins, even if the environment variable
+ // MOZ_GFX_SPOOF_GL_RENDERER was set.
+ void SpoofRenderer(const nsCString& s) { mRenderer = s; }
+
+ const nsCString& Version() {
+ EnsureInitialized();
+ return mVersion;
+ }
+
+ // This spoofed value wins, even if the environment variable
+ // MOZ_GFX_SPOOF_GL_VERSION was set.
+ void SpoofVersion(const nsCString& s) { mVersion = s; }
+
+ const nsTArray<nsCString>& Extensions() {
+ EnsureInitialized();
+ return mExtensions;
+ }
+
+ void EnsureInitialized() {
+ if (mReady) {
+ return;
+ }
+
+ RefPtr<gl::GLContext> gl;
+ nsCString discardFailureId;
+ gl = gl::GLContextProvider::CreateHeadless(
+ {gl::CreateContextFlags::REQUIRE_COMPAT_PROFILE}, &discardFailureId);
+
+ if (!gl) {
+ // Setting mReady to true here means that we won't retry. Everything will
+ // remain blocklisted forever. Ideally, we would like to update that once
+ // any GLContext is successfully created, like the compositor's GLContext.
+ mReady = true;
+ return;
+ }
+
+ gl->MakeCurrent();
+
+ if (mVendor.IsEmpty()) {
+ const char* spoofedVendor = PR_GetEnv("MOZ_GFX_SPOOF_GL_VENDOR");
+ if (spoofedVendor) {
+ mVendor.Assign(spoofedVendor);
+ } else {
+ mVendor.Assign((const char*)gl->fGetString(LOCAL_GL_VENDOR));
+ }
+ }
+
+ if (mRenderer.IsEmpty()) {
+ const char* spoofedRenderer = PR_GetEnv("MOZ_GFX_SPOOF_GL_RENDERER");
+ if (spoofedRenderer) {
+ mRenderer.Assign(spoofedRenderer);
+ } else {
+ mRenderer.Assign((const char*)gl->fGetString(LOCAL_GL_RENDERER));
+ }
+ }
+
+ if (mVersion.IsEmpty()) {
+ const char* spoofedVersion = PR_GetEnv("MOZ_GFX_SPOOF_GL_VERSION");
+ if (spoofedVersion) {
+ mVersion.Assign(spoofedVersion);
+ } else {
+ mVersion.Assign((const char*)gl->fGetString(LOCAL_GL_VERSION));
+ }
+ }
+
+ if (mExtensions.IsEmpty()) {
+ nsCString rawExtensions;
+ rawExtensions.Assign((const char*)gl->fGetString(LOCAL_GL_EXTENSIONS));
+ rawExtensions.Trim(" ");
+
+ for (auto extension : rawExtensions.Split(' ')) {
+ mExtensions.AppendElement(extension);
+ }
+ }
+
+ mReady = true;
+ }
+};
+
+#ifdef DEBUG
+NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug)
+#endif
+
+GfxInfo::GfxInfo()
+ : mInitialized(false),
+ mGLStrings(new GLStrings),
+ mOSVersionInteger(0),
+ mSDKVersion(0) {}
+
+GfxInfo::~GfxInfo() {}
+
+/* GetD2DEnabled and GetDwriteEnabled shouldn't be called until after
+ * gfxPlatform initialization has occurred because they depend on it for
+ * information. (See bug 591561) */
+nsresult GfxInfo::GetD2DEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+nsresult GfxInfo::GetDWriteEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+nsresult GfxInfo::GetHasBattery(bool* aHasBattery) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetTestType(nsAString& aTestType) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+void GfxInfo::EnsureInitialized() {
+ if (mInitialized) return;
+
+ if (!jni::IsAvailable()) {
+ gfxWarning() << "JNI missing during initialization";
+ return;
+ }
+
+ jni::String::LocalRef model = java::sdk::Build::MODEL();
+ mModel = model->ToString();
+ mAdapterDescription.AppendPrintf("Model: %s",
+ NS_LossyConvertUTF16toASCII(mModel).get());
+
+ jni::String::LocalRef product = java::sdk::Build::PRODUCT();
+ mProduct = product->ToString();
+ mAdapterDescription.AppendPrintf(", Product: %s",
+ NS_LossyConvertUTF16toASCII(mProduct).get());
+
+ jni::String::LocalRef manufacturer =
+ mozilla::java::sdk::Build::MANUFACTURER();
+ mManufacturer = manufacturer->ToString();
+ mAdapterDescription.AppendPrintf(
+ ", Manufacturer: %s", NS_LossyConvertUTF16toASCII(mManufacturer).get());
+
+ mSDKVersion = java::sdk::Build::VERSION::SDK_INT();
+ jni::String::LocalRef hardware = java::sdk::Build::HARDWARE();
+ mHardware = hardware->ToString();
+ mAdapterDescription.AppendPrintf(
+ ", Hardware: %s", NS_LossyConvertUTF16toASCII(mHardware).get());
+
+ jni::String::LocalRef release = java::sdk::Build::VERSION::RELEASE();
+ mOSVersion = release->ToCString();
+
+ mOSVersionInteger = 0;
+ char a[5], b[5], c[5], d[5];
+ SplitDriverVersion(mOSVersion.get(), a, b, c, d);
+ uint8_t na = atoi(a);
+ uint8_t nb = atoi(b);
+ uint8_t nc = atoi(c);
+ uint8_t nd = atoi(d);
+
+ mOSVersionInteger = (uint32_t(na) << 24) | (uint32_t(nb) << 16) |
+ (uint32_t(nc) << 8) | uint32_t(nd);
+
+ mAdapterDescription.AppendPrintf(
+ ", OpenGL: %s -- %s -- %s", mGLStrings->Vendor().get(),
+ mGLStrings->Renderer().get(), mGLStrings->Version().get());
+
+ AddCrashReportAnnotations();
+ mInitialized = true;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) {
+ EnsureInitialized();
+ aAdapterDescription = NS_ConvertASCIItoUTF16(mAdapterDescription);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) {
+ EnsureInitialized();
+ *aAdapterRAM = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) {
+ EnsureInitialized();
+ aAdapterDriver.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) {
+ EnsureInitialized();
+ aAdapterDriverVendor.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) {
+ EnsureInitialized();
+ aAdapterDriverVersion = NS_ConvertASCIItoUTF16(mGLStrings->Version());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) {
+ EnsureInitialized();
+ aAdapterDriverDate.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) {
+ EnsureInitialized();
+ aAdapterVendorID = NS_ConvertASCIItoUTF16(mGLStrings->Vendor());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) {
+ EnsureInitialized();
+ aAdapterDeviceID = NS_ConvertASCIItoUTF16(mGLStrings->Renderer());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) {
+ EnsureInitialized();
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+void GfxInfo::AddCrashReportAnnotations() {
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID,
+ mGLStrings->Vendor());
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID,
+ mGLStrings->Renderer());
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AdapterDriverVersion, mGLStrings->Version());
+}
+
+const nsTArray<GfxDriverInfo>& GfxInfo::GetGfxDriverInfo() {
+ if (sDriverInfo->IsEmpty()) {
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Android, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_STATUS_OK,
+ DRIVER_COMPARISON_IGNORED, GfxDriverInfo::allDriverVersions,
+ "FEATURE_OK_FORCE_OPENGL");
+ }
+
+ return *sDriverInfo;
+}
+
+nsresult GfxInfo::GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS /* = nullptr */) {
+ NS_ENSURE_ARG_POINTER(aStatus);
+ aSuggestedDriverVersion.SetIsVoid(true);
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ OperatingSystem os = OperatingSystem::Android;
+ if (aOS) *aOS = os;
+
+ if (sShutdownOccurred) {
+ return NS_OK;
+ }
+
+ // OpenGL layers are never blocklisted on Android.
+ // This early return is so we avoid potentially slow
+ // GLStrings initialization on startup when we initialize GL layers.
+ if (aFeature == nsIGfxInfo::FEATURE_OPENGL_LAYERS) {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ return NS_OK;
+ }
+
+ EnsureInitialized();
+
+ if (mGLStrings->Vendor().IsEmpty() || mGLStrings->Renderer().IsEmpty()) {
+ if (OnlyAllowFeatureOnKnownConfig(aFeature)) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_EMPTY_VENDOR_OR_RENDERER";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ // Don't evaluate special cases when evaluating the downloaded blocklist.
+ if (aDriverInfo.IsEmpty()) {
+ if (aFeature == nsIGfxInfo::FEATURE_CANVAS2D_ACCELERATION) {
+ if (mGLStrings->Renderer().Find("Vivante GC1000") != -1) {
+ // Blocklist Vivante GC1000. See bug 1248183.
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILED_CANVAS_2D_HW";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ if (aFeature == FEATURE_WEBGL_OPENGL) {
+ if (mGLStrings->Renderer().Find("Adreno 200") != -1 ||
+ mGLStrings->Renderer().Find("Adreno 205") != -1) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_ADRENO_20x";
+ return NS_OK;
+ }
+
+ if (mHardware.EqualsLiteral("ville")) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_VILLE";
+ return NS_OK;
+ }
+ }
+
+ if (aFeature == FEATURE_STAGEFRIGHT) {
+ NS_LossyConvertUTF16toASCII cManufacturer(mManufacturer);
+ NS_LossyConvertUTF16toASCII cModel(mModel);
+ NS_LossyConvertUTF16toASCII cHardware(mHardware);
+
+ if (cHardware.EqualsLiteral("antares") ||
+ cHardware.EqualsLiteral("harmony") ||
+ cHardware.EqualsLiteral("picasso") ||
+ cHardware.EqualsLiteral("picasso_e") ||
+ cHardware.EqualsLiteral("ventana") ||
+ cHardware.EqualsLiteral("rk30board")) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_STAGE_HW";
+ return NS_OK;
+ }
+
+ if (CompareVersions(mOSVersion.get(), "4.1.0") < 0) {
+ // Whitelist:
+ // All Samsung ICS devices, except for:
+ // Samsung SGH-I717 (Bug 845729)
+ // Samsung SGH-I727 (Bug 845729)
+ // Samsung SGH-I757 (Bug 845729)
+ // All Galaxy nexus ICS devices
+ // Sony Xperia Ion (LT28) ICS devices
+ bool isWhitelisted =
+ cModel.Equals("LT28h", nsCaseInsensitiveCStringComparator) ||
+ cManufacturer.Equals("samsung",
+ nsCaseInsensitiveCStringComparator) ||
+ cModel.Equals(
+ "galaxy nexus",
+ nsCaseInsensitiveCStringComparator); // some Galaxy Nexus
+ // have
+ // manufacturer=amazon
+
+ if (cModel.LowerCaseFindASCII("sgh-i717") != -1 ||
+ cModel.LowerCaseFindASCII("sgh-i727") != -1 ||
+ cModel.LowerCaseFindASCII("sgh-i757") != -1) {
+ isWhitelisted = false;
+ }
+
+ if (!isWhitelisted) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_4_1_HW";
+ return NS_OK;
+ }
+ } else if (CompareVersions(mOSVersion.get(), "4.2.0") < 0) {
+ // Whitelist:
+ // All JB phones except for those in blocklist below
+ // Blocklist:
+ // Samsung devices from bug 812881 and 853522.
+ // Motorola XT890 from bug 882342.
+ bool isBlocklisted = cModel.LowerCaseFindASCII("gt-p3100") != -1 ||
+ cModel.LowerCaseFindASCII("gt-p3110") != -1 ||
+ cModel.LowerCaseFindASCII("gt-p3113") != -1 ||
+ cModel.LowerCaseFindASCII("gt-p5100") != -1 ||
+ cModel.LowerCaseFindASCII("gt-p5110") != -1 ||
+ cModel.LowerCaseFindASCII("gt-p5113") != -1 ||
+ cModel.LowerCaseFindASCII("xt890") != -1;
+
+ if (isBlocklisted) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_4_2_HW";
+ return NS_OK;
+ }
+ } else if (CompareVersions(mOSVersion.get(), "4.3.0") < 0) {
+ // Blocklist all Sony devices
+ if (cManufacturer.LowerCaseFindASCII("sony") != -1) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_4_3_SONY";
+ return NS_OK;
+ }
+ }
+ }
+
+ if (aFeature == FEATURE_WEBRTC_HW_ACCELERATION_ENCODE) {
+ if (jni::IsAvailable()) {
+ *aStatus = WebRtcHwVp8EncodeSupported();
+ aFailureId = "FEATURE_FAILURE_WEBRTC_ENCODE";
+ return NS_OK;
+ }
+ }
+ if (aFeature == FEATURE_WEBRTC_HW_ACCELERATION_DECODE) {
+ if (jni::IsAvailable()) {
+ *aStatus = WebRtcHwVp8DecodeSupported();
+ aFailureId = "FEATURE_FAILURE_WEBRTC_DECODE";
+ return NS_OK;
+ }
+ }
+ if (aFeature == FEATURE_WEBRTC_HW_ACCELERATION_H264) {
+ if (jni::IsAvailable()) {
+ *aStatus = WebRtcHwH264Supported();
+ aFailureId = "FEATURE_FAILURE_WEBRTC_H264";
+ return NS_OK;
+ }
+ }
+ if (aFeature == FEATURE_VP8_HW_DECODE ||
+ aFeature == FEATURE_VP9_HW_DECODE) {
+ NS_LossyConvertUTF16toASCII model(mModel);
+ bool isBlocked =
+ // GIFV crash, see bug 1232911.
+ model.Equals("GT-N8013", nsCaseInsensitiveCStringComparator);
+
+ if (isBlocked) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_VPx";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ if (aFeature == FEATURE_WEBRENDER) {
+ const bool isMali4xx =
+ mGLStrings->Renderer().LowerCaseFindASCII("mali-4") >= 0;
+
+ const bool isPowerVrG6110 =
+ mGLStrings->Renderer().LowerCaseFindASCII("powervr rogue g6110") >= 0;
+
+ const bool isVivanteGC7000UL =
+ mGLStrings->Renderer().LowerCaseFindASCII("vivante gc7000ul") >= 0;
+
+ const bool isPowerVrFenceSyncCrash =
+ (mGLStrings->Renderer().LowerCaseFindASCII("powervr rogue g6200") >=
+ 0 ||
+ mGLStrings->Renderer().LowerCaseFindASCII("powervr rogue g6430") >=
+ 0 ||
+ mGLStrings->Renderer().LowerCaseFindASCII("powervr rogue gx6250") >=
+ 0) &&
+ (mGLStrings->Version().Find("3283119") >= 0 ||
+ mGLStrings->Version().Find("3443629") >= 0 ||
+ mGLStrings->Version().Find("3573678") >= 0 ||
+ mGLStrings->Version().Find("3830101") >= 0);
+
+ if (isMali4xx) {
+ // Mali 4xx does not support GLES 3.
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_NO_GLES_3";
+ } else if (isPowerVrG6110) {
+ // Blocked on PowerVR Rogue G6110 due to bug 1742986 and bug 1717863.
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_POWERVR_G6110";
+ } else if (isVivanteGC7000UL) {
+ // Blocked on Vivante GC7000UL due to bug 1719327.
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_VIVANTE_GC7000UL";
+ } else if (isPowerVrFenceSyncCrash) {
+ // Blocked on various PowerVR GPUs due to bug 1773128.
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_POWERVR_FENCE_SYNC_CRASH";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ if (aFeature == FEATURE_WEBRENDER_SCISSORED_CACHE_CLEARS) {
+ // Emulator with SwiftShader is buggy when attempting to clear picture
+ // cache textures with a scissor rect set.
+ const bool isEmulatorSwiftShader =
+ mGLStrings->Renderer().Find(
+ "Android Emulator OpenGL ES Translator (Google SwiftShader)") >=
+ 0;
+ if (isEmulatorSwiftShader) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_BUG_1603515";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ if (aFeature == FEATURE_WEBRENDER_SHADER_CACHE) {
+ // Program binaries are known to be buggy on Adreno 3xx. While we haven't
+ // encountered any correctness or stability issues with them, loading them
+ // fails more often than not, so is a waste of time. Better to just not
+ // even attempt to cache them. See bug 1615574.
+ const bool isAdreno3xx =
+ mGLStrings->Renderer().LowerCaseFindASCII("adreno (tm) 3") >= 0;
+ if (isAdreno3xx) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_ADRENO_3XX";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ }
+
+ if (aFeature == FEATURE_WEBRENDER_OPTIMIZED_SHADERS) {
+ // Optimized shaders result in completely broken rendering on some Mali-T
+ // devices. We have seen this on T6xx, T7xx, and T8xx on android versions
+ // up to 5.1, and on T6xx on versions up to android 7.1. As a precaution
+ // disable for all Mali-T regardless of version. See bug 1689064 and bug
+ // 1707283 for details.
+ const bool isMaliT =
+ mGLStrings->Renderer().LowerCaseFindASCII("mali-t") >= 0;
+ if (isMaliT) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_BUG_1689064";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ if (aFeature == FEATURE_WEBRENDER_PARTIAL_PRESENT) {
+ // Block partial present on some devices due to rendering issues.
+ // On Mali-Txxx due to bug 1680087 and bug 1707815.
+ // On Adreno 3xx GPUs due to bug 1695771.
+ const bool isMaliT =
+ mGLStrings->Renderer().LowerCaseFindASCII("mali-t") >= 0;
+ const bool isAdreno3xx =
+ mGLStrings->Renderer().LowerCaseFindASCII("adreno (tm) 3") >= 0;
+ if (isMaliT || isAdreno3xx) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_BUG_1680087_1695771_1707815";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+ }
+
+ if (aFeature == FEATURE_GL_SWIZZLE) {
+ // Swizzling appears to be buggy on PowerVR Rogue devices with webrender.
+ // See bug 1704783.
+ const bool isPowerVRRogue =
+ mGLStrings->Renderer().LowerCaseFindASCII("powervr rogue") >= 0;
+ if (isPowerVRRogue) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_POWERVR_ROGUE";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ return GfxInfoBase::GetFeatureStatusImpl(
+ aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os);
+}
+
+static nsCString FeatureCacheOsVerPrefName(int32_t aFeature) {
+ nsCString osPrefName;
+ osPrefName.AppendASCII("gfxinfo.cache.");
+ osPrefName.AppendInt(aFeature);
+ osPrefName.AppendASCII(".osver");
+ return osPrefName;
+}
+
+static nsCString FeatureCacheAppVerPrefName(int32_t aFeature) {
+ nsCString osPrefName;
+ osPrefName.AppendASCII("gfxinfo.cache.");
+ osPrefName.AppendInt(aFeature);
+ osPrefName.AppendASCII(".appver");
+ return osPrefName;
+}
+
+static nsCString FeatureCacheValuePrefName(int32_t aFeature) {
+ nsCString osPrefName;
+ osPrefName.AppendASCII("gfxinfo.cache.");
+ osPrefName.AppendInt(aFeature);
+ osPrefName.AppendASCII(".value");
+ return osPrefName;
+}
+
+static bool GetCachedFeatureVal(int32_t aFeature, uint32_t aExpectedOsVer,
+ const nsCString& aCurrentAppVer,
+ int32_t& aOutStatus) {
+ uint32_t osVer = 0;
+ nsresult rv =
+ Preferences::GetUint(FeatureCacheOsVerPrefName(aFeature).get(), &osVer);
+ if (NS_FAILED(rv) || osVer != aExpectedOsVer) {
+ return false;
+ }
+ // Bug 1804287 requires we invalidate cached values for new builds to allow
+ // for code changes to modify the features support.
+ nsAutoCString cachedAppVersion;
+ rv = Preferences::GetCString(FeatureCacheAppVerPrefName(aFeature).get(),
+ cachedAppVersion);
+ if (NS_FAILED(rv) || !aCurrentAppVer.Equals(cachedAppVersion)) {
+ return false;
+ }
+ int32_t status = 0;
+ rv = Preferences::GetInt(FeatureCacheValuePrefName(aFeature).get(), &status);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ aOutStatus = status;
+ return true;
+}
+
+static void SetCachedFeatureVal(int32_t aFeature, uint32_t aOsVer,
+ const nsCString& aCurrentAppVer,
+ int32_t aStatus) {
+ // Ignore failures; not much we can do anyway.
+ Preferences::SetUint(FeatureCacheOsVerPrefName(aFeature).get(), aOsVer);
+ Preferences::SetCString(FeatureCacheAppVerPrefName(aFeature).get(),
+ aCurrentAppVer);
+ Preferences::SetInt(FeatureCacheValuePrefName(aFeature).get(), aStatus);
+}
+
+int32_t GfxInfo::WebRtcHwVp8EncodeSupported() {
+ MOZ_ASSERT(jni::IsAvailable());
+
+ // The Android side of this calculation is very slow, so we cache the result
+ // in preferences, invalidating if the OS version changes.
+
+ int32_t status = 0;
+ const auto& currentAppVersion = GfxInfoBase::GetApplicationVersion();
+ if (GetCachedFeatureVal(FEATURE_WEBRTC_HW_ACCELERATION_ENCODE,
+ mOSVersionInteger, currentAppVersion, status)) {
+ return status;
+ }
+
+ status = java::GeckoAppShell::HasHWVP8Encoder()
+ ? nsIGfxInfo::FEATURE_STATUS_OK
+ : nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+
+ SetCachedFeatureVal(FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, mOSVersionInteger,
+ currentAppVersion, status);
+
+ return status;
+}
+
+int32_t GfxInfo::WebRtcHwVp8DecodeSupported() {
+ MOZ_ASSERT(jni::IsAvailable());
+
+ // The Android side of this caclulation is very slow, so we cache the result
+ // in preferences, invalidating if the OS version changes.
+
+ int32_t status = 0;
+ const auto& appVersion = GfxInfoBase::GetApplicationVersion();
+ if (GetCachedFeatureVal(FEATURE_WEBRTC_HW_ACCELERATION_DECODE,
+ mOSVersionInteger, appVersion, status)) {
+ return status;
+ }
+
+ status = java::GeckoAppShell::HasHWVP8Decoder()
+ ? nsIGfxInfo::FEATURE_STATUS_OK
+ : nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+
+ SetCachedFeatureVal(FEATURE_WEBRTC_HW_ACCELERATION_DECODE, mOSVersionInteger,
+ appVersion, status);
+
+ return status;
+}
+
+int32_t GfxInfo::WebRtcHwH264Supported() {
+ MOZ_ASSERT(jni::IsAvailable());
+
+ // The Android side of this calculation is very slow, so we cache the result
+ // in preferences, invalidating if the OS version changes.
+
+ int32_t status = 0;
+ const auto& currentAppVersion = GfxInfoBase::GetApplicationVersion();
+ if (GetCachedFeatureVal(FEATURE_WEBRTC_HW_ACCELERATION_H264,
+ mOSVersionInteger, currentAppVersion, status)) {
+ return status;
+ }
+
+ status = java::HardwareCodecCapabilityUtils::HasHWH264()
+ ? nsIGfxInfo::FEATURE_STATUS_OK
+ : nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+
+ SetCachedFeatureVal(FEATURE_WEBRTC_HW_ACCELERATION_H264, mOSVersionInteger,
+ currentAppVersion, status);
+
+ return status;
+}
+
+#ifdef DEBUG
+
+// Implement nsIGfxInfoDebug
+
+NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) {
+ mGLStrings->SpoofVendor(NS_LossyConvertUTF16toASCII(aVendorID));
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) {
+ mGLStrings->SpoofRenderer(NS_LossyConvertUTF16toASCII(aDeviceID));
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) {
+ mGLStrings->SpoofVersion(NS_LossyConvertUTF16toASCII(aDriverVersion));
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) {
+ EnsureInitialized();
+ mOSVersion = aVersion;
+ return NS_OK;
+}
+
+#endif
+
+nsString GfxInfo::Model() {
+ EnsureInitialized();
+ return mModel;
+}
+
+nsString GfxInfo::Hardware() {
+ EnsureInitialized();
+ return mHardware;
+}
+
+nsString GfxInfo::Product() {
+ EnsureInitialized();
+ return mProduct;
+}
+
+nsString GfxInfo::Manufacturer() {
+ EnsureInitialized();
+ return mManufacturer;
+}
+
+uint32_t GfxInfo::OperatingSystemVersion() {
+ EnsureInitialized();
+ return mOSVersionInteger;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/GfxInfo.h b/widget/android/GfxInfo.h
new file mode 100644
index 0000000000..7a963b0275
--- /dev/null
+++ b/widget/android/GfxInfo.h
@@ -0,0 +1,109 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_GfxInfo_h__
+#define __mozilla_widget_GfxInfo_h__
+
+#include "GfxInfoBase.h"
+#include "GfxDriverInfo.h"
+
+#include "nsString.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+namespace widget {
+
+class GfxInfo : public GfxInfoBase {
+ private:
+ ~GfxInfo();
+
+ public:
+ GfxInfo();
+
+ // We only declare the subset of nsIGfxInfo that we actually implement. The
+ // rest is brought forward from GfxInfoBase.
+ NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override;
+ NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override;
+ NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override;
+ NS_IMETHOD GetEmbeddedInFirefoxReality(
+ bool* aEmbeddedInFirefoxReality) override;
+ NS_IMETHOD GetHasBattery(bool* aHasBattery) override;
+ NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override;
+ NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override;
+ NS_IMETHOD GetTestType(nsAString& aTestType) override;
+ NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion2(
+ nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override;
+ NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override;
+ using GfxInfoBase::GetFeatureStatus;
+ using GfxInfoBase::GetFeatureSuggestedDriverVersion;
+
+ void EnsureInitialized();
+
+ virtual nsString Model() override;
+ virtual nsString Hardware() override;
+ virtual nsString Product() override;
+ virtual nsString Manufacturer() override;
+
+#ifdef DEBUG
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIGFXINFODEBUG
+#endif
+
+ virtual uint32_t OperatingSystemVersion() override;
+
+ protected:
+ OperatingSystem GetOperatingSystem() override {
+ return OperatingSystem::Android;
+ }
+ virtual nsresult GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS = nullptr) override;
+ virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override;
+
+ private:
+ void AddCrashReportAnnotations();
+ int32_t WebRtcHwVp8EncodeSupported();
+ int32_t WebRtcHwVp8DecodeSupported();
+ int32_t WebRtcHwH264Supported();
+
+ bool mInitialized;
+
+ class GLStrings;
+ UniquePtr<GLStrings> mGLStrings;
+
+ nsCString mAdapterDescription;
+
+ nsString mModel, mHardware, mManufacturer, mProduct;
+ nsCString mOSVersion;
+ uint32_t mOSVersionInteger;
+ int32_t mSDKVersion;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_GfxInfo_h__ */
diff --git a/widget/android/ImageDecoderSupport.cpp b/widget/android/ImageDecoderSupport.cpp
new file mode 100644
index 0000000000..4418b79746
--- /dev/null
+++ b/widget/android/ImageDecoderSupport.cpp
@@ -0,0 +1,185 @@
+/* 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 "ImageDecoderSupport.h"
+
+#include "imgINotificationObserver.h"
+#include "imgITools.h"
+#include "imgINotificationObserver.h"
+#include "gfxUtils.h"
+#include "AndroidGraphics.h"
+#include "JavaExceptions.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Swizzle.h"
+#include "mozilla/java/ImageWrappers.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla::gfx;
+
+namespace mozilla {
+namespace widget {
+
+namespace {
+
+class ImageCallbackHelper;
+
+HashSet<RefPtr<ImageCallbackHelper>, PointerHasher<ImageCallbackHelper*>>
+ gDecodeRequests;
+
+class ImageCallbackHelper : public imgIContainerCallback,
+ public imgINotificationObserver {
+ public:
+ NS_DECL_ISUPPORTS
+
+ void CompleteExceptionally(nsresult aRv) {
+ nsPrintfCString error("Could not process image: 0x%08X", uint32_t(aRv));
+ mResult->CompleteExceptionally(
+ java::Image::ImageProcessingException::New(error.get())
+ .Cast<jni::Throwable>());
+ gDecodeRequests.remove(this);
+ }
+
+ void Complete(DataSourceSurface::ScopedMap& aSourceSurface, int32_t width,
+ int32_t height) {
+ auto pixels = mozilla::jni::ByteBuffer::New(
+ reinterpret_cast<int8_t*>(aSourceSurface.GetData()),
+ aSourceSurface.GetStride() * height);
+ auto bitmap = java::sdk::Bitmap::CreateBitmap(
+ width, height, java::sdk::Bitmap::Config::ARGB_8888());
+ bitmap->CopyPixelsFromBuffer(pixels);
+ mResult->Complete(bitmap);
+ gDecodeRequests.remove(this);
+ }
+
+ ImageCallbackHelper(java::GeckoResult::Param aResult, int32_t aDesiredLength)
+ : mResult(aResult), mDesiredLength(aDesiredLength), mImage(nullptr) {
+ MOZ_ASSERT(mResult);
+ }
+
+ NS_IMETHOD
+ OnImageReady(imgIContainer* aImage, nsresult aStatus) override {
+ // Let's make sure we are alive until the request completes
+ MOZ_ALWAYS_TRUE(gDecodeRequests.putNew(this));
+
+ if (NS_FAILED(aStatus)) {
+ CompleteExceptionally(aStatus);
+ return aStatus;
+ }
+
+ mImage = aImage;
+ return mImage->StartDecoding(
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY,
+ imgIContainer::FRAME_FIRST);
+ }
+
+ // This method assumes that the image is ready to be processed
+ nsresult SendBitmap() {
+ RefPtr<gfx::SourceSurface> surface;
+
+ NS_ENSURE_TRUE(mImage, NS_ERROR_FAILURE);
+ if (mDesiredLength > 0) {
+ surface = mImage->GetFrameAtSize(
+ gfx::IntSize(mDesiredLength, mDesiredLength),
+ imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ } else {
+ surface = mImage->GetFrame(
+ imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ }
+
+ NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+ RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
+
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+ int32_t width = dataSurface->GetSize().width;
+ int32_t height = dataSurface->GetSize().height;
+
+ DataSourceSurface::ScopedMap sourceMap(dataSurface,
+ DataSourceSurface::READ);
+
+ // Android's Bitmap only supports R8G8B8A8, so we need to convert the
+ // data to the right format
+ RefPtr<DataSourceSurface> destDataSurface =
+ Factory::CreateDataSourceSurfaceWithStride(dataSurface->GetSize(),
+ SurfaceFormat::R8G8B8A8,
+ sourceMap.GetStride());
+ NS_ENSURE_TRUE(destDataSurface, NS_ERROR_FAILURE);
+
+ DataSourceSurface::ScopedMap destMap(destDataSurface,
+ DataSourceSurface::READ_WRITE);
+
+ SwizzleData(sourceMap.GetData(), sourceMap.GetStride(),
+ surface->GetFormat(), destMap.GetData(), destMap.GetStride(),
+ SurfaceFormat::R8G8B8A8, destDataSurface->GetSize());
+
+ Complete(destMap, width, height);
+
+ return NS_OK;
+ }
+
+ void Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) override {
+ if (aType == imgINotificationObserver::DECODE_COMPLETE) {
+ nsresult status = SendBitmap();
+ if (NS_FAILED(status)) {
+ CompleteExceptionally(status);
+ }
+
+ // Breack the cyclic reference between `ImageDecoderListener` (which is a
+ // `imgIContainer`) and `ImageCallbackHelper`.
+ mImage = nullptr;
+ }
+ }
+
+ private:
+ const java::GeckoResult::GlobalRef mResult;
+ int32_t mDesiredLength;
+ nsCOMPtr<imgIContainer> mImage;
+ virtual ~ImageCallbackHelper() {}
+};
+
+NS_IMPL_ISUPPORTS(ImageCallbackHelper, imgIContainerCallback,
+ imgINotificationObserver)
+
+} // namespace
+
+/* static */ void ImageDecoderSupport::Decode(jni::String::Param aUri,
+ int32_t aDesiredLength,
+ jni::Object::Param aResult) {
+ auto result = java::GeckoResult::LocalRef(aResult);
+ RefPtr<ImageCallbackHelper> helper =
+ new ImageCallbackHelper(result, aDesiredLength);
+
+ nsresult rv = DecodeInternal(aUri->ToString(), helper, helper);
+ if (NS_FAILED(rv)) {
+ helper->OnImageReady(nullptr, rv);
+ }
+}
+
+/* static */ nsresult ImageDecoderSupport::DecodeInternal(
+ const nsAString& aUri, imgIContainerCallback* aCallback,
+ imgINotificationObserver* aObserver) {
+ nsCOMPtr<imgITools> imgTools = do_GetService("@mozilla.org/image/tools;1");
+ if (NS_WARN_IF(!imgTools)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aUri);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel), uri,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_IMAGE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return imgTools->DecodeImageFromChannelAsync(uri, channel, aCallback,
+ aObserver);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/ImageDecoderSupport.h b/widget/android/ImageDecoderSupport.h
new file mode 100644
index 0000000000..d38b3e7e1b
--- /dev/null
+++ b/widget/android/ImageDecoderSupport.h
@@ -0,0 +1,30 @@
+/* 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/. */
+
+#ifndef ImageDecoderSupport_h__
+#define ImageDecoderSupport_h__
+
+#include "mozilla/java/ImageDecoderNatives.h"
+
+class imgIContainerCallback;
+
+namespace mozilla {
+namespace widget {
+
+class ImageDecoderSupport final
+ : public java::ImageDecoder::Natives<ImageDecoderSupport> {
+ public:
+ static void Decode(jni::String::Param aUri, int32_t aDesiredLength,
+ jni::Object::Param aResult);
+
+ private:
+ static nsresult DecodeInternal(const nsAString& aUri,
+ imgIContainerCallback* aCallback,
+ imgINotificationObserver* aObserver);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // ImageDecoderSupport_h__
diff --git a/widget/android/InProcessAndroidCompositorWidget.cpp b/widget/android/InProcessAndroidCompositorWidget.cpp
new file mode 100644
index 0000000000..1ae221acb9
--- /dev/null
+++ b/widget/android/InProcessAndroidCompositorWidget.cpp
@@ -0,0 +1,57 @@
+/* -*- 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 "HeadlessCompositorWidget.h"
+#include "HeadlessWidget.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+
+#include "InProcessAndroidCompositorWidget.h"
+#include "nsWindow.h"
+
+namespace mozilla {
+namespace widget {
+
+/* static */
+RefPtr<CompositorWidget> CompositorWidget::CreateLocal(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsIWidget* aWidget) {
+ if (aInitData.type() ==
+ CompositorWidgetInitData::THeadlessCompositorWidgetInitData) {
+ return new HeadlessCompositorWidget(
+ aInitData.get_HeadlessCompositorWidgetInitData(), aOptions,
+ static_cast<HeadlessWidget*>(aWidget));
+ } else {
+ return new InProcessAndroidCompositorWidget(
+ aInitData.get_AndroidCompositorWidgetInitData(), aOptions,
+ static_cast<nsWindow*>(aWidget));
+ }
+}
+
+InProcessAndroidCompositorWidget::InProcessAndroidCompositorWidget(
+ const AndroidCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsWindow* aWindow)
+ : AndroidCompositorWidget(aInitData, aOptions), mWindow(aWindow) {}
+
+void InProcessAndroidCompositorWidget::ObserveVsync(VsyncObserver* aObserver) {
+ if (RefPtr<CompositorVsyncDispatcher> cvd =
+ mWindow->GetCompositorVsyncDispatcher()) {
+ cvd->SetCompositorVsyncObserver(aObserver);
+ }
+}
+
+nsIWidget* InProcessAndroidCompositorWidget::RealWidget() { return mWindow; }
+
+void InProcessAndroidCompositorWidget::OnCompositorSurfaceChanged() {
+ mSurface = java::sdk::Surface::Ref::From(
+ static_cast<jobject>(mWindow->GetNativeData(NS_JAVA_SURFACE)));
+}
+
+void InProcessAndroidCompositorWidget::NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ AndroidCompositorWidget::NotifyClientSizeChanged(aClientSize);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/InProcessAndroidCompositorWidget.h b/widget/android/InProcessAndroidCompositorWidget.h
new file mode 100644
index 0000000000..b7ba280d5c
--- /dev/null
+++ b/widget/android/InProcessAndroidCompositorWidget.h
@@ -0,0 +1,45 @@
+/* -*- 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/. */
+
+#ifndef widget_android_InProcessAndroidCompositorWidget_h
+#define widget_android_InProcessAndroidCompositorWidget_h
+
+#include "AndroidCompositorWidget.h"
+#include "CompositorWidget.h"
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+class InProcessAndroidCompositorWidget final
+ : public AndroidCompositorWidget,
+ public PlatformCompositorWidgetDelegate {
+ public:
+ InProcessAndroidCompositorWidget(
+ const AndroidCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsWindow* aWidget);
+
+ // CompositorWidget overrides
+
+ void ObserveVsync(VsyncObserver* aObserver) override;
+ nsIWidget* RealWidget() override;
+ CompositorWidgetDelegate* AsDelegate() override { return this; }
+
+ // PlatformCompositorWidgetDelegate overrides
+
+ void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override;
+
+ private:
+ // AndroidCompositorWidget overrides
+ void OnCompositorSurfaceChanged() override;
+
+ nsWindow* mWindow;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_android_InProcessAndroidCompositorWidget_h
diff --git a/widget/android/MediaKeysEventSourceFactory.cpp b/widget/android/MediaKeysEventSourceFactory.cpp
new file mode 100644
index 0000000000..b52919d4cc
--- /dev/null
+++ b/widget/android/MediaKeysEventSourceFactory.cpp
@@ -0,0 +1,17 @@
+/* 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 "MediaKeysEventSourceFactory.h"
+
+namespace mozilla {
+namespace widget {
+
+mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() {
+ // GeckoView uses MediaController.webidl for media session events and control,
+ // see bug 1623715.
+ return nullptr;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/PCompositorWidget.ipdl b/widget/android/PCompositorWidget.ipdl
new file mode 100644
index 0000000000..35a150cbba
--- /dev/null
+++ b/widget/android/PCompositorWidget.ipdl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 protocol PCompositorBridge;
+
+using mozilla::LayoutDeviceIntSize from "Units.h";
+
+namespace mozilla {
+namespace widget {
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PCompositorWidget
+{
+ manager PCompositorBridge;
+
+parent:
+ async __delete__();
+
+ async NotifyClientSizeChanged(LayoutDeviceIntSize aClientSize);
+
+child:
+ async ObserveVsync();
+ async UnobserveVsync();
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/PlatformWidgetTypes.ipdlh b/widget/android/PlatformWidgetTypes.ipdlh
new file mode 100644
index 0000000000..62ac0df9ff
--- /dev/null
+++ b/widget/android/PlatformWidgetTypes.ipdlh
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 HeadlessWidgetTypes;
+
+include "mozilla/GfxMessageUtils.h";
+
+using mozilla::LayoutDeviceIntSize from "Units.h";
+
+namespace mozilla {
+namespace widget {
+
+struct AndroidCompositorWidgetInitData
+{
+ int32_t widgetId;
+ LayoutDeviceIntSize clientSize;
+};
+
+union CompositorWidgetInitData
+{
+ AndroidCompositorWidgetInitData;
+ HeadlessCompositorWidgetInitData;
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/ScreenHelperAndroid.cpp b/widget/android/ScreenHelperAndroid.cpp
new file mode 100644
index 0000000000..33c91fe6e0
--- /dev/null
+++ b/widget/android/ScreenHelperAndroid.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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 "ScreenHelperAndroid.h"
+#include "AndroidRect.h"
+#include "nsThreadUtils.h"
+
+#include <mozilla/jni/Refs.h>
+
+#include "AndroidVsync.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+#include "mozilla/java/ScreenManagerHelperNatives.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "nsXULAppAPI.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+static ScreenHelperAndroid* gHelper = nullptr;
+
+class ScreenHelperAndroid::ScreenHelperSupport final
+ : public java::ScreenManagerHelper::Natives<ScreenHelperSupport> {
+ public:
+ typedef java::ScreenManagerHelper::Natives<ScreenHelperSupport> Base;
+
+ static void RefreshScreenInfo() { gHelper->Refresh(); }
+};
+
+static already_AddRefed<Screen> MakePrimaryScreen() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ java::sdk::Rect::LocalRef rect = java::GeckoAppShell::GetScreenSize();
+ LayoutDeviceIntRect bounds = LayoutDeviceIntRect(
+ rect->Left(), rect->Top(), rect->Width(), rect->Height());
+ uint32_t depth = java::GeckoAppShell::GetScreenDepth();
+ float density = java::GeckoAppShell::GetDensity();
+ float dpi = java::GeckoAppShell::GetDpi();
+ auto orientation =
+ hal::ScreenOrientation(java::GeckoAppShell::GetScreenOrientation());
+ uint16_t angle = java::GeckoAppShell::GetScreenAngle();
+ float refreshRate = java::GeckoAppShell::GetScreenRefreshRate();
+ return MakeAndAddRef<Screen>(bounds, bounds, depth, depth, refreshRate,
+ DesktopToLayoutDeviceScale(density),
+ CSSToLayoutDeviceScale(1.0f), dpi,
+ Screen::IsPseudoDisplay::No, orientation, angle);
+}
+
+ScreenHelperAndroid::ScreenHelperAndroid() {
+ MOZ_ASSERT(!gHelper);
+ gHelper = this;
+
+ ScreenHelperSupport::Base::Init();
+
+ Refresh();
+}
+
+ScreenHelperAndroid::~ScreenHelperAndroid() { gHelper = nullptr; }
+
+void ScreenHelperAndroid::Refresh() {
+ AutoTArray<RefPtr<Screen>, 1> screens;
+ screens.AppendElement(MakePrimaryScreen());
+ ScreenManager::Refresh(std::move(screens));
+
+ if (RefPtr<AndroidVsync> vsync = AndroidVsync::GetInstance()) {
+ vsync->OnMaybeUpdateRefreshRate();
+ }
+}
diff --git a/widget/android/ScreenHelperAndroid.h b/widget/android/ScreenHelperAndroid.h
new file mode 100644
index 0000000000..c7015ee873
--- /dev/null
+++ b/widget/android/ScreenHelperAndroid.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: ts=4 sw=2 expandtab:
+ * 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/. */
+
+#ifndef ScreenHelperAndroid_h___
+#define ScreenHelperAndroid_h___
+
+#include "mozilla/widget/ScreenManager.h"
+#include "nsTHashMap.h"
+
+namespace mozilla {
+namespace widget {
+
+class ScreenHelperAndroid final : public ScreenManager::Helper {
+ public:
+ class ScreenHelperSupport;
+
+ ScreenHelperAndroid();
+ ~ScreenHelperAndroid();
+
+ void Refresh();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* ScreenHelperAndroid_h___ */
diff --git a/widget/android/Telemetry.h b/widget/android/Telemetry.h
new file mode 100644
index 0000000000..ea8f2dd418
--- /dev/null
+++ b/widget/android/Telemetry.h
@@ -0,0 +1,39 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef mozilla_widget_Telemetry_h__
+#define mozilla_widget_Telemetry_h__
+
+#include "mozilla/java/TelemetryUtilsNatives.h"
+#include "nsAppShell.h"
+#include "nsIAndroidBridge.h"
+
+#include "mozilla/TimeStamp.h"
+#include "mozilla/glean/GleanMetrics.h"
+
+namespace mozilla {
+namespace widget {
+
+class Telemetry final : public java::TelemetryUtils::Natives<Telemetry> {
+ Telemetry() = delete;
+
+ public:
+ static void AddHistogram(jni::String::Param aName, int32_t aValue) {
+ MOZ_ASSERT(aName);
+ nsCString name = aName->ToCString();
+ if (name.EqualsLiteral("GV_STARTUP_RUNTIME_MS")) {
+ glean::geckoview::startup_runtime.AccumulateRawDuration(
+ TimeDuration::FromMilliseconds(aValue));
+ } else if (name.EqualsLiteral("GV_CONTENT_PROCESS_LIFETIME_MS")) {
+ glean::geckoview::content_process_lifetime.AccumulateRawDuration(
+ TimeDuration::FromMilliseconds(aValue));
+ }
+ }
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_Telemetry_h__
diff --git a/widget/android/WebExecutorSupport.cpp b/widget/android/WebExecutorSupport.cpp
new file mode 100644
index 0000000000..44b9a4696e
--- /dev/null
+++ b/widget/android/WebExecutorSupport.cpp
@@ -0,0 +1,472 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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 <algorithm>
+
+#include "GeckoViewStreamListener.h"
+#include "InetAddress.h" // for java::sdk::InetAddress and java::sdk::UnknownHostException
+#include "ReferrerInfo.h"
+#include "WebExecutorSupport.h"
+
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsICancelable.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIHttpHeaderVisitor.h"
+#include "nsIInputStream.h"
+#include "nsIDNSService.h"
+#include "nsIDNSListener.h"
+#include "nsIDNSRecord.h"
+#include "nsINSSErrorsService.h"
+#include "nsContentUtils.h"
+#include "nsNetUtil.h" // for NS_NewURI, NS_NewChannel, NS_NewStreamLoader
+#include "nsIPrivateBrowsingChannel.h"
+#include "nsIUploadChannel2.h"
+#include "nsIX509Cert.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/net/DNS.h" // for NetAddr
+#include "mozilla/java/GeckoWebExecutorWrappers.h"
+#include "mozilla/java/WebMessageWrappers.h"
+#include "mozilla/java/WebRequestErrorWrappers.h"
+#include "mozilla/java/WebResponseWrappers.h"
+
+namespace mozilla {
+using namespace net;
+
+namespace widget {
+
+static void CompleteWithError(java::GeckoResult::Param aResult,
+ nsresult aStatus, nsIChannel* aChannel) {
+ nsCOMPtr<nsINSSErrorsService> errSvc =
+ do_GetService("@mozilla.org/nss_errors_service;1");
+ MOZ_ASSERT(errSvc);
+
+ uint32_t errorClass;
+ nsresult rv = errSvc->GetErrorClass(aStatus, &errorClass);
+ if (NS_FAILED(rv)) {
+ errorClass = 0;
+ }
+
+ jni::ByteArray::LocalRef certBytes;
+ if (aChannel) {
+ std::tie(certBytes, std::ignore) =
+ GeckoViewStreamListener::CertificateFromChannel(aChannel);
+ }
+
+ java::WebRequestError::LocalRef error = java::WebRequestError::FromGeckoError(
+ int64_t(aStatus), NS_ERROR_GET_MODULE(aStatus), errorClass, certBytes);
+
+ aResult->CompleteExceptionally(error.Cast<jni::Throwable>());
+}
+
+static void CompleteWithError(java::GeckoResult::Param aResult,
+ nsresult aStatus) {
+ CompleteWithError(aResult, aStatus, nullptr);
+}
+
+class ByteBufferStream final : public nsIInputStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ explicit ByteBufferStream(jni::ByteBuffer::Param buffer)
+ : mBuffer(buffer), mPosition(0), mClosed(false) {
+ MOZ_ASSERT(mBuffer);
+ MOZ_ASSERT(mBuffer->Address());
+ }
+
+ NS_IMETHOD
+ Close() override {
+ mClosed = true;
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ Available(uint64_t* aResult) override {
+ if (mClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ *aResult = (mBuffer->Capacity() - mPosition);
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ StreamStatus() override { return mClosed ? NS_BASE_STREAM_CLOSED : NS_OK; }
+
+ NS_IMETHOD
+ Read(char* aBuf, uint32_t aCount, uint32_t* aCountRead) override {
+ if (mClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ *aCountRead = uint32_t(
+ std::min(uint64_t(mBuffer->Capacity() - mPosition), uint64_t(aCount)));
+
+ if (*aCountRead > 0) {
+ memcpy(aBuf, (char*)mBuffer->Address() + mPosition, *aCountRead);
+ mPosition += *aCountRead;
+ }
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount,
+ uint32_t* aResult) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ NS_IMETHOD
+ IsNonBlocking(bool* aResult) override {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ protected:
+ virtual ~ByteBufferStream() {}
+
+ const jni::ByteBuffer::GlobalRef mBuffer;
+ uint64_t mPosition;
+ bool mClosed;
+};
+
+NS_IMPL_ISUPPORTS(ByteBufferStream, nsIInputStream)
+
+class LoaderListener final : public GeckoViewStreamListener {
+ public:
+ explicit LoaderListener(java::GeckoResult::Param aResult,
+ bool aAllowRedirects, bool testStreamFailure)
+ : GeckoViewStreamListener(),
+ mResult(aResult),
+ mTestStreamFailure(testStreamFailure),
+ mAllowRedirects(aAllowRedirects) {
+ MOZ_ASSERT(mResult);
+ }
+
+ NS_IMETHOD
+ OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream,
+ uint64_t aOffset, uint32_t aCount) override {
+ MOZ_ASSERT(mStream);
+
+ if (mTestStreamFailure) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // We only need this for the ReadSegments call, the value is unused.
+ uint32_t countRead;
+ nsresult rv =
+ aInputStream->ReadSegments(WriteSegment, this, aCount, &countRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+ }
+
+ NS_IMETHOD
+ AsyncOnChannelRedirect(nsIChannel* aOldChannel, nsIChannel* aNewChannel,
+ uint32_t flags,
+ nsIAsyncVerifyRedirectCallback* callback) override {
+ if (!mAllowRedirects) {
+ return NS_ERROR_ABORT;
+ }
+
+ callback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+ }
+
+ void SendWebResponse(java::WebResponse::Param aResponse) override {
+ mResult->Complete(aResponse);
+ }
+
+ void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) override {
+ mozilla::widget::CompleteWithError(mResult, aStatus, aChannel);
+ }
+
+ virtual ~LoaderListener() {}
+
+ const java::GeckoResult::GlobalRef mResult;
+ const bool mTestStreamFailure;
+ bool mAllowRedirects;
+};
+
+class DNSListener final : public nsIDNSListener {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ DNSListener(const nsCString& aHost, java::GeckoResult::Param aResult)
+ : mHost(aHost), mResult(aResult) {}
+
+ NS_IMETHOD
+ OnLookupComplete(nsICancelable* aRequest, nsIDNSRecord* aRecord,
+ nsresult aStatus) override {
+ if (NS_FAILED(aStatus)) {
+ CompleteUnknownHostError();
+ return NS_OK;
+ }
+
+ nsresult rv = CompleteWithRecord(aRecord);
+ if (NS_FAILED(rv)) {
+ CompleteUnknownHostError();
+ return NS_OK;
+ }
+
+ return NS_OK;
+ }
+
+ void CompleteUnknownHostError() {
+ java::sdk::UnknownHostException::LocalRef error =
+ java::sdk::UnknownHostException::New();
+ mResult->CompleteExceptionally(error.Cast<jni::Throwable>());
+ }
+
+ private:
+ nsresult CompleteWithRecord(nsIDNSRecord* aRecord) {
+ nsTArray<NetAddr> addrs;
+ nsCOMPtr<nsIDNSAddrRecord> rec = do_QueryInterface(aRecord);
+ if (!rec) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsresult rv = rec->GetAddresses(addrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ jni::ByteArray::LocalRef bytes;
+ auto objects =
+ jni::ObjectArray::New<java::sdk::InetAddress>(addrs.Length());
+ for (size_t i = 0; i < addrs.Length(); i++) {
+ const auto& addr = addrs[i];
+ if (addr.raw.family == AF_INET) {
+ bytes = jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(&addr.inet.ip), 4);
+ } else if (addr.raw.family == AF_INET6) {
+ bytes = jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(&addr.inet6.ip), 16);
+ } else {
+ // We don't handle this, skip it.
+ continue;
+ }
+
+ objects->SetElement(i,
+ java::sdk::InetAddress::GetByAddress(mHost, bytes));
+ }
+
+ mResult->Complete(objects);
+ return NS_OK;
+ }
+
+ virtual ~DNSListener() {}
+
+ const nsCString mHost;
+ const java::GeckoResult::GlobalRef mResult;
+};
+
+NS_IMPL_ISUPPORTS(DNSListener, nsIDNSListener)
+
+static nsresult ConvertCacheMode(int32_t mode, int32_t& result) {
+ switch (mode) {
+ case java::WebRequest::CACHE_MODE_DEFAULT:
+ result = nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT;
+ break;
+ case java::WebRequest::CACHE_MODE_NO_STORE:
+ result = nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE;
+ break;
+ case java::WebRequest::CACHE_MODE_RELOAD:
+ result = nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD;
+ break;
+ case java::WebRequest::CACHE_MODE_NO_CACHE:
+ result = nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE;
+ break;
+ case java::WebRequest::CACHE_MODE_FORCE_CACHE:
+ result = nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE;
+ break;
+ case java::WebRequest::CACHE_MODE_ONLY_IF_CACHED:
+ result = nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED;
+ break;
+ default:
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+static nsresult SetupHttpChannel(nsIHttpChannel* aHttpChannel,
+ nsIChannel* aChannel,
+ java::WebRequest::Param aRequest) {
+ const auto req = java::WebRequest::LocalRef(aRequest);
+ const auto reqBase = java::WebMessage::LocalRef(req.Cast<java::WebMessage>());
+
+ // Method
+ nsresult rv = aHttpChannel->SetRequestMethod(aRequest->Method()->ToCString());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Headers
+ const auto keys = reqBase->GetHeaderKeys();
+ const auto values = reqBase->GetHeaderValues();
+ nsCString contentType;
+ for (size_t i = 0; i < keys->Length(); i++) {
+ const auto key = jni::String::LocalRef(keys->GetElement(i))->ToCString();
+ const auto value =
+ jni::String::LocalRef(values->GetElement(i))->ToCString();
+
+ if (key.LowerCaseEqualsASCII("content-type")) {
+ contentType = value;
+ }
+
+ // We clobber any duplicate keys here because we've already merged them
+ // in the upstream WebRequest.
+ rv = aHttpChannel->SetRequestHeader(key, value, false /* merge */);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Body
+ const auto body = req->Body();
+ if (body) {
+ nsCOMPtr<nsIInputStream> stream = new ByteBufferStream(body);
+
+ nsCOMPtr<nsIUploadChannel2> uploadChannel(do_QueryInterface(aChannel, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = uploadChannel->ExplicitSetUploadStream(
+ stream, contentType, -1, aRequest->Method()->ToCString(), false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Referrer
+ RefPtr<nsIURI> referrerUri;
+ const auto referrer = req->Referrer();
+ if (referrer) {
+ rv = NS_NewURI(getter_AddRefs(referrerUri), referrer->ToString());
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+ }
+
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = new dom::ReferrerInfo(referrerUri);
+ rv = aHttpChannel->SetReferrerInfoWithoutClone(referrerInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Cache mode
+ nsCOMPtr<nsIHttpChannelInternal> internalChannel(
+ do_QueryInterface(aChannel, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t cacheMode;
+ rv = ConvertCacheMode(req->CacheMode(), cacheMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = internalChannel->SetFetchCacheMode(cacheMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (req->BeConservative()) {
+ rv = internalChannel->SetBeConservative(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // We don't have any UI
+ rv = internalChannel->SetBlockAuthPrompt(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult WebExecutorSupport::CreateStreamLoader(
+ java::WebRequest::Param aRequest, int32_t aFlags,
+ java::GeckoResult::Param aResult) {
+ const auto req = java::WebRequest::LocalRef(aRequest);
+ const auto reqBase = java::WebMessage::LocalRef(req.Cast<java::WebMessage>());
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), reqBase->Uri()->ToString());
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel), uri,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aFlags & java::GeckoWebExecutor::FETCH_FLAGS_ANONYMOUS) {
+ channel->SetLoadFlags(nsIRequest::LOAD_ANONYMOUS);
+ }
+
+ bool shouldResistFingerprinting = nsContentUtils::ShouldResistFingerprinting(
+ channel, RFPTarget::IsAlwaysEnabledForPrecompute);
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ if (aFlags & java::GeckoWebExecutor::FETCH_FLAGS_PRIVATE) {
+ nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(channel);
+ NS_ENSURE_TRUE(pbChannel, NS_ERROR_FAILURE);
+ pbChannel->SetPrivate(true);
+ cookieJarSettings = CookieJarSettings::Create(CookieJarSettings::ePrivate,
+ shouldResistFingerprinting);
+ } else {
+ cookieJarSettings = CookieJarSettings::Create(CookieJarSettings::eRegular,
+ shouldResistFingerprinting);
+ }
+ MOZ_ASSERT(cookieJarSettings);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ loadInfo->SetCookieJarSettings(cookieJarSettings);
+
+ // setup http/https specific things
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel, &rv));
+ if (httpChannel) {
+ rv = SetupHttpChannel(httpChannel, channel, aRequest);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // set up the listener
+ const bool allowRedirects =
+ !(aFlags & java::GeckoWebExecutor::FETCH_FLAGS_NO_REDIRECTS);
+ const bool testStreamFailure =
+ (aFlags & java::GeckoWebExecutor::FETCH_FLAGS_STREAM_FAILURE_TEST);
+
+ RefPtr<LoaderListener> listener =
+ new LoaderListener(aResult, allowRedirects, testStreamFailure);
+
+ rv = channel->SetNotificationCallbacks(listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, open the channel
+ return channel->AsyncOpen(listener);
+}
+
+void WebExecutorSupport::Fetch(jni::Object::Param aRequest, int32_t aFlags,
+ jni::Object::Param aResult) {
+ const auto request = java::WebRequest::LocalRef(aRequest);
+ auto result = java::GeckoResult::LocalRef(aResult);
+
+ nsresult rv = CreateStreamLoader(request, aFlags, result);
+ if (NS_FAILED(rv)) {
+ CompleteWithError(result, rv);
+ }
+}
+
+static nsresult ResolveHost(nsCString& host, java::GeckoResult::Param result) {
+ nsresult rv;
+ nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsICancelable> cancelable;
+ RefPtr<DNSListener> listener = new DNSListener(host, result);
+ rv = dns->AsyncResolveNative(host, nsIDNSService::RESOLVE_TYPE_DEFAULT,
+ nsIDNSService::RESOLVE_DEFAULT_FLAGS, nullptr,
+ listener, nullptr /* aListenerTarget */,
+ OriginAttributes(), getter_AddRefs(cancelable));
+ return rv;
+}
+
+void WebExecutorSupport::Resolve(jni::String::Param aUri,
+ jni::Object::Param aResult) {
+ auto result = java::GeckoResult::LocalRef(aResult);
+
+ nsCString uri = aUri->ToCString();
+ nsresult rv = ResolveHost(uri, result);
+ if (NS_FAILED(rv)) {
+ java::sdk::UnknownHostException::LocalRef error =
+ java::sdk::UnknownHostException::New();
+ result->CompleteExceptionally(error.Cast<jni::Throwable>());
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/android/WebExecutorSupport.h b/widget/android/WebExecutorSupport.h
new file mode 100644
index 0000000000..65a68fcc40
--- /dev/null
+++ b/widget/android/WebExecutorSupport.h
@@ -0,0 +1,32 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef WebExecutorSupport_h__
+#define WebExecutorSupport_h__
+
+#include "mozilla/java/GeckoWebExecutorNatives.h"
+#include "mozilla/java/GeckoResultWrappers.h"
+#include "mozilla/java/WebRequestWrappers.h"
+
+namespace mozilla {
+namespace widget {
+
+class WebExecutorSupport final
+ : public java::GeckoWebExecutor::Natives<WebExecutorSupport> {
+ public:
+ static void Fetch(jni::Object::Param request, int32_t flags,
+ jni::Object::Param result);
+ static void Resolve(jni::String::Param aUri, jni::Object::Param result);
+
+ protected:
+ static nsresult CreateStreamLoader(java::WebRequest::Param aRequest,
+ int32_t aFlags,
+ java::GeckoResult::Param aResult);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // WebExecutorSupport_h__
diff --git a/widget/android/WindowEvent.h b/widget/android/WindowEvent.h
new file mode 100644
index 0000000000..fdb73bf692
--- /dev/null
+++ b/widget/android/WindowEvent.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_WindowEvent_h
+#define mozilla_widget_WindowEvent_h
+
+#include "nsThreadUtils.h"
+#include "mozilla/jni/Natives.h"
+
+namespace mozilla {
+namespace widget {
+
+// An Event subclass that guards against stale events.
+// (See the implmentation of mozilla::jni::detail::ProxyNativeCall for info
+// about the default template parameters for this class)
+template <typename Lambda, bool IsStatic = Lambda::isStatic,
+ typename InstanceType = typename Lambda::ThisArgType,
+ class Impl = typename Lambda::TargetClass>
+class WindowEvent : public Runnable {
+ bool IsStaleCall() {
+ if (IsStatic) {
+ // Static calls are never stale.
+ return false;
+ }
+
+ return jni::NativePtrTraits<Impl>::IsStale(mInstance);
+ }
+
+ Lambda mLambda;
+ const InstanceType mInstance;
+
+ public:
+ WindowEvent(Lambda&& aLambda, InstanceType&& aInstance)
+ : Runnable("mozilla::widget::WindowEvent"),
+ mLambda(std::move(aLambda)),
+ mInstance(std::forward<InstanceType>(aInstance)) {}
+
+ explicit WindowEvent(Lambda&& aLambda)
+ : Runnable("mozilla::widget::WindowEvent"),
+ mLambda(std::move(aLambda)),
+ mInstance(mLambda.GetThisArg()) {}
+
+ NS_IMETHOD Run() override {
+ if (!IsStaleCall()) {
+ mLambda();
+ }
+ return NS_OK;
+ }
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_WindowEvent_h
diff --git a/widget/android/bindings/AccessibilityEvent-classes.txt b/widget/android/bindings/AccessibilityEvent-classes.txt
new file mode 100644
index 0000000000..b54e3ce105
--- /dev/null
+++ b/widget/android/bindings/AccessibilityEvent-classes.txt
@@ -0,0 +1,3 @@
+# We only use constants from AccessibilityEvent
+[android.view.accessibility.AccessibilityEvent = skip:true]
+<field> = skip:false
diff --git a/widget/android/bindings/AndroidBuild-classes.txt b/widget/android/bindings/AndroidBuild-classes.txt
new file mode 100644
index 0000000000..a76aa12c66
--- /dev/null
+++ b/widget/android/bindings/AndroidBuild-classes.txt
@@ -0,0 +1,5 @@
+[android.os.Build]
+<field> = noLiteral:true
+
+[android.os.Build$VERSION]
+<field> = noLiteral:true
diff --git a/widget/android/bindings/AndroidDragEvent-classes.txt b/widget/android/bindings/AndroidDragEvent-classes.txt
new file mode 100644
index 0000000000..368f500139
--- /dev/null
+++ b/widget/android/bindings/AndroidDragEvent-classes.txt
@@ -0,0 +1,3 @@
+# We only use constants from DragEvent
+[android.view.DragEvent = skip:true]
+<field> = skip:false
diff --git a/widget/android/bindings/AndroidGraphics-classes.txt b/widget/android/bindings/AndroidGraphics-classes.txt
new file mode 100644
index 0000000000..452ba404e8
--- /dev/null
+++ b/widget/android/bindings/AndroidGraphics-classes.txt
@@ -0,0 +1,10 @@
+[android.graphics.Bitmap = skip:true]
+copyPixelsFromBuffer(Ljava/nio/Buffer;)V =
+createBitmap(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap; =
+
+[android.graphics.Bitmap$Config = skip:true]
+valueOf(Ljava/lang/String;)Landroid/graphics/Bitmap$Config; =
+ALPHA_8 =
+ARGB_8888 =
+RGBA_F16 =
+RGB_565 = \ No newline at end of file
diff --git a/widget/android/bindings/AndroidInputType-classes.txt b/widget/android/bindings/AndroidInputType-classes.txt
new file mode 100644
index 0000000000..fa1138dce4
--- /dev/null
+++ b/widget/android/bindings/AndroidInputType-classes.txt
@@ -0,0 +1,3 @@
+# We only use constants from InputType
+[android.text.InputType = skip:true]
+<field> = skip:false
diff --git a/widget/android/bindings/AndroidProcess-classes.txt b/widget/android/bindings/AndroidProcess-classes.txt
new file mode 100644
index 0000000000..a6929ac5bc
--- /dev/null
+++ b/widget/android/bindings/AndroidProcess-classes.txt
@@ -0,0 +1,5 @@
+[android.os.Process = skip:true]
+setThreadPriority =
+getThreadPriority =
+myTid =
+THREAD_PRIORITY_URGENT_AUDIO =
diff --git a/widget/android/bindings/AndroidRect-classes.txt b/widget/android/bindings/AndroidRect-classes.txt
new file mode 100644
index 0000000000..76d3094a9f
--- /dev/null
+++ b/widget/android/bindings/AndroidRect-classes.txt
@@ -0,0 +1,2 @@
+[android.graphics.Rect]
+[android.graphics.RectF]
diff --git a/widget/android/bindings/InetAddress-classes.txt b/widget/android/bindings/InetAddress-classes.txt
new file mode 100644
index 0000000000..65788be252
--- /dev/null
+++ b/widget/android/bindings/InetAddress-classes.txt
@@ -0,0 +1,6 @@
+# We only want getByAddress(String, byte[])
+[java.net.InetAddress = skip:true]
+getByAddress(Ljava/lang/String;[B)Ljava/net/InetAddress; = skip:false
+
+[java.net.UnknownHostException = skip:true]
+<init>()V =
diff --git a/widget/android/bindings/JavaBuiltins-classes.txt b/widget/android/bindings/JavaBuiltins-classes.txt
new file mode 100644
index 0000000000..c6be5dde34
--- /dev/null
+++ b/widget/android/bindings/JavaBuiltins-classes.txt
@@ -0,0 +1,25 @@
+[java.lang.Boolean = skip:true]
+# Use static fields for boxing boolean.
+TRUE =
+FALSE =
+booleanValue =
+
+[java.lang.Double = skip:true]
+<init>(D)V =
+
+[java.lang.Integer = skip:true]
+# Use valueOf() for boxing int; don't use constructor
+# because some Integer values are cached.
+valueOf(I)Ljava/lang/Integer; =
+
+[java.lang.Long = skip:true]
+valueOf(J)Ljava/lang/Long; =
+
+[java.lang.Number = skip:true]
+# Use doubleValue() for unboxing Double/Float/Long.
+doubleValue =
+# Use intValue() for unboxing Byte/Int/Short.
+intValue =
+
+[java.lang.String = skip:true]
+valueOf(Ljava/lang/Object;)Ljava/lang/String; =
diff --git a/widget/android/bindings/JavaExceptions-classes.txt b/widget/android/bindings/JavaExceptions-classes.txt
new file mode 100644
index 0000000000..ebaff375d5
--- /dev/null
+++ b/widget/android/bindings/JavaExceptions-classes.txt
@@ -0,0 +1,8 @@
+[java.lang.IllegalStateException = skip:true]
+<init>(Ljava/lang/String;)V =
+
+[java.lang.IllegalArgumentException = skip:true]
+<init>(Ljava/lang/String;)V =
+
+[java.lang.Throwable = skip:true]
+getMessage()Ljava/lang/String; =
diff --git a/widget/android/bindings/KeyEvent-classes.txt b/widget/android/bindings/KeyEvent-classes.txt
new file mode 100644
index 0000000000..6001a5025b
--- /dev/null
+++ b/widget/android/bindings/KeyEvent-classes.txt
@@ -0,0 +1,3 @@
+# We only use constants from KeyEvent
+[android.view.KeyEvent = skip:true]
+<field> = skip:false
diff --git a/widget/android/bindings/MediaCodec-classes.txt b/widget/android/bindings/MediaCodec-classes.txt
new file mode 100644
index 0000000000..a68b86e148
--- /dev/null
+++ b/widget/android/bindings/MediaCodec-classes.txt
@@ -0,0 +1,15 @@
+[android.media.MediaCodec = exceptionMode:nsresult]
+[android.media.MediaCodec$BufferInfo = exceptionMode:nsresult]
+[android.media.MediaCodec$CryptoInfo = exceptionMode:nsresult]
+
+# We only use constants from CodecCapabilities
+[android.media.MediaCodecInfo$CodecCapabilities = skip:true]
+<field> = skip:false
+
+# We only use constants from KeyStatus
+[android.media.MediaDrm$KeyStatus = skip:true]
+<field> = skip:false
+[android.media.AudioFormat = skip:true]
+<field> = skip:false
+
+[android.media.MediaFormat = exceptionMode:nsresult]
diff --git a/widget/android/bindings/MotionEvent-classes.txt b/widget/android/bindings/MotionEvent-classes.txt
new file mode 100644
index 0000000000..17874a16af
--- /dev/null
+++ b/widget/android/bindings/MotionEvent-classes.txt
@@ -0,0 +1,3 @@
+# We only use constants from MotionEvent
+[android.view.MotionEvent = skip:true]
+<field> = skip:false
diff --git a/widget/android/bindings/SurfaceTexture-classes.txt b/widget/android/bindings/SurfaceTexture-classes.txt
new file mode 100644
index 0000000000..554c8e7c3f
--- /dev/null
+++ b/widget/android/bindings/SurfaceTexture-classes.txt
@@ -0,0 +1,5 @@
+[android.graphics.SurfaceTexture = exceptionMode:nsresult]
+[android.view.Surface = exceptionMode:nsresult]
+<init>(Landroid/view/SurfaceControl;)V = stubName:FromSurfaceControl, exceptionMode:abort
+[android.view.SurfaceControl = exceptionMode:nsresult]
+isValid()Z = exceptionMode:abort
diff --git a/widget/android/bindings/ViewConfiguration-classes.txt b/widget/android/bindings/ViewConfiguration-classes.txt
new file mode 100644
index 0000000000..cf8689f25a
--- /dev/null
+++ b/widget/android/bindings/ViewConfiguration-classes.txt
@@ -0,0 +1 @@
+[android.view.ViewConfiguration = exceptionMode:nsresult]
diff --git a/widget/android/bindings/moz.build b/widget/android/bindings/moz.build
new file mode 100644
index 0000000000..f3f4d138ff
--- /dev/null
+++ b/widget/android/bindings/moz.build
@@ -0,0 +1,55 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("GeckoView", "General")
+
+# List of stems to generate .cpp and .h files for. To add a stem, add it to
+# this list and ensure that $(stem)-classes.txt exists in this directory.
+generated = [
+ "AccessibilityEvent",
+ "AndroidBuild",
+ "AndroidDragEvent",
+ "AndroidGraphics",
+ "AndroidInputType",
+ "AndroidProcess",
+ "AndroidRect",
+ "InetAddress",
+ "JavaBuiltins",
+ "JavaExceptions",
+ "KeyEvent",
+ "MediaCodec",
+ "MotionEvent",
+ "SurfaceTexture",
+ "ViewConfiguration",
+]
+
+SOURCES += ["!%s.cpp" % stem for stem in generated]
+
+EXPORTS += ["!%s.h" % stem for stem in generated]
+
+# The recursive make backend treats the first output specially: it's passed as
+# an open FileAvoidWrite to the invoked script. That doesn't work well with
+# the Gradle task that generates all of the outputs, so we add a dummy first
+# output.
+t = tuple(
+ ["sdk_bindings"]
+ + ["%s.cpp" % stem for stem in generated]
+ + ["%s.h" % stem for stem in generated]
+)
+
+GeneratedFile(
+ *t,
+ script="/mobile/android/gradle.py",
+ entry_point="generate_sdk_bindings",
+ inputs=["%s-classes.txt" % stem for stem in generated]
+)
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/widget/android",
+]
diff --git a/widget/android/components.conf b/widget/android/components.conf
new file mode 100644
index 0000000000..1c7b49cd38
--- /dev/null
+++ b/widget/android/components.conf
@@ -0,0 +1,109 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Headers = [
+ '/widget/android/nsWidgetFactory.h',
+]
+
+InitFunc = 'nsWidgetAndroidModuleCtor'
+UnloadFunc = 'nsWidgetAndroidModuleDtor'
+
+Classes = [
+ {
+ 'cid': '{2d96b3df-c051-11d1-a827-0040959a28c9}',
+ 'contract_ids': ['@mozilla.org/widget/appshell/android;1'],
+ 'legacy_constructor': 'nsAppShellConstructor',
+ 'headers': ['/widget/android/nsWidgetFactory.h'],
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_UTILITY_AND_GMPLUGIN_PROCESS,
+ },
+ {
+ 'cid': '{d594094c-28b6-466b-97d7-66c039c3dea9}',
+ 'contract_ids': ['@mozilla.org/gfx/parent/screenmanager;1'],
+ 'singleton': True,
+ 'type': 'mozilla::widget::ScreenManager',
+ 'headers': ['mozilla/widget/ScreenManager.h'],
+ 'constructor': 'mozilla::widget::ScreenManager::GetAddRefedSingleton',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{6987230e-0098-4e78-bc5f-1493ee7519fa}',
+ 'contract_ids': ['@mozilla.org/widget/useridleservice;1'],
+ 'singleton': True,
+ 'type': 'nsUserIdleServiceAndroid',
+ 'headers': ['/widget/android/nsUserIdleServiceAndroid.h'],
+ 'constructor': 'nsUserIdleServiceAndroid::GetInstance',
+ },
+ {
+ 'cid': '{8b5314bc-db01-11d2-96ce-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/transferable;1'],
+ 'type': 'nsTransferable',
+ 'headers': ['/widget/nsTransferable.h'],
+ },
+ {
+ 'cid': '{9d5adbb9-1da4-4162-acba-b373fe3ae837}',
+ 'contract_ids': ['@mozilla.org/widget/parent/clipboard;1'],
+ 'interfaces': ['nsIClipboard'],
+ 'type': 'nsClipboard',
+ 'headers': ['/widget/android/nsClipboard.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}',
+ 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'],
+ 'type': 'nsClipboardHelper',
+ 'headers': ['/widget/nsClipboardHelper.h'],
+ },
+ {
+ 'cid': '{841387c8-72e6-484b-9296-bf6eea80d58a}',
+ 'contract_ids': ['@mozilla.org/gfx/printsettings-service;1'],
+ 'type': 'nsPrintSettingsServiceAndroid',
+ 'headers': ['/widget/android/nsPrintSettingsServiceAndroid.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'cid': '{d3f69889-e13a-4321-980c-a39332e21f34}',
+ 'contract_ids': ['@mozilla.org/gfx/devicecontextspec;1'],
+ 'type': 'nsDeviceContextSpecAndroid',
+ 'headers': ['/widget/android/nsDeviceContextAndroid.h'],
+ },
+ {
+ 'cid': '{948a0023-e3a7-11d2-96cf-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/htmlformatconverter;1'],
+ 'type': 'nsHTMLFormatConverter',
+ 'headers': ['/widget/nsHTMLFormatConverter.h'],
+ },
+ {
+ 'name': 'GfxInfo',
+ 'cid': '{d755a760-9f27-11df-0800-200c9a664242}',
+ 'contract_ids': ['@mozilla.org/gfx/info;1'],
+ 'type': 'mozilla::widget::GfxInfo',
+ 'headers': ['/widget/android/GfxInfo.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'js_name': 'androidBridge',
+ 'cid': '{0fe2321d-ebd9-467d-a743-03a68d40599e}',
+ 'contract_ids': ['@mozilla.org/android/bridge;1'],
+ 'interfaces': ['nsIAndroidBridge'],
+ 'type': 'nsAndroidBridge',
+ 'headers': ['/widget/android/AndroidBridge.h'],
+ },
+ {
+ 'cid': '{84e11f80-ca55-11dd-ad8b-0800200c9a66}',
+ 'contract_ids': ['@mozilla.org/system-alerts-service;1'],
+ 'type': 'mozilla::widget::AndroidAlerts',
+ 'headers': ['/widget/android/AndroidAlerts.h'],
+ },
+ {
+ 'cid': '{b1abaf0e-52b2-4e65-aee1-299ea9a74230}',
+ 'contract_ids': ['@mozilla.org/widget/parent/dragservice;1'],
+ 'singleton': True,
+ 'type': 'nsDragService',
+ 'headers': ['/widget/android/nsDragService.h'],
+ 'constructor': 'nsDragService::GetInstance',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
diff --git a/widget/android/jni/Accessors.h b/widget/android/jni/Accessors.h
new file mode 100644
index 0000000000..7496cbcb5a
--- /dev/null
+++ b/widget/android/jni/Accessors.h
@@ -0,0 +1,251 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_Accessors_h__
+#define mozilla_jni_Accessors_h__
+
+#include <jni.h>
+
+#include "mozilla/jni/Refs.h"
+#include "mozilla/jni/Types.h"
+#include "mozilla/jni/Utils.h"
+#include "AndroidBridge.h"
+
+namespace mozilla {
+namespace jni {
+
+namespace detail {
+
+// Helper class to convert an arbitrary type to a jvalue, e.g. Value(123).val.
+struct Value {
+ explicit Value(jboolean z) { val.z = z; }
+ explicit Value(jbyte b) { val.b = b; }
+ explicit Value(jchar c) { val.c = c; }
+ explicit Value(jshort s) { val.s = s; }
+ explicit Value(jint i) { val.i = i; }
+ explicit Value(jlong j) { val.j = j; }
+ explicit Value(jfloat f) { val.f = f; }
+ explicit Value(jdouble d) { val.d = d; }
+ explicit Value(jobject l) { val.l = l; }
+
+ jvalue val;
+};
+
+} // namespace detail
+
+using namespace detail;
+
+// Base class for Method<>, Field<>, and Constructor<>.
+class Accessor {
+ static void GetNsresult(JNIEnv* env, nsresult* rv) {
+ if (env->ExceptionCheck()) {
+#ifdef MOZ_CHECK_JNI
+ env->ExceptionDescribe();
+#endif
+ env->ExceptionClear();
+ *rv = NS_ERROR_FAILURE;
+ } else {
+ *rv = NS_OK;
+ }
+ }
+
+ protected:
+ // Called after making a JNIEnv call.
+ template <class Traits>
+ static void EndAccess(const typename Traits::Owner::Context& ctx,
+ nsresult* rv) {
+ if (Traits::exceptionMode == ExceptionMode::ABORT) {
+ MOZ_CATCH_JNI_EXCEPTION(ctx.Env());
+
+ } else if (Traits::exceptionMode == ExceptionMode::NSRESULT) {
+ GetNsresult(ctx.Env(), rv);
+ }
+ }
+};
+
+// Member<> is used to call a JNI method given a traits class.
+template <class Traits, typename ReturnType = typename Traits::ReturnType>
+class Method : public Accessor {
+ typedef Accessor Base;
+ typedef typename Traits::Owner::Context Context;
+
+ protected:
+ static jmethodID sID;
+
+ static void BeginAccess(const Context& ctx) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+ static_assert(Traits::dispatchTarget == DispatchTarget::CURRENT,
+ "Dispatching not supported for method call");
+
+ if (sID) {
+ return;
+ }
+
+ if (Traits::isStatic) {
+ MOZ_ALWAYS_TRUE(
+ sID = AndroidBridge::GetStaticMethodID(
+ ctx.Env(), ctx.ClassRef(), Traits::name, Traits::signature));
+ } else {
+ MOZ_ALWAYS_TRUE(
+ sID = AndroidBridge::GetMethodID(ctx.Env(), ctx.ClassRef(),
+ Traits::name, Traits::signature));
+ }
+ }
+
+ static void EndAccess(const Context& ctx, nsresult* rv) {
+ return Base::EndAccess<Traits>(ctx, rv);
+ }
+
+ public:
+ template <typename... Args>
+ static ReturnType Call(const Context& ctx, nsresult* rv,
+ const Args&... args) {
+ JNIEnv* const env = ctx.Env();
+ BeginAccess(ctx);
+
+ jvalue jargs[] = {Value(TypeAdapter<Args>::FromNative(env, args)).val...};
+
+ auto result = TypeAdapter<ReturnType>::ToNative(
+ env, Traits::isStatic ? (env->*TypeAdapter<ReturnType>::StaticCall)(
+ ctx.ClassRef(), sID, jargs)
+ : (env->*TypeAdapter<ReturnType>::Call)(
+ ctx.Get(), sID, jargs));
+
+ EndAccess(ctx, rv);
+ return result;
+ }
+};
+
+// Define sID member.
+template <class T, typename R>
+jmethodID Method<T, R>::sID;
+
+// Specialize void because C++ forbids us from
+// using a "void" temporary result variable.
+template <class Traits>
+class Method<Traits, void> : public Method<Traits, bool> {
+ typedef Method<Traits, bool> Base;
+ typedef typename Traits::Owner::Context Context;
+
+ public:
+ template <typename... Args>
+ static void Call(const Context& ctx, nsresult* rv, const Args&... args) {
+ JNIEnv* const env = ctx.Env();
+ Base::BeginAccess(ctx);
+
+ jvalue jargs[] = {Value(TypeAdapter<Args>::FromNative(env, args)).val...};
+
+ if (Traits::isStatic) {
+ env->CallStaticVoidMethodA(ctx.ClassRef(), Base::sID, jargs);
+ } else {
+ env->CallVoidMethodA(ctx.Get(), Base::sID, jargs);
+ }
+
+ Base::EndAccess(ctx, rv);
+ }
+};
+
+// Constructor<> is used to construct a JNI instance given a traits class.
+template <class Traits>
+class Constructor : protected Method<Traits, typename Traits::ReturnType> {
+ typedef typename Traits::Owner::Context Context;
+ typedef typename Traits::ReturnType ReturnType;
+ typedef Method<Traits, ReturnType> Base;
+
+ public:
+ template <typename... Args>
+ static ReturnType Call(const Context& ctx, nsresult* rv,
+ const Args&... args) {
+ JNIEnv* const env = ctx.Env();
+ Base::BeginAccess(ctx);
+
+ jvalue jargs[] = {Value(TypeAdapter<Args>::FromNative(env, args)).val...};
+
+ auto result = TypeAdapter<ReturnType>::ToNative(
+ env, env->NewObjectA(ctx.ClassRef(), Base::sID, jargs));
+
+ Base::EndAccess(ctx, rv);
+ return result;
+ }
+};
+
+// Field<> is used to access a JNI field given a traits class.
+template <class Traits>
+class Field : public Accessor {
+ typedef Accessor Base;
+ typedef typename Traits::Owner::Context Context;
+ typedef typename Traits::ReturnType GetterType;
+ typedef typename Traits::SetterType SetterType;
+
+ private:
+ static jfieldID sID;
+
+ static void BeginAccess(const Context& ctx) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+ static_assert(Traits::dispatchTarget == DispatchTarget::CURRENT,
+ "Dispatching not supported for field access");
+
+ if (sID) {
+ return;
+ }
+
+ if (Traits::isStatic) {
+ MOZ_ALWAYS_TRUE(
+ sID = AndroidBridge::GetStaticFieldID(
+ ctx.Env(), ctx.ClassRef(), Traits::name, Traits::signature));
+ } else {
+ MOZ_ALWAYS_TRUE(sID = AndroidBridge::GetFieldID(ctx.Env(), ctx.ClassRef(),
+ Traits::name,
+ Traits::signature));
+ }
+ }
+
+ static void EndAccess(const Context& ctx, nsresult* rv) {
+ return Base::EndAccess<Traits>(ctx, rv);
+ }
+
+ public:
+ static GetterType Get(const Context& ctx, nsresult* rv) {
+ JNIEnv* const env = ctx.Env();
+ BeginAccess(ctx);
+
+ auto result = TypeAdapter<GetterType>::ToNative(
+ env, Traits::isStatic
+ ?
+
+ (env->*TypeAdapter<GetterType>::StaticGet)(ctx.ClassRef(), sID)
+ :
+
+ (env->*TypeAdapter<GetterType>::Get)(ctx.Get(), sID));
+
+ EndAccess(ctx, rv);
+ return result;
+ }
+
+ static void Set(const Context& ctx, nsresult* rv, SetterType val) {
+ JNIEnv* const env = ctx.Env();
+ BeginAccess(ctx);
+
+ if (Traits::isStatic) {
+ (env->*TypeAdapter<SetterType>::StaticSet)(
+ ctx.ClassRef(), sID, TypeAdapter<SetterType>::FromNative(env, val));
+ } else {
+ (env->*TypeAdapter<SetterType>::Set)(
+ ctx.Get(), sID, TypeAdapter<SetterType>::FromNative(env, val));
+ }
+
+ EndAccess(ctx, rv);
+ }
+};
+
+// Define sID member.
+template <class T>
+jfieldID Field<T>::sID;
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_Accessors_h__
diff --git a/widget/android/jni/Conversions.cpp b/widget/android/jni/Conversions.cpp
new file mode 100644
index 0000000000..f7742b9535
--- /dev/null
+++ b/widget/android/jni/Conversions.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "Conversions.h"
+#include "JavaBuiltins.h"
+
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+
+namespace mozilla {
+namespace jni {
+
+template <class T>
+jfieldID GetValueFieldID(JNIEnv* aEnv, const char* aType) {
+ const jfieldID id = aEnv->GetFieldID(
+ typename T::Context(aEnv, nullptr).ClassRef(), "value", aType);
+ aEnv->ExceptionClear();
+ return id;
+}
+
+// Cached locations of the primitive types within their standard boxed objects
+// to skip doing that lookup on every get.
+static jfieldID gBooleanValueField;
+static jfieldID gIntValueField;
+static jfieldID gDoubleValueField;
+
+void InitConversionStatics() {
+ MOZ_ASSERT(NS_IsMainThread());
+ JNIEnv* const env = jni::GetGeckoThreadEnv();
+ gBooleanValueField = GetValueFieldID<java::sdk::Boolean>(env, "Z");
+ gIntValueField = GetValueFieldID<java::sdk::Integer>(env, "I");
+ gDoubleValueField = GetValueFieldID<java::sdk::Double>(env, "D");
+}
+
+template <>
+bool Java2Native(mozilla::jni::Object::Param aData, JNIEnv* aEnv) {
+ MOZ_ASSERT(aData.IsInstanceOf<jni::Boolean>());
+
+ bool result = false;
+ if (gBooleanValueField) {
+ if (!aEnv) {
+ aEnv = jni::GetEnvForThread();
+ }
+ result =
+ aEnv->GetBooleanField(aData.Get(), gBooleanValueField) != JNI_FALSE;
+ MOZ_CATCH_JNI_EXCEPTION(aEnv);
+ } else {
+ result = java::sdk::Boolean::Ref::From(aData)->BooleanValue();
+ }
+
+ return result;
+}
+
+template <>
+int Java2Native(mozilla::jni::Object::Param aData, JNIEnv* aEnv) {
+ MOZ_ASSERT(aData.IsInstanceOf<jni::Integer>());
+
+ int result = 0;
+ if (gIntValueField) {
+ if (!aEnv) {
+ aEnv = jni::GetEnvForThread();
+ }
+ result = aEnv->GetIntField(aData.Get(), gIntValueField);
+ MOZ_CATCH_JNI_EXCEPTION(aEnv);
+ } else {
+ result = java::sdk::Number::Ref::From(aData)->IntValue();
+ }
+
+ return result;
+}
+
+template <>
+double Java2Native(mozilla::jni::Object::Param aData, JNIEnv* aEnv) {
+ MOZ_ASSERT(aData.IsInstanceOf<jni::Double>());
+
+ double result = 0;
+ if (gDoubleValueField) {
+ if (!aEnv) {
+ aEnv = jni::GetEnvForThread();
+ }
+ result = aEnv->GetDoubleField(aData.Get(), gDoubleValueField);
+ MOZ_CATCH_JNI_EXCEPTION(aEnv);
+ } else {
+ result = java::sdk::Number::Ref::From(aData)->DoubleValue();
+ }
+
+ return result;
+}
+
+template <>
+ipc::LaunchError Java2Native(mozilla::jni::Object::Param aData, JNIEnv* aEnv) {
+ // Bug 1819311: there is not much we can really catch due to how Android
+ // services are started, so for now we just expose it this way.
+ return ipc::LaunchError("Java2Native");
+}
+
+template <>
+nsString Java2Native(mozilla::jni::Object::Param aData, JNIEnv* aEnv) {
+ nsString result;
+ if (aData != NULL && aData.IsInstanceOf<jni::String>()) {
+ result = jni::String::Ref::From(aData)->ToString();
+ }
+ return result;
+}
+
+template <>
+nsresult Java2Native(mozilla::jni::Object::Param aData, JNIEnv* aEnv) {
+ MOZ_ASSERT(aData.IsInstanceOf<jni::Throwable>());
+ return NS_ERROR_FAILURE;
+}
+
+} // namespace jni
+} // namespace mozilla
diff --git a/widget/android/jni/Conversions.h b/widget/android/jni/Conversions.h
new file mode 100644
index 0000000000..1d9e20acc7
--- /dev/null
+++ b/widget/android/jni/Conversions.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_Conversions_h__
+#define mozilla_jni_Conversions_h__
+
+#include "mozilla/jni/Refs.h"
+
+namespace mozilla {
+namespace jni {
+
+template <typename ArgType>
+ArgType Java2Native(mozilla::jni::Object::Param, JNIEnv* aEnv = nullptr);
+
+void InitConversionStatics();
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_Conversions_h__
diff --git a/widget/android/jni/GeckoBundleUtils.cpp b/widget/android/jni/GeckoBundleUtils.cpp
new file mode 100644
index 0000000000..310f284093
--- /dev/null
+++ b/widget/android/jni/GeckoBundleUtils.cpp
@@ -0,0 +1,309 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/jni/GeckoBundleUtils.h"
+
+#include "JavaBuiltins.h"
+#include "js/Warnings.h"
+#include "nsJSUtils.h"
+
+#include "js/Array.h"
+#include "js/experimental/TypedData.h"
+
+namespace mozilla::jni {
+namespace detail {
+bool CheckJS(JSContext* aCx, bool aResult) {
+ if (!aResult) {
+ JS_ClearPendingException(aCx);
+ }
+ return aResult;
+}
+
+nsresult BoxString(JSContext* aCx, JS::Handle<JS::Value> aData,
+ jni::Object::LocalRef& aOut) {
+ if (aData.isNullOrUndefined()) {
+ aOut = nullptr;
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aData.isString());
+
+ JS::Rooted<JSString*> str(aCx, aData.toString());
+
+ if (JS::StringHasLatin1Chars(str)) {
+ nsAutoJSString autoStr;
+ NS_ENSURE_TRUE(CheckJS(aCx, autoStr.init(aCx, str)), NS_ERROR_FAILURE);
+
+ // StringParam can automatically convert a nsString to jstring.
+ aOut = jni::StringParam(autoStr, aOut.Env(), fallible);
+ if (!aOut) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+ }
+
+ // Two-byte string
+ JNIEnv* const env = aOut.Env();
+ const char16_t* chars;
+ {
+ JS::AutoCheckCannotGC nogc;
+ size_t len = 0;
+ chars = JS_GetTwoByteStringCharsAndLength(aCx, nogc, str, &len);
+ if (chars) {
+ aOut = jni::String::LocalRef::Adopt(
+ env, env->NewString(reinterpret_cast<const jchar*>(chars), len));
+ }
+ }
+ if (NS_WARN_IF(!CheckJS(aCx, !!chars) || !aOut)) {
+ env->ExceptionClear();
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+nsresult BoxObject(JSContext* aCx, JS::Handle<JS::Value> aData,
+ jni::Object::LocalRef& aOut);
+
+template <typename Type, bool (JS::Value::*IsType)() const,
+ Type (JS::Value::*ToType)() const, class ArrayType,
+ typename ArrayType::LocalRef (*NewArray)(const Type*, size_t)>
+nsresult BoxArrayPrimitive(JSContext* aCx, JS::Handle<JSObject*> aData,
+ jni::Object::LocalRef& aOut, size_t aLength,
+ JS::Handle<JS::Value> aElement) {
+ JS::Rooted<JS::Value> element(aCx);
+ auto data = MakeUnique<Type[]>(aLength);
+ data[0] = (aElement.get().*ToType)();
+
+ for (size_t i = 1; i < aLength; i++) {
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, aData, i, &element)),
+ NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE((element.get().*IsType)(), NS_ERROR_INVALID_ARG);
+
+ data[i] = (element.get().*ToType)();
+ }
+ aOut = (*NewArray)(data.get(), aLength);
+ return NS_OK;
+}
+
+nsresult BoxByteArray(JSContext* aCx, JS::Handle<JSObject*> aData,
+ jni::Object::LocalRef& aOut) {
+ JS::AutoCheckCannotGC nogc;
+ bool isShared = false;
+ const void* data = JS_GetArrayBufferViewData(aData, &isShared, nogc);
+ size_t length = JS_GetArrayBufferViewByteLength(aData);
+
+ aOut = jni::ByteArray::New(reinterpret_cast<const int8_t*>(data), length);
+ if (!aOut) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+template <class Type,
+ nsresult (*Box)(JSContext*, JS::Handle<JS::Value>,
+ jni::Object::LocalRef&),
+ typename IsType>
+nsresult BoxArrayObject(JSContext* aCx, JS::Handle<JSObject*> aData,
+ jni::Object::LocalRef& aOut, size_t aLength,
+ JS::Handle<JS::Value> aElement, IsType&& aIsType) {
+ auto out = jni::ObjectArray::New<Type>(aLength);
+ JS::Rooted<JS::Value> element(aCx);
+ jni::Object::LocalRef jniElement(aOut.Env());
+
+ nsresult rv = (*Box)(aCx, aElement, jniElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ out->SetElement(0, jniElement);
+
+ for (size_t i = 1; i < aLength; i++) {
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, aData, i, &element)),
+ NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(element.isNullOrUndefined() || aIsType(element),
+ NS_ERROR_INVALID_ARG);
+
+ rv = (*Box)(aCx, element, jniElement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ out->SetElement(i, jniElement);
+ }
+ aOut = out;
+ return NS_OK;
+}
+
+nsresult BoxArray(JSContext* aCx, JS::Handle<JSObject*> aData,
+ jni::Object::LocalRef& aOut) {
+ uint32_t length = 0;
+ NS_ENSURE_TRUE(CheckJS(aCx, JS::GetArrayLength(aCx, aData, &length)),
+ NS_ERROR_FAILURE);
+
+ if (!length) {
+ // Always represent empty arrays as an empty boolean array.
+ aOut = java::GeckoBundle::EMPTY_BOOLEAN_ARRAY();
+ return NS_OK;
+ }
+
+ // We only check the first element's type. If the array has mixed types,
+ // we'll throw an error during actual conversion.
+ JS::Rooted<JS::Value> element(aCx);
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, aData, 0, &element)),
+ NS_ERROR_FAILURE);
+
+ if (element.isBoolean()) {
+ return BoxArrayPrimitive<bool, &JS::Value::isBoolean, &JS::Value::toBoolean,
+ jni::BooleanArray, &jni::BooleanArray::New>(
+ aCx, aData, aOut, length, element);
+ }
+
+ if (element.isInt32()) {
+ nsresult rv =
+ BoxArrayPrimitive<int32_t, &JS::Value::isInt32, &JS::Value::toInt32,
+ jni::IntArray, &jni::IntArray::New>(aCx, aData, aOut,
+ length, element);
+ if (rv != NS_ERROR_INVALID_ARG) {
+ return rv;
+ }
+ // Not int32, but we can still try a double array.
+ }
+
+ if (element.isNumber()) {
+ return BoxArrayPrimitive<double, &JS::Value::isNumber, &JS::Value::toNumber,
+ jni::DoubleArray, &jni::DoubleArray::New>(
+ aCx, aData, aOut, length, element);
+ }
+
+ if (element.isNullOrUndefined() || element.isString()) {
+ const auto isString = [](JS::Handle<JS::Value> val) -> bool {
+ return val.isString();
+ };
+ nsresult rv = BoxArrayObject<jni::String, &BoxString>(
+ aCx, aData, aOut, length, element, isString);
+ if (element.isString() || rv != NS_ERROR_INVALID_ARG) {
+ return rv;
+ }
+ // First element was null/undefined, so it may still be an object array.
+ }
+
+ const auto isObject = [aCx](JS::Handle<JS::Value> val) -> bool {
+ if (!val.isObject()) {
+ return false;
+ }
+ bool array = false;
+ JS::Rooted<JSObject*> obj(aCx, &val.toObject());
+ // We don't support array of arrays.
+ return CheckJS(aCx, JS::IsArrayObject(aCx, obj, &array)) && !array;
+ };
+
+ if (element.isNullOrUndefined() || isObject(element)) {
+ return BoxArrayObject<java::GeckoBundle, &BoxObject>(
+ aCx, aData, aOut, length, element, isObject);
+ }
+
+ NS_WARNING("Unknown type");
+ return NS_ERROR_INVALID_ARG;
+}
+
+nsresult BoxValue(JSContext* aCx, JS::Handle<JS::Value> aData,
+ jni::Object::LocalRef& aOut);
+
+nsresult BoxObject(JSContext* aCx, JS::Handle<JS::Value> aData,
+ jni::Object::LocalRef& aOut) {
+ if (aData.isNullOrUndefined()) {
+ aOut = nullptr;
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aData.isObject());
+
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ JS::Rooted<JSObject*> obj(aCx, &aData.toObject());
+
+ bool isArray = false;
+ if (CheckJS(aCx, JS::IsArrayObject(aCx, obj, &isArray)) && isArray) {
+ return BoxArray(aCx, obj, aOut);
+ }
+
+ if (JS_IsTypedArrayObject(obj)) {
+ return BoxByteArray(aCx, obj, aOut);
+ }
+
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_Enumerate(aCx, obj, &ids)), NS_ERROR_FAILURE);
+
+ const size_t length = ids.length();
+ auto keys = jni::ObjectArray::New<jni::String>(length);
+ auto values = jni::ObjectArray::New<jni::Object>(length);
+
+ // Iterate through each property of the JS object.
+ for (size_t i = 0; i < ids.length(); i++) {
+ const JS::RootedId id(aCx, ids[i]);
+ JS::Rooted<JS::Value> idVal(aCx);
+ JS::Rooted<JS::Value> val(aCx);
+ jni::Object::LocalRef key(aOut.Env());
+ jni::Object::LocalRef value(aOut.Env());
+
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_IdToValue(aCx, id, &idVal)),
+ NS_ERROR_FAILURE);
+
+ JS::Rooted<JSString*> idStr(aCx, JS::ToString(aCx, idVal));
+ NS_ENSURE_TRUE(CheckJS(aCx, !!idStr), NS_ERROR_FAILURE);
+
+ idVal.setString(idStr);
+ NS_ENSURE_SUCCESS(BoxString(aCx, idVal, key), NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(CheckJS(aCx, JS_GetPropertyById(aCx, obj, id, &val)),
+ NS_ERROR_FAILURE);
+
+ nsresult rv = BoxValue(aCx, val, value);
+ if (rv == NS_ERROR_INVALID_ARG && !JS_IsExceptionPending(aCx)) {
+ nsAutoJSString autoStr;
+ if (CheckJS(aCx, autoStr.init(aCx, idVal.toString()))) {
+ JS_ReportErrorUTF8(aCx, "Invalid event data property %s",
+ NS_ConvertUTF16toUTF8(autoStr).get());
+ }
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ keys->SetElement(i, key);
+ values->SetElement(i, value);
+ }
+
+ aOut = java::GeckoBundle::New(keys, values);
+ return NS_OK;
+}
+
+nsresult BoxValue(JSContext* aCx, JS::Handle<JS::Value> aData,
+ jni::Object::LocalRef& aOut) {
+ if (aData.isNullOrUndefined()) {
+ aOut = nullptr;
+ } else if (aData.isBoolean()) {
+ aOut = aData.toBoolean() ? java::sdk::Boolean::TRUE()
+ : java::sdk::Boolean::FALSE();
+ } else if (aData.isInt32()) {
+ aOut = java::sdk::Integer::ValueOf(aData.toInt32());
+ } else if (aData.isNumber()) {
+ aOut = java::sdk::Double::New(aData.toNumber());
+ } else if (aData.isString()) {
+ return BoxString(aCx, aData, aOut);
+ } else if (aData.isObject()) {
+ return BoxObject(aCx, aData, aOut);
+ } else {
+ NS_WARNING("Unknown type");
+ return NS_ERROR_INVALID_ARG;
+ }
+ return NS_OK;
+}
+
+} // namespace detail
+
+nsresult BoxData(JSContext* aCx, JS::Handle<JS::Value> aData,
+ jni::Object::LocalRef& aOut, bool aObjectOnly) {
+ nsresult rv = NS_ERROR_INVALID_ARG;
+
+ if (!aObjectOnly) {
+ rv = detail::BoxValue(aCx, aData, aOut);
+ } else if (aData.isObject() || aData.isNullOrUndefined()) {
+ rv = detail::BoxObject(aCx, aData, aOut);
+ }
+
+ return rv;
+}
+} // namespace mozilla::jni
diff --git a/widget/android/jni/GeckoBundleUtils.h b/widget/android/jni/GeckoBundleUtils.h
new file mode 100644
index 0000000000..a92a27abc3
--- /dev/null
+++ b/widget/android/jni/GeckoBundleUtils.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_GeckoBundleUtils_h
+#define mozilla_jni_GeckoBundleUtils_h
+
+#include "mozilla/java/GeckoBundleWrappers.h"
+
+#include "jsapi.h"
+
+namespace mozilla {
+namespace jni {
+
+#define GECKOBUNDLE_START(name) \
+ nsTArray<jni::String::LocalRef> _##name##_keys; \
+ nsTArray<jni::Object::LocalRef> _##name##_values;
+
+#define GECKOBUNDLE_PUT(name, key, value) \
+ _##name##_keys.AppendElement( \
+ jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING(key))); \
+ _##name##_values.AppendElement(value);
+
+#define GECKOBUNDLE_FINISH(name) \
+ MOZ_ASSERT(_##name##_keys.Length() == _##name##_values.Length()); \
+ auto _##name##_jkeys = \
+ jni::ObjectArray::New<jni::String>(_##name##_keys.Length()); \
+ auto _##name##_jvalues = \
+ jni::ObjectArray::New<jni::Object>(_##name##_values.Length()); \
+ for (size_t i = 0; \
+ i < _##name##_keys.Length() && i < _##name##_values.Length(); i++) { \
+ _##name##_jkeys->SetElement(i, _##name##_keys.ElementAt(i)); \
+ _##name##_jvalues->SetElement(i, _##name##_values.ElementAt(i)); \
+ } \
+ auto name = \
+ mozilla::java::GeckoBundle::New(_##name##_jkeys, _##name##_jvalues);
+
+nsresult BoxData(JSContext* aCx, JS::Handle<JS::Value> aData,
+ jni::Object::LocalRef& aOut, bool aObjectOnly);
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_GeckoBundleUtils_h
diff --git a/widget/android/jni/GeckoResultUtils.h b/widget/android/jni/GeckoResultUtils.h
new file mode 100644
index 0000000000..da103f35ee
--- /dev/null
+++ b/widget/android/jni/GeckoResultUtils.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_GeckoResultUtils_h
+#define mozilla_jni_GeckoResultUtils_h
+
+#include "mozilla/java/GeckoResultNatives.h"
+#include "mozilla/jni/Conversions.h"
+
+namespace mozilla {
+namespace jni {
+
+// C++-side object bound to Java's GeckoResult.GeckoCallback.
+//
+// Note that we can't template this class because that breaks JNI dispatch
+// (surprisingly: it compiles, but selects the wrong method specialization
+// during dispatch). So instead we use a templated factory function, which
+// bundles the per-ArgType conversion logic into the callback.
+class GeckoResultCallback final
+ : public java::GeckoResult::GeckoCallback::Natives<GeckoResultCallback> {
+ public:
+ typedef java::GeckoResult::GeckoCallback::Natives<GeckoResultCallback> Base;
+ typedef std::function<void(mozilla::jni::Object::Param)> OuterCallback;
+
+ void Call(mozilla::jni::Object::Param aArg) { mCallback(aArg); }
+
+ template <typename ArgType>
+ static java::GeckoResult::GeckoCallback::LocalRef CreateAndAttach(
+ std::function<void(ArgType)>&& aInnerCallback) {
+ auto java = java::GeckoResult::GeckoCallback::New();
+ OuterCallback outerCallback =
+ [inner{std::move(aInnerCallback)}](mozilla::jni::Object::Param aParam) {
+ ArgType converted = Java2Native<ArgType>(aParam);
+ inner(std::move(converted));
+ };
+ auto native = MakeUnique<GeckoResultCallback>(std::move(outerCallback));
+ Base::AttachNative(java, std::move(native));
+ return java;
+ }
+
+ explicit GeckoResultCallback(OuterCallback&& aCallback)
+ : mCallback(std::move(aCallback)) {}
+
+ private:
+ OuterCallback mCallback;
+};
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_GeckoResultUtils_h
diff --git a/widget/android/jni/Natives.h b/widget/android/jni/Natives.h
new file mode 100644
index 0000000000..45b351ba21
--- /dev/null
+++ b/widget/android/jni/Natives.h
@@ -0,0 +1,1540 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_Natives_h__
+#define mozilla_jni_Natives_h__
+
+#include <jni.h>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/RWLock.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/jni/Accessors.h"
+#include "mozilla/jni/Refs.h"
+#include "mozilla/jni/Types.h"
+#include "mozilla/jni/Utils.h"
+#include "nsThreadUtils.h"
+
+#if defined(_MSC_VER) // MSVC
+# define FUNCTION_SIGNATURE __FUNCSIG__
+#elif defined(__GNUC__) // GCC, Clang
+# define FUNCTION_SIGNATURE __PRETTY_FUNCTION__
+#endif
+
+struct NativeException {
+ const char* str;
+};
+
+template <class T>
+static NativeException NullHandle() {
+ return {FUNCTION_SIGNATURE};
+}
+
+template <class T>
+static NativeException NullWeakPtr() {
+ return {FUNCTION_SIGNATURE};
+}
+
+namespace mozilla {
+
+template <typename ResolveValueT, typename RejectValueT, bool IsExclusive>
+class MozPromise;
+
+namespace jni {
+
+/**
+ * C++ classes implementing instance (non-static) native methods can choose
+ * from one of two ownership models, when associating a C++ object with a Java
+ * instance.
+ *
+ * * If the C++ class inherits from mozilla::SupportsWeakPtr, weak pointers
+ * will be used. The Java instance will store and own the pointer to a
+ * WeakPtr object. The C++ class itself is otherwise not owned or directly
+ * referenced. Note that mozilla::SupportsWeakPtr only supports being used on
+ * a single thread. To attach a Java instance to a C++ instance, pass in a
+ * mozilla::SupportsWeakPtr pointer to the C++ class (i.e. MyClass*).
+ *
+ * class MyClass : public SupportsWeakPtr
+ * , public MyJavaClass::Natives<MyClass>
+ * {
+ * // ...
+ *
+ * public:
+ * using MyJavaClass::Natives<MyClass>::DisposeNative;
+ *
+ * void AttachTo(const MyJavaClass::LocalRef& instance)
+ * {
+ * MyJavaClass::Natives<MyClass>::AttachNative(
+ * instance, static_cast<SupportsWeakPtr*>(this));
+ *
+ * // "instance" does NOT own "this", so the C++ object
+ * // lifetime is separate from the Java object lifetime.
+ * }
+ * };
+ *
+ * * If the C++ class contains public members AddRef() and Release(), the Java
+ * instance will store and own the pointer to a RefPtr object, which holds a
+ * strong reference on the C++ instance. Normal ref-counting considerations
+ * apply in this case; for example, disposing may cause the C++ instance to
+ * be deleted and the destructor to be run on the current thread, which may
+ * not be desirable. To attach a Java instance to a C++ instance, pass in a
+ * pointer to the C++ class (i.e. MyClass*).
+ *
+ * class MyClass : public RefCounted<MyClass>
+ * , public MyJavaClass::Natives<MyClass>
+ * {
+ * // ...
+ *
+ * public:
+ * using MyJavaClass::Natives<MyClass>::DisposeNative;
+ *
+ * void AttachTo(const MyJavaClass::LocalRef& instance)
+ * {
+ * MyJavaClass::Natives<MyClass>::AttachNative(instance, this);
+ *
+ * // "instance" owns "this" through the RefPtr, so the C++ object
+ * // may be destroyed as soon as instance.disposeNative() is called.
+ * }
+ * };
+ *
+ * * In other cases, the Java instance will store and own a pointer to the C++
+ * object itself. This pointer must not be stored or deleted elsewhere. To
+ * attach a Java instance to a C++ instance, pass in a reference to a
+ * UniquePtr of the C++ class (i.e. UniquePtr<MyClass>).
+ *
+ * class MyClass : public MyJavaClass::Natives<MyClass>
+ * {
+ * // ...
+ *
+ * public:
+ * using MyJavaClass::Natives<MyClass>::DisposeNative;
+ *
+ * static void AttachTo(const MyJavaClass::LocalRef& instance)
+ * {
+ * MyJavaClass::Natives<MyClass>::AttachNative(
+ * instance, mozilla::MakeUnique<MyClass>());
+ *
+ * // "instance" owns the newly created C++ object, so the C++
+ * // object is destroyed as soon as instance.disposeNative() is
+ * // called.
+ * }
+ * };
+ */
+
+namespace detail {
+
+/**
+ * Type trait that determines whether a given class has a member named
+ * T::OnWeakNonIntrusiveDetach.
+ *
+ * Example usage:
+ * class Foo {};
+ * class Bar {
+ * public:
+ * void OnWeakNonIntrusiveDetach(already_AddRefed<nsIRunnable> aRunnable);
+ * };
+ *
+ * constexpr bool foo = HasWeakNonIntrusiveDetach<Foo>::value; // Expect false
+ * constexpr bool bar = HasWeakNonIntrusiveDetach<Bar>::value; // Expect true
+ */
+template <typename, typename = std::void_t<>>
+struct HasWeakNonIntrusiveDetach : std::false_type {};
+
+template <typename T>
+struct HasWeakNonIntrusiveDetach<
+ T, std::void_t<decltype(std::declval<T>().OnWeakNonIntrusiveDetach(
+ std::declval<already_AddRefed<nsIRunnable>>()))>> : std::true_type {
+};
+
+/**
+ * Type trait that determines whether a given class is refcounted, ie. it has
+ * both T::AddRef and T::Release methods.
+ *
+ * Example usage:
+ * class Foo {};
+ * class Bar {
+ * public:
+ * void AddRef();
+ * void Release();
+ * };
+ *
+ * constexpr bool foo = IsRefCounted<Foo>::value; // Expect false
+ * constexpr bool bar = IsRefCounted<Bar>::value; // Expect true
+ */
+template <typename, typename = std::void_t<>>
+struct IsRefCounted : std::false_type {};
+
+template <typename T>
+struct IsRefCounted<T, std::void_t<decltype(std::declval<T>().AddRef(),
+ std::declval<T>().Release())>>
+ : std::true_type {};
+
+/**
+ * This enum is used for classifying the type of pointer that is stored
+ * within a NativeWeakPtr. This classification is different from the one used
+ * for normal native pointers.
+ */
+enum class NativePtrInternalType : size_t {
+ OWNING = 1,
+ WEAK = 2,
+ REFPTR = 3,
+};
+
+/**
+ * NativePtrInternalPicker uses some C++ SFINAE template-fu to figure out
+ * what type of pointer the class specified by Impl needs to be.
+ *
+ * It does this by supplying multiple overloads of a method named Test.
+ * Various overloads are enabled or disabled depending on whether or not Impl
+ * can possibly support them.
+ *
+ * Each overload "returns" a reference to an array whose size corresponds to the
+ * value of each enum in NativePtrInternalType. That size is then converted back
+ * to the enum value, yielding the right type.
+ */
+template <class Impl>
+class NativePtrInternalPicker {
+ // Enable if Impl derives from SupportsWeakPtr, yielding type WEAK
+ template <class I>
+ static std::enable_if_t<
+ std::is_base_of<SupportsWeakPtr, I>::value,
+ char (&)[static_cast<size_t>(NativePtrInternalType::WEAK)]>
+ Test(char);
+
+ // Enable if Impl implements AddRef and Release, yielding type REFPTR
+ template <class I, typename = decltype(&I::AddRef, &I::Release)>
+ static char (&Test(int))[static_cast<size_t>(NativePtrInternalType::REFPTR)];
+
+ // This overload uses '...' as its param to make its arguments less specific;
+ // the compiler prefers more-specific overloads to less-specific ones.
+ // OWNING is the fallback type.
+ template <class>
+ static char (&Test(...))[static_cast<size_t>(NativePtrInternalType::OWNING)];
+
+ public:
+ // Given a hypothetical function call Test<Impl>, convert the size of its
+ // resulting array back into a NativePtrInternalType enum value.
+ static const NativePtrInternalType value = static_cast<NativePtrInternalType>(
+ sizeof(Test<Impl>('\0')) / sizeof(char));
+};
+
+/**
+ * This enum is used for classifying the type of pointer that is stored in a
+ * JNIObject's handle.
+ *
+ * We have two different weak pointer types:
+ * * WEAK_INTRUSIVE is a pointer to a class that derives from
+ * mozilla::SupportsWeakPtr.
+ * * WEAK_NON_INTRUSIVE is a pointer to a class that does not have any
+ * internal support for weak pointers, but does supply a
+ * OnWeakNonIntrusiveDetach method.
+ */
+enum class NativePtrType : size_t {
+ OWNING = 1,
+ WEAK_INTRUSIVE = 2,
+ WEAK_NON_INTRUSIVE = 3,
+ REFPTR = 4,
+};
+
+/**
+ * NativePtrPicker uses some C++ SFINAE template-fu to figure out what type of
+ * pointer the class specified by Impl needs to be.
+ *
+ * It does this by supplying multiple overloads of a method named Test.
+ * Various overloads are enabled or disabled depending on whether or not Impl
+ * can possibly support them.
+ *
+ * Each overload "returns" a reference to an array whose size corresponds to the
+ * value of each enum in NativePtrInternalType. That size is then converted back
+ * to the enum value, yielding the right type.
+ */
+template <class Impl>
+class NativePtrPicker {
+ // Just shorthand for each overload's return type
+ template <NativePtrType PtrType>
+ using ResultTypeT = char (&)[static_cast<size_t>(PtrType)];
+
+ // Enable if Impl derives from SupportsWeakPtr, yielding type WEAK_INTRUSIVE
+ template <typename I>
+ static auto Test(void*)
+ -> std::enable_if_t<std::is_base_of<SupportsWeakPtr, I>::value,
+ ResultTypeT<NativePtrType::WEAK_INTRUSIVE>>;
+
+ // Enable if Impl implements OnWeakNonIntrusiveDetach, yielding type
+ // WEAK_NON_INTRUSIVE
+ template <typename I>
+ static auto Test(void*)
+ -> std::enable_if_t<HasWeakNonIntrusiveDetach<I>::value,
+ ResultTypeT<NativePtrType::WEAK_NON_INTRUSIVE>>;
+
+ // We want the WEAK_NON_INTRUSIVE overload to take precedence over this one,
+ // so we only enable this overload if Impl is refcounted AND it does not
+ // implement OnWeakNonIntrusiveDetach. Yields type REFPTR.
+ template <typename I>
+ static auto Test(void*) -> std::enable_if_t<
+ std::conjunction_v<IsRefCounted<I>,
+ std::negation<HasWeakNonIntrusiveDetach<I>>>,
+ ResultTypeT<NativePtrType::REFPTR>>;
+
+ // This overload uses '...' as its param to make its arguments less specific;
+ // the compiler prefers more-specific overloads to less-specific ones.
+ // OWNING is the fallback type.
+ template <typename>
+ static char (&Test(...))[static_cast<size_t>(NativePtrType::OWNING)];
+
+ public:
+ // Given a hypothetical function call Test<Impl>, convert the size of its
+ // resulting array back into a NativePtrType enum value.
+ static const NativePtrType value =
+ static_cast<NativePtrType>(sizeof(Test<Impl>(nullptr)));
+};
+
+template <class Impl>
+inline uintptr_t CheckNativeHandle(JNIEnv* env, uintptr_t handle) {
+ if (!handle) {
+ if (!env->ExceptionCheck()) {
+ ThrowException(env, "java/lang/NullPointerException",
+ NullHandle<Impl>().str);
+ }
+ return 0;
+ }
+ return handle;
+}
+
+/**
+ * This struct is used to describe various traits of a native pointer of type
+ * Impl that will be attached to a JNIObject.
+ *
+ * See the definition of the NativePtrType::OWNING specialization for comments
+ * describing the required fields.
+ */
+template <class Impl, NativePtrType Type = NativePtrPicker<Impl>::value>
+struct NativePtrTraits;
+
+template <class Impl>
+struct NativePtrTraits<Impl, /* Type = */ NativePtrType::OWNING> {
+ using AccessorType =
+ Impl*; // Pointer-like type returned by Access() (an actual pointer in
+ // this case, but this is not strictly necessary)
+ using HandleType = Impl*; // Type of the pointer stored in JNIObject.mHandle
+ using RefType = Impl*; // Type of the pointer returned by Get()
+
+ /**
+ * Returns a RefType to the native implementation belonging to
+ * the given Java object.
+ */
+ static RefType Get(JNIEnv* env, jobject instance) {
+ static_assert(
+ std::is_same<HandleType, RefType>::value,
+ "HandleType and RefType must be identical for owning pointers");
+ return reinterpret_cast<HandleType>(
+ CheckNativeHandle<Impl>(env, GetNativeHandle(env, instance)));
+ }
+
+ /**
+ * Returns a RefType to the native implementation belonging to
+ * the given Java object.
+ */
+ template <class LocalRef>
+ static RefType Get(const LocalRef& instance) {
+ return Get(instance.Env(), instance.Get());
+ }
+
+ /**
+ * Given a RefType, returns the pointer-like AccessorType used for
+ * manipulating the native object.
+ */
+ static AccessorType Access(RefType aImpl, JNIEnv* aEnv = nullptr) {
+ static_assert(
+ std::is_same<AccessorType, RefType>::value,
+ "AccessorType and RefType must be identical for owning pointers");
+ return aImpl;
+ }
+
+ /**
+ * Set the JNIObject's handle to the provided pointer, clearing any previous
+ * handle if necessary.
+ */
+ template <class LocalRef>
+ static void Set(const LocalRef& instance, UniquePtr<Impl>&& ptr) {
+ Clear(instance);
+ SetNativeHandle(instance.Env(), instance.Get(),
+ reinterpret_cast<uintptr_t>(ptr.release()));
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+ }
+
+ /**
+ * Clear the JNIObject's handle.
+ */
+ template <class LocalRef>
+ static void Clear(const LocalRef& instance) {
+ UniquePtr<Impl> ptr(reinterpret_cast<RefType>(
+ GetNativeHandle(instance.Env(), instance.Get())));
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+
+ if (ptr) {
+ SetNativeHandle(instance.Env(), instance.Get(), 0);
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+ }
+ }
+};
+
+template <class Impl>
+struct NativePtrTraits<Impl, /* Type = */ NativePtrType::WEAK_INTRUSIVE> {
+ using AccessorType = Impl*;
+ using HandleType = WeakPtr<Impl>*;
+ using RefType = WeakPtr<Impl>;
+
+ static RefType Get(JNIEnv* env, jobject instance) {
+ const auto ptr = reinterpret_cast<HandleType>(
+ CheckNativeHandle<Impl>(env, GetNativeHandle(env, instance)));
+ return *ptr;
+ }
+
+ template <class LocalRef>
+ static RefType Get(const LocalRef& instance) {
+ return Get(instance.Env(), instance.Get());
+ }
+
+ static AccessorType Access(RefType aPtr, JNIEnv* aEnv = nullptr) {
+ AccessorType const impl = *aPtr;
+ if (!impl) {
+ JNIEnv* env = aEnv ? aEnv : mozilla::jni::GetEnvForThread();
+ ThrowException(env, "java/lang/NullPointerException",
+ NullWeakPtr<Impl>().str);
+ }
+
+ return impl;
+ }
+
+ template <class LocalRef>
+ static void Set(const LocalRef& instance, Impl* ptr) {
+ // Create the new handle first before clearing any old handle, so the
+ // new handle is guaranteed to have different value than any old handle.
+ const uintptr_t handle =
+ reinterpret_cast<uintptr_t>(new WeakPtr<Impl>(ptr));
+ Clear(instance);
+ SetNativeHandle(instance.Env(), instance.Get(), handle);
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+ }
+
+ template <class LocalRef>
+ static void Clear(const LocalRef& instance) {
+ const auto ptr = reinterpret_cast<HandleType>(
+ GetNativeHandle(instance.Env(), instance.Get()));
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+
+ if (ptr) {
+ SetNativeHandle(instance.Env(), instance.Get(), 0);
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+ delete ptr;
+ }
+ }
+};
+
+template <class Impl>
+struct NativePtrTraits<Impl, /* Type = */ NativePtrType::REFPTR> {
+ using AccessorType = Impl*;
+ using HandleType = RefPtr<Impl>*;
+ using RefType = Impl*;
+
+ static RefType Get(JNIEnv* env, jobject instance) {
+ const auto ptr = reinterpret_cast<HandleType>(
+ CheckNativeHandle<Impl>(env, GetNativeHandle(env, instance)));
+ if (!ptr) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(*ptr);
+ return *ptr;
+ }
+
+ template <class LocalRef>
+ static RefType Get(const LocalRef& instance) {
+ return Get(instance.Env(), instance.Get());
+ }
+
+ static AccessorType Access(RefType aImpl, JNIEnv* aEnv = nullptr) {
+ static_assert(std::is_same<AccessorType, RefType>::value,
+ "AccessorType and RefType must be identical for refpointers");
+ return aImpl;
+ }
+
+ template <class LocalRef>
+ static void Set(const LocalRef& instance, RefType ptr) {
+ // Create the new handle first before clearing any old handle, so the
+ // new handle is guaranteed to have different value than any old handle.
+ const uintptr_t handle = reinterpret_cast<uintptr_t>(new RefPtr<Impl>(ptr));
+ Clear(instance);
+ SetNativeHandle(instance.Env(), instance.Get(), handle);
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+ }
+
+ template <class LocalRef>
+ static void Clear(const LocalRef& instance) {
+ const auto ptr = reinterpret_cast<HandleType>(
+ GetNativeHandle(instance.Env(), instance.Get()));
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+
+ if (ptr) {
+ SetNativeHandle(instance.Env(), instance.Get(), 0);
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+ delete ptr;
+ }
+ }
+};
+
+} // namespace detail
+
+// Forward declarations
+template <typename NativeImpl>
+class NativeWeakPtr;
+template <typename NativeImpl>
+class NativeWeakPtrHolder;
+
+namespace detail {
+
+/**
+ * Given the class of a native implementation, as well as its
+ * NativePtrInternalType, resolve traits for that type that will be used by
+ * the NativeWeakPtrControlBlock.
+ *
+ * Note that we only implement specializations for OWNING and REFPTR types,
+ * as a WEAK_INTRUSIVE type should not be using NativeWeakPtr anyway. The build
+ * will fail if such an attempt is made.
+ *
+ * Traits need to implement two things:
+ * 1. A |Type| field that resolves to a pointer type to be stored in the
+ * JNIObject's handle. It is assumed that setting a |Type| object to nullptr
+ * is sufficient to delete the underlying object.
+ * 2. A static |AsRaw| method that converts a pointer of |Type| into a raw
+ * pointer.
+ */
+template <
+ typename NativeImpl,
+ NativePtrInternalType PtrType =
+ ::mozilla::jni::detail::NativePtrInternalPicker<NativeImpl>::value>
+struct NativeWeakPtrControlBlockStorageTraits;
+
+template <typename NativeImpl>
+struct NativeWeakPtrControlBlockStorageTraits<
+ NativeImpl, ::mozilla::jni::detail::NativePtrInternalType::OWNING> {
+ using Type = UniquePtr<NativeImpl>;
+
+ static NativeImpl* AsRaw(const Type& aStorage) { return aStorage.get(); }
+};
+
+template <typename NativeImpl>
+struct NativeWeakPtrControlBlockStorageTraits<
+ NativeImpl, ::mozilla::jni::detail::NativePtrInternalType::REFPTR> {
+ using Type = RefPtr<NativeImpl>;
+
+ static NativeImpl* AsRaw(const Type& aStorage) { return aStorage.get(); }
+};
+
+// Forward Declaration
+template <typename NativeImpl>
+class Accessor;
+
+/**
+ * This class contains the shared data that is referenced by all NativeWeakPtr
+ * objects that reference the same object.
+ *
+ * It retains a WeakRef to the Java object that owns this native object.
+ * It uses a RWLock to control access to the native pointer itself.
+ * Read locks are used when accessing the pointer (even when calling non-const
+ * methods on the native object).
+ * A write lock is only used when it is time to destroy the native object and
+ * we need to clear the value of mNativeImpl.
+ */
+template <typename NativeImpl>
+class MOZ_HEAP_CLASS NativeWeakPtrControlBlock final {
+ public:
+ using StorageTraits = NativeWeakPtrControlBlockStorageTraits<NativeImpl>;
+ using StorageType = typename StorageTraits::Type;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NativeWeakPtrControlBlock)
+
+ NativeWeakPtrControlBlock(const NativeWeakPtrControlBlock&) = delete;
+ NativeWeakPtrControlBlock(NativeWeakPtrControlBlock&&) = delete;
+ NativeWeakPtrControlBlock& operator=(const NativeWeakPtrControlBlock&) =
+ delete;
+ NativeWeakPtrControlBlock& operator=(NativeWeakPtrControlBlock&&) = delete;
+
+ // This is safe to call on any thread because mJavaOwner is immutable.
+ mozilla::jni::Object::WeakRef GetJavaOwner() const { return mJavaOwner; }
+
+ private:
+ NativeWeakPtrControlBlock(::mozilla::jni::Object::Param aJavaOwner,
+ StorageType&& aNativeImpl)
+ : mJavaOwner(aJavaOwner),
+ mLock("mozilla::jni::detail::NativeWeakPtrControlBlock"),
+ mNativeImpl(std::move(aNativeImpl)) {}
+
+ ~NativeWeakPtrControlBlock() {
+ // Make sure that somebody, somewhere, has detached us before destroying.
+ MOZ_ASSERT(!(*this));
+ }
+
+ /**
+ * Clear the native pointer so that subsequent accesses to the native pointer
+ * via this control block are no longer available.
+ *
+ * We return the native pointer to the caller so that it may proceed with
+ * cleaning up its resources.
+ */
+ StorageType Clear() {
+ StorageType nativeImpl(nullptr);
+
+ { // Scope for lock
+ AutoWriteLock lock(mLock);
+ std::swap(mNativeImpl, nativeImpl);
+ }
+
+ return nativeImpl;
+ }
+
+ MOZ_PUSH_IGNORE_THREAD_SAFETY
+ void Lock() const { mLock.ReadLock(); }
+
+ void Unlock() const { mLock.ReadUnlock(); }
+ MOZ_POP_THREAD_SAFETY
+
+#if defined(DEBUG)
+ // This is kind of expensive, so we only support it in debug builds.
+ explicit operator bool() const {
+ AutoReadLock lock(mLock);
+ return !!mNativeImpl;
+ }
+#endif // defined(DEBUG)
+
+ private:
+ friend class Accessor<NativeImpl>;
+ friend class NativeWeakPtr<NativeImpl>;
+ friend class NativeWeakPtrHolder<NativeImpl>;
+
+ private:
+ const mozilla::jni::Object::WeakRef mJavaOwner;
+ mutable RWLock mLock MOZ_UNANNOTATED; // Protects mNativeImpl
+ StorageType mNativeImpl;
+};
+
+/**
+ * If you want to temporarily access the object held by a NativeWeakPtr, you
+ * must obtain one of these Accessor objects from the pointer. Access must
+ * be done _exclusively_ using once of these objects!
+ */
+template <typename NativeImpl>
+class MOZ_STACK_CLASS Accessor final {
+ public:
+ ~Accessor() {
+ if (mCtlBlock) {
+ mCtlBlock->Unlock();
+ }
+ }
+
+ // Check whether the object is still valid before doing anything else
+ explicit operator bool() const { return mCtlBlock && mCtlBlock->mNativeImpl; }
+
+ // Normal member access
+ NativeImpl* operator->() const {
+ return NativeWeakPtrControlBlockStorageTraits<NativeImpl>::AsRaw(
+ mCtlBlock->mNativeImpl);
+ }
+
+ // This allows us to support calling a pointer to a member function
+ template <typename Member>
+ auto operator->*(Member aMember) const {
+ NativeImpl* impl =
+ NativeWeakPtrControlBlockStorageTraits<NativeImpl>::AsRaw(
+ mCtlBlock->mNativeImpl);
+ return [impl, member = aMember](auto&&... aArgs) {
+ return (impl->*member)(std::forward<decltype(aArgs)>(aArgs)...);
+ };
+ }
+
+ // Only available for NativeImpl types that actually use refcounting.
+ // The idea here is that it should be possible to obtain a strong ref from
+ // a NativeWeakPtr if and only if NativeImpl supports refcounting.
+ template <typename I = NativeImpl>
+ auto AsRefPtr() const -> std::enable_if_t<IsRefCounted<I>::value, RefPtr<I>> {
+ MOZ_ASSERT(I::HasThreadSafeRefCnt::value || NS_IsMainThread());
+ return mCtlBlock->mNativeImpl;
+ }
+
+ Accessor(const Accessor&) = delete;
+ Accessor(Accessor&&) = delete;
+ Accessor& operator=(const Accessor&) = delete;
+ Accessor& operator=(Accessor&&) = delete;
+
+ private:
+ explicit Accessor(
+ const RefPtr<detail::NativeWeakPtrControlBlock<NativeImpl>>& aCtlBlock)
+ : mCtlBlock(aCtlBlock) {
+ if (aCtlBlock) {
+ aCtlBlock->Lock();
+ }
+ }
+
+ private:
+ friend class NativeWeakPtr<NativeImpl>;
+ friend class NativeWeakPtrHolder<NativeImpl>;
+
+ private:
+ const RefPtr<NativeWeakPtrControlBlock<NativeImpl>> mCtlBlock;
+};
+
+} // namespace detail
+
+using DetachPromise = mozilla::MozPromise<bool, nsresult, true>;
+
+/**
+ * This class implements support for thread-safe weak pointers to native objects
+ * that are owned by Java objects deriving from JNIObject.
+ *
+ * Any code that wants to access such a native object must have a copy of
+ * a NativeWeakPtr to that object.
+ */
+template <typename NativeImpl>
+class NativeWeakPtr {
+ public:
+ using Accessor = detail::Accessor<NativeImpl>;
+
+ /**
+ * Call this method to access the underlying object referenced by this
+ * NativeWeakPtr.
+ *
+ * Always check the returned Accessor object for availability before calling
+ * methods on it.
+ *
+ * For example, given:
+ *
+ * NativeWeakPtr<Foo> foo;
+ * auto accessor = foo.Access();
+ * if (accessor) {
+ * // Okay, safe to work with
+ * accessor->DoStuff();
+ * } else {
+ * // The object's strong reference was cleared and is no longer available!
+ * }
+ */
+ Accessor Access() const { return Accessor(mCtlBlock); }
+
+ /**
+ * Detach the underlying object's strong reference from its owning Java object
+ * and clean it up.
+ */
+ RefPtr<DetachPromise> Detach();
+
+ /**
+ * This method does not indicate whether or not the weak pointer is still
+ * valid; it only indicates whether we're actually attached to one.
+ */
+ bool IsAttached() const { return !!mCtlBlock; }
+
+ /**
+ * Does this pointer reference the same object as the one referenced by the
+ * provided Accessor?
+ */
+ bool IsSame(const Accessor& aAccessor) const {
+ return mCtlBlock == aAccessor.mCtlBlock;
+ }
+
+ /**
+ * Does this pointer reference the same object as the one referenced by the
+ * provided Control Block?
+ */
+ bool IsSame(const RefPtr<detail::NativeWeakPtrControlBlock<NativeImpl>>&
+ aOther) const {
+ return mCtlBlock == aOther;
+ }
+
+ NativeWeakPtr() = default;
+ MOZ_IMPLICIT NativeWeakPtr(decltype(nullptr)) {}
+ NativeWeakPtr(const NativeWeakPtr& aOther) = default;
+ NativeWeakPtr(NativeWeakPtr&& aOther) = default;
+ NativeWeakPtr& operator=(const NativeWeakPtr& aOther) = default;
+ NativeWeakPtr& operator=(NativeWeakPtr&& aOther) = default;
+
+ NativeWeakPtr& operator=(decltype(nullptr)) {
+ mCtlBlock = nullptr;
+ return *this;
+ }
+
+ protected:
+ // Construction of initial NativeWeakPtr for aCtlBlock
+ explicit NativeWeakPtr(
+ already_AddRefed<detail::NativeWeakPtrControlBlock<NativeImpl>> aCtlBlock)
+ : mCtlBlock(aCtlBlock) {}
+
+ private:
+ // Construction of subsequent NativeWeakPtrs for aCtlBlock
+ explicit NativeWeakPtr(
+ const RefPtr<detail::NativeWeakPtrControlBlock<NativeImpl>>& aCtlBlock)
+ : mCtlBlock(aCtlBlock) {}
+
+ friend class NativeWeakPtrHolder<NativeImpl>;
+
+ protected:
+ RefPtr<detail::NativeWeakPtrControlBlock<NativeImpl>> mCtlBlock;
+};
+
+/**
+ * A pointer to an instance of this class should be stored in a Java object's
+ * JNIObject handle. New instances of native objects wrapped by NativeWeakPtr
+ * are created using the static methods of this class.
+ *
+ * Why do we have distinct methods here instead of using AttachNative like other
+ * pointer types that may be stored in JNIObject?
+ *
+ * Essentially, we want the creation and use of NativeWeakPtr to be as
+ * deliberate as possible. Forcing a different creation mechanism is part of
+ * that emphasis.
+ *
+ * Example:
+ *
+ * class NativeFoo {
+ * public:
+ * NativeFoo();
+ * void Bar();
+ * // The following method is required to be used with NativeWeakPtr
+ * void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer);
+ * };
+ *
+ * java::Object::LocalRef javaObj(...);
+ *
+ * // Create a new Foo that is attached to javaObj
+ * auto weakFoo = NativeWeakPtrHolder<NativeFoo>::Attach(javaObj);
+ *
+ * // Now I can save weakFoo, access it, do whatever I want
+ * if (auto accWeakFoo = weakFoo.Access()) {
+ * accWeakFoo->Bar();
+ * }
+ *
+ * // Detach from javaObj and clean up
+ * weakFoo.Detach();
+ */
+template <typename NativeImpl>
+class MOZ_HEAP_CLASS NativeWeakPtrHolder final
+ : public NativeWeakPtr<NativeImpl> {
+ using Base = NativeWeakPtr<NativeImpl>;
+
+ public:
+ using Accessor = typename Base::Accessor;
+ using StorageTraits =
+ typename detail::NativeWeakPtrControlBlock<NativeImpl>::StorageTraits;
+ using StorageType = typename StorageTraits::Type;
+
+ /**
+ * Create a new NativeImpl object, wrap it in a NativeWeakPtr, and store it
+ * in the Java object's JNIObject handle.
+ *
+ * @return A NativeWeakPtr object that references the newly-attached object.
+ */
+ template <typename Cls, typename JNIType, typename... Args>
+ static NativeWeakPtr<NativeImpl> Attach(const Ref<Cls, JNIType>& aJavaObject,
+ Args&&... aArgs) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ StorageType nativeImpl(new NativeImpl(std::forward<Args>(aArgs)...));
+ return AttachInternal(aJavaObject, std::move(nativeImpl));
+ }
+
+ /**
+ * Given a new NativeImpl object, wrap it in a NativeWeakPtr, and store it
+ * in the Java object's JNIObject handle.
+ *
+ * @return A NativeWeakPtr object that references the newly-attached object.
+ */
+ template <typename Cls, typename JNIType>
+ static NativeWeakPtr<NativeImpl> AttachExisting(
+ const Ref<Cls, JNIType>& aJavaObject,
+ already_AddRefed<NativeImpl> aNativeImpl) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ StorageType nativeImpl(aNativeImpl);
+ return AttachInternal(aJavaObject, std::move(nativeImpl));
+ }
+
+ ~NativeWeakPtrHolder() = default;
+
+ MOZ_IMPLICIT NativeWeakPtrHolder(decltype(nullptr)) = delete;
+ NativeWeakPtrHolder(const NativeWeakPtrHolder&) = delete;
+ NativeWeakPtrHolder(NativeWeakPtrHolder&&) = delete;
+ NativeWeakPtrHolder& operator=(const NativeWeakPtrHolder&) = delete;
+ NativeWeakPtrHolder& operator=(NativeWeakPtrHolder&&) = delete;
+ NativeWeakPtrHolder& operator=(decltype(nullptr)) = delete;
+
+ private:
+ template <typename Cls>
+ NativeWeakPtrHolder(const LocalRef<Cls>& aJavaObject,
+ StorageType&& aNativeImpl)
+ : NativeWeakPtr<NativeImpl>(
+ do_AddRef(new NativeWeakPtrControlBlock<NativeImpl>(
+ aJavaObject, std::move(aNativeImpl)))) {}
+
+ /**
+ * Internal function that actually wraps the native pointer, binds it to the
+ * JNIObject, and then returns the NativeWeakPtr result.
+ */
+ template <typename Cls, typename JNIType>
+ static NativeWeakPtr<NativeImpl> AttachInternal(
+ const Ref<Cls, JNIType>& aJavaObject, StorageType&& aPtr) {
+ auto localJavaObject = ToLocalRef(aJavaObject);
+ NativeWeakPtrHolder<NativeImpl>* holder =
+ new NativeWeakPtrHolder<NativeImpl>(localJavaObject, std::move(aPtr));
+ static_assert(
+ NativePtrPicker<NativeImpl>::value == NativePtrType::WEAK_NON_INTRUSIVE,
+ "This type is not compatible with mozilla::jni::NativeWeakPtr");
+ NativePtrTraits<NativeImpl>::Set(localJavaObject, holder);
+ return NativeWeakPtr<NativeImpl>(holder->mCtlBlock);
+ }
+};
+
+namespace detail {
+
+/**
+ * NativePtrTraits for the WEAK_NON_INTRUSIVE pointer type.
+ */
+template <class Impl>
+struct NativePtrTraits<Impl, /* Type = */ NativePtrType::WEAK_NON_INTRUSIVE> {
+ using AccessorType = typename NativeWeakPtrHolder<Impl>::Accessor;
+ using HandleType = NativeWeakPtrHolder<Impl>*;
+ using RefType = NativeWeakPtrHolder<Impl>* const;
+
+ static RefType Get(JNIEnv* env, jobject instance) {
+ return GetHandle(env, instance);
+ }
+
+ template <typename Cls>
+ static RefType Get(const LocalRef<Cls>& instance) {
+ return GetHandle(instance.Env(), instance.Get());
+ }
+
+ static AccessorType Access(RefType aPtr) { return aPtr->Access(); }
+
+ template <typename Cls>
+ static void Set(const LocalRef<Cls>& instance, HandleType ptr) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ const uintptr_t handle = reinterpret_cast<uintptr_t>(ptr);
+ Clear(instance);
+ SetNativeHandle(instance.Env(), instance.Get(), handle);
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+ }
+
+ template <typename Cls>
+ static void Clear(const LocalRef<Cls>& instance) {
+ auto ptr = reinterpret_cast<HandleType>(
+ GetNativeHandle(instance.Env(), instance.Get()));
+ MOZ_CATCH_JNI_EXCEPTION(instance.Env());
+
+ if (!ptr) {
+ return;
+ }
+
+ ptr->Detach();
+ }
+
+ // This call is not safe to do unless we know for sure that instance's
+ // native handle has not changed. It is up to NativeWeakPtrDetachRunnable
+ // to perform this check.
+ template <typename Cls>
+ static void ClearFinish(const LocalRef<Cls>& instance) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ JNIEnv* const env = instance.Env();
+ auto ptr =
+ reinterpret_cast<HandleType>(GetNativeHandle(env, instance.Get()));
+ MOZ_CATCH_JNI_EXCEPTION(env);
+ MOZ_RELEASE_ASSERT(!!ptr);
+
+ SetNativeHandle(env, instance.Get(), 0);
+ MOZ_CATCH_JNI_EXCEPTION(env);
+ // Deletion of ptr is done by the caller
+ }
+
+ // The call is stale if the native object has been destroyed on the
+ // Gecko side, but the Java object is still attached to it through
+ // a weak pointer. Stale calls should be discarded. Note that it's
+ // an error if holder is nullptr here; we return false but the
+ // native call will throw an error.
+ template <class LocalRef>
+ static bool IsStale(const LocalRef& instance) {
+ JNIEnv* const env = mozilla::jni::GetEnvForThread();
+
+ // We cannot use Get here because that method throws an exception when the
+ // object is null, which is a valid state for a stale call.
+ const auto holder =
+ reinterpret_cast<HandleType>(GetNativeHandle(env, instance.Get()));
+ MOZ_CATCH_JNI_EXCEPTION(env);
+
+ if (!holder || !holder->IsAttached()) {
+ return true;
+ }
+
+ auto acc(holder->Access());
+ return !acc;
+ }
+
+ private:
+ static HandleType GetHandle(JNIEnv* env, jobject instance) {
+ return reinterpret_cast<HandleType>(
+ CheckNativeHandle<Impl>(env, GetNativeHandle(env, instance)));
+ }
+
+ template <typename Cls>
+ static HandleType GetHandle(const LocalRef<Cls>& instance) {
+ return GetHandle(instance.Env(), instance.Get());
+ }
+
+ friend class NativeWeakPtrHolder<Impl>;
+};
+
+} // namespace detail
+
+using namespace detail;
+
+/**
+ * For JNI native methods that are dispatched to a proxy, i.e. using
+ * @WrapForJNI(dispatchTo = "proxy"), the implementing C++ class must provide a
+ * OnNativeCall member. Subsequently, every native call is automatically
+ * wrapped in a functor object, and the object is passed to OnNativeCall. The
+ * OnNativeCall implementation can choose to invoke the call, save it, dispatch
+ * it to a different thread, etc. Each copy of functor may only be invoked
+ * once.
+ *
+ * class MyClass : public MyJavaClass::Natives<MyClass>
+ * {
+ * // ...
+ *
+ * template<class Functor>
+ * class ProxyRunnable final : public Runnable
+ * {
+ * Functor mCall;
+ * public:
+ * ProxyRunnable(Functor&& call) : mCall(std::move(call)) {}
+ * virtual void run() override { mCall(); }
+ * };
+ *
+ * public:
+ * template<class Functor>
+ * static void OnNativeCall(Functor&& call)
+ * {
+ * RunOnAnotherThread(new ProxyRunnable(std::move(call)));
+ * }
+ * };
+ */
+
+namespace detail {
+
+// ProxyArg is used to handle JNI ref arguments for proxies. Because a proxied
+// call may happen outside of the original JNI native call, we must save all
+// JNI ref arguments as global refs to avoid the arguments going out of scope.
+template <typename T>
+struct ProxyArg {
+ static_assert(std::is_trivial_v<T> && std::is_standard_layout_v<T>,
+ "T must be primitive type");
+
+ // Primitive types can be saved by value.
+ typedef T Type;
+ typedef typename TypeAdapter<T>::JNIType JNIType;
+
+ static void Clear(JNIEnv* env, Type&) {}
+
+ static Type From(JNIEnv* env, JNIType val) {
+ return TypeAdapter<T>::ToNative(env, val);
+ }
+};
+
+template <class C, typename T>
+struct ProxyArg<Ref<C, T>> {
+ // Ref types need to be saved by global ref.
+ typedef typename C::GlobalRef Type;
+ typedef typename TypeAdapter<Ref<C, T>>::JNIType JNIType;
+
+ static void Clear(JNIEnv* env, Type& ref) { ref.Clear(env); }
+
+ static Type From(JNIEnv* env, JNIType val) {
+ return Type(env, C::Ref::From(val));
+ }
+};
+
+template <typename C>
+struct ProxyArg<const C&> : ProxyArg<C> {};
+template <>
+struct ProxyArg<StringParam> : ProxyArg<String::Ref> {};
+template <class C>
+struct ProxyArg<LocalRef<C>> : ProxyArg<typename C::Ref> {};
+
+// ProxyNativeCall implements the functor object that is passed to OnNativeCall
+template <class Impl, class Owner, bool IsStatic,
+ bool HasThisArg /* has instance/class local ref in the call */,
+ typename... Args>
+class ProxyNativeCall {
+ // "this arg" refers to the Class::LocalRef (for static methods) or
+ // Owner::LocalRef (for instance methods) that we optionally (as indicated
+ // by HasThisArg) pass into the destination C++ function.
+ using ThisArgClass = std::conditional_t<IsStatic, Class, Owner>;
+ using ThisArgJNIType = std::conditional_t<IsStatic, jclass, jobject>;
+
+ // Type signature of the destination C++ function, which matches the
+ // Method template parameter in NativeStubImpl::Wrap.
+ using NativeCallType = std::conditional_t<
+ IsStatic,
+ std::conditional_t<HasThisArg, void (*)(const Class::LocalRef&, Args...),
+ void (*)(Args...)>,
+ std::conditional_t<
+ HasThisArg, void (Impl::*)(const typename Owner::LocalRef&, Args...),
+ void (Impl::*)(Args...)>>;
+
+ // Destination C++ function.
+ NativeCallType mNativeCall;
+ // Saved this arg.
+ typename ThisArgClass::GlobalRef mThisArg;
+ // Saved arguments.
+ std::tuple<typename ProxyArg<Args>::Type...> mArgs;
+
+ // We cannot use IsStatic and HasThisArg directly (without going through
+ // extra hoops) because GCC complains about invalid overloads, so we use
+ // another pair of template parameters, Static and ThisArg.
+
+ template <bool Static, bool ThisArg, size_t... Indices>
+ std::enable_if_t<Static && ThisArg, void> Call(
+ const Class::LocalRef& cls, std::index_sequence<Indices...>) const {
+ (*mNativeCall)(cls, std::get<Indices>(mArgs)...);
+ }
+
+ template <bool Static, bool ThisArg, size_t... Indices>
+ std::enable_if_t<Static && !ThisArg, void> Call(
+ const Class::LocalRef& cls, std::index_sequence<Indices...>) const {
+ (*mNativeCall)(std::get<Indices>(mArgs)...);
+ }
+
+ template <bool Static, bool ThisArg, size_t... Indices>
+ std::enable_if_t<!Static && ThisArg, void> Call(
+ const typename Owner::LocalRef& inst,
+ std::index_sequence<Indices...>) const {
+ auto impl = NativePtrTraits<Impl>::Access(NativePtrTraits<Impl>::Get(inst));
+ MOZ_CATCH_JNI_EXCEPTION(inst.Env());
+ (impl->*mNativeCall)(inst, std::get<Indices>(mArgs)...);
+ }
+
+ template <bool Static, bool ThisArg, size_t... Indices>
+ std::enable_if_t<!Static && !ThisArg, void> Call(
+ const typename Owner::LocalRef& inst,
+ std::index_sequence<Indices...>) const {
+ auto impl = NativePtrTraits<Impl>::Access(NativePtrTraits<Impl>::Get(inst));
+ MOZ_CATCH_JNI_EXCEPTION(inst.Env());
+ (impl->*mNativeCall)(std::get<Indices>(mArgs)...);
+ }
+
+ template <size_t... Indices>
+ void Clear(JNIEnv* env, std::index_sequence<Indices...>) {
+ int dummy[] = {
+ (ProxyArg<Args>::Clear(env, std::get<Indices>(mArgs)), 0)...};
+ mozilla::Unused << dummy;
+ }
+
+ static decltype(auto) GetNativeObject(Class::Param thisArg) {
+ return nullptr;
+ }
+
+ static decltype(auto) GetNativeObject(typename Owner::Param thisArg) {
+ return NativePtrTraits<Impl>::Access(
+ NativePtrTraits<Impl>::Get(GetEnvForThread(), thisArg.Get()));
+ }
+
+ public:
+ // The class that implements the call target.
+ typedef Impl TargetClass;
+ typedef typename ThisArgClass::Param ThisArgType;
+
+ static const bool isStatic = IsStatic;
+
+ ProxyNativeCall(ThisArgJNIType thisArg, NativeCallType nativeCall,
+ JNIEnv* env, typename ProxyArg<Args>::JNIType... args)
+ : mNativeCall(nativeCall),
+ mThisArg(env, ThisArgClass::Ref::From(thisArg)),
+ mArgs(ProxyArg<Args>::From(env, args)...) {}
+
+ ProxyNativeCall(ProxyNativeCall&&) = default;
+ ProxyNativeCall(const ProxyNativeCall&) = default;
+
+ // Get class ref for static calls or object ref for instance calls.
+ typename ThisArgClass::Param GetThisArg() const { return mThisArg; }
+
+ // Get the native object targeted by this call.
+ // Returns nullptr for static calls.
+ decltype(auto) GetNativeObject() const { return GetNativeObject(mThisArg); }
+
+ // Return if target is the given function pointer / pointer-to-member.
+ // Because we can only compare pointers of the same type, we use a
+ // templated overload that is chosen only if given a different type of
+ // pointer than our target pointer type.
+ bool IsTarget(NativeCallType call) const { return call == mNativeCall; }
+ template <typename T>
+ bool IsTarget(T&&) const {
+ return false;
+ }
+
+ // Redirect the call to another function / class member with the same
+ // signature as the original target. Crash if given a wrong signature.
+ void SetTarget(NativeCallType call) { mNativeCall = call; }
+ template <typename T>
+ void SetTarget(T&&) const {
+ MOZ_CRASH();
+ }
+
+ void operator()() {
+ JNIEnv* const env = GetEnvForThread();
+ typename ThisArgClass::LocalRef thisArg(env, mThisArg);
+ Call<IsStatic, HasThisArg>(thisArg, std::index_sequence_for<Args...>{});
+
+ // Clear all saved global refs. We do this after the call is invoked,
+ // and not inside the destructor because we already have a JNIEnv here,
+ // so it's more efficient to clear out the saved args here. The
+ // downside is that the call can only be invoked once.
+ Clear(env, std::index_sequence_for<Args...>{});
+ mThisArg.Clear(env);
+ }
+};
+
+template <class Impl, bool HasThisArg, typename... Args>
+struct Dispatcher {
+ template <class Traits, bool IsStatic = Traits::isStatic,
+ typename... ProxyArgs>
+ static std::enable_if_t<Traits::dispatchTarget == DispatchTarget::PROXY, void>
+ Run(ProxyArgs&&... args) {
+ Impl::OnNativeCall(
+ ProxyNativeCall<Impl, typename Traits::Owner, IsStatic, HasThisArg,
+ Args...>(std::forward<ProxyArgs>(args)...));
+ }
+
+ template <class Traits, bool IsStatic = Traits::isStatic, typename ThisArg,
+ typename... ProxyArgs>
+ static std::enable_if_t<
+ Traits::dispatchTarget == DispatchTarget::GECKO_PRIORITY, void>
+ Run(ThisArg thisArg, ProxyArgs&&... args) {
+ // For a static method, do not forward the "this arg" (i.e. the class
+ // local ref) if the implementation does not request it. This saves us
+ // a pair of calls to add/delete global ref.
+ auto proxy =
+ ProxyNativeCall<Impl, typename Traits::Owner, IsStatic, HasThisArg,
+ Args...>((HasThisArg || !IsStatic) ? thisArg : nullptr,
+ std::forward<ProxyArgs>(args)...);
+ DispatchToGeckoPriorityQueue(
+ NS_NewRunnableFunction("PriorityNativeCall", std::move(proxy)));
+ }
+
+ template <class Traits, bool IsStatic = Traits::isStatic, typename ThisArg,
+ typename... ProxyArgs>
+ static std::enable_if_t<Traits::dispatchTarget == DispatchTarget::GECKO, void>
+ Run(ThisArg thisArg, ProxyArgs&&... args) {
+ // For a static method, do not forward the "this arg" (i.e. the class
+ // local ref) if the implementation does not request it. This saves us
+ // a pair of calls to add/delete global ref.
+ auto proxy =
+ ProxyNativeCall<Impl, typename Traits::Owner, IsStatic, HasThisArg,
+ Args...>((HasThisArg || !IsStatic) ? thisArg : nullptr,
+ std::forward<ProxyArgs>(args)...);
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("GeckoNativeCall", std::move(proxy)));
+ }
+
+ template <class Traits, bool IsStatic = false, typename... ProxyArgs>
+ static std::enable_if_t<Traits::dispatchTarget == DispatchTarget::CURRENT,
+ void>
+ Run(ProxyArgs&&... args) {
+ MOZ_CRASH("Unreachable code");
+ }
+};
+
+} // namespace detail
+
+// Wrapper methods that convert arguments from the JNI types to the native
+// types, e.g. from jobject to jni::Object::Ref. For instance methods, the
+// wrapper methods also convert calls to calls on objects.
+//
+// We need specialization for static/non-static because the two have different
+// signatures (jobject vs jclass and Impl::*Method vs *Method).
+// We need specialization for return type, because void return type requires
+// us to not deal with the return value.
+
+// Bug 1207642 - Work around Dalvik bug by realigning stack on JNI entry
+#ifdef __i386__
+# define MOZ_JNICALL JNICALL __attribute__((force_align_arg_pointer))
+#else
+# define MOZ_JNICALL JNICALL
+#endif
+
+template <class Traits, class Impl, class Args = typename Traits::Args>
+class NativeStub;
+
+template <class Traits, class Impl, typename... Args>
+class NativeStub<Traits, Impl, jni::Args<Args...>> {
+ using Owner = typename Traits::Owner;
+ using ReturnType = typename Traits::ReturnType;
+
+ static constexpr bool isStatic = Traits::isStatic;
+ static constexpr bool isVoid = std::is_void_v<ReturnType>;
+
+ struct VoidType {
+ using JNIType = void;
+ };
+ using ReturnJNIType =
+ typename std::conditional_t<isVoid, VoidType,
+ TypeAdapter<ReturnType>>::JNIType;
+
+ using ReturnTypeForNonVoidInstance =
+ std::conditional_t<!isStatic && !isVoid, ReturnType, VoidType>;
+ using ReturnTypeForVoidInstance =
+ std::conditional_t<!isStatic && isVoid, ReturnType, VoidType&>;
+ using ReturnTypeForNonVoidStatic =
+ std::conditional_t<isStatic && !isVoid, ReturnType, VoidType>;
+ using ReturnTypeForVoidStatic =
+ std::conditional_t<isStatic && isVoid, ReturnType, VoidType&>;
+
+ static_assert(Traits::dispatchTarget == DispatchTarget::CURRENT || isVoid,
+ "Dispatched calls must have void return type");
+
+ public:
+ // Non-void instance method
+ template <ReturnTypeForNonVoidInstance (Impl::*Method)(Args...)>
+ static MOZ_JNICALL ReturnJNIType
+ Wrap(JNIEnv* env, jobject instance,
+ typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ auto impl = NativePtrTraits<Impl>::Access(
+ NativePtrTraits<Impl>::Get(env, instance));
+ if (!impl) {
+ // There is a pending JNI exception at this point.
+ return ReturnJNIType();
+ }
+ return TypeAdapter<ReturnType>::FromNative(
+ env, (impl->*Method)(TypeAdapter<Args>::ToNative(env, args)...));
+ }
+
+ // Non-void instance method with instance reference
+ template <ReturnTypeForNonVoidInstance (Impl::*Method)(
+ const typename Owner::LocalRef&, Args...)>
+ static MOZ_JNICALL ReturnJNIType
+ Wrap(JNIEnv* env, jobject instance,
+ typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ auto impl = NativePtrTraits<Impl>::Access(
+ NativePtrTraits<Impl>::Get(env, instance));
+ if (!impl) {
+ // There is a pending JNI exception at this point.
+ return ReturnJNIType();
+ }
+ auto self = Owner::LocalRef::Adopt(env, instance);
+ const auto res = TypeAdapter<ReturnType>::FromNative(
+ env, (impl->*Method)(self, TypeAdapter<Args>::ToNative(env, args)...));
+ self.Forget();
+ return res;
+ }
+
+ // Void instance method
+ template <ReturnTypeForVoidInstance (Impl::*Method)(Args...)>
+ static MOZ_JNICALL void Wrap(JNIEnv* env, jobject instance,
+ typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ if (Traits::dispatchTarget != DispatchTarget::CURRENT) {
+ Dispatcher<Impl, /* HasThisArg */ false, Args...>::template Run<Traits>(
+ instance, Method, env, args...);
+ return;
+ }
+
+ auto impl = NativePtrTraits<Impl>::Access(
+ NativePtrTraits<Impl>::Get(env, instance));
+ if (!impl) {
+ // There is a pending JNI exception at this point.
+ return;
+ }
+ (impl->*Method)(TypeAdapter<Args>::ToNative(env, args)...);
+ }
+
+ // Void instance method with instance reference
+ template <ReturnTypeForVoidInstance (Impl::*Method)(
+ const typename Owner::LocalRef&, Args...)>
+ static MOZ_JNICALL void Wrap(JNIEnv* env, jobject instance,
+ typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ if (Traits::dispatchTarget != DispatchTarget::CURRENT) {
+ Dispatcher<Impl, /* HasThisArg */ true, Args...>::template Run<Traits>(
+ instance, Method, env, args...);
+ return;
+ }
+
+ auto impl = NativePtrTraits<Impl>::Access(
+ NativePtrTraits<Impl>::Get(env, instance));
+ if (!impl) {
+ // There is a pending JNI exception at this point.
+ return;
+ }
+ auto self = Owner::LocalRef::Adopt(env, instance);
+ (impl->*Method)(self, TypeAdapter<Args>::ToNative(env, args)...);
+ self.Forget();
+ }
+
+ // Overload for DisposeNative
+ template <ReturnTypeForVoidInstance (*DisposeNative)(
+ const typename Owner::LocalRef&)>
+ static MOZ_JNICALL void Wrap(JNIEnv* env, jobject instance) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ if (Traits::dispatchTarget != DispatchTarget::CURRENT) {
+ using LocalRef = typename Owner::LocalRef;
+ Dispatcher<Impl, /* HasThisArg */ false, const LocalRef&>::template Run<
+ Traits, /* IsStatic */ true>(
+ /* ThisArg */ nullptr, DisposeNative, env, instance);
+ return;
+ }
+
+ auto self = Owner::LocalRef::Adopt(env, instance);
+ DisposeNative(self);
+ self.Forget();
+ }
+
+ // Non-void static method
+ template <ReturnTypeForNonVoidStatic (*Method)(Args...)>
+ static MOZ_JNICALL ReturnJNIType
+ Wrap(JNIEnv* env, jclass, typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ return TypeAdapter<ReturnType>::FromNative(
+ env, (*Method)(TypeAdapter<Args>::ToNative(env, args)...));
+ }
+
+ // Non-void static method with class reference
+ template <ReturnTypeForNonVoidStatic (*Method)(const Class::LocalRef&,
+ Args...)>
+ static MOZ_JNICALL ReturnJNIType
+ Wrap(JNIEnv* env, jclass cls, typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ auto clazz = Class::LocalRef::Adopt(env, cls);
+ const auto res = TypeAdapter<ReturnType>::FromNative(
+ env, (*Method)(clazz, TypeAdapter<Args>::ToNative(env, args)...));
+ clazz.Forget();
+ return res;
+ }
+
+ // Void static method
+ template <ReturnTypeForVoidStatic (*Method)(Args...)>
+ static MOZ_JNICALL void Wrap(JNIEnv* env, jclass cls,
+ typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ if (Traits::dispatchTarget != DispatchTarget::CURRENT) {
+ Dispatcher<Impl, /* HasThisArg */ false, Args...>::template Run<Traits>(
+ cls, Method, env, args...);
+ return;
+ }
+
+ (*Method)(TypeAdapter<Args>::ToNative(env, args)...);
+ }
+
+ // Void static method with class reference
+ template <ReturnTypeForVoidStatic (*Method)(const Class::LocalRef&, Args...)>
+ static MOZ_JNICALL void Wrap(JNIEnv* env, jclass cls,
+ typename TypeAdapter<Args>::JNIType... args) {
+ MOZ_ASSERT_JNI_THREAD(Traits::callingThread);
+
+ if (Traits::dispatchTarget != DispatchTarget::CURRENT) {
+ Dispatcher<Impl, /* HasThisArg */ true, Args...>::template Run<Traits>(
+ cls, Method, env, args...);
+ return;
+ }
+
+ auto clazz = Class::LocalRef::Adopt(env, cls);
+ (*Method)(clazz, TypeAdapter<Args>::ToNative(env, args)...);
+ clazz.Forget();
+ }
+};
+
+// Generate a JNINativeMethod from a native
+// method's traits class and a wrapped stub.
+template <class Traits, typename Ret, typename... Args>
+constexpr JNINativeMethod MakeNativeMethod(MOZ_JNICALL Ret (*stub)(JNIEnv*,
+ Args...)) {
+ return {Traits::name, Traits::signature, reinterpret_cast<void*>(stub)};
+}
+
+// Class inherited by implementing class.
+template <class Cls, class Impl>
+class NativeImpl {
+ typedef typename Cls::template Natives<Impl> Natives;
+
+ static bool sInited;
+
+ public:
+ static void Init() {
+ if (sInited) {
+ return;
+ }
+ const auto& ctx = typename Cls::Context();
+ ctx.Env()->RegisterNatives(
+ ctx.ClassRef(), Natives::methods,
+ sizeof(Natives::methods) / sizeof(Natives::methods[0]));
+ MOZ_CATCH_JNI_EXCEPTION(ctx.Env());
+ sInited = true;
+ }
+
+ protected:
+ // Associate a C++ instance with a Java instance.
+ static void AttachNative(const typename Cls::LocalRef& instance,
+ SupportsWeakPtr* ptr) {
+ static_assert(NativePtrPicker<Impl>::value == NativePtrType::WEAK_INTRUSIVE,
+ "Use another AttachNative for non-WeakPtr usage");
+ return NativePtrTraits<Impl>::Set(instance, static_cast<Impl*>(ptr));
+ }
+
+ static void AttachNative(const typename Cls::LocalRef& instance,
+ UniquePtr<Impl>&& ptr) {
+ static_assert(NativePtrPicker<Impl>::value == NativePtrType::OWNING,
+ "Use another AttachNative for WeakPtr or RefPtr usage");
+ return NativePtrTraits<Impl>::Set(instance, std::move(ptr));
+ }
+
+ static void AttachNative(const typename Cls::LocalRef& instance, Impl* ptr) {
+ static_assert(NativePtrPicker<Impl>::value == NativePtrType::REFPTR,
+ "Use another AttachNative for non-RefPtr usage");
+ return NativePtrTraits<Impl>::Set(instance, ptr);
+ }
+
+ // Get the C++ instance associated with a Java instance.
+ // There is always a pending exception if the return value is nullptr.
+ static decltype(auto) GetNative(const typename Cls::LocalRef& instance) {
+ return NativePtrTraits<Impl>::Get(instance);
+ }
+
+ static void DisposeNative(const typename Cls::LocalRef& instance) {
+ NativePtrTraits<Impl>::Clear(instance);
+ }
+
+ NativeImpl() {
+ // Initialize on creation if not already initialized.
+ Init();
+ }
+};
+
+// Define static member.
+template <class C, class I>
+bool NativeImpl<C, I>::sInited;
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_Natives_h__
diff --git a/widget/android/jni/NativesInlines.h b/widget/android/jni/NativesInlines.h
new file mode 100644
index 0000000000..858b677715
--- /dev/null
+++ b/widget/android/jni/NativesInlines.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "Natives.h"
+
+#include "mozilla/MozPromise.h"
+
+namespace mozilla::jni {
+
+namespace details {
+
+/**
+ * When a NativeWeakPtr is detached from its owning Java object, the calling
+ * thread invokes the implementation's OnWeakNonIntrusiveDetach to perform
+ * cleanup. We complete the remainder of the cleanup sequence on the Gecko
+ * main thread by expecting OnWeakNonIntrusiveDetach implementations to invoke
+ * this Runnable before exiting. It will move itself to the main thread if it
+ * is not already there.
+ */
+template <typename NativeImpl>
+class NativeWeakPtrDetachRunnable final : public Runnable {
+ public:
+ NativeWeakPtrDetachRunnable(
+ already_AddRefed<detail::NativeWeakPtrControlBlock<NativeImpl>> aCtlBlock,
+ const Object::LocalRef& aOwner,
+ typename NativeWeakPtrControlBlockStorageTraits<NativeImpl>::Type
+ aNativeImpl)
+ : Runnable("mozilla::jni::detail::NativeWeakPtrDetachRunnable"),
+ mCtlBlock(aCtlBlock),
+ mOwner(aOwner),
+ mNativeImpl(std::move(aNativeImpl)),
+ mHasRun(false) {
+ MOZ_RELEASE_ASSERT(!!mCtlBlock);
+ MOZ_RELEASE_ASSERT(!!mNativeImpl);
+ }
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(NativeWeakPtrDetachRunnable, Runnable)
+
+ NS_IMETHOD Run() override {
+ mHasRun = true;
+
+ if (!NS_IsMainThread()) {
+ NS_DispatchToMainThread(this);
+ return NS_OK;
+ }
+
+ // Get the owner object's native implementation
+ auto owner = ToLocalRef(mOwner);
+ auto attachedNativeImpl = NativePtrTraits<NativeImpl>::Get(owner);
+ MOZ_RELEASE_ASSERT(!!attachedNativeImpl);
+
+ // NativePtrTraits::ClearFinish cleans out the JNIObject's handle, which
+ // obviously we don't want to attempt unless that handle still points to
+ // our native implementation.
+ if (attachedNativeImpl->IsSame(mCtlBlock)) {
+ NativePtrTraits<NativeImpl>::ClearFinish(owner);
+ }
+
+ // Now we destroy that native object.
+ mNativeImpl = nullptr;
+ mHolder.Resolve(true, __func__);
+ return NS_OK;
+ }
+
+ RefPtr<DetachPromise> GetPromise() { return mHolder.Ensure(__func__); }
+
+ private:
+ ~NativeWeakPtrDetachRunnable() {
+ // Guard against somebody forgetting to call this runnable.
+ MOZ_RELEASE_ASSERT(mHasRun, "You must run/dispatch this runnable!");
+ }
+
+ private:
+ RefPtr<detail::NativeWeakPtrControlBlock<NativeImpl>> mCtlBlock;
+ Object::GlobalRef mOwner;
+ MozPromiseHolder<DetachPromise> mHolder;
+ typename NativeWeakPtrControlBlockStorageTraits<NativeImpl>::Type mNativeImpl;
+ bool mHasRun;
+};
+
+} // namespace details
+
+template <typename NativeImpl>
+RefPtr<DetachPromise> NativeWeakPtr<NativeImpl>::Detach() {
+ if (!IsAttached()) {
+ // Never attached to begin with; no-op
+ return DetachPromise::CreateAndResolve(true, __func__);
+ }
+
+ auto native = mCtlBlock->Clear();
+ if (!native) {
+ // Detach already in progress
+ return DetachPromise::CreateAndResolve(true, __func__);
+ }
+
+ Object::LocalRef owner(mCtlBlock->GetJavaOwner());
+ MOZ_RELEASE_ASSERT(!!owner);
+
+ // Save the raw pointer before we move native into the runnable so that we
+ // may call OnWeakNonIntrusiveDetach on it even after moving native into
+ // the runnable.
+ NativeImpl* rawImpl =
+ detail::NativeWeakPtrControlBlock<NativeImpl>::StorageTraits::AsRaw(
+ native);
+ RefPtr<details::NativeWeakPtrDetachRunnable<NativeImpl>> runnable =
+ new details::NativeWeakPtrDetachRunnable<NativeImpl>(
+ mCtlBlock.forget(), owner, std::move(native));
+ RefPtr<DetachPromise> promise = runnable->GetPromise();
+ rawImpl->OnWeakNonIntrusiveDetach(runnable.forget());
+ return promise;
+}
+
+} // namespace mozilla::jni
diff --git a/widget/android/jni/Refs.h b/widget/android/jni/Refs.h
new file mode 100644
index 0000000000..29812fe6a5
--- /dev/null
+++ b/widget/android/jni/Refs.h
@@ -0,0 +1,1135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_Refs_h__
+#define mozilla_jni_Refs_h__
+
+#include <jni.h>
+
+#include <utility>
+
+#include "mozilla/fallible.h"
+#include "mozilla/jni/Utils.h"
+#include "mozilla/jni/TypeAdapter.h"
+#include "nsError.h" // for nsresult
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace jni {
+
+// Wrapped object reference (e.g. jobject, jclass, etc...)
+template <class Cls, typename JNIType>
+class Ref;
+// Represents a calling context for JNI methods.
+template <class Cls, typename JNIType>
+class Context;
+// Wrapped local reference that inherits from Ref.
+template <class Cls>
+class LocalRef;
+// Wrapped global reference that inherits from Ref.
+template <class Cls>
+class GlobalRef;
+// Wrapped weak reference that inherits from Ref.
+template <class Cls>
+class WeakRef;
+// Wrapped dangling reference that's owned by someone else.
+template <class Cls>
+class DependentRef;
+
+// Class to hold the native types of a method's arguments.
+// For example, if a method has signature (ILjava/lang/String;)V,
+// its arguments class would be jni::Args<int32_t, jni::String::Param>
+template <typename...>
+struct Args {};
+
+class Object;
+
+// Base class for Ref and its specializations.
+template <class Cls, typename Type>
+class Ref {
+ template <class C, typename T>
+ friend class Ref;
+
+ using Self = Ref<Cls, Type>;
+ using bool_type = void (Self::*)() const;
+ void non_null_reference() const {}
+
+ // A Cls-derivative that allows copying
+ // (e.g. when acting as a return value).
+ struct CopyableCtx : public Context<Cls, Type> {
+ CopyableCtx(JNIEnv* env, Type instance)
+ : Context<Cls, Type>(env, instance) {}
+
+ CopyableCtx(const CopyableCtx& cls)
+ : Context<Cls, Type>(cls.Env(), cls.Get()) {}
+ };
+
+ // Private copy constructor so that there's no danger of assigning a
+ // temporary LocalRef/GlobalRef to a Ref, and potentially use the Ref
+ // after the source had been freed.
+ Ref(const Ref&) = default;
+
+ protected:
+ static JNIEnv* FindEnv() {
+ return Cls::callingThread == CallingThread::GECKO ? GetGeckoThreadEnv()
+ : GetEnvForThread();
+ }
+
+ Type mInstance;
+
+ // Protected jobject constructor because outside code should be using
+ // Ref::From. Using Ref::From makes it very easy to see which code is using
+ // raw JNI types for future refactoring.
+ explicit Ref(Type instance) : mInstance(instance) {}
+
+ public:
+ using JNIType = Type;
+
+ class AutoLock {
+ friend class Ref<Cls, Type>;
+
+ JNIEnv* const mEnv;
+ Type mInstance;
+
+ explicit AutoLock(Type aInstance)
+ : mEnv(FindEnv()), mInstance(mEnv->NewLocalRef(aInstance)) {
+ mEnv->MonitorEnter(mInstance);
+ MOZ_CATCH_JNI_EXCEPTION(mEnv);
+ }
+
+ public:
+ AutoLock(AutoLock&& aOther)
+ : mEnv(aOther.mEnv), mInstance(aOther.mInstance) {
+ aOther.mInstance = nullptr;
+ }
+
+ ~AutoLock() { Unlock(); }
+
+ void Unlock() {
+ if (mInstance) {
+ mEnv->MonitorExit(mInstance);
+ mEnv->DeleteLocalRef(mInstance);
+ MOZ_CATCH_JNI_EXCEPTION(mEnv);
+ mInstance = nullptr;
+ }
+ }
+ };
+
+ // Construct a Ref form a raw JNI reference.
+ static Ref<Cls, Type> From(JNIType obj) { return Ref<Cls, Type>(obj); }
+
+ // Construct a Ref form a generic object reference.
+ static Ref<Cls, Type> From(const Ref<Object, jobject>& obj) {
+ return Ref<Cls, Type>(JNIType(obj.Get()));
+ }
+
+ MOZ_IMPLICIT Ref(decltype(nullptr)) : mInstance(nullptr) {}
+
+ // Get the raw JNI reference.
+ JNIType Get() const { return mInstance; }
+
+ template <class T>
+ bool IsInstanceOf() const {
+ return FindEnv()->IsInstanceOf(mInstance, typename T::Context().ClassRef());
+ }
+
+ template <class T>
+ typename T::Ref Cast() const {
+#ifdef MOZ_CHECK_JNI
+ MOZ_RELEASE_ASSERT(FindEnv()->IsAssignableFrom(
+ Context<Cls, Type>().ClassRef(), typename T::Context().ClassRef()));
+#endif
+ return T::Ref::From(*this);
+ }
+
+ AutoLock Lock() const { return AutoLock(mInstance); }
+
+ bool operator==(const Ref& other) const {
+ // Treat two references of the same object as being the same.
+ return mInstance == other.mInstance ||
+ JNI_FALSE != FindEnv()->IsSameObject(mInstance, other.mInstance);
+ }
+
+ bool operator!=(const Ref& other) const { return !operator==(other); }
+
+ bool operator==(decltype(nullptr)) const { return !mInstance; }
+
+ bool operator!=(decltype(nullptr)) const { return !!mInstance; }
+
+ CopyableCtx operator->() const { return CopyableCtx(FindEnv(), mInstance); }
+
+ CopyableCtx operator*() const { return operator->(); }
+
+ // Any ref can be cast to an object ref.
+ operator Ref<Object, jobject>() const {
+ return Ref<Object, jobject>(mInstance);
+ }
+
+ // Null checking (e.g. !!ref) using the safe-bool idiom.
+ operator bool_type() const {
+ return mInstance ? &Self::non_null_reference : nullptr;
+ }
+
+ // We don't allow implicit conversion to jobject because that can lead
+ // to easy mistakes such as assigning a temporary LocalRef to a jobject,
+ // and using the jobject after the LocalRef has been freed.
+
+ // We don't allow explicit conversion, to make outside code use Ref::Get.
+ // Using Ref::Get makes it very easy to see which code is using raw JNI
+ // types to make future refactoring easier.
+
+ // operator JNIType() const = delete;
+};
+
+// Represents a calling context for JNI methods.
+template <class Cls, typename Type>
+class Context : public Ref<Cls, Type> {
+ using Ref = jni::Ref<Cls, Type>;
+
+ static jclass sClassRef; // global reference
+
+ protected:
+ JNIEnv* const mEnv;
+
+ public:
+ Context() : Ref(nullptr), mEnv(Ref::FindEnv()) {}
+
+ Context(JNIEnv* env, Type instance) : Ref(instance), mEnv(env) {}
+
+ jclass ClassRef() const {
+ if (!sClassRef) {
+ const jclass cls = GetClassRef(mEnv, Cls::name);
+ sClassRef = jclass(mEnv->NewGlobalRef(cls));
+ mEnv->DeleteLocalRef(cls);
+ }
+ return sClassRef;
+ }
+
+ JNIEnv* Env() const { return mEnv; }
+
+ template <class T>
+ bool IsInstanceOf() const {
+ return mEnv->IsInstanceOf(Ref::mInstance,
+ typename T::Context(mEnv, nullptr).ClassRef());
+ }
+
+ bool operator==(const Ref& other) const {
+ // Treat two references of the same object as being the same.
+ return Ref::mInstance == other.Get() ||
+ JNI_FALSE != mEnv->IsSameObject(Ref::mInstance, other.Get());
+ }
+
+ bool operator!=(const Ref& other) const { return !operator==(other); }
+
+ bool operator==(decltype(nullptr)) const { return !Ref::mInstance; }
+
+ bool operator!=(decltype(nullptr)) const { return !!Ref::mInstance; }
+
+ Cls operator->() const {
+ MOZ_ASSERT(Ref::mInstance, "Null jobject");
+ return Cls(*this);
+ }
+
+ const Context<Cls, Type>& operator*() const { return *this; }
+};
+
+template <class C, typename T>
+jclass Context<C, T>::sClassRef;
+
+template <class Cls, typename Type = jobject>
+class ObjectBase {
+ protected:
+ const jni::Context<Cls, Type>& mCtx;
+
+ jclass ClassRef() const { return mCtx.ClassRef(); }
+ JNIEnv* Env() const { return mCtx.Env(); }
+ Type Instance() const { return mCtx.Get(); }
+
+ public:
+ using Ref = jni::Ref<Cls, Type>;
+ using Context = jni::Context<Cls, Type>;
+ using LocalRef = jni::LocalRef<Cls>;
+ using GlobalRef = jni::GlobalRef<Cls>;
+ using WeakRef = jni::WeakRef<Cls>;
+ using Param = const Ref&;
+
+ static const CallingThread callingThread = CallingThread::ANY;
+ static const char name[];
+
+ explicit ObjectBase(const Context& ctx) : mCtx(ctx) {}
+
+ Cls* operator->() { return static_cast<Cls*>(this); }
+};
+
+// Binding for a plain jobject.
+class Object : public ObjectBase<Object, jobject> {
+ public:
+ explicit Object(const Context& ctx) : ObjectBase<Object, jobject>(ctx) {}
+};
+
+// Binding for a built-in object reference other than jobject.
+template <typename T>
+class TypedObject : public ObjectBase<TypedObject<T>, T> {
+ public:
+ explicit TypedObject(const Context<TypedObject<T>, T>& ctx)
+ : ObjectBase<TypedObject<T>, T>(ctx) {}
+};
+
+// Binding for a boxed primitive object.
+template <typename T>
+class BoxedObject : public ObjectBase<BoxedObject<T>, jobject> {
+ public:
+ explicit BoxedObject(const Context<BoxedObject<T>, jobject>& ctx)
+ : ObjectBase<BoxedObject<T>, jobject>(ctx) {}
+};
+
+template <>
+const char ObjectBase<Object, jobject>::name[];
+template <>
+const char ObjectBase<TypedObject<jstring>, jstring>::name[];
+template <>
+const char ObjectBase<TypedObject<jclass>, jclass>::name[];
+template <>
+const char ObjectBase<TypedObject<jthrowable>, jthrowable>::name[];
+template <>
+const char ObjectBase<BoxedObject<jboolean>, jobject>::name[];
+template <>
+const char ObjectBase<BoxedObject<jbyte>, jobject>::name[];
+template <>
+const char ObjectBase<BoxedObject<jchar>, jobject>::name[];
+template <>
+const char ObjectBase<BoxedObject<jshort>, jobject>::name[];
+template <>
+const char ObjectBase<BoxedObject<jint>, jobject>::name[];
+template <>
+const char ObjectBase<BoxedObject<jlong>, jobject>::name[];
+template <>
+const char ObjectBase<BoxedObject<jfloat>, jobject>::name[];
+template <>
+const char ObjectBase<BoxedObject<jdouble>, jobject>::name[];
+template <>
+const char ObjectBase<TypedObject<jbooleanArray>, jbooleanArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jbyteArray>, jbyteArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jcharArray>, jcharArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jshortArray>, jshortArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jintArray>, jintArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jlongArray>, jlongArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jfloatArray>, jfloatArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jdoubleArray>, jdoubleArray>::name[];
+template <>
+const char ObjectBase<TypedObject<jobjectArray>, jobjectArray>::name[];
+
+// Define bindings for built-in types.
+using String = TypedObject<jstring>;
+using Class = TypedObject<jclass>;
+using Throwable = TypedObject<jthrowable>;
+
+using Boolean = BoxedObject<jboolean>;
+using Byte = BoxedObject<jbyte>;
+using Character = BoxedObject<jchar>;
+using Short = BoxedObject<jshort>;
+using Integer = BoxedObject<jint>;
+using Long = BoxedObject<jlong>;
+using Float = BoxedObject<jfloat>;
+using Double = BoxedObject<jdouble>;
+
+using BooleanArray = TypedObject<jbooleanArray>;
+using ByteArray = TypedObject<jbyteArray>;
+using CharArray = TypedObject<jcharArray>;
+using ShortArray = TypedObject<jshortArray>;
+using IntArray = TypedObject<jintArray>;
+using LongArray = TypedObject<jlongArray>;
+using FloatArray = TypedObject<jfloatArray>;
+using DoubleArray = TypedObject<jdoubleArray>;
+using ObjectArray = TypedObject<jobjectArray>;
+
+namespace detail {
+
+// See explanation in LocalRef.
+template <class Cls>
+struct GenericObject {
+ using Type = Object;
+};
+template <>
+struct GenericObject<Object> {
+ struct Type {
+ using Ref = jni::Ref<Type, jobject>;
+ using Context = jni::Context<Type, jobject>;
+ };
+};
+template <class Cls>
+struct GenericLocalRef {
+ template <class C>
+ struct Type : jni::Object {};
+};
+template <>
+struct GenericLocalRef<Object> {
+ template <class C>
+ using Type = jni::LocalRef<C>;
+};
+
+} // namespace detail
+
+template <class Cls>
+class LocalRef : public Cls::Context {
+ template <class C>
+ friend class LocalRef;
+
+ using Ctx = typename Cls::Context;
+ using Ref = typename Cls::Ref;
+ using JNIType = typename Ref::JNIType;
+
+ // In order to be able to convert LocalRef<Object> to LocalRef<Cls>, we
+ // need constructors and copy assignment operators that take in a
+ // LocalRef<Object> argument. However, if Cls *is* Object, we would have
+ // duplicated constructors and operators with LocalRef<Object> arguments. To
+ // avoid this conflict, we use GenericObject, which is defined as Object for
+ // LocalRef<non-Object> and defined as a dummy class for LocalRef<Object>.
+ using GenericObject = typename detail::GenericObject<Cls>::Type;
+
+ // Similarly, GenericLocalRef is useed to convert LocalRef<Cls> to,
+ // LocalRef<Object>. It's defined as LocalRef<C> for Cls == Object,
+ // and defined as a dummy template class for Cls != Object.
+ template <class C>
+ using GenericLocalRef =
+ typename detail::GenericLocalRef<Cls>::template Type<C>;
+
+ static JNIType NewLocalRef(JNIEnv* env, JNIType obj) {
+ return JNIType(obj ? env->NewLocalRef(obj) : nullptr);
+ }
+
+ LocalRef(JNIEnv* env, JNIType instance) : Ctx(env, instance) {}
+
+ LocalRef& swap(LocalRef& other) {
+ auto instance = other.mInstance;
+ other.mInstance = Ctx::mInstance;
+ Ctx::mInstance = instance;
+ return *this;
+ }
+
+ public:
+ // Construct a LocalRef from a raw JNI local reference. Unlike Ref::From,
+ // LocalRef::Adopt returns a LocalRef that will delete the local reference
+ // when going out of scope.
+ static LocalRef Adopt(JNIType instance) {
+ return LocalRef(Ref::FindEnv(), instance);
+ }
+
+ static LocalRef Adopt(JNIEnv* env, JNIType instance) {
+ return LocalRef(env, instance);
+ }
+
+ // Copy constructor.
+ LocalRef(const LocalRef<Cls>& ref)
+ : Ctx(ref.mEnv, NewLocalRef(ref.mEnv, ref.mInstance)) {}
+
+ // Move constructor.
+ LocalRef(LocalRef<Cls>&& ref) : Ctx(ref.mEnv, ref.mInstance) {
+ ref.mInstance = nullptr;
+ }
+
+ explicit LocalRef(JNIEnv* env = Ref::FindEnv()) : Ctx(env, nullptr) {}
+
+ // Construct a LocalRef from any Ref,
+ // which means creating a new local reference.
+ MOZ_IMPLICIT LocalRef(const Ref& ref) : Ctx(Ref::FindEnv(), nullptr) {
+ Ctx::mInstance = NewLocalRef(Ctx::mEnv, ref.Get());
+ }
+
+ LocalRef(JNIEnv* env, const Ref& ref)
+ : Ctx(env, NewLocalRef(env, ref.Get())) {}
+
+ // Move a LocalRef<Object> into a LocalRef<Cls> without
+ // creating/deleting local references.
+ MOZ_IMPLICIT LocalRef(LocalRef<GenericObject>&& ref)
+ : Ctx(ref.mEnv, JNIType(ref.mInstance)) {
+ ref.mInstance = nullptr;
+ }
+
+ template <class C>
+ MOZ_IMPLICIT LocalRef(GenericLocalRef<C>&& ref)
+ : Ctx(ref.mEnv, ref.mInstance) {
+ ref.mInstance = nullptr;
+ }
+
+ // Implicitly converts nullptr to LocalRef.
+ MOZ_IMPLICIT LocalRef(decltype(nullptr)) : Ctx(Ref::FindEnv(), nullptr) {}
+
+ ~LocalRef() {
+ if (Ctx::mInstance) {
+ Ctx::mEnv->DeleteLocalRef(Ctx::mInstance);
+ Ctx::mInstance = nullptr;
+ }
+ }
+
+ // Get the raw JNI reference that can be used as a return value.
+ // Returns the same JNI type (jobject, jstring, etc.) as the underlying Ref.
+ typename Ref::JNIType Forget() {
+ const auto obj = Ctx::Get();
+ Ctx::mInstance = nullptr;
+ return obj;
+ }
+
+ LocalRef<Cls>& operator=(LocalRef<Cls> ref) & { return swap(ref); }
+
+ LocalRef<Cls>& operator=(const Ref& ref) & {
+ LocalRef<Cls> newRef(Ctx::mEnv, ref);
+ return swap(newRef);
+ }
+
+ LocalRef<Cls>& operator=(LocalRef<GenericObject>&& ref) & {
+ LocalRef<Cls> newRef(std::move(ref));
+ return swap(newRef);
+ }
+
+ template <class C>
+ LocalRef<Cls>& operator=(GenericLocalRef<C>&& ref) & {
+ LocalRef<Cls> newRef(std::move(ref));
+ return swap(newRef);
+ }
+
+ LocalRef<Cls>& operator=(decltype(nullptr)) & {
+ LocalRef<Cls> newRef(Ctx::mEnv, nullptr);
+ return swap(newRef);
+ }
+};
+
+template <class Cls>
+class GlobalRef : public Cls::Ref {
+ using Ref = typename Cls::Ref;
+ using JNIType = typename Ref::JNIType;
+
+ static JNIType NewGlobalRef(JNIEnv* env, JNIType instance) {
+ return JNIType(instance ? env->NewGlobalRef(instance) : nullptr);
+ }
+
+ GlobalRef& swap(GlobalRef& other) {
+ auto instance = other.mInstance;
+ other.mInstance = Ref::mInstance;
+ Ref::mInstance = instance;
+ return *this;
+ }
+
+ public:
+ GlobalRef() : Ref(nullptr) {}
+
+ // Copy constructor
+ GlobalRef(const GlobalRef& ref)
+ : Ref(NewGlobalRef(GetEnvForThread(), ref.mInstance)) {}
+
+ // Move constructor
+ GlobalRef(GlobalRef&& ref) : Ref(ref.mInstance) { ref.mInstance = nullptr; }
+
+ MOZ_IMPLICIT GlobalRef(const Ref& ref)
+ : Ref(NewGlobalRef(GetEnvForThread(), ref.Get())) {}
+
+ GlobalRef(JNIEnv* env, const Ref& ref) : Ref(NewGlobalRef(env, ref.Get())) {}
+
+ MOZ_IMPLICIT GlobalRef(const LocalRef<Cls>& ref)
+ : Ref(NewGlobalRef(ref.Env(), ref.Get())) {}
+
+ // Implicitly converts nullptr to GlobalRef.
+ MOZ_IMPLICIT GlobalRef(decltype(nullptr)) : Ref(nullptr) {}
+
+ ~GlobalRef() {
+ if (Ref::mInstance) {
+ Clear(GetEnvForThread());
+ }
+ }
+
+ // Get the raw JNI reference that can be used as a return value.
+ // Returns the same JNI type (jobject, jstring, etc.) as the underlying Ref.
+ typename Ref::JNIType Forget() {
+ const auto obj = Ref::Get();
+ Ref::mInstance = nullptr;
+ return obj;
+ }
+
+ void Clear(JNIEnv* env) {
+ if (Ref::mInstance) {
+ env->DeleteGlobalRef(Ref::mInstance);
+ Ref::mInstance = nullptr;
+ }
+ }
+
+ GlobalRef<Cls>& operator=(GlobalRef<Cls> ref) & { return swap(ref); }
+
+ GlobalRef<Cls>& operator=(const Ref& ref) & {
+ GlobalRef<Cls> newRef(ref);
+ return swap(newRef);
+ }
+
+ GlobalRef<Cls>& operator=(const LocalRef<Cls>& ref) & {
+ GlobalRef<Cls> newRef(ref);
+ return swap(newRef);
+ }
+
+ GlobalRef<Cls>& operator=(decltype(nullptr)) & {
+ GlobalRef<Cls> newRef(nullptr);
+ return swap(newRef);
+ }
+};
+
+template <class Cls>
+class WeakRef : public Ref<Cls, jweak> {
+ using Ref = Ref<Cls, jweak>;
+ using JNIType = typename Ref::JNIType;
+
+ static JNIType NewWeakRef(JNIEnv* env, JNIType instance) {
+ return JNIType(instance ? env->NewWeakGlobalRef(instance) : nullptr);
+ }
+
+ WeakRef& swap(WeakRef& other) {
+ auto instance = other.mInstance;
+ other.mInstance = Ref::mInstance;
+ Ref::mInstance = instance;
+ return *this;
+ }
+
+ public:
+ WeakRef() : Ref(nullptr) {}
+
+ // Copy constructor
+ WeakRef(const WeakRef& ref)
+ : Ref(NewWeakRef(GetEnvForThread(), ref.mInstance)) {}
+
+ // Move constructor
+ WeakRef(WeakRef&& ref) : Ref(ref.mInstance) { ref.mInstance = nullptr; }
+
+ MOZ_IMPLICIT WeakRef(const Ref& ref)
+ : Ref(NewWeakRef(GetEnvForThread(), ref.Get())) {}
+
+ WeakRef(JNIEnv* env, const Ref& ref) : Ref(NewWeakRef(env, ref.Get())) {}
+
+ MOZ_IMPLICIT WeakRef(const LocalRef<Cls>& ref)
+ : Ref(NewWeakRef(ref.Env(), ref.Get())) {}
+
+ // Implicitly converts nullptr to WeakRef.
+ MOZ_IMPLICIT WeakRef(decltype(nullptr)) : Ref(nullptr) {}
+
+ ~WeakRef() {
+ if (Ref::mInstance) {
+ Clear(GetEnvForThread());
+ }
+ }
+
+ // Get the raw JNI reference that can be used as a return value.
+ // Returns the same JNI type (jobject, jstring, etc.) as the underlying Ref.
+ typename Ref::JNIType Forget() {
+ const auto obj = Ref::Get();
+ Ref::mInstance = nullptr;
+ return obj;
+ }
+
+ void Clear(JNIEnv* env) {
+ if (Ref::mInstance) {
+ env->DeleteWeakGlobalRef(Ref::mInstance);
+ Ref::mInstance = nullptr;
+ }
+ }
+
+ WeakRef<Cls>& operator=(WeakRef<Cls> ref) & { return swap(ref); }
+
+ WeakRef<Cls>& operator=(const Ref& ref) & {
+ WeakRef<Cls> newRef(ref);
+ return swap(newRef);
+ }
+
+ WeakRef<Cls>& operator=(const LocalRef<Cls>& ref) & {
+ WeakRef<Cls> newRef(ref);
+ return swap(newRef);
+ }
+
+ WeakRef<Cls>& operator=(decltype(nullptr)) & {
+ WeakRef<Cls> newRef(nullptr);
+ return swap(newRef);
+ }
+
+ void operator->() const = delete;
+ void operator*() const = delete;
+};
+
+template <class Cls>
+class DependentRef : public Cls::Ref {
+ using Ref = typename Cls::Ref;
+
+ public:
+ explicit DependentRef(typename Ref::JNIType instance) : Ref(instance) {}
+
+ DependentRef(const DependentRef& ref) : Ref(ref.Get()) {}
+};
+
+class StringParam;
+
+template <>
+class TypedObject<jstring> : public ObjectBase<TypedObject<jstring>, jstring> {
+ using Base = ObjectBase<TypedObject<jstring>, jstring>;
+
+ public:
+ using Param = const StringParam&;
+
+ explicit TypedObject(const Context& ctx) : Base(ctx) {}
+
+ size_t Length() const {
+ const size_t ret = Base::Env()->GetStringLength(Base::Instance());
+ MOZ_CATCH_JNI_EXCEPTION(Base::Env());
+ return ret;
+ }
+
+ nsString ToString() const {
+ const jchar* const str =
+ Base::Env()->GetStringChars(Base::Instance(), nullptr);
+ const jsize len = Base::Env()->GetStringLength(Base::Instance());
+
+ nsString result(reinterpret_cast<const char16_t*>(str), len);
+ Base::Env()->ReleaseStringChars(Base::Instance(), str);
+ return result;
+ }
+
+ nsCString ToCString() const { return NS_ConvertUTF16toUTF8(ToString()); }
+
+ // Convert jstring to a nsString.
+ operator nsString() const { return ToString(); }
+
+ // Convert jstring to a nsCString.
+ operator nsCString() const { return ToCString(); }
+};
+
+// Define a custom parameter type for String,
+// which accepts both String::Ref and nsAString/nsACString
+class StringParam : public String::Ref {
+ using Ref = String::Ref;
+
+ private:
+ // Not null if we should delete ref on destruction.
+ JNIEnv* const mEnv;
+
+ static jstring GetString(JNIEnv* env, const nsAString& str) {
+ const jstring result = env->NewString(
+ reinterpret_cast<const jchar*>(str.BeginReading()), str.Length());
+ if (!result) {
+ NS_ABORT_OOM(str.Length() * sizeof(char16_t));
+ }
+ MOZ_CATCH_JNI_EXCEPTION(env);
+ return result;
+ }
+
+ static jstring GetString(JNIEnv* env, const nsAString& str,
+ const fallible_t&) {
+ const jstring result = env->NewString(
+ reinterpret_cast<const jchar*>(str.BeginReading()), str.Length());
+ if (env->ExceptionCheck()) {
+#ifdef MOZ_CHECK_JNI
+ env->ExceptionDescribe();
+#endif
+ env->ExceptionClear();
+ }
+ return result;
+ }
+
+ static jstring GetString(JNIEnv* env, const nsACString& str,
+ const fallible_t& aFallible) {
+ nsAutoString utf16;
+ if (!CopyUTF8toUTF16(str, utf16, aFallible)) {
+ return nullptr;
+ }
+ return GetString(env, utf16, aFallible);
+ }
+
+ public:
+ MOZ_IMPLICIT StringParam(decltype(nullptr)) : Ref(nullptr), mEnv(nullptr) {}
+
+ MOZ_IMPLICIT StringParam(const Ref& ref) : Ref(ref.Get()), mEnv(nullptr) {}
+
+ MOZ_IMPLICIT StringParam(const nsAString& str, JNIEnv* env = Ref::FindEnv())
+ : Ref(GetString(env, str)), mEnv(env) {}
+
+ MOZ_IMPLICIT StringParam(const nsAString& str, JNIEnv* env,
+ const fallible_t& aFallible)
+ : Ref(GetString(env, str, aFallible)), mEnv(env) {}
+
+ MOZ_IMPLICIT StringParam(const nsLiteralString& str,
+ JNIEnv* env = Ref::FindEnv())
+ : Ref(GetString(env, str)), mEnv(env) {}
+
+ MOZ_IMPLICIT StringParam(const char16_t* str, JNIEnv* env = Ref::FindEnv())
+ : Ref(GetString(env, nsDependentString(str))), mEnv(env) {}
+
+ MOZ_IMPLICIT StringParam(const nsACString& str, JNIEnv* env = Ref::FindEnv())
+ : Ref(GetString(env, NS_ConvertUTF8toUTF16(str))), mEnv(env) {}
+
+ MOZ_IMPLICIT StringParam(const nsACString& str, JNIEnv* env,
+ const fallible_t& aFallible)
+ : Ref(GetString(env, str, aFallible)), mEnv(env) {}
+
+ MOZ_IMPLICIT StringParam(const nsLiteralCString& str,
+ JNIEnv* env = Ref::FindEnv())
+ : Ref(GetString(env, NS_ConvertUTF8toUTF16(str))), mEnv(env) {}
+
+ MOZ_IMPLICIT StringParam(const char* str, JNIEnv* env = Ref::FindEnv())
+ : Ref(GetString(env, NS_ConvertUTF8toUTF16(str))), mEnv(env) {}
+
+ StringParam(StringParam&& other) : Ref(other.Get()), mEnv(other.mEnv) {
+ other.mInstance = nullptr;
+ }
+
+ ~StringParam() {
+ if (mEnv && Get()) {
+ mEnv->DeleteLocalRef(Get());
+ }
+ }
+
+ operator String::LocalRef() const {
+ // We can't return our existing ref because the returned
+ // LocalRef could be freed first, so we need a new local ref.
+ return String::LocalRef(mEnv ? mEnv : Ref::FindEnv(), *this);
+ }
+};
+
+namespace detail {
+template <typename T>
+struct TypeAdapter;
+}
+
+// Ref specialization for arrays.
+template <typename JNIType, class ElementType>
+class ArrayRefBase : public ObjectBase<TypedObject<JNIType>, JNIType> {
+ protected:
+ using Base = ObjectBase<TypedObject<JNIType>, JNIType>;
+
+ public:
+ explicit ArrayRefBase(const Context<TypedObject<JNIType>, JNIType>& ctx)
+ : Base(ctx) {}
+
+ static typename Base::LocalRef New(const ElementType* data, size_t length) {
+ using JNIElemType = typename detail::TypeAdapter<ElementType>::JNIType;
+ static_assert(sizeof(ElementType) == sizeof(JNIElemType),
+ "Size of native type must match size of JNI type");
+ JNIEnv* const jenv = mozilla::jni::GetEnvForThread();
+ auto result = (jenv->*detail::TypeAdapter<ElementType>::NewArray)(length);
+ MOZ_CATCH_JNI_EXCEPTION(jenv);
+ (jenv->*detail::TypeAdapter<ElementType>::SetArray)(
+ result, jsize(0), length, reinterpret_cast<const JNIElemType*>(data));
+ MOZ_CATCH_JNI_EXCEPTION(jenv);
+ return Base::LocalRef::Adopt(jenv, result);
+ }
+
+ static typename Base::LocalRef New(const ElementType* data, size_t length,
+ const fallible_t&) {
+ using JNIElemType = typename detail::TypeAdapter<ElementType>::JNIType;
+ static_assert(sizeof(ElementType) == sizeof(JNIElemType),
+ "Size of native type must match size of JNI type");
+ JNIEnv* const jenv = mozilla::jni::GetEnvForThread();
+ auto result = (jenv->*detail::TypeAdapter<ElementType>::NewArray)(length);
+ if (jenv->ExceptionCheck()) {
+ if (!IsOOMException(jenv)) {
+ // This exception isn't excepted due not to OOM. This is unrecoverable
+ // error.
+ MOZ_CATCH_JNI_EXCEPTION(jenv);
+ }
+#ifdef MOZ_CHECK_JNI
+ jenv->ExceptionDescribe();
+#endif
+ jenv->ExceptionClear();
+ return Base::LocalRef::Adopt(jenv, nullptr);
+ }
+ (jenv->*detail::TypeAdapter<ElementType>::SetArray)(
+ result, jsize(0), length, reinterpret_cast<const JNIElemType*>(data));
+ if (jenv->ExceptionCheck()) {
+ if (!IsOOMException(jenv)) {
+ // This exception isn't excepted due not to OOM. This is unrecoverable
+ // error.
+ MOZ_CATCH_JNI_EXCEPTION(jenv);
+ }
+#ifdef MOZ_CHECK_JNI
+ jenv->ExceptionDescribe();
+#endif
+ jenv->ExceptionClear();
+ jenv->DeleteLocalRef(result);
+ return Base::LocalRef::Adopt(jenv, nullptr);
+ }
+ return Base::LocalRef::Adopt(jenv, result);
+ }
+
+ size_t Length() const {
+ const size_t ret = Base::Env()->GetArrayLength(Base::Instance());
+ MOZ_CATCH_JNI_EXCEPTION(Base::Env());
+ return ret;
+ }
+
+ ElementType GetElement(size_t index) const {
+ using JNIElemType = typename detail::TypeAdapter<ElementType>::JNIType;
+ static_assert(sizeof(ElementType) == sizeof(JNIElemType),
+ "Size of native type must match size of JNI type");
+
+ ElementType ret;
+ (Base::Env()->*detail::TypeAdapter<ElementType>::GetArray)(
+ Base::Instance(), jsize(index), 1,
+ reinterpret_cast<JNIElemType*>(&ret));
+ MOZ_CATCH_JNI_EXCEPTION(Base::Env());
+ return ret;
+ }
+
+ nsTArray<ElementType> GetElements() const {
+ using JNIElemType = typename detail::TypeAdapter<ElementType>::JNIType;
+ static_assert(sizeof(ElementType) == sizeof(JNIElemType),
+ "Size of native type must match size of JNI type");
+
+ const size_t len = size_t(Base::Env()->GetArrayLength(Base::Instance()));
+
+ nsTArray<ElementType> array(len);
+ array.SetLength(len);
+ CopyTo(array.Elements(), len);
+ return array;
+ }
+
+ // returns number of elements copied
+ size_t CopyTo(ElementType* buffer, size_t size) const {
+ using JNIElemType = typename detail::TypeAdapter<ElementType>::JNIType;
+ static_assert(sizeof(ElementType) == sizeof(JNIElemType),
+ "Size of native type must match size of JNI type");
+
+ const size_t len = size_t(Base::Env()->GetArrayLength(Base::Instance()));
+ const size_t amountToCopy = (len > size ? size : len);
+ (Base::Env()->*detail::TypeAdapter<ElementType>::GetArray)(
+ Base::Instance(), 0, jsize(amountToCopy),
+ reinterpret_cast<JNIElemType*>(buffer));
+ return amountToCopy;
+ }
+
+ ElementType operator[](size_t index) const { return GetElement(index); }
+
+ operator nsTArray<ElementType>() const { return GetElements(); }
+};
+
+#define DEFINE_PRIMITIVE_ARRAY_REF_HEADER(JNIType, ElementType) \
+ template <> \
+ class TypedObject<JNIType> : public ArrayRefBase<JNIType, ElementType> { \
+ public: \
+ explicit TypedObject(const Context& ctx) \
+ : ArrayRefBase<JNIType, ElementType>(ctx) {} \
+ static typename Base::LocalRef From(const nsTArray<ElementType>& aArray) { \
+ return New(aArray.Elements(), aArray.Length()); \
+ }
+
+#define DEFINE_PRIMITIVE_ARRAY_REF_FOOTER }
+
+#define DEFINE_PRIMITIVE_ARRAY_REF_FROM_IMPLICIT_CONVERSION(ElementType, \
+ ConvertFromType) \
+ static typename Base::LocalRef From( \
+ const nsTArray<ConvertFromType>& aArray) { \
+ return New(reinterpret_cast<const ElementType*>(aArray.Elements()), \
+ aArray.Length()); \
+ }
+
+#define DEFINE_PRIMITIVE_ARRAY_REF(JNIType, ElementType) \
+ DEFINE_PRIMITIVE_ARRAY_REF_HEADER(JNIType, ElementType) \
+ DEFINE_PRIMITIVE_ARRAY_REF_FOOTER
+
+#define DEFINE_PRIMITIVE_ARRAY_REF_WITH_IMPLICIT_CONVERSION( \
+ JNIType, ElementType, ConvertFromType) \
+ DEFINE_PRIMITIVE_ARRAY_REF_HEADER(JNIType, ElementType) \
+ DEFINE_PRIMITIVE_ARRAY_REF_FROM_IMPLICIT_CONVERSION(ElementType, \
+ ConvertFromType) \
+ DEFINE_PRIMITIVE_ARRAY_REF_FOOTER
+
+DEFINE_PRIMITIVE_ARRAY_REF(jbooleanArray, bool);
+DEFINE_PRIMITIVE_ARRAY_REF_WITH_IMPLICIT_CONVERSION(jbyteArray, int8_t,
+ uint8_t);
+DEFINE_PRIMITIVE_ARRAY_REF(jcharArray, char16_t);
+DEFINE_PRIMITIVE_ARRAY_REF_WITH_IMPLICIT_CONVERSION(jshortArray, int16_t,
+ uint16_t);
+DEFINE_PRIMITIVE_ARRAY_REF_WITH_IMPLICIT_CONVERSION(jintArray, int32_t,
+ uint32_t);
+DEFINE_PRIMITIVE_ARRAY_REF(jfloatArray, float);
+DEFINE_PRIMITIVE_ARRAY_REF_WITH_IMPLICIT_CONVERSION(jlongArray, int64_t,
+ uint64_t);
+DEFINE_PRIMITIVE_ARRAY_REF(jdoubleArray, double);
+
+#undef DEFINE_PRIMITIVE_ARRAY_REF
+#undef DEFINE_PRIMITIVE_ARRAY_REF_WITH_IMPLICIT_CONVERSION
+#undef DEFINE_PRIMITIVE_ARRAY_HEADER
+#undef DEFINE_PRIMITIVE_ARRAY_FROM_IMPLICIT_CONVERSION
+#undef DEFINE_PRIMITIVE_ARRAY_FOOTER
+
+class ByteBuffer : public ObjectBase<ByteBuffer, jobject> {
+ public:
+ explicit ByteBuffer(const Context& ctx)
+ : ObjectBase<ByteBuffer, jobject>(ctx) {}
+
+ static LocalRef New(void* data, size_t capacity) {
+ JNIEnv* const env = GetEnvForThread();
+ const auto ret =
+ LocalRef::Adopt(env, env->NewDirectByteBuffer(data, jlong(capacity)));
+ MOZ_CATCH_JNI_EXCEPTION(env);
+ return ret;
+ }
+
+ static LocalRef New(void* data, size_t capacity, const fallible_t&) {
+ JNIEnv* const env = GetEnvForThread();
+ const jobject result = env->NewDirectByteBuffer(data, jlong(capacity));
+ if (env->ExceptionCheck()) {
+ if (!IsOOMException(env)) {
+ // This exception isn't excepted due not to OOM. This is unrecoverable
+ // error.
+ MOZ_CATCH_JNI_EXCEPTION(env);
+ }
+#ifdef MOZ_CHECK_JNI
+ env->ExceptionDescribe();
+#endif
+ env->ExceptionClear();
+ return LocalRef::Adopt(env, nullptr);
+ }
+ return LocalRef::Adopt(env, result);
+ }
+
+ void* Address() {
+ void* const ret = Env()->GetDirectBufferAddress(Instance());
+ MOZ_CATCH_JNI_EXCEPTION(Env());
+ return ret;
+ }
+
+ size_t Capacity() {
+ const size_t ret = size_t(Env()->GetDirectBufferCapacity(Instance()));
+ MOZ_CATCH_JNI_EXCEPTION(Env());
+ return ret;
+ }
+};
+
+template <>
+const char ObjectBase<ByteBuffer, jobject>::name[];
+
+template <>
+class TypedObject<jobjectArray>
+ : public ObjectBase<TypedObject<jobjectArray>, jobjectArray> {
+ using Base = ObjectBase<TypedObject<jobjectArray>, jobjectArray>;
+
+ public:
+ template <class Cls = Object>
+ static Base::LocalRef New(size_t length,
+ typename Cls::Param initialElement = nullptr) {
+ JNIEnv* const env = GetEnvForThread();
+ jobjectArray array = env->NewObjectArray(
+ jsize(length), typename Cls::Context(env, nullptr).ClassRef(),
+ initialElement.Get());
+ MOZ_CATCH_JNI_EXCEPTION(env);
+ return Base::LocalRef::Adopt(env, array);
+ }
+
+ explicit TypedObject(const Context& ctx) : Base(ctx) {}
+
+ size_t Length() const {
+ const size_t ret = Base::Env()->GetArrayLength(Base::Instance());
+ MOZ_CATCH_JNI_EXCEPTION(Base::Env());
+ return ret;
+ }
+
+ Object::LocalRef GetElement(size_t index) const {
+ auto ret = Object::LocalRef::Adopt(
+ Base::Env(),
+ Base::Env()->GetObjectArrayElement(Base::Instance(), jsize(index)));
+ MOZ_CATCH_JNI_EXCEPTION(Base::Env());
+ return ret;
+ }
+
+ nsTArray<Object::LocalRef> GetElements() const {
+ const jsize len = size_t(Base::Env()->GetArrayLength(Base::Instance()));
+
+ nsTArray<Object::LocalRef> array((size_t(len)));
+ for (jsize i = 0; i < len; i++) {
+ array.AppendElement(Object::LocalRef::Adopt(
+ Base::Env(),
+ Base::Env()->GetObjectArrayElement(Base::Instance(), i)));
+ MOZ_CATCH_JNI_EXCEPTION(Base::Env());
+ }
+ return array;
+ }
+
+ Object::LocalRef operator[](size_t index) const { return GetElement(index); }
+
+ operator nsTArray<Object::LocalRef>() const { return GetElements(); }
+
+ void SetElement(size_t index, Object::Param element) const {
+ Base::Env()->SetObjectArrayElement(Base::Instance(), jsize(index),
+ element.Get());
+ MOZ_CATCH_JNI_EXCEPTION(Base::Env());
+ }
+};
+
+// Support conversion from LocalRef<T>* to LocalRef<Object>*:
+// LocalRef<Foo> foo;
+// Foo::GetFoo(&foo); // error because parameter type is LocalRef<Object>*.
+// Foo::GetFoo(ReturnTo(&foo)); // OK because ReturnTo converts the argument.
+template <class Cls>
+class ReturnToLocal {
+ private:
+ LocalRef<Cls>* const localRef;
+ LocalRef<Object> objRef;
+
+ public:
+ explicit ReturnToLocal(LocalRef<Cls>* ref) : localRef(ref) {}
+ operator LocalRef<Object>*() { return &objRef; }
+
+ ~ReturnToLocal() {
+ if (objRef) {
+ *localRef = std::move(objRef);
+ }
+ }
+};
+
+template <class Cls>
+ReturnToLocal<Cls> ReturnTo(LocalRef<Cls>* ref) {
+ return ReturnToLocal<Cls>(ref);
+}
+
+// Support conversion from GlobalRef<T>* to LocalRef<Object/T>*:
+// GlobalRef<Foo> foo;
+// Foo::GetFoo(&foo); // error because parameter type is LocalRef<Foo>*.
+// Foo::GetFoo(ReturnTo(&foo)); // OK because ReturnTo converts the argument.
+template <class Cls>
+class ReturnToGlobal {
+ private:
+ GlobalRef<Cls>* const globalRef;
+ LocalRef<Object> objRef;
+ LocalRef<Cls> clsRef;
+
+ public:
+ explicit ReturnToGlobal(GlobalRef<Cls>* ref) : globalRef(ref) {}
+ operator LocalRef<Object>*() { return &objRef; }
+ operator LocalRef<Cls>*() { return &clsRef; }
+
+ ~ReturnToGlobal() {
+ if (objRef) {
+ *globalRef = (clsRef = std::move(objRef));
+ } else if (clsRef) {
+ *globalRef = clsRef;
+ }
+ }
+};
+
+template <class Cls>
+ReturnToGlobal<Cls> ReturnTo(GlobalRef<Cls>* ref) {
+ return ReturnToGlobal<Cls>(ref);
+}
+
+// Make a LocalRef<T> from any other Ref<T>
+template <typename Cls, typename JNIType>
+LocalRef<Cls> ToLocalRef(const Ref<Cls, JNIType>& aRef) {
+ return LocalRef<Cls>(aRef);
+}
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_Refs_h__
diff --git a/widget/android/jni/TypeAdapter.h b/widget/android/jni/TypeAdapter.h
new file mode 100644
index 0000000000..5ab1e14bf5
--- /dev/null
+++ b/widget/android/jni/TypeAdapter.h
@@ -0,0 +1,71 @@
+/* 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/. */
+
+#ifndef mozilla_jni_TypeAdapter_h__
+#define mozilla_jni_TypeAdapter_h__
+
+#include <jni.h>
+
+#include "mozilla/jni/Refs.h"
+
+namespace mozilla {
+namespace jni {
+namespace detail {
+
+// TypeAdapter specializations are the interfaces between native/C++ types such
+// as int32_t and JNI types such as jint. The template parameter T is the native
+// type, and each TypeAdapter specialization can have the following members:
+//
+// * Call: JNIEnv member pointer for making a method call that returns T.
+// * StaticCall: JNIEnv member pointer for making a static call that returns T.
+// * Get: JNIEnv member pointer for getting a field of type T.
+// * StaticGet: JNIEnv member pointer for getting a static field of type T.
+// * Set: JNIEnv member pointer for setting a field of type T.
+// * StaticGet: JNIEnv member pointer for setting a static field of type T.
+// * ToNative: static function that converts the JNI type to the native type.
+// * FromNative: static function that converts the native type to the JNI type.
+
+template <typename T>
+struct TypeAdapter;
+
+#define DEFINE_PRIMITIVE_TYPE_ADAPTER(NativeType, JNIType, JNIName) \
+ \
+ template <> \
+ struct TypeAdapter<NativeType> { \
+ using JNI##Type = JNIType; \
+ \
+ static constexpr auto Call = &JNIEnv::Call##JNIName##MethodA; \
+ static constexpr auto StaticCall = &JNIEnv::CallStatic##JNIName##MethodA; \
+ static constexpr auto Get = &JNIEnv::Get##JNIName##Field; \
+ static constexpr auto StaticGet = &JNIEnv::GetStatic##JNIName##Field; \
+ static constexpr auto Set = &JNIEnv::Set##JNIName##Field; \
+ static constexpr auto StaticSet = &JNIEnv::SetStatic##JNIName##Field; \
+ static constexpr auto GetArray = &JNIEnv::Get##JNIName##ArrayRegion; \
+ static constexpr auto SetArray = &JNIEnv::Set##JNIName##ArrayRegion; \
+ static constexpr auto NewArray = &JNIEnv::New##JNIName##Array; \
+ \
+ static JNIType FromNative(JNIEnv*, NativeType val) { \
+ return static_cast<JNIType>(val); \
+ } \
+ static NativeType ToNative(JNIEnv*, JNIType val) { \
+ return static_cast<NativeType>(val); \
+ } \
+ }
+
+DEFINE_PRIMITIVE_TYPE_ADAPTER(bool, jboolean, Boolean);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int8_t, jbyte, Byte);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(char16_t, jchar, Char);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int16_t, jshort, Short);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int32_t, jint, Int);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int64_t, jlong, Long);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(float, jfloat, Float);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(double, jdouble, Double);
+
+#undef DEFINE_PRIMITIVE_TYPE_ADAPTER
+
+} // namespace detail
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_Types_h__
diff --git a/widget/android/jni/Types.h b/widget/android/jni/Types.h
new file mode 100644
index 0000000000..1fe5fa4400
--- /dev/null
+++ b/widget/android/jni/Types.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_Types_h__
+#define mozilla_jni_Types_h__
+
+#include <jni.h>
+
+#include "mozilla/jni/Refs.h"
+#include "mozilla/jni/TypeAdapter.h"
+
+namespace mozilla {
+namespace jni {
+namespace detail {
+
+// TypeAdapter specializations are the interfaces between native/C++ types such
+// as int32_t and JNI types such as jint. The template parameter T is the native
+// type, and each TypeAdapter specialization can have the following members:
+//
+// * Call: JNIEnv member pointer for making a method call that returns T.
+// * StaticCall: JNIEnv member pointer for making a static call that returns T.
+// * Get: JNIEnv member pointer for getting a field of type T.
+// * StaticGet: JNIEnv member pointer for getting a static field of type T.
+// * Set: JNIEnv member pointer for setting a field of type T.
+// * StaticGet: JNIEnv member pointer for setting a static field of type T.
+// * ToNative: static function that converts the JNI type to the native type.
+// * FromNative: static function that converts the native type to the JNI type.
+
+// TypeAdapter<LocalRef<Cls>> applies when jobject is a return value.
+template <class Cls>
+struct TypeAdapter<LocalRef<Cls>> {
+ using JNIType = typename Cls::Ref::JNIType;
+
+ static constexpr auto Call = &JNIEnv::CallObjectMethodA;
+ static constexpr auto StaticCall = &JNIEnv::CallStaticObjectMethodA;
+ static constexpr auto Get = &JNIEnv::GetObjectField;
+ static constexpr auto StaticGet = &JNIEnv::GetStaticObjectField;
+
+ // Declare instance as jobject because JNI methods return
+ // jobject even if the return value is really jstring, etc.
+ static LocalRef<Cls> ToNative(JNIEnv* env, jobject instance) {
+ return LocalRef<Cls>::Adopt(env, JNIType(instance));
+ }
+
+ static JNIType FromNative(JNIEnv*, LocalRef<Cls>&& instance) {
+ return instance.Forget();
+ }
+};
+
+// clang is picky about function types, including attributes that modify the
+// calling convention, lining up. GCC appears to be somewhat less so.
+#ifdef __clang__
+# define MOZ_JNICALL_ABI JNICALL
+#else
+# define MOZ_JNICALL_ABI
+#endif
+
+// NDK r18 made jvalue* method parameters const. We detect the change directly
+// instead of using ndk-version.h in order to remain compatible with r15 for
+// now, which doesn't include those headers.
+class CallArgs {
+ static const jvalue* test(void (JNIEnv::*)(jobject, jmethodID,
+ const jvalue*));
+ static jvalue* test(void (JNIEnv::*)(jobject, jmethodID, jvalue*));
+
+ public:
+ using JValueType = decltype(test(&JNIEnv::CallVoidMethodA));
+};
+
+template <class Cls>
+constexpr jobject (JNIEnv::*TypeAdapter<LocalRef<Cls>>::Call)(
+ jobject, jmethodID, CallArgs::JValueType) MOZ_JNICALL_ABI;
+template <class Cls>
+constexpr jobject (JNIEnv::*TypeAdapter<LocalRef<Cls>>::StaticCall)(
+ jclass, jmethodID, CallArgs::JValueType) MOZ_JNICALL_ABI;
+template <class Cls>
+constexpr jobject (JNIEnv::*TypeAdapter<LocalRef<Cls>>::Get)(jobject, jfieldID);
+template <class Cls>
+constexpr jobject (JNIEnv::*TypeAdapter<LocalRef<Cls>>::StaticGet)(jclass,
+ jfieldID);
+
+// TypeAdapter<Ref<Cls>> applies when jobject is a parameter value.
+template <class Cls, typename T>
+struct TypeAdapter<Ref<Cls, T>> {
+ using JNIType = typename Ref<Cls, T>::JNIType;
+
+ static constexpr auto Set = &JNIEnv::SetObjectField;
+ static constexpr auto StaticSet = &JNIEnv::SetStaticObjectField;
+
+ static DependentRef<Cls> ToNative(JNIEnv* env, JNIType instance) {
+ return DependentRef<Cls>(instance);
+ }
+
+ static JNIType FromNative(JNIEnv*, const Ref<Cls, T>& instance) {
+ return instance.Get();
+ }
+};
+
+template <class Cls, typename T>
+constexpr void (JNIEnv::*TypeAdapter<Ref<Cls, T>>::Set)(jobject, jfieldID,
+ jobject);
+template <class Cls, typename T>
+constexpr void (JNIEnv::*TypeAdapter<Ref<Cls, T>>::StaticSet)(jclass, jfieldID,
+ jobject);
+
+// jstring has its own Param type.
+template <>
+struct TypeAdapter<StringParam> : public TypeAdapter<String::Ref> {};
+
+template <class Cls>
+struct TypeAdapter<const Cls&> : public TypeAdapter<Cls> {};
+
+} // namespace detail
+
+using namespace detail;
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_Types_h__
diff --git a/widget/android/jni/Utils.cpp b/widget/android/jni/Utils.cpp
new file mode 100644
index 0000000000..08164e4950
--- /dev/null
+++ b/widget/android/jni/Utils.cpp
@@ -0,0 +1,348 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "Utils.h"
+#include "Types.h"
+
+#include <android/log.h>
+#include <pthread.h>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+#include "mozilla/java/GeckoThreadWrappers.h"
+
+#include "AndroidBuild.h"
+#include "nsAppShell.h"
+#include "nsExceptionHandler.h"
+
+namespace mozilla {
+namespace jni {
+
+namespace detail {
+
+#define DEFINE_PRIMITIVE_TYPE_ADAPTER(NativeType, JNIType, JNIName, ABIName) \
+ \
+ constexpr JNIType (JNIEnv::*TypeAdapter<NativeType>::Call)( \
+ jobject, jmethodID, CallArgs::JValueType) MOZ_JNICALL_ABI; \
+ constexpr JNIType (JNIEnv::*TypeAdapter<NativeType>::StaticCall)( \
+ jclass, jmethodID, CallArgs::JValueType) MOZ_JNICALL_ABI; \
+ constexpr JNIType (JNIEnv::*TypeAdapter<NativeType>::Get)(jobject, jfieldID) \
+ ABIName; \
+ constexpr JNIType (JNIEnv::*TypeAdapter<NativeType>::StaticGet)( \
+ jclass, jfieldID) ABIName; \
+ constexpr void (JNIEnv::*TypeAdapter<NativeType>::Set)(jobject, jfieldID, \
+ JNIType) ABIName; \
+ constexpr void (JNIEnv::*TypeAdapter<NativeType>::StaticSet)( \
+ jclass, jfieldID, JNIType) ABIName; \
+ constexpr void (JNIEnv::*TypeAdapter<NativeType>::GetArray)( \
+ JNIType##Array, jsize, jsize, JNIType*)
+
+DEFINE_PRIMITIVE_TYPE_ADAPTER(bool, jboolean, Boolean, /*nothing*/);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int8_t, jbyte, Byte, /*nothing*/);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(char16_t, jchar, Char, /*nothing*/);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int16_t, jshort, Short, /*nothing*/);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int32_t, jint, Int, /*nothing*/);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(int64_t, jlong, Long, /*nothing*/);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(float, jfloat, Float, MOZ_JNICALL_ABI);
+DEFINE_PRIMITIVE_TYPE_ADAPTER(double, jdouble, Double, MOZ_JNICALL_ABI);
+
+#undef DEFINE_PRIMITIVE_TYPE_ADAPTER
+
+} // namespace detail
+
+template <>
+const char ObjectBase<Object, jobject>::name[] = "java/lang/Object";
+template <>
+const char ObjectBase<TypedObject<jstring>, jstring>::name[] =
+ "java/lang/String";
+template <>
+const char ObjectBase<TypedObject<jclass>, jclass>::name[] = "java/lang/Class";
+template <>
+const char ObjectBase<TypedObject<jthrowable>, jthrowable>::name[] =
+ "java/lang/Throwable";
+template <>
+const char ObjectBase<BoxedObject<jboolean>, jobject>::name[] =
+ "java/lang/Boolean";
+template <>
+const char ObjectBase<BoxedObject<jbyte>, jobject>::name[] = "java/lang/Byte";
+template <>
+const char ObjectBase<BoxedObject<jchar>, jobject>::name[] =
+ "java/lang/Character";
+template <>
+const char ObjectBase<BoxedObject<jshort>, jobject>::name[] = "java/lang/Short";
+template <>
+const char ObjectBase<BoxedObject<jint>, jobject>::name[] = "java/lang/Integer";
+template <>
+const char ObjectBase<BoxedObject<jlong>, jobject>::name[] = "java/lang/Long";
+template <>
+const char ObjectBase<BoxedObject<jfloat>, jobject>::name[] = "java/lang/Float";
+template <>
+const char ObjectBase<BoxedObject<jdouble>, jobject>::name[] =
+ "java/lang/Double";
+template <>
+const char ObjectBase<TypedObject<jbooleanArray>, jbooleanArray>::name[] = "[Z";
+template <>
+const char ObjectBase<TypedObject<jbyteArray>, jbyteArray>::name[] = "[B";
+template <>
+const char ObjectBase<TypedObject<jcharArray>, jcharArray>::name[] = "[C";
+template <>
+const char ObjectBase<TypedObject<jshortArray>, jshortArray>::name[] = "[S";
+template <>
+const char ObjectBase<TypedObject<jintArray>, jintArray>::name[] = "[I";
+template <>
+const char ObjectBase<TypedObject<jlongArray>, jlongArray>::name[] = "[J";
+template <>
+const char ObjectBase<TypedObject<jfloatArray>, jfloatArray>::name[] = "[F";
+template <>
+const char ObjectBase<TypedObject<jdoubleArray>, jdoubleArray>::name[] = "[D";
+template <>
+const char ObjectBase<TypedObject<jobjectArray>, jobjectArray>::name[] =
+ "[Ljava/lang/Object;";
+template <>
+const char ObjectBase<ByteBuffer, jobject>::name[] = "java/nio/ByteBuffer";
+
+JavaVM* sJavaVM;
+JNIEnv* sGeckoThreadEnv;
+
+namespace {
+
+pthread_key_t sThreadEnvKey;
+jclass sOOMErrorClass;
+jobject sClassLoader;
+jmethodID sClassLoaderLoadClass;
+
+void UnregisterThreadEnv(void* env) {
+ if (!env) {
+ // We were never attached.
+ return;
+ }
+ // The thread may have already been detached. In that case, it's still
+ // okay to call DetachCurrentThread(); it'll simply return an error.
+ // However, we must not access | env | because it may be invalid.
+ MOZ_ASSERT(sJavaVM);
+ sJavaVM->DetachCurrentThread();
+}
+
+} // namespace
+
+void SetGeckoThreadEnv(JNIEnv* aEnv) {
+ MOZ_ASSERT(aEnv);
+ MOZ_ASSERT(!sGeckoThreadEnv || sGeckoThreadEnv == aEnv);
+
+ if (!sGeckoThreadEnv &&
+ pthread_key_create(&sThreadEnvKey, UnregisterThreadEnv)) {
+ MOZ_CRASH("Failed to initialize required TLS");
+ }
+
+ sGeckoThreadEnv = aEnv;
+ MOZ_ALWAYS_TRUE(!pthread_setspecific(sThreadEnvKey, aEnv));
+
+ MOZ_ALWAYS_TRUE(!aEnv->GetJavaVM(&sJavaVM));
+ MOZ_ASSERT(sJavaVM);
+
+ sOOMErrorClass =
+ Class::GlobalRef(
+ Class::LocalRef::Adopt(aEnv->FindClass("java/lang/OutOfMemoryError")))
+ .Forget();
+ aEnv->ExceptionClear();
+
+ sClassLoader = Object::GlobalRef(java::GeckoThread::ClsLoader()).Forget();
+ sClassLoaderLoadClass = aEnv->GetMethodID(
+ Class::LocalRef::Adopt(aEnv->GetObjectClass(sClassLoader)).Get(),
+ "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
+ MOZ_ASSERT(sClassLoader && sClassLoaderLoadClass);
+}
+
+JNIEnv* GetEnvForThread() {
+ MOZ_ASSERT(sGeckoThreadEnv);
+
+ JNIEnv* env = static_cast<JNIEnv*>(pthread_getspecific(sThreadEnvKey));
+ if (env) {
+ return env;
+ }
+
+ // We don't have a saved JNIEnv, so try to get one.
+ // AttachCurrentThread() does the same thing as GetEnv() when a thread is
+ // already attached, so we don't have to call GetEnv() at all.
+ if (!sJavaVM->AttachCurrentThread(&env, nullptr)) {
+ MOZ_ASSERT(env);
+ MOZ_ALWAYS_TRUE(!pthread_setspecific(sThreadEnvKey, env));
+ return env;
+ }
+
+ MOZ_CRASH("Failed to get JNIEnv for thread");
+ return nullptr; // unreachable
+}
+
+bool ThrowException(JNIEnv* aEnv, const char* aClass, const char* aMessage) {
+ MOZ_ASSERT(aEnv, "Invalid thread JNI env");
+
+ Class::LocalRef cls = Class::LocalRef::Adopt(aEnv->FindClass(aClass));
+ MOZ_ASSERT(cls, "Cannot find exception class");
+
+ return !aEnv->ThrowNew(cls.Get(), aMessage);
+}
+
+bool HandleUncaughtException(JNIEnv* aEnv) {
+ MOZ_ASSERT(aEnv, "Invalid thread JNI env");
+
+ if (!aEnv->ExceptionCheck()) {
+ return false;
+ }
+
+#ifdef MOZ_CHECK_JNI
+ aEnv->ExceptionDescribe();
+#endif
+
+ Throwable::LocalRef e =
+ Throwable::LocalRef::Adopt(aEnv, aEnv->ExceptionOccurred());
+ MOZ_ASSERT(e);
+ aEnv->ExceptionClear();
+
+ String::LocalRef stack = java::GeckoAppShell::GetExceptionStackTrace(e);
+ if (stack && ReportException(aEnv, e.Get(), stack.Get())) {
+ return true;
+ }
+
+ aEnv->ExceptionClear();
+ java::GeckoAppShell::HandleUncaughtException(e);
+
+ if (NS_WARN_IF(aEnv->ExceptionCheck())) {
+ aEnv->ExceptionDescribe();
+ aEnv->ExceptionClear();
+ }
+
+ return true;
+}
+
+bool ReportException(JNIEnv* aEnv, jthrowable aExc, jstring aStack) {
+ bool result = true;
+
+ result &= NS_SUCCEEDED(CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::JavaStackTrace,
+ String::Ref::From(aStack)->ToCString()));
+
+ auto appNotes = java::GeckoAppShell::GetAppNotes();
+ if (NS_WARN_IF(aEnv->ExceptionCheck())) {
+ aEnv->ExceptionDescribe();
+ aEnv->ExceptionClear();
+ } else if (appNotes) {
+ CrashReporter::AppendAppNotesToCrashReport("\n"_ns + appNotes->ToCString());
+ }
+
+ if (sOOMErrorClass && aEnv->IsInstanceOf(aExc, sOOMErrorClass)) {
+ NS_ABORT_OOM(0); // Unknown OOM size
+ }
+ return result;
+}
+
+namespace {
+
+jclass sJNIObjectClass;
+jfieldID sJNIObjectHandleField;
+
+bool EnsureJNIObject(JNIEnv* env, jobject instance) {
+ if (!sJNIObjectClass) {
+ sJNIObjectClass =
+ Class::GlobalRef(Class::LocalRef::Adopt(GetClassRef(
+ env, "org/mozilla/gecko/mozglue/JNIObject")))
+ .Forget();
+
+ sJNIObjectHandleField = env->GetFieldID(sJNIObjectClass, "mHandle", "J");
+ }
+
+ MOZ_ASSERT(env->IsInstanceOf(instance, sJNIObjectClass),
+ "Java class is not derived from JNIObject");
+ return true;
+}
+
+} // namespace
+
+uintptr_t GetNativeHandle(JNIEnv* env, jobject instance) {
+ if (!EnsureJNIObject(env, instance)) {
+ return 0;
+ }
+
+ return static_cast<uintptr_t>(
+ env->GetLongField(instance, sJNIObjectHandleField));
+}
+
+void SetNativeHandle(JNIEnv* env, jobject instance, uintptr_t handle) {
+ if (!EnsureJNIObject(env, instance)) {
+ return;
+ }
+
+ env->SetLongField(instance, sJNIObjectHandleField,
+ static_cast<jlong>(handle));
+}
+
+jclass GetClassRef(JNIEnv* aEnv, const char* aClassName) {
+ // First try the default class loader.
+ auto classRef = Class::LocalRef::Adopt(aEnv, aEnv->FindClass(aClassName));
+
+ if ((!classRef || aEnv->ExceptionCheck()) && sClassLoader) {
+ // If the default class loader failed but we have an app class loader, try
+ // that. Clear the pending exception from failed FindClass call above.
+ aEnv->ExceptionClear();
+ classRef = Class::LocalRef::Adopt(
+ aEnv,
+ jclass(aEnv->CallObjectMethod(sClassLoader, sClassLoaderLoadClass,
+ StringParam(aClassName, aEnv).Get())));
+ }
+
+ if (classRef && !aEnv->ExceptionCheck()) {
+ return classRef.Forget();
+ }
+
+ __android_log_print(
+ ANDROID_LOG_ERROR, "Gecko",
+ ">>> FATAL JNI ERROR! FindClass(\"%s\") failed. "
+ "Does the class require a newer API version? "
+ "Or did ProGuard optimize away something it shouldn't have?",
+ aClassName);
+ aEnv->ExceptionDescribe();
+ MOZ_CRASH("Cannot find JNI class");
+ return nullptr;
+}
+
+void DispatchToGeckoPriorityQueue(already_AddRefed<nsIRunnable> aCall) {
+ class RunnableEvent : public nsAppShell::Event {
+ nsCOMPtr<nsIRunnable> mCall;
+
+ public:
+ explicit RunnableEvent(already_AddRefed<nsIRunnable> aCall)
+ : mCall(aCall) {}
+ void Run() override { NS_ENSURE_SUCCESS_VOID(mCall->Run()); }
+ };
+
+ nsAppShell::PostEvent(MakeUnique<RunnableEvent>(std::move(aCall)));
+}
+
+int GetAPIVersion() {
+ static int32_t apiVersion = 0;
+ if (!apiVersion && IsAvailable()) {
+ apiVersion = java::sdk::Build::VERSION::SDK_INT();
+ }
+ return apiVersion;
+}
+
+pid_t GetUIThreadId() {
+ static pid_t uiThreadId;
+ if (!uiThreadId) {
+ uiThreadId = pid_t(java::GeckoThread::UiThreadId());
+ }
+ return uiThreadId;
+}
+
+bool IsOOMException(JNIEnv* aEnv) {
+ MOZ_ASSERT(aEnv->ExceptionCheck());
+ Throwable::LocalRef e =
+ Throwable::LocalRef::Adopt(aEnv, aEnv->ExceptionOccurred());
+ return sOOMErrorClass && aEnv->IsInstanceOf(e.Get(), sOOMErrorClass);
+}
+
+} // namespace jni
+} // namespace mozilla
diff --git a/widget/android/jni/Utils.h b/widget/android/jni/Utils.h
new file mode 100644
index 0000000000..adcafeb44e
--- /dev/null
+++ b/widget/android/jni/Utils.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_jni_Utils_h__
+#define mozilla_jni_Utils_h__
+
+#include <jni.h>
+
+#include "nsIRunnable.h"
+
+#include "mozilla/UniquePtr.h"
+
+#if defined(DEBUG) || !defined(RELEASE_OR_BETA)
+# define MOZ_CHECK_JNI
+#endif
+
+#ifdef MOZ_CHECK_JNI
+# include <unistd.h>
+# include "mozilla/Assertions.h"
+# include "APKOpen.h"
+# include "MainThreadUtils.h"
+#endif
+
+namespace mozilla {
+namespace jni {
+
+// How exception during a JNI call should be treated.
+enum class ExceptionMode {
+ // Abort on unhandled excepion (default).
+ ABORT,
+ // Ignore the exception and return to caller.
+ IGNORE,
+ // Catch any exception and return a nsresult.
+ NSRESULT,
+};
+
+// Thread that a particular JNI call is allowed on.
+enum class CallingThread {
+ // Can be called from any thread (default).
+ ANY,
+ // Can be called from the Gecko thread.
+ GECKO,
+ // Can be called from the Java UI thread.
+ UI,
+};
+
+// If and where a JNI call will be dispatched.
+enum class DispatchTarget {
+ // Call happens synchronously on the calling thread (default).
+ CURRENT,
+ // Call happens synchronously on the calling thread, but the call is
+ // wrapped in a function object and is passed thru UsesNativeCallProxy.
+ // Method must return void.
+ PROXY,
+ // Call is dispatched asynchronously on the Gecko thread to the XPCOM
+ // (nsThread) event queue. Method must return void.
+ GECKO,
+ // Call is dispatched asynchronously on the Gecko thread to the widget
+ // (nsAppShell) event queue. In most cases, events in the widget event
+ // queue (aka native event queue) are favored over events in the XPCOM
+ // event queue. Method must return void.
+ GECKO_PRIORITY,
+};
+
+extern JavaVM* sJavaVM;
+extern JNIEnv* sGeckoThreadEnv;
+
+inline bool IsAvailable() { return !!sGeckoThreadEnv; }
+
+inline JavaVM* GetVM() {
+#ifdef MOZ_CHECK_JNI
+ MOZ_ASSERT(sJavaVM);
+#endif
+ return sJavaVM;
+}
+
+inline JNIEnv* GetGeckoThreadEnv() {
+#ifdef MOZ_CHECK_JNI
+ MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Must be on Gecko thread");
+ MOZ_RELEASE_ASSERT(sGeckoThreadEnv, "Must have a JNIEnv");
+#endif
+ return sGeckoThreadEnv;
+}
+
+void SetGeckoThreadEnv(JNIEnv* aEnv);
+
+JNIEnv* GetEnvForThread();
+
+#ifdef MOZ_CHECK_JNI
+# define MOZ_ASSERT_JNI_THREAD(thread) \
+ do { \
+ if ((thread) == mozilla::jni::CallingThread::GECKO) { \
+ MOZ_RELEASE_ASSERT(::NS_IsMainThread()); \
+ } else if ((thread) == mozilla::jni::CallingThread::UI) { \
+ const bool isOnUiThread = (GetUIThreadId() == ::gettid()); \
+ MOZ_RELEASE_ASSERT(isOnUiThread); \
+ } \
+ } while (0)
+#else
+# define MOZ_ASSERT_JNI_THREAD(thread) \
+ do { \
+ } while (0)
+#endif
+
+bool ThrowException(JNIEnv* aEnv, const char* aClass, const char* aMessage);
+
+inline bool ThrowException(JNIEnv* aEnv, const char* aMessage) {
+ return ThrowException(aEnv, "java/lang/Exception", aMessage);
+}
+
+inline bool ThrowException(const char* aClass, const char* aMessage) {
+ return ThrowException(GetEnvForThread(), aClass, aMessage);
+}
+
+inline bool ThrowException(const char* aMessage) {
+ return ThrowException(GetEnvForThread(), aMessage);
+}
+
+bool HandleUncaughtException(JNIEnv* aEnv);
+
+bool ReportException(JNIEnv* aEnv, jthrowable aExc, jstring aStack);
+
+#define MOZ_CATCH_JNI_EXCEPTION(env) \
+ do { \
+ if (mozilla::jni::HandleUncaughtException((env))) { \
+ MOZ_CRASH("JNI exception"); \
+ } \
+ } while (0)
+
+uintptr_t GetNativeHandle(JNIEnv* env, jobject instance);
+
+void SetNativeHandle(JNIEnv* env, jobject instance, uintptr_t handle);
+
+jclass GetClassRef(JNIEnv* aEnv, const char* aClassName);
+
+void DispatchToGeckoPriorityQueue(already_AddRefed<nsIRunnable> aCall);
+
+int GetAPIVersion();
+
+pid_t GetUIThreadId();
+
+bool IsOOMException(JNIEnv* aEnv);
+
+} // namespace jni
+} // namespace mozilla
+
+#endif // mozilla_jni_Utils_h__
diff --git a/widget/android/jni/moz.build b/widget/android/jni/moz.build
new file mode 100644
index 0000000000..53bef6663f
--- /dev/null
+++ b/widget/android/jni/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("GeckoView", "General")
+
+EXPORTS.mozilla.jni += [
+ "Accessors.h",
+ "Conversions.h",
+ "GeckoBundleUtils.h",
+ "GeckoResultUtils.h",
+ "Natives.h",
+ "NativesInlines.h",
+ "Refs.h",
+ "TypeAdapter.h",
+ "Types.h",
+ "Utils.h",
+]
+
+UNIFIED_SOURCES += [
+ "Conversions.cpp",
+ "GeckoBundleUtils.cpp",
+ "Utils.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/widget",
+ "/widget/android",
+]
diff --git a/widget/android/moz.build b/widget/android/moz.build
new file mode 100644
index 0000000000..e4525bb747
--- /dev/null
+++ b/widget/android/moz.build
@@ -0,0 +1,207 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("GeckoView", "General")
+ SCHEDULES.exclusive = ["android"]
+
+with Files("*CompositorWidget*"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+DIRS += [
+ "bindings",
+ "jni",
+]
+
+XPIDL_SOURCES += [
+ "nsIAndroidBridge.idl",
+]
+
+XPIDL_MODULE = "widget_android"
+
+EXPORTS += [
+ "AndroidBridge.h",
+]
+
+classes_with_WrapForJNI = [
+ "AndroidGamepadManager",
+ "AndroidVsync",
+ "Base64Utils",
+ "Clipboard",
+ "CodecProxy",
+ "CompositorSurfaceManager",
+ "ContentInputStream",
+ "EnterpriseRoots",
+ "EventCallback",
+ "EventDispatcher",
+ "GeckoAppShell",
+ "GeckoAudioInfo",
+ "GeckoBatteryManager",
+ "GeckoBundle",
+ "GeckoDragAndDrop",
+ "GeckoEditableChild",
+ "GeckoHLSDemuxerWrapper",
+ "GeckoHLSResourceWrapper",
+ "GeckoHLSSample",
+ "GeckoInputStream",
+ "GeckoJavaSampler",
+ "GeckoNetworkManager",
+ "GeckoProcessManager",
+ "GeckoProcessType",
+ "GeckoResult",
+ "GeckoRuntime",
+ "GeckoServiceChildProcess",
+ "GeckoServiceGpuProcess",
+ "GeckoSession",
+ "GeckoSurface",
+ "GeckoSurfaceTexture",
+ "GeckoSystemStateListener",
+ "GeckoThread",
+ "GeckoViewInputStream",
+ "GeckoVRManager",
+ "GeckoVideoInfo",
+ "GeckoWebExecutor",
+ "HardwareCodecCapabilityUtils",
+ "Image",
+ "ImageDecoder",
+ "MediaDrmProxy",
+ "PanZoomController",
+ "RuntimeTelemetry",
+ "Sample",
+ "SampleBuffer",
+ "ScreenManagerHelper",
+ "ServiceAllocator",
+ "SessionAccessibility",
+ "SessionKeyInfo",
+ "SessionTextInput",
+ "SpeechSynthesisService",
+ "SurfaceAllocator",
+ "SurfaceControlManager",
+ "SurfaceTextureListener",
+ "TelemetryUtils",
+ "WebAuthnTokenManager",
+ "WebMessage",
+ "WebNotification",
+ "WebNotificationDelegate",
+ "WebRequest",
+ "WebRequestError",
+ "WebResponse",
+ "XPCOMEventTarget",
+]
+
+natives_from_WrapForJNI = sorted(
+ ["GeneratedJNI/{}Natives.h".format(c) for c in classes_with_WrapForJNI]
+)
+
+wrappers_from_WrapForJNI = sorted(
+ ["GeneratedJNI/{}Wrappers.h".format(c) for c in classes_with_WrapForJNI]
+)
+
+sources_from_WrapForJNI = sorted(
+ "GeneratedJNI{}Wrappers.cpp".format(c) for c in classes_with_WrapForJNI
+)
+
+EXPORTS.mozilla.widget += [
+ "AndroidCompositorWidget.h",
+ "AndroidUiThread.h",
+ "AndroidView.h",
+ "AndroidVsync.h",
+ "AndroidWidgetUtils.h",
+ "CompositorWidgetChild.h",
+ "CompositorWidgetParent.h",
+ "EventDispatcher.h",
+ "GeckoViewSupport.h",
+ "InProcessAndroidCompositorWidget.h",
+ "nsWindow.h",
+ "WindowEvent.h",
+]
+
+EXPORTS.mozilla.java += ["!{}".format(c) for c in natives_from_WrapForJNI]
+
+EXPORTS.mozilla.java += ["!{}".format(c) for c in wrappers_from_WrapForJNI]
+
+SOURCES += ["!{}".format(c) for c in sources_from_WrapForJNI]
+
+SOURCES += [
+ "MediaKeysEventSourceFactory.cpp",
+]
+
+UNIFIED_SOURCES += [
+ "AndroidAlerts.cpp",
+ "AndroidBridge.cpp",
+ "AndroidCompositorWidget.cpp",
+ "AndroidContentController.cpp",
+ "AndroidUiThread.cpp",
+ "AndroidVsync.cpp",
+ "AndroidWidgetUtils.cpp",
+ "CompositorWidgetChild.cpp",
+ "CompositorWidgetParent.cpp",
+ "EventDispatcher.cpp",
+ "GeckoEditableSupport.cpp",
+ "GeckoProcessManager.cpp",
+ "GfxInfo.cpp",
+ "ImageDecoderSupport.cpp",
+ "InProcessAndroidCompositorWidget.cpp",
+ "nsAppShell.cpp",
+ "nsClipboard.cpp",
+ "nsDeviceContextAndroid.cpp",
+ "nsDragService.cpp",
+ "nsLookAndFeel.cpp",
+ "nsPrintSettingsServiceAndroid.cpp",
+ "nsUserIdleServiceAndroid.cpp",
+ "nsWidgetFactory.cpp",
+ "nsWindow.cpp",
+ "ScreenHelperAndroid.cpp",
+ "WebExecutorSupport.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# The recursive make backend treats the first output specially: it's passed as
+# an open FileAvoidWrite to the invoked script. That doesn't work well with
+# the Gradle task that generates all of the outputs, so we add a dummy first
+# output.
+
+t = tuple(
+ ["generated_jni_wrappers"]
+ + natives_from_WrapForJNI
+ + sources_from_WrapForJNI
+ + wrappers_from_WrapForJNI
+)
+
+GeneratedFile(
+ *t,
+ script="/mobile/android/gradle.py",
+ entry_point="generate_generated_jni_wrappers"
+)
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/docshell/base",
+ "/dom/base",
+ "/dom/system/android",
+ "/gfx/2d",
+ "/gfx/vr",
+ "/layout/forms",
+ "/layout/painting",
+ "/netwerk/base",
+ "/toolkit/components/telemetry",
+ "/widget",
+ "/widget/headless",
+ "/xpcom/threads",
+]
+
+OS_LIBS += ["android"]
+
+if CONFIG["MOZ_NATIVE_DEVICES"]:
+ DEFINES["MOZ_NATIVE_DEVICES"] = True
+
+# DEFINES['DEBUG_WIDGETS'] = True
diff --git a/widget/android/nsAppShell.cpp b/widget/android/nsAppShell.cpp
new file mode 100644
index 0000000000..4d396945c4
--- /dev/null
+++ b/widget/android/nsAppShell.cpp
@@ -0,0 +1,755 @@
+/* -*- Mode: c++; tab-width: 4; 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 "nsAppShell.h"
+
+#include "base/basictypes.h"
+#include "base/message_loop.h"
+#include "base/task.h"
+#include "mozilla/Hal.h"
+#include "gfxConfig.h"
+#include "nsDragService.h"
+#include "nsExceptionHandler.h"
+#include "nsIScreen.h"
+#include "nsWindow.h"
+#include "nsThreadUtils.h"
+#include "nsIObserverService.h"
+#include "nsIAppStartup.h"
+#include "nsIGeolocationProvider.h"
+#include "nsIDOMWakeLockListener.h"
+#include "nsIPowerManagerService.h"
+#include "nsISpeculativeConnect.h"
+#include "nsIURIFixup.h"
+#include "nsCategoryManagerUtils.h"
+#include "mozilla/dom/GeolocationPosition.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/Hal.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/intl/OSPreferences.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "mozilla/java/GeckoAppShellNatives.h"
+#include "mozilla/java/GeckoDragAndDropNatives.h"
+#include "mozilla/java/GeckoResultWrappers.h"
+#include "mozilla/java/GeckoThreadNatives.h"
+#include "mozilla/java/XPCOMEventTargetNatives.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "prenv.h"
+#include "prtime.h"
+
+#include "AndroidBridge.h"
+#include "AndroidBridgeUtilities.h"
+#include "AndroidSurfaceTexture.h"
+#include <android/log.h>
+#include <pthread.h>
+#include <wchar.h>
+
+#ifdef MOZ_ANDROID_HISTORY
+# include "nsNetUtil.h"
+# include "nsIURI.h"
+# include "IHistory.h"
+#endif
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+#endif
+
+#include "AndroidAlerts.h"
+#include "AndroidUiThread.h"
+#include "GeckoBatteryManager.h"
+#include "GeckoEditableSupport.h"
+#include "GeckoNetworkManager.h"
+#include "GeckoProcessManager.h"
+#include "GeckoSystemStateListener.h"
+#include "GeckoTelemetryDelegate.h"
+#include "GeckoVRManager.h"
+#include "ImageDecoderSupport.h"
+#include "JavaBuiltins.h"
+#include "ScreenHelperAndroid.h"
+#include "Telemetry.h"
+#include "WebExecutorSupport.h"
+#include "Base64UtilsSupport.h"
+
+#ifdef DEBUG_ANDROID_EVENTS
+# define EVLOG(args...) ALOG(args)
+#else
+# define EVLOG(args...) \
+ do { \
+ } while (0)
+#endif
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+nsIGeolocationUpdate* gLocationCallback = nullptr;
+
+nsAppShell* nsAppShell::sAppShell;
+StaticAutoPtr<Mutex> nsAppShell::sAppShellLock;
+
+NS_IMPL_ISUPPORTS_INHERITED(nsAppShell, nsBaseAppShell, nsIObserver)
+
+class WakeLockListener final : public nsIDOMMozWakeLockListener {
+ private:
+ ~WakeLockListener() {}
+
+ public:
+ NS_DECL_ISUPPORTS;
+
+ nsresult Callback(const nsAString& topic, const nsAString& state) override {
+ java::GeckoAppShell::NotifyWakeLockChanged(topic, state);
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(WakeLockListener, nsIDOMMozWakeLockListener)
+nsCOMPtr<nsIPowerManagerService> sPowerManagerService = nullptr;
+StaticRefPtr<WakeLockListener> sWakeLockListener;
+
+class GeckoThreadSupport final
+ : public java::GeckoThread::Natives<GeckoThreadSupport> {
+ // When this number goes above 0, the app is paused. When less than or
+ // equal to zero, the app is resumed.
+ static int32_t sPauseCount;
+
+ public:
+ static void SpeculativeConnect(jni::String::Param aUriStr) {
+ if (!NS_IsMainThread()) {
+ // We will be on the main thread if the call was queued on the Java
+ // side during startup. Otherwise, the call was not queued, which
+ // means Gecko is already sufficiently loaded, and we don't really
+ // care about speculative connections at this point.
+ return;
+ }
+
+ nsCOMPtr<nsIIOService> ioServ = do_GetIOService();
+ nsCOMPtr<nsISpeculativeConnect> specConn = do_QueryInterface(ioServ);
+ if (!specConn) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri = nsAppShell::ResolveURI(aUriStr->ToCString());
+ if (!uri) {
+ return;
+ }
+
+ OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, attrs);
+ specConn->SpeculativeConnect(uri, principal, nullptr, false);
+ }
+
+ static void OnPause() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ sPauseCount++;
+ // If sPauseCount is now 1, we just crossed the threshold from "resumed"
+ // "paused". so we should notify observers and so on.
+ if (sPauseCount != 1) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ obsServ->NotifyObservers(nullptr, "application-background", nullptr);
+
+ obsServ->NotifyObservers(nullptr, "memory-pressure", u"heap-minimize");
+
+ // We really want to send a notification like profile-before-change,
+ // but profile-before-change ends up shutting some things down instead
+ // of flushing data
+ Preferences* prefs = static_cast<Preferences*>(Preferences::GetService());
+ if (prefs) {
+ // Force a main thread blocking save
+ prefs->SavePrefFileBlocking();
+ }
+ }
+
+ static void OnResume() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ sPauseCount--;
+ // If sPauseCount is now 0, we just crossed the threshold from "paused"
+ // to "resumed", so we should notify observers and so on.
+ if (sPauseCount != 0) {
+ return;
+ }
+
+ // We didn't return from one of our own activities, so restore
+ // to foreground status
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ obsServ->NotifyObservers(nullptr, "application-foreground", nullptr);
+ }
+
+ static void CreateServices(jni::String::Param aCategory,
+ jni::String::Param aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCString category(aCategory->ToCString());
+
+ NS_CreateServicesFromCategory(category.get(),
+ nullptr, // aOrigin
+ category.get(),
+ aData ? aData->ToString().get() : nullptr);
+ }
+
+ static int64_t RunUiThreadCallback() { return RunAndroidUiTasks(); }
+
+ static void ForceQuit() {
+ nsCOMPtr<nsIAppStartup> appStartup = components::AppStartup::Service();
+
+ if (appStartup) {
+ bool userAllowedQuit = true;
+ appStartup->Quit(nsIAppStartup::eForceQuit, 0, &userAllowedQuit);
+ }
+ }
+
+ static void Crash() {
+ printf_stderr("Intentionally crashing...\n");
+ MOZ_CRASH("intentional crash");
+ }
+};
+
+int32_t GeckoThreadSupport::sPauseCount;
+
+class GeckoAppShellSupport final
+ : public java::GeckoAppShell::Natives<GeckoAppShellSupport> {
+ public:
+ static void ReportJavaCrash(const jni::Class::LocalRef& aCls,
+ jni::Throwable::Param aException,
+ jni::String::Param aStack) {
+ if (!jni::ReportException(aCls.Env(), aException.Get(), aStack.Get())) {
+ // Only crash below if crash reporter is initialized and annotation
+ // succeeded. Otherwise try other means of reporting the crash in
+ // Java.
+ return;
+ }
+
+ MOZ_CRASH("Uncaught Java exception");
+ }
+
+ static void NotifyObservers(jni::String::Param aTopic,
+ jni::String::Param aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aTopic);
+
+ nsCOMPtr<nsIObserverService> obsServ = services::GetObserverService();
+ if (!obsServ) {
+ return;
+ }
+
+ obsServ->NotifyObservers(nullptr, aTopic->ToCString().get(),
+ aData ? aData->ToString().get() : nullptr);
+ }
+
+ static void AppendAppNotesToCrashReport(jni::String::Param aNotes) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aNotes);
+ CrashReporter::AppendAppNotesToCrashReport(aNotes->ToCString());
+ }
+
+ static void OnSensorChanged(int32_t aType, float aX, float aY, float aZ,
+ float aW, int64_t aTime) {
+ AutoTArray<float, 4> values;
+
+ switch (aType) {
+ // Bug 938035, transfer HAL data for orientation sensor to meet w3c
+ // spec, ex: HAL report alpha=90 means East but alpha=90 means West
+ // in w3c spec
+ case hal::SENSOR_ORIENTATION:
+ values.AppendElement(360.0f - aX);
+ values.AppendElement(-aY);
+ values.AppendElement(-aZ);
+ break;
+
+ case hal::SENSOR_LINEAR_ACCELERATION:
+ case hal::SENSOR_ACCELERATION:
+ case hal::SENSOR_GYROSCOPE:
+ values.AppendElement(aX);
+ values.AppendElement(aY);
+ values.AppendElement(aZ);
+ break;
+
+ case hal::SENSOR_LIGHT:
+ values.AppendElement(aX);
+ break;
+
+ case hal::SENSOR_ROTATION_VECTOR:
+ case hal::SENSOR_GAME_ROTATION_VECTOR:
+ values.AppendElement(aX);
+ values.AppendElement(aY);
+ values.AppendElement(aZ);
+ values.AppendElement(aW);
+ break;
+
+ default:
+ __android_log_print(ANDROID_LOG_ERROR, "Gecko",
+ "Unknown sensor type %d", aType);
+ }
+
+ hal::SensorData sdata(hal::SensorType(aType), aTime, values);
+ hal::NotifySensorChange(sdata);
+ }
+
+ static void OnLocationChanged(double aLatitude, double aLongitude,
+ double aAltitude, float aAccuracy,
+ float aAltitudeAccuracy, float aHeading,
+ float aSpeed) {
+ if (!gLocationCallback) {
+ return;
+ }
+
+ static constexpr float kEpsilon = 0.0001f;
+ double heading = (aHeading >= kEpsilon && aHeading < (360.0f - kEpsilon) &&
+ aSpeed > kEpsilon)
+ ? aHeading
+ : UnspecifiedNaN<double>();
+
+ RefPtr<nsIDOMGeoPosition> geoPosition(new nsGeoPosition(
+ aLatitude, aLongitude, aAltitude, aAccuracy, aAltitudeAccuracy, heading,
+ aSpeed, PR_Now() / PR_USEC_PER_MSEC));
+ gLocationCallback->Update(geoPosition);
+ }
+
+ static void NotifyAlertListener(jni::String::Param aName,
+ jni::String::Param aTopic,
+ jni::String::Param aCookie) {
+ if (!aName || !aTopic || !aCookie) {
+ return;
+ }
+
+ widget::AndroidAlerts::NotifyListener(aName->ToString(),
+ aTopic->ToCString().get(),
+ aCookie->ToString().get());
+ }
+
+ static bool IsParentProcess() { return XRE_IsParentProcess(); }
+
+ static jni::Object::LocalRef IsGpuProcessEnabled() {
+ java::GeckoResult::GlobalRef result = java::GeckoResult::New();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "GeckoAppShellSupport::IsGpuProcessEnabled", [result]() {
+ result->Complete(gfx::gfxConfig::IsEnabled(gfx::Feature::GPU_PROCESS)
+ ? java::sdk::Boolean::TRUE()
+ : java::sdk::Boolean::FALSE());
+ }));
+
+ return jni::Object::Ref::From(result);
+ }
+};
+
+class XPCOMEventTargetWrapper final
+ : public java::XPCOMEventTarget::Natives<XPCOMEventTargetWrapper> {
+ public:
+ // Wraps a java runnable into an XPCOM runnable and dispatches it to mTarget.
+ void DispatchNative(mozilla::jni::Object::Param aJavaRunnable) {
+ if (AppShutdown::GetCurrentShutdownPhase() >=
+ ShutdownPhase::XPCOMShutdownThreads) {
+ // No point in trying to dispatch this if we're already shutting down.
+ return;
+ }
+ java::XPCOMEventTarget::JNIRunnable::GlobalRef r =
+ java::XPCOMEventTarget::JNIRunnable::Ref::From(aJavaRunnable);
+ mTarget->Dispatch(NS_NewRunnableFunction(
+ "XPCOMEventTargetWrapper::DispatchNative",
+ [runnable = std::move(r)]() { runnable->Run(); }));
+ }
+
+ bool IsOnCurrentThread() { return mTarget->IsOnCurrentThread(); }
+
+ static void Init() {
+ java::XPCOMEventTarget::Natives<XPCOMEventTargetWrapper>::Init();
+ CreateWrapper(u"main"_ns, do_GetMainThread());
+ if (XRE_IsParentProcess()) {
+ CreateWrapper(u"launcher"_ns, ipc::GetIPCLauncher());
+ }
+ }
+
+ static void CreateWrapper(mozilla::jni::String::Param aName,
+ nsCOMPtr<nsIEventTarget> aTarget) {
+ auto java = java::XPCOMEventTarget::New();
+ auto native = MakeUnique<XPCOMEventTargetWrapper>(aTarget.forget());
+ AttachNative(java, std::move(native));
+
+ java::XPCOMEventTarget::SetTarget(aName, java);
+ }
+
+ static void ResolveAndDispatchNative(mozilla::jni::String::Param aName,
+ mozilla::jni::Object::Param aRunnable) {
+ java::XPCOMEventTarget::ResolveAndDispatch(aName, aRunnable);
+ }
+
+ explicit XPCOMEventTargetWrapper(already_AddRefed<nsIEventTarget> aTarget)
+ : mTarget(aTarget) {}
+
+ private:
+ nsCOMPtr<nsIEventTarget> mTarget;
+};
+
+nsAppShell::nsAppShell()
+ : mSyncRunFinished(*(sAppShellLock = new Mutex("nsAppShell")),
+ "nsAppShell.SyncRun"),
+ mSyncRunQuit(false) {
+ {
+ MutexAutoLock lock(*sAppShellLock);
+ sAppShell = this;
+ }
+
+ hal::Init();
+
+ if (!XRE_IsParentProcess()) {
+ if (jni::IsAvailable()) {
+ GeckoThreadSupport::Init();
+ GeckoAppShellSupport::Init();
+ XPCOMEventTargetWrapper::Init();
+ mozilla::widget::Telemetry::Init();
+ mozilla::widget::GeckoTelemetryDelegate::Init();
+
+ if (XRE_IsGPUProcess()) {
+ mozilla::gl::AndroidSurfaceTexture::Init();
+ }
+
+ // Set the corresponding state in GeckoThread.
+ java::GeckoThread::SetState(java::GeckoThread::State::RUNNING());
+ }
+ return;
+ }
+
+ if (jni::IsAvailable()) {
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+ screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperAndroid>());
+
+ // Initialize JNI and Set the corresponding state in GeckoThread.
+ AndroidBridge::ConstructBridge();
+ GeckoAppShellSupport::Init();
+ GeckoThreadSupport::Init();
+ XPCOMEventTargetWrapper::Init();
+ mozilla::GeckoBatteryManager::Init();
+ mozilla::GeckoNetworkManager::Init();
+ mozilla::GeckoProcessManager::Init();
+ mozilla::GeckoSystemStateListener::Init();
+ mozilla::widget::Telemetry::Init();
+ mozilla::widget::ImageDecoderSupport::Init();
+ mozilla::widget::WebExecutorSupport::Init();
+ mozilla::widget::Base64UtilsSupport::Init();
+ nsWindow::InitNatives();
+ mozilla::gl::AndroidSurfaceTexture::Init();
+ mozilla::widget::GeckoTelemetryDelegate::Init();
+
+ java::GeckoThread::SetState(java::GeckoThread::State::JNI_READY());
+
+ CreateAndroidUiThread();
+ }
+
+ sPowerManagerService = do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+
+ if (sPowerManagerService) {
+ sWakeLockListener = new WakeLockListener();
+ } else {
+ NS_WARNING(
+ "Failed to retrieve PowerManagerService, wakelocks will be broken!");
+ }
+}
+
+nsAppShell::~nsAppShell() {
+ {
+ // Release any thread waiting for a sync call to finish.
+ MutexAutoLock lock(*sAppShellLock);
+ sAppShell = nullptr;
+ mSyncRunFinished.NotifyAll();
+ }
+
+ while (mEventQueue.Pop(/* mayWait */ false)) {
+ NS_WARNING("Discarded event on shutdown");
+ }
+
+ if (sPowerManagerService) {
+ sPowerManagerService->RemoveWakeLockListener(sWakeLockListener);
+
+ sPowerManagerService = nullptr;
+ sWakeLockListener = nullptr;
+ }
+
+ hal::Shutdown();
+
+ if (jni::IsAvailable() && XRE_IsParentProcess()) {
+ DestroyAndroidUiThread();
+ AndroidBridge::DeconstructBridge();
+ }
+}
+
+void nsAppShell::NotifyNativeEvent() { mEventQueue.Signal(); }
+
+nsresult nsAppShell::Init() {
+ nsresult rv = nsBaseAppShell::Init();
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ if (obsServ) {
+ obsServ->AddObserver(this, "browser-delayed-startup-finished", false);
+ obsServ->AddObserver(this, "geckoview-startup-complete", false);
+ obsServ->AddObserver(this, "profile-after-change", false);
+ obsServ->AddObserver(this, "quit-application", false);
+ obsServ->AddObserver(this, "quit-application-granted", false);
+
+ if (XRE_IsParentProcess()) {
+ obsServ->AddObserver(this, "chrome-document-loaded", false);
+ } else {
+ obsServ->AddObserver(this, "content-document-global-created", false);
+ obsServ->AddObserver(this, "geckoview-content-global-transferred", false);
+ }
+ }
+
+ if (sPowerManagerService)
+ sPowerManagerService->AddWakeLockListener(sWakeLockListener);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAppShell::Exit(void) {
+ {
+ // Release any thread waiting for a sync call to finish.
+ mozilla::MutexAutoLock shellLock(*sAppShellLock);
+ mSyncRunQuit = true;
+ mSyncRunFinished.NotifyAll();
+ }
+ // We need to ensure no observers stick around after XPCOM shuts down
+ // or we'll see crashes, as the app shell outlives XPConnect.
+ mObserversHash.Clear();
+ return nsBaseAppShell::Exit();
+}
+
+NS_IMETHODIMP
+nsAppShell::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ bool removeObserver = false;
+
+ if (!strcmp(aTopic, "browser-delayed-startup-finished")) {
+ NS_CreateServicesFromCategory("browser-delayed-startup-finished", nullptr,
+ "browser-delayed-startup-finished");
+ } else if (!strcmp(aTopic, "geckoview-startup-complete")) {
+ if (jni::IsAvailable()) {
+ java::GeckoThread::CheckAndSetState(
+ java::GeckoThread::State::PROFILE_READY(),
+ java::GeckoThread::State::RUNNING());
+ }
+ } else if (!strcmp(aTopic, "profile-after-change")) {
+ if (jni::IsAvailable()) {
+ java::GeckoThread::SetState(java::GeckoThread::State::PROFILE_READY());
+
+ // Gecko on Android follows the Android app model where it never
+ // stops until it is killed by the system or told explicitly to
+ // quit. Therefore, we should *not* exit Gecko when there is no
+ // window or the last window is closed. nsIAppStartup::Quit will
+ // still force Gecko to exit.
+ nsCOMPtr<nsIAppStartup> appStartup = components::AppStartup::Service();
+ if (appStartup) {
+ appStartup->EnterLastWindowClosingSurvivalArea();
+ }
+ }
+ removeObserver = true;
+
+ } else if (!strcmp(aTopic, "chrome-document-loaded")) {
+ // Set the global ready state and enable the window event dispatcher
+ // for this particular GeckoView.
+ nsCOMPtr<dom::Document> doc = do_QueryInterface(aSubject);
+ MOZ_ASSERT(doc);
+ if (const RefPtr<nsWindow> window = nsWindow::From(doc->GetWindow())) {
+ window->OnGeckoViewReady();
+ }
+ } else if (!strcmp(aTopic, "quit-application")) {
+ if (jni::IsAvailable()) {
+ const bool restarting = aData && u"restart"_ns.Equals(aData);
+ java::GeckoThread::SetState(restarting
+ ? java::GeckoThread::State::RESTARTING()
+ : java::GeckoThread::State::EXITING());
+ }
+ removeObserver = true;
+
+ } else if (!strcmp(aTopic, "quit-application-granted")) {
+ if (jni::IsAvailable()) {
+ // We are told explicitly to quit, perhaps due to
+ // nsIAppStartup::Quit being called. We should release our hold on
+ // nsIAppStartup and let it continue to quit.
+ nsCOMPtr<nsIAppStartup> appStartup = components::AppStartup::Service();
+ if (appStartup) {
+ appStartup->ExitLastWindowClosingSurvivalArea();
+ }
+ }
+ removeObserver = true;
+
+ } else if (!strcmp(aTopic, "content-document-global-created")) {
+ // Associate the PuppetWidget of the newly-created BrowserChild with a
+ // GeckoEditableChild instance.
+ MOZ_ASSERT(!XRE_IsParentProcess());
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow = do_QueryInterface(aSubject);
+ MOZ_ASSERT(domWindow);
+ nsCOMPtr<nsIWidget> domWidget = widget::WidgetUtils::DOMWindowToWidget(
+ nsPIDOMWindowOuter::From(domWindow));
+ NS_ENSURE_TRUE(domWidget, NS_OK);
+
+ widget::GeckoEditableSupport::SetOnBrowserChild(
+ domWidget->GetOwningBrowserChild());
+
+ } else if (!strcmp(aTopic, "geckoview-content-global-transferred")) {
+ // We're transferring to a new GeckoEditableParent, so notify the
+ // existing GeckoEditableChild instance associated with the docshell.
+ nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(aSubject);
+ widget::GeckoEditableSupport::SetOnBrowserChild(
+ dom::BrowserChild::GetFrom(docShell));
+ } else {
+ return nsBaseAppShell::Observe(aSubject, aTopic, aData);
+ }
+
+ if (removeObserver) {
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ if (obsServ) {
+ obsServ->RemoveObserver(this, aTopic);
+ }
+ }
+ return NS_OK;
+}
+
+bool nsAppShell::ProcessNextNativeEvent(bool mayWait) {
+ EVLOG("nsAppShell::ProcessNextNativeEvent %d", mayWait);
+
+ AUTO_PROFILER_LABEL("nsAppShell::ProcessNextNativeEvent", OTHER);
+
+ mozilla::UniquePtr<Event> curEvent;
+
+ {
+ curEvent = mEventQueue.Pop(/* mayWait */ false);
+
+ if (!curEvent && mayWait) {
+ // This processes messages in the Android Looper. Note that we only
+ // get here if the normal Gecko event loop has been awoken
+ // (bug 750713). Looper messages effectively have the lowest
+ // priority because we only process them before we're about to
+ // wait for new events.
+ if (jni::IsAvailable() && XRE_IsParentProcess() &&
+ AndroidBridge::Bridge()->PumpMessageLoop()) {
+ return true;
+ }
+
+ AUTO_PROFILER_LABEL("nsAppShell::ProcessNextNativeEvent:Wait", IDLE);
+ mozilla::BackgroundHangMonitor().NotifyWait();
+
+ curEvent = mEventQueue.Pop(/* mayWait */ true);
+ }
+ }
+
+ if (!curEvent) return false;
+
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+
+ curEvent->Run();
+ return true;
+}
+
+bool nsAppShell::SyncRunEvent(
+ Event&& event, UniquePtr<Event> (*eventFactory)(UniquePtr<Event>&&),
+ const TimeDuration timeout) {
+ // Perform the call on the Gecko thread in a separate lambda, and wait
+ // on the monitor on the current thread.
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // This is the lock to check that app shell is still alive,
+ // and to wait on for the sync call to complete.
+ mozilla::MutexAutoLock shellLock(*sAppShellLock);
+ nsAppShell* const appShell = sAppShell;
+
+ if (MOZ_UNLIKELY(!appShell)) {
+ // Post-shutdown.
+ return false;
+ }
+
+ bool finished = false;
+ auto runAndNotify = [&event, &finished] {
+ nsAppShell* const appShell = nsAppShell::Get();
+ if (MOZ_UNLIKELY(!appShell || appShell->mSyncRunQuit)) {
+ return false;
+ }
+ event.Run();
+ finished = true;
+ mozilla::MutexAutoLock shellLock(*sAppShellLock);
+ appShell->mSyncRunFinished.NotifyAll();
+ return finished;
+ };
+
+ UniquePtr<Event> runAndNotifyEvent =
+ mozilla::MakeUnique<LambdaEvent<decltype(runAndNotify)>>(
+ std::move(runAndNotify));
+
+ if (eventFactory) {
+ runAndNotifyEvent = (*eventFactory)(std::move(runAndNotifyEvent));
+ }
+
+ appShell->mEventQueue.Post(std::move(runAndNotifyEvent));
+
+ while (!finished && MOZ_LIKELY(sAppShell && !sAppShell->mSyncRunQuit)) {
+ appShell->mSyncRunFinished.Wait(timeout);
+ }
+
+ return finished;
+}
+
+already_AddRefed<nsIURI> nsAppShell::ResolveURI(const nsCString& aUriStr) {
+ nsCOMPtr<nsIIOService> ioServ = do_GetIOService();
+ nsCOMPtr<nsIURI> uri;
+
+ if (NS_SUCCEEDED(
+ ioServ->NewURI(aUriStr, nullptr, nullptr, getter_AddRefs(uri)))) {
+ return uri.forget();
+ }
+
+ nsCOMPtr<nsIURIFixup> fixup = components::URIFixup::Service();
+ nsCOMPtr<nsIURIFixupInfo> fixupInfo;
+ if (fixup &&
+ NS_SUCCEEDED(fixup->GetFixupURIInfo(aUriStr, nsIURIFixup::FIXUP_FLAG_NONE,
+ getter_AddRefs(fixupInfo))) &&
+ NS_SUCCEEDED(fixupInfo->GetPreferredURI(getter_AddRefs(uri)))) {
+ return uri.forget();
+ }
+ return nullptr;
+}
+
+nsresult nsAppShell::AddObserver(const nsAString& aObserverKey,
+ nsIObserver* aObserver) {
+ NS_ASSERTION(aObserver != nullptr,
+ "nsAppShell::AddObserver: aObserver is null!");
+ mObserversHash.InsertOrUpdate(aObserverKey, aObserver);
+ return NS_OK;
+}
+
+// Used by IPC code
+namespace mozilla {
+
+bool ProcessNextEvent() {
+ nsAppShell* const appShell = nsAppShell::Get();
+ if (!appShell) {
+ return false;
+ }
+
+ return appShell->ProcessNextNativeEvent(true) ? true : false;
+}
+
+void NotifyEvent() {
+ nsAppShell* const appShell = nsAppShell::Get();
+ if (!appShell) {
+ return;
+ }
+ appShell->NotifyNativeEvent();
+}
+
+} // namespace mozilla
diff --git a/widget/android/nsAppShell.h b/widget/android/nsAppShell.h
new file mode 100644
index 0000000000..9b1dc5ca14
--- /dev/null
+++ b/widget/android/nsAppShell.h
@@ -0,0 +1,217 @@
+/* -*- Mode: c++; tab-width: 4; 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/. */
+
+#ifndef nsAppShell_h__
+#define nsAppShell_h__
+
+#include <time.h>
+
+#include <type_traits>
+#include <utility>
+
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TimeStamp.h" // for mozilla::TimeDuration
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/jni/Natives.h"
+#include "nsBaseAppShell.h"
+#include "nsCOMPtr.h"
+#include "nsIAndroidBridge.h"
+#include "nsInterfaceHashtable.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+bool ProcessNextEvent();
+void NotifyEvent();
+} // namespace mozilla
+
+class nsWindow;
+
+class nsAppShell : public nsBaseAppShell {
+ public:
+ struct Event : mozilla::LinkedListElement<Event> {
+ static uint64_t GetTime() {
+ timespec time;
+ if (clock_gettime(CLOCK_MONOTONIC, &time)) {
+ return 0ull;
+ }
+ return uint64_t(time.tv_sec) * 1000000000ull + time.tv_nsec;
+ }
+
+ uint64_t mPostTime{0};
+
+ bool HasSameTypeAs(const Event* other) const {
+ // Compare vtable addresses to determine same type.
+ return *reinterpret_cast<const uintptr_t*>(this) ==
+ *reinterpret_cast<const uintptr_t*>(other);
+ }
+
+ virtual ~Event() {}
+ virtual void Run() = 0;
+
+ virtual void PostTo(mozilla::LinkedList<Event>& queue) {
+ queue.insertBack(this);
+ }
+
+ virtual bool IsUIEvent() const { return false; }
+ };
+
+ template <typename T>
+ class LambdaEvent : public Event {
+ protected:
+ T lambda;
+
+ public:
+ explicit LambdaEvent(T&& l) : lambda(std::move(l)) {}
+ void Run() override { lambda(); }
+ };
+
+ class ProxyEvent : public Event {
+ protected:
+ mozilla::UniquePtr<Event> baseEvent;
+
+ public:
+ explicit ProxyEvent(mozilla::UniquePtr<Event>&& event)
+ : baseEvent(std::move(event)) {}
+
+ void PostTo(mozilla::LinkedList<Event>& queue) override {
+ baseEvent->PostTo(queue);
+ }
+
+ void Run() override { baseEvent->Run(); }
+ };
+
+ static nsAppShell* Get() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return sAppShell;
+ }
+
+ nsAppShell();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIOBSERVER
+
+ nsresult Init();
+
+ void NotifyNativeEvent();
+ bool ProcessNextNativeEvent(bool mayWait) override;
+
+ // Post a subclass of Event.
+ // e.g. PostEvent(mozilla::MakeUnique<MyEvent>());
+ template <typename T, typename D>
+ static void PostEvent(mozilla::UniquePtr<T, D>&& event) {
+ mozilla::MutexAutoLock lock(*sAppShellLock);
+ if (!sAppShell) {
+ return;
+ }
+ sAppShell->mEventQueue.Post(std::move(event));
+ }
+
+ // Post a event that will call a lambda
+ // e.g. PostEvent([=] { /* do something */ });
+ template <typename T>
+ static void PostEvent(T&& lambda) {
+ mozilla::MutexAutoLock lock(*sAppShellLock);
+ if (!sAppShell) {
+ return;
+ }
+ sAppShell->mEventQueue.Post(
+ mozilla::MakeUnique<LambdaEvent<T>>(std::move(lambda)));
+ }
+
+ // Post a event and wait for it to finish running on the Gecko thread.
+ static bool SyncRunEvent(
+ Event&& event,
+ mozilla::UniquePtr<Event> (*eventFactory)(mozilla::UniquePtr<Event>&&) =
+ nullptr,
+ const mozilla::TimeDuration timeout = mozilla::TimeDuration::Forever());
+
+ template <typename T>
+ static std::enable_if_t<!std::is_base_of<Event, T>::value, void> SyncRunEvent(
+ T&& lambda) {
+ SyncRunEvent(LambdaEvent<T>(std::forward<T>(lambda)));
+ }
+
+ static already_AddRefed<nsIURI> ResolveURI(const nsCString& aUriStr);
+
+ protected:
+ static nsAppShell* sAppShell;
+ static mozilla::StaticAutoPtr<mozilla::Mutex> sAppShellLock;
+
+ virtual ~nsAppShell();
+
+ NS_IMETHOD Exit() override;
+ nsresult AddObserver(const nsAString& aObserverKey, nsIObserver* aObserver);
+
+ class NativeCallbackEvent : public Event {
+ // Capturing the nsAppShell instance is safe because if the app
+ // shell is destroyed, this lambda will not be called either.
+ nsAppShell* const appShell;
+
+ public:
+ explicit NativeCallbackEvent(nsAppShell* as) : appShell(as) {}
+ void Run() override { appShell->NativeEventCallback(); }
+ };
+
+ void ScheduleNativeEventCallback() override {
+ mEventQueue.Post(mozilla::MakeUnique<NativeCallbackEvent>(this));
+ }
+
+ class Queue {
+ private:
+ mozilla::Monitor mMonitor MOZ_UNANNOTATED;
+ mozilla::LinkedList<Event> mQueue;
+
+ public:
+ enum { LATENCY_UI, LATENCY_OTHER, LATENCY_COUNT };
+ Queue() : mMonitor("nsAppShell.Queue") {}
+
+ void Signal() {
+ mozilla::MonitorAutoLock lock(mMonitor);
+ lock.NotifyAll();
+ }
+
+ void Post(mozilla::UniquePtr<Event>&& event) {
+ MOZ_ASSERT(event && !event->isInList());
+
+ mozilla::MonitorAutoLock lock(mMonitor);
+ event->PostTo(mQueue);
+ if (event->isInList()) {
+ event->mPostTime = Event::GetTime();
+ // Ownership of event object transfers to the queue.
+ mozilla::Unused << event.release();
+ }
+ lock.NotifyAll();
+ }
+
+ mozilla::UniquePtr<Event> Pop(bool mayWait) {
+ mozilla::MonitorAutoLock lock(mMonitor);
+
+ if (mayWait && mQueue.isEmpty()) {
+ lock.Wait();
+ }
+
+ // Ownership of event object transfers to the return value.
+ mozilla::UniquePtr<Event> event(mQueue.popFirst());
+ if (!event || !event->mPostTime) {
+ return event;
+ }
+
+ return event;
+ }
+
+ } mEventQueue;
+
+ private:
+ mozilla::CondVar mSyncRunFinished;
+ bool mSyncRunQuit;
+
+ nsInterfaceHashtable<nsStringHashKey, nsIObserver> mObserversHash;
+};
+
+#endif // nsAppShell_h__
diff --git a/widget/android/nsClipboard.cpp b/widget/android/nsClipboard.cpp
new file mode 100644
index 0000000000..c2a03dd540
--- /dev/null
+++ b/widget/android/nsClipboard.cpp
@@ -0,0 +1,214 @@
+/* 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 "mozilla/java/ClipboardWrappers.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+#include "nsClipboard.h"
+#include "nsISupportsPrimitives.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMemory.h"
+#include "nsStringStream.h"
+#include "nsPrimitiveHelpers.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS_INHERITED0(nsClipboard, nsBaseClipboard)
+
+/* The Android clipboard only supports text and doesn't support mime types
+ * so we assume all clipboard data is text/plain for now. Documentation
+ * indicates that support for other data types is planned for future
+ * releases.
+ */
+
+nsClipboard::nsClipboard()
+ : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(
+ false /* supportsSelectionClipboard */,
+ false /* supportsFindClipboard */,
+ false /* supportsSelectionCache */)) {
+ java::Clipboard::StartTrackingClipboardData(
+ java::GeckoAppShell::GetApplicationContext());
+}
+
+nsClipboard::~nsClipboard() {
+ java::Clipboard::StopTrackingClipboardData(
+ java::GeckoAppShell::GetApplicationContext());
+}
+
+// static
+nsresult nsClipboard::GetTextFromTransferable(nsITransferable* aTransferable,
+ nsString& aText,
+ nsString& aHTML) {
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ for (auto& flavorStr : flavors) {
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ nsCOMPtr<nsISupports> item;
+ nsresult rv =
+ aTransferable->GetTransferData(kTextMime, getter_AddRefs(item));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ nsCOMPtr<nsISupportsString> supportsString = do_QueryInterface(item);
+ if (supportsString) {
+ supportsString->GetData(aText);
+ }
+ } else if (flavorStr.EqualsLiteral(kHTMLMime)) {
+ nsCOMPtr<nsISupports> item;
+ nsresult rv =
+ aTransferable->GetTransferData(kHTMLMime, getter_AddRefs(item));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ nsCOMPtr<nsISupportsString> supportsString = do_QueryInterface(item);
+ if (supportsString) {
+ supportsString->GetData(aHTML);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsClipboard::SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (!jni::IsAvailable()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsString text;
+ nsString html;
+ nsresult rv = GetTextFromTransferable(aTransferable, text, html);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!html.IsEmpty() &&
+ java::Clipboard::SetHTML(java::GeckoAppShell::GetApplicationContext(),
+ text, html)) {
+ return NS_OK;
+ }
+ if (!text.IsEmpty() &&
+ java::Clipboard::SetText(java::GeckoAppShell::GetApplicationContext(),
+ text)) {
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (!jni::IsAvailable()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsTArray<nsCString> flavors;
+ aTransferable->FlavorsTransferableCanImport(flavors);
+
+ for (auto& flavorStr : flavors) {
+ if (flavorStr.EqualsLiteral(kTextMime) ||
+ flavorStr.EqualsLiteral(kHTMLMime)) {
+ auto text = java::Clipboard::GetTextData(
+ java::GeckoAppShell::GetApplicationContext(), flavorStr);
+ if (!text) {
+ continue;
+ }
+ nsString buffer = text->ToString();
+ if (buffer.IsEmpty()) {
+ continue;
+ }
+ nsCOMPtr<nsISupports> wrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(flavorStr, buffer.get(),
+ buffer.Length() * 2,
+ getter_AddRefs(wrapper));
+ if (wrapper) {
+ aTransferable->SetTransferData(flavorStr.get(), wrapper);
+ return NS_OK;
+ }
+ continue;
+ }
+
+ mozilla::jni::ByteArray::LocalRef bytes;
+ nsresult rv = java::Clipboard::GetRawData(flavorStr, &bytes);
+ if (NS_FAILED(rv) || !bytes) {
+ continue;
+ }
+ nsCOMPtr<nsIInputStream> byteStream;
+ rv = NS_NewByteInputStream(
+ getter_AddRefs(byteStream),
+ mozilla::Span(
+ reinterpret_cast<const char*>(bytes->GetElements().Elements()),
+ bytes->Length()),
+ NS_ASSIGNMENT_COPY);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ rv = aTransferable->SetTransferData(flavorStr.get(), byteStream);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsClipboard::EmptyNativeClipboardData(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (!jni::IsAvailable()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ java::Clipboard::Clear(java::GeckoAppShell::GetApplicationContext());
+
+ return NS_OK;
+}
+
+mozilla::Result<int32_t, nsresult>
+nsClipboard::GetNativeClipboardSequenceNumber(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (!jni::IsAvailable()) {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ return java::Clipboard::GetSequenceNumber(
+ java::GeckoAppShell::GetApplicationContext());
+}
+
+mozilla::Result<bool, nsresult>
+nsClipboard::HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (!jni::IsAvailable()) {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ for (auto& flavor : aFlavorList) {
+ if (java::Clipboard::HasData(java::GeckoAppShell::GetApplicationContext(),
+ NS_ConvertASCIItoUTF16(flavor))) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/widget/android/nsClipboard.h b/widget/android/nsClipboard.h
new file mode 100644
index 0000000000..d3a45d47e5
--- /dev/null
+++ b/widget/android/nsClipboard.h
@@ -0,0 +1,36 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef NS_CLIPBOARD_H
+#define NS_CLIPBOARD_H
+
+#include "nsBaseClipboard.h"
+
+class nsClipboard final : public nsBaseClipboard {
+ private:
+ ~nsClipboard();
+
+ public:
+ nsClipboard();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ static nsresult GetTextFromTransferable(nsITransferable* aTransferable,
+ nsString& aText, nsString& aHTML);
+
+ protected:
+ // Implement the native clipboard behavior.
+ NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) override;
+ mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber(
+ int32_t aWhichClipboard) override;
+ mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override;
+};
+
+#endif
diff --git a/widget/android/nsDeviceContextAndroid.cpp b/widget/android/nsDeviceContextAndroid.cpp
new file mode 100644
index 0000000000..e6643ef9b3
--- /dev/null
+++ b/widget/android/nsDeviceContextAndroid.cpp
@@ -0,0 +1,100 @@
+/* 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 "nsDeviceContextAndroid.h"
+
+#include "mozilla/gfx/PrintTargetPDF.h"
+#include "mozilla/RefPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "nsIPrintSettings.h"
+#include "nsIFileStreams.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsAnonymousTemporaryFile.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+NS_IMPL_ISUPPORTS(nsDeviceContextSpecAndroid, nsIDeviceContextSpec)
+
+nsDeviceContextSpecAndroid::~nsDeviceContextSpecAndroid() {
+ if (mTempFile) {
+ mTempFile->Remove(false);
+ }
+}
+
+already_AddRefed<PrintTarget> nsDeviceContextSpecAndroid::MakePrintTarget() {
+ double width, height;
+ mPrintSettings->GetEffectiveSheetSize(&width, &height);
+
+ // convert twips to points
+ width /= TWIPS_PER_POINT_FLOAT;
+ height /= TWIPS_PER_POINT_FLOAT;
+
+ auto stream = [&]() -> nsCOMPtr<nsIOutputStream> {
+ if (mPrintSettings->GetOutputDestination() ==
+ nsIPrintSettings::kOutputDestinationStream) {
+ nsCOMPtr<nsIOutputStream> out;
+ mPrintSettings->GetOutputStream(getter_AddRefs(out));
+ return out;
+ }
+ if (NS_FAILED(
+ NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(mTempFile)))) {
+ return nullptr;
+ }
+ // Print to printer not supported...
+ nsCOMPtr<nsIFileOutputStream> s =
+ do_CreateInstance("@mozilla.org/network/file-output-stream;1");
+ if (NS_FAILED(s->Init(mTempFile, -1, -1, 0))) {
+ return nullptr;
+ }
+ return s;
+ }();
+
+ return PrintTargetPDF::CreateOrNull(stream, IntSize::Ceil(width, height));
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecAndroid::Init(nsIPrintSettings* aPS, bool aIsPrintPreview) {
+ mPrintSettings = aPS;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecAndroid::BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage,
+ int32_t aEndPage) {
+ return NS_OK;
+}
+
+RefPtr<PrintEndDocumentPromise> nsDeviceContextSpecAndroid::EndDocument() {
+ return nsIDeviceContextSpec::EndDocumentPromiseFromResult(DoEndDocument(),
+ __func__);
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecAndroid::DoEndDocument() {
+ if (mPrintSettings->GetOutputDestination() ==
+ nsIPrintSettings::kOutputDestinationFile &&
+ mTempFile) {
+ nsAutoString targetPath;
+ mPrintSettings->GetToFileName(targetPath);
+ nsCOMPtr<nsIFile> destFile;
+ MOZ_TRY(NS_NewLocalFile(targetPath, false, getter_AddRefs(destFile)));
+ nsAutoString destLeafName;
+ MOZ_TRY(destFile->GetLeafName(destLeafName));
+
+ nsCOMPtr<nsIFile> destDir;
+ MOZ_TRY(destFile->GetParent(getter_AddRefs(destDir)));
+
+ MOZ_TRY(mTempFile->MoveTo(destDir, destLeafName));
+ destFile->SetPermissions(0666);
+
+ mTempFile = nullptr;
+ }
+ return NS_OK;
+}
diff --git a/widget/android/nsDeviceContextAndroid.h b/widget/android/nsDeviceContextAndroid.h
new file mode 100644
index 0000000000..094f391151
--- /dev/null
+++ b/widget/android/nsDeviceContextAndroid.h
@@ -0,0 +1,36 @@
+/* -*- 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/. */
+#ifndef nsDeviceContextAndroid_h__
+#define nsDeviceContextAndroid_h__
+
+#include "nsIDeviceContextSpec.h"
+#include "nsCOMPtr.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/PrintPromise.h"
+
+class nsDeviceContextSpecAndroid final : public nsIDeviceContextSpec {
+ private:
+ virtual ~nsDeviceContextSpecAndroid();
+
+ public:
+ using IntSize = mozilla::gfx::IntSize;
+
+ NS_DECL_ISUPPORTS
+
+ already_AddRefed<PrintTarget> MakePrintTarget() final;
+
+ NS_IMETHOD Init(nsIPrintSettings* aPS, bool aIsPrintPreview) override;
+ NS_IMETHOD BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) override;
+ RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() override;
+ NS_IMETHOD BeginPage(const IntSize& aSizeInPoints) override { return NS_OK; }
+ NS_IMETHOD EndPage() override { return NS_OK; }
+
+ private:
+ nsresult DoEndDocument();
+ nsCOMPtr<nsIFile> mTempFile;
+};
+#endif // nsDeviceContextAndroid_h__
diff --git a/widget/android/nsDragService.cpp b/widget/android/nsDragService.cpp
new file mode 100644
index 0000000000..8f12ab65e3
--- /dev/null
+++ b/widget/android/nsDragService.cpp
@@ -0,0 +1,272 @@
+/* -*- 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 "nsDragService.h"
+
+#include "AndroidGraphics.h"
+#include "AndroidWidgetUtils.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/java/GeckoDragAndDropWrappers.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "nsArrayUtils.h"
+#include "nsClipboard.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIArray.h"
+#include "nsITransferable.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsViewManager.h"
+#include "nsWindow.h"
+
+NS_IMPL_ISUPPORTS_INHERITED0(nsDragService, nsBaseDragService)
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+StaticRefPtr<nsDragService> sDragServiceInstance;
+
+/* static */
+already_AddRefed<nsDragService> nsDragService::GetInstance() {
+ if (!sDragServiceInstance) {
+ sDragServiceInstance = new nsDragService();
+ ClearOnShutdown(&sDragServiceInstance);
+ }
+
+ RefPtr<nsDragService> service = sDragServiceInstance.get();
+ return service.forget();
+}
+
+static nsWindow* GetWindow(dom::Document* aDocument) {
+ if (!aDocument) {
+ return nullptr;
+ }
+
+ PresShell* presShell = aDocument->GetPresShell();
+ if (!presShell) {
+ return nullptr;
+ }
+
+ RefPtr<nsViewManager> vm = presShell->GetViewManager();
+ if (!vm) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIWidget> widget = vm->GetRootWidget();
+ if (!widget) {
+ return nullptr;
+ }
+
+ RefPtr<nsWindow> window = nsWindow::From(widget);
+ return window.get();
+}
+
+nsresult nsDragService::InvokeDragSessionImpl(
+ nsIArray* aTransferableArray, const Maybe<CSSIntRegion>& aRegion,
+ uint32_t aActionType) {
+ if (jni::GetAPIVersion() < 24) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ uint32_t count = 0;
+ aTransferableArray->GetLength(&count);
+ if (count != 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsITransferable> transferable =
+ do_QueryElementAt(aTransferableArray, 0);
+
+ nsAutoString html;
+ nsAutoString text;
+ nsresult rv = nsClipboard::GetTextFromTransferable(transferable, text, html);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ java::GeckoDragAndDrop::SetDragData(text, html);
+
+ if (nsWindow* window = GetWindow(mSourceDocument)) {
+ mTransferable = transferable;
+
+ nsBaseDragService::StartDragSession();
+ nsBaseDragService::OpenDragPopup();
+
+ auto bitmap = CreateDragImage(mSourceNode, aRegion);
+ window->StartDragAndDrop(bitmap);
+
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItem) {
+ if (!aTransferable) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& flavor : flavors) {
+ nsCOMPtr<nsISupports> data;
+ rv = mTransferable->GetTransferData(flavor.get(), getter_AddRefs(data));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ rv = aTransferable->SetTransferData(flavor.get(), data);
+ if (NS_SUCCEEDED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsDragService::GetNumDropItems(uint32_t* aNumItems) {
+ if (mTransferable) {
+ *aNumItems = 1;
+ return NS_OK;
+ }
+ *aNumItems = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) {
+ *_retval = false;
+
+ nsDependentCString dataFlavor(aDataFlavor);
+ auto logging = MakeScopeExit([&] {
+ MOZ_DRAGSERVICE_LOG("IsDataFlavorSupported: %s is%s found", aDataFlavor,
+ *_retval ? "" : " not");
+ });
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = mTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ for (const auto& flavor : flavors) {
+ if (dataFlavor.Equals(flavor)) {
+ *_retval = true;
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
+ java::GeckoDragAndDrop::EndDragSession();
+
+ nsresult rv = nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers);
+ mTransferable = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDragService::UpdateDragImage(nsINode* aImage, int32_t aImageX,
+ int32_t aImageY) {
+ nsBaseDragService::UpdateDragImage(aImage, aImageX, aImageY);
+ auto bitmap = CreateDragImage(mSourceNode, Nothing());
+
+ if (nsWindow* window = GetWindow(mSourceDocument)) {
+ window->UpdateDragImage(bitmap);
+ }
+
+ return NS_OK;
+}
+
+bool nsDragService::MustUpdateDataTransfer(EventMessage aMessage) {
+ // Android's drag and drop API sets drop item in drop event.
+ // So we have to invalidate data transfer cache on drop event.
+ return aMessage == eDrop;
+}
+
+java::sdk::Bitmap::LocalRef nsDragService::CreateDragImage(
+ nsINode* aNode, const Maybe<CSSIntRegion>& aRegion) {
+ LayoutDeviceIntRect dragRect;
+ RefPtr<SourceSurface> surface;
+ nsPresContext* pc;
+ DrawDrag(aNode, aRegion, mScreenPosition, &dragRect, &surface, &pc);
+ if (!surface) {
+ return nullptr;
+ }
+
+ RefPtr<DataSourceSurface> destDataSurface =
+ AndroidWidgetUtils::GetDataSourceSurfaceForAndroidBitmap(
+ surface, &dragRect, dragRect.width * 4);
+ if (!destDataSurface) {
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap destMap(destDataSurface,
+ DataSourceSurface::READ);
+
+ java::sdk::Bitmap::LocalRef bitmap;
+ auto pixels = mozilla::jni::ByteBuffer::New(
+ reinterpret_cast<int8_t*>(destMap.GetData()),
+ destMap.GetStride() * destDataSurface->GetSize().height);
+ bitmap = java::sdk::Bitmap::CreateBitmap(
+ dragRect.width, dragRect.height, java::sdk::Bitmap::Config::ARGB_8888());
+ bitmap->CopyPixelsFromBuffer(pixels);
+ return bitmap;
+}
+
+void nsDragService::SetData(nsITransferable* aTransferable) {
+ mTransferable = aTransferable;
+ // Reset DataTransfer
+ mDataTransfer = nullptr;
+}
+
+// static
+void nsDragService::SetDropData(
+ mozilla::java::GeckoDragAndDrop::DropData::Param aDropData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<nsDragService> dragService = nsDragService::GetInstance();
+ if (!dragService) {
+ return;
+ }
+
+ if (!aDropData) {
+ dragService->SetData(nullptr);
+ return;
+ }
+
+ nsCString mime(aDropData->MimeType()->ToCString());
+
+ if (mime.EqualsLiteral("application/x-moz-draganddrop")) {
+ // The drop data isn't changed.
+ return;
+ }
+
+ if (!mime.EqualsLiteral("text/plain") && !mime.EqualsLiteral("text/html")) {
+ // Not supported data.
+ dragService->SetData(nullptr);
+ return;
+ }
+
+ nsString buffer(aDropData->Text()->ToString());
+ nsCOMPtr<nsISupports> wrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ mime, buffer.get(), buffer.Length() * 2, getter_AddRefs(wrapper));
+ if (!wrapper) {
+ dragService->SetData(nullptr);
+ return;
+ }
+ nsCOMPtr<nsITransferable> transferable =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ transferable->Init(nullptr);
+ transferable->SetTransferData(mime.get(), wrapper);
+ dragService->SetData(transferable);
+}
diff --git a/widget/android/nsDragService.h b/widget/android/nsDragService.h
new file mode 100644
index 0000000000..5e3a32ef3d
--- /dev/null
+++ b/widget/android/nsDragService.h
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+#ifndef nsDragService_h__
+#define nsDragService_h__
+
+#include "nsBaseDragService.h"
+
+#include "AndroidGraphics.h"
+#include "mozilla/java/GeckoDragAndDropNatives.h"
+
+class nsITransferable;
+
+class nsDragService final : public nsBaseDragService {
+ public:
+ nsDragService() = default;
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ static already_AddRefed<nsDragService> GetInstance();
+
+ // nsIDragSession
+ NS_IMETHOD GetData(nsITransferable* aTransferable, uint32_t anItem) override;
+ NS_IMETHOD GetNumDropItems(uint32_t* aNumItems) override;
+ NS_IMETHOD IsDataFlavorSupported(const char* aDataFlavor,
+ bool* _retval) override;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndDragSession(bool aDoneDrag,
+ uint32_t aKeyModifiers) override;
+ NS_IMETHOD
+ UpdateDragImage(nsINode* aImage, int32_t aImageX, int32_t aImageY) override;
+ virtual bool MustUpdateDataTransfer(mozilla::EventMessage aMessage) override;
+
+ void SetData(nsITransferable* aTransferable);
+
+ static void SetDropData(
+ mozilla::java::GeckoDragAndDrop::DropData::Param aDropData);
+
+ protected:
+ virtual ~nsDragService() = default;
+
+ // nsBaseDragService
+ MOZ_CAN_RUN_SCRIPT nsresult
+ InvokeDragSessionImpl(nsIArray* anArrayTransferables,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ uint32_t aActionType) override;
+
+ private:
+ mozilla::java::sdk::Bitmap::LocalRef CreateDragImage(
+ nsINode* aNode, const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion);
+
+ // our source data items
+ nsCOMPtr<nsITransferable> mTransferable;
+};
+
+#endif // nsDragService_h__
diff --git a/widget/android/nsIAndroidBridge.idl b/widget/android/nsIAndroidBridge.idl
new file mode 100644
index 0000000000..b30ed60d77
--- /dev/null
+++ b/widget/android/nsIAndroidBridge.idl
@@ -0,0 +1,58 @@
+/* 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 "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+
+[scriptable, uuid(e64c39b8-b8ec-477d-aef5-89d517ff9219)]
+interface nsIAndroidEventCallback : nsISupports
+{
+ [implicit_jscontext]
+ void onSuccess([optional] in jsval data);
+ [implicit_jscontext]
+ void onError([optional] in jsval data);
+};
+
+[scriptable, function, uuid(819ee2db-d3b8-46dd-a476-40f89c49133c)]
+interface nsIAndroidEventFinalizer : nsISupports
+{
+ void onFinalize();
+};
+
+[scriptable, function, uuid(73569a75-78eb-4c7f-82b9-2d4f5ccf44c3)]
+interface nsIAndroidEventListener : nsISupports
+{
+ void onEvent(in AString event,
+ [optional] in jsval data,
+ [optional] in nsIAndroidEventCallback callback);
+};
+
+[scriptable, uuid(e98bf792-4145-411e-b298-8219d9b03817)]
+interface nsIAndroidEventDispatcher : nsISupports
+{
+ [implicit_jscontext]
+ void dispatch(in jsval event,
+ [optional] in jsval data,
+ [optional] in nsIAndroidEventCallback callback,
+ [optional] in nsIAndroidEventFinalizer finalizer);
+ [implicit_jscontext]
+ void registerListener(in nsIAndroidEventListener listener,
+ in jsval events);
+ [implicit_jscontext]
+ void unregisterListener(in nsIAndroidEventListener listener,
+ in jsval events);
+};
+
+[scriptable, uuid(60a78a94-6117-432f-9d49-304913a931c5)]
+interface nsIAndroidView : nsIAndroidEventDispatcher
+{
+ [implicit_jscontext] readonly attribute jsval initData;
+};
+
+[scriptable, uuid(1beb70d3-70f3-4742-98cc-a3d301b26c0c)]
+interface nsIAndroidBridge : nsIAndroidEventDispatcher
+{
+ nsIAndroidEventDispatcher getDispatcherByName(in string name);
+};
diff --git a/widget/android/nsLookAndFeel.cpp b/widget/android/nsLookAndFeel.cpp
new file mode 100644
index 0000000000..02919e0dbf
--- /dev/null
+++ b/widget/android/nsLookAndFeel.cpp
@@ -0,0 +1,460 @@
+/* -*- Mode: C++; tab-width: 20; 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 "nsStyleConsts.h"
+#include "nsXULAppAPI.h"
+#include "nsLookAndFeel.h"
+#include "Theme.h"
+#include "gfxFont.h"
+#include "gfxFontConstants.h"
+#include "mozilla/FontPropertyTypes.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+#include "mozilla/java/GeckoRuntimeWrappers.h"
+#include "mozilla/java/GeckoSystemStateListenerWrappers.h"
+#include "ThemeColors.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+static const char16_t UNICODE_BULLET = 0x2022;
+
+nsLookAndFeel::nsLookAndFeel() = default;
+
+nsLookAndFeel::~nsLookAndFeel() = default;
+
+nsresult nsLookAndFeel::GetSystemColors() {
+ if (!jni::IsAvailable()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto arr = java::GeckoAppShell::GetSystemColors();
+ if (!arr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JNIEnv* const env = arr.Env();
+ uint32_t len = static_cast<uint32_t>(env->GetArrayLength(arr.Get()));
+ jint* elements = env->GetIntArrayElements(arr.Get(), 0);
+
+ uint32_t colorsCount = sizeof(AndroidSystemColors) / sizeof(nscolor);
+ if (len < colorsCount) colorsCount = len;
+
+ // Convert Android colors to nscolor by switching R and B in the ARGB 32 bit
+ // value
+ nscolor* colors = (nscolor*)&mSystemColors;
+
+ for (uint32_t i = 0; i < colorsCount; i++) {
+ uint32_t androidColor = static_cast<uint32_t>(elements[i]);
+ uint8_t r = (androidColor & 0x00ff0000) >> 16;
+ uint8_t b = (androidColor & 0x000000ff);
+ colors[i] = (androidColor & 0xff00ff00) | (b << 16) | r;
+ }
+
+ env->ReleaseIntArrayElements(arr.Get(), elements, 0);
+
+ return NS_OK;
+}
+
+void nsLookAndFeel::NativeInit() {
+ EnsureInitSystemColors();
+ EnsureInitShowPassword();
+ RecordTelemetry();
+}
+
+void nsLookAndFeel::RefreshImpl() {
+ mInitializedSystemColors = false;
+ mInitializedShowPassword = false;
+ nsXPLookAndFeel::RefreshImpl();
+}
+
+nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aColorScheme,
+ nscolor& aColor) {
+ EnsureInitSystemColors();
+ if (!mInitializedSystemColors) {
+ // Failure to initialize colors is an error condition. Return black.
+ aColor = 0;
+ return NS_ERROR_FAILURE;
+ }
+
+ // Highlight/Highlighttext have native equivalents that we can map to (on
+ // Android) which should work fine, regardless of the color-scheme.
+ switch (aID) {
+ case ColorID::Highlight: {
+ // Matched to action_accent in java codebase. This works fine with both
+ // light and dark color scheme.
+ nscolor accent =
+ Color(ColorID::Accentcolor, aColorScheme, UseStandins::No);
+ aColor =
+ NS_RGBA(NS_GET_R(accent), NS_GET_G(accent), NS_GET_B(accent), 78);
+ return NS_OK;
+ }
+ case ColorID::Highlighttext:
+ // Selection background is transparent enough that any foreground color
+ // will do.
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ return NS_OK;
+ default:
+ break;
+ }
+
+ if (aColorScheme == ColorScheme::Dark) {
+ if (auto darkColor = GenericDarkColor(aID)) {
+ aColor = *darkColor;
+ return NS_OK;
+ }
+ }
+
+ // XXX we'll want to use context.obtainStyledAttributes on the java side to
+ // get all of these; see TextView.java for a good example.
+ auto UseNativeAccent = [this] {
+ return mSystemColors.colorAccent &&
+ StaticPrefs::widget_non_native_theme_use_theme_accent();
+ };
+
+ switch (aID) {
+ // These colors don't seem to be used for anything anymore in Mozilla
+ // The CSS2 colors below are used.
+ case ColorID::ThemedScrollbarThumbInactive:
+ case ColorID::ThemedScrollbarThumb:
+ // We don't need to care about the Active and Hover colors because Android
+ // scrollbars can't be hovered (they always have pointer-events: none).
+ aColor = NS_RGBA(119, 119, 119, 102);
+ break;
+
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ aColor = mSystemColors.textColorHighlight;
+ break;
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ aColor = mSystemColors.textColorPrimaryInverse;
+ break;
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ aColor = NS_TRANSPARENT;
+ break;
+
+ // css2 http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
+ case ColorID::Activeborder: // active window border
+ case ColorID::Appworkspace: // MDI background color
+ case ColorID::Activecaption: // active window caption background
+ case ColorID::Background: // desktop background
+ case ColorID::Inactiveborder: // inactive window border
+ case ColorID::Inactivecaption: // inactive window caption
+ case ColorID::Scrollbar: // scrollbar gray area
+ aColor = mSystemColors.colorBackground;
+ break;
+ case ColorID::Graytext: // disabled text in windows, menus, etc.
+ aColor = NS_RGB(0xb1, 0xa5, 0x98);
+ break;
+ // FIXME: -moz-cellhighlight should show some kind of unfocused state.
+ case ColorID::MozCellhighlight:
+ case ColorID::Selecteditem:
+ case ColorID::Accentcolor:
+ aColor = UseNativeAccent() ? mSystemColors.colorAccent
+ : GetStandinForNativeColor(
+ ColorID::Accentcolor, aColorScheme);
+ break;
+ case ColorID::MozCellhighlighttext:
+ case ColorID::Selecteditemtext:
+ case ColorID::Accentcolortext:
+ aColor = UseNativeAccent() ? ThemeColors::ComputeCustomAccentForeground(
+ mSystemColors.colorAccent)
+ : GetStandinForNativeColor(
+ ColorID::Accentcolortext, aColorScheme);
+ break;
+ case ColorID::Fieldtext:
+ aColor = NS_RGB(0x1a, 0x1a, 0x1a);
+ break;
+ case ColorID::Inactivecaptiontext:
+ // text in inactive window caption
+ aColor = mSystemColors.textColorTertiary;
+ break;
+ case ColorID::Infobackground:
+ aColor = NS_RGB(0xf5, 0xf5, 0xb5);
+ break;
+ case ColorID::Infotext:
+ case ColorID::Threeddarkshadow: // 3-D shadow outer edge color
+ aColor = NS_RGB(0x00, 0x00, 0x00);
+ break;
+ case ColorID::Menu:
+ aColor = NS_RGB(0xf7, 0xf5, 0xf3);
+ break;
+
+ case ColorID::Buttonface:
+ case ColorID::MozButtondisabledface:
+ case ColorID::Threedface:
+ case ColorID::Threedlightshadow:
+ case ColorID::Buttonborder:
+ case ColorID::MozDisabledfield:
+ aColor = NS_RGB(0xec, 0xe7, 0xe2);
+ break;
+
+ case ColorID::Buttonhighlight:
+ case ColorID::Field:
+ case ColorID::Threedhighlight:
+ case ColorID::MozCombobox:
+ case ColorID::MozEventreerow:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ break;
+
+ case ColorID::Buttonshadow:
+ case ColorID::Threedshadow:
+ aColor = NS_RGB(0xae, 0xa1, 0x94);
+ break;
+
+ case ColorID::MozDialog:
+ case ColorID::Window:
+ case ColorID::Windowframe:
+ aColor = NS_RGB(0xef, 0xeb, 0xe7);
+ break;
+ case ColorID::Buttontext:
+ case ColorID::Captiontext:
+ case ColorID::Menutext:
+ case ColorID::MozButtonhovertext:
+ case ColorID::MozDialogtext:
+ case ColorID::MozComboboxtext:
+ case ColorID::Windowtext:
+ case ColorID::MozColheadertext:
+ case ColorID::MozColheaderhovertext:
+ aColor = NS_RGB(0x10, 0x10, 0x10);
+ break;
+ case ColorID::MozButtonhoverface:
+ case ColorID::MozButtonactiveface:
+ aColor = NS_RGB(0xf3, 0xf0, 0xed);
+ break;
+ case ColorID::MozMenuhover:
+ aColor = NS_RGB(0xee, 0xee, 0xee);
+ break;
+ case ColorID::MozMenubarhovertext:
+ case ColorID::MozMenuhovertext:
+ aColor = NS_RGB(0x77, 0x77, 0x77);
+ break;
+ case ColorID::MozOddtreerow:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::MozNativehyperlinktext:
+ aColor = NS_RGB(0, 0, 0xee);
+ break;
+ case ColorID::Marktext:
+ case ColorID::Mark:
+ case ColorID::SpellCheckerUnderline:
+ aColor = GetStandinForNativeColor(aID, aColorScheme);
+ break;
+ default:
+ /* default color is BLACK */
+ aColor = 0;
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ nsresult rv = NS_OK;
+
+ switch (aID) {
+ case IntID::ScrollbarFadeBeginDelay:
+ aResult = 450;
+ break;
+
+ case IntID::ScrollbarFadeDuration:
+ aResult = 300;
+ break;
+
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ break;
+
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 3;
+ break;
+
+ case IntID::CaretBlinkTime:
+ aResult = 500;
+ break;
+
+ case IntID::CaretBlinkCount:
+ aResult = 10;
+ break;
+
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+
+ case IntID::SelectTextfieldsOnKeyFocus:
+ // Select textfield content when focused by kbd
+ // used by EventStateManager::sTextfieldSelectModel
+ aResult = 1;
+ break;
+
+ case IntID::SubmenuDelay:
+ aResult = 200;
+ break;
+
+ case IntID::TooltipDelay:
+ aResult = 500;
+ break;
+
+ case IntID::MenusCanOverlapOSBar:
+ // we want XUL popups to be able to overlap the task bar.
+ aResult = 1;
+ break;
+
+ case IntID::ScrollArrowStyle:
+ aResult = eScrollArrowStyle_Single;
+ break;
+
+ case IntID::UseOverlayScrollbars:
+ aResult = 1;
+ break;
+
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = int32_t(StyleTextDecorationStyle::Wavy);
+ break;
+
+ case IntID::ScrollbarButtonAutoRepeatBehavior:
+ aResult = 0;
+ break;
+
+ case IntID::ContextMenuOffsetVertical:
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 2;
+ break;
+
+ case IntID::PrefersReducedMotion:
+ aResult = java::GeckoSystemStateListener::PrefersReducedMotion();
+ break;
+
+ case IntID::UseAccessibilityTheme:
+ // If high contrast is enabled, enable prefers-reduced-transparency media
+ // query as well as there is no dedicated option.
+ case IntID::PrefersReducedTransparency:
+ aResult = java::GeckoSystemStateListener::PrefersContrast();
+ break;
+
+ case IntID::InvertedColors:
+ aResult = java::GeckoSystemStateListener::IsInvertedColors();
+ break;
+
+ case IntID::PrimaryPointerCapabilities:
+ aResult = java::GeckoAppShell::GetAllPointerCapabilities();
+
+ // We cannot assume what is primary device, so we use Blink's way for web
+ // compatibility (https://crbug.com/136119#c6). If having coarse
+ // capability in any devices, return it.
+ if (aResult & static_cast<int32_t>(PointerCapabilities::Coarse)) {
+ aResult = static_cast<int32_t>(PointerCapabilities::Coarse);
+ }
+ break;
+
+ case IntID::AllPointerCapabilities:
+ aResult = java::GeckoAppShell::GetAllPointerCapabilities();
+ break;
+
+ case IntID::SystemUsesDarkTheme: {
+ java::GeckoRuntime::LocalRef runtime = java::GeckoRuntime::GetInstance();
+ aResult = runtime && runtime->UsesDarkTheme();
+ break;
+ }
+
+ case IntID::DragThresholdX:
+ case IntID::DragThresholdY:
+ // Threshold where a tap becomes a drag, in 1/240" reference pixels.
+ aResult = 25;
+ break;
+
+ case IntID::TouchDeviceSupportPresent:
+ // Touch support is always enabled on android.
+ aResult = 1;
+ break;
+
+ default:
+ aResult = 0;
+ rv = NS_ERROR_FAILURE;
+ }
+
+ return rv;
+}
+
+nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ nsresult rv = NS_OK;
+
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::TextScaleFactor: {
+ java::GeckoRuntime::LocalRef runtime = java::GeckoRuntime::GetInstance();
+ aResult = runtime ? runtime->TextScaleFactor() : 1.0f;
+ break;
+ }
+ default:
+ aResult = -1.0;
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ return rv;
+}
+
+bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ aFontName.AssignLiteral("Roboto");
+ aFontStyle.style = FontSlantStyle::NORMAL;
+ aFontStyle.weight = FontWeight::NORMAL;
+ aFontStyle.stretch = FontStretch::NORMAL;
+ aFontStyle.size = 9.0 * 96.0f / 72.0f;
+ aFontStyle.systemFont = true;
+ return true;
+}
+
+bool nsLookAndFeel::GetEchoPasswordImpl() {
+ EnsureInitShowPassword();
+ return mShowPassword;
+}
+
+uint32_t nsLookAndFeel::GetPasswordMaskDelayImpl() {
+ // This value is hard-coded in Android OS's PasswordTransformationMethod.java
+ return 1500;
+}
+
+char16_t nsLookAndFeel::GetPasswordCharacterImpl() {
+ // This value is hard-coded in Android OS's PasswordTransformationMethod.java
+ return UNICODE_BULLET;
+}
+
+void nsLookAndFeel::EnsureInitSystemColors() {
+ if (!mInitializedSystemColors) {
+ mInitializedSystemColors = NS_SUCCEEDED(GetSystemColors());
+ }
+}
+
+void nsLookAndFeel::EnsureInitShowPassword() {
+ if (!mInitializedShowPassword && jni::IsAvailable()) {
+ mShowPassword = java::GeckoAppShell::GetShowPasswordSetting();
+ mInitializedShowPassword = true;
+ }
+}
diff --git a/widget/android/nsLookAndFeel.h b/widget/android/nsLookAndFeel.h
new file mode 100644
index 0000000000..128b871a08
--- /dev/null
+++ b/widget/android/nsLookAndFeel.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+#ifndef __nsLookAndFeel
+#define __nsLookAndFeel
+
+#include "nsXPLookAndFeel.h"
+
+namespace mozilla {
+// The order and number of the members in this structure must correspond
+// to the attrsAppearance array in GeckoAppShell.getSystemColors()
+struct AndroidSystemColors {
+ nscolor textColorPrimary;
+ nscolor textColorPrimaryInverse;
+ nscolor textColorSecondary;
+ nscolor textColorSecondaryInverse;
+ nscolor textColorTertiary;
+ nscolor textColorTertiaryInverse;
+ nscolor textColorHighlight;
+ nscolor colorForeground;
+ nscolor colorBackground;
+ nscolor panelColorForeground;
+ nscolor panelColorBackground;
+ nscolor colorAccent;
+};
+} // namespace mozilla
+
+class nsLookAndFeel final : public nsXPLookAndFeel {
+ public:
+ explicit nsLookAndFeel();
+ virtual ~nsLookAndFeel();
+
+ void NativeInit() final;
+ virtual void RefreshImpl() override;
+ nsresult NativeGetInt(IntID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID, float& aResult) override;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override;
+ bool NativeGetFont(FontID aID, nsString& aName,
+ gfxFontStyle& aStyle) override;
+ bool GetEchoPasswordImpl() override;
+ uint32_t GetPasswordMaskDelayImpl() override;
+ char16_t GetPasswordCharacterImpl() override;
+
+ protected:
+ bool mInitializedSystemColors = false;
+ mozilla::AndroidSystemColors mSystemColors;
+ bool mInitializedShowPassword = false;
+ bool mShowPassword = false;
+
+ nsresult GetSystemColors();
+
+ void EnsureInitSystemColors();
+ void EnsureInitShowPassword();
+};
+
+#endif
diff --git a/widget/android/nsPrintSettingsServiceAndroid.cpp b/widget/android/nsPrintSettingsServiceAndroid.cpp
new file mode 100644
index 0000000000..67f978c6e3
--- /dev/null
+++ b/widget/android/nsPrintSettingsServiceAndroid.cpp
@@ -0,0 +1,33 @@
+/* -*- 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 "nsPrintSettingsServiceAndroid.h"
+
+#include "nsPrintSettingsImpl.h"
+
+class nsPrintSettingsAndroid : public nsPrintSettings {
+ public:
+ nsPrintSettingsAndroid() {
+ // The aim here is to set up the objects enough that silent printing works
+ SetOutputFormat(nsIPrintSettings::kOutputFormatPDF);
+ SetPrinterName(u"PDF printer"_ns);
+ }
+};
+
+nsresult nsPrintSettingsServiceAndroid::_CreatePrintSettings(
+ nsIPrintSettings** _retval) {
+ nsPrintSettings* printSettings = new nsPrintSettingsAndroid();
+ NS_ENSURE_TRUE(printSettings, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(*_retval = printSettings);
+ (void)InitPrintSettingsFromPrefs(*_retval, false,
+ nsIPrintSettings::kInitSaveAll);
+ return NS_OK;
+}
+
+already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings(
+ const mozilla::PrintSettingsInitializer& aSettings) {
+ RefPtr<nsPrintSettings> settings = new nsPrintSettingsAndroid();
+ settings->InitWithInitializer(aSettings);
+ return settings.forget();
+}
diff --git a/widget/android/nsPrintSettingsServiceAndroid.h b/widget/android/nsPrintSettingsServiceAndroid.h
new file mode 100644
index 0000000000..28710feb54
--- /dev/null
+++ b/widget/android/nsPrintSettingsServiceAndroid.h
@@ -0,0 +1,18 @@
+/* -*- 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/. */
+#ifndef nsPrintSettingsServiceAndroid_h
+#define nsPrintSettingsServiceAndroid_h
+
+#include "nsPrintSettingsService.h"
+#include "nsIPrintSettings.h"
+
+class nsPrintSettingsServiceAndroid final : public nsPrintSettingsService {
+ public:
+ nsPrintSettingsServiceAndroid() {}
+
+ nsresult _CreatePrintSettings(nsIPrintSettings** _retval) override;
+};
+
+#endif // nsPrintSettingsServiceAndroid_h
diff --git a/widget/android/nsUserIdleServiceAndroid.cpp b/widget/android/nsUserIdleServiceAndroid.cpp
new file mode 100644
index 0000000000..670aebe82e
--- /dev/null
+++ b/widget/android/nsUserIdleServiceAndroid.cpp
@@ -0,0 +1,12 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 "nsUserIdleServiceAndroid.h"
+
+bool nsUserIdleServiceAndroid::PollIdleTime(uint32_t* aIdleTime) {
+ return false;
+}
diff --git a/widget/android/nsUserIdleServiceAndroid.h b/widget/android/nsUserIdleServiceAndroid.h
new file mode 100644
index 0000000000..4e089b3db6
--- /dev/null
+++ b/widget/android/nsUserIdleServiceAndroid.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef nsUserIdleServiceAndroid_h__
+#define nsUserIdleServiceAndroid_h__
+
+#include "nsUserIdleService.h"
+#include "mozilla/AppShutdown.h"
+
+class nsUserIdleServiceAndroid : public nsUserIdleService {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsUserIdleServiceAndroid,
+ nsUserIdleService)
+
+ bool PollIdleTime(uint32_t* aIdleTime) override;
+
+ static already_AddRefed<nsUserIdleServiceAndroid> GetInstance() {
+ RefPtr<nsUserIdleService> idleService = nsUserIdleService::GetInstance();
+ if (!idleService) {
+ // Avoid late instantiation or resurrection during shutdown.
+ if (mozilla::AppShutdown::IsInOrBeyond(
+ mozilla::ShutdownPhase::AppShutdownConfirmed)) {
+ return nullptr;
+ }
+ idleService = new nsUserIdleServiceAndroid();
+ }
+
+ return idleService.forget().downcast<nsUserIdleServiceAndroid>();
+ }
+
+ protected:
+ nsUserIdleServiceAndroid() {}
+ virtual ~nsUserIdleServiceAndroid() {}
+};
+
+#endif // nsUserIdleServiceAndroid_h__
diff --git a/widget/android/nsWidgetFactory.cpp b/widget/android/nsWidgetFactory.cpp
new file mode 100644
index 0000000000..00ebd20dd9
--- /dev/null
+++ b/widget/android/nsWidgetFactory.cpp
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 4; 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 "mozilla/WidgetUtils.h"
+
+#include "nsAppShell.h"
+
+#include "nsLookAndFeel.h"
+#include "nsAppShellSingleton.h"
+
+nsresult nsWidgetAndroidModuleCtor() { return nsAppShellInit(); }
+
+void nsWidgetAndroidModuleDtor() {
+ // Shutdown all XP level widget classes.
+ mozilla::widget::WidgetUtils::Shutdown();
+
+ nsLookAndFeel::Shutdown();
+ nsAppShellShutdown();
+}
diff --git a/widget/android/nsWidgetFactory.h b/widget/android/nsWidgetFactory.h
new file mode 100644
index 0000000000..934d75d8be
--- /dev/null
+++ b/widget/android/nsWidgetFactory.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef widget_android_nsWidgetFactory_h
+#define widget_android_nsWidgetFactory_h
+
+#include "nscore.h"
+#include "nsID.h"
+
+class nsISupports;
+
+nsresult nsAppShellConstructor(const nsIID& iid, void** result);
+
+nsresult nsWidgetAndroidModuleCtor();
+void nsWidgetAndroidModuleDtor();
+
+#endif // defined widget_android_nsWidgetFactory_h
diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp
new file mode 100644
index 0000000000..e9756c3f92
--- /dev/null
+++ b/widget/android/nsWindow.cpp
@@ -0,0 +1,3381 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 4; indent-tabs-mode: nil; -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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 <algorithm>
+#include <atomic>
+#include <android/log.h>
+#include <android/native_window.h>
+#include <android/native_window_jni.h>
+#include <math.h>
+#include <queue>
+#include <type_traits>
+#include <unistd.h>
+
+#include "AndroidBridge.h"
+#include "AndroidBridgeUtilities.h"
+#include "AndroidCompositorWidget.h"
+#include "AndroidContentController.h"
+#include "AndroidDragEvent.h"
+#include "AndroidUiThread.h"
+#include "AndroidView.h"
+#include "AndroidWidgetUtils.h"
+#include "gfxContext.h"
+#include "GeckoEditableSupport.h"
+#include "GeckoViewOutputStream.h"
+#include "GeckoViewSupport.h"
+#include "GLContext.h"
+#include "GLContextProvider.h"
+#include "JavaBuiltins.h"
+#include "JavaExceptions.h"
+#include "KeyEvent.h"
+#include "MotionEvent.h"
+#include "ScopedGLHelpers.h"
+#include "ScreenHelperAndroid.h"
+#include "TouchResampler.h"
+#include "WidgetUtils.h"
+#include "WindowRenderer.h"
+
+#include "mozilla/EventForwards.h"
+#include "nsAppShell.h"
+#include "nsContentUtils.h"
+#include "nsFocusManager.h"
+#include "nsGkAtoms.h"
+#include "nsGfxCIID.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsLayoutUtils.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+#include "nsUserIdleService.h"
+#include "nsViewManager.h"
+#include "nsWidgetsCID.h"
+#include "nsWindow.h"
+
+#include "nsIWidgetListener.h"
+#include "nsIWindowWatcher.h"
+#include "nsIAppWindow.h"
+#include "nsIPrintSettings.h"
+#include "nsIPrintSettingsService.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_android.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaAdjustmentStrategy
+#include "mozilla/a11y/SessionAccessibility.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/BrowserHost.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/gfx/Swizzle.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/ipc/Shmem.h"
+#include "mozilla/java/EventDispatcherWrappers.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+#include "mozilla/java/GeckoEditableChildWrappers.h"
+#include "mozilla/java/GeckoResultWrappers.h"
+#include "mozilla/java/GeckoSessionNatives.h"
+#include "mozilla/java/GeckoSystemStateListenerWrappers.h"
+#include "mozilla/java/PanZoomControllerNatives.h"
+#include "mozilla/java/SessionAccessibilityWrappers.h"
+#include "mozilla/java/SurfaceControlManagerWrappers.h"
+#include "mozilla/jni/NativesInlines.h"
+#include "mozilla/layers/APZEventState.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/CompositorOGL.h"
+#include "mozilla/layers/CompositorSession.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/UiCompositorControllerChild.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/widget/AndroidVsync.h"
+#include "mozilla/widget/Screen.h"
+
+#define GVS_LOG(...) MOZ_LOG(sGVSupportLog, LogLevel::Warning, (__VA_ARGS__))
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+using namespace mozilla::ipc;
+
+using mozilla::dom::ContentChild;
+using mozilla::dom::ContentParent;
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::IntSize;
+using mozilla::gfx::Matrix;
+using mozilla::gfx::SurfaceFormat;
+using mozilla::java::GeckoSession;
+using mozilla::java::sdk::IllegalStateException;
+using GeckoPrintException = GeckoSession::GeckoPrintException;
+static mozilla::LazyLogModule sGVSupportLog("GeckoViewSupport");
+
+// All the toplevel windows that have been created; these are in
+// stacking order, so the window at gTopLevelWindows[0] is the topmost
+// one.
+static nsTArray<nsWindow*> gTopLevelWindows;
+
+static bool sFailedToCreateGLContext = false;
+
+// Multitouch swipe thresholds in inches
+static const double SWIPE_MAX_PINCH_DELTA_INCHES = 0.4;
+static const double SWIPE_MIN_DISTANCE_INCHES = 0.6;
+
+static const double kTouchResampleVsyncAdjustMs = 5.0;
+
+static const int32_t INPUT_RESULT_UNHANDLED =
+ java::PanZoomController::INPUT_RESULT_UNHANDLED;
+static const int32_t INPUT_RESULT_HANDLED =
+ java::PanZoomController::INPUT_RESULT_HANDLED;
+static const int32_t INPUT_RESULT_HANDLED_CONTENT =
+ java::PanZoomController::INPUT_RESULT_HANDLED_CONTENT;
+static const int32_t INPUT_RESULT_IGNORED =
+ java::PanZoomController::INPUT_RESULT_IGNORED;
+
+static const nsCString::size_type MAX_TOPLEVEL_DATA_URI_LEN = 2 * 1024 * 1024;
+
+// Unique ID given to each widget, to identify it for the
+// CompositorSurfaceManager.
+static std::atomic<int32_t> sWidgetId{0};
+
+namespace {
+template <class Instance, class Impl>
+std::enable_if_t<jni::detail::NativePtrPicker<Impl>::value ==
+ jni::detail::NativePtrType::REFPTR,
+ void>
+CallAttachNative(Instance aInstance, Impl* aImpl) {
+ Impl::AttachNative(aInstance, RefPtr<Impl>(aImpl).get());
+}
+
+template <class Instance, class Impl>
+std::enable_if_t<jni::detail::NativePtrPicker<Impl>::value ==
+ jni::detail::NativePtrType::OWNING,
+ void>
+CallAttachNative(Instance aInstance, Impl* aImpl) {
+ Impl::AttachNative(aInstance, UniquePtr<Impl>(aImpl));
+}
+
+template <class Lambda>
+bool DispatchToUiThread(const char* aName, Lambda&& aLambda) {
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ uiThread->Dispatch(NS_NewRunnableFunction(aName, std::move(aLambda)));
+ return true;
+ }
+ return false;
+}
+} // namespace
+
+namespace mozilla {
+namespace widget {
+
+using WindowPtr = jni::NativeWeakPtr<GeckoViewSupport>;
+
+/**
+ * PanZoomController handles its native calls on the UI thread, so make
+ * it separate from GeckoViewSupport.
+ */
+class NPZCSupport final
+ : public java::PanZoomController::NativeProvider::Natives<NPZCSupport> {
+ WindowPtr mWindow;
+ java::PanZoomController::NativeProvider::WeakRef mNPZC;
+
+ // Stores the returnResult of each pending motion event between
+ // HandleMotionEvent and FinishHandlingMotionEvent.
+ std::queue<std::pair<uint64_t, java::GeckoResult::GlobalRef>>
+ mPendingMotionEventReturnResults;
+
+ RefPtr<AndroidVsync> mAndroidVsync;
+ TouchResampler mTouchResampler;
+ int mPreviousButtons = 0;
+ bool mListeningToVsync = false;
+
+ // Only true if mAndroidVsync is non-null and the resampling pref is set.
+ bool mTouchResamplingEnabled = false;
+
+ template <typename Lambda>
+ class InputEvent final : public nsAppShell::Event {
+ java::PanZoomController::NativeProvider::GlobalRef mNPZC;
+ Lambda mLambda;
+
+ public:
+ InputEvent(const NPZCSupport* aNPZCSupport, Lambda&& aLambda)
+ : mNPZC(aNPZCSupport->mNPZC), mLambda(std::move(aLambda)) {}
+
+ void Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ JNIEnv* const env = jni::GetGeckoThreadEnv();
+ const auto npzcSupportWeak = GetNative(
+ java::PanZoomController::NativeProvider::LocalRef(env, mNPZC));
+ if (!npzcSupportWeak) {
+ // We already shut down.
+ env->ExceptionClear();
+ return;
+ }
+
+ auto acc = npzcSupportWeak->Access();
+ if (!acc) {
+ // We already shut down.
+ env->ExceptionClear();
+ return;
+ }
+
+ auto win = acc->mWindow.Access();
+ if (!win) {
+ // We already shut down.
+ env->ExceptionClear();
+ return;
+ }
+
+ nsWindow* const window = win->GetNsWindow();
+ if (!window) {
+ // We already shut down.
+ env->ExceptionClear();
+ return;
+ }
+
+ window->UserActivity();
+ return mLambda(window);
+ }
+
+ bool IsUIEvent() const override { return true; }
+ };
+
+ class MOZ_HEAP_CLASS Observer final : public AndroidVsync::Observer {
+ public:
+ static Observer* Create(jni::NativeWeakPtr<NPZCSupport>&& aNPZCSupport) {
+ return new Observer(std::move(aNPZCSupport));
+ }
+
+ private:
+ // Private constructor, part of a strategy to make sure
+ // we're only able to create these on the heap.
+ explicit Observer(jni::NativeWeakPtr<NPZCSupport>&& aNPZCSupport)
+ : mNPZCSupport(std::move(aNPZCSupport)) {}
+
+ void OnVsync(const TimeStamp& aTimeStamp) override {
+ auto accessor = mNPZCSupport.Access();
+
+ if (!accessor) {
+ return;
+ }
+
+ accessor->mTouchResampler.NotifyFrame(
+ aTimeStamp -
+ TimeDuration::FromMilliseconds(kTouchResampleVsyncAdjustMs));
+ accessor->ConsumeMotionEventsFromResampler();
+ }
+
+ void Dispose() override { delete this; }
+
+ jni::NativeWeakPtr<NPZCSupport> mNPZCSupport;
+ };
+
+ Observer* mObserver = nullptr;
+
+ template <typename Lambda>
+ void PostInputEvent(Lambda&& aLambda) {
+ // Use priority queue for input events.
+ nsAppShell::PostEvent(
+ MakeUnique<InputEvent<Lambda>>(this, std::move(aLambda)));
+ }
+
+ public:
+ typedef java::PanZoomController::NativeProvider::Natives<NPZCSupport> Base;
+
+ NPZCSupport(WindowPtr aWindow,
+ const java::PanZoomController::NativeProvider::LocalRef& aNPZC)
+ : mWindow(aWindow), mNPZC(aNPZC) {
+#if defined(DEBUG)
+ auto win(mWindow.Access());
+ MOZ_ASSERT(!!win);
+#endif // defined(DEBUG)
+
+ mAndroidVsync = AndroidVsync::GetInstance();
+ }
+
+ ~NPZCSupport() {
+ if (mListeningToVsync) {
+ MOZ_RELEASE_ASSERT(mAndroidVsync);
+ mAndroidVsync->UnregisterObserver(mObserver, AndroidVsync::INPUT);
+ mListeningToVsync = false;
+ }
+ }
+
+ using Base::AttachNative;
+ using Base::DisposeNative;
+
+ void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
+ RefPtr<Runnable> disposer = aDisposer;
+ // There are several considerations when shutting down NPZC. 1) The
+ // Gecko thread may destroy NPZC at any time when nsWindow closes. 2)
+ // There may be pending events on the Gecko thread when NPZC is
+ // destroyed. 3) mWindow may not be available when the pending event
+ // runs. 4) The UI thread may destroy NPZC at any time when GeckoView
+ // is destroyed. 5) The UI thread may destroy NPZC at the same time as
+ // Gecko thread trying to destroy NPZC. 6) There may be pending calls
+ // on the UI thread when NPZC is destroyed. 7) mWindow may have been
+ // cleared on the Gecko thread when the pending call happens on the UI
+ // thread.
+ //
+ // 1) happens through OnWeakNonIntrusiveDetach, which first notifies the UI
+ // thread through Destroy; Destroy then calls DisposeNative, which
+ // finally disposes the native instance back on the Gecko thread. Using
+ // Destroy to indirectly call DisposeNative here also solves 5), by
+ // making everything go through the UI thread, avoiding contention.
+ //
+ // 2) and 3) are solved by clearing mWindow, which signals to the
+ // pending event that we had shut down. In that case the event bails
+ // and does not touch mWindow.
+ //
+ // 4) happens through DisposeNative directly.
+ //
+ // 6) is solved by keeping a destroyed flag in the Java NPZC instance,
+ // and only make a pending call if the destroyed flag is not set.
+ //
+ // 7) is solved by taking a lock whenever mWindow is modified on the
+ // Gecko thread or accessed on the UI thread. That way, we don't
+ // release mWindow until the UI thread is done using it, thus avoiding
+ // the race condition.
+
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ auto npzc = java::PanZoomController::NativeProvider::GlobalRef(mNPZC);
+ if (!npzc) {
+ return;
+ }
+
+ uiThread->Dispatch(
+ NS_NewRunnableFunction("NPZCSupport::OnWeakNonIntrusiveDetach",
+ [npzc, disposer = std::move(disposer)] {
+ npzc->SetAttached(false);
+ disposer->Run();
+ }));
+ }
+ }
+
+ const java::PanZoomController::NativeProvider::Ref& GetJavaNPZC() const {
+ return mNPZC;
+ }
+
+ public:
+ void SetIsLongpressEnabled(bool aIsLongpressEnabled) {
+ RefPtr<IAPZCTreeManager> controller;
+
+ if (auto window = mWindow.Access()) {
+ nsWindow* gkWindow = window->GetNsWindow();
+ if (gkWindow) {
+ controller = gkWindow->mAPZC;
+ }
+ }
+
+ if (controller) {
+ controller->SetLongTapEnabled(aIsLongpressEnabled);
+ }
+ }
+
+ int32_t HandleScrollEvent(int64_t aTime, int32_t aMetaState, float aX,
+ float aY, float aHScroll, float aVScroll) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ RefPtr<IAPZCTreeManager> controller;
+
+ if (auto window = mWindow.Access()) {
+ nsWindow* gkWindow = window->GetNsWindow();
+ if (gkWindow) {
+ controller = gkWindow->mAPZC;
+ }
+ }
+
+ if (!controller) {
+ return INPUT_RESULT_UNHANDLED;
+ }
+
+ ScreenPoint origin = ScreenPoint(aX, aY);
+
+ if (StaticPrefs::ui_scrolling_negate_wheel_scroll()) {
+ aHScroll = -aHScroll;
+ aVScroll = -aVScroll;
+ }
+
+ ScrollWheelInput input(
+ nsWindow::GetEventTimeStamp(aTime), nsWindow::GetModifiers(aMetaState),
+ ScrollWheelInput::SCROLLMODE_SMOOTH,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, aHScroll, aVScroll, false,
+ // XXX Do we need to support auto-dir scrolling
+ // for Android widgets with a wheel device?
+ // Currently, I just leave it unimplemented. If
+ // we need to implement it, what's the extra work
+ // to do?
+ WheelDeltaAdjustmentStrategy::eNone);
+
+ APZEventResult result = controller->InputBridge()->ReceiveInputEvent(input);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return INPUT_RESULT_IGNORED;
+ }
+
+ PostInputEvent([input = std::move(input), result](nsWindow* window) {
+ WidgetWheelEvent wheelEvent = input.ToWidgetEvent(window);
+ window->ProcessUntransformedAPZEvent(&wheelEvent, result);
+ });
+
+ switch (result.GetStatus()) {
+ case nsEventStatus_eIgnore:
+ return INPUT_RESULT_UNHANDLED;
+ case nsEventStatus_eConsumeDoDefault:
+ return result.GetHandledResult()->IsHandledByRoot()
+ ? INPUT_RESULT_HANDLED
+ : INPUT_RESULT_HANDLED_CONTENT;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected nsEventStatus");
+ return INPUT_RESULT_UNHANDLED;
+ }
+ }
+
+ private:
+ static MouseInput::ButtonType GetButtonType(int button) {
+ MouseInput::ButtonType result = MouseInput::NONE;
+
+ switch (button) {
+ case java::sdk::MotionEvent::BUTTON_PRIMARY:
+ result = MouseInput::PRIMARY_BUTTON;
+ break;
+ case java::sdk::MotionEvent::BUTTON_SECONDARY:
+ result = MouseInput::SECONDARY_BUTTON;
+ break;
+ case java::sdk::MotionEvent::BUTTON_TERTIARY:
+ result = MouseInput::MIDDLE_BUTTON;
+ break;
+ default:
+ break;
+ }
+
+ return result;
+ }
+
+ static int16_t ConvertButtons(int buttons) {
+ int16_t result = 0;
+
+ if (buttons & java::sdk::MotionEvent::BUTTON_PRIMARY) {
+ result |= MouseButtonsFlag::ePrimaryFlag;
+ }
+ if (buttons & java::sdk::MotionEvent::BUTTON_SECONDARY) {
+ result |= MouseButtonsFlag::eSecondaryFlag;
+ }
+ if (buttons & java::sdk::MotionEvent::BUTTON_TERTIARY) {
+ result |= MouseButtonsFlag::eMiddleFlag;
+ }
+ if (buttons & java::sdk::MotionEvent::BUTTON_BACK) {
+ result |= MouseButtonsFlag::e4thFlag;
+ }
+ if (buttons & java::sdk::MotionEvent::BUTTON_FORWARD) {
+ result |= MouseButtonsFlag::e5thFlag;
+ }
+
+ return result;
+ }
+
+ static int32_t ConvertAPZHandledPlace(APZHandledPlace aHandledPlace) {
+ switch (aHandledPlace) {
+ case APZHandledPlace::Unhandled:
+ return INPUT_RESULT_UNHANDLED;
+ case APZHandledPlace::HandledByRoot:
+ return INPUT_RESULT_HANDLED;
+ case APZHandledPlace::HandledByContent:
+ return INPUT_RESULT_HANDLED_CONTENT;
+ case APZHandledPlace::Invalid:
+ MOZ_ASSERT_UNREACHABLE("The handled result should NOT be Invalid");
+ return INPUT_RESULT_UNHANDLED;
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown handled result");
+ return INPUT_RESULT_UNHANDLED;
+ }
+
+ static int32_t ConvertSideBits(SideBits aSideBits) {
+ int32_t ret = java::PanZoomController::SCROLLABLE_FLAG_NONE;
+ if (aSideBits & SideBits::eTop) {
+ ret |= java::PanZoomController::SCROLLABLE_FLAG_TOP;
+ }
+ if (aSideBits & SideBits::eRight) {
+ ret |= java::PanZoomController::SCROLLABLE_FLAG_RIGHT;
+ }
+ if (aSideBits & SideBits::eBottom) {
+ ret |= java::PanZoomController::SCROLLABLE_FLAG_BOTTOM;
+ }
+ if (aSideBits & SideBits::eLeft) {
+ ret |= java::PanZoomController::SCROLLABLE_FLAG_LEFT;
+ }
+ return ret;
+ }
+
+ static int32_t ConvertScrollDirections(
+ layers::ScrollDirections aScrollDirections) {
+ int32_t ret = java::PanZoomController::OVERSCROLL_FLAG_NONE;
+ if (aScrollDirections.contains(layers::HorizontalScrollDirection)) {
+ ret |= java::PanZoomController::OVERSCROLL_FLAG_HORIZONTAL;
+ }
+ if (aScrollDirections.contains(layers::VerticalScrollDirection)) {
+ ret |= java::PanZoomController::OVERSCROLL_FLAG_VERTICAL;
+ }
+ return ret;
+ }
+
+ static java::PanZoomController::InputResultDetail::LocalRef
+ ConvertAPZHandledResult(const APZHandledResult& aHandledResult) {
+ return java::PanZoomController::InputResultDetail::New(
+ ConvertAPZHandledPlace(aHandledResult.mPlace),
+ ConvertSideBits(aHandledResult.mScrollableDirections),
+ ConvertScrollDirections(aHandledResult.mOverscrollDirections));
+ }
+
+ public:
+ int32_t HandleMouseEvent(int32_t aAction, int64_t aTime, int32_t aMetaState,
+ float aX, float aY, int buttons) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ RefPtr<IAPZCTreeManager> controller;
+
+ if (auto window = mWindow.Access()) {
+ nsWindow* gkWindow = window->GetNsWindow();
+ if (gkWindow) {
+ controller = gkWindow->mAPZC;
+ }
+ }
+
+ if (!controller) {
+ return INPUT_RESULT_UNHANDLED;
+ }
+
+ MouseInput::MouseType mouseType = MouseInput::MOUSE_NONE;
+ MouseInput::ButtonType buttonType = MouseInput::NONE;
+ switch (aAction) {
+ case java::sdk::MotionEvent::ACTION_DOWN:
+ mouseType = MouseInput::MOUSE_DOWN;
+ buttonType = GetButtonType(buttons ^ mPreviousButtons);
+ mPreviousButtons = buttons;
+ break;
+ case java::sdk::MotionEvent::ACTION_UP:
+ mouseType = MouseInput::MOUSE_UP;
+ buttonType = GetButtonType(buttons ^ mPreviousButtons);
+ mPreviousButtons = buttons;
+ break;
+ case java::sdk::MotionEvent::ACTION_MOVE:
+ mouseType = MouseInput::MOUSE_MOVE;
+ break;
+ case java::sdk::MotionEvent::ACTION_HOVER_MOVE:
+ mouseType = MouseInput::MOUSE_MOVE;
+ break;
+ case java::sdk::MotionEvent::ACTION_HOVER_ENTER:
+ mouseType = MouseInput::MOUSE_WIDGET_ENTER;
+ break;
+ case java::sdk::MotionEvent::ACTION_HOVER_EXIT:
+ mouseType = MouseInput::MOUSE_WIDGET_EXIT;
+ break;
+ default:
+ break;
+ }
+
+ if (mouseType == MouseInput::MOUSE_NONE) {
+ return INPUT_RESULT_UNHANDLED;
+ }
+
+ ScreenPoint origin = ScreenPoint(aX, aY);
+
+ MouseInput input(
+ mouseType, buttonType, MouseEvent_Binding::MOZ_SOURCE_MOUSE,
+ ConvertButtons(buttons), origin, nsWindow::GetEventTimeStamp(aTime),
+ nsWindow::GetModifiers(aMetaState));
+
+ APZEventResult result = controller->InputBridge()->ReceiveInputEvent(input);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return INPUT_RESULT_IGNORED;
+ }
+
+ PostInputEvent([input = std::move(input), result](nsWindow* window) {
+ WidgetMouseEvent mouseEvent = input.ToWidgetEvent(window);
+ window->ProcessUntransformedAPZEvent(&mouseEvent, result);
+ });
+
+ switch (result.GetStatus()) {
+ case nsEventStatus_eIgnore:
+ return INPUT_RESULT_UNHANDLED;
+ case nsEventStatus_eConsumeDoDefault:
+ return result.GetHandledResult()->IsHandledByRoot()
+ ? INPUT_RESULT_HANDLED
+ : INPUT_RESULT_HANDLED_CONTENT;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected nsEventStatus");
+ return INPUT_RESULT_UNHANDLED;
+ }
+ }
+
+ // Convert MotionEvent touch radius and orientation into the format required
+ // by w3c touchevents.
+ // toolMajor and toolMinor span a rectangle that's oriented as per
+ // aOrientation, centered around the touch point.
+ static std::pair<float, ScreenSize> ConvertOrientationAndRadius(
+ float aOrientation, float aToolMajor, float aToolMinor) {
+ float angle = aOrientation * 180.0f / M_PI;
+ // w3c touchevents spec does not allow orientations == 90
+ // this shifts it to -90, which will be shifted to zero below
+ if (angle >= 90.0) {
+ angle -= 180.0f;
+ }
+
+ // w3c touchevent radii are given with an orientation between 0 and
+ // 90. The radii are found by removing the orientation and
+ // measuring the x and y radii of the resulting ellipse. For
+ // Android orientations >= 0 and < 90, use the y radius as the
+ // major radius, and x as the minor radius. However, for an
+ // orientation < 0, we have to shift the orientation by adding 90,
+ // and reverse which radius is major and minor.
+ ScreenSize radius;
+ if (angle < 0.0f) {
+ angle += 90.0f;
+ radius =
+ ScreenSize(int32_t(aToolMajor / 2.0f), int32_t(aToolMinor / 2.0f));
+ } else {
+ radius =
+ ScreenSize(int32_t(aToolMinor / 2.0f), int32_t(aToolMajor / 2.0f));
+ }
+
+ return std::make_pair(angle, radius);
+ }
+
+ void HandleMotionEvent(
+ const java::PanZoomController::NativeProvider::LocalRef& aInstance,
+ jni::Object::Param aEventData, float aScreenX, float aScreenY,
+ jni::Object::Param aResult) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ auto returnResult = java::GeckoResult::Ref::From(aResult);
+ auto eventData =
+ java::PanZoomController::MotionEventData::Ref::From(aEventData);
+ nsTArray<int32_t> pointerId(eventData->PointerId()->GetElements());
+ size_t pointerCount = pointerId.Length();
+ MultiTouchInput::MultiTouchType type;
+ size_t startIndex = 0;
+ size_t endIndex = pointerCount;
+
+ switch (eventData->Action()) {
+ case java::sdk::MotionEvent::ACTION_DOWN:
+ case java::sdk::MotionEvent::ACTION_POINTER_DOWN:
+ type = MultiTouchInput::MULTITOUCH_START;
+ break;
+ case java::sdk::MotionEvent::ACTION_MOVE:
+ type = MultiTouchInput::MULTITOUCH_MOVE;
+ break;
+ case java::sdk::MotionEvent::ACTION_UP:
+ case java::sdk::MotionEvent::ACTION_POINTER_UP:
+ // for pointer-up events we only want the data from
+ // the one pointer that went up
+ type = MultiTouchInput::MULTITOUCH_END;
+ startIndex = eventData->ActionIndex();
+ endIndex = startIndex + 1;
+ break;
+ case java::sdk::MotionEvent::ACTION_OUTSIDE:
+ case java::sdk::MotionEvent::ACTION_CANCEL:
+ type = MultiTouchInput::MULTITOUCH_CANCEL;
+ break;
+ default:
+ if (returnResult) {
+ returnResult->Complete(
+ java::sdk::Integer::ValueOf(INPUT_RESULT_UNHANDLED));
+ }
+ return;
+ }
+
+ MultiTouchInput input(type, eventData->Time(),
+ nsWindow::GetEventTimeStamp(eventData->Time()), 0);
+ input.modifiers = nsWindow::GetModifiers(eventData->MetaState());
+ input.mTouches.SetCapacity(endIndex - startIndex);
+ input.mScreenOffset =
+ ExternalIntPoint(int32_t(floorf(aScreenX)), int32_t(floorf(aScreenY)));
+
+ size_t historySize = eventData->HistorySize();
+ nsTArray<int64_t> historicalTime(
+ eventData->HistoricalTime()->GetElements());
+ MOZ_RELEASE_ASSERT(historicalTime.Length() == historySize);
+
+ // Each of these is |historySize| sets of |pointerCount| values.
+ size_t historicalDataCount = historySize * pointerCount;
+ nsTArray<float> historicalX(eventData->HistoricalX()->GetElements());
+ nsTArray<float> historicalY(eventData->HistoricalY()->GetElements());
+ nsTArray<float> historicalOrientation(
+ eventData->HistoricalOrientation()->GetElements());
+ nsTArray<float> historicalPressure(
+ eventData->HistoricalPressure()->GetElements());
+ nsTArray<float> historicalToolMajor(
+ eventData->HistoricalToolMajor()->GetElements());
+ nsTArray<float> historicalToolMinor(
+ eventData->HistoricalToolMinor()->GetElements());
+
+ MOZ_RELEASE_ASSERT(historicalX.Length() == historicalDataCount);
+ MOZ_RELEASE_ASSERT(historicalY.Length() == historicalDataCount);
+ MOZ_RELEASE_ASSERT(historicalOrientation.Length() == historicalDataCount);
+ MOZ_RELEASE_ASSERT(historicalPressure.Length() == historicalDataCount);
+ MOZ_RELEASE_ASSERT(historicalToolMajor.Length() == historicalDataCount);
+ MOZ_RELEASE_ASSERT(historicalToolMinor.Length() == historicalDataCount);
+
+ // Each of these is |pointerCount| values.
+ nsTArray<float> x(eventData->X()->GetElements());
+ nsTArray<float> y(eventData->Y()->GetElements());
+ nsTArray<float> orientation(eventData->Orientation()->GetElements());
+ nsTArray<float> pressure(eventData->Pressure()->GetElements());
+ nsTArray<float> toolMajor(eventData->ToolMajor()->GetElements());
+ nsTArray<float> toolMinor(eventData->ToolMinor()->GetElements());
+
+ MOZ_ASSERT(x.Length() == pointerCount);
+ MOZ_ASSERT(y.Length() == pointerCount);
+ MOZ_ASSERT(orientation.Length() == pointerCount);
+ MOZ_ASSERT(pressure.Length() == pointerCount);
+ MOZ_ASSERT(toolMajor.Length() == pointerCount);
+ MOZ_ASSERT(toolMinor.Length() == pointerCount);
+
+ for (size_t i = startIndex; i < endIndex; i++) {
+ auto [orien, radius] = ConvertOrientationAndRadius(
+ orientation[i], toolMajor[i], toolMinor[i]);
+
+ ScreenIntPoint point(int32_t(floorf(x[i])), int32_t(floorf(y[i])));
+ SingleTouchData singleTouchData(pointerId[i], point, radius, orien,
+ pressure[i]);
+
+ for (size_t historyIndex = 0; historyIndex < historySize;
+ historyIndex++) {
+ size_t historicalI = historyIndex * pointerCount + i;
+ auto [historicalAngle, historicalRadius] = ConvertOrientationAndRadius(
+ historicalOrientation[historicalI],
+ historicalToolMajor[historicalI], historicalToolMinor[historicalI]);
+ ScreenIntPoint historicalPoint(
+ int32_t(floorf(historicalX[historicalI])),
+ int32_t(floorf(historicalY[historicalI])));
+ singleTouchData.mHistoricalData.AppendElement(
+ SingleTouchData::HistoricalTouchData{
+ nsWindow::GetEventTimeStamp(historicalTime[historyIndex]),
+ historicalPoint,
+ {}, // mLocalScreenPoint will be computed later by APZ
+ historicalRadius,
+ historicalAngle,
+ historicalPressure[historicalI]});
+ }
+
+ input.mTouches.AppendElement(singleTouchData);
+ }
+
+ if (mAndroidVsync &&
+ eventData->Action() == java::sdk::MotionEvent::ACTION_DOWN) {
+ // Query pref value at the beginning of a touch gesture so that we don't
+ // leave events stuck in the resampler after a pref flip.
+ mTouchResamplingEnabled = StaticPrefs::android_touch_resampling_enabled();
+ }
+
+ if (!mTouchResamplingEnabled) {
+ FinishHandlingMotionEvent(std::move(input),
+ java::GeckoResult::LocalRef(returnResult));
+ return;
+ }
+
+ uint64_t eventId = mTouchResampler.ProcessEvent(std::move(input));
+ mPendingMotionEventReturnResults.push(
+ {eventId, java::GeckoResult::GlobalRef(returnResult)});
+
+ RegisterOrUnregisterForVsync(mTouchResampler.InTouchingState());
+ ConsumeMotionEventsFromResampler();
+ }
+
+ void RegisterOrUnregisterForVsync(bool aNeedVsync) {
+ MOZ_RELEASE_ASSERT(mAndroidVsync);
+ if (aNeedVsync && !mListeningToVsync) {
+ MOZ_ASSERT(!mObserver);
+ auto win = mWindow.Access();
+ if (!win) {
+ return;
+ }
+ RefPtr<nsWindow> gkWindow = win->GetNsWindow();
+ if (!gkWindow) {
+ return;
+ }
+ MutexAutoLock lock(gkWindow->GetDestroyMutex());
+ if (gkWindow->Destroyed()) {
+ return;
+ }
+ jni::NativeWeakPtr<NPZCSupport> weakPtrToThis =
+ gkWindow->GetNPZCSupportWeakPtr();
+ mObserver = Observer::Create(std::move(weakPtrToThis));
+ mAndroidVsync->RegisterObserver(mObserver, AndroidVsync::INPUT);
+ } else if (!aNeedVsync && mListeningToVsync) {
+ mAndroidVsync->UnregisterObserver(mObserver, AndroidVsync::INPUT);
+ mObserver = nullptr;
+ }
+ mListeningToVsync = aNeedVsync;
+ }
+
+ void HandleDragEvent(int32_t aAction, int64_t aTime, float aX, float aY,
+ jni::Object::Param aDropData) {
+ // APZ handles some drag event type on APZ thread, but it cannot handle all
+ // types.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (auto window = mWindow.Access()) {
+ if (nsWindow* gkWindow = window->GetNsWindow()) {
+ gkWindow->OnDragEvent(aAction, aTime, aX, aY, aDropData);
+ }
+ }
+ }
+
+ void ConsumeMotionEventsFromResampler() {
+ auto outgoing = mTouchResampler.ConsumeOutgoingEvents();
+ while (!outgoing.empty()) {
+ auto outgoingEvent = std::move(outgoing.front());
+ outgoing.pop();
+ java::GeckoResult::GlobalRef returnResult;
+ if (outgoingEvent.mEventId) {
+ // Look up the GeckoResult for this event.
+ // The outgoing events from the resampler are in the same order as the
+ // original events, and no event IDs are skipped.
+ MOZ_RELEASE_ASSERT(!mPendingMotionEventReturnResults.empty());
+ auto pair = mPendingMotionEventReturnResults.front();
+ mPendingMotionEventReturnResults.pop();
+ MOZ_RELEASE_ASSERT(pair.first == *outgoingEvent.mEventId);
+ returnResult = pair.second;
+ }
+ FinishHandlingMotionEvent(std::move(outgoingEvent.mEvent),
+ java::GeckoResult::LocalRef(returnResult));
+ }
+ }
+
+ void FinishHandlingMotionEvent(MultiTouchInput&& aInput,
+ java::GeckoResult::LocalRef&& aReturnResult) {
+ RefPtr<IAPZCTreeManager> controller;
+
+ if (auto window = mWindow.Access()) {
+ nsWindow* gkWindow = window->GetNsWindow();
+ if (gkWindow) {
+ controller = gkWindow->mAPZC;
+ }
+ }
+
+ if (!controller) {
+ if (aReturnResult) {
+ aReturnResult->Complete(java::PanZoomController::InputResultDetail::New(
+ INPUT_RESULT_UNHANDLED,
+ java::PanZoomController::SCROLLABLE_FLAG_NONE,
+ java::PanZoomController::OVERSCROLL_FLAG_NONE));
+ }
+ return;
+ }
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aReturnResult) {
+ callback = [aReturnResult = java::GeckoResult::GlobalRef(aReturnResult)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ aReturnResult->Complete(ConvertAPZHandledResult(aHandledResult));
+ };
+ }
+ APZEventResult result = controller->InputBridge()->ReceiveInputEvent(
+ aInput, std::move(callback));
+
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ if (aReturnResult) {
+ if (result.GetHandledResult() != Nothing()) {
+ aReturnResult->Complete(
+ ConvertAPZHandledResult(result.GetHandledResult().value()));
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "nsEventStatus_eConsumeNoDefault should involve a valid "
+ "APZHandledResult");
+ aReturnResult->Complete(
+ java::PanZoomController::InputResultDetail::New(
+ INPUT_RESULT_IGNORED,
+ java::PanZoomController::SCROLLABLE_FLAG_NONE,
+ java::PanZoomController::OVERSCROLL_FLAG_NONE));
+ }
+ }
+ return;
+ }
+
+ // Dispatch APZ input event on Gecko thread.
+ PostInputEvent([input = std::move(aInput), result](nsWindow* window) {
+ WidgetTouchEvent touchEvent = input.ToWidgetEvent(window);
+ window->ProcessUntransformedAPZEvent(&touchEvent, result);
+ window->DispatchHitTest(touchEvent);
+ });
+
+ if (aReturnResult && result.GetHandledResult() != Nothing()) {
+ MOZ_ASSERT(result.GetStatus() == nsEventStatus_eConsumeDoDefault ||
+ result.GetStatus() == nsEventStatus_eIgnore);
+ aReturnResult->Complete(
+ ConvertAPZHandledResult(result.GetHandledResult().value()));
+ }
+ }
+};
+
+NS_IMPL_ISUPPORTS(AndroidView, nsIAndroidEventDispatcher, nsIAndroidView)
+
+nsresult AndroidView::GetInitData(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aOut) {
+ if (!mInitData) {
+ aOut.setNull();
+ return NS_OK;
+ }
+
+ return widget::EventDispatcher::UnboxBundle(aCx, mInitData, aOut);
+}
+
+/**
+ * Compositor has some unique requirements for its native calls, so make it
+ * separate from GeckoViewSupport.
+ */
+class LayerViewSupport final
+ : public GeckoSession::Compositor::Natives<LayerViewSupport> {
+ WindowPtr mWindow;
+ GeckoSession::Compositor::WeakRef mCompositor;
+ Atomic<bool, ReleaseAcquire> mCompositorPaused;
+ java::sdk::Surface::GlobalRef mSurface;
+ java::sdk::SurfaceControl::GlobalRef mSurfaceControl;
+ int32_t mX;
+ int32_t mY;
+ int32_t mWidth;
+ int32_t mHeight;
+ // Used to communicate with the gecko compositor from the UI thread.
+ // Set in NotifyCompositorCreated and cleared in
+ // NotifyCompositorSessionLost.
+ RefPtr<UiCompositorControllerChild> mUiCompositorControllerChild;
+ // Whether we have requested a new Surface from the GeckoSession.
+ bool mRequestedNewSurface = false;
+
+ Maybe<uint32_t> mDefaultClearColor;
+
+ struct CaptureRequest {
+ explicit CaptureRequest() : mResult(nullptr) {}
+ explicit CaptureRequest(java::GeckoResult::GlobalRef aResult,
+ java::sdk::Bitmap::GlobalRef aBitmap,
+ const ScreenRect& aSource,
+ const IntSize& aOutputSize)
+ : mResult(aResult),
+ mBitmap(aBitmap),
+ mSource(aSource),
+ mOutputSize(aOutputSize) {}
+
+ // where to send the pixels
+ java::GeckoResult::GlobalRef mResult;
+
+ // where to store the pixels
+ java::sdk::Bitmap::GlobalRef mBitmap;
+
+ ScreenRect mSource;
+
+ IntSize mOutputSize;
+ };
+ std::queue<CaptureRequest> mCapturePixelsResults;
+
+ // In order to use Event::HasSameTypeAs in PostTo(), we cannot make
+ // LayerViewEvent a template because each template instantiation is
+ // a different type. So implement LayerViewEvent as a ProxyEvent.
+ class LayerViewEvent final : public nsAppShell::ProxyEvent {
+ using Event = nsAppShell::Event;
+
+ public:
+ static UniquePtr<Event> MakeEvent(UniquePtr<Event>&& event) {
+ return MakeUnique<LayerViewEvent>(std::move(event));
+ }
+
+ explicit LayerViewEvent(UniquePtr<Event>&& event)
+ : nsAppShell::ProxyEvent(std::move(event)) {}
+
+ void PostTo(LinkedList<Event>& queue) override {
+ // Give priority to compositor events, but keep in order with
+ // existing compositor events.
+ nsAppShell::Event* event = queue.getFirst();
+ while (event && event->HasSameTypeAs(this)) {
+ event = event->getNext();
+ }
+ if (event) {
+ event->setPrevious(this);
+ } else {
+ queue.insertBack(this);
+ }
+ }
+ };
+
+ public:
+ typedef GeckoSession::Compositor::Natives<LayerViewSupport> Base;
+
+ LayerViewSupport(WindowPtr aWindow,
+ const GeckoSession::Compositor::LocalRef& aInstance)
+ : mWindow(aWindow), mCompositor(aInstance), mCompositorPaused(true) {
+#if defined(DEBUG)
+ auto win(mWindow.Access());
+ MOZ_ASSERT(!!win);
+#endif // defined(DEBUG)
+ }
+
+ ~LayerViewSupport() {}
+
+ using Base::AttachNative;
+ using Base::DisposeNative;
+
+ void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
+ RefPtr<Runnable> disposer = aDisposer;
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ GeckoSession::Compositor::GlobalRef compositor(mCompositor);
+ if (!compositor) {
+ return;
+ }
+
+ uiThread->Dispatch(NS_NewRunnableFunction(
+ "LayerViewSupport::OnWeakNonIntrusiveDetach",
+ [compositor, disposer = std::move(disposer),
+ results = &mCapturePixelsResults, window = mWindow]() mutable {
+ if (auto accWindow = window.Access()) {
+ while (!results->empty()) {
+ auto aResult =
+ java::GeckoResult::LocalRef(results->front().mResult);
+ if (aResult) {
+ aResult->CompleteExceptionally(
+ java::sdk::IllegalStateException::New(
+ "The compositor has detached from the session")
+ .Cast<jni::Throwable>());
+ }
+ results->pop();
+ }
+ }
+
+ compositor->OnCompositorDetached();
+ disposer->Run();
+ }));
+ }
+ }
+
+ const GeckoSession::Compositor::Ref& GetJavaCompositor() const {
+ return mCompositor;
+ }
+
+ bool CompositorPaused() const { return mCompositorPaused; }
+
+ /// Called from the main thread whenever the compositor has been
+ /// (re)initialized.
+ void NotifyCompositorCreated(
+ RefPtr<UiCompositorControllerChild> aUiCompositorControllerChild) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ mUiCompositorControllerChild = aUiCompositorControllerChild;
+
+ if (mDefaultClearColor) {
+ mUiCompositorControllerChild->SetDefaultClearColor(*mDefaultClearColor);
+ }
+
+ if (!mCompositorPaused) {
+ // If we are using SurfaceControl but mSurface is null, that means the
+ // previous surface was destroyed along with the the previous
+ // compositor, and we need to create a new one.
+ if (mSurfaceControl && !mSurface) {
+ mSurface = java::SurfaceControlManager::GetInstance()->GetChildSurface(
+ mSurfaceControl, mWidth, mHeight);
+ }
+
+ if (auto window{mWindow.Access()}) {
+ nsWindow* gkWindow = window->GetNsWindow();
+ if (gkWindow) {
+ mUiCompositorControllerChild->OnCompositorSurfaceChanged(
+ gkWindow->mWidgetId, mSurface);
+ }
+ }
+
+ bool resumed = mUiCompositorControllerChild->ResumeAndResize(
+ mX, mY, mWidth, mHeight);
+ if (!resumed) {
+ gfxCriticalNote
+ << "Failed to resume compositor from NotifyCompositorCreated";
+ RequestNewSurface();
+ }
+ }
+ }
+
+ /// Called from the main thread whenever the compositor has been destroyed.
+ void NotifyCompositorSessionLost() {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ mUiCompositorControllerChild = nullptr;
+
+ if (mSurfaceControl) {
+ // If we are using SurfaceControl then we must set the Surface to null
+ // here to ensure we create a new one when the new compositor is
+ // created.
+ mSurface = nullptr;
+ }
+
+ if (auto window = mWindow.Access()) {
+ while (!mCapturePixelsResults.empty()) {
+ auto result =
+ java::GeckoResult::LocalRef(mCapturePixelsResults.front().mResult);
+ if (result) {
+ result->CompleteExceptionally(
+ java::sdk::IllegalStateException::New(
+ "Compositor session lost during screen pixels request")
+ .Cast<jni::Throwable>());
+ }
+ mCapturePixelsResults.pop();
+ }
+ }
+ }
+
+ java::sdk::Surface::Param GetSurface() { return mSurface; }
+
+ private:
+ already_AddRefed<DataSourceSurface> FlipScreenPixels(
+ Shmem& aMem, const ScreenIntSize& aInSize, const ScreenRect& aInRegion,
+ const IntSize& aOutSize) {
+ RefPtr<gfx::DataSourceSurface> image =
+ gfx::Factory::CreateWrappingDataSourceSurface(
+ aMem.get<uint8_t>(),
+ StrideForFormatAndWidth(SurfaceFormat::B8G8R8A8, aInSize.width),
+ IntSize(aInSize.width, aInSize.height), SurfaceFormat::B8G8R8A8);
+ RefPtr<gfx::DrawTarget> drawTarget =
+ gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
+ aOutSize, SurfaceFormat::B8G8R8A8);
+ if (!drawTarget) {
+ return nullptr;
+ }
+
+ drawTarget->SetTransform(Matrix::Scaling(1.0, -1.0) *
+ Matrix::Translation(0, aOutSize.height));
+
+ gfx::Rect srcRect(aInRegion.x,
+ (aInSize.height - aInRegion.height) - aInRegion.y,
+ aInRegion.width, aInRegion.height);
+ gfx::Rect destRect(0, 0, aOutSize.width, aOutSize.height);
+ drawTarget->DrawSurface(image, destRect, srcRect);
+
+ RefPtr<gfx::SourceSurface> snapshot = drawTarget->Snapshot();
+ RefPtr<gfx::DataSourceSurface> data = snapshot->GetDataSurface();
+ return data.forget();
+ }
+
+ /**
+ * Compositor methods
+ */
+ public:
+ void AttachNPZC(jni::Object::Param aNPZC) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aNPZC);
+
+ auto locked(mWindow.Access());
+ if (!locked) {
+ return; // Already shut down.
+ }
+
+ nsWindow* gkWindow = locked->GetNsWindow();
+
+ // We can have this situation if we get two GeckoViewSupport::Transfer()
+ // called before the first AttachNPZC() gets here. Just detach the current
+ // instance since that's what happens in GeckoViewSupport::Transfer() as
+ // well.
+ gkWindow->mNPZCSupport.Detach();
+
+ auto npzc = java::PanZoomController::NativeProvider::LocalRef(
+ jni::GetGeckoThreadEnv(),
+ java::PanZoomController::NativeProvider::Ref::From(aNPZC));
+ gkWindow->mNPZCSupport =
+ jni::NativeWeakPtrHolder<NPZCSupport>::Attach(npzc, mWindow, npzc);
+
+ DispatchToUiThread(
+ "LayerViewSupport::AttachNPZC",
+ [npzc = java::PanZoomController::NativeProvider::GlobalRef(npzc)] {
+ npzc->SetAttached(true);
+ });
+ }
+
+ void OnBoundsChanged(int32_t aLeft, int32_t aTop, int32_t aWidth,
+ int32_t aHeight) {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto acc = mWindow.Access();
+ if (!acc) {
+ return; // Already shut down.
+ }
+
+ nsWindow* gkWindow = acc->GetNsWindow();
+ if (!gkWindow) {
+ return;
+ }
+
+ gkWindow->Resize(aLeft, aTop, aWidth, aHeight, /* repaint */ false);
+ }
+
+ void NotifyMemoryPressure() {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto acc = mWindow.Access();
+ if (!acc) {
+ return; // Already shut down.
+ }
+
+ nsWindow* gkWindow = acc->GetNsWindow();
+ if (!gkWindow || !gkWindow->mCompositorBridgeChild) {
+ return;
+ }
+
+ gkWindow->mCompositorBridgeChild->SendNotifyMemoryPressure();
+ }
+
+ void SetDynamicToolbarMaxHeight(int32_t aHeight) {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto acc = mWindow.Access();
+ if (!acc) {
+ return; // Already shut down.
+ }
+
+ nsWindow* gkWindow = acc->GetNsWindow();
+ if (!gkWindow) {
+ return;
+ }
+
+ gkWindow->UpdateDynamicToolbarMaxHeight(ScreenIntCoord(aHeight));
+ }
+
+ void SyncPauseCompositor() {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ mCompositorPaused = true;
+
+ if (mUiCompositorControllerChild) {
+ mUiCompositorControllerChild->Pause();
+
+ mSurface = nullptr;
+ mSurfaceControl = nullptr;
+ if (auto window = mWindow.Access()) {
+ nsWindow* gkWindow = window->GetNsWindow();
+ if (gkWindow) {
+ mUiCompositorControllerChild->OnCompositorSurfaceChanged(
+ gkWindow->mWidgetId, nullptr);
+ }
+ }
+ }
+
+ if (auto lock{mWindow.Access()}) {
+ while (!mCapturePixelsResults.empty()) {
+ auto result =
+ java::GeckoResult::LocalRef(mCapturePixelsResults.front().mResult);
+ if (result) {
+ result->CompleteExceptionally(
+ java::sdk::IllegalStateException::New(
+ "The compositor has detached from the session")
+ .Cast<jni::Throwable>());
+ }
+ mCapturePixelsResults.pop();
+ }
+ }
+ }
+
+ void SyncResumeCompositor() {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ if (mUiCompositorControllerChild) {
+ mCompositorPaused = false;
+ bool resumed = mUiCompositorControllerChild->Resume();
+ if (!resumed) {
+ gfxCriticalNote
+ << "Failed to resume compositor from SyncResumeCompositor";
+ RequestNewSurface();
+ }
+ }
+ }
+
+ void SyncResumeResizeCompositor(
+ const GeckoSession::Compositor::LocalRef& aObj, int32_t aX, int32_t aY,
+ int32_t aWidth, int32_t aHeight, jni::Object::Param aSurface,
+ jni::Object::Param aSurfaceControl) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ mX = aX;
+ mY = aY;
+ mWidth = aWidth;
+ mHeight = aHeight;
+ if (StaticPrefs::widget_android_use_surfacecontrol_AtStartup()) {
+ mSurfaceControl =
+ java::sdk::SurfaceControl::GlobalRef::From(aSurfaceControl);
+ }
+ if (mSurfaceControl) {
+ // When using SurfaceControl, we create a child Surface to render in to
+ // rather than rendering directly in to the Surface provided by the
+ // application. This allows us to work around a bug on some versions of
+ // Android when recovering from a GPU process crash.
+ mSurface = java::SurfaceControlManager::GetInstance()->GetChildSurface(
+ mSurfaceControl, mWidth, mHeight);
+ } else {
+ mSurface = java::sdk::Surface::GlobalRef::From(aSurface);
+ }
+
+ if (mUiCompositorControllerChild) {
+ if (auto window = mWindow.Access()) {
+ nsWindow* gkWindow = window->GetNsWindow();
+ if (gkWindow) {
+ // Send new Surface to GPU process, if one exists.
+ mUiCompositorControllerChild->OnCompositorSurfaceChanged(
+ gkWindow->mWidgetId, mSurface);
+ }
+ }
+
+ bool resumed = mUiCompositorControllerChild->ResumeAndResize(
+ aX, aY, aWidth, aHeight);
+ if (!resumed) {
+ gfxCriticalNote
+ << "Failed to resume compositor from SyncResumeResizeCompositor";
+ // Only request a new Surface if this SyncResumeAndResize call is not
+ // response to a previous request, otherwise we will get stuck in an
+ // infinite loop.
+ if (!mRequestedNewSurface) {
+ RequestNewSurface();
+ }
+ return;
+ }
+ }
+
+ mRequestedNewSurface = false;
+
+ mCompositorPaused = false;
+
+ class OnResumedEvent : public nsAppShell::Event {
+ GeckoSession::Compositor::GlobalRef mCompositor;
+
+ public:
+ explicit OnResumedEvent(GeckoSession::Compositor::GlobalRef&& aCompositor)
+ : mCompositor(std::move(aCompositor)) {}
+
+ void Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ JNIEnv* const env = jni::GetGeckoThreadEnv();
+ const auto lvsHolder =
+ GetNative(GeckoSession::Compositor::LocalRef(env, mCompositor));
+
+ if (!lvsHolder) {
+ env->ExceptionClear();
+ return; // Already shut down.
+ }
+
+ auto lvs(lvsHolder->Access());
+ if (!lvs) {
+ env->ExceptionClear();
+ return; // Already shut down.
+ }
+
+ auto win = lvs->mWindow.Access();
+ if (!win) {
+ env->ExceptionClear();
+ return; // Already shut down.
+ }
+
+ // When we get here, the compositor has already been told to
+ // resume. This means it's now safe for layer updates to occur.
+ // Since we might have prevented one or more draw events from
+ // occurring while the compositor was paused, we need to
+ // schedule a draw event now.
+ if (!lvs->mCompositorPaused) {
+ nsWindow* const gkWindow = win->GetNsWindow();
+ if (gkWindow) {
+ gkWindow->RedrawAll();
+ }
+ }
+ }
+ };
+
+ // Use priority queue for timing-sensitive event.
+ nsAppShell::PostEvent(
+ MakeUnique<LayerViewEvent>(MakeUnique<OnResumedEvent>(aObj)));
+ }
+
+ void RequestNewSurface() {
+ if (const auto& compositor = GetJavaCompositor()) {
+ mRequestedNewSurface = true;
+ if (mSurfaceControl) {
+ java::SurfaceControlManager::GetInstance()->RemoveSurface(
+ mSurfaceControl);
+ }
+ compositor->RequestNewSurface();
+ }
+ }
+
+ mozilla::jni::Object::LocalRef GetMagnifiableSurface() {
+ return mozilla::jni::Object::LocalRef::From(GetSurface());
+ }
+
+ void SyncInvalidateAndScheduleComposite() {
+ if (!mUiCompositorControllerChild) {
+ return;
+ }
+
+ if (AndroidBridge::IsJavaUiThread()) {
+ mUiCompositorControllerChild->InvalidateAndRender();
+ return;
+ }
+
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ uiThread->Dispatch(NewRunnableMethod<>(
+ "LayerViewSupport::InvalidateAndRender",
+ mUiCompositorControllerChild,
+ &UiCompositorControllerChild::InvalidateAndRender),
+ nsIThread::DISPATCH_NORMAL);
+ }
+ }
+
+ void SetMaxToolbarHeight(int32_t aHeight) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+
+ if (mUiCompositorControllerChild) {
+ mUiCompositorControllerChild->SetMaxToolbarHeight(aHeight);
+ }
+ }
+
+ void SetFixedBottomOffset(int32_t aOffset) {
+ if (auto acc{mWindow.Access()}) {
+ nsWindow* gkWindow = acc->GetNsWindow();
+ if (gkWindow) {
+ gkWindow->UpdateDynamicToolbarOffset(ScreenIntCoord(aOffset));
+ }
+ }
+
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ uiThread->Dispatch(NS_NewRunnableFunction(
+ "LayerViewSupport::SetFixedBottomOffset", [this, offset = aOffset] {
+ if (mUiCompositorControllerChild) {
+ mUiCompositorControllerChild->SetFixedBottomOffset(offset);
+ }
+ }));
+ }
+ }
+
+ void SendToolbarAnimatorMessage(int32_t aMessage) {
+ if (!mUiCompositorControllerChild) {
+ return;
+ }
+
+ if (AndroidBridge::IsJavaUiThread()) {
+ mUiCompositorControllerChild->ToolbarAnimatorMessageFromUI(aMessage);
+ return;
+ }
+
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ uiThread->Dispatch(
+ NewRunnableMethod<int32_t>(
+ "LayerViewSupport::ToolbarAnimatorMessageFromUI",
+ mUiCompositorControllerChild,
+ &UiCompositorControllerChild::ToolbarAnimatorMessageFromUI,
+ aMessage),
+ nsIThread::DISPATCH_NORMAL);
+ }
+ }
+
+ void RecvToolbarAnimatorMessage(int32_t aMessage) {
+ auto compositor = GeckoSession::Compositor::LocalRef(mCompositor);
+ if (compositor) {
+ compositor->RecvToolbarAnimatorMessage(aMessage);
+ }
+ }
+
+ void SetDefaultClearColor(int32_t aColor) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ mDefaultClearColor = Some((uint32_t)aColor);
+ if (mUiCompositorControllerChild) {
+ mUiCompositorControllerChild->SetDefaultClearColor((uint32_t)aColor);
+ }
+ }
+
+ void RequestScreenPixels(jni::Object::Param aResult,
+ jni::Object::Param aTarget, int32_t aXOffset,
+ int32_t aYOffset, int32_t aSrcWidth,
+ int32_t aSrcHeight, int32_t aOutWidth,
+ int32_t aOutHeight) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ auto result = java::GeckoResult::LocalRef(aResult);
+
+ if (!mUiCompositorControllerChild) {
+ if (result) {
+ if (auto window = mWindow.Access()) {
+ result->CompleteExceptionally(
+ java::sdk::IllegalStateException::New(
+ "Compositor session lost prior to screen pixels request")
+ .Cast<jni::Throwable>());
+ }
+ }
+ return;
+ }
+
+ int size = 0;
+ if (auto window = mWindow.Access()) {
+ mCapturePixelsResults.push(CaptureRequest(
+ java::GeckoResult::GlobalRef(result),
+ java::sdk::Bitmap::GlobalRef(java::sdk::Bitmap::LocalRef(aTarget)),
+ ScreenRect(aXOffset, aYOffset, aSrcWidth, aSrcHeight),
+ IntSize(aOutWidth, aOutHeight)));
+ size = mCapturePixelsResults.size();
+ }
+
+ if (size == 1) {
+ mUiCompositorControllerChild->RequestScreenPixels();
+ }
+ }
+
+ void RecvScreenPixels(Shmem&& aMem, const ScreenIntSize& aSize,
+ bool aNeedsYFlip) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ CaptureRequest request;
+ java::GeckoResult::LocalRef result = nullptr;
+ java::sdk::Bitmap::LocalRef bitmap = nullptr;
+ if (auto window = mWindow.Access()) {
+ // The result might have been already rejected if the compositor was
+ // detached from the session
+ if (!mCapturePixelsResults.empty()) {
+ request = mCapturePixelsResults.front();
+ result = java::GeckoResult::LocalRef(request.mResult);
+ bitmap = java::sdk::Bitmap::LocalRef(request.mBitmap);
+ mCapturePixelsResults.pop();
+ }
+ }
+
+ if (result) {
+ if (bitmap) {
+ RefPtr<DataSourceSurface> surf;
+ if (aNeedsYFlip) {
+ surf = FlipScreenPixels(aMem, aSize, request.mSource,
+ request.mOutputSize);
+ } else {
+ surf = gfx::Factory::CreateWrappingDataSourceSurface(
+ aMem.get<uint8_t>(),
+ StrideForFormatAndWidth(SurfaceFormat::B8G8R8A8, aSize.width),
+ IntSize(aSize.width, aSize.height), SurfaceFormat::B8G8R8A8);
+ }
+ if (surf) {
+ DataSourceSurface::ScopedMap smap(surf, DataSourceSurface::READ);
+ auto pixels = mozilla::jni::ByteBuffer::New(
+ reinterpret_cast<int8_t*>(smap.GetData()),
+ smap.GetStride() * request.mOutputSize.height);
+ bitmap->CopyPixelsFromBuffer(pixels);
+ result->Complete(bitmap);
+ } else {
+ result->CompleteExceptionally(
+ java::sdk::IllegalStateException::New(
+ "Failed to create flipped snapshot surface (probably out "
+ "of memory)")
+ .Cast<jni::Throwable>());
+ }
+ } else {
+ result->CompleteExceptionally(java::sdk::IllegalArgumentException::New(
+ "No target bitmap argument provided")
+ .Cast<jni::Throwable>());
+ }
+ }
+
+ // Pixels have been copied, so Dealloc Shmem
+ if (mUiCompositorControllerChild) {
+ mUiCompositorControllerChild->DeallocPixelBuffer(aMem);
+
+ if (auto window = mWindow.Access()) {
+ if (!mCapturePixelsResults.empty()) {
+ mUiCompositorControllerChild->RequestScreenPixels();
+ }
+ }
+ }
+ }
+
+ void EnableLayerUpdateNotifications(bool aEnable) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ if (mUiCompositorControllerChild) {
+ mUiCompositorControllerChild->EnableLayerUpdateNotifications(aEnable);
+ }
+ }
+
+ void OnSafeAreaInsetsChanged(int32_t aTop, int32_t aRight, int32_t aBottom,
+ int32_t aLeft) {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto win(mWindow.Access());
+ if (!win) {
+ return; // Already shut down.
+ }
+
+ nsWindow* gkWindow = win->GetNsWindow();
+ if (!gkWindow) {
+ return;
+ }
+
+ ScreenIntMargin safeAreaInsets(aTop, aRight, aBottom, aLeft);
+ gkWindow->UpdateSafeAreaInsets(safeAreaInsets);
+ }
+};
+
+GeckoViewSupport::~GeckoViewSupport() {
+ if (mWindow) {
+ mWindow->DetachNatives();
+ }
+}
+
+/* static */
+void GeckoViewSupport::Open(
+ const jni::Class::LocalRef& aCls, GeckoSession::Window::Param aWindow,
+ jni::Object::Param aQueue, jni::Object::Param aCompositor,
+ jni::Object::Param aDispatcher, jni::Object::Param aSessionAccessibility,
+ jni::Object::Param aInitData, jni::String::Param aId,
+ jni::String::Param aChromeURI, bool aPrivateMode) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ AUTO_PROFILER_LABEL("mozilla::widget::GeckoViewSupport::Open", OTHER);
+
+ // We'll need gfxPlatform to be initialized to create a compositor later.
+ // Might as well do that now so that the GPU process launch can get a head
+ // start.
+ gfxPlatform::GetPlatform();
+
+ nsCOMPtr<nsIWindowWatcher> ww = do_GetService(NS_WINDOWWATCHER_CONTRACTID);
+ MOZ_RELEASE_ASSERT(ww);
+
+ nsAutoCString url;
+ if (aChromeURI) {
+ url = aChromeURI->ToCString();
+ } else {
+ nsresult rv = Preferences::GetCString("toolkit.defaultChromeURI", url);
+ if (NS_FAILED(rv)) {
+ url = "chrome://geckoview/content/geckoview.xhtml"_ns;
+ }
+ }
+
+ // Prepare an nsIAndroidView to pass as argument to the window.
+ RefPtr<AndroidView> androidView = new AndroidView();
+ androidView->mEventDispatcher->Attach(
+ java::EventDispatcher::Ref::From(aDispatcher), nullptr);
+ androidView->mInitData = java::GeckoBundle::Ref::From(aInitData);
+
+ nsAutoCString chromeFlags("chrome,dialog=0,remote,resizable,scrollbars");
+ if (aPrivateMode) {
+ chromeFlags += ",private";
+ }
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ ww->OpenWindow(nullptr, url, nsDependentCString(aId->ToCString().get()),
+ chromeFlags, androidView, getter_AddRefs(domWindow));
+ MOZ_RELEASE_ASSERT(domWindow);
+
+ nsCOMPtr<nsPIDOMWindowOuter> pdomWindow = nsPIDOMWindowOuter::From(domWindow);
+ const RefPtr<nsWindow> window = nsWindow::From(pdomWindow);
+ MOZ_ASSERT(window);
+
+ // Attach a new GeckoView support object to the new window.
+ GeckoSession::Window::LocalRef sessionWindow(aCls.Env(), aWindow);
+ auto weakGeckoViewSupport =
+ jni::NativeWeakPtrHolder<GeckoViewSupport>::Attach(
+ sessionWindow, window, sessionWindow, pdomWindow);
+
+ window->mGeckoViewSupport = weakGeckoViewSupport;
+ window->mAndroidView = androidView;
+
+ // Attach other session support objects.
+ { // Scope for gvsAccess
+ auto gvsAccess = weakGeckoViewSupport.Access();
+ MOZ_ASSERT(gvsAccess);
+
+ gvsAccess->Transfer(sessionWindow, aQueue, aCompositor, aDispatcher,
+ aSessionAccessibility, aInitData);
+ }
+
+ if (window->mWidgetListener) {
+ nsCOMPtr<nsIAppWindow> appWindow(window->mWidgetListener->GetAppWindow());
+ if (appWindow) {
+ // Our window is not intrinsically sized, so tell AppWindow to
+ // not set a size for us.
+ appWindow->SetIntrinsicallySized(false);
+ }
+ }
+}
+
+void GeckoViewSupport::Close() {
+ if (mWindow) {
+ if (mWindow->mAndroidView) {
+ mWindow->mAndroidView->mEventDispatcher->Detach();
+ }
+ mWindow = nullptr;
+ }
+
+ if (!mDOMWindow) {
+ return;
+ }
+
+ mDOMWindow->ForceClose();
+ mDOMWindow = nullptr;
+ mGeckoViewWindow = nullptr;
+}
+
+void GeckoViewSupport::Transfer(const GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aQueue,
+ jni::Object::Param aCompositor,
+ jni::Object::Param aDispatcher,
+ jni::Object::Param aSessionAccessibility,
+ jni::Object::Param aInitData) {
+ mWindow->mNPZCSupport.Detach();
+
+ auto compositor = GeckoSession::Compositor::LocalRef(
+ inst.Env(), GeckoSession::Compositor::Ref::From(aCompositor));
+
+ bool attachLvs;
+ { // Scope for lvsAccess
+ auto lvsAccess{mWindow->mLayerViewSupport.Access()};
+ // If we do not yet have mLayerViewSupport, or if the compositor has
+ // changed, then we must attach a new one.
+ attachLvs = !lvsAccess || lvsAccess->GetJavaCompositor() != compositor;
+ }
+
+ if (attachLvs) {
+ mWindow->mLayerViewSupport =
+ jni::NativeWeakPtrHolder<LayerViewSupport>::Attach(
+ compositor, mWindow->mGeckoViewSupport, compositor);
+
+ if (RefPtr<UiCompositorControllerChild> uiCompositorController =
+ mWindow->GetUiCompositorControllerChild()) {
+ DispatchToUiThread(
+ "LayerViewSupport::NotifyCompositorCreated",
+ [lvs = mWindow->mLayerViewSupport, uiCompositorController] {
+ if (auto lvsAccess{lvs.Access()}) {
+ lvsAccess->NotifyCompositorCreated(uiCompositorController);
+ }
+ });
+ }
+ }
+
+ MOZ_ASSERT(mWindow->mAndroidView);
+ mWindow->mAndroidView->mEventDispatcher->Attach(
+ java::EventDispatcher::Ref::From(aDispatcher), mDOMWindow);
+
+ RefPtr<jni::DetachPromise> promise = mWindow->mSessionAccessibility.Detach();
+ if (aSessionAccessibility) {
+ // SessionAccessibility's JNI object isn't released immediately, it uses
+ // recycled object, we have to wait for released object completely.
+ auto sa = java::SessionAccessibility::NativeProvider::LocalRef(
+ aSessionAccessibility);
+ promise->Then(
+ GetMainThreadSerialEventTarget(),
+ "GeckoViewSupprt::Transfer::SessionAccessibility",
+ [inst = GeckoSession::Window::GlobalRef(inst),
+ sa = java::SessionAccessibility::NativeProvider::GlobalRef(sa),
+ window = mWindow, gvs = mWindow->mGeckoViewSupport](
+ const mozilla::jni::DetachPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(aValue.IsResolve());
+ if (window->Destroyed()) {
+ return;
+ }
+
+ MOZ_ASSERT(!window->mSessionAccessibility.IsAttached());
+ if (auto gvsAccess{gvs.Access()}) {
+ gvsAccess->AttachAccessibility(inst, sa);
+ }
+ });
+ }
+
+ if (mIsReady) {
+ // We're in a transfer; update init-data and notify JS code.
+ mWindow->mAndroidView->mInitData = java::GeckoBundle::Ref::From(aInitData);
+ OnReady(aQueue);
+ mWindow->mAndroidView->mEventDispatcher->Dispatch(
+ u"GeckoView:UpdateInitData");
+ }
+
+ DispatchToUiThread("GeckoViewSupport::Transfer",
+ [compositor = GeckoSession::Compositor::GlobalRef(
+ compositor)] { compositor->OnCompositorAttached(); });
+}
+
+void GeckoViewSupport::AttachEditable(
+ const GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aEditableParent) {
+ if (auto win{mWindow->mEditableSupport.Access()}) {
+ win->TransferParent(aEditableParent);
+ } else {
+ auto editableChild = java::GeckoEditableChild::New(aEditableParent,
+ /* default */ true);
+ mWindow->mEditableSupport =
+ jni::NativeWeakPtrHolder<GeckoEditableSupport>::Attach(
+ editableChild, mWindow->mGeckoViewSupport, editableChild);
+ }
+
+ mWindow->mEditableParent = aEditableParent;
+}
+
+void GeckoViewSupport::AttachAccessibility(
+ const GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aSessionAccessibility) {
+ java::SessionAccessibility::NativeProvider::LocalRef sessionAccessibility(
+ inst.Env());
+ sessionAccessibility = java::SessionAccessibility::NativeProvider::Ref::From(
+ aSessionAccessibility);
+
+ mWindow->mSessionAccessibility =
+ jni::NativeWeakPtrHolder<a11y::SessionAccessibility>::Attach(
+ sessionAccessibility, mWindow->mGeckoViewSupport,
+ sessionAccessibility);
+}
+
+auto GeckoViewSupport::OnLoadRequest(mozilla::jni::String::Param aUri,
+ int32_t aWindowType, int32_t aFlags,
+ mozilla::jni::String::Param aTriggeringUri,
+ bool aHasUserGesture,
+ bool aIsTopLevel) const
+ -> java::GeckoResult::LocalRef {
+ GeckoSession::Window::LocalRef window(mGeckoViewWindow);
+ if (!window) {
+ return nullptr;
+ }
+ return window->OnLoadRequest(aUri, aWindowType, aFlags, aTriggeringUri,
+ aHasUserGesture, aIsTopLevel);
+}
+
+void GeckoViewSupport::OnShowDynamicToolbar() const {
+ GeckoSession::Window::LocalRef window(mGeckoViewWindow);
+ if (!window) {
+ return;
+ }
+
+ window->OnShowDynamicToolbar();
+}
+
+void GeckoViewSupport::OnReady(jni::Object::Param aQueue) {
+ GeckoSession::Window::LocalRef window(mGeckoViewWindow);
+ if (!window) {
+ return;
+ }
+ window->OnReady(aQueue);
+ mIsReady = true;
+}
+
+void GeckoViewSupport::PassExternalResponse(
+ java::WebResponse::Param aResponse) {
+ GeckoSession::Window::LocalRef window(mGeckoViewWindow);
+ if (!window) {
+ return;
+ }
+
+ auto response = java::WebResponse::GlobalRef(aResponse);
+
+ DispatchToUiThread("GeckoViewSupport::PassExternalResponse",
+ [window = java::GeckoSession::Window::GlobalRef(window),
+ response] { window->PassExternalWebResponse(response); });
+}
+
+RefPtr<CanonicalBrowsingContext>
+GeckoViewSupport::GetContentCanonicalBrowsingContext() {
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner = mDOMWindow->GetTreeOwner();
+ if (!treeOwner) {
+ return nullptr;
+ }
+ RefPtr<BrowsingContext> bc;
+ nsresult rv = treeOwner->GetPrimaryContentBrowsingContext(getter_AddRefs(bc));
+ if (NS_WARN_IF(NS_FAILED(rv)) || !bc) {
+ return nullptr;
+ }
+ return bc->Canonical();
+}
+
+void GeckoViewSupport::CreatePdf(
+ jni::LocalRef<mozilla::java::GeckoResult> aGeckoResult,
+ RefPtr<dom::CanonicalBrowsingContext> aCbc) {
+ MOZ_ASSERT(NS_IsMainThread());
+ const auto pdfErrorMsg = "Could not save this page as PDF.";
+ auto stream = java::GeckoInputStream::New(nullptr);
+ RefPtr<GeckoViewOutputStream> streamListener =
+ new GeckoViewOutputStream(stream);
+
+ nsCOMPtr<nsIPrintSettingsService> printSettingsService =
+ do_GetService("@mozilla.org/gfx/printsettings-service;1");
+ if (!printSettingsService) {
+ aGeckoResult->CompleteExceptionally(
+ GeckoPrintException::New(
+ GeckoPrintException::ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE)
+ .Cast<jni::Throwable>());
+ GVS_LOG("Could not create print settings service.");
+ return;
+ }
+
+ nsCOMPtr<nsIPrintSettings> printSettings;
+ nsresult rv = printSettingsService->CreateNewPrintSettings(
+ getter_AddRefs(printSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aGeckoResult->CompleteExceptionally(
+ GeckoPrintException::New(
+ GeckoPrintException::ERROR_UNABLE_TO_CREATE_PRINT_SETTINGS)
+ .Cast<jni::Throwable>());
+ GVS_LOG("Could not create print settings.");
+ return;
+ }
+
+ printSettings->SetPrinterName(u"Mozilla Save to PDF"_ns);
+ printSettings->SetOutputDestination(
+ nsIPrintSettings::kOutputDestinationStream);
+ printSettings->SetOutputFormat(nsIPrintSettings::kOutputFormatPDF);
+ printSettings->SetOutputStream(streamListener);
+ printSettings->SetPrintSilent(true);
+
+ RefPtr<CanonicalBrowsingContext::PrintPromise> print =
+ aCbc->Print(printSettings);
+
+ aGeckoResult->Complete(stream);
+ print->Then(
+ mozilla::GetCurrentSerialEventTarget(), __func__,
+ [result = java::GeckoResult::GlobalRef(aGeckoResult), stream,
+ pdfErrorMsg](
+ const CanonicalBrowsingContext::PrintPromise::ResolveOrRejectValue&
+ aValue) {
+ if (aValue.IsReject()) {
+ GVS_LOG("Could not print. %s", pdfErrorMsg);
+ stream->WriteError();
+ }
+ });
+}
+
+void GeckoViewSupport::PrintToPdf(
+ const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aResult) {
+ auto geckoResult = java::GeckoResult::Ref::From(aResult);
+ RefPtr<CanonicalBrowsingContext> cbc = GetContentCanonicalBrowsingContext();
+ if (!cbc) {
+ geckoResult->CompleteExceptionally(
+ GeckoPrintException::New(
+ GeckoPrintException::
+ ERROR_UNABLE_TO_RETRIEVE_CANONICAL_BROWSING_CONTEXT)
+ .Cast<jni::Throwable>());
+ GVS_LOG("Could not retrieve content canonical browsing context.");
+ return;
+ }
+ CreatePdf(geckoResult, cbc);
+}
+
+void GeckoViewSupport::PrintToPdf(
+ const java::GeckoSession::Window::LocalRef& inst,
+ jni::Object::Param aResult, int64_t aBcId) {
+ auto geckoResult = java::GeckoResult::Ref::From(aResult);
+
+ RefPtr<CanonicalBrowsingContext> cbc = CanonicalBrowsingContext::Get(aBcId);
+ if (!cbc) {
+ geckoResult->CompleteExceptionally(
+ GeckoPrintException::New(
+ GeckoPrintException::
+ ERROR_UNABLE_TO_RETRIEVE_CANONICAL_BROWSING_CONTEXT)
+ .Cast<jni::Throwable>());
+ GVS_LOG("Could not retrieve content canonical browsing context by ID.");
+ return;
+ }
+ CreatePdf(geckoResult, cbc);
+}
+} // namespace widget
+} // namespace mozilla
+
+void nsWindow::InitNatives() {
+ jni::InitConversionStatics();
+ mozilla::widget::GeckoViewSupport::Base::Init();
+ mozilla::widget::LayerViewSupport::Init();
+ mozilla::widget::NPZCSupport::Init();
+
+ mozilla::widget::GeckoEditableSupport::Init();
+ a11y::SessionAccessibility::Init();
+}
+
+void nsWindow::DetachNatives() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mEditableSupport.Detach();
+ mNPZCSupport.Detach();
+ mLayerViewSupport.Detach();
+ mSessionAccessibility.Detach();
+}
+
+/* static */
+already_AddRefed<nsWindow> nsWindow::From(nsPIDOMWindowOuter* aDOMWindow) {
+ nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aDOMWindow);
+ return From(widget);
+}
+
+/* static */
+already_AddRefed<nsWindow> nsWindow::From(nsIWidget* aWidget) {
+ // `widget` may be one of several different types in the parent
+ // process, including the Android nsWindow, PuppetWidget, etc. To
+ // ensure that the cast to the Android nsWindow is valid, we check that the
+ // widget is a top-level window and that its NS_NATIVE_WIDGET value is
+ // non-null, which is not the case for non-native widgets like
+ // PuppetWidget.
+ if (aWidget && aWidget->GetWindowType() == WindowType::TopLevel &&
+ aWidget->GetNativeData(NS_NATIVE_WIDGET) == aWidget) {
+ RefPtr<nsWindow> window = static_cast<nsWindow*>(aWidget);
+ return window.forget();
+ }
+ return nullptr;
+}
+
+nsWindow* nsWindow::TopWindow() {
+ if (!gTopLevelWindows.IsEmpty()) return gTopLevelWindows[0];
+ return nullptr;
+}
+
+void nsWindow::LogWindow(nsWindow* win, int index, int indent) {
+#if defined(DEBUG) || defined(FORCE_ALOG)
+ char spaces[] = " ";
+ spaces[indent < 20 ? indent : 20] = 0;
+ ALOG("%s [% 2d] 0x%p [parent 0x%p] [% 3d,% 3dx% 3d,% 3d] vis %d type %d",
+ spaces, index, win, win->mParent, win->mBounds.x, win->mBounds.y,
+ win->mBounds.width, win->mBounds.height, win->mIsVisible,
+ int(win->mWindowType));
+#endif
+}
+
+void nsWindow::DumpWindows() { DumpWindows(gTopLevelWindows); }
+
+void nsWindow::DumpWindows(const nsTArray<nsWindow*>& wins, int indent) {
+ for (uint32_t i = 0; i < wins.Length(); ++i) {
+ nsWindow* w = wins[i];
+ LogWindow(w, i, indent);
+ DumpWindows(w->mChildren, indent + 1);
+ }
+}
+
+nsWindow::nsWindow()
+ : mWidgetId(++sWidgetId),
+ mIsVisible(false),
+ mParent(nullptr),
+ mDynamicToolbarMaxHeight(0),
+ mSizeMode(nsSizeMode_Normal),
+ mIsFullScreen(false),
+ mCompositorWidgetDelegate(nullptr),
+ mDestroyMutex("nsWindow::mDestroyMutex") {}
+
+nsWindow::~nsWindow() {
+ gTopLevelWindows.RemoveElement(this);
+ ALOG("nsWindow %p destructor", (void*)this);
+ // The mCompositorSession should have been cleaned up in nsWindow::Destroy()
+ // DestroyLayerManager() will call DestroyCompositor() which will crash if
+ // called from nsBaseWidget destructor. See Bug 1392705
+ MOZ_ASSERT(!mCompositorSession);
+}
+
+bool nsWindow::IsTopLevel() {
+ return mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog ||
+ mWindowType == WindowType::Invisible;
+}
+
+nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* aInitData) {
+ ALOG("nsWindow[%p]::Create %p [%d %d %d %d]", (void*)this, (void*)aParent,
+ aRect.x, aRect.y, aRect.width, aRect.height);
+
+ nsWindow* parent = (nsWindow*)aParent;
+ if (aNativeParent) {
+ if (parent) {
+ ALOG(
+ "Ignoring native parent on Android window [%p], "
+ "since parent was specified (%p %p)",
+ (void*)this, (void*)aNativeParent, (void*)aParent);
+ } else {
+ parent = (nsWindow*)aNativeParent;
+ }
+ }
+
+ // A default size of 1x1 confuses MobileViewportManager, so
+ // use 0x0 instead. This is also a little more fitting since
+ // we don't yet have a surface yet (and therefore a valid size)
+ // and 0x0 is usually recognized as invalid.
+ LayoutDeviceIntRect rect = aRect;
+ if (aRect.width == 1 && aRect.height == 1) {
+ rect.width = 0;
+ rect.height = 0;
+ }
+
+ mBounds = rect;
+ SetSizeConstraints(SizeConstraints());
+
+ BaseCreate(nullptr, aInitData);
+
+ NS_ASSERTION(IsTopLevel() || parent,
+ "non-top-level window doesn't have a parent!");
+
+ if (IsTopLevel()) {
+ gTopLevelWindows.AppendElement(this);
+
+ } else if (parent) {
+ parent->mChildren.AppendElement(this);
+ mParent = parent;
+ }
+
+#ifdef DEBUG_ANDROID_WIDGET
+ DumpWindows();
+#endif
+
+ return NS_OK;
+}
+
+void nsWindow::Destroy() {
+ MutexAutoLock lock(mDestroyMutex);
+
+ nsBaseWidget::mOnDestroyCalled = true;
+
+ // Disassociate our native object from GeckoView.
+ mGeckoViewSupport.Detach();
+
+ // Stuff below may release the last ref to this
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ while (mChildren.Length()) {
+ // why do we still have children?
+ ALOG("### Warning: Destroying window %p and reparenting child %p to null!",
+ (void*)this, (void*)mChildren[0]);
+ mChildren[0]->SetParent(nullptr);
+ }
+
+ // Ensure the compositor has been shutdown before this nsWindow is potentially
+ // deleted
+ nsBaseWidget::DestroyCompositor();
+
+ nsBaseWidget::Destroy();
+
+ if (IsTopLevel()) gTopLevelWindows.RemoveElement(this);
+
+ SetParent(nullptr);
+
+ nsBaseWidget::OnDestroy();
+
+#ifdef DEBUG_ANDROID_WIDGET
+ DumpWindows();
+#endif
+}
+
+mozilla::widget::EventDispatcher* nsWindow::GetEventDispatcher() const {
+ if (mAndroidView) {
+ return mAndroidView->mEventDispatcher;
+ }
+ return nullptr;
+}
+
+void nsWindow::RedrawAll() {
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->RequestRepaint();
+ } else if (mWidgetListener) {
+ mWidgetListener->RequestRepaint();
+ }
+}
+
+RefPtr<UiCompositorControllerChild> nsWindow::GetUiCompositorControllerChild() {
+ return mCompositorSession
+ ? mCompositorSession->GetUiCompositorControllerChild()
+ : nullptr;
+}
+
+mozilla::layers::LayersId nsWindow::GetRootLayerId() const {
+ return mCompositorSession ? mCompositorSession->RootLayerTreeId()
+ : mozilla::layers::LayersId{0};
+}
+
+void nsWindow::OnGeckoViewReady() {
+ auto acc(mGeckoViewSupport.Access());
+ if (!acc) {
+ return;
+ }
+
+ acc->OnReady();
+}
+
+void nsWindow::SetParent(nsIWidget* aNewParent) {
+ if ((nsIWidget*)mParent == aNewParent) return;
+
+ // If we had a parent before, remove ourselves from its list of
+ // children.
+ if (mParent) mParent->mChildren.RemoveElement(this);
+
+ mParent = (nsWindow*)aNewParent;
+
+ if (mParent) mParent->mChildren.AppendElement(this);
+
+ // if we are now in the toplevel window's hierarchy, schedule a redraw
+ if (FindTopLevel() == nsWindow::TopWindow()) RedrawAll();
+}
+
+nsIWidget* nsWindow::GetParent() { return mParent; }
+
+RefPtr<MozPromise<bool, bool, false>> nsWindow::OnLoadRequest(
+ nsIURI* aUri, int32_t aWindowType, int32_t aFlags,
+ nsIPrincipal* aTriggeringPrincipal, bool aHasUserGesture,
+ bool aIsTopLevel) {
+ auto geckoViewSupport(mGeckoViewSupport.Access());
+ if (!geckoViewSupport) {
+ return MozPromise<bool, bool, false>::CreateAndResolve(false, __func__);
+ }
+
+ nsAutoCString spec, triggeringSpec;
+ if (aUri) {
+ aUri->GetDisplaySpec(spec);
+ if (aIsTopLevel && mozilla::net::SchemeIsData(aUri) &&
+ spec.Length() > MAX_TOPLEVEL_DATA_URI_LEN) {
+ return MozPromise<bool, bool, false>::CreateAndResolve(false, __func__);
+ }
+ }
+
+ bool isNullPrincipal = false;
+ if (aTriggeringPrincipal) {
+ aTriggeringPrincipal->GetIsNullPrincipal(&isNullPrincipal);
+
+ if (!isNullPrincipal) {
+ nsCOMPtr<nsIURI> triggeringUri;
+ BasePrincipal::Cast(aTriggeringPrincipal)
+ ->GetURI(getter_AddRefs(triggeringUri));
+ if (triggeringUri) {
+ triggeringUri->GetDisplaySpec(triggeringSpec);
+ }
+ }
+ }
+
+ auto geckoResult = geckoViewSupport->OnLoadRequest(
+ spec.get(), aWindowType, aFlags,
+ isNullPrincipal ? nullptr : triggeringSpec.get(), aHasUserGesture,
+ aIsTopLevel);
+ return geckoResult
+ ? MozPromise<bool, bool, false>::FromGeckoResult(geckoResult)
+ : nullptr;
+}
+
+void nsWindow::OnUpdateSessionStore(mozilla::jni::Object::Param aBundle) {
+ auto geckoViewSupport(mGeckoViewSupport.Access());
+ if (!geckoViewSupport) {
+ return;
+ }
+
+ geckoViewSupport->OnUpdateSessionStore(aBundle);
+}
+
+float nsWindow::GetDPI() {
+ float dpi = 160.0f;
+
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ screen->GetDpi(&dpi);
+ }
+
+ return dpi;
+}
+
+double nsWindow::GetDefaultScaleInternal() {
+ double scale = 1.0f;
+
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ screen->GetContentsScaleFactor(&scale);
+ }
+
+ return scale;
+}
+
+void nsWindow::Show(bool aState) {
+ ALOG("nsWindow[%p]::Show %d", (void*)this, aState);
+
+ if (mWindowType == WindowType::Invisible) {
+ ALOG("trying to show invisible window! ignoring..");
+ return;
+ }
+
+ if (aState == mIsVisible) return;
+
+ mIsVisible = aState;
+
+ if (IsTopLevel()) {
+ // XXX should we bring this to the front when it's shown,
+ // if it's a toplevel widget?
+
+ // XXX we should synthesize a eMouseExitFromWidget (for old top
+ // window)/eMouseEnterIntoWidget (for new top window) since we need
+ // to pretend that the top window always has focus. Not sure
+ // if Show() is the right place to do this, though.
+
+ if (aState) {
+ // It just became visible, so bring it to the front.
+ BringToFront();
+
+ } else if (nsWindow::TopWindow() == this) {
+ // find the next visible window to show
+ unsigned int i;
+ for (i = 1; i < gTopLevelWindows.Length(); i++) {
+ nsWindow* win = gTopLevelWindows[i];
+ if (!win->mIsVisible) continue;
+
+ win->BringToFront();
+ break;
+ }
+ }
+ } else if (FindTopLevel() == nsWindow::TopWindow()) {
+ RedrawAll();
+ }
+
+#ifdef DEBUG_ANDROID_WIDGET
+ DumpWindows();
+#endif
+}
+
+bool nsWindow::IsVisible() const { return mIsVisible; }
+
+void nsWindow::ConstrainPosition(DesktopIntPoint& aPoint) {
+ ALOG("nsWindow[%p]::ConstrainPosition [%d %d]", (void*)this, aPoint.x.value,
+ aPoint.y.value);
+
+ // Constrain toplevel windows; children we don't care about
+ if (IsTopLevel()) {
+ aPoint = DesktopIntPoint();
+ }
+}
+
+void nsWindow::Move(double aX, double aY) {
+ if (IsTopLevel()) return;
+
+ Resize(aX, aY, mBounds.width, mBounds.height, true);
+}
+
+void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
+ Resize(mBounds.x, mBounds.y, aWidth, aHeight, aRepaint);
+}
+
+void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ ALOG("nsWindow[%p]::Resize [%f %f %f %f] (repaint %d)", (void*)this, aX, aY,
+ aWidth, aHeight, aRepaint);
+
+ LayoutDeviceIntRect oldBounds = mBounds;
+
+ mBounds.x = NSToIntRound(aX);
+ mBounds.y = NSToIntRound(aY);
+ mBounds.width = NSToIntRound(aWidth);
+ mBounds.height = NSToIntRound(aHeight);
+
+ ConstrainSize(&mBounds.width, &mBounds.height);
+
+ bool needPositionDispatch = mBounds.TopLeft() != oldBounds.TopLeft();
+ bool needSizeDispatch = mBounds.Size() != oldBounds.Size();
+
+ if (needSizeDispatch) {
+ OnSizeChanged(mBounds.Size().ToUnknownSize());
+ }
+
+ if (needPositionDispatch) {
+ NotifyWindowMoved(mBounds.x, mBounds.y);
+ }
+
+ // Should we skip honoring aRepaint here?
+ if (aRepaint && FindTopLevel() == nsWindow::TopWindow()) RedrawAll();
+}
+
+void nsWindow::SetZIndex(int32_t aZIndex) {
+ ALOG("nsWindow[%p]::SetZIndex %d ignored", (void*)this, aZIndex);
+}
+
+void nsWindow::SetSizeMode(nsSizeMode aMode) {
+ if (aMode == mSizeMode) {
+ return;
+ }
+
+ mSizeMode = aMode;
+
+ switch (aMode) {
+ case nsSizeMode_Minimized:
+ java::GeckoAppShell::MoveTaskToBack();
+ break;
+ case nsSizeMode_Fullscreen:
+ MakeFullScreen(true);
+ break;
+ default:
+ break;
+ }
+}
+
+void nsWindow::Enable(bool aState) {
+ ALOG("nsWindow[%p]::Enable %d ignored", (void*)this, aState);
+}
+
+bool nsWindow::IsEnabled() const { return true; }
+
+void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) {}
+
+nsWindow* nsWindow::FindTopLevel() {
+ nsWindow* toplevel = this;
+ while (toplevel) {
+ if (toplevel->IsTopLevel()) return toplevel;
+
+ toplevel = toplevel->mParent;
+ }
+
+ ALOG(
+ "nsWindow::FindTopLevel(): couldn't find a toplevel or dialog window in "
+ "this [%p] widget's hierarchy!",
+ (void*)this);
+ return this;
+}
+
+void nsWindow::SetFocus(Raise, mozilla::dom::CallerType aCallerType) {
+ FindTopLevel()->BringToFront();
+}
+
+void nsWindow::BringToFront() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // If the window to be raised is the same as the currently raised one,
+ // do nothing. We need to check the focus manager as well, as the first
+ // window that is created will be first in the window list but won't yet
+ // be focused.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm && fm->GetActiveWindow() && FindTopLevel() == nsWindow::TopWindow()) {
+ return;
+ }
+
+ if (!IsTopLevel()) {
+ FindTopLevel()->BringToFront();
+ return;
+ }
+
+ RefPtr<nsWindow> kungFuDeathGrip(this);
+
+ nsWindow* oldTop = nullptr;
+ if (!gTopLevelWindows.IsEmpty()) {
+ oldTop = gTopLevelWindows[0];
+ }
+
+ gTopLevelWindows.RemoveElement(this);
+ gTopLevelWindows.InsertElementAt(0, this);
+
+ if (oldTop) {
+ nsIWidgetListener* listener = oldTop->GetWidgetListener();
+ if (listener) {
+ listener->WindowDeactivated();
+ }
+ }
+
+ if (mWidgetListener) {
+ mWidgetListener->WindowActivated();
+ }
+
+ RedrawAll();
+}
+
+LayoutDeviceIntRect nsWindow::GetScreenBounds() {
+ return LayoutDeviceIntRect(WidgetToScreenOffset(), mBounds.Size());
+}
+
+LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() {
+ LayoutDeviceIntPoint p(0, 0);
+
+ for (nsWindow* w = this; !!w; w = w->mParent) {
+ p.x += w->mBounds.x;
+ p.y += w->mBounds.y;
+
+ if (w->IsTopLevel()) {
+ break;
+ }
+ }
+ return p;
+}
+
+nsresult nsWindow::DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) {
+ aStatus = DispatchEvent(aEvent);
+ return NS_OK;
+}
+
+nsEventStatus nsWindow::DispatchEvent(WidgetGUIEvent* aEvent) {
+ if (mAttachedWidgetListener) {
+ return mAttachedWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+ } else if (mWidgetListener) {
+ return mWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsresult nsWindow::MakeFullScreen(bool aFullScreen) {
+ if (!mAndroidView) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mIsFullScreen = aFullScreen;
+ mAndroidView->mEventDispatcher->Dispatch(
+ aFullScreen ? u"GeckoView:FullScreenEnter" : u"GeckoView:FullScreenExit");
+
+ nsIWidgetListener* listener = GetWidgetListener();
+ if (listener) {
+ mSizeMode = mIsFullScreen ? nsSizeMode_Fullscreen : nsSizeMode_Normal;
+ listener->SizeModeChanged(mSizeMode);
+ }
+ return NS_OK;
+}
+
+mozilla::WindowRenderer* nsWindow::GetWindowRenderer() {
+ if (!mWindowRenderer) {
+ CreateLayerManager();
+ }
+
+ return mWindowRenderer;
+}
+
+void nsWindow::CreateLayerManager() {
+ if (mWindowRenderer) {
+ return;
+ }
+
+ nsWindow* topLevelWindow = FindTopLevel();
+ if (!topLevelWindow || topLevelWindow->mWindowType == WindowType::Invisible) {
+ // don't create a layer manager for an invisible top-level window
+ return;
+ }
+
+ // Ensure that gfxPlatform is initialized first.
+ gfxPlatform::GetPlatform();
+
+ if (ShouldUseOffMainThreadCompositing()) {
+ LayoutDeviceIntRect rect = GetBounds();
+ CreateCompositor(rect.Width(), rect.Height());
+ if (mWindowRenderer) {
+ if (mLayerViewSupport.IsAttached()) {
+ DispatchToUiThread(
+ "LayerViewSupport::NotifyCompositorCreated",
+ [lvs = mLayerViewSupport,
+ uiCompositorController = GetUiCompositorControllerChild()] {
+ if (auto lvsAccess{lvs.Access()}) {
+ lvsAccess->NotifyCompositorCreated(uiCompositorController);
+ }
+ });
+ }
+
+ return;
+ }
+
+ // If we get here, then off main thread compositing failed to initialize.
+ sFailedToCreateGLContext = true;
+ }
+
+ if (!ComputeShouldAccelerate() || sFailedToCreateGLContext) {
+ printf_stderr(" -- creating basic, not accelerated\n");
+ mWindowRenderer = CreateFallbackRenderer();
+ }
+}
+
+void nsWindow::NotifyCompositorSessionLost(
+ mozilla::layers::CompositorSession* aSession) {
+ nsBaseWidget::NotifyCompositorSessionLost(aSession);
+
+ DispatchToUiThread("nsWindow::NotifyCompositorSessionLost",
+ [lvs = mLayerViewSupport] {
+ if (auto lvsAccess{lvs.Access()}) {
+ lvsAccess->NotifyCompositorSessionLost();
+ }
+ });
+
+ RedrawAll();
+}
+
+void nsWindow::ShowDynamicToolbar() {
+ auto acc(mGeckoViewSupport.Access());
+ if (!acc) {
+ return;
+ }
+
+ acc->OnShowDynamicToolbar();
+}
+
+void GeckoViewSupport::OnUpdateSessionStore(
+ mozilla::jni::Object::Param aBundle) {
+ GeckoSession::Window::LocalRef window(mGeckoViewWindow);
+ if (!window) {
+ return;
+ }
+
+ window->OnUpdateSessionStore(aBundle);
+}
+
+static EventMessage convertDragEventActionToGeckoEvent(int32_t aAction) {
+ switch (aAction) {
+ case java::sdk::DragEvent::ACTION_DRAG_ENTERED:
+ return eDragEnter;
+ case java::sdk::DragEvent::ACTION_DRAG_EXITED:
+ return eDragExit;
+ case java::sdk::DragEvent::ACTION_DRAG_LOCATION:
+ return eDragOver;
+ case java::sdk::DragEvent::ACTION_DROP:
+ return eDrop;
+ }
+ return eVoidEvent;
+}
+
+void nsWindow::OnDragEvent(int32_t aAction, int64_t aTime, float aX, float aY,
+ jni::Object::Param aDropData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<nsDragService> dragService = nsDragService::GetInstance();
+ if (!dragService) {
+ return;
+ }
+
+ LayoutDeviceIntPoint point =
+ LayoutDeviceIntPoint(int32_t(floorf(aX)), int32_t(floorf(aY)));
+
+ if (aAction == java::sdk::DragEvent::ACTION_DRAG_STARTED) {
+ dragService->SetDragEndPoint(point);
+ return;
+ }
+
+ if (aAction == java::sdk::DragEvent::ACTION_DRAG_ENDED) {
+ dragService->EndDragSession(false, 0);
+ return;
+ }
+
+ EventMessage message = convertDragEventActionToGeckoEvent(aAction);
+
+ if (message == eDragEnter) {
+ dragService->StartDragSession();
+ // For compatibility, we have to set temporary data.
+ auto dropData =
+ mozilla::java::GeckoDragAndDrop::DropData::Ref::From(aDropData);
+ nsDragService::SetDropData(dropData);
+ }
+
+ nsCOMPtr<nsIDragSession> dragSession;
+ dragService->GetCurrentSession(getter_AddRefs(dragSession));
+ if (dragSession) {
+ switch (message) {
+ case eDragOver:
+ dragService->SetDragEndPoint(point);
+ dragService->FireDragEventAtSource(eDrag, 0);
+ break;
+ case eDrop: {
+ bool canDrop = false;
+ dragSession->GetCanDrop(&canDrop);
+ if (!canDrop) {
+ nsCOMPtr<nsINode> sourceNode;
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode));
+ if (!sourceNode) {
+ dragService->EndDragSession(false, 0);
+ }
+ return;
+ }
+ auto dropData =
+ mozilla::java::GeckoDragAndDrop::DropData::Ref::From(aDropData);
+ nsDragService::SetDropData(dropData);
+ dragService->SetDragEndPoint(point);
+ break;
+ }
+ default:
+ break;
+ }
+
+ dragSession->SetDragAction(nsIDragService::DRAGDROP_ACTION_MOVE);
+ }
+
+ WidgetDragEvent geckoEvent(true, message, this);
+ geckoEvent.mRefPoint = point;
+ geckoEvent.mTimeStamp = nsWindow::GetEventTimeStamp(aTime);
+ geckoEvent.mModifiers = 0; // DragEvent has no modifiers
+ DispatchInputEvent(&geckoEvent);
+
+ if (!dragSession) {
+ return;
+ }
+
+ switch (message) {
+ case eDragExit: {
+ nsCOMPtr<nsINode> sourceNode;
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode));
+ if (!sourceNode) {
+ // We're leaving a window while doing a drag that was
+ // initiated in a different app. End the drag session,
+ // since we're done with it for now (until the user
+ // drags back into mozilla).
+ dragService->EndDragSession(false, 0);
+ }
+ break;
+ }
+ case eDrop:
+ dragService->EndDragSession(true, 0);
+ break;
+ default:
+ break;
+ }
+}
+
+void nsWindow::StartDragAndDrop(java::sdk::Bitmap::LocalRef aBitmap) {
+ if (mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ const auto& compositor = lvs->GetJavaCompositor();
+
+ DispatchToUiThread(
+ "nsWindow::StartDragAndDrop",
+ [compositor = GeckoSession::Compositor::GlobalRef(compositor),
+ bitmap = java::sdk::Bitmap::GlobalRef(aBitmap)] {
+ compositor->StartDragAndDrop(bitmap);
+ });
+ }
+}
+
+void nsWindow::UpdateDragImage(java::sdk::Bitmap::LocalRef aBitmap) {
+ if (mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ const auto& compositor = lvs->GetJavaCompositor();
+
+ DispatchToUiThread(
+ "nsWindow::UpdateDragImage",
+ [compositor = GeckoSession::Compositor::GlobalRef(compositor),
+ bitmap = java::sdk::Bitmap::GlobalRef(aBitmap)] {
+ compositor->UpdateDragImage(bitmap);
+ });
+ }
+}
+
+void nsWindow::OnSizeChanged(const gfx::IntSize& aSize) {
+ ALOG("nsWindow: %p OnSizeChanged [%d %d]", (void*)this, aSize.width,
+ aSize.height);
+
+ if (mWidgetListener) {
+ mWidgetListener->WindowResized(this, aSize.width, aSize.height);
+ }
+
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->WindowResized(this, aSize.width, aSize.height);
+ }
+
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyClientSizeChanged(
+ LayoutDeviceIntSize::FromUnknownSize(aSize));
+ }
+}
+
+void nsWindow::InitEvent(WidgetGUIEvent& event, LayoutDeviceIntPoint* aPoint) {
+ if (aPoint) {
+ event.mRefPoint = *aPoint;
+ } else {
+ event.mRefPoint = LayoutDeviceIntPoint(0, 0);
+ }
+}
+
+void nsWindow::UpdateOverscrollVelocity(const float aX, const float aY) {
+ if (::mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ const auto& compositor = lvs->GetJavaCompositor();
+ if (AndroidBridge::IsJavaUiThread()) {
+ compositor->UpdateOverscrollVelocity(aX, aY);
+ return;
+ }
+
+ DispatchToUiThread(
+ "nsWindow::UpdateOverscrollVelocity",
+ [compositor = GeckoSession::Compositor::GlobalRef(compositor), aX, aY] {
+ compositor->UpdateOverscrollVelocity(aX, aY);
+ });
+ }
+}
+
+void nsWindow::UpdateOverscrollOffset(const float aX, const float aY) {
+ if (::mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ const auto& compositor = lvs->GetJavaCompositor();
+ if (AndroidBridge::IsJavaUiThread()) {
+ compositor->UpdateOverscrollOffset(aX, aY);
+ return;
+ }
+
+ DispatchToUiThread(
+ "nsWindow::UpdateOverscrollOffset",
+ [compositor = GeckoSession::Compositor::GlobalRef(compositor), aX, aY] {
+ compositor->UpdateOverscrollOffset(aX, aY);
+ });
+ }
+}
+
+void* nsWindow::GetNativeData(uint32_t aDataType) {
+ switch (aDataType) {
+ // used by GLContextProviderEGL, nullptr is EGL_DEFAULT_DISPLAY
+ case NS_NATIVE_WIDGET:
+ return (void*)this;
+
+ case NS_RAW_NATIVE_IME_CONTEXT: {
+ void* pseudoIMEContext = GetPseudoIMEContext();
+ if (pseudoIMEContext) {
+ return pseudoIMEContext;
+ }
+ // We assume that there is only one context per process on Android
+ return NS_ONLY_ONE_NATIVE_IME_CONTEXT;
+ }
+
+ case NS_JAVA_SURFACE:
+ if (::mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ return lvs->GetSurface().Get();
+ }
+ return nullptr;
+ }
+
+ return nullptr;
+}
+
+void nsWindow::DispatchHitTest(const WidgetTouchEvent& aEvent) {
+ if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() == 1) {
+ // Since touch events don't get retargeted by PositionedEventTargeting.cpp
+ // code, we dispatch a dummy mouse event that *does* get retargeted.
+ // Front-end code can use this to activate the highlight element in case
+ // this touchstart is the start of a tap.
+ WidgetMouseEvent hittest(true, eMouseHitTest, this,
+ WidgetMouseEvent::eReal);
+ hittest.mRefPoint = aEvent.mTouches[0]->mRefPoint;
+ hittest.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+ nsEventStatus status;
+ DispatchEvent(&hittest, status);
+ }
+}
+
+void nsWindow::PassExternalResponse(java::WebResponse::Param aResponse) {
+ auto acc(mGeckoViewSupport.Access());
+ if (!acc) {
+ return;
+ }
+
+ acc->PassExternalResponse(aResponse);
+}
+
+mozilla::Modifiers nsWindow::GetModifiers(int32_t metaState) {
+ using mozilla::java::sdk::KeyEvent;
+ return (metaState & KeyEvent::META_ALT_MASK ? MODIFIER_ALT : 0) |
+ (metaState & KeyEvent::META_SHIFT_MASK ? MODIFIER_SHIFT : 0) |
+ (metaState & KeyEvent::META_CTRL_MASK ? MODIFIER_CONTROL : 0) |
+ (metaState & KeyEvent::META_META_MASK ? MODIFIER_META : 0) |
+ (metaState & KeyEvent::META_FUNCTION_ON ? MODIFIER_FN : 0) |
+ (metaState & KeyEvent::META_CAPS_LOCK_ON ? MODIFIER_CAPSLOCK : 0) |
+ (metaState & KeyEvent::META_NUM_LOCK_ON ? MODIFIER_NUMLOCK : 0) |
+ (metaState & KeyEvent::META_SCROLL_LOCK_ON ? MODIFIER_SCROLLLOCK : 0);
+}
+
+TimeStamp nsWindow::GetEventTimeStamp(int64_t aEventTime) {
+ // Android's event time is SystemClock.uptimeMillis that is counted in ms
+ // since OS was booted.
+ // (https://developer.android.com/reference/android/os/SystemClock.html)
+ // and this SystemClock.uptimeMillis uses SYSTEM_TIME_MONOTONIC.
+ // Our posix implemententaion of TimeStamp::Now uses SYSTEM_TIME_MONOTONIC
+ // too. Due to same implementation, we can use this via FromSystemTime.
+ int64_t tick =
+ BaseTimeDurationPlatformUtils::TicksFromMilliseconds(aEventTime);
+ return TimeStamp::FromSystemTime(tick);
+}
+
+void nsWindow::UserActivity() {
+ if (!mIdleService) {
+ mIdleService = do_GetService("@mozilla.org/widget/useridleservice;1");
+ }
+
+ if (mIdleService) {
+ mIdleService->ResetIdleTimeOut(0);
+ }
+
+ if (FindTopLevel() != nsWindow::TopWindow()) {
+ BringToFront();
+ }
+}
+
+RefPtr<mozilla::a11y::SessionAccessibility>
+nsWindow::GetSessionAccessibility() {
+ auto acc(mSessionAccessibility.Access());
+ if (!acc) {
+ return nullptr;
+ }
+
+ return acc.AsRefPtr();
+}
+
+TextEventDispatcherListener* nsWindow::GetNativeTextEventDispatcherListener() {
+ nsWindow* top = FindTopLevel();
+ MOZ_ASSERT(top);
+
+ auto acc(top->mEditableSupport.Access());
+ if (!acc) {
+ // Non-GeckoView windows don't support IME operations.
+ return nullptr;
+ }
+
+ nsCOMPtr<TextEventDispatcherListener> ptr;
+ if (NS_FAILED(acc->QueryInterface(NS_GET_IID(TextEventDispatcherListener),
+ getter_AddRefs(ptr)))) {
+ return nullptr;
+ }
+
+ return ptr.get();
+}
+
+void nsWindow::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ nsWindow* top = FindTopLevel();
+ MOZ_ASSERT(top);
+
+ auto acc(top->mEditableSupport.Access());
+ if (!acc) {
+ // Non-GeckoView windows don't support IME operations.
+ return;
+ }
+
+ // We are using an IME event later to notify Java, and the IME event
+ // will be processed by the top window. Therefore, to ensure the
+ // IME event uses the correct mInputContext, we need to let the top
+ // window process SetInputContext
+ acc->SetInputContext(aContext, aAction);
+}
+
+InputContext nsWindow::GetInputContext() {
+ nsWindow* top = FindTopLevel();
+ MOZ_ASSERT(top);
+
+ auto acc(top->mEditableSupport.Access());
+ if (!acc) {
+ // Non-GeckoView windows don't support IME operations.
+ return InputContext();
+ }
+
+ // We let the top window process SetInputContext,
+ // so we should let it process GetInputContext as well.
+ return acc->GetInputContext();
+}
+
+nsresult nsWindow::SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) {
+ mozilla::widget::AutoObserverNotifier notifier(aObserver, "touchpoint");
+
+ int eventType;
+ switch (aPointerState) {
+ case TOUCH_CONTACT:
+ // This could be a ACTION_DOWN or ACTION_MOVE depending on the
+ // existing state; it is mapped to the right thing in Java.
+ eventType = java::sdk::MotionEvent::ACTION_POINTER_DOWN;
+ break;
+ case TOUCH_REMOVE:
+ // This could be turned into a ACTION_UP in Java
+ eventType = java::sdk::MotionEvent::ACTION_POINTER_UP;
+ break;
+ case TOUCH_CANCEL:
+ eventType = java::sdk::MotionEvent::ACTION_CANCEL;
+ break;
+ case TOUCH_HOVER: // not supported for now
+ default:
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ MOZ_ASSERT(mNPZCSupport.IsAttached());
+ auto npzcSup(mNPZCSupport.Access());
+ MOZ_ASSERT(!!npzcSup);
+
+ const auto& npzc = npzcSup->GetJavaNPZC();
+ const auto& bounds = FindTopLevel()->mBounds;
+ aPoint.x -= bounds.x;
+ aPoint.y -= bounds.y;
+
+ DispatchToUiThread(
+ "nsWindow::SynthesizeNativeTouchPoint",
+ [npzc = java::PanZoomController::NativeProvider::GlobalRef(npzc),
+ aPointerId, eventType, aPoint, aPointerPressure, aPointerOrientation] {
+ npzc->SynthesizeNativeTouchPoint(aPointerId, eventType, aPoint.x,
+ aPoint.y, aPointerPressure,
+ aPointerOrientation);
+ });
+ return NS_OK;
+}
+
+nsresult nsWindow::SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ mozilla::widget::AutoObserverNotifier notifier(aObserver, "mouseevent");
+
+ MOZ_ASSERT(mNPZCSupport.IsAttached());
+ auto npzcSup(mNPZCSupport.Access());
+ MOZ_ASSERT(!!npzcSup);
+
+ const auto& npzc = npzcSup->GetJavaNPZC();
+ const auto& bounds = FindTopLevel()->mBounds;
+ aPoint.x -= bounds.x;
+ aPoint.y -= bounds.y;
+
+ int32_t nativeMessage;
+ switch (aNativeMessage) {
+ case NativeMouseMessage::ButtonDown:
+ nativeMessage = java::sdk::MotionEvent::ACTION_POINTER_DOWN;
+ break;
+ case NativeMouseMessage::ButtonUp:
+ nativeMessage = java::sdk::MotionEvent::ACTION_POINTER_UP;
+ break;
+ case NativeMouseMessage::Move:
+ nativeMessage = java::sdk::MotionEvent::ACTION_HOVER_MOVE;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Non supported mouse event on Android");
+ return NS_ERROR_INVALID_ARG;
+ }
+ int32_t button = 0;
+ if (aNativeMessage != NativeMouseMessage::ButtonUp) {
+ switch (aButton) {
+ case MouseButton::ePrimary:
+ button = java::sdk::MotionEvent::BUTTON_PRIMARY;
+ break;
+ case MouseButton::eMiddle:
+ button = java::sdk::MotionEvent::BUTTON_TERTIARY;
+ break;
+ case MouseButton::eSecondary:
+ button = java::sdk::MotionEvent::BUTTON_SECONDARY;
+ break;
+ case MouseButton::eX1:
+ button = java::sdk::MotionEvent::BUTTON_BACK;
+ break;
+ case MouseButton::eX2:
+ button = java::sdk::MotionEvent::BUTTON_FORWARD;
+ break;
+ default:
+ if (aNativeMessage == NativeMouseMessage::ButtonDown) {
+ MOZ_ASSERT_UNREACHABLE("Non supported mouse button type on Android");
+ return NS_ERROR_INVALID_ARG;
+ }
+ break;
+ }
+ }
+
+ // TODO (bug 1693237): Handle aModifierFlags.
+ DispatchToUiThread(
+ "nsWindow::SynthesizeNativeMouseEvent",
+ [npzc = java::PanZoomController::NativeProvider::GlobalRef(npzc),
+ nativeMessage, aPoint, button] {
+ npzc->SynthesizeNativeMouseEvent(nativeMessage, aPoint.x, aPoint.y,
+ button);
+ });
+ return NS_OK;
+}
+
+nsresult nsWindow::SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) {
+ return SynthesizeNativeMouseEvent(
+ aPoint, NativeMouseMessage::Move, MouseButton::eNotPressed,
+ nsIWidget::Modifiers::NO_MODIFIERS, aObserver);
+}
+
+void nsWindow::SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) {
+ if (delegate) {
+ mCompositorWidgetDelegate = delegate->AsPlatformSpecificDelegate();
+ MOZ_ASSERT(mCompositorWidgetDelegate,
+ "nsWindow::SetCompositorWidgetDelegate called with a "
+ "non-PlatformCompositorWidgetDelegate");
+ } else {
+ mCompositorWidgetDelegate = nullptr;
+ }
+}
+
+void nsWindow::GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) {
+ *aInitData = mozilla::widget::AndroidCompositorWidgetInitData(
+ mWidgetId, GetClientSize());
+}
+
+bool nsWindow::WidgetPaintsBackground() {
+ return StaticPrefs::android_widget_paints_background();
+}
+
+bool nsWindow::NeedsPaint() {
+ auto lvs(mLayerViewSupport.Access());
+ if (!lvs || lvs->CompositorPaused() || !GetWindowRenderer()) {
+ return false;
+ }
+
+ return nsIWidget::NeedsPaint();
+}
+
+void nsWindow::ConfigureAPZControllerThread() {
+ nsCOMPtr<nsISerialEventTarget> thread = mozilla::GetAndroidUiThread();
+ APZThreadUtils::SetControllerThread(thread);
+}
+
+already_AddRefed<GeckoContentController>
+nsWindow::CreateRootContentController() {
+ RefPtr<GeckoContentController> controller =
+ new AndroidContentController(this, mAPZEventState, mAPZC);
+ return controller.forget();
+}
+
+uint32_t nsWindow::GetMaxTouchPoints() const {
+ return java::GeckoAppShell::GetMaxTouchPoints();
+}
+
+void nsWindow::UpdateZoomConstraints(
+ const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
+ const mozilla::Maybe<ZoomConstraints>& aConstraints) {
+ nsBaseWidget::UpdateZoomConstraints(aPresShellId, aViewId, aConstraints);
+}
+
+CompositorBridgeChild* nsWindow::GetCompositorBridgeChild() const {
+ return mCompositorSession ? mCompositorSession->GetCompositorBridgeChild()
+ : nullptr;
+}
+
+void nsWindow::SetContentDocumentDisplayed(bool aDisplayed) {
+ mContentDocumentDisplayed = aDisplayed;
+}
+
+bool nsWindow::IsContentDocumentDisplayed() {
+ return mContentDocumentDisplayed;
+}
+
+void nsWindow::RecvToolbarAnimatorMessageFromCompositor(int32_t aMessage) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ if (::mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ lvs->RecvToolbarAnimatorMessage(aMessage);
+ }
+}
+
+void nsWindow::UpdateRootFrameMetrics(const ScreenPoint& aScrollOffset,
+ const CSSToScreenScale& aZoom) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ if (::mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ const auto& compositor = lvs->GetJavaCompositor();
+ mContentDocumentDisplayed = true;
+ compositor->UpdateRootFrameMetrics(aScrollOffset.x, aScrollOffset.y,
+ aZoom.scale);
+ }
+}
+
+void nsWindow::RecvScreenPixels(Shmem&& aMem, const ScreenIntSize& aSize,
+ bool aNeedsYFlip) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ if (::mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ lvs->RecvScreenPixels(std::move(aMem), aSize, aNeedsYFlip);
+ }
+}
+
+void nsWindow::UpdateDynamicToolbarMaxHeight(ScreenIntCoord aHeight) {
+ if (mDynamicToolbarMaxHeight == aHeight) {
+ return;
+ }
+
+ mDynamicToolbarMaxHeight = aHeight;
+
+ if (mWidgetListener) {
+ mWidgetListener->DynamicToolbarMaxHeightChanged(aHeight);
+ }
+
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->DynamicToolbarMaxHeightChanged(aHeight);
+ }
+}
+
+void nsWindow::UpdateDynamicToolbarOffset(ScreenIntCoord aOffset) {
+ if (mWidgetListener) {
+ mWidgetListener->DynamicToolbarOffsetChanged(aOffset);
+ }
+
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->DynamicToolbarOffsetChanged(aOffset);
+ }
+}
+
+ScreenIntMargin nsWindow::GetSafeAreaInsets() const { return mSafeAreaInsets; }
+
+void nsWindow::UpdateSafeAreaInsets(const ScreenIntMargin& aSafeAreaInsets) {
+ mSafeAreaInsets = aSafeAreaInsets;
+
+ if (mWidgetListener) {
+ mWidgetListener->SafeAreaInsetsChanged(aSafeAreaInsets);
+ }
+
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->SafeAreaInsetsChanged(aSafeAreaInsets);
+ }
+}
+
+jni::NativeWeakPtr<NPZCSupport> nsWindow::GetNPZCSupportWeakPtr() {
+ return mNPZCSupport;
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow();
+ return window.forget();
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow();
+ return window.forget();
+}
+
+static already_AddRefed<DataSourceSurface> GetCursorImage(
+ const nsIWidget::Cursor& aCursor, mozilla::CSSToLayoutDeviceScale aScale) {
+ if (!aCursor.IsCustom()) {
+ return nullptr;
+ }
+
+ RefPtr<DataSourceSurface> destDataSurface;
+
+ nsIntSize size = nsIWidget::CustomCursorSize(aCursor);
+ // prevent DoS attacks
+ if (size.width > 128 || size.height > 128) {
+ return nullptr;
+ }
+
+ RefPtr<gfx::SourceSurface> surface = aCursor.mContainer->GetFrameAtSize(
+ size * aScale.scale, imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ if (NS_WARN_IF(!surface)) {
+ return nullptr;
+ }
+
+ return AndroidWidgetUtils::GetDataSourceSurfaceForAndroidBitmap(surface);
+}
+
+static int32_t GetCursorType(nsCursor aCursor) {
+ // When our minimal requirement of SDK version is 25+,
+ // we should replace with JNI auto-generator.
+ switch (aCursor) {
+ case eCursor_standard:
+ // android.view.PointerIcon.TYPE_ARROW
+ return 0x3e8;
+ case eCursor_wait:
+ // android.view.PointerIcon.TYPE_WAIT
+ return 0x3ec;
+ case eCursor_select:
+ // android.view.PointerIcon.TYPE_TEXT;
+ return 0x3f0;
+ case eCursor_hyperlink:
+ // android.view.PointerIcon.TYPE_HAND
+ return 0x3ea;
+ case eCursor_n_resize:
+ case eCursor_s_resize:
+ case eCursor_ns_resize:
+ case eCursor_row_resize:
+ // android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW
+ return 0x3f7;
+ case eCursor_w_resize:
+ case eCursor_e_resize:
+ case eCursor_ew_resize:
+ case eCursor_col_resize:
+ // android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW
+ return 0x3f6;
+ case eCursor_nw_resize:
+ case eCursor_se_resize:
+ case eCursor_nwse_resize:
+ // android.view.PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW
+ return 0x3f9;
+ case eCursor_ne_resize:
+ case eCursor_sw_resize:
+ case eCursor_nesw_resize:
+ // android.view.PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW
+ return 0x3f8;
+ case eCursor_crosshair:
+ // android.view.PointerIcon.TYPE_CROSSHAIR
+ return 0x3ef;
+ case eCursor_move:
+ // android.view.PointerIcon.TYPE_ARROW
+ return 0x3e8;
+ case eCursor_help:
+ // android.view.PointerIcon.TYPE_HELP
+ return 0x3eb;
+ case eCursor_copy:
+ // android.view.PointerIcon.TYPE_COPY
+ return 0x3f3;
+ case eCursor_alias:
+ // android.view.PointerIcon.TYPE_ALIAS
+ return 0x3f2;
+ case eCursor_context_menu:
+ // android.view.PointerIcon.TYPE_CONTEXT_MENU
+ return 0x3e9;
+ case eCursor_cell:
+ // android.view.PointerIcon.TYPE_CELL
+ return 0x3ee;
+ case eCursor_grab:
+ // android.view.PointerIcon.TYPE_GRAB
+ return 0x3fc;
+ case eCursor_grabbing:
+ // android.view.PointerIcon.TYPE_GRABBING
+ return 0x3fd;
+ case eCursor_spinning:
+ // android.view.PointerIcon.TYPE_WAIT
+ return 0x3ec;
+ case eCursor_zoom_in:
+ // android.view.PointerIcon.TYPE_ZOOM_IN
+ return 0x3fa;
+ case eCursor_zoom_out:
+ // android.view.PointerIcon.TYPE_ZOOM_OUT
+ return 0x3fb;
+ case eCursor_not_allowed:
+ // android.view.PointerIcon.TYPE_NO_DROP:
+ return 0x3f4;
+ case eCursor_no_drop:
+ // android.view.PointerIcon.TYPE_NO_DROP:
+ return 0x3f4;
+ case eCursor_vertical_text:
+ // android.view.PointerIcon.TYPE_VERTICAL_TEXT
+ return 0x3f1;
+ case eCursor_all_scroll:
+ // android.view.PointerIcon.TYPE_ALL_SCROLL
+ return 0x3f5;
+ case eCursor_none:
+ // android.view.PointerIcon.TYPE_NULL
+ return 0;
+ default:
+ NS_WARNING_ASSERTION(aCursor, "Invalid cursor type");
+ // android.view.PointerIcon.TYPE_ARROW
+ return 0x3e8;
+ }
+}
+
+void nsWindow::SetCursor(const Cursor& aCursor) {
+ if (mozilla::jni::GetAPIVersion() < 24) {
+ return;
+ }
+
+ // Only change cursor if it's actually been changed
+ if (!mUpdateCursor && mCursor == aCursor) {
+ return;
+ }
+
+ mUpdateCursor = false;
+ mCursor = aCursor;
+
+ int32_t type = 0;
+ RefPtr<DataSourceSurface> destDataSurface =
+ GetCursorImage(aCursor, GetDefaultScale());
+ if (!destDataSurface) {
+ type = GetCursorType(aCursor.mDefaultCursor);
+ }
+
+ if (mozilla::jni::NativeWeakPtr<LayerViewSupport>::Accessor lvs{
+ mLayerViewSupport.Access()}) {
+ const auto& compositor = lvs->GetJavaCompositor();
+
+ DispatchToUiThread(
+ "nsWindow::SetCursor",
+ [compositor = GeckoSession::Compositor::GlobalRef(compositor), type,
+ destDataSurface = std::move(destDataSurface),
+ hotspotX = aCursor.mHotspotX, hotspotY = aCursor.mHotspotY] {
+ java::sdk::Bitmap::LocalRef bitmap;
+ if (destDataSurface) {
+ DataSourceSurface::ScopedMap destMap(destDataSurface,
+ DataSourceSurface::READ);
+ auto pixels = mozilla::jni::ByteBuffer::New(
+ reinterpret_cast<int8_t*>(destMap.GetData()),
+ destMap.GetStride() * destDataSurface->GetSize().height);
+ bitmap = java::sdk::Bitmap::CreateBitmap(
+ destDataSurface->GetSize().width,
+ destDataSurface->GetSize().height,
+ java::sdk::Bitmap::Config::ARGB_8888());
+ bitmap->CopyPixelsFromBuffer(pixels);
+ }
+ compositor->SetPointerIcon(type, bitmap, hotspotX, hotspotY);
+ });
+ }
+}
diff --git a/widget/android/nsWindow.h b/widget/android/nsWindow.h
new file mode 100644
index 0000000000..a2c2a798fe
--- /dev/null
+++ b/widget/android/nsWindow.h
@@ -0,0 +1,299 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: set sw=2 ts=4 expandtab:
+ * 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/. */
+
+#ifndef NSWINDOW_H_
+#define NSWINDOW_H_
+
+#include "AndroidGraphics.h"
+#include "nsBaseWidget.h"
+#include "gfxPoint.h"
+#include "nsIUserIdleServiceInternal.h"
+#include "nsTArray.h"
+#include "EventDispatcher.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/java/GeckoSessionNatives.h"
+#include "mozilla/java/WebResponseWrappers.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TextRange.h"
+#include "mozilla/UniquePtr.h"
+
+struct ANPEvent;
+
+namespace mozilla {
+class WidgetTouchEvent;
+
+namespace layers {
+class CompositorBridgeChild;
+class LayerManager;
+class APZCTreeManager;
+class UiCompositorControllerChild;
+} // namespace layers
+
+namespace widget {
+class AndroidView;
+class GeckoEditableSupport;
+class GeckoViewSupport;
+class LayerViewSupport;
+class NPZCSupport;
+class PlatformCompositorWidgetDelegate;
+} // namespace widget
+
+namespace ipc {
+class Shmem;
+} // namespace ipc
+
+namespace a11y {
+class SessionAccessibility;
+} // namespace a11y
+} // namespace mozilla
+
+class nsWindow final : public nsBaseWidget {
+ private:
+ virtual ~nsWindow();
+
+ public:
+ using nsBaseWidget::GetWindowRenderer;
+
+ nsWindow();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsWindow, nsBaseWidget)
+
+ static void InitNatives();
+ void OnGeckoViewReady();
+ RefPtr<mozilla::MozPromise<bool, bool, false>> OnLoadRequest(
+ nsIURI* aUri, int32_t aWindowType, int32_t aFlags,
+ nsIPrincipal* aTriggeringPrincipal, bool aHasUserGesture,
+ bool aIsTopLevel);
+
+ void OnUpdateSessionStore(mozilla::jni::Object::Param aBundle);
+
+ private:
+ // Unique ID given to each widget, used to map Surfaces to widgets
+ // in the CompositorSurfaceManager.
+ int32_t mWidgetId;
+
+ private:
+ RefPtr<mozilla::widget::AndroidView> mAndroidView;
+
+ // Object that implements native LayerView calls.
+ // Owned by the Java Compositor instance.
+ mozilla::jni::NativeWeakPtr<mozilla::widget::LayerViewSupport>
+ mLayerViewSupport;
+
+ // Object that implements native NativePanZoomController calls.
+ // Owned by the Java NativePanZoomController instance.
+ mozilla::jni::NativeWeakPtr<mozilla::widget::NPZCSupport> mNPZCSupport;
+
+ // Object that implements native GeckoEditable calls.
+ // Strong referenced by the Java instance.
+ mozilla::jni::NativeWeakPtr<mozilla::widget::GeckoEditableSupport>
+ mEditableSupport;
+ mozilla::jni::Object::GlobalRef mEditableParent;
+
+ // Object that implements native SessionAccessibility calls.
+ // Strong referenced by the Java instance.
+ mozilla::jni::NativeWeakPtr<mozilla::a11y::SessionAccessibility>
+ mSessionAccessibility;
+
+ // Object that implements native GeckoView calls and associated states.
+ // nullptr for nsWindows that were not opened from GeckoView.
+ mozilla::jni::NativeWeakPtr<mozilla::widget::GeckoViewSupport>
+ mGeckoViewSupport;
+
+ mozilla::Atomic<bool, mozilla::ReleaseAcquire> mContentDocumentDisplayed;
+
+ public:
+ static already_AddRefed<nsWindow> From(nsPIDOMWindowOuter* aDOMWindow);
+ static already_AddRefed<nsWindow> From(nsIWidget* aWidget);
+
+ static nsWindow* TopWindow();
+
+ static mozilla::Modifiers GetModifiers(int32_t aMetaState);
+ static mozilla::TimeStamp GetEventTimeStamp(int64_t aEventTime);
+
+ void InitEvent(mozilla::WidgetGUIEvent& event,
+ LayoutDeviceIntPoint* aPoint = 0);
+
+ void UpdateOverscrollVelocity(const float aX, const float aY);
+ void UpdateOverscrollOffset(const float aX, const float aY);
+
+ mozilla::widget::EventDispatcher* GetEventDispatcher() const;
+
+ void PassExternalResponse(mozilla::java::WebResponse::Param aResponse);
+
+ void ShowDynamicToolbar();
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void OnDragEvent(
+ int32_t aAction, int64_t aTime, float aX, float aY,
+ mozilla::jni::Object::Param aDropData);
+ void StartDragAndDrop(mozilla::java::sdk::Bitmap::LocalRef aBitmap);
+ void UpdateDragImage(mozilla::java::sdk::Bitmap::LocalRef aBitmap);
+
+ void DetachNatives();
+
+ mozilla::Mutex& GetDestroyMutex() { return mDestroyMutex; }
+
+ //
+ // nsIWidget
+ //
+
+ using nsBaseWidget::Create; // for Create signature not overridden here
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* aInitData) override;
+ virtual void Destroy() override;
+ virtual void SetParent(nsIWidget* aNewParent) override;
+ virtual nsIWidget* GetParent(void) override;
+ virtual float GetDPI() override;
+ virtual double GetDefaultScaleInternal() override;
+ virtual void Show(bool aState) override;
+ virtual bool IsVisible() const override;
+ virtual void ConstrainPosition(DesktopIntPoint&) override;
+ virtual void Move(double aX, double aY) override;
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+ void SetZIndex(int32_t aZIndex) override;
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override;
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual LayoutDeviceIntRect GetScreenBounds() override;
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+ nsEventStatus DispatchEvent(mozilla::WidgetGUIEvent* aEvent);
+ virtual nsresult MakeFullScreen(bool aFullScreen) override;
+ void SetCursor(const Cursor& aDefaultCursor) override;
+ void* GetNativeData(uint32_t aDataType) override;
+ virtual nsresult SetTitle(const nsAString& aTitle) override { return NS_OK; }
+ [[nodiscard]] virtual nsresult GetAttention(int32_t aCycleCount) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override;
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ virtual InputContext GetInputContext() override;
+
+ WindowRenderer* GetWindowRenderer() override;
+
+ void NotifyCompositorSessionLost(
+ mozilla::layers::CompositorSession* aSession) override;
+
+ virtual bool NeedsPaint() override;
+
+ virtual bool WidgetPaintsBackground() override;
+
+ virtual uint32_t GetMaxTouchPoints() const override;
+
+ void UpdateZoomConstraints(
+ const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
+ const mozilla::Maybe<ZoomConstraints>& aConstraints) override;
+
+ nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+ nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+ nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override;
+
+ void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override;
+
+ virtual void GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) override;
+
+ mozilla::layers::CompositorBridgeChild* GetCompositorBridgeChild() const;
+
+ void SetContentDocumentDisplayed(bool aDisplayed);
+ bool IsContentDocumentDisplayed();
+
+ // Call this function when the users activity is the direct cause of an
+ // event (like a keypress or mouse click).
+ void UserActivity();
+
+ mozilla::jni::Object::Ref& GetEditableParent() { return mEditableParent; }
+
+ RefPtr<mozilla::a11y::SessionAccessibility> GetSessionAccessibility();
+
+ void RecvToolbarAnimatorMessageFromCompositor(int32_t aMessage) override;
+ void UpdateRootFrameMetrics(const ScreenPoint& aScrollOffset,
+ const CSSToScreenScale& aZoom) override;
+ void RecvScreenPixels(mozilla::ipc::Shmem&& aMem, const ScreenIntSize& aSize,
+ bool aNeedsYFlip) override;
+ void UpdateDynamicToolbarMaxHeight(mozilla::ScreenIntCoord aHeight) override;
+ mozilla::ScreenIntCoord GetDynamicToolbarMaxHeight() const override {
+ return mDynamicToolbarMaxHeight;
+ }
+
+ void UpdateDynamicToolbarOffset(mozilla::ScreenIntCoord aOffset);
+
+ virtual mozilla::ScreenIntMargin GetSafeAreaInsets() const override;
+ void UpdateSafeAreaInsets(const mozilla::ScreenIntMargin& aSafeAreaInsets);
+
+ mozilla::jni::NativeWeakPtr<mozilla::widget::NPZCSupport>
+ GetNPZCSupportWeakPtr();
+
+ protected:
+ void BringToFront();
+ nsWindow* FindTopLevel();
+ bool IsTopLevel();
+
+ void ConfigureAPZControllerThread() override;
+ void DispatchHitTest(const mozilla::WidgetTouchEvent& aEvent);
+
+ already_AddRefed<GeckoContentController> CreateRootContentController()
+ override;
+
+ bool mIsVisible;
+ nsTArray<nsWindow*> mChildren;
+ nsWindow* mParent;
+
+ nsCOMPtr<nsIUserIdleServiceInternal> mIdleService;
+ mozilla::ScreenIntCoord mDynamicToolbarMaxHeight;
+ mozilla::ScreenIntMargin mSafeAreaInsets;
+
+ nsSizeMode mSizeMode;
+ bool mIsFullScreen;
+
+ bool UseExternalCompositingSurface() const override { return true; }
+
+ static void DumpWindows();
+ static void DumpWindows(const nsTArray<nsWindow*>& wins, int indent = 0);
+ static void LogWindow(nsWindow* win, int index, int indent);
+
+ private:
+ void CreateLayerManager();
+ void RedrawAll();
+
+ void OnSizeChanged(const mozilla::gfx::IntSize& aSize);
+
+ mozilla::layers::LayersId GetRootLayerId() const;
+ RefPtr<mozilla::layers::UiCompositorControllerChild>
+ GetUiCompositorControllerChild();
+
+ mozilla::widget::PlatformCompositorWidgetDelegate* mCompositorWidgetDelegate;
+
+ mozilla::Mutex mDestroyMutex;
+
+ friend class mozilla::widget::GeckoViewSupport;
+ friend class mozilla::widget::LayerViewSupport;
+ friend class mozilla::widget::NPZCSupport;
+};
+
+#endif /* NSWINDOW_H_ */
diff --git a/widget/cocoa/AppearanceOverride.h b/widget/cocoa/AppearanceOverride.h
new file mode 100644
index 0000000000..c4ae629320
--- /dev/null
+++ b/widget/cocoa/AppearanceOverride.h
@@ -0,0 +1,19 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef AppearanceOverride_h
+#define AppearanceOverride_h
+
+#import <Cocoa/Cocoa.h>
+
+// Implements support for the browser.theme.toolbar-theme pref.
+// Use MOZGlobalAppearance.sharedInstance.effectiveAppearance
+// in all places where you would like the global override to be respected. The
+// effectiveAppearance property can be key-value observed.
+@interface MOZGlobalAppearance : NSObject <NSAppearanceCustomization>
+@property(class, readonly) MOZGlobalAppearance* sharedInstance;
+@end
+
+#endif
diff --git a/widget/cocoa/AppearanceOverride.mm b/widget/cocoa/AppearanceOverride.mm
new file mode 100644
index 0000000000..c3938d6b93
--- /dev/null
+++ b/widget/cocoa/AppearanceOverride.mm
@@ -0,0 +1,90 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "AppearanceOverride.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+#include "nsXULAppAPI.h"
+
+static void ToolbarThemePrefChanged(const char* aPref, void* aUserInfo);
+
+@interface MOZGlobalAppearance ()
+@property NSInteger toolbarTheme;
+@end
+
+@implementation MOZGlobalAppearance
+
++ (MOZGlobalAppearance*)sharedInstance {
+ static MOZGlobalAppearance* sInstance = nil;
+ if (!sInstance) {
+ sInstance = [[MOZGlobalAppearance alloc] init];
+ if (XRE_IsParentProcess()) {
+ mozilla::Preferences::RegisterCallbackAndCall(
+ &ToolbarThemePrefChanged,
+ nsDependentCString(
+ mozilla::StaticPrefs::GetPrefName_browser_theme_toolbar_theme()));
+ }
+ }
+ return sInstance;
+}
+
++ (NSSet*)keyPathsForValuesAffectingAppearance {
+ return [NSSet setWithObjects:@"toolbarTheme", nil];
+}
+
+- (NSAppearance*)appearance {
+ switch (self.toolbarTheme) { // Value for browser.theme.toolbar-theme pref
+ case 0: // Dark
+ return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
+ case 1: // Light
+ return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
+ case 2: // System
+ default:
+ return nil; // nil means "no override".
+ }
+}
+
+- (void)setAppearance:(NSAppearance*)aAppearance {
+ // ignored
+}
+
+- (NSApplication*)_app {
+ return NSApp;
+}
+
++ (NSSet*)keyPathsForValuesAffectingEffectiveAppearance {
+ // Automatically notify any key-value observers of our effectiveAppearance
+ // property whenever the pref or the NSApp's effectiveAppearance change.
+ return
+ [NSSet setWithObjects:@"toolbarTheme", @"_app.effectiveAppearance", nil];
+}
+
+- (NSAppearance*)effectiveAppearance {
+ switch (self.toolbarTheme) { // Value for browser.theme.toolbar-theme pref
+ case 0: // Dark
+ return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
+ case 1: // Light
+ return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
+ case 2: // System
+ default:
+ // Use the NSApp effectiveAppearance. This is the system appearance.
+ return NSApp.effectiveAppearance;
+ }
+}
+
+@end
+
+static void ToolbarThemePrefChanged(const char* aPref, void* aUserInfo) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ MOZGlobalAppearance.sharedInstance.toolbarTheme =
+ mozilla::StaticPrefs::browser_theme_toolbar_theme();
+}
diff --git a/widget/cocoa/CFTypeRefPtr.h b/widget/cocoa/CFTypeRefPtr.h
new file mode 100644
index 0000000000..185355777e
--- /dev/null
+++ b/widget/cocoa/CFTypeRefPtr.h
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef CFTypeRefPtr_h
+#define CFTypeRefPtr_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DbgMacro.h"
+#include "mozilla/HashFunctions.h"
+
+// A smart pointer for CoreFoundation classes which does reference counting.
+//
+// Manual reference counting:
+//
+// UInt32 someNumber = 10;
+// CFNumberRef numberObject =
+// CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &someNumber);
+// // do something with numberObject
+// CFRelease(numberObject);
+//
+// Automatic reference counting using CFTypeRefPtr:
+//
+// UInt32 someNumber = 10;
+// auto numberObject =
+// CFTypeRefPtr<CFNumberRef>::WrapUnderCreateRule(
+// CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &someNumber));
+// // do something with numberObject
+// // no CFRelease
+
+template <class PtrT>
+class CFTypeRefPtr {
+ private:
+ void assign_with_CFRetain(PtrT aRawPtr) {
+ CFRetain(aRawPtr);
+ assign_assuming_CFRetain(aRawPtr);
+ }
+
+ void assign_assuming_CFRetain(PtrT aNewPtr) {
+ PtrT oldPtr = mRawPtr;
+ mRawPtr = aNewPtr;
+ if (oldPtr) {
+ CFRelease(oldPtr);
+ }
+ }
+
+ private:
+ PtrT mRawPtr;
+
+ public:
+ ~CFTypeRefPtr() {
+ if (mRawPtr) {
+ CFRelease(mRawPtr);
+ }
+ }
+
+ // Constructors
+
+ CFTypeRefPtr() : mRawPtr(nullptr) {}
+
+ CFTypeRefPtr(const CFTypeRefPtr<PtrT>& aSmartPtr)
+ : mRawPtr(aSmartPtr.mRawPtr) {
+ if (mRawPtr) {
+ CFRetain(mRawPtr);
+ }
+ }
+
+ CFTypeRefPtr(CFTypeRefPtr<PtrT>&& aRefPtr) : mRawPtr(aRefPtr.mRawPtr) {
+ aRefPtr.mRawPtr = nullptr;
+ }
+
+ MOZ_IMPLICIT CFTypeRefPtr(decltype(nullptr)) : mRawPtr(nullptr) {}
+
+ // There is no constructor from a raw pointer value.
+ // Use one of the static WrapUnder*Rule methods below instead.
+
+ static CFTypeRefPtr<PtrT> WrapUnderCreateRule(PtrT aRawPtr) {
+ CFTypeRefPtr<PtrT> ptr;
+ ptr.AssignUnderCreateRule(aRawPtr);
+ return ptr;
+ }
+
+ static CFTypeRefPtr<PtrT> WrapUnderGetRule(PtrT aRawPtr) {
+ CFTypeRefPtr<PtrT> ptr;
+ ptr.AssignUnderGetRule(aRawPtr);
+ return ptr;
+ }
+
+ // Assignment operators
+
+ CFTypeRefPtr<PtrT>& operator=(decltype(nullptr)) {
+ assign_assuming_CFRetain(nullptr);
+ return *this;
+ }
+
+ CFTypeRefPtr<PtrT>& operator=(const CFTypeRefPtr<PtrT>& aRhs) {
+ assign_with_CFRetain(aRhs.mRawPtr);
+ return *this;
+ }
+
+ CFTypeRefPtr<PtrT>& operator=(CFTypeRefPtr<PtrT>&& aRefPtr) {
+ assign_assuming_CFRetain(aRefPtr.mRawPtr);
+ aRefPtr.mRawPtr = nullptr;
+ return *this;
+ }
+
+ // There is no operator= for a raw pointer value.
+ // Use one of the AssignUnder*Rule methods below instead.
+
+ CFTypeRefPtr<PtrT>& AssignUnderCreateRule(PtrT aRawPtr) {
+ // Freshly-created objects come with a retain count of 1.
+ assign_assuming_CFRetain(aRawPtr);
+ return *this;
+ }
+
+ CFTypeRefPtr<PtrT>& AssignUnderGetRule(PtrT aRawPtr) {
+ assign_with_CFRetain(aRawPtr);
+ return *this;
+ }
+
+ // Other pointer operators
+
+ // This is the only way to get the raw pointer out of the smart pointer.
+ // There is no implicit conversion to a raw pointer.
+ PtrT get() const { return mRawPtr; }
+
+ // Don't allow implicit conversion of temporary CFTypeRefPtr to raw pointer,
+ // because the refcount might be one and the pointer will immediately become
+ // invalid.
+ operator PtrT() const&& = delete;
+ // Also don't allow implicit conversion of non-temporary CFTypeRefPtr.
+ operator PtrT() const& = delete;
+
+ // These let you null-check a pointer without calling get().
+ explicit operator bool() const { return !!mRawPtr; }
+};
+
+template <class PtrT>
+inline bool operator==(const CFTypeRefPtr<PtrT>& aLhs,
+ const CFTypeRefPtr<PtrT>& aRhs) {
+ return aLhs.get() == aRhs.get();
+}
+
+template <class PtrT>
+inline bool operator!=(const CFTypeRefPtr<PtrT>& aLhs,
+ const CFTypeRefPtr<PtrT>& aRhs) {
+ return !(aLhs == aRhs);
+}
+
+// Comparing an |CFTypeRefPtr| to |nullptr|
+
+template <class PtrT>
+inline bool operator==(const CFTypeRefPtr<PtrT>& aLhs, decltype(nullptr)) {
+ return aLhs.get() == nullptr;
+}
+
+template <class PtrT>
+inline bool operator==(decltype(nullptr), const CFTypeRefPtr<PtrT>& aRhs) {
+ return nullptr == aRhs.get();
+}
+
+template <class PtrT>
+inline bool operator!=(const CFTypeRefPtr<PtrT>& aLhs, decltype(nullptr)) {
+ return aLhs.get() != nullptr;
+}
+
+template <class PtrT>
+inline bool operator!=(decltype(nullptr), const CFTypeRefPtr<PtrT>& aRhs) {
+ return nullptr != aRhs.get();
+}
+
+// MOZ_DBG support
+
+template <class PtrT>
+std::ostream& operator<<(std::ostream& aOut, const CFTypeRefPtr<PtrT>& aObj) {
+ return mozilla::DebugValue(aOut, aObj.get());
+}
+
+// std::hash support (e.g. for unordered_map)
+namespace std {
+template <class PtrT>
+struct hash<CFTypeRefPtr<PtrT>> {
+ typedef CFTypeRefPtr<PtrT> argument_type;
+ typedef std::size_t result_type;
+ result_type operator()(argument_type const& aPtr) const {
+ return mozilla::HashGeneric(reinterpret_cast<uintptr_t>(aPtr.get()));
+ }
+};
+} // namespace std
+
+#endif /* CFTypeRefPtr_h */
diff --git a/widget/cocoa/CustomCocoaEvents.h b/widget/cocoa/CustomCocoaEvents.h
new file mode 100644
index 0000000000..3c02feb4b0
--- /dev/null
+++ b/widget/cocoa/CustomCocoaEvents.h
@@ -0,0 +1,18 @@
+/* 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/. */
+
+/*
+ * This file defines constants to be used in the "subtype" field of
+ * NSEventTypeApplicationDefined type NSEvents.
+ */
+
+#ifndef WIDGET_COCOA_CUSTOMCOCOAEVENTS_H_
+#define WIDGET_COCOA_CUSTOMCOCOAEVENTS_H_
+
+// Empty event, just used for prodding the event loop into responding.
+const short kEventSubtypeNone = 0;
+// Tracer event, used for timing the event loop responsiveness.
+const short kEventSubtypeTrace = 1;
+
+#endif /* WIDGET_COCOA_CUSTOMCOCOAEVENTS_H_ */
diff --git a/widget/cocoa/DesktopBackgroundImage.h b/widget/cocoa/DesktopBackgroundImage.h
new file mode 100644
index 0000000000..2fd7565369
--- /dev/null
+++ b/widget/cocoa/DesktopBackgroundImage.h
@@ -0,0 +1,19 @@
+/* -*- 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/. */
+
+#ifndef WIDGET_COCOA_DESKTOPBACKGROUNDIMAGE_H_
+#define WIDGET_COCOA_DESKTOPBACKGROUNDIMAGE_H_
+
+class nsIFile;
+
+namespace mozilla {
+namespace widget {
+
+void SetDesktopImage(nsIFile* aImage);
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // WIDGET_COCOA_DESKTOPBACKGROUNDIMAGE_H_
diff --git a/widget/cocoa/DesktopBackgroundImage.mm b/widget/cocoa/DesktopBackgroundImage.mm
new file mode 100644
index 0000000000..7c00109db5
--- /dev/null
+++ b/widget/cocoa/DesktopBackgroundImage.mm
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 20; 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 "mozilla/Logging.h"
+#include "nsCocoaUtils.h"
+#include "nsIFile.h"
+#include "DesktopBackgroundImage.h"
+
+#import <Foundation/Foundation.h>
+
+extern mozilla::LazyLogModule gCocoaUtilsLog;
+#undef LOG
+#define LOG(...) MOZ_LOG(gCocoaUtilsLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace widget {
+
+void SetDesktopImage(nsIFile* aImage) {
+ nsAutoCString imagePath;
+ nsresult rv = aImage->GetNativePath(imagePath);
+ if (NS_FAILED(rv)) {
+ LOG("%s ERROR: failed to get image path", __func__);
+ return;
+ }
+
+ bool exists = false;
+ rv = aImage->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) {
+ LOG("%s ERROR: file \"%s\" does not exist", __func__, imagePath.get());
+ return;
+ }
+
+ NSString* urlString = [NSString stringWithUTF8String:imagePath.get()];
+ if (!urlString) {
+ LOG("%s ERROR: null image path \"%s\"", __func__, imagePath.get());
+ return;
+ }
+
+ NSURL* url = [NSURL fileURLWithPath:urlString];
+ if (!url) {
+ LOG("%s ERROR: null image path URL \"%s\"", __func__, imagePath.get());
+ return;
+ }
+
+ // Only apply the background to the screen with focus
+ NSScreen* currentScreen = [NSScreen mainScreen];
+ if (!currentScreen) {
+ LOG("%s ERROR: got null NSScreen", __func__);
+ return;
+ }
+
+ // Use existing options for this screen
+ NSDictionary* screenOptions = [[NSWorkspace sharedWorkspace]
+ desktopImageOptionsForScreen:currentScreen];
+
+ NSError* error = nil;
+ if (![[NSWorkspace sharedWorkspace] setDesktopImageURL:url
+ forScreen:currentScreen
+ options:screenOptions
+ error:&error]) {
+ LOG("%s ERROR: setDesktopImageURL failed (%ld)", __func__,
+ (long)[error code]);
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/GfxInfo.h b/widget/cocoa/GfxInfo.h
new file mode 100644
index 0000000000..0c6e50c04c
--- /dev/null
+++ b/widget/cocoa/GfxInfo.h
@@ -0,0 +1,98 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_GfxInfo_h__
+#define __mozilla_widget_GfxInfo_h__
+
+#include "GfxInfoBase.h"
+
+#include "nsString.h"
+
+namespace mozilla {
+namespace widget {
+
+class GfxInfo : public GfxInfoBase {
+ public:
+ GfxInfo();
+ // We only declare the subset of nsIGfxInfo that we actually implement. The
+ // rest is brought forward from GfxInfoBase.
+ NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override;
+ NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override;
+ NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override;
+ NS_IMETHOD GetEmbeddedInFirefoxReality(
+ bool* aEmbeddedInFirefoxReality) override;
+ NS_IMETHOD GetHasBattery(bool* aHasBattery) override;
+ NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override;
+ NS_IMETHOD GetTestType(nsAString& aTestType) override;
+ NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override;
+ NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion2(
+ nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override;
+ NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override;
+
+ using GfxInfoBase::GetFeatureStatus;
+ using GfxInfoBase::GetFeatureSuggestedDriverVersion;
+
+ virtual nsresult Init() override;
+
+#ifdef DEBUG
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIGFXINFODEBUG
+#endif
+
+ virtual uint32_t OperatingSystemVersion() override { return mOSXVersion; }
+
+ protected:
+ virtual ~GfxInfo() {}
+
+ OperatingSystem GetOperatingSystem() override;
+ virtual nsresult GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS = nullptr) override;
+ virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override;
+
+ private:
+ void GetDeviceInfo();
+ void GetSelectedCityInfo();
+ void AddCrashReportAnnotations();
+
+ uint32_t mNumGPUsDetected;
+
+ uint32_t mAdapterRAM[2];
+ nsString mDeviceID[2];
+ nsString mDriverVersion[2];
+ nsString mDriverDate[2];
+ nsString mDeviceKey[2];
+
+ nsString mAdapterVendorID[2];
+ nsString mAdapterDeviceID[2];
+
+ uint32_t mOSXVersion;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_GfxInfo_h__ */
diff --git a/widget/cocoa/GfxInfo.mm b/widget/cocoa/GfxInfo.mm
new file mode 100644
index 0000000000..1476cc27db
--- /dev/null
+++ b/widget/cocoa/GfxInfo.mm
@@ -0,0 +1,562 @@
+/* -*- 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 <OpenGL/OpenGL.h>
+#include <OpenGL/CGLRenderers.h>
+
+#include "mozilla/ArrayUtils.h"
+
+#include "GfxInfo.h"
+#include "nsUnicharUtils.h"
+#include "nsExceptionHandler.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/Preferences.h"
+#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty
+
+#include <algorithm>
+
+#import <Foundation/Foundation.h>
+#import <IOKit/IOKitLib.h>
+#import <Cocoa/Cocoa.h>
+
+#include "jsapi.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#ifdef DEBUG
+NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug)
+#endif
+
+GfxInfo::GfxInfo() : mNumGPUsDetected(0), mOSXVersion{0} {
+ mAdapterRAM[0] = mAdapterRAM[1] = 0;
+}
+
+static OperatingSystem OSXVersionToOperatingSystem(uint32_t aOSXVersion) {
+ switch (nsCocoaFeatures::ExtractMajorVersion(aOSXVersion)) {
+ case 10:
+ switch (nsCocoaFeatures::ExtractMinorVersion(aOSXVersion)) {
+ case 6:
+ return OperatingSystem::OSX10_6;
+ case 7:
+ return OperatingSystem::OSX10_7;
+ case 8:
+ return OperatingSystem::OSX10_8;
+ case 9:
+ return OperatingSystem::OSX10_9;
+ case 10:
+ return OperatingSystem::OSX10_10;
+ case 11:
+ return OperatingSystem::OSX10_11;
+ case 12:
+ return OperatingSystem::OSX10_12;
+ case 13:
+ return OperatingSystem::OSX10_13;
+ case 14:
+ return OperatingSystem::OSX10_14;
+ case 15:
+ return OperatingSystem::OSX10_15;
+ case 16:
+ // Depending on the SDK version, we either get 10.16 or 11.0.
+ // Normalize this to 11.0.
+ return OperatingSystem::OSX11_0;
+ default:
+ break;
+ }
+ break;
+ case 11:
+ switch (nsCocoaFeatures::ExtractMinorVersion(aOSXVersion)) {
+ case 0:
+ return OperatingSystem::OSX11_0;
+ default:
+ break;
+ }
+ break;
+ }
+
+ return OperatingSystem::Unknown;
+}
+// The following three functions are derived from Chromium code
+static CFTypeRef SearchPortForProperty(io_registry_entry_t dspPort,
+ CFStringRef propertyName) {
+ return IORegistryEntrySearchCFProperty(
+ dspPort, kIOServicePlane, propertyName, kCFAllocatorDefault,
+ kIORegistryIterateRecursively | kIORegistryIterateParents);
+}
+
+static uint32_t IntValueOfCFData(CFDataRef d) {
+ uint32_t value = 0;
+
+ if (d) {
+ const uint32_t* vp = reinterpret_cast<const uint32_t*>(CFDataGetBytePtr(d));
+ if (vp != NULL) value = *vp;
+ }
+
+ return value;
+}
+
+void GfxInfo::GetDeviceInfo() {
+ mNumGPUsDetected = 0;
+
+ CFMutableDictionaryRef pci_dev_dict = IOServiceMatching("IOPCIDevice");
+ io_iterator_t io_iter;
+ if (IOServiceGetMatchingServices(kIOMasterPortDefault, pci_dev_dict,
+ &io_iter) != kIOReturnSuccess) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ false,
+ "Failed to detect any GPUs (couldn't enumerate IOPCIDevice services)");
+ return;
+ }
+
+ io_registry_entry_t entry = IO_OBJECT_NULL;
+ while ((entry = IOIteratorNext(io_iter)) != IO_OBJECT_NULL) {
+ constexpr uint32_t kClassCodeDisplayVGA = 0x30000;
+ CFTypeRef class_code_ref =
+ SearchPortForProperty(entry, CFSTR("class-code"));
+ if (class_code_ref) {
+ const uint32_t class_code = IntValueOfCFData((CFDataRef)class_code_ref);
+ CFRelease(class_code_ref);
+
+ if (class_code == kClassCodeDisplayVGA) {
+ CFTypeRef vendor_id_ref =
+ SearchPortForProperty(entry, CFSTR("vendor-id"));
+ if (vendor_id_ref) {
+ mAdapterVendorID[mNumGPUsDetected].AppendPrintf(
+ "0x%04x", IntValueOfCFData((CFDataRef)vendor_id_ref));
+ CFRelease(vendor_id_ref);
+ }
+ CFTypeRef device_id_ref =
+ SearchPortForProperty(entry, CFSTR("device-id"));
+ if (device_id_ref) {
+ mAdapterDeviceID[mNumGPUsDetected].AppendPrintf(
+ "0x%04x", IntValueOfCFData((CFDataRef)device_id_ref));
+ CFRelease(device_id_ref);
+ }
+ ++mNumGPUsDetected;
+ }
+ }
+ IOObjectRelease(entry);
+ if (mNumGPUsDetected == 2) {
+ break;
+ }
+ }
+ IOObjectRelease(io_iter);
+
+ // If we found IOPCI VGA devices, don't look for other devices
+ if (mNumGPUsDetected > 0) {
+ return;
+ }
+
+#if defined(__aarch64__)
+ CFMutableDictionaryRef agx_dev_dict = IOServiceMatching("AGXAccelerator");
+ if (IOServiceGetMatchingServices(kIOMasterPortDefault, agx_dev_dict,
+ &io_iter) == kIOReturnSuccess) {
+ io_registry_entry_t entry = IO_OBJECT_NULL;
+ while ((entry = IOIteratorNext(io_iter)) != IO_OBJECT_NULL) {
+ CFTypeRef vendor_id_ref =
+ SearchPortForProperty(entry, CFSTR("vendor-id"));
+ if (vendor_id_ref) {
+ mAdapterVendorID[mNumGPUsDetected].AppendPrintf(
+ "0x%04x", IntValueOfCFData((CFDataRef)vendor_id_ref));
+ CFRelease(vendor_id_ref);
+ ++mNumGPUsDetected;
+ }
+ IOObjectRelease(entry);
+ }
+
+ IOObjectRelease(io_iter);
+ }
+
+ // If we found an AGXAccelerator, don't look for an AppleParavirtGPU
+ if (mNumGPUsDetected > 0) {
+ return;
+ }
+#endif
+
+ CFMutableDictionaryRef apv_dev_dict = IOServiceMatching("AppleParavirtGPU");
+ if (IOServiceGetMatchingServices(kIOMasterPortDefault, apv_dev_dict,
+ &io_iter) == kIOReturnSuccess) {
+ io_registry_entry_t entry = IO_OBJECT_NULL;
+ while ((entry = IOIteratorNext(io_iter)) != IO_OBJECT_NULL) {
+ CFTypeRef vendor_id_ref =
+ SearchPortForProperty(entry, CFSTR("vendor-id"));
+ if (vendor_id_ref) {
+ mAdapterVendorID[mNumGPUsDetected].AppendPrintf(
+ "0x%04x", IntValueOfCFData((CFDataRef)vendor_id_ref));
+ CFRelease(vendor_id_ref);
+ }
+
+ CFTypeRef device_id_ref =
+ SearchPortForProperty(entry, CFSTR("device-id"));
+ if (device_id_ref) {
+ mAdapterDeviceID[mNumGPUsDetected].AppendPrintf(
+ "0x%04x", IntValueOfCFData((CFDataRef)device_id_ref));
+ CFRelease(device_id_ref);
+ }
+ ++mNumGPUsDetected;
+ IOObjectRelease(entry);
+ }
+
+ IOObjectRelease(io_iter);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mNumGPUsDetected > 0, "Failed to detect any GPUs");
+}
+
+nsresult GfxInfo::Init() {
+ nsresult rv = GfxInfoBase::Init();
+
+ // Calling CGLQueryRendererInfo causes us to switch to the discrete GPU
+ // even when we don't want to. We'll avoid doing so for now and just
+ // use the device ids.
+
+ GetDeviceInfo();
+
+ AddCrashReportAnnotations();
+
+ mOSXVersion = nsCocoaFeatures::macOSVersion();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetD2DEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetDWriteEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+/* readonly attribute bool HasBattery; */
+NS_IMETHODIMP GfxInfo::GetHasBattery(bool* aHasBattery) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* readonly attribute DOMString DWriteVersion; */
+NS_IMETHODIMP
+GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) {
+ return NS_ERROR_FAILURE;
+}
+
+/* readonly attribute DOMString cleartypeParameters; */
+NS_IMETHODIMP
+GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) {
+ return NS_ERROR_FAILURE;
+}
+
+/* readonly attribute DOMString windowProtocol; */
+NS_IMETHODIMP
+GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* readonly attribute DOMString testType; */
+NS_IMETHODIMP
+GfxInfo::GetTestType(nsAString& aTestType) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+/* readonly attribute DOMString adapterDescription; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) {
+ aAdapterDescription.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDescription2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDescription.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterRAM; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) {
+ *aAdapterRAM = mAdapterRAM[0];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterRAM2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ *aAdapterRAM = mAdapterRAM[1];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriver; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) {
+ aAdapterDriver.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriver2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriver.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVendor; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) {
+ aAdapterDriverVendor.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVendor2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriverVendor.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVersion; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) {
+ aAdapterDriverVersion.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverVersion2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriverVersion.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverDate; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) {
+ aAdapterDriverDate.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDriverDate2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDriverDate.AssignLiteral("");
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterVendorID; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) {
+ aAdapterVendorID = mAdapterVendorID[0];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterVendorID2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterVendorID = mAdapterVendorID[1];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDeviceID; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) {
+ aAdapterDeviceID = mAdapterDeviceID[0];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterDeviceID2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) {
+ if (mNumGPUsDetected < 2) {
+ return NS_ERROR_FAILURE;
+ }
+ aAdapterDeviceID = mAdapterDeviceID[1];
+ return NS_OK;
+}
+
+/* readonly attribute DOMString adapterSubsysID; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) {
+ return NS_ERROR_FAILURE;
+}
+
+/* readonly attribute DOMString adapterSubsysID2; */
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* readonly attribute boolean isGPU2Active; */
+NS_IMETHODIMP
+GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) { return NS_ERROR_FAILURE; }
+
+void GfxInfo::AddCrashReportAnnotations() {
+ nsString deviceID, vendorID, driverVersion;
+ nsAutoCString narrowDeviceID, narrowVendorID, narrowDriverVersion;
+
+ GetAdapterDeviceID(deviceID);
+ CopyUTF16toUTF8(deviceID, narrowDeviceID);
+ GetAdapterVendorID(vendorID);
+ CopyUTF16toUTF8(vendorID, narrowVendorID);
+ GetAdapterDriverVersion(driverVersion);
+ CopyUTF16toUTF8(driverVersion, narrowDriverVersion);
+
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID,
+ narrowVendorID);
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID,
+ narrowDeviceID);
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AdapterDriverVersion, narrowDriverVersion);
+}
+
+// We don't support checking driver versions on Mac.
+#define IMPLEMENT_MAC_DRIVER_BLOCKLIST(os, device, features, blockOn, ruleId) \
+ APPEND_TO_DRIVER_BLOCKLIST(os, device, features, blockOn, \
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), ruleId, \
+ "")
+
+const nsTArray<GfxDriverInfo>& GfxInfo::GetGfxDriverInfo() {
+ if (!sDriverInfo->Length()) {
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::RadeonX1000,
+ nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_MAC_RADEONX1000_NO_TEXTURE2D");
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::Geforce7300GT,
+ nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_MAC_7300_NO_WEBGL");
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::IntelHDGraphicsToIvyBridge,
+ nsIGfxInfo::FEATURE_GL_SWIZZLE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_MAC_INTELHD4000_NO_SWIZZLE");
+ // We block texture swizzling everwhere on mac because it's broken in some
+ // configurations and we want to support GPU switching.
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::All, nsIGfxInfo::FEATURE_GL_SWIZZLE,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_MAC_GPU_SWITCHING_NO_SWIZZLE");
+
+ // Older generation Intel devices do not perform well with WebRender.
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::IntelWebRenderBlocked,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_INTEL_GEN5_OR_OLDER");
+
+ // Intel HD3000 disabled due to bug 1661505
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::IntelSandyBridge,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_INTEL_MAC_HD3000_NO_WEBRENDER");
+
+ // wgpu doesn't safely support OOB behavior on Metal yet.
+ IMPLEMENT_MAC_DRIVER_BLOCKLIST(
+ OperatingSystem::OSX, DeviceFamily::All, nsIGfxInfo::FEATURE_WEBGPU,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ "FEATURE_FAILURE_MAC_WGPU_NO_METAL_BOUNDS_CHECKS");
+ }
+ return *sDriverInfo;
+}
+
+OperatingSystem GfxInfo::GetOperatingSystem() {
+ return OSXVersionToOperatingSystem(mOSXVersion);
+}
+
+nsresult GfxInfo::GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS /* = nullptr */) {
+ NS_ENSURE_ARG_POINTER(aStatus);
+ aSuggestedDriverVersion.SetIsVoid(true);
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ OperatingSystem os = OSXVersionToOperatingSystem(mOSXVersion);
+ if (aOS) *aOS = os;
+
+ if (sShutdownOccurred) {
+ return NS_OK;
+ }
+
+ // Don't evaluate special cases when we're evaluating the downloaded
+ // blocklist.
+ if (!aDriverInfo.Length()) {
+ if (aFeature == nsIGfxInfo::FEATURE_CANVAS2D_ACCELERATION) {
+ // See bug 1249659
+ switch (os) {
+ case OperatingSystem::OSX10_5:
+ case OperatingSystem::OSX10_6:
+ case OperatingSystem::OSX10_7:
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_OS_VERSION;
+ aFailureId = "FEATURE_FAILURE_CANVAS_OSX_VERSION";
+ break;
+ default:
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ break;
+ }
+ return NS_OK;
+ } else if (aFeature == nsIGfxInfo::FEATURE_WEBRENDER &&
+ nsCocoaFeatures::ProcessIsRosettaTranslated()) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_UNQUALIFIED_WEBRENDER_MAC_ROSETTA";
+ return NS_OK;
+ }
+ }
+
+ return GfxInfoBase::GetFeatureStatusImpl(
+ aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os);
+}
+
+#ifdef DEBUG
+
+// Implement nsIGfxInfoDebug
+
+/* void spoofVendorID (in DOMString aVendorID); */
+NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) {
+ mAdapterVendorID[0] = aVendorID;
+ return NS_OK;
+}
+
+/* void spoofDeviceID (in unsigned long aDeviceID); */
+NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) {
+ mAdapterDeviceID[0] = aDeviceID;
+ return NS_OK;
+}
+
+/* void spoofDriverVersion (in DOMString aDriverVersion); */
+NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) {
+ mDriverVersion[0] = aDriverVersion;
+ return NS_OK;
+}
+
+/* void spoofOSVersion (in unsigned long aVersion); */
+NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) {
+ mOSXVersion = aVersion;
+ return NS_OK;
+}
+
+#endif
diff --git a/widget/cocoa/MOZIconHelper.h b/widget/cocoa/MOZIconHelper.h
new file mode 100644
index 0000000000..4b66f13c05
--- /dev/null
+++ b/widget/cocoa/MOZIconHelper.h
@@ -0,0 +1,35 @@
+/* -*- 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/. */
+
+#ifndef MOZIconHelper_h
+#define MOZIconHelper_h
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsRect.h"
+
+class imgIContainer;
+class nsPresContext;
+
+namespace mozilla {
+class ComputedStyle;
+}
+
+@interface MOZIconHelper : NSObject
+
+// Returns an autoreleased empty NSImage.
++ (NSImage*)placeholderIconWithSize:(NSSize)aSize;
+
+// Returns an autoreleased NSImage.
++ (NSImage*)iconImageFromImageContainer:(imgIContainer*)aImage
+ withSize:(NSSize)aSize
+ presContext:(const nsPresContext*)aPresContext
+ computedStyle:
+ (const mozilla::ComputedStyle*)aComputedStyle
+ scaleFactor:(CGFloat)aScaleFactor;
+
+@end
+
+#endif // MOZIconHelper_h
diff --git a/widget/cocoa/MOZIconHelper.mm b/widget/cocoa/MOZIconHelper.mm
new file mode 100644
index 0000000000..e15407b797
--- /dev/null
+++ b/widget/cocoa/MOZIconHelper.mm
@@ -0,0 +1,65 @@
+/* -*- 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/. */
+
+/*
+ * Creates icons for display in native menu items on macOS.
+ */
+
+#include "MOZIconHelper.h"
+
+#include "imgIContainer.h"
+#include "nsCocoaUtils.h"
+
+@implementation MOZIconHelper
+
+// Returns an autoreleased empty NSImage.
++ (NSImage*)placeholderIconWithSize:(NSSize)aSize {
+ return [[[NSImage alloc] initWithSize:aSize] autorelease];
+}
+
+// Returns an autoreleased NSImage.
++ (NSImage*)iconImageFromImageContainer:(imgIContainer*)aImage
+ withSize:(NSSize)aSize
+ presContext:(const nsPresContext*)aPresContext
+ computedStyle:
+ (const mozilla::ComputedStyle*)aComputedStyle
+ scaleFactor:(CGFloat)aScaleFactor {
+ bool isEntirelyBlack = false;
+ NSImage* retainedImage = nil;
+ nsresult rv;
+ if (aScaleFactor != 0.0f) {
+ rv = nsCocoaUtils::CreateNSImageFromImageContainer(
+ aImage, imgIContainer::FRAME_CURRENT, aPresContext, aComputedStyle,
+ &retainedImage, aScaleFactor, &isEntirelyBlack);
+ } else {
+ rv = nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
+ aImage, imgIContainer::FRAME_CURRENT, aPresContext, aComputedStyle,
+ &retainedImage, &isEntirelyBlack);
+ }
+
+ NSImage* image = [retainedImage autorelease];
+
+ if (NS_FAILED(rv) || !image) {
+ return nil;
+ }
+
+ int32_t origWidth = 0, origHeight = 0;
+ aImage->GetWidth(&origWidth);
+ aImage->GetHeight(&origHeight);
+
+ // If all the color channels in the image are black, treat the image as a
+ // template. This will cause macOS to use the image's alpha channel as a mask
+ // and it will fill it with a color that looks good in the context that it's
+ // used in. For example, for regular menu items, the image will be black, but
+ // when the menu item is hovered (and its background is blue), it will be
+ // filled with white.
+ [image setTemplate:isEntirelyBlack];
+
+ [image setSize:aSize];
+
+ return image;
+}
+
+@end
diff --git a/widget/cocoa/MOZMenuOpeningCoordinator.h b/widget/cocoa/MOZMenuOpeningCoordinator.h
new file mode 100644
index 0000000000..eed6ed58fa
--- /dev/null
+++ b/widget/cocoa/MOZMenuOpeningCoordinator.h
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+#ifndef MOZMenuOpeningCoordinator_h
+#define MOZMenuOpeningCoordinator_h
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+class Runnable;
+}
+
+/*
+ * MOZMenuOpeningCoordinator is a workaround for the fact that opening an NSMenu
+ * creates a nested event loop. This event loop is only exited after the menu is
+ * closed. The caller of NativeMenuMac::ShowAsContextMenu does not expect
+ * ShowAsContextMenu to create a nested event loop, so we need to make sure to
+ * open the NSMenu asynchronously.
+ */
+
+@interface MOZMenuOpeningCoordinator : NSObject
+
++ (instancetype)sharedInstance;
+
+// Queue aMenu for opening.
+// The menu will open from a new event loop tick so that its nested event loop
+// does not block the caller. If another menu's nested event loop is currently
+// on the stack, we wait for that nested event loop to unwind before opening
+// aMenu. Returns a handle that can be passed to cancelAsynchronousOpening:. Can
+// only be called on the main thread.
+- (NSInteger)asynchronouslyOpenMenu:(NSMenu*)aMenu
+ atScreenPosition:(NSPoint)aPosition
+ forView:(NSView*)aView
+ withAppearance:(NSAppearance*)aAppearance
+ asContextMenu:(BOOL)aIsContextMenu;
+
+// If the menu opening request for aHandle hasn't been processed yet, cancel it.
+// Can only be called on the main thread.
+- (void)cancelAsynchronousOpening:(NSInteger)aHandle;
+
+// This field is a terrible workaround for a gnarly problem.
+// It should be set to YES by the caller of -[NSMenu
+// cancelTracking(WithoutAnimation)]. This field gets checked by the native
+// event loop code in nsAppShell.mm to avoid calling
+// -[NSApplication nextEventMatchingMask:...] between the call to cancelTracking
+// and the point at which the menu has finished closing and unwound from its
+// tracking event loop, because such calls can interfere with menu closing and
+// get us stuck in the menu event loop forever.
+@property(class) BOOL needToUnwindForMenuClosing;
+
+@end
+
+#endif // MOZMenuOpeningCoordinator_h
diff --git a/widget/cocoa/MOZMenuOpeningCoordinator.mm b/widget/cocoa/MOZMenuOpeningCoordinator.mm
new file mode 100644
index 0000000000..800859a92a
--- /dev/null
+++ b/widget/cocoa/MOZMenuOpeningCoordinator.mm
@@ -0,0 +1,224 @@
+/* -*- 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/. */
+
+/*
+ * Makes sure that the nested event loop for NSMenu tracking is situated as low
+ * on the stack as possible, and that two NSMenu event loops are never nested.
+ */
+
+#include "MOZMenuOpeningCoordinator.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "nsMenuX.h"
+#include "nsObjCExceptions.h"
+
+static BOOL sNeedToUnwindForMenuClosing = NO;
+
+@interface MOZMenuOpeningInfo : NSObject
+@property NSInteger handle;
+@property(retain) NSMenu* menu;
+@property NSPoint position;
+@property(retain) NSView* view;
+@property(retain) NSAppearance* appearance;
+@property BOOL isContextMenu;
+@end
+
+@implementation MOZMenuOpeningInfo
+@end
+
+@implementation MOZMenuOpeningCoordinator {
+ // non-nil between asynchronouslyOpenMenu:atScreenPosition:forView: and the
+ // time at at which it is unqueued in _runMenu.
+ MOZMenuOpeningInfo* mPendingOpening; // strong
+
+ // An incrementing counter
+ NSInteger mLastHandle;
+
+ // YES while _runMenu is on the stack
+ BOOL mRunMenuIsOnTheStack;
+}
+
++ (instancetype)sharedInstance {
+ static MOZMenuOpeningCoordinator* sInstance = nil;
+ if (!sInstance) {
+ sInstance = [[MOZMenuOpeningCoordinator alloc] init];
+ mozilla::RunOnShutdown([&]() {
+ [sInstance release];
+ sInstance = nil;
+ });
+ }
+ return sInstance;
+}
+
+- (void)dealloc {
+ MOZ_RELEASE_ASSERT(!mPendingOpening, "should be empty at shutdown");
+ [super dealloc];
+}
+
+- (NSInteger)asynchronouslyOpenMenu:(NSMenu*)aMenu
+ atScreenPosition:(NSPoint)aPosition
+ forView:(NSView*)aView
+ withAppearance:(NSAppearance*)aAppearance
+ asContextMenu:(BOOL)aIsContextMenu {
+ MOZ_RELEASE_ASSERT(!mPendingOpening,
+ "A menu is already waiting to open. Before opening the "
+ "next one, either wait "
+ "for this one to open or cancel the request.");
+
+ NSInteger handle = ++mLastHandle;
+
+ MOZMenuOpeningInfo* info = [[MOZMenuOpeningInfo alloc] init];
+ info.handle = handle;
+ info.menu = aMenu;
+ info.position = aPosition;
+ info.view = aView;
+ info.appearance = aAppearance;
+ info.isContextMenu = aIsContextMenu;
+ mPendingOpening = [info retain];
+ [info release];
+
+ if (!mRunMenuIsOnTheStack) {
+ // Call _runMenu from the event loop, so that it doesn't block this call.
+ [self performSelector:@selector(_runMenu) withObject:nil afterDelay:0.0];
+ }
+
+ return handle;
+}
+
+- (void)_runMenu {
+ MOZ_RELEASE_ASSERT(!mRunMenuIsOnTheStack);
+
+ mRunMenuIsOnTheStack = YES;
+
+ while (mPendingOpening) {
+ MOZMenuOpeningInfo* info = [mPendingOpening retain];
+ [mPendingOpening release];
+ mPendingOpening = nil;
+
+ @try {
+ [self _openMenu:info.menu
+ atScreenPosition:info.position
+ forView:info.view
+ withAppearance:info.appearance
+ asContextMenu:info.isContextMenu];
+ } @catch (NSException* exception) {
+ nsObjCExceptionLog(exception);
+ }
+
+ [info release];
+
+ // We have exited _openMenu's nested event loop.
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = NO;
+ }
+
+ mRunMenuIsOnTheStack = NO;
+}
+
+- (void)cancelAsynchronousOpening:(NSInteger)aHandle {
+ if (mPendingOpening && mPendingOpening.handle == aHandle) {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(_runMenu)
+ object:nil];
+ [mPendingOpening release];
+ mPendingOpening = nil;
+ }
+}
+
+- (void)_openMenu:(NSMenu*)aMenu
+ atScreenPosition:(NSPoint)aPosition
+ forView:(NSView*)aView
+ withAppearance:(NSAppearance*)aAppearance
+ asContextMenu:(BOOL)aIsContextMenu {
+ // There are multiple ways to display an NSMenu as a context menu.
+ //
+ // 1. We can return the NSMenu from -[ChildView menuForEvent:] and the NSView
+ // will open it for us.
+ // 2. We can call +[NSMenu popUpContextMenu:withEvent:forView:] inside a
+ // mouseDown handler with a real mouse down event.
+ // 3. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at a later
+ // time, with a real mouse event that we stored earlier.
+ // 4. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at any time,
+ // with a synthetic mouse event that we create just for that purpose.
+ // 5. We can call -[NSMenu popUpMenuPositioningItem:atLocation:inView:] and
+ // it just takes a position, not an event.
+ //
+ // 1-4 look the same, 5 looks different: 5 is made for use with NSPopUpButton,
+ // where the selected item needs to be shown at a specific position. If a tall
+ // menu is opened with a position close to the bottom edge of the screen, 5
+ // results in a cropped menu with scroll arrows, even if the entire menu would
+ // fit on the screen, due to the positioning constraint. 1-2 only work if the
+ // menu contents are known synchronously during the call to menuForEvent or
+ // during the mouseDown event handler.
+ // NativeMenuMac::ShowAsContextMenu can be called at any time. It could be
+ // called during a menuForEvent call (during a "contextmenu" event handler),
+ // or during a mouseDown handler, or at a later time. The code below uses
+ // option 4 as the preferred option for context menus because it's the
+ // simplest: It works in all scenarios and it doesn't have the drawbacks of
+ // option 5. For popups that aren't context menus and that should be
+ // positioned as close as possible to the given screen position, we use
+ // option 5.
+
+ if (aAppearance) {
+ if (@available(macOS 11.0, *)) {
+ // By default, NSMenu inherits its appearance from the opening NSEvent's
+ // window. If CSS has overridden it, on Big Sur + we can respect it with
+ // -[NSMenu setAppearance].
+ aMenu.appearance = aAppearance;
+ }
+ }
+
+ if (aView) {
+ NSWindow* window = aView.window;
+ NSPoint locationInWindow =
+ nsCocoaUtils::ConvertPointFromScreen(window, aPosition);
+ if (aIsContextMenu) {
+ // Create a synthetic event at the right location and open the menu
+ // [option 4].
+ NSEvent* event =
+ [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
+ location:locationInWindow
+ modifierFlags:0
+ timestamp:NSProcessInfo.processInfo.systemUptime
+ windowNumber:window.windowNumber
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:0.0f];
+ [NSMenu popUpContextMenu:aMenu withEvent:event forView:aView];
+ } else {
+ // For popups which are not context menus, we open the menu using [option
+ // 5]. We pass `nil` to indicate that we're positioning the top left
+ // corner of the menu. This path is used for anchored menupopups, so we
+ // prefer option 5 over option 4 so that the menu doesn't get flipped if
+ // space is tight.
+ NSPoint locationInView = [aView convertPoint:locationInWindow
+ fromView:nil];
+ [aMenu popUpMenuPositioningItem:nil
+ atLocation:locationInView
+ inView:aView];
+ }
+ } else {
+ // Open the menu using popUpMenuPositioningItem:atLocation:inView: [option
+ // 5]. This is not preferred, because it positions the menu differently from
+ // how a native context menu would be positioned; it enforces aPosition for
+ // the top left corner even if this means that the menu will be displayed in
+ // a clipped fashion with scroll arrows.
+ [aMenu popUpMenuPositioningItem:nil atLocation:aPosition inView:nil];
+ }
+}
+
++ (void)setNeedToUnwindForMenuClosing:(BOOL)aValue {
+ sNeedToUnwindForMenuClosing = aValue;
+}
+
++ (BOOL)needToUnwindForMenuClosing {
+ return sNeedToUnwindForMenuClosing;
+}
+
+@end
diff --git a/widget/cocoa/MacThemeGeometryType.h b/widget/cocoa/MacThemeGeometryType.h
new file mode 100644
index 0000000000..4691dbdec2
--- /dev/null
+++ b/widget/cocoa/MacThemeGeometryType.h
@@ -0,0 +1,17 @@
+/* -*- 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/. */
+#ifndef mozilla_widget_ThemeGeometryType
+#define mozilla_widget_ThemeGeometryType
+
+enum MacThemeGeometryType {
+ eThemeGeometryTypeTitlebar = 1,
+ eThemeGeometryTypeToolbar,
+ eThemeGeometryTypeToolbox,
+ eThemeGeometryTypeWindowButtons,
+ eThemeGeometryTypeMenu,
+ eThemeGeometryTypeTooltip,
+};
+
+#endif
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMac.h b/widget/cocoa/MediaHardwareKeysEventSourceMac.h
new file mode 100644
index 0000000000..da08b8108d
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMac.h
@@ -0,0 +1,47 @@
+/* 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/. */
+
+#ifndef WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMAC_H_
+#define WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMAC_H_
+
+#import <ApplicationServices/ApplicationServices.h>
+#import <CoreFoundation/CoreFoundation.h>
+
+#include "mozilla/dom/MediaControlKeySource.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+namespace widget {
+
+class MediaHardwareKeysEventSourceMac final
+ : public mozilla::dom::MediaControlKeySource {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaHardwareKeysEventSourceMac, override)
+ MediaHardwareKeysEventSourceMac() = default;
+
+ static CGEventRef EventTapCallback(CGEventTapProxy proxy, CGEventType type,
+ CGEventRef event, void* refcon);
+
+ bool Open() override;
+ void Close() override;
+ bool IsOpened() const override;
+
+ // Currently we don't support showing supported keys on the touch bar.
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override {}
+
+ private:
+ ~MediaHardwareKeysEventSourceMac() = default;
+
+ bool StartEventTap();
+ void StopEventTap();
+
+ // They are used to intercept mac hardware media keys.
+ CFMachPortRef mEventTap = nullptr;
+ CFRunLoopSourceRef mEventTapSource = nullptr;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMac.mm b/widget/cocoa/MediaHardwareKeysEventSourceMac.mm
new file mode 100644
index 0000000000..9a6991a181
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMac.mm
@@ -0,0 +1,191 @@
+/* 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 "MediaHardwareKeysEventSourceMac.h"
+
+#import <AppKit/AppKit.h>
+#import <AppKit/NSEvent.h>
+#import <IOKit/hidsystem/ev_keymap.h>
+
+#include "mozilla/dom/MediaControlUtils.h"
+
+using namespace mozilla::dom;
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaHardwareKeysEventSourceMac=%p, " msg, this, ##__VA_ARGS__))
+
+// This macro is used in static callback function, where we have to send `this`
+// explicitly.
+#define LOG2(msg, this, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaHardwareKeysEventSourceMac=%p, " msg, this, ##__VA_ARGS__))
+
+static const char* ToMediaControlKeyStr(int aKeyCode) {
+ switch (aKeyCode) {
+ case NX_KEYTYPE_PLAY:
+ return "Play";
+ case NX_KEYTYPE_NEXT:
+ return "Next";
+ case NX_KEYTYPE_PREVIOUS:
+ return "Previous";
+ case NX_KEYTYPE_FAST:
+ return "Fast";
+ case NX_KEYTYPE_REWIND:
+ return "Rewind";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid key code.");
+ return "UNKNOWN";
+ }
+}
+
+// The media keys subtype. No official docs found, but widely known.
+// http://lists.apple.com/archives/cocoa-dev/2007/Aug/msg00499.html
+const int kSystemDefinedEventMediaKeysSubtype = 8;
+
+static bool IsSupportedKeyCode(int aKeyCode) {
+ return aKeyCode == NX_KEYTYPE_PLAY || aKeyCode == NX_KEYTYPE_NEXT ||
+ aKeyCode == NX_KEYTYPE_FAST || aKeyCode == NX_KEYTYPE_PREVIOUS ||
+ aKeyCode == NX_KEYTYPE_REWIND;
+}
+
+static MediaControlKey ToMediaControlKey(int aKeyCode) {
+ MOZ_ASSERT(IsSupportedKeyCode(aKeyCode));
+ switch (aKeyCode) {
+ case NX_KEYTYPE_NEXT:
+ case NX_KEYTYPE_FAST:
+ return MediaControlKey::Nexttrack;
+ case NX_KEYTYPE_PREVIOUS:
+ case NX_KEYTYPE_REWIND:
+ return MediaControlKey::Previoustrack;
+ default:
+ MOZ_ASSERT(aKeyCode == NX_KEYTYPE_PLAY);
+ return MediaControlKey::Playpause;
+ }
+}
+
+namespace mozilla {
+namespace widget {
+
+bool MediaHardwareKeysEventSourceMac::IsOpened() const {
+ return mEventTap && mEventTapSource;
+}
+
+bool MediaHardwareKeysEventSourceMac::Open() {
+ LOG("Open MediaHardwareKeysEventSourceMac");
+ return StartEventTap();
+}
+
+void MediaHardwareKeysEventSourceMac::Close() {
+ LOG("Close MediaHardwareKeysEventSourceMac");
+ StopEventTap();
+ MediaControlKeySource::Close();
+}
+
+bool MediaHardwareKeysEventSourceMac::StartEventTap() {
+ LOG("StartEventTap");
+ MOZ_ASSERT(!mEventTap);
+ MOZ_ASSERT(!mEventTapSource);
+
+ // Add an event tap to intercept the system defined media key events.
+ mEventTap = CGEventTapCreate(
+ kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly,
+ CGEventMaskBit(NX_SYSDEFINED), EventTapCallback, this);
+ if (!mEventTap) {
+ LOG("Fail to create event tap");
+ return false;
+ }
+
+ mEventTapSource =
+ CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mEventTap, 0);
+ if (!mEventTapSource) {
+ LOG("Fail to create an event tap source.");
+ return false;
+ }
+
+ LOG("Add an event tap source to current loop");
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), mEventTapSource,
+ kCFRunLoopCommonModes);
+ return true;
+}
+
+void MediaHardwareKeysEventSourceMac::StopEventTap() {
+ LOG("StopEventTapIfNecessary");
+ if (mEventTap) {
+ CFMachPortInvalidate(mEventTap);
+ mEventTap = nullptr;
+ }
+ if (mEventTapSource) {
+ CFRunLoopRemoveSource(CFRunLoopGetCurrent(), mEventTapSource,
+ kCFRunLoopCommonModes);
+ CFRelease(mEventTapSource);
+ mEventTapSource = nullptr;
+ }
+}
+
+CGEventRef MediaHardwareKeysEventSourceMac::EventTapCallback(
+ CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon) {
+ // Re-enable event tap when receiving disabled events.
+ MediaHardwareKeysEventSourceMac* source =
+ static_cast<MediaHardwareKeysEventSourceMac*>(refcon);
+ if (type == kCGEventTapDisabledByUserInput ||
+ type == kCGEventTapDisabledByTimeout) {
+ MOZ_ASSERT(source->mEventTap);
+ CGEventTapEnable(source->mEventTap, true);
+ return event;
+ }
+
+ NSEvent* nsEvent = [NSEvent eventWithCGEvent:event];
+ if (nsEvent == nil) {
+ return event;
+ }
+
+ // Ignore not system defined media keys event.
+ if ([nsEvent type] != NSEventTypeSystemDefined ||
+ [nsEvent subtype] != kSystemDefinedEventMediaKeysSubtype) {
+ return event;
+ }
+
+ // Ignore media keys that aren't previous, next and play/pause.
+ // Magical constants are from http://weblog.rogueamoeba.com/2007/09/29/
+ // - keyCode = (([event data1] & 0xFFFF0000) >> 16)
+ // - keyFlags = ([event data1] & 0x0000FFFF)
+ // - keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
+ // - keyRepeat = (keyFlags & 0x1);
+ const NSInteger data1 = [nsEvent data1];
+ int keyCode = (data1 & 0xFFFF0000) >> 16;
+ if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_NEXT &&
+ keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_FAST &&
+ keyCode != NX_KEYTYPE_REWIND) {
+ return event;
+ }
+
+ // Ignore non-key pressed event (eg. key released).
+ int keyFlags = data1 & 0x0000FFFF;
+ bool isKeyPressed = ((keyFlags & 0xFF00) >> 8) == 0xA;
+ if (!isKeyPressed) {
+ return event;
+ }
+
+ // There is no listener waiting to process event.
+ if (source->mListeners.IsEmpty()) {
+ return event;
+ }
+
+ if (!IsSupportedKeyCode(keyCode)) {
+ return event;
+ }
+
+ LOG2("Get media key %s", source, ToMediaControlKeyStr(keyCode));
+ for (auto iter = source->mListeners.begin(); iter != source->mListeners.end();
+ ++iter) {
+ (*iter)->OnActionPerformed(MediaControlAction(ToMediaControlKey(keyCode)));
+ }
+ return event;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h
new file mode 100644
index 0000000000..c2b9d93000
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.h
@@ -0,0 +1,62 @@
+/* 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/. */
+
+#ifndef WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMACMEDIACENTER_H_
+#define WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMACMEDIACENTER_H_
+
+#include "mozilla/dom/MediaControlKeySource.h"
+
+#ifdef __OBJC__
+@class MPRemoteCommandEvent;
+#else
+typedef struct objc_object MPRemoteCommandEvent;
+#endif
+enum MPRemoteCommandHandlerStatus : long;
+
+namespace mozilla {
+namespace widget {
+
+typedef MPRemoteCommandHandlerStatus (^MediaCenterEventHandler)(
+ MPRemoteCommandEvent* event);
+
+class MediaHardwareKeysEventSourceMacMediaCenter final
+ : public mozilla::dom::MediaControlKeySource {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaHardwareKeysEventSourceMacMediaCenter,
+ override)
+ MediaHardwareKeysEventSourceMacMediaCenter();
+
+ MediaCenterEventHandler CreatePlayPauseHandler();
+ MediaCenterEventHandler CreateNextTrackHandler();
+ MediaCenterEventHandler CreatePreviousTrackHandler();
+ MediaCenterEventHandler CreatePlayHandler();
+ MediaCenterEventHandler CreatePauseHandler();
+
+ bool Open() override;
+ void Close() override;
+ bool IsOpened() const override;
+ void SetPlaybackState(dom::MediaSessionPlaybackState aState) override;
+ void SetMediaMetadata(const dom::MediaMetadataBase& aMetadata) override;
+ // Currently we don't support showing supported keys on the touch bar.
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override {}
+
+ private:
+ ~MediaHardwareKeysEventSourceMacMediaCenter();
+ void BeginListeningForEvents();
+ void EndListeningForEvents();
+ void HandleEvent(dom::MediaControlKey aKey);
+
+ bool mOpened = false;
+
+ MediaCenterEventHandler mPlayPauseHandler;
+ MediaCenterEventHandler mNextTrackHandler;
+ MediaCenterEventHandler mPreviousTrackHandler;
+ MediaCenterEventHandler mPauseHandler;
+ MediaCenterEventHandler mPlayHandler;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // WIDGET_COCOA_MEDIAHARDWAREKEYSEVENTSOURCEMACMEDIACENTER_H_
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm
new file mode 100644
index 0000000000..c7a85326fb
--- /dev/null
+++ b/widget/cocoa/MediaHardwareKeysEventSourceMacMediaCenter.mm
@@ -0,0 +1,187 @@
+/* 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/. */
+
+#import <MediaPlayer/MediaPlayer.h>
+
+#include "MediaHardwareKeysEventSourceMacMediaCenter.h"
+
+#include "mozilla/dom/MediaControlUtils.h"
+#include "nsCocoaUtils.h"
+
+using namespace mozilla::dom;
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaHardwareKeysEventSourceMacMediaCenter=%p, " msg, this, \
+ ##__VA_ARGS__))
+
+namespace mozilla {
+namespace widget {
+
+MediaCenterEventHandler
+MediaHardwareKeysEventSourceMacMediaCenter::CreatePlayPauseHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState =
+ center.playbackState == MPNowPlayingPlaybackStatePlaying
+ ? MPNowPlayingPlaybackStatePaused
+ : MPNowPlayingPlaybackStatePlaying;
+ HandleEvent(MediaControlKey::Playpause);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler
+MediaHardwareKeysEventSourceMacMediaCenter::CreateNextTrackHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ HandleEvent(MediaControlKey::Nexttrack);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler
+MediaHardwareKeysEventSourceMacMediaCenter::CreatePreviousTrackHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ HandleEvent(MediaControlKey::Previoustrack);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler
+MediaHardwareKeysEventSourceMacMediaCenter::CreatePlayHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ if (center.playbackState != MPNowPlayingPlaybackStatePlaying) {
+ center.playbackState = MPNowPlayingPlaybackStatePlaying;
+ }
+ HandleEvent(MediaControlKey::Play);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaCenterEventHandler
+MediaHardwareKeysEventSourceMacMediaCenter::CreatePauseHandler() {
+ return Block_copy(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent* event) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ if (center.playbackState != MPNowPlayingPlaybackStatePaused) {
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+ }
+ HandleEvent(MediaControlKey::Pause);
+ return MPRemoteCommandHandlerStatusSuccess;
+ });
+}
+
+MediaHardwareKeysEventSourceMacMediaCenter::
+ MediaHardwareKeysEventSourceMacMediaCenter() {
+ mPlayPauseHandler = CreatePlayPauseHandler();
+ mNextTrackHandler = CreateNextTrackHandler();
+ mPreviousTrackHandler = CreatePreviousTrackHandler();
+ mPlayHandler = CreatePlayHandler();
+ mPauseHandler = CreatePauseHandler();
+ LOG("Create MediaHardwareKeysEventSourceMacMediaCenter");
+}
+
+MediaHardwareKeysEventSourceMacMediaCenter::
+ ~MediaHardwareKeysEventSourceMacMediaCenter() {
+ LOG("Destroy MediaHardwareKeysEventSourceMacMediaCenter");
+ EndListeningForEvents();
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState = MPNowPlayingPlaybackStateStopped;
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::BeginListeningForEvents() {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState = MPNowPlayingPlaybackStatePlaying;
+ MPRemoteCommandCenter* commandCenter =
+ [MPRemoteCommandCenter sharedCommandCenter];
+ commandCenter.togglePlayPauseCommand.enabled = true;
+ [commandCenter.togglePlayPauseCommand addTargetWithHandler:mPlayPauseHandler];
+ commandCenter.nextTrackCommand.enabled = true;
+ [commandCenter.nextTrackCommand addTargetWithHandler:mNextTrackHandler];
+ commandCenter.previousTrackCommand.enabled = true;
+ [commandCenter.previousTrackCommand
+ addTargetWithHandler:mPreviousTrackHandler];
+ commandCenter.playCommand.enabled = true;
+ [commandCenter.playCommand addTargetWithHandler:mPlayHandler];
+ commandCenter.pauseCommand.enabled = true;
+ [commandCenter.pauseCommand addTargetWithHandler:mPauseHandler];
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::EndListeningForEvents() {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+ center.nowPlayingInfo = nil;
+ MPRemoteCommandCenter* commandCenter =
+ [MPRemoteCommandCenter sharedCommandCenter];
+ commandCenter.togglePlayPauseCommand.enabled = false;
+ [commandCenter.togglePlayPauseCommand removeTarget:nil];
+ commandCenter.nextTrackCommand.enabled = false;
+ [commandCenter.nextTrackCommand removeTarget:nil];
+ commandCenter.previousTrackCommand.enabled = false;
+ [commandCenter.previousTrackCommand removeTarget:nil];
+ commandCenter.playCommand.enabled = false;
+ [commandCenter.playCommand removeTarget:nil];
+ commandCenter.pauseCommand.enabled = false;
+ [commandCenter.pauseCommand removeTarget:nil];
+}
+
+bool MediaHardwareKeysEventSourceMacMediaCenter::Open() {
+ LOG("Open MediaHardwareKeysEventSourceMacMediaCenter");
+ mOpened = true;
+ BeginListeningForEvents();
+ return true;
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::Close() {
+ LOG("Close MediaHardwareKeysEventSourceMacMediaCenter");
+ SetPlaybackState(dom::MediaSessionPlaybackState::None);
+ EndListeningForEvents();
+ mOpened = false;
+ MediaControlKeySource::Close();
+}
+
+bool MediaHardwareKeysEventSourceMacMediaCenter::IsOpened() const {
+ return mOpened;
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::HandleEvent(
+ MediaControlKey aEvent) {
+ for (auto iter = mListeners.begin(); iter != mListeners.end(); ++iter) {
+ (*iter)->OnActionPerformed(MediaControlAction(aEvent));
+ }
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::SetPlaybackState(
+ MediaSessionPlaybackState aState) {
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ if (aState == MediaSessionPlaybackState::Playing) {
+ center.playbackState = MPNowPlayingPlaybackStatePlaying;
+ } else if (aState == MediaSessionPlaybackState::Paused) {
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+ } else if (aState == MediaSessionPlaybackState::None) {
+ center.playbackState = MPNowPlayingPlaybackStateStopped;
+ }
+ MediaControlKeySource::SetPlaybackState(aState);
+}
+
+void MediaHardwareKeysEventSourceMacMediaCenter::SetMediaMetadata(
+ const dom::MediaMetadataBase& aMetadata) {
+ NSMutableDictionary* nowPlayingInfo = [NSMutableDictionary dictionary];
+ [nowPlayingInfo setObject:nsCocoaUtils::ToNSString(aMetadata.mTitle)
+ forKey:MPMediaItemPropertyTitle];
+ [nowPlayingInfo setObject:nsCocoaUtils::ToNSString(aMetadata.mArtist)
+ forKey:MPMediaItemPropertyArtist];
+ [nowPlayingInfo setObject:nsCocoaUtils::ToNSString(aMetadata.mAlbum)
+ forKey:MPMediaItemPropertyAlbumTitle];
+ // The procedure of updating `nowPlayingInfo` is actually an async operation
+ // from our testing, Apple's documentation doesn't mention that though. So be
+ // aware that checking `nowPlayingInfo` immedately after setting it might not
+ // yield the expected result.
+ [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/MediaKeysEventSourceFactory.cpp b/widget/cocoa/MediaKeysEventSourceFactory.cpp
new file mode 100644
index 0000000000..5751a716df
--- /dev/null
+++ b/widget/cocoa/MediaKeysEventSourceFactory.cpp
@@ -0,0 +1,18 @@
+/* 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 "MediaKeysEventSourceFactory.h"
+
+#include "MediaHardwareKeysEventSourceMac.h"
+#include "MediaHardwareKeysEventSourceMacMediaCenter.h"
+
+namespace mozilla {
+namespace widget {
+
+mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() {
+ return new MediaHardwareKeysEventSourceMacMediaCenter();
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/NativeKeyBindings.h b/widget/cocoa/NativeKeyBindings.h
new file mode 100644
index 0000000000..6ab885fd95
--- /dev/null
+++ b/widget/cocoa/NativeKeyBindings.h
@@ -0,0 +1,67 @@
+/* -*- 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/. */
+
+#ifndef NativeKeyBindings_h
+#define NativeKeyBindings_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "nsTHashMap.h"
+#include "nsIWidget.h"
+
+struct objc_selector;
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
+namespace widget {
+
+typedef nsTHashMap<nsPtrHashKey<objc_selector>, Command>
+ SelectorCommandHashtable;
+
+class NativeKeyBindings final {
+ public:
+ static NativeKeyBindings* GetInstance(NativeKeyBindingsType aType);
+ static void Shutdown();
+
+ /**
+ * GetEditCommandsForTests() returns commands performed in native widget
+ * in typical environment. I.e., this does NOT refer customized shortcut
+ * key mappings of the environment.
+ */
+ static void GetEditCommandsForTests(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+
+ void Init(NativeKeyBindingsType aType);
+
+ void GetEditCommands(const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+
+ private:
+ NativeKeyBindings();
+
+ void AppendEditCommandsForSelector(objc_selector* aSelector,
+ nsTArray<CommandInt>& aCommands) const;
+
+ void LogEditCommands(const nsTArray<CommandInt>& aCommands,
+ const char* aDescription) const;
+
+ SelectorCommandHashtable mSelectorToCommand;
+
+ static NativeKeyBindings* sInstanceForSingleLineEditor;
+ static NativeKeyBindings* sInstanceForMultiLineEditor;
+}; // NativeKeyBindings
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // NativeKeyBindings_h
diff --git a/widget/cocoa/NativeKeyBindings.mm b/widget/cocoa/NativeKeyBindings.mm
new file mode 100644
index 0000000000..e4bdf715e2
--- /dev/null
+++ b/widget/cocoa/NativeKeyBindings.mm
@@ -0,0 +1,655 @@
+/* -*- 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 "NativeKeyBindings.h"
+
+#include "nsTArray.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
+
+#import <Cocoa/Cocoa.h>
+#import <Carbon/Carbon.h>
+
+namespace mozilla {
+namespace widget {
+
+static LazyLogModule gNativeKeyBindingsLog("NativeKeyBindings");
+
+NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr;
+NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr;
+
+// static
+NativeKeyBindings* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) {
+ switch (aType) {
+ case NativeKeyBindingsType::SingleLineEditor:
+ if (!sInstanceForSingleLineEditor) {
+ sInstanceForSingleLineEditor = new NativeKeyBindings();
+ sInstanceForSingleLineEditor->Init(aType);
+ }
+ return sInstanceForSingleLineEditor;
+ case NativeKeyBindingsType::MultiLineEditor:
+ case NativeKeyBindingsType::RichTextEditor:
+ if (!sInstanceForMultiLineEditor) {
+ sInstanceForMultiLineEditor = new NativeKeyBindings();
+ sInstanceForMultiLineEditor->Init(aType);
+ }
+ return sInstanceForMultiLineEditor;
+ default:
+ MOZ_CRASH("Not implemented");
+ return nullptr;
+ }
+}
+
+// static
+void NativeKeyBindings::Shutdown() {
+ delete sInstanceForSingleLineEditor;
+ sInstanceForSingleLineEditor = nullptr;
+ delete sInstanceForMultiLineEditor;
+ sInstanceForMultiLineEditor = nullptr;
+}
+
+NativeKeyBindings::NativeKeyBindings() {}
+
+inline objc_selector* ToObjcSelectorPtr(SEL aSel) {
+ return reinterpret_cast<objc_selector*>(aSel);
+}
+#define SEL_TO_COMMAND(aSel, aCommand) \
+ mSelectorToCommand.InsertOrUpdate(ToObjcSelectorPtr(@selector(aSel)), \
+ aCommand)
+
+void NativeKeyBindings::Init(NativeKeyBindingsType aType) {
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::Init", this));
+
+ // Many selectors have a one-to-one mapping to a Gecko command. Those mappings
+ // are registered in mSelectorToCommand.
+
+ // Selectors from NSResponder's "Responding to Action Messages" section and
+ // from NSText's "Action Methods for Editing" section
+
+ // TODO: Improves correctness of left / right meaning
+ // TODO: Add real paragraph motions
+
+ // SEL_TO_COMMAND(cancelOperation:, );
+ // SEL_TO_COMMAND(capitalizeWord:, );
+ // SEL_TO_COMMAND(centerSelectionInVisibleArea:, );
+ // SEL_TO_COMMAND(changeCaseOfLetter:, );
+ // SEL_TO_COMMAND(complete:, );
+ SEL_TO_COMMAND(copy:, Command::Copy);
+ // SEL_TO_COMMAND(copyFont:, );
+ // SEL_TO_COMMAND(copyRuler:, );
+ SEL_TO_COMMAND(cut:, Command::Cut);
+ SEL_TO_COMMAND(delete:, Command::Delete);
+ SEL_TO_COMMAND(deleteBackward:, Command::DeleteCharBackward);
+ // SEL_TO_COMMAND(deleteBackwardByDecomposingPreviousCharacter:, );
+ SEL_TO_COMMAND(deleteForward:, Command::DeleteCharForward);
+
+ // TODO: deleteTo* selectors are also supposed to add text to a kill buffer
+ SEL_TO_COMMAND(deleteToBeginningOfLine:, Command::DeleteToBeginningOfLine);
+ SEL_TO_COMMAND(deleteToBeginningOfParagraph:,
+ Command::DeleteToBeginningOfLine);
+ SEL_TO_COMMAND(deleteToEndOfLine:, Command::DeleteToEndOfLine);
+ SEL_TO_COMMAND(deleteToEndOfParagraph:, Command::DeleteToEndOfLine);
+ // SEL_TO_COMMAND(deleteToMark:, );
+
+ SEL_TO_COMMAND(deleteWordBackward:, Command::DeleteWordBackward);
+ SEL_TO_COMMAND(deleteWordForward:, Command::DeleteWordForward);
+ // SEL_TO_COMMAND(indent:, );
+ // SEL_TO_COMMAND(insertBacktab:, );
+ // SEL_TO_COMMAND(insertContainerBreak:, );
+ // SEL_TO_COMMAND(insertLineBreak:, );
+ // SEL_TO_COMMAND(insertNewline:, );
+ // SEL_TO_COMMAND(insertNewlineIgnoringFieldEditor:, );
+ // SEL_TO_COMMAND(insertParagraphSeparator:, );
+ // SEL_TO_COMMAND(insertTab:, );
+ // SEL_TO_COMMAND(insertTabIgnoringFieldEditor:, );
+ // SEL_TO_COMMAND(insertDoubleQuoteIgnoringSubstitution:, );
+ // SEL_TO_COMMAND(insertSingleQuoteIgnoringSubstitution:, );
+ // SEL_TO_COMMAND(lowercaseWord:, );
+ SEL_TO_COMMAND(moveBackward:, Command::CharPrevious);
+ SEL_TO_COMMAND(moveBackwardAndModifySelection:, Command::SelectCharPrevious);
+ if (aType == NativeKeyBindingsType::SingleLineEditor) {
+ SEL_TO_COMMAND(moveDown:, Command::EndLine);
+ } else {
+ SEL_TO_COMMAND(moveDown:, Command::LineNext);
+ }
+ SEL_TO_COMMAND(moveDownAndModifySelection:, Command::SelectLineNext);
+ SEL_TO_COMMAND(moveForward:, Command::CharNext);
+ SEL_TO_COMMAND(moveForwardAndModifySelection:, Command::SelectCharNext);
+ SEL_TO_COMMAND(moveLeft:, Command::CharPrevious);
+ SEL_TO_COMMAND(moveLeftAndModifySelection:, Command::SelectCharPrevious);
+ SEL_TO_COMMAND(moveParagraphBackwardAndModifySelection:,
+ Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveParagraphForwardAndModifySelection:,
+ Command::SelectEndLine);
+ SEL_TO_COMMAND(moveRight:, Command::CharNext);
+ SEL_TO_COMMAND(moveRightAndModifySelection:, Command::SelectCharNext);
+ SEL_TO_COMMAND(moveToBeginningOfDocument:, Command::MoveTop);
+ SEL_TO_COMMAND(moveToBeginningOfDocumentAndModifySelection:,
+ Command::SelectTop);
+ SEL_TO_COMMAND(moveToBeginningOfLine:, Command::BeginLine);
+ SEL_TO_COMMAND(moveToBeginningOfLineAndModifySelection:,
+ Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveToBeginningOfParagraph:, Command::BeginLine);
+ SEL_TO_COMMAND(moveToBeginningOfParagraphAndModifySelection:,
+ Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveToEndOfDocument:, Command::MoveBottom);
+ SEL_TO_COMMAND(moveToEndOfDocumentAndModifySelection:, Command::SelectBottom);
+ SEL_TO_COMMAND(moveToEndOfLine:, Command::EndLine);
+ SEL_TO_COMMAND(moveToEndOfLineAndModifySelection:, Command::SelectEndLine);
+ SEL_TO_COMMAND(moveToEndOfParagraph:, Command::EndLine);
+ SEL_TO_COMMAND(moveToEndOfParagraphAndModifySelection:,
+ Command::SelectEndLine);
+ SEL_TO_COMMAND(moveToLeftEndOfLine:, Command::BeginLine);
+ SEL_TO_COMMAND(moveToLeftEndOfLineAndModifySelection:,
+ Command::SelectBeginLine);
+ SEL_TO_COMMAND(moveToRightEndOfLine:, Command::EndLine);
+ SEL_TO_COMMAND(moveToRightEndOfLineAndModifySelection:,
+ Command::SelectEndLine);
+ if (aType == NativeKeyBindingsType::SingleLineEditor) {
+ SEL_TO_COMMAND(moveUp:, Command::BeginLine);
+ } else {
+ SEL_TO_COMMAND(moveUp:, Command::LinePrevious);
+ }
+ SEL_TO_COMMAND(moveUpAndModifySelection:, Command::SelectLinePrevious);
+ SEL_TO_COMMAND(moveWordBackward:, Command::WordPrevious);
+ SEL_TO_COMMAND(moveWordBackwardAndModifySelection:,
+ Command::SelectWordPrevious);
+ SEL_TO_COMMAND(moveWordForward:, Command::WordNext);
+ SEL_TO_COMMAND(moveWordForwardAndModifySelection:, Command::SelectWordNext);
+ SEL_TO_COMMAND(moveWordLeft:, Command::WordPrevious);
+ SEL_TO_COMMAND(moveWordLeftAndModifySelection:, Command::SelectWordPrevious);
+ SEL_TO_COMMAND(moveWordRight:, Command::WordNext);
+ SEL_TO_COMMAND(moveWordRightAndModifySelection:, Command::SelectWordNext);
+ SEL_TO_COMMAND(pageDown:, Command::MovePageDown);
+ SEL_TO_COMMAND(pageDownAndModifySelection:, Command::SelectPageDown);
+ SEL_TO_COMMAND(pageUp:, Command::MovePageUp);
+ SEL_TO_COMMAND(pageUpAndModifySelection:, Command::SelectPageUp);
+ SEL_TO_COMMAND(paste:, Command::Paste);
+ // SEL_TO_COMMAND(pasteFont:, );
+ // SEL_TO_COMMAND(pasteRuler:, );
+ SEL_TO_COMMAND(scrollLineDown:, Command::ScrollLineDown);
+ SEL_TO_COMMAND(scrollLineUp:, Command::ScrollLineUp);
+ SEL_TO_COMMAND(scrollPageDown:, Command::ScrollPageDown);
+ SEL_TO_COMMAND(scrollPageUp:, Command::ScrollPageUp);
+ SEL_TO_COMMAND(scrollToBeginningOfDocument:, Command::ScrollTop);
+ SEL_TO_COMMAND(scrollToEndOfDocument:, Command::ScrollBottom);
+ SEL_TO_COMMAND(selectAll:, Command::SelectAll);
+ // selectLine: is complex, see KeyDown
+ if (aType == NativeKeyBindingsType::SingleLineEditor) {
+ SEL_TO_COMMAND(selectParagraph:, Command::SelectAll);
+ }
+ // SEL_TO_COMMAND(selectToMark:, );
+ // selectWord: is complex, see KeyDown
+ // SEL_TO_COMMAND(setMark:, );
+ // SEL_TO_COMMAND(showContextHelp:, );
+ // SEL_TO_COMMAND(supplementalTargetForAction:sender:, );
+ // SEL_TO_COMMAND(swapWithMark:, );
+ // SEL_TO_COMMAND(transpose:, );
+ // SEL_TO_COMMAND(transposeWords:, );
+ // SEL_TO_COMMAND(uppercaseWord:, );
+ // SEL_TO_COMMAND(yank:, );
+}
+
+#undef SEL_TO_COMMAND
+
+void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands) {
+ MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
+ MOZ_ASSERT(aCommands.IsEmpty());
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands", this));
+
+ // Recover the current event, which should always be the key down we are
+ // responding to.
+
+ NSEvent* cocoaEvent = reinterpret_cast<NSEvent*>(aEvent.mNativeKeyEvent);
+
+ if (!cocoaEvent || [cocoaEvent type] != NSEventTypeKeyDown) {
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, no Cocoa key down event",
+ this));
+
+ return;
+ }
+
+ if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() &&
+ aWritingMode.ref().IsVertical()) {
+ NSEvent* originalEvent = cocoaEvent;
+
+ // TODO: Use KeyNameIndex rather than legacy keyCode.
+ uint32_t remappedGeckoKeyCode =
+ aEvent.GetRemappedKeyCode(aWritingMode.ref());
+ uint32_t remappedCocoaKeyCode = 0;
+ switch (remappedGeckoKeyCode) {
+ case NS_VK_UP:
+ remappedCocoaKeyCode = kVK_UpArrow;
+ break;
+ case NS_VK_DOWN:
+ remappedCocoaKeyCode = kVK_DownArrow;
+ break;
+ case NS_VK_LEFT:
+ remappedCocoaKeyCode = kVK_LeftArrow;
+ break;
+ case NS_VK_RIGHT:
+ remappedCocoaKeyCode = kVK_RightArrow;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
+ return;
+ }
+ unichar ch =
+ nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(remappedGeckoKeyCode);
+ NSString* chars = [[[NSString alloc] initWithCharacters:&ch
+ length:1] autorelease];
+ cocoaEvent = [NSEvent keyEventWithType:[originalEvent type]
+ location:[originalEvent locationInWindow]
+ modifierFlags:[originalEvent modifierFlags]
+ timestamp:[originalEvent timestamp]
+ windowNumber:[originalEvent windowNumber]
+ context:nil
+ characters:chars
+ charactersIgnoringModifiers:chars
+ isARepeat:[originalEvent isARepeat]
+ keyCode:remappedCocoaKeyCode];
+ }
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, interpreting", this));
+
+ AutoTArray<KeyBindingsCommand, 2> bindingCommands;
+ nsCocoaUtils::GetCommandsFromKeyEvent(cocoaEvent, bindingCommands);
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, bindingCommands=%zu", this,
+ bindingCommands.Length()));
+
+ for (uint32_t i = 0; i < bindingCommands.Length(); i++) {
+ SEL selector = bindingCommands[i].selector;
+
+ if (MOZ_LOG_TEST(gNativeKeyBindingsLog, LogLevel::Info)) {
+ NSString* selectorString = NSStringFromSelector(selector);
+ nsAutoString nsSelectorString;
+ nsCocoaUtils::GetStringForNSString(selectorString, nsSelectorString);
+
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p NativeKeyBindings::GetEditCommands, selector=%s", this,
+ NS_LossyConvertUTF16toASCII(nsSelectorString).get()));
+ }
+
+ AppendEditCommandsForSelector(ToObjcSelectorPtr(selector), aCommands);
+ }
+
+ LogEditCommands(aCommands, "NativeKeyBindings::GetEditCommands");
+}
+
+void NativeKeyBindings::AppendEditCommandsForSelector(
+ objc_selector* aSelector, nsTArray<CommandInt>& aCommands) const {
+ // Try to find a simple mapping in the hashtable
+ Command geckoCommand = Command::DoNothing;
+ if (mSelectorToCommand.Get(aSelector, &geckoCommand) &&
+ geckoCommand != Command::DoNothing) {
+ aCommands.AppendElement(static_cast<CommandInt>(geckoCommand));
+ } else if (aSelector == ToObjcSelectorPtr(@selector(selectLine:))) {
+ // This is functional, but Cocoa's version is direction-less in that
+ // selection direction is not determined until some future directed action
+ // is taken. See bug 282097, comment 79 for more details.
+ aCommands.AppendElement(static_cast<CommandInt>(Command::BeginLine));
+ aCommands.AppendElement(static_cast<CommandInt>(Command::SelectEndLine));
+ } else if (aSelector == ToObjcSelectorPtr(@selector(selectWord:))) {
+ // This is functional, but Cocoa's version is direction-less in that
+ // selection direction is not determined until some future directed action
+ // is taken. See bug 282097, comment 79 for more details.
+ aCommands.AppendElement(static_cast<CommandInt>(Command::WordPrevious));
+ aCommands.AppendElement(static_cast<CommandInt>(Command::SelectWordNext));
+ }
+}
+
+void NativeKeyBindings::LogEditCommands(const nsTArray<CommandInt>& aCommands,
+ const char* aDescription) const {
+ if (!MOZ_LOG_TEST(gNativeKeyBindingsLog, LogLevel::Info)) {
+ return;
+ }
+
+ if (aCommands.IsEmpty()) {
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p %s, no edit commands", this, aDescription));
+ return;
+ }
+
+ for (CommandInt commandInt : aCommands) {
+ Command geckoCommand = static_cast<Command>(commandInt);
+ MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
+ ("%p %s, command=%s", this, aDescription,
+ WidgetKeyboardEvent::GetCommandStr(geckoCommand)));
+ }
+}
+
+// static
+void NativeKeyBindings::GetEditCommandsForTests(
+ NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
+ MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted());
+
+ // The following mapping is checked on Big Sur. Some of them are defined in:
+ // https://support.apple.com/en-us/HT201236#text
+ NativeKeyBindings* instance = NativeKeyBindings::GetInstance(aType);
+ if (NS_WARN_IF(!instance)) {
+ return;
+ }
+ switch (aWritingMode.isSome()
+ ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref())
+ : aEvent.mKeyNameIndex) {
+ case KEY_NAME_INDEX_USE_STRING:
+ if (!aEvent.IsControl() || aEvent.IsAlt() || aEvent.IsMeta()) {
+ break;
+ }
+ switch (aEvent.PseudoCharCode()) {
+ case 'a':
+ case 'A':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToBeginningOfParagraph:))
+ : ToObjcSelectorPtr(@selector(
+ moveToBeginningOfParagraphAndModifySelection:)),
+ aCommands);
+ break;
+ case 'b':
+ case 'B':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveBackward:))
+ : ToObjcSelectorPtr(@selector(
+ moveBackwardAndModifySelection:)),
+ aCommands);
+ break;
+ case 'd':
+ case 'D':
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteForward:)), aCommands);
+ }
+ break;
+ case 'e':
+ case 'E':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToEndOfParagraph:))
+ : ToObjcSelectorPtr(
+ @selector(moveToEndOfParagraphAndModifySelection:)),
+ aCommands);
+ break;
+ case 'f':
+ case 'F':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift() ? ToObjcSelectorPtr(@selector(moveForward:))
+ : ToObjcSelectorPtr(@selector(
+ moveForwardAndModifySelection:)),
+ aCommands);
+ break;
+ case 'h':
+ case 'H':
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteBackward:)), aCommands);
+ }
+ break;
+ case 'k':
+ case 'K':
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteToEndOfParagraph:)),
+ aCommands);
+ }
+ break;
+ case 'n':
+ case 'N':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveDown:))
+ : ToObjcSelectorPtr(@selector(moveDownAndModifySelection:)),
+ aCommands);
+ break;
+ case 'p':
+ case 'P':
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveUp:))
+ : ToObjcSelectorPtr(@selector(moveUpAndModifySelection:)),
+ aCommands);
+ break;
+ default:
+ break;
+ }
+ break;
+ case KEY_NAME_INDEX_Backspace:
+ if (aEvent.IsMeta()) {
+ if (aEvent.IsAlt() || aEvent.IsControl()) {
+ break;
+ }
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteToBeginningOfLine:)), aCommands);
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift and Control are ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteWordBackward:)), aCommands);
+ break;
+ }
+ if (aEvent.IsControl()) {
+ if (aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(
+ @selector(deleteBackwardByDecomposingPreviousCharacter:)),
+ aCommands);
+ }
+ break;
+ }
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteBackward:)), aCommands);
+ break;
+ case KEY_NAME_INDEX_Delete:
+ if (aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteWordForward:)), aCommands);
+ break;
+ }
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(deleteForward:)), aCommands);
+ break;
+ case KEY_NAME_INDEX_PageDown:
+ if (aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(pageDown:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(scrollPageDown:))
+ : ToObjcSelectorPtr(@selector(pageDownAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_PageUp:
+ if (aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ // Shift is ignored.
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(pageUp:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(scrollPageUp:))
+ : ToObjcSelectorPtr(@selector(pageUpAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_Home:
+ if (aEvent.IsAlt() || aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(scrollToBeginningOfDocument:))
+ : ToObjcSelectorPtr(
+ @selector(moveToBeginningOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_End:
+ if (aEvent.IsAlt() || aEvent.IsControl() || aEvent.IsMeta()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(scrollToEndOfDocument:))
+ : ToObjcSelectorPtr(@selector
+ (moveToEndOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowLeft:
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) {
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToLeftEndOfLine:))
+ : ToObjcSelectorPtr(@selector
+ (moveToLeftEndOfLineAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsControl()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveLeft:))
+ : ToObjcSelectorPtr(@selector(moveLeftAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowRight:
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) {
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToRightEndOfLine:))
+ : ToObjcSelectorPtr(@selector
+ (moveToRightEndOfLineAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsControl()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveRight:))
+ : ToObjcSelectorPtr(@selector(moveRightAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowUp:
+ if (aEvent.IsControl()) {
+ break;
+ }
+ if (aEvent.IsMeta()) {
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToBeginningOfDocument:))
+ : ToObjcSelectorPtr(
+ @selector(moveToBegginingOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveBackward:)), aCommands);
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveToBeginningOfParagraph:)),
+ aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector
+ (moveParagraphBackwardAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveUp:))
+ : ToObjcSelectorPtr(@selector(moveUpAndModifySelection:)),
+ aCommands);
+ break;
+ case KEY_NAME_INDEX_ArrowDown:
+ if (aEvent.IsControl()) {
+ break;
+ }
+ if (aEvent.IsMeta()) {
+ if (aEvent.IsAlt()) {
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveToEndOfDocument:))
+ : ToObjcSelectorPtr(@selector
+ (moveToEndOfDocumentAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ if (aEvent.IsAlt()) {
+ if (!aEvent.IsShift()) {
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveForward:)), aCommands);
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector(moveToEndOfParagraph:)), aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ ToObjcSelectorPtr(@selector
+ (moveParagraphForwardAndModifySelection:)),
+ aCommands);
+ break;
+ }
+ instance->AppendEditCommandsForSelector(
+ !aEvent.IsShift()
+ ? ToObjcSelectorPtr(@selector(moveDown:))
+ : ToObjcSelectorPtr(@selector(moveDownAndModifySelection:)),
+ aCommands);
+ break;
+ default:
+ break;
+ }
+
+ instance->LogEditCommands(aCommands,
+ "NativeKeyBindings::GetEditCommandsForTests");
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/NativeMenuMac.h b/widget/cocoa/NativeMenuMac.h
new file mode 100644
index 0000000000..e230fd184c
--- /dev/null
+++ b/widget/cocoa/NativeMenuMac.h
@@ -0,0 +1,94 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef NativeMenuMac_h
+#define NativeMenuMac_h
+
+#include "mozilla/widget/NativeMenu.h"
+
+#include "nsMenuItemIconX.h"
+#include "nsMenuX.h"
+
+class nsIContent;
+class nsMenuGroupOwnerX;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+}
+
+namespace widget {
+
+class NativeMenuMac : public NativeMenu,
+ public nsMenuItemIconX::Listener,
+ public nsMenuX::Observer {
+ public:
+ explicit NativeMenuMac(dom::Element* aElement);
+
+ // NativeMenu
+ void ShowAsContextMenu(nsIFrame* aClickedFrame, const CSSIntPoint& aPosition,
+ bool aIsContextMenu) override;
+ bool Close() override;
+ void ActivateItem(dom::Element* aItemElement, Modifiers aModifiers,
+ int16_t aButton, ErrorResult& aRv) override;
+ void OpenSubmenu(dom::Element* aMenuElement) override;
+ void CloseSubmenu(dom::Element* aMenuElement) override;
+ RefPtr<dom::Element> Element() override;
+ void AddObserver(NativeMenu::Observer* aObserver) override {
+ mObservers.AppendElement(aObserver);
+ }
+ void RemoveObserver(NativeMenu::Observer* aObserver) override {
+ mObservers.RemoveElement(aObserver);
+ }
+
+ // nsMenuItemIconX::Listener
+ void IconUpdated() override;
+
+ // nsMenuX::Observer
+ void OnMenuWillOpen(dom::Element* aPopupElement) override;
+ void OnMenuDidOpen(dom::Element* aPopupElement) override;
+ void OnMenuWillActivateItem(dom::Element* aPopupElement,
+ dom::Element* aMenuItemElement) override;
+ void OnMenuClosed(dom::Element* aPopupElement) override;
+
+ NSMenu* NativeNSMenu() { return mMenu ? mMenu->NativeNSMenu() : nil; }
+ void MenuWillOpen();
+
+ // Returns whether a menu item was found at the specified path.
+ bool ActivateNativeMenuItemAt(const nsAString& aIndexString);
+
+ void ForceUpdateNativeMenuAt(const nsAString& aIndexString);
+ void Dump();
+
+ // If this menu is the menu of a system status bar item (NSStatusItem),
+ // let the menu know about the status item so that it can propagate
+ // any icon changes to the status item.
+ void SetContainerStatusBarItem(NSStatusItem* aItem);
+
+ protected:
+ virtual ~NativeMenuMac();
+
+ // Find the deepest nsMenuX which contains aElement, only descending into open
+ // menus.
+ // Returns nullptr if the element was not found or if the menus on the path
+ // were not all open.
+ RefPtr<nsMenuX> GetOpenMenuContainingElement(dom::Element* aElement);
+
+ RefPtr<dom::Element> mElement;
+ RefPtr<nsMenuGroupOwnerX> mMenuGroupOwner;
+ RefPtr<nsMenuX> mMenu;
+ nsTArray<NativeMenu::Observer*> mObservers;
+ NSStatusItem* mContainerStatusBarItem;
+
+ // Non-zero after a call to ShowAsContextMenu. Stores the handle from the
+ // MOZMenuOpeningCoordinator.
+ NSInteger mOpeningHandle = 0;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/cocoa/NativeMenuMac.mm b/widget/cocoa/NativeMenuMac.mm
new file mode 100644
index 0000000000..4efa48c53f
--- /dev/null
+++ b/widget/cocoa/NativeMenuMac.mm
@@ -0,0 +1,410 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "NativeMenuMac.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+
+#include "MOZMenuOpeningCoordinator.h"
+#include "nsISupports.h"
+#include "nsGkAtoms.h"
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsNativeThemeColors.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+#include "PresShell.h"
+#include "nsCocoaUtils.h"
+#include "nsIFrame.h"
+#include "nsPresContext.h"
+#include "nsDeviceContext.h"
+
+namespace mozilla {
+
+using dom::Element;
+
+namespace widget {
+
+NativeMenuMac::NativeMenuMac(dom::Element* aElement)
+ : mElement(aElement), mContainerStatusBarItem(nil) {
+ MOZ_RELEASE_ASSERT(
+ aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup));
+ mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, nullptr);
+ mMenu = MakeRefPtr<nsMenuX>(nullptr, mMenuGroupOwner, aElement);
+ mMenu->SetObserver(this);
+ mMenu->SetIconListener(this);
+ mMenu->SetupIcon();
+}
+
+NativeMenuMac::~NativeMenuMac() {
+ mMenu->DetachFromGroupOwnerRecursive();
+ mMenu->ClearObserver();
+ mMenu->ClearIconListener();
+}
+
+static void UpdateMenu(nsMenuX* aMenu) {
+ aMenu->MenuOpened();
+ aMenu->MenuClosed();
+
+ uint32_t itemCount = aMenu->GetItemCount();
+ for (uint32_t i = 0; i < itemCount; i++) {
+ nsMenuX::MenuChild menuObject = *aMenu->GetItemAt(i);
+ if (menuObject.is<RefPtr<nsMenuX>>()) {
+ UpdateMenu(menuObject.as<RefPtr<nsMenuX>>());
+ }
+ }
+}
+
+void NativeMenuMac::MenuWillOpen() {
+ // Force an update on the mMenu by faking an open/close on all of
+ // its submenus.
+ UpdateMenu(mMenu.get());
+}
+
+bool NativeMenuMac::ActivateNativeMenuItemAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSMenu* menu = mMenu->NativeNSMenu();
+
+ nsMenuUtilsX::CheckNativeMenuConsistency(menu);
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSMenuItem* item =
+ nsMenuUtilsX::NativeMenuItemWithLocation(menu, locationString, false);
+
+ // We can't perform an action on an item with a submenu, that will raise
+ // an obj-c exception.
+ if (item && !item.hasSubmenu) {
+ NSMenu* parent = item.menu;
+ if (parent) {
+ // NSLog(@"Performing action for native menu item titled: %@\n",
+ // [[currentSubmenu itemAtIndex:targetIndex] title]);
+ mozilla::AutoRestore<bool> autoRestore(
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
+ [parent performActionForItemAtIndex:[parent indexOfItem:item]];
+ return true;
+ }
+ }
+
+ return false;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSArray<NSString*>* indexes =
+ [locationString componentsSeparatedByString:@"|"];
+ RefPtr<nsMenuX> currentMenu = mMenu.get();
+
+ // now find the correct submenu
+ unsigned int indexCount = indexes.count;
+ for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
+ int targetIndex = [indexes objectAtIndex:i].intValue;
+ int visible = 0;
+ uint32_t length = currentMenu->GetItemCount();
+ for (unsigned int j = 0; j < length; j++) {
+ Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
+ if (!targetMenu) {
+ return;
+ }
+ RefPtr<nsIContent> content = targetMenu->match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->Content();
+ });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ visible++;
+ if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
+ currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
+ break;
+ }
+ }
+ }
+ }
+
+ // fake open/close to cause lazy update to happen
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::IconUpdated() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mContainerStatusBarItem) {
+ NSImage* menuImage = mMenu->NativeNSMenuItem().image;
+ if (menuImage) {
+ [menuImage setTemplate:YES];
+ }
+ mContainerStatusBarItem.button.image = menuImage;
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
+ mContainerStatusBarItem = aItem;
+ IconUpdated();
+}
+
+void NativeMenuMac::Dump() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenu->Dump(0);
+ nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
+ if (aPopupElement == mElement) {
+ return;
+ }
+
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout
+ // this function in case one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ observer->OnNativeSubMenuWillOpen(aPopupElement);
+ }
+}
+
+void NativeMenuMac::OnMenuDidOpen(dom::Element* aPopupElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout
+ // this function in case one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ if (aPopupElement == mElement) {
+ observer->OnNativeMenuOpened();
+ } else {
+ observer->OnNativeSubMenuDidOpen(aPopupElement);
+ }
+ }
+}
+
+void NativeMenuMac::OnMenuWillActivateItem(dom::Element* aPopupElement,
+ dom::Element* aMenuItemElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout
+ // this function in case one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ observer->OnNativeMenuWillActivateItem(aMenuItemElement);
+ }
+}
+
+void NativeMenuMac::OnMenuClosed(dom::Element* aPopupElement) {
+ // Our caller isn't keeping us alive, so make sure we stay alive throughout
+ // this function in case one of the observer notifications destroys us.
+ RefPtr<NativeMenuMac> kungFuDeathGrip(this);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ if (aPopupElement == mElement) {
+ observer->OnNativeMenuClosed();
+ } else {
+ observer->OnNativeSubMenuClosed(aPopupElement);
+ }
+ }
+}
+
+static NSView* NativeViewForFrame(nsIFrame* aFrame) {
+ nsIWidget* widget = aFrame->GetNearestWidget();
+ return (NSView*)widget->GetNativeData(NS_NATIVE_WIDGET);
+}
+
+static NSAppearance* NativeAppearanceForContent(nsIContent* aContent) {
+ nsIFrame* f = aContent->GetPrimaryFrame();
+ if (!f) {
+ return nil;
+ }
+ return NSAppearanceForColorScheme(LookAndFeel::ColorSchemeForFrame(f));
+}
+
+void NativeMenuMac::ShowAsContextMenu(nsIFrame* aClickedFrame,
+ const CSSIntPoint& aPosition,
+ bool aIsContextMenu) {
+ nsPresContext* pc = aClickedFrame->PresContext();
+ auto cssToDesktopScale =
+ pc->CSSToDevPixelScale() / pc->DeviceContext()->GetDesktopToDeviceScale();
+ const DesktopPoint desktopPoint = aPosition * cssToDesktopScale;
+
+ mMenu->PopupShowingEventWasSentAndApprovedExternally();
+
+ NSMenu* menu = mMenu->NativeNSMenu();
+ NSView* view = NativeViewForFrame(aClickedFrame);
+ NSAppearance* appearance = NativeAppearanceForContent(mMenu->Content());
+ NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(desktopPoint);
+
+ // Let the MOZMenuOpeningCoordinator do the actual opening, so that this
+ // ShowAsContextMenu call does not spawn a nested event loop, which would be
+ // surprising to our callers.
+ mOpeningHandle = [MOZMenuOpeningCoordinator.sharedInstance
+ asynchronouslyOpenMenu:menu
+ atScreenPosition:locationOnScreen
+ forView:view
+ withAppearance:appearance
+ asContextMenu:aIsContextMenu];
+}
+
+bool NativeMenuMac::Close() {
+ if (mOpeningHandle) {
+ // In case the menu was trying to open, but this Close() call interrupted
+ // it, cancel opening.
+ [MOZMenuOpeningCoordinator.sharedInstance
+ cancelAsynchronousOpening:mOpeningHandle];
+ }
+ return mMenu->Close();
+}
+
+RefPtr<nsMenuX> NativeMenuMac::GetOpenMenuContainingElement(
+ dom::Element* aElement) {
+ nsTArray<RefPtr<dom::Element>> submenuChain;
+ RefPtr<dom::Element> currentElement = aElement->GetParentElement();
+ while (currentElement && currentElement != mElement) {
+ if (currentElement->IsXULElement(nsGkAtoms::menu)) {
+ submenuChain.AppendElement(currentElement);
+ }
+ currentElement = currentElement->GetParentElement();
+ }
+ if (!currentElement) {
+ // aElement was not a descendent of mElement. Refuse to activate the item.
+ return nullptr;
+ }
+
+ // Traverse submenuChain from shallow to deep, to find the nsMenuX that
+ // contains aElement.
+ submenuChain.Reverse();
+ RefPtr<nsMenuX> menu = mMenu;
+ for (const auto& submenu : submenuChain) {
+ if (!menu->IsOpenForGecko()) {
+ // Refuse to descend into closed menus.
+ return nullptr;
+ }
+ Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
+ if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
+ // Couldn't find submenu.
+ return nullptr;
+ }
+ menu = menuChild->as<RefPtr<nsMenuX>>();
+ }
+
+ if (!menu->IsOpenForGecko()) {
+ // Refuse to descend into closed menus.
+ return nullptr;
+ }
+ return menu;
+}
+
+static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
+ NSEventModifierFlags flags = 0;
+ if (aModifiers & MODIFIER_CONTROL) {
+ flags |= NSEventModifierFlagControl;
+ }
+ if (aModifiers & MODIFIER_ALT) {
+ flags |= NSEventModifierFlagOption;
+ }
+ if (aModifiers & MODIFIER_SHIFT) {
+ flags |= NSEventModifierFlagShift;
+ }
+ if (aModifiers & MODIFIER_META) {
+ flags |= NSEventModifierFlagCommand;
+ }
+ return flags;
+}
+
+void NativeMenuMac::ActivateItem(dom::Element* aItemElement,
+ Modifiers aModifiers, int16_t aButton,
+ ErrorResult& aRv) {
+ RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
+ if (!menu) {
+ aRv.ThrowInvalidStateError("Menu containing menu item is not open");
+ return;
+ }
+ Maybe<nsMenuX::MenuChild> child = menu->GetItemForElement(aItemElement);
+ if (!child || !child->is<RefPtr<nsMenuItemX>>()) {
+ aRv.ThrowInvalidStateError("Could not find the supplied menu item");
+ return;
+ }
+
+ RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
+ if (!item->IsVisible()) {
+ aRv.ThrowInvalidStateError("Menu item is not visible");
+ return;
+ }
+
+ NSMenuItem* nativeItem = [item->NativeNSMenuItem() retain];
+
+ // First, initiate the closing of the NSMenu.
+ // This synchronously calls the menu delegate's menuDidClose handler. So
+ // menuDidClose is what runs first; this matches the order of events for
+ // user-initiated menu item activation. This call doesn't immediately hide the
+ // menu; the menu only hides once the stack unwinds from NSMenu's nested
+ // "tracking" event loop.
+ [mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
+
+ // Next, call OnWillActivateItem. This also matches the order of calls that
+ // happen when a user activates a menu item in the real world: -[MenuDelegate
+ // menu:willActivateItem:] runs after menuDidClose.
+ menu->OnWillActivateItem(nativeItem);
+
+ // Finally, call ActivateItemAfterClosing. This also mimics the order in the
+ // real world: menuItemHit is called after menu:willActivateItem:.
+ menu->ActivateItemAfterClosing(std::move(item),
+ ConvertModifierFlags(aModifiers), aButton);
+
+ // Tell our native event loop that it should not process any more work before
+ // unwinding the stack, so that we can get out of the menu's nested event loop
+ // as fast as possible. This was needed to fix spurious failures in tests,
+ // where a call to cancelTrackingWithoutAnimation was ignored if more native
+ // events were processed before the event loop was exited. As a result, the
+ // menu stayed open forever and the test never finished.
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
+
+ [nativeItem release];
+}
+
+void NativeMenuMac::OpenSubmenu(dom::Element* aMenuElement) {
+ if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
+ Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
+ if (item && item->is<RefPtr<nsMenuX>>()) {
+ item->as<RefPtr<nsMenuX>>()->MenuOpened();
+ }
+ }
+}
+
+void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) {
+ if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
+ Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
+ if (item && item->is<RefPtr<nsMenuX>>()) {
+ item->as<RefPtr<nsMenuX>>()->MenuClosed();
+ }
+ }
+}
+
+RefPtr<Element> NativeMenuMac::Element() { return mElement; }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/NativeMenuSupport.mm b/widget/cocoa/NativeMenuSupport.mm
new file mode 100644
index 0000000000..2e94f6f7cf
--- /dev/null
+++ b/widget/cocoa/NativeMenuSupport.mm
@@ -0,0 +1,37 @@
+/* -*- 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 "mozilla/widget/NativeMenuSupport.h"
+
+#include "MainThreadUtils.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "NativeMenuMac.h"
+#include "nsCocoaWindow.h"
+#include "nsMenuBarX.h"
+
+namespace mozilla::widget {
+
+void NativeMenuSupport::CreateNativeMenuBar(nsIWidget* aParent,
+ dom::Element* aMenuBarElement) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread(),
+ "Attempting to create native menu bar on wrong thread!");
+
+ // Create the menubar and give it to the parent window. The parent takes
+ // ownership.
+ static_cast<nsCocoaWindow*>(aParent)->SetMenuBar(
+ MakeRefPtr<nsMenuBarX>(aMenuBarElement));
+}
+
+already_AddRefed<NativeMenu> NativeMenuSupport::CreateNativeContextMenu(
+ dom::Element* aPopup) {
+ return MakeAndAddRef<NativeMenuMac>(aPopup);
+}
+
+bool NativeMenuSupport::ShouldUseNativeContextMenus() {
+ return StaticPrefs::widget_macos_native_context_menus();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/cocoa/OSXNotificationCenter.h b/widget/cocoa/OSXNotificationCenter.h
new file mode 100644
index 0000000000..65025d04ab
--- /dev/null
+++ b/widget/cocoa/OSXNotificationCenter.h
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+#ifndef OSXNotificationCenter_h
+#define OSXNotificationCenter_h
+
+#import <Foundation/Foundation.h>
+#include "nsIAlertsService.h"
+#include "nsTArray.h"
+#include "mozilla/RefPtr.h"
+
+// mozNotificationCenterDelegate is used to access the macOS notification
+// center. It is not related to the DesktopNotificationCenter object, which was
+// removed in bug 952453. While there are no direct references to this class
+// elsewhere, removing this will cause push notifications on macOS to stop
+// working.
+@class mozNotificationCenterDelegate;
+
+namespace mozilla {
+
+class OSXNotificationInfo;
+
+class OSXNotificationCenter : public nsIAlertsService,
+ public nsIAlertsIconData,
+ public nsIAlertsDoNotDisturb,
+ public nsIAlertNotificationImageListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIALERTSSERVICE
+ NS_DECL_NSIALERTSICONDATA
+ NS_DECL_NSIALERTSDONOTDISTURB
+ NS_DECL_NSIALERTNOTIFICATIONIMAGELISTENER
+
+ OSXNotificationCenter();
+
+ nsresult Init();
+ void CloseAlertCocoaString(NSString* aAlertName);
+ void OnActivate(NSString* aAlertName,
+ NSUserNotificationActivationType aActivationType,
+ unsigned long long aAdditionalActionIndex);
+ void ShowPendingNotification(OSXNotificationInfo* osxni);
+
+ protected:
+ virtual ~OSXNotificationCenter();
+
+ private:
+ mozNotificationCenterDelegate* mDelegate;
+ nsTArray<RefPtr<OSXNotificationInfo> > mActiveAlerts;
+ nsTArray<RefPtr<OSXNotificationInfo> > mPendingAlerts;
+ bool mSuppressForScreenSharing;
+};
+
+} // namespace mozilla
+
+#endif // OSXNotificationCenter_h
diff --git a/widget/cocoa/OSXNotificationCenter.mm b/widget/cocoa/OSXNotificationCenter.mm
new file mode 100644
index 0000000000..07cb026f1f
--- /dev/null
+++ b/widget/cocoa/OSXNotificationCenter.mm
@@ -0,0 +1,591 @@
+/* -*- 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 "OSXNotificationCenter.h"
+#import <AppKit/AppKit.h>
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "nsICancelable.h"
+#include "nsIStringBundle.h"
+#include "nsNetUtil.h"
+#import "nsCocoaUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+
+using namespace mozilla;
+
+#define MAX_NOTIFICATION_NAME_LEN 5000
+
+@protocol FakeNSUserNotification <NSObject>
+@property(copy) NSString* title;
+@property(copy) NSString* subtitle;
+@property(copy) NSString* informativeText;
+@property(copy) NSString* actionButtonTitle;
+@property(copy) NSDictionary* userInfo;
+@property(copy) NSDate* deliveryDate;
+@property(copy) NSTimeZone* deliveryTimeZone;
+@property(copy) NSDateComponents* deliveryRepeatInterval;
+@property(readonly) NSDate* actualDeliveryDate;
+@property(readonly, getter=isPresented) BOOL presented;
+@property(readonly, getter=isRemote) BOOL remote;
+@property(copy) NSString* soundName;
+@property BOOL hasActionButton;
+@property(readonly) NSUserNotificationActivationType activationType;
+@property(copy) NSString* otherButtonTitle;
+@property(copy) NSImage* contentImage;
+@end
+
+@protocol FakeNSUserNotificationCenter <NSObject>
++ (id<FakeNSUserNotificationCenter>)defaultUserNotificationCenter;
+@property(assign) id<NSUserNotificationCenterDelegate> delegate;
+@property(copy) NSArray* scheduledNotifications;
+- (void)scheduleNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeScheduledNotification:(id<FakeNSUserNotification>)notification;
+@property(readonly) NSArray* deliveredNotifications;
+- (void)deliverNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeDeliveredNotification:(id<FakeNSUserNotification>)notification;
+- (void)removeAllDeliveredNotifications;
+- (void)_removeAllDisplayedNotifications;
+- (void)_removeDisplayedNotification:(id<FakeNSUserNotification>)notification;
+@end
+
+@interface mozNotificationCenterDelegate
+ : NSObject <NSUserNotificationCenterDelegate> {
+ OSXNotificationCenter* mOSXNC;
+}
+- (id)initWithOSXNC:(OSXNotificationCenter*)osxnc;
+@end
+
+@implementation mozNotificationCenterDelegate
+
+- (id)initWithOSXNC:(OSXNotificationCenter*)osxnc {
+ [super init];
+ // We should *never* outlive this OSXNotificationCenter.
+ mOSXNC = osxnc;
+ return self;
+}
+
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didDeliverNotification:(id<FakeNSUserNotification>)notification {
+}
+
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didActivateNotification:(id<FakeNSUserNotification>)notification {
+ unsigned long long additionalActionIndex = ULLONG_MAX;
+ if ([notification respondsToSelector:@selector(_alternateActionIndex)]) {
+ NSNumber* alternateActionIndex =
+ [(NSObject*)notification valueForKey:@"_alternateActionIndex"];
+ additionalActionIndex = [alternateActionIndex unsignedLongLongValue];
+ }
+ mOSXNC->OnActivate([[notification userInfo] valueForKey:@"name"],
+ notification.activationType, additionalActionIndex);
+}
+
+- (BOOL)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ shouldPresentNotification:(id<FakeNSUserNotification>)notification {
+ return YES;
+}
+
+// This is an undocumented method that we need for parity with Safari.
+// Apple bug #15440664.
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didRemoveDeliveredNotifications:(NSArray*)notifications {
+ for (id<FakeNSUserNotification> notification in notifications) {
+ NSString* name = [[notification userInfo] valueForKey:@"name"];
+ mOSXNC->CloseAlertCocoaString(name);
+ }
+}
+
+// This is an undocumented method that we need to be notified if a user clicks
+// the close button.
+- (void)userNotificationCenter:(id<FakeNSUserNotificationCenter>)center
+ didDismissAlert:(id<FakeNSUserNotification>)notification {
+ NSString* name = [[notification userInfo] valueForKey:@"name"];
+ mOSXNC->CloseAlertCocoaString(name);
+}
+
+@end
+
+namespace mozilla {
+
+enum {
+ OSXNotificationActionDisable = 0,
+ OSXNotificationActionSettings = 1,
+};
+
+class OSXNotificationInfo final : public nsISupports {
+ private:
+ virtual ~OSXNotificationInfo();
+
+ public:
+ NS_DECL_ISUPPORTS
+ OSXNotificationInfo(NSString* name, nsIObserver* observer,
+ const nsAString& alertCookie);
+
+ NSString* mName;
+ nsCOMPtr<nsIObserver> mObserver;
+ nsString mCookie;
+ RefPtr<nsICancelable> mIconRequest;
+ id<FakeNSUserNotification> mPendingNotification;
+};
+
+NS_IMPL_ISUPPORTS0(OSXNotificationInfo)
+
+OSXNotificationInfo::OSXNotificationInfo(NSString* name, nsIObserver* observer,
+ const nsAString& alertCookie) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ASSERTION(name, "Cannot create OSXNotificationInfo without a name!");
+ mName = [name retain];
+ mObserver = observer;
+ mCookie = alertCookie;
+ mPendingNotification = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+OSXNotificationInfo::~OSXNotificationInfo() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mName release];
+ [mPendingNotification release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static id<FakeNSUserNotificationCenter> GetNotificationCenter() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ Class c = NSClassFromString(@"NSUserNotificationCenter");
+ return [c performSelector:@selector(defaultUserNotificationCenter)];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+OSXNotificationCenter::OSXNotificationCenter() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mDelegate = [[mozNotificationCenterDelegate alloc] initWithOSXNC:this];
+ GetNotificationCenter().delegate = mDelegate;
+ mSuppressForScreenSharing = false;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+OSXNotificationCenter::~OSXNotificationCenter() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [GetNotificationCenter() removeAllDeliveredNotifications];
+ [mDelegate release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, nsIAlertsIconData,
+ nsIAlertsDoNotDisturb, nsIAlertNotificationImageListener)
+
+nsresult OSXNotificationCenter::Init() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return (!!NSClassFromString(@"NSUserNotification")) ? NS_OK
+ : NS_ERROR_FAILURE;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowAlertNotification(
+ const nsAString& aImageUrl, const nsAString& aAlertTitle,
+ const nsAString& aAlertText, bool aAlertTextClickable,
+ const nsAString& aAlertCookie, nsIObserver* aAlertListener,
+ const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
+ const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
+ bool aRequireInteraction) {
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
+ // vibrate is unused for now
+ nsTArray<uint32_t> vibrate;
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText,
+ aAlertTextClickable, aAlertCookie, aBidi, aLang,
+ aData, aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction, false, vibrate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ShowAlert(alert, aAlertListener);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowPersistentNotification(
+ const nsAString& aPersistentData, nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ return ShowAlert(aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener,
+ uint32_t aIconSize,
+ const uint8_t* aIconData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_ARG(aAlert);
+
+ if (mSuppressForScreenSharing) {
+ return NS_OK;
+ }
+
+ Class unClass = NSClassFromString(@"NSUserNotification");
+ id<FakeNSUserNotification> notification = [[unClass alloc] init];
+
+ nsAutoString title;
+ nsresult rv = aAlert->GetTitle(title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ notification.title = nsCocoaUtils::ToNSString(title);
+
+ nsAutoString hostPort;
+ rv = aAlert->GetSource(hostPort);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsCOMPtr<nsIStringBundleService> sbs =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ sbs->CreateBundle("chrome://alerts/locale/alert.properties",
+ getter_AddRefs(bundle));
+
+ if (!hostPort.IsEmpty() && bundle) {
+ AutoTArray<nsString, 1> formatStrings = {hostPort};
+ nsAutoString notificationSource;
+ bundle->FormatStringFromName("source.label", formatStrings,
+ notificationSource);
+ notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
+ }
+
+ nsAutoString text;
+ rv = aAlert->GetText(text);
+ NS_ENSURE_SUCCESS(rv, rv);
+ notification.informativeText = nsCocoaUtils::ToNSString(text);
+
+ bool isSilent;
+ aAlert->GetSilent(&isSilent);
+ notification.soundName = isSilent ? nil : NSUserNotificationDefaultSoundName;
+ notification.hasActionButton = NO;
+
+ // If this is not an application/extension alert, show additional actions
+ // dealing with permissions.
+ bool isActionable;
+ if (bundle && NS_SUCCEEDED(aAlert->GetActionable(&isActionable)) &&
+ isActionable) {
+ nsAutoString closeButtonTitle, actionButtonTitle, disableButtonTitle,
+ settingsButtonTitle;
+ bundle->GetStringFromName("closeButton.title", closeButtonTitle);
+ bundle->GetStringFromName("actionButton.label", actionButtonTitle);
+ if (!hostPort.IsEmpty()) {
+ AutoTArray<nsString, 1> formatStrings = {hostPort};
+ bundle->FormatStringFromName("webActions.disableForOrigin.label",
+ formatStrings, disableButtonTitle);
+ }
+ bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
+
+ notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
+
+ // OS X 10.8 only shows action buttons if the "Alerts" style is set in
+ // Notification Center preferences, and doesn't support the alternate
+ // action menu.
+ if ([notification respondsToSelector:@selector(set_showsButtons:)] &&
+ [notification
+ respondsToSelector:@selector(set_alwaysShowAlternateActionMenu:)] &&
+ [notification
+ respondsToSelector:@selector(set_alternateActionButtonTitles:)]) {
+ notification.hasActionButton = YES;
+ notification.actionButtonTitle =
+ nsCocoaUtils::ToNSString(actionButtonTitle);
+
+ [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
+ [(NSObject*)notification setValue:@(YES)
+ forKey:@"_alwaysShowAlternateActionMenu"];
+ [(NSObject*)notification setValue:@[
+ nsCocoaUtils::ToNSString(disableButtonTitle),
+ nsCocoaUtils::ToNSString(settingsButtonTitle)
+ ]
+ forKey:@"_alternateActionButtonTitles"];
+ }
+ }
+ nsAutoString name;
+ rv = aAlert->GetName(name);
+ // Don't let an alert name be more than MAX_NOTIFICATION_NAME_LEN characters.
+ // More than that shouldn't be necessary and userInfo (assigned to below) has
+ // a length limit of 16k on OS X 10.11. Exception thrown if limit exceeded.
+ if (name.Length() > MAX_NOTIFICATION_NAME_LEN) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ NSString* alertName = nsCocoaUtils::ToNSString(name);
+ if (!alertName) {
+ return NS_ERROR_FAILURE;
+ }
+ notification.userInfo = [NSDictionary
+ dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil]
+ forKeys:[NSArray arrayWithObjects:@"name", nil]];
+
+ nsAutoString cookie;
+ rv = aAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ OSXNotificationInfo* osxni =
+ new OSXNotificationInfo(alertName, aAlertListener, cookie);
+
+ // Show the favicon if supported on this version of OS X.
+ if (aIconSize > 0 &&
+ [notification respondsToSelector:@selector(set_identityImage:)] &&
+ [notification
+ respondsToSelector:@selector(set_identityImageHasBorder:)]) {
+ NSData* iconData = [NSData dataWithBytes:aIconData length:aIconSize];
+ NSImage* icon = [[[NSImage alloc] initWithData:iconData] autorelease];
+
+ [(NSObject*)notification setValue:icon forKey:@"_identityImage"];
+ [(NSObject*)notification setValue:@(NO) forKey:@"_identityImageHasBorder"];
+ }
+
+ bool inPrivateBrowsing;
+ rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Show the notification without waiting for an image if there is no icon URL
+ // or notification icons are not supported on this version of OS X.
+ if (![unClass instancesRespondToSelector:@selector(setContentImage:)]) {
+ CloseAlertCocoaString(alertName);
+ mActiveAlerts.AppendElement(osxni);
+ [GetNotificationCenter() deliverNotification:notification];
+ [notification release];
+ if (aAlertListener) {
+ aAlertListener->Observe(nullptr, "alertshow", cookie.get());
+ }
+ } else {
+ mPendingAlerts.AppendElement(osxni);
+ osxni->mPendingNotification = notification;
+ // Wait six seconds for the image to load.
+ rv = aAlert->LoadImage(6000, this, osxni,
+ getter_AddRefs(osxni->mIconRequest));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ShowPendingNotification(osxni);
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::CloseAlert(const nsAString& aAlertName,
+ bool aContextClosed) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* alertName = nsCocoaUtils::ToNSString(aAlertName);
+ CloseAlertCocoaString(alertName);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void OSXNotificationCenter::CloseAlertCocoaString(NSString* aAlertName) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aAlertName) {
+ return; // Can't do anything without a name
+ }
+
+ NSArray* notifications = [GetNotificationCenter() deliveredNotifications];
+ for (id<FakeNSUserNotification> notification in notifications) {
+ NSString* name = [[notification userInfo] valueForKey:@"name"];
+ if ([name isEqualToString:aAlertName]) {
+ [GetNotificationCenter() removeDeliveredNotification:notification];
+ [GetNotificationCenter() _removeDisplayedNotification:notification];
+ break;
+ }
+ }
+
+ for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
+ OSXNotificationInfo* osxni = mActiveAlerts[i];
+ if ([aAlertName isEqualToString:osxni->mName]) {
+ if (osxni->mObserver) {
+ osxni->mObserver->Observe(nullptr, "alertfinished",
+ osxni->mCookie.get());
+ }
+ if (osxni->mIconRequest) {
+ osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
+ osxni->mIconRequest = nullptr;
+ }
+ mActiveAlerts.RemoveElementAt(i);
+ break;
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void OSXNotificationCenter::OnActivate(
+ NSString* aAlertName, NSUserNotificationActivationType aActivationType,
+ unsigned long long aAdditionalActionIndex) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aAlertName) {
+ return; // Can't do anything without a name
+ }
+
+ for (unsigned int i = 0; i < mActiveAlerts.Length(); i++) {
+ OSXNotificationInfo* osxni = mActiveAlerts[i];
+ if ([aAlertName isEqualToString:osxni->mName]) {
+ if (osxni->mObserver) {
+ switch ((int)aActivationType) {
+ case NSUserNotificationActivationTypeAdditionalActionClicked:
+ case NSUserNotificationActivationTypeActionButtonClicked:
+ switch (aAdditionalActionIndex) {
+ case OSXNotificationActionDisable:
+ osxni->mObserver->Observe(nullptr, "alertdisablecallback",
+ osxni->mCookie.get());
+ break;
+ case OSXNotificationActionSettings:
+ osxni->mObserver->Observe(nullptr, "alertsettingscallback",
+ osxni->mCookie.get());
+ break;
+ default:
+ NS_WARNING(
+ "Unknown NSUserNotification additional action clicked");
+ break;
+ }
+ break;
+ default:
+ osxni->mObserver->Observe(nullptr, "alertclickcallback",
+ osxni->mCookie.get());
+ break;
+ }
+ }
+ return;
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void OSXNotificationCenter::ShowPendingNotification(
+ OSXNotificationInfo* osxni) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (osxni->mIconRequest) {
+ osxni->mIconRequest->Cancel(NS_BINDING_ABORTED);
+ osxni->mIconRequest = nullptr;
+ }
+
+ CloseAlertCocoaString(osxni->mName);
+
+ for (unsigned int i = 0; i < mPendingAlerts.Length(); i++) {
+ if (mPendingAlerts[i] == osxni) {
+ mActiveAlerts.AppendElement(osxni);
+ mPendingAlerts.RemoveElementAt(i);
+ break;
+ }
+ }
+
+ [GetNotificationCenter() deliverNotification:osxni->mPendingNotification];
+
+ if (osxni->mObserver) {
+ osxni->mObserver->Observe(nullptr, "alertshow", osxni->mCookie.get());
+ }
+
+ [osxni->mPendingNotification release];
+ osxni->mPendingNotification = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::OnImageMissing(nsISupports* aUserData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
+ if (osxni->mPendingNotification) {
+ // If there was an error getting the image, or the request timed out, show
+ // the notification without a content image.
+ ShowPendingNotification(osxni);
+ }
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::OnImageReady(nsISupports* aUserData,
+ imgIRequest* aRequest) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCOMPtr<imgIContainer> image;
+ nsresult rv = aRequest->GetImage(getter_AddRefs(image));
+ if (NS_WARN_IF(NS_FAILED(rv) || !image)) {
+ return rv;
+ }
+
+ OSXNotificationInfo* osxni = static_cast<OSXNotificationInfo*>(aUserData);
+ if (!osxni->mPendingNotification) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSImage* cocoaImage = nil;
+ // TODO: Pass pres context / ComputedStyle here to support context paint
+ // properties
+ nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
+ image, imgIContainer::FRAME_FIRST, nullptr, nullptr, &cocoaImage);
+ (osxni->mPendingNotification).contentImage = cocoaImage;
+ [cocoaImage release];
+ ShowPendingNotification(osxni);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// nsIAlertsDoNotDisturb
+NS_IMETHODIMP
+OSXNotificationCenter::GetManualDoNotDisturb(bool* aRetVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::SetManualDoNotDisturb(bool aDoNotDisturb) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::GetSuppressForScreenSharing(bool* aRetVal) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ NS_ENSURE_ARG(aRetVal);
+ *aRetVal = mSuppressForScreenSharing;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+NS_IMETHODIMP
+OSXNotificationCenter::SetSuppressForScreenSharing(bool aSuppress) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ mSuppressForScreenSharing = aSuppress;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+} // namespace mozilla
diff --git a/widget/cocoa/ScreenHelperCocoa.h b/widget/cocoa/ScreenHelperCocoa.h
new file mode 100644
index 0000000000..91f4a19677
--- /dev/null
+++ b/widget/cocoa/ScreenHelperCocoa.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_cocoa_ScreenHelperCocoa_h
+#define mozilla_widget_cocoa_ScreenHelperCocoa_h
+
+#include "mozilla/widget/ScreenManager.h"
+
+@class ScreenHelperDelegate;
+@class NSScreen;
+
+namespace mozilla {
+namespace widget {
+
+class ScreenHelperCocoa final : public ScreenManager::Helper {
+ public:
+ ScreenHelperCocoa();
+ ~ScreenHelperCocoa() override;
+
+ void RefreshScreens();
+
+ static NSScreen* CocoaScreenForScreen(nsIScreen* aScreen);
+
+ private:
+ ScreenHelperDelegate* mDelegate;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_gtk_ScreenHelperGtk_h
diff --git a/widget/cocoa/ScreenHelperCocoa.mm b/widget/cocoa/ScreenHelperCocoa.mm
new file mode 100644
index 0000000000..57e1313320
--- /dev/null
+++ b/widget/cocoa/ScreenHelperCocoa.mm
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ScreenHelperCocoa.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/Logging.h"
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+static LazyLogModule sScreenLog("WidgetScreen");
+
+@interface ScreenHelperDelegate : NSObject {
+ @private
+ mozilla::widget::ScreenHelperCocoa* mHelper;
+}
+
+- (id)initWithScreenHelper:(mozilla::widget::ScreenHelperCocoa*)aScreenHelper;
+- (void)didChangeScreenParameters:(NSNotification*)aNotification;
+@end
+
+@implementation ScreenHelperDelegate
+- (id)initWithScreenHelper:(mozilla::widget::ScreenHelperCocoa*)aScreenHelper {
+ if ((self = [self init])) {
+ mHelper = aScreenHelper;
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(didChangeScreenParameters:)
+ name:NSApplicationDidChangeScreenParametersNotification
+ object:nil];
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)didChangeScreenParameters:(NSNotification*)aNotification {
+ MOZ_LOG(sScreenLog, LogLevel::Debug,
+ ("Received NSApplicationDidChangeScreenParametersNotification"));
+
+ mHelper->RefreshScreens();
+}
+@end
+
+namespace mozilla {
+namespace widget {
+
+ScreenHelperCocoa::ScreenHelperCocoa() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("ScreenHelperCocoa created"));
+
+ mDelegate = [[ScreenHelperDelegate alloc] initWithScreenHelper:this];
+
+ RefreshScreens();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+ScreenHelperCocoa::~ScreenHelperCocoa() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mDelegate release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static already_AddRefed<Screen> MakeScreen(NSScreen* aScreen) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ DesktopToLayoutDeviceScale contentsScaleFactor(
+ nsCocoaUtils::GetBackingScaleFactor(aScreen));
+ CSSToLayoutDeviceScale defaultCssScaleFactor(contentsScaleFactor.scale);
+ NSRect frame = [aScreen frame];
+ LayoutDeviceIntRect rect = nsCocoaUtils::CocoaRectToGeckoRectDevPix(
+ frame, contentsScaleFactor.scale);
+ frame = [aScreen visibleFrame];
+ LayoutDeviceIntRect availRect = nsCocoaUtils::CocoaRectToGeckoRectDevPix(
+ frame, contentsScaleFactor.scale);
+
+ // aScreen may be capable of displaying multiple pixel depths, for example by
+ // transitioning to an HDR-capable depth when required by a window displayed
+ // on the screen. We want to note the maximum capabilities of the screen, so
+ // we use the largest depth it offers.
+ uint32_t pixelDepth = 0;
+ const NSWindowDepth* depths = [aScreen supportedWindowDepths];
+ for (size_t d = 0; NSWindowDepth depth = depths[d]; d++) {
+ uint32_t bpp = NSBitsPerPixelFromDepth(depth);
+ if (bpp > pixelDepth) {
+ pixelDepth = bpp;
+ }
+ }
+
+ // But it confuses content if we return too-high a value here. Cap depth with
+ // a value that matches what Chrome returns for high bpp screens.
+ static const uint32_t MAX_REPORTED_PIXEL_DEPTH = 30;
+ if (pixelDepth > MAX_REPORTED_PIXEL_DEPTH) {
+ pixelDepth = MAX_REPORTED_PIXEL_DEPTH;
+ }
+
+ float dpi = 96.0f;
+ CGDirectDisplayID displayID =
+ [[[aScreen deviceDescription] objectForKey:@"NSScreenNumber"] intValue];
+ CGFloat heightMM = ::CGDisplayScreenSize(displayID).height;
+ if (heightMM > 0) {
+ dpi = rect.height / (heightMM / MM_PER_INCH_FLOAT);
+ }
+ MOZ_LOG(sScreenLog, LogLevel::Debug,
+ ("New screen [%d %d %d %d (%d %d %d %d) %d %f %f %f]", rect.x, rect.y,
+ rect.width, rect.height, availRect.x, availRect.y, availRect.width,
+ availRect.height, pixelDepth, contentsScaleFactor.scale,
+ defaultCssScaleFactor.scale, dpi));
+
+ // Getting the refresh rate is a little hard on OS X. We could use
+ // CVDisplayLinkGetNominalOutputVideoRefreshPeriod, but that's a little
+ // involved. Ideally we could query it from vsync. For now, we leave it out.
+ RefPtr<Screen> screen = new Screen(rect, availRect, pixelDepth, pixelDepth, 0,
+ contentsScaleFactor, defaultCssScaleFactor,
+ dpi, Screen::IsPseudoDisplay::No);
+ return screen.forget();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+void ScreenHelperCocoa::RefreshScreens() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refreshing screens"));
+
+ AutoTArray<RefPtr<Screen>, 4> screens;
+
+ for (NSScreen* screen in [NSScreen screens]) {
+ NSDictionary* desc = [screen deviceDescription];
+ if ([desc objectForKey:NSDeviceIsScreen] == nil) {
+ continue;
+ }
+ screens.AppendElement(MakeScreen(screen));
+ }
+
+ ScreenManager::Refresh(std::move(screens));
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NSScreen* ScreenHelperCocoa::CocoaScreenForScreen(nsIScreen* aScreen) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ for (NSScreen* screen in [NSScreen screens]) {
+ NSDictionary* desc = [screen deviceDescription];
+ if ([desc objectForKey:NSDeviceIsScreen] == nil) {
+ continue;
+ }
+ LayoutDeviceIntRect rect;
+ double scale;
+ aScreen->GetRect(&rect.x, &rect.y, &rect.width, &rect.height);
+ aScreen->GetContentsScaleFactor(&scale);
+ NSRect frame = [screen frame];
+ LayoutDeviceIntRect frameRect =
+ nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, scale);
+ if (rect == frameRect) {
+ return screen;
+ }
+ }
+ return [NSScreen mainScreen];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/cocoa/TextInputHandler.h b/widget/cocoa/TextInputHandler.h
new file mode 100644
index 0000000000..d55a7c32b9
--- /dev/null
+++ b/widget/cocoa/TextInputHandler.h
@@ -0,0 +1,1352 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* 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/. */
+
+#ifndef TextInputHandler_h_
+#define TextInputHandler_h_
+
+#include "nsCocoaUtils.h"
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+#include "mozView.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "WritingModes.h"
+
+class nsChildView;
+
+namespace mozilla {
+namespace widget {
+
+// Key code constants
+enum {
+ kVK_PC_PrintScreen = kVK_F13,
+ kVK_PC_ScrollLock = kVK_F14,
+ kVK_PC_Pause = kVK_F15,
+
+ kVK_PC_Insert = kVK_Help,
+ kVK_PC_Backspace = kVK_Delete,
+ kVK_PC_Delete = kVK_ForwardDelete,
+
+ kVK_PC_ContextMenu = 0x6E,
+
+ kVK_Powerbook_KeypadEnter =
+ 0x34 // Enter on Powerbook's keyboard is different
+};
+
+/**
+ * TISInputSourceWrapper is a wrapper for the TISInputSourceRef. If we get the
+ * TISInputSourceRef from InputSourceID, we need to release the CFArray instance
+ * which is returned by TISCreateInputSourceList. However, when we release the
+ * list, we cannot access the TISInputSourceRef. So, it's not usable, and it
+ * may cause the memory leak bugs. nsTISInputSource automatically releases the
+ * list when the instance is destroyed.
+ */
+class TISInputSourceWrapper {
+ public:
+ static TISInputSourceWrapper& CurrentInputSource();
+ /**
+ * Shutdown() should be called when nobody doesn't need to use this class.
+ */
+ static void Shutdown();
+
+ TISInputSourceWrapper()
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ Clear();
+ }
+
+ explicit TISInputSourceWrapper(const char* aID)
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ InitByInputSourceID(aID);
+ }
+
+ explicit TISInputSourceWrapper(SInt32 aLayoutID)
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ InitByLayoutID(aLayoutID);
+ }
+
+ explicit TISInputSourceWrapper(TISInputSourceRef aInputSource)
+ : mInputSource{nullptr},
+ mKeyboardLayout{nullptr},
+ mUCKeyboardLayout{nullptr},
+ mIsRTL{0},
+ mOverrideKeyboard{false} {
+ mInputSourceList = nullptr;
+ InitByTISInputSourceRef(aInputSource);
+ }
+
+ ~TISInputSourceWrapper() { Clear(); }
+
+ void InitByInputSourceID(const char* aID);
+ void InitByInputSourceID(const nsString& aID);
+ void InitByInputSourceID(const CFStringRef aID);
+ /**
+ * InitByLayoutID() initializes the keyboard layout by the layout ID.
+ *
+ * @param aLayoutID An ID of keyboard layout.
+ * 0: US
+ * 1: Greek
+ * 2: German
+ * 3: Swedish-Pro
+ * 4: Dvorak-Qwerty Cmd
+ * 5: Thai
+ * 6: Arabic
+ * 7: French
+ * 8: Hebrew
+ * 9: Lithuanian
+ * 10: Norwegian
+ * 11: Spanish
+ * @param aOverrideKeyboard When testing set to TRUE, otherwise, set to
+ * FALSE. When TRUE, we use an ANSI keyboard
+ * instead of the actual keyboard.
+ */
+ void InitByLayoutID(SInt32 aLayoutID, bool aOverrideKeyboard = false);
+ void InitByCurrentInputSource();
+ void InitByCurrentKeyboardLayout();
+ void InitByCurrentASCIICapableInputSource();
+ void InitByCurrentASCIICapableKeyboardLayout();
+ void InitByCurrentInputMethodKeyboardLayoutOverride();
+ void InitByTISInputSourceRef(TISInputSourceRef aInputSource);
+ void InitByLanguage(CFStringRef aLanguage);
+
+ /**
+ * If the instance is initialized with a keyboard layout input source,
+ * returns it.
+ * If the instance is initialized with an IME mode input source, the result
+ * references the keyboard layout for the IME mode. However, this can be
+ * initialized only when the IME mode is actually selected. I.e, if IME mode
+ * input source is initialized with LayoutID or SourceID, this returns null.
+ */
+ TISInputSourceRef GetKeyboardLayoutInputSource() const {
+ return mKeyboardLayout;
+ }
+ const UCKeyboardLayout* GetUCKeyboardLayout();
+
+ bool IsOpenedIMEMode();
+ bool IsIMEMode();
+ bool IsKeyboardLayout();
+
+ bool IsASCIICapable() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetBoolProperty(kTISPropertyInputSourceIsASCIICapable);
+ }
+
+ bool IsEnabled() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetBoolProperty(kTISPropertyInputSourceIsEnabled);
+ }
+
+ bool GetLanguageList(CFArrayRef& aLanguageList);
+ bool GetPrimaryLanguage(CFStringRef& aPrimaryLanguage);
+ bool GetPrimaryLanguage(nsAString& aPrimaryLanguage);
+
+ bool GetLocalizedName(CFStringRef& aName) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyLocalizedName, aName);
+ }
+
+ bool GetLocalizedName(nsAString& aName) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyLocalizedName, aName);
+ }
+
+ bool GetInputSourceID(CFStringRef& aID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceID, aID);
+ }
+
+ bool GetInputSourceID(nsAString& aID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceID, aID);
+ }
+
+ bool GetBundleID(CFStringRef& aBundleID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyBundleID, aBundleID);
+ }
+
+ bool GetBundleID(nsAString& aBundleID) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyBundleID, aBundleID);
+ }
+
+ bool GetInputSourceType(CFStringRef& aType) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceType, aType);
+ }
+
+ bool GetInputSourceType(nsAString& aType) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ return GetStringProperty(kTISPropertyInputSourceType, aType);
+ }
+
+ bool IsForRTLLanguage();
+ bool IsForJapaneseLanguage();
+ bool IsInitializedByCurrentInputSource();
+
+ enum {
+ // 40 is an actual result of the ::LMGetKbdType() when we connect an
+ // unknown keyboard and set the keyboard type to ANSI manually on the
+ // set up dialog.
+ eKbdType_ANSI = 40
+ };
+
+ void Select();
+ void Clear();
+
+ /**
+ * InitKeyEvent() initializes aKeyEvent for aNativeKeyEvent.
+ *
+ * @param aNativeKeyEvent A native key event for which you want to
+ * dispatch a Gecko key event.
+ * @param aKeyEvent The result -- a Gecko key event initialized
+ * from the native key event.
+ * @param aIsProcessedByIME true if aNativeKeyEvent has been handled
+ * by IME (but except if the composition was
+ * started with dead key).
+ * @param aInsertString If caller expects that the event will cause
+ * a character to be input (say in an editor),
+ * the caller should set this. Otherwise,
+ * if caller sets null to this, this method will
+ * compute the character to be input from
+ * characters of aNativeKeyEvent.
+ */
+ void InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME,
+ const nsAString* aInsertString = nullptr);
+
+ /**
+ * WillDispatchKeyboardEvent() computes aKeyEvent.mAlternativeCharCodes and
+ * recompute aKeyEvent.mCharCode if it's necessary.
+ *
+ * @param aNativeKeyEvent A native key event for which you want to
+ * dispatch a Gecko key event.
+ * @param aInsertString If caller expects that the event will cause
+ * a character to be input (say in an editor),
+ * the caller should set this. Otherwise,
+ * if caller sets null to this, this method will
+ * compute the character to be input from
+ * characters of aNativeKeyEvent.
+ * @param aIndexOfKeypress Index of the eKeyPress event. If a key
+ * inputs 2 or more characters, eKeyPress events
+ * are dispatched for each character. This is
+ * 0 for the first eKeyPress event.
+ * @param aKeyEvent The result -- a Gecko key event initialized
+ * from the native key event. This must be
+ * eKeyPress event.
+ */
+ void WillDispatchKeyboardEvent(NSEvent* aNativeKeyEvent,
+ const nsAString* aInsertString,
+ uint32_t aIndexOfKeypress,
+ WidgetKeyboardEvent& aKeyEvent);
+
+ /**
+ * ComputeGeckoKeyCode() returns Gecko keycode for aNativeKeyCode on current
+ * keyboard layout.
+ *
+ * @param aNativeKeyCode A native keycode.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @param aCmdIsPressed TRUE if Cmd key is pressed. Otherwise, FALSE.
+ * @return The computed Gecko keycode.
+ */
+ uint32_t ComputeGeckoKeyCode(UInt32 aNativeKeyCode, UInt32 aKbType,
+ bool aCmdIsPressed);
+
+ /**
+ * ComputeGeckoKeyNameIndex() returns Gecko key name index for the key.
+ *
+ * @param aNativeKeyCode A native keycode.
+ */
+ static KeyNameIndex ComputeGeckoKeyNameIndex(UInt32 aNativeKeyCode);
+
+ /**
+ * ComputeGeckoCodeNameIndex() returns Gecko code name index for the key.
+ *
+ * @param aNativeKeyCode A native keycode.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ */
+ static CodeNameIndex ComputeGeckoCodeNameIndex(UInt32 aNativeKeyCode,
+ UInt32 aKbType);
+
+ /**
+ * TranslateToChar() checks if aNativeKeyEvent is a dead key.
+ *
+ * @param aNativeKeyEvent A native key event.
+ * @return Returns true if the key event is a dead key
+ * event. Otherwise, false.
+ */
+ bool IsDeadKey(NSEvent* aNativeKeyEvent);
+
+ protected:
+ /**
+ * TranslateToString() computes the inputted text from the native keyCode,
+ * modifier flags and keyboard type.
+ *
+ * @param aKeyCode A native keyCode.
+ * @param aModifiers Combination of native modifier flags.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @param aStr Result, i.e., inputted text.
+ * The result can be two or more characters.
+ * @return If succeeded, TRUE. Otherwise, FALSE.
+ * Even if TRUE, aStr can be empty string.
+ */
+ bool TranslateToString(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType,
+ nsAString& aStr);
+
+ /**
+ * TranslateToChar() computes the inputted character from the native keyCode,
+ * modifier flags and keyboard type. If two or more characters would be
+ * input, this returns 0.
+ *
+ * @param aKeyCode A native keyCode.
+ * @param aModifiers Combination of native modifier flags.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @return If succeeded and the result is one character,
+ * returns the charCode of it. Otherwise,
+ * returns 0.
+ */
+ uint32_t TranslateToChar(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType);
+
+ /**
+ * TranslateToChar() checks if aKeyCode with aModifiers is a dead key.
+ *
+ * @param aKeyCode A native keyCode.
+ * @param aModifiers Combination of native modifier flags.
+ * @param aKbType A native Keyboard Type value. Typically,
+ * this is a result of ::LMGetKbdType().
+ * @return Returns true if the key with specified
+ * modifier state is a dead key. Otherwise,
+ * false.
+ */
+ bool IsDeadKey(UInt32 aKeyCode, UInt32 aModifiers, UInt32 aKbType);
+
+ /**
+ * ComputeInsertString() computes string to be inserted with the key event.
+ *
+ * @param aNativeKeyEvent The native key event which causes our keyboard
+ * event(s).
+ * @param aKeyEvent A Gecko key event which was partially
+ * initialized with aNativeKeyEvent.
+ * @param aInsertString The string to be inputting by aNativeKeyEvent.
+ * This should be specified by InsertText().
+ * In other words, if the key event doesn't cause
+ * a call of InsertText(), this can be nullptr.
+ * @param aResult The string which should be set to charCode of
+ * keypress event(s).
+ */
+ void ComputeInsertStringForCharCode(NSEvent* aNativeKeyEvent,
+ const WidgetKeyboardEvent& aKeyEvent,
+ const nsAString* aInsertString,
+ nsAString& aResult);
+
+ /**
+ * IsPrintableKeyEvent() returns true if aNativeKeyEvent is caused by
+ * a printable key. Otherwise, returns false.
+ */
+ bool IsPrintableKeyEvent(NSEvent* aNativeKeyEvent) const;
+
+ /**
+ * GetKbdType() returns physical keyboard type.
+ */
+ UInt32 GetKbdType() const;
+
+ bool GetBoolProperty(const CFStringRef aKey);
+ bool GetStringProperty(const CFStringRef aKey, CFStringRef& aStr);
+ bool GetStringProperty(const CFStringRef aKey, nsAString& aStr);
+
+ TISInputSourceRef mInputSource;
+ TISInputSourceRef mKeyboardLayout;
+ CFArrayRef mInputSourceList;
+ const UCKeyboardLayout* mUCKeyboardLayout;
+ int8_t mIsRTL;
+
+ bool mOverrideKeyboard;
+
+ static TISInputSourceWrapper* sCurrentInputSource;
+};
+
+/**
+ * TextInputHandlerBase is a base class of IMEInputHandler and TextInputHandler.
+ * Utility methods should be implemented this level.
+ */
+
+class TextInputHandlerBase : public TextEventDispatcherListener {
+ public:
+ /**
+ * Other TextEventDispatcherListener methods should be implemented in
+ * IMEInputHandler.
+ */
+ NS_DECL_ISUPPORTS
+
+ /**
+ * DispatchEvent() dispatches aEvent on mWidget.
+ *
+ * @param aEvent An event which you want to dispatch.
+ * @return TRUE if the event is consumed by web contents
+ * or chrome contents. Otherwise, FALSE.
+ */
+ bool DispatchEvent(WidgetGUIEvent& aEvent);
+
+ /**
+ * SetSelection() dispatches eSetSelection event for the aRange.
+ *
+ * @param aRange The range which will be selected.
+ * @return TRUE if setting selection is succeeded and
+ * the widget hasn't been destroyed.
+ * Otherwise, FALSE.
+ */
+ bool SetSelection(NSRange& aRange);
+
+ /**
+ * InitKeyEvent() initializes aKeyEvent for aNativeKeyEvent.
+ *
+ * @param aNativeKeyEvent A native key event for which you want to
+ * dispatch a Gecko key event.
+ * @param aKeyEvent The result -- a Gecko key event initialized
+ * from the native key event.
+ * @param aIsProcessedByIME true if aNativeKeyEvent has been handled
+ * by IME (but except if the composition was
+ * started with dead key).
+ * @param aInsertString If caller expects that the event will cause
+ * a character to be input (say in an editor),
+ * the caller should set this. Otherwise,
+ * if caller sets null to this, this method will
+ * compute the character to be input from
+ * characters of aNativeKeyEvent.
+ */
+ void InitKeyEvent(NSEvent* aNativeKeyEvent, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME,
+ const nsAString* aInsertString = nullptr);
+
+ /**
+ * SynthesizeNativeKeyEvent() is an implementation of
+ * nsIWidget::SynthesizeNativeKeyEvent(). See the document in nsIWidget.h
+ * for the detail.
+ */
+ nsresult SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout,
+ int32_t aNativeKeyCode,
+ uint32_t aModifierFlags,
+ const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters);
+
+ /**
+ * Utility method intended for testing. Attempts to construct a native key
+ * event that would have been generated during an actual key press. This
+ * *does not dispatch* the native event. Instead, it is attached to the
+ * |mNativeKeyEvent| field of the Gecko event that is passed in.
+ * @param aKeyEvent Gecko key event to attach the native event to
+ */
+ NS_IMETHOD AttachNativeKeyEvent(WidgetKeyboardEvent& aKeyEvent);
+
+ /**
+ * GetWindowLevel() returns the window level of current focused (in Gecko)
+ * window. E.g., if an <input> element in XUL panel has focus, this returns
+ * the XUL panel's window level.
+ */
+ NSInteger GetWindowLevel();
+
+ /**
+ * IsSpecialGeckoKey() checks whether aNativeKeyCode is mapped to a special
+ * Gecko keyCode. A key is "special" if it isn't used for text input.
+ *
+ * @param aNativeKeyCode A native keycode.
+ * @return If the keycode is mapped to a special key,
+ * TRUE. Otherwise, FALSE.
+ */
+ static bool IsSpecialGeckoKey(UInt32 aNativeKeyCode);
+
+ /**
+ * EnableSecureEventInput() and DisableSecureEventInput() wrap the Carbon
+ * Event Manager APIs with the same names. In addition they keep track of
+ * how many times we've called them (in the same process) -- unlike the
+ * Carbon Event Manager APIs, which only keep track of how many times they've
+ * been called from any and all processes.
+ *
+ * The Carbon Event Manager's IsSecureEventInputEnabled() returns whether
+ * secure event input mode is enabled (in any process). This class's
+ * IsSecureEventInputEnabled() returns whether we've made any calls to
+ * EnableSecureEventInput() that are not (yet) offset by the calls we've
+ * made to DisableSecureEventInput().
+ */
+ static void EnableSecureEventInput();
+ static void DisableSecureEventInput();
+ static bool IsSecureEventInputEnabled();
+
+ /**
+ * EnsureSecureEventInputDisabled() calls DisableSecureEventInput() until
+ * our call count becomes 0.
+ */
+ static void EnsureSecureEventInputDisabled();
+
+ public:
+ /**
+ * mWidget must not be destroyed without OnDestroyWidget being called.
+ *
+ * @param aDestroyingWidget Destroying widget. This might not be mWidget.
+ * @return This result doesn't have any meaning for
+ * callers. When aDstroyingWidget isn't the same
+ * as mWidget, FALSE. Then, inherited methods in
+ * sub classes should return from this method
+ * without cleaning up.
+ */
+ virtual bool OnDestroyWidget(nsChildView* aDestroyingWidget);
+
+ protected:
+ // The creator of this instance, client and its text event dispatcher.
+ // These members must not be nullptr after initialized until
+ // OnDestroyWidget() is called.
+ nsChildView* mWidget; // [WEAK]
+ RefPtr<TextEventDispatcher> mDispatcher;
+
+ // The native view for mWidget.
+ // This view handles the actual text inputting.
+ NSView<mozView>* mView; // [STRONG]
+
+ TextInputHandlerBase(nsChildView* aWidget, NSView<mozView>* aNativeView);
+ virtual ~TextInputHandlerBase();
+
+ bool Destroyed() { return !mWidget; }
+
+ /**
+ * mCurrentKeyEvent indicates what key event we are handling. While
+ * handling a native keydown event, we need to store the event for insertText,
+ * doCommandBySelector and various action message handlers of NSResponder
+ * such as [NSResponder insertNewline:sender].
+ */
+ struct KeyEventState {
+ // Handling native key event
+ NSEvent* mKeyEvent;
+ // String specified by InsertText(). This is not null only during a
+ // call of InsertText().
+ nsAString* mInsertString;
+ // String which are included in [mKeyEvent characters] and already handled
+ // by InsertText() call(s).
+ nsString mInsertedString;
+ // Unique id associated with a keydown / keypress event. It's ok if this
+ // wraps over long periods.
+ uint32_t mUniqueId;
+ // Whether keydown event was dispatched for mKeyEvent.
+ bool mKeyDownDispatched;
+ // Whether keydown event was consumed by web contents or chrome contents.
+ bool mKeyDownHandled;
+ // Whether keypress event was dispatched for mKeyEvent.
+ bool mKeyPressDispatched;
+ // Whether keypress event was consumed by web contents or chrome contents.
+ bool mKeyPressHandled;
+ // Whether the key event causes other key events via IME or something.
+ bool mCausedOtherKeyEvents;
+ // Whether the key event causes composition change or committing
+ // composition. So, even if InsertText() is called, this may be false
+ // if it dispatches keypress event.
+ bool mCompositionDispatched;
+
+ KeyEventState() : mKeyEvent(nullptr), mUniqueId(0) { Clear(); }
+
+ explicit KeyEventState(NSEvent* aNativeKeyEvent, uint32_t aUniqueId = 0)
+ : mKeyEvent(nullptr), mUniqueId(0) {
+ Clear();
+ Set(aNativeKeyEvent, aUniqueId);
+ }
+
+ KeyEventState(const KeyEventState& aOther) = delete;
+
+ ~KeyEventState() { Clear(); }
+
+ void Set(NSEvent* aNativeKeyEvent, uint32_t aUniqueId = 0) {
+ MOZ_ASSERT(aNativeKeyEvent, "aNativeKeyEvent must not be NULL");
+ Clear();
+ mKeyEvent = [aNativeKeyEvent retain];
+ mUniqueId = aUniqueId;
+ }
+
+ void Clear() {
+ if (mKeyEvent) {
+ [mKeyEvent release];
+ mKeyEvent = nullptr;
+ mUniqueId = 0;
+ }
+ mInsertString = nullptr;
+ mInsertedString.Truncate();
+ mKeyDownDispatched = false;
+ mKeyDownHandled = false;
+ mKeyPressDispatched = false;
+ mKeyPressHandled = false;
+ mCausedOtherKeyEvents = false;
+ mCompositionDispatched = false;
+ }
+
+ bool IsDefaultPrevented() const {
+ return mKeyDownHandled || mKeyPressHandled || mCausedOtherKeyEvents ||
+ mCompositionDispatched;
+ }
+
+ bool CanDispatchKeyDownEvent() const { return !mKeyDownDispatched; }
+
+ bool CanDispatchKeyPressEvent() const {
+ return !mKeyPressDispatched && !IsDefaultPrevented();
+ }
+
+ bool CanHandleCommand() const {
+ return !mKeyDownHandled && !mKeyPressHandled;
+ }
+
+ bool IsProperKeyEvent(Command aCommand) const {
+ if (NS_WARN_IF(!mKeyEvent)) {
+ return false;
+ }
+ KeyNameIndex keyNameIndex =
+ TISInputSourceWrapper::ComputeGeckoKeyNameIndex([mKeyEvent keyCode]);
+ Modifiers modifiers =
+ nsCocoaUtils::ModifiersForEvent(mKeyEvent) &
+ (MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ return keyNameIndex == KEY_NAME_INDEX_Enter &&
+ modifiers == MODIFIER_CONTROL;
+ case Command::InsertParagraph:
+ return keyNameIndex == KEY_NAME_INDEX_Enter &&
+ modifiers == MODIFIER_NONE;
+ case Command::DeleteCharBackward:
+ return keyNameIndex == KEY_NAME_INDEX_Backspace &&
+ modifiers == MODIFIER_NONE;
+ case Command::DeleteToBeginningOfLine:
+ return keyNameIndex == KEY_NAME_INDEX_Backspace &&
+ modifiers == MODIFIER_META;
+ case Command::DeleteWordBackward:
+ return keyNameIndex == KEY_NAME_INDEX_Backspace &&
+ modifiers == MODIFIER_ALT;
+ case Command::DeleteCharForward:
+ return keyNameIndex == KEY_NAME_INDEX_Delete &&
+ modifiers == MODIFIER_NONE;
+ case Command::DeleteWordForward:
+ return keyNameIndex == KEY_NAME_INDEX_Delete &&
+ modifiers == MODIFIER_ALT;
+ case Command::InsertTab:
+ return keyNameIndex == KEY_NAME_INDEX_Tab &&
+ modifiers == MODIFIER_NONE;
+ case Command::InsertBacktab:
+ return keyNameIndex == KEY_NAME_INDEX_Tab &&
+ modifiers == MODIFIER_SHIFT;
+ case Command::CharNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == MODIFIER_NONE;
+ case Command::SelectCharNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == MODIFIER_SHIFT;
+ case Command::WordNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == MODIFIER_ALT;
+ case Command::SelectWordNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == (MODIFIER_ALT | MODIFIER_SHIFT);
+ case Command::EndLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == MODIFIER_META;
+ case Command::SelectEndLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowRight &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT);
+ case Command::CharPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == MODIFIER_NONE;
+ case Command::SelectCharPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == MODIFIER_SHIFT;
+ case Command::WordPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == MODIFIER_ALT;
+ case Command::SelectWordPrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == (MODIFIER_ALT | MODIFIER_SHIFT);
+ case Command::BeginLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == MODIFIER_META;
+ case Command::SelectBeginLine:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowLeft &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT);
+ case Command::LinePrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowUp &&
+ modifiers == MODIFIER_NONE;
+ case Command::SelectLinePrevious:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowUp &&
+ modifiers == MODIFIER_SHIFT;
+ case Command::MoveTop:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowUp &&
+ modifiers == MODIFIER_META;
+ case Command::SelectTop:
+ return (keyNameIndex == KEY_NAME_INDEX_ArrowUp &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT)) ||
+ (keyNameIndex == KEY_NAME_INDEX_Home &&
+ modifiers == MODIFIER_SHIFT);
+ case Command::LineNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowDown &&
+ modifiers == MODIFIER_NONE;
+ case Command::SelectLineNext:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowDown &&
+ modifiers == MODIFIER_SHIFT;
+ case Command::MoveBottom:
+ return keyNameIndex == KEY_NAME_INDEX_ArrowDown &&
+ modifiers == MODIFIER_META;
+ case Command::SelectBottom:
+ return (keyNameIndex == KEY_NAME_INDEX_ArrowDown &&
+ modifiers == (MODIFIER_META | MODIFIER_SHIFT)) ||
+ (keyNameIndex == KEY_NAME_INDEX_End &&
+ modifiers == MODIFIER_SHIFT);
+ case Command::ScrollPageUp:
+ return keyNameIndex == KEY_NAME_INDEX_PageUp &&
+ modifiers == MODIFIER_NONE;
+ case Command::SelectPageUp:
+ return keyNameIndex == KEY_NAME_INDEX_PageUp &&
+ modifiers == MODIFIER_SHIFT;
+ case Command::ScrollPageDown:
+ return keyNameIndex == KEY_NAME_INDEX_PageDown &&
+ modifiers == MODIFIER_NONE;
+ case Command::SelectPageDown:
+ return keyNameIndex == KEY_NAME_INDEX_PageDown &&
+ modifiers == MODIFIER_SHIFT;
+ case Command::ScrollBottom:
+ return keyNameIndex == KEY_NAME_INDEX_End &&
+ modifiers == MODIFIER_NONE;
+ case Command::ScrollTop:
+ return keyNameIndex == KEY_NAME_INDEX_Home &&
+ modifiers == MODIFIER_NONE;
+ case Command::CancelOperation:
+ return (keyNameIndex == KEY_NAME_INDEX_Escape &&
+ (modifiers == MODIFIER_NONE ||
+ modifiers == MODIFIER_SHIFT)) ||
+ ([mKeyEvent keyCode] == kVK_ANSI_Period &&
+ modifiers == MODIFIER_META);
+ case Command::Complete:
+ return keyNameIndex == KEY_NAME_INDEX_Escape &&
+ (modifiers == MODIFIER_ALT ||
+ modifiers == (MODIFIER_ALT | MODIFIER_SHIFT));
+ default:
+ return false;
+ }
+ }
+
+ void InitKeyEvent(TextInputHandlerBase* aHandler,
+ WidgetKeyboardEvent& aKeyEvent, bool aIsProcessedByIME);
+
+ /**
+ * GetUnhandledString() returns characters of the event which have not been
+ * handled with InsertText() yet. For example, if there is a composition
+ * caused by a dead key press like '`' and it's committed by some key
+ * combinations like |Cmd+v|, then, the |v|'s KeyDown event's |characters|
+ * is |`v|. Then, after |`| is committed with a call of InsertString(),
+ * this returns only 'v'.
+ */
+ void GetUnhandledString(nsAString& aUnhandledString) const;
+ };
+
+ /**
+ * Helper classes for guaranteeing cleaning mCurrentKeyEvent
+ */
+ class AutoKeyEventStateCleaner {
+ public:
+ explicit AutoKeyEventStateCleaner(TextInputHandlerBase* aHandler)
+ : mHandler(aHandler) {}
+
+ ~AutoKeyEventStateCleaner() { mHandler->RemoveCurrentKeyEvent(); }
+
+ private:
+ RefPtr<TextInputHandlerBase> mHandler;
+ };
+
+ class MOZ_STACK_CLASS AutoInsertStringClearer {
+ public:
+ explicit AutoInsertStringClearer(KeyEventState* aState) : mState(aState) {}
+ ~AutoInsertStringClearer();
+
+ private:
+ KeyEventState* mState;
+ };
+
+ /**
+ * mCurrentKeyEvents stores all key events which are being processed.
+ * When we call interpretKeyEvents, IME may generate other key events.
+ * mCurrentKeyEvents[0] is the latest key event.
+ */
+ nsTArray<KeyEventState*> mCurrentKeyEvents;
+
+ /**
+ * mFirstKeyEvent must be used for first key event. This member prevents
+ * memory fragmentation for most key events.
+ */
+ KeyEventState mFirstKeyEvent;
+
+ /**
+ * PushKeyEvent() adds the current key event to mCurrentKeyEvents.
+ */
+ KeyEventState* PushKeyEvent(NSEvent* aNativeKeyEvent,
+ uint32_t aUniqueId = 0) {
+ uint32_t nestCount = mCurrentKeyEvents.Length();
+ for (uint32_t i = 0; i < nestCount; i++) {
+ // When the key event is caused by another key event, all key events
+ // which are being handled should be marked as "consumed".
+ mCurrentKeyEvents[i]->mCausedOtherKeyEvents = true;
+ }
+
+ KeyEventState* keyEvent = nullptr;
+ if (nestCount == 0) {
+ mFirstKeyEvent.Set(aNativeKeyEvent, aUniqueId);
+ keyEvent = &mFirstKeyEvent;
+ } else {
+ keyEvent = new KeyEventState(aNativeKeyEvent, aUniqueId);
+ }
+ return *mCurrentKeyEvents.AppendElement(keyEvent);
+ }
+
+ /**
+ * RemoveCurrentKeyEvent() removes the current key event from
+ * mCurrentKeyEvents.
+ */
+ void RemoveCurrentKeyEvent() {
+ NS_ASSERTION(mCurrentKeyEvents.Length() > 0,
+ "RemoveCurrentKeyEvent() is called unexpectedly");
+ KeyEventState* keyEvent = mCurrentKeyEvents.PopLastElement();
+ if (keyEvent == &mFirstKeyEvent) {
+ keyEvent->Clear();
+ } else {
+ delete keyEvent;
+ }
+ }
+
+ /**
+ * GetCurrentKeyEvent() returns current processing key event.
+ */
+ KeyEventState* GetCurrentKeyEvent() {
+ if (mCurrentKeyEvents.Length() == 0) {
+ return nullptr;
+ }
+ return mCurrentKeyEvents[mCurrentKeyEvents.Length() - 1];
+ }
+
+ struct KeyboardLayoutOverride final {
+ int32_t mKeyboardLayout;
+ bool mOverrideEnabled;
+
+ KeyboardLayoutOverride() : mKeyboardLayout(0), mOverrideEnabled(false) {}
+ };
+
+ const KeyboardLayoutOverride& KeyboardLayoutOverrideRef() const {
+ return mKeyboardOverride;
+ }
+
+ /**
+ * IsPrintableChar() checks whether the unicode character is
+ * a non-printable ASCII character or not. Note that this returns
+ * TRUE even if aChar is a non-printable UNICODE character.
+ *
+ * @param aChar A unicode character.
+ * @return TRUE if aChar is a printable ASCII character
+ * or a unicode character. Otherwise, i.e,
+ * if aChar is a non-printable ASCII character,
+ * FALSE.
+ */
+ static bool IsPrintableChar(char16_t aChar);
+
+ /**
+ * IsNormalCharInputtingEvent() checks whether aNativeEvent causes text input.
+ *
+ * @param aNativeEvent A key event.
+ * @return TRUE if the key event causes text input.
+ * Otherwise, FALSE.
+ */
+ static bool IsNormalCharInputtingEvent(NSEvent* aNativeEvent);
+
+ /**
+ * IsModifierKey() checks whether the native keyCode is for a modifier key.
+ *
+ * @param aNativeKeyCode A native keyCode.
+ * @return TRUE if aNativeKeyCode is for a modifier key.
+ * Otherwise, FALSE.
+ */
+ static bool IsModifierKey(UInt32 aNativeKeyCode);
+
+ private:
+ KeyboardLayoutOverride mKeyboardOverride;
+
+ static int32_t sSecureEventInputCount;
+};
+
+/**
+ * IMEInputHandler manages:
+ * 1. The IME/keyboard layout statement of nsChildView.
+ * 2. The IME composition statement of nsChildView.
+ * And also provides the methods which controls the current IME transaction of
+ * the instance.
+ *
+ * Note that an nsChildView handles one or more NSView's events. E.g., even if
+ * a text editor on XUL panel element, the input events handled on the parent
+ * (or its ancestor) widget handles it (the native focus is set to it). The
+ * actual focused view is notified by OnFocusChangeInGecko.
+ */
+
+class IMEInputHandler : public TextInputHandlerBase {
+ public:
+ // TextEventDispatcherListener methods
+ NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) override;
+ NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
+ NS_IMETHOD_(void)
+ OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;
+ NS_IMETHOD_(void)
+ WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) override;
+
+ public:
+ virtual bool OnDestroyWidget(nsChildView* aDestroyingWidget) override;
+
+ virtual void OnFocusChangeInGecko(bool aFocus);
+
+ void OnSelectionChange(const IMENotification& aIMENotification);
+ void OnLayoutChange();
+
+ /**
+ * Call [NSTextInputContext handleEvent] for mouse event support of IME
+ */
+ bool OnHandleEvent(NSEvent* aEvent);
+
+ /**
+ * SetMarkedText() is a handler of setMarkedText of NSTextInput.
+ *
+ * @param aAttrString This mut be an instance of NSAttributedString.
+ * If the aString parameter to
+ * [ChildView setMarkedText:setSelectedRange:]
+ * isn't an instance of NSAttributedString,
+ * create an NSAttributedString from it and pass
+ * that instead.
+ * @param aSelectedRange Current selected range (or caret position).
+ * @param aReplacementRange The range which will be replaced with the
+ * aAttrString instead of current marked range.
+ */
+ void SetMarkedText(NSAttributedString* aAttrString, NSRange& aSelectedRange,
+ NSRange* aReplacementRange = nullptr);
+
+ /**
+ * GetAttributedSubstringFromRange() returns an NSAttributedString instance
+ * which is allocated as autorelease for aRange.
+ *
+ * @param aRange The range of string which you want.
+ * @param aActualRange The actual range of the result.
+ * @return The string in aRange. If the string is empty,
+ * this returns nil. If succeeded, this returns
+ * an instance which is allocated as autorelease.
+ * If this has some troubles, returns nil.
+ */
+ NSAttributedString* GetAttributedSubstringFromRange(
+ NSRange& aRange, NSRange* aActualRange = nullptr);
+
+ /**
+ * SelectedRange() returns current selected range.
+ *
+ * @return If an editor has focus, this returns selection
+ * range in the editor. Otherwise, this returns
+ * selection range in the focused document.
+ */
+ NSRange SelectedRange();
+
+ /**
+ * DrawsVerticallyForCharacterAtIndex() returns whether the character at
+ * the given index is being rendered vertically.
+ *
+ * @param aCharIndex The character offset to query.
+ *
+ * @return True if writing-mode is vertical at the given
+ * character offset; otherwise false.
+ */
+ bool DrawsVerticallyForCharacterAtIndex(uint32_t aCharIndex);
+
+ /**
+ * FirstRectForCharacterRange() returns first *character* rect in the range.
+ * Cocoa needs the first line rect in the range, but we cannot compute it
+ * on current implementation.
+ *
+ * @param aRange A range of text to examine. Its position is
+ * an offset from the beginning of the focused
+ * editor or document.
+ * @param aActualRange If this is not null, this returns the actual
+ * range used for computing the result.
+ * @return An NSRect containing the first character in
+ * aRange, in screen coordinates.
+ * If the length of aRange is 0, the width will
+ * be 0.
+ */
+ NSRect FirstRectForCharacterRange(NSRange& aRange,
+ NSRange* aActualRange = nullptr);
+
+ /**
+ * CharacterIndexForPoint() returns an offset of a character at aPoint.
+ * XXX This isn't implemented, always returns 0.
+ *
+ * @param The point in screen coordinates.
+ * @return The offset of the character at aPoint from
+ * the beginning of the focused editor or
+ * document.
+ */
+ NSUInteger CharacterIndexForPoint(NSPoint& aPoint);
+
+ /**
+ * GetValidAttributesForMarkedText() returns attributes which we support.
+ *
+ * @return Always empty array for now.
+ */
+ NSArray* GetValidAttributesForMarkedText();
+
+ bool HasMarkedText();
+ NSRange MarkedRange();
+
+ bool IsIMEComposing() { return mIsIMEComposing; }
+ bool IsDeadKeyComposing() { return mIsDeadKeyComposing; }
+ bool IsIMEOpened();
+ bool IsIMEEnabled() { return mIsIMEEnabled; }
+ bool IsASCIICapableOnly() { return mIsASCIICapableOnly; }
+ bool IsEditableContent() const {
+ return mIsIMEEnabled || mIsASCIICapableOnly;
+ }
+ bool IgnoreIMECommit() { return mIgnoreIMECommit; }
+
+ void CommitIMEComposition();
+ void CancelIMEComposition();
+
+ void EnableIME(bool aEnableIME);
+ void SetIMEOpenState(bool aOpen);
+ void SetASCIICapableOnly(bool aASCIICapableOnly);
+
+ /**
+ * True if OSX believes that our view has keyboard focus.
+ */
+ bool IsFocused();
+
+ static CFArrayRef CreateAllIMEModeList();
+ static void DebugPrintAllIMEModes();
+
+ // Don't use ::TSMGetActiveDocument() API directly, the document may not
+ // be what you want.
+ static TSMDocumentID GetCurrentTSMDocumentID();
+
+ protected:
+ // We cannot do some jobs in the given stack by some reasons.
+ // Following flags and the timer provide the execution pending mechanism,
+ // See the comment in nsCocoaTextInputHandler.mm.
+ nsCOMPtr<nsITimer> mTimer;
+ enum { kNotifyIMEOfFocusChangeInGecko = 1, kSyncASCIICapableOnly = 2 };
+ uint32_t mPendingMethods;
+
+ IMEInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView);
+ virtual ~IMEInputHandler();
+
+ void ResetTimer();
+
+ virtual void ExecutePendingMethods();
+
+ /**
+ * InsertTextAsCommittingComposition() commits current composition. If there
+ * is no composition, this starts a composition and commits it immediately.
+ *
+ * @param aString A string which is committed.
+ * @param aReplacementRange The range which will be replaced with the
+ * aAttrString instead of current selection.
+ */
+ void InsertTextAsCommittingComposition(NSString* aString,
+ NSRange* aReplacementRange);
+
+ /**
+ * MaybeDispatchCurrentKeydownEvent() dispatches eKeyDown event for current
+ * key event. If eKeyDown for current key event has already been dispatched,
+ * this does nothing.
+ *
+ * @param aIsProcessedByIME true if current key event is handled by IME.
+ * @return true if the caller can continue to handle
+ * current key event. Otherwise, false. E.g.,
+ * focus is moved, the widget has been destroyed
+ * or something.
+ */
+ bool MaybeDispatchCurrentKeydownEvent(bool aIsProcessedByIME);
+
+ private:
+ // If mIsIMEComposing is true, the composition string is stored here.
+ NSString* mIMECompositionString;
+ // If mIsIMEComposing is true, the start offset of the composition string.
+ uint32_t mIMECompositionStart;
+
+ NSRange mMarkedRange;
+ NSRange mSelectedRange;
+
+ NSRange mRangeForWritingMode; // range within which mWritingMode applies
+ mozilla::WritingMode mWritingMode;
+
+ bool mIsIMEComposing;
+ // If the composition started with dead key, mIsDeadKeyComposing is set to
+ // true.
+ bool mIsDeadKeyComposing;
+ bool mIsIMEEnabled;
+ bool mIsASCIICapableOnly;
+ bool mIgnoreIMECommit;
+ bool mIMEHasFocus;
+
+ void KillIMEComposition();
+ void SendCommittedText(NSString* aString);
+ void OpenSystemPreferredLanguageIME();
+
+ // Pending methods
+ void NotifyIMEOfFocusChangeInGecko();
+ void SyncASCIICapableOnly();
+
+ static bool sStaticMembersInitialized;
+ static CFStringRef sLatestIMEOpenedModeInputSourceID;
+ static void InitStaticMembers();
+ static void OnCurrentTextInputSourceChange(CFNotificationCenterRef aCenter,
+ void* aObserver, CFStringRef aName,
+ const void* aObject,
+ CFDictionaryRef aUserInfo);
+
+ static void FlushPendingMethods(nsITimer* aTimer, void* aClosure);
+
+ /**
+ * ConvertToTextRangeStyle converts the given native underline style to
+ * our defined text range type.
+ *
+ * @param aUnderlineStyle NSUnderlineStyleSingle or
+ * NSUnderlineStyleThick.
+ * @param aSelectedRange Current selected range (or caret position).
+ * @return NS_TEXTRANGE_*.
+ */
+ TextRangeType ConvertToTextRangeType(uint32_t aUnderlineStyle,
+ NSRange& aSelectedRange);
+
+ /**
+ * GetRangeCount() computes the range count of aAttrString.
+ *
+ * @param aAttrString An NSAttributedString instance whose number of
+ * NSUnderlineStyleAttributeName ranges you with
+ * to know.
+ * @return The count of NSUnderlineStyleAttributeName
+ * ranges in aAttrString.
+ */
+ uint32_t GetRangeCount(NSAttributedString* aString);
+
+ /**
+ * CreateTextRangeArray() returns text ranges for clauses and/or caret.
+ *
+ * @param aAttrString An NSAttributedString instance which indicates
+ * current composition string.
+ * @param aSelectedRange Current selected range (or caret position).
+ * @return The result is set to the
+ * NSUnderlineStyleAttributeName ranges in
+ * aAttrString.
+ */
+ already_AddRefed<mozilla::TextRangeArray> CreateTextRangeArray(
+ NSAttributedString* aAttrString, NSRange& aSelectedRange);
+
+ /**
+ * DispatchCompositionStartEvent() dispatches a compositionstart event and
+ * initializes the members indicating composition state.
+ *
+ * @return true if it can continues handling composition.
+ * Otherwise, e.g., canceled by the web page,
+ * this returns false.
+ */
+ bool DispatchCompositionStartEvent();
+
+ /**
+ * DispatchCompositionChangeEvent() dispatches a compositionchange event on
+ * mWidget and modifies the members indicating composition state.
+ *
+ * @param aText User text input.
+ * @param aAttrString An NSAttributedString instance which indicates
+ * current composition string.
+ * @param aSelectedRange Current selected range (or caret position).
+ *
+ * @return true if it can continues handling composition.
+ * Otherwise, e.g., canceled by the web page,
+ * this returns false.
+ */
+ bool DispatchCompositionChangeEvent(const nsString& aText,
+ NSAttributedString* aAttrString,
+ NSRange& aSelectedRange);
+
+ /**
+ * DispatchCompositionCommitEvent() dispatches a compositioncommit event or
+ * compositioncommitasis event. If aCommitString is null, dispatches
+ * compositioncommitasis event. I.e., if aCommitString is null, this
+ * commits the composition with the last data. Otherwise, commits the
+ * composition with aCommitString value.
+ *
+ * @return true if the widget isn't destroyed.
+ * Otherwise, false.
+ */
+ bool DispatchCompositionCommitEvent(const nsAString* aCommitString = nullptr);
+
+ // The focused IME handler. Please note that the handler might lost the
+ // actual focus by deactivating the application. If we are active, this
+ // must have the actual focused handle.
+ // We cannot access to the NSInputManager during we aren't active, so, the
+ // focused handler can have an IME transaction even if we are deactive.
+ static IMEInputHandler* sFocusedIMEHandler;
+
+ static bool sCachedIsForRTLLangage;
+};
+
+/**
+ * TextInputHandler implements the NSTextInput protocol.
+ */
+class TextInputHandler : public IMEInputHandler {
+ public:
+ static NSUInteger sLastModifierState;
+
+ static CFArrayRef CreateAllKeyboardLayoutList();
+ static void DebugPrintAllKeyboardLayouts();
+
+ TextInputHandler(nsChildView* aWidget, NSView<mozView>* aNativeView);
+ virtual ~TextInputHandler();
+
+ /**
+ * KeyDown event handler.
+ *
+ * @param aNativeEvent A native NSEventTypeKeyDown event.
+ * @param aUniqueId A unique ID for the event.
+ * @return TRUE if the event is dispatched to web
+ * contents or chrome contents. Otherwise, FALSE.
+ */
+ bool HandleKeyDownEvent(NSEvent* aNativeEvent, uint32_t aUniqueId);
+
+ /**
+ * KeyUp event handler.
+ *
+ * @param aNativeEvent A native NSEventTypeKeyUp event.
+ */
+ void HandleKeyUpEvent(NSEvent* aNativeEvent);
+
+ /**
+ * FlagsChanged event handler.
+ *
+ * @param aNativeEvent A native NSEventTypeFlagsChanged event.
+ */
+ void HandleFlagsChanged(NSEvent* aNativeEvent);
+
+ /**
+ * Insert the string to content. I.e., this is a text input event handler.
+ * If this is called during keydown event handling, this may dispatch a
+ * eKeyPress event. If this is called during composition, this commits
+ * the composition by the aAttrString.
+ *
+ * @param aString An inserted string.
+ * @param aReplacementRange The range which will be replaced with the
+ * aAttrString instead of current selection.
+ */
+ void InsertText(NSString* aString, NSRange* aReplacementRange = nullptr);
+
+ /**
+ * Handles aCommand. This may cause dispatching an eKeyPress event.
+ *
+ * @param aCommand The command which receives from Cocoa.
+ * @return true if this handles the command even if it does
+ * nothing actually. Otherwise, false.
+ */
+ bool HandleCommand(Command aCommand);
+
+ /**
+ * doCommandBySelector event handler.
+ *
+ * @param aSelector A selector of the command.
+ * @return TRUE if the command is consumed. Otherwise,
+ * FALSE.
+ */
+ bool DoCommandBySelector(const char* aSelector);
+
+ /**
+ * KeyPressWasHandled() checks whether keypress event was handled or not.
+ *
+ * @return TRUE if keypress event for latest native key
+ * event was handled. Otherwise, FALSE.
+ * If this handler isn't handling any key events,
+ * always returns FALSE.
+ */
+ bool KeyPressWasHandled() {
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ return currentKeyEvent && currentKeyEvent->mKeyPressHandled;
+ }
+
+ protected:
+ // Stores the association of device dependent modifier flags with a modifier
+ // keyCode. Being device dependent, this association may differ from one kind
+ // of hardware to the next.
+ struct ModifierKey {
+ NSUInteger flags;
+ unsigned short keyCode;
+
+ ModifierKey(NSUInteger aFlags, unsigned short aKeyCode)
+ : flags(aFlags), keyCode(aKeyCode) {}
+
+ NSUInteger GetDeviceDependentFlags() const {
+ return (flags & ~NSEventModifierFlagDeviceIndependentFlagsMask);
+ }
+
+ NSUInteger GetDeviceIndependentFlags() const {
+ return (flags & NSEventModifierFlagDeviceIndependentFlagsMask);
+ }
+ };
+ typedef nsTArray<ModifierKey> ModifierKeyArray;
+ ModifierKeyArray mModifierKeys;
+
+ /**
+ * GetModifierKeyForNativeKeyCode() returns the stored ModifierKey for
+ * the key.
+ */
+ const ModifierKey* GetModifierKeyForNativeKeyCode(
+ unsigned short aKeyCode) const;
+
+ /**
+ * GetModifierKeyForDeviceDependentFlags() returns the stored ModifierKey for
+ * the device dependent flags.
+ */
+ const ModifierKey* GetModifierKeyForDeviceDependentFlags(
+ NSUInteger aFlags) const;
+
+ /**
+ * DispatchKeyEventForFlagsChanged() dispatches keydown event or keyup event
+ * for the aNativeEvent.
+ *
+ * @param aNativeEvent A native flagschanged event which you want to
+ * dispatch our key event for.
+ * @param aDispatchKeyDown TRUE if you want to dispatch a keydown event.
+ * Otherwise, i.e., to dispatch keyup event,
+ * FALSE.
+ */
+ void DispatchKeyEventForFlagsChanged(NSEvent* aNativeEvent,
+ bool aDispatchKeyDown);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // TextInputHandler_h_
diff --git a/widget/cocoa/TextInputHandler.mm b/widget/cocoa/TextInputHandler.mm
new file mode 100644
index 0000000000..1d96a3ce82
--- /dev/null
+++ b/widget/cocoa/TextInputHandler.mm
@@ -0,0 +1,5389 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* 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 "TextInputHandler.h"
+
+#include "mozilla/Logging.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
+
+#include "nsChildView.h"
+#include "nsObjCExceptions.h"
+#include "nsBidiUtils.h"
+#include "nsToolkit.h"
+#include "nsCocoaUtils.h"
+#include "WidgetUtils.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+// For collecting other people's log, tell them `MOZ_LOG=IMEHandler:4,sync`
+// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gIMELog("IMEHandler");
+
+// For collecting other people's log, tell them `MOZ_LOG=KeyboardHandler:4,sync`
+// rather than `MOZ_LOG=KeyboardHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gKeyLog("KeyboardHandler");
+
+// The behavior of `TextInputHandler` class is important both for logging
+// keyboard handler and IME handler. Therefore, the behavior is logged when
+// either `IMEHandler` or `KeyboardHandler` is set to `MOZ_LOG`. Therefore,
+// you may not need to tell people
+// `MOZ_LOG=IMEHandler:4,KeyboardHandler:4,sync`.
+#define MOZ_LOG_KEY_OR_IME(aLogLevel, aArgs) \
+ MOZ_LOG(MOZ_LOG_TEST(gIMELog, aLogLevel) ? gIMELog : gKeyLog, aLogLevel, \
+ aArgs)
+
+static const char* OnOrOff(bool aBool) { return aBool ? "ON" : "off"; }
+
+static const char* TrueOrFalse(bool aBool) { return aBool ? "TRUE" : "FALSE"; }
+
+static const char* GetKeyNameForNativeKeyCode(unsigned short aNativeKeyCode) {
+ switch (aNativeKeyCode) {
+ case kVK_Escape:
+ return "Escape";
+ case kVK_RightCommand:
+ return "Right-Command";
+ case kVK_Command:
+ return "Command";
+ case kVK_Shift:
+ return "Shift";
+ case kVK_CapsLock:
+ return "CapsLock";
+ case kVK_Option:
+ return "Option";
+ case kVK_Control:
+ return "Control";
+ case kVK_RightShift:
+ return "Right-Shift";
+ case kVK_RightOption:
+ return "Right-Option";
+ case kVK_RightControl:
+ return "Right-Control";
+ case kVK_ANSI_KeypadClear:
+ return "Clear";
+
+ case kVK_F1:
+ return "F1";
+ case kVK_F2:
+ return "F2";
+ case kVK_F3:
+ return "F3";
+ case kVK_F4:
+ return "F4";
+ case kVK_F5:
+ return "F5";
+ case kVK_F6:
+ return "F6";
+ case kVK_F7:
+ return "F7";
+ case kVK_F8:
+ return "F8";
+ case kVK_F9:
+ return "F9";
+ case kVK_F10:
+ return "F10";
+ case kVK_F11:
+ return "F11";
+ case kVK_F12:
+ return "F12";
+ case kVK_F13:
+ return "F13/PrintScreen";
+ case kVK_F14:
+ return "F14/ScrollLock";
+ case kVK_F15:
+ return "F15/Pause";
+
+ case kVK_ANSI_Keypad0:
+ return "NumPad-0";
+ case kVK_ANSI_Keypad1:
+ return "NumPad-1";
+ case kVK_ANSI_Keypad2:
+ return "NumPad-2";
+ case kVK_ANSI_Keypad3:
+ return "NumPad-3";
+ case kVK_ANSI_Keypad4:
+ return "NumPad-4";
+ case kVK_ANSI_Keypad5:
+ return "NumPad-5";
+ case kVK_ANSI_Keypad6:
+ return "NumPad-6";
+ case kVK_ANSI_Keypad7:
+ return "NumPad-7";
+ case kVK_ANSI_Keypad8:
+ return "NumPad-8";
+ case kVK_ANSI_Keypad9:
+ return "NumPad-9";
+
+ case kVK_ANSI_KeypadMultiply:
+ return "NumPad-*";
+ case kVK_ANSI_KeypadPlus:
+ return "NumPad-+";
+ case kVK_ANSI_KeypadMinus:
+ return "NumPad--";
+ case kVK_ANSI_KeypadDecimal:
+ return "NumPad-.";
+ case kVK_ANSI_KeypadDivide:
+ return "NumPad-/";
+ case kVK_ANSI_KeypadEquals:
+ return "NumPad-=";
+ case kVK_ANSI_KeypadEnter:
+ return "NumPad-Enter";
+ case kVK_Return:
+ return "Return";
+ case kVK_Powerbook_KeypadEnter:
+ return "NumPad-EnterOnPowerBook";
+
+ case kVK_PC_Insert:
+ return "Insert/Help";
+ case kVK_PC_Delete:
+ return "Delete";
+ case kVK_Tab:
+ return "Tab";
+ case kVK_PC_Backspace:
+ return "Backspace";
+ case kVK_Home:
+ return "Home";
+ case kVK_End:
+ return "End";
+ case kVK_PageUp:
+ return "PageUp";
+ case kVK_PageDown:
+ return "PageDown";
+ case kVK_LeftArrow:
+ return "LeftArrow";
+ case kVK_RightArrow:
+ return "RightArrow";
+ case kVK_UpArrow:
+ return "UpArrow";
+ case kVK_DownArrow:
+ return "DownArrow";
+ case kVK_PC_ContextMenu:
+ return "ContextMenu";
+
+ case kVK_Function:
+ return "Function";
+ case kVK_VolumeUp:
+ return "VolumeUp";
+ case kVK_VolumeDown:
+ return "VolumeDown";
+ case kVK_Mute:
+ return "Mute";
+
+ case kVK_ISO_Section:
+ return "ISO_Section";
+
+ case kVK_JIS_Yen:
+ return "JIS_Yen";
+ case kVK_JIS_Underscore:
+ return "JIS_Underscore";
+ case kVK_JIS_KeypadComma:
+ return "JIS_KeypadComma";
+ case kVK_JIS_Eisu:
+ return "JIS_Eisu";
+ case kVK_JIS_Kana:
+ return "JIS_Kana";
+
+ case kVK_ANSI_A:
+ return "A";
+ case kVK_ANSI_B:
+ return "B";
+ case kVK_ANSI_C:
+ return "C";
+ case kVK_ANSI_D:
+ return "D";
+ case kVK_ANSI_E:
+ return "E";
+ case kVK_ANSI_F:
+ return "F";
+ case kVK_ANSI_G:
+ return "G";
+ case kVK_ANSI_H:
+ return "H";
+ case kVK_ANSI_I:
+ return "I";
+ case kVK_ANSI_J:
+ return "J";
+ case kVK_ANSI_K:
+ return "K";
+ case kVK_ANSI_L:
+ return "L";
+ case kVK_ANSI_M:
+ return "M";
+ case kVK_ANSI_N:
+ return "N";
+ case kVK_ANSI_O:
+ return "O";
+ case kVK_ANSI_P:
+ return "P";
+ case kVK_ANSI_Q:
+ return "Q";
+ case kVK_ANSI_R:
+ return "R";
+ case kVK_ANSI_S:
+ return "S";
+ case kVK_ANSI_T:
+ return "T";
+ case kVK_ANSI_U:
+ return "U";
+ case kVK_ANSI_V:
+ return "V";
+ case kVK_ANSI_W:
+ return "W";
+ case kVK_ANSI_X:
+ return "X";
+ case kVK_ANSI_Y:
+ return "Y";
+ case kVK_ANSI_Z:
+ return "Z";
+
+ case kVK_ANSI_1:
+ return "1";
+ case kVK_ANSI_2:
+ return "2";
+ case kVK_ANSI_3:
+ return "3";
+ case kVK_ANSI_4:
+ return "4";
+ case kVK_ANSI_5:
+ return "5";
+ case kVK_ANSI_6:
+ return "6";
+ case kVK_ANSI_7:
+ return "7";
+ case kVK_ANSI_8:
+ return "8";
+ case kVK_ANSI_9:
+ return "9";
+ case kVK_ANSI_0:
+ return "0";
+ case kVK_ANSI_Equal:
+ return "Equal";
+ case kVK_ANSI_Minus:
+ return "Minus";
+ case kVK_ANSI_RightBracket:
+ return "RightBracket";
+ case kVK_ANSI_LeftBracket:
+ return "LeftBracket";
+ case kVK_ANSI_Quote:
+ return "Quote";
+ case kVK_ANSI_Semicolon:
+ return "Semicolon";
+ case kVK_ANSI_Backslash:
+ return "Backslash";
+ case kVK_ANSI_Comma:
+ return "Comma";
+ case kVK_ANSI_Slash:
+ return "Slash";
+ case kVK_ANSI_Period:
+ return "Period";
+ case kVK_ANSI_Grave:
+ return "Grave";
+
+ default:
+ return "undefined";
+ }
+}
+
+static const char* GetCharacters(const nsAString& aString) {
+ if (aString.IsEmpty()) {
+ return "";
+ }
+ nsAutoString escapedStr;
+ for (uint32_t i = 0; i < aString.Length(); i++) {
+ char16_t ch = aString.CharAt(i);
+ if (ch < 0x20) {
+ nsPrintfCString utf8str("(U+%04X)", ch);
+ escapedStr += NS_ConvertUTF8toUTF16(utf8str);
+ } else if (ch <= 0x7E) {
+ escapedStr += ch;
+ } else {
+ nsPrintfCString utf8str("(U+%04X)", ch);
+ escapedStr += ch;
+ escapedStr += NS_ConvertUTF8toUTF16(utf8str);
+ }
+ }
+
+ // the result will be freed automatically by cocoa.
+ NSString* result = nsCocoaUtils::ToNSString(escapedStr);
+ return [result UTF8String];
+}
+
+static const char* GetCharacters(const NSString* aString) {
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString(aString, str);
+ return GetCharacters(str);
+}
+
+static const char* GetCharacters(const CFStringRef aString) {
+ const NSString* str = reinterpret_cast<const NSString*>(aString);
+ return GetCharacters(str);
+}
+
+static const char* GetNativeKeyEventType(NSEvent* aNativeEvent) {
+ switch ([aNativeEvent type]) {
+ case NSEventTypeKeyDown:
+ return "NSEventTypeKeyDown";
+ case NSEventTypeKeyUp:
+ return "NSEventTypeKeyUp";
+ case NSEventTypeFlagsChanged:
+ return "NSEventTypeFlagsChanged";
+ default:
+ return "not key event";
+ }
+}
+
+static const char* GetGeckoKeyEventType(const WidgetEvent& aEvent) {
+ switch (aEvent.mMessage) {
+ case eKeyDown:
+ return "eKeyDown";
+ case eKeyUp:
+ return "eKeyUp";
+ case eKeyPress:
+ return "eKeyPress";
+ default:
+ return "not key event";
+ }
+}
+
+static const char* GetWindowLevelName(NSInteger aWindowLevel) {
+ switch (aWindowLevel) {
+ case kCGBaseWindowLevelKey:
+ return "kCGBaseWindowLevelKey (NSNormalWindowLevel)";
+ case kCGMinimumWindowLevelKey:
+ return "kCGMinimumWindowLevelKey";
+ case kCGDesktopWindowLevelKey:
+ return "kCGDesktopWindowLevelKey";
+ case kCGBackstopMenuLevelKey:
+ return "kCGBackstopMenuLevelKey";
+ case kCGNormalWindowLevelKey:
+ return "kCGNormalWindowLevelKey";
+ case kCGFloatingWindowLevelKey:
+ return "kCGFloatingWindowLevelKey (NSFloatingWindowLevel)";
+ case kCGTornOffMenuWindowLevelKey:
+ return "kCGTornOffMenuWindowLevelKey (NSSubmenuWindowLevel, "
+ "NSTornOffMenuWindowLevel)";
+ case kCGDockWindowLevelKey:
+ return "kCGDockWindowLevelKey (NSDockWindowLevel)";
+ case kCGMainMenuWindowLevelKey:
+ return "kCGMainMenuWindowLevelKey (NSMainMenuWindowLevel)";
+ case kCGStatusWindowLevelKey:
+ return "kCGStatusWindowLevelKey (NSStatusWindowLevel)";
+ case kCGModalPanelWindowLevelKey:
+ return "kCGModalPanelWindowLevelKey (NSModalPanelWindowLevel)";
+ case kCGPopUpMenuWindowLevelKey:
+ return "kCGPopUpMenuWindowLevelKey (NSPopUpMenuWindowLevel)";
+ case kCGDraggingWindowLevelKey:
+ return "kCGDraggingWindowLevelKey";
+ case kCGScreenSaverWindowLevelKey:
+ return "kCGScreenSaverWindowLevelKey (NSScreenSaverWindowLevel)";
+ case kCGMaximumWindowLevelKey:
+ return "kCGMaximumWindowLevelKey";
+ case kCGOverlayWindowLevelKey:
+ return "kCGOverlayWindowLevelKey";
+ case kCGHelpWindowLevelKey:
+ return "kCGHelpWindowLevelKey";
+ case kCGUtilityWindowLevelKey:
+ return "kCGUtilityWindowLevelKey";
+ case kCGDesktopIconWindowLevelKey:
+ return "kCGDesktopIconWindowLevelKey";
+ case kCGCursorWindowLevelKey:
+ return "kCGCursorWindowLevelKey";
+ case kCGNumberOfWindowLevelKeys:
+ return "kCGNumberOfWindowLevelKeys";
+ default:
+ return "unknown window level";
+ }
+}
+
+static bool IsControlChar(uint32_t aCharCode) {
+ return aCharCode < ' ' || aCharCode == 0x7F;
+}
+
+static uint32_t gHandlerInstanceCount = 0;
+
+static void EnsureToLogAllKeyboardLayoutsAndIMEs() {
+ static bool sDone = false;
+ if (!sDone) {
+ sDone = true;
+ TextInputHandler::DebugPrintAllKeyboardLayouts();
+ IMEInputHandler::DebugPrintAllIMEModes();
+ }
+}
+
+inline NSRange MakeNSRangeFrom(
+ const Maybe<OffsetAndData<uint32_t>>& aOffsetAndData) {
+ if (aOffsetAndData.isNothing()) {
+ return NSMakeRange(NSNotFound, 0);
+ }
+ return NSMakeRange(aOffsetAndData->StartOffset(), aOffsetAndData->Length());
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TISInputSourceWrapper implementation
+ *
+ ******************************************************************************/
+
+TISInputSourceWrapper* TISInputSourceWrapper::sCurrentInputSource = nullptr;
+
+// static
+TISInputSourceWrapper& TISInputSourceWrapper::CurrentInputSource() {
+ if (!sCurrentInputSource) {
+ sCurrentInputSource = new TISInputSourceWrapper();
+ }
+ if (!sCurrentInputSource->IsInitializedByCurrentInputSource()) {
+ sCurrentInputSource->InitByCurrentInputSource();
+ }
+ return *sCurrentInputSource;
+}
+
+// static
+void TISInputSourceWrapper::Shutdown() {
+ if (!sCurrentInputSource) {
+ return;
+ }
+ sCurrentInputSource->Clear();
+ delete sCurrentInputSource;
+ sCurrentInputSource = nullptr;
+}
+
+bool TISInputSourceWrapper::TranslateToString(UInt32 aKeyCode,
+ UInt32 aModifiers, UInt32 aKbType,
+ nsAString& aStr) {
+ aStr.Truncate();
+
+ const UCKeyboardLayout* UCKey = GetUCKeyboardLayout();
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, aKeyCode=0x%X, "
+ "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n "
+ "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s",
+ this, static_cast<unsigned int>(aKeyCode),
+ static_cast<unsigned int>(aModifiers),
+ static_cast<unsigned int>(aKbType), UCKey,
+ OnOrOff(aModifiers & shiftKey), OnOrOff(aModifiers & controlKey),
+ OnOrOff(aModifiers & optionKey), OnOrOff(aModifiers & cmdKey),
+ OnOrOff(aModifiers & alphaLock),
+ OnOrOff(aModifiers & kEventKeyModifierNumLockMask)));
+
+ NS_ENSURE_TRUE(UCKey, false);
+
+ UInt32 deadKeyState = 0;
+ UniCharCount len;
+ UniChar chars[5];
+ OSStatus err = ::UCKeyTranslate(
+ UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8, aKbType,
+ kUCKeyTranslateNoDeadKeysMask, &deadKeyState, 5, &len, chars);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, err=0x%X, len=%zu",
+ this, static_cast<int>(err), len));
+
+ NS_ENSURE_TRUE(err == noErr, false);
+ if (len == 0) {
+ return true;
+ }
+ if (!aStr.SetLength(len, fallible)) {
+ return false;
+ }
+ NS_ASSERTION(sizeof(char16_t) == sizeof(UniChar),
+ "size of char16_t and size of UniChar are different");
+ memcpy(aStr.BeginWriting(), chars, len * sizeof(char16_t));
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::TranslateToString, aStr=\"%s\"", this,
+ NS_ConvertUTF16toUTF8(aStr).get()));
+
+ return true;
+}
+
+uint32_t TISInputSourceWrapper::TranslateToChar(UInt32 aKeyCode,
+ UInt32 aModifiers,
+ UInt32 aKbType) {
+ nsAutoString str;
+ if (!TranslateToString(aKeyCode, aModifiers, aKbType, str) ||
+ str.Length() != 1) {
+ return 0;
+ }
+ return static_cast<uint32_t>(str.CharAt(0));
+}
+
+bool TISInputSourceWrapper::IsDeadKey(NSEvent* aNativeKeyEvent) {
+ if ([[aNativeKeyEvent characters] length]) {
+ return false;
+ }
+
+ // Assmue that if control key or command key is pressed, it's not a dead key.
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags];
+ if (cocoaState & (NSEventModifierFlagControl | NSEventModifierFlagCommand)) {
+ return false;
+ }
+
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+ switch (nativeKeyCode) {
+ case kVK_ANSI_A:
+ case kVK_ANSI_B:
+ case kVK_ANSI_C:
+ case kVK_ANSI_D:
+ case kVK_ANSI_E:
+ case kVK_ANSI_F:
+ case kVK_ANSI_G:
+ case kVK_ANSI_H:
+ case kVK_ANSI_I:
+ case kVK_ANSI_J:
+ case kVK_ANSI_K:
+ case kVK_ANSI_L:
+ case kVK_ANSI_M:
+ case kVK_ANSI_N:
+ case kVK_ANSI_O:
+ case kVK_ANSI_P:
+ case kVK_ANSI_Q:
+ case kVK_ANSI_R:
+ case kVK_ANSI_S:
+ case kVK_ANSI_T:
+ case kVK_ANSI_U:
+ case kVK_ANSI_V:
+ case kVK_ANSI_W:
+ case kVK_ANSI_X:
+ case kVK_ANSI_Y:
+ case kVK_ANSI_Z:
+ case kVK_ANSI_1:
+ case kVK_ANSI_2:
+ case kVK_ANSI_3:
+ case kVK_ANSI_4:
+ case kVK_ANSI_5:
+ case kVK_ANSI_6:
+ case kVK_ANSI_7:
+ case kVK_ANSI_8:
+ case kVK_ANSI_9:
+ case kVK_ANSI_0:
+ case kVK_ANSI_Equal:
+ case kVK_ANSI_Minus:
+ case kVK_ANSI_RightBracket:
+ case kVK_ANSI_LeftBracket:
+ case kVK_ANSI_Quote:
+ case kVK_ANSI_Semicolon:
+ case kVK_ANSI_Backslash:
+ case kVK_ANSI_Comma:
+ case kVK_ANSI_Slash:
+ case kVK_ANSI_Period:
+ case kVK_ANSI_Grave:
+ case kVK_JIS_Yen:
+ case kVK_JIS_Underscore:
+ break;
+ default:
+ // Let's assume that dead key can be only a printable key in standard
+ // position.
+ return false;
+ }
+
+ // If TranslateToChar() returns non-zero value, that means that
+ // the key may input a character with different dead key state.
+ UInt32 kbType = GetKbdType();
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ return IsDeadKey(nativeKeyCode, carbonState, kbType);
+}
+
+bool TISInputSourceWrapper::IsDeadKey(UInt32 aKeyCode, UInt32 aModifiers,
+ UInt32 aKbType) {
+ const UCKeyboardLayout* UCKey = GetUCKeyboardLayout();
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::IsDeadKey, aKeyCode=0x%X, "
+ "aModifiers=0x%X, aKbType=0x%X UCKey=%p\n "
+ "Shift: %s, Ctrl: %s, Opt: %s, Cmd: %s, CapsLock: %s, NumLock: %s",
+ this, static_cast<unsigned int>(aKeyCode),
+ static_cast<unsigned int>(aModifiers),
+ static_cast<unsigned int>(aKbType), UCKey,
+ OnOrOff(aModifiers & shiftKey), OnOrOff(aModifiers & controlKey),
+ OnOrOff(aModifiers & optionKey), OnOrOff(aModifiers & cmdKey),
+ OnOrOff(aModifiers & alphaLock),
+ OnOrOff(aModifiers & kEventKeyModifierNumLockMask)));
+
+ if (NS_WARN_IF(!UCKey)) {
+ return false;
+ }
+
+ UInt32 deadKeyState = 0;
+ UniCharCount len;
+ UniChar chars[5];
+ OSStatus err =
+ ::UCKeyTranslate(UCKey, aKeyCode, kUCKeyActionDown, aModifiers >> 8,
+ aKbType, 0, &deadKeyState, 5, &len, chars);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::IsDeadKey, err=0x%X, "
+ "len=%zu, deadKeyState=%u",
+ this, static_cast<int>(err), len, deadKeyState));
+
+ if (NS_WARN_IF(err != noErr)) {
+ return false;
+ }
+
+ return deadKeyState != 0;
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const char* aID) {
+ Clear();
+ if (!aID) return;
+
+ CFStringRef idstr = ::CFStringCreateWithCString(kCFAllocatorDefault, aID,
+ kCFStringEncodingASCII);
+ InitByInputSourceID(idstr);
+ ::CFRelease(idstr);
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const nsString& aID) {
+ Clear();
+ if (aID.IsEmpty()) return;
+ CFStringRef idstr = ::CFStringCreateWithCharacters(
+ kCFAllocatorDefault, reinterpret_cast<const UniChar*>(aID.get()),
+ aID.Length());
+ InitByInputSourceID(idstr);
+ ::CFRelease(idstr);
+}
+
+void TISInputSourceWrapper::InitByInputSourceID(const CFStringRef aID) {
+ Clear();
+ if (!aID) return;
+ const void* keys[] = {kTISPropertyInputSourceID};
+ const void* values[] = {aID};
+ CFDictionaryRef filter =
+ ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ mInputSourceList = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ if (::CFArrayGetCount(mInputSourceList) > 0) {
+ mInputSource = static_cast<TISInputSourceRef>(
+ const_cast<void*>(::CFArrayGetValueAtIndex(mInputSourceList, 0)));
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+ }
+}
+
+void TISInputSourceWrapper::InitByLayoutID(SInt32 aLayoutID,
+ bool aOverrideKeyboard) {
+ // NOTE: Doument new layout IDs in TextInputHandler.h when you add ones.
+ switch (aLayoutID) {
+ case 0:
+ InitByInputSourceID("com.apple.keylayout.US");
+ break;
+ case 1:
+ InitByInputSourceID("com.apple.keylayout.Greek");
+ break;
+ case 2:
+ InitByInputSourceID("com.apple.keylayout.German");
+ break;
+ case 3:
+ InitByInputSourceID("com.apple.keylayout.Swedish-Pro");
+ break;
+ case 4:
+ InitByInputSourceID("com.apple.keylayout.DVORAK-QWERTYCMD");
+ break;
+ case 5:
+ InitByInputSourceID("com.apple.keylayout.Thai");
+ break;
+ case 6:
+ InitByInputSourceID("com.apple.keylayout.Arabic");
+ break;
+ case 7:
+ InitByInputSourceID("com.apple.keylayout.ArabicPC");
+ break;
+ case 8:
+ InitByInputSourceID("com.apple.keylayout.French");
+ break;
+ case 9:
+ InitByInputSourceID("com.apple.keylayout.Hebrew");
+ break;
+ case 10:
+ InitByInputSourceID("com.apple.keylayout.Lithuanian");
+ break;
+ case 11:
+ InitByInputSourceID("com.apple.keylayout.Norwegian");
+ break;
+ case 12:
+ InitByInputSourceID("com.apple.keylayout.Spanish");
+ break;
+ case 13:
+ InitByInputSourceID("com.apple.keylayout.French-PC");
+ break;
+ default:
+ Clear();
+ break;
+ }
+ mOverrideKeyboard = aOverrideKeyboard;
+}
+
+void TISInputSourceWrapper::InitByCurrentInputSource() {
+ Clear();
+ mInputSource = ::TISCopyCurrentKeyboardInputSource();
+ mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride();
+ if (!mKeyboardLayout) {
+ mKeyboardLayout = ::TISCopyCurrentKeyboardLayoutInputSource();
+ }
+ // If this causes composition, the current keyboard layout may input non-ASCII
+ // characters such as Japanese Kana characters or Hangul characters.
+ // However, we need to set ASCII characters to DOM key events for consistency
+ // with other platforms.
+ if (IsOpenedIMEMode()) {
+ TISInputSourceWrapper tis(mKeyboardLayout);
+ if (!tis.IsASCIICapable()) {
+ mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ }
+ }
+}
+
+void TISInputSourceWrapper::InitByCurrentKeyboardLayout() {
+ Clear();
+ mInputSource = ::TISCopyCurrentKeyboardLayoutInputSource();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByCurrentASCIICapableInputSource() {
+ Clear();
+ mInputSource = ::TISCopyCurrentASCIICapableKeyboardInputSource();
+ mKeyboardLayout = ::TISCopyInputMethodKeyboardLayoutOverride();
+ if (mKeyboardLayout) {
+ TISInputSourceWrapper tis(mKeyboardLayout);
+ if (!tis.IsASCIICapable()) {
+ mKeyboardLayout = nullptr;
+ }
+ }
+ if (!mKeyboardLayout) {
+ mKeyboardLayout = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ }
+}
+
+void TISInputSourceWrapper::InitByCurrentASCIICapableKeyboardLayout() {
+ Clear();
+ mInputSource = ::TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByCurrentInputMethodKeyboardLayoutOverride() {
+ Clear();
+ mInputSource = ::TISCopyInputMethodKeyboardLayoutOverride();
+ mKeyboardLayout = mInputSource;
+}
+
+void TISInputSourceWrapper::InitByTISInputSourceRef(
+ TISInputSourceRef aInputSource) {
+ Clear();
+ mInputSource = aInputSource;
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+}
+
+void TISInputSourceWrapper::InitByLanguage(CFStringRef aLanguage) {
+ Clear();
+ mInputSource = ::TISCopyInputSourceForLanguage(aLanguage);
+ if (IsKeyboardLayout()) {
+ mKeyboardLayout = mInputSource;
+ }
+}
+
+const UCKeyboardLayout* TISInputSourceWrapper::GetUCKeyboardLayout() {
+ NS_ENSURE_TRUE(mKeyboardLayout, nullptr);
+ if (mUCKeyboardLayout) {
+ return mUCKeyboardLayout;
+ }
+ CFDataRef uchr = static_cast<CFDataRef>(::TISGetInputSourceProperty(
+ mKeyboardLayout, kTISPropertyUnicodeKeyLayoutData));
+
+ // We should be always able to get the layout here.
+ NS_ENSURE_TRUE(uchr, nullptr);
+ mUCKeyboardLayout =
+ reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(uchr));
+ return mUCKeyboardLayout;
+}
+
+bool TISInputSourceWrapper::GetBoolProperty(const CFStringRef aKey) {
+ CFBooleanRef ret = static_cast<CFBooleanRef>(
+ ::TISGetInputSourceProperty(mInputSource, aKey));
+ return ::CFBooleanGetValue(ret);
+}
+
+bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey,
+ CFStringRef& aStr) {
+ aStr =
+ static_cast<CFStringRef>(::TISGetInputSourceProperty(mInputSource, aKey));
+ return aStr != nullptr;
+}
+
+bool TISInputSourceWrapper::GetStringProperty(const CFStringRef aKey,
+ nsAString& aStr) {
+ CFStringRef str;
+ GetStringProperty(aKey, str);
+ nsCocoaUtils::GetStringForNSString((const NSString*)str, aStr);
+ return !aStr.IsEmpty();
+}
+
+bool TISInputSourceWrapper::IsOpenedIMEMode() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ if (!IsIMEMode()) return false;
+ return !IsASCIICapable();
+}
+
+bool TISInputSourceWrapper::IsIMEMode() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef str;
+ GetInputSourceType(str);
+ NS_ENSURE_TRUE(str, false);
+ return ::CFStringCompare(kTISTypeKeyboardInputMode, str, 0) ==
+ kCFCompareEqualTo;
+}
+
+bool TISInputSourceWrapper::IsKeyboardLayout() {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef str;
+ GetInputSourceType(str);
+ NS_ENSURE_TRUE(str, false);
+ return ::CFStringCompare(kTISTypeKeyboardLayout, str, 0) == kCFCompareEqualTo;
+}
+
+bool TISInputSourceWrapper::GetLanguageList(CFArrayRef& aLanguageList) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ aLanguageList = static_cast<CFArrayRef>(::TISGetInputSourceProperty(
+ mInputSource, kTISPropertyInputSourceLanguages));
+ return aLanguageList != nullptr;
+}
+
+bool TISInputSourceWrapper::GetPrimaryLanguage(CFStringRef& aPrimaryLanguage) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFArrayRef langList;
+ NS_ENSURE_TRUE(GetLanguageList(langList), false);
+ if (::CFArrayGetCount(langList) == 0) return false;
+ aPrimaryLanguage =
+ static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, 0));
+ return aPrimaryLanguage != nullptr;
+}
+
+bool TISInputSourceWrapper::GetPrimaryLanguage(nsAString& aPrimaryLanguage) {
+ NS_ENSURE_TRUE(mInputSource, false);
+ CFStringRef primaryLanguage;
+ NS_ENSURE_TRUE(GetPrimaryLanguage(primaryLanguage), false);
+ nsCocoaUtils::GetStringForNSString((const NSString*)primaryLanguage,
+ aPrimaryLanguage);
+ return !aPrimaryLanguage.IsEmpty();
+}
+
+bool TISInputSourceWrapper::IsForRTLLanguage() {
+ if (mIsRTL < 0) {
+ // Get the input character of the 'A' key of ANSI keyboard layout.
+ nsAutoString str;
+ bool ret = TranslateToString(kVK_ANSI_A, 0, eKbdType_ANSI, str);
+ NS_ENSURE_TRUE(ret, ret);
+ char16_t ch = str.IsEmpty() ? char16_t(0) : str.CharAt(0);
+ mIsRTL = UTF16_CODE_UNIT_IS_BIDI(ch);
+ }
+ return mIsRTL != 0;
+}
+
+bool TISInputSourceWrapper::IsForJapaneseLanguage() {
+ nsAutoString lang;
+ GetPrimaryLanguage(lang);
+ return lang.EqualsLiteral("ja");
+}
+
+bool TISInputSourceWrapper::IsInitializedByCurrentInputSource() {
+ return mInputSource == ::TISCopyCurrentKeyboardInputSource();
+}
+
+void TISInputSourceWrapper::Select() {
+ if (!mInputSource) return;
+ ::TISSelectInputSource(mInputSource);
+}
+
+void TISInputSourceWrapper::Clear() {
+ // Clear() is always called when TISInputSourceWrappper is created.
+ EnsureToLogAllKeyboardLayoutsAndIMEs();
+
+ if (mInputSourceList) {
+ ::CFRelease(mInputSourceList);
+ }
+ mInputSourceList = nullptr;
+ mInputSource = nullptr;
+ mKeyboardLayout = nullptr;
+ mIsRTL = -1;
+ mUCKeyboardLayout = nullptr;
+ mOverrideKeyboard = false;
+}
+
+bool TISInputSourceWrapper::IsPrintableKeyEvent(
+ NSEvent* aNativeKeyEvent) const {
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+
+ bool isPrintableKey = !TextInputHandler::IsSpecialGeckoKey(nativeKeyCode);
+ if (isPrintableKey && [aNativeKeyEvent type] != NSEventTypeKeyDown &&
+ [aNativeKeyEvent type] != NSEventTypeKeyUp) {
+ NS_WARNING("Why would a printable key not be an NSEventTypeKeyDown or "
+ "NSEventTypeKeyUp event?");
+ isPrintableKey = false;
+ }
+ return isPrintableKey;
+}
+
+UInt32 TISInputSourceWrapper::GetKbdType() const {
+ // If a keyboard layout override is set, we also need to force the keyboard
+ // type to something ANSI to avoid test failures on machines with JIS
+ // keyboards (since the pair of keyboard layout and physical keyboard type
+ // form the actual key layout). This assumes that the test setting the
+ // override was written assuming an ANSI keyboard.
+ return mOverrideKeyboard ? eKbdType_ANSI : ::LMGetKbdType();
+}
+
+void TISInputSourceWrapper::ComputeInsertStringForCharCode(
+ NSEvent* aNativeKeyEvent, const WidgetKeyboardEvent& aKeyEvent,
+ const nsAString* aInsertString, nsAString& aResult) {
+ if (aInsertString) {
+ // If the caller expects that the aInsertString will be input, we shouldn't
+ // change it.
+ aResult = *aInsertString;
+ } else if (IsPrintableKeyEvent(aNativeKeyEvent)) {
+ // If IME is open, [aNativeKeyEvent characters] may be a character
+ // which will be appended to the composition string. However, especially,
+ // while IME is disabled, most users and developers expect the key event
+ // works as IME closed. So, we should compute the aResult with
+ // the ASCII capable keyboard layout.
+ // NOTE: Such keyboard layouts typically change the layout to its ASCII
+ // capable layout when Command key is pressed. And we don't worry
+ // when Control key is pressed too because it causes inputting
+ // control characters.
+ // Additionally, if the key event doesn't input any text, the event may be
+ // dead key event. In this case, the charCode value should be the dead
+ // character.
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+ if ((!aKeyEvent.IsMeta() && !aKeyEvent.IsControl() && IsOpenedIMEMode()) ||
+ ![[aNativeKeyEvent characters] length]) {
+ UInt32 state = nsCocoaUtils::ConvertToCarbonModifier(
+ [aNativeKeyEvent modifierFlags]);
+ uint32_t ch = TranslateToChar(nativeKeyCode, state, GetKbdType());
+ if (ch) {
+ aResult = ch;
+ }
+ } else {
+ // If the caller isn't sure what string will be input, let's use
+ // characters of NSEvent.
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], aResult);
+ }
+
+ // If control key is pressed and the eventChars is a non-printable control
+ // character, we should convert it to ASCII alphabet.
+ if (aKeyEvent.IsControl() && !aResult.IsEmpty() &&
+ aResult[0] <= char16_t(26)) {
+ aResult = (aKeyEvent.IsShift() ^ aKeyEvent.IsCapsLocked())
+ ? static_cast<char16_t>(aResult[0] + ('A' - 1))
+ : static_cast<char16_t>(aResult[0] + ('a' - 1));
+ }
+ // If Meta key is pressed, it may cause to switch the keyboard layout like
+ // Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY.
+ else if (aKeyEvent.IsMeta() &&
+ !(aKeyEvent.IsControl() || aKeyEvent.IsAlt())) {
+ UInt32 kbType = GetKbdType();
+ UInt32 numLockState =
+ aKeyEvent.IsNumLocked() ? kEventKeyModifierNumLockMask : 0;
+ UInt32 capsLockState = aKeyEvent.IsCapsLocked() ? alphaLock : 0;
+ UInt32 shiftState = aKeyEvent.IsShift() ? shiftKey : 0;
+ uint32_t uncmdedChar =
+ TranslateToChar(nativeKeyCode, numLockState, kbType);
+ uint32_t cmdedChar =
+ TranslateToChar(nativeKeyCode, cmdKey | numLockState, kbType);
+ // If we can make a good guess at the characters that the user would
+ // expect this key combination to produce (with and without Shift) then
+ // use those characters. This also corrects for CapsLock.
+ uint32_t ch = 0;
+ if (uncmdedChar == cmdedChar) {
+ // The characters produced with Command seem similar to those without
+ // Command.
+ ch = TranslateToChar(nativeKeyCode,
+ shiftState | capsLockState | numLockState, kbType);
+ } else {
+ TISInputSourceWrapper USLayout("com.apple.keylayout.US");
+ uint32_t uncmdedUSChar =
+ USLayout.TranslateToChar(nativeKeyCode, numLockState, kbType);
+ // If it looks like characters from US keyboard layout when Command key
+ // is pressed, we should compute a character in the layout.
+ if (uncmdedUSChar == cmdedChar) {
+ ch = USLayout.TranslateToChar(
+ nativeKeyCode, shiftState | capsLockState | numLockState, kbType);
+ }
+ }
+
+ // If there is a more preferred character for the commanded key event,
+ // we should use it.
+ if (ch) {
+ aResult = ch;
+ }
+ }
+ }
+
+ // Remove control characters which shouldn't be inputted on editor.
+ // XXX Currently, we don't find any cases inserting control characters with
+ // printable character. So, just checking first character is enough.
+ if (!aResult.IsEmpty() && IsControlChar(aResult[0])) {
+ aResult.Truncate();
+ }
+}
+
+void TISInputSourceWrapper::InitKeyEvent(NSEvent* aNativeKeyEvent,
+ WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME,
+ const nsAString* aInsertString) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(!aIsProcessedByIME || aKeyEvent.mMessage != eKeyPress,
+ "eKeyPress event should not be marked as proccessed by IME");
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::InitKeyEvent, aNativeKeyEvent=%p, "
+ "aKeyEvent.mMessage=%s, aProcessedByIME=%s, aInsertString=%p, "
+ "IsOpenedIMEMode()=%s",
+ this, aNativeKeyEvent, GetGeckoKeyEventType(aKeyEvent),
+ TrueOrFalse(aIsProcessedByIME), aInsertString,
+ TrueOrFalse(IsOpenedIMEMode())));
+
+ if (NS_WARN_IF(!aNativeKeyEvent)) {
+ return;
+ }
+
+ nsCocoaUtils::InitInputEvent(aKeyEvent, aNativeKeyEvent);
+
+ // This is used only while dispatching the event (which is a synchronous
+ // call), so there is no need to retain and release this data.
+ aKeyEvent.mNativeKeyEvent = aNativeKeyEvent;
+
+ aKeyEvent.mRefPoint = LayoutDeviceIntPoint(0, 0);
+
+ UInt32 kbType = GetKbdType();
+ UInt32 nativeKeyCode = [aNativeKeyEvent keyCode];
+
+ // macOS handles dead key as IME. If the key is first key press of dead
+ // key, we should use KEY_NAME_INDEX_Dead for first (dead) key event.
+ // So, if aIsProcessedByIME is true, it may be dead key. Let's check
+ // if current key event is a dead key's keydown event.
+ bool isProcessedByIME =
+ aIsProcessedByIME &&
+ !TISInputSourceWrapper::CurrentInputSource().IsDeadKey(aNativeKeyEvent);
+
+ aKeyEvent.mKeyCode =
+ isProcessedByIME
+ ? NS_VK_PROCESSKEY
+ : ComputeGeckoKeyCode(nativeKeyCode, kbType, aKeyEvent.IsMeta());
+
+ switch (nativeKeyCode) {
+ case kVK_Command:
+ case kVK_Shift:
+ case kVK_Option:
+ case kVK_Control:
+ aKeyEvent.mLocation = eKeyLocationLeft;
+ break;
+
+ case kVK_RightCommand:
+ case kVK_RightShift:
+ case kVK_RightOption:
+ case kVK_RightControl:
+ aKeyEvent.mLocation = eKeyLocationRight;
+ break;
+
+ case kVK_ANSI_Keypad0:
+ case kVK_ANSI_Keypad1:
+ case kVK_ANSI_Keypad2:
+ case kVK_ANSI_Keypad3:
+ case kVK_ANSI_Keypad4:
+ case kVK_ANSI_Keypad5:
+ case kVK_ANSI_Keypad6:
+ case kVK_ANSI_Keypad7:
+ case kVK_ANSI_Keypad8:
+ case kVK_ANSI_Keypad9:
+ case kVK_ANSI_KeypadMultiply:
+ case kVK_ANSI_KeypadPlus:
+ case kVK_ANSI_KeypadMinus:
+ case kVK_ANSI_KeypadDecimal:
+ case kVK_ANSI_KeypadDivide:
+ case kVK_ANSI_KeypadEquals:
+ case kVK_ANSI_KeypadEnter:
+ case kVK_JIS_KeypadComma:
+ case kVK_Powerbook_KeypadEnter:
+ aKeyEvent.mLocation = eKeyLocationNumpad;
+ break;
+
+ default:
+ aKeyEvent.mLocation = eKeyLocationStandard;
+ break;
+ }
+
+ aKeyEvent.mIsRepeat = ([aNativeKeyEvent type] == NSEventTypeKeyDown)
+ ? [aNativeKeyEvent isARepeat]
+ : false;
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::InitKeyEvent, "
+ "shift=%s, ctrl=%s, alt=%s, meta=%s",
+ this, OnOrOff(aKeyEvent.IsShift()), OnOrOff(aKeyEvent.IsControl()),
+ OnOrOff(aKeyEvent.IsAlt()), OnOrOff(aKeyEvent.IsMeta())));
+
+ if (isProcessedByIME) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
+ } else if (IsPrintableKeyEvent(aNativeKeyEvent)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ // If insertText calls this method, let's use the string.
+ if (aInsertString && !aInsertString->IsEmpty() &&
+ !IsControlChar((*aInsertString)[0])) {
+ aKeyEvent.mKeyValue = *aInsertString;
+ }
+ // If meta key is pressed, the printable key layout may be switched from
+ // non-ASCII capable layout to ASCII capable, or from Dvorak to QWERTY.
+ // KeyboardEvent.key value should be the switched layout's character.
+ else if (aKeyEvent.IsMeta()) {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters],
+ aKeyEvent.mKeyValue);
+ }
+ // If control key is pressed, some keys may produce printable character via
+ // [aNativeKeyEvent characters]. Otherwise, translate input character of
+ // the key without control key.
+ else if (aKeyEvent.IsControl()) {
+ NSUInteger cocoaState =
+ [aNativeKeyEvent modifierFlags] & ~NSEventModifierFlagControl;
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ if (IsDeadKey(nativeKeyCode, carbonState, kbType)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Dead;
+ } else {
+ aKeyEvent.mKeyValue =
+ TranslateToChar(nativeKeyCode, carbonState, kbType);
+ if (!aKeyEvent.mKeyValue.IsEmpty() &&
+ IsControlChar(aKeyEvent.mKeyValue[0])) {
+ // Don't expose control character to the web.
+ aKeyEvent.mKeyValue.Truncate();
+ }
+ }
+ }
+ // Otherwise, KeyboardEvent.key expose
+ // [aNativeKeyEvent characters] value. However, if IME is open and the
+ // keyboard layout isn't ASCII capable, exposing the non-ASCII character
+ // doesn't match with other platform's behavior. For the compatibility
+ // with other platform's Gecko, we need to set a translated character.
+ else if (IsOpenedIMEMode()) {
+ UInt32 state = nsCocoaUtils::ConvertToCarbonModifier(
+ [aNativeKeyEvent modifierFlags]);
+ aKeyEvent.mKeyValue = TranslateToChar(nativeKeyCode, state, kbType);
+ } else {
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters],
+ aKeyEvent.mKeyValue);
+ // If the key value is empty, the event may be a dead key event.
+ // If TranslateToChar() returns non-zero value, that means that
+ // the key may input a character with different dead key state.
+ if (aKeyEvent.mKeyValue.IsEmpty()) {
+ NSUInteger cocoaState = [aNativeKeyEvent modifierFlags];
+ UInt32 carbonState = nsCocoaUtils::ConvertToCarbonModifier(cocoaState);
+ if (TranslateToChar(nativeKeyCode, carbonState, kbType)) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_Dead;
+ }
+ }
+ }
+
+ // Last resort. If .key value becomes empty string, we should use
+ // charactersIgnoringModifiers, if it's available.
+ if (aKeyEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
+ (aKeyEvent.mKeyValue.IsEmpty() ||
+ IsControlChar(aKeyEvent.mKeyValue[0]))) {
+ nsCocoaUtils::GetStringForNSString(
+ [aNativeKeyEvent charactersIgnoringModifiers], aKeyEvent.mKeyValue);
+ // But don't expose it if it's a control character.
+ if (!aKeyEvent.mKeyValue.IsEmpty() &&
+ IsControlChar(aKeyEvent.mKeyValue[0])) {
+ aKeyEvent.mKeyValue.Truncate();
+ }
+ }
+ } else {
+ // Compute the key for non-printable keys and some special printable keys.
+ aKeyEvent.mKeyNameIndex = ComputeGeckoKeyNameIndex(nativeKeyCode);
+ }
+
+ aKeyEvent.mCodeNameIndex = ComputeGeckoCodeNameIndex(nativeKeyCode, kbType);
+ MOZ_ASSERT(aKeyEvent.mCodeNameIndex != CODE_NAME_INDEX_USE_STRING);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+void TISInputSourceWrapper::WillDispatchKeyboardEvent(
+ NSEvent* aNativeKeyEvent, const nsAString* aInsertString,
+ uint32_t aIndexOfKeypress, WidgetKeyboardEvent& aKeyEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Nothing to do here if the native key event is neither NSEventTypeKeyDown
+ // nor NSEventTypeKeyUp because accessing [aNativeKeyEvent characters] causes
+ // throwing an exception.
+ if ([aNativeKeyEvent type] != NSEventTypeKeyDown &&
+ [aNativeKeyEvent type] != NSEventTypeKeyUp) {
+ return;
+ }
+
+ UInt32 kbType = GetKbdType();
+
+ if (MOZ_LOG_TEST(gKeyLog, LogLevel::Info)) {
+ nsAutoString chars;
+ nsCocoaUtils::GetStringForNSString([aNativeKeyEvent characters], chars);
+ NS_ConvertUTF16toUTF8 utf8Chars(chars);
+ char16_t uniChar = static_cast<char16_t>(aKeyEvent.mCharCode);
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aNativeKeyEvent=%p, aInsertString=%p (\"%s\"), "
+ "aIndexOfKeypress=%u, [aNativeKeyEvent characters]=\"%s\", "
+ "aKeyEvent={ mMessage=%s, mCharCode=0x%X(%s) }, kbType=0x%X, "
+ "IsOpenedIMEMode()=%s",
+ this, aNativeKeyEvent, aInsertString,
+ aInsertString ? GetCharacters(*aInsertString) : "", aIndexOfKeypress,
+ GetCharacters([aNativeKeyEvent characters]),
+ GetGeckoKeyEventType(aKeyEvent), aKeyEvent.mCharCode,
+ uniChar ? NS_ConvertUTF16toUTF8(&uniChar, 1).get() : "",
+ static_cast<unsigned int>(kbType), TrueOrFalse(IsOpenedIMEMode())));
+ }
+
+ nsAutoString insertStringForCharCode;
+ ComputeInsertStringForCharCode(aNativeKeyEvent, aKeyEvent, aInsertString,
+ insertStringForCharCode);
+
+ // The mCharCode was set from mKeyValue. However, for example, when Ctrl key
+ // is pressed, its value should indicate an ASCII character for backward
+ // compatibility rather than inputting character without the modifiers.
+ // Therefore, we need to modify mCharCode value here.
+ uint32_t charCode = 0;
+ if (aIndexOfKeypress < insertStringForCharCode.Length()) {
+ charCode = insertStringForCharCode[aIndexOfKeypress];
+ }
+ aKeyEvent.SetCharCode(charCode);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aKeyEvent.mKeyCode=0x%X, aKeyEvent.mCharCode=0x%X",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode));
+
+ // If aInsertString is not nullptr (it means InsertText() is called)
+ // and it acutally inputs a character, we don't need to append alternative
+ // charCode values since such keyboard event shouldn't be handled as
+ // a shortcut key.
+ if (aInsertString && charCode) {
+ return;
+ }
+
+ TISInputSourceWrapper USLayout("com.apple.keylayout.US");
+ bool isRomanKeyboardLayout = IsASCIICapable();
+
+ UInt32 key = [aNativeKeyEvent keyCode];
+
+ // Caps lock and num lock modifier state:
+ UInt32 lockState = 0;
+ if ([aNativeKeyEvent modifierFlags] & NSEventModifierFlagCapsLock) {
+ lockState |= alphaLock;
+ }
+ if ([aNativeKeyEvent modifierFlags] & NSEventModifierFlagNumericPad) {
+ lockState |= kEventKeyModifierNumLockMask;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "isRomanKeyboardLayout=%s, kbType=0x%X, key=0x%X",
+ this, TrueOrFalse(isRomanKeyboardLayout),
+ static_cast<unsigned int>(kbType), static_cast<unsigned int>(key)));
+
+ nsString str;
+
+ // normal chars
+ uint32_t unshiftedChar = TranslateToChar(key, lockState, kbType);
+ UInt32 shiftLockMod = shiftKey | lockState;
+ uint32_t shiftedChar = TranslateToChar(key, shiftLockMod, kbType);
+
+ // characters generated with Cmd key
+ // XXX we should remove CapsLock state, which changes characters from
+ // Latin to Cyrillic with Russian layout on 10.4 only when Cmd key
+ // is pressed.
+ UInt32 numState = (lockState & ~alphaLock); // only num lock state
+ uint32_t uncmdedChar = TranslateToChar(key, numState, kbType);
+ UInt32 shiftNumMod = numState | shiftKey;
+ uint32_t uncmdedShiftChar = TranslateToChar(key, shiftNumMod, kbType);
+ uint32_t uncmdedUSChar = USLayout.TranslateToChar(key, numState, kbType);
+ UInt32 cmdNumMod = cmdKey | numState;
+ uint32_t cmdedChar = TranslateToChar(key, cmdNumMod, kbType);
+ UInt32 cmdShiftNumMod = shiftKey | cmdNumMod;
+ uint32_t cmdedShiftChar = TranslateToChar(key, cmdShiftNumMod, kbType);
+
+ // Is the keyboard layout changed by Cmd key?
+ // E.g., Arabic, Russian, Hebrew, Greek and Dvorak-QWERTY.
+ bool isCmdSwitchLayout = uncmdedChar != cmdedChar;
+ // Is the keyboard layout for Latin, but Cmd key switches the layout?
+ // I.e., Dvorak-QWERTY
+ bool isDvorakQWERTY = isCmdSwitchLayout && isRomanKeyboardLayout;
+
+ // If the current keyboard is not Dvorak-QWERTY or Cmd is not pressed,
+ // we should append unshiftedChar and shiftedChar for handling the
+ // normal characters. These are the characters that the user is most
+ // likely to associate with this key.
+ if ((unshiftedChar || shiftedChar) &&
+ (!aKeyEvent.IsMeta() || !isDvorakQWERTY)) {
+ AlternativeCharCode altCharCodes(unshiftedChar, shiftedChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "aKeyEvent.isMeta=%s, isDvorakQWERTY=%s, "
+ "unshiftedChar=U+%X, shiftedChar=U+%X",
+ this, OnOrOff(aKeyEvent.IsMeta()), TrueOrFalse(isDvorakQWERTY),
+ unshiftedChar, shiftedChar));
+
+ // Most keyboard layouts provide the same characters in the NSEvents
+ // with Command+Shift as with Command. However, with Command+Shift we
+ // want the character on the second level. e.g. With a US QWERTY
+ // layout, we want "?" when the "/","?" key is pressed with
+ // Command+Shift.
+
+ // On a German layout, the OS gives us '/' with Cmd+Shift+SS(eszett)
+ // even though Cmd+SS is 'SS' and Shift+'SS' is '?'. This '/' seems
+ // like a hack to make the Cmd+"?" event look the same as the Cmd+"?"
+ // event on a US keyboard. The user thinks they are typing Cmd+"?", so
+ // we'll prefer the "?" character, replacing mCharCode with shiftedChar
+ // when Shift is pressed. However, in case there is a layout where the
+ // character unique to Cmd+Shift is the character that the user expects,
+ // we'll send it as an alternative char.
+ bool hasCmdShiftOnlyChar =
+ cmdedChar != cmdedShiftChar && uncmdedShiftChar != cmdedShiftChar;
+ uint32_t originalCmdedShiftChar = cmdedShiftChar;
+
+ // If we can make a good guess at the characters that the user would
+ // expect this key combination to produce (with and without Shift) then
+ // use those characters. This also corrects for CapsLock, which was
+ // ignored above.
+ if (!isCmdSwitchLayout) {
+ // The characters produced with Command seem similar to those without
+ // Command.
+ if (unshiftedChar) {
+ cmdedChar = unshiftedChar;
+ }
+ if (shiftedChar) {
+ cmdedShiftChar = shiftedChar;
+ }
+ } else if (uncmdedUSChar == cmdedChar) {
+ // It looks like characters from a US layout are provided when Command
+ // is down.
+ uint32_t ch = USLayout.TranslateToChar(key, lockState, kbType);
+ if (ch) {
+ cmdedChar = ch;
+ }
+ ch = USLayout.TranslateToChar(key, shiftLockMod, kbType);
+ if (ch) {
+ cmdedShiftChar = ch;
+ }
+ }
+
+ // If the current keyboard layout is switched by the Cmd key,
+ // we should append cmdedChar and shiftedCmdChar that are
+ // Latin char for the key.
+ // If the keyboard layout is Dvorak-QWERTY, we should append them only when
+ // command key is pressed because when command key isn't pressed, uncmded
+ // chars have been appended already.
+ if ((cmdedChar || cmdedShiftChar) && isCmdSwitchLayout &&
+ (aKeyEvent.IsMeta() || !isDvorakQWERTY)) {
+ AlternativeCharCode altCharCodes(cmdedChar, cmdedShiftChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "hasCmdShiftOnlyChar=%s, isCmdSwitchLayout=%s, isDvorakQWERTY=%s, "
+ "cmdedChar=U+%X, cmdedShiftChar=U+%X",
+ this, TrueOrFalse(hasCmdShiftOnlyChar), TrueOrFalse(isDvorakQWERTY),
+ TrueOrFalse(isDvorakQWERTY), cmdedChar, cmdedShiftChar));
+ // Special case for 'SS' key of German layout. See the comment of
+ // hasCmdShiftOnlyChar definition for the detail.
+ if (hasCmdShiftOnlyChar && originalCmdedShiftChar) {
+ AlternativeCharCode altCharCodes(0, originalCmdedShiftChar);
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::WillDispatchKeyboardEvent, "
+ "hasCmdShiftOnlyChar=%s, originalCmdedShiftChar=U+%X",
+ this, TrueOrFalse(hasCmdShiftOnlyChar), originalCmdedShiftChar));
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+uint32_t TISInputSourceWrapper::ComputeGeckoKeyCode(UInt32 aNativeKeyCode,
+ UInt32 aKbType,
+ bool aCmdIsPressed) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p TISInputSourceWrapper::ComputeGeckoKeyCode, aNativeKeyCode=0x%X, "
+ "aKbType=0x%X, aCmdIsPressed=%s, IsOpenedIMEMode()=%s, "
+ "IsASCIICapable()=%s",
+ this, static_cast<unsigned int>(aNativeKeyCode),
+ static_cast<unsigned int>(aKbType), TrueOrFalse(aCmdIsPressed),
+ TrueOrFalse(IsOpenedIMEMode()), TrueOrFalse(IsASCIICapable())));
+
+ switch (aNativeKeyCode) {
+ case kVK_Space:
+ return NS_VK_SPACE;
+ case kVK_Escape:
+ return NS_VK_ESCAPE;
+
+ // modifiers
+ case kVK_RightCommand:
+ case kVK_Command:
+ return NS_VK_META;
+ case kVK_RightShift:
+ case kVK_Shift:
+ return NS_VK_SHIFT;
+ case kVK_CapsLock:
+ return NS_VK_CAPS_LOCK;
+ case kVK_RightControl:
+ case kVK_Control:
+ return NS_VK_CONTROL;
+ case kVK_RightOption:
+ case kVK_Option:
+ return NS_VK_ALT;
+
+ case kVK_ANSI_KeypadClear:
+ return NS_VK_CLEAR;
+
+ // function keys
+ case kVK_F1:
+ return NS_VK_F1;
+ case kVK_F2:
+ return NS_VK_F2;
+ case kVK_F3:
+ return NS_VK_F3;
+ case kVK_F4:
+ return NS_VK_F4;
+ case kVK_F5:
+ return NS_VK_F5;
+ case kVK_F6:
+ return NS_VK_F6;
+ case kVK_F7:
+ return NS_VK_F7;
+ case kVK_F8:
+ return NS_VK_F8;
+ case kVK_F9:
+ return NS_VK_F9;
+ case kVK_F10:
+ return NS_VK_F10;
+ case kVK_F11:
+ return NS_VK_F11;
+ case kVK_F12:
+ return NS_VK_F12;
+ // case kVK_F13: return NS_VK_F13; // clash with the 3 below
+ // case kVK_F14: return NS_VK_F14;
+ // case kVK_F15: return NS_VK_F15;
+ case kVK_F16:
+ return NS_VK_F16;
+ case kVK_F17:
+ return NS_VK_F17;
+ case kVK_F18:
+ return NS_VK_F18;
+ case kVK_F19:
+ return NS_VK_F19;
+
+ case kVK_PC_Pause:
+ return NS_VK_PAUSE;
+ case kVK_PC_ScrollLock:
+ return NS_VK_SCROLL_LOCK;
+ case kVK_PC_PrintScreen:
+ return NS_VK_PRINTSCREEN;
+
+ // keypad
+ case kVK_ANSI_Keypad0:
+ return NS_VK_NUMPAD0;
+ case kVK_ANSI_Keypad1:
+ return NS_VK_NUMPAD1;
+ case kVK_ANSI_Keypad2:
+ return NS_VK_NUMPAD2;
+ case kVK_ANSI_Keypad3:
+ return NS_VK_NUMPAD3;
+ case kVK_ANSI_Keypad4:
+ return NS_VK_NUMPAD4;
+ case kVK_ANSI_Keypad5:
+ return NS_VK_NUMPAD5;
+ case kVK_ANSI_Keypad6:
+ return NS_VK_NUMPAD6;
+ case kVK_ANSI_Keypad7:
+ return NS_VK_NUMPAD7;
+ case kVK_ANSI_Keypad8:
+ return NS_VK_NUMPAD8;
+ case kVK_ANSI_Keypad9:
+ return NS_VK_NUMPAD9;
+
+ case kVK_ANSI_KeypadMultiply:
+ return NS_VK_MULTIPLY;
+ case kVK_ANSI_KeypadPlus:
+ return NS_VK_ADD;
+ case kVK_ANSI_KeypadMinus:
+ return NS_VK_SUBTRACT;
+ case kVK_ANSI_KeypadDecimal:
+ return NS_VK_DECIMAL;
+ case kVK_ANSI_KeypadDivide:
+ return NS_VK_DIVIDE;
+
+ case kVK_JIS_KeypadComma:
+ return NS_VK_SEPARATOR;
+
+ // IME keys
+ case kVK_JIS_Eisu:
+ return NS_VK_EISU;
+ case kVK_JIS_Kana:
+ return NS_VK_KANA;
+
+ // these may clash with forward delete and help
+ case kVK_PC_Insert:
+ return NS_VK_INSERT;
+ case kVK_PC_Delete:
+ return NS_VK_DELETE;
+
+ case kVK_PC_Backspace:
+ return NS_VK_BACK;
+ case kVK_Tab:
+ return NS_VK_TAB;
+
+ case kVK_Home:
+ return NS_VK_HOME;
+ case kVK_End:
+ return NS_VK_END;
+
+ case kVK_PageUp:
+ return NS_VK_PAGE_UP;
+ case kVK_PageDown:
+ return NS_VK_PAGE_DOWN;
+
+ case kVK_LeftArrow:
+ return NS_VK_LEFT;
+ case kVK_RightArrow:
+ return NS_VK_RIGHT;
+ case kVK_UpArrow:
+ return NS_VK_UP;
+ case kVK_DownArrow:
+ return NS_VK_DOWN;
+
+ case kVK_PC_ContextMenu:
+ return NS_VK_CONTEXT_MENU;
+
+ case kVK_ANSI_1:
+ return NS_VK_1;
+ case kVK_ANSI_2:
+ return NS_VK_2;
+ case kVK_ANSI_3:
+ return NS_VK_3;
+ case kVK_ANSI_4:
+ return NS_VK_4;
+ case kVK_ANSI_5:
+ return NS_VK_5;
+ case kVK_ANSI_6:
+ return NS_VK_6;
+ case kVK_ANSI_7:
+ return NS_VK_7;
+ case kVK_ANSI_8:
+ return NS_VK_8;
+ case kVK_ANSI_9:
+ return NS_VK_9;
+ case kVK_ANSI_0:
+ return NS_VK_0;
+
+ case kVK_ANSI_KeypadEnter:
+ case kVK_Return:
+ case kVK_Powerbook_KeypadEnter:
+ return NS_VK_RETURN;
+ }
+
+ // If Cmd key is pressed, that causes switching keyboard layout temporarily.
+ // E.g., Dvorak-QWERTY. Therefore, if Cmd key is pressed, we should honor it.
+ UInt32 modifiers = aCmdIsPressed ? cmdKey : 0;
+
+ uint32_t charCode = TranslateToChar(aNativeKeyCode, modifiers, aKbType);
+
+ // Special case for Mac. Mac inputs Yen sign (U+00A5) directly instead of
+ // Back slash (U+005C). We should return NS_VK_BACK_SLASH for compatibility
+ // with other platforms.
+ // XXX How about Won sign (U+20A9) which has same problem as Yen sign?
+ if (charCode == 0x00A5) {
+ return NS_VK_BACK_SLASH;
+ }
+
+ uint32_t keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode);
+ if (keyCode) {
+ return keyCode;
+ }
+
+ // If the unshifed char isn't an ASCII character, use shifted char.
+ charCode = TranslateToChar(aNativeKeyCode, modifiers | shiftKey, aKbType);
+ keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode);
+ if (keyCode) {
+ return keyCode;
+ }
+
+ if (!IsASCIICapable()) {
+ // Retry with ASCII capable keyboard layout.
+ TISInputSourceWrapper currentKeyboardLayout;
+ currentKeyboardLayout.InitByCurrentASCIICapableKeyboardLayout();
+ NS_ENSURE_TRUE(mInputSource != currentKeyboardLayout.mInputSource, 0);
+ keyCode = currentKeyboardLayout.ComputeGeckoKeyCode(aNativeKeyCode, aKbType,
+ aCmdIsPressed);
+ // We've returned 0 for long time if keyCode isn't for an alphabet keys or
+ // a numeric key even in alternative ASCII capable keyboard layout because
+ // we decided that we should avoid setting same keyCode value to 2 or
+ // more keys since active keyboard layout may have a key to input the
+ // punctuation with different key. However, setting keyCode to 0 makes
+ // some web applications which are aware of neither KeyboardEvent.key nor
+ // KeyboardEvent.code not work with Firefox when user selects non-ASCII
+ // capable keyboard layout such as Russian and Thai. So, if alternative
+ // ASCII capable keyboard layout has keyCode value for the key, we should
+ // use it. In other words, this behavior does that non-ASCII capable
+ // keyboard layout overrides some keys' keyCode value only if the key
+ // produces ASCII character by itself or with Shift key.
+ if (keyCode) {
+ return keyCode;
+ }
+ }
+
+ // Otherwise, let's decide keyCode value from the native virtual keycode
+ // value on major keyboard layout.
+ CodeNameIndex code = ComputeGeckoCodeNameIndex(aNativeKeyCode, aKbType);
+ return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code);
+}
+
+// static
+KeyNameIndex TISInputSourceWrapper::ComputeGeckoKeyNameIndex(
+ UInt32 aNativeKeyCode) {
+ // NOTE:
+ // When unsupported keys like Convert, Nonconvert of Japanese keyboard is
+ // pressed:
+ // on 10.6.x, 'A' key event is fired (and also actually 'a' is inserted).
+ // on 10.7.x, Nothing happens.
+ // on 10.8.x, Nothing happens.
+ // on 10.9.x, FlagsChanged event is fired with keyCode 0xFF.
+ switch (aNativeKeyCode) {
+#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ return KEY_NAME_INDEX_Unidentified;
+ }
+}
+
+// static
+CodeNameIndex TISInputSourceWrapper::ComputeGeckoCodeNameIndex(
+ UInt32 aNativeKeyCode, UInt32 aKbType) {
+ // macOS swaps native key code between Backquote key and IntlBackslash key
+ // only when the keyboard type is ISO. Let's treat the key code after
+ // swapping them here because Chromium does so only when computing .code
+ // value.
+ if (::KBGetLayoutType(aKbType) == kKeyboardISO) {
+ if (aNativeKeyCode == kVK_ISO_Section) {
+ aNativeKeyCode = kVK_ANSI_Grave;
+ } else if (aNativeKeyCode == kVK_ANSI_Grave) {
+ aNativeKeyCode = kVK_ISO_Section;
+ }
+ }
+
+ switch (aNativeKeyCode) {
+#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \
+ case aNativeKey: \
+ return aCodeNameIndex;
+
+#include "NativeKeyToDOMCodeName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX
+
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandler implementation (static methods)
+ *
+ ******************************************************************************/
+
+NSUInteger TextInputHandler::sLastModifierState = 0;
+
+// static
+CFArrayRef TextInputHandler::CreateAllKeyboardLayoutList() {
+ const void* keys[] = {kTISPropertyInputSourceType};
+ const void* values[] = {kTISTypeKeyboardLayout};
+ CFDictionaryRef filter =
+ ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ CFArrayRef list = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ return list;
+}
+
+// static
+void TextInputHandler::DebugPrintAllKeyboardLayouts() {
+ if (MOZ_LOG_TEST(gKeyLog, LogLevel::Info)) {
+ CFArrayRef list = CreateAllKeyboardLayoutList();
+ MOZ_LOG(gKeyLog, LogLevel::Info, ("Keyboard layout configuration:"));
+ CFIndex idx = ::CFArrayGetCount(list);
+ TISInputSourceWrapper tis;
+ for (CFIndex i = 0; i < idx; ++i) {
+ TISInputSourceRef inputSource = static_cast<TISInputSourceRef>(
+ const_cast<void*>(::CFArrayGetValueAtIndex(list, i)));
+ tis.InitByTISInputSourceRef(inputSource);
+ nsAutoString name, isid;
+ tis.GetLocalizedName(name);
+ tis.GetInputSourceID(isid);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" %s\t<%s>%s%s\n", NS_ConvertUTF16toUTF8(name).get(),
+ NS_ConvertUTF16toUTF8(isid).get(),
+ tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)",
+ tis.IsKeyboardLayout() && tis.GetUCKeyboardLayout()
+ ? ""
+ : "\t(uchr is NOT AVAILABLE)"));
+ }
+ ::CFRelease(list);
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandler implementation
+ *
+ ******************************************************************************/
+
+TextInputHandler::TextInputHandler(nsChildView* aWidget,
+ NSView<mozView>* aNativeView)
+ : IMEInputHandler(aWidget, aNativeView) {
+ EnsureToLogAllKeyboardLayoutsAndIMEs();
+ [mView installTextInputHandler:this];
+}
+
+TextInputHandler::~TextInputHandler() { [mView uninstallTextInputHandler]; }
+
+bool TextInputHandler::HandleKeyDownEvent(NSEvent* aNativeEvent,
+ uint32_t aUniqueId) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, "
+ "widget has been already destroyed",
+ this));
+ return false;
+ }
+
+ // Insert empty line to the log for easier to read.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, (""));
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, aNativeEvent=%p, "
+ "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", "
+ "charactersIgnoringModifiers=\"%s\"",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ [aNativeEvent keyCode], [aNativeEvent keyCode],
+ static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ GetCharacters([aNativeEvent characters]),
+ GetCharacters([aNativeEvent charactersIgnoringModifiers])));
+
+ // Except when Command key is pressed, we should hide mouse cursor until
+ // next mousemove. Handling here means that:
+ // - Don't hide mouse cursor at pressing modifier key
+ // - Hide mouse cursor even if the key event will be handled by IME (i.e.,
+ // even without dispatching eKeyPress events)
+ // - Hide mouse cursor even when a plugin has focus
+ if (!([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) {
+ [NSCursor setHiddenUntilMouseMoves:YES];
+ }
+
+ RefPtr<nsChildView> widget(mWidget);
+
+ KeyEventState* currentKeyEvent = PushKeyEvent(aNativeEvent, aUniqueId);
+ AutoKeyEventStateCleaner remover(this);
+
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+
+ // When we're already in a composition, we need always to mark the eKeyDown
+ // event as "processed by IME". So, let's dispatch eKeyDown event here in
+ // such case.
+ if (IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyDown "
+ "caused focus move or "
+ "something and canceling the composition",
+ this));
+ return false;
+ }
+
+ // Let Cocoa interpret the key events, caching IsIMEComposing first.
+ bool wasComposing = IsIMEComposing();
+ bool interpretKeyEventsCalled = false;
+ // Don't call interpretKeyEvents when a plugin has focus. If we call it,
+ // for example, a character is inputted twice during a composition in e10s
+ // mode.
+ if (IsIMEEnabled() || IsASCIICapableOnly()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, calling "
+ "interpretKeyEvents",
+ this));
+ [mView interpretKeyEvents:[NSArray arrayWithObject:aNativeEvent]];
+ interpretKeyEventsCalled = true;
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, called interpretKeyEvents",
+ this));
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, widget was destroyed",
+ this));
+ return currentKeyEvent->IsDefaultPrevented();
+ }
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, wasComposing=%s, "
+ "IsIMEComposing()=%s",
+ this, TrueOrFalse(wasComposing), TrueOrFalse(IsIMEComposing())));
+
+ if (currentKeyEvent->CanDispatchKeyDownEvent()) {
+ // Dispatch eKeyDown event if nobody has dispatched it yet.
+ // NOTE: Although reaching here means that the native keydown event may
+ // not be handled by IME. However, we cannot know if it is.
+ // For example, Japanese IME of Apple shows candidate window for
+ // typing window. They, you can switch the sort order with Tab key.
+ // However, when you choose "Symbol" of the sort order, there may
+ // be no candiate words. In this case, IME handles the Tab key
+ // actually, but we cannot know it because composition string is
+ // not updated. So, let's mark eKeyDown event as "processed by IME"
+ // when there is composition string. This is same as Chrome.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, trying to "
+ "dispatch eKeyDown "
+ "event since it's not yet dispatched",
+ this));
+ if (!MaybeDispatchCurrentKeydownEvent(IsIMEComposing())) {
+ return true; // treat the eKeydDown event as consumed.
+ }
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyDown event has been "
+ "dispatched",
+ this));
+ }
+
+ if (currentKeyEvent->CanDispatchKeyPressEvent() && !wasComposing &&
+ !IsIMEComposing()) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Error,
+ ("%p TextInputHandler::HandleKeyDownEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure "
+ "at dispatching keypress",
+ this));
+ return false;
+ }
+
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+
+ // If we called interpretKeyEvents and this isn't normal character input
+ // then IME probably ate the event for some reason. We do not want to
+ // send a key press event in that case.
+ // TODO:
+ // There are some other cases which IME eats the current event.
+ // 1. If key events were nested during calling interpretKeyEvents, it means
+ // that IME did something. Then, we should do nothing.
+ // 2. If one or more commands are called like "deleteBackward", we should
+ // dispatch keypress event at that time. Note that the command may have
+ // been a converted or generated action by IME. Then, we shouldn't do
+ // our default action for this key.
+ if (!(interpretKeyEventsCalled &&
+ IsNormalCharInputtingEvent(aNativeEvent))) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, trying to dispatch "
+ "eKeyPress event since it's not yet dispatched",
+ this));
+ nsEventStatus status = nsEventStatus_eIgnore;
+ currentKeyEvent->mKeyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status,
+ currentKeyEvent);
+ currentKeyEvent->mKeyPressHandled =
+ (status == nsEventStatus_eConsumeNoDefault);
+ currentKeyEvent->mKeyPressDispatched = true;
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, eKeyPress event has been "
+ "dispatched",
+ this));
+ }
+ }
+
+ // Note: mWidget might have become null here. Don't count on it from here on.
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyDownEvent, "
+ "keydown handled=%s, keypress handled=%s, causedOtherKeyEvents=%s, "
+ "compositionDispatched=%s",
+ this, TrueOrFalse(currentKeyEvent->mKeyDownHandled),
+ TrueOrFalse(currentKeyEvent->mKeyPressHandled),
+ TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents),
+ TrueOrFalse(currentKeyEvent->mCompositionDispatched)));
+ // Insert empty line to the log for easier to read.
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, (""));
+ return currentKeyEvent->IsDefaultPrevented();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void TextInputHandler::HandleKeyUpEvent(NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyUpEvent, aNativeEvent=%p, "
+ "type=%s, keyCode=%u (0x%X), modifierFlags=0x%lX, characters=\"%s\", "
+ "charactersIgnoringModifiers=\"%s\", "
+ "IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ [aNativeEvent keyCode], [aNativeEvent keyCode],
+ static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ GetCharacters([aNativeEvent characters]),
+ GetCharacters([aNativeEvent charactersIgnoringModifiers]),
+ TrueOrFalse(IsIMEComposing())));
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleKeyUpEvent, "
+ "widget has been already destroyed",
+ this));
+ return;
+ }
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error,
+ ("%p TextInputHandler::HandleKeyUpEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ // Neither Chrome for macOS nor Safari marks "keyup" event as "processed by
+ // IME" even during composition. So, let's follow this behavior.
+ WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget);
+ InitKeyEvent(aNativeEvent, keyupEvent, false);
+
+ KeyEventState currentKeyEvent(aNativeEvent);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mDispatcher->DispatchKeyboardEvent(eKeyUp, keyupEvent, status,
+ &currentKeyEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandler::HandleFlagsChanged(NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandler::HandleFlagsChanged, "
+ "widget has been already destroyed",
+ this));
+ return;
+ }
+
+ RefPtr<nsChildView> kungFuDeathGrip(mWidget);
+ mozilla::Unused << kungFuDeathGrip; // Not referenced within this function
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleFlagsChanged, aNativeEvent=%p, "
+ "type=%s, keyCode=%s (0x%X), modifierFlags=0x%08lX, "
+ "sLastModifierState=0x%08lX, IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ GetKeyNameForNativeKeyCode([aNativeEvent keyCode]),
+ [aNativeEvent keyCode],
+ static_cast<unsigned long>([aNativeEvent modifierFlags]),
+ static_cast<unsigned long>(sLastModifierState),
+ TrueOrFalse(IsIMEComposing())));
+
+ MOZ_ASSERT([aNativeEvent type] == NSEventTypeFlagsChanged);
+
+ NSUInteger diff = [aNativeEvent modifierFlags] ^ sLastModifierState;
+ // Device dependent flags for left-control key, both shift keys, both command
+ // keys and both option keys have been defined in Next's SDK. But we
+ // shouldn't use it directly as far as possible since Cocoa SDK doesn't
+ // define them. Fortunately, we need them only when we dispatch keyup
+ // events. So, we can usually know the actual relation between keyCode and
+ // device dependent flags. However, we need to remove following flags first
+ // since the differences don't indicate modifier key state.
+ // NX_STYLUSPROXIMITYMASK: Probably used for pen like device.
+ // kCGEventFlagMaskNonCoalesced (= NX_NONCOALSESCEDMASK): See the document for
+ // Quartz Event Services.
+ diff &= ~(NX_STYLUSPROXIMITYMASK | kCGEventFlagMaskNonCoalesced);
+
+ switch ([aNativeEvent keyCode]) {
+ // CapsLock state and other modifier states are different:
+ // CapsLock state does not revert when the CapsLock key goes up, as the
+ // modifier state does for other modifier keys on key up.
+ case kVK_CapsLock: {
+ // Fire key down event for caps lock.
+ DispatchKeyEventForFlagsChanged(aNativeEvent, true);
+ // XXX should we fire keyup event too? The keyup event for CapsLock key
+ // is never dispatched on Gecko.
+ // XXX WebKit dispatches keydown event when CapsLock is locked, otherwise,
+ // keyup event. If we do so, we cannot keep the consistency with other
+ // platform's behavior...
+ break;
+ }
+
+ // If the event is caused by pressing or releasing a modifier key, just
+ // dispatch the key's event.
+ case kVK_Shift:
+ case kVK_RightShift:
+ case kVK_Command:
+ case kVK_RightCommand:
+ case kVK_Control:
+ case kVK_RightControl:
+ case kVK_Option:
+ case kVK_RightOption:
+ case kVK_Help: {
+ // We assume that at most one modifier is changed per event if the event
+ // is caused by pressing or releasing a modifier key.
+ bool isKeyDown = ([aNativeEvent modifierFlags] & diff) != 0;
+ DispatchKeyEventForFlagsChanged(aNativeEvent, isKeyDown);
+ // XXX Some applications might send the event with incorrect device-
+ // dependent flags.
+ if (isKeyDown &&
+ ((diff & ~NSEventModifierFlagDeviceIndependentFlagsMask) != 0)) {
+ unsigned short keyCode = [aNativeEvent keyCode];
+ const ModifierKey* modifierKey =
+ GetModifierKeyForDeviceDependentFlags(diff);
+ if (modifierKey && modifierKey->keyCode != keyCode) {
+ // Although, we're not sure the actual cause of this case, the stored
+ // modifier information and the latest key event information may be
+ // mismatched. Then, let's reset the stored information.
+ // NOTE: If this happens, it may fail to handle
+ // NSEventTypeFlagsChanged event in the default case (below). However,
+ // it's the rare case handler and this case occurs rarely. So, we can
+ // ignore the edge case bug.
+ NS_WARNING("Resetting stored modifier key information");
+ mModifierKeys.Clear();
+ modifierKey = nullptr;
+ }
+ if (!modifierKey) {
+ mModifierKeys.AppendElement(ModifierKey(diff, keyCode));
+ }
+ }
+ break;
+ }
+
+ // Currently we don't support Fn key since other browsers don't dispatch
+ // events for it and we don't have keyCode for this key.
+ // It should be supported when we implement .key and .char.
+ case kVK_Function:
+ break;
+
+ // If the event is caused by something else than pressing or releasing a
+ // single modifier key (for example by the app having been deactivated
+ // using command-tab), use the modifiers themselves to determine which
+ // key's event to dispatch, and whether it's a keyup or keydown event.
+ // In all cases we assume one or more modifiers are being deactivated
+ // (never activated) -- otherwise we'd have received one or more events
+ // corresponding to a single modifier key being pressed.
+ default: {
+ NSUInteger modifiers = sLastModifierState;
+ AutoTArray<unsigned short, 10> dispatchedKeyCodes;
+ for (int32_t bit = 0; bit < 32; ++bit) {
+ NSUInteger flag = 1 << bit;
+ if (!(diff & flag)) {
+ continue;
+ }
+
+ // Given correct information from the application, a flag change here
+ // will normally be a deactivation (except for some lockable modifiers
+ // such as CapsLock). But some applications (like VNC) can send an
+ // activating event with a zero keyCode. So we need to check for that
+ // here.
+ bool dispatchKeyDown = ((flag & [aNativeEvent modifierFlags]) != 0);
+
+ unsigned short keyCode = 0;
+ if (flag & NSEventModifierFlagDeviceIndependentFlagsMask) {
+ switch (flag) {
+ case NSEventModifierFlagCapsLock:
+ keyCode = kVK_CapsLock;
+ dispatchKeyDown = true;
+ break;
+
+ case NSEventModifierFlagNumericPad:
+ // NSEventModifierFlagNumericPad is fired by VNC a lot. But not
+ // all of these events can really be Clear key events, so we just
+ // ignore them.
+ continue;
+
+ case NSEventModifierFlagHelp:
+ keyCode = kVK_Help;
+ break;
+
+ case NSEventModifierFlagFunction:
+ // An NSEventModifierFlagFunction change here will normally be a
+ // deactivation. But sometimes it will be an activation send (by
+ // VNC for example) with a zero keyCode.
+ continue;
+
+ // These cases (NSEventModifierFlagShift,
+ // NSEventModifierFlagControl, NSEventModifierFlagOption and
+ // NSEventModifierFlagCommand) should be handled by the other branch
+ // of the if statement, below (which handles device dependent
+ // flags). However, some applications (like VNC) can send key events
+ // without any device dependent flags, so we handle them here
+ // instead.
+ case NSEventModifierFlagShift:
+ keyCode = (modifiers & 0x0004) ? kVK_RightShift : kVK_Shift;
+ break;
+ case NSEventModifierFlagControl:
+ keyCode = (modifiers & 0x2000) ? kVK_RightControl : kVK_Control;
+ break;
+ case NSEventModifierFlagOption:
+ keyCode = (modifiers & 0x0040) ? kVK_RightOption : kVK_Option;
+ break;
+ case NSEventModifierFlagCommand:
+ keyCode = (modifiers & 0x0010) ? kVK_RightCommand : kVK_Command;
+ break;
+
+ default:
+ continue;
+ }
+ } else {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForDeviceDependentFlags(flag);
+ if (!modifierKey) {
+ // See the note above (in the other branch of the if statement)
+ // about the NSEventModifierFlagShift, NSEventModifierFlagControl,
+ // NSEventModifierFlagOption and NSEventModifierFlagCommand cases.
+ continue;
+ }
+ keyCode = modifierKey->keyCode;
+ }
+
+ // Remove flags
+ modifiers &= ~flag;
+ switch (keyCode) {
+ case kVK_Shift: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_RightShift);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagShift;
+ }
+ break;
+ }
+ case kVK_RightShift: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_Shift);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagShift;
+ }
+ break;
+ }
+ case kVK_Command: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_RightCommand);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagCommand;
+ }
+ break;
+ }
+ case kVK_RightCommand: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_Command);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagCommand;
+ }
+ break;
+ }
+ case kVK_Control: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_RightControl);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagControl;
+ }
+ break;
+ }
+ case kVK_RightControl: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_Control);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagControl;
+ }
+ break;
+ }
+ case kVK_Option: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_RightOption);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagOption;
+ }
+ break;
+ }
+ case kVK_RightOption: {
+ const ModifierKey* modifierKey =
+ GetModifierKeyForNativeKeyCode(kVK_Option);
+ if (!modifierKey ||
+ !(modifiers & modifierKey->GetDeviceDependentFlags())) {
+ modifiers &= ~NSEventModifierFlagOption;
+ }
+ break;
+ }
+ case kVK_Help:
+ modifiers &= ~NSEventModifierFlagHelp;
+ break;
+ default:
+ break;
+ }
+
+ // Avoid dispatching same keydown/keyup events twice or more.
+ // We must be able to assume that there is no case to dispatch
+ // both keydown and keyup events with same key code value here.
+ if (dispatchedKeyCodes.Contains(keyCode)) {
+ continue;
+ }
+ dispatchedKeyCodes.AppendElement(keyCode);
+
+ NSEvent* event =
+ [NSEvent keyEventWithType:NSEventTypeFlagsChanged
+ location:[aNativeEvent locationInWindow]
+ modifierFlags:modifiers
+ timestamp:[aNativeEvent timestamp]
+ windowNumber:[aNativeEvent windowNumber]
+ context:nil
+ characters:@""
+ charactersIgnoringModifiers:@""
+ isARepeat:NO
+ keyCode:keyCode];
+ DispatchKeyEventForFlagsChanged(event, dispatchKeyDown);
+ if (Destroyed()) {
+ break;
+ }
+
+ // Stop if focus has changed.
+ // Check to see if mView is still the first responder.
+ if (![mView isFirstResponder]) {
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ // Be aware, the widget may have been destroyed.
+ sLastModifierState = [aNativeEvent modifierFlags];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+const TextInputHandler::ModifierKey*
+TextInputHandler::GetModifierKeyForNativeKeyCode(
+ unsigned short aKeyCode) const {
+ for (ModifierKeyArray::index_type i = 0; i < mModifierKeys.Length(); ++i) {
+ if (mModifierKeys[i].keyCode == aKeyCode) {
+ return &((ModifierKey&)mModifierKeys[i]);
+ }
+ }
+ return nullptr;
+}
+
+const TextInputHandler::ModifierKey*
+TextInputHandler::GetModifierKeyForDeviceDependentFlags(
+ NSUInteger aFlags) const {
+ for (ModifierKeyArray::index_type i = 0; i < mModifierKeys.Length(); ++i) {
+ if (mModifierKeys[i].GetDeviceDependentFlags() ==
+ (aFlags & ~NSEventModifierFlagDeviceIndependentFlagsMask)) {
+ return &((ModifierKey&)mModifierKeys[i]);
+ }
+ }
+ return nullptr;
+}
+
+void TextInputHandler::DispatchKeyEventForFlagsChanged(NSEvent* aNativeEvent,
+ bool aDispatchKeyDown) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return;
+ }
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::DispatchKeyEventForFlagsChanged, aNativeEvent=%p, "
+ "type=%s, keyCode=%s (0x%X), aDispatchKeyDown=%s, IsIMEComposing()=%s",
+ this, aNativeEvent, GetNativeKeyEventType(aNativeEvent),
+ GetKeyNameForNativeKeyCode([aNativeEvent keyCode]),
+ [aNativeEvent keyCode], TrueOrFalse(aDispatchKeyDown),
+ TrueOrFalse(IsIMEComposing())));
+
+ if ([aNativeEvent type] != NSEventTypeFlagsChanged) {
+ return;
+ }
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Error,
+ ("%p TextInputHandler::DispatchKeyEventForFlagsChanged, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ EventMessage message = aDispatchKeyDown ? eKeyDown : eKeyUp;
+
+ // Fire a key event for the modifier key. Note that even if modifier key
+ // is pressed during composition, we shouldn't mark the keyboard event as
+ // "processed by IME" since neither Chrome for macOS nor Safari does it.
+ WidgetKeyboardEvent keyEvent(true, message, mWidget);
+ InitKeyEvent(aNativeEvent, keyEvent, false);
+
+ KeyEventState currentKeyEvent(aNativeEvent);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mDispatcher->DispatchKeyboardEvent(message, keyEvent, status,
+ &currentKeyEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandler::InsertText(NSString* aString,
+ NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return;
+ }
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::InsertText, aString=\"%s\", "
+ "aReplacementRange=%p { location=%lu, length=%lu }, "
+ "IsIMEComposing()=%s, "
+ "keyevent=%p, keydownDispatched=%s, "
+ "keydownHandled=%s, keypressDispatched=%s, "
+ "causedOtherKeyEvents=%s, compositionDispatched=%s",
+ this, GetCharacters(aString), aReplacementRange,
+ static_cast<unsigned long>(
+ aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length
+ : 0),
+ TrueOrFalse(IsIMEComposing()),
+ currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched)
+ : "N/A"));
+
+ InputContext context = mWidget->GetInputContext();
+ bool isEditable = (context.mIMEState.mEnabled == IMEEnabled::Enabled ||
+ context.mIMEState.mEnabled == IMEEnabled::Password);
+ NSRange selectedRange = SelectedRange();
+
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString(aString, str);
+
+ AutoInsertStringClearer clearer(currentKeyEvent);
+ if (currentKeyEvent) {
+ currentKeyEvent->mInsertString = &str;
+ }
+
+ if (!IsIMEComposing() && str.IsEmpty()) {
+ // nothing to do if there is no content which can be removed.
+ if (!isEditable) {
+ return;
+ }
+ // If replacement range is specified, we need to remove the range.
+ // Otherwise, we need to remove the selected range if it's not collapsed.
+ if (aReplacementRange && aReplacementRange->location != NSNotFound) {
+ // nothing to do since the range is collapsed.
+ if (aReplacementRange->length == 0) {
+ return;
+ }
+ // If the replacement range is different from current selected range,
+ // select the range.
+ if (!NSEqualRanges(selectedRange, *aReplacementRange)) {
+ NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
+ }
+ selectedRange = SelectedRange();
+ }
+ NS_ENSURE_TRUE_VOID(selectedRange.location != NSNotFound);
+ if (selectedRange.length == 0) {
+ return; // nothing to do
+ }
+ // If this is caused by a key input, the keypress event which will be
+ // dispatched later should cause the delete. Therefore, nothing to do here.
+ // Although, we're not sure if such case is actually possible.
+ if (!currentKeyEvent) {
+ return;
+ }
+
+ // When current keydown event causes this empty text input, let's
+ // dispatch eKeyDown event before any other events. Note that if we're
+ // in a composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ // XXX Should we mark this eKeyDown event as "processed by IME"?
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::InsertText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+
+ // Delete the selected range.
+ WidgetContentCommandEvent deleteCommandEvent(true, eContentCommandDelete,
+ mWidget);
+ DispatchEvent(deleteCommandEvent);
+ NS_ENSURE_TRUE_VOID(deleteCommandEvent.mSucceeded);
+ // Be aware! The widget might be destroyed here.
+ return;
+ }
+
+ bool isReplacingSpecifiedRange =
+ isEditable && aReplacementRange &&
+ aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(selectedRange, *aReplacementRange);
+
+ // If this is not caused by pressing a key, there is a composition or
+ // replacing a range which is different from current selection, let's
+ // insert the text as committing a composition.
+ // If InsertText() is called two or more times, we should insert all
+ // text with composition events.
+ // XXX When InsertText() is called multiple times, Chromium dispatches
+ // only one composition event. So, we need to store InsertText()
+ // calls and flush later.
+ if (!currentKeyEvent || currentKeyEvent->mCompositionDispatched ||
+ IsIMEComposing() || isReplacingSpecifiedRange) {
+ InsertTextAsCommittingComposition(aString, aReplacementRange);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ return;
+ }
+
+ // Don't let the same event be fired twice when hitting
+ // enter/return for Bug 420502. However, Korean IME (or some other
+ // simple IME) may work without marked text. For example, composing
+ // character may be inserted as committed text and it's modified with
+ // aReplacementRange. When a keydown starts new composition with
+ // committing previous character, InsertText() may be called twice,
+ // one is for committing previous character and then, inserting new
+ // composing character as committed character. In the latter case,
+ // |CanDispatchKeyPressEvent()| returns true but we need to dispatch
+ // keypress event for the new character. So, when IME tries to insert
+ // printable characters, we should ignore current key event state even
+ // after the keydown has already caused dispatching composition event.
+ // XXX Anyway, we should sort out around this at fixing bug 1338460.
+ if (currentKeyEvent && !currentKeyEvent->CanDispatchKeyPressEvent() &&
+ (str.IsEmpty() || (str.Length() == 1 && !IsPrintableChar(str[0])))) {
+ return;
+ }
+
+ // This is the normal path to input a character when you press a key.
+ // Let's dispatch eKeyDown event now.
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::InsertText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+
+ // XXX Shouldn't we hold mDispatcher instead of mWidget?
+ RefPtr<nsChildView> widget(mWidget);
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error,
+ ("%p TextInputHandler::InsertText, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info, ("%p TextInputHandler::InsertText, "
+ "maybe dispatches eKeyPress event "
+ "without control, alt and meta modifiers",
+ this));
+
+ // Dispatch keypress event with char instead of compositionchange event
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ // XXX Why do we need to dispatch keypress event for not inputting any
+ // string? If it wants to delete the specified range, should we
+ // dispatch an eContentCommandDelete event instead? Because this
+ // must not be caused by a key operation, a part of IME's processing.
+
+ // Don't set other modifiers from the current event, because here in
+ // -insertText: they've already been taken into account in creating
+ // the input string.
+
+ if (currentKeyEvent) {
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+ } else {
+ nsCocoaUtils::InitInputEvent(keypressEvent, static_cast<NSEvent*>(nullptr));
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ keypressEvent.mKeyValue = str;
+ // FYI: TextEventDispatcher will set mKeyCode to 0 for printable key's
+ // keypress events even if they don't cause inputting non-empty string.
+ }
+
+ // TODO:
+ // If mCurrentKeyEvent.mKeyEvent is null, the text should be inputted as
+ // composition events.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ bool keyPressDispatched = mDispatcher->MaybeDispatchKeypressEvents(
+ keypressEvent, status, currentKeyEvent);
+ bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+ // Note: mWidget might have become null here. Don't count on it from here on.
+
+ if (currentKeyEvent) {
+ currentKeyEvent->mKeyPressHandled = keyPressHandled;
+ currentKeyEvent->mKeyPressDispatched = keyPressDispatched;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool TextInputHandler::HandleCommand(Command aCommand) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ return false;
+ }
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::HandleCommand, "
+ "aCommand=%s, IsIMEComposing()=%s, "
+ "keyevent=%p, keydownHandled=%s, keypressDispatched=%s, "
+ "causedOtherKeyEvents=%s, compositionDispatched=%s",
+ this, ToChar(aCommand), TrueOrFalse(IsIMEComposing()),
+ currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched)
+ : "N/A"));
+
+ // The command shouldn't be handled, let's ignore it.
+ if (currentKeyEvent && !currentKeyEvent->CanHandleCommand()) {
+ return false;
+ }
+
+ // When current keydown event causes this command, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return false;
+ }
+
+ // If it's in composition, we cannot dispatch keypress event.
+ // Therefore, we should use different approach or give up to handle
+ // the command.
+ if (IsIMEComposing()) {
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ case Command::InsertParagraph: {
+ // Insert '\n' as committing composition.
+ // Otherwise, we need to dispatch keypress event because HTMLEditor
+ // doesn't treat "\n" in composition string as a line break unless
+ // the whitespace is treated as pre (see bug 1350541). In strictly
+ // speaking, we should dispatch keypress event as-is if it's handling
+ // NSEventTypeKeyDown event or should insert it with committing
+ // composition.
+ InsertTextAsCommittingComposition(@"\n", nullptr);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ return true;
+ }
+ case Command::DeleteCharBackward:
+ case Command::DeleteCharForward:
+ case Command::DeleteToBeginningOfLine:
+ case Command::DeleteWordBackward:
+ case Command::DeleteWordForward:
+ // Don't remove any contents during composition.
+ return false;
+ case Command::InsertTab:
+ case Command::InsertBacktab:
+ // Don't move focus during composition.
+ return false;
+ case Command::CharNext:
+ case Command::SelectCharNext:
+ case Command::WordNext:
+ case Command::SelectWordNext:
+ case Command::EndLine:
+ case Command::SelectEndLine:
+ case Command::CharPrevious:
+ case Command::SelectCharPrevious:
+ case Command::WordPrevious:
+ case Command::SelectWordPrevious:
+ case Command::BeginLine:
+ case Command::SelectBeginLine:
+ case Command::LinePrevious:
+ case Command::SelectLinePrevious:
+ case Command::MoveTop:
+ case Command::LineNext:
+ case Command::SelectLineNext:
+ case Command::MoveBottom:
+ case Command::SelectBottom:
+ case Command::SelectPageUp:
+ case Command::SelectPageDown:
+ case Command::ScrollBottom:
+ case Command::ScrollTop:
+ // Don't move selection during composition.
+ return false;
+ case Command::CancelOperation:
+ case Command::Complete:
+ // Don't handle Escape key by ourselves during composition.
+ return false;
+ case Command::ScrollPageUp:
+ case Command::ScrollPageDown:
+ // Allow to scroll.
+ break;
+ default:
+ break;
+ }
+ }
+
+ RefPtr<nsChildView> widget(mWidget);
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Error,
+ ("%p, TextInputHandler::HandleCommand, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ // TODO: If it's not appropriate keypress but user customized the OS
+ // settings to do the command with other key, we should just set
+ // command to the keypress event and it should be handled as
+ // the key press in editor.
+
+ // If it's handling actual key event and hasn't cause any composition
+ // events nor other key events, we should expose actual modifier state.
+ // Otherwise, we should adjust Control, Option and Command state since
+ // editor may behave differently if some of them are active.
+ bool dispatchFakeKeyPress =
+ !(currentKeyEvent && currentKeyEvent->IsProperKeyEvent(aCommand));
+
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget);
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ if (!dispatchFakeKeyPress) {
+ // If we're acutally handling a key press, we should dispatch
+ // the keypress event as-is.
+ currentKeyEvent->InitKeyEvent(this, keydownEvent, false);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+ } else {
+ // Otherwise, we should dispatch "fake" keypress event.
+ // However, for making it possible to compute edit commands, we need to
+ // set current native key event to the fake keyboard event even if it's
+ // not same as what we expect since the native keyboard event caused
+ // this command.
+ NSEvent* keyEvent = currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr;
+ keydownEvent.mNativeKeyEvent = keypressEvent.mNativeKeyEvent = keyEvent;
+ NS_WARNING_ASSERTION(
+ keypressEvent.mNativeKeyEvent,
+ "Without native key event, NativeKeyBindings cannot compute aCommand");
+ switch (aCommand) {
+ case Command::InsertLineBreak:
+ case Command::InsertParagraph: {
+ // Although, Shift+Enter and Enter are work differently in HTML
+ // editor, we should expose actual Shift state if it's caused by
+ // Enter key for compatibility with Chromium. Chromium breaks
+ // line in HTML editor with default pargraph separator when Enter
+ // is pressed, with <br> element when Shift+Enter. Safari breaks
+ // line in HTML editor with default paragraph separator when
+ // Enter, Shift+Enter or Option+Enter. So, we should not change
+ // Shift+Enter meaning when there was composition string or not.
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_RETURN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Enter;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::InsertLineBreak) {
+ // In default settings, Ctrl + Enter causes insertLineBreak command.
+ // So, let's make Ctrl state active of the keypress event.
+ keypressEvent.mModifiers |= MODIFIER_CONTROL;
+ }
+ break;
+ }
+ case Command::InsertTab:
+ case Command::InsertBacktab:
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_TAB;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Tab;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::InsertBacktab) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ case Command::DeleteCharBackward:
+ case Command::DeleteToBeginningOfLine:
+ case Command::DeleteWordBackward: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_BACK;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Backspace;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::DeleteToBeginningOfLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ } else if (aCommand == Command::DeleteWordBackward) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ case Command::DeleteCharForward:
+ case Command::DeleteWordForward: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_DELETE;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Delete;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::DeleteWordForward) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ case Command::CharNext:
+ case Command::SelectCharNext:
+ case Command::WordNext:
+ case Command::SelectWordNext:
+ case Command::EndLine:
+ case Command::SelectEndLine: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_RIGHT;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowRight;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectCharNext ||
+ aCommand == Command::SelectWordNext ||
+ aCommand == Command::SelectEndLine) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::WordNext ||
+ aCommand == Command::SelectWordNext) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ if (aCommand == Command::EndLine ||
+ aCommand == Command::SelectEndLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::CharPrevious:
+ case Command::SelectCharPrevious:
+ case Command::WordPrevious:
+ case Command::SelectWordPrevious:
+ case Command::BeginLine:
+ case Command::SelectBeginLine: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_LEFT;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowLeft;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectCharPrevious ||
+ aCommand == Command::SelectWordPrevious ||
+ aCommand == Command::SelectBeginLine) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::WordPrevious ||
+ aCommand == Command::SelectWordPrevious) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ if (aCommand == Command::BeginLine ||
+ aCommand == Command::SelectBeginLine) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::LinePrevious:
+ case Command::SelectLinePrevious:
+ case Command::MoveTop:
+ case Command::SelectTop: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_UP;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowUp;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectLinePrevious ||
+ aCommand == Command::SelectTop) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::MoveTop || aCommand == Command::SelectTop) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::LineNext:
+ case Command::SelectLineNext:
+ case Command::MoveBottom:
+ case Command::SelectBottom: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_DOWN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_ArrowDown;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectLineNext ||
+ aCommand == Command::SelectBottom) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ if (aCommand == Command::MoveBottom ||
+ aCommand == Command::SelectBottom) {
+ keypressEvent.mModifiers |= MODIFIER_META;
+ }
+ break;
+ }
+ case Command::ScrollPageUp:
+ case Command::SelectPageUp: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_PAGE_UP;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageUp;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectPageUp) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ }
+ case Command::ScrollPageDown:
+ case Command::SelectPageDown: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_PAGE_DOWN;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_PageDown;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::SelectPageDown) {
+ keypressEvent.mModifiers |= MODIFIER_SHIFT;
+ }
+ break;
+ }
+ case Command::ScrollBottom:
+ case Command::ScrollTop: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ if (aCommand == Command::ScrollBottom) {
+ keypressEvent.mKeyCode = NS_VK_END;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_End;
+ } else {
+ keypressEvent.mKeyCode = NS_VK_HOME;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Home;
+ }
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ break;
+ }
+ case Command::CancelOperation:
+ case Command::Complete: {
+ nsCocoaUtils::InitInputEvent(keypressEvent, keyEvent);
+ keypressEvent.mKeyCode = NS_VK_ESCAPE;
+ keypressEvent.mKeyNameIndex = KEY_NAME_INDEX_Escape;
+ keypressEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ if (aCommand == Command::Complete) {
+ keypressEvent.mModifiers |= MODIFIER_ALT;
+ }
+ break;
+ }
+ default:
+ return false;
+ }
+
+ nsCocoaUtils::InitInputEvent(keydownEvent, keyEvent);
+ keydownEvent.mKeyCode = keypressEvent.mKeyCode;
+ keydownEvent.mKeyNameIndex = keypressEvent.mKeyNameIndex;
+ keydownEvent.mModifiers = keypressEvent.mModifiers;
+ }
+
+ // We've stopped dispatching "keypress" events of non-printable keys on
+ // the web. Therefore, we need to dispatch eKeyDown event here for web
+ // apps. This is non-standard behavior if we've already dispatched a
+ // "keydown" event. However, Chrome also dispatches such fake "keydown"
+ // (and "keypress") event for making same behavior as Safari.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ if (mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status,
+ nullptr)) {
+ bool keydownHandled = status == nsEventStatus_eConsumeNoDefault;
+ if (currentKeyEvent) {
+ currentKeyEvent->mKeyDownDispatched = true;
+ currentKeyEvent->mKeyDownHandled |= keydownHandled;
+ }
+ if (keydownHandled) {
+ // Don't dispatch eKeyPress event if preceding eKeyDown event is
+ // consumed for conforming to UI Events.
+ // XXX Perhaps, we should ignore previous eKeyDown event result
+ // even if we've already dispatched because it may notify web apps
+ // of different key information, e.g., it's handled by IME, but
+ // web apps want to handle only this key.
+ return true;
+ }
+ }
+
+ bool keyPressDispatched = mDispatcher->MaybeDispatchKeypressEvents(
+ keypressEvent, status, currentKeyEvent);
+ bool keyPressHandled = (status == nsEventStatus_eConsumeNoDefault);
+
+ // NOTE: mWidget might have become null here.
+
+ if (keyPressDispatched) {
+ // Record the keypress event state only when it dispatched actual Enter
+ // keypress event because in other cases, the keypress event just a
+ // messenger. E.g., if it's caused by different key, keypress event for
+ // the actual key should be dispatched.
+ if (!dispatchFakeKeyPress && currentKeyEvent) {
+ currentKeyEvent->mKeyPressHandled = keyPressHandled;
+ currentKeyEvent->mKeyPressDispatched = keyPressDispatched;
+ }
+ return true;
+ }
+
+ // If keypress event isn't dispatched as expected, we should fallback to
+ // using composition events.
+ if (aCommand == Command::InsertLineBreak ||
+ aCommand == Command::InsertParagraph) {
+ InsertTextAsCommittingComposition(@"\n", nullptr);
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+ }
+ return true;
+ }
+
+ return false;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool TextInputHandler::DoCommandBySelector(const char* aSelector) {
+ RefPtr<nsChildView> widget(mWidget);
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::DoCommandBySelector, aSelector=\"%s\", "
+ "Destroyed()=%s, keydownDispatched=%s, keydownHandled=%s, "
+ "keypressDispatched=%s, keypressHandled=%s, causedOtherKeyEvents=%s",
+ this, aSelector ? aSelector : "", TrueOrFalse(Destroyed()),
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents)
+ : "N/A"));
+
+ // If the command isn't caused by key operation, the command should
+ // be handled in the super class of the caller.
+ if (!currentKeyEvent) {
+ return Destroyed();
+ }
+
+ // When current keydown event causes this command, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ RefPtr<TextInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return true;
+ }
+
+ // If the key operation causes this command, should dispatch a keypress
+ // event.
+ // XXX This must be worng. Even if this command is caused by the key
+ // operation, its our default action can be different from the
+ // command. So, in this case, we should dispatch a keypress event
+ // which have the command and editor should handle it.
+ if (currentKeyEvent->CanDispatchKeyPressEvent()) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Error,
+ ("%p TextInputHandler::DoCommandBySelector, "
+ "FAILED, due to BeginNativeInputTransaction() failure "
+ "at dispatching keypress",
+ this));
+ return Destroyed();
+ }
+
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, widget);
+ currentKeyEvent->InitKeyEvent(this, keypressEvent, false);
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ currentKeyEvent->mKeyPressDispatched =
+ mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status,
+ currentKeyEvent);
+ currentKeyEvent->mKeyPressHandled =
+ (status == nsEventStatus_eConsumeNoDefault);
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandler::DoCommandBySelector, keypress event "
+ "dispatched, Destroyed()=%s, keypressHandled=%s",
+ this, TrueOrFalse(Destroyed()),
+ TrueOrFalse(currentKeyEvent->mKeyPressHandled)));
+ // This command is now dispatched with keypress event.
+ // So, this shouldn't be handled by nobody anymore.
+ return true;
+ }
+
+ // If the key operation didn't cause keypress event or caused keypress event
+ // but not prevented its default, we need to honor the command. For example,
+ // Korean IME sends "insertNewline:" when committing existing composition
+ // with Enter key press. In such case, the key operation has been consumed
+ // by the committing composition but we still need to handle the command.
+ if (Destroyed() || !currentKeyEvent->CanHandleCommand()) {
+ return true;
+ }
+
+ // cancelOperation: command is fired after Escape or Command + Period.
+ // However, if ChildView implements cancelOperation:, calling
+ // [[ChildView super] doCommandBySelector:aSelector] when Command + Period
+ // causes only a call of [ChildView cancelOperation:sender]. I.e.,
+ // [ChildView keyDown:theEvent] becomes to be never called. For avoiding
+ // this odd behavior, we need to handle the command before super class of
+ // ChildView only when current key event is proper event to fire Escape
+ // keypress event.
+ if (!strcmp(aSelector, "cancelOperation:") && currentKeyEvent &&
+ currentKeyEvent->IsProperKeyEvent(Command::CancelOperation)) {
+ return HandleCommand(Command::CancelOperation);
+ }
+
+ // Otherwise, we've not handled the command yet. Propagate the command
+ // to the super class of ChildView.
+ return false;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation (static methods)
+ *
+ ******************************************************************************/
+
+bool IMEInputHandler::sStaticMembersInitialized = false;
+bool IMEInputHandler::sCachedIsForRTLLangage = false;
+CFStringRef IMEInputHandler::sLatestIMEOpenedModeInputSourceID = nullptr;
+IMEInputHandler* IMEInputHandler::sFocusedIMEHandler = nullptr;
+
+// static
+void IMEInputHandler::InitStaticMembers() {
+ if (sStaticMembersInitialized) return;
+ sStaticMembersInitialized = true;
+ // We need to check the keyboard layout changes on all applications.
+ CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
+ // XXX Don't we need to remove the observer at shut down?
+ // Mac Dev Center's document doesn't say how to remove the observer if
+ // the second parameter is NULL.
+ ::CFNotificationCenterAddObserver(
+ center, NULL, OnCurrentTextInputSourceChange,
+ kTISNotifySelectedKeyboardInputSourceChanged, NULL,
+ CFNotificationSuspensionBehaviorDeliverImmediately);
+ // Initiailize with the current keyboard layout
+ OnCurrentTextInputSourceChange(
+ NULL, NULL, kTISNotifySelectedKeyboardInputSourceChanged, NULL, NULL);
+}
+
+// static
+void IMEInputHandler::OnCurrentTextInputSourceChange(
+ CFNotificationCenterRef aCenter, void* aObserver, CFStringRef aName,
+ const void* aObject, CFDictionaryRef aUserInfo) {
+ // Cache the latest IME opened mode to sLatestIMEOpenedModeInputSourceID.
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ if (tis.IsOpenedIMEMode()) {
+ tis.GetInputSourceID(sLatestIMEOpenedModeInputSourceID);
+ // Collect Input Source ID which includes input mode in most cases.
+ // However, if it's Japanese IME, collecting input mode (e.g.,
+ // "HiraganaKotei") does not make sense because in most languages,
+ // input mode changes "how to input", but Japanese IME changes
+ // "which type of characters to input". I.e., only Japanese IME
+ // users may use multiple input modes. If we'd collect each type of
+ // input mode of Japanese IMEs, it'd be difficult to count actual
+ // users of each IME from the result. So, only when active IME is
+ // a Japanese IME, we should use Bundle ID which does not contain
+ // input mode instead.
+ nsAutoString key;
+ if (tis.IsForJapaneseLanguage()) {
+ tis.GetBundleID(key);
+ } else {
+ tis.GetInputSourceID(key);
+ }
+ // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
+ if (key.Length() > 72) {
+ if (NS_IS_SURROGATE_PAIR(key[72 - 2], key[72 - 1])) {
+ key.Truncate(72 - 2);
+ } else {
+ key.Truncate(72 - 1);
+ }
+ // U+2026 is "..."
+ key.Append(char16_t(0x2026));
+ }
+ Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_MAC, key,
+ true);
+ }
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ static CFStringRef sLastTIS = nullptr;
+ CFStringRef newTIS;
+ tis.GetInputSourceID(newTIS);
+ if (!sLastTIS ||
+ ::CFStringCompare(sLastTIS, newTIS, 0) != kCFCompareEqualTo) {
+ TISInputSourceWrapper tis1, tis2, tis3, tis4, tis5;
+ tis1.InitByCurrentKeyboardLayout();
+ tis2.InitByCurrentASCIICapableInputSource();
+ tis3.InitByCurrentASCIICapableKeyboardLayout();
+ tis4.InitByCurrentInputMethodKeyboardLayoutOverride();
+ tis5.InitByTISInputSourceRef(tis.GetKeyboardLayoutInputSource());
+ CFStringRef is0 = nullptr, is1 = nullptr, is2 = nullptr, is3 = nullptr,
+ is4 = nullptr, is5 = nullptr, type0 = nullptr,
+ lang0 = nullptr, bundleID0 = nullptr;
+ tis.GetInputSourceID(is0);
+ tis1.GetInputSourceID(is1);
+ tis2.GetInputSourceID(is2);
+ tis3.GetInputSourceID(is3);
+ tis4.GetInputSourceID(is4);
+ tis5.GetInputSourceID(is5);
+ tis.GetInputSourceType(type0);
+ tis.GetPrimaryLanguage(lang0);
+ tis.GetBundleID(bundleID0);
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMEInputHandler::OnCurrentTextInputSourceChange,\n"
+ " Current Input Source is changed to:\n"
+ " currentInputContext=%p\n"
+ " %s\n"
+ " type=%s %s\n"
+ " overridden keyboard layout=%s\n"
+ " used keyboard layout for translation=%s\n"
+ " primary language=%s\n"
+ " bundle ID=%s\n"
+ " current ASCII capable Input Source=%s\n"
+ " current Keyboard Layout=%s\n"
+ " current ASCII capable Keyboard Layout=%s",
+ [NSTextInputContext currentInputContext], GetCharacters(is0),
+ GetCharacters(type0), tis.IsASCIICapable() ? "- ASCII capable " : "",
+ GetCharacters(is4), GetCharacters(is5), GetCharacters(lang0),
+ GetCharacters(bundleID0), GetCharacters(is2), GetCharacters(is1),
+ GetCharacters(is3)));
+ }
+ sLastTIS = newTIS;
+ }
+
+ /**
+ * When the direction is changed, all the children are notified.
+ * No need to treat the initial case separately because it is covered
+ * by the general case (sCachedIsForRTLLangage is initially false)
+ */
+ if (sCachedIsForRTLLangage != tis.IsForRTLLanguage()) {
+ WidgetUtils::SendBidiKeyboardInfoToContent();
+ sCachedIsForRTLLangage = tis.IsForRTLLanguage();
+ }
+}
+
+// static
+void IMEInputHandler::FlushPendingMethods(nsITimer* aTimer, void* aClosure) {
+ NS_ASSERTION(aClosure, "aClosure is null");
+ static_cast<IMEInputHandler*>(aClosure)->ExecutePendingMethods();
+}
+
+// static
+CFArrayRef IMEInputHandler::CreateAllIMEModeList() {
+ const void* keys[] = {kTISPropertyInputSourceType};
+ const void* values[] = {kTISTypeKeyboardInputMode};
+ CFDictionaryRef filter =
+ ::CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, NULL, NULL);
+ NS_ASSERTION(filter, "failed to create the filter");
+ CFArrayRef list = ::TISCreateInputSourceList(filter, true);
+ ::CFRelease(filter);
+ return list;
+}
+
+// static
+void IMEInputHandler::DebugPrintAllIMEModes() {
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ CFArrayRef list = CreateAllIMEModeList();
+ MOZ_LOG(gIMELog, LogLevel::Info, ("IME mode configuration:"));
+ CFIndex idx = ::CFArrayGetCount(list);
+ TISInputSourceWrapper tis;
+ for (CFIndex i = 0; i < idx; ++i) {
+ TISInputSourceRef inputSource = static_cast<TISInputSourceRef>(
+ const_cast<void*>(::CFArrayGetValueAtIndex(list, i)));
+ tis.InitByTISInputSourceRef(inputSource);
+ nsAutoString name, isid, bundleID;
+ tis.GetLocalizedName(name);
+ tis.GetInputSourceID(isid);
+ tis.GetBundleID(bundleID);
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ (" %s\t<%s>%s%s\n"
+ " bundled in <%s>\n",
+ NS_ConvertUTF16toUTF8(name).get(), NS_ConvertUTF16toUTF8(isid).get(),
+ tis.IsASCIICapable() ? "" : "\t(Isn't ASCII capable)",
+ tis.IsEnabled() ? "" : "\t(Isn't Enabled)",
+ NS_ConvertUTF16toUTF8(bundleID).get()));
+ }
+ ::CFRelease(list);
+ }
+}
+
+// static
+TSMDocumentID IMEInputHandler::GetCurrentTSMDocumentID() {
+ // At least on Mac OS X 10.6.x and 10.7.x, ::TSMGetActiveDocument() has a bug.
+ // The result of ::TSMGetActiveDocument() isn't modified for new active text
+ // input context until [NSTextInputContext currentInputContext] is called.
+ // Therefore, we need to call it here.
+ [NSTextInputContext currentInputContext];
+ return ::TSMGetActiveDocument();
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation #1
+ * The methods are releated to the pending methods. Some jobs should be
+ * run after the stack is finished, e.g, some methods cannot run the jobs
+ * during processing the focus event. And also some other jobs should be
+ * run at the next focus event is processed.
+ * The pending methods are recorded in mPendingMethods. They are executed
+ * by ExecutePendingMethods via FlushPendingMethods.
+ *
+ ******************************************************************************/
+
+nsresult IMEInputHandler::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) {
+ switch (aNotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ CommitIMEComposition();
+ return NS_OK;
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ CancelIMEComposition();
+ return NS_OK;
+ case NOTIFY_IME_OF_FOCUS:
+ if (IsFocused()) {
+ nsIWidget* widget = aTextEventDispatcher->GetWidget();
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("%p IMEInputHandler::NotifyIME(), IsFocused()=true, "
+ "widget=%p, IsPasswordEditor()=%s",
+ this, widget,
+ TrueOrFalse(widget &&
+ widget->GetInputContext().IsPasswordEditor())));
+ if (widget && widget->GetInputContext().IsPasswordEditor()) {
+ EnableSecureEventInput();
+ } else {
+ EnsureSecureEventInputDisabled();
+ }
+ } else if (MOZ_LOG_TEST(gIMELog, LogLevel::Debug)) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+ NSWindow* window = mView ? [mView window] : nil;
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("%p IMEInputHandler::NotifyIME(), IsFocused()=false, "
+ "Destroyed()=%s, mView=%p, "
+ "[mView window]=%p, firstResponder=%p, isKeyWindow=%s, "
+ "isActive=%s",
+ this, TrueOrFalse(Destroyed()), mView, window,
+ window ? [window firstResponder] : nil,
+ TrueOrFalse(window && [window isKeyWindow]),
+ TrueOrFalse([[NSApplication sharedApplication] isActive])));
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+ }
+ OnFocusChangeInGecko(true);
+ return NS_OK;
+ case NOTIFY_IME_OF_BLUR:
+ OnFocusChangeInGecko(false);
+ return NS_OK;
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ OnSelectionChange(aNotification);
+ return NS_OK;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ OnLayoutChange();
+ return NS_OK;
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+NS_IMETHODIMP_(IMENotificationRequests)
+IMEInputHandler::GetIMENotificationRequests() {
+ // XXX Shouldn't we move floating window which shows composition string
+ // when plugin has focus and its parent is scrolled or the window is
+ // moved?
+ return IMENotificationRequests();
+}
+
+NS_IMETHODIMP_(void)
+IMEInputHandler::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
+ // XXX When input transaction is being stolen by add-on, what should we do?
+}
+
+NS_IMETHODIMP_(void)
+IMEInputHandler::WillDispatchKeyboardEvent(
+ TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
+ void* aData) {
+ // If the keyboard event is not caused by a native key event, we can do
+ // nothing here.
+ if (!aData) {
+ return;
+ }
+
+ KeyEventState* currentKeyEvent = static_cast<KeyEventState*>(aData);
+ NSEvent* nativeEvent = currentKeyEvent->mKeyEvent;
+ nsAString* insertString = currentKeyEvent->mInsertString;
+ if (aKeyboardEvent.mMessage == eKeyPress && aIndexOfKeypress == 0 &&
+ (!insertString || insertString->IsEmpty())) {
+ // Inform the child process that this is an event that we want a reply
+ // from.
+ // XXX This should be called only when the target is a remote process.
+ // However, it's difficult to check it under widget/.
+ // So, let's do this here for now, then,
+ // EventStateManager::PreHandleEvent() will reset the flags if
+ // the event target isn't in remote process.
+ aKeyboardEvent.MarkAsWaitingReplyFromRemoteProcess();
+ }
+ if (KeyboardLayoutOverrideRef().mOverrideEnabled) {
+ TISInputSourceWrapper tis;
+ tis.InitByLayoutID(KeyboardLayoutOverrideRef().mKeyboardLayout, true);
+ tis.WillDispatchKeyboardEvent(nativeEvent, insertString, aIndexOfKeypress,
+ aKeyboardEvent);
+ } else {
+ TISInputSourceWrapper::CurrentInputSource().WillDispatchKeyboardEvent(
+ nativeEvent, insertString, aIndexOfKeypress, aKeyboardEvent);
+ }
+
+ // Remove basic modifiers from keypress event because if they are included
+ // but this causes inputting text, since TextEditor won't handle eKeyPress
+ // events whose ctrlKey, altKey or metaKey is true as text input.
+ // Note that this hack should be used only when an editor has focus because
+ // this is a hack for TextEditor and modifier key information may be
+ // important for current web app.
+ if (IsEditableContent() && insertString &&
+ aKeyboardEvent.mMessage == eKeyPress && aKeyboardEvent.mCharCode) {
+ aKeyboardEvent.mModifiers &=
+ ~(MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META);
+ }
+}
+
+void IMEInputHandler::NotifyIMEOfFocusChangeInGecko() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::NotifyIMEOfFocusChangeInGecko, "
+ "Destroyed()=%s, IsFocused()=%s, inputContext=%p",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()),
+ mView ? [mView inputContext] : nullptr));
+
+ if (Destroyed()) {
+ return;
+ }
+
+ if (!IsFocused()) {
+ // retry at next focus event
+ mPendingMethods |= kNotifyIMEOfFocusChangeInGecko;
+ return;
+ }
+
+ MOZ_ASSERT(mView);
+ NSTextInputContext* inputContext = [mView inputContext];
+ NS_ENSURE_TRUE_VOID(inputContext);
+
+ // When an <input> element on a XUL <panel> element gets focus from an <input>
+ // element on the opener window of the <panel> element, the owner window
+ // still has native focus. Therefore, IMEs may store the opener window's
+ // level at this time because they don't know the actual focus is moved to
+ // different window. If IMEs try to get the newest window level after the
+ // focus change, we return the window level of the XUL <panel>'s widget.
+ // Therefore, let's emulate the native focus change. Then, IMEs can refresh
+ // the stored window level.
+ [inputContext deactivate];
+ [inputContext activate];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::SyncASCIICapableOnly() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SyncASCIICapableOnly, "
+ "Destroyed()=%s, IsFocused()=%s, mIsASCIICapableOnly=%s, "
+ "GetCurrentTSMDocumentID()=%p",
+ this, TrueOrFalse(Destroyed()), TrueOrFalse(IsFocused()),
+ TrueOrFalse(mIsASCIICapableOnly), GetCurrentTSMDocumentID()));
+
+ if (Destroyed()) {
+ return;
+ }
+
+ if (!IsFocused()) {
+ // retry at next focus event
+ mPendingMethods |= kSyncASCIICapableOnly;
+ return;
+ }
+
+ TSMDocumentID doc = GetCurrentTSMDocumentID();
+ if (!doc) {
+ // retry
+ mPendingMethods |= kSyncASCIICapableOnly;
+ NS_WARNING("Application is active but there is no active document");
+ ResetTimer();
+ return;
+ }
+
+ if (mIsASCIICapableOnly) {
+ CFArrayRef ASCIICapableTISList = ::TISCreateASCIICapableInputSourceList();
+ ::TSMSetDocumentProperty(doc, kTSMDocumentEnabledInputSourcesPropertyTag,
+ sizeof(CFArrayRef), &ASCIICapableTISList);
+ ::CFRelease(ASCIICapableTISList);
+ } else {
+ ::TSMRemoveDocumentProperty(doc,
+ kTSMDocumentEnabledInputSourcesPropertyTag);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::ResetTimer() {
+ NS_ASSERTION(mPendingMethods != 0,
+ "There are not pending methods, why this is called?");
+ if (mTimer) {
+ mTimer->Cancel();
+ } else {
+ mTimer = NS_NewTimer();
+ NS_ENSURE_TRUE(mTimer, );
+ }
+ mTimer->InitWithNamedFuncCallback(FlushPendingMethods, this, 0,
+ nsITimer::TYPE_ONE_SHOT,
+ "IMEInputHandler::FlushPendingMethods");
+}
+
+void IMEInputHandler::ExecutePendingMethods() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ if (![[NSApplication sharedApplication] isActive]) {
+ // If we're not active, we should retry at focus event
+ return;
+ }
+
+ uint32_t pendingMethods = mPendingMethods;
+ // First, reset the pending method flags because if each methods cannot
+ // run now, they can reentry to the pending flags by theirselves.
+ mPendingMethods = 0;
+
+ if (pendingMethods & kSyncASCIICapableOnly) SyncASCIICapableOnly();
+ if (pendingMethods & kNotifyIMEOfFocusChangeInGecko) {
+ NotifyIMEOfFocusChangeInGecko();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation (native event handlers)
+ *
+ ******************************************************************************/
+
+TextRangeType IMEInputHandler::ConvertToTextRangeType(uint32_t aUnderlineStyle,
+ NSRange& aSelectedRange) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::ConvertToTextRangeType, "
+ "aUnderlineStyle=%u, aSelectedRange.length=%lu,",
+ this, aUnderlineStyle,
+ static_cast<unsigned long>(aSelectedRange.length)));
+
+ // We assume that aUnderlineStyle is NSUnderlineStyleSingle or
+ // NSUnderlineStyleThick. NSUnderlineStyleThick should indicate a selected
+ // clause. Otherwise, should indicate non-selected clause.
+
+ if (aSelectedRange.length == 0) {
+ switch (aUnderlineStyle) {
+ case NSUnderlineStyleSingle:
+ return TextRangeType::eRawClause;
+ case NSUnderlineStyleThick:
+ return TextRangeType::eSelectedRawClause;
+ default:
+ NS_WARNING("Unexpected line style");
+ return TextRangeType::eSelectedRawClause;
+ }
+ }
+
+ switch (aUnderlineStyle) {
+ case NSUnderlineStyleSingle:
+ return TextRangeType::eConvertedClause;
+ case NSUnderlineStyleThick:
+ return TextRangeType::eSelectedClause;
+ default:
+ NS_WARNING("Unexpected line style");
+ return TextRangeType::eSelectedClause;
+ }
+}
+
+uint32_t IMEInputHandler::GetRangeCount(NSAttributedString* aAttrString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Iterate through aAttrString for the NSUnderlineStyleAttributeName and
+ // count the different segments adjusting limitRange as we go.
+ uint32_t count = 0;
+ NSRange effectiveRange;
+ NSRange limitRange = NSMakeRange(0, [aAttrString length]);
+ while (limitRange.length > 0) {
+ [aAttrString attribute:NSUnderlineStyleAttributeName
+ atIndex:limitRange.location
+ longestEffectiveRange:&effectiveRange
+ inRange:limitRange];
+ limitRange =
+ NSMakeRange(NSMaxRange(effectiveRange),
+ NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
+ count++;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetRangeCount, aAttrString=\"%s\", count=%u",
+ this, GetCharacters([aAttrString string]), count));
+
+ return count;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(0);
+}
+
+already_AddRefed<mozilla::TextRangeArray> IMEInputHandler::CreateTextRangeArray(
+ NSAttributedString* aAttrString, NSRange& aSelectedRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ RefPtr<mozilla::TextRangeArray> textRangeArray =
+ new mozilla::TextRangeArray();
+
+ // Note that we shouldn't append ranges when composition string
+ // is empty because it may cause TextComposition confused.
+ if (![aAttrString length]) {
+ return textRangeArray.forget();
+ }
+
+ // Convert the Cocoa range into the TextRange Array used in Gecko.
+ // Iterate through the attributed string and map the underline attribute to
+ // Gecko IME textrange attributes. We may need to change the code here if
+ // we change the implementation of validAttributesForMarkedText.
+ NSRange limitRange = NSMakeRange(0, [aAttrString length]);
+ uint32_t rangeCount = GetRangeCount(aAttrString);
+ for (uint32_t i = 0; i < rangeCount && limitRange.length > 0; i++) {
+ NSRange effectiveRange;
+ id attributeValue = [aAttrString attribute:NSUnderlineStyleAttributeName
+ atIndex:limitRange.location
+ longestEffectiveRange:&effectiveRange
+ inRange:limitRange];
+
+ TextRange range;
+ range.mStartOffset = effectiveRange.location;
+ range.mEndOffset = NSMaxRange(effectiveRange);
+ range.mRangeType =
+ ConvertToTextRangeType([attributeValue intValue], aSelectedRange);
+ textRangeArray->AppendElement(range);
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CreateTextRangeArray, "
+ "range={ mStartOffset=%u, mEndOffset=%u, mRangeType=%s }",
+ this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
+
+ limitRange =
+ NSMakeRange(NSMaxRange(effectiveRange),
+ NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
+ }
+
+ // Get current caret position.
+ TextRange range;
+ range.mStartOffset = aSelectedRange.location + aSelectedRange.length;
+ range.mEndOffset = range.mStartOffset;
+ range.mRangeType = TextRangeType::eCaret;
+ textRangeArray->AppendElement(range);
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CreateTextRangeArray, "
+ "range={ mStartOffset=%u, mEndOffset=%u, mRangeType=%s }",
+ this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
+
+ return textRangeArray.forget();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+bool IMEInputHandler::DispatchCompositionStartEvent() {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "mSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, "
+ "mView=%p, mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
+ this, static_cast<unsigned long>(SelectedRange().location),
+ static_cast<unsigned long>(mSelectedRange.length),
+ TrueOrFalse(Destroyed()), mView, mWidget,
+ mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ NS_ASSERTION(!mIsIMEComposing, "There is a composition already");
+ mIsIMEComposing = true;
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ mIsDeadKeyComposing = currentKeyEvent && currentKeyEvent->mKeyEvent &&
+ TISInputSourceWrapper::CurrentInputSource().IsDeadKey(
+ currentKeyEvent->mKeyEvent);
+
+ nsEventStatus status;
+ rv = mDispatcher->StartComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "FAILED, due to StartComposition() failure",
+ this));
+ return false;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionStartEvent, "
+ "destroyed by compositionstart event",
+ this));
+ return false;
+ }
+
+ // FYI: compositionstart may cause committing composition by the webapp.
+ if (!mIsIMEComposing) {
+ return false;
+ }
+
+ // FYI: The selection range might have been modified by a compositionstart
+ // event handler.
+ mIMECompositionStart = SelectedRange().location;
+ return true;
+}
+
+bool IMEInputHandler::DispatchCompositionChangeEvent(
+ const nsString& aText, NSAttributedString* aAttrString,
+ NSRange& aSelectedRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "aText=\"%s\", aAttrString=\"%s\", "
+ "aSelectedRange={ location=%lu, length=%lu }, Destroyed()=%s, mView=%p, "
+ "mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
+ this, NS_ConvertUTF16toUTF8(aText).get(),
+ GetCharacters([aAttrString string]),
+ static_cast<unsigned long>(aSelectedRange.location),
+ static_cast<unsigned long>(aSelectedRange.length),
+ TrueOrFalse(Destroyed()), mView, mWidget,
+ mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ NS_ENSURE_TRUE(!Destroyed(), false);
+
+ NS_ASSERTION(mIsIMEComposing, "We're not in composition");
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ RefPtr<TextRangeArray> rangeArray =
+ CreateTextRangeArray(aAttrString, aSelectedRange);
+
+ rv = mDispatcher->SetPendingComposition(aText, rangeArray);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to SetPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ mSelectedRange.location = mIMECompositionStart + aSelectedRange.location;
+ mSelectedRange.length = aSelectedRange.length;
+
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ }
+ mIMECompositionString = [[aAttrString string] retain];
+
+ nsEventStatus status;
+ rv = mDispatcher->FlushPendingComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "FAILED, due to FlushPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionChangeEvent, "
+ "destroyed by compositionchange event",
+ this));
+ return false;
+ }
+
+ // FYI: compositionstart may cause committing composition by the webapp.
+ return mIsIMEComposing;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::DispatchCompositionCommitEvent(
+ const nsAString* aCommitString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "aCommitString=0x%p (\"%s\"), Destroyed()=%s, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s",
+ this, aCommitString,
+ aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "",
+ TrueOrFalse(Destroyed()), mView, mWidget,
+ mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
+
+ NS_ASSERTION(mIsIMEComposing, "We're not in composition");
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ if (!Destroyed()) {
+ // IME may query selection immediately after this, however, in e10s mode,
+ // OnSelectionChange() will be called asynchronously. Until then, we
+ // should emulate expected selection range if the webapp does nothing.
+ mSelectedRange.location = mIMECompositionStart;
+ if (aCommitString) {
+ mSelectedRange.location += aCommitString->Length();
+ } else if (mIMECompositionString) {
+ nsAutoString commitString;
+ nsCocoaUtils::GetStringForNSString(mIMECompositionString, commitString);
+ mSelectedRange.location += commitString.Length();
+ }
+ mSelectedRange.length = 0;
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ } else {
+ nsEventStatus status;
+ rv = mDispatcher->CommitComposition(status, aCommitString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ }
+ }
+ }
+
+ mIsIMEComposing = mIsDeadKeyComposing = false;
+ mIMECompositionStart = UINT32_MAX;
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ mIMECompositionString = nullptr;
+ }
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::DispatchCompositionCommitEvent, "
+ "destroyed by compositioncommit event",
+ this));
+ return false;
+ }
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::MaybeDispatchCurrentKeydownEvent(bool aIsProcessedByIME) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (Destroyed()) {
+ return false;
+ }
+ MOZ_ASSERT(mWidget);
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+ if (!currentKeyEvent || !currentKeyEvent->CanDispatchKeyDownEvent()) {
+ return true;
+ }
+
+ NSEvent* nativeEvent = currentKeyEvent->mKeyEvent;
+ if (NS_WARN_IF(!nativeEvent) || [nativeEvent type] != NSEventTypeKeyDown) {
+ return true;
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, aIsProcessedByIME=%s "
+ "currentKeyEvent={ mKeyEvent(%p)={ type=%s, keyCode=%s (0x%X) } }, "
+ "aIsProcessedBy=%s, IsDeadKeyComposing()=%s",
+ this, TrueOrFalse(aIsProcessedByIME), nativeEvent,
+ GetNativeKeyEventType(nativeEvent),
+ GetKeyNameForNativeKeyCode([nativeEvent keyCode]), [nativeEvent keyCode],
+ TrueOrFalse(IsIMEComposing()), TrueOrFalse(IsDeadKeyComposing())));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+ RefPtr<TextEventDispatcher> dispatcher(mDispatcher);
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("%p IMEInputHandler::DispatchKeyEventForFlagsChanged, "
+ "FAILED, due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ NSResponder* firstResponder = [[mView window] firstResponder];
+
+ // Mark currentKeyEvent as "dispatched eKeyDown event" and actually do it.
+ currentKeyEvent->mKeyDownDispatched = true;
+
+ RefPtr<nsChildView> widget(mWidget);
+
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, widget);
+ // Don't mark the eKeyDown event as "processed by IME" if the composition
+ // is started with dead key.
+ currentKeyEvent->InitKeyEvent(this, keydownEvent,
+ aIsProcessedByIME && !IsDeadKeyComposing());
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ dispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status,
+ currentKeyEvent);
+ currentKeyEvent->mKeyDownHandled =
+ (status == nsEventStatus_eConsumeNoDefault);
+
+ if (Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, "
+ "widget was destroyed by keydown event",
+ this));
+ return false;
+ }
+
+ // The key down event may have shifted the focus, in which case, we should
+ // not continue to handle current key sequence and let's commit current
+ // composition.
+ if (firstResponder != [[mView window] firstResponder]) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MaybeDispatchKeydownEvent, "
+ "view lost focus by keydown event",
+ this));
+ CommitIMEComposition();
+ return false;
+ }
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void IMEInputHandler::InsertTextAsCommittingComposition(
+ NSString* aString, NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "aAttrString=\"%s\", aReplacementRange=%p { location=%lu, length=%lu }, "
+ "Destroyed()=%s, IsIMEComposing()=%s, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, GetCharacters(aString), aReplacementRange,
+ static_cast<unsigned long>(
+ aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length
+ : 0),
+ TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()),
+ static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ if (IgnoreIMECommit()) {
+ MOZ_CRASH("IMEInputHandler::InsertTextAsCommittingComposition() must not"
+ "be called while canceling the composition");
+ }
+
+ if (Destroyed()) {
+ return;
+ }
+
+ // When current keydown event causes this text input, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent().
+ // XXX Should we mark the eKeyDown event as "processed by IME"?
+ // However, if the key causes two or more Unicode characters as
+ // UTF-16 string, this is used. So, perhaps, we need to improve
+ // HandleKeyDownEvent() before do that.
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(false)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, eKeyDown "
+ "caused focus move or something and canceling the composition",
+ this));
+ return;
+ }
+
+ // First, commit current composition with the latest composition string if the
+ // replacement range is different from marked range.
+ if (IsIMEComposing() && aReplacementRange &&
+ aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(MarkedRange(), *aReplacementRange)) {
+ if (!DispatchCompositionCommitEvent()) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "destroyed by commiting composition for setting replacement range",
+ this));
+ return;
+ }
+ }
+
+ nsString str;
+ nsCocoaUtils::GetStringForNSString(aString, str);
+
+ if (!IsIMEComposing()) {
+ MOZ_DIAGNOSTIC_ASSERT(!str.IsEmpty());
+
+ // If there is no selection and replacement range is specified, set the
+ // range as selection.
+ if (aReplacementRange && aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(SelectedRange(), *aReplacementRange)) {
+ NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
+ }
+
+ if (!StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
+ // In the default settings, we should not use composition events for
+ // inserting text without key press nor IME composition because the
+ // other browsers do so. This will cause only a cancelable `beforeinput`
+ // event whose `inputType` is `insertText`.
+ WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText,
+ mWidget);
+ insertTextEvent.mString = Some(str);
+ DispatchEvent(insertTextEvent);
+ return;
+ }
+
+ // Otherise, emulate an IME composition. This is our traditional behavior,
+ // but `beforeinput` events are not cancelable since they should be so for
+ // native IME limitation. So, this is now seriously imcompatible with the
+ // other browsers.
+ if (!DispatchCompositionStartEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "cannot continue handling composition after compositionstart",
+ this));
+ return;
+ }
+ }
+
+ if (!DispatchCompositionCommitEvent(&str)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::InsertTextAsCommittingComposition, "
+ "destroyed by compositioncommit event",
+ this));
+ return;
+ }
+
+ mMarkedRange = NSMakeRange(NSNotFound, 0);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::SetMarkedText(NSAttributedString* aAttrString,
+ NSRange& aSelectedRange,
+ NSRange* aReplacementRange) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ KeyEventState* currentKeyEvent = GetCurrentKeyEvent();
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "aAttrString=\"%s\", aSelectedRange={ location=%lu, length=%lu }, "
+ "aReplacementRange=%p { location=%lu, length=%lu }, "
+ "Destroyed()=%s, IsIMEComposing()=%s, "
+ "mMarkedRange={ location=%lu, length=%lu }, keyevent=%p, "
+ "keydownDispatched=%s, keydownHandled=%s, "
+ "keypressDispatched=%s, causedOtherKeyEvents=%s, "
+ "compositionDispatched=%s",
+ this, GetCharacters([aAttrString string]),
+ static_cast<unsigned long>(aSelectedRange.location),
+ static_cast<unsigned long>(aSelectedRange.length), aReplacementRange,
+ static_cast<unsigned long>(
+ aReplacementRange ? aReplacementRange->location : 0),
+ static_cast<unsigned long>(aReplacementRange ? aReplacementRange->length
+ : 0),
+ TrueOrFalse(Destroyed()), TrueOrFalse(IsIMEComposing()),
+ static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length),
+ currentKeyEvent ? currentKeyEvent->mKeyEvent : nullptr,
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownDispatched)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyDownHandled) : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mKeyPressDispatched)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCausedOtherKeyEvents)
+ : "N/A",
+ currentKeyEvent ? TrueOrFalse(currentKeyEvent->mCompositionDispatched)
+ : "N/A"));
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ // If SetMarkedText() is called during handling a key press, that means that
+ // the key event caused this composition. So, keypress event shouldn't
+ // be dispatched later, let's mark the key event causing composition event.
+ if (currentKeyEvent) {
+ currentKeyEvent->mCompositionDispatched = true;
+
+ // When current keydown event causes this text input, let's dispatch
+ // eKeyDown event before any other events. Note that if we're in a
+ // composition, we've already dispatched eKeyDown event from
+ // TextInputHandler::HandleKeyDownEvent(). On the other hand, if we're
+ // not in composition, the key event starts new composition. So, we
+ // need to mark the eKeyDown event as "processed by IME".
+ if (!IsIMEComposing() && !MaybeDispatchCurrentKeydownEvent(true)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, eKeyDown caused focus move or "
+ "something and canceling the composition",
+ this));
+ return;
+ }
+ }
+
+ if (Destroyed()) {
+ return;
+ }
+
+ // First, commit current composition with the latest composition string if the
+ // replacement range is different from marked range.
+ if (IsIMEComposing() && aReplacementRange &&
+ aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(MarkedRange(), *aReplacementRange)) {
+ AutoRestore<bool> ignoreIMECommit(mIgnoreIMECommit);
+ mIgnoreIMECommit = false;
+ if (!DispatchCompositionCommitEvent()) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "destroyed by commiting composition for setting replacement range",
+ this));
+ return;
+ }
+ }
+
+ nsString str;
+ nsCocoaUtils::GetStringForNSString([aAttrString string], str);
+
+ mMarkedRange.length = str.Length();
+
+ if (!IsIMEComposing() && !str.IsEmpty()) {
+ // If there is no selection and replacement range is specified, set the
+ // range as selection.
+ if (aReplacementRange && aReplacementRange->location != NSNotFound &&
+ !NSEqualRanges(SelectedRange(), *aReplacementRange)) {
+ // Set temporary selection range since OnSelectionChange is async.
+ mSelectedRange = *aReplacementRange;
+ if (NS_WARN_IF(!SetSelection(*aReplacementRange))) {
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ return;
+ }
+ }
+
+ mMarkedRange.location = SelectedRange().location;
+
+ if (!DispatchCompositionStartEvent()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, cannot continue handling "
+ "composition after dispatching compositionstart",
+ this));
+ return;
+ }
+ }
+
+ if (!str.IsEmpty()) {
+ if (!DispatchCompositionChangeEvent(str, aAttrString, aSelectedRange)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, cannot continue handling "
+ "composition after dispatching compositionchange",
+ this));
+ }
+ return;
+ }
+
+ // If the composition string becomes empty string, we should commit
+ // current composition.
+ if (!DispatchCompositionCommitEvent(&EmptyString())) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SetMarkedText, "
+ "destroyed by compositioncommit event",
+ this));
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NSAttributedString* IMEInputHandler::GetAttributedSubstringFromRange(
+ NSRange& aRange, NSRange* aActualRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "aRange={ location=%lu, length=%lu }, aActualRange=%p, Destroyed()=%s",
+ this, static_cast<unsigned long>(aRange.location),
+ static_cast<unsigned long>(aRange.length), aActualRange,
+ TrueOrFalse(Destroyed())));
+
+ if (aActualRange) {
+ *aActualRange = NSMakeRange(NSNotFound, 0);
+ }
+
+ if (Destroyed() || aRange.location == NSNotFound || aRange.length == 0) {
+ return nil;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ // If we're in composing, the queried range may be in the composition string.
+ // In such case, we should use mIMECompositionString since if the composition
+ // string is handled by a remote process, the content cache may be out of
+ // date.
+ // XXX Should we set composition string attributes? Although, Blink claims
+ // that some attributes of marked text are supported, but they return
+ // just marked string without any style. So, let's keep current behavior
+ // at least for now.
+ NSUInteger compositionLength =
+ mIMECompositionString ? [mIMECompositionString length] : 0;
+ if (mIMECompositionStart != UINT32_MAX &&
+ aRange.location >= mIMECompositionStart &&
+ aRange.location + aRange.length <=
+ mIMECompositionStart + compositionLength) {
+ NSRange range =
+ NSMakeRange(aRange.location - mIMECompositionStart, aRange.length);
+ NSString* nsstr = [mIMECompositionString substringWithRange:range];
+ NSMutableAttributedString* result =
+ [[[NSMutableAttributedString alloc] initWithString:nsstr
+ attributes:nil] autorelease];
+ // XXX We cannot return font information in this case. However, this
+ // case must occur only when IME tries to confirm if composing string
+ // is handled as expected.
+ if (aActualRange) {
+ *aActualRange = aRange;
+ }
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ nsAutoString str;
+ nsCocoaUtils::GetStringForNSString(nsstr, str);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "computed with mIMECompositionString (result string=\"%s\")",
+ this, NS_ConvertUTF16toUTF8(str).get()));
+ }
+ return result;
+ }
+
+ nsAutoString str;
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryTextContentEvent.InitForQueryTextContent(startOffset, aRange.length,
+ options);
+ queryTextContentEvent.RequestFontRanges();
+ DispatchEvent(queryTextContentEvent);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetAttributedSubstringFromRange, "
+ "queryTextContentEvent={ mReply=%s }",
+ this, ToString(queryTextContentEvent.mReply).c_str()));
+
+ if (queryTextContentEvent.Failed()) {
+ return nil;
+ }
+
+ // We don't set vertical information at this point. If required,
+ // OS will calls drawsVerticallyForCharacterAtIndex.
+ NSMutableAttributedString* result =
+ nsCocoaUtils::GetNSMutableAttributedString(
+ queryTextContentEvent.mReply->DataRef(),
+ queryTextContentEvent.mReply->mFontRanges, false,
+ mWidget->BackingScaleFactor());
+ if (aActualRange) {
+ *aActualRange =
+ MakeNSRangeFrom(queryTextContentEvent.mReply->mOffsetAndData);
+ }
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+bool IMEInputHandler::HasMarkedText() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::HasMarkedText, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ return (mMarkedRange.location != NSNotFound) && (mMarkedRange.length != 0);
+}
+
+NSRange IMEInputHandler::MarkedRange() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::MarkedRange, "
+ "mMarkedRange={ location=%lu, length=%lu }",
+ this, static_cast<unsigned long>(mMarkedRange.location),
+ static_cast<unsigned long>(mMarkedRange.length)));
+
+ if (!HasMarkedText()) {
+ return NSMakeRange(NSNotFound, 0);
+ }
+ return mMarkedRange;
+}
+
+NSRange IMEInputHandler::SelectedRange() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SelectedRange, Destroyed()=%s, mSelectedRange={ "
+ "location=%lu, length=%lu }",
+ this, TrueOrFalse(Destroyed()),
+ static_cast<unsigned long>(mSelectedRange.location),
+ static_cast<unsigned long>(mSelectedRange.length)));
+
+ if (Destroyed()) {
+ return mSelectedRange;
+ }
+
+ if (mSelectedRange.location != NSNotFound) {
+ MOZ_ASSERT(mIMEHasFocus);
+ return mSelectedRange;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ mWidget);
+ DispatchEvent(querySelectedTextEvent);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SelectedRange, querySelectedTextEvent={ "
+ "mReply=%s }",
+ this, ToString(querySelectedTextEvent.mReply).c_str()));
+
+ if (querySelectedTextEvent.Failed()) {
+ return mSelectedRange;
+ }
+
+ mWritingMode = querySelectedTextEvent.mReply->WritingModeRef();
+ mRangeForWritingMode =
+ MakeNSRangeFrom(querySelectedTextEvent.mReply->mOffsetAndData);
+
+ if (mIMEHasFocus) {
+ mSelectedRange = mRangeForWritingMode;
+ }
+
+ return mRangeForWritingMode;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(mSelectedRange);
+}
+
+bool IMEInputHandler::DrawsVerticallyForCharacterAtIndex(uint32_t aCharIndex) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (Destroyed()) {
+ return false;
+ }
+
+ if (mRangeForWritingMode.location == NSNotFound) {
+ // Update cached writing-mode value for the current selection.
+ SelectedRange();
+ }
+
+ if (aCharIndex < mRangeForWritingMode.location ||
+ aCharIndex >
+ mRangeForWritingMode.location + mRangeForWritingMode.length) {
+ // It's not clear to me whether this ever happens in practice, but if an
+ // IME ever wants to query writing mode at an offset outside the current
+ // selection, the writing-mode value may not be correct for the index.
+ // In that case, use FirstRectForCharacterRange to get a fresh value.
+ // This does more work than strictly necessary (we don't need the rect
+ // here), but should be a rare case.
+ NS_WARNING(
+ "DrawsVerticallyForCharacterAtIndex not using cached writing mode");
+ NSRange range = NSMakeRange(aCharIndex, 1);
+ NSRange actualRange;
+ FirstRectForCharacterRange(range, &actualRange);
+ }
+
+ return mWritingMode.IsVertical();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+NSRect IMEInputHandler::FirstRectForCharacterRange(NSRange& aRange,
+ NSRange* aActualRange) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::FirstRectForCharacterRange, Destroyed()=%s, "
+ "aRange={ location=%lu, length=%lu }, aActualRange=%p }",
+ this, TrueOrFalse(Destroyed()),
+ static_cast<unsigned long>(aRange.location),
+ static_cast<unsigned long>(aRange.length), aActualRange));
+
+ // XXX this returns first character rect or caret rect, it is limitation of
+ // now. We need more work for returns first line rect. But current
+ // implementation is enough for IMEs.
+
+ NSRect rect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
+ NSRange actualRange = NSMakeRange(NSNotFound, 0);
+ if (aActualRange) {
+ *aActualRange = actualRange;
+ }
+ if (Destroyed() || aRange.location == NSNotFound) {
+ return rect;
+ }
+
+ RefPtr<IMEInputHandler> kungFuDeathGrip(this);
+
+ LayoutDeviceIntRect r;
+ bool useCaretRect = (aRange.length == 0);
+ if (!useCaretRect) {
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryTextRectEvent.InitForQueryTextRect(startOffset, 1, options);
+ DispatchEvent(queryTextRectEvent);
+ if (queryTextRectEvent.Succeeded()) {
+ r = queryTextRectEvent.mReply->mRect;
+ actualRange = MakeNSRangeFrom(queryTextRectEvent.mReply->mOffsetAndData);
+ mWritingMode = queryTextRectEvent.mReply->WritingModeRef();
+ mRangeForWritingMode = actualRange;
+ } else {
+ useCaretRect = true;
+ }
+ }
+
+ if (useCaretRect) {
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, mWidget);
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = aRange.location;
+ if (IsIMEComposing()) {
+ // The composition may be at different offset from the selection start
+ // offset at dispatching compositionstart because start of composition
+ // is fixed when composition string becomes non-empty in the editor.
+ // Therefore, we need to use query event which is relative to insertion
+ // point.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mIMECompositionStart;
+ }
+ queryCaretRectEvent.InitForQueryCaretRect(startOffset, options);
+ DispatchEvent(queryCaretRectEvent);
+ if (queryCaretRectEvent.Failed()) {
+ return rect;
+ }
+ r = queryCaretRectEvent.mReply->mRect;
+ r.width = 0;
+ actualRange.location = queryCaretRectEvent.mReply->StartOffset();
+ actualRange.length = 0;
+ }
+
+ nsIWidget* rootWidget = mWidget->GetTopLevelWidget();
+ NSWindow* rootWindow =
+ static_cast<NSWindow*>(rootWidget->GetNativeData(NS_NATIVE_WINDOW));
+ NSView* rootView =
+ static_cast<NSView*>(rootWidget->GetNativeData(NS_NATIVE_WIDGET));
+ if (!rootWindow || !rootView) {
+ return rect;
+ }
+ rect = nsCocoaUtils::DevPixelsToCocoaPoints(r, mWidget->BackingScaleFactor());
+ rect = [rootView convertRect:rect toView:nil];
+ rect.origin = nsCocoaUtils::ConvertPointToScreen(rootWindow, rect.origin);
+
+ if (aActualRange) {
+ *aActualRange = actualRange;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::FirstRectForCharacterRange, "
+ "useCaretRect=%s rect={ x=%f, y=%f, width=%f, height=%f }, "
+ "actualRange={ location=%lu, length=%lu }",
+ this, TrueOrFalse(useCaretRect), rect.origin.x, rect.origin.y,
+ rect.size.width, rect.size.height,
+ static_cast<unsigned long>(actualRange.location),
+ static_cast<unsigned long>(actualRange.length)));
+
+ return rect;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakeRect(0.0, 0.0, 0.0, 0.0));
+}
+
+NSUInteger IMEInputHandler::CharacterIndexForPoint(NSPoint& aPoint) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CharacterIndexForPoint, aPoint={ x=%f, y=%f }",
+ this, aPoint.x, aPoint.y));
+
+ NSWindow* mainWindow = [NSApp mainWindow];
+ if (!mWidget || !mainWindow) {
+ return NSNotFound;
+ }
+
+ WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint,
+ mWidget);
+ NSPoint ptInWindow = nsCocoaUtils::ConvertPointFromScreen(mainWindow, aPoint);
+ NSPoint ptInView = [mView convertPoint:ptInWindow fromView:nil];
+ queryCharAtPointEvent.mRefPoint.x =
+ static_cast<int32_t>(ptInView.x * mWidget->BackingScaleFactor());
+ queryCharAtPointEvent.mRefPoint.y =
+ static_cast<int32_t>(ptInView.y * mWidget->BackingScaleFactor());
+ mWidget->DispatchWindowEvent(queryCharAtPointEvent);
+ if (queryCharAtPointEvent.Failed() ||
+ queryCharAtPointEvent.DidNotFindChar() ||
+ queryCharAtPointEvent.mReply->StartOffset() >=
+ static_cast<uint32_t>(NSNotFound)) {
+ return NSNotFound;
+ }
+
+ return queryCharAtPointEvent.mReply->StartOffset();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNotFound);
+}
+
+extern "C" {
+extern NSString* NSTextInputReplacementRangeAttributeName;
+}
+
+NSArray* IMEInputHandler::GetValidAttributesForMarkedText() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::GetValidAttributesForMarkedText", this));
+
+ // Return same attributes as Chromium (see render_widget_host_view_mac.mm)
+ // because most IMEs must be tested with Safari (OS default) and Chrome
+ // (having most market share). Therefore, we need to follow their behavior.
+ // XXX It might be better to reuse an array instance for this result because
+ // this may be called a lot. Note that Chromium does so.
+ return
+ [NSArray arrayWithObjects:NSUnderlineStyleAttributeName,
+ NSUnderlineColorAttributeName,
+ NSMarkedClauseSegmentAttributeName,
+ NSTextInputReplacementRangeAttributeName, nil];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * IMEInputHandler implementation #2
+ *
+ ******************************************************************************/
+
+IMEInputHandler::IMEInputHandler(nsChildView* aWidget,
+ NSView<mozView>* aNativeView)
+ : TextInputHandlerBase(aWidget, aNativeView),
+ mPendingMethods(0),
+ mIMECompositionString(nullptr),
+ mIMECompositionStart(UINT32_MAX),
+ mRangeForWritingMode(),
+ mIsIMEComposing(false),
+ mIsDeadKeyComposing(false),
+ mIsIMEEnabled(true),
+ mIsASCIICapableOnly(false),
+ mIgnoreIMECommit(false),
+ mIMEHasFocus(false) {
+ InitStaticMembers();
+
+ mMarkedRange.location = NSNotFound;
+ mMarkedRange.length = 0;
+ mSelectedRange.location = NSNotFound;
+ mSelectedRange.length = 0;
+}
+
+IMEInputHandler::~IMEInputHandler() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ if (sFocusedIMEHandler == this) {
+ sFocusedIMEHandler = nullptr;
+ }
+ if (mIMECompositionString) {
+ [mIMECompositionString release];
+ mIMECompositionString = nullptr;
+ }
+}
+
+void IMEInputHandler::OnFocusChangeInGecko(bool aFocus) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OnFocusChangeInGecko, aFocus=%s, Destroyed()=%s, "
+ "sFocusedIMEHandler=%p",
+ this, TrueOrFalse(aFocus), TrueOrFalse(Destroyed()),
+ sFocusedIMEHandler));
+
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ mIMEHasFocus = aFocus;
+
+ // This is called when the native focus is changed and when the native focus
+ // isn't changed but the focus is changed in Gecko.
+ if (!aFocus) {
+ if (sFocusedIMEHandler == this) sFocusedIMEHandler = nullptr;
+ return;
+ }
+
+ sFocusedIMEHandler = this;
+
+ // We need to notify IME of focus change in Gecko as native focus change
+ // because the window level of the focused element in Gecko may be changed.
+ mPendingMethods |= kNotifyIMEOfFocusChangeInGecko;
+ ResetTimer();
+}
+
+bool IMEInputHandler::OnDestroyWidget(nsChildView* aDestroyingWidget) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OnDestroyWidget, aDestroyingWidget=%p, "
+ "sFocusedIMEHandler=%p, IsIMEComposing()=%s",
+ this, aDestroyingWidget, sFocusedIMEHandler,
+ TrueOrFalse(IsIMEComposing())));
+
+ // If we're not focused, the focused IMEInputHandler may have been
+ // created by another widget/nsChildView.
+ if (sFocusedIMEHandler && sFocusedIMEHandler != this) {
+ sFocusedIMEHandler->OnDestroyWidget(aDestroyingWidget);
+ }
+
+ if (!TextInputHandlerBase::OnDestroyWidget(aDestroyingWidget)) {
+ return false;
+ }
+
+ if (IsIMEComposing()) {
+ // If our view is in the composition, we should clean up it.
+ CancelIMEComposition();
+ }
+
+ mSelectedRange.location = NSNotFound; // Marking dirty
+ mIMEHasFocus = false;
+
+ return true;
+}
+
+void IMEInputHandler::SendCommittedText(NSString* aString) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SendCommittedText, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s",
+ this, mView, mWidget, mView ? [mView inputContext] : nullptr,
+ TrueOrFalse(mIsIMEComposing)));
+
+ NS_ENSURE_TRUE(mWidget, );
+ // XXX We should send the string without mView.
+ if (!mView) {
+ return;
+ }
+
+ if ([mView conformsToProtocol:@protocol(NSTextInputClient)]) {
+ NSObject<NSTextInputClient>* textInputClient =
+ static_cast<NSObject<NSTextInputClient>*>(mView);
+ [textInputClient insertText:aString
+ replacementRange:NSMakeRange(NSNotFound, 0)];
+ }
+
+ // Last resort. If we cannot retrieve NSTextInputProtocol from mView
+ // or blocking to call our InsertText(), we should call InsertText()
+ // directly to commit composition forcibly.
+ if (mIsIMEComposing) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::SendCommittedText, trying to insert text "
+ "directly "
+ "due to IME not calling our InsertText()",
+ this));
+ static_cast<TextInputHandler*>(this)->InsertText(aString);
+ MOZ_ASSERT(!mIsIMEComposing);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::KillIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::KillIMEComposition, mView=%p, mWidget=%p, "
+ "inputContext=%p, mIsIMEComposing=%s, "
+ "Destroyed()=%s, IsFocused()=%s",
+ this, mView, mWidget, mView ? [mView inputContext] : nullptr,
+ TrueOrFalse(mIsIMEComposing), TrueOrFalse(Destroyed()),
+ TrueOrFalse(IsFocused())));
+
+ if (Destroyed() || NS_WARN_IF(!mView)) {
+ return;
+ }
+
+ NSTextInputContext* inputContext = [mView inputContext];
+ if (NS_WARN_IF(!inputContext)) {
+ return;
+ }
+ [inputContext discardMarkedText];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::CommitIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CommitIMEComposition, mIMECompositionString=%s",
+ this, GetCharacters(mIMECompositionString)));
+
+ // If this is called before dispatching eCompositionStart, IsIMEComposing()
+ // returns false. Even in such case, we need to commit composition *in*
+ // IME if this is called by preceding eKeyDown event of eCompositionStart.
+ // So, we need to call KillIMEComposition() even when IsIMEComposing()
+ // returns false.
+ KillIMEComposition();
+
+ if (!IsIMEComposing()) return;
+
+ // If the composition is still there, KillIMEComposition only kills the
+ // composition in TSM. We also need to finish the our composition too.
+ SendCommittedText(mIMECompositionString);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void IMEInputHandler::CancelIMEComposition() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!IsIMEComposing()) return;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::CancelIMEComposition, mIMECompositionString=%s",
+ this, GetCharacters(mIMECompositionString)));
+
+ // For canceling the current composing, we need to ignore the param of
+ // insertText. But this code is ugly...
+ mIgnoreIMECommit = true;
+ KillIMEComposition();
+ mIgnoreIMECommit = false;
+
+ if (!IsIMEComposing()) return;
+
+ // If the composition is still there, KillIMEComposition only kills the
+ // composition in TSM. We also need to kill the our composition too.
+ SendCommittedText(@"");
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool IMEInputHandler::IsFocused() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE(!Destroyed(), false);
+ NSWindow* window = [mView window];
+ NS_ENSURE_TRUE(window, false);
+ return [window firstResponder] == mView && [window isKeyWindow] &&
+ [[NSApplication sharedApplication] isActive];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+bool IMEInputHandler::IsIMEOpened() {
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ return tis.IsOpenedIMEMode();
+}
+
+void IMEInputHandler::SetASCIICapableOnly(bool aASCIICapableOnly) {
+ if (aASCIICapableOnly == mIsASCIICapableOnly) return;
+
+ CommitIMEComposition();
+ mIsASCIICapableOnly = aASCIICapableOnly;
+ SyncASCIICapableOnly();
+}
+
+void IMEInputHandler::EnableIME(bool aEnableIME) {
+ if (aEnableIME == mIsIMEEnabled) return;
+
+ CommitIMEComposition();
+ mIsIMEEnabled = aEnableIME;
+}
+
+void IMEInputHandler::SetIMEOpenState(bool aOpenIME) {
+ if (!IsFocused() || IsIMEOpened() == aOpenIME) return;
+
+ if (!aOpenIME) {
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentASCIICapableInputSource();
+ tis.Select();
+ return;
+ }
+
+ // If we know the latest IME opened mode, we should select it.
+ if (sLatestIMEOpenedModeInputSourceID) {
+ TISInputSourceWrapper tis;
+ tis.InitByInputSourceID(sLatestIMEOpenedModeInputSourceID);
+ tis.Select();
+ return;
+ }
+
+ // XXX If the current input source is a mode of IME, we should turn on it,
+ // but we haven't found such way...
+
+ // Finally, we should refer the system locale but this is a little expensive,
+ // we shouldn't retry this (if it was succeeded, we already set
+ // sLatestIMEOpenedModeInputSourceID at that time).
+ static bool sIsPrefferredIMESearched = false;
+ if (sIsPrefferredIMESearched) return;
+ sIsPrefferredIMESearched = true;
+ OpenSystemPreferredLanguageIME();
+}
+
+void IMEInputHandler::OpenSystemPreferredLanguageIME() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OpenSystemPreferredLanguageIME", this));
+
+ CFArrayRef langList = ::CFLocaleCopyPreferredLanguages();
+ if (!langList) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, langList "
+ "is NULL",
+ this));
+ return;
+ }
+ CFIndex count = ::CFArrayGetCount(langList);
+ for (CFIndex i = 0; i < count; i++) {
+ CFLocaleRef locale = ::CFLocaleCreate(
+ kCFAllocatorDefault,
+ static_cast<CFStringRef>(::CFArrayGetValueAtIndex(langList, i)));
+ if (!locale) {
+ continue;
+ }
+
+ bool changed = false;
+ CFStringRef lang = static_cast<CFStringRef>(
+ ::CFLocaleGetValue(locale, kCFLocaleLanguageCode));
+ NS_ASSERTION(lang, "lang is null");
+ if (lang) {
+ TISInputSourceWrapper tis;
+ tis.InitByLanguage(lang);
+ if (tis.IsOpenedIMEMode()) {
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ CFStringRef foundTIS;
+ tis.GetInputSourceID(foundTIS);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OpenSystemPreferredLanguageIME, "
+ "foundTIS=%s, lang=%s",
+ this, GetCharacters(foundTIS), GetCharacters(lang)));
+ }
+ tis.Select();
+ changed = true;
+ }
+ }
+ ::CFRelease(locale);
+ if (changed) {
+ break;
+ }
+ }
+ ::CFRelease(langList);
+}
+
+void IMEInputHandler::OnSelectionChange(
+ const IMENotification& aIMENotification) {
+ MOZ_ASSERT(aIMENotification.mSelectionChangeData.IsInitialized());
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("%p IMEInputHandler::OnSelectionChange", this));
+
+ if (!aIMENotification.mSelectionChangeData.HasRange()) {
+ mSelectedRange.location = NSNotFound;
+ mSelectedRange.length = 0;
+ mRangeForWritingMode.location = NSNotFound;
+ mRangeForWritingMode.length = 0;
+ return;
+ }
+
+ mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode();
+ mRangeForWritingMode =
+ NSMakeRange(aIMENotification.mSelectionChangeData.mOffset,
+ aIMENotification.mSelectionChangeData.Length());
+ if (mIMEHasFocus) {
+ mSelectedRange = mRangeForWritingMode;
+ }
+}
+
+void IMEInputHandler::OnLayoutChange() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!IsFocused()) {
+ return;
+ }
+ NSTextInputContext* inputContext = [mView inputContext];
+ [inputContext invalidateCharacterCoordinates];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool IMEInputHandler::OnHandleEvent(NSEvent* aEvent) {
+ if (!IsFocused()) {
+ return false;
+ }
+
+ bool allowConsumeEvent = true;
+ if (!IsIMEComposing()) {
+ // Hack for bug of Korean IMEs on Catalina (10.15).
+ // If we are inactivated during composition, active Korean IME keeps
+ // consuming all mousedown events of any mouse buttons. So, we should
+ // allow Korean IMEs to handle mousedown events only when there is
+ // composition string.
+ // List of ID of Korean IME:
+ // * com.apple.inputmethod.Korean.2SetKorean
+ // * com.apple.inputmethod.Korean.3SetKorean
+ // * com.apple.inputmethod.Korean.390Sebulshik
+ // * com.apple.inputmethod.Korean.GongjinCheongRomaja
+ // * com.apple.inputmethod.Korean.HNCRomaja
+ TISInputSourceWrapper tis;
+ tis.InitByCurrentInputSource();
+ nsAutoString inputSourceID;
+ tis.GetInputSourceID(inputSourceID);
+ allowConsumeEvent =
+ !StringBeginsWith(inputSourceID, u"com.apple.inputmethod.Korean."_ns);
+ }
+ NSTextInputContext* inputContext = [mView inputContext];
+ return [inputContext handleEvent:aEvent] && allowConsumeEvent;
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase implementation
+ *
+ ******************************************************************************/
+
+int32_t TextInputHandlerBase::sSecureEventInputCount = 0;
+
+NS_IMPL_ISUPPORTS(TextInputHandlerBase, TextEventDispatcherListener,
+ nsISupportsWeakReference)
+
+TextInputHandlerBase::TextInputHandlerBase(nsChildView* aWidget,
+ NSView<mozView>* aNativeView)
+ : mWidget(aWidget), mDispatcher(aWidget->GetTextEventDispatcher()) {
+ gHandlerInstanceCount++;
+ mView = [aNativeView retain];
+}
+
+TextInputHandlerBase::~TextInputHandlerBase() {
+ [mView release];
+ if (--gHandlerInstanceCount == 0) {
+ TISInputSourceWrapper::Shutdown();
+ }
+}
+
+bool TextInputHandlerBase::OnDestroyWidget(nsChildView* aDestroyingWidget) {
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandlerBase::OnDestroyWidget, "
+ "aDestroyingWidget=%p, mWidget=%p",
+ this, aDestroyingWidget, mWidget));
+
+ if (aDestroyingWidget != mWidget) {
+ return false;
+ }
+
+ mWidget = nullptr;
+ mDispatcher = nullptr;
+ return true;
+}
+
+bool TextInputHandlerBase::DispatchEvent(WidgetGUIEvent& aEvent) {
+ return mWidget->DispatchWindowEvent(aEvent);
+}
+
+void TextInputHandlerBase::InitKeyEvent(NSEvent* aNativeKeyEvent,
+ WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME,
+ const nsAString* aInsertString) {
+ NS_ASSERTION(aNativeKeyEvent, "aNativeKeyEvent must not be NULL");
+
+ if (mKeyboardOverride.mOverrideEnabled) {
+ TISInputSourceWrapper tis;
+ tis.InitByLayoutID(mKeyboardOverride.mKeyboardLayout, true);
+ tis.InitKeyEvent(aNativeKeyEvent, aKeyEvent, aIsProcessedByIME,
+ aInsertString);
+ return;
+ }
+ TISInputSourceWrapper::CurrentInputSource().InitKeyEvent(
+ aNativeKeyEvent, aKeyEvent, aIsProcessedByIME, aInsertString);
+}
+
+nsresult TextInputHandlerBase::SynthesizeNativeKeyEvent(
+ int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ uint32_t modifierFlags =
+ nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(
+ static_cast<nsIWidget::Modifiers>(aModifierFlags));
+ NSInteger windowNumber = [[mView window] windowNumber];
+ bool sendFlagsChangedEvent = IsModifierKey(aNativeKeyCode);
+ NSEventType eventType =
+ sendFlagsChangedEvent ? NSEventTypeFlagsChanged : NSEventTypeKeyDown;
+ NSEvent* downEvent =
+ [NSEvent keyEventWithType:eventType
+ location:NSMakePoint(0, 0)
+ modifierFlags:modifierFlags
+ timestamp:0
+ windowNumber:windowNumber
+ context:nil
+ characters:nsCocoaUtils::ToNSString(aCharacters)
+ charactersIgnoringModifiers:nsCocoaUtils::ToNSString(
+ aUnmodifiedCharacters)
+ isARepeat:NO
+ keyCode:aNativeKeyCode];
+
+ NSEvent* upEvent = sendFlagsChangedEvent
+ ? nil
+ : nsCocoaUtils::MakeNewCocoaEventWithType(
+ NSEventTypeKeyUp, downEvent);
+
+ if (downEvent && (sendFlagsChangedEvent || upEvent)) {
+ KeyboardLayoutOverride currentLayout = mKeyboardOverride;
+ mKeyboardOverride.mKeyboardLayout = aNativeKeyboardLayout;
+ mKeyboardOverride.mOverrideEnabled = true;
+ [NSApp sendEvent:downEvent];
+ if (upEvent) {
+ [NSApp sendEvent:upEvent];
+ }
+ // processKeyDownEvent and keyUp block exceptions so we're sure to
+ // reach here to restore mKeyboardOverride
+ mKeyboardOverride = currentLayout;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NSInteger TextInputHandlerBase::GetWindowLevel() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG_KEY_OR_IME(LogLevel::Info,
+ ("%p TextInputHandlerBase::GetWindowLevel, Destryoed()=%s",
+ this, TrueOrFalse(Destroyed())));
+
+ if (Destroyed()) {
+ return NSNormalWindowLevel;
+ }
+
+ // When an <input> element on a XUL <panel> is focused, the actual focused
+ // view is the panel's parent view (mView). But the editor is displayed on the
+ // popped-up widget's view (editorView). We want the latter's window level.
+ NSView<mozView>* editorView = mWidget->GetEditorView();
+ NS_ENSURE_TRUE(editorView, NSNormalWindowLevel);
+ NSInteger windowLevel = [[editorView window] level];
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandlerBase::GetWindowLevel, windowLevel=%s (%lX)", this,
+ GetWindowLevelName(windowLevel),
+ static_cast<unsigned long>(windowLevel)));
+
+ return windowLevel;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNormalWindowLevel);
+}
+
+NS_IMETHODIMP
+TextInputHandlerBase::AttachNativeKeyEvent(WidgetKeyboardEvent& aKeyEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Don't try to replace a native event if one already exists.
+ // OS X doesn't have an OS modifier, can't make a native event.
+ if (aKeyEvent.mNativeKeyEvent) {
+ return NS_OK;
+ }
+
+ MOZ_LOG_KEY_OR_IME(
+ LogLevel::Info,
+ ("%p TextInputHandlerBase::AttachNativeKeyEvent, key=0x%X, char=0x%X, "
+ "mod=0x%X",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, aKeyEvent.mModifiers));
+
+ NSInteger windowNumber = [[mView window] windowNumber];
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ aKeyEvent.mNativeKeyEvent = nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(
+ aKeyEvent, windowNumber, context);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool TextInputHandlerBase::SetSelection(NSRange& aRange) {
+ MOZ_ASSERT(!Destroyed());
+
+ RefPtr<TextInputHandlerBase> kungFuDeathGrip(this);
+ WidgetSelectionEvent selectionEvent(true, eSetSelection, mWidget);
+ selectionEvent.mOffset = aRange.location;
+ selectionEvent.mLength = aRange.length;
+ selectionEvent.mReversed = false;
+ selectionEvent.mExpandToClusterBoundary = false;
+ DispatchEvent(selectionEvent);
+ NS_ENSURE_TRUE(selectionEvent.mSucceeded, false);
+ return !Destroyed();
+}
+
+/* static */ bool TextInputHandlerBase::IsPrintableChar(char16_t aChar) {
+ return (aChar >= 0x20 && aChar <= 0x7E) || aChar >= 0xA0;
+}
+
+/* static */ bool TextInputHandlerBase::IsSpecialGeckoKey(
+ UInt32 aNativeKeyCode) {
+ // this table is used to determine which keys are special and should not
+ // generate a charCode
+ switch (aNativeKeyCode) {
+ // modifiers - we don't get separate events for these yet
+ case kVK_Escape:
+ case kVK_Shift:
+ case kVK_RightShift:
+ case kVK_Command:
+ case kVK_RightCommand:
+ case kVK_CapsLock:
+ case kVK_Control:
+ case kVK_RightControl:
+ case kVK_Option:
+ case kVK_RightOption:
+ case kVK_ANSI_KeypadClear:
+ case kVK_Function:
+
+ // function keys
+ case kVK_F1:
+ case kVK_F2:
+ case kVK_F3:
+ case kVK_F4:
+ case kVK_F5:
+ case kVK_F6:
+ case kVK_F7:
+ case kVK_F8:
+ case kVK_F9:
+ case kVK_F10:
+ case kVK_F11:
+ case kVK_F12:
+ case kVK_PC_Pause:
+ case kVK_PC_ScrollLock:
+ case kVK_PC_PrintScreen:
+ case kVK_F16:
+ case kVK_F17:
+ case kVK_F18:
+ case kVK_F19:
+
+ case kVK_PC_Insert:
+ case kVK_PC_Delete:
+ case kVK_Tab:
+ case kVK_PC_Backspace:
+ case kVK_PC_ContextMenu:
+
+ case kVK_JIS_Eisu:
+ case kVK_JIS_Kana:
+
+ case kVK_Home:
+ case kVK_End:
+ case kVK_PageUp:
+ case kVK_PageDown:
+ case kVK_LeftArrow:
+ case kVK_RightArrow:
+ case kVK_UpArrow:
+ case kVK_DownArrow:
+ case kVK_Return:
+ case kVK_ANSI_KeypadEnter:
+ case kVK_Powerbook_KeypadEnter:
+ return true;
+ }
+ return false;
+}
+
+/* static */ bool TextInputHandlerBase::IsNormalCharInputtingEvent(
+ NSEvent* aNativeEvent) {
+ if ([aNativeEvent type] != NSEventTypeKeyDown &&
+ [aNativeEvent type] != NSEventTypeKeyUp) {
+ return false;
+ }
+ nsAutoString nativeChars;
+ nsCocoaUtils::GetStringForNSString([aNativeEvent characters], nativeChars);
+
+ // this is not character inputting event, simply.
+ if (nativeChars.IsEmpty() ||
+ ([aNativeEvent modifierFlags] & NSEventModifierFlagCommand)) {
+ return false;
+ }
+ return !IsControlChar(nativeChars[0]);
+}
+
+/* static */ bool TextInputHandlerBase::IsModifierKey(UInt32 aNativeKeyCode) {
+ switch (aNativeKeyCode) {
+ case kVK_CapsLock:
+ case kVK_RightCommand:
+ case kVK_Command:
+ case kVK_Shift:
+ case kVK_Option:
+ case kVK_Control:
+ case kVK_RightShift:
+ case kVK_RightOption:
+ case kVK_RightControl:
+ case kVK_Function:
+ return true;
+ }
+ return false;
+}
+
+/* static */ void TextInputHandlerBase::EnableSecureEventInput() {
+ sSecureEventInputCount++;
+ ::EnableSecureEventInput();
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("EnableSecureEventInput() called (%d)", sSecureEventInputCount));
+}
+
+/* static */ void TextInputHandlerBase::DisableSecureEventInput() {
+ if (!sSecureEventInputCount) {
+ return;
+ }
+ sSecureEventInputCount--;
+ ::DisableSecureEventInput();
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("DisableSecureEventInput() called (%d)", sSecureEventInputCount));
+}
+
+/* static */ bool TextInputHandlerBase::IsSecureEventInputEnabled() {
+ // sSecureEventInputCount is our mechanism to track when Secure Event Input
+ // is enabled. Non-zero indicates we have enabled Secure Input. But
+ // zero does not mean that Secure Input is _disabled_ because another
+ // application may have enabled it. If the OS reports Secure Event
+ // Input is disabled though, a non-zero sSecureEventInputCount is an error.
+ NS_ASSERTION(::IsSecureEventInputEnabled() || 0 == sSecureEventInputCount,
+ "sSecureEventInputCount is not zero when the OS thinks "
+ "SecureEventInput is disabled.");
+ return !!sSecureEventInputCount;
+}
+
+/* static */ void TextInputHandlerBase::EnsureSecureEventInputDisabled() {
+ while (sSecureEventInputCount) {
+ TextInputHandlerBase::DisableSecureEventInput();
+ }
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase::KeyEventState implementation
+ *
+ ******************************************************************************/
+
+void TextInputHandlerBase::KeyEventState::InitKeyEvent(
+ TextInputHandlerBase* aHandler, WidgetKeyboardEvent& aKeyEvent,
+ bool aIsProcessedByIME) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(aHandler);
+ MOZ_RELEASE_ASSERT(mKeyEvent);
+
+ NSEvent* nativeEvent = mKeyEvent;
+ if (!mInsertedString.IsEmpty()) {
+ nsAutoString unhandledString;
+ GetUnhandledString(unhandledString);
+ NSString* unhandledNSString = nsCocoaUtils::ToNSString(unhandledString);
+ // If the key event's some characters were already handled by
+ // InsertString() calls, we need to create a dummy event which doesn't
+ // include the handled characters.
+ nativeEvent =
+ [NSEvent keyEventWithType:[mKeyEvent type]
+ location:[mKeyEvent locationInWindow]
+ modifierFlags:[mKeyEvent modifierFlags]
+ timestamp:[mKeyEvent timestamp]
+ windowNumber:[mKeyEvent windowNumber]
+ context:nil
+ characters:unhandledNSString
+ charactersIgnoringModifiers:[mKeyEvent charactersIgnoringModifiers]
+ isARepeat:[mKeyEvent isARepeat]
+ keyCode:[mKeyEvent keyCode]];
+ }
+
+ aKeyEvent.mUniqueId = mUniqueId;
+ aHandler->InitKeyEvent(nativeEvent, aKeyEvent, aIsProcessedByIME,
+ mInsertString);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void TextInputHandlerBase::KeyEventState::GetUnhandledString(
+ nsAString& aUnhandledString) const {
+ aUnhandledString.Truncate();
+ if (NS_WARN_IF(!mKeyEvent)) {
+ return;
+ }
+ nsAutoString characters;
+ nsCocoaUtils::GetStringForNSString([mKeyEvent characters], characters);
+ if (characters.IsEmpty()) {
+ return;
+ }
+ if (mInsertedString.IsEmpty()) {
+ aUnhandledString = characters;
+ return;
+ }
+
+ // The insertes string must match with the start of characters.
+ MOZ_ASSERT(StringBeginsWith(characters, mInsertedString));
+
+ aUnhandledString = nsDependentSubstring(characters, mInsertedString.Length());
+}
+
+#pragma mark -
+
+/******************************************************************************
+ *
+ * TextInputHandlerBase::AutoInsertStringClearer implementation
+ *
+ ******************************************************************************/
+
+TextInputHandlerBase::AutoInsertStringClearer::~AutoInsertStringClearer() {
+ if (mState && mState->mInsertString) {
+ // If inserting string is a part of characters of the event,
+ // we should record it as inserted string.
+ nsAutoString characters;
+ nsCocoaUtils::GetStringForNSString([mState->mKeyEvent characters],
+ characters);
+ nsAutoString insertedString(mState->mInsertedString);
+ insertedString += *mState->mInsertString;
+ if (StringBeginsWith(characters, insertedString)) {
+ mState->mInsertedString = insertedString;
+ }
+ }
+ if (mState) {
+ mState->mInsertString = nullptr;
+ }
+}
+
+#undef MOZ_LOG_KEY_OR_IME
diff --git a/widget/cocoa/TextRecognition.mm b/widget/cocoa/TextRecognition.mm
new file mode 100644
index 0000000000..c365f51982
--- /dev/null
+++ b/widget/cocoa/TextRecognition.mm
@@ -0,0 +1,121 @@
+/* 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/. */
+
+#import <Vision/Vision.h>
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/ErrorResult.h"
+#include "ErrorList.h"
+#include "nsClipboard.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/MacStringHelpers.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/widget/TextRecognition.h"
+#include "mozilla/dom/PContent.h"
+
+namespace mozilla::widget {
+
+auto TextRecognition::DoFindText(gfx::DataSourceSurface& aSurface,
+ const nsTArray<nsCString>& aLanguages)
+ -> RefPtr<NativePromise> {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ // TODO - Is this the most efficient path? Maybe we can write a new
+ // CreateCGImageFromXXX that enables more efficient marshalling of the data.
+ CGImageRef imageRef = NULL;
+ nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(&aSurface, &imageRef);
+ if (NS_FAILED(rv) || !imageRef) {
+ return NativePromise::CreateAndReject("Failed to create CGImage"_ns,
+ __func__);
+ }
+
+ auto promise = MakeRefPtr<NativePromise::Private>(__func__);
+
+ NSMutableArray* recognitionLanguages = [[NSMutableArray alloc] init];
+ for (const auto& locale : aLanguages) {
+ [recognitionLanguages addObject:nsCocoaUtils::ToNSString(locale)];
+ }
+
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ __func__,
+ [promise, imageRef, recognitionLanguages] {
+ auto unrefImage = MakeScopeExit([&] {
+ ::CGImageRelease(imageRef);
+ [recognitionLanguages release];
+ });
+
+ dom::TextRecognitionResult result;
+ dom::TextRecognitionResult* pResult = &result;
+
+ // Define the request to use, which also handles the result. It will
+ // be run below directly in this thread. After creating this
+ // request.
+ VNRecognizeTextRequest* textRecognitionRequest =
+ [[VNRecognizeTextRequest alloc] initWithCompletionHandler:^(
+ VNRequest* _Nonnull request,
+ NSError* _Nullable error) {
+ NSArray<VNRecognizedTextObservation*>* observations =
+ request.results;
+
+ [observations enumerateObjectsUsingBlock:^(
+ VNRecognizedTextObservation* _Nonnull obj,
+ NSUInteger idx, BOOL* _Nonnull stop) {
+ // Requests the n top candidates for a recognized text
+ // string.
+ VNRecognizedText* recognizedText =
+ [obj topCandidates:1].firstObject;
+
+ // https://developer.apple.com/documentation/vision/vnrecognizedtext?language=objc
+ auto& quad = *pResult->quads().AppendElement();
+ CopyCocoaStringToXPCOMString(recognizedText.string,
+ quad.string());
+ quad.confidence() = recognizedText.confidence;
+
+ auto ToImagePoint = [](CGPoint aPoint) -> ImagePoint {
+ return {static_cast<float>(aPoint.x),
+ static_cast<float>(aPoint.y)};
+ };
+ *quad.points().AppendElement() =
+ ToImagePoint(obj.bottomLeft);
+ *quad.points().AppendElement() = ToImagePoint(obj.topLeft);
+ *quad.points().AppendElement() = ToImagePoint(obj.topRight);
+ *quad.points().AppendElement() =
+ ToImagePoint(obj.bottomRight);
+ }];
+ }];
+
+ textRecognitionRequest.recognitionLevel =
+ VNRequestTextRecognitionLevelAccurate;
+ textRecognitionRequest.recognitionLanguages = recognitionLanguages;
+ textRecognitionRequest.usesLanguageCorrection = true;
+
+ // Send out the request. This blocks execution of this thread with
+ // an expensive CPU call.
+ NSError* error = nil;
+ VNImageRequestHandler* requestHandler =
+ [[[VNImageRequestHandler alloc] initWithCGImage:imageRef
+ options:@{}]
+ autorelease];
+
+ [requestHandler performRequests:@[ textRecognitionRequest ]
+ error:&error];
+ if (error != nil) {
+ promise->Reject(
+ nsPrintfCString(
+ "Failed to perform text recognition request (%ld)\n",
+ error.code),
+ __func__);
+ } else {
+ promise->Resolve(std::move(result), __func__);
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return promise;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+} // namespace mozilla::widget
diff --git a/widget/cocoa/VibrancyManager.h b/widget/cocoa/VibrancyManager.h
new file mode 100644
index 0000000000..d431540830
--- /dev/null
+++ b/widget/cocoa/VibrancyManager.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef VibrancyManager_h
+#define VibrancyManager_h
+
+#include "mozilla/Assertions.h"
+#include "nsClassHashtable.h"
+#include "nsRegion.h"
+#include "nsTArray.h"
+#include "ViewRegion.h"
+
+#import <Foundation/NSGeometry.h>
+
+@class NSColor;
+@class NSView;
+class nsChildView;
+
+namespace mozilla {
+
+enum class VibrancyType {
+ TOOLTIP,
+ MENU,
+};
+
+/**
+ * VibrancyManager takes care of updating the vibrant regions of a window.
+ * Vibrancy is a visual look that was introduced on OS X starting with 10.10.
+ * An app declares vibrant window regions to the window server, and the window
+ * server will display a blurred rendering of the screen contents from behind
+ * the window in these areas, behind the actual window contents. Consequently,
+ * the effect is only visible in areas where the window contents are not
+ * completely opaque. Usually this is achieved by clearing the background of
+ * the window prior to drawing in the vibrant areas. This is possible even if
+ * the window is declared as opaque.
+ */
+class VibrancyManager {
+ public:
+ /**
+ * Create a new VibrancyManager instance and provide it with an NSView
+ * to attach NSVisualEffectViews to.
+ *
+ * @param aCoordinateConverter The nsChildView to use for converting
+ * nsIntRect device pixel coordinates into Cocoa NSRect coordinates. Must
+ * outlive this VibrancyManager instance.
+ * @param aContainerView The view that's going to be the superview of the
+ * NSVisualEffectViews which will be created for vibrant regions.
+ */
+ VibrancyManager(const nsChildView& aCoordinateConverter,
+ NSView* aContainerView)
+ : mCoordinateConverter(aCoordinateConverter),
+ mContainerView(aContainerView) {}
+
+ /**
+ * Update the placement of the NSVisualEffectViews inside the container
+ * NSView so that they cover aRegion, and create new NSVisualEffectViews
+ * or remove existing ones as needed.
+ * @param aType The vibrancy type to use in the region.
+ * @param aRegion The vibrant area, in device pixels.
+ * @return Whether the region changed.
+ */
+ bool UpdateVibrantRegion(VibrancyType aType,
+ const LayoutDeviceIntRegion& aRegion);
+
+ bool HasVibrantRegions() { return !mVibrantRegions.IsEmpty(); }
+
+ LayoutDeviceIntRegion GetUnionOfVibrantRegions() const;
+
+ /**
+ * Create an NSVisualEffectView for the specified vibrancy type. The return
+ * value is not autoreleased. We return an object of type NSView* because we
+ * compile with an SDK that does not contain a definition for
+ * NSVisualEffectView.
+ * @param aIsContainer Whether this NSView will have child views. This value
+ * affects hit testing: Container views will pass through
+ * hit testing requests to their children, and leaf views
+ * will be transparent to hit testing.
+ */
+ static NSView* CreateEffectView(VibrancyType aType, BOOL aIsContainer = NO);
+
+ protected:
+ const nsChildView& mCoordinateConverter;
+ NSView* mContainerView;
+ nsClassHashtable<nsUint32HashKey, ViewRegion> mVibrantRegions;
+};
+
+} // namespace mozilla
+
+#endif // VibrancyManager_h
diff --git a/widget/cocoa/VibrancyManager.mm b/widget/cocoa/VibrancyManager.mm
new file mode 100644
index 0000000000..e9cfcc3be0
--- /dev/null
+++ b/widget/cocoa/VibrancyManager.mm
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "VibrancyManager.h"
+
+#import <objc/message.h>
+
+#include "nsChildView.h"
+
+using namespace mozilla;
+
+@interface MOZVibrantView : NSVisualEffectView {
+ VibrancyType mType;
+}
+- (instancetype)initWithFrame:(NSRect)aRect
+ vibrancyType:(VibrancyType)aVibrancyType;
+@end
+
+@interface MOZVibrantLeafView : MOZVibrantView
+@end
+
+static NSVisualEffectState VisualEffectStateForVibrancyType(
+ VibrancyType aType) {
+ switch (aType) {
+ case VibrancyType::TOOLTIP:
+ case VibrancyType::MENU:
+ // Tooltip and menu windows are never "key", so we need to tell the
+ // vibrancy effect to look active regardless of window state.
+ return NSVisualEffectStateActive;
+ default:
+ return NSVisualEffectStateFollowsWindowActiveState;
+ }
+}
+
+static NSVisualEffectMaterial VisualEffectMaterialForVibrancyType(
+ VibrancyType aType, BOOL* aOutIsEmphasized) {
+ switch (aType) {
+ case VibrancyType::TOOLTIP:
+ return (NSVisualEffectMaterial)NSVisualEffectMaterialToolTip;
+ case VibrancyType::MENU:
+ return NSVisualEffectMaterialMenu;
+ }
+}
+
+@implementation MOZVibrantView
+
+- (instancetype)initWithFrame:(NSRect)aRect vibrancyType:(VibrancyType)aType {
+ self = [super initWithFrame:aRect];
+ mType = aType;
+
+ self.appearance = nil;
+ self.state = VisualEffectStateForVibrancyType(mType);
+
+ BOOL isEmphasized = NO;
+ self.material = VisualEffectMaterialForVibrancyType(mType, &isEmphasized);
+ self.emphasized = isEmphasized;
+
+ return self;
+}
+
+// Don't override allowsVibrancy here, because this view may have subviews, and
+// returning YES from allowsVibrancy forces on foreground vibrancy for all
+// descendant views, which can have unintended effects.
+
+@end
+
+@implementation MOZVibrantLeafView
+
+- (NSView*)hitTest:(NSPoint)aPoint {
+ // This view must be transparent to mouse events.
+ return nil;
+}
+
+// MOZVibrantLeafView does not have subviews, so we can return YES here without
+// having unintended effects on other contents of the window.
+- (BOOL)allowsVibrancy {
+ return NO;
+}
+
+@end
+
+bool VibrancyManager::UpdateVibrantRegion(
+ VibrancyType aType, const LayoutDeviceIntRegion& aRegion) {
+ if (aRegion.IsEmpty()) {
+ return mVibrantRegions.Remove(uint32_t(aType));
+ }
+ auto& vr = *mVibrantRegions.GetOrInsertNew(uint32_t(aType));
+ return vr.UpdateRegion(aRegion, mCoordinateConverter, mContainerView, ^() {
+ return this->CreateEffectView(aType);
+ });
+}
+
+LayoutDeviceIntRegion VibrancyManager::GetUnionOfVibrantRegions() const {
+ LayoutDeviceIntRegion result;
+ for (const auto& region : mVibrantRegions.Values()) {
+ result.OrWith(region->Region());
+ }
+ return result;
+}
+
+/* static */ NSView* VibrancyManager::CreateEffectView(VibrancyType aType,
+ BOOL aIsContainer) {
+ return aIsContainer ? [[MOZVibrantView alloc] initWithFrame:NSZeroRect
+ vibrancyType:aType]
+ : [[MOZVibrantLeafView alloc] initWithFrame:NSZeroRect
+ vibrancyType:aType];
+}
diff --git a/widget/cocoa/ViewRegion.h b/widget/cocoa/ViewRegion.h
new file mode 100644
index 0000000000..b2ed0c8835
--- /dev/null
+++ b/widget/cocoa/ViewRegion.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef ViewRegion_h
+#define ViewRegion_h
+
+#include "Units.h"
+#include "nsTArray.h"
+
+class nsChildView;
+
+@class NSView;
+
+namespace mozilla {
+
+/**
+ * Manages a set of NSViews to cover a LayoutDeviceIntRegion.
+ */
+class ViewRegion {
+ public:
+ ~ViewRegion();
+
+ mozilla::LayoutDeviceIntRegion Region() { return mRegion; }
+
+ /**
+ * Update the region.
+ * @param aRegion The new region.
+ * @param aCoordinateConverter The nsChildView to use for converting
+ * LayoutDeviceIntRect device pixel coordinates into Cocoa NSRect
+ * coordinates.
+ * @param aContainerView The view that's going to be the superview of the
+ * NSViews which will be created for this region.
+ * @param aViewCreationCallback A block that instantiates new NSViews.
+ * @return Whether or not the region changed.
+ */
+ bool UpdateRegion(const mozilla::LayoutDeviceIntRegion& aRegion,
+ const nsChildView& aCoordinateConverter,
+ NSView* aContainerView, NSView* (^aViewCreationCallback)());
+
+ /**
+ * Return an NSView from the region, if there is any.
+ */
+ NSView* GetAnyView() { return mViews.Length() > 0 ? mViews[0] : NULL; }
+
+ private:
+ mozilla::LayoutDeviceIntRegion mRegion;
+ nsTArray<NSView*> mViews;
+};
+
+} // namespace mozilla
+
+#endif // ViewRegion_h
diff --git a/widget/cocoa/ViewRegion.mm b/widget/cocoa/ViewRegion.mm
new file mode 100644
index 0000000000..62e76d2df8
--- /dev/null
+++ b/widget/cocoa/ViewRegion.mm
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ViewRegion.h"
+#import <Cocoa/Cocoa.h>
+
+#include "nsChildView.h"
+
+using namespace mozilla;
+
+ViewRegion::~ViewRegion() {
+ for (size_t i = 0; i < mViews.Length(); i++) {
+ [mViews[i] removeFromSuperview];
+ }
+}
+
+bool ViewRegion::UpdateRegion(const LayoutDeviceIntRegion& aRegion,
+ const nsChildView& aCoordinateConverter,
+ NSView* aContainerView,
+ NSView* (^aViewCreationCallback)()) {
+ if (mRegion == aRegion) {
+ return false;
+ }
+
+ // We need to construct the required region using as many EffectViews
+ // as necessary. We try to update the geometry of existing views if
+ // possible, or create new ones or remove old ones if the number of
+ // rects in the region has changed.
+
+ nsTArray<NSView*> viewsToRecycle = std::move(mViews);
+ // The mViews array is now empty.
+
+ size_t i = 0;
+ for (auto iter = aRegion.RectIter();
+ !iter.Done() || i < viewsToRecycle.Length(); i++) {
+ if (!iter.Done()) {
+ NSView* view = nil;
+ NSRect rect = aCoordinateConverter.DevPixelsToCocoaPoints(iter.Get());
+ if (i < viewsToRecycle.Length()) {
+ view = viewsToRecycle[i];
+ } else {
+ view = aViewCreationCallback();
+ [aContainerView addSubview:view];
+
+ // Now that the view is in the view hierarchy, it'll be kept alive by
+ // its superview, so we can drop our reference.
+ [view release];
+ }
+ if (!NSEqualRects(rect, [view frame])) {
+ [view setFrame:rect];
+ }
+ [view setNeedsDisplay:YES];
+ mViews.AppendElement(view);
+ iter.Next();
+ } else {
+ // Our new region is made of fewer rects than the old region, so we can
+ // remove this view. We only have a weak reference to it, so removing it
+ // from the view hierarchy will release it.
+ [viewsToRecycle[i] removeFromSuperview];
+ }
+ }
+
+ mRegion = aRegion;
+ return true;
+}
diff --git a/widget/cocoa/WidgetTraceEvent.mm b/widget/cocoa/WidgetTraceEvent.mm
new file mode 100644
index 0000000000..54fe497e3b
--- /dev/null
+++ b/widget/cocoa/WidgetTraceEvent.mm
@@ -0,0 +1,79 @@
+/* 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 <Cocoa/Cocoa.h>
+#include "CustomCocoaEvents.h"
+#include <Foundation/NSAutoreleasePool.h>
+#include <mozilla/CondVar.h>
+#include <mozilla/Mutex.h>
+#include "mozilla/WidgetTraceEvent.h"
+
+using mozilla::CondVar;
+using mozilla::Mutex;
+using mozilla::MutexAutoLock;
+
+namespace {
+
+Mutex* sMutex = NULL;
+CondVar* sCondVar = NULL;
+bool sTracerProcessed = false;
+
+} // namespace
+
+namespace mozilla {
+
+bool InitWidgetTracing() {
+ sMutex = new Mutex("Event tracer thread mutex");
+ sCondVar = new CondVar(*sMutex, "Event tracer thread condvar");
+ return sMutex && sCondVar;
+}
+
+void CleanUpWidgetTracing() {
+ delete sMutex;
+ delete sCondVar;
+ sMutex = NULL;
+ sCondVar = NULL;
+}
+
+// This function is called from the main (UI) thread.
+void SignalTracerThread() {
+ if (!sMutex || !sCondVar) return;
+ MutexAutoLock lock(*sMutex);
+ if (!sTracerProcessed) {
+ sTracerProcessed = true;
+ sCondVar->Notify();
+ }
+}
+
+// This function is called from the background tracer thread.
+bool FireAndWaitForTracerEvent() {
+ MOZ_ASSERT(sMutex && sCondVar, "Tracing not initialized!");
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+ MutexAutoLock lock(*sMutex);
+ if (sTracerProcessed) {
+ // Things are out of sync. This is likely because we're in
+ // the middle of shutting down. Just return false and hope the
+ // tracer thread is quitting anyway.
+ return false;
+ }
+
+ // Post an application-defined event to the main thread's event queue
+ // and wait for it to get processed.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeTrace
+ data1:0
+ data2:0]
+ atStart:NO];
+ while (!sTracerProcessed) sCondVar->Wait();
+ sTracerProcessed = false;
+ [pool release];
+ return true;
+}
+
+} // namespace mozilla
diff --git a/widget/cocoa/components.conf b/widget/cocoa/components.conf
new file mode 100644
index 0000000000..e8570fd502
--- /dev/null
+++ b/widget/cocoa/components.conf
@@ -0,0 +1,168 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Headers = '/widget/cocoa/nsWidgetFactory.h',
+
+InitFunc = 'nsWidgetCocoaModuleCtor'
+UnloadFunc = 'nsWidgetCocoaModuleDtor'
+
+Classes = [
+ {
+ 'cid': '{49f428e8-baf9-4ba3-b1b0-7d2fd3abbcea}',
+ 'contract_ids': ['@mozilla.org/widget/parent/clipboard;1'],
+ 'interfaces': ['nsIClipboard'],
+ 'type': 'nsIClipboard',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'name': 'GfxInfo',
+ 'cid': '{d755a760-9f27-11df-0800-200c9a664242}',
+ 'contract_ids': ['@mozilla.org/gfx/info;1'],
+ 'type': 'mozilla::widget::GfxInfo',
+ 'headers': ['/widget/cocoa/GfxInfo.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'cid': '{e5170091-c16b-492d-bf00-f45d72470553}',
+ 'contract_ids': ['@mozilla.org/parent/filepicker;1'],
+ 'type': 'nsFilePicker',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{b90f5fdd-c23e-4ad6-a10e-1da8ffe07799}',
+ 'contract_ids': ['@mozilla.org/parent/colorpicker;1'],
+ 'type': 'nsColorPicker',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{2d96b3df-c051-11d1-a827-0040959a28c9}',
+ 'contract_ids': ['@mozilla.org/widget/appshell/mac;1'],
+ 'legacy_constructor': 'nsAppShellConstructor',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_UTILITY_AND_GMPLUGIN_PROCESS,
+ },
+ {
+ 'cid': '{b148eed2-236d-11d3-b35c-00a0cc3c1cde}',
+ 'contract_ids': ['@mozilla.org/sound;1'],
+ 'type': 'nsSound',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{8b5314bc-db01-11d2-96ce-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/transferable;1'],
+ 'type': 'nsTransferable',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{948a0023-e3a7-11d2-96cf-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/htmlformatconverter;1'],
+ 'type': 'nsHTMLFormatConverter',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}',
+ 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'],
+ 'type': 'nsClipboardHelper',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{9a155bb2-2b67-45de-83e3-13a9dacf8336}',
+ 'contract_ids': ['@mozilla.org/widget/parent/dragservice;1'],
+ 'type': 'nsDragService',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{f0ddedd7-e8d5-4f95-a5b4-0f48f1741b36}',
+ 'contract_ids': ['@mozilla.org/gfx/parent/screenmanager;1'],
+ 'type': 'mozilla::widget::ScreenManager',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ 'singleton': True,
+ },
+ {
+ 'cid': '{d3f69889-e13a-4321-980c-a39332e21f34}',
+ 'contract_ids': ['@mozilla.org/gfx/devicecontextspec;1'],
+ 'type': 'nsDeviceContextSpecX',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{a6cf9129-15b3-11d2-932e-00805f8add32}',
+ 'contract_ids': ['@mozilla.org/gfx/printerlist;1'],
+ 'type': 'nsPrinterListCUPS',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{841387c8-72e6-484b-9296-bf6eea80d58a}',
+ 'contract_ids': ['@mozilla.org/gfx/printsettings-service;1'],
+ 'type': 'nsPrintSettingsServiceX',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{06beec76-a183-4d9f-85dd-085f26da565a}',
+ 'contract_ids': ['@mozilla.org/widget/printdialog-service;1'],
+ 'type': 'nsPrintDialogServiceX',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{6987230e-0089-4e78-bc5f-1493ee7519fa}',
+ 'contract_ids': ['@mozilla.org/widget/useridleservice;1'],
+ 'type': 'nsUserIdleServiceX',
+ 'singleton': True,
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{84e11f80-ca55-11dd-ad8b-0800200c9a66}',
+ 'contract_ids': ['@mozilla.org/system-alerts-service;1'],
+ 'type': 'mozilla::OSXNotificationCenter',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{2451baed-8dc3-46d9-9e30-96e1baa03666}',
+ 'contract_ids': ['@mozilla.org/widget/macdocksupport;1'],
+ 'type': 'nsMacDockSupport',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{74ea4101-a5bb-49bc-9984-66da8b225a37}',
+ 'contract_ids': ['@mozilla.org/widget/macfinderprogress;1'],
+ 'type': 'nsMacFinderProgress',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{de59fe1a-46c8-490f-b04d-34545acb06c9}',
+ 'contract_ids': ['@mozilla.org/widget/macsharingservice;1'],
+ 'type': 'nsMacSharingService',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{29046c8f-cba6-4ffa-9141-1685e96c4ea0}',
+ 'contract_ids': ['@mozilla.org/widget/macuseractivityupdater;1'],
+ 'type': 'nsMacUserActivityUpdater',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{e9096367-ddd9-45e4-b762-49c0c18b7119}',
+ 'contract_ids': ['@mozilla.org/widget/mac-web-app-utils;1'],
+ 'type': 'nsMacWebAppUtils',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{1f39ae50-b6a0-4b37-90f4-60af614193d8}',
+ 'contract_ids': ['@mozilla.org/widget/standalonenativemenu;1'],
+ 'type': 'nsStandaloneNativeMenu',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{b6e1a890-b2b8-4883-a65f-9476f6185313}',
+ 'contract_ids': ['@mozilla.org/widget/systemstatusbar;1'],
+ 'type': 'nsSystemStatusBarCocoa',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{38f396e2-93c9-4a77-aaf7-2d50b9962186}',
+ 'contract_ids': ['@mozilla.org/widget/touchbarupdater;1'],
+ 'type': 'nsTouchBarUpdater',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS,
+ },
+]
diff --git a/widget/cocoa/crashtests/373122-1-inner.html b/widget/cocoa/crashtests/373122-1-inner.html
new file mode 100644
index 0000000000..5c14166b75
--- /dev/null
+++ b/widget/cocoa/crashtests/373122-1-inner.html
@@ -0,0 +1,39 @@
+<html>
+<head>
+
+<script>
+function boom()
+{
+ document.body.style.position = "fixed"
+
+ setTimeout(boom2, 1);
+}
+
+function boom2()
+{
+ lappy = document.getElementById("lappy");
+ lappy.style.display = "none"
+
+ setTimeout(boom3, 200);
+}
+
+function boom3()
+{
+ dump("Reloading\n");
+ location.reload();
+}
+
+</script>
+
+
+</head>
+
+
+<body bgcolor="black" onload="boom()">
+
+ <span style="overflow: scroll; display: -moz-box;"></span>
+
+ <embed id="lappy" src="" width=550 height=400 TYPE="application/x-shockwave-flash" ></embed>
+
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/373122-1.html b/widget/cocoa/crashtests/373122-1.html
new file mode 100644
index 0000000000..a57e5f4249
--- /dev/null
+++ b/widget/cocoa/crashtests/373122-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 1000);
+</script>
+<body>
+<iframe src="373122-1-inner.html"></iframe>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/397209-1.html b/widget/cocoa/crashtests/397209-1.html
new file mode 100644
index 0000000000..554b2dac72
--- /dev/null
+++ b/widget/cocoa/crashtests/397209-1.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+</head>
+<body>
+<button style="width: 8205em;"></button>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/403296-1.xhtml b/widget/cocoa/crashtests/403296-1.xhtml
new file mode 100644
index 0000000000..800eaa3558
--- /dev/null
+++ b/widget/cocoa/crashtests/403296-1.xhtml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ class="reftest-wait"
+ style="margin: 12em; padding: 20px 10em; opacity: 0.2; font-size: 11.2px; -moz-appearance: toolbar; white-space: nowrap;"><body
+ style="position: absolute;"
+ onload="setTimeout(function() { document.body.removeChild(document.getElementById('tr')); document.documentElement.removeAttribute('class'); }, 30);">
+
+xxx
+yyy
+
+<tr id="tr">300</tr></body></html>
diff --git a/widget/cocoa/crashtests/419737-1.html b/widget/cocoa/crashtests/419737-1.html
new file mode 100644
index 0000000000..fe6e4532b4
--- /dev/null
+++ b/widget/cocoa/crashtests/419737-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<div><span style="-moz-appearance: radio; padding: 15000px;"></span></div>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/435223-1.html b/widget/cocoa/crashtests/435223-1.html
new file mode 100644
index 0000000000..c7f70860fc
--- /dev/null
+++ b/widget/cocoa/crashtests/435223-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<div style="min-width: max-content;"><div style="-moz-appearance: button;"><div style="margin: 0 100%;"></div></div></div>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/444260-1.xhtml b/widget/cocoa/crashtests/444260-1.xhtml
new file mode 100644
index 0000000000..f1a84023df
--- /dev/null
+++ b/widget/cocoa/crashtests/444260-1.xhtml
@@ -0,0 +1,3 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<hbox><button width="7788025414616">S</button></hbox>
+</window>
diff --git a/widget/cocoa/crashtests/444864-1.html b/widget/cocoa/crashtests/444864-1.html
new file mode 100644
index 0000000000..f8bac76e6a
--- /dev/null
+++ b/widget/cocoa/crashtests/444864-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div style="padding: 10px;"><input type="button" value="Go" style="letter-spacing: 331989pt;"></div>
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/449111-1.html b/widget/cocoa/crashtests/449111-1.html
new file mode 100644
index 0000000000..4494591803
--- /dev/null
+++ b/widget/cocoa/crashtests/449111-1.html
@@ -0,0 +1,4 @@
+<html>
+<head></head>
+<body><div style="display: -moz-box; word-spacing: 549755813889px;"><button>T </button></div></body>
+</html>
diff --git a/widget/cocoa/crashtests/460349-1.xhtml b/widget/cocoa/crashtests/460349-1.xhtml
new file mode 100644
index 0000000000..cc9b9700c7
--- /dev/null
+++ b/widget/cocoa/crashtests/460349-1.xhtml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head></head>
+<body><div><mstyle xmlns="http://www.w3.org/1998/Math/MathML" style="-moz-appearance: button;"/></div></body>
+</html>
diff --git a/widget/cocoa/crashtests/460387-1.html b/widget/cocoa/crashtests/460387-1.html
new file mode 100644
index 0000000000..cab7e7eb32
--- /dev/null
+++ b/widget/cocoa/crashtests/460387-1.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<html><head></head><body><div style="display: table; padding: 625203mm; -moz-appearance: menulist;"></div></body></html>
diff --git a/widget/cocoa/crashtests/464589-1.html b/widget/cocoa/crashtests/464589-1.html
new file mode 100644
index 0000000000..d25d92315d
--- /dev/null
+++ b/widget/cocoa/crashtests/464589-1.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ var o2 = document.createElement("option");
+ document.getElementById("o1").appendChild(o2);
+ o2.style.padding = "131072cm";
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+
+<select><option id="o1" style="height: 0cm;"></option></select>
+
+</body>
+</html>
diff --git a/widget/cocoa/crashtests/crashtests.list b/widget/cocoa/crashtests/crashtests.list
new file mode 100644
index 0000000000..0559273a43
--- /dev/null
+++ b/widget/cocoa/crashtests/crashtests.list
@@ -0,0 +1,11 @@
+skip-if(!cocoaWidget) load 373122-1.html # bug 1300017
+load 397209-1.html
+load 403296-1.xhtml
+load 419737-1.html
+load 435223-1.html
+load chrome://reftest/content/crashtests/widget/cocoa/crashtests/444260-1.xhtml
+load 444864-1.html
+load 449111-1.html
+load 460349-1.xhtml
+load 460387-1.html
+load 464589-1.html
diff --git a/widget/cocoa/cursors/arrowN.png b/widget/cocoa/cursors/arrowN.png
new file mode 100644
index 0000000000..c65924091a
--- /dev/null
+++ b/widget/cocoa/cursors/arrowN.png
Binary files differ
diff --git a/widget/cocoa/cursors/arrowN@2x.png b/widget/cocoa/cursors/arrowN@2x.png
new file mode 100644
index 0000000000..496f856a1d
--- /dev/null
+++ b/widget/cocoa/cursors/arrowN@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/arrowS.png b/widget/cocoa/cursors/arrowS.png
new file mode 100644
index 0000000000..3975e0837b
--- /dev/null
+++ b/widget/cocoa/cursors/arrowS.png
Binary files differ
diff --git a/widget/cocoa/cursors/arrowS@2x.png b/widget/cocoa/cursors/arrowS@2x.png
new file mode 100644
index 0000000000..c7f817afd4
--- /dev/null
+++ b/widget/cocoa/cursors/arrowS@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/cell.png b/widget/cocoa/cursors/cell.png
new file mode 100644
index 0000000000..1400d93f6e
--- /dev/null
+++ b/widget/cocoa/cursors/cell.png
Binary files differ
diff --git a/widget/cocoa/cursors/cell@2x.png b/widget/cocoa/cursors/cell@2x.png
new file mode 100644
index 0000000000..5a1543b16b
--- /dev/null
+++ b/widget/cocoa/cursors/cell@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/colResize.png b/widget/cocoa/cursors/colResize.png
new file mode 100644
index 0000000000..ef4b24936b
--- /dev/null
+++ b/widget/cocoa/cursors/colResize.png
Binary files differ
diff --git a/widget/cocoa/cursors/colResize@2x.png b/widget/cocoa/cursors/colResize@2x.png
new file mode 100644
index 0000000000..d868ee6b57
--- /dev/null
+++ b/widget/cocoa/cursors/colResize@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/help.png b/widget/cocoa/cursors/help.png
new file mode 100644
index 0000000000..7070b4b7bc
--- /dev/null
+++ b/widget/cocoa/cursors/help.png
Binary files differ
diff --git a/widget/cocoa/cursors/help@2x.png b/widget/cocoa/cursors/help@2x.png
new file mode 100644
index 0000000000..7ddf157cd3
--- /dev/null
+++ b/widget/cocoa/cursors/help@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/move.png b/widget/cocoa/cursors/move.png
new file mode 100644
index 0000000000..f6291500a4
--- /dev/null
+++ b/widget/cocoa/cursors/move.png
Binary files differ
diff --git a/widget/cocoa/cursors/move@2x.png b/widget/cocoa/cursors/move@2x.png
new file mode 100644
index 0000000000..d094ce7a15
--- /dev/null
+++ b/widget/cocoa/cursors/move@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/rowResize.png b/widget/cocoa/cursors/rowResize.png
new file mode 100644
index 0000000000..7f68a4007c
--- /dev/null
+++ b/widget/cocoa/cursors/rowResize.png
Binary files differ
diff --git a/widget/cocoa/cursors/rowResize@2x.png b/widget/cocoa/cursors/rowResize@2x.png
new file mode 100644
index 0000000000..e0987a3b95
--- /dev/null
+++ b/widget/cocoa/cursors/rowResize@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNE.png b/widget/cocoa/cursors/sizeNE.png
new file mode 100644
index 0000000000..fd71dea6ab
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNE.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNE@2x.png b/widget/cocoa/cursors/sizeNE@2x.png
new file mode 100644
index 0000000000..400f5fe46f
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNE@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNESW.png b/widget/cocoa/cursors/sizeNESW.png
new file mode 100644
index 0000000000..5b2c300f4a
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNESW.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNESW@2x.png b/widget/cocoa/cursors/sizeNESW@2x.png
new file mode 100644
index 0000000000..7299b98be1
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNESW@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNS.png b/widget/cocoa/cursors/sizeNS.png
new file mode 100644
index 0000000000..12b1602f14
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNS.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNS@2x.png b/widget/cocoa/cursors/sizeNS@2x.png
new file mode 100644
index 0000000000..0dc7d15d75
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNS@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNW.png b/widget/cocoa/cursors/sizeNW.png
new file mode 100644
index 0000000000..57d270e0db
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNW.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNW@2x.png b/widget/cocoa/cursors/sizeNW@2x.png
new file mode 100644
index 0000000000..312ee61ce4
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNW@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNWSE.png b/widget/cocoa/cursors/sizeNWSE.png
new file mode 100644
index 0000000000..d33c2486e4
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNWSE.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeNWSE@2x.png b/widget/cocoa/cursors/sizeNWSE@2x.png
new file mode 100644
index 0000000000..ecf1438265
--- /dev/null
+++ b/widget/cocoa/cursors/sizeNWSE@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSE.png b/widget/cocoa/cursors/sizeSE.png
new file mode 100644
index 0000000000..1689138419
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSE.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSE@2x.png b/widget/cocoa/cursors/sizeSE@2x.png
new file mode 100644
index 0000000000..7abce00fd5
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSE@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSW.png b/widget/cocoa/cursors/sizeSW.png
new file mode 100644
index 0000000000..5eadafb054
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSW.png
Binary files differ
diff --git a/widget/cocoa/cursors/sizeSW@2x.png b/widget/cocoa/cursors/sizeSW@2x.png
new file mode 100644
index 0000000000..b9ad862aa5
--- /dev/null
+++ b/widget/cocoa/cursors/sizeSW@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/vtIBeam.png b/widget/cocoa/cursors/vtIBeam.png
new file mode 100644
index 0000000000..4609922319
--- /dev/null
+++ b/widget/cocoa/cursors/vtIBeam.png
Binary files differ
diff --git a/widget/cocoa/cursors/vtIBeam@2x.png b/widget/cocoa/cursors/vtIBeam@2x.png
new file mode 100644
index 0000000000..a001362b04
--- /dev/null
+++ b/widget/cocoa/cursors/vtIBeam@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomIn.png b/widget/cocoa/cursors/zoomIn.png
new file mode 100644
index 0000000000..7ddfd4056d
--- /dev/null
+++ b/widget/cocoa/cursors/zoomIn.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomIn@2x.png b/widget/cocoa/cursors/zoomIn@2x.png
new file mode 100644
index 0000000000..b1b844fa8c
--- /dev/null
+++ b/widget/cocoa/cursors/zoomIn@2x.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomOut.png b/widget/cocoa/cursors/zoomOut.png
new file mode 100644
index 0000000000..ebacf25889
--- /dev/null
+++ b/widget/cocoa/cursors/zoomOut.png
Binary files differ
diff --git a/widget/cocoa/cursors/zoomOut@2x.png b/widget/cocoa/cursors/zoomOut@2x.png
new file mode 100644
index 0000000000..5f84b767ec
--- /dev/null
+++ b/widget/cocoa/cursors/zoomOut@2x.png
Binary files differ
diff --git a/widget/cocoa/docs/index.md b/widget/cocoa/docs/index.md
new file mode 100644
index 0000000000..cda6b2e349
--- /dev/null
+++ b/widget/cocoa/docs/index.md
@@ -0,0 +1,9 @@
+# Firefox on macOS
+
+```{toctree}
+:titlesonly:
+:maxdepth: 1
+:glob:
+
+*
+```
diff --git a/widget/cocoa/docs/macos-apis.md b/widget/cocoa/docs/macos-apis.md
new file mode 100644
index 0000000000..f0538dfb76
--- /dev/null
+++ b/widget/cocoa/docs/macos-apis.md
@@ -0,0 +1,188 @@
+# Using macOS APIs
+
+With each new macOS release, new APIs are added. Due to the wide range of platforms that Firefox runs on,
+and due to the [wide range of SDKs that we support building with](sdks.md#supported-sdks),
+using macOS APIs in Firefox requires some extra care.
+
+## Availability of APIs, and runtime checks
+
+First of all, if you use an API that is supported by all versions of macOS that Firefox runs on,
+i.e. 10.15 and above, then you don't need to worry about anything:
+The API declaration will be present in any of the supported SDKs, and you don't need any runtime checks.
+
+If you want to use a macOS API that was added after 10.15, then you have to have a runtime check.
+This requirement is completely independent of what SDK is being used for building.
+
+The runtime check [should have the following form](https://developer.apple.com/documentation/macos-release-notes/appkit-release-notes-for-macos-11?language=objc)
+(replace `11.0` with the appropriate version):
+
+```objc++
+if (@available(macOS 11.0, *)) {
+ // Code for macOS 11.0 or later
+} else {
+ // Code for versions earlier than 11.0.
+}
+```
+
+`@available` guards can be used in Objective-C(++) code.
+(In C++ code, you can use [these `nsCocoaFeatures` methods](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/nsCocoaFeatures.h#21-27) instead.)
+
+For each API, the API declarations in the SDK headers are annotated with `API_AVAILABLE` macros.
+For example, the definition of the `NSVisualEffectMaterial` enum looks like this:
+
+```objc++
+typedef NS_ENUM(NSInteger, NSVisualEffectMaterial) {
+ NSVisualEffectMaterialTitlebar = 3,
+ NSVisualEffectMaterialSelection = 4,
+ NSVisualEffectMaterialMenu API_AVAILABLE(macos(10.11)) = 5,
+ // [...]
+ NSVisualEffectMaterialSheet API_AVAILABLE(macos(10.14)) = 11,
+ // [...]
+} API_AVAILABLE(macos(10.10));
+```
+
+The compiler understands these annotations and makes sure that you wrap all uses of the annotated APIs
+in appropriate `@available` runtime checks.
+
+### Frameworks
+
+In some rare cases, you need functionality from frameworks that are not available on all supported macOS versions.
+Examples of this are `Metal.framework` (added in 10.11) and `MediaPlayer.framework` (added in 10.12.2).
+
+In that case, you can either `dlopen` your framework at runtime ([like we do for MediaPlayer](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/MediaPlayerWrapper.mm#21-27)),
+or you can [use `-weak_framework`](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html#//apple_ref/doc/uid/20002378-107026)
+[like we do for Metal](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/toolkit/library/moz.build#301-304):
+
+```python
+if CONFIG['OS_ARCH'] == 'Darwin':
+ OS_LIBS += [
+ # Link to Metal as required by the Metal gfx-hal backend
+ '-weak_framework Metal',
+ ]
+```
+
+## Using new APIs with old SDKs
+
+If you want to use an API that was introduced after 10.15, you now have one extra thing to worry about.
+In addition to the runtime check [described in the previous section](#using-macos-apis), you also
+have to jump through extra hoops in order to allow the build to succeed, because
+[our build target for Firefox has to remain at 10.15 in order for Firefox to run on macOS versions all the way down to macOS 10.15](sdks.md#supported-sdks).
+
+In order to make the compiler accept your code, you will need to copy some amount of the API declaration
+into your own code. Copy it from the newest recent SDK you can get your hands on.
+The exact procedure varies based on the type of API (enum, objc class, method, etc.),
+but the general approach looks like this:
+
+```objc++
+#if !defined(MAC_OS_VERSION_12_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_12_0
+@interface NSScreen (NSScreen12_0)
+// https://developer.apple.com/documentation/appkit/nsscreen/3882821-safeareainsets?language=objc&changes=latest_major
+@property(readonly) NSEdgeInsets safeAreaInsets;
+@end
+#endif
+```
+
+See the [Supporting Multiple SDKs](sdks.md#supporting-multiple-sdks) docs for more information on the `MAC_OS_X_VERSION_MAX_ALLOWED` macro.
+
+Keep these three things in mind:
+
+ - Copy only what you need.
+ - Wrap your declaration in `MAC_OS_X_VERSION_MAX_ALLOWED` checks so that, if an SDK is used that
+ already contains these declarations, your declaration does not conflict with the declaration in the SDK.
+ - Include the `API_AVAILABLE` annotations so that the compiler can protect you from accidentally
+ calling the API on unsupported macOS versions.
+
+Our current code does not always follow the `API_AVAILABLE` advice, but it should.
+
+### Enum types and C structs
+
+If you need a new enum type or C struct, copy the entire type declaration and wrap it in the appropriate ifdefs. Example:
+
+```objc++
+#if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2
+typedef NS_ENUM(NSUInteger, MPNowPlayingPlaybackState) {
+ MPNowPlayingPlaybackStateUnknown = 0,
+ MPNowPlayingPlaybackStatePlaying,
+ MPNowPlayingPlaybackStatePaused,
+ MPNowPlayingPlaybackStateStopped,
+ MPNowPlayingPlaybackStateInterrupted
+} MP_API(ios(11.0), tvos(11.0), macos(10.12.2), watchos(5.0));
+#endif
+```
+### New enum values for existing enum type
+
+If the enum type itself already exists, but gained a new value, define the value in an unnamed enum:
+
+```objc++
+#if !defined(MAC_OS_X_VERSION_10_12) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12
+enum { NSVisualEffectMaterialSelection = 4 };
+#endif
+```
+
+(This is an example of an interesting case: `NSVisualEffectMaterialSelection` is available starting with
+macOS 10.10, but it's only defined in SDKs starting with the 10.12 SDK.)
+
+### Objective-C classes
+
+For a new Objective-C class, copy the entire `@interface` declaration and wrap it in the appropriate ifdefs.
+
+I haven't personally tested this. If this does not compile (or maybe link?), you can use the following workaround:
+
+ - Define your methods and properties as a category on `NSObject`.
+ - Look up the class at runtime using `NSClassFromString()`.
+ - If you need to create a subclass, do it at runtime using `objc_allocateClassPair` and `class_addMethod`.
+ [Here's an example of that.](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/VibrancyManager.mm#44-60)
+
+### Objective-C properties and methods on an existing class
+
+If an Objective-C class that already exists gains a new method or property, you can "add" it to the
+existing class declaration with the help of a [category](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html):
+
+```objc++
+@interface ExistingClass (YourMadeUpCategoryName)
+// methods and properties here
+@end
+```
+
+### Functions
+
+With free-standing functions I'm not entirely sure what to do.
+In theory, copying the declarations from the new SDK headers should work. Example:
+
+```objc++
+extern "C" {
+ __attribute__((warn_unused_result)) bool
+SecTrustEvaluateWithError(SecTrustRef trust, CFErrorRef _Nullable * _Nullable CF_RETURNS_RETAINED error)
+ API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0));
+
+ __nullable
+CFDataRef SecCertificateCopyNormalizedSubjectSequence(SecCertificateRef certificate)
+ __OSX_AVAILABLE_STARTING(__MAC_10_12_4, __IPHONE_10_3);
+}
+```
+
+I'm not sure what the linker or the dynamic linker do when the symbol is not available.
+Does this require [`__attribute__((weak_import))` annotations](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html#//apple_ref/doc/uid/20002378-107262-CJBJAEID)?
+
+And maybe this is where .tbd files in the SDK come in? So that the linker knows which symbols to allow?
+So then that part cannot be worked around by copying code from headers.
+
+Anyway, what always works is the pure runtime approach:
+
+ 1. Define types for the functions you need, but not the functions themselves.
+ 2. At runtime, look up the functions using `dlsym`.
+
+## Notes on Rust
+
+If you call macOS APIs from Rust code, you're kind of on your own. Apple does not provide any Rust
+"headers", so there isn't really an SDK to speak of. So you have to supply your own API declarations
+anyway, regardless of what SDK is being used for building.
+
+In a way, you're side-stepping some of the build time trouble. You don't need to worry about any
+`#ifdefs` because there are no system headers you could conflict with.
+
+On the other hand, you still need to worry about API availability at runtime.
+And in Rust, there are no [availability attributes](https://clang.llvm.org/docs/AttributeReference.html#availability)
+on your API declarations, and there are no
+[`@available` runtime check helpers](https://clang.llvm.org/docs/LanguageExtensions.html#objective-c-available),
+and the compiler cannot warn you if you call APIs outside of availability checks.
diff --git a/widget/cocoa/docs/sdks.md b/widget/cocoa/docs/sdks.md
new file mode 100644
index 0000000000..66565571bb
--- /dev/null
+++ b/widget/cocoa/docs/sdks.md
@@ -0,0 +1,227 @@
+# A primer on macOS SDKs
+
+## Overview
+
+A macOS SDK is an on-disk directory that contains header files and meta information for macOS APIs.
+Apple distributes SDKs as part of the Xcode app bundle. Each Xcode version comes with one macOS SDK,
+the SDK for the most recent released version of macOS at the time of the Xcode release.
+The SDK is located at `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`.
+
+Compiling Firefox for macOS requires a macOS SDK. The build system bootstraps an adequate SDK by
+default, but you can select a different SDK using the `mozconfig` option `--with-macos-sdk`:
+
+```text
+ac_add_options --with-macos-sdk=/Users/username/SDKs/MacOSX11.3.sdk
+```
+
+## Supported SDKs
+
+First off, Firefox runs on 10.15 and above. This is called the "minimum deployment target" and is
+independent of the SDK version.
+
+Our official Firefox builds compiled in CI (continuous integration) currently use the 13.3 SDK (last updated in [bug 1833998](https://bugzilla.mozilla.org/show_bug.cgi?id=1833998)). This is also the minimum supported SDK version for local builds.
+
+Compiling with different SDKs breaks from time to time. Such breakages should be [reported in Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?blocked=mach-busted&bug_type=defect&cc=:spohl,:mstange&component=General&form_name=enter_bug&keywords=regression&op_sys=macOS&product=Firefox%20Build%20System&rep_platform=All) and fixed quickly.
+
+## Obtaining SDKs
+
+Sometimes you need an SDK that's different from the one in your Xcode.app, for example
+to check whether your code change breaks building with other SDKs, or to verify the
+runtime behavior with the SDK used for CI builds.
+
+The easy but slightly questionable way to obtain an SDK is to download it from a public github repo.
+
+Here's another option:
+
+ 1. Have your Apple ID login details ready, and bring enough time and patience for a 5GB download.
+ 2. Check [these tables in the Xcode wikipedia article](https://en.wikipedia.org/wiki/Xcode#Xcode_7.0_-_10.x_(since_Free_On-Device_Development))
+ and find an Xcode version that contains the SDK you need.
+ 3. Look up the Xcode version number on [xcodereleases.com](https://xcodereleases.com/) and click the Download link for it.
+ 4. Log in with your Apple ID. Then the download should start.
+ 5. Wait for the 5GB Xcode_*.xip download to finish.
+ 6. Open the downloaded xip file. This will extract the Xcode.app bundle.
+ 7. Inside the app bundle, the SDK is at `Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`.
+
+## Effects of the SDK version
+
+An SDK only contains declarations of APIs. It does not contain the implementations for these APIs.
+
+The implementation of an API is provided by the OS that the app runs on. It is supplied at runtime,
+when your app starts up, by the dynamic linker. For example, the AppKit implementation comes
+from `/System/Library/Frameworks/AppKit.framework` from the OS that the app is run on, regardless
+of what SDK was used when compiling the app.
+
+In other words, building with a macOS SDK of a higher version doesn't magically make new APIs available
+when running on older versions of macOS. And, conversely, building with a lower macOS SDK doesn't limit
+which APIs you can use if your app is run on a newer version of macOS, assuming you manage to convince the
+compiler to accept your code.
+
+The SDK used for building an app determines three things:
+
+ 1. Whether your code compiles at all,
+ 2. which range of macOS versions your app can run on (available deployment targets), and
+ 3. certain aspects of runtime behavior.
+
+The first is straightforward: An SDK contains header files. If you call an API that's not declared
+anywhere - neither in a header file nor in your own code - then your compiler will emit an error.
+(Special case: Calling an unknown Objective-C method usually only emits a warning, not an error.)
+
+The second aspect, available deployment targets, is usually not worth worrying about:
+SDKs have large ranges of supported macOS deployment targets.
+For example, the 10.15 SDK supports running your app on macOS versions all the way back to 10.6.
+This information is written down in the SDK's `SDKSettings.plist`.
+
+The third aspect, varying runtime behavior, is perhaps the most insidious and surprising aspect, and is described
+in the next section.
+
+## Runtime differences based on macOS SDK version
+
+When a new version of macOS is released, existing APIs can change their behavior.
+These changes are usually described in the AppKit release notes:
+
+ - [macOS 10.15 release notes](https://developer.apple.com/documentation/macos_release_notes/macos_catalina_10_15_release_notes?language=objc)
+ - [macOS 10.14 AppKit release notes](https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes/appkit_release_notes_for_macos_10_14?language=objc)
+ - [macOS 10.13 AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/)
+ - [macOS 10.12 and older AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKitOlderNotes/)
+
+Sometimes, these differences in behavior have the potential to break existing apps. In those instances,
+Apple often provides the old (compatible) behavior until the app is re-built with the new SDK, expecting
+developers to update their apps so that they work with the new behavior, at the same time as
+they update to the new SDK.
+
+Here's an [example from the 10.13 release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling):
+
+> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13.
+
+Here, "linked on or after macOS 10.13" means "linked against the macOS 10.13 SDK or newer".
+
+Apple's expectation is that you upgrade to the new macOS version when it is released, download a new
+Xcode version when it is released, synchronize these updates across the machines of all developers
+that work on your app, use the SDK in the newest Xcode to compile your app, and make changes to your
+app to be compatible with any behavior changes whenever you update Xcode.
+This expectation does not always match reality. It definitely doesn't match what we're doing with Firefox.
+
+For Firefox, SDK-dependent compatibility behaviors mean that developers who build Firefox locally
+can see different runtime behavior than the users of our CI builds, if they use a different SDK than
+the SDK used in CI.
+That is, unless we change the Firefox code so that it has the same behavior regardless of SDK version.
+Often this can be achieved by using APIs in a way that's more in line with the API's recommended use.
+
+For example, we've had cases of
+[broken placeholder text in search fields](https://bugzilla.mozilla.org/show_bug.cgi?id=1273106),
+[missing](https://bugzilla.mozilla.org/show_bug.cgi?id=941325) or [double-drawn focus rings](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/nsNativeThemeCocoa.mm#149-169),
+[a startup crash](https://bugzilla.mozilla.org/show_bug.cgi?id=1516437),
+[fully black windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1494022),
+[fully gray windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1576113#c4),
+[broken vibrancy](https://bugzilla.mozilla.org/show_bug.cgi?id=1475694), and
+[broken colors in dark mode](https://bugzilla.mozilla.org/show_bug.cgi?id=1578917).
+
+In most of these cases, the breakage was either very minor, or it was caused by Firefox doing things
+that were explicitly discouraged, like creating unexpected NSView hierarchies, or relying on unspecified
+implementation details. (With one exception: In 10.14, HiDPI-aware `NSOpenGLContext` rendering in
+layer-backed windows simply broke.)
+
+And in all of these cases, it was the SDK-dependent compatibility behavior that protected our users from being
+exposed to the breakage. Our CI builds continued to work because they were built with an older SDK.
+
+We have addressed all known cases of breakage when building Firefox with newer SDKs.
+I am not aware of any current instances of this problem as of this writing (June 2020).
+
+For more information about how these compatibility tricks work,
+read the [Overriding SDK-dependent runtime behavior](#overriding-sdk-dependent-runtime-behavior) section.
+
+## Supporting multiple SDKs
+
+As described under [Supported SDKs](#supported-sdks), Firefox can be built with a wide variety of SDK versions.
+
+This ability comes at the cost of some manual labor; it requires some well-placed `#ifdefs` and
+copying of header definitions.
+
+Every SDK defines the macro `MAC_OS_X_VERSION_MAX_ALLOWED` with a value that matches the SDK version,
+in the SDK's `AvailabilityMacros.h` header. This header also defines version constants like `MAC_OS_X_VERSION_10_12`.
+For example, I have a version of the 10.12 SDK which contains the line
+
+```cpp
+#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_12_4
+```
+
+The name `MAC_OS_X_VERSION_MAX_ALLOWED` is rather misleading; a better name would be
+`MAC_OS_X_VERSION_MAX_KNOWN_BY_SDK`. Compiling with an old SDK *does not* prevent apps from running
+on newer versions of macOS.
+
+With the help of the `MAC_OS_X_VERSION_MAX_ALLOWED` macro, we can make our code adapt to the SDK that's
+being used. Here's [an example](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/toolkit/xre/MacApplicationDelegate.mm#345-351) where the 10.14 SDK changed the signature of
+[an `NSApplicationDelegate` method](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428471-application?language=objc):
+
+```objc++
+- (BOOL)application:(NSApplication*)application
+ continueUserActivity:(NSUserActivity*)userActivity
+#if defined(MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14
+ restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>>*))restorationHandler {
+#else
+ restorationHandler:(void (^)(NSArray*))restorationHandler {
+#endif
+ ...
+}
+```
+
+We can also use this macro to supply missing API definitions in such a way that
+they don't conflict with the definitions from the SDK.
+This is described in the "Using macOS APIs" document, under [Using new APIs with old SDKs](./macos-apis.md#using-new-apis-with-old-sdks).
+
+## Overriding SDK-dependent runtime behavior
+
+This section contains some more details on the compatibility tricks that cause different runtime
+behavior dependent on the SDK, as described in
+[Runtime differences based on macOS SDK version](#runtime-differences-based-on-macos-sdk-version).
+
+### How it works
+
+AppKit is the one system framework I know of that employs these tricks. Let's explore how AppKit makes this work,
+by going back to the [NSCollectionView example](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling) from above:
+
+> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13.
+
+For each of these SDK-dependent behavior differences, both the old and the new behavior are implemented
+in the version of AppKit that ships with the new macOS version.
+At runtime, AppKit selects one of the behaviors based on the SDK version, with a call to
+`_CFExecutableLinkedOnOrAfter()`. This call checks the SDK version of the main executable of the
+process that's running AppKit code; in our case that's the `firefox` or `plugin-container` executable.
+The SDK version is stored in the mach-o headers of the executable by the linker.
+
+One interesting design aspect of AppKit's compatibility tricks is the fact that most of these behavior differences
+can be toggled with a "user default" preference.
+For example, the "responsive scrolling in NSCollectionViews" behavior change can be controlled with
+a user default with the name "NSCollectionViewPrefetchingEnabled".
+The SDK check only happens if "NSCollectionViewPrefetchingEnabled" is not set to either YES or NO.
+
+More precisely, this example works as follows:
+
+ - `-[NSCollectionView prepareContentInRect:]` is the function that supports both the old and the new behavior.
+ - It calls `_NSGetBoolAppConfig` for the value "NSCollectionViewPrefetchingEnabled", and also supplies a "default
+ value function".
+ - If the user default is not set, the default value function is called. This function has the name
+ `NSCollectionViewPrefetchingEnabledDefaultValueFunction`.
+ - `NSCollectionViewPrefetchingEnabledDefaultValueFunction` calls `_CFExecutableLinkedOnOrAfter(13)`.
+
+You can find many similar toggles if you list the AppKit symbols that end in `DefaultValueFunction`,
+for example by executing `nm /System/Library/Frameworks/AppKit.framework/AppKit | grep DefaultValueFunction`.
+
+### Overriding SDK-dependent runtime behavior
+
+You can set these preferences programmatically, in a way that `_NSGetBoolAppConfig()` can pick them up,
+for example with [`registerDefaults`](https://developer.apple.com/documentation/foundation/nsuserdefaults/1417065-registerdefaults?language=objc)
+or like this:
+
+```objc++
+[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSViewAllowsRootLayerBacking"];
+```
+
+The AppKit release notes mention this ability but ask for it to only be used for debugging purposes:
+
+> In some cases, we provide defaults (preferences) settings which can be used to get the old or new behavior,
+> independent of what system an application was built against. Often these preferences are provided for
+> debugging purposes only; in some cases the preferences can be used to globally modify the behavior
+> of an application by registering the values (do it somewhere very early, with `-[NSUserDefaults registerDefaults:]`).
+
+It's interesting that they mention this at all because, as far as I can tell, none of these values are documented.
diff --git a/widget/cocoa/metrics.yaml b/widget/cocoa/metrics.yaml
new file mode 100644
index 0000000000..d354da069e
--- /dev/null
+++ b/widget/cocoa/metrics.yaml
@@ -0,0 +1,11 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: Widget: Cocoa'
diff --git a/widget/cocoa/moz.build b/widget/cocoa/moz.build
new file mode 100644
index 0000000000..ebee9439e0
--- /dev/null
+++ b/widget/cocoa/moz.build
@@ -0,0 +1,181 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+ SCHEDULES.exclusive = ["macosx"]
+
+with Files("*TextInput*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+XPIDL_SOURCES += [
+ "nsPIWidgetCocoa.idl",
+]
+
+XPIDL_MODULE = "widget_cocoa"
+
+EXPORTS += [
+ "CFTypeRefPtr.h",
+ "DesktopBackgroundImage.h",
+ "MediaHardwareKeysEventSourceMac.h",
+ "MediaHardwareKeysEventSourceMacMediaCenter.h",
+ "mozView.h",
+ "nsBidiKeyboard.h",
+ "nsChangeObserver.h",
+ "nsCocoaFeatures.h",
+ "nsCocoaUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "AppearanceOverride.mm",
+ "GfxInfo.mm",
+ "MOZIconHelper.mm",
+ "MOZMenuOpeningCoordinator.mm",
+ "NativeKeyBindings.mm",
+ "NativeMenuMac.mm",
+ "NativeMenuSupport.mm",
+ "nsAppShell.mm",
+ "nsBidiKeyboard.mm",
+ "nsCocoaFeatures.mm",
+ "nsCocoaUtils.mm",
+ "nsCocoaWindow.mm",
+ "nsColorPicker.mm",
+ "nsCursorManager.mm",
+ "nsDeviceContextSpecX.mm",
+ "nsFilePicker.mm",
+ "nsLookAndFeel.mm",
+ "nsMacCursor.mm",
+ "nsMacDockSupport.mm",
+ "nsMacFinderProgress.mm",
+ "nsMacSharingService.mm",
+ "nsMacUserActivityUpdater.mm",
+ "nsMacWebAppUtils.mm",
+ "nsMenuBarX.mm",
+ "nsMenuGroupOwnerX.mm",
+ "nsMenuItemIconX.mm",
+ "nsMenuItemX.mm",
+ "nsMenuUtilsX.mm",
+ "nsMenuX.mm",
+ "nsPrintDialogX.mm",
+ "nsPrintSettingsServiceX.mm",
+ "nsPrintSettingsX.mm",
+ "nsSound.mm",
+ "nsStandaloneNativeMenu.mm",
+ "nsSystemStatusBarCocoa.mm",
+ "nsToolkit.mm",
+ "nsTouchBar.mm",
+ "nsTouchBarInput.mm",
+ "nsTouchBarInputIcon.mm",
+ "nsTouchBarUpdater.mm",
+ "nsUserIdleServiceX.mm",
+ "nsWidgetFactory.mm",
+ "nsWindowMap.mm",
+ "OSXNotificationCenter.mm",
+ "ScreenHelperCocoa.mm",
+ "TextInputHandler.mm",
+ "TextRecognition.mm",
+ "VibrancyManager.mm",
+ "ViewRegion.mm",
+ "WidgetTraceEvent.mm",
+]
+
+# These files cannot be built in unified mode because they cause symbol conflicts
+SOURCES += [
+ "DesktopBackgroundImage.mm",
+ "MediaHardwareKeysEventSourceMac.mm",
+ "MediaHardwareKeysEventSourceMacMediaCenter.mm",
+ "MediaKeysEventSourceFactory.cpp",
+ "nsChildView.mm",
+ "nsClipboard.mm",
+ "nsDragService.mm",
+ "nsNativeThemeCocoa.mm",
+]
+
+if not CONFIG["RELEASE_OR_BETA"] or CONFIG["MOZ_DEBUG"]:
+ SOURCES += [
+ "nsSandboxViolationSink.mm",
+ ]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/dom/events",
+ "/dom/media/platforms/apple",
+ "/layout/base",
+ "/layout/forms",
+ "/layout/generic",
+ "/layout/style",
+ "/layout/xul",
+ "/widget",
+ "/widget/headless",
+]
+
+if CONFIG["MOZ_ENABLE_SKIA_PDF"]:
+ # Skia includes because widget code includes PrintTargetSkPDF.h, and that
+ # includes skia headers.
+ LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"]
+
+RESOURCE_FILES.cursors += [
+ "cursors/arrowN.png",
+ "cursors/arrowN@2x.png",
+ "cursors/arrowS.png",
+ "cursors/arrowS@2x.png",
+ "cursors/cell.png",
+ "cursors/cell@2x.png",
+ "cursors/colResize.png",
+ "cursors/colResize@2x.png",
+ "cursors/help.png",
+ "cursors/help@2x.png",
+ "cursors/move.png",
+ "cursors/move@2x.png",
+ "cursors/rowResize.png",
+ "cursors/rowResize@2x.png",
+ "cursors/sizeNE.png",
+ "cursors/sizeNE@2x.png",
+ "cursors/sizeNESW.png",
+ "cursors/sizeNESW@2x.png",
+ "cursors/sizeNS.png",
+ "cursors/sizeNS@2x.png",
+ "cursors/sizeNW.png",
+ "cursors/sizeNW@2x.png",
+ "cursors/sizeNWSE.png",
+ "cursors/sizeNWSE@2x.png",
+ "cursors/sizeSE.png",
+ "cursors/sizeSE@2x.png",
+ "cursors/sizeSW.png",
+ "cursors/sizeSW@2x.png",
+ "cursors/vtIBeam.png",
+ "cursors/vtIBeam@2x.png",
+ "cursors/zoomIn.png",
+ "cursors/zoomIn@2x.png",
+ "cursors/zoomOut.png",
+ "cursors/zoomOut@2x.png",
+]
+
+# These resources go in $(DIST)/bin/res/MainMenu.nib, but we can't use a magic
+# RESOURCE_FILES.MainMenu.nib attribute, since that would put the files in
+# $(DIST)/bin/res/MainMenu/nib. Instead, we call __setattr__ directly to create
+# an attribute with the correct name.
+RESOURCE_FILES.__setattr__(
+ "MainMenu.nib",
+ [
+ "resources/MainMenu.nib/classes.nib",
+ "resources/MainMenu.nib/info.nib",
+ "resources/MainMenu.nib/keyedobjects.nib",
+ ],
+)
+
+OS_LIBS += [
+ "-framework IOSurface",
+ "-framework Vision",
+]
+
+SPHINX_TREES["/widget/cocoa"] = "docs"
diff --git a/widget/cocoa/mozView.h b/widget/cocoa/mozView.h
new file mode 100644
index 0000000000..deb719254e
--- /dev/null
+++ b/widget/cocoa/mozView.h
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+
+#ifndef mozView_h_
+#define mozView_h_
+
+#undef DARWIN
+#import <Cocoa/Cocoa.h>
+class nsIWidget;
+
+namespace mozilla {
+namespace widget {
+class TextInputHandler;
+} // namespace widget
+} // namespace mozilla
+
+// A protocol with some of the methods that ChildView implements. In the distant
+// past, this protocol was used by embedders: They would create their own NSView
+// subclass, implement mozView on it, and then embed a Gecko ChildView by adding
+// it as a subview of this view. This scenario no longer exists.
+// Now this protocol is mostly just used by TextInputHandler and mozAccessible
+// in order to communicate with ChildView without seeing the entire ChildView
+// interface definition.
+@protocol mozView
+
+// aHandler is Gecko's default text input handler: It implements the
+// NSTextInput protocol to handle key events. Don't make aHandler a
+// strong reference -- that causes a memory leak.
+- (void)installTextInputHandler:(mozilla::widget::TextInputHandler*)aHandler;
+- (void)uninstallTextInputHandler;
+
+// access the nsIWidget associated with this view. DOES NOT ADDREF.
+- (nsIWidget*)widget;
+
+// called when our corresponding Gecko view goes away
+- (void)widgetDestroyed;
+
+- (BOOL)isDragInProgress;
+
+// Checks whether the view is first responder or not
+- (BOOL)isFirstResponder;
+
+// Call when you dispatch an event which may cause to open context menu.
+- (void)maybeInitContextMenuTracking;
+
+@end
+
+// An informal protocol implemented by the NSWindow of the host application.
+//
+// It's used to prevent re-entrant calls to -makeKeyAndOrderFront: when gecko
+// focus/activate events propagate out to the embedder's
+// nsIEmbeddingSiteWindow::SetFocus implementation.
+@interface NSObject (mozWindow)
+
+- (BOOL)suppressMakeKeyFront;
+- (void)setSuppressMakeKeyFront:(BOOL)inSuppress;
+
+@end
+
+#endif // mozView_h_
diff --git a/widget/cocoa/nsAppShell.h b/widget/cocoa/nsAppShell.h
new file mode 100644
index 0000000000..921a166436
--- /dev/null
+++ b/widget/cocoa/nsAppShell.h
@@ -0,0 +1,96 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+/*
+ * Runs the main native Cocoa run loop, interrupting it as needed to process
+ * Gecko events.
+ */
+
+#ifndef nsAppShell_h_
+#define nsAppShell_h_
+
+#import <AppKit/NSApplication.h>
+
+#include "nsBaseAppShell.h"
+#include "nsTArray.h"
+
+class ProfilingStack;
+
+namespace mozilla {
+class nsAvailableMemoryWatcherBase;
+}
+
+// GeckoNSApplication
+//
+// Subclass of NSApplication for filtering out certain events.
+@interface GeckoNSApplication : NSApplication {
+}
+@property(readonly) BOOL didLaunch;
+@end
+
+@class AppShellDelegate;
+
+class nsAppShell : public nsBaseAppShell {
+ public:
+ NS_IMETHOD ResumeNative(void) override;
+
+ nsAppShell();
+
+ nsresult Init();
+
+ NS_IMETHOD Run(void) override;
+ NS_IMETHOD Exit(void) override;
+ NS_IMETHOD OnProcessNextEvent(nsIThreadInternal* aThread,
+ bool aMayWait) override;
+ NS_IMETHOD AfterProcessNextEvent(nsIThreadInternal* aThread,
+ bool aEventWasProcessed) override;
+
+ void OnRunLoopActivityChanged(CFRunLoopActivity aActivity);
+
+ // public only to be visible to Objective-C code that must call it
+ void WillTerminate();
+
+ static void OnMemoryPressureChanged(
+ dispatch_source_memorypressure_flags_t aPressureLevel);
+
+ protected:
+ virtual ~nsAppShell();
+
+ virtual void ScheduleNativeEventCallback() override;
+ virtual bool ProcessNextNativeEvent(bool aMayWait) override;
+
+ void InitMemoryPressureObserver();
+
+ static void ProcessGeckoEvents(void* aInfo);
+
+ protected:
+ CFMutableArrayRef mAutoreleasePools;
+
+ AppShellDelegate* mDelegate;
+ CFRunLoopRef mCFRunLoop;
+ CFRunLoopSourceRef mCFRunLoopSource;
+
+ // An observer for the profiler that is notified when the event loop enters
+ // and exits the waiting state.
+ CFRunLoopObserverRef mCFRunLoopObserver;
+
+ // Non-null while the native event loop is in the waiting state.
+ ProfilingStack* mProfilingStackWhileWaiting = nullptr;
+
+ // For getting notifications from the OS about memory pressure state changes.
+ dispatch_source_t mMemoryPressureSource = nullptr;
+
+ bool mRunningEventLoop;
+ bool mStarted;
+ bool mTerminated;
+ bool mSkippedNativeCallback;
+ bool mRunningCocoaEmbedded;
+
+ int32_t mNativeEventCallbackDepth;
+ // Can be set from different threads, so must be modified atomically
+ int32_t mNativeEventScheduledDepth;
+};
+
+#endif // nsAppShell_h_
diff --git a/widget/cocoa/nsAppShell.mm b/widget/cocoa/nsAppShell.mm
new file mode 100644
index 0000000000..3dbc527f65
--- /dev/null
+++ b/widget/cocoa/nsAppShell.mm
@@ -0,0 +1,1154 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/*
+ * Runs the main native Cocoa run loop, interrupting it as needed to process
+ * Gecko events.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+#include <dlfcn.h>
+
+#include "mozilla/AvailableMemoryWatcher.h"
+#include "CustomCocoaEvents.h"
+#include "mozilla/WidgetTraceEvent.h"
+#include "nsAppShell.h"
+#include "gfxPlatform.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsString.h"
+#include "nsIRollupListener.h"
+#include "nsIWidget.h"
+#include "nsMemoryPressure.h"
+#include "nsThreadUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsCocoaFeatures.h"
+#include "nsChildView.h"
+#include "nsToolkit.h"
+#include "TextInputHandler.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "ScreenHelperCocoa.h"
+#include "mozilla/Hal.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerThreadSleep.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "HeadlessScreenHelper.h"
+#include "MOZMenuOpeningCoordinator.h"
+#include "pratom.h"
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+# include "nsSandboxViolationSink.h"
+#endif
+
+#include <IOKit/pwr_mgt/IOPMLib.h>
+#include "nsIDOMWakeLockListener.h"
+#include "nsIPowerManagerService.h"
+
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#define WAKE_LOCK_LOG(...) \
+ MOZ_LOG(gMacWakeLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+static mozilla::LazyLogModule gMacWakeLockLog("MacWakeLock");
+
+// A wake lock listener that disables screen saver when requested by
+// Gecko. For example when we're playing video in a foreground tab we
+// don't want the screen saver to turn on.
+
+class MacWakeLockListener final : public nsIDOMMozWakeLockListener {
+ public:
+ NS_DECL_ISUPPORTS;
+
+ private:
+ ~MacWakeLockListener() {}
+
+ IOPMAssertionID mAssertionNoDisplaySleepID = kIOPMNullAssertionID;
+ IOPMAssertionID mAssertionNoIdleSleepID = kIOPMNullAssertionID;
+
+ NS_IMETHOD Callback(const nsAString& aTopic,
+ const nsAString& aState) override {
+ if (!aTopic.EqualsASCII("screen") && !aTopic.EqualsASCII("audio-playing") &&
+ !aTopic.EqualsASCII("video-playing")) {
+ return NS_OK;
+ }
+
+ // we should still hold the lock for background audio.
+ if (aTopic.EqualsASCII("audio-playing") &&
+ aState.EqualsASCII("locked-background")) {
+ WAKE_LOCK_LOG("keep audio playing even in background");
+ return NS_OK;
+ }
+
+ bool shouldKeepDisplayOn =
+ aTopic.EqualsASCII("screen") || aTopic.EqualsASCII("video-playing");
+ CFStringRef assertionType = shouldKeepDisplayOn
+ ? kIOPMAssertionTypeNoDisplaySleep
+ : kIOPMAssertionTypeNoIdleSleep;
+ IOPMAssertionID& assertionId = shouldKeepDisplayOn
+ ? mAssertionNoDisplaySleepID
+ : mAssertionNoIdleSleepID;
+ WAKE_LOCK_LOG("topic=%s, state=%s, shouldKeepDisplayOn=%d",
+ NS_ConvertUTF16toUTF8(aTopic).get(),
+ NS_ConvertUTF16toUTF8(aState).get(), shouldKeepDisplayOn);
+
+ // Note the wake lock code ensures that we're not sent duplicate
+ // "locked-foreground" notifications when multiple wake locks are held.
+ if (aState.EqualsASCII("locked-foreground")) {
+ if (assertionId != kIOPMNullAssertionID) {
+ WAKE_LOCK_LOG("already has a lock");
+ return NS_OK;
+ }
+ // Prevent screen saver.
+ CFStringRef cf_topic = ::CFStringCreateWithCharacters(
+ kCFAllocatorDefault, reinterpret_cast<const UniChar*>(aTopic.Data()),
+ aTopic.Length());
+ IOReturn success = ::IOPMAssertionCreateWithName(
+ assertionType, kIOPMAssertionLevelOn, cf_topic, &assertionId);
+ CFRelease(cf_topic);
+ if (success != kIOReturnSuccess) {
+ WAKE_LOCK_LOG("failed to disable screensaver");
+ }
+ WAKE_LOCK_LOG("create screensaver");
+ } else {
+ // Re-enable screen saver.
+ if (assertionId != kIOPMNullAssertionID) {
+ IOReturn result = ::IOPMAssertionRelease(assertionId);
+ if (result != kIOReturnSuccess) {
+ WAKE_LOCK_LOG("failed to release screensaver");
+ }
+ WAKE_LOCK_LOG("Release screensaver");
+ assertionId = kIOPMNullAssertionID;
+ }
+ }
+ return NS_OK;
+ }
+}; // MacWakeLockListener
+
+// defined in nsCocoaWindow.mm
+extern int32_t gXULModalLevel;
+
+static bool gAppShellMethodsSwizzled = false;
+
+void OnUncaughtException(NSException* aException) {
+ nsObjCExceptionLog(aException);
+ MOZ_CRASH(
+ "Uncaught Objective C exception from NSSetUncaughtExceptionHandler");
+}
+
+@implementation GeckoNSApplication
+
+// Load is called very early during startup, when the Objective C runtime loads
+// this class.
++ (void)load {
+ NSSetUncaughtExceptionHandler(OnUncaughtException);
+}
+
+// This method is called from NSDefaultTopLevelErrorHandler, which is invoked
+// when an Objective C exception propagates up into the native event loop. It is
+// possible that it is also called in other cases.
+- (void)reportException:(NSException*)aException {
+ if (ShouldIgnoreObjCException(aException)) {
+ return;
+ }
+
+ nsObjCExceptionLog(aException);
+
+#ifdef NIGHTLY_BUILD
+ MOZ_CRASH("Uncaught Objective C exception from -[GeckoNSApplication "
+ "reportException:]");
+#endif
+}
+
+- (void)run {
+ _didLaunch = YES;
+ [super run];
+}
+
+- (void)sendEvent:(NSEvent*)anEvent {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+
+ if ([anEvent type] == NSEventTypeApplicationDefined &&
+ [anEvent subtype] == kEventSubtypeTrace) {
+ mozilla::SignalTracerThread();
+ return;
+ }
+ [super sendEvent:anEvent];
+}
+
+- (NSEvent*)nextEventMatchingMask:(NSEventMask)mask
+ untilDate:(NSDate*)expiration
+ inMode:(NSString*)mode
+ dequeue:(BOOL)flag {
+ MOZ_ASSERT([NSApp didLaunch]);
+ if (expiration) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+ NSEvent* nextEvent = [super nextEventMatchingMask:mask
+ untilDate:expiration
+ inMode:mode
+ dequeue:flag];
+ if (expiration) {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+ }
+ return nextEvent;
+}
+
+@end
+
+// AppShellDelegate
+//
+// Cocoa bridge class. An object of this class is registered to receive
+// notifications.
+//
+@interface AppShellDelegate : NSObject {
+ @private
+ nsAppShell* mAppShell;
+}
+
+- (id)initWithAppShell:(nsAppShell*)aAppShell;
+- (void)applicationWillTerminate:(NSNotification*)aNotification;
+@end
+
+// nsAppShell implementation
+
+NS_IMETHODIMP
+nsAppShell::ResumeNative(void) {
+ nsresult retval = nsBaseAppShell::ResumeNative();
+ if (NS_SUCCEEDED(retval) && (mSuspendNativeCount == 0) &&
+ mSkippedNativeCallback) {
+ mSkippedNativeCallback = false;
+ ScheduleNativeEventCallback();
+ }
+ return retval;
+}
+
+nsAppShell::nsAppShell()
+ : mAutoreleasePools(nullptr),
+ mDelegate(nullptr),
+ mCFRunLoop(NULL),
+ mCFRunLoopSource(NULL),
+ mRunningEventLoop(false),
+ mStarted(false),
+ mTerminated(false),
+ mSkippedNativeCallback(false),
+ mNativeEventCallbackDepth(0),
+ mNativeEventScheduledDepth(0) {
+ // A Cocoa event loop is running here if (and only if) we've been embedded
+ // by a Cocoa app.
+ mRunningCocoaEmbedded = [NSApp isRunning] ? true : false;
+}
+
+nsAppShell::~nsAppShell() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ hal::Shutdown();
+
+ if (mMemoryPressureSource) {
+ dispatch_release(mMemoryPressureSource);
+ mMemoryPressureSource = nullptr;
+ }
+
+ if (mCFRunLoop) {
+ if (mCFRunLoopSource) {
+ ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource,
+ kCFRunLoopCommonModes);
+ ::CFRelease(mCFRunLoopSource);
+ }
+ if (mCFRunLoopObserver) {
+ ::CFRunLoopRemoveObserver(mCFRunLoop, mCFRunLoopObserver,
+ kCFRunLoopCommonModes);
+ ::CFRelease(mCFRunLoopObserver);
+ }
+ ::CFRelease(mCFRunLoop);
+ }
+
+ if (mAutoreleasePools) {
+ NS_ASSERTION(::CFArrayGetCount(mAutoreleasePools) == 0,
+ "nsAppShell destroyed without popping all autorelease pools");
+ ::CFRelease(mAutoreleasePools);
+ }
+
+ [mDelegate release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+NS_IMPL_ISUPPORTS(MacWakeLockListener, nsIDOMMozWakeLockListener)
+mozilla::StaticRefPtr<MacWakeLockListener> sWakeLockListener;
+
+static void AddScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sWakeLockListener = new MacWakeLockListener();
+ sPowerManagerService->AddWakeLockListener(sWakeLockListener);
+ } else {
+ NS_WARNING(
+ "Failed to retrieve PowerManagerService, wakelocks will be broken!");
+ }
+}
+
+static void RemoveScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sPowerManagerService->RemoveWakeLockListener(sWakeLockListener);
+ sPowerManagerService = nullptr;
+ sWakeLockListener = nullptr;
+ }
+}
+
+void RunLoopObserverCallback(CFRunLoopObserverRef aObserver,
+ CFRunLoopActivity aActivity, void* aInfo) {
+ static_cast<nsAppShell*>(aInfo)->OnRunLoopActivityChanged(aActivity);
+}
+
+void nsAppShell::OnRunLoopActivityChanged(CFRunLoopActivity aActivity) {
+ if (aActivity == kCFRunLoopBeforeWaiting) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+
+ // When the event loop is in its waiting state, we would like the profiler to
+ // know that the thread is idle. The usual way to notify the profiler of
+ // idleness would be to place a profiler label frame with the IDLE category on
+ // the stack, for the duration of the function that does the waiting. However,
+ // since macOS uses an event loop model where "the event loop calls you", we
+ // do not control the function that does the waiting; the waiting happens
+ // inside CFRunLoop code. Instead, the run loop notifies us when it enters and
+ // exits the waiting state, by calling this function. So we do not have a
+ // function under our control that stays on the stack for the duration of the
+ // wait. So, rather than putting an AutoProfilerLabel on the stack, we will
+ // manually push and pop the label frame here. The location in the stack where
+ // this label frame is inserted is somewhat arbitrary. In practice, the label
+ // frame will be at the very tip of the stack, looking like it's "inside" the
+ // mach_msg_trap wait function.
+ if (aActivity == kCFRunLoopBeforeWaiting) {
+ using ThreadRegistration = mozilla::profiler::ThreadRegistration;
+ ThreadRegistration::WithOnThreadRef(
+ [&](ThreadRegistration::OnThreadRef aOnThreadRef) {
+ ProfilingStack& profilingStack =
+ aOnThreadRef.UnlockedConstReaderAndAtomicRWRef()
+ .ProfilingStackRef();
+ mProfilingStackWhileWaiting = &profilingStack;
+ uint8_t variableOnStack = 0;
+ profilingStack.pushLabelFrame("Native event loop idle", nullptr,
+ &variableOnStack,
+ JS::ProfilingCategoryPair::IDLE, 0);
+ profiler_thread_sleep();
+ });
+ } else {
+ if (mProfilingStackWhileWaiting) {
+ mProfilingStackWhileWaiting->pop();
+ mProfilingStackWhileWaiting = nullptr;
+ profiler_thread_wake();
+ }
+ }
+}
+
+// Init
+//
+// Loads the nib (see bug 316076c21) and sets up the CFRunLoopSource used to
+// interrupt the main native run loop.
+//
+// public
+nsresult nsAppShell::Init() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // No event loop is running yet (unless an embedding app that uses
+ // NSApplicationMain() is running).
+ NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init];
+
+ char* mozAppNoDock = PR_GetEnv("MOZ_APP_NO_DOCK");
+ if (mozAppNoDock && strcmp(mozAppNoDock, "") != 0) {
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
+ }
+
+ // mAutoreleasePools is used as a stack of NSAutoreleasePool objects created
+ // by |this|. CFArray is used instead of NSArray because NSArray wants to
+ // retain each object you add to it, and you can't retain an
+ // NSAutoreleasePool.
+ mAutoreleasePools = ::CFArrayCreateMutable(nullptr, 0, nullptr);
+ NS_ENSURE_STATE(mAutoreleasePools);
+
+ bool isNSApplicationProcessType =
+ (XRE_GetProcessType() != GeckoProcessType_RDD) &&
+ (XRE_GetProcessType() != GeckoProcessType_Socket);
+
+ if (isNSApplicationProcessType) {
+ // This call initializes NSApplication unless:
+ // 1) we're using xre -- NSApp's already been initialized by
+ // MacApplicationDelegate.mm's EnsureUseCocoaDockAPI().
+ // 2) an embedding app that uses NSApplicationMain() is running -- NSApp's
+ // already been initialized and its main run loop is already running.
+ [[NSBundle mainBundle] loadNibNamed:@"res/MainMenu"
+ owner:[GeckoNSApplication sharedApplication]
+ topLevelObjects:nil];
+ }
+
+ mDelegate = [[AppShellDelegate alloc] initWithAppShell:this];
+ NS_ENSURE_STATE(mDelegate);
+
+ // Add a CFRunLoopSource to the main native run loop. The source is
+ // responsible for interrupting the run loop when Gecko events are ready.
+
+ mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
+ NS_ENSURE_STATE(mCFRunLoop);
+ ::CFRetain(mCFRunLoop);
+
+ CFRunLoopSourceContext context;
+ bzero(&context, sizeof(context));
+ // context.version = 0;
+ context.info = this;
+ context.perform = ProcessGeckoEvents;
+
+ mCFRunLoopSource = ::CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
+ NS_ENSURE_STATE(mCFRunLoopSource);
+
+ ::CFRunLoopAddSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes);
+
+ // Add a CFRunLoopObserver so that the profiler can be notified when we enter
+ // and exit the waiting state.
+ CFRunLoopObserverContext observerContext;
+ PodZero(&observerContext);
+ observerContext.info = this;
+
+ mCFRunLoopObserver = ::CFRunLoopObserverCreate(
+ kCFAllocatorDefault,
+ kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting | kCFRunLoopExit, true,
+ 0, RunLoopObserverCallback, &observerContext);
+ NS_ENSURE_STATE(mCFRunLoopObserver);
+
+ ::CFRunLoopAddObserver(mCFRunLoop, mCFRunLoopObserver, kCFRunLoopCommonModes);
+
+ hal::Init();
+
+ if (XRE_IsParentProcess()) {
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+
+ if (gfxPlatform::IsHeadless()) {
+ screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>());
+ } else {
+ screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperCocoa>());
+ }
+
+ InitMemoryPressureObserver();
+ }
+
+ nsresult rv = nsBaseAppShell::Init();
+
+ if (isNSApplicationProcessType && !gAppShellMethodsSwizzled) {
+ // We should only replace the original terminate: method if we're not
+ // running in a Cocoa embedder. See bug 604901.
+ if (!mRunningCocoaEmbedded) {
+ nsToolkit::SwizzleMethods([NSApplication class], @selector(terminate:),
+ @selector(nsAppShell_NSApplication_terminate:));
+ }
+ gAppShellMethodsSwizzled = true;
+ }
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ if (Preferences::GetBool("security.sandbox.mac.track.violations", false)) {
+ nsSandboxViolationSink::Start();
+ }
+#endif
+
+ [localPool release];
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// ProcessGeckoEvents
+//
+// The "perform" target of mCFRunLoop, called when mCFRunLoopSource is
+// signalled from ScheduleNativeEventCallback.
+//
+// Arrange for Gecko events to be processed on demand (in response to a call
+// to ScheduleNativeEventCallback(), if processing of Gecko events via "native
+// methods" hasn't been suspended). This happens in NativeEventCallback().
+//
+// protected static
+void nsAppShell::ProcessGeckoEvents(void* aInfo) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ AUTO_PROFILER_LABEL("nsAppShell::ProcessGeckoEvents", OTHER);
+
+ nsAppShell* self = static_cast<nsAppShell*>(aInfo);
+
+ if (self->mRunningEventLoop) {
+ self->mRunningEventLoop = false;
+
+ // The run loop may be sleeping -- [NSRunLoop runMode:...]
+ // won't return until it's given a reason to wake up. Awaken it by
+ // posting a bogus event. There's no need to make the event
+ // presentable.
+ //
+ // But _don't_ set windowNumber to '-1' -- that can lead to nasty
+ // weirdness like bmo bug 397039 (a crash in [NSApp sendEvent:] on one of
+ // these fake events, because the -1 has gotten changed into the number
+ // of an actual NSWindow object, and that NSWindow object has just been
+ // destroyed). Setting windowNumber to '0' seems to work fine -- this
+ // seems to prevent the OS from ever trying to associate our bogus event
+ // with a particular NSWindow object.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ // Previously we used to send this second event regardless of
+ // self->mRunningEventLoop. However, that was removed in bug 1690687 for
+ // performance reasons. It is still needed for the mRunningEventLoop case
+ // otherwise we'll get in a cycle of sending postEvent followed by the
+ // DummyEvent inserted by nsBaseAppShell::OnProcessNextEvent. This second
+ // event will cause the second call to AcquireFirstMatchingEventInQueue in
+ // nsAppShell::ProcessNextNativeEvent to return true. Which makes
+ // nsBaseAppShell::OnProcessNextEvent call
+ // nsAppShell::ProcessNextNativeEvent again during which it will loop until
+ // it sleeps because ProcessGeckoEvents() won't be called for the
+ // DummyEvent.
+ //
+ // This is not a good approach and we should fix things up so that only
+ // one postEvent is needed.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ }
+
+ if (self->mSuspendNativeCount <= 0) {
+ ++self->mNativeEventCallbackDepth;
+ self->NativeEventCallback();
+ --self->mNativeEventCallbackDepth;
+ } else {
+ self->mSkippedNativeCallback = true;
+ }
+
+ if (self->mTerminated) {
+ // Still needed to avoid crashes on quit in most Mochitests.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ }
+ // Normally every call to ScheduleNativeEventCallback() results in
+ // exactly one call to ProcessGeckoEvents(). So each Release() here
+ // normally balances exactly one AddRef() in ScheduleNativeEventCallback().
+ // But if Exit() is called just after ScheduleNativeEventCallback(), the
+ // corresponding call to ProcessGeckoEvents() will never happen. We check
+ // for this possibility in two different places -- here and in Exit()
+ // itself. If we find here that Exit() has been called (that mTerminated
+ // is true), it's because we've been called recursively, that Exit() was
+ // called from self->NativeEventCallback() above, and that we're unwinding
+ // the recursion. In this case we'll never be called again, and we balance
+ // here any extra calls to ScheduleNativeEventCallback().
+ //
+ // When ProcessGeckoEvents() is called recursively, it's because of a
+ // call to ScheduleNativeEventCallback() from NativeEventCallback(). We
+ // balance the "extra" AddRefs here (rather than always in Exit()) in order
+ // to ensure that 'self' stays alive until the end of this method. We also
+ // make sure not to finish the balancing until all the recursion has been
+ // unwound.
+ if (self->mTerminated) {
+ int32_t releaseCount = 0;
+ if (self->mNativeEventScheduledDepth > self->mNativeEventCallbackDepth) {
+ releaseCount = PR_ATOMIC_SET(&self->mNativeEventScheduledDepth,
+ self->mNativeEventCallbackDepth);
+ }
+ while (releaseCount-- > self->mNativeEventCallbackDepth) self->Release();
+ } else {
+ // As best we can tell, every call to ProcessGeckoEvents() is triggered
+ // by a call to ScheduleNativeEventCallback(). But we've seen a few
+ // (non-reproducible) cases of double-frees that *might* have been caused
+ // by spontaneous calls (from the OS) to ProcessGeckoEvents(). So we
+ // deal with that possibility here.
+ if (PR_ATOMIC_DECREMENT(&self->mNativeEventScheduledDepth) < 0) {
+ PR_ATOMIC_SET(&self->mNativeEventScheduledDepth, 0);
+ NS_WARNING("Spontaneous call to ProcessGeckoEvents()!");
+ } else {
+ self->Release();
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// WillTerminate
+//
+// Called by the AppShellDelegate when an NSApplicationWillTerminate
+// notification is posted. After this method is called, native events should
+// no longer be processed. The NSApplicationWillTerminate notification is
+// only posted when [NSApp terminate:] is called, which doesn't happen on a
+// "normal" application quit.
+//
+// public
+void nsAppShell::WillTerminate() {
+ if (mTerminated) return;
+
+ // Make sure that the nsAppExitEvent posted by nsAppStartup::Quit() (called
+ // from [MacApplicationDelegate applicationShouldTerminate:]) gets run.
+ NS_ProcessPendingEvents(NS_GetCurrentThread());
+
+ mTerminated = true;
+}
+
+// ScheduleNativeEventCallback
+//
+// Called (possibly on a non-main thread) when Gecko has an event that
+// needs to be processed. The Gecko event needs to be processed on the
+// main thread, so the native run loop must be interrupted.
+//
+// In nsBaseAppShell.cpp, the mNativeEventPending variable is used to
+// ensure that ScheduleNativeEventCallback() is called no more than once
+// per call to NativeEventCallback(). ProcessGeckoEvents() can skip its
+// call to NativeEventCallback() if processing of Gecko events by native
+// means is suspended (using nsIAppShell::SuspendNative()), which will
+// suspend calls from nsBaseAppShell::OnDispatchedEvent() to
+// ScheduleNativeEventCallback(). But when Gecko event processing by
+// native means is resumed (in ResumeNative()), an extra call is made to
+// ScheduleNativeEventCallback() (from ResumeNative()). This triggers
+// another call to ProcessGeckoEvents(), which calls NativeEventCallback(),
+// and nsBaseAppShell::OnDispatchedEvent() resumes calling
+// ScheduleNativeEventCallback().
+//
+// protected virtual
+void nsAppShell::ScheduleNativeEventCallback() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTerminated) return;
+
+ // Each AddRef() here is normally balanced by exactly one Release() in
+ // ProcessGeckoEvents(). But there are exceptions, for which see
+ // ProcessGeckoEvents() and Exit().
+ NS_ADDREF_THIS();
+ PR_ATOMIC_INCREMENT(&mNativeEventScheduledDepth);
+
+ // This will invoke ProcessGeckoEvents on the main thread.
+ ::CFRunLoopSourceSignal(mCFRunLoopSource);
+ ::CFRunLoopWakeUp(mCFRunLoop);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Undocumented Cocoa Event Manager function, present in the same form since
+// at least OS X 10.6.
+extern "C" EventAttributes GetEventAttributes(EventRef inEvent);
+
+// ProcessNextNativeEvent
+//
+// If aMayWait is false, process a single native event. If it is true, run
+// the native run loop until stopped by ProcessGeckoEvents.
+//
+// Returns true if more events are waiting in the native event queue.
+//
+// protected virtual
+bool nsAppShell::ProcessNextNativeEvent(bool aMayWait) {
+ bool moreEvents = false;
+
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ bool eventProcessed = false;
+ NSString* currentMode = nil;
+
+ if (mTerminated) return false;
+
+ // Do not call -[NSApplication nextEventMatchingMask:...] when we're trying to
+ // close a native menu. Doing so could confuse the NSMenu's closing mechanism.
+ // Instead, we try to unwind the stack as quickly as possible and return to
+ // the parent event loop. At that point, native events will be processed.
+ if (MOZMenuOpeningCoordinator.needToUnwindForMenuClosing) {
+ return false;
+ }
+
+ bool wasRunningEventLoop = mRunningEventLoop;
+ mRunningEventLoop = aMayWait;
+ NSDate* waitUntil = nil;
+ if (aMayWait) waitUntil = [NSDate distantFuture];
+
+ NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
+
+ EventQueueRef currentEventQueue = GetCurrentEventQueue();
+
+ if (aMayWait) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+
+ // Only call -[NSApp sendEvent:] (and indirectly send user-input events to
+ // Gecko) if aMayWait is true. Tbis ensures most calls to -[NSApp
+ // sendEvent:] happen under nsAppShell::Run(), at the lowest level of
+ // recursion -- thereby making it less likely Gecko will process user-input
+ // events in the wrong order or skip some of them. It also avoids eating
+ // too much CPU in nsBaseAppShell::OnProcessNextEvent() (which calls
+ // us) -- thereby avoiding the starvation of nsIRunnable events in
+ // nsThread::ProcessNextEvent(). For more information see bug 996848.
+ do {
+ // No autorelease pool is provided here, because OnProcessNextEvent
+ // and AfterProcessNextEvent are responsible for maintaining it.
+ NS_ASSERTION(mAutoreleasePools && ::CFArrayGetCount(mAutoreleasePools),
+ "No autorelease pool for native event");
+
+ if (aMayWait && [[GeckoNSApplication sharedApplication] didLaunch]) {
+ currentMode = [currentRunLoop currentMode];
+ if (!currentMode) currentMode = NSDefaultRunLoopMode;
+ NSEvent* nextEvent = [NSApp nextEventMatchingMask:NSEventMaskAny
+ untilDate:waitUntil
+ inMode:currentMode
+ dequeue:YES];
+ if (nextEvent) {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+ [NSApp sendEvent:nextEvent];
+ eventProcessed = true;
+ }
+ } else {
+ // In at least 10.15, AcquireFirstMatchingEventInQueue will move 1
+ // CGEvent from the CGEvent queue into the Carbon event queue.
+ // Unfortunately, once an event has been moved to the Carbon event queue
+ // it's no longer a candidate for coalescing. This means that even if we
+ // don't remove the event from the queue, just calling
+ // AcquireFirstMatchingEventInQueue can cause behaviour change. Prior to
+ // bug 1690687 landing, the event that we got from
+ // AcquireFirstMatchingEventInQueue was often our own ApplicationDefined
+ // event. However, once we stopped posting that event on every Gecko
+ // event we're much more likely to get a CGEvent. When we have a high
+ // amount of load on the main thread, we end up alternating between Gecko
+ // events and native events. Without CGEvent coalescing, the native
+ // event events can accumulate in the Carbon event queue which will
+ // manifest as laggy scrolling.
+#if 1
+ eventProcessed = false;
+ break;
+#else
+ // AcquireFirstMatchingEventInQueue() doesn't spin the (native) event
+ // loop, though it does queue up any newly available events from the
+ // window server.
+ EventRef currentEvent = AcquireFirstMatchingEventInQueue(
+ currentEventQueue, 0, NULL, kEventQueueOptionsNone);
+ if (!currentEvent) {
+ continue;
+ }
+ EventAttributes attrs = GetEventAttributes(currentEvent);
+ UInt32 eventKind = GetEventKind(currentEvent);
+ UInt32 eventClass = GetEventClass(currentEvent);
+ bool osCocoaEvent =
+ ((eventClass == 'appl') || (eventClass == kEventClassAppleEvent) ||
+ ((eventClass == 'cgs ') &&
+ (eventKind != NSEventTypeApplicationDefined)));
+ // If attrs is kEventAttributeUserEvent or kEventAttributeMonitored
+ // (i.e. a user input event), we shouldn't process it here while
+ // aMayWait is false. Likewise if currentEvent will eventually be
+ // turned into an OS-defined Cocoa event, or otherwise needs AppKit
+ // processing. Doing otherwise risks doing too much work here, and
+ // preventing the event from being properly processed by the AppKit
+ // framework.
+ if ((attrs != kEventAttributeNone) || osCocoaEvent) {
+ // Since we can't process the next event here (while aMayWait is false),
+ // we want moreEvents to be false on return.
+ eventProcessed = false;
+ // This call to ReleaseEvent() matches a call to RetainEvent() in
+ // AcquireFirstMatchingEventInQueue() above.
+ ReleaseEvent(currentEvent);
+ break;
+ }
+ // This call to RetainEvent() matches a call to ReleaseEvent() in
+ // RemoveEventFromQueue() below.
+ RetainEvent(currentEvent);
+ RemoveEventFromQueue(currentEventQueue, currentEvent);
+ EventTargetRef eventDispatcherTarget = GetEventDispatcherTarget();
+ SendEventToEventTarget(currentEvent, eventDispatcherTarget);
+ // This call to ReleaseEvent() matches a call to RetainEvent() in
+ // AcquireFirstMatchingEventInQueue() above.
+ ReleaseEvent(currentEvent);
+ eventProcessed = true;
+#endif
+ }
+ } while (mRunningEventLoop);
+
+ if (eventProcessed) {
+ moreEvents =
+ (AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL,
+ kEventQueueOptionsNone) != NULL);
+ }
+
+ mRunningEventLoop = wasRunningEventLoop;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+
+ if (!moreEvents) {
+ nsChildView::UpdateCurrentInputEventCount();
+ }
+
+ return moreEvents;
+}
+
+// Attempt to work around bug 1801419 by loading and initializing the
+// SidecarCore private framework as the app shell starts up. This normally
+// happens on demand, the first time any Cmd-key combination is pressed, and
+// sometimes triggers crashes, caused by an Apple bug. We hope that doing it
+// now, and somewhat more simply, will avoid the crashes. They happen
+// (intermittently) when SidecarCore code tries to access C strings in special
+// sections of its own __TEXT segment, and triggers fatal page faults (which
+// is Apple's bug). Many of the C strings are part of the Objective-C class
+// hierarchy (class names and so forth). We hope that adding them to this
+// hierarchy will "pin" them in place -- so they'll rarely, if ever, be paged
+// out again. Bug 1801419's crashes happen much more often on macOS 13
+// (Ventura) than on other versions of macOS. So we only use this hack on
+// macOS 13 and up.
+static void PinSidecarCoreTextCStringSections() {
+ if (!dlopen(
+ "/System/Library/PrivateFrameworks/SidecarCore.framework/SidecarCore",
+ RTLD_LAZY)) {
+ return;
+ }
+
+ // Explicitly run the most basic part of the initialization code that
+ // normally runs automatically on the first Cmd-key combination.
+ Class displayManagerClass = NSClassFromString(@"SidecarDisplayManager");
+ if ([displayManagerClass respondsToSelector:@selector(sharedManager)]) {
+ id sharedManager =
+ [displayManagerClass performSelector:@selector(sharedManager)];
+ if ([sharedManager respondsToSelector:@selector(devices)]) {
+ [sharedManager performSelector:@selector(devices)];
+ }
+ }
+}
+
+// Run
+//
+// Overrides the base class's Run() method to call [NSApp run] (which spins
+// the native run loop until the application quits). Since (unlike the base
+// class's Run() method) we don't process any Gecko events here, they need
+// to be processed elsewhere (in NativeEventCallback(), called from
+// ProcessGeckoEvents()).
+//
+// Camino called [NSApp run] on its own (via NSApplicationMain()), and so
+// didn't call nsAppShell::Run().
+//
+// public
+NS_IMETHODIMP
+nsAppShell::Run(void) {
+ NS_ASSERTION(!mStarted, "nsAppShell::Run() called multiple times");
+ if (mStarted || mTerminated) return NS_OK;
+
+ mStarted = true;
+
+ if (XRE_IsParentProcess()) {
+ if (nsCocoaFeatures::OnVenturaOrLater()) {
+ PinSidecarCoreTextCStringSections();
+ }
+ AddScreenWakeLockListener();
+ }
+
+ // We use the native Gecko event loop in content processes.
+ nsresult rv = NS_OK;
+ if (XRE_UseNativeEventProcessing()) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [NSApp run];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ } else {
+ rv = nsBaseAppShell::Run();
+ }
+
+ if (XRE_IsParentProcess()) {
+ RemoveScreenWakeLockListener();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAppShell::Exit(void) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // This method is currently called more than once -- from (according to
+ // mento) an nsAppExitEvent dispatched by nsAppStartup::Quit() and from an
+ // XPCOM shutdown notification that nsBaseAppShell has registered to
+ // receive. So we need to ensure that multiple calls won't break anything.
+ if (mTerminated) {
+ return NS_OK;
+ }
+
+ mTerminated = true;
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ nsSandboxViolationSink::Stop();
+#endif
+
+ // Quoting from Apple's doc on the [NSApplication stop:] method (from their
+ // doc on the NSApplication class): "If this method is invoked during a
+ // modal event loop, it will break that loop but not the main event loop."
+ // nsAppShell::Exit() shouldn't be called from a modal event loop. So if
+ // it is we complain about it (to users of debug builds) and call [NSApp
+ // stop:] one extra time. (I'm not sure if modal event loops can be nested
+ // -- Apple's docs don't say one way or the other. But the return value
+ // of [NSApp _isRunningModal] doesn't change immediately after a call to
+ // [NSApp stop:], so we have to assume that one extra call to [NSApp stop:]
+ // will do the job.)
+ BOOL cocoaModal = [NSApp _isRunningModal];
+ NS_ASSERTION(!cocoaModal,
+ "Don't call nsAppShell::Exit() from a modal event loop!");
+ if (cocoaModal) [NSApp stop:nullptr];
+ [NSApp stop:nullptr];
+
+ // A call to Exit() just after a call to ScheduleNativeEventCallback()
+ // prevents the (normally) matching call to ProcessGeckoEvents() from
+ // happening. If we've been called from ProcessGeckoEvents() (as usually
+ // happens), we take care of it there. But if we have an unbalanced call
+ // to ScheduleNativeEventCallback() and ProcessGeckoEvents() isn't on the
+ // stack, we need to take care of the problem here.
+ if (!mNativeEventCallbackDepth && mNativeEventScheduledDepth) {
+ int32_t releaseCount = PR_ATOMIC_SET(&mNativeEventScheduledDepth, 0);
+ while (releaseCount-- > 0) NS_RELEASE_THIS();
+ }
+
+ return nsBaseAppShell::Exit();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// OnProcessNextEvent
+//
+// This nsIThreadObserver method is called prior to processing an event.
+// Set up an autorelease pool that will service any autoreleased Cocoa
+// objects during this event. This includes native events processed by
+// ProcessNextNativeEvent. The autorelease pool will be popped by
+// AfterProcessNextEvent, it is important for these two methods to be
+// tightly coupled.
+//
+// public
+NS_IMETHODIMP
+nsAppShell::OnProcessNextEvent(nsIThreadInternal* aThread, bool aMayWait) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ASSERTION(mAutoreleasePools,
+ "No stack on which to store autorelease pool");
+
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+ ::CFArrayAppendValue(mAutoreleasePools, pool);
+
+ return nsBaseAppShell::OnProcessNextEvent(aThread, aMayWait);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// AfterProcessNextEvent
+//
+// This nsIThreadObserver method is called after event processing is complete.
+// The Cocoa implementation cleans up the autorelease pool create by the
+// previous OnProcessNextEvent call.
+//
+// public
+NS_IMETHODIMP
+nsAppShell::AfterProcessNextEvent(nsIThreadInternal* aThread,
+ bool aEventWasProcessed) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CFIndex count = ::CFArrayGetCount(mAutoreleasePools);
+
+ NS_ASSERTION(mAutoreleasePools && count,
+ "Processed an event, but there's no autorelease pool?");
+
+ const NSAutoreleasePool* pool = static_cast<const NSAutoreleasePool*>(
+ ::CFArrayGetValueAtIndex(mAutoreleasePools, count - 1));
+ ::CFArrayRemoveValueAtIndex(mAutoreleasePools, count - 1);
+ [pool release];
+
+ return nsBaseAppShell::AfterProcessNextEvent(aThread, aEventWasProcessed);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsAppShell::InitMemoryPressureObserver() {
+ // Testing shows that sometimes the memory pressure event is not fired for
+ // over a minute after the memory pressure change is reflected in sysctl
+ // values. Hence this may need to be augmented with polling of the memory
+ // pressure sysctls for lower latency reactions to OS memory pressure. This
+ // was also observed when using DISPATCH_QUEUE_PRIORITY_HIGH.
+ mMemoryPressureSource = dispatch_source_create(
+ DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0,
+ DISPATCH_MEMORYPRESSURE_NORMAL | DISPATCH_MEMORYPRESSURE_WARN |
+ DISPATCH_MEMORYPRESSURE_CRITICAL,
+ dispatch_get_main_queue());
+
+ dispatch_source_set_event_handler(mMemoryPressureSource, ^{
+ dispatch_source_memorypressure_flags_t pressureLevel =
+ dispatch_source_get_data(mMemoryPressureSource);
+ nsAppShell::OnMemoryPressureChanged(pressureLevel);
+ });
+
+ dispatch_resume(mMemoryPressureSource);
+
+ // Initialize the memory watcher.
+ RefPtr<mozilla::nsAvailableMemoryWatcherBase> watcher(
+ nsAvailableMemoryWatcherBase::GetSingleton());
+}
+
+void nsAppShell::OnMemoryPressureChanged(
+ dispatch_source_memorypressure_flags_t aPressureLevel) {
+ // The memory pressure dispatch source is created (above) with
+ // dispatch_get_main_queue() which always fires on the main thread.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MacMemoryPressureLevel geckoPressureLevel;
+ switch (aPressureLevel) {
+ case DISPATCH_MEMORYPRESSURE_NORMAL:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eNormal;
+ break;
+ case DISPATCH_MEMORYPRESSURE_WARN:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eWarning;
+ break;
+ case DISPATCH_MEMORYPRESSURE_CRITICAL:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eCritical;
+ break;
+ default:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eUnexpected;
+ }
+
+ RefPtr<mozilla::nsAvailableMemoryWatcherBase> watcher(
+ nsAvailableMemoryWatcherBase::GetSingleton());
+ watcher->OnMemoryPressureChanged(geckoPressureLevel);
+}
+
+// AppShellDelegate implementation
+
+@implementation AppShellDelegate
+// initWithAppShell:
+//
+// Constructs the AppShellDelegate object
+- (id)initWithAppShell:(nsAppShell*)aAppShell {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [self init])) {
+ mAppShell = aAppShell;
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(applicationWillTerminate:)
+ name:NSApplicationWillTerminateNotification
+ object:NSApp];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(applicationDidBecomeActive:)
+ name:NSApplicationDidBecomeActiveNotification
+ object:NSApp];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(timezoneChanged:)
+ name:NSSystemTimeZoneDidChangeNotification
+ object:nil];
+ }
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// applicationWillTerminate:
+//
+// Notify the nsAppShell that native event processing should be discontinued.
+- (void)applicationWillTerminate:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mAppShell->WillTerminate();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// applicationDidBecomeActive
+//
+// Make sure TextInputHandler::sLastModifierState is updated when we become
+// active (since we won't have received [ChildView flagsChanged:] messages
+// while inactive).
+- (void)applicationDidBecomeActive:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // [NSEvent modifierFlags] is valid on every kind of event, so we don't need
+ // to worry about getting an NSInternalInconsistencyException here.
+ NSEvent* currentEvent = [NSApp currentEvent];
+ if (currentEvent) {
+ TextInputHandler::sLastModifierState =
+ [currentEvent modifierFlags] &
+ NSEventModifierFlagDeviceIndependentFlagsMask;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService) {
+ observerService->NotifyObservers(
+ nullptr, NS_WIDGET_MAC_APP_ACTIVATE_OBSERVER_TOPIC, nullptr);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)timezoneChanged:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ nsBaseAppShell::OnSystemTimezoneChange();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
+
+// We hook terminate: in order to make OS-initiated termination work nicely
+// with Gecko's shutdown sequence. (Two ways to trigger OS-initiated
+// termination: 1) Quit from the Dock menu; 2) Log out from (or shut down)
+// your computer while the browser is active.)
+@interface NSApplication (MethodSwizzling)
+- (void)nsAppShell_NSApplication_terminate:(id)sender;
+@end
+
+@implementation NSApplication (MethodSwizzling)
+
+// Called by the OS after [MacApplicationDelegate applicationShouldTerminate:]
+// has returned NSTerminateNow. This method "subclasses" and replaces the
+// OS's original implementation. The only thing the orginal method does which
+// we need is that it posts NSApplicationWillTerminateNotification. Everything
+// else is unneeded (because it's handled elsewhere), or actively interferes
+// with Gecko's shutdown sequence. For example the original terminate: method
+// causes the app to exit() inside [NSApp run] (called from nsAppShell::Run()
+// above), which means that nothing runs after the call to nsAppStartup::Run()
+// in XRE_Main(), which in particular means that ScopedXPCOMStartup's destructor
+// and NS_ShutdownXPCOM() never get called.
+- (void)nsAppShell_NSApplication_terminate:(id)sender {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:NSApplicationWillTerminateNotification
+ object:NSApp];
+}
+
+@end
diff --git a/widget/cocoa/nsBidiKeyboard.h b/widget/cocoa/nsBidiKeyboard.h
new file mode 100644
index 0000000000..3a9a6fe2fb
--- /dev/null
+++ b/widget/cocoa/nsBidiKeyboard.h
@@ -0,0 +1,23 @@
+/* -*- 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/. */
+
+#ifndef nsBidiKeyboard_h_
+#define nsBidiKeyboard_h_
+
+#include "nsIBidiKeyboard.h"
+
+class nsBidiKeyboard : public nsIBidiKeyboard {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIBIDIKEYBOARD
+
+ nsBidiKeyboard();
+
+ protected:
+ virtual ~nsBidiKeyboard();
+};
+
+#endif // nsBidiKeyboard_h_
diff --git a/widget/cocoa/nsBidiKeyboard.mm b/widget/cocoa/nsBidiKeyboard.mm
new file mode 100644
index 0000000000..4193bdf6f0
--- /dev/null
+++ b/widget/cocoa/nsBidiKeyboard.mm
@@ -0,0 +1,38 @@
+/* -*- 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 "nsBidiKeyboard.h"
+#include "nsCocoaUtils.h"
+#include "TextInputHandler.h"
+#include "nsIWidget.h"
+
+// This must be the last include:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla::widget;
+
+NS_IMPL_ISUPPORTS(nsBidiKeyboard, nsIBidiKeyboard)
+
+nsBidiKeyboard::nsBidiKeyboard() : nsIBidiKeyboard() { Reset(); }
+
+nsBidiKeyboard::~nsBidiKeyboard() {}
+
+NS_IMETHODIMP nsBidiKeyboard::Reset() { return NS_OK; }
+
+NS_IMETHODIMP nsBidiKeyboard::IsLangRTL(bool* aIsRTL) {
+ *aIsRTL = TISInputSourceWrapper::CurrentInputSource().IsForRTLLanguage();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBidiKeyboard::GetHaveBidiKeyboards(bool* aResult) {
+ // not implemented yet
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// static
+already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() {
+ return do_AddRef(new nsBidiKeyboard());
+}
diff --git a/widget/cocoa/nsChangeObserver.h b/widget/cocoa/nsChangeObserver.h
new file mode 100644
index 0000000000..aacf837bb5
--- /dev/null
+++ b/widget/cocoa/nsChangeObserver.h
@@ -0,0 +1,71 @@
+/* -*- Mode: IDL; 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/. */
+
+#ifndef nsChangeObserver_h_
+#define nsChangeObserver_h_
+
+class nsIContent;
+class nsAtom;
+namespace mozilla {
+namespace dom {
+class Document;
+}
+} // namespace mozilla
+
+#define NS_DECL_CHANGEOBSERVER \
+ void ObserveAttributeChanged(mozilla::dom::Document* aDocument, \
+ nsIContent* aContent, nsAtom* aAttribute) \
+ override; \
+ void ObserveContentRemoved(mozilla::dom::Document* aDocument, \
+ nsIContent* aContainer, nsIContent* aChild, \
+ nsIContent* aPreviousChild) override; \
+ void ObserveContentInserted(mozilla::dom::Document* aDocument, \
+ nsIContent* aContainer, nsIContent* aChild) \
+ override;
+
+// Something that wants to be alerted to changes in attributes or changes in
+// its corresponding content object.
+//
+// This interface is used by our menu code so we only have to have one
+// nsIMutationObserver per menu subtree root (e.g. per menubar).
+//
+// Any class that implements this interface must take care to unregister itself
+// on deletion.
+//
+// XXXmstange The methods below use nsIContent*. Eventually, the should be
+// converted to use mozilla::dom::Element* instead.
+class nsChangeObserver {
+ public:
+ // Called when the attribute aAttribute on the element aContent has changed.
+ // Only if aContent is being observed by this nsChangeObserver.
+ virtual void ObserveAttributeChanged(mozilla::dom::Document* aDocument,
+ nsIContent* aContent,
+ nsAtom* aAttribute) = 0;
+
+ // Called when aChild has been removed from its parent aContainer.
+ // aPreviousSibling is the old previous sibling of aChild.
+ // aContainer is always the old parent node of aChild and of aPreviousSibling.
+ // Only called if aContainer or aContainer's parent node are being observed
+ // by this nsChangeObserver.
+ // In other words: If you observe an element, ObserveContentRemoved is called
+ // if that element's children and grandchildren are removed. NOT if the
+ // observed element itself is removed.
+ virtual void ObserveContentRemoved(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer, nsIContent* aChild,
+ nsIContent* aPreviousSibling) = 0;
+
+ // Called when aChild has been inserted into its new parent aContainer.
+ // Only called if aContainer or aContainer's parent node are being observed
+ // by this nsChangeObserver.
+ // In other words: If you observe an element, ObserveContentInserted is called
+ // if that element receives a new child or grandchild. NOT if the observed
+ // element itself is inserted anywhere.
+ virtual void ObserveContentInserted(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild) = 0;
+};
+
+#endif // nsChangeObserver_h_
diff --git a/widget/cocoa/nsChildView.h b/widget/cocoa/nsChildView.h
new file mode 100644
index 0000000000..3dfc3af9d8
--- /dev/null
+++ b/widget/cocoa/nsChildView.h
@@ -0,0 +1,621 @@
+/* -*- 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/. */
+
+#ifndef nsChildView_h_
+#define nsChildView_h_
+
+// formal protocols
+#include "mozView.h"
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/LocalAccessible.h"
+# include "mozAccessibleProtocol.h"
+#endif
+
+#include "nsISupports.h"
+#include "nsBaseWidget.h"
+#include "nsIWeakReferenceUtils.h"
+#include "TextInputHandler.h"
+#include "nsCocoaUtils.h"
+#include "gfxQuartzSurface.h"
+#include "GLContextTypes.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/Mutex.h"
+#include "nsRegion.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+#include "nsString.h"
+#include "nsIDragService.h"
+#include "ViewRegion.h"
+#include "CFTypeRefPtr.h"
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+#import <AppKit/NSOpenGL.h>
+
+class nsChildView;
+class nsCocoaWindow;
+
+namespace {
+class GLPresenter;
+} // namespace
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class InputData;
+class PanGestureInput;
+class VibrancyManager;
+namespace layers {
+class GLManager;
+class IAPZCTreeManager;
+class NativeLayerRootCA;
+class NativeLayerCA;
+} // namespace layers
+namespace widget {
+class WidgetRenderingContext;
+} // namespace widget
+} // namespace mozilla
+
+@class PixelHostingView;
+
+@interface NSEvent (Undocumented)
+
+// Return Cocoa event's corresponding Carbon event. Not initialized (on
+// synthetic events) until the OS actually "sends" the event. This method
+// has been present in the same form since at least OS X 10.2.8.
+- (EventRef)_eventRef;
+
+// stage From 10.10.3 for force touch event
+@property(readonly) NSInteger stage;
+
+@end
+
+@interface NSView (Undocumented)
+
+// Undocumented method of one or more of NSFrameView's subclasses. Called
+// when one or more of the titlebar buttons needs to be repositioned, to
+// disappear, or to reappear (say if the window's style changes). If
+// 'redisplay' is true, the entire titlebar (the window's top 22 pixels) is
+// marked as needing redisplay. This method has been present in the same
+// format since at least OS X 10.5.
+- (void)_tileTitlebarAndRedisplay:(BOOL)redisplay;
+
+// The following undocumented methods are used to work around bug 1069658,
+// which is an Apple bug or design flaw that effects Yosemite. None of them
+// were present prior to Yosemite (OS X 10.10).
+- (NSView*)titlebarView; // Method of NSThemeFrame
+- (NSView*)titlebarContainerView; // Method of NSThemeFrame
+- (BOOL)transparent; // Method of NSTitlebarView and NSTitlebarContainerView
+- (void)setTransparent:(BOOL)transparent; // Method of NSTitlebarView and
+ // NSTitlebarContainerView
+
+// Available since 10.7.4:
+- (void)viewDidChangeBackingProperties;
+@end
+
+@interface ChildView : NSView <
+#ifdef ACCESSIBILITY
+ mozAccessible,
+#endif
+ mozView,
+ NSTextInputClient,
+ NSDraggingSource,
+ NSDraggingDestination,
+ NSPasteboardItemDataProvider> {
+ @private
+ // the nsChildView that created the view. It retains this NSView, so
+ // the link back to it must be weak.
+ nsChildView* mGeckoChild;
+
+ // Text input handler for mGeckoChild and us. Note that this is a weak
+ // reference. Ideally, this should be a strong reference but a ChildView
+ // object can live longer than the mGeckoChild that owns it. And if
+ // mTextInputHandler were a strong reference, this would make it difficult
+ // for Gecko's leak detector to detect leaked TextInputHandler objects.
+ // This is initialized by [mozView installTextInputHandler:aHandler] and
+ // cleared by [mozView uninstallTextInputHandler].
+ mozilla::widget::TextInputHandler* mTextInputHandler; // [WEAK]
+
+ // when mouseDown: is called, we store its event here (strong)
+ NSEvent* mLastMouseDownEvent;
+
+ // Needed for IME support in e10s mode. Strong.
+ NSEvent* mLastKeyDownEvent;
+
+ // Whether the last mouse down event was blocked from Gecko.
+ BOOL mBlockedLastMouseDown;
+
+ // when acceptsFirstMouse: is called, we store the event here (strong)
+ NSEvent* mClickThroughMouseDownEvent;
+
+ // WheelStart/Stop events should always come in pairs. This BOOL records the
+ // last received event so that, when we receive one of the events, we make
+ // sure to send its pair event first, in case we didn't yet for any reason.
+ BOOL mExpectingWheelStop;
+
+ // Whether we're inside updateRootCALayer at the moment.
+ BOOL mIsUpdatingLayer;
+
+ // Whether the drag and drop was performed.
+ BOOL mPerformedDrag;
+
+ // Holds our drag service across multiple drag calls. The reference to the
+ // service is obtained when the mouse enters the view and is released when
+ // the mouse exits or there is a drop. This prevents us from having to
+ // re-establish the connection to the service manager many times per second
+ // when handling |draggingUpdated:| messages.
+ nsIDragService* mDragService;
+
+ // Gestures support
+ //
+ // mGestureState is used to detect when Cocoa has called both
+ // magnifyWithEvent and rotateWithEvent within the same
+ // beginGestureWithEvent and endGestureWithEvent sequence. We
+ // discard the spurious gesture event so as not to confuse Gecko.
+ //
+ // mCumulativeRotation keeps track of the total amount of rotation
+ // performed during a rotate gesture so we can send that value with
+ // the final MozRotateGesture event.
+ enum {
+ eGestureState_None,
+ eGestureState_StartGesture,
+ eGestureState_MagnifyGesture,
+ eGestureState_RotateGesture
+ } mGestureState;
+ float mCumulativeRotation;
+
+#ifdef __LP64__
+ // Support for fluid swipe tracking.
+ BOOL* mCancelSwipeAnimation;
+#endif
+
+ // Whether this uses off-main-thread compositing.
+ BOOL mUsingOMTCompositor;
+
+ // Subviews of self, which act as container views for vibrancy views and
+ // non-draggable views.
+ NSView* mVibrancyViewsContainer; // [STRONG]
+ NSView* mNonDraggableViewsContainer; // [STRONG]
+
+ // The layer-backed view that hosts our drawing. Always non-null.
+ // This is a subview of self so that it can be ordered on top of
+ // mVibrancyViewsContainer.
+ PixelHostingView* mPixelHostingView;
+
+ // The CALayer that wraps Gecko's rendered contents. It's a sublayer of
+ // mPixelHostingView's backing layer. Always non-null.
+ CALayer* mRootCALayer; // [STRONG]
+
+ // Last pressure stage by trackpad's force click
+ NSInteger mLastPressureStage;
+}
+
+// class initialization
++ (void)initialize;
+
++ (void)registerViewForDraggedTypes:(NSView*)aView;
+
+// these are sent to the first responder when the window key status changes
+- (void)viewsWindowDidBecomeKey;
+- (void)viewsWindowDidResignKey;
+
+// Stop NSView hierarchy being changed during [ChildView drawRect:]
+- (void)delayedTearDown;
+
+- (void)handleMouseMoved:(NSEvent*)aEvent;
+
+- (void)sendMouseEnterOrExitEvent:(NSEvent*)aEvent
+ enter:(BOOL)aEnter
+ exitFrom:
+ (mozilla::WidgetMouseEvent::ExitFrom)aExitFrom;
+
+// Call this during operations that will likely trigger a main thread
+// CoreAnimation paint of the window, during which Gecko should do its own
+// painting and present the results atomically with that main thread
+// transaction. This method will suspend off-thread window updates so that the
+// upcoming paint can be atomic, and mark the layer as needing display so that
+// HandleMainThreadCATransaction gets called and Gecko gets a chance to paint.
+- (void)ensureNextCompositeIsAtomicWithMainThreadPaint;
+
+- (NSView*)vibrancyViewsContainer;
+- (NSView*)nonDraggableViewsContainer;
+- (NSView*)pixelHostingView;
+
+- (BOOL)isCoveringTitlebar;
+
+- (void)viewWillStartLiveResize;
+- (void)viewDidEndLiveResize;
+
+/*
+ * Gestures support
+ *
+ * The prototypes swipeWithEvent, beginGestureWithEvent, smartMagnifyWithEvent,
+ * rotateWithEvent and endGestureWithEvent were obtained from the following
+ * links:
+ * https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/Reference/Reference.html
+ * https://developer.apple.com/library/mac/#releasenotes/Cocoa/AppKit.html
+ */
+- (void)swipeWithEvent:(NSEvent*)anEvent;
+- (void)beginGestureWithEvent:(NSEvent*)anEvent;
+- (void)magnifyWithEvent:(NSEvent*)anEvent;
+- (void)smartMagnifyWithEvent:(NSEvent*)anEvent;
+- (void)rotateWithEvent:(NSEvent*)anEvent;
+- (void)endGestureWithEvent:(NSEvent*)anEvent;
+
+- (void)scrollWheel:(NSEvent*)anEvent;
+
+- (void)setUsingOMTCompositor:(BOOL)aUseOMTC;
+
+- (NSEvent*)lastKeyDownEvent;
+
++ (uint32_t)sUniqueKeyEventId;
+
++ (NSMutableDictionary*)sNativeKeyEventsMap;
+@end
+
+class ChildViewMouseTracker {
+ public:
+ static void MouseMoved(NSEvent* aEvent);
+ static void MouseScrolled(NSEvent* aEvent);
+ static void OnDestroyView(ChildView* aView);
+ static void OnDestroyWindow(NSWindow* aWindow);
+ static BOOL WindowAcceptsEvent(NSWindow* aWindow, NSEvent* aEvent,
+ ChildView* aView, BOOL isClickThrough = NO);
+ static void MouseExitedWindow(NSEvent* aEvent);
+ static void MouseEnteredWindow(NSEvent* aEvent);
+ static void NativeMenuOpened();
+ static void NativeMenuClosed();
+ static void ReEvaluateMouseEnterState(NSEvent* aEvent = nil,
+ ChildView* aOldView = nil);
+ static void ResendLastMouseMoveEvent();
+ static ChildView* ViewForEvent(NSEvent* aEvent);
+
+ static ChildView* sLastMouseEventView;
+ static NSEvent* sLastMouseMoveEvent;
+ static NSWindow* sWindowUnderMouse;
+ static NSPoint sLastScrollEventScreenLocation;
+};
+
+//-------------------------------------------------------------------------
+//
+// nsChildView
+//
+//-------------------------------------------------------------------------
+
+class nsChildView final : public nsBaseWidget {
+ private:
+ typedef nsBaseWidget Inherited;
+ typedef mozilla::layers::IAPZCTreeManager IAPZCTreeManager;
+
+ public:
+ nsChildView();
+
+ // nsIWidget interface
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* = nullptr) override;
+
+ virtual void Destroy() override;
+
+ virtual void Show(bool aState) override;
+ virtual bool IsVisible() const override;
+
+ virtual void SetParent(nsIWidget* aNewParent) override;
+ virtual nsIWidget* GetParent(void) override;
+ virtual float GetDPI() override;
+
+ virtual void Move(double aX, double aY) override;
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override { mSizeMode = aMode; }
+
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual LayoutDeviceIntRect GetBounds() override;
+ virtual LayoutDeviceIntRect GetClientBounds() override;
+ virtual LayoutDeviceIntRect GetScreenBounds() override;
+
+ // Refresh mBounds with up-to-date values from [mView frame].
+ // Only called if this nsChildView is the popup content view of a popup
+ // window. For popup windows, the nsIWidget interface to Gecko is provided by
+ // nsCocoaWindow, not by nsChildView. So nsCocoaWindow manages resize requests
+ // from Gecko, fires resize events, and resizes the native NSWindow and
+ // NSView.
+ void UpdateBoundsFromView();
+
+ // Returns the "backing scale factor" of the view's window, which is the
+ // ratio of pixels in the window's backing store to Cocoa points. Prior to
+ // HiDPI support in OS X 10.7, this was always 1.0, but in HiDPI mode it
+ // will be 2.0 (and might potentially other values as screen resolutions
+ // evolve). This gives the relationship between what Gecko calls "device
+ // pixels" and the Cocoa "points" coordinate system.
+ CGFloat BackingScaleFactor() const;
+
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() final {
+ return mozilla::DesktopToLayoutDeviceScale(BackingScaleFactor());
+ }
+
+ // Call if the window's backing scale factor changes - i.e., it is moved
+ // between HiDPI and non-HiDPI screens
+ void BackingScaleFactorChanged();
+
+ virtual double GetDefaultScaleInternal() override;
+
+ virtual int32_t RoundsWidgetCoordinatesTo() override;
+
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ void EnsureContentLayerForMainThreadPainting();
+
+ virtual void* GetNativeData(uint32_t aDataType) override;
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ virtual bool ShowsResizeIndicator(
+ LayoutDeviceIntRect* aResizerRect) override {
+ return false;
+ }
+
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+
+ virtual bool WidgetTypeSupportsAcceleration() override;
+ virtual bool ShouldUseOffMainThreadCompositing() override;
+
+ virtual void SetCursor(const Cursor&) override;
+
+ virtual nsresult SetTitle(const nsAString& title) override;
+
+ [[nodiscard]] virtual nsresult GetAttention(int32_t aCycleCount) override;
+
+ virtual bool HasPendingInputEvent() override;
+
+ bool SendEventToNativeMenuSystem(NSEvent* aEvent);
+ virtual void PostHandleKeyEvent(
+ mozilla::WidgetKeyboardEvent* aEvent) override;
+ virtual nsresult ActivateNativeMenuItemAt(
+ const nsAString& indexString) override;
+ virtual nsresult ForceUpdateNativeMenuAt(
+ const nsAString& indexString) override;
+ [[nodiscard]] virtual nsresult GetSelectionAsPlaintext(
+ nsAString& aResult) override;
+
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ virtual InputContext GetInputContext() override;
+ virtual TextEventDispatcherListener* GetNativeTextEventDispatcherListener()
+ override;
+ [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
+ mozilla::WidgetKeyboardEvent& aEvent) override;
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ mozilla::NativeKeyBindingsType aType,
+ const mozilla::WidgetKeyboardEvent& aEvent,
+ nsTArray<mozilla::CommandInt>& aCommands) override;
+
+ virtual void SuppressAnimation(bool aSuppress) override;
+
+ virtual nsresult SynthesizeNativeKeyEvent(
+ int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters, nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ return SynthesizeNativeMouseEvent(
+ aPoint, NativeMouseMessage::Move, mozilla::MouseButton::eNotPressed,
+ nsIWidget::Modifiers::NO_MODIFIERS, aObserver);
+ }
+ virtual nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeTouchpadDoubleTap(
+ LayoutDeviceIntPoint aPoint, uint32_t aModifierFlags) override;
+
+ // Mac specific methods
+ void WillPaintWindow();
+ bool PaintWindow(LayoutDeviceIntRegion aRegion);
+ bool PaintWindowInDrawTarget(mozilla::gfx::DrawTarget* aDT,
+ const LayoutDeviceIntRegion& aRegion,
+ const mozilla::gfx::IntSize& aSurfaceSize);
+
+ void PaintWindowInContentLayer();
+ void HandleMainThreadCATransaction();
+
+#ifdef ACCESSIBILITY
+ already_AddRefed<mozilla::a11y::LocalAccessible> GetDocumentAccessible();
+#endif
+
+ virtual void CreateCompositor() override;
+
+ virtual bool WidgetPaintsBackground() override { return true; }
+
+ virtual bool PreRender(
+ mozilla::widget::WidgetRenderingContext* aContext) override;
+ virtual void PostRender(
+ mozilla::widget::WidgetRenderingContext* aContext) override;
+ virtual RefPtr<mozilla::layers::NativeLayerRoot> GetNativeLayerRoot()
+ override;
+
+ virtual void UpdateThemeGeometries(
+ const nsTArray<ThemeGeometry>& aThemeGeometries) override;
+
+ virtual void UpdateWindowDraggingRegion(
+ const LayoutDeviceIntRegion& aRegion) override;
+ LayoutDeviceIntRegion GetNonDraggableRegion() {
+ return mNonDraggableRegion.Region();
+ }
+
+ virtual void LookUpDictionary(
+ const nsAString& aText,
+ const nsTArray<mozilla::FontRange>& aFontRangeArray,
+ const bool aIsVertical, const LayoutDeviceIntPoint& aPoint) override;
+
+ void ResetParent();
+
+ static bool DoHasPendingInputEvent();
+ static uint32_t GetCurrentInputEventCount();
+ static void UpdateCurrentInputEventCount();
+
+ NSView<mozView>* GetEditorView();
+
+ nsCocoaWindow* GetAppWindowWidget() const;
+
+ virtual void ReparentNativeWidget(nsIWidget* aNewParent) override;
+
+ mozilla::widget::TextInputHandler* GetTextInputHandler() {
+ return mTextInputHandler;
+ }
+
+ // unit conversion convenience functions
+ int32_t CocoaPointsToDevPixels(CGFloat aPts) const {
+ return nsCocoaUtils::CocoaPointsToDevPixels(aPts, BackingScaleFactor());
+ }
+ LayoutDeviceIntPoint CocoaPointsToDevPixels(const NSPoint& aPt) const {
+ return nsCocoaUtils::CocoaPointsToDevPixels(aPt, BackingScaleFactor());
+ }
+ LayoutDeviceIntPoint CocoaPointsToDevPixelsRoundDown(
+ const NSPoint& aPt) const {
+ return nsCocoaUtils::CocoaPointsToDevPixelsRoundDown(aPt,
+ BackingScaleFactor());
+ }
+ LayoutDeviceIntRect CocoaPointsToDevPixels(const NSRect& aRect) const {
+ return nsCocoaUtils::CocoaPointsToDevPixels(aRect, BackingScaleFactor());
+ }
+ CGFloat DevPixelsToCocoaPoints(int32_t aPixels) const {
+ return nsCocoaUtils::DevPixelsToCocoaPoints(aPixels, BackingScaleFactor());
+ }
+ NSRect DevPixelsToCocoaPoints(const LayoutDeviceIntRect& aRect) const {
+ return nsCocoaUtils::DevPixelsToCocoaPoints(aRect, BackingScaleFactor());
+ }
+
+ virtual LayoutDeviceIntPoint GetClientOffset() override;
+
+ void DispatchAPZWheelInputEvent(mozilla::InputData& aEvent);
+ nsEventStatus DispatchAPZInputEvent(mozilla::InputData& aEvent);
+
+ void DispatchDoubleTapGesture(mozilla::TimeStamp aEventTimeStamp,
+ LayoutDeviceIntPoint aScreenPosition,
+ mozilla::Modifiers aModifiers);
+
+ // Called when the main thread enters a phase during which visual changes
+ // are imminent and any layer updates on the compositor thread would interfere
+ // with visual atomicity.
+ // "Async" CATransactions are CATransactions which happen on a thread that's
+ // not the main thread.
+ void SuspendAsyncCATransactions();
+
+ // Called when we know that the current main thread paint will be completed
+ // once the main thread goes back to the event loop.
+ void MaybeScheduleUnsuspendAsyncCATransactions();
+
+ // Called from the runnable dispatched by
+ // MaybeScheduleUnsuspendAsyncCATransactions(). At this point we know that the
+ // main thread is done handling the visual change (such as a window resize)
+ // and we can start modifying CALayers from the compositor thread again.
+ void UnsuspendAsyncCATransactions();
+
+ // Called by nsCocoaWindow when the window's fullscreen state changes.
+ void UpdateFullscreen(bool aFullscreen);
+
+#ifdef DEBUG
+ // test only.
+ virtual nsresult SetHiDPIMode(bool aHiDPI) override;
+ virtual nsresult RestoreHiDPIMode() override;
+#endif
+
+ protected:
+ virtual ~nsChildView();
+
+ void ReportMoveEvent();
+ void ReportSizeEvent();
+
+ void TearDownView();
+
+ virtual already_AddRefed<nsIWidget> AllocateChildPopupWidget() override {
+ return nsIWidget::CreateTopLevelWindow();
+ }
+
+ void ConfigureAPZCTreeManager() override;
+ void ConfigureAPZControllerThread() override;
+
+ void UpdateVibrancy(const nsTArray<ThemeGeometry>& aThemeGeometries);
+ mozilla::VibrancyManager& EnsureVibrancyManager();
+
+ nsIWidget* GetWidgetForListenerEvents();
+
+ protected:
+ ChildView* mView; // my parallel cocoa view, [STRONG]
+ RefPtr<mozilla::widget::TextInputHandler> mTextInputHandler;
+ InputContext mInputContext;
+
+ NSView* mParentView;
+ nsIWidget* mParentWidget;
+
+#ifdef ACCESSIBILITY
+ // weak ref to this childview's associated mozAccessible for speed reasons
+ // (we get queried for it *a lot* but don't want to own it)
+ nsWeakPtr mAccessible;
+#endif
+
+ // Held while the compositor (or WR renderer) thread is compositing.
+ // Protects from tearing down the view during compositing and from presenting
+ // half-composited layers to the screen.
+ mozilla::Mutex mCompositingLock MOZ_UNANNOTATED;
+
+ mozilla::ViewRegion mNonDraggableRegion;
+
+ // Cached value of [mView backingScaleFactor], to avoid sending two obj-c
+ // messages (respondsToSelector, backingScaleFactor) every time we need to
+ // use it.
+ // ** We'll need to reinitialize this if the backing resolution changes. **
+ mutable CGFloat mBackingScaleFactor;
+
+ bool mVisible;
+ nsSizeMode mSizeMode;
+ bool mDrawing;
+ bool mIsDispatchPaint; // Is a paint event being dispatched
+
+ RefPtr<mozilla::layers::NativeLayerRootCA> mNativeLayerRoot;
+
+ // In BasicLayers mode, this is the CoreAnimation layer that contains the
+ // rendering from Gecko. It is a sublayer of mNativeLayerRoot's underlying
+ // wrapper layer.
+ // Lazily created by EnsureContentLayerForMainThreadPainting().
+ RefPtr<mozilla::layers::NativeLayerCA> mContentLayer;
+ RefPtr<mozilla::layers::SurfacePoolHandle> mPoolHandle;
+
+ // In BasicLayers mode, this is the invalid region of mContentLayer.
+ LayoutDeviceIntRegion mContentLayerInvalidRegion;
+
+ mozilla::UniquePtr<mozilla::VibrancyManager> mVibrancyManager;
+
+ RefPtr<mozilla::CancelableRunnable> mUnsuspendAsyncCATransactionsRunnable;
+
+ static uint32_t sLastInputEventCount;
+
+ // This is used by SynthesizeNativeTouchPoint to maintain state between
+ // multiple synthesized points
+ mozilla::UniquePtr<mozilla::MultiTouchInput> mSynthesizedTouchInput;
+};
+
+#endif // nsChildView_h_
diff --git a/widget/cocoa/nsChildView.mm b/widget/cocoa/nsChildView.mm
new file mode 100644
index 0000000000..79919ba69a
--- /dev/null
+++ b/widget/cocoa/nsChildView.mm
@@ -0,0 +1,5159 @@
+/* -*- Mode: objc; 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 "mozilla/ArrayUtils.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/Unused.h"
+
+#include <unistd.h>
+#include <math.h>
+
+#include "nsChildView.h"
+#include "nsCocoaWindow.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/SwipeTracker.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaAdjustmentStrategy
+#include "mozilla/WritingModes.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/SimpleGestureEventBinding.h"
+#include "mozilla/dom/WheelEventBinding.h"
+
+#include "nsArrayUtils.h"
+#include "nsExceptionHandler.h"
+#include "nsObjCExceptions.h"
+#include "nsCOMPtr.h"
+#include "nsThreadUtils.h"
+#include "nsToolkit.h"
+#include "nsCRT.h"
+
+#include "nsFontMetrics.h"
+#include "nsIRollupListener.h"
+#include "nsViewManager.h"
+#include "nsIFile.h"
+#include "nsILocalFileMac.h"
+#include "nsGfxCIID.h"
+#include "nsStyleConsts.h"
+#include "nsIWidgetListener.h"
+#include "nsIScreen.h"
+
+#include "nsDragService.h"
+#include "nsClipboard.h"
+#include "nsCursorManager.h"
+#include "nsWindowMap.h"
+#include "nsCocoaUtils.h"
+#include "nsMenuUtilsX.h"
+#include "nsMenuBarX.h"
+#include "NativeKeyBindings.h"
+#include "MacThemeGeometryType.h"
+
+#include "gfxContext.h"
+#include "gfxQuartzSurface.h"
+#include "gfxUtils.h"
+#include "nsRegion.h"
+#include "GfxTexturesReporter.h"
+#include "GLTextureImage.h"
+#include "GLContextProvider.h"
+#include "GLContextCGL.h"
+#include "OGLShaderProgram.h"
+#include "ScopedGLHelpers.h"
+#include "HeapCopyOfStackArray.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/CompositorOGL.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/NativeLayerCA.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/widget/CompositorWidget.h"
+#include "mozilla/widget/Screen.h"
+#include "gfxUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/BorrowedContext.h"
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+# include "mozilla/a11y/Platform.h"
+#endif
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_ui.h"
+
+#include <dlfcn.h>
+
+#include <ApplicationServices/ApplicationServices.h>
+
+#include "GeckoProfiler.h"
+
+#include "mozilla/layers/ChromeProcessController.h"
+#include "nsLayoutUtils.h"
+#include "InputData.h"
+#include "VibrancyManager.h"
+#include "nsNativeThemeCocoa.h"
+#include "nsIDOMWindowUtils.h"
+#include "Units.h"
+#include "UnitTransforms.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "CustomCocoaEvents.h"
+#include "NativeMenuSupport.h"
+
+using namespace mozilla;
+using namespace mozilla::layers;
+using namespace mozilla::gl;
+using namespace mozilla::widget;
+
+using mozilla::gfx::Matrix4x4;
+
+#undef DEBUG_UPDATE
+#undef INVALIDATE_DEBUGGING // flash areas as they are invalidated
+
+// Don't put more than this many rects in the dirty region, just fluff
+// out to the bounding-box if there are more
+#define MAX_RECTS_IN_REGION 100
+
+LazyLogModule sCocoaLog("nsCocoaWidgets");
+
+extern "C" {
+CG_EXTERN void CGContextResetCTM(CGContextRef);
+CG_EXTERN void CGContextSetCTM(CGContextRef, CGAffineTransform);
+CG_EXTERN void CGContextResetClip(CGContextRef);
+
+typedef CFTypeRef CGSRegionObj;
+CGError CGSNewRegionWithRect(const CGRect* rect, CGSRegionObj* outRegion);
+CGError CGSNewRegionWithRectList(const CGRect* rects, int rectCount,
+ CGSRegionObj* outRegion);
+}
+
+// defined in nsMenuBarX.mm
+extern NSMenu* sApplicationMenu; // Application menu shared by all menubars
+
+extern nsIArray* gDraggedTransferables;
+
+ChildView* ChildViewMouseTracker::sLastMouseEventView = nil;
+NSEvent* ChildViewMouseTracker::sLastMouseMoveEvent = nil;
+NSWindow* ChildViewMouseTracker::sWindowUnderMouse = nil;
+NSPoint ChildViewMouseTracker::sLastScrollEventScreenLocation = NSZeroPoint;
+
+#ifdef INVALIDATE_DEBUGGING
+static void blinkRect(Rect* r);
+static void blinkRgn(RgnHandle rgn);
+#endif
+
+bool gUserCancelledDrag = false;
+
+uint32_t nsChildView::sLastInputEventCount = 0;
+
+static bool sIsTabletPointerActivated = false;
+
+static uint32_t sUniqueKeyEventId = 0;
+
+// The view that will do our drawing or host our NSOpenGLContext or Core
+// Animation layer.
+@interface PixelHostingView : NSView {
+}
+
+@end
+
+@interface ChildView (Private)
+
+// sets up our view, attaching it to its owning gecko view
+- (id)initWithFrame:(NSRect)inFrame geckoChild:(nsChildView*)inChild;
+
+// set up a gecko mouse event based on a cocoa mouse event
+- (void)convertCocoaMouseWheelEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetWheelEvent*)outWheelEvent;
+- (void)convertCocoaMouseEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetInputEvent*)outGeckoEvent;
+- (void)convertCocoaTabletPointerEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetMouseEvent*)outGeckoEvent;
+- (NSMenu*)contextMenu;
+
+- (void)markLayerForDisplay;
+- (CALayer*)rootCALayer;
+- (void)updateRootCALayer;
+
+#ifdef ACCESSIBILITY
+- (id<mozAccessible>)accessible;
+#endif
+
+- (LayoutDeviceIntPoint)convertWindowCoordinates:(NSPoint)aPoint;
+- (LayoutDeviceIntPoint)convertWindowCoordinatesRoundDown:(NSPoint)aPoint;
+
+- (BOOL)inactiveWindowAcceptsMouseEvent:(NSEvent*)aEvent;
+- (void)updateWindowDraggableState;
+
+- (bool)beginOrEndGestureForEventPhase:(NSEvent*)aEvent;
+
+@end
+
+#pragma mark -
+
+// Flips a screen coordinate from a point in the cocoa coordinate system
+// (bottom-left rect) to a point that is a "flipped" cocoa coordinate system
+// (starts in the top-left).
+static inline void FlipCocoaScreenCoordinate(NSPoint& inPoint) {
+ inPoint.y = nsCocoaUtils::FlippedScreenY(inPoint.y);
+}
+
+#pragma mark -
+
+nsChildView::nsChildView()
+ : nsBaseWidget(),
+ mView(nullptr),
+ mParentView(nil),
+ mParentWidget(nullptr),
+ mCompositingLock("ChildViewCompositing"),
+ mBackingScaleFactor(0.0),
+ mVisible(false),
+ mSizeMode(nsSizeMode_Normal),
+ mDrawing(false),
+ mIsDispatchPaint(false) {}
+
+nsChildView::~nsChildView() {
+ // Notify the children that we're gone. childView->ResetParent() can change
+ // our list of children while it's being iterated, so the way we iterate the
+ // list must allow for this.
+ for (nsIWidget* kid = mLastChild; kid;) {
+ nsChildView* childView = static_cast<nsChildView*>(kid);
+ kid = kid->GetPrevSibling();
+ childView->ResetParent();
+ }
+
+ NS_WARNING_ASSERTION(
+ mOnDestroyCalled,
+ "nsChildView object destroyed without calling Destroy()");
+
+ if (mContentLayer) {
+ mNativeLayerRoot->RemoveLayer(mContentLayer); // safe if already removed
+ }
+
+ DestroyCompositor();
+
+ // An nsChildView object that was in use can be destroyed without Destroy()
+ // ever being called on it. So we also need to do a quick, safe cleanup
+ // here (it's too late to just call Destroy(), which can cause crashes).
+ // It's particularly important to make sure widgetDestroyed is called on our
+ // mView -- this method NULLs mView's mGeckoChild, and NULL checks on
+ // mGeckoChild are used throughout the ChildView class to tell if it's safe
+ // to use a ChildView object.
+ [mView widgetDestroyed]; // Safe if mView is nil.
+ mParentWidget = nil;
+ TearDownView(); // Safe if called twice.
+}
+
+nsresult nsChildView::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Because the hidden window is created outside of an event loop,
+ // we need to provide an autorelease pool to avoid leaking cocoa objects
+ // (see bug 559075).
+ nsAutoreleasePool localPool;
+
+ mBounds = aRect;
+
+ // Ensure that the toolkit is created.
+ nsToolkit::GetToolkit();
+
+ BaseCreate(aParent, aInitData);
+
+ mParentView = nil;
+ if (aParent) {
+ // This is the popup window case. aParent is the nsCocoaWindow for the
+ // popup window, and mParentView will be its content view.
+ mParentView = (NSView*)aParent->GetNativeData(NS_NATIVE_WIDGET);
+ mParentWidget = aParent;
+ } else {
+ // This is the top-level window case.
+ // aNativeParent will be the contentView of our window, since that's what
+ // nsCocoaWindow returns when asked for an NS_NATIVE_VIEW.
+ // We do not have a direct "parent widget" association with the top level
+ // window's nsCocoaWindow object.
+ mParentView = reinterpret_cast<NSView*>(aNativeParent);
+ }
+
+ // create our parallel NSView and hook it up to our parent. Recall
+ // that NS_NATIVE_WIDGET is the NSView.
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mParentView);
+ NSRect r = nsCocoaUtils::DevPixelsToCocoaPoints(mBounds, scaleFactor);
+ mView = [[ChildView alloc] initWithFrame:r geckoChild:this];
+
+ mNativeLayerRoot = NativeLayerRootCA::CreateForCALayer([mView rootCALayer]);
+ mNativeLayerRoot->SetBackingScale(scaleFactor);
+
+ // If this view was created in a Gecko view hierarchy, the initial state
+ // is hidden. If the view is attached only to a native NSView but has
+ // no Gecko parent (as in embedding), the initial state is visible.
+ if (mParentWidget)
+ [mView setHidden:YES];
+ else
+ mVisible = true;
+
+ // Hook it up in the NSView hierarchy.
+ if (mParentView) {
+ [mParentView addSubview:mView];
+ }
+
+ // if this is a ChildView, make sure that our per-window data
+ // is set up
+ if ([mView isKindOfClass:[ChildView class]])
+ [[WindowDataMap sharedWindowDataMap] ensureDataForWindow:[mView window]];
+
+ NS_ASSERTION(!mTextInputHandler, "mTextInputHandler has already existed");
+ mTextInputHandler = new TextInputHandler(this, mView);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsChildView::TearDownView() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mView) return;
+
+ NSWindow* win = [mView window];
+ NSResponder* responder = [win firstResponder];
+
+ // We're being unhooked from the view hierarchy, don't leave our view
+ // or a child view as the window first responder.
+ if (responder && [responder isKindOfClass:[NSView class]] &&
+ [(NSView*)responder isDescendantOf:mView]) {
+ [win makeFirstResponder:[mView superview]];
+ }
+
+ // If mView is win's contentView, win (mView's NSWindow) "owns" mView --
+ // win has retained mView, and will detach it from the view hierarchy and
+ // release it when necessary (when win is itself destroyed (in a call to
+ // [win dealloc])). So all we need to do here is call [mView release] (to
+ // match the call to [mView retain] in nsChildView::StandardCreate()).
+ // Also calling [mView removeFromSuperviewWithoutNeedingDisplay] causes
+ // mView to be released again and dealloced, while remaining win's
+ // contentView. So if we do that here, win will (for a short while) have
+ // an invalid contentView (for the consequences see bmo bugs 381087 and
+ // 374260).
+ if ([mView isEqual:[win contentView]]) {
+ [mView release];
+ } else {
+ // Stop NSView hierarchy being changed during [ChildView drawRect:]
+ [mView performSelectorOnMainThread:@selector(delayedTearDown)
+ withObject:nil
+ waitUntilDone:false];
+ }
+ mView = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsCocoaWindow* nsChildView::GetAppWindowWidget() const {
+ id windowDelegate = [[mView window] delegate];
+ if (windowDelegate && [windowDelegate isKindOfClass:[WindowDelegate class]]) {
+ return [(WindowDelegate*)windowDelegate geckoWidget];
+ }
+ return nullptr;
+}
+
+void nsChildView::Destroy() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mOnDestroyCalled) return;
+ mOnDestroyCalled = true;
+
+ // Stuff below may delete the last ref to this
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ {
+ // Make sure that no composition is in progress while disconnecting
+ // ourselves from the view.
+ MutexAutoLock lock(mCompositingLock);
+
+ [mView widgetDestroyed];
+ }
+
+ nsBaseWidget::Destroy();
+
+ NotifyWindowDestroyed();
+ mParentWidget = nil;
+
+ TearDownView();
+
+ nsBaseWidget::OnDestroy();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+#if 0
+static void PrintViewHierarchy(NSView *view)
+{
+ while (view) {
+ NSLog(@" view is %x, frame %@", view, NSStringFromRect([view frame]));
+ view = [view superview];
+ }
+}
+#endif
+
+// Return native data according to aDataType
+void* nsChildView::GetNativeData(uint32_t aDataType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ void* retVal = nullptr;
+
+ switch (aDataType) {
+ case NS_NATIVE_WIDGET:
+ retVal = (void*)mView;
+ break;
+
+ case NS_NATIVE_WINDOW:
+ retVal = [mView window];
+ break;
+
+ case NS_NATIVE_GRAPHIC:
+ NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a Mac OS X child view!");
+ retVal = nullptr;
+ break;
+
+ case NS_NATIVE_OFFSETX:
+ retVal = 0;
+ break;
+
+ case NS_NATIVE_OFFSETY:
+ retVal = 0;
+ break;
+
+ case NS_RAW_NATIVE_IME_CONTEXT:
+ retVal = GetPseudoIMEContext();
+ if (retVal) {
+ break;
+ }
+ retVal = [mView inputContext];
+ // If input context isn't available on this widget, we should set |this|
+ // instead of nullptr since if this returns nullptr, IMEStateManager
+ // cannot manage composition with TextComposition instance. Although,
+ // this case shouldn't occur.
+ if (NS_WARN_IF(!retVal)) {
+ retVal = this;
+ }
+ break;
+
+ case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID: {
+ NSWindow* win = [mView window];
+ if (win) {
+ retVal = (void*)[win windowNumber];
+ }
+ break;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+#pragma mark -
+
+void nsChildView::SuppressAnimation(bool aSuppress) {
+ GetAppWindowWidget()->SuppressAnimation(aSuppress);
+}
+
+bool nsChildView::IsVisible() const {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mVisible) {
+ return mVisible;
+ }
+
+ if (!GetAppWindowWidget()->IsVisible()) {
+ return false;
+ }
+
+ // mVisible does not accurately reflect the state of a hidden tabbed view
+ // so verify that the view has a window as well
+ // then check native widget hierarchy visibility
+ return ([mView window] != nil) && !NSIsEmptyRect([mView visibleRect]);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+// Some NSView methods (e.g. setFrame and setHidden) invalidate the view's
+// bounds in our window. However, we don't want these invalidations because
+// they are unnecessary and because they actually slow us down since we
+// block on the compositor inside drawRect.
+// When we actually need something invalidated, there will be an explicit call
+// to Invalidate from Gecko, so turning these automatic invalidations off
+// won't hurt us in the non-OMTC case.
+// The invalidations inside these NSView methods happen via a call to the
+// private method -[NSWindow _setNeedsDisplayInRect:]. Our BaseWindow
+// implementation of that method is augmented to let us ignore those calls
+// using -[BaseWindow disable/enableSetNeedsDisplay].
+static void ManipulateViewWithoutNeedingDisplay(NSView* aView,
+ void (^aCallback)()) {
+ BaseWindow* win = nil;
+ if ([[aView window] isKindOfClass:[BaseWindow class]]) {
+ win = (BaseWindow*)[aView window];
+ }
+ [win disableSetNeedsDisplay];
+ aCallback();
+ [win enableSetNeedsDisplay];
+}
+
+// Hide or show this component
+void nsChildView::Show(bool aState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (aState != mVisible) {
+ // Provide an autorelease pool because this gets called during startup
+ // on the "hidden window", resulting in cocoa object leakage if there's
+ // no pool in place.
+ nsAutoreleasePool localPool;
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setHidden:!aState];
+ });
+
+ mVisible = aState;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Change the parent of this widget
+void nsChildView::SetParent(nsIWidget* aNewParent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mOnDestroyCalled) return;
+
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ if (mParentWidget) {
+ mParentWidget->RemoveChild(this);
+ }
+
+ if (aNewParent) {
+ ReparentNativeWidget(aNewParent);
+ } else {
+ [mView removeFromSuperview];
+ mParentView = nil;
+ }
+
+ mParentWidget = aNewParent;
+
+ if (mParentWidget) {
+ mParentWidget->AddChild(this);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::ReparentNativeWidget(nsIWidget* aNewParent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(aNewParent, "null widget");
+
+ if (mOnDestroyCalled) return;
+
+ NSView<mozView>* newParentView =
+ (NSView<mozView>*)aNewParent->GetNativeData(NS_NATIVE_WIDGET);
+ NS_ENSURE_TRUE_VOID(newParentView);
+
+ // we hold a ref to mView, so this is safe
+ [mView removeFromSuperview];
+ mParentView = newParentView;
+ [mParentView addSubview:mView];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::ResetParent() {
+ if (!mOnDestroyCalled) {
+ if (mParentWidget) mParentWidget->RemoveChild(this);
+ if (mView) [mView removeFromSuperview];
+ }
+ mParentWidget = nullptr;
+}
+
+nsIWidget* nsChildView::GetParent() { return mParentWidget; }
+
+float nsChildView::GetDPI() {
+ float dpi = 96.0;
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ screen->GetDpi(&dpi);
+ }
+ return dpi;
+}
+
+void nsChildView::Enable(bool aState) {}
+
+bool nsChildView::IsEnabled() const { return true; }
+
+void nsChildView::SetFocus(Raise, mozilla::dom::CallerType aCallerType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSWindow* window = [mView window];
+ if (window) [window makeFirstResponder:mView];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Override to set the cursor on the mac
+void nsChildView::SetCursor(const Cursor& aCursor) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([mView isDragInProgress]) {
+ return; // Don't change the cursor during dragging.
+ }
+
+ nsBaseWidget::SetCursor(aCursor);
+
+ bool forceUpdate = mUpdateCursor;
+ mUpdateCursor = false;
+ if (mCustomCursorAllowed && NS_SUCCEEDED([[nsCursorManager sharedInstance]
+ setCustomCursor:aCursor
+ widgetScaleFactor:BackingScaleFactor()
+ forceUpdate:forceUpdate])) {
+ return;
+ }
+
+ [[nsCursorManager sharedInstance] setNonCustomCursor:aCursor];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+// Get this component dimension
+LayoutDeviceIntRect nsChildView::GetBounds() {
+ return !mView ? mBounds : CocoaPointsToDevPixels([mView frame]);
+}
+
+LayoutDeviceIntRect nsChildView::GetClientBounds() {
+ LayoutDeviceIntRect rect = GetBounds();
+ if (!mParentWidget) {
+ // For top level widgets we want the position on screen, not the position
+ // of this view inside the window.
+ rect.MoveTo(WidgetToScreenOffset());
+ }
+ return rect;
+}
+
+LayoutDeviceIntRect nsChildView::GetScreenBounds() {
+ LayoutDeviceIntRect rect = GetBounds();
+ rect.MoveTo(WidgetToScreenOffset());
+ return rect;
+}
+
+double nsChildView::GetDefaultScaleInternal() { return BackingScaleFactor(); }
+
+CGFloat nsChildView::BackingScaleFactor() const {
+ if (mBackingScaleFactor > 0.0) {
+ return mBackingScaleFactor;
+ }
+ if (!mView) {
+ return 1.0;
+ }
+ mBackingScaleFactor = nsCocoaUtils::GetBackingScaleFactor(mView);
+ return mBackingScaleFactor;
+}
+
+void nsChildView::BackingScaleFactorChanged() {
+ CGFloat newScale = nsCocoaUtils::GetBackingScaleFactor(mView);
+
+ // ignore notification if it hasn't really changed (or maybe we have
+ // disabled HiDPI mode via prefs)
+ if (mBackingScaleFactor == newScale) {
+ return;
+ }
+
+ SuspendAsyncCATransactions();
+ mBackingScaleFactor = newScale;
+ NSRect frame = [mView frame];
+ mBounds = nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, newScale);
+
+ mNativeLayerRoot->SetBackingScale(mBackingScaleFactor);
+
+ if (mWidgetListener && !mWidgetListener->GetAppWindow()) {
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ }
+}
+
+int32_t nsChildView::RoundsWidgetCoordinatesTo() {
+ if (BackingScaleFactor() == 2.0) {
+ return 2;
+ }
+ return 1;
+}
+
+// Move this component, aX and aY are in the parent widget coordinate system
+void nsChildView::Move(double aX, double aY) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ int32_t x = NSToIntRound(aX);
+ int32_t y = NSToIntRound(aY);
+
+ if (!mView || (mBounds.x == x && mBounds.y == y)) return;
+
+ mBounds.x = x;
+ mBounds.y = y;
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setFrame:DevPixelsToCocoaPoints(mBounds)];
+ });
+
+ ReportMoveEvent();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::Resize(double aWidth, double aHeight, bool aRepaint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ int32_t width = NSToIntRound(aWidth);
+ int32_t height = NSToIntRound(aHeight);
+
+ if (!mView || (mBounds.width == width && mBounds.height == height)) return;
+
+ SuspendAsyncCATransactions();
+ mBounds.width = width;
+ mBounds.height = height;
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setFrame:DevPixelsToCocoaPoints(mBounds)];
+ });
+
+ if (mVisible && aRepaint) {
+ [[mView pixelHostingView] setNeedsDisplay:YES];
+ }
+
+ ReportSizeEvent();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsChildView::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ int32_t x = NSToIntRound(aX);
+ int32_t y = NSToIntRound(aY);
+ int32_t width = NSToIntRound(aWidth);
+ int32_t height = NSToIntRound(aHeight);
+
+ BOOL isMoving = (mBounds.x != x || mBounds.y != y);
+ BOOL isResizing = (mBounds.width != width || mBounds.height != height);
+ if (!mView || (!isMoving && !isResizing)) return;
+
+ if (isMoving) {
+ mBounds.x = x;
+ mBounds.y = y;
+ }
+ if (isResizing) {
+ SuspendAsyncCATransactions();
+ mBounds.width = width;
+ mBounds.height = height;
+
+ CALayer* layer = [mView rootCALayer];
+ double scale = BackingScaleFactor();
+ layer.bounds = CGRectMake(0, 0, width / scale, height / scale);
+ }
+
+ ManipulateViewWithoutNeedingDisplay(mView, ^{
+ [mView setFrame:DevPixelsToCocoaPoints(mBounds)];
+ });
+
+ if (mVisible && aRepaint) {
+ [[mView pixelHostingView] setNeedsDisplay:YES];
+ }
+
+ if (isMoving) {
+ ReportMoveEvent();
+ if (mOnDestroyCalled) return;
+ }
+ if (isResizing) ReportSizeEvent();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// The following three methods are primarily an attempt to avoid glitches during
+// window resizing.
+// Here's some background on how these glitches come to be:
+// CoreAnimation transactions are per-thread. They don't nest across threads.
+// If you submit a transaction on the main thread and a transaction on a
+// different thread, the two will race to the window server and show up on the
+// screen in the order that they happen to arrive in at the window server.
+// When the window size changes, there's another event that needs to be
+// synchronized with: the window "shape" change. Cocoa has built-in
+// synchronization mechanics that make sure that *main thread* window paints
+// during window resizes are synchronized properly with the window shape change.
+// But no such built-in synchronization exists for CATransactions that are
+// triggered on a non-main thread. To cope with this, we define a "danger zone"
+// during which we simply avoid triggering any CATransactions on a non-main
+// thread (called "async" CATransactions here). This danger zone starts at the
+// earliest opportunity at which we know about the size change, which is
+// nsChildView::Resize, and ends at a point at which we know for sure that the
+// paint has been handled completely, which is when we return to the event loop
+// after layer display.
+void nsChildView::SuspendAsyncCATransactions() {
+ if (mUnsuspendAsyncCATransactionsRunnable) {
+ mUnsuspendAsyncCATransactionsRunnable->Cancel();
+ mUnsuspendAsyncCATransactionsRunnable = nullptr;
+ }
+
+ // Make sure that there actually will be a CATransaction on the main thread
+ // during which we get a chance to schedule unsuspension. Otherwise we might
+ // accidentally stay suspended indefinitely.
+ [mView markLayerForDisplay];
+
+ mNativeLayerRoot->SuspendOffMainThreadCommits();
+}
+
+void nsChildView::MaybeScheduleUnsuspendAsyncCATransactions() {
+ if (mNativeLayerRoot->AreOffMainThreadCommitsSuspended() &&
+ !mUnsuspendAsyncCATransactionsRunnable) {
+ mUnsuspendAsyncCATransactionsRunnable = NewCancelableRunnableMethod(
+ "nsChildView::MaybeScheduleUnsuspendAsyncCATransactions", this,
+ &nsChildView::UnsuspendAsyncCATransactions);
+ NS_DispatchToMainThread(mUnsuspendAsyncCATransactionsRunnable);
+ }
+}
+
+void nsChildView::UnsuspendAsyncCATransactions() {
+ mUnsuspendAsyncCATransactionsRunnable = nullptr;
+
+ if (mNativeLayerRoot->UnsuspendOffMainThreadCommits()) {
+ // We need to call mNativeLayerRoot->CommitToScreen() at the next available
+ // opportunity.
+ // The easiest way to handle this request is to mark the layer as needing
+ // display, because this will schedule a main thread CATransaction, during
+ // which HandleMainThreadCATransaction will call CommitToScreen().
+ [mView markLayerForDisplay];
+ }
+}
+
+void nsChildView::UpdateFullscreen(bool aFullscreen) {
+ if (mNativeLayerRoot) {
+ mNativeLayerRoot->SetWindowIsFullscreen(aFullscreen);
+ }
+}
+
+nsresult nsChildView::SynthesizeNativeKeyEvent(
+ int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "keyevent");
+ return mTextInputHandler->SynthesizeNativeKeyEvent(
+ aNativeKeyboardLayout, aNativeKeyCode, aModifierFlags, aCharacters,
+ aUnmodifiedCharacters);
+}
+
+nsresult nsChildView::SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+
+ NSPoint pt =
+ nsCocoaUtils::DevPixelsToCocoaPoints(aPoint, BackingScaleFactor());
+
+ // Move the mouse cursor to the requested position and reconnect it to the
+ // mouse.
+ CGWarpMouseCursorPosition(NSPointToCGPoint(pt));
+ CGAssociateMouseAndMouseCursorPosition(true);
+
+ // aPoint is given with the origin on the top left, but convertScreenToBase
+ // expects a point in a coordinate system that has its origin on the bottom
+ // left.
+ NSPoint screenPoint = NSMakePoint(pt.x, nsCocoaUtils::FlippedScreenY(pt.y));
+ NSPoint windowPoint =
+ nsCocoaUtils::ConvertPointFromScreen([mView window], screenPoint);
+ NSEventModifierFlags modifierFlags =
+ nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(aModifierFlags);
+
+ if (aButton == MouseButton::eX1 || aButton == MouseButton::eX2) {
+ // NSEvent has `buttonNumber` for `NSEventTypeOther*`. However, it seems
+ // that there is no way to specify it. Therefore, we should return error
+ // for now.
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ NSEventType nativeEventType;
+ switch (aNativeMessage) {
+ case NativeMouseMessage::ButtonDown:
+ case NativeMouseMessage::ButtonUp: {
+ switch (aButton) {
+ case MouseButton::ePrimary:
+ nativeEventType = aNativeMessage == NativeMouseMessage::ButtonDown
+ ? NSEventTypeLeftMouseDown
+ : NSEventTypeLeftMouseUp;
+ break;
+ case MouseButton::eMiddle:
+ nativeEventType = aNativeMessage == NativeMouseMessage::ButtonDown
+ ? NSEventTypeOtherMouseDown
+ : NSEventTypeOtherMouseUp;
+ break;
+ case MouseButton::eSecondary:
+ nativeEventType = aNativeMessage == NativeMouseMessage::ButtonDown
+ ? NSEventTypeRightMouseDown
+ : NSEventTypeRightMouseUp;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+ break;
+ }
+ case NativeMouseMessage::Move:
+ nativeEventType = NSEventTypeMouseMoved;
+ break;
+ case NativeMouseMessage::EnterWindow:
+ nativeEventType = NSEventTypeMouseEntered;
+ break;
+ case NativeMouseMessage::LeaveWindow:
+ nativeEventType = NSEventTypeMouseExited;
+ break;
+ }
+
+ NSEvent* event =
+ [NSEvent mouseEventWithType:nativeEventType
+ location:windowPoint
+ modifierFlags:modifierFlags
+ timestamp:[[NSProcessInfo processInfo] systemUptime]
+ windowNumber:[[mView window] windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:0.0];
+
+ if (!event) return NS_ERROR_FAILURE;
+
+ if ([[mView window] isKindOfClass:[BaseWindow class]]) {
+ // Tracking area events don't end up in their tracking areas when sent
+ // through [NSApp sendEvent:], so pass them directly to the right methods.
+ BaseWindow* window = (BaseWindow*)[mView window];
+ if (nativeEventType == NSEventTypeMouseEntered) {
+ [window mouseEntered:event];
+ return NS_OK;
+ }
+ if (nativeEventType == NSEventTypeMouseExited) {
+ [window mouseExited:event];
+ return NS_OK;
+ }
+ if (nativeEventType == NSEventTypeMouseMoved) {
+ [window mouseMoved:event];
+ return NS_OK;
+ }
+ }
+
+ [NSApp sendEvent:event];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsChildView::SynthesizeNativeMouseScrollEvent(
+ mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage,
+ double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+
+ NSPoint pt =
+ nsCocoaUtils::DevPixelsToCocoaPoints(aPoint, BackingScaleFactor());
+
+ // Move the mouse cursor to the requested position and reconnect it to the
+ // mouse.
+ CGWarpMouseCursorPosition(NSPointToCGPoint(pt));
+ CGAssociateMouseAndMouseCursorPosition(true);
+
+ // Mostly copied from http://stackoverflow.com/a/6130349
+ CGScrollEventUnit units =
+ (aAdditionalFlags & nsIDOMWindowUtils::MOUSESCROLL_SCROLL_LINES)
+ ? kCGScrollEventUnitLine
+ : kCGScrollEventUnitPixel;
+ CGEventRef cgEvent = CGEventCreateScrollWheelEvent(
+ NULL, units, 3, (int32_t)aDeltaY, (int32_t)aDeltaX, (int32_t)aDeltaZ);
+ if (!cgEvent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aNativeMessage) {
+ CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventScrollPhase,
+ aNativeMessage);
+ }
+
+ // On macOS 10.14 and up CGEventPost won't work because of changes in macOS
+ // to improve security. This code makes an NSEvent corresponding to the
+ // wheel event and dispatches it directly to the scrollWheel handler. Some
+ // fiddling is needed with the coordinates in order to simulate what macOS
+ // would do; this code adapted from the Chromium equivalent function at
+ // https://chromium.googlesource.com/chromium/src.git/+/62.0.3178.1/ui/events/test/cocoa_test_event_utils.mm#38
+ CGPoint location = CGEventGetLocation(cgEvent);
+ location.y += NSMinY([[mView window] frame]);
+ location.x -= NSMinX([[mView window] frame]);
+ CGEventSetLocation(cgEvent, location);
+
+ uint64_t kNanosPerSec = 1000000000L;
+ CGEventSetTimestamp(
+ cgEvent, [[NSProcessInfo processInfo] systemUptime] * kNanosPerSec);
+
+ NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
+ [event setValue:[mView window] forKey:@"_window"];
+ [mView scrollWheel:event];
+
+ CFRelease(cgEvent);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsChildView::SynthesizeNativeTouchPoint(
+ uint32_t aPointerId, TouchPointerState aPointerState,
+ mozilla::LayoutDeviceIntPoint aPoint, double aPointerPressure,
+ uint32_t aPointerOrientation, nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "touchpoint");
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aPointerState == TOUCH_HOVER) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!mSynthesizedTouchInput) {
+ mSynthesizedTouchInput = MakeUnique<MultiTouchInput>();
+ }
+
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState(
+ mSynthesizedTouchInput.get(), TimeStamp::Now(), aPointerId, aPointerState,
+ pointInWindow, aPointerPressure, aPointerOrientation);
+ DispatchTouchInput(inputToDispatch);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsChildView::SynthesizeNativeTouchpadDoubleTap(
+ mozilla::LayoutDeviceIntPoint aPoint, uint32_t aModifierFlags) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ DispatchDoubleTapGesture(TimeStamp::Now(), aPoint - WidgetToScreenOffset(),
+ static_cast<Modifiers>(aModifierFlags));
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool nsChildView::SendEventToNativeMenuSystem(NSEvent* aEvent) {
+ bool handled = false;
+ nsCocoaWindow* widget = GetAppWindowWidget();
+ if (widget) {
+ nsMenuBarX* mb = widget->GetMenuBar();
+ if (mb) {
+ // Check if main menu wants to handle the event.
+ handled = mb->PerformKeyEquivalent(aEvent);
+ }
+ }
+
+ if (!handled && sApplicationMenu) {
+ // Check if application menu wants to handle the event.
+ handled = [sApplicationMenu performKeyEquivalent:aEvent];
+ }
+
+ return handled;
+}
+
+void nsChildView::PostHandleKeyEvent(mozilla::WidgetKeyboardEvent* aEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // We always allow keyboard events to propagate to keyDown: but if they are
+ // not handled we give menu items a chance to act. This allows for handling of
+ // custom shortcuts. Note that existing shortcuts cannot be reassigned yet and
+ // will have been handled by keyDown: before we get here.
+ NSMutableDictionary* nativeKeyEventsMap = [ChildView sNativeKeyEventsMap];
+ NSEvent* cocoaEvent = [nativeKeyEventsMap objectForKey:@(aEvent->mUniqueId)];
+ if (!cocoaEvent) {
+ return;
+ }
+
+ // If the escape key is pressed, the expectations are as follows:
+ // 1. If the page is loading, interrupt loading.
+ // 2. Give a website an opportunity to handle the event and call
+ // preventDefault() on it.
+ // 3. If the browser is fullscreen and the page isn't loading, exit
+ // fullscreen.
+ // 4. Ignore.
+ // Case 1 and 2 are handled before we get here. Below, we handle case 3.
+ if (StaticPrefs::browser_fullscreen_exit_on_escape() &&
+ [cocoaEvent keyCode] == kVK_Escape &&
+ [[mView window] styleMask] & NSWindowStyleMaskFullScreen) {
+ [[mView window] toggleFullScreen:nil];
+ }
+
+ if (SendEventToNativeMenuSystem(cocoaEvent)) {
+ aEvent->PreventDefault();
+ }
+ [nativeKeyEventsMap removeObjectForKey:@(aEvent->mUniqueId)];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Used for testing native menu system structure and event handling.
+nsresult nsChildView::ActivateNativeMenuItemAt(const nsAString& indexString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsMenuUtilsX::CheckNativeMenuConsistency([NSApp mainMenu]);
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ indexString.BeginReading())
+ length:indexString.Length()];
+ NSMenuItem* item = nsMenuUtilsX::NativeMenuItemWithLocation(
+ [NSApp mainMenu], locationString, true);
+ // We can't perform an action on an item with a submenu, that will raise
+ // an obj-c exception.
+ if (item && ![item hasSubmenu]) {
+ NSMenu* parent = [item menu];
+ if (parent) {
+ // NSLog(@"Performing action for native menu item titled: %@\n",
+ // [[currentSubmenu itemAtIndex:targetIndex] title]);
+ mozilla::AutoRestore<bool> autoRestore(
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
+ nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
+ [parent performActionForItemAtIndex:[parent indexOfItem:item]];
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_FAILURE;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// Used for testing native menu system structure and event handling.
+nsresult nsChildView::ForceUpdateNativeMenuAt(const nsAString& indexString) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCocoaWindow* widget = GetAppWindowWidget();
+ if (widget) {
+ nsMenuBarX* mb = widget->GetMenuBar();
+ if (mb) {
+ if (indexString.IsEmpty())
+ mb->ForceNativeMenuReload();
+ else
+ mb->ForceUpdateNativeMenuAt(indexString);
+ }
+ }
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+#pragma mark -
+
+#ifdef INVALIDATE_DEBUGGING
+
+static Boolean KeyDown(const UInt8 theKey) {
+ KeyMap map;
+ GetKeys(map);
+ return ((*((UInt8*)map + (theKey >> 3)) >> (theKey & 7)) & 1) != 0;
+}
+
+static Boolean caps_lock() { return KeyDown(0x39); }
+
+static void blinkRect(Rect* r) {
+ StRegionFromPool oldClip;
+ if (oldClip != NULL) ::GetClip(oldClip);
+
+ ::ClipRect(r);
+ ::InvertRect(r);
+ UInt32 end = ::TickCount() + 5;
+ while (::TickCount() < end)
+ ;
+ ::InvertRect(r);
+
+ if (oldClip != NULL) ::SetClip(oldClip);
+}
+
+static void blinkRgn(RgnHandle rgn) {
+ StRegionFromPool oldClip;
+ if (oldClip != NULL) ::GetClip(oldClip);
+
+ ::SetClip(rgn);
+ ::InvertRgn(rgn);
+ UInt32 end = ::TickCount() + 5;
+ while (::TickCount() < end)
+ ;
+ ::InvertRgn(rgn);
+
+ if (oldClip != NULL) ::SetClip(oldClip);
+}
+
+#endif
+
+// Invalidate this component's visible area
+void nsChildView::Invalidate(const LayoutDeviceIntRect& aRect) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mView || !mVisible) return;
+
+ NS_ASSERTION(
+ GetWindowRenderer()->GetBackendType() != LayersBackend::LAYERS_WR,
+ "Shouldn't need to invalidate with accelerated OMTC layers!");
+
+ EnsureContentLayerForMainThreadPainting();
+ mContentLayerInvalidRegion.OrWith(aRect.Intersect(GetBounds()));
+ [mView markLayerForDisplay];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool nsChildView::WidgetTypeSupportsAcceleration() {
+ // All widget types support acceleration.
+ return true;
+}
+
+bool nsChildView::ShouldUseOffMainThreadCompositing() {
+ // We need to enable OMTC in popups which contain remote layer
+ // trees, since the remote content won't be rendered at all otherwise.
+ if (HasRemoteContent()) {
+ return true;
+ }
+
+ // Don't use OMTC for popup windows, because we do not want context menus to
+ // pay the overhead of starting up a compositor. With the OpenGL compositor,
+ // new windows are expensive because of shader re-compilation, and with
+ // WebRender, new windows are expensive because they create their own threads
+ // and texture caches.
+ // Using OMTC with BasicCompositor for context menus would probably be fine
+ // but isn't a well-tested configuration.
+ if ([mView window] && [[mView window] isKindOfClass:[PopupWindow class]]) {
+ // Use main-thread BasicLayerManager for drawing menus.
+ return false;
+ }
+
+ return nsBaseWidget::ShouldUseOffMainThreadCompositing();
+}
+
+#pragma mark -
+
+// Invokes callback and ProcessEvent methods on Event Listener object
+nsresult nsChildView::DispatchEvent(WidgetGUIEvent* event,
+ nsEventStatus& aStatus) {
+ RefPtr<nsChildView> kungFuDeathGrip(this);
+
+#ifdef DEBUG
+ debug_DumpEvent(stdout, event->mWidget, event, "something", 0);
+#endif
+
+ if (event->mFlags.mIsSynthesizedForTests) {
+ WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
+ if (keyEvent) {
+ nsresult rv = mTextInputHandler->AttachNativeKeyEvent(*keyEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ aStatus = nsEventStatus_eIgnore;
+
+ nsIWidgetListener* listener = mWidgetListener;
+
+ // If the listener is NULL, check if the parent is a popup. If it is, then
+ // this child is the popup content view attached to a popup. Get the
+ // listener from the parent popup instead.
+ nsCOMPtr<nsIWidget> parentWidget = mParentWidget;
+ if (!listener && parentWidget) {
+ if (parentWidget->GetWindowType() == WindowType::Popup) {
+ // Check just in case event->mWidget isn't this widget
+ if (event->mWidget) {
+ listener = event->mWidget->GetWidgetListener();
+ }
+ if (!listener) {
+ event->mWidget = parentWidget;
+ listener = parentWidget->GetWidgetListener();
+ }
+ }
+ }
+
+ if (listener) aStatus = listener->HandleEvent(event, mUseAttachedEvents);
+
+ return NS_OK;
+}
+
+nsIWidget* nsChildView::GetWidgetForListenerEvents() {
+ // If there is no listener, use the parent popup's listener if that exists.
+ if (!mWidgetListener && mParentWidget &&
+ mParentWidget->GetWindowType() == WindowType::Popup) {
+ return mParentWidget;
+ }
+
+ return this;
+}
+
+void nsChildView::WillPaintWindow() {
+ nsCOMPtr<nsIWidget> widget = GetWidgetForListenerEvents();
+
+ nsIWidgetListener* listener = widget->GetWidgetListener();
+ if (listener) {
+ listener->WillPaintWindow(widget);
+ }
+}
+
+bool nsChildView::PaintWindow(LayoutDeviceIntRegion aRegion) {
+ nsCOMPtr<nsIWidget> widget = GetWidgetForListenerEvents();
+
+ nsIWidgetListener* listener = widget->GetWidgetListener();
+ if (!listener) return false;
+
+ bool returnValue = false;
+ bool oldDispatchPaint = mIsDispatchPaint;
+ mIsDispatchPaint = true;
+ returnValue = listener->PaintWindow(widget, aRegion);
+
+ listener = widget->GetWidgetListener();
+ if (listener) {
+ listener->DidPaintWindow();
+ }
+
+ mIsDispatchPaint = oldDispatchPaint;
+ return returnValue;
+}
+
+bool nsChildView::PaintWindowInDrawTarget(gfx::DrawTarget* aDT,
+ const LayoutDeviceIntRegion& aRegion,
+ const gfx::IntSize& aSurfaceSize) {
+ if (!aDT || !aDT->IsValid()) {
+ return false;
+ }
+ gfxContext targetContext(aDT);
+
+ // Set up the clip region and clear existing contents in the backing surface.
+ targetContext.NewPath();
+ for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) {
+ const LayoutDeviceIntRect& r = iter.Get();
+ targetContext.Rectangle(gfxRect(r.x, r.y, r.width, r.height));
+ aDT->ClearRect(gfx::Rect(r.ToUnknownRect()));
+ }
+ targetContext.Clip();
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(mView);
+ if (GetWindowRenderer()->GetBackendType() == LayersBackend::LAYERS_NONE) {
+ nsBaseWidget::AutoLayerManagerSetup setupLayerManager(
+ this, &targetContext, BufferMode::BUFFER_NONE);
+ return PaintWindow(aRegion);
+ }
+ return false;
+}
+
+void nsChildView::EnsureContentLayerForMainThreadPainting() {
+ // Ensure we have an mContentLayer of the correct size.
+ // The content layer gets created on demand for BasicLayers windows. We do
+ // not create it during widget creation because, for non-BasicLayers windows,
+ // the compositing layer manager will create any layers it needs.
+ gfx::IntSize size = GetBounds().Size().ToUnknownSize();
+ if (mContentLayer && mContentLayer->GetSize() != size) {
+ mNativeLayerRoot->RemoveLayer(mContentLayer);
+ mContentLayer = nullptr;
+ }
+ if (!mContentLayer) {
+ mPoolHandle = SurfacePool::Create(0)->GetHandleForGL(nullptr);
+ RefPtr<NativeLayer> contentLayer =
+ mNativeLayerRoot->CreateLayer(size, false, mPoolHandle);
+ mNativeLayerRoot->AppendLayer(contentLayer);
+ mContentLayer = contentLayer->AsNativeLayerCA();
+ mContentLayer->SetSurfaceIsFlipped(false);
+ mContentLayerInvalidRegion = GetBounds();
+ }
+}
+
+void nsChildView::PaintWindowInContentLayer() {
+ EnsureContentLayerForMainThreadPainting();
+ mPoolHandle->OnBeginFrame();
+ RefPtr<DrawTarget> dt = mContentLayer->NextSurfaceAsDrawTarget(
+ gfx::IntRect({}, mContentLayer->GetSize()),
+ mContentLayerInvalidRegion.ToUnknownRegion(), gfx::BackendType::SKIA);
+ if (!dt) {
+ return;
+ }
+
+ PaintWindowInDrawTarget(dt, mContentLayerInvalidRegion, dt->GetSize());
+ mContentLayer->NotifySurfaceReady();
+ mContentLayerInvalidRegion.SetEmpty();
+ mPoolHandle->OnEndFrame();
+}
+
+void nsChildView::HandleMainThreadCATransaction() {
+ WillPaintWindow();
+
+ if (GetWindowRenderer()->GetBackendType() == LayersBackend::LAYERS_NONE) {
+ // We're in BasicLayers mode, i.e. main thread software compositing.
+ // Composite the window into our layer's surface.
+ PaintWindowInContentLayer();
+ } else {
+ // Trigger a synchronous OMTC composite. This will call NextSurface and
+ // NotifySurfaceReady on the compositor thread to update mNativeLayerRoot's
+ // contents, and the main thread (this thread) will wait inside PaintWindow
+ // during that time.
+ PaintWindow(LayoutDeviceIntRegion(GetBounds()));
+ }
+
+ {
+ // Apply the changes inside mNativeLayerRoot to the underlying CALayers. Now
+ // is a good time to call this because we know we're currently inside a main
+ // thread CATransaction, and the lock makes sure that no composition is
+ // currently in progress, so we won't present half-composited state to the
+ // screen.
+ MutexAutoLock lock(mCompositingLock);
+ mNativeLayerRoot->CommitToScreen();
+ }
+
+ MaybeScheduleUnsuspendAsyncCATransactions();
+}
+
+#pragma mark -
+
+void nsChildView::ReportMoveEvent() { NotifyWindowMoved(mBounds.x, mBounds.y); }
+
+void nsChildView::ReportSizeEvent() {
+ if (mWidgetListener)
+ mWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
+}
+
+#pragma mark -
+
+LayoutDeviceIntPoint nsChildView::GetClientOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSPoint origin = [mView convertPoint:NSMakePoint(0, 0) toView:nil];
+ origin.y = [[mView window] frame].size.height - origin.y;
+ return CocoaPointsToDevPixels(origin);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+// Return the offset between this child view and the screen.
+// @return -- widget origin in device-pixel coords
+LayoutDeviceIntPoint nsChildView::WidgetToScreenOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSPoint origin = NSMakePoint(0, 0);
+
+ // 1. First translate view origin point into window coords.
+ // The returned point is in bottom-left coordinates.
+ origin = [mView convertPoint:origin toView:nil];
+
+ // 2. We turn the window-coord rect's origin into screen (still bottom-left)
+ // coords.
+ origin = nsCocoaUtils::ConvertPointToScreen([mView window], origin);
+
+ // 3. Since we're dealing in bottom-left coords, we need to make it top-left
+ // coords
+ // before we pass it back to Gecko.
+ FlipCocoaScreenCoordinate(origin);
+
+ // convert to device pixels
+ return CocoaPointsToDevPixels(origin);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+nsresult nsChildView::SetTitle(const nsAString& title) {
+ // child views don't have titles
+ return NS_OK;
+}
+
+nsresult nsChildView::GetAttention(int32_t aCycleCount) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [NSApp requestUserAttention:NSInformationalRequest];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+/* static */
+bool nsChildView::DoHasPendingInputEvent() {
+ return sLastInputEventCount != GetCurrentInputEventCount();
+}
+
+/* static */
+uint32_t nsChildView::GetCurrentInputEventCount() {
+ // Can't use kCGAnyInputEventType because that updates too rarely for us (and
+ // always in increments of 30+!) and because apparently it's sort of broken
+ // on Tiger. So just go ahead and query the counters we care about.
+ static const CGEventType eventTypes[] = {kCGEventLeftMouseDown,
+ kCGEventLeftMouseUp,
+ kCGEventRightMouseDown,
+ kCGEventRightMouseUp,
+ kCGEventMouseMoved,
+ kCGEventLeftMouseDragged,
+ kCGEventRightMouseDragged,
+ kCGEventKeyDown,
+ kCGEventKeyUp,
+ kCGEventScrollWheel,
+ kCGEventTabletPointer,
+ kCGEventOtherMouseDown,
+ kCGEventOtherMouseUp,
+ kCGEventOtherMouseDragged};
+
+ uint32_t eventCount = 0;
+ for (uint32_t i = 0; i < ArrayLength(eventTypes); ++i) {
+ eventCount += CGEventSourceCounterForEventType(
+ kCGEventSourceStateCombinedSessionState, eventTypes[i]);
+ }
+ return eventCount;
+}
+
+/* static */
+void nsChildView::UpdateCurrentInputEventCount() {
+ sLastInputEventCount = GetCurrentInputEventCount();
+}
+
+bool nsChildView::HasPendingInputEvent() { return DoHasPendingInputEvent(); }
+
+#pragma mark -
+
+void nsChildView::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ NS_ENSURE_TRUE_VOID(mTextInputHandler);
+
+ if (mTextInputHandler->IsFocused()) {
+ if (aContext.IsPasswordEditor()) {
+ TextInputHandler::EnableSecureEventInput();
+ } else {
+ TextInputHandler::EnsureSecureEventInputDisabled();
+ }
+ }
+
+ // IMEInputHandler::IsEditableContent() returns false when both
+ // IsASCIICableOnly() and IsIMEEnabled() return false. So, be careful
+ // when you change the following code. You might need to change
+ // IMEInputHandler::IsEditableContent() too.
+ mInputContext = aContext;
+ switch (aContext.mIMEState.mEnabled) {
+ case IMEEnabled::Enabled:
+ mTextInputHandler->SetASCIICapableOnly(false);
+ mTextInputHandler->EnableIME(true);
+ if (mInputContext.mIMEState.mOpen != IMEState::DONT_CHANGE_OPEN_STATE) {
+ mTextInputHandler->SetIMEOpenState(mInputContext.mIMEState.mOpen ==
+ IMEState::OPEN);
+ }
+ break;
+ case IMEEnabled::Disabled:
+ mTextInputHandler->SetASCIICapableOnly(false);
+ mTextInputHandler->EnableIME(false);
+ break;
+ case IMEEnabled::Password:
+ mTextInputHandler->SetASCIICapableOnly(true);
+ mTextInputHandler->EnableIME(false);
+ break;
+ default:
+ NS_ERROR("not implemented!");
+ }
+}
+
+InputContext nsChildView::GetInputContext() {
+ switch (mInputContext.mIMEState.mEnabled) {
+ case IMEEnabled::Enabled:
+ if (mTextInputHandler) {
+ mInputContext.mIMEState.mOpen = mTextInputHandler->IsIMEOpened()
+ ? IMEState::OPEN
+ : IMEState::CLOSED;
+ break;
+ }
+ // If mTextInputHandler is null, set CLOSED instead...
+ [[fallthrough]];
+ default:
+ mInputContext.mIMEState.mOpen = IMEState::CLOSED;
+ break;
+ }
+ return mInputContext;
+}
+
+TextEventDispatcherListener*
+nsChildView::GetNativeTextEventDispatcherListener() {
+ if (NS_WARN_IF(!mTextInputHandler)) {
+ return nullptr;
+ }
+ return mTextInputHandler;
+}
+
+nsresult nsChildView::AttachNativeKeyEvent(
+ mozilla::WidgetKeyboardEvent& aEvent) {
+ NS_ENSURE_TRUE(mTextInputHandler, NS_ERROR_NOT_AVAILABLE);
+ return mTextInputHandler->AttachNativeKeyEvent(aEvent);
+}
+
+bool nsChildView::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+
+ Maybe<WritingMode> writingMode;
+ if (aEvent.NeedsToRemapNavigationKey()) {
+ if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+ writingMode = dispatcher->MaybeQueryWritingModeAtSelection();
+ }
+ }
+
+ NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
+ keyBindings->GetEditCommands(aEvent, writingMode, aCommands);
+ return true;
+}
+
+NSView<mozView>* nsChildView::GetEditorView() {
+ NSView<mozView>* editorView = mView;
+ // We need to get editor's view. E.g., when the focus is in the bookmark
+ // dialog, the view is <panel> element of the dialog. At this time, the key
+ // events are processed the parent window's view that has native focus.
+ WidgetQueryContentEvent queryContentState(true, eQueryContentState, this);
+ // This may be called during creating a menu popup frame due to creating
+ // widget synchronously and that causes Cocoa asking current window level.
+ // In this case, it's not safe to flush layout on the document and we don't
+ // need any layout information right now.
+ queryContentState.mNeedsToFlushLayout = false;
+ DispatchWindowEvent(queryContentState);
+ if (queryContentState.Succeeded() &&
+ queryContentState.mReply->mFocusedWidget) {
+ NSView<mozView>* view = static_cast<NSView<mozView>*>(
+ queryContentState.mReply->mFocusedWidget->GetNativeData(
+ NS_NATIVE_WIDGET));
+ if (view) editorView = view;
+ }
+ return editorView;
+}
+
+#pragma mark -
+
+void nsChildView::CreateCompositor() {
+ nsBaseWidget::CreateCompositor();
+ if (mCompositorBridgeChild) {
+ [mView setUsingOMTCompositor:true];
+ }
+}
+
+void nsChildView::ConfigureAPZCTreeManager() {
+ nsBaseWidget::ConfigureAPZCTreeManager();
+}
+
+void nsChildView::ConfigureAPZControllerThread() {
+ nsBaseWidget::ConfigureAPZControllerThread();
+}
+
+bool nsChildView::PreRender(WidgetRenderingContext* aContext)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ // The lock makes sure that we don't attempt to tear down the view while
+ // compositing. That would make us unable to call postRender on it when the
+ // composition is done, thus keeping the GL context locked forever.
+ mCompositingLock.Lock();
+
+ if (aContext->mGL && gfxPlatform::CanMigrateMacGPUs()) {
+ GLContextCGL::Cast(aContext->mGL)->MigrateToActiveGPU();
+ }
+
+ return true;
+}
+
+void nsChildView::PostRender(WidgetRenderingContext* aContext)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ mCompositingLock.Unlock();
+}
+
+RefPtr<layers::NativeLayerRoot> nsChildView::GetNativeLayerRoot() {
+ return mNativeLayerRoot;
+}
+
+static int32_t FindTitlebarBottom(
+ const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries,
+ int32_t aWindowWidth) {
+ int32_t titlebarBottom = 0;
+ for (auto& g : aThemeGeometries) {
+ if (g.mType == eThemeGeometryTypeTitlebar && g.mRect.X() <= 0 &&
+ g.mRect.XMost() >= aWindowWidth && g.mRect.Y() <= 0) {
+ titlebarBottom = std::max(titlebarBottom, g.mRect.YMost());
+ }
+ }
+ return titlebarBottom;
+}
+
+static int32_t FindUnifiedToolbarBottom(
+ const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries,
+ int32_t aWindowWidth, int32_t aTitlebarBottom) {
+ int32_t unifiedToolbarBottom = aTitlebarBottom;
+ for (uint32_t i = 0; i < aThemeGeometries.Length(); ++i) {
+ const nsIWidget::ThemeGeometry& g = aThemeGeometries[i];
+ if ((g.mType == eThemeGeometryTypeToolbar) && g.mRect.X() <= 0 &&
+ g.mRect.XMost() >= aWindowWidth && g.mRect.Y() <= aTitlebarBottom) {
+ unifiedToolbarBottom = std::max(unifiedToolbarBottom, g.mRect.YMost());
+ }
+ }
+ return unifiedToolbarBottom;
+}
+
+static LayoutDeviceIntRect FindFirstRectOfType(
+ const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries,
+ nsITheme::ThemeGeometryType aThemeGeometryType) {
+ for (uint32_t i = 0; i < aThemeGeometries.Length(); ++i) {
+ const nsIWidget::ThemeGeometry& g = aThemeGeometries[i];
+ if (g.mType == aThemeGeometryType) {
+ return g.mRect;
+ }
+ }
+ return LayoutDeviceIntRect();
+}
+
+void nsChildView::UpdateThemeGeometries(
+ const nsTArray<ThemeGeometry>& aThemeGeometries) {
+ if (![mView window]) return;
+
+ UpdateVibrancy(aThemeGeometries);
+
+ if (![[mView window] isKindOfClass:[ToolbarWindow class]]) return;
+
+ // Update unified toolbar height and sheet attachment position.
+ int32_t windowWidth = mBounds.width;
+ int32_t titlebarBottom = FindTitlebarBottom(aThemeGeometries, windowWidth);
+ int32_t unifiedToolbarBottom =
+ FindUnifiedToolbarBottom(aThemeGeometries, windowWidth, titlebarBottom);
+ int32_t toolboxBottom =
+ FindFirstRectOfType(aThemeGeometries, eThemeGeometryTypeToolbox).YMost();
+
+ ToolbarWindow* win = (ToolbarWindow*)[mView window];
+ int32_t titlebarHeight = [win drawsContentsIntoWindowFrame]
+ ? 0
+ : CocoaPointsToDevPixels([win titlebarHeight]);
+ int32_t devUnifiedHeight = titlebarHeight + unifiedToolbarBottom;
+ [win setUnifiedToolbarHeight:DevPixelsToCocoaPoints(devUnifiedHeight)];
+
+ int32_t sheetPositionDevPx = std::max(toolboxBottom, unifiedToolbarBottom);
+ NSPoint sheetPositionView = {0, DevPixelsToCocoaPoints(sheetPositionDevPx)};
+ NSPoint sheetPositionWindow = [mView convertPoint:sheetPositionView
+ toView:nil];
+ [win setSheetAttachmentPosition:sheetPositionWindow.y];
+
+ // Update titlebar control offsets.
+ LayoutDeviceIntRect windowButtonRect =
+ FindFirstRectOfType(aThemeGeometries, eThemeGeometryTypeWindowButtons);
+ [win placeWindowButtons:[mView convertRect:DevPixelsToCocoaPoints(
+ windowButtonRect)
+ toView:nil]];
+}
+
+static Maybe<VibrancyType> ThemeGeometryTypeToVibrancyType(
+ nsITheme::ThemeGeometryType aThemeGeometryType) {
+ switch (aThemeGeometryType) {
+ case eThemeGeometryTypeTooltip:
+ return Some(VibrancyType::TOOLTIP);
+ case eThemeGeometryTypeMenu:
+ return Some(VibrancyType::MENU);
+ default:
+ return Nothing();
+ }
+}
+
+static LayoutDeviceIntRegion GatherVibrantRegion(
+ const nsTArray<nsIWidget::ThemeGeometry>& aThemeGeometries,
+ VibrancyType aVibrancyType) {
+ LayoutDeviceIntRegion region;
+ for (auto& geometry : aThemeGeometries) {
+ if (ThemeGeometryTypeToVibrancyType(geometry.mType) ==
+ Some(aVibrancyType)) {
+ region.OrWith(geometry.mRect);
+ }
+ }
+ return region;
+}
+
+template <typename Region>
+static void MakeRegionsNonOverlappingImpl(Region& aOutUnion) {}
+
+template <typename Region, typename... Regions>
+static void MakeRegionsNonOverlappingImpl(Region& aOutUnion, Region& aFirst,
+ Regions&... aRest) {
+ MakeRegionsNonOverlappingImpl(aOutUnion, aRest...);
+ aFirst.SubOut(aOutUnion);
+ aOutUnion.OrWith(aFirst);
+}
+
+// Subtracts parts from regions in such a way that they don't have any overlap.
+// Each region in the argument list will have the union of all the regions
+// *following* it subtracted from itself. In other words, the arguments are
+// sorted low priority to high priority.
+template <typename Region, typename... Regions>
+static void MakeRegionsNonOverlapping(Region& aFirst, Regions&... aRest) {
+ Region unionOfAll;
+ MakeRegionsNonOverlappingImpl(unionOfAll, aFirst, aRest...);
+}
+
+void nsChildView::UpdateVibrancy(
+ const nsTArray<ThemeGeometry>& aThemeGeometries) {
+ LayoutDeviceIntRegion menuRegion =
+ GatherVibrantRegion(aThemeGeometries, VibrancyType::MENU);
+ LayoutDeviceIntRegion tooltipRegion =
+ GatherVibrantRegion(aThemeGeometries, VibrancyType::TOOLTIP);
+
+ MakeRegionsNonOverlapping(menuRegion, tooltipRegion);
+
+ auto& vm = EnsureVibrancyManager();
+ bool changed = false;
+ changed |= vm.UpdateVibrantRegion(VibrancyType::MENU, menuRegion);
+ changed |= vm.UpdateVibrantRegion(VibrancyType::TOOLTIP, tooltipRegion);
+
+ if (changed) {
+ SuspendAsyncCATransactions();
+ }
+}
+
+mozilla::VibrancyManager& nsChildView::EnsureVibrancyManager() {
+ MOZ_ASSERT(mView, "Only call this once we have a view!");
+ if (!mVibrancyManager) {
+ mVibrancyManager =
+ MakeUnique<VibrancyManager>(*this, [mView vibrancyViewsContainer]);
+ }
+ return *mVibrancyManager;
+}
+
+void nsChildView::UpdateBoundsFromView() {
+ auto oldSize = mBounds.Size();
+ mBounds = CocoaPointsToDevPixels([mView frame]);
+ if (mBounds.Size() != oldSize) {
+ SuspendAsyncCATransactions();
+ }
+}
+
+@interface NonDraggableView : NSView
+@end
+
+@implementation NonDraggableView
+- (BOOL)mouseDownCanMoveWindow {
+ return NO;
+}
+- (NSView*)hitTest:(NSPoint)aPoint {
+ return nil;
+}
+- (NSRect)_opaqueRectForWindowMoveWhenInTitlebar {
+ // In NSWindows that use NSWindowStyleMaskFullSizeContentView, NSViews which
+ // overlap the titlebar do not disable window dragging in the overlapping
+ // areas even if they return NO from mouseDownCanMoveWindow. This can have
+ // unfortunate effects: For example, dragging tabs in a browser window would
+ // move the window if those tabs are in the titlebar.
+ // macOS does not seem to offer a documented way to opt-out of the forced
+ // window dragging in the titlebar.
+ // Overriding _opaqueRectForWindowMoveWhenInTitlebar is an undocumented way
+ // of opting out of this behavior. This method was added in 10.11 and is used
+ // by some NSControl subclasses to prevent window dragging in the titlebar.
+ // The function which assembles the draggable area of the window calls
+ // _opaqueRect for the content area and _opaqueRectForWindowMoveWhenInTitlebar
+ // for the titlebar area, on all visible NSViews. The default implementation
+ // of _opaqueRect returns [self visibleRect], and the default implementation
+ // of _opaqueRectForWindowMoveWhenInTitlebar returns NSZeroRect unless it's
+ // overridden.
+ //
+ // Since this view is constructed and used such that its entire bounds is the
+ // relevant region, we just return our bounds.
+ return self.bounds;
+}
+@end
+
+void nsChildView::UpdateWindowDraggingRegion(
+ const LayoutDeviceIntRegion& aRegion) {
+ // mView returns YES from mouseDownCanMoveWindow, so we need to put NSViews
+ // that return NO from mouseDownCanMoveWindow in the places that shouldn't
+ // be draggable. We can't do it the other way round because returning
+ // YES from mouseDownCanMoveWindow doesn't have any effect if there's a
+ // superview that returns NO.
+ LayoutDeviceIntRegion nonDraggable;
+ nonDraggable.Sub(LayoutDeviceIntRect(0, 0, mBounds.width, mBounds.height),
+ aRegion);
+
+ __block bool changed = false;
+
+ // Suppress calls to setNeedsDisplay during NSView geometry changes.
+ ManipulateViewWithoutNeedingDisplay(mView, ^() {
+ changed = mNonDraggableRegion.UpdateRegion(
+ nonDraggable, *this, [mView nonDraggableViewsContainer], ^() {
+ return [[NonDraggableView alloc] initWithFrame:NSZeroRect];
+ });
+ });
+
+ if (changed) {
+ // Trigger an update to the window server. This will call
+ // mouseDownCanMoveWindow.
+ // Doing this manually is only necessary because we're suppressing
+ // setNeedsDisplay calls above.
+ [[mView window] setMovableByWindowBackground:NO];
+ [[mView window] setMovableByWindowBackground:YES];
+ }
+}
+
+nsEventStatus nsChildView::DispatchAPZInputEvent(InputData& aEvent) {
+ APZEventResult result;
+
+ if (mAPZC) {
+ result = mAPZC->InputBridge()->ReceiveInputEvent(aEvent);
+ }
+
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return result.GetStatus();
+ }
+
+ if (aEvent.mInputType == PINCHGESTURE_INPUT) {
+ PinchGestureInput& pinchEvent = aEvent.AsPinchGestureInput();
+ WidgetWheelEvent wheelEvent = pinchEvent.ToWidgetEvent(this);
+ ProcessUntransformedAPZEvent(&wheelEvent, result);
+ } else if (aEvent.mInputType == TAPGESTURE_INPUT) {
+ TapGestureInput& tapEvent = aEvent.AsTapGestureInput();
+ WidgetSimpleGestureEvent gestureEvent = tapEvent.ToWidgetEvent(this);
+ ProcessUntransformedAPZEvent(&gestureEvent, result);
+ } else {
+ MOZ_ASSERT_UNREACHABLE();
+ }
+
+ return result.GetStatus();
+}
+
+void nsChildView::DispatchAPZWheelInputEvent(InputData& aEvent) {
+ if (mSwipeTracker && aEvent.mInputType == PANGESTURE_INPUT) {
+ // Give the swipe tracker a first pass at the event. If a new pan gesture
+ // has been started since the beginning of the swipe, the swipe tracker
+ // will know to ignore the event.
+ nsEventStatus status =
+ mSwipeTracker->ProcessEvent(aEvent.AsPanGestureInput());
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ }
+
+ WidgetWheelEvent event(true, eWheel, this);
+
+ if (mAPZC) {
+ APZEventResult result;
+
+ switch (aEvent.mInputType) {
+ case PANGESTURE_INPUT: {
+ result = mAPZC->InputBridge()->ReceiveInputEvent(aEvent);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+
+ event = MayStartSwipeForAPZ(aEvent.AsPanGestureInput(), result);
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ // For wheel events on OS X, send it to APZ using the WidgetInputEvent
+ // variant of ReceiveInputEvent, because the APZInputBridge version of
+ // that function has special handling (for delta multipliers etc.) that
+ // we need to run. Using the InputData variant would bypass that and
+ // go straight to the APZCTreeManager subclass.
+ event = aEvent.AsScrollWheelInput().ToWidgetEvent(this);
+ result = mAPZC->InputBridge()->ReceiveInputEvent(event);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ break;
+ };
+ default:
+ MOZ_CRASH("unsupported event type");
+ return;
+ }
+ if (event.mMessage == eWheel &&
+ (event.mDeltaX != 0 || event.mDeltaY != 0)) {
+ ProcessUntransformedAPZEvent(&event, result);
+ }
+ return;
+ }
+
+ nsEventStatus status;
+ switch (aEvent.mInputType) {
+ case PANGESTURE_INPUT: {
+ if (MayStartSwipeForNonAPZ(aEvent.AsPanGestureInput())) {
+ return;
+ }
+ event = aEvent.AsPanGestureInput().ToWidgetEvent(this);
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ event = aEvent.AsScrollWheelInput().ToWidgetEvent(this);
+ break;
+ }
+ default:
+ MOZ_CRASH("unexpected event type");
+ return;
+ }
+ if (event.mMessage == eWheel && (event.mDeltaX != 0 || event.mDeltaY != 0)) {
+ DispatchEvent(&event, status);
+ }
+}
+
+void nsChildView::DispatchDoubleTapGesture(TimeStamp aEventTimeStamp,
+ LayoutDeviceIntPoint aScreenPosition,
+ mozilla::Modifiers aModifiers) {
+ if (StaticPrefs::apz_mac_enable_double_tap_zoom_touchpad_gesture()) {
+ TapGestureInput event{
+ TapGestureInput::TAPGESTURE_DOUBLE, aEventTimeStamp,
+ ViewAs<ScreenPixel>(
+ aScreenPosition,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent),
+ aModifiers};
+
+ DispatchAPZInputEvent(event);
+ } else {
+ // Setup the "double tap" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eTapGesture, this);
+ // do what convertCocoaMouseEvent does basically.
+ geckoEvent.mRefPoint = aScreenPosition;
+ geckoEvent.mModifiers = aModifiers;
+ geckoEvent.mTimeStamp = aEventTimeStamp;
+ geckoEvent.mClickCount = 1;
+
+ // Send the event.
+ DispatchWindowEvent(geckoEvent);
+ }
+}
+
+void nsChildView::LookUpDictionary(
+ const nsAString& aText, const nsTArray<mozilla::FontRange>& aFontRangeArray,
+ const bool aIsVertical, const LayoutDeviceIntPoint& aPoint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSMutableAttributedString* attrStr =
+ nsCocoaUtils::GetNSMutableAttributedString(
+ aText, aFontRangeArray, aIsVertical, BackingScaleFactor());
+ NSPoint pt =
+ nsCocoaUtils::DevPixelsToCocoaPoints(aPoint, BackingScaleFactor());
+ NSDictionary* attributes = [attrStr attributesAtIndex:0 effectiveRange:nil];
+ NSFont* font = [attributes objectForKey:NSFontAttributeName];
+ if (font) {
+ if (aIsVertical) {
+ pt.x -= [font descender];
+ } else {
+ pt.y += [font ascender];
+ }
+ }
+
+ [mView showDefinitionForAttributedString:attrStr atPoint:pt];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#ifdef ACCESSIBILITY
+already_AddRefed<a11y::LocalAccessible> nsChildView::GetDocumentAccessible() {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) return nullptr;
+
+ // mAccessible might be dead if accessibility was previously disabled and is
+ // now being enabled again.
+ if (mAccessible && mAccessible->IsAlive()) {
+ RefPtr<a11y::LocalAccessible> ret;
+ CallQueryReferent(mAccessible.get(), static_cast<a11y::LocalAccessible**>(
+ getter_AddRefs(ret)));
+ return ret.forget();
+ }
+
+ // need to fetch the accessible anew, because it has gone away.
+ // cache the accessible in our weak ptr
+ RefPtr<a11y::LocalAccessible> acc = GetRootAccessible();
+ mAccessible = do_GetWeakReference(acc.get());
+
+ return acc.forget();
+}
+#endif
+
+class WidgetsReleaserRunnable final : public mozilla::Runnable {
+ public:
+ explicit WidgetsReleaserRunnable(nsTArray<nsCOMPtr<nsIWidget>>&& aWidgetArray)
+ : mozilla::Runnable("WidgetsReleaserRunnable"),
+ mWidgetArray(std::move(aWidgetArray)) {}
+
+ // Do nothing; all this runnable does is hold a reference the widgets in
+ // mWidgetArray, and those references will be dropped when this runnable
+ // is destroyed.
+
+ private:
+ nsTArray<nsCOMPtr<nsIWidget>> mWidgetArray;
+};
+
+#pragma mark -
+
+// ViewRegionContainerView is a view class for certain subviews of ChildView
+// which contain the NSViews created for ViewRegions (see ViewRegion.h).
+// It doesn't do anything interesting, it only acts as a container so that it's
+// easier for ChildView to control the z order of its children.
+@interface ViewRegionContainerView : NSView {
+}
+@end
+
+@implementation ViewRegionContainerView
+
+- (NSView*)hitTest:(NSPoint)aPoint {
+ return nil; // Be transparent to mouse events.
+}
+
+- (BOOL)isFlipped {
+ return [[self superview] isFlipped];
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+ return [[self superview] mouseDownCanMoveWindow];
+}
+
+@end
+
+@implementation ChildView
+
+// globalDragPboard is non-null during native drag sessions that did not
+// originate in our native NSView (it is set in |draggingEntered:|). It is unset
+// when the drag session ends for this view, either with the mouse exiting or
+// when a drop occurs in this view.
+NSPasteboard* globalDragPboard = nil;
+
+// gLastDragView and gLastDragMouseDownEvent are used to communicate information
+// to the drag service during drag invocation (starting a drag in from the
+// view). gLastDragView is only non-null while a mouse button is pressed, so
+// between mouseDown and mouseUp.
+NSView* gLastDragView = nil; // [weak]
+NSEvent* gLastDragMouseDownEvent = nil; // [strong]
+
++ (void)initialize {
+ static BOOL initialized = NO;
+
+ if (!initialized) {
+ // Inform the OS about the types of services (from the "Services" menu)
+ // that we can handle.
+ NSArray* types = @[
+ [UTIHelper stringFromPboardType:NSPasteboardTypeString],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeHTML]
+ ];
+ [NSApp registerServicesMenuSendTypes:types returnTypes:types];
+ initialized = YES;
+ }
+}
+
++ (void)registerViewForDraggedTypes:(NSView*)aView {
+ [aView
+ registerForDraggedTypes:
+ [NSArray
+ arrayWithObjects:
+ [UTIHelper stringFromPboardType:NSFilenamesPboardType],
+ [UTIHelper stringFromPboardType:kMozFileUrlsPboardType],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeString],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeHTML],
+ [UTIHelper
+ stringFromPboardType:(NSString*)
+ kPasteboardTypeFileURLPromise],
+ [UTIHelper stringFromPboardType:kMozWildcardPboardType],
+ [UTIHelper stringFromPboardType:kPublicUrlPboardType],
+ [UTIHelper stringFromPboardType:kPublicUrlNamePboardType],
+ [UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType],
+ nil]];
+}
+
+// initWithFrame:geckoChild:
+- (id)initWithFrame:(NSRect)inFrame geckoChild:(nsChildView*)inChild {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super initWithFrame:inFrame])) {
+ mGeckoChild = inChild;
+ mBlockedLastMouseDown = NO;
+ mExpectingWheelStop = NO;
+
+ mLastMouseDownEvent = nil;
+ mLastKeyDownEvent = nil;
+ mClickThroughMouseDownEvent = nil;
+ mDragService = nullptr;
+
+ mGestureState = eGestureState_None;
+ mCumulativeRotation = 0.0;
+
+ mIsUpdatingLayer = NO;
+
+ [self setFocusRingType:NSFocusRingTypeNone];
+
+#ifdef __LP64__
+ mCancelSwipeAnimation = nil;
+#endif
+
+ mNonDraggableViewsContainer =
+ [[ViewRegionContainerView alloc] initWithFrame:[self bounds]];
+ mVibrancyViewsContainer =
+ [[ViewRegionContainerView alloc] initWithFrame:[self bounds]];
+
+ [mNonDraggableViewsContainer
+ setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+ [mVibrancyViewsContainer
+ setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+
+ [self addSubview:mNonDraggableViewsContainer];
+ [self addSubview:mVibrancyViewsContainer];
+
+ mPixelHostingView = [[PixelHostingView alloc] initWithFrame:[self bounds]];
+ [mPixelHostingView
+ setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+
+ [self addSubview:mPixelHostingView];
+
+ mRootCALayer = [[CALayer layer] retain];
+ mRootCALayer.position = NSZeroPoint;
+ mRootCALayer.bounds = NSZeroRect;
+ mRootCALayer.anchorPoint = NSZeroPoint;
+ mRootCALayer.contentsGravity = kCAGravityTopLeft;
+ [[mPixelHostingView layer] addSublayer:mRootCALayer];
+
+ mLastPressureStage = 0;
+ }
+
+ // register for things we'll take from other applications
+ [ChildView registerViewForDraggedTypes:self];
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSTextInputContext*)inputContext {
+ if (!mGeckoChild) {
+ // -[ChildView widgetDestroyed] has been called, but
+ // -[ChildView delayedTearDown] has not yet completed. Accessing
+ // [super inputContext] now would uselessly recreate a text input context
+ // for us, under which -[ChildView validAttributesForMarkedText] would
+ // be called and the assertion checking for mTextInputHandler would fail.
+ // We return nil to avoid that.
+ return nil;
+ }
+ return [super inputContext];
+}
+
+- (void)installTextInputHandler:(TextInputHandler*)aHandler {
+ mTextInputHandler = aHandler;
+}
+
+- (void)uninstallTextInputHandler {
+ mTextInputHandler = nullptr;
+}
+
+- (NSView*)vibrancyViewsContainer {
+ return mVibrancyViewsContainer;
+}
+
+- (NSView*)nonDraggableViewsContainer {
+ return mNonDraggableViewsContainer;
+}
+
+- (NSView*)pixelHostingView {
+ return mPixelHostingView;
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mLastMouseDownEvent release];
+ [mLastKeyDownEvent release];
+ [mClickThroughMouseDownEvent release];
+ ChildViewMouseTracker::OnDestroyView(self);
+
+ [mVibrancyViewsContainer removeFromSuperview];
+ [mVibrancyViewsContainer release];
+ [mNonDraggableViewsContainer removeFromSuperview];
+ [mNonDraggableViewsContainer release];
+ [mPixelHostingView removeFromSuperview];
+ [mPixelHostingView release];
+ [mRootCALayer release];
+
+ if (gLastDragView == self) {
+ gLastDragView = nil;
+ }
+
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)widgetDestroyed {
+ if (mTextInputHandler) {
+ mTextInputHandler->OnDestroyWidget(mGeckoChild);
+ mTextInputHandler = nullptr;
+ }
+ mGeckoChild = nullptr;
+
+ // Just in case we're destroyed abruptly and missed the draggingExited
+ // or performDragOperation message.
+ NS_IF_RELEASE(mDragService);
+}
+
+// mozView method, return our gecko child view widget. Note this does not
+// AddRef.
+- (nsIWidget*)widget {
+ return static_cast<nsIWidget*>(mGeckoChild);
+}
+
+- (NSString*)description {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [NSString stringWithFormat:@"ChildView %p, gecko child %p, frame %@",
+ self, mGeckoChild,
+ NSStringFromRect([self frame])];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Make the origin of this view the topLeft corner (gecko origin) rather
+// than the bottomLeft corner (standard cocoa origin).
+- (BOOL)isFlipped {
+ return YES;
+}
+
+// We accept key and mouse events, so don't keep passing them up the chain.
+// Allow this to be a 'focused' widget for event dispatch.
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+// Accept mouse down events on background windows
+- (BOOL)acceptsFirstMouse:(NSEvent*)aEvent {
+ if (![[self window] isKindOfClass:[PopupWindow class]]) {
+ // We rely on this function to tell us that the mousedown was on a
+ // background window. Inside mouseDown we can't tell whether we were
+ // inactive because at that point we've already been made active.
+ // Unfortunately, acceptsFirstMouse is called for PopupWindows even when
+ // their parent window is active, so ignore this on them for now.
+ mClickThroughMouseDownEvent = [aEvent retain];
+ }
+ return YES;
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+ // Return YES so that parts of this view can be draggable. The non-draggable
+ // parts will be covered by NSViews that return NO from
+ // mouseDownCanMoveWindow and thus override draggability from the inside.
+ // These views are assembled in nsChildView::UpdateWindowDraggingRegion.
+ return YES;
+}
+
+- (void)viewDidChangeBackingProperties {
+ [super viewDidChangeBackingProperties];
+ if (mGeckoChild) {
+ // actually, it could be the color space that's changed,
+ // but we can't tell the difference here except by retrieving
+ // the backing scale factor and comparing to the old value
+ mGeckoChild->BackingScaleFactorChanged();
+ }
+}
+
+- (BOOL)isCoveringTitlebar {
+ return [[self window] isKindOfClass:[BaseWindow class]] &&
+ [(BaseWindow*)[self window] mainChildView] == self &&
+ [(BaseWindow*)[self window] drawsContentsIntoWindowFrame];
+}
+
+- (void)viewWillStartLiveResize {
+ nsCocoaWindow* windowWidget =
+ mGeckoChild ? mGeckoChild->GetAppWindowWidget() : nullptr;
+ if (windowWidget) {
+ windowWidget->NotifyLiveResizeStarted();
+ }
+}
+
+- (void)viewDidEndLiveResize {
+ // mGeckoChild may legitimately be null here. It should also have been null
+ // in viewWillStartLiveResize, so there's no problem. However if we run into
+ // cases where the windowWidget was non-null in viewWillStartLiveResize but
+ // is null here, that might be problematic because we might get stuck with
+ // a content process that has the displayport suppressed. If that scenario
+ // arises (I'm not sure that it does) we will need to handle it gracefully.
+ nsCocoaWindow* windowWidget =
+ mGeckoChild ? mGeckoChild->GetAppWindowWidget() : nullptr;
+ if (windowWidget) {
+ windowWidget->NotifyLiveResizeStopped();
+ }
+}
+
+- (void)markLayerForDisplay {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ if (!mIsUpdatingLayer) {
+ // This call will cause updateRootCALayer to be called during the upcoming
+ // main thread CoreAnimation transaction. It will also trigger a transaction
+ // if no transaction is currently pending.
+ [[mPixelHostingView layer] setNeedsDisplay];
+ }
+}
+
+- (void)ensureNextCompositeIsAtomicWithMainThreadPaint {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ if (mGeckoChild) {
+ mGeckoChild->SuspendAsyncCATransactions();
+ }
+}
+
+- (void)updateRootCALayer {
+ if (NS_IsMainThread() && mGeckoChild) {
+ MOZ_RELEASE_ASSERT(!mIsUpdatingLayer, "Re-entrant layer display?");
+ mIsUpdatingLayer = YES;
+ mGeckoChild->HandleMainThreadCATransaction();
+ mIsUpdatingLayer = NO;
+ }
+}
+
+- (CALayer*)rootCALayer {
+ return mRootCALayer;
+}
+
+// If we've just created a non-native context menu, we need to mark it as
+// such and let the OS (and other programs) know when it opens and closes
+// (this is how the OS knows to close other programs' context menus when
+// ours open). We send the initial notification here, but others are sent
+// in nsCocoaWindow::Show().
+- (void)maybeInitContextMenuTracking {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus()) {
+ return;
+ }
+
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ NS_ENSURE_TRUE_VOID(rollupListener);
+ nsCOMPtr<nsIWidget> widget = rollupListener->GetRollupWidget();
+ NS_ENSURE_TRUE_VOID(widget);
+
+ NSWindow* popupWindow = (NSWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (!popupWindow || ![popupWindow isKindOfClass:[PopupWindow class]]) return;
+
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:@"com.apple.HIToolbox.beginMenuTrackingNotification"
+ object:@"org.mozilla.gecko.PopupWindow"];
+ [(PopupWindow*)popupWindow setIsContextMenu:YES];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Returns true if the event should no longer be processed, false otherwise.
+// This does not return whether or not anything was rolled up.
+- (BOOL)maybeRollup:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ BOOL consumeEvent = NO;
+
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ NS_ENSURE_TRUE(rollupListener, false);
+
+ BOOL isWheelTypeEvent = [theEvent type] == NSEventTypeScrollWheel ||
+ [theEvent type] == NSEventTypeMagnify ||
+ [theEvent type] == NSEventTypeSmartMagnify;
+
+ if (!isWheelTypeEvent && rollupListener->RollupNativeMenu()) {
+ // A native menu was rolled up.
+ // Don't consume this event; if the menu wanted to consume this event it
+ // would already have done so and we wouldn't even get here. For example, we
+ // won't get here for left clicks that close native menus (because the
+ // native menu consumes it), but we will get here for right clicks that
+ // close native menus, and we do not want to consume those right clicks.
+ return NO;
+ }
+
+ nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
+ if (!rollupWidget) {
+ return consumeEvent;
+ }
+
+ NSWindow* currentPopup =
+ static_cast<NSWindow*>(rollupWidget->GetNativeData(NS_NATIVE_WINDOW));
+ if (nsCocoaUtils::IsEventOverWindow(theEvent, currentPopup)) {
+ return consumeEvent;
+ }
+
+ // Check to see if scroll/zoom events should roll up the popup
+ if (isWheelTypeEvent) {
+ // consume scroll events that aren't over the popup unless the popup is an
+ // arrow panel.
+ consumeEvent = rollupListener->ShouldConsumeOnMouseWheelEvent();
+ if (!rollupListener->ShouldRollupOnMouseWheelEvent()) {
+ return consumeEvent;
+ }
+ }
+
+ // if we're dealing with menus, we probably have submenus and
+ // we don't want to rollup if the click is in a parent menu of
+ // the current submenu
+ uint32_t popupsToRollup = UINT32_MAX;
+ AutoTArray<nsIWidget*, 5> widgetChain;
+ uint32_t sameTypeCount = rollupListener->GetSubmenuWidgetChain(&widgetChain);
+ for (uint32_t i = 0; i < widgetChain.Length(); i++) {
+ nsIWidget* widget = widgetChain[i];
+ NSWindow* currWindow = (NSWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (nsCocoaUtils::IsEventOverWindow(theEvent, currWindow)) {
+ // don't roll up if the mouse event occurred within a menu of the
+ // same type. If the mouse event occurred in a menu higher than
+ // that, roll up, but pass the number of popups to Rollup so
+ // that only those of the same type close up.
+ if (i < sameTypeCount) {
+ return consumeEvent;
+ }
+ popupsToRollup = sameTypeCount;
+ break;
+ }
+ }
+
+ LayoutDeviceIntPoint devPoint;
+ nsIRollupListener::RollupOptions rollupOptions{
+ popupsToRollup, nsIRollupListener::FlushViews::Yes};
+ if ([theEvent type] == NSEventTypeLeftMouseDown) {
+ NSPoint point = [NSEvent mouseLocation];
+ FlipCocoaScreenCoordinate(point);
+ devPoint = mGeckoChild->CocoaPointsToDevPixels(point);
+ rollupOptions.mPoint = &devPoint;
+ }
+ consumeEvent = (BOOL)rollupListener->Rollup(rollupOptions);
+ return consumeEvent;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (void)swipeWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ float deltaX = [anEvent deltaX]; // left=1.0, right=-1.0
+ float deltaY = [anEvent deltaY]; // up=1.0, down=-1.0
+
+ // Setup the "swipe" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eSwipeGesture, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+
+ // Record the left/right direction.
+ if (deltaX > 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_LEFT;
+ else if (deltaX < 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT;
+
+ // Record the up/down direction.
+ if (deltaY > 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_UP;
+ else if (deltaY < 0.0)
+ geckoEvent.mDirection |= dom::SimpleGestureEvent_Binding::DIRECTION_DOWN;
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Pinch zoom gesture.
+- (void)magnifyWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([self maybeRollup:anEvent]) {
+ return;
+ }
+
+ if (!mGeckoChild) {
+ return;
+ }
+
+ // Instead of calling beginOrEndGestureForEventPhase we basically inline
+ // the effects of it here, because that function doesn't play too well with
+ // how we create PinchGestureInput events below. The main point of that
+ // function is to avoid flip-flopping between rotation/magnify gestures, which
+ // we can do by checking and setting mGestureState appropriately. A secondary
+ // result of that function is to send the final eMagnifyGesture event when
+ // the gesture ends, but APZ takes care of that for us.
+ if (mGestureState == eGestureState_RotateGesture &&
+ [anEvent phase] != NSEventPhaseBegan) {
+ // If we're already in a rotation and not "starting" a magnify, abort.
+ return;
+ }
+ mGestureState = eGestureState_MagnifyGesture;
+
+ NSPoint locationInWindow =
+ nsCocoaUtils::EventLocationForWindow(anEvent, [self window]);
+ ScreenPoint position = ViewAs<ScreenPixel>(
+ [self convertWindowCoordinatesRoundDown:locationInWindow],
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+ ExternalPoint screenOffset = ViewAs<ExternalPixel>(
+ mGeckoChild->WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ TimeStamp eventTimeStamp =
+ nsCocoaUtils::GetEventTimeStamp([anEvent timestamp]);
+ NSEventPhase eventPhase = [anEvent phase];
+ PinchGestureInput::PinchGestureType pinchGestureType;
+
+ switch (eventPhase) {
+ case NSEventPhaseBegan: {
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_START;
+ break;
+ }
+ case NSEventPhaseChanged: {
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
+ break;
+ }
+ case NSEventPhaseEnded: {
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_END;
+ mGestureState = eGestureState_None;
+ break;
+ }
+ default: {
+ NS_WARNING("Unexpected phase for pinch gesture event.");
+ return;
+ }
+ }
+
+ PinchGestureInput event{pinchGestureType,
+ PinchGestureInput::TRACKPAD,
+ eventTimeStamp,
+ screenOffset,
+ position,
+ 100.0,
+ 100.0 * (1.0 - [anEvent magnification]),
+ nsCocoaUtils::ModifiersForEvent(anEvent)};
+
+ mGeckoChild->DispatchAPZInputEvent(event);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Smart zoom gesture, i.e. two-finger double tap on trackpads.
+- (void)smartMagnifyWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild ||
+ [self beginOrEndGestureForEventPhase:anEvent]) {
+ return;
+ }
+
+ if ([self maybeRollup:anEvent]) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if (StaticPrefs::apz_mac_enable_double_tap_zoom_touchpad_gesture()) {
+ TimeStamp eventTimeStamp =
+ nsCocoaUtils::GetEventTimeStamp([anEvent timestamp]);
+ NSPoint locationInWindow =
+ nsCocoaUtils::EventLocationForWindow(anEvent, [self window]);
+ LayoutDevicePoint position =
+ [self convertWindowCoordinatesRoundDown:locationInWindow];
+
+ mGeckoChild->DispatchDoubleTapGesture(
+ eventTimeStamp, RoundedToInt(position),
+ nsCocoaUtils::ModifiersForEvent(anEvent));
+ } else {
+ // Setup the "double tap" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eTapGesture, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mClickCount = 1;
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+ }
+
+ // Clear the gesture state
+ mGestureState = eGestureState_None;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rotateWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild ||
+ [self beginOrEndGestureForEventPhase:anEvent]) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ float rotation = [anEvent rotation];
+
+ EventMessage msg;
+ switch (mGestureState) {
+ case eGestureState_StartGesture:
+ msg = eRotateGestureStart;
+ mGestureState = eGestureState_RotateGesture;
+ break;
+
+ case eGestureState_RotateGesture:
+ msg = eRotateGestureUpdate;
+ break;
+
+ case eGestureState_None:
+ case eGestureState_MagnifyGesture:
+ default:
+ return;
+ }
+
+ // Setup the event.
+ WidgetSimpleGestureEvent geckoEvent(true, msg, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mDelta = -rotation;
+ if (rotation > 0.0) {
+ geckoEvent.mDirection =
+ dom::SimpleGestureEvent_Binding::ROTATION_COUNTERCLOCKWISE;
+ } else {
+ geckoEvent.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_CLOCKWISE;
+ }
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+
+ // Keep track of the cumulative rotation for the final "rotate" event.
+ mCumulativeRotation += rotation;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// `beginGestureWithEvent` and `endGestureWithEvent` are not called for
+// applications that link against the macOS 10.11 or later SDK when we're
+// running on macOS 10.11 or later. For compatibility with all supported macOS
+// versions, we have to call {begin,end}GestureWithEvent ourselves based on
+// the event phase when we're handling gestures.
+- (bool)beginOrEndGestureForEventPhase:(NSEvent*)aEvent {
+ if (!aEvent) {
+ return false;
+ }
+
+ if (aEvent.phase == NSEventPhaseBegan) {
+ [self beginGestureWithEvent:aEvent];
+ return true;
+ }
+
+ if (aEvent.phase == NSEventPhaseEnded ||
+ aEvent.phase == NSEventPhaseCancelled) {
+ [self endGestureWithEvent:aEvent];
+ return true;
+ }
+
+ return false;
+}
+
+- (void)beginGestureWithEvent:(NSEvent*)aEvent {
+ if (!aEvent) {
+ return;
+ }
+
+ mGestureState = eGestureState_StartGesture;
+ mCumulativeRotation = 0.0;
+}
+
+- (void)endGestureWithEvent:(NSEvent*)anEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!anEvent || !mGeckoChild) {
+ // Clear the gestures state if we cannot send an event.
+ mGestureState = eGestureState_None;
+ mCumulativeRotation = 0.0;
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ switch (mGestureState) {
+ case eGestureState_RotateGesture: {
+ // Setup the "rotate" event.
+ WidgetSimpleGestureEvent geckoEvent(true, eRotateGesture, mGeckoChild);
+ [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mDelta = -mCumulativeRotation;
+ if (mCumulativeRotation > 0.0) {
+ geckoEvent.mDirection =
+ dom::SimpleGestureEvent_Binding::ROTATION_COUNTERCLOCKWISE;
+ } else {
+ geckoEvent.mDirection =
+ dom::SimpleGestureEvent_Binding::ROTATION_CLOCKWISE;
+ }
+
+ // Send the event.
+ mGeckoChild->DispatchWindowEvent(geckoEvent);
+ } break;
+
+ case eGestureState_MagnifyGesture: // APZ handles sending the widget events
+ case eGestureState_None:
+ case eGestureState_StartGesture:
+ default:
+ break;
+ }
+
+ // Clear the gestures state.
+ mGestureState = eGestureState_None;
+ mCumulativeRotation = 0.0;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)setUsingOMTCompositor:(BOOL)aUseOMTC {
+ mUsingOMTCompositor = aUseOMTC;
+}
+
+// Returning NO from this method only disallows ordering on mousedown - in order
+// to prevent it for mouseup too, we need to call [NSApp preventWindowOrdering]
+// when handling the mousedown event.
+- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)aEvent {
+ // Always using system-provided window ordering for normal windows.
+ if (![[self window] isKindOfClass:[PopupWindow class]]) return NO;
+
+ // Don't reorder when we don't have a parent window, like when we're a
+ // context menu or a tooltip.
+ return ![[self window] parentWindow];
+}
+
+- (void)mouseDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ mPerformedDrag = NO;
+
+ if ([self shouldDelayWindowOrderingForEvent:theEvent]) {
+ [NSApp preventWindowOrdering];
+ }
+
+ // If we've already seen this event due to direct dispatch from menuForEvent:
+ // just bail; if not, remember it.
+ if (mLastMouseDownEvent == theEvent) {
+ [mLastMouseDownEvent release];
+ mLastMouseDownEvent = nil;
+ return;
+ } else {
+ [mLastMouseDownEvent release];
+ mLastMouseDownEvent = [theEvent retain];
+ }
+
+ [gLastDragMouseDownEvent release];
+ gLastDragMouseDownEvent = [theEvent retain];
+ gLastDragView = self;
+
+ // We need isClickThrough because at this point the window we're in might
+ // already have become main, so the check for isMainWindow in
+ // WindowAcceptsEvent isn't enough. It also has to check isClickThrough.
+ BOOL isClickThrough = (theEvent == mClickThroughMouseDownEvent);
+ [mClickThroughMouseDownEvent release];
+ mClickThroughMouseDownEvent = nil;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if ([self maybeRollup:theEvent] ||
+ !ChildViewMouseTracker::WindowAcceptsEvent([self window], theEvent, self,
+ isClickThrough)) {
+ // Remember blocking because that means we want to block mouseup as well.
+ mBlockedLastMouseDown = YES;
+ return;
+ }
+
+ // in order to send gecko events we'll need a gecko widget
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseDown, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ NSInteger clickCount = [theEvent clickCount];
+ if (mBlockedLastMouseDown && clickCount > 1) {
+ // Don't send a double click if the first click of the double click was
+ // blocked.
+ clickCount--;
+ }
+ geckoEvent.mClickCount = clickCount;
+
+ if (!StaticPrefs::dom_event_treat_ctrl_click_as_right_click_disabled() &&
+ geckoEvent.IsControl()) {
+ geckoEvent.mButton = MouseButton::eSecondary;
+ } else {
+ geckoEvent.mButton = MouseButton::ePrimary;
+ // Don't send a click if ctrl key is pressed.
+ geckoEvent.mClickEventPrevented = geckoEvent.IsControl();
+ }
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ mBlockedLastMouseDown = NO;
+
+ // XXX maybe call markedTextSelectionChanged:client: here?
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)mouseUp:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ gLastDragView = nil;
+
+ if (!mGeckoChild || mBlockedLastMouseDown || mPerformedDrag) {
+ // There is case that mouseUp event will be fired right after DnD on OSX. As
+ // mPerformedDrag will be YES at end of DnD processing, ignore this mouseUp
+ // event fired right after DnD.
+ return;
+ }
+
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ if (!StaticPrefs::dom_event_treat_ctrl_click_as_right_click_disabled() &&
+ ([theEvent modifierFlags] & NSEventModifierFlagControl)) {
+ geckoEvent.mButton = MouseButton::eSecondary;
+ } else {
+ geckoEvent.mButton = MouseButton::ePrimary;
+ }
+
+ // Remember the event's position before calling DispatchInputEvent, because
+ // that call can mutate it and convert it into a different coordinate space.
+ LayoutDeviceIntPoint pos = geckoEvent.mRefPoint;
+
+ // This might destroy our widget (and null out mGeckoChild).
+ bool defaultPrevented =
+ (mGeckoChild->DispatchInputEvent(&geckoEvent).mContentStatus ==
+ nsEventStatus_eConsumeNoDefault);
+
+ if (!mGeckoChild) {
+ return;
+ }
+
+ // Check to see if we are double-clicking in draggable parts of the window.
+ if (!defaultPrevented && [theEvent clickCount] == 2 &&
+ !mGeckoChild->GetNonDraggableRegion().Contains(pos.x, pos.y)) {
+ if (nsCocoaUtils::ShouldZoomOnTitlebarDoubleClick()) {
+ [[self window] performZoom:nil];
+ } else if (nsCocoaUtils::ShouldMinimizeOnTitlebarDoubleClick()) {
+ [[self window] performMiniaturize:nil];
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)sendMouseEnterOrExitEvent:(NSEvent*)aEvent
+ enter:(BOOL)aEnter
+ exitFrom:(WidgetMouseEvent::ExitFrom)aExitFrom {
+ if (!mGeckoChild) return;
+
+ NSPoint windowEventLocation =
+ nsCocoaUtils::EventLocationForWindow(aEvent, [self window]);
+ NSPoint localEventLocation = [self convertPoint:windowEventLocation
+ fromView:nil];
+
+ EventMessage msg = aEnter ? eMouseEnterIntoWidget : eMouseExitFromWidget;
+ WidgetMouseEvent event(true, msg, mGeckoChild, WidgetMouseEvent::eReal);
+ event.mRefPoint = mGeckoChild->CocoaPointsToDevPixels(localEventLocation);
+ if (event.mMessage == eMouseExitFromWidget) {
+ event.mExitFrom = Some(aExitFrom);
+ }
+ nsEventStatus status; // ignored
+ mGeckoChild->DispatchEvent(&event, status);
+}
+
+- (void)handleMouseMoved:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)mouseDragged:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+
+ // Note, sending the above event might have destroyed our widget since we
+ // didn't retain. Fine so long as we don't access any local variables from
+ // here on.
+
+ // XXX maybe call markedTextSelectionChanged:client: here?
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rightMouseDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ mPerformedDrag = NO;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ [self maybeRollup:theEvent];
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ // The right mouse went down, fire off a right mouse down event to gecko
+ WidgetMouseEvent geckoEvent(true, eMouseDown, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = MouseButton::eSecondary;
+ geckoEvent.mClickCount = [theEvent clickCount];
+
+ nsIWidget::ContentAndAPZEventStatus eventStatus =
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return;
+
+ if (!StaticPrefs::ui_context_menus_after_mouseup() &&
+ eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
+ // Let the superclass do the context menu stuff.
+ [super rightMouseDown:theEvent];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rightMouseUp:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = MouseButton::eSecondary;
+ geckoEvent.mClickCount = [theEvent clickCount];
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ nsIWidget::ContentAndAPZEventStatus eventStatus =
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return;
+
+ if (StaticPrefs::ui_context_menus_after_mouseup() &&
+ eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
+ // Let the superclass do the context menu stuff, but pretend it's
+ // rightMouseDown.
+ NSEvent* dupeEvent = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
+ location:theEvent.locationInWindow
+ modifierFlags:theEvent.modifierFlags
+ timestamp:theEvent.timestamp
+ windowNumber:theEvent.windowNumber
+ context:nil
+ eventNumber:theEvent.eventNumber
+ clickCount:theEvent.clickCount
+ pressure:theEvent.pressure];
+
+ [super rightMouseDown:dupeEvent];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)rightMouseDragged:(NSEvent*)theEvent {
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = MouseButton::eSecondary;
+
+ // send event into Gecko by going directly to the
+ // the widget.
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+}
+
+static bool ShouldDispatchBackForwardCommandForMouseButton(int16_t aButton) {
+ return (aButton == MouseButton::eX1 &&
+ Preferences::GetBool("mousebutton.4th.enabled", true)) ||
+ (aButton == MouseButton::eX2 &&
+ Preferences::GetBool("mousebutton.5th.enabled", true));
+}
+
+- (void)otherMouseDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ mPerformedDrag = NO;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if ([self maybeRollup:theEvent] ||
+ !ChildViewMouseTracker::WindowAcceptsEvent([self window], theEvent, self))
+ return;
+
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ int16_t button = nsCocoaUtils::ButtonForEvent(theEvent);
+ if (ShouldDispatchBackForwardCommandForMouseButton(button)) {
+ WidgetCommandEvent appCommandEvent(
+ true,
+ (button == MouseButton::eX2) ? nsGkAtoms::Forward : nsGkAtoms::Back,
+ mGeckoChild);
+ mGeckoChild->DispatchWindowEvent(appCommandEvent);
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseDown, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = button;
+ geckoEvent.mClickCount = [theEvent clickCount];
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)otherMouseUp:(NSEvent*)theEvent {
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ int16_t button = nsCocoaUtils::ButtonForEvent(theEvent);
+ if (ShouldDispatchBackForwardCommandForMouseButton(button)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseUp, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ geckoEvent.mButton = button;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+}
+
+- (void)otherMouseDragged:(NSEvent*)theEvent {
+ if (!mGeckoChild) return;
+ if (mTextInputHandler->OnHandleEvent(theEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eMouseMove, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ int16_t button = nsCocoaUtils::ButtonForEvent(theEvent);
+ geckoEvent.mButton = button;
+
+ // send event into Gecko by going directly to the
+ // the widget.
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+}
+
+- (void)sendWheelStartOrStop:(EventMessage)msg forEvent:(NSEvent*)theEvent {
+ WidgetWheelEvent wheelEvent(true, msg, mGeckoChild);
+ [self convertCocoaMouseWheelEvent:theEvent toGeckoEvent:&wheelEvent];
+ mExpectingWheelStop = (msg == eWheelOperationStart);
+ mGeckoChild->DispatchInputEvent(wheelEvent.AsInputEvent());
+}
+
+- (void)sendWheelCondition:(BOOL)condition
+ first:(EventMessage)first
+ second:(EventMessage)second
+ forEvent:(NSEvent*)theEvent {
+ if (mExpectingWheelStop == condition) {
+ [self sendWheelStartOrStop:first forEvent:theEvent];
+ }
+ [self sendWheelStartOrStop:second forEvent:theEvent];
+}
+
+static int32_t RoundUp(double aDouble) {
+ return aDouble < 0 ? static_cast<int32_t>(floor(aDouble))
+ : static_cast<int32_t>(ceil(aDouble));
+}
+
+static gfx::IntPoint GetIntegerDeltaForEvent(NSEvent* aEvent) {
+ if ([aEvent hasPreciseScrollingDeltas]) {
+ // Pixel scroll events (events with hasPreciseScrollingDeltas == YES)
+ // carry pixel deltas in the scrollingDeltaX/Y fields and line scroll
+ // information in the deltaX/Y fields.
+ // Prior to 10.12, these line scroll fields would be zero for most pixel
+ // scroll events and non-zero for some, whenever at least a full line
+ // worth of pixel scrolling had accumulated. That's the behavior we want.
+ // Starting with 10.12 however, pixel scroll events no longer accumulate
+ // deltaX and deltaY; they just report floating point values for every
+ // single event. So we need to do our own accumulation.
+ return PanGestureInput::GetIntegerDeltaForEvent(
+ [aEvent phase] == NSEventPhaseBegan, [aEvent deltaX], [aEvent deltaY]);
+ }
+
+ // For line scrolls, or pre-10.12, just use the rounded up value of deltaX /
+ // deltaY.
+ return gfx::IntPoint(RoundUp([aEvent deltaX]), RoundUp([aEvent deltaY]));
+}
+
+- (void)scrollWheel:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ ChildViewMouseTracker::MouseScrolled(theEvent);
+
+ if ([self maybeRollup:theEvent]) {
+ return;
+ }
+
+ if (!mGeckoChild) {
+ return;
+ }
+
+ NSEventPhase phase = [theEvent phase];
+ // Fire eWheelOperationStart/End events when 2 fingers touch/release the
+ // touchpad.
+ if (phase & NSEventPhaseMayBegin) {
+ [self sendWheelCondition:YES
+ first:eWheelOperationEnd
+ second:eWheelOperationStart
+ forEvent:theEvent];
+ } else if (phase & (NSEventPhaseEnded | NSEventPhaseCancelled)) {
+ [self sendWheelCondition:NO
+ first:eWheelOperationStart
+ second:eWheelOperationEnd
+ forEvent:theEvent];
+ }
+
+ if (!mGeckoChild) {
+ return;
+ }
+ RefPtr<nsChildView> geckoChildDeathGrip(mGeckoChild);
+
+ NSPoint locationInWindow =
+ nsCocoaUtils::EventLocationForWindow(theEvent, [self window]);
+
+ // Use convertWindowCoordinatesRoundDown when converting the position to
+ // integer screen pixels in order to ensure that coordinates which are just
+ // inside the right / bottom edges of the window don't end up outside of the
+ // window after rounding.
+ ScreenPoint position = ViewAs<ScreenPixel>(
+ [self convertWindowCoordinatesRoundDown:locationInWindow],
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ bool usePreciseDeltas =
+ [theEvent hasPreciseScrollingDeltas] &&
+ Preferences::GetBool("mousewheel.enable_pixel_scrolling", true);
+ bool hasPhaseInformation = nsCocoaUtils::EventHasPhaseInformation(theEvent);
+
+ gfx::IntPoint lineOrPageDelta = -GetIntegerDeltaForEvent(theEvent);
+
+ Modifiers modifiers = nsCocoaUtils::ModifiersForEvent(theEvent);
+
+ TimeStamp eventTimeStamp =
+ nsCocoaUtils::GetEventTimeStamp([theEvent timestamp]);
+
+ ScreenPoint preciseDelta;
+ if (usePreciseDeltas) {
+ CGFloat pixelDeltaX = [theEvent scrollingDeltaX];
+ CGFloat pixelDeltaY = [theEvent scrollingDeltaY];
+ double scale = geckoChildDeathGrip->BackingScaleFactor();
+ preciseDelta = ScreenPoint(-pixelDeltaX * scale, -pixelDeltaY * scale);
+ }
+
+ if (usePreciseDeltas && hasPhaseInformation) {
+ PanGestureInput panEvent = nsCocoaUtils::CreatePanGestureEvent(
+ theEvent, eventTimeStamp, position, preciseDelta, lineOrPageDelta,
+ modifiers);
+
+ geckoChildDeathGrip->DispatchAPZWheelInputEvent(panEvent);
+ } else if (usePreciseDeltas) {
+ // This is on 10.6 or old touchpads that don't have any phase information.
+ ScrollWheelInput wheelEvent(eventTimeStamp, modifiers,
+ ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, position,
+ preciseDelta.x, preciseDelta.y, false,
+ // This parameter is used for wheel delta
+ // adjustment, such as auto-dir scrolling,
+ // but we do't need to do anything special here
+ // since this wheel event is sent to
+ // DispatchAPZWheelInputEvent, which turns this
+ // ScrollWheelInput back into a WidgetWheelEvent
+ // and then it goes through the regular handling
+ // in APZInputBridge. So passing |eNone| won't
+ // pass up the necessary wheel delta adjustment.
+ WheelDeltaAdjustmentStrategy::eNone);
+ wheelEvent.mLineOrPageDeltaX = lineOrPageDelta.x;
+ wheelEvent.mLineOrPageDeltaY = lineOrPageDelta.y;
+ wheelEvent.mIsMomentum = nsCocoaUtils::IsMomentumScrollEvent(theEvent);
+ geckoChildDeathGrip->DispatchAPZWheelInputEvent(wheelEvent);
+ } else {
+ ScrollWheelInput::ScrollMode scrollMode =
+ ScrollWheelInput::SCROLLMODE_INSTANT;
+ if (StaticPrefs::general_smoothScroll() &&
+ StaticPrefs::general_smoothScroll_mouseWheel()) {
+ scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
+ }
+ ScrollWheelInput wheelEvent(eventTimeStamp, modifiers, scrollMode,
+ ScrollWheelInput::SCROLLDELTA_LINE, position,
+ lineOrPageDelta.x, lineOrPageDelta.y, false,
+ // This parameter is used for wheel delta
+ // adjustment, such as auto-dir scrolling,
+ // but we do't need to do anything special here
+ // since this wheel event is sent to
+ // DispatchAPZWheelInputEvent, which turns this
+ // ScrollWheelInput back into a WidgetWheelEvent
+ // and then it goes through the regular handling
+ // in APZInputBridge. So passing |eNone| won't
+ // pass up the necessary wheel delta adjustment.
+ WheelDeltaAdjustmentStrategy::eNone);
+ wheelEvent.mLineOrPageDeltaX = lineOrPageDelta.x;
+ wheelEvent.mLineOrPageDeltaY = lineOrPageDelta.y;
+ geckoChildDeathGrip->DispatchAPZWheelInputEvent(wheelEvent);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSMenu*)menuForEvent:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mGeckoChild) return nil;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ [self maybeRollup:theEvent];
+ if (!mGeckoChild) return nil;
+
+ // Cocoa doesn't always dispatch a mouseDown: for a control-click event,
+ // depends on what we return from menuForEvent:. Gecko always expects one
+ // and expects the mouse down event before the context menu event, so
+ // get that event sent first if this is a left mouse click.
+ if ([theEvent type] == NSEventTypeLeftMouseDown) {
+ [self mouseDown:theEvent];
+ if (!mGeckoChild) return nil;
+ }
+
+ WidgetMouseEvent geckoEvent(true, eContextMenu, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+ if (StaticPrefs::dom_event_treat_ctrl_click_as_right_click_disabled() &&
+ [theEvent type] == NSEventTypeLeftMouseDown) {
+ geckoEvent.mContextMenuTrigger = WidgetMouseEvent::eControlClick;
+ geckoEvent.mButton = MouseButton::ePrimary;
+ } else {
+ geckoEvent.mButton = MouseButton::eSecondary;
+ }
+
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return nil;
+
+ [self maybeInitContextMenuTracking];
+
+ // We never return an actual NSMenu* for the context menu. Gecko might have
+ // responded to the eContextMenu event by putting up a fake context menu.
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)willOpenMenu:(NSMenu*)aMenu withEvent:(NSEvent*)aEvent {
+ ChildViewMouseTracker::NativeMenuOpened();
+}
+
+- (void)didCloseMenu:(NSMenu*)aMenu withEvent:(NSEvent*)aEvent {
+ ChildViewMouseTracker::NativeMenuClosed();
+}
+
+- (void)convertCocoaMouseWheelEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetWheelEvent*)outWheelEvent {
+ [self convertCocoaMouseEvent:aMouseEvent toGeckoEvent:outWheelEvent];
+
+ bool usePreciseDeltas =
+ [aMouseEvent hasPreciseScrollingDeltas] &&
+ Preferences::GetBool("mousewheel.enable_pixel_scrolling", true);
+
+ outWheelEvent->mDeltaMode = usePreciseDeltas
+ ? dom::WheelEvent_Binding::DOM_DELTA_PIXEL
+ : dom::WheelEvent_Binding::DOM_DELTA_LINE;
+ outWheelEvent->mIsMomentum = nsCocoaUtils::IsMomentumScrollEvent(aMouseEvent);
+}
+
+- (void)convertCocoaMouseEvent:(NSEvent*)aMouseEvent
+ toGeckoEvent:(WidgetInputEvent*)outGeckoEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ASSERTION(
+ outGeckoEvent,
+ "convertCocoaMouseEvent:toGeckoEvent: requires non-null aoutGeckoEvent");
+ if (!outGeckoEvent) return;
+
+ nsCocoaUtils::InitInputEvent(*outGeckoEvent, aMouseEvent);
+
+ // convert point to view coordinate system
+ NSPoint locationInWindow =
+ nsCocoaUtils::EventLocationForWindow(aMouseEvent, [self window]);
+
+ outGeckoEvent->mRefPoint = [self convertWindowCoordinates:locationInWindow];
+
+ WidgetMouseEventBase* mouseEvent = outGeckoEvent->AsMouseEventBase();
+ mouseEvent->mButtons = 0;
+ NSUInteger mouseButtons = [NSEvent pressedMouseButtons];
+
+ if (mouseButtons & 0x01) {
+ mouseEvent->mButtons |= MouseButtonsFlag::ePrimaryFlag;
+ }
+ if (mouseButtons & 0x02) {
+ mouseEvent->mButtons |= MouseButtonsFlag::eSecondaryFlag;
+ }
+ if (mouseButtons & 0x04) {
+ mouseEvent->mButtons |= MouseButtonsFlag::eMiddleFlag;
+ }
+ if (mouseButtons & 0x08) {
+ mouseEvent->mButtons |= MouseButtonsFlag::e4thFlag;
+ }
+ if (mouseButtons & 0x10) {
+ mouseEvent->mButtons |= MouseButtonsFlag::e5thFlag;
+ }
+
+ switch ([aMouseEvent type]) {
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseUp:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseUp:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseUp:
+ case NSEventTypeOtherMouseDragged:
+ case NSEventTypeMouseMoved:
+ if ([aMouseEvent subtype] == NSEventSubtypeTabletPoint) {
+ [self convertCocoaTabletPointerEvent:aMouseEvent
+ toGeckoEvent:mouseEvent->AsMouseEvent()];
+ }
+ break;
+
+ default:
+ // Don't check other NSEvents for pressure.
+ break;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)convertCocoaTabletPointerEvent:(NSEvent*)aPointerEvent
+ toGeckoEvent:(WidgetMouseEvent*)aOutGeckoEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+ if (!aOutGeckoEvent || !sIsTabletPointerActivated) {
+ return;
+ }
+ if ([aPointerEvent type] != NSEventTypeMouseMoved) {
+ aOutGeckoEvent->mPressure = [aPointerEvent pressure];
+ MOZ_ASSERT(aOutGeckoEvent->mPressure >= 0.0 &&
+ aOutGeckoEvent->mPressure <= 1.0);
+ }
+ aOutGeckoEvent->mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_PEN;
+ aOutGeckoEvent->tiltX = (int32_t)lround([aPointerEvent tilt].x * 90);
+ aOutGeckoEvent->tiltY = (int32_t)lround([aPointerEvent tilt].y * 90);
+ aOutGeckoEvent->tangentialPressure = [aPointerEvent tangentialPressure];
+ // Make sure the twist value is in the range of 0-359.
+ int32_t twist = (int32_t)fmod([aPointerEvent rotation], 360);
+ aOutGeckoEvent->twist = twist >= 0 ? twist : twist + 360;
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)tabletProximity:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+ sIsTabletPointerActivated = [theEvent isEnteringProximity];
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+#pragma mark -
+// NSTextInputClient implementation
+
+- (NSRange)markedRange {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, NSMakeRange(NSNotFound, 0));
+ return mTextInputHandler->MarkedRange();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakeRange(0, 0));
+}
+
+- (NSRange)selectedRange {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, NSMakeRange(NSNotFound, 0));
+ return mTextInputHandler->SelectedRange();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakeRange(0, 0));
+}
+
+- (BOOL)drawsVerticallyForCharacterAtIndex:(NSUInteger)charIndex {
+ NS_ENSURE_TRUE(mTextInputHandler, NO);
+ if (charIndex == NSNotFound) {
+ return NO;
+ }
+ return mTextInputHandler->DrawsVerticallyForCharacterAtIndex(charIndex);
+}
+
+- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint {
+ NS_ENSURE_TRUE(mTextInputHandler, 0);
+ return mTextInputHandler->CharacterIndexForPoint(thePoint);
+}
+
+- (NSArray*)validAttributesForMarkedText {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, [NSArray array]);
+ return mTextInputHandler->GetValidAttributesForMarkedText();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE_VOID(mGeckoChild);
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ NSString* str;
+ if ([aString isKindOfClass:[NSAttributedString class]]) {
+ str = [aString string];
+ } else {
+ str = aString;
+ }
+
+ mTextInputHandler->InsertText(str, &replacementRange);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)doCommandBySelector:(SEL)aSelector {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild || !mTextInputHandler) {
+ return;
+ }
+
+ const char* sel = reinterpret_cast<const char*>(aSelector);
+ if (!mTextInputHandler->DoCommandBySelector(sel)) {
+ [super doCommandBySelector:aSelector];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)unmarkText {
+ NS_ENSURE_TRUE_VOID(mTextInputHandler);
+ mTextInputHandler->CommitIMEComposition();
+}
+
+- (BOOL)hasMarkedText {
+ NS_ENSURE_TRUE(mTextInputHandler, NO);
+ return mTextInputHandler->HasMarkedText();
+}
+
+- (void)setMarkedText:(id)aString
+ selectedRange:(NSRange)selectedRange
+ replacementRange:(NSRange)replacementRange {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE_VOID(mTextInputHandler);
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ NSAttributedString* attrStr;
+ if ([aString isKindOfClass:[NSAttributedString class]]) {
+ attrStr = static_cast<NSAttributedString*>(aString);
+ } else {
+ attrStr = [[[NSAttributedString alloc] initWithString:aString] autorelease];
+ }
+
+ mTextInputHandler->SetMarkedText(attrStr, selectedRange, &replacementRange);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)aRange
+ actualRange:
+ (NSRangePointer)actualRange {
+ NS_ENSURE_TRUE(mTextInputHandler, nil);
+ return mTextInputHandler->GetAttributedSubstringFromRange(aRange,
+ actualRange);
+}
+
+- (NSRect)firstRectForCharacterRange:(NSRange)aRange
+ actualRange:(NSRangePointer)actualRange {
+ NS_ENSURE_TRUE(mTextInputHandler, NSMakeRect(0.0, 0.0, 0.0, 0.0));
+ return mTextInputHandler->FirstRectForCharacterRange(aRange, actualRange);
+}
+
+- (void)quickLookWithEvent:(NSEvent*)event {
+ // Show dictionary by current point
+ WidgetContentCommandEvent contentCommandEvent(
+ true, eContentCommandLookUpDictionary, mGeckoChild);
+ NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
+ contentCommandEvent.mRefPoint = mGeckoChild->CocoaPointsToDevPixels(point);
+ mGeckoChild->DispatchWindowEvent(contentCommandEvent);
+ // The widget might have been destroyed.
+}
+
+- (NSInteger)windowLevel {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_TRUE(mTextInputHandler, [[self window] level]);
+ return mTextInputHandler->GetWindowLevel();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSNormalWindowLevel);
+}
+
+#pragma mark -
+
+// This is a private API that Cocoa uses.
+// Cocoa will call this after the menu system returns "NO" for
+// "performKeyEquivalent:". We want all they key events we can get so just
+// return YES. In particular, this fixes ctrl-tab - we don't get a "keyDown:"
+// call for that without this.
+- (BOOL)_wantsKeyDownForEvent:(NSEvent*)event {
+ return YES;
+}
+
+- (NSEvent*)lastKeyDownEvent {
+ return mLastKeyDownEvent;
+}
+
+- (void)keyDown:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mLastKeyDownEvent release];
+ mLastKeyDownEvent = [theEvent retain];
+
+ // Weird things can happen on keyboard input if the key window isn't in the
+ // current space. For example see bug 1056251. To get around this, always
+ // make sure that, if our window is key, it's also made frontmost. Doing
+ // this automatically switches to whatever space our window is in. Safari
+ // does something similar. Our window should normally always be key --
+ // otherwise why is the OS sending us a key down event? But it's just
+ // possible we're in Gecko's hidden window, so we check first.
+ NSWindow* viewWindow = [self window];
+ if (viewWindow && [viewWindow isKeyWindow]) {
+ [viewWindow orderWindow:NSWindowAbove relativeTo:0];
+ }
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ if (!Preferences::GetBool("intl.allow-insecure-text-input", false) &&
+ mGeckoChild && mTextInputHandler && mTextInputHandler->IsFocused()) {
+ NSWindow* window = [self window];
+ NSString* info = [NSString
+ stringWithFormat:@"\nview [%@], window [%@], window is key %i, is "
+ @"fullscreen %i, app is active %i",
+ self, window, [window isKeyWindow],
+ ([window styleMask] & NSWindowStyleMaskFullScreen) !=
+ 0,
+ [NSApp isActive]];
+ nsAutoCString additionalInfo([info UTF8String]);
+
+ if (mGeckoChild->GetInputContext().IsPasswordEditor() &&
+ !TextInputHandler::IsSecureEventInputEnabled()) {
+# define CRASH_MESSAGE \
+ "A password editor has focus, but not in secure input mode"
+
+ CrashReporter::AppendAppNotesToCrashReport(
+ "\nBug 893973: "_ns + nsLiteralCString(CRASH_MESSAGE));
+ CrashReporter::AppendAppNotesToCrashReport(additionalInfo);
+
+ MOZ_CRASH(CRASH_MESSAGE);
+# undef CRASH_MESSAGE
+ } else if (!mGeckoChild->GetInputContext().IsPasswordEditor() &&
+ TextInputHandler::IsSecureEventInputEnabled()) {
+# define CRASH_MESSAGE \
+ "A non-password editor has focus, but in secure input mode"
+
+ CrashReporter::AppendAppNotesToCrashReport(
+ "\nBug 893973: "_ns + nsLiteralCString(CRASH_MESSAGE));
+ CrashReporter::AppendAppNotesToCrashReport(additionalInfo);
+
+ MOZ_CRASH(CRASH_MESSAGE);
+# undef CRASH_MESSAGE
+ }
+ }
+#endif // #if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ if (mGeckoChild) {
+ if (mTextInputHandler) {
+ sUniqueKeyEventId++;
+ NSMutableDictionary* nativeKeyEventsMap = [ChildView sNativeKeyEventsMap];
+ [nativeKeyEventsMap setObject:theEvent forKey:@(sUniqueKeyEventId)];
+ // Purge old native events, in case we're still holding on to them. We
+ // keep at most 10 references to 10 different native events.
+ [nativeKeyEventsMap removeObjectForKey:@(sUniqueKeyEventId - 10)];
+ mTextInputHandler->HandleKeyDownEvent(theEvent, sUniqueKeyEventId);
+ } else {
+ // There was no text input handler. Offer the event to the native menu
+ // system to check if there are any registered custom shortcuts for this
+ // event.
+ mGeckoChild->SendEventToNativeMenuSystem(theEvent);
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)keyUp:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE(mGeckoChild, );
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ mTextInputHandler->HandleKeyUpEvent(theEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)insertNewline:(id)sender {
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertParagraph);
+ }
+}
+
+- (void)insertLineBreak:(id)sender {
+ // Ctrl + Enter in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertLineBreak);
+ }
+}
+
+- (void)deleteBackward:(id)sender {
+ // Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteCharBackward);
+ }
+}
+
+- (void)deleteBackwardByDecomposingPreviousCharacter:(id)sender {
+ // Ctrl + Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteCharBackward);
+ }
+}
+
+- (void)deleteWordBackward:(id)sender {
+ // Alt + Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteWordBackward);
+ }
+}
+
+- (void)deleteToBeginningOfBackward:(id)sender {
+ // Command + Backspace in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteToBeginningOfLine);
+ }
+}
+
+- (void)deleteForward:(id)sender {
+ // Delete in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteCharForward);
+ }
+}
+
+- (void)deleteWordForward:(id)sender {
+ // Alt + Delete in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::DeleteWordForward);
+ }
+}
+
+- (void)insertTab:(id)sender {
+ // Tab in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertTab);
+ }
+}
+
+- (void)insertBacktab:(id)sender {
+ // Shift + Tab in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::InsertBacktab);
+ }
+}
+
+- (void)moveRight:(id)sender {
+ // RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::CharNext);
+ }
+}
+
+- (void)moveRightAndModifySelection:(id)sender {
+ // Shift + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectCharNext);
+ }
+}
+
+- (void)moveWordRight:(id)sender {
+ // Alt + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::WordNext);
+ }
+}
+
+- (void)moveWordRightAndModifySelection:(id)sender {
+ // Alt + Shift + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectWordNext);
+ }
+}
+
+- (void)moveToRightEndOfLine:(id)sender {
+ // Command + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::EndLine);
+ }
+}
+
+- (void)moveToRightEndOfLineAndModifySelection:(id)sender {
+ // Command + Shift + RightArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectEndLine);
+ }
+}
+
+- (void)moveLeft:(id)sender {
+ // LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::CharPrevious);
+ }
+}
+
+- (void)moveLeftAndModifySelection:(id)sender {
+ // Shift + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectCharPrevious);
+ }
+}
+
+- (void)moveWordLeft:(id)sender {
+ // Alt + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::WordPrevious);
+ }
+}
+
+- (void)moveWordLeftAndModifySelection:(id)sender {
+ // Alt + Shift + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectWordPrevious);
+ }
+}
+
+- (void)moveToLeftEndOfLine:(id)sender {
+ // Command + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::BeginLine);
+ }
+}
+
+- (void)moveToLeftEndOfLineAndModifySelection:(id)sender {
+ // Command + Shift + LeftArrow in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectBeginLine);
+ }
+}
+
+- (void)moveUp:(id)sender {
+ // ArrowUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::LinePrevious);
+ }
+}
+
+- (void)moveUpAndModifySelection:(id)sender {
+ // Shift + ArrowUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectLinePrevious);
+ }
+}
+
+- (void)moveToBeginningOfDocument:(id)sender {
+ // Command + ArrowUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::MoveTop);
+ }
+}
+
+- (void)moveToBeginningOfDocumentAndModifySelection:(id)sender {
+ // Command + Shift + ArrowUp or Shift + Home in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectTop);
+ }
+}
+
+- (void)moveDown:(id)sender {
+ // ArrowDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::LineNext);
+ }
+}
+
+- (void)moveDownAndModifySelection:(id)sender {
+ // Shift + ArrowDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectLineNext);
+ }
+}
+
+- (void)moveToEndOfDocument:(id)sender {
+ // Command + ArrowDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::MoveBottom);
+ }
+}
+
+- (void)moveToEndOfDocumentAndModifySelection:(id)sender {
+ // Command + Shift + ArrowDown or Shift + End in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectBottom);
+ }
+}
+
+- (void)scrollPageUp:(id)sender {
+ // PageUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollPageUp);
+ }
+}
+
+- (void)pageUpAndModifySelection:(id)sender {
+ // Shift + PageUp in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectPageUp);
+ }
+}
+
+- (void)scrollPageDown:(id)sender {
+ // PageDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollPageDown);
+ }
+}
+
+- (void)pageDownAndModifySelection:(id)sender {
+ // Shift + PageDown in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::SelectPageDown);
+ }
+}
+
+- (void)scrollToEndOfDocument:(id)sender {
+ // End in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollBottom);
+ }
+}
+
+- (void)scrollToBeginningOfDocument:(id)sender {
+ // Home in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::ScrollTop);
+ }
+}
+
+// XXX Don't decleare nor implement calcelOperation: because it
+// causes not calling keyDown: for Command + Period.
+// We need to handle it from doCommandBySelector:.
+
+- (void)complete:(id)sender {
+ // Alt + Escape or Alt + Shift + Escape in the default settings.
+ if (mTextInputHandler) {
+ mTextInputHandler->HandleCommand(Command::Complete);
+ }
+}
+
+- (void)flagsChanged:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NS_ENSURE_TRUE(mGeckoChild, );
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ mTextInputHandler->HandleFlagsChanged(theEvent);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)isFirstResponder {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSResponder* resp = [[self window] firstResponder];
+ return (resp == (NSResponder*)self);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (BOOL)isDragInProgress {
+ if (!mDragService) return NO;
+
+ nsCOMPtr<nsIDragSession> dragSession;
+ mDragService->GetCurrentSession(getter_AddRefs(dragSession));
+ return dragSession != nullptr;
+}
+
+- (BOOL)inactiveWindowAcceptsMouseEvent:(NSEvent*)aEvent {
+ // If we're being destroyed assume the default -- return YES.
+ if (!mGeckoChild) return YES;
+
+ WidgetMouseEvent geckoEvent(true, eMouseActivate, mGeckoChild,
+ WidgetMouseEvent::eReal);
+ [self convertCocoaMouseEvent:aEvent toGeckoEvent:&geckoEvent];
+ return (mGeckoChild->DispatchInputEvent(&geckoEvent).mContentStatus !=
+ nsEventStatus_eConsumeNoDefault);
+}
+
+// We must always call through to our superclass, even when mGeckoChild is
+// nil -- otherwise the keyboard focus can end up in the wrong NSView.
+- (BOOL)becomeFirstResponder {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [super becomeFirstResponder];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(YES);
+}
+
+- (void)viewsWindowDidBecomeKey {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mGeckoChild) return;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ // check to see if the window implements the mozWindow protocol. This
+ // allows embedders to avoid re-entrant calls to -makeKeyAndOrderFront,
+ // which can happen because these activate calls propagate out
+ // to the embedder via nsIEmbeddingSiteWindow::SetFocus().
+ BOOL isMozWindow =
+ [[self window] respondsToSelector:@selector(setSuppressMakeKeyFront:)];
+ if (isMozWindow) [[self window] setSuppressMakeKeyFront:YES];
+
+ nsIWidgetListener* listener = mGeckoChild->GetWidgetListener();
+ if (listener) listener->WindowActivated();
+
+ if (isMozWindow) [[self window] setSuppressMakeKeyFront:NO];
+
+ if (mGeckoChild->GetInputContext().IsPasswordEditor()) {
+ TextInputHandler::EnableSecureEventInput();
+ } else {
+ TextInputHandler::EnsureSecureEventInputDisabled();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)viewsWindowDidResignKey {
+ if (!mGeckoChild) return;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ nsIWidgetListener* listener = mGeckoChild->GetWidgetListener();
+ if (listener) listener->WindowDeactivated();
+
+ TextInputHandler::EnsureSecureEventInputDisabled();
+}
+
+// If the call to removeFromSuperview isn't delayed from nsChildView::
+// TearDownView(), the NSView hierarchy might get changed during calls to
+// [ChildView drawRect:], which leads to "beyond bounds" exceptions in
+// NSCFArray. For more info see bmo bug 373122. Apple's docs claim that
+// removeFromSuperviewWithoutNeedingDisplay "can be safely invoked during
+// display" (whatever "display" means). But it's _not_ true that it can be
+// safely invoked during calls to [NSView drawRect:]. We use
+// removeFromSuperview here because there's no longer any danger of being
+// "invoked during display", and because doing do clears up bmo bug 384343.
+- (void)delayedTearDown {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self removeFromSuperview];
+ [self release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+// drag'n'drop stuff
+#define kDragServiceContractID "@mozilla.org/widget/dragservice;1"
+
+- (NSDragOperation)dragOperationFromDragAction:(int32_t)aDragAction {
+ if (nsIDragService::DRAGDROP_ACTION_LINK & aDragAction)
+ return NSDragOperationLink;
+ if (nsIDragService::DRAGDROP_ACTION_COPY & aDragAction)
+ return NSDragOperationCopy;
+ if (nsIDragService::DRAGDROP_ACTION_MOVE & aDragAction)
+ return NSDragOperationGeneric;
+ return NSDragOperationNone;
+}
+
+- (LayoutDeviceIntPoint)convertWindowCoordinates:(NSPoint)aPoint {
+ if (!mGeckoChild) {
+ return LayoutDeviceIntPoint(0, 0);
+ }
+
+ NSPoint localPoint = [self convertPoint:aPoint fromView:nil];
+ return mGeckoChild->CocoaPointsToDevPixels(localPoint);
+}
+
+- (LayoutDeviceIntPoint)convertWindowCoordinatesRoundDown:(NSPoint)aPoint {
+ if (!mGeckoChild) {
+ return LayoutDeviceIntPoint(0, 0);
+ }
+
+ NSPoint localPoint = [self convertPoint:aPoint fromView:nil];
+ return mGeckoChild->CocoaPointsToDevPixelsRoundDown(localPoint);
+}
+
+// This is a utility function used by NSView drag event methods
+// to send events. It contains all of the logic needed for Gecko
+// dragging to work. Returns the appropriate cocoa drag operation code.
+- (NSDragOperation)doDragAction:(EventMessage)aMessage sender:(id)aSender {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mGeckoChild) return NSDragOperationNone;
+
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView doDragAction: entered\n"));
+
+ if (!mDragService) {
+ CallGetService(kDragServiceContractID, &mDragService);
+ NS_ASSERTION(mDragService, "Couldn't get a drag service - big problem!");
+ if (!mDragService) return NSDragOperationNone;
+ }
+
+ if (aMessage == eDragEnter) {
+ mDragService->StartDragSession();
+ }
+
+ nsCOMPtr<nsIDragSession> dragSession;
+ mDragService->GetCurrentSession(getter_AddRefs(dragSession));
+ if (dragSession) {
+ if (aMessage == eDragOver) {
+ // fire the drag event at the source. Just ignore whether it was
+ // cancelled or not as there isn't actually a means to stop the drag
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->FireDragEventAtSource(
+ eDrag, nsCocoaUtils::ModifiersForEvent([NSApp currentEvent]));
+ dragSession->SetCanDrop(false);
+ } else if (aMessage == eDrop) {
+ // We make the assumption that the dragOver handlers have correctly set
+ // the |canDrop| property of the Drag Session.
+ bool canDrop = false;
+ if (!NS_SUCCEEDED(dragSession->GetCanDrop(&canDrop)) || !canDrop) {
+ [self doDragAction:eDragExit sender:aSender];
+
+ nsCOMPtr<nsINode> sourceNode;
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode));
+ if (!sourceNode) {
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->EndDragSession(
+ false, nsCocoaUtils::ModifiersForEvent([NSApp currentEvent]));
+ }
+ return NSDragOperationNone;
+ }
+ }
+
+ unsigned int modifierFlags = [[NSApp currentEvent] modifierFlags];
+ uint32_t action = nsIDragService::DRAGDROP_ACTION_MOVE;
+ // force copy = option, alias = cmd-option, default is move
+ if (modifierFlags & NSEventModifierFlagOption) {
+ if (modifierFlags & NSEventModifierFlagCommand)
+ action = nsIDragService::DRAGDROP_ACTION_LINK;
+ else
+ action = nsIDragService::DRAGDROP_ACTION_COPY;
+ }
+ dragSession->SetDragAction(action);
+ }
+
+ // set up gecko event
+ WidgetDragEvent geckoEvent(true, aMessage, mGeckoChild);
+ nsCocoaUtils::InitInputEvent(geckoEvent, [NSApp currentEvent]);
+
+ // Use our own coordinates in the gecko event.
+ // Convert event from gecko global coords to gecko view coords.
+ NSPoint draggingLoc = [aSender draggingLocation];
+
+ geckoEvent.mRefPoint = [self convertWindowCoordinates:draggingLoc];
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ mGeckoChild->DispatchInputEvent(&geckoEvent);
+ if (!mGeckoChild) return NSDragOperationNone;
+
+ if (dragSession) {
+ switch (aMessage) {
+ case eDragEnter:
+ case eDragOver: {
+ uint32_t dragAction;
+ dragSession->GetDragAction(&dragAction);
+
+ // If TakeChildProcessDragAction returns something other than
+ // DRAGDROP_ACTION_UNINITIALIZED, it means that the last event was sent
+ // to the child process and this event is also being sent to the child
+ // process. In this case, use the last event's action instead.
+ nsDragService* dragService = static_cast<nsDragService*>(mDragService);
+ int32_t childDragAction = dragService->TakeChildProcessDragAction();
+ if (childDragAction != nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) {
+ dragAction = childDragAction;
+ }
+
+ return [self dragOperationFromDragAction:dragAction];
+ }
+ case eDragExit:
+ case eDrop: {
+ nsCOMPtr<nsINode> sourceNode;
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode));
+ if (!sourceNode) {
+ // We're leaving a window while doing a drag that was
+ // initiated in a different app. End the drag session,
+ // since we're done with it for now (until the user
+ // drags back into mozilla).
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->EndDragSession(
+ false, nsCocoaUtils::ModifiersForEvent([NSApp currentEvent]));
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ return NSDragOperationGeneric;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSDragOperationNone);
+}
+
+// NSDraggingDestination
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView draggingEntered: entered\n"));
+
+ // there should never be a globalDragPboard when "draggingEntered:" is
+ // called, but just in case we'll take care of it here.
+ [globalDragPboard release];
+
+ // Set the global drag pasteboard that will be used for this drag session.
+ // This will be set back to nil when the drag session ends (mouse exits
+ // the view or a drop happens within the view).
+ globalDragPboard = [[sender draggingPasteboard] retain];
+
+ return [self doDragAction:eDragEnter sender:sender];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSDragOperationNone);
+}
+
+// NSDraggingDestination
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView draggingUpdated: entered\n"));
+ return [self doDragAction:eDragOver sender:sender];
+}
+
+// NSDraggingDestination
+- (void)draggingExited:(id<NSDraggingInfo>)sender {
+ MOZ_LOG(sCocoaLog, LogLevel::Info, ("ChildView draggingExited: entered\n"));
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ [self doDragAction:eDragExit sender:sender];
+ NS_IF_RELEASE(mDragService);
+}
+
+// NSDraggingDestination
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ BOOL handled = [self doDragAction:eDrop sender:sender] != NSDragOperationNone;
+ NS_IF_RELEASE(mDragService);
+ return handled;
+}
+
+// NSDraggingSource
+// This is just implemented so we comply with the NSDraggingSource protocol.
+- (NSDragOperation)draggingSession:(NSDraggingSession*)session
+ sourceOperationMaskForDraggingContext:(NSDraggingContext)context {
+ return UINT_MAX;
+}
+
+// NSDraggingSource
+- (BOOL)ignoreModifierKeysForDraggingSession:(NSDraggingSession*)session {
+ return YES;
+}
+
+// NSDraggingSource
+- (void)draggingSession:(NSDraggingSession*)aSession
+ endedAtPoint:(NSPoint)aPoint
+ operation:(NSDragOperation)aOperation {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+#ifdef NIGHTLY_BUILD
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#endif
+
+ gDraggedTransferables = nullptr;
+
+ NSEvent* currentEvent = [NSApp currentEvent];
+ gUserCancelledDrag = ([currentEvent type] == NSEventTypeKeyDown &&
+ [currentEvent keyCode] == kVK_Escape);
+
+ if (!mDragService) {
+ CallGetService(kDragServiceContractID, &mDragService);
+ NS_ASSERTION(mDragService, "Couldn't get a drag service - big problem!");
+ }
+
+ if (mDragService) {
+ RefPtr<nsDragService> dragService =
+ static_cast<nsDragService*>(mDragService);
+
+ // Set the dragend point from the current mouse location
+ // FIXME(emilio): Weird that we wouldn't use aPoint instead? Seems to work
+ // locally as well...
+ // NSPoint pnt = aPoint;
+ NSPoint pnt = [NSEvent mouseLocation];
+ NSPoint locationInWindow =
+ nsCocoaUtils::ConvertPointFromScreen([self window], pnt);
+ FlipCocoaScreenCoordinate(pnt);
+ dragService->SetDragEndPoint(
+ [self convertWindowCoordinates:locationInWindow]);
+
+ // XXX: dropEffect should be updated per |aOperation|.
+ // As things stand though, |aOperation| isn't well handled within "our"
+ // events, that is, when the drop happens within the window: it is set
+ // either to NSDragOperationGeneric or to NSDragOperationNone.
+ // For that reason, it's not yet possible to override dropEffect per the
+ // given OS value, and it's also unclear what's the correct dropEffect
+ // value for NSDragOperationGeneric that is passed by other applications.
+ // All that said, NSDragOperationNone is still reliable.
+ if (aOperation == NSDragOperationNone) {
+ RefPtr<dom::DataTransfer> dataTransfer = dragService->GetDataTransfer();
+ if (dataTransfer) {
+ dataTransfer->SetDropEffectInt(nsIDragService::DRAGDROP_ACTION_NONE);
+ }
+ }
+
+ dragService->EndDragSession(true,
+ nsCocoaUtils::ModifiersForEvent(currentEvent));
+ NS_RELEASE(mDragService);
+ }
+
+ [globalDragPboard release];
+ globalDragPboard = nil;
+ [gLastDragMouseDownEvent release];
+ gLastDragMouseDownEvent = nil;
+ mPerformedDrag = YES;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// NSDraggingSource
+- (void)draggingSession:(NSDraggingSession*)aSession
+ movedToPoint:(NSPoint)aPoint {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Get the drag service if it isn't already cached. The drag service
+ // isn't cached when dragging over a different application.
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ if (!dragService) {
+ dragService = do_GetService(kDragServiceContractID);
+ }
+
+ if (dragService) {
+ nsDragService* ds = static_cast<nsDragService*>(dragService.get());
+ ds->DragMovedWithView(aSession, aPoint);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// NSDraggingSource
+- (void)draggingSession:(NSDraggingSession*)aSession
+ willBeginAtPoint:(NSPoint)aPoint {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // there should never be a globalDragPboard when "willBeginAtPoint:" is
+ // called, but just in case we'll take care of it here.
+ [globalDragPboard release];
+
+ // Set the global drag pasteboard that will be used for this drag session.
+ // This will be set back to nil when the drag session ends (mouse exits
+ // the view or a drop happens within the view).
+ globalDragPboard = [[aSession draggingPasteboard] retain];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Get the paste location from the low level pasteboard.
+static CFTypeRefPtr<CFURLRef> GetPasteLocation(NSPasteboard* aPasteboard) {
+ PasteboardRef pboardRef = nullptr;
+ PasteboardCreate((CFStringRef)[aPasteboard name], &pboardRef);
+ if (!pboardRef) {
+ return nullptr;
+ }
+
+ auto pasteBoard = CFTypeRefPtr<PasteboardRef>::WrapUnderCreateRule(pboardRef);
+ PasteboardSynchronize(pasteBoard.get());
+
+ CFURLRef urlRef = nullptr;
+ PasteboardCopyPasteLocation(pasteBoard.get(), &urlRef);
+ return CFTypeRefPtr<CFURLRef>::WrapUnderCreateRule(urlRef);
+}
+
+// NSPasteboardItemDataProvider
+- (void)pasteboard:(NSPasteboard*)aPasteboard
+ item:(NSPasteboardItem*)aItem
+ provideDataForType:(NSString*)aType {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+#ifdef NIGHTLY_BUILD
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#endif
+
+ if (!gDraggedTransferables) {
+ return;
+ }
+
+ uint32_t count = 0;
+ gDraggedTransferables->GetLength(&count);
+
+ for (uint32_t j = 0; j < count; j++) {
+ nsCOMPtr<nsITransferable> currentTransferable =
+ do_QueryElementAt(gDraggedTransferables, j);
+ if (!currentTransferable) {
+ return;
+ }
+
+ // Transform the transferable to an NSDictionary.
+ NSDictionary* pasteboardOutputDict =
+ nsClipboard::PasteboardDictFromTransferable(currentTransferable);
+ if (!pasteboardOutputDict) {
+ return;
+ }
+
+ // Write everything out to the pasteboard.
+ unsigned int typeCount = [pasteboardOutputDict count];
+ NSMutableArray* types = [NSMutableArray arrayWithCapacity:typeCount + 1];
+ [types addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ [types addObject:[UTIHelper stringFromPboardType:kMozWildcardPboardType]];
+ for (unsigned int k = 0; k < typeCount; k++) {
+ NSString* curType = [types objectAtIndex:k];
+ if ([curType isEqualToString:[UTIHelper stringFromPboardType:
+ NSPasteboardTypeString]] ||
+ [curType
+ isEqualToString:[UTIHelper
+ stringFromPboardType:kPublicUrlPboardType]] ||
+ [curType isEqualToString:[UTIHelper stringFromPboardType:
+ kPublicUrlNamePboardType]] ||
+ [curType
+ isEqualToString:[UTIHelper
+ stringFromPboardType:(NSString*)
+ kUTTypeFileURL]]) {
+ [aPasteboard setString:[pasteboardOutputDict valueForKey:curType]
+ forType:curType];
+ } else if ([curType isEqualToString:[UTIHelper
+ stringFromPboardType:
+ kUrlsWithTitlesPboardType]]) {
+ [aPasteboard setPropertyList:[pasteboardOutputDict valueForKey:curType]
+ forType:curType];
+ } else if ([curType
+ isEqualToString:[UTIHelper stringFromPboardType:
+ NSPasteboardTypeHTML]]) {
+ [aPasteboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(
+ [pasteboardOutputDict valueForKey:curType]))
+ forType:curType];
+ } else if ([curType
+ isEqualToString:[UTIHelper stringFromPboardType:
+ NSPasteboardTypeTIFF]] ||
+ [curType isEqualToString:[UTIHelper
+ stringFromPboardType:
+ kMozCustomTypesPboardType]]) {
+ [aPasteboard setData:[pasteboardOutputDict valueForKey:curType]
+ forType:curType];
+ } else if ([curType
+ isEqualToString:[UTIHelper stringFromPboardType:
+ kMozFileUrlsPboardType]]) {
+ [aPasteboard writeObjects:[pasteboardOutputDict valueForKey:curType]];
+ } else if ([curType
+ isEqualToString:
+ [UTIHelper
+ stringFromPboardType:
+ (NSString*)kPasteboardTypeFileURLPromise]]) {
+ nsCOMPtr<nsIFile> targFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(targFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(targFile);
+ if (!macLocalFile) {
+ NS_ERROR("No Mac local file");
+ continue;
+ }
+
+ CFTypeRefPtr<CFURLRef> url = GetPasteLocation(aPasteboard);
+ if (!url) {
+ continue;
+ }
+
+ if (!NS_SUCCEEDED(macLocalFile->InitWithCFURL(url.get()))) {
+ NS_ERROR("failed InitWithCFURL");
+ continue;
+ }
+
+ if (!gDraggedTransferables) {
+ continue;
+ }
+
+ uint32_t transferableCount;
+ nsresult rv = gDraggedTransferables->GetLength(&transferableCount);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ for (uint32_t i = 0; i < transferableCount; i++) {
+ nsCOMPtr<nsITransferable> item =
+ do_QueryElementAt(gDraggedTransferables, i);
+ if (!item) {
+ NS_ERROR("no transferable");
+ continue;
+ }
+
+ item->SetTransferData(kFilePromiseDirectoryMime, macLocalFile);
+
+ // Now request the kFilePromiseMime data, which will invoke the data
+ // provider. If successful, the file will have been created.
+ nsCOMPtr<nsISupports> fileDataPrimitive;
+ Unused << item->GetTransferData(kFilePromiseMime,
+ getter_AddRefs(fileDataPrimitive));
+ }
+
+ [aPasteboard setPropertyList:[pasteboardOutputDict valueForKey:curType]
+ forType:curType];
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark -
+
+// Support for the "Services" menu. We currently only support sending strings
+// and HTML to system services.
+// This method can be called on any thread (see bug 1751687). We can only
+// usefully handle it on the main thread.
+- (id)validRequestorForSendType:(NSString*)sendType
+ returnType:(NSString*)returnType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!NS_IsMainThread()) {
+ // We don't have any thread-safe ways of checking whether we can send
+ // or receive content. Just say no. In normal cases, we expect this
+ // method to be called on the main thread.
+ return [super validRequestorForSendType:sendType returnType:returnType];
+ }
+
+ // sendType contains the type of data that the service would like this
+ // application to send to it. sendType is nil if the service is not
+ // requesting any data.
+ //
+ // returnType contains the type of data the the service would like to
+ // return to this application (e.g., to overwrite the selection).
+ // returnType is nil if the service will not return any data.
+ //
+ // The following condition thus triggers when the service expects a string
+ // or HTML from us or no data at all AND when the service will either not
+ // send back any data to us or will send a string or HTML back to us.
+
+ id result = nil;
+
+ NSString* stringType =
+ [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ NSString* htmlType = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
+ if ((!sendType || [sendType isEqualToString:stringType] ||
+ [sendType isEqualToString:htmlType]) &&
+ (!returnType || [returnType isEqualToString:stringType] ||
+ [returnType isEqualToString:htmlType])) {
+ if (mGeckoChild) {
+ // Assume that this object will be able to handle this request.
+ result = self;
+
+ // Keep the ChildView alive during this operation.
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ if (sendType) {
+ // Determine if there is a current selection (chrome/content).
+ if (!nsClipboard::sSelectionCache) {
+ result = nil;
+ }
+ }
+
+ // Determine if we can paste (if receiving data from the service).
+ if (mGeckoChild && returnType) {
+ WidgetContentCommandEvent command(
+ true, eContentCommandPasteTransferable, mGeckoChild, true);
+ // This might possibly destroy our widget (and null out mGeckoChild).
+ mGeckoChild->DispatchWindowEvent(command);
+ if (!mGeckoChild || !command.mSucceeded || !command.mIsEnabled)
+ result = nil;
+ }
+ }
+ }
+
+ // Give the superclass a chance if this object will not handle this request.
+ if (!result)
+ result = [super validRequestorForSendType:sendType returnType:returnType];
+
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)writeSelectionToPasteboard:(NSPasteboard*)pboard types:(NSArray*)types {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+
+ // Make sure that the service will accept strings or HTML.
+ if (![types
+ containsObject:[UTIHelper stringFromPboardType:NSStringPboardType]] &&
+ ![types
+ containsObject:[UTIHelper
+ stringFromPboardType:NSPasteboardTypeString]] &&
+ ![types containsObject:[UTIHelper
+ stringFromPboardType:NSPasteboardTypeHTML]]) {
+ return NO;
+ }
+
+ // Bail out if there is no Gecko object.
+ if (!mGeckoChild) return NO;
+
+ // Transform the transferable to an NSDictionary.
+ NSDictionary* pasteboardOutputDict = nullptr;
+
+ pasteboardOutputDict =
+ nsClipboard::PasteboardDictFromTransferable(nsClipboard::sSelectionCache);
+
+ if (!pasteboardOutputDict) return NO;
+
+ // Declare the pasteboard types.
+ unsigned int typeCount = [pasteboardOutputDict count];
+ NSMutableArray* declaredTypes = [NSMutableArray arrayWithCapacity:typeCount];
+ [declaredTypes addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ [pboard declareTypes:declaredTypes owner:nil];
+
+ // Write the data to the pasteboard.
+ for (unsigned int i = 0; i < typeCount; i++) {
+ NSString* currentKey = [declaredTypes objectAtIndex:i];
+ id currentValue = [pasteboardOutputDict valueForKey:currentKey];
+
+ if ([currentKey
+ isEqualToString:[UTIHelper
+ stringFromPboardType:NSPasteboardTypeString]] ||
+ [currentKey
+ isEqualToString:[UTIHelper
+ stringFromPboardType:kPublicUrlPboardType]] ||
+ [currentKey isEqualToString:[UTIHelper stringFromPboardType:
+ kPublicUrlNamePboardType]]) {
+ [pboard setString:currentValue forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:
+ [UTIHelper stringFromPboardType:NSPasteboardTypeHTML]]) {
+ [pboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue))
+ forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:
+ [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF]]) {
+ [pboard setData:currentValue forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:
+ [UTIHelper
+ stringFromPboardType:
+ (NSString*)kPasteboardTypeFileURLPromise]] ||
+ [currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:
+ kUrlsWithTitlesPboardType]]) {
+ [pboard setPropertyList:currentValue forType:currentKey];
+ }
+ }
+ return YES;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+// Called if the service wants us to replace the current selection.
+- (BOOL)readSelectionFromPasteboard:(NSPasteboard*)pboard {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> trans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
+ if (NS_FAILED(rv)) return NO;
+ trans->Init(nullptr);
+
+ trans->AddDataFlavor(kTextMime);
+ trans->AddDataFlavor(kHTMLMime);
+
+ rv = nsClipboard::TransferableFromPasteboard(trans, pboard);
+ if (NS_FAILED(rv)) return NO;
+
+ NS_ENSURE_TRUE(mGeckoChild, false);
+
+ WidgetContentCommandEvent command(true, eContentCommandPasteTransferable,
+ mGeckoChild);
+ command.mTransferable = trans;
+ mGeckoChild->DispatchWindowEvent(command);
+
+ return command.mSucceeded && command.mIsEnabled;
+}
+
+- (void)pressureChangeWithEvent:(NSEvent*)event {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ NSInteger stage = [event stage];
+ if (mLastPressureStage == 1 && stage == 2) {
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ if ([userDefaults integerForKey:@"com.apple.trackpad.forceClick"] == 1) {
+ // This is no public API to get configuration for current force click.
+ // This is filed as radar 29294285.
+ [self quickLookWithEvent:event];
+ }
+ }
+ mLastPressureStage = stage;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+nsresult nsChildView::GetSelectionAsPlaintext(nsAString& aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!nsClipboard::sSelectionCache) {
+ MOZ_ASSERT(aResult.IsEmpty());
+ return NS_OK;
+ }
+
+ // Get the current chrome or content selection.
+ NSDictionary* pasteboardOutputDict = nullptr;
+ pasteboardOutputDict =
+ nsClipboard::PasteboardDictFromTransferable(nsClipboard::sSelectionCache);
+
+ if (NS_WARN_IF(!pasteboardOutputDict)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Declare the pasteboard types.
+ unsigned int typeCount = [pasteboardOutputDict count];
+ NSMutableArray* declaredTypes = [NSMutableArray arrayWithCapacity:typeCount];
+ [declaredTypes addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ NSString* currentKey = [declaredTypes objectAtIndex:0];
+ NSString* currentValue = [pasteboardOutputDict valueForKey:currentKey];
+ const char* textSelection = [currentValue UTF8String];
+ aResult = NS_ConvertUTF8toUTF16(textSelection);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+#ifdef DEBUG
+nsresult nsChildView::SetHiDPIMode(bool aHiDPI) {
+ nsCocoaUtils::InvalidateHiDPIState();
+ Preferences::SetInt("gfx.hidpi.enabled", aHiDPI ? 1 : 0);
+ BackingScaleFactorChanged();
+ return NS_OK;
+}
+
+nsresult nsChildView::RestoreHiDPIMode() {
+ nsCocoaUtils::InvalidateHiDPIState();
+ Preferences::ClearUser("gfx.hidpi.enabled");
+ BackingScaleFactorChanged();
+ return NS_OK;
+}
+#endif
+
+#pragma mark -
+
+#ifdef ACCESSIBILITY
+
+/* Every ChildView has a corresponding mozDocAccessible object that is doing all
+ the heavy lifting. The topmost ChildView corresponds to a mozRootAccessible
+ object.
+
+ All ChildView needs to do is to route all accessibility calls (from the
+ NSAccessibility APIs) down to its object, pretending that they are the same.
+*/
+- (id<mozAccessible>)accessible {
+ if (!mGeckoChild) return nil;
+
+ id<mozAccessible> nativeAccessible = nil;
+
+ nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ RefPtr<nsChildView> geckoChild(mGeckoChild);
+ RefPtr<a11y::LocalAccessible> accessible =
+ geckoChild->GetDocumentAccessible();
+ if (!accessible) return nil;
+
+ accessible->GetNativeInterface((void**)&nativeAccessible);
+
+# ifdef DEBUG_hakan
+ NSAssert(![nativeAccessible isExpired], @"native acc is expired!!!");
+# endif
+
+ return nativeAccessible;
+}
+
+/* Implementation of formal mozAccessible formal protocol (enabling mozViews
+ to talk to mozAccessible objects in the accessibility module). */
+
+- (BOOL)hasRepresentedView {
+ return YES;
+}
+
+- (id)representedView {
+ return self;
+}
+
+- (BOOL)isRoot {
+ return [[self accessible] isRoot];
+}
+
+# pragma mark -
+
+// general
+
+- (BOOL)isAccessibilityElement {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super isAccessibilityElement];
+
+ return [[self accessible] isAccessibilityElement];
+}
+
+- (id)accessibilityHitTest:(NSPoint)point {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityHitTest:point];
+
+ return [[self accessible] accessibilityHitTest:point];
+}
+
+- (id)accessibilityFocusedUIElement {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityFocusedUIElement];
+
+ return [[self accessible] accessibilityFocusedUIElement];
+}
+
+// actions
+
+- (NSArray*)accessibilityActionNames {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityActionNames];
+
+ return [[self accessible] accessibilityActionNames];
+}
+
+- (NSString*)accessibilityActionDescription:(NSString*)action {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityActionDescription:action];
+
+ return [[self accessible] accessibilityActionDescription:action];
+}
+
+- (void)accessibilityPerformAction:(NSString*)action {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityPerformAction:action];
+
+ return [[self accessible] accessibilityPerformAction:action];
+}
+
+// attributes
+
+- (NSArray*)accessibilityAttributeNames {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityAttributeNames];
+
+ return [[self accessible] accessibilityAttributeNames];
+}
+
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityIsAttributeSettable:attribute];
+
+ return [[self accessible] accessibilityIsAttributeSettable:attribute];
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mozilla::a11y::ShouldA11yBeEnabled())
+ return [super accessibilityAttributeValue:attribute];
+
+ id<mozAccessible> accessible = [self accessible];
+
+ // if we're the root (topmost) accessible, we need to return our native
+ // AXParent as we traverse outside to the hierarchy of whoever embeds us.
+ // thus, fall back on NSView's default implementation for this attribute.
+ if ([attribute isEqualToString:NSAccessibilityParentAttribute] &&
+ [accessible isRoot]) {
+ id parentAccessible = [super accessibilityAttributeValue:attribute];
+ return parentAccessible;
+ }
+
+ return [accessible accessibilityAttributeValue:attribute];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+#endif /* ACCESSIBILITY */
+
++ (uint32_t)sUniqueKeyEventId {
+ return sUniqueKeyEventId;
+}
+
++ (NSMutableDictionary*)sNativeKeyEventsMap {
+ // This dictionary is "leaked".
+ static NSMutableDictionary* sNativeKeyEventsMap =
+ [[NSMutableDictionary alloc] init];
+ return sNativeKeyEventsMap;
+}
+
+@end
+
+@implementation PixelHostingView
+
+- (id)initWithFrame:(NSRect)aRect {
+ self = [super initWithFrame:aRect];
+
+ self.wantsLayer = YES;
+ self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
+
+ return self;
+}
+
+- (BOOL)isFlipped {
+ return YES;
+}
+
+- (NSView*)hitTest:(NSPoint)aPoint {
+ return nil;
+}
+
+- (void)drawRect:(NSRect)aRect {
+ NS_WARNING("Unexpected call to drawRect: This view returns YES from "
+ "wantsUpdateLayer, so "
+ "drawRect should not be called.");
+}
+
+- (BOOL)wantsUpdateLayer {
+ return YES;
+}
+
+- (void)updateLayer {
+ [(ChildView*)[self superview] updateRootCALayer];
+}
+
+- (BOOL)wantsBestResolutionOpenGLSurface {
+ return nsCocoaUtils::HiDPIEnabled() ? YES : NO;
+}
+
+@end
+
+#pragma mark -
+
+void ChildViewMouseTracker::OnDestroyView(ChildView* aView) {
+ if (sLastMouseEventView == aView) {
+ sLastMouseEventView = nil;
+ [sLastMouseMoveEvent release];
+ sLastMouseMoveEvent = nil;
+ }
+}
+
+void ChildViewMouseTracker::OnDestroyWindow(NSWindow* aWindow) {
+ if (sWindowUnderMouse == aWindow) {
+ sWindowUnderMouse = nil;
+ }
+}
+
+void ChildViewMouseTracker::MouseEnteredWindow(NSEvent* aEvent) {
+ NSWindow* window = aEvent.window;
+ if (!window.ignoresMouseEvents) {
+ sWindowUnderMouse = window;
+ ReEvaluateMouseEnterState(aEvent);
+ }
+}
+
+void ChildViewMouseTracker::MouseExitedWindow(NSEvent* aEvent) {
+ if (sWindowUnderMouse == aEvent.window) {
+ sWindowUnderMouse = nil;
+ [sLastMouseMoveEvent release];
+ sLastMouseMoveEvent = nil;
+ ReEvaluateMouseEnterState(aEvent);
+ }
+}
+
+void ChildViewMouseTracker::NativeMenuOpened() {
+ // Send a mouse exit event now.
+ // The menu consumes all mouse events while it's open, and we don't want to be
+ // stuck thinking the mouse is still hovering our window after the mouse has
+ // already moved. This could result in unintended cursor changes or tooltips.
+ sWindowUnderMouse = nil;
+ ReEvaluateMouseEnterState(nil);
+}
+
+void ChildViewMouseTracker::NativeMenuClosed() {
+ // If a window was hovered before the menu opened, re-enter that window at the
+ // last known mouse position. After -[NSView didCloseMenu:withEvent:] is
+ // called, any NSTrackingArea updates that were buffered while the menu was
+ // open will be replayed.
+ if (sLastMouseMoveEvent) {
+ sWindowUnderMouse = sLastMouseMoveEvent.window;
+ ReEvaluateMouseEnterState(sLastMouseMoveEvent);
+ }
+}
+
+void ChildViewMouseTracker::ReEvaluateMouseEnterState(NSEvent* aEvent,
+ ChildView* aOldView) {
+ ChildView* oldView = aOldView ? aOldView : sLastMouseEventView;
+ sLastMouseEventView = ViewForEvent(aEvent);
+ if (sLastMouseEventView != oldView) {
+ // Send enter and / or exit events.
+ WidgetMouseEvent::ExitFrom exitFrom =
+ [sLastMouseEventView window] == [oldView window]
+ ? WidgetMouseEvent::ePlatformChild
+ : WidgetMouseEvent::ePlatformTopLevel;
+ [oldView sendMouseEnterOrExitEvent:aEvent enter:NO exitFrom:exitFrom];
+ // After the cursor exits the window set it to a visible regular arrow
+ // cursor.
+ if (exitFrom == WidgetMouseEvent::ePlatformTopLevel) {
+ [[nsCursorManager sharedInstance]
+ setNonCustomCursor:nsIWidget::Cursor{eCursor_standard}];
+ }
+ [sLastMouseEventView sendMouseEnterOrExitEvent:aEvent
+ enter:YES
+ exitFrom:exitFrom];
+ }
+}
+
+void ChildViewMouseTracker::ResendLastMouseMoveEvent() {
+ if (sLastMouseMoveEvent) {
+ MouseMoved(sLastMouseMoveEvent);
+ }
+}
+
+void ChildViewMouseTracker::MouseMoved(NSEvent* aEvent) {
+ MouseEnteredWindow(aEvent);
+ [sLastMouseEventView handleMouseMoved:aEvent];
+ if (sLastMouseMoveEvent != aEvent) {
+ [sLastMouseMoveEvent release];
+ sLastMouseMoveEvent = [aEvent retain];
+ }
+}
+
+void ChildViewMouseTracker::MouseScrolled(NSEvent* aEvent) {
+ if (!nsCocoaUtils::IsMomentumScrollEvent(aEvent)) {
+ // Store the position so we can pin future momentum scroll events.
+ sLastScrollEventScreenLocation =
+ nsCocoaUtils::ScreenLocationForEvent(aEvent);
+ }
+}
+
+ChildView* ChildViewMouseTracker::ViewForEvent(NSEvent* aEvent) {
+ NSWindow* window = sWindowUnderMouse;
+ if (!window) return nil;
+
+ NSPoint windowEventLocation =
+ nsCocoaUtils::EventLocationForWindow(aEvent, window);
+ NSView* view = [[[window contentView] superview] hitTest:windowEventLocation];
+
+ if (![view isKindOfClass:[ChildView class]]) return nil;
+
+ ChildView* childView = (ChildView*)view;
+ // If childView is being destroyed return nil.
+ if (![childView widget]) return nil;
+ return WindowAcceptsEvent(window, aEvent, childView) ? childView : nil;
+}
+
+BOOL ChildViewMouseTracker::WindowAcceptsEvent(NSWindow* aWindow,
+ NSEvent* aEvent,
+ ChildView* aView,
+ BOOL aIsClickThrough) {
+ // Right mouse down events may get through to all windows, even to a top level
+ // window with an open sheet.
+ if (!aWindow || [aEvent type] == NSEventTypeRightMouseDown) return YES;
+
+ id delegate = [aWindow delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) return YES;
+
+ nsIWidget* windowWidget = [(WindowDelegate*)delegate geckoWidget];
+ if (!windowWidget) return YES;
+
+ NSWindow* topLevelWindow = nil;
+
+ switch (windowWidget->GetWindowType()) {
+ case WindowType::Popup:
+ // If this is a context menu, it won't have a parent. So we'll always
+ // accept mouse move events on context menus even when none of our windows
+ // is active, which is the right thing to do.
+ // For panels, the parent window is the XUL window that owns the panel.
+ return WindowAcceptsEvent([aWindow parentWindow], aEvent, aView,
+ aIsClickThrough);
+
+ case WindowType::TopLevel:
+ case WindowType::Dialog:
+ if ([aWindow attachedSheet]) return NO;
+
+ topLevelWindow = aWindow;
+ break;
+ case WindowType::Sheet: {
+ nsIWidget* parentWidget = windowWidget->GetSheetWindowParent();
+ if (!parentWidget) return YES;
+
+ topLevelWindow = (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW);
+ break;
+ }
+
+ default:
+ return YES;
+ }
+
+ if (!topLevelWindow || ([topLevelWindow isMainWindow] && !aIsClickThrough) ||
+ [aEvent type] == NSEventTypeOtherMouseDown ||
+ (([aEvent modifierFlags] & NSEventModifierFlagCommand) != 0 &&
+ [aEvent type] != NSEventTypeMouseMoved))
+ return YES;
+
+ // If we're here then we're dealing with a left click or mouse move on an
+ // inactive window or something similar. Ask Gecko what to do.
+ return [aView inactiveWindowAcceptsMouseEvent:aEvent];
+}
+
+#pragma mark -
diff --git a/widget/cocoa/nsClipboard.h b/widget/cocoa/nsClipboard.h
new file mode 100644
index 0000000000..a9d5e79d96
--- /dev/null
+++ b/widget/cocoa/nsClipboard.h
@@ -0,0 +1,65 @@
+/* -*- 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/. */
+
+#ifndef nsClipboard_h_
+#define nsClipboard_h_
+
+#include "nsBaseClipboard.h"
+#include "nsCOMPtr.h"
+#include "nsIClipboard.h"
+#include "nsString.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPtr.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsITransferable;
+
+class nsClipboard : public nsBaseClipboard {
+ public:
+ nsClipboard();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // On macOS, cache the transferable of the current selection (chrome/content)
+ // in the parent process. This is needed for the services menu which
+ // requires synchronous access to the current selection.
+ static mozilla::StaticRefPtr<nsITransferable> sSelectionCache;
+ static int32_t sSelectionCacheChangeCount;
+
+ // Helper methods, used also by nsDragService
+ static NSDictionary* PasteboardDictFromTransferable(
+ nsITransferable* aTransferable);
+ // aPasteboardType is being retained and needs to be released by the caller.
+ static bool IsStringType(const nsCString& aMIMEType,
+ NSString** aPasteboardType);
+ static bool IsImageType(const nsACString& aMIMEType);
+ static NSString* WrapHtmlForSystemPasteboard(NSString* aString);
+ static nsresult TransferableFromPasteboard(nsITransferable* aTransferable,
+ NSPasteboard* pboard);
+
+ protected:
+ // Implement the native clipboard behavior.
+ NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) override;
+ mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber(
+ int32_t aWhichClipboard) override;
+ mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override;
+
+ void ClearSelectionCache();
+ void SetSelectionCache(nsITransferable* aTransferable);
+
+ private:
+ virtual ~nsClipboard();
+
+ static mozilla::Maybe<uint32_t> FindIndexOfImageFlavor(
+ const nsTArray<nsCString>& aMIMETypes);
+};
+
+#endif // nsClipboard_h_
diff --git a/widget/cocoa/nsClipboard.mm b/widget/cocoa/nsClipboard.mm
new file mode 100644
index 0000000000..ba747e5b27
--- /dev/null
+++ b/widget/cocoa/nsClipboard.mm
@@ -0,0 +1,875 @@
+/* -*- 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 <algorithm>
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Unused.h"
+
+#include "gfxPlatform.h"
+#include "nsArrayUtils.h"
+#include "nsCOMPtr.h"
+#include "nsClipboard.h"
+#include "nsString.h"
+#include "nsISupportsPrimitives.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsIFile.h"
+#include "nsStringStream.h"
+#include "nsEscape.h"
+#include "nsPrintfCString.h"
+#include "nsObjCExceptions.h"
+#include "imgIContainer.h"
+#include "nsCocoaUtils.h"
+
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::SourceSurface;
+
+mozilla::StaticRefPtr<nsITransferable> nsClipboard::sSelectionCache;
+int32_t nsClipboard::sSelectionCacheChangeCount = 0;
+
+nsClipboard::nsClipboard()
+ : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(
+ false /* supportsSelectionClipboard */,
+ true /* supportsFindClipboard */,
+ true /* supportsSelectionCache */)) {}
+
+nsClipboard::~nsClipboard() { ClearSelectionCache(); }
+
+NS_IMPL_ISUPPORTS_INHERITED0(nsClipboard, nsBaseClipboard)
+
+namespace {
+
+// We separate this into its own function because after an @try, all local
+// variables within that function get marked as volatile, and our C++ type
+// system doesn't like volatile things.
+static NSData* GetDataFromPasteboard(NSPasteboard* aPasteboard,
+ NSString* aType) {
+ NSData* data = nil;
+ @try {
+ data = [aPasteboard dataForType:aType];
+ } @catch (NSException* e) {
+ NS_WARNING(nsPrintfCString("Exception raised while getting data from the "
+ "pasteboard: \"%s - %s\"",
+ [[e name] UTF8String], [[e reason] UTF8String])
+ .get());
+ mozilla::Unused << e;
+ }
+ return data;
+}
+
+static NSPasteboard* GetPasteboard(int32_t aWhichClipboard) {
+ switch (aWhichClipboard) {
+ case nsIClipboard::kGlobalClipboard:
+ return [NSPasteboard generalPasteboard];
+ case nsIClipboard::kFindClipboard:
+ return [NSPasteboard pasteboardWithName:NSPasteboardNameFind];
+ default:
+ return nil;
+ }
+}
+
+} // namespace
+
+void nsClipboard::SetSelectionCache(nsITransferable* aTransferable) {
+ sSelectionCacheChangeCount++;
+ sSelectionCache = aTransferable;
+}
+
+void nsClipboard::ClearSelectionCache() { SetSelectionCache(nullptr); }
+
+NS_IMETHODIMP
+nsClipboard::SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aTransferable);
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (aWhichClipboard == kSelectionCache) {
+ SetSelectionCache(aTransferable);
+ return NS_OK;
+ }
+
+ NSDictionary* pasteboardOutputDict =
+ PasteboardDictFromTransferable(aTransferable);
+ if (!pasteboardOutputDict) return NS_ERROR_FAILURE;
+
+ unsigned int outputCount = [pasteboardOutputDict count];
+ NSArray* outputKeys = [pasteboardOutputDict allKeys];
+ NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
+ MOZ_ASSERT(cocoaPasteboard);
+ if (aWhichClipboard == kFindClipboard) {
+ NSString* stringType =
+ [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ [cocoaPasteboard declareTypes:[NSArray arrayWithObject:stringType]
+ owner:nil];
+ } else {
+ // Write everything else out to the general pasteboard.
+ MOZ_ASSERT(aWhichClipboard == kGlobalClipboard);
+ [cocoaPasteboard declareTypes:outputKeys owner:nil];
+ }
+
+ for (unsigned int i = 0; i < outputCount; i++) {
+ NSString* currentKey = [outputKeys objectAtIndex:i];
+ id currentValue = [pasteboardOutputDict valueForKey:currentKey];
+ if (aWhichClipboard == kFindClipboard) {
+ if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:
+ NSPasteboardTypeString]]) {
+ [cocoaPasteboard setString:currentValue forType:currentKey];
+ }
+ } else {
+ if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:
+ NSPasteboardTypeString]] ||
+ [currentKey
+ isEqualToString:[UTIHelper
+ stringFromPboardType:kPublicUrlPboardType]] ||
+ [currentKey
+ isEqualToString:
+ [UTIHelper stringFromPboardType:kPublicUrlNamePboardType]]) {
+ [cocoaPasteboard setString:currentValue forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:
+ [UTIHelper
+ stringFromPboardType:kUrlsWithTitlesPboardType]]) {
+ [cocoaPasteboard
+ setPropertyList:[pasteboardOutputDict valueForKey:currentKey]
+ forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:
+ NSPasteboardTypeHTML]]) {
+ [cocoaPasteboard
+ setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue))
+ forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:[UTIHelper stringFromPboardType:
+ kMozFileUrlsPboardType]]) {
+ [cocoaPasteboard writeObjects:currentValue];
+ } else if ([currentKey
+ isEqualToString:
+ [UTIHelper
+ stringFromPboardType:(NSString*)kUTTypeFileURL]]) {
+ [cocoaPasteboard setString:currentValue forType:currentKey];
+ } else if ([currentKey
+ isEqualToString:
+ [UTIHelper
+ stringFromPboardType:kPasteboardConcealedType]]) {
+ // It's fine to set the data to null for this field - this field is an
+ // addition to a value's other type and works like a flag.
+ [cocoaPasteboard setData:NULL forType:currentKey];
+ } else {
+ [cocoaPasteboard setData:currentValue forType:currentKey];
+ }
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsClipboard::TransferableFromPasteboard(
+ nsITransferable* aTransferable, NSPasteboard* cocoaPasteboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // get flavor list that includes all acceptable flavors (including ones
+ // obtained through conversion)
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ // printf("looking for clipboard data of type %s\n", flavorStr.get());
+
+ NSString* pboardType = nil;
+ if (nsClipboard::IsStringType(flavorStr, &pboardType)) {
+ NSString* pString = [cocoaPasteboard stringForType:pboardType];
+ if (!pString) {
+ continue;
+ }
+
+ NSData* stringData;
+ bool isRTF = [pboardType
+ isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeRTF]];
+ if (isRTF) {
+ stringData = [pString dataUsingEncoding:NSASCIIStringEncoding];
+ } else {
+ stringData = [pString dataUsingEncoding:NSUnicodeStringEncoding];
+ }
+ unsigned int dataLength = [stringData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ [stringData getBytes:clipboardDataPtr length:dataLength];
+
+ // The DOM only wants LF, so convert from MacOS line endings to DOM line
+ // endings.
+ int32_t signedDataLength = dataLength;
+ nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(
+ isRTF, &clipboardDataPtr, &signedDataLength);
+ dataLength = signedDataLength;
+
+ // skip BOM (Byte Order Mark to distinguish little or big endian)
+ char16_t* clipboardDataPtrNoBOM = (char16_t*)clipboardDataPtr;
+ if ((dataLength > 2) && ((clipboardDataPtrNoBOM[0] == 0xFEFF) ||
+ (clipboardDataPtrNoBOM[0] == 0xFFFE))) {
+ dataLength -= sizeof(char16_t);
+ clipboardDataPtrNoBOM += 1;
+ }
+
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ flavorStr, clipboardDataPtrNoBOM, dataLength,
+ getter_AddRefs(genericDataWrapper));
+ aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ break;
+ } else if (flavorStr.EqualsLiteral(kFileMime)) {
+ NSArray* items = [cocoaPasteboard pasteboardItems];
+ if (!items || [items count] <= 0) {
+ continue;
+ }
+
+ // XXX we don't support multiple clipboard item on DOM and XPCOM interface
+ // for now, so we only get the data from the first pasteboard item.
+ NSPasteboardItem* item = [items objectAtIndex:0];
+ if (!item) {
+ continue;
+ }
+
+ nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(aTransferable,
+ flavorStr, item);
+ } else if (flavorStr.EqualsLiteral(kCustomTypesMime)) {
+ NSString* type = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray
+ arrayWithObject:[UTIHelper stringFromPboardType:
+ kMozCustomTypesPboardType]]];
+ if (!type) {
+ continue;
+ }
+
+ NSData* pasteboardData = GetDataFromPasteboard(cocoaPasteboard, type);
+ if (!pasteboardData) {
+ continue;
+ }
+
+ unsigned int dataLength = [pasteboardData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ [pasteboardData getBytes:clipboardDataPtr length:dataLength];
+
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ flavorStr, clipboardDataPtr, dataLength,
+ getter_AddRefs(genericDataWrapper));
+
+ aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ } else if (flavorStr.EqualsLiteral(kJPEGImageMime) ||
+ flavorStr.EqualsLiteral(kJPGImageMime) ||
+ flavorStr.EqualsLiteral(kPNGImageMime) ||
+ flavorStr.EqualsLiteral(kGIFImageMime)) {
+ // Figure out if there's data on the pasteboard we can grab (sanity check)
+ NSString* type = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray
+ arrayWithObjects:
+ [UTIHelper
+ stringFromPboardType:(NSString*)kUTTypeFileURL],
+ [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF],
+ [UTIHelper stringFromPboardType:NSPasteboardTypePNG],
+ nil]];
+ if (!type) continue;
+
+ // Read data off the clipboard
+ NSData* pasteboardData = GetDataFromPasteboard(cocoaPasteboard, type);
+ if (!pasteboardData) continue;
+
+ // Figure out what type we're converting to
+ CFStringRef outputType = NULL;
+ if (flavorStr.EqualsLiteral(kJPEGImageMime) ||
+ flavorStr.EqualsLiteral(kJPGImageMime))
+ outputType = CFSTR("public.jpeg");
+ else if (flavorStr.EqualsLiteral(kPNGImageMime))
+ outputType = CFSTR("public.png");
+ else if (flavorStr.EqualsLiteral(kGIFImageMime))
+ outputType = CFSTR("com.compuserve.gif");
+ else
+ continue;
+
+ // Use ImageIO to interpret the data on the clipboard and transcode.
+ // Note that ImageIO, like all CF APIs, allows NULLs to propagate freely
+ // and safely in most cases (like ObjC). A notable exception is CFRelease.
+ NSDictionary* options = [NSDictionary
+ dictionaryWithObjectsAndKeys:(NSNumber*)kCFBooleanTrue,
+ kCGImageSourceShouldAllowFloat, type,
+ kCGImageSourceTypeIdentifierHint, nil];
+ CGImageSourceRef source = nullptr;
+ if (type == [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL]) {
+ NSString* urlStr = [cocoaPasteboard stringForType:type];
+ NSURL* url = [NSURL URLWithString:urlStr];
+ source =
+ CGImageSourceCreateWithURL((CFURLRef)url, (CFDictionaryRef)options);
+ } else {
+ source = CGImageSourceCreateWithData((CFDataRef)pasteboardData,
+ (CFDictionaryRef)options);
+ }
+
+ NSMutableData* encodedData = [NSMutableData data];
+ CGImageDestinationRef dest = CGImageDestinationCreateWithData(
+ (CFMutableDataRef)encodedData, outputType, 1, NULL);
+ CGImageDestinationAddImageFromSource(dest, source, 0, NULL);
+ bool successfullyConverted = CGImageDestinationFinalize(dest);
+
+ if (successfullyConverted) {
+ // Put the converted data in a form Gecko can understand
+ nsCOMPtr<nsIInputStream> byteStream;
+ NS_NewByteInputStream(getter_AddRefs(byteStream),
+ mozilla::Span((const char*)[encodedData bytes],
+ [encodedData length]),
+ NS_ASSIGNMENT_COPY);
+
+ aTransferable->SetTransferData(flavorStr.get(), byteStream);
+ }
+
+ if (dest) CFRelease(dest);
+ if (source) CFRelease(source);
+
+ if (successfullyConverted) {
+ // XXX Maybe try to fill in more types? Is there a point?
+ break;
+ } else {
+ continue;
+ }
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (kSelectionCache == aWhichClipboard) {
+ if (!sSelectionCache) {
+ return NS_OK;
+ }
+
+ // get flavor list that includes all acceptable flavors (including ones
+ // obtained through conversion)
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& flavor : flavors) {
+ nsCOMPtr<nsISupports> dataSupports;
+ rv = sSelectionCache->GetTransferData(flavor.get(),
+ getter_AddRefs(dataSupports));
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_CLIPBOARD_LOG("%s: getting %s from cache.", __FUNCTION__,
+ flavor.get());
+ aTransferable->SetTransferData(flavor.get(), dataSupports);
+ // XXX Maybe try to fill in more types? Is there a point?
+ break;
+ }
+ }
+ return NS_OK;
+ }
+
+ NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
+ if (!cocoaPasteboard) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return TransferableFromPasteboard(aTransferable, cocoaPasteboard);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// returns true if we have *any* of the passed in flavors available for pasting
+mozilla::Result<bool, nsresult>
+nsClipboard::HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (kSelectionCache == aWhichClipboard) {
+ nsTArray<nsCString> transferableFlavors;
+ if (sSelectionCache &&
+ NS_SUCCEEDED(sSelectionCache->FlavorsTransferableCanExport(
+ transferableFlavors))) {
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ MOZ_CLIPBOARD_LOG(" SelectionCache types (nums %zu)\n",
+ transferableFlavors.Length());
+ for (const auto& transferableFlavor : transferableFlavors) {
+ MOZ_CLIPBOARD_LOG(" MIME %s", transferableFlavor.get());
+ }
+ }
+
+ for (const auto& transferableFlavor : transferableFlavors) {
+ for (const auto& flavor : aFlavorList) {
+ if (transferableFlavor.Equals(flavor)) {
+ MOZ_CLIPBOARD_LOG(" has %s", flavor.get());
+ return true;
+ }
+ }
+ }
+ }
+
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ MOZ_CLIPBOARD_LOG(" no targets at clipboard (bad match)\n");
+ }
+
+ return false;
+ }
+
+ NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
+ MOZ_ASSERT(cocoaPasteboard);
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ NSArray* types = [cocoaPasteboard types];
+ uint32_t count = [types count];
+ MOZ_CLIPBOARD_LOG(" Pasteboard types (nums %d)\n", count);
+ for (uint32_t i = 0; i < count; i++) {
+ NSPasteboardType type = [types objectAtIndex:i];
+ if (!type) {
+ MOZ_CLIPBOARD_LOG(" failed to get MIME\n");
+ continue;
+ }
+ MOZ_CLIPBOARD_LOG(" MIME %s\n", [type UTF8String]);
+ }
+ }
+
+ for (auto& mimeType : aFlavorList) {
+ NSString* pboardType = nil;
+ if (nsClipboard::IsStringType(mimeType, &pboardType)) {
+ NSString* availableType = [cocoaPasteboard
+ availableTypeFromArray:[NSArray arrayWithObject:pboardType]];
+ if (availableType && [availableType isEqualToString:pboardType]) {
+ MOZ_CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ return true;
+ }
+ } else if (mimeType.EqualsLiteral(kCustomTypesMime)) {
+ NSString* availableType = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray
+ arrayWithObject:[UTIHelper stringFromPboardType:
+ kMozCustomTypesPboardType]]];
+ if (availableType) {
+ MOZ_CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ return true;
+ }
+ } else if (mimeType.EqualsLiteral(kJPEGImageMime) ||
+ mimeType.EqualsLiteral(kJPGImageMime) ||
+ mimeType.EqualsLiteral(kPNGImageMime) ||
+ mimeType.EqualsLiteral(kGIFImageMime)) {
+ NSString* availableType = [cocoaPasteboard
+ availableTypeFromArray:
+ [NSArray
+ arrayWithObjects:
+ [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF],
+ [UTIHelper stringFromPboardType:NSPasteboardTypePNG],
+ nil]];
+ if (availableType) {
+ MOZ_CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ return true;
+ }
+ } else if (mimeType.EqualsLiteral(kFileMime)) {
+ NSArray* items = [cocoaPasteboard pasteboardItems];
+ if (items && [items count] > 0) {
+ // XXX we only check the first pasteboard item as we only get data from
+ // first item in TransferableFromPasteboard for now.
+ if (NSPasteboardItem* item = [items objectAtIndex:0]) {
+ if (NSString *availableType = [item
+ availableTypeFromArray:
+ [NSArray
+ arrayWithObjects:[UTIHelper
+ stringFromPboardType:
+ (NSString*)kUTTypeFileURL],
+ nil]]) {
+ MOZ_CLIPBOARD_LOG(" has %s\n", mimeType.get());
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ MOZ_CLIPBOARD_LOG(" no targets at clipboard (bad match)\n");
+ }
+
+ return false;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(mozilla::Err(NS_ERROR_FAILURE));
+}
+
+// static
+mozilla::Maybe<uint32_t> nsClipboard::FindIndexOfImageFlavor(
+ const nsTArray<nsCString>& aMIMETypes) {
+ for (uint32_t i = 0; i < aMIMETypes.Length(); ++i) {
+ if (nsClipboard::IsImageType(aMIMETypes[i])) {
+ return mozilla::Some(i);
+ }
+ }
+
+ return mozilla::Nothing();
+}
+
+// This function converts anything that other applications might understand into
+// the system format and puts it into a dictionary which it returns. static
+NSDictionary* nsClipboard::PasteboardDictFromTransferable(
+ nsITransferable* aTransferable) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!aTransferable) {
+ return nil;
+ }
+
+ NSMutableDictionary* pasteboardOutputDict = [NSMutableDictionary dictionary];
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanExport(flavors);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ const mozilla::Maybe<uint32_t> imageFlavorIndex =
+ nsClipboard::FindIndexOfImageFlavor(flavors);
+
+ if (imageFlavorIndex) {
+ // When right-clicking and "Copy Image" is clicked on macOS, some apps
+ // expect the first flavor to be the image flavor. See bug 1689992. For
+ // other apps, the order shouldn't matter.
+ std::swap(*flavors.begin(), flavors[*imageFlavorIndex]);
+ }
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ MOZ_CLIPBOARD_LOG("writing out clipboard data of type %s (%d)\n",
+ flavorStr.get(), i);
+
+ NSString* pboardType = nil;
+ if (nsClipboard::IsStringType(flavorStr, &pboardType)) {
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ rv = aTransferable->GetTransferData(flavorStr.get(),
+ getter_AddRefs(genericDataWrapper));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsAutoString data;
+ if (nsCOMPtr<nsISupportsString> text =
+ do_QueryInterface(genericDataWrapper)) {
+ text->GetData(data);
+ }
+
+ NSString* nativeString;
+ if (!data.IsEmpty()) {
+ nativeString = [NSString stringWithCharacters:(const unichar*)data.get()
+ length:data.Length()];
+ } else {
+ nativeString = [NSString string];
+ }
+
+ // be nice to Carbon apps, normalize the receiver's contents using Form C.
+ nativeString = [nativeString precomposedStringWithCanonicalMapping];
+ if (nativeString) {
+ [pasteboardOutputDict setObject:nativeString forKey:pboardType];
+ }
+
+ if (aTransferable->GetIsPrivateData()) {
+ // In the case of password strings, we want to include the key for
+ // concealed type. These will be flagged as private data.
+ [pasteboardOutputDict
+ setObject:nativeString
+ forKey:[UTIHelper
+ stringFromPboardType:kPasteboardConcealedType]];
+ }
+ } else if (flavorStr.EqualsLiteral(kCustomTypesMime)) {
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ rv = aTransferable->GetTransferData(flavorStr.get(),
+ getter_AddRefs(genericDataWrapper));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsAutoCString data;
+ if (nsCOMPtr<nsISupportsCString> text =
+ do_QueryInterface(genericDataWrapper)) {
+ text->GetData(data);
+ }
+
+ if (!data.IsEmpty()) {
+ NSData* nativeData = [NSData dataWithBytes:data.get()
+ length:data.Length()];
+ NSString* customType =
+ [UTIHelper stringFromPboardType:kMozCustomTypesPboardType];
+ if (!nativeData) {
+ continue;
+ }
+ [pasteboardOutputDict setObject:nativeData forKey:customType];
+ }
+ } else if (nsClipboard::IsImageType(flavorStr)) {
+ nsCOMPtr<nsISupports> transferSupports;
+ rv = aTransferable->GetTransferData(flavorStr.get(),
+ getter_AddRefs(transferSupports));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsCOMPtr<imgIContainer> image(do_QueryInterface(transferSupports));
+ if (!image) {
+ NS_WARNING("Image isn't an imgIContainer in transferable");
+ continue;
+ }
+
+ RefPtr<SourceSurface> surface = image->GetFrame(
+ imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ if (!surface) {
+ continue;
+ }
+ CGImageRef imageRef = NULL;
+ rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &imageRef);
+ if (NS_FAILED(rv) || !imageRef) {
+ continue;
+ }
+
+ // Convert the CGImageRef to TIFF and PNG data.
+ CFMutableDataRef tiffData = CFDataCreateMutable(kCFAllocatorDefault, 0);
+ CFMutableDataRef pngData = CFDataCreateMutable(kCFAllocatorDefault, 0);
+ CGImageDestinationRef destRefTIFF = CGImageDestinationCreateWithData(
+ tiffData, CFSTR("public.tiff"), 1, NULL);
+ CGImageDestinationRef destRefPNG = CGImageDestinationCreateWithData(
+ pngData, CFSTR("public.png"), 1, NULL);
+ CGImageDestinationAddImage(destRefTIFF, imageRef, NULL);
+ CGImageDestinationAddImage(destRefPNG, imageRef, NULL);
+ const bool successfullyConvertedTIFF =
+ CGImageDestinationFinalize(destRefTIFF);
+ const bool successfullyConvertedPNG =
+ CGImageDestinationFinalize(destRefPNG);
+
+ CGImageRelease(imageRef);
+ if (destRefTIFF) {
+ CFRelease(destRefTIFF);
+ }
+ if (destRefPNG) {
+ CFRelease(destRefPNG);
+ }
+
+ if (successfullyConvertedTIFF && tiffData) {
+ NSString* tiffType =
+ [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF];
+ [pasteboardOutputDict setObject:(NSMutableData*)tiffData
+ forKey:tiffType];
+ }
+ if (successfullyConvertedPNG && pngData) {
+ NSString* pngType =
+ [UTIHelper stringFromPboardType:NSPasteboardTypePNG];
+
+ [pasteboardOutputDict setObject:(NSMutableData*)pngData forKey:pngType];
+ }
+ if (tiffData) {
+ CFRelease(tiffData);
+ }
+ if (pngData) {
+ CFRelease(pngData);
+ }
+ } else if (flavorStr.EqualsLiteral(kFileMime)) {
+ nsCOMPtr<nsISupports> genericFile;
+ rv = aTransferable->GetTransferData(flavorStr.get(),
+ getter_AddRefs(genericFile));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> file(do_QueryInterface(genericFile));
+ if (!file) {
+ continue;
+ }
+
+ nsAutoString fileURI;
+ rv = file->GetPath(fileURI);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ NSString* str = nsCocoaUtils::ToNSString(fileURI);
+ NSURL* url = [NSURL fileURLWithPath:str isDirectory:NO];
+ if (!url || ![url absoluteString]) {
+ continue;
+ }
+ NSString* fileUTType =
+ [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL];
+ [pasteboardOutputDict setObject:[url absoluteString] forKey:fileUTType];
+ } else if (flavorStr.EqualsLiteral(kFilePromiseMime)) {
+ NSString* urlPromise = [UTIHelper
+ stringFromPboardType:(NSString*)kPasteboardTypeFileURLPromise];
+ NSString* urlPromiseContent = [UTIHelper
+ stringFromPboardType:(NSString*)kPasteboardTypeFilePromiseContent];
+ [pasteboardOutputDict setObject:[NSArray arrayWithObject:@""]
+ forKey:urlPromise];
+ [pasteboardOutputDict setObject:[NSArray arrayWithObject:@""]
+ forKey:urlPromiseContent];
+ } else if (flavorStr.EqualsLiteral(kURLMime)) {
+ nsCOMPtr<nsISupports> genericURL;
+ rv = aTransferable->GetTransferData(flavorStr.get(),
+ getter_AddRefs(genericURL));
+ nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
+
+ nsAutoString url;
+ urlObject->GetData(url);
+
+ NSString* nativeTitle = nil;
+
+ // A newline embedded in the URL means that the form is actually URL +
+ // title. This embedding occurs in nsDragService::GetData.
+ int32_t newlinePos = url.FindChar(char16_t('\n'));
+ if (newlinePos >= 0) {
+ url.Truncate(newlinePos);
+
+ nsAutoString urlTitle;
+ urlObject->GetData(urlTitle);
+ urlTitle.Mid(urlTitle, newlinePos + 1,
+ urlTitle.Length() - (newlinePos + 1));
+
+ nativeTitle =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ urlTitle.get())
+ length:urlTitle.Length()];
+ }
+ // The Finder doesn't like getting random binary data aka
+ // Unicode, so change it into an escaped URL containing only
+ // ASCII.
+ nsAutoCString utf8Data = NS_ConvertUTF16toUTF8(url.get(), url.Length());
+ nsAutoCString escData;
+ NS_EscapeURL(utf8Data.get(), utf8Data.Length(),
+ esc_OnlyNonASCII | esc_AlwaysCopy, escData);
+
+ NSString* nativeURL = [NSString stringWithUTF8String:escData.get()];
+ NSString* publicUrl =
+ [UTIHelper stringFromPboardType:kPublicUrlPboardType];
+ if (!nativeURL) {
+ continue;
+ }
+ [pasteboardOutputDict setObject:nativeURL forKey:publicUrl];
+ if (nativeTitle) {
+ NSArray* urlsAndTitles = @[ @[ nativeURL ], @[ nativeTitle ] ];
+ NSString* urlName =
+ [UTIHelper stringFromPboardType:kPublicUrlNamePboardType];
+ NSString* urlsWithTitles =
+ [UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType];
+ [pasteboardOutputDict setObject:nativeTitle forKey:urlName];
+ [pasteboardOutputDict setObject:urlsAndTitles forKey:urlsWithTitles];
+ }
+ }
+ // If it wasn't a type that we recognize as exportable we don't put it on
+ // the system clipboard. We'll just access it from our cached transferable
+ // when we need it.
+ }
+
+ return pasteboardOutputDict;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+bool nsClipboard::IsStringType(const nsCString& aMIMEType,
+ NSString** aPboardType) {
+ if (aMIMEType.EqualsLiteral(kTextMime)) {
+ *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ return true;
+ } else if (aMIMEType.EqualsLiteral(kRTFMime)) {
+ *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeRTF];
+ return true;
+ } else if (aMIMEType.EqualsLiteral(kHTMLMime)) {
+ *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
+ return true;
+ } else {
+ return false;
+ }
+}
+
+// static
+bool nsClipboard::IsImageType(const nsACString& aMIMEType) {
+ return aMIMEType.EqualsLiteral(kPNGImageMime) ||
+ aMIMEType.EqualsLiteral(kJPEGImageMime) ||
+ aMIMEType.EqualsLiteral(kJPGImageMime) ||
+ aMIMEType.EqualsLiteral(kGIFImageMime) ||
+ aMIMEType.EqualsLiteral(kNativeImageMime);
+}
+
+NSString* nsClipboard::WrapHtmlForSystemPasteboard(NSString* aString) {
+ NSString* wrapped =
+ [NSString stringWithFormat:@"<html>"
+ "<head>"
+ "<meta http-equiv=\"content-type\" "
+ "content=\"text/html; charset=utf-8\">"
+ "</head>"
+ "<body>"
+ "%@"
+ "</body>"
+ "</html>",
+ aString];
+ return wrapped;
+}
+
+nsresult nsClipboard::EmptyNativeClipboardData(int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (kSelectionCache == aWhichClipboard) {
+ ClearSelectionCache();
+ return NS_OK;
+ }
+
+ if (NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard)) {
+ [cocoaPasteboard clearContents];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+mozilla::Result<int32_t, nsresult>
+nsClipboard::GetNativeClipboardSequenceNumber(int32_t aWhichClipboard) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ if (kSelectionCache == aWhichClipboard) {
+ return sSelectionCacheChangeCount;
+ }
+
+ NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
+ if (!cocoaPasteboard) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+
+ return [cocoaPasteboard changeCount];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(mozilla::Err(NS_ERROR_FAILURE));
+}
diff --git a/widget/cocoa/nsCocoaFeatures.h b/widget/cocoa/nsCocoaFeatures.h
new file mode 100644
index 0000000000..0b157648cc
--- /dev/null
+++ b/widget/cocoa/nsCocoaFeatures.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 20; 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/. */
+
+#ifndef nsCocoaFeatures_h_
+#define nsCocoaFeatures_h_
+
+#include <stdint.h>
+
+/// Note that this class assumes we support the platform we are running on.
+/// For better or worse, if the version is unknown or less than what we
+/// support, we set it to the minimum supported version. GetSystemVersion
+/// is the only call that returns the unadjusted values.
+class nsCocoaFeatures {
+ public:
+ static int32_t macOSVersion();
+ static int32_t macOSVersionMajor();
+ static int32_t macOSVersionMinor();
+ static int32_t macOSVersionBugFix();
+ static bool OnBigSurOrLater();
+ static bool OnMontereyOrLater();
+ static bool OnVenturaOrLater();
+
+ static bool IsAtLeastVersion(int32_t aMajor, int32_t aMinor,
+ int32_t aBugFix = 0);
+
+ static bool ProcessIsRosettaTranslated();
+
+ // These are utilities that do not change or depend on the value of
+ // mOSVersion and instead just encapsulate the encoding algorithm. Note that
+ // GetVersion actually adjusts to the lowest supported OS, so it will always
+ // return a "supported" version. GetSystemVersion does not make any
+ // modifications.
+ static void GetSystemVersion(int& aMajor, int& aMinor, int& aBugFix);
+ static int32_t GetVersion(int32_t aMajor, int32_t aMinor, int32_t aBugFix);
+ static int32_t ExtractMajorVersion(int32_t aVersion);
+ static int32_t ExtractMinorVersion(int32_t aVersion);
+ static int32_t ExtractBugFixVersion(int32_t aVersion);
+
+ private:
+ nsCocoaFeatures() = delete; // Prevent instantiation.
+ static void InitializeVersionNumbers();
+
+ static int32_t mOSVersion;
+};
+
+#endif // nsCocoaFeatures_h_
diff --git a/widget/cocoa/nsCocoaFeatures.mm b/widget/cocoa/nsCocoaFeatures.mm
new file mode 100644
index 0000000000..1dd6344051
--- /dev/null
+++ b/widget/cocoa/nsCocoaFeatures.mm
@@ -0,0 +1,206 @@
+/* -*- Mode: C++; tab-width: 20; 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/. */
+
+// This file makes some assumptions about the versions of macOS.
+// We are assuming that the major, minor and bugfix versions are each less than
+// 256.
+// There are MOZ_ASSERTs for that.
+
+// The formula for the version integer is (major << 16) + (minor << 8) + bugfix.
+
+#define MACOS_VERSION_MASK 0x00FFFFFF
+#define MACOS_MAJOR_VERSION_MASK 0x00FFFFFF
+#define MACOS_MINOR_VERSION_MASK 0x00FFFFFF
+#define MACOS_BUGFIX_VERSION_MASK 0x00FFFFFF
+#define MACOS_VERSION_10_0_HEX 0x000A0000
+#define MACOS_VERSION_10_9_HEX 0x000A0900
+#define MACOS_VERSION_10_10_HEX 0x000A0A00
+#define MACOS_VERSION_10_11_HEX 0x000A0B00
+#define MACOS_VERSION_10_12_HEX 0x000A0C00
+#define MACOS_VERSION_10_13_HEX 0x000A0D00
+#define MACOS_VERSION_10_14_HEX 0x000A0E00
+#define MACOS_VERSION_10_15_HEX 0x000A0F00
+#define MACOS_VERSION_10_16_HEX 0x000A1000
+#define MACOS_VERSION_11_0_HEX 0x000B0000
+#define MACOS_VERSION_12_0_HEX 0x000C0000
+#define MACOS_VERSION_13_0_HEX 0x000D0000
+
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "nsDebug.h"
+#include "nsObjCExceptions.h"
+
+#import <Cocoa/Cocoa.h>
+#include <sys/sysctl.h>
+
+/*static*/ int32_t nsCocoaFeatures::mOSVersion = 0;
+
+// This should not be called with unchecked aMajor, which should be >= 10.
+inline int32_t AssembleVersion(int32_t aMajor, int32_t aMinor,
+ int32_t aBugFix) {
+ MOZ_ASSERT(aMajor >= 10);
+ return (aMajor << 16) + (aMinor << 8) + aBugFix;
+}
+
+int32_t nsCocoaFeatures::ExtractMajorVersion(int32_t aVersion) {
+ MOZ_ASSERT((aVersion & MACOS_VERSION_MASK) == aVersion);
+ return (aVersion & 0xFF0000) >> 16;
+}
+
+int32_t nsCocoaFeatures::ExtractMinorVersion(int32_t aVersion) {
+ MOZ_ASSERT((aVersion & MACOS_VERSION_MASK) == aVersion);
+ return (aVersion & 0xFF00) >> 8;
+}
+
+int32_t nsCocoaFeatures::ExtractBugFixVersion(int32_t aVersion) {
+ MOZ_ASSERT((aVersion & MACOS_VERSION_MASK) == aVersion);
+ return aVersion & 0xFF;
+}
+
+static int intAtStringIndex(NSArray* array, int index) {
+ return [(NSString*)[array objectAtIndex:index] integerValue];
+}
+
+void nsCocoaFeatures::GetSystemVersion(int& major, int& minor, int& bugfix) {
+ major = minor = bugfix = 0;
+
+ NSString* versionString =
+ [[NSDictionary dictionaryWithContentsOfFile:
+ @"/System/Library/CoreServices/SystemVersion.plist"]
+ objectForKey:@"ProductVersion"];
+ if (!versionString) {
+ NS_ERROR("Couldn't read /System/Library/CoreServices/SystemVersion.plist "
+ "to determine macOS "
+ "version.");
+ return;
+ }
+ NSArray* versions = [versionString componentsSeparatedByString:@"."];
+ NSUInteger count = [versions count];
+ if (count > 0) {
+ major = intAtStringIndex(versions, 0);
+ if (count > 1) {
+ minor = intAtStringIndex(versions, 1);
+ if (count > 2) {
+ bugfix = intAtStringIndex(versions, 2);
+ }
+ }
+ }
+}
+
+int32_t nsCocoaFeatures::GetVersion(int32_t aMajor, int32_t aMinor,
+ int32_t aBugFix) {
+ int32_t macOSVersion;
+ if (aMajor < 10) {
+ aMajor = 10;
+ NS_ERROR("Couldn't determine macOS version, assuming 10.9");
+ macOSVersion = MACOS_VERSION_10_9_HEX;
+ } else if (aMajor == 10 && aMinor < 9) {
+ aMinor = 9;
+ NS_ERROR("macOS version too old, assuming 10.9");
+ macOSVersion = MACOS_VERSION_10_9_HEX;
+ } else {
+ MOZ_ASSERT(aMajor >= 10);
+ MOZ_ASSERT(aMajor < 256);
+ MOZ_ASSERT(aMinor >= 0);
+ MOZ_ASSERT(aMinor < 256);
+ MOZ_ASSERT(aBugFix >= 0);
+ MOZ_ASSERT(aBugFix < 256);
+ macOSVersion = AssembleVersion(aMajor, aMinor, aBugFix);
+ }
+ MOZ_ASSERT(aMajor == ExtractMajorVersion(macOSVersion));
+ MOZ_ASSERT(aMinor == ExtractMinorVersion(macOSVersion));
+ MOZ_ASSERT(aBugFix == ExtractBugFixVersion(macOSVersion));
+ return macOSVersion;
+}
+
+/*static*/ void nsCocoaFeatures::InitializeVersionNumbers() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Provide an autorelease pool to avoid leaking Cocoa objects,
+ // as this gets called before the main autorelease pool is in place.
+ nsAutoreleasePool localPool;
+
+ int major, minor, bugfix;
+ GetSystemVersion(major, minor, bugfix);
+ mOSVersion = GetVersion(major, minor, bugfix);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersion() {
+ // Don't let this be called while we're first setting the value...
+ MOZ_ASSERT((mOSVersion & MACOS_VERSION_MASK) >= 0);
+ if (!mOSVersion) {
+ mOSVersion = -1;
+ InitializeVersionNumbers();
+ }
+ return mOSVersion;
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersionMajor() {
+ return ExtractMajorVersion(macOSVersion());
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersionMinor() {
+ return ExtractMinorVersion(macOSVersion());
+}
+
+/* static */ int32_t nsCocoaFeatures::macOSVersionBugFix() {
+ return ExtractBugFixVersion(macOSVersion());
+}
+
+/* static */ bool nsCocoaFeatures::OnBigSurOrLater() {
+ // Account for the version being 10.16 or 11.0 on Big Sur.
+ // The version is reported as 10.16 if SYSTEM_VERSION_COMPAT is set to 1,
+ // or if SYSTEM_VERSION_COMPAT is not set and the application is linked
+ // with a pre-Big Sur SDK.
+ // Firefox sets SYSTEM_VERSION_COMPAT to 0 in its Info.plist, so it'll
+ // usually see the correct 11.* version, despite being linked against an
+ // old SDK. However, it still sees the 10.16 compatibility version when
+ // launched from the command line, see bug 1727624. (This only applies to
+ // the Intel build - the arm64 build is linked against a Big Sur SDK and
+ // always sees the correct version.)
+ return ((macOSVersion() >= MACOS_VERSION_10_16_HEX) ||
+ (macOSVersion() >= MACOS_VERSION_11_0_HEX));
+}
+
+/* static */ bool nsCocoaFeatures::OnMontereyOrLater() {
+ // This check only works if SYSTEM_VERSION_COMPAT is off, otherwise
+ // Monterey pretends to be 10.16 and is indistinguishable from Big Sur.
+ // In practice, this means that an Intel Firefox build can return false
+ // from this function if it's launched from the command line, see bug 1727624.
+ // This will not be an issue anymore once we link against the Big Sur SDK.
+ return (macOSVersion() >= MACOS_VERSION_12_0_HEX);
+}
+
+/* static */ bool nsCocoaFeatures::OnVenturaOrLater() {
+ // See comments above regarding SYSTEM_VERSION_COMPAT.
+ return (macOSVersion() >= MACOS_VERSION_13_0_HEX);
+}
+
+/* static */ bool nsCocoaFeatures::IsAtLeastVersion(int32_t aMajor,
+ int32_t aMinor,
+ int32_t aBugFix) {
+ return macOSVersion() >= GetVersion(aMajor, aMinor, aBugFix);
+}
+
+/*
+ * Returns true if the process is running under Rosetta translation. Returns
+ * false if running natively or if an error was encountered. We use the
+ * `sysctl.proc_translated` sysctl which is documented by Apple to be used
+ * for this purpose. Note: using this in a sandboxed process requires allowing
+ * the sysctl in the sandbox policy.
+ */
+/* static */ bool nsCocoaFeatures::ProcessIsRosettaTranslated() {
+ int ret = 0;
+ size_t size = sizeof(ret);
+ if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) {
+ if (errno != ENOENT) {
+ fprintf(stderr, "Failed to check for translation environment\n");
+ }
+ return false;
+ }
+ return (ret == 1);
+}
diff --git a/widget/cocoa/nsCocoaUtils.h b/widget/cocoa/nsCocoaUtils.h
new file mode 100644
index 0000000000..9e3b76a920
--- /dev/null
+++ b/widget/cocoa/nsCocoaUtils.h
@@ -0,0 +1,595 @@
+/* -*- Mode: C++; tab-width: 20; 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/. */
+
+#ifndef nsCocoaUtils_h_
+#define nsCocoaUtils_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "InputData.h"
+#include "nsRect.h"
+#include "imgIContainer.h"
+#include "nsTArray.h"
+#include "Units.h"
+
+// This must be the last include:
+#include "nsObjCExceptions.h"
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIWidget.h"
+
+// Declare the backingScaleFactor method that we want to call
+// on NSView/Window/Screen objects, if they recognize it.
+@interface NSObject (BackingScaleFactorCategory)
+- (CGFloat)backingScaleFactor;
+@end
+
+// Pasteborad types
+extern NSString* const kPublicUrlPboardType;
+extern NSString* const kPublicUrlNamePboardType;
+extern NSString* const kPasteboardConcealedType;
+extern NSString* const kUrlsWithTitlesPboardType;
+extern NSString* const kMozWildcardPboardType;
+extern NSString* const kMozCustomTypesPboardType;
+extern NSString* const kMozFileUrlsPboardType;
+
+@interface UTIHelper : NSObject
++ (NSString*)stringFromPboardType:(NSString*)aType;
+@end
+
+class nsITransferable;
+class nsIWidget;
+
+namespace mozilla {
+class TimeStamp;
+namespace gfx {
+class SourceSurface;
+} // namespace gfx
+namespace dom {
+class Promise;
+} // namespace dom
+} // namespace mozilla
+
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+
+// Used to retain a Cocoa object for the remainder of a method's execution.
+class nsAutoRetainCocoaObject {
+ public:
+ explicit nsAutoRetainCocoaObject(id anObject) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ mObject = [anObject retain];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+ ~nsAutoRetainCocoaObject() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [mObject release];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+
+ private:
+ id mObject; // [STRONG]
+};
+
+// Provide a local autorelease pool for the remainder of a method's execution.
+class nsAutoreleasePool {
+ public:
+ nsAutoreleasePool() { mLocalPool = [[NSAutoreleasePool alloc] init]; }
+ ~nsAutoreleasePool() { [mLocalPool release]; }
+
+ private:
+ NSAutoreleasePool* mLocalPool;
+};
+
+@interface NSApplication (Undocumented)
+
+// Present in all versions of OS X from (at least) 10.2.8 through 10.5.
+- (BOOL)_isRunningModal;
+- (BOOL)_isRunningAppModal;
+
+// Send an event to the current Cocoa app-modal session. Present in all
+// versions of OS X from (at least) 10.2.8 through 10.5.
+- (void)_modalSession:(NSModalSession)aSession sendEvent:(NSEvent*)theEvent;
+
+@end
+
+struct KeyBindingsCommand {
+ SEL selector;
+ id data;
+};
+
+@interface NativeKeyBindingsRecorder : NSResponder {
+ @private
+ nsTArray<KeyBindingsCommand>* mCommands;
+}
+
+- (void)startRecording:(nsTArray<KeyBindingsCommand>&)aCommands;
+
+- (void)doCommandBySelector:(SEL)aSelector;
+
+- (void)insertText:(id)aString;
+
+@end // NativeKeyBindingsRecorder
+
+class nsCocoaUtils {
+ typedef mozilla::gfx::SourceSurface SourceSurface;
+ typedef mozilla::LayoutDeviceIntPoint LayoutDeviceIntPoint;
+ typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect;
+ typedef mozilla::dom::Promise Promise;
+ typedef StaticAutoPtr<nsTArray<RefPtr<Promise>>> PromiseArray;
+
+ public:
+ // Get the backing scale factor from an object that supports this selector
+ // (NSView/Window/Screen, on 10.7 or later), returning 1.0 if not supported
+ static CGFloat GetBackingScaleFactor(id aObject) {
+ if (HiDPIEnabled() &&
+ [aObject respondsToSelector:@selector(backingScaleFactor)]) {
+ return [aObject backingScaleFactor];
+ }
+ return 1.0;
+ }
+
+ // Conversions between Cocoa points and device pixels, given the backing
+ // scale factor from a view/window/screen.
+ static int32_t CocoaPointsToDevPixels(CGFloat aPts, CGFloat aBackingScale) {
+ return NSToIntRound(aPts * aBackingScale);
+ }
+
+ static LayoutDeviceIntPoint CocoaPointsToDevPixels(const NSPoint& aPt,
+ CGFloat aBackingScale) {
+ return LayoutDeviceIntPoint(NSToIntRound(aPt.x * aBackingScale),
+ NSToIntRound(aPt.y * aBackingScale));
+ }
+
+ static LayoutDeviceIntPoint CocoaPointsToDevPixelsRoundDown(
+ const NSPoint& aPt, CGFloat aBackingScale) {
+ return LayoutDeviceIntPoint(NSToIntFloor(aPt.x * aBackingScale),
+ NSToIntFloor(aPt.y * aBackingScale));
+ }
+
+ static LayoutDeviceIntRect CocoaPointsToDevPixels(const NSRect& aRect,
+ CGFloat aBackingScale) {
+ return LayoutDeviceIntRect(NSToIntRound(aRect.origin.x * aBackingScale),
+ NSToIntRound(aRect.origin.y * aBackingScale),
+ NSToIntRound(aRect.size.width * aBackingScale),
+ NSToIntRound(aRect.size.height * aBackingScale));
+ }
+
+ static CGFloat DevPixelsToCocoaPoints(int32_t aPixels,
+ CGFloat aBackingScale) {
+ return (CGFloat)aPixels / aBackingScale;
+ }
+
+ static NSPoint DevPixelsToCocoaPoints(
+ const mozilla::LayoutDeviceIntPoint& aPt, CGFloat aBackingScale) {
+ return NSMakePoint((CGFloat)aPt.x / aBackingScale,
+ (CGFloat)aPt.y / aBackingScale);
+ }
+
+ // Implements an NSPoint equivalent of -[NSWindow convertRectFromScreen:].
+ static NSPoint ConvertPointFromScreen(NSWindow* aWindow, const NSPoint& aPt) {
+ return
+ [aWindow convertRectFromScreen:NSMakeRect(aPt.x, aPt.y, 0, 0)].origin;
+ }
+
+ // Implements an NSPoint equivalent of -[NSWindow convertRectToScreen:].
+ static NSPoint ConvertPointToScreen(NSWindow* aWindow, const NSPoint& aPt) {
+ return [aWindow convertRectToScreen:NSMakeRect(aPt.x, aPt.y, 0, 0)].origin;
+ }
+
+ static NSRect DevPixelsToCocoaPoints(const LayoutDeviceIntRect& aRect,
+ CGFloat aBackingScale) {
+ return NSMakeRect((CGFloat)aRect.X() / aBackingScale,
+ (CGFloat)aRect.Y() / aBackingScale,
+ (CGFloat)aRect.Width() / aBackingScale,
+ (CGFloat)aRect.Height() / aBackingScale);
+ }
+
+ // Returns the given y coordinate, which must be in screen coordinates,
+ // flipped from Gecko to Cocoa or Cocoa to Gecko.
+ static float FlippedScreenY(float y);
+
+ // The following functions come in "DevPix" variants that work with
+ // backing-store (device pixel) coordinates, as well as the original
+ // versions that expect coordinates in Cocoa points/CSS pixels.
+ // The difference becomes important in HiDPI display modes, where Cocoa
+ // points and backing-store pixels are no longer 1:1.
+
+ // Gecko rects (nsRect) contain an origin (x,y) in a coordinate
+ // system with (0,0) in the top-left of the primary screen. Cocoa rects
+ // (NSRect) contain an origin (x,y) in a coordinate system with (0,0)
+ // in the bottom-left of the primary screen. Both nsRect and NSRect
+ // contain width/height info, with no difference in their use.
+ // This function does no scaling, so the Gecko coordinates are
+ // expected to be desktop pixels, which are equal to Cocoa points
+ // (by definition).
+ static NSRect GeckoRectToCocoaRect(const mozilla::DesktopIntRect& geckoRect);
+ static NSPoint GeckoPointToCocoaPoint(const mozilla::DesktopPoint& aPoint);
+
+ // Converts aGeckoRect in dev pixels to points in Cocoa coordinates
+ static NSRect GeckoRectToCocoaRectDevPix(
+ const mozilla::LayoutDeviceIntRect& aGeckoRect, CGFloat aBackingScale);
+
+ // See explanation for geckoRectToCocoaRect, guess what this does...
+ static mozilla::DesktopIntRect CocoaRectToGeckoRect(const NSRect& cocoaRect);
+
+ static mozilla::LayoutDeviceIntRect CocoaRectToGeckoRectDevPix(
+ const NSRect& aCocoaRect, CGFloat aBackingScale);
+
+ // Gives the location for the event in screen coordinates. Do not call this
+ // unless the window the event was originally targeted at is still alive!
+ // anEvent may be nil -- in that case the current mouse location is returned.
+ static NSPoint ScreenLocationForEvent(NSEvent* anEvent);
+
+ // Determines if an event happened over a window, whether or not the event
+ // is for the window. Does not take window z-order into account.
+ static BOOL IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow);
+
+ // Events are set up so that their coordinates refer to the window to which
+ // they were originally sent. If we reroute the event somewhere else, we'll
+ // have to get the window coordinates this way. Do not call this unless the
+ // window the event was originally targeted at is still alive!
+ static NSPoint EventLocationForWindow(NSEvent* anEvent, NSWindow* aWindow);
+
+ static BOOL IsMomentumScrollEvent(NSEvent* aEvent);
+ static BOOL EventHasPhaseInformation(NSEvent* aEvent);
+
+ // Hides the Menu bar and the Dock. Multiple hide/show requests can be nested.
+ static void HideOSChromeOnScreen(bool aShouldHide);
+
+ static nsIWidget* GetHiddenWindowWidget();
+
+ /**
+ * Should the application restore its state because it was launched by the OS
+ * at login?
+ */
+ static BOOL ShouldRestoreStateDueToLaunchAtLogin();
+
+ static void PrepareForNativeAppModalDialog();
+ static void CleanUpAfterNativeAppModalDialog();
+
+ // 3 utility functions to go from a frame of imgIContainer to CGImage and then
+ // to NSImage Convert imgIContainer -> CGImageRef, caller owns result
+
+ /** Creates a <code>CGImageRef</code> from a frame contained in an
+ <code>imgIContainer</code>. Copies the pixel data from the indicated frame
+ of the <code>imgIContainer</code> into a new <code>CGImageRef</code>. The
+ caller owns the <code>CGImageRef</code>.
+ @param aFrame the frame to convert
+ @param aResult the resulting CGImageRef
+ @param aIsEntirelyBlack an outparam that, if non-null, will be set to a
+ bool that indicates whether the RGB values on all
+ pixels are zero
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateCGImageFromSurface(SourceSurface* aSurface,
+ CGImageRef* aResult,
+ bool* aIsEntirelyBlack = nullptr);
+
+ /** Creates a Cocoa <code>NSImage</code> from a <code>CGImageRef</code>.
+ Copies the pixel data from the <code>CGImageRef</code> into a new
+ <code>NSImage</code>. The caller owns the <code>NSImage</code>.
+ @param aInputImage the image to convert
+ @param aResult the resulting NSImage
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateNSImageFromCGImage(CGImageRef aInputImage,
+ NSImage** aResult);
+
+ /** Creates a Cocoa <code>NSImage</code> from a frame of an
+ <code>imgIContainer</code>. Combines the two methods above. The caller owns
+ the <code>NSImage</code>.
+ @param aImage the image to extract a frame from
+ @param aWhichFrame the frame to extract (see imgIContainer FRAME_*)
+ @param aComputedStyle the ComputedStyle of the element that the image is
+ for, to support SVG context paint properties, can be null
+ @param aResult the resulting NSImage
+ @param scaleFactor the desired scale factor of the NSImage (2 for a retina
+ display)
+ @param aIsEntirelyBlack an outparam that, if non-null, will be set to a
+ bool that indicates whether the RGB values on all
+ pixels are zero
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateNSImageFromImageContainer(
+ imgIContainer* aImage, uint32_t aWhichFrame,
+ const nsPresContext* aPresContext,
+ const mozilla::ComputedStyle* aComputedStyle, NSImage** aResult,
+ CGFloat scaleFactor, bool* aIsEntirelyBlack = nullptr);
+
+ /** Creates a Cocoa <code>NSImage</code> from a frame of an
+ <code>imgIContainer</code>. The new <code>NSImage</code> will have both a
+ regular and HiDPI representation. The caller owns the <code>NSImage</code>.
+ @param aImage the image to extract a frame from
+ @param aWhichFrame the frame to extract (see imgIContainer FRAME_*)
+ @param aComputedStyle the ComputedStyle of the element that the image is
+ for, to support SVG context paint properties, can be null
+ @param aResult the resulting NSImage
+ @param aIsEntirelyBlack an outparam that, if non-null, will be set to a
+ bool that indicates whether the RGB values on all
+ pixels are zero
+ @return NS_OK if the conversion worked, NS_ERROR_FAILURE otherwise
+ */
+ static nsresult CreateDualRepresentationNSImageFromImageContainer(
+ imgIContainer* aImage, uint32_t aWhichFrame,
+ const nsPresContext* aPresContext,
+ const mozilla::ComputedStyle* aComputedStyle, NSImage** aResult,
+ bool* aIsEntirelyBlack = nullptr);
+
+ /**
+ * Returns nsAString for aSrc.
+ */
+ static void GetStringForNSString(const NSString* aSrc, nsAString& aDist);
+
+ /**
+ * Makes NSString instance for aString.
+ */
+ static NSString* ToNSString(const nsAString& aString);
+
+ /**
+ * Returns an NSURL instance for the provided string.
+ */
+ static NSURL* ToNSURL(const nsAString& aURLString);
+
+ /**
+ * Makes NSString instance for aCString.
+ */
+ static NSString* ToNSString(const nsACString& aCString);
+
+ /**
+ * Returns NSRect for aGeckoRect.
+ * Just copies values between the two types; it does no coordinate-system
+ * conversion, so both rects must have the same coordinate origin/direction.
+ */
+ static void GeckoRectToNSRect(const nsIntRect& aGeckoRect,
+ NSRect& aOutCocoaRect);
+
+ /**
+ * Returns Gecko rect for aCocoaRect.
+ * Just copies values between the two types; it does no coordinate-system
+ * conversion, so both rects must have the same coordinate origin/direction.
+ */
+ static void NSRectToGeckoRect(const NSRect& aCocoaRect,
+ nsIntRect& aOutGeckoRect);
+
+ /**
+ * Makes NSEvent instance for aEventTytpe and aEvent.
+ */
+ static NSEvent* MakeNewCocoaEventWithType(NSEventType aEventType,
+ NSEvent* aEvent);
+
+ /**
+ * Makes a cocoa event from a widget keyboard event.
+ */
+ static NSEvent* MakeNewCococaEventFromWidgetEvent(
+ const mozilla::WidgetKeyboardEvent& aKeyEvent, NSInteger aWindowNumber,
+ NSGraphicsContext* aContext);
+
+ /**
+ * Initializes WidgetInputEvent for aNativeEvent or aModifiers.
+ */
+ static void InitInputEvent(mozilla::WidgetInputEvent& aInputEvent,
+ NSEvent* aNativeEvent);
+
+ /**
+ * Converts the native modifiers from aNativeEvent into WidgetMouseEvent
+ * Modifiers. aNativeEvent can be null.
+ */
+ static mozilla::Modifiers ModifiersForEvent(NSEvent* aNativeEvent);
+
+ /**
+ * ConvertToCarbonModifier() returns carbon modifier flags for the cocoa
+ * modifier flags.
+ * NOTE: The result never includes right*Key.
+ */
+ static UInt32 ConvertToCarbonModifier(NSUInteger aCocoaModifier);
+
+ /**
+ * Whether to support HiDPI rendering. For testing purposes, to be removed
+ * once we're comfortable with the HiDPI behavior.
+ */
+ static bool HiDPIEnabled();
+
+ /**
+ * Keys can optionally be bound by system or user key bindings to one or more
+ * commands based on selectors. This collects any such commands in the
+ * provided array.
+ */
+ static void GetCommandsFromKeyEvent(NSEvent* aEvent,
+ nsTArray<KeyBindingsCommand>& aCommands);
+
+ /**
+ * Converts the string name of a Gecko key (like "VK_HOME") to the
+ * corresponding Cocoa Unicode character.
+ */
+ static uint32_t ConvertGeckoNameToMacCharCode(const nsAString& aKeyCodeName);
+
+ /**
+ * Converts a Gecko key code (like NS_VK_HOME) to the corresponding Cocoa
+ * Unicode character.
+ */
+ static uint32_t ConvertGeckoKeyCodeToMacCharCode(uint32_t aKeyCode);
+
+ /**
+ * Converts Gecko native modifier flags for `nsIWidget::SynthesizeNative*()`
+ * to native modifier flags of macOS.
+ */
+ static NSEventModifierFlags ConvertWidgetModifiersToMacModifierFlags(
+ nsIWidget::Modifiers aNativeModifiers);
+
+ /**
+ * Get the mouse button, which depends on the event's type and buttonNumber.
+ * Returns MouseButton::ePrimary for non-mouse events.
+ */
+ static mozilla::MouseButton ButtonForEvent(NSEvent* aEvent);
+
+ /**
+ * Convert string with font attribute to NSMutableAttributedString
+ */
+ static NSMutableAttributedString* GetNSMutableAttributedString(
+ const nsAString& aText, const nsTArray<mozilla::FontRange>& aFontRanges,
+ const bool aIsVertical, const CGFloat aBackingScaleFactor);
+
+ /**
+ * Compute TimeStamp from an event's timestamp.
+ * If aEventTime is 0, this returns current timestamp.
+ */
+ static mozilla::TimeStamp GetEventTimeStamp(NSTimeInterval aEventTime);
+
+ /**
+ * Check whether double clicking on the titlebar should cause the window to
+ * zoom (maximize).
+ */
+ static bool ShouldZoomOnTitlebarDoubleClick();
+
+ /**
+ * Check whether double clicking on the titlebar should cause the window to
+ * minimize.
+ */
+ static bool ShouldMinimizeOnTitlebarDoubleClick();
+
+ /**
+ * Get the current video capture permission status.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult GetVideoCapturePermissionState(uint16_t& aPermissionState);
+
+ /**
+ * Get the current audio capture permission status.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult GetAudioCapturePermissionState(uint16_t& aPermissionState);
+
+ /**
+ * Get the current screen capture permission status.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.14 and earlier macOS versions.
+ */
+ static nsresult GetScreenCapturePermissionState(uint16_t& aPermissionState);
+
+ /**
+ * Request video capture permission from the OS. Caller must be running
+ * on the main thread and the promise will be resolved on the main thread.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult RequestVideoCapturePermission(RefPtr<Promise>& aPromise);
+
+ /**
+ * Request audio capture permission from the OS. Caller must be running
+ * on the main thread and the promise will be resolved on the main thread.
+ * Returns NS_ERROR_NOT_IMPLEMENTED on 10.13 and earlier macOS versions.
+ */
+ static nsresult RequestAudioCapturePermission(RefPtr<Promise>& aPromise);
+
+ /**
+ * Request screen capture permission from the OS using an unreliable method.
+ */
+ static nsresult MaybeRequestScreenCapturePermission();
+
+ static void InvalidateHiDPIState();
+
+ static mozilla::PanGestureInput CreatePanGestureEvent(
+ NSEvent* aNativeEvent, mozilla::TimeStamp aTimeStamp,
+ const mozilla::ScreenPoint& aPanStartPoint,
+ const mozilla::ScreenPoint& aPreciseDelta,
+ const mozilla::gfx::IntPoint& aLineOrPageDelta,
+ mozilla::Modifiers aModifiers);
+
+ /**
+ * Return true if aAvailableType is a vaild NSPasteboard type.
+ */
+ static bool IsValidPasteboardType(NSString* aAvailableType,
+ bool aAllowFileURL);
+
+ /**
+ * Set data for specific type from NSPasteboardItem to Transferable.
+ */
+ static void SetTransferDataForTypeFromPasteboardItem(
+ nsITransferable* aTransferable, const nsCString& aFlavor,
+ NSPasteboardItem* aItem);
+
+ private:
+ /**
+ * Completion handlers used as an argument to the macOS API to
+ * request media capture permission. These are called asynchronously
+ * on an arbitrary dispatch queue.
+ */
+ static void (^AudioCompletionHandler)(BOOL);
+ static void (^VideoCompletionHandler)(BOOL);
+
+ /**
+ * Called from the audio and video completion handlers in order to
+ * dispatch the handling back to the main thread.
+ */
+ static void ResolveAudioCapturePromises(bool aGranted);
+ static void ResolveVideoCapturePromises(bool aGranted);
+
+ /**
+ * Main implementation for Request{Audio,Video}CapturePermission.
+ * @param aType the AVMediaType to request capture permission for
+ * @param aPromise the Promise to resolve when capture permission
+ * is either allowed or denied
+ * @param aPromiseList the array of promises to save |aPromise| in
+ * @param aHandler the block function (either ResolveAudioCapturePromises
+ * or ResolveVideoCapturePromises) to be used as
+ * the requestAccessForMediaType callback.
+ */
+ static nsresult RequestCapturePermission(NSString* aType,
+ RefPtr<Promise>& aPromise,
+ PromiseArray& aPromiseList,
+ void (^aHandler)(BOOL granted));
+ /**
+ * Resolves the pending promises that are waiting for a response
+ * to a request video or audio capture permission.
+ */
+ static void ResolveMediaCapturePromises(bool aGranted,
+ PromiseArray& aPromiseList);
+
+ /**
+ * Get string data for a specific type from NSPasteboardItem.
+ */
+ static NSString* GetStringForTypeFromPasteboardItem(
+ NSPasteboardItem* aItem, const NSString* aType,
+ bool aAllowFileURL = false);
+
+ /**
+ * Get the file path from NSPasteboardItem.
+ */
+ static NSString* GetFilePathFromPasteboardItem(NSPasteboardItem* aItem);
+
+ /**
+ * Get the title for URL from NSPasteboardItem.
+ */
+ static NSString* GetTitleForURLFromPasteboardItem(NSPasteboardItem* item);
+
+ /**
+ * Did the OS launch the application at login?
+ */
+ static BOOL WasLaunchedAtLogin();
+
+ /**
+ * Should the application restore its state because it was launched by the OS
+ * at login?
+ */
+ static BOOL ShouldRestoreStateDueToLaunchAtLoginImpl();
+
+ /**
+ * Array of promises waiting to be resolved due to a video capture request.
+ */
+ static PromiseArray sVideoCapturePromises;
+
+ /**
+ * Array of promises waiting to be resolved due to an audio capture request.
+ */
+ static PromiseArray sAudioCapturePromises;
+
+ /**
+ * Lock protecting |sVideoCapturePromises| and |sAudioCapturePromises|.
+ */
+ static StaticMutex sMediaCaptureMutex MOZ_UNANNOTATED;
+};
+
+#endif // nsCocoaUtils_h_
diff --git a/widget/cocoa/nsCocoaUtils.mm b/widget/cocoa/nsCocoaUtils.mm
new file mode 100644
index 0000000000..f3a7604762
--- /dev/null
+++ b/widget/cocoa/nsCocoaUtils.mm
@@ -0,0 +1,1857 @@
+/* -*- Mode: C++; tab-width: 20; 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/. */
+
+#import <AVFoundation/AVFoundation.h>
+
+#include <cmath>
+
+#include "AppleUtils.h"
+#include "gfx2DGlue.h"
+#include "gfxContext.h"
+#include "gfxPlatform.h"
+#include "gfxUtils.h"
+#include "ImageRegion.h"
+#include "nsClipboard.h"
+#include "nsCocoaUtils.h"
+#include "nsChildView.h"
+#include "nsMenuBarX.h"
+#include "nsCocoaWindow.h"
+#include "nsCOMPtr.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIAppShellService.h"
+#include "nsIOSPermissionRequest.h"
+#include "nsIRunnable.h"
+#include "nsIAppWindow.h"
+#include "nsIBaseWindow.h"
+#include "nsITransferable.h"
+#include "nsMenuUtilsX.h"
+#include "nsNetUtil.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsToolkit.h"
+#include "nsCRT.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SVGImageContext.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/gfx/2D.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+using mozilla::dom::Promise;
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::DrawTarget;
+using mozilla::gfx::IntPoint;
+using mozilla::gfx::IntRect;
+using mozilla::gfx::IntSize;
+using mozilla::gfx::SamplingFilter;
+using mozilla::gfx::SourceSurface;
+using mozilla::gfx::SurfaceFormat;
+using mozilla::image::ImageRegion;
+
+LazyLogModule gCocoaUtilsLog("nsCocoaUtils");
+#undef LOG
+#define LOG(...) MOZ_LOG(gCocoaUtilsLog, LogLevel::Debug, (__VA_ARGS__))
+
+/*
+ * For each audio and video capture request, we hold an owning reference
+ * to a promise to be resolved when the request's async callback is invoked.
+ * sVideoCapturePromises and sAudioCapturePromises are arrays of video and
+ * audio promises waiting for to be resolved. Each array is protected by a
+ * mutex.
+ */
+nsCocoaUtils::PromiseArray nsCocoaUtils::sVideoCapturePromises;
+nsCocoaUtils::PromiseArray nsCocoaUtils::sAudioCapturePromises;
+StaticMutex nsCocoaUtils::sMediaCaptureMutex;
+
+/**
+ * Pasteboard types
+ */
+NSString* const kPublicUrlPboardType = @"public.url";
+NSString* const kPublicUrlNamePboardType = @"public.url-name";
+NSString* const kPasteboardConcealedType = @"org.nspasteboard.ConcealedType";
+NSString* const kUrlsWithTitlesPboardType = @"WebURLsWithTitlesPboardType";
+NSString* const kMozWildcardPboardType = @"org.mozilla.MozillaWildcard";
+NSString* const kMozCustomTypesPboardType = @"org.mozilla.custom-clipdata";
+NSString* const kMozFileUrlsPboardType = @"org.mozilla.file-urls";
+
+@implementation UTIHelper
+
++ (NSString*)stringFromPboardType:(NSString*)aType {
+ if ([aType isEqualToString:kMozWildcardPboardType] ||
+ [aType isEqualToString:kMozCustomTypesPboardType] ||
+ [aType isEqualToString:kPasteboardConcealedType] ||
+ [aType isEqualToString:kPublicUrlPboardType] ||
+ [aType isEqualToString:kPublicUrlNamePboardType] ||
+ [aType isEqualToString:kMozFileUrlsPboardType] ||
+ [aType isEqualToString:(NSString*)kPasteboardTypeFileURLPromise] ||
+ [aType isEqualToString:(NSString*)kPasteboardTypeFilePromiseContent] ||
+ [aType isEqualToString:(NSString*)kUTTypeFileURL] ||
+ [aType isEqualToString:NSStringPboardType] ||
+ [aType isEqualToString:NSPasteboardTypeString] ||
+ [aType isEqualToString:NSPasteboardTypeHTML] ||
+ [aType isEqualToString:NSPasteboardTypeRTF] ||
+ [aType isEqualToString:NSPasteboardTypeTIFF] ||
+ [aType isEqualToString:NSPasteboardTypePNG]) {
+ return [NSString stringWithString:aType];
+ }
+ NSString* dynamicType = (NSString*)UTTypeCreatePreferredIdentifierForTag(
+ kUTTagClassNSPboardType, (CFStringRef)aType, kUTTypeData);
+ NSString* result = [NSString stringWithString:dynamicType];
+ [dynamicType release];
+ return result;
+}
+
+@end // UTIHelper
+
+static float MenuBarScreenHeight() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSArray* allScreens = [NSScreen screens];
+ if ([allScreens count]) {
+ return [[allScreens objectAtIndex:0] frame].size.height;
+ }
+
+ return 0.0;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(0.0);
+}
+
+float nsCocoaUtils::FlippedScreenY(float y) {
+ return MenuBarScreenHeight() - y;
+}
+
+NSRect nsCocoaUtils::GeckoRectToCocoaRect(const DesktopIntRect& geckoRect) {
+ // We only need to change the Y coordinate by starting with the primary screen
+ // height and subtracting the gecko Y coordinate of the bottom of the rect.
+ return NSMakeRect(geckoRect.x, MenuBarScreenHeight() - geckoRect.YMost(),
+ geckoRect.width, geckoRect.height);
+}
+
+NSPoint nsCocoaUtils::GeckoPointToCocoaPoint(
+ const mozilla::DesktopPoint& aPoint) {
+ return NSMakePoint(aPoint.x, MenuBarScreenHeight() - aPoint.y);
+}
+
+NSRect nsCocoaUtils::GeckoRectToCocoaRectDevPix(
+ const LayoutDeviceIntRect& aGeckoRect, CGFloat aBackingScale) {
+ return NSMakeRect(aGeckoRect.x / aBackingScale,
+ MenuBarScreenHeight() - aGeckoRect.YMost() / aBackingScale,
+ aGeckoRect.width / aBackingScale,
+ aGeckoRect.height / aBackingScale);
+}
+
+DesktopIntRect nsCocoaUtils::CocoaRectToGeckoRect(const NSRect& cocoaRect) {
+ // We only need to change the Y coordinate by starting with the primary screen
+ // height and subtracting both the cocoa y origin and the height of the
+ // cocoa rect.
+ DesktopIntRect rect;
+ rect.x = NSToIntRound(cocoaRect.origin.x);
+ rect.y =
+ NSToIntRound(FlippedScreenY(cocoaRect.origin.y + cocoaRect.size.height));
+ rect.width = NSToIntRound(cocoaRect.origin.x + cocoaRect.size.width) - rect.x;
+ rect.height = NSToIntRound(FlippedScreenY(cocoaRect.origin.y)) - rect.y;
+ return rect;
+}
+
+LayoutDeviceIntRect nsCocoaUtils::CocoaRectToGeckoRectDevPix(
+ const NSRect& aCocoaRect, CGFloat aBackingScale) {
+ LayoutDeviceIntRect rect;
+ rect.x = NSToIntRound(aCocoaRect.origin.x * aBackingScale);
+ rect.y = NSToIntRound(
+ FlippedScreenY(aCocoaRect.origin.y + aCocoaRect.size.height) *
+ aBackingScale);
+ rect.width = NSToIntRound((aCocoaRect.origin.x + aCocoaRect.size.width) *
+ aBackingScale) -
+ rect.x;
+ rect.height =
+ NSToIntRound(FlippedScreenY(aCocoaRect.origin.y) * aBackingScale) -
+ rect.y;
+ return rect;
+}
+
+NSPoint nsCocoaUtils::ScreenLocationForEvent(NSEvent* anEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Don't trust mouse locations of mouse move events, see bug 443178.
+ if (!anEvent || [anEvent type] == NSEventTypeMouseMoved)
+ return [NSEvent mouseLocation];
+
+ // Pin momentum scroll events to the location of the last user-controlled
+ // scroll event.
+ if (IsMomentumScrollEvent(anEvent))
+ return ChildViewMouseTracker::sLastScrollEventScreenLocation;
+
+ return nsCocoaUtils::ConvertPointToScreen([anEvent window],
+ [anEvent locationInWindow]);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakePoint(0.0, 0.0));
+}
+
+BOOL nsCocoaUtils::IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return NSPointInRect(ScreenLocationForEvent(anEvent), [aWindow frame]);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+NSPoint nsCocoaUtils::EventLocationForWindow(NSEvent* anEvent,
+ NSWindow* aWindow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return nsCocoaUtils::ConvertPointFromScreen(aWindow,
+ ScreenLocationForEvent(anEvent));
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NSMakePoint(0.0, 0.0));
+}
+
+BOOL nsCocoaUtils::IsMomentumScrollEvent(NSEvent* aEvent) {
+ return [aEvent type] == NSEventTypeScrollWheel &&
+ [aEvent momentumPhase] != NSEventPhaseNone;
+}
+
+BOOL nsCocoaUtils::EventHasPhaseInformation(NSEvent* aEvent) {
+ return [aEvent phase] != NSEventPhaseNone ||
+ [aEvent momentumPhase] != NSEventPhaseNone;
+}
+
+void nsCocoaUtils::HideOSChromeOnScreen(bool aShouldHide) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Keep track of how many hiding requests have been made, so that they can
+ // be nested.
+ static int sHiddenCount = 0;
+
+ sHiddenCount += aShouldHide ? 1 : -1;
+ NS_ASSERTION(sHiddenCount >= 0, "Unbalanced HideMenuAndDockForWindow calls");
+
+ NSApplicationPresentationOptions options =
+ sHiddenCount <= 0 ? NSApplicationPresentationDefault
+ : NSApplicationPresentationHideDock |
+ NSApplicationPresentationHideMenuBar;
+ [NSApp setPresentationOptions:options];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1"
+nsIWidget* nsCocoaUtils::GetHiddenWindowWidget() {
+ nsCOMPtr<nsIAppShellService> appShell(
+ do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ if (!appShell) {
+ NS_WARNING(
+ "Couldn't get AppShellService in order to get hidden window ref");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIAppWindow> hiddenWindow;
+ appShell->GetHiddenWindow(getter_AddRefs(hiddenWindow));
+ if (!hiddenWindow) {
+ // Don't warn, this happens during shutdown, bug 358607.
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIBaseWindow> baseHiddenWindow;
+ baseHiddenWindow = do_GetInterface(hiddenWindow);
+ if (!baseHiddenWindow) {
+ NS_WARNING("Couldn't get nsIBaseWindow from hidden window (nsIAppWindow)");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIWidget> hiddenWindowWidget;
+ if (NS_FAILED(baseHiddenWindow->GetMainWidget(
+ getter_AddRefs(hiddenWindowWidget)))) {
+ NS_WARNING("Couldn't get nsIWidget from hidden window (nsIBaseWindow)");
+ return nullptr;
+ }
+
+ return hiddenWindowWidget;
+}
+
+BOOL nsCocoaUtils::WasLaunchedAtLogin() {
+ ProcessSerialNumber processSerialNumber = {0, kCurrentProcess};
+ ProcessInfoRec processInfoRec = {};
+ processInfoRec.processInfoLength = sizeof(processInfoRec);
+
+ // There is currently no replacement for ::GetProcessInformation, which has
+ // been deprecated since macOS 10.9.
+ if (::GetProcessInformation(&processSerialNumber, &processInfoRec) == noErr) {
+ ProcessInfoRec parentProcessInfo = {};
+ parentProcessInfo.processInfoLength = sizeof(parentProcessInfo);
+ if (::GetProcessInformation(&processInfoRec.processLauncher,
+ &parentProcessInfo) == noErr) {
+ return parentProcessInfo.processSignature == 'lgnw';
+ }
+ }
+ return NO;
+}
+
+BOOL nsCocoaUtils::ShouldRestoreStateDueToLaunchAtLoginImpl() {
+ // Check if we were launched by macOS as a result of having
+ // "Reopen windows..." selected during a restart.
+ if (!WasLaunchedAtLogin()) {
+ return NO;
+ }
+
+ CFStringRef lgnwPlistName = CFSTR("com.apple.loginwindow");
+ CFStringRef saveStateKey = CFSTR("TALLogoutSavesState");
+ CFPropertyListRef lgnwPlist = (CFPropertyListRef)(::CFPreferencesCopyAppValue(
+ saveStateKey, lgnwPlistName));
+ // The .plist doesn't exist unless the user changed the "Reopen windows..."
+ // preference. If it doesn't exist, restore by default (as this is the macOS
+ // default).
+ // https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpsystemstartup/chapters/CustomLogin.html
+ if (!lgnwPlist) {
+ return YES;
+ }
+
+ if (CFBooleanRef shouldRestoreState = static_cast<CFBooleanRef>(lgnwPlist)) {
+ return ::CFBooleanGetValue(shouldRestoreState);
+ }
+
+ return NO;
+}
+
+BOOL nsCocoaUtils::ShouldRestoreStateDueToLaunchAtLogin() {
+ BOOL shouldRestore = ShouldRestoreStateDueToLaunchAtLoginImpl();
+ Telemetry::ScalarSet(Telemetry::ScalarID::STARTUP_IS_RESTORED_BY_MACOS,
+ !!shouldRestore);
+ return shouldRestore;
+}
+
+void nsCocoaUtils::PrepareForNativeAppModalDialog() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Don't do anything if this is embedding. We'll assume that if there is no
+ // hidden window we shouldn't do anything, and that should cover the embedding
+ // case.
+ nsMenuBarX* hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
+ if (!hiddenWindowMenuBar) return;
+
+ // First put up the hidden window menu bar so that app menu event handling is
+ // correct.
+ hiddenWindowMenuBar->Paint();
+
+ NSMenu* mainMenu = [NSApp mainMenu];
+ NS_ASSERTION(
+ [mainMenu numberOfItems] > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+
+ // Create new menu bar for use with modal dialog
+ NSMenu* newMenuBar = [[NSMenu alloc] initWithTitle:@""];
+
+ // Swap in our app menu. Note that the event target is whatever window is up
+ // when the app modal dialog goes up.
+ NSMenuItem* firstMenuItem = [[mainMenu itemAtIndex:0] retain];
+ [mainMenu removeItemAtIndex:0];
+ [newMenuBar insertItem:firstMenuItem atIndex:0];
+ [firstMenuItem release];
+
+ // Add standard edit menu
+ [newMenuBar addItem:nsMenuUtilsX::GetStandardEditMenuItem()];
+
+ // Show the new menu bar
+ [NSApp setMainMenu:newMenuBar];
+ [newMenuBar release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaUtils::CleanUpAfterNativeAppModalDialog() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Don't do anything if this is embedding. We'll assume that if there is no
+ // hidden window we shouldn't do anything, and that should cover the embedding
+ // case.
+ nsMenuBarX* hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
+ if (!hiddenWindowMenuBar) return;
+
+ NSWindow* mainWindow = [NSApp mainWindow];
+ if (!mainWindow)
+ hiddenWindowMenuBar->Paint();
+ else
+ [WindowDelegate paintMenubarForWindow:mainWindow];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static void data_ss_release_callback(void* aDataSourceSurface, const void* data,
+ size_t size) {
+ if (aDataSourceSurface) {
+ static_cast<DataSourceSurface*>(aDataSourceSurface)->Unmap();
+ static_cast<DataSourceSurface*>(aDataSourceSurface)->Release();
+ }
+}
+
+// This function assumes little endian byte order.
+static bool ComputeIsEntirelyBlack(const DataSourceSurface::MappedSurface& aMap,
+ const IntSize& aSize) {
+ for (int32_t y = 0; y < aSize.height; y++) {
+ size_t rowStart = y * aMap.mStride;
+ for (int32_t x = 0; x < aSize.width; x++) {
+ size_t index = rowStart + x * 4;
+ if (aMap.mData[index + 0] != 0 || aMap.mData[index + 1] != 0 ||
+ aMap.mData[index + 2] != 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+nsresult nsCocoaUtils::CreateCGImageFromSurface(SourceSurface* aSurface,
+ CGImageRef* aResult,
+ bool* aIsEntirelyBlack) {
+ RefPtr<DataSourceSurface> dataSurface;
+
+ if (aSurface->GetFormat() == SurfaceFormat::B8G8R8A8) {
+ dataSurface = aSurface->GetDataSurface();
+ } else {
+ // CGImageCreate only supports 16- and 32-bit bit-depth
+ // Convert format to SurfaceFormat::B8G8R8A8
+ dataSurface = gfxUtils::CopySurfaceToDataSourceSurfaceWithFormat(
+ aSurface, SurfaceFormat::B8G8R8A8);
+ }
+
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+
+ int32_t width = dataSurface->GetSize().width;
+ int32_t height = dataSurface->GetSize().height;
+ if (height < 1 || width < 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) {
+ return NS_ERROR_FAILURE;
+ }
+ // The Unmap() call happens in data_ss_release_callback
+
+ if (aIsEntirelyBlack) {
+ *aIsEntirelyBlack = ComputeIsEntirelyBlack(map, dataSurface->GetSize());
+ }
+
+ // Create a CGImageRef with the bits from the image, taking into account
+ // the alpha ordering and endianness of the machine so we don't have to
+ // touch the bits ourselves.
+ CGDataProviderRef dataProvider = ::CGDataProviderCreateWithData(
+ dataSurface.forget().take(), map.mData, map.mStride * height,
+ data_ss_release_callback);
+ CGColorSpaceRef colorSpace =
+ ::CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
+ *aResult = ::CGImageCreate(
+ width, height, 8, 32, map.mStride, colorSpace,
+ kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst, dataProvider,
+ NULL, 0, kCGRenderingIntentDefault);
+ ::CGColorSpaceRelease(colorSpace);
+ ::CGDataProviderRelease(dataProvider);
+ return *aResult ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsCocoaUtils::CreateNSImageFromCGImage(CGImageRef aInputImage,
+ NSImage** aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Be very careful when creating the NSImage that the backing NSImageRep is
+ // exactly 1:1 with the input image. On a retina display, both [NSImage
+ // lockFocus] and [NSImage initWithCGImage:size:] will create an image with a
+ // 2x backing NSImageRep. This prevents NSCursor from recognizing a retina
+ // cursor, which only occurs if pixelsWide and pixelsHigh are exactly 2x the
+ // size of the NSImage.
+ //
+ // For example, if a 32x32 SVG cursor is rendered on a retina display, then
+ // aInputImage will be 64x64. The resulting NSImage will be scaled back down
+ // to 32x32 so it stays the correct size on the screen by changing its size
+ // (resizing a NSImage only scales the image and doesn't resample the data).
+ // If aInputImage is converted using [NSImage initWithCGImage:size:] then the
+ // bitmap will be 128x128 and NSCursor won't recognize a retina cursor, since
+ // it will expect a 64x64 bitmap.
+
+ int32_t width = ::CGImageGetWidth(aInputImage);
+ int32_t height = ::CGImageGetHeight(aInputImage);
+ NSRect imageRect = ::NSMakeRect(0.0, 0.0, width, height);
+
+ NSBitmapImageRep* offscreenRep = [[NSBitmapImageRep alloc]
+ initWithBitmapDataPlanes:NULL
+ pixelsWide:width
+ pixelsHigh:height
+ bitsPerSample:8
+ samplesPerPixel:4
+ hasAlpha:YES
+ isPlanar:NO
+ colorSpaceName:NSDeviceRGBColorSpace
+ bitmapFormat:NSBitmapFormatAlphaFirst
+ bytesPerRow:0
+ bitsPerPixel:0];
+
+ NSGraphicsContext* context =
+ [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
+ [NSGraphicsContext saveGraphicsState];
+ [NSGraphicsContext setCurrentContext:context];
+
+ // Get the Quartz context and draw.
+ CGContextRef imageContext = [[NSGraphicsContext currentContext] CGContext];
+ ::CGContextDrawImage(imageContext, *(CGRect*)&imageRect, aInputImage);
+
+ [NSGraphicsContext restoreGraphicsState];
+
+ *aResult = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
+ [*aResult addRepresentation:offscreenRep];
+ [offscreenRep release];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsCocoaUtils::CreateNSImageFromImageContainer(
+ imgIContainer* aImage, uint32_t aWhichFrame,
+ const nsPresContext* aPresContext, const ComputedStyle* aComputedStyle,
+ NSImage** aResult, CGFloat scaleFactor, bool* aIsEntirelyBlack) {
+ RefPtr<SourceSurface> surface;
+ int32_t width = 0, height = 0;
+ aImage->GetWidth(&width);
+ aImage->GetHeight(&height);
+
+ // Render a vector image at the correct resolution on a retina display
+ if (aImage->GetType() == imgIContainer::TYPE_VECTOR) {
+ IntSize scaledSize =
+ IntSize::Ceil(width * scaleFactor, height * scaleFactor);
+
+ RefPtr<DrawTarget> drawTarget =
+ gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
+ scaledSize, SurfaceFormat::B8G8R8A8);
+ if (!drawTarget || !drawTarget->IsValid()) {
+ NS_ERROR("Failed to create valid DrawTarget");
+ return NS_ERROR_FAILURE;
+ }
+
+ gfxContext context(drawTarget);
+
+ SVGImageContext svgContext;
+ if (aPresContext && aComputedStyle) {
+ SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext,
+ *aComputedStyle, aImage);
+ }
+ mozilla::image::ImgDrawResult res =
+ aImage->Draw(&context, scaledSize, ImageRegion::Create(scaledSize),
+ aWhichFrame, SamplingFilter::POINT, svgContext,
+ imgIContainer::FLAG_SYNC_DECODE, 1.0);
+
+ if (res != mozilla::image::ImgDrawResult::SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ surface = drawTarget->Snapshot();
+ } else {
+ surface =
+ aImage->GetFrame(aWhichFrame, imgIContainer::FLAG_SYNC_DECODE |
+ imgIContainer::FLAG_ASYNC_NOTIFY);
+ }
+
+ NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+
+ CGImageRef imageRef = NULL;
+ nsresult rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &imageRef,
+ aIsEntirelyBlack);
+ if (NS_FAILED(rv) || !imageRef) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = nsCocoaUtils::CreateNSImageFromCGImage(imageRef, aResult);
+ if (NS_FAILED(rv) || !aResult) {
+ return NS_ERROR_FAILURE;
+ }
+ ::CGImageRelease(imageRef);
+
+ // Ensure the image will be rendered the correct size on a retina display
+ NSSize size = NSMakeSize(width, height);
+ [*aResult setSize:size];
+ [[[*aResult representations] objectAtIndex:0] setSize:size];
+ return NS_OK;
+}
+
+nsresult nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
+ imgIContainer* aImage, uint32_t aWhichFrame,
+ const nsPresContext* aPresContext, const ComputedStyle* aComputedStyle,
+ NSImage** aResult, bool* aIsEntirelyBlack) {
+ int32_t width = 0, height = 0;
+ aImage->GetWidth(&width);
+ aImage->GetHeight(&height);
+ NSSize size = NSMakeSize(width, height);
+ *aResult = [[NSImage alloc] init];
+ [*aResult setSize:size];
+
+ NSImage* newRepresentation = nil;
+ nsresult rv = CreateNSImageFromImageContainer(
+ aImage, aWhichFrame, aPresContext, aComputedStyle, &newRepresentation,
+ 1.0f, aIsEntirelyBlack);
+ if (NS_FAILED(rv) || !newRepresentation) {
+ return NS_ERROR_FAILURE;
+ }
+
+ [[[newRepresentation representations] objectAtIndex:0] setSize:size];
+ [*aResult
+ addRepresentation:[[newRepresentation representations] objectAtIndex:0]];
+ [newRepresentation release];
+ newRepresentation = nil;
+
+ rv = CreateNSImageFromImageContainer(aImage, aWhichFrame, aPresContext,
+ aComputedStyle, &newRepresentation, 2.0f,
+ aIsEntirelyBlack);
+ if (NS_FAILED(rv) || !newRepresentation) {
+ return NS_ERROR_FAILURE;
+ }
+
+ [[[newRepresentation representations] objectAtIndex:0] setSize:size];
+ [*aResult
+ addRepresentation:[[newRepresentation representations] objectAtIndex:0]];
+ [newRepresentation release];
+ return NS_OK;
+}
+
+// static
+void nsCocoaUtils::GetStringForNSString(const NSString* aSrc,
+ nsAString& aDist) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aSrc) {
+ aDist.Truncate();
+ return;
+ }
+
+ aDist.SetLength([aSrc length]);
+ [aSrc getCharacters:reinterpret_cast<unichar*>(aDist.BeginWriting())
+ range:NSMakeRange(0, [aSrc length])];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// static
+NSString* nsCocoaUtils::ToNSString(const nsAString& aString) {
+ if (aString.IsEmpty()) {
+ return [NSString string];
+ }
+ return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ aString.BeginReading())
+ length:aString.Length()];
+}
+
+// static
+NSString* nsCocoaUtils::ToNSString(const nsACString& aCString) {
+ if (aCString.IsEmpty()) {
+ return [NSString string];
+ }
+ return [[[NSString alloc] initWithBytes:aCString.BeginReading()
+ length:aCString.Length()
+ encoding:NSUTF8StringEncoding] autorelease];
+}
+
+// static
+NSURL* nsCocoaUtils::ToNSURL(const nsAString& aURLString) {
+ nsAutoCString encodedURLString;
+ nsresult rv = NS_GetSpecWithNSURLEncoding(encodedURLString,
+ NS_ConvertUTF16toUTF8(aURLString));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ NSString* encodedURLNSString = ToNSString(encodedURLString);
+ if (!encodedURLNSString) {
+ return nullptr;
+ }
+
+ return [NSURL URLWithString:encodedURLNSString];
+}
+
+// static
+void nsCocoaUtils::GeckoRectToNSRect(const nsIntRect& aGeckoRect,
+ NSRect& aOutCocoaRect) {
+ aOutCocoaRect.origin.x = aGeckoRect.x;
+ aOutCocoaRect.origin.y = aGeckoRect.y;
+ aOutCocoaRect.size.width = aGeckoRect.width;
+ aOutCocoaRect.size.height = aGeckoRect.height;
+}
+
+// static
+void nsCocoaUtils::NSRectToGeckoRect(const NSRect& aCocoaRect,
+ nsIntRect& aOutGeckoRect) {
+ aOutGeckoRect.x = NSToIntRound(aCocoaRect.origin.x);
+ aOutGeckoRect.y = NSToIntRound(aCocoaRect.origin.y);
+ aOutGeckoRect.width =
+ NSToIntRound(aCocoaRect.origin.x + aCocoaRect.size.width) -
+ aOutGeckoRect.x;
+ aOutGeckoRect.height =
+ NSToIntRound(aCocoaRect.origin.y + aCocoaRect.size.height) -
+ aOutGeckoRect.y;
+}
+
+// static
+NSEvent* nsCocoaUtils::MakeNewCocoaEventWithType(NSEventType aEventType,
+ NSEvent* aEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSEvent* newEvent =
+ [NSEvent keyEventWithType:aEventType
+ location:[aEvent locationInWindow]
+ modifierFlags:[aEvent modifierFlags]
+ timestamp:[aEvent timestamp]
+ windowNumber:[aEvent windowNumber]
+ context:nil
+ characters:[aEvent characters]
+ charactersIgnoringModifiers:[aEvent charactersIgnoringModifiers]
+ isARepeat:[aEvent isARepeat]
+ keyCode:[aEvent keyCode]];
+ return newEvent;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// static
+NSEvent* nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(
+ const WidgetKeyboardEvent& aKeyEvent, NSInteger aWindowNumber,
+ NSGraphicsContext* aContext) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSEventType eventType;
+ if (aKeyEvent.mMessage == eKeyUp) {
+ eventType = NSEventTypeKeyUp;
+ } else {
+ eventType = NSEventTypeKeyDown;
+ }
+
+ static const uint32_t sModifierFlagMap[][2] = {
+ {MODIFIER_SHIFT, NSEventModifierFlagShift},
+ {MODIFIER_CONTROL, NSEventModifierFlagControl},
+ {MODIFIER_ALT, NSEventModifierFlagOption},
+ {MODIFIER_ALTGRAPH, NSEventModifierFlagOption},
+ {MODIFIER_META, NSEventModifierFlagCommand},
+ {MODIFIER_CAPSLOCK, NSEventModifierFlagCapsLock},
+ {MODIFIER_NUMLOCK, NSEventModifierFlagNumericPad}};
+
+ NSUInteger modifierFlags = 0;
+ for (uint32_t i = 0; i < ArrayLength(sModifierFlagMap); ++i) {
+ if (aKeyEvent.mModifiers & sModifierFlagMap[i][0]) {
+ modifierFlags |= sModifierFlagMap[i][1];
+ }
+ }
+
+ NSString* characters;
+ if (aKeyEvent.mCharCode) {
+ characters =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ &(aKeyEvent.mCharCode))
+ length:1];
+ } else {
+ uint32_t cocoaCharCode =
+ nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(aKeyEvent.mKeyCode);
+ characters = [NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(&cocoaCharCode)
+ length:1];
+ }
+
+ return [NSEvent keyEventWithType:eventType
+ location:NSMakePoint(0, 0)
+ modifierFlags:modifierFlags
+ timestamp:0
+ windowNumber:aWindowNumber
+ context:aContext
+ characters:characters
+ charactersIgnoringModifiers:characters
+ isARepeat:NO
+ keyCode:0]; // Native key code not currently needed
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// static
+void nsCocoaUtils::InitInputEvent(WidgetInputEvent& aInputEvent,
+ NSEvent* aNativeEvent) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ aInputEvent.mModifiers = ModifiersForEvent(aNativeEvent);
+ aInputEvent.mTimeStamp = GetEventTimeStamp([aNativeEvent timestamp]);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// static
+Modifiers nsCocoaUtils::ModifiersForEvent(NSEvent* aNativeEvent) {
+ NSUInteger modifiers =
+ aNativeEvent ? [aNativeEvent modifierFlags] : [NSEvent modifierFlags];
+ Modifiers result = 0;
+ if (modifiers & NSEventModifierFlagShift) {
+ result |= MODIFIER_SHIFT;
+ }
+ if (modifiers & NSEventModifierFlagControl) {
+ result |= MODIFIER_CONTROL;
+ }
+ if (modifiers & NSEventModifierFlagOption) {
+ result |= MODIFIER_ALT;
+ // Mac's option key is similar to other platforms' AltGr key.
+ // Let's set AltGr flag when option key is pressed for consistency with
+ // other platforms.
+ result |= MODIFIER_ALTGRAPH;
+ }
+ if (modifiers & NSEventModifierFlagCommand) {
+ result |= MODIFIER_META;
+ }
+
+ if (modifiers & NSEventModifierFlagCapsLock) {
+ result |= MODIFIER_CAPSLOCK;
+ }
+ // Mac doesn't have NumLock key. We can assume that NumLock is always locked
+ // if user is using a keyboard which has numpad. Otherwise, if user is using
+ // a keyboard which doesn't have numpad, e.g., MacBook's keyboard, we can
+ // assume that NumLock is always unlocked.
+ // Unfortunately, we cannot know whether current keyboard has numpad or not.
+ // We should notify locked state only when keys in numpad are pressed.
+ // By this, web applications may not be confused by unexpected numpad key's
+ // key event with unlocked state.
+ if (modifiers & NSEventModifierFlagNumericPad) {
+ result |= MODIFIER_NUMLOCK;
+ }
+
+ // Be aware, NSEventModifierFlagFunction is included when arrow keys, home key
+ // or some other keys are pressed. We cannot check whether 'fn' key is pressed
+ // or not by the flag.
+
+ return result;
+}
+
+// static
+UInt32 nsCocoaUtils::ConvertToCarbonModifier(NSUInteger aCocoaModifier) {
+ UInt32 carbonModifier = 0;
+ if (aCocoaModifier & NSEventModifierFlagCapsLock) {
+ carbonModifier |= alphaLock;
+ }
+ if (aCocoaModifier & NSEventModifierFlagControl) {
+ carbonModifier |= controlKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagOption) {
+ carbonModifier |= optionKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagShift) {
+ carbonModifier |= shiftKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagCommand) {
+ carbonModifier |= cmdKey;
+ }
+ if (aCocoaModifier & NSEventModifierFlagNumericPad) {
+ carbonModifier |= kEventKeyModifierNumLockMask;
+ }
+ if (aCocoaModifier & NSEventModifierFlagFunction) {
+ carbonModifier |= kEventKeyModifierFnMask;
+ }
+ return carbonModifier;
+}
+
+// While HiDPI support is not 100% complete and tested, we'll have a pref
+// to allow it to be turned off in case of problems (or for testing purposes).
+
+// gfx.hidpi.enabled is an integer with the meaning:
+// <= 0 : HiDPI support is disabled
+// 1 : HiDPI enabled provided all screens have the same backing resolution
+// > 1 : HiDPI enabled even if there are a mixture of screen modes
+
+// All the following code is to be removed once HiDPI work is more complete.
+
+static bool sHiDPIEnabled = false;
+static bool sHiDPIPrefInitialized = false;
+
+// static
+bool nsCocoaUtils::HiDPIEnabled() {
+ if (!sHiDPIPrefInitialized) {
+ sHiDPIPrefInitialized = true;
+
+ int prefSetting = Preferences::GetInt("gfx.hidpi.enabled", 1);
+ if (prefSetting <= 0) {
+ return false;
+ }
+
+ // prefSetting is at least 1, need to check attached screens...
+
+ int scaleFactors = 0; // used as a bitset to track the screen types found
+ NSEnumerator* screenEnum = [[NSScreen screens] objectEnumerator];
+ while (NSScreen* screen = [screenEnum nextObject]) {
+ NSDictionary* desc = [screen deviceDescription];
+ if ([desc objectForKey:NSDeviceIsScreen] == nil) {
+ continue;
+ }
+ // Currently, we only care about differentiating "1.0" and "2.0",
+ // so we set one of the two low bits to record which.
+ if ([screen backingScaleFactor] > 1.0) {
+ scaleFactors |= 2;
+ } else {
+ scaleFactors |= 1;
+ }
+ }
+
+ // Now scaleFactors will be:
+ // 0 if no screens (supporting backingScaleFactor) found
+ // 1 if only lo-DPI screens
+ // 2 if only hi-DPI screens
+ // 3 if both lo- and hi-DPI screens
+ // We'll enable HiDPI support if there's only a single screen type,
+ // OR if the pref setting is explicitly greater than 1.
+ sHiDPIEnabled = (scaleFactors <= 2) || (prefSetting > 1);
+ }
+
+ return sHiDPIEnabled;
+}
+
+// static
+void nsCocoaUtils::InvalidateHiDPIState() { sHiDPIPrefInitialized = false; }
+
+void nsCocoaUtils::GetCommandsFromKeyEvent(
+ NSEvent* aEvent, nsTArray<KeyBindingsCommand>& aCommands) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ MOZ_ASSERT(aEvent);
+
+ static NativeKeyBindingsRecorder* sNativeKeyBindingsRecorder;
+ if (!sNativeKeyBindingsRecorder) {
+ sNativeKeyBindingsRecorder = [NativeKeyBindingsRecorder new];
+ }
+
+ [sNativeKeyBindingsRecorder startRecording:aCommands];
+
+ // This will trigger 0 - N calls to doCommandBySelector: and insertText:
+ [sNativeKeyBindingsRecorder
+ interpretKeyEvents:[NSArray arrayWithObject:aEvent]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@implementation NativeKeyBindingsRecorder
+
+- (void)startRecording:(nsTArray<KeyBindingsCommand>&)aCommands {
+ mCommands = &aCommands;
+ mCommands->Clear();
+}
+
+- (void)doCommandBySelector:(SEL)aSelector {
+ KeyBindingsCommand command = {aSelector, nil};
+
+ mCommands->AppendElement(command);
+}
+
+- (void)insertText:(id)aString {
+ KeyBindingsCommand command = {@selector(insertText:), aString};
+
+ mCommands->AppendElement(command);
+}
+
+@end // NativeKeyBindingsRecorder
+
+struct KeyConversionData {
+ const char* str;
+ size_t strLength;
+ uint32_t geckoKeyCode;
+ uint32_t charCode;
+};
+
+static const KeyConversionData gKeyConversions[] = {
+
+#define KEYCODE_ENTRY(aStr, aCode) \
+ { #aStr, sizeof(#aStr) - 1, NS_##aStr, aCode }
+
+// Some keycodes may have different name in KeyboardEvent from its key name.
+#define KEYCODE_ENTRY2(aStr, aNSName, aCode) \
+ { #aStr, sizeof(#aStr) - 1, NS_##aNSName, aCode }
+
+ KEYCODE_ENTRY(VK_CANCEL, 0x001B),
+ KEYCODE_ENTRY(VK_DELETE, NSDeleteFunctionKey),
+ KEYCODE_ENTRY(VK_BACK, NSBackspaceCharacter),
+ KEYCODE_ENTRY2(VK_BACK_SPACE, VK_BACK, NSBackspaceCharacter),
+ KEYCODE_ENTRY(VK_TAB, NSTabCharacter),
+ KEYCODE_ENTRY(VK_CLEAR, NSClearLineFunctionKey),
+ KEYCODE_ENTRY(VK_RETURN, NSEnterCharacter),
+ KEYCODE_ENTRY(VK_SHIFT, 0),
+ KEYCODE_ENTRY(VK_CONTROL, 0),
+ KEYCODE_ENTRY(VK_ALT, 0),
+ KEYCODE_ENTRY(VK_PAUSE, NSPauseFunctionKey),
+ KEYCODE_ENTRY(VK_CAPS_LOCK, 0),
+ KEYCODE_ENTRY(VK_ESCAPE, 0),
+ KEYCODE_ENTRY(VK_SPACE, ' '),
+ KEYCODE_ENTRY(VK_PAGE_UP, NSPageUpFunctionKey),
+ KEYCODE_ENTRY(VK_PAGE_DOWN, NSPageDownFunctionKey),
+ KEYCODE_ENTRY(VK_END, NSEndFunctionKey),
+ KEYCODE_ENTRY(VK_HOME, NSHomeFunctionKey),
+ KEYCODE_ENTRY(VK_LEFT, NSLeftArrowFunctionKey),
+ KEYCODE_ENTRY(VK_UP, NSUpArrowFunctionKey),
+ KEYCODE_ENTRY(VK_RIGHT, NSRightArrowFunctionKey),
+ KEYCODE_ENTRY(VK_DOWN, NSDownArrowFunctionKey),
+ KEYCODE_ENTRY(VK_PRINTSCREEN, NSPrintScreenFunctionKey),
+ KEYCODE_ENTRY(VK_INSERT, NSInsertFunctionKey),
+ KEYCODE_ENTRY(VK_HELP, NSHelpFunctionKey),
+ KEYCODE_ENTRY(VK_0, '0'),
+ KEYCODE_ENTRY(VK_1, '1'),
+ KEYCODE_ENTRY(VK_2, '2'),
+ KEYCODE_ENTRY(VK_3, '3'),
+ KEYCODE_ENTRY(VK_4, '4'),
+ KEYCODE_ENTRY(VK_5, '5'),
+ KEYCODE_ENTRY(VK_6, '6'),
+ KEYCODE_ENTRY(VK_7, '7'),
+ KEYCODE_ENTRY(VK_8, '8'),
+ KEYCODE_ENTRY(VK_9, '9'),
+ KEYCODE_ENTRY(VK_SEMICOLON, ':'),
+ KEYCODE_ENTRY(VK_EQUALS, '='),
+ KEYCODE_ENTRY(VK_A, 'A'),
+ KEYCODE_ENTRY(VK_B, 'B'),
+ KEYCODE_ENTRY(VK_C, 'C'),
+ KEYCODE_ENTRY(VK_D, 'D'),
+ KEYCODE_ENTRY(VK_E, 'E'),
+ KEYCODE_ENTRY(VK_F, 'F'),
+ KEYCODE_ENTRY(VK_G, 'G'),
+ KEYCODE_ENTRY(VK_H, 'H'),
+ KEYCODE_ENTRY(VK_I, 'I'),
+ KEYCODE_ENTRY(VK_J, 'J'),
+ KEYCODE_ENTRY(VK_K, 'K'),
+ KEYCODE_ENTRY(VK_L, 'L'),
+ KEYCODE_ENTRY(VK_M, 'M'),
+ KEYCODE_ENTRY(VK_N, 'N'),
+ KEYCODE_ENTRY(VK_O, 'O'),
+ KEYCODE_ENTRY(VK_P, 'P'),
+ KEYCODE_ENTRY(VK_Q, 'Q'),
+ KEYCODE_ENTRY(VK_R, 'R'),
+ KEYCODE_ENTRY(VK_S, 'S'),
+ KEYCODE_ENTRY(VK_T, 'T'),
+ KEYCODE_ENTRY(VK_U, 'U'),
+ KEYCODE_ENTRY(VK_V, 'V'),
+ KEYCODE_ENTRY(VK_W, 'W'),
+ KEYCODE_ENTRY(VK_X, 'X'),
+ KEYCODE_ENTRY(VK_Y, 'Y'),
+ KEYCODE_ENTRY(VK_Z, 'Z'),
+ KEYCODE_ENTRY(VK_CONTEXT_MENU, NSMenuFunctionKey),
+ KEYCODE_ENTRY(VK_NUMPAD0, '0'),
+ KEYCODE_ENTRY(VK_NUMPAD1, '1'),
+ KEYCODE_ENTRY(VK_NUMPAD2, '2'),
+ KEYCODE_ENTRY(VK_NUMPAD3, '3'),
+ KEYCODE_ENTRY(VK_NUMPAD4, '4'),
+ KEYCODE_ENTRY(VK_NUMPAD5, '5'),
+ KEYCODE_ENTRY(VK_NUMPAD6, '6'),
+ KEYCODE_ENTRY(VK_NUMPAD7, '7'),
+ KEYCODE_ENTRY(VK_NUMPAD8, '8'),
+ KEYCODE_ENTRY(VK_NUMPAD9, '9'),
+ KEYCODE_ENTRY(VK_MULTIPLY, '*'),
+ KEYCODE_ENTRY(VK_ADD, '+'),
+ KEYCODE_ENTRY(VK_SEPARATOR, 0),
+ KEYCODE_ENTRY(VK_SUBTRACT, '-'),
+ KEYCODE_ENTRY(VK_DECIMAL, '.'),
+ KEYCODE_ENTRY(VK_DIVIDE, '/'),
+ KEYCODE_ENTRY(VK_F1, NSF1FunctionKey),
+ KEYCODE_ENTRY(VK_F2, NSF2FunctionKey),
+ KEYCODE_ENTRY(VK_F3, NSF3FunctionKey),
+ KEYCODE_ENTRY(VK_F4, NSF4FunctionKey),
+ KEYCODE_ENTRY(VK_F5, NSF5FunctionKey),
+ KEYCODE_ENTRY(VK_F6, NSF6FunctionKey),
+ KEYCODE_ENTRY(VK_F7, NSF7FunctionKey),
+ KEYCODE_ENTRY(VK_F8, NSF8FunctionKey),
+ KEYCODE_ENTRY(VK_F9, NSF9FunctionKey),
+ KEYCODE_ENTRY(VK_F10, NSF10FunctionKey),
+ KEYCODE_ENTRY(VK_F11, NSF11FunctionKey),
+ KEYCODE_ENTRY(VK_F12, NSF12FunctionKey),
+ KEYCODE_ENTRY(VK_F13, NSF13FunctionKey),
+ KEYCODE_ENTRY(VK_F14, NSF14FunctionKey),
+ KEYCODE_ENTRY(VK_F15, NSF15FunctionKey),
+ KEYCODE_ENTRY(VK_F16, NSF16FunctionKey),
+ KEYCODE_ENTRY(VK_F17, NSF17FunctionKey),
+ KEYCODE_ENTRY(VK_F18, NSF18FunctionKey),
+ KEYCODE_ENTRY(VK_F19, NSF19FunctionKey),
+ KEYCODE_ENTRY(VK_F20, NSF20FunctionKey),
+ KEYCODE_ENTRY(VK_F21, NSF21FunctionKey),
+ KEYCODE_ENTRY(VK_F22, NSF22FunctionKey),
+ KEYCODE_ENTRY(VK_F23, NSF23FunctionKey),
+ KEYCODE_ENTRY(VK_F24, NSF24FunctionKey),
+ KEYCODE_ENTRY(VK_NUM_LOCK, NSClearLineFunctionKey),
+ KEYCODE_ENTRY(VK_SCROLL_LOCK, NSScrollLockFunctionKey),
+ KEYCODE_ENTRY(VK_COMMA, ','),
+ KEYCODE_ENTRY(VK_PERIOD, '.'),
+ KEYCODE_ENTRY(VK_SLASH, '/'),
+ KEYCODE_ENTRY(VK_BACK_QUOTE, '`'),
+ KEYCODE_ENTRY(VK_OPEN_BRACKET, '['),
+ KEYCODE_ENTRY(VK_BACK_SLASH, '\\'),
+ KEYCODE_ENTRY(VK_CLOSE_BRACKET, ']'),
+ KEYCODE_ENTRY(VK_QUOTE, '\'')
+
+#undef KEYCODE_ENTRY
+
+};
+
+uint32_t nsCocoaUtils::ConvertGeckoNameToMacCharCode(
+ const nsAString& aKeyCodeName) {
+ if (aKeyCodeName.IsEmpty()) {
+ return 0;
+ }
+
+ nsAutoCString keyCodeName;
+ LossyCopyUTF16toASCII(aKeyCodeName, keyCodeName);
+ // We want case-insensitive comparison with data stored as uppercase.
+ ToUpperCase(keyCodeName);
+
+ uint32_t keyCodeNameLength = keyCodeName.Length();
+ const char* keyCodeNameStr = keyCodeName.get();
+ for (uint16_t i = 0; i < ArrayLength(gKeyConversions); ++i) {
+ if (keyCodeNameLength == gKeyConversions[i].strLength &&
+ nsCRT::strcmp(gKeyConversions[i].str, keyCodeNameStr) == 0) {
+ return gKeyConversions[i].charCode;
+ }
+ }
+
+ return 0;
+}
+
+uint32_t nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(uint32_t aKeyCode) {
+ if (!aKeyCode) {
+ return 0;
+ }
+
+ for (uint16_t i = 0; i < ArrayLength(gKeyConversions); ++i) {
+ if (gKeyConversions[i].geckoKeyCode == aKeyCode) {
+ return gKeyConversions[i].charCode;
+ }
+ }
+
+ return 0;
+}
+
+NSEventModifierFlags nsCocoaUtils::ConvertWidgetModifiersToMacModifierFlags(
+ nsIWidget::Modifiers aNativeModifiers) {
+ if (!aNativeModifiers) {
+ return 0;
+ }
+ struct ModifierFlagMapEntry {
+ nsIWidget::Modifiers mWidgetModifier;
+ NSEventModifierFlags mModifierFlags;
+ };
+ static constexpr ModifierFlagMapEntry sModifierFlagMap[] = {
+ {nsIWidget::CAPS_LOCK, NSEventModifierFlagCapsLock},
+ {nsIWidget::SHIFT_L, NSEventModifierFlagShift | 0x0002},
+ {nsIWidget::SHIFT_R, NSEventModifierFlagShift | 0x0004},
+ {nsIWidget::CTRL_L, NSEventModifierFlagControl | 0x0001},
+ {nsIWidget::CTRL_R, NSEventModifierFlagControl | 0x2000},
+ {nsIWidget::ALT_L, NSEventModifierFlagOption | 0x0020},
+ {nsIWidget::ALT_R, NSEventModifierFlagOption | 0x0040},
+ {nsIWidget::COMMAND_L, NSEventModifierFlagCommand | 0x0008},
+ {nsIWidget::COMMAND_R, NSEventModifierFlagCommand | 0x0010},
+ {nsIWidget::NUMERIC_KEY_PAD, NSEventModifierFlagNumericPad},
+ {nsIWidget::HELP, NSEventModifierFlagHelp},
+ {nsIWidget::FUNCTION, NSEventModifierFlagFunction}};
+
+ NSEventModifierFlags modifierFlags = 0;
+ for (const ModifierFlagMapEntry& entry : sModifierFlagMap) {
+ if (aNativeModifiers & entry.mWidgetModifier) {
+ modifierFlags |= entry.mModifierFlags;
+ }
+ }
+ return modifierFlags;
+}
+
+mozilla::MouseButton nsCocoaUtils::ButtonForEvent(NSEvent* aEvent) {
+ switch (aEvent.type) {
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeLeftMouseUp:
+ return MouseButton::ePrimary;
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeRightMouseUp:
+ return MouseButton::eSecondary;
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseDragged:
+ case NSEventTypeOtherMouseUp:
+ switch (aEvent.buttonNumber) {
+ case 3:
+ return MouseButton::eX1;
+ case 4:
+ return MouseButton::eX2;
+ default:
+ // The middle button usually has button 2, but if this is a
+ // synthesized event (for which you cannot specify a buttonNumber),
+ // then the button will be 0. Treat all remaining OtherMouse events as
+ // the middle button.
+ return MouseButton::eMiddle;
+ }
+ default:
+ // Treat non-mouse events as the primary mouse button.
+ return MouseButton::ePrimary;
+ }
+}
+
+NSMutableAttributedString* nsCocoaUtils::GetNSMutableAttributedString(
+ const nsAString& aText, const nsTArray<mozilla::FontRange>& aFontRanges,
+ const bool aIsVertical, const CGFloat aBackingScaleFactor) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ NSString* nsstr = nsCocoaUtils::ToNSString(aText);
+ NSMutableAttributedString* attrStr =
+ [[[NSMutableAttributedString alloc] initWithString:nsstr
+ attributes:nil] autorelease];
+
+ int32_t lastOffset = aText.Length();
+ for (auto i = aFontRanges.Length(); i > 0; --i) {
+ const FontRange& fontRange = aFontRanges[i - 1];
+ NSString* fontName = nsCocoaUtils::ToNSString(fontRange.mFontName);
+ CGFloat fontSize = fontRange.mFontSize / aBackingScaleFactor;
+ NSFont* font = [NSFont fontWithName:fontName size:fontSize];
+ if (!font) {
+ font = [NSFont systemFontOfSize:fontSize];
+ }
+
+ NSDictionary* attrs = @{NSFontAttributeName : font};
+ NSRange range = NSMakeRange(fontRange.mStartOffset,
+ lastOffset - fontRange.mStartOffset);
+ [attrStr setAttributes:attrs range:range];
+ lastOffset = fontRange.mStartOffset;
+ }
+
+ if (aIsVertical) {
+ [attrStr addAttribute:NSVerticalGlyphFormAttributeName
+ value:[NSNumber numberWithInt:1]
+ range:NSMakeRange(0, [attrStr length])];
+ }
+
+ return attrStr;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil)
+}
+
+TimeStamp nsCocoaUtils::GetEventTimeStamp(NSTimeInterval aEventTime) {
+ if (!aEventTime) {
+ // If the event is generated by a 3rd party application, its timestamp
+ // may be 0. In this case, just return current timestamp.
+ // XXX Should we cache last event time?
+ return TimeStamp::Now();
+ }
+ // The internal value of the macOS implementation of TimeStamp is based on
+ // mach_absolute_time(), which measures "ticks" since boot.
+ // Event timestamps are NSTimeIntervals (seconds) since boot. So the two time
+ // representations already have the same base; we only need to convert
+ // seconds into ticks.
+ int64_t tick =
+ BaseTimeDurationPlatformUtils::TicksFromMilliseconds(aEventTime * 1000.0);
+ return TimeStamp::FromSystemTime(tick);
+}
+
+static NSString* ActionOnDoubleClickSystemPref() {
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ NSString* kAppleActionOnDoubleClickKey = @"AppleActionOnDoubleClick";
+ id value = [userDefaults objectForKey:kAppleActionOnDoubleClickKey];
+ if ([value isKindOfClass:[NSString class]]) {
+ return value;
+ }
+ return nil;
+}
+
+@interface NSWindow (NSWindowShouldZoomOnDoubleClick)
++ (BOOL)_shouldZoomOnDoubleClick; // present on 10.7 and above
+@end
+
+bool nsCocoaUtils::ShouldZoomOnTitlebarDoubleClick() {
+ if ([NSWindow respondsToSelector:@selector(_shouldZoomOnDoubleClick)]) {
+ return [NSWindow _shouldZoomOnDoubleClick];
+ }
+ return [ActionOnDoubleClickSystemPref() isEqualToString:@"Maximize"];
+}
+
+bool nsCocoaUtils::ShouldMinimizeOnTitlebarDoubleClick() {
+ // Check the system preferences.
+ // We could also check -[NSWindow _shouldMiniaturizeOnDoubleClick]. It's not
+ // clear to me which approach would be preferable; neither is public API.
+ return [ActionOnDoubleClickSystemPref() isEqualToString:@"Minimize"];
+}
+
+static const char* AVMediaTypeToString(AVMediaType aType) {
+ if (aType == AVMediaTypeVideo) {
+ return "video";
+ }
+
+ if (aType == AVMediaTypeAudio) {
+ return "audio";
+ }
+
+ return "unexpected type";
+}
+
+static void LogAuthorizationStatus(AVMediaType aType, int aState) {
+ const char* stateString;
+
+ switch (aState) {
+ case AVAuthorizationStatusAuthorized:
+ stateString = "AVAuthorizationStatusAuthorized";
+ break;
+ case AVAuthorizationStatusDenied:
+ stateString = "AVAuthorizationStatusDenied";
+ break;
+ case AVAuthorizationStatusNotDetermined:
+ stateString = "AVAuthorizationStatusNotDetermined";
+ break;
+ case AVAuthorizationStatusRestricted:
+ stateString = "AVAuthorizationStatusRestricted";
+ break;
+ default:
+ stateString = "Invalid state";
+ }
+
+ LOG("%s authorization status: %s\n", AVMediaTypeToString(aType), stateString);
+}
+
+static nsresult GetPermissionState(AVMediaType aMediaType, uint16_t& aState) {
+ MOZ_ASSERT(aMediaType == AVMediaTypeVideo || aMediaType == AVMediaTypeAudio);
+
+ AVAuthorizationStatus authStatus = static_cast<AVAuthorizationStatus>(
+ [AVCaptureDevice authorizationStatusForMediaType:aMediaType]);
+ LogAuthorizationStatus(aMediaType, authStatus);
+
+ // Convert AVAuthorizationStatus to nsIOSPermissionRequest const
+ switch (authStatus) {
+ case AVAuthorizationStatusAuthorized:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+ return NS_OK;
+ case AVAuthorizationStatusDenied:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
+ return NS_OK;
+ case AVAuthorizationStatusNotDetermined:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_NOTDETERMINED;
+ return NS_OK;
+ case AVAuthorizationStatusRestricted:
+ aState = nsIOSPermissionRequest::PERMISSION_STATE_RESTRICTED;
+ return NS_OK;
+ default:
+ MOZ_ASSERT(false, "Invalid authorization status");
+ return NS_ERROR_UNEXPECTED;
+ }
+}
+
+nsresult nsCocoaUtils::GetVideoCapturePermissionState(
+ uint16_t& aPermissionState) {
+ return GetPermissionState(AVMediaTypeVideo, aPermissionState);
+}
+
+nsresult nsCocoaUtils::GetAudioCapturePermissionState(
+ uint16_t& aPermissionState) {
+ return GetPermissionState(AVMediaTypeAudio, aPermissionState);
+}
+
+// Set |aPermissionState| to PERMISSION_STATE_AUTHORIZED if this application
+// has already been granted permission to record the screen in macOS Security
+// and Privacy system settings. If we do not have permission (because the user
+// hasn't yet been asked yet or the user previously denied the prompt), use
+// PERMISSION_STATE_DENIED. Returns NS_ERROR_NOT_IMPLEMENTED on macOS 10.14
+// and earlier.
+nsresult nsCocoaUtils::GetScreenCapturePermissionState(
+ uint16_t& aPermissionState) {
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_NOTDETERMINED;
+
+ if (!StaticPrefs::media_macos_screenrecording_oscheck_enabled()) {
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+ LOG("screen authorization status: authorized (test disabled via pref)");
+ return NS_OK;
+ }
+
+ // Unlike with camera and microphone capture, there is no support for
+ // checking the screen recording permission status. Instead, an application
+ // can use the presence of window names (which are privacy sensitive) in
+ // the window info list as an indication. The list only includes window
+ // names if the calling application has been authorized to record the
+ // screen. We use the window name, window level, and owning PID as
+ // heuristics to determine if we have screen recording permission.
+ AutoCFRelease<CFArrayRef> windowArray =
+ CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
+ if (!windowArray) {
+ LOG("GetScreenCapturePermissionState() ERROR: got NULL window info list");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ int32_t windowLevelDock = CGWindowLevelForKey(kCGDockWindowLevelKey);
+ int32_t windowLevelNormal = CGWindowLevelForKey(kCGNormalWindowLevelKey);
+ LOG("GetScreenCapturePermissionState(): DockWindowLevel: %d, "
+ "NormalWindowLevel: %d",
+ windowLevelDock, windowLevelNormal);
+
+ int32_t thisPid = [[NSProcessInfo processInfo] processIdentifier];
+
+ CFIndex windowCount = CFArrayGetCount(windowArray);
+ LOG("GetScreenCapturePermissionState() returned %ld windows", windowCount);
+ if (windowCount == 0) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ for (CFIndex i = 0; i < windowCount; i++) {
+ CFDictionaryRef windowDict = reinterpret_cast<CFDictionaryRef>(
+ CFArrayGetValueAtIndex(windowArray, i));
+
+ // Get the window owner's PID
+ int32_t windowOwnerPid = -1;
+ CFNumberRef windowPidRef = reinterpret_cast<CFNumberRef>(
+ CFDictionaryGetValue(windowDict, kCGWindowOwnerPID));
+ if (!windowPidRef ||
+ !CFNumberGetValue(windowPidRef, kCFNumberIntType, &windowOwnerPid)) {
+ LOG("GetScreenCapturePermissionState() ERROR: failed to get window "
+ "owner");
+ continue;
+ }
+
+ // Our own window names are always readable and
+ // therefore not relevant to the heuristic.
+ if (thisPid == windowOwnerPid) {
+ continue;
+ }
+
+ CFStringRef windowName = reinterpret_cast<CFStringRef>(
+ CFDictionaryGetValue(windowDict, kCGWindowName));
+ if (!windowName) {
+ continue;
+ }
+
+ CFNumberRef windowLayerRef = reinterpret_cast<CFNumberRef>(
+ CFDictionaryGetValue(windowDict, kCGWindowLayer));
+ int32_t windowLayer;
+ if (!windowLayerRef ||
+ !CFNumberGetValue(windowLayerRef, kCFNumberIntType, &windowLayer)) {
+ LOG("GetScreenCapturePermissionState() ERROR: failed to get layer");
+ continue;
+ }
+
+ // If we have a window name and the window is in the dock or normal window
+ // level, and for another process, assume we have screen recording access.
+ LOG("GetScreenCapturePermissionState(): windowLayer: %d", windowLayer);
+ if (windowLayer == windowLevelDock || windowLayer == windowLevelNormal) {
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+ LOG("screen authorization status: authorized");
+ return NS_OK;
+ }
+ }
+
+ aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
+ LOG("screen authorization status: not authorized");
+ return NS_OK;
+}
+
+nsresult nsCocoaUtils::RequestVideoCapturePermission(
+ RefPtr<Promise>& aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return nsCocoaUtils::RequestCapturePermission(AVMediaTypeVideo, aPromise,
+ sVideoCapturePromises,
+ VideoCompletionHandler);
+}
+
+nsresult nsCocoaUtils::RequestAudioCapturePermission(
+ RefPtr<Promise>& aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return nsCocoaUtils::RequestCapturePermission(AVMediaTypeAudio, aPromise,
+ sAudioCapturePromises,
+ AudioCompletionHandler);
+}
+
+//
+// Stores |aPromise| on |aPromiseList| and starts an asynchronous media
+// capture request for the given media type |aType|. If we are already
+// waiting for a capture request for this media type, don't start a new
+// request. |aHandler| is invoked on an arbitrary dispatch queue when the
+// request completes and must resolve any waiting Promises on the main
+// thread.
+//
+nsresult nsCocoaUtils::RequestCapturePermission(
+ AVMediaType aType, RefPtr<Promise>& aPromise, PromiseArray& aPromiseList,
+ void (^aHandler)(BOOL granted)) {
+ MOZ_ASSERT(aType == AVMediaTypeVideo || aType == AVMediaTypeAudio);
+ LOG("RequestCapturePermission(%s)", AVMediaTypeToString(aType));
+
+ sMediaCaptureMutex.Lock();
+
+ // Initialize our list of promises on first invocation
+ if (aPromiseList == nullptr) {
+ aPromiseList = new nsTArray<RefPtr<Promise>>;
+ ClearOnShutdown(&aPromiseList);
+ }
+
+ aPromiseList->AppendElement(aPromise);
+ size_t nPromises = aPromiseList->Length();
+
+ sMediaCaptureMutex.Unlock();
+
+ LOG("RequestCapturePermission(%s): %ld promise(s) unresolved",
+ AVMediaTypeToString(aType), nPromises);
+
+ // If we had one or more more existing promises waiting to be resolved
+ // by the completion handler, we don't need to start another request.
+ if (nPromises > 1) {
+ return NS_OK;
+ }
+
+ // Start the request
+ [AVCaptureDevice requestAccessForMediaType:aType completionHandler:aHandler];
+ return NS_OK;
+}
+
+//
+// Audio capture request completion handler. Called from an arbitrary
+// dispatch queue.
+//
+void (^nsCocoaUtils::AudioCompletionHandler)(BOOL) = ^void(BOOL granted) {
+ nsCocoaUtils::ResolveAudioCapturePromises(granted);
+};
+
+//
+// Video capture request completion handler. Called from an arbitrary
+// dispatch queue.
+//
+void (^nsCocoaUtils::VideoCompletionHandler)(BOOL) = ^void(BOOL granted) {
+ nsCocoaUtils::ResolveVideoCapturePromises(granted);
+};
+
+void nsCocoaUtils::ResolveMediaCapturePromises(bool aGranted,
+ PromiseArray& aPromiseList) {
+ StaticMutexAutoLock lock(sMediaCaptureMutex);
+
+ // Remove each promise from the list and resolve it.
+ while (aPromiseList->Length() > 0) {
+ RefPtr<Promise> promise = aPromiseList->PopLastElement();
+
+ // Resolve on main thread
+ nsCOMPtr<nsIRunnable> runnable(
+ NS_NewRunnableFunction("ResolveMediaAccessPromise",
+ [aGranted, aPromise = std::move(promise)]() {
+ aPromise->MaybeResolve(aGranted);
+ }));
+ NS_DispatchToMainThread(runnable.forget());
+ }
+}
+
+void nsCocoaUtils::ResolveAudioCapturePromises(bool aGranted) {
+ // Resolve on main thread
+ nsCOMPtr<nsIRunnable> runnable(
+ NS_NewRunnableFunction("ResolveAudioCapturePromise", [aGranted]() {
+ ResolveMediaCapturePromises(aGranted, sAudioCapturePromises);
+ }));
+ NS_DispatchToMainThread(runnable.forget());
+}
+
+//
+// Attempt to trigger a dialog requesting permission to record the screen.
+// Unlike with the camera and microphone, there is no API to request permission
+// to record the screen or to receive a callback when permission is explicitly
+// allowed or denied. Here we attempt to trigger the dialog by attempting to
+// capture a 1x1 pixel section of the screen. The permission dialog is not
+// guaranteed to be displayed because the user may have already been prompted
+// in which case macOS does not display the dialog again.
+//
+nsresult nsCocoaUtils::MaybeRequestScreenCapturePermission() {
+ LOG("MaybeRequestScreenCapturePermission()");
+ AutoCFRelease<CGImageRef> image =
+ CGDisplayCreateImageForRect(kCGDirectMainDisplay, CGRectMake(0, 0, 1, 1));
+ return NS_OK;
+}
+
+void nsCocoaUtils::ResolveVideoCapturePromises(bool aGranted) {
+ // Resolve on main thread
+ nsCOMPtr<nsIRunnable> runnable(
+ NS_NewRunnableFunction("ResolveVideoCapturePromise", [aGranted]() {
+ ResolveMediaCapturePromises(aGranted, sVideoCapturePromises);
+ }));
+ NS_DispatchToMainThread(runnable.forget());
+}
+
+static PanGestureInput::PanGestureType PanGestureTypeForEvent(NSEvent* aEvent) {
+ switch ([aEvent phase]) {
+ case NSEventPhaseMayBegin:
+ return PanGestureInput::PANGESTURE_MAYSTART;
+ case NSEventPhaseCancelled:
+ return PanGestureInput::PANGESTURE_CANCELLED;
+ case NSEventPhaseBegan:
+ return PanGestureInput::PANGESTURE_START;
+ case NSEventPhaseChanged:
+ return PanGestureInput::PANGESTURE_PAN;
+ case NSEventPhaseEnded:
+ return PanGestureInput::PANGESTURE_END;
+ case NSEventPhaseNone:
+ switch ([aEvent momentumPhase]) {
+ case NSEventPhaseBegan:
+ return PanGestureInput::PANGESTURE_MOMENTUMSTART;
+ case NSEventPhaseChanged:
+ return PanGestureInput::PANGESTURE_MOMENTUMPAN;
+ case NSEventPhaseEnded:
+ return PanGestureInput::PANGESTURE_MOMENTUMEND;
+ default:
+ NS_ERROR("unexpected event phase");
+ return PanGestureInput::PANGESTURE_PAN;
+ }
+ default:
+ NS_ERROR("unexpected event phase");
+ return PanGestureInput::PANGESTURE_PAN;
+ }
+}
+
+bool static ShouldConsiderStartingSwipeFromEvent(NSEvent* anEvent) {
+ // Only initiate horizontal tracking for gestures that have just begun --
+ // otherwise a scroll to one side of the page can have a swipe tacked on
+ // to it.
+ // [NSEvent isSwipeTrackingFromScrollEventsEnabled] checks whether the
+ // AppleEnableSwipeNavigateWithScrolls global preference is set. If it isn't,
+ // fluid swipe tracking is disabled, and a horizontal two-finger gesture is
+ // always a scroll (even in Safari). This preference can't (currently) be set
+ // from the Preferences UI -- only using 'defaults write'.
+ NSEventPhase eventPhase = [anEvent phase];
+ return [anEvent type] == NSEventTypeScrollWheel &&
+ eventPhase == NSEventPhaseBegan &&
+ [anEvent hasPreciseScrollingDeltas] &&
+ [NSEvent isSwipeTrackingFromScrollEventsEnabled];
+}
+
+PanGestureInput nsCocoaUtils::CreatePanGestureEvent(
+ NSEvent* aNativeEvent, TimeStamp aTimeStamp,
+ const ScreenPoint& aPanStartPoint, const ScreenPoint& aPreciseDelta,
+ const gfx::IntPoint& aLineOrPageDelta, Modifiers aModifiers) {
+ PanGestureInput::PanGestureType type = PanGestureTypeForEvent(aNativeEvent);
+ // Always force zero deltas on event types that shouldn't cause any scrolling,
+ // so that we don't dispatch DOM wheel events for them.
+ bool shouldIgnoreDeltas = type == PanGestureInput::PANGESTURE_MAYSTART ||
+ type == PanGestureInput::PANGESTURE_CANCELLED;
+
+ PanGestureInput panEvent(
+ type, aTimeStamp, aPanStartPoint,
+ !shouldIgnoreDeltas ? aPreciseDelta : ScreenPoint(), aModifiers,
+ PanGestureInput::IsEligibleForSwipe(
+ ShouldConsiderStartingSwipeFromEvent(aNativeEvent)));
+
+ if (!shouldIgnoreDeltas) {
+ panEvent.SetLineOrPageDeltas(aLineOrPageDelta.x, aLineOrPageDelta.y);
+ }
+
+ return panEvent;
+}
+
+bool nsCocoaUtils::IsValidPasteboardType(NSString* aAvailableType,
+ bool aAllowFileURL) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Prevent exposing fileURL for non-fileURL type.
+ // We need URL provided by dropped webloc file, but don't need file's URL.
+ // kUTTypeFileURL is returned by [NSPasteboard availableTypeFromArray:] for
+ // kPublicUrlPboardType, since it conforms to kPublicUrlPboardType.
+ bool isValid = true;
+ if (!aAllowFileURL &&
+ [aAvailableType
+ isEqualToString:[UTIHelper
+ stringFromPboardType:(NSString*)
+ kUTTypeFileURL]]) {
+ isValid = false;
+ }
+
+ return isValid;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+NSString* nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ NSPasteboardItem* aItem, const NSString* aType, bool aAllowFileURL) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* availableType =
+ [aItem availableTypeFromArray:[NSArray arrayWithObjects:(id)aType, nil]];
+ if (availableType && IsValidPasteboardType(availableType, aAllowFileURL)) {
+ return [aItem stringForType:(id)availableType];
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NSString* nsCocoaUtils::GetFilePathFromPasteboardItem(NSPasteboardItem* aItem) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* urlString = GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL], true);
+ if (urlString) {
+ NSURL* url = [NSURL URLWithString:urlString];
+ if (url) {
+ return [url path];
+ }
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NSString* nsCocoaUtils::GetTitleForURLFromPasteboardItem(
+ NSPasteboardItem* item) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* name = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ item, [UTIHelper stringFromPboardType:kPublicUrlNamePboardType]);
+ if (name) {
+ return name;
+ }
+
+ NSString* filePath = nsCocoaUtils::GetFilePathFromPasteboardItem(item);
+ if (filePath) {
+ return [filePath lastPathComponent];
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+void nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(
+ nsITransferable* aTransferable, const nsCString& aFlavor,
+ NSPasteboardItem* aItem) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!aTransferable || !aItem) {
+ return;
+ }
+
+ MOZ_LOG(gCocoaUtilsLog, LogLevel::Info,
+ ("nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem: looking "
+ "for pasteboard data of "
+ "type %s\n",
+ aFlavor.get()));
+
+ if (aFlavor.EqualsLiteral(kFileMime)) {
+ NSString* filePath = nsCocoaUtils::GetFilePathFromPasteboardItem(aItem);
+ if (!filePath) {
+ return;
+ }
+
+ unsigned int stringLength = [filePath length];
+ unsigned int dataLength =
+ (stringLength + 1) * sizeof(char16_t); // in bytes
+ char16_t* clipboardDataPtr = (char16_t*)malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return;
+ }
+
+ [filePath getCharacters:reinterpret_cast<unichar*>(clipboardDataPtr)];
+ clipboardDataPtr[stringLength] = 0; // null terminate
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(nsDependentString(clipboardDataPtr), true,
+ getter_AddRefs(file));
+ free(clipboardDataPtr);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ aTransferable->SetTransferData(aFlavor.get(), file);
+ return;
+ }
+
+ if (aFlavor.EqualsLiteral(kCustomTypesMime)) {
+ NSString* availableType = [aItem
+ availableTypeFromArray:[NSArray
+ arrayWithObject:kMozCustomTypesPboardType]];
+ if (!availableType ||
+ !nsCocoaUtils::IsValidPasteboardType(availableType, false)) {
+ return;
+ }
+ NSData* pasteboardData = [aItem dataForType:availableType];
+ if (!pasteboardData) {
+ return;
+ }
+
+ unsigned int dataLength = [pasteboardData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return;
+ }
+ [pasteboardData getBytes:clipboardDataPtr length:dataLength];
+
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ aFlavor, clipboardDataPtr, dataLength,
+ getter_AddRefs(genericDataWrapper));
+
+ aTransferable->SetTransferData(aFlavor.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ return;
+ }
+
+ NSString* pString = nil;
+ if (aFlavor.EqualsLiteral(kTextMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:NSPasteboardTypeString]);
+ } else if (aFlavor.EqualsLiteral(kHTMLMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:NSPasteboardTypeHTML]);
+ } else if (aFlavor.EqualsLiteral(kURLMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:kPublicUrlPboardType]);
+ if (pString) {
+ NSString* title = GetTitleForURLFromPasteboardItem(aItem);
+ if (!title) {
+ title = pString;
+ }
+ pString = [NSString stringWithFormat:@"%@\n%@", pString, title];
+ }
+ } else if (aFlavor.EqualsLiteral(kURLDataMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:kPublicUrlPboardType]);
+ } else if (aFlavor.EqualsLiteral(kURLDescriptionMime)) {
+ pString = GetTitleForURLFromPasteboardItem(aItem);
+ } else if (aFlavor.EqualsLiteral(kRTFMime)) {
+ pString = nsCocoaUtils::GetStringForTypeFromPasteboardItem(
+ aItem, [UTIHelper stringFromPboardType:NSPasteboardTypeRTF]);
+ }
+ if (pString) {
+ NSData* stringData;
+ bool isRTF = aFlavor.EqualsLiteral(kRTFMime);
+ if (isRTF) {
+ stringData = [pString dataUsingEncoding:NSASCIIStringEncoding];
+ } else {
+ stringData = [pString dataUsingEncoding:NSUnicodeStringEncoding];
+ }
+ unsigned int dataLength = [stringData length];
+ void* clipboardDataPtr = malloc(dataLength);
+ if (!clipboardDataPtr) {
+ return;
+ }
+ [stringData getBytes:clipboardDataPtr length:dataLength];
+
+ // The DOM only wants LF, so convert from MacOS line endings to DOM line
+ // endings.
+ int32_t signedDataLength = dataLength;
+ nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(isRTF, &clipboardDataPtr,
+ &signedDataLength);
+ dataLength = signedDataLength;
+
+ // skip BOM (Byte Order Mark to distinguish little or big endian)
+ char16_t* clipboardDataPtrNoBOM = (char16_t*)clipboardDataPtr;
+ if ((dataLength > 2) && ((clipboardDataPtrNoBOM[0] == 0xFEFF) ||
+ (clipboardDataPtrNoBOM[0] == 0xFFFE))) {
+ dataLength -= sizeof(char16_t);
+ clipboardDataPtrNoBOM += 1;
+ }
+
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ aFlavor, clipboardDataPtrNoBOM, dataLength,
+ getter_AddRefs(genericDataWrapper));
+ aTransferable->SetTransferData(aFlavor.get(), genericDataWrapper);
+ free(clipboardDataPtr);
+ return;
+ }
+
+ // We have never supported this on Mac OS X, we should someday. Normally
+ // dragging images in is accomplished with a file path drag instead of the
+ // image data itself.
+ /*
+ if (aFlavor.EqualsLiteral(kPNGImageMime) ||
+ aFlavor.EqualsLiteral(kJPEGImageMime) || aFlavor.EqualsLiteral(kJPGImageMime)
+ || aFlavor.EqualsLiteral(kGIFImageMime)) {
+
+ }
+ */
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h
new file mode 100644
index 0000000000..c2e595677f
--- /dev/null
+++ b/widget/cocoa/nsCocoaWindow.h
@@ -0,0 +1,515 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsCocoaWindow_h_
+#define nsCocoaWindow_h_
+
+#undef DARWIN
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/RefPtr.h"
+#include "nsBaseWidget.h"
+#include "nsPIWidgetCocoa.h"
+#include "nsCocoaUtils.h"
+#include "nsTouchBar.h"
+#include <dlfcn.h>
+#include <queue>
+
+class nsCocoaWindow;
+class nsChildView;
+class nsMenuBarX;
+@class ChildView;
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+} // namespace mozilla
+
+typedef struct _nsCocoaWindowList {
+ _nsCocoaWindowList() : prev(nullptr), window(nullptr) {}
+ struct _nsCocoaWindowList* prev;
+ nsCocoaWindow* window; // Weak
+} nsCocoaWindowList;
+
+// NSWindow subclass that is the base class for all of our own window classes.
+// Among other things, this class handles the storage of those settings that
+// need to be persisted across window destruction and reconstruction, i.e. when
+// switching to and from fullscreen mode.
+// We don't save shadow, transparency mode or background color because it's not
+// worth the hassle - Gecko will reset them anyway as soon as the window is
+// resized.
+@interface BaseWindow : NSWindow {
+ // Data Storage
+ NSMutableDictionary* mState;
+ BOOL mDrawsIntoWindowFrame;
+
+ // Invalidation disabling
+ BOOL mDisabledNeedsDisplay;
+
+ NSTrackingArea* mTrackingArea;
+
+ NSRect mDirtyRect;
+
+ BOOL mBeingShown;
+ BOOL mDrawTitle;
+ BOOL mIsAnimationSuppressed;
+
+ nsTouchBar* mTouchBar;
+}
+
+- (void)importState:(NSDictionary*)aState;
+- (NSMutableDictionary*)exportState;
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState;
+- (BOOL)drawsContentsIntoWindowFrame;
+
+// These two methods are like contentRectForFrameRect and
+// frameRectForContentRect, but they deal with the rect of the window's "main
+// ChildView" instead of the rect of the window's content view. The two are
+// sometimes sized differently: The window's content view always covers the
+// entire window, whereas the ChildView only covers the full window when
+// drawsContentsIntoWindowFrame is YES. When drawsContentsIntoWindowFrame is NO,
+// there's a titlebar-sized gap above the ChildView within the content view.
+- (NSRect)childViewRectForFrameRect:(NSRect)aFrameRect;
+- (NSRect)frameRectForChildViewRect:(NSRect)aChildViewRect;
+
+- (void)mouseEntered:(NSEvent*)aEvent;
+- (void)mouseExited:(NSEvent*)aEvent;
+- (void)mouseMoved:(NSEvent*)aEvent;
+- (void)updateTrackingArea;
+- (NSView*)trackingAreaView;
+
+- (void)setBeingShown:(BOOL)aValue;
+- (BOOL)isBeingShown;
+- (BOOL)isVisibleOrBeingShown;
+
+- (void)setIsAnimationSuppressed:(BOOL)aValue;
+- (BOOL)isAnimationSuppressed;
+
+// Returns an autoreleased NSArray containing the NSViews that we consider the
+// "contents" of this window. All views in the returned array are subviews of
+// this window's content view. However, the array may not include all of the
+// content view's subviews; concretely, the ToolbarWindow implementation will
+// exclude its MOZTitlebarView from the array that is returned here.
+// In the vast majority of cases, the array will only have a single element:
+// this window's mainChildView.
+- (NSArray<NSView*>*)contentViewContents;
+
+- (ChildView*)mainChildView;
+
+- (void)setWantsTitleDrawn:(BOOL)aDrawTitle;
+- (BOOL)wantsTitleDrawn;
+
+- (void)disableSetNeedsDisplay;
+- (void)enableSetNeedsDisplay;
+
+- (NSRect)getAndResetNativeDirtyRect;
+
+- (void)setEffectViewWrapperForStyle:(mozilla::WindowShadow)aStyle;
+@property(nonatomic) mozilla::WindowShadow shadowStyle;
+
+- (void)releaseJSObjects;
+
+@end
+
+@interface NSWindow (Undocumented)
+- (NSDictionary*)shadowParameters;
+
+// Present in the same form on OS X since at least OS X 10.5.
+- (NSRect)contentRectForFrameRect:(NSRect)windowFrame
+ styleMask:(NSUInteger)windowStyle;
+- (NSRect)frameRectForContentRect:(NSRect)windowContentRect
+ styleMask:(NSUInteger)windowStyle;
+
+// Present since at least OS X 10.5. The OS calls this method on NSWindow
+// (and its subclasses) to find out which NSFrameView subclass to instantiate
+// to create its "frame view".
++ (Class)frameViewClassForStyleMask:(NSUInteger)styleMask;
+
+@end
+
+@interface PopupWindow : BaseWindow {
+ @private
+ BOOL mIsContextMenu;
+}
+
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(NSUInteger)styleMask
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)deferCreation;
+- (BOOL)isContextMenu;
+- (void)setIsContextMenu:(BOOL)flag;
+- (BOOL)canBecomeMainWindow;
+
+@end
+
+@interface BorderlessWindow : BaseWindow {
+}
+
+- (BOOL)canBecomeKeyWindow;
+- (BOOL)canBecomeMainWindow;
+
+@end
+
+@interface WindowDelegate : NSObject <NSWindowDelegate> {
+ nsCocoaWindow* mGeckoWindow; // [WEAK] (we are owned by the window)
+ // Used to avoid duplication when we send NS_ACTIVATE and
+ // NS_DEACTIVATE to Gecko for toplevel widgets. Starts out
+ // false.
+ bool mToplevelActiveState;
+ BOOL mHasEverBeenZoomed;
+}
++ (void)paintMenubarForWindow:(NSWindow*)aWindow;
+- (id)initWithGeckoWindow:(nsCocoaWindow*)geckoWind;
+- (void)windowDidResize:(NSNotification*)aNotification;
+- (nsCocoaWindow*)geckoWidget;
+- (bool)toplevelActiveState;
+- (void)sendToplevelActivateEvents;
+- (void)sendToplevelDeactivateEvents;
+@end
+
+@interface MOZTitlebarView : NSVisualEffectView
+@end
+
+@interface FullscreenTitlebarTracker : NSTitlebarAccessoryViewController
+- (FullscreenTitlebarTracker*)init;
+@end
+
+// NSWindow subclass for handling windows with toolbars.
+@interface ToolbarWindow : BaseWindow {
+ // This window's titlebar view, if present.
+ // Will be nil if the window has neither a titlebar nor a unified toolbar.
+ // This view is a subview of the window's content view and gets created and
+ // destroyed by updateTitlebarView.
+ MOZTitlebarView* mTitlebarView; // [STRONG]
+ // mFullscreenTitlebarTracker attaches an invisible rectangle to the system
+ // title bar. This allows us to detect when the title bar is showing in
+ // fullscreen.
+ FullscreenTitlebarTracker* mFullscreenTitlebarTracker;
+
+ CGFloat mUnifiedToolbarHeight;
+ CGFloat mSheetAttachmentPosition;
+ CGFloat mMenuBarHeight;
+ /* Store the height of the titlebar when this window is initialized. The
+ titlebarHeight getter returns 0 when in fullscreen, which is not useful in
+ some cases. */
+ CGFloat mInitialTitlebarHeight;
+ NSRect mWindowButtonsRect;
+}
+- (void)setUnifiedToolbarHeight:(CGFloat)aHeight;
+- (CGFloat)unifiedToolbarHeight;
+- (CGFloat)titlebarHeight;
+- (NSRect)titlebarRect;
+- (void)setTitlebarNeedsDisplay;
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState;
+- (void)setSheetAttachmentPosition:(CGFloat)aY;
+- (CGFloat)sheetAttachmentPosition;
+- (void)placeWindowButtons:(NSRect)aRect;
+- (NSRect)windowButtonsRect;
+- (void)windowMainStateChanged;
+@end
+
+class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa {
+ private:
+ typedef nsBaseWidget Inherited;
+
+ public:
+ nsCocoaWindow();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSPIWIDGETCOCOA; // semicolon for clang-format bug 1629756
+
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const DesktopIntRect& aRect,
+ InitData* = nullptr) override;
+
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* = nullptr) override;
+
+ virtual void Destroy() override;
+
+ virtual void Show(bool aState) override;
+ virtual bool NeedsRecreateToReshow() override;
+
+ virtual nsIWidget* GetSheetWindowParent(void) override;
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+ virtual void SetModal(bool aState) override;
+ virtual void SetFakeModal(bool aState) override;
+ virtual bool IsRunningAppModal() override;
+ virtual bool IsVisible() const override;
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ virtual LayoutDeviceIntPoint GetClientOffset() override;
+ virtual LayoutDeviceIntMargin ClientToWindowMargin() override;
+
+ virtual void* GetNativeData(uint32_t aDataType) override;
+
+ virtual void ConstrainPosition(DesktopIntPoint&) override;
+ virtual void SetSizeConstraints(const SizeConstraints& aConstraints) override;
+ virtual void Move(double aX, double aY) override;
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override;
+ virtual void GetWorkspaceID(nsAString& workspaceID) override;
+ virtual void MoveToWorkspace(const nsAString& workspaceID) override;
+ virtual void SuppressAnimation(bool aSuppress) override;
+ virtual void HideWindowChrome(bool aShouldHide) override;
+
+ virtual bool PrepareForFullscreenTransition(nsISupports** aData) override;
+ virtual void PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration,
+ nsISupports* aData,
+ nsIRunnable* aCallback) override;
+ virtual void CleanupFullscreenTransition() override;
+ nsresult MakeFullScreen(bool aFullScreen) final;
+ nsresult MakeFullScreenWithNativeTransition(bool aFullScreen) final;
+ NSAnimation* FullscreenTransitionAnimation() const {
+ return mFullscreenTransitionAnimation;
+ }
+ void ReleaseFullscreenTransitionAnimation() {
+ MOZ_ASSERT(mFullscreenTransitionAnimation,
+ "Should only be called when there is animation");
+ [mFullscreenTransitionAnimation release];
+ mFullscreenTransitionAnimation = nil;
+ }
+
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+ NSRect GetClientCocoaRect();
+ virtual LayoutDeviceIntRect GetClientBounds() override;
+ virtual LayoutDeviceIntRect GetScreenBounds() override;
+ void ReportMoveEvent();
+ void ReportSizeEvent();
+ virtual void SetCursor(const Cursor&) override;
+
+ CGFloat BackingScaleFactor();
+ void BackingScaleFactorChanged();
+ virtual double GetDefaultScaleInternal() override;
+ virtual int32_t RoundsWidgetCoordinatesTo() override;
+
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() final {
+ return mozilla::DesktopToLayoutDeviceScale(BackingScaleFactor());
+ }
+
+ virtual nsresult SetTitle(const nsAString& aTitle) override;
+
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ virtual WindowRenderer* GetWindowRenderer() override;
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+ virtual void CaptureRollupEvents(bool aDoCapture) override;
+ [[nodiscard]] virtual nsresult GetAttention(int32_t aCycleCount) override;
+ virtual bool HasPendingInputEvent() override;
+ virtual TransparencyMode GetTransparencyMode() override;
+ virtual void SetTransparencyMode(TransparencyMode aMode) override;
+ virtual void SetWindowShadowStyle(mozilla::WindowShadow aStyle) override;
+ virtual void SetWindowOpacity(float aOpacity) override;
+ virtual void SetWindowTransform(
+ const mozilla::gfx::Matrix& aTransform) override;
+ virtual void SetInputRegion(const InputRegion&) override;
+ virtual void SetColorScheme(
+ const mozilla::Maybe<mozilla::ColorScheme>&) override;
+ virtual void SetShowsToolbarButton(bool aShow) override;
+ virtual void SetSupportsNativeFullscreen(bool aShow) override;
+ virtual void SetWindowAnimationType(WindowAnimationType aType) override;
+ virtual void SetDrawsTitle(bool aDrawTitle) override;
+ virtual nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) override;
+ void SetDrawsInTitlebar(bool aState);
+ virtual void UpdateThemeGeometries(
+ const nsTArray<ThemeGeometry>& aThemeGeometries) override;
+ virtual nsresult SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override;
+ virtual void LockAspectRatio(bool aShouldLock) override;
+
+ void DispatchSizeModeEvent();
+ void DispatchOcclusionEvent();
+
+ // be notified that a some form of drag event needs to go into Gecko
+ virtual bool DragEvent(unsigned int aMessage,
+ mozilla::gfx::Point aMouseGlobal,
+ UInt16 aKeyModifiers);
+
+ bool HasModalDescendents() { return mNumModalDescendents > 0; }
+ NSWindow* GetCocoaWindow() { return mWindow; }
+
+ void SetMenuBar(RefPtr<nsMenuBarX>&& aMenuBar);
+ nsMenuBarX* GetMenuBar();
+
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ virtual InputContext GetInputContext() override { return mInputContext; }
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ mozilla::NativeKeyBindingsType aType,
+ const mozilla::WidgetKeyboardEvent& aEvent,
+ nsTArray<mozilla::CommandInt>& aCommands) override;
+
+ void SetPopupWindowLevel();
+
+ bool InFullScreenMode() const { return mInFullScreenMode; }
+
+ void PauseOrResumeCompositor(bool aPause) override;
+
+ bool AsyncPanZoomEnabled() const override;
+
+ bool StartAsyncAutoscroll(const ScreenPoint& aAnchorLocation,
+ const ScrollableLayerGuid& aGuid) override;
+ void StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ // Class method versions of NSWindow/Delegate callbacks which need to
+ // access object state.
+ void CocoaWindowWillEnterFullscreen(bool aFullscreen);
+ void CocoaWindowDidEnterFullscreen(bool aFullscreen);
+ void CocoaWindowDidResize();
+ void CocoaSendToplevelActivateEvents();
+ void CocoaSendToplevelDeactivateEvents();
+
+ enum class TransitionType {
+ Windowed,
+ Fullscreen,
+ EmulatedFullscreen,
+ Miniaturize,
+ Deminiaturize,
+ Zoom,
+ };
+ void FinishCurrentTransitionIfMatching(const TransitionType& aTransition);
+
+ // Called when something has happened that might cause us to update our
+ // fullscreen state. Returns true if we updated state. We'll call this
+ // on window resize, and we'll call it when we enter or exit fullscreen,
+ // since fullscreen to-and-from zoomed windows won't necessarily trigger
+ // a resize.
+ bool HandleUpdateFullscreenOnResize();
+
+ protected:
+ virtual ~nsCocoaWindow();
+
+ nsresult CreateNativeWindow(const NSRect& aRect, BorderStyle aBorderStyle,
+ bool aRectIsFrameRect, bool aIsPrivateBrowsing);
+ nsresult CreatePopupContentView(const LayoutDeviceIntRect& aRect, InitData*);
+ void DestroyNativeWindow();
+ void UpdateBounds();
+ int32_t GetWorkspaceID();
+
+ void DoResize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint, bool aConstrainToCurrentScreen);
+
+ void UpdateFullscreenState(bool aFullScreen, bool aNativeMode);
+ nsresult DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition);
+
+ virtual already_AddRefed<nsIWidget> AllocateChildPopupWidget() override {
+ return nsIWidget::CreateTopLevelWindow();
+ }
+
+ nsIWidget* mParent; // if we're a popup, this is our parent [WEAK]
+ nsIWidget* mAncestorLink; // link to traverse ancestors [WEAK]
+ BaseWindow* mWindow; // our cocoa window [STRONG]
+ WindowDelegate*
+ mDelegate; // our delegate for processing window msgs [STRONG]
+ RefPtr<nsMenuBarX> mMenuBar;
+ NSWindow* mSheetWindowParent; // if this is a sheet, this is the NSWindow
+ // it's attached to
+ nsChildView*
+ mPopupContentView; // if this is a popup, this is its content widget
+ // if this is a toplevel window, and there is any ongoing fullscreen
+ // transition, it is the animation object.
+ NSAnimation* mFullscreenTransitionAnimation;
+ mozilla::WindowShadow mShadowStyle;
+
+ CGFloat mBackingScaleFactor;
+ CGFloat mAspectRatio;
+
+ WindowAnimationType mAnimationType;
+
+ bool mWindowMadeHere; // true if we created the window, false for embedding
+ bool mSheetNeedsShow; // if this is a sheet, are we waiting to be shown?
+ // this is used for sibling sheet contention only
+ nsSizeMode mSizeMode;
+ bool mInFullScreenMode;
+ // Whether we are currently using native fullscreen. It could be false because
+ // we are in the emulated fullscreen where we do not use the native
+ // fullscreen.
+ bool mInNativeFullScreenMode;
+
+ mozilla::Maybe<TransitionType> mTransitionCurrent;
+ std::queue<TransitionType> mTransitionsPending;
+
+ // Sometimes we add a transition that wasn't requested by a caller. We do this
+ // to manage transitions between states that otherwise would be rejected by
+ // Cocoa. When we do this, it's useful to know when we are handling an added
+ // transition because we don't want to send size mode events when they
+ // execute.
+ bool mIsTransitionCurrentAdded = false;
+
+ // Whether we are treating the next resize as the start of a fullscreen
+ // transition. If we are, which direction are we going: Fullscreen or
+ // Windowed.
+ mozilla::Maybe<TransitionType> mUpdateFullscreenOnResize;
+
+ bool IsInTransition() { return mTransitionCurrent.isSome(); }
+ void QueueTransition(const TransitionType& aTransition);
+ void ProcessTransitions();
+
+ // Call this to stop all transition processing, which is useful during
+ // window closing and shutdown.
+ void CancelAllTransitions();
+
+ bool mInProcessTransitions = false;
+
+ // While running an emulated fullscreen transition, we want to suppress
+ // sending size mode events due to window resizing. We fix it up at the end
+ // when the transition is complete.
+ bool mSuppressSizeModeEvents = false;
+
+ // Ignore occlusion events caused by displaying the temporary fullscreen
+ // window during the fullscreen transition animation because only focused
+ // contexts are permitted to enter DOM fullscreen.
+ int mIgnoreOcclusionCount;
+
+ // Set to true when a native fullscreen transition is initiated -- either to
+ // or from fullscreen -- and set to false when it is complete. During this
+ // period, we presume the window is visible, which prevents us from sending
+ // unnecessary OcclusionStateChanged events.
+ bool mHasStartedNativeFullscreen;
+
+ bool mModal;
+ bool mFakeModal;
+
+ bool mIsAnimationSuppressed;
+
+ bool mInReportMoveEvent; // true if in a call to ReportMoveEvent().
+ bool mInResize; // true if in a call to DoResize().
+ bool mWindowTransformIsIdentity;
+ bool mAlwaysOnTop;
+ bool mAspectRatioLocked;
+
+ int32_t mNumModalDescendents;
+ InputContext mInputContext;
+ NSWindowAnimationBehavior mWindowAnimationBehavior;
+
+ private:
+ // This is class state for tracking which nsCocoaWindow, if any, is in the
+ // middle of a native fullscreen transition.
+ static nsCocoaWindow* sWindowInNativeTransition;
+
+ // This function returns true if the caller has been able to claim the sole
+ // permission to start a native transition. It must be followed by a call
+ // to EndOurNativeTransition() when the native transition is complete.
+ bool CanStartNativeTransition();
+ void EndOurNativeTransition();
+
+ // true if Show() has been called.
+ bool mWasShown;
+};
+
+#endif // nsCocoaWindow_h_
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
new file mode 100644
index 0000000000..e5abaa5a87
--- /dev/null
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -0,0 +1,4460 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "nsCocoaWindow.h"
+
+#include "AppearanceOverride.h"
+#include "NativeKeyBindings.h"
+#include "ScreenHelperCocoa.h"
+#include "TextInputHandler.h"
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsCOMPtr.h"
+#include "nsWidgetsCID.h"
+#include "nsIRollupListener.h"
+#include "nsChildView.h"
+#include "nsWindowMap.h"
+#include "nsAppShell.h"
+#include "nsIAppShellService.h"
+#include "nsIBaseWindow.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIAppWindow.h"
+#include "nsToolkit.h"
+#include "nsPIDOMWindow.h"
+#include "nsThreadUtils.h"
+#include "nsMenuBarX.h"
+#include "nsMenuUtilsX.h"
+#include "nsStyleConsts.h"
+#include "nsNativeThemeColors.h"
+#include "nsNativeThemeCocoa.h"
+#include "nsChildView.h"
+#include "nsCocoaFeatures.h"
+#include "nsIScreenManager.h"
+#include "nsIWidgetListener.h"
+#include "nsXULPopupManager.h"
+#include "VibrancyManager.h"
+#include "nsPresContext.h"
+#include "nsDocShell.h"
+
+#include "gfxPlatform.h"
+#include "qcms.h"
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/widget/Screen.h"
+#include <algorithm>
+
+namespace mozilla {
+namespace layers {
+class LayerManager;
+} // namespace layers
+} // namespace mozilla
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+using namespace mozilla;
+
+int32_t gXULModalLevel = 0;
+
+// In principle there should be only one app-modal window at any given time.
+// But sometimes, despite our best efforts, another window appears above the
+// current app-modal window. So we need to keep a linked list of app-modal
+// windows. (A non-sheet window that appears above an app-modal window is
+// also made app-modal.) See nsCocoaWindow::SetModal().
+nsCocoaWindowList* gGeckoAppModalWindowList = NULL;
+
+BOOL sTouchBarIsInitialized = NO;
+
+// defined in nsMenuBarX.mm
+extern NSMenu* sApplicationMenu; // Application menu shared by all menubars
+
+// defined in nsChildView.mm
+extern BOOL gSomeMenuBarPainted;
+
+extern "C" {
+// CGSPrivate.h
+typedef NSInteger CGSConnection;
+typedef NSUInteger CGSSpaceID;
+typedef NSInteger CGSWindow;
+typedef enum {
+ kCGSSpaceIncludesCurrent = 1 << 0,
+ kCGSSpaceIncludesOthers = 1 << 1,
+ kCGSSpaceIncludesUser = 1 << 2,
+
+ kCGSAllSpacesMask =
+ kCGSSpaceIncludesCurrent | kCGSSpaceIncludesOthers | kCGSSpaceIncludesUser
+} CGSSpaceMask;
+static NSString* const CGSSpaceIDKey = @"ManagedSpaceID";
+static NSString* const CGSSpacesKey = @"Spaces";
+extern CGSConnection _CGSDefaultConnection(void);
+extern CGError CGSSetWindowTransform(CGSConnection cid, CGSWindow wid,
+ CGAffineTransform transform);
+}
+
+#define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1"
+
+NS_IMPL_ISUPPORTS_INHERITED(nsCocoaWindow, Inherited, nsPIWidgetCocoa)
+
+// A note on testing to see if your object is a sheet...
+// |mWindowType == WindowType::Sheet| is true if your gecko nsIWidget is a sheet
+// widget - whether or not the sheet is showing. |[mWindow isSheet]| will return
+// true *only when the sheet is actually showing*. Choose your test wisely.
+
+static void RollUpPopups(nsIRollupListener::AllowAnimations aAllowAnimations =
+ nsIRollupListener::AllowAnimations::Yes) {
+ if (RefPtr pm = nsXULPopupManager::GetInstance()) {
+ pm->RollupTooltips();
+ }
+
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ if (!rollupListener) {
+ return;
+ }
+ if (rollupListener->RollupNativeMenu()) {
+ return;
+ }
+ nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
+ if (!rollupWidget) {
+ return;
+ }
+ nsIRollupListener::RollupOptions options{
+ 0, nsIRollupListener::FlushViews::Yes, nullptr, aAllowAnimations};
+ rollupListener->Rollup(options);
+}
+
+nsCocoaWindow::nsCocoaWindow()
+ : mParent(nullptr),
+ mAncestorLink(nullptr),
+ mWindow(nil),
+ mDelegate(nil),
+ mSheetWindowParent(nil),
+ mPopupContentView(nil),
+ mFullscreenTransitionAnimation(nil),
+ mShadowStyle(WindowShadow::None),
+ mBackingScaleFactor(0.0),
+ mAnimationType(nsIWidget::eGenericWindowAnimation),
+ mWindowMadeHere(false),
+ mSheetNeedsShow(false),
+ mSizeMode(nsSizeMode_Normal),
+ mInFullScreenMode(false),
+ mInNativeFullScreenMode(false),
+ mIgnoreOcclusionCount(0),
+ mHasStartedNativeFullscreen(false),
+ mModal(false),
+ mFakeModal(false),
+ mIsAnimationSuppressed(false),
+ mInReportMoveEvent(false),
+ mInResize(false),
+ mWindowTransformIsIdentity(true),
+ mAlwaysOnTop(false),
+ mAspectRatioLocked(false),
+ mNumModalDescendents(0),
+ mWindowAnimationBehavior(NSWindowAnimationBehaviorDefault),
+ mWasShown(false) {
+ // Disable automatic tabbing. We need to do this before we
+ // orderFront any of our windows.
+ [NSWindow setAllowsAutomaticWindowTabbing:NO];
+}
+
+void nsCocoaWindow::DestroyNativeWindow() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ MOZ_ASSERT(mWindowMadeHere,
+ "We shouldn't be trying to destroy a window we didn't create.");
+
+ // Clear our class state that is keyed off of mWindow. It's our last
+ // chance! This ensures that other nsCocoaWindow instances are not waiting
+ // for us to finish a native transition that will have no listener once
+ // we clear our delegate.
+ EndOurNativeTransition();
+
+ [mWindow releaseJSObjects];
+ // We want to unhook the delegate here because we don't want events
+ // sent to it after this object has been destroyed.
+ [mWindow setDelegate:nil];
+ [mWindow close];
+ mWindow = nil;
+ [mDelegate autorelease];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsCocoaWindow::~nsCocoaWindow() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Notify the children that we're gone. Popup windows (e.g. tooltips) can
+ // have nsChildView children. 'kid' is an nsChildView object if and only if
+ // its 'type' is 'WindowType::Child'.
+ // childView->ResetParent() can change our list of children while it's
+ // being iterated, so the way we iterate the list must allow for this.
+ for (nsIWidget* kid = mLastChild; kid;) {
+ WindowType kidType = kid->GetWindowType();
+ if (kidType == WindowType::Child) {
+ nsChildView* childView = static_cast<nsChildView*>(kid);
+ kid = kid->GetPrevSibling();
+ childView->ResetParent();
+ } else {
+ nsCocoaWindow* childWindow = static_cast<nsCocoaWindow*>(kid);
+ childWindow->mParent = nullptr;
+ childWindow->mAncestorLink = mAncestorLink;
+ kid = kid->GetPrevSibling();
+ }
+ }
+
+ if (mWindow && mWindowMadeHere) {
+ CancelAllTransitions();
+ DestroyNativeWindow();
+ }
+
+ NS_IF_RELEASE(mPopupContentView);
+
+ // Deal with the possiblity that we're being destroyed while running modal.
+ if (mModal) {
+ NS_WARNING("Widget destroyed while running modal!");
+ --gXULModalLevel;
+ NS_ASSERTION(gXULModalLevel >= 0, "Weirdness setting modality!");
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Find the screen that overlaps aRect the most,
+// if none are found default to the mainScreen.
+static NSScreen* FindTargetScreenForRect(const DesktopIntRect& aRect) {
+ NSScreen* targetScreen = [NSScreen mainScreen];
+ NSEnumerator* screenEnum = [[NSScreen screens] objectEnumerator];
+ int largestIntersectArea = 0;
+ while (NSScreen* screen = [screenEnum nextObject]) {
+ DesktopIntRect screenRect =
+ nsCocoaUtils::CocoaRectToGeckoRect([screen visibleFrame]);
+ screenRect = screenRect.Intersect(aRect);
+ int area = screenRect.width * screenRect.height;
+ if (area > largestIntersectArea) {
+ largestIntersectArea = area;
+ targetScreen = screen;
+ }
+ }
+ return targetScreen;
+}
+
+DesktopToLayoutDeviceScale ParentBackingScaleFactor(nsIWidget* aParent,
+ NSView* aParentView) {
+ if (aParent) {
+ return aParent->GetDesktopToDeviceScale();
+ }
+ NSWindow* parentWindow = [aParentView window];
+ if (parentWindow) {
+ return DesktopToLayoutDeviceScale([parentWindow backingScaleFactor]);
+ }
+ return DesktopToLayoutDeviceScale(1.0);
+}
+
+// Returns the screen rectangle for the given widget.
+// Child widgets are positioned relative to this rectangle.
+// Exactly one of the arguments must be non-null.
+static DesktopRect GetWidgetScreenRectForChildren(nsIWidget* aWidget,
+ NSView* aView) {
+ if (aWidget) {
+ mozilla::DesktopToLayoutDeviceScale scale =
+ aWidget->GetDesktopToDeviceScale();
+ if (aWidget->GetWindowType() == WindowType::Child) {
+ return aWidget->GetScreenBounds() / scale;
+ }
+ return aWidget->GetClientBounds() / scale;
+ }
+
+ MOZ_RELEASE_ASSERT(aView);
+
+ // 1. Transform the view rect into window coords.
+ // The returned rect is in "origin bottom-left" coordinates.
+ NSRect rectInWindowCoordinatesOBL = [aView convertRect:[aView bounds]
+ toView:nil];
+
+ // 2. Turn the window-coord rect into screen coords, still origin bottom-left.
+ NSRect rectInScreenCoordinatesOBL =
+ [[aView window] convertRectToScreen:rectInWindowCoordinatesOBL];
+
+ // 3. Convert the NSRect to a DesktopRect. This will convert to coordinates
+ // with the origin in the top left corner of the primary screen.
+ return DesktopRect(
+ nsCocoaUtils::CocoaRectToGeckoRect(rectInScreenCoordinatesOBL));
+}
+
+// aRect here is specified in desktop pixels
+//
+// For child windows (where either aParent or aNativeParent is non-null),
+// aRect.{x,y} are offsets from the origin of the parent window and not an
+// absolute position.
+nsresult nsCocoaWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const DesktopIntRect& aRect,
+ widget::InitData* aInitData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Because the hidden window is created outside of an event loop,
+ // we have to provide an autorelease pool (see bug 559075).
+ nsAutoreleasePool localPool;
+
+ // Set defaults which can be overriden from aInitData in BaseCreate
+ mWindowType = WindowType::TopLevel;
+ mBorderStyle = BorderStyle::Default;
+
+ // Ensure that the toolkit is created.
+ nsToolkit::GetToolkit();
+
+ Inherited::BaseCreate(aParent, aInitData);
+
+ mParent = aParent;
+ mAncestorLink = aParent;
+ mAlwaysOnTop = aInitData->mAlwaysOnTop;
+
+ // If we have a parent widget, the new widget will be offset from the
+ // parent widget by aRect.{x,y}. Otherwise, we'll use aRect for the
+ // new widget coordinates.
+ DesktopIntPoint parentOrigin;
+
+ // Do we have a parent widget?
+ if (aParent || aNativeParent) {
+ DesktopRect parentDesktopRect =
+ GetWidgetScreenRectForChildren(aParent, (NSView*)aNativeParent);
+ parentOrigin = gfx::RoundedToInt(parentDesktopRect.TopLeft());
+ }
+
+ DesktopIntRect widgetRect = aRect + parentOrigin;
+
+ nsresult rv =
+ CreateNativeWindow(nsCocoaUtils::GeckoRectToCocoaRect(widgetRect),
+ mBorderStyle, false, aInitData->mIsPrivate);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mWindowType == WindowType::Popup) {
+ // now we can convert widgetRect to device pixels for the window we created,
+ // as the child view expects a rect expressed in the dev pix of its parent
+ LayoutDeviceIntRect devRect =
+ RoundedToInt(aRect * GetDesktopToDeviceScale());
+ return CreatePopupContentView(devRect, aInitData);
+ }
+
+ mIsAnimationSuppressed = aInitData->mIsAnimationSuppressed;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsCocoaWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ DesktopIntRect desktopRect = RoundedToInt(
+ aRect / ParentBackingScaleFactor(aParent, (NSView*)aNativeParent));
+ return Create(aParent, aNativeParent, desktopRect, aInitData);
+}
+
+static unsigned int WindowMaskForBorderStyle(BorderStyle aBorderStyle) {
+ bool allOrDefault = (aBorderStyle == BorderStyle::All ||
+ aBorderStyle == BorderStyle::Default);
+
+ /* Apple's docs on NSWindow styles say that "a window's style mask should
+ * include NSWindowStyleMaskTitled if it includes any of the others [besides
+ * NSWindowStyleMaskBorderless]". This implies that a borderless window
+ * shouldn't have any other styles than NSWindowStyleMaskBorderless.
+ */
+ if (!allOrDefault && !(aBorderStyle & BorderStyle::Title)) {
+ if (aBorderStyle & BorderStyle::Minimize) {
+ /* It appears that at a minimum, borderless windows can be miniaturizable,
+ * effectively contradicting some of Apple's documentation referenced
+ * above. One such exception is the screen share indicator, see
+ * bug 1742877.
+ */
+ return NSWindowStyleMaskBorderless | NSWindowStyleMaskMiniaturizable;
+ }
+ return NSWindowStyleMaskBorderless;
+ }
+
+ unsigned int mask = NSWindowStyleMaskTitled;
+ if (allOrDefault || aBorderStyle & BorderStyle::Close) {
+ mask |= NSWindowStyleMaskClosable;
+ }
+ if (allOrDefault || aBorderStyle & BorderStyle::Minimize) {
+ mask |= NSWindowStyleMaskMiniaturizable;
+ }
+ if (allOrDefault || aBorderStyle & BorderStyle::ResizeH) {
+ mask |= NSWindowStyleMaskResizable;
+ }
+
+ return mask;
+}
+
+// If aRectIsFrameRect, aRect specifies the frame rect of the new window.
+// Otherwise, aRect.x/y specify the position of the window's frame relative to
+// the bottom of the menubar and aRect.width/height specify the size of the
+// content rect.
+nsresult nsCocoaWindow::CreateNativeWindow(const NSRect& aRect,
+ BorderStyle aBorderStyle,
+ bool aRectIsFrameRect,
+ bool aIsPrivateBrowsing) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We default to NSWindowStyleMaskBorderless, add features if needed.
+ unsigned int features = NSWindowStyleMaskBorderless;
+
+ // Configure the window we will create based on the window type.
+ switch (mWindowType) {
+ case WindowType::Invisible:
+ case WindowType::Child:
+ break;
+ case WindowType::Popup:
+ if (aBorderStyle != BorderStyle::Default &&
+ mBorderStyle & BorderStyle::Title) {
+ features |= NSWindowStyleMaskTitled;
+ if (aBorderStyle & BorderStyle::Close) {
+ features |= NSWindowStyleMaskClosable;
+ }
+ }
+ break;
+ case WindowType::TopLevel:
+ case WindowType::Dialog:
+ features = WindowMaskForBorderStyle(aBorderStyle);
+ break;
+ case WindowType::Sheet:
+ if (mParent->GetWindowType() != WindowType::Invisible &&
+ aBorderStyle & BorderStyle::ResizeH) {
+ features = NSWindowStyleMaskResizable;
+ } else {
+ features = NSWindowStyleMaskMiniaturizable;
+ }
+ features |= NSWindowStyleMaskTitled;
+ break;
+ default:
+ NS_ERROR("Unhandled window type!");
+ return NS_ERROR_FAILURE;
+ }
+
+ NSRect contentRect;
+
+ if (aRectIsFrameRect) {
+ contentRect = [NSWindow contentRectForFrameRect:aRect styleMask:features];
+ } else {
+ /*
+ * We pass a content area rect to initialize the native Cocoa window. The
+ * content rect we give is the same size as the size we're given by gecko.
+ * The origin we're given for non-popup windows is moved down by the height
+ * of the menu bar so that an origin of (0,100) from gecko puts the window
+ * 100 pixels below the top of the available desktop area. We also move the
+ * origin down by the height of a title bar if it exists. This is so the
+ * origin that gecko gives us for the top-left of the window turns out to
+ * be the top-left of the window we create. This is how it was done in
+ * Carbon. If it ought to be different we'll probably need to look at all
+ * the callers.
+ *
+ * Note: This means that if you put a secondary screen on top of your main
+ * screen and open a window in the top screen, it'll be incorrectly shifted
+ * down by the height of the menu bar. Same thing would happen in Carbon.
+ *
+ * Note: If you pass a rect with 0,0 for an origin, the window ends up in a
+ * weird place for some reason. This stops that without breaking popups.
+ */
+ // Compensate for difference between frame and content area height (e.g.
+ // title bar).
+ NSRect newWindowFrame = [NSWindow frameRectForContentRect:aRect
+ styleMask:features];
+
+ contentRect = aRect;
+ contentRect.origin.y -= (newWindowFrame.size.height - aRect.size.height);
+
+ if (mWindowType != WindowType::Popup)
+ contentRect.origin.y -= [[NSApp mainMenu] menuBarHeight];
+ }
+
+ // NSLog(@"Top-level window being created at Cocoa rect: %f, %f, %f, %f\n",
+ // rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
+
+ Class windowClass = [BaseWindow class];
+ // If we have a titlebar on a top-level window, we want to be able to control
+ // the titlebar color (for unified windows), so use the special ToolbarWindow
+ // class. Note that we need to check the window type because we mark sheets as
+ // having titlebars.
+ if ((mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog) &&
+ (features & NSWindowStyleMaskTitled))
+ windowClass = [ToolbarWindow class];
+ // If we're a popup window we need to use the PopupWindow class.
+ else if (mWindowType == WindowType::Popup)
+ windowClass = [PopupWindow class];
+ // If we're a non-popup borderless window we need to use the
+ // BorderlessWindow class.
+ else if (features == NSWindowStyleMaskBorderless)
+ windowClass = [BorderlessWindow class];
+
+ // Create the window
+ mWindow = [[windowClass alloc] initWithContentRect:contentRect
+ styleMask:features
+ backing:NSBackingStoreBuffered
+ defer:YES];
+
+ // Make sure that window titles don't leak to disk in private browsing mode
+ // due to macOS' resume feature.
+ [mWindow setRestorable:!aIsPrivateBrowsing];
+ if (aIsPrivateBrowsing) {
+ [mWindow disableSnapshotRestoration];
+ }
+
+ // setup our notification delegate. Note that setDelegate: does NOT retain.
+ mDelegate = [[WindowDelegate alloc] initWithGeckoWindow:this];
+ [mWindow setDelegate:mDelegate];
+
+ // Make sure that the content rect we gave has been honored.
+ NSRect wantedFrame = [mWindow frameRectForChildViewRect:contentRect];
+ if (!NSEqualRects([mWindow frame], wantedFrame)) {
+ // This can happen when the window is not on the primary screen.
+ [mWindow setFrame:wantedFrame display:NO];
+ }
+ UpdateBounds();
+
+ if (mWindowType == WindowType::Invisible) {
+ [mWindow setLevel:kCGDesktopWindowLevelKey];
+ }
+
+ if (mWindowType == WindowType::Popup) {
+ SetPopupWindowLevel();
+ [mWindow setBackgroundColor:NSColor.clearColor];
+ [mWindow setOpaque:NO];
+
+ // When multiple spaces are in use and the browser is assigned to a
+ // particular space, override the "Assign To" space and display popups on
+ // the active space. Does not work with multiple displays. See
+ // NeedsRecreateToReshow() for multi-display with multi-space workaround.
+ if (!mAlwaysOnTop) {
+ NSWindowCollectionBehavior behavior = [mWindow collectionBehavior];
+ behavior |= NSWindowCollectionBehaviorMoveToActiveSpace;
+ [mWindow setCollectionBehavior:behavior];
+ }
+ } else {
+ // Non-popup windows are always opaque.
+ [mWindow setOpaque:YES];
+ }
+
+ NSWindowCollectionBehavior newBehavior = [mWindow collectionBehavior];
+ if (mAlwaysOnTop) {
+ [mWindow setLevel:NSFloatingWindowLevel];
+ newBehavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
+ }
+ [mWindow setCollectionBehavior:newBehavior];
+
+ [mWindow setContentMinSize:NSMakeSize(60, 60)];
+ [mWindow disableCursorRects];
+
+ // Make the window use CoreAnimation from the start, so that we don't
+ // switch from a non-CA window to a CA-window in the middle.
+ [[mWindow contentView] setWantsLayer:YES];
+
+ // Make sure the window starts out not draggable by the background.
+ // We will turn it on as necessary.
+ [mWindow setMovableByWindowBackground:NO];
+
+ [[WindowDataMap sharedWindowDataMap] ensureDataForWindow:mWindow];
+ mWindowMadeHere = true;
+
+ // Make the window respect the global appearance, which follows the
+ // browser.theme.toolbar-theme pref.
+ mWindow.appearanceSource = MOZGlobalAppearance.sharedInstance;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsCocoaWindow::CreatePopupContentView(const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We need to make our content view a ChildView.
+ mPopupContentView = new nsChildView();
+ if (!mPopupContentView) return NS_ERROR_FAILURE;
+
+ NS_ADDREF(mPopupContentView);
+
+ nsIWidget* thisAsWidget = static_cast<nsIWidget*>(this);
+ nsresult rv =
+ mPopupContentView->Create(thisAsWidget, nullptr, aRect, aInitData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ NSView* contentView = [mWindow contentView];
+ ChildView* childView =
+ (ChildView*)mPopupContentView->GetNativeData(NS_NATIVE_WIDGET);
+ [childView setFrame:[contentView bounds]];
+ [childView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+ [contentView addSubview:childView];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::Destroy() {
+ if (mOnDestroyCalled) return;
+ mOnDestroyCalled = true;
+
+ // SetFakeModal(true) is called for non-modal window opened by modal window.
+ // On Cocoa, it needs corresponding SetFakeModal(false) on destroy to restore
+ // ancestor windows' state.
+ if (mFakeModal) {
+ SetFakeModal(false);
+ }
+
+ // If we don't hide here we run into problems with panels, this is not ideal.
+ // (Bug 891424)
+ Show(false);
+
+ if (mPopupContentView) mPopupContentView->Destroy();
+
+ if (mFullscreenTransitionAnimation) {
+ [mFullscreenTransitionAnimation stopAnimation];
+ ReleaseFullscreenTransitionAnimation();
+ }
+
+ nsBaseWidget::Destroy();
+ // nsBaseWidget::Destroy() calls GetParent()->RemoveChild(this). But we
+ // don't implement GetParent(), so we need to do the equivalent here.
+ if (mParent) {
+ mParent->RemoveChild(this);
+ }
+ nsBaseWidget::OnDestroy();
+
+ if (mInFullScreenMode && !mInNativeFullScreenMode) {
+ // Keep these calls balanced for emulated fullscreen.
+ nsCocoaUtils::HideOSChromeOnScreen(false);
+ }
+
+ // Destroy the native window here (and not wait for that to happen in our
+ // destructor). Otherwise this might not happen for several seconds because
+ // at least one object holding a reference to ourselves is usually waiting
+ // to be garbage-collected.
+ if (mWindow && mWindowMadeHere) {
+ CancelAllTransitions();
+ DestroyNativeWindow();
+ }
+}
+
+nsIWidget* nsCocoaWindow::GetSheetWindowParent(void) {
+ if (mWindowType != WindowType::Sheet) return nullptr;
+ nsCocoaWindow* parent = static_cast<nsCocoaWindow*>(mParent);
+ while (parent && (parent->mWindowType == WindowType::Sheet))
+ parent = static_cast<nsCocoaWindow*>(parent->mParent);
+ return parent;
+}
+
+void* nsCocoaWindow::GetNativeData(uint32_t aDataType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ void* retVal = nullptr;
+
+ switch (aDataType) {
+ // to emulate how windows works, we always have to return a NSView
+ // for NS_NATIVE_WIDGET
+ case NS_NATIVE_WIDGET:
+ retVal = [mWindow contentView];
+ break;
+
+ case NS_NATIVE_WINDOW:
+ retVal = mWindow;
+ break;
+
+ case NS_NATIVE_GRAPHIC:
+ // There isn't anything that makes sense to return here,
+ // and it doesn't matter so just return nullptr.
+ NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a top-level window!");
+ break;
+ case NS_RAW_NATIVE_IME_CONTEXT: {
+ retVal = GetPseudoIMEContext();
+ if (retVal) {
+ break;
+ }
+ NSView* view = mWindow ? [mWindow contentView] : nil;
+ if (view) {
+ retVal = [view inputContext];
+ }
+ // If inputContext isn't available on this window, return this window's
+ // pointer instead of nullptr since if this returns nullptr,
+ // IMEStateManager cannot manage composition with TextComposition
+ // instance. Although, this case shouldn't occur.
+ if (NS_WARN_IF(!retVal)) {
+ retVal = this;
+ }
+ break;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+bool nsCocoaWindow::IsVisible() const {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return (mWindow && ([mWindow isVisibleOrBeingShown] || mSheetNeedsShow));
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void nsCocoaWindow::SetModal(bool aState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Unlike many functions here, we explicitly *do not check* for the
+ // existence of mWindow. This is to ensure that calls to SetModal have
+ // no early exits and always update state. That way, if the calls are
+ // balanced, we get expected behavior even if the native window has
+ // been destroyed during the modal period. Within this function, all
+ // the calls to mWindow will resolve even if mWindow is nil (as is
+ // guaranteed by Objective-C). And since those calls are only concerned
+ // with changing mWindow appearance/level, it's fine for them to be
+ // no-ops if mWindow has already been destroyed.
+
+ // This is used during startup (outside the event loop) when creating
+ // the add-ons compatibility checking dialog and the profile manager UI;
+ // therefore, it needs to provide an autorelease pool to avoid cocoa
+ // objects leaking.
+ nsAutoreleasePool localPool;
+
+ mModal = aState;
+ nsCocoaWindow* ancestor = static_cast<nsCocoaWindow*>(mAncestorLink);
+ if (aState) {
+ ++gXULModalLevel;
+ // When a non-sheet window gets "set modal", make the window(s) that it
+ // appears over behave as they should. We can't rely on native methods to
+ // do this, for the following reason: The OS runs modal non-sheet windows
+ // in an event loop (using [NSApplication runModalForWindow:] or similar
+ // methods) that's incompatible with the modal event loop in AppWindow::
+ // ShowModal() (each of these event loops is "exclusive", and can't run at
+ // the same time as other (similar) event loops).
+ if (mWindowType != WindowType::Sheet) {
+ while (ancestor) {
+ if (ancestor->mNumModalDescendents++ == 0) {
+ NSWindow* aWindow = ancestor->GetCocoaWindow();
+ if (ancestor->mWindowType != WindowType::Invisible) {
+ [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:NO];
+ [[aWindow standardWindowButton:NSWindowMiniaturizeButton]
+ setEnabled:NO];
+ [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:NO];
+ }
+ }
+ ancestor = static_cast<nsCocoaWindow*>(ancestor->mParent);
+ }
+ [mWindow setLevel:NSModalPanelWindowLevel];
+ nsCocoaWindowList* windowList = new nsCocoaWindowList;
+ if (windowList) {
+ windowList->window = this; // Don't ADDREF
+ windowList->prev = gGeckoAppModalWindowList;
+ gGeckoAppModalWindowList = windowList;
+ }
+ }
+ } else {
+ --gXULModalLevel;
+ NS_ASSERTION(gXULModalLevel >= 0,
+ "Mismatched call to nsCocoaWindow::SetModal(false)!");
+ if (mWindowType != WindowType::Sheet) {
+ while (ancestor) {
+ if (--ancestor->mNumModalDescendents == 0) {
+ NSWindow* aWindow = ancestor->GetCocoaWindow();
+ if (ancestor->mWindowType != WindowType::Invisible) {
+ [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:YES];
+ [[aWindow standardWindowButton:NSWindowMiniaturizeButton]
+ setEnabled:YES];
+ [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:YES];
+ }
+ }
+ NS_ASSERTION(ancestor->mNumModalDescendents >= 0,
+ "Widget hierarchy changed while modal!");
+ ancestor = static_cast<nsCocoaWindow*>(ancestor->mParent);
+ }
+ if (gGeckoAppModalWindowList) {
+ NS_ASSERTION(gGeckoAppModalWindowList->window == this,
+ "Widget hierarchy changed while modal!");
+ nsCocoaWindowList* saved = gGeckoAppModalWindowList;
+ gGeckoAppModalWindowList = gGeckoAppModalWindowList->prev;
+ delete saved; // "window" not ADDREFed
+ }
+ if (mWindowType == WindowType::Popup)
+ SetPopupWindowLevel();
+ else
+ [mWindow setLevel:NSNormalWindowLevel];
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetFakeModal(bool aState) {
+ mFakeModal = aState;
+ SetModal(aState);
+}
+
+bool nsCocoaWindow::IsRunningAppModal() { return [NSApp _isRunningAppModal]; }
+
+// Hide or show this window
+void nsCocoaWindow::Show(bool bState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) return;
+
+ if (!mSheetNeedsShow) {
+ // Early exit if our current visibility state is already the requested
+ // state.
+ if (bState == ([mWindow isVisible] || [mWindow isBeingShown])) {
+ return;
+ }
+ }
+
+ [mWindow setBeingShown:bState];
+ if (bState && !mWasShown) {
+ mWasShown = true;
+ }
+
+ nsIWidget* parentWidget = mParent;
+ nsCOMPtr<nsPIWidgetCocoa> piParentWidget(do_QueryInterface(parentWidget));
+ NSWindow* nativeParentWindow =
+ (parentWidget) ? (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW)
+ : nil;
+
+ if (bState && !mBounds.IsEmpty()) {
+ // If we had set the activationPolicy to accessory, then right now we won't
+ // have a dock icon. Make sure that we undo that and show a dock icon now
+ // that we're going to show a window.
+ if ([NSApp activationPolicy] != NSApplicationActivationPolicyRegular) {
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
+ PR_SetEnv("MOZ_APP_NO_DOCK=");
+ }
+
+ // Don't try to show a popup when the parent isn't visible or is minimized.
+ if (mWindowType == WindowType::Popup && nativeParentWindow) {
+ if (![nativeParentWindow isVisible] ||
+ [nativeParentWindow isMiniaturized]) {
+ return;
+ }
+ }
+
+ if (mPopupContentView) {
+ // Ensure our content view is visible. We never need to hide it.
+ mPopupContentView->Show(true);
+ }
+
+ if (mWindowType == WindowType::Sheet) {
+ // bail if no parent window (its basically what we do in Carbon)
+ if (!nativeParentWindow || !piParentWidget) return;
+
+ NSWindow* topNonSheetWindow = nativeParentWindow;
+
+ // If this sheet is the child of another sheet, hide the parent so that
+ // this sheet can be displayed. Leave the parent mSheetNeedsShow alone,
+ // that is only used to handle sibling sheet contention. The parent will
+ // return once there are no more child sheets.
+ bool parentIsSheet = false;
+ if (NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) &&
+ parentIsSheet) {
+ piParentWidget->GetSheetWindowParent(&topNonSheetWindow);
+#ifdef MOZ_THUNDERBIRD
+ [NSApp endSheet:nativeParentWindow];
+#else
+ [nativeParentWindow.sheetParent endSheet:nativeParentWindow];
+#endif
+ }
+
+ nsCOMPtr<nsIWidget> sheetShown;
+ if (NS_SUCCEEDED(piParentWidget->GetChildSheet(
+ true, getter_AddRefs(sheetShown))) &&
+ (!sheetShown || sheetShown == this)) {
+ // If this sheet is already the sheet actually being shown, don't
+ // tell it to show again. Otherwise the number of calls to
+#ifdef MOZ_THUNDERBIRD
+ // [NSApp beginSheet...] won't match up with [NSApp endSheet...].
+#else
+ // [NSWindow beginSheet...] won't match up with [NSWindow endSheet...].
+#endif
+ if (![mWindow isVisible]) {
+ mSheetNeedsShow = false;
+ mSheetWindowParent = topNonSheetWindow;
+#ifdef MOZ_THUNDERBIRD
+ // Only set contextInfo if our parent isn't a sheet.
+ NSWindow* contextInfo = parentIsSheet ? nil : mSheetWindowParent;
+ [TopLevelWindowData deactivateInWindow:mSheetWindowParent];
+ [NSApp beginSheet:mWindow
+ modalForWindow:mSheetWindowParent
+ modalDelegate:mDelegate
+ didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+ contextInfo:contextInfo];
+#else
+ NSWindow* sheet = mWindow;
+ NSWindow* nonSheetParent = parentIsSheet ? nil : mSheetWindowParent;
+ [TopLevelWindowData deactivateInWindow:mSheetWindowParent];
+ [mSheetWindowParent beginSheet:sheet
+ completionHandler:^(NSModalResponse returnCode) {
+ // Note: 'nonSheetParent' (if it is set) is the window
+ // that is the parent of the sheet. If it's set,
+ // 'nonSheetParent' is always the top- level window,
+ // not another sheet itself. But 'nonSheetParent' is
+ // nil if our parent window is also a sheet -- in that
+ // case we shouldn't send the top-level window any
+ // activate events (because it's our parent window that
+ // needs to get these events, not the top-level
+ // window).
+ [TopLevelWindowData deactivateInWindow:sheet];
+ [sheet orderOut:nil];
+ if (nonSheetParent) {
+ [TopLevelWindowData activateInWindow:nonSheetParent];
+ }
+ }];
+#endif
+ [TopLevelWindowData activateInWindow:mWindow];
+ SendSetZLevelEvent();
+ }
+ } else {
+ // A sibling of this sheet is active, don't show this sheet yet.
+ // When the active sheet hides, its brothers and sisters that have
+ // mSheetNeedsShow set will have their opportunities to display.
+ mSheetNeedsShow = true;
+ }
+ } else if (mWindowType == WindowType::Popup) {
+ // For reasons that aren't yet clear, calls to [NSWindow orderFront:] or
+ // [NSWindow makeKeyAndOrderFront:] can sometimes trigger "Error (1000)
+ // creating CGSWindow", which in turn triggers an internal inconsistency
+ // NSException. These errors shouldn't be fatal. So we need to wrap
+ // calls to ...orderFront: in TRY blocks. See bmo bug 470864.
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [[mWindow contentView] setNeedsDisplay:YES];
+ [mWindow orderFront:nil];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ SendSetZLevelEvent();
+ // If our popup window is a non-native context menu, tell the OS (and
+ // other programs) that a menu has opened. This is how the OS knows to
+ // close other programs' context menus when ours open.
+ if ([mWindow isKindOfClass:[PopupWindow class]] &&
+ [(PopupWindow*)mWindow isContextMenu]) {
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:
+ @"com.apple.HIToolbox.beginMenuTrackingNotification"
+ object:@"org.mozilla.gecko.PopupWindow"];
+ }
+
+ // If a parent window was supplied and this is a popup at the parent
+ // level, set its child window. This will cause the child window to
+ // appear above the parent and move when the parent does. Setting this
+ // needs to happen after the _setWindowNumber calls above, otherwise the
+ // window doesn't focus properly.
+ if (nativeParentWindow && mPopupLevel == PopupLevel::Parent)
+ [nativeParentWindow addChildWindow:mWindow ordered:NSWindowAbove];
+ } else {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ if (mWindowType == WindowType::TopLevel &&
+ [mWindow respondsToSelector:@selector(setAnimationBehavior:)]) {
+ NSWindowAnimationBehavior behavior;
+ if (mIsAnimationSuppressed) {
+ behavior = NSWindowAnimationBehaviorNone;
+ } else {
+ switch (mAnimationType) {
+ case nsIWidget::eDocumentWindowAnimation:
+ behavior = NSWindowAnimationBehaviorDocumentWindow;
+ break;
+ default:
+ MOZ_FALLTHROUGH_ASSERT("unexpected mAnimationType value");
+ case nsIWidget::eGenericWindowAnimation:
+ behavior = NSWindowAnimationBehaviorDefault;
+ break;
+ }
+ }
+ [mWindow setAnimationBehavior:behavior];
+ mWindowAnimationBehavior = behavior;
+ }
+
+ // We don't want alwaysontop windows to pull focus when they're opened,
+ // as these tend to be for peripheral indicators and displays.
+ if (mAlwaysOnTop) {
+ [mWindow orderFront:nil];
+ } else {
+ [mWindow makeKeyAndOrderFront:nil];
+ }
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ SendSetZLevelEvent();
+ }
+ } else {
+ // roll up any popups if a top-level window is going away
+ if (mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog) {
+ RollUpPopups();
+ }
+
+ // now get rid of the window/sheet
+ if (mWindowType == WindowType::Sheet) {
+ if (mSheetNeedsShow) {
+ // This is an attempt to hide a sheet that never had a chance to
+ // be shown. There's nothing to do other than make sure that it
+ // won't show.
+ mSheetNeedsShow = false;
+ } else {
+ // get sheet's parent *before* hiding the sheet (which breaks the
+ // linkage)
+ NSWindow* sheetParent = mSheetWindowParent;
+
+ // hide the sheet
+#ifdef MOZ_THUNDERBIRD
+ [NSApp endSheet:mWindow];
+#else
+ [mSheetWindowParent endSheet:mWindow];
+#endif
+ [TopLevelWindowData deactivateInWindow:mWindow];
+
+ nsCOMPtr<nsIWidget> siblingSheetToShow;
+ bool parentIsSheet = false;
+
+ if (nativeParentWindow && piParentWidget &&
+ NS_SUCCEEDED(piParentWidget->GetChildSheet(
+ false, getter_AddRefs(siblingSheetToShow))) &&
+ siblingSheetToShow) {
+ // First, give sibling sheets an opportunity to show.
+ siblingSheetToShow->Show(true);
+ } else if (nativeParentWindow && piParentWidget &&
+ NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) &&
+ parentIsSheet) {
+#ifdef MOZ_THUNDERBIRD
+ // Only set contextInfo if the parent of the parent sheet we're about
+ // to restore isn't itself a sheet.
+ NSWindow* contextInfo = sheetParent;
+#else
+ // Only set nonSheetGrandparent if the parent of the parent sheet
+ // we're about to restore isn't itself a sheet.
+ NSWindow* nonSheetGrandparent = sheetParent;
+#endif
+ nsIWidget* grandparentWidget = nil;
+ if (NS_SUCCEEDED(piParentWidget->GetRealParent(&grandparentWidget)) &&
+ grandparentWidget) {
+ nsCOMPtr<nsPIWidgetCocoa> piGrandparentWidget(
+ do_QueryInterface(grandparentWidget));
+ bool grandparentIsSheet = false;
+ if (piGrandparentWidget &&
+ NS_SUCCEEDED(
+ piGrandparentWidget->GetIsSheet(&grandparentIsSheet)) &&
+ grandparentIsSheet) {
+#ifdef MOZ_THUNDERBIRD
+ contextInfo = nil;
+#else
+ nonSheetGrandparent = nil;
+#endif
+ }
+ }
+ // If there are no sibling sheets, but the parent is a sheet, restore
+ // it. It wasn't sent any deactivate events when it was hidden, so
+ // don't call through Show, just let the OS put it back up.
+#ifdef MOZ_THUNDERBIRD
+ [NSApp beginSheet:nativeParentWindow
+ modalForWindow:sheetParent
+ modalDelegate:[nativeParentWindow delegate]
+ didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+ contextInfo:contextInfo];
+#else
+ [nativeParentWindow
+ beginSheet:sheetParent
+ completionHandler:^(NSModalResponse returnCode) {
+ // Note: 'nonSheetGrandparent' (if it is set) is the window that
+ // is the parent of sheetParent. If it's set,
+ // 'nonSheetGrandparent' is always the top-level window, not
+ // another sheet itself. But 'nonSheetGrandparent' is nil if
+ // our parent window is also a sheet -- in that case we
+ // shouldn't send the top-level window any activate events
+ // (because it's our parent window that needs to get these
+ // events, not the top-level window).
+ [TopLevelWindowData deactivateInWindow:sheetParent];
+ [sheetParent orderOut:nil];
+ if (nonSheetGrandparent) {
+ [TopLevelWindowData activateInWindow:nonSheetGrandparent];
+ }
+ }];
+#endif
+ } else {
+ // Sheet, that was hard. No more siblings or parents, going back
+ // to a real window.
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [sheetParent makeKeyAndOrderFront:nil];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+ SendSetZLevelEvent();
+ }
+ } else {
+ // If the window is a popup window with a parent window we need to
+ // unhook it here before ordering it out. When you order out the child
+ // of a window it hides the parent window.
+ if (mWindowType == WindowType::Popup && nativeParentWindow)
+ [nativeParentWindow removeChildWindow:mWindow];
+
+ [mWindow orderOut:nil];
+
+ // If our popup window is a non-native context menu, tell the OS (and
+ // other programs) that a menu has closed.
+ if ([mWindow isKindOfClass:[PopupWindow class]] &&
+ [(PopupWindow*)mWindow isContextMenu]) {
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:
+ @"com.apple.HIToolbox.endMenuTrackingNotification"
+ object:@"org.mozilla.gecko.PopupWindow"];
+ }
+ }
+ }
+
+ [mWindow setBeingShown:NO];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Work around a problem where with multiple displays and multiple spaces
+// enabled, where the browser is assigned to a single display or space, popup
+// windows that are reshown after being hidden with [NSWindow orderOut] show on
+// the assigned space even when opened from another display. Apply the
+// workaround whenever more than one display is enabled.
+bool nsCocoaWindow::NeedsRecreateToReshow() {
+ // Limit the workaround to popup windows because only they need to override
+ // the "Assign To" setting. i.e., to display where the parent window is.
+ return (mWindowType == WindowType::Popup) && mWasShown &&
+ ([[NSScreen screens] count] > 1);
+}
+
+WindowRenderer* nsCocoaWindow::GetWindowRenderer() {
+ if (mPopupContentView) {
+ return mPopupContentView->GetWindowRenderer();
+ }
+ return nullptr;
+}
+
+TransparencyMode nsCocoaWindow::GetTransparencyMode() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return (!mWindow || [mWindow isOpaque]) ? TransparencyMode::Opaque
+ : TransparencyMode::Transparent;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(TransparencyMode::Opaque);
+}
+
+// This is called from nsMenuPopupFrame when making a popup transparent.
+void nsCocoaWindow::SetTransparencyMode(TransparencyMode aMode) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ BOOL isTransparent = aMode == TransparencyMode::Transparent;
+ BOOL currentTransparency = !mWindow.isOpaque;
+ if (isTransparent == currentTransparency) {
+ return;
+ }
+ [mWindow setOpaque:!isTransparent];
+ [mWindow setBackgroundColor:(isTransparent ? NSColor.clearColor
+ : NSColor.whiteColor)];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::Enable(bool aState) {}
+
+bool nsCocoaWindow::IsEnabled() const { return true; }
+
+void nsCocoaWindow::ConstrainPosition(DesktopIntPoint& aPoint) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow || ![mWindow screen]) {
+ return;
+ }
+
+ nsIntRect screenBounds;
+
+ int32_t width, height;
+
+ NSRect frame = [mWindow frame];
+
+ // zero size rects confuse the screen manager
+ width = std::max<int32_t>(frame.size.width, 1);
+ height = std::max<int32_t>(frame.size.height, 1);
+
+ nsCOMPtr<nsIScreenManager> screenMgr =
+ do_GetService("@mozilla.org/gfx/screenmanager;1");
+ if (screenMgr) {
+ nsCOMPtr<nsIScreen> screen;
+ screenMgr->ScreenForRect(aPoint.x, aPoint.y, width, height,
+ getter_AddRefs(screen));
+
+ if (screen) {
+ screen->GetRectDisplayPix(&(screenBounds.x), &(screenBounds.y),
+ &(screenBounds.width), &(screenBounds.height));
+ }
+ }
+
+ if (aPoint.x < screenBounds.x) {
+ aPoint.x = screenBounds.x;
+ } else if (aPoint.x >= screenBounds.x + screenBounds.width - width) {
+ aPoint.x = screenBounds.x + screenBounds.width - width;
+ }
+
+ if (aPoint.y < screenBounds.y) {
+ aPoint.y = screenBounds.y;
+ } else if (aPoint.y >= screenBounds.y + screenBounds.height - height) {
+ aPoint.y = screenBounds.y + screenBounds.height - height;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetSizeConstraints(const SizeConstraints& aConstraints) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Popups can be smaller than (32, 32)
+ NSRect rect = (mWindowType == WindowType::Popup)
+ ? NSZeroRect
+ : NSMakeRect(0.0, 0.0, 32, 32);
+ rect = [mWindow frameRectForChildViewRect:rect];
+
+ SizeConstraints c = aConstraints;
+
+ if (c.mScale.scale == MOZ_WIDGET_INVALID_SCALE) {
+ c.mScale.scale = BackingScaleFactor();
+ }
+
+ c.mMinSize.width = std::max(
+ nsCocoaUtils::CocoaPointsToDevPixels(rect.size.width, c.mScale.scale),
+ c.mMinSize.width);
+ c.mMinSize.height = std::max(
+ nsCocoaUtils::CocoaPointsToDevPixels(rect.size.height, c.mScale.scale),
+ c.mMinSize.height);
+
+ NSSize minSize = {
+ nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.width, c.mScale.scale),
+ nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.height, c.mScale.scale)};
+ [mWindow setMinSize:minSize];
+
+ c.mMaxSize.width = std::max(
+ nsCocoaUtils::CocoaPointsToDevPixels(c.mMaxSize.width, c.mScale.scale),
+ c.mMaxSize.width);
+ c.mMaxSize.height = std::max(
+ nsCocoaUtils::CocoaPointsToDevPixels(c.mMaxSize.height, c.mScale.scale),
+ c.mMaxSize.height);
+
+ NSSize maxSize = {
+ c.mMaxSize.width == NS_MAXSIZE ? FLT_MAX
+ : nsCocoaUtils::DevPixelsToCocoaPoints(
+ c.mMaxSize.width, c.mScale.scale),
+ c.mMaxSize.height == NS_MAXSIZE ? FLT_MAX
+ : nsCocoaUtils::DevPixelsToCocoaPoints(
+ c.mMaxSize.height, c.mScale.scale)};
+ [mWindow setMaxSize:maxSize];
+
+ nsBaseWidget::SetSizeConstraints(c);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::Move(double aX, double aY) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ // The point we have is in Gecko coordinates (origin top-left). Convert
+ // it to Cocoa ones (origin bottom-left).
+ NSPoint coord = {
+ static_cast<float>(aX),
+ static_cast<float>(nsCocoaUtils::FlippedScreenY(NSToIntRound(aY)))};
+
+ NSRect frame = [mWindow frame];
+ if (frame.origin.x != coord.x ||
+ frame.origin.y + frame.size.height != coord.y) {
+ [mWindow setFrameTopLeftPoint:coord];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetSizeMode(nsSizeMode aMode) {
+ if (aMode == nsSizeMode_Normal) {
+ QueueTransition(TransitionType::Windowed);
+ } else if (aMode == nsSizeMode_Minimized) {
+ QueueTransition(TransitionType::Miniaturize);
+ } else if (aMode == nsSizeMode_Maximized) {
+ QueueTransition(TransitionType::Zoom);
+ } else if (aMode == nsSizeMode_Fullscreen) {
+ MakeFullScreen(true);
+ }
+}
+
+// The (work)space switching implementation below was inspired by Phoenix:
+// https://github.com/kasper/phoenix/tree/d6c877f62b30a060dff119d8416b0934f76af534
+// License: MIT.
+
+// Runtime `CGSGetActiveSpace` library function feature detection.
+typedef CGSSpaceID (*CGSGetActiveSpaceFunc)(CGSConnection cid);
+static CGSGetActiveSpaceFunc GetCGSGetActiveSpaceFunc() {
+ static CGSGetActiveSpaceFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSGetActiveSpaceFunc)dlsym(RTLD_DEFAULT, "CGSGetActiveSpace");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSCopyManagedDisplaySpaces` library function feature detection.
+typedef CFArrayRef (*CGSCopyManagedDisplaySpacesFunc)(CGSConnection cid);
+static CGSCopyManagedDisplaySpacesFunc GetCGSCopyManagedDisplaySpacesFunc() {
+ static CGSCopyManagedDisplaySpacesFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSCopyManagedDisplaySpacesFunc)dlsym(
+ RTLD_DEFAULT, "CGSCopyManagedDisplaySpaces");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSCopySpacesForWindows` library function feature detection.
+typedef CFArrayRef (*CGSCopySpacesForWindowsFunc)(CGSConnection cid,
+ CGSSpaceMask mask,
+ CFArrayRef windowIDs);
+static CGSCopySpacesForWindowsFunc GetCGSCopySpacesForWindowsFunc() {
+ static CGSCopySpacesForWindowsFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSCopySpacesForWindowsFunc)dlsym(RTLD_DEFAULT,
+ "CGSCopySpacesForWindows");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSAddWindowsToSpaces` library function feature detection.
+typedef void (*CGSAddWindowsToSpacesFunc)(CGSConnection cid,
+ CFArrayRef windowIDs,
+ CFArrayRef spaceIDs);
+static CGSAddWindowsToSpacesFunc GetCGSAddWindowsToSpacesFunc() {
+ static CGSAddWindowsToSpacesFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func =
+ (CGSAddWindowsToSpacesFunc)dlsym(RTLD_DEFAULT, "CGSAddWindowsToSpaces");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+// Runtime `CGSRemoveWindowsFromSpaces` library function feature detection.
+typedef void (*CGSRemoveWindowsFromSpacesFunc)(CGSConnection cid,
+ CFArrayRef windowIDs,
+ CFArrayRef spaceIDs);
+static CGSRemoveWindowsFromSpacesFunc GetCGSRemoveWindowsFromSpacesFunc() {
+ static CGSRemoveWindowsFromSpacesFunc func = nullptr;
+ static bool lookedUpFunc = false;
+ if (!lookedUpFunc) {
+ func = (CGSRemoveWindowsFromSpacesFunc)dlsym(RTLD_DEFAULT,
+ "CGSRemoveWindowsFromSpaces");
+ lookedUpFunc = true;
+ }
+ return func;
+}
+
+void nsCocoaWindow::GetWorkspaceID(nsAString& workspaceID) {
+ workspaceID.Truncate();
+ int32_t sid = GetWorkspaceID();
+ if (sid != 0) {
+ workspaceID.AppendInt(sid);
+ }
+}
+
+int32_t nsCocoaWindow::GetWorkspaceID() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Mac OSX space IDs start at '1' (default space), so '0' means 'unknown',
+ // effectively.
+ CGSSpaceID sid = 0;
+
+ CGSCopySpacesForWindowsFunc CopySpacesForWindows =
+ GetCGSCopySpacesForWindowsFunc();
+ if (!CopySpacesForWindows) {
+ return sid;
+ }
+
+ CGSConnection cid = _CGSDefaultConnection();
+ // Fetch all spaces that this window belongs to (in order).
+ NSArray<NSNumber*>* spaceIDs = CFBridgingRelease(CopySpacesForWindows(
+ cid, kCGSAllSpacesMask,
+ (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ]));
+ if ([spaceIDs count]) {
+ // When spaces are found, return the first one.
+ // We don't support a single window painted across multiple places for now.
+ sid = [spaceIDs[0] integerValue];
+ } else {
+ // Fall back to the workspace that's currently active, which is '1' in the
+ // common case.
+ CGSGetActiveSpaceFunc GetActiveSpace = GetCGSGetActiveSpaceFunc();
+ if (GetActiveSpace) {
+ sid = GetActiveSpace(cid);
+ }
+ }
+
+ return sid;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::MoveToWorkspace(const nsAString& workspaceIDStr) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([NSScreen screensHaveSeparateSpaces] && [[NSScreen screens] count] > 1) {
+ // We don't support moving to a workspace when the user has this option
+ // enabled in Mission Control.
+ return;
+ }
+
+ nsresult rv = NS_OK;
+ int32_t workspaceID = workspaceIDStr.ToInteger(&rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ CGSConnection cid = _CGSDefaultConnection();
+ int32_t currentSpace = GetWorkspaceID();
+ // If an empty workspace ID is passed in (not valid on OSX), or when the
+ // window is already on this workspace, we don't need to do anything.
+ if (!workspaceID || workspaceID == currentSpace) {
+ return;
+ }
+
+ CGSCopyManagedDisplaySpacesFunc CopyManagedDisplaySpaces =
+ GetCGSCopyManagedDisplaySpacesFunc();
+ CGSAddWindowsToSpacesFunc AddWindowsToSpaces = GetCGSAddWindowsToSpacesFunc();
+ CGSRemoveWindowsFromSpacesFunc RemoveWindowsFromSpaces =
+ GetCGSRemoveWindowsFromSpacesFunc();
+ if (!CopyManagedDisplaySpaces || !AddWindowsToSpaces ||
+ !RemoveWindowsFromSpaces) {
+ return;
+ }
+
+ // Fetch an ordered list of all known spaces.
+ NSArray* displaySpacesInfo = CFBridgingRelease(CopyManagedDisplaySpaces(cid));
+ // When we found the space we're looking for, we can bail out of the loop
+ // early, which this local variable is used for.
+ BOOL found = false;
+ for (NSDictionary<NSString*, id>* spacesInfo in displaySpacesInfo) {
+ NSArray<NSNumber*>* sids =
+ [spacesInfo[CGSSpacesKey] valueForKey:CGSSpaceIDKey];
+ for (NSNumber* sid in sids) {
+ // If we found our space in the list, we're good to go and can jump out of
+ // this loop.
+ if ((int)[sid integerValue] == workspaceID) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ break;
+ }
+ }
+
+ // We were unable to find the space to correspond with the workspaceID as
+ // requested, so let's bail out.
+ if (!found) {
+ return;
+ }
+
+ // First we add the window to the appropriate space.
+ AddWindowsToSpaces(cid, (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ],
+ (__bridge CFArrayRef) @[ @(workspaceID) ]);
+ // Then we remove the window from the active space.
+ RemoveWindowsFromSpaces(cid,
+ (__bridge CFArrayRef) @[ @([mWindow windowNumber]) ],
+ (__bridge CFArrayRef) @[ @(currentSpace) ]);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SuppressAnimation(bool aSuppress) {
+ if ([mWindow respondsToSelector:@selector(setAnimationBehavior:)]) {
+ if (aSuppress) {
+ [mWindow setIsAnimationSuppressed:YES];
+ [mWindow setAnimationBehavior:NSWindowAnimationBehaviorNone];
+ } else {
+ [mWindow setIsAnimationSuppressed:NO];
+ [mWindow setAnimationBehavior:mWindowAnimationBehavior];
+ }
+ }
+}
+
+// This has to preserve the window's frame bounds.
+// This method requires (as does the Windows impl.) that you call Resize shortly
+// after calling HideWindowChrome. See bug 498835 for fixing this.
+void nsCocoaWindow::HideWindowChrome(bool aShouldHide) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow || !mWindowMadeHere ||
+ (mWindowType != WindowType::TopLevel &&
+ mWindowType != WindowType::Dialog))
+ return;
+
+ BOOL isVisible = [mWindow isVisible];
+
+ // Remove child windows.
+ NSArray* childWindows = [mWindow childWindows];
+ NSEnumerator* enumerator = [childWindows objectEnumerator];
+ NSWindow* child = nil;
+ while ((child = [enumerator nextObject])) {
+ [mWindow removeChildWindow:child];
+ }
+
+ // Remove the views in the old window's content view.
+ // The NSArray is autoreleased and retains its NSViews.
+ NSArray<NSView*>* contentViewContents = [mWindow contentViewContents];
+ for (NSView* view in contentViewContents) {
+ [view removeFromSuperviewWithoutNeedingDisplay];
+ }
+
+ // Save state (like window title).
+ NSMutableDictionary* state = [mWindow exportState];
+
+ // Recreate the window with the right border style.
+ NSRect frameRect = [mWindow frame];
+ DestroyNativeWindow();
+ nsresult rv = CreateNativeWindow(
+ frameRect, aShouldHide ? BorderStyle::None : mBorderStyle, true,
+ mWindow.restorable);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Re-import state.
+ [mWindow importState:state];
+
+ // Add the old content view subviews to the new window's content view.
+ for (NSView* view in contentViewContents) {
+ [[mWindow contentView] addSubview:view];
+ }
+
+ // Reparent child windows.
+ enumerator = [childWindows objectEnumerator];
+ while ((child = [enumerator nextObject])) {
+ [mWindow addChildWindow:child ordered:NSWindowAbove];
+ }
+
+ // Show the new window.
+ if (isVisible) {
+ bool wasAnimationSuppressed = mIsAnimationSuppressed;
+ mIsAnimationSuppressed = true;
+ Show(true);
+ mIsAnimationSuppressed = wasAnimationSuppressed;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+class FullscreenTransitionData : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit FullscreenTransitionData(NSWindow* aWindow)
+ : mTransitionWindow(aWindow) {}
+
+ NSWindow* mTransitionWindow;
+
+ private:
+ virtual ~FullscreenTransitionData() { [mTransitionWindow close]; }
+};
+
+NS_IMPL_ISUPPORTS0(FullscreenTransitionData)
+
+@interface FullscreenTransitionDelegate : NSObject <NSAnimationDelegate> {
+ @public
+ nsCocoaWindow* mWindow;
+ nsIRunnable* mCallback;
+}
+@end
+
+@implementation FullscreenTransitionDelegate
+- (void)cleanupAndDispatch:(NSAnimation*)animation {
+ [animation setDelegate:nil];
+ [self autorelease];
+ // The caller should have added ref for us.
+ NS_DispatchToMainThread(already_AddRefed<nsIRunnable>(mCallback));
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ MOZ_ASSERT(animation == mWindow->FullscreenTransitionAnimation(),
+ "Should be handling the only animation on the window");
+ mWindow->ReleaseFullscreenTransitionAnimation();
+ [self cleanupAndDispatch:animation];
+}
+
+- (void)animationDidStop:(NSAnimation*)animation {
+ [self cleanupAndDispatch:animation];
+}
+@end
+
+static bool AlwaysUsesNativeFullScreen() {
+ return Preferences::GetBool("full-screen-api.macos-native-full-screen",
+ false);
+}
+
+/* virtual */ bool nsCocoaWindow::PrepareForFullscreenTransition(
+ nsISupports** aData) {
+ if (AlwaysUsesNativeFullScreen()) {
+ return false;
+ }
+
+ // Our fullscreen transition creates a new window occluding this window.
+ // That triggers an occlusion event which can cause DOM fullscreen requests
+ // to fail due to the context not being focused at the time the focus check
+ // is performed in the child process. Until the transition is cleaned up in
+ // CleanupFullscreenTransition(), ignore occlusion events for this window.
+ // If this method is changed to return false, the transition will not be
+ // performed and mIgnoreOcclusionCount should not be incremented.
+ MOZ_ASSERT(mIgnoreOcclusionCount >= 0);
+ mIgnoreOcclusionCount++;
+
+ nsCOMPtr<nsIScreen> widgetScreen = GetWidgetScreen();
+ NSScreen* cocoaScreen = ScreenHelperCocoa::CocoaScreenForScreen(widgetScreen);
+
+ NSWindow* win =
+ [[NSWindow alloc] initWithContentRect:[cocoaScreen frame]
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:YES];
+ [win setBackgroundColor:[NSColor blackColor]];
+ [win setAlphaValue:0];
+ [win setIgnoresMouseEvents:YES];
+ [win setLevel:NSScreenSaverWindowLevel];
+ [win makeKeyAndOrderFront:nil];
+
+ auto data = new FullscreenTransitionData(win);
+ *aData = data;
+ NS_ADDREF(data);
+ return true;
+}
+
+/* virtual */ void nsCocoaWindow::CleanupFullscreenTransition() {
+ MOZ_ASSERT(mIgnoreOcclusionCount > 0);
+ mIgnoreOcclusionCount--;
+}
+
+/* virtual */ void nsCocoaWindow::PerformFullscreenTransition(
+ FullscreenTransitionStage aStage, uint16_t aDuration, nsISupports* aData,
+ nsIRunnable* aCallback) {
+ auto data = static_cast<FullscreenTransitionData*>(aData);
+ FullscreenTransitionDelegate* delegate =
+ [[FullscreenTransitionDelegate alloc] init];
+ delegate->mWindow = this;
+ // Storing already_AddRefed directly could cause static checking fail.
+ delegate->mCallback = nsCOMPtr<nsIRunnable>(aCallback).forget().take();
+
+ if (mFullscreenTransitionAnimation) {
+ [mFullscreenTransitionAnimation stopAnimation];
+ ReleaseFullscreenTransitionAnimation();
+ }
+
+ NSDictionary* dict = @{
+ NSViewAnimationTargetKey : data->mTransitionWindow,
+ NSViewAnimationEffectKey : aStage == eBeforeFullscreenToggle
+ ? NSViewAnimationFadeInEffect
+ : NSViewAnimationFadeOutEffect
+ };
+ mFullscreenTransitionAnimation =
+ [[NSViewAnimation alloc] initWithViewAnimations:@[ dict ]];
+ [mFullscreenTransitionAnimation setDelegate:delegate];
+ [mFullscreenTransitionAnimation setDuration:aDuration / 1000.0];
+ [mFullscreenTransitionAnimation startAnimation];
+}
+
+void nsCocoaWindow::CocoaWindowWillEnterFullscreen(bool aFullscreen) {
+ MOZ_ASSERT(mUpdateFullscreenOnResize.isNothing());
+
+ mHasStartedNativeFullscreen = true;
+
+ // Ensure that we update our fullscreen state as early as possible, when the
+ // resize happens.
+ mUpdateFullscreenOnResize =
+ Some(aFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed);
+}
+
+void nsCocoaWindow::CocoaWindowDidEnterFullscreen(bool aFullscreen) {
+ EndOurNativeTransition();
+ mHasStartedNativeFullscreen = false;
+ DispatchOcclusionEvent();
+
+ // Check if aFullscreen matches our expected fullscreen state. It might not if
+ // there was a failure somewhere along the way, in which case we'll recover
+ // from that.
+ bool receivedExpectedFullscreen = false;
+ if (mUpdateFullscreenOnResize.isSome()) {
+ bool expectingFullscreen =
+ (*mUpdateFullscreenOnResize == TransitionType::Fullscreen);
+ receivedExpectedFullscreen = (expectingFullscreen == aFullscreen);
+ } else {
+ receivedExpectedFullscreen = (mInFullScreenMode == aFullscreen);
+ }
+
+ TransitionType transition =
+ aFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed;
+ if (receivedExpectedFullscreen) {
+ // Everything is as expected. Update our state if needed.
+ HandleUpdateFullscreenOnResize();
+ } else {
+ // We weren't expecting this fullscreen state. Update our fullscreen state
+ // to the new reality.
+ UpdateFullscreenState(aFullscreen, true);
+
+ // If we have a current transition, switch it to match what we just did.
+ if (mTransitionCurrent.isSome()) {
+ mTransitionCurrent = Some(transition);
+ }
+ }
+
+ // Whether we expected this transition or not, we're ready to finish it.
+ FinishCurrentTransitionIfMatching(transition);
+}
+
+void nsCocoaWindow::UpdateFullscreenState(bool aFullScreen, bool aNativeMode) {
+ bool wasInFullscreen = mInFullScreenMode;
+ mInFullScreenMode = aFullScreen;
+ if (aNativeMode || mInNativeFullScreenMode) {
+ mInNativeFullScreenMode = aFullScreen;
+ }
+
+ if (aFullScreen == wasInFullscreen) {
+ return;
+ }
+
+ DispatchSizeModeEvent();
+
+ // Notify the mainChildView with our new fullscreen state.
+ nsChildView* mainChildView =
+ static_cast<nsChildView*>([[mWindow mainChildView] widget]);
+ if (mainChildView) {
+ mainChildView->UpdateFullscreen(aFullScreen);
+ }
+}
+
+nsresult nsCocoaWindow::MakeFullScreen(bool aFullScreen) {
+ return DoMakeFullScreen(aFullScreen, AlwaysUsesNativeFullScreen());
+}
+
+nsresult nsCocoaWindow::MakeFullScreenWithNativeTransition(bool aFullScreen) {
+ return DoMakeFullScreen(aFullScreen, true);
+}
+
+nsresult nsCocoaWindow::DoMakeFullScreen(bool aFullScreen,
+ bool aUseSystemTransition) {
+ if (!mWindow) {
+ return NS_OK;
+ }
+
+ // Figure out what type of transition is being requested.
+ TransitionType transition = TransitionType::Windowed;
+ if (aFullScreen) {
+ // Decide whether to use fullscreen or emulated fullscreen.
+ transition =
+ (aUseSystemTransition && (mWindow.collectionBehavior &
+ NSWindowCollectionBehaviorFullScreenPrimary))
+ ? TransitionType::Fullscreen
+ : TransitionType::EmulatedFullscreen;
+ }
+
+ QueueTransition(transition);
+ return NS_OK;
+}
+
+void nsCocoaWindow::QueueTransition(const TransitionType& aTransition) {
+ mTransitionsPending.push(aTransition);
+ ProcessTransitions();
+}
+
+void nsCocoaWindow::ProcessTransitions() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ if (mInProcessTransitions) {
+ return;
+ }
+
+ mInProcessTransitions = true;
+
+ // Start a loop that will continue as long as we have transitions to process
+ // and we aren't waiting on an asynchronous transition to complete. Any
+ // transition that starts something async will `continue` this loop to exit.
+ while (!mTransitionsPending.empty() && !IsInTransition()) {
+ TransitionType nextTransition = mTransitionsPending.front();
+
+ // We have to check for some incompatible transition states, and if we find
+ // one, instead perform an alternative transition and leave the queue
+ // untouched. If we add one of these transitions, we set
+ // mIsTransitionCurrentAdded because we don't want to confuse listeners who
+ // are expecting to receive exactly one event when the requested transition
+ // has completed.
+ switch (nextTransition) {
+ case TransitionType::Fullscreen:
+ case TransitionType::EmulatedFullscreen:
+ case TransitionType::Windowed:
+ case TransitionType::Zoom:
+ // These can't handle miniaturized windows, so deminiaturize first.
+ if (mWindow.miniaturized) {
+ mTransitionCurrent = Some(TransitionType::Deminiaturize);
+ mIsTransitionCurrentAdded = true;
+ }
+ break;
+ case TransitionType::Miniaturize:
+ // This can't handle fullscreen, so go to windowed first.
+ if (mInFullScreenMode) {
+ mTransitionCurrent = Some(TransitionType::Windowed);
+ mIsTransitionCurrentAdded = true;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // If mTransitionCurrent is still empty, then we use the nextTransition and
+ // pop the queue.
+ if (mTransitionCurrent.isNothing()) {
+ mTransitionCurrent = Some(nextTransition);
+ mTransitionsPending.pop();
+ }
+
+ switch (*mTransitionCurrent) {
+ case TransitionType::Fullscreen: {
+ if (!mInFullScreenMode) {
+ // Run a local run loop until it is safe to start a native fullscreen
+ // transition.
+ NSRunLoop* localRunLoop = [NSRunLoop currentRunLoop];
+ while (mWindow && !CanStartNativeTransition() &&
+ [localRunLoop runMode:NSDefaultRunLoopMode
+ beforeDate:[NSDate distantFuture]]) {
+ // This loop continues to process events until
+ // CanStartNativeTransition() returns true or our native
+ // window has been destroyed.
+ }
+
+ // This triggers an async animation, so continue.
+ [mWindow toggleFullScreen:nil];
+ continue;
+ }
+ break;
+ }
+
+ case TransitionType::EmulatedFullscreen: {
+ if (!mInFullScreenMode) {
+ NSDisableScreenUpdates();
+ mSuppressSizeModeEvents = true;
+ // The order here matters. When we exit full screen mode, we need to
+ // show the Dock first, otherwise the newly-created window won't have
+ // its minimize button enabled. See bug 526282.
+ nsCocoaUtils::HideOSChromeOnScreen(true);
+ nsBaseWidget::InfallibleMakeFullScreen(true);
+ mSuppressSizeModeEvents = false;
+ NSEnableScreenUpdates();
+ UpdateFullscreenState(true, false);
+ }
+ break;
+ }
+
+ case TransitionType::Windowed: {
+ if (mInFullScreenMode) {
+ if (mInNativeFullScreenMode) {
+ // Run a local run loop until it is safe to start a native
+ // fullscreen transition.
+ NSRunLoop* localRunLoop = [NSRunLoop currentRunLoop];
+ while (mWindow && !CanStartNativeTransition() &&
+ [localRunLoop runMode:NSDefaultRunLoopMode
+ beforeDate:[NSDate distantFuture]]) {
+ // This loop continues to process events until
+ // CanStartNativeTransition() returns true or our native
+ // window has been destroyed.
+ }
+
+ // This triggers an async animation, so continue.
+ [mWindow toggleFullScreen:nil];
+ continue;
+ } else {
+ NSDisableScreenUpdates();
+ mSuppressSizeModeEvents = true;
+ // The order here matters. When we exit full screen mode, we need to
+ // show the Dock first, otherwise the newly-created window won't
+ // have its minimize button enabled. See bug 526282.
+ nsCocoaUtils::HideOSChromeOnScreen(false);
+ nsBaseWidget::InfallibleMakeFullScreen(false);
+ mSuppressSizeModeEvents = false;
+ NSEnableScreenUpdates();
+ UpdateFullscreenState(false, false);
+ }
+ } else if (mWindow.zoomed) {
+ [mWindow zoom:nil];
+
+ // Check if we're still zoomed. If we are, we need to do *something*
+ // to make the window smaller than the zoom size so Cocoa will treat
+ // us as being out of the zoomed state. Otherwise, we could stay
+ // zoomed and never be able to be "normal" from calls to SetSizeMode.
+ if (mWindow.zoomed) {
+ NSRect maximumFrame = mWindow.frame;
+ const CGFloat INSET_OUT_OF_ZOOM = 20.0f;
+ [mWindow setFrame:NSInsetRect(maximumFrame, INSET_OUT_OF_ZOOM,
+ INSET_OUT_OF_ZOOM)
+ display:YES];
+ MOZ_ASSERT(
+ !mWindow.zoomed,
+ "We should be able to unzoom by shrinking the frame a bit.");
+ }
+ }
+ break;
+ }
+
+ case TransitionType::Miniaturize:
+ if (!mWindow.miniaturized) {
+ // This triggers an async animation, so continue.
+ [mWindow miniaturize:nil];
+ continue;
+ }
+ break;
+
+ case TransitionType::Deminiaturize:
+ if (mWindow.miniaturized) {
+ // This triggers an async animation, so continue.
+ [mWindow deminiaturize:nil];
+ continue;
+ }
+ break;
+
+ case TransitionType::Zoom:
+ if (!mWindow.zoomed) {
+ [mWindow zoom:nil];
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ mTransitionCurrent.reset();
+ mIsTransitionCurrentAdded = false;
+ }
+
+ mInProcessTransitions = false;
+
+ // When we finish processing transitions, dispatch a size mode event to cover
+ // the cases where an inserted transition suppressed one, and the original
+ // transition never sent one because it detected it was at the desired state
+ // when it ran. If we've already sent a size mode event, then this will be a
+ // no-op.
+ if (!IsInTransition()) {
+ DispatchSizeModeEvent();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::CancelAllTransitions() {
+ // Clear our current and pending transitions. This simplifies our
+ // reasoning about what happens next, and ensures that whatever is
+ // currently happening won't trigger another call to
+ // ProcessTransitions().
+ mTransitionCurrent.reset();
+ mIsTransitionCurrentAdded = false;
+ std::queue<TransitionType>().swap(mTransitionsPending);
+}
+
+void nsCocoaWindow::FinishCurrentTransitionIfMatching(
+ const TransitionType& aTransition) {
+ // We've just finished some transition activity, and we're not sure whether it
+ // was triggered programmatically, or by the user. If it matches our current
+ // transition, then assume it was triggered programmatically and we can clean
+ // up that transition and start processing transitions again.
+
+ // Whether programmatic or user-initiated, we send out a size mode event.
+ DispatchSizeModeEvent();
+
+ if (mTransitionCurrent.isSome() && (*mTransitionCurrent == aTransition)) {
+ // This matches our current transition, so do the safe parts of transition
+ // cleanup.
+ mTransitionCurrent.reset();
+ mIsTransitionCurrentAdded = false;
+
+ // Since this function is called from nsWindowDelegate transition callbacks,
+ // we want to make sure those callbacks are all the way done before we
+ // continue processing more transitions. To accomplish this, we dispatch
+ // ProcessTransitions on the next event loop. Doing this will ensure that
+ // any async native transition methods we call (like toggleFullScreen) will
+ // succeed.
+ if (!mTransitionsPending.empty()) {
+ NS_DispatchToCurrentThread(NewRunnableMethod(
+ "FinishCurrentTransition", this, &nsCocoaWindow::ProcessTransitions));
+ }
+ }
+}
+
+bool nsCocoaWindow::HandleUpdateFullscreenOnResize() {
+ if (mUpdateFullscreenOnResize.isNothing()) {
+ return false;
+ }
+
+ bool toFullscreen =
+ (*mUpdateFullscreenOnResize == TransitionType::Fullscreen);
+ mUpdateFullscreenOnResize.reset();
+ UpdateFullscreenState(toFullscreen, true);
+
+ return true;
+}
+
+/* static */ nsCocoaWindow* nsCocoaWindow::sWindowInNativeTransition(nullptr);
+
+bool nsCocoaWindow::CanStartNativeTransition() {
+ if (sWindowInNativeTransition == nullptr) {
+ // Claim it and return true, indicating that the caller has permission to
+ // start the native fullscreen transition.
+ sWindowInNativeTransition = this;
+ return true;
+ }
+ return false;
+}
+
+void nsCocoaWindow::EndOurNativeTransition() {
+ if (sWindowInNativeTransition == this) {
+ sWindowInNativeTransition = nullptr;
+ }
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::DoResize(double aX, double aY, double aWidth,
+ double aHeight, bool aRepaint,
+ bool aConstrainToCurrentScreen) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow || mInResize) {
+ return;
+ }
+
+ // We are able to resize a window outside of any aspect ratio contraints
+ // applied to it, but in order to "update" the aspect ratio contraint to the
+ // new window dimensions, we must re-lock the aspect ratio.
+ auto relockAspectRatio = MakeScopeExit([&]() {
+ if (mAspectRatioLocked) {
+ LockAspectRatio(true);
+ }
+ });
+
+ AutoRestore<bool> reentrantResizeGuard(mInResize);
+ mInResize = true;
+
+ CGFloat scale = mSizeConstraints.mScale.scale;
+ if (scale == MOZ_WIDGET_INVALID_SCALE) {
+ scale = BackingScaleFactor();
+ }
+
+ // mSizeConstraints is in device pixels.
+ int32_t width = NSToIntRound(aWidth * scale);
+ int32_t height = NSToIntRound(aHeight * scale);
+
+ width = std::max(mSizeConstraints.mMinSize.width,
+ std::min(mSizeConstraints.mMaxSize.width, width));
+ height = std::max(mSizeConstraints.mMinSize.height,
+ std::min(mSizeConstraints.mMaxSize.height, height));
+
+ DesktopIntRect newBounds(NSToIntRound(aX), NSToIntRound(aY),
+ NSToIntRound(width / scale),
+ NSToIntRound(height / scale));
+
+ // convert requested bounds into Cocoa coordinate system
+ NSRect newFrame = nsCocoaUtils::GeckoRectToCocoaRect(newBounds);
+
+ NSRect frame = [mWindow frame];
+ BOOL isMoving = newFrame.origin.x != frame.origin.x ||
+ newFrame.origin.y != frame.origin.y;
+ BOOL isResizing = newFrame.size.width != frame.size.width ||
+ newFrame.size.height != frame.size.height;
+
+ if (!isMoving && !isResizing) {
+ return;
+ }
+
+ // We ignore aRepaint -- we have to call display:YES, otherwise the
+ // title bar doesn't immediately get repainted and is displayed in
+ // the wrong place, leading to a visual jump.
+ [mWindow setFrame:newFrame display:YES];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ DoResize(aX, aY, aWidth, aHeight, aRepaint, false);
+}
+
+// Coordinates are desktop pixels
+void nsCocoaWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
+ double invScale = 1.0 / BackingScaleFactor();
+ DoResize(mBounds.x * invScale, mBounds.y * invScale, aWidth, aHeight,
+ aRepaint, true);
+}
+
+// Return the area that the Gecko ChildView in our window should cover, as an
+// NSRect in screen coordinates (with 0,0 being the bottom left corner of the
+// primary screen).
+NSRect nsCocoaWindow::GetClientCocoaRect() {
+ if (!mWindow) {
+ return NSZeroRect;
+ }
+
+ return [mWindow childViewRectForFrameRect:[mWindow frame]];
+}
+
+LayoutDeviceIntRect nsCocoaWindow::GetClientBounds() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = BackingScaleFactor();
+ return nsCocoaUtils::CocoaRectToGeckoRectDevPix(GetClientCocoaRect(),
+ scaleFactor);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0));
+}
+
+void nsCocoaWindow::UpdateBounds() {
+ NSRect frame = NSZeroRect;
+ if (mWindow) {
+ frame = [mWindow frame];
+ }
+ mBounds =
+ nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, BackingScaleFactor());
+
+ if (mPopupContentView) {
+ mPopupContentView->UpdateBoundsFromView();
+ }
+}
+
+LayoutDeviceIntRect nsCocoaWindow::GetScreenBounds() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+#ifdef DEBUG
+ LayoutDeviceIntRect r = nsCocoaUtils::CocoaRectToGeckoRectDevPix(
+ [mWindow frame], BackingScaleFactor());
+ NS_ASSERTION(mWindow && mBounds == r, "mBounds out of sync!");
+#endif
+
+ return mBounds;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0));
+}
+
+double nsCocoaWindow::GetDefaultScaleInternal() { return BackingScaleFactor(); }
+
+static CGFloat GetBackingScaleFactor(NSWindow* aWindow) {
+ NSRect frame = [aWindow frame];
+ if (frame.size.width > 0 && frame.size.height > 0) {
+ return nsCocoaUtils::GetBackingScaleFactor(aWindow);
+ }
+
+ // For windows with zero width or height, the backingScaleFactor method
+ // is broken - it will always return 2 on a retina macbook, even when
+ // the window position implies it's on a non-hidpi external display
+ // (to the extent that a zero-area window can be said to be "on" a
+ // display at all!)
+ // And to make matters worse, Cocoa even fires a
+ // windowDidChangeBackingProperties notification with the
+ // NSBackingPropertyOldScaleFactorKey key when a window on an
+ // external display is resized to/from zero height, even though it hasn't
+ // really changed screens.
+
+ // This causes us to handle popup window sizing incorrectly when the
+ // popup is resized to zero height (bug 820327) - nsXULPopupManager
+ // becomes (incorrectly) convinced the popup has been explicitly forced
+ // to a non-default size and needs to have size attributes attached.
+
+ // Workaround: instead of asking the window, we'll find the screen it is on
+ // and ask that for *its* backing scale factor.
+
+ // (See bug 853252 and additional comments in windowDidChangeScreen: below
+ // for further complications this causes.)
+
+ // First, expand the rect so that it actually has a measurable area,
+ // for FindTargetScreenForRect to use.
+ if (frame.size.width == 0) {
+ frame.size.width = 1;
+ }
+ if (frame.size.height == 0) {
+ frame.size.height = 1;
+ }
+
+ // Then identify the screen it belongs to, and return its scale factor.
+ NSScreen* screen =
+ FindTargetScreenForRect(nsCocoaUtils::CocoaRectToGeckoRect(frame));
+ return nsCocoaUtils::GetBackingScaleFactor(screen);
+}
+
+CGFloat nsCocoaWindow::BackingScaleFactor() {
+ if (mBackingScaleFactor > 0.0) {
+ return mBackingScaleFactor;
+ }
+ if (!mWindow) {
+ return 1.0;
+ }
+ mBackingScaleFactor = GetBackingScaleFactor(mWindow);
+ return mBackingScaleFactor;
+}
+
+void nsCocoaWindow::BackingScaleFactorChanged() {
+ CGFloat newScale = GetBackingScaleFactor(mWindow);
+
+ // ignore notification if it hasn't really changed (or maybe we have
+ // disabled HiDPI mode via prefs)
+ if (mBackingScaleFactor == newScale) {
+ return;
+ }
+
+ mBackingScaleFactor = newScale;
+
+ if (!mWidgetListener || mWidgetListener->GetAppWindow()) {
+ return;
+ }
+
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ mWidgetListener->UIResolutionChanged();
+}
+
+int32_t nsCocoaWindow::RoundsWidgetCoordinatesTo() {
+ if (BackingScaleFactor() == 2.0) {
+ return 2;
+ }
+ return 1;
+}
+
+void nsCocoaWindow::SetCursor(const Cursor& aCursor) {
+ if (mPopupContentView) {
+ mPopupContentView->SetCursor(aCursor);
+ }
+}
+
+nsresult nsCocoaWindow::SetTitle(const nsAString& aTitle) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mWindow) {
+ return NS_OK;
+ }
+
+ const nsString& strTitle = PromiseFlatString(aTitle);
+ const unichar* uniTitle = reinterpret_cast<const unichar*>(strTitle.get());
+ NSString* title = [NSString stringWithCharacters:uniTitle
+ length:strTitle.Length()];
+ if ([mWindow drawsContentsIntoWindowFrame] && ![mWindow wantsTitleDrawn]) {
+ // Don't cause invalidations when the title isn't displayed.
+ [mWindow disableSetNeedsDisplay];
+ [mWindow setTitle:title];
+ [mWindow enableSetNeedsDisplay];
+ } else {
+ [mWindow setTitle:title];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::Invalidate(const LayoutDeviceIntRect& aRect) {
+ if (mPopupContentView) {
+ mPopupContentView->Invalidate(aRect);
+ }
+}
+
+// Pass notification of some drag event to Gecko
+//
+// The drag manager has let us know that something related to a drag has
+// occurred in this window. It could be any number of things, ranging from
+// a drop, to a drag enter/leave, or a drag over event. The actual event
+// is passed in |aMessage| and is passed along to our event hanlder so Gecko
+// knows about it.
+bool nsCocoaWindow::DragEvent(unsigned int aMessage,
+ mozilla::gfx::Point aMouseGlobal,
+ UInt16 aKeyModifiers) {
+ return false;
+}
+
+NS_IMETHODIMP nsCocoaWindow::SendSetZLevelEvent() {
+ nsWindowZ placement = nsWindowZTop;
+ nsCOMPtr<nsIWidget> actualBelow;
+ if (mWidgetListener)
+ mWidgetListener->ZLevelChanged(true, &placement, nullptr,
+ getter_AddRefs(actualBelow));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetChildSheet(bool aShown, nsIWidget** _retval) {
+ nsIWidget* child = GetFirstChild();
+
+ while (child) {
+ if (child->GetWindowType() == WindowType::Sheet) {
+ // if it's a sheet, it must be an nsCocoaWindow
+ nsCocoaWindow* cocoaWindow = static_cast<nsCocoaWindow*>(child);
+ if (cocoaWindow->mWindow &&
+ ((aShown && [cocoaWindow->mWindow isVisible]) ||
+ (!aShown && cocoaWindow->mSheetNeedsShow))) {
+ nsCOMPtr<nsIWidget> widget = cocoaWindow;
+ widget.forget(_retval);
+ return NS_OK;
+ }
+ }
+ child = child->GetNextSibling();
+ }
+
+ *_retval = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetRealParent(nsIWidget** parent) {
+ *parent = mParent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetIsSheet(bool* isSheet) {
+ mWindowType == WindowType::Sheet ? * isSheet = true : * isSheet = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::GetSheetWindowParent(
+ NSWindow** sheetWindowParent) {
+ *sheetWindowParent = mSheetWindowParent;
+ return NS_OK;
+}
+
+// Invokes callback and ProcessEvent methods on Event Listener object
+nsresult nsCocoaWindow::DispatchEvent(WidgetGUIEvent* event,
+ nsEventStatus& aStatus) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(event->mWidget);
+ mozilla::Unused << kungFuDeathGrip; // Not used within this function
+
+ if (mWidgetListener)
+ aStatus = mWidgetListener->HandleEvent(event, mUseAttachedEvents);
+
+ return NS_OK;
+}
+
+// aFullScreen should be the window's mInFullScreenMode. We don't have access to
+// that from here, so we need to pass it in. mInFullScreenMode should be the
+// canonical indicator that a window is currently full screen and it makes sense
+// to keep all sizemode logic here.
+static nsSizeMode GetWindowSizeMode(NSWindow* aWindow, bool aFullScreen) {
+ if (aFullScreen) return nsSizeMode_Fullscreen;
+ if ([aWindow isMiniaturized]) return nsSizeMode_Minimized;
+ if (([aWindow styleMask] & NSWindowStyleMaskResizable) && [aWindow isZoomed])
+ return nsSizeMode_Maximized;
+ return nsSizeMode_Normal;
+}
+
+void nsCocoaWindow::ReportMoveEvent() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Prevent recursion, which can become infinite (see bug 708278). This
+ // can happen when the call to [NSWindow setFrameTopLeftPoint:] in
+ // nsCocoaWindow::Move() triggers an immediate NSWindowDidMove notification
+ // (and a call to [WindowDelegate windowDidMove:]).
+ if (mInReportMoveEvent) {
+ return;
+ }
+ mInReportMoveEvent = true;
+
+ UpdateBounds();
+
+ // The zoomed state can change when we're moving, in which case we need to
+ // update our internal mSizeMode. This can happen either if we're maximized
+ // and then moved, or if we're not maximized and moved back to zoomed state.
+ if (mWindow && ((mSizeMode == nsSizeMode_Maximized) ^ [mWindow isZoomed])) {
+ DispatchSizeModeEvent();
+ }
+
+ // Dispatch the move event to Gecko
+ NotifyWindowMoved(mBounds.x, mBounds.y);
+
+ mInReportMoveEvent = false;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::DispatchSizeModeEvent() {
+ if (!mWindow) {
+ return;
+ }
+
+ if (mSuppressSizeModeEvents || mIsTransitionCurrentAdded) {
+ return;
+ }
+
+ nsSizeMode newMode = GetWindowSizeMode(mWindow, mInFullScreenMode);
+ if (mSizeMode == newMode) {
+ return;
+ }
+
+ mSizeMode = newMode;
+ if (mWidgetListener) {
+ mWidgetListener->SizeModeChanged(newMode);
+ }
+}
+
+void nsCocoaWindow::DispatchOcclusionEvent() {
+ if (!mWindow) {
+ return;
+ }
+
+ // Our new occlusion state is true if the window is not visible.
+ bool newOcclusionState =
+ !(mHasStartedNativeFullscreen ||
+ ([mWindow occlusionState] & NSWindowOcclusionStateVisible));
+
+ // Don't dispatch if the new occlustion state is the same as the current
+ // state.
+ if (mIsFullyOccluded == newOcclusionState) {
+ return;
+ }
+
+ MOZ_ASSERT(mIgnoreOcclusionCount >= 0);
+ if (newOcclusionState && mIgnoreOcclusionCount > 0) {
+ return;
+ }
+
+ mIsFullyOccluded = newOcclusionState;
+ if (mWidgetListener) {
+ mWidgetListener->OcclusionStateChanged(mIsFullyOccluded);
+ }
+}
+
+void nsCocoaWindow::ReportSizeEvent() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ UpdateBounds();
+ if (mWidgetListener) {
+ LayoutDeviceIntRect innerBounds = GetClientBounds();
+ mWidgetListener->WindowResized(this, innerBounds.width, innerBounds.height);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetMenuBar(RefPtr<nsMenuBarX>&& aMenuBar) {
+ if (!mWindow) {
+ mMenuBar = nullptr;
+ return;
+ }
+ mMenuBar = std::move(aMenuBar);
+
+ // Only paint for active windows, or paint the hidden window menu bar if no
+ // other menu bar has been painted yet so that some reasonable menu bar is
+ // displayed when the app starts up.
+ if (mMenuBar && ((!gSomeMenuBarPainted &&
+ nsMenuUtilsX::GetHiddenWindowMenuBar() == mMenuBar) ||
+ [mWindow isMainWindow]))
+ mMenuBar->Paint();
+}
+
+void nsCocoaWindow::SetFocus(Raise aRaise,
+ mozilla::dom::CallerType aCallerType) {
+ if (!mWindow) return;
+
+ if (mPopupContentView) {
+ return mPopupContentView->SetFocus(aRaise, aCallerType);
+ }
+
+ if (aRaise == Raise::Yes &&
+ ([mWindow isVisible] || [mWindow isMiniaturized])) {
+ if ([mWindow isMiniaturized]) {
+ [mWindow deminiaturize:nil];
+ }
+
+ [mWindow makeKeyAndOrderFront:nil];
+ SendSetZLevelEvent();
+ }
+}
+
+LayoutDeviceIntPoint nsCocoaWindow::WidgetToScreenOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return nsCocoaUtils::CocoaRectToGeckoRectDevPix(GetClientCocoaRect(),
+ BackingScaleFactor())
+ .TopLeft();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+LayoutDeviceIntPoint nsCocoaWindow::GetClientOffset() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ LayoutDeviceIntRect clientRect = GetClientBounds();
+
+ return clientRect.TopLeft() - mBounds.TopLeft();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0));
+}
+
+LayoutDeviceIntMargin nsCocoaWindow::ClientToWindowMargin() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mWindow || mWindow.drawsContentsIntoWindowFrame ||
+ mWindowType == WindowType::Popup) {
+ return {};
+ }
+
+ NSRect clientNSRect = mWindow.contentLayoutRect;
+ NSRect frameNSRect = [mWindow frameRectForChildViewRect:clientNSRect];
+
+ CGFloat backingScale = BackingScaleFactor();
+ const auto clientRect =
+ nsCocoaUtils::CocoaRectToGeckoRectDevPix(clientNSRect, backingScale);
+ const auto frameRect =
+ nsCocoaUtils::CocoaRectToGeckoRectDevPix(frameNSRect, backingScale);
+
+ return frameRect - clientRect;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN({});
+}
+
+nsMenuBarX* nsCocoaWindow::GetMenuBar() { return mMenuBar; }
+
+void nsCocoaWindow::CaptureRollupEvents(bool aDoCapture) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (aDoCapture) {
+ if (![NSApp isActive]) {
+ // We need to capture mouse event if we aren't
+ // the active application. We only set this up when needed
+ // because they cause spurious mouse event after crash
+ // and gdb sessions. See bug 699538.
+ nsToolkit::GetToolkit()->MonitorAllProcessMouseEvents();
+ }
+
+ // Sometimes more than one popup window can be visible at the same time
+ // (e.g. nested non-native context menus, or the test case (attachment
+ // 276885) for bmo bug 392389, which displays a non-native combo-box in a
+ // non-native popup window). In these cases the "active" popup window
+ // should be the topmost -- the (nested) context menu the mouse is currently
+ // over, or the combo-box's drop-down list (when it's displayed). But
+ // (among windows that have the same "level") OS X makes topmost the window
+ // that last received a mouse-down event, which may be incorrect (in the
+ // combo-box case, it makes topmost the window containing the combo-box).
+ // So here we fiddle with a non-native popup window's level to make sure the
+ // "active" one is always above any other non-native popup windows that
+ // may be visible.
+ if (mWindow && (mWindowType == WindowType::Popup)) SetPopupWindowLevel();
+ } else {
+ nsToolkit::GetToolkit()->StopMonitoringAllProcessMouseEvents();
+
+ // XXXndeakin this doesn't make sense.
+ // Why is the new window assumed to be a modal panel?
+ if (mWindow && (mWindowType == WindowType::Popup))
+ [mWindow setLevel:NSModalPanelWindowLevel];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsresult nsCocoaWindow::GetAttention(int32_t aCycleCount) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [NSApp requestUserAttention:NSInformationalRequest];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool nsCocoaWindow::HasPendingInputEvent() {
+ return nsChildView::DoHasPendingInputEvent();
+}
+
+void nsCocoaWindow::SetWindowShadowStyle(WindowShadow aStyle) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mShadowStyle == aStyle) {
+ return;
+ }
+
+ mShadowStyle = aStyle;
+
+ if (!mWindow || mWindowType != WindowType::Popup) {
+ return;
+ }
+
+ mWindow.shadowStyle = mShadowStyle;
+ [mWindow setEffectViewWrapperForStyle:mShadowStyle];
+ [mWindow setHasShadow:aStyle != WindowShadow::None];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetWindowOpacity(float aOpacity) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ [mWindow setAlphaValue:(CGFloat)aOpacity];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetColorScheme(const Maybe<ColorScheme>& aScheme) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ mWindow.appearance = aScheme ? NSAppearanceForColorScheme(*aScheme) : nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static inline CGAffineTransform GfxMatrixToCGAffineTransform(
+ const gfx::Matrix& m) {
+ CGAffineTransform t;
+ t.a = m._11;
+ t.b = m._12;
+ t.c = m._21;
+ t.d = m._22;
+ t.tx = m._31;
+ t.ty = m._32;
+ return t;
+}
+
+void nsCocoaWindow::SetWindowTransform(const gfx::Matrix& aTransform) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) {
+ return;
+ }
+
+ // Calling CGSSetWindowTransform when the window is not visible results in
+ // misplacing the window into doubled x,y coordinates (see bug 1448132).
+ if (![mWindow isVisible] || NSIsEmptyRect([mWindow frame])) {
+ return;
+ }
+
+ if (StaticPrefs::widget_window_transforms_disabled()) {
+ // CGSSetWindowTransform is a private API. In case calling it causes
+ // problems either now or in the future, we'll want to have an easy kill
+ // switch. So we allow disabling it with a pref.
+ return;
+ }
+
+ gfx::Matrix transform = aTransform;
+
+ // aTransform is a transform that should be applied to the window relative
+ // to its regular position: If aTransform._31 is 100, then we want the
+ // window to be displayed 100 pixels to the right of its regular position.
+ // The transform that CGSSetWindowTransform accepts has a different meaning:
+ // It's used to answer the question "For the screen pixel at x,y (with the
+ // origin at the top left), what pixel in the window's buffer (again with
+ // origin top left) should be displayed at that position?"
+ // In the example above, this means that we need to call
+ // CGSSetWindowTransform with a horizontal translation of -windowPos.x - 100.
+ // So we need to invert the transform and adjust it by the window's position.
+ if (!transform.Invert()) {
+ // Treat non-invertible transforms as the identity transform.
+ transform = gfx::Matrix();
+ }
+
+ bool isIdentity = transform.IsIdentity();
+ if (isIdentity && mWindowTransformIsIdentity) {
+ return;
+ }
+
+ transform.PreTranslate(-mBounds.x, -mBounds.y);
+
+ // Snap translations to device pixels, to match what we do for CSS transforms
+ // and because the window server rounds down instead of to nearest.
+ if (!transform.HasNonTranslation() && transform.HasNonIntegerTranslation()) {
+ auto snappedTranslation = gfx::IntPoint::Round(transform.GetTranslation());
+ transform =
+ gfx::Matrix::Translation(snappedTranslation.x, snappedTranslation.y);
+ }
+
+ // We also need to account for the backing scale factor: aTransform is given
+ // in device pixels, but CGSSetWindowTransform works with logical display
+ // pixels.
+ CGFloat backingScale = BackingScaleFactor();
+ transform.PreScale(backingScale, backingScale);
+ transform.PostScale(1 / backingScale, 1 / backingScale);
+
+ CGSConnection cid = _CGSDefaultConnection();
+ CGSSetWindowTransform(cid, [mWindow windowNumber],
+ GfxMatrixToCGAffineTransform(transform));
+
+ mWindowTransformIsIdentity = isIdentity;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetInputRegion(const InputRegion& aInputRegion) {
+ MOZ_ASSERT(mWindowType == WindowType::Popup,
+ "This should only be called on popup windows.");
+ // TODO: Somehow support aInputRegion.mMargin? Though maybe not.
+ if (aInputRegion.mFullyTransparent) {
+ [mWindow setIgnoresMouseEvents:YES];
+ } else {
+ [mWindow setIgnoresMouseEvents:NO];
+ }
+}
+
+void nsCocoaWindow::SetShowsToolbarButton(bool aShow) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mWindow) [mWindow setShowsToolbarButton:aShow];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetSupportsNativeFullscreen(
+ bool aSupportsNativeFullscreen) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mWindow) {
+ // This determines whether we tell cocoa that the window supports native
+ // full screen. If we do so, and another window is in native full screen,
+ // this window will also appear in native full screen. We generally only
+ // want to do this for primary application windows. We'll set the
+ // relevant macnativefullscreen attribute on those, which will lead to us
+ // being called with aSupportsNativeFullscreen set to `true` here.
+ NSWindowCollectionBehavior newBehavior = [mWindow collectionBehavior];
+ if (aSupportsNativeFullscreen) {
+ newBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
+ } else {
+ newBehavior &= ~NSWindowCollectionBehaviorFullScreenPrimary;
+ }
+ [mWindow setCollectionBehavior:newBehavior];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetWindowAnimationType(
+ nsIWidget::WindowAnimationType aType) {
+ mAnimationType = aType;
+}
+
+void nsCocoaWindow::SetDrawsTitle(bool aDrawTitle) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (![mWindow drawsContentsIntoWindowFrame]) {
+ // If we don't draw into the window frame, we always want to display window
+ // titles.
+ [mWindow setWantsTitleDrawn:YES];
+ } else {
+ [mWindow setWantsTitleDrawn:aDrawTitle];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsresult nsCocoaWindow::SetNonClientMargins(
+ const LayoutDeviceIntMargin& margins) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ SetDrawsInTitlebar(margins.top == 0);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::SetDrawsInTitlebar(bool aState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mWindow) {
+ [mWindow setDrawsContentsIntoWindowFrame:aState];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMETHODIMP nsCocoaWindow::SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+ if (mPopupContentView) {
+ return mPopupContentView->SynthesizeNativeMouseEvent(
+ aPoint, aNativeMessage, aButton, aModifierFlags, nullptr);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsCocoaWindow::SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+ if (mPopupContentView) {
+ // Pass nullptr as the observer so that the AutoObserverNotification in
+ // nsChildView::SynthesizeNativeMouseScrollEvent will be ignored.
+ return mPopupContentView->SynthesizeNativeMouseScrollEvent(
+ aPoint, aNativeMessage, aDeltaX, aDeltaY, aDeltaZ, aModifierFlags,
+ aAdditionalFlags, nullptr);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsCocoaWindow::LockAspectRatio(bool aShouldLock) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (aShouldLock) {
+ [mWindow setContentAspectRatio:mWindow.frame.size];
+ mAspectRatioLocked = true;
+ } else {
+ // According to
+ // https://developer.apple.com/documentation/appkit/nswindow/1419507-aspectratio,
+ // aspect ratios and resize increments are mutually exclusive, and the
+ // accepted way of cancelling an established aspect ratio is to set the
+ // resize increments to 1.0, 1.0
+ [mWindow setResizeIncrements:NSMakeSize(1.0, 1.0)];
+ mAspectRatioLocked = false;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::UpdateThemeGeometries(
+ const nsTArray<ThemeGeometry>& aThemeGeometries) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mPopupContentView) {
+ return mPopupContentView->UpdateThemeGeometries(aThemeGeometries);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsCocoaWindow::SetPopupWindowLevel() {
+ if (!mWindow) return;
+
+ // Floating popups are at the floating level and hide when the window is
+ // deactivated.
+ if (mPopupLevel == PopupLevel::Floating) {
+ [mWindow setLevel:NSFloatingWindowLevel];
+ [mWindow setHidesOnDeactivate:YES];
+ } else {
+ // Otherwise, this is a top-level or parent popup. Parent popups always
+ // appear just above their parent and essentially ignore the level.
+ [mWindow setLevel:NSPopUpMenuWindowLevel];
+ [mWindow setHidesOnDeactivate:NO];
+ }
+}
+
+void nsCocoaWindow::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mInputContext = aContext;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+bool nsCocoaWindow::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+
+ NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
+ // When the keyboard event is fired from this widget, it must mean that no web
+ // content has focus because any web contents should be on `nsChildView`. And
+ // in any locales, the system UI is always horizontal layout. So, let's pass
+ // `Nothing()` for the writing mode here, it won't be treated as in a vertical
+ // content.
+ keyBindings->GetEditCommands(aEvent, Nothing(), aCommands);
+ return true;
+}
+
+void nsCocoaWindow::PauseOrResumeCompositor(bool aPause) {
+ if (auto* mainChildView =
+ static_cast<nsIWidget*>([[mWindow mainChildView] widget])) {
+ mainChildView->PauseOrResumeCompositor(aPause);
+ }
+}
+
+bool nsCocoaWindow::AsyncPanZoomEnabled() const {
+ if (mPopupContentView) {
+ return mPopupContentView->AsyncPanZoomEnabled();
+ }
+ return nsBaseWidget::AsyncPanZoomEnabled();
+}
+
+bool nsCocoaWindow::StartAsyncAutoscroll(const ScreenPoint& aAnchorLocation,
+ const ScrollableLayerGuid& aGuid) {
+ if (mPopupContentView) {
+ return mPopupContentView->StartAsyncAutoscroll(aAnchorLocation, aGuid);
+ }
+ return nsBaseWidget::StartAsyncAutoscroll(aAnchorLocation, aGuid);
+}
+
+void nsCocoaWindow::StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) {
+ if (mPopupContentView) {
+ mPopupContentView->StopAsyncAutoscroll(aGuid);
+ return;
+ }
+ nsBaseWidget::StopAsyncAutoscroll(aGuid);
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
+ nsCOMPtr<nsIWidget> window = new nsCocoaWindow();
+ return window.forget();
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
+ nsCOMPtr<nsIWidget> window = new nsChildView();
+ return window.forget();
+}
+
+@implementation WindowDelegate
+
+// We try to find a gecko menu bar to paint. If one does not exist, just paint
+// the application menu by itself so that a window doesn't have some other
+// window's menu bar.
++ (void)paintMenubarForWindow:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // make sure we only act on windows that have this kind of
+ // object as a delegate
+ id windowDelegate = [aWindow delegate];
+ if ([windowDelegate class] != [self class]) return;
+
+ nsCocoaWindow* geckoWidget = [windowDelegate geckoWidget];
+ NS_ASSERTION(geckoWidget, "Window delegate not returning a gecko widget!");
+
+ nsMenuBarX* geckoMenuBar = geckoWidget->GetMenuBar();
+ if (geckoMenuBar) {
+ geckoMenuBar->Paint();
+ } else {
+ // sometimes we don't have a native application menu early in launching
+ if (!sApplicationMenu) return;
+
+ NSMenu* mainMenu = [NSApp mainMenu];
+ NS_ASSERTION(
+ [mainMenu numberOfItems] > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+
+ // Create a new menu bar.
+ // We create a GeckoNSMenu because all menu bar NSMenu objects should use
+ // that subclass for key handling reasons.
+ GeckoNSMenu* newMenuBar =
+ [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"];
+
+ // move the application menu from the existing menu bar to the new one
+ NSMenuItem* firstMenuItem = [[mainMenu itemAtIndex:0] retain];
+ [mainMenu removeItemAtIndex:0];
+ [newMenuBar insertItem:firstMenuItem atIndex:0];
+ [firstMenuItem release];
+
+ // set our new menu bar as the main menu
+ [NSApp setMainMenu:newMenuBar];
+ [newMenuBar release];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (id)initWithGeckoWindow:(nsCocoaWindow*)geckoWind {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [super init];
+ mGeckoWindow = geckoWind;
+ mToplevelActiveState = false;
+ mHasEverBeenZoomed = false;
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)proposedFrameSize {
+ RollUpPopups();
+ return proposedFrameSize;
+}
+
+- (NSRect)windowWillUseStandardFrame:(NSWindow*)window
+ defaultFrame:(NSRect)newFrame {
+ // This function needs to return a rect representing the frame a window would
+ // have if it is in its "maximized" size mode. The parameter newFrame is
+ // supposed to be a frame representing the maximum window size on the screen
+ // where the window currently appears. However, in practice, newFrame can be a
+ // much smaller size. So, we ignore newframe and instead return the frame of
+ // the entire screen associated with the window. That frame is bigger than the
+ // window could actually be, due to the presence of the menubar and possibly
+ // the dock, but we never call this function directly, and Cocoa callers will
+ // shrink it to its true maximum size.
+ return window.screen.frame;
+}
+
+void nsCocoaWindow::CocoaSendToplevelActivateEvents() {
+ if (mWidgetListener) {
+ mWidgetListener->WindowActivated();
+ }
+}
+
+void nsCocoaWindow::CocoaSendToplevelDeactivateEvents() {
+ if (mWidgetListener) {
+ mWidgetListener->WindowDeactivated();
+ }
+}
+
+void nsCocoaWindow::CocoaWindowDidResize() {
+ // It's important to update our bounds before we trigger any listeners. This
+ // ensures that our bounds are correct when GetScreenBounds is called.
+ UpdateBounds();
+
+ if (HandleUpdateFullscreenOnResize()) {
+ ReportSizeEvent();
+ return;
+ }
+
+ // Resizing might have changed our zoom state.
+ DispatchSizeModeEvent();
+ ReportSizeEvent();
+}
+
+- (void)windowDidResize:(NSNotification*)aNotification {
+ BaseWindow* window = [aNotification object];
+ [window updateTrackingArea];
+
+ if (!mGeckoWindow) return;
+
+ mGeckoWindow->CocoaWindowDidResize();
+}
+
+- (void)windowDidChangeScreen:(NSNotification*)aNotification {
+ if (!mGeckoWindow) return;
+
+ // Because of Cocoa's peculiar treatment of zero-size windows (see comments
+ // at GetBackingScaleFactor() above), we sometimes have a situation where
+ // our concept of backing scale (based on the screen where the zero-sized
+ // window is positioned) differs from Cocoa's idea (always based on the
+ // Retina screen, AFAICT, even when an external non-Retina screen is the
+ // primary display).
+ //
+ // As a result, if the window was created with zero size on an external
+ // display, but then made visible on the (secondary) Retina screen, we
+ // will *not* get a windowDidChangeBackingProperties notification for it.
+ // This leads to an incorrect GetDefaultScale(), and widget coordinate
+ // confusion, as per bug 853252.
+ //
+ // To work around this, we check for a backing scale mismatch when we
+ // receive a windowDidChangeScreen notification, as we will receive this
+ // even if Cocoa was already treating the zero-size window as having
+ // Retina backing scale.
+ NSWindow* window = (NSWindow*)[aNotification object];
+ if ([window respondsToSelector:@selector(backingScaleFactor)]) {
+ if (GetBackingScaleFactor(window) != mGeckoWindow->BackingScaleFactor()) {
+ mGeckoWindow->BackingScaleFactorChanged();
+ }
+ }
+
+ mGeckoWindow->ReportMoveEvent();
+}
+
+- (void)windowWillEnterFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowWillEnterFullscreen(true);
+}
+
+// Lion's full screen mode will bypass our internal fullscreen tracking, so
+// we need to catch it when we transition and call our own methods, which in
+// turn will fire "fullscreen" events.
+- (void)windowDidEnterFullScreen:(NSNotification*)notification {
+ // On Yosemite, the NSThemeFrame class has two new properties --
+ // titlebarView (an NSTitlebarView object) and titlebarContainerView (an
+ // NSTitlebarContainerView object). These are used to display the titlebar
+ // in fullscreen mode. In Safari they're not transparent. But in Firefox
+ // for some reason they are, which causes bug 1069658. The following code
+ // works around this Apple bug or design flaw.
+ NSWindow* window = (NSWindow*)[notification object];
+ NSView* frameView = [[window contentView] superview];
+ NSView* titlebarView = nil;
+ NSView* titlebarContainerView = nil;
+ if ([frameView respondsToSelector:@selector(titlebarView)]) {
+ titlebarView = [frameView titlebarView];
+ }
+ if ([frameView respondsToSelector:@selector(titlebarContainerView)]) {
+ titlebarContainerView = [frameView titlebarContainerView];
+ }
+ if ([titlebarView respondsToSelector:@selector(setTransparent:)]) {
+ [titlebarView setTransparent:NO];
+ }
+ if ([titlebarContainerView respondsToSelector:@selector(setTransparent:)]) {
+ [titlebarContainerView setTransparent:NO];
+ }
+
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowDidEnterFullscreen(true);
+}
+
+- (void)windowWillExitFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowWillEnterFullscreen(false);
+}
+
+- (void)windowDidExitFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->CocoaWindowDidEnterFullscreen(false);
+}
+
+- (void)windowDidBecomeMain:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups();
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
+ // app modally. If one of those is up then we want it to retain its menu bar.
+ if ([NSApp _isRunningAppModal]) return;
+ NSWindow* window = [aNotification object];
+ if (window) [WindowDelegate paintMenubarForWindow:window];
+
+ if ([window isKindOfClass:[ToolbarWindow class]]) {
+ [(ToolbarWindow*)window windowMainStateChanged];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)windowDidResignMain:(NSNotification*)aNotification {
+ RollUpPopups();
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
+ // app modally. If one of those is up then we want it to retain its menu bar.
+ if ([NSApp _isRunningAppModal]) return;
+ RefPtr<nsMenuBarX> hiddenWindowMenuBar =
+ nsMenuUtilsX::GetHiddenWindowMenuBar();
+ if (hiddenWindowMenuBar) {
+ // printf("painting hidden window menu bar due to window losing main
+ // status\n");
+ hiddenWindowMenuBar->Paint();
+ }
+
+ NSWindow* window = [aNotification object];
+ if ([window isKindOfClass:[ToolbarWindow class]]) {
+ [(ToolbarWindow*)window windowMainStateChanged];
+ }
+}
+
+- (void)windowDidBecomeKey:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups();
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ NSWindow* window = [aNotification object];
+ if ([window isSheet]) [WindowDelegate paintMenubarForWindow:window];
+
+ nsChildView* mainChildView =
+ static_cast<nsChildView*>([[(BaseWindow*)window mainChildView] widget]);
+ if (mainChildView) {
+ if (mainChildView->GetInputContext().IsPasswordEditor()) {
+ TextInputHandler::EnableSecureEventInput();
+ } else {
+ TextInputHandler::EnsureSecureEventInputDisabled();
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)windowDidResignKey:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups(nsIRollupListener::AllowAnimations::No);
+
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ // If a sheet just resigned key then we should paint the menu bar
+ // for whatever window is now main.
+ NSWindow* window = [aNotification object];
+ if ([window isSheet])
+ [WindowDelegate paintMenubarForWindow:[NSApp mainWindow]];
+
+ TextInputHandler::EnsureSecureEventInputDisabled();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)windowWillMove:(NSNotification*)aNotification {
+ RollUpPopups();
+}
+
+- (void)windowDidMove:(NSNotification*)aNotification {
+ if (mGeckoWindow) mGeckoWindow->ReportMoveEvent();
+}
+
+- (BOOL)windowShouldClose:(id)sender {
+ nsIWidgetListener* listener =
+ mGeckoWindow ? mGeckoWindow->GetWidgetListener() : nullptr;
+ if (listener) listener->RequestWindowClose(mGeckoWindow);
+ return NO; // gecko will do it
+}
+
+- (void)windowWillClose:(NSNotification*)aNotification {
+ RollUpPopups();
+}
+
+- (void)windowWillMiniaturize:(NSNotification*)aNotification {
+ RollUpPopups();
+}
+
+- (void)windowDidMiniaturize:(NSNotification*)aNotification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->FinishCurrentTransitionIfMatching(
+ nsCocoaWindow::TransitionType::Miniaturize);
+}
+
+- (void)windowDidDeminiaturize:(NSNotification*)aNotification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ mGeckoWindow->FinishCurrentTransitionIfMatching(
+ nsCocoaWindow::TransitionType::Deminiaturize);
+}
+
+- (BOOL)windowShouldZoom:(NSWindow*)window toFrame:(NSRect)proposedFrame {
+ if (!mHasEverBeenZoomed && [window isZoomed]) return NO; // See bug 429954.
+
+ mHasEverBeenZoomed = YES;
+ return YES;
+}
+
+- (NSRect)window:(NSWindow*)window
+ willPositionSheet:(NSWindow*)sheet
+ usingRect:(NSRect)rect {
+ if ([window isKindOfClass:[ToolbarWindow class]]) {
+ rect.origin.y = [(ToolbarWindow*)window sheetAttachmentPosition];
+ }
+ return rect;
+}
+
+#ifdef MOZ_THUNDERBIRD
+- (void)didEndSheet:(NSWindow*)sheet
+ returnCode:(int)returnCode
+ contextInfo:(void*)contextInfo {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Note: 'contextInfo' (if it is set) is the window that is the parent of
+ // the sheet. The value of contextInfo is determined in
+ // nsCocoaWindow::Show(). If it's set, 'contextInfo' is always the top-
+ // level window, not another sheet itself. But 'contextInfo' is nil if
+ // our parent window is also a sheet -- in that case we shouldn't send
+ // the top-level window any activate events (because it's our parent
+ // window that needs to get these events, not the top-level window).
+ [TopLevelWindowData deactivateInWindow:sheet];
+ [sheet orderOut:self];
+ if (contextInfo) [TopLevelWindowData activateInWindow:(NSWindow*)contextInfo];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+#endif
+
+- (void)windowDidChangeBackingProperties:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSWindow* window = (NSWindow*)[aNotification object];
+
+ if ([window respondsToSelector:@selector(backingScaleFactor)]) {
+ CGFloat oldFactor = [[[aNotification userInfo]
+ objectForKey:@"NSBackingPropertyOldScaleFactorKey"] doubleValue];
+ if ([window backingScaleFactor] != oldFactor) {
+ mGeckoWindow->BackingScaleFactorChanged();
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// This method is on NSWindowDelegate starting with 10.9
+- (void)windowDidChangeOcclusionState:(NSNotification*)aNotification {
+ if (mGeckoWindow) {
+ mGeckoWindow->DispatchOcclusionEvent();
+ }
+}
+
+- (nsCocoaWindow*)geckoWidget {
+ return mGeckoWindow;
+}
+
+- (bool)toplevelActiveState {
+ return mToplevelActiveState;
+}
+
+- (void)sendToplevelActivateEvents {
+ if (!mToplevelActiveState && mGeckoWindow) {
+ mGeckoWindow->CocoaSendToplevelActivateEvents();
+
+ mToplevelActiveState = true;
+ }
+}
+
+- (void)sendToplevelDeactivateEvents {
+ if (mToplevelActiveState && mGeckoWindow) {
+ mGeckoWindow->CocoaSendToplevelDeactivateEvents();
+ mToplevelActiveState = false;
+ }
+}
+
+@end
+
+@interface NSView (FrameViewMethodSwizzling)
+- (NSPoint)FrameView__closeButtonOrigin;
+- (CGFloat)FrameView__titlebarHeight;
+@end
+
+@implementation NSView (FrameViewMethodSwizzling)
+
+- (NSPoint)FrameView__closeButtonOrigin {
+ if (![self.window isKindOfClass:[ToolbarWindow class]]) {
+ return self.FrameView__closeButtonOrigin;
+ }
+ ToolbarWindow* win = (ToolbarWindow*)[self window];
+ if (win.drawsContentsIntoWindowFrame &&
+ !(win.styleMask & NSWindowStyleMaskFullScreen) &&
+ (win.styleMask & NSWindowStyleMaskTitled)) {
+ const NSRect buttonsRect = win.windowButtonsRect;
+ if (NSIsEmptyRect(buttonsRect)) {
+ // Empty rect. Let's hide the buttons.
+ // Position is in non-flipped window coordinates. Using frame's height
+ // for the vertical coordinate will move the buttons above the window,
+ // making them invisible.
+ return NSMakePoint(buttonsRect.origin.x, win.frame.size.height);
+ } else if (win.windowTitlebarLayoutDirection ==
+ NSUserInterfaceLayoutDirectionRightToLeft) {
+ // We're in RTL mode, which means that the close button is the rightmost
+ // button of the three window buttons. and buttonsRect.origin is the
+ // bottom left corner of the green (zoom) button. The close button is 40px
+ // to the right of the zoom button. This is confirmed to be the same on
+ // all macOS versions between 10.12 - 12.0.
+ return NSMakePoint(buttonsRect.origin.x + 40.0f, buttonsRect.origin.y);
+ }
+ return buttonsRect.origin;
+ }
+ return self.FrameView__closeButtonOrigin;
+}
+
+- (CGFloat)FrameView__titlebarHeight {
+ CGFloat height = [self FrameView__titlebarHeight];
+ if ([[self window] isKindOfClass:[ToolbarWindow class]]) {
+ // Make sure that the titlebar height includes our shifted buttons.
+ // The following coordinates are in window space, with the origin being at
+ // the bottom left corner of the window.
+ ToolbarWindow* win = (ToolbarWindow*)[self window];
+ CGFloat frameHeight = [self frame].size.height;
+ CGFloat windowButtonY = frameHeight;
+ if (!NSIsEmptyRect(win.windowButtonsRect) &&
+ win.drawsContentsIntoWindowFrame &&
+ !(win.styleMask & NSWindowStyleMaskFullScreen) &&
+ (win.styleMask & NSWindowStyleMaskTitled)) {
+ windowButtonY = win.windowButtonsRect.origin.y;
+ }
+ height = std::max(height, frameHeight - windowButtonY);
+ }
+ return height;
+}
+
+@end
+
+static NSMutableSet* gSwizzledFrameViewClasses = nil;
+
+@interface NSWindow (PrivateSetNeedsDisplayInRectMethod)
+- (void)_setNeedsDisplayInRect:(NSRect)aRect;
+@end
+
+@interface NSView (NSVisualEffectViewSetMaskImage)
+- (void)setMaskImage:(NSImage*)image;
+@end
+
+@interface BaseWindow (Private)
+- (void)removeTrackingArea;
+- (void)cursorUpdated:(NSEvent*)aEvent;
+- (void)reflowTitlebarElements;
+@end
+
+@implementation BaseWindow
+
+// The frame of a window is implemented using undocumented NSView subclasses.
+// We offset the window buttons by overriding the method _closeButtonOrigin on
+// these frame view classes. The class which is
+// used for a window is determined in the window's frameViewClassForStyleMask:
+// method, so this is where we make sure that we have swizzled the method on
+// all encountered classes.
++ (Class)frameViewClassForStyleMask:(NSUInteger)styleMask {
+ Class frameViewClass = [super frameViewClassForStyleMask:styleMask];
+
+ if (!gSwizzledFrameViewClasses) {
+ gSwizzledFrameViewClasses = [[NSMutableSet setWithCapacity:3] retain];
+ if (!gSwizzledFrameViewClasses) {
+ return frameViewClass;
+ }
+ }
+
+ static IMP our_closeButtonOrigin = class_getMethodImplementation(
+ [NSView class], @selector(FrameView__closeButtonOrigin));
+ static IMP our_titlebarHeight = class_getMethodImplementation(
+ [NSView class], @selector(FrameView__titlebarHeight));
+
+ if (![gSwizzledFrameViewClasses containsObject:frameViewClass]) {
+ // Either of these methods might be implemented in both a subclass of
+ // NSFrameView and one of its own subclasses. Which means that if we
+ // aren't careful we might end up swizzling the same method twice.
+ // Since method swizzling involves swapping pointers, this would break
+ // things.
+ IMP _closeButtonOrigin = class_getMethodImplementation(
+ frameViewClass, @selector(_closeButtonOrigin));
+ if (_closeButtonOrigin && _closeButtonOrigin != our_closeButtonOrigin) {
+ nsToolkit::SwizzleMethods(frameViewClass, @selector(_closeButtonOrigin),
+ @selector(FrameView__closeButtonOrigin));
+ }
+
+ // Override _titlebarHeight so that the floating titlebar doesn't clip the
+ // bottom of the window buttons which we move down with our override of
+ // _closeButtonOrigin.
+ IMP _titlebarHeight = class_getMethodImplementation(
+ frameViewClass, @selector(_titlebarHeight));
+ if (_titlebarHeight && _titlebarHeight != our_titlebarHeight) {
+ nsToolkit::SwizzleMethods(frameViewClass, @selector(_titlebarHeight),
+ @selector(FrameView__titlebarHeight));
+ }
+
+ [gSwizzledFrameViewClasses addObject:frameViewClass];
+ }
+
+ return frameViewClass;
+}
+
+- (id)initWithContentRect:(NSRect)aContentRect
+ styleMask:(NSUInteger)aStyle
+ backing:(NSBackingStoreType)aBufferingType
+ defer:(BOOL)aFlag {
+ mDrawsIntoWindowFrame = NO;
+ [super initWithContentRect:aContentRect
+ styleMask:aStyle
+ backing:aBufferingType
+ defer:aFlag];
+ mState = nil;
+ mDisabledNeedsDisplay = NO;
+ mTrackingArea = nil;
+ mDirtyRect = NSZeroRect;
+ mBeingShown = NO;
+ mDrawTitle = NO;
+ mTouchBar = nil;
+ mIsAnimationSuppressed = NO;
+ [self updateTrackingArea];
+
+ return self;
+}
+
+// Returns an autoreleased NSImage.
+static NSImage* GetMenuMaskImage() {
+ const CGFloat radius = 6.0f;
+ const NSSize maskSize = {radius * 3.0f, radius * 3.0f};
+ NSImage* maskImage = [NSImage imageWithSize:maskSize
+ flipped:FALSE
+ drawingHandler:^BOOL(NSRect dstRect) {
+ NSBezierPath* path = [NSBezierPath
+ bezierPathWithRoundedRect:dstRect
+ xRadius:radius
+ yRadius:radius];
+ [NSColor.blackColor set];
+ [path fill];
+ return YES;
+ }];
+ maskImage.capInsets = NSEdgeInsetsMake(radius, radius, radius, radius);
+ return maskImage;
+}
+
+- (void)swapOutChildViewWrapper:(NSView*)aNewWrapper {
+ [aNewWrapper setFrame:[[self contentView] frame]];
+ NSView* childView = [[self mainChildView] retain];
+ [childView removeFromSuperview];
+ [aNewWrapper addSubview:childView];
+ [childView release];
+ [super setContentView:aNewWrapper];
+}
+
+- (void)setEffectViewWrapperForStyle:(WindowShadow)aStyle {
+ if (aStyle == WindowShadow::Menu || aStyle == WindowShadow::Tooltip) {
+ // Add an effect view wrapper so that the OS draws the appropriate
+ // vibrancy effect and window border.
+ BOOL isMenu = aStyle == WindowShadow::Menu;
+ NSView* effectView = VibrancyManager::CreateEffectView(
+ isMenu ? VibrancyType::MENU : VibrancyType::TOOLTIP, YES);
+ if (isMenu) {
+ // Turn on rounded corner masking.
+ [effectView setMaskImage:GetMenuMaskImage()];
+ }
+ [self swapOutChildViewWrapper:effectView];
+ [effectView release];
+ } else {
+ // Remove the existing wrapper.
+ NSView* wrapper = [[NSView alloc] initWithFrame:NSZeroRect];
+ [wrapper setWantsLayer:YES];
+ [self swapOutChildViewWrapper:wrapper];
+ [wrapper release];
+ }
+}
+
+- (NSTouchBar*)makeTouchBar {
+ mTouchBar = [[nsTouchBar alloc] init];
+ if (mTouchBar) {
+ sTouchBarIsInitialized = YES;
+ }
+ return mTouchBar;
+}
+
+- (void)setBeingShown:(BOOL)aValue {
+ mBeingShown = aValue;
+}
+
+- (BOOL)isBeingShown {
+ return mBeingShown;
+}
+
+- (BOOL)isVisibleOrBeingShown {
+ return [super isVisible] || mBeingShown;
+}
+
+- (void)setIsAnimationSuppressed:(BOOL)aValue {
+ mIsAnimationSuppressed = aValue;
+}
+
+- (BOOL)isAnimationSuppressed {
+ return mIsAnimationSuppressed;
+}
+
+- (void)disableSetNeedsDisplay {
+ mDisabledNeedsDisplay = YES;
+}
+
+- (void)enableSetNeedsDisplay {
+ mDisabledNeedsDisplay = NO;
+}
+
+- (void)dealloc {
+ [mTouchBar release];
+ [self removeTrackingArea];
+ ChildViewMouseTracker::OnDestroyWindow(self);
+ [super dealloc];
+}
+
+static const NSString* kStateTitleKey = @"title";
+static const NSString* kStateDrawsContentsIntoWindowFrameKey =
+ @"drawsContentsIntoWindowFrame";
+static const NSString* kStateShowsToolbarButton = @"showsToolbarButton";
+static const NSString* kStateCollectionBehavior = @"collectionBehavior";
+static const NSString* kStateWantsTitleDrawn = @"wantsTitleDrawn";
+
+- (void)importState:(NSDictionary*)aState {
+ if (NSString* title = [aState objectForKey:kStateTitleKey]) {
+ [self setTitle:title];
+ }
+ [self setDrawsContentsIntoWindowFrame:
+ [[aState objectForKey:kStateDrawsContentsIntoWindowFrameKey]
+ boolValue]];
+ [self setShowsToolbarButton:[[aState objectForKey:kStateShowsToolbarButton]
+ boolValue]];
+ [self setCollectionBehavior:[[aState objectForKey:kStateCollectionBehavior]
+ unsignedIntValue]];
+ [self setWantsTitleDrawn:[[aState objectForKey:kStateWantsTitleDrawn]
+ boolValue]];
+}
+
+- (NSMutableDictionary*)exportState {
+ NSMutableDictionary* state = [NSMutableDictionary dictionaryWithCapacity:10];
+ if (NSString* title = [self title]) {
+ [state setObject:title forKey:kStateTitleKey];
+ }
+ [state setObject:[NSNumber numberWithBool:[self drawsContentsIntoWindowFrame]]
+ forKey:kStateDrawsContentsIntoWindowFrameKey];
+ [state setObject:[NSNumber numberWithBool:[self showsToolbarButton]]
+ forKey:kStateShowsToolbarButton];
+ [state setObject:[NSNumber numberWithUnsignedInt:[self collectionBehavior]]
+ forKey:kStateCollectionBehavior];
+ [state setObject:[NSNumber numberWithBool:[self wantsTitleDrawn]]
+ forKey:kStateWantsTitleDrawn];
+ return state;
+}
+
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState {
+ bool changed = (aState != mDrawsIntoWindowFrame);
+ mDrawsIntoWindowFrame = aState;
+ if (changed) {
+ [self reflowTitlebarElements];
+ }
+}
+
+- (BOOL)drawsContentsIntoWindowFrame {
+ return mDrawsIntoWindowFrame;
+}
+
+- (NSRect)childViewRectForFrameRect:(NSRect)aFrameRect {
+ if (mDrawsIntoWindowFrame) {
+ return aFrameRect;
+ }
+ NSUInteger styleMask = [self styleMask];
+ styleMask &= ~NSWindowStyleMaskFullSizeContentView;
+ return [NSWindow contentRectForFrameRect:aFrameRect styleMask:styleMask];
+}
+
+- (NSRect)frameRectForChildViewRect:(NSRect)aChildViewRect {
+ if (mDrawsIntoWindowFrame) {
+ return aChildViewRect;
+ }
+ NSUInteger styleMask = [self styleMask];
+ styleMask &= ~NSWindowStyleMaskFullSizeContentView;
+ return [NSWindow frameRectForContentRect:aChildViewRect styleMask:styleMask];
+}
+
+- (NSTimeInterval)animationResizeTime:(NSRect)newFrame {
+ if (mIsAnimationSuppressed) {
+ // Should not animate the initial session-restore size change
+ return 0.0;
+ }
+
+ return [super animationResizeTime:newFrame];
+}
+
+- (void)setWantsTitleDrawn:(BOOL)aDrawTitle {
+ mDrawTitle = aDrawTitle;
+ [self setTitleVisibility:mDrawTitle ? NSWindowTitleVisible
+ : NSWindowTitleHidden];
+}
+
+- (BOOL)wantsTitleDrawn {
+ return mDrawTitle;
+}
+
+- (NSView*)trackingAreaView {
+ NSView* contentView = [self contentView];
+ return [contentView superview] ? [contentView superview] : contentView;
+}
+
+- (NSArray<NSView*>*)contentViewContents {
+ return [[[[self contentView] subviews] copy] autorelease];
+}
+
+- (ChildView*)mainChildView {
+ NSView* contentView = [self contentView];
+ NSView* lastView = [[contentView subviews] lastObject];
+ if ([lastView isKindOfClass:[ChildView class]]) {
+ return (ChildView*)lastView;
+ }
+ return nil;
+}
+
+- (void)removeTrackingArea {
+ if (mTrackingArea) {
+ [[self trackingAreaView] removeTrackingArea:mTrackingArea];
+ [mTrackingArea release];
+ mTrackingArea = nil;
+ }
+}
+
+- (void)updateTrackingArea {
+ [self removeTrackingArea];
+
+ NSView* view = [self trackingAreaView];
+ const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
+ NSTrackingMouseMoved |
+ NSTrackingActiveAlways;
+ mTrackingArea = [[NSTrackingArea alloc] initWithRect:[view bounds]
+ options:options
+ owner:self
+ userInfo:nil];
+ [view addTrackingArea:mTrackingArea];
+}
+
+- (void)mouseEntered:(NSEvent*)aEvent {
+ ChildViewMouseTracker::MouseEnteredWindow(aEvent);
+}
+
+- (void)mouseExited:(NSEvent*)aEvent {
+ ChildViewMouseTracker::MouseExitedWindow(aEvent);
+}
+
+- (void)mouseMoved:(NSEvent*)aEvent {
+ ChildViewMouseTracker::MouseMoved(aEvent);
+}
+
+- (void)cursorUpdated:(NSEvent*)aEvent {
+ // Nothing to do here, but NSTrackingArea wants us to implement this method.
+}
+
+- (void)_setNeedsDisplayInRect:(NSRect)aRect {
+ // Prevent unnecessary invalidations due to moving NSViews (e.g. for plugins)
+ if (!mDisabledNeedsDisplay) {
+ // This method is only called by Cocoa, so when we're here, we know that
+ // it's available and don't need to check whether our superclass responds
+ // to the selector.
+ [super _setNeedsDisplayInRect:aRect];
+ mDirtyRect = NSUnionRect(mDirtyRect, aRect);
+ }
+}
+
+- (NSRect)getAndResetNativeDirtyRect {
+ NSRect dirtyRect = mDirtyRect;
+ mDirtyRect = NSZeroRect;
+ return dirtyRect;
+}
+
+// Possibly move the titlebar buttons.
+- (void)reflowTitlebarElements {
+ NSView* frameView = [[self contentView] superview];
+ if ([frameView respondsToSelector:@selector(_tileTitlebarAndRedisplay:)]) {
+ [frameView _tileTitlebarAndRedisplay:NO];
+ }
+}
+
+- (BOOL)respondsToSelector:(SEL)aSelector {
+ // Claim the window doesn't respond to this so that the system
+ // doesn't steal keyboard equivalents for it. Bug 613710.
+ if (aSelector == @selector(cancelOperation:)) {
+ return NO;
+ }
+
+ return [super respondsToSelector:aSelector];
+}
+
+- (void)doCommandBySelector:(SEL)aSelector {
+ // We override this so that it won't beep if it can't act.
+ // We want to control the beeping for missing or disabled
+ // commands ourselves.
+ [self tryToPerform:aSelector with:nil];
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ id retval = [super accessibilityAttributeValue:attribute];
+
+ // The following works around a problem with Text-to-Speech on OS X 10.7.
+ // See bug 674612 for more info.
+ //
+ // When accessibility is off, AXUIElementCopyAttributeValue(), when called
+ // on an AXApplication object to get its AXFocusedUIElement attribute,
+ // always returns an AXWindow object (the actual browser window -- never a
+ // mozAccessible object). This also happens with accessibility turned on,
+ // if no other object in the browser window has yet been focused. But if
+ // the browser window has a title bar (as it currently always does), the
+ // AXWindow object will always have four "accessible" children, one of which
+ // is an AXStaticText object (the title bar's "title"; the other three are
+ // the close, minimize and zoom buttons). This means that (for complicated
+ // reasons, for which see bug 674612) Text-to-Speech on OS X 10.7 will often
+ // "speak" the window title, no matter what text is selected, or even if no
+ // text at all is selected. (This always happens when accessibility is off.
+ // It doesn't happen in Firefox releases because Apple has (on OS X 10.7)
+ // special-cased the handling of apps whose CFBundleIdentifier is
+ // org.mozilla.firefox.)
+ //
+ // We work around this problem by only returning AXChildren that are
+ // mozAccessible object or are one of the titlebar's buttons (which
+ // instantiate subclasses of NSButtonCell).
+ if ([retval isKindOfClass:[NSArray class]] &&
+ [attribute isEqualToString:@"AXChildren"]) {
+ NSMutableArray* holder = [NSMutableArray arrayWithCapacity:10];
+ [holder addObjectsFromArray:(NSArray*)retval];
+ NSUInteger count = [holder count];
+ for (NSInteger i = count - 1; i >= 0; --i) {
+ id item = [holder objectAtIndex:i];
+ // Remove anything from holder that isn't one of the titlebar's buttons
+ // (which instantiate subclasses of NSButtonCell) or a mozAccessible
+ // object (or one of its subclasses).
+ if (![item isKindOfClass:[NSButtonCell class]] &&
+ ![item respondsToSelector:@selector(hasRepresentedView)]) {
+ [holder removeObjectAtIndex:i];
+ }
+ }
+ retval = [NSArray arrayWithArray:holder];
+ }
+
+ return retval;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)releaseJSObjects {
+ [mTouchBar releaseJSObjects];
+}
+
+@end
+
+@interface NSView (NSThemeFrame)
+- (void)_drawTitleStringInClip:(NSRect)aRect;
+- (void)_maskCorners:(NSUInteger)aFlags clipRect:(NSRect)aRect;
+@end
+
+@implementation MOZTitlebarView
+
+- (instancetype)initWithFrame:(NSRect)aFrame {
+ self = [super initWithFrame:aFrame];
+
+ self.material = NSVisualEffectMaterialTitlebar;
+ self.blendingMode = NSVisualEffectBlendingModeWithinWindow;
+
+ // Add a separator line at the bottom of the titlebar. NSBoxSeparator isn't a
+ // perfect match for a native titlebar separator, but it's better than
+ // nothing. We really want the appearance that _NSTitlebarDecorationView
+ // creates with the help of CoreUI, but there's no public API for that.
+ NSBox* separatorLine =
+ [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, aFrame.size.width, 1)];
+ separatorLine.autoresizingMask = NSViewWidthSizable | NSViewMaxYMargin;
+ separatorLine.boxType = NSBoxSeparator;
+ [self addSubview:separatorLine];
+ [separatorLine release];
+
+ return self;
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+ return YES;
+}
+
+- (void)mouseUp:(NSEvent*)event {
+ if ([event clickCount] == 2) {
+ // Handle titlebar double click. We don't get the window's default behavior
+ // here because the window uses NSWindowStyleMaskFullSizeContentView, and
+ // this view (the titlebar gradient view) is technically part of the window
+ // "contents" (it's a subview of the content view).
+ if (nsCocoaUtils::ShouldZoomOnTitlebarDoubleClick()) {
+ [[self window] performZoom:nil];
+ } else if (nsCocoaUtils::ShouldMinimizeOnTitlebarDoubleClick()) {
+ [[self window] performMiniaturize:nil];
+ }
+ }
+}
+
+@end
+
+@interface MOZTitlebarAccessoryView : NSView
+@end
+
+@implementation MOZTitlebarAccessoryView : NSView
+- (void)viewWillMoveToWindow:(NSWindow*)aWindow {
+ if (aWindow) {
+ // When entering full screen mode, titlebar accessory views are inserted
+ // into a floating NSWindow which houses the window titlebar and toolbars.
+ // In order to work around a drawing bug with titlebarAppearsTransparent
+ // windows in full screen mode, disable titlebar separators for all
+ // NSWindows that this view is used in, including the floating full screen
+ // toolbar window. The drawing bug was filed as FB9056136. See bug 1700211
+ // for more details.
+ if (@available(macOS 11.0, *)) {
+ aWindow.titlebarSeparatorStyle = NSTitlebarSeparatorStyleNone;
+ }
+ }
+}
+@end
+
+@implementation FullscreenTitlebarTracker
+- (FullscreenTitlebarTracker*)init {
+ [super init];
+ self.view =
+ [[[MOZTitlebarAccessoryView alloc] initWithFrame:NSZeroRect] autorelease];
+ self.hidden = YES;
+ return self;
+}
+@end
+
+// This class allows us to exercise control over the window's title bar. It is
+// used for all windows with titlebars.
+//
+// ToolbarWindow supports two modes:
+// - drawsContentsIntoWindowFrame mode: In this mode, the Gecko ChildView is
+// sized to cover the entire window frame and manages titlebar drawing.
+// - separate titlebar mode, with support for unified toolbars: In this mode,
+// the Gecko ChildView does not extend into the titlebar. However, this
+// window's content view (which is the ChildView's superview) *does* extend
+// into the titlebar. Moreover, in this mode, we place a MOZTitlebarView
+// in the content view, as a sibling of the ChildView.
+//
+// The "separate titlebar mode" supports the "unified toolbar" look:
+// If there's a toolbar right below the titlebar, the two can "connect" and
+// form a single gradient without a separator line in between.
+//
+// The following mechanism communicates the height of the unified toolbar to
+// the ToolbarWindow:
+//
+// 1) In the style sheet we set the toolbar's -moz-appearance to toolbar.
+// 2) When the toolbar is visible and we paint the application chrome
+// window, the array that Gecko passes nsChildView::UpdateThemeGeometries
+// will contain an entry for the widget type StyleAppearance::Toolbar.
+// 3) nsChildView::UpdateThemeGeometries passes the toolbar's height, plus the
+// titlebar height, to -[ToolbarWindow setUnifiedToolbarHeight:].
+//
+// The actual drawing of the gradient happens in two parts: The titlebar part
+// (i.e. the top 22 pixels of the gradient) is drawn by the MOZTitlebarView,
+// which is a subview of the window's content view and a sibling of the
+// ChildView. The rest of the gradient is drawn by Gecko into the ChildView, as
+// part of the -moz-appearance rendering of the toolbar.
+@implementation ToolbarWindow
+
+- (id)initWithContentRect:(NSRect)aChildViewRect
+ styleMask:(NSUInteger)aStyle
+ backing:(NSBackingStoreType)aBufferingType
+ defer:(BOOL)aFlag {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We treat aChildViewRect as the rectangle that the window's main ChildView
+ // should be sized to. Get the right frameRect for the requested child view
+ // rect.
+ NSRect frameRect = [NSWindow frameRectForContentRect:aChildViewRect
+ styleMask:aStyle];
+
+ // Always size the content view to the full frame size of the window.
+ // We do this even if we want this window to have a titlebar; in that case,
+ // the window's content view covers the entire window but the ChildView inside
+ // it will only cover the content area. We do this so that we can render the
+ // titlebar gradient manually, with a subview of our content view that's
+ // positioned in the titlebar area. This lets us have a smooth connection
+ // between titlebar and toolbar gradient in case the window has a "unified
+ // toolbar + titlebar" look. Moreover, always using a full size content view
+ // lets us toggle the titlebar on and off without changing the window's style
+ // mask (which would have other subtle effects, for example on keyboard
+ // focus).
+ aStyle |= NSWindowStyleMaskFullSizeContentView;
+
+ // -[NSWindow initWithContentRect:styleMask:backing:defer:] calls
+ // [self frameRectForContentRect:styleMask:] to convert the supplied content
+ // rect to the window's frame rect. We've overridden that method to be a
+ // pass-through function. So, in order to get the intended frameRect, we need
+ // to supply frameRect itself as the "content rect".
+ NSRect contentRect = frameRect;
+
+ if ((self = [super initWithContentRect:contentRect
+ styleMask:aStyle
+ backing:aBufferingType
+ defer:aFlag])) {
+ mTitlebarView = nil;
+ mUnifiedToolbarHeight = 22.0f;
+ mSheetAttachmentPosition = aChildViewRect.size.height;
+ mWindowButtonsRect = NSZeroRect;
+ mInitialTitlebarHeight = [self titlebarHeight];
+
+ [self setTitlebarAppearsTransparent:YES];
+ if (@available(macOS 11.0, *)) {
+ self.titlebarSeparatorStyle = NSTitlebarSeparatorStyleNone;
+ }
+ [self updateTitlebarView];
+
+ mFullscreenTitlebarTracker = [[FullscreenTitlebarTracker alloc] init];
+ // revealAmount is an undocumented property of
+ // NSTitlebarAccessoryViewController that updates whenever the menubar
+ // slides down in fullscreen mode.
+ [mFullscreenTitlebarTracker addObserver:self
+ forKeyPath:@"revealAmount"
+ options:NSKeyValueObservingOptionNew
+ context:nil];
+ // Adding this accessory view controller allows us to shift the toolbar down
+ // when the user mouses to the top of the screen in fullscreen.
+ [(NSWindow*)self
+ addTitlebarAccessoryViewController:mFullscreenTitlebarTracker];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey, id>*)change
+ context:(void*)context {
+ if ([keyPath isEqualToString:@"revealAmount"]) {
+ [[self mainChildView] ensureNextCompositeIsAtomicWithMainThreadPaint];
+ NSNumber* revealAmount = (change[NSKeyValueChangeNewKey]);
+ [self updateTitlebarShownAmount:[revealAmount doubleValue]];
+ } else {
+ [super observeValueForKeyPath:keyPath
+ ofObject:object
+ change:change
+ context:context];
+ }
+}
+
+static bool ScreenHasNotch(nsCocoaWindow* aGeckoWindow) {
+ if (@available(macOS 12.0, *)) {
+ nsCOMPtr<nsIScreen> widgetScreen = aGeckoWindow->GetWidgetScreen();
+ NSScreen* cocoaScreen =
+ ScreenHelperCocoa::CocoaScreenForScreen(widgetScreen);
+ return cocoaScreen.safeAreaInsets.top != 0.0f;
+ }
+ return false;
+}
+
+static bool ShouldShiftByMenubarHeightInFullscreen(nsCocoaWindow* aWindow) {
+ switch (StaticPrefs::widget_macos_shift_by_menubar_on_fullscreen()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ break;
+ }
+ // TODO: On notch-less macbooks, this creates extra space when the
+ // "automatically show and hide the menubar on fullscreen" option is unchecked
+ // (default checked). We tried to detect that in bug 1737831 but it wasn't
+ // reliable enough, see the regressions from that bug. For now, stick to the
+ // good behavior for default configurations (that is, shift by menubar height
+ // on notch-less macbooks, and don't for devices that have a notch). This will
+ // need refinement in the future.
+ return !ScreenHasNotch(aWindow);
+}
+
+- (void)updateTitlebarShownAmount:(CGFloat)aShownAmount {
+ NSInteger styleMask = [self styleMask];
+ if (!(styleMask & NSWindowStyleMaskFullScreen)) {
+ // We are not interested in the size of the titlebar unless we are in
+ // fullscreen.
+ return;
+ }
+
+ // [NSApp mainMenu] menuBarHeight] returns one of two values: the full height
+ // if the menubar is shown or is in the process of being shown, and 0
+ // otherwise. Since we are multiplying the menubar height by aShownAmount, we
+ // always want the full height.
+ CGFloat menuBarHeight = NSApp.mainMenu.menuBarHeight;
+ if (menuBarHeight > 0.0f) {
+ mMenuBarHeight = menuBarHeight;
+ }
+ if ([[self delegate] isKindOfClass:[WindowDelegate class]]) {
+ WindowDelegate* windowDelegate = (WindowDelegate*)[self delegate];
+ nsCocoaWindow* geckoWindow = [windowDelegate geckoWidget];
+ if (!geckoWindow) {
+ return;
+ }
+
+ if (nsIWidgetListener* listener = geckoWindow->GetWidgetListener()) {
+ // Use the titlebar height cached in our frame rather than
+ // [ToolbarWindow titlebarHeight]. titlebarHeight returns 0 when we're in
+ // fullscreen.
+ CGFloat shiftByPixels = mInitialTitlebarHeight * aShownAmount;
+ if (ShouldShiftByMenubarHeightInFullscreen(geckoWindow)) {
+ shiftByPixels += mMenuBarHeight * aShownAmount;
+ }
+ // Use mozilla::DesktopToLayoutDeviceScale rather than the
+ // DesktopToLayoutDeviceScale in nsCocoaWindow. The latter accounts for
+ // screen DPI. We don't want that because the revealAmount property
+ // already accounts for it, so we'd be compounding DPI scales > 1.
+ mozilla::DesktopCoord coord = LayoutDeviceCoord(shiftByPixels) /
+ mozilla::DesktopToLayoutDeviceScale();
+
+ listener->MacFullscreenMenubarOverlapChanged(coord);
+ }
+ }
+}
+
+- (void)dealloc {
+ [mTitlebarView release];
+ [mFullscreenTitlebarTracker removeObserver:self forKeyPath:@"revealAmount"];
+ [mFullscreenTitlebarTracker removeFromParentViewController];
+ [mFullscreenTitlebarTracker release];
+
+ [super dealloc];
+}
+
+- (NSArray<NSView*>*)contentViewContents {
+ NSMutableArray<NSView*>* contents =
+ [[[self contentView] subviews] mutableCopy];
+ if (mTitlebarView) {
+ // Do not include the titlebar gradient view in the returned array.
+ [contents removeObject:mTitlebarView];
+ }
+ return [contents autorelease];
+}
+
+- (void)updateTitlebarView {
+ BOOL needTitlebarView =
+ ![self drawsContentsIntoWindowFrame] || mUnifiedToolbarHeight > 0;
+ if (needTitlebarView && !mTitlebarView) {
+ mTitlebarView =
+ [[MOZTitlebarView alloc] initWithFrame:[self unifiedToolbarRect]];
+ mTitlebarView.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
+ [self.contentView addSubview:mTitlebarView
+ positioned:NSWindowBelow
+ relativeTo:nil];
+ } else if (needTitlebarView && mTitlebarView) {
+ mTitlebarView.frame = [self unifiedToolbarRect];
+ } else if (!needTitlebarView && mTitlebarView) {
+ [mTitlebarView removeFromSuperview];
+ [mTitlebarView release];
+ mTitlebarView = nil;
+ }
+}
+
+- (void)windowMainStateChanged {
+ [self setTitlebarNeedsDisplay];
+ [[self mainChildView] ensureNextCompositeIsAtomicWithMainThreadPaint];
+}
+
+- (void)setTitlebarNeedsDisplay {
+ [mTitlebarView setNeedsDisplay:YES];
+}
+
+- (NSRect)titlebarRect {
+ CGFloat titlebarHeight = [self titlebarHeight];
+ return NSMakeRect(0, [self frame].size.height - titlebarHeight,
+ [self frame].size.width, titlebarHeight);
+}
+
+// In window contentView coordinates (origin bottom left)
+- (NSRect)unifiedToolbarRect {
+ return NSMakeRect(0, [self frame].size.height - mUnifiedToolbarHeight,
+ [self frame].size.width, mUnifiedToolbarHeight);
+}
+
+// Returns the unified height of titlebar + toolbar.
+- (CGFloat)unifiedToolbarHeight {
+ return mUnifiedToolbarHeight;
+}
+
+- (CGFloat)titlebarHeight {
+ // We use the original content rect here, not what we return from
+ // [self contentRectForFrameRect:], because that would give us a
+ // titlebarHeight of zero.
+ NSRect frameRect = [self frame];
+ NSUInteger styleMask = [self styleMask];
+ styleMask &= ~NSWindowStyleMaskFullSizeContentView;
+ NSRect originalContentRect = [NSWindow contentRectForFrameRect:frameRect
+ styleMask:styleMask];
+ return NSMaxY(frameRect) - NSMaxY(originalContentRect);
+}
+
+// Stores the complete height of titlebar + toolbar.
+- (void)setUnifiedToolbarHeight:(CGFloat)aHeight {
+ if (aHeight == mUnifiedToolbarHeight) return;
+
+ mUnifiedToolbarHeight = aHeight;
+
+ [self updateTitlebarView];
+}
+
+// Extending the content area into the title bar works by resizing the
+// mainChildView so that it covers the titlebar.
+- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState {
+ BOOL stateChanged = ([self drawsContentsIntoWindowFrame] != aState);
+ [super setDrawsContentsIntoWindowFrame:aState];
+ if (stateChanged && [[self delegate] isKindOfClass:[WindowDelegate class]]) {
+ // Here we extend / shrink our mainChildView. We do that by firing a resize
+ // event which will cause the ChildView to be resized to the rect returned
+ // by nsCocoaWindow::GetClientBounds. GetClientBounds bases its return
+ // value on what we return from drawsContentsIntoWindowFrame.
+ WindowDelegate* windowDelegate = (WindowDelegate*)[self delegate];
+ nsCocoaWindow* geckoWindow = [windowDelegate geckoWidget];
+ if (geckoWindow) {
+ // Re-layout our contents.
+ geckoWindow->ReportSizeEvent();
+ }
+
+ // Resizing the content area causes a reflow which would send a synthesized
+ // mousemove event to the old mouse position relative to the top left
+ // corner of the content area. But the mouse has shifted relative to the
+ // content area, so that event would have wrong position information. So
+ // we'll send a mouse move event with the correct new position.
+ ChildViewMouseTracker::ResendLastMouseMoveEvent();
+ }
+
+ [self updateTitlebarView];
+}
+
+- (void)setWantsTitleDrawn:(BOOL)aDrawTitle {
+ [super setWantsTitleDrawn:aDrawTitle];
+ [self setTitlebarNeedsDisplay];
+}
+
+- (void)setSheetAttachmentPosition:(CGFloat)aY {
+ mSheetAttachmentPosition = aY;
+}
+
+- (CGFloat)sheetAttachmentPosition {
+ return mSheetAttachmentPosition;
+}
+
+- (void)placeWindowButtons:(NSRect)aRect {
+ if (!NSEqualRects(mWindowButtonsRect, aRect)) {
+ mWindowButtonsRect = aRect;
+ [self reflowTitlebarElements];
+ }
+}
+
+- (NSRect)windowButtonsRect {
+ return mWindowButtonsRect;
+}
+
+// Returning YES here makes the setShowsToolbarButton method work even though
+// the window doesn't contain an NSToolbar.
+- (BOOL)_hasToolbar {
+ return YES;
+}
+
+// Dispatch a toolbar pill button clicked message to Gecko.
+- (void)_toolbarPillButtonClicked:(id)sender {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups();
+
+ if ([[self delegate] isKindOfClass:[WindowDelegate class]]) {
+ WindowDelegate* windowDelegate = (WindowDelegate*)[self delegate];
+ nsCocoaWindow* geckoWindow = [windowDelegate geckoWidget];
+ if (!geckoWindow) return;
+
+ nsIWidgetListener* listener = geckoWindow->GetWidgetListener();
+ if (listener) listener->OSToolbarButtonPressed();
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Retain and release "self" to avoid crashes when our widget (and its native
+// window) is closed as a result of processing a key equivalent (e.g.
+// Command+w or Command+q). This workaround is only needed for a window
+// that can become key.
+- (BOOL)performKeyEquivalent:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSWindow* nativeWindow = [self retain];
+ BOOL retval = [super performKeyEquivalent:theEvent];
+ [nativeWindow release];
+ return retval;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (void)sendEvent:(NSEvent*)anEvent {
+ NSEventType type = [anEvent type];
+
+ switch (type) {
+ case NSEventTypeScrollWheel:
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseUp:
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseUp:
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseUp:
+ case NSEventTypeMouseMoved:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeOtherMouseDragged: {
+ // Drop all mouse events if a modal window has appeared above us.
+ // This helps make us behave as if the OS were running a "real" modal
+ // event loop.
+ id delegate = [self delegate];
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]]) {
+ nsCocoaWindow* widget = [(WindowDelegate*)delegate geckoWidget];
+ if (widget) {
+ if (gGeckoAppModalWindowList &&
+ (widget != gGeckoAppModalWindowList->window))
+ return;
+ if (widget->HasModalDescendents()) return;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ [super sendEvent:anEvent];
+}
+
+@end
+
+@implementation PopupWindow
+
+- (id)initWithContentRect:(NSRect)contentRect
+ styleMask:(NSUInteger)styleMask
+ backing:(NSBackingStoreType)bufferingType
+ defer:(BOOL)deferCreation {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ mIsContextMenu = false;
+ return [super initWithContentRect:contentRect
+ styleMask:styleMask
+ backing:bufferingType
+ defer:deferCreation];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Override the private API _backdropBleedAmount. This determines how much the
+// desktop wallpaper contributes to the vibrancy backdrop.
+// Return 0 in order to match what the system does for sheet windows and
+// _NSPopoverWindows.
+- (CGFloat)_backdropBleedAmount {
+ return 0.0;
+}
+
+// Override the private API shadowOptions.
+// The constants below were found in AppKit's implementations of the
+// shadowOptions method on the various window types.
+static const NSUInteger kWindowShadowOptionsNoShadow = 0;
+static const NSUInteger kWindowShadowOptionsMenu = 2;
+static const NSUInteger kWindowShadowOptionsTooltip = 4;
+
+- (NSDictionary*)shadowParameters {
+ NSDictionary* parent = [super shadowParameters];
+ // NSLog(@"%@", parent);
+ if (self.shadowStyle != WindowShadow::Panel) {
+ return parent;
+ }
+ NSMutableDictionary* copy = [parent mutableCopy];
+ for (auto* key : {@"com.apple.WindowShadowRimDensityActive",
+ @"com.apple.WindowShadowRimDensityInactive"}) {
+ if ([parent objectForKey:key] != nil) {
+ [copy setValue:@(0) forKey:key];
+ }
+ }
+ return copy;
+}
+
+- (NSUInteger)shadowOptions {
+ if (!self.hasShadow) {
+ return kWindowShadowOptionsNoShadow;
+ }
+
+ switch (self.shadowStyle) {
+ case WindowShadow::None:
+ return kWindowShadowOptionsNoShadow;
+
+ case WindowShadow::Menu:
+ case WindowShadow::Panel:
+ return kWindowShadowOptionsMenu;
+
+ case WindowShadow::Tooltip:
+ return kWindowShadowOptionsTooltip;
+ }
+}
+
+- (BOOL)isContextMenu {
+ return mIsContextMenu;
+}
+
+- (void)setIsContextMenu:(BOOL)flag {
+ mIsContextMenu = flag;
+}
+
+- (BOOL)canBecomeMainWindow {
+ // This is overriden because the default is 'yes' when a titlebar is present.
+ return NO;
+}
+
+@end
+
+// According to Apple's docs on [NSWindow canBecomeKeyWindow] and [NSWindow
+// canBecomeMainWindow], windows without a title bar or resize bar can't (by
+// default) become key or main. But if a window can't become key, it can't
+// accept keyboard input (bmo bug 393250). And it should also be possible for
+// an otherwise "ordinary" window to become main. We need to override these
+// two methods to make this happen.
+@implementation BorderlessWindow
+
+- (BOOL)canBecomeKeyWindow {
+ return YES;
+}
+
+- (void)sendEvent:(NSEvent*)anEvent {
+ NSEventType type = [anEvent type];
+
+ switch (type) {
+ case NSEventTypeScrollWheel:
+ case NSEventTypeLeftMouseDown:
+ case NSEventTypeLeftMouseUp:
+ case NSEventTypeRightMouseDown:
+ case NSEventTypeRightMouseUp:
+ case NSEventTypeOtherMouseDown:
+ case NSEventTypeOtherMouseUp:
+ case NSEventTypeMouseMoved:
+ case NSEventTypeLeftMouseDragged:
+ case NSEventTypeRightMouseDragged:
+ case NSEventTypeOtherMouseDragged: {
+ // Drop all mouse events if a modal window has appeared above us.
+ // This helps make us behave as if the OS were running a "real" modal
+ // event loop.
+ id delegate = [self delegate];
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]]) {
+ nsCocoaWindow* widget = [(WindowDelegate*)delegate geckoWidget];
+ if (widget) {
+ if (gGeckoAppModalWindowList &&
+ (widget != gGeckoAppModalWindowList->window))
+ return;
+ if (widget->HasModalDescendents()) return;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ [super sendEvent:anEvent];
+}
+
+// Apple's doc on this method says that the NSWindow class's default is not to
+// become main if the window isn't "visible" -- so we should replicate that
+// behavior here. As best I can tell, the [NSWindow isVisible] method is an
+// accurate test of what Apple means by "visibility".
+- (BOOL)canBecomeMainWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (![self isVisible]) return NO;
+ return YES;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+// Retain and release "self" to avoid crashes when our widget (and its native
+// window) is closed as a result of processing a key equivalent (e.g.
+// Command+w or Command+q). This workaround is only needed for a window
+// that can become key.
+- (BOOL)performKeyEquivalent:(NSEvent*)theEvent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSWindow* nativeWindow = [self retain];
+ BOOL retval = [super performKeyEquivalent:theEvent];
+ [nativeWindow release];
+ return retval;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+@end
diff --git a/widget/cocoa/nsColorPicker.h b/widget/cocoa/nsColorPicker.h
new file mode 100644
index 0000000000..325f159fde
--- /dev/null
+++ b/widget/cocoa/nsColorPicker.h
@@ -0,0 +1,41 @@
+/* -*- 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/. */
+
+#ifndef nsColorPicker_h_
+#define nsColorPicker_h_
+
+#include "nsIColorPicker.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+
+class nsIColorPickerShownCallback;
+class mozIDOMWindowProxy;
+@class NSColorPanelWrapper;
+@class NSColor;
+
+class nsColorPicker final : public nsIColorPicker {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOLORPICKER
+
+ // For NSColorPanelWrapper.
+ void Update(NSColor* aColor);
+ void Done();
+
+ private:
+ ~nsColorPicker();
+
+ static NSColor* GetNSColorFromHexString(const nsAString& aColor);
+ static void GetHexStringFromNSColor(NSColor* aColor, nsAString& aResult);
+
+ NSColorPanelWrapper* mColorPanelWrapper;
+
+ nsString mTitle;
+ nsString mColor;
+ nsCOMPtr<nsIColorPickerShownCallback> mCallback;
+};
+
+#endif // nsColorPicker_h_
diff --git a/widget/cocoa/nsColorPicker.mm b/widget/cocoa/nsColorPicker.mm
new file mode 100644
index 0000000000..3666557772
--- /dev/null
+++ b/widget/cocoa/nsColorPicker.mm
@@ -0,0 +1,165 @@
+/* -*- 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsColorPicker.h"
+#include "nsCocoaUtils.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+
+static unsigned int HexStrToInt(NSString* str) {
+ unsigned int result = 0;
+
+ for (unsigned int i = 0; i < [str length]; ++i) {
+ char c = [str characterAtIndex:i];
+ result *= 16;
+ if (c >= '0' && c <= '9') {
+ result += c - '0';
+ } else if (c >= 'A' && c <= 'F') {
+ result += 10 + (c - 'A');
+ } else {
+ result += 10 + (c - 'a');
+ }
+ }
+
+ return result;
+}
+
+@interface NSColorPanelWrapper : NSObject <NSWindowDelegate> {
+ NSColorPanel* mColorPanel;
+ nsColorPicker* mColorPicker;
+}
+- (id)initWithPicker:(nsColorPicker*)aPicker;
+- (void)open:(NSColor*)aInitialColor title:(NSString*)aTitle;
+- (void)colorChanged:(NSColorPanel*)aPanel;
+- (void)windowWillClose:(NSNotification*)aNotification;
+- (void)close;
+@end
+
+@implementation NSColorPanelWrapper
+- (id)initWithPicker:(nsColorPicker*)aPicker {
+ mColorPicker = aPicker;
+ mColorPanel = [NSColorPanel sharedColorPanel];
+
+ self = [super init];
+ return self;
+}
+
+- (void)open:(NSColor*)aInitialColor title:(NSString*)aTitle {
+ [mColorPanel setTarget:self];
+ [mColorPanel setAction:@selector(colorChanged:)];
+ [mColorPanel setDelegate:self];
+ [mColorPanel setTitle:aTitle];
+ [mColorPanel setColor:aInitialColor];
+ [mColorPanel setFrameOrigin:[NSEvent mouseLocation]];
+ [mColorPanel makeKeyAndOrderFront:nil];
+}
+
+- (void)colorChanged:(NSColorPanel*)aPanel {
+ if (!mColorPicker) {
+ return;
+ }
+ mColorPicker->Update([mColorPanel color]);
+}
+
+- (void)windowWillClose:(NSNotification*)aNotification {
+ if (!mColorPicker) {
+ return;
+ }
+ mColorPicker->Done();
+}
+
+- (void)close {
+ [mColorPanel setTarget:nil];
+ [mColorPanel setAction:nil];
+ [mColorPanel setDelegate:nil];
+
+ mColorPanel = nil;
+ mColorPicker = nullptr;
+}
+@end
+
+NS_IMPL_ISUPPORTS(nsColorPicker, nsIColorPicker)
+
+nsColorPicker::~nsColorPicker() {
+ if (mColorPanelWrapper) {
+ [mColorPanelWrapper close];
+ [mColorPanelWrapper release];
+ mColorPanelWrapper = nullptr;
+ }
+}
+
+// TODO(bug 1805397): Implement default colors
+NS_IMETHODIMP
+nsColorPicker::Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ const nsAString& aInitialColor,
+ const nsTArray<nsString>& aDefaultColors) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Color pickers can only be opened from main thread currently");
+ mTitle = aTitle;
+ mColor = aInitialColor;
+ mColorPanelWrapper = [[NSColorPanelWrapper alloc] initWithPicker:this];
+ return NS_OK;
+}
+
+/* static */ NSColor* nsColorPicker::GetNSColorFromHexString(
+ const nsAString& aColor) {
+ NSString* str = nsCocoaUtils::ToNSString(aColor);
+
+ double red = HexStrToInt([str substringWithRange:NSMakeRange(1, 2)]) / 255.0;
+ double green =
+ HexStrToInt([str substringWithRange:NSMakeRange(3, 2)]) / 255.0;
+ double blue = HexStrToInt([str substringWithRange:NSMakeRange(5, 2)]) / 255.0;
+
+ return [NSColor colorWithDeviceRed:red green:green blue:blue alpha:1.0];
+}
+
+/* static */ void nsColorPicker::GetHexStringFromNSColor(NSColor* aColor,
+ nsAString& aResult) {
+ CGFloat redFloat, greenFloat, blueFloat;
+
+ NSColor* color = aColor;
+ @try {
+ [color getRed:&redFloat green:&greenFloat blue:&blueFloat alpha:nil];
+ } @catch (NSException* e) {
+ color = [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]];
+ [color getRed:&redFloat green:&greenFloat blue:&blueFloat alpha:nil];
+ }
+
+ nsCocoaUtils::GetStringForNSString(
+ [NSString stringWithFormat:@"#%02x%02x%02x", (int)(redFloat * 255),
+ (int)(greenFloat * 255),
+ (int)(blueFloat * 255)],
+ aResult);
+}
+
+NS_IMETHODIMP
+nsColorPicker::Open(nsIColorPickerShownCallback* aCallback) {
+ MOZ_ASSERT(aCallback);
+ mCallback = aCallback;
+
+ [mColorPanelWrapper open:GetNSColorFromHexString(mColor)
+ title:nsCocoaUtils::ToNSString(mTitle)];
+
+ NS_ADDREF_THIS();
+
+ return NS_OK;
+}
+
+void nsColorPicker::Update(NSColor* aColor) {
+ GetHexStringFromNSColor(aColor, mColor);
+ mCallback->Update(mColor);
+}
+
+void nsColorPicker::Done() {
+ [mColorPanelWrapper close];
+ [mColorPanelWrapper release];
+ mColorPanelWrapper = nullptr;
+ mCallback->Done(u""_ns);
+ mCallback = nullptr;
+ NS_RELEASE_THIS();
+}
diff --git a/widget/cocoa/nsCursorManager.h b/widget/cocoa/nsCursorManager.h
new file mode 100644
index 0000000000..cc42a3b842
--- /dev/null
+++ b/widget/cocoa/nsCursorManager.h
@@ -0,0 +1,63 @@
+/* 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/. */
+
+#ifndef nsCursorManager_h_
+#define nsCursorManager_h_
+
+#import <Foundation/Foundation.h>
+#include "nsIWidget.h"
+#include "nsMacCursor.h"
+
+/*! @class nsCursorManager
+ @abstract Singleton service provides access to all cursors available in
+ the application.
+ @discussion Use <code>nsCusorManager</code> to set the current cursor using
+ an XP <code>nsCusor</code> enum value.
+ <code>nsCursorManager</code> encapsulates the details of
+ setting different types of cursors, animating cursors and
+ cleaning up cursors when they are no longer in use.
+ */
+@interface nsCursorManager : NSObject {
+ @private
+ NSMutableDictionary* mCursors;
+ nsMacCursor* mCurrentMacCursor;
+}
+
+/*! @method setCursor:
+ @abstract Sets the current cursor.
+ @discussion Sets the current cursor to the cursor indicated by the XP
+ cursor given in the argument. Resources associated with the
+ previous cursor are cleaned up.
+ @param aCursor the cursor to use
+*/
+- (nsresult)setNonCustomCursor:(const nsIWidget::Cursor&)aCursor;
+
+// As above, but returns an error if the cursor isn't custom or we couldn't set
+// it for some reason.
+- (nsresult)setCustomCursor:(const nsIWidget::Cursor&)aCursor
+ widgetScaleFactor:(CGFloat)aWidgetScaleFactor
+ forceUpdate:(bool)aForceUpdate;
+
+/*! @method sharedInstance
+ @abstract Get the Singleton instance of the cursor manager.
+ @discussion Use this method to obtain a reference to the cursor manager.
+ @result a reference to the cursor manager
+*/
++ (nsCursorManager*)sharedInstance;
+
+/*! @method dispose
+ @abstract Releases the shared instance of the cursor manager.
+ @discussion Use dispose to clean up the cursor manager and associated
+ cursors.
+*/
++ (void)dispose;
+@end
+
+@interface NSCursor (Undocumented)
+// busyButClickableCursor is an undocumented NSCursor API, but has been in use
+// since at least OS X 10.4 and through 10.9.
++ (NSCursor*)busyButClickableCursor;
+@end
+
+#endif // nsCursorManager_h_
diff --git a/widget/cocoa/nsCursorManager.mm b/widget/cocoa/nsCursorManager.mm
new file mode 100644
index 0000000000..2db20681ad
--- /dev/null
+++ b/widget/cocoa/nsCursorManager.mm
@@ -0,0 +1,363 @@
+/* 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 "imgIContainer.h"
+#include "nsCocoaUtils.h"
+#include "nsCursorManager.h"
+#include "nsObjCExceptions.h"
+#include <math.h>
+
+static nsCursorManager* gInstance;
+static CGFloat sCurrentCursorScaleFactor = 0.0f;
+static nsIWidget::Cursor sCurrentCursor;
+static constexpr nsCursor kCustomCursor = eCursorCount;
+
+/*! @category nsCursorManager(PrivateMethods)
+ Private methods for the cursor manager class.
+*/
+@interface nsCursorManager (PrivateMethods)
+/*! @method getCursor:
+ @abstract Get a reference to the native Mac representation of a cursor.
+ @discussion Gets a reference to the Mac native implementation of a cursor.
+ If the cursor has been requested before, it is retreived from
+ the cursor cache, otherwise it is created and cached.
+ @param aCursor the cursor to get
+ @result the Mac native implementation of the cursor
+*/
+- (nsMacCursor*)getCursor:(nsCursor)aCursor;
+
+/*! @method setMacCursor:
+ @abstract Set the current Mac native cursor
+ @discussion Sets the current cursor - this routine is what actually causes the
+ cursor to change. The argument is retained and the old cursor is
+ released.
+ @param aMacCursor the cursor to set
+ @result NS_OK
+ */
+- (nsresult)setMacCursor:(nsMacCursor*)aMacCursor;
+
+/*! @method createCursor:
+ @abstract Create a Mac native representation of a cursor.
+ @discussion Creates a version of the Mac native representation of this
+ cursor.
+ @param aCursor the cursor to create
+ @result the Mac native implementation of the cursor
+*/
++ (nsMacCursor*)createCursor:(enum nsCursor)aCursor;
+
+@end
+
+@implementation nsCursorManager
+
++ (nsCursorManager*)sharedInstance {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!gInstance) {
+ gInstance = [[nsCursorManager alloc] init];
+ }
+ return gInstance;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (void)dispose {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [gInstance release];
+ gInstance = nil;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
++ (nsMacCursor*)createCursor:(enum nsCursor)aCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ switch (aCursor) {
+ case eCursor_standard:
+ return [nsMacCursor cursorWithCursor:[NSCursor arrowCursor] type:aCursor];
+ case eCursor_wait:
+ case eCursor_spinning: {
+ return [nsMacCursor cursorWithCursor:[NSCursor busyButClickableCursor]
+ type:aCursor];
+ }
+ case eCursor_select:
+ return [nsMacCursor cursorWithCursor:[NSCursor IBeamCursor] type:aCursor];
+ case eCursor_hyperlink:
+ return [nsMacCursor cursorWithCursor:[NSCursor pointingHandCursor]
+ type:aCursor];
+ case eCursor_crosshair:
+ return [nsMacCursor cursorWithCursor:[NSCursor crosshairCursor]
+ type:aCursor];
+ case eCursor_move:
+ return [nsMacCursor cursorWithImageNamed:@"move"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ case eCursor_help:
+ return [nsMacCursor cursorWithImageNamed:@"help"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ case eCursor_copy: {
+ SEL cursorSelector = @selector(dragCopyCursor);
+ return [nsMacCursor
+ cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ case eCursor_alias: {
+ SEL cursorSelector = @selector(dragLinkCursor);
+ return [nsMacCursor
+ cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ case eCursor_context_menu: {
+ SEL cursorSelector = @selector(contextualMenuCursor);
+ return [nsMacCursor
+ cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ case eCursor_cell:
+ return [nsMacCursor cursorWithImageNamed:@"cell"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ case eCursor_grab:
+ return [nsMacCursor cursorWithCursor:[NSCursor openHandCursor]
+ type:aCursor];
+ case eCursor_grabbing:
+ return [nsMacCursor cursorWithCursor:[NSCursor closedHandCursor]
+ type:aCursor];
+ case eCursor_zoom_in:
+ return [nsMacCursor cursorWithImageNamed:@"zoomIn"
+ hotSpot:NSMakePoint(10, 10)
+ type:aCursor];
+ case eCursor_zoom_out:
+ return [nsMacCursor cursorWithImageNamed:@"zoomOut"
+ hotSpot:NSMakePoint(10, 10)
+ type:aCursor];
+ case eCursor_vertical_text:
+ return [nsMacCursor cursorWithImageNamed:@"vtIBeam"
+ hotSpot:NSMakePoint(12, 11)
+ type:aCursor];
+ case eCursor_all_scroll:
+ return [nsMacCursor cursorWithCursor:[NSCursor openHandCursor]
+ type:aCursor];
+ case eCursor_not_allowed:
+ case eCursor_no_drop: {
+ SEL cursorSelector = @selector(operationNotAllowedCursor);
+ return [nsMacCursor
+ cursorWithCursor:[NSCursor respondsToSelector:cursorSelector]
+ ? [NSCursor performSelector:cursorSelector]
+ : [NSCursor arrowCursor]
+ type:aCursor];
+ }
+ // Resize Cursors:
+ // North
+ case eCursor_n_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeUpCursor]
+ type:aCursor];
+ // North East
+ case eCursor_ne_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNE"
+ hotSpot:NSMakePoint(12, 11)
+ type:aCursor];
+ // East
+ case eCursor_e_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeRightCursor]
+ type:aCursor];
+ // South East
+ case eCursor_se_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeSE"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ // South
+ case eCursor_s_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeDownCursor]
+ type:aCursor];
+ // South West
+ case eCursor_sw_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeSW"
+ hotSpot:NSMakePoint(10, 12)
+ type:aCursor];
+ // West
+ case eCursor_w_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeLeftCursor]
+ type:aCursor];
+ // North West
+ case eCursor_nw_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNW"
+ hotSpot:NSMakePoint(11, 11)
+ type:aCursor];
+ // North & South
+ case eCursor_ns_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeUpDownCursor]
+ type:aCursor];
+ // East & West
+ case eCursor_ew_resize:
+ return [nsMacCursor cursorWithCursor:[NSCursor resizeLeftRightCursor]
+ type:aCursor];
+ // North East & South West
+ case eCursor_nesw_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNESW"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ // North West & South East
+ case eCursor_nwse_resize:
+ return [nsMacCursor cursorWithImageNamed:@"sizeNWSE"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ // Column Resize
+ case eCursor_col_resize:
+ return [nsMacCursor cursorWithImageNamed:@"colResize"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ // Row Resize
+ case eCursor_row_resize:
+ return [nsMacCursor cursorWithImageNamed:@"rowResize"
+ hotSpot:NSMakePoint(12, 12)
+ type:aCursor];
+ default:
+ return [nsMacCursor cursorWithCursor:[NSCursor arrowCursor] type:aCursor];
+ }
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)init {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super init])) {
+ mCursors = [[NSMutableDictionary alloc] initWithCapacity:25];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (nsresult)setNonCustomCursor:(const nsIWidget::Cursor&)aCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [self setMacCursor:[self getCursor:aCursor.mDefaultCursor]];
+
+ sCurrentCursor = aCursor;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+- (nsresult)setMacCursor:(nsMacCursor*)aMacCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCursor oldType = [mCurrentMacCursor type];
+ nsCursor newType = [aMacCursor type];
+ if (oldType != newType) {
+ if (newType == eCursor_none) {
+ [NSCursor hide];
+ } else if (oldType == eCursor_none) {
+ [NSCursor unhide];
+ }
+ }
+
+ if (mCurrentMacCursor != aMacCursor || ![mCurrentMacCursor isSet]) {
+ [aMacCursor retain];
+ [mCurrentMacCursor unset];
+ [aMacCursor set];
+ [mCurrentMacCursor release];
+ mCurrentMacCursor = aMacCursor;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+- (nsresult)setCustomCursor:(const nsIWidget::Cursor&)aCursor
+ widgetScaleFactor:(CGFloat)scaleFactor
+ forceUpdate:(bool)aForceUpdate {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // As the user moves the mouse, this gets called repeatedly with the same
+ // aCursorImage
+ if (!aForceUpdate && sCurrentCursor == aCursor &&
+ sCurrentCursorScaleFactor == scaleFactor && mCurrentMacCursor) {
+ // Native dragging can unset our cursor apparently (see bug 1739352).
+ if (MOZ_UNLIKELY(![mCurrentMacCursor isSet])) {
+ [mCurrentMacCursor set];
+ }
+ return NS_OK;
+ }
+
+ sCurrentCursor = aCursor;
+ sCurrentCursorScaleFactor = scaleFactor;
+
+ if (!aCursor.IsCustom()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIntSize size = nsIWidget::CustomCursorSize(aCursor);
+ // prevent DoS attacks
+ if (size.width > 128 || size.height > 128) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSImage* cursorImage;
+ nsresult rv = nsCocoaUtils::CreateNSImageFromImageContainer(
+ aCursor.mContainer, imgIContainer::FRAME_FIRST, nullptr, nullptr,
+ &cursorImage, scaleFactor);
+ if (NS_FAILED(rv) || !cursorImage) {
+ return NS_ERROR_FAILURE;
+ }
+
+ {
+ NSSize cocoaSize = NSMakeSize(size.width, size.height);
+ [cursorImage setSize:cocoaSize];
+ [[[cursorImage representations] objectAtIndex:0] setSize:cocoaSize];
+ }
+
+ // if the hotspot is nonsensical, make it 0,0
+ uint32_t hotspotX =
+ aCursor.mHotspotX > (uint32_t(size.width) - 1) ? 0 : aCursor.mHotspotX;
+ uint32_t hotspotY =
+ aCursor.mHotspotY > (uint32_t(size.height) - 1) ? 0 : aCursor.mHotspotY;
+ NSPoint hotSpot = ::NSMakePoint(hotspotX, hotspotY);
+ [self setMacCursor:[nsMacCursor cursorWithCursor:[[NSCursor alloc]
+ initWithImage:cursorImage
+ hotSpot:hotSpot]
+ type:kCustomCursor]];
+ [cursorImage release];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+- (nsMacCursor*)getCursor:(enum nsCursor)aCursor {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsMacCursor* result =
+ [mCursors objectForKey:[NSNumber numberWithInt:aCursor]];
+ if (!result) {
+ result = [nsCursorManager createCursor:aCursor];
+ [mCursors setObject:result forKey:[NSNumber numberWithInt:aCursor]];
+ }
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mCurrentMacCursor unset];
+ [mCurrentMacCursor release];
+ [mCursors release];
+ sCurrentCursor = {};
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
diff --git a/widget/cocoa/nsDeviceContextSpecX.h b/widget/cocoa/nsDeviceContextSpecX.h
new file mode 100644
index 0000000000..a5a5bbb9d9
--- /dev/null
+++ b/widget/cocoa/nsDeviceContextSpecX.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsDeviceContextSpecX_h_
+#define nsDeviceContextSpecX_h_
+
+#include "nsIDeviceContextSpec.h"
+#include "nsIPrinter.h"
+#include "nsIPrinterList.h"
+
+#include "nsCOMPtr.h"
+
+#include "mozilla/gfx/PrintPromise.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+
+class nsDeviceContextSpecX : public nsIDeviceContextSpec {
+ public:
+ NS_DECL_ISUPPORTS
+
+ nsDeviceContextSpecX();
+
+ NS_IMETHOD Init(nsIPrintSettings* aPS, bool aIsPrintPreview) override;
+ already_AddRefed<PrintTarget> MakePrintTarget() final;
+ NS_IMETHOD BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) override;
+ RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() override;
+ NS_IMETHOD BeginPage(const IntSize& aSizeInPoints) override { return NS_OK; };
+ NS_IMETHOD EndPage() override { return NS_OK; };
+
+ void GetPaperRect(double* aTop, double* aLeft, double* aBottom,
+ double* aRight);
+
+ protected:
+ virtual ~nsDeviceContextSpecX();
+
+ protected:
+ PMPrintSession mPrintSession = nullptr;
+ PMPageFormat mPageFormat = nullptr;
+ PMPrintSettings mPMPrintSettings = nullptr;
+ nsCOMPtr<nsIOutputStream> mOutputStream; // Output stream from settings.
+#ifdef MOZ_ENABLE_SKIA_PDF
+ // file "print" output generated if printing via PDF
+ nsCOMPtr<nsIFile> mTempFile;
+#endif
+ private:
+ nsresult DoEndDocument();
+};
+
+#endif // nsDeviceContextSpecX_h_
diff --git a/widget/cocoa/nsDeviceContextSpecX.mm b/widget/cocoa/nsDeviceContextSpecX.mm
new file mode 100644
index 0000000000..9bb8449b70
--- /dev/null
+++ b/widget/cocoa/nsDeviceContextSpecX.mm
@@ -0,0 +1,318 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsDeviceContextSpecX.h"
+
+#import <Cocoa/Cocoa.h>
+#include "mozilla/gfx/PrintPromise.h"
+#include <CoreFoundation/CoreFoundation.h>
+#include <unistd.h>
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+# include "mozilla/gfx/PrintTargetSkPDF.h"
+#endif
+#include "mozilla/gfx/PrintTargetCG.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Telemetry.h"
+
+#include "AppleUtils.h"
+#include "nsCocoaUtils.h"
+#include "nsCRT.h"
+#include "nsCUPSShim.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsILocalFileMac.h"
+#include "nsIOutputStream.h"
+#include "nsPaper.h"
+#include "nsPrinterListCUPS.h"
+#include "nsPrintSettingsX.h"
+#include "nsQueryObject.h"
+#include "prenv.h"
+
+// This must be the last include:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+using mozilla::gfx::IntSize;
+using mozilla::gfx::PrintEndDocumentPromise;
+using mozilla::gfx::PrintTarget;
+using mozilla::gfx::PrintTargetCG;
+#ifdef MOZ_ENABLE_SKIA_PDF
+using mozilla::gfx::PrintTargetSkPDF;
+#endif
+
+//----------------------------------------------------------------------
+// nsDeviceContentSpecX
+
+nsDeviceContextSpecX::nsDeviceContextSpecX() = default;
+
+nsDeviceContextSpecX::~nsDeviceContextSpecX() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mPrintSession) {
+ ::PMRelease(mPrintSession);
+ }
+ if (mPageFormat) {
+ ::PMRelease(mPageFormat);
+ }
+ if (mPMPrintSettings) {
+ ::PMRelease(mPMPrintSettings);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+NS_IMPL_ISUPPORTS(nsDeviceContextSpecX, nsIDeviceContextSpec)
+
+NS_IMETHODIMP nsDeviceContextSpecX::Init(nsIPrintSettings* aPS,
+ bool aIsPrintPreview) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ RefPtr<nsPrintSettingsX> settings(do_QueryObject(aPS));
+ if (!settings) {
+ return NS_ERROR_NO_INTERFACE;
+ }
+ // Note: unlike other platforms, we don't set our base class's mPrintSettings
+ // here since we don't need it currently (we do set mPMPrintSettings below).
+
+ NSPrintInfo* printInfo = settings->CreateOrCopyPrintInfo();
+ if (!printInfo) {
+ return NS_ERROR_FAILURE;
+ }
+ if (aPS->GetOutputDestination() ==
+ nsIPrintSettings::kOutputDestinationStream) {
+ aPS->GetOutputStream(getter_AddRefs(mOutputStream));
+ if (!mOutputStream) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ mPrintSession = static_cast<PMPrintSession>([printInfo PMPrintSession]);
+ mPageFormat = static_cast<PMPageFormat>([printInfo PMPageFormat]);
+ mPMPrintSettings = static_cast<PMPrintSettings>([printInfo PMPrintSettings]);
+ MOZ_ASSERT(mPrintSession && mPageFormat && mPMPrintSettings);
+ ::PMRetain(mPrintSession);
+ ::PMRetain(mPageFormat);
+ ::PMRetain(mPMPrintSettings);
+ [printInfo release];
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ nsAutoString printViaPdf;
+ mozilla::Preferences::GetString("print.print_via_pdf_encoder", printViaPdf);
+ if (printViaPdf.EqualsLiteral("skia-pdf")) {
+ // Annoyingly, PMPrinterPrintWithFile does not pay attention to the
+ // kPMDestination* value set in the PMPrintSession; it always sends the PDF
+ // to the specified printer. This means that if we create the PDF using
+ // SkPDF then we need to manually handle user actions like "Open PDF in
+ // Preview" and "Save as PDF...".
+ // TODO: Currently we do not support using SkPDF for kPMDestinationFax or
+ // kPMDestinationProcessPDF ("Add PDF to iBooks, etc.), and we only support
+ // it for kPMDestinationFile if the destination file is a PDF.
+ // XXX Could PMWorkflowSubmitPDFWithSettings/PMPrinterPrintWithProvider
+ // help?
+ OSStatus status = noErr;
+ PMDestinationType destination;
+ status = ::PMSessionGetDestinationType(mPrintSession, mPMPrintSettings,
+ &destination);
+ if (status == noErr) {
+ if (destination == kPMDestinationPrinter ||
+ destination == kPMDestinationPreview) {
+ mPrintViaSkPDF = true;
+ } else if (destination == kPMDestinationFile) {
+ AutoCFRelease<CFURLRef> destURL(nullptr);
+ status = ::PMSessionCopyDestinationLocation(
+ mPrintSession, mPMPrintSettings, destURL.receive());
+ if (status == noErr) {
+ AutoCFRelease<CFStringRef> destPathRef =
+ CFURLCopyFileSystemPath(destURL, kCFURLPOSIXPathStyle);
+ NSString* destPath = (NSString*)CFStringRef(destPathRef);
+ NSString* destPathExt = [destPath pathExtension];
+ if ([destPathExt isEqualToString:@"pdf"]) {
+ mPrintViaSkPDF = true;
+ }
+ }
+ }
+ }
+ }
+#endif
+
+ int16_t outputFormat = aPS->GetOutputFormat();
+
+ if (outputFormat == nsIPrintSettings::kOutputFormatPDF) {
+ // We don't actually currently support/use kOutputFormatPDF on mac, but
+ // this is for completeness in case we add that (we probably need to in
+ // order to support adding links into saved PDFs, for example).
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"pdf_file"_ns, 1);
+ } else {
+ PMDestinationType destination;
+ OSStatus status = ::PMSessionGetDestinationType(
+ mPrintSession, mPMPrintSettings, &destination);
+ if (status == noErr && (destination == kPMDestinationFile ||
+ destination == kPMDestinationPreview ||
+ destination == kPMDestinationProcessPDF)) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"pdf_file"_ns, 1);
+ } else {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"unknown"_ns, 1);
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsDeviceContextSpecX::BeginDocument(
+ const nsAString& aTitle, const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+RefPtr<PrintEndDocumentPromise> nsDeviceContextSpecX::EndDocument() {
+ return nsIDeviceContextSpec::EndDocumentPromiseFromResult(DoEndDocument(),
+ __func__);
+}
+
+nsresult nsDeviceContextSpecX::DoEndDocument() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ if (mPrintViaSkPDF) {
+ OSStatus status = noErr;
+
+ nsCOMPtr<nsILocalFileMac> tmpPDFFile = do_QueryInterface(mTempFile);
+ if (!tmpPDFFile) {
+ return NS_ERROR_FAILURE;
+ }
+ AutoCFRelease<CFURLRef> pdfURL(nullptr);
+ // Note that the caller is responsible to release pdfURL according to
+ // nsILocalFileMac.idl, even though we didn't follow the Core Foundation
+ // naming conventions here (the method should've been called CopyCFURL).
+ nsresult rv = tmpPDFFile->GetCFURL(pdfURL.receive());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PMDestinationType destination;
+ status = ::PMSessionGetDestinationType(mPrintSession, mPMPrintSettings,
+ &destination);
+
+ switch (destination) {
+ case kPMDestinationPrinter: {
+ PMPrinter currentPrinter = NULL;
+ status = ::PMSessionGetCurrentPrinter(mPrintSession, &currentPrinter);
+ if (status != noErr) {
+ return NS_ERROR_FAILURE;
+ }
+ CFStringRef mimeType = CFSTR("application/pdf");
+ status = ::PMPrinterPrintWithFile(currentPrinter, mPMPrintSettings,
+ mPageFormat, mimeType, pdfURL);
+ break;
+ }
+ case kPMDestinationPreview: {
+ // XXXjwatt Or should we use CocoaFileUtils::RevealFileInFinder(pdfURL);
+ AutoCFRelease<CFStringRef> pdfPath =
+ CFURLCopyFileSystemPath(pdfURL, kCFURLPOSIXPathStyle);
+ NSString* path = (NSString*)CFStringRef(pdfPath);
+ NSWorkspace* ws = [NSWorkspace sharedWorkspace];
+ [ws openFile:path];
+ break;
+ }
+ case kPMDestinationFile: {
+ AutoCFRelease<CFURLRef> destURL(nullptr);
+ status = ::PMSessionCopyDestinationLocation(
+ mPrintSession, mPMPrintSettings, destURL.receive());
+ if (status == noErr) {
+ AutoCFRelease<CFStringRef> sourcePathRef =
+ CFURLCopyFileSystemPath(pdfURL, kCFURLPOSIXPathStyle);
+ NSString* sourcePath = (NSString*)CFStringRef(sourcePathRef);
+# ifdef DEBUG
+ AutoCFRelease<CFStringRef> destPathRef =
+ CFURLCopyFileSystemPath(destURL, kCFURLPOSIXPathStyle);
+ NSString* destPath = (NSString*)CFStringRef(destPathRef);
+ NSString* destPathExt = [destPath pathExtension];
+ MOZ_ASSERT([destPathExt isEqualToString:@"pdf"],
+ "nsDeviceContextSpecX::Init only allows '.pdf' for now");
+ // We could use /usr/sbin/cupsfilter to convert the PDF to PS, but
+ // currently we don't.
+# endif
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ if ([fileManager fileExistsAtPath:sourcePath]) {
+ NSURL* src = static_cast<NSURL*>(CFURLRef(pdfURL));
+ NSURL* dest = static_cast<NSURL*>(CFURLRef(destURL));
+ bool ok = [fileManager
+ replaceItemAtURL:dest
+ withItemAtURL:src
+ backupItemName:nil
+ options:
+ NSFileManagerItemReplacementUsingNewMetadataOnly
+ resultingItemURL:nil
+ error:nil];
+ if (!ok) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("nsDeviceContextSpecX::Init doesn't set "
+ "mPrintViaSkPDF for other values");
+ }
+
+ return (status == noErr) ? NS_OK : NS_ERROR_FAILURE;
+ }
+#endif
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsDeviceContextSpecX::GetPaperRect(double* aTop, double* aLeft,
+ double* aBottom, double* aRight) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ PMRect paperRect;
+ ::PMGetAdjustedPaperRect(mPageFormat, &paperRect);
+
+ *aTop = paperRect.top;
+ *aLeft = paperRect.left;
+ *aBottom = paperRect.bottom;
+ *aRight = paperRect.right;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+already_AddRefed<PrintTarget> nsDeviceContextSpecX::MakePrintTarget() {
+ double top, left, bottom, right;
+ GetPaperRect(&top, &left, &bottom, &right);
+ const double width = right - left;
+ const double height = bottom - top;
+ IntSize size = IntSize::Ceil(width, height);
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ if (mPrintViaSkPDF) {
+ // TODO: Add support for stream printing via SkPDF if we enable that again.
+ nsresult rv =
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mTempFile));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ nsAutoCString tempPath("tmp-printing.pdf");
+ mTempFile->AppendNative(tempPath);
+ rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ mTempFile->GetNativePath(tempPath);
+ auto stream = MakeUnique<SkFILEWStream>(tempPath.get());
+ return PrintTargetSkPDF::CreateOrNull(std::move(stream), size);
+ }
+#endif
+
+ return PrintTargetCG::CreateOrNull(mOutputStream, mPrintSession, mPageFormat,
+ mPMPrintSettings, size);
+}
diff --git a/widget/cocoa/nsDragService.h b/widget/cocoa/nsDragService.h
new file mode 100644
index 0000000000..b175b93247
--- /dev/null
+++ b/widget/cocoa/nsDragService.h
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+
+#ifndef nsDragService_h_
+#define nsDragService_h_
+
+#include "nsBaseDragService.h"
+#include "nsChildView.h"
+
+#include <Cocoa/Cocoa.h>
+
+class nsDragService : public nsBaseDragService {
+ public:
+ nsDragService();
+
+ // nsBaseDragService
+ MOZ_CAN_RUN_SCRIPT virtual nsresult InvokeDragSessionImpl(
+ nsIArray* anArrayTransferables,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ uint32_t aActionType) override;
+ // nsIDragService
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndDragSession(bool aDoneDrag,
+ uint32_t aKeyModifiers) override;
+ NS_IMETHOD UpdateDragImage(nsINode* aImage, int32_t aImageX,
+ int32_t aImageY) override;
+
+ // nsIDragSession
+ NS_IMETHOD GetData(nsITransferable* aTransferable,
+ uint32_t aItemIndex) override;
+ NS_IMETHOD IsDataFlavorSupported(const char* aDataFlavor,
+ bool* _retval) override;
+ NS_IMETHOD GetNumDropItems(uint32_t* aNumItems) override;
+
+ void DragMovedWithView(NSDraggingSession* aSession, NSPoint aPoint);
+
+ protected:
+ virtual ~nsDragService();
+
+ private:
+ // Creates and returns the drag image for a drag. aImagePoint will be set to
+ // the origin of the drag relative to mNativeDragView.
+ NSImage* ConstructDragImage(
+ nsINode* aDOMNode, const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ NSPoint* aImagePoint);
+
+ // Creates and returns the drag image for a drag. aPoint should be the origin
+ // of the drag, for example the mouse coordinate of the mousedown event.
+ // aDragRect will be set the area of the drag relative to this.
+ NSImage* ConstructDragImage(
+ nsINode* aDOMNode, const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ mozilla::CSSIntPoint aPoint, mozilla::LayoutDeviceIntRect* aDragRect);
+
+ nsCOMPtr<nsIArray> mDataItems; // only valid for a drag started within gecko
+ ChildView* mNativeDragView;
+ NSEvent* mNativeDragEvent;
+
+ bool mDragImageChanged;
+};
+
+#endif // nsDragService_h_
diff --git a/widget/cocoa/nsDragService.mm b/widget/cocoa/nsDragService.mm
new file mode 100644
index 0000000000..26bb834a89
--- /dev/null
+++ b/widget/cocoa/nsDragService.mm
@@ -0,0 +1,507 @@
+/* -*- 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 "mozilla/Logging.h"
+
+#include "gfxContext.h"
+#include "nsArrayUtils.h"
+#include "nsDragService.h"
+#include "nsArrayUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsITransferable.h"
+#include "nsString.h"
+#include "nsClipboard.h"
+#include "nsXPCOM.h"
+#include "nsCOMPtr.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsLinebreakConverter.h"
+#include "nsINode.h"
+#include "nsRect.h"
+#include "nsPoint.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsIContent.h"
+#include "nsView.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "gfxPlatform.h"
+#include "nsDeviceContext.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+extern mozilla::LazyLogModule sCocoaLog;
+
+extern NSPasteboard* globalDragPboard;
+extern ChildView* gLastDragView;
+extern NSEvent* gLastDragMouseDownEvent;
+extern bool gUserCancelledDrag;
+
+// This global makes the transferable array available to Cocoa's promised
+// file destination callback.
+mozilla::StaticRefPtr<nsIArray> gDraggedTransferables;
+
+nsDragService::nsDragService()
+ : mNativeDragView(nil), mNativeDragEvent(nil), mDragImageChanged(false) {}
+
+nsDragService::~nsDragService() {}
+
+NSImage* nsDragService::ConstructDragImage(nsINode* aDOMNode,
+ const Maybe<CSSIntRegion>& aRegion,
+ NSPoint* aDragPoint) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+
+ LayoutDeviceIntRect dragRect(0, 0, 20, 20);
+ NSImage* image =
+ ConstructDragImage(mSourceNode, aRegion, mScreenPosition, &dragRect);
+ if (!image) {
+ // if no image was returned, just draw a rectangle
+ NSSize size;
+ size.width =
+ nsCocoaUtils::DevPixelsToCocoaPoints(dragRect.width, scaleFactor);
+ size.height =
+ nsCocoaUtils::DevPixelsToCocoaPoints(dragRect.height, scaleFactor);
+ image = [NSImage imageWithSize:size
+ flipped:YES
+ drawingHandler:^BOOL(NSRect dstRect) {
+ [[NSColor grayColor] set];
+ NSBezierPath* path =
+ [NSBezierPath bezierPathWithRect:dstRect];
+ [path setLineWidth:2.0];
+ [path stroke];
+ return YES;
+ }];
+ }
+
+ LayoutDeviceIntPoint pt(dragRect.x, dragRect.YMost());
+ NSPoint point = nsCocoaUtils::DevPixelsToCocoaPoints(pt, scaleFactor);
+ point.y = nsCocoaUtils::FlippedScreenY(point.y);
+
+ point = nsCocoaUtils::ConvertPointFromScreen([mNativeDragView window], point);
+ *aDragPoint = [mNativeDragView convertPoint:point fromView:nil];
+
+ return image;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NSImage* nsDragService::ConstructDragImage(nsINode* aDOMNode,
+ const Maybe<CSSIntRegion>& aRegion,
+ CSSIntPoint aPoint,
+ LayoutDeviceIntRect* aDragRect) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+
+ RefPtr<SourceSurface> surface;
+ nsPresContext* pc;
+ nsresult rv = DrawDrag(aDOMNode, aRegion, aPoint, aDragRect, &surface, &pc);
+ if (pc && (!aDragRect->width || !aDragRect->height)) {
+ // just use some suitable defaults
+ int32_t size = nsCocoaUtils::CocoaPointsToDevPixels(20, scaleFactor);
+ aDragRect->SetRect(pc->CSSPixelsToDevPixels(aPoint.x),
+ pc->CSSPixelsToDevPixels(aPoint.y), size, size);
+ }
+
+ if (NS_FAILED(rv) || !surface) return nil;
+
+ uint32_t width = aDragRect->width;
+ uint32_t height = aDragRect->height;
+
+ RefPtr<DataSourceSurface> dataSurface = Factory::CreateDataSourceSurface(
+ IntSize(width, height), SurfaceFormat::B8G8R8A8);
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ_WRITE, &map)) {
+ return nil;
+ }
+
+ RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
+ BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride,
+ dataSurface->GetFormat());
+ if (!dt) {
+ dataSurface->Unmap();
+ return nil;
+ }
+
+ dt->FillRect(gfx::Rect(0, 0, width, height),
+ SurfacePattern(surface, ExtendMode::CLAMP),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+
+ NSBitmapImageRep* imageRep =
+ [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
+ pixelsWide:width
+ pixelsHigh:height
+ bitsPerSample:8
+ samplesPerPixel:4
+ hasAlpha:YES
+ isPlanar:NO
+ colorSpaceName:NSDeviceRGBColorSpace
+ bytesPerRow:width * 4
+ bitsPerPixel:32];
+
+ uint8_t* dest = [imageRep bitmapData];
+ for (uint32_t i = 0; i < height; ++i) {
+ uint8_t* src = map.mData + i * map.mStride;
+ for (uint32_t j = 0; j < width; ++j) {
+ // Reduce transparency overall by multipying by a factor. Remember, Alpha
+ // is premultipled here. Also, Quartz likes RGBA, so do that translation
+ // as well.
+#ifdef IS_BIG_ENDIAN
+ dest[0] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+ dest[1] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+ dest[2] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
+ dest[3] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
+#else
+ dest[0] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+ dest[1] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+ dest[2] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
+ dest[3] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
+#endif
+ src += 4;
+ dest += 4;
+ }
+ }
+ dataSurface->Unmap();
+
+ NSImage* image = [[NSImage alloc]
+ initWithSize:NSMakeSize(width / scaleFactor, height / scaleFactor)];
+ [image addRepresentation:imageRep];
+ [imageRep release];
+
+ return [image autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+nsresult nsDragService::InvokeDragSessionImpl(
+ nsIArray* aTransferableArray, const Maybe<CSSIntRegion>& aRegion,
+ uint32_t aActionType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+#ifdef NIGHTLY_BUILD
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#endif
+
+ if (!gLastDragView) {
+ // gLastDragView is non-null between -[ChildView mouseDown:] and -[ChildView
+ // mouseUp:]. If we get here with gLastDragView being null, that means that
+ // the mouse button has already been released. In that case we need to abort
+ // the drag because the OS won't know where to drop whatever's being
+ // dragged, and we might end up with a stuck drag & drop session.
+ return NS_ERROR_FAILURE;
+ }
+
+ mDataItems = aTransferableArray;
+
+ // Save the transferables away in case a promised file callback is invoked.
+ gDraggedTransferables = aTransferableArray;
+
+ // We need to retain the view and the event during the drag in case either
+ // gets destroyed.
+ mNativeDragView = [gLastDragView retain];
+ mNativeDragEvent = [gLastDragMouseDownEvent retain];
+
+ gUserCancelledDrag = false;
+
+ NSPasteboardItem* pbItem = [NSPasteboardItem new];
+ NSMutableArray* types = [NSMutableArray arrayWithCapacity:5];
+
+ if (gDraggedTransferables) {
+ uint32_t count = 0;
+ gDraggedTransferables->GetLength(&count);
+
+ for (uint32_t j = 0; j < count; j++) {
+ nsCOMPtr<nsITransferable> currentTransferable =
+ do_QueryElementAt(aTransferableArray, j);
+ if (!currentTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Transform the transferable to an NSDictionary
+ NSDictionary* pasteboardOutputDict =
+ nsClipboard::PasteboardDictFromTransferable(currentTransferable);
+ if (!pasteboardOutputDict) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // write everything out to the general pasteboard
+ [types addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ // Gecko is initiating this drag so we always want its own views to
+ // consider it. Add our wildcard type to the pasteboard to accomplish
+ // this.
+ [types addObject:[UTIHelper stringFromPboardType:kMozWildcardPboardType]];
+ }
+ }
+ [pbItem setDataProvider:mNativeDragView forTypes:types];
+
+ NSPoint draggingPoint;
+ NSImage* image = ConstructDragImage(mSourceNode, aRegion, &draggingPoint);
+
+ NSRect localDragRect = image.alignmentRect;
+ localDragRect.origin.x = draggingPoint.x;
+ localDragRect.origin.y = draggingPoint.y - localDragRect.size.height;
+
+ NSDraggingItem* dragItem =
+ [[NSDraggingItem alloc] initWithPasteboardWriter:pbItem];
+ [pbItem release];
+ [dragItem setDraggingFrame:localDragRect contents:image];
+
+ nsBaseDragService::StartDragSession();
+ nsBaseDragService::OpenDragPopup();
+
+ NSDraggingSession* draggingSession = [mNativeDragView
+ beginDraggingSessionWithItems:[NSArray
+ arrayWithObject:[dragItem autorelease]]
+ event:mNativeDragEvent
+ source:mNativeDragView];
+ draggingSession.animatesToStartingPositionsOnCancelOrFail =
+ !mDataTransfer || mDataTransfer->MozShowFailAnimation();
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!aTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get flavor list that includes all acceptable flavors (including ones
+ // obtained through conversion)
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // if this drag originated within Mozilla we should just use the cached data
+ // from when the drag started if possible
+ if (mDataItems) {
+ nsCOMPtr<nsITransferable> currentTransferable =
+ do_QueryElementAt(mDataItems, aItemIndex);
+ if (currentTransferable) {
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ nsCOMPtr<nsISupports> dataSupports;
+ rv = currentTransferable->GetTransferData(flavorStr.get(),
+ getter_AddRefs(dataSupports));
+ if (NS_SUCCEEDED(rv)) {
+ aTransferable->SetTransferData(flavorStr.get(), dataSupports);
+ return NS_OK; // maybe try to fill in more types? Is there a point?
+ }
+ }
+ }
+ }
+
+ NSArray* droppedItems = [globalDragPboard pasteboardItems];
+ if (!droppedItems) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t itemCount = [droppedItems count];
+ if (aItemIndex >= itemCount) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSPasteboardItem* item = [droppedItems objectAtIndex:aItemIndex];
+ if (!item) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // now check the actual clipboard for data
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(aTransferable,
+ flavors[i], item);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *_retval = false;
+
+ if (!globalDragPboard) return NS_ERROR_FAILURE;
+
+ nsDependentCString dataFlavor(aDataFlavor);
+
+ // first see if we have data for this in our cached transferable
+ if (mDataItems) {
+ uint32_t dataItemsCount;
+ mDataItems->GetLength(&dataItemsCount);
+ for (unsigned int i = 0; i < dataItemsCount; i++) {
+ nsCOMPtr<nsITransferable> currentTransferable =
+ do_QueryElementAt(mDataItems, i);
+ if (!currentTransferable) continue;
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = currentTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) continue;
+
+ for (uint32_t j = 0; j < flavors.Length(); j++) {
+ if (dataFlavor.Equals(flavors[j])) {
+ *_retval = true;
+ return NS_OK;
+ }
+ }
+ }
+ }
+
+ const NSString* type = nil;
+ bool allowFileURL = false;
+ if (dataFlavor.EqualsLiteral(kFileMime)) {
+ type = [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL];
+ allowFileURL = true;
+ } else if (dataFlavor.EqualsLiteral(kTextMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ } else if (dataFlavor.EqualsLiteral(kHTMLMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
+ } else if (dataFlavor.EqualsLiteral(kURLMime) ||
+ dataFlavor.EqualsLiteral(kURLDataMime)) {
+ type = [UTIHelper stringFromPboardType:kPublicUrlPboardType];
+ } else if (dataFlavor.EqualsLiteral(kURLDescriptionMime)) {
+ type = [UTIHelper stringFromPboardType:kPublicUrlNamePboardType];
+ } else if (dataFlavor.EqualsLiteral(kRTFMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeRTF];
+ } else if (dataFlavor.EqualsLiteral(kCustomTypesMime)) {
+ type = [UTIHelper stringFromPboardType:kMozCustomTypesPboardType];
+ }
+
+ NSString* availableType = [globalDragPboard
+ availableTypeFromArray:[NSArray arrayWithObjects:(id)type, nil]];
+ if (availableType &&
+ nsCocoaUtils::IsValidPasteboardType(availableType, allowFileURL)) {
+ *_retval = true;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::GetNumDropItems(uint32_t* aNumItems) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *aNumItems = 0;
+
+ // first check to see if we have a number of items cached
+ if (mDataItems) {
+ mDataItems->GetLength(aNumItems);
+ return NS_OK;
+ }
+
+ NSArray* droppedItems = [globalDragPboard pasteboardItems];
+ if (droppedItems) {
+ *aNumItems = [droppedItems count];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::UpdateDragImage(nsINode* aImage, int32_t aImageX,
+ int32_t aImageY) {
+ nsBaseDragService::UpdateDragImage(aImage, aImageX, aImageY);
+ mDragImageChanged = true;
+ return NS_OK;
+}
+
+void nsDragService::DragMovedWithView(NSDraggingSession* aSession,
+ NSPoint aPoint) {
+ aPoint.y = nsCocoaUtils::FlippedScreenY(aPoint.y);
+
+ // XXX It feels like we should be using the backing scale factor at aPoint
+ // rather than the initial drag view, but I've seen no ill effects of this.
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+ LayoutDeviceIntPoint devPoint =
+ nsCocoaUtils::CocoaPointsToDevPixels(aPoint, scaleFactor);
+
+ // If the image has changed, call enumerateDraggingItemsWithOptions to get
+ // the item being dragged and update its image.
+ if (mDragImageChanged && mNativeDragView) {
+ mDragImageChanged = false;
+
+ nsPresContext* pc = nullptr;
+ nsCOMPtr<nsIContent> content = do_QueryInterface(mImage);
+ if (content) {
+ pc = content->OwnerDoc()->GetPresContext();
+ }
+
+ if (pc) {
+ void (^changeImageBlock)(NSDraggingItem*, NSInteger, BOOL*) = ^(
+ NSDraggingItem* draggingItem, NSInteger idx, BOOL* stop) {
+ // We never add more than one item right now, but check just in case.
+ if (idx > 0) {
+ return;
+ }
+
+ nsPoint pt = LayoutDevicePixel::ToAppUnits(
+ devPoint, pc->DeviceContext()->AppUnitsPerDevPixel());
+ CSSIntPoint screenPoint =
+ CSSIntPoint(nsPresContext::AppUnitsToIntCSSPixels(pt.x),
+ nsPresContext::AppUnitsToIntCSSPixels(pt.y));
+
+ // Create a new image; if one isn't returned don't change the current
+ // one.
+ LayoutDeviceIntRect newRect;
+ NSImage* image =
+ ConstructDragImage(mSourceNode, Nothing(), screenPoint, &newRect);
+ if (image) {
+ NSRect draggingRect =
+ nsCocoaUtils::GeckoRectToCocoaRectDevPix(newRect, scaleFactor);
+ [draggingItem setDraggingFrame:draggingRect contents:image];
+ }
+ };
+
+ [aSession
+ enumerateDraggingItemsWithOptions:NSDraggingItemEnumerationConcurrent
+ forView:nil
+ classes:[NSArray
+ arrayWithObject:
+ [NSPasteboardItem class]]
+ searchOptions:@{}
+ usingBlock:changeImageBlock];
+ }
+ }
+
+ DragMoved(devPoint.x, devPoint.y);
+}
+
+NS_IMETHODIMP
+nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mNativeDragView) {
+ [mNativeDragView release];
+ mNativeDragView = nil;
+ }
+ if (mNativeDragEvent) {
+ [mNativeDragEvent release];
+ mNativeDragEvent = nil;
+ }
+
+ mUserCancelled = gUserCancelledDrag;
+
+ nsresult rv = nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers);
+ mDataItems = nullptr;
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsFilePicker.h b/widget/cocoa/nsFilePicker.h
new file mode 100644
index 0000000000..a7ea5557c4
--- /dev/null
+++ b/widget/cocoa/nsFilePicker.h
@@ -0,0 +1,74 @@
+/* -*- 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/. */
+
+#ifndef nsFilePicker_h_
+#define nsFilePicker_h_
+
+#include "nsBaseFilePicker.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsCOMArray.h"
+#include "nsTArray.h"
+
+class nsILocalFileMac;
+@class NSArray;
+
+class nsFilePicker : public nsBaseFilePicker {
+ public:
+ nsFilePicker();
+ using nsIFilePicker::ResultCode;
+
+ NS_DECL_ISUPPORTS
+
+ // nsIFilePicker (less what's in nsBaseFilePicker)
+ NS_IMETHOD GetDefaultString(nsAString& aDefaultString) override;
+ NS_IMETHOD SetDefaultString(const nsAString& aDefaultString) override;
+ NS_IMETHOD GetDefaultExtension(nsAString& aDefaultExtension) override;
+ NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override;
+ NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override;
+ NS_IMETHOD SetDefaultExtension(const nsAString& aDefaultExtension) override;
+ NS_IMETHOD GetFile(nsIFile** aFile) override;
+ NS_IMETHOD GetFileURL(nsIURI** aFileURL) override;
+ NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override;
+ NS_IMETHOD AppendFilter(const nsAString& aTitle,
+ const nsAString& aFilter) override;
+
+ /**
+ * Returns the current filter list in the format used by Cocoa's NSSavePanel
+ * and NSOpenPanel.
+ * Returns nil if no filter currently apply.
+ */
+ NSArray* GetFilterList();
+
+ protected:
+ virtual ~nsFilePicker();
+
+ virtual void InitNative(nsIWidget* aParent, const nsAString& aTitle) override;
+ nsresult Show(ResultCode* _retval) override;
+
+ // actual implementations of get/put dialogs using NSOpenPanel & NSSavePanel
+ // aFile is an existing but unspecified file. These functions must specify it.
+ //
+ // will return |returnCancel| or |returnOK| as result.
+ ResultCode GetLocalFiles(bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles);
+ ResultCode GetLocalFolder(nsIFile** outFile);
+ ResultCode PutLocalFile(nsIFile** outFile);
+
+ void SetDialogTitle(const nsString& inTitle, id aDialog);
+ NSString* PanelDefaultDirectory();
+ NSView* GetAccessoryView();
+
+ nsString mTitle;
+ nsCOMArray<nsIFile> mFiles;
+ nsString mDefaultFilename;
+
+ nsTArray<nsString> mFilters;
+ nsTArray<nsString> mTitles;
+
+ int32_t mSelectedTypeIndex;
+};
+
+#endif // nsFilePicker_h_
diff --git a/widget/cocoa/nsFilePicker.mm b/widget/cocoa/nsFilePicker.mm
new file mode 100644
index 0000000000..31c64c5367
--- /dev/null
+++ b/widget/cocoa/nsFilePicker.mm
@@ -0,0 +1,679 @@
+/* -*- 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsFilePicker.h"
+#include "nsCOMPtr.h"
+#include "nsReadableUtils.h"
+#include "nsNetUtil.h"
+#include "nsIFile.h"
+#include "nsILocalFileMac.h"
+#include "nsArrayEnumerator.h"
+#include "nsIStringBundle.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/Preferences.h"
+
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+const float kAccessoryViewPadding = 5;
+const int kSaveTypeControlTag = 1;
+
+static bool gCallSecretHiddenFileAPI = false;
+const char kShowHiddenFilesPref[] = "filepicker.showHiddenFiles";
+
+/**
+ * This class is an observer of NSPopUpButton selection change.
+ */
+@interface NSPopUpButtonObserver : NSObject {
+ NSPopUpButton* mPopUpButton;
+ NSOpenPanel* mOpenPanel;
+ nsFilePicker* mFilePicker;
+}
+- (void)setPopUpButton:(NSPopUpButton*)aPopUpButton;
+- (void)setOpenPanel:(NSOpenPanel*)aOpenPanel;
+- (void)setFilePicker:(nsFilePicker*)aFilePicker;
+- (void)menuChangedItem:(NSNotification*)aSender;
+@end
+
+NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
+
+// We never want to call the secret show hidden files API unless the pref
+// has been set. Once the pref has been set we always need to call it even
+// if it disappears so that we stop showing hidden files if a user deletes
+// the pref. If the secret API was used once and things worked out it should
+// continue working for subsequent calls so the user is at no more risk.
+static void SetShowHiddenFileState(NSSavePanel* panel) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ bool show = false;
+ if (NS_SUCCEEDED(Preferences::GetBool(kShowHiddenFilesPref, &show))) {
+ gCallSecretHiddenFileAPI = true;
+ }
+
+ if (gCallSecretHiddenFileAPI) {
+ // invoke a method to get a Cocoa-internal nav view
+ SEL navViewSelector = @selector(_navView);
+ NSMethodSignature* navViewSignature =
+ [panel methodSignatureForSelector:navViewSelector];
+ if (!navViewSignature) return;
+ NSInvocation* navViewInvocation =
+ [NSInvocation invocationWithMethodSignature:navViewSignature];
+ [navViewInvocation setSelector:navViewSelector];
+ [navViewInvocation setTarget:panel];
+ [navViewInvocation invoke];
+
+ // get the returned nav view
+ id navView = nil;
+ [navViewInvocation getReturnValue:&navView];
+
+ // invoke the secret show hidden file state method on the nav view
+ SEL showHiddenFilesSelector = @selector(setShowsHiddenFiles:);
+ NSMethodSignature* showHiddenFilesSignature =
+ [navView methodSignatureForSelector:showHiddenFilesSelector];
+ if (!showHiddenFilesSignature) return;
+ NSInvocation* showHiddenFilesInvocation =
+ [NSInvocation invocationWithMethodSignature:showHiddenFilesSignature];
+ [showHiddenFilesInvocation setSelector:showHiddenFilesSelector];
+ [showHiddenFilesInvocation setTarget:navView];
+ [showHiddenFilesInvocation setArgument:&show atIndex:2];
+ [showHiddenFilesInvocation invoke];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsFilePicker::nsFilePicker() : mSelectedTypeIndex(0) {}
+
+nsFilePicker::~nsFilePicker() {}
+
+void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
+ mTitle = aTitle;
+}
+
+NSView* nsFilePicker::GetAccessoryView() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSView* accessoryView =
+ [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)] autorelease];
+
+ // Set a label's default value.
+ NSString* label = @"Format:";
+
+ // Try to get the localized string.
+ nsCOMPtr<nsIStringBundleService> sbs =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = sbs->CreateBundle(
+ "chrome://global/locale/filepicker.properties", getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString locaLabel;
+ rv = bundle->GetStringFromName("formatLabel", locaLabel);
+ if (NS_SUCCEEDED(rv)) {
+ label = [NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(locaLabel.get())
+ length:locaLabel.Length()];
+ }
+ }
+
+ // set up label text field
+ NSTextField* textField = [[[NSTextField alloc] init] autorelease];
+ [textField setEditable:NO];
+ [textField setSelectable:NO];
+ [textField setDrawsBackground:NO];
+ [textField setBezeled:NO];
+ [textField setBordered:NO];
+ [textField setFont:[NSFont labelFontOfSize:13.0]];
+ [textField setStringValue:label];
+ [textField setTag:0];
+ [textField sizeToFit];
+
+ // set up popup button
+ NSPopUpButton* popupButton =
+ [[[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)
+ pullsDown:NO] autorelease];
+ uint32_t numMenuItems = mTitles.Length();
+ for (uint32_t i = 0; i < numMenuItems; i++) {
+ const nsString& currentTitle = mTitles[i];
+ NSString* titleString;
+ if (currentTitle.IsEmpty()) {
+ const nsString& currentFilter = mFilters[i];
+ titleString =
+ [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(
+ currentFilter.get())
+ length:currentFilter.Length()];
+ } else {
+ titleString =
+ [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(
+ currentTitle.get())
+ length:currentTitle.Length()];
+ }
+ [popupButton addItemWithTitle:titleString];
+ [titleString release];
+ }
+ if (mSelectedTypeIndex >= 0 && (uint32_t)mSelectedTypeIndex < numMenuItems)
+ [popupButton selectItemAtIndex:mSelectedTypeIndex];
+ [popupButton setTag:kSaveTypeControlTag];
+ [popupButton sizeToFit]; // we have to do sizeToFit to get the height
+ // calculated for us
+ // This is just a default width that works well, doesn't truncate the vast
+ // majority of things that might end up in the menu.
+ [popupButton setFrameSize:NSMakeSize(180, [popupButton frame].size.height)];
+
+ // position everything based on control sizes with kAccessoryViewPadding pix
+ // padding on each side kAccessoryViewPadding pix horizontal padding between
+ // controls
+ float greatestHeight = [textField frame].size.height;
+ if ([popupButton frame].size.height > greatestHeight)
+ greatestHeight = [popupButton frame].size.height;
+ float totalViewHeight = greatestHeight + kAccessoryViewPadding * 2;
+ float totalViewWidth = [textField frame].size.width +
+ [popupButton frame].size.width +
+ kAccessoryViewPadding * 3;
+ [accessoryView setFrameSize:NSMakeSize(totalViewWidth, totalViewHeight)];
+
+ float textFieldOriginY =
+ ((greatestHeight - [textField frame].size.height) / 2 + 1) +
+ kAccessoryViewPadding;
+ [textField
+ setFrameOrigin:NSMakePoint(kAccessoryViewPadding, textFieldOriginY)];
+
+ float popupOriginX = [textField frame].size.width + kAccessoryViewPadding * 2;
+ float popupOriginY =
+ ((greatestHeight - [popupButton frame].size.height) / 2) +
+ kAccessoryViewPadding;
+ [popupButton setFrameOrigin:NSMakePoint(popupOriginX, popupOriginY)];
+
+ [accessoryView addSubview:textField];
+ [accessoryView addSubview:popupButton];
+ return accessoryView;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Display the file dialog
+nsresult nsFilePicker::Show(ResultCode* retval) {
+ NS_ENSURE_ARG_POINTER(retval);
+
+ *retval = returnCancel;
+
+ ResultCode userClicksOK = returnCancel;
+
+ mFiles.Clear();
+ nsCOMPtr<nsIFile> theFile;
+
+ // Note that GetLocalFolder shares a lot of code with GetLocalFiles.
+ // Could combine the functions and just pass the mode in.
+ switch (mMode) {
+ case modeOpen:
+ userClicksOK = GetLocalFiles(false, mFiles);
+ break;
+
+ case modeOpenMultiple:
+ userClicksOK = GetLocalFiles(true, mFiles);
+ break;
+
+ case modeSave:
+ userClicksOK = PutLocalFile(getter_AddRefs(theFile));
+ break;
+
+ case modeGetFolder:
+ userClicksOK = GetLocalFolder(getter_AddRefs(theFile));
+ break;
+
+ default:
+ NS_ERROR("Unknown file picker mode");
+ break;
+ }
+
+ if (theFile) mFiles.AppendObject(theFile);
+
+ *retval = userClicksOK;
+ return NS_OK;
+}
+
+static void UpdatePanelFileTypes(NSOpenPanel* aPanel, NSArray* aFilters) {
+ // If we show all file types, also "expose" bundles' contents.
+ [aPanel setTreatsFilePackagesAsDirectories:!aFilters];
+
+ [aPanel setAllowedFileTypes:aFilters];
+}
+
+@implementation NSPopUpButtonObserver
+- (void)setPopUpButton:(NSPopUpButton*)aPopUpButton {
+ mPopUpButton = aPopUpButton;
+}
+
+- (void)setOpenPanel:(NSOpenPanel*)aOpenPanel {
+ mOpenPanel = aOpenPanel;
+}
+
+- (void)setFilePicker:(nsFilePicker*)aFilePicker {
+ mFilePicker = aFilePicker;
+}
+
+- (void)menuChangedItem:(NSNotification*)aSender {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ int32_t selectedItem = [mPopUpButton indexOfSelectedItem];
+ if (selectedItem < 0) {
+ return;
+ }
+
+ mFilePicker->SetFilterIndex(selectedItem);
+ UpdatePanelFileTypes(mOpenPanel, mFilePicker->GetFilterList());
+
+ NS_OBJC_END_TRY_BLOCK_RETURN();
+}
+@end
+
+// Use OpenPanel to do a GetFile. Returns |returnOK| if the user presses OK in
+// the dialog.
+nsIFilePicker::ResultCode nsFilePicker::GetLocalFiles(
+ bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ ResultCode retVal = nsIFilePicker::returnCancel;
+ NSOpenPanel* thePanel = [NSOpenPanel openPanel];
+
+ SetShowHiddenFileState(thePanel);
+
+ // Set the options for how the get file dialog will appear
+ SetDialogTitle(mTitle, thePanel);
+ [thePanel setAllowsMultipleSelection:inAllowMultiple];
+ [thePanel setCanSelectHiddenExtension:YES];
+ [thePanel setCanChooseDirectories:NO];
+ [thePanel setCanChooseFiles:YES];
+ [thePanel setResolvesAliases:YES];
+
+ // Get filters
+ // filters may be null, if we should allow all file types.
+ NSArray* filters = GetFilterList();
+
+ // set up default directory
+ NSString* theDir = PanelDefaultDirectory();
+
+ // if this is the "Choose application..." dialog, and no other start
+ // dir has been set, then use the Applications folder.
+ if (!theDir) {
+ if (filters && [filters count] == 1 &&
+ [(NSString*)[filters objectAtIndex:0] isEqualToString:@"app"])
+ theDir = @"/Applications/";
+ else
+ theDir = @"";
+ }
+
+ if (theDir) {
+ [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
+ }
+
+ int result;
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ if (mFilters.Length() > 1) {
+ // [NSURL initWithString:] (below) throws an exception if URLString is nil.
+
+ NSPopUpButtonObserver* observer = [[NSPopUpButtonObserver alloc] init];
+
+ NSView* accessoryView = GetAccessoryView();
+ [thePanel setAccessoryView:accessoryView];
+
+ [observer setPopUpButton:[accessoryView viewWithTag:kSaveTypeControlTag]];
+ [observer setOpenPanel:thePanel];
+ [observer setFilePicker:this];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:observer
+ selector:@selector(menuChangedItem:)
+ name:NSMenuWillSendActionNotification
+ object:nil];
+
+ UpdatePanelFileTypes(thePanel, filters);
+ result = [thePanel runModal];
+
+ [[NSNotificationCenter defaultCenter] removeObserver:observer];
+ [observer release];
+ } else {
+ // If we show all file types, also "expose" bundles' contents.
+ if (!filters) {
+ [thePanel setTreatsFilePackagesAsDirectories:YES];
+ }
+ [thePanel setAllowedFileTypes:filters];
+ result = [thePanel runModal];
+ }
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ if (result == NSModalResponseCancel) return retVal;
+
+ // Converts data from a NSArray of NSURL to the returned format.
+ // We should be careful to not call [thePanel URLs] more than once given that
+ // it creates a new array each time.
+ // We are using Fast Enumeration, thus the NSURL array is created once then
+ // iterated.
+ for (NSURL* url in [thePanel URLs]) {
+ if (!url) {
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
+ if (macLocalFile &&
+ NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)url))) {
+ outFiles.AppendObject(localFile);
+ }
+ }
+
+ if (outFiles.Count() > 0) retVal = returnOK;
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK);
+}
+
+// Use OpenPanel to do a GetFolder. Returns |returnOK| if the user presses OK in
+// the dialog.
+nsIFilePicker::ResultCode nsFilePicker::GetLocalFolder(nsIFile** outFile) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ NS_ASSERTION(
+ outFile,
+ "this protected member function expects a null initialized out pointer");
+
+ ResultCode retVal = nsIFilePicker::returnCancel;
+ NSOpenPanel* thePanel = [NSOpenPanel openPanel];
+
+ SetShowHiddenFileState(thePanel);
+
+ // Set the options for how the get file dialog will appear
+ SetDialogTitle(mTitle, thePanel);
+ [thePanel setAllowsMultipleSelection:NO];
+ [thePanel setCanSelectHiddenExtension:YES];
+ [thePanel setCanChooseDirectories:YES];
+ [thePanel setCanChooseFiles:NO];
+ [thePanel setResolvesAliases:YES];
+ [thePanel setCanCreateDirectories:YES];
+
+ // packages != folders
+ [thePanel setTreatsFilePackagesAsDirectories:NO];
+
+ // set up default directory
+ NSString* theDir = PanelDefaultDirectory();
+ if (theDir) {
+ [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
+ }
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ int result = [thePanel runModal];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ if (result == NSModalResponseCancel) return retVal;
+
+ // get the path for the folder (we allow just 1, so that's all we get)
+ NSURL* theURL = [[thePanel URLs] objectAtIndex:0];
+ if (theURL) {
+ nsCOMPtr<nsIFile> localFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
+ if (macLocalFile &&
+ NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)theURL))) {
+ *outFile = localFile;
+ NS_ADDREF(*outFile);
+ retVal = returnOK;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK);
+}
+
+// Returns |returnOK| if the user presses OK in the dialog.
+nsIFilePicker::ResultCode nsFilePicker::PutLocalFile(nsIFile** outFile) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ NS_ASSERTION(
+ outFile,
+ "this protected member function expects a null initialized out pointer");
+
+ ResultCode retVal = nsIFilePicker::returnCancel;
+ NSSavePanel* thePanel = [NSSavePanel savePanel];
+
+ SetShowHiddenFileState(thePanel);
+
+ SetDialogTitle(mTitle, thePanel);
+
+ // set up accessory view for file format options
+ NSView* accessoryView = GetAccessoryView();
+ [thePanel setAccessoryView:accessoryView];
+
+ // set up default file name
+ NSString* defaultFilename =
+ [NSString stringWithCharacters:(const unichar*)mDefaultFilename.get()
+ length:mDefaultFilename.Length()];
+
+ // Set up the allowed type. This prevents the extension from being selected.
+ NSString* extension = defaultFilename.pathExtension;
+ if (extension.length != 0) {
+ thePanel.allowedFileTypes = @[ extension ];
+ }
+ // Allow users to change the extension.
+ thePanel.allowsOtherFileTypes = YES;
+
+ // If extensions are hidden and we’re saving a file with multiple extensions,
+ // only the last extension will be hidden in the panel (".tar.gz" will become
+ // ".tar"). If the remaining extension is known, the OS will think that we're
+ // trying to add a non-default extension. To avoid the confusion, we ensure
+ // that all extensions are shown in the panel if the remaining extension is
+ // known by the OS.
+ NSString* fileName =
+ [[defaultFilename lastPathComponent] stringByDeletingPathExtension];
+ NSString* otherExtension = fileName.pathExtension;
+ if (otherExtension.length != 0) {
+ // There's another extension here. Get the UTI.
+ CFStringRef type = UTTypeCreatePreferredIdentifierForTag(
+ kUTTagClassFilenameExtension, (CFStringRef)otherExtension, NULL);
+ if (type) {
+ if (!CFStringHasPrefix(type, CFSTR("dyn."))) {
+ // We have a UTI, otherwise the type would have a "dyn." prefix. Ensure
+ // extensions are shown in the panel.
+ [thePanel setExtensionHidden:NO];
+ }
+ CFRelease(type);
+ }
+ }
+
+ // set up default directory
+ NSString* theDir = PanelDefaultDirectory();
+ if (theDir) {
+ [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
+ }
+
+ // load the panel
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ [thePanel setNameFieldStringValue:defaultFilename];
+ int result = [thePanel runModal];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+ if (result == NSModalResponseCancel) return retVal;
+
+ // get the save type
+ NSPopUpButton* popupButton = [accessoryView viewWithTag:kSaveTypeControlTag];
+ if (popupButton) {
+ mSelectedTypeIndex = [popupButton indexOfSelectedItem];
+ }
+
+ NSURL* fileURL = [thePanel URL];
+ if (fileURL) {
+ nsCOMPtr<nsIFile> localFile;
+ NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
+ nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
+ if (macLocalFile &&
+ NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)fileURL))) {
+ *outFile = localFile;
+ NS_ADDREF(*outFile);
+ // We tell if we are replacing or not by just looking to see if the file
+ // exists. The user could not have hit OK and not meant to replace the
+ // file.
+ if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]])
+ retVal = returnReplace;
+ else
+ retVal = returnOK;
+ }
+ }
+
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnCancel);
+}
+
+NSArray* nsFilePicker::GetFilterList() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mFilters.Length()) {
+ return nil;
+ }
+
+ if (mFilters.Length() <= (uint32_t)mSelectedTypeIndex) {
+ NS_WARNING("An out of range index has been selected. Using the first index "
+ "instead.");
+ mSelectedTypeIndex = 0;
+ }
+
+ const nsString& filterWide = mFilters[mSelectedTypeIndex];
+ if (!filterWide.Length()) {
+ return nil;
+ }
+
+ if (filterWide.Equals(u"*"_ns)) {
+ return nil;
+ }
+
+ // The extensions in filterWide are in the format "*.ext" but are expected
+ // in the format "ext" by NSOpenPanel. So we need to filter some characters.
+ NSMutableString* filterString = [[[NSMutableString alloc]
+ initWithString:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(
+ filterWide.get())
+ length:filterWide.Length()]]
+ autorelease];
+ NSCharacterSet* set =
+ [NSCharacterSet characterSetWithCharactersInString:@". *"];
+ NSRange range = [filterString rangeOfCharacterFromSet:set];
+ while (range.length) {
+ [filterString replaceCharactersInRange:range withString:@""];
+ range = [filterString rangeOfCharacterFromSet:set];
+ }
+
+ return [[[NSArray alloc]
+ initWithArray:[filterString componentsSeparatedByString:@";"]]
+ autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// Sets the dialog title to whatever it should be. If it fails, eh,
+// the OS will provide a sensible default.
+void nsFilePicker::SetDialogTitle(const nsString& inTitle, id aPanel) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [aPanel setTitle:[NSString stringWithCharacters:(const unichar*)inTitle.get()
+ length:inTitle.Length()]];
+
+ if (!mOkButtonLabel.IsEmpty()) {
+ [aPanel
+ setPrompt:[NSString
+ stringWithCharacters:(const unichar*)mOkButtonLabel.get()
+ length:mOkButtonLabel.Length()]];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Converts path from an nsIFile into a NSString path
+// If it fails, returns an empty string.
+NSString* nsFilePicker::PanelDefaultDirectory() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* directory = nil;
+ if (mDisplayDirectory) {
+ nsAutoString pathStr;
+ mDisplayDirectory->GetPath(pathStr);
+ directory = [[[NSString alloc]
+ initWithCharacters:reinterpret_cast<const unichar*>(pathStr.get())
+ length:pathStr.Length()] autorelease];
+ }
+ return directory;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NS_IMETHODIMP nsFilePicker::GetFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ *aFile = nullptr;
+
+ // just return the first file
+ if (mFiles.Count() > 0) {
+ *aFile = mFiles.ObjectAt(0);
+ NS_IF_ADDREF(*aFile);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFilePicker::GetFileURL(nsIURI** aFileURL) {
+ NS_ENSURE_ARG_POINTER(aFileURL);
+ *aFileURL = nullptr;
+
+ if (mFiles.Count() == 0) return NS_OK;
+
+ return NS_NewFileURI(aFileURL, mFiles.ObjectAt(0));
+}
+
+NS_IMETHODIMP nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
+ return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
+}
+
+NS_IMETHODIMP nsFilePicker::SetDefaultString(const nsAString& aString) {
+ mDefaultFilename = aString;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFilePicker::GetDefaultString(nsAString& aString) {
+ return NS_ERROR_FAILURE;
+}
+
+// The default extension to use for files
+NS_IMETHODIMP nsFilePicker::GetDefaultExtension(nsAString& aExtension) {
+ aExtension.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFilePicker::SetDefaultExtension(const nsAString& aExtension) {
+ return NS_OK;
+}
+
+// Append an entry to the filters array
+NS_IMETHODIMP
+nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
+ // "..apps" has to be translated with native executable extensions.
+ if (aFilter.EqualsLiteral("..apps")) {
+ mFilters.AppendElement(u"*.app"_ns);
+ } else {
+ mFilters.AppendElement(aFilter);
+ }
+ mTitles.AppendElement(aTitle);
+
+ return NS_OK;
+}
+
+// Get the filter index - do we still need this?
+NS_IMETHODIMP nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
+ *aFilterIndex = mSelectedTypeIndex;
+ return NS_OK;
+}
+
+// Set the filter index - do we still need this?
+NS_IMETHODIMP nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
+ mSelectedTypeIndex = aFilterIndex;
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsLookAndFeel.h b/widget/cocoa/nsLookAndFeel.h
new file mode 100644
index 0000000000..adce685a4e
--- /dev/null
+++ b/widget/cocoa/nsLookAndFeel.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsLookAndFeel_h_
+#define nsLookAndFeel_h_
+#include "nsXPLookAndFeel.h"
+
+class nsLookAndFeel final : public nsXPLookAndFeel {
+ public:
+ nsLookAndFeel();
+ virtual ~nsLookAndFeel();
+
+ void NativeInit() final;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aColor) override;
+ nsresult NativeGetInt(IntID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID, float& aResult) override;
+ bool NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) override;
+
+ virtual char16_t GetPasswordCharacterImpl() override {
+ // unicode value for the bullet character, used for password textfields.
+ return 0x2022;
+ }
+
+ void RecordLookAndFeelSpecificTelemetry() override {
+ RecordAccessibilityTelemetry();
+ }
+
+ // Having a separate, static method allows us to rely on the same
+ // chunk of telemetry logging code at initialization and when we
+ // recieve an event that changes the value of our telemetry probe.
+ static void RecordAccessibilityTelemetry();
+
+ protected:
+ static bool SystemWantsDarkTheme();
+ static bool IsSystemOrientationRTL();
+ static nscolor ProcessSelectionBackground(nscolor aColor,
+ ColorScheme aScheme);
+};
+
+#endif // nsLookAndFeel_h_
diff --git a/widget/cocoa/nsLookAndFeel.mm b/widget/cocoa/nsLookAndFeel.mm
new file mode 100644
index 0000000000..73fd6b689c
--- /dev/null
+++ b/widget/cocoa/nsLookAndFeel.mm
@@ -0,0 +1,679 @@
+/* -*- Mode: C++; tab-width: 4; 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 "AppearanceOverride.h"
+#include "mozilla/widget/ThemeChangeKind.h"
+#include "nsLookAndFeel.h"
+#include "nsCocoaFeatures.h"
+#include "nsNativeThemeColors.h"
+#include "nsStyleConsts.h"
+#include "nsIContent.h"
+#include "gfxFont.h"
+#include "gfxFontConstants.h"
+#include "gfxPlatformMac.h"
+#include "nsCSSColorUtils.h"
+#include "mozilla/FontPropertyTypes.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/widget/WidgetMessageUtils.h"
+
+#import <Cocoa/Cocoa.h>
+#import <AppKit/NSColor.h>
+
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+@interface MOZLookAndFeelDynamicChangeObserver : NSObject
++ (void)startObserving;
+@end
+
+nsLookAndFeel::nsLookAndFeel() = default;
+
+nsLookAndFeel::~nsLookAndFeel() = default;
+
+void nsLookAndFeel::NativeInit() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK
+
+ [MOZLookAndFeelDynamicChangeObserver startObserving];
+ RecordTelemetry();
+
+ NS_OBJC_END_TRY_ABORT_BLOCK
+}
+
+static nscolor GetColorFromNSColor(NSColor* aColor) {
+ NSColor* deviceColor =
+ [aColor colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
+ return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
+ (unsigned int)(deviceColor.greenComponent * 255.0),
+ (unsigned int)(deviceColor.blueComponent * 255.0),
+ (unsigned int)(deviceColor.alphaComponent * 255.0));
+}
+
+static nscolor GetColorFromNSColorWithCustomAlpha(NSColor* aColor,
+ float alpha) {
+ NSColor* deviceColor =
+ [aColor colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
+ return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
+ (unsigned int)(deviceColor.greenComponent * 255.0),
+ (unsigned int)(deviceColor.blueComponent * 255.0),
+ (unsigned int)(alpha * 255.0));
+}
+
+// Turns an opaque selection color into a partially transparent selection color,
+// which usually leads to better contrast with the text color and which should
+// look more visually appealing in most contexts.
+// The idea is that the text and its regular, non-selected background are
+// usually chosen in such a way that they contrast well. Making the selection
+// color partially transparent causes the selection color to mix with the text's
+// regular background, so the end result will often have better contrast with
+// the text than an arbitrary opaque selection color.
+// The motivating example for this is the light selection color on dark web
+// pages: White text on a light blue selection color has very bad contrast,
+// whereas white text on dark blue (which what you get if you mix
+// partially-transparent light blue with the black textbox background) has much
+// better contrast.
+nscolor nsLookAndFeel::ProcessSelectionBackground(nscolor aColor,
+ ColorScheme aScheme) {
+ if (aScheme == ColorScheme::Dark) {
+ // When we use a dark selection color, we do not change alpha because we do
+ // not use dark selection in content. The dark system color is appropriate
+ // for Firefox UI without needing to adjust its alpha.
+ return aColor;
+ }
+ uint16_t hue, sat, value;
+ uint8_t alpha;
+ nscolor resultColor = aColor;
+ NS_RGB2HSV(resultColor, hue, sat, value, alpha);
+ int factor = 2;
+ alpha = alpha / factor;
+ if (sat > 0) {
+ // The color is not a shade of grey, restore the saturation taken away by
+ // the transparency.
+ sat = mozilla::clamped(sat * factor, 0, 255);
+ } else {
+ // The color is a shade of grey, find the value that looks equivalent
+ // on a white background with the given opacity.
+ value = mozilla::clamped(255 - (255 - value) * factor, 0, 255);
+ }
+ NS_HSV2RGB(resultColor, hue, sat, value, alpha);
+ return resultColor;
+}
+
+nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
+ nscolor& aColor) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK
+
+ NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
+
+ nscolor color = 0;
+ switch (aID) {
+ case ColorID::Infobackground:
+ color = aScheme == ColorScheme::Light
+ ? NS_RGB(0xdd, 0xdd, 0xdd)
+ : GetColorFromNSColor(NSColor.windowBackgroundColor);
+ break;
+ case ColorID::Highlight:
+ color = ProcessSelectionBackground(
+ GetColorFromNSColor(NSColor.selectedTextBackgroundColor), aScheme);
+ break;
+ // This is used to gray out the selection when it's not focused. Used with
+ // nsISelectionController::SELECTION_DISABLED.
+ case ColorID::TextSelectDisabledBackground:
+ color = ProcessSelectionBackground(
+ GetColorFromNSColor(NSColor.secondarySelectedControlColor), aScheme);
+ break;
+ case ColorID::MozMenuhoverdisabled:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::Accentcolor:
+ color = GetColorFromNSColor(NSColor.controlAccentColor);
+ break;
+ case ColorID::MozMenuhover:
+ case ColorID::Selecteditem:
+ color = GetColorFromNSColor(NSColor.selectedContentBackgroundColor);
+ break;
+ case ColorID::Accentcolortext:
+ case ColorID::MozMenuhovertext:
+ case ColorID::Selecteditemtext:
+ color = GetColorFromNSColor(NSColor.selectedMenuItemTextColor);
+ break;
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ color = NS_TRANSPARENT;
+ break;
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ case ColorID::Highlighttext:
+ color = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ color = NS_40PERCENT_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ color = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+
+ //
+ // css2 system colors http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
+ //
+ // It's really hard to effectively map these to the Appearance Manager
+ // properly, since they are modeled word for word after the win32 system
+ // colors and don't have any real counterparts in the Mac world. I'm sure
+ // we'll be tweaking these for years to come.
+ //
+ // Thanks to mpt26@student.canterbury.ac.nz for the hardcoded values that
+ // form the defaults
+ // if querying the Appearance Manager fails ;)
+ //
+ case ColorID::MozMacDefaultbuttontext:
+ color = NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::MozSidebar:
+ color = aScheme == ColorScheme::Light ? NS_RGB(0xf6, 0xf6, 0xf6)
+ : NS_RGB(0x2d, 0x2d, 0x2d);
+ break;
+ case ColorID::MozSidebarborder:
+ // hsla(240, 5%, 5%, .1)
+ color = NS_RGBA(12, 12, 13, 26);
+ break;
+ case ColorID::MozButtonactivetext:
+ // Pre-macOS 12, pressed buttons were filled with the highlight color and
+ // the text was white. Starting with macOS 12, pressed (non-default)
+ // buttons are filled with medium gray and the text color is the same as
+ // in the non-pressed state.
+ color = nsCocoaFeatures::OnMontereyOrLater()
+ ? GetColorFromNSColor(NSColor.controlTextColor)
+ : NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::Windowtext:
+ color = GetColorFromNSColor(NSColor.windowFrameTextColor);
+ break;
+ case ColorID::Appworkspace:
+ color = NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::Background:
+ color = NS_RGB(0x63, 0x63, 0xCE);
+ break;
+ case ColorID::Buttonface:
+ case ColorID::MozButtonhoverface:
+ case ColorID::MozButtonactiveface:
+ case ColorID::MozButtondisabledface:
+ case ColorID::MozColheader:
+ case ColorID::MozColheaderhover:
+ case ColorID::MozColheaderactive:
+ color = GetColorFromNSColor(NSColor.controlColor);
+ if (!NS_GET_A(color)) {
+ color = GetColorFromNSColor(NSColor.controlBackgroundColor);
+ }
+ break;
+ case ColorID::Buttonhighlight:
+ color = GetColorFromNSColor(NSColor.selectedControlColor);
+ break;
+ case ColorID::Scrollbar:
+ color = GetColorFromNSColor(NSColor.scrollBarColor);
+ break;
+ case ColorID::Threedhighlight:
+ color = GetColorFromNSColor(NSColor.highlightColor);
+ break;
+ case ColorID::Buttonshadow:
+ case ColorID::Threeddarkshadow:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
+ : NS_RGB(0xDC, 0xDC, 0xDC);
+ break;
+ case ColorID::Threedshadow:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
+ : NS_RGB(0xE0, 0xE0, 0xE0);
+ break;
+ case ColorID::Threedface:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
+ : NS_RGB(0xF0, 0xF0, 0xF0);
+ break;
+ case ColorID::Threedlightshadow:
+ case ColorID::Buttonborder:
+ case ColorID::MozDisabledfield:
+ color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
+ : NS_RGB(0xDA, 0xDA, 0xDA);
+ break;
+ case ColorID::Menu:
+ // Hand-picked from Sonoma because there doesn't seem to be any
+ // appropriate menu system color.
+ color = aScheme == ColorScheme::Dark ? NS_RGB(0x36, 0x36, 0x39)
+ : NS_RGB(0xeb, 0xeb, 0xeb);
+ break;
+ case ColorID::Windowframe:
+ color = GetColorFromNSColor(NSColor.windowFrameColor);
+ break;
+ case ColorID::Window: {
+ color = GetColorFromNSColor(NSColor.windowBackgroundColor);
+ break;
+ }
+ case ColorID::Field:
+ case ColorID::MozCombobox:
+ case ColorID::MozDialog:
+ color = GetColorFromNSColor(NSColor.controlBackgroundColor);
+ break;
+ case ColorID::Fieldtext:
+ case ColorID::MozComboboxtext:
+ case ColorID::Buttontext:
+ case ColorID::MozButtonhovertext:
+ case ColorID::Menutext:
+ case ColorID::Infotext:
+ case ColorID::MozDialogtext:
+ case ColorID::MozCellhighlighttext:
+ case ColorID::MozColheadertext:
+ case ColorID::MozColheaderhovertext:
+ case ColorID::MozColheaderactivetext:
+ case ColorID::MozSidebartext:
+ color = GetColorFromNSColor(NSColor.controlTextColor);
+ break;
+ case ColorID::MozMacFocusring:
+ color = GetColorFromNSColorWithCustomAlpha(
+ NSColor.keyboardFocusIndicatorColor, 0.48);
+ break;
+ case ColorID::MozMacDisabledtoolbartext:
+ case ColorID::Graytext:
+ color = GetColorFromNSColor(NSColor.disabledControlTextColor);
+ break;
+ case ColorID::MozCellhighlight:
+ // For inactive list selection
+ color = GetColorFromNSColor(NSColor.secondarySelectedControlColor);
+ break;
+ case ColorID::MozEventreerow:
+ // Background color of even list rows.
+ color =
+ GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[0]);
+ break;
+ case ColorID::MozOddtreerow:
+ // Background color of odd list rows.
+ color =
+ GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[1]);
+ break;
+ case ColorID::MozNativehyperlinktext:
+ color = GetColorFromNSColor(NSColor.linkColor);
+ break;
+ case ColorID::MozNativevisitedhyperlinktext:
+ color = GetColorFromNSColor(NSColor.systemPurpleColor);
+ break;
+ case ColorID::MozHeaderbartext:
+ case ColorID::MozHeaderbarinactivetext:
+ case ColorID::Inactivecaptiontext:
+ case ColorID::Captiontext:
+ aColor = GetColorFromNSColor(NSColor.textColor);
+ return NS_OK;
+ case ColorID::MozHeaderbar:
+ case ColorID::MozHeaderbarinactive:
+ case ColorID::Inactivecaption:
+ case ColorID::Activecaption:
+ // This has better contrast than the stand-in colors.
+ aColor = GetColorFromNSColor(NSColor.windowBackgroundColor);
+ return NS_OK;
+ case ColorID::Marktext:
+ case ColorID::Mark:
+ case ColorID::SpellCheckerUnderline:
+ case ColorID::Activeborder:
+ case ColorID::Inactiveborder:
+ aColor = GetStandinForNativeColor(aID, aScheme);
+ return NS_OK;
+ default:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ return NS_ERROR_FAILURE;
+ }
+
+ aColor = color;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK
+}
+
+nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ break;
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 3;
+ break;
+ case IntID::CaretBlinkTime:
+ aResult = 567;
+ break;
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+ case IntID::SelectTextfieldsOnKeyFocus:
+ // Select textfield content when focused by kbd
+ // used by EventStateManager::sTextfieldSelectModel
+ aResult = 1;
+ break;
+ case IntID::SubmenuDelay:
+ aResult = 200;
+ break;
+ case IntID::TooltipDelay:
+ aResult = 500;
+ break;
+ case IntID::MenusCanOverlapOSBar:
+ // xul popups are not allowed to overlap the menubar.
+ aResult = 0;
+ break;
+ case IntID::SkipNavigatingDisabledMenuItem:
+ aResult = 1;
+ break;
+ case IntID::DragThresholdX:
+ case IntID::DragThresholdY:
+ aResult = 4;
+ break;
+ case IntID::ScrollArrowStyle:
+ aResult = eScrollArrow_None;
+ break;
+ case IntID::UseOverlayScrollbars:
+ case IntID::AllowOverlayScrollbarsOverlap:
+ aResult = NSScroller.preferredScrollerStyle == NSScrollerStyleOverlay;
+ break;
+ case IntID::ScrollbarDisplayOnMouseMove:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarFadeBeginDelay:
+ aResult = 450;
+ break;
+ case IntID::ScrollbarFadeDuration:
+ aResult = 200;
+ break;
+ case IntID::TreeOpenDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeCloseDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeLazyScrollDelay:
+ aResult = 150;
+ break;
+ case IntID::TreeScrollDelay:
+ aResult = 100;
+ break;
+ case IntID::TreeScrollLinesMax:
+ aResult = 3;
+ break;
+ case IntID::MacBigSurTheme:
+ aResult = nsCocoaFeatures::OnBigSurOrLater();
+ break;
+ case IntID::MacRTL:
+ aResult = IsSystemOrientationRTL();
+ break;
+ case IntID::AlertNotificationOrigin:
+ aResult = NS_ALERT_TOP;
+ break;
+ case IntID::TabFocusModel:
+ aResult = [NSApp isFullKeyboardAccessEnabled]
+ ? nsIContent::eTabFocus_any
+ : nsIContent::eTabFocus_textControlsMask;
+ break;
+ case IntID::ScrollToClick: {
+ aResult = [[NSUserDefaults standardUserDefaults]
+ boolForKey:@"AppleScrollerPagingBehavior"];
+ } break;
+ case IntID::ChosenMenuItemsShouldBlink:
+ aResult = 1;
+ break;
+ case IntID::IMERawInputUnderlineStyle:
+ case IntID::IMEConvertedTextUnderlineStyle:
+ case IntID::IMESelectedRawTextUnderlineStyle:
+ case IntID::IMESelectedConvertedTextUnderline:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
+ break;
+ case IntID::ScrollbarButtonAutoRepeatBehavior:
+ aResult = 0;
+ break;
+ case IntID::SwipeAnimationEnabled:
+ aResult = NSEvent.isSwipeTrackingFromScrollEventsEnabled;
+ break;
+ case IntID::ContextMenuOffsetVertical:
+ aResult = -6;
+ break;
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 1;
+ break;
+ case IntID::SystemUsesDarkTheme:
+ aResult = SystemWantsDarkTheme();
+ break;
+ case IntID::PrefersReducedMotion:
+ aResult =
+ NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
+ break;
+ case IntID::PrefersReducedTransparency:
+ aResult = NSWorkspace.sharedWorkspace
+ .accessibilityDisplayShouldReduceTransparency;
+ break;
+ case IntID::InvertedColors:
+ aResult =
+ NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
+ break;
+ case IntID::UseAccessibilityTheme:
+ aResult = NSWorkspace.sharedWorkspace
+ .accessibilityDisplayShouldIncreaseContrast;
+ break;
+ case IntID::VideoDynamicRange: {
+ // If the platform says it supports HDR, then we claim to support
+ // video-dynamic-range.
+ gfxPlatform* platform = gfxPlatform::GetPlatform();
+ MOZ_ASSERT(platform);
+ aResult = platform->SupportsHDR();
+ break;
+ }
+ case IntID::PanelAnimations:
+ aResult = 1;
+ break;
+ default:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ }
+ return res;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 2.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 2.0f;
+ break;
+ case FloatID::CursorScale: {
+ id uaDefaults = [[NSUserDefaults alloc]
+ initWithSuiteName:@"com.apple.universalaccess"];
+ float f = [uaDefaults floatForKey:@"mouseDriverCursorSize"];
+ [uaDefaults release];
+ aResult = f > 0.0 ? f : 1.0; // default to 1.0 if value not available
+ break;
+ }
+ default:
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ }
+
+ return res;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+bool nsLookAndFeel::SystemWantsDarkTheme() {
+ // This returns true if the macOS system appearance is set to dark mode, false
+ // otherwise.
+ NSAppearanceName aquaOrDarkAqua =
+ [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
+ NSAppearanceNameAqua, NSAppearanceNameDarkAqua
+ ]];
+ return [aquaOrDarkAqua isEqualToString:NSAppearanceNameDarkAqua];
+}
+
+/*static*/
+bool nsLookAndFeel::IsSystemOrientationRTL() {
+ NSWindow* window =
+ [[NSWindow alloc] initWithContentRect:NSZeroRect
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ auto direction = window.windowTitlebarLayoutDirection;
+ [window release];
+ return direction == NSUserInterfaceLayoutDirectionRightToLeft;
+}
+
+bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoCString name;
+ gfxPlatformMac::LookupSystemFont(aID, name, aFontStyle);
+ aFontName.Append(NS_ConvertUTF8toUTF16(name));
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
+
+void nsLookAndFeel::RecordAccessibilityTelemetry() {
+ if ([[NSWorkspace sharedWorkspace]
+ respondsToSelector:@selector
+ (accessibilityDisplayShouldInvertColors)]) {
+ bool val =
+ [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
+ Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INVERT_COLORS, val);
+ }
+}
+
+@implementation MOZLookAndFeelDynamicChangeObserver
+
++ (void)startObserving {
+ static MOZLookAndFeelDynamicChangeObserver* gInstance = nil;
+ if (!gInstance) {
+ gInstance = [[MOZLookAndFeelDynamicChangeObserver alloc] init]; // leaked
+ }
+}
+
+- (instancetype)init {
+ self = [super init];
+
+ [NSNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(colorsChanged)
+ name:NSControlTintDidChangeNotification
+ object:nil];
+ [NSNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(colorsChanged)
+ name:NSSystemColorsDidChangeNotification
+ object:nil];
+
+ [NSWorkspace.sharedWorkspace.notificationCenter
+ addObserver:self
+ selector:@selector(mediaQueriesChanged)
+ name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
+ object:nil];
+
+ [NSNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(scrollbarsChanged)
+ name:NSPreferredScrollerStyleDidChangeNotification
+ object:nil];
+ [NSDistributedNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(scrollbarsChanged)
+ name:@"AppleAquaScrollBarVariantChanged"
+ object:nil
+ suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
+ [NSDistributedNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(cachedValuesChanged)
+ name:@"AppleNoRedisplayAppearancePreferenceChanged"
+ object:nil
+ suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
+ [NSDistributedNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(cachedValuesChanged)
+ name:@"com.apple.KeyboardUIModeDidChange"
+ object:nil
+ suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
+
+ [MOZGlobalAppearance.sharedInstance addObserver:self
+ forKeyPath:@"effectiveAppearance"
+ options:0
+ context:nil];
+ [NSApp addObserver:self
+ forKeyPath:@"effectiveAppearance"
+ options:0
+ context:nil];
+
+ return self;
+}
+
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey, id>*)change
+ context:(void*)context {
+ if ([keyPath isEqualToString:@"effectiveAppearance"]) {
+ [self entireThemeChanged];
+ } else {
+ [super observeValueForKeyPath:keyPath
+ ofObject:object
+ change:change
+ context:context];
+ }
+}
+
+- (void)entireThemeChanged {
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
+}
+
+- (void)scrollbarsChanged {
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
+}
+
+- (void)mediaQueriesChanged {
+ // Changing`Invert Colors` sends
+ // AccessibilityDisplayOptionsDidChangeNotifications. We monitor that setting
+ // via telemetry, so call into that recording method here.
+ nsLookAndFeel::RecordAccessibilityTelemetry();
+ LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::MediaQueriesOnly);
+}
+
+- (void)colorsChanged {
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
+}
+
+- (void)cachedValuesChanged {
+ // We only need to re-cache (and broadcast) updated LookAndFeel values, so
+ // that they're up-to-date the next time they're queried. No further change
+ // handling is needed.
+ // TODO: Add a change hint for this which avoids the unnecessary media query
+ // invalidation.
+ LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::MediaQueriesOnly);
+}
+@end
diff --git a/widget/cocoa/nsMacCursor.h b/widget/cocoa/nsMacCursor.h
new file mode 100644
index 0000000000..d68ebcf387
--- /dev/null
+++ b/widget/cocoa/nsMacCursor.h
@@ -0,0 +1,129 @@
+/* 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/. */
+
+#ifndef nsMacCursor_h_
+#define nsMacCursor_h_
+
+#import <Cocoa/Cocoa.h>
+#import "nsIWidget.h"
+
+/*! @class nsMacCursor
+ @abstract Represents a native Mac cursor.
+ @discussion <code>nsMacCursor</code> provides a simple API for creating and
+ working with native Macintosh cursors. Cursors can be created
+ used without needing to be aware of the way different cursors
+ are implemented, in particular the details of managing an
+ animated cursor are hidden.
+*/
+@interface nsMacCursor : NSObject {
+ @private
+ NSTimer* mTimer;
+ @protected
+ nsCursor mType;
+ int mFrameCounter;
+}
+
+/*! @method cursorWithCursor:
+ @abstract Create a cursor by specifying a Cocoa <code>NSCursor</code>.
+ @discussion Creates a cursor representing the given Cocoa built-in cursor.
+ @param aCursor the <code>NSCursor</code> to use
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an autoreleased instance of <code>nsMacCursor</code>
+ representing the given <code>NSCursor</code>
+ */
++ (nsMacCursor*)cursorWithCursor:(NSCursor*)aCursor type:(nsCursor)aType;
+
+/*! @method cursorWithImageNamed:hotSpot:type:
+ @abstract Create a cursor by specifying the name of an image resource to
+ use for the cursor and a hotspot.
+ @discussion Creates a cursor by loading the named image using the
+ <code>+[NSImage imageNamed:]</code> method.
+ <p>The image must be compatible with any restrictions laid down
+ by <code>NSCursor</code>. These vary by operating system
+ version.</p>
+ <p>The hotspot precisely determines the point where the user
+ clicks when using the cursor.</p>
+ @param aCursor the name of the image to use for the cursor
+ @param aPoint the point within the cursor to use as the hotspot
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an autoreleased instance of <code>nsMacCursor</code> that uses
+ the given image and hotspot
+ */
++ (nsMacCursor*)cursorWithImageNamed:(NSString*)aCursorImage
+ hotSpot:(NSPoint)aPoint
+ type:(nsCursor)aType;
+
+/*! @method cursorWithFrames:type:
+ @abstract Create an animated cursor by specifying the frames to use for
+ the animation.
+ @discussion Creates a cursor that will animate by cycling through the given
+ frames. Each element of the array must be an instance of
+ <code>NSCursor</code>
+ @param aCursorFrames an array of <code>NSCursor</code>, representing
+ the frames of an animated cursor, in the order they should be
+ played.
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an autoreleased instance of <code>nsMacCursor</code> that will
+ animate the given cursor frames
+ */
++ (nsMacCursor*)cursorWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType;
+
+/*! @method cocoaCursorWithImageNamed:hotSpot:
+ @abstract Create a Cocoa NSCursor object with a Gecko image resource name
+ and a hotspot point.
+ @discussion Create a Cocoa NSCursor object with a Gecko image resource name
+ and a hotspot point.
+ @param imageName the name of the gecko image resource, "tiff"
+ extension is assumed, do not append.
+ @param aPoint the point within the cursor to use as the hotspot
+ @result an autoreleased instance of <code>nsMacCursor</code> that will
+ animate the given cursor frames
+ */
++ (NSCursor*)cocoaCursorWithImageNamed:(NSString*)imageName
+ hotSpot:(NSPoint)aPoint;
+
+/*! @method isSet
+ @abstract Determines whether this cursor is currently active.
+ @discussion This can be helpful when the Cocoa NSCursor state can be
+ influenced without going through nsCursorManager.
+ @result whether the cursor is currently set
+ */
+- (BOOL)isSet;
+
+/*! @method set
+ @abstract Set the cursor.
+ @discussion Makes this cursor the current cursor. If the cursor is
+ animated, the animation is started.
+ */
+- (void)set;
+
+/*! @method unset
+ @abstract Unset the cursor. The cursor will return to the default
+ (usually the arrow cursor).
+ @discussion Unsets the cursor. If the cursor is animated, the animation is
+ stopped.
+ */
+- (void)unset;
+
+/*! @method isAnimated
+ @abstract Tests whether this cursor is animated.
+ @discussion Use this method to determine whether a cursor is animated
+ @result YES if the cursor is animated (has more than one frame), NO if
+ it is a simple static cursor.
+ */
+- (BOOL)isAnimated;
+
+/** @method cursorType
+ @abstract Get the cursor type for this cursor
+ @discussion This method returns the <code>nsCursor</code> constant that
+ corresponds to this cursor, which is equivalent to the CSS
+ name for the cursor.
+ @result The nsCursor constant corresponding to this cursor, or
+ nsCursor's 'eCursorCount' if the cursor is a custom cursor
+ loaded from a URI
+ */
+- (nsCursor)type;
+@end
+
+#endif // nsMacCursor_h_
diff --git a/widget/cocoa/nsMacCursor.mm b/widget/cocoa/nsMacCursor.mm
new file mode 100644
index 0000000000..c4dda5acab
--- /dev/null
+++ b/widget/cocoa/nsMacCursor.mm
@@ -0,0 +1,393 @@
+/* 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 "nsMacCursor.h"
+#include "nsObjCExceptions.h"
+#include "nsDebug.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsString.h"
+
+/*! @category nsMacCursor (PrivateMethods)
+ @abstract Private methods internal to the nsMacCursor class.
+ @discussion <code>nsMacCursor</code> is effectively an abstract class. It
+ does not define complete behaviour in and of itself, the subclasses defined
+ in this file provide the useful implementations.
+*/
+@interface nsMacCursor (PrivateMethods)
+
+/*! @method getNextCursorFrame
+ @abstract get the index of the next cursor frame to display.
+ @discussion Increments and returns the frame counter of an animated cursor.
+ @result The index of the next frame to display in the cursor animation
+*/
+- (int)getNextCursorFrame;
+
+/*! @method numFrames
+ @abstract Query the number of frames in this cursor's animation.
+ @discussion Returns the number of frames in this cursor's animation. Static
+ cursors return 1.
+*/
+- (int)numFrames;
+
+/*! @method createTimer
+ @abstract Create a Timer to use to animate the cursor.
+ @discussion Creates an instance of <code>NSTimer</code> which is used to
+ drive the cursor animation. This method should only be called for cursors
+ that are animated.
+*/
+- (void)createTimer;
+
+/*! @method destroyTimer
+ @abstract Destroy any timer instance associated with this cursor.
+ @discussion Invalidates and releases any <code>NSTimer</code> instance
+ associated with this cursor.
+ */
+- (void)destroyTimer;
+/*! @method destroyTimer
+ @abstract Destroy any timer instance associated with this cursor.
+ @discussion Invalidates and releases any <code>NSTimer</code> instance
+ associated with this cursor.
+*/
+
+/*! @method advanceAnimatedCursor:
+ @abstract Method called by animation timer to perform animation.
+ @discussion Called by an animated cursor's associated timer to advance the
+ animation to the next frame. Determines which frame should occur next and
+ sets the cursor to that frame.
+ @param aTimer the timer causing the animation
+*/
+- (void)advanceAnimatedCursor:(NSTimer*)aTimer;
+
+/*! @method setFrame:
+ @abstract Sets the current cursor, using an index to determine which frame
+ in the animation to display.
+ @discussion Sets the current cursor. The frame index determines which frame
+ is shown if the cursor is animated. Frames and numbered from <code>0</code>
+ to <code>-[nsMacCursor numFrames] - 1</code>. A static cursor has a single
+ frame, numbered 0.
+ @param aFrameIndex the index indicating which frame from the animation
+ to display
+*/
+- (void)setFrame:(int)aFrameIndex;
+
+@end
+
+/*! @class nsCocoaCursor
+ @abstract Implementation of <code>nsMacCursor</code> that uses Cocoa
+ <code>NSCursor</code> instances.
+ @discussion Displays a static or animated cursor, using Cocoa
+ <code>NSCursor</code> instances. These can be either built-in
+ <code>NSCursor</code> instances, or custom <code>NSCursor</code>s created
+ from images. When more than one <code>NSCursor</code> is provided, the cursor
+ will use these as animation frames.
+*/
+@interface nsCocoaCursor : nsMacCursor {
+ @private
+ NSArray* mFrames;
+ NSCursor* mLastSetCocoaCursor;
+}
+
+/*! @method initWithFrames:
+ @abstract Create an animated cursor by specifying the frames to use for
+ the animation.
+ @discussion Creates a cursor that will animate by cycling through the given
+ frames. Each element of the array must be an instance of
+ <code>NSCursor</code>
+ @param aCursorFrames an array of <code>NSCursor</code>, representing
+ the frames of an animated cursor, in the order they should be played.
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an instance of <code>nsCocoaCursor</code> that will animate the
+ given cursor frames
+ */
+- (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType;
+
+/*! @method initWithCursor:
+ @abstract Create a cursor by specifying a Cocoa <code>NSCursor</code>.
+ @discussion Creates a cursor representing the given Cocoa built-in cursor.
+ @param aCursor the <code>NSCursor</code> to use
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an instance of <code>nsCocoaCursor</code> representing the given
+ <code>NSCursor</code>
+*/
+- (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType;
+
+/*! @method initWithImageNamed:hotSpot:
+ @abstract Create a cursor by specifying the name of an image resource to
+ use for the cursor and a hotspot.
+ @discussion Creates a cursor by loading the named image using the
+ <code>+[NSImage imageNamed:]</code> method. <p>The image must be compatible
+ with any restrictions laid down by <code>NSCursor</code>. These vary by
+ operating system version.</p> <p>The hotspot precisely determines the point
+ where the user clicks when using the cursor.</p>
+ @param aCursor the name of the image to use for the cursor
+ @param aPoint the point within the cursor to use as the hotspot
+ @param aType the corresponding <code>nsCursor</code> constant
+ @result an instance of <code>nsCocoaCursor</code> that uses the given
+ image and hotspot
+*/
+- (id)initWithImageNamed:(NSString*)aCursorImage
+ hotSpot:(NSPoint)aPoint
+ type:(nsCursor)aType;
+
+@end
+
+@implementation nsMacCursor
+
++ (nsMacCursor*)cursorWithCursor:(NSCursor*)aCursor type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [[[nsCocoaCursor alloc] initWithCursor:aCursor
+ type:aType] autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (nsMacCursor*)cursorWithImageNamed:(NSString*)aCursorImage
+ hotSpot:(NSPoint)aPoint
+ type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [[[nsCocoaCursor alloc] initWithImageNamed:aCursorImage
+ hotSpot:aPoint
+ type:aType] autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (nsMacCursor*)cursorWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [[[nsCocoaCursor alloc] initWithFrames:aCursorFrames
+ type:aType] autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
++ (NSCursor*)cocoaCursorWithImageNamed:(NSString*)imageName
+ hotSpot:(NSPoint)aPoint {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCOMPtr<nsIFile> resDir;
+ nsAutoCString resPath;
+ NSString *pathToImage, *pathToHiDpiImage;
+ NSImage *cursorImage, *hiDpiCursorImage;
+
+ nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(resDir));
+ if (NS_FAILED(rv)) goto INIT_FAILURE;
+ resDir->AppendNative("res"_ns);
+ resDir->AppendNative("cursors"_ns);
+
+ rv = resDir->GetNativePath(resPath);
+ if (NS_FAILED(rv)) goto INIT_FAILURE;
+
+ pathToImage = [NSString stringWithUTF8String:(const char*)resPath.get()];
+ if (!pathToImage) goto INIT_FAILURE;
+ pathToImage = [pathToImage stringByAppendingPathComponent:imageName];
+ pathToHiDpiImage = [pathToImage stringByAppendingString:@"@2x"];
+ // Add same extension to both image paths.
+ pathToImage = [pathToImage stringByAppendingPathExtension:@"png"];
+ pathToHiDpiImage = [pathToHiDpiImage stringByAppendingPathExtension:@"png"];
+
+ cursorImage =
+ [[[NSImage alloc] initWithContentsOfFile:pathToImage] autorelease];
+ if (!cursorImage) goto INIT_FAILURE;
+
+ // Note 1: There are a few different ways to get a hidpi image via
+ // initWithContentsOfFile. We let the OS handle this here: when the
+ // file basename ends in "@2x", it will be displayed at native resolution
+ // instead of being pixel-doubled. See bug 784909 comment 7 for alternates
+ // ways.
+ //
+ // Note 2: The OS is picky, and will ignore the hidpi representation
+ // unless it is exactly twice the size of the lowdpi image.
+ hiDpiCursorImage =
+ [[[NSImage alloc] initWithContentsOfFile:pathToHiDpiImage] autorelease];
+ if (hiDpiCursorImage) {
+ NSImageRep* imageRep = [[hiDpiCursorImage representations] objectAtIndex:0];
+ [cursorImage addRepresentation:imageRep];
+ }
+ return [[[NSCursor alloc] initWithImage:cursorImage
+ hotSpot:aPoint] autorelease];
+
+INIT_FAILURE:
+ NS_WARNING("Problem getting path to cursor image file!");
+ [self release];
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isSet {
+ // implemented by subclasses
+ return NO;
+}
+
+- (void)set {
+ if ([self isAnimated]) {
+ [self createTimer];
+ }
+ // if the cursor isn't animated or the timer creation fails for any reason...
+ if (!mTimer) {
+ [self setFrame:0];
+ }
+}
+
+- (void)unset {
+ [self destroyTimer];
+}
+
+- (BOOL)isAnimated {
+ return [self numFrames] > 1;
+}
+
+- (int)numFrames {
+ // subclasses need to override this to support animation
+ return 1;
+}
+
+- (int)getNextCursorFrame {
+ mFrameCounter = (mFrameCounter + 1) % [self numFrames];
+ return mFrameCounter;
+}
+
+- (void)createTimer {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mTimer) {
+ mTimer = [[NSTimer
+ scheduledTimerWithTimeInterval:0.25
+ target:self
+ selector:@selector(advanceAnimatedCursor:)
+ userInfo:nil
+ repeats:YES] retain];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)destroyTimer {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTimer) {
+ [mTimer invalidate];
+ [mTimer release];
+ mTimer = nil;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)advanceAnimatedCursor:(NSTimer*)aTimer {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([aTimer isValid]) {
+ [self setFrame:[self getNextCursorFrame]];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)setFrame:(int)aFrameIndex {
+ // subclasses need to do something useful here
+}
+
+- (nsCursor)type {
+ return mType;
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self destroyTimer];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
+
+@implementation nsCocoaCursor
+
+- (id)initWithFrames:(NSArray*)aCursorFrames type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ self = [super init];
+ NSEnumerator* it = [aCursorFrames objectEnumerator];
+ NSObject* frame = nil;
+ while ((frame = [it nextObject])) {
+ NS_ASSERTION([frame isKindOfClass:[NSCursor class]],
+ "Invalid argument: All frames must be of type NSCursor");
+ }
+ mFrames = [aCursorFrames retain];
+ mFrameCounter = 0;
+ mType = aType;
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)initWithCursor:(NSCursor*)aCursor type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSArray* frame = [NSArray arrayWithObjects:aCursor, nil];
+ return [self initWithFrames:frame type:aType];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)initWithImageNamed:(NSString*)aCursorImage
+ hotSpot:(NSPoint)aPoint
+ type:(nsCursor)aType {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return
+ [self initWithCursor:[nsMacCursor cocoaCursorWithImageNamed:aCursorImage
+ hotSpot:aPoint]
+ type:aType];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isSet {
+ return [NSCursor currentCursor] == mLastSetCocoaCursor;
+}
+
+- (void)setFrame:(int)aFrameIndex {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSCursor* newCursor = [mFrames objectAtIndex:aFrameIndex];
+ [newCursor set];
+ mLastSetCocoaCursor = newCursor;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (int)numFrames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mFrames count];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(0);
+}
+
+- (NSString*)description {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mFrames description];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mFrames release];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
diff --git a/widget/cocoa/nsMacDockSupport.h b/widget/cocoa/nsMacDockSupport.h
new file mode 100644
index 0000000000..f3a12485b3
--- /dev/null
+++ b/widget/cocoa/nsMacDockSupport.h
@@ -0,0 +1,35 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 "nsIMacDockSupport.h"
+#include "nsIStandaloneNativeMenu.h"
+#include "nsITaskbarProgress.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+@class MOZProgressDockOverlayView;
+
+class nsMacDockSupport : public nsIMacDockSupport, public nsITaskbarProgress {
+ public:
+ nsMacDockSupport();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACDOCKSUPPORT
+ NS_DECL_NSITASKBARPROGRESS
+
+ protected:
+ virtual ~nsMacDockSupport();
+
+ nsCOMPtr<nsIStandaloneNativeMenu> mDockMenu;
+ nsString mBadgeText;
+
+ NSView* mDockTileWrapperView;
+ MOZProgressDockOverlayView* mProgressDockOverlayView;
+
+ nsTaskbarProgressState mProgressState;
+ double mProgressFraction;
+
+ nsresult UpdateDockTile();
+};
diff --git a/widget/cocoa/nsMacDockSupport.mm b/widget/cocoa/nsMacDockSupport.mm
new file mode 100644
index 0000000000..5e9da63e10
--- /dev/null
+++ b/widget/cocoa/nsMacDockSupport.mm
@@ -0,0 +1,436 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <signal.h>
+
+#include "nsCocoaUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMacDockSupport.h"
+#include "nsObjCExceptions.h"
+#include "nsNativeThemeColors.h"
+#include "nsString.h"
+
+NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
+
+// This view is used in the dock tile when we're downloading a file.
+// It draws a progress bar that looks similar to the native progress bar on
+// 10.12. This style of progress bar is not animated, unlike the pre-10.10
+// progress bar look which had to redrawn multiple times per second.
+@interface MOZProgressDockOverlayView : NSView {
+ double mFractionValue;
+}
+@property double fractionValue;
+
+@end
+
+@implementation MOZProgressDockOverlayView
+
+@synthesize fractionValue = mFractionValue;
+
+- (void)drawRect:(NSRect)aRect {
+ // Erase the background behind this view, i.e. cut a rectangle hole in the
+ // icon.
+ [[NSColor clearColor] set];
+ NSRectFill(self.bounds);
+
+ // Split the height of this view into four quarters. The middle two quarters
+ // will be covered by the actual progress bar.
+ CGFloat radius = self.bounds.size.height / 4;
+ NSRect barBounds = NSInsetRect(self.bounds, 0, radius);
+
+ NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:barBounds
+ xRadius:radius
+ yRadius:radius];
+
+ // Draw a grayish background first.
+ [[NSColor colorWithDeviceWhite:0 alpha:0.1] setFill];
+ [path fill];
+
+ // Draw a fill in the control accent color for the progress part.
+ NSRect progressFillRect = self.bounds;
+ progressFillRect.size.width *= mFractionValue;
+ [NSGraphicsContext saveGraphicsState];
+ [NSBezierPath clipRect:progressFillRect];
+ [[NSColor controlAccentColor] setFill];
+ [path fill];
+ [NSGraphicsContext restoreGraphicsState];
+
+ // Add a shadowy stroke on top.
+ [NSGraphicsContext saveGraphicsState];
+ [path addClip];
+ [[NSColor colorWithDeviceWhite:0 alpha:0.2] setStroke];
+ path.lineWidth = barBounds.size.height / 10;
+ [path stroke];
+ [NSGraphicsContext restoreGraphicsState];
+}
+
+@end
+
+nsMacDockSupport::nsMacDockSupport()
+ : mDockTileWrapperView(nil),
+ mProgressDockOverlayView(nil),
+ mProgressState(STATE_NO_PROGRESS),
+ mProgressFraction(0.0) {}
+
+nsMacDockSupport::~nsMacDockSupport() {
+ if (mDockTileWrapperView) {
+ [mDockTileWrapperView release];
+ mDockTileWrapperView = nil;
+ }
+ if (mProgressDockOverlayView) {
+ [mProgressDockOverlayView release];
+ mProgressDockOverlayView = nil;
+ }
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::GetDockMenu(nsIStandaloneNativeMenu** aDockMenu) {
+ nsCOMPtr<nsIStandaloneNativeMenu> dockMenu(mDockMenu);
+ dockMenu.forget(aDockMenu);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetDockMenu(nsIStandaloneNativeMenu* aDockMenu) {
+ mDockMenu = aDockMenu;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::ActivateApplication(bool aIgnoreOtherApplications) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [[NSApplication sharedApplication]
+ activateIgnoringOtherApps:aIgnoreOtherApplications];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
+ mBadgeText = aBadgeText;
+ if (aBadgeText.IsEmpty())
+ [tile setBadgeLabel:nil];
+ else
+ [tile
+ setBadgeLabel:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(
+ mBadgeText.get())
+ length:mBadgeText.Length()]];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
+ aBadgeText = mBadgeText;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState,
+ uint64_t aCurrentValue, uint64_t aMaxValue) {
+ NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
+ if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
+ NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
+ }
+ if (aCurrentValue > aMaxValue) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ mProgressState = aState;
+ if (aMaxValue == 0) {
+ mProgressFraction = 0;
+ } else {
+ mProgressFraction = (double)aCurrentValue / aMaxValue;
+ }
+
+ return UpdateDockTile();
+}
+
+nsresult nsMacDockSupport::UpdateDockTile() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) {
+ if (!mDockTileWrapperView) {
+ // Create the following NSView hierarchy:
+ // * mDockTileWrapperView (NSView)
+ // * imageView (NSImageView) <- has the application icon
+ // * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the
+ // progress bar
+
+ mDockTileWrapperView =
+ [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
+ mDockTileWrapperView.autoresizingMask =
+ NSViewWidthSizable | NSViewHeightSizable;
+
+ NSImageView* imageView =
+ [[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
+ imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
+ imageView.imageScaling = NSImageScaleAxesIndependently;
+ imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ [mDockTileWrapperView addSubview:imageView];
+
+ mProgressDockOverlayView = [[MOZProgressDockOverlayView alloc]
+ initWithFrame:NSMakeRect(1, 3, 30, 4)];
+ mProgressDockOverlayView.autoresizingMask =
+ NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin |
+ NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin;
+ [mDockTileWrapperView addSubview:mProgressDockOverlayView];
+ }
+ if (NSApp.dockTile.contentView != mDockTileWrapperView) {
+ NSApp.dockTile.contentView = mDockTileWrapperView;
+ }
+
+ if (mProgressState == STATE_NORMAL) {
+ mProgressDockOverlayView.fractionValue = mProgressFraction;
+ } else {
+ // Indeterminate states are rare. Just fill the entire progress bar in
+ // that case.
+ mProgressDockOverlayView.fractionValue = 1.0;
+ }
+ [NSApp.dockTile display];
+ } else if (NSApp.dockTile.contentView) {
+ NSApp.dockTile.contentView = nil;
+ [NSApp.dockTile display];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+extern "C" {
+// Private CFURL API used by the Dock.
+CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
+CFURLRef _CFURLCreateFromPropertyListRepresentation(
+ CFAllocatorRef alloc, CFPropertyListRef pListRepresentation);
+} // extern "C"
+
+namespace {
+
+const NSArray* const browserAppNames = [NSArray
+ arrayWithObjects:@"Firefox.app", @"Firefox Beta.app",
+ @"Firefox Nightly.app", @"Safari.app", @"WebKit.app",
+ @"Google Chrome.app", @"Google Chrome Canary.app",
+ @"Chromium.app", @"Opera.app", nil];
+
+constexpr NSString* const kDockDomainName = @"com.apple.dock";
+// See https://developer.apple.com/documentation/devicemanagement/dock
+constexpr NSString* const kDockPersistentAppsKey = @"persistent-apps";
+// See
+// https://developer.apple.com/documentation/devicemanagement/dock/staticitem
+constexpr NSString* const kDockTileDataKey = @"tile-data";
+constexpr NSString* const kDockFileDataKey = @"file-data";
+
+NSArray* GetPersistentAppsFromDockPlist(NSDictionary* aDockPlist) {
+ if (!aDockPlist) {
+ return nil;
+ }
+ NSArray* persistentApps = [aDockPlist objectForKey:kDockPersistentAppsKey];
+ if (![persistentApps isKindOfClass:[NSArray class]]) {
+ return nil;
+ }
+ return persistentApps;
+}
+
+NSString* GetPathForApp(NSDictionary* aPersistantApp) {
+ if (![aPersistantApp isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+ NSDictionary* tileData = aPersistantApp[kDockTileDataKey];
+ if (![tileData isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+ NSDictionary* fileData = tileData[kDockFileDataKey];
+ if (![fileData isKindOfClass:[NSDictionary class]]) {
+ // Some special tiles may not have DockFileData but we can ignore those.
+ return nil;
+ }
+ NSURL* url = CFBridgingRelease(
+ _CFURLCreateFromPropertyListRepresentation(NULL, fileData));
+ if (!url) {
+ return nil;
+ }
+ return [url isFileURL] ? [url path] : nullptr;
+}
+
+// The only reliable way to get our changes to take effect seems to be to use
+// `kill`.
+void RefreshDock(NSDictionary* aDockPlist) {
+ [[NSUserDefaults standardUserDefaults] setPersistentDomain:aDockPlist
+ forName:kDockDomainName];
+ NSRunningApplication* dockApp = [[NSRunningApplication
+ runningApplicationsWithBundleIdentifier:@"com.apple.dock"] firstObject];
+ if (!dockApp) {
+ return;
+ }
+ pid_t pid = [dockApp processIdentifier];
+ if (pid > 0) {
+ kill(pid, SIGTERM);
+ }
+}
+
+} // namespace
+
+nsresult nsMacDockSupport::GetIsAppInDock(bool* aIsInDock) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *aIsInDock = false;
+
+ NSDictionary* dockPlist = [[NSUserDefaults standardUserDefaults]
+ persistentDomainForName:kDockDomainName];
+ if (!dockPlist) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSArray* persistentApps = GetPersistentAppsFromDockPlist(dockPlist);
+ if (!persistentApps) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSString* appPath = [[NSBundle mainBundle] bundlePath];
+
+ for (id app in persistentApps) {
+ NSString* persistentAppPath = GetPathForApp(app);
+ if (persistentAppPath && [appPath isEqual:persistentAppPath]) {
+ *aIsInDock = true;
+ break;
+ }
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsMacDockSupport::EnsureAppIsPinnedToDock(
+ const nsAString& aAppPath, const nsAString& aAppToReplacePath,
+ bool* aIsInDock) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aAppPath != aAppToReplacePath || !aAppPath.IsEmpty());
+
+ *aIsInDock = false;
+
+ NSString* appPath = !aAppPath.IsEmpty() ? nsCocoaUtils::ToNSString(aAppPath)
+ : [[NSBundle mainBundle] bundlePath];
+ NSString* appToReplacePath = nsCocoaUtils::ToNSString(aAppToReplacePath);
+
+ NSMutableDictionary* dockPlist = [NSMutableDictionary
+ dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults]
+ persistentDomainForName:kDockDomainName]];
+ if (!dockPlist) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSMutableArray* persistentApps =
+ [NSMutableArray arrayWithArray:GetPersistentAppsFromDockPlist(dockPlist)];
+ if (!persistentApps) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // See the comment for this method in the .idl file for the strategy that we
+ // use here to determine where to pin the app.
+ NSUInteger preexistingAppIndex = NSNotFound; // full path matches
+ NSUInteger sameNameAppIndex = NSNotFound; // app name matches only
+ NSUInteger toReplaceAppIndex = NSNotFound;
+ NSUInteger lastBrowserAppIndex = NSNotFound;
+ for (NSUInteger index = 0; index < [persistentApps count]; ++index) {
+ NSString* persistentAppPath =
+ GetPathForApp([persistentApps objectAtIndex:index]);
+
+ if ([persistentAppPath isEqualToString:appPath]) {
+ preexistingAppIndex = index;
+ } else if (appToReplacePath &&
+ [persistentAppPath isEqualToString:appToReplacePath]) {
+ toReplaceAppIndex = index;
+ } else {
+ NSString* appName = [appPath lastPathComponent];
+ NSString* persistentAppName = [persistentAppPath lastPathComponent];
+
+ if ([persistentAppName isEqual:appName]) {
+ if ([appToReplacePath hasPrefix:@"/private/var/folders/"] &&
+ [appToReplacePath containsString:@"/AppTranslocation/"] &&
+ [persistentAppPath hasPrefix:@"/Volumes/"]) {
+ // This is a special case when an app with the same name was
+ // previously dragged and pinned from a quarantined DMG straight to
+ // the Dock and an attempt is now made to pin the same named app to
+ // the Dock. In this case we want to replace the currently pinned app
+ // icon.
+ toReplaceAppIndex = index;
+ } else {
+ sameNameAppIndex = index;
+ }
+ } else {
+ if ([browserAppNames containsObject:persistentAppName]) {
+ lastBrowserAppIndex = index;
+ }
+ }
+ }
+ }
+
+ // Special cases where we're not going to add a new Dock tile:
+ if (preexistingAppIndex != NSNotFound) {
+ if (toReplaceAppIndex != NSNotFound) {
+ [persistentApps removeObjectAtIndex:toReplaceAppIndex];
+ [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
+ RefreshDock(dockPlist);
+ }
+ *aIsInDock = true;
+ return NS_OK;
+ }
+
+ // Create new tile:
+ NSDictionary* newDockTile = nullptr;
+ {
+ NSURL* appUrl = [NSURL fileURLWithPath:appPath isDirectory:YES];
+ NSDictionary* dict = CFBridgingRelease(
+ _CFURLCopyPropertyListRepresentation((__bridge CFURLRef)appUrl));
+ if (!dict) {
+ return NS_ERROR_FAILURE;
+ }
+ NSDictionary* dockTileData =
+ [NSDictionary dictionaryWithObject:dict forKey:kDockFileDataKey];
+ if (dockTileData) {
+ newDockTile = [NSDictionary dictionaryWithObject:dockTileData
+ forKey:kDockTileDataKey];
+ }
+ if (!newDockTile) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Update the Dock:
+ if (toReplaceAppIndex != NSNotFound) {
+ [persistentApps replaceObjectAtIndex:toReplaceAppIndex
+ withObject:newDockTile];
+ } else {
+ NSUInteger index;
+ if (sameNameAppIndex != NSNotFound) {
+ index = sameNameAppIndex + 1;
+ } else if (lastBrowserAppIndex != NSNotFound) {
+ index = lastBrowserAppIndex + 1;
+ } else {
+ index = [persistentApps count];
+ }
+ [persistentApps insertObject:newDockTile atIndex:index];
+ }
+ [dockPlist setObject:persistentApps forKey:kDockPersistentAppsKey];
+ RefreshDock(dockPlist);
+
+ *aIsInDock = true;
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMacFinderProgress.h b/widget/cocoa/nsMacFinderProgress.h
new file mode 100644
index 0000000000..a0e48a0d59
--- /dev/null
+++ b/widget/cocoa/nsMacFinderProgress.h
@@ -0,0 +1,24 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+#ifndef _MACFINDERPROGRESS_H_
+#define _MACFINDERPROGRESS_H_
+
+#include "nsIMacFinderProgress.h"
+#include "nsCOMPtr.h"
+
+class nsMacFinderProgress : public nsIMacFinderProgress {
+ public:
+ nsMacFinderProgress();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACFINDERPROGRESS
+
+ protected:
+ virtual ~nsMacFinderProgress();
+
+ NSProgress* mProgress;
+};
+
+#endif
diff --git a/widget/cocoa/nsMacFinderProgress.mm b/widget/cocoa/nsMacFinderProgress.mm
new file mode 100644
index 0000000000..e2b0401313
--- /dev/null
+++ b/widget/cocoa/nsMacFinderProgress.mm
@@ -0,0 +1,93 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset:
+ * 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsMacFinderProgress.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "nsString.h"
+#include "nsObjCExceptions.h"
+
+NS_IMPL_ISUPPORTS(nsMacFinderProgress, nsIMacFinderProgress)
+
+nsMacFinderProgress::nsMacFinderProgress() : mProgress(nil) {}
+
+nsMacFinderProgress::~nsMacFinderProgress() {
+ if (mProgress) {
+ [mProgress unpublish];
+ [mProgress release];
+ }
+}
+
+NS_IMETHODIMP
+nsMacFinderProgress::Init(
+ const nsAString& path,
+ nsIMacFinderProgressCanceledCallback* cancellationCallback) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSURL* pathUrl = [NSURL
+ fileURLWithPath:[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(
+ path.BeginReading())
+ length:path.Length()]];
+ NSDictionary* userInfo = @{
+ @"NSProgressFileOperationKindKey" :
+ @"NSProgressFileOperationKindDownloading",
+ @"NSProgressFileURLKey" : pathUrl
+ };
+
+ mProgress = [[NSProgress alloc] initWithParent:nil userInfo:userInfo];
+ mProgress.kind = NSProgressKindFile;
+ mProgress.cancellable = YES;
+
+ nsMainThreadPtrHandle<nsIMacFinderProgressCanceledCallback>
+ cancellationCallbackHandle(
+ new nsMainThreadPtrHolder<nsIMacFinderProgressCanceledCallback>(
+ "MacFinderProgress::CancellationCallback", cancellationCallback));
+
+ mProgress.cancellationHandler = ^{
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MacFinderProgress::Canceled", [cancellationCallbackHandle] {
+ MOZ_ASSERT(NS_IsMainThread());
+ cancellationCallbackHandle->Canceled();
+ }));
+ };
+
+ [mProgress publish];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacFinderProgress::UpdateProgress(uint64_t currentProgress,
+ uint64_t totalProgress) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if (mProgress) {
+ mProgress.totalUnitCount = totalProgress;
+ mProgress.completedUnitCount = currentProgress;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacFinderProgress::End() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mProgress) {
+ [mProgress unpublish];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMacSharingService.h b/widget/cocoa/nsMacSharingService.h
new file mode 100644
index 0000000000..d97ce59380
--- /dev/null
+++ b/widget/cocoa/nsMacSharingService.h
@@ -0,0 +1,22 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsMacSharingService_h_
+#define nsMacSharingService_h_
+
+#include "nsIMacSharingService.h"
+
+class nsMacSharingService : public nsIMacSharingService {
+ public:
+ nsMacSharingService() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACSHARINGSERVICE
+
+ protected:
+ virtual ~nsMacSharingService() {}
+};
+
+#endif // nsMacSharingService_h_
diff --git a/widget/cocoa/nsMacSharingService.mm b/widget/cocoa/nsMacSharingService.mm
new file mode 100644
index 0000000000..694617ba65
--- /dev/null
+++ b/widget/cocoa/nsMacSharingService.mm
@@ -0,0 +1,218 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsMacSharingService.h"
+
+#include "jsapi.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty
+#include "nsCocoaUtils.h"
+#include "mozilla/MacStringHelpers.h"
+
+NS_IMPL_ISUPPORTS(nsMacSharingService, nsIMacSharingService)
+
+NSString* const remindersServiceName =
+ @"com.apple.reminders.RemindersShareExtension";
+
+// These are some undocumented constants also used by Safari
+// to let us open the preferences window
+NSString* const extensionPrefPanePath =
+ @"/System/Library/PreferencePanes/Extensions.prefPane";
+const UInt32 openSharingSubpaneDescriptorType = 'ptru';
+NSString* const openSharingSubpaneActionKey = @"action";
+NSString* const openSharingSubpaneActionValue = @"revealExtensionPoint";
+NSString* const openSharingSubpaneProtocolKey = @"protocol";
+NSString* const openSharingSubpaneProtocolValue = @"com.apple.share-services";
+
+// Expose the id so we can pass reference through to JS and back
+@interface NSSharingService (ExposeName)
+- (id)name;
+@end
+
+// Filter providers that we do not want to expose to the user, because they are
+// duplicates or do not work correctly within the context
+static bool ShouldIgnoreProvider(NSString* aProviderName) {
+ return [aProviderName
+ isEqualToString:@"com.apple.share.System.add-to-safari-reading-list"];
+}
+
+// Clean up the activity once the share is complete
+@interface SharingServiceDelegate : NSObject <NSSharingServiceDelegate> {
+ NSUserActivity* mShareActivity;
+}
+
+- (void)cleanup;
+
+@end
+
+@implementation SharingServiceDelegate
+
+- (id)initWithActivity:(NSUserActivity*)activity {
+ self = [super init];
+ mShareActivity = [activity retain];
+ return self;
+}
+
+- (void)cleanup {
+ [mShareActivity resignCurrent];
+ [mShareActivity invalidate];
+ [mShareActivity release];
+ mShareActivity = nil;
+}
+
+- (void)sharingService:(NSSharingService*)sharingService
+ didShareItems:(NSArray*)items {
+ [self cleanup];
+}
+
+- (void)sharingService:(NSSharingService*)service
+ didFailToShareItems:(NSArray*)items
+ error:(NSError*)error {
+ [self cleanup];
+}
+
+- (void)dealloc {
+ [mShareActivity release];
+ [super dealloc];
+}
+
+@end
+
+static NSString* NSImageToBase64(const NSImage* aImage) {
+ CGImageRef cgRef = [aImage CGImageForProposedRect:nil context:nil hints:nil];
+ NSBitmapImageRep* bitmapRep =
+ [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
+ [bitmapRep setSize:[aImage size]];
+ NSData* imageData =
+ [bitmapRep representationUsingType:NSBitmapImageFileTypePNG
+ properties:@{}];
+ NSString* base64Encoded = [imageData base64EncodedStringWithOptions:0];
+ [bitmapRep release];
+ return [NSString stringWithFormat:@"data:image/png;base64,%@", base64Encoded];
+}
+
+static void SetStrAttribute(JSContext* aCx, JS::Rooted<JSObject*>& aObj,
+ const char* aKey, NSString* aVal) {
+ nsAutoString strVal;
+ mozilla::CopyCocoaStringToXPCOMString(aVal, strVal);
+ JS::Rooted<JSString*> title(aCx, JS_NewUCStringCopyZ(aCx, strVal.get()));
+ JS::Rooted<JS::Value> attVal(aCx, JS::StringValue(title));
+ JS_SetProperty(aCx, aObj, aKey, attVal);
+}
+
+nsresult nsMacSharingService::GetSharingProviders(
+ const nsAString& aPageUrl, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSURL* url = nsCocoaUtils::ToNSURL(aPageUrl);
+ if (!url) {
+ // aPageUrl is not a valid URL.
+ return NS_ERROR_FAILURE;
+ }
+
+ NSArray* sharingService = [NSSharingService sharingServicesForItems:@[ url ]];
+ int32_t serviceCount = 0;
+ JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, 0));
+
+ for (NSSharingService* currentService in sharingService) {
+ if (ShouldIgnoreProvider([currentService name])) {
+ continue;
+ }
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+
+ SetStrAttribute(aCx, obj, "name", [currentService name]);
+ SetStrAttribute(aCx, obj, "menuItemTitle", currentService.menuItemTitle);
+ SetStrAttribute(aCx, obj, "image", NSImageToBase64(currentService.image));
+
+ JS::Rooted<JS::Value> element(aCx, JS::ObjectValue(*obj));
+ JS_SetElement(aCx, array, serviceCount++, element);
+ }
+
+ aResult.setObject(*array);
+
+ return NS_OK;
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacSharingService::OpenSharingPreferences() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSURL* prefPaneURL = [NSURL fileURLWithPath:extensionPrefPanePath
+ isDirectory:YES];
+ NSDictionary* args = @{
+ openSharingSubpaneActionKey : openSharingSubpaneActionValue,
+ openSharingSubpaneProtocolKey : openSharingSubpaneProtocolValue
+ };
+ NSData* data = [NSPropertyListSerialization
+ dataWithPropertyList:args
+ format:NSPropertyListXMLFormat_v1_0
+ options:0
+ error:nil];
+ NSAppleEventDescriptor* descriptor = [[NSAppleEventDescriptor alloc]
+ initWithDescriptorType:openSharingSubpaneDescriptorType
+ data:data];
+
+ [[NSWorkspace sharedWorkspace] openURLs:@[ prefPaneURL ]
+ withAppBundleIdentifier:nil
+ options:NSWorkspaceLaunchAsync
+ additionalEventParamDescriptor:descriptor
+ launchIdentifiers:NULL];
+
+ [descriptor release];
+
+ return NS_OK;
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMacSharingService::ShareUrl(const nsAString& aServiceName,
+ const nsAString& aPageUrl,
+ const nsAString& aPageTitle) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSString* serviceName = nsCocoaUtils::ToNSString(aServiceName);
+ NSURL* pageUrl = nsCocoaUtils::ToNSURL(aPageUrl);
+ NSString* pageTitle = nsCocoaUtils::ToNSString(aPageTitle);
+ NSSharingService* service =
+ [NSSharingService sharingServiceNamed:serviceName];
+
+ // Reminders fetch its data from an activity, not the share data
+ if ([[service name] isEqual:remindersServiceName]) {
+ NSUserActivity* shareActivity = [[NSUserActivity alloc]
+ initWithActivityType:NSUserActivityTypeBrowsingWeb];
+
+ if ([pageUrl.scheme hasPrefix:@"http"]) {
+ [shareActivity setWebpageURL:pageUrl];
+ }
+ [shareActivity setEligibleForHandoff:NO];
+ [shareActivity setTitle:pageTitle];
+ [shareActivity becomeCurrent];
+
+ // Pass ownership of shareActivity to shareDelegate, which will release the
+ // activity once sharing has completed.
+ SharingServiceDelegate* shareDelegate =
+ [[SharingServiceDelegate alloc] initWithActivity:shareActivity];
+ [shareActivity release];
+
+ [service setDelegate:shareDelegate];
+ [shareDelegate release];
+ }
+
+ // Twitter likes the the title as an additional share item
+ NSArray* toShare = [[service name] isEqual:NSSharingServiceNamePostOnTwitter]
+ ? @[ pageUrl, pageTitle ]
+ : @[ pageUrl ];
+
+ [service setSubject:pageTitle];
+ [service performWithItems:toShare];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMacUserActivityUpdater.h b/widget/cocoa/nsMacUserActivityUpdater.h
new file mode 100644
index 0000000000..6870a66343
--- /dev/null
+++ b/widget/cocoa/nsMacUserActivityUpdater.h
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#ifndef nsMacUserActivityUpdater_h_
+#define nsMacUserActivityUpdater_h_
+
+#include "nsIMacUserActivityUpdater.h"
+#include "nsCocoaWindow.h"
+
+class nsMacUserActivityUpdater : public nsIMacUserActivityUpdater {
+ public:
+ nsMacUserActivityUpdater() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACUSERACTIVITYUPDATER
+
+ protected:
+ virtual ~nsMacUserActivityUpdater() {}
+ BaseWindow* GetCocoaWindow(nsIBaseWindow* aWindow);
+};
+
+#endif // nsMacUserActivityUpdater_h_
diff --git a/widget/cocoa/nsMacUserActivityUpdater.mm b/widget/cocoa/nsMacUserActivityUpdater.mm
new file mode 100644
index 0000000000..9fedb618a4
--- /dev/null
+++ b/widget/cocoa/nsMacUserActivityUpdater.mm
@@ -0,0 +1,65 @@
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsMacUserActivityUpdater.h"
+
+#include "nsCocoaUtils.h"
+#include "nsIBaseWindow.h"
+#include "gfxPlatform.h"
+
+NS_IMPL_ISUPPORTS(nsMacUserActivityUpdater, nsIMacUserActivityUpdater)
+
+NS_IMETHODIMP
+nsMacUserActivityUpdater::UpdateLocation(const nsAString& aPageUrl,
+ const nsAString& aPageTitle,
+ nsIBaseWindow* aWindow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if (gfxPlatform::IsHeadless()) {
+ // In headless mode, Handoff will fail since there is no Cocoa window.
+ return NS_OK;
+ }
+
+ BaseWindow* cocoaWin = nsMacUserActivityUpdater::GetCocoaWindow(aWindow);
+ if (!cocoaWin) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSURL* pageUrl = nsCocoaUtils::ToNSURL(aPageUrl);
+ if (!pageUrl || (![pageUrl.scheme isEqualToString:@"https"] &&
+ ![pageUrl.scheme isEqualToString:@"http"])) {
+ [cocoaWin.userActivity invalidate];
+ return NS_OK;
+ }
+
+ NSString* pageTitle = nsCocoaUtils::ToNSString(aPageTitle);
+ if (!pageTitle) {
+ pageTitle = pageUrl.absoluteString;
+ }
+
+ NSUserActivity* userActivity = [[NSUserActivity alloc]
+ initWithActivityType:NSUserActivityTypeBrowsingWeb];
+ userActivity.webpageURL = pageUrl;
+ userActivity.title = pageTitle;
+ cocoaWin.userActivity = userActivity;
+ [userActivity release];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+BaseWindow* nsMacUserActivityUpdater::GetCocoaWindow(nsIBaseWindow* aWindow) {
+ nsCOMPtr<nsIWidget> widget = nullptr;
+ aWindow->GetMainWidget(getter_AddRefs(widget));
+ if (!widget) {
+ return nil;
+ }
+ BaseWindow* cocoaWin = (BaseWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (!cocoaWin) {
+ return nil;
+ }
+ return cocoaWin;
+}
diff --git a/widget/cocoa/nsMacWebAppUtils.h b/widget/cocoa/nsMacWebAppUtils.h
new file mode 100644
index 0000000000..1e79ca92d5
--- /dev/null
+++ b/widget/cocoa/nsMacWebAppUtils.h
@@ -0,0 +1,22 @@
+/* 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/. */
+#ifndef _MAC_WEB_APP_UTILS_H_
+#define _MAC_WEB_APP_UTILS_H_
+
+#include "nsIMacWebAppUtils.h"
+
+#define NS_MACWEBAPPUTILS_CONTRACTID "@mozilla.org/widget/mac-web-app-utils;1"
+
+class nsMacWebAppUtils : public nsIMacWebAppUtils {
+ public:
+ nsMacWebAppUtils() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMACWEBAPPUTILS
+
+ protected:
+ virtual ~nsMacWebAppUtils() {}
+};
+
+#endif //_MAC_WEB_APP_UTILS_H_
diff --git a/widget/cocoa/nsMacWebAppUtils.mm b/widget/cocoa/nsMacWebAppUtils.mm
new file mode 100644
index 0000000000..e1a06f873d
--- /dev/null
+++ b/widget/cocoa/nsMacWebAppUtils.mm
@@ -0,0 +1,98 @@
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsMacWebAppUtils.h"
+#include "nsCOMPtr.h"
+#include "nsCocoaUtils.h"
+#include "nsString.h"
+
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+// Find the path to the app with the given bundleIdentifier, if any.
+// Note that the OS will return the path to the newest binary, if there is more
+// than one. The determination of 'newest' is complex and beyond the scope of
+// this comment.
+
+NS_IMPL_ISUPPORTS(nsMacWebAppUtils, nsIMacWebAppUtils)
+
+NS_IMETHODIMP nsMacWebAppUtils::PathForAppWithIdentifier(
+ const nsAString& bundleIdentifier, nsAString& outPath) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ outPath.Truncate();
+
+ nsAutoreleasePool localPool;
+
+ // note that the result of this expression might be nil, meaning no matching
+ // app was found.
+ NSString* temp = [[NSWorkspace sharedWorkspace]
+ absolutePathForAppBundleWithIdentifier:
+ [NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(
+ ((nsString)bundleIdentifier).get())
+ length:((nsString)bundleIdentifier).Length()]];
+
+ if (temp) {
+ // Copy out the resultant absolute path into outPath if non-nil.
+ nsCocoaUtils::GetStringForNSString(temp, outPath);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsMacWebAppUtils::LaunchAppWithIdentifier(
+ const nsAString& bundleIdentifier) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoreleasePool localPool;
+
+ // Note this might return false, meaning the app wasnt launched for some
+ // reason.
+ BOOL success = [[NSWorkspace sharedWorkspace]
+ launchAppWithBundleIdentifier:
+ [NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(
+ ((nsString)bundleIdentifier).get())
+ length:((nsString)bundleIdentifier).Length()]
+ options:(NSWorkspaceLaunchOptions)0
+ additionalEventParamDescriptor:nil
+ launchIdentifier:NULL];
+
+ return success ? NS_OK : NS_ERROR_FAILURE;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsMacWebAppUtils::TrashApp(const nsAString& path,
+ nsITrashAppCallback* aCallback) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (NS_WARN_IF(!aCallback)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsITrashAppCallback> callback = aCallback;
+
+ NSString* tempString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ ((nsString)path).get())
+ length:path.Length()];
+
+ [[NSWorkspace sharedWorkspace]
+ recycleURLs:[NSArray
+ arrayWithObject:[NSURL fileURLWithPath:tempString]]
+ completionHandler:^(NSDictionary* newURLs, NSError* error) {
+ nsresult rv = (error == nil) ? NS_OK : NS_ERROR_FAILURE;
+ callback->TrashAppFinished(rv);
+ }];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsMenuBarX.h b/widget/cocoa/nsMenuBarX.h
new file mode 100644
index 0000000000..bcf124a7ac
--- /dev/null
+++ b/widget/cocoa/nsMenuBarX.h
@@ -0,0 +1,161 @@
+/* -*- 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/. */
+
+#ifndef nsMenuBarX_h_
+#define nsMenuBarX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WeakPtr.h"
+
+#include "nsISupports.h"
+#include "nsMenuParentX.h"
+#include "nsChangeObserver.h"
+#include "nsTArray.h"
+#include "nsString.h"
+
+class nsMenuBarX;
+class nsMenuGroupOwnerX;
+class nsMenuX;
+class nsIWidget;
+class nsIContent;
+
+namespace mozilla {
+namespace dom {
+class Document;
+class Element;
+} // namespace dom
+} // namespace mozilla
+
+// ApplicationMenuDelegate is used to receive Cocoa notifications.
+@interface ApplicationMenuDelegate : NSObject <NSMenuDelegate> {
+ nsMenuBarX* mApplicationMenu; // weak ref
+}
+- (id)initWithApplicationMenu:(nsMenuBarX*)aApplicationMenu;
+@end
+
+// Objective-C class used to allow us to intervene with keyboard event handling.
+// We allow mouse actions to work normally.
+@interface GeckoNSMenu : NSMenu {
+}
+- (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent;
+@end
+
+// Objective-C class used as action target for menu items
+@interface NativeMenuItemTarget : NSObject {
+}
+- (IBAction)menuItemHit:(id)aSender;
+@end
+
+// Objective-C class used for menu items on the Services menu to allow Gecko
+// to override their standard behavior in order to stop key equivalents from
+// firing in certain instances.
+@interface GeckoServicesNSMenuItem : NSMenuItem {
+}
+- (id)target;
+- (SEL)action;
+- (void)_doNothing:(id)aSender;
+@end
+
+// Objective-C class used as the Services menu so that Gecko can override the
+// standard behavior of the Services menu in order to stop key equivalents
+// from firing in certain instances.
+@interface GeckoServicesNSMenu : NSMenu {
+}
+- (void)addItem:(NSMenuItem*)aNewItem;
+- (NSMenuItem*)addItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv;
+- (void)insertItem:(NSMenuItem*)aNewItem atIndex:(NSInteger)aIndex;
+- (NSMenuItem*)insertItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv
+ atIndex:(NSInteger)aIndex;
+- (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem;
+@end
+
+// Once instantiated, this object lives until its DOM node or its parent window
+// is destroyed. Do not hold references to this, they can become invalid any
+// time the DOM node can be destroyed.
+class nsMenuBarX : public nsMenuParentX,
+ public nsChangeObserver,
+ public mozilla::SupportsWeakPtr {
+ public:
+ explicit nsMenuBarX(mozilla::dom::Element* aElement);
+
+ NS_INLINE_DECL_REFCOUNTING(nsMenuBarX)
+
+ static NativeMenuItemTarget* sNativeEventTarget;
+ static nsMenuBarX* sLastGeckoMenuBarPainted;
+
+ // The following content nodes have been removed from the menu system.
+ // We save them here for use in command handling.
+ RefPtr<nsIContent> mAboutItemContent;
+ RefPtr<nsIContent> mPrefItemContent;
+ RefPtr<nsIContent> mAccountItemContent;
+ RefPtr<nsIContent> mQuitItemContent;
+
+ // nsChangeObserver
+ NS_DECL_CHANGEOBSERVER
+
+ // nsMenuParentX
+ nsMenuBarX* AsMenuBar() override { return this; }
+
+ // nsMenuBarX
+ uint32_t GetMenuCount();
+ bool MenuContainsAppMenu();
+ nsMenuX* GetMenuAt(uint32_t aIndex);
+ nsMenuX* GetXULHelpMenu();
+ void SetSystemHelpMenu();
+ nsresult Paint();
+ void ForceUpdateNativeMenuAt(const nsAString& aIndexString);
+ void ForceNativeMenuReload(); // used for testing
+ static void ResetNativeApplicationMenu();
+ void SetNeedsRebuild();
+ void ApplicationMenuOpened();
+ bool PerformKeyEquivalent(NSEvent* aEvent);
+ GeckoNSMenu* NativeNSMenu() { return mNativeMenu; }
+
+ // nsMenuParentX
+ void MenuChildChangedVisibility(const MenuChild& aChild,
+ bool aIsVisible) override;
+
+ protected:
+ virtual ~nsMenuBarX();
+
+ void ConstructNativeMenus();
+ void ConstructFallbackNativeMenus();
+ void InsertMenuAtIndex(RefPtr<nsMenuX>&& aMenu, uint32_t aIndex);
+ void RemoveMenuAtIndex(uint32_t aIndex);
+ RefPtr<mozilla::dom::Element> HideItem(mozilla::dom::Document* aDocument,
+ const nsAString& aID);
+ void AquifyMenuBar();
+ NSMenuItem* CreateNativeAppMenuItem(nsMenuX* aMenu, const nsAString& aNodeID,
+ SEL aAction, int aTag,
+ NativeMenuItemTarget* aTarget);
+ void CreateApplicationMenu(nsMenuX* aMenu);
+
+ // Calculates the index at which aChild's NSMenuItem should be inserted into
+ // our NSMenu. The order of NSMenuItems in the NSMenu is the same as the order
+ // of nsMenuX objects in mMenuArray; there are two differences:
+ // - mMenuArray contains both visible and invisible menus, and the NSMenu
+ // only contains visible
+ // menus.
+ // - Our NSMenu may also contain an item for the app menu, whereas mMenuArray
+ // never does.
+ // So the insertion index is equal to the number of visible previous siblings
+ // of aChild in mMenuArray, plus one if the app menu is present.
+ NSInteger CalculateNativeInsertionPoint(nsMenuX* aChild);
+
+ RefPtr<nsIContent> mContent;
+ RefPtr<nsMenuGroupOwnerX> mMenuGroupOwner;
+ nsTArray<RefPtr<nsMenuX>> mMenuArray;
+ GeckoNSMenu* mNativeMenu; // root menu, representing entire menu bar
+ bool mNeedsRebuild;
+ ApplicationMenuDelegate* mApplicationMenuDelegate;
+};
+
+#endif // nsMenuBarX_h_
diff --git a/widget/cocoa/nsMenuBarX.mm b/widget/cocoa/nsMenuBarX.mm
new file mode 100644
index 0000000000..9865fc196d
--- /dev/null
+++ b/widget/cocoa/nsMenuBarX.mm
@@ -0,0 +1,1169 @@
+/* -*- 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 <objc/objc-runtime.h>
+
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+#include "nsChildView.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsGkAtoms.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+
+#include "nsIContent.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+#include "nsIAppStartup.h"
+#include "nsIStringBundle.h"
+#include "nsToolkitCompsCID.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/dom/Element.h"
+
+using namespace mozilla;
+using mozilla::dom::Element;
+
+NativeMenuItemTarget* nsMenuBarX::sNativeEventTarget = nil;
+nsMenuBarX* nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
+NSMenu* sApplicationMenu = nil;
+BOOL sApplicationMenuIsFallback = NO;
+BOOL gSomeMenuBarPainted = NO;
+
+// defined in nsCocoaWindow.mm.
+extern BOOL sTouchBarIsInitialized;
+
+// We keep references to the first quit and pref item content nodes we find,
+// which will be from the hidden window. We use these when the document for the
+// current window does not have a quit or pref item. We don't need strong refs
+// here because these items are always strong ref'd by their owning menu bar
+// (instance variable).
+static nsIContent* sAboutItemContent = nullptr;
+static nsIContent* sPrefItemContent = nullptr;
+static nsIContent* sAccountItemContent = nullptr;
+static nsIContent* sQuitItemContent = nullptr;
+
+//
+// ApplicationMenuDelegate Objective-C class
+//
+
+@implementation ApplicationMenuDelegate
+
+- (id)initWithApplicationMenu:(nsMenuBarX*)aApplicationMenu {
+ if ((self = [super init])) {
+ mApplicationMenu = aApplicationMenu;
+ }
+ return self;
+}
+
+- (void)menuWillOpen:(NSMenu*)menu {
+ mApplicationMenu->ApplicationMenuOpened();
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+}
+
+@end
+
+nsMenuBarX::nsMenuBarX(mozilla::dom::Element* aElement)
+ : mNeedsRebuild(false), mApplicationMenuDelegate(nil) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, this);
+ mMenuGroupOwner->RegisterForLocaleChanges();
+ mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"];
+
+ mContent = aElement;
+
+ if (mContent) {
+ AquifyMenuBar();
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+ ConstructNativeMenus();
+ } else {
+ ConstructFallbackNativeMenus();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuBarX::~nsMenuBarX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (nsMenuBarX::sLastGeckoMenuBarPainted == this) {
+ nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
+ }
+
+ // the quit/pref items of a random window might have been used if there was no
+ // hidden window, thus we need to invalidate the weak references.
+ if (sAboutItemContent == mAboutItemContent) {
+ sAboutItemContent = nullptr;
+ }
+ if (sQuitItemContent == mQuitItemContent) {
+ sQuitItemContent = nullptr;
+ }
+ if (sPrefItemContent == mPrefItemContent) {
+ sPrefItemContent = nullptr;
+ }
+ if (sAccountItemContent == mAccountItemContent) {
+ sAccountItemContent = nullptr;
+ }
+
+ mMenuGroupOwner->UnregisterForLocaleChanges();
+
+ // make sure we unregister ourselves as a content observer
+ if (mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+
+ for (nsMenuX* menu : mMenuArray) {
+ menu->DetachFromGroupOwnerRecursive();
+ menu->DetachFromParent();
+ }
+
+ if (mApplicationMenuDelegate) {
+ [mApplicationMenuDelegate release];
+ }
+
+ [mNativeMenu release];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::ConstructNativeMenus() {
+ for (nsIContent* menuContent = mContent->GetFirstChild(); menuContent;
+ menuContent = menuContent->GetNextSibling()) {
+ if (menuContent->IsXULElement(nsGkAtoms::menu)) {
+ InsertMenuAtIndex(
+ MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, menuContent->AsElement()),
+ GetMenuCount());
+ }
+ }
+}
+
+void nsMenuBarX::ConstructFallbackNativeMenus() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (sApplicationMenu) {
+ // Menu has already been built.
+ return;
+ }
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ bundleSvc->CreateBundle("chrome://global/locale/fallbackMenubar.properties",
+ getter_AddRefs(stringBundle));
+
+ if (!stringBundle) {
+ return;
+ }
+
+ nsAutoString labelUTF16;
+ nsAutoString keyUTF16;
+
+ const char* labelProp = "quitMenuitem.label";
+ const char* keyProp = "quitMenuitem.key";
+
+ stringBundle->GetStringFromName(labelProp, labelUTF16);
+ stringBundle->GetStringFromName(keyProp, keyUTF16);
+
+ NSString* labelStr =
+ [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(labelUTF16).get()];
+ NSString* keyStr =
+ [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(keyUTF16).get()];
+
+ if (!nsMenuBarX::sNativeEventTarget) {
+ nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
+ }
+
+ sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
+ if (!mApplicationMenuDelegate) {
+ mApplicationMenuDelegate =
+ [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
+ }
+ sApplicationMenu.delegate = mApplicationMenuDelegate;
+ NSMenuItem* quitMenuItem =
+ [[[NSMenuItem alloc] initWithTitle:labelStr
+ action:@selector(menuItemHit:)
+ keyEquivalent:keyStr] autorelease];
+ quitMenuItem.target = nsMenuBarX::sNativeEventTarget;
+ quitMenuItem.tag = eCommand_ID_Quit;
+ [sApplicationMenu addItem:quitMenuItem];
+ sApplicationMenuIsFallback = YES;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+uint32_t nsMenuBarX::GetMenuCount() { return mMenuArray.Length(); }
+
+bool nsMenuBarX::MenuContainsAppMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ return (mNativeMenu.numberOfItems > 0 &&
+ [mNativeMenu itemAtIndex:0].submenu == sApplicationMenu);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::InsertMenuAtIndex(RefPtr<nsMenuX>&& aMenu, uint32_t aIndex) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // If we've only yet created a fallback global Application menu (using
+ // ContructFallbackNativeMenus()), destroy it before recreating it properly.
+ if (sApplicationMenu && sApplicationMenuIsFallback) {
+ ResetNativeApplicationMenu();
+ }
+ // If we haven't created a global Application menu yet, do it.
+ if (!sApplicationMenu) {
+ CreateApplicationMenu(aMenu.get());
+
+ // Hook the new Application menu up to the menu bar.
+ NSMenu* mainMenu = NSApp.mainMenu;
+ NS_ASSERTION(
+ mainMenu.numberOfItems > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+ [mainMenu itemAtIndex:0].submenu = sApplicationMenu;
+ }
+
+ // add menu to array that owns our menus
+ mMenuArray.InsertElementAt(aIndex, aMenu);
+
+ // hook up submenus
+ RefPtr<nsIContent> menuContent = aMenu->Content();
+ if (menuContent->GetChildCount() > 0 &&
+ !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
+ MenuChildChangedVisibility(MenuChild(aMenu), true);
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::RemoveMenuAtIndex(uint32_t aIndex) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mMenuArray.Length() <= aIndex) {
+ NS_ERROR("Attempting submenu removal with bad index!");
+ return;
+ }
+
+ RefPtr<nsMenuX> menu = mMenuArray[aIndex];
+ mMenuArray.RemoveElementAt(aIndex);
+
+ menu->DetachFromGroupOwnerRecursive();
+ menu->DetachFromParent();
+
+ // Our native menu and our internal menu object array might be out of sync.
+ // This happens, for example, when a submenu is hidden. Because of this we
+ // should not assume that a native submenu is hooked up.
+ NSMenuItem* nativeMenuItem = menu->NativeNSMenuItem();
+ int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem];
+ if (nativeMenuItemIndex != -1) {
+ [mNativeMenu removeItemAtIndex:nativeMenuItemIndex];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::ObserveAttributeChanged(mozilla::dom::Document* aDocument,
+ nsIContent* aContent,
+ nsAtom* aAttribute) {}
+
+void nsMenuBarX::ObserveContentRemoved(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ nsINode* parent = NODE_FROM(aContainer, aDocument);
+ MOZ_ASSERT(parent);
+ const Maybe<uint32_t> index = parent->ComputeIndexOf(aPreviousSibling);
+ MOZ_ASSERT(*index != UINT32_MAX);
+ RemoveMenuAtIndex(index.isSome() ? *index + 1u : 0u);
+}
+
+void nsMenuBarX::ObserveContentInserted(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild) {
+ InsertMenuAtIndex(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aChild),
+ aContainer->ComputeIndexOf(aChild).valueOr(UINT32_MAX));
+}
+
+void nsMenuBarX::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSArray* indexes = [locationString componentsSeparatedByString:@"|"];
+ unsigned int indexCount = indexes.count;
+ if (indexCount == 0) {
+ return;
+ }
+
+ RefPtr<nsMenuX> currentMenu = nullptr;
+ int targetIndex = [[indexes objectAtIndex:0] intValue];
+ int visible = 0;
+ uint32_t length = mMenuArray.Length();
+ // first find a menu in the menu bar
+ for (unsigned int i = 0; i < length; i++) {
+ RefPtr<nsMenuX> menu = mMenuArray[i];
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) {
+ visible++;
+ if (visible == (targetIndex + 1)) {
+ currentMenu = std::move(menu);
+ break;
+ }
+ }
+ }
+
+ if (!currentMenu) {
+ return;
+ }
+
+ // fake open/close to cause lazy update to happen so submenus populate
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+
+ // now find the correct submenu
+ for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
+ targetIndex = [[indexes objectAtIndex:i] intValue];
+ visible = 0;
+ length = currentMenu->GetItemCount();
+ for (unsigned int j = 0; j < length; j++) {
+ Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
+ if (!targetMenu) {
+ return;
+ }
+ RefPtr<nsIContent> content = targetMenu->match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->Content();
+ });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ visible++;
+ if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
+ currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
+ // fake open/close to cause lazy update to happen
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+ break;
+ }
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// Calling this forces a full reload of the menu system, reloading all native
+// menus and their items.
+// Without this testing is hard because changes to the DOM affect the native
+// menu system lazily.
+void nsMenuBarX::ForceNativeMenuReload() {
+ // tear down everything
+ while (GetMenuCount() > 0) {
+ RemoveMenuAtIndex(0);
+ }
+
+ // construct everything
+ ConstructNativeMenus();
+}
+
+nsMenuX* nsMenuBarX::GetMenuAt(uint32_t aIndex) {
+ if (mMenuArray.Length() <= aIndex) {
+ NS_ERROR("Requesting menu at invalid index!");
+ return nullptr;
+ }
+ return mMenuArray[aIndex].get();
+}
+
+nsMenuX* nsMenuBarX::GetXULHelpMenu() {
+ // The Help menu is usually (always?) the last one, so we start there and
+ // count back.
+ for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
+ nsMenuX* aMenu = GetMenuAt(i);
+ if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content())) {
+ return aMenu;
+ }
+ }
+ return nil;
+}
+
+// On SnowLeopard and later we must tell the OS which is our Help menu.
+// Otherwise it will only add Spotlight for Help (the Search item) to our
+// Help menu if its label/title is "Help" -- i.e. if the menu is in English.
+// This resolves bugs 489196 and 539317.
+void nsMenuBarX::SetSystemHelpMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ nsMenuX* xulHelpMenu = GetXULHelpMenu();
+ if (xulHelpMenu) {
+ NSMenu* helpMenu = xulHelpMenu->NativeNSMenu();
+ if (helpMenu) {
+ NSApp.helpMenu = helpMenu;
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// macOS is adding some (currently 3) hidden menu items every time that
+// `NSApp.mainMenu` is set, but never removes them. This ultimately causes a
+// significant slowdown when switching between windows because the number of
+// items in `NSApp.mainMenu` is growing without bounds.
+//
+// The known hidden, problematic menu items are associated with the following
+// menus:
+// - Start Dictation...
+// - Emoji & Symbols
+//
+// Removing these items before setting `NSApp.mainMenu` prevents this slowdown.
+// See bug 1808223.
+static bool RemoveProblematicMenuItems(NSMenu* aMenu) {
+ uint8_t problematicMenuItemCount = 3;
+ NSMutableArray* itemsToRemove =
+ [NSMutableArray arrayWithCapacity:problematicMenuItemCount];
+
+ for (NSInteger i = 0; i < aMenu.numberOfItems; i++) {
+ NSMenuItem* item = [aMenu itemAtIndex:i];
+
+ if (item.hidden &&
+ (item.action == @selector(startDictation:) ||
+ item.action == @selector(orderFrontCharacterPalette:))) {
+ [itemsToRemove addObject:@(i)];
+ }
+
+ if (item.hasSubmenu && RemoveProblematicMenuItems(item.submenu)) {
+ return true;
+ }
+ }
+
+ bool didRemoveItems = false;
+ for (NSNumber* index in [itemsToRemove reverseObjectEnumerator]) {
+ [aMenu removeItemAtIndex:index.integerValue];
+ didRemoveItems = true;
+ }
+
+ return didRemoveItems;
+}
+
+nsresult nsMenuBarX::Paint() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Don't try to optimize anything in this painting by checking
+ // sLastGeckoMenuBarPainted because the menubar can be manipulated by
+ // native dialogs and sheet code and other things besides this paint method.
+
+ // We have to keep the same menu item for the Application menu so we keep
+ // passing it along.
+ NSMenu* outgoingMenu = [NSApp.mainMenu retain];
+ NS_ASSERTION(
+ outgoingMenu.numberOfItems > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+
+ NSMenuItem* appMenuItem = [[outgoingMenu itemAtIndex:0] retain];
+ [outgoingMenu removeItemAtIndex:0];
+ if (appMenuItem) {
+ [mNativeMenu insertItem:appMenuItem atIndex:0];
+ }
+ [appMenuItem release];
+ [outgoingMenu release];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ RemoveProblematicMenuItems(mNativeMenu);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Set menu bar and event target.
+ NSApp.mainMenu = mNativeMenu;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ SetSystemHelpMenu();
+ nsMenuBarX::sLastGeckoMenuBarPainted = this;
+
+ gSomeMenuBarPainted = YES;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+/* static */
+void nsMenuBarX::ResetNativeApplicationMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ [sApplicationMenu removeAllItems];
+ [sApplicationMenu release];
+ sApplicationMenu = nil;
+ sApplicationMenuIsFallback = NO;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::SetNeedsRebuild() { mNeedsRebuild = true; }
+
+void nsMenuBarX::ApplicationMenuOpened() {
+ if (mNeedsRebuild) {
+ if (!mMenuArray.IsEmpty()) {
+ ResetNativeApplicationMenu();
+ CreateApplicationMenu(mMenuArray[0].get());
+ }
+ mNeedsRebuild = false;
+ }
+}
+
+bool nsMenuBarX::PerformKeyEquivalent(NSEvent* aEvent) {
+ return [mNativeMenu performSuperKeyEquivalent:aEvent];
+}
+
+void nsMenuBarX::MenuChildChangedVisibility(const MenuChild& aChild,
+ bool aIsVisible) {
+ MOZ_RELEASE_ASSERT(aChild.is<RefPtr<nsMenuX>>(),
+ "nsMenuBarX only has nsMenuX children");
+ const RefPtr<nsMenuX>& child = aChild.as<RefPtr<nsMenuX>>();
+ NSMenuItem* item = child->NativeNSMenuItem();
+ if (aIsVisible) {
+ NSInteger insertionPoint = CalculateNativeInsertionPoint(child);
+ [mNativeMenu insertItem:item atIndex:insertionPoint];
+ } else if ([mNativeMenu indexOfItem:item] != -1) {
+ [mNativeMenu removeItem:item];
+ }
+}
+
+NSInteger nsMenuBarX::CalculateNativeInsertionPoint(nsMenuX* aChild) {
+ NSInteger insertionPoint = MenuContainsAppMenu() ? 1 : 0;
+ for (auto& currMenu : mMenuArray) {
+ if (currMenu == aChild) {
+ return insertionPoint;
+ }
+ // Only count items that are inside a menu.
+ // XXXmstange Not sure what would cause free-standing items. Maybe for
+ // collapsed/hidden menus? In that case, an nsMenuX::IsVisible() method
+ // would be better.
+ if (currMenu->NativeNSMenuItem().menu) {
+ insertionPoint++;
+ }
+ }
+ return insertionPoint;
+}
+
+// Hide the item in the menu by setting the 'hidden' attribute. Returns it so
+// the caller can hang onto it if they so choose.
+RefPtr<Element> nsMenuBarX::HideItem(mozilla::dom::Document* aDocument,
+ const nsAString& aID) {
+ RefPtr<Element> menuElement = aDocument->GetElementById(aID);
+ if (menuElement) {
+ menuElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, u"true"_ns,
+ false);
+ }
+ return menuElement;
+}
+
+// Do what is necessary to conform to the Aqua guidelines for menus.
+void nsMenuBarX::AquifyMenuBar() {
+ RefPtr<mozilla::dom::Document> domDoc = mContent->GetComposedDoc();
+ if (domDoc) {
+ // remove the "About..." item and its separator
+ HideItem(domDoc, u"aboutSeparator"_ns);
+ mAboutItemContent = HideItem(domDoc, u"aboutName"_ns);
+ if (!sAboutItemContent) {
+ sAboutItemContent = mAboutItemContent;
+ }
+
+ // remove quit item and its separator
+ HideItem(domDoc, u"menu_FileQuitSeparator"_ns);
+ mQuitItemContent = HideItem(domDoc, u"menu_FileQuitItem"_ns);
+ if (!sQuitItemContent) {
+ sQuitItemContent = mQuitItemContent;
+ }
+
+ // remove prefs item and its separator, but save off the pref content node
+ // so we can invoke its command later.
+ HideItem(domDoc, u"menu_PrefsSeparator"_ns);
+ mPrefItemContent = HideItem(domDoc, u"menu_preferences"_ns);
+ if (!sPrefItemContent) {
+ sPrefItemContent = mPrefItemContent;
+ }
+
+ // remove Account Settings item.
+ mAccountItemContent = HideItem(domDoc, u"menu_accountmgr"_ns);
+ if (!sAccountItemContent) {
+ sAccountItemContent = mAccountItemContent;
+ }
+
+ // hide items that we use for the Application menu
+ HideItem(domDoc, u"menu_mac_services"_ns);
+ HideItem(domDoc, u"menu_mac_hide_app"_ns);
+ HideItem(domDoc, u"menu_mac_hide_others"_ns);
+ HideItem(domDoc, u"menu_mac_show_all"_ns);
+ HideItem(domDoc, u"menu_mac_touch_bar"_ns);
+ }
+}
+
+// for creating menu items destined for the Application menu
+NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* aMenu,
+ const nsAString& aNodeID,
+ SEL aAction, int aTag,
+ NativeMenuItemTarget* aTarget) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ RefPtr<mozilla::dom::Document> doc = aMenu->Content()->GetUncomposedDoc();
+ if (!doc) {
+ return nil;
+ }
+
+ RefPtr<mozilla::dom::Element> menuItem = doc->GetElementById(aNodeID);
+ if (!menuItem) {
+ return nil;
+ }
+
+ // Check collapsed rather than hidden since the app menu items are always
+ // hidden in AquifyMenuBar.
+ if (menuItem->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return nil;
+ }
+
+ // Get information from the gecko menu item
+ nsAutoString label;
+ nsAutoString modifiers;
+ nsAutoString key;
+ menuItem->GetAttr(nsGkAtoms::label, label);
+ menuItem->GetAttr(nsGkAtoms::modifiers, modifiers);
+ menuItem->GetAttr(nsGkAtoms::key, key);
+
+ // Get more information about the key equivalent. Start by
+ // finding the key node we need.
+ NSString* keyEquiv = nil;
+ unsigned int macKeyModifiers = 0;
+ if (!key.IsEmpty()) {
+ RefPtr<Element> keyElement = doc->GetElementById(key);
+ if (keyElement) {
+ // first grab the key equivalent character
+ nsAutoString keyChar(u" "_ns);
+ keyElement->GetAttr(nsGkAtoms::key, keyChar);
+ if (!keyChar.EqualsLiteral(" ")) {
+ keyEquiv = [[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(keyChar.get())
+ length:keyChar.Length()] lowercaseString];
+ }
+ // now grab the key equivalent modifiers
+ nsAutoString modifiersStr;
+ keyElement->GetAttr(nsGkAtoms::modifiers, modifiersStr);
+ uint8_t geckoModifiers =
+ nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
+ macKeyModifiers =
+ nsMenuUtilsX::MacModifiersForGeckoModifiers(geckoModifiers);
+ }
+ }
+ // get the label into NSString-form
+ NSString* labelString = [NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
+ length:label.Length()];
+
+ if (!labelString) {
+ labelString = @"";
+ }
+ if (!keyEquiv) {
+ keyEquiv = @"";
+ }
+
+ // put together the actual NSMenuItem
+ NSMenuItem* newMenuItem = [[NSMenuItem alloc] initWithTitle:labelString
+ action:aAction
+ keyEquivalent:keyEquiv];
+
+ newMenuItem.tag = aTag;
+ newMenuItem.target = aTarget;
+ newMenuItem.keyEquivalentModifierMask = macKeyModifiers;
+ newMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
+
+ return newMenuItem;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// build the Application menu shared by all menu bars
+void nsMenuBarX::CreateApplicationMenu(nsMenuX* aMenu) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // At this point, the application menu is the application menu from
+ // the nib in cocoa widgets. We do not have a way to create an application
+ // menu manually, so we grab the one from the nib and use that.
+ sApplicationMenu = [[NSApp.mainMenu itemAtIndex:0].submenu retain];
+
+ /*
+ We support the following menu items here:
+
+ Menu Item DOM Node ID Notes
+
+ ========================
+ = About This App = <- aboutName
+ ========================
+ = Preferences... = <- menu_preferences
+ = Account Settings = <- menu_accountmgr Only on Thunderbird
+ ========================
+ = Services > = <- menu_mac_services <- (do not define key
+ equivalent)
+ ========================
+ = Hide App = <- menu_mac_hide_app
+ = Hide Others = <- menu_mac_hide_others
+ = Show All = <- menu_mac_show_all
+ ========================
+ = Customize Touch Bar… = <- menu_mac_touch_bar
+ ========================
+ = Quit = <- menu_FileQuitItem
+ ========================
+
+ If any of them are ommitted from the application's DOM, we just don't add
+ them. We always add a "Quit" item, but if an app developer does not provide
+ a DOM node with the right ID for the Quit item, we add it in English. App
+ developers need only add each node with a label and a key equivalent (if
+ they want one). Other attributes are optional. Like so:
+
+ <menuitem id="menu_preferences"
+ label="&preferencesCmdMac.label;"
+ key="open_prefs_key"/>
+
+ We need to use this system for localization purposes, until we have a better
+ way to define the Application menu to be used on Mac OS X.
+ */
+
+ if (sApplicationMenu) {
+ if (!mApplicationMenuDelegate) {
+ mApplicationMenuDelegate =
+ [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
+ }
+ sApplicationMenu.delegate = mApplicationMenuDelegate;
+
+ // This code reads attributes we are going to care about from the DOM
+ // elements
+
+ NSMenuItem* itemBeingAdded = nil;
+ BOOL addAboutSeparator = FALSE;
+ BOOL addPrefsSeparator = FALSE;
+
+ // Add the About menu item
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"aboutName"_ns, @selector(menuItemHit:), eCommand_ID_About,
+ nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addAboutSeparator = TRUE;
+ }
+
+ // Add separator if either the About item or software update item exists
+ if (addAboutSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add the Preferences menu item
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_preferences"_ns, @selector(menuItemHit:),
+ eCommand_ID_Prefs, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addPrefsSeparator = TRUE;
+ }
+
+ // Add the Account Settings menu item. This is Thunderbird only
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_accountmgr"_ns, @selector(menuItemHit:),
+ eCommand_ID_Account, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ }
+
+ // Add separator after Preferences menu
+ if (addPrefsSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add Services menu item
+ itemBeingAdded =
+ CreateNativeAppMenuItem(aMenu, u"menu_mac_services"_ns, nil, 0, nil);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+
+ // set this menu item up as the Mac OS X Services menu
+ NSMenu* servicesMenu = [[GeckoServicesNSMenu alloc] initWithTitle:@""];
+ itemBeingAdded.submenu = servicesMenu;
+ NSApp.servicesMenu = servicesMenu;
+
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ // Add separator after Services menu
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ BOOL addHideShowSeparator = FALSE;
+
+ // Add menu item to hide this application
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_hide_app"_ns, @selector(menuItemHit:),
+ eCommand_ID_HideApp, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add menu item to hide other applications
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_hide_others"_ns, @selector(menuItemHit:),
+ eCommand_ID_HideOthers, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add menu item to show all applications
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_show_all"_ns, @selector(menuItemHit:),
+ eCommand_ID_ShowAll, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add a separator after the hide/show menus if at least one exists
+ if (addHideShowSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ BOOL addTouchBarSeparator = NO;
+
+ // Add Touch Bar customization menu item.
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_touch_bar"_ns, @selector(menuItemHit:),
+ eCommand_ID_TouchBar, nsMenuBarX::sNativeEventTarget);
+
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ // We hide the menu item on Macs that don't have a Touch Bar.
+ if (!sTouchBarIsInitialized) {
+ [itemBeingAdded setHidden:YES];
+ } else {
+ addTouchBarSeparator = YES;
+ }
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ }
+
+ // Add a separator after the Touch Bar menu item if it exists
+ if (addTouchBarSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add quit menu item
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_FileQuitItem"_ns, @selector(menuItemHit:),
+ eCommand_ID_Quit, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ } else {
+ // the current application does not have a DOM node for "Quit". Add one
+ // anyway, in English.
+ NSMenuItem* defaultQuitItem =
+ [[[NSMenuItem alloc] initWithTitle:@"Quit"
+ action:@selector(menuItemHit:)
+ keyEquivalent:@"q"] autorelease];
+ defaultQuitItem.target = nsMenuBarX::sNativeEventTarget;
+ defaultQuitItem.tag = eCommand_ID_Quit;
+ [sApplicationMenu addItem:defaultQuitItem];
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+//
+// Objective-C class used to allow us to have keyboard commands
+// look like they are doing something but actually do nothing.
+// We allow mouse actions to work normally.
+//
+
+// Controls whether or not native menu items should invoke their commands.
+static BOOL gMenuItemsExecuteCommands = YES;
+
+@implementation GeckoNSMenu
+
+// Keyboard commands should not cause menu items to invoke their
+// commands when there is a key window because we'd rather send
+// the keyboard command to the window. We still have the menus
+// go through the mechanics so they'll give the proper visual
+// feedback.
+- (BOOL)performKeyEquivalent:(NSEvent*)aEvent {
+ // We've noticed that Mac OS X expects this check in subclasses before
+ // calling NSMenu's "performKeyEquivalent:".
+ //
+ // There is no case in which we'd need to do anything or return YES
+ // when we have no items so we can just do this check first.
+ if (self.numberOfItems <= 0) {
+ return NO;
+ }
+
+ NSWindow* keyWindow = NSApp.keyWindow;
+
+ // If there is no key window then just behave normally. This
+ // probably means that this menu is associated with Gecko's
+ // hidden window.
+ if (!keyWindow) {
+ return [super performKeyEquivalent:aEvent];
+ }
+
+ NSResponder* firstResponder = keyWindow.firstResponder;
+
+ gMenuItemsExecuteCommands = NO;
+ [super performKeyEquivalent:aEvent];
+ gMenuItemsExecuteCommands = YES; // return to default
+
+ // Return YES if we invoked a command and there is now no key window or we
+ // changed the first responder. In this case we do not want to propagate the
+ // event because we don't want it handled again.
+ if (!NSApp.keyWindow || NSApp.keyWindow.firstResponder != firstResponder) {
+ return YES;
+ }
+
+ // Return NO so that we can handle the event via NSView's "keyDown:".
+ return NO;
+}
+
+- (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent {
+ return [super performKeyEquivalent:aEvent];
+}
+
+@end
+
+//
+// Objective-C class used as action target for menu items
+//
+
+@implementation NativeMenuItemTarget
+
+// called when some menu item in this menu gets hit
+- (IBAction)menuItemHit:(id)aSender {
+ if (!gMenuItemsExecuteCommands) {
+ return;
+ }
+
+ if (![aSender isKindOfClass:[NSMenuItem class]]) {
+ return;
+ }
+
+ NSMenuItem* nativeMenuItem = (NSMenuItem*)aSender;
+ NSInteger tag = nativeMenuItem.tag;
+
+ nsMenuGroupOwnerX* menuGroupOwner = nullptr;
+ nsMenuBarX* menuBar = nullptr;
+ MOZMenuItemRepresentedObject* representedObject =
+ nativeMenuItem.representedObject;
+
+ if (representedObject) {
+ menuGroupOwner = representedObject.menuGroupOwner;
+ if (!menuGroupOwner) {
+ return;
+ }
+ menuBar = menuGroupOwner->GetMenuBar();
+ }
+
+ // Notify containing menu about the fact that a menu item will be activated.
+ NSMenu* menu = nativeMenuItem.menu;
+ if ([menu.delegate isKindOfClass:[MenuDelegate class]]) {
+ [(MenuDelegate*)menu.delegate menu:menu willActivateItem:nativeMenuItem];
+ }
+
+ // Get the modifier flags and button for this menu item activation. The menu
+ // system does not pass an NSEvent to our action selector, but we can query
+ // the current NSEvent instead. The current NSEvent can be a key event or a
+ // mouseup event, depending on how the menu item is activated.
+ NSEventModifierFlags modifierFlags =
+ NSApp.currentEvent ? NSApp.currentEvent.modifierFlags : 0;
+ mozilla::MouseButton button =
+ NSApp.currentEvent ? nsCocoaUtils::ButtonForEvent(NSApp.currentEvent)
+ : mozilla::MouseButton::ePrimary;
+
+ // Do special processing if this is for an app-global command.
+ if (tag == eCommand_ID_About) {
+ nsIContent* mostSpecificContent = sAboutItemContent;
+ if (menuBar && menuBar->mAboutItemContent) {
+ mostSpecificContent = menuBar->mAboutItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_Prefs) {
+ nsIContent* mostSpecificContent = sPrefItemContent;
+ if (menuBar && menuBar->mPrefItemContent) {
+ mostSpecificContent = menuBar->mPrefItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_Account) {
+ nsIContent* mostSpecificContent = sAccountItemContent;
+ if (menuBar && menuBar->mAccountItemContent) {
+ mostSpecificContent = menuBar->mAccountItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_HideApp) {
+ [NSApp hide:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_HideOthers) {
+ [NSApp hideOtherApplications:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_ShowAll) {
+ [NSApp unhideAllApplications:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_TouchBar) {
+ [NSApp toggleTouchBarCustomizationPalette:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_Quit) {
+ nsIContent* mostSpecificContent = sQuitItemContent;
+ if (menuBar && menuBar->mQuitItemContent) {
+ mostSpecificContent = menuBar->mQuitItemContent;
+ }
+ // If we have some content for quit we execute it. Otherwise we send a
+ // native app terminate message. If you want to stop a quit from happening,
+ // provide quit content and return the event as unhandled.
+ if (mostSpecificContent) {
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags,
+ button);
+ } else {
+ nsCOMPtr<nsIAppStartup> appStartup =
+ mozilla::components::AppStartup::Service();
+ if (appStartup) {
+ bool userAllowedQuit = true;
+ appStartup->Quit(nsIAppStartup::eAttemptQuit, 0, &userAllowedQuit);
+ }
+ }
+ return;
+ }
+
+ // given the commandID, look it up in our hashtable and dispatch to
+ // that menu item.
+ if (menuGroupOwner) {
+ if (RefPtr<nsMenuItemX> menuItem = menuGroupOwner->GetMenuItemForCommandID(
+ static_cast<uint32_t>(tag))) {
+ if (nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest) {
+ menuItem->DoCommand(modifierFlags, button);
+ } else if (RefPtr<nsMenuX> menu = menuItem->ParentMenu()) {
+ menu->ActivateItemAfterClosing(std::move(menuItem), modifierFlags,
+ button);
+ }
+ }
+ }
+}
+
+@end
+
+// Objective-C class used for menu items on the Services menu to allow Gecko
+// to override their standard behavior in order to stop key equivalents from
+// firing in certain instances. When gMenuItemsExecuteCommands is NO, we return
+// a dummy target and action instead of the actual target and action.
+
+@implementation GeckoServicesNSMenuItem
+
+- (id)target {
+ id realTarget = super.target;
+ if (gMenuItemsExecuteCommands) {
+ return realTarget;
+ }
+ return realTarget ? self : nil;
+}
+
+- (SEL)action {
+ SEL realAction = super.action;
+ if (gMenuItemsExecuteCommands) {
+ return realAction;
+ }
+ return realAction ? @selector(_doNothing:) : nullptr;
+}
+
+- (void)_doNothing:(id)aSender {
+}
+
+@end
+
+// Objective-C class used as the Services menu so that Gecko can override the
+// standard behavior of the Services menu in order to stop key equivalents
+// from firing in certain instances.
+
+@implementation GeckoServicesNSMenu
+
+- (void)addItem:(NSMenuItem*)aNewItem {
+ [self _overrideClassOfMenuItem:aNewItem];
+ [super addItem:aNewItem];
+}
+
+- (NSMenuItem*)addItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv {
+ NSMenuItem* newItem = [super addItemWithTitle:aString
+ action:aSelector
+ keyEquivalent:aKeyEquiv];
+ [self _overrideClassOfMenuItem:newItem];
+ return newItem;
+}
+
+- (void)insertItem:(NSMenuItem*)aNewItem atIndex:(NSInteger)aIndex {
+ [self _overrideClassOfMenuItem:aNewItem];
+ [super insertItem:aNewItem atIndex:aIndex];
+}
+
+- (NSMenuItem*)insertItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv
+ atIndex:(NSInteger)aIndex {
+ NSMenuItem* newItem = [super insertItemWithTitle:aString
+ action:aSelector
+ keyEquivalent:aKeyEquiv
+ atIndex:aIndex];
+ [self _overrideClassOfMenuItem:newItem];
+ return newItem;
+}
+
+- (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem {
+ if ([aMenuItem class] == [NSMenuItem class]) {
+ object_setClass(aMenuItem, [GeckoServicesNSMenuItem class]);
+ }
+}
+
+@end
diff --git a/widget/cocoa/nsMenuGroupOwnerX.h b/widget/cocoa/nsMenuGroupOwnerX.h
new file mode 100644
index 0000000000..da859d14f3
--- /dev/null
+++ b/widget/cocoa/nsMenuGroupOwnerX.h
@@ -0,0 +1,102 @@
+/* -*- 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/. */
+
+#ifndef nsMenuGroupOwnerX_h_
+#define nsMenuGroupOwnerX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/WeakPtr.h"
+
+#include "nsStubMutationObserver.h"
+#include "nsHashKeys.h"
+#include "nsIObserver.h"
+#include "nsMenuBarX.h"
+#include "nsTHashMap.h"
+#include "nsString.h"
+
+class nsMenuItemX;
+class nsChangeObserver;
+class nsIWidget;
+class nsIContent;
+
+@class MOZMenuItemRepresentedObject;
+
+// Fixed command IDs that work even without a JS listener, for our fallback menu
+// bar. Dynamic command IDs start counting from eCommand_ID_Last.
+enum {
+ eCommand_ID_About = 1,
+ eCommand_ID_Prefs = 2,
+ eCommand_ID_Quit = 3,
+ eCommand_ID_HideApp = 4,
+ eCommand_ID_HideOthers = 5,
+ eCommand_ID_ShowAll = 6,
+ eCommand_ID_Update = 7,
+ eCommand_ID_TouchBar = 8,
+ eCommand_ID_Account = 9,
+ eCommand_ID_Last = 10
+};
+
+// The menu group owner observes DOM mutations, notifies registered
+// nsChangeObservers, and manages command registration. There is one owner per
+// menubar, and one per standalone native menu.
+class nsMenuGroupOwnerX : public nsMultiMutationObserver, public nsIObserver {
+ public:
+ // Both parameters can be null.
+ nsMenuGroupOwnerX(mozilla::dom::Element* aElement,
+ nsMenuBarX* aMenuBarIfMenuBar);
+
+ void RegisterForContentChanges(nsIContent* aContent,
+ nsChangeObserver* aMenuObject);
+ void UnregisterForContentChanges(nsIContent* aContent);
+ uint32_t RegisterForCommand(nsMenuItemX* aMenuItem);
+ void UnregisterCommand(uint32_t aCommandID);
+ nsMenuItemX* GetMenuItemForCommandID(uint32_t aCommandID);
+
+ void RegisterForLocaleChanges();
+ void UnregisterForLocaleChanges();
+
+ // The representedObject that's used for all menu items under this menu group
+ // owner.
+ MOZMenuItemRepresentedObject* GetRepresentedObject() {
+ return mRepresentedObject;
+ }
+
+ // If this is the group owner for a menubar, return the menubar, otherwise
+ // nullptr.
+ nsMenuBarX* GetMenuBar() { return mMenuBar.get(); }
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMUTATIONOBSERVER
+
+ protected:
+ virtual ~nsMenuGroupOwnerX();
+
+ nsChangeObserver* LookupContentChangeObserver(nsIContent* aContent);
+
+ RefPtr<nsIContent> mContent;
+
+ // Unique command id (per menu-bar) to give to next item that asks.
+ uint32_t mCurrentCommandID = eCommand_ID_Last;
+
+ // stores observers for content change notification
+ nsTHashMap<nsPtrHashKey<nsIContent>, nsChangeObserver*>
+ mContentToObserverTable;
+
+ // stores mapping of command IDs to menu objects
+ nsTHashMap<nsUint32HashKey, nsMenuItemX*> mCommandToMenuObjectTable;
+
+ MOZMenuItemRepresentedObject* mRepresentedObject = nil; // [strong]
+ mozilla::WeakPtr<nsMenuBarX> mMenuBar;
+};
+
+@interface MOZMenuItemRepresentedObject : NSObject
+- (id)initWithMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner;
+- (void)setMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner;
+- (nsMenuGroupOwnerX*)menuGroupOwner;
+@end
+
+#endif // nsMenuGroupOwner_h_
diff --git a/widget/cocoa/nsMenuGroupOwnerX.mm b/widget/cocoa/nsMenuGroupOwnerX.mm
new file mode 100644
index 0000000000..45d074e12f
--- /dev/null
+++ b/widget/cocoa/nsMenuGroupOwnerX.mm
@@ -0,0 +1,238 @@
+/* -*- 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 "nsMenuGroupOwnerX.h"
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+#include "nsCocoaWindow.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+
+#include "mozilla/dom/Element.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+
+#include "nsINode.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsMenuGroupOwnerX, nsIObserver, nsIMutationObserver)
+
+nsMenuGroupOwnerX::nsMenuGroupOwnerX(mozilla::dom::Element* aElement,
+ nsMenuBarX* aMenuBarIfMenuBar)
+ : mContent(aElement), mMenuBar(aMenuBarIfMenuBar) {
+ mRepresentedObject =
+ [[MOZMenuItemRepresentedObject alloc] initWithMenuGroupOwner:this];
+}
+
+nsMenuGroupOwnerX::~nsMenuGroupOwnerX() {
+ MOZ_ASSERT(mContentToObserverTable.Count() == 0,
+ "have outstanding mutation observers!\n");
+ [mRepresentedObject setMenuGroupOwner:nullptr];
+ [mRepresentedObject release];
+}
+
+//
+// nsIMutationObserver
+//
+
+void nsMenuGroupOwnerX::CharacterDataWillChange(
+ nsIContent* aContent, const CharacterDataChangeInfo&) {}
+
+void nsMenuGroupOwnerX::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {}
+
+void nsMenuGroupOwnerX::ContentAppended(nsIContent* aFirstNewContent) {
+ for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
+ ContentInserted(cur);
+ }
+}
+
+void nsMenuGroupOwnerX::NodeWillBeDestroyed(nsINode* aNode) {}
+
+void nsMenuGroupOwnerX::AttributeWillChange(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {}
+
+void nsMenuGroupOwnerX::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ nsChangeObserver* obs = LookupContentChangeObserver(aElement);
+ if (obs) {
+ obs->ObserveAttributeChanged(aElement->OwnerDoc(), aElement, aAttribute);
+ }
+}
+
+void nsMenuGroupOwnerX::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ nsIContent* container = aChild->GetParent();
+ if (!container) {
+ return;
+ }
+
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ nsChangeObserver* obs = LookupContentChangeObserver(container);
+ if (obs) {
+ obs->ObserveContentRemoved(aChild->OwnerDoc(), container, aChild,
+ aPreviousSibling);
+ } else if (container != mContent) {
+ // We do a lookup on the parent container in case things were removed
+ // under a "menupopup" item. That is basically a wrapper for the contents
+ // of a "menu" node.
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ obs = LookupContentChangeObserver(parent);
+ if (obs) {
+ obs->ObserveContentRemoved(aChild->OwnerDoc(), container, aChild,
+ aPreviousSibling);
+ }
+ }
+ }
+}
+
+void nsMenuGroupOwnerX::ContentInserted(nsIContent* aChild) {
+ nsIContent* container = aChild->GetParent();
+ if (!container) {
+ return;
+ }
+
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ nsChangeObserver* obs = LookupContentChangeObserver(container);
+ if (obs) {
+ obs->ObserveContentInserted(aChild->OwnerDoc(), container, aChild);
+ } else if (container != mContent) {
+ // We do a lookup on the parent container in case things were removed
+ // under a "menupopup" item. That is basically a wrapper for the contents
+ // of a "menu" node.
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ obs = LookupContentChangeObserver(parent);
+ if (obs) {
+ obs->ObserveContentInserted(aChild->OwnerDoc(), container, aChild);
+ }
+ }
+ }
+}
+
+void nsMenuGroupOwnerX::ParentChainChanged(nsIContent* aContent) {}
+
+void nsMenuGroupOwnerX::ARIAAttributeDefaultWillChange(
+ mozilla::dom::Element* aElement, nsAtom* aAttribute, int32_t aModType) {}
+
+void nsMenuGroupOwnerX::ARIAAttributeDefaultChanged(
+ mozilla::dom::Element* aElement, nsAtom* aAttribute, int32_t aModType) {}
+
+// For change management, we don't use a |nsSupportsHashtable| because
+// we know that the lifetime of all these items is bounded by the
+// lifetime of the menubar. No need to add any more strong refs to the
+// picture because the containment hierarchy already uses strong refs.
+void nsMenuGroupOwnerX::RegisterForContentChanges(
+ nsIContent* aContent, nsChangeObserver* aMenuObject) {
+ if (!mContentToObserverTable.Contains(aContent)) {
+ aContent->AddMutationObserver(this);
+ }
+ mContentToObserverTable.InsertOrUpdate(aContent, aMenuObject);
+}
+
+void nsMenuGroupOwnerX::UnregisterForContentChanges(nsIContent* aContent) {
+ if (mContentToObserverTable.Contains(aContent)) {
+ aContent->RemoveMutationObserver(this);
+ }
+ mContentToObserverTable.Remove(aContent);
+}
+
+void nsMenuGroupOwnerX::RegisterForLocaleChanges() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "intl:app-locales-changed", false);
+ }
+}
+
+void nsMenuGroupOwnerX::UnregisterForLocaleChanges() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "intl:app-locales-changed");
+ }
+}
+
+NS_IMETHODIMP
+nsMenuGroupOwnerX::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (mMenuBar && !strcmp(aTopic, "intl:app-locales-changed")) {
+ // Rebuild the menu with the new locale strings.
+ mMenuBar->SetNeedsRebuild();
+ }
+ return NS_OK;
+}
+
+nsChangeObserver* nsMenuGroupOwnerX::LookupContentChangeObserver(
+ nsIContent* aContent) {
+ nsChangeObserver* result;
+ if (mContentToObserverTable.Get(aContent, &result)) {
+ return result;
+ }
+ return nullptr;
+}
+
+// Given a menu item, creates a unique 4-character command ID and
+// maps it to the item. Returns the id for use by the client.
+uint32_t nsMenuGroupOwnerX::RegisterForCommand(nsMenuItemX* aMenuItem) {
+ // no real need to check for uniqueness. We always start afresh with each
+ // window at 1. Even if we did get close to the reserved Apple command id's,
+ // those don't start until at least ' ', which is integer 538976288. If
+ // we have that many menu items in one window, I think we have other
+ // problems.
+
+ // make id unique
+ ++mCurrentCommandID;
+
+ mCommandToMenuObjectTable.InsertOrUpdate(mCurrentCommandID, aMenuItem);
+
+ return mCurrentCommandID;
+}
+
+// Removes the mapping between the given 4-character command ID
+// and its associated menu item.
+void nsMenuGroupOwnerX::UnregisterCommand(uint32_t aCommandID) {
+ mCommandToMenuObjectTable.Remove(aCommandID);
+}
+
+nsMenuItemX* nsMenuGroupOwnerX::GetMenuItemForCommandID(uint32_t aCommandID) {
+ nsMenuItemX* result;
+ if (mCommandToMenuObjectTable.Get(aCommandID, &result)) {
+ return result;
+ }
+ return nullptr;
+}
+
+@implementation MOZMenuItemRepresentedObject {
+ nsMenuGroupOwnerX*
+ mMenuGroupOwner; // weak, cleared by nsMenuGroupOwnerX's destructor
+}
+
+- (id)initWithMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner {
+ self = [super init];
+ mMenuGroupOwner = aMenuGroupOwner;
+ return self;
+}
+
+- (void)setMenuGroupOwner:(nsMenuGroupOwnerX*)aMenuGroupOwner {
+ mMenuGroupOwner = aMenuGroupOwner;
+}
+
+- (nsMenuGroupOwnerX*)menuGroupOwner {
+ return mMenuGroupOwner;
+}
+
+@end
diff --git a/widget/cocoa/nsMenuItemIconX.h b/widget/cocoa/nsMenuItemIconX.h
new file mode 100644
index 0000000000..e60e920f9d
--- /dev/null
+++ b/widget/cocoa/nsMenuItemIconX.h
@@ -0,0 +1,70 @@
+/* -*- 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/. */
+
+/*
+ * Retrieves and displays icons in native menu items on Mac OS X.
+ */
+
+#ifndef nsMenuItemIconX_h_
+#define nsMenuItemIconX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/widget/IconLoader.h"
+#include "mozilla/WeakPtr.h"
+
+class nsIconLoaderService;
+class nsIURI;
+class nsIContent;
+class nsIPrincipal;
+class imgRequestProxy;
+class nsMenuParentX;
+class nsPresContext;
+
+namespace mozilla {
+class ComputedStyle;
+}
+
+class nsMenuItemIconX final : public mozilla::widget::IconLoader::Listener {
+ public:
+ class Listener {
+ public:
+ virtual void IconUpdated() = 0;
+ };
+
+ explicit nsMenuItemIconX(Listener* aListener);
+ ~nsMenuItemIconX();
+
+ // SetupIcon starts the icon load. Once the icon has loaded,
+ // nsMenuParentX::IconUpdated will be called. The icon image needs to be
+ // retrieved from GetIconImage(). If aContent is an icon-less menuitem,
+ // GetIconImage() will return nil. If it does have an icon, GetIconImage()
+ // will return a transparent placeholder icon during the load and the actual
+ // icon when the load is completed.
+ void SetupIcon(nsIContent* aContent);
+
+ // Implements this method for mozilla::widget::IconLoader::Listener.
+ // Called once the icon load is complete.
+ nsresult OnComplete(imgIContainer* aImage) override;
+
+ // Returns a weak reference to the icon image that is owned by this class. Can
+ // return nil.
+ NSImage* GetIconImage() const { return mIconImage; }
+
+ protected:
+ // Returns whether there should be an icon.
+ bool StartIconLoad(nsIContent* aContent);
+
+ // GetIconURI returns null if the item should not have any icon.
+ already_AddRefed<nsIURI> GetIconURI(nsIContent* aContent);
+
+ Listener* mListener; // [weak]
+ RefPtr<const mozilla::ComputedStyle> mComputedStyle;
+ mozilla::WeakPtr<nsPresContext> mPresContext;
+ NSImage* mIconImage = nil; // [strong]
+ RefPtr<mozilla::widget::IconLoader> mIconLoader;
+};
+
+#endif // nsMenuItemIconX_h_
diff --git a/widget/cocoa/nsMenuItemIconX.mm b/widget/cocoa/nsMenuItemIconX.mm
new file mode 100644
index 0000000000..6749d787de
--- /dev/null
+++ b/widget/cocoa/nsMenuItemIconX.mm
@@ -0,0 +1,173 @@
+/* -*- 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/. */
+
+/*
+ * Retrieves and displays icons in native menu items on Mac OS X.
+ */
+
+/* exception_defines.h defines 'try' to 'if (true)' which breaks objective-c
+ exceptions and produces errors like: error: unexpected '@' in program'.
+ If we define __EXCEPTIONS exception_defines.h will avoid doing this.
+
+ See bug 666609 for more information.
+
+ We use <limits> to get the libstdc++ version. */
+#include <limits>
+#if __GLIBCXX__ <= 20070719
+# ifndef __EXCEPTIONS
+# define __EXCEPTIONS
+# endif
+#endif
+
+#include "MOZIconHelper.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsCocoaUtils.h"
+#include "nsComputedDOMStyle.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIContentPolicy.h"
+#include "nsMenuItemX.h"
+#include "nsMenuItemIconX.h"
+#include "nsNameSpaceManager.h"
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+
+using mozilla::dom::Element;
+using mozilla::widget::IconLoader;
+
+static const uint32_t kIconSize = 16;
+
+nsMenuItemIconX::nsMenuItemIconX(Listener* aListener) : mListener(aListener) {
+ MOZ_COUNT_CTOR(nsMenuItemIconX);
+}
+
+nsMenuItemIconX::~nsMenuItemIconX() {
+ if (mIconLoader) {
+ mIconLoader->Destroy();
+ }
+ if (mIconImage) {
+ [mIconImage release];
+ mIconImage = nil;
+ }
+ MOZ_COUNT_DTOR(nsMenuItemIconX);
+}
+
+void nsMenuItemIconX::SetupIcon(nsIContent* aContent) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ bool shouldHaveIcon = StartIconLoad(aContent);
+ if (!shouldHaveIcon) {
+ // There is no icon for this menu item, as an error occurred while loading
+ // it. An icon might have been set earlier or the place holder icon may have
+ // been set. Clear it.
+ if (mIconImage) {
+ [mIconImage release];
+ mIconImage = nil;
+ }
+ return;
+ }
+
+ if (!mIconImage) {
+ // Set a placeholder icon, so that the menuitem reserves space for the icon
+ // during the load and there is no sudden shift once the icon finishes
+ // loading.
+ NSSize iconSize = NSMakeSize(kIconSize, kIconSize);
+ mIconImage = [[MOZIconHelper placeholderIconWithSize:iconSize] retain];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+bool nsMenuItemIconX::StartIconLoad(nsIContent* aContent) {
+ RefPtr<nsIURI> iconURI = GetIconURI(aContent);
+ if (!iconURI) {
+ return false;
+ }
+
+ if (!mIconLoader) {
+ mIconLoader = new IconLoader(this);
+ }
+
+ nsresult rv = mIconLoader->LoadIcon(iconURI, aContent);
+ return NS_SUCCEEDED(rv);
+}
+
+already_AddRefed<nsIURI> nsMenuItemIconX::GetIconURI(nsIContent* aContent) {
+ // First, look at the content node's "image" attribute.
+ nsAutoString imageURIString;
+ bool hasImageAttr =
+ aContent->IsElement() &&
+ aContent->AsElement()->GetAttr(nsGkAtoms::image, imageURIString);
+
+ if (hasImageAttr) {
+ // Use the URL from the image attribute.
+ // If this menu item shouldn't have an icon, the string will be empty,
+ // and NS_NewURI will fail.
+ RefPtr<nsIURI> iconURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+ return iconURI.forget();
+ }
+
+ // If the content node has no "image" attribute, get the
+ // "list-style-image" property from CSS.
+ RefPtr<mozilla::dom::Document> document = aContent->GetComposedDoc();
+ if (!document || !aContent->IsElement()) {
+ return nullptr;
+ }
+
+ RefPtr<const ComputedStyle> sc =
+ nsComputedDOMStyle::GetComputedStyle(aContent->AsElement());
+ if (!sc) {
+ return nullptr;
+ }
+
+ RefPtr<nsIURI> iconURI = sc->StyleList()->GetListStyleImageURI();
+ if (!iconURI) {
+ return nullptr;
+ }
+
+ mComputedStyle = std::move(sc);
+ mPresContext = document->GetPresContext();
+
+ return iconURI.forget();
+}
+
+//
+// mozilla::widget::IconLoader::Listener
+//
+
+nsresult nsMenuItemIconX::OnComplete(imgIContainer* aImage) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mIconImage) {
+ [mIconImage release];
+ mIconImage = nil;
+ }
+ RefPtr<nsPresContext> pc = mPresContext.get();
+ mIconImage = [[MOZIconHelper
+ iconImageFromImageContainer:aImage
+ withSize:NSMakeSize(kIconSize, kIconSize)
+ presContext:pc
+ computedStyle:mComputedStyle
+ scaleFactor:0.0f] retain];
+ mComputedStyle = nullptr;
+ mPresContext = nullptr;
+
+ if (mListener) {
+ mListener->IconUpdated();
+ }
+
+ mIconLoader->Destroy();
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
diff --git a/widget/cocoa/nsMenuItemX.h b/widget/cocoa/nsMenuItemX.h
new file mode 100644
index 0000000000..980105e123
--- /dev/null
+++ b/widget/cocoa/nsMenuItemX.h
@@ -0,0 +1,105 @@
+/* -*- 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/. */
+
+#ifndef nsMenuItemX_h_
+#define nsMenuItemX_h_
+
+#include "mozilla/RefPtr.h"
+#include "nsISupports.h"
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuItemIconX.h"
+#include "nsChangeObserver.h"
+#include "nsStringFwd.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsMenuItemIconX;
+class nsMenuX;
+class nsMenuParentX;
+
+namespace mozilla {
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+enum {
+ knsMenuItemNoModifier = 0,
+ knsMenuItemShiftModifier = (1 << 0),
+ knsMenuItemAltModifier = (1 << 1),
+ knsMenuItemControlModifier = (1 << 2),
+ knsMenuItemCommandModifier = (1 << 3)
+};
+
+enum EMenuItemType {
+ eRegularMenuItemType = 0,
+ eCheckboxMenuItemType,
+ eRadioMenuItemType,
+ eSeparatorMenuItemType
+};
+
+// Once instantiated, this object lives until its DOM node or its parent window
+// is destroyed. Do not hold references to this, they can become invalid any
+// time the DOM node can be destroyed.
+class nsMenuItemX final : public nsChangeObserver,
+ public nsMenuItemIconX::Listener {
+ public:
+ nsMenuItemX(nsMenuX* aParent, const nsString& aLabel, EMenuItemType aItemType,
+ nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode);
+
+ bool IsVisible() const { return mIsVisible; }
+
+ // Unregisters nsMenuX from the nsMenuGroupOwner, and nulls out the group
+ // owner pointer. This is needed because nsMenuX is reference-counted and can
+ // outlive its owner, and the menu group owner asserts that everything has
+ // been unregistered when it is destroyed.
+ void DetachFromGroupOwner();
+
+ // Nulls out our reference to the parent.
+ // This is needed because nsMenuX is reference-counted and can outlive its
+ // parent.
+ void DetachFromParent() { mMenuParent = nullptr; }
+
+ NS_INLINE_DECL_REFCOUNTING(nsMenuItemX)
+
+ NS_DECL_CHANGEOBSERVER
+
+ // nsMenuItemIconX::Listener
+ void IconUpdated() override;
+
+ // nsMenuItemX
+ nsresult SetChecked(bool aIsChecked);
+ EMenuItemType GetMenuItemType();
+ void DoCommand(NSEventModifierFlags aModifierFlags, int16_t aButton);
+ nsresult DispatchDOMEvent(const nsString& eventName,
+ bool* preventDefaultCalled);
+ void SetupIcon();
+ nsMenuX* ParentMenu() { return mMenuParent; }
+ nsIContent* Content() { return mContent; }
+ NSMenuItem* NativeNSMenuItem() { return mNativeMenuItem; }
+
+ void Dump(uint32_t aIndent) const;
+
+ protected:
+ virtual ~nsMenuItemX();
+
+ void UncheckRadioSiblings(nsIContent* aCheckedElement);
+ void SetKeyEquiv();
+
+ nsCOMPtr<nsIContent> mContent; // XUL <menuitem> or <menuseparator>
+
+ EMenuItemType mType;
+
+ // nsMenuItemX objects should always have a valid native menu item.
+ NSMenuItem* mNativeMenuItem = nil; // [strong]
+ nsMenuX* mMenuParent = nullptr; // [weak]
+ nsMenuGroupOwnerX* mMenuGroupOwner = nullptr; // [weak]
+ RefPtr<mozilla::dom::Element> mCommandElement;
+ mozilla::UniquePtr<nsMenuItemIconX> mIcon; // always non-null
+ bool mIsChecked = false;
+ bool mIsVisible = false;
+};
+
+#endif // nsMenuItemX_h_
diff --git a/widget/cocoa/nsMenuItemX.mm b/widget/cocoa/nsMenuItemX.mm
new file mode 100644
index 0000000000..26241217dc
--- /dev/null
+++ b/widget/cocoa/nsMenuItemX.mm
@@ -0,0 +1,449 @@
+/* -*- 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 "nsMenuItemX.h"
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemIconX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+
+#include "nsObjCExceptions.h"
+
+#include "nsCOMPtr.h"
+#include "nsGkAtoms.h"
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/ErrorResult.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+
+using mozilla::dom::CallerType;
+using mozilla::dom::Event;
+
+nsMenuItemX::nsMenuItemX(nsMenuX* aParent, const nsString& aLabel,
+ EMenuItemType aItemType,
+ nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aNode)
+ : mContent(aNode),
+ mType(aItemType),
+ mMenuParent(aParent),
+ mMenuGroupOwner(aMenuGroupOwner) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ MOZ_COUNT_CTOR(nsMenuItemX);
+
+ MOZ_RELEASE_ASSERT(mContent->IsElement(),
+ "nsMenuItemX should only be created for elements");
+ NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one!");
+
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+
+ dom::Document* doc = mContent->GetUncomposedDoc();
+
+ // if we have a command associated with this menu item, register for changes
+ // to the command DOM node
+ if (doc) {
+ nsAutoString ourCommand;
+ mContent->AsElement()->GetAttr(nsGkAtoms::command, ourCommand);
+
+ if (!ourCommand.IsEmpty()) {
+ dom::Element* commandElement = doc->GetElementById(ourCommand);
+
+ if (commandElement) {
+ mCommandElement = commandElement;
+ // register to observe the command DOM element
+ mMenuGroupOwner->RegisterForContentChanges(mCommandElement, this);
+ }
+ }
+ }
+
+ // decide enabled state based on command content if it exists, otherwise do it
+ // based on our own content
+ bool isEnabled;
+ if (mCommandElement) {
+ isEnabled = !mCommandElement->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
+ } else {
+ isEnabled = !mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true, eCaseMatters);
+ }
+
+ // set up the native menu item
+ if (mType == eSeparatorMenuItemType) {
+ mNativeMenuItem = [[NSMenuItem separatorItem] retain];
+ } else {
+ NSString* newCocoaLabelString =
+ nsMenuUtilsX::GetTruncatedCocoaLabel(aLabel);
+ mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
+ action:nil
+ keyEquivalent:@""];
+
+ mIsChecked = mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true, eCaseMatters);
+
+ mNativeMenuItem.enabled = isEnabled;
+ mNativeMenuItem.state =
+ mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
+
+ SetKeyEquiv();
+ }
+
+ mIcon = MakeUnique<nsMenuItemIconX>(this);
+
+ mIsVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ // All menu items other than the "Copy" menu item share the same target and
+ // action, and are differentiated be a unique (representedObject, tag) pair.
+ // The "Copy" menu item is a special case that requires a macOS-default
+ // action of `copy:` and a default target in order for the "Edit" menu to be
+ // populated with OS-provided menu items such as the Emoji picker,
+ // especially in multi-language environments (see bug 1478347). Our
+ // application delegate implements `copy:` by simply forwarding it to
+ // [nsMenuBarX::sNativeEventTarget menuItemHit:].
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id,
+ u"menu_copy"_ns, eCaseMatters)) {
+ mNativeMenuItem.action = @selector(copy:);
+ } else {
+ mNativeMenuItem.action = @selector(menuItemHit:);
+ mNativeMenuItem.target = nsMenuBarX::sNativeEventTarget;
+ }
+
+ mNativeMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
+ mNativeMenuItem.tag = mMenuGroupOwner->RegisterForCommand(this);
+
+ if (mIsVisible) {
+ SetupIcon();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuItemX::~nsMenuItemX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // autorelease the native menu item so that anything else happening to this
+ // object happens before the native menu item actually dies
+ [mNativeMenuItem autorelease];
+
+ DetachFromGroupOwner();
+
+ MOZ_COUNT_DTOR(nsMenuItemX);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuItemX::DetachFromGroupOwner() {
+ if (mMenuGroupOwner) {
+ mMenuGroupOwner->UnregisterCommand(mNativeMenuItem.tag);
+
+ if (mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+ if (mCommandElement) {
+ mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
+ }
+ }
+
+ mMenuGroupOwner = nullptr;
+}
+
+nsresult nsMenuItemX::SetChecked(bool aIsChecked) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mIsChecked = aIsChecked;
+
+ // update the content model. This will also handle unchecking our siblings
+ // if we are a radiomenu
+ if (mIsChecked) {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ u"true"_ns, true);
+ } else {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ true);
+ }
+
+ // update native menu item
+ mNativeMenuItem.state =
+ mIsChecked ? NSControlStateValueOn : NSControlStateValueOff;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+EMenuItemType nsMenuItemX::GetMenuItemType() { return mType; }
+
+// Executes the "cached" javaScript command.
+// Returns NS_OK if the command was executed properly, otherwise an error code.
+void nsMenuItemX::DoCommand(NSEventModifierFlags aModifierFlags,
+ int16_t aButton) {
+ // flip "checked" state if we're a checkbox menu, or an un-checked radio menu
+ if (mType == eCheckboxMenuItemType ||
+ (mType == eRadioMenuItemType && !mIsChecked)) {
+ if (!mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::autocheck,
+ nsGkAtoms::_false, eCaseMatters)) {
+ SetChecked(!mIsChecked);
+ }
+ /* the AttributeChanged code will update all the internal state */
+ }
+
+ nsMenuUtilsX::DispatchCommandTo(mContent, aModifierFlags, aButton);
+}
+
+nsresult nsMenuItemX::DispatchDOMEvent(const nsString& eventName,
+ bool* preventDefaultCalled) {
+ if (!mContent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get owner document for content
+ nsCOMPtr<dom::Document> parentDoc = mContent->OwnerDoc();
+
+ // create DOM event
+ ErrorResult rv;
+ RefPtr<Event> event =
+ parentDoc->CreateEvent(u"Events"_ns, CallerType::System, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to create Event");
+ return rv.StealNSResult();
+ }
+ event->InitEvent(eventName, true, true);
+
+ // mark DOM event as trusted
+ event->SetTrusted(true);
+
+ // send DOM event
+ *preventDefaultCalled =
+ mContent->DispatchEvent(*event, CallerType::System, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to send DOM event via EventTarget");
+ return rv.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+// Walk the sibling list looking for nodes with the same name and
+// uncheck them all.
+void nsMenuItemX::UncheckRadioSiblings(nsIContent* aCheckedContent) {
+ nsAutoString myGroupName;
+ aCheckedContent->AsElement()->GetAttr(nsGkAtoms::name, myGroupName);
+ if (!myGroupName.Length()) { // no groupname, nothing to do
+ return;
+ }
+
+ nsCOMPtr<nsIContent> parent = aCheckedContent->GetParent();
+ if (!parent) {
+ return;
+ }
+
+ // loop over siblings
+ for (nsIContent* sibling = parent->GetFirstChild(); sibling;
+ sibling = sibling->GetNextSibling()) {
+ if (sibling != aCheckedContent && sibling->IsElement()) { // skip this node
+ // if the current sibling is in the same group, clear it
+ if (sibling->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
+ myGroupName, eCaseMatters)) {
+ sibling->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ u"false"_ns, true);
+ }
+ }
+ }
+}
+
+void nsMenuItemX::SetKeyEquiv() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Set key shortcut and modifiers
+ nsAutoString keyValue;
+ mContent->AsElement()->GetAttr(nsGkAtoms::key, keyValue);
+
+ if (!keyValue.IsEmpty() && mContent->GetUncomposedDoc()) {
+ dom::Element* keyContent =
+ mContent->GetUncomposedDoc()->GetElementById(keyValue);
+ if (keyContent) {
+ nsAutoString keyChar;
+ bool hasKey = keyContent->GetAttr(nsGkAtoms::key, keyChar);
+
+ if (!hasKey || keyChar.IsEmpty()) {
+ nsAutoString keyCodeName;
+ keyContent->GetAttr(nsGkAtoms::keycode, keyCodeName);
+ uint32_t charCode =
+ nsCocoaUtils::ConvertGeckoNameToMacCharCode(keyCodeName);
+ if (charCode) {
+ keyChar.Assign(charCode);
+ } else {
+ keyChar.AssignLiteral(u" ");
+ }
+ }
+
+ nsAutoString modifiersStr;
+ keyContent->GetAttr(nsGkAtoms::modifiers, modifiersStr);
+ uint8_t modifiers =
+ nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
+
+ unsigned int macModifiers =
+ nsMenuUtilsX::MacModifiersForGeckoModifiers(modifiers);
+ mNativeMenuItem.keyEquivalentModifierMask = macModifiers;
+
+ NSString* keyEquivalent =
+ [[NSString stringWithCharacters:(unichar*)keyChar.get()
+ length:keyChar.Length()] lowercaseString];
+ if ([keyEquivalent isEqualToString:@" "]) {
+ mNativeMenuItem.keyEquivalent = @"";
+ } else {
+ mNativeMenuItem.keyEquivalent = keyEquivalent;
+ }
+
+ return;
+ }
+ }
+
+ // if the key was removed, clear the key
+ mNativeMenuItem.keyEquivalent = @"";
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuItemX::Dump(uint32_t aIndent) const {
+ printf("%*s - item [%p] %-16s <%s>\n", aIndent * 2, "", this,
+ mType == eSeparatorMenuItemType ? "----"
+ : [mNativeMenuItem.title UTF8String],
+ NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
+}
+
+//
+// nsChangeObserver
+//
+
+void nsMenuItemX::ObserveAttributeChanged(dom::Document* aDocument,
+ nsIContent* aContent,
+ nsAtom* aAttribute) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (!aContent) {
+ return;
+ }
+
+ if (aContent == mContent) { // our own content node changed
+ if (aAttribute == nsGkAtoms::checked) {
+ // if we're a radio menu, uncheck our sibling radio items. No need to
+ // do any of this if we're just a normal check menu.
+ if (mType == eRadioMenuItemType &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::checked,
+ nsGkAtoms::_true, eCaseMatters)) {
+ UncheckRadioSiblings(mContent);
+ }
+ mMenuParent->SetRebuild(true);
+ } else if (aAttribute == nsGkAtoms::hidden ||
+ aAttribute == nsGkAtoms::collapsed) {
+ bool isVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+ if (isVisible != mIsVisible) {
+ mIsVisible = isVisible;
+ RefPtr<nsMenuItemX> self = this;
+ mMenuParent->MenuChildChangedVisibility(nsMenuParentX::MenuChild(self),
+ isVisible);
+ if (mIsVisible) {
+ SetupIcon();
+ }
+ }
+ mMenuParent->SetRebuild(true);
+ } else if (aAttribute == nsGkAtoms::label) {
+ if (mType != eSeparatorMenuItemType) {
+ nsAutoString newLabel;
+ mContent->AsElement()->GetAttr(nsGkAtoms::label, newLabel);
+ mNativeMenuItem.title = nsMenuUtilsX::GetTruncatedCocoaLabel(newLabel);
+ }
+ } else if (aAttribute == nsGkAtoms::key) {
+ SetKeyEquiv();
+ } else if (aAttribute == nsGkAtoms::image) {
+ SetupIcon();
+ } else if (aAttribute == nsGkAtoms::disabled) {
+ mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
+ eCaseMatters);
+ }
+ } else if (aContent == mCommandElement) {
+ // the only thing that really matters when the menu isn't showing is the
+ // enabled state since it enables/disables keyboard commands
+ if (aAttribute == nsGkAtoms::disabled) {
+ // first we sync our menu item DOM node with the command DOM node
+ nsAutoString commandDisabled;
+ nsAutoString menuDisabled;
+ aContent->AsElement()->GetAttr(nsGkAtoms::disabled, commandDisabled);
+ mContent->AsElement()->GetAttr(nsGkAtoms::disabled, menuDisabled);
+ if (!commandDisabled.Equals(menuDisabled)) {
+ // The menu's disabled state needs to be updated to match the command.
+ if (commandDisabled.IsEmpty()) {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::disabled, true);
+ } else {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ commandDisabled, true);
+ }
+ }
+ // now we sync our native menu item with the command DOM node
+ mNativeMenuItem.enabled = !aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
+ eCaseMatters);
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+bool IsMenuStructureElement(nsIContent* aContent) {
+ return aContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem,
+ nsGkAtoms::menuseparator);
+}
+
+void nsMenuItemX::ObserveContentRemoved(dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ MOZ_RELEASE_ASSERT(mMenuGroupOwner);
+ MOZ_RELEASE_ASSERT(mMenuParent);
+
+ if (aChild == mCommandElement) {
+ mMenuGroupOwner->UnregisterForContentChanges(mCommandElement);
+ mCommandElement = nullptr;
+ }
+ if (IsMenuStructureElement(aChild)) {
+ mMenuParent->SetRebuild(true);
+ }
+}
+
+void nsMenuItemX::ObserveContentInserted(dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild) {
+ MOZ_RELEASE_ASSERT(mMenuParent);
+
+ // The child node could come from the custom element that is for display, so
+ // only rebuild the menu if the child is related to the structure of the
+ // menu.
+ if (IsMenuStructureElement(aChild)) {
+ mMenuParent->SetRebuild(true);
+ }
+}
+
+void nsMenuItemX::SetupIcon() {
+ if (mType != eRegularMenuItemType) {
+ // Don't support icons on checkbox and radio menuitems, for consistency with
+ // Windows & Linux.
+ return;
+ }
+
+ mIcon->SetupIcon(mContent);
+ mNativeMenuItem.image = mIcon->GetIconImage();
+}
+
+void nsMenuItemX::IconUpdated() {
+ mNativeMenuItem.image = mIcon->GetIconImage();
+}
diff --git a/widget/cocoa/nsMenuParentX.h b/widget/cocoa/nsMenuParentX.h
new file mode 100644
index 0000000000..a1f705631a
--- /dev/null
+++ b/widget/cocoa/nsMenuParentX.h
@@ -0,0 +1,31 @@
+/* -*- 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/. */
+
+#ifndef nsMenuParentX_h_
+#define nsMenuParentX_h_
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/Variant.h"
+
+class nsMenuX;
+class nsMenuBarX;
+class nsMenuItemX;
+
+// A base class for objects that can be the parent of an nsMenuX or nsMenuItemX.
+class nsMenuParentX {
+ public:
+ using MenuChild = mozilla::Variant<RefPtr<nsMenuX>, RefPtr<nsMenuItemX>>;
+
+ // XXXmstange double-check that this is still needed
+ virtual nsMenuBarX* AsMenuBar() { return nullptr; }
+
+ // Called when aChild becomes visible or hidden, so that the parent can insert
+ // or remove the child's native menu item from its NSMenu and update its state
+ // of visible items.
+ virtual void MenuChildChangedVisibility(const MenuChild& aChild,
+ bool aIsVisible) = 0;
+};
+
+#endif // nsMenuParentX_h_
diff --git a/widget/cocoa/nsMenuUtilsX.h b/widget/cocoa/nsMenuUtilsX.h
new file mode 100644
index 0000000000..5b8993713b
--- /dev/null
+++ b/widget/cocoa/nsMenuUtilsX.h
@@ -0,0 +1,53 @@
+/* -*- 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/. */
+
+#ifndef nsMenuUtilsX_h_
+#define nsMenuUtilsX_h_
+
+#include "nscore.h"
+#include "nsStringFwd.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsIContent;
+class nsMenuBarX;
+class nsMenuX;
+
+// Namespace containing utility functions used in our native menu
+// implementation.
+namespace nsMenuUtilsX {
+void DispatchCommandTo(nsIContent* aTargetContent,
+ NSEventModifierFlags aModifierFlags, int16_t aButton);
+NSString* GetTruncatedCocoaLabel(const nsString& itemLabel);
+uint8_t GeckoModifiersForNodeAttribute(const nsString& modifiersAttribute);
+unsigned int MacModifiersForGeckoModifiers(uint8_t geckoModifiers);
+nsMenuBarX* GetHiddenWindowMenuBar(); // returned object is not retained
+NSMenuItem* GetStandardEditMenuItem(); // returned object is not retained
+bool NodeIsHiddenOrCollapsed(nsIContent* aContent);
+
+// Find the menu item by following the path aLocationString from aRootMenu.
+// aLocationString is a '|'-separated list of integers, where each integer is
+// the index of the menu item in the menu.
+// aIsMenuBar needs to be true if aRootMenu is the app's mainMenu, so that the
+// app menu can be skipped during the search.
+NSMenuItem* NativeMenuItemWithLocation(NSMenu* aRootMenu,
+ NSString* aLocationString,
+ bool aIsMenuBar);
+
+// Traverse the menu tree and check that there are no cycles or NSMenu(Item)
+// objects that are used more than once. If inconsistencies are found, these
+// functions crash the process.
+void CheckNativeMenuConsistency(NSMenu* aMenu);
+void CheckNativeMenuConsistency(NSMenuItem* aMenuItem);
+
+// Print out debugging information about the native menu tree structure.
+void DumpNativeMenu(NSMenu* aMenu);
+void DumpNativeMenuItem(NSMenuItem* aMenuItem);
+
+extern bool gIsSynchronouslyActivatingNativeMenuItemDuringTest;
+
+} // namespace nsMenuUtilsX
+
+#endif // nsMenuUtilsX_h_
diff --git a/widget/cocoa/nsMenuUtilsX.mm b/widget/cocoa/nsMenuUtilsX.mm
new file mode 100644
index 0000000000..9a950221cd
--- /dev/null
+++ b/widget/cocoa/nsMenuUtilsX.mm
@@ -0,0 +1,316 @@
+/* -*- 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 "nsMenuUtilsX.h"
+#include <unordered_set>
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/XULCommandEvent.h"
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemX.h"
+#include "NativeMenuMac.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsCocoaWindow.h"
+#include "nsGkAtoms.h"
+#include "nsGlobalWindowInner.h"
+#include "nsPIDOMWindow.h"
+#include "nsQueryObject.h"
+
+using namespace mozilla;
+
+bool nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = false;
+
+void nsMenuUtilsX::DispatchCommandTo(nsIContent* aTargetContent,
+ NSEventModifierFlags aModifierFlags,
+ int16_t aButton) {
+ MOZ_ASSERT(aTargetContent, "null ptr");
+
+ dom::Document* doc = aTargetContent->OwnerDoc();
+ if (doc) {
+ RefPtr<dom::XULCommandEvent> event =
+ new dom::XULCommandEvent(doc, doc->GetPresContext(), nullptr);
+
+ bool ctrlKey = aModifierFlags & NSEventModifierFlagControl;
+ bool altKey = aModifierFlags & NSEventModifierFlagOption;
+ bool shiftKey = aModifierFlags & NSEventModifierFlagShift;
+ bool cmdKey = aModifierFlags & NSEventModifierFlagCommand;
+
+ IgnoredErrorResult rv;
+ event->InitCommandEvent(u"command"_ns, true, true,
+ nsGlobalWindowInner::Cast(doc->GetInnerWindow()), 0,
+ ctrlKey, altKey, shiftKey, cmdKey, aButton, nullptr,
+ 0, rv);
+ if (!rv.Failed()) {
+ event->SetTrusted(true);
+ aTargetContent->DispatchEvent(*event);
+ }
+ }
+}
+
+NSString* nsMenuUtilsX::GetTruncatedCocoaLabel(const nsString& itemLabel) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // We want to truncate long strings to some reasonable pixel length but there
+ // is no good API for doing that which works for all OS versions and
+ // architectures. For now we'll do nothing for consistency and depend on good
+ // user interface design to limit string lengths.
+ return [NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(itemLabel.get())
+ length:itemLabel.Length()];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+uint8_t nsMenuUtilsX::GeckoModifiersForNodeAttribute(
+ const nsString& modifiersAttribute) {
+ uint8_t modifiers = knsMenuItemNoModifier;
+ char* str = ToNewCString(modifiersAttribute);
+ char* newStr;
+ char* token = strtok_r(str, ", \t", &newStr);
+ while (token != nullptr) {
+ if (strcmp(token, "shift") == 0) {
+ modifiers |= knsMenuItemShiftModifier;
+ } else if (strcmp(token, "alt") == 0) {
+ modifiers |= knsMenuItemAltModifier;
+ } else if (strcmp(token, "control") == 0) {
+ modifiers |= knsMenuItemControlModifier;
+ } else if ((strcmp(token, "accel") == 0) || (strcmp(token, "meta") == 0)) {
+ modifiers |= knsMenuItemCommandModifier;
+ }
+ token = strtok_r(newStr, ", \t", &newStr);
+ }
+ free(str);
+
+ return modifiers;
+}
+
+unsigned int nsMenuUtilsX::MacModifiersForGeckoModifiers(
+ uint8_t geckoModifiers) {
+ unsigned int macModifiers = 0;
+
+ if (geckoModifiers & knsMenuItemShiftModifier) {
+ macModifiers |= NSEventModifierFlagShift;
+ }
+ if (geckoModifiers & knsMenuItemAltModifier) {
+ macModifiers |= NSEventModifierFlagOption;
+ }
+ if (geckoModifiers & knsMenuItemControlModifier) {
+ macModifiers |= NSEventModifierFlagControl;
+ }
+ if (geckoModifiers & knsMenuItemCommandModifier) {
+ macModifiers |= NSEventModifierFlagCommand;
+ }
+
+ return macModifiers;
+}
+
+nsMenuBarX* nsMenuUtilsX::GetHiddenWindowMenuBar() {
+ if (gfxPlatform::IsHeadless()) {
+ return nullptr;
+ }
+ nsIWidget* hiddenWindowWidgetNoCOMPtr = nsCocoaUtils::GetHiddenWindowWidget();
+ if (hiddenWindowWidgetNoCOMPtr) {
+ return static_cast<nsCocoaWindow*>(hiddenWindowWidgetNoCOMPtr)
+ ->GetMenuBar();
+ }
+ return nullptr;
+}
+
+// It would be nice if we could localize these edit menu names.
+NSMenuItem* nsMenuUtilsX::GetStandardEditMenuItem() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // In principle we should be able to allocate this once and then always
+ // return the same object. But weird interactions happen between native
+ // app-modal dialogs and Gecko-modal dialogs that open above them. So what
+ // we return here isn't always released before it needs to be added to
+ // another menu. See bmo bug 468393.
+ NSMenuItem* standardEditMenuItem =
+ [[[NSMenuItem alloc] initWithTitle:@"Edit" action:nil
+ keyEquivalent:@""] autorelease];
+ NSMenu* standardEditMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
+ standardEditMenuItem.submenu = standardEditMenu;
+ [standardEditMenu release];
+
+ // Add Undo
+ NSMenuItem* undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo"
+ action:@selector(undo:)
+ keyEquivalent:@"z"];
+ [standardEditMenu addItem:undoItem];
+ [undoItem release];
+
+ // Add Redo
+ NSMenuItem* redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo"
+ action:@selector(redo:)
+ keyEquivalent:@"Z"];
+ [standardEditMenu addItem:redoItem];
+ [redoItem release];
+
+ // Add separator
+ [standardEditMenu addItem:[NSMenuItem separatorItem]];
+
+ // Add Cut
+ NSMenuItem* cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut"
+ action:@selector(cut:)
+ keyEquivalent:@"x"];
+ [standardEditMenu addItem:cutItem];
+ [cutItem release];
+
+ // Add Copy
+ NSMenuItem* copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy"
+ action:@selector(copy:)
+ keyEquivalent:@"c"];
+ [standardEditMenu addItem:copyItem];
+ [copyItem release];
+
+ // Add Paste
+ NSMenuItem* pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste"
+ action:@selector(paste:)
+ keyEquivalent:@"v"];
+ [standardEditMenu addItem:pasteItem];
+ [pasteItem release];
+
+ // Add Delete
+ NSMenuItem* deleteItem = [[NSMenuItem alloc] initWithTitle:@"Delete"
+ action:@selector(delete:)
+ keyEquivalent:@""];
+ [standardEditMenu addItem:deleteItem];
+ [deleteItem release];
+
+ // Add Select All
+ NSMenuItem* selectAllItem =
+ [[NSMenuItem alloc] initWithTitle:@"Select All"
+ action:@selector(selectAll:)
+ keyEquivalent:@"a"];
+ [standardEditMenu addItem:selectAllItem];
+ [selectAllItem release];
+
+ return standardEditMenuItem;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+bool nsMenuUtilsX::NodeIsHiddenOrCollapsed(nsIContent* aContent) {
+ return aContent->IsElement() && (aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters) ||
+ aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::collapsed,
+ nsGkAtoms::_true, eCaseMatters));
+}
+
+NSMenuItem* nsMenuUtilsX::NativeMenuItemWithLocation(NSMenu* aRootMenu,
+ NSString* aLocationString,
+ bool aIsMenuBar) {
+ NSArray<NSString*>* indexes =
+ [aLocationString componentsSeparatedByString:@"|"];
+ unsigned int pathLength = indexes.count;
+ if (pathLength == 0) {
+ return nil;
+ }
+
+ NSMenu* currentSubmenu = aRootMenu;
+ for (unsigned int depth = 0; depth < pathLength; depth++) {
+ NSInteger targetIndex = [indexes objectAtIndex:depth].integerValue;
+ if (aIsMenuBar && depth == 0) {
+ // We remove the application menu from consideration for the top-level
+ // menu.
+ targetIndex++;
+ }
+ int itemCount = currentSubmenu.numberOfItems;
+ if (targetIndex >= itemCount) {
+ return nil;
+ }
+ NSMenuItem* menuItem = [currentSubmenu itemAtIndex:targetIndex];
+ // if this is the last index just return the menu item
+ if (depth == pathLength - 1) {
+ return menuItem;
+ }
+ // if this is not the last index find the submenu and keep going
+ if (menuItem.hasSubmenu) {
+ currentSubmenu = menuItem.submenu;
+ } else {
+ return nil;
+ }
+ }
+
+ return nil;
+}
+
+static void CheckNativeMenuConsistencyImpl(
+ NSMenu* aMenu, std::unordered_set<void*>& aSeenObjects);
+
+static void CheckNativeMenuItemConsistencyImpl(
+ NSMenuItem* aMenuItem, std::unordered_set<void*>& aSeenObjects) {
+ bool inserted = aSeenObjects.insert(aMenuItem).second;
+ MOZ_RELEASE_ASSERT(inserted,
+ "Duplicate NSMenuItem object in native menu structure");
+ if (aMenuItem.hasSubmenu) {
+ CheckNativeMenuConsistencyImpl(aMenuItem.submenu, aSeenObjects);
+ }
+}
+
+static void CheckNativeMenuConsistencyImpl(
+ NSMenu* aMenu, std::unordered_set<void*>& aSeenObjects) {
+ bool inserted = aSeenObjects.insert(aMenu).second;
+ MOZ_RELEASE_ASSERT(inserted,
+ "Duplicate NSMenu object in native menu structure");
+ for (NSMenuItem* item in aMenu.itemArray) {
+ CheckNativeMenuItemConsistencyImpl(item, aSeenObjects);
+ }
+}
+
+void nsMenuUtilsX::CheckNativeMenuConsistency(NSMenu* aMenu) {
+ std::unordered_set<void*> seenObjects;
+ CheckNativeMenuConsistencyImpl(aMenu, seenObjects);
+}
+
+void nsMenuUtilsX::CheckNativeMenuConsistency(NSMenuItem* aMenuItem) {
+ std::unordered_set<void*> seenObjects;
+ CheckNativeMenuItemConsistencyImpl(aMenuItem, seenObjects);
+}
+
+static void DumpNativeNSMenuItemImpl(NSMenuItem* aItem, uint32_t aIndent,
+ const Maybe<int>& aIndexInParentMenu);
+
+static void DumpNativeNSMenuImpl(NSMenu* aMenu, uint32_t aIndent) {
+ printf("%*sNSMenu [%p] %-16s\n", aIndent * 2, "", aMenu,
+ (aMenu.title.length == 0 ? "(no title)" : aMenu.title.UTF8String));
+ int index = 0;
+ for (NSMenuItem* item in aMenu.itemArray) {
+ DumpNativeNSMenuItemImpl(item, aIndent + 1, Some(index));
+ index++;
+ }
+}
+
+static void DumpNativeNSMenuItemImpl(NSMenuItem* aItem, uint32_t aIndent,
+ const Maybe<int>& aIndexInParentMenu) {
+ printf("%*s", aIndent * 2, "");
+ if (aIndexInParentMenu) {
+ printf("[%d] ", *aIndexInParentMenu);
+ }
+ printf(
+ "NSMenuItem [%p] %-16s%s\n", aItem,
+ aItem.isSeparatorItem
+ ? "----"
+ : (aItem.title.length == 0 ? "(no title)" : aItem.title.UTF8String),
+ aItem.hasSubmenu ? " [hasSubmenu]" : "");
+ if (aItem.hasSubmenu) {
+ DumpNativeNSMenuImpl(aItem.submenu, aIndent + 1);
+ }
+}
+
+void nsMenuUtilsX::DumpNativeMenu(NSMenu* aMenu) {
+ DumpNativeNSMenuImpl(aMenu, 0);
+}
+
+void nsMenuUtilsX::DumpNativeMenuItem(NSMenuItem* aMenuItem) {
+ DumpNativeNSMenuItemImpl(aMenuItem, 0, Nothing());
+}
diff --git a/widget/cocoa/nsMenuX.h b/widget/cocoa/nsMenuX.h
new file mode 100644
index 0000000000..cdc5884a99
--- /dev/null
+++ b/widget/cocoa/nsMenuX.h
@@ -0,0 +1,313 @@
+/* -*- 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/. */
+
+#ifndef nsMenuX_h_
+#define nsMenuX_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Variant.h"
+#include "nsISupports.h"
+#include "nsMenuParentX.h"
+#include "nsMenuBarX.h"
+#include "nsMenuGroupOwnerX.h"
+#include "nsMenuItemIconX.h"
+#include "nsCOMPtr.h"
+#include "nsChangeObserver.h"
+#include "nsThreadUtils.h"
+
+class nsMenuX;
+class nsMenuItemX;
+class nsIWidget;
+
+// MenuDelegate is used to receive Cocoa notifications for setting
+// up carbon events. Protocol is defined as of 10.6 SDK.
+@interface MenuDelegate : NSObject <NSMenuDelegate> {
+ nsMenuX* mGeckoMenu; // weak ref
+ NSMutableArray* mBlocksToRunWhenOpen;
+}
+- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu;
+- (void)runBlockWhenOpen:(void (^)())block;
+- (void)menu:(NSMenu*)menu willActivateItem:(NSMenuItem*)item;
+@end
+
+class nsMenuXObserver {
+ public:
+ // Called when a menu in this menu subtree opens, before popupshowing.
+ // No strong reference is held to the observer during the call.
+ virtual void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) = 0;
+
+ // Called when a menu in this menu subtree opened, after popupshown.
+ // No strong reference is held to the observer during the call.
+ virtual void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) = 0;
+
+ // Called before a menu item is activated.
+ virtual void OnMenuWillActivateItem(
+ mozilla::dom::Element* aPopupElement,
+ mozilla::dom::Element* aMenuItemElement) = 0;
+
+ // Called when a menu in this menu subtree closed, after popuphidden.
+ // No strong reference is held to the observer during the call.
+ virtual void OnMenuClosed(mozilla::dom::Element* aPopupElement) = 0;
+};
+
+// Once instantiated, this object lives until its DOM node or its parent window
+// is destroyed. Do not hold references to this, they can become invalid any
+// time the DOM node can be destroyed.
+class nsMenuX final : public nsMenuParentX,
+ public nsChangeObserver,
+ public nsMenuItemIconX::Listener,
+ public nsMenuXObserver {
+ public:
+ using Observer = nsMenuXObserver;
+
+ // aParent is optional.
+ nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner,
+ nsIContent* aContent);
+
+ NS_INLINE_DECL_REFCOUNTING(nsMenuX)
+
+ // If > 0, the OS is indexing all the app's menus (triggered by opening
+ // Help menu on Leopard and higher). There are some things that are
+ // unsafe to do while this is happening.
+ static int32_t sIndexingMenuLevel;
+
+ NS_DECL_CHANGEOBSERVER
+
+ // nsMenuItemIconX::Listener
+ void IconUpdated() override;
+
+ // nsMenuXObserver, to forward notifications from our children to our
+ // observer.
+ void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnMenuWillActivateItem(mozilla::dom::Element* aPopupElement,
+ mozilla::dom::Element* aMenuItemElement) override;
+ void OnMenuClosed(mozilla::dom::Element* aPopupElement) override;
+
+ bool IsVisible() const { return mVisible; }
+
+ // Unregisters nsMenuX from the nsMenuGroupOwner, and nulls out the group
+ // owner pointer, on this nsMenuX and also all nested nsMenuX and nsMenuItemX
+ // objects. This is needed because nsMenuX is reference-counted and can
+ // outlive its owner, and the menu group owner asserts that everything has
+ // been unregistered when it is destroyed.
+ void DetachFromGroupOwnerRecursive();
+
+ // Nulls out our reference to the parent.
+ // This is needed because nsMenuX is reference-counted and can outlive its
+ // parent.
+ void DetachFromParent() { mParent = nullptr; }
+
+ mozilla::Maybe<MenuChild> GetItemAt(uint32_t aPos);
+ uint32_t GetItemCount();
+
+ mozilla::Maybe<MenuChild> GetVisibleItemAt(uint32_t aPos);
+ nsresult GetVisibleItemCount(uint32_t& aCount);
+
+ mozilla::Maybe<MenuChild> GetItemForElement(
+ mozilla::dom::Element* aMenuChildElement);
+
+ // Asynchronously runs the command event on aItem, after the root menu has
+ // closed.
+ void ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem,
+ NSEventModifierFlags aModifiers,
+ int16_t aButton);
+
+ bool IsOpenForGecko() const { return mIsOpenForGecko; }
+
+ // Fires the popupshowing event and returns whether the handler allows the
+ // popup to open. When calling this method, the caller must hold a strong
+ // reference to this object, because other references to this object can be
+ // dropped during the handling of the DOM event.
+ MOZ_CAN_RUN_SCRIPT bool OnOpen();
+
+ void PopupShowingEventWasSentAndApprovedExternally() {
+ DidFirePopupShowing();
+ }
+
+ // Called from the menu delegate during menuWillOpen, or to simulate opening.
+ // Ignored if the menu is already considered open.
+ // When calling this method, the caller must hold a strong reference to this
+ // object, because other references to this object can be dropped during the
+ // handling of the DOM event.
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void MenuOpened();
+
+ // Called from the menu delegate during menuDidClose, or to simulate closing.
+ // Ignored if the menu is already considered closed.
+ // When calling this method, the caller must hold a strong reference to this
+ // object, because other references to this object can be dropped during the
+ // handling of the DOM event.
+ void MenuClosed();
+
+ // Close the menu if it's open, and flush any pending popuphiding /
+ // popuphidden events.
+ bool Close();
+
+ // Called from the menu delegate during menu:willHighlightItem:.
+ // If called with Nothing(), it means that no item is highlighted.
+ // The index only accounts for visible items, i.e. items for which there
+ // exists an NSMenuItem* in mNativeMenu.
+ void OnHighlightedItemChanged(
+ const mozilla::Maybe<uint32_t>& aNewHighlightedIndex);
+
+ // Called from the menu delegate before an item anywhere in this menu is
+ // activated. Called after MenuClosed().
+ void OnWillActivateItem(NSMenuItem* aItem);
+
+ void SetRebuild(bool aMenuEvent);
+ void SetupIcon();
+ nsIContent* Content() { return mContent; }
+ NSMenuItem* NativeNSMenuItem() { return mNativeMenuItem; }
+ GeckoNSMenu* NativeNSMenu() { return mNativeMenu; }
+
+ void SetIconListener(nsMenuItemIconX::Listener* aListener) {
+ mIconListener = aListener;
+ }
+ void ClearIconListener() { mIconListener = nullptr; }
+
+ // nsMenuParentX
+ void MenuChildChangedVisibility(const MenuChild& aChild,
+ bool aIsVisible) override;
+
+ void Dump(uint32_t aIndent) const;
+
+ static bool IsXULHelpMenu(nsIContent* aMenuContent);
+ static bool IsXULWindowMenu(nsIContent* aMenuContent);
+
+ // Set an observer that gets notified of menu opening and closing.
+ // The menu does not keep a strong reference the observer. The observer must
+ // remove itself before it is destroyed.
+ void SetObserver(Observer* aObserver) { mObserver = aObserver; }
+
+ // Stop observing.
+ void ClearObserver() { mObserver = nullptr; }
+
+ protected:
+ virtual ~nsMenuX();
+
+ void RebuildMenu();
+ nsresult RemoveAll();
+ nsresult SetEnabled(bool aIsEnabled);
+ nsresult GetEnabled(bool* aIsEnabled);
+ already_AddRefed<nsIContent> GetMenuPopupContent();
+ void WillInsertChild(const MenuChild& aChild);
+ void WillRemoveChild(const MenuChild& aChild);
+ void AddMenuChild(MenuChild&& aChild);
+ void InsertMenuChild(MenuChild&& aChild);
+ void RemoveMenuChild(const MenuChild& aChild);
+ mozilla::Maybe<MenuChild> CreateMenuChild(nsIContent* aContent);
+ RefPtr<nsMenuItemX> CreateMenuItem(nsIContent* aMenuItemContent);
+ GeckoNSMenu* CreateMenuWithGeckoString(nsString& aMenuTitle,
+ bool aShowServices);
+ void DidFirePopupShowing();
+
+ // Find the index at which aChild needs to be inserted into mMenuChildren such
+ // that mMenuChildren remains in correct content order, i.e. the order in
+ // mMenuChildren is the same as the order of the DOM children of our
+ // <menupopup>.
+ size_t FindInsertionIndex(const MenuChild& aChild);
+
+ // Calculates the index at which aChild's NSMenuItem should be inserted into
+ // our NSMenu. The order of NSMenuItems in the NSMenu is the same as the order
+ // of menu children in mMenuChildren; the only difference is that
+ // mMenuChildren contains both visible and invisible children, and the NSMenu
+ // only contains visible items. So the insertion index is equal to the number
+ // of visible previous siblings of aChild in mMenuChildren.
+ NSInteger CalculateNativeInsertionPoint(const MenuChild& aChild);
+
+ // Fires the popupshown event.
+ MOZ_CAN_RUN_SCRIPT void MenuOpenedAsync();
+
+ // Called from mPendingAsyncMenuCloseRunnable asynchronously after
+ // MenuClosed(), so that it runs after any potential menuItemHit calls for
+ // clicked menu items. Fires popuphiding and popuphidden events. When calling
+ // this method, the caller must hold a strong reference to this object,
+ // because other references to this object can be dropped during the handling
+ // of the DOM event.
+ MOZ_CAN_RUN_SCRIPT void MenuClosedAsync();
+
+ // If mPendingAsyncMenuOpenRunnable is non-null, call MenuOpenedAsync() to
+ // send out the pending popupshown event.
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void FlushMenuOpenedRunnable();
+
+ // If mPendingAsyncMenuCloseRunnable is non-null, call MenuClosedAsync() to
+ // send out pending popuphiding/popuphidden events.
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void FlushMenuClosedRunnable();
+
+ // Make sure the NSMenu contains at least one item, even if mVisibleItemsCount
+ // is zero. Otherwise it won't open.
+ void InsertPlaceholderIfNeeded();
+ // Remove the placeholder before adding an item to mNativeNSMenu.
+ void RemovePlaceholderIfPresent();
+
+ nsCOMPtr<nsIContent> mContent; // XUL <menu> or <menupopup>
+
+ // Contains nsMenuX and nsMenuItemX objects
+ nsTArray<MenuChild> mMenuChildren;
+
+ nsString mLabel;
+ uint32_t mVisibleItemsCount = 0; // cache
+ nsMenuParentX* mParent = nullptr; // [weak]
+ nsMenuGroupOwnerX* mMenuGroupOwner = nullptr; // [weak]
+ nsMenuItemIconX::Listener* mIconListener = nullptr; // [weak]
+ mozilla::UniquePtr<nsMenuItemIconX> mIcon;
+
+ Observer* mObserver = nullptr; // non-owning pointer to our observer
+
+ // Non-null between a call to MenuOpened() and MenuOpenedAsync().
+ RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuOpenRunnable;
+
+ // Non-null between a call to MenuClosed() and MenuClosedAsync().
+ // This is asynchronous so that, if a menu item is clicked, we can fire
+ // popuphiding *after* we execute the menu item command. The macOS menu system
+ // calls menuWillClose *before* it calls menuItemHit.
+ RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuCloseRunnable;
+
+ struct PendingCommandEvent {
+ RefPtr<nsMenuItemX> mMenuItem;
+ NSEventModifierFlags mModifiers;
+ int16_t mButton;
+ };
+
+ // Any pending command events.
+ // These are queued by ActivateItemAfterClosing and run by MenuClosedAsync.
+ nsTArray<PendingCommandEvent> mPendingCommandEvents;
+
+ GeckoNSMenu* mNativeMenu = nil; // [strong]
+ MenuDelegate* mMenuDelegate = nil; // [strong]
+ // nsMenuX objects should always have a valid native menu item.
+ NSMenuItem* mNativeMenuItem = nil; // [strong]
+
+ // Nothing() if no item is highlighted. The index only accounts for visible
+ // items.
+ mozilla::Maybe<uint32_t> mHighlightedItemIndex;
+
+ bool mIsEnabled = true;
+ bool mNeedsRebuild = true;
+
+ // Whether the native NSMenu is considered open.
+ // Also affected by MenuOpened() / MenuClosed() calls for simulated opening /
+ // closing.
+ bool mIsOpen = false;
+
+ // Whether the popup is open from Gecko's perspective, based on popupshowing /
+ // popuphiding events.
+ bool mIsOpenForGecko = false;
+
+ bool mVisible = true;
+
+ // true between an OnOpen() call that returned true, and the subsequent call
+ // to MenuOpened().
+ bool mDidFirePopupshowingAndIsApprovedToOpen = false;
+};
+
+#endif // nsMenuX_h_
diff --git a/widget/cocoa/nsMenuX.mm b/widget/cocoa/nsMenuX.mm
new file mode 100644
index 0000000000..d6003c4faf
--- /dev/null
+++ b/widget/cocoa/nsMenuX.mm
@@ -0,0 +1,1490 @@
+/* -*- 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 "nsMenuX.h"
+
+#include <_types/_uint32_t.h>
+#include <dlfcn.h>
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MouseEvents.h"
+
+#include "MOZMenuOpeningCoordinator.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsMenuItemIconX.h"
+
+#include "nsObjCExceptions.h"
+
+#include "nsComputedDOMStyle.h"
+#include "nsThreadUtils.h"
+#include "nsToolkit.h"
+#include "nsCocoaUtils.h"
+#include "nsCOMPtr.h"
+#include "prinrval.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsGkAtoms.h"
+#include "nsCRT.h"
+#include "nsBaseWidget.h"
+
+#include "nsIContent.h"
+#include "nsIDocumentObserver.h"
+#include "nsIComponentManager.h"
+#include "nsIRollupListener.h"
+#include "nsIServiceManager.h"
+#include "nsXULPopupManager.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static bool gConstructingMenu = false;
+static bool gMenuMethodsSwizzled = false;
+
+int32_t nsMenuX::sIndexingMenuLevel = 0;
+
+// TODO: It is unclear whether this is still needed.
+static void SwizzleDynamicIndexingMethods() {
+ if (gMenuMethodsSwizzled) {
+ return;
+ }
+
+ nsToolkit::SwizzleMethods([NSMenu class], @selector(_addItem:toTable:),
+ @selector(nsMenuX_NSMenu_addItem:toTable:), true);
+ nsToolkit::SwizzleMethods([NSMenu class], @selector(_removeItem:fromTable:),
+ @selector(nsMenuX_NSMenu_removeItem:fromTable:),
+ true);
+ // On SnowLeopard the Shortcut framework (which contains the
+ // SCTGRLIndex class) is loaded on demand, whenever the user first opens
+ // a menu (which normally hasn't happened yet). So we need to load it
+ // here explicitly.
+ dlopen("/System/Library/PrivateFrameworks/Shortcut.framework/Shortcut",
+ RTLD_LAZY);
+ Class SCTGRLIndexClass = ::NSClassFromString(@"SCTGRLIndex");
+ nsToolkit::SwizzleMethods(
+ SCTGRLIndexClass, @selector(indexMenuBarDynamically),
+ @selector(nsMenuX_SCTGRLIndex_indexMenuBarDynamically));
+
+ Class NSServicesMenuUpdaterClass =
+ ::NSClassFromString(@"_NSServicesMenuUpdater");
+ nsToolkit::SwizzleMethods(
+ NSServicesMenuUpdaterClass,
+ @selector(populateMenu:withServiceEntries:forDisplay:),
+ @selector(nsMenuX_populateMenu:withServiceEntries:forDisplay:));
+
+ gMenuMethodsSwizzled = true;
+}
+
+//
+// nsMenuX
+//
+
+nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner,
+ nsIContent* aContent)
+ : mContent(aContent), mParent(aParent), mMenuGroupOwner(aMenuGroupOwner) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ MOZ_COUNT_CTOR(nsMenuX);
+
+ SwizzleDynamicIndexingMethods();
+
+ mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this];
+
+ if (!nsMenuBarX::sNativeEventTarget) {
+ nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
+ }
+
+ bool shouldShowServices = false;
+ if (mContent->IsElement()) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
+
+ shouldShowServices =
+ mContent->AsElement()->HasAttr(nsGkAtoms::showservicesmenu);
+ }
+ mNativeMenu = CreateMenuWithGeckoString(mLabel, shouldShowServices);
+
+ // register this menu to be notified when changes are made to our content
+ // object
+ NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one");
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+
+ mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
+ mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
+ action:nil
+ keyEquivalent:@""];
+ mNativeMenuItem.submenu = mNativeMenu;
+
+ SetEnabled(!mContent->IsElement() ||
+ !mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
+ eCaseMatters));
+
+ // We call RebuildMenu here because keyboard commands are dependent upon
+ // native menu items being created. If we only call RebuildMenu when a menu
+ // is actually selected, then we can't access keyboard commands until the
+ // menu gets selected, which is bad.
+ RebuildMenu();
+
+ if (IsXULWindowMenu(mContent)) {
+ // Let the OS know that this is our Window menu.
+ NSApp.windowsMenu = mNativeMenu;
+ }
+
+ mIcon = MakeUnique<nsMenuItemIconX>(this);
+
+ if (mVisible) {
+ SetupIcon();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuX::~nsMenuX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Make sure a pending popupshown event isn't dropped.
+ FlushMenuOpenedRunnable();
+
+ if (mIsOpen) {
+ [mNativeMenu cancelTracking];
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
+ }
+
+ // Make sure pending popuphiding/popuphidden events aren't dropped.
+ FlushMenuClosedRunnable();
+
+ OnHighlightedItemChanged(Nothing());
+ RemoveAll();
+
+ mNativeMenu.delegate = nil;
+ [mNativeMenu release];
+ [mMenuDelegate release];
+ // autorelease the native menu item so that anything else happening to this
+ // object happens before the native menu item actually dies
+ [mNativeMenuItem autorelease];
+
+ DetachFromGroupOwnerRecursive();
+
+ MOZ_COUNT_DTOR(nsMenuX);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::DetachFromGroupOwnerRecursive() {
+ if (!mMenuGroupOwner) {
+ // Don't recurse if this subtree is already detached.
+ // This avoids repeated recursion during the destruction of nested nsMenuX
+ // structures. Our invariant is: If we are detached, all of our contents are
+ // also detached.
+ return;
+ }
+
+ if (mMenuGroupOwner && mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+ mMenuGroupOwner = nullptr;
+
+ // Also detach all our children.
+ for (auto& child : mMenuChildren) {
+ child.match(
+ [](const RefPtr<nsMenuX>& aMenu) {
+ aMenu->DetachFromGroupOwnerRecursive();
+ },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ aMenuItem->DetachFromGroupOwner();
+ });
+ }
+}
+
+void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuWillOpen(aPopupElement);
+ }
+}
+
+void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuDidOpen(aPopupElement);
+ }
+}
+
+void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement,
+ dom::Element* aMenuItemElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement);
+ }
+}
+
+void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) {
+ RefPtr<nsMenuX> kungFuDeathGrip(this);
+ if (mObserver) {
+ mObserver->OnMenuClosed(aPopupElement);
+ }
+}
+
+void nsMenuX::AddMenuChild(MenuChild&& aChild) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ WillInsertChild(aChild);
+ mMenuChildren.AppendElement(aChild);
+
+ bool isVisible = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->IsVisible();
+ });
+ NSMenuItem* nativeItem = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->NativeNSMenuItem();
+ });
+
+ if (isVisible) {
+ RemovePlaceholderIfPresent();
+ [mNativeMenu addItem:nativeItem];
+ ++mVisibleItemsCount;
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::InsertMenuChild(MenuChild&& aChild) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ WillInsertChild(aChild);
+ size_t insertionIndex = FindInsertionIndex(aChild);
+ mMenuChildren.InsertElementAt(insertionIndex, aChild);
+
+ bool isVisible = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->IsVisible();
+ });
+ if (isVisible) {
+ MenuChildChangedVisibility(aChild, true);
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::RemoveMenuChild(const MenuChild& aChild) {
+ bool isVisible = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->IsVisible();
+ });
+ if (isVisible) {
+ MenuChildChangedVisibility(aChild, false);
+ }
+
+ WillRemoveChild(aChild);
+ mMenuChildren.RemoveElement(aChild);
+}
+
+size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) {
+ nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
+ MOZ_RELEASE_ASSERT(menuPopup);
+
+ RefPtr<nsIContent> insertedContent = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->Content();
+ });
+
+ MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup);
+
+ // Iterate over menuPopup's children (insertedContent's siblings) until we
+ // encounter insertedContent. At the same time, keep track of the index in
+ // mMenuChildren.
+ size_t index = 0;
+ for (nsIContent* child = menuPopup->GetFirstChild();
+ child && index < mMenuChildren.Length();
+ child = child->GetNextSibling()) {
+ if (child == insertedContent) {
+ break;
+ }
+
+ RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->Content();
+ });
+ if (child == contentAtIndex) {
+ index++;
+ }
+ }
+
+ return index;
+}
+
+// Includes all items, including hidden/collapsed ones
+uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); }
+
+// Includes all items, including hidden/collapsed ones
+mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) {
+ if (aPos >= (uint32_t)mMenuChildren.Length()) {
+ return {};
+ }
+
+ return Some(mMenuChildren[aPos]);
+}
+
+// Only includes visible items
+nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) {
+ aCount = mVisibleItemsCount;
+ return NS_OK;
+}
+
+// Only includes visible items. Note that this is provides O(N) access
+// If you need to iterate or search, consider using GetItemAt and doing your own
+// filtering
+Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) {
+ uint32_t count = mMenuChildren.Length();
+ if (aPos >= mVisibleItemsCount || aPos >= count) {
+ return {};
+ }
+
+ // If there are no invisible items, can provide direct access
+ if (mVisibleItemsCount == count) {
+ return GetItemAt(aPos);
+ }
+
+ // Otherwise, traverse the array until we find the the item we're looking for.
+ uint32_t visibleNodeIndex = 0;
+ for (uint32_t i = 0; i < count; i++) {
+ MenuChild item = *GetItemAt(i);
+ RefPtr<nsIContent> content = item.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->Content();
+ });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ if (aPos == visibleNodeIndex) {
+ // we found the visible node we're looking for, return it
+ return Some(item);
+ }
+ visibleNodeIndex++;
+ }
+ }
+
+ return {};
+}
+
+Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(
+ Element* aMenuChildElement) {
+ for (auto& child : mMenuChildren) {
+ RefPtr<nsIContent> content = child.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->Content();
+ });
+ if (content == aMenuChildElement) {
+ return Some(child);
+ }
+ }
+ return {};
+}
+
+nsresult nsMenuX::RemoveAll() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ [mNativeMenu removeAllItems];
+
+ for (auto& child : mMenuChildren) {
+ WillRemoveChild(child);
+ }
+
+ mMenuChildren.Clear();
+ mVisibleItemsCount = 0;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::WillInsertChild(const MenuChild& aChild) {
+ if (aChild.is<RefPtr<nsMenuX>>()) {
+ aChild.as<RefPtr<nsMenuX>>()->SetObserver(this);
+ }
+}
+
+void nsMenuX::WillRemoveChild(const MenuChild& aChild) {
+ aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) {
+ aMenu->DetachFromGroupOwnerRecursive();
+ aMenu->DetachFromParent();
+ aMenu->SetObserver(nullptr);
+ },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ aMenuItem->DetachFromGroupOwner();
+ aMenuItem->DetachFromParent();
+ });
+}
+
+void nsMenuX::MenuOpened() {
+ if (mIsOpen) {
+ return;
+ }
+
+ // Make sure we fire any pending popupshown / popuphiding / popuphidden events
+ // first.
+ FlushMenuOpenedRunnable();
+ FlushMenuClosedRunnable();
+
+ if (!mDidFirePopupshowingAndIsApprovedToOpen) {
+ // Fire popupshowing now.
+ bool approvedToOpen = OnOpen();
+ if (!approvedToOpen) {
+ // We can only stop menus from opening which we open ourselves. We cannot
+ // stop menubar root menus or menu submenus from opening. For context
+ // menus, we can call OnOpen() before we ask the system to open the menu.
+ NS_WARNING("The popupshowing event had preventDefault() called on it, "
+ "but in MenuOpened() it "
+ "is too late to stop the menu from opening.");
+ }
+ }
+
+ mIsOpen = true;
+
+ // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
+ mDidFirePopupshowingAndIsApprovedToOpen = false;
+
+ if (mNeedsRebuild) {
+ OnHighlightedItemChanged(Nothing());
+ RemoveAll();
+ RebuildMenu();
+ }
+
+ // Fire the popupshown event in MenuOpenedAsync.
+ // MenuOpened() is called during menuWillOpen, and if cancelTracking is called
+ // now, menuDidClose will not be called. The runnable object must not hold a
+ // strong reference to the nsMenuX, so that there is no reference cycle.
+ class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
+ public:
+ explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
+ : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
+
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
+ if (RefPtr<nsMenuX> menu = mMenu) {
+ menu->MenuOpenedAsync();
+ mMenu = nullptr;
+ }
+ return NS_OK;
+ }
+ nsresult Cancel() override {
+ mMenu = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ nsMenuX* mMenu; // weak, cleared by Cancel() and Run()
+ };
+ mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
+ NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
+}
+
+void nsMenuX::FlushMenuOpenedRunnable() {
+ if (mPendingAsyncMenuOpenRunnable) {
+ MenuOpenedAsync();
+ }
+}
+
+void nsMenuX::MenuOpenedAsync() {
+ if (mPendingAsyncMenuOpenRunnable) {
+ mPendingAsyncMenuOpenRunnable->Cancel();
+ mPendingAsyncMenuOpenRunnable = nullptr;
+ }
+
+ mIsOpenForGecko = true;
+
+ // Open the node.
+ if (mContent->IsElement()) {
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open,
+ u"true"_ns, true);
+ }
+
+ RefPtr<nsIContent> popupContent = GetMenuPopupContent();
+
+ // Notify our observer.
+ if (mObserver && popupContent) {
+ mObserver->OnMenuDidOpen(popupContent->AsElement());
+ }
+
+ // Fire popupshown.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupShown, nullptr,
+ WidgetMouseEvent::eReal);
+ RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
+ EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
+}
+
+void nsMenuX::MenuClosed() {
+ if (!mIsOpen) {
+ return;
+ }
+
+ // Make sure we fire any pending popupshown events first.
+ FlushMenuOpenedRunnable();
+
+ // If any of our submenus were opened programmatically, make sure they get
+ // closed first.
+ for (auto& child : mMenuChildren) {
+ if (child.is<RefPtr<nsMenuX>>()) {
+ child.as<RefPtr<nsMenuX>>()->MenuClosed();
+ }
+ }
+
+ mIsOpen = false;
+
+ // Do the rest of the MenuClosed work in MenuClosedAsync.
+ // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem
+ // was clicked, menuDidClose is called *before* menuItemHit for the clicked
+ // menu item is called. This runnable will be canceled if ~nsMenuX runs before
+ // the runnable. The runnable object must not hold a strong reference to the
+ // nsMenuX, so that there is no reference cycle.
+ class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable {
+ public:
+ explicit MenuClosedAsyncRunnable(nsMenuX* aMenu)
+ : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {}
+
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
+ if (RefPtr<nsMenuX> menu = mMenu) {
+ menu->MenuClosedAsync();
+ mMenu = nullptr;
+ }
+ return NS_OK;
+ }
+ nsresult Cancel() override {
+ mMenu = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ nsMenuX* mMenu; // weak, cleared by Cancel() and Run()
+ };
+
+ mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this);
+
+ NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable);
+}
+
+void nsMenuX::FlushMenuClosedRunnable() {
+ // If any of our submenus have a pending menu closed runnable, make sure those
+ // run first.
+ for (auto& child : mMenuChildren) {
+ if (child.is<RefPtr<nsMenuX>>()) {
+ child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable();
+ }
+ }
+
+ if (mPendingAsyncMenuCloseRunnable) {
+ MenuClosedAsync();
+ }
+}
+
+void nsMenuX::MenuClosedAsync() {
+ if (mPendingAsyncMenuCloseRunnable) {
+ mPendingAsyncMenuCloseRunnable->Cancel();
+ mPendingAsyncMenuCloseRunnable = nullptr;
+ }
+
+ // If we have pending command events, run those first.
+ nsTArray<PendingCommandEvent> events = std::move(mPendingCommandEvents);
+ for (auto& event : events) {
+ event.mMenuItem->DoCommand(event.mModifiers, event.mButton);
+ }
+
+ // Make sure no item is highlighted.
+ OnHighlightedItemChanged(Nothing());
+
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr,
+ WidgetMouseEvent::eReal);
+ EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr,
+ &status);
+
+ mIsOpenForGecko = false;
+
+ if (mContent->IsElement()) {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true);
+ }
+
+ WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr,
+ WidgetMouseEvent::eReal);
+ EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr,
+ &status);
+
+ // Notify our observer.
+ if (mObserver && popupContent) {
+ mObserver->OnMenuClosed(popupContent->AsElement());
+ }
+}
+
+void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem,
+ NSEventModifierFlags aModifiers,
+ int16_t aButton) {
+ if (mIsOpenForGecko) {
+ // Queue the event into mPendingCommandEvents. We will call aItem->DoCommand
+ // in MenuClosedAsync(). We rely on the assumption that MenuClosedAsync will
+ // run soon.
+ mPendingCommandEvents.AppendElement(
+ PendingCommandEvent{std::move(aItem), aModifiers, aButton});
+ } else {
+ // The menu item was activated outside of a regular open / activate / close
+ // sequence. This happens in multiple cases:
+ // - When a menu item is activated by a keyboard shortcut while all windows
+ // are closed
+ // (otherwise those shortcuts go through Gecko's manual keyboard
+ // handling)
+ // - When a menu item in the Dock menu is clicked
+ // - During native menu tests
+ //
+ // Run the command synchronously.
+ aItem->DoCommand(aModifiers, aButton);
+ }
+}
+
+bool nsMenuX::Close() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) {
+ // Close is being called right after this menu was opened, but before
+ // MenuOpened() had a chance to run. Call it here so that we can go through
+ // the entire popupshown -> popuphiding -> popuphidden sequence. Some
+ // callers expect to get a popuphidden event even if they close the popup
+ // before it was fully open.
+ MenuOpened();
+ }
+
+ FlushMenuOpenedRunnable();
+
+ bool wasOpen = mIsOpenForGecko;
+
+ if (mIsOpen) {
+ // Close the menu.
+ // We usually don't get here during normal Firefox usage: If the user closes
+ // the menu by clicking an item, or by clicking outside the menu, or by
+ // pressing escape, then the menu gets closed by macOS, and not by a call to
+ // nsMenuX::Close(). If we do get here, it's usually because we're running
+ // an automated test. Close the menu without the fade-out animation so that
+ // we don't unnecessarily slow down the automated tests.
+ [mNativeMenu cancelTrackingWithoutAnimation];
+ MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
+
+ // Handle closing synchronously.
+ MenuClosed();
+ }
+
+ FlushMenuClosedRunnable();
+
+ return wasOpen;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::OnHighlightedItemChanged(
+ const Maybe<uint32_t>& aNewHighlightedIndex) {
+ if (mHighlightedItemIndex == aNewHighlightedIndex) {
+ return;
+ }
+
+ if (mHighlightedItemIndex) {
+ Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex);
+ if (target && target->is<RefPtr<nsMenuItemX>>()) {
+ bool handlerCalledPreventDefault; // but we don't actually care
+ target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
+ u"DOMMenuItemInactive"_ns, &handlerCalledPreventDefault);
+ }
+ }
+ if (aNewHighlightedIndex) {
+ Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex);
+ if (target && target->is<RefPtr<nsMenuItemX>>()) {
+ bool handlerCalledPreventDefault; // but we don't actually care
+ target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
+ u"DOMMenuItemActive"_ns, &handlerCalledPreventDefault);
+ }
+ }
+ mHighlightedItemIndex = aNewHighlightedIndex;
+}
+
+void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) {
+ if (!mIsOpenForGecko) {
+ return;
+ }
+
+ if (mMenuGroupOwner && mObserver) {
+ nsMenuItemX* item =
+ mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag));
+ if (item && item->Content()->IsElement()) {
+ RefPtr<dom::Element> itemElement = item->Content()->AsElement();
+ if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) {
+ mObserver->OnMenuWillActivateItem(popupContent->AsElement(),
+ itemElement);
+ }
+ }
+ }
+}
+
+// Flushes style.
+static NSUserInterfaceLayoutDirection DirectionForElement(
+ dom::Element* aElement) {
+ // Get the direction from the computed style so that inheritance into submenus
+ // is respected. aElement may not have a frame.
+ RefPtr<const ComputedStyle> sc =
+ nsComputedDOMStyle::GetComputedStyle(aElement);
+ if (!sc) {
+ return NSApp.userInterfaceLayoutDirection;
+ }
+
+ switch (sc->StyleVisibility()->mDirection) {
+ case StyleDirection::Ltr:
+ return NSUserInterfaceLayoutDirectionLeftToRight;
+ case StyleDirection::Rtl:
+ return NSUserInterfaceLayoutDirectionRightToLeft;
+ }
+}
+
+void nsMenuX::RebuildMenu() {
+ MOZ_RELEASE_ASSERT(mNeedsRebuild);
+ gConstructingMenu = true;
+
+ // Retrieve our menupopup.
+ nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
+ if (!menuPopup) {
+ gConstructingMenu = false;
+ return;
+ }
+
+ if (menuPopup->IsElement()) {
+ mNativeMenu.userInterfaceLayoutDirection =
+ DirectionForElement(menuPopup->AsElement());
+ }
+
+ // Iterate over the kids
+ for (nsIContent* child = menuPopup->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) {
+ AddMenuChild(std::move(*menuChild));
+ }
+ } // for each menu item
+
+ InsertPlaceholderIfNeeded();
+
+ gConstructingMenu = false;
+ mNeedsRebuild = false;
+}
+
+void nsMenuX::InsertPlaceholderIfNeeded() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if ([mNativeMenu numberOfItems] == 0) {
+ MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0);
+ NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@""
+ action:nil
+ keyEquivalent:@""];
+ item.enabled = NO;
+ item.view =
+ [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease];
+ [mNativeMenu addItem:item];
+ [item release];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::RemovePlaceholderIfPresent() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) {
+ // Remove the placeholder.
+ [mNativeMenu removeItemAtIndex:0];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::SetRebuild(bool aNeedsRebuild) {
+ if (!gConstructingMenu) {
+ mNeedsRebuild = aNeedsRebuild;
+ if (mParent && mParent->AsMenuBar()) {
+ mParent->AsMenuBar()->SetNeedsRebuild();
+ }
+ }
+}
+
+nsresult nsMenuX::SetEnabled(bool aIsEnabled) {
+ if (aIsEnabled != mIsEnabled) {
+ // we always want to rebuild when this changes
+ mIsEnabled = aIsEnabled;
+ mNativeMenuItem.enabled = mIsEnabled;
+ }
+ return NS_OK;
+}
+
+nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ *aIsEnabled = mIsEnabled;
+ return NS_OK;
+}
+
+GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle,
+ bool aShowServices) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
+ length:aMenuTitle.Length()];
+ GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
+ myMenu.delegate = mMenuDelegate;
+
+ // We don't want this menu to auto-enable menu items because then Cocoa
+ // overrides our decisions and things get incorrectly enabled/disabled.
+ myMenu.autoenablesItems = NO;
+
+ // Only show "Services", "Autofill" and similar entries provided by macOS
+ // if our caller wants them:
+ myMenu.allowsContextMenuPlugIns = aShowServices;
+
+ // we used to install Carbon event handlers here, but since NSMenu* doesn't
+ // create its underlying MenuRef until just before display, we delay until
+ // that happens. Now we install the event handlers when Cocoa notifies
+ // us that a menu is about to display - see the Cocoa MenuDelegate class.
+
+ return myMenu;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) {
+ if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem,
+ nsGkAtoms::menuseparator)) {
+ return Some(MenuChild(CreateMenuItem(aContent)));
+ }
+ if (aContent->IsXULElement(nsGkAtoms::menu)) {
+ return Some(
+ MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent)));
+ }
+ return {};
+}
+
+RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) {
+ MOZ_RELEASE_ASSERT(aMenuItemContent);
+
+ nsAutoString menuitemName;
+ if (aMenuItemContent->IsElement()) {
+ aMenuItemContent->AsElement()->GetAttr(nsGkAtoms::label, menuitemName);
+ }
+
+ EMenuItemType itemType = eRegularMenuItemType;
+ if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) {
+ itemType = eSeparatorMenuItemType;
+ } else if (aMenuItemContent->IsElement()) {
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox,
+ nsGkAtoms::radio, nullptr};
+ switch (aMenuItemContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters)) {
+ case 0:
+ itemType = eCheckboxMenuItemType;
+ break;
+ case 1:
+ itemType = eRadioMenuItemType;
+ break;
+ }
+ }
+
+ return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner,
+ aMenuItemContent);
+}
+
+// This menu is about to open. Returns false if the handler wants to stop the
+// opening of the menu.
+bool nsMenuX::OnOpen() {
+ if (mDidFirePopupshowingAndIsApprovedToOpen) {
+ return true;
+ }
+
+ if (mIsOpen) {
+ NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered "
+ "to be open. This "
+ "seems odd.");
+ }
+
+ RefPtr<nsIContent> popupContent = GetMenuPopupContent();
+
+ if (mObserver && popupContent) {
+ mObserver->OnMenuWillOpen(popupContent->AsElement());
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
+ WidgetMouseEvent::eReal);
+
+ nsresult rv = NS_OK;
+ RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
+ rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
+ if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) {
+ return false;
+ }
+
+ DidFirePopupShowing();
+
+ return true;
+}
+
+void nsMenuX::DidFirePopupShowing() {
+ mDidFirePopupshowingAndIsApprovedToOpen = true;
+
+ // If the open is going to succeed we need to walk our menu items, checking to
+ // see if any of them have a command attribute. If so, several attributes
+ // must potentially be updated.
+
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ if (!popupContent) {
+ return;
+ }
+
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ pm->UpdateMenuItems(popupContent->AsElement());
+ }
+}
+
+// Find the |menupopup| child in the |popup| representing this menu. It should
+// be one of a very few children so we won't be iterating over a bazillion menu
+// items to find it (so the strcmp won't kill us).
+already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() {
+ // Check to see if we are a "menupopup" node (if we are a native menu).
+ if (mContent->IsXULElement(nsGkAtoms::menupopup)) {
+ return do_AddRef(mContent);
+ }
+
+ // Otherwise check our child nodes.
+
+ for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsXULElement(nsGkAtoms::menupopup)) {
+ return child.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) {
+ bool retval = false;
+ if (aMenuContent && aMenuContent->IsElement()) {
+ nsAutoString id;
+ aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
+ if (id.Equals(u"helpMenu"_ns)) {
+ retval = true;
+ }
+ }
+ return retval;
+}
+
+bool nsMenuX::IsXULWindowMenu(nsIContent* aMenuContent) {
+ bool retval = false;
+ if (aMenuContent && aMenuContent->IsElement()) {
+ nsAutoString id;
+ aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
+ if (id.Equals(u"windowMenu"_ns)) {
+ retval = true;
+ }
+ }
+ return retval;
+}
+
+//
+// nsChangeObserver
+//
+
+void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument,
+ nsIContent* aContent,
+ nsAtom* aAttribute) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // ignore the |open| attribute, which is by far the most common
+ if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) {
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::disabled) {
+ SetEnabled(!mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
+ eCaseMatters));
+ } else if (aAttribute == nsGkAtoms::label) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
+ NSString* newCocoaLabelString =
+ nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
+ mNativeMenu.title = newCocoaLabelString;
+ mNativeMenuItem.title = newCocoaLabelString;
+ } else if (aAttribute == nsGkAtoms::hidden ||
+ aAttribute == nsGkAtoms::collapsed) {
+ SetRebuild(true);
+
+ bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ // don't do anything if the state is correct already
+ if (newVisible == mVisible) {
+ return;
+ }
+
+ mVisible = newVisible;
+ if (mParent) {
+ RefPtr<nsMenuX> self = this;
+ mParent->MenuChildChangedVisibility(MenuChild(self), newVisible);
+ }
+ if (mVisible) {
+ SetupIcon();
+ }
+ } else if (aAttribute == nsGkAtoms::image) {
+ SetupIcon();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuX::ObserveContentRemoved(dom::Document* aDocument,
+ nsIContent* aContainer, nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ if (gConstructingMenu) {
+ return;
+ }
+
+ SetRebuild(true);
+ mMenuGroupOwner->UnregisterForContentChanges(aChild);
+
+ if (!mIsOpen) {
+ // We will update the menu contents the next time the menu is opened.
+ return;
+ }
+
+ // The menu is currently open. Remove the child from mMenuChildren and from
+ // our NSMenu.
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ if (popupContent && aContainer == popupContent && aChild->IsElement()) {
+ if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) {
+ RemoveMenuChild(*child);
+ }
+ }
+}
+
+void nsMenuX::ObserveContentInserted(dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild) {
+ if (gConstructingMenu) {
+ return;
+ }
+
+ SetRebuild(true);
+
+ if (!mIsOpen) {
+ // We will update the menu contents the next time the menu is opened.
+ return;
+ }
+
+ // The menu is currently open. Insert the child into mMenuChildren and into
+ // our NSMenu.
+ nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
+ if (popupContent && aContainer == popupContent) {
+ if (Maybe<MenuChild> child = CreateMenuChild(aChild)) {
+ InsertMenuChild(std::move(*child));
+ }
+ }
+}
+
+void nsMenuX::SetupIcon() {
+ mIcon->SetupIcon(mContent);
+ mNativeMenuItem.image = mIcon->GetIconImage();
+}
+
+void nsMenuX::IconUpdated() {
+ mNativeMenuItem.image = mIcon->GetIconImage();
+ if (mIconListener) {
+ mIconListener->IconUpdated();
+ }
+}
+
+void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild,
+ bool aIsVisible) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSMenuItem* nativeItem = aChild.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->NativeNSMenuItem();
+ });
+ if (aIsVisible) {
+ MOZ_RELEASE_ASSERT(
+ !nativeItem.menu,
+ "The native item should not be in a menu while it is hidden");
+ RemovePlaceholderIfPresent();
+ NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild);
+ [mNativeMenu insertItem:nativeItem atIndex:insertionPoint];
+ mVisibleItemsCount++;
+ } else {
+ MOZ_RELEASE_ASSERT(
+ [mNativeMenu indexOfItem:nativeItem] != -1,
+ "The native item should be in this menu while it is visible");
+ [mNativeMenu removeItem:nativeItem];
+ mVisibleItemsCount--;
+ InsertPlaceholderIfNeeded();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) {
+ NSInteger insertionPoint = 0;
+ for (auto& currItem : mMenuChildren) {
+ // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2)
+ if (currItem == aChild) {
+ return insertionPoint;
+ }
+ NSMenuItem* nativeItem = currItem.match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->NativeNSMenuItem();
+ });
+ // Only count visible items.
+ if (nativeItem.menu) {
+ insertionPoint++;
+ }
+ }
+ return insertionPoint;
+}
+
+void nsMenuX::Dump(uint32_t aIndent) const {
+ printf(
+ "%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this,
+ mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(),
+ NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
+ if (mNeedsRebuild) {
+ printf(" [NeedsRebuild]");
+ }
+ if (mIsOpen) {
+ printf(" [Open]");
+ }
+ if (mVisible) {
+ printf(" [Visible]");
+ }
+ if (mIsEnabled) {
+ printf(" [IsEnabled]");
+ }
+ printf(" (%d visible items)", int(mVisibleItemsCount));
+ printf("\n");
+ for (const auto& subitem : mMenuChildren) {
+ subitem.match(
+ [=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); },
+ [=](const RefPtr<nsMenuItemX>& aMenuItem) {
+ aMenuItem->Dump(aIndent + 1);
+ });
+ }
+}
+
+//
+// MenuDelegate Objective-C class, used to set up Carbon events
+//
+
+@implementation MenuDelegate
+
+- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu {
+ if ((self = [super init])) {
+ NS_ASSERTION(geckoMenu, "Cannot initialize native menu delegate with NULL "
+ "gecko menu! Will crash!");
+ mGeckoMenu = geckoMenu;
+ mBlocksToRunWhenOpen = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [mBlocksToRunWhenOpen release];
+ [super dealloc];
+}
+
+- (void)runBlockWhenOpen:(void (^)())block {
+ [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]];
+}
+
+- (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem {
+ if (!aMenu || !mGeckoMenu) {
+ return;
+ }
+
+ Maybe<uint32_t> index =
+ aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem]))
+ : Nothing();
+ mGeckoMenu->OnHighlightedItemChanged(index);
+}
+
+- (void)menuWillOpen:(NSMenu*)menu {
+ for (void (^block)() in mBlocksToRunWhenOpen) {
+ block();
+ }
+ [mBlocksToRunWhenOpen removeAllObjects];
+
+ if (!mGeckoMenu) {
+ return;
+ }
+
+ // Don't do anything while the OS is (re)indexing our menus (on Leopard and
+ // higher). This stops the Help menu from being able to search in our
+ // menus, but it also resolves many other problems.
+ if (nsMenuX::sIndexingMenuLevel > 0) {
+ return;
+ }
+
+ // Hold a strong reference to mGeckoMenu while calling its methods.
+ RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
+ geckoMenu->MenuOpened();
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+ if (!mGeckoMenu) {
+ return;
+ }
+
+ // Don't do anything while the OS is (re)indexing our menus (on Leopard and
+ // higher). This stops the Help menu from being able to search in our
+ // menus, but it also resolves many other problems.
+ if (nsMenuX::sIndexingMenuLevel > 0) {
+ return;
+ }
+
+ // Hold a strong reference to mGeckoMenu while calling its methods.
+ RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
+ geckoMenu->MenuClosed();
+}
+
+// This is called after menuDidClose:.
+- (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem {
+ if (!mGeckoMenu) {
+ return;
+ }
+
+ // Hold a strong reference to mGeckoMenu while calling its methods.
+ RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
+ geckoMenu->OnWillActivateItem(aItem);
+}
+
+@end
+
+// OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some
+// behavior that's present in Mozilla.org browsers but not (as best I can
+// tell) in Apple products like Safari. (It's not yet clear exactly what this
+// behavior is.)
+//
+// The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a
+// call to [NSMenu removeItemAtIndex:]. The crash is caused by trying to
+// access a deleted NSMenuItem object (sometimes (perhaps always?) by trying
+// to send it a _setChangedFlags: message). Though this object was deleted
+// some time ago, it remains registered as a potential target for a particular
+// key equivalent. So when [NSMenu removeItemAtIndex:] removes the current
+// target for that same key equivalent, the OS tries to "activate" the
+// previous target.
+//
+// The underlying reason appears to be that NSMenu's _addItem:toTable: and
+// _removeItem:fromTable: methods (which are used to keep a hashtable of
+// registered key equivalents) don't properly "retain" and "release"
+// NSMenuItem objects as they are added to and removed from the hashtable.
+//
+// Our (hackish) workaround is to shadow the OS's hashtable with another
+// hastable of our own (gShadowKeyEquivDB), and use it to "retain" and
+// "release" NSMenuItem objects as needed. This resolves bmo bugs 422287 and
+// 423669. When (if) Apple fixes this bug, we can remove this workaround.
+
+static NSMutableDictionary* gShadowKeyEquivDB = nil;
+
+// Class for values in gShadowKeyEquivDB.
+
+@interface KeyEquivDBItem : NSObject {
+ NSMenuItem* mItem;
+ NSMutableSet* mTables;
+}
+
+- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable;
+- (BOOL)hasTable:(NSMapTable*)aTable;
+- (int)addTable:(NSMapTable*)aTable;
+- (int)removeTable:(NSMapTable*)aTable;
+
+@end
+
+@implementation KeyEquivDBItem
+
+- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable {
+ if (!gShadowKeyEquivDB) {
+ gShadowKeyEquivDB = [[NSMutableDictionary alloc] init];
+ }
+ self = [super init];
+ if (aItem && aTable) {
+ mTables = [[NSMutableSet alloc] init];
+ mItem = [aItem retain];
+ [mTables addObject:[NSValue valueWithPointer:aTable]];
+ } else {
+ mTables = nil;
+ mItem = nil;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (mTables) {
+ [mTables release];
+ }
+ if (mItem) {
+ [mItem release];
+ }
+ [super dealloc];
+}
+
+- (BOOL)hasTable:(NSMapTable*)aTable {
+ return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO;
+}
+
+// Does nothing if aTable (its index value) is already present in mTables.
+- (int)addTable:(NSMapTable*)aTable {
+ if (aTable) {
+ [mTables addObject:[NSValue valueWithPointer:aTable]];
+ }
+ return [mTables count];
+}
+
+- (int)removeTable:(NSMapTable*)aTable {
+ if (aTable) {
+ NSValue* objectToRemove =
+ [mTables member:[NSValue valueWithPointer:aTable]];
+ if (objectToRemove) {
+ [mTables removeObject:objectToRemove];
+ }
+ }
+ return [mTables count];
+}
+
+@end
+
+@interface NSMenu (MethodSwizzling)
++ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable;
++ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
+ fromTable:(NSMapTable*)aTable;
+@end
+
+@implementation NSMenu (MethodSwizzling)
+
++ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable {
+ if (aItem && aTable) {
+ NSValue* key = [NSValue valueWithPointer:aItem];
+ KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
+ if (shadowItem) {
+ [shadowItem addTable:aTable];
+ } else {
+ shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable];
+ [gShadowKeyEquivDB setObject:shadowItem forKey:key];
+ // Release after [NSMutableDictionary setObject:forKey:] retains it (so
+ // that it will get dealloced when removeObjectForKey: is called).
+ [shadowItem release];
+ }
+ }
+
+ [self nsMenuX_NSMenu_addItem:aItem toTable:aTable];
+}
+
++ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
+ fromTable:(NSMapTable*)aTable {
+ [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable];
+
+ if (aItem && aTable) {
+ NSValue* key = [NSValue valueWithPointer:aItem];
+ KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
+ if (shadowItem && [shadowItem hasTable:aTable]) {
+ if (![shadowItem removeTable:aTable]) {
+ [gShadowKeyEquivDB removeObjectForKey:key];
+ }
+ }
+ }
+}
+
+@end
+
+// This class is needed to keep track of when the OS is (re)indexing all of
+// our menus. This appears to only happen on Leopard and higher, and can
+// be triggered by opening the Help menu. Some operations are unsafe while
+// this is happening -- notably the calls to [[NSImage alloc]
+// initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX::
+// OnStopFrame(). But we don't yet have a complete list, and Apple doesn't
+// yet have any documentation on this subject. (Apple also doesn't yet have
+// any documented way to find the information we seek here.) The "original"
+// of this class (the one whose indexMenuBarDynamically method we hook) is
+// defined in the Shortcut framework in /System/Library/PrivateFrameworks.
+@interface NSObject (SCTGRLIndexMethodSwizzling)
+- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically;
+@end
+
+@implementation NSObject (SCTGRLIndexMethodSwizzling)
+
+- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically {
+ // This method appears to be called (once) whenever the OS (re)indexes our
+ // menus. sIndexingMenuLevel is a int32_t just in case it might be
+ // reentered. As it's running, it spawns calls to two undocumented
+ // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()),
+ // which "simulate" the opening and closing of our menus without actually
+ // displaying them.
+ ++nsMenuX::sIndexingMenuLevel;
+ [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically];
+ --nsMenuX::sIndexingMenuLevel;
+}
+
+@end
+
+@interface NSObject (NSServicesMenuUpdaterSwizzling)
+- (void)nsMenuX_populateMenu:(NSMenu*)aMenu
+ withServiceEntries:(NSArray*)aServices
+ forDisplay:(BOOL)aForDisplay;
+@end
+
+@interface _NSServiceEntry : NSObject
+- (NSString*)bundleIdentifier;
+@end
+
+@implementation NSObject (NSServicesMenuUpdaterSwizzling)
+
+- (void)nsMenuX_populateMenu:(NSMenu*)aMenu
+ withServiceEntries:(NSArray*)aServices
+ forDisplay:(BOOL)aForDisplay {
+ NSMutableArray* filteredServices = [NSMutableArray array];
+
+ // We need to filter some services, such as "Search with Google", since this
+ // service is duplicating functionality already exposed by our "Search Google
+ // for..." context menu entry and because it opens in Safari, which can cause
+ // confusion for users.
+ for (_NSServiceEntry* service in aServices) {
+ NSString* bundleId = [service bundleIdentifier];
+ NSString* msg = [service valueForKey:@"message"];
+ bool shouldSkip = ([bundleId isEqualToString:@"com.apple.Safari"]) ||
+ ([bundleId isEqualToString:@"com.apple.systemuiserver"] &&
+ [msg isEqualToString:@"openURL"]);
+ if (!shouldSkip) {
+ [filteredServices addObject:service];
+ }
+ }
+
+ [self nsMenuX_populateMenu:aMenu
+ withServiceEntries:filteredServices
+ forDisplay:aForDisplay];
+}
+
+@end
diff --git a/widget/cocoa/nsNativeThemeCocoa.h b/widget/cocoa/nsNativeThemeCocoa.h
new file mode 100644
index 0000000000..9488e50e1e
--- /dev/null
+++ b/widget/cocoa/nsNativeThemeCocoa.h
@@ -0,0 +1,408 @@
+/* -*- 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/. */
+
+#ifndef nsNativeThemeCocoa_h_
+#define nsNativeThemeCocoa_h_
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/Variant.h"
+
+#include "nsITheme.h"
+#include "ThemeCocoa.h"
+#include "mozilla/dom/RustTypes.h"
+
+@class MOZCellDrawWindow;
+@class MOZCellDrawView;
+@class MOZSearchFieldCell;
+@class NSProgressBarCell;
+class nsDeviceContext;
+struct SegmentedControlRenderSettings;
+
+namespace mozilla {
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+} // namespace mozilla
+
+class nsNativeThemeCocoa : public mozilla::widget::ThemeCocoa {
+ using ThemeCocoa = mozilla::widget::ThemeCocoa;
+
+ public:
+ enum class CheckboxOrRadioState : uint8_t { eOff, eOn, eIndeterminate };
+
+ enum class ButtonType : uint8_t {
+ eRegularPushButton,
+ eDefaultPushButton,
+ eSquareBezelPushButton,
+ eArrowButton,
+ eHelpButton,
+ eTreeTwistyPointingRight,
+ eTreeTwistyPointingDown,
+ eDisclosureButtonClosed,
+ eDisclosureButtonOpen
+ };
+
+ enum class SpinButton : uint8_t { eUp, eDown };
+
+ enum class SegmentType : uint8_t { eToolbarButton, eTab };
+
+ enum class OptimumState : uint8_t { eOptimum, eSubOptimum, eSubSubOptimum };
+
+ struct ControlParams {
+ ControlParams()
+ : disabled(false),
+ insideActiveWindow(false),
+ pressed(false),
+ focused(false),
+ rtl(false) {}
+
+ bool disabled : 1;
+ bool insideActiveWindow : 1;
+ bool pressed : 1;
+ bool focused : 1;
+ bool rtl : 1;
+ };
+
+ struct CheckboxOrRadioParams {
+ ControlParams controlParams;
+ CheckboxOrRadioState state = CheckboxOrRadioState::eOff;
+ float verticalAlignFactor = 0.5f;
+ };
+
+ struct ButtonParams {
+ ControlParams controlParams;
+ ButtonType button = ButtonType::eRegularPushButton;
+ };
+
+ struct DropdownParams {
+ ControlParams controlParams;
+ bool pullsDown = false;
+ bool editable = false;
+ };
+
+ struct SpinButtonParams {
+ mozilla::Maybe<SpinButton> pressedButton;
+ bool disabled = false;
+ bool insideActiveWindow = false;
+ };
+
+ struct SegmentParams {
+ SegmentType segmentType = SegmentType::eToolbarButton;
+ bool insideActiveWindow = false;
+ bool pressed = false;
+ bool selected = false;
+ bool focused = false;
+ bool atLeftEnd = false;
+ bool atRightEnd = false;
+ bool drawsLeftSeparator = false;
+ bool drawsRightSeparator = false;
+ bool rtl = false;
+ };
+
+ struct TextFieldParams {
+ float verticalAlignFactor = 0.5f;
+ bool insideToolbar = false;
+ bool disabled = false;
+ bool focused = false;
+ bool rtl = false;
+ };
+
+ struct ProgressParams {
+ double value = 0.0;
+ double max = 0.0;
+ float verticalAlignFactor = 0.5f;
+ bool insideActiveWindow = false;
+ bool indeterminate = false;
+ bool horizontal = false;
+ bool rtl = false;
+ };
+
+ struct MeterParams {
+ double value = 0;
+ double min = 0;
+ double max = 0;
+ OptimumState optimumState = OptimumState::eOptimum;
+ float verticalAlignFactor = 0.5f;
+ bool horizontal = true;
+ bool rtl = false;
+ };
+
+ struct TreeHeaderCellParams {
+ ControlParams controlParams;
+ TreeSortDirection sortDirection = eTreeSortDirection_Natural;
+ bool lastTreeHeaderCell = false;
+ };
+
+ struct ScaleParams {
+ int32_t value = 0;
+ int32_t min = 0;
+ int32_t max = 0;
+ bool insideActiveWindow = false;
+ bool disabled = false;
+ bool focused = false;
+ bool horizontal = true;
+ bool reverse = false;
+ };
+
+ enum Widget : uint8_t {
+ eColorFill, // mozilla::gfx::sRGBColor
+ eCheckbox, // CheckboxOrRadioParams
+ eRadio, // CheckboxOrRadioParams
+ eButton, // ButtonParams
+ eDropdown, // DropdownParams
+ eSpinButtons, // SpinButtonParams
+ eSpinButtonUp, // SpinButtonParams
+ eSpinButtonDown, // SpinButtonParams
+ eSegment, // SegmentParams
+ eSeparator,
+ eToolbar, // bool
+ eStatusBar, // bool
+ eGroupBox,
+ eTextField, // TextFieldParams
+ eSearchField, // TextFieldParams
+ eProgressBar, // ProgressParams
+ eMeter, // MeterParams
+ eTreeHeaderCell, // TreeHeaderCellParams
+ eScale, // ScaleParams
+ eMultilineTextField, // bool
+ eListBox,
+ eTabPanel,
+ };
+
+ struct WidgetInfo {
+ static WidgetInfo ColorFill(const mozilla::gfx::sRGBColor& aParams) {
+ return WidgetInfo(Widget::eColorFill, aParams);
+ }
+ static WidgetInfo Checkbox(const CheckboxOrRadioParams& aParams) {
+ return WidgetInfo(Widget::eCheckbox, aParams);
+ }
+ static WidgetInfo Radio(const CheckboxOrRadioParams& aParams) {
+ return WidgetInfo(Widget::eRadio, aParams);
+ }
+ static WidgetInfo Button(const ButtonParams& aParams) {
+ return WidgetInfo(Widget::eButton, aParams);
+ }
+ static WidgetInfo Dropdown(const DropdownParams& aParams) {
+ return WidgetInfo(Widget::eDropdown, aParams);
+ }
+ static WidgetInfo SpinButtons(const SpinButtonParams& aParams) {
+ return WidgetInfo(Widget::eSpinButtons, aParams);
+ }
+ static WidgetInfo SpinButtonUp(const SpinButtonParams& aParams) {
+ return WidgetInfo(Widget::eSpinButtonUp, aParams);
+ }
+ static WidgetInfo SpinButtonDown(const SpinButtonParams& aParams) {
+ return WidgetInfo(Widget::eSpinButtonDown, aParams);
+ }
+ static WidgetInfo Segment(const SegmentParams& aParams) {
+ return WidgetInfo(Widget::eSegment, aParams);
+ }
+ static WidgetInfo Separator() {
+ return WidgetInfo(Widget::eSeparator, false);
+ }
+ static WidgetInfo Toolbar(bool aParams) {
+ return WidgetInfo(Widget::eToolbar, aParams);
+ }
+ static WidgetInfo StatusBar(bool aParams) {
+ return WidgetInfo(Widget::eStatusBar, aParams);
+ }
+ static WidgetInfo GroupBox() {
+ return WidgetInfo(Widget::eGroupBox, false);
+ }
+ static WidgetInfo TextField(const TextFieldParams& aParams) {
+ return WidgetInfo(Widget::eTextField, aParams);
+ }
+ static WidgetInfo SearchField(const TextFieldParams& aParams) {
+ return WidgetInfo(Widget::eSearchField, aParams);
+ }
+ static WidgetInfo ProgressBar(const ProgressParams& aParams) {
+ return WidgetInfo(Widget::eProgressBar, aParams);
+ }
+ static WidgetInfo Meter(const MeterParams& aParams) {
+ return WidgetInfo(Widget::eMeter, aParams);
+ }
+ static WidgetInfo TreeHeaderCell(const TreeHeaderCellParams& aParams) {
+ return WidgetInfo(Widget::eTreeHeaderCell, aParams);
+ }
+ static WidgetInfo Scale(const ScaleParams& aParams) {
+ return WidgetInfo(Widget::eScale, aParams);
+ }
+ static WidgetInfo MultilineTextField(bool aParams) {
+ return WidgetInfo(Widget::eMultilineTextField, aParams);
+ }
+ static WidgetInfo ListBox() { return WidgetInfo(Widget::eListBox, false); }
+ static WidgetInfo TabPanel(bool aParams) {
+ return WidgetInfo(Widget::eTabPanel, aParams);
+ }
+
+ template <typename T>
+ T Params() const {
+ MOZ_RELEASE_ASSERT(mVariant.is<T>());
+ return mVariant.as<T>();
+ }
+
+ enum Widget Widget() const { return mWidget; }
+
+ private:
+ template <typename T>
+ WidgetInfo(enum Widget aWidget, const T& aParams)
+ : mVariant(aParams), mWidget(aWidget) {}
+
+ mozilla::Variant<mozilla::gfx::sRGBColor, CheckboxOrRadioParams,
+ ButtonParams, DropdownParams, SpinButtonParams,
+ SegmentParams, TextFieldParams, ProgressParams,
+ MeterParams, TreeHeaderCellParams, ScaleParams, bool>
+ mVariant;
+
+ enum Widget mWidget;
+ };
+
+ explicit nsNativeThemeCocoa();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // The nsITheme interface.
+ NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect, const nsRect& aDirtyRect,
+ DrawOverflow) override;
+ bool CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) override;
+ [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) override;
+
+ virtual bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) override;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(nsPresContext*, nsIFrame*,
+ StyleAppearance) override;
+ NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) override;
+ NS_IMETHOD ThemeChanged() override;
+ bool ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+ bool WidgetIsContainer(StyleAppearance aAppearance) override;
+ bool ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) override;
+ bool ThemeNeedsComboboxDropmarker() override;
+ virtual bool WidgetAppearanceDependsOnWindowFocus(
+ StyleAppearance aAppearance) override;
+ virtual ThemeGeometryType ThemeGeometryTypeForWidget(
+ nsIFrame* aFrame, StyleAppearance aAppearance) override;
+ virtual Transparency GetWidgetTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) override;
+ mozilla::Maybe<WidgetInfo> ComputeWidgetInfo(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect);
+ void DrawProgress(CGContextRef context, const HIRect& inBoxRect,
+ const ProgressParams& aParams);
+
+ protected:
+ virtual ~nsNativeThemeCocoa();
+
+ LayoutDeviceIntMargin DirectionAwareMargin(
+ const LayoutDeviceIntMargin& aMargin, nsIFrame* aFrame);
+ nsIFrame* SeparatorResponsibility(nsIFrame* aBefore, nsIFrame* aAfter);
+ ControlParams ComputeControlParams(nsIFrame* aFrame,
+ mozilla::dom::ElementState aEventState);
+ SegmentParams ComputeSegmentParams(nsIFrame* aFrame,
+ mozilla::dom::ElementState aEventState,
+ SegmentType aSegmentType);
+ TextFieldParams ComputeTextFieldParams(
+ nsIFrame* aFrame, mozilla::dom::ElementState aEventState);
+ ProgressParams ComputeProgressParams(nsIFrame* aFrame,
+ mozilla::dom::ElementState aEventState,
+ bool aIsHorizontal);
+ MeterParams ComputeMeterParams(nsIFrame* aFrame);
+ TreeHeaderCellParams ComputeTreeHeaderCellParams(
+ nsIFrame* aFrame, mozilla::dom::ElementState aEventState);
+ mozilla::Maybe<ScaleParams> ComputeHTMLScaleParams(
+ nsIFrame* aFrame, mozilla::dom::ElementState aEventState);
+
+ // HITheme drawing routines
+ void DrawMeter(CGContextRef context, const HIRect& inBoxRect,
+ const MeterParams& aParams);
+ void DrawSegment(CGContextRef cgContext, const HIRect& inBoxRect,
+ const SegmentParams& aParams);
+ void DrawSegmentBackground(CGContextRef cgContext, const HIRect& inBoxRect,
+ const SegmentParams& aParams);
+ void DrawTabPanel(CGContextRef context, const HIRect& inBoxRect,
+ bool aIsInsideActiveWindow);
+ void DrawScale(CGContextRef context, const HIRect& inBoxRect,
+ const ScaleParams& aParams);
+ void DrawCheckboxOrRadio(CGContextRef cgContext, bool inCheckbox,
+ const HIRect& inBoxRect,
+ const CheckboxOrRadioParams& aParams);
+ void DrawSearchField(CGContextRef cgContext, const HIRect& inBoxRect,
+ const TextFieldParams& aParams);
+ void DrawTextField(CGContextRef cgContext, const HIRect& inBoxRect,
+ const TextFieldParams& aParams);
+ void DrawPushButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ButtonType aButtonType, ControlParams aControlParams);
+ void DrawSquareBezelPushButton(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ ControlParams aControlParams);
+ void DrawHelpButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams);
+ void DrawDisclosureButton(CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams,
+ NSControlStateValue aState);
+ void DrawHIThemeButton(CGContextRef cgContext, const HIRect& aRect,
+ ThemeButtonKind aKind, ThemeButtonValue aValue,
+ ThemeDrawState aState, ThemeButtonAdornment aAdornment,
+ const ControlParams& aParams);
+ void DrawButton(CGContextRef context, const HIRect& inBoxRect,
+ const ButtonParams& aParams);
+ void DrawTreeHeaderCell(CGContextRef context, const HIRect& inBoxRect,
+ const TreeHeaderCellParams& aParams);
+ void DrawDropdown(CGContextRef context, const HIRect& inBoxRect,
+ const DropdownParams& aParams);
+ HIThemeButtonDrawInfo SpinButtonDrawInfo(ThemeButtonKind aKind,
+ const SpinButtonParams& aParams);
+ void DrawSpinButtons(CGContextRef context, const HIRect& inBoxRect,
+ const SpinButtonParams& aParams);
+ void DrawSpinButton(CGContextRef context, const HIRect& inBoxRect,
+ SpinButton aDrawnButton, const SpinButtonParams& aParams);
+ void DrawToolbar(CGContextRef cgContext, const CGRect& inBoxRect,
+ bool aIsMain);
+ void DrawStatusBar(CGContextRef cgContext, const HIRect& inBoxRect,
+ bool aIsMain);
+ void DrawMultilineTextField(CGContextRef cgContext, const CGRect& inBoxRect,
+ bool aIsFocused);
+ void RenderWidget(const WidgetInfo& aWidgetInfo, mozilla::ColorScheme,
+ mozilla::gfx::DrawTarget& aDrawTarget,
+ const mozilla::gfx::Rect& aWidgetRect,
+ const mozilla::gfx::Rect& aDirtyRect, float aScale);
+
+ private:
+ NSButtonCell* mDisclosureButtonCell;
+ NSButtonCell* mHelpButtonCell;
+ NSButtonCell* mPushButtonCell;
+ NSButtonCell* mRadioButtonCell;
+ NSButtonCell* mCheckboxCell;
+ NSTextFieldCell* mTextFieldCell;
+ MOZSearchFieldCell* mSearchFieldCell;
+ NSPopUpButtonCell* mDropdownCell;
+ NSComboBoxCell* mComboBoxCell;
+ NSProgressBarCell* mProgressBarCell;
+ NSLevelIndicatorCell* mMeterBarCell;
+ NSTableHeaderCell* mTreeHeaderCell;
+ MOZCellDrawWindow* mCellDrawWindow = nil;
+ MOZCellDrawView* mCellDrawView;
+};
+
+#endif // nsNativeThemeCocoa_h_
diff --git a/widget/cocoa/nsNativeThemeCocoa.mm b/widget/cocoa/nsNativeThemeCocoa.mm
new file mode 100644
index 0000000000..ff715ac57c
--- /dev/null
+++ b/widget/cocoa/nsNativeThemeCocoa.mm
@@ -0,0 +1,3328 @@
+/* -*- 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 "nsNativeThemeCocoa.h"
+#include <objc/NSObjCRuntime.h>
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/gfx/PathHelpers.h"
+#include "nsChildView.h"
+#include "nsDeviceContext.h"
+#include "nsLayoutUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsNumberControlFrame.h"
+#include "nsRangeFrame.h"
+#include "nsRect.h"
+#include "nsSize.h"
+#include "nsStyleConsts.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFrame.h"
+#include "nsAtom.h"
+#include "nsNameSpaceManager.h"
+#include "nsPresContext.h"
+#include "nsGkAtoms.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaWindow.h"
+#include "nsNativeThemeColors.h"
+#include "nsIScrollableFrame.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Range.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLMeterElement.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsLookAndFeel.h"
+#include "MacThemeGeometryType.h"
+#include "VibrancyManager.h"
+
+#include "gfxContext.h"
+#include "gfxQuartzSurface.h"
+#include "gfxQuartzNativeDrawing.h"
+#include "gfxUtils.h" // for ToDeviceColor
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using mozilla::dom::HTMLMeterElement;
+
+#define DRAW_IN_FRAME_DEBUG 0
+#define SCROLLBARS_VISUAL_DEBUG 0
+
+// private Quartz routines needed here
+extern "C" {
+CG_EXTERN void CGContextSetCTM(CGContextRef, CGAffineTransform);
+CG_EXTERN void CGContextSetBaseCTM(CGContextRef, CGAffineTransform);
+typedef CFTypeRef CUIRendererRef;
+void CUIDraw(CUIRendererRef r, CGRect rect, CGContextRef ctx,
+ CFDictionaryRef options, CFDictionaryRef* result);
+}
+
+// Workaround for NSCell control tint drawing
+// Without this workaround, NSCells are always drawn with the clear control tint
+// as long as they're not attached to an NSControl which is a subview of an
+// active window.
+// XXXmstange Why doesn't Webkit need this?
+@implementation NSCell (ControlTintWorkaround)
+- (int)_realControlTint {
+ return [self controlTint];
+}
+@end
+
+// This is the window for our MOZCellDrawView. When an NSCell is drawn, some
+// NSCell implementations look at the draw view's window to determine whether
+// the cell should draw with the active look.
+@interface MOZCellDrawWindow : NSWindow
+@property BOOL cellsShouldLookActive;
+@end
+
+@implementation MOZCellDrawWindow
+
+// Override three different methods, for good measure. The NSCell implementation
+// could call any one of them.
+- (BOOL)_hasActiveAppearance {
+ return self.cellsShouldLookActive;
+}
+- (BOOL)hasKeyAppearance {
+ return self.cellsShouldLookActive;
+}
+- (BOOL)_hasKeyAppearance {
+ return self.cellsShouldLookActive;
+}
+
+@end
+
+// The purpose of this class is to provide objects that can be used when drawing
+// NSCells using drawWithFrame:inView: without causing any harm. Only a small
+// number of methods are called on the draw view, among those "isFlipped" and
+// "currentEditor": isFlipped needs to return YES in order to avoid drawing bugs
+// on 10.4 (see bug 465069); currentEditor (which isn't even a method of
+// NSView) will be called when drawing search fields, and we only provide it in
+// order to prevent "unrecognized selector" exceptions.
+// There's no need to pass the actual NSView that we're drawing into to
+// drawWithFrame:inView:. What's more, doing so even causes unnecessary
+// invalidations as soon as we draw a focusring!
+// This class needs to be an NSControl so that NSTextFieldCell (and
+// NSSearchFieldCell, which is a subclass of NSTextFieldCell) draws a focus
+// ring.
+@interface MOZCellDrawView : NSControl
+// Called by NSTreeHeaderCell during drawing.
+@property BOOL _drawingEndSeparator;
+@end
+
+@implementation MOZCellDrawView
+
+- (BOOL)isFlipped {
+ return YES;
+}
+
+- (NSText*)currentEditor {
+ return nil;
+}
+
+@end
+
+static void DrawFocusRingForCellIfNeeded(NSCell* aCell, NSRect aWithFrame,
+ NSView* aInView) {
+ if ([aCell showsFirstResponder]) {
+ CGContextRef cgContext = [[NSGraphicsContext currentContext] CGContext];
+ CGContextSaveGState(cgContext);
+
+ // It's important to set the focus ring style before we enter the
+ // transparency layer so that the transparency layer only contains
+ // the normal button mask without the focus ring, and the conversion
+ // to the focus ring shape happens only when the transparency layer is
+ // ended.
+ NSSetFocusRingStyle(NSFocusRingOnly);
+
+ // We need to draw the whole button into a transparency layer because
+ // many button types are composed of multiple parts, and if these parts
+ // were drawn while the focus ring style was active, each individual part
+ // would produce a focus ring for itself. But we only want one focus ring
+ // for the whole button. The transparency layer is a way to merge the
+ // individual button parts together before the focus ring shape is
+ // calculated.
+ CGContextBeginTransparencyLayerWithRect(cgContext,
+ NSRectToCGRect(aWithFrame), 0);
+ [aCell drawFocusRingMaskWithFrame:aWithFrame inView:aInView];
+ CGContextEndTransparencyLayer(cgContext);
+
+ CGContextRestoreGState(cgContext);
+ }
+}
+
+static void DrawCellIncludingFocusRing(NSCell* aCell, NSRect aWithFrame,
+ NSView* aInView) {
+ [aCell drawWithFrame:aWithFrame inView:aInView];
+ DrawFocusRingForCellIfNeeded(aCell, aWithFrame, aInView);
+}
+
+/**
+ * NSProgressBarCell is used to draw progress bars of any size.
+ */
+@interface NSProgressBarCell : NSCell {
+ /*All instance variables are private*/
+ double mValue;
+ double mMax;
+ bool mIsIndeterminate;
+ bool mIsHorizontal;
+}
+
+- (void)setValue:(double)value;
+- (double)value;
+- (void)setMax:(double)max;
+- (double)max;
+- (void)setIndeterminate:(bool)aIndeterminate;
+- (bool)isIndeterminate;
+- (void)setHorizontal:(bool)aIsHorizontal;
+- (bool)isHorizontal;
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView;
+@end
+
+@implementation NSProgressBarCell
+
+- (void)setMax:(double)aMax {
+ mMax = aMax;
+}
+
+- (double)max {
+ return mMax;
+}
+
+- (void)setValue:(double)aValue {
+ mValue = aValue;
+}
+
+- (double)value {
+ return mValue;
+}
+
+- (void)setIndeterminate:(bool)aIndeterminate {
+ mIsIndeterminate = aIndeterminate;
+}
+
+- (bool)isIndeterminate {
+ return mIsIndeterminate;
+}
+
+- (void)setHorizontal:(bool)aIsHorizontal {
+ mIsHorizontal = aIsHorizontal;
+}
+
+- (bool)isHorizontal {
+ return mIsHorizontal;
+}
+
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ CGContext* cgContext = [[NSGraphicsContext currentContext] CGContext];
+
+ HIThemeTrackDrawInfo tdi;
+
+ tdi.version = 0;
+ tdi.min = 0;
+
+ tdi.value = INT32_MAX * (mValue / mMax);
+ tdi.max = INT32_MAX;
+ tdi.bounds = NSRectToCGRect(cellFrame);
+ tdi.attributes = mIsHorizontal ? kThemeTrackHorizontal : 0;
+ tdi.enableState = [self controlTint] == NSClearControlTint
+ ? kThemeTrackInactive
+ : kThemeTrackActive;
+
+ NSControlSize size = [self controlSize];
+ if (size == NSControlSizeRegular) {
+ tdi.kind =
+ mIsIndeterminate ? kThemeLargeIndeterminateBar : kThemeLargeProgressBar;
+ } else {
+ NS_ASSERTION(
+ size == NSControlSizeSmall,
+ "We shouldn't have another size than small and regular for the moment");
+ tdi.kind = mIsIndeterminate ? kThemeMediumIndeterminateBar
+ : kThemeMediumProgressBar;
+ }
+
+ int32_t stepsPerSecond = mIsIndeterminate ? 60 : 30;
+ int32_t milliSecondsPerStep = 1000 / stepsPerSecond;
+ tdi.trackInfo.progress.phase = uint8_t(
+ PR_IntervalToMilliseconds(PR_IntervalNow()) / milliSecondsPerStep);
+
+ HIThemeDrawTrack(&tdi, NULL, cgContext, kHIThemeOrientationNormal);
+}
+
+@end
+
+@interface MOZSearchFieldCell : NSSearchFieldCell
+@property BOOL shouldUseToolbarStyle;
+@end
+
+@implementation MOZSearchFieldCell
+
+- (instancetype)init {
+ // We would like to render a search field which has the magnifying glass icon
+ // at the start of the search field, and no cancel button. On 10.12 and 10.13,
+ // empty search fields render the magnifying glass icon in the middle of the
+ // field. So in order to get the icon to show at the start of the field, we
+ // need to give the field some content. We achieve this with a single space
+ // character.
+ self = [super initTextCell:@" "];
+
+ // However, because the field is now non-empty, by default it shows a cancel
+ // button. To hide the cancel button, override it with a custom NSButtonCell
+ // which renders nothing.
+ NSButtonCell* invisibleCell = [[NSButtonCell alloc] initImageCell:nil];
+ invisibleCell.bezeled = NO;
+ invisibleCell.bordered = NO;
+ self.cancelButtonCell = invisibleCell;
+ [invisibleCell release];
+
+ return self;
+}
+
+- (BOOL)_isToolbarMode {
+ return self.shouldUseToolbarStyle;
+}
+
+@end
+
+#define HITHEME_ORIENTATION kHIThemeOrientationNormal
+
+static CGFloat kMaxFocusRingWidth =
+ 0; // initialized by the nsNativeThemeCocoa constructor
+
+// These enums are for indexing into the margin array.
+enum {
+ leopardOSorlater = 0, // 10.6 - 10.9
+ yosemiteOSorlater = 1 // 10.10+
+};
+
+enum { miniControlSize, smallControlSize, regularControlSize };
+
+enum { leftMargin, topMargin, rightMargin, bottomMargin };
+
+static size_t EnumSizeForCocoaSize(NSControlSize cocoaControlSize) {
+ if (cocoaControlSize == NSControlSizeMini)
+ return miniControlSize;
+ else if (cocoaControlSize == NSControlSizeSmall)
+ return smallControlSize;
+ else
+ return regularControlSize;
+}
+
+static NSControlSize CocoaSizeForEnum(int32_t enumControlSize) {
+ if (enumControlSize == miniControlSize)
+ return NSControlSizeMini;
+ else if (enumControlSize == smallControlSize)
+ return NSControlSizeSmall;
+ else
+ return NSControlSizeRegular;
+}
+
+static NSString* CUIControlSizeForCocoaSize(NSControlSize aControlSize) {
+ if (aControlSize == NSControlSizeRegular)
+ return @"regular";
+ else if (aControlSize == NSControlSizeSmall)
+ return @"small";
+ else
+ return @"mini";
+}
+
+static void InflateControlRect(NSRect* rect, NSControlSize cocoaControlSize,
+ const float marginSet[][3][4]) {
+ if (!marginSet) return;
+
+ static int osIndex = yosemiteOSorlater;
+ size_t controlSize = EnumSizeForCocoaSize(cocoaControlSize);
+ const float* buttonMargins = marginSet[osIndex][controlSize];
+ rect->origin.x -= buttonMargins[leftMargin];
+ rect->origin.y -= buttonMargins[bottomMargin];
+ rect->size.width += buttonMargins[leftMargin] + buttonMargins[rightMargin];
+ rect->size.height += buttonMargins[bottomMargin] + buttonMargins[topMargin];
+}
+
+static NSWindow* NativeWindowForFrame(nsIFrame* aFrame,
+ nsIWidget** aTopLevelWidget = NULL) {
+ if (!aFrame) return nil;
+
+ nsIWidget* widget = aFrame->GetNearestWidget();
+ if (!widget) return nil;
+
+ nsIWidget* topLevelWidget = widget->GetTopLevelWidget();
+ if (aTopLevelWidget) *aTopLevelWidget = topLevelWidget;
+
+ return (NSWindow*)topLevelWidget->GetNativeData(NS_NATIVE_WINDOW);
+}
+
+static NSSize WindowButtonsSize(nsIFrame* aFrame) {
+ NSWindow* window = NativeWindowForFrame(aFrame);
+ if (!window) {
+ // Return fallback values.
+ return NSMakeSize(54, 16);
+ }
+
+ NSRect buttonBox = NSZeroRect;
+ NSButton* closeButton = [window standardWindowButton:NSWindowCloseButton];
+ if (closeButton) {
+ buttonBox = NSUnionRect(buttonBox, [closeButton frame]);
+ }
+ NSButton* minimizeButton =
+ [window standardWindowButton:NSWindowMiniaturizeButton];
+ if (minimizeButton) {
+ buttonBox = NSUnionRect(buttonBox, [minimizeButton frame]);
+ }
+ NSButton* zoomButton = [window standardWindowButton:NSWindowZoomButton];
+ if (zoomButton) {
+ buttonBox = NSUnionRect(buttonBox, [zoomButton frame]);
+ }
+ return buttonBox.size;
+}
+
+static BOOL FrameIsInActiveWindow(nsIFrame* aFrame) {
+ nsIWidget* topLevelWidget = NULL;
+ NSWindow* win = NativeWindowForFrame(aFrame, &topLevelWidget);
+ if (!topLevelWidget || !win) return YES;
+
+ // XUL popups, e.g. the toolbar customization popup, can't become key windows,
+ // but controls in these windows should still get the active look.
+ if (topLevelWidget->GetWindowType() == widget::WindowType::Popup) {
+ return YES;
+ }
+ if ([win isSheet]) {
+ return [win isKeyWindow];
+ }
+ return [win isMainWindow] && ![win attachedSheet];
+}
+
+// Toolbar controls and content controls respond to different window
+// activeness states.
+static BOOL IsActive(nsIFrame* aFrame, BOOL aIsToolbarControl) {
+ if (aIsToolbarControl) return [NativeWindowForFrame(aFrame) isMainWindow];
+ return FrameIsInActiveWindow(aFrame);
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeCocoa, nsNativeTheme, nsITheme)
+
+nsNativeThemeCocoa::nsNativeThemeCocoa() : ThemeCocoa(ScrollbarStyle()) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ kMaxFocusRingWidth = 7;
+
+ // provide a local autorelease pool, as this is called during startup
+ // before the main event-loop pool is in place
+ nsAutoreleasePool pool;
+
+ mDisclosureButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mDisclosureButtonCell setBezelStyle:NSBezelStyleRoundedDisclosure];
+ [mDisclosureButtonCell setButtonType:NSButtonTypePushOnPushOff];
+ [mDisclosureButtonCell setHighlightsBy:NSPushInCellMask];
+
+ mHelpButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mHelpButtonCell setBezelStyle:NSBezelStyleHelpButton];
+ [mHelpButtonCell setButtonType:NSButtonTypeMomentaryPushIn];
+ [mHelpButtonCell setHighlightsBy:NSPushInCellMask];
+
+ mPushButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mPushButtonCell setButtonType:NSButtonTypeMomentaryPushIn];
+ [mPushButtonCell setHighlightsBy:NSPushInCellMask];
+
+ mRadioButtonCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mRadioButtonCell setButtonType:NSButtonTypeRadio];
+
+ mCheckboxCell = [[NSButtonCell alloc] initTextCell:@""];
+ [mCheckboxCell setButtonType:NSButtonTypeSwitch];
+ [mCheckboxCell setAllowsMixedState:YES];
+
+ mTextFieldCell = [[NSTextFieldCell alloc] initTextCell:@""];
+ [mTextFieldCell setBezeled:YES];
+ [mTextFieldCell setEditable:YES];
+ [mTextFieldCell setFocusRingType:NSFocusRingTypeExterior];
+
+ mSearchFieldCell = [[MOZSearchFieldCell alloc] init];
+ [mSearchFieldCell setBezelStyle:NSTextFieldRoundedBezel];
+ [mSearchFieldCell setBezeled:YES];
+ [mSearchFieldCell setEditable:YES];
+ [mSearchFieldCell setFocusRingType:NSFocusRingTypeExterior];
+
+ mDropdownCell = [[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO];
+
+ mComboBoxCell = [[NSComboBoxCell alloc] initTextCell:@""];
+ [mComboBoxCell setBezeled:YES];
+ [mComboBoxCell setEditable:YES];
+ [mComboBoxCell setFocusRingType:NSFocusRingTypeExterior];
+
+ mProgressBarCell = [[NSProgressBarCell alloc] init];
+
+ mMeterBarCell = [[NSLevelIndicatorCell alloc]
+ initWithLevelIndicatorStyle:NSLevelIndicatorStyleContinuousCapacity];
+
+ mTreeHeaderCell = [[NSTableHeaderCell alloc] init];
+
+ mCellDrawView = [[MOZCellDrawView alloc] init];
+
+ if (XRE_IsParentProcess()) {
+ // Put the cell draw view into a window that is never shown.
+ // This allows us to convince some NSCell implementations (such as
+ // NSButtonCell for default buttons) to draw with the active appearance.
+ // Another benefit of putting the draw view in a window is the fact that it
+ // lets NSTextFieldCell (and its subclass NSSearchFieldCell) inherit the
+ // current NSApplication effectiveAppearance automatically, so the field
+ // adapts to Dark Mode correctly. We don't create this window when the
+ // native theme is used in the content process because NSWindow creation
+ // runs into the sandbox and because we never run default buttons in content
+ // processes anyway.
+ mCellDrawWindow = [[MOZCellDrawWindow alloc]
+ initWithContentRect:NSZeroRect
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ [mCellDrawWindow.contentView addSubview:mCellDrawView];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsNativeThemeCocoa::~nsNativeThemeCocoa() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mMeterBarCell release];
+ [mProgressBarCell release];
+ [mDisclosureButtonCell release];
+ [mHelpButtonCell release];
+ [mPushButtonCell release];
+ [mRadioButtonCell release];
+ [mCheckboxCell release];
+ [mTextFieldCell release];
+ [mSearchFieldCell release];
+ [mDropdownCell release];
+ [mComboBoxCell release];
+ [mTreeHeaderCell release];
+ [mCellDrawWindow release];
+ [mCellDrawView release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Limit on the area of the target rect (in pixels^2) in
+// DrawCellWithScaling() and DrawButton() and above which we
+// don't draw the object into a bitmap buffer. This is to avoid crashes in
+// [NSGraphicsContext graphicsContextWithCGContext:flipped:] and
+// CGContextDrawImage(), and also to avoid very poor drawing performance in
+// CGContextDrawImage() when it scales the bitmap (particularly if xscale or
+// yscale is less than but near 1 -- e.g. 0.9). This value was determined
+// by trial and error, on OS X 10.4.11 and 10.5.4, and on systems with
+// different amounts of RAM.
+#define BITMAP_MAX_AREA 500000
+
+static int GetBackingScaleFactorForRendering(CGContextRef cgContext) {
+ CGAffineTransform ctm =
+ CGContextGetUserSpaceToDeviceSpaceTransform(cgContext);
+ CGRect transformedUserSpacePixel =
+ CGRectApplyAffineTransform(CGRectMake(0, 0, 1, 1), ctm);
+ float maxScale = std::max(fabs(transformedUserSpacePixel.size.width),
+ fabs(transformedUserSpacePixel.size.height));
+ return maxScale > 1.0 ? 2 : 1;
+}
+
+/*
+ * Draw the given NSCell into the given cgContext.
+ *
+ * destRect - the size and position of the resulting control rectangle
+ * controlSize - the NSControlSize which will be given to the NSCell before
+ * asking it to render
+ * naturalSize - The natural dimensions of this control.
+ * If the control rect size is not equal to either of these, a scale
+ * will be applied to the context so that rendering the control at the
+ * natural size will result in it filling the destRect space.
+ * If a control has no natural dimensions in either/both axes, pass 0.0f.
+ * minimumSize - The minimum dimensions of this control.
+ * If the control rect size is less than the minimum for a given axis,
+ * a scale will be applied to the context so that the minimum is used
+ * for drawing. If a control has no minimum dimensions in either/both
+ * axes, pass 0.0f.
+ * marginSet - an array of margins; a multidimensional array of [2][3][4],
+ * with the first dimension being the OS version (Tiger or Leopard),
+ * the second being the control size (mini, small, regular), and the third
+ * being the 4 margin values (left, top, right, bottom).
+ * view - The NSView that we're drawing into. As far as I can tell, it doesn't
+ * matter if this is really the right view; it just has to return YES when
+ * asked for isFlipped. Otherwise we'll get drawing bugs on 10.4.
+ * mirrorHorizontal - whether to mirror the cell horizontally
+ */
+static void DrawCellWithScaling(NSCell* cell, CGContextRef cgContext,
+ const HIRect& destRect,
+ NSControlSize controlSize, NSSize naturalSize,
+ NSSize minimumSize,
+ const float marginSet[][3][4], NSView* view,
+ BOOL mirrorHorizontal) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSRect drawRect = NSMakeRect(destRect.origin.x, destRect.origin.y,
+ destRect.size.width, destRect.size.height);
+
+ if (naturalSize.width != 0.0f) drawRect.size.width = naturalSize.width;
+ if (naturalSize.height != 0.0f) drawRect.size.height = naturalSize.height;
+
+ // Keep aspect ratio when scaling if one dimension is free.
+ if (naturalSize.width == 0.0f && naturalSize.height != 0.0f)
+ drawRect.size.width =
+ destRect.size.width * naturalSize.height / destRect.size.height;
+ if (naturalSize.height == 0.0f && naturalSize.width != 0.0f)
+ drawRect.size.height =
+ destRect.size.height * naturalSize.width / destRect.size.width;
+
+ // Honor minimum sizes.
+ if (drawRect.size.width < minimumSize.width)
+ drawRect.size.width = minimumSize.width;
+ if (drawRect.size.height < minimumSize.height)
+ drawRect.size.height = minimumSize.height;
+
+ [NSGraphicsContext saveGraphicsState];
+
+ // Only skip the buffer if the area of our cell (in pixels^2) is too large.
+ if (drawRect.size.width * drawRect.size.height > BITMAP_MAX_AREA) {
+ // Inflate the rect Gecko gave us by the margin for the control.
+ InflateControlRect(&drawRect, controlSize, marginSet);
+
+ NSGraphicsContext* savedContext = [NSGraphicsContext currentContext];
+ [NSGraphicsContext
+ setCurrentContext:[NSGraphicsContext
+ graphicsContextWithCGContext:cgContext
+ flipped:YES]];
+
+ DrawCellIncludingFocusRing(cell, drawRect, view);
+
+ [NSGraphicsContext setCurrentContext:savedContext];
+ } else {
+ float w = ceil(drawRect.size.width);
+ float h = ceil(drawRect.size.height);
+ NSRect tmpRect = NSMakeRect(kMaxFocusRingWidth, kMaxFocusRingWidth, w, h);
+
+ // inflate to figure out the frame we need to tell NSCell to draw in, to get
+ // something that's 0,0,w,h
+ InflateControlRect(&tmpRect, controlSize, marginSet);
+
+ // and then, expand by kMaxFocusRingWidth size to make sure we can capture
+ // any focus ring
+ w += kMaxFocusRingWidth * 2.0;
+ h += kMaxFocusRingWidth * 2.0;
+
+ int backingScaleFactor = GetBackingScaleFactorForRendering(cgContext);
+ CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
+ CGContextRef ctx = CGBitmapContextCreate(
+ NULL, (int)w * backingScaleFactor, (int)h * backingScaleFactor, 8,
+ (int)w * backingScaleFactor * 4, rgb, kCGImageAlphaPremultipliedFirst);
+ CGColorSpaceRelease(rgb);
+
+ // We need to flip the image twice in order to avoid drawing bugs on 10.4,
+ // see bug 465069. This is the first flip transform, applied to cgContext.
+ CGContextScaleCTM(cgContext, 1.0f, -1.0f);
+ CGContextTranslateCTM(cgContext, 0.0f,
+ -(2.0 * destRect.origin.y + destRect.size.height));
+ if (mirrorHorizontal) {
+ CGContextScaleCTM(cgContext, -1.0f, 1.0f);
+ CGContextTranslateCTM(
+ cgContext, -(2.0 * destRect.origin.x + destRect.size.width), 0.0f);
+ }
+
+ NSGraphicsContext* savedContext = [NSGraphicsContext currentContext];
+ [NSGraphicsContext
+ setCurrentContext:[NSGraphicsContext graphicsContextWithCGContext:ctx
+ flipped:YES]];
+
+ CGContextScaleCTM(ctx, backingScaleFactor, backingScaleFactor);
+
+ // Set the context's "base transform" to in order to get correctly-sized
+ // focus rings.
+ CGContextSetBaseCTM(ctx, CGAffineTransformMakeScale(backingScaleFactor,
+ backingScaleFactor));
+
+ // This is the second flip transform, applied to ctx.
+ CGContextScaleCTM(ctx, 1.0f, -1.0f);
+ CGContextTranslateCTM(ctx, 0.0f,
+ -(2.0 * tmpRect.origin.y + tmpRect.size.height));
+
+ DrawCellIncludingFocusRing(cell, tmpRect, view);
+
+ [NSGraphicsContext setCurrentContext:savedContext];
+
+ CGImageRef img = CGBitmapContextCreateImage(ctx);
+
+ // Drop the image into the original destination rectangle, scaling to fit
+ // Only scale kMaxFocusRingWidth by xscale/yscale when the resulting rect
+ // doesn't extend beyond the overflow rect
+ float xscale = destRect.size.width / drawRect.size.width;
+ float yscale = destRect.size.height / drawRect.size.height;
+ float scaledFocusRingX =
+ xscale < 1.0f ? kMaxFocusRingWidth * xscale : kMaxFocusRingWidth;
+ float scaledFocusRingY =
+ yscale < 1.0f ? kMaxFocusRingWidth * yscale : kMaxFocusRingWidth;
+ CGContextDrawImage(cgContext,
+ CGRectMake(destRect.origin.x - scaledFocusRingX,
+ destRect.origin.y - scaledFocusRingY,
+ destRect.size.width + scaledFocusRingX * 2,
+ destRect.size.height + scaledFocusRingY * 2),
+ img);
+
+ CGImageRelease(img);
+ CGContextRelease(ctx);
+ }
+
+ [NSGraphicsContext restoreGraphicsState];
+
+#if DRAW_IN_FRAME_DEBUG
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25);
+ CGContextFillRect(cgContext, destRect);
+#endif
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+struct CellRenderSettings {
+ // The natural dimensions of the control.
+ // If a control has no natural dimensions in either/both axes, set to 0.0f.
+ NSSize naturalSizes[3];
+
+ // The minimum dimensions of the control.
+ // If a control has no minimum dimensions in either/both axes, set to 0.0f.
+ NSSize minimumSizes[3];
+
+ // A three-dimensional array,
+ // with the first dimension being the OS version ([0] 10.6-10.9, [1] 10.10 and
+ // above), the second being the control size (mini, small, regular), and the
+ // third being the 4 margin values (left, top, right, bottom).
+ float margins[2][3][4];
+};
+
+/*
+ * This is a helper method that returns the required NSControlSize given a size
+ * and the size of the three controls plus a tolerance.
+ * size - The width or the height of the element to draw.
+ * sizes - An array with the all the width/height of the element for its
+ * different sizes.
+ * tolerance - The tolerance as passed to DrawCellWithSnapping.
+ * NOTE: returns NSControlSizeRegular if all values in 'sizes' are zero.
+ */
+static NSControlSize FindControlSize(CGFloat size, const CGFloat* sizes,
+ CGFloat tolerance) {
+ for (uint32_t i = miniControlSize; i <= regularControlSize; ++i) {
+ if (sizes[i] == 0) {
+ continue;
+ }
+
+ CGFloat next = 0;
+ // Find next value.
+ for (uint32_t j = i + 1; j <= regularControlSize; ++j) {
+ if (sizes[j] != 0) {
+ next = sizes[j];
+ break;
+ }
+ }
+
+ // If it's the latest value, we pick it.
+ if (next == 0) {
+ return CocoaSizeForEnum(i);
+ }
+
+ if (size <= sizes[i] + tolerance && size < next) {
+ return CocoaSizeForEnum(i);
+ }
+ }
+
+ // If we are here, that means sizes[] was an array with only empty values
+ // or the algorithm above is wrong.
+ // The former can happen but the later would be wrong.
+ NS_ASSERTION(sizes[0] == 0 && sizes[1] == 0 && sizes[2] == 0,
+ "We found no control! We shouldn't be there!");
+ return CocoaSizeForEnum(regularControlSize);
+}
+
+/*
+ * Draw the given NSCell into the given cgContext with a nice control size.
+ *
+ * This function is similar to DrawCellWithScaling, but it decides what
+ * control size to use based on the destRect's size.
+ * Scaling is only applied when the difference between the destRect's size
+ * and the next smaller natural size is greater than snapTolerance. Otherwise
+ * it snaps to the next smaller control size without scaling because unscaled
+ * controls look nicer.
+ */
+static void DrawCellWithSnapping(NSCell* cell, CGContextRef cgContext,
+ const HIRect& destRect,
+ const CellRenderSettings settings,
+ float verticalAlignFactor, NSView* view,
+ BOOL mirrorHorizontal,
+ float snapTolerance = 2.0f) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ const float rectWidth = destRect.size.width,
+ rectHeight = destRect.size.height;
+ const NSSize* sizes = settings.naturalSizes;
+ const NSSize miniSize = sizes[EnumSizeForCocoaSize(NSControlSizeMini)];
+ const NSSize smallSize = sizes[EnumSizeForCocoaSize(NSControlSizeSmall)];
+ const NSSize regularSize = sizes[EnumSizeForCocoaSize(NSControlSizeRegular)];
+
+ HIRect drawRect = destRect;
+
+ CGFloat controlWidths[3] = {miniSize.width, smallSize.width,
+ regularSize.width};
+ NSControlSize controlSizeX =
+ FindControlSize(rectWidth, controlWidths, snapTolerance);
+ CGFloat controlHeights[3] = {miniSize.height, smallSize.height,
+ regularSize.height};
+ NSControlSize controlSizeY =
+ FindControlSize(rectHeight, controlHeights, snapTolerance);
+
+ NSControlSize controlSize = NSControlSizeRegular;
+ size_t sizeIndex = 0;
+
+ // At some sizes, don't scale but snap.
+ const NSControlSize smallerControlSize =
+ EnumSizeForCocoaSize(controlSizeX) < EnumSizeForCocoaSize(controlSizeY)
+ ? controlSizeX
+ : controlSizeY;
+ const size_t smallerControlSizeIndex =
+ EnumSizeForCocoaSize(smallerControlSize);
+ const NSSize size = sizes[smallerControlSizeIndex];
+ float diffWidth = size.width ? rectWidth - size.width : 0.0f;
+ float diffHeight = size.height ? rectHeight - size.height : 0.0f;
+ if (diffWidth >= 0.0f && diffHeight >= 0.0f && diffWidth <= snapTolerance &&
+ diffHeight <= snapTolerance) {
+ // Snap to the smaller control size.
+ controlSize = smallerControlSize;
+ sizeIndex = smallerControlSizeIndex;
+ MOZ_ASSERT(sizeIndex < ArrayLength(settings.naturalSizes));
+
+ // Resize and center the drawRect.
+ if (sizes[sizeIndex].width) {
+ drawRect.origin.x +=
+ ceil((destRect.size.width - sizes[sizeIndex].width) / 2);
+ drawRect.size.width = sizes[sizeIndex].width;
+ }
+ if (sizes[sizeIndex].height) {
+ drawRect.origin.y +=
+ floor((destRect.size.height - sizes[sizeIndex].height) *
+ verticalAlignFactor);
+ drawRect.size.height = sizes[sizeIndex].height;
+ }
+ } else {
+ // Use the larger control size.
+ controlSize =
+ EnumSizeForCocoaSize(controlSizeX) > EnumSizeForCocoaSize(controlSizeY)
+ ? controlSizeX
+ : controlSizeY;
+ sizeIndex = EnumSizeForCocoaSize(controlSize);
+ }
+
+ [cell setControlSize:controlSize];
+
+ MOZ_ASSERT(sizeIndex < ArrayLength(settings.minimumSizes));
+ const NSSize minimumSize = settings.minimumSizes[sizeIndex];
+ DrawCellWithScaling(cell, cgContext, drawRect, controlSize, sizes[sizeIndex],
+ minimumSize, settings.margins, view, mirrorHorizontal);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@interface NSWindow (CoreUIRendererPrivate)
++ (CUIRendererRef)coreUIRenderer;
+@end
+
+@interface NSObject (NSAppearanceCoreUIRendering)
+- (void)_drawInRect:(CGRect)rect
+ context:(CGContextRef)cgContext
+ options:(id)options;
+@end
+
+static void RenderWithCoreUI(CGRect aRect, CGContextRef cgContext,
+ NSDictionary* aOptions,
+ bool aSkipAreaCheck = false) {
+ if (!aSkipAreaCheck &&
+ aRect.size.width * aRect.size.height > BITMAP_MAX_AREA) {
+ return;
+ }
+
+ NSAppearance* appearance = NSAppearance.currentAppearance;
+ if (appearance &&
+ [appearance respondsToSelector:@selector(_drawInRect:context:options:)]) {
+ // Render through NSAppearance on Mac OS 10.10 and up. This will call
+ // CUIDraw with a CoreUI renderer that will give us the correct 10.10
+ // style. Calling CUIDraw directly with [NSWindow coreUIRenderer] still
+ // renders 10.9-style widgets on 10.10.
+ [appearance _drawInRect:aRect context:cgContext options:aOptions];
+ } else {
+ // 10.9 and below
+ CUIRendererRef renderer =
+ [NSWindow respondsToSelector:@selector(coreUIRenderer)]
+ ? [NSWindow coreUIRenderer]
+ : nil;
+ CUIDraw(renderer, aRect, cgContext, (CFDictionaryRef)aOptions, NULL);
+ }
+}
+
+static float VerticalAlignFactor(nsIFrame* aFrame) {
+ if (!aFrame) return 0.5f; // default: center
+
+ const auto& va = aFrame->StyleDisplay()->mVerticalAlign;
+ auto kw = va.IsKeyword() ? va.AsKeyword() : StyleVerticalAlignKeyword::Middle;
+ switch (kw) {
+ case StyleVerticalAlignKeyword::Top:
+ case StyleVerticalAlignKeyword::TextTop:
+ return 0.0f;
+
+ case StyleVerticalAlignKeyword::Sub:
+ case StyleVerticalAlignKeyword::Super:
+ case StyleVerticalAlignKeyword::Middle:
+ case StyleVerticalAlignKeyword::MozMiddleWithBaseline:
+ return 0.5f;
+
+ case StyleVerticalAlignKeyword::Baseline:
+ case StyleVerticalAlignKeyword::Bottom:
+ case StyleVerticalAlignKeyword::TextBottom:
+ return 1.0f;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid vertical-align");
+ return 0.5f;
+ }
+}
+
+static void ApplyControlParamsToNSCell(
+ nsNativeThemeCocoa::ControlParams aControlParams, NSCell* aCell) {
+ [aCell setEnabled:!aControlParams.disabled];
+ [aCell setShowsFirstResponder:(aControlParams.focused &&
+ !aControlParams.disabled &&
+ aControlParams.insideActiveWindow)];
+ [aCell setHighlighted:aControlParams.pressed];
+}
+
+// These are the sizes that Gecko needs to request to draw if it wants
+// to get a standard-sized Aqua radio button drawn. Note that the rects
+// that draw these are actually a little bigger.
+static const CellRenderSettings radioSettings = {
+ {
+ NSMakeSize(11, 11), // mini
+ NSMakeSize(13, 13), // small
+ NSMakeSize(16, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {0, 1, 1, 1}, // small
+ {0, 0, 0, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 2}, // small
+ {0, 0, 0, 0} // regular
+ }}};
+
+static const CellRenderSettings checkboxSettings = {
+ {
+ NSMakeSize(11, 11), // mini
+ NSMakeSize(13, 13), // small
+ NSMakeSize(16, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 1, 0, 0}, // mini
+ {0, 1, 0, 1}, // small
+ {0, 1, 0, 1} // regular
+ },
+ {
+ // Yosemite
+ {0, 1, 0, 0}, // mini
+ {0, 1, 0, 1}, // small
+ {0, 1, 0, 1} // regular
+ }}};
+
+static NSControlStateValue CellStateForCheckboxOrRadioState(
+ nsNativeThemeCocoa::CheckboxOrRadioState aState) {
+ switch (aState) {
+ case nsNativeThemeCocoa::CheckboxOrRadioState::eOff:
+ return NSControlStateValueOff;
+ case nsNativeThemeCocoa::CheckboxOrRadioState::eOn:
+ return NSControlStateValueOn;
+ case nsNativeThemeCocoa::CheckboxOrRadioState::eIndeterminate:
+ return NSControlStateValueMixed;
+ }
+}
+
+void nsNativeThemeCocoa::DrawCheckboxOrRadio(
+ CGContextRef cgContext, bool inCheckbox, const HIRect& inBoxRect,
+ const CheckboxOrRadioParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSButtonCell* cell = inCheckbox ? mCheckboxCell : mRadioButtonCell;
+ ApplyControlParamsToNSCell(aParams.controlParams, cell);
+
+ [cell setState:CellStateForCheckboxOrRadioState(aParams.state)];
+ [cell setControlTint:(aParams.controlParams.insideActiveWindow
+ ? [NSColor currentControlTint]
+ : NSClearControlTint)];
+
+ // Ensure that the control is square.
+ float length = std::min(inBoxRect.size.width, inBoxRect.size.height);
+ HIRect drawRect = CGRectMake(
+ inBoxRect.origin.x + (int)((inBoxRect.size.width - length) / 2.0f),
+ inBoxRect.origin.y + (int)((inBoxRect.size.height - length) / 2.0f),
+ length, length);
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive =
+ aParams.controlParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(cell, cgContext, drawRect,
+ inCheckbox ? checkboxSettings : radioSettings,
+ aParams.verticalAlignFactor, mCellDrawView, NO);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings searchFieldSettings = {
+ {
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 19), // small
+ NSMakeSize(0, 22) // regular
+ },
+ {
+ NSMakeSize(32, 0), // mini
+ NSMakeSize(38, 0), // small
+ NSMakeSize(44, 0) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ }}};
+
+static bool IsToolbarStyleContainer(nsIFrame* aFrame) {
+ nsIContent* content = aFrame->GetContent();
+ if (!content) {
+ return false;
+ }
+
+ if (content->IsAnyOfXULElements(nsGkAtoms::toolbar, nsGkAtoms::toolbox,
+ nsGkAtoms::statusbar)) {
+ return true;
+ }
+
+ switch (aFrame->StyleDisplay()->EffectiveAppearance()) {
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Statusbar:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool IsInsideToolbar(nsIFrame* aFrame) {
+ for (nsIFrame* frame = aFrame; frame; frame = frame->GetParent()) {
+ if (IsToolbarStyleContainer(frame)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+nsNativeThemeCocoa::TextFieldParams nsNativeThemeCocoa::ComputeTextFieldParams(
+ nsIFrame* aFrame, ElementState aEventState) {
+ TextFieldParams params;
+ params.insideToolbar = IsInsideToolbar(aFrame);
+ params.disabled = aEventState.HasState(ElementState::DISABLED);
+
+ // See ShouldUnconditionallyDrawFocusRingIfFocused.
+ params.focused = aEventState.HasState(ElementState::FOCUS);
+
+ params.rtl = IsFrameRTL(aFrame);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawTextField(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const TextFieldParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSTextFieldCell* cell = mTextFieldCell;
+ [cell setEnabled:!aParams.disabled];
+ [cell setShowsFirstResponder:aParams.focused];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive =
+ YES; // TODO: propagate correct activeness state
+ }
+ DrawCellWithSnapping(cell, cgContext, inBoxRect, searchFieldSettings,
+ aParams.verticalAlignFactor, mCellDrawView, aParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawSearchField(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const TextFieldParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mSearchFieldCell.enabled = !aParams.disabled;
+ mSearchFieldCell.showsFirstResponder = aParams.focused;
+ mSearchFieldCell.placeholderString = @"";
+ mSearchFieldCell.shouldUseToolbarStyle = aParams.insideToolbar;
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive =
+ YES; // TODO: propagate correct activeness state
+ }
+ DrawCellWithSnapping(mSearchFieldCell, cgContext, inBoxRect,
+ searchFieldSettings, aParams.verticalAlignFactor,
+ mCellDrawView, aParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static bool ShouldUnconditionallyDrawFocusRingIfFocused(nsIFrame* aFrame) {
+ // Mac always draws focus rings for textboxes and lists.
+ switch (aFrame->StyleDisplay()->EffectiveAppearance()) {
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::Listbox:
+ return true;
+ default:
+ return false;
+ }
+}
+
+nsNativeThemeCocoa::ControlParams nsNativeThemeCocoa::ComputeControlParams(
+ nsIFrame* aFrame, ElementState aEventState) {
+ ControlParams params;
+ params.disabled = aEventState.HasState(ElementState::DISABLED);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.pressed =
+ aEventState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER);
+ params.focused = aEventState.HasState(ElementState::FOCUS) &&
+ (aEventState.HasState(ElementState::FOCUSRING) ||
+ ShouldUnconditionallyDrawFocusRingIfFocused(aFrame));
+ params.rtl = IsFrameRTL(aFrame);
+ return params;
+}
+
+static const NSSize kHelpButtonSize = NSMakeSize(20, 20);
+static const NSSize kDisclosureButtonSize = NSMakeSize(21, 21);
+
+static const CellRenderSettings pushButtonSettings = {
+ {
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 19), // small
+ NSMakeSize(0, 22) // regular
+ },
+ {
+ NSMakeSize(18, 0), // mini
+ NSMakeSize(26, 0), // small
+ NSMakeSize(30, 0) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {4, 0, 4, 1}, // small
+ {5, 0, 5, 2} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {4, 0, 4, 1}, // small
+ {5, 0, 5, 2} // regular
+ }}};
+
+// The height at which we start doing square buttons instead of rounded buttons
+// Rounded buttons look bad if drawn at a height greater than 26, so at that
+// point we switch over to doing square buttons which looks fine at any size.
+#define DO_SQUARE_BUTTON_HEIGHT 26
+
+void nsNativeThemeCocoa::DrawPushButton(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ ButtonType aButtonType,
+ ControlParams aControlParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mPushButtonCell);
+ [mPushButtonCell setBezelStyle:NSBezelStyleRounded];
+ mPushButtonCell.keyEquivalent =
+ aButtonType == ButtonType::eDefaultPushButton ? @"\r" : @"";
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(mPushButtonCell, cgContext, inBoxRect,
+ pushButtonSettings, 0.5f, mCellDrawView,
+ aControlParams.rtl, 1.0f);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawSquareBezelPushButton(
+ CGContextRef cgContext, const HIRect& inBoxRect,
+ ControlParams aControlParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mPushButtonCell);
+ [mPushButtonCell setBezelStyle:NSBezelStyleShadowlessSquare];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithScaling(mPushButtonCell, cgContext, inBoxRect,
+ NSControlSizeRegular, NSZeroSize, NSMakeSize(14, 0), NULL,
+ mCellDrawView, aControlParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawHelpButton(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ ControlParams aControlParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mHelpButtonCell);
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithScaling(mHelpButtonCell, cgContext, inBoxRect,
+ NSControlSizeRegular, NSZeroSize, kHelpButtonSize, NULL,
+ mCellDrawView,
+ false); // Don't mirror icon in RTL.
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawDisclosureButton(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ ControlParams aControlParams,
+ NSControlStateValue aCellState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ ApplyControlParamsToNSCell(aControlParams, mDisclosureButtonCell);
+ [mDisclosureButtonCell setState:aCellState];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow;
+ }
+ DrawCellWithScaling(mDisclosureButtonCell, cgContext, inBoxRect,
+ NSControlSizeRegular, NSZeroSize, kDisclosureButtonSize,
+ NULL, mCellDrawView,
+ false); // Don't mirror icon in RTL.
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+typedef void (*RenderHIThemeControlFunction)(CGContextRef cgContext,
+ const HIRect& aRenderRect,
+ void* aData);
+
+static void RenderTransformedHIThemeControl(CGContextRef aCGContext,
+ const HIRect& aRect,
+ RenderHIThemeControlFunction aFunc,
+ void* aData,
+ BOOL mirrorHorizontally = NO) {
+ CGAffineTransform savedCTM = CGContextGetCTM(aCGContext);
+ CGContextTranslateCTM(aCGContext, aRect.origin.x, aRect.origin.y);
+
+ bool drawDirect;
+ HIRect drawRect = aRect;
+ drawRect.origin = CGPointZero;
+
+ if (!mirrorHorizontally && savedCTM.a == 1.0f && savedCTM.b == 0.0f &&
+ savedCTM.c == 0.0f && (savedCTM.d == 1.0f || savedCTM.d == -1.0f)) {
+ drawDirect = TRUE;
+ } else {
+ drawDirect = FALSE;
+ }
+
+ // Fall back to no bitmap buffer if the area of our control (in pixels^2)
+ // is too large.
+ if (drawDirect || (aRect.size.width * aRect.size.height > BITMAP_MAX_AREA)) {
+ aFunc(aCGContext, drawRect, aData);
+ } else {
+ // Inflate the buffer to capture focus rings.
+ int w = ceil(drawRect.size.width) + 2 * kMaxFocusRingWidth;
+ int h = ceil(drawRect.size.height) + 2 * kMaxFocusRingWidth;
+
+ int backingScaleFactor = GetBackingScaleFactorForRendering(aCGContext);
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
+ CGContextRef bitmapctx = CGBitmapContextCreate(
+ NULL, w * backingScaleFactor, h * backingScaleFactor, 8,
+ w * backingScaleFactor * 4, colorSpace,
+ kCGImageAlphaPremultipliedFirst);
+ CGColorSpaceRelease(colorSpace);
+
+ CGContextScaleCTM(bitmapctx, backingScaleFactor, backingScaleFactor);
+ CGContextTranslateCTM(bitmapctx, kMaxFocusRingWidth, kMaxFocusRingWidth);
+
+ // Set the context's "base transform" to in order to get correctly-sized
+ // focus rings.
+ CGContextSetBaseCTM(bitmapctx, CGAffineTransformMakeScale(
+ backingScaleFactor, backingScaleFactor));
+
+ // HITheme always wants to draw into a flipped context, or things
+ // get confused.
+ CGContextTranslateCTM(bitmapctx, 0.0f, aRect.size.height);
+ CGContextScaleCTM(bitmapctx, 1.0f, -1.0f);
+
+ aFunc(bitmapctx, drawRect, aData);
+
+ CGImageRef bitmap = CGBitmapContextCreateImage(bitmapctx);
+
+ CGAffineTransform ctm = CGContextGetCTM(aCGContext);
+
+ // We need to unflip, so that we can do a DrawImage without getting a
+ // flipped image.
+ CGContextTranslateCTM(aCGContext, 0.0f, aRect.size.height);
+ CGContextScaleCTM(aCGContext, 1.0f, -1.0f);
+
+ if (mirrorHorizontally) {
+ CGContextTranslateCTM(aCGContext, aRect.size.width, 0);
+ CGContextScaleCTM(aCGContext, -1.0f, 1.0f);
+ }
+
+ HIRect inflatedDrawRect =
+ CGRectMake(-kMaxFocusRingWidth, -kMaxFocusRingWidth, w, h);
+ CGContextDrawImage(aCGContext, inflatedDrawRect, bitmap);
+
+ CGContextSetCTM(aCGContext, ctm);
+
+ CGImageRelease(bitmap);
+ CGContextRelease(bitmapctx);
+ }
+
+ CGContextSetCTM(aCGContext, savedCTM);
+}
+
+static void RenderButton(CGContextRef cgContext, const HIRect& aRenderRect,
+ void* aData) {
+ HIThemeButtonDrawInfo* bdi = (HIThemeButtonDrawInfo*)aData;
+ HIThemeDrawButton(&aRenderRect, bdi, cgContext, kHIThemeOrientationNormal,
+ NULL);
+}
+
+static ThemeDrawState ToThemeDrawState(
+ const nsNativeThemeCocoa::ControlParams& aParams) {
+ if (aParams.disabled) {
+ return kThemeStateUnavailable;
+ }
+ if (aParams.pressed) {
+ return kThemeStatePressed;
+ }
+ return kThemeStateActive;
+}
+
+void nsNativeThemeCocoa::DrawHIThemeButton(
+ CGContextRef cgContext, const HIRect& aRect, ThemeButtonKind aKind,
+ ThemeButtonValue aValue, ThemeDrawState aState,
+ ThemeButtonAdornment aAdornment, const ControlParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeButtonDrawInfo bdi;
+ bdi.version = 0;
+ bdi.kind = aKind;
+ bdi.value = aValue;
+ bdi.state = aState;
+ bdi.adornment = aAdornment;
+
+ if (aParams.focused && aParams.insideActiveWindow) {
+ bdi.adornment |= kThemeAdornmentFocus;
+ }
+
+ RenderTransformedHIThemeControl(cgContext, aRect, RenderButton, &bdi,
+ aParams.rtl);
+
+#if DRAW_IN_FRAME_DEBUG
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25);
+ CGContextFillRect(cgContext, inBoxRect);
+#endif
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawButton(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const ButtonParams& aParams) {
+ ControlParams controlParams = aParams.controlParams;
+
+ switch (aParams.button) {
+ case ButtonType::eRegularPushButton:
+ case ButtonType::eDefaultPushButton:
+ DrawPushButton(cgContext, inBoxRect, aParams.button, controlParams);
+ return;
+ case ButtonType::eSquareBezelPushButton:
+ DrawSquareBezelPushButton(cgContext, inBoxRect, controlParams);
+ return;
+ case ButtonType::eArrowButton:
+ DrawHIThemeButton(cgContext, inBoxRect, kThemeArrowButton, kThemeButtonOn,
+ kThemeStateUnavailable, kThemeAdornmentArrowDownArrow,
+ controlParams);
+ return;
+ case ButtonType::eHelpButton:
+ DrawHelpButton(cgContext, inBoxRect, controlParams);
+ return;
+ case ButtonType::eTreeTwistyPointingRight:
+ DrawHIThemeButton(cgContext, inBoxRect, kThemeDisclosureButton,
+ kThemeDisclosureRight, ToThemeDrawState(controlParams),
+ kThemeAdornmentNone, controlParams);
+ return;
+ case ButtonType::eTreeTwistyPointingDown:
+ DrawHIThemeButton(cgContext, inBoxRect, kThemeDisclosureButton,
+ kThemeDisclosureDown, ToThemeDrawState(controlParams),
+ kThemeAdornmentNone, controlParams);
+ return;
+ case ButtonType::eDisclosureButtonClosed:
+ DrawDisclosureButton(cgContext, inBoxRect, controlParams,
+ NSControlStateValueOff);
+ return;
+ case ButtonType::eDisclosureButtonOpen:
+ DrawDisclosureButton(cgContext, inBoxRect, controlParams,
+ NSControlStateValueOn);
+ return;
+ }
+}
+
+nsNativeThemeCocoa::TreeHeaderCellParams
+nsNativeThemeCocoa::ComputeTreeHeaderCellParams(nsIFrame* aFrame,
+ ElementState aEventState) {
+ TreeHeaderCellParams params;
+ params.controlParams = ComputeControlParams(aFrame, aEventState);
+ params.sortDirection = GetTreeSortDirection(aFrame);
+ params.lastTreeHeaderCell = IsLastTreeHeaderCell(aFrame);
+ return params;
+}
+
+@interface NSTableHeaderCell (NSTableHeaderCell_setSortable)
+// This method has been present in the same form since at least macOS 10.4.
+- (void)_setSortable:(BOOL)arg1
+ showSortIndicator:(BOOL)arg2
+ ascending:(BOOL)arg3
+ priority:(NSInteger)arg4
+ highlightForSort:(BOOL)arg5;
+@end
+
+void nsNativeThemeCocoa::DrawTreeHeaderCell(
+ CGContextRef cgContext, const HIRect& inBoxRect,
+ const TreeHeaderCellParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Without clearing the cell's title, it takes on a default value of "Field",
+ // which is displayed underneath the title set in the front-end.
+ NSCell* cell = (NSCell*)mTreeHeaderCell;
+ cell.title = @"";
+
+ if ([mTreeHeaderCell
+ respondsToSelector:@selector
+ (_setSortable:
+ showSortIndicator:ascending:priority:highlightForSort:)]) {
+ switch (aParams.sortDirection) {
+ case eTreeSortDirection_Ascending:
+ [mTreeHeaderCell _setSortable:YES
+ showSortIndicator:YES
+ ascending:YES
+ priority:0
+ highlightForSort:YES];
+ break;
+ case eTreeSortDirection_Descending:
+ [mTreeHeaderCell _setSortable:YES
+ showSortIndicator:YES
+ ascending:NO
+ priority:0
+ highlightForSort:YES];
+ break;
+ default:
+ // eTreeSortDirection_Natural
+ [mTreeHeaderCell _setSortable:YES
+ showSortIndicator:NO
+ ascending:YES
+ priority:0
+ highlightForSort:NO];
+ break;
+ }
+ }
+
+ mTreeHeaderCell.enabled = !aParams.controlParams.disabled;
+ mTreeHeaderCell.state =
+ (mTreeHeaderCell.enabled && aParams.controlParams.pressed)
+ ? NSControlStateValueOn
+ : NSControlStateValueOff;
+
+ mCellDrawView._drawingEndSeparator = !aParams.lastTreeHeaderCell;
+
+ NSGraphicsContext* savedContext = NSGraphicsContext.currentContext;
+ NSGraphicsContext.currentContext =
+ [NSGraphicsContext graphicsContextWithCGContext:cgContext flipped:YES];
+ DrawCellIncludingFocusRing(mTreeHeaderCell, inBoxRect, mCellDrawView);
+ NSGraphicsContext.currentContext = savedContext;
+
+#if DRAW_IN_FRAME_DEBUG
+ CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25);
+ CGContextFillRect(cgContext, inBoxRect);
+#endif
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings dropdownSettings = {
+ {
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 19), // small
+ NSMakeSize(0, 22) // regular
+ },
+ {
+ NSMakeSize(18, 0), // mini
+ NSMakeSize(38, 0), // small
+ NSMakeSize(44, 0) // regular
+ },
+ {{
+ // Leopard
+ {1, 1, 2, 1}, // mini
+ {3, 0, 3, 1}, // small
+ {3, 0, 3, 0} // regular
+ },
+ {
+ // Yosemite
+ {1, 1, 2, 1}, // mini
+ {3, 0, 3, 1}, // small
+ {3, 0, 3, 0} // regular
+ }}};
+
+static const CellRenderSettings editableMenulistSettings = {
+ {
+ NSMakeSize(0, 15), // mini
+ NSMakeSize(0, 18), // small
+ NSMakeSize(0, 21) // regular
+ },
+ {
+ NSMakeSize(18, 0), // mini
+ NSMakeSize(38, 0), // small
+ NSMakeSize(44, 0) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 2, 2}, // mini
+ {0, 0, 3, 2}, // small
+ {0, 1, 3, 3} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 2, 2}, // mini
+ {0, 0, 3, 2}, // small
+ {0, 1, 3, 3} // regular
+ }}};
+
+void nsNativeThemeCocoa::DrawDropdown(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const DropdownParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mDropdownCell setPullsDown:aParams.pullsDown];
+ NSCell* cell =
+ aParams.editable ? (NSCell*)mComboBoxCell : (NSCell*)mDropdownCell;
+
+ ApplyControlParamsToNSCell(aParams.controlParams, cell);
+
+ if (aParams.controlParams.insideActiveWindow) {
+ [cell setControlTint:[NSColor currentControlTint]];
+ } else {
+ [cell setControlTint:NSClearControlTint];
+ }
+
+ const CellRenderSettings& settings =
+ aParams.editable ? editableMenulistSettings : dropdownSettings;
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive =
+ aParams.controlParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(cell, cgContext, inBoxRect, settings, 0.5f,
+ mCellDrawView, aParams.controlParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings spinnerSettings = {
+ {
+ NSMakeSize(11,
+ 16), // mini (width trimmed by 2px to reduce blank border)
+ NSMakeSize(15, 22), // small
+ NSMakeSize(19, 27) // regular
+ },
+ {
+ NSMakeSize(11,
+ 16), // mini (width trimmed by 2px to reduce blank border)
+ NSMakeSize(15, 22), // small
+ NSMakeSize(19, 27) // regular
+ },
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {0, 0, 0, 0}, // small
+ {0, 0, 0, 0} // regular
+ }}};
+
+HIThemeButtonDrawInfo nsNativeThemeCocoa::SpinButtonDrawInfo(
+ ThemeButtonKind aKind, const SpinButtonParams& aParams) {
+ HIThemeButtonDrawInfo bdi;
+ bdi.version = 0;
+ bdi.kind = aKind;
+ bdi.value = kThemeButtonOff;
+ bdi.adornment = kThemeAdornmentNone;
+
+ if (aParams.disabled) {
+ bdi.state = kThemeStateUnavailable;
+ } else if (aParams.insideActiveWindow && aParams.pressedButton) {
+ if (*aParams.pressedButton == SpinButton::eUp) {
+ bdi.state = kThemeStatePressedUp;
+ } else {
+ bdi.state = kThemeStatePressedDown;
+ }
+ } else {
+ bdi.state = kThemeStateActive;
+ }
+
+ return bdi;
+}
+
+void nsNativeThemeCocoa::DrawSpinButtons(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const SpinButtonParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeButtonDrawInfo bdi = SpinButtonDrawInfo(kThemeIncDecButton, aParams);
+ HIThemeDrawButton(&inBoxRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawSpinButton(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ SpinButton aDrawnButton,
+ const SpinButtonParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeButtonDrawInfo bdi =
+ SpinButtonDrawInfo(kThemeIncDecButtonMini, aParams);
+
+ // Cocoa only allows kThemeIncDecButton to paint the up and down spin buttons
+ // together as a single unit (presumably because when one button is active,
+ // the appearance of both changes (in different ways)). Here we have to paint
+ // both buttons, using clip to hide the one we don't want to paint.
+ HIRect drawRect = inBoxRect;
+ drawRect.size.height *= 2;
+ if (aDrawnButton == SpinButton::eDown) {
+ drawRect.origin.y -= inBoxRect.size.height;
+ }
+
+ // Shift the drawing a little to the left, since cocoa paints with more
+ // blank space around the visual buttons than we'd like:
+ drawRect.origin.x -= 1;
+
+ CGContextSaveGState(cgContext);
+ CGContextClipToRect(cgContext, inBoxRect);
+
+ HIThemeDrawButton(&drawRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL);
+
+ CGContextRestoreGState(cgContext);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings progressSettings[2][2] = {
+ // Vertical progress bar.
+ {// Determined settings.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(10, 0), // small
+ NSMakeSize(16, 0) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ }}},
+ // There is no horizontal margin in regular undetermined size.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(10, 0), // small
+ NSMakeSize(16, 0) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 0, 1, 0} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 0, 1, 0} // regular
+ }}}},
+ // Horizontal progress bar.
+ {// Determined settings.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(0, 10), // small
+ NSMakeSize(0, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ }}},
+ // There is no horizontal margin in regular undetermined size.
+ {{
+ NSZeroSize, // mini
+ NSMakeSize(0, 10), // small
+ NSMakeSize(0, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {0, 1, 0, 1} // regular
+ },
+ {
+ // Yosemite
+ {0, 0, 0, 0}, // mini
+ {1, 1, 1, 1}, // small
+ {0, 1, 0, 1} // regular
+ }}}}};
+
+nsNativeThemeCocoa::ProgressParams nsNativeThemeCocoa::ComputeProgressParams(
+ nsIFrame* aFrame, ElementState aEventState, bool aIsHorizontal) {
+ ProgressParams params;
+ params.value = GetProgressValue(aFrame);
+ params.max = GetProgressMaxValue(aFrame);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.indeterminate = aEventState.HasState(ElementState::INDETERMINATE);
+ params.horizontal = aIsHorizontal;
+ params.rtl = IsFrameRTL(aFrame);
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawProgress(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const ProgressParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSProgressBarCell* cell = mProgressBarCell;
+
+ [cell setValue:aParams.value];
+ [cell setMax:aParams.max];
+ [cell setIndeterminate:aParams.indeterminate];
+ [cell setHorizontal:aParams.horizontal];
+ [cell
+ setControlTint:(aParams.insideActiveWindow ? [NSColor currentControlTint]
+ : NSClearControlTint)];
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = aParams.insideActiveWindow;
+ }
+ DrawCellWithSnapping(
+ cell, cgContext, inBoxRect,
+ progressSettings[aParams.horizontal][aParams.indeterminate],
+ aParams.verticalAlignFactor, mCellDrawView, aParams.rtl);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+static const CellRenderSettings meterSetting = {
+ {
+ NSMakeSize(0, 16), // mini
+ NSMakeSize(0, 16), // small
+ NSMakeSize(0, 16) // regular
+ },
+ {NSZeroSize, NSZeroSize, NSZeroSize},
+ {{
+ // Leopard
+ {1, 1, 1, 1}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ },
+ {
+ // Yosemite
+ {1, 1, 1, 1}, // mini
+ {1, 1, 1, 1}, // small
+ {1, 1, 1, 1} // regular
+ }}};
+
+nsNativeThemeCocoa::MeterParams nsNativeThemeCocoa::ComputeMeterParams(
+ nsIFrame* aFrame) {
+ nsIContent* content = aFrame->GetContent();
+ if (!(content && content->IsHTMLElement(nsGkAtoms::meter))) {
+ return MeterParams();
+ }
+
+ HTMLMeterElement* meterElement = static_cast<HTMLMeterElement*>(content);
+ MeterParams params;
+ params.value = meterElement->Value();
+ params.min = meterElement->Min();
+ params.max = meterElement->Max();
+ ElementState states = meterElement->State();
+ if (states.HasState(ElementState::SUB_OPTIMUM)) {
+ params.optimumState = OptimumState::eSubOptimum;
+ } else if (states.HasState(ElementState::SUB_SUB_OPTIMUM)) {
+ params.optimumState = OptimumState::eSubSubOptimum;
+ }
+ params.horizontal = !IsVerticalMeter(aFrame);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ params.rtl = IsFrameRTL(aFrame);
+
+ return params;
+}
+
+void nsNativeThemeCocoa::DrawMeter(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const MeterParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ NSLevelIndicatorCell* cell = mMeterBarCell;
+
+ [cell setMinValue:aParams.min];
+ [cell setMaxValue:aParams.max];
+ [cell setDoubleValue:aParams.value];
+
+ /**
+ * The way HTML and Cocoa defines the meter/indicator widget are different.
+ * So, we are going to use a trick to get the Cocoa widget showing what we
+ * are expecting: we set the warningValue or criticalValue to the current
+ * value when we want to have the widget to be in the warning or critical
+ * state.
+ */
+ switch (aParams.optimumState) {
+ case OptimumState::eOptimum:
+ [cell setWarningValue:aParams.max + 1];
+ [cell setCriticalValue:aParams.max + 1];
+ break;
+ case OptimumState::eSubOptimum:
+ [cell setWarningValue:aParams.value];
+ [cell setCriticalValue:aParams.max + 1];
+ break;
+ case OptimumState::eSubSubOptimum:
+ [cell setWarningValue:aParams.max + 1];
+ [cell setCriticalValue:aParams.value];
+ break;
+ }
+
+ HIRect rect = CGRectStandardize(inBoxRect);
+ BOOL vertical = !aParams.horizontal;
+
+ CGContextSaveGState(cgContext);
+
+ if (vertical) {
+ /**
+ * Cocoa doesn't provide a vertical meter bar so to show one, we have to
+ * show a rotated horizontal meter bar.
+ * Given that we want to show a vertical meter bar, we assume that the rect
+ * has vertical dimensions but we can't correctly draw a meter widget inside
+ * such a rectangle so we need to inverse width and height (and re-position)
+ * to get a rectangle with horizontal dimensions.
+ * Finally, we want to show a vertical meter so we want to rotate the result
+ * so it is vertical. We do that by changing the context.
+ */
+ CGFloat tmp = rect.size.width;
+ rect.size.width = rect.size.height;
+ rect.size.height = tmp;
+ rect.origin.x += rect.size.height / 2.f - rect.size.width / 2.f;
+ rect.origin.y += rect.size.width / 2.f - rect.size.height / 2.f;
+
+ CGContextTranslateCTM(cgContext, CGRectGetMidX(rect), CGRectGetMidY(rect));
+ CGContextRotateCTM(cgContext, -M_PI / 2.f);
+ CGContextTranslateCTM(cgContext, -CGRectGetMidX(rect),
+ -CGRectGetMidY(rect));
+ }
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive =
+ YES; // TODO: propagate correct activeness state
+ }
+ DrawCellWithSnapping(cell, cgContext, rect, meterSetting,
+ aParams.verticalAlignFactor, mCellDrawView,
+ !vertical && aParams.rtl);
+
+ CGContextRestoreGState(cgContext);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+void nsNativeThemeCocoa::DrawTabPanel(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ bool aIsInsideActiveWindow) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeTabPaneDrawInfo tpdi;
+
+ tpdi.version = 1;
+ tpdi.state = aIsInsideActiveWindow ? kThemeStateActive : kThemeStateInactive;
+ tpdi.direction = kThemeTabNorth;
+ tpdi.size = kHIThemeTabSizeNormal;
+ tpdi.kind = kHIThemeTabKindNormal;
+
+ HIThemeDrawTabPane(&inBoxRect, &tpdi, cgContext, HITHEME_ORIENTATION);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+Maybe<nsNativeThemeCocoa::ScaleParams>
+nsNativeThemeCocoa::ComputeHTMLScaleParams(nsIFrame* aFrame,
+ ElementState aEventState) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(aFrame);
+ if (!rangeFrame) {
+ return Nothing();
+ }
+
+ bool isHorizontal = IsRangeHorizontal(aFrame);
+
+ // ScaleParams requires integer min, max and value. This is purely for
+ // drawing, so we normalize to a range 0-1000 here.
+ ScaleParams params;
+ params.value = int32_t(rangeFrame->GetValueAsFractionOfRange() * 1000);
+ params.min = 0;
+ params.max = 1000;
+ params.reverse = !isHorizontal || rangeFrame->IsRightToLeft();
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.focused = aEventState.HasState(ElementState::FOCUSRING);
+ params.disabled = aEventState.HasState(ElementState::DISABLED);
+ params.horizontal = isHorizontal;
+ return Some(params);
+}
+
+void nsNativeThemeCocoa::DrawScale(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const ScaleParams& aParams) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ HIThemeTrackDrawInfo tdi;
+
+ tdi.version = 0;
+ tdi.kind = kThemeMediumSlider;
+ tdi.bounds = inBoxRect;
+ tdi.min = aParams.min;
+ tdi.max = aParams.max;
+ tdi.value = aParams.value;
+ tdi.attributes = kThemeTrackShowThumb;
+ if (aParams.horizontal) {
+ tdi.attributes |= kThemeTrackHorizontal;
+ }
+ if (aParams.reverse) {
+ tdi.attributes |= kThemeTrackRightToLeft;
+ }
+ if (aParams.focused) {
+ tdi.attributes |= kThemeTrackHasFocus;
+ }
+ if (aParams.disabled) {
+ tdi.enableState = kThemeTrackDisabled;
+ } else {
+ tdi.enableState =
+ aParams.insideActiveWindow ? kThemeTrackActive : kThemeTrackInactive;
+ }
+ tdi.trackInfo.slider.thumbDir = kThemeThumbPlain;
+ tdi.trackInfo.slider.pressState = 0;
+
+ HIThemeDrawTrack(&tdi, NULL, cgContext, HITHEME_ORIENTATION);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsIFrame* nsNativeThemeCocoa::SeparatorResponsibility(nsIFrame* aBefore,
+ nsIFrame* aAfter) {
+ // Usually a separator is drawn by the segment to the right of the
+ // separator, but pressed and selected segments have higher priority.
+ if (!aBefore || !aAfter) return nullptr;
+ if (IsSelectedButton(aAfter)) return aAfter;
+ if (IsSelectedButton(aBefore) || IsPressedButton(aBefore)) return aBefore;
+ return aAfter;
+}
+
+static CGRect SeparatorAdjustedRect(CGRect aRect,
+ nsNativeThemeCocoa::SegmentParams aParams) {
+ // A separator between two segments should always be located in the leftmost
+ // pixel column of the segment to the right of the separator, regardless of
+ // who ends up drawing it.
+ // CoreUI draws the separators inside the drawing rect.
+ if (!aParams.atLeftEnd && !aParams.drawsLeftSeparator) {
+ // The segment to the left of us draws the separator, so we need to make
+ // room for it.
+ aRect.origin.x += 1;
+ aRect.size.width -= 1;
+ }
+ if (aParams.drawsRightSeparator) {
+ // We draw the right separator, so we need to extend the draw rect into the
+ // segment to our right.
+ aRect.size.width += 1;
+ }
+ return aRect;
+}
+
+static NSString* ToolbarButtonPosition(BOOL aIsFirst, BOOL aIsLast) {
+ if (aIsFirst) {
+ if (aIsLast) return @"kCUISegmentPositionOnly";
+ return @"kCUISegmentPositionFirst";
+ }
+ if (aIsLast) return @"kCUISegmentPositionLast";
+ return @"kCUISegmentPositionMiddle";
+}
+
+struct SegmentedControlRenderSettings {
+ const CGFloat* heights;
+ const NSString* widgetName;
+};
+
+static const CGFloat tabHeights[3] = {17, 20, 23};
+
+static const SegmentedControlRenderSettings tabRenderSettings = {tabHeights,
+ @"tab"};
+
+static const CGFloat toolbarButtonHeights[3] = {15, 18, 22};
+
+static const SegmentedControlRenderSettings toolbarButtonRenderSettings = {
+ toolbarButtonHeights, @"kCUIWidgetButtonSegmentedSCurve"};
+
+nsNativeThemeCocoa::SegmentParams nsNativeThemeCocoa::ComputeSegmentParams(
+ nsIFrame* aFrame, ElementState aEventState, SegmentType aSegmentType) {
+ SegmentParams params;
+ params.segmentType = aSegmentType;
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ params.pressed = IsPressedButton(aFrame);
+ params.selected = IsSelectedButton(aFrame);
+ params.focused = aEventState.HasState(ElementState::FOCUSRING);
+ bool isRTL = IsFrameRTL(aFrame);
+ nsIFrame* left = GetAdjacentSiblingFrameWithSameAppearance(aFrame, isRTL);
+ nsIFrame* right = GetAdjacentSiblingFrameWithSameAppearance(aFrame, !isRTL);
+ params.atLeftEnd = !left;
+ params.atRightEnd = !right;
+ params.drawsLeftSeparator = SeparatorResponsibility(left, aFrame) == aFrame;
+ params.drawsRightSeparator = SeparatorResponsibility(aFrame, right) == aFrame;
+ params.rtl = isRTL;
+ return params;
+}
+
+static SegmentedControlRenderSettings RenderSettingsForSegmentType(
+ nsNativeThemeCocoa::SegmentType aSegmentType) {
+ switch (aSegmentType) {
+ case nsNativeThemeCocoa::SegmentType::eToolbarButton:
+ return toolbarButtonRenderSettings;
+ case nsNativeThemeCocoa::SegmentType::eTab:
+ return tabRenderSettings;
+ }
+}
+
+void nsNativeThemeCocoa::DrawSegment(CGContextRef cgContext,
+ const HIRect& inBoxRect,
+ const SegmentParams& aParams) {
+ SegmentedControlRenderSettings renderSettings =
+ RenderSettingsForSegmentType(aParams.segmentType);
+ NSControlSize controlSize =
+ FindControlSize(inBoxRect.size.height, renderSettings.heights, 4.0f);
+ CGRect drawRect = SeparatorAdjustedRect(inBoxRect, aParams);
+
+ NSDictionary* dict = @{
+ @"widget" : renderSettings.widgetName,
+ @"kCUIPresentationStateKey" :
+ (aParams.insideActiveWindow ? @"kCUIPresentationStateActiveKey"
+ : @"kCUIPresentationStateInactive"),
+ @"kCUIPositionKey" :
+ ToolbarButtonPosition(aParams.atLeftEnd, aParams.atRightEnd),
+ @"kCUISegmentLeadingSeparatorKey" :
+ [NSNumber numberWithBool:aParams.drawsLeftSeparator],
+ @"kCUISegmentTrailingSeparatorKey" :
+ [NSNumber numberWithBool:aParams.drawsRightSeparator],
+ @"value" : [NSNumber numberWithBool:aParams.selected],
+ @"state" : (aParams.pressed
+ ? @"pressed"
+ : (aParams.insideActiveWindow ? @"normal" : @"inactive")),
+ @"focus" : [NSNumber numberWithBool:aParams.focused],
+ @"size" : CUIControlSizeForCocoaSize(controlSize),
+ @"is.flipped" : [NSNumber numberWithBool:YES],
+ @"direction" : @"up"
+ };
+
+ RenderWithCoreUI(drawRect, cgContext, dict);
+}
+
+void nsNativeThemeCocoa::DrawToolbar(CGContextRef cgContext,
+ const CGRect& inBoxRect, bool aIsMain) {
+ CGRect drawRect = inBoxRect;
+
+ // top border
+ drawRect.size.height = 1.0f;
+ DrawNativeGreyColorInRect(cgContext, toolbarTopBorderGrey, drawRect, aIsMain);
+
+ // background
+ drawRect.origin.y += drawRect.size.height;
+ drawRect.size.height = inBoxRect.size.height - 2.0f;
+ DrawNativeGreyColorInRect(cgContext, toolbarFillGrey, drawRect, aIsMain);
+
+ // bottom border
+ drawRect.origin.y += drawRect.size.height;
+ drawRect.size.height = 1.0f;
+ DrawNativeGreyColorInRect(cgContext, toolbarBottomBorderGrey, drawRect,
+ aIsMain);
+}
+
+static bool ToolbarCanBeUnified(const gfx::Rect& aRect, NSWindow* aWindow) {
+ if (![aWindow isKindOfClass:[ToolbarWindow class]]) return false;
+
+ ToolbarWindow* win = (ToolbarWindow*)aWindow;
+ float unifiedToolbarHeight = [win unifiedToolbarHeight];
+ return aRect.X() == 0 && aRect.Width() >= [win frame].size.width &&
+ aRect.YMost() <= unifiedToolbarHeight;
+}
+
+void nsNativeThemeCocoa::DrawStatusBar(CGContextRef cgContext,
+ const HIRect& inBoxRect, bool aIsMain) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (inBoxRect.size.height < 2.0f) return;
+
+ CGContextSaveGState(cgContext);
+ CGContextClipToRect(cgContext, inBoxRect);
+
+ // kCUIWidgetWindowFrame draws a complete window frame with both title bar
+ // and bottom bar. We only want the bottom bar, so we extend the draw rect
+ // upwards to make space for the title bar, and then we clip it away.
+ CGRect drawRect = inBoxRect;
+ const int extendUpwards = 40;
+ drawRect.origin.y -= extendUpwards;
+ drawRect.size.height += extendUpwards;
+ RenderWithCoreUI(
+ drawRect, cgContext,
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"kCUIWidgetWindowFrame", @"widget", @"regularwin",
+ @"windowtype", (aIsMain ? @"normal" : @"inactive"),
+ @"state",
+ [NSNumber numberWithInt:inBoxRect.size.height],
+ @"kCUIWindowFrameBottomBarHeightKey",
+ [NSNumber numberWithBool:YES],
+ @"kCUIWindowFrameDrawBottomBarSeparatorKey",
+ [NSNumber numberWithBool:YES], @"is.flipped", nil]);
+
+ CGContextRestoreGState(cgContext);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsNativeThemeCocoa::DrawMultilineTextField(CGContextRef cgContext,
+ const CGRect& inBoxRect,
+ bool aIsFocused) {
+ mTextFieldCell.enabled = YES;
+ mTextFieldCell.showsFirstResponder = aIsFocused;
+
+ if (mCellDrawWindow) {
+ mCellDrawWindow.cellsShouldLookActive = YES;
+ }
+
+ // DrawCellIncludingFocusRing draws into the current NSGraphicsContext, so do
+ // the usual save+restore dance.
+ NSGraphicsContext* savedContext = NSGraphicsContext.currentContext;
+ NSGraphicsContext.currentContext =
+ [NSGraphicsContext graphicsContextWithCGContext:cgContext flipped:YES];
+ DrawCellIncludingFocusRing(mTextFieldCell, inBoxRect, mCellDrawView);
+ NSGraphicsContext.currentContext = savedContext;
+}
+
+static bool IsHiDPIContext(nsDeviceContext* aContext) {
+ return AppUnitsPerCSSPixel() >=
+ 2 * aContext->AppUnitsPerDevPixelAtUnitFullZoom();
+}
+
+Maybe<nsNativeThemeCocoa::WidgetInfo> nsNativeThemeCocoa::ComputeWidgetInfo(
+ nsIFrame* aFrame, StyleAppearance aAppearance, const nsRect& aRect) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // setup to draw into the correct port
+ int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel();
+
+ gfx::Rect nativeWidgetRect(aRect.x, aRect.y, aRect.width, aRect.height);
+ nativeWidgetRect.Scale(1.0 / gfxFloat(p2a));
+ float originalHeight = nativeWidgetRect.Height();
+ nativeWidgetRect.Round();
+ if (nativeWidgetRect.IsEmpty()) {
+ return Nothing(); // Don't attempt to draw invisible widgets.
+ }
+
+ bool hidpi = IsHiDPIContext(aFrame->PresContext()->DeviceContext());
+ if (hidpi) {
+ // Use high-resolution drawing.
+ nativeWidgetRect.Scale(0.5f);
+ originalHeight *= 0.5f;
+ }
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+
+ switch (aAppearance) {
+ case StyleAppearance::Menupopup:
+ return Nothing();
+
+ case StyleAppearance::Tooltip:
+ return Nothing();
+
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio: {
+ bool isCheckbox = aAppearance == StyleAppearance::Checkbox;
+
+ CheckboxOrRadioParams params;
+ params.state = CheckboxOrRadioState::eOff;
+ if (isCheckbox && elementState.HasState(ElementState::INDETERMINATE)) {
+ params.state = CheckboxOrRadioState::eIndeterminate;
+ } else if (elementState.HasState(ElementState::CHECKED)) {
+ params.state = CheckboxOrRadioState::eOn;
+ }
+ params.controlParams = ComputeControlParams(aFrame, elementState);
+ params.verticalAlignFactor = VerticalAlignFactor(aFrame);
+ if (isCheckbox) {
+ return Some(WidgetInfo::Checkbox(params));
+ }
+ return Some(WidgetInfo::Radio(params));
+ }
+
+ case StyleAppearance::Button:
+ if (IsDefaultButton(aFrame)) {
+ // Check whether the default button is in a document that does not
+ // match the :-moz-window-inactive pseudoclass. This activeness check
+ // is different from the other "active window" checks in this file
+ // because we absolutely need the button's default button appearance to
+ // be in sync with its text color, and the text color is changed by
+ // such a :-moz-window-inactive rule. (That's because on 10.10 and up,
+ // default buttons in active windows have blue background and white
+ // text, and default buttons in inactive windows have white background
+ // and black text.)
+ DocumentState docState = aFrame->PresContext()->Document()->State();
+ ControlParams params = ComputeControlParams(aFrame, elementState);
+ params.insideActiveWindow =
+ !docState.HasState(DocumentState::WINDOW_INACTIVE);
+ return Some(WidgetInfo::Button(
+ ButtonParams{params, ButtonType::eDefaultPushButton}));
+ }
+ if (IsButtonTypeMenu(aFrame)) {
+ ControlParams controlParams =
+ ComputeControlParams(aFrame, elementState);
+ controlParams.pressed = IsOpenButton(aFrame);
+ DropdownParams params;
+ params.controlParams = controlParams;
+ params.pullsDown = true;
+ params.editable = false;
+ return Some(WidgetInfo::Dropdown(params));
+ }
+ if (originalHeight > DO_SQUARE_BUTTON_HEIGHT) {
+ // If the button is tall enough, draw the square button style so that
+ // buttons with non-standard content look good. Otherwise draw normal
+ // rounded aqua buttons.
+ // This comparison is done based on the height that is calculated
+ // without the top, because the snapped height can be affected by the
+ // top of the rect and that may result in different height depending on
+ // the top value.
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eSquareBezelPushButton}));
+ }
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eRegularPushButton}));
+
+ case StyleAppearance::MozMacHelpButton:
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eHelpButton}));
+
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed: {
+ ButtonType buttonType =
+ (aAppearance == StyleAppearance::MozMacDisclosureButtonClosed)
+ ? ButtonType::eDisclosureButtonClosed
+ : ButtonType::eDisclosureButtonOpen;
+ return Some(WidgetInfo::Button(ButtonParams{
+ ComputeControlParams(aFrame, elementState), buttonType}));
+ }
+
+ case StyleAppearance::Spinner: {
+ bool isSpinner = (aAppearance == StyleAppearance::Spinner);
+ nsIContent* content = aFrame->GetContent();
+ if (isSpinner && content->IsHTMLElement()) {
+ // In HTML the theming for the spin buttons is drawn individually into
+ // their own backgrounds instead of being drawn into the background of
+ // their spinner parent as it is for XUL.
+ break;
+ }
+ SpinButtonParams params;
+ if (content->IsElement()) {
+ if (content->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::state, u"up"_ns, eCaseMatters)) {
+ params.pressedButton = Some(SpinButton::eUp);
+ } else if (content->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::state, u"down"_ns,
+ eCaseMatters)) {
+ params.pressedButton = Some(SpinButton::eDown);
+ }
+ }
+ params.disabled = elementState.HasState(ElementState::DISABLED);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+
+ return Some(WidgetInfo::SpinButtons(params));
+ }
+
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton: {
+ nsNumberControlFrame* numberControlFrame =
+ nsNumberControlFrame::GetNumberControlFrameForSpinButton(aFrame);
+ if (numberControlFrame) {
+ SpinButtonParams params;
+ if (numberControlFrame->SpinnerUpButtonIsDepressed()) {
+ params.pressedButton = Some(SpinButton::eUp);
+ } else if (numberControlFrame->SpinnerDownButtonIsDepressed()) {
+ params.pressedButton = Some(SpinButton::eDown);
+ }
+ params.disabled = elementState.HasState(ElementState::DISABLED);
+ params.insideActiveWindow = FrameIsInActiveWindow(aFrame);
+ if (aAppearance == StyleAppearance::SpinnerUpbutton) {
+ return Some(WidgetInfo::SpinButtonUp(params));
+ }
+ return Some(WidgetInfo::SpinButtonDown(params));
+ }
+ } break;
+
+ case StyleAppearance::Toolbarbutton: {
+ SegmentParams params = ComputeSegmentParams(aFrame, elementState,
+ SegmentType::eToolbarButton);
+ params.insideActiveWindow = [NativeWindowForFrame(aFrame) isMainWindow];
+ return Some(WidgetInfo::Segment(params));
+ }
+
+ case StyleAppearance::Separator:
+ return Some(WidgetInfo::Separator());
+
+ case StyleAppearance::Toolbar: {
+ NSWindow* win = NativeWindowForFrame(aFrame);
+ bool isMain = [win isMainWindow];
+ if (ToolbarCanBeUnified(nativeWidgetRect, win)) {
+ // Unified toolbars are drawn similar to vibrancy; we communicate their
+ // extents via the theme geometry mechanism and then place native views
+ // under Gecko's rendering. So Gecko just needs to be transparent in the
+ // place where the toolbar should be visible.
+ return Nothing();
+ }
+ return Some(WidgetInfo::Toolbar(isMain));
+ }
+
+ case StyleAppearance::MozWindowTitlebar: {
+ return Nothing();
+ }
+
+ case StyleAppearance::Statusbar:
+ return Some(WidgetInfo::StatusBar(IsActive(aFrame, YES)));
+
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist: {
+ ControlParams controlParams = ComputeControlParams(aFrame, elementState);
+ controlParams.pressed = IsOpenButton(aFrame);
+ DropdownParams params;
+ params.controlParams = controlParams;
+ params.pullsDown = false;
+ params.editable = false;
+ return Some(WidgetInfo::Dropdown(params));
+ }
+
+ case StyleAppearance::MozMenulistArrowButton:
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eArrowButton}));
+
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ return Some(
+ WidgetInfo::TextField(ComputeTextFieldParams(aFrame, elementState)));
+
+ case StyleAppearance::Searchfield:
+ return Some(WidgetInfo::SearchField(
+ ComputeTextFieldParams(aFrame, elementState)));
+
+ case StyleAppearance::ProgressBar: {
+ if (elementState.HasState(ElementState::INDETERMINATE)) {
+ if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) {
+ NS_WARNING("Unable to animate progressbar!");
+ }
+ }
+ return Some(WidgetInfo::ProgressBar(ComputeProgressParams(
+ aFrame, elementState, !IsVerticalProgress(aFrame))));
+ }
+
+ case StyleAppearance::Meter:
+ return Some(WidgetInfo::Meter(ComputeMeterParams(aFrame)));
+
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Meterchunk:
+ // Do nothing: progress and meter bars cases will draw chunks.
+ break;
+
+ case StyleAppearance::Treetwisty:
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eTreeTwistyPointingRight}));
+
+ case StyleAppearance::Treetwistyopen:
+ return Some(WidgetInfo::Button(
+ ButtonParams{ComputeControlParams(aFrame, elementState),
+ ButtonType::eTreeTwistyPointingDown}));
+
+ case StyleAppearance::Treeheadercell:
+ return Some(WidgetInfo::TreeHeaderCell(
+ ComputeTreeHeaderCellParams(aFrame, elementState)));
+
+ case StyleAppearance::Treeitem:
+ case StyleAppearance::Treeview:
+ return Some(WidgetInfo::ColorFill(sRGBColor(1.0, 1.0, 1.0, 1.0)));
+
+ case StyleAppearance::Treeheader:
+ // do nothing, taken care of by individual header cells
+ case StyleAppearance::Treeline:
+ // do nothing, these lines don't exist on macos
+ break;
+
+ case StyleAppearance::Range: {
+ Maybe<ScaleParams> params = ComputeHTMLScaleParams(aFrame, elementState);
+ if (params) {
+ return Some(WidgetInfo::Scale(*params));
+ }
+ break;
+ }
+
+ case StyleAppearance::Textarea:
+ return Some(WidgetInfo::MultilineTextField(
+ elementState.HasState(ElementState::FOCUS)));
+
+ case StyleAppearance::Listbox:
+ return Some(WidgetInfo::ListBox());
+
+ case StyleAppearance::Tab: {
+ SegmentParams params =
+ ComputeSegmentParams(aFrame, elementState, SegmentType::eTab);
+ params.pressed = params.pressed && !params.selected;
+ return Some(WidgetInfo::Segment(params));
+ }
+
+ case StyleAppearance::Tabpanels:
+ return Some(WidgetInfo::TabPanel(FrameIsInActiveWindow(aFrame)));
+
+ default:
+ break;
+ }
+
+ return Nothing();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(Nothing());
+}
+
+NS_IMETHODIMP
+nsNativeThemeCocoa::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect,
+ DrawOverflow aDrawOverflow) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return ThemeCocoa::DrawWidgetBackground(aContext, aFrame, aAppearance,
+ aRect, aDirtyRect, aDrawOverflow);
+ }
+
+ Maybe<WidgetInfo> widgetInfo = ComputeWidgetInfo(aFrame, aAppearance, aRect);
+
+ if (!widgetInfo) {
+ return NS_OK;
+ }
+
+ int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel();
+
+ gfx::Rect nativeWidgetRect = NSRectToRect(aRect, p2a);
+ nativeWidgetRect.Round();
+
+ bool hidpi = IsHiDPIContext(aFrame->PresContext()->DeviceContext());
+
+ auto colorScheme = LookAndFeel::ColorSchemeForFrame(aFrame);
+
+ RenderWidget(*widgetInfo, colorScheme, *aContext->GetDrawTarget(),
+ nativeWidgetRect, NSRectToRect(aDirtyRect, p2a),
+ hidpi ? 2.0f : 1.0f);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsNativeThemeCocoa::RenderWidget(const WidgetInfo& aWidgetInfo,
+ LookAndFeel::ColorScheme aScheme,
+ DrawTarget& aDrawTarget,
+ const gfx::Rect& aWidgetRect,
+ const gfx::Rect& aDirtyRect,
+ float aScale) {
+ // Some of the drawing below uses NSAppearance.currentAppearance behind the
+ // scenes. Set it to the appearance we want, the same way as
+ // nsLookAndFeel::NativeGetColor.
+ NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
+
+ // Also set the cell draw window's appearance; this is respected by
+ // NSTextFieldCell (and its subclass NSSearchFieldCell).
+ if (mCellDrawWindow) {
+ mCellDrawWindow.appearance = NSAppearance.currentAppearance;
+ }
+
+ const Widget widget = aWidgetInfo.Widget();
+
+ // Some widgets render using DrawTarget, and some using CGContext.
+ switch (widget) {
+ case Widget::eColorFill: {
+ sRGBColor color = aWidgetInfo.Params<sRGBColor>();
+ aDrawTarget.FillRect(aWidgetRect, ColorPattern(ToDeviceColor(color)));
+ break;
+ }
+ default: {
+ AutoRestoreTransform autoRestoreTransform(&aDrawTarget);
+ gfx::Rect widgetRect = aWidgetRect;
+ gfx::Rect dirtyRect = aDirtyRect;
+
+ dirtyRect.Scale(1.0f / aScale);
+ widgetRect.Scale(1.0f / aScale);
+ aDrawTarget.SetTransform(
+ aDrawTarget.GetTransform().PreScale(aScale, aScale));
+
+ // The remaining widgets require a CGContext.
+ CGRect macRect = CGRectMake(widgetRect.X(), widgetRect.Y(),
+ widgetRect.Width(), widgetRect.Height());
+
+ gfxQuartzNativeDrawing nativeDrawing(aDrawTarget, dirtyRect);
+
+ CGContextRef cgContext = nativeDrawing.BeginNativeDrawing();
+ if (cgContext == nullptr) {
+ // The Quartz surface handles 0x0 surfaces by internally
+ // making all operations no-ops; there's no cgcontext created for them.
+ // Unfortunately, this means that callers that want to render
+ // directly to the CGContext need to be aware of this quirk.
+ return;
+ }
+
+ // Set the context's "base transform" to in order to get correctly-sized
+ // focus rings.
+ CGContextSetBaseCTM(cgContext,
+ CGAffineTransformMakeScale(aScale, aScale));
+
+ switch (widget) {
+ case Widget::eColorFill:
+ MOZ_CRASH("already handled in outer switch");
+ break;
+ case Widget::eCheckbox: {
+ CheckboxOrRadioParams params =
+ aWidgetInfo.Params<CheckboxOrRadioParams>();
+ DrawCheckboxOrRadio(cgContext, true, macRect, params);
+ break;
+ }
+ case Widget::eRadio: {
+ CheckboxOrRadioParams params =
+ aWidgetInfo.Params<CheckboxOrRadioParams>();
+ DrawCheckboxOrRadio(cgContext, false, macRect, params);
+ break;
+ }
+ case Widget::eButton: {
+ ButtonParams params = aWidgetInfo.Params<ButtonParams>();
+ DrawButton(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eDropdown: {
+ DropdownParams params = aWidgetInfo.Params<DropdownParams>();
+ DrawDropdown(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSpinButtons: {
+ SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>();
+ DrawSpinButtons(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSpinButtonUp: {
+ SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>();
+ DrawSpinButton(cgContext, macRect, SpinButton::eUp, params);
+ break;
+ }
+ case Widget::eSpinButtonDown: {
+ SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>();
+ DrawSpinButton(cgContext, macRect, SpinButton::eDown, params);
+ break;
+ }
+ case Widget::eSegment: {
+ SegmentParams params = aWidgetInfo.Params<SegmentParams>();
+ DrawSegment(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSeparator: {
+ HIThemeSeparatorDrawInfo sdi = {0, kThemeStateActive};
+ HIThemeDrawSeparator(&macRect, &sdi, cgContext, HITHEME_ORIENTATION);
+ break;
+ }
+ case Widget::eToolbar: {
+ bool isMain = aWidgetInfo.Params<bool>();
+ DrawToolbar(cgContext, macRect, isMain);
+ break;
+ }
+ case Widget::eStatusBar: {
+ bool isMain = aWidgetInfo.Params<bool>();
+ DrawStatusBar(cgContext, macRect, isMain);
+ break;
+ }
+ case Widget::eGroupBox: {
+ HIThemeGroupBoxDrawInfo gdi = {0, kThemeStateActive,
+ kHIThemeGroupBoxKindPrimary};
+ HIThemeDrawGroupBox(&macRect, &gdi, cgContext, HITHEME_ORIENTATION);
+ break;
+ }
+ case Widget::eTextField: {
+ TextFieldParams params = aWidgetInfo.Params<TextFieldParams>();
+ DrawTextField(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eSearchField: {
+ TextFieldParams params = aWidgetInfo.Params<TextFieldParams>();
+ DrawSearchField(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eProgressBar: {
+ ProgressParams params = aWidgetInfo.Params<ProgressParams>();
+ DrawProgress(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eMeter: {
+ MeterParams params = aWidgetInfo.Params<MeterParams>();
+ DrawMeter(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eTreeHeaderCell: {
+ TreeHeaderCellParams params =
+ aWidgetInfo.Params<TreeHeaderCellParams>();
+ DrawTreeHeaderCell(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eScale: {
+ ScaleParams params = aWidgetInfo.Params<ScaleParams>();
+ DrawScale(cgContext, macRect, params);
+ break;
+ }
+ case Widget::eMultilineTextField: {
+ bool isFocused = aWidgetInfo.Params<bool>();
+ DrawMultilineTextField(cgContext, macRect, isFocused);
+ break;
+ }
+ case Widget::eListBox: {
+ // Fill the content with the control background color.
+ CGContextSetFillColorWithColor(
+ cgContext, [NSColor.controlBackgroundColor CGColor]);
+ CGContextFillRect(cgContext, macRect);
+ // Draw the frame using kCUIWidgetScrollViewFrame. This is what
+ // NSScrollView uses in
+ // -[NSScrollView drawRect:] if you give it a borderType of
+ // NSBezelBorder.
+ RenderWithCoreUI(
+ macRect, cgContext, @{
+ @"widget" : @"kCUIWidgetScrollViewFrame",
+ @"kCUIIsFlippedKey" : @YES,
+ @"kCUIVariantMetal" : @NO,
+ });
+ break;
+ }
+ case Widget::eTabPanel: {
+ bool isInsideActiveWindow = aWidgetInfo.Params<bool>();
+ DrawTabPanel(cgContext, macRect, isInsideActiveWindow);
+ break;
+ }
+ }
+
+ // Reset the base CTM.
+ CGContextSetBaseCTM(cgContext, CGAffineTransformIdentity);
+
+ nativeDrawing.EndNativeDrawing();
+ }
+ }
+}
+
+bool nsNativeThemeCocoa::CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return ThemeCocoa::CreateWebRenderCommandsForWidget(
+ aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect);
+ }
+
+ // This list needs to stay consistent with the list in DrawWidgetBackground.
+ // For every switch case in DrawWidgetBackground, there are three choices:
+ // - If the case in DrawWidgetBackground draws nothing for the given widget
+ // type, then don't list it here. We will hit the "default: return true;"
+ // case.
+ // - If the case in DrawWidgetBackground draws something simple for the given
+ // widget type, imitate that drawing using WebRender commands.
+ // - If the case in DrawWidgetBackground draws something complicated for the
+ // given widget type, return false here.
+ switch (aAppearance) {
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Button:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Separator:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Treeheadercell:
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::Treeitem:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Range:
+ return false;
+
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Tab:
+ case StyleAppearance::Tabpanels:
+ return false;
+
+ default:
+ return true;
+ }
+}
+
+LayoutDeviceIntMargin nsNativeThemeCocoa::DirectionAwareMargin(
+ const LayoutDeviceIntMargin& aMargin, nsIFrame* aFrame) {
+ // Assuming aMargin was originally specified for a horizontal LTR context,
+ // reinterpret the values as logical, and then map to physical coords
+ // according to aFrame's actual writing mode.
+ WritingMode wm = aFrame->GetWritingMode();
+ nsMargin m = LogicalMargin(wm, aMargin.top, aMargin.right, aMargin.bottom,
+ aMargin.left)
+ .GetPhysicalMargin(wm);
+ return LayoutDeviceIntMargin(m.top, m.right, m.bottom, m.left);
+}
+
+static const LayoutDeviceIntMargin kAquaDropdownBorder(1, 22, 2, 5);
+static const LayoutDeviceIntMargin kAquaComboboxBorder(3, 20, 3, 4);
+static const LayoutDeviceIntMargin kAquaSearchfieldBorder(3, 5, 2, 19);
+static const LayoutDeviceIntMargin kAquaSearchfieldBorderBigSur(5, 5, 4, 26);
+
+LayoutDeviceIntMargin nsNativeThemeCocoa::GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetWidgetBorder(aContext, aFrame, aAppearance);
+ }
+
+ LayoutDeviceIntMargin result;
+
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ if (IsButtonTypeMenu(aFrame)) {
+ result = DirectionAwareMargin(kAquaDropdownBorder, aFrame);
+ } else {
+ result =
+ DirectionAwareMargin(LayoutDeviceIntMargin(1, 7, 3, 7), aFrame);
+ }
+ break;
+ }
+
+ case StyleAppearance::Toolbarbutton: {
+ result = DirectionAwareMargin(LayoutDeviceIntMargin(1, 4, 1, 4), aFrame);
+ break;
+ }
+
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio: {
+ // nsCheckboxRadioFrame::GetIntrinsicWidth and
+ // nsCheckboxRadioFrame::GetIntrinsicHeight assume a border width of 2px.
+ result.SizeTo(2, 2, 2, 2);
+ break;
+ }
+
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ result = DirectionAwareMargin(kAquaDropdownBorder, aFrame);
+ break;
+
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield: {
+ SInt32 frameOutset = 0;
+ ::GetThemeMetric(kThemeMetricEditTextFrameOutset, &frameOutset);
+
+ SInt32 textPadding = 0;
+ ::GetThemeMetric(kThemeMetricEditTextWhitespace, &textPadding);
+
+ frameOutset += textPadding;
+
+ result.SizeTo(frameOutset, frameOutset, frameOutset, frameOutset);
+ break;
+ }
+
+ case StyleAppearance::Textarea:
+ result.SizeTo(1, 1, 1, 1);
+ break;
+
+ case StyleAppearance::Searchfield: {
+ auto border = nsCocoaFeatures::OnBigSurOrLater()
+ ? kAquaSearchfieldBorderBigSur
+ : kAquaSearchfieldBorder;
+ result = DirectionAwareMargin(border, aFrame);
+ break;
+ }
+
+ case StyleAppearance::Listbox: {
+ SInt32 frameOutset = 0;
+ ::GetThemeMetric(kThemeMetricListBoxFrameOutset, &frameOutset);
+ result.SizeTo(frameOutset, frameOutset, frameOutset, frameOutset);
+ break;
+ }
+
+ case StyleAppearance::Statusbar:
+ result.SizeTo(1, 0, 0, 0);
+ break;
+
+ default:
+ break;
+ }
+
+ if (IsHiDPIContext(aContext)) {
+ result = result + result; // doubled
+ }
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(result);
+}
+
+// Return false here to indicate that CSS padding values should be used. There
+// is no reason to make a distinction between padding and border values, just
+// specify whatever values you want in GetWidgetBorder and only use this to
+// return true if you want to override CSS padding values.
+bool nsNativeThemeCocoa::GetWidgetPadding(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetWidgetPadding(aContext, aFrame, aAppearance, aResult);
+ }
+
+ // We don't want CSS padding being used for certain widgets.
+ // See bug 381639 for an example of why.
+ switch (aAppearance) {
+ // Radios and checkboxes return a fixed size in GetMinimumWidgetSize
+ // and have a meaningful baseline, so they can't have
+ // author-specified padding.
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ aResult->SizeTo(0, 0, 0, 0);
+ return true;
+
+ case StyleAppearance::Searchfield:
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+ return true;
+ }
+ break;
+
+ default:
+ break;
+ }
+ return false;
+}
+
+bool nsNativeThemeCocoa::GetWidgetOverflow(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return ThemeCocoa::GetWidgetOverflow(aContext, aFrame, aAppearance,
+ aOverflowRect);
+ }
+ nsIntMargin overflow;
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Tab: {
+ overflow.SizeTo(static_cast<int32_t>(kMaxFocusRingWidth),
+ static_cast<int32_t>(kMaxFocusRingWidth),
+ static_cast<int32_t>(kMaxFocusRingWidth),
+ static_cast<int32_t>(kMaxFocusRingWidth));
+ break;
+ }
+ case StyleAppearance::ProgressBar: {
+ // Progress bars draw a 2 pixel white shadow under their progress
+ // indicators.
+ overflow.bottom = 2;
+ break;
+ }
+ case StyleAppearance::Meter: {
+ // Meter bars overflow their boxes by about 2 pixels.
+ overflow.SizeTo(2, 2, 2, 2);
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (IsHiDPIContext(aContext)) {
+ // Double the number of device pixels.
+ overflow += overflow;
+ }
+
+ if (overflow != nsIntMargin()) {
+ int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel();
+ aOverflowRect->Inflate(nsMargin(NSIntPixelsToAppUnits(overflow.top, p2a),
+ NSIntPixelsToAppUnits(overflow.right, p2a),
+ NSIntPixelsToAppUnits(overflow.bottom, p2a),
+ NSIntPixelsToAppUnits(overflow.left, p2a)));
+ return true;
+ }
+
+ return false;
+}
+
+LayoutDeviceIntSize nsNativeThemeCocoa::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return ThemeCocoa::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance);
+ }
+
+ LayoutDeviceIntSize result;
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ result.SizeTo(pushButtonSettings.minimumSizes[miniControlSize].width,
+ pushButtonSettings.naturalSizes[miniControlSize].height);
+ break;
+ }
+
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed: {
+ result.SizeTo(kDisclosureButtonSize.width, kDisclosureButtonSize.height);
+ break;
+ }
+
+ case StyleAppearance::MozMacHelpButton: {
+ result.SizeTo(kHelpButtonSize.width, kHelpButtonSize.height);
+ break;
+ }
+
+ case StyleAppearance::Toolbarbutton: {
+ result.SizeTo(0, toolbarButtonHeights[miniControlSize]);
+ break;
+ }
+
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton: {
+ SInt32 buttonHeight = 0, buttonWidth = 0;
+ if (aFrame->GetContent()->IsXULElement()) {
+ ::GetThemeMetric(kThemeMetricLittleArrowsWidth, &buttonWidth);
+ ::GetThemeMetric(kThemeMetricLittleArrowsHeight, &buttonHeight);
+ } else {
+ NSSize size =
+ spinnerSettings
+ .minimumSizes[EnumSizeForCocoaSize(NSControlSizeMini)];
+ buttonWidth = size.width;
+ buttonHeight = size.height;
+ if (aAppearance != StyleAppearance::Spinner) {
+ // the buttons are half the height of the spinner
+ buttonHeight /= 2;
+ }
+ }
+ result.SizeTo(buttonWidth, buttonHeight);
+ break;
+ }
+
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton: {
+ SInt32 popupHeight = 0;
+ ::GetThemeMetric(kThemeMetricPopupButtonHeight, &popupHeight);
+ result.SizeTo(0, popupHeight);
+ break;
+ }
+
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield: {
+ // at minimum, we should be tall enough for 9pt text.
+ // I'm using hardcoded values here because the appearance manager
+ // values for the frame size are incorrect.
+ result.SizeTo(0, (2 + 2) /* top */ + 9 + (1 + 1) /* bottom */);
+ break;
+ }
+
+ case StyleAppearance::MozWindowButtonBox: {
+ NSSize size = WindowButtonsSize(aFrame);
+ result.SizeTo(size.width, size.height);
+ break;
+ }
+
+ case StyleAppearance::ProgressBar: {
+ SInt32 barHeight = 0;
+ ::GetThemeMetric(kThemeMetricNormalProgressBarThickness, &barHeight);
+ result.SizeTo(0, barHeight);
+ break;
+ }
+
+ case StyleAppearance::Separator: {
+ result.SizeTo(1, 1);
+ break;
+ }
+
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen: {
+ SInt32 twistyHeight = 0, twistyWidth = 0;
+ ::GetThemeMetric(kThemeMetricDisclosureButtonWidth, &twistyWidth);
+ ::GetThemeMetric(kThemeMetricDisclosureButtonHeight, &twistyHeight);
+ result.SizeTo(twistyWidth, twistyHeight);
+ break;
+ }
+
+ case StyleAppearance::Treeheader:
+ case StyleAppearance::Treeheadercell: {
+ SInt32 headerHeight = 0;
+ ::GetThemeMetric(kThemeMetricListHeaderHeight, &headerHeight);
+ result.SizeTo(0, headerHeight);
+ break;
+ }
+
+ case StyleAppearance::Tab: {
+ result.SizeTo(0, tabHeights[miniControlSize]);
+ break;
+ }
+
+ case StyleAppearance::RangeThumb: {
+ SInt32 width = 0;
+ SInt32 height = 0;
+ ::GetThemeMetric(kThemeMetricSliderMinThumbWidth, &width);
+ ::GetThemeMetric(kThemeMetricSliderMinThumbHeight, &height);
+ result.SizeTo(width, height);
+ break;
+ }
+
+ case StyleAppearance::MozMenulistArrowButton:
+ return ThemeCocoa::GetMinimumWidgetSize(aPresContext, aFrame,
+ aAppearance);
+
+ default:
+ break;
+ }
+
+ if (IsHiDPIContext(aPresContext->DeviceContext())) {
+ result = result * 2;
+ }
+
+ return result;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntSize());
+}
+
+NS_IMETHODIMP
+nsNativeThemeCocoa::WidgetStateChanged(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) {
+ // Some widget types just never change state.
+ switch (aAppearance) {
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::Tabpanel:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Meterchunk:
+ *aShouldRepaint = false;
+ return NS_OK;
+ default:
+ break;
+ }
+
+ // XXXdwh Not sure what can really be done here. Can at least guess for
+ // specific widgets that they're highly unlikely to have certain states.
+ // For example, a toolbar doesn't care about any states.
+ if (!aAttribute) {
+ // Hover/focus/active changed. Always repaint.
+ *aShouldRepaint = true;
+ } else {
+ // Check the attribute to see if it's relevant.
+ // disabled, checked, dlgtype, default, etc.
+ *aShouldRepaint = false;
+ if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
+ aAttribute == nsGkAtoms::selected ||
+ aAttribute == nsGkAtoms::visuallyselected ||
+ aAttribute == nsGkAtoms::menuactive ||
+ aAttribute == nsGkAtoms::sortDirection ||
+ aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::_default ||
+ aAttribute == nsGkAtoms::open || aAttribute == nsGkAtoms::hover)
+ *aShouldRepaint = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNativeThemeCocoa::ThemeChanged() {
+ // This is unimplemented because we don't care if gecko changes its theme
+ // and macOS system appearance changes are handled by
+ // nsLookAndFeel::SystemWantsDarkTheme.
+ return NS_OK;
+}
+
+bool nsNativeThemeCocoa::ThemeSupportsWidget(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return ThemeCocoa::ThemeSupportsWidget(aPresContext, aFrame, aAppearance);
+ }
+ // if this is a dropdown button in a combobox the answer is always no
+ if (aAppearance == StyleAppearance::MozMenulistArrowButton) {
+ nsIFrame* parentFrame = aFrame->GetParent();
+ if (parentFrame && parentFrame->IsComboboxControlFrame()) return false;
+ }
+
+ switch (aAppearance) {
+ // Combobox dropdowns don't support native theming in vertical mode.
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MozMenulistArrowButton:
+ if (aFrame && aFrame->GetWritingMode().IsVertical()) {
+ return false;
+ }
+ [[fallthrough]];
+
+ case StyleAppearance::Listbox:
+ case StyleAppearance::MozWindowButtonBox:
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Tooltip:
+
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::MozMacUnifiedToolbarWindow:
+ case StyleAppearance::Button:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Meterchunk:
+ case StyleAppearance::Separator:
+
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::Tab:
+
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Treeheader:
+ case StyleAppearance::Treeheadercell:
+ case StyleAppearance::Treeitem:
+ case StyleAppearance::Treeline:
+
+ case StyleAppearance::Range:
+ return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool nsNativeThemeCocoa::WidgetIsContainer(StyleAppearance aAppearance) {
+ // flesh this out at some point
+ switch (aAppearance) {
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Meter:
+ case StyleAppearance::Range:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ return false;
+ default:
+ break;
+ }
+ return true;
+}
+
+bool nsNativeThemeCocoa::ThemeDrawsFocusForWidget(nsIFrame*,
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Button:
+ case StyleAppearance::MozMacHelpButton:
+ case StyleAppearance::MozMacDisclosureButtonOpen:
+ case StyleAppearance::MozMacDisclosureButtonClosed:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Range:
+ case StyleAppearance::Checkbox:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool nsNativeThemeCocoa::ThemeNeedsComboboxDropmarker() { return false; }
+
+bool nsNativeThemeCocoa::WidgetAppearanceDependsOnWindowFocus(
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::Separator:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Treeline:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Listbox:
+ return false;
+ default:
+ return true;
+ }
+}
+
+nsITheme::ThemeGeometryType nsNativeThemeCocoa::ThemeGeometryTypeForWidget(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::MozWindowTitlebar:
+ return eThemeGeometryTypeTitlebar;
+ case StyleAppearance::Toolbar:
+ return eThemeGeometryTypeToolbar;
+ case StyleAppearance::Toolbox:
+ return eThemeGeometryTypeToolbox;
+ case StyleAppearance::MozWindowButtonBox:
+ return eThemeGeometryTypeWindowButtons;
+ case StyleAppearance::Tooltip:
+ return eThemeGeometryTypeTooltip;
+ case StyleAppearance::Menupopup:
+ return eThemeGeometryTypeMenu;
+ default:
+ return eThemeGeometryTypeUnknown;
+ }
+}
+
+nsITheme::Transparency nsNativeThemeCocoa::GetWidgetTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (IsWidgetScrollbarPart(aAppearance)) {
+ return ThemeCocoa::GetWidgetTransparency(aFrame, aAppearance);
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Toolbar:
+ return eTransparent;
+ case StyleAppearance::MozMacUnifiedToolbarWindow:
+ // We want these to be treated as opaque by Gecko. We ensure there's an
+ // appropriate OS-level clear color to make sure that's the case.
+ return eOpaque;
+ case StyleAppearance::Statusbar:
+ // Knowing that scrollbars and statusbars are opaque improves
+ // performance, because we create layers for them.
+ return eOpaque;
+
+ default:
+ return eUnknownTransparency;
+ }
+}
+
+already_AddRefed<widget::Theme> do_CreateNativeThemeDoNotUseDirectly() {
+ return do_AddRef(new nsNativeThemeCocoa());
+}
diff --git a/widget/cocoa/nsNativeThemeColors.h b/widget/cocoa/nsNativeThemeColors.h
new file mode 100644
index 0000000000..02ec96f51a
--- /dev/null
+++ b/widget/cocoa/nsNativeThemeColors.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsNativeThemeColors_h_
+#define nsNativeThemeColors_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/ColorScheme.h"
+
+enum ColorName {
+ toolbarTopBorderGrey,
+ toolbarFillGrey,
+ toolbarBottomBorderGrey,
+};
+
+static const int sLionThemeColors[][2] = {
+ /* { active window, inactive window } */
+ // toolbar:
+ {0xD0, 0xF0}, // top separator line
+ {0xB2, 0xE1}, // fill color
+ {0x59, 0x87}, // bottom separator line
+};
+
+static const int sYosemiteThemeColors[][2] = {
+ /* { active window, inactive window } */
+ // toolbar:
+ {0xBD, 0xDF}, // top separator line
+ {0xD3, 0xF6}, // fill color
+ {0xB3, 0xD1}, // bottom separator line
+};
+
+inline int NativeGreyColorAsInt(ColorName name, BOOL isMain) {
+ return sYosemiteThemeColors[name][isMain ? 0 : 1];
+}
+
+inline float NativeGreyColorAsFloat(ColorName name, BOOL isMain) {
+ return NativeGreyColorAsInt(name, isMain) / 255.0f;
+}
+
+inline void DrawNativeGreyColorInRect(CGContextRef context, ColorName name,
+ CGRect rect, BOOL isMain) {
+ float grey = NativeGreyColorAsFloat(name, isMain);
+ CGContextSetRGBFillColor(context, grey, grey, grey, 1.0f);
+ CGContextFillRect(context, rect);
+}
+
+inline NSAppearance* NSAppearanceForColorScheme(mozilla::ColorScheme aScheme) {
+ NSAppearanceName appearanceName = aScheme == mozilla::ColorScheme::Light
+ ? NSAppearanceNameAqua
+ : NSAppearanceNameDarkAqua;
+ return [NSAppearance appearanceNamed:appearanceName];
+}
+
+#endif // nsNativeThemeColors_h_
diff --git a/widget/cocoa/nsPIWidgetCocoa.idl b/widget/cocoa/nsPIWidgetCocoa.idl
new file mode 100644
index 0000000000..54aa0d5113
--- /dev/null
+++ b/widget/cocoa/nsPIWidgetCocoa.idl
@@ -0,0 +1,37 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIWidget;
+
+[ptr] native NSWindowPtr(NSWindow);
+
+//
+// nsPIWidgetCocoa
+//
+// A private interface (unfrozen, private to the widget implementation) that
+// gives us access to some extra features on a widget/window.
+//
+[uuid(f75ff69e-3a51-419e-bd29-042f804bc2ed)]
+interface nsPIWidgetCocoa : nsISupports
+{
+ void SendSetZLevelEvent();
+
+ // Find the displayed child sheet (if aShown) or a child sheet that
+ // wants to be displayed (if !aShown)
+ nsIWidget GetChildSheet(in boolean aShown);
+
+ // Get the parent widget (if any) StandardCreate() was called with.
+ nsIWidget GetRealParent();
+
+ // If the object implementing this interface is a sheet, this will return the
+ // native NSWindow it is attached to
+ readonly attribute NSWindowPtr sheetWindowParent;
+
+ // True if window is a sheet
+ readonly attribute boolean isSheet;
+
+}; // nsPIWidgetCocoa
diff --git a/widget/cocoa/nsPrintDialogX.h b/widget/cocoa/nsPrintDialogX.h
new file mode 100644
index 0000000000..9c4b55647b
--- /dev/null
+++ b/widget/cocoa/nsPrintDialogX.h
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+
+#ifndef nsPrintDialog_h_
+#define nsPrintDialog_h_
+
+#include "nsIPrintDialogService.h"
+#include "nsCOMPtr.h"
+#include "nsCocoaUtils.h"
+
+#import <Cocoa/Cocoa.h>
+
+class nsIPrintSettings;
+class nsIStringBundle;
+
+class nsPrintDialogServiceX final : public nsIPrintDialogService {
+ virtual ~nsPrintDialogServiceX();
+
+ public:
+ nsPrintDialogServiceX();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRINTDIALOGSERVICE
+};
+
+@interface PrintPanelAccessoryView : NSView {
+ nsIPrintSettings* mSettings;
+ nsIStringBundle* mPrintBundle;
+ NSButton* mPrintSelectionOnlyCheckbox;
+ NSButton* mShrinkToFitCheckbox;
+ NSButton* mPrintBGColorsCheckbox;
+ NSButton* mPrintBGImagesCheckbox;
+ NSPopUpButton* mHeaderLeftList;
+ NSPopUpButton* mHeaderCenterList;
+ NSPopUpButton* mHeaderRightList;
+ NSPopUpButton* mFooterLeftList;
+ NSPopUpButton* mFooterCenterList;
+ NSPopUpButton* mFooterRightList;
+}
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings
+ haveSelection:(bool)aHaveSelection;
+
+- (void)exportSettings;
+
+@end
+
+@interface PrintPanelAccessoryController
+ : NSViewController <NSPrintPanelAccessorizing>
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings
+ haveSelection:(bool)aHaveSelection;
+
+- (void)exportSettings;
+
+@end
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintDialogServiceX, NS_IPRINTDIALOGSERVICE_IID)
+
+#endif // nsPrintDialog_h_
diff --git a/widget/cocoa/nsPrintDialogX.mm b/widget/cocoa/nsPrintDialogX.mm
new file mode 100644
index 0000000000..5cad69d547
--- /dev/null
+++ b/widget/cocoa/nsPrintDialogX.mm
@@ -0,0 +1,636 @@
+/* -*- 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 "mozilla/ArrayUtils.h"
+#include "mozilla/gfx/PrintTargetCG.h"
+#include "mozilla/Preferences.h"
+
+#include "nsPrintDialogX.h"
+#include "nsIPrintSettings.h"
+#include "nsIPrintSettingsService.h"
+#include "nsPrintSettingsX.h"
+#include "nsCOMPtr.h"
+#include "nsQueryObject.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIStringBundle.h"
+#include "nsCRT.h"
+
+#import <Cocoa/Cocoa.h>
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+using mozilla::gfx::PrintTarget;
+
+NS_IMPL_ISUPPORTS(nsPrintDialogServiceX, nsIPrintDialogService)
+
+// Splits our single pages-per-sheet count for native NSPrintInfo:
+static void setPagesPerSheet(NSPrintInfo* aPrintInfo, int32_t aPPS) {
+ int32_t across, down;
+ // Assumes portrait - we'll swap if landscape.
+ switch (aPPS) {
+ case 2:
+ across = 1;
+ down = 2;
+ break;
+ case 4:
+ across = 2;
+ down = 2;
+ break;
+ case 6:
+ across = 2;
+ down = 3;
+ break;
+ case 9:
+ across = 3;
+ down = 3;
+ break;
+ case 16:
+ across = 4;
+ down = 4;
+ break;
+ default:
+ across = 1;
+ down = 1;
+ break;
+ }
+ if ([aPrintInfo orientation] == NSPaperOrientationLandscape) {
+ std::swap(across, down);
+ }
+
+ NSMutableDictionary* dict = [aPrintInfo dictionary];
+
+ [dict setObject:[NSNumber numberWithInt:across] forKey:@"NSPagesAcross"];
+ [dict setObject:[NSNumber numberWithInt:down] forKey:@"NSPagesDown"];
+}
+
+nsPrintDialogServiceX::nsPrintDialogServiceX() {}
+
+nsPrintDialogServiceX::~nsPrintDialogServiceX() {}
+
+NS_IMETHODIMP
+nsPrintDialogServiceX::Init() { return NS_OK; }
+
+NS_IMETHODIMP
+nsPrintDialogServiceX::ShowPrintDialog(mozIDOMWindowProxy* aParent,
+ bool aHaveSelection,
+ nsIPrintSettings* aSettings) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aSettings, "aSettings must not be null");
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aSettings));
+ if (!settingsX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSPrintInfo* printInfo =
+ settingsX->CreateOrCopyPrintInfo(/* aWithScaling = */ true);
+ if (NS_WARN_IF(!printInfo)) {
+ return NS_ERROR_FAILURE;
+ }
+ [printInfo autorelease];
+
+ // Set the print job title
+ nsAutoString docName;
+ nsresult rv = aSettings->GetTitle(docName);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString adjustedTitle;
+ PrintTarget::AdjustPrintJobNameForIPP(docName, adjustedTitle);
+ CFStringRef cfTitleString = CFStringCreateWithCharacters(
+ NULL, reinterpret_cast<const UniChar*>(adjustedTitle.BeginReading()),
+ adjustedTitle.Length());
+ if (cfTitleString) {
+ auto pmPrintSettings =
+ static_cast<PMPrintSettings>([printInfo PMPrintSettings]);
+ ::PMPrintSettingsSetJobName(pmPrintSettings, cfTitleString);
+ [printInfo updateFromPMPrintSettings];
+ CFRelease(cfTitleString);
+ }
+ }
+
+ // Temporarily set the pages-per-sheet count set in our print preview to
+ // pre-populate the system dialog with the same value:
+ int32_t pagesPerSheet;
+ aSettings->GetNumPagesPerSheet(&pagesPerSheet);
+ setPagesPerSheet(printInfo, pagesPerSheet);
+
+ // Put the print info into the current print operation, since that's where
+ // [panel runModal] will look for it. We create the view because otherwise
+ // we'll get unrelated warnings printed to the console.
+ NSView* tmpView = [[NSView alloc] init];
+ NSPrintOperation* printOperation =
+ [NSPrintOperation printOperationWithView:tmpView printInfo:printInfo];
+ [NSPrintOperation setCurrentOperation:printOperation];
+
+ NSPrintPanel* panel = [NSPrintPanel printPanel];
+ [panel setOptions:NSPrintPanelShowsCopies | NSPrintPanelShowsPageRange |
+ NSPrintPanelShowsPaperSize | NSPrintPanelShowsOrientation |
+ NSPrintPanelShowsScaling];
+ PrintPanelAccessoryController* viewController =
+ [[PrintPanelAccessoryController alloc] initWithSettings:aSettings
+ haveSelection:aHaveSelection];
+ [panel addAccessoryController:viewController];
+ [viewController release];
+
+ // Show the dialog.
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ int button = [panel runModal];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ // Retrieve a printInfo with the updated settings. (The NSPrintOperation
+ // operates on a copy, so the object we passed in will not have been
+ // modified.)
+ NSPrintInfo* result = [[NSPrintOperation currentOperation] printInfo];
+ if (!result) {
+ return NS_ERROR_FAILURE;
+ }
+
+ [NSPrintOperation setCurrentOperation:nil];
+ [tmpView release];
+
+ if (button != NSModalResponseOK) {
+ return NS_ERROR_ABORT;
+ }
+
+ // We handle pages-per-sheet internally and we want to prevent the macOS
+ // printing code from also applying the pages-per-sheet count. So we need
+ // to move the count off the NSPrintInfo and over to the nsIPrintSettings.
+ NSMutableDictionary* dict = [result dictionary];
+ auto pagesAcross = [[dict objectForKey:@"NSPagesAcross"] intValue];
+ auto pagesDown = [[dict objectForKey:@"NSPagesDown"] intValue];
+ [dict setObject:[NSNumber numberWithUnsignedInt:1] forKey:@"NSPagesAcross"];
+ [dict setObject:[NSNumber numberWithUnsignedInt:1] forKey:@"NSPagesDown"];
+ aSettings->SetNumPagesPerSheet(pagesAcross * pagesDown);
+
+ // Export settings.
+ [viewController exportSettings];
+
+ // Update our settings object based on the user's choices in the dialog.
+ // We tell settingsX to adopt this printInfo so that it will be used to run
+ // print job, so that any printer-specific custom settings from print dialog
+ // extension panels will be carried through.
+ settingsX->SetFromPrintInfo(result, /* aAdoptPrintInfo = */ true);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsPrintDialogServiceX::ShowPageSetupDialog(mozIDOMWindowProxy* aParent,
+ nsIPrintSettings* aNSSettings) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(aParent, "aParent must not be null");
+ MOZ_ASSERT(aNSSettings, "aSettings must not be null");
+ NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE);
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aNSSettings));
+ if (!settingsX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSPrintInfo* printInfo =
+ settingsX->CreateOrCopyPrintInfo(/* aWithScaling = */ true);
+ if (NS_WARN_IF(!printInfo)) {
+ return NS_ERROR_FAILURE;
+ }
+ [printInfo autorelease];
+
+ NSPageLayout* pageLayout = [NSPageLayout pageLayout];
+ nsCocoaUtils::PrepareForNativeAppModalDialog();
+ int button = [pageLayout runModalWithPrintInfo:printInfo];
+ nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
+
+ if (button == NSModalResponseOK) {
+ // The Page Setup dialog does not include non-standard settings that need to
+ // be preserved, separate from what the base printSettings object handles,
+ // so we do not need it to adopt the printInfo object here.
+ settingsX->SetFromPrintInfo(printInfo, /* aAdoptPrintInfo = */ false);
+ nsCOMPtr<nsIPrintSettingsService> printSettingsService =
+ do_GetService("@mozilla.org/gfx/printsettings-service;1");
+ if (printSettingsService &&
+ Preferences::GetBool("print.save_print_settings", false)) {
+ uint32_t flags = nsIPrintSettings::kInitSavePaperSize |
+ nsIPrintSettings::kInitSaveOrientation |
+ nsIPrintSettings::kInitSaveScaling;
+ printSettingsService->MaybeSavePrintSettingsToPrefs(aNSSettings, flags);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_ABORT;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// Accessory view
+
+@interface PrintPanelAccessoryView (Private)
+
+- (NSString*)localizedString:(const char*)aKey;
+
+- (const char*)headerFooterStringForList:(NSPopUpButton*)aList;
+
+- (void)exportHeaderFooterSettings;
+
+- (void)initBundle;
+
+- (NSTextField*)label:(const char*)aLabel
+ withFrame:(NSRect)aRect
+ alignment:(NSTextAlignment)aAlignment;
+
+- (void)addLabel:(const char*)aLabel
+ withFrame:(NSRect)aRect
+ alignment:(NSTextAlignment)aAlignment;
+
+- (void)addLabel:(const char*)aLabel withFrame:(NSRect)aRect;
+
+- (void)addCenteredLabel:(const char*)aLabel withFrame:(NSRect)aRect;
+
+- (NSButton*)checkboxWithLabel:(const char*)aLabel andFrame:(NSRect)aRect;
+
+- (NSPopUpButton*)headerFooterItemListWithFrame:(NSRect)aRect
+ selectedItem:
+ (const nsAString&)aCurrentString;
+
+- (void)addOptionsSection:(bool)aHaveSelection;
+
+- (void)addAppearanceSection;
+
+- (void)addHeaderFooterSection;
+
+- (NSString*)summaryValueForCheckbox:(NSButton*)aCheckbox;
+
+- (NSString*)headerSummaryValue;
+
+- (NSString*)footerSummaryValue;
+
+@end
+
+static const char sHeaderFooterTags[][4] = {"", "&T", "&U", "&D", "&P", "&PT"};
+
+@implementation PrintPanelAccessoryView
+
+// Public methods
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings
+ haveSelection:(bool)aHaveSelection {
+ [super initWithFrame:NSMakeRect(0, 0, 540, 185)];
+
+ mSettings = aSettings;
+ [self initBundle];
+ [self addOptionsSection:aHaveSelection];
+ [self addAppearanceSection];
+ [self addHeaderFooterSection];
+
+ return self;
+}
+
+- (void)exportSettings {
+ mSettings->SetPrintSelectionOnly([mPrintSelectionOnlyCheckbox state] ==
+ NSControlStateValueOn);
+ mSettings->SetShrinkToFit([mShrinkToFitCheckbox state] ==
+ NSControlStateValueOn);
+ mSettings->SetPrintBGColors([mPrintBGColorsCheckbox state] ==
+ NSControlStateValueOn);
+ mSettings->SetPrintBGImages([mPrintBGImagesCheckbox state] ==
+ NSControlStateValueOn);
+
+ [self exportHeaderFooterSettings];
+}
+
+- (void)dealloc {
+ NS_IF_RELEASE(mPrintBundle);
+ [super dealloc];
+}
+
+// Localization
+
+- (void)initBundle {
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties",
+ &mPrintBundle);
+}
+
+- (NSString*)localizedString:(const char*)aKey {
+ if (!mPrintBundle) return @"";
+
+ nsAutoString intlString;
+ mPrintBundle->GetStringFromName(aKey, intlString);
+ NSMutableString* s = [NSMutableString
+ stringWithUTF8String:NS_ConvertUTF16toUTF8(intlString).get()];
+
+ // Remove all underscores (they're used in the GTK dialog for accesskeys).
+ [s replaceOccurrencesOfString:@"_"
+ withString:@""
+ options:0
+ range:NSMakeRange(0, [s length])];
+ return s;
+}
+
+// Widget helpers
+
+- (NSTextField*)label:(const char*)aLabel
+ withFrame:(NSRect)aRect
+ alignment:(NSTextAlignment)aAlignment {
+ NSTextField* label = [[[NSTextField alloc] initWithFrame:aRect] autorelease];
+ [label setStringValue:[self localizedString:aLabel]];
+ [label setEditable:NO];
+ [label setSelectable:NO];
+ [label setBezeled:NO];
+ [label setBordered:NO];
+ [label setDrawsBackground:NO];
+ [label setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
+ [label setAlignment:aAlignment];
+ return label;
+}
+
+- (void)addLabel:(const char*)aLabel
+ withFrame:(NSRect)aRect
+ alignment:(NSTextAlignment)aAlignment {
+ NSTextField* label = [self label:aLabel withFrame:aRect alignment:aAlignment];
+ [self addSubview:label];
+}
+
+- (void)addLabel:(const char*)aLabel withFrame:(NSRect)aRect {
+ [self addLabel:aLabel withFrame:aRect alignment:NSTextAlignmentRight];
+}
+
+- (void)addCenteredLabel:(const char*)aLabel withFrame:(NSRect)aRect {
+ [self addLabel:aLabel withFrame:aRect alignment:NSTextAlignmentCenter];
+}
+
+- (NSButton*)checkboxWithLabel:(const char*)aLabel andFrame:(NSRect)aRect {
+ aRect.origin.y += 4.0f;
+ NSButton* checkbox = [[[NSButton alloc] initWithFrame:aRect] autorelease];
+ [checkbox setButtonType:NSButtonTypeSwitch];
+ [checkbox setTitle:[self localizedString:aLabel]];
+ [checkbox setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
+ [checkbox sizeToFit];
+ return checkbox;
+}
+
+- (NSPopUpButton*)headerFooterItemListWithFrame:(NSRect)aRect
+ selectedItem:
+ (const nsAString&)aCurrentString {
+ NSPopUpButton* list = [[[NSPopUpButton alloc] initWithFrame:aRect
+ pullsDown:NO] autorelease];
+ [list setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [[list cell] setControlSize:NSControlSizeSmall];
+ NSArray* items = [NSArray
+ arrayWithObjects:[self localizedString:"headerFooterBlank"],
+ [self localizedString:"headerFooterTitle"],
+ [self localizedString:"headerFooterURL"],
+ [self localizedString:"headerFooterDate"],
+ [self localizedString:"headerFooterPage"],
+ [self localizedString:"headerFooterPageTotal"], nil];
+ [list addItemsWithTitles:items];
+
+ NS_ConvertUTF16toUTF8 currentStringUTF8(aCurrentString);
+ for (unsigned int i = 0; i < ArrayLength(sHeaderFooterTags); i++) {
+ if (!strcmp(currentStringUTF8.get(), sHeaderFooterTags[i])) {
+ [list selectItemAtIndex:i];
+ break;
+ }
+ }
+
+ return list;
+}
+
+// Build sections
+
+- (void)addOptionsSection:(bool)aHaveSelection {
+ // Title
+ [self addLabel:"optionsTitleMac" withFrame:NSMakeRect(0, 155, 151, 22)];
+
+ // "Print Selection Only"
+ mPrintSelectionOnlyCheckbox =
+ [self checkboxWithLabel:"selectionOnly"
+ andFrame:NSMakeRect(156, 155, 0, 0)];
+ [mPrintSelectionOnlyCheckbox setEnabled:aHaveSelection];
+
+ if (mSettings->GetPrintSelectionOnly()) {
+ [mPrintSelectionOnlyCheckbox setState:NSControlStateValueOn];
+ }
+
+ [self addSubview:mPrintSelectionOnlyCheckbox];
+
+ // "Shrink To Fit"
+ mShrinkToFitCheckbox = [self checkboxWithLabel:"shrinkToFit"
+ andFrame:NSMakeRect(156, 133, 0, 0)];
+
+ bool shrinkToFit;
+ mSettings->GetShrinkToFit(&shrinkToFit);
+ [mShrinkToFitCheckbox
+ setState:(shrinkToFit ? NSControlStateValueOn : NSControlStateValueOff)];
+
+ [self addSubview:mShrinkToFitCheckbox];
+}
+
+- (void)addAppearanceSection {
+ // Title
+ [self addLabel:"appearanceTitleMac" withFrame:NSMakeRect(0, 103, 151, 22)];
+
+ // "Print Background Colors"
+ mPrintBGColorsCheckbox = [self checkboxWithLabel:"printBGColors"
+ andFrame:NSMakeRect(156, 103, 0, 0)];
+
+ bool geckoBool = mSettings->GetPrintBGColors();
+ [mPrintBGColorsCheckbox
+ setState:(geckoBool ? NSControlStateValueOn : NSControlStateValueOff)];
+
+ [self addSubview:mPrintBGColorsCheckbox];
+
+ // "Print Background Images"
+ mPrintBGImagesCheckbox = [self checkboxWithLabel:"printBGImages"
+ andFrame:NSMakeRect(156, 81, 0, 0)];
+
+ geckoBool = mSettings->GetPrintBGImages();
+ [mPrintBGImagesCheckbox
+ setState:(geckoBool ? NSControlStateValueOn : NSControlStateValueOff)];
+
+ [self addSubview:mPrintBGImagesCheckbox];
+}
+
+- (void)addHeaderFooterSection {
+ // Labels
+ [self addLabel:"pageHeadersTitleMac" withFrame:NSMakeRect(0, 44, 151, 22)];
+ [self addLabel:"pageFootersTitleMac" withFrame:NSMakeRect(0, 0, 151, 22)];
+ [self addCenteredLabel:"left" withFrame:NSMakeRect(156, 22, 100, 22)];
+ [self addCenteredLabel:"center" withFrame:NSMakeRect(256, 22, 100, 22)];
+ [self addCenteredLabel:"right" withFrame:NSMakeRect(356, 22, 100, 22)];
+
+ // Lists
+ nsString sel;
+
+ mSettings->GetHeaderStrLeft(sel);
+ mHeaderLeftList =
+ [self headerFooterItemListWithFrame:NSMakeRect(156, 44, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mHeaderLeftList];
+
+ mSettings->GetHeaderStrCenter(sel);
+ mHeaderCenterList =
+ [self headerFooterItemListWithFrame:NSMakeRect(256, 44, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mHeaderCenterList];
+
+ mSettings->GetHeaderStrRight(sel);
+ mHeaderRightList =
+ [self headerFooterItemListWithFrame:NSMakeRect(356, 44, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mHeaderRightList];
+
+ mSettings->GetFooterStrLeft(sel);
+ mFooterLeftList =
+ [self headerFooterItemListWithFrame:NSMakeRect(156, 0, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mFooterLeftList];
+
+ mSettings->GetFooterStrCenter(sel);
+ mFooterCenterList =
+ [self headerFooterItemListWithFrame:NSMakeRect(256, 0, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mFooterCenterList];
+
+ mSettings->GetFooterStrRight(sel);
+ mFooterRightList =
+ [self headerFooterItemListWithFrame:NSMakeRect(356, 0, 100, 22)
+ selectedItem:sel];
+ [self addSubview:mFooterRightList];
+}
+
+// Export settings
+
+- (const char*)headerFooterStringForList:(NSPopUpButton*)aList {
+ NSInteger index = [aList indexOfSelectedItem];
+ NS_ASSERTION(index < NSInteger(ArrayLength(sHeaderFooterTags)),
+ "Index of dropdown is higher than expected!");
+ return sHeaderFooterTags[index];
+}
+
+- (void)exportHeaderFooterSettings {
+ const char* headerFooterStr;
+ headerFooterStr = [self headerFooterStringForList:mHeaderLeftList];
+ mSettings->SetHeaderStrLeft(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mHeaderCenterList];
+ mSettings->SetHeaderStrCenter(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mHeaderRightList];
+ mSettings->SetHeaderStrRight(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mFooterLeftList];
+ mSettings->SetFooterStrLeft(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mFooterCenterList];
+ mSettings->SetFooterStrCenter(NS_ConvertUTF8toUTF16(headerFooterStr));
+
+ headerFooterStr = [self headerFooterStringForList:mFooterRightList];
+ mSettings->SetFooterStrRight(NS_ConvertUTF8toUTF16(headerFooterStr));
+}
+
+// Summary
+
+- (NSString*)summaryValueForCheckbox:(NSButton*)aCheckbox {
+ if (![aCheckbox isEnabled]) return [self localizedString:"summaryNAValue"];
+
+ return [aCheckbox state] == NSControlStateValueOn
+ ? [self localizedString:"summaryOnValue"]
+ : [self localizedString:"summaryOffValue"];
+}
+
+- (NSString*)headerSummaryValue {
+ return [[mHeaderLeftList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", "
+ stringByAppendingString:
+ [[mHeaderCenterList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", " stringByAppendingString:
+ [mHeaderRightList titleOfSelectedItem]]]]];
+}
+
+- (NSString*)footerSummaryValue {
+ return [[mFooterLeftList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", "
+ stringByAppendingString:
+ [[mFooterCenterList titleOfSelectedItem]
+ stringByAppendingString:
+ [@", " stringByAppendingString:
+ [mFooterRightList titleOfSelectedItem]]]]];
+}
+
+- (NSArray*)localizedSummaryItems {
+ return [NSArray
+ arrayWithObjects:
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:
+ [self localizedString:"summarySelectionOnlyTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self summaryValueForCheckbox:mPrintSelectionOnlyCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey, nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ [self localizedString:"summaryShrinkToFitTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self summaryValueForCheckbox:mShrinkToFitCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey,
+ nil],
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:
+ [self localizedString:"summaryPrintBGColorsTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self summaryValueForCheckbox:mPrintBGColorsCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey, nil],
+ [NSDictionary
+ dictionaryWithObjectsAndKeys:
+ [self localizedString:"summaryPrintBGImagesTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self summaryValueForCheckbox:mPrintBGImagesCheckbox],
+ NSPrintPanelAccessorySummaryItemDescriptionKey, nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ [self localizedString:"summaryHeaderTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self headerSummaryValue],
+ NSPrintPanelAccessorySummaryItemDescriptionKey,
+ nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ [self localizedString:"summaryFooterTitle"],
+ NSPrintPanelAccessorySummaryItemNameKey,
+ [self footerSummaryValue],
+ NSPrintPanelAccessorySummaryItemDescriptionKey,
+ nil],
+ nil];
+}
+
+@end
+
+// Accessory controller
+
+@implementation PrintPanelAccessoryController
+
+- (id)initWithSettings:(nsIPrintSettings*)aSettings
+ haveSelection:(bool)aHaveSelection {
+ [super initWithNibName:nil bundle:nil];
+
+ NSView* accView =
+ [[PrintPanelAccessoryView alloc] initWithSettings:aSettings
+ haveSelection:aHaveSelection];
+ [self setView:accView];
+ [accView release];
+ return self;
+}
+
+- (void)exportSettings {
+ return [(PrintPanelAccessoryView*)[self view] exportSettings];
+}
+
+- (NSArray*)localizedSummaryItems {
+ return [(PrintPanelAccessoryView*)[self view] localizedSummaryItems];
+}
+
+@end
diff --git a/widget/cocoa/nsPrintSettingsServiceX.h b/widget/cocoa/nsPrintSettingsServiceX.h
new file mode 100644
index 0000000000..e6e6bff2b9
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsServiceX.h
@@ -0,0 +1,33 @@
+/* -*- 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/. */
+
+#ifndef nsPrintSettingsServiceX_h
+#define nsPrintSettingsServiceX_h
+
+#include "nsPrintSettingsService.h"
+
+namespace mozilla {
+namespace embedding {
+class PrintData;
+} // namespace embedding
+} // namespace mozilla
+
+class nsPrintSettingsServiceX final : public nsPrintSettingsService {
+ public:
+ nsPrintSettingsServiceX() {}
+
+ NS_IMETHODIMP SerializeToPrintData(
+ nsIPrintSettings* aSettings,
+ mozilla::embedding::PrintData* data) override;
+
+ NS_IMETHODIMP DeserializeToPrintSettings(
+ const mozilla::embedding::PrintData& data,
+ nsIPrintSettings* settings) override;
+
+ protected:
+ nsresult _CreatePrintSettings(nsIPrintSettings** _retval) override;
+};
+
+#endif // nsPrintSettingsServiceX_h
diff --git a/widget/cocoa/nsPrintSettingsServiceX.mm b/widget/cocoa/nsPrintSettingsServiceX.mm
new file mode 100644
index 0000000000..d2cb4486af
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsServiceX.mm
@@ -0,0 +1,78 @@
+/* -*- 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 "nsPrintSettingsServiceX.h"
+
+#include "mozilla/embedding/PPrintingTypes.h"
+#include "nsCOMPtr.h"
+#include "nsQueryObject.h"
+#include "nsPrintSettingsX.h"
+#include "nsCocoaUtils.h"
+
+using namespace mozilla::embedding;
+
+NS_IMETHODIMP
+nsPrintSettingsServiceX::SerializeToPrintData(nsIPrintSettings* aSettings,
+ PrintData* data) {
+ nsresult rv = nsPrintSettingsService::SerializeToPrintData(aSettings, data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(aSettings));
+ if (NS_WARN_IF(!settingsX)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ settingsX->GetDisposition(data->disposition());
+ settingsX->GetDestination(&data->destination());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsServiceX::DeserializeToPrintSettings(
+ const PrintData& data, nsIPrintSettings* settings) {
+ nsresult rv =
+ nsPrintSettingsService::DeserializeToPrintSettings(data, settings);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<nsPrintSettingsX> settingsX(do_QueryObject(settings));
+ if (NS_WARN_IF(!settingsX)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ settingsX->SetDisposition(data.disposition());
+ settingsX->SetDestination(data.destination());
+
+ return NS_OK;
+}
+
+nsresult nsPrintSettingsServiceX::_CreatePrintSettings(
+ nsIPrintSettings** _retval) {
+ nsresult rv;
+ *_retval = nullptr;
+
+ nsPrintSettingsX* printSettings =
+ new nsPrintSettingsX; // does not initially ref count
+ NS_ENSURE_TRUE(printSettings, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(*_retval = printSettings);
+
+ rv = printSettings->Init();
+ if (NS_FAILED(rv)) {
+ NS_RELEASE(*_retval);
+ return rv;
+ }
+
+ auto globalPrintSettings = nsIPrintSettings::kGlobalSettings;
+
+ // XXX Why is Mac special? Why are we copying global print settings here?
+ // nsPrintSettingsService::InitPrintSettingsFromPrefs already gets the few
+ // global defaults that we want, doesn't it?
+ InitPrintSettingsFromPrefs(*_retval, false, globalPrintSettings);
+ return rv;
+}
diff --git a/widget/cocoa/nsPrintSettingsX.h b/widget/cocoa/nsPrintSettingsX.h
new file mode 100644
index 0000000000..e771df25b0
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsX.h
@@ -0,0 +1,106 @@
+/* -*- 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/. */
+
+#ifndef nsPrintSettingsX_h_
+#define nsPrintSettingsX_h_
+
+#include "nsPrintSettingsImpl.h"
+#import <Cocoa/Cocoa.h>
+
+// clang-format off
+#define NS_PRINTSETTINGSX_IID \
+ { \
+ 0x0DF2FDBD, 0x906D, 0x4726, { \
+ 0x9E, 0x4D, 0xCF, 0xE0, 0x87, 0x8D, 0x70, 0x7C \
+ } \
+ }
+// clang-format on
+
+class nsPrintSettingsX : public nsPrintSettings {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_PRINTSETTINGSX_IID)
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsPrintSettingsX();
+ explicit nsPrintSettingsX(const PrintSettingsInitializer& aSettings);
+
+ nsresult Init() { return NS_OK; }
+
+ void SetDestination(uint16_t aDestination) { mDestination = aDestination; }
+ void GetDestination(uint16_t* aDestination) { *aDestination = mDestination; }
+
+ void SetDisposition(const nsString& aDisposition) {
+ mDisposition = aDisposition;
+ }
+ void GetDisposition(nsString& aDisposition) { aDisposition = mDisposition; }
+
+ // Get a Cocoa NSPrintInfo that is configured with our current settings.
+ // This follows Create semantics, so the caller is responsible to release
+ // the returned object when no longer required.
+ //
+ // Pass true for aWithScaling to have the print scaling factor included in
+ // the returned printInfo. Normally we pass false, as scaling is handled
+ // by Gecko and we don't want the Cocoa print system to impose scaling again
+ // on the output, but if we're retrieving the info in order to populate the
+ // system print UI, then we do want to know about it.
+ NSPrintInfo* CreateOrCopyPrintInfo(bool aWithScaling = false);
+
+ // Update our internal settings to reflect the properties of the given
+ // NSPrintInfo.
+ //
+ // If aAdoptPrintInfo is set, the given NSPrintInfo will be retained and
+ // returned by subsequent CreateOrCopyPrintInfo calls, which is required
+ // for custom settings from the OS print dialog to be passed through to
+ // print jobs. However, this means that subsequent changes to print settings
+ // via the generic nsPrintSettings methods will NOT be reflected in the
+ // resulting NSPrintInfo.
+ void SetFromPrintInfo(NSPrintInfo* aPrintInfo, bool aAdoptPrintInfo);
+
+ protected:
+ virtual ~nsPrintSettingsX() {
+ if (mSystemPrintInfo) {
+ [mSystemPrintInfo release];
+ }
+ };
+
+ nsPrintSettingsX& operator=(const nsPrintSettingsX& rhs);
+
+ nsresult _Clone(nsIPrintSettings** _retval) override;
+ nsresult _Assign(nsIPrintSettings* aPS) override;
+
+ int GetCocoaUnit(int16_t aGeckoUnit);
+
+ double PaperSizeFromCocoaPoints(double aPointsValue) {
+ return aPointsValue *
+ (mPaperSizeUnit == kPaperSizeInches ? 1.0 / 72.0 : 25.4 / 72.0);
+ }
+
+ double CocoaPointsFromPaperSize(double aSizeUnitValue) {
+ return aSizeUnitValue *
+ (mPaperSizeUnit == kPaperSizeInches ? 72.0 : 72.0 / 25.4);
+ }
+
+ // Needed to correctly track the various job dispositions (spool, preview,
+ // save to file) that the user can choose via the system print dialog.
+ // Unfortunately it seems to be necessary to set both the Cocoa "job
+ // disposition" and the PrintManager "destination type" in order for all the
+ // various workflows such as "Save to Web Receipts" to work.
+ nsString mDisposition;
+ uint16_t mDestination;
+
+ // If the user has used the system print UI, we retain a reference to its
+ // printInfo because it may contain settings that we don't know how to handle
+ // and that will be lost if we round-trip through nsPrintSettings fields.
+ // We'll use this printInfo if asked to run a print job.
+ //
+ // This "wrapped" printInfo is NOT serialized or copied when printSettings
+ // objects are passed around; it is used only by the settings object to which
+ // it was originally passed.
+ NSPrintInfo* mSystemPrintInfo = nullptr;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintSettingsX, NS_PRINTSETTINGSX_IID)
+
+#endif // nsPrintSettingsX_h_
diff --git a/widget/cocoa/nsPrintSettingsX.mm b/widget/cocoa/nsPrintSettingsX.mm
new file mode 100644
index 0000000000..26c5ca9c37
--- /dev/null
+++ b/widget/cocoa/nsPrintSettingsX.mm
@@ -0,0 +1,372 @@
+/* -*- 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 "nsPrintSettingsX.h"
+#include "nsObjCExceptions.h"
+
+#include "plbase64.h"
+
+#include "nsCocoaUtils.h"
+#include "nsXULAppAPI.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_print.h"
+#include "nsPrinterCUPS.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsX, nsPrintSettings, nsPrintSettingsX)
+
+nsPrintSettingsX::nsPrintSettingsX() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mDestination = kPMDestinationInvalid;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings(
+ const PrintSettingsInitializer& aSettings) {
+ RefPtr<nsPrintSettings> settings = new nsPrintSettingsX();
+ settings->InitWithInitializer(aSettings);
+ settings->SetDefaultFileName();
+ return settings.forget();
+}
+
+nsPrintSettingsX& nsPrintSettingsX::operator=(const nsPrintSettingsX& rhs) {
+ if (this == &rhs) {
+ return *this;
+ }
+
+ nsPrintSettings::operator=(rhs);
+
+ mDestination = rhs.mDestination;
+ mDisposition = rhs.mDisposition;
+
+ // We don't copy mSystemPrintInfo here, so any copied printSettings will start
+ // out without a wrapped printInfo, just using our internal settings. The
+ // system printInfo is used *only* by the nsPrintSettingsX to which it was
+ // originally passed (when the user ran a system print UI dialog).
+
+ return *this;
+}
+
+nsresult nsPrintSettingsX::_Clone(nsIPrintSettings** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ auto newSettings = MakeRefPtr<nsPrintSettingsX>();
+ *newSettings = *this;
+ newSettings.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettingsX::_Assign(nsIPrintSettings* aPS) {
+ nsPrintSettingsX* printSettingsX = static_cast<nsPrintSettingsX*>(aPS);
+ if (!printSettingsX) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ *this = *printSettingsX;
+ return NS_OK;
+}
+
+struct KnownMonochromeSetting {
+ const NSString* mName;
+ const NSString* mValue;
+};
+
+#define DECLARE_KNOWN_MONOCHROME_SETTING(key_, value_) \
+ { @key_, @value_ } \
+ ,
+static const KnownMonochromeSetting kKnownMonochromeSettings[] = {
+ CUPS_EACH_MONOCHROME_PRINTER_SETTING(DECLARE_KNOWN_MONOCHROME_SETTING)};
+#undef DECLARE_KNOWN_MONOCHROME_SETTING
+
+NSPrintInfo* nsPrintSettingsX::CreateOrCopyPrintInfo(bool aWithScaling) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // If we have a printInfo that came from the system print UI, use it so that
+ // any printer-specific settings we don't know about will still be used.
+ if (mSystemPrintInfo) {
+ NSPrintInfo* sysPrintInfo = [mSystemPrintInfo copy];
+ // Any required scaling will be done by Gecko, so we don't want it here.
+ [sysPrintInfo setScalingFactor:1.0f];
+ return sysPrintInfo;
+ }
+
+ // Note that the app shared `sharedPrintInfo` object is special! The system
+ // print dialog and print settings dialog update it with the values chosen
+ // by the user. Using that object here to initialize new nsPrintSettingsX
+ // objects could mask bugs in our code where we fail to save and/or restore
+ // print settings ourselves (e.g., bug 1636725). On other platforms we only
+ // initialize new nsPrintSettings objects from the settings that we save to
+ // prefs. Perhaps we should stop using sharedPrintInfo here for consistency?
+ NSPrintInfo* printInfo = [[NSPrintInfo sharedPrintInfo] copy];
+
+ NSSize paperSize;
+ if (GetSheetOrientation() == kPortraitOrientation) {
+ paperSize.width = CocoaPointsFromPaperSize(mPaperWidth);
+ paperSize.height = CocoaPointsFromPaperSize(mPaperHeight);
+ } else {
+ paperSize.width = CocoaPointsFromPaperSize(mPaperHeight);
+ paperSize.height = CocoaPointsFromPaperSize(mPaperWidth);
+ }
+ [printInfo setPaperSize:paperSize];
+
+ if (paperSize.width > paperSize.height) {
+ [printInfo setOrientation:NSPaperOrientationLandscape];
+ } else {
+ [printInfo setOrientation:NSPaperOrientationPortrait];
+ }
+
+ [printInfo setTopMargin:mUnwriteableMargin.top];
+ [printInfo setRightMargin:mUnwriteableMargin.right];
+ [printInfo setBottomMargin:mUnwriteableMargin.bottom];
+ [printInfo setLeftMargin:mUnwriteableMargin.left];
+
+ // If the printer name is the name of our pseudo print-to-PDF printer, the
+ // following `setPrinter` call will fail silently, since macOS doesn't know
+ // anything about it. That's OK, because mPrinter is our canonical source of
+ // truth.
+ // Actually, it seems Mac OS X 10.12 (the oldest version of Mac that we
+ // support) hangs if the printer name is not recognized. For now we explicitly
+ // check for our print-to-PDF printer, but that is not ideal since we should
+ // really localize the name of this printer at some point. Once we drop
+ // support for 10.12 we should remove this check.
+ if (!mPrinter.EqualsLiteral("Mozilla Save to PDF")) {
+ [printInfo setPrinter:[NSPrinter printerWithName:nsCocoaUtils::ToNSString(
+ mPrinter)]];
+ }
+
+ // Scaling is handled by gecko, we do NOT want the cocoa printing system to
+ // add a second scaling on top of that. So we only set the true scaling factor
+ // here if the caller explicitly asked for it.
+ [printInfo setScalingFactor:CGFloat(aWithScaling ? mScaling : 1.0f)];
+
+ const bool allPages = mPageRanges.IsEmpty();
+
+ NSMutableDictionary* dict = [printInfo dictionary];
+ [dict setObject:[NSNumber numberWithInt:mNumCopies] forKey:NSPrintCopies];
+ [dict setObject:[NSNumber numberWithBool:allPages] forKey:NSPrintAllPages];
+
+ int32_t start = 1;
+ int32_t end = 1;
+ for (size_t i = 0; i < mPageRanges.Length(); i += 2) {
+ start = std::min(start, mPageRanges[i]);
+ end = std::max(end, mPageRanges[i + 1]);
+ }
+
+ [dict setObject:[NSNumber numberWithInt:start] forKey:NSPrintFirstPage];
+ [dict setObject:[NSNumber numberWithInt:end] forKey:NSPrintLastPage];
+
+ NSURL* jobSavingURL = nullptr;
+ if (!mToFileName.IsEmpty()) {
+ jobSavingURL =
+ [NSURL fileURLWithPath:nsCocoaUtils::ToNSString(mToFileName)];
+ if (jobSavingURL) {
+ // Note: the PMPrintSettingsSetJobName call in nsPrintDialogServiceX::Show
+ // seems to mean that we get a sensible file name pre-populated in the
+ // dialog there, although our mToFileName is expected to be a full path,
+ // and it's less clear where the rest of the path (the directory to save
+ // to) in nsPrintDialogServiceX::Show comes from (perhaps from the use
+ // of `sharedPrintInfo` to initialize new nsPrintSettingsX objects).
+ [dict setObject:jobSavingURL forKey:NSPrintJobSavingURL];
+ }
+ }
+
+ if (mDisposition.IsEmpty()) {
+ // NOTE: It's unclear what to do for kOutputDestinationStream but this is
+ // only for the native print dialog where that can't happen.
+ if (mOutputDestination == kOutputDestinationFile) {
+ [printInfo setJobDisposition:NSPrintSaveJob];
+ } else {
+ [printInfo setJobDisposition:NSPrintSpoolJob];
+ }
+ } else {
+ [printInfo setJobDisposition:nsCocoaUtils::ToNSString(mDisposition)];
+ }
+
+ PMDuplexMode duplexSetting;
+ switch (mDuplex) {
+ default:
+ // This can't happen :) but if it does, we treat it as "none".
+ MOZ_FALLTHROUGH_ASSERT("Unknown duplex value");
+ case kDuplexNone:
+ duplexSetting = kPMDuplexNone;
+ break;
+ case kDuplexFlipOnLongEdge:
+ duplexSetting = kPMDuplexNoTumble;
+ break;
+ case kDuplexFlipOnShortEdge:
+ duplexSetting = kPMDuplexTumble;
+ break;
+ }
+
+ NSMutableDictionary* printSettings = [printInfo printSettings];
+ [printSettings setObject:[NSNumber numberWithUnsignedShort:duplexSetting]
+ forKey:@"com_apple_print_PrintSettings_PMDuplexing"];
+
+ if (mDestination != kPMDestinationInvalid) {
+ // Required to support PDF-workflow destinations such as Save to Web
+ // Receipts.
+ [printSettings
+ setObject:[NSNumber numberWithUnsignedShort:mDestination]
+ forKey:@"com_apple_print_PrintSettings_PMDestinationType"];
+ if (jobSavingURL) {
+ [printSettings
+ setObject:[jobSavingURL absoluteString]
+ forKey:@"com_apple_print_PrintSettings_PMOutputFilename"];
+ }
+ }
+
+ if (StaticPrefs::print_cups_monochrome_enabled() && !GetPrintInColor()) {
+ for (const auto& setting : kKnownMonochromeSettings) {
+ [printSettings setObject:setting.mValue forKey:setting.mName];
+ }
+ auto applySetting = [&](const nsACString& aKey, const nsACString& aValue) {
+ [printSettings setObject:nsCocoaUtils::ToNSString(aValue)
+ forKey:nsCocoaUtils::ToNSString(aKey)];
+ };
+ nsPrinterCUPS::ForEachExtraMonochromeSetting(applySetting);
+ }
+
+ return printInfo;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+void nsPrintSettingsX::SetFromPrintInfo(NSPrintInfo* aPrintInfo,
+ bool aAdoptPrintInfo) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // Set page-size/margins.
+ NSSize paperSize = [aPrintInfo paperSize];
+ const bool areSheetsOfPaperPortraitMode =
+ ([aPrintInfo orientation] == NSPaperOrientationPortrait);
+
+ // If our MacOS print settings say that we're producing portrait-mode sheets
+ // of paper, then our page format must also be portrait-mode; unless we've
+ // got a pages-per-sheet value with orthogonal pages/sheets, in which case
+ // it's reversed.
+ const bool arePagesPortraitMode =
+ (areSheetsOfPaperPortraitMode != HasOrthogonalPagesPerSheet());
+
+ if (arePagesPortraitMode) {
+ mOrientation = nsIPrintSettings::kPortraitOrientation;
+ SetPaperWidth(PaperSizeFromCocoaPoints(paperSize.width));
+ SetPaperHeight(PaperSizeFromCocoaPoints(paperSize.height));
+ } else {
+ mOrientation = nsIPrintSettings::kLandscapeOrientation;
+ SetPaperWidth(PaperSizeFromCocoaPoints(paperSize.height));
+ SetPaperHeight(PaperSizeFromCocoaPoints(paperSize.width));
+ }
+
+ mUnwriteableMargin.top = static_cast<int32_t>([aPrintInfo topMargin]);
+ mUnwriteableMargin.right = static_cast<int32_t>([aPrintInfo rightMargin]);
+ mUnwriteableMargin.bottom = static_cast<int32_t>([aPrintInfo bottomMargin]);
+ mUnwriteableMargin.left = static_cast<int32_t>([aPrintInfo leftMargin]);
+
+ if (aAdoptPrintInfo) {
+ // Keep a reference to the printInfo; it may have settings that we don't
+ // know how to handle otherwise.
+ if (mSystemPrintInfo != aPrintInfo) {
+ if (mSystemPrintInfo) {
+ [mSystemPrintInfo release];
+ }
+ mSystemPrintInfo = aPrintInfo;
+ [mSystemPrintInfo retain];
+ }
+ } else {
+ // Clear any stored printInfo.
+ if (mSystemPrintInfo) {
+ [mSystemPrintInfo release];
+ mSystemPrintInfo = nullptr;
+ }
+ }
+
+ nsCocoaUtils::GetStringForNSString([[aPrintInfo printer] name], mPrinter);
+
+ // Only get the scaling value if shrink-to-fit is not selected:
+ bool isShrinkToFitChecked;
+ GetShrinkToFit(&isShrinkToFitChecked);
+ if (!isShrinkToFitChecked) {
+ // Limit scaling precision to whole percentage values.
+ mScaling = round(double([aPrintInfo scalingFactor]) * 100.0) / 100.0;
+ }
+
+ mOutputDestination = [&] {
+ if ([aPrintInfo jobDisposition] == NSPrintSaveJob) {
+ return kOutputDestinationFile;
+ }
+ return kOutputDestinationPrinter;
+ }();
+
+ NSDictionary* dict = [aPrintInfo dictionary];
+ const char* filePath =
+ [[dict objectForKey:NSPrintJobSavingURL] fileSystemRepresentation];
+ if (filePath && *filePath) {
+ CopyUTF8toUTF16(Span(filePath, strlen(filePath)), mToFileName);
+ }
+
+ nsCocoaUtils::GetStringForNSString([aPrintInfo jobDisposition], mDisposition);
+
+ mNumCopies = [[dict objectForKey:NSPrintCopies] intValue];
+ mPageRanges.Clear();
+ if (![[dict objectForKey:NSPrintAllPages] boolValue]) {
+ mPageRanges.AppendElement([[dict objectForKey:NSPrintFirstPage] intValue]);
+ mPageRanges.AppendElement([[dict objectForKey:NSPrintLastPage] intValue]);
+ }
+
+ NSDictionary* printSettings = [aPrintInfo printSettings];
+ NSNumber* value =
+ [printSettings objectForKey:@"com_apple_print_PrintSettings_PMDuplexing"];
+ if (value) {
+ PMDuplexMode duplexSetting = [value unsignedShortValue];
+ switch (duplexSetting) {
+ default:
+ // An unknown value is treated as None.
+ MOZ_FALLTHROUGH_ASSERT("Unknown duplex value");
+ case kPMDuplexNone:
+ mDuplex = kDuplexNone;
+ break;
+ case kPMDuplexNoTumble:
+ mDuplex = kDuplexFlipOnLongEdge;
+ break;
+ case kPMDuplexTumble:
+ mDuplex = kDuplexFlipOnShortEdge;
+ break;
+ }
+ } else {
+ // By default a printSettings dictionary doesn't initially contain the
+ // duplex key at all, so this is not an error; its absence just means no
+ // duplexing has been requested, so we return kDuplexNone.
+ mDuplex = kDuplexNone;
+ }
+
+ value = [printSettings
+ objectForKey:@"com_apple_print_PrintSettings_PMDestinationType"];
+ if (value) {
+ mDestination = [value unsignedShortValue];
+ }
+
+ const bool color = [&] {
+ if (StaticPrefs::print_cups_monochrome_enabled()) {
+ for (const auto& setting : kKnownMonochromeSettings) {
+ NSString* value = [printSettings objectForKey:setting.mName];
+ if (!value) {
+ continue;
+ }
+ if ([setting.mValue isEqualToString:value]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }();
+
+ SetPrintInColor(color);
+
+ SetIsInitializedFromPrinter(true);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
diff --git a/widget/cocoa/nsSandboxViolationSink.h b/widget/cocoa/nsSandboxViolationSink.h
new file mode 100644
index 0000000000..d4b6e7ce07
--- /dev/null
+++ b/widget/cocoa/nsSandboxViolationSink.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 20; 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/. */
+
+#ifndef nsSandboxViolationSink_h_
+#define nsSandboxViolationSink_h_
+
+#include <stdint.h>
+
+// Class for tracking sandbox violations. Currently it just logs them to
+// stdout and the system console. In the future it may do more.
+
+// What makes this possible is the fact that Apple' sandboxd calls
+// notify_post("com.apple.sandbox.violation.*") whenever it's notified by the
+// Sandbox kernel extension of a sandbox violation. We register to receive
+// these notifications. But the notifications are empty, and are sent for
+// every violation in every process. So we need to do more to get only "our"
+// violations, and to find out what kind of violation they were. See the
+// implementation of nsSandboxViolationSink::ViolationHandler().
+
+#define SANDBOX_VIOLATION_QUEUE_NAME "org.mozilla.sandbox.violation.queue"
+#define SANDBOX_VIOLATION_NOTIFICATION_NAME "com.apple.sandbox.violation.*"
+
+class nsSandboxViolationSink {
+ public:
+ static void Start();
+ static void Stop();
+
+ private:
+ static void ViolationHandler();
+ static int mNotifyToken;
+ static uint64_t mLastMsgReceived;
+};
+
+#endif // nsSandboxViolationSink_h_
diff --git a/widget/cocoa/nsSandboxViolationSink.mm b/widget/cocoa/nsSandboxViolationSink.mm
new file mode 100644
index 0000000000..611e0387b1
--- /dev/null
+++ b/widget/cocoa/nsSandboxViolationSink.mm
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 20; 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 "nsSandboxViolationSink.h"
+
+#import <Foundation/NSObjCRuntime.h>
+
+#include <unistd.h>
+#include <time.h>
+#include <asl.h>
+#include <dispatch/dispatch.h>
+#include <notify.h>
+#include "mozilla/Preferences.h"
+#include "mozilla/Sprintf.h"
+
+int nsSandboxViolationSink::mNotifyToken = 0;
+uint64_t nsSandboxViolationSink::mLastMsgReceived = 0;
+
+void nsSandboxViolationSink::Start() {
+ if (mNotifyToken) {
+ return;
+ }
+ notify_register_dispatch(SANDBOX_VIOLATION_NOTIFICATION_NAME, &mNotifyToken,
+ dispatch_queue_create(SANDBOX_VIOLATION_QUEUE_NAME,
+ DISPATCH_QUEUE_SERIAL),
+ ^(int token) {
+ ViolationHandler();
+ });
+}
+
+void nsSandboxViolationSink::Stop() {
+ if (!mNotifyToken) {
+ return;
+ }
+ notify_cancel(mNotifyToken);
+ mNotifyToken = 0;
+}
+
+// We need to query syslogd to find out what violations occurred, and whether
+// they were "ours". We can use the Apple System Log facility to do this.
+// Besides calling notify_post("com.apple.sandbox.violation.*"), Apple's
+// sandboxd also reports all sandbox violations (sent to it by the Sandbox
+// kernel extension) to syslogd, which stores them and makes them viewable
+// in the system console. This is the database we query.
+
+// ViolationHandler() is always called on its own secondary thread. This
+// makes it unlikely it will interfere with other browser activity.
+
+void nsSandboxViolationSink::ViolationHandler() {
+ aslmsg query = asl_new(ASL_TYPE_QUERY);
+
+ asl_set_query(query, ASL_KEY_FACILITY, "com.apple.sandbox",
+ ASL_QUERY_OP_EQUAL);
+
+ // Only get reports that were generated very recently.
+ char query_time[30] = {0};
+ SprintfLiteral(query_time, "%li", time(NULL) - 2);
+ asl_set_query(query, ASL_KEY_TIME, query_time,
+ ASL_QUERY_OP_NUMERIC | ASL_QUERY_OP_GREATER_EQUAL);
+
+ // This code is easier to test if we don't just track "our" violations,
+ // which are (normally) few and far between. For example (for the time
+ // being at least) four appleeventsd sandbox violations happen every time
+ // we start the browser in e10s mode. But it makes sense to default to
+ // only tracking "our" violations.
+ if (mozilla::Preferences::GetBool(
+ "security.sandbox.mac.track.violations.oursonly", true)) {
+ // This makes each of our processes log its own violations. It might
+ // be better to make the chrome process log all the other processes'
+ // violations.
+ char query_pid[20] = {0};
+ SprintfLiteral(query_pid, "%u", getpid());
+ asl_set_query(query, ASL_KEY_REF_PID, query_pid, ASL_QUERY_OP_EQUAL);
+ }
+
+ aslresponse response = asl_search(nullptr, query);
+
+ // Each time ViolationHandler() is called we grab as many messages as are
+ // available. Otherwise we might not get them all.
+ if (response) {
+ while (true) {
+ aslmsg hit = nullptr;
+ aslmsg found = nullptr;
+ const char* id_str;
+
+ while ((hit = aslresponse_next(response))) {
+ // Record the message id to avoid logging the same violation more
+ // than once.
+ id_str = asl_get(hit, ASL_KEY_MSG_ID);
+ uint64_t id_val = atoll(id_str);
+ if (id_val <= mLastMsgReceived) {
+ continue;
+ }
+ mLastMsgReceived = id_val;
+ found = hit;
+ break;
+ }
+ if (!found) {
+ break;
+ }
+
+ const char* pid_str = asl_get(found, ASL_KEY_REF_PID);
+ const char* message_str = asl_get(found, ASL_KEY_MSG);
+ NSLog(@"nsSandboxViolationSink::ViolationHandler(): id %s, pid %s, "
+ @"message %s",
+ id_str, pid_str, message_str);
+ }
+ aslresponse_free(response);
+ }
+}
diff --git a/widget/cocoa/nsSound.h b/widget/cocoa/nsSound.h
new file mode 100644
index 0000000000..cf3b02bd77
--- /dev/null
+++ b/widget/cocoa/nsSound.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsSound_h_
+#define nsSound_h_
+
+#include "nsISound.h"
+#include "nsIStreamLoader.h"
+
+class nsSound : public nsISound, public nsIStreamLoaderObserver {
+ public:
+ nsSound();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOUND
+ NS_DECL_NSISTREAMLOADEROBSERVER
+
+ protected:
+ virtual ~nsSound();
+};
+
+#endif // nsSound_h_
diff --git a/widget/cocoa/nsSound.mm b/widget/cocoa/nsSound.mm
new file mode 100644
index 0000000000..cbaad44be7
--- /dev/null
+++ b/widget/cocoa/nsSound.mm
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsSound.h"
+#include "nsContentUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsNetUtil.h"
+#include "nsCOMPtr.h"
+#include "nsIURL.h"
+#include "nsString.h"
+
+#import <Cocoa/Cocoa.h>
+
+NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver)
+
+nsSound::nsSound() {}
+
+nsSound::~nsSound() {}
+
+NS_IMETHODIMP
+nsSound::Beep() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSBeep();
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsSound::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* context,
+ nsresult aStatus, uint32_t dataLen,
+ const uint8_t* data) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSData* value = [NSData dataWithBytes:data length:dataLen];
+
+ NSSound* sound = [[NSSound alloc] initWithData:value];
+
+ [sound play];
+
+ [sound autorelease];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsSound::Play(nsIURL* aURL) {
+ nsCOMPtr<nsIURI> uri(aURL);
+ nsCOMPtr<nsIStreamLoader> loader;
+ return NS_NewStreamLoader(
+ getter_AddRefs(loader), uri,
+ this, // aObserver
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+}
+
+NS_IMETHODIMP
+nsSound::Init() { return NS_OK; }
+
+NS_IMETHODIMP
+nsSound::PlayEventSound(uint32_t aEventId) {
+ // Mac doesn't have system sound settings for each user actions.
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsStandaloneNativeMenu.h b/widget/cocoa/nsStandaloneNativeMenu.h
new file mode 100644
index 0000000000..9d43e5e64d
--- /dev/null
+++ b/widget/cocoa/nsStandaloneNativeMenu.h
@@ -0,0 +1,27 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsStandaloneNativeMenu_h_
+#define nsStandaloneNativeMenu_h_
+
+#include "nsIStandaloneNativeMenu.h"
+#include "NativeMenuMac.h"
+
+class nsStandaloneNativeMenu : public nsIStandaloneNativeMenu {
+ public:
+ nsStandaloneNativeMenu();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTANDALONENATIVEMENU
+
+ RefPtr<mozilla::widget::NativeMenuMac> GetNativeMenu() { return mMenu; }
+
+ protected:
+ virtual ~nsStandaloneNativeMenu();
+
+ RefPtr<mozilla::widget::NativeMenuMac> mMenu;
+};
+
+#endif
diff --git a/widget/cocoa/nsStandaloneNativeMenu.mm b/widget/cocoa/nsStandaloneNativeMenu.mm
new file mode 100644
index 0000000000..d5a70f817e
--- /dev/null
+++ b/widget/cocoa/nsStandaloneNativeMenu.mm
@@ -0,0 +1,81 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsStandaloneNativeMenu.h"
+
+#include "mozilla/dom/Element.h"
+#include "NativeMenuMac.h"
+#include "nsISupports.h"
+#include "nsGkAtoms.h"
+
+using namespace mozilla;
+
+using mozilla::dom::Element;
+
+NS_IMPL_ISUPPORTS(nsStandaloneNativeMenu, nsIStandaloneNativeMenu)
+
+nsStandaloneNativeMenu::nsStandaloneNativeMenu() = default;
+
+nsStandaloneNativeMenu::~nsStandaloneNativeMenu() = default;
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::Init(Element* aElement) {
+ NS_ASSERTION(mMenu == nullptr, "nsNativeMenu::Init - mMenu not null!");
+
+ NS_ENSURE_ARG(aElement);
+
+ if (!aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mMenu = new mozilla::widget::NativeMenuMac(aElement);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::MenuWillOpen(bool* aResult) {
+ NS_ASSERTION(mMenu != nullptr,
+ "nsStandaloneNativeMenu::OnOpen - mMenu is null!");
+
+ mMenu->MenuWillOpen();
+
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::ActivateNativeMenuItemAt(
+ const nsAString& aIndexString) {
+ if (!mMenu) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (mMenu->ActivateNativeMenuItemAt(aIndexString)) {
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ if (!mMenu) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mMenu->ForceUpdateNativeMenuAt(aIndexString);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStandaloneNativeMenu::Dump() {
+ mMenu->Dump();
+
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsSystemStatusBarCocoa.h b/widget/cocoa/nsSystemStatusBarCocoa.h
new file mode 100644
index 0000000000..2af13e2805
--- /dev/null
+++ b/widget/cocoa/nsSystemStatusBarCocoa.h
@@ -0,0 +1,40 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsSystemStatusBarCocoa_h_
+#define nsSystemStatusBarCocoa_h_
+
+#include "mozilla/RefPtr.h"
+#include "nsISystemStatusBar.h"
+#include "nsClassHashtable.h"
+
+namespace mozilla::widget {
+class NativeMenuMac;
+}
+@class NSStatusItem;
+
+class nsSystemStatusBarCocoa : public nsISystemStatusBar {
+ public:
+ nsSystemStatusBarCocoa() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISYSTEMSTATUSBAR
+
+ protected:
+ virtual ~nsSystemStatusBarCocoa() {}
+
+ struct StatusItem {
+ explicit StatusItem(mozilla::widget::NativeMenuMac* aMenu);
+ ~StatusItem();
+
+ private:
+ RefPtr<mozilla::widget::NativeMenuMac> mMenu;
+ NSStatusItem* mStatusItem;
+ };
+
+ nsClassHashtable<nsISupportsHashKey, StatusItem> mItems;
+};
+
+#endif // nsSystemStatusBarCocoa_h_
diff --git a/widget/cocoa/nsSystemStatusBarCocoa.mm b/widget/cocoa/nsSystemStatusBarCocoa.mm
new file mode 100644
index 0000000000..f46ee0ce3e
--- /dev/null
+++ b/widget/cocoa/nsSystemStatusBarCocoa.mm
@@ -0,0 +1,70 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsComponentManagerUtils.h"
+#include "nsSystemStatusBarCocoa.h"
+#include "NativeMenuMac.h"
+#include "nsObjCExceptions.h"
+#include "mozilla/dom/Element.h"
+
+using mozilla::dom::Element;
+using mozilla::widget::NativeMenuMac;
+
+NS_IMPL_ISUPPORTS(nsSystemStatusBarCocoa, nsISystemStatusBar)
+
+NS_IMETHODIMP
+nsSystemStatusBarCocoa::AddItem(Element* aElement) {
+ if (!aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<NativeMenuMac> menu = new NativeMenuMac(aElement);
+
+ nsCOMPtr<nsISupports> keyPtr = aElement;
+ mItems.InsertOrUpdate(keyPtr, mozilla::MakeUnique<StatusItem>(menu));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSystemStatusBarCocoa::RemoveItem(Element* aElement) {
+ mItems.Remove(aElement);
+ return NS_OK;
+}
+
+nsSystemStatusBarCocoa::StatusItem::StatusItem(NativeMenuMac* aMenu)
+ : mMenu(aMenu) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ MOZ_COUNT_CTOR(nsSystemStatusBarCocoa::StatusItem);
+
+ mStatusItem = [[NSStatusBar.systemStatusBar
+ statusItemWithLength:NSSquareStatusItemLength] retain];
+ mStatusItem.menu = mMenu->NativeNSMenu();
+ mStatusItem.highlightMode = YES;
+
+ // We want the status item to get its image from menu item that mMenu was
+ // initialized with. Icon loads are asynchronous, so we need to let the menu
+ // know about the item so that it can update its icon as soon as it has
+ // loaded.
+ mMenu->SetContainerStatusBarItem(mStatusItem);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsSystemStatusBarCocoa::StatusItem::~StatusItem() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenu->SetContainerStatusBarItem(nil);
+ [NSStatusBar.systemStatusBar removeStatusItem:mStatusItem];
+ [mStatusItem release];
+ mStatusItem = nil;
+
+ MOZ_COUNT_DTOR(nsSystemStatusBarCocoa::StatusItem);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
diff --git a/widget/cocoa/nsToolkit.h b/widget/cocoa/nsToolkit.h
new file mode 100644
index 0000000000..46b08d0ebe
--- /dev/null
+++ b/widget/cocoa/nsToolkit.h
@@ -0,0 +1,49 @@
+/* -*- 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/. */
+
+#ifndef nsToolkit_h_
+#define nsToolkit_h_
+
+#include "nscore.h"
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+#import <objc/Object.h>
+#import <IOKit/IOKitLib.h>
+
+class nsToolkit {
+ public:
+ nsToolkit();
+ virtual ~nsToolkit();
+
+ static nsToolkit* GetToolkit();
+
+ static void Shutdown() {
+ delete gToolkit;
+ gToolkit = nullptr;
+ }
+
+ static void PostSleepWakeNotification(const char* aNotification);
+
+ static nsresult SwizzleMethods(Class aClass, SEL orgMethod, SEL posedMethod,
+ bool classMethods = false);
+
+ void MonitorAllProcessMouseEvents();
+ void StopMonitoringAllProcessMouseEvents();
+
+ protected:
+ nsresult RegisterForSleepWakeNotifications();
+ void RemoveSleepWakeNotifications();
+
+ protected:
+ static nsToolkit* gToolkit;
+
+ CFRunLoopSourceRef mSleepWakeNotificationRLS;
+ io_object_t mPowerNotifier;
+
+ id mAllProcessMouseMonitor;
+};
+
+#endif // nsToolkit_h_
diff --git a/widget/cocoa/nsToolkit.mm b/widget/cocoa/nsToolkit.mm
new file mode 100644
index 0000000000..065100a107
--- /dev/null
+++ b/widget/cocoa/nsToolkit.mm
@@ -0,0 +1,265 @@
+/* -*- 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 "nsToolkit.h"
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <mach/mach_port.h>
+#include <mach/mach_interface.h>
+#include <mach/mach_init.h>
+
+extern "C" {
+#include <mach-o/getsect.h>
+}
+#include <unistd.h>
+#include <dlfcn.h>
+
+#import <Cocoa/Cocoa.h>
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#import <IOKit/IOMessage.h>
+
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+
+#include "nsGkAtoms.h"
+#include "nsIRollupListener.h"
+#include "nsIWidget.h"
+#include "nsBaseWidget.h"
+
+#include "nsIObserverService.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+
+#include "NativeMenuSupport.h"
+
+using namespace mozilla;
+
+static io_connect_t gRootPort = MACH_PORT_NULL;
+
+nsToolkit* nsToolkit::gToolkit = nullptr;
+
+nsToolkit::nsToolkit()
+ : mSleepWakeNotificationRLS(nullptr),
+ mPowerNotifier{0},
+ mAllProcessMouseMonitor(nil) {
+ MOZ_COUNT_CTOR(nsToolkit);
+ RegisterForSleepWakeNotifications();
+}
+
+nsToolkit::~nsToolkit() {
+ MOZ_COUNT_DTOR(nsToolkit);
+ RemoveSleepWakeNotifications();
+ StopMonitoringAllProcessMouseEvents();
+}
+
+void nsToolkit::PostSleepWakeNotification(const char* aNotification) {
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService)
+ observerService->NotifyObservers(nullptr, aNotification, nullptr);
+}
+
+// http://developer.apple.com/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/PowerMgmt/chapter_10_section_3.html
+static void ToolkitSleepWakeCallback(void* refCon, io_service_t service,
+ natural_t messageType,
+ void* messageArgument) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ switch (messageType) {
+ case kIOMessageSystemWillSleep:
+ // System is going to sleep now.
+ nsToolkit::PostSleepWakeNotification(NS_WIDGET_SLEEP_OBSERVER_TOPIC);
+ ::IOAllowPowerChange(gRootPort, (long)messageArgument);
+ break;
+
+ case kIOMessageCanSystemSleep:
+ // In this case, the computer has been idle for several minutes
+ // and will sleep soon so you must either allow or cancel
+ // this notification. Important: if you don’t respond, there will
+ // be a 30-second timeout before the computer sleeps.
+ // In Mozilla's case, we always allow sleep.
+ ::IOAllowPowerChange(gRootPort, (long)messageArgument);
+ break;
+
+ case kIOMessageSystemHasPoweredOn:
+ // Handle wakeup.
+ nsToolkit::PostSleepWakeNotification(NS_WIDGET_WAKE_OBSERVER_TOPIC);
+ break;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+nsresult nsToolkit::RegisterForSleepWakeNotifications() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ IONotificationPortRef notifyPortRef;
+
+ NS_ASSERTION(!mSleepWakeNotificationRLS, "Already registered for sleep/wake");
+
+ gRootPort = ::IORegisterForSystemPower(
+ 0, &notifyPortRef, ToolkitSleepWakeCallback, &mPowerNotifier);
+ if (gRootPort == MACH_PORT_NULL) {
+ NS_ERROR("IORegisterForSystemPower failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ mSleepWakeNotificationRLS =
+ ::IONotificationPortGetRunLoopSource(notifyPortRef);
+ ::CFRunLoopAddSource(::CFRunLoopGetCurrent(), mSleepWakeNotificationRLS,
+ kCFRunLoopDefaultMode);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsToolkit::RemoveSleepWakeNotifications() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mSleepWakeNotificationRLS) {
+ ::IODeregisterForSystemPower(&mPowerNotifier);
+ ::CFRunLoopRemoveSource(::CFRunLoopGetCurrent(), mSleepWakeNotificationRLS,
+ kCFRunLoopDefaultMode);
+
+ mSleepWakeNotificationRLS = nullptr;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Cocoa Firefox's use of custom context menus requires that we explicitly
+// handle mouse events from other processes that the OS handles
+// "automatically" for native context menus -- mouseMoved events so that
+// right-click context menus work properly when our browser doesn't have the
+// focus (bmo bug 368077), and mouseDown events so that our browser can
+// dismiss a context menu when a mouseDown happens in another process (bmo
+// bug 339945).
+void nsToolkit::MonitorAllProcessMouseEvents() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus()) {
+ // Don't do this if we are using native context menus.
+ return;
+ }
+
+ if (getenv("MOZ_NO_GLOBAL_MOUSE_MONITOR")) return;
+
+ if (mAllProcessMouseMonitor == nil) {
+ mAllProcessMouseMonitor = [NSEvent
+ addGlobalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown |
+ NSEventMaskLeftMouseDown
+ handler:^(NSEvent* evt) {
+ if ([NSApp isActive]) {
+ return;
+ }
+
+ nsIRollupListener* rollupListener =
+ nsBaseWidget::
+ GetActiveRollupListener();
+ if (!rollupListener) {
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> rollupWidget =
+ rollupListener->GetRollupWidget();
+ if (!rollupWidget) {
+ return;
+ }
+
+ NSWindow* ctxMenuWindow =
+ (NSWindow*)
+ rollupWidget->GetNativeData(
+ NS_NATIVE_WINDOW);
+ if (!ctxMenuWindow) {
+ return;
+ }
+
+ // Don't roll up the rollup widget if
+ // our mouseDown happens over it (doing
+ // so would break the corresponding
+ // context menu).
+ NSPoint screenLocation =
+ [NSEvent mouseLocation];
+ if (NSPointInRect(
+ screenLocation,
+ [ctxMenuWindow frame])) {
+ return;
+ }
+
+ rollupListener->Rollup({});
+ }];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+void nsToolkit::StopMonitoringAllProcessMouseEvents() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mAllProcessMouseMonitor != nil) {
+ [NSEvent removeMonitor:mAllProcessMouseMonitor];
+ mAllProcessMouseMonitor = nil;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Return the nsToolkit instance. If a toolkit does not yet exist, then one
+// will be created.
+// static
+nsToolkit* nsToolkit::GetToolkit() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!gToolkit) {
+ gToolkit = new nsToolkit();
+ }
+
+ return gToolkit;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+}
+
+// An alternative to [NSObject poseAsClass:] that isn't deprecated on OS X
+// Leopard and is available to 64-bit binaries on Leopard and above. Based on
+// ideas and code from http://www.cocoadev.com/index.pl?MethodSwizzling.
+// Since the Method type becomes an opaque type as of Objective-C 2.0, we'll
+// have to switch to using accessor methods like
+// method_exchangeImplementations() when we build 64-bit binaries that use
+// Objective-C 2.0 (on and for Leopard and above).
+//
+// Be aware that, if aClass doesn't have an orgMethod selector but one of its
+// superclasses does, the method substitution will (in effect) take place in
+// that superclass (rather than in aClass itself). The substitution has
+// effect on the class where it takes place and all of that class's
+// subclasses. In order for method swizzling to work properly, posedMethod
+// needs to be unique in the class where the substitution takes place and all
+// of its subclasses.
+nsresult nsToolkit::SwizzleMethods(Class aClass, SEL orgMethod, SEL posedMethod,
+ bool classMethods) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ Method original = nil;
+ Method posed = nil;
+
+ if (classMethods) {
+ original = class_getClassMethod(aClass, orgMethod);
+ posed = class_getClassMethod(aClass, posedMethod);
+ } else {
+ original = class_getInstanceMethod(aClass, orgMethod);
+ posed = class_getInstanceMethod(aClass, posedMethod);
+ }
+
+ if (!original || !posed) return NS_ERROR_FAILURE;
+
+ method_exchangeImplementations(original, posed);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/widget/cocoa/nsTouchBar.h b/widget/cocoa/nsTouchBar.h
new file mode 100644
index 0000000000..4d0aa28d63
--- /dev/null
+++ b/widget/cocoa/nsTouchBar.h
@@ -0,0 +1,141 @@
+/* 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/. */
+
+#ifndef nsTouchBar_h_
+#define nsTouchBar_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsITouchBarHelper.h"
+#include "nsTouchBarInput.h"
+
+const NSTouchBarItemIdentifier kTouchBarBaseIdentifier =
+ @"com.mozilla.firefox.touchbar";
+
+/**
+ * Our TouchBar is its own delegate. This is adequate for our purposes,
+ * since the current implementation only defines Touch Bar buttons for the
+ * main Firefox window. If modals and other windows were to have custom
+ * Touch Bar views, each window would have to be a NSTouchBarDelegate so
+ * they could define their own custom sets of buttons.
+ */
+@interface nsTouchBar : NSTouchBar <NSTouchBarDelegate,
+ NSSharingServicePickerTouchBarItemDelegate,
+ NSSharingServiceDelegate> {
+ /**
+ * Link to the frontend API that determines which buttons appear
+ * in the Touch Bar
+ */
+ nsCOMPtr<nsITouchBarHelper> mTouchBarHelper;
+}
+
+/**
+ * Contains TouchBarInput representations of the inputs currently in
+ * the Touch Bar. Populated in `init` and updated by nsITouchBarUpdater.
+ */
+@property(strong) NSMutableDictionary<NSTouchBarItemIdentifier, TouchBarInput*>*
+ mappedLayoutItems;
+
+/**
+ * Stores buttons displayed in a NSScrollView. They must be stored separately
+ * because they are untethered from the nsTouchBar. As such, they
+ * cannot be retrieved with [NSTouchBar itemForIdentifier].
+ */
+@property(strong)
+ NSMutableDictionary<NSTouchBarItemIdentifier, NSCustomTouchBarItem*>*
+ scrollViewButtons;
+
+/**
+ * Returns an instance of nsTouchBar based on implementation details
+ * fetched from the frontend through nsTouchBarHelper.
+ */
+- (instancetype)init;
+
+/**
+ * If aInputs is not nil, a nsTouchBar containing the inputs specified is
+ * initialized. Otherwise, a nsTouchBar is initialized containing a default set
+ * of inputs.
+ */
+- (instancetype)initWithInputs:(NSMutableArray<TouchBarInput*>*)aInputs;
+
+- (void)dealloc;
+
+/**
+ * Creates a new NSTouchBarItem and adds it to the Touch Bar.
+ * Reads the passed identifier and creates the
+ * appropriate item type (eg. NSCustomTouchBarItem).
+ * Required as a member of NSTouchBarDelegate.
+ */
+- (NSTouchBarItem*)touchBar:(NSTouchBar*)aTouchBar
+ makeItemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+
+/**
+ * Updates an input on the Touch Bar by redirecting to one of the specific
+ * TouchBarItem types updaters.
+ * Returns true if the input was successfully updated.
+ */
+- (bool)updateItem:(TouchBarInput*)aInput;
+
+/**
+ * Helper function for updateItem. Checks to see if a given input exists within
+ * any of this Touch Bar's popovers and updates it if it exists.
+ */
+- (bool)maybeUpdatePopoverChild:(TouchBarInput*)aInput;
+
+/**
+ * Helper function for updateItem. Checks to see if a given input exists within
+ * any of this Touch Bar's scroll views and updates it if it exists.
+ */
+- (bool)maybeUpdateScrollViewChild:(TouchBarInput*)aInput;
+
+/**
+ * Helper function for updateItem. Replaces an item in the
+ * self.mappedLayoutItems dictionary.
+ */
+- (void)replaceMappedLayoutItem:(TouchBarInput*)aItem;
+
+/**
+ * Update or create various subclasses of TouchBarItem.
+ */
+- (void)updateButton:(NSCustomTouchBarItem*)aButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updateMainButton:(NSCustomTouchBarItem*)aMainButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updatePopover:(NSPopoverTouchBarItem*)aPopoverItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updateScrollView:(NSCustomTouchBarItem*)aScrollViewItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (void)updateLabel:(NSTextField*)aLabel
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
+- (NSTouchBarItem*)makeShareScrubberForIdentifier:
+ (NSTouchBarItemIdentifier)aIdentifier;
+
+/**
+ * If aShowing is true, aPopover is shown. Otherwise, it is hidden.
+ */
+- (void)showPopover:(TouchBarInput*)aPopover showing:(bool)aShowing;
+
+/**
+ * Redirects button actions to the appropriate handler.
+ */
+- (void)touchBarAction:(id)aSender;
+
+/**
+ * Helper function to initialize a new nsTouchBarInputIcon and load an icon.
+ */
+- (void)loadIconForInput:(TouchBarInput*)aInput forItem:(NSTouchBarItem*)aItem;
+
+- (NSArray*)itemsForSharingServicePickerTouchBarItem:
+ (NSSharingServicePickerTouchBarItem*)aPickerTouchBarItem;
+
+- (NSArray<NSSharingService*>*)
+ sharingServicePicker:(NSSharingServicePicker*)aSharingServicePicker
+ sharingServicesForItems:(NSArray*)aItems
+ proposedSharingServices:(NSArray<NSSharingService*>*)aProposedServices;
+
+- (void)releaseJSObjects;
+
+@end // nsTouchBar
+
+#endif // nsTouchBar_h_
diff --git a/widget/cocoa/nsTouchBar.mm b/widget/cocoa/nsTouchBar.mm
new file mode 100644
index 0000000000..067ce03129
--- /dev/null
+++ b/widget/cocoa/nsTouchBar.mm
@@ -0,0 +1,650 @@
+/* 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 "nsTouchBar.h"
+
+#include <objc/runtime.h>
+
+#include "mozilla/MacStringHelpers.h"
+#include "mozilla/dom/Document.h"
+#include "nsArrayUtils.h"
+#include "nsCocoaUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIArray.h"
+#include "nsTouchBarInputIcon.h"
+#include "nsWidgetsCID.h"
+
+@implementation nsTouchBar
+
+// Used to tie action strings to buttons.
+static char sIdentifierAssociationKey;
+
+// The default space between inputs, used where layout is not automatic.
+static const uint32_t kInputSpacing = 8;
+// The width of buttons in Apple's Share ScrollView. We use this in our
+// ScrollViews to give them a native appearance.
+static const uint32_t kScrollViewButtonWidth = 144;
+static const uint32_t kInputIconSize = 16;
+
+// The system default width for Touch Bar inputs is 128px. This is double.
+#define MAIN_BUTTON_WIDTH 256
+
+#pragma mark - NSTouchBarDelegate
+
+- (instancetype)init {
+ return [self initWithInputs:nil];
+}
+
+- (instancetype)initWithInputs:(NSMutableArray<TouchBarInput*>*)aInputs {
+ if ((self = [super init])) {
+ mTouchBarHelper = do_GetService(NS_TOUCHBARHELPER_CID);
+ if (!mTouchBarHelper) {
+ NS_ERROR("Unable to create Touch Bar Helper.");
+ return nil;
+ }
+
+ self.delegate = self;
+ self.mappedLayoutItems = [NSMutableDictionary dictionary];
+ self.customizationAllowedItemIdentifiers = @[];
+
+ if (!aInputs) {
+ // This customization identifier is how users' custom layouts are saved by
+ // macOS. If this changes, all users' layouts would be reset to the
+ // default layout.
+ self.customizationIdentifier = [kTouchBarBaseIdentifier
+ stringByAppendingPathExtension:@"defaultbar"];
+ nsCOMPtr<nsIArray> allItems;
+
+ nsresult rv = mTouchBarHelper->GetAllItems(getter_AddRefs(allItems));
+ if (NS_FAILED(rv) || !allItems) {
+ return nil;
+ }
+
+ uint32_t itemCount = 0;
+ allItems->GetLength(&itemCount);
+ // This is copied to self.customizationAllowedItemIdentifiers.
+ // Required since [self.mappedItems allKeys] does not preserve order.
+ // One slot is added for the spacer item.
+ NSMutableArray* orderedIdentifiers =
+ [NSMutableArray arrayWithCapacity:itemCount + 1];
+ for (uint32_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITouchBarInput> input = do_QueryElementAt(allItems, i);
+ if (!input) {
+ continue;
+ }
+
+ TouchBarInput* convertedInput;
+ NSTouchBarItemIdentifier newInputIdentifier =
+ [TouchBarInput nativeIdentifierWithXPCOM:input];
+ if (!newInputIdentifier) {
+ continue;
+ }
+
+ // If there is already an input in mappedLayoutItems with this
+ // identifier, that means updateItem fired before this initialization.
+ // The input cached by updateItem is more current, so we should use that
+ // one.
+ if (self.mappedLayoutItems[newInputIdentifier]) {
+ convertedInput = self.mappedLayoutItems[newInputIdentifier];
+ } else {
+ convertedInput = [[TouchBarInput alloc] initWithXPCOM:input];
+ // Add new input to dictionary for lookup of properties in delegate.
+ self.mappedLayoutItems[[convertedInput nativeIdentifier]] =
+ convertedInput;
+ }
+
+ orderedIdentifiers[i] = [convertedInput nativeIdentifier];
+ }
+ [orderedIdentifiers addObject:@"NSTouchBarItemIdentifierFlexibleSpace"];
+ self.customizationAllowedItemIdentifiers = [orderedIdentifiers copy];
+
+ NSArray* defaultItemIdentifiers = @[
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"back"],
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"forward"],
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"reload"],
+ [TouchBarInput nativeIdentifierWithType:@"mainButton"
+ withKey:@"open-location"],
+ [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"new-tab"],
+ [TouchBarInput shareScrubberIdentifier],
+ [TouchBarInput searchPopoverIdentifier]
+ ];
+ self.defaultItemIdentifiers = [defaultItemIdentifiers copy];
+ } else {
+ NSMutableArray* defaultItemIdentifiers =
+ [NSMutableArray arrayWithCapacity:[aInputs count]];
+ for (TouchBarInput* input in aInputs) {
+ self.mappedLayoutItems[[input nativeIdentifier]] = input;
+ [defaultItemIdentifiers addObject:[input nativeIdentifier]];
+ }
+ self.defaultItemIdentifiers = [defaultItemIdentifiers copy];
+ }
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ NSTouchBarItem* item = [self itemForIdentifier:identifier];
+ if (!item) {
+ continue;
+ }
+ if ([item isKindOfClass:[NSPopoverTouchBarItem class]]) {
+ [(NSPopoverTouchBarItem*)item setCollapsedRepresentationImage:nil];
+ [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar] release];
+ } else if ([[item view] isKindOfClass:[NSScrollView class]]) {
+ [[(NSScrollView*)[item view] documentView] release];
+ [(NSScrollView*)[item view] release];
+ }
+
+ [item release];
+ }
+
+ [self.defaultItemIdentifiers release];
+ [self.customizationAllowedItemIdentifiers release];
+ [self.scrollViewButtons removeAllObjects];
+ [self.scrollViewButtons release];
+ [self.mappedLayoutItems removeAllObjects];
+ [self.mappedLayoutItems release];
+ [super dealloc];
+}
+
+- (NSTouchBarItem*)touchBar:(NSTouchBar*)aTouchBar
+ makeItemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!mTouchBarHelper) {
+ return nil;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return nil;
+ }
+
+ if ([input baseType] == TouchBarInputBaseType::kScrubber) {
+ // We check the identifier rather than the baseType here as a special case.
+ if (![aIdentifier
+ isEqualToString:[TouchBarInput shareScrubberIdentifier]]) {
+ // We're only supporting the Share scrubber for now.
+ return nil;
+ }
+ return [self makeShareScrubberForIdentifier:aIdentifier];
+ }
+
+ if ([input baseType] == TouchBarInputBaseType::kPopover) {
+ NSPopoverTouchBarItem* newPopoverItem =
+ [[NSPopoverTouchBarItem alloc] initWithIdentifier:aIdentifier];
+ [newPopoverItem setCustomizationLabel:[input title]];
+ // We initialize popoverTouchBar here because we only allow setting this
+ // property on popover creation. Updating popoverTouchBar for every update
+ // of the popover item would be very expensive.
+ newPopoverItem.popoverTouchBar =
+ [[nsTouchBar alloc] initWithInputs:[input children]];
+ [self updatePopover:newPopoverItem withIdentifier:[input nativeIdentifier]];
+ return newPopoverItem;
+ }
+
+ // Our new item, which will be initialized depending on aIdentifier.
+ NSCustomTouchBarItem* newItem =
+ [[NSCustomTouchBarItem alloc] initWithIdentifier:aIdentifier];
+ [newItem setCustomizationLabel:[input title]];
+
+ if ([input baseType] == TouchBarInputBaseType::kScrollView) {
+ [self updateScrollView:newItem withIdentifier:[input nativeIdentifier]];
+ return newItem;
+ } else if ([input baseType] == TouchBarInputBaseType::kLabel) {
+ NSTextField* label = [NSTextField labelWithString:@""];
+ [self updateLabel:label withIdentifier:[input nativeIdentifier]];
+ newItem.view = label;
+ return newItem;
+ }
+
+ // The cases of a button or main button require the same setup.
+ NSButton* button = [NSButton buttonWithTitle:@""
+ target:self
+ action:@selector(touchBarAction:)];
+ newItem.view = button;
+
+ if ([input baseType] == TouchBarInputBaseType::kButton &&
+ ![[input type] hasPrefix:@"scrollView"]) {
+ [self updateButton:newItem withIdentifier:[input nativeIdentifier]];
+ } else if ([input baseType] == TouchBarInputBaseType::kMainButton) {
+ [self updateMainButton:newItem withIdentifier:[input nativeIdentifier]];
+ }
+ return newItem;
+}
+
+- (bool)updateItem:(TouchBarInput*)aInput {
+ if (!mTouchBarHelper) {
+ return false;
+ }
+
+ NSTouchBarItem* item = [self itemForIdentifier:[aInput nativeIdentifier]];
+
+ // If we can't immediately find item, there are three possibilities:
+ // * It is a button in a ScrollView, or
+ // * It is contained within a popover, or
+ // * It simply does not exist.
+ // We check for each possibility here.
+ if (!self.mappedLayoutItems[[aInput nativeIdentifier]]) {
+ if ([self maybeUpdateScrollViewChild:aInput]) {
+ return true;
+ }
+ if ([self maybeUpdatePopoverChild:aInput]) {
+ return true;
+ }
+ return false;
+ }
+
+ // Update our canonical copy of the input.
+ [self replaceMappedLayoutItem:aInput];
+
+ if ([aInput baseType] == TouchBarInputBaseType::kButton) {
+ [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updateButton:(NSCustomTouchBarItem*)item
+ withIdentifier:[aInput nativeIdentifier]];
+ } else if ([aInput baseType] == TouchBarInputBaseType::kMainButton) {
+ [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updateMainButton:(NSCustomTouchBarItem*)item
+ withIdentifier:[aInput nativeIdentifier]];
+ } else if ([aInput baseType] == TouchBarInputBaseType::kScrollView) {
+ [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updateScrollView:(NSCustomTouchBarItem*)item
+ withIdentifier:[aInput nativeIdentifier]];
+ } else if ([aInput baseType] == TouchBarInputBaseType::kPopover) {
+ [(NSPopoverTouchBarItem*)item setCustomizationLabel:[aInput title]];
+ [self updatePopover:(NSPopoverTouchBarItem*)item
+ withIdentifier:[aInput nativeIdentifier]];
+ for (TouchBarInput* child in [aInput children]) {
+ [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar]
+ updateItem:child];
+ }
+ } else if ([aInput baseType] == TouchBarInputBaseType::kLabel) {
+ [self updateLabel:(NSTextField*)item.view
+ withIdentifier:[aInput nativeIdentifier]];
+ }
+
+ return true;
+}
+
+- (bool)maybeUpdatePopoverChild:(TouchBarInput*)aInput {
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ TouchBarInput* potentialPopover = self.mappedLayoutItems[identifier];
+ if ([potentialPopover baseType] != TouchBarInputBaseType::kPopover) {
+ continue;
+ }
+ NSTouchBarItem* popover =
+ [self itemForIdentifier:[potentialPopover nativeIdentifier]];
+ if (popover) {
+ if ([(nsTouchBar*)[(NSPopoverTouchBarItem*)popover popoverTouchBar]
+ updateItem:aInput]) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+- (bool)maybeUpdateScrollViewChild:(TouchBarInput*)aInput {
+ NSCustomTouchBarItem* scrollViewButton =
+ self.scrollViewButtons[[aInput nativeIdentifier]];
+ if (scrollViewButton) {
+ // ScrollView buttons are similar to mainButtons except for their width.
+ [self updateMainButton:scrollViewButton
+ withIdentifier:[aInput nativeIdentifier]];
+ NSButton* button = (NSButton*)scrollViewButton.view;
+ uint32_t buttonSize =
+ MAX(button.attributedTitle.size.width + kInputIconSize + kInputSpacing,
+ kScrollViewButtonWidth);
+ [[button widthAnchor] constraintGreaterThanOrEqualToConstant:buttonSize]
+ .active = YES;
+ }
+ // Updating the TouchBarInput* in the ScrollView's mChildren array.
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ TouchBarInput* potentialScrollView = self.mappedLayoutItems[identifier];
+ if ([potentialScrollView baseType] != TouchBarInputBaseType::kScrollView) {
+ continue;
+ }
+ for (uint32_t i = 0; i < [[potentialScrollView children] count]; ++i) {
+ TouchBarInput* child = [potentialScrollView children][i];
+ if (![[child nativeIdentifier]
+ isEqualToString:[aInput nativeIdentifier]]) {
+ continue;
+ }
+ [[potentialScrollView children] replaceObjectAtIndex:i withObject:aInput];
+ [child release];
+ return true;
+ }
+ }
+ return false;
+}
+
+- (void)replaceMappedLayoutItem:(TouchBarInput*)aItem {
+ [self.mappedLayoutItems[[aItem nativeIdentifier]] release];
+ self.mappedLayoutItems[[aItem nativeIdentifier]] = aItem;
+}
+
+- (void)updateButton:(NSCustomTouchBarItem*)aButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aButton || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return;
+ }
+
+ NSButton* button = (NSButton*)[aButton view];
+ button.title = [input title];
+ if ([input imageURI]) {
+ [button setImagePosition:NSImageOnly];
+ [self loadIconForInput:input forItem:aButton];
+ // Because we are hiding the title, NSAccessibility also does not get it.
+ // Therefore, set an accessibility label as alternative text for image-only
+ // buttons.
+ [button setAccessibilityLabel:[input title]];
+ }
+
+ [button setEnabled:![input isDisabled]];
+ if ([input color]) {
+ button.bezelColor = [input color];
+ }
+
+ objc_setAssociatedObject(button, &sIdentifierAssociationKey, aIdentifier,
+ OBJC_ASSOCIATION_RETAIN);
+}
+
+- (void)updateMainButton:(NSCustomTouchBarItem*)aMainButton
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aMainButton || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return;
+ }
+
+ [self updateButton:aMainButton withIdentifier:aIdentifier];
+ NSButton* button = (NSButton*)[aMainButton view];
+
+ // If empty, string is still being localized. Display a blank input instead.
+ if ([[input title] isEqualToString:@""]) {
+ [button setImagePosition:NSNoImage];
+ } else {
+ [button setImagePosition:NSImageLeft];
+ }
+ button.imageHugsTitle = YES;
+ [button.widthAnchor constraintGreaterThanOrEqualToConstant:MAIN_BUTTON_WIDTH]
+ .active = YES;
+ [button setContentHuggingPriority:1.0
+ forOrientation:NSLayoutConstraintOrientationHorizontal];
+}
+
+- (void)updatePopover:(NSPopoverTouchBarItem*)aPopoverItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aPopoverItem || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input) {
+ return;
+ }
+
+ aPopoverItem.showsCloseButton = YES;
+ if ([input imageURI]) {
+ [self loadIconForInput:input forItem:aPopoverItem];
+ } else if ([input title]) {
+ aPopoverItem.collapsedRepresentationLabel = [input title];
+ }
+
+ // Special handling to show/hide the search popover if the Urlbar is focused.
+ if ([[input nativeIdentifier]
+ isEqualToString:[TouchBarInput searchPopoverIdentifier]]) {
+ // We can reach this code during window shutdown. We only want to toggle
+ // showPopover if we are in a normal running state.
+ if (!mTouchBarHelper) {
+ return;
+ }
+ bool urlbarIsFocused = false;
+ mTouchBarHelper->GetIsUrlbarFocused(&urlbarIsFocused);
+ if (urlbarIsFocused) {
+ [aPopoverItem showPopover:self];
+ }
+ }
+}
+
+- (void)updateScrollView:(NSCustomTouchBarItem*)aScrollViewItem
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aScrollViewItem || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input || ![input children]) {
+ return;
+ }
+
+ NSMutableDictionary* constraintViews = [NSMutableDictionary dictionary];
+ NSView* documentView = [[NSView alloc] initWithFrame:NSZeroRect];
+ NSString* layoutFormat = @"H:|-8-";
+ NSSize size = NSMakeSize(kInputSpacing, 30);
+ // Layout strings allow only alphanumeric characters. We will use this
+ // NSCharacterSet to strip illegal characters.
+ NSCharacterSet* charactersToRemove =
+ [[NSCharacterSet alphanumericCharacterSet] invertedSet];
+
+ for (TouchBarInput* childInput in [input children]) {
+ if ([childInput baseType] != TouchBarInputBaseType::kButton) {
+ continue;
+ }
+ [self replaceMappedLayoutItem:childInput];
+ NSCustomTouchBarItem* newItem = [[NSCustomTouchBarItem alloc]
+ initWithIdentifier:[childInput nativeIdentifier]];
+ NSButton* button = [NSButton buttonWithTitle:[childInput title]
+ target:self
+ action:@selector(touchBarAction:)];
+ newItem.view = button;
+ // ScrollView buttons are similar to mainButtons except for their width.
+ [self updateMainButton:newItem
+ withIdentifier:[childInput nativeIdentifier]];
+ uint32_t buttonSize =
+ MAX(button.attributedTitle.size.width + kInputIconSize + kInputSpacing,
+ kScrollViewButtonWidth);
+ [[button widthAnchor] constraintGreaterThanOrEqualToConstant:buttonSize]
+ .active = YES;
+
+ NSCustomTouchBarItem* tempItem =
+ self.scrollViewButtons[[childInput nativeIdentifier]];
+ self.scrollViewButtons[[childInput nativeIdentifier]] = newItem;
+ [tempItem release];
+
+ button.translatesAutoresizingMaskIntoConstraints = NO;
+ [documentView addSubview:button];
+ NSString* layoutKey = [[[childInput nativeIdentifier]
+ componentsSeparatedByCharactersInSet:charactersToRemove]
+ componentsJoinedByString:@""];
+
+ // Iteratively create our layout string.
+ layoutFormat = [layoutFormat
+ stringByAppendingString:[NSString
+ stringWithFormat:@"[%@]-8-", layoutKey]];
+ [constraintViews setObject:button forKey:layoutKey];
+ size.width += kInputSpacing + buttonSize;
+ }
+ layoutFormat =
+ [layoutFormat stringByAppendingString:[NSString stringWithFormat:@"|"]];
+ NSArray* hConstraints = [NSLayoutConstraint
+ constraintsWithVisualFormat:layoutFormat
+ options:NSLayoutFormatAlignAllCenterY
+ metrics:nil
+ views:constraintViews];
+ NSScrollView* scrollView = [[NSScrollView alloc]
+ initWithFrame:CGRectMake(0, 0, size.width, size.height)];
+ [documentView setFrame:NSMakeRect(0, 0, size.width, size.height)];
+ [NSLayoutConstraint activateConstraints:hConstraints];
+ scrollView.documentView = documentView;
+
+ aScrollViewItem.view = scrollView;
+}
+
+- (void)updateLabel:(NSTextField*)aLabel
+ withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
+ if (!aLabel || !aIdentifier) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ if (!input || ![input title]) {
+ return;
+ }
+ [aLabel setStringValue:[input title]];
+}
+
+- (NSTouchBarItem*)makeShareScrubberForIdentifier:
+ (NSTouchBarItemIdentifier)aIdentifier {
+ TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
+ // System-default share menu
+ NSSharingServicePickerTouchBarItem* servicesItem =
+ [[NSSharingServicePickerTouchBarItem alloc]
+ initWithIdentifier:aIdentifier];
+
+ // buttonImage needs to be set to nil while we wait for our icon to load.
+ // Otherwise, the default Apple share icon is automatically loaded.
+ servicesItem.buttonImage = nil;
+
+ [self loadIconForInput:input forItem:servicesItem];
+
+ servicesItem.delegate = self;
+ return servicesItem;
+}
+
+- (void)showPopover:(TouchBarInput*)aPopover showing:(bool)aShowing {
+ if (!aPopover) {
+ return;
+ }
+ NSPopoverTouchBarItem* popoverItem = (NSPopoverTouchBarItem*)[self
+ itemForIdentifier:[aPopover nativeIdentifier]];
+ if (!popoverItem) {
+ return;
+ }
+ if (aShowing) {
+ [popoverItem showPopover:self];
+ } else {
+ [popoverItem dismissPopover:self];
+ }
+}
+
+- (void)touchBarAction:(id)aSender {
+ NSTouchBarItemIdentifier identifier =
+ objc_getAssociatedObject(aSender, &sIdentifierAssociationKey);
+ if (!identifier || [identifier isEqualToString:@""]) {
+ return;
+ }
+
+ TouchBarInput* input = self.mappedLayoutItems[identifier];
+ if (!input) {
+ return;
+ }
+
+ nsCOMPtr<nsITouchBarInputCallback> callback = [input callback];
+ if (!callback) {
+ NSLog(@"Touch Bar action attempted with no valid callback! Identifier: %@",
+ [input nativeIdentifier]);
+ return;
+ }
+ callback->OnCommand();
+}
+
+- (void)loadIconForInput:(TouchBarInput*)aInput forItem:(NSTouchBarItem*)aItem {
+ if (!aInput || ![aInput imageURI] || !aItem || !mTouchBarHelper) {
+ return;
+ }
+
+ RefPtr<nsTouchBarInputIcon> icon = [aInput icon];
+
+ if (!icon) {
+ RefPtr<Document> document;
+ nsresult rv = mTouchBarHelper->GetDocument(getter_AddRefs(document));
+ if (NS_FAILED(rv) || !document) {
+ return;
+ }
+ icon = new nsTouchBarInputIcon(document, aInput, aItem);
+ [aInput setIcon:icon];
+ }
+ icon->SetupIcon([aInput imageURI]);
+}
+
+- (void)releaseJSObjects {
+ mTouchBarHelper = nil;
+
+ for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
+ TouchBarInput* input = self.mappedLayoutItems[identifier];
+ if (!input) {
+ continue;
+ }
+
+ // Childless popovers contain the default Touch Bar as its popoverTouchBar.
+ // We check for [input children] since the default Touch Bar contains a
+ // popover (search-popover), so this would infinitely loop if there was no
+ // check.
+ if ([input baseType] == TouchBarInputBaseType::kPopover &&
+ [input children]) {
+ NSTouchBarItem* item = [self itemForIdentifier:identifier];
+ [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar]
+ releaseJSObjects];
+ }
+
+ [input releaseJSObjects];
+ }
+}
+
+#pragma mark - NSSharingServicePickerTouchBarItemDelegate
+
+- (NSArray*)itemsForSharingServicePickerTouchBarItem:
+ (NSSharingServicePickerTouchBarItem*)aPickerTouchBarItem {
+ NSURL* urlToShare = nil;
+ NSString* titleToShare = @"";
+ nsAutoString url;
+ nsAutoString title;
+ if (mTouchBarHelper) {
+ nsresult rv = mTouchBarHelper->GetActiveUrl(url);
+ if (!NS_FAILED(rv)) {
+ urlToShare = [NSURL URLWithString:nsCocoaUtils::ToNSString(url)];
+ // NSURL URLWithString returns nil if the URL is invalid. At this point,
+ // it is too late to simply shut down the share menu, so we default to
+ // about:blank if the share button is clicked when the URL is invalid.
+ if (urlToShare == nil) {
+ urlToShare = [NSURL URLWithString:@"about:blank"];
+ }
+ }
+
+ rv = mTouchBarHelper->GetActiveTitle(title);
+ if (!NS_FAILED(rv)) {
+ titleToShare = nsCocoaUtils::ToNSString(title);
+ }
+ }
+
+ return @[ urlToShare, titleToShare ];
+}
+
+- (NSArray<NSSharingService*>*)
+ sharingServicePicker:(NSSharingServicePicker*)aSharingServicePicker
+ sharingServicesForItems:(NSArray*)aItems
+ proposedSharingServices:(NSArray<NSSharingService*>*)aProposedServices {
+ // redundant services
+ NSArray* excludedServices = @[
+ @"com.apple.share.System.add-to-safari-reading-list",
+ ];
+
+ NSArray* sharingServices = [aProposedServices
+ filteredArrayUsingPredicate:[NSPredicate
+ predicateWithFormat:@"NOT (name IN %@)",
+ excludedServices]];
+
+ return sharingServices;
+}
+
+@end
diff --git a/widget/cocoa/nsTouchBarInput.h b/widget/cocoa/nsTouchBarInput.h
new file mode 100644
index 0000000000..41ed7979a4
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInput.h
@@ -0,0 +1,91 @@
+/* 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/. */
+
+#ifndef nsTouchBarInput_h_
+#define nsTouchBarInput_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsITouchBarInput.h"
+#include "nsCOMPtr.h"
+
+using namespace mozilla::dom;
+
+enum class TouchBarInputBaseType : uint8_t {
+ kButton,
+ kLabel,
+ kMainButton,
+ kPopover,
+ kScrollView,
+ kScrubber
+};
+
+class nsTouchBarInputIcon;
+
+/**
+ * NSObject representation of nsITouchBarInput.
+ */
+@interface TouchBarInput : NSObject {
+ nsCOMPtr<nsIURI> mImageURI;
+ RefPtr<nsTouchBarInputIcon> mIcon;
+ TouchBarInputBaseType mBaseType;
+ NSString* mType;
+ nsCOMPtr<nsITouchBarInputCallback> mCallback;
+ NSMutableArray<TouchBarInput*>* mChildren;
+}
+
+@property(strong) NSString* key;
+@property(strong) NSString* type;
+@property(strong) NSString* title;
+@property(strong) NSColor* color;
+@property(nonatomic, getter=isDisabled) BOOL disabled;
+
+- (nsCOMPtr<nsIURI>)imageURI;
+- (RefPtr<nsTouchBarInputIcon>)icon;
+- (TouchBarInputBaseType)baseType;
+- (NSTouchBarItemIdentifier)nativeIdentifier;
+- (nsCOMPtr<nsITouchBarInputCallback>)callback;
+- (NSMutableArray<TouchBarInput*>*)children;
+- (void)setImageURI:(nsCOMPtr<nsIURI>)aImageURI;
+- (void)setIcon:(RefPtr<nsTouchBarInputIcon>)aIcon;
+- (void)setCallback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback;
+- (void)setChildren:(NSMutableArray<TouchBarInput*>*)aChildren;
+
+- (id)initWithKey:(NSString*)aKey
+ title:(NSString*)aTitle
+ imageURI:(nsCOMPtr<nsIURI>)aImageURI
+ type:(NSString*)aType
+ callback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback
+ color:(uint32_t)aColor
+ disabled:(BOOL)aDisabled
+ children:(nsCOMPtr<nsIArray>)aChildren;
+
+- (TouchBarInput*)initWithXPCOM:(nsCOMPtr<nsITouchBarInput>)aInput;
+
+- (void)releaseJSObjects;
+
+- (void)dealloc;
+
+/**
+ * We make these helper methods static so that other classes can query a
+ * TouchBarInput's nativeIdentifier (e.g. nsTouchBarUpdater looking up a
+ * popover in mappedLayoutItems).
+ */
++ (NSTouchBarItemIdentifier)nativeIdentifierWithType:(NSString*)aType
+ withKey:(NSString*)aKey;
++ (NSTouchBarItemIdentifier)nativeIdentifierWithXPCOM:
+ (nsCOMPtr<nsITouchBarInput>)aInput;
+
+// Non-JS scrubber implemention for the Share Scrubber,
+// since it is defined by an Apple API.
++ (NSTouchBarItemIdentifier)shareScrubberIdentifier;
+
+// The search popover needs to show/hide depending on if the Urlbar is focused
+// when it is created. We keep track of its identifier to accommodate this
+// special handling.
++ (NSTouchBarItemIdentifier)searchPopoverIdentifier;
+
+@end
+
+#endif // nsTouchBarInput_h_
diff --git a/widget/cocoa/nsTouchBarInput.mm b/widget/cocoa/nsTouchBarInput.mm
new file mode 100644
index 0000000000..af638f52d3
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInput.mm
@@ -0,0 +1,251 @@
+/* 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 "nsTouchBarInput.h"
+
+#include "mozilla/MacStringHelpers.h"
+#include "nsArrayUtils.h"
+#include "nsCocoaUtils.h"
+#include "nsTouchBar.h"
+#include "nsTouchBarInputIcon.h"
+
+@implementation TouchBarInput
+
+- (nsCOMPtr<nsIURI>)imageURI {
+ return mImageURI;
+}
+
+- (void)setImageURI:(nsCOMPtr<nsIURI>)aImageURI {
+ mImageURI = aImageURI;
+}
+
+- (RefPtr<nsTouchBarInputIcon>)icon {
+ return mIcon;
+}
+
+- (void)setIcon:(RefPtr<nsTouchBarInputIcon>)aIcon {
+ mIcon = aIcon;
+}
+
+- (TouchBarInputBaseType)baseType {
+ return mBaseType;
+}
+
+- (NSString*)type {
+ return mType;
+}
+
+- (void)setType:(NSString*)aType {
+ [aType retain];
+ [mType release];
+ if ([aType hasSuffix:@"button"]) {
+ mBaseType = TouchBarInputBaseType::kButton;
+ } else if ([aType hasSuffix:@"label"]) {
+ mBaseType = TouchBarInputBaseType::kLabel;
+ } else if ([aType hasSuffix:@"mainButton"]) {
+ mBaseType = TouchBarInputBaseType::kMainButton;
+ } else if ([aType hasSuffix:@"popover"]) {
+ mBaseType = TouchBarInputBaseType::kPopover;
+ } else if ([aType hasSuffix:@"scrollView"]) {
+ mBaseType = TouchBarInputBaseType::kScrollView;
+ } else if ([aType hasSuffix:@"scrubber"]) {
+ mBaseType = TouchBarInputBaseType::kScrubber;
+ }
+ mType = aType;
+}
+
+- (NSTouchBarItemIdentifier)nativeIdentifier {
+ return [TouchBarInput nativeIdentifierWithType:mType withKey:self.key];
+}
+
+- (nsCOMPtr<nsITouchBarInputCallback>)callback {
+ return mCallback;
+}
+
+- (void)setCallback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback {
+ mCallback = aCallback;
+}
+
+- (NSMutableArray<TouchBarInput*>*)children {
+ return mChildren;
+}
+
+- (void)setChildren:(NSMutableArray<TouchBarInput*>*)aChildren {
+ [aChildren retain];
+ for (TouchBarInput* child in mChildren) {
+ [child releaseJSObjects];
+ }
+ [mChildren removeAllObjects];
+ [mChildren release];
+ mChildren = aChildren;
+}
+
+- (id)initWithKey:(NSString*)aKey
+ title:(NSString*)aTitle
+ imageURI:(nsCOMPtr<nsIURI>)aImageURI
+ type:(NSString*)aType
+ callback:(nsCOMPtr<nsITouchBarInputCallback>)aCallback
+ color:(uint32_t)aColor
+ disabled:(BOOL)aDisabled
+ children:(nsCOMPtr<nsIArray>)aChildren {
+ if (self = [super init]) {
+ mType = nil;
+
+ self.key = aKey;
+ self.title = aTitle;
+ self.type = aType;
+ self.disabled = aDisabled;
+ [self setImageURI:aImageURI];
+ [self setCallback:aCallback];
+ if (aColor) {
+ [self setColor:[NSColor
+ colorWithDisplayP3Red:((aColor >> 16) & 0xFF) / 255.0
+ green:((aColor >> 8) & 0xFF) / 255.0
+ blue:((aColor) & 0xFF) / 255.0
+ alpha:1.0]];
+ }
+ if (aChildren) {
+ uint32_t itemCount = 0;
+ aChildren->GetLength(&itemCount);
+ NSMutableArray* orderedChildren =
+ [NSMutableArray arrayWithCapacity:itemCount];
+ for (uint32_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITouchBarInput> child = do_QueryElementAt(aChildren, i);
+ if (!child) {
+ continue;
+ }
+ TouchBarInput* convertedChild =
+ [[TouchBarInput alloc] initWithXPCOM:child];
+ if (convertedChild) {
+ orderedChildren[i] = convertedChild;
+ }
+ }
+ [self setChildren:orderedChildren];
+ }
+ }
+
+ return self;
+}
+
+- (TouchBarInput*)initWithXPCOM:(nsCOMPtr<nsITouchBarInput>)aInput {
+ nsAutoString keyStr;
+ nsresult rv = aInput->GetKey(keyStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsAutoString titleStr;
+ rv = aInput->GetTitle(titleStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsCOMPtr<nsIURI> imageURI;
+ rv = aInput->GetImage(getter_AddRefs(imageURI));
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsAutoString typeStr;
+ rv = aInput->GetType(typeStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsCOMPtr<nsITouchBarInputCallback> callback;
+ rv = aInput->GetCallback(getter_AddRefs(callback));
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ uint32_t colorInt;
+ rv = aInput->GetColor(&colorInt);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ bool disabled = false;
+ rv = aInput->GetDisabled(&disabled);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ nsCOMPtr<nsIArray> children;
+ rv = aInput->GetChildren(getter_AddRefs(children));
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+
+ return [self initWithKey:nsCocoaUtils::ToNSString(keyStr)
+ title:nsCocoaUtils::ToNSString(titleStr)
+ imageURI:imageURI
+ type:nsCocoaUtils::ToNSString(typeStr)
+ callback:callback
+ color:colorInt
+ disabled:(BOOL)disabled
+ children:children];
+}
+
+- (void)releaseJSObjects {
+ if (mIcon) {
+ mIcon->Destroy();
+ mIcon = nil;
+ }
+ [self setCallback:nil];
+ [self setImageURI:nil];
+ for (TouchBarInput* child in mChildren) {
+ [child releaseJSObjects];
+ }
+}
+
+- (void)dealloc {
+ if (mIcon) {
+ mIcon->Destroy();
+ mIcon = nil;
+ }
+ [mType release];
+ [mChildren removeAllObjects];
+ [mChildren release];
+ [super dealloc];
+}
+
++ (NSTouchBarItemIdentifier)nativeIdentifierWithType:(NSString*)aType
+ withKey:(NSString*)aKey {
+ NSTouchBarItemIdentifier identifier;
+ identifier = [kTouchBarBaseIdentifier stringByAppendingPathExtension:aType];
+ if (aKey) {
+ identifier = [identifier stringByAppendingPathExtension:aKey];
+ }
+ return identifier;
+}
+
++ (NSTouchBarItemIdentifier)nativeIdentifierWithXPCOM:
+ (nsCOMPtr<nsITouchBarInput>)aInput {
+ nsAutoString keyStr;
+ nsresult rv = aInput->GetKey(keyStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+ NSString* key = nsCocoaUtils::ToNSString(keyStr);
+
+ nsAutoString typeStr;
+ rv = aInput->GetType(typeStr);
+ if (NS_FAILED(rv)) {
+ return nil;
+ }
+ NSString* type = nsCocoaUtils::ToNSString(typeStr);
+
+ return [TouchBarInput nativeIdentifierWithType:type withKey:key];
+}
+
++ (NSTouchBarItemIdentifier)shareScrubberIdentifier {
+ return [TouchBarInput nativeIdentifierWithType:@"scrubber" withKey:@"share"];
+}
+
++ (NSTouchBarItemIdentifier)searchPopoverIdentifier {
+ return [TouchBarInput nativeIdentifierWithType:@"popover"
+ withKey:@"search-popover"];
+}
+
+@end
diff --git a/widget/cocoa/nsTouchBarInputIcon.h b/widget/cocoa/nsTouchBarInputIcon.h
new file mode 100644
index 0000000000..d018afb756
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInputIcon.h
@@ -0,0 +1,70 @@
+/* -*- 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/. */
+
+/*
+ * Retrieves and displays icons on the macOS Touch Bar.
+ */
+
+#ifndef nsTouchBarInputIcon_h_
+#define nsTouchBarInputIcon_h_
+
+#import <Cocoa/Cocoa.h>
+
+#include "mozilla/widget/IconLoader.h"
+#include "nsTouchBarInput.h"
+
+using namespace mozilla::dom;
+
+class nsIURI;
+class nsIPrincipal;
+class imgRequestProxy;
+
+namespace mozilla::dom {
+class Document;
+}
+
+class nsTouchBarInputIcon : public mozilla::widget::IconLoader::Listener {
+ public:
+ explicit nsTouchBarInputIcon(RefPtr<Document> aDocument,
+ TouchBarInput* aInput, NSTouchBarItem* aItem);
+
+ NS_INLINE_DECL_REFCOUNTING(nsTouchBarInputIcon)
+
+ private:
+ virtual ~nsTouchBarInputIcon();
+
+ public:
+ // SetupIcon succeeds if it was able to set up the icon, or if there should
+ // be no icon, in which case it clears any existing icon but still succeeds.
+ nsresult SetupIcon(nsCOMPtr<nsIURI> aIconURI);
+
+ // Implements this method for mozilla::widget::IconLoader::Listener.
+ // Called once the icon load is complete.
+ nsresult OnComplete(imgIContainer* aImage) override;
+
+ // Unless we take precautions, we may outlive the object that created us
+ // (mTouchBar, which owns our native menu item (mTouchBarInput)).
+ // Destroy() should be called from mTouchBar's destructor to prevent
+ // this from happening.
+ void Destroy();
+
+ void ReleaseJSObjects();
+
+ protected:
+ RefPtr<Document> mDocument;
+ bool mSetIcon;
+ NSButton* mButton;
+ // We accept a mShareScrubber only as a special case since
+ // NSSharingServicePickerTouchBarItem does not expose an NSButton* on which we
+ // can set the `image` property.
+ NSSharingServicePickerTouchBarItem* mShareScrubber;
+ // We accept a popover only as a special case.
+ NSPopoverTouchBarItem* mPopoverItem;
+ // The icon loader object should never outlive its creating
+ // nsTouchBarInputIcon object.
+ RefPtr<mozilla::widget::IconLoader> mIconLoader;
+};
+
+#endif // nsTouchBarInputIcon_h_
diff --git a/widget/cocoa/nsTouchBarInputIcon.mm b/widget/cocoa/nsTouchBarInputIcon.mm
new file mode 100644
index 0000000000..69e0c2aa0f
--- /dev/null
+++ b/widget/cocoa/nsTouchBarInputIcon.mm
@@ -0,0 +1,141 @@
+/* -*- 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/. */
+
+/*
+ * Retrieves and displays icons on the macOS Touch Bar.
+ */
+
+#include "nsTouchBarInputIcon.h"
+
+#include "MOZIconHelper.h"
+#include "mozilla/dom/Document.h"
+#include "nsCocoaUtils.h"
+#include "nsComputedDOMStyle.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsINode.h"
+#include "nsNameSpaceManager.h"
+#include "nsObjCExceptions.h"
+
+using namespace mozilla;
+using mozilla::widget::IconLoader;
+
+static const uint32_t kIconHeight = 16;
+static const CGFloat kHiDPIScalingFactor = 2.0f;
+
+nsTouchBarInputIcon::nsTouchBarInputIcon(RefPtr<Document> aDocument,
+ TouchBarInput* aInput,
+ NSTouchBarItem* aItem)
+ : mDocument(aDocument),
+ mSetIcon(false),
+ mButton(nil),
+ mShareScrubber(nil),
+ mPopoverItem(nil) {
+ if ([[aInput nativeIdentifier]
+ isEqualToString:[TouchBarInput shareScrubberIdentifier]]) {
+ mShareScrubber = (NSSharingServicePickerTouchBarItem*)aItem;
+ } else if ([aInput baseType] == TouchBarInputBaseType::kPopover) {
+ mPopoverItem = (NSPopoverTouchBarItem*)aItem;
+ } else if ([aInput baseType] == TouchBarInputBaseType::kButton ||
+ [aInput baseType] == TouchBarInputBaseType::kMainButton) {
+ mButton = (NSButton*)[aItem view];
+ } else {
+ NS_ERROR("Incompatible Touch Bar input passed to nsTouchBarInputIcon.");
+ }
+ aInput = nil;
+ MOZ_COUNT_CTOR(nsTouchBarInputIcon);
+}
+
+nsTouchBarInputIcon::~nsTouchBarInputIcon() {
+ Destroy();
+ MOZ_COUNT_DTOR(nsTouchBarInputIcon);
+}
+
+// Called from nsTouchBar's destructor, to prevent us from outliving it
+// (as might otherwise happen if calls to our imgINotificationObserver methods
+// are still outstanding). nsTouchBar owns our mTouchBarInput.
+void nsTouchBarInputIcon::Destroy() {
+ ReleaseJSObjects();
+ if (mIconLoader) {
+ mIconLoader->Destroy();
+ mIconLoader = nullptr;
+ }
+
+ mButton = nil;
+ mShareScrubber = nil;
+ mPopoverItem = nil;
+}
+
+nsresult nsTouchBarInputIcon::SetupIcon(nsCOMPtr<nsIURI> aIconURI) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // We might not have a document if the Touch Bar tries to update when the main
+ // window is closed.
+ if (!mDocument) {
+ return NS_OK;
+ }
+
+ if (!(mButton || mShareScrubber || mPopoverItem)) {
+ NS_ERROR("No Touch Bar input provided.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mIconLoader) {
+ mIconLoader = new IconLoader(this);
+ }
+
+ if (!mSetIcon) {
+ // Load placeholder icon.
+ NSSize iconSize = NSMakeSize(kIconHeight, kIconHeight);
+ NSImage* placeholder = [MOZIconHelper placeholderIconWithSize:iconSize];
+ [mButton setImage:placeholder];
+ [mShareScrubber setButtonImage:placeholder];
+ [mPopoverItem setCollapsedRepresentationImage:placeholder];
+ }
+
+ nsresult rv =
+ mIconLoader->LoadIcon(aIconURI, mDocument, true /* aIsInternalIcon */);
+ if (NS_FAILED(rv)) {
+ // There is no icon for this menu item, as an error occurred while loading
+ // it. An icon might have been set earlier or the place holder icon may have
+ // been set. Clear it.
+ [mButton setImage:nil];
+ [mShareScrubber setButtonImage:nil];
+ [mPopoverItem setCollapsedRepresentationImage:nil];
+ }
+
+ mSetIcon = true;
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsTouchBarInputIcon::ReleaseJSObjects() { mDocument = nil; }
+
+//
+// mozilla::widget::IconLoader::Listener
+//
+
+nsresult nsTouchBarInputIcon::OnComplete(imgIContainer* aImage) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ // We ask only for the HiDPI images since all Touch Bars are Retina
+ // displays and we have no need for icons @1x.
+ NSImage* image = [MOZIconHelper
+ iconImageFromImageContainer:aImage
+ withSize:NSMakeSize(kIconHeight, kIconHeight)
+ presContext:nullptr
+ computedStyle:nullptr
+ scaleFactor:kHiDPIScalingFactor];
+ [mButton setImage:image];
+ [mShareScrubber setButtonImage:image];
+ [mPopoverItem setCollapsedRepresentationImage:image];
+
+ mIconLoader->Destroy();
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
diff --git a/widget/cocoa/nsTouchBarUpdater.h b/widget/cocoa/nsTouchBarUpdater.h
new file mode 100644
index 0000000000..38039f69a0
--- /dev/null
+++ b/widget/cocoa/nsTouchBarUpdater.h
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#ifndef nsTouchBarUpdater_h_
+#define nsTouchBarUpdater_h_
+
+#include "nsITouchBarUpdater.h"
+#include "nsCocoaWindow.h"
+
+class nsTouchBarUpdater : public nsITouchBarUpdater {
+ public:
+ nsTouchBarUpdater() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITOUCHBARUPDATER
+
+ protected:
+ virtual ~nsTouchBarUpdater() {}
+ BaseWindow* GetCocoaWindow(nsIBaseWindow* aWindow);
+};
+
+#endif // nsTouchBarUpdater_h_
diff --git a/widget/cocoa/nsTouchBarUpdater.mm b/widget/cocoa/nsTouchBarUpdater.mm
new file mode 100644
index 0000000000..ed0b78c6d1
--- /dev/null
+++ b/widget/cocoa/nsTouchBarUpdater.mm
@@ -0,0 +1,116 @@
+/* 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/. */
+
+#import <Cocoa/Cocoa.h>
+
+#include "nsTouchBar.h"
+#include "nsTouchBarInput.h"
+#include "nsTouchBarUpdater.h"
+
+#include "nsIBaseWindow.h"
+#include "nsIWidget.h"
+
+// defined in nsCocoaWindow.mm.
+extern BOOL sTouchBarIsInitialized;
+
+NS_IMPL_ISUPPORTS(nsTouchBarUpdater, nsITouchBarUpdater);
+
+NS_IMETHODIMP
+nsTouchBarUpdater::UpdateTouchBarInputs(
+ nsIBaseWindow* aWindow, const nsTArray<RefPtr<nsITouchBarInput>>& aInputs) {
+ if (!sTouchBarIsInitialized || !aWindow) {
+ return NS_OK;
+ }
+
+ BaseWindow* cocoaWin = nsTouchBarUpdater::GetCocoaWindow(aWindow);
+ if (!cocoaWin) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if ([cocoaWin respondsToSelector:@selector(touchBar)]) {
+ size_t itemCount = aInputs.Length();
+ for (size_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITouchBarInput> input(aInputs.ElementAt(i));
+ if (!input) {
+ continue;
+ }
+
+ NSTouchBarItemIdentifier newIdentifier =
+ [TouchBarInput nativeIdentifierWithXPCOM:input];
+ // We don't support updating the Share scrubber since it's a special
+ // Apple-made component that behaves differently from the other inputs.
+ if ([newIdentifier
+ isEqualToString:[TouchBarInput
+ nativeIdentifierWithType:@"scrubber"
+ withKey:@"share"]]) {
+ continue;
+ }
+
+ TouchBarInput* convertedInput =
+ [[TouchBarInput alloc] initWithXPCOM:input];
+ [(nsTouchBar*)cocoaWin.touchBar updateItem:convertedInput];
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTouchBarUpdater::ShowPopover(nsIBaseWindow* aWindow,
+ nsITouchBarInput* aPopover, bool aShowing) {
+ if (!sTouchBarIsInitialized || !aPopover || !aWindow) {
+ return NS_OK;
+ }
+
+ BaseWindow* cocoaWin = nsTouchBarUpdater::GetCocoaWindow(aWindow);
+ if (!cocoaWin) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if ([cocoaWin respondsToSelector:@selector(touchBar)]) {
+ // We don't need to completely reinitialize the popover. We only need its
+ // identifier to look it up in [nsTouchBar mappedLayoutItems].
+ NSTouchBarItemIdentifier popoverIdentifier =
+ [TouchBarInput nativeIdentifierWithXPCOM:aPopover];
+
+ TouchBarInput* popoverItem =
+ [[(nsTouchBar*)cocoaWin.touchBar mappedLayoutItems]
+ objectForKey:popoverIdentifier];
+
+ [(nsTouchBar*)cocoaWin.touchBar showPopover:popoverItem showing:aShowing];
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTouchBarUpdater::EnterCustomizeMode() {
+ [NSApp toggleTouchBarCustomizationPalette:(id)this];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTouchBarUpdater::IsTouchBarInitialized(bool* aResult) {
+ *aResult = sTouchBarIsInitialized;
+ return NS_OK;
+}
+
+BaseWindow* nsTouchBarUpdater::GetCocoaWindow(nsIBaseWindow* aWindow) {
+ nsCOMPtr<nsIWidget> widget = nullptr;
+ aWindow->GetMainWidget(getter_AddRefs(widget));
+ if (!widget) {
+ return nil;
+ }
+ BaseWindow* cocoaWin = (BaseWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (!cocoaWin) {
+ return nil;
+ }
+ return cocoaWin;
+}
+
+// NOTE: This method is for internal unit tests only.
+NS_IMETHODIMP
+nsTouchBarUpdater::SetTouchBarInitialized(bool aIsInitialized) {
+ sTouchBarIsInitialized = aIsInitialized;
+ return NS_OK;
+}
diff --git a/widget/cocoa/nsUserIdleServiceX.h b/widget/cocoa/nsUserIdleServiceX.h
new file mode 100644
index 0000000000..ff559293e9
--- /dev/null
+++ b/widget/cocoa/nsUserIdleServiceX.h
@@ -0,0 +1,36 @@
+/* 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/. */
+
+#ifndef nsUserIdleServiceX_h_
+#define nsUserIdleServiceX_h_
+
+#include "nsUserIdleService.h"
+#include "mozilla/AppShutdown.h"
+
+class nsUserIdleServiceX : public nsUserIdleService {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsUserIdleServiceX, nsUserIdleService)
+
+ bool PollIdleTime(uint32_t* aIdleTime) override;
+
+ static already_AddRefed<nsUserIdleServiceX> GetInstance() {
+ RefPtr<nsUserIdleService> idleService = nsUserIdleService::GetInstance();
+ if (!idleService) {
+ // Avoid late instantiation or resurrection during shutdown.
+ if (mozilla::AppShutdown::IsInOrBeyond(
+ mozilla::ShutdownPhase::AppShutdownConfirmed)) {
+ return nullptr;
+ }
+ idleService = new nsUserIdleServiceX();
+ }
+
+ return idleService.forget().downcast<nsUserIdleServiceX>();
+ }
+
+ protected:
+ nsUserIdleServiceX() {}
+ virtual ~nsUserIdleServiceX() {}
+};
+
+#endif // nsUserIdleServiceX_h_
diff --git a/widget/cocoa/nsUserIdleServiceX.mm b/widget/cocoa/nsUserIdleServiceX.mm
new file mode 100644
index 0000000000..c3ae3f904f
--- /dev/null
+++ b/widget/cocoa/nsUserIdleServiceX.mm
@@ -0,0 +1,60 @@
+/* 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 "nsUserIdleServiceX.h"
+#include "nsObjCExceptions.h"
+#import <Foundation/Foundation.h>
+
+bool nsUserIdleServiceX::PollIdleTime(uint32_t* aIdleTime) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ kern_return_t rval;
+ mach_port_t masterPort;
+
+ rval = IOMasterPort(kIOMasterPortDefault, &masterPort);
+ if (rval != KERN_SUCCESS) return false;
+
+ io_iterator_t hidItr;
+ rval = IOServiceGetMatchingServices(
+ masterPort, IOServiceMatching("IOHIDSystem"), &hidItr);
+
+ if (rval != KERN_SUCCESS) return false;
+ NS_ASSERTION(hidItr, "Our iterator is null, but it ought not to be!");
+
+ io_registry_entry_t entry = IOIteratorNext(hidItr);
+ NS_ASSERTION(entry, "Our IO Registry Entry is null, but it shouldn't be!");
+
+ IOObjectRelease(hidItr);
+
+ NSMutableDictionary* hidProps;
+ rval = IORegistryEntryCreateCFProperties(
+ entry, (CFMutableDictionaryRef*)&hidProps, kCFAllocatorDefault, 0);
+ if (rval != KERN_SUCCESS) return false;
+ NS_ASSERTION(hidProps, "HIDProperties is null, but no error was returned.");
+ [hidProps autorelease];
+
+ id idleObj = [hidProps objectForKey:@"HIDIdleTime"];
+ NS_ASSERTION([idleObj isKindOfClass:[NSData class]] ||
+ [idleObj isKindOfClass:[NSNumber class]],
+ "What we got for the idle object is not what we expect!");
+
+ uint64_t time;
+ if ([idleObj isKindOfClass:[NSData class]])
+ [idleObj getBytes:&time length:sizeof(time)];
+ else
+ time = [idleObj unsignedLongLongValue];
+
+ IOObjectRelease(entry);
+
+ // convert to ms from ns
+ time /= 1000000;
+ if (time > UINT32_MAX) // Overflow will occur
+ return false;
+
+ *aIdleTime = static_cast<uint32_t>(time);
+
+ return true;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+}
diff --git a/widget/cocoa/nsWidgetFactory.h b/widget/cocoa/nsWidgetFactory.h
new file mode 100644
index 0000000000..ce3ca756db
--- /dev/null
+++ b/widget/cocoa/nsWidgetFactory.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+// This file contains forward declarations for classes defined in static
+// components. The appropriate headers for those types cannot be included in
+// the generated static component code directly.
+
+#include "nsID.h"
+
+namespace mozilla {
+class OSXNotificationCenter;
+} // namespace mozilla
+
+namespace mozilla::widget {
+class ScreenManager;
+}
+
+class nsClipboardHelper;
+class nsColorPicker;
+class nsDeviceContextSpecX;
+class nsDragService;
+class nsFilePicker;
+class nsHTMLFormatConverter;
+class nsMacDockSupport;
+class nsMacFinderProgress;
+class nsMacSharingService;
+class nsMacUserActivityUpdater;
+class nsMacWebAppUtils;
+class nsPrintDialogServiceX;
+class nsPrintSettingsServiceX;
+class nsPrinterListCUPS;
+class nsSound;
+class nsStandaloneNativeMenu;
+class nsSystemStatusBarCocoa;
+class nsTouchBarUpdater;
+class nsTransferable;
+class nsUserIdleServiceX;
+
+nsresult nsAppShellConstructor(const nsIID&, void**);
+
+void nsWidgetCocoaModuleCtor();
+void nsWidgetCocoaModuleDtor();
diff --git a/widget/cocoa/nsWidgetFactory.mm b/widget/cocoa/nsWidgetFactory.mm
new file mode 100644
index 0000000000..3126b8c225
--- /dev/null
+++ b/widget/cocoa/nsWidgetFactory.mm
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsISupports.h"
+#include "mozilla/Components.h"
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/WidgetUtils.h"
+
+#include "nsWidgetsCID.h"
+
+#include "nsChildView.h"
+#include "nsAppShell.h"
+#include "nsAppShellSingleton.h"
+#include "nsFilePicker.h"
+#include "nsColorPicker.h"
+
+#include "nsClipboard.h"
+#include "nsClipboardHelper.h"
+#include "HeadlessClipboard.h"
+#include "gfxPlatform.h"
+#include "nsTransferable.h"
+#include "nsHTMLFormatConverter.h"
+#include "nsDragService.h"
+#include "nsToolkit.h"
+
+#include "nsLookAndFeel.h"
+
+#include "nsSound.h"
+#include "nsUserIdleServiceX.h"
+#include "NativeKeyBindings.h"
+#include "OSXNotificationCenter.h"
+
+#include "nsDeviceContextSpecX.h"
+#include "nsPrinterListCUPS.h"
+#include "nsPrintSettingsServiceX.h"
+#include "nsPrintDialogX.h"
+#include "nsToolkitCompsCID.h"
+
+#include "mozilla/widget/ScreenManager.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+NS_IMPL_COMPONENT_FACTORY(nsIClipboard) {
+ nsCOMPtr<nsIClipboard> inst;
+ if (gfxPlatform::IsHeadless()) {
+ inst = new HeadlessClipboard();
+ } else {
+ inst = new nsClipboard();
+ }
+
+ return inst.forget();
+}
+
+#define MAKE_GENERIC_CTOR(class_, iface_) \
+ NS_IMPL_COMPONENT_FACTORY(class_) { \
+ RefPtr inst = new class_(); \
+ return inst.forget().downcast<iface_>(); \
+ }
+
+#define MAKE_GENERIC_CTOR_INIT(class_, iface_, init_) \
+ NS_IMPL_COMPONENT_FACTORY(class_) { \
+ RefPtr inst = new class_(); \
+ if (NS_SUCCEEDED(inst->init_())) { \
+ return inst.forget().downcast<iface_>(); \
+ } \
+ return nullptr; \
+ }
+
+#define MAKE_GENERIC_SINGLETON_CTOR(iface_, func_) \
+ NS_IMPL_COMPONENT_FACTORY(iface_) { return func_(); }
+
+MAKE_GENERIC_CTOR(nsFilePicker, nsIFilePicker)
+MAKE_GENERIC_CTOR(nsColorPicker, nsIColorPicker)
+MAKE_GENERIC_CTOR(nsSound, nsISound)
+MAKE_GENERIC_CTOR(nsTransferable, nsITransferable)
+MAKE_GENERIC_CTOR(nsHTMLFormatConverter, nsIFormatConverter)
+MAKE_GENERIC_CTOR(nsClipboardHelper, nsIClipboardHelper)
+MAKE_GENERIC_CTOR(nsDragService, nsIDragService)
+MAKE_GENERIC_CTOR(nsDeviceContextSpecX, nsIDeviceContextSpec)
+MAKE_GENERIC_CTOR(nsPrinterListCUPS, nsIPrinterList)
+MAKE_GENERIC_CTOR_INIT(nsPrintSettingsServiceX, nsIPrintSettingsService, Init)
+MAKE_GENERIC_CTOR_INIT(nsPrintDialogServiceX, nsIPrintDialogService, Init)
+MAKE_GENERIC_SINGLETON_CTOR(nsUserIdleServiceX, nsUserIdleServiceX::GetInstance)
+MAKE_GENERIC_SINGLETON_CTOR(ScreenManager, ScreenManager::GetAddRefedSingleton)
+MAKE_GENERIC_CTOR_INIT(OSXNotificationCenter, nsIAlertsService, Init)
+
+#include "nsMacDockSupport.h"
+MAKE_GENERIC_CTOR(nsMacDockSupport, nsIMacDockSupport)
+
+#include "nsMacFinderProgress.h"
+MAKE_GENERIC_CTOR(nsMacFinderProgress, nsIMacFinderProgress)
+
+#include "nsMacSharingService.h"
+MAKE_GENERIC_CTOR(nsMacSharingService, nsIMacSharingService)
+
+#include "nsMacUserActivityUpdater.h"
+MAKE_GENERIC_CTOR(nsMacUserActivityUpdater, nsIMacUserActivityUpdater)
+
+#include "nsMacWebAppUtils.h"
+MAKE_GENERIC_CTOR(nsMacWebAppUtils, nsIMacWebAppUtils)
+
+#include "nsStandaloneNativeMenu.h"
+MAKE_GENERIC_CTOR(nsStandaloneNativeMenu, nsIStandaloneNativeMenu)
+
+#include "nsSystemStatusBarCocoa.h"
+MAKE_GENERIC_CTOR(nsSystemStatusBarCocoa, nsISystemStatusBar)
+
+#include "nsTouchBarUpdater.h"
+MAKE_GENERIC_CTOR(nsTouchBarUpdater, nsITouchBarUpdater)
+
+void nsWidgetCocoaModuleCtor() { nsAppShellInit(); }
+
+void nsWidgetCocoaModuleDtor() {
+ // Shutdown all XP level widget classes.
+ WidgetUtils::Shutdown();
+
+ NativeKeyBindings::Shutdown();
+ nsLookAndFeel::Shutdown();
+ nsToolkit::Shutdown();
+ nsAppShellShutdown();
+}
diff --git a/widget/cocoa/nsWindowMap.h b/widget/cocoa/nsWindowMap.h
new file mode 100644
index 0000000000..ad742bd0d1
--- /dev/null
+++ b/widget/cocoa/nsWindowMap.h
@@ -0,0 +1,61 @@
+/* -*- 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/. */
+
+#ifndef nsWindowMap_h_
+#define nsWindowMap_h_
+
+#import <Cocoa/Cocoa.h>
+
+// WindowDataMap
+//
+// In both mozilla and embedding apps, we need to have a place to put
+// per-top-level-window logic and data, to handle such things as IME
+// commit when the window gains/loses focus. We can't use a window
+// delegate, because an embeddor probably already has one. Nor can we
+// subclass NSWindow, again because we can't impose that burden on the
+// embeddor.
+//
+// So we have a global map of NSWindow -> TopLevelWindowData, and set
+// up TopLevelWindowData as a notification observer etc.
+
+@interface WindowDataMap : NSObject {
+ @private
+ NSMutableDictionary*
+ mWindowMap; // dict of TopLevelWindowData keyed by address of NSWindow
+}
+
++ (WindowDataMap*)sharedWindowDataMap;
+
+- (void)ensureDataForWindow:(NSWindow*)inWindow;
+- (id)dataForWindow:(NSWindow*)inWindow;
+
+// set data for a given window. inData is retained (and any previously set data
+// is released).
+- (void)setData:(id)inData forWindow:(NSWindow*)inWindow;
+
+// remove the data for the given window. the data is released.
+- (void)removeDataForWindow:(NSWindow*)inWindow;
+
+@end
+
+@class ChildView;
+
+// TopLevelWindowData
+//
+// Class to hold per-window data, and handle window state changes.
+
+@interface TopLevelWindowData : NSObject {
+ @private
+}
+
+ - (id)initWithWindow:(NSWindow*)inWindow;
+ + (void)activateInWindow:(NSWindow*)aWindow;
+ + (void)deactivateInWindow:(NSWindow*)aWindow;
+ + (void)activateInWindowViews:(NSWindow*)aWindow;
+ + (void)deactivateInWindowViews:(NSWindow*)aWindow;
+
+ @end
+
+#endif // nsWindowMap_h_
diff --git a/widget/cocoa/nsWindowMap.mm b/widget/cocoa/nsWindowMap.mm
new file mode 100644
index 0000000000..d93f89cb58
--- /dev/null
+++ b/widget/cocoa/nsWindowMap.mm
@@ -0,0 +1,291 @@
+/* -*- 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 "nsWindowMap.h"
+#include "nsObjCExceptions.h"
+#include "nsChildView.h"
+#include "nsCocoaWindow.h"
+
+@interface WindowDataMap (Private)
+
+- (NSString*)keyForWindow:(NSWindow*)inWindow;
+
+@end
+
+@interface TopLevelWindowData (Private)
+
+- (void)windowResignedKey:(NSNotification*)inNotification;
+- (void)windowBecameKey:(NSNotification*)inNotification;
+- (void)windowWillClose:(NSNotification*)inNotification;
+
+@end
+
+#pragma mark -
+
+@implementation WindowDataMap
+
++ (WindowDataMap*)sharedWindowDataMap {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ static WindowDataMap* sWindowMap = nil;
+ if (!sWindowMap) sWindowMap = [[WindowDataMap alloc] init];
+
+ return sWindowMap;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)init {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super init])) {
+ mWindowMap = [[NSMutableDictionary alloc] initWithCapacity:10];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mWindowMap release];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)ensureDataForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!inWindow || [self dataForWindow:inWindow]) return;
+
+ TopLevelWindowData* windowData =
+ [[TopLevelWindowData alloc] initWithWindow:inWindow];
+ [self setData:windowData forWindow:inWindow]; // takes ownership
+ [windowData release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (id)dataForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mWindowMap objectForKey:[self keyForWindow:inWindow]];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)setData:(id)inData forWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mWindowMap setObject:inData forKey:[self keyForWindow:inWindow]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)removeDataForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [mWindowMap removeObjectForKey:[self keyForWindow:inWindow]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSString*)keyForWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [NSString stringWithFormat:@"%p", inWindow];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+@end
+
+// TopLevelWindowData
+//
+// This class holds data about top-level windows. We can't use a window
+// delegate, because an embedder may already have one.
+
+@implementation TopLevelWindowData
+
+- (id)initWithWindow:(NSWindow*)inWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [super init])) {
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(windowBecameKey:)
+ name:NSWindowDidBecomeKeyNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(windowResignedKey:)
+ name:NSWindowDidResignKeyNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(windowBecameMain:)
+ name:NSWindowDidBecomeMainNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(windowResignedMain:)
+ name:NSWindowDidResignMainNotification
+ object:inWindow];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(windowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:inWindow];
+ }
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// As best I can tell, if the notification's object has a corresponding
+// top-level widget (an nsCocoaWindow object), it has a delegate (set in
+// nsCocoaWindow::StandardCreate()) of class WindowDelegate, and otherwise
+// not (Camino didn't use top-level widgets (nsCocoaWindow objects) --
+// only child widgets (nsChildView objects)). (The notification is sent
+// to windowBecameKey: or windowBecameMain: below.)
+//
+// For use with clients that (like Firefox) do use top-level widgets (and
+// have NSWindow delegates of class WindowDelegate).
++ (void)activateInWindow:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ WindowDelegate* delegate = (WindowDelegate*)[aWindow delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) return;
+
+ if ([delegate toplevelActiveState]) return;
+ [delegate sendToplevelActivateEvents];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// See comments above activateInWindow:
+//
+// If we're using top-level widgets (nsCocoaWindow objects), we send them
+// NS_DEACTIVATE events (which propagate to child widgets (nsChildView
+// objects) via nsWebShellWindow::HandleEvent()).
+//
+// For use with clients that (like Firefox) do use top-level widgets (and
+// have NSWindow delegates of class WindowDelegate).
++ (void)deactivateInWindow:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ WindowDelegate* delegate = (WindowDelegate*)[aWindow delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) return;
+
+ if (![delegate toplevelActiveState]) return;
+ [delegate sendToplevelDeactivateEvents];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// For use with clients that (like Camino) don't use top-level widgets (and
+// don't have NSWindow delegates of class WindowDelegate).
++ (void)activateInWindowViews:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ id firstResponder = [aWindow firstResponder];
+ if ([firstResponder isKindOfClass:[ChildView class]])
+ [firstResponder viewsWindowDidBecomeKey];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// For use with clients that (like Camino) don't use top-level widgets (and
+// don't have NSWindow delegates of class WindowDelegate).
++ (void)deactivateInWindowViews:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ id firstResponder = [aWindow firstResponder];
+ if ([firstResponder isKindOfClass:[ChildView class]])
+ [firstResponder viewsWindowDidResignKey];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// We make certain exceptions for top-level windows in non-embedders (see
+// comment above windowBecameMain below). And we need (elsewhere) to guard
+// against sending duplicate events. But in general the NS_ACTIVATE event
+// should be sent when a native window becomes key, and the NS_DEACTIVATE
+// event should be sent when it resignes key.
+- (void)windowBecameKey:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) {
+ [TopLevelWindowData activateInWindowViews:window];
+ } else if ([window isSheet] || [NSApp modalWindow]) {
+ [TopLevelWindowData activateInWindow:window];
+ }
+}
+
+- (void)windowResignedKey:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) {
+ [TopLevelWindowData deactivateInWindowViews:window];
+ } else if ([window isSheet] || [NSApp modalWindow]) {
+ [TopLevelWindowData deactivateInWindow:window];
+ }
+}
+
+// The appearance of a top-level window depends on its main state (not its key
+// state). So (for non-embedders) we need to ensure that a top-level window
+// is main when an NS_ACTIVATE event is sent to Gecko for it.
+- (void)windowBecameMain:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ // Don't send events to a top-level window that has a sheet/modal-window open
+ // above it -- as far as Gecko is concerned, it's inactive, and stays so until
+ // the sheet/modal-window closes.
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]] &&
+ ![window attachedSheet] && ![NSApp modalWindow])
+ [TopLevelWindowData activateInWindow:window];
+}
+
+- (void)windowResignedMain:(NSNotification*)inNotification {
+ NSWindow* window = (NSWindow*)[inNotification object];
+
+ id delegate = [window delegate];
+ if (delegate && [delegate isKindOfClass:[WindowDelegate class]] &&
+ ![window attachedSheet] && ![NSApp modalWindow])
+ [TopLevelWindowData deactivateInWindow:window];
+}
+
+- (void)windowWillClose:(NSNotification*)inNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // postpone our destruction
+ [[self retain] autorelease];
+
+ // remove ourselves from the window map (which owns us)
+ [[WindowDataMap sharedWindowDataMap]
+ removeDataForWindow:[inNotification object]];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
diff --git a/widget/cocoa/resources/MainMenu.nib/classes.nib b/widget/cocoa/resources/MainMenu.nib/classes.nib
new file mode 100644
index 0000000000..b9b4b09f6b
--- /dev/null
+++ b/widget/cocoa/resources/MainMenu.nib/classes.nib
@@ -0,0 +1,4 @@
+{
+ IBClasses = ({CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; });
+ IBVersion = 1;
+} \ No newline at end of file
diff --git a/widget/cocoa/resources/MainMenu.nib/info.nib b/widget/cocoa/resources/MainMenu.nib/info.nib
new file mode 100644
index 0000000000..bcf3ace841
--- /dev/null
+++ b/widget/cocoa/resources/MainMenu.nib/info.nib
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBDocumentLocation</key>
+ <string>159 127 356 240 0 0 1920 1178 </string>
+ <key>IBEditorPositions</key>
+ <dict>
+ <key>29</key>
+ <string>413 971 130 44 0 0 1920 1178 </string>
+ </dict>
+ <key>IBFramework Version</key>
+ <string>443.0</string>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>29</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>8F46</string>
+</dict>
+</plist>
diff --git a/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib b/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 0000000000..16b3f7e523
--- /dev/null
+++ b/widget/cocoa/resources/MainMenu.nib/keyedobjects.nib
Binary files differ
diff --git a/widget/components.conf b/widget/components.conf
new file mode 100644
index 0000000000..ecf1cd5009
--- /dev/null
+++ b/widget/components.conf
@@ -0,0 +1,83 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'js_name': 'clipboard',
+ 'cid': '{8b5314ba-db01-11d2-96ce-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/clipboard;1'],
+ 'constructor': 'nsClipboardSelector',
+ 'headers': ['/widget/nsContentProcessWidgetFactory.h'],
+ 'interfaces': ['nsIClipboard'],
+ 'overridable': True,
+ },
+ {
+ 'cid': '{c0ed2a75-96f8-4166-91d4-2fe8774448dc}',
+ 'type': 'nsClipboardProxy',
+ 'headers': ['/widget/nsClipboardProxy.h'],
+ 'contract_ids': ['@mozilla.org/widget/content/clipboard;1'],
+ 'processes': ProcessSelector.CONTENT_PROCESS_ONLY,
+ },
+
+ {
+ 'cid': '{0f872c8c-3ee6-46bd-92a2-69652c6b474e}',
+ 'contract_ids': ['@mozilla.org/colorpicker;1'],
+ 'constructor': 'nsColorPickerSelector',
+ 'headers': ['/widget/nsContentProcessWidgetFactory.h'],
+ },
+ {
+ 'cid': '{11a77259-9d16-4386-8ac8-94338ee22f78}',
+ 'type': 'nsColorPickerProxy',
+ 'headers': ['/widget/nsColorPickerProxy.h'],
+ 'contract_ids': ['@mozilla.org/content/colorpicker;1'],
+ 'processes': ProcessSelector.CONTENT_PROCESS_ONLY,
+ },
+
+ {
+ 'cid': '{8b5314bb-db01-11d2-96ce-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/dragservice;1'],
+ 'constructor': 'nsDragServiceSelector',
+ 'headers': ['/widget/nsContentProcessWidgetFactory.h'],
+ },
+ {
+ 'cid': '{28be18ae-73ee-494f-8c6d-5d14b7c998c7}',
+ 'type': 'nsDragServiceProxy',
+ 'headers': ['/widget/nsDragServiceProxy.h'],
+ 'contract_ids': ['@mozilla.org/widget/content/dragservice;1'],
+ 'processes': ProcessSelector.CONTENT_PROCESS_ONLY,
+ },
+
+ {
+ 'cid': '{bd57cee8-1dd1-11b2-9fe7-95cf4709aea3}',
+ 'contract_ids': ['@mozilla.org/filepicker;1'],
+ 'constructor': 'nsFilePickerSelector',
+ 'headers': ['/widget/nsContentProcessWidgetFactory.h'],
+ },
+ {
+ 'cid': '{40fd47f2-463a-4e4a-a33f-27eb148bfee4}',
+ 'type': 'nsFilePickerProxy',
+ 'headers': ['/widget/nsFilePickerProxy.h'],
+ 'contract_ids': ['@mozilla.org/content/filepicker;1'],
+ 'processes': ProcessSelector.CONTENT_PROCESS_ONLY,
+ },
+
+ {
+ 'cid': '{c401eb80-f9ea-11d3-bb6f-e732b73ebe7c}',
+ 'contract_ids': ['@mozilla.org/gfx/screenmanager;1'],
+ 'singleton': True,
+ 'constructor': 'nsScreenManagerSelector',
+ 'headers': ['/widget/nsContentProcessWidgetFactory.h'],
+ },
+ {
+ 'cid': '{b2cdd51c-4277-417b-a931-08306c7814c3}',
+ 'type': 'mozilla::widget::ScreenManager',
+ 'constructor': 'mozilla::widget::ScreenManager::GetAddRefedSingleton',
+ 'headers': ['mozilla/widget/ScreenManager.h'],
+ 'contract_ids': ['@mozilla.org/gfx/content/screenmanager;1'],
+ 'singleton': True,
+ 'processes': ProcessSelector.CONTENT_PROCESS_ONLY,
+ },
+]
diff --git a/widget/crashtests/1128214.html b/widget/crashtests/1128214.html
new file mode 100644
index 0000000000..749871c9e6
--- /dev/null
+++ b/widget/crashtests/1128214.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase for bug 1128214</title>
+ <style type="text/css">
+ html,body {
+ color:black;
+ background-color:white;
+ font-size:16px;
+ padding:10px;
+ margin:0;
+ }
+ </style>
+</head>
+<body>
+<div style="-moz-appearance:-moz-window-button-close; background-color: #ffd800; width:20px; height:20px;"></div>
+</body>
+</html>
diff --git a/widget/crashtests/303901-1.html b/widget/crashtests/303901-1.html
new file mode 100644
index 0000000000..51511ba182
--- /dev/null
+++ b/widget/crashtests/303901-1.html
@@ -0,0 +1,29 @@
+<HEAD>
+<IFRAME SRC=? SRC=808080 MARGINHEIGHT=https: WIDTH=808080 MARGINWIDTH=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.html onLoad=* HEIGHT=
+ MARGINWIDTH=$ SRC=& ADDRESS=^ onLoad=.gif SRC=ffff ADDRESS=? FRAMEBORDER=0xfffffff STYLE=QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ.rdf MARGINHEIGHT=? MARGINHEIGHT=89000 NAME=808080 WIDTH=-1 SCROLLING=chrome: >
+<TD onLoad=chrome: STYLE=22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222.jpg ALIGN=$ ROWSPAN=.html VALIGN=https:iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii VALIGN=127 STYLE=about: onLoad=chrome: STYLE=7897 onLoad=" COLSPAN=90928345 COLSPAN=.bmp ROWSPAN=7897 BGCOLOR=ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt.jpg VALIGN=^ ALIGN=89000 VALIGN=7897 ROWSPAN=* ALIGN=? ROWSPAN=% >
+<FORM SCRIPT=90928345 METHOD=nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn.png STYLE=-1 SCRIPT=about:&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& ACTION=.xul TARGET=& SCRIPT=.rdf TARGET=A TARGET=%n%n%n%n%n%n%n%n%n%n%n%n TARGET=\ TARGET=-1 ACTION=VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV.rdf onLoad=about:OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO METHOD=74326794236234 TARGET=-1 STYLE=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.xul METHOD=" ACTION=0xfffffff SCRIPT=* METHOD== >
+<OPTION STYLE=
+ VALUE=-1 onLoad=89000 SHAPE=90928345 SHAPE=HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH.xpt VALUE=https: onLoad=https: STYLE=.txt STYLE=tttttttttttttttttttttt.xbm SHAPE=" VALUE=%n%n%n%n%n%n%n%n%n%n%n%n SHAPE=%n%n%n%n%n%n%n%n%n%n%n%n STYLE=chrome: onLoad== VALUE=.ico onLoad=" VALUE=74326794236234 SHAPE=A onLoad=89000 STYLE=%s%n%s%n%s%n%s%n%s%n%s%n >
+<TFOOT VALIGN=? BGCOLOR=-1 VALIGN== VALIGN=about: ALIGN=0xfffffff BGCOLOR=127 COLSPAN=0 BGCOLOR=& COLSPAN=A ROWSPAN=file: VALIGN=0 onLoad=: STYLE=: VALIGN=7897 ALIGN=7897 ALIGN=file:uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu VALIGN=$ VALIGN=90928345 onLoad=-1 VALIGN=.xpi >
+<FORM onLoad=
+ TARGET=-1 ACTION== ENCTYPE=chrome: onLoad=.xbm ACTION=.gif STYLE=-1 SCRIPT== ACTION=chrome:BBBBBBBBBB ACTION=ffff METHOD=%n%n%n%n%n%n%n%n%n%n%n%n STYLE=
+ onLoad=% TARGET=%s%n%s%n%s%n%s%n%s%n%s%n SCRIPT=74326794236234 onLoad=
+ ACTION=oooooooooooooooooooooooo.png ENCTYPE=^ STYLE=http: ACTION=" >
+<CENTER STYLE=wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.png onLoad=0xfffffff onLoad=https: STYLE=????????????????????????????????????????????????????????????????????????????.ico onLoad=127 STYLE=127 onLoad=-1 STYLE=ffff STYLE=.xbm STYLE=http:mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm STYLE=& onLoad=: onLoad=0 STYLE=89000 STYLE=about: STYLE=chrome:ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss onLoad=$ onLoad=%s%n%s%n%s%n%s%n%s%n%s%n onLoad=44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444.html onLoad=about: >
+<B onLoad=.swf STYLE=74326794236234 STYLE=chrome: onLoad=? onLoad=ffff onLoad=0 STYLE=0xfffffff STYLE=89000 onLoad=^ onLoad=0 STYLE=http: onLoad=about:rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr STYLE=90928345 STYLE=ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.xml onLoad=127 STYLE=90928345 STYLE=//////////////////////////////////////////////////////////////////////////////////////.xbm STYLE=-1 onLoad=% STYLE=$ >
+<FRAMESET onLoad=127 COLS=* STYLE=\ onLoad=7897 STYLE=? onLoad== STYLE=%n%n%n%n%n%n%n%n%n%n%n%n onLoad=74326794236234 STYLE=https:"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" onLoad=sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss.rdf COLS=808080 ROWS=0 ROWS=.xpi COLS=A STYLE=%s%n%s%n%s%n%s%n%s%n%s%n ROWS=90928345 COLS=? COLS=SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS.xml STYLE=: ROWS=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.dtd >
+<BQ CLEAR=about:kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk CLEAR=74326794236234 STYLE=%n%n%n%n%n%n%n%n%n%n%n%n NOWRAP=http: NOWRAP=gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.rdf onLoad=.swf NOWRAP=90928345 STYLE=89000 onLoad=: onLoad=http: NOWRAP=chrome: STYLE=89000 NOWRAP=90928345 CLEAR=89000 CLEAR=ffff onLoad=https:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy CLEAR=.png STYLE=74326794236234 onLoad=https: NOWRAP=http: >
+<A HREF=https: TITLE=https: SHAPE=7897 HREF=% HREF=127 SHAPE=.rdf REV=http: STYLE=-1 STYLE=? HREF=& REV=%s%n%s%n%s%n%s%n%s%n%s%n REF=\ REV=89000 TITLE=0 NAME=90928345 REV=808080 HREF=808080 REF=808080 STYLE=(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((.png onLoad=.bmp >
+<SPACER TYPE=%n%n%n%n%n%n%n%n%n%n%n%n ALIGN=.gif SIZE=0 TYPE=.bmp WIDTH=$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$.xul SIZE=0 SIZE=& SIZE=7897 HEIGHT=.xpt TYPE=89000 SIZE=ffff WIDTH=file: WIDTH=90928345 STYLE=-1 WIDTH=7897 onLoad=0xfffffff ALIGN=-1 TYPE=about:9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 TYPE=89000 SIZE=UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU.swf >
+<COLGROUP onLoad=file:IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII onLoad=7897 WIDTH=.xpt WIDTH=
+ ALIGN== VALIGN=about:DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD HALIGN=90928345 HALIGN=file:lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll VALIGN=$ HALIGN=0xfffffff WIDTH=74326794236234 WIDTH=0xfffffff VALIGN=89000 STYLE=7897 WIDTH=^ HALIGN=% onLoad=74326794236234 ALIGN=127 HALIGN=* VALIGN=vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv.txt >
+<SPACER TYPE=.xpi ALIGN=.png WIDTH=127 SIZE=%s%n%s%n%s%n%s%n%s%n%s%n TYPE=& onLoad=89000 STYLE=808080 HEIGHT=chrome:``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` WIDTH=KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK.swf WIDTH=$ TYPE=http:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! TYPE=" HEIGHT=% TYPE=0 TYPE=127 STYLE=90928345 SIZE=
+ STYLE=\ SIZE=https: HEIGHT=https: >
+<BODY BGPROPERTIES=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.jpg LEFTMARGIN=about:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq TEXT=ffff BGPROPERTIES=89000 TOPMARGIN=.png TEXT=about: VLINK=^ TOPMARGIN=0xfffffff TOPMARGIN=% onLoad=90928345 onLoad=999999999999999999999999999999999999999999999999999999999999999.xpi BGCOLOR=`````````````````````````````````````````````````````````````````````````````````````.xpt TEXT=% LINK=: TOPMARGIN=%s%n%s%n%s%n%s%n%s%n%s%n LEFTMARGIN=? TOPMARGIN=chrome:............................................................................................... BGPROPERTIES=& LEFTMARGIN=111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.xpi onLoad=" >
+<BGSOUND STYLE=% LOOP=about:MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM STYLE=90928345 SRC=7897 onLoad=90928345 SRC=.xul onLoad=ffff onLoad=0xfffffff SRC=^ SRC=222222222222222222222222222222222.tmpl SRC=0xfffffff LOOP=%s%n%s%n%s%n%s%n%s%n%s%n SRC=7897 STYLE=about: LOOP=0 STYLE=74326794236234 SRC=90928345 onLoad=-1 SRC=74326794236234 LOOP=" >
+<HEAD STYLE=http: onLoad=.swf STYLE=http:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> onLoad=: onLoad=chrome: STYLE=89000 STYLE=http:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy STYLE=? STYLE=ffff onLoad=90928345 onLoad=https: STYLE=file: STYLE=\ onLoad=.xul onLoad=about:3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333 onLoad=.swf STYLE=? onLoad=chrome:GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG STYLE=.ico STYLE=& >
+<SCRIPT LANGUAGE=% onLoad=808080 STYLE=.xbm LANGUAGE=file:666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666 onLoad=about: onLoad=.xpi STYLE=ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss.ico onLoad=$ onLoad=.ico LANGUAGE=% onLoad=.xul onLoad=0xfffffff LANGUAGE=808080 onLoad=.ico STYLE=0 STYLE=%s%n%s%n%s%n%s%n%s%n%s%n STYLE=ffff STYLE=* LANGUAGE=* LANGUAGE=.gif >
+<OVERLAY X=hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh.rdf WIDTH=0xfffffff UNITS=0 Y=74326794236234 WIDTH=.png STYLE=about:TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT IMAGEMAP=^ WIDTH=ffff onLoad=A X=-1 IMAGEMAP=0xfffffff SRC=https:oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo SRC=7897 HEIGHT=.xpi HEIGHT=0xfffffff UNITS=* WIDTH=file: Y=fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.xpi IMAGEMAP=.dtd WIDTH=% >
+<B STYLE=^ STYLE=? STYLE=74326794236234 onLoad=https: onLoad=http: STYLE=& onLoad=7897 onLoad=
+ onLoad=808080 STYLE=https:QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ onLoad=74326794236234 onLoad=%s%n%s%n%s%n%s%n%s%n%s%n STYLE=7897 onLoad=: onLoad=90928345 onLoad=ffff onLoad=0xfffffff onLoad=%n%n%n%n%n%n%n%n%n%n%n%n STYLE=^ onLoad=$ >
diff --git a/widget/crashtests/303901-2.html b/widget/crashtests/303901-2.html
new file mode 100644
index 0000000000..00a70945d5
--- /dev/null
+++ b/widget/crashtests/303901-2.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html><head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase for bug </title>
+ <style type="text/css">
+
+ html,body {
+ color:black; background-color:white; font-size:16px; padding:0; margin:0;
+ }
+
+
+ </style>
+</head>
+<body>
+
+<div style="overflow:scroll; height:200000px;"></div>
+
+
+</body>
+</html>
diff --git a/widget/crashtests/380359-1.xhtml b/widget/crashtests/380359-1.xhtml
new file mode 100644
index 0000000000..00b60506a1
--- /dev/null
+++ b/widget/crashtests/380359-1.xhtml
@@ -0,0 +1,8 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+
+<xul:hbox style="display: block; position: fixed; -moz-appearance: checkbox;" />
+
+</body>
+</html>
diff --git a/widget/crashtests/crashtests.list b/widget/crashtests/crashtests.list
new file mode 100644
index 0000000000..957f3f005c
--- /dev/null
+++ b/widget/crashtests/crashtests.list
@@ -0,0 +1,4 @@
+load 303901-1.html
+load 303901-2.html
+load 380359-1.xhtml
+load 1128214.html
diff --git a/widget/generic/PCompositorWidget.ipdl b/widget/generic/PCompositorWidget.ipdl
new file mode 100644
index 0000000000..faa7bcda5a
--- /dev/null
+++ b/widget/generic/PCompositorWidget.ipdl
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 protocol PCompositorBridge;
+
+// This file is a stub, for platforms that do not yet support out-of-process
+// compositing or do not need specialized types to do so.
+
+namespace mozilla {
+namespace widget {
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PCompositorWidget
+{
+ manager PCompositorBridge;
+
+parent:
+ async __delete__();
+
+child:
+ async ObserveVsync();
+ async UnobserveVsync();
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/generic/PlatformWidgetTypes.ipdlh b/widget/generic/PlatformWidgetTypes.ipdlh
new file mode 100644
index 0000000000..c4b6d2ca4f
--- /dev/null
+++ b/widget/generic/PlatformWidgetTypes.ipdlh
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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/. */
+
+// This file is a stub, for platforms that do not yet support out-of-process
+// compositing or do not need specialized types to do so.
+
+include HeadlessWidgetTypes;
+
+namespace mozilla {
+namespace widget {
+
+union CompositorWidgetInitData
+{
+ HeadlessCompositorWidgetInitData;
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/AsyncDBus.cpp b/widget/gtk/AsyncDBus.cpp
new file mode 100644
index 0000000000..ca560aca96
--- /dev/null
+++ b/widget/gtk/AsyncDBus.cpp
@@ -0,0 +1,90 @@
+/* -*- 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 "AsyncDBus.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+namespace mozilla::widget {
+
+static void CreateProxyCallback(GObject*, GAsyncResult* aResult,
+ gpointer aUserData) {
+ RefPtr<DBusProxyPromise::Private> promise =
+ dont_AddRef(static_cast<DBusProxyPromise::Private*>(aUserData));
+ GUniquePtr<GError> error;
+ RefPtr<GDBusProxy> proxy = dont_AddRef(
+ g_dbus_proxy_new_for_bus_finish(aResult, getter_Transfers(error)));
+ if (proxy) {
+ promise->Resolve(std::move(proxy), __func__);
+ } else {
+ promise->Reject(std::move(error), __func__);
+ }
+}
+
+RefPtr<DBusProxyPromise> CreateDBusProxyForBus(
+ GBusType aBusType, GDBusProxyFlags aFlags,
+ GDBusInterfaceInfo* aInterfaceInfo, const char* aName,
+ const char* aObjectPath, const char* aInterfaceName,
+ GCancellable* aCancellable) {
+ auto promise = MakeRefPtr<DBusProxyPromise::Private>(__func__);
+ g_dbus_proxy_new_for_bus(aBusType, aFlags, aInterfaceInfo, aName, aObjectPath,
+ aInterfaceName, aCancellable, CreateProxyCallback,
+ do_AddRef(promise).take());
+ return promise.forget();
+}
+
+static void ProxyCallCallback(GObject* aSourceObject, GAsyncResult* aResult,
+ gpointer aUserData) {
+ RefPtr<DBusCallPromise::Private> promise =
+ dont_AddRef(static_cast<DBusCallPromise::Private*>(aUserData));
+ GUniquePtr<GError> error;
+ RefPtr<GVariant> result = dont_AddRef(g_dbus_proxy_call_finish(
+ G_DBUS_PROXY(aSourceObject), aResult, getter_Transfers(error)));
+ if (result) {
+ promise->Resolve(std::move(result), __func__);
+ } else {
+ promise->Reject(std::move(error), __func__);
+ }
+}
+
+RefPtr<DBusCallPromise> DBusProxyCall(GDBusProxy* aProxy, const char* aMethod,
+ GVariant* aArgs, GDBusCallFlags aFlags,
+ gint aTimeout,
+ GCancellable* aCancellable) {
+ auto promise = MakeRefPtr<DBusCallPromise::Private>(__func__);
+ g_dbus_proxy_call(aProxy, aMethod, aArgs, aFlags, aTimeout, aCancellable,
+ ProxyCallCallback, do_AddRef(promise).take());
+ return promise.forget();
+}
+
+static void ProxyCallWithUnixFDListCallback(GObject* aSourceObject,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ RefPtr<DBusCallPromise::Private> promise =
+ dont_AddRef(static_cast<DBusCallPromise::Private*>(aUserData));
+ GUniquePtr<GError> error;
+ GUnixFDList** aFDList = nullptr;
+ RefPtr<GVariant> result =
+ dont_AddRef(g_dbus_proxy_call_with_unix_fd_list_finish(
+ G_DBUS_PROXY(aSourceObject), aFDList, aResult,
+ getter_Transfers(error)));
+ if (result) {
+ promise->Resolve(std::move(result), __func__);
+ } else {
+ promise->Reject(std::move(error), __func__);
+ }
+}
+
+RefPtr<DBusCallPromise> DBusProxyCallWithUnixFDList(
+ GDBusProxy* aProxy, const char* aMethod, GVariant* aArgs,
+ GDBusCallFlags aFlags, gint aTimeout, GUnixFDList* aFDList,
+ GCancellable* aCancellable) {
+ auto promise = MakeRefPtr<DBusCallPromise::Private>(__func__);
+ g_dbus_proxy_call_with_unix_fd_list(
+ aProxy, aMethod, aArgs, aFlags, aTimeout, aFDList, aCancellable,
+ ProxyCallWithUnixFDListCallback, do_AddRef(promise).take());
+ return promise.forget();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/AsyncDBus.h b/widget/gtk/AsyncDBus.h
new file mode 100644
index 0000000000..b1ac9d9039
--- /dev/null
+++ b/widget/gtk/AsyncDBus.h
@@ -0,0 +1,38 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_AsyncDBus_h
+#define mozilla_widget_AsyncDBus_h
+
+#include "mozilla/GRefPtr.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/MozPromise.h"
+
+namespace mozilla::widget {
+
+using DBusProxyPromise = MozPromise<RefPtr<GDBusProxy>, GUniquePtr<GError>,
+ /* IsExclusive = */ true>;
+
+using DBusCallPromise = MozPromise<RefPtr<GVariant>, GUniquePtr<GError>,
+ /* IsExclusive = */ true>;
+
+RefPtr<DBusProxyPromise> CreateDBusProxyForBus(
+ GBusType aBusType, GDBusProxyFlags aFlags,
+ GDBusInterfaceInfo* aInterfaceInfo, const char* aName,
+ const char* aObjectPath, const char* aInterfaceName,
+ GCancellable* aCancellable = nullptr);
+
+RefPtr<DBusCallPromise> DBusProxyCall(GDBusProxy*, const char* aMethod,
+ GVariant* aArgs, GDBusCallFlags,
+ gint aTimeout = -1,
+ GCancellable* = nullptr);
+
+RefPtr<DBusCallPromise> DBusProxyCallWithUnixFDList(
+ GDBusProxy*, const char* aMethod, GVariant* aArgs, GDBusCallFlags,
+ gint aTimeout = -1, GUnixFDList* = nullptr, GCancellable* = nullptr);
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/gtk/AsyncGtkClipboardRequest.cpp b/widget/gtk/AsyncGtkClipboardRequest.cpp
new file mode 100644
index 0000000000..75801c698d
--- /dev/null
+++ b/widget/gtk/AsyncGtkClipboardRequest.cpp
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=4 et sw=2 tw=80: */
+/* 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 "AsyncGtkClipboardRequest.h"
+
+namespace mozilla {
+
+AsyncGtkClipboardRequest::AsyncGtkClipboardRequest(ClipboardDataType aDataType,
+ int32_t aWhichClipboard,
+ const char* aMimeType) {
+ GtkClipboard* clipboard =
+ gtk_clipboard_get(GetSelectionAtom(aWhichClipboard));
+ mRequest = MakeUnique<Request>(aDataType);
+
+ switch (aDataType) {
+ case ClipboardDataType::Data:
+ LOGCLIP(" getting DATA MIME %s\n", aMimeType);
+ gtk_clipboard_request_contents(clipboard,
+ gdk_atom_intern(aMimeType, FALSE),
+ OnDataReceived, mRequest.get());
+ break;
+ case ClipboardDataType::Text:
+ LOGCLIP(" getting TEXT\n");
+ gtk_clipboard_request_text(clipboard, OnTextReceived, mRequest.get());
+ break;
+ case ClipboardDataType::Targets:
+ LOGCLIP(" getting TARGETS\n");
+ gtk_clipboard_request_contents(clipboard,
+ gdk_atom_intern("TARGETS", FALSE),
+ OnDataReceived, mRequest.get());
+ break;
+ }
+}
+
+void AsyncGtkClipboardRequest::OnDataReceived(GtkClipboard* clipboard,
+ GtkSelectionData* selection_data,
+ gpointer data) {
+ int whichClipboard = GetGeckoClipboardType(clipboard);
+ LOGCLIP("OnDataReceived(%s) callback\n",
+ whichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+ static_cast<Request*>(data)->Complete(selection_data);
+}
+
+void AsyncGtkClipboardRequest::OnTextReceived(GtkClipboard* clipboard,
+ const gchar* text,
+ gpointer data) {
+ int whichClipboard = GetGeckoClipboardType(clipboard);
+ LOGCLIP("OnTextReceived(%s) callback\n",
+ whichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+ static_cast<Request*>(data)->Complete(text);
+}
+
+void AsyncGtkClipboardRequest::Request::Complete(const void* aData) {
+ LOGCLIP("Request::Complete(), aData = %p, timedOut = %d\n", aData, mTimedOut);
+
+ if (mTimedOut) {
+ delete this;
+ return;
+ }
+
+ mData.emplace();
+
+ gint dataLength = 0;
+ if (mDataType == ClipboardDataType::Targets ||
+ mDataType == ClipboardDataType::Data) {
+ dataLength = gtk_selection_data_get_length((GtkSelectionData*)aData);
+ } else {
+ dataLength = aData ? strlen((const char*)aData) : 0;
+ }
+
+ // Negative size means no data or data error.
+ if (dataLength <= 0) {
+ LOGCLIP(" zero dataLength, quit.\n");
+ return;
+ }
+
+ switch (mDataType) {
+ case ClipboardDataType::Targets: {
+ LOGCLIP(" getting %d bytes of clipboard targets.\n", dataLength);
+ gint n_targets = 0;
+ GdkAtom* targets = nullptr;
+ if (!gtk_selection_data_get_targets((GtkSelectionData*)aData, &targets,
+ &n_targets) ||
+ !n_targets) {
+ // We failed to get targets
+ return;
+ }
+ mData->SetTargets(
+ ClipboardTargets{GUniquePtr<GdkAtom>(targets), uint32_t(n_targets)});
+ break;
+ }
+ case ClipboardDataType::Text: {
+ LOGCLIP(" getting %d bytes of text.\n", dataLength);
+ mData->SetText(Span(static_cast<const char*>(aData), dataLength));
+ LOGCLIP(" done, mClipboardData = %p\n", mData->AsSpan().data());
+ break;
+ }
+ case ClipboardDataType::Data: {
+ LOGCLIP(" getting %d bytes of data.\n", dataLength);
+ mData->SetData(Span(gtk_selection_data_get_data((GtkSelectionData*)aData),
+ dataLength));
+ LOGCLIP(" done, mClipboardData = %p\n", mData->AsSpan().data());
+ break;
+ }
+ }
+}
+
+AsyncGtkClipboardRequest::~AsyncGtkClipboardRequest() {
+ if (mRequest && mRequest->mData.isNothing()) {
+ mRequest->mTimedOut = true;
+ Unused << mRequest.release();
+ }
+}
+
+} // namespace mozilla
diff --git a/widget/gtk/AsyncGtkClipboardRequest.h b/widget/gtk/AsyncGtkClipboardRequest.h
new file mode 100644
index 0000000000..1d72691560
--- /dev/null
+++ b/widget/gtk/AsyncGtkClipboardRequest.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_AsyncGtkClipboardRequest_h
+#define mozilla_AsyncGtkClipboardRequest_h
+
+#include "nsClipboard.h"
+
+namespace mozilla {
+
+// An asynchronous clipboard request that we wait for synchronously by
+// spinning the event loop.
+class MOZ_STACK_CLASS AsyncGtkClipboardRequest {
+ // Heap-allocated object that we give GTK as a callback.
+ struct Request {
+ explicit Request(ClipboardDataType aDataType) : mDataType(aDataType) {}
+
+ void Complete(const void*);
+
+ const ClipboardDataType mDataType;
+ Maybe<ClipboardData> mData;
+ bool mTimedOut = false;
+ };
+
+ UniquePtr<Request> mRequest;
+
+ static void OnDataReceived(GtkClipboard*, GtkSelectionData*, gpointer);
+ static void OnTextReceived(GtkClipboard*, const gchar*, gpointer);
+
+ public:
+ // Launch a request for a particular GTK clipboard. The current status of the
+ // request can be observed by calling HasCompleted() and TakeResult().
+ AsyncGtkClipboardRequest(ClipboardDataType, int32_t aWhichClipboard,
+ const char* aMimeType = nullptr);
+
+ // Returns whether the request has been answered already.
+ bool HasCompleted() const { return mRequest->mData.isSome(); }
+
+ // Takes the result from the current request if completed, or a
+ // default-constructed data otherwise. The destructor will take care of
+ // flagging the request as timed out in that case.
+ ClipboardData TakeResult() {
+ if (!HasCompleted()) {
+ return {};
+ }
+ auto request = std::move(mRequest);
+ return request->mData.extract();
+ }
+
+ // If completed, frees the request if needed. Otherwise, marks it as a timed
+ // out request so that when it completes the Request object is properly
+ // freed.
+ ~AsyncGtkClipboardRequest();
+};
+
+}; // namespace mozilla
+
+#endif
diff --git a/widget/gtk/CompositorWidgetChild.cpp b/widget/gtk/CompositorWidgetChild.cpp
new file mode 100644
index 0000000000..b7908a43d4
--- /dev/null
+++ b/widget/gtk/CompositorWidgetChild.cpp
@@ -0,0 +1,51 @@
+/* -*- 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 "CompositorWidgetChild.h"
+#include "mozilla/Unused.h"
+#include "gfxPlatform.h"
+
+namespace mozilla {
+namespace widget {
+
+CompositorWidgetChild::CompositorWidgetChild(
+ RefPtr<CompositorVsyncDispatcher> aVsyncDispatcher,
+ RefPtr<CompositorWidgetVsyncObserver> aVsyncObserver,
+ const CompositorWidgetInitData&)
+ : mVsyncDispatcher(aVsyncDispatcher), mVsyncObserver(aVsyncObserver) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(!gfxPlatform::IsHeadless());
+}
+
+CompositorWidgetChild::~CompositorWidgetChild() = default;
+
+bool CompositorWidgetChild::Initialize() { return true; }
+
+mozilla::ipc::IPCResult CompositorWidgetChild::RecvObserveVsync() {
+ mVsyncDispatcher->SetCompositorVsyncObserver(mVsyncObserver);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetChild::RecvUnobserveVsync() {
+ mVsyncDispatcher->SetCompositorVsyncObserver(nullptr);
+ return IPC_OK();
+}
+
+void CompositorWidgetChild::NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ Unused << SendNotifyClientSizeChanged(aClientSize);
+}
+
+void CompositorWidgetChild::DisableRendering() {
+ Unused << SendDisableRendering();
+}
+
+void CompositorWidgetChild::EnableRendering(const uintptr_t aXWindow,
+ const bool aShaped) {
+ Unused << SendEnableRendering(aXWindow, aShaped);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/CompositorWidgetChild.h b/widget/gtk/CompositorWidgetChild.h
new file mode 100644
index 0000000000..b1cad75da3
--- /dev/null
+++ b/widget/gtk/CompositorWidgetChild.h
@@ -0,0 +1,41 @@
+/* -*- 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/. */
+
+#ifndef widget_gtk_CompositorWidgetChild_h
+#define widget_gtk_CompositorWidgetChild_h
+
+#include "GtkCompositorWidget.h"
+#include "mozilla/widget/PCompositorWidgetChild.h"
+#include "mozilla/widget/CompositorWidgetVsyncObserver.h"
+
+namespace mozilla {
+namespace widget {
+
+class CompositorWidgetChild final : public PCompositorWidgetChild,
+ public PlatformCompositorWidgetDelegate {
+ public:
+ CompositorWidgetChild(RefPtr<CompositorVsyncDispatcher> aVsyncDispatcher,
+ RefPtr<CompositorWidgetVsyncObserver> aVsyncObserver,
+ const CompositorWidgetInitData&);
+ ~CompositorWidgetChild() override;
+
+ bool Initialize();
+
+ mozilla::ipc::IPCResult RecvObserveVsync() override;
+ mozilla::ipc::IPCResult RecvUnobserveVsync() override;
+
+ void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override;
+ void DisableRendering() override;
+ void EnableRendering(const uintptr_t aXWindow, const bool aShaped) override;
+
+ private:
+ RefPtr<CompositorVsyncDispatcher> mVsyncDispatcher;
+ RefPtr<CompositorWidgetVsyncObserver> mVsyncObserver;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_gtk_CompositorWidgetChild_h
diff --git a/widget/gtk/CompositorWidgetParent.cpp b/widget/gtk/CompositorWidgetParent.cpp
new file mode 100644
index 0000000000..998614622e
--- /dev/null
+++ b/widget/gtk/CompositorWidgetParent.cpp
@@ -0,0 +1,54 @@
+/* -*- 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 "CompositorWidgetParent.h"
+#include "mozilla/Unused.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "nsWindow.h"
+
+namespace mozilla::widget {
+
+CompositorWidgetParent::CompositorWidgetParent(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions)
+ : GtkCompositorWidget(aInitData.get_GtkCompositorWidgetInitData(), aOptions,
+ nullptr) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+}
+
+CompositorWidgetParent::~CompositorWidgetParent() = default;
+
+void CompositorWidgetParent::ObserveVsync(VsyncObserver* aObserver) {
+ if (aObserver) {
+ Unused << SendObserveVsync();
+ } else {
+ Unused << SendUnobserveVsync();
+ }
+ mVsyncObserver = aObserver;
+}
+
+RefPtr<VsyncObserver> CompositorWidgetParent::GetVsyncObserver() const {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+ return mVsyncObserver;
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvNotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ NotifyClientSizeChanged(aClientSize);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvDisableRendering() {
+ DisableRendering();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvEnableRendering(
+ const uintptr_t& aXWindow, const bool& aShaped) {
+ EnableRendering(aXWindow, aShaped);
+ return IPC_OK();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/CompositorWidgetParent.h b/widget/gtk/CompositorWidgetParent.h
new file mode 100644
index 0000000000..2bbc70af3e
--- /dev/null
+++ b/widget/gtk/CompositorWidgetParent.h
@@ -0,0 +1,42 @@
+/* -*- 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/. */
+
+#ifndef widget_gtk_CompositorWidgetParent_h
+#define widget_gtk_CompositorWidgetParent_h
+
+#include "GtkCompositorWidget.h"
+#include "mozilla/VsyncDispatcher.h"
+#include "mozilla/widget/PCompositorWidgetParent.h"
+
+namespace mozilla {
+namespace widget {
+
+class CompositorWidgetParent final : public PCompositorWidgetParent,
+ public GtkCompositorWidget {
+ public:
+ explicit CompositorWidgetParent(const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions);
+ ~CompositorWidgetParent() override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override {}
+
+ void ObserveVsync(VsyncObserver* aObserver) override;
+ RefPtr<VsyncObserver> GetVsyncObserver() const override;
+
+ mozilla::ipc::IPCResult RecvNotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) override;
+
+ mozilla::ipc::IPCResult RecvDisableRendering() override;
+ mozilla::ipc::IPCResult RecvEnableRendering(const uintptr_t& aXWindow,
+ const bool& aShaped) override;
+
+ private:
+ RefPtr<VsyncObserver> mVsyncObserver;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_gtk_CompositorWidgetParent_h
diff --git a/widget/gtk/DMABufLibWrapper.cpp b/widget/gtk/DMABufLibWrapper.cpp
new file mode 100644
index 0000000000..3db0219f88
--- /dev/null
+++ b/widget/gtk/DMABufLibWrapper.cpp
@@ -0,0 +1,346 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "DMABufLibWrapper.h"
+#ifdef MOZ_WAYLAND
+# include "nsWaylandDisplay.h"
+#endif
+#include "base/message_loop.h" // for MessageLoop
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "WidgetUtilsGtk.h"
+#include "gfxConfig.h"
+#include "nsIGfxInfo.h"
+#include "mozilla/Components.h"
+#include "mozilla/ClearOnShutdown.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dlfcn.h>
+#include <mutex>
+#include <unistd.h>
+#include "gbm.h"
+
+using namespace mozilla::gfx;
+
+namespace mozilla {
+namespace widget {
+
+bool sUseWebGLDmabufBackend = true;
+
+#define GBMLIB_NAME "libgbm.so.1"
+#define DRMLIB_NAME "libdrm.so.2"
+
+// Use static lock to protect dri operation as
+// gbm_dri.c is not thread safe.
+// https://gitlab.freedesktop.org/mesa/mesa/-/issues/4422
+mozilla::StaticMutex GbmLib::sDRILock MOZ_UNANNOTATED;
+
+bool GbmLib::sLoaded = false;
+void* GbmLib::sGbmLibHandle = nullptr;
+void* GbmLib::sXf86DrmLibHandle = nullptr;
+CreateDeviceFunc GbmLib::sCreateDevice;
+DestroyDeviceFunc GbmLib::sDestroyDevice;
+CreateFunc GbmLib::sCreate;
+CreateWithModifiersFunc GbmLib::sCreateWithModifiers;
+GetModifierFunc GbmLib::sGetModifier;
+GetStrideFunc GbmLib::sGetStride;
+GetFdFunc GbmLib::sGetFd;
+DestroyFunc GbmLib::sDestroy;
+MapFunc GbmLib::sMap;
+UnmapFunc GbmLib::sUnmap;
+GetPlaneCountFunc GbmLib::sGetPlaneCount;
+GetHandleForPlaneFunc GbmLib::sGetHandleForPlane;
+GetStrideForPlaneFunc GbmLib::sGetStrideForPlane;
+GetOffsetFunc GbmLib::sGetOffset;
+DeviceIsFormatSupportedFunc GbmLib::sDeviceIsFormatSupported;
+DrmPrimeHandleToFDFunc GbmLib::sDrmPrimeHandleToFD;
+CreateSurfaceFunc GbmLib::sCreateSurface;
+DestroySurfaceFunc GbmLib::sDestroySurface;
+
+bool GbmLib::IsLoaded() {
+ return sCreateDevice != nullptr && sDestroyDevice != nullptr &&
+ sCreate != nullptr && sCreateWithModifiers != nullptr &&
+ sGetModifier != nullptr && sGetStride != nullptr &&
+ sGetFd != nullptr && sDestroy != nullptr && sMap != nullptr &&
+ sUnmap != nullptr && sGetPlaneCount != nullptr &&
+ sGetHandleForPlane != nullptr && sGetStrideForPlane != nullptr &&
+ sGetOffset != nullptr && sDeviceIsFormatSupported != nullptr &&
+ sDrmPrimeHandleToFD != nullptr && sCreateSurface != nullptr &&
+ sDestroySurface != nullptr;
+}
+
+bool GbmLib::Load() {
+ static bool sTriedToLoad = false;
+ if (sTriedToLoad) {
+ return sLoaded;
+ }
+
+ sTriedToLoad = true;
+
+ MOZ_ASSERT(!sGbmLibHandle);
+ MOZ_ASSERT(!sLoaded);
+
+ LOGDMABUF(("Loading DMABuf system library %s ...\n", GBMLIB_NAME));
+
+ sGbmLibHandle = dlopen(GBMLIB_NAME, RTLD_LAZY | RTLD_LOCAL);
+ if (!sGbmLibHandle) {
+ LOGDMABUF(("Failed to load %s, dmabuf isn't available.\n", GBMLIB_NAME));
+ return false;
+ }
+
+ sCreateDevice = (CreateDeviceFunc)dlsym(sGbmLibHandle, "gbm_create_device");
+ sDestroyDevice =
+ (DestroyDeviceFunc)dlsym(sGbmLibHandle, "gbm_device_destroy");
+ sCreate = (CreateFunc)dlsym(sGbmLibHandle, "gbm_bo_create");
+ sCreateWithModifiers = (CreateWithModifiersFunc)dlsym(
+ sGbmLibHandle, "gbm_bo_create_with_modifiers");
+ sGetModifier = (GetModifierFunc)dlsym(sGbmLibHandle, "gbm_bo_get_modifier");
+ sGetStride = (GetStrideFunc)dlsym(sGbmLibHandle, "gbm_bo_get_stride");
+ sGetFd = (GetFdFunc)dlsym(sGbmLibHandle, "gbm_bo_get_fd");
+ sDestroy = (DestroyFunc)dlsym(sGbmLibHandle, "gbm_bo_destroy");
+ sMap = (MapFunc)dlsym(sGbmLibHandle, "gbm_bo_map");
+ sUnmap = (UnmapFunc)dlsym(sGbmLibHandle, "gbm_bo_unmap");
+ sGetPlaneCount =
+ (GetPlaneCountFunc)dlsym(sGbmLibHandle, "gbm_bo_get_plane_count");
+ sGetHandleForPlane = (GetHandleForPlaneFunc)dlsym(
+ sGbmLibHandle, "gbm_bo_get_handle_for_plane");
+ sGetStrideForPlane = (GetStrideForPlaneFunc)dlsym(
+ sGbmLibHandle, "gbm_bo_get_stride_for_plane");
+ sGetOffset = (GetOffsetFunc)dlsym(sGbmLibHandle, "gbm_bo_get_offset");
+ sDeviceIsFormatSupported = (DeviceIsFormatSupportedFunc)dlsym(
+ sGbmLibHandle, "gbm_device_is_format_supported");
+ sCreateSurface =
+ (CreateSurfaceFunc)dlsym(sGbmLibHandle, "gbm_surface_create");
+ sDestroySurface =
+ (DestroySurfaceFunc)dlsym(sGbmLibHandle, "gbm_surface_destroy");
+
+ sXf86DrmLibHandle = dlopen(DRMLIB_NAME, RTLD_LAZY | RTLD_LOCAL);
+ if (!sXf86DrmLibHandle) {
+ LOGDMABUF(("Failed to load %s, dmabuf isn't available.\n", DRMLIB_NAME));
+ return false;
+ }
+ sDrmPrimeHandleToFD =
+ (DrmPrimeHandleToFDFunc)dlsym(sXf86DrmLibHandle, "drmPrimeHandleToFD");
+ sLoaded = IsLoaded();
+ if (!sLoaded) {
+ LOGDMABUF(("Failed to load all symbols from %s\n", GBMLIB_NAME));
+ }
+ return sLoaded;
+}
+
+int DMABufDevice::GetDmabufFD(uint32_t aGEMHandle) {
+ int fd;
+ return GbmLib::DrmPrimeHandleToFD(mDRMFd, aGEMHandle, 0, &fd) < 0 ? -1 : fd;
+}
+
+gbm_device* DMABufDevice::GetGbmDevice() {
+ std::call_once(mFlagGbmDevice, [&] {
+ mGbmDevice = (mDRMFd != -1) ? GbmLib::CreateDevice(mDRMFd) : nullptr;
+ });
+ return mGbmDevice;
+}
+
+int DMABufDevice::OpenDRMFd() { return open(mDrmRenderNode.get(), O_RDWR); }
+
+bool DMABufDevice::IsEnabled(nsACString& aFailureId) {
+ if (mDRMFd == -1) {
+ aFailureId = mFailureId;
+ }
+ return mDRMFd != -1;
+}
+
+DMABufDevice::DMABufDevice()
+ : mXRGBFormat({true, false, GBM_FORMAT_XRGB8888, {}}),
+ mARGBFormat({true, true, GBM_FORMAT_ARGB8888, {}}) {
+ Configure();
+}
+
+DMABufDevice::~DMABufDevice() {
+ if (mGbmDevice) {
+ GbmLib::DestroyDevice(mGbmDevice);
+ mGbmDevice = nullptr;
+ }
+ if (mDRMFd != -1) {
+ close(mDRMFd);
+ mDRMFd = -1;
+ }
+}
+
+void DMABufDevice::Configure() {
+ LOGDMABUF(("DMABufDevice::Configure()"));
+
+ if (!GbmLib::IsAvailable()) {
+ LOGDMABUF(("GbmLib is not available!"));
+ mFailureId = "FEATURE_FAILURE_NO_LIBGBM";
+ return;
+ }
+
+ mDrmRenderNode = nsAutoCString(getenv("MOZ_DRM_DEVICE"));
+ if (mDrmRenderNode.IsEmpty()) {
+ mDrmRenderNode.Assign(gfx::gfxVars::DrmRenderDevice());
+ }
+ if (mDrmRenderNode.IsEmpty()) {
+ LOGDMABUF(("We're missing DRM render device!\n"));
+ mFailureId = "FEATURE_FAILURE_NO_DRM_DEVICE";
+ return;
+ }
+
+ LOGDMABUF(("Using DRM device %s", mDrmRenderNode.get()));
+ mDRMFd = open(mDrmRenderNode.get(), O_RDWR);
+ if (mDRMFd < 0) {
+ LOGDMABUF(("Failed to open drm render node %s error %s\n",
+ mDrmRenderNode.get(), strerror(errno)));
+ mFailureId = "FEATURE_FAILURE_NO_DRM_DEVICE";
+ return;
+ }
+
+#ifdef MOZ_WAYLAND
+ LoadFormatModifiers();
+#endif
+
+ LOGDMABUF(("DMABuf is enabled"));
+}
+
+#ifdef NIGHTLY_BUILD
+bool DMABufDevice::IsDMABufTexturesEnabled() {
+ return gfx::gfxVars::UseDMABuf() &&
+ StaticPrefs::widget_dmabuf_textures_enabled();
+}
+#else
+bool DMABufDevice::IsDMABufTexturesEnabled() { return false; }
+#endif
+bool DMABufDevice::IsDMABufWebGLEnabled() {
+ LOGDMABUF(
+ ("DMABufDevice::IsDMABufWebGLEnabled: UseDMABuf %d "
+ "sUseWebGLDmabufBackend %d "
+ "widget_dmabuf_webgl_enabled %d\n",
+ gfx::gfxVars::UseDMABuf(), sUseWebGLDmabufBackend,
+ StaticPrefs::widget_dmabuf_webgl_enabled()));
+ return gfx::gfxVars::UseDMABuf() && sUseWebGLDmabufBackend &&
+ StaticPrefs::widget_dmabuf_webgl_enabled();
+}
+
+#ifdef MOZ_WAYLAND
+void DMABufDevice::SetModifiersToGfxVars() {
+ gfxVars::SetDMABufModifiersXRGB(mXRGBFormat.mModifiers);
+ gfxVars::SetDMABufModifiersARGB(mARGBFormat.mModifiers);
+}
+
+void DMABufDevice::GetModifiersFromGfxVars() {
+ mXRGBFormat.mModifiers = gfxVars::DMABufModifiersXRGB().Clone();
+ mARGBFormat.mModifiers = gfxVars::DMABufModifiersARGB().Clone();
+}
+#endif
+
+void DMABufDevice::DisableDMABufWebGL() { sUseWebGLDmabufBackend = false; }
+
+GbmFormat* DMABufDevice::GetGbmFormat(bool aHasAlpha) {
+ GbmFormat* format = aHasAlpha ? &mARGBFormat : &mXRGBFormat;
+ return format->mIsSupported ? format : nullptr;
+}
+
+#ifdef MOZ_WAYLAND
+void DMABufDevice::AddFormatModifier(bool aHasAlpha, int aFormat,
+ uint32_t mModifierHi,
+ uint32_t mModifierLo) {
+ GbmFormat* format = aHasAlpha ? &mARGBFormat : &mXRGBFormat;
+ format->mIsSupported = true;
+ format->mHasAlpha = aHasAlpha;
+ format->mFormat = aFormat;
+ format->mModifiers.AppendElement(((uint64_t)mModifierHi << 32) | mModifierLo);
+}
+
+static void dmabuf_modifiers(void* data,
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf,
+ uint32_t format, uint32_t modifier_hi,
+ uint32_t modifier_lo) {
+ // skip modifiers marked as invalid
+ if (modifier_hi == (DRM_FORMAT_MOD_INVALID >> 32) &&
+ modifier_lo == (DRM_FORMAT_MOD_INVALID & 0xffffffff)) {
+ return;
+ }
+
+ auto* device = static_cast<DMABufDevice*>(data);
+ switch (format) {
+ case GBM_FORMAT_ARGB8888:
+ device->AddFormatModifier(true, format, modifier_hi, modifier_lo);
+ break;
+ case GBM_FORMAT_XRGB8888:
+ device->AddFormatModifier(false, format, modifier_hi, modifier_lo);
+ break;
+ default:
+ break;
+ }
+}
+
+static void dmabuf_format(void* data,
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf,
+ uint32_t format) {
+ // XXX: deprecated
+}
+
+static const struct zwp_linux_dmabuf_v1_listener dmabuf_listener = {
+ dmabuf_format, dmabuf_modifiers};
+
+static void global_registry_handler(void* data, wl_registry* registry,
+ uint32_t id, const char* interface,
+ uint32_t version) {
+ if (strcmp(interface, "zwp_linux_dmabuf_v1") == 0 && version > 2) {
+ auto* dmabuf = WaylandRegistryBind<zwp_linux_dmabuf_v1>(
+ registry, id, &zwp_linux_dmabuf_v1_interface, 3);
+ LOGDMABUF(("zwp_linux_dmabuf_v1 is available."));
+ zwp_linux_dmabuf_v1_add_listener(dmabuf, &dmabuf_listener, data);
+ } else if (strcmp(interface, "wl_drm") == 0) {
+ LOGDMABUF(("wl_drm is available."));
+ }
+}
+
+static void global_registry_remover(void* data, wl_registry* registry,
+ uint32_t id) {}
+
+static const struct wl_registry_listener registry_listener = {
+ global_registry_handler, global_registry_remover};
+
+void DMABufDevice::LoadFormatModifiers() {
+ if (!GdkIsWaylandDisplay()) {
+ return;
+ }
+ if (XRE_IsParentProcess()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ wl_display* display = WaylandDisplayGetWLDisplay();
+ wl_registry* registry = wl_display_get_registry(display);
+ wl_registry_add_listener(registry, &registry_listener, this);
+ wl_display_roundtrip(display);
+ wl_display_roundtrip(display);
+ wl_registry_destroy(registry);
+ SetModifiersToGfxVars();
+ } else {
+ GetModifiersFromGfxVars();
+ }
+}
+#endif
+
+DMABufDevice* GetDMABufDevice() {
+ static StaticAutoPtr<DMABufDevice> sDmaBufDevice;
+ static std::once_flag onceFlag;
+ std::call_once(onceFlag, [] {
+ sDmaBufDevice = new DMABufDevice();
+ if (NS_IsMainThread()) {
+ ClearOnShutdown(&sDmaBufDevice);
+ } else {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "ClearDmaBufDevice", [] { ClearOnShutdown(&sDmaBufDevice); }));
+ }
+ });
+ return sDmaBufDevice.get();
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/DMABufLibWrapper.h b/widget/gtk/DMABufLibWrapper.h
new file mode 100644
index 0000000000..9723083ad8
--- /dev/null
+++ b/widget/gtk/DMABufLibWrapper.h
@@ -0,0 +1,231 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __MOZ_DMABUF_LIB_WRAPPER_H__
+#define __MOZ_DMABUF_LIB_WRAPPER_H__
+
+#include "mozilla/widget/gbm.h"
+#include "mozilla/StaticMutex.h"
+#include <mutex>
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gDmabufLog;
+# define LOGDMABUF(args) MOZ_LOG(gDmabufLog, mozilla::LogLevel::Debug, args)
+#else
+# define LOGDMABUF(args)
+#endif /* MOZ_LOGGING */
+
+#ifndef DRM_FORMAT_MOD_INVALID
+# define DRM_FORMAT_MOD_INVALID ((1ULL << 56) - 1)
+#endif
+
+namespace mozilla {
+namespace widget {
+
+typedef struct gbm_device* (*CreateDeviceFunc)(int);
+typedef void (*DestroyDeviceFunc)(struct gbm_device*);
+typedef struct gbm_bo* (*CreateFunc)(struct gbm_device*, uint32_t, uint32_t,
+ uint32_t, uint32_t);
+typedef struct gbm_bo* (*CreateWithModifiersFunc)(struct gbm_device*, uint32_t,
+ uint32_t, uint32_t,
+ const uint64_t*,
+ const unsigned int);
+typedef uint64_t (*GetModifierFunc)(struct gbm_bo*);
+typedef uint32_t (*GetStrideFunc)(struct gbm_bo*);
+typedef int (*GetFdFunc)(struct gbm_bo*);
+typedef void (*DestroyFunc)(struct gbm_bo*);
+typedef void* (*MapFunc)(struct gbm_bo*, uint32_t, uint32_t, uint32_t, uint32_t,
+ uint32_t, uint32_t*, void**);
+typedef void (*UnmapFunc)(struct gbm_bo*, void*);
+typedef int (*GetPlaneCountFunc)(struct gbm_bo*);
+typedef union gbm_bo_handle (*GetHandleForPlaneFunc)(struct gbm_bo*, int);
+typedef uint32_t (*GetStrideForPlaneFunc)(struct gbm_bo*, int);
+typedef uint32_t (*GetOffsetFunc)(struct gbm_bo*, int);
+typedef int (*DeviceIsFormatSupportedFunc)(struct gbm_device*, uint32_t,
+ uint32_t);
+typedef int (*DrmPrimeHandleToFDFunc)(int, uint32_t, uint32_t, int*);
+typedef struct gbm_surface* (*CreateSurfaceFunc)(struct gbm_device*, uint32_t,
+ uint32_t, uint32_t, uint32_t);
+typedef void (*DestroySurfaceFunc)(struct gbm_surface*);
+
+class GbmLib {
+ public:
+ static bool IsAvailable() { return sLoaded || Load(); }
+ static bool IsModifierAvailable();
+
+ static struct gbm_device* CreateDevice(int fd) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sCreateDevice(fd);
+ };
+ static void DestroyDevice(struct gbm_device* gdm) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sDestroyDevice(gdm);
+ };
+ static struct gbm_bo* Create(struct gbm_device* gbm, uint32_t width,
+ uint32_t height, uint32_t format,
+ uint32_t flags) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sCreate(gbm, width, height, format, flags);
+ }
+ static void Destroy(struct gbm_bo* bo) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ sDestroy(bo);
+ }
+ static uint32_t GetStride(struct gbm_bo* bo) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sGetStride(bo);
+ }
+ static int GetFd(struct gbm_bo* bo) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sGetFd(bo);
+ }
+ static void* Map(struct gbm_bo* bo, uint32_t x, uint32_t y, uint32_t width,
+ uint32_t height, uint32_t flags, uint32_t* stride,
+ void** map_data) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sMap(bo, x, y, width, height, flags, stride, map_data);
+ }
+ static void Unmap(struct gbm_bo* bo, void* map_data) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ sUnmap(bo, map_data);
+ }
+ static struct gbm_bo* CreateWithModifiers(struct gbm_device* gbm,
+ uint32_t width, uint32_t height,
+ uint32_t format,
+ const uint64_t* modifiers,
+ const unsigned int count) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sCreateWithModifiers(gbm, width, height, format, modifiers, count);
+ }
+ static uint64_t GetModifier(struct gbm_bo* bo) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sGetModifier(bo);
+ }
+ static int GetPlaneCount(struct gbm_bo* bo) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sGetPlaneCount(bo);
+ }
+ static union gbm_bo_handle GetHandleForPlane(struct gbm_bo* bo, int plane) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sGetHandleForPlane(bo, plane);
+ }
+ static uint32_t GetStrideForPlane(struct gbm_bo* bo, int plane) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sGetStrideForPlane(bo, plane);
+ }
+ static uint32_t GetOffset(struct gbm_bo* bo, int plane) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sGetOffset(bo, plane);
+ }
+ static int DeviceIsFormatSupported(struct gbm_device* gbm, uint32_t format,
+ uint32_t usage) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sDeviceIsFormatSupported(gbm, format, usage);
+ }
+ static int DrmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags,
+ int* prime_fd) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sDrmPrimeHandleToFD(fd, handle, flags, prime_fd);
+ }
+ static struct gbm_surface* CreateSurface(struct gbm_device* gbm,
+ uint32_t width, uint32_t height,
+ uint32_t format, uint32_t flags) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sCreateSurface(gbm, width, height, format, flags);
+ }
+ static void DestroySurface(struct gbm_surface* surface) {
+ StaticMutexAutoLock lockDRI(sDRILock);
+ return sDestroySurface(surface);
+ }
+
+ private:
+ static bool Load();
+ static bool IsLoaded();
+
+ static CreateDeviceFunc sCreateDevice;
+ static DestroyDeviceFunc sDestroyDevice;
+ static CreateFunc sCreate;
+ static CreateWithModifiersFunc sCreateWithModifiers;
+ static GetModifierFunc sGetModifier;
+ static GetStrideFunc sGetStride;
+ static GetFdFunc sGetFd;
+ static DestroyFunc sDestroy;
+ static MapFunc sMap;
+ static UnmapFunc sUnmap;
+ static GetPlaneCountFunc sGetPlaneCount;
+ static GetHandleForPlaneFunc sGetHandleForPlane;
+ static GetStrideForPlaneFunc sGetStrideForPlane;
+ static GetOffsetFunc sGetOffset;
+ static DeviceIsFormatSupportedFunc sDeviceIsFormatSupported;
+ static DrmPrimeHandleToFDFunc sDrmPrimeHandleToFD;
+ static CreateSurfaceFunc sCreateSurface;
+ static DestroySurfaceFunc sDestroySurface;
+ static bool sLoaded;
+
+ static void* sGbmLibHandle;
+ static void* sXf86DrmLibHandle;
+ static mozilla::StaticMutex sDRILock MOZ_UNANNOTATED;
+};
+
+struct GbmFormat {
+ bool mIsSupported;
+ bool mHasAlpha;
+ int mFormat;
+ nsTArray<uint64_t> mModifiers;
+};
+
+class DMABufDevice {
+ public:
+ DMABufDevice();
+ ~DMABufDevice();
+
+ int OpenDRMFd();
+ gbm_device* GetGbmDevice();
+ int GetDmabufFD(uint32_t aGEMHandle);
+
+ bool IsEnabled(nsACString& aFailureId);
+
+ // Use dmabuf for WebRender general web content
+ static bool IsDMABufTexturesEnabled();
+ // Use dmabuf for WebGL content
+ static bool IsDMABufWebGLEnabled();
+ static void DisableDMABufWebGL();
+
+#ifdef MOZ_WAYLAND
+ void AddFormatModifier(bool aHasAlpha, int aFormat, uint32_t mModifierHi,
+ uint32_t mModifierLo);
+#endif
+ GbmFormat* GetGbmFormat(bool aHasAlpha);
+
+ private:
+ void Configure();
+#ifdef MOZ_WAYLAND
+ void LoadFormatModifiers();
+ void SetModifiersToGfxVars();
+ void GetModifiersFromGfxVars();
+#endif
+
+ private:
+ GbmFormat mXRGBFormat;
+ GbmFormat mARGBFormat;
+
+ int mDRMFd = -1;
+ std::once_flag mFlagGbmDevice;
+ gbm_device* mGbmDevice = nullptr;
+ const char* mFailureId = nullptr;
+ nsAutoCString mDrmRenderNode;
+};
+
+DMABufDevice* GetDMABufDevice();
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // __MOZ_DMABUF_LIB_WRAPPER_H__
diff --git a/widget/gtk/DMABufSurface.cpp b/widget/gtk/DMABufSurface.cpp
new file mode 100644
index 0000000000..b8ec593e30
--- /dev/null
+++ b/widget/gtk/DMABufSurface.cpp
@@ -0,0 +1,1729 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "DMABufSurface.h"
+#include "DMABufLibWrapper.h"
+
+#ifdef MOZ_WAYLAND
+# include "nsWaylandDisplay.h"
+#endif
+
+#include <fcntl.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <dlfcn.h>
+#include <sys/mman.h>
+#ifdef HAVE_EVENTFD
+# include <sys/eventfd.h>
+#endif
+#include <poll.h>
+#ifdef HAVE_SYSIOCCOM_H
+# include <sys/ioccom.h>
+#endif
+#include <sys/ioctl.h>
+
+#include "mozilla/widget/gbm.h"
+#include "mozilla/widget/va_drmcommon.h"
+#include "YCbCrUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "GLContextTypes.h" // for GLContext, etc
+#include "GLContextEGL.h"
+#include "GLContextProvider.h"
+#include "ScopedGLHelpers.h"
+#include "GLBlitHelper.h"
+#include "GLReadTexImageHelper.h"
+#include "nsGtkUtils.h"
+
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/ScopeExit.h"
+
+/*
+TODO:
+DRM device selection:
+https://lists.freedesktop.org/archives/wayland-devel/2018-November/039660.html
+*/
+
+/* C++ / C typecast macros for special EGL handle values */
+#if defined(__cplusplus)
+# define EGL_CAST(type, value) (static_cast<type>(value))
+#else
+# define EGL_CAST(type, value) ((type)(value))
+#endif
+
+using namespace mozilla;
+using namespace mozilla::widget;
+using namespace mozilla::gl;
+using namespace mozilla::layers;
+using namespace mozilla::gfx;
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+static LazyLogModule gDmabufRefLog("DmabufRef");
+# define LOGDMABUFREF(args) \
+ MOZ_LOG(gDmabufRefLog, mozilla::LogLevel::Debug, args)
+#else
+# define LOGDMABUFREF(args)
+#endif /* MOZ_LOGGING */
+
+#define BUFFER_FLAGS 0
+
+static RefPtr<GLContext> sSnapshotContext;
+static StaticMutex sSnapshotContextMutex MOZ_UNANNOTATED;
+static Atomic<int> gNewSurfaceUID(1);
+
+RefPtr<GLContext> ClaimSnapshotGLContext() {
+ if (!sSnapshotContext) {
+ nsCString discardFailureId;
+ sSnapshotContext = GLContextProvider::CreateHeadless({}, &discardFailureId);
+ if (!sSnapshotContext) {
+ LOGDMABUF(
+ ("ClaimSnapshotGLContext: Failed to create snapshot GLContext."));
+ return nullptr;
+ }
+ sSnapshotContext->mOwningThreadId = Nothing(); // No singular owner.
+ }
+ if (!sSnapshotContext->MakeCurrent()) {
+ LOGDMABUF(("ClaimSnapshotGLContext: Failed to make GLContext current."));
+ return nullptr;
+ }
+ return sSnapshotContext;
+}
+
+void ReturnSnapshotGLContext(RefPtr<GLContext> aGLContext) {
+ // direct eglMakeCurrent() call breaks current context caching so make sure
+ // it's not used.
+ MOZ_ASSERT(!aGLContext->mUseTLSIsCurrent);
+ if (!aGLContext->IsCurrent()) {
+ LOGDMABUF(("ReturnSnapshotGLContext() failed, is not current!"));
+ return;
+ }
+ const auto& gle = gl::GLContextEGL::Cast(aGLContext);
+ const auto& egl = gle->mEgl;
+ egl->fMakeCurrent(EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+}
+
+bool DMABufSurface::IsGlobalRefSet() const {
+ if (!mGlobalRefCountFd) {
+ return false;
+ }
+ struct pollfd pfd;
+ pfd.fd = mGlobalRefCountFd;
+ pfd.events = POLLIN;
+ return poll(&pfd, 1, 0) == 1;
+}
+
+void DMABufSurface::GlobalRefRelease() {
+#ifdef HAVE_EVENTFD
+ if (!mGlobalRefCountFd) {
+ return;
+ }
+ LOGDMABUFREF(("DMABufSurface::GlobalRefRelease UID %d", mUID));
+ uint64_t counter;
+ if (read(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) {
+ if (errno == EAGAIN) {
+ LOGDMABUFREF(
+ (" GlobalRefRelease failed: already zero reference! UID %d", mUID));
+ }
+ // EAGAIN means the refcount is already zero. It happens when we release
+ // last reference to the surface.
+ if (errno != EAGAIN) {
+ NS_WARNING(nsPrintfCString("Failed to unref dmabuf global ref count: %s",
+ strerror(errno))
+ .get());
+ }
+ }
+#endif
+}
+
+void DMABufSurface::GlobalRefAdd() {
+#ifdef HAVE_EVENTFD
+ LOGDMABUFREF(("DMABufSurface::GlobalRefAdd UID %d", mUID));
+ MOZ_DIAGNOSTIC_ASSERT(mGlobalRefCountFd);
+ uint64_t counter = 1;
+ if (write(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) {
+ NS_WARNING(nsPrintfCString("Failed to ref dmabuf global ref count: %s",
+ strerror(errno))
+ .get());
+ }
+#endif
+}
+
+void DMABufSurface::GlobalRefCountCreate() {
+#ifdef HAVE_EVENTFD
+ LOGDMABUFREF(("DMABufSurface::GlobalRefCountCreate UID %d", mUID));
+ MOZ_DIAGNOSTIC_ASSERT(!mGlobalRefCountFd);
+ // Create global ref count initialized to 0,
+ // i.e. is not referenced after create.
+ mGlobalRefCountFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | EFD_SEMAPHORE);
+ if (mGlobalRefCountFd < 0) {
+ NS_WARNING(nsPrintfCString("Failed to create dmabuf global ref count: %s",
+ strerror(errno))
+ .get());
+ mGlobalRefCountFd = 0;
+ return;
+ }
+#endif
+}
+
+void DMABufSurface::GlobalRefCountImport(int aFd) {
+#ifdef HAVE_EVENTFD
+ mGlobalRefCountFd = aFd;
+ if (mGlobalRefCountFd) {
+ LOGDMABUFREF(("DMABufSurface::GlobalRefCountImport UID %d", mUID));
+ GlobalRefAdd();
+ }
+#endif
+}
+
+int DMABufSurface::GlobalRefCountExport() {
+#ifdef MOZ_LOGGING
+ if (mGlobalRefCountFd) {
+ LOGDMABUFREF(("DMABufSurface::GlobalRefCountExport UID %d", mUID));
+ }
+#endif
+ return mGlobalRefCountFd;
+}
+
+void DMABufSurface::GlobalRefCountDelete() {
+ if (mGlobalRefCountFd) {
+ LOGDMABUFREF(("DMABufSurface::GlobalRefCountDelete UID %d", mUID));
+ close(mGlobalRefCountFd);
+ mGlobalRefCountFd = 0;
+ }
+}
+
+void DMABufSurface::ReleaseDMABuf() {
+ LOGDMABUF(("DMABufSurface::ReleaseDMABuf() UID %d", mUID));
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ Unmap(i);
+ }
+
+ MutexAutoLock lockFD(mSurfaceLock);
+ CloseFileDescriptors(lockFD, /* aForceClose */ true);
+
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ if (mGbmBufferObject[i]) {
+ GbmLib::Destroy(mGbmBufferObject[i]);
+ mGbmBufferObject[i] = nullptr;
+ }
+ }
+ mBufferPlaneCount = 0;
+}
+
+DMABufSurface::DMABufSurface(SurfaceType aSurfaceType)
+ : mSurfaceType(aSurfaceType),
+ mBufferPlaneCount(0),
+ mDrmFormats(),
+ mStrides(),
+ mOffsets(),
+ mGbmBufferObject(),
+ mMappedRegion(),
+ mMappedRegionStride(),
+ mSyncFd(-1),
+ mSync(nullptr),
+ mGlobalRefCountFd(0),
+ mUID(gNewSurfaceUID++),
+ mSurfaceLock("DMABufSurface") {
+ for (auto& slot : mDmabufFds) {
+ slot = -1;
+ }
+ for (auto& modifier : mBufferModifiers) {
+ modifier = DRM_FORMAT_MOD_INVALID;
+ }
+}
+
+DMABufSurface::~DMABufSurface() {
+ FenceDelete();
+ GlobalRefRelease();
+ GlobalRefCountDelete();
+}
+
+already_AddRefed<DMABufSurface> DMABufSurface::CreateDMABufSurface(
+ const mozilla::layers::SurfaceDescriptor& aDesc) {
+ const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf();
+ RefPtr<DMABufSurface> surf;
+
+ switch (desc.bufferType()) {
+ case SURFACE_RGBA:
+ surf = new DMABufSurfaceRGBA();
+ break;
+ case SURFACE_NV12:
+ case SURFACE_YUV420:
+ surf = new DMABufSurfaceYUV();
+ break;
+ default:
+ return nullptr;
+ }
+
+ if (!surf->Create(desc)) {
+ return nullptr;
+ }
+ return surf.forget();
+}
+
+void DMABufSurface::FenceDelete() {
+ if (mSyncFd > 0) {
+ close(mSyncFd);
+ mSyncFd = -1;
+ }
+
+ if (!mGL) {
+ return;
+ }
+ const auto& gle = gl::GLContextEGL::Cast(mGL);
+ const auto& egl = gle->mEgl;
+
+ if (mSync) {
+ egl->fDestroySync(mSync);
+ mSync = nullptr;
+ }
+}
+
+void DMABufSurface::FenceSet() {
+ if (!mGL || !mGL->MakeCurrent()) {
+ MOZ_DIAGNOSTIC_ASSERT(mGL,
+ "DMABufSurface::FenceSet(): missing GL context!");
+ return;
+ }
+ const auto& gle = gl::GLContextEGL::Cast(mGL);
+ const auto& egl = gle->mEgl;
+
+ if (egl->IsExtensionSupported(EGLExtension::KHR_fence_sync) &&
+ egl->IsExtensionSupported(EGLExtension::ANDROID_native_fence_sync)) {
+ FenceDelete();
+
+ mSync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr);
+ if (mSync) {
+ mSyncFd = egl->fDupNativeFenceFDANDROID(mSync);
+ mGL->fFlush();
+ return;
+ }
+ }
+
+ // ANDROID_native_fence_sync may not be supported so call glFinish()
+ // as a slow path.
+ mGL->fFinish();
+}
+
+void DMABufSurface::FenceWait() {
+ if (!mGL || mSyncFd < 0) {
+ MOZ_DIAGNOSTIC_ASSERT(mGL,
+ "DMABufSurface::FenceWait() missing GL context!");
+ return;
+ }
+
+ const auto& gle = gl::GLContextEGL::Cast(mGL);
+ const auto& egl = gle->mEgl;
+
+ const EGLint attribs[] = {LOCAL_EGL_SYNC_NATIVE_FENCE_FD_ANDROID, mSyncFd,
+ LOCAL_EGL_NONE};
+ EGLSync sync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
+ if (!sync) {
+ MOZ_ASSERT(false, "DMABufSurface::FenceWait(): Failed to create GLFence!");
+ // We failed to create GLFence so clear mSyncFd to avoid another try.
+ close(mSyncFd);
+ mSyncFd = -1;
+ return;
+ }
+
+ // mSyncFd is owned by GLFence so clear local reference to avoid double close
+ // at DMABufSurface::FenceDelete().
+ mSyncFd = -1;
+
+ egl->fClientWaitSync(sync, 0, LOCAL_EGL_FOREVER);
+ egl->fDestroySync(sync);
+}
+
+bool DMABufSurface::OpenFileDescriptors(const MutexAutoLock& aProofOfLock) {
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ if (!OpenFileDescriptorForPlane(aProofOfLock, i)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// We can safely close DMABuf file descriptors only when we have a valid
+// GbmBufferObject. When we don't have a valid GbmBufferObject and a DMABuf
+// file descriptor is closed, whole surface is released.
+void DMABufSurface::CloseFileDescriptors(const MutexAutoLock& aProofOfLock,
+ bool aForceClose) {
+ for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) {
+ CloseFileDescriptorForPlane(aProofOfLock, i, aForceClose);
+ }
+}
+
+DMABufSurfaceRGBA::DMABufSurfaceRGBA()
+ : DMABufSurface(SURFACE_RGBA),
+ mSurfaceFlags(0),
+ mWidth(0),
+ mHeight(0),
+ mGmbFormat(nullptr),
+ mEGLImage(LOCAL_EGL_NO_IMAGE),
+ mTexture(0),
+ mGbmBufferFlags(0) {}
+
+DMABufSurfaceRGBA::~DMABufSurfaceRGBA() {
+#ifdef MOZ_WAYLAND
+ ReleaseWlBuffer();
+#endif
+ ReleaseSurface();
+}
+
+bool DMABufSurfaceRGBA::OpenFileDescriptorForPlane(
+ const MutexAutoLock& aProofOfLock, int aPlane) {
+ if (mDmabufFds[aPlane] >= 0) {
+ return true;
+ }
+ gbm_bo* bo = mGbmBufferObject[0];
+ if (NS_WARN_IF(!bo)) {
+ LOGDMABUF(
+ ("DMABufSurfaceRGBA::OpenFileDescriptorForPlane: Missing "
+ "mGbmBufferObject object!"));
+ return false;
+ }
+
+ if (mBufferPlaneCount == 1) {
+ MOZ_ASSERT(aPlane == 0, "DMABuf: wrong surface plane!");
+ mDmabufFds[0] = GbmLib::GetFd(bo);
+ } else {
+ mDmabufFds[aPlane] = GetDMABufDevice()->GetDmabufFD(
+ GbmLib::GetHandleForPlane(bo, aPlane).u32);
+ }
+
+ if (mDmabufFds[aPlane] < 0) {
+ CloseFileDescriptors(aProofOfLock);
+ return false;
+ }
+
+ return true;
+}
+
+void DMABufSurfaceRGBA::CloseFileDescriptorForPlane(
+ const MutexAutoLock& aProofOfLock, int aPlane, bool aForceClose = false) {
+ if ((aForceClose || mGbmBufferObject[0]) && mDmabufFds[aPlane] >= 0) {
+ close(mDmabufFds[aPlane]);
+ mDmabufFds[aPlane] = -1;
+ }
+}
+
+bool DMABufSurfaceRGBA::Create(int aWidth, int aHeight,
+ int aDMABufSurfaceFlags) {
+ MOZ_ASSERT(mGbmBufferObject[0] == nullptr, "Already created?");
+
+ mSurfaceFlags = aDMABufSurfaceFlags;
+ mWidth = aWidth;
+ mHeight = aHeight;
+
+ LOGDMABUF(("DMABufSurfaceRGBA::Create() UID %d size %d x %d\n", mUID, mWidth,
+ mHeight));
+
+ if (!GetDMABufDevice()->GetGbmDevice()) {
+ LOGDMABUF((" Missing GbmDevice!"));
+ return false;
+ }
+
+ mGmbFormat = GetDMABufDevice()->GetGbmFormat(mSurfaceFlags & DMABUF_ALPHA);
+ if (!mGmbFormat) {
+ // Requested DRM format is not supported.
+ return false;
+ }
+ mDrmFormats[0] = mGmbFormat->mFormat;
+
+ bool useModifiers = (aDMABufSurfaceFlags & DMABUF_USE_MODIFIERS) &&
+ !mGmbFormat->mModifiers.IsEmpty();
+ if (useModifiers) {
+ LOGDMABUF((" Creating with modifiers\n"));
+ mGbmBufferObject[0] = GbmLib::CreateWithModifiers(
+ GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight, mDrmFormats[0],
+ mGmbFormat->mModifiers.Elements(), mGmbFormat->mModifiers.Length());
+ if (mGbmBufferObject[0]) {
+ mBufferModifiers[0] = GbmLib::GetModifier(mGbmBufferObject[0]);
+ }
+ }
+
+ if (!mGbmBufferObject[0]) {
+ LOGDMABUF((" Creating without modifiers\n"));
+ mGbmBufferFlags = GBM_BO_USE_LINEAR;
+ mGbmBufferObject[0] =
+ GbmLib::Create(GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight,
+ mDrmFormats[0], mGbmBufferFlags);
+ mBufferModifiers[0] = DRM_FORMAT_MOD_INVALID;
+ }
+
+ if (!mGbmBufferObject[0]) {
+ LOGDMABUF((" Failed to create GbmBufferObject\n"));
+ return false;
+ }
+
+ if (mBufferModifiers[0] != DRM_FORMAT_MOD_INVALID) {
+ mBufferPlaneCount = GbmLib::GetPlaneCount(mGbmBufferObject[0]);
+ if (mBufferPlaneCount > DMABUF_BUFFER_PLANES) {
+ LOGDMABUF(
+ (" There's too many dmabuf planes! (%d)", mBufferPlaneCount));
+ mBufferPlaneCount = DMABUF_BUFFER_PLANES;
+ return false;
+ }
+
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ mStrides[i] = GbmLib::GetStrideForPlane(mGbmBufferObject[0], i);
+ mOffsets[i] = GbmLib::GetOffset(mGbmBufferObject[0], i);
+ }
+ } else {
+ mBufferPlaneCount = 1;
+ mStrides[0] = GbmLib::GetStride(mGbmBufferObject[0]);
+ }
+
+ LOGDMABUF((" Success\n"));
+ return true;
+}
+
+bool DMABufSurfaceRGBA::Create(mozilla::gl::GLContext* aGLContext,
+ const EGLImageKHR aEGLImage, int aWidth,
+ int aHeight) {
+ LOGDMABUF(("DMABufSurfaceRGBA::Create() from EGLImage UID = %d\n", mUID));
+ if (!aGLContext) {
+ return false;
+ }
+ const auto& gle = gl::GLContextEGL::Cast(aGLContext);
+ const auto& egl = gle->mEgl;
+
+ mGL = aGLContext;
+ mWidth = aWidth;
+ mHeight = aHeight;
+ mEGLImage = aEGLImage;
+ if (!egl->fExportDMABUFImageQuery(mEGLImage, mDrmFormats, &mBufferPlaneCount,
+ mBufferModifiers)) {
+ LOGDMABUF((" ExportDMABUFImageQueryMESA failed, quit\n"));
+ return false;
+ }
+ if (mBufferPlaneCount > DMABUF_BUFFER_PLANES) {
+ LOGDMABUF((" wrong plane count %d, quit\n", mBufferPlaneCount));
+ mBufferPlaneCount = DMABUF_BUFFER_PLANES;
+ return false;
+ }
+ if (!egl->fExportDMABUFImage(mEGLImage, mDmabufFds, mStrides, mOffsets)) {
+ LOGDMABUF((" ExportDMABUFImageMESA failed, quit\n"));
+ return false;
+ }
+
+ // A broken driver can return dmabuf without valid file descriptors
+ // which leads to fails later so quit now.
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ if (mDmabufFds[i] < 0) {
+ LOGDMABUF(
+ (" ExportDMABUFImageMESA failed, mDmabufFds[%d] is invalid, quit",
+ i));
+ return false;
+ }
+ }
+
+ LOGDMABUF((" imported size %d x %d format %x planes %d modifiers %" PRIx64,
+ mWidth, mHeight, mDrmFormats[0], mBufferPlaneCount,
+ mBufferModifiers[0]));
+ return true;
+}
+
+bool DMABufSurfaceRGBA::ImportSurfaceDescriptor(
+ const SurfaceDescriptor& aDesc) {
+ const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf();
+
+ mWidth = desc.width()[0];
+ mHeight = desc.height()[0];
+ mBufferModifiers[0] = desc.modifier()[0];
+ mDrmFormats[0] = desc.format()[0];
+ mBufferPlaneCount = desc.fds().Length();
+ mGbmBufferFlags = desc.flags();
+ MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES);
+ mUID = desc.uid();
+
+ LOGDMABUF(
+ ("DMABufSurfaceRGBA::ImportSurfaceDescriptor() UID %d size %d x %d\n",
+ mUID, mWidth, mHeight));
+
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ mDmabufFds[i] = desc.fds()[i].ClonePlatformHandle().release();
+ if (mDmabufFds[i] < 0) {
+ LOGDMABUF(
+ (" failed to get DMABuf file descriptor: %s", strerror(errno)));
+ return false;
+ }
+ mStrides[i] = desc.strides()[i];
+ mOffsets[i] = desc.offsets()[i];
+ }
+
+ if (desc.fence().Length() > 0) {
+ mSyncFd = desc.fence()[0].ClonePlatformHandle().release();
+ if (mSyncFd < 0) {
+ LOGDMABUF(
+ (" failed to get GL fence file descriptor: %s", strerror(errno)));
+ return false;
+ }
+ }
+
+ if (desc.refCount().Length() > 0) {
+ GlobalRefCountImport(desc.refCount()[0].ClonePlatformHandle().release());
+ }
+
+ LOGDMABUF((" imported size %d x %d format %x planes %d", mWidth, mHeight,
+ mDrmFormats[0], mBufferPlaneCount));
+ return true;
+}
+
+bool DMABufSurfaceRGBA::Create(const SurfaceDescriptor& aDesc) {
+ return ImportSurfaceDescriptor(aDesc);
+}
+
+bool DMABufSurfaceRGBA::Serialize(
+ mozilla::layers::SurfaceDescriptor& aOutDescriptor) {
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> width;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> height;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> format;
+ AutoTArray<ipc::FileDescriptor, DMABUF_BUFFER_PLANES> fds;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> strides;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> offsets;
+ AutoTArray<uintptr_t, DMABUF_BUFFER_PLANES> images;
+ AutoTArray<uint64_t, DMABUF_BUFFER_PLANES> modifiers;
+ AutoTArray<ipc::FileDescriptor, 1> fenceFDs;
+ AutoTArray<ipc::FileDescriptor, 1> refCountFDs;
+
+ LOGDMABUF(("DMABufSurfaceRGBA::Serialize() UID %d\n", mUID));
+
+ MutexAutoLock lockFD(mSurfaceLock);
+ if (!OpenFileDescriptors(lockFD)) {
+ return false;
+ }
+
+ width.AppendElement(mWidth);
+ height.AppendElement(mHeight);
+ format.AppendElement(mDrmFormats[0]);
+ modifiers.AppendElement(mBufferModifiers[0]);
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i]));
+ strides.AppendElement(mStrides[i]);
+ offsets.AppendElement(mOffsets[i]);
+ }
+
+ CloseFileDescriptors(lockFD);
+
+ if (mSync) {
+ fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd));
+ }
+
+ if (mGlobalRefCountFd) {
+ refCountFDs.AppendElement(ipc::FileDescriptor(GlobalRefCountExport()));
+ }
+
+ aOutDescriptor = SurfaceDescriptorDMABuf(
+ mSurfaceType, modifiers, mGbmBufferFlags, fds, width, height, width,
+ height, format, strides, offsets, GetYUVColorSpace(), mColorRange,
+ fenceFDs, mUID, refCountFDs);
+ return true;
+}
+
+bool DMABufSurfaceRGBA::CreateTexture(GLContext* aGLContext, int aPlane) {
+ LOGDMABUF(("DMABufSurfaceRGBA::CreateTexture() UID %d\n", mUID));
+ MOZ_ASSERT(!mEGLImage && !mTexture, "EGLImage is already created!");
+
+ nsTArray<EGLint> attribs;
+ attribs.AppendElement(LOCAL_EGL_WIDTH);
+ attribs.AppendElement(mWidth);
+ attribs.AppendElement(LOCAL_EGL_HEIGHT);
+ attribs.AppendElement(mHeight);
+ attribs.AppendElement(LOCAL_EGL_LINUX_DRM_FOURCC_EXT);
+ attribs.AppendElement(mDrmFormats[0]);
+#define ADD_PLANE_ATTRIBS(plane_idx) \
+ { \
+ attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_FD_EXT); \
+ attribs.AppendElement(mDmabufFds[plane_idx]); \
+ attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_OFFSET_EXT); \
+ attribs.AppendElement((int)mOffsets[plane_idx]); \
+ attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_PITCH_EXT); \
+ attribs.AppendElement((int)mStrides[plane_idx]); \
+ if (mBufferModifiers[0] != DRM_FORMAT_MOD_INVALID) { \
+ attribs.AppendElement( \
+ LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_LO_EXT); \
+ attribs.AppendElement(mBufferModifiers[0] & 0xFFFFFFFF); \
+ attribs.AppendElement( \
+ LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_HI_EXT); \
+ attribs.AppendElement(mBufferModifiers[0] >> 32); \
+ } \
+ }
+
+ MutexAutoLock lockFD(mSurfaceLock);
+ if (!OpenFileDescriptors(lockFD)) {
+ return false;
+ }
+ ADD_PLANE_ATTRIBS(0);
+ if (mBufferPlaneCount > 1) ADD_PLANE_ATTRIBS(1);
+ if (mBufferPlaneCount > 2) ADD_PLANE_ATTRIBS(2);
+ if (mBufferPlaneCount > 3) ADD_PLANE_ATTRIBS(3);
+#undef ADD_PLANE_ATTRIBS
+ attribs.AppendElement(LOCAL_EGL_NONE);
+
+ if (!aGLContext) return false;
+
+ if (!aGLContext->MakeCurrent()) {
+ LOGDMABUF(
+ ("DMABufSurfaceRGBA::CreateTexture(): failed to make GL context "
+ "current"));
+ return false;
+ }
+
+ const auto& gle = gl::GLContextEGL::Cast(aGLContext);
+ const auto& egl = gle->mEgl;
+ mEGLImage =
+ egl->fCreateImage(LOCAL_EGL_NO_CONTEXT, LOCAL_EGL_LINUX_DMA_BUF_EXT,
+ nullptr, attribs.Elements());
+
+ CloseFileDescriptors(lockFD);
+
+ if (mEGLImage == LOCAL_EGL_NO_IMAGE) {
+ LOGDMABUF(("EGLImageKHR creation failed"));
+ return false;
+ }
+
+ aGLContext->fGenTextures(1, &mTexture);
+ const ScopedBindTexture savedTex(aGLContext, mTexture);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER,
+ LOCAL_GL_LINEAR);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER,
+ LOCAL_GL_LINEAR);
+ aGLContext->fEGLImageTargetTexture2D(LOCAL_GL_TEXTURE_2D, mEGLImage);
+ mGL = aGLContext;
+
+ return true;
+}
+
+void DMABufSurfaceRGBA::ReleaseTextures() {
+ LOGDMABUF(("DMABufSurfaceRGBA::ReleaseTextures() UID %d\n", mUID));
+ FenceDelete();
+
+ if (!mTexture && !mEGLImage) {
+ return;
+ }
+
+ if (!mGL) {
+#ifdef NIGHTLY_BUILD
+ MOZ_DIAGNOSTIC_ASSERT(mGL, "Missing GL context!");
+#else
+ NS_WARNING(
+ "DMABufSurfaceRGBA::ReleaseTextures(): Missing GL context! We're "
+ "leaking textures!");
+ return;
+#endif
+ }
+
+ const auto& gle = gl::GLContextEGL::Cast(mGL);
+ const auto& egl = gle->mEgl;
+
+ if (mTexture && mGL->MakeCurrent()) {
+ mGL->fDeleteTextures(1, &mTexture);
+ mTexture = 0;
+ }
+
+ if (mEGLImage != LOCAL_EGL_NO_IMAGE) {
+ egl->fDestroyImage(mEGLImage);
+ mEGLImage = LOCAL_EGL_NO_IMAGE;
+ }
+ mGL = nullptr;
+}
+
+void DMABufSurfaceRGBA::ReleaseSurface() {
+ MOZ_ASSERT(!IsMapped(), "We can't release mapped buffer!");
+
+ ReleaseTextures();
+ ReleaseDMABuf();
+}
+
+#ifdef MOZ_WAYLAND
+bool DMABufSurfaceRGBA::CreateWlBuffer() {
+ MutexAutoLock lockFD(mSurfaceLock);
+ if (!OpenFileDescriptors(lockFD)) {
+ return false;
+ }
+
+ nsWaylandDisplay* waylandDisplay = widget::WaylandDisplayGet();
+ if (!waylandDisplay->GetDmabuf()) {
+ CloseFileDescriptors(lockFD);
+ return false;
+ }
+
+ struct zwp_linux_buffer_params_v1* params =
+ zwp_linux_dmabuf_v1_create_params(waylandDisplay->GetDmabuf());
+ zwp_linux_buffer_params_v1_add(params, mDmabufFds[0], 0, mOffsets[0],
+ mStrides[0], mBufferModifiers[0] >> 32,
+ mBufferModifiers[0] & 0xffffffff);
+
+ mWlBuffer = zwp_linux_buffer_params_v1_create_immed(
+ params, GetWidth(), GetHeight(), mDrmFormats[0], 0);
+
+ CloseFileDescriptors(lockFD);
+
+ return mWlBuffer != nullptr;
+}
+
+void DMABufSurfaceRGBA::ReleaseWlBuffer() {
+ MozClearPointer(mWlBuffer, wl_buffer_destroy);
+}
+#endif
+
+// We should synchronize DMA Buffer object access from CPU to avoid potential
+// cache incoherency and data loss.
+// See
+// https://01.org/linuxgraphics/gfx-docs/drm/driver-api/dma-buf.html#cpu-access-to-dma-buffer-objects
+struct dma_buf_sync {
+ uint64_t flags;
+};
+#define DMA_BUF_SYNC_READ (1 << 0)
+#define DMA_BUF_SYNC_WRITE (2 << 0)
+#define DMA_BUF_SYNC_START (0 << 2)
+#define DMA_BUF_SYNC_END (1 << 2)
+#define DMA_BUF_BASE 'b'
+#define DMA_BUF_IOCTL_SYNC _IOW(DMA_BUF_BASE, 0, struct dma_buf_sync)
+
+static void SyncDmaBuf(int aFd, uint64_t aFlags) {
+ struct dma_buf_sync sync = {0};
+
+ sync.flags = aFlags | DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE;
+ while (true) {
+ int ret;
+ ret = ioctl(aFd, DMA_BUF_IOCTL_SYNC, &sync);
+ if (ret == -1 && errno == EINTR) {
+ continue;
+ } else if (ret == -1) {
+ LOGDMABUF(
+ ("Failed to synchronize DMA buffer: %s FD %d", strerror(errno), aFd));
+ break;
+ } else {
+ break;
+ }
+ }
+}
+
+void* DMABufSurface::MapInternal(uint32_t aX, uint32_t aY, uint32_t aWidth,
+ uint32_t aHeight, uint32_t* aStride,
+ int aGbmFlags, int aPlane) {
+ NS_ASSERTION(!IsMapped(aPlane), "Already mapped!");
+ if (!mGbmBufferObject[aPlane]) {
+ NS_WARNING("We can't map DMABufSurfaceRGBA without mGbmBufferObject");
+ return nullptr;
+ }
+
+ LOGDMABUF(
+ ("DMABufSurfaceRGBA::MapInternal() UID %d plane %d size %d x %d -> %d x "
+ "%d\n",
+ mUID, aPlane, aX, aY, aWidth, aHeight));
+
+ mMappedRegionStride[aPlane] = 0;
+ mMappedRegionData[aPlane] = nullptr;
+ mMappedRegion[aPlane] =
+ GbmLib::Map(mGbmBufferObject[aPlane], aX, aY, aWidth, aHeight, aGbmFlags,
+ &mMappedRegionStride[aPlane], &mMappedRegionData[aPlane]);
+ if (!mMappedRegion[aPlane]) {
+ LOGDMABUF((" Surface mapping failed: %s", strerror(errno)));
+ return nullptr;
+ }
+ if (aStride) {
+ *aStride = mMappedRegionStride[aPlane];
+ }
+
+ MutexAutoLock lockFD(mSurfaceLock);
+ if (OpenFileDescriptorForPlane(lockFD, aPlane)) {
+ SyncDmaBuf(mDmabufFds[aPlane], DMA_BUF_SYNC_START);
+ CloseFileDescriptorForPlane(lockFD, aPlane);
+ }
+
+ return mMappedRegion[aPlane];
+}
+
+void* DMABufSurfaceRGBA::MapReadOnly(uint32_t aX, uint32_t aY, uint32_t aWidth,
+ uint32_t aHeight, uint32_t* aStride) {
+ return MapInternal(aX, aY, aWidth, aHeight, aStride, GBM_BO_TRANSFER_READ);
+}
+
+void* DMABufSurfaceRGBA::MapReadOnly(uint32_t* aStride) {
+ return MapInternal(0, 0, mWidth, mHeight, aStride, GBM_BO_TRANSFER_READ);
+}
+
+void* DMABufSurfaceRGBA::Map(uint32_t aX, uint32_t aY, uint32_t aWidth,
+ uint32_t aHeight, uint32_t* aStride) {
+ return MapInternal(aX, aY, aWidth, aHeight, aStride,
+ GBM_BO_TRANSFER_READ_WRITE);
+}
+
+void* DMABufSurfaceRGBA::Map(uint32_t* aStride) {
+ return MapInternal(0, 0, mWidth, mHeight, aStride,
+ GBM_BO_TRANSFER_READ_WRITE);
+}
+
+void DMABufSurface::Unmap(int aPlane) {
+ if (mMappedRegion[aPlane]) {
+ LOGDMABUF(("DMABufSurface::Unmap() UID %d plane %d\n", mUID, aPlane));
+ MutexAutoLock lockFD(mSurfaceLock);
+ if (OpenFileDescriptorForPlane(lockFD, aPlane)) {
+ SyncDmaBuf(mDmabufFds[aPlane], DMA_BUF_SYNC_END);
+ CloseFileDescriptorForPlane(lockFD, aPlane);
+ }
+ GbmLib::Unmap(mGbmBufferObject[aPlane], mMappedRegionData[aPlane]);
+ mMappedRegion[aPlane] = nullptr;
+ mMappedRegionData[aPlane] = nullptr;
+ mMappedRegionStride[aPlane] = 0;
+ }
+}
+
+nsresult DMABufSurface::BuildSurfaceDescriptorBuffer(
+ SurfaceDescriptorBuffer& aSdBuffer, Image::BuildSdbFlags aFlags,
+ const std::function<MemoryOrShmem(uint32_t)>& aAllocate) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+#ifdef DEBUG
+void DMABufSurfaceRGBA::DumpToFile(const char* pFile) {
+ uint32_t stride;
+
+ if (!MapReadOnly(&stride)) {
+ return;
+ }
+ cairo_surface_t* surface = nullptr;
+
+ auto unmap = MakeScopeExit([&] {
+ if (surface) {
+ cairo_surface_destroy(surface);
+ }
+ Unmap();
+ });
+
+ surface = cairo_image_surface_create_for_data(
+ (unsigned char*)mMappedRegion[0], CAIRO_FORMAT_ARGB32, mWidth, mHeight,
+ stride);
+ if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) {
+ cairo_surface_write_to_png(surface, pFile);
+ }
+}
+#endif
+
+#if 0
+// Copy from source surface by GL
+# include "GLBlitHelper.h"
+
+bool DMABufSurfaceRGBA::CopyFrom(class DMABufSurface* aSourceSurface,
+ GLContext* aGLContext) {
+ MOZ_ASSERT(aSourceSurface->GetTexture());
+ MOZ_ASSERT(GetTexture());
+
+ gfx::IntSize size(GetWidth(), GetHeight());
+ aGLContext->BlitHelper()->BlitTextureToTexture(aSourceSurface->GetTexture(),
+ GetTexture(), size, size);
+ return true;
+}
+#endif
+
+// TODO - Clear the surface by EGL
+void DMABufSurfaceRGBA::Clear() {
+ uint32_t destStride;
+ void* destData = Map(&destStride);
+ memset(destData, 0, GetHeight() * destStride);
+ Unmap();
+}
+
+bool DMABufSurfaceRGBA::HasAlpha() {
+ return !mGmbFormat || mGmbFormat->mHasAlpha;
+}
+
+gfx::SurfaceFormat DMABufSurfaceRGBA::GetFormat() {
+ return HasAlpha() ? gfx::SurfaceFormat::B8G8R8A8
+ : gfx::SurfaceFormat::B8G8R8X8;
+}
+
+// GL uses swapped R and B components so report accordingly.
+gfx::SurfaceFormat DMABufSurfaceRGBA::GetFormatGL() {
+ return HasAlpha() ? gfx::SurfaceFormat::R8G8B8A8
+ : gfx::SurfaceFormat::R8G8B8X8;
+}
+
+already_AddRefed<DMABufSurfaceRGBA> DMABufSurfaceRGBA::CreateDMABufSurface(
+ int aWidth, int aHeight, int aDMABufSurfaceFlags) {
+ RefPtr<DMABufSurfaceRGBA> surf = new DMABufSurfaceRGBA();
+ if (!surf->Create(aWidth, aHeight, aDMABufSurfaceFlags)) {
+ return nullptr;
+ }
+ return surf.forget();
+}
+
+already_AddRefed<DMABufSurface> DMABufSurfaceRGBA::CreateDMABufSurface(
+ mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage, int aWidth,
+ int aHeight) {
+ RefPtr<DMABufSurfaceRGBA> surf = new DMABufSurfaceRGBA();
+ if (!surf->Create(aGLContext, aEGLImage, aWidth, aHeight)) {
+ return nullptr;
+ }
+ return surf.forget();
+}
+
+already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CreateYUVSurface(
+ const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) {
+ RefPtr<DMABufSurfaceYUV> surf = new DMABufSurfaceYUV();
+ LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurface() UID %d from desc\n",
+ surf->GetUID()));
+ if (!surf->UpdateYUVData(aDesc, aWidth, aHeight, /* aCopy */ false)) {
+ return nullptr;
+ }
+ return surf.forget();
+}
+
+already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CopyYUVSurface(
+ const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) {
+ RefPtr<DMABufSurfaceYUV> surf = new DMABufSurfaceYUV();
+ LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurfaceCopy() UID %d from desc\n",
+ surf->GetUID()));
+ if (!surf->UpdateYUVData(aDesc, aWidth, aHeight, /* aCopy */ true)) {
+ return nullptr;
+ }
+ return surf.forget();
+}
+
+already_AddRefed<DMABufSurfaceYUV> DMABufSurfaceYUV::CreateYUVSurface(
+ int aWidth, int aHeight, void** aPixelData, int* aLineSizes) {
+ RefPtr<DMABufSurfaceYUV> surf = new DMABufSurfaceYUV();
+ LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurface() UID %d %d x %d\n",
+ surf->GetUID(), aWidth, aHeight));
+ if (!surf->Create(aWidth, aHeight, aPixelData, aLineSizes)) {
+ return nullptr;
+ }
+ return surf.forget();
+}
+
+DMABufSurfaceYUV::DMABufSurfaceYUV()
+ : DMABufSurface(SURFACE_NV12),
+ mWidth(),
+ mHeight(),
+ mWidthAligned(),
+ mHeightAligned(),
+ mTexture() {
+ for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) {
+ mEGLImage[i] = LOCAL_EGL_NO_IMAGE;
+ }
+}
+
+DMABufSurfaceYUV::~DMABufSurfaceYUV() { ReleaseSurface(); }
+
+bool DMABufSurfaceYUV::OpenFileDescriptorForPlane(
+ const MutexAutoLock& aProofOfLock, int aPlane) {
+ // The fd is already opened, no need to reopen.
+ // This can happen when we import dmabuf surface from VA-API decoder,
+ // mGbmBufferObject is null and we don't close
+ // file descriptors for surface as they are our only reference to it.
+ if (mDmabufFds[aPlane] >= 0) {
+ return true;
+ }
+
+ if (mGbmBufferObject[aPlane] == nullptr) {
+ LOGDMABUF(
+ ("DMABufSurfaceYUV::OpenFileDescriptorForPlane: Missing "
+ "mGbmBufferObject object!"));
+ return false;
+ }
+
+ mDmabufFds[aPlane] = GbmLib::GetFd(mGbmBufferObject[aPlane]);
+ if (mDmabufFds[aPlane] < 0) {
+ CloseFileDescriptors(aProofOfLock);
+ return false;
+ }
+ return true;
+}
+
+void DMABufSurfaceYUV::CloseFileDescriptorForPlane(
+ const MutexAutoLock& aProofOfLock, int aPlane, bool aForceClose = false) {
+ if ((aForceClose || mGbmBufferObject[aPlane]) && mDmabufFds[aPlane] >= 0) {
+ close(mDmabufFds[aPlane]);
+ mDmabufFds[aPlane] = -1;
+ }
+}
+
+bool DMABufSurfaceYUV::ImportPRIMESurfaceDescriptor(
+ const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) {
+ LOGDMABUF(("DMABufSurfaceYUV::ImportPRIMESurfaceDescriptor() UID %d", mUID));
+ // Already exists?
+ MOZ_DIAGNOSTIC_ASSERT(mDmabufFds[0] < 0);
+
+ if (aDesc.num_layers > DMABUF_BUFFER_PLANES ||
+ aDesc.num_objects > DMABUF_BUFFER_PLANES) {
+ LOGDMABUF((" Can't import, wrong layers/objects number (%d, %d)",
+ aDesc.num_layers, aDesc.num_objects));
+ return false;
+ }
+ if (aDesc.fourcc == VA_FOURCC_NV12) {
+ mSurfaceType = SURFACE_NV12;
+ } else if (aDesc.fourcc == VA_FOURCC_P010) {
+ mSurfaceType = SURFACE_NV12;
+ } else if (aDesc.fourcc == VA_FOURCC_YV12) {
+ mSurfaceType = SURFACE_YUV420;
+ } else {
+ LOGDMABUF((" Can't import surface data of 0x%x format", aDesc.fourcc));
+ return false;
+ }
+
+ mBufferPlaneCount = aDesc.num_layers;
+
+ for (unsigned int i = 0; i < aDesc.num_layers; i++) {
+ // All supported formats have 4:2:0 chroma sub-sampling.
+ unsigned int subsample = i == 0 ? 0 : 1;
+
+ unsigned int object = aDesc.layers[i].object_index[0];
+ mBufferModifiers[i] = aDesc.objects[object].drm_format_modifier;
+ mDrmFormats[i] = aDesc.layers[i].drm_format;
+ mOffsets[i] = aDesc.layers[i].offset[0];
+ mStrides[i] = aDesc.layers[i].pitch[0];
+ mWidthAligned[i] = aDesc.width >> subsample;
+ mHeightAligned[i] = aDesc.height >> subsample;
+ mWidth[i] = aWidth >> subsample;
+ mHeight[i] = aHeight >> subsample;
+ LOGDMABUF((" plane %d size %d x %d format %x", i, mWidth[i], mHeight[i],
+ mDrmFormats[i]));
+ }
+ return true;
+}
+
+bool DMABufSurfaceYUV::MoveYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc,
+ int aWidth, int aHeight) {
+ if (!ImportPRIMESurfaceDescriptor(aDesc, aWidth, aHeight)) {
+ return false;
+ }
+ for (unsigned int i = 0; i < aDesc.num_layers; i++) {
+ unsigned int object = aDesc.layers[i].object_index[0];
+ // Keep VADRMPRIMESurfaceDescriptor untouched and dup() dmabuf
+ // file descriptors.
+ mDmabufFds[i] = dup(aDesc.objects[object].fd);
+ }
+ return true;
+}
+
+void DMABufSurfaceYUV::ReleaseVADRMPRIMESurfaceDescriptor(
+ VADRMPRIMESurfaceDescriptor& aDesc) {
+ for (unsigned int i = 0; i < aDesc.num_layers; i++) {
+ unsigned int object = aDesc.layers[i].object_index[0];
+ if (aDesc.objects[object].fd != -1) {
+ close(aDesc.objects[object].fd);
+ aDesc.objects[object].fd = -1;
+ }
+ }
+}
+
+bool DMABufSurfaceYUV::CreateYUVPlane(int aPlane) {
+ LOGDMABUF(("DMABufSurfaceYUV::CreateYUVPlane() UID %d size %d x %d", mUID,
+ mWidth[aPlane], mHeight[aPlane]));
+
+ if (!GetDMABufDevice()->GetGbmDevice()) {
+ LOGDMABUF((" Missing GbmDevice!"));
+ return false;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mGbmBufferObject[aPlane] == nullptr);
+ bool useModifiers = (mBufferModifiers[aPlane] != DRM_FORMAT_MOD_INVALID);
+ if (useModifiers) {
+ LOGDMABUF((" Creating with modifiers"));
+ mGbmBufferObject[aPlane] = GbmLib::CreateWithModifiers(
+ GetDMABufDevice()->GetGbmDevice(), mWidth[aPlane], mHeight[aPlane],
+ mDrmFormats[aPlane], mBufferModifiers + aPlane, 1);
+ }
+ if (!mGbmBufferObject[aPlane]) {
+ LOGDMABUF((" Creating without modifiers"));
+ mGbmBufferObject[aPlane] = GbmLib::Create(
+ GetDMABufDevice()->GetGbmDevice(), mWidth[aPlane], mHeight[aPlane],
+ mDrmFormats[aPlane], GBM_BO_USE_RENDERING);
+ mBufferModifiers[aPlane] = DRM_FORMAT_MOD_INVALID;
+ }
+ if (!mGbmBufferObject[aPlane]) {
+ LOGDMABUF((" Failed to create GbmBufferObject: %s", strerror(errno)));
+ return false;
+ }
+
+ mStrides[aPlane] = GbmLib::GetStride(mGbmBufferObject[aPlane]);
+ mOffsets[aPlane] = GbmLib::GetOffset(mGbmBufferObject[aPlane], 0);
+ mWidthAligned[aPlane] = mWidth[aPlane];
+ mHeightAligned[aPlane] = mHeight[aPlane];
+ return true;
+}
+
+bool DMABufSurfaceYUV::CopyYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc,
+ int aWidth, int aHeight) {
+ RefPtr<DMABufSurfaceYUV> tmpSurf = CreateYUVSurface(aDesc, aWidth, aHeight);
+ if (!tmpSurf) {
+ return false;
+ }
+
+ if (!ImportPRIMESurfaceDescriptor(aDesc, aWidth, aHeight)) {
+ return false;
+ }
+
+ StaticMutexAutoLock lock(sSnapshotContextMutex);
+ RefPtr<GLContext> context = ClaimSnapshotGLContext();
+ auto releaseTextures = MakeScopeExit([&] {
+ tmpSurf->ReleaseTextures();
+ ReleaseTextures();
+ ReturnSnapshotGLContext(context);
+ });
+
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ if (!tmpSurf->CreateTexture(context, i)) {
+ return false;
+ }
+ if (!CreateYUVPlane(i) || !CreateTexture(context, i)) {
+ return false;
+ }
+ gfx::IntSize size(GetWidth(i), GetHeight(i));
+ context->BlitHelper()->BlitTextureToTexture(
+ tmpSurf->GetTexture(i), GetTexture(i), size, size, LOCAL_GL_TEXTURE_2D,
+ LOCAL_GL_TEXTURE_2D);
+ }
+ return true;
+}
+
+bool DMABufSurfaceYUV::UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc,
+ int aWidth, int aHeight, bool aCopy) {
+ LOGDMABUF(("DMABufSurfaceYUV::UpdateYUVData() UID %d copy %d", mUID, aCopy));
+ return aCopy ? CopyYUVDataImpl(aDesc, aWidth, aHeight)
+ : MoveYUVDataImpl(aDesc, aWidth, aHeight);
+}
+
+bool DMABufSurfaceYUV::CreateLinearYUVPlane(int aPlane, int aWidth, int aHeight,
+ int aDrmFormat) {
+ LOGDMABUF(("DMABufSurfaceYUV::CreateLinearYUVPlane() UID %d size %d x %d",
+ mUID, aWidth, aHeight));
+
+ if (!GetDMABufDevice()->GetGbmDevice()) {
+ LOGDMABUF((" Missing GbmDevice!"));
+ return false;
+ }
+
+ mWidth[aPlane] = aWidth;
+ mHeight[aPlane] = aHeight;
+ mDrmFormats[aPlane] = aDrmFormat;
+
+ mGbmBufferObject[aPlane] =
+ GbmLib::Create(GetDMABufDevice()->GetGbmDevice(), aWidth, aHeight,
+ aDrmFormat, GBM_BO_USE_LINEAR);
+ if (!mGbmBufferObject[aPlane]) {
+ LOGDMABUF((" Failed to create GbmBufferObject: %s", strerror(errno)));
+ return false;
+ }
+
+ mStrides[aPlane] = GbmLib::GetStride(mGbmBufferObject[aPlane]);
+ mDmabufFds[aPlane] = -1;
+
+ return true;
+}
+
+void DMABufSurfaceYUV::UpdateYUVPlane(int aPlane, void* aPixelData,
+ int aLineSize) {
+ LOGDMABUF(
+ ("DMABufSurfaceYUV::UpdateYUVPlane() UID %d plane %d", mUID, aPlane));
+ if (aLineSize == mWidth[aPlane] &&
+ (int)mMappedRegionStride[aPlane] == mWidth[aPlane]) {
+ memcpy(mMappedRegion[aPlane], aPixelData, aLineSize * mHeight[aPlane]);
+ } else {
+ char* src = (char*)aPixelData;
+ char* dest = (char*)mMappedRegion[aPlane];
+ for (int i = 0; i < mHeight[aPlane]; i++) {
+ memcpy(dest, src, mWidth[aPlane]);
+ src += aLineSize;
+ dest += mMappedRegionStride[aPlane];
+ }
+ }
+}
+
+bool DMABufSurfaceYUV::UpdateYUVData(void** aPixelData, int* aLineSizes) {
+ LOGDMABUF(("DMABufSurfaceYUV::UpdateYUVData() UID %d", mUID));
+ if (mSurfaceType != SURFACE_YUV420) {
+ LOGDMABUF((" UpdateYUVData can upload YUV420 surface type only!"));
+ return false;
+ }
+
+ if (mBufferPlaneCount != 3) {
+ LOGDMABUF((" DMABufSurfaceYUV planes does not match!"));
+ return false;
+ }
+
+ auto unmapBuffers = MakeScopeExit([&] {
+ Unmap(0);
+ Unmap(1);
+ Unmap(2);
+ });
+
+ // Map planes
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ MapInternal(0, 0, mWidth[i], mHeight[i], nullptr, GBM_BO_TRANSFER_WRITE, i);
+ if (!mMappedRegion[i]) {
+ LOGDMABUF((" DMABufSurfaceYUV plane can't be mapped!"));
+ return false;
+ }
+ if ((int)mMappedRegionStride[i] < mWidth[i]) {
+ LOGDMABUF((" DMABufSurfaceYUV plane size stride does not match!"));
+ return false;
+ }
+ }
+
+ // Copy planes
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ UpdateYUVPlane(i, aPixelData[i], aLineSizes[i]);
+ }
+
+ return true;
+}
+
+bool DMABufSurfaceYUV::Create(int aWidth, int aHeight, void** aPixelData,
+ int* aLineSizes) {
+ LOGDMABUF(("DMABufSurfaceYUV::Create() UID %d size %d x %d", mUID, aWidth,
+ aHeight));
+
+ mSurfaceType = SURFACE_YUV420;
+ mBufferPlaneCount = 3;
+
+ if (!CreateLinearYUVPlane(0, aWidth, aHeight, GBM_FORMAT_R8)) {
+ return false;
+ }
+ if (!CreateLinearYUVPlane(1, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) {
+ return false;
+ }
+ if (!CreateLinearYUVPlane(2, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) {
+ return false;
+ }
+ if (!aPixelData || !aLineSizes) {
+ return true;
+ }
+ return UpdateYUVData(aPixelData, aLineSizes);
+}
+
+bool DMABufSurfaceYUV::Create(const SurfaceDescriptor& aDesc) {
+ return ImportSurfaceDescriptor(aDesc);
+}
+
+bool DMABufSurfaceYUV::ImportSurfaceDescriptor(
+ const SurfaceDescriptorDMABuf& aDesc) {
+ mBufferPlaneCount = aDesc.fds().Length();
+ mSurfaceType = (mBufferPlaneCount == 2) ? SURFACE_NV12 : SURFACE_YUV420;
+ mColorSpace = aDesc.yUVColorSpace();
+ mColorRange = aDesc.colorRange();
+ mUID = aDesc.uid();
+
+ LOGDMABUF(("DMABufSurfaceYUV::ImportSurfaceDescriptor() UID %d", mUID));
+
+ MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES);
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ mDmabufFds[i] = aDesc.fds()[i].ClonePlatformHandle().release();
+ if (mDmabufFds[i] < 0) {
+ LOGDMABUF((" failed to get DMABuf plane file descriptor: %s",
+ strerror(errno)));
+ return false;
+ }
+ mWidth[i] = aDesc.width()[i];
+ mHeight[i] = aDesc.height()[i];
+ mWidthAligned[i] = aDesc.widthAligned()[i];
+ mHeightAligned[i] = aDesc.heightAligned()[i];
+ mDrmFormats[i] = aDesc.format()[i];
+ mStrides[i] = aDesc.strides()[i];
+ mOffsets[i] = aDesc.offsets()[i];
+ mBufferModifiers[i] = aDesc.modifier()[i];
+ LOGDMABUF((" plane %d fd %d size %d x %d format %x", i, mDmabufFds[i],
+ mWidth[i], mHeight[i], mDrmFormats[i]));
+ }
+
+ if (aDesc.fence().Length() > 0) {
+ mSyncFd = aDesc.fence()[0].ClonePlatformHandle().release();
+ if (mSyncFd < 0) {
+ LOGDMABUF(
+ (" failed to get GL fence file descriptor: %s", strerror(errno)));
+ return false;
+ }
+ }
+
+ if (aDesc.refCount().Length() > 0) {
+ GlobalRefCountImport(aDesc.refCount()[0].ClonePlatformHandle().release());
+ }
+
+ return true;
+}
+
+bool DMABufSurfaceYUV::Serialize(
+ mozilla::layers::SurfaceDescriptor& aOutDescriptor) {
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> width;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> height;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> widthBytes;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> heightBytes;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> format;
+ AutoTArray<ipc::FileDescriptor, DMABUF_BUFFER_PLANES> fds;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> strides;
+ AutoTArray<uint32_t, DMABUF_BUFFER_PLANES> offsets;
+ AutoTArray<uint64_t, DMABUF_BUFFER_PLANES> modifiers;
+ AutoTArray<ipc::FileDescriptor, 1> fenceFDs;
+ AutoTArray<ipc::FileDescriptor, 1> refCountFDs;
+
+ LOGDMABUF(("DMABufSurfaceYUV::Serialize() UID %d", mUID));
+
+ MutexAutoLock lockFD(mSurfaceLock);
+ if (!OpenFileDescriptors(lockFD)) {
+ return false;
+ }
+
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ width.AppendElement(mWidth[i]);
+ height.AppendElement(mHeight[i]);
+ widthBytes.AppendElement(mWidthAligned[i]);
+ heightBytes.AppendElement(mHeightAligned[i]);
+ format.AppendElement(mDrmFormats[i]);
+ fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i]));
+ strides.AppendElement(mStrides[i]);
+ offsets.AppendElement(mOffsets[i]);
+ modifiers.AppendElement(mBufferModifiers[i]);
+ }
+
+ CloseFileDescriptors(lockFD);
+
+ if (mSync) {
+ fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd));
+ }
+
+ if (mGlobalRefCountFd) {
+ refCountFDs.AppendElement(ipc::FileDescriptor(GlobalRefCountExport()));
+ }
+
+ aOutDescriptor = SurfaceDescriptorDMABuf(
+ mSurfaceType, modifiers, 0, fds, width, height, widthBytes, heightBytes,
+ format, strides, offsets, GetYUVColorSpace(), mColorRange, fenceFDs, mUID,
+ refCountFDs);
+ return true;
+}
+
+bool DMABufSurfaceYUV::CreateEGLImage(GLContext* aGLContext, int aPlane) {
+ LOGDMABUF(
+ ("DMABufSurfaceYUV::CreateEGLImage() UID %d plane %d", mUID, aPlane));
+ MOZ_ASSERT(mEGLImage[aPlane] == LOCAL_EGL_NO_IMAGE,
+ "EGLImage is already created!");
+ MOZ_ASSERT(aGLContext, "Missing GLContext!");
+
+ const auto& gle = gl::GLContextEGL::Cast(aGLContext);
+ const auto& egl = gle->mEgl;
+
+ MutexAutoLock lockFD(mSurfaceLock);
+ if (!OpenFileDescriptorForPlane(lockFD, aPlane)) {
+ LOGDMABUF((" failed to open dmabuf file descriptors"));
+ return false;
+ }
+
+ nsTArray<EGLint> attribs;
+ attribs.AppendElement(LOCAL_EGL_WIDTH);
+ attribs.AppendElement(mWidthAligned[aPlane]);
+ attribs.AppendElement(LOCAL_EGL_HEIGHT);
+ attribs.AppendElement(mHeightAligned[aPlane]);
+ attribs.AppendElement(LOCAL_EGL_LINUX_DRM_FOURCC_EXT);
+ attribs.AppendElement(mDrmFormats[aPlane]);
+#define ADD_PLANE_ATTRIBS_NV12(plane_idx) \
+ attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_FD_EXT); \
+ attribs.AppendElement(mDmabufFds[aPlane]); \
+ attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_OFFSET_EXT); \
+ attribs.AppendElement((int)mOffsets[aPlane]); \
+ attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_PITCH_EXT); \
+ attribs.AppendElement((int)mStrides[aPlane]); \
+ if (mBufferModifiers[aPlane] != DRM_FORMAT_MOD_INVALID) { \
+ attribs.AppendElement( \
+ LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_LO_EXT); \
+ attribs.AppendElement(mBufferModifiers[aPlane] & 0xFFFFFFFF); \
+ attribs.AppendElement( \
+ LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_HI_EXT); \
+ attribs.AppendElement(mBufferModifiers[aPlane] >> 32); \
+ }
+ ADD_PLANE_ATTRIBS_NV12(0);
+#undef ADD_PLANE_ATTRIBS_NV12
+ attribs.AppendElement(LOCAL_EGL_NONE);
+
+ mEGLImage[aPlane] =
+ egl->fCreateImage(LOCAL_EGL_NO_CONTEXT, LOCAL_EGL_LINUX_DMA_BUF_EXT,
+ nullptr, attribs.Elements());
+
+ CloseFileDescriptorForPlane(lockFD, aPlane);
+
+ if (mEGLImage[aPlane] == LOCAL_EGL_NO_IMAGE) {
+ LOGDMABUF((" EGLImageKHR creation failed"));
+ return false;
+ }
+
+ LOGDMABUF((" Success."));
+ return true;
+}
+
+void DMABufSurfaceYUV::ReleaseEGLImages(GLContext* aGLContext) {
+ LOGDMABUF(("DMABufSurfaceYUV::ReleaseEGLImages() UID %d", mUID));
+ MOZ_ASSERT(aGLContext, "Missing GLContext!");
+
+ const auto& gle = gl::GLContextEGL::Cast(aGLContext);
+ const auto& egl = gle->mEgl;
+
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ if (mEGLImage[i] != LOCAL_EGL_NO_IMAGE) {
+ egl->fDestroyImage(mEGLImage[i]);
+ mEGLImage[i] = LOCAL_EGL_NO_IMAGE;
+ }
+ }
+}
+
+bool DMABufSurfaceYUV::CreateTexture(GLContext* aGLContext, int aPlane) {
+ LOGDMABUF(
+ ("DMABufSurfaceYUV::CreateTexture() UID %d plane %d", mUID, aPlane));
+ MOZ_ASSERT(!mTexture[aPlane], "Texture is already created!");
+ MOZ_ASSERT(aGLContext, "Missing GLContext!");
+
+ if (!aGLContext->MakeCurrent()) {
+ LOGDMABUF((" Failed to make GL context current."));
+ return false;
+ }
+ if (!CreateEGLImage(aGLContext, aPlane)) {
+ return false;
+ }
+ aGLContext->fGenTextures(1, &mTexture[aPlane]);
+ const ScopedBindTexture savedTex(aGLContext, mTexture[aPlane]);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER,
+ LOCAL_GL_LINEAR);
+ aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER,
+ LOCAL_GL_LINEAR);
+ aGLContext->fEGLImageTargetTexture2D(LOCAL_GL_TEXTURE_2D, mEGLImage[aPlane]);
+ mGL = aGLContext;
+ return true;
+}
+
+void DMABufSurfaceYUV::ReleaseTextures() {
+ LOGDMABUF(("DMABufSurfaceYUV::ReleaseTextures() UID %d", mUID));
+
+ FenceDelete();
+
+ bool textureActive = false;
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ if (mTexture[i] || mEGLImage[i]) {
+ textureActive = true;
+ break;
+ }
+ }
+
+ if (!textureActive) {
+ return;
+ }
+
+ if (!mGL) {
+#ifdef NIGHTLY_BUILD
+ MOZ_DIAGNOSTIC_ASSERT(mGL, "Missing GL context!");
+#else
+ NS_WARNING(
+ "DMABufSurfaceYUV::ReleaseTextures(): Missing GL context! We're "
+ "leaking textures!");
+ return;
+#endif
+ }
+
+ if (!mGL->MakeCurrent()) {
+ NS_WARNING(
+ "DMABufSurfaceYUV::ReleaseTextures(): MakeCurrent failed. We're "
+ "leaking textures!");
+ return;
+ }
+
+ mGL->fDeleteTextures(DMABUF_BUFFER_PLANES, mTexture);
+ for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) {
+ mTexture[i] = 0;
+ }
+ ReleaseEGLImages(mGL);
+ mGL = nullptr;
+}
+
+bool DMABufSurfaceYUV::VerifyTextureCreation() {
+ LOGDMABUF(("DMABufSurfaceYUV::VerifyTextureCreation() UID %d", mUID));
+
+ StaticMutexAutoLock lock(sSnapshotContextMutex);
+ RefPtr<GLContext> context = ClaimSnapshotGLContext();
+ auto release = MakeScopeExit([&] {
+ ReleaseEGLImages(context);
+ ReturnSnapshotGLContext(context);
+ });
+
+ for (int i = 0; i < mBufferPlaneCount; i++) {
+ if (!CreateEGLImage(context, i)) {
+ LOGDMABUF((" failed to create EGL image!"));
+ return false;
+ }
+ }
+
+ LOGDMABUF((" success"));
+ return true;
+}
+
+gfx::SurfaceFormat DMABufSurfaceYUV::GetFormat() {
+ switch (mSurfaceType) {
+ case SURFACE_NV12:
+ return gfx::SurfaceFormat::NV12;
+ case SURFACE_YUV420:
+ return gfx::SurfaceFormat::YUV;
+ default:
+ NS_WARNING("DMABufSurfaceYUV::GetFormat(): Wrong surface format!");
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+}
+
+// GL uses swapped R and B components so report accordingly.
+gfx::SurfaceFormat DMABufSurfaceYUV::GetFormatGL() { return GetFormat(); }
+
+int DMABufSurfaceYUV::GetTextureCount() {
+ switch (mSurfaceType) {
+ case SURFACE_NV12:
+ return 2;
+ case SURFACE_YUV420:
+ return 3;
+ default:
+ NS_WARNING("DMABufSurfaceYUV::GetTextureCount(): Wrong surface format!");
+ return 1;
+ }
+}
+
+void DMABufSurfaceYUV::ReleaseSurface() {
+ LOGDMABUF(("DMABufSurfaceYUV::ReleaseSurface() UID %d", mUID));
+ ReleaseTextures();
+ ReleaseDMABuf();
+}
+
+nsresult DMABufSurfaceYUV::ReadIntoBuffer(uint8_t* aData, int32_t aStride,
+ const gfx::IntSize& aSize,
+ gfx::SurfaceFormat aFormat) {
+ LOGDMABUF(("DMABufSurfaceYUV::ReadIntoBuffer UID %d", mUID));
+
+ MOZ_ASSERT(aSize.width == GetWidth());
+ MOZ_ASSERT(aSize.height == GetHeight());
+
+ StaticMutexAutoLock lock(sSnapshotContextMutex);
+ RefPtr<GLContext> context = ClaimSnapshotGLContext();
+ auto releaseTextures = mozilla::MakeScopeExit([&] {
+ ReleaseTextures();
+ ReturnSnapshotGLContext(context);
+ });
+
+ for (int i = 0; i < GetTextureCount(); i++) {
+ if (!GetTexture(i) && !CreateTexture(context, i)) {
+ LOGDMABUF(("ReadIntoBuffer: Failed to create DMABuf textures."));
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ ScopedTexture scopedTex(context);
+ ScopedBindTexture boundTex(context, scopedTex.Texture());
+
+ context->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA, aSize.width,
+ aSize.height, 0, LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE,
+ nullptr);
+
+ ScopedFramebufferForTexture autoFBForTex(context, scopedTex.Texture());
+ if (!autoFBForTex.IsComplete()) {
+ LOGDMABUF(("ReadIntoBuffer: ScopedFramebufferForTexture failed."));
+ return NS_ERROR_FAILURE;
+ }
+
+ const gl::OriginPos destOrigin = gl::OriginPos::BottomLeft;
+ {
+ const ScopedBindFramebuffer bindFB(context, autoFBForTex.FB());
+ if (!context->BlitHelper()->Blit(this, aSize, destOrigin)) {
+ LOGDMABUF(("ReadIntoBuffer: Blit failed."));
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ ScopedBindFramebuffer bind(context, autoFBForTex.FB());
+ ReadPixelsIntoBuffer(context, aData, aStride, aSize, aFormat);
+ return NS_OK;
+}
+
+already_AddRefed<gfx::DataSourceSurface>
+DMABufSurfaceYUV::GetAsSourceSurface() {
+ LOGDMABUF(("DMABufSurfaceYUV::GetAsSourceSurface UID %d", mUID));
+
+ gfx::IntSize size(GetWidth(), GetHeight());
+ const auto format = gfx::SurfaceFormat::B8G8R8A8;
+ RefPtr<gfx::DataSourceSurface> source =
+ gfx::Factory::CreateDataSourceSurface(size, format);
+ if (NS_WARN_IF(!source)) {
+ LOGDMABUF(("GetAsSourceSurface: CreateDataSourceSurface failed."));
+ return nullptr;
+ }
+
+ gfx::DataSourceSurface::ScopedMap map(source,
+ gfx::DataSourceSurface::READ_WRITE);
+ if (NS_WARN_IF(!map.IsMapped())) {
+ LOGDMABUF(("GetAsSourceSurface: Mapping surface failed."));
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(
+ ReadIntoBuffer(map.GetData(), map.GetStride(), size, format)))) {
+ LOGDMABUF(("GetAsSourceSurface: Reading into buffer failed."));
+ return nullptr;
+ }
+
+ return source.forget();
+}
+
+nsresult DMABufSurfaceYUV::BuildSurfaceDescriptorBuffer(
+ SurfaceDescriptorBuffer& aSdBuffer, Image::BuildSdbFlags aFlags,
+ const std::function<MemoryOrShmem(uint32_t)>& aAllocate) {
+ LOGDMABUF(("DMABufSurfaceYUV::BuildSurfaceDescriptorBuffer UID %d", mUID));
+
+ gfx::IntSize size(GetWidth(), GetHeight());
+ const auto format = gfx::SurfaceFormat::B8G8R8A8;
+
+ uint8_t* buffer = nullptr;
+ int32_t stride = 0;
+ nsresult rv = Image::AllocateSurfaceDescriptorBufferRgb(
+ size, format, buffer, aSdBuffer, stride, aAllocate);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOGDMABUF(("BuildSurfaceDescriptorBuffer allocate descriptor failed"));
+ return rv;
+ }
+
+ return ReadIntoBuffer(buffer, stride, size, format);
+}
+
+#if 0
+void DMABufSurfaceYUV::ClearPlane(int aPlane) {
+ if (!MapInternal(0, 0, mWidth[aPlane], mHeight[aPlane], nullptr,
+ GBM_BO_TRANSFER_WRITE, aPlane)) {
+ return;
+ }
+ if ((int)mMappedRegionStride[aPlane] < mWidth[aPlane]) {
+ return;
+ }
+ memset((char*)mMappedRegion[aPlane], 0,
+ mMappedRegionStride[aPlane] * mHeight[aPlane]);
+ Unmap(aPlane);
+}
+
+# include "gfxUtils.h"
+
+void DMABufSurfaceYUV::DumpToFile(const char* aFile) {
+ RefPtr<gfx::DataSourceSurface> surf = GetAsSourceSurface();
+ gfxUtils::WriteAsPNG(surf, aFile);
+}
+#endif
diff --git a/widget/gtk/DMABufSurface.h b/widget/gtk/DMABufSurface.h
new file mode 100644
index 0000000000..9bb8928cca
--- /dev/null
+++ b/widget/gtk/DMABufSurface.h
@@ -0,0 +1,413 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef DMABufSurface_h__
+#define DMABufSurface_h__
+
+#include <functional>
+#include <stdint.h>
+#include "mozilla/widget/va_drmcommon.h"
+#include "GLTypes.h"
+#include "ImageContainer.h"
+#include "nsISupportsImpl.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/Mutex.h"
+
+typedef void* EGLImageKHR;
+typedef void* EGLSyncKHR;
+
+#define DMABUF_BUFFER_PLANES 4
+
+#ifndef VA_FOURCC_NV12
+# define VA_FOURCC_NV12 0x3231564E
+#endif
+#ifndef VA_FOURCC_YV12
+# define VA_FOURCC_YV12 0x32315659
+#endif
+#ifndef VA_FOURCC_P010
+# define VA_FOURCC_P010 0x30313050
+#endif
+
+namespace mozilla {
+namespace gfx {
+class DataSourceSurface;
+}
+namespace layers {
+class MemoryOrShmem;
+class SurfaceDescriptor;
+class SurfaceDescriptorBuffer;
+class SurfaceDescriptorDMABuf;
+} // namespace layers
+namespace gl {
+class GLContext;
+}
+} // namespace mozilla
+
+typedef enum {
+ // Use alpha pixel format
+ DMABUF_ALPHA = 1 << 0,
+ // Surface is used as texture and may be also shared
+ DMABUF_TEXTURE = 1 << 1,
+ // Use modifiers. Such dmabuf surface may have more planes
+ // and complex internal structure (tiling/compression/etc.)
+ // so we can't do direct rendering to it.
+ DMABUF_USE_MODIFIERS = 1 << 3,
+} DMABufSurfaceFlags;
+
+class DMABufSurfaceRGBA;
+class DMABufSurfaceYUV;
+struct wl_buffer;
+
+namespace mozilla::widget {
+struct GbmFormat;
+}
+
+class DMABufSurface {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DMABufSurface)
+
+ enum SurfaceType {
+ SURFACE_RGBA,
+ SURFACE_NV12,
+ SURFACE_YUV420,
+ };
+
+ // Import surface from SurfaceDescriptor. This is usually
+ // used to copy surface from another process over IPC.
+ // When a global reference counter was created for the surface
+ // (see bellow) it's automatically referenced.
+ static already_AddRefed<DMABufSurface> CreateDMABufSurface(
+ const mozilla::layers::SurfaceDescriptor& aDesc);
+
+ // Export surface to another process via. SurfaceDescriptor.
+ virtual bool Serialize(
+ mozilla::layers::SurfaceDescriptor& aOutDescriptor) = 0;
+
+ virtual int GetWidth(int aPlane = 0) = 0;
+ virtual int GetHeight(int aPlane = 0) = 0;
+ virtual mozilla::gfx::SurfaceFormat GetFormat() = 0;
+ virtual mozilla::gfx::SurfaceFormat GetFormatGL() = 0;
+
+ virtual bool CreateTexture(mozilla::gl::GLContext* aGLContext,
+ int aPlane = 0) = 0;
+ virtual void ReleaseTextures() = 0;
+ virtual GLuint GetTexture(int aPlane = 0) = 0;
+ virtual EGLImageKHR GetEGLImage(int aPlane = 0) = 0;
+
+ SurfaceType GetSurfaceType() { return mSurfaceType; };
+ virtual int GetTextureCount() = 0;
+
+ bool IsMapped(int aPlane = 0) { return (mMappedRegion[aPlane] != nullptr); };
+ void Unmap(int aPlane = 0);
+
+ virtual DMABufSurfaceRGBA* GetAsDMABufSurfaceRGBA() { return nullptr; }
+ virtual DMABufSurfaceYUV* GetAsDMABufSurfaceYUV() { return nullptr; }
+ virtual already_AddRefed<mozilla::gfx::DataSourceSurface>
+ GetAsSourceSurface() {
+ return nullptr;
+ }
+
+ virtual nsresult BuildSurfaceDescriptorBuffer(
+ mozilla::layers::SurfaceDescriptorBuffer& aSdBuffer,
+ mozilla::layers::Image::BuildSdbFlags aFlags,
+ const std::function<mozilla::layers::MemoryOrShmem(uint32_t)>& aAllocate);
+
+ virtual mozilla::gfx::YUVColorSpace GetYUVColorSpace() {
+ return mozilla::gfx::YUVColorSpace::Default;
+ };
+
+ bool IsFullRange() { return mColorRange == mozilla::gfx::ColorRange::FULL; };
+ void SetColorRange(mozilla::gfx::ColorRange aColorRange) {
+ mColorRange = aColorRange;
+ };
+
+ void FenceSet();
+ void FenceWait();
+ void FenceDelete();
+
+ // Set and get a global surface UID. The UID is shared across process
+ // and it's used to track surface lifetime in various parts of rendering
+ // engine.
+ uint32_t GetUID() const { return mUID; };
+
+ // Creates a global reference counter objects attached to the surface.
+ // It's created as unreferenced, i.e. IsGlobalRefSet() returns false
+ // right after GlobalRefCountCreate() call.
+ //
+ // The counter is shared by all surface instances across processes
+ // so it tracks global surface usage.
+ //
+ // The counter is automatically referenced when a new surface instance is
+ // created with SurfaceDescriptor (usually copied to another process over IPC)
+ // and it's unreferenced when surface is deleted.
+ //
+ // So without any additional GlobalRefAdd()/GlobalRefRelease() calls
+ // the IsGlobalRefSet() returns true if any other process use the surface.
+ void GlobalRefCountCreate();
+ void GlobalRefCountDelete();
+
+ // If global reference counter was created by GlobalRefCountCreate()
+ // returns true when there's an active surface reference.
+ bool IsGlobalRefSet() const;
+
+ // Add/Remove additional reference to the surface global reference counter.
+ void GlobalRefAdd();
+ void GlobalRefRelease();
+
+ // Release all underlying data.
+ virtual void ReleaseSurface() = 0;
+
+#ifdef DEBUG
+ virtual void DumpToFile(const char* pFile){};
+#endif
+
+ DMABufSurface(SurfaceType aSurfaceType);
+
+ protected:
+ virtual bool Create(const mozilla::layers::SurfaceDescriptor& aDesc) = 0;
+
+ // Import global ref count object from IPC by file descriptor.
+ // This adds global ref count reference to the surface.
+ void GlobalRefCountImport(int aFd);
+ // Export global ref count object by file descriptor.
+ int GlobalRefCountExport();
+
+ void ReleaseDMABuf();
+
+ void* MapInternal(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight,
+ uint32_t* aStride, int aGbmFlags, int aPlane = 0);
+
+ // We want to keep number of opened file descriptors low so open/close
+ // DMABuf file handles only when we need them, i.e. when DMABuf is exported
+ // to another process or to EGL.
+ virtual bool OpenFileDescriptorForPlane(
+ const mozilla::MutexAutoLock& aProofOfLock, int aPlane) = 0;
+ virtual void CloseFileDescriptorForPlane(
+ const mozilla::MutexAutoLock& aProofOfLock, int aPlane,
+ bool aForceClose = false) = 0;
+ bool OpenFileDescriptors(const mozilla::MutexAutoLock& aProofOfLock);
+ void CloseFileDescriptors(const mozilla::MutexAutoLock& aProofOfLock,
+ bool aForceClose = false);
+
+ virtual ~DMABufSurface();
+
+ SurfaceType mSurfaceType;
+ uint64_t mBufferModifiers[DMABUF_BUFFER_PLANES];
+
+ int mBufferPlaneCount;
+ int mDmabufFds[DMABUF_BUFFER_PLANES];
+ int32_t mDrmFormats[DMABUF_BUFFER_PLANES];
+ int32_t mStrides[DMABUF_BUFFER_PLANES];
+ int32_t mOffsets[DMABUF_BUFFER_PLANES];
+
+ struct gbm_bo* mGbmBufferObject[DMABUF_BUFFER_PLANES];
+ void* mMappedRegion[DMABUF_BUFFER_PLANES];
+ void* mMappedRegionData[DMABUF_BUFFER_PLANES];
+ uint32_t mMappedRegionStride[DMABUF_BUFFER_PLANES];
+
+ int mSyncFd;
+ EGLSyncKHR mSync;
+ RefPtr<mozilla::gl::GLContext> mGL;
+
+ int mGlobalRefCountFd;
+ uint32_t mUID;
+ mozilla::Mutex mSurfaceLock MOZ_UNANNOTATED;
+
+ mozilla::gfx::ColorRange mColorRange = mozilla::gfx::ColorRange::LIMITED;
+};
+
+class DMABufSurfaceRGBA final : public DMABufSurface {
+ public:
+ static already_AddRefed<DMABufSurfaceRGBA> CreateDMABufSurface(
+ int aWidth, int aHeight, int aDMABufSurfaceFlags);
+
+ static already_AddRefed<DMABufSurface> CreateDMABufSurface(
+ mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage,
+ int aWidth, int aHeight);
+
+ bool Serialize(mozilla::layers::SurfaceDescriptor& aOutDescriptor) override;
+
+ DMABufSurfaceRGBA* GetAsDMABufSurfaceRGBA() override { return this; }
+
+ void Clear();
+
+ void ReleaseSurface() override;
+
+ bool CopyFrom(class DMABufSurface* aSourceSurface);
+
+ int GetWidth(int aPlane = 0) override { return mWidth; };
+ int GetHeight(int aPlane = 0) override { return mHeight; };
+ mozilla::gfx::SurfaceFormat GetFormat() override;
+ mozilla::gfx::SurfaceFormat GetFormatGL() override;
+ bool HasAlpha();
+
+ void* MapReadOnly(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight,
+ uint32_t* aStride = nullptr);
+ void* MapReadOnly(uint32_t* aStride = nullptr);
+ void* Map(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight,
+ uint32_t* aStride = nullptr);
+ void* Map(uint32_t* aStride = nullptr);
+ void* GetMappedRegion(int aPlane = 0) { return mMappedRegion[aPlane]; };
+ uint32_t GetMappedRegionStride(int aPlane = 0) {
+ return mMappedRegionStride[aPlane];
+ };
+
+ bool CreateTexture(mozilla::gl::GLContext* aGLContext,
+ int aPlane = 0) override;
+ void ReleaseTextures() override;
+ GLuint GetTexture(int aPlane = 0) override { return mTexture; };
+ EGLImageKHR GetEGLImage(int aPlane = 0) override { return mEGLImage; };
+
+#ifdef MOZ_WAYLAND
+ bool CreateWlBuffer();
+ void ReleaseWlBuffer();
+ wl_buffer* GetWlBuffer() { return mWlBuffer; };
+#endif
+
+ int GetTextureCount() override { return 1; };
+
+#ifdef DEBUG
+ void DumpToFile(const char* pFile) override;
+#endif
+
+ DMABufSurfaceRGBA();
+
+ private:
+ DMABufSurfaceRGBA(const DMABufSurfaceRGBA&) = delete;
+ DMABufSurfaceRGBA& operator=(const DMABufSurfaceRGBA&) = delete;
+ ~DMABufSurfaceRGBA();
+
+ bool Create(int aWidth, int aHeight, int aDMABufSurfaceFlags);
+ bool Create(const mozilla::layers::SurfaceDescriptor& aDesc) override;
+ bool Create(mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage,
+ int aWidth, int aHeight);
+
+ bool ImportSurfaceDescriptor(const mozilla::layers::SurfaceDescriptor& aDesc);
+
+ bool OpenFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock,
+ int aPlane) override;
+ void CloseFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock,
+ int aPlane, bool aForceClose) override;
+
+ private:
+ int mSurfaceFlags;
+
+ int mWidth;
+ int mHeight;
+ mozilla::widget::GbmFormat* mGmbFormat;
+
+ EGLImageKHR mEGLImage;
+ GLuint mTexture;
+ uint32_t mGbmBufferFlags;
+#ifdef MOZ_WAYLAND
+ wl_buffer* mWlBuffer = nullptr;
+#endif
+};
+
+class DMABufSurfaceYUV final : public DMABufSurface {
+ public:
+ static already_AddRefed<DMABufSurfaceYUV> CreateYUVSurface(
+ int aWidth, int aHeight, void** aPixelData = nullptr,
+ int* aLineSizes = nullptr);
+ static already_AddRefed<DMABufSurfaceYUV> CreateYUVSurface(
+ const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight);
+ static already_AddRefed<DMABufSurfaceYUV> CopyYUVSurface(
+ const VADRMPRIMESurfaceDescriptor& aVaDesc, int aWidth, int aHeight);
+ static void ReleaseVADRMPRIMESurfaceDescriptor(
+ VADRMPRIMESurfaceDescriptor& aDesc);
+
+ bool Serialize(mozilla::layers::SurfaceDescriptor& aOutDescriptor) override;
+
+ DMABufSurfaceYUV* GetAsDMABufSurfaceYUV() override { return this; };
+ already_AddRefed<mozilla::gfx::DataSourceSurface> GetAsSourceSurface()
+ override;
+
+ nsresult BuildSurfaceDescriptorBuffer(
+ mozilla::layers::SurfaceDescriptorBuffer& aSdBuffer,
+ mozilla::layers::Image::BuildSdbFlags aFlags,
+ const std::function<mozilla::layers::MemoryOrShmem(uint32_t)>& aAllocate)
+ override;
+
+ int GetWidth(int aPlane = 0) override { return mWidth[aPlane]; }
+ int GetHeight(int aPlane = 0) override { return mHeight[aPlane]; }
+ mozilla::gfx::SurfaceFormat GetFormat() override;
+ mozilla::gfx::SurfaceFormat GetFormatGL() override;
+
+ bool CreateTexture(mozilla::gl::GLContext* aGLContext,
+ int aPlane = 0) override;
+ void ReleaseTextures() override;
+
+ void ReleaseSurface() override;
+
+ GLuint GetTexture(int aPlane = 0) override { return mTexture[aPlane]; };
+ EGLImageKHR GetEGLImage(int aPlane = 0) override {
+ return mEGLImage[aPlane];
+ };
+
+ int GetTextureCount() override;
+
+ void SetYUVColorSpace(mozilla::gfx::YUVColorSpace aColorSpace) {
+ mColorSpace = aColorSpace;
+ }
+ mozilla::gfx::YUVColorSpace GetYUVColorSpace() override {
+ return mColorSpace;
+ }
+
+ DMABufSurfaceYUV();
+
+ bool UpdateYUVData(void** aPixelData, int* aLineSizes);
+ bool UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth,
+ int aHeight, bool aCopy);
+ bool VerifyTextureCreation();
+
+ private:
+ DMABufSurfaceYUV(const DMABufSurfaceYUV&) = delete;
+ DMABufSurfaceYUV& operator=(const DMABufSurfaceYUV&) = delete;
+ ~DMABufSurfaceYUV();
+
+ bool Create(const mozilla::layers::SurfaceDescriptor& aDesc) override;
+ bool Create(int aWidth, int aHeight, void** aPixelData, int* aLineSizes);
+ bool CreateYUVPlane(int aPlane);
+ bool CreateLinearYUVPlane(int aPlane, int aWidth, int aHeight,
+ int aDrmFormat);
+ void UpdateYUVPlane(int aPlane, void* aPixelData, int aLineSize);
+
+ bool MoveYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth,
+ int aHeight);
+ bool CopyYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth,
+ int aHeight);
+
+ bool ImportPRIMESurfaceDescriptor(const VADRMPRIMESurfaceDescriptor& aDesc,
+ int aWidth, int aHeight);
+ bool ImportSurfaceDescriptor(
+ const mozilla::layers::SurfaceDescriptorDMABuf& aDesc);
+
+ bool OpenFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock,
+ int aPlane) override;
+ void CloseFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock,
+ int aPlane, bool aForceClose) override;
+
+ bool CreateEGLImage(mozilla::gl::GLContext* aGLContext, int aPlane);
+ void ReleaseEGLImages(mozilla::gl::GLContext* aGLContext);
+
+ nsresult ReadIntoBuffer(uint8_t* aData, int32_t aStride,
+ const mozilla::gfx::IntSize& aSize,
+ mozilla::gfx::SurfaceFormat aFormat);
+
+ int mWidth[DMABUF_BUFFER_PLANES];
+ int mHeight[DMABUF_BUFFER_PLANES];
+ // Aligned size of the surface imported from VADRMPRIMESurfaceDescriptor.
+ // It's used only internally to create EGLImage as some GL drivers
+ // needs that (Bug 1724385).
+ int mWidthAligned[DMABUF_BUFFER_PLANES];
+ int mHeightAligned[DMABUF_BUFFER_PLANES];
+ EGLImageKHR mEGLImage[DMABUF_BUFFER_PLANES];
+ GLuint mTexture[DMABUF_BUFFER_PLANES];
+ mozilla::gfx::YUVColorSpace mColorSpace =
+ mozilla::gfx::YUVColorSpace::Default;
+};
+
+#endif
diff --git a/widget/gtk/GRefPtr.h b/widget/gtk/GRefPtr.h
new file mode 100644
index 0000000000..1970008811
--- /dev/null
+++ b/widget/gtk/GRefPtr.h
@@ -0,0 +1,71 @@
+/* -*- 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/. */
+
+#ifndef GRefPtr_h_
+#define GRefPtr_h_
+
+// Allows to use RefPtr<T> with various kinds of GObjects
+
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+template <typename T>
+struct GObjectRefPtrTraits {
+ static void AddRef(T* aObject) { g_object_ref(aObject); }
+ static void Release(T* aObject) { g_object_unref(aObject); }
+};
+
+#define GOBJECT_TRAITS(type_) \
+ template <> \
+ struct RefPtrTraits<type_> : public GObjectRefPtrTraits<type_> {};
+
+GOBJECT_TRAITS(GtkWidget)
+GOBJECT_TRAITS(GFile)
+GOBJECT_TRAITS(GFileMonitor)
+GOBJECT_TRAITS(GMenu)
+GOBJECT_TRAITS(GMenuItem)
+GOBJECT_TRAITS(GSimpleAction)
+GOBJECT_TRAITS(GSimpleActionGroup)
+GOBJECT_TRAITS(GDBusProxy)
+GOBJECT_TRAITS(GAppInfo)
+GOBJECT_TRAITS(GAppLaunchContext)
+GOBJECT_TRAITS(GdkDragContext)
+GOBJECT_TRAITS(GDBusMessage)
+GOBJECT_TRAITS(GdkPixbuf)
+GOBJECT_TRAITS(GCancellable)
+GOBJECT_TRAITS(GtkIMContext)
+GOBJECT_TRAITS(GUnixFDList)
+GOBJECT_TRAITS(GtkCssProvider)
+GOBJECT_TRAITS(GDBusMethodInvocation)
+
+#undef GOBJECT_TRAITS
+
+template <>
+struct RefPtrTraits<GVariant> {
+ static void AddRef(GVariant* aVariant) { g_variant_ref(aVariant); }
+ static void Release(GVariant* aVariant) { g_variant_unref(aVariant); }
+};
+
+template <>
+struct RefPtrTraits<GHashTable> {
+ static void AddRef(GHashTable* aObject) { g_hash_table_ref(aObject); }
+ static void Release(GHashTable* aObject) { g_hash_table_unref(aObject); }
+};
+
+template <>
+struct RefPtrTraits<GDBusNodeInfo> {
+ static void AddRef(GDBusNodeInfo* aObject) { g_dbus_node_info_ref(aObject); }
+ static void Release(GDBusNodeInfo* aObject) {
+ g_dbus_node_info_unref(aObject);
+ }
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/widget/gtk/GUniquePtr.h b/widget/gtk/GUniquePtr.h
new file mode 100644
index 0000000000..0d164f9369
--- /dev/null
+++ b/widget/gtk/GUniquePtr.h
@@ -0,0 +1,35 @@
+/* -*- 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/. */
+
+#ifndef GUniquePtr_h_
+#define GUniquePtr_h_
+
+// Provides GUniquePtr to g_free a given pointer.
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+struct GFreeDeleter {
+ constexpr GFreeDeleter() = default;
+ void operator()(GdkEventCrossing* aPtr) const {
+ gdk_event_free((GdkEvent*)aPtr);
+ }
+ void operator()(GdkEventKey* aPtr) const { gdk_event_free((GdkEvent*)aPtr); }
+ void operator()(GdkEvent* aPtr) const { gdk_event_free(aPtr); }
+ void operator()(GError* aPtr) const { g_error_free(aPtr); }
+ void operator()(void* aPtr) const { g_free(aPtr); }
+ void operator()(GtkPaperSize* aPtr) const { gtk_paper_size_free(aPtr); }
+ void operator()(gchar** aPtr) const { g_strfreev(aPtr); }
+};
+
+template <typename T>
+using GUniquePtr = UniquePtr<T, GFreeDeleter>;
+
+} // namespace mozilla
+
+#endif
diff --git a/widget/gtk/GfxInfo.cpp b/widget/gtk/GfxInfo.cpp
new file mode 100644
index 0000000000..4dadbc81b5
--- /dev/null
+++ b/widget/gtk/GfxInfo.cpp
@@ -0,0 +1,1556 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 "GfxInfo.h"
+
+#include <cctype>
+#include <errno.h>
+#include <unistd.h>
+#include <string>
+#include <poll.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <glib.h>
+#include <fcntl.h>
+
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/SSE.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/XREAppData.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsCRTGlue.h"
+#include "nsExceptionHandler.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsUnicharUtils.h"
+#include "nsWhitespaceTokenizer.h"
+#include "prenv.h"
+#include "WidgetUtilsGtk.h"
+#include "MediaCodecsSupport.h"
+#include "nsAppRunner.h"
+
+// How long we wait for data from glxtest/vaapi test process in milliseconds.
+#define GFX_TEST_TIMEOUT 4000
+#define VAAPI_TEST_TIMEOUT 2000
+#define V4L2_TEST_TIMEOUT 2000
+
+#define GLX_PROBE_BINARY u"glxtest"_ns
+#define VAAPI_PROBE_BINARY u"vaapitest"_ns
+#define V4L2_PROBE_BINARY u"v4l2test"_ns
+
+namespace mozilla::widget {
+
+#ifdef DEBUG
+NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug)
+#endif
+
+int GfxInfo::sGLXTestPipe = -1;
+pid_t GfxInfo::sGLXTestPID = 0;
+
+// bits to use decoding codec information returned from glxtest
+constexpr int CODEC_HW_H264 = 1 << 4;
+constexpr int CODEC_HW_VP8 = 1 << 5;
+constexpr int CODEC_HW_VP9 = 1 << 6;
+constexpr int CODEC_HW_AV1 = 1 << 7;
+
+nsresult GfxInfo::Init() {
+ mGLMajorVersion = 0;
+ mGLMinorVersion = 0;
+ mHasTextureFromPixmap = false;
+ mIsMesa = false;
+ mIsAccelerated = true;
+ mIsWayland = false;
+ mIsXWayland = false;
+ mHasMultipleGPUs = false;
+ mGlxTestError = false;
+ return GfxInfoBase::Init();
+}
+
+void GfxInfo::AddCrashReportAnnotations() {
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID,
+ mVendorId);
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID,
+ mDeviceId);
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AdapterDriverVendor, mDriverVendor);
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AdapterDriverVersion, mDriverVersion);
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::IsWayland,
+ mIsWayland);
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::DesktopEnvironment,
+ GetDesktopEnvironmentIdentifier());
+
+ if (mHasMultipleGPUs) {
+ nsAutoCString note;
+ note.AppendLiteral("Has dual GPUs.");
+ if (!mSecondaryVendorId.IsEmpty()) {
+ note.AppendLiteral(" GPU #2: AdapterVendorID2: ");
+ note.Append(mSecondaryVendorId);
+ note.AppendLiteral(", AdapterDeviceID2: ");
+ note.Append(mSecondaryDeviceId);
+ }
+ CrashReporter::AppendAppNotesToCrashReport(note);
+ }
+}
+
+static bool MakeFdNonBlocking(int fd) {
+ return fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) != -1;
+}
+
+static bool ManageChildProcess(const char* aProcessName, int* aPID, int* aPipe,
+ int aTimeout, char** aData) {
+ // Don't try anything if we failed before
+ if (*aPID < 0) {
+ return false;
+ }
+
+ GIOChannel* channel = nullptr;
+ *aData = nullptr;
+
+ auto free = mozilla::MakeScopeExit([&] {
+ if (channel) {
+ g_io_channel_unref(channel);
+ }
+ if (*aPipe >= 0) {
+ close(*aPipe);
+ *aPipe = -1;
+ }
+ });
+
+ const TimeStamp deadline =
+ TimeStamp::Now() + TimeDuration::FromMilliseconds(aTimeout);
+
+ struct pollfd pfd {};
+ pfd.fd = *aPipe;
+ pfd.events = POLLIN;
+
+ while (poll(&pfd, 1, aTimeout) != 1) {
+ if (errno != EAGAIN && errno != EINTR) {
+ gfxCriticalNote << "ManageChildProcess(" << aProcessName
+ << "): poll failed: " << strerror(errno) << "\n";
+ return false;
+ }
+ if (TimeStamp::Now() > deadline) {
+ gfxCriticalNote << "ManageChildProcess(" << aProcessName
+ << "): process hangs\n";
+ return false;
+ }
+ }
+
+ channel = g_io_channel_unix_new(*aPipe);
+ MakeFdNonBlocking(*aPipe);
+
+ GUniquePtr<GError> error;
+ gsize length = 0;
+ int ret;
+ do {
+ error = nullptr;
+ ret = g_io_channel_read_to_end(channel, aData, &length,
+ getter_Transfers(error));
+ } while (ret == G_IO_STATUS_AGAIN && TimeStamp::Now() < deadline);
+ if (error || ret != G_IO_STATUS_NORMAL) {
+ gfxCriticalNote << "ManageChildProcess(" << aProcessName
+ << "): failed to read data from child process: ";
+ if (error) {
+ gfxCriticalNote << error->message;
+ } else {
+ gfxCriticalNote << "timeout";
+ }
+ return false;
+ }
+
+ int status = 0;
+ int pid = *aPID;
+ *aPID = -1;
+
+ while (true) {
+ int ret = waitpid(pid, &status, WNOHANG);
+ if (ret > 0) {
+ break;
+ }
+ if (ret < 0) {
+ if (errno == ECHILD) {
+ // Bug 718629
+ // ECHILD happens when the glxtest process got reaped got reaped after a
+ // PR_CreateProcess as per bug 227246. This shouldn't matter, as we
+ // still seem to get the data from the pipe, and if we didn't, the
+ // outcome would be to blocklist anyway.
+ return true;
+ }
+ if (errno != EAGAIN && errno != EINTR) {
+ gfxCriticalNote << "ManageChildProcess(" << aProcessName
+ << "): waitpid failed: " << strerror(errno) << "\n";
+ return false;
+ }
+ }
+ if (TimeStamp::Now() > deadline) {
+ gfxCriticalNote << "ManageChildProcess(" << aProcessName
+ << "): process hangs\n";
+ return false;
+ }
+ // Wait 50ms to another waitpid() check.
+ usleep(50000);
+ }
+
+ return WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS;
+}
+
+// to understand this function, see bug 639842. We retrieve the OpenGL driver
+// information in a separate process to protect against bad drivers.
+void GfxInfo::GetData() {
+ if (mInitialized) {
+ return;
+ }
+ mInitialized = true;
+
+ // In some cases (xpcshell test, Profile manager etc.)
+ // FireGLXTestProcess() is not fired in advance
+ // so we call it here.
+ GfxInfo::FireGLXTestProcess();
+
+ GfxInfoBase::GetData();
+
+ char* glxData = nullptr;
+ auto free = mozilla::MakeScopeExit([&] { g_free((void*)glxData); });
+
+ bool error = !ManageChildProcess("glxtest", &sGLXTestPID, &sGLXTestPipe,
+ GFX_TEST_TIMEOUT, &glxData);
+ if (error) {
+ gfxCriticalNote << "glxtest: ManageChildProcess failed\n";
+ }
+
+ nsCString glVendor;
+ nsCString glRenderer;
+ nsCString glVersion;
+ nsCString textureFromPixmap;
+ nsCString testType;
+
+ // Available if GLX_MESA_query_renderer is supported.
+ nsCString mesaVendor;
+ nsCString mesaDevice;
+ nsCString mesaAccelerated;
+ // Available if using a DRI-based libGL stack.
+ nsCString driDriver;
+ nsCString adapterRam;
+
+ nsCString drmRenderDevice;
+
+ nsCString ddxDriver;
+
+ AutoTArray<nsCString, 2> pciVendors;
+ AutoTArray<nsCString, 2> pciDevices;
+
+ nsCString* stringToFill = nullptr;
+ bool logString = false;
+ bool errorLog = false;
+
+ char* bufptr = glxData;
+
+ while (true) {
+ char* line = NS_strtok("\n", &bufptr);
+ if (!line) break;
+ if (stringToFill) {
+ stringToFill->Assign(line);
+ stringToFill = nullptr;
+ } else if (logString) {
+ gfxCriticalNote << "glxtest: " << line;
+ logString = false;
+ } else if (!strcmp(line, "VENDOR")) {
+ stringToFill = &glVendor;
+ } else if (!strcmp(line, "RENDERER")) {
+ stringToFill = &glRenderer;
+ } else if (!strcmp(line, "VERSION")) {
+ stringToFill = &glVersion;
+ } else if (!strcmp(line, "TFP")) {
+ stringToFill = &textureFromPixmap;
+ } else if (!strcmp(line, "MESA_VENDOR_ID")) {
+ stringToFill = &mesaVendor;
+ } else if (!strcmp(line, "MESA_DEVICE_ID")) {
+ stringToFill = &mesaDevice;
+ } else if (!strcmp(line, "MESA_ACCELERATED")) {
+ stringToFill = &mesaAccelerated;
+ } else if (!strcmp(line, "MESA_VRAM")) {
+ stringToFill = &adapterRam;
+ } else if (!strcmp(line, "DDX_DRIVER")) {
+ stringToFill = &ddxDriver;
+ } else if (!strcmp(line, "DRI_DRIVER")) {
+ stringToFill = &driDriver;
+ } else if (!strcmp(line, "PCI_VENDOR_ID")) {
+ stringToFill = pciVendors.AppendElement();
+ } else if (!strcmp(line, "PCI_DEVICE_ID")) {
+ stringToFill = pciDevices.AppendElement();
+ } else if (!strcmp(line, "DRM_RENDERDEVICE")) {
+ stringToFill = &drmRenderDevice;
+ } else if (!strcmp(line, "TEST_TYPE")) {
+ stringToFill = &testType;
+ } else if (!strcmp(line, "WARNING")) {
+ logString = true;
+ } else if (!strcmp(line, "ERROR")) {
+ logString = true;
+ errorLog = true;
+ }
+ }
+
+ MOZ_ASSERT(pciDevices.Length() == pciVendors.Length(),
+ "Missing PCI vendors/devices");
+
+ size_t pciLen = std::min(pciVendors.Length(), pciDevices.Length());
+ mHasMultipleGPUs = pciLen > 1;
+
+ if (!strcmp(textureFromPixmap.get(), "TRUE")) mHasTextureFromPixmap = true;
+
+ // only useful for Linux kernel version check for FGLRX driver.
+ // assumes X client == X server, which is sad.
+ struct utsname unameobj {};
+ if (uname(&unameobj) >= 0) {
+ mOS.Assign(unameobj.sysname);
+ mOSRelease.Assign(unameobj.release);
+ }
+
+ const char* spoofedVendor = PR_GetEnv("MOZ_GFX_SPOOF_GL_VENDOR");
+ if (spoofedVendor) glVendor.Assign(spoofedVendor);
+ const char* spoofedRenderer = PR_GetEnv("MOZ_GFX_SPOOF_GL_RENDERER");
+ if (spoofedRenderer) glRenderer.Assign(spoofedRenderer);
+ const char* spoofedVersion = PR_GetEnv("MOZ_GFX_SPOOF_GL_VERSION");
+ if (spoofedVersion) glVersion.Assign(spoofedVersion);
+ const char* spoofedOS = PR_GetEnv("MOZ_GFX_SPOOF_OS");
+ if (spoofedOS) mOS.Assign(spoofedOS);
+ const char* spoofedOSRelease = PR_GetEnv("MOZ_GFX_SPOOF_OS_RELEASE");
+ if (spoofedOSRelease) mOSRelease.Assign(spoofedOSRelease);
+
+ // Scan the GL_VERSION string for the GL and driver versions.
+ nsCWhitespaceTokenizer tokenizer(glVersion);
+ while (tokenizer.hasMoreTokens()) {
+ nsCString token(tokenizer.nextToken());
+ unsigned int major = 0, minor = 0, revision = 0, patch = 0;
+ if (sscanf(token.get(), "%u.%u.%u.%u", &major, &minor, &revision, &patch) >=
+ 2) {
+ // A survey of GL_VENDOR strings indicates that the first version is
+ // always the GL version, the second is usually the driver version.
+ if (mGLMajorVersion == 0) {
+ mGLMajorVersion = major;
+ mGLMinorVersion = minor;
+ } else if (mDriverVersion.IsEmpty()) { // Not already spoofed.
+ mDriverVersion =
+ nsPrintfCString("%u.%u.%u.%u", major, minor, revision, patch);
+ }
+ }
+ }
+
+ if (mGLMajorVersion == 0) {
+ NS_WARNING("Failed to parse GL version!");
+ }
+
+ mDrmRenderDevice = std::move(drmRenderDevice);
+ mTestType = std::move(testType);
+
+ // Mesa always exposes itself in the GL_VERSION string, but not always the
+ // GL_VENDOR string.
+ mIsMesa = glVersion.Find("Mesa") != -1;
+
+ // We need to use custom driver vendor IDs for mesa so we can treat them
+ // differently than the proprietary drivers.
+ if (mIsMesa) {
+ mIsAccelerated = !mesaAccelerated.Equals("FALSE");
+ // Process software rasterizers before the DRI driver string; we may be
+ // forcing software rasterization on a DRI-accelerated X server by using
+ // LIBGL_ALWAYS_SOFTWARE or a similar restriction.
+ if (strcasestr(glRenderer.get(), "llvmpipe")) {
+ CopyUTF16toUTF8(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::MesaLLVMPipe),
+ mDriverVendor);
+ mIsAccelerated = false;
+ } else if (strcasestr(glRenderer.get(), "softpipe")) {
+ CopyUTF16toUTF8(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSoftPipe),
+ mDriverVendor);
+ mIsAccelerated = false;
+ } else if (strcasestr(glRenderer.get(), "software rasterizer")) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSWRast),
+ mDriverVendor);
+ mIsAccelerated = false;
+ } else if (strcasestr(driDriver.get(), "vmwgfx")) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaVM),
+ mDriverVendor);
+ mIsAccelerated = false;
+ } else if (!mIsAccelerated) {
+ CopyUTF16toUTF8(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSWUnknown),
+ mDriverVendor);
+ } else if (!driDriver.IsEmpty()) {
+ mDriverVendor = nsPrintfCString("mesa/%s", driDriver.get());
+ } else {
+ // Some other mesa configuration where we couldn't get enough info.
+ NS_WARNING("Failed to detect Mesa driver being used!");
+ CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaUnknown),
+ mDriverVendor);
+ }
+
+ if (!mesaVendor.IsEmpty()) {
+ mVendorId = mesaVendor;
+ }
+
+ if (!mesaDevice.IsEmpty()) {
+ mDeviceId = mesaDevice;
+ }
+
+ if (!mIsAccelerated && mVendorId.IsEmpty()) {
+ mVendorId.Assign(glVendor.get());
+ }
+
+ if (!mIsAccelerated && mDeviceId.IsEmpty()) {
+ mDeviceId.Assign(glRenderer.get());
+ }
+ } else if (glVendor.EqualsLiteral("NVIDIA Corporation")) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA),
+ mVendorId);
+ mDriverVendor.AssignLiteral("nvidia/unknown");
+ // TODO: Use NV-CONTROL X11 extension to query Device ID and VRAM.
+ } else if (glVendor.EqualsLiteral("ATI Technologies Inc.")) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI),
+ mVendorId);
+ mDriverVendor.AssignLiteral("ati/unknown");
+ // TODO: Look into ways to find the device ID on FGLRX.
+ } else {
+ NS_WARNING("Failed to detect GL vendor!");
+ }
+
+ if (!adapterRam.IsEmpty()) {
+ mAdapterRAM = (uint32_t)atoi(adapterRam.get());
+ }
+
+ // If we have the DRI driver, we can derive the vendor ID from that if needed.
+ if (mVendorId.IsEmpty() && !driDriver.IsEmpty()) {
+ const char* nvidiaDrivers[] = {"nouveau", "tegra", nullptr};
+ for (size_t i = 0; nvidiaDrivers[i]; ++i) {
+ if (driDriver.Equals(nvidiaDrivers[i])) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA),
+ mVendorId);
+ break;
+ }
+ }
+
+ if (mVendorId.IsEmpty()) {
+ const char* intelDrivers[] = {"iris", "crocus", "i915", "i965",
+ "i810", "intel", nullptr};
+ for (size_t i = 0; intelDrivers[i]; ++i) {
+ if (driDriver.Equals(intelDrivers[i])) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::Intel),
+ mVendorId);
+ break;
+ }
+ }
+ }
+
+ if (mVendorId.IsEmpty()) {
+ const char* amdDrivers[] = {"r600", "r200", "r100",
+ "radeon", "radeonsi", nullptr};
+ for (size_t i = 0; amdDrivers[i]; ++i) {
+ if (driDriver.Equals(amdDrivers[i])) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI),
+ mVendorId);
+ break;
+ }
+ }
+ }
+
+ if (mVendorId.IsEmpty()) {
+ if (driDriver.EqualsLiteral("freedreno")) {
+ CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::Qualcomm),
+ mVendorId);
+ }
+ }
+ }
+
+ // If we still don't have a vendor ID, we can try the PCI vendor list.
+ if (mVendorId.IsEmpty()) {
+ if (pciVendors.IsEmpty()) {
+ gfxCriticalNote << "No GPUs detected via PCI\n";
+ } else {
+ for (size_t i = 0; i < pciVendors.Length(); ++i) {
+ if (mVendorId.IsEmpty()) {
+ mVendorId = pciVendors[i];
+ } else if (mVendorId != pciVendors[i]) {
+ gfxCriticalNote << "More than 1 GPU vendor detected via PCI, cannot "
+ "deduce vendor\n";
+ mVendorId.Truncate();
+ break;
+ }
+ }
+ }
+ }
+
+ // If we know the vendor ID, but didn't get a device ID, we can guess from the
+ // PCI device list.
+ if (mDeviceId.IsEmpty() && !mVendorId.IsEmpty()) {
+ for (size_t i = 0; i < pciLen; ++i) {
+ if (mVendorId.Equals(pciVendors[i])) {
+ if (mDeviceId.IsEmpty()) {
+ mDeviceId = pciDevices[i];
+ } else if (mDeviceId != pciDevices[i]) {
+ gfxCriticalNote << "More than 1 GPU from same vendor detected via "
+ "PCI, cannot deduce device\n";
+ mDeviceId.Truncate();
+ break;
+ }
+ }
+ }
+ }
+
+ // Assuming we know the vendor, we should check for a secondary card.
+ if (!mVendorId.IsEmpty()) {
+ if (pciLen > 2) {
+ gfxCriticalNote
+ << "More than 2 GPUs detected via PCI, secondary GPU is arbitrary\n";
+ }
+ for (size_t i = 0; i < pciLen; ++i) {
+ if (!mVendorId.Equals(pciVendors[i]) ||
+ (!mDeviceId.IsEmpty() && !mDeviceId.Equals(pciDevices[i]))) {
+ mSecondaryVendorId = pciVendors[i];
+ mSecondaryDeviceId = pciDevices[i];
+ break;
+ }
+ }
+ }
+
+ // If we couldn't choose, log them.
+ if (mVendorId.IsEmpty()) {
+ for (size_t i = 0; i < pciLen; ++i) {
+ gfxCriticalNote << "PCI candidate " << pciVendors[i].get() << "/"
+ << pciDevices[i].get() << "\n";
+ }
+ }
+
+ // Fallback to GL_VENDOR and GL_RENDERER.
+ if (mVendorId.IsEmpty()) {
+ mVendorId.Assign(glVendor.get());
+ }
+ if (mDeviceId.IsEmpty()) {
+ mDeviceId.Assign(glRenderer.get());
+ }
+
+ mAdapterDescription.Assign(glRenderer);
+
+ // Make a best effort guess at whether or not we are using the XWayland compat
+ // layer. For all intents and purposes, we should otherwise believe we are
+ // using X11.
+ mIsWayland = GdkIsWaylandDisplay();
+ mIsXWayland = IsXWaylandProtocol();
+
+ if (!ddxDriver.IsEmpty()) {
+ PRInt32 start = 0;
+ PRInt32 loc = ddxDriver.Find(";", start);
+ while (loc != kNotFound) {
+ nsCString line(ddxDriver.get() + start, loc - start);
+ mDdxDrivers.AppendElement(std::move(line));
+
+ start = loc + 1;
+ loc = ddxDriver.Find(";", start);
+ }
+ }
+
+ if (error || errorLog || mTestType.IsEmpty()) {
+ if (!mAdapterDescription.IsEmpty()) {
+ mAdapterDescription.AppendLiteral(" (See failure log)");
+ } else {
+ mAdapterDescription.AppendLiteral("See failure log");
+ }
+
+ mGlxTestError = true;
+ }
+
+ AddCrashReportAnnotations();
+}
+
+int GfxInfo::FireTestProcess(const nsAString& aBinaryFile, int* aOutPipe,
+ const char** aStringArgs) {
+ nsCOMPtr<nsIFile> appFile;
+ nsresult rv = XRE_GetBinaryPath(getter_AddRefs(appFile));
+ if (NS_FAILED(rv)) {
+ gfxCriticalNote << "Couldn't find application file.\n";
+ return false;
+ }
+ nsCOMPtr<nsIFile> exePath;
+ rv = appFile->GetParent(getter_AddRefs(exePath));
+ if (NS_FAILED(rv)) {
+ gfxCriticalNote << "Couldn't get application directory.\n";
+ return false;
+ }
+ exePath->Append(aBinaryFile);
+
+#define MAX_ARGS 8
+ char* argv[MAX_ARGS + 2];
+
+ argv[0] = strdup(exePath->NativePath().get());
+ for (int i = 0; i < MAX_ARGS; i++) {
+ if (aStringArgs[i]) {
+ argv[i + 1] = strdup(aStringArgs[i]);
+ } else {
+ argv[i + 1] = nullptr;
+ break;
+ }
+ }
+
+ // Use G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_DO_NOT_REAP_CHILD flags
+ // to g_spawn_async_with_pipes() run posix_spawn() directly.
+ int pid;
+ GUniquePtr<GError> err;
+ g_spawn_async_with_pipes(
+ nullptr, argv, nullptr,
+ GSpawnFlags(G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_DO_NOT_REAP_CHILD),
+ nullptr, nullptr, &pid, nullptr, aOutPipe, nullptr,
+ getter_Transfers(err));
+ if (err) {
+ gfxCriticalNote << "FireTestProcess failed: " << err->message << "\n";
+ pid = 0;
+ }
+ for (auto& arg : argv) {
+ if (!arg) {
+ break;
+ }
+ free(arg);
+ }
+ return pid;
+}
+
+bool GfxInfo::FireGLXTestProcess() {
+ if (sGLXTestPID != 0) {
+ return true;
+ }
+
+ int pfd[2];
+ if (pipe(pfd) == -1) {
+ gfxCriticalNote << "FireGLXTestProcess failed to create pipe\n";
+ return false;
+ }
+ sGLXTestPipe = pfd[0];
+
+ auto pipeID = std::to_string(pfd[1]);
+ const char* args[] = {"-f", pipeID.c_str(),
+ IsWaylandEnabled() ? "-w" : nullptr, nullptr};
+ sGLXTestPID = FireTestProcess(GLX_PROBE_BINARY, nullptr, args);
+ // Set pid to -1 to avoid further test launch.
+ if (!sGLXTestPID) {
+ sGLXTestPID = -1;
+ }
+ close(pfd[1]);
+ return true;
+}
+
+void GfxInfo::GetDataVAAPI() {
+ if (mIsVAAPISupported.isSome()) {
+ return;
+ }
+ mIsVAAPISupported = Some(false);
+
+#ifdef MOZ_ENABLE_VAAPI
+ char* vaapiData = nullptr;
+ auto free = mozilla::MakeScopeExit([&] { g_free((void*)vaapiData); });
+
+ int vaapiPipe = -1;
+ int vaapiPID = 0;
+ const char* args[] = {"-d", mDrmRenderDevice.get(), nullptr};
+ vaapiPID = FireTestProcess(VAAPI_PROBE_BINARY, &vaapiPipe, args);
+ if (!vaapiPID) {
+ return;
+ }
+
+ if (!ManageChildProcess("vaapitest", &vaapiPID, &vaapiPipe,
+ VAAPI_TEST_TIMEOUT, &vaapiData)) {
+ gfxCriticalNote << "vaapitest: ManageChildProcess failed\n";
+ return;
+ }
+
+ char* bufptr = vaapiData;
+ char* line;
+ while ((line = NS_strtok("\n", &bufptr))) {
+ if (!strcmp(line, "VAAPI_SUPPORTED")) {
+ line = NS_strtok("\n", &bufptr);
+ if (!line) {
+ gfxCriticalNote << "vaapitest: Failed to get VAAPI support\n";
+ return;
+ }
+ mIsVAAPISupported = Some(!strcmp(line, "TRUE"));
+ } else if (!strcmp(line, "VAAPI_HWCODECS")) {
+ line = NS_strtok("\n", &bufptr);
+ if (!line) {
+ gfxCriticalNote << "vaapitest: Failed to get VAAPI codecs\n";
+ return;
+ }
+
+ std::istringstream(line) >> mVAAPISupportedCodecs;
+ if (mVAAPISupportedCodecs & CODEC_HW_H264) {
+ media::MCSInfo::AddSupport(
+ media::MediaCodecsSupport::H264HardwareDecode);
+ }
+ if (mVAAPISupportedCodecs & CODEC_HW_VP8) {
+ media::MCSInfo::AddSupport(
+ media::MediaCodecsSupport::VP8HardwareDecode);
+ }
+ if (mVAAPISupportedCodecs & CODEC_HW_VP9) {
+ media::MCSInfo::AddSupport(
+ media::MediaCodecsSupport::VP9HardwareDecode);
+ }
+ if (mVAAPISupportedCodecs & CODEC_HW_AV1) {
+ media::MCSInfo::AddSupport(
+ media::MediaCodecsSupport::AV1HardwareDecode);
+ }
+ } else if (!strcmp(line, "WARNING") || !strcmp(line, "ERROR")) {
+ gfxCriticalNote << "vaapitest: " << line;
+ line = NS_strtok("\n", &bufptr);
+ if (line) {
+ gfxCriticalNote << "vaapitest: " << line << "\n";
+ }
+ return;
+ }
+ }
+#endif
+}
+
+// Probe all V4L2 devices and check their capabilities
+void GfxInfo::GetDataV4L2() {
+ if (mIsV4L2Supported.isSome()) {
+ // We have already probed v4l2 support, no need to do it again.
+ return;
+ }
+ mIsV4L2Supported = Some(false);
+
+#ifdef MOZ_ENABLE_V4L2
+ DIR* dir = opendir("/dev");
+ if (!dir) {
+ gfxCriticalNote << "Could not list /dev\n";
+ return;
+ }
+ struct dirent* dir_entry;
+ while ((dir_entry = readdir(dir))) {
+ if (!strncmp(dir_entry->d_name, "video", 5)) {
+ nsCString path = "/dev/"_ns;
+ path += nsDependentCString(dir_entry->d_name);
+ V4L2ProbeDevice(path);
+ }
+ }
+ closedir(dir);
+#endif // MOZ_ENABLE_V4L2
+}
+
+// Check the capabilities of a single V4L2 device. If the device doesn't work
+// or doesn't support any codecs we recognise, then we just ignore it. If it
+// does support recognised codecs then add these codecs to the supported list
+// and mark V4L2 as supported: We only need a single working device to enable
+// V4L2, when we come to decode FFmpeg will probe all the devices and choose
+// the appropriate one.
+void GfxInfo::V4L2ProbeDevice(nsCString& dev) {
+ char* v4l2Data = nullptr;
+ auto free = mozilla::MakeScopeExit([&] { g_free((void*)v4l2Data); });
+
+ int v4l2Pipe = -1;
+ int v4l2PID = 0;
+ const char* args[] = {"-d", dev.get(), nullptr};
+ v4l2PID = FireTestProcess(V4L2_PROBE_BINARY, &v4l2Pipe, args);
+ if (!v4l2PID) {
+ gfxCriticalNote << "Failed to start v4l2test process\n";
+ return;
+ }
+
+ if (!ManageChildProcess("v4l2test", &v4l2PID, &v4l2Pipe, V4L2_TEST_TIMEOUT,
+ &v4l2Data)) {
+ gfxCriticalNote << "v4l2test: ManageChildProcess failed\n";
+ return;
+ }
+
+ char* bufptr = v4l2Data;
+ char* line;
+ nsTArray<nsCString> capFormats;
+ nsTArray<nsCString> outFormats;
+ bool supported = false;
+ // Use gfxWarning rather than gfxCriticalNote from here on because the
+ // errors/warnings output by v4l2test are generally just caused by devices
+ // which aren't M2M decoders. Set gfx.logging.level=5 to see these messages.
+
+ while ((line = NS_strtok("\n", &bufptr))) {
+ if (!strcmp(line, "V4L2_SUPPORTED")) {
+ line = NS_strtok("\n", &bufptr);
+ if (!line) {
+ gfxWarning() << "v4l2test: Failed to get V4L2 support\n";
+ return;
+ }
+ supported = !strcmp(line, "TRUE");
+ } else if (!strcmp(line, "V4L2_CAPTURE_FMTS")) {
+ line = NS_strtok("\n", &bufptr);
+ if (!line) {
+ gfxWarning() << "v4l2test: Failed to get V4L2 CAPTURE formats\n";
+ return;
+ }
+ char* capture_fmt;
+ while ((capture_fmt = NS_strtok(" ", &line))) {
+ capFormats.AppendElement(capture_fmt);
+ }
+ } else if (!strcmp(line, "V4L2_OUTPUT_FMTS")) {
+ line = NS_strtok("\n", &bufptr);
+ if (!line) {
+ gfxWarning() << "v4l2test: Failed to get V4L2 OUTPUT formats\n";
+ return;
+ }
+ char* output_fmt;
+ while ((output_fmt = NS_strtok(" ", &line))) {
+ outFormats.AppendElement(output_fmt);
+ }
+ } else if (!strcmp(line, "WARNING") || !strcmp(line, "ERROR")) {
+ line = NS_strtok("\n", &bufptr);
+ if (line) {
+ gfxWarning() << "v4l2test: " << line << "\n";
+ }
+ return;
+ }
+ }
+
+ // If overall SUPPORTED flag is not TRUE then stop now
+ if (!supported) {
+ return;
+ }
+
+ // Currently the V4L2 decode platform only supports YUV420 and NV12
+ if (!capFormats.Contains("YV12") && !capFormats.Contains("NV12")) {
+ return;
+ }
+
+ // Supported codecs
+ if (outFormats.Contains("H264")) {
+ mIsV4L2Supported = Some(true);
+ media::MCSInfo::AddSupport(media::MediaCodecsSupport::H264HardwareDecode);
+ mV4L2SupportedCodecs |= CODEC_HW_H264;
+ }
+}
+
+const nsTArray<GfxDriverInfo>& GfxInfo::GetGfxDriverInfo() {
+ if (!sDriverInfo->Length()) {
+ // Mesa 10.0 provides the GLX_MESA_query_renderer extension, which allows us
+ // to query device IDs backing a GL context for blocklisting.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(10, 0, 0, 0), "FEATURE_FAILURE_OLD_MESA", "Mesa 10.0");
+
+ // NVIDIA Mesa baseline (see bug 1714391).
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaNouveau, DeviceFamily::All,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(11, 0, 0, 0), "FEATURE_FAILURE_OLD_NV_MESA", "Mesa 11.0");
+
+ // NVIDIA baseline (ported from old blocklist)
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(257, 21, 0, 0), "FEATURE_FAILURE_OLD_NVIDIA", "NVIDIA 257.21");
+
+ // fglrx baseline (chosen arbitrarily as 2013-07-22 release).
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Linux, DeviceFamily::AtiAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(13, 15, 100, 1), "FEATURE_FAILURE_OLD_FGLRX", "fglrx 13.15.100.1");
+
+ ////////////////////////////////////
+ // FEATURE_WEBRENDER
+
+ // All Mesa software drivers, they should get Software WebRender instead.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::SoftwareMesaAll, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_SOFTWARE_GL",
+ "");
+
+ // Older generation Intel devices do not perform well with WebRender.
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Linux, DeviceFamily::IntelWebRenderBlocked,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "INTEL_DEVICE_GEN5_OR_OLDER",
+ "");
+
+ // Nvidia Mesa baseline, see bug 1563859.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(18, 2, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA", "Mesa 18.2.0.0");
+
+ // Disable on all older Nvidia drivers due to stability issues.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, V(470, 82, 0, 0),
+ "FEATURE_FAILURE_WEBRENDER_OLD_NVIDIA", "470.82.0");
+
+ // Older generation NVIDIA devices do not perform well with WebRender.
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Linux, DeviceFamily::NvidiaWebRenderBlocked,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "NVIDIA_EARLY_TESLA_AND_C67_C68", "");
+
+ // Mesa baseline, chosen arbitrarily. Linux users are generally good about
+ // updating their Mesa libraries so we don't want to arbitarily support
+ // WebRender on old drivers with outstanding bugs to work around.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(17, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA", "Mesa 17.0.0.0");
+
+ // Mesa baseline for non-Intel/NVIDIA/ATI devices. These other devices will
+ // often have less mature drivers so let's block older Mesa versions.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaNonIntelNvidiaAtiAll,
+ DeviceFamily::All, nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(22, 2, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA_OTHER",
+ "Mesa 22.2.0.0");
+
+ // Bug 1690568 / Bug 1393793 - Require Mesa 17.3.0+ for devices using the
+ // AMD r600 driver to avoid shader compilation issues.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaR600, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(17, 3, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA_R600",
+ "Mesa 17.3.0.0");
+
+ // Disable on all ATI devices not using Mesa for now.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "FEATURE_FAILURE_WEBRENDER_NO_LINUX_ATI", "");
+
+ // Disable R600 GPUs with Mesa drivers.
+ // Bug 1673939 - Garbled text on RS880 GPUs with Mesa drivers.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AmdR600,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "FEATURE_FAILURE_WEBRENDER_BUG_1673939",
+ "https://gitlab.freedesktop.org/mesa/mesa/-/issues/3720");
+
+ // Bug 1635186 - Poor performance with video playing in a background window
+ // on XWayland. Keep in sync with FEATURE_X11_EGL below to only enable them
+ // together by default. Only Mesa and Nvidia binary drivers are expected
+ // on Wayland rigth now.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::XWayland, DriverVendor::MesaAll, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(21, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_BUG_1635186",
+ "Mesa 21.0.0.0");
+
+ // Bug 1815481 - Disable mesa drivers in virtual machines.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaVM, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "FEATURE_FAILURE_WEBRENDER_MESA_VM", "");
+ // Disable hardware mesa drivers in virtual machines due to instability.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaVM, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBGL_USE_HARDWARE,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_FAILURE_WEBGL_MESA_VM", "");
+
+ ////////////////////////////////////
+ // FEATURE_WEBRENDER_COMPOSITOR
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Linux, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_COMPOSITOR_DISABLED", "");
+
+ ////////////////////////////////////
+ // FEATURE_X11_EGL
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_LESS_THAN, V(17, 0, 0, 0), "FEATURE_X11_EGL_OLD_MESA",
+ "Mesa 17.0.0.0");
+
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_LESS_THAN, V(18, 2, 0, 0), "FEATURE_X11_EGL_OLD_MESA_NOUVEAU",
+ "Mesa 18.2.0.0");
+
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_LESS_THAN, V(470, 82, 0, 0),
+ "FEATURE_ROLLOUT_X11_EGL_NVIDIA_BINARY", "470.82.0");
+
+ // Disable on all AMD devices not using Mesa.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "FEATURE_FAILURE_X11_EGL_NO_LINUX_ATI", "");
+
+ ////////////////////////////////////
+ // FEATURE_DMABUF
+#ifdef EARLY_BETA_OR_EARLIER
+ // Disabled due to high volume crash tracked in bug 1788573, fixed in the
+ // 545 driver.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_DMABUF, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, V(545, 23, 6, 0), "FEATURE_FAILURE_BUG_1788573", "");
+#else
+ // Disabled due to high volume crash tracked in bug 1788573.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_DMABUF, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1788573",
+ "");
+#endif
+
+ ////////////////////////////////////
+ // FEATURE_DMABUF_SURFACE_EXPORT
+ // Disabled due to:
+ // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6666
+ // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6796
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", "");
+
+ // Disabled due to:
+ // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6688
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", "");
+
+ // Disabled due to:
+ // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6988
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::QualcommAll,
+ nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", "");
+
+ ////////////////////////////////////
+ // FEATURE_HARDWARE_VIDEO_DECODING
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(21, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_MESA",
+ "Mesa 21.0.0.0");
+
+ // Disable on all NVIDIA hardware
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::All, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_LINUX_NVIDIA", "");
+
+ // Disable on all AMD devices not using Mesa.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_LINUX_AMD", "");
+
+ // Disable on r600 driver due to decoding artifacts (Bug 1824307)
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaR600, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_R600", "");
+
+ // Disable on AMD devices using broken Mesa (Bug 1832080).
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN, V(23, 1, 1, 0),
+ "FEATURE_HARDWARE_VIDEO_DECODING_AMD_DISABLE", "Mesa 23.1.1.0");
+
+ // Disable on Release/late Beta on AMD
+#if !defined(EARLY_BETA_OR_EARLIER)
+ APPEND_TO_DRIVER_BLOCKLIST(OperatingSystem::Linux, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "FEATURE_HARDWARE_VIDEO_DECODING_DISABLE", "");
+#endif
+ ////////////////////////////////////
+ // FEATURE_HW_DECODED_VIDEO_ZERO_COPY - ALLOWLIST
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Linux, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_ALLOW_ALWAYS,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "FEATURE_ROLLOUT_ALL");
+
+ // Disable on AMD devices using broken Mesa (Bug 1837138).
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN, V(23, 1, 1, 0),
+ "FEATURE_HARDWARE_VIDEO_ZERO_COPY_LINUX_AMD_DISABLE", "Mesa 23.1.1.0");
+
+ ////////////////////////////////////
+ // FEATURE_WEBRENDER_PARTIAL_PRESENT
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::X11, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_WEBRENDER_PARTIAL_PRESENT,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_ROLLOUT_WR_PARTIAL_PRESENT_NVIDIA_BINARY", "");
+
+ ////////////////////////////////////
+
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::MesaNouveau, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_THREADSAFE_GL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0),
+ "FEATURE_FAILURE_THREADSAFE_GL_NOUVEAU", "");
+
+#ifdef EARLY_BETA_OR_EARLIER
+ // Disabled due to high volume crash tracked in bug 1788573, fixed in the
+ // 545 driver.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_THREADSAFE_GL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, V(545, 23, 6, 0), "FEATURE_FAILURE_BUG_1788573", "");
+#else
+ // Disabled due to high volume crash tracked in bug 1788573.
+ APPEND_TO_DRIVER_BLOCKLIST_EXT(
+ OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All,
+ WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_THREADSAFE_GL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1788573",
+ "");
+#endif
+
+ // AMD R600 family does not perform well with WebRender.
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Linux, DeviceFamily::AmdR600,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "AMD_R600_FAMILY", "");
+ }
+ return *sDriverInfo;
+}
+
+bool GfxInfo::DoesWindowProtocolMatch(const nsAString& aBlocklistWindowProtocol,
+ const nsAString& aWindowProtocol) {
+ if (mIsWayland &&
+ aBlocklistWindowProtocol.Equals(
+ GfxDriverInfo::GetWindowProtocol(WindowProtocol::WaylandAll),
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ if (!mIsWayland &&
+ aBlocklistWindowProtocol.Equals(
+ GfxDriverInfo::GetWindowProtocol(WindowProtocol::X11All),
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ return GfxInfoBase::DoesWindowProtocolMatch(aBlocklistWindowProtocol,
+ aWindowProtocol);
+}
+
+bool GfxInfo::DoesDriverVendorMatch(const nsAString& aBlocklistVendor,
+ const nsAString& aDriverVendor) {
+ if (mIsMesa) {
+ if (aBlocklistVendor.Equals(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::MesaAll),
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ if (mIsAccelerated &&
+ aBlocklistVendor.Equals(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::HardwareMesaAll),
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ if (!mIsAccelerated &&
+ aBlocklistVendor.Equals(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::SoftwareMesaAll),
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ if (aBlocklistVendor.Equals(GfxDriverInfo::GetDriverVendor(
+ DriverVendor::MesaNonIntelNvidiaAtiAll),
+ nsCaseInsensitiveStringComparator)) {
+ return !mVendorId.Equals("0x8086") && !mVendorId.Equals("0x10de") &&
+ !mVendorId.Equals("0x1002");
+ }
+ }
+ if (!mIsMesa && aBlocklistVendor.Equals(
+ GfxDriverInfo::GetDriverVendor(DriverVendor::NonMesaAll),
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+ return GfxInfoBase::DoesDriverVendorMatch(aBlocklistVendor, aDriverVendor);
+}
+
+nsresult GfxInfo::GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS /* = nullptr */)
+
+{
+ NS_ENSURE_ARG_POINTER(aStatus);
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ aSuggestedDriverVersion.SetIsVoid(true);
+ OperatingSystem os = OperatingSystem::Linux;
+ if (aOS) *aOS = os;
+
+ if (sShutdownOccurred) {
+ return NS_OK;
+ }
+
+ GetData();
+
+ if (mGlxTestError) {
+ // If glxtest failed, block most features by default.
+ if (OnlyAllowFeatureOnKnownConfig(aFeature)) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_GLXTEST_FAILED";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ if (mGLMajorVersion == 1) {
+ // We're on OpenGL 1. In most cases that indicates really old hardware.
+ // We better block them, rather than rely on them to fail gracefully,
+ // because they don't! see bug 696636
+ if (OnlyAllowFeatureOnKnownConfig(aFeature)) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_OPENGL_1";
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ // Blocklist software GL implementations from using layers acceleration.
+ // On the test infrastructure, we'll force-enable layers acceleration.
+ if (aFeature == nsIGfxInfo::FEATURE_OPENGL_LAYERS && !mIsAccelerated &&
+ !PR_GetEnv("MOZ_LAYERS_ALLOW_SOFTWARE_GL")) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_SOFTWARE_GL";
+ return NS_OK;
+ }
+
+ if (aFeature == nsIGfxInfo::FEATURE_WEBRENDER) {
+ // Don't try Webrender on devices where we are guaranteed to fail.
+ if (mGLMajorVersion < 3) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_OPENGL_LESS_THAN_3";
+ return NS_OK;
+ }
+
+ // Bug 1710400: Disable Webrender on the deprecated Intel DDX driver
+ for (const nsCString& driver : mDdxDrivers) {
+ if (strcasestr(driver.get(), "Intel")) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE;
+ aFailureId = "FEATURE_FAILURE_DDX_INTEL";
+ return NS_OK;
+ }
+ }
+ }
+
+ const struct {
+ int32_t mFeature;
+ int32_t mCodec;
+ } kFeatureToCodecs[] = {{nsIGfxInfo::FEATURE_H264_HW_DECODE, CODEC_HW_H264},
+ {nsIGfxInfo::FEATURE_VP8_HW_DECODE, CODEC_HW_VP8},
+ {nsIGfxInfo::FEATURE_VP9_HW_DECODE, CODEC_HW_VP9},
+ {nsIGfxInfo::FEATURE_AV1_HW_DECODE, CODEC_HW_AV1}};
+
+ for (const auto& pair : kFeatureToCodecs) {
+ if (aFeature != pair.mFeature) {
+ continue;
+ }
+ if ((mVAAPISupportedCodecs & pair.mCodec) ||
+ (mV4L2SupportedCodecs & pair.mCodec)) {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ } else {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_PLATFORM_TEST;
+ aFailureId = "FEATURE_FAILURE_VIDEO_DECODING_MISSING";
+ }
+ return NS_OK;
+ }
+
+ auto ret = GfxInfoBase::GetFeatureStatusImpl(
+ aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os);
+
+ // Probe VA-API/V4L2 on supported devices only
+ if (aFeature == nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING) {
+ if (!StaticPrefs::media_hardware_video_decoding_enabled_AtStartup()) {
+ return ret;
+ }
+ bool probeHWDecode =
+ mIsAccelerated &&
+ (*aStatus == nsIGfxInfo::FEATURE_STATUS_OK ||
+ StaticPrefs::media_hardware_video_decoding_force_enabled_AtStartup() ||
+ StaticPrefs::media_ffmpeg_vaapi_enabled_AtStartup());
+ if (probeHWDecode) {
+ GetDataVAAPI();
+ GetDataV4L2();
+ } else {
+ mIsVAAPISupported = Some(false);
+ mIsV4L2Supported = Some(false);
+ }
+ if (!mIsVAAPISupported.value() && !mIsV4L2Supported.value()) {
+ *aStatus = nsIGfxInfo::FEATURE_BLOCKED_PLATFORM_TEST;
+ aFailureId = "FEATURE_FAILURE_VIDEO_DECODING_TEST_FAILED";
+ }
+ }
+
+ return ret;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetD2DEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetDWriteEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP GfxInfo::GetHasBattery(bool* aHasBattery) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) {
+ GetData();
+ if (mIsWayland) {
+ aWindowProtocol = GfxDriverInfo::GetWindowProtocol(WindowProtocol::Wayland);
+ } else if (mIsXWayland) {
+ aWindowProtocol =
+ GfxDriverInfo::GetWindowProtocol(WindowProtocol::XWayland);
+ } else {
+ aWindowProtocol = GfxDriverInfo::GetWindowProtocol(WindowProtocol::X11);
+ }
+ Telemetry::ScalarSet(Telemetry::ScalarID::GFX_LINUX_WINDOW_PROTOCOL,
+ aWindowProtocol);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetTestType(nsAString& aTestType) {
+ GetData();
+ AppendASCIItoUTF16(mTestType, aTestType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) {
+ GetData();
+ AppendASCIItoUTF16(mAdapterDescription, aAdapterDescription);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) {
+ GetData();
+ *aAdapterRAM = mAdapterRAM;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) {
+ aAdapterDriver.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) {
+ GetData();
+ CopyASCIItoUTF16(mDriverVendor, aAdapterDriverVendor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) {
+ GetData();
+ CopyASCIItoUTF16(mDriverVersion, aAdapterDriverVersion);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) {
+ aAdapterDriverDate.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) {
+ GetData();
+ CopyUTF8toUTF16(mVendorId, aAdapterVendorID);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) {
+ GetData();
+ CopyUTF8toUTF16(mSecondaryVendorId, aAdapterVendorID);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) {
+ GetData();
+ CopyUTF8toUTF16(mDeviceId, aAdapterDeviceID);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) {
+ GetData();
+ CopyUTF8toUTF16(mSecondaryDeviceId, aAdapterDeviceID);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) {
+ // This is never the case, as the active GPU should be the primary GPU.
+ *aIsGPU2Active = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) {
+ GetData();
+ aDrmRenderDevice.Assign(mDrmRenderDevice);
+ return NS_OK;
+}
+
+#ifdef DEBUG
+
+// Implement nsIGfxInfoDebug
+// We don't support spoofing anything on Linux
+
+NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) {
+ GetData();
+ CopyUTF16toUTF8(aVendorID, mVendorId);
+ mIsAccelerated = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) {
+ GetData();
+ CopyUTF16toUTF8(aDeviceID, mDeviceId);
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) {
+ GetData();
+ CopyUTF16toUTF8(aDriverVersion, mDriverVersion);
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) {
+ // We don't support OS versioning on Linux. There's just "Linux".
+ return NS_OK;
+}
+
+#endif
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/GfxInfo.h b/widget/gtk/GfxInfo.h
new file mode 100644
index 0000000000..26b4554b4a
--- /dev/null
+++ b/widget/gtk/GfxInfo.h
@@ -0,0 +1,137 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef WIDGET_GTK_GFXINFO_h__
+#define WIDGET_GTK_GFXINFO_h__
+
+#include "GfxInfoBase.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace widget {
+
+class GfxInfo final : public GfxInfoBase {
+ public:
+ // We only declare the subset of nsIGfxInfo that we actually implement. The
+ // rest is brought forward from GfxInfoBase.
+ NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override;
+ NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override;
+ NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override;
+ NS_IMETHOD GetEmbeddedInFirefoxReality(
+ bool* aEmbeddedInFirefoxReality) override;
+ NS_IMETHOD GetHasBattery(bool* aHasBattery) override;
+ NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override;
+ NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override;
+ NS_IMETHOD GetTestType(nsAString& aTestType) override;
+ NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion2(
+ nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override;
+ NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override;
+ using GfxInfoBase::GetFeatureStatus;
+ using GfxInfoBase::GetFeatureSuggestedDriverVersion;
+
+ virtual nsresult Init() override;
+
+ NS_IMETHOD_(void) GetData() override;
+
+ static bool FireGLXTestProcess();
+
+#ifdef DEBUG
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIGFXINFODEBUG
+#endif
+
+ protected:
+ ~GfxInfo() = default;
+
+ OperatingSystem GetOperatingSystem() override {
+ return OperatingSystem::Linux;
+ }
+ virtual nsresult GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS = nullptr) override;
+ virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override;
+
+ virtual bool DoesWindowProtocolMatch(
+ const nsAString& aBlocklistWindowProtocol,
+ const nsAString& aWindowProtocol) override;
+
+ virtual bool DoesDriverVendorMatch(const nsAString& aBlocklistVendor,
+ const nsAString& aDriverVendor) override;
+ static int FireTestProcess(const nsAString& aBinaryFile, int* aOutPipe,
+ const char** aStringArgs);
+
+ private:
+ bool mInitialized = false;
+ nsCString mVendorId;
+ nsCString mDeviceId;
+ nsCString mDriverVendor;
+ nsCString mDriverVersion;
+ nsCString mAdapterDescription;
+ uint32_t mAdapterRAM;
+ nsCString mOS;
+ nsCString mOSRelease;
+ nsCString mTestType;
+
+ nsCString mSecondaryVendorId;
+ nsCString mSecondaryDeviceId;
+
+ nsCString mDrmRenderDevice;
+
+ nsTArray<nsCString> mDdxDrivers;
+
+ struct ScreenInfo {
+ uint32_t mWidth;
+ uint32_t mHeight;
+ bool mIsDefault;
+ };
+
+ nsTArray<ScreenInfo> mScreenInfo;
+ bool mHasTextureFromPixmap;
+ unsigned int mGLMajorVersion, mGLMinorVersion;
+ bool mIsMesa;
+ bool mIsAccelerated;
+ bool mIsWayland;
+ bool mIsXWayland;
+ bool mHasMultipleGPUs;
+ bool mGlxTestError;
+ mozilla::Maybe<bool> mIsVAAPISupported;
+ int mVAAPISupportedCodecs = 0;
+ mozilla::Maybe<bool> mIsV4L2Supported;
+ int mV4L2SupportedCodecs = 0;
+
+ static int sGLXTestPipe;
+ static pid_t sGLXTestPID;
+
+ void GetDataVAAPI();
+ void GetDataV4L2();
+ void V4L2ProbeDevice(nsCString& dev);
+ void AddCrashReportAnnotations();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* WIDGET_GTK_GFXINFO_h__ */
diff --git a/widget/gtk/GfxInfoUtils.h b/widget/gtk/GfxInfoUtils.h
new file mode 100644
index 0000000000..f4d96604e3
--- /dev/null
+++ b/widget/gtk/GfxInfoUtils.h
@@ -0,0 +1,98 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef WIDGET_GTK_GFXINFO_UTILS_h__
+#define WIDGET_GTK_GFXINFO_UTILS_h__
+
+// An alternative to mozilla::Unused for use in (a) C code and (b) code where
+// linking with unused.o is difficult.
+#define MOZ_UNUSED(expr) \
+ do { \
+ if (expr) { \
+ (void)0; \
+ } \
+ } while (0)
+
+#define LOG_PIPE 2
+
+static bool enable_logging = false;
+static void log(const char* format, ...) {
+ if (!enable_logging) {
+ return;
+ }
+ va_list args;
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+}
+
+static int output_pipe = 1;
+static void close_logging() {
+ // we want to redirect to /dev/null stdout, stderr, and while we're at it,
+ // any PR logging file descriptors. To that effect, we redirect all positive
+ // file descriptors up to what open() returns here. In particular, 1 is stdout
+ // and 2 is stderr.
+ int fd = open("/dev/null", O_WRONLY);
+ for (int i = 1; i < fd; i++) {
+ if (output_pipe != i) {
+ dup2(fd, i);
+ }
+ }
+ close(fd);
+}
+
+// C++ standard collides with C standard in that it doesn't allow casting void*
+// to function pointer types. So the work-around is to convert first to size_t.
+// http://www.trilithium.com/johan/2004/12/problem-with-dlsym/
+template <typename func_ptr_type>
+static func_ptr_type cast(void* ptr) {
+ return reinterpret_cast<func_ptr_type>(reinterpret_cast<size_t>(ptr));
+}
+
+#define BUFFER_SIZE_STEP 4000
+
+static char* test_buf = nullptr;
+static int test_bufsize = 0;
+static int test_length = 0;
+
+static void record_value(const char* format, ...) {
+ if (!test_buf || test_length + BUFFER_SIZE_STEP / 2 > test_bufsize) {
+ test_bufsize += BUFFER_SIZE_STEP;
+ test_buf = (char*)realloc(test_buf, test_bufsize);
+ }
+ int remaining = test_bufsize - test_length;
+
+ // Append the new values to the buffer, not to exceed the remaining space.
+ va_list args;
+ va_start(args, format);
+ int max_added = vsnprintf(test_buf + test_length, remaining, format, args);
+ va_end(args);
+
+ if (max_added >= remaining) {
+ test_length += remaining;
+ } else {
+ test_length += max_added;
+ }
+}
+
+#define record_error(str_, ...) record_value("ERROR\n" str_ "\n", ##__VA_ARGS__)
+#define record_warning(str_, ...) \
+ record_value("WARNING\n" str_ "\n", ##__VA_ARGS__)
+
+static void record_flush() {
+ if (!test_buf) {
+ return;
+ }
+ MOZ_UNUSED(write(output_pipe, test_buf, test_length));
+ if (enable_logging) {
+ MOZ_UNUSED(write(LOG_PIPE, test_buf, test_length));
+ }
+ free(test_buf);
+ test_buf = nullptr;
+}
+
+#endif /* WIDGET_GTK_GFXINFO_h__ */
diff --git a/widget/gtk/GtkCompositorWidget.cpp b/widget/gtk/GtkCompositorWidget.cpp
new file mode 100644
index 0000000000..a3443b9828
--- /dev/null
+++ b/widget/gtk/GtkCompositorWidget.cpp
@@ -0,0 +1,247 @@
+/* -*- 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 "GtkCompositorWidget.h"
+
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/widget/InProcessCompositorWidget.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "nsWindow.h"
+
+#ifdef MOZ_X11
+# include "mozilla/X11Util.h"
+#endif
+
+#ifdef MOZ_WAYLAND
+# include "mozilla/layers/NativeLayerWayland.h"
+#endif
+
+#ifdef MOZ_LOGGING
+# undef LOG
+# define LOG(str, ...) \
+ MOZ_LOG(IsPopup() ? gWidgetPopupLog : gWidgetLog, \
+ mozilla::LogLevel::Debug, \
+ ("[%p]: " str, mWidget.get(), ##__VA_ARGS__))
+#endif /* MOZ_LOGGING */
+
+namespace mozilla {
+namespace widget {
+
+GtkCompositorWidget::GtkCompositorWidget(
+ const GtkCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, RefPtr<nsWindow> aWindow)
+ : CompositorWidget(aOptions),
+ mWidget(std::move(aWindow)),
+ mClientSize(LayoutDeviceIntSize(aInitData.InitialClientSize()),
+ "GtkCompositorWidget::mClientSize") {
+#if defined(MOZ_X11)
+ if (GdkIsX11Display()) {
+ mXWindow = (Window)aInitData.XWindow();
+ ConfigureX11Backend(mXWindow, aInitData.Shaped());
+ LOG("GtkCompositorWidget::GtkCompositorWidget() [%p] mXWindow %p "
+ "mIsRenderingSuspended %d\n",
+ (void*)mWidget.get(), (void*)mXWindow, !!mIsRenderingSuspended);
+ }
+#endif
+#if defined(MOZ_WAYLAND)
+ if (GdkIsWaylandDisplay()) {
+ ConfigureWaylandBackend();
+ LOG("GtkCompositorWidget::GtkCompositorWidget() [%p] mWidget %p "
+ "mIsRenderingSuspended %d\n",
+ (void*)mWidget.get(), (void*)mWidget, !!mIsRenderingSuspended);
+ }
+#endif
+}
+
+GtkCompositorWidget::~GtkCompositorWidget() {
+ LOG("GtkCompositorWidget::~GtkCompositorWidget [%p]\n", (void*)mWidget.get());
+ DisableRendering();
+ RefPtr<nsIWidget> widget = mWidget.forget();
+ NS_ReleaseOnMainThread("GtkCompositorWidget::mWidget", widget.forget());
+}
+
+already_AddRefed<gfx::DrawTarget> GtkCompositorWidget::StartRemoteDrawing() {
+ return nullptr;
+}
+void GtkCompositorWidget::EndRemoteDrawing() {}
+
+already_AddRefed<gfx::DrawTarget>
+GtkCompositorWidget::StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) {
+ return mProvider.StartRemoteDrawingInRegion(aInvalidRegion, aBufferMode);
+}
+
+void GtkCompositorWidget::EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
+ mProvider.EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion);
+}
+
+nsIWidget* GtkCompositorWidget::RealWidget() { return mWidget; }
+
+void GtkCompositorWidget::NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ LOG("GtkCompositorWidget::NotifyClientSizeChanged() to %d x %d",
+ aClientSize.width, aClientSize.height);
+
+ auto size = mClientSize.Lock();
+ *size = aClientSize;
+}
+
+LayoutDeviceIntSize GtkCompositorWidget::GetClientSize() {
+ auto size = mClientSize.Lock();
+ return *size;
+}
+
+void GtkCompositorWidget::RemoteLayoutSizeUpdated(
+ const LayoutDeviceRect& aSize) {
+ if (!mWidget || !mWidget->IsWaitingForCompositorResume()) {
+ return;
+ }
+
+ LOG("GtkCompositorWidget::RemoteLayoutSizeUpdated() %d x %d",
+ (int)aSize.width, (int)aSize.height);
+
+ // We're waiting for layout to match widget size.
+ auto clientSize = mClientSize.Lock();
+ if (clientSize->width != (int)aSize.width ||
+ clientSize->height != (int)aSize.height) {
+ LOG("quit, client size doesn't match (%d x %d)", clientSize->width,
+ clientSize->height);
+ return;
+ }
+
+ mWidget->ResumeCompositorFromCompositorThread();
+}
+
+EGLNativeWindowType GtkCompositorWidget::GetEGLNativeWindow() {
+ EGLNativeWindowType window = nullptr;
+ if (mWidget) {
+ window = (EGLNativeWindowType)mWidget->GetNativeData(NS_NATIVE_EGL_WINDOW);
+ }
+#if defined(MOZ_X11)
+ if (mXWindow) {
+ window = (EGLNativeWindowType)mXWindow;
+ }
+#endif
+ LOG("GtkCompositorWidget::GetEGLNativeWindow [%p] window %p\n", mWidget.get(),
+ window);
+ return window;
+}
+
+bool GtkCompositorWidget::SetEGLNativeWindowSize(
+ const LayoutDeviceIntSize& aEGLWindowSize) {
+#if defined(MOZ_WAYLAND)
+ if (mWidget) {
+ return mWidget->SetEGLNativeWindowSize(aEGLWindowSize);
+ }
+#endif
+ return true;
+}
+
+LayoutDeviceIntRegion GtkCompositorWidget::GetTransparentRegion() {
+ // We need to clear target buffer alpha values of popup windows as
+ // SW-WR paints with alpha blending (see Bug 1674473).
+ if (!mWidget || mWidget->IsPopup()) {
+ return LayoutDeviceIntRect(LayoutDeviceIntPoint(0, 0), GetClientSize());
+ }
+
+ // Clear background of titlebar area to render titlebar
+ // transparent corners correctly.
+ return mWidget->GetTitlebarRect();
+}
+
+#ifdef MOZ_WAYLAND
+RefPtr<mozilla::layers::NativeLayerRoot>
+GtkCompositorWidget::GetNativeLayerRoot() {
+ if (gfx::gfxVars::UseWebRenderCompositor()) {
+ if (!mNativeLayerRoot) {
+ MOZ_ASSERT(mWidget && mWidget->GetMozContainer());
+ mNativeLayerRoot = NativeLayerRootWayland::CreateForMozContainer(
+ mWidget->GetMozContainer());
+ }
+ return mNativeLayerRoot;
+ }
+ return nullptr;
+}
+#endif
+
+void GtkCompositorWidget::DisableRendering() {
+ LOG("GtkCompositorWidget::DisableRendering [%p]\n", (void*)mWidget.get());
+ mIsRenderingSuspended = true;
+ mProvider.CleanupResources();
+#if defined(MOZ_X11)
+ mXWindow = {};
+#endif
+}
+
+#if defined(MOZ_WAYLAND)
+bool GtkCompositorWidget::ConfigureWaylandBackend() {
+ mProvider.Initialize(this);
+ return true;
+}
+#endif
+
+#if defined(MOZ_X11)
+bool GtkCompositorWidget::ConfigureX11Backend(Window aXWindow, bool aShaped) {
+ mXWindow = aXWindow;
+
+ // We don't have X window yet.
+ if (!mXWindow) {
+ mIsRenderingSuspended = true;
+ return false;
+ }
+
+ // Grab the window's visual and depth
+ XWindowAttributes windowAttrs;
+ if (!XGetWindowAttributes(DefaultXDisplay(), mXWindow, &windowAttrs)) {
+ NS_WARNING("GtkCompositorWidget(): XGetWindowAttributes() failed!");
+ return false;
+ }
+
+ Visual* visual = windowAttrs.visual;
+ int depth = windowAttrs.depth;
+
+ // Initialize the window surface provider
+ mProvider.Initialize(mXWindow, visual, depth, aShaped);
+ return true;
+}
+#endif
+
+void GtkCompositorWidget::EnableRendering(const uintptr_t aXWindow,
+ const bool aShaped) {
+ LOG("GtkCompositorWidget::EnableRendering() [%p]\n", mWidget.get());
+
+ if (!mIsRenderingSuspended) {
+ LOG(" quit, mIsRenderingSuspended = false\n");
+ return;
+ }
+#if defined(MOZ_WAYLAND)
+ if (GdkIsWaylandDisplay()) {
+ LOG(" configure widget %p\n", mWidget.get());
+ if (!ConfigureWaylandBackend()) {
+ return;
+ }
+ }
+#endif
+#if defined(MOZ_X11)
+ if (GdkIsX11Display()) {
+ LOG(" configure XWindow %p shaped %d\n", (void*)aXWindow, aShaped);
+ if (!ConfigureX11Backend((Window)aXWindow, aShaped)) {
+ return;
+ }
+ }
+#endif
+ mIsRenderingSuspended = false;
+}
+#ifdef MOZ_LOGGING
+bool GtkCompositorWidget::IsPopup() {
+ return mWidget ? mWidget->IsPopup() : false;
+}
+#endif
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/GtkCompositorWidget.h b/widget/gtk/GtkCompositorWidget.h
new file mode 100644
index 0000000000..5bf89835d7
--- /dev/null
+++ b/widget/gtk/GtkCompositorWidget.h
@@ -0,0 +1,140 @@
+/* -*- 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/. */
+
+#ifndef widget_gtk_GtkCompositorWidget_h
+#define widget_gtk_GtkCompositorWidget_h
+
+#include "GLDefs.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/widget/CompositorWidget.h"
+#include "WindowSurfaceProvider.h"
+
+class nsIWidget;
+class nsWindow;
+
+namespace mozilla {
+
+namespace layers {
+class NativeLayerRootWayland;
+} // namespace layers
+
+namespace widget {
+
+class PlatformCompositorWidgetDelegate : public CompositorWidgetDelegate {
+ public:
+ virtual void NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) = 0;
+ virtual GtkCompositorWidget* AsGtkCompositorWidget() { return nullptr; };
+
+ virtual void DisableRendering() = 0;
+ virtual void EnableRendering(const uintptr_t aXWindow,
+ const bool aShaped) = 0;
+
+ // CompositorWidgetDelegate Overrides
+
+ PlatformCompositorWidgetDelegate* AsPlatformSpecificDelegate() override {
+ return this;
+ }
+};
+
+class GtkCompositorWidgetInitData;
+
+class GtkCompositorWidget : public CompositorWidget,
+ public PlatformCompositorWidgetDelegate {
+ public:
+ GtkCompositorWidget(const GtkCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions,
+ RefPtr<nsWindow> aWindow /* = nullptr*/);
+ ~GtkCompositorWidget();
+
+ // CompositorWidget Overrides
+
+ already_AddRefed<gfx::DrawTarget> StartRemoteDrawing() override;
+ void EndRemoteDrawing() override;
+
+ already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) override;
+ void EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget,
+ const LayoutDeviceIntRegion& aInvalidRegion) override;
+
+ LayoutDeviceIntSize GetClientSize() override;
+ void RemoteLayoutSizeUpdated(const LayoutDeviceRect& aSize);
+
+ nsIWidget* RealWidget() override;
+ GtkCompositorWidget* AsGTK() override { return this; }
+ CompositorWidgetDelegate* AsDelegate() override { return this; }
+
+ EGLNativeWindowType GetEGLNativeWindow();
+
+ LayoutDeviceIntRegion GetTransparentRegion() override;
+
+ // Suspend rendering of this remote widget and clear all resources.
+ // Can be used when underlying window is hidden/unmapped.
+ void DisableRendering() override;
+
+ // Resume rendering with to given aXWindow (X11) or nsWindow (Wayland).
+ void EnableRendering(const uintptr_t aXWindow, const bool aShaped) override;
+
+ // If we fail to set window size (due to different screen scale or so)
+ // we can't paint the frame by compositor.
+ bool SetEGLNativeWindowSize(const LayoutDeviceIntSize& aEGLWindowSize);
+
+#if defined(MOZ_X11)
+ Window XWindow() const { return mXWindow; }
+#endif
+#if defined(MOZ_WAYLAND)
+ RefPtr<mozilla::layers::NativeLayerRoot> GetNativeLayerRoot() override;
+#endif
+
+ bool PreRender(WidgetRenderingContext* aContext) override {
+ return !mIsRenderingSuspended;
+ }
+ bool IsHidden() const override { return mIsRenderingSuspended; }
+
+ // PlatformCompositorWidgetDelegate Overrides
+
+ void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override;
+ GtkCompositorWidget* AsGtkCompositorWidget() override { return this; }
+
+ private:
+#if defined(MOZ_WAYLAND)
+ bool ConfigureWaylandBackend();
+#endif
+#if defined(MOZ_X11)
+ bool ConfigureX11Backend(Window aXWindow, bool aShaped);
+#endif
+#ifdef MOZ_LOGGING
+ bool IsPopup();
+#endif
+
+ protected:
+ RefPtr<nsWindow> mWidget;
+
+ private:
+ // This field is written to on the main thread and read from on the compositor
+ // or renderer thread. During window resizing, this is subject to a (largely
+ // benign) read/write race, see bug 1665726. The DataMutex doesn't prevent the
+ // read/write race, but it does make it Not Undefined Behaviour, and also
+ // ensures we only ever use the old or new size, and not some weird synthesis
+ // of the two.
+ DataMutex<LayoutDeviceIntSize> mClientSize;
+
+ WindowSurfaceProvider mProvider;
+
+#if defined(MOZ_X11)
+ Window mXWindow = {};
+#endif
+#ifdef MOZ_WAYLAND
+ RefPtr<mozilla::layers::NativeLayerRootWayland> mNativeLayerRoot;
+#endif
+ Atomic<bool> mIsRenderingSuspended{true};
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_gtk_GtkCompositorWidget_h
diff --git a/widget/gtk/IMContextWrapper.cpp b/widget/gtk/IMContextWrapper.cpp
new file mode 100644
index 0000000000..fc87acbf86
--- /dev/null
+++ b/widget/gtk/IMContextWrapper.cpp
@@ -0,0 +1,3358 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=4 et sw=2 tw=80: */
+/* 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 "mozilla/Logging.h"
+#include "nsString.h"
+#include "prtime.h"
+#include "prenv.h"
+
+#include "IMContextWrapper.h"
+
+#include "GRefPtr.h"
+#include "nsGtkKeyUtils.h"
+#include "nsWindow.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/Likely.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
+#include "mozilla/WritingModes.h"
+
+// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync`
+// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gIMELog("IMEHandler");
+
+namespace mozilla {
+namespace widget {
+
+static inline const char* ToChar(bool aBool) {
+ return aBool ? "true" : "false";
+}
+
+static const char* GetEventType(GdkEventKey* aKeyEvent) {
+ switch (aKeyEvent->type) {
+ case GDK_KEY_PRESS:
+ return "GDK_KEY_PRESS";
+ case GDK_KEY_RELEASE:
+ return "GDK_KEY_RELEASE";
+ default:
+ return "Unknown";
+ }
+}
+
+class GetEventStateName : public nsAutoCString {
+ public:
+ explicit GetEventStateName(guint aState,
+ IMContextWrapper::IMContextID aIMContextID =
+ IMContextWrapper::IMContextID::Unknown) {
+ if (aState & GDK_SHIFT_MASK) {
+ AppendModifier("shift");
+ }
+ if (aState & GDK_CONTROL_MASK) {
+ AppendModifier("control");
+ }
+ if (aState & GDK_MOD1_MASK) {
+ AppendModifier("mod1");
+ }
+ if (aState & GDK_MOD2_MASK) {
+ AppendModifier("mod2");
+ }
+ if (aState & GDK_MOD3_MASK) {
+ AppendModifier("mod3");
+ }
+ if (aState & GDK_MOD4_MASK) {
+ AppendModifier("mod4");
+ }
+ if (aState & GDK_MOD4_MASK) {
+ AppendModifier("mod5");
+ }
+ if (aState & GDK_MOD4_MASK) {
+ AppendModifier("mod5");
+ }
+ switch (aIMContextID) {
+ case IMContextWrapper::IMContextID::IBus:
+ static const guint IBUS_HANDLED_MASK = 1 << 24;
+ static const guint IBUS_IGNORED_MASK = 1 << 25;
+ if (aState & IBUS_HANDLED_MASK) {
+ AppendModifier("IBUS_HANDLED_MASK");
+ }
+ if (aState & IBUS_IGNORED_MASK) {
+ AppendModifier("IBUS_IGNORED_MASK");
+ }
+ break;
+ case IMContextWrapper::IMContextID::Fcitx:
+ case IMContextWrapper::IMContextID::Fcitx5:
+ static const guint FcitxKeyState_HandledMask = 1 << 24;
+ static const guint FcitxKeyState_IgnoredMask = 1 << 25;
+ if (aState & FcitxKeyState_HandledMask) {
+ AppendModifier("FcitxKeyState_HandledMask");
+ }
+ if (aState & FcitxKeyState_IgnoredMask) {
+ AppendModifier("FcitxKeyState_IgnoredMask");
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private:
+ void AppendModifier(const char* aModifierName) {
+ if (!IsEmpty()) {
+ AppendLiteral(" + ");
+ }
+ Append(aModifierName);
+ }
+};
+
+class GetTextRangeStyleText final : public nsAutoCString {
+ public:
+ explicit GetTextRangeStyleText(const TextRangeStyle& aStyle) {
+ if (!aStyle.IsDefined()) {
+ AssignLiteral("{ IsDefined()=false }");
+ return;
+ }
+
+ if (aStyle.IsLineStyleDefined()) {
+ AppendLiteral("{ mLineStyle=");
+ AppendLineStyle(aStyle.mLineStyle);
+ if (aStyle.IsUnderlineColorDefined()) {
+ AppendLiteral(", mUnderlineColor=");
+ AppendColor(aStyle.mUnderlineColor);
+ } else {
+ AppendLiteral(", IsUnderlineColorDefined=false");
+ }
+ } else {
+ AppendLiteral("{ IsLineStyleDefined()=false");
+ }
+
+ if (aStyle.IsForegroundColorDefined()) {
+ AppendLiteral(", mForegroundColor=");
+ AppendColor(aStyle.mForegroundColor);
+ } else {
+ AppendLiteral(", IsForegroundColorDefined()=false");
+ }
+
+ if (aStyle.IsBackgroundColorDefined()) {
+ AppendLiteral(", mBackgroundColor=");
+ AppendColor(aStyle.mBackgroundColor);
+ } else {
+ AppendLiteral(", IsBackgroundColorDefined()=false");
+ }
+
+ AppendLiteral(" }");
+ }
+ void AppendLineStyle(TextRangeStyle::LineStyle aLineStyle) {
+ switch (aLineStyle) {
+ case TextRangeStyle::LineStyle::None:
+ AppendLiteral("LineStyle::None");
+ break;
+ case TextRangeStyle::LineStyle::Solid:
+ AppendLiteral("LineStyle::Solid");
+ break;
+ case TextRangeStyle::LineStyle::Dotted:
+ AppendLiteral("LineStyle::Dotted");
+ break;
+ case TextRangeStyle::LineStyle::Dashed:
+ AppendLiteral("LineStyle::Dashed");
+ break;
+ case TextRangeStyle::LineStyle::Double:
+ AppendLiteral("LineStyle::Double");
+ break;
+ case TextRangeStyle::LineStyle::Wavy:
+ AppendLiteral("LineStyle::Wavy");
+ break;
+ default:
+ AppendPrintf("Invalid(0x%02X)",
+ static_cast<TextRangeStyle::LineStyleType>(aLineStyle));
+ break;
+ }
+ }
+ void AppendColor(nscolor aColor) {
+ AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }", NS_GET_R(aColor),
+ NS_GET_G(aColor), NS_GET_B(aColor), NS_GET_A(aColor));
+ }
+ virtual ~GetTextRangeStyleText() = default;
+};
+
+const static bool kUseSimpleContextDefault = false;
+
+/******************************************************************************
+ * SelectionStyleProvider
+ *
+ * IME (e.g., fcitx, ~4.2.8.3) may look up selection colors of widget, which
+ * is related to the window associated with the IM context, to support any
+ * colored widgets. Our editor (like <input type="text">) is rendered as
+ * native GtkTextView as far as possible by default and if editor color is
+ * changed by web apps, nsTextFrame may swap background color of foreground
+ * color of composition string for making composition string is always
+ * visually distinct in normal text.
+ *
+ * So, we would like IME to set style of composition string to good colors
+ * in GtkTextView. Therefore, this class overwrites selection colors of
+ * our widget with selection colors of GtkTextView so that it's possible IME
+ * to refer selection colors of GtkTextView via our widget.
+ ******************************************************************************/
+
+static Maybe<nscolor> GetSystemColor(LookAndFeel::ColorID aId) {
+ return LookAndFeel::GetColor(aId, LookAndFeel::ColorScheme::Light,
+ LookAndFeel::UseStandins::No);
+}
+
+class SelectionStyleProvider final {
+ public:
+ static SelectionStyleProvider* GetExistingInstance() { return sInstance; }
+
+ static SelectionStyleProvider* GetInstance() {
+ if (sHasShutDown) {
+ return nullptr;
+ }
+ if (!sInstance) {
+ sInstance = new SelectionStyleProvider();
+ }
+ return sInstance;
+ }
+
+ static void Shutdown() {
+ if (sInstance) {
+ g_object_unref(sInstance->mProvider);
+ }
+ delete sInstance;
+ sInstance = nullptr;
+ sHasShutDown = true;
+ }
+
+ // mContainer associated with an IM context.
+ void AttachTo(MozContainer* aContainer) {
+ gtk_style_context_add_provider(
+ gtk_widget_get_style_context(GTK_WIDGET(aContainer)),
+ GTK_STYLE_PROVIDER(mProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ void OnThemeChanged() {
+ // fcitx refers GtkStyle::text[GTK_STATE_SELECTED] and
+ // GtkStyle::bg[GTK_STATE_SELECTED] (although pair of text and *base*
+ // or *fg* and bg is correct). gtk_style_update_from_context() will
+ // set these colors using the widget's GtkStyleContext and so the
+ // colors can be controlled by a ":selected" CSS rule.
+ nsAutoCString style(":selected{");
+ // FYI: LookAndFeel always returns selection colors of GtkTextView.
+ if (auto selectionForegroundColor =
+ GetSystemColor(LookAndFeel::ColorID::Highlight)) {
+ double alpha =
+ static_cast<double>(NS_GET_A(*selectionForegroundColor)) / 0xFF;
+ style.AppendPrintf("color:rgba(%u,%u,%u,",
+ NS_GET_R(*selectionForegroundColor),
+ NS_GET_G(*selectionForegroundColor),
+ NS_GET_B(*selectionForegroundColor));
+ // We can't use AppendPrintf here, because it does locale-specific
+ // formatting of floating-point values.
+ style.AppendFloat(alpha);
+ style.AppendPrintf(");");
+ }
+ if (auto selectionBackgroundColor =
+ GetSystemColor(LookAndFeel::ColorID::Highlighttext)) {
+ double alpha =
+ static_cast<double>(NS_GET_A(*selectionBackgroundColor)) / 0xFF;
+ style.AppendPrintf("background-color:rgba(%u,%u,%u,",
+ NS_GET_R(*selectionBackgroundColor),
+ NS_GET_G(*selectionBackgroundColor),
+ NS_GET_B(*selectionBackgroundColor));
+ style.AppendFloat(alpha);
+ style.AppendPrintf(");");
+ }
+ style.AppendLiteral("}");
+ gtk_css_provider_load_from_data(mProvider, style.get(), -1, nullptr);
+ }
+
+ private:
+ static SelectionStyleProvider* sInstance;
+ static bool sHasShutDown;
+ GtkCssProvider* const mProvider;
+
+ SelectionStyleProvider() : mProvider(gtk_css_provider_new()) {
+ OnThemeChanged();
+ }
+};
+
+SelectionStyleProvider* SelectionStyleProvider::sInstance = nullptr;
+bool SelectionStyleProvider::sHasShutDown = false;
+
+/******************************************************************************
+ * IMContextWrapper
+ ******************************************************************************/
+
+IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr;
+guint16 IMContextWrapper::sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
+bool IMContextWrapper::sUseSimpleContext;
+
+NS_IMPL_ISUPPORTS(IMContextWrapper, TextEventDispatcherListener,
+ nsISupportsWeakReference)
+
+IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow)
+ : mOwnerWindow(aOwnerWindow),
+ mLastFocusedWindow(nullptr),
+ mContext(nullptr),
+ mSimpleContext(nullptr),
+ mDummyContext(nullptr),
+ mComposingContext(nullptr),
+ mCompositionStart(UINT32_MAX),
+ mProcessingKeyEvent(nullptr),
+ mCompositionState(eCompositionState_NotComposing),
+ mIMContextID(IMContextID::Unknown),
+ mFallbackToKeyEvent(false),
+ mKeyboardEventWasDispatched(false),
+ mKeyboardEventWasConsumed(false),
+ mIsDeletingSurrounding(false),
+ mLayoutChanged(false),
+ mSetCursorPositionOnKeyEvent(true),
+ mPendingResettingIMContext(false),
+ mRetrieveSurroundingSignalReceived(false),
+ mMaybeInDeadKeySequence(false),
+ mIsIMInAsyncKeyHandlingMode(false),
+ mSetInputPurposeAndInputHints(false) {
+ static bool sFirstInstance = true;
+ if (sFirstInstance) {
+ sFirstInstance = false;
+ sUseSimpleContext =
+ Preferences::GetBool("intl.ime.use_simple_context_on_password_field",
+ kUseSimpleContextDefault);
+ }
+ Init();
+}
+
+static bool IsIBusInSyncMode() {
+ // See ibus_im_context_class_init() in client/gtk2/ibusimcontext.c
+ // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L610
+ const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE");
+
+ // See _get_boolean_env() in client/gtk2/ibusimcontext.c
+ // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L520-L537
+ if (!env) {
+ return false;
+ }
+ nsDependentCString envStr(env);
+ if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
+ envStr.EqualsLiteral("false") || envStr.EqualsLiteral("False") ||
+ envStr.EqualsLiteral("FALSE")) {
+ return false;
+ }
+ return true;
+}
+
+static bool GetFcitxBoolEnv(const char* aEnv) {
+ // See fcitx_utils_get_boolean_env in src/lib/fcitx-utils/utils.c
+ // https://github.com/fcitx/fcitx/blob/0c87840dc7d9460c2cb5feaeefec299d0d3d62ec/src/lib/fcitx-utils/utils.c#L721-L736
+ const char* env = PR_GetEnv(aEnv);
+ if (!env) {
+ return false;
+ }
+ nsDependentCString envStr(env);
+ if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
+ envStr.EqualsLiteral("false")) {
+ return false;
+ }
+ return true;
+}
+
+static bool IsFcitxInSyncMode() {
+ // See fcitx_im_context_class_init() in src/frontend/gtk2/fcitximcontext.c
+ // https://github.com/fcitx/fcitx/blob/78b98d9230dc9630e99d52e3172bdf440ffd08c4/src/frontend/gtk2/fcitximcontext.c#L395-L398
+ return GetFcitxBoolEnv("IBUS_ENABLE_SYNC_MODE") ||
+ GetFcitxBoolEnv("FCITX_ENABLE_SYNC_MODE");
+}
+
+nsDependentCSubstring IMContextWrapper::GetIMName() const {
+ const char* contextIDChar =
+ gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext));
+ if (!contextIDChar) {
+ return nsDependentCSubstring();
+ }
+
+ nsDependentCSubstring im(contextIDChar, strlen(contextIDChar));
+
+ // If the context is XIM, actual engine must be specified with
+ // |XMODIFIERS=@im=foo|.
+ const char* xmodifiersChar = PR_GetEnv("XMODIFIERS");
+ if (!xmodifiersChar || !im.EqualsLiteral("xim")) {
+ return im;
+ }
+
+ nsDependentCString xmodifiers(xmodifiersChar);
+ int32_t atIMValueStart = xmodifiers.Find("@im=") + 4;
+ if (atIMValueStart < 4 ||
+ xmodifiers.Length() <= static_cast<size_t>(atIMValueStart)) {
+ return im;
+ }
+
+ int32_t atIMValueEnd = xmodifiers.Find("@", atIMValueStart);
+ if (atIMValueEnd > atIMValueStart) {
+ return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
+ atIMValueEnd - atIMValueStart);
+ }
+
+ if (atIMValueEnd == kNotFound) {
+ return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
+ strlen(xmodifiersChar) - atIMValueStart);
+ }
+
+ return im;
+}
+
+void IMContextWrapper::Init() {
+ // Overwrite selection colors of the window before associating the window
+ // with IM context since IME may look up selection colors via IM context
+ // to support any colored widgets.
+ SelectionStyleProvider::GetInstance()->AttachTo(
+ mOwnerWindow->GetMozContainer());
+
+ // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
+ // So, we don't need to check the result.
+
+ // Normal context.
+ mContext = gtk_im_multicontext_new();
+ g_signal_connect(mContext, "preedit_changed",
+ G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback),
+ this);
+ g_signal_connect(mContext, "retrieve_surrounding",
+ G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback),
+ this);
+ g_signal_connect(mContext, "delete_surrounding",
+ G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback),
+ this);
+ g_signal_connect(mContext, "commit",
+ G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback),
+ this);
+ g_signal_connect(mContext, "preedit_start",
+ G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
+ this);
+ g_signal_connect(mContext, "preedit_end",
+ G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
+ this);
+ nsDependentCSubstring im = GetIMName();
+ if (im.EqualsLiteral("ibus")) {
+ mIMContextID = IMContextID::IBus;
+ mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode();
+ // Although ibus has key snooper mode, it's forcibly disabled on Firefox
+ // in default settings by its whitelist since we always send key events
+ // to IME before handling shortcut keys. The whitelist can be
+ // customized with env, IBUS_NO_SNOOPER_APPS, but we don't need to
+ // support such rare cases for reducing maintenance cost.
+ mIsKeySnooped = false;
+ } else if (im.EqualsLiteral("fcitx")) {
+ mIMContextID = IMContextID::Fcitx;
+ mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode();
+ // Although Fcitx has key snooper mode similar to ibus, it's also
+ // disabled on Firefox in default settings by its whitelist. The
+ // whitelist can be customized with env, IBUS_NO_SNOOPER_APPS or
+ // FCITX_NO_SNOOPER_APPS, but we don't need to support such rare cases
+ // for reducing maintenance cost.
+ mIsKeySnooped = false;
+ } else if (im.EqualsLiteral("fcitx5")) {
+ mIMContextID = IMContextID::Fcitx5;
+ mIsIMInAsyncKeyHandlingMode = true; // does not have sync mode.
+ mIsKeySnooped = false; // never use key snooper.
+ } else if (im.EqualsLiteral("uim")) {
+ mIMContextID = IMContextID::Uim;
+ mIsIMInAsyncKeyHandlingMode = false;
+ // We cannot know if uim uses key snooper since it's build option of
+ // uim. Therefore, we need to retrieve the consideration from the
+ // pref for making users and distributions allowed to choose their
+ // preferred value.
+ mIsKeySnooped =
+ Preferences::GetBool("intl.ime.hack.uim.using_key_snooper", true);
+ } else if (im.EqualsLiteral("scim")) {
+ mIMContextID = IMContextID::Scim;
+ mIsIMInAsyncKeyHandlingMode = false;
+ mIsKeySnooped = false;
+ } else if (im.EqualsLiteral("iiim")) {
+ mIMContextID = IMContextID::IIIMF;
+ mIsIMInAsyncKeyHandlingMode = false;
+ mIsKeySnooped = false;
+ } else if (im.EqualsLiteral("wayland")) {
+ mIMContextID = IMContextID::Wayland;
+ mIsIMInAsyncKeyHandlingMode = false;
+ mIsKeySnooped = true;
+ } else {
+ mIMContextID = IMContextID::Unknown;
+ mIsIMInAsyncKeyHandlingMode = false;
+ mIsKeySnooped = false;
+ }
+
+ // Simple context
+ if (sUseSimpleContext) {
+ mSimpleContext = gtk_im_context_simple_new();
+ g_signal_connect(mSimpleContext, "preedit_changed",
+ G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback),
+ this);
+ g_signal_connect(
+ mSimpleContext, "retrieve_surrounding",
+ G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback), this);
+ g_signal_connect(mSimpleContext, "delete_surrounding",
+ G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback),
+ this);
+ g_signal_connect(mSimpleContext, "commit",
+ G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback),
+ this);
+ g_signal_connect(mSimpleContext, "preedit_start",
+ G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
+ this);
+ g_signal_connect(mSimpleContext, "preedit_end",
+ G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
+ this);
+ }
+
+ // Dummy context
+ mDummyContext = gtk_im_multicontext_new();
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p Init(), mOwnerWindow=%p, mContext=%p (im=\"%s\"), "
+ "mIsIMInAsyncKeyHandlingMode=%s, mIsKeySnooped=%s, "
+ "mSimpleContext=%p, mDummyContext=%p, "
+ "gtk_im_multicontext_get_context_id()=\"%s\", "
+ "PR_GetEnv(\"XMODIFIERS\")=\"%s\"",
+ this, mOwnerWindow, mContext, nsAutoCString(im).get(),
+ ToChar(mIsIMInAsyncKeyHandlingMode), ToChar(mIsKeySnooped),
+ mSimpleContext, mDummyContext,
+ gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)),
+ PR_GetEnv("XMODIFIERS")));
+}
+
+/* static */
+void IMContextWrapper::Shutdown() { SelectionStyleProvider::Shutdown(); }
+
+IMContextWrapper::~IMContextWrapper() {
+ MOZ_ASSERT(!mContext);
+ MOZ_ASSERT(!mComposingContext);
+ if (this == sLastFocusedContext) {
+ sLastFocusedContext = nullptr;
+ }
+ MOZ_LOG(gIMELog, LogLevel::Info, ("0x%p ~IMContextWrapper()", this));
+}
+
+void IMContextWrapper::SetGdkWindow(GdkWindow* aGdkWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p GdkWindowChanged(%p)", this, aGdkWindow));
+ MOZ_ASSERT(!aGdkWindow || mOwnerWindow->GetGdkWindow() == aGdkWindow);
+ gtk_im_context_set_client_window(mContext, aGdkWindow);
+ if (mSimpleContext) {
+ gtk_im_context_set_client_window(mSimpleContext, aGdkWindow);
+ }
+ gtk_im_context_set_client_window(mDummyContext, aGdkWindow);
+}
+
+NS_IMETHODIMP
+IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) {
+ switch (aNotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ case REQUEST_TO_CANCEL_COMPOSITION: {
+ nsWindow* window =
+ static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
+ return IsComposing() ? EndIMEComposition(window) : NS_OK;
+ }
+ case NOTIFY_IME_OF_FOCUS:
+ OnFocusChangeInGecko(true);
+ return NS_OK;
+ case NOTIFY_IME_OF_BLUR:
+ OnFocusChangeInGecko(false);
+ return NS_OK;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ OnLayoutChange();
+ return NS_OK;
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ OnUpdateComposition();
+ return NS_OK;
+ case NOTIFY_IME_OF_SELECTION_CHANGE: {
+ nsWindow* window =
+ static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
+ OnSelectionChange(window, aNotification);
+ return NS_OK;
+ }
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+NS_IMETHODIMP_(void)
+IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
+ // XXX When input transaction is being stolen by add-on, what should we do?
+}
+
+NS_IMETHODIMP_(void)
+IMContextWrapper::WillDispatchKeyboardEvent(
+ TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
+ void* aData) {
+ KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent,
+ static_cast<GdkEventKey*>(aData));
+}
+
+TextEventDispatcher* IMContextWrapper::GetTextEventDispatcher() {
+ if (NS_WARN_IF(!mLastFocusedWindow)) {
+ return nullptr;
+ }
+ TextEventDispatcher* dispatcher =
+ mLastFocusedWindow->GetTextEventDispatcher();
+ // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr.
+ MOZ_RELEASE_ASSERT(dispatcher);
+ return dispatcher;
+}
+
+NS_IMETHODIMP_(IMENotificationRequests)
+IMContextWrapper::GetIMENotificationRequests() {
+ IMENotificationRequests::Notifications notifications =
+ IMENotificationRequests::NOTIFY_NOTHING;
+ // If it's not enabled, we don't need position change notification.
+ if (IsEnabled()) {
+ notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE;
+ }
+ return IMENotificationRequests(notifications);
+}
+
+void IMContextWrapper::OnDestroyWindow(nsWindow* aWindow) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
+ "mOwnerWindow=0x%p, mLastFocusedModule=0x%p",
+ this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext));
+
+ MOZ_ASSERT(aWindow, "aWindow must not be null");
+
+ if (mLastFocusedWindow == aWindow) {
+ if (IsComposing()) {
+ EndIMEComposition(aWindow);
+ }
+ NotifyIMEOfFocusChange(IMEFocusState::Blurred);
+ mLastFocusedWindow = nullptr;
+ }
+
+ if (mOwnerWindow != aWindow) {
+ return;
+ }
+
+ if (sLastFocusedContext == this) {
+ sLastFocusedContext = nullptr;
+ }
+
+ /**
+ * NOTE:
+ * The given window is the owner of this, so, we must disconnect from the
+ * contexts now. But that might be referred from other nsWindows
+ * (they are children of this. But we don't know why there are the
+ * cases). So, we need to clear the pointers that refers to contexts
+ * and this if the other referrers are still alive. See bug 349727.
+ */
+ if (mContext) {
+ PrepareToDestroyContext(mContext);
+ gtk_im_context_set_client_window(mContext, nullptr);
+ g_signal_handlers_disconnect_by_data(mContext, this);
+ g_object_unref(mContext);
+ mContext = nullptr;
+ }
+
+ if (mSimpleContext) {
+ gtk_im_context_set_client_window(mSimpleContext, nullptr);
+ g_signal_handlers_disconnect_by_data(mSimpleContext, this);
+ g_object_unref(mSimpleContext);
+ mSimpleContext = nullptr;
+ }
+
+ if (mDummyContext) {
+ // mContext and mDummyContext have the same slaveType and signal_data
+ // so no need for another workaround_gtk_im_display_closed.
+ gtk_im_context_set_client_window(mDummyContext, nullptr);
+ g_object_unref(mDummyContext);
+ mDummyContext = nullptr;
+ }
+
+ if (NS_WARN_IF(mComposingContext)) {
+ g_object_unref(mComposingContext);
+ mComposingContext = nullptr;
+ }
+
+ mOwnerWindow = nullptr;
+ mLastFocusedWindow = nullptr;
+ mInputContext.mIMEState.mEnabled = IMEEnabled::Disabled;
+ mPostingKeyEvents.Clear();
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p OnDestroyWindow(), succeeded, Completely destroyed", this));
+}
+
+void IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext) {
+ if (mIMContextID == IMContextID::IIIMF) {
+ // IIIM module registers handlers for the "closed" signal on the
+ // display, but the signal handler is not disconnected when the module
+ // is unloaded. To prevent the module from being unloaded, use static
+ // variable to hold reference of slave context class declared by IIIM.
+ // Note that this does not grab any instance, it grabs the "class".
+ static gpointer sGtkIIIMContextClass = nullptr;
+ if (!sGtkIIIMContextClass) {
+ // We retrieved slave context class with g_type_name() and actual
+ // slave context instance when our widget was GTK2. That must be
+ // _GtkIMContext::priv::slave in GTK3. However, _GtkIMContext::priv
+ // is an opacity struct named _GtkIMMulticontextPrivate, i.e., it's
+ // not exposed by GTK3. Therefore, we cannot access the instance
+ // safely. So, we need to retrieve the slave context class with
+ // g_type_from_name("GtkIMContextIIIM") directly (anyway, we needed
+ // to compare the class name with "GtkIMContextIIIM").
+ GType IIMContextType = g_type_from_name("GtkIMContextIIIM");
+ if (IIMContextType) {
+ sGtkIIIMContextClass = g_type_class_ref(IIMContextType);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p PrepareToDestroyContext(), added to reference to "
+ "GtkIMContextIIIM class to prevent it from being unloaded",
+ this));
+ } else {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p PrepareToDestroyContext(), FAILED to prevent the "
+ "IIIM module from being uploaded",
+ this));
+ }
+ }
+ }
+}
+
+void IMContextWrapper::OnFocusWindow(nsWindow* aWindow) {
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this,
+ aWindow, mLastFocusedWindow));
+ mLastFocusedWindow = aWindow;
+}
+
+void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) {
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
+ "mIMEFocusState=%s",
+ this, aWindow, mLastFocusedWindow, ToString(mIMEFocusState).c_str()));
+
+ if (mLastFocusedWindow != aWindow) {
+ return;
+ }
+
+ NotifyIMEOfFocusChange(IMEFocusState::Blurred);
+}
+
+KeyHandlingState IMContextWrapper::OnKeyEvent(
+ nsWindow* aCaller, GdkEventKey* aEvent,
+ bool aKeyboardEventWasDispatched /* = false */) {
+ MOZ_ASSERT(aEvent, "aEvent must be non-null");
+
+ if (!mInputContext.mIMEState.IsEditable() || MOZ_UNLIKELY(IsDestroyed())) {
+ return KeyHandlingState::eNotHandled;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info, (">>>>>>>>>>>>>>>>"));
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p OnKeyEvent(aCaller=0x%p, "
+ "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X, state=%s, "
+ "time=%u, hardware_keycode=%u, group=%u }, "
+ "aKeyboardEventWasDispatched=%s)",
+ this, aCaller, aEvent, GetEventType(aEvent),
+ gdk_keyval_name(aEvent->keyval), gdk_keyval_to_unicode(aEvent->keyval),
+ GetEventStateName(aEvent->state, mIMContextID).get(), aEvent->time,
+ aEvent->hardware_keycode, aEvent->group,
+ ToChar(aKeyboardEventWasDispatched)));
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p OnKeyEvent(), mMaybeInDeadKeySequence=%s, "
+ "mCompositionState=%s, current context=%p, active context=%p, "
+ "mIMContextID=%s, mIsIMInAsyncKeyHandlingMode=%s",
+ this, ToChar(mMaybeInDeadKeySequence), GetCompositionStateName(),
+ GetCurrentContext(), GetActiveContext(), ToString(mIMContextID).c_str(),
+ ToChar(mIsIMInAsyncKeyHandlingMode)));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
+ "window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return KeyHandlingState::eNotHandled;
+ }
+
+ // Even if old IM context has composition, key event should be sent to
+ // current context since the user expects so.
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (MOZ_UNLIKELY(!currentContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnKeyEvent(), FAILED, there are no context", this));
+ return KeyHandlingState::eNotHandled;
+ }
+
+ if (mSetCursorPositionOnKeyEvent) {
+ SetCursorPosition(currentContext);
+ mSetCursorPositionOnKeyEvent = false;
+ }
+
+ // Let's support dead key event even if active keyboard layout also
+ // supports complicated composition like CJK IME.
+ bool isDeadKey =
+ KeymapWrapper::ComputeDOMKeyNameIndex(aEvent) == KEY_NAME_INDEX_Dead;
+ mMaybeInDeadKeySequence |= isDeadKey;
+
+ // If current context is mSimpleContext, both ibus and fcitx handles key
+ // events synchronously. So, only when current context is mContext which
+ // is GtkIMMulticontext, the key event may be handled by IME asynchronously.
+ bool probablyHandledAsynchronously =
+ mIsIMInAsyncKeyHandlingMode && currentContext == mContext;
+
+ // If we're not sure whether the event is handled asynchronously, this is
+ // set to true.
+ bool maybeHandledAsynchronously = false;
+
+ // If aEvent is a synthesized event for async handling, this will be set to
+ // true.
+ bool isHandlingAsyncEvent = false;
+
+ // If we've decided that the event won't be synthesized asyncrhonously
+ // by IME, but actually IME did it, this is set to true.
+ bool isUnexpectedAsyncEvent = false;
+
+ // If IM is ibus or fcitx and it handles key events asynchronously,
+ // they mark aEvent->state as "handled by me" when they post key event
+ // to another process. Unfortunately, we need to check this hacky
+ // flag because it's difficult to store all pending key events by
+ // an array or a hashtable.
+ if (probablyHandledAsynchronously) {
+ switch (mIMContextID) {
+ case IMContextID::IBus: {
+ // See src/ibustypes.h
+ static const guint IBUS_IGNORED_MASK = 1 << 25;
+ // If IBUS_IGNORED_MASK was set to aEvent->state, the event
+ // has already been handled by another process and it wasn't
+ // used by IME.
+ isHandlingAsyncEvent = !!(aEvent->state & IBUS_IGNORED_MASK);
+ if (!isHandlingAsyncEvent) {
+ // On some environments, IBUS_IGNORED_MASK flag is not set as
+ // expected. In such case, we keep pusing all events into the queue.
+ // I.e., that causes eating a lot of memory until it's blurred.
+ // Therefore, we need to check whether there is same timestamp event
+ // in the queue. This redundant cost should be low because in most
+ // causes, key events in the queue should be 2 or 4.
+ isHandlingAsyncEvent =
+ mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
+ if (isHandlingAsyncEvent) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnKeyEvent(), aEvent->state does not have "
+ "IBUS_IGNORED_MASK but "
+ "same event in the queue. So, assuming it's a "
+ "synthesized event",
+ this));
+ }
+ }
+
+ // If it's a synthesized event, let's remove it from the posting
+ // event queue first. Otherwise the following blocks cannot use
+ // `break`.
+ if (isHandlingAsyncEvent) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnKeyEvent(), aEvent->state has IBUS_IGNORED_MASK "
+ "or aEvent is in the "
+ "posting event queue, so, it won't be handled "
+ "asynchronously anymore. Removing "
+ "the posted events from the queue",
+ this));
+ probablyHandledAsynchronously = false;
+ mPostingKeyEvents.RemoveEvent(aEvent);
+ }
+
+ // ibus won't send back key press events in a dead key sequcne.
+ if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
+ probablyHandledAsynchronously = false;
+ if (isHandlingAsyncEvent) {
+ isUnexpectedAsyncEvent = true;
+ break;
+ }
+ // Some keyboard layouts which have dead keys may send
+ // "empty" key event to make us call
+ // gtk_im_context_filter_keypress() to commit composed
+ // character during a GDK_KEY_PRESS event dispatching.
+ if (!gdk_keyval_to_unicode(aEvent->keyval) &&
+ !aEvent->hardware_keycode) {
+ isUnexpectedAsyncEvent = true;
+ break;
+ }
+ break;
+ }
+ // ibus may handle key events synchronously if focused editor is
+ // <input type="password"> or |ime-mode: disabled;|. However, in
+ // some environments, not so actually. Therefore, we need to check
+ // the result of gtk_im_context_filter_keypress() later.
+ if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
+ probablyHandledAsynchronously = false;
+ maybeHandledAsynchronously = !isHandlingAsyncEvent;
+ break;
+ }
+ break;
+ }
+ case IMContextID::Fcitx:
+ case IMContextID::Fcitx5: {
+ // See src/lib/fcitx-utils/keysym.h
+ static const guint FcitxKeyState_IgnoredMask = 1 << 25;
+ // If FcitxKeyState_IgnoredMask was set to aEvent->state,
+ // the event has already been handled by another process and
+ // it wasn't used by IME.
+ isHandlingAsyncEvent = !!(aEvent->state & FcitxKeyState_IgnoredMask);
+ if (!isHandlingAsyncEvent) {
+ // On some environments, FcitxKeyState_IgnoredMask flag *might* be not
+ // set as expected. If there were such cases, we'd keep pusing all
+ // events into the queue. I.e., that would cause eating a lot of
+ // memory until it'd be blurred. Therefore, we should check whether
+ // there is same timestamp event in the queue. This redundant cost
+ // should be low because in most causes, key events in the queue
+ // should be 2 or 4.
+ isHandlingAsyncEvent =
+ mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
+ if (isHandlingAsyncEvent) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnKeyEvent(), aEvent->state does not have "
+ "FcitxKeyState_IgnoredMask "
+ "but same event in the queue. So, assuming it's a "
+ "synthesized event",
+ this));
+ }
+ }
+
+ // fcitx won't send back key press events in a dead key sequcne.
+ if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
+ probablyHandledAsynchronously = false;
+ if (isHandlingAsyncEvent) {
+ isUnexpectedAsyncEvent = true;
+ break;
+ }
+ // Some keyboard layouts which have dead keys may send
+ // "empty" key event to make us call
+ // gtk_im_context_filter_keypress() to commit composed
+ // character during a GDK_KEY_PRESS event dispatching.
+ if (!gdk_keyval_to_unicode(aEvent->keyval) &&
+ !aEvent->hardware_keycode) {
+ isUnexpectedAsyncEvent = true;
+ break;
+ }
+ }
+
+ // fcitx handles key events asynchronously even if focused
+ // editor cannot use IME actually.
+
+ if (isHandlingAsyncEvent) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnKeyEvent(), aEvent->state has "
+ "FcitxKeyState_IgnoredMask or aEvent is in "
+ "the posting event queue, so, it won't be handled "
+ "asynchronously anymore. "
+ "Removing the posted events from the queue",
+ this));
+ probablyHandledAsynchronously = false;
+ mPostingKeyEvents.RemoveEvent(aEvent);
+ break;
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "IME may handle key event "
+ "asyncrhonously, but not yet confirmed if it comes agian "
+ "actually");
+ }
+ }
+
+ if (!isUnexpectedAsyncEvent) {
+ mKeyboardEventWasDispatched = aKeyboardEventWasDispatched;
+ mKeyboardEventWasConsumed = false;
+ } else {
+ // If we didn't expect this event, we've alreday dispatched eKeyDown
+ // event or eKeyUp event for that.
+ mKeyboardEventWasDispatched = true;
+ // And in this case, we need to assume that another key event hasn't
+ // been receivied and mKeyboardEventWasConsumed keeps storing the
+ // dispatched eKeyDown or eKeyUp event's state.
+ }
+ mFallbackToKeyEvent = false;
+ mProcessingKeyEvent = aEvent;
+ gboolean isFiltered = gtk_im_context_filter_keypress(currentContext, aEvent);
+
+ // If we're not sure whether the event is handled by IME asynchronously or
+ // synchronously, we need to trust the result of
+ // gtk_im_context_filter_keypress(). If it consumed and but did nothing,
+ // we can assume that another event will be synthesized.
+ if (!isHandlingAsyncEvent && maybeHandledAsynchronously) {
+ probablyHandledAsynchronously |=
+ isFiltered && !mFallbackToKeyEvent && !mKeyboardEventWasDispatched;
+ }
+
+ if (aEvent->type == GDK_KEY_PRESS) {
+ if (isFiltered && probablyHandledAsynchronously) {
+ sWaitingSynthesizedKeyPressHardwareKeyCode = aEvent->hardware_keycode;
+ } else {
+ sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
+ }
+ }
+
+ // The caller of this shouldn't handle aEvent anymore if we've dispatched
+ // composition events or modified content with other events.
+ bool filterThisEvent = isFiltered && !mFallbackToKeyEvent;
+
+ if (IsComposingOnCurrentContext() && !isFiltered &&
+ aEvent->type == GDK_KEY_PRESS && mDispatchedCompositionString.IsEmpty()) {
+ // A Hangul input engine for SCIM doesn't emit preedit_end
+ // signal even when composition string becomes empty. On the
+ // other hand, we should allow to make composition with empty
+ // string for other languages because there *might* be such
+ // IM. For compromising this issue, we should dispatch
+ // compositionend event, however, we don't need to reset IM
+ // actually.
+ // NOTE: Don't dispatch key events as "processed by IME" since
+ // we need to dispatch keyboard events as IME wasn't handled it.
+ mProcessingKeyEvent = nullptr;
+ DispatchCompositionCommitEvent(currentContext, &EmptyString());
+ mProcessingKeyEvent = aEvent;
+ // In this case, even though we handle the keyboard event here,
+ // but we should dispatch keydown event as
+ filterThisEvent = false;
+ }
+
+ if (filterThisEvent && !mKeyboardEventWasDispatched) {
+ // If IME handled the key event but we've not dispatched eKeyDown nor
+ // eKeyUp event yet, we need to dispatch here unless the key event is
+ // now being handled by other IME process.
+ if (!probablyHandledAsynchronously) {
+ MaybeDispatchKeyEventAsProcessedByIME(eVoidEvent);
+ // Be aware, the widget might have been gone here.
+ }
+ // If we need to wait reply from IM, IM may send some signals to us
+ // without sending the key event again. In such case, we need to
+ // dispatch keyboard events with a copy of aEvent. Therefore, we
+ // need to use information of this key event to dispatch an KeyDown
+ // or eKeyUp event later.
+ else {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnKeyEvent(), putting aEvent into the queue...", this));
+ mPostingKeyEvents.PutEvent(aEvent);
+ }
+ }
+
+ mProcessingKeyEvent = nullptr;
+
+ if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) {
+ // If the key event hasn't been handled by active IME nor keyboard
+ // layout, we can assume that the dead key sequence has been or was
+ // ended. Note that we should not reset it when the key event is
+ // GDK_KEY_RELEASE since it may not be filtered by active keyboard
+ // layout even in composition.
+ mMaybeInDeadKeySequence = false;
+ }
+
+ if (aEvent->type == GDK_KEY_RELEASE) {
+ if (const GdkEventKey* pendingKeyPressEvent =
+ mPostingKeyEvents.GetCorrespondingKeyPressEvent(aEvent)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p OnKeyEvent(), forgetting a pending GDK_KEY_PRESS event "
+ "because GDK_KEY_RELEASE for the event is handled",
+ this));
+ mPostingKeyEvents.RemoveEvent(pendingKeyPressEvent);
+ }
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s "
+ "(isFiltered=%s, mFallbackToKeyEvent=%s, "
+ "probablyHandledAsynchronously=%s, maybeHandledAsynchronously=%s), "
+ "mPostingKeyEvents.Length()=%zu, mCompositionState=%s, "
+ "mMaybeInDeadKeySequence=%s, mKeyboardEventWasDispatched=%s, "
+ "mKeyboardEventWasConsumed=%s",
+ this, ToChar(filterThisEvent), ToChar(isFiltered),
+ ToChar(mFallbackToKeyEvent), ToChar(probablyHandledAsynchronously),
+ ToChar(maybeHandledAsynchronously), mPostingKeyEvents.Length(),
+ GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence),
+ ToChar(mKeyboardEventWasDispatched), ToChar(mKeyboardEventWasConsumed)));
+ MOZ_LOG(gIMELog, LogLevel::Info, ("<<<<<<<<<<<<<<<<\n\n"));
+
+ if (filterThisEvent) {
+ return KeyHandlingState::eHandled;
+ }
+ // If another call of this method has already dispatched eKeyDown event,
+ // we should return KeyHandlingState::eNotHandledButEventDispatched because
+ // the caller should've stopped handling the event if preceding eKeyDown
+ // event was consumed.
+ if (aKeyboardEventWasDispatched) {
+ return KeyHandlingState::eNotHandledButEventDispatched;
+ }
+ if (!mKeyboardEventWasDispatched) {
+ return KeyHandlingState::eNotHandled;
+ }
+ return mKeyboardEventWasConsumed
+ ? KeyHandlingState::eNotHandledButEventConsumed
+ : KeyHandlingState::eNotHandledButEventDispatched;
+}
+
+void IMContextWrapper::OnFocusChangeInGecko(bool aFocus) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnFocusChangeInGecko(aFocus=%s),mCompositionState=%s, "
+ "mIMEFocusState=%s, mSetInputPurposeAndInputHints=%s",
+ this, ToChar(aFocus), GetCompositionStateName(),
+ ToString(mIMEFocusState).c_str(),
+ ToChar(mSetInputPurposeAndInputHints)));
+
+ // We shouldn't carry over the removed string to another editor.
+ mSelectedStringRemovedByComposition.Truncate();
+ mContentSelection.reset();
+
+ if (aFocus) {
+ if (mSetInputPurposeAndInputHints) {
+ mSetInputPurposeAndInputHints = false;
+ SetInputPurposeAndInputHints();
+ }
+ NotifyIMEOfFocusChange(IMEFocusState::Focused);
+ } else {
+ NotifyIMEOfFocusChange(IMEFocusState::Blurred);
+ }
+
+ // When the focus changes, we need to inform IM about the new cursor
+ // position. Chinese input methods generally rely on this because they
+ // usually don't start composition until a character is picked.
+ if (aFocus && EnsureToCacheContentSelection()) {
+ SetCursorPosition(GetActiveContext());
+ }
+}
+
+void IMContextWrapper::ResetIME() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p ResetIME(), mCompositionState=%s, mIMEFocusState=%s", this,
+ GetCompositionStateName(), ToString(mIMEFocusState).c_str()));
+
+ GtkIMContext* activeContext = GetActiveContext();
+ if (MOZ_UNLIKELY(!activeContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p ResetIME(), FAILED, there are no context", this));
+ return;
+ }
+
+ RefPtr<IMContextWrapper> kungFuDeathGrip(this);
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+
+ mPendingResettingIMContext = false;
+ gtk_im_context_reset(activeContext);
+
+ // The last focused window might have been destroyed by a DOM event handler
+ // which was called by us during a call of gtk_im_context_reset().
+ if (!lastFocusedWindow ||
+ NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) ||
+ lastFocusedWindow->Destroyed()) {
+ return;
+ }
+
+ nsAutoString compositionString;
+ GetCompositionString(activeContext, compositionString);
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p ResetIME() called gtk_im_context_reset(), "
+ "activeContext=0x%p, mCompositionState=%s, compositionString=%s, "
+ "mIMEFocusState=%s",
+ this, activeContext, GetCompositionStateName(),
+ NS_ConvertUTF16toUTF8(compositionString).get(),
+ ToString(mIMEFocusState).c_str()));
+
+ // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still
+ // used in Japan!) sends only "preedit_changed" signal with empty
+ // composition string synchronously. Therefore, if composition string
+ // is now empty string, we should assume that the IME won't send
+ // "commit" signal.
+ if (IsComposing() && compositionString.IsEmpty()) {
+ // WARNING: The widget might have been gone after this.
+ DispatchCompositionCommitEvent(activeContext, &EmptyString());
+ }
+}
+
+nsresult IMContextWrapper::EndIMEComposition(nsWindow* aCaller) {
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return NS_OK;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p EndIMEComposition(aCaller=0x%p), "
+ "mCompositionState=%s",
+ this, aCaller, GetCompositionStateName()));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p EndIMEComposition(), FAILED, the caller isn't "
+ "focused window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return NS_OK;
+ }
+
+ if (!IsComposing()) {
+ return NS_OK;
+ }
+
+ // Currently, GTK has API neither to commit nor to cancel composition
+ // forcibly. Therefore, TextComposition will recompute commit string for
+ // the request even if native IME will cause unexpected commit string.
+ // So, we don't need to emulate commit or cancel composition with
+ // proper composition events.
+ // XXX ResetIME() might not enough for finishing compositoin on some
+ // environments. We should emulate focus change too because some IMEs
+ // may commit or cancel composition at blur.
+ ResetIME();
+
+ return NS_OK;
+}
+
+void IMContextWrapper::OnLayoutChange() {
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ if (IsComposing()) {
+ SetCursorPosition(GetActiveContext());
+ } else {
+ // If not composing, candidate window position is updated before key
+ // down
+ mSetCursorPositionOnKeyEvent = true;
+ }
+ mLayoutChanged = true;
+}
+
+void IMContextWrapper::OnUpdateComposition() {
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ if (!IsComposing()) {
+ // Composition has been committed. So we need update selection for
+ // caret later
+ mContentSelection.reset();
+ EnsureToCacheContentSelection();
+ mSetCursorPositionOnKeyEvent = true;
+ }
+
+ // If we've already set candidate window position, we don't need to update
+ // the position with update composition notification.
+ if (!mLayoutChanged) {
+ SetCursorPosition(GetActiveContext());
+ }
+}
+
+void IMContextWrapper::SetInputContext(nsWindow* aCaller,
+ const InputContext* aContext,
+ const InputContextAction* aAction) {
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ "
+ "mEnabled=%s }, mHTMLInputType=%s })",
+ this, aCaller, ToString(aContext->mIMEState.mEnabled).c_str(),
+ NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetInputContext(), FAILED, "
+ "the caller isn't focused window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return;
+ }
+
+ if (!mContext) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetInputContext(), FAILED, "
+ "there are no context",
+ this));
+ return;
+ }
+
+ if (sLastFocusedContext != this) {
+ mInputContext = *aContext;
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p SetInputContext(), succeeded, "
+ "but we're not active",
+ this));
+ return;
+ }
+
+ const bool changingEnabledState =
+ aContext->IsInputAttributeChanged(mInputContext);
+
+ // Release current IME focus if IME is enabled.
+ if (changingEnabledState && mInputContext.mIMEState.IsEditable()) {
+ if (IsComposing()) {
+ EndIMEComposition(mLastFocusedWindow);
+ }
+ if (mIMEFocusState == IMEFocusState::Focused) {
+ NotifyIMEOfFocusChange(IMEFocusState::BlurredWithoutFocusChange);
+ }
+ }
+
+ mInputContext = *aContext;
+ mSetInputPurposeAndInputHints = false;
+
+ if (!changingEnabledState || !mInputContext.mIMEState.IsEditable()) {
+ return;
+ }
+
+ // If the input context was temporarily disabled without a focus change,
+ // it must be ready to query content even if the focused content is in
+ // a remote process. In this case, we should set IME focus right now.
+ if (mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
+ SetInputPurposeAndInputHints();
+ NotifyIMEOfFocusChange(IMEFocusState::Focused);
+ return;
+ }
+
+ // Otherwise, we cannot set input-purpose and input-hints right now because
+ // setting them may require to set focus immediately for IME own's UI.
+ // However, at this moment, `ContentCacheInParent` does not have content
+ // cache, it'll be available after `NOTIFY_IME_OF_FOCUS` notification.
+ // Therefore, we set them at receiving the notification.
+ mSetInputPurposeAndInputHints = true;
+}
+
+void IMContextWrapper::SetInputPurposeAndInputHints() {
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (!currentContext) {
+ return;
+ }
+
+ GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM;
+ const nsString& inputType = mInputContext.mHTMLInputType;
+ // Password case has difficult issue. Desktop IMEs disable composition if
+ // input-purpose is password. For disabling IME on |ime-mode: disabled;|, we
+ // need to check mEnabled value instead of inputType value. This hack also
+ // enables composition on <input type="password" style="ime-mode: enabled;">.
+ // This is right behavior of ime-mode on desktop.
+ //
+ // On the other hand, IME for tablet devices may provide a specific software
+ // keyboard for password field. If so, the behavior might look strange on
+ // both:
+ // <input type="text" style="ime-mode: disabled;">
+ // <input type="password" style="ime-mode: enabled;">
+ //
+ // Temporarily, we should focus on desktop environment for now. I.e., let's
+ // ignore tablet devices for now. When somebody reports actual trouble on
+ // tablet devices, we should try to look for a way to solve actual problem.
+ if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
+ purpose = GTK_INPUT_PURPOSE_PASSWORD;
+ } else if (inputType.EqualsLiteral("email")) {
+ purpose = GTK_INPUT_PURPOSE_EMAIL;
+ } else if (inputType.EqualsLiteral("url")) {
+ purpose = GTK_INPUT_PURPOSE_URL;
+ } else if (inputType.EqualsLiteral("tel")) {
+ purpose = GTK_INPUT_PURPOSE_PHONE;
+ } else if (inputType.EqualsLiteral("number")) {
+ purpose = GTK_INPUT_PURPOSE_NUMBER;
+ } else if (mInputContext.mHTMLInputMode.EqualsLiteral("decimal")) {
+ purpose = GTK_INPUT_PURPOSE_NUMBER;
+ } else if (mInputContext.mHTMLInputMode.EqualsLiteral("email")) {
+ purpose = GTK_INPUT_PURPOSE_EMAIL;
+ } else if (mInputContext.mHTMLInputMode.EqualsLiteral("numeric")) {
+ purpose = GTK_INPUT_PURPOSE_DIGITS;
+ } else if (mInputContext.mHTMLInputMode.EqualsLiteral("tel")) {
+ purpose = GTK_INPUT_PURPOSE_PHONE;
+ } else if (mInputContext.mHTMLInputMode.EqualsLiteral("url")) {
+ purpose = GTK_INPUT_PURPOSE_URL;
+ }
+ // Search by type and inputmode isn't supported on GTK.
+
+ g_object_set(currentContext, "input-purpose", purpose, nullptr);
+
+ // Although GtkInputHints is enum type, value is bit field.
+ gint hints = GTK_INPUT_HINT_NONE;
+ if (mInputContext.mHTMLInputMode.EqualsLiteral("none")) {
+ hints |= GTK_INPUT_HINT_INHIBIT_OSK;
+ }
+
+ if (mInputContext.mAutocapitalize.EqualsLiteral("characters")) {
+ hints |= GTK_INPUT_HINT_UPPERCASE_CHARS;
+ } else if (mInputContext.mAutocapitalize.EqualsLiteral("sentences")) {
+ hints |= GTK_INPUT_HINT_UPPERCASE_SENTENCES;
+ } else if (mInputContext.mAutocapitalize.EqualsLiteral("words")) {
+ hints |= GTK_INPUT_HINT_UPPERCASE_WORDS;
+ }
+
+ g_object_set(currentContext, "input-hints", hints, nullptr);
+}
+
+InputContext IMContextWrapper::GetInputContext() {
+ mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
+ return mInputContext;
+}
+
+GtkIMContext* IMContextWrapper::GetCurrentContext() const {
+ if (IsEnabled()) {
+ return mContext;
+ }
+ if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
+ return mSimpleContext;
+ }
+ return mDummyContext;
+}
+
+bool IMContextWrapper::IsValidContext(GtkIMContext* aContext) const {
+ if (!aContext) {
+ return false;
+ }
+ return aContext == mContext || aContext == mSimpleContext ||
+ aContext == mDummyContext;
+}
+
+bool IMContextWrapper::IsEnabled() const {
+ return mInputContext.mIMEState.mEnabled == IMEEnabled::Enabled ||
+ (!sUseSimpleContext &&
+ mInputContext.mIMEState.mEnabled == IMEEnabled::Password);
+}
+
+void IMContextWrapper::NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState) {
+ MOZ_ASSERT_IF(aIMEFocusState == IMEFocusState::BlurredWithoutFocusChange,
+ mIMEFocusState != IMEFocusState::Blurred);
+ if (mIMEFocusState == aIMEFocusState) {
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p NotifyIMEOfFocusChange(aIMEFocusState=%s), mIMEFocusState=%s, "
+ "sLastFocusedContext=0x%p",
+ this, ToString(aIMEFocusState).c_str(),
+ ToString(mIMEFocusState).c_str(), sLastFocusedContext));
+ MOZ_ASSERT(!mSetInputPurposeAndInputHints);
+
+ // If we've already made IME blurred at setting the input context disabled
+ // and it's now completely blurred by a focus move, we need only to update
+ // mIMEFocusState and when the input context gets enabled, we cannot set
+ // IME focus immediately.
+ if (aIMEFocusState == IMEFocusState::Blurred &&
+ mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
+ mIMEFocusState = IMEFocusState::Blurred;
+ return;
+ }
+
+ auto Blur = [&](IMEFocusState aInternalState) {
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (MOZ_UNLIKELY(!currentContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p NotifyIMEOfFocusChange()::Blur(), FAILED, "
+ "there is no context",
+ this));
+ return;
+ }
+ gtk_im_context_focus_out(currentContext);
+ mIMEFocusState = aInternalState;
+ };
+
+ if (aIMEFocusState != IMEFocusState::Focused) {
+ return Blur(aIMEFocusState);
+ }
+
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (MOZ_UNLIKELY(!currentContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p NotifyIMEOfFocusChange(), FAILED, "
+ "there is no context",
+ this));
+ return;
+ }
+
+ if (sLastFocusedContext && sLastFocusedContext != this) {
+ sLastFocusedContext->NotifyIMEOfFocusChange(IMEFocusState::Blurred);
+ }
+
+ sLastFocusedContext = this;
+
+ // Forget all posted key events when focus is moved since they shouldn't
+ // be fired in different editor.
+ sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
+ mPostingKeyEvents.Clear();
+
+ gtk_im_context_focus_in(currentContext);
+ mIMEFocusState = aIMEFocusState;
+ mSetCursorPositionOnKeyEvent = true;
+
+ if (!IsEnabled()) {
+ // We should release IME focus for uim and scim.
+ // These IMs are using snooper that is released at losing focus.
+ Blur(IMEFocusState::BlurredWithoutFocusChange);
+ }
+}
+
+void IMContextWrapper::OnSelectionChange(
+ nsWindow* aCaller, const IMENotification& aIMENotification) {
+ const bool isSelectionRangeChanged =
+ mContentSelection.isNothing() ||
+ !aIMENotification.mSelectionChangeData.EqualsRange(
+ mContentSelection.ref());
+ mContentSelection =
+ Some(ContentSelection(aIMENotification.mSelectionChangeData));
+ const bool retrievedSurroundingSignalReceived =
+ mRetrieveSurroundingSignalReceived;
+ mRetrieveSurroundingSignalReceived = false;
+
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ const IMENotification::SelectionChangeDataBase& selectionChangeData =
+ aIMENotification.mSelectionChangeData;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
+ "mSelectionChangeData=%s }), "
+ "mCompositionState=%s, mIsDeletingSurrounding=%s, "
+ "mRetrieveSurroundingSignalReceived=%s, isSelectionRangeChanged=%s",
+ this, aCaller, ToString(selectionChangeData).c_str(),
+ GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
+ ToChar(retrievedSurroundingSignalReceived),
+ ToChar(isSelectionRangeChanged)));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnSelectionChange(), FAILED, "
+ "the caller isn't focused window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return;
+ }
+
+ if (!IsComposing()) {
+ // Now we have no composition (mostly situation on calling this method)
+ // If we have it, it will set by
+ // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED.
+ mSetCursorPositionOnKeyEvent = true;
+ }
+
+ // The focused editor might have placeholder text with normal text node.
+ // In such case, the text node must be removed from a compositionstart
+ // event handler. So, we're dispatching eCompositionStart,
+ // we should ignore selection change notification.
+ if (mCompositionState == eCompositionState_CompositionStartDispatched) {
+ if (NS_WARN_IF(mContentSelection.isNothing())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnSelectionChange(), FAILED, "
+ "new offset is too large, cannot keep composing",
+ this));
+ } else if (mContentSelection->HasRange()) {
+ // Modify the selection start offset with new offset.
+ mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
+ // XXX We should modify mSelectedStringRemovedByComposition?
+ // But how?
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p OnSelectionChange(), ignored, mCompositionStart "
+ "is updated to %u, the selection change doesn't cause "
+ "resetting IM context",
+ this, mCompositionStart));
+ // And don't reset the IM context.
+ return;
+ } else {
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p OnSelectionChange(), ignored, because of no selection range",
+ this));
+ return;
+ }
+ // Otherwise, reset the IM context due to impossible to keep composing.
+ }
+
+ // If the selection change is caused by deleting surrounding text,
+ // we shouldn't need to notify IME of selection change.
+ if (mIsDeletingSurrounding) {
+ return;
+ }
+
+ bool occurredBeforeComposition =
+ IsComposing() && !selectionChangeData.mOccurredDuringComposition &&
+ !selectionChangeData.mCausedByComposition;
+ if (occurredBeforeComposition) {
+ mPendingResettingIMContext = true;
+ }
+
+ // When the selection change is caused by dispatching composition event,
+ // selection set event and/or occurred before starting current composition,
+ // we shouldn't notify IME of that and commit existing composition.
+ // Don't do this even if selection is not changed actually. For example,
+ // fcitx has direct input mode which does not insert composing string, but
+ // inserts commited text for each key sequence (i.e., there is "invisible"
+ // composition string). In the world after bug 1712269, we don't use a
+ // set of composition events for this kind of IME. Therefore,
+ // SelectionChangeData.mCausedByComposition is not expected value for here
+ // if this call is caused by a preceding commit. And if the preceding commit
+ // is triggered by a key type for next word, resetting IME state makes fcitx
+ // discard the pending input for the next word. Thus, we need to check
+ // whether the selection range is actually changed here.
+ if (!selectionChangeData.mCausedByComposition &&
+ !selectionChangeData.mCausedBySelectionEvent && isSelectionRangeChanged &&
+ !occurredBeforeComposition) {
+ // Hack for ibus-pinyin. ibus-pinyin will synthesize a set of
+ // composition which commits with empty string after calling
+ // gtk_im_context_reset(). Therefore, selecting text causes
+ // unexpectedly removing it. For preventing it but not breaking the
+ // other IMEs which use surrounding text, we should call it only when
+ // surrounding text has been retrieved after last selection range was
+ // set. If it's not retrieved, that means that current IME doesn't
+ // have any content cache, so, it must not need the notification of
+ // selection change.
+ if (IsComposing() || retrievedSurroundingSignalReceived) {
+ ResetIME();
+ }
+ }
+}
+
+/* static */
+void IMContextWrapper::OnThemeChanged() {
+ if (auto* provider = SelectionStyleProvider::GetExistingInstance()) {
+ provider->OnThemeChanged();
+ }
+}
+
+/* static */
+void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule) {
+ aModule->OnStartCompositionNative(aContext);
+}
+
+void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) {
+ // IME may synthesize composition asynchronously after filtering a
+ // GDK_KEY_PRESS event. In that case, we should handle composition with
+ // emulating the usual case, i.e., this is called in the stack of
+ // OnKeyEvent().
+ Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
+ if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
+ GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
+ if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
+ KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
+ KEY_NAME_INDEX_USE_STRING) {
+ maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
+ mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
+ }
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnStartCompositionNative(aContext=0x%p), "
+ "current context=0x%p, mComposingContext=0x%p",
+ this, aContext, GetCurrentContext(), mComposingContext));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (GetCurrentContext() != aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnStartCompositionNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return;
+ }
+
+ if (mComposingContext && aContext != mComposingContext) {
+ // XXX For now, we should ignore this odd case, just logging.
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p OnStartCompositionNative(), Warning, "
+ "there is already a composing context but starting new "
+ "composition with different context",
+ this));
+ }
+
+ // IME may start composition without "preedit_start" signal. Therefore,
+ // mComposingContext will be initialized in DispatchCompositionStart().
+
+ if (!DispatchCompositionStart(aContext)) {
+ return;
+ }
+ mCompositionTargetRange.mOffset = mCompositionStart;
+ mCompositionTargetRange.mLength = 0;
+}
+
+/* static */
+void IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule) {
+ aModule->OnEndCompositionNative(aContext);
+}
+
+void IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnEndCompositionNative(aContext=0x%p), mComposingContext=0x%p",
+ this, aContext, mComposingContext));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ // Note that if this is called after focus move, the context may different
+ // from any our owning context.
+ if (!IsValidContext(aContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnEndCompositionNative(), FAILED, "
+ "given context doesn't match with any context",
+ this));
+ return;
+ }
+
+ // If we've not started composition with aContext, we should ignore it.
+ if (aContext != mComposingContext) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p OnEndCompositionNative(), Warning, "
+ "given context doesn't match with mComposingContext",
+ this));
+ return;
+ }
+
+ g_object_unref(mComposingContext);
+ mComposingContext = nullptr;
+
+ // If we already handled the commit event, we should do nothing here.
+ if (IsComposing()) {
+ if (!DispatchCompositionCommitEvent(aContext)) {
+ // If the widget is destroyed, we should do nothing anymore.
+ return;
+ }
+ }
+
+ if (mPendingResettingIMContext) {
+ ResetIME();
+ }
+}
+
+/* static */
+void IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule) {
+ RefPtr module = aModule;
+ module->OnChangeCompositionNative(aContext);
+
+ if (module->IsDestroyed()) {
+ // A strong reference is already held during "preedit-changed" emission,
+ // but _ibus_context_destroy_cb() in ibus 1.5.28 and
+ // _fcitx_im_context_close_im_cb() in fcitx 4.2.9.9 want their
+ // GtkIMContexts to live a little longer. See bug 1824634.
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction(__func__, [context = RefPtr{aContext}]() {}));
+ }
+}
+
+void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) {
+ // IME may synthesize composition asynchronously after filtering a
+ // GDK_KEY_PRESS event. In that case, we should handle composition with
+ // emulating the usual case, i.e., this is called in the stack of
+ // OnKeyEvent().
+ Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
+ if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
+ GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
+ if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
+ KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
+ KEY_NAME_INDEX_USE_STRING) {
+ maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
+ mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
+ }
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnChangeCompositionNative(aContext=0x%p), "
+ "mComposingContext=0x%p",
+ this, aContext, mComposingContext));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ // Note that if this is called after focus move, the context may different
+ // from any our owning context.
+ if (!IsValidContext(aContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnChangeCompositionNative(), FAILED, "
+ "given context doesn't match with any context",
+ this));
+ return;
+ }
+
+ if (mComposingContext && aContext != mComposingContext) {
+ // XXX For now, we should ignore this odd case, just logging.
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p OnChangeCompositionNative(), Warning, "
+ "given context doesn't match with composing context",
+ this));
+ }
+
+ nsAutoString compositionString;
+ GetCompositionString(aContext, compositionString);
+ if (!IsComposing() && compositionString.IsEmpty()) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p OnChangeCompositionNative(), Warning, does nothing "
+ "because has not started composition and composing string is "
+ "empty",
+ this));
+ mDispatchedCompositionString.Truncate();
+ return; // Don't start the composition with empty string.
+ }
+
+ // Be aware, widget can be gone
+ DispatchCompositionChangeEvent(aContext, compositionString);
+}
+
+/* static */
+gboolean IMContextWrapper::OnRetrieveSurroundingCallback(
+ GtkIMContext* aContext, IMContextWrapper* aModule) {
+ return aModule->OnRetrieveSurroundingNative(aContext);
+}
+
+gboolean IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), "
+ "current context=0x%p",
+ this, aContext, GetCurrentContext()));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (GetCurrentContext() != aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnRetrieveSurroundingNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return FALSE;
+ }
+
+ nsAutoString uniStr;
+ uint32_t cursorPos;
+ if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) {
+ return FALSE;
+ }
+
+ // Despite taking a pointer and a length, IBus wants the string to be
+ // zero-terminated and doesn't like U+0000 within the string.
+ uniStr.ReplaceChar(char16_t(0), char16_t(0xFFFD));
+
+ NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
+ uint32_t cursorPosInUTF8 = utf8Str.Length();
+ AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
+ gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
+ cursorPosInUTF8);
+ mRetrieveSurroundingSignalReceived = true;
+ return TRUE;
+}
+
+/* static */
+gboolean IMContextWrapper::OnDeleteSurroundingCallback(
+ GtkIMContext* aContext, gint aOffset, gint aNChars,
+ IMContextWrapper* aModule) {
+ return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
+}
+
+gboolean IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext,
+ gint aOffset,
+ gint aNChars) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, "
+ "aNChar=%d), current context=0x%p",
+ this, aContext, aOffset, aNChars, GetCurrentContext()));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (GetCurrentContext() != aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnDeleteSurroundingNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return FALSE;
+ }
+
+ AutoRestore<bool> saveDeletingSurrounding(mIsDeletingSurrounding);
+ mIsDeletingSurrounding = true;
+ if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) {
+ return TRUE;
+ }
+
+ // failed
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnDeleteSurroundingNative(), FAILED, "
+ "cannot delete text",
+ this));
+ return FALSE;
+}
+
+/* static */
+void IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext,
+ const gchar* aString,
+ IMContextWrapper* aModule) {
+ aModule->OnCommitCompositionNative(aContext, aString);
+}
+
+void IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext,
+ const gchar* aUTF8Char) {
+ const gchar emptyStr = 0;
+ const gchar* commitString = aUTF8Char ? aUTF8Char : &emptyStr;
+ NS_ConvertUTF8toUTF16 utf16CommitString(commitString);
+
+ // IME may synthesize composition asynchronously after filtering a
+ // GDK_KEY_PRESS event. In that case, we should handle composition with
+ // emulating the usual case, i.e., this is called in the stack of
+ // OnKeyEvent().
+ Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
+ if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
+ GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
+ if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
+ KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
+ KEY_NAME_INDEX_USE_STRING) {
+ maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
+ mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
+ }
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnCommitCompositionNative(aContext=0x%p), "
+ "current context=0x%p, active context=0x%p, commitString=\"%s\", "
+ "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s",
+ this, aContext, GetCurrentContext(), GetActiveContext(),
+ commitString, mProcessingKeyEvent, ToChar(IsComposingOn(aContext))));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (!IsValidContext(aContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnCommitCompositionNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return;
+ }
+
+ // If we are not in composition and committing with empty string,
+ // we need to do nothing because if we continued to handle this
+ // signal, we would dispatch compositionstart, text, compositionend
+ // events with empty string. Of course, they are unnecessary events
+ // for Web applications and our editor.
+ if (!IsComposingOn(aContext) && utf16CommitString.IsEmpty()) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p OnCommitCompositionNative(), Warning, does nothing "
+ "because has not started composition and commit string is empty",
+ this));
+ return;
+ }
+
+ // If IME doesn't change their keyevent that generated this commit,
+ // we should treat that IME didn't handle the key event because
+ // web applications want to receive "keydown" and "keypress" event
+ // in such case.
+ // NOTE: While a key event is being handled, this might be caused on
+ // current context. Otherwise, this may be caused on active context.
+ if (!IsComposingOn(aContext) && mProcessingKeyEvent &&
+ mProcessingKeyEvent->type == GDK_KEY_PRESS &&
+ aContext == GetCurrentContext()) {
+ char keyval_utf8[8]; /* should have at least 6 bytes of space */
+ gint keyval_utf8_len;
+ guint32 keyval_unicode;
+
+ keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval);
+ keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8);
+ keyval_utf8[keyval_utf8_len] = '\0';
+
+ // If committing string is exactly same as a character which is
+ // produced by the key, eKeyDown and eKeyPress event should be
+ // dispatched by the caller of OnKeyEvent() normally. Note that
+ // mMaybeInDeadKeySequence will be set to false by OnKeyEvent()
+ // since we set mFallbackToKeyEvent to true here.
+ if (!strcmp(commitString, keyval_utf8)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnCommitCompositionNative(), "
+ "we'll send normal key event",
+ this));
+ mFallbackToKeyEvent = true;
+ return;
+ }
+
+ // If we're in a dead key sequence, commit string is a character in
+ // the BMP and mProcessingKeyEvent produces some characters but it's
+ // not same as committing string, we should dispatch an eKeyPress
+ // event from here.
+ WidgetKeyboardEvent keyDownEvent(true, eKeyDown, mLastFocusedWindow);
+ KeymapWrapper::InitKeyEvent(keyDownEvent, mProcessingKeyEvent, false);
+ if (mMaybeInDeadKeySequence && utf16CommitString.Length() == 1 &&
+ keyDownEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
+ mKeyboardEventWasDispatched = true;
+ // Anyway, we're not in dead key sequence anymore.
+ mMaybeInDeadKeySequence = false;
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p OnCommitCompositionNative(), FAILED, "
+ "due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+
+ // First, dispatch eKeyDown event.
+ keyDownEvent.mKeyValue = utf16CommitString;
+ nsEventStatus status = nsEventStatus_eIgnore;
+ bool dispatched = dispatcher->DispatchKeyboardEvent(
+ eKeyDown, keyDownEvent, status, mProcessingKeyEvent);
+ if (!dispatched || status == nsEventStatus_eConsumeNoDefault) {
+ mKeyboardEventWasConsumed = true;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnCommitCompositionNative(), "
+ "doesn't dispatch eKeyPress event because the preceding "
+ "eKeyDown event was not dispatched or was consumed",
+ this));
+ return;
+ }
+ if (mLastFocusedWindow != keyDownEvent.mWidget ||
+ mLastFocusedWindow->Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p OnCommitCompositionNative(), Warning, "
+ "stop dispatching eKeyPress event because the preceding "
+ "eKeyDown event caused changing focused widget or "
+ "destroyed",
+ this));
+ return;
+ }
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnCommitCompositionNative(), "
+ "dispatched eKeyDown event for the committed character",
+ this));
+
+ // Next, dispatch eKeyPress event.
+ dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status,
+ mProcessingKeyEvent);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p OnCommitCompositionNative(), "
+ "dispatched eKeyPress event for the committed character",
+ this));
+ return;
+ }
+ }
+
+ NS_ConvertUTF8toUTF16 str(commitString);
+ // Be aware, widget can be gone
+ DispatchCompositionCommitEvent(aContext, &str);
+}
+
+void IMContextWrapper::GetCompositionString(GtkIMContext* aContext,
+ nsAString& aCompositionString) {
+ gchar* preedit_string;
+ gint cursor_pos;
+ PangoAttrList* feedback_list;
+ gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
+ &cursor_pos);
+ if (preedit_string && *preedit_string) {
+ CopyUTF8toUTF16(MakeStringSpan(preedit_string), aCompositionString);
+ } else {
+ aCompositionString.Truncate();
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p GetCompositionString(aContext=0x%p), "
+ "aCompositionString=\"%s\"",
+ this, aContext, preedit_string));
+
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+}
+
+bool IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME(
+ EventMessage aFollowingEvent) {
+ if (!mLastFocusedWindow) {
+ return false;
+ }
+
+ if (!mIsKeySnooped &&
+ ((!mProcessingKeyEvent && mPostingKeyEvents.IsEmpty()) ||
+ (mProcessingKeyEvent && mKeyboardEventWasDispatched))) {
+ return true;
+ }
+
+ // A "keydown" or "keyup" event handler may change focus with the
+ // following event. In such case, we need to cancel this composition.
+ // So, we need to store IM context now because mComposingContext may be
+ // overwritten with different context if calling this method recursively.
+ // Note that we don't need to grab the context here because |context|
+ // will be used only for checking if it's same as mComposingContext.
+ GtkIMContext* oldCurrentContext = GetCurrentContext();
+ GtkIMContext* oldComposingContext = mComposingContext;
+
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+
+ if (mProcessingKeyEvent || !mPostingKeyEvents.IsEmpty()) {
+ if (mProcessingKeyEvent) {
+ mKeyboardEventWasDispatched = true;
+ }
+ // If we're not handling a key event synchronously, the signal may be
+ // sent by IME without sending key event to us. In such case, we
+ // should dispatch keyboard event for the last key event which was
+ // posted to other IME process.
+ GdkEventKey* sourceEvent = mProcessingKeyEvent
+ ? mProcessingKeyEvent
+ : mPostingKeyEvents.GetFirstEvent();
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
+ "aFollowingEvent=%s), dispatch %s %s "
+ "event: { type=%s, keyval=%s, unicode=0x%X, state=%s, "
+ "time=%u, hardware_keycode=%u, group=%u }",
+ this, ToChar(aFollowingEvent),
+ ToChar(sourceEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp),
+ mProcessingKeyEvent ? "processing" : "posted",
+ GetEventType(sourceEvent), gdk_keyval_name(sourceEvent->keyval),
+ gdk_keyval_to_unicode(sourceEvent->keyval),
+ GetEventStateName(sourceEvent->state, mIMContextID).get(),
+ sourceEvent->time, sourceEvent->hardware_keycode, sourceEvent->group));
+
+ // Let's dispatch eKeyDown event or eKeyUp event now. Note that only
+ // when we're not in a dead key composition, we should mark the
+ // eKeyDown and eKeyUp event as "processed by IME" since we should
+ // expose raw keyCode and key value to web apps the key event is a
+ // part of a dead key sequence.
+ // FYI: We should ignore if default of preceding keydown or keyup
+ // event is prevented since even on the other browsers, web
+ // applications cannot cancel the following composition event.
+ // Spec bug: https://github.com/w3c/uievents/issues/180
+ KeymapWrapper::DispatchKeyDownOrKeyUpEvent(lastFocusedWindow, sourceEvent,
+ !mMaybeInDeadKeySequence,
+ &mKeyboardEventWasConsumed);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup "
+ "event is dispatched",
+ this));
+
+ if (!mProcessingKeyEvent) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), removing first "
+ "event from the queue",
+ this));
+ mPostingKeyEvents.RemoveEvent(sourceEvent);
+ }
+ } else {
+ MOZ_ASSERT(mIsKeySnooped);
+ // Currently, we support key snooper mode of uim and wayland only.
+ MOZ_ASSERT(mIMContextID == IMContextID::Uim ||
+ mIMContextID == IMContextID::Wayland);
+ // uim sends "preedit_start" signal and "preedit_changed" separately
+ // at starting composition, "commit" and "preedit_end" separately at
+ // committing composition.
+
+ // Currently, we should dispatch only fake eKeyDown event because
+ // we cannot decide which is the last signal of each key operation
+ // and Chromium also dispatches only "keydown" event in this case.
+ bool dispatchFakeKeyDown = false;
+ switch (aFollowingEvent) {
+ case eCompositionStart:
+ case eCompositionCommit:
+ case eCompositionCommitAsIs:
+ case eContentCommandInsertText:
+ dispatchFakeKeyDown = true;
+ break;
+ // XXX Unfortunately, I don't have a good idea to prevent to
+ // dispatch redundant eKeyDown event for eCompositionStart
+ // immediately after "delete_surrounding" signal. However,
+ // not dispatching eKeyDown event is worse than dispatching
+ // redundant eKeyDown events.
+ case eContentCommandDelete:
+ dispatchFakeKeyDown = true;
+ break;
+ // We need to prevent to dispatch redundant eKeyDown event for
+ // eCompositionChange immediately after eCompositionStart. So,
+ // We should not dispatch eKeyDown event if dispatched composition
+ // string is still empty string.
+ case eCompositionChange:
+ dispatchFakeKeyDown = !mDispatchedCompositionString.IsEmpty();
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Do you forget to handle the case?");
+ break;
+ }
+
+ if (dispatchFakeKeyDown) {
+ WidgetKeyboardEvent fakeKeyDownEvent(true, eKeyDown, lastFocusedWindow);
+ fakeKeyDownEvent.mKeyCode = NS_VK_PROCESSKEY;
+ fakeKeyDownEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
+ // It's impossible to get physical key information in this case but
+ // this should be okay since web apps shouldn't do anything with
+ // physical key information during composition.
+ fakeKeyDownEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
+ "aFollowingEvent=%s), dispatch fake eKeyDown event",
+ this, ToChar(aFollowingEvent)));
+
+ KeymapWrapper::DispatchKeyDownOrKeyUpEvent(
+ lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), "
+ "fake keydown event is dispatched",
+ this));
+ }
+ }
+
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the "
+ "focused widget was destroyed/changed by a key event",
+ this));
+ return false;
+ }
+
+ // If the dispatched keydown event caused moving focus and that also
+ // caused changing active context, we need to cancel composition here.
+ if (GetCurrentContext() != oldCurrentContext) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the key "
+ "event causes changing active IM context",
+ this));
+ if (mComposingContext == oldComposingContext) {
+ // Only when the context is still composing, we should call
+ // ResetIME() here. Otherwise, it should've already been
+ // cleaned up.
+ ResetIME();
+ }
+ return false;
+ }
+
+ return true;
+}
+
+bool IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext));
+
+ if (IsComposing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "we're already in composition",
+ this));
+ return true;
+ }
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "there are no focused window in this module",
+ this));
+ return false;
+ }
+
+ if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "cannot query the selection offset",
+ this));
+ return false;
+ }
+
+ if (NS_WARN_IF(!mContentSelection->HasRange())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "due to no selection",
+ this));
+ return false;
+ }
+
+ mComposingContext = static_cast<GtkIMContext*>(g_object_ref(aContext));
+ MOZ_ASSERT(mComposingContext);
+
+ // Keep the last focused window alive
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+
+ // XXX The composition start point might be changed by composition events
+ // even though we strongly hope it doesn't happen.
+ // Every composition event should have the start offset for the result
+ // because it may high cost if we query the offset every time.
+ mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
+ mDispatchedCompositionString.Truncate();
+
+ // If this composition is started by a key press, we need to dispatch
+ // eKeyDown or eKeyUp event before dispatching eCompositionStart event.
+ // Note that dispatching a keyboard event which is marked as "processed
+ // by IME" is okay since Chromium also dispatches keyboard event as so.
+ if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionStart)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p DispatchCompositionStart(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ return false;
+ }
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ static bool sHasSetTelemetry = false;
+ if (!sHasSetTelemetry) {
+ sHasSetTelemetry = true;
+ NS_ConvertUTF8toUTF16 im(GetIMName());
+ // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
+ if (im.Length() > 72) {
+ if (NS_IS_SURROGATE_PAIR(im[72 - 2], im[72 - 1])) {
+ im.Truncate(72 - 2);
+ } else {
+ im.Truncate(72 - 1);
+ }
+ // U+2026 is "..."
+ im.Append(char16_t(0x2026));
+ }
+ Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_LINUX, im,
+ true);
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p DispatchCompositionStart(), dispatching "
+ "compositionstart... (mCompositionStart=%u)",
+ this, mCompositionStart));
+ mCompositionState = eCompositionState_CompositionStartDispatched;
+ nsEventStatus status;
+ dispatcher->StartComposition(status);
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, the focused "
+ "widget was destroyed/changed by compositionstart event",
+ this));
+ return false;
+ }
+
+ return true;
+}
+
+bool IMContextWrapper::DispatchCompositionChangeEvent(
+ GtkIMContext* aContext, const nsAString& aCompositionString) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "there are no focused window in this module",
+ this));
+ return false;
+ }
+
+ if (!IsComposing()) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p DispatchCompositionChangeEvent(), the composition "
+ "wasn't started, force starting...",
+ this));
+ if (!DispatchCompositionStart(aContext)) {
+ return false;
+ }
+ }
+ // If this composition string change caused by a key press, we need to
+ // dispatch eKeyDown or eKeyUp before dispatching eCompositionChange event.
+ else if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionChange)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p DispatchCompositionChangeEvent(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ return false;
+ }
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ // Store the selected string which will be removed by following
+ // compositionchange event.
+ if (mCompositionState == eCompositionState_CompositionStartDispatched) {
+ if (NS_WARN_IF(!EnsureToCacheContentSelection(
+ &mSelectedStringRemovedByComposition))) {
+ // XXX How should we behave in this case??
+ } else if (mContentSelection->HasRange()) {
+ // XXX We should assume, for now, any web applications don't change
+ // selection at handling this compositionchange event.
+ mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
+ } else {
+ // If there is no selection range, we should keep previously storing
+ // mCompositionStart.
+ }
+ }
+
+ RefPtr<TextRangeArray> rangeArray =
+ CreateTextRangeArray(aContext, aCompositionString);
+
+ rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to SetPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ mCompositionState = eCompositionState_CompositionChangeEventDispatched;
+
+ // We cannot call SetCursorPosition for e10s-aware.
+ // DispatchEvent is async on e10s, so composition rect isn't updated now
+ // on tab parent.
+ mDispatchedCompositionString = aCompositionString;
+ mLayoutChanged = false;
+ mCompositionTargetRange.mOffset =
+ mCompositionStart + rangeArray->TargetClauseOffset();
+ mCompositionTargetRange.mLength = rangeArray->TargetClauseLength();
+
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+ nsEventStatus status;
+ rv = dispatcher->FlushPendingComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to FlushPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, the "
+ "focused widget was destroyed/changed by "
+ "compositionchange event",
+ this));
+ return false;
+ }
+ return true;
+}
+
+bool IMContextWrapper::DispatchCompositionCommitEvent(
+ GtkIMContext* aContext, const nsAString* aCommitString) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, "
+ "aCommitString=0x%p, (\"%s\"))",
+ this, aContext, aCommitString,
+ aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "there are no focused window in this module",
+ this));
+ return false;
+ }
+
+ // TODO: We need special care to handle request to commit composition
+ // by content while we're committing composition because we have
+ // commit string information now but IME may not have composition
+ // anymore. Therefore, we may not be able to handle commit as
+ // expected. However, this is rare case because this situation
+ // never occurs with remote content. So, it's okay to fix this
+ // issue later. (Perhaps, TextEventDisptcher should do it for
+ // all platforms. E.g., creating WillCommitComposition()?)
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+ RefPtr<TextEventDispatcher> dispatcher;
+ if (!IsComposing() &&
+ !StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
+ if (!aCommitString || aCommitString->IsEmpty()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "did nothing due to inserting empty string without composition",
+ this));
+ return true;
+ }
+ if (MOZ_UNLIKELY(!EnsureToCacheContentSelection())) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p DispatchCompositionCommitEvent(), Warning, "
+ "Failed to cache selection before dispatching "
+ "eContentCommandInsertText event",
+ this));
+ }
+ if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandInsertText)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p DispatchCompositionCommitEvent(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ return false;
+ }
+ // Emulate selection until receiving actual selection range. This is
+ // important for OnSelectionChange. If selection is not changed by web
+ // apps, i.e., selection range is same as what selection expects, we
+ // shouldn't reset IME because the trigger of causing this commit may be an
+ // input for next composition and we shouldn't cancel it.
+ if (mContentSelection.isSome()) {
+ mContentSelection->Collapse(
+ (mContentSelection->HasRange()
+ ? mContentSelection->OffsetAndDataRef().StartOffset()
+ : mCompositionStart) +
+ aCommitString->Length());
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p DispatchCompositionCommitEvent(), mContentSelection=%s",
+ this, ToString(mContentSelection).c_str()));
+ }
+ MOZ_ASSERT(!dispatcher);
+ } else {
+ if (!IsComposing()) {
+ if (!aCommitString || aCommitString->IsEmpty()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "there is no composition and empty commit string",
+ this));
+ return true;
+ }
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p DispatchCompositionCommitEvent(), "
+ "the composition wasn't started, force starting...",
+ this));
+ if (!DispatchCompositionStart(aContext)) {
+ return false;
+ }
+ }
+ // If this commit caused by a key press, we need to dispatch eKeyDown or
+ // eKeyUp before dispatching composition events.
+ else if (!MaybeDispatchKeyEventAsProcessedByIME(
+ aCommitString ? eCompositionCommit : eCompositionCommitAsIs)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p DispatchCompositionCommitEvent(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ mCompositionState = eCompositionState_NotComposing;
+ return false;
+ }
+
+ dispatcher = GetTextEventDispatcher();
+ MOZ_ASSERT(dispatcher);
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ // Emulate selection until receiving actual selection range.
+ const uint32_t offsetToPutCaret =
+ mCompositionStart + (aCommitString
+ ? aCommitString->Length()
+ : mDispatchedCompositionString.Length());
+ if (mContentSelection.isSome()) {
+ mContentSelection->Collapse(offsetToPutCaret);
+ } else {
+ // TODO: We should guarantee that there should be at least fake selection
+ // for IME at here. Then, we can keep the last writing mode.
+ mContentSelection.emplace(offsetToPutCaret, WritingMode());
+ }
+ }
+
+ mCompositionState = eCompositionState_NotComposing;
+ // Reset dead key sequence too because GTK doesn't support dead key chain
+ // (i.e., a key press doesn't cause both producing some characters and
+ // restarting new dead key sequence at one time). So, committing
+ // composition means end of a dead key sequence.
+ mMaybeInDeadKeySequence = false;
+ mCompositionStart = UINT32_MAX;
+ mCompositionTargetRange.Clear();
+ mDispatchedCompositionString.Truncate();
+ mSelectedStringRemovedByComposition.Truncate();
+
+ if (!dispatcher) {
+ MOZ_ASSERT(aCommitString);
+ MOZ_ASSERT(!aCommitString->IsEmpty());
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText,
+ lastFocusedWindow);
+ insertTextEvent.mString.emplace(*aCommitString);
+ lastFocusedWindow->DispatchEvent(&insertTextEvent, status);
+
+ if (!insertTextEvent.mSucceeded) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, inserting "
+ "text failed",
+ this));
+ return false;
+ }
+ } else {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ nsresult rv = dispatcher->CommitComposition(status, aCommitString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to CommitComposition() failure",
+ this));
+ return false;
+ }
+ }
+
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "the focused widget was destroyed/changed by "
+ "compositioncommit event",
+ this));
+ return false;
+ }
+
+ return true;
+}
+
+already_AddRefed<TextRangeArray> IMContextWrapper::CreateTextRangeArray(
+ GtkIMContext* aContext, const nsAString& aCompositionString) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p CreateTextRangeArray(aContext=0x%p, "
+ "aCompositionString=\"%s\" (Length()=%zu))",
+ this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(),
+ aCompositionString.Length()));
+
+ RefPtr<TextRangeArray> textRangeArray = new TextRangeArray();
+
+ gchar* preedit_string;
+ gint cursor_pos_in_chars;
+ PangoAttrList* feedback_list;
+ gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
+ &cursor_pos_in_chars);
+ if (!preedit_string || !*preedit_string) {
+ if (!aCompositionString.IsEmpty()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p CreateTextRangeArray(), FAILED, due to "
+ "preedit_string is null",
+ this));
+ }
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+ return textRangeArray.forget();
+ }
+
+ // Convert caret offset from offset in characters to offset in UTF-16
+ // string. If we couldn't proper offset in UTF-16 string, we should
+ // assume that the caret is at the end of the composition string.
+ uint32_t caretOffsetInUTF16 = aCompositionString.Length();
+ if (NS_WARN_IF(cursor_pos_in_chars < 0)) {
+ // Note that this case is undocumented. We should assume that the
+ // caret is at the end of the composition string.
+ } else if (cursor_pos_in_chars == 0) {
+ caretOffsetInUTF16 = 0;
+ } else {
+ gchar* charAfterCaret =
+ g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars);
+ if (NS_WARN_IF(!charAfterCaret)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), failed to get UTF-8 "
+ "string before the caret (cursor_pos_in_chars=%d)",
+ this, cursor_pos_in_chars));
+ } else {
+ glong caretOffset = 0;
+ gunichar2* utf16StrBeforeCaret =
+ g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string,
+ nullptr, &caretOffset, nullptr);
+ if (NS_WARN_IF(!utf16StrBeforeCaret) || NS_WARN_IF(caretOffset < 0)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), WARNING, failed to "
+ "convert to UTF-16 string before the caret "
+ "(cursor_pos_in_chars=%d, caretOffset=%ld)",
+ this, cursor_pos_in_chars, caretOffset));
+ } else {
+ caretOffsetInUTF16 = static_cast<uint32_t>(caretOffset);
+ uint32_t compositionStringLength = aCompositionString.Length();
+ if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), WARNING, "
+ "caretOffsetInUTF16=%u is larger than "
+ "compositionStringLength=%u",
+ this, caretOffsetInUTF16, compositionStringLength));
+ caretOffsetInUTF16 = compositionStringLength;
+ }
+ }
+ if (utf16StrBeforeCaret) {
+ g_free(utf16StrBeforeCaret);
+ }
+ }
+ }
+
+ PangoAttrIterator* iter;
+ iter = pango_attr_list_get_iterator(feedback_list);
+ if (!iter) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p CreateTextRangeArray(), FAILED, iterator couldn't "
+ "be allocated",
+ this));
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+ return textRangeArray.forget();
+ }
+
+ uint32_t minOffsetOfClauses = aCompositionString.Length();
+ uint32_t maxOffsetOfClauses = 0;
+ do {
+ TextRange range;
+ if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) {
+ continue;
+ }
+ MOZ_ASSERT(range.Length());
+ minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset);
+ maxOffsetOfClauses = std::max(maxOffsetOfClauses, range.mEndOffset);
+ textRangeArray->AppendElement(range);
+ } while (pango_attr_iterator_next(iter));
+
+ // If the IME doesn't define clause from the start of the composition,
+ // we should insert dummy clause information since TextRangeArray assumes
+ // that there must be a clause whose start is 0 when there is one or
+ // more clauses.
+ if (minOffsetOfClauses) {
+ TextRange dummyClause;
+ dummyClause.mStartOffset = 0;
+ dummyClause.mEndOffset = minOffsetOfClauses;
+ dummyClause.mRangeType = TextRangeType::eRawClause;
+ textRangeArray->InsertElementAt(0, dummyClause);
+ maxOffsetOfClauses = std::max(maxOffsetOfClauses, dummyClause.mEndOffset);
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), inserting a dummy clause "
+ "at the beginning of the composition string mStartOffset=%u, "
+ "mEndOffset=%u, mRangeType=%s",
+ this, dummyClause.mStartOffset, dummyClause.mEndOffset,
+ ToChar(dummyClause.mRangeType)));
+ }
+
+ // If the IME doesn't define clause at end of the composition, we should
+ // insert dummy clause information since TextRangeArray assumes that there
+ // must be a clase whose end is the length of the composition string when
+ // there is one or more clauses.
+ if (!textRangeArray->IsEmpty() &&
+ maxOffsetOfClauses < aCompositionString.Length()) {
+ TextRange dummyClause;
+ dummyClause.mStartOffset = maxOffsetOfClauses;
+ dummyClause.mEndOffset = aCompositionString.Length();
+ dummyClause.mRangeType = TextRangeType::eRawClause;
+ textRangeArray->AppendElement(dummyClause);
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), inserting a dummy clause "
+ "at the end of the composition string mStartOffset=%u, "
+ "mEndOffset=%u, mRangeType=%s",
+ this, dummyClause.mStartOffset, dummyClause.mEndOffset,
+ ToChar(dummyClause.mRangeType)));
+ }
+
+ TextRange range;
+ range.mStartOffset = range.mEndOffset = caretOffsetInUTF16;
+ range.mRangeType = TextRangeType::eCaret;
+ textRangeArray->AppendElement(range);
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p CreateTextRangeArray(), mStartOffset=%u, "
+ "mEndOffset=%u, mRangeType=%s",
+ this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
+
+ pango_attr_iterator_destroy(iter);
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+
+ return textRangeArray.forget();
+}
+
+/* static */
+nscolor IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor) {
+ PangoColor& pangoColor = aPangoAttrColor->color;
+ uint8_t r = pangoColor.red / 0x100;
+ uint8_t g = pangoColor.green / 0x100;
+ uint8_t b = pangoColor.blue / 0x100;
+ return NS_RGB(r, g, b);
+}
+
+bool IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter,
+ const gchar* aUTF8CompositionString,
+ uint32_t aUTF16CaretOffset,
+ TextRange& aTextRange) const {
+ // Set the range offsets in UTF-16 string.
+ gint utf8ClauseStart, utf8ClauseEnd;
+ pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd);
+ if (utf8ClauseStart == utf8ClauseEnd) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetTextRange(), FAILED, due to collapsed range", this));
+ return false;
+ }
+
+ if (!utf8ClauseStart) {
+ aTextRange.mStartOffset = 0;
+ } else {
+ glong utf16PreviousClausesLength;
+ gunichar2* utf16PreviousClausesString =
+ g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr,
+ &utf16PreviousClausesLength, nullptr);
+
+ if (NS_WARN_IF(!utf16PreviousClausesString)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
+ "failure (retrieving previous string of current clause)",
+ this));
+ return false;
+ }
+
+ aTextRange.mStartOffset = utf16PreviousClausesLength;
+ g_free(utf16PreviousClausesString);
+ }
+
+ glong utf16CurrentClauseLength;
+ gunichar2* utf16CurrentClauseString = g_utf8_to_utf16(
+ aUTF8CompositionString + utf8ClauseStart, utf8ClauseEnd - utf8ClauseStart,
+ nullptr, &utf16CurrentClauseLength, nullptr);
+
+ if (NS_WARN_IF(!utf16CurrentClauseString)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
+ "failure (retrieving current clause)",
+ this));
+ return false;
+ }
+
+ // iBus Chewing IME tells us that there is an empty clause at the end of
+ // the composition string but we should ignore it since our code doesn't
+ // assume that there is an empty clause.
+ if (!utf16CurrentClauseLength) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p SetTextRange(), FAILED, due to current clause length "
+ "is 0",
+ this));
+ return false;
+ }
+
+ aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength;
+ g_free(utf16CurrentClauseString);
+ utf16CurrentClauseString = nullptr;
+
+ // Set styles
+ TextRangeStyle& style = aTextRange.mRangeStyle;
+
+ // Underline
+ PangoAttrInt* attrUnderline = reinterpret_cast<PangoAttrInt*>(
+ pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE));
+ if (attrUnderline) {
+ switch (attrUnderline->value) {
+ case PANGO_UNDERLINE_NONE:
+ style.mLineStyle = TextRangeStyle::LineStyle::None;
+ break;
+ case PANGO_UNDERLINE_DOUBLE:
+ style.mLineStyle = TextRangeStyle::LineStyle::Double;
+ break;
+ case PANGO_UNDERLINE_ERROR:
+ style.mLineStyle = TextRangeStyle::LineStyle::Wavy;
+ break;
+ case PANGO_UNDERLINE_SINGLE:
+ case PANGO_UNDERLINE_LOW:
+ style.mLineStyle = TextRangeStyle::LineStyle::Solid;
+ break;
+ default:
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p SetTextRange(), retrieved unknown underline "
+ "style: %d",
+ this, attrUnderline->value));
+ style.mLineStyle = TextRangeStyle::LineStyle::Solid;
+ break;
+ }
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
+
+ // Underline color
+ PangoAttrColor* attrUnderlineColor = reinterpret_cast<PangoAttrColor*>(
+ pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE_COLOR));
+ if (attrUnderlineColor) {
+ style.mUnderlineColor = ToNscolor(attrUnderlineColor);
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR;
+ }
+ } else {
+ style.mLineStyle = TextRangeStyle::LineStyle::None;
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
+ }
+
+ // Don't set colors if they are not specified. They should be computed by
+ // textframe if only one of the colors are specified.
+
+ // Foreground color (text color)
+ PangoAttrColor* attrForeground = reinterpret_cast<PangoAttrColor*>(
+ pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND));
+ if (attrForeground) {
+ style.mForegroundColor = ToNscolor(attrForeground);
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR;
+ }
+
+ // Background color
+ PangoAttrColor* attrBackground = reinterpret_cast<PangoAttrColor*>(
+ pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND));
+ if (attrBackground) {
+ style.mBackgroundColor = ToNscolor(attrBackground);
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR;
+ }
+
+ /**
+ * We need to judge the meaning of the clause for a11y. Before we support
+ * IME specific composition string style, we used following rules:
+ *
+ * 1: If attrUnderline and attrForground are specified, we assumed the
+ * clause is TextRangeType::eSelectedClause.
+ * 2: If only attrUnderline is specified, we assumed the clause is
+ * TextRangeType::eConvertedClause.
+ * 3: If only attrForground is specified, we assumed the clause is
+ * TextRangeType::eSelectedRawClause.
+ * 4: If neither attrUnderline nor attrForeground is specified, we assumed
+ * the clause is TextRangeType::eRawClause.
+ *
+ * However, this rules are odd since there can be two or more selected
+ * clauses. Additionally, our old rules caused that IME developers/users
+ * cannot specify composition string style as they want.
+ *
+ * So, we shouldn't guess the meaning from its visual style.
+ */
+
+ // If the range covers whole of composition string and the caret is at
+ // the end of the composition string, the range is probably not converted.
+ if (!utf8ClauseStart &&
+ utf8ClauseEnd == static_cast<gint>(strlen(aUTF8CompositionString)) &&
+ aTextRange.mEndOffset == aUTF16CaretOffset) {
+ aTextRange.mRangeType = TextRangeType::eRawClause;
+ }
+ // Typically, the caret is set at the start of the selected clause.
+ // So, if the caret is in the clause, we can assume that the clause is
+ // selected.
+ else if (aTextRange.mStartOffset <= aUTF16CaretOffset &&
+ aTextRange.mEndOffset > aUTF16CaretOffset) {
+ aTextRange.mRangeType = TextRangeType::eSelectedClause;
+ }
+ // Otherwise, we should assume that the clause is converted but not
+ // selected.
+ else {
+ aTextRange.mRangeType = TextRangeType::eConvertedClause;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p SetTextRange(), succeeded, aTextRange= { "
+ "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }",
+ this, aTextRange.mStartOffset, aTextRange.mEndOffset,
+ ToChar(aTextRange.mRangeType),
+ GetTextRangeStyleText(aTextRange.mRangeStyle).get()));
+
+ return true;
+}
+
+void IMContextWrapper::SetCursorPosition(GtkIMContext* aContext) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p SetCursorPosition(aContext=0x%p), "
+ "mCompositionTargetRange={ mOffset=%u, mLength=%u }, "
+ "mContentSelection=%s",
+ this, aContext, mCompositionTargetRange.mOffset,
+ mCompositionTargetRange.mLength, ToString(mContentSelection).c_str()));
+
+ bool useCaret = false;
+ if (!mCompositionTargetRange.IsValid()) {
+ if (mContentSelection.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, "
+ "mCompositionTargetRange and mContentSelection are invalid",
+ this));
+ return;
+ }
+ if (!mContentSelection->HasRange()) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p SetCursorPosition(), FAILED, "
+ "mCompositionTargetRange is invalid and there is no selection",
+ this));
+ return;
+ }
+ useCaret = true;
+ }
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, due to no focused "
+ "window",
+ this));
+ return;
+ }
+
+ if (MOZ_UNLIKELY(!aContext)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, due to no context", this));
+ return;
+ }
+
+ WidgetQueryContentEvent queryCaretOrTextRectEvent(
+ true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow);
+ if (useCaret) {
+ queryCaretOrTextRectEvent.InitForQueryCaretRect(
+ mContentSelection->OffsetAndDataRef().StartOffset());
+ } else {
+ if (mContentSelection->WritingModeRef().IsVertical()) {
+ // For preventing the candidate window to overlap the target
+ // clause, we should set fake (typically, very tall) caret rect.
+ uint32_t length =
+ mCompositionTargetRange.mLength ? mCompositionTargetRange.mLength : 1;
+ queryCaretOrTextRectEvent.InitForQueryTextRect(
+ mCompositionTargetRange.mOffset, length);
+ } else {
+ queryCaretOrTextRectEvent.InitForQueryTextRect(
+ mCompositionTargetRange.mOffset, 1);
+ }
+ }
+ nsEventStatus status;
+ mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status);
+ if (queryCaretOrTextRectEvent.Failed()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, %s was failed", this,
+ useCaret ? "eQueryCaretRect" : "eQueryTextRect"));
+ return;
+ }
+
+ nsWindow* rootWindow =
+ static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());
+
+ // Get the position of the rootWindow in screen.
+ LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset();
+
+ // Get the position of IM context owner window in screen.
+ LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset();
+
+ // Compute the caret position in the IM owner window.
+ LayoutDeviceIntRect rect =
+ queryCaretOrTextRectEvent.mReply->mRect + root - owner;
+ rect.width = 0;
+ GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect);
+
+ gtk_im_context_set_cursor_location(aContext, &area);
+}
+
+nsresult IMContextWrapper::GetCurrentParagraph(nsAString& aText,
+ uint32_t& aCursorPos) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p GetCurrentParagraph(), mCompositionState=%s", this,
+ GetCompositionStateName()));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, there are no "
+ "focused window in this module",
+ this));
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsEventStatus status;
+
+ uint32_t selOffset = mCompositionStart;
+ uint32_t selLength = mSelectedStringRemovedByComposition.Length();
+
+ // If focused editor doesn't have composition string, we should use
+ // current selection.
+ if (!EditorHasCompositionString()) {
+ // Query cursor position & selection
+ if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, due to no "
+ "valid selection information",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mContentSelection.isSome() && mContentSelection->HasRange()) {
+ selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
+ selLength = mContentSelection->OffsetAndDataRef().Length();
+ } else {
+ // If there is no range, let's get all text instead...
+ selOffset = 0u;
+ selLength = INT32_MAX; // TODO: Change to UINT32_MAX, but see below
+ }
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u", this,
+ selOffset, selLength));
+
+ // XXX nsString::Find and nsString::RFind take int32_t for offset, so,
+ // we cannot support this request when the current offset is larger
+ // than INT32_MAX.
+ if (selOffset > INT32_MAX || selLength > INT32_MAX ||
+ selOffset + selLength > INT32_MAX) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, The selection is "
+ "out of range",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get all text contents of the focused editor
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ mLastFocusedWindow);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
+ if (NS_WARN_IF(queryTextContentEvent.Failed())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (selOffset + selLength > queryTextContentEvent.mReply->DataLength()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, The selection is "
+ "invalid, queryTextContentEvent={ mReply=%s }",
+ this, ToString(queryTextContentEvent.mReply).c_str()));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Remove composing string and restore the selected string because
+ // GtkEntry doesn't remove selected string until committing, however,
+ // our editor does it. We should emulate the behavior for IME.
+ nsAutoString textContent(queryTextContentEvent.mReply->DataRef());
+ if (EditorHasCompositionString() &&
+ mDispatchedCompositionString != mSelectedStringRemovedByComposition) {
+ textContent.Replace(mCompositionStart,
+ mDispatchedCompositionString.Length(),
+ mSelectedStringRemovedByComposition);
+ }
+
+ // Get only the focused paragraph, by looking for newlines
+ int32_t parStart = 0;
+ if (selOffset > 0) {
+ parStart = Substring(textContent, 0, selOffset - 1).RFind(u"\n") + 1;
+ }
+ int32_t parEnd = textContent.Find(u"\n", selOffset + selLength);
+ if (parEnd < 0) {
+ parEnd = textContent.Length();
+ }
+ aText = nsDependentSubstring(textContent, parStart, parEnd - parStart);
+ aCursorPos = selOffset - uint32_t(parStart);
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p GetCurrentParagraph(), succeeded, aText=%s, "
+ "aText.Length()=%zu, aCursorPos=%u",
+ this, NS_ConvertUTF16toUTF8(aText).get(), aText.Length(), aCursorPos));
+
+ return NS_OK;
+}
+
+nsresult IMContextWrapper::DeleteText(GtkIMContext* aContext, int32_t aOffset,
+ uint32_t aNChars) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), "
+ "mCompositionState=%s",
+ this, aContext, aOffset, aNChars, GetCompositionStateName()));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, there are no focused window "
+ "in this module",
+ this));
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (!aNChars) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, aNChars must not be zero", this));
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+ nsEventStatus status;
+
+ // First, we should cancel current composition because editor cannot
+ // handle changing selection and deleting text.
+ uint32_t selOffset;
+ bool wasComposing = IsComposing();
+ bool editorHadCompositionString = EditorHasCompositionString();
+ if (wasComposing) {
+ selOffset = mCompositionStart;
+ if (!DispatchCompositionCommitEvent(aContext,
+ &mSelectedStringRemovedByComposition)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, quitting from DeletText", this));
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, due to no valid selection "
+ "information",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+ if (!mContentSelection->HasRange()) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p DeleteText(), does nothing, due to no selection range",
+ this));
+ return NS_OK;
+ }
+ selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
+ }
+
+ // Get all text contents of the focused editor
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ mLastFocusedWindow);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
+ if (NS_WARN_IF(queryTextContentEvent.Failed())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (queryTextContentEvent.mReply->IsDataEmpty()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, there is no contents", this));
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(
+ queryTextContentEvent.mReply->DataRef(), 0, selOffset));
+ glong offsetInUTF8Characters =
+ g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset;
+ if (offsetInUTF8Characters < 0) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, aOffset is too small for "
+ "current cursor pos (computed offset: %ld)",
+ this, offsetInUTF8Characters));
+ return NS_ERROR_FAILURE;
+ }
+
+ AppendUTF16toUTF8(
+ nsDependentSubstring(queryTextContentEvent.mReply->DataRef(), selOffset),
+ utf8Str);
+ glong countOfCharactersInUTF8 =
+ g_utf8_strlen(utf8Str.get(), utf8Str.Length());
+ glong endInUTF8Characters = offsetInUTF8Characters + aNChars;
+ if (countOfCharactersInUTF8 < endInUTF8Characters) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, aNChars is too large for "
+ "current contents (content length: %ld, computed end offset: %ld)",
+ this, countOfCharactersInUTF8, endInUTF8Characters));
+ return NS_ERROR_FAILURE;
+ }
+
+ gchar* charAtOffset =
+ g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters);
+ gchar* charAtEnd =
+ g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters);
+
+ // Set selection to delete
+ WidgetSelectionEvent selectionEvent(true, eSetSelection, mLastFocusedWindow);
+
+ nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0,
+ charAtOffset - utf8Str.get());
+ selectionEvent.mOffset = NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length();
+
+ nsDependentCSubstring utf8DeletingStr(utf8Str, utf8StrBeforeOffset.Length(),
+ charAtEnd - charAtOffset);
+ selectionEvent.mLength = NS_ConvertUTF8toUTF16(utf8DeletingStr).Length();
+
+ selectionEvent.mReversed = false;
+ selectionEvent.mExpandToClusterBoundary = false;
+ lastFocusedWindow->DispatchEvent(&selectionEvent, status);
+
+ if (!selectionEvent.mSucceeded || lastFocusedWindow != mLastFocusedWindow ||
+ lastFocusedWindow->Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, setting selection caused "
+ "focus change or window destroyed",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ // If this deleting text caused by a key press, we need to dispatch
+ // eKeyDown or eKeyUp before dispatching eContentCommandDelete event.
+ if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandDelete)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p DeleteText(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Delete the selection
+ WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete,
+ mLastFocusedWindow);
+ mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
+
+ if (!contentCommandEvent.mSucceeded ||
+ lastFocusedWindow != mLastFocusedWindow ||
+ lastFocusedWindow->Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, deleting the selection caused "
+ "focus change or window destroyed",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!wasComposing) {
+ return NS_OK;
+ }
+
+ // Restore the composition at new caret position.
+ if (!DispatchCompositionStart(aContext)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, resterting composition start", this));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!editorHadCompositionString) {
+ return NS_OK;
+ }
+
+ nsAutoString compositionString;
+ GetCompositionString(aContext, compositionString);
+ if (!DispatchCompositionChangeEvent(aContext, compositionString)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, restoring composition string", this));
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+bool IMContextWrapper::EnsureToCacheContentSelection(
+ nsAString* aSelectedString) {
+ if (aSelectedString) {
+ aSelectedString->Truncate();
+ }
+
+ if (mContentSelection.isSome()) {
+ if (mContentSelection->HasRange() && aSelectedString) {
+ aSelectedString->Assign(mContentSelection->OffsetAndDataRef().DataRef());
+ }
+ return true;
+ }
+
+ RefPtr<nsWindow> dispatcherWindow =
+ mLastFocusedWindow ? mLastFocusedWindow : mOwnerWindow;
+ if (NS_WARN_IF(!dispatcherWindow)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
+ "no focused window",
+ this));
+ return false;
+ }
+
+ nsEventStatus status;
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ dispatcherWindow);
+ dispatcherWindow->DispatchEvent(&querySelectedTextEvent, status);
+ if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
+ "failure of query selection event",
+ this));
+ return false;
+ }
+
+ mContentSelection = Some(ContentSelection(querySelectedTextEvent));
+ if (mContentSelection->HasRange()) {
+ if (!mContentSelection->OffsetAndDataRef().IsDataEmpty() &&
+ aSelectedString) {
+ aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef());
+ }
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p EnsureToCacheContentSelection(), Succeeded, mContentSelection=%s",
+ this, ToString(mContentSelection).c_str()));
+ return true;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/IMContextWrapper.h b/widget/gtk/IMContextWrapper.h
new file mode 100644
index 0000000000..213c5ce8d3
--- /dev/null
+++ b/widget/gtk/IMContextWrapper.h
@@ -0,0 +1,692 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef IMContextWrapper_h_
+#define IMContextWrapper_h_
+
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsIWidget.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ContentData.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/widget/IMEData.h"
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * KeyHandlingState is result of IMContextWrapper::OnKeyEvent().
+ */
+enum class KeyHandlingState {
+ // The native key event has not been handled by IMContextWrapper.
+ eNotHandled,
+ // The native key event was handled by IMContextWrapper.
+ eHandled,
+ // The native key event has not been handled by IMContextWrapper,
+ // but eKeyDown or eKeyUp event has been dispatched.
+ eNotHandledButEventDispatched,
+ // The native key event has not been handled by IMContextWrapper,
+ // but eKeyDown or eKeyUp event has been dispatched and consumed.
+ eNotHandledButEventConsumed,
+};
+
+class IMContextWrapper final : public TextEventDispatcherListener {
+ public:
+ // TextEventDispatcherListener implementation
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) override;
+ NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
+ NS_IMETHOD_(void)
+ OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;
+ NS_IMETHOD_(void)
+ WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) override;
+
+ public:
+ // aOwnerWindow is a pointer of the owner window. When aOwnerWindow is
+ // destroyed, the related IME contexts are released (i.e., IME cannot be
+ // used with the instance after that).
+ explicit IMContextWrapper(nsWindow* aOwnerWindow);
+
+ // Called when the process is being shut down.
+ static void Shutdown();
+
+ // "Enabled" means the users can use all IMEs.
+ // I.e., the focus is in the normal editors.
+ bool IsEnabled() const;
+
+ // OnFocusWindow is a notification that aWindow is going to be focused.
+ void OnFocusWindow(nsWindow* aWindow);
+ // OnBlurWindow is a notification that aWindow is going to be unfocused.
+ void OnBlurWindow(nsWindow* aWindow);
+ // OnDestroyWindow is a notification that aWindow is going to be destroyed.
+ void OnDestroyWindow(nsWindow* aWindow);
+ // OnFocusChangeInGecko is a notification that an editor gets focus.
+ void OnFocusChangeInGecko(bool aFocus);
+ // OnSelectionChange is a notification that selection (caret) is changed
+ // in the focused editor.
+ void OnSelectionChange(nsWindow* aCaller,
+ const IMENotification& aIMENotification);
+ // OnThemeChanged is called when desktop theme is changed.
+ static void OnThemeChanged();
+
+ /**
+ * OnKeyEvent() is called when aWindow gets a native key press event or a
+ * native key release event. If this returns true, the key event was
+ * filtered by IME. Otherwise, this returns false.
+ * NOTE: When the native key press event starts composition, this returns
+ * true but dispatches an eKeyDown event or eKeyUp event before
+ * dispatching composition events or content command event.
+ *
+ * @param aWindow A window on which user operate the
+ * key.
+ * @param aEvent A native key press or release
+ * event.
+ * @param aKeyboardEventWasDispatched true if eKeyDown or eKeyUp event
+ * for aEvent has already been
+ * dispatched. In this case,
+ * this class doesn't dispatch
+ * keyboard event anymore.
+ */
+ KeyHandlingState OnKeyEvent(nsWindow* aWindow, GdkEventKey* aEvent,
+ bool aKeyboardEventWasDispatched = false);
+
+ // IME related nsIWidget methods.
+ nsresult EndIMEComposition(nsWindow* aCaller);
+ void SetInputContext(nsWindow* aCaller, const InputContext* aContext,
+ const InputContextAction* aAction);
+ InputContext GetInputContext();
+ void OnUpdateComposition();
+ void OnLayoutChange();
+
+ // Set GdkWindow associated with IM context.
+ // It can be null which disables context operations.
+ void SetGdkWindow(GdkWindow* aGdkWindow);
+
+ TextEventDispatcher* GetTextEventDispatcher();
+
+ // TODO: Typically, new IM comes every several years. And now, our code
+ // becomes really IM behavior dependent. So, perhaps, we need prefs
+ // to control related flags for IM developers.
+ enum class IMContextID : uint8_t {
+ Fcitx, // 4.x or earlier
+ Fcitx5,
+ IBus,
+ IIIMF,
+ Scim,
+ Uim,
+ Wayland,
+ Unknown,
+ };
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const IMContextID& aIMContextID) {
+ switch (aIMContextID) {
+ case IMContextID::Fcitx:
+ return aStream << "Fcitx";
+ case IMContextID::Fcitx5:
+ return aStream << "Fcitx5";
+ case IMContextID::IBus:
+ return aStream << "IBus";
+ case IMContextID::IIIMF:
+ return aStream << "IIIMF";
+ case IMContextID::Scim:
+ return aStream << "Scim";
+ case IMContextID::Uim:
+ return aStream << "Uim";
+ case IMContextID::Wayland:
+ return aStream << "Wayland";
+ case IMContextID::Unknown:
+ return aStream << "Unknown";
+ }
+ MOZ_ASSERT_UNREACHABLE("Add new case for the new IM support");
+ return aStream << "Unknown";
+ }
+
+ /**
+ * GetIMName() returns IM name associated with mContext. If the context is
+ * xim, this look for actual engine from XMODIFIERS environment variable.
+ */
+ nsDependentCSubstring GetIMName() const;
+
+ /**
+ * GetWaitingSynthesizedKeyPressHardwareKeyCode() returns hardware_keycode
+ * value of last handled GDK_KEY_PRESS event which is probable handled by
+ * IME asynchronously and we have not received synthesized GDK_KEY_PRESS
+ * event yet.
+ */
+ static guint16 GetWaitingSynthesizedKeyPressHardwareKeyCode() {
+ return sWaitingSynthesizedKeyPressHardwareKeyCode;
+ }
+
+ protected:
+ ~IMContextWrapper();
+
+ /**
+ * SetInputPurposeAndInputHints() sets input-purpose and input-hints of
+ * current IM context to the values computed with mInputContext.
+ */
+ void SetInputPurposeAndInputHints();
+
+ // Owner of an instance of this class. This should be top level window.
+ // The owner window must release the contexts when it's destroyed because
+ // the IME contexts need the native window. If OnDestroyWindow() is called
+ // with the owner window, it'll release IME contexts. Otherwise, it'll
+ // just clean up any existing composition if it's related to the destroying
+ // child window.
+ nsWindow* mOwnerWindow;
+
+ // A last focused window in this class's context.
+ nsWindow* mLastFocusedWindow;
+
+ // Actual context. This is used for handling the user's input.
+ GtkIMContext* mContext;
+
+ // mSimpleContext is used for the password field and
+ // the |ime-mode: disabled;| editors if sUseSimpleContext is true.
+ // These editors disable IME. But dead keys should work. Fortunately,
+ // the simple IM context of GTK2 support only them.
+ GtkIMContext* mSimpleContext;
+
+ // mDummyContext is a dummy context and will be used in Focus()
+ // when the state of mEnabled means disabled. This context's IME state is
+ // always "closed", so it closes IME forcedly.
+ GtkIMContext* mDummyContext;
+
+ // mComposingContext is not nullptr while one of mContext, mSimpleContext
+ // and mDummyContext has composition.
+ // XXX: We don't assume that two or more context have composition same time.
+ GtkIMContext* mComposingContext;
+
+ // IME enabled state and other things defined in InputContext.
+ // Use following helper methods if you don't need the detail of the status.
+ InputContext mInputContext;
+
+ // mCompositionStart is the start offset of the composition string in the
+ // current content. When <textarea> or <input> have focus, it means offset
+ // from the first character of them. When a HTML editor has focus, it
+ // means offset from the first character of the root element of the editor.
+ uint32_t mCompositionStart;
+
+ // mDispatchedCompositionString is the latest composition string which
+ // was dispatched by compositionupdate event.
+ nsString mDispatchedCompositionString;
+
+ // mSelectedStringRemovedByComposition is the selected string which was
+ // removed by first compositionchange event.
+ nsString mSelectedStringRemovedByComposition;
+
+ // OnKeyEvent() temporarily sets mProcessingKeyEvent to the given native
+ // event.
+ GdkEventKey* mProcessingKeyEvent;
+
+ /**
+ * GdkEventKeyQueue stores *copy* of GdkEventKey instances. However, this
+ * must be safe to our usecase since it has |time| and the value should not
+ * be same as older event.
+ */
+ class GdkEventKeyQueue final {
+ public:
+ ~GdkEventKeyQueue() { Clear(); }
+
+ void Clear() { mEvents.Clear(); }
+
+ /**
+ * PutEvent() puts new event into the queue.
+ */
+ void PutEvent(const GdkEventKey* aEvent) {
+ GdkEventKey* newEvent = reinterpret_cast<GdkEventKey*>(
+ gdk_event_copy(reinterpret_cast<const GdkEvent*>(aEvent)));
+ newEvent->state &= GDK_MODIFIER_MASK;
+ mEvents.AppendElement(newEvent);
+ }
+
+ /**
+ * RemoveEvent() removes oldest same event and its preceding events
+ * from the queue.
+ */
+ void RemoveEvent(const GdkEventKey* aEvent) {
+ size_t index = IndexOf(aEvent);
+ if (NS_WARN_IF(index == GdkEventKeyQueue::NoIndex())) {
+ return;
+ }
+ mEvents.RemoveElementAt(index);
+ }
+
+ /**
+ * Return corresponding GDK_KEY_PRESS event for aEvent. aEvent must be a
+ * GDK_KEY_RELEASE event.
+ */
+ const GdkEventKey* GetCorrespondingKeyPressEvent(
+ const GdkEventKey* aEvent) const {
+ MOZ_ASSERT(aEvent->type == GDK_KEY_RELEASE);
+ for (const GUniquePtr<GdkEventKey>& pendingKeyEvent : mEvents) {
+ if (pendingKeyEvent->type == GDK_KEY_PRESS &&
+ aEvent->hardware_keycode == pendingKeyEvent->hardware_keycode) {
+ return pendingKeyEvent.get();
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * FirstEvent() returns oldest event in the queue.
+ */
+ GdkEventKey* GetFirstEvent() const {
+ if (mEvents.IsEmpty()) {
+ return nullptr;
+ }
+ return mEvents[0].get();
+ }
+
+ bool IsEmpty() const { return mEvents.IsEmpty(); }
+
+ static size_t NoIndex() { return nsTArray<GdkEventKey*>::NoIndex; }
+ size_t Length() const { return mEvents.Length(); }
+ size_t IndexOf(const GdkEventKey* aEvent) const {
+ static_assert(!(GDK_MODIFIER_MASK & (1 << 24)),
+ "We assumes 25th bit is used by some IM, but used by GDK");
+ static_assert(!(GDK_MODIFIER_MASK & (1 << 25)),
+ "We assumes 26th bit is used by some IM, but used by GDK");
+ for (size_t i = 0; i < mEvents.Length(); i++) {
+ GdkEventKey* event = mEvents[i].get();
+ // It must be enough to compare only type, time, keyval and
+ // part of state. Note that we cannot compaire two events
+ // simply since IME may have changed unused bits of state.
+ if (event->time == aEvent->time) {
+ if (NS_WARN_IF(event->type != aEvent->type) ||
+ NS_WARN_IF(event->keyval != aEvent->keyval) ||
+ NS_WARN_IF(event->state != (aEvent->state & GDK_MODIFIER_MASK))) {
+ continue;
+ }
+ }
+ return i;
+ }
+ return GdkEventKeyQueue::NoIndex();
+ }
+
+ private:
+ nsTArray<GUniquePtr<GdkEventKey>> mEvents;
+ };
+ // OnKeyEvent() append mPostingKeyEvents when it believes that a key event
+ // is posted to other IME process.
+ GdkEventKeyQueue mPostingKeyEvents;
+
+ static guint16 sWaitingSynthesizedKeyPressHardwareKeyCode;
+
+ struct Range {
+ uint32_t mOffset;
+ uint32_t mLength;
+
+ Range() : mOffset(UINT32_MAX), mLength(UINT32_MAX) {}
+
+ bool IsValid() const { return mOffset != UINT32_MAX; }
+ void Clear() {
+ mOffset = UINT32_MAX;
+ mLength = UINT32_MAX;
+ }
+ };
+
+ // current target offset and length of IME composition
+ Range mCompositionTargetRange;
+
+ // mCompositionState indicates current status of composition.
+ enum eCompositionState : uint8_t {
+ eCompositionState_NotComposing,
+ eCompositionState_CompositionStartDispatched,
+ eCompositionState_CompositionChangeEventDispatched
+ };
+ eCompositionState mCompositionState;
+
+ bool IsComposing() const {
+ return (mCompositionState != eCompositionState_NotComposing);
+ }
+
+ bool IsComposingOn(GtkIMContext* aContext) const {
+ return IsComposing() && mComposingContext == aContext;
+ }
+
+ bool IsComposingOnCurrentContext() const {
+ return IsComposingOn(GetCurrentContext());
+ }
+
+ bool EditorHasCompositionString() {
+ return (mCompositionState ==
+ eCompositionState_CompositionChangeEventDispatched);
+ }
+
+ /**
+ * Checks if aContext is valid context for handling composition.
+ *
+ * @param aContext An IM context which is specified by native
+ * composition events.
+ * @return true if the context is valid context for
+ * handling composition. Otherwise, false.
+ */
+ bool IsValidContext(GtkIMContext* aContext) const;
+
+ const char* GetCompositionStateName() {
+ switch (mCompositionState) {
+ case eCompositionState_NotComposing:
+ return "NotComposing";
+ case eCompositionState_CompositionStartDispatched:
+ return "CompositionStartDispatched";
+ case eCompositionState_CompositionChangeEventDispatched:
+ return "CompositionChangeEventDispatched";
+ default:
+ return "InvaildState";
+ }
+ }
+
+ // mIMContextID indicates the ID of mContext. This is actually indicates
+ // IM which user selected.
+ IMContextID mIMContextID;
+
+ // If mContentSelection is Nothing, it means that
+ // EnsureToCacheContentSelection failed to get selection or just not caching
+ // the selection.
+ Maybe<ContentSelection> mContentSelection;
+
+ /**
+ * Return true if mContentSelection is set to some. Otherwise, false.
+ */
+ bool EnsureToCacheContentSelection(nsAString* aSelectedString = nullptr);
+
+ enum class IMEFocusState : uint8_t {
+ // IME has focus
+ Focused,
+ // IME was blurred
+ Blurred,
+ // IME was blurred without a focus change
+ BlurredWithoutFocusChange,
+ };
+ friend std::ostream& operator<<(std::ostream& aStream, IMEFocusState aState) {
+ switch (aState) {
+ case IMEFocusState::Focused:
+ return aStream << "IMEFocusState::Focused";
+ case IMEFocusState::Blurred:
+ return aStream << "IMEFocusState::Blurred";
+ case IMEFocusState::BlurredWithoutFocusChange:
+ return aStream << "IMEFocusState::BlurredWithoutFocusChange";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid value");
+ return aStream << "<illegal value>";
+ }
+ }
+ IMEFocusState mIMEFocusState = IMEFocusState::Blurred;
+
+ // mFallbackToKeyEvent is set to false when this class starts to handle
+ // a native key event (at that time, mProcessingKeyEvent is set to the
+ // native event). If active IME just commits composition with a character
+ // which is produced by the key with current keyboard layout, this is set
+ // to true.
+ bool mFallbackToKeyEvent;
+ // mKeyboardEventWasDispatched is used by OnKeyEvent() and
+ // MaybeDispatchKeyEventAsProcessedByIME().
+ // MaybeDispatchKeyEventAsProcessedByIME() dispatches an eKeyDown or
+ // eKeyUp event event if the composition is caused by a native
+ // key press event. If this is true, a keyboard event has been dispatched
+ // for the native event. If so, MaybeDispatchKeyEventAsProcessedByIME()
+ // won't dispatch keyboard event anymore.
+ bool mKeyboardEventWasDispatched;
+ // Whether the keyboard event which as dispatched at setting
+ // mKeyboardEventWasDispatched to true was consumed or not.
+ bool mKeyboardEventWasConsumed;
+ // mIsDeletingSurrounding is true while OnDeleteSurroundingNative() is
+ // trying to delete the surrounding text.
+ bool mIsDeletingSurrounding;
+ // mLayoutChanged is true after OnLayoutChange() is called. This is reset
+ // when eCompositionChange is being dispatched.
+ bool mLayoutChanged;
+ // mSetCursorPositionOnKeyEvent true when caret rect or position is updated
+ // with no composition. If true, we update candidate window position
+ // before key down
+ bool mSetCursorPositionOnKeyEvent;
+ // mPendingResettingIMContext becomes true if selection change notification
+ // is received during composition but the selection change occurred before
+ // starting the composition. In such case, we cannot notify IME of
+ // selection change during composition because we don't want to commit
+ // the composition in such case. However, we should notify IME of the
+ // selection change after the composition is committed.
+ bool mPendingResettingIMContext;
+ // mRetrieveSurroundingSignalReceived is true after "retrieve_surrounding"
+ // signal is received until selection is changed in Gecko.
+ bool mRetrieveSurroundingSignalReceived;
+ // mMaybeInDeadKeySequence is set to true when we detect a dead key press
+ // and set to false when we're sure dead key sequence has been finished.
+ // Note that we cannot detect which key event causes ending a dead key
+ // sequence. For example, when you press dead key grave with ibus Spanish
+ // keyboard layout, it just consumes the key event when we call
+ // gtk_im_context_filter_keypress(). Then, pressing "Escape" key cancels
+ // the dead key sequence but we don't receive any signal and it's consumed
+ // by gtk_im_context_filter_keypress() normally. On the other hand, when
+ // pressing "Shift" key causes exactly same behavior but dead key sequence
+ // isn't finished yet.
+ bool mMaybeInDeadKeySequence;
+ // mIsIMInAsyncKeyHandlingMode is set to true if we know that IM handles
+ // key events asynchronously. I.e., filtered key event may come again
+ // later.
+ bool mIsIMInAsyncKeyHandlingMode;
+ // mIsKeySnooped is set to true if IM uses key snooper to listen key events.
+ // In such case, we won't receive key events if IME consumes the event.
+ bool mIsKeySnooped;
+ // mSetInputPurposeAndInputHints is set if `SetInputContext` wants `Focus`
+ // to set input-purpose and input-hints.
+ bool mSetInputPurposeAndInputHints;
+
+ // sLastFocusedContext is a pointer to the last focused instance of this
+ // class. When a instance is destroyed and sLastFocusedContext refers it,
+ // this is cleared. So, this refers valid pointer always.
+ static IMContextWrapper* sLastFocusedContext;
+
+ // sUseSimpleContext indeicates if password editors and editors with
+ // |ime-mode: disabled;| should use GtkIMContextSimple.
+ // If true, they use GtkIMContextSimple. Otherwise, not.
+ static bool sUseSimpleContext;
+
+ // Callback methods for native IME events. These methods should call
+ // the related instance methods simply.
+ static gboolean OnRetrieveSurroundingCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule);
+ static gboolean OnDeleteSurroundingCallback(GtkIMContext* aContext,
+ gint aOffset, gint aNChars,
+ IMContextWrapper* aModule);
+ static void OnCommitCompositionCallback(GtkIMContext* aContext,
+ const gchar* aString,
+ IMContextWrapper* aModule);
+ static void OnChangeCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule);
+ static void OnStartCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule);
+ static void OnEndCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule);
+
+ // The instance methods for the native IME events.
+ gboolean OnRetrieveSurroundingNative(GtkIMContext* aContext);
+ gboolean OnDeleteSurroundingNative(GtkIMContext* aContext, gint aOffset,
+ gint aNChars);
+ void OnCommitCompositionNative(GtkIMContext* aContext, const gchar* aString);
+ void OnChangeCompositionNative(GtkIMContext* aContext);
+ void OnStartCompositionNative(GtkIMContext* aContext);
+ void OnEndCompositionNative(GtkIMContext* aContext);
+
+ /**
+ * GetCurrentContext() returns current IM context which is chosen with the
+ * enabled state.
+ * WARNING:
+ * When this class receives some signals for a composition after focus
+ * is moved in Gecko, the result of this may be different from given
+ * context by the signals.
+ */
+ GtkIMContext* GetCurrentContext() const;
+
+ /**
+ * GetActiveContext() returns a composing context or current context.
+ */
+ GtkIMContext* GetActiveContext() const {
+ return mComposingContext ? mComposingContext : GetCurrentContext();
+ }
+
+ // If the owner window and IM context have been destroyed, returns TRUE.
+ bool IsDestroyed() { return !mOwnerWindow; }
+
+ void NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState);
+
+ // Initializes the instance.
+ void Init();
+
+ /**
+ * Reset the active context, i.e., if there is mComposingContext, reset it.
+ * Otherwise, reset current context. Note that all native composition
+ * events during calling this will be ignored.
+ */
+ void ResetIME();
+
+ // Gets the current composition string by the native APIs.
+ void GetCompositionString(GtkIMContext* aContext,
+ nsAString& aCompositionString);
+
+ /**
+ * Generates our text range array from current composition string.
+ *
+ * @param aContext A GtkIMContext which is being handled.
+ * @param aCompositionString The data to be dispatched with
+ * compositionchange event.
+ */
+ already_AddRefed<TextRangeArray> CreateTextRangeArray(
+ GtkIMContext* aContext, const nsAString& aCompositionString);
+
+ /**
+ * SetTextRange() initializes aTextRange with aPangoAttrIter.
+ *
+ * @param aPangoAttrIter An iter which represents a clause of the
+ * composition string.
+ * @param aUTF8CompositionString The whole composition string (UTF-8).
+ * @param aUTF16CaretOffset The caret offset in the composition
+ * string encoded as UTF-16.
+ * @param aTextRange The result.
+ * @return true if this initializes aTextRange.
+ * Otherwise, false.
+ */
+ bool SetTextRange(PangoAttrIterator* aPangoAttrIter,
+ const gchar* aUTF8CompositionString,
+ uint32_t aUTF16CaretOffset, TextRange& aTextRange) const;
+
+ /**
+ * ToNscolor() converts the PangoColor in aPangoAttrColor to nscolor.
+ */
+ static nscolor ToNscolor(PangoAttrColor* aPangoAttrColor);
+
+ /**
+ * Move the candidate window with "fake" cursor position.
+ *
+ * @param aContext A GtkIMContext which is being handled.
+ */
+ void SetCursorPosition(GtkIMContext* aContext);
+
+ // Queries the current selection offset of the window.
+ uint32_t GetSelectionOffset(nsWindow* aWindow);
+
+ // Get current paragraph text content and cursor position
+ nsresult GetCurrentParagraph(nsAString& aText, uint32_t& aCursorPos);
+
+ /**
+ * Delete text portion
+ *
+ * @param aContext A GtkIMContext which is being handled.
+ * @param aOffset Start offset of the range to delete.
+ * @param aNChars Count of characters to delete. It depends
+ * on |g_utf8_strlen()| what is one character.
+ */
+ nsresult DeleteText(GtkIMContext* aContext, int32_t aOffset,
+ uint32_t aNChars);
+
+ // Called before destroying the context to work around some platform bugs.
+ void PrepareToDestroyContext(GtkIMContext* aContext);
+
+ /**
+ * WARNING:
+ * Following methods dispatch gecko events. Then, the focused widget
+ * can be destroyed, and also it can be stolen focus. If they returns
+ * FALSE, callers cannot continue the composition.
+ * - MaybeDispatchKeyEventAsProcessedByIME
+ * - DispatchCompositionStart
+ * - DispatchCompositionChangeEvent
+ * - DispatchCompositionCommitEvent
+ */
+
+ /**
+ * Dispatch an eKeyDown or eKeyUp event whose mKeyCode value is
+ * NS_VK_PROCESSKEY and mKeyNameIndex is KEY_NAME_INDEX_Process if
+ * we're not in a dead key sequence, mProcessingKeyEvent is nullptr
+ * but mPostingKeyEvents is not empty or mProcessingKeyEvent is not
+ * nullptr and mKeyboardEventWasDispatched is still false. If this
+ * dispatches a keyboard event, this sets mKeyboardEventWasDispatched
+ * to true.
+ *
+ * @param aFollowingEvent The following event message.
+ * @return If the caller can continue to handle
+ * composition, returns true. Otherwise,
+ * false. For example, if focus is moved
+ * by dispatched keyboard event, returns
+ * false.
+ */
+ bool MaybeDispatchKeyEventAsProcessedByIME(EventMessage aFollowingEvent);
+
+ /**
+ * Dispatches a composition start event.
+ *
+ * @param aContext A GtkIMContext which is being handled.
+ * @return true if the focused widget is neither
+ * destroyed nor changed. Otherwise, false.
+ */
+ bool DispatchCompositionStart(GtkIMContext* aContext);
+
+ /**
+ * Dispatches a compositionchange event.
+ *
+ * @param aContext A GtkIMContext which is being handled.
+ * @param aCompositionString New composition string.
+ * @return true if the focused widget is neither
+ * destroyed nor changed. Otherwise, false.
+ */
+ bool DispatchCompositionChangeEvent(GtkIMContext* aContext,
+ const nsAString& aCompositionString);
+
+ /**
+ * Dispatches a compositioncommit event or compositioncommitasis event.
+ *
+ * @param aContext A GtkIMContext which is being handled.
+ * @param aCommitString If this is nullptr, the composition will
+ * be committed with last dispatched data.
+ * Otherwise, the composition will be
+ * committed with this value.
+ * @return true if the focused widget is neither
+ * destroyed nor changed. Otherwise, false.
+ */
+ bool DispatchCompositionCommitEvent(GtkIMContext* aContext,
+ const nsAString* aCommitString = nullptr);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef IMContextWrapper_h_
diff --git a/widget/gtk/InProcessGtkCompositorWidget.cpp b/widget/gtk/InProcessGtkCompositorWidget.cpp
new file mode 100644
index 0000000000..bffd6f5a7f
--- /dev/null
+++ b/widget/gtk/InProcessGtkCompositorWidget.cpp
@@ -0,0 +1,44 @@
+/* -*- 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 "HeadlessCompositorWidget.h"
+#include "HeadlessWidget.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+
+#include "InProcessGtkCompositorWidget.h"
+#include "VsyncDispatcher.h"
+#include "nsWindow.h"
+
+namespace mozilla::widget {
+
+/* static */
+RefPtr<CompositorWidget> CompositorWidget::CreateLocal(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsIWidget* aWidget) {
+ if (aInitData.type() ==
+ CompositorWidgetInitData::THeadlessCompositorWidgetInitData) {
+ return new HeadlessCompositorWidget(
+ aInitData.get_HeadlessCompositorWidgetInitData(), aOptions,
+ static_cast<HeadlessWidget*>(aWidget));
+ } else {
+ return new InProcessGtkCompositorWidget(
+ aInitData.get_GtkCompositorWidgetInitData(), aOptions,
+ static_cast<nsWindow*>(aWidget));
+ }
+}
+
+InProcessGtkCompositorWidget::InProcessGtkCompositorWidget(
+ const GtkCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsWindow* aWindow)
+ : GtkCompositorWidget(aInitData, aOptions, aWindow) {}
+
+void InProcessGtkCompositorWidget::ObserveVsync(VsyncObserver* aObserver) {
+ if (RefPtr<CompositorVsyncDispatcher> cvd =
+ mWidget->GetCompositorVsyncDispatcher()) {
+ cvd->SetCompositorVsyncObserver(aObserver);
+ }
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/InProcessGtkCompositorWidget.h b/widget/gtk/InProcessGtkCompositorWidget.h
new file mode 100644
index 0000000000..bd7e68d6fb
--- /dev/null
+++ b/widget/gtk/InProcessGtkCompositorWidget.h
@@ -0,0 +1,30 @@
+/* -*- 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/. */
+
+#ifndef widget_gtk_InProcessGtkCompositorWidget_h
+#define widget_gtk_InProcessGtkCompositorWidget_h
+
+#include "GtkCompositorWidget.h"
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+class InProcessGtkCompositorWidget final : public GtkCompositorWidget {
+ public:
+ InProcessGtkCompositorWidget(const GtkCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions,
+ nsWindow* aWindow);
+
+ // CompositorWidgetDelegate
+
+ void ObserveVsync(VsyncObserver* aObserver) override;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_gtk_InProcessGtkCompositorWidget_h
diff --git a/widget/gtk/MPRISInterfaceDescription.h b/widget/gtk/MPRISInterfaceDescription.h
new file mode 100644
index 0000000000..28e719e651
--- /dev/null
+++ b/widget/gtk/MPRISInterfaceDescription.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_
+#define WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_
+
+#include <gio/gio.h>
+
+extern const gchar introspection_xml[] =
+ // adopted from https://github.com/freedesktop/mpris-spec/blob/master/spec/org.mpris.MediaPlayer2.xml
+ // everything starting with tp can be removed, as it is used for HTML Spec Documentation Generation
+ "<node>"
+ "<interface name=\"org.mpris.MediaPlayer2\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "<method name=\"Raise\"/>"
+ "<method name=\"Quit\"/>"
+ "<property name=\"CanQuit\" type=\"b\" access=\"read\"/>"
+ "<property name=\"CanRaise\" type=\"b\" access=\"read\"/>"
+ "<property name=\"HasTrackList\" type=\"b\" access=\"read\"/>"
+ "<property name=\"Identity\" type=\"s\" access=\"read\"/>"
+ "<property name=\"DesktopEntry\" type=\"s\" access=\"read\"/>"
+ "<property name=\"SupportedUriSchemes\" type=\"as\" access=\"read\"/>"
+ "<property name=\"SupportedMimeTypes\" type=\"as\" access=\"read\"/>"
+ "</interface>"
+ // Note that every property emits a changed signal (which is default) apart from Position.
+ "<interface name=\"org.mpris.MediaPlayer2.Player\">"
+ "<method name=\"Next\"/>"
+ "<method name=\"Previous\"/>"
+ "<method name=\"Pause\"/>"
+ "<method name=\"PlayPause\"/>"
+ "<method name=\"Stop\"/>"
+ "<method name=\"Play\"/>"
+ "<method name=\"Seek\">"
+ "<arg direction=\"in\" type=\"x\" name=\"Offset\"/>"
+ "</method>"
+ "<method name=\"SetPosition\">"
+ "<arg direction=\"in\" type=\"o\" name=\"TrackId\"/>"
+ "<arg direction=\"in\" type=\"x\" name=\"Position\"/>"
+ "</method>"
+ "<method name=\"OpenUri\">"
+ "<arg direction=\"in\" type=\"s\" name=\"Uri\"/>"
+ "</method>"
+ "<property name=\"PlaybackStatus\" type=\"s\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"Rate\" type=\"d\" access=\"readwrite\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"Metadata\" type=\"a{sv}\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"Volume\" type=\"d\" access=\"readwrite\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"Position\" type=\"x\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"false\"/>"
+ "</property>"
+ "<property name=\"MinimumRate\" type=\"d\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"MaximumRate\" type=\"d\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"CanGoNext\" type=\"b\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"CanGoPrevious\" type=\"b\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"CanPlay\" type=\"b\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"CanPause\" type=\"b\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"CanSeek\" type=\"b\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>"
+ "</property>"
+ "<property name=\"CanControl\" type=\"b\" access=\"read\">"
+ "<annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"false\"/>"
+ "</property>"
+ "<signal name=\"Seeked\">"
+ "<arg name=\"Position\" type=\"x\"/>"
+ "</signal>"
+ "</interface>"
+ "</node>";
+
+#endif // WIDGET_GTK_MPRIS_INTERFACE_DESCRIPTION_H_
diff --git a/widget/gtk/MPRISServiceHandler.cpp b/widget/gtk/MPRISServiceHandler.cpp
new file mode 100644
index 0000000000..ae1ef8654d
--- /dev/null
+++ b/widget/gtk/MPRISServiceHandler.cpp
@@ -0,0 +1,909 @@
+/* -*- Mode: C++; tab-width: 4; 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 "MPRISServiceHandler.h"
+
+#include <stdint.h>
+#include <inttypes.h>
+#include <unordered_map>
+
+#include "MPRISInterfaceDescription.h"
+#include "mozilla/dom/MediaControlUtils.h"
+#include "mozilla/GRefPtr.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Sprintf.h"
+#include "nsXULAppAPI.h"
+#include "nsIXULAppInfo.h"
+#include "nsIOutputStream.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "WidgetUtilsGtk.h"
+#include "AsyncDBus.h"
+#include "prio.h"
+
+#define LOGMPRIS(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla {
+namespace widget {
+
+// A global counter tracking the total images saved in the system and it will be
+// used to form a unique image file name.
+static uint32_t gImageNumber = 0;
+
+static inline Maybe<dom::MediaControlKey> GetMediaControlKey(
+ const gchar* aMethodName) {
+ const std::unordered_map<std::string, dom::MediaControlKey> map = {
+ {"Raise", dom::MediaControlKey::Focus},
+ {"Next", dom::MediaControlKey::Nexttrack},
+ {"Previous", dom::MediaControlKey::Previoustrack},
+ {"Pause", dom::MediaControlKey::Pause},
+ {"PlayPause", dom::MediaControlKey::Playpause},
+ {"Stop", dom::MediaControlKey::Stop},
+ {"Play", dom::MediaControlKey::Play}};
+
+ auto it = map.find(aMethodName);
+ return it == map.end() ? Nothing() : Some(it->second);
+}
+
+static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender,
+ const gchar* aObjectPath,
+ const gchar* aInterfaceName,
+ const gchar* aMethodName, GVariant* aParameters,
+ GDBusMethodInvocation* aInvocation,
+ gpointer aUserData) {
+ MOZ_ASSERT(aUserData);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ Maybe<dom::MediaControlKey> key = GetMediaControlKey(aMethodName);
+ if (key.isNothing()) {
+ g_dbus_method_invocation_return_error(
+ aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
+ "Method %s.%s.%s not supported", aObjectPath, aInterfaceName,
+ aMethodName);
+ return;
+ }
+
+ MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
+ if (handler->PressKey(key.value())) {
+ g_dbus_method_invocation_return_value(aInvocation, nullptr);
+ } else {
+ g_dbus_method_invocation_return_error(
+ aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "%s.%s.%s is not available now", aObjectPath, aInterfaceName,
+ aMethodName);
+ }
+}
+
+enum class Property : uint8_t {
+ eIdentity,
+ eDesktopEntry,
+ eHasTrackList,
+ eCanRaise,
+ eCanQuit,
+ eSupportedUriSchemes,
+ eSupportedMimeTypes,
+ eCanGoNext,
+ eCanGoPrevious,
+ eCanPlay,
+ eCanPause,
+ eCanSeek,
+ eCanControl,
+ eGetPlaybackStatus,
+ eGetMetadata,
+};
+
+static inline Maybe<dom::MediaControlKey> GetPairedKey(Property aProperty) {
+ switch (aProperty) {
+ case Property::eCanRaise:
+ return Some(dom::MediaControlKey::Focus);
+ case Property::eCanGoNext:
+ return Some(dom::MediaControlKey::Nexttrack);
+ case Property::eCanGoPrevious:
+ return Some(dom::MediaControlKey::Previoustrack);
+ case Property::eCanPlay:
+ return Some(dom::MediaControlKey::Play);
+ case Property::eCanPause:
+ return Some(dom::MediaControlKey::Pause);
+ default:
+ return Nothing();
+ }
+}
+
+static inline Maybe<Property> GetProperty(const gchar* aPropertyName) {
+ const std::unordered_map<std::string, Property> map = {
+ // org.mpris.MediaPlayer2 properties
+ {"Identity", Property::eIdentity},
+ {"DesktopEntry", Property::eDesktopEntry},
+ {"HasTrackList", Property::eHasTrackList},
+ {"CanRaise", Property::eCanRaise},
+ {"CanQuit", Property::eCanQuit},
+ {"SupportedUriSchemes", Property::eSupportedUriSchemes},
+ {"SupportedMimeTypes", Property::eSupportedMimeTypes},
+ // org.mpris.MediaPlayer2.Player properties
+ {"CanGoNext", Property::eCanGoNext},
+ {"CanGoPrevious", Property::eCanGoPrevious},
+ {"CanPlay", Property::eCanPlay},
+ {"CanPause", Property::eCanPause},
+ {"CanSeek", Property::eCanSeek},
+ {"CanControl", Property::eCanControl},
+ {"PlaybackStatus", Property::eGetPlaybackStatus},
+ {"Metadata", Property::eGetMetadata}};
+
+ auto it = map.find(aPropertyName);
+ return (it == map.end() ? Nothing() : Some(it->second));
+}
+
+static GVariant* HandleGetProperty(GDBusConnection* aConnection,
+ const gchar* aSender,
+ const gchar* aObjectPath,
+ const gchar* aInterfaceName,
+ const gchar* aPropertyName, GError** aError,
+ gpointer aUserData) {
+ MOZ_ASSERT(aUserData);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ Maybe<Property> property = GetProperty(aPropertyName);
+ if (property.isNothing()) {
+ g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
+ "%s.%s %s is not supported", aObjectPath, aInterfaceName,
+ aPropertyName);
+ return nullptr;
+ }
+
+ MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
+ switch (property.value()) {
+ case Property::eSupportedUriSchemes:
+ case Property::eSupportedMimeTypes:
+ // No plan to implement OpenUri for now
+ return g_variant_new_strv(nullptr, 0);
+ case Property::eGetPlaybackStatus:
+ return handler->GetPlaybackStatus();
+ case Property::eGetMetadata:
+ return handler->GetMetadataAsGVariant();
+ case Property::eIdentity:
+ return g_variant_new_string(handler->Identity());
+ case Property::eDesktopEntry:
+ return g_variant_new_string(handler->DesktopEntry());
+ case Property::eHasTrackList:
+ case Property::eCanQuit:
+ case Property::eCanSeek:
+ return g_variant_new_boolean(false);
+ // Play/Pause would be blocked if CanControl is false
+ case Property::eCanControl:
+ return g_variant_new_boolean(true);
+ case Property::eCanRaise:
+ case Property::eCanGoNext:
+ case Property::eCanGoPrevious:
+ case Property::eCanPlay:
+ case Property::eCanPause:
+ Maybe<dom::MediaControlKey> key = GetPairedKey(property.value());
+ MOZ_ASSERT(key.isSome());
+ return g_variant_new_boolean(handler->IsMediaKeySupported(key.value()));
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Switch statement is incomplete");
+ return nullptr;
+}
+
+static gboolean HandleSetProperty(GDBusConnection* aConnection,
+ const gchar* aSender,
+ const gchar* aObjectPath,
+ const gchar* aInterfaceName,
+ const gchar* aPropertyName, GVariant* aValue,
+ GError** aError, gpointer aUserData) {
+ MOZ_ASSERT(aUserData);
+ MOZ_ASSERT(NS_IsMainThread());
+ g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "%s:%s setting is not supported", aInterfaceName, aPropertyName);
+ return false;
+}
+
+static const GDBusInterfaceVTable gInterfaceVTable = {
+ HandleMethodCall, HandleGetProperty, HandleSetProperty};
+
+void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection,
+ const gchar* aName,
+ gpointer aUserData) {
+ MOZ_ASSERT(aUserData);
+ static_cast<MPRISServiceHandler*>(aUserData)->OnNameAcquired(aConnection,
+ aName);
+}
+
+void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection,
+ const gchar* aName,
+ gpointer aUserData) {
+ MOZ_ASSERT(aUserData);
+ static_cast<MPRISServiceHandler*>(aUserData)->OnNameLost(aConnection, aName);
+}
+
+void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection,
+ const gchar* aName,
+ gpointer aUserData) {
+ MOZ_ASSERT(aUserData);
+ static_cast<MPRISServiceHandler*>(aUserData)->OnBusAcquired(aConnection,
+ aName);
+}
+
+void MPRISServiceHandler::OnNameAcquired(GDBusConnection* aConnection,
+ const gchar* aName) {
+ LOGMPRIS("OnNameAcquired: %s", aName);
+ mConnection = aConnection;
+}
+
+void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection,
+ const gchar* aName) {
+ LOGMPRIS("OnNameLost: %s", aName);
+ mConnection = nullptr;
+ if (!mRootRegistrationId) {
+ return;
+ }
+
+ if (!aConnection) {
+ return;
+ }
+
+ if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) {
+ mRootRegistrationId = 0;
+ } else {
+ // Note: Most code examples in the internet probably dont't even check the
+ // result here, but
+ // according to the spec it _can_ return false.
+ LOGMPRIS("Unable to unregister root object from within onNameLost!");
+ }
+
+ if (!mPlayerRegistrationId) {
+ return;
+ }
+
+ if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) {
+ mPlayerRegistrationId = 0;
+ } else {
+ // Note: Most code examples in the internet probably dont't even check the
+ // result here, but
+ // according to the spec it _can_ return false.
+ LOGMPRIS("Unable to unregister object from within onNameLost!");
+ }
+}
+
+void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection,
+ const gchar* aName) {
+ GUniquePtr<GError> error;
+ LOGMPRIS("OnBusAcquired: %s", aName);
+
+ mRootRegistrationId = g_dbus_connection_register_object(
+ aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[0],
+ &gInterfaceVTable, this, /* user_data */
+ nullptr, /* user_data_free_func */
+ getter_Transfers(error)); /* GError** */
+
+ if (mRootRegistrationId == 0) {
+ LOGMPRIS("Failed at root registration: %s",
+ error ? error->message : "Unknown Error");
+ return;
+ }
+
+ mPlayerRegistrationId = g_dbus_connection_register_object(
+ aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[1],
+ &gInterfaceVTable, this, /* user_data */
+ nullptr, /* user_data_free_func */
+ getter_Transfers(error)); /* GError** */
+
+ if (mPlayerRegistrationId == 0) {
+ LOGMPRIS("Failed at object registration: %s",
+ error ? error->message : "Unknown Error");
+ }
+}
+
+void MPRISServiceHandler::SetServiceName(const char* aName) {
+ nsCString dbusName(aName);
+ dbusName.ReplaceChar(':', '_');
+ dbusName.ReplaceChar('.', '_');
+ mServiceName =
+ nsCString(DBUS_MPRIS_SERVICE_NAME) + nsCString(".instance") + dbusName;
+}
+
+const char* MPRISServiceHandler::GetServiceName() { return mServiceName.get(); }
+
+/* static */
+void g_bus_get_callback(GObject* aSourceObject, GAsyncResult* aRes,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+
+ GDBusConnection* conn = g_bus_get_finish(aRes, getter_Transfers(error));
+ if (!conn) {
+ if (!IsCancelledGError(error.get())) {
+ NS_WARNING(nsPrintfCString("Failure at g_bus_get_finish: %s",
+ error ? error->message : "Unknown Error")
+ .get());
+ }
+ return;
+ }
+
+ MPRISServiceHandler* handler = static_cast<MPRISServiceHandler*>(aUserData);
+ if (!handler) {
+ NS_WARNING(
+ nsPrintfCString("Failure to get a MPRISServiceHandler*: %p", handler)
+ .get());
+ return;
+ }
+
+ handler->OwnName(conn);
+}
+
+void MPRISServiceHandler::OwnName(GDBusConnection* aConnection) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ SetServiceName(g_dbus_connection_get_unique_name(aConnection));
+
+ GUniquePtr<GError> error;
+
+ InitIdentity();
+ mOwnerId = g_bus_own_name_on_connection(
+ aConnection, GetServiceName(),
+ // Enter a waiting queue until this service name is free
+ // (likely another FF instance is running/has been crashed)
+ G_BUS_NAME_OWNER_FLAGS_NONE, OnNameAcquiredStatic, OnNameLostStatic, this,
+ nullptr);
+
+ /* parse introspection data */
+ mIntrospectionData = dont_AddRef(
+ g_dbus_node_info_new_for_xml(introspection_xml, getter_Transfers(error)));
+
+ if (!mIntrospectionData) {
+ LOGMPRIS("Failed at parsing XML Interface definition: %s",
+ error ? error->message : "Unknown Error");
+ return;
+ }
+
+ OnBusAcquired(aConnection, GetServiceName());
+}
+
+bool MPRISServiceHandler::Open() {
+ MOZ_ASSERT(!mInitialized);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mDBusGetCancellable = dont_AddRef(g_cancellable_new());
+ g_bus_get(G_BUS_TYPE_SESSION, mDBusGetCancellable, g_bus_get_callback, this);
+
+ mInitialized = true;
+ return true;
+}
+
+MPRISServiceHandler::MPRISServiceHandler() = default;
+MPRISServiceHandler::~MPRISServiceHandler() {
+ MOZ_ASSERT(!mInitialized, "Close hasn't been called!");
+}
+
+void MPRISServiceHandler::Close() {
+ // Reset playback state and metadata before disconnect from dbus.
+ SetPlaybackState(dom::MediaSessionPlaybackState::None);
+ ClearMetadata();
+
+ OnNameLost(mConnection, GetServiceName());
+
+ if (mDBusGetCancellable) {
+ g_cancellable_cancel(mDBusGetCancellable);
+ mDBusGetCancellable = nullptr;
+ }
+
+ if (mOwnerId != 0) {
+ g_bus_unown_name(mOwnerId);
+ }
+
+ mIntrospectionData = nullptr;
+
+ mInitialized = false;
+ MediaControlKeySource::Close();
+}
+
+bool MPRISServiceHandler::IsOpened() const { return mInitialized; }
+
+void MPRISServiceHandler::InitIdentity() {
+ nsresult rv;
+ nsCOMPtr<nsIXULAppInfo> appInfo =
+ do_GetService("@mozilla.org/xre/app-info;1", &rv);
+
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = appInfo->GetVendor(mIdentity);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = appInfo->GetName(mDesktopEntry);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ mIdentity.Append(' ');
+ mIdentity.Append(mDesktopEntry);
+
+ // Compute the desktop entry name like nsAppRunner does for g_set_prgname
+ ToLowerCase(mDesktopEntry);
+}
+
+const char* MPRISServiceHandler::Identity() const {
+ NS_WARNING_ASSERTION(mInitialized,
+ "MPRISServiceHandler should have been initialized.");
+ return mIdentity.get();
+}
+
+const char* MPRISServiceHandler::DesktopEntry() const {
+ NS_WARNING_ASSERTION(mInitialized,
+ "MPRISServiceHandler should have been initialized.");
+ return mDesktopEntry.get();
+}
+
+bool MPRISServiceHandler::PressKey(dom::MediaControlKey aKey) const {
+ MOZ_ASSERT(mInitialized);
+ if (!IsMediaKeySupported(aKey)) {
+ LOGMPRIS("%s is not supported", ToMediaControlKeyStr(aKey));
+ return false;
+ }
+ LOGMPRIS("Press %s", ToMediaControlKeyStr(aKey));
+ EmitEvent(aKey);
+ return true;
+}
+
+void MPRISServiceHandler::SetPlaybackState(
+ dom::MediaSessionPlaybackState aState) {
+ LOGMPRIS("SetPlaybackState");
+ if (mPlaybackState == aState) {
+ return;
+ }
+
+ MediaControlKeySource::SetPlaybackState(aState);
+
+ GVariant* state = GetPlaybackStatus();
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", state);
+
+ GVariant* parameters = g_variant_new(
+ "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr);
+
+ LOGMPRIS("Emitting MPRIS property changes for 'PlaybackStatus'");
+ Unused << EmitPropertiesChangedSignal(parameters);
+}
+
+GVariant* MPRISServiceHandler::GetPlaybackStatus() const {
+ switch (GetPlaybackState()) {
+ case dom::MediaSessionPlaybackState::Playing:
+ return g_variant_new_string("Playing");
+ case dom::MediaSessionPlaybackState::Paused:
+ return g_variant_new_string("Paused");
+ case dom::MediaSessionPlaybackState::None:
+ return g_variant_new_string("Stopped");
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid Playback State");
+ return nullptr;
+ }
+}
+
+void MPRISServiceHandler::SetMediaMetadata(
+ const dom::MediaMetadataBase& aMetadata) {
+ // Reset the index of the next available image to be fetched in the artwork,
+ // before checking the fetching process should be started or not. The image
+ // fetching process could be skipped if the image being fetching currently is
+ // in the artwork. If the current image fetching fails, the next availabe
+ // candidate should be the first image in the latest artwork
+ mNextImageIndex = 0;
+
+ // No need to fetch a MPRIS image if
+ // 1) MPRIS image is being fetched, and the one in fetching is in the artwork
+ // 2) MPRIS image is not being fetched, and the one in use is in the artwork
+ if (!mFetchingUrl.IsEmpty()) {
+ if (dom::IsImageIn(aMetadata.mArtwork, mFetchingUrl)) {
+ LOGMPRIS(
+ "No need to load MPRIS image. The one being processed is in the "
+ "artwork");
+ // Set MPRIS without the image first. The image will be loaded to MPRIS
+ // asynchronously once it's fetched and saved into a local file
+ SetMediaMetadataInternal(aMetadata);
+ return;
+ }
+ } else if (!mCurrentImageUrl.IsEmpty()) {
+ if (dom::IsImageIn(aMetadata.mArtwork, mCurrentImageUrl)) {
+ LOGMPRIS("No need to load MPRIS image. The one in use is in the artwork");
+ SetMediaMetadataInternal(aMetadata, false);
+ return;
+ }
+ }
+
+ // Set MPRIS without the image first then load the image to MPRIS
+ // asynchronously
+ SetMediaMetadataInternal(aMetadata);
+ LoadImageAtIndex(mNextImageIndex++);
+}
+
+bool MPRISServiceHandler::EmitMetadataChanged() const {
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}", "Metadata", GetMetadataAsGVariant());
+
+ GVariant* parameters = g_variant_new(
+ "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr);
+
+ LOGMPRIS("Emit MPRIS property changes for 'Metadata'");
+ return EmitPropertiesChangedSignal(parameters);
+}
+
+void MPRISServiceHandler::SetMediaMetadataInternal(
+ const dom::MediaMetadataBase& aMetadata, bool aClearArtUrl) {
+ mMPRISMetadata.UpdateFromMetadataBase(aMetadata);
+ if (aClearArtUrl) {
+ mMPRISMetadata.mArtUrl.Truncate();
+ }
+ EmitMetadataChanged();
+}
+
+void MPRISServiceHandler::ClearMetadata() {
+ mMPRISMetadata.Clear();
+ mImageFetchRequest.DisconnectIfExists();
+ RemoveAllLocalImages();
+ mCurrentImageUrl.Truncate();
+ mFetchingUrl.Truncate();
+ mNextImageIndex = 0;
+ mSupportedKeys = 0;
+ EmitMetadataChanged();
+}
+
+void MPRISServiceHandler::LoadImageAtIndex(const size_t aIndex) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aIndex >= mMPRISMetadata.mArtwork.Length()) {
+ LOGMPRIS("Stop loading image to MPRIS. No available image");
+ mImageFetchRequest.DisconnectIfExists();
+ return;
+ }
+
+ const dom::MediaImage& image = mMPRISMetadata.mArtwork[aIndex];
+
+ if (!dom::IsValidImageUrl(image.mSrc)) {
+ LOGMPRIS("Skip the image with invalid URL. Try next image");
+ LoadImageAtIndex(mNextImageIndex++);
+ return;
+ }
+
+ mImageFetchRequest.DisconnectIfExists();
+ mFetchingUrl = image.mSrc;
+
+ mImageFetcher = MakeUnique<dom::FetchImageHelper>(image);
+ RefPtr<MPRISServiceHandler> self = this;
+ mImageFetcher->FetchImage()
+ ->Then(
+ AbstractThread::MainThread(), __func__,
+ [this, self](const nsCOMPtr<imgIContainer>& aImage) {
+ LOGMPRIS("The image is fetched successfully");
+ mImageFetchRequest.Complete();
+
+ uint32_t size = 0;
+ char* data = nullptr;
+ // Only used to hold the image data
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = dom::GetEncodedImageBuffer(
+ aImage, mMimeType, getter_AddRefs(inputStream), &size, &data);
+ if (NS_FAILED(rv) || !inputStream || size == 0 || !data) {
+ LOGMPRIS("Failed to get the image buffer info. Try next image");
+ LoadImageAtIndex(mNextImageIndex++);
+ return;
+ }
+
+ if (SetImageToDisplay(data, size)) {
+ mCurrentImageUrl = mFetchingUrl;
+ LOGMPRIS("The MPRIS image is updated to the image from: %s",
+ NS_ConvertUTF16toUTF8(mCurrentImageUrl).get());
+ } else {
+ LOGMPRIS("Failed to set image to MPRIS");
+ mCurrentImageUrl.Truncate();
+ }
+
+ mFetchingUrl.Truncate();
+ },
+ [this, self](bool) {
+ LOGMPRIS("Failed to fetch image. Try next image");
+ mImageFetchRequest.Complete();
+ mFetchingUrl.Truncate();
+ LoadImageAtIndex(mNextImageIndex++);
+ })
+ ->Track(mImageFetchRequest);
+}
+
+bool MPRISServiceHandler::SetImageToDisplay(const char* aImageData,
+ uint32_t aDataSize) {
+ if (!RenewLocalImageFile(aImageData, aDataSize)) {
+ return false;
+ }
+ MOZ_ASSERT(mLocalImageFile);
+
+ mMPRISMetadata.mArtUrl = nsCString("file://");
+ mMPRISMetadata.mArtUrl.Append(mLocalImageFile->NativePath());
+
+ LOGMPRIS("The image file is created at %s", mMPRISMetadata.mArtUrl.get());
+ return EmitMetadataChanged();
+}
+
+bool MPRISServiceHandler::RenewLocalImageFile(const char* aImageData,
+ uint32_t aDataSize) {
+ MOZ_ASSERT(aImageData);
+ MOZ_ASSERT(aDataSize != 0);
+
+ if (!InitLocalImageFile()) {
+ LOGMPRIS("Failed to create a new image");
+ return false;
+ }
+
+ MOZ_ASSERT(mLocalImageFile);
+ nsCOMPtr<nsIOutputStream> out;
+ NS_NewLocalFileOutputStream(getter_AddRefs(out), mLocalImageFile,
+ PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+ uint32_t written;
+ nsresult rv = out->Write(aImageData, aDataSize, &written);
+ if (NS_FAILED(rv) || written != aDataSize) {
+ LOGMPRIS("Failed to write an image file");
+ RemoveAllLocalImages();
+ return false;
+ }
+
+ return true;
+}
+
+static const char* GetImageFileExtension(const char* aMimeType) {
+ MOZ_ASSERT(strcmp(aMimeType, IMAGE_PNG) == 0);
+ return "png";
+}
+
+bool MPRISServiceHandler::InitLocalImageFile() {
+ RemoveAllLocalImages();
+
+ if (!InitLocalImageFolder()) {
+ return false;
+ }
+
+ MOZ_ASSERT(mLocalImageFolder);
+ MOZ_ASSERT(!mLocalImageFile);
+ nsresult rv = mLocalImageFolder->Clone(getter_AddRefs(mLocalImageFile));
+ if (NS_FAILED(rv)) {
+ LOGMPRIS("Failed to get the image folder");
+ return false;
+ }
+
+ auto cleanup =
+ MakeScopeExit([this, self = RefPtr<MPRISServiceHandler>(this)] {
+ mLocalImageFile = nullptr;
+ });
+
+ // Create an unique file name to work around the file caching mechanism in the
+ // Ubuntu. Once the image X specified by the filename Y is used in Ubuntu's
+ // MPRIS, this pair will be cached. As long as the filename is same, even the
+ // file content specified by Y is changed to Z, the image will stay unchanged.
+ // The image shown in the Ubuntu's notification is still X instead of Z.
+ // Changing the filename constantly works around this problem
+ char filename[64];
+ SprintfLiteral(filename, "%d_%d.%s", getpid(), gImageNumber++,
+ GetImageFileExtension(mMimeType.get()));
+
+ rv = mLocalImageFile->Append(NS_ConvertUTF8toUTF16(filename));
+ if (NS_FAILED(rv)) {
+ LOGMPRIS("Failed to create an image filename");
+ return false;
+ }
+
+ rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
+ if (NS_FAILED(rv)) {
+ LOGMPRIS("Failed to create an image file");
+ return false;
+ }
+
+ cleanup.release();
+ return true;
+}
+
+bool MPRISServiceHandler::InitLocalImageFolder() {
+ if (mLocalImageFolder && LocalImageFolderExists()) {
+ return true;
+ }
+
+ nsresult rv = NS_ERROR_FAILURE;
+ if (IsRunningUnderFlatpak()) {
+ // The XDG_DATA_HOME points to the same location in the host and guest
+ // filesystem.
+ if (const auto* xdgDataHome = g_getenv("XDG_DATA_HOME")) {
+ rv = NS_NewNativeLocalFile(nsDependentCString(xdgDataHome), true,
+ getter_AddRefs(mLocalImageFolder));
+ }
+ } else {
+ rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR,
+ getter_AddRefs(mLocalImageFolder));
+ }
+
+ if (NS_FAILED(rv) || !mLocalImageFolder) {
+ LOGMPRIS("Failed to get the image folder");
+ return false;
+ }
+
+ auto cleanup = MakeScopeExit([&] { mLocalImageFolder = nullptr; });
+
+ rv = mLocalImageFolder->Append(u"firefox-mpris"_ns);
+ if (NS_FAILED(rv)) {
+ LOGMPRIS("Failed to name an image folder");
+ return false;
+ }
+
+ if (!LocalImageFolderExists()) {
+ rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv)) {
+ LOGMPRIS("Failed to create an image folder");
+ return false;
+ }
+ }
+
+ cleanup.release();
+ return true;
+}
+
+void MPRISServiceHandler::RemoveAllLocalImages() {
+ if (!mLocalImageFolder || !LocalImageFolderExists()) {
+ return;
+ }
+
+ nsresult rv = mLocalImageFolder->Remove(/* aRecursive */ true);
+ if (NS_FAILED(rv)) {
+ // It's ok to fail. The next removal is called when updating the
+ // media-session image, or closing the MPRIS.
+ LOGMPRIS("Failed to remove images");
+ }
+
+ LOGMPRIS("Abandon %s",
+ mLocalImageFile ? mLocalImageFile->NativePath().get() : "nothing");
+ mMPRISMetadata.mArtUrl.Truncate();
+ mLocalImageFile = nullptr;
+ mLocalImageFolder = nullptr;
+}
+
+bool MPRISServiceHandler::LocalImageFolderExists() {
+ MOZ_ASSERT(mLocalImageFolder);
+
+ bool exists;
+ nsresult rv = mLocalImageFolder->Exists(&exists);
+ return NS_SUCCEEDED(rv) && exists;
+}
+
+GVariant* MPRISServiceHandler::GetMetadataAsGVariant() const {
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}", "mpris:trackid",
+ g_variant_new("o", DBUS_MPRIS_TRACK_PATH));
+
+ g_variant_builder_add(
+ &builder, "{sv}", "xesam:title",
+ g_variant_new_string(static_cast<const gchar*>(
+ NS_ConvertUTF16toUTF8(mMPRISMetadata.mTitle).get())));
+
+ g_variant_builder_add(
+ &builder, "{sv}", "xesam:album",
+ g_variant_new_string(static_cast<const gchar*>(
+ NS_ConvertUTF16toUTF8(mMPRISMetadata.mAlbum).get())));
+
+ GVariantBuilder artistBuilder;
+ g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as"));
+ g_variant_builder_add(
+ &artistBuilder, "s",
+ static_cast<const gchar*>(
+ NS_ConvertUTF16toUTF8(mMPRISMetadata.mArtist).get()));
+ g_variant_builder_add(&builder, "{sv}", "xesam:artist",
+ g_variant_builder_end(&artistBuilder));
+
+ if (!mMPRISMetadata.mArtUrl.IsEmpty()) {
+ g_variant_builder_add(&builder, "{sv}", "mpris:artUrl",
+ g_variant_new_string(static_cast<const gchar*>(
+ mMPRISMetadata.mArtUrl.get())));
+ }
+
+ return g_variant_builder_end(&builder);
+}
+
+void MPRISServiceHandler::EmitEvent(dom::MediaControlKey aKey) const {
+ for (const auto& listener : mListeners) {
+ listener->OnActionPerformed(dom::MediaControlAction(aKey));
+ }
+}
+
+struct InterfaceProperty {
+ const char* interface;
+ const char* property;
+};
+static const std::unordered_map<dom::MediaControlKey, InterfaceProperty>
+ gKeyProperty = {
+ {dom::MediaControlKey::Focus, {DBUS_MPRIS_INTERFACE, "CanRaise"}},
+ {dom::MediaControlKey::Nexttrack,
+ {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoNext"}},
+ {dom::MediaControlKey::Previoustrack,
+ {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoPrevious"}},
+ {dom::MediaControlKey::Play, {DBUS_MPRIS_PLAYER_INTERFACE, "CanPlay"}},
+ {dom::MediaControlKey::Pause,
+ {DBUS_MPRIS_PLAYER_INTERFACE, "CanPause"}}};
+
+void MPRISServiceHandler::SetSupportedMediaKeys(
+ const MediaKeysArray& aSupportedKeys) {
+ uint32_t supportedKeys = 0;
+ for (const dom::MediaControlKey& key : aSupportedKeys) {
+ supportedKeys |= GetMediaKeyMask(key);
+ }
+
+ if (mSupportedKeys == supportedKeys) {
+ LOGMPRIS("Supported keys stay the same");
+ return;
+ }
+
+ uint32_t oldSupportedKeys = mSupportedKeys;
+ mSupportedKeys = supportedKeys;
+
+ // Emit related property changes
+ for (auto it : gKeyProperty) {
+ bool keyWasSupported = oldSupportedKeys & GetMediaKeyMask(it.first);
+ bool keyIsSupported = mSupportedKeys & GetMediaKeyMask(it.first);
+ if (keyWasSupported != keyIsSupported) {
+ LOGMPRIS("Emit PropertiesChanged signal: %s.%s=%s", it.second.interface,
+ it.second.property, keyIsSupported ? "true" : "false");
+ EmitSupportedKeyChanged(it.first, keyIsSupported);
+ }
+ }
+}
+
+bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey) const {
+ return mSupportedKeys & GetMediaKeyMask(aKey);
+}
+
+bool MPRISServiceHandler::EmitSupportedKeyChanged(dom::MediaControlKey aKey,
+ bool aSupported) const {
+ auto it = gKeyProperty.find(aKey);
+ if (it == gKeyProperty.end()) {
+ LOGMPRIS("No property for %s", ToMediaControlKeyStr(aKey));
+ return false;
+ }
+
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add(&builder, "{sv}",
+ static_cast<const gchar*>(it->second.property),
+ g_variant_new_boolean(aSupported));
+
+ GVariant* parameters = g_variant_new(
+ "(sa{sv}as)", static_cast<const gchar*>(it->second.interface), &builder,
+ nullptr);
+
+ LOGMPRIS("Emit MPRIS property changes for '%s.%s'", it->second.interface,
+ it->second.property);
+ return EmitPropertiesChangedSignal(parameters);
+}
+
+bool MPRISServiceHandler::EmitPropertiesChangedSignal(
+ GVariant* aParameters) const {
+ if (!mConnection) {
+ LOGMPRIS("No D-Bus Connection. Cannot emit properties changed signal");
+ return false;
+ }
+
+ GError* error = nullptr;
+ if (!g_dbus_connection_emit_signal(
+ mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH,
+ "org.freedesktop.DBus.Properties", "PropertiesChanged", aParameters,
+ &error)) {
+ LOGMPRIS("Failed to emit MPRIS property changes: %s",
+ error ? error->message : "Unknown Error");
+ if (error) {
+ g_error_free(error);
+ }
+ return false;
+ }
+
+ return true;
+}
+
+#undef LOGMPRIS
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/MPRISServiceHandler.h b/widget/gtk/MPRISServiceHandler.h
new file mode 100644
index 0000000000..469090efc0
--- /dev/null
+++ b/widget/gtk/MPRISServiceHandler.h
@@ -0,0 +1,195 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_
+#define WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_
+
+#include <gio/gio.h>
+#include "mozilla/dom/FetchImageHelper.h"
+#include "mozilla/dom/MediaControlKeySource.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIFile.h"
+#include "nsMimeTypes.h"
+#include "nsString.h"
+
+#define DBUS_MPRIS_SERVICE_NAME "org.mpris.MediaPlayer2.firefox"
+#define DBUS_MPRIS_OBJECT_PATH "/org/mpris/MediaPlayer2"
+#define DBUS_MPRIS_INTERFACE "org.mpris.MediaPlayer2"
+#define DBUS_MPRIS_PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player"
+#define DBUS_MPRIS_TRACK_PATH "/org/mpris/MediaPlayer2/firefox"
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * This class implements the "MPRIS" D-Bus Service
+ * (https://specifications.freedesktop.org/mpris-spec/2.2),
+ * which is used to communicate with the Desktop Environment about the
+ * Multimedia playing in Gecko.
+ * Note that this interface requires many methods which may not be supported by
+ * Gecko, the interface
+ * however provides CanXYZ properties for these methods, so the method is
+ * defined but won't be executed.
+ *
+ * Also note that the following defines are for parts that the MPRIS Spec
+ * defines optional. The code won't
+ * compile with any of the defines set, yet, as those aren't implemented yet and
+ * probably never will be of
+ * use for gecko. For sake of completeness, they have been added until the
+ * decision about their implementation
+ * is finally made.
+ *
+ * The constexpr'ed methods are capabilities of the user agent known at compile
+ * time, e.g. we decided at
+ * compile time whether we ever want to support closing the user agent via MPRIS
+ * (Quit() and CanQuit()).
+ *
+ * Other properties like CanPlay() might depend on the runtime state (is there
+ * media available for playback?)
+ * and thus aren't a constexpr but merely a const method.
+ */
+class MPRISServiceHandler final : public dom::MediaControlKeySource {
+ NS_INLINE_DECL_REFCOUNTING(MPRISServiceHandler, override)
+ public:
+ // Note that this constructor does NOT initialize the MPRIS Service but only
+ // this class. The method Open() is responsible for registering and MAY FAIL.
+
+ MPRISServiceHandler();
+ bool Open() override;
+ void Close() override;
+ bool IsOpened() const override;
+
+ // From the EventSource.
+ void SetPlaybackState(dom::MediaSessionPlaybackState aState) override;
+
+ // GetPlaybackState returns dom::PlaybackState. GetPlaybackStatus returns this
+ // state converted into d-bus variants.
+ GVariant* GetPlaybackStatus() const;
+
+ const char* Identity() const;
+ const char* DesktopEntry() const;
+ bool PressKey(dom::MediaControlKey aKey) const;
+
+ void SetMediaMetadata(const dom::MediaMetadataBase& aMetadata) override;
+ GVariant* GetMetadataAsGVariant() const;
+
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override;
+
+ bool IsMediaKeySupported(dom::MediaControlKey aKey) const;
+
+ void OwnName(GDBusConnection* aConnection);
+
+ private:
+ ~MPRISServiceHandler();
+
+ // Note: Registration Ids for the D-Bus start with 1, so a value of 0
+ // indicates an error (or an object which wasn't initialized yet)
+
+ // a handle to our bus registration/ownership
+ guint mOwnerId = 0;
+ // This is for the interface org.mpris.MediaPlayer2
+ guint mRootRegistrationId = 0;
+ // This is for the interface org.mpris.MediaPlayer2.Player
+ guint mPlayerRegistrationId = 0;
+ RefPtr<GDBusNodeInfo> mIntrospectionData;
+ GDBusConnection* mConnection = nullptr;
+ bool mInitialized = false;
+ nsAutoCString mIdentity;
+ nsAutoCString mDesktopEntry;
+
+ // The image format used in MPRIS is based on the mMimeType here. Although
+ // IMAGE_JPEG or IMAGE_BMP are valid types as well but a png image with
+ // transparent background will be converted into a jpeg/bmp file with a
+ // colored background IMAGE_PNG format seems to be the best choice for now.
+ nsCString mMimeType{IMAGE_PNG};
+
+ // A bitmask indicating what keys are enabled
+ uint32_t mSupportedKeys = 0;
+
+ class MPRISMetadata : public dom::MediaMetadataBase {
+ public:
+ MPRISMetadata() = default;
+ ~MPRISMetadata() = default;
+
+ void UpdateFromMetadataBase(const dom::MediaMetadataBase& aMetadata) {
+ mTitle = aMetadata.mTitle;
+ mArtist = aMetadata.mArtist;
+ mAlbum = aMetadata.mAlbum;
+ mArtwork = aMetadata.mArtwork;
+ }
+ void Clear() {
+ UpdateFromMetadataBase(MediaMetadataBase::EmptyData());
+ mArtUrl.Truncate();
+ }
+
+ nsCString mArtUrl;
+ };
+ MPRISMetadata mMPRISMetadata;
+
+ // The saved image file fetched from the URL
+ nsCOMPtr<nsIFile> mLocalImageFile;
+ nsCOMPtr<nsIFile> mLocalImageFolder;
+
+ UniquePtr<dom::FetchImageHelper> mImageFetcher;
+ MozPromiseRequestHolder<dom::ImagePromise> mImageFetchRequest;
+
+ nsString mFetchingUrl;
+ nsString mCurrentImageUrl;
+
+ size_t mNextImageIndex = 0;
+
+ // Load the image at index aIndex of the metadta's artwork to MPRIS
+ // asynchronously
+ void LoadImageAtIndex(const size_t aIndex);
+ bool SetImageToDisplay(const char* aImageData, uint32_t aDataSize);
+
+ bool RenewLocalImageFile(const char* aImageData, uint32_t aDataSize);
+ bool InitLocalImageFile();
+ bool InitLocalImageFolder();
+ void RemoveAllLocalImages();
+ bool LocalImageFolderExists();
+
+ // Queries nsAppInfo to get the branded browser name and vendor
+ void InitIdentity();
+
+ // non-public API, called from events
+ void OnNameAcquired(GDBusConnection* aConnection, const gchar* aName);
+ void OnNameLost(GDBusConnection* aConnection, const gchar* aName);
+ void OnBusAcquired(GDBusConnection* aConnection, const gchar* aName);
+
+ static void OnNameAcquiredStatic(GDBusConnection* aConnection,
+ const gchar* aName, gpointer aUserData);
+ static void OnNameLostStatic(GDBusConnection* aConnection, const gchar* aName,
+ gpointer aUserData);
+ static void OnBusAcquiredStatic(GDBusConnection* aConnection,
+ const gchar* aName, gpointer aUserData);
+
+ void EmitEvent(dom::MediaControlKey aKey) const;
+
+ bool EmitMetadataChanged() const;
+
+ void SetMediaMetadataInternal(const dom::MediaMetadataBase& aMetadata,
+ bool aClearArtUrl = true);
+
+ bool EmitSupportedKeyChanged(dom::MediaControlKey aKey,
+ bool aSupported) const;
+
+ bool EmitPropertiesChangedSignal(GVariant* aParameters) const;
+
+ void ClearMetadata();
+
+ RefPtr<GCancellable> mDBusGetCancellable;
+
+ nsCString mServiceName;
+ void SetServiceName(const char* aName);
+ const char* GetServiceName();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // WIDGET_GTK_MPRIS_SERVICE_HANDLER_H_
diff --git a/widget/gtk/MediaKeysEventSourceFactory.cpp b/widget/gtk/MediaKeysEventSourceFactory.cpp
new file mode 100644
index 0000000000..b9a3dfde41
--- /dev/null
+++ b/widget/gtk/MediaKeysEventSourceFactory.cpp
@@ -0,0 +1,14 @@
+/* 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 "MediaKeysEventSourceFactory.h"
+#include "MPRISServiceHandler.h"
+
+namespace mozilla::widget {
+
+mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() {
+ return new MPRISServiceHandler();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/MozContainer.cpp b/widget/gtk/MozContainer.cpp
new file mode 100644
index 0000000000..95d32b57b4
--- /dev/null
+++ b/widget/gtk/MozContainer.cpp
@@ -0,0 +1,402 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "MozContainer.h"
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <stdio.h>
+#include "mozilla/WidgetUtilsGtk.h"
+#include "nsWindow.h"
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gWidgetLog;
+# define LOGCONTAINER(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args)
+#else
+# define LOGCONTAINER(args)
+#endif /* MOZ_LOGGING */
+
+/* init methods */
+void moz_container_class_init(MozContainerClass* klass);
+static void moz_container_init(MozContainer* container);
+
+/* widget class methods */
+static void moz_container_map(GtkWidget* widget);
+void moz_container_unmap(GtkWidget* widget);
+static void moz_container_size_allocate(GtkWidget* widget,
+ GtkAllocation* allocation);
+static void moz_container_realize(GtkWidget* widget);
+static void moz_container_unrealize(GtkWidget* widget);
+
+/* container class methods */
+static void moz_container_remove(GtkContainer* container,
+ GtkWidget* child_widget);
+static void moz_container_forall(GtkContainer* container,
+ gboolean include_internals,
+ GtkCallback callback, gpointer callback_data);
+static void moz_container_add(GtkContainer* container, GtkWidget* widget);
+
+typedef struct _MozContainerChild MozContainerChild;
+
+struct _MozContainerChild {
+ GtkWidget* widget;
+ gint x;
+ gint y;
+};
+
+static void moz_container_allocate_child(MozContainer* container,
+ MozContainerChild* child);
+static MozContainerChild* moz_container_get_child(MozContainer* container,
+ GtkWidget* child);
+
+/* public methods */
+
+GType moz_container_get_type(void) {
+ static GType moz_container_type = 0;
+
+ if (!moz_container_type) {
+ static GTypeInfo moz_container_info = {
+ sizeof(MozContainerClass), /* class_size */
+ NULL, /* base_init */
+ NULL, /* base_finalize */
+ (GClassInitFunc)moz_container_class_init, /* class_init */
+ NULL, /* class_destroy */
+ NULL, /* class_data */
+ sizeof(MozContainer), /* instance_size */
+ 0, /* n_preallocs */
+ (GInstanceInitFunc)moz_container_init, /* instance_init */
+ NULL, /* value_table */
+ };
+
+ moz_container_type =
+ g_type_register_static(GTK_TYPE_CONTAINER, "MozContainer",
+ &moz_container_info, static_cast<GTypeFlags>(0));
+ }
+
+ return moz_container_type;
+}
+
+GtkWidget* moz_container_new(void) {
+ MozContainer* container;
+
+ container =
+ static_cast<MozContainer*>(g_object_new(MOZ_CONTAINER_TYPE, nullptr));
+
+ return GTK_WIDGET(container);
+}
+
+void moz_container_put(MozContainer* container, GtkWidget* child_widget, gint x,
+ gint y) {
+ MozContainerChild* child;
+
+ child = g_new(MozContainerChild, 1);
+
+ child->widget = child_widget;
+ child->x = x;
+ child->y = y;
+
+ /* printf("moz_container_put %p %p %d %d\n", (void *)container,
+ (void *)child_widget, x, y); */
+
+ container->data.children = g_list_append(container->data.children, child);
+
+ /* we assume that the caller of this function will have already set
+ the parent GdkWindow because we can have many anonymous children. */
+ gtk_widget_set_parent(child_widget, GTK_WIDGET(container));
+}
+
+static void moz_container_destroy(GtkWidget* widget) {
+ auto* container = MOZ_CONTAINER(widget);
+ if (container->destroyed) {
+ return; // The destroy signal may run multiple times.
+ }
+ LOGCONTAINER(("moz_container_destroy() [%p]\n",
+ (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget))));
+ container->destroyed = TRUE;
+ container->data.~Data();
+}
+
+void moz_container_class_init(MozContainerClass* klass) {
+ /*GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GtkObjectClass *object_class = GTK_OBJECT_CLASS (klass); */
+ GtkContainerClass* container_class = GTK_CONTAINER_CLASS(klass);
+ GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass);
+
+ widget_class->realize = moz_container_realize;
+ widget_class->unrealize = moz_container_unrealize;
+ widget_class->destroy = moz_container_destroy;
+
+#ifdef MOZ_WAYLAND
+ if (mozilla::widget::GdkIsWaylandDisplay()) {
+ widget_class->map = moz_container_wayland_map;
+ widget_class->size_allocate = moz_container_wayland_size_allocate;
+ widget_class->map_event = moz_container_wayland_map_event;
+ widget_class->unmap = moz_container_wayland_unmap;
+ } else {
+#endif
+ widget_class->map = moz_container_map;
+ widget_class->size_allocate = moz_container_size_allocate;
+ widget_class->unmap = moz_container_unmap;
+#ifdef MOZ_WAYLAND
+ }
+#endif
+
+ container_class->remove = moz_container_remove;
+ container_class->forall = moz_container_forall;
+ container_class->add = moz_container_add;
+}
+
+void moz_container_init(MozContainer* container) {
+ container->destroyed = FALSE;
+ new (&container->data) MozContainer::Data();
+ gtk_widget_set_can_focus(GTK_WIDGET(container), TRUE);
+ gtk_widget_set_redraw_on_allocate(GTK_WIDGET(container), FALSE);
+ LOGCONTAINER(("%s [%p]\n", __FUNCTION__,
+ (void*)moz_container_get_nsWindow(container)));
+}
+
+void moz_container_map(GtkWidget* widget) {
+ MozContainer* container;
+ GList* tmp_list;
+ GtkWidget* tmp_child;
+
+ g_return_if_fail(IS_MOZ_CONTAINER(widget));
+ container = MOZ_CONTAINER(widget);
+
+ LOGCONTAINER(("moz_container_map() [%p]",
+ (void*)moz_container_get_nsWindow(container)));
+
+ gtk_widget_set_mapped(widget, TRUE);
+
+ tmp_list = container->data.children;
+ while (tmp_list) {
+ tmp_child = ((MozContainerChild*)tmp_list->data)->widget;
+
+ if (gtk_widget_get_visible(tmp_child)) {
+ if (!gtk_widget_get_mapped(tmp_child)) gtk_widget_map(tmp_child);
+ }
+ tmp_list = tmp_list->next;
+ }
+
+ if (gtk_widget_get_has_window(widget)) {
+ gdk_window_show(gtk_widget_get_window(widget));
+ }
+}
+
+void moz_container_unmap(GtkWidget* widget) {
+ g_return_if_fail(IS_MOZ_CONTAINER(widget));
+
+ LOGCONTAINER(("moz_container_unmap() [%p]",
+ (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget))));
+
+ // Disable rendering to MozContainer before we unmap it.
+ nsWindow* window = moz_container_get_nsWindow(MOZ_CONTAINER(widget));
+ window->DisableRendering();
+
+ gtk_widget_set_mapped(widget, FALSE);
+
+ if (gtk_widget_get_has_window(widget)) {
+ gdk_window_hide(gtk_widget_get_window(widget));
+ }
+}
+
+void moz_container_realize(GtkWidget* widget) {
+ GdkWindow* parent = gtk_widget_get_parent_window(widget);
+ GdkWindow* window;
+
+ gtk_widget_set_realized(widget, TRUE);
+
+ GdkWindowAttr attributes;
+ gint attributes_mask = GDK_WA_VISUAL | GDK_WA_X | GDK_WA_Y;
+ GtkAllocation allocation;
+
+ gtk_widget_get_allocation(widget, &allocation);
+ attributes.event_mask = gtk_widget_get_events(widget);
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ MozContainer* container = MOZ_CONTAINER(widget);
+ attributes.visual =
+ container->data.force_default_visual
+ ? gdk_screen_get_system_visual(gtk_widget_get_screen(widget))
+ : gtk_widget_get_visual(widget);
+
+ window = gdk_window_new(parent, &attributes, attributes_mask);
+
+ LOGCONTAINER(("moz_container_realize() [%p] GdkWindow %p\n",
+ (void*)moz_container_get_nsWindow(container), (void*)window));
+
+ gtk_widget_register_window(widget, window);
+ gtk_widget_set_window(widget, window);
+}
+
+void moz_container_unrealize(GtkWidget* widget) {
+ GdkWindow* window = gtk_widget_get_window(widget);
+ LOGCONTAINER(("moz_container_unrealize() [%p] GdkWindow %p\n",
+ (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget)),
+ (void*)window));
+
+ if (gtk_widget_get_mapped(widget)) {
+ gtk_widget_unmap(widget);
+ }
+
+ gtk_widget_unregister_window(widget, window);
+ gtk_widget_set_window(widget, nullptr);
+ gdk_window_destroy(window);
+ gtk_widget_set_realized(widget, false);
+}
+
+void moz_container_size_allocate(GtkWidget* widget, GtkAllocation* allocation) {
+ MozContainer* container;
+ GList* tmp_list;
+ GtkAllocation tmp_allocation;
+
+ g_return_if_fail(IS_MOZ_CONTAINER(widget));
+
+ LOGCONTAINER(("moz_container_size_allocate [%p] %d,%d -> %d x %d\n",
+ (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget)),
+ allocation->x, allocation->y, allocation->width,
+ allocation->height));
+
+ /* short circuit if you can */
+ container = MOZ_CONTAINER(widget);
+ gtk_widget_get_allocation(widget, &tmp_allocation);
+ if (!container->data.children && tmp_allocation.x == allocation->x &&
+ tmp_allocation.y == allocation->y &&
+ tmp_allocation.width == allocation->width &&
+ tmp_allocation.height == allocation->height) {
+ return;
+ }
+
+ gtk_widget_set_allocation(widget, allocation);
+
+ tmp_list = container->data.children;
+
+ while (tmp_list) {
+ MozContainerChild* child = static_cast<MozContainerChild*>(tmp_list->data);
+
+ moz_container_allocate_child(container, child);
+
+ tmp_list = tmp_list->next;
+ }
+
+ if (gtk_widget_get_has_window(widget) && gtk_widget_get_realized(widget)) {
+ gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x,
+ allocation->y, allocation->width,
+ allocation->height);
+ }
+}
+
+void moz_container_remove(GtkContainer* container, GtkWidget* child_widget) {
+ MozContainerChild* child;
+ MozContainer* moz_container;
+ GdkWindow* parent_window;
+
+ g_return_if_fail(IS_MOZ_CONTAINER(container));
+ g_return_if_fail(GTK_IS_WIDGET(child_widget));
+
+ moz_container = MOZ_CONTAINER(container);
+
+ child = moz_container_get_child(moz_container, child_widget);
+ g_return_if_fail(child);
+
+ /* gtk_widget_unparent will remove the parent window (as well as the
+ * parent widget), but, in Mozilla's window hierarchy, the parent window
+ * may need to be kept because it may be part of a GdkWindow sub-hierarchy
+ * that is being moved to another MozContainer.
+ *
+ * (In a conventional GtkWidget hierarchy, GdkWindows being reparented
+ * would have their own GtkWidget and that widget would be the one being
+ * reparented. In Mozilla's hierarchy, the parent_window needs to be
+ * retained so that the GdkWindow sub-hierarchy is maintained.)
+ */
+ parent_window = gtk_widget_get_parent_window(child_widget);
+ if (parent_window) g_object_ref(parent_window);
+
+ gtk_widget_unparent(child_widget);
+
+ if (parent_window) {
+ /* The child_widget will always still exist because g_signal_emit,
+ * which invokes this function, holds a reference.
+ *
+ * If parent_window is the container's root window then it will not be
+ * the parent_window if the child_widget is placed in another
+ * container.
+ */
+ if (parent_window != gtk_widget_get_window(GTK_WIDGET(container))) {
+ gtk_widget_set_parent_window(child_widget, parent_window);
+ }
+
+ g_object_unref(parent_window);
+ }
+
+ moz_container->data.children =
+ g_list_remove(moz_container->data.children, child);
+ g_free(child);
+}
+
+void moz_container_forall(GtkContainer* container, gboolean include_internals,
+ GtkCallback callback, gpointer callback_data) {
+ g_return_if_fail(IS_MOZ_CONTAINER(container));
+ g_return_if_fail(callback);
+
+ MozContainer* moz_container = MOZ_CONTAINER(container);
+
+ GList* tmp_list = moz_container->data.children;
+ while (tmp_list) {
+ MozContainerChild* child;
+ child = static_cast<MozContainerChild*>(tmp_list->data);
+ tmp_list = tmp_list->next;
+ (*callback)(child->widget, callback_data);
+ }
+}
+
+static void moz_container_allocate_child(MozContainer* container,
+ MozContainerChild* child) {
+ GtkAllocation allocation;
+
+ gtk_widget_get_allocation(child->widget, &allocation);
+ allocation.x = child->x;
+ allocation.y = child->y;
+
+ gtk_widget_size_allocate(child->widget, &allocation);
+}
+
+MozContainerChild* moz_container_get_child(MozContainer* container,
+ GtkWidget* child_widget) {
+ GList* tmp_list = container->data.children;
+ while (tmp_list) {
+ MozContainerChild* child;
+
+ child = static_cast<MozContainerChild*>(tmp_list->data);
+ tmp_list = tmp_list->next;
+
+ if (child->widget == child_widget) return child;
+ }
+ return nullptr;
+}
+
+static void moz_container_add(GtkContainer* container, GtkWidget* widget) {
+ moz_container_put(MOZ_CONTAINER(container), widget, 0, 0);
+}
+
+void moz_container_force_default_visual(MozContainer* container) {
+ container->data.force_default_visual = true;
+}
+
+nsWindow* moz_container_get_nsWindow(MozContainer* container) {
+ gpointer user_data = g_object_get_data(G_OBJECT(container), "nsWindow");
+ return static_cast<nsWindow*>(user_data);
+}
+
+#undef LOGCONTAINER
diff --git a/widget/gtk/MozContainer.h b/widget/gtk/MozContainer.h
new file mode 100644
index 0000000000..27fa2a701f
--- /dev/null
+++ b/widget/gtk/MozContainer.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __MOZ_CONTAINER_H__
+#define __MOZ_CONTAINER_H__
+
+#ifdef MOZ_WAYLAND
+# include "MozContainerWayland.h"
+#endif
+
+#include <gtk/gtk.h>
+#include <functional>
+
+/*
+ * MozContainer
+ *
+ * This class serves three purposes in the nsIWidget implementation.
+ *
+ * - It provides objects to receive signals from GTK for events on native
+ * windows.
+ *
+ * - It provides GdkWindow to draw content on Wayland or when Gtk+ renders
+ * client side decorations to mShell.
+ *
+ * - It provides a container parent for GtkWidgets. The only GtkWidgets
+ * that need this in Mozilla are the GtkSockets for windowed plugins (Xt
+ * and XEmbed).
+ *
+ * Note that the window hierarchy in Mozilla differs from conventional
+ * GtkWidget hierarchies.
+ *
+ * Mozilla's hierarchy exists through the GdkWindow hierarchy, and all child
+ * GdkWindows (within a child nsIWidget hierarchy) belong to one MozContainer
+ * GtkWidget. If the MozContainer is unrealized or its GdkWindows are
+ * destroyed for some other reason, then the hierarchy no longer exists. (In
+ * conventional GTK clients, the hierarchy is recorded by the GtkWidgets, and
+ * so can be re-established after destruction of the GdkWindows.)
+ *
+ * One consequence of this is that the MozContainer does not know which of its
+ * GdkWindows should parent child GtkWidgets. (Conventional GtkContainers
+ * determine which GdkWindow to assign child GtkWidgets.)
+ *
+ * Therefore, when adding a child GtkWidget to a MozContainer,
+ * gtk_widget_set_parent_window should be called on the child GtkWidget before
+ * it is realized.
+ */
+
+#define MOZ_CONTAINER_TYPE (moz_container_get_type())
+#define MOZ_CONTAINER(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj), MOZ_CONTAINER_TYPE, MozContainer))
+#define MOZ_CONTAINER_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass), MOZ_CONTAINER_TYPE, MozContainerClass))
+#define IS_MOZ_CONTAINER(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj), MOZ_CONTAINER_TYPE))
+#define IS_MOZ_CONTAINER_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass), MOZ_CONTAINER_TYPE))
+#define MOZ_CONTAINER_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS((obj), MOZ_CONTAINER_TYPE, MozContainerClass))
+#ifdef MOZ_WAYLAND
+# define MOZ_WL_CONTAINER(obj) (&MOZ_CONTAINER(obj)->data.wl_container)
+#endif
+
+typedef struct _MozContainer MozContainer;
+typedef struct _MozContainerClass MozContainerClass;
+
+struct _MozContainer {
+ GtkContainer container;
+ gboolean destroyed;
+ struct Data {
+ GList* children = nullptr;
+ gboolean force_default_visual = false;
+#ifdef MOZ_WAYLAND
+ MozContainerWayland wl_container;
+#endif
+ } data;
+};
+
+struct _MozContainerClass {
+ GtkContainerClass parent_class;
+};
+
+GType moz_container_get_type(void);
+GtkWidget* moz_container_new(void);
+void moz_container_unmap(GtkWidget* widget);
+void moz_container_put(MozContainer* container, GtkWidget* child_widget, gint x,
+ gint y);
+void moz_container_force_default_visual(MozContainer* container);
+void moz_container_class_init(MozContainerClass* klass);
+
+class nsWindow;
+nsWindow* moz_container_get_nsWindow(MozContainer* container);
+
+#endif /* __MOZ_CONTAINER_H__ */
diff --git a/widget/gtk/MozContainerWayland.cpp b/widget/gtk/MozContainerWayland.cpp
new file mode 100644
index 0000000000..0e50a3f27c
--- /dev/null
+++ b/widget/gtk/MozContainerWayland.cpp
@@ -0,0 +1,824 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+/*
+ * MozContainerWayland is a wrapper over MozContainer which provides
+ * wl_surface for MozContainer widget.
+ *
+ * The widget scheme looks like:
+ *
+ * ---------------------------------------------------------
+ * | mShell Gtk widget (contains wl_surface owned by Gtk+) |
+ * | |
+ * | --------------------------------------------------- |
+ * | | mContainer (contains wl_surface owned by Gtk+) | |
+ * | | | |
+ * | | --------------------------------------------- | |
+ * | | | wl_subsurface (attached to wl_surface | | |
+ * | | | of mContainer) | | |
+ * | | | | | |
+ * | | | | | |
+ * | | --------------------------------------------- | |
+ * | --------------------------------------------------- |
+ * ---------------------------------------------------------
+ *
+ * We draw to wl_subsurface owned by MozContainerWayland.
+ * We need to wait until wl_surface of mContainer is created
+ * and then we create and attach our wl_subsurface to it.
+ *
+ * First wl_subsurface creation has these steps:
+ *
+ * 1) moz_container_wayland_size_allocate() handler is called when
+ * mContainer size/position is known.
+ * It calls moz_container_wayland_surface_create_locked(), registers
+ * a frame callback handler
+ * moz_container_wayland_frame_callback_handler().
+ *
+ * 2) moz_container_wayland_frame_callback_handler() is called
+ * when wl_surface owned by mozContainer is ready.
+ * We call initial_draw_cbs() handler and we can create our wl_subsurface
+ * on top of wl_surface owned by mozContainer.
+ *
+ * When MozContainer hides/show again, moz_container_wayland_size_allocate()
+ * handler may not be called as MozContainer size is set. So after first
+ * show/hide sequence use moz_container_wayland_map_event() to create
+ * wl_subsurface of MozContainer.
+ */
+
+#include "MozContainer.h"
+
+#include <dlfcn.h>
+#include <glib.h>
+#include <stdio.h>
+#include <wayland-egl.h>
+
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsGtkUtils.h"
+#include "nsWaylandDisplay.h"
+#include "base/task.h"
+
+#ifdef MOZ_LOGGING
+
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+# include "nsWindow.h"
+extern mozilla::LazyLogModule gWidgetWaylandLog;
+extern mozilla::LazyLogModule gWidgetLog;
+# define LOGWAYLAND(...) \
+ MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define LOGCONTAINER(...) \
+ MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# define LOGWAYLAND(...)
+# define LOGCONTAINER(...)
+#endif /* MOZ_LOGGING */
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+static bool moz_container_wayland_surface_create_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container);
+static void moz_container_wayland_set_opaque_region_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container);
+
+// Lock mozcontainer and get wayland surface of it. You need to pair with
+// moz_container_wayland_surface_unlock() even
+// if moz_container_wayland_surface_lock() fails and returns nullptr.
+static struct wl_surface* moz_container_wayland_surface_lock(
+ MozContainer* container);
+static void moz_container_wayland_surface_unlock(MozContainer* container,
+ struct wl_surface** surface);
+
+MozContainerSurfaceLock::MozContainerSurfaceLock(MozContainer* aContainer) {
+ mContainer = aContainer;
+ mSurface = moz_container_wayland_surface_lock(aContainer);
+}
+MozContainerSurfaceLock::~MozContainerSurfaceLock() {
+ moz_container_wayland_surface_unlock(mContainer, &mSurface);
+}
+struct wl_surface* MozContainerSurfaceLock::GetSurface() { return mSurface; }
+
+// Invalidate gtk wl_surface to commit changes to wl_subsurface.
+// wl_subsurface changes are effective when parent surface is commited.
+static void moz_container_wayland_invalidate(MozContainer* container) {
+ LOGWAYLAND("moz_container_wayland_invalidate [%p]\n",
+ (void*)moz_container_get_nsWindow(container));
+
+ GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container));
+ if (!window) {
+ LOGWAYLAND(" Failed - missing GdkWindow!\n");
+ return;
+ }
+ gdk_window_invalidate_rect(window, nullptr, true);
+}
+
+// Route input to parent wl_surface owned by Gtk+ so we get input
+// events from Gtk+.
+static void moz_container_clear_input_region(MozContainer* container) {
+ struct wl_compositor* compositor = WaylandDisplayGet()->GetCompositor();
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ wl_region* region = wl_compositor_create_region(compositor);
+ wl_surface_set_input_region(wl_container->surface, region);
+ wl_region_destroy(region);
+}
+
+static void moz_container_wayland_move_locked(const MutexAutoLock& aProofOfLock,
+ MozContainer* container, int dx,
+ int dy) {
+ LOGCONTAINER("moz_container_wayland_move [%p] %d,%d\n",
+ (void*)moz_container_get_nsWindow(container), dx, dy);
+
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ if (!wl_container->subsurface || (wl_container->subsurface_dx == dx &&
+ wl_container->subsurface_dy == dy)) {
+ return;
+ }
+
+ wl_container->subsurface_dx = dx;
+ wl_container->subsurface_dy = dy;
+ wl_subsurface_set_position(wl_container->subsurface,
+ wl_container->subsurface_dx,
+ wl_container->subsurface_dy);
+}
+
+// This is called from layout/compositor code only with
+// size equal to GL rendering context.
+
+// Return false if scale factor doesn't match buffer size.
+// We need to skip painting in such case do avoid Wayland compositor freaking.
+bool moz_container_wayland_egl_window_set_size(MozContainer* container,
+ nsIntSize aSize, int aScale) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ MutexAutoLock lock(wl_container->container_lock);
+
+ // We may be called after unmap so we're missing egl window completelly.
+ // In such case don't return false which would block compositor.
+ // We return true here and don't block flush WebRender queue.
+ // We'll be repainted if our window become visible again anyway.
+ if (!wl_container->eglwindow) {
+ return true;
+ }
+
+ if (wl_container->buffer_scale != aScale) {
+ moz_container_wayland_set_scale_factor_locked(lock, container, aScale);
+ }
+
+ /* Enable for size changes logging
+ LOGCONTAINER(
+ "moz_container_wayland_egl_window_set_size [%p] %d x %d scale %d "
+ "(unscaled %d x %d)",
+ (void*)moz_container_get_nsWindow(container), aSize.width, aSize.height,
+ aScale, aSize.width / aScale, aSize.height / aScale);
+ */
+ wl_egl_window_resize(wl_container->eglwindow, aSize.width, aSize.height, 0,
+ 0);
+
+ return moz_container_wayland_size_matches_scale_factor_locked(
+ lock, container, aSize.width, aSize.height);
+}
+
+void moz_container_wayland_add_initial_draw_callback_locked(
+ MozContainer* container, const std::function<void(void)>& initial_draw_cb) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+
+ if (wl_container->ready_to_draw && !wl_container->surface) {
+ NS_WARNING(
+ "moz_container_wayland_add_or_fire_initial_draw_callback:"
+ " ready to draw without wayland surface!");
+ }
+ MOZ_DIAGNOSTIC_ASSERT(!wl_container->ready_to_draw || !wl_container->surface);
+ wl_container->initial_draw_cbs.push_back(initial_draw_cb);
+}
+
+void moz_container_wayland_add_or_fire_initial_draw_callback(
+ MozContainer* container, const std::function<void(void)>& initial_draw_cb) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ {
+ MutexAutoLock lock(wl_container->container_lock);
+ if (wl_container->ready_to_draw && !wl_container->surface) {
+ NS_WARNING(
+ "moz_container_wayland_add_or_fire_initial_draw_callback: ready to "
+ "draw "
+ "without wayland surface!");
+ }
+ if (!wl_container->ready_to_draw || !wl_container->surface) {
+ wl_container->initial_draw_cbs.push_back(initial_draw_cb);
+ return;
+ }
+ }
+
+ // We're ready to draw as
+ // wl_container->ready_to_draw && wl_container->surface
+ // call the callback directly instead of store them.
+ initial_draw_cb();
+}
+
+static void moz_container_wayland_clear_initial_draw_callback_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ MozClearPointer(wl_container->frame_callback_handler, wl_callback_destroy);
+ wl_container->initial_draw_cbs.clear();
+}
+
+void moz_container_wayland_clear_initial_draw_callback(
+ MozContainer* container) {
+ MutexAutoLock lock(container->data.wl_container.container_lock);
+ moz_container_wayland_clear_initial_draw_callback_locked(lock, container);
+}
+
+static void moz_container_wayland_frame_callback_handler(
+ void* data, struct wl_callback* callback, uint32_t time) {
+ MozContainerWayland* wl_container = MOZ_WL_CONTAINER(data);
+
+ LOGWAYLAND(
+ "%s [%p] frame_callback_handler %p ready_to_draw %d (set to true)"
+ " initial_draw callback %zd\n",
+ __FUNCTION__, (void*)moz_container_get_nsWindow(MOZ_CONTAINER(data)),
+ (void*)wl_container->frame_callback_handler, wl_container->ready_to_draw,
+ wl_container->initial_draw_cbs.size());
+
+ std::vector<std::function<void(void)>> cbs;
+ {
+ // Protect mozcontainer internals changes by container_lock.
+ MutexAutoLock lock(wl_container->container_lock);
+ MozClearPointer(wl_container->frame_callback_handler, wl_callback_destroy);
+ // It's possible that container is already unmapped so quit in such case.
+ if (!wl_container->surface) {
+ LOGWAYLAND(" container is unmapped, quit.");
+ if (!wl_container->initial_draw_cbs.empty()) {
+ NS_WARNING("Unmapping MozContainer with active draw callback!");
+ wl_container->initial_draw_cbs.clear();
+ }
+ return;
+ }
+ if (wl_container->ready_to_draw) {
+ return;
+ }
+ wl_container->ready_to_draw = true;
+ cbs = std::move(wl_container->initial_draw_cbs);
+ }
+
+ // Call the callbacks registered by
+ // moz_container_wayland_add_or_fire_initial_draw_callback().
+ // and we can't do that under mozcontainer lock.
+ for (auto const& cb : cbs) {
+ cb();
+ }
+}
+
+static const struct wl_callback_listener moz_container_frame_listener = {
+ moz_container_wayland_frame_callback_handler};
+
+static void after_frame_clock_after_paint(GdkFrameClock* clock,
+ MozContainer* container) {
+ MozContainerSurfaceLock lock(container);
+ struct wl_surface* surface = lock.GetSurface();
+ if (surface) {
+ wl_surface_commit(surface);
+ }
+}
+
+static bool moz_gdk_wayland_window_add_frame_callback_surface_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container) {
+ static auto sGdkWaylandWindowAddCallbackSurface =
+ (void (*)(GdkWindow*, struct wl_surface*))dlsym(
+ RTLD_DEFAULT, "gdk_wayland_window_add_frame_callback_surface");
+
+ if (!StaticPrefs::widget_wayland_opaque_region_enabled_AtStartup() ||
+ !sGdkWaylandWindowAddCallbackSurface) {
+ return false;
+ }
+
+ GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container));
+ MozContainerWayland* wl_container = &container->data.wl_container;
+
+ sGdkWaylandWindowAddCallbackSurface(window, wl_container->surface);
+
+ GdkFrameClock* frame_clock = gdk_window_get_frame_clock(window);
+ g_signal_connect_after(frame_clock, "after-paint",
+ G_CALLBACK(after_frame_clock_after_paint), container);
+ return true;
+}
+
+static void moz_gdk_wayland_window_remove_frame_callback_surface_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container) {
+ static auto sGdkWaylandWindowRemoveCallbackSurface =
+ (void (*)(GdkWindow*, struct wl_surface*))dlsym(
+ RTLD_DEFAULT, "gdk_wayland_window_remove_frame_callback_surface");
+
+ if (!sGdkWaylandWindowRemoveCallbackSurface) {
+ return;
+ }
+
+ GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container));
+ MozContainerWayland* wl_container = &container->data.wl_container;
+
+ if (wl_container->surface) {
+ sGdkWaylandWindowRemoveCallbackSurface(window, wl_container->surface);
+ }
+
+ GdkFrameClock* frame_clock = gdk_window_get_frame_clock(window);
+ g_signal_handlers_disconnect_by_func(
+ frame_clock, FuncToGpointer(after_frame_clock_after_paint), container);
+}
+
+void moz_container_wayland_unmap(GtkWidget* widget) {
+ g_return_if_fail(IS_MOZ_CONTAINER(widget));
+
+ // Unmap MozContainer first so we can remove our resources
+ moz_container_unmap(widget);
+
+ MozContainer* container = MOZ_CONTAINER(widget);
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ MutexAutoLock lock(wl_container->container_lock);
+
+ LOGCONTAINER("%s [%p]\n", __FUNCTION__,
+ (void*)moz_container_get_nsWindow(container));
+
+ moz_container_wayland_clear_initial_draw_callback_locked(lock, container);
+
+ if (wl_container->opaque_region_used) {
+ moz_gdk_wayland_window_remove_frame_callback_surface_locked(lock,
+ container);
+ }
+ if (wl_container->commit_to_parent) {
+ wl_container->surface = nullptr;
+ }
+
+ MozClearPointer(wl_container->eglwindow, wl_egl_window_destroy);
+ MozClearPointer(wl_container->subsurface, wl_subsurface_destroy);
+ MozClearPointer(wl_container->surface, wl_surface_destroy);
+ MozClearPointer(wl_container->viewport, wp_viewport_destroy);
+ MozClearPointer(wl_container->fractional_scale,
+ wp_fractional_scale_v1_destroy);
+
+ wl_container->ready_to_draw = false;
+ wl_container->buffer_scale = 1;
+ wl_container->current_fractional_scale = 0.0;
+}
+
+gboolean moz_container_wayland_map_event(GtkWidget* widget,
+ GdkEventAny* event) {
+ MozContainerWayland* wl_container = &MOZ_CONTAINER(widget)->data.wl_container;
+
+ LOGCONTAINER("%s [%p]\n", __FUNCTION__,
+ (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget)));
+
+ // Return early if we're not mapped. Gtk may send bogus map_event signal
+ // to unmapped widgets (see Bug 1875369).
+ if (!gtk_widget_get_mapped(widget)) {
+ return false;
+ }
+
+ // Make sure we're on main thread as we can't lock mozContainer here
+ // due to moz_container_wayland_add_or_fire_initial_draw_callback() call
+ // below.
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ // Set waiting_to_show flag. It means the mozcontainer is cofigured/mapped
+ // and it's supposed to be visible. *But* it's really visible when we get
+ // moz_container_wayland_add_or_fire_initial_draw_callback() which means
+ // wayland compositor makes it live.
+ wl_container->waiting_to_show = true;
+ MozContainer* container = MOZ_CONTAINER(widget);
+ moz_container_wayland_add_or_fire_initial_draw_callback(
+ container, [container]() -> void {
+ LOGCONTAINER(
+ "[%p] moz_container_wayland_add_or_fire_initial_draw_callback set "
+ "visible",
+ moz_container_get_nsWindow(container));
+ moz_container_wayland_clear_waiting_to_show_flag(container);
+ });
+
+ MutexAutoLock lock(wl_container->container_lock);
+
+ // Don't create wl_subsurface in map_event when it's already created or
+ // if we create it for the first time.
+ if (wl_container->ready_to_draw || wl_container->before_first_size_alloc) {
+ return FALSE;
+ }
+
+ if (!wl_container->surface) {
+ if (!moz_container_wayland_surface_create_locked(lock,
+ MOZ_CONTAINER(widget))) {
+ return FALSE;
+ }
+ }
+
+ nsWindow* window = moz_container_get_nsWindow(MOZ_CONTAINER(widget));
+ moz_container_wayland_set_scale_factor_locked(lock, MOZ_CONTAINER(widget),
+ window->GdkCeiledScaleFactor());
+ moz_container_wayland_set_opaque_region_locked(lock, MOZ_CONTAINER(widget));
+ moz_container_clear_input_region(MOZ_CONTAINER(widget));
+ moz_container_wayland_invalidate(MOZ_CONTAINER(widget));
+ return FALSE;
+}
+
+void moz_container_wayland_map(GtkWidget* widget) {
+ LOGCONTAINER("%s [%p]\n", __FUNCTION__,
+ (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget)));
+
+ g_return_if_fail(IS_MOZ_CONTAINER(widget));
+
+ // We need to mark MozContainer as mapped to make sure
+ // moz_container_wayland_unmap() is called on hide/withdraw.
+ gtk_widget_set_mapped(widget, TRUE);
+
+ if (gtk_widget_get_has_window(widget)) {
+ gdk_window_show(gtk_widget_get_window(widget));
+ }
+}
+
+void moz_container_wayland_size_allocate(GtkWidget* widget,
+ GtkAllocation* allocation) {
+ MozContainer* container;
+ GtkAllocation tmp_allocation;
+
+ g_return_if_fail(IS_MOZ_CONTAINER(widget));
+
+ LOGCONTAINER("moz_container_wayland_size_allocate [%p] %d,%d -> %d x %d\n",
+ (void*)moz_container_get_nsWindow(MOZ_CONTAINER(widget)),
+ allocation->x, allocation->y, allocation->width,
+ allocation->height);
+
+ /* short circuit if you can */
+ container = MOZ_CONTAINER(widget);
+ gtk_widget_get_allocation(widget, &tmp_allocation);
+ if (!container->data.children && tmp_allocation.x == allocation->x &&
+ tmp_allocation.y == allocation->y &&
+ tmp_allocation.width == allocation->width &&
+ tmp_allocation.height == allocation->height) {
+ return;
+ }
+ gtk_widget_set_allocation(widget, allocation);
+
+ if (gtk_widget_get_has_window(widget) && gtk_widget_get_realized(widget)) {
+ gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x,
+ allocation->y, allocation->width,
+ allocation->height);
+ // We need to position our subsurface according to GdkWindow
+ // when offset changes (GdkWindow is maximized for instance).
+ // see gtk-clutter-embed.c for reference.
+ MutexAutoLock lock(container->data.wl_container.container_lock);
+ if (!container->data.wl_container.surface) {
+ if (!moz_container_wayland_surface_create_locked(lock, container)) {
+ return;
+ }
+ }
+ nsWindow* window = moz_container_get_nsWindow(container);
+ moz_container_wayland_set_scale_factor_locked(
+ lock, container, window->GdkCeiledScaleFactor());
+ moz_container_wayland_set_opaque_region_locked(lock, container);
+ moz_container_wayland_move_locked(lock, container, allocation->x,
+ allocation->y);
+ moz_container_clear_input_region(container);
+ moz_container_wayland_invalidate(MOZ_CONTAINER(widget));
+ container->data.wl_container.before_first_size_alloc = false;
+ }
+}
+
+static wl_region* moz_container_wayland_create_opaque_region(
+ int aX, int aY, int aWidth, int aHeight, int aCornerRadius) {
+ struct wl_compositor* compositor = WaylandDisplayGet()->GetCompositor();
+ wl_region* region = wl_compositor_create_region(compositor);
+ wl_region_add(region, aX, aY, aWidth, aHeight);
+ if (aCornerRadius) {
+ wl_region_subtract(region, aX, aY, aCornerRadius, aCornerRadius);
+ wl_region_subtract(region, aX + aWidth - aCornerRadius, aY, aCornerRadius,
+ aCornerRadius);
+ wl_region_subtract(region, aX, aY + aHeight - aCornerRadius, aCornerRadius,
+ aCornerRadius);
+ wl_region_subtract(region, aX + aWidth - aCornerRadius,
+ aY + aHeight - aCornerRadius, aCornerRadius,
+ aCornerRadius);
+ }
+ return region;
+}
+
+static void moz_container_wayland_set_opaque_region_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+
+ if (!wl_container->opaque_region_needs_updates) {
+ return;
+ }
+
+ if (!wl_container->opaque_region_used) {
+ wl_container->opaque_region_needs_updates = false;
+ return;
+ }
+
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(container), &allocation);
+
+ wl_region* region = moz_container_wayland_create_opaque_region(
+ 0, 0, allocation.width, allocation.height,
+ wl_container->opaque_region_corner_radius);
+ wl_surface_set_opaque_region(wl_container->surface, region);
+ wl_region_destroy(region);
+ wl_container->opaque_region_needs_updates = false;
+}
+
+static void moz_container_wayland_set_opaque_region(MozContainer* container) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ MutexAutoLock lock(wl_container->container_lock);
+ if (wl_container->surface) {
+ moz_container_wayland_set_opaque_region_locked(lock, container);
+ }
+}
+
+static void moz_container_wayland_surface_set_scale_locked(
+ const MutexAutoLock& aProofOfLock, MozContainerWayland* wl_container,
+ int scale) {
+ if (!wl_container->surface) {
+ return;
+ }
+ if (wl_container->buffer_scale == scale) {
+ return;
+ }
+
+ LOGCONTAINER("%s scale %d\n", __FUNCTION__, scale);
+
+ // There is a chance that the attached wl_buffer has not yet been doubled
+ // on the main thread when scale factor changed to 2. This leads to
+ // crash with the following message:
+ // Buffer size (AxB) must be an integer multiple of the buffer_scale (2)
+ // Removing the possibly wrong wl_buffer to prevent that crash:
+ wl_surface_attach(wl_container->surface, nullptr, 0, 0);
+ wl_surface_set_buffer_scale(wl_container->surface, scale);
+ wl_container->buffer_scale = scale;
+}
+
+static void fractional_scale_handle_preferred_scale(
+ void* data, struct wp_fractional_scale_v1* info, uint32_t wire_scale) {
+ MozContainer* container = MOZ_CONTAINER(data);
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ wl_container->current_fractional_scale = wire_scale / 120.0;
+
+ RefPtr<nsWindow> window = moz_container_get_nsWindow(container);
+ LOGWAYLAND("%s [%p] scale: %f\n", __func__, window.get(),
+ wl_container->current_fractional_scale);
+ MOZ_DIAGNOSTIC_ASSERT(window);
+ window->OnScaleChanged(/* aNotify = */ true);
+}
+
+static const struct wp_fractional_scale_v1_listener fractional_scale_listener =
+ {
+ .preferred_scale = fractional_scale_handle_preferred_scale,
+};
+
+void moz_container_wayland_set_scale_factor_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container, int aScale) {
+ if (gfx::gfxVars::UseWebRenderCompositor()) {
+ // the compositor backend handles scaling itself
+ return;
+ }
+
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ wl_container->container_lock.AssertCurrentThreadOwns();
+
+ if (StaticPrefs::widget_wayland_fractional_scale_enabled_AtStartup()) {
+ if (!wl_container->fractional_scale) {
+ if (auto* manager = WaylandDisplayGet()->GetFractionalScaleManager()) {
+ wl_container->fractional_scale =
+ wp_fractional_scale_manager_v1_get_fractional_scale(
+ manager, wl_container->surface);
+ wp_fractional_scale_v1_add_listener(wl_container->fractional_scale,
+ &fractional_scale_listener,
+ container);
+ }
+ }
+
+ if (wl_container->fractional_scale) {
+ if (!wl_container->viewport) {
+ if (auto* viewporter = WaylandDisplayGet()->GetViewporter()) {
+ wl_container->viewport =
+ wp_viewporter_get_viewport(viewporter, wl_container->surface);
+ }
+ }
+ if (wl_container->viewport) {
+ GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container));
+ wp_viewport_set_destination(wl_container->viewport,
+ gdk_window_get_width(gdkWindow),
+ gdk_window_get_height(gdkWindow));
+ return;
+ }
+ }
+ }
+
+ moz_container_wayland_surface_set_scale_locked(aProofOfLock, wl_container,
+ aScale);
+}
+
+bool moz_container_wayland_size_matches_scale_factor_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container, int aWidth,
+ int aHeight) {
+ return aWidth % container->data.wl_container.buffer_scale == 0 &&
+ aHeight % container->data.wl_container.buffer_scale == 0;
+}
+
+static bool moz_container_wayland_surface_create_locked(
+ const MutexAutoLock& aProofOfLock, MozContainer* container) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+
+ LOGWAYLAND("%s [%p]\n", __FUNCTION__,
+ (void*)moz_container_get_nsWindow(container));
+
+ GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container));
+ MOZ_DIAGNOSTIC_ASSERT(window);
+
+ wl_surface* parent_surface = gdk_wayland_window_get_wl_surface(window);
+ if (!parent_surface) {
+ LOGWAYLAND(" Failed - missing parent surface!");
+ return false;
+ }
+ LOGWAYLAND(" gtk wl_surface %p ID %d\n", (void*)parent_surface,
+ wl_proxy_get_id((struct wl_proxy*)parent_surface));
+
+ if (wl_container->commit_to_parent) {
+ LOGWAYLAND(" commit to parent");
+ wl_container->surface = parent_surface;
+ NS_DispatchToCurrentThread(NewRunnableFunction(
+ "moz_container_wayland_frame_callback_handler",
+ &moz_container_wayland_frame_callback_handler, container, nullptr, 0));
+ return true;
+ }
+
+ // Available as of GTK 3.8+
+ struct wl_compositor* compositor = WaylandDisplayGet()->GetCompositor();
+ wl_container->surface = wl_compositor_create_surface(compositor);
+ if (!wl_container->surface) {
+ LOGWAYLAND(" Failed - can't create surface!");
+ return false;
+ }
+
+ wl_container->subsurface =
+ wl_subcompositor_get_subsurface(WaylandDisplayGet()->GetSubcompositor(),
+ wl_container->surface, parent_surface);
+ if (!wl_container->subsurface) {
+ MozClearPointer(wl_container->surface, wl_surface_destroy);
+ LOGWAYLAND(" Failed - can't create sub-surface!");
+ return false;
+ }
+ wl_subsurface_set_desync(wl_container->subsurface);
+
+ // Try to guess subsurface offset to avoid potential flickering.
+ int dx, dy;
+ if (moz_container_get_nsWindow(container)->GetCSDDecorationOffset(&dx, &dy)) {
+ wl_container->subsurface_dx = dx;
+ wl_container->subsurface_dy = dy;
+ wl_subsurface_set_position(wl_container->subsurface, dx, dy);
+ LOGWAYLAND(" guessing subsurface position %d %d\n", dx, dy);
+ }
+
+ // If there's pending frame callback it's for wrong parent surface,
+ // so delete it.
+ if (wl_container->frame_callback_handler) {
+ MozClearPointer(wl_container->frame_callback_handler, wl_callback_destroy);
+ }
+ wl_container->frame_callback_handler = wl_surface_frame(parent_surface);
+ wl_callback_add_listener(wl_container->frame_callback_handler,
+ &moz_container_frame_listener, container);
+ LOGWAYLAND(
+ " created frame callback ID %d\n",
+ wl_proxy_get_id((struct wl_proxy*)wl_container->frame_callback_handler));
+
+ wl_surface_commit(wl_container->surface);
+ wl_display_flush(WaylandDisplayGet()->GetDisplay());
+
+ wl_container->opaque_region_used =
+ moz_gdk_wayland_window_add_frame_callback_surface_locked(aProofOfLock,
+ container);
+
+ LOGWAYLAND(" created surface %p ID %d\n", (void*)wl_container->surface,
+ wl_proxy_get_id((struct wl_proxy*)wl_container->surface));
+ return true;
+}
+
+struct wl_surface* moz_container_wayland_surface_lock(MozContainer* container)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ // LOGWAYLAND("%s [%p] surface %p ready_to_draw %d\n", __FUNCTION__,
+ // (void*)container, (void*)container->data.wl_container.surface,
+ // container->data.wl_container.ready_to_draw);
+ container->data.wl_container.container_lock.Lock();
+ if (!container->data.wl_container.surface ||
+ !container->data.wl_container.ready_to_draw) {
+ return nullptr;
+ }
+ return container->data.wl_container.surface;
+}
+
+void moz_container_wayland_surface_unlock(MozContainer* container,
+ struct wl_surface** surface)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ // Temporarily disabled to avoid log noise
+ // LOGWAYLAND("%s [%p] surface %p\n", __FUNCTION__, (void*)container,
+ // (void*)container->data.wl_container.surface);
+ if (*surface) {
+ *surface = nullptr;
+ }
+ container->data.wl_container.container_lock.Unlock();
+}
+
+struct wl_egl_window* moz_container_wayland_get_egl_window(
+ MozContainer* container, double scale) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+
+ LOGCONTAINER("%s [%p] eglwindow %p scale %d\n", __FUNCTION__,
+ (void*)moz_container_get_nsWindow(container),
+ (void*)wl_container->eglwindow, (int)scale);
+
+ MutexAutoLock lock(wl_container->container_lock);
+ if (!wl_container->surface || !wl_container->ready_to_draw) {
+ LOGCONTAINER(
+ " quit, wl_container->surface %p wl_container->ready_to_draw %d\n",
+ wl_container->surface, wl_container->ready_to_draw);
+ return nullptr;
+ }
+
+ GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(container));
+ nsIntSize requestedSize((int)round(gdk_window_get_width(window) * scale),
+ (int)round(gdk_window_get_height(window) * scale));
+
+ if (!wl_container->eglwindow) {
+ wl_container->eglwindow = wl_egl_window_create(
+ wl_container->surface, requestedSize.width, requestedSize.height);
+
+ LOGCONTAINER("%s [%p] created eglwindow %p size %d x %d (with scale %f)\n",
+ __FUNCTION__, (void*)moz_container_get_nsWindow(container),
+ (void*)wl_container->eglwindow, requestedSize.width,
+ requestedSize.height, scale);
+ } else {
+ nsIntSize recentSize;
+ wl_egl_window_get_attached_size(wl_container->eglwindow, &recentSize.width,
+ &recentSize.height);
+ if (requestedSize != recentSize) {
+ LOGCONTAINER("%s [%p] resized to %d x %d (with scale %f)\n", __FUNCTION__,
+ (void*)moz_container_get_nsWindow(container),
+ requestedSize.width, requestedSize.height, scale);
+ wl_egl_window_resize(wl_container->eglwindow, requestedSize.width,
+ requestedSize.height, 0, 0);
+ }
+ }
+ moz_container_wayland_surface_set_scale_locked(lock, wl_container,
+ static_cast<int>(scale));
+ return wl_container->eglwindow;
+}
+
+gboolean moz_container_wayland_has_egl_window(MozContainer* container) {
+ return !!container->data.wl_container.eglwindow;
+}
+
+void moz_container_wayland_update_opaque_region(MozContainer* container,
+ int corner_radius) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ wl_container->opaque_region_needs_updates = true;
+ wl_container->opaque_region_corner_radius = corner_radius;
+
+ // When GL compositor / WebRender is used,
+ // moz_container_wayland_get_egl_window() is called only once when window
+ // is created or resized so update opaque region now.
+ if (moz_container_wayland_has_egl_window(container)) {
+ moz_container_wayland_set_opaque_region(container);
+ }
+}
+
+gboolean moz_container_wayland_can_draw(MozContainer* container) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ MutexAutoLock lock(wl_container->container_lock);
+ return wl_container->ready_to_draw;
+}
+
+double moz_container_wayland_get_fractional_scale(MozContainer* container) {
+ return container->data.wl_container.current_fractional_scale;
+}
+
+double moz_container_wayland_get_scale(MozContainer* container) {
+ nsWindow* window = moz_container_get_nsWindow(container);
+ return window ? window->FractionalScaleFactor() : 1.0;
+}
+
+void moz_container_wayland_set_commit_to_parent(MozContainer* container) {
+ MozContainerWayland* wl_container = &container->data.wl_container;
+ MOZ_DIAGNOSTIC_ASSERT(!wl_container->surface);
+ wl_container->commit_to_parent = true;
+}
+
+bool moz_container_wayland_is_commiting_to_parent(MozContainer* container) {
+ return container->data.wl_container.commit_to_parent;
+}
+
+bool moz_container_wayland_is_waiting_to_show(MozContainer* container) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ return container->data.wl_container.waiting_to_show;
+}
+
+void moz_container_wayland_clear_waiting_to_show_flag(MozContainer* container) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ container->data.wl_container.waiting_to_show = false;
+}
diff --git a/widget/gtk/MozContainerWayland.h b/widget/gtk/MozContainerWayland.h
new file mode 100644
index 0000000000..068c674256
--- /dev/null
+++ b/widget/gtk/MozContainerWayland.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __MOZ_CONTAINER_WAYLAND_H__
+#define __MOZ_CONTAINER_WAYLAND_H__
+
+#include <gtk/gtk.h>
+#include <functional>
+#include <vector>
+#include "mozilla/Mutex.h"
+#include "WindowSurface.h"
+
+/*
+ * MozContainer
+ *
+ * This class serves three purposes in the nsIWidget implementation.
+ *
+ * - It provides objects to receive signals from GTK for events on native
+ * windows.
+ *
+ * - It provides GdkWindow to draw content on Wayland or when Gtk+ renders
+ * client side decorations to mShell.
+ */
+
+/* Workaround for bug at wayland-util.h,
+ * present in wayland-devel < 1.12
+ */
+struct wl_surface;
+struct wl_subsurface;
+
+struct MozContainerWayland {
+ struct wl_surface* surface = nullptr;
+ struct wl_subsurface* subsurface = nullptr;
+ int subsurface_dx = 0;
+ int subsurface_dy = 0;
+ struct wl_egl_window* eglwindow = nullptr;
+ struct wl_callback* frame_callback_handler = nullptr;
+ struct wp_viewport* viewport = nullptr;
+ struct wp_fractional_scale_v1* fractional_scale = nullptr;
+ gboolean opaque_region_needs_updates = false;
+ int opaque_region_corner_radius = 0;
+ gboolean opaque_region_used = false;
+ gboolean ready_to_draw = false;
+ gboolean commit_to_parent = false;
+ gboolean before_first_size_alloc = false;
+ gboolean waiting_to_show = false;
+ // Zero means no fractional scale set.
+ double current_fractional_scale = 0.0;
+ int buffer_scale = 1;
+ std::vector<std::function<void(void)>> initial_draw_cbs;
+ // mozcontainer is used from Compositor and Rendering threads so we need to
+ // control access to mozcontainer where wayland internals are used directly.
+ mozilla::Mutex container_lock{"MozContainerWayland::container_lock"};
+};
+
+struct _MozContainer;
+struct _MozContainerClass;
+typedef struct _MozContainer MozContainer;
+typedef struct _MozContainerClass MozContainerClass;
+
+class MozContainerSurfaceLock {
+ MozContainer* mContainer;
+ struct wl_surface* mSurface;
+
+ public:
+ explicit MozContainerSurfaceLock(MozContainer* aContainer);
+ ~MozContainerSurfaceLock();
+ struct wl_surface* GetSurface();
+};
+
+void moz_container_wayland_map(GtkWidget*);
+gboolean moz_container_wayland_map_event(GtkWidget*, GdkEventAny*);
+void moz_container_wayland_size_allocate(GtkWidget*, GtkAllocation*);
+void moz_container_wayland_unmap(GtkWidget*);
+
+struct wl_egl_window* moz_container_wayland_get_egl_window(
+ MozContainer* container, double scale);
+
+gboolean moz_container_wayland_has_egl_window(MozContainer* container);
+bool moz_container_wayland_egl_window_set_size(MozContainer* container,
+ nsIntSize aSize, int aScale);
+void moz_container_wayland_set_scale_factor_locked(
+ const mozilla::MutexAutoLock& aProofOfLock, MozContainer* container,
+ int aScale);
+bool moz_container_wayland_size_matches_scale_factor_locked(
+ const mozilla::MutexAutoLock& aProofOfLock, MozContainer* container,
+ int aWidth, int aHeight);
+
+void moz_container_wayland_add_initial_draw_callback_locked(
+ MozContainer* container, const std::function<void(void)>& initial_draw_cb);
+void moz_container_wayland_add_or_fire_initial_draw_callback(
+ MozContainer* container, const std::function<void(void)>& initial_draw_cb);
+void moz_container_wayland_clear_initial_draw_callback(MozContainer* container);
+
+wl_surface* moz_gtk_widget_get_wl_surface(GtkWidget* aWidget);
+void moz_container_wayland_update_opaque_region(MozContainer* container,
+ int corner_radius);
+gboolean moz_container_wayland_can_draw(MozContainer* container);
+double moz_container_wayland_get_scale(MozContainer* container);
+double moz_container_wayland_get_fractional_scale(MozContainer* container);
+void moz_container_wayland_set_commit_to_parent(MozContainer* container);
+bool moz_container_wayland_is_commiting_to_parent(MozContainer* container);
+bool moz_container_wayland_is_waiting_to_show(MozContainer* container);
+void moz_container_wayland_clear_waiting_to_show_flag(MozContainer* container);
+
+#endif /* __MOZ_CONTAINER_WAYLAND_H__ */
diff --git a/widget/gtk/NativeKeyBindings.cpp b/widget/gtk/NativeKeyBindings.cpp
new file mode 100644
index 0000000000..e050fd07c0
--- /dev/null
+++ b/widget/gtk/NativeKeyBindings.cpp
@@ -0,0 +1,527 @@
+/* -*- 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 "mozilla/ArrayUtils.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
+
+#include "NativeKeyBindings.h"
+#include "nsString.h"
+#include "nsGtkKeyUtils.h"
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdk/gdkkeysyms-compat.h>
+#include <gdk/gdk.h>
+
+namespace mozilla {
+namespace widget {
+
+static nsTArray<CommandInt>* gCurrentCommands = nullptr;
+static bool gHandled = false;
+
+inline void AddCommand(Command aCommand) {
+ MOZ_ASSERT(gCurrentCommands);
+ gCurrentCommands->AppendElement(static_cast<CommandInt>(aCommand));
+}
+
+// Common GtkEntry and GtkTextView signals
+static void copy_clipboard_cb(GtkWidget* w, gpointer user_data) {
+ AddCommand(Command::Copy);
+ g_signal_stop_emission_by_name(w, "copy_clipboard");
+ gHandled = true;
+}
+
+static void cut_clipboard_cb(GtkWidget* w, gpointer user_data) {
+ AddCommand(Command::Cut);
+ g_signal_stop_emission_by_name(w, "cut_clipboard");
+ gHandled = true;
+}
+
+// GTK distinguishes between display lines (wrapped, as they appear on the
+// screen) and paragraphs, which are runs of text terminated by a newline.
+// We don't have this distinction, so we always use editor's notion of
+// lines, which are newline-terminated.
+
+static const Command sDeleteCommands[][2] = {
+ // backward, forward
+ // CHARS
+ {Command::DeleteCharBackward, Command::DeleteCharForward},
+ // WORD_ENDS
+ {Command::DeleteWordBackward, Command::DeleteWordForward},
+ // WORDS
+ {Command::DeleteWordBackward, Command::DeleteWordForward},
+ // LINES
+ {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
+ // LINE_ENDS
+ {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
+ // PARAGRAPH_ENDS
+ {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
+ // PARAGRAPHS
+ {Command::DeleteToBeginningOfLine, Command::DeleteToEndOfLine},
+ // This deletes from the end of the previous word to the beginning of the
+ // next word, but only if the caret is not in a word.
+ // XXX need to implement in editor
+ {Command::DoNothing, Command::DoNothing} // WHITESPACE
+};
+
+static void delete_from_cursor_cb(GtkWidget* w, GtkDeleteType del_type,
+ gint count, gpointer user_data) {
+ g_signal_stop_emission_by_name(w, "delete_from_cursor");
+ if (count == 0) {
+ // Nothing to do.
+ return;
+ }
+
+ bool forward = count > 0;
+
+ // Ignore GTK's Ctrl-K keybinding introduced in GTK 3.14 and removed in
+ // 3.18 if the user has custom bindings set. See bug 1176929.
+ if (del_type == GTK_DELETE_PARAGRAPH_ENDS && forward && GTK_IS_ENTRY(w) &&
+ !gtk_check_version(3, 14, 1) && gtk_check_version(3, 17, 9)) {
+ GtkStyleContext* context = gtk_widget_get_style_context(w);
+ GtkStateFlags flags = gtk_widget_get_state_flags(w);
+
+ GPtrArray* array;
+ gtk_style_context_get(context, flags, "gtk-key-bindings", &array, nullptr);
+ if (!array) return;
+ g_ptr_array_unref(array);
+ }
+
+ gHandled = true;
+ if (uint32_t(del_type) >= ArrayLength(sDeleteCommands)) {
+ // unsupported deletion type
+ return;
+ }
+
+ if (del_type == GTK_DELETE_WORDS) {
+ // This works like word_ends, except we first move the caret to the
+ // beginning/end of the current word.
+ if (forward) {
+ AddCommand(Command::WordNext);
+ AddCommand(Command::WordPrevious);
+ } else {
+ AddCommand(Command::WordPrevious);
+ AddCommand(Command::WordNext);
+ }
+ } else if (del_type == GTK_DELETE_DISPLAY_LINES ||
+ del_type == GTK_DELETE_PARAGRAPHS) {
+ // This works like display_line_ends, except we first move the caret to the
+ // beginning/end of the current line.
+ if (forward) {
+ AddCommand(Command::BeginLine);
+ } else {
+ AddCommand(Command::EndLine);
+ }
+ }
+
+ Command command = sDeleteCommands[del_type][forward];
+ if (command == Command::DoNothing) {
+ return;
+ }
+
+ unsigned int absCount = Abs(count);
+ for (unsigned int i = 0; i < absCount; ++i) {
+ AddCommand(command);
+ }
+}
+
+static const Command sMoveCommands[][2][2] = {
+ // non-extend { backward, forward }, extend { backward, forward }
+ // GTK differentiates between logical position, which is prev/next,
+ // and visual position, which is always left/right.
+ // We should fix this to work the same way for RTL text input.
+ {// LOGICAL_POSITIONS
+ {Command::CharPrevious, Command::CharNext},
+ {Command::SelectCharPrevious, Command::SelectCharNext}},
+ {// VISUAL_POSITIONS
+ {Command::CharPrevious, Command::CharNext},
+ {Command::SelectCharPrevious, Command::SelectCharNext}},
+ {// WORDS
+ {Command::WordPrevious, Command::WordNext},
+ {Command::SelectWordPrevious, Command::SelectWordNext}},
+ {// DISPLAY_LINES
+ {Command::LinePrevious, Command::LineNext},
+ {Command::SelectLinePrevious, Command::SelectLineNext}},
+ {// DISPLAY_LINE_ENDS
+ {Command::BeginLine, Command::EndLine},
+ {Command::SelectBeginLine, Command::SelectEndLine}},
+ {// PARAGRAPHS
+ {Command::LinePrevious, Command::LineNext},
+ {Command::SelectLinePrevious, Command::SelectLineNext}},
+ {// PARAGRAPH_ENDS
+ {Command::BeginLine, Command::EndLine},
+ {Command::SelectBeginLine, Command::SelectEndLine}},
+ {// PAGES
+ {Command::MovePageUp, Command::MovePageDown},
+ {Command::SelectPageUp, Command::SelectPageDown}},
+ {// BUFFER_ENDS
+ {Command::MoveTop, Command::MoveBottom},
+ {Command::SelectTop, Command::SelectBottom}},
+ {// HORIZONTAL_PAGES (unsupported)
+ {Command::DoNothing, Command::DoNothing},
+ {Command::DoNothing, Command::DoNothing}}};
+
+static void move_cursor_cb(GtkWidget* w, GtkMovementStep step, gint count,
+ gboolean extend_selection, gpointer user_data) {
+ g_signal_stop_emission_by_name(w, "move_cursor");
+ if (count == 0) {
+ // Nothing to do.
+ return;
+ }
+
+ gHandled = true;
+ bool forward = count > 0;
+ if (uint32_t(step) >= ArrayLength(sMoveCommands)) {
+ // unsupported movement type
+ return;
+ }
+
+ Command command = sMoveCommands[step][extend_selection][forward];
+ if (command == Command::DoNothing) {
+ return;
+ }
+
+ unsigned int absCount = Abs(count);
+ for (unsigned int i = 0; i < absCount; ++i) {
+ AddCommand(command);
+ }
+}
+
+static void paste_clipboard_cb(GtkWidget* w, gpointer user_data) {
+ AddCommand(Command::Paste);
+ g_signal_stop_emission_by_name(w, "paste_clipboard");
+ gHandled = true;
+}
+
+// GtkTextView-only signals
+static void select_all_cb(GtkWidget* aWidget, gboolean aSelect,
+ gpointer aUserData) {
+ // We don't support "Unselect All" command.
+ // Note that if we'd support it, `Ctrl-Shift-a` will be mapped to it and
+ // overrides open `about:addons` shortcut.
+ if (aSelect) {
+ AddCommand(Command::SelectAll);
+ }
+ g_signal_stop_emission_by_name(aWidget, "select_all");
+ // Although we prevent the default of `GtkTExtView` with
+ // `g_signal_stop_emission_by_name`, but `gHandled` is used for asserting
+ // if it does not match with the emptiness of the command array.
+ // Therefore, we should not set it to `true` if we don't add a command.
+ gHandled |= aSelect;
+}
+
+NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr;
+NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr;
+
+// static
+NativeKeyBindings* NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) {
+ switch (aType) {
+ case NativeKeyBindingsType::SingleLineEditor:
+ if (!sInstanceForSingleLineEditor) {
+ sInstanceForSingleLineEditor = new NativeKeyBindings();
+ sInstanceForSingleLineEditor->Init(aType);
+ }
+ return sInstanceForSingleLineEditor;
+
+ default:
+ // fallback to multiline editor case in release build
+ MOZ_FALLTHROUGH_ASSERT("aType is invalid or not yet implemented");
+ case NativeKeyBindingsType::MultiLineEditor:
+ case NativeKeyBindingsType::RichTextEditor:
+ if (!sInstanceForMultiLineEditor) {
+ sInstanceForMultiLineEditor = new NativeKeyBindings();
+ sInstanceForMultiLineEditor->Init(aType);
+ }
+ return sInstanceForMultiLineEditor;
+ }
+}
+
+// static
+void NativeKeyBindings::Shutdown() {
+ delete sInstanceForSingleLineEditor;
+ sInstanceForSingleLineEditor = nullptr;
+ delete sInstanceForMultiLineEditor;
+ sInstanceForMultiLineEditor = nullptr;
+}
+
+void NativeKeyBindings::Init(NativeKeyBindingsType aType) {
+ switch (aType) {
+ case NativeKeyBindingsType::SingleLineEditor:
+ mNativeTarget = gtk_entry_new();
+ break;
+ default:
+ mNativeTarget = gtk_text_view_new();
+ g_signal_connect(mNativeTarget, "select_all", G_CALLBACK(select_all_cb),
+ this);
+ break;
+ }
+
+ g_object_ref_sink(mNativeTarget);
+
+ g_signal_connect(mNativeTarget, "copy_clipboard",
+ G_CALLBACK(copy_clipboard_cb), this);
+ g_signal_connect(mNativeTarget, "cut_clipboard", G_CALLBACK(cut_clipboard_cb),
+ this);
+ g_signal_connect(mNativeTarget, "delete_from_cursor",
+ G_CALLBACK(delete_from_cursor_cb), this);
+ g_signal_connect(mNativeTarget, "move_cursor", G_CALLBACK(move_cursor_cb),
+ this);
+ g_signal_connect(mNativeTarget, "paste_clipboard",
+ G_CALLBACK(paste_clipboard_cb), this);
+}
+
+NativeKeyBindings::~NativeKeyBindings() {
+ gtk_widget_destroy(mNativeTarget);
+ g_object_unref(mNativeTarget);
+}
+
+void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands) {
+ MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
+ MOZ_ASSERT(aCommands.IsEmpty());
+
+ // It must be a DOM event dispached by chrome script.
+ if (!aEvent.mNativeKeyEvent) {
+ return;
+ }
+
+ guint keyval;
+ if (aEvent.mCharCode) {
+ keyval = gdk_unicode_to_keyval(aEvent.mCharCode);
+ } else if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() &&
+ aWritingMode.ref().IsVertical()) {
+ // TODO: Use KeyNameIndex rather than legacy keyCode.
+ uint32_t remappedGeckoKeyCode =
+ aEvent.GetRemappedKeyCode(aWritingMode.ref());
+ switch (remappedGeckoKeyCode) {
+ case NS_VK_UP:
+ keyval = GDK_Up;
+ break;
+ case NS_VK_DOWN:
+ keyval = GDK_Down;
+ break;
+ case NS_VK_LEFT:
+ keyval = GDK_Left;
+ break;
+ case NS_VK_RIGHT:
+ keyval = GDK_Right;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
+ return;
+ }
+ } else {
+ keyval = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->keyval;
+ }
+
+ if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < aEvent.mAlternativeCharCodes.Length(); ++i) {
+ uint32_t ch = aEvent.IsShift()
+ ? aEvent.mAlternativeCharCodes[i].mShiftedCharCode
+ : aEvent.mAlternativeCharCodes[i].mUnshiftedCharCode;
+ if (ch && ch != aEvent.mCharCode) {
+ keyval = gdk_unicode_to_keyval(ch);
+ if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
+ return;
+ }
+ }
+ }
+
+ // If the key event does not cause any commands, and we're for single line
+ // editor, let's check whether the key combination is for "select-all" in
+ // GtkTextView because the signal is not supported by GtkEntry.
+ if (aCommands.IsEmpty() && this == sInstanceForSingleLineEditor &&
+ StaticPrefs::ui_key_use_select_all_in_single_line_editor()) {
+ if (NativeKeyBindings* bindingsForMultilineEditor =
+ GetInstance(NativeKeyBindingsType::MultiLineEditor)) {
+ bindingsForMultilineEditor->GetEditCommands(aEvent, aWritingMode,
+ aCommands);
+ if (aCommands.Length() == 1u &&
+ aCommands[0u] == static_cast<CommandInt>(Command::SelectAll)) {
+ return;
+ }
+ aCommands.Clear();
+ }
+ }
+
+ /*
+ gtk_bindings_activate_event is preferable, but it has unresolved bug:
+ http://bugzilla.gnome.org/show_bug.cgi?id=162726
+ The bug was already marked as FIXED. However, somebody reports that the
+ bug still exists.
+ Also gtk_bindings_activate may work with some non-shortcuts operations
+ (todo: check it). See bug 411005 and bug 406407.
+
+ Code, which should be used after fixing GNOME bug 162726:
+
+ gtk_bindings_activate_event(GTK_OBJECT(mNativeTarget),
+ static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent));
+ */
+}
+
+bool NativeKeyBindings::GetEditCommandsInternal(
+ const WidgetKeyboardEvent& aEvent, nsTArray<CommandInt>& aCommands,
+ guint aKeyval) {
+ guint modifiers = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->state;
+
+ gCurrentCommands = &aCommands;
+
+ gHandled = false;
+ gtk_bindings_activate(G_OBJECT(mNativeTarget), aKeyval,
+ GdkModifierType(modifiers));
+
+ gCurrentCommands = nullptr;
+
+ return gHandled;
+}
+
+// static
+void NativeKeyBindings::GetEditCommandsForTests(
+ NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
+ MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted());
+
+ if (aEvent.IsAlt() || aEvent.IsMeta()) {
+ return;
+ }
+
+ static const size_t kBackward = 0;
+ static const size_t kForward = 1;
+ const size_t extentSelection = aEvent.IsShift() ? 1 : 0;
+ // https://github.com/GNOME/gtk/blob/1f141c19533f4b3f397c3959ade673ce243b6138/gtk/gtktext.c#L1289
+ // https://github.com/GNOME/gtk/blob/c5dd34344f0c660ceffffb3bf9da43c263db16e1/gtk/gtktextview.c#L1534
+ Command command = Command::DoNothing;
+ const KeyNameIndex remappedKeyNameIndex =
+ aWritingMode.isSome() ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref())
+ : aEvent.mKeyNameIndex;
+ switch (remappedKeyNameIndex) {
+ case KEY_NAME_INDEX_USE_STRING:
+ switch (aEvent.PseudoCharCode()) {
+ case 'a':
+ case 'A':
+ if (aEvent.IsControl()) {
+ command = Command::SelectAll;
+ }
+ break;
+ case 'c':
+ case 'C':
+ if (aEvent.IsControl() && !aEvent.IsShift()) {
+ command = Command::Copy;
+ }
+ break;
+ case 'u':
+ case 'U':
+ if (aType == NativeKeyBindingsType::SingleLineEditor &&
+ aEvent.IsControl() && !aEvent.IsShift()) {
+ command = sDeleteCommands[GTK_DELETE_PARAGRAPH_ENDS][kBackward];
+ }
+ break;
+ case 'v':
+ case 'V':
+ if (aEvent.IsControl() && !aEvent.IsShift()) {
+ command = Command::Paste;
+ }
+ break;
+ case 'x':
+ case 'X':
+ if (aEvent.IsControl() && !aEvent.IsShift()) {
+ command = Command::Cut;
+ }
+ break;
+ case '/':
+ if (aEvent.IsControl() && !aEvent.IsShift()) {
+ command = Command::SelectAll;
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ case KEY_NAME_INDEX_Insert:
+ if (aEvent.IsControl() && !aEvent.IsShift()) {
+ command = Command::Copy;
+ } else if (aEvent.IsShift() && !aEvent.IsControl()) {
+ command = Command::Paste;
+ }
+ break;
+ case KEY_NAME_INDEX_Delete:
+ if (aEvent.IsShift()) {
+ command = Command::Cut;
+ break;
+ }
+ [[fallthrough]];
+ case KEY_NAME_INDEX_Backspace: {
+ const size_t direction =
+ remappedKeyNameIndex == KEY_NAME_INDEX_Delete ? kForward : kBackward;
+ const GtkDeleteType amount =
+ aEvent.IsControl() && aEvent.IsShift()
+ ? GTK_DELETE_PARAGRAPH_ENDS
+ // FYI: Shift key for Backspace is ignored to help mis-typing.
+ : (aEvent.IsControl() ? GTK_DELETE_WORD_ENDS : GTK_DELETE_CHARS);
+ command = sDeleteCommands[amount][direction];
+ break;
+ }
+ case KEY_NAME_INDEX_ArrowLeft:
+ case KEY_NAME_INDEX_ArrowRight: {
+ const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowRight
+ ? kForward
+ : kBackward;
+ const GtkMovementStep amount = aEvent.IsControl()
+ ? GTK_MOVEMENT_WORDS
+ : GTK_MOVEMENT_VISUAL_POSITIONS;
+ command = sMoveCommands[amount][extentSelection][direction];
+ break;
+ }
+ case KEY_NAME_INDEX_ArrowUp:
+ case KEY_NAME_INDEX_ArrowDown: {
+ const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowDown
+ ? kForward
+ : kBackward;
+ const GtkMovementStep amount = aEvent.IsControl()
+ ? GTK_MOVEMENT_PARAGRAPHS
+ : GTK_MOVEMENT_DISPLAY_LINES;
+ command = sMoveCommands[amount][extentSelection][direction];
+ break;
+ }
+ case KEY_NAME_INDEX_Home:
+ case KEY_NAME_INDEX_End: {
+ const size_t direction =
+ remappedKeyNameIndex == KEY_NAME_INDEX_End ? kForward : kBackward;
+ const GtkMovementStep amount = aEvent.IsControl()
+ ? GTK_MOVEMENT_BUFFER_ENDS
+ : GTK_MOVEMENT_DISPLAY_LINE_ENDS;
+ command = sMoveCommands[amount][extentSelection][direction];
+ break;
+ }
+ case KEY_NAME_INDEX_PageUp:
+ case KEY_NAME_INDEX_PageDown: {
+ const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_PageDown
+ ? kForward
+ : kBackward;
+ const GtkMovementStep amount = aEvent.IsControl()
+ ? GTK_MOVEMENT_HORIZONTAL_PAGES
+ : GTK_MOVEMENT_PAGES;
+ command = sMoveCommands[amount][extentSelection][direction];
+ break;
+ }
+ default:
+ break;
+ }
+ if (command != Command::DoNothing) {
+ aCommands.AppendElement(static_cast<CommandInt>(command));
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/NativeKeyBindings.h b/widget/gtk/NativeKeyBindings.h
new file mode 100644
index 0000000000..1d0c528621
--- /dev/null
+++ b/widget/gtk/NativeKeyBindings.h
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+
+#ifndef NativeKeyBindings_h
+#define NativeKeyBindings_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "nsIWidget.h"
+
+#include <glib.h> // for guint
+
+using GtkWidget = struct _GtkWidget;
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
+namespace widget {
+
+class NativeKeyBindings final {
+ public:
+ static NativeKeyBindings* GetInstance(NativeKeyBindingsType aType);
+ static void Shutdown();
+
+ /**
+ * GetEditCommandsForTests() returns commands performed in native widget
+ * in typical environment. I.e., this does NOT refer customized shortcut
+ * key mappings of the environment.
+ */
+ static void GetEditCommandsForTests(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+
+ void Init(NativeKeyBindingsType aType);
+
+ void GetEditCommands(const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+
+ private:
+ ~NativeKeyBindings();
+
+ bool GetEditCommandsInternal(const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands, guint aKeyval);
+
+ GtkWidget* mNativeTarget;
+
+ static NativeKeyBindings* sInstanceForSingleLineEditor;
+ static NativeKeyBindings* sInstanceForMultiLineEditor;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // NativeKeyBindings_h
diff --git a/widget/gtk/NativeMenuGtk.cpp b/widget/gtk/NativeMenuGtk.cpp
new file mode 100644
index 0000000000..9d413d475e
--- /dev/null
+++ b/widget/gtk/NativeMenuGtk.cpp
@@ -0,0 +1,424 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 "NativeMenuGtk.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/XULCommandEvent.h"
+#include "mozilla/WidgetUtilsGtk.h"
+#include "mozilla/EventDispatcher.h"
+#include "nsPresContext.h"
+#include "nsIWidget.h"
+#include "nsWindow.h"
+#include "nsStubMutationObserver.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+#include <dlfcn.h>
+#include <gtk/gtk.h>
+
+namespace mozilla::widget {
+
+using GtkMenuPopupAtRect = void (*)(GtkMenu* menu, GdkWindow* rect_window,
+ const GdkRectangle* rect,
+ GdkGravity rect_anchor,
+ GdkGravity menu_anchor,
+ const GdkEvent* trigger_event);
+
+static bool IsDisabled(const dom::Element& aElement) {
+ return aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters) ||
+ aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters);
+}
+static bool NodeIsRelevant(const nsINode& aNode) {
+ return aNode.IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuseparator,
+ nsGkAtoms::menuitem, nsGkAtoms::menugroup);
+}
+
+// If this is a radio / checkbox menuitem, get the current value.
+static Maybe<bool> GetChecked(const dom::Element& aMenuItem) {
+ static dom::Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox,
+ nsGkAtoms::radio, nullptr};
+ switch (aMenuItem.FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, strings,
+ eCaseMatters)) {
+ case 0:
+ break;
+ case 1:
+ break;
+ default:
+ return Nothing();
+ }
+
+ return Some(aMenuItem.AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked,
+ nsGkAtoms::_true, eCaseMatters));
+}
+
+struct Actions {
+ RefPtr<GSimpleActionGroup> mGroup;
+ size_t mNextActionIndex = 0;
+
+ nsPrintfCString Register(const dom::Element&, bool aForSubmenu);
+ void Clear();
+};
+
+static MOZ_CAN_RUN_SCRIPT void ActivateItem(dom::Element& aElement) {
+ if (Maybe<bool> checked = GetChecked(aElement)) {
+ if (!aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
+ nsGkAtoms::_false, eCaseMatters)) {
+ bool newValue = !*checked;
+ if (newValue) {
+ aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns,
+ true);
+ } else {
+ aElement.UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
+ }
+ }
+ }
+
+ RefPtr doc = aElement.OwnerDoc();
+ RefPtr event = new dom::XULCommandEvent(doc, doc->GetPresContext(), nullptr);
+ IgnoredErrorResult rv;
+ event->InitCommandEvent(u"command"_ns, true, true,
+ nsGlobalWindowInner::Cast(doc->GetInnerWindow()), 0,
+ /* ctrlKey = */ false, /* altKey = */ false,
+ /* shiftKey = */ false, /* cmdKey = */ false,
+ /* button = */ MouseButton::ePrimary, nullptr, 0, rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return;
+ }
+ aElement.DispatchEvent(*event);
+}
+
+static MOZ_CAN_RUN_SCRIPT void ActivateSignal(GSimpleAction* aAction,
+ GVariant* aParam,
+ gpointer aUserData) {
+ RefPtr element = static_cast<dom::Element*>(aUserData);
+ ActivateItem(*element);
+}
+
+static MOZ_CAN_RUN_SCRIPT void FireEvent(dom::Element* aTarget,
+ EventMessage aPopupMessage) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, aPopupMessage, nullptr, WidgetMouseEvent::eReal);
+ EventDispatcher::Dispatch(aTarget, nullptr, &event, nullptr, &status);
+}
+
+static MOZ_CAN_RUN_SCRIPT void ChangeStateSignal(GSimpleAction* aAction,
+ GVariant* aParam,
+ gpointer aUserData) {
+ // TODO: Fire events when safe. These run at a bad time for now.
+ static constexpr bool kEnabled = false;
+ if (!kEnabled) {
+ return;
+ }
+ const bool open = g_variant_get_boolean(aParam);
+ RefPtr popup = static_cast<dom::Element*>(aUserData);
+ if (open) {
+ FireEvent(popup, eXULPopupShowing);
+ FireEvent(popup, eXULPopupShown);
+ } else {
+ FireEvent(popup, eXULPopupHiding);
+ FireEvent(popup, eXULPopupHidden);
+ }
+}
+
+nsPrintfCString Actions::Register(const dom::Element& aMenuItem,
+ bool aForSubmenu) {
+ nsPrintfCString actionName("item-%zu", mNextActionIndex++);
+ Maybe<bool> paramValue = aForSubmenu ? Some(false) : GetChecked(aMenuItem);
+ RefPtr<GSimpleAction> action;
+ if (paramValue) {
+ action = dont_AddRef(g_simple_action_new_stateful(
+ actionName.get(), nullptr, g_variant_new_boolean(*paramValue)));
+ } else {
+ action = dont_AddRef(g_simple_action_new(actionName.get(), nullptr));
+ }
+ if (aForSubmenu) {
+ g_signal_connect(action, "change-state", G_CALLBACK(ChangeStateSignal),
+ gpointer(&aMenuItem));
+ } else {
+ g_signal_connect(action, "activate", G_CALLBACK(ActivateSignal),
+ gpointer(&aMenuItem));
+ }
+ g_action_map_add_action(G_ACTION_MAP(mGroup.get()), G_ACTION(action.get()));
+ return actionName;
+}
+
+void Actions::Clear() {
+ for (size_t i = 0; i < mNextActionIndex; ++i) {
+ g_action_map_remove_action(G_ACTION_MAP(mGroup.get()),
+ nsPrintfCString("item-%zu", i).get());
+ }
+ mNextActionIndex = 0;
+}
+
+class MenuModel final : public nsStubMutationObserver {
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+
+ public:
+ explicit MenuModel(dom::Element* aElement) : mElement(aElement) {
+ mElement->AddMutationObserver(this);
+ mGMenu = dont_AddRef(g_menu_new());
+ mActions.mGroup = dont_AddRef(g_simple_action_group_new());
+ }
+
+ GMenuModel* GetModel() { return G_MENU_MODEL(mGMenu.get()); }
+ GActionGroup* GetActionGroup() {
+ return G_ACTION_GROUP(mActions.mGroup.get());
+ }
+
+ dom::Element* Element() { return mElement; }
+
+ void RecomputeModelIfNeeded();
+
+ bool IsShowing() { return mPoppedUp; }
+ void WillShow() {
+ mPoppedUp = true;
+ RecomputeModelIfNeeded();
+ }
+ void DidHide() { mPoppedUp = false; }
+
+ private:
+ virtual ~MenuModel() { mElement->RemoveMutationObserver(this); }
+
+ void DirtyModel() {
+ mDirty = true;
+ if (mPoppedUp) {
+ RecomputeModelIfNeeded();
+ }
+ }
+
+ RefPtr<dom::Element> mElement;
+ RefPtr<GMenu> mGMenu;
+ Actions mActions;
+ bool mDirty = true;
+ bool mPoppedUp = false;
+};
+
+NS_IMPL_ISUPPORTS(MenuModel, nsIMutationObserver)
+
+void MenuModel::ContentRemoved(nsIContent* aChild, nsIContent*) {
+ if (NodeIsRelevant(*aChild)) {
+ DirtyModel();
+ }
+}
+
+void MenuModel::ContentInserted(nsIContent* aChild) {
+ if (NodeIsRelevant(*aChild)) {
+ DirtyModel();
+ }
+}
+
+void MenuModel::ContentAppended(nsIContent* aChild) {
+ if (NodeIsRelevant(*aChild)) {
+ DirtyModel();
+ }
+}
+
+void MenuModel::AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ if (NodeIsRelevant(*aElement) &&
+ (aAttribute == nsGkAtoms::label || aAttribute == nsGkAtoms::aria_label ||
+ aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::hidden)) {
+ DirtyModel();
+ }
+}
+
+static const dom::Element* GetMenuPopupChild(const dom::Element& aElement) {
+ for (const nsIContent* child = aElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsXULElement(nsGkAtoms::menupopup)) {
+ return child->AsElement();
+ }
+ }
+ return nullptr;
+}
+
+static void RecomputeModelFor(GMenu* aMenu, Actions& aActions,
+ const dom::Element& aElement) {
+ RefPtr<GMenu> sectionMenu;
+ auto FlushSectionMenu = [&] {
+ if (sectionMenu) {
+ g_menu_append_section(aMenu, nullptr, G_MENU_MODEL(sectionMenu.get()));
+ sectionMenu = nullptr;
+ }
+ };
+
+ for (const nsIContent* child = aElement.GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsXULElement(nsGkAtoms::menuitem) &&
+ !IsDisabled(*child->AsElement())) {
+ nsAutoString label;
+ child->AsElement()->GetAttr(nsGkAtoms::label, label);
+ if (label.IsEmpty()) {
+ child->AsElement()->GetAttr(nsGkAtoms::aria_label, label);
+ }
+ nsPrintfCString actionName(
+ "menu.%s",
+ aActions.Register(*child->AsElement(), /* aForSubmenu = */ false)
+ .get());
+ g_menu_append(sectionMenu ? sectionMenu.get() : aMenu,
+ NS_ConvertUTF16toUTF8(label).get(), actionName.get());
+ continue;
+ }
+ if (child->IsXULElement(nsGkAtoms::menuseparator)) {
+ FlushSectionMenu();
+ sectionMenu = dont_AddRef(g_menu_new());
+ continue;
+ }
+ if (child->IsXULElement(nsGkAtoms::menugroup)) {
+ FlushSectionMenu();
+ sectionMenu = dont_AddRef(g_menu_new());
+ RecomputeModelFor(sectionMenu, aActions, *child->AsElement());
+ FlushSectionMenu();
+ continue;
+ }
+ if (child->IsXULElement(nsGkAtoms::menu) &&
+ !IsDisabled(*child->AsElement())) {
+ if (const auto* popup = GetMenuPopupChild(*child->AsElement())) {
+ RefPtr<GMenu> submenu = dont_AddRef(g_menu_new());
+ RecomputeModelFor(submenu, aActions, *popup);
+ nsAutoString label;
+ child->AsElement()->GetAttr(nsGkAtoms::label, label);
+ RefPtr<GMenuItem> submenuItem = dont_AddRef(g_menu_item_new_submenu(
+ NS_ConvertUTF16toUTF8(label).get(), G_MENU_MODEL(submenu.get())));
+ nsPrintfCString actionName(
+ "menu.%s",
+ aActions.Register(*popup, /* aForSubmenu = */ true).get());
+ g_menu_item_set_attribute_value(submenuItem.get(), "submenu-action",
+ g_variant_new_string(actionName.get()));
+ g_menu_append_item(sectionMenu ? sectionMenu.get() : aMenu,
+ submenuItem.get());
+ }
+ }
+ }
+
+ FlushSectionMenu();
+}
+
+void MenuModel::RecomputeModelIfNeeded() {
+ if (!mDirty) {
+ return;
+ }
+ mActions.Clear();
+ g_menu_remove_all(mGMenu.get());
+ RecomputeModelFor(mGMenu.get(), mActions, *mElement);
+}
+
+static GtkMenuPopupAtRect GetPopupAtRectFn() {
+ static GtkMenuPopupAtRect sFunc =
+ (GtkMenuPopupAtRect)dlsym(RTLD_DEFAULT, "gtk_menu_popup_at_rect");
+ return sFunc;
+}
+
+bool NativeMenuGtk::CanUse() {
+ return StaticPrefs::widget_gtk_native_context_menus() && GetPopupAtRectFn();
+}
+
+void NativeMenuGtk::FireEvent(EventMessage aPopupMessage) {
+ RefPtr target = Element();
+ widget::FireEvent(target, aPopupMessage);
+}
+
+#define METHOD_SIGNAL(name_) \
+ static MOZ_CAN_RUN_SCRIPT_BOUNDARY void On##name_##Signal( \
+ GtkWidget* widget, gpointer user_data) { \
+ RefPtr menu = static_cast<NativeMenuGtk*>(user_data); \
+ return menu->On##name_(); \
+ }
+
+METHOD_SIGNAL(Unmap);
+
+#undef METHOD_SIGNAL
+
+NativeMenuGtk::NativeMenuGtk(dom::Element* aElement)
+ : mMenuModel(MakeRefPtr<MenuModel>(aElement)) {
+ // Floating, so no need to dont_AddRef.
+ mNativeMenu = gtk_menu_new_from_model(mMenuModel->GetModel());
+ gtk_widget_insert_action_group(mNativeMenu.get(), "menu",
+ mMenuModel->GetActionGroup());
+ g_signal_connect(mNativeMenu, "unmap", G_CALLBACK(OnUnmapSignal), this);
+}
+
+NativeMenuGtk::~NativeMenuGtk() {
+ g_signal_handlers_disconnect_by_data(mNativeMenu, this);
+}
+
+RefPtr<dom::Element> NativeMenuGtk::Element() { return mMenuModel->Element(); }
+
+void NativeMenuGtk::ShowAsContextMenu(nsIFrame* aClickedFrame,
+ const CSSIntPoint& aPosition,
+ bool aIsContextMenu) {
+ if (mMenuModel->IsShowing()) {
+ return;
+ }
+ RefPtr<nsIWidget> widget = aClickedFrame->PresContext()->GetRootWidget();
+ if (NS_WARN_IF(!widget)) {
+ // XXX Do we need to close menus here?
+ return;
+ }
+ auto* win = static_cast<GdkWindow*>(widget->GetNativeData(NS_NATIVE_WINDOW));
+ if (NS_WARN_IF(!win)) {
+ return;
+ }
+
+ auto* geckoWin = static_cast<nsWindow*>(widget.get());
+ // The position needs to be relative to our window.
+ auto pos = (aPosition * aClickedFrame->PresContext()->CSSToDevPixelScale()) -
+ geckoWin->WidgetToScreenOffset();
+ auto gdkPos = geckoWin->DevicePixelsToGdkPointRoundDown(
+ LayoutDeviceIntPoint::Round(pos));
+
+ mMenuModel->WillShow();
+ const GdkRectangle rect = {gdkPos.x, gdkPos.y, 1, 1};
+ auto openFn = GetPopupAtRectFn();
+ openFn(GTK_MENU(mNativeMenu.get()), win, &rect, GDK_GRAVITY_NORTH_WEST,
+ GDK_GRAVITY_NORTH_WEST, GetLastMousePressEvent());
+
+ RefPtr pin{this};
+ FireEvent(eXULPopupShown);
+}
+
+bool NativeMenuGtk::Close() {
+ if (!mMenuModel->IsShowing()) {
+ return false;
+ }
+ gtk_menu_popdown(GTK_MENU(mNativeMenu.get()));
+ return true;
+}
+
+void NativeMenuGtk::OnUnmap() {
+ FireEvent(eXULPopupHiding);
+
+ mMenuModel->DidHide();
+
+ FireEvent(eXULPopupHidden);
+
+ for (NativeMenu::Observer* observer : mObservers.Clone()) {
+ observer->OnNativeMenuClosed();
+ }
+}
+
+void NativeMenuGtk::ActivateItem(dom::Element* aItemElement, Modifiers,
+ int16_t aButton, ErrorResult&) {
+ // TODO: For testing only.
+}
+
+void NativeMenuGtk::OpenSubmenu(dom::Element*) {
+ // TODO: For testing mostly.
+}
+
+void NativeMenuGtk::CloseSubmenu(dom::Element*) {
+ // TODO: For testing mostly.
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/NativeMenuGtk.h b/widget/gtk/NativeMenuGtk.h
new file mode 100644
index 0000000000..3f1f3213c1
--- /dev/null
+++ b/widget/gtk/NativeMenuGtk.h
@@ -0,0 +1,64 @@
+
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef mozilla_widget_NativeMenuGtk_h
+#define mozilla_widget_NativeMenuGtk_h
+
+#include "mozilla/widget/NativeMenu.h"
+#include "mozilla/EventForwards.h"
+#include "GRefPtr.h"
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+}
+
+namespace widget {
+
+class MenuModel;
+
+class NativeMenuGtk : public NativeMenu {
+ public:
+ // Whether we can use native menu popups on GTK.
+ static bool CanUse();
+
+ explicit NativeMenuGtk(dom::Element* aElement);
+
+ // NativeMenu
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void ShowAsContextMenu(
+ nsIFrame* aClickedFrame, const CSSIntPoint& aPosition,
+ bool aIsContextMenu) override;
+ bool Close() override;
+ void ActivateItem(dom::Element* aItemElement, Modifiers aModifiers,
+ int16_t aButton, ErrorResult& aRv) override;
+ void OpenSubmenu(dom::Element* aMenuElement) override;
+ void CloseSubmenu(dom::Element* aMenuElement) override;
+ RefPtr<dom::Element> Element() override;
+ void AddObserver(NativeMenu::Observer* aObserver) override {
+ mObservers.AppendElement(aObserver);
+ }
+ void RemoveObserver(NativeMenu::Observer* aObserver) override {
+ mObservers.RemoveElement(aObserver);
+ }
+
+ MOZ_CAN_RUN_SCRIPT void OnUnmap();
+
+ protected:
+ virtual ~NativeMenuGtk();
+
+ MOZ_CAN_RUN_SCRIPT void FireEvent(EventMessage);
+
+ bool mPoppedUp = false;
+ RefPtr<GtkWidget> mNativeMenu;
+ RefPtr<MenuModel> mMenuModel;
+ nsTArray<NativeMenu::Observer*> mObservers;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/gtk/NativeMenuSupport.cpp b/widget/gtk/NativeMenuSupport.cpp
new file mode 100644
index 0000000000..4360867fff
--- /dev/null
+++ b/widget/gtk/NativeMenuSupport.cpp
@@ -0,0 +1,29 @@
+/* -*- 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 "mozilla/widget/NativeMenuSupport.h"
+
+#include "MainThreadUtils.h"
+#include "NativeMenuGtk.h"
+
+namespace mozilla::widget {
+
+void NativeMenuSupport::CreateNativeMenuBar(nsIWidget* aParent,
+ dom::Element* aMenuBarElement) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread(),
+ "Attempting to create native menu bar on wrong thread!");
+ // TODO
+}
+
+already_AddRefed<NativeMenu> NativeMenuSupport::CreateNativeContextMenu(
+ dom::Element* aPopup) {
+ return MakeAndAddRef<NativeMenuGtk>(aPopup);
+}
+
+bool NativeMenuSupport::ShouldUseNativeContextMenus() {
+ return NativeMenuGtk::CanUse();
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/PCompositorWidget.ipdl b/widget/gtk/PCompositorWidget.ipdl
new file mode 100644
index 0000000000..d554a33144
--- /dev/null
+++ b/widget/gtk/PCompositorWidget.ipdl
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 protocol PCompositorBridge;
+
+include "mozilla/GfxMessageUtils.h";
+
+using mozilla::LayoutDeviceIntSize from "Units.h";
+
+namespace mozilla {
+namespace widget {
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PCompositorWidget
+{
+ manager PCompositorBridge;
+
+parent:
+ async __delete__();
+
+ async NotifyClientSizeChanged(LayoutDeviceIntSize aClientSize);
+ async DisableRendering();
+ async EnableRendering(uintptr_t aXWindow, bool aShaped);
+
+child:
+
+ async ObserveVsync();
+ async UnobserveVsync();
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/PlatformWidgetTypes.ipdlh b/widget/gtk/PlatformWidgetTypes.ipdlh
new file mode 100644
index 0000000000..c85da1711e
--- /dev/null
+++ b/widget/gtk/PlatformWidgetTypes.ipdlh
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 HeadlessWidgetTypes;
+
+include "mozilla/GfxMessageUtils.h";
+
+using mozilla::LayoutDeviceIntSize from "Units.h";
+
+namespace mozilla {
+namespace widget {
+
+struct GtkCompositorWidgetInitData
+{
+ uintptr_t XWindow;
+ nsCString XDisplayString;
+ bool Shaped;
+ bool IsX11Display;
+
+ LayoutDeviceIntSize InitialClientSize;
+};
+
+union CompositorWidgetInitData
+{
+ GtkCompositorWidgetInitData;
+ HeadlessCompositorWidgetInitData;
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/ScreenHelperGTK.cpp b/widget/gtk/ScreenHelperGTK.cpp
new file mode 100644
index 0000000000..60be234c60
--- /dev/null
+++ b/widget/gtk/ScreenHelperGTK.cpp
@@ -0,0 +1,306 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ScreenHelperGTK.h"
+
+#ifdef MOZ_X11
+# include <gdk/gdkx.h>
+# include <X11/Xlib.h>
+# include "X11UndefineNone.h"
+#endif /* MOZ_X11 */
+#ifdef MOZ_WAYLAND
+# include <gdk/gdkwayland.h>
+#endif /* MOZ_WAYLAND */
+#include <dlfcn.h>
+#include <gtk/gtk.h>
+
+#include "gfxPlatformGtk.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/WidgetUtilsGtk.h"
+#include "nsGtkUtils.h"
+#include "nsTArray.h"
+#include "nsWindow.h"
+
+struct wl_registry;
+
+#ifdef MOZ_WAYLAND
+# include "nsWaylandDisplay.h"
+#endif
+
+namespace mozilla::widget {
+
+#ifdef MOZ_LOGGING
+static LazyLogModule sScreenLog("WidgetScreen");
+# define LOG_SCREEN(...) MOZ_LOG(sScreenLog, LogLevel::Debug, (__VA_ARGS__))
+#else
+# define LOG_SCREEN(...)
+#endif /* MOZ_LOGGING */
+
+using GdkMonitor = struct _GdkMonitor;
+
+class ScreenGetterGtk final {
+ public:
+ ScreenGetterGtk() = default;
+ ~ScreenGetterGtk();
+
+ void Init();
+
+#ifdef MOZ_X11
+ Atom NetWorkareaAtom() { return mNetWorkareaAtom; }
+#endif
+
+ // For internal use from signal callback functions
+ void RefreshScreens();
+
+ private:
+ GdkWindow* mRootWindow = nullptr;
+#ifdef MOZ_X11
+ Atom mNetWorkareaAtom = 0;
+#endif
+};
+
+static GdkMonitor* GdkDisplayGetMonitor(GdkDisplay* aDisplay, int aMonitorNum) {
+ static auto s_gdk_display_get_monitor = (GdkMonitor * (*)(GdkDisplay*, int))
+ dlsym(RTLD_DEFAULT, "gdk_display_get_monitor");
+ if (!s_gdk_display_get_monitor) {
+ return nullptr;
+ }
+ return s_gdk_display_get_monitor(aDisplay, aMonitorNum);
+}
+
+RefPtr<Screen> ScreenHelperGTK::GetScreenForWindow(nsWindow* aWindow) {
+ LOG_SCREEN("GetScreenForWindow() [%p]", aWindow);
+
+ static auto s_gdk_display_get_monitor_at_window =
+ (GdkMonitor * (*)(GdkDisplay*, GdkWindow*))
+ dlsym(RTLD_DEFAULT, "gdk_display_get_monitor_at_window");
+
+ if (!s_gdk_display_get_monitor_at_window) {
+ LOG_SCREEN(" failed, missing Gtk helpers");
+ return nullptr;
+ }
+
+ GdkWindow* gdkWindow = aWindow->GetToplevelGdkWindow();
+ if (!gdkWindow) {
+ LOG_SCREEN(" failed, can't get GdkWindow");
+ return nullptr;
+ }
+
+ GdkDisplay* display = gdk_display_get_default();
+ GdkMonitor* monitor = s_gdk_display_get_monitor_at_window(display, gdkWindow);
+ if (!monitor) {
+ LOG_SCREEN(" failed, can't get monitor for GdkWindow");
+ return nullptr;
+ }
+
+ int index = -1;
+ while (GdkMonitor* m = GdkDisplayGetMonitor(display, ++index)) {
+ if (m == monitor) {
+ return ScreenManager::GetSingleton().CurrentScreenList().SafeElementAt(
+ index);
+ }
+ }
+
+ LOG_SCREEN(" Couldn't find monitor %p", monitor);
+ return nullptr;
+}
+
+static StaticAutoPtr<ScreenGetterGtk> gScreenGetter;
+
+static void monitors_changed(GdkScreen* aScreen, gpointer aClosure) {
+ LOG_SCREEN("Received monitors-changed event");
+ auto* self = static_cast<ScreenGetterGtk*>(aClosure);
+ self->RefreshScreens();
+}
+
+static void screen_resolution_changed(GdkScreen* aScreen, GParamSpec* aPspec,
+ ScreenGetterGtk* self) {
+ self->RefreshScreens();
+}
+
+static GdkFilterReturn root_window_event_filter(GdkXEvent* aGdkXEvent,
+ GdkEvent* aGdkEvent,
+ gpointer aClosure) {
+#ifdef MOZ_X11
+ ScreenGetterGtk* self = static_cast<ScreenGetterGtk*>(aClosure);
+ XEvent* xevent = static_cast<XEvent*>(aGdkXEvent);
+
+ switch (xevent->type) {
+ case PropertyNotify: {
+ XPropertyEvent* propertyEvent = &xevent->xproperty;
+ if (propertyEvent->atom == self->NetWorkareaAtom()) {
+ LOG_SCREEN("Work area size changed");
+ self->RefreshScreens();
+ }
+ } break;
+ default:
+ break;
+ }
+#endif
+
+ return GDK_FILTER_CONTINUE;
+}
+
+void ScreenGetterGtk::Init() {
+ LOG_SCREEN("ScreenGetterGtk created");
+ GdkScreen* defaultScreen = gdk_screen_get_default();
+ if (!defaultScreen) {
+ // Sometimes we don't initial X (e.g., xpcshell)
+ MOZ_LOG(sScreenLog, LogLevel::Debug,
+ ("defaultScreen is nullptr, running headless"));
+ return;
+ }
+ mRootWindow = gdk_get_default_root_window();
+ MOZ_ASSERT(mRootWindow);
+
+ g_object_ref(mRootWindow);
+
+ // GDK_PROPERTY_CHANGE_MASK ==> PropertyChangeMask, for PropertyNotify
+ gdk_window_set_events(mRootWindow,
+ GdkEventMask(gdk_window_get_events(mRootWindow) |
+ GDK_PROPERTY_CHANGE_MASK));
+
+ g_signal_connect(defaultScreen, "monitors-changed",
+ G_CALLBACK(monitors_changed), this);
+ // Use _after to ensure this callback is run after gfxPlatformGtk.cpp's
+ // handler.
+ g_signal_connect_after(defaultScreen, "notify::resolution",
+ G_CALLBACK(screen_resolution_changed), this);
+#ifdef MOZ_X11
+ gdk_window_add_filter(mRootWindow, root_window_event_filter, this);
+ if (GdkIsX11Display()) {
+ mNetWorkareaAtom = XInternAtom(GDK_WINDOW_XDISPLAY(mRootWindow),
+ "_NET_WORKAREA", X11False);
+ }
+#endif
+ RefreshScreens();
+}
+
+ScreenGetterGtk::~ScreenGetterGtk() {
+ if (mRootWindow) {
+ g_signal_handlers_disconnect_by_data(gdk_screen_get_default(), this);
+
+ gdk_window_remove_filter(mRootWindow, root_window_event_filter, this);
+ g_object_unref(mRootWindow);
+ mRootWindow = nullptr;
+ }
+}
+
+static uint32_t GetGTKPixelDepth() {
+ GdkVisual* visual = gdk_screen_get_system_visual(gdk_screen_get_default());
+ return gdk_visual_get_depth(visual);
+}
+
+static already_AddRefed<Screen> MakeScreenGtk(GdkScreen* aScreen,
+ gint aMonitorNum) {
+ gint gdkScaleFactor = ScreenHelperGTK::GetGTKMonitorScaleFactor(aMonitorNum);
+
+ // gdk_screen_get_monitor_geometry / workarea returns application pixels
+ // (desktop pixels), so we need to convert it to device pixels with
+ // gdkScaleFactor.
+ gint geometryScaleFactor = gdkScaleFactor;
+
+ gint refreshRate = [&] {
+ // Since gtk 3.22
+ static auto s_gdk_monitor_get_refresh_rate = (int (*)(GdkMonitor*))dlsym(
+ RTLD_DEFAULT, "gdk_monitor_get_refresh_rate");
+
+ if (!s_gdk_monitor_get_refresh_rate) {
+ return 0;
+ }
+ GdkMonitor* monitor =
+ GdkDisplayGetMonitor(gdk_display_get_default(), aMonitorNum);
+ if (!monitor) {
+ return 0;
+ }
+ // Convert to Hz.
+ return NSToIntRound(s_gdk_monitor_get_refresh_rate(monitor) / 1000.0f);
+ }();
+
+ GdkRectangle workarea;
+ gdk_screen_get_monitor_workarea(aScreen, aMonitorNum, &workarea);
+ LayoutDeviceIntRect availRect(workarea.x * geometryScaleFactor,
+ workarea.y * geometryScaleFactor,
+ workarea.width * geometryScaleFactor,
+ workarea.height * geometryScaleFactor);
+ LayoutDeviceIntRect rect;
+ DesktopToLayoutDeviceScale contentsScale(1.0);
+ if (GdkIsX11Display()) {
+ GdkRectangle monitor;
+ gdk_screen_get_monitor_geometry(aScreen, aMonitorNum, &monitor);
+ rect = LayoutDeviceIntRect(monitor.x * geometryScaleFactor,
+ monitor.y * geometryScaleFactor,
+ monitor.width * geometryScaleFactor,
+ monitor.height * geometryScaleFactor);
+ } else {
+ // Don't report screen shift in Wayland, see bug 1795066.
+ availRect.MoveTo(0, 0);
+ // We use Gtk workarea on Wayland as it matches our needs (Bug 1732682).
+ rect = availRect;
+ // Use per-monitor scaling factor in Wayland.
+ contentsScale.scale = gdkScaleFactor;
+ }
+
+ uint32_t pixelDepth = GetGTKPixelDepth();
+ if (pixelDepth == 32) {
+ // If a device uses 32 bits per pixel, it's still only using 8 bits
+ // per color component, which is what our callers want to know.
+ // (Some devices report 32 and some devices report 24.)
+ pixelDepth = 24;
+ }
+
+ CSSToLayoutDeviceScale defaultCssScale(gdkScaleFactor);
+
+ float dpi = 96.0f;
+ gint heightMM = gdk_screen_get_monitor_height_mm(aScreen, aMonitorNum);
+ if (heightMM > 0) {
+ dpi = rect.height / (heightMM / MM_PER_INCH_FLOAT);
+ }
+
+ LOG_SCREEN(
+ "New monitor %d size [%d,%d -> %d x %d] depth %d scale %f CssScale %f "
+ "DPI %f refresh %d ]",
+ aMonitorNum, rect.x, rect.y, rect.width, rect.height, pixelDepth,
+ contentsScale.scale, defaultCssScale.scale, dpi, refreshRate);
+ return MakeAndAddRef<Screen>(rect, availRect, pixelDepth, pixelDepth,
+ refreshRate, contentsScale, defaultCssScale, dpi,
+ Screen::IsPseudoDisplay::No);
+}
+
+void ScreenGetterGtk::RefreshScreens() {
+ LOG_SCREEN("ScreenGetterGtk::RefreshScreens()");
+ AutoTArray<RefPtr<Screen>, 4> screenList;
+
+ GdkScreen* defaultScreen = gdk_screen_get_default();
+ gint numScreens = gdk_screen_get_n_monitors(defaultScreen);
+ LOG_SCREEN("GDK reports %d screens", numScreens);
+
+ for (gint i = 0; i < numScreens; i++) {
+ screenList.AppendElement(MakeScreenGtk(defaultScreen, i));
+ }
+
+ ScreenManager::Refresh(std::move(screenList));
+}
+
+gint ScreenHelperGTK::GetGTKMonitorScaleFactor(gint aMonitorNum) {
+ GdkScreen* screen = gdk_screen_get_default();
+ return gdk_screen_get_monitor_scale_factor(screen, aMonitorNum);
+}
+
+ScreenHelperGTK::ScreenHelperGTK() {
+ gScreenGetter = new ScreenGetterGtk();
+ gScreenGetter->Init();
+}
+
+int ScreenHelperGTK::GetMonitorCount() {
+ return gdk_screen_get_n_monitors(gdk_screen_get_default());
+}
+
+ScreenHelperGTK::~ScreenHelperGTK() { gScreenGetter = nullptr; }
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/ScreenHelperGTK.h b/widget/gtk/ScreenHelperGTK.h
new file mode 100644
index 0000000000..b9efb67746
--- /dev/null
+++ b/widget/gtk/ScreenHelperGTK.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_gtk_ScreenHelperGTK_h
+#define mozilla_widget_gtk_ScreenHelperGTK_h
+
+#include "mozilla/widget/ScreenManager.h"
+
+#include "gdk/gdk.h"
+
+class nsWindow;
+
+namespace mozilla::widget {
+
+class ScreenHelperGTK final : public ScreenManager::Helper {
+ public:
+ ScreenHelperGTK();
+ ~ScreenHelperGTK();
+
+ static int GetMonitorCount();
+ static gint GetGTKMonitorScaleFactor(gint aMonitorNum = 0);
+ static RefPtr<widget::Screen> GetScreenForWindow(nsWindow* aWindow);
+};
+
+} // namespace mozilla::widget
+
+#endif // mozilla_widget_gtk_ScreenHelperGTK_h
diff --git a/widget/gtk/TaskbarProgress.cpp b/widget/gtk/TaskbarProgress.cpp
new file mode 100644
index 0000000000..396f39b5e7
--- /dev/null
+++ b/widget/gtk/TaskbarProgress.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Logging.h"
+
+#include "TaskbarProgress.h"
+#include "nsWindow.h"
+#include "WidgetUtils.h"
+#include "nsPIDOMWindow.h"
+
+using mozilla::LogLevel;
+static mozilla::LazyLogModule gGtkTaskbarProgressLog("nsIGtkTaskbarProgress");
+
+/******************************************************************************
+ * TaskbarProgress
+ ******************************************************************************/
+
+NS_IMPL_ISUPPORTS(TaskbarProgress, nsIGtkTaskbarProgress, nsITaskbarProgress)
+
+TaskbarProgress::TaskbarProgress() : mPrimaryWindow(nullptr) {
+ MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Info,
+ ("%p TaskbarProgress()", this));
+}
+
+TaskbarProgress::~TaskbarProgress() {
+ MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Info,
+ ("%p ~TaskbarProgress()", this));
+}
+
+NS_IMETHODIMP
+TaskbarProgress::SetProgressState(nsTaskbarProgressState aState,
+ uint64_t aCurrentValue, uint64_t aMaxValue) {
+ NS_ENSURE_ARG_RANGE(aState, 0, STATE_PAUSED);
+
+ if (aState == STATE_NO_PROGRESS || aState == STATE_INDETERMINATE) {
+ NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
+ }
+
+ NS_ENSURE_TRUE((aCurrentValue <= aMaxValue), NS_ERROR_ILLEGAL_VALUE);
+
+ // See TaskbarProgress::SetPrimaryWindow: if we're running in headless
+ // mode, mPrimaryWindow will be null.
+ if (!mPrimaryWindow) {
+ return NS_OK;
+ }
+
+ gulong progress;
+
+ if (aMaxValue == 0) {
+ progress = 0;
+ } else {
+ // Rounding down to ensure we don't set to 'full' until the operation
+ // is completely finished.
+ progress = (gulong)(((double)aCurrentValue / aMaxValue) * 100.0);
+ }
+
+ // Check if the resultant value is the same as the previous call, and
+ // ignore this update if it is.
+
+ if (progress == mCurrentProgress) {
+ return NS_OK;
+ }
+
+ mCurrentProgress = progress;
+
+ MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Debug,
+ ("GtkTaskbarProgress::SetProgressState progress: %lu", progress));
+
+ mPrimaryWindow->SetProgress(progress);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarProgress::SetPrimaryWindow(mozIDOMWindowProxy* aWindow) {
+ NS_ENSURE_TRUE(aWindow != nullptr, NS_ERROR_ILLEGAL_VALUE);
+
+ auto* parent = nsPIDOMWindowOuter::From(aWindow);
+ RefPtr<nsIWidget> widget =
+ mozilla::widget::WidgetUtils::DOMWindowToWidget(parent);
+
+ // Only nsWindows have a native window, HeadlessWidgets do not. Stop here if
+ // the window does not have one.
+ if (!widget->GetNativeData(NS_NATIVE_WINDOW)) {
+ return NS_OK;
+ }
+
+ mPrimaryWindow = static_cast<nsWindow*>(widget.get());
+
+ // Clear our current progress. We get a forced update from the
+ // DownloadsTaskbar after returning from this function - zeroing out our
+ // progress will make sure the new window gets the property set on it
+ // immediately, rather than waiting for the progress value to change (which
+ // could be a while depending on size.)
+ mCurrentProgress = 0;
+
+ MOZ_LOG(gGtkTaskbarProgressLog, LogLevel::Debug,
+ ("GtkTaskbarProgress::SetPrimaryWindow window: %p",
+ mPrimaryWindow.get()));
+
+ return NS_OK;
+}
diff --git a/widget/gtk/TaskbarProgress.h b/widget/gtk/TaskbarProgress.h
new file mode 100644
index 0000000000..819df0c089
--- /dev/null
+++ b/widget/gtk/TaskbarProgress.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef TaskbarProgress_h_
+#define TaskbarProgress_h_
+
+#include "nsIGtkTaskbarProgress.h"
+
+class nsWindow;
+
+class TaskbarProgress final : public nsIGtkTaskbarProgress {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGTKTASKBARPROGRESS
+ NS_DECL_NSITASKBARPROGRESS
+
+ TaskbarProgress();
+
+ protected:
+ ~TaskbarProgress();
+
+ // We track the progress value so we can avoid updating the X window property
+ // unnecessarily.
+ unsigned long mCurrentProgress;
+
+ RefPtr<nsWindow> mPrimaryWindow;
+};
+
+#endif // #ifndef TaskbarProgress_h_
diff --git a/widget/gtk/WakeLockListener.cpp b/widget/gtk/WakeLockListener.cpp
new file mode 100644
index 0000000000..b2c43cb485
--- /dev/null
+++ b/widget/gtk/WakeLockListener.cpp
@@ -0,0 +1,913 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "WakeLockListener.h"
+#include "WidgetUtilsGtk.h"
+#include "mozilla/ScopeExit.h"
+
+#ifdef MOZ_ENABLE_DBUS
+# include <gio/gio.h>
+# include "AsyncDBus.h"
+#endif
+
+#if defined(MOZ_X11)
+# include "prlink.h"
+# include <gdk/gdk.h>
+# include <gdk/gdkx.h>
+# include "X11UndefineNone.h"
+#endif
+
+#if defined(MOZ_WAYLAND)
+# include "mozilla/widget/nsWaylandDisplay.h"
+# include "nsWindow.h"
+#endif
+
+#ifdef MOZ_ENABLE_DBUS
+# define FREEDESKTOP_PORTAL_DESKTOP_TARGET "org.freedesktop.portal.Desktop"
+# define FREEDESKTOP_PORTAL_DESKTOP_OBJECT "/org/freedesktop/portal/desktop"
+# define FREEDESKTOP_PORTAL_DESKTOP_INTERFACE "org.freedesktop.portal.Inhibit"
+# define FREEDESKTOP_PORTAL_DESKTOP_INHIBIT_IDLE_FLAG 8
+
+# define FREEDESKTOP_SCREENSAVER_TARGET "org.freedesktop.ScreenSaver"
+# define FREEDESKTOP_SCREENSAVER_OBJECT "/ScreenSaver"
+# define FREEDESKTOP_SCREENSAVER_INTERFACE "org.freedesktop.ScreenSaver"
+
+# define FREEDESKTOP_POWER_TARGET "org.freedesktop.PowerManagement"
+# define FREEDESKTOP_POWER_OBJECT "/org/freedesktop/PowerManagement/Inhibit"
+# define FREEDESKTOP_POWER_INTERFACE "org.freedesktop.PowerManagement.Inhibit"
+
+# define SESSION_MANAGER_TARGET "org.gnome.SessionManager"
+# define SESSION_MANAGER_OBJECT "/org/gnome/SessionManager"
+# define SESSION_MANAGER_INTERFACE "org.gnome.SessionManager"
+
+# define DBUS_TIMEOUT (-1)
+#endif
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+NS_IMPL_ISUPPORTS(WakeLockListener, nsIDOMMozWakeLockListener)
+
+#define WAKE_LOCK_LOG(str, ...) \
+ MOZ_LOG(gLinuxWakeLockLog, mozilla::LogLevel::Debug, \
+ ("[%p] " str, this, ##__VA_ARGS__))
+static mozilla::LazyLogModule gLinuxWakeLockLog("LinuxWakeLock");
+
+enum WakeLockType {
+ Initial = 0,
+#if defined(MOZ_ENABLE_DBUS)
+ FreeDesktopScreensaver = 1,
+ FreeDesktopPower = 2,
+ FreeDesktopPortal = 3,
+ GNOME = 4,
+#endif
+#if defined(MOZ_X11)
+ XScreenSaver = 5,
+#endif
+#if defined(MOZ_WAYLAND)
+ WaylandIdleInhibit = 6,
+#endif
+ Unsupported = 7,
+};
+
+#if defined(MOZ_ENABLE_DBUS)
+bool IsDBusWakeLock(int aWakeLockType) {
+ switch (aWakeLockType) {
+ case FreeDesktopScreensaver:
+ case FreeDesktopPower:
+ case GNOME:
+ case FreeDesktopPortal:
+ return true;
+ default:
+ return false;
+ }
+}
+#endif
+
+#ifdef MOZ_LOGGING
+const char* WakeLockTypeNames[] = {
+ "Initial",
+ "FreeDesktopScreensaver",
+ "FreeDesktopPower",
+ "FreeDesktopPortal",
+ "GNOME",
+ "XScreenSaver",
+ "WaylandIdleInhibit",
+ "Unsupported",
+};
+#endif
+
+class WakeLockTopic {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(WakeLockTopic)
+
+ explicit WakeLockTopic(const nsAString& aTopic) {
+ CopyUTF16toUTF8(aTopic, mTopic);
+ WAKE_LOCK_LOG("WakeLockTopic::WakeLockTopic() created %s", mTopic.get());
+ if (sWakeLockType == Initial) {
+ SwitchToNextWakeLockType();
+ }
+#ifdef MOZ_ENABLE_DBUS
+ mCancellable = dont_AddRef(g_cancellable_new());
+#endif
+ }
+
+ nsresult InhibitScreensaver();
+ nsresult UninhibitScreensaver();
+
+ void Shutdown();
+
+ private:
+ bool SendInhibit();
+ bool SendUninhibit();
+
+#if defined(MOZ_X11)
+ bool CheckXScreenSaverSupport();
+ bool InhibitXScreenSaver(bool inhibit);
+#endif
+
+#if defined(MOZ_WAYLAND)
+ zwp_idle_inhibitor_v1* mWaylandInhibitor = nullptr;
+ static bool CheckWaylandIdleInhibitSupport();
+ bool InhibitWaylandIdle();
+ bool UninhibitWaylandIdle();
+#endif
+
+ bool IsWakeLockTypeAvailable(int aWakeLockType);
+ bool SwitchToNextWakeLockType();
+
+#ifdef MOZ_ENABLE_DBUS
+ void DBusInhibitScreensaver(const char* aName, const char* aPath,
+ const char* aCall, const char* aMethod,
+ RefPtr<GVariant> aArgs);
+ void DBusUninhibitScreensaver(const char* aName, const char* aPath,
+ const char* aCall, const char* aMethod);
+
+ void InhibitFreeDesktopPortal();
+ void InhibitFreeDesktopScreensaver();
+ void InhibitFreeDesktopPower();
+ void InhibitGNOME();
+
+ void UninhibitFreeDesktopPortal();
+ void UninhibitFreeDesktopScreensaver();
+ void UninhibitFreeDesktopPower();
+ void UninhibitGNOME();
+
+ void DBusInhibitSucceeded(uint32_t aInhibitRequestID);
+ void DBusInhibitFailed(bool aFatal);
+ void DBusUninhibitSucceeded();
+ void DBusUninhibitFailed();
+ void ClearDBusInhibitToken();
+#endif
+ ~WakeLockTopic() = default;
+
+ // Why is screensaver inhibited
+ nsCString mTopic;
+
+ // Our desired state
+ bool mShouldInhibit = false;
+
+ // Our actual sate
+ bool mInhibited = false;
+
+#ifdef MOZ_ENABLE_DBUS
+ // We're waiting for DBus reply (inhibit/uninhibit calls).
+ bool mWaitingForDBusInhibit = false;
+ bool mWaitingForDBusUninhibit = false;
+
+ // mInhibitRequestID is received from success screen saver inhibit call
+ // and it's needed for screen saver enablement.
+ Maybe<uint32_t> mInhibitRequestID;
+
+ RefPtr<GCancellable> mCancellable;
+ // Used to uninhibit org.freedesktop.portal.Inhibit request
+ nsCString mRequestObjectPath;
+#endif
+
+ static int sWakeLockType;
+};
+
+int WakeLockTopic::sWakeLockType = Initial;
+
+#ifdef MOZ_ENABLE_DBUS
+void WakeLockTopic::DBusInhibitSucceeded(uint32_t aInhibitRequestID) {
+ mWaitingForDBusInhibit = false;
+ mInhibitRequestID = Some(aInhibitRequestID);
+ mInhibited = true;
+
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusInhibitSucceeded(), mInhibitRequestID %u "
+ "mShouldInhibit %d",
+ *mInhibitRequestID, mShouldInhibit);
+
+ // Uninhibit was requested before inhibit request was finished.
+ // So ask for it now.
+ if (!mShouldInhibit) {
+ UninhibitScreensaver();
+ }
+}
+
+void WakeLockTopic::DBusInhibitFailed(bool aFatal) {
+ WAKE_LOCK_LOG("WakeLockTopic::DBusInhibitFailed(%d)", aFatal);
+
+ mWaitingForDBusInhibit = false;
+ ClearDBusInhibitToken();
+
+ // Non-recoverable DBus error. Switch to another wake lock type.
+ if (aFatal && SwitchToNextWakeLockType()) {
+ SendInhibit();
+ }
+}
+
+void WakeLockTopic::DBusUninhibitSucceeded() {
+ WAKE_LOCK_LOG("WakeLockTopic::DBusUninhibitSucceeded() mShouldInhibit %d",
+ mShouldInhibit);
+
+ mWaitingForDBusUninhibit = false;
+ mInhibited = false;
+ ClearDBusInhibitToken();
+
+ // Inhibit was requested before uninhibit request was finished.
+ // So ask for it now.
+ if (mShouldInhibit) {
+ InhibitScreensaver();
+ }
+}
+
+void WakeLockTopic::DBusUninhibitFailed() {
+ WAKE_LOCK_LOG("WakeLockTopic::DBusUninhibitFailed()");
+ mWaitingForDBusUninhibit = false;
+ mInhibitRequestID = Nothing();
+}
+
+void WakeLockTopic::ClearDBusInhibitToken() {
+ mRequestObjectPath.Truncate();
+ mInhibitRequestID = Nothing();
+}
+
+void WakeLockTopic::DBusInhibitScreensaver(const char* aName, const char* aPath,
+ const char* aCall,
+ const char* aMethod,
+ RefPtr<GVariant> aArgs) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusInhibitScreensaver() mWaitingForDBusInhibit %d "
+ "mWaitingForDBusUninhibit %d",
+ mWaitingForDBusInhibit, mWaitingForDBusUninhibit);
+ if (mWaitingForDBusInhibit) {
+ WAKE_LOCK_LOG(" already waiting to inihibit, return");
+ return;
+ }
+ if (mWaitingForDBusUninhibit) {
+ WAKE_LOCK_LOG(" cancel un-inihibit request");
+ g_cancellable_cancel(mCancellable);
+ mWaitingForDBusUninhibit = false;
+ }
+ mWaitingForDBusInhibit = true;
+
+ widget::CreateDBusProxyForBus(
+ G_BUS_TYPE_SESSION,
+ GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
+ G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
+ /* aInterfaceInfo = */ nullptr, aName, aPath, aCall, mCancellable)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, this, args = RefPtr{aArgs},
+ aMethod](RefPtr<GDBusProxy>&& aProxy) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusInhibitScreensaver() proxy created");
+ DBusProxyCall(aProxy.get(), aMethod, args.get(),
+ G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
+ if (!g_variant_is_of_type(aResult.get(),
+ G_VARIANT_TYPE_TUPLE) ||
+ g_variant_n_children(aResult.get()) != 1) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusInhibitScreensaver() wrong "
+ "reply type %s\n",
+ g_variant_get_type_string(aResult.get()));
+ DBusInhibitFailed(/* aFatal */ true);
+ return;
+ }
+ RefPtr<GVariant> variant = dont_AddRef(
+ g_variant_get_child_value(aResult.get(), 0));
+ if (!g_variant_is_of_type(variant,
+ G_VARIANT_TYPE_UINT32)) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusInhibitScreensaver() wrong "
+ "reply type %s\n",
+ g_variant_get_type_string(aResult.get()));
+ DBusInhibitFailed(/* aFatal */ true);
+ return;
+ }
+ DBusInhibitSucceeded(g_variant_get_uint32(variant));
+ },
+ [s = RefPtr{this}, this,
+ aMethod](GUniquePtr<GError>&& aError) {
+ // Failed to send inhibit request over proxy.
+ // Switch to another wake lock type.
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusInhibitFailed() %s call failed : "
+ "%s\n",
+ aMethod, aError->message);
+ DBusInhibitFailed(
+ /* aFatal */ !IsCancelledGError(aError.get()));
+ });
+ },
+ [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
+ // We failed to create DBus proxy. Switch to another
+ // wake lock type.
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusInhibitScreensaver() Proxy creation "
+ "failed: %s\n",
+ aError->message);
+ DBusInhibitFailed(/* aFatal */ !IsCancelledGError(aError.get()));
+ });
+}
+
+void WakeLockTopic::DBusUninhibitScreensaver(const char* aName,
+ const char* aPath,
+ const char* aCall,
+ const char* aMethod) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusUninhibitScreensaver() mWaitingForDBusInhibit %d "
+ "mWaitingForDBusUninhibit %d request id %d",
+ mWaitingForDBusInhibit, mWaitingForDBusUninhibit,
+ mInhibitRequestID ? *mInhibitRequestID : -1);
+
+ if (mWaitingForDBusUninhibit) {
+ WAKE_LOCK_LOG(" already waiting to uninihibit, return");
+ return;
+ }
+
+ if (mWaitingForDBusInhibit) {
+ WAKE_LOCK_LOG(" cancel inihibit request");
+ g_cancellable_cancel(mCancellable);
+ mWaitingForDBusInhibit = false;
+ }
+
+ if (!mInhibitRequestID.isSome()) {
+ WAKE_LOCK_LOG(" missing inihibit token, quit.");
+ // missing uninhibit token, just quit.
+ return;
+ }
+ mWaitingForDBusUninhibit = true;
+
+ RefPtr<GVariant> variant =
+ dont_AddRef(g_variant_ref_sink(g_variant_new("(u)", *mInhibitRequestID)));
+ nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget();
+ widget::CreateDBusProxyForBus(
+ G_BUS_TYPE_SESSION,
+ GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
+ G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
+ /* aInterfaceInfo = */ nullptr, aName, aPath, aCall, mCancellable)
+ ->Then(
+ target, __func__,
+ [self = RefPtr{this}, this, args = std::move(variant), target,
+ aMethod](RefPtr<GDBusProxy>&& aProxy) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusUninhibitScreensaver() proxy created");
+ DBusProxyCall(aProxy.get(), aMethod, args.get(),
+ G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
+ ->Then(
+ target, __func__,
+ [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
+ DBusUninhibitSucceeded();
+ },
+ [s = RefPtr{this}, this,
+ aMethod](GUniquePtr<GError>&& aError) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusUninhibitFailed() %s call failed "
+ ": %s\n",
+ aMethod, aError->message);
+ DBusUninhibitFailed();
+ });
+ },
+ [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::DBusUninhibitFailed() Proxy creation failed: "
+ "%s\n",
+ aError->message);
+ DBusUninhibitFailed();
+ });
+}
+
+void WakeLockTopic::InhibitFreeDesktopPortal() {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::InhibitFreeDesktopPortal() mWaitingForDBusInhibit %d "
+ "mWaitingForDBusUninhibit %d",
+ mWaitingForDBusInhibit, mWaitingForDBusUninhibit);
+ if (mWaitingForDBusInhibit) {
+ WAKE_LOCK_LOG(" already waiting to inihibit, return");
+ return;
+ }
+ if (mWaitingForDBusUninhibit) {
+ WAKE_LOCK_LOG(" cancel un-inihibit request");
+ g_cancellable_cancel(mCancellable);
+ mWaitingForDBusUninhibit = false;
+ }
+ mWaitingForDBusInhibit = true;
+
+ CreateDBusProxyForBus(
+ G_BUS_TYPE_SESSION,
+ GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
+ G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
+ nullptr, FREEDESKTOP_PORTAL_DESKTOP_TARGET,
+ FREEDESKTOP_PORTAL_DESKTOP_OBJECT, FREEDESKTOP_PORTAL_DESKTOP_INTERFACE,
+ mCancellable)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, this](RefPtr<GDBusProxy>&& aProxy) {
+ GVariantBuilder b;
+ g_variant_builder_init(&b, G_VARIANT_TYPE_VARDICT);
+ g_variant_builder_add(&b, "{sv}", "reason",
+ g_variant_new_string(self->mTopic.get()));
+
+ // From
+ // https://flatpak.github.io/xdg-desktop-portal/docs/#gdbus-org.freedesktop.portal.Inhibit
+ DBusProxyCall(
+ aProxy.get(), "Inhibit",
+ g_variant_new("(sua{sv})", g_get_prgname(),
+ FREEDESKTOP_PORTAL_DESKTOP_INHIBIT_IDLE_FLAG, &b),
+ G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
+ gchar* requestObjectPath = nullptr;
+ g_variant_get(aResult, "(o)", &requestObjectPath);
+ if (!requestObjectPath) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::InhibitFreeDesktopPortal(): Unable "
+ "to get requestObjectPath\n");
+ DBusInhibitFailed(/* aFatal */ true);
+ return;
+ }
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::InhibitFreeDesktopPortal(): "
+ "inhibited, objpath to unihibit: %s\n",
+ requestObjectPath);
+ mRequestObjectPath.Adopt(requestObjectPath);
+ DBusInhibitSucceeded(0);
+ },
+ [s = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
+ DBusInhibitFailed(
+ /* aFatal */ !IsCancelledGError(aError.get()));
+ WAKE_LOCK_LOG(
+ "Failed to create DBus proxy for "
+ "org.freedesktop.portal.Desktop: %s\n",
+ aError->message);
+ });
+ },
+ [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
+ WAKE_LOCK_LOG(
+ "Failed to create DBus proxy for "
+ "org.freedesktop.portal.Desktop: %s\n",
+ aError->message);
+ DBusInhibitFailed(/* aFatal */ !IsCancelledGError(aError.get()));
+ });
+}
+
+void WakeLockTopic::InhibitFreeDesktopScreensaver() {
+ WAKE_LOCK_LOG("InhibitFreeDesktopScreensaver()");
+ DBusInhibitScreensaver(FREEDESKTOP_SCREENSAVER_TARGET,
+ FREEDESKTOP_SCREENSAVER_OBJECT,
+ FREEDESKTOP_SCREENSAVER_INTERFACE, "Inhibit",
+ dont_AddRef(g_variant_ref_sink(g_variant_new(
+ "(ss)", g_get_prgname(), mTopic.get()))));
+}
+
+void WakeLockTopic::InhibitFreeDesktopPower() {
+ WAKE_LOCK_LOG("InhibitFreeDesktopPower()");
+ DBusInhibitScreensaver(FREEDESKTOP_POWER_TARGET, FREEDESKTOP_POWER_OBJECT,
+ FREEDESKTOP_POWER_INTERFACE, "Inhibit",
+ dont_AddRef(g_variant_ref_sink(g_variant_new(
+ "(ss)", g_get_prgname(), mTopic.get()))));
+}
+
+void WakeLockTopic::InhibitGNOME() {
+ WAKE_LOCK_LOG("InhibitGNOME()");
+ static const uint32_t xid = 0;
+ static const uint32_t flags = (1 << 3); // Inhibit idle
+ DBusInhibitScreensaver(
+ SESSION_MANAGER_TARGET, SESSION_MANAGER_OBJECT, SESSION_MANAGER_INTERFACE,
+ "Inhibit",
+ dont_AddRef(g_variant_ref_sink(
+ g_variant_new("(susu)", g_get_prgname(), xid, mTopic.get(), flags))));
+}
+
+void WakeLockTopic::UninhibitFreeDesktopPortal() {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::UninhibitFreeDesktopPortal() mWaitingForDBusInhibit %d "
+ "mWaitingForDBusUninhibit %d object path: %s",
+ mWaitingForDBusInhibit, mWaitingForDBusUninhibit,
+ mRequestObjectPath.get());
+
+ if (mWaitingForDBusUninhibit) {
+ WAKE_LOCK_LOG(" already waiting to uninihibit, return");
+ return;
+ }
+
+ if (mWaitingForDBusInhibit) {
+ WAKE_LOCK_LOG(" cancel inihibit request");
+ g_cancellable_cancel(mCancellable);
+ mWaitingForDBusInhibit = false;
+ }
+ if (mRequestObjectPath.IsEmpty()) {
+ WAKE_LOCK_LOG("UninhibitFreeDesktopPortal() failed: unknown object path\n");
+ return;
+ }
+ mWaitingForDBusUninhibit = true;
+
+ nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget();
+ CreateDBusProxyForBus(
+ G_BUS_TYPE_SESSION,
+ GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
+ G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
+ nullptr, FREEDESKTOP_PORTAL_DESKTOP_TARGET, mRequestObjectPath.get(),
+ "org.freedesktop.portal.Request", mCancellable)
+ ->Then(
+ target, __func__,
+ [self = RefPtr{this}, target, this](RefPtr<GDBusProxy>&& aProxy) {
+ DBusProxyCall(aProxy.get(), "Close", nullptr,
+ G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, mCancellable)
+ ->Then(
+ target, __func__,
+ [s = RefPtr{this}, this](RefPtr<GVariant>&& aResult) {
+ DBusUninhibitSucceeded();
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::UninhibitFreeDesktopPortal() Inhibit "
+ "removed\n");
+ },
+ [s = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
+ DBusUninhibitFailed();
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::UninhibitFreeDesktopPortal() "
+ "Removing inhibit failed: %s\n",
+ aError->message);
+ });
+ },
+ [self = RefPtr{this}, this](GUniquePtr<GError>&& aError) {
+ WAKE_LOCK_LOG(
+ "WakeLockTopic::UninhibitFreeDesktopPortal() Proxy creation "
+ "failed: %s\n",
+ aError->message);
+ DBusUninhibitFailed();
+ });
+}
+
+void WakeLockTopic::UninhibitFreeDesktopScreensaver() {
+ WAKE_LOCK_LOG("UninhibitFreeDesktopScreensaver()");
+ DBusUninhibitScreensaver(FREEDESKTOP_SCREENSAVER_TARGET,
+ FREEDESKTOP_SCREENSAVER_OBJECT,
+ FREEDESKTOP_SCREENSAVER_INTERFACE, "UnInhibit");
+}
+
+void WakeLockTopic::UninhibitFreeDesktopPower() {
+ WAKE_LOCK_LOG("UninhibitFreeDesktopPower()");
+ DBusUninhibitScreensaver(FREEDESKTOP_POWER_TARGET, FREEDESKTOP_POWER_OBJECT,
+ FREEDESKTOP_POWER_INTERFACE, "UnInhibit");
+}
+
+void WakeLockTopic::UninhibitGNOME() {
+ WAKE_LOCK_LOG("UninhibitGNOME()");
+ DBusUninhibitScreensaver(SESSION_MANAGER_TARGET, SESSION_MANAGER_OBJECT,
+ SESSION_MANAGER_INTERFACE, "Uninhibit");
+}
+#endif
+
+#if defined(MOZ_X11)
+// TODO: Merge with Idle service?
+typedef Bool (*_XScreenSaverQueryExtension_fn)(Display* dpy, int* event_base,
+ int* error_base);
+typedef Bool (*_XScreenSaverQueryVersion_fn)(Display* dpy, int* major,
+ int* minor);
+typedef void (*_XScreenSaverSuspend_fn)(Display* dpy, Bool suspend);
+
+static PRLibrary* sXssLib = nullptr;
+static _XScreenSaverQueryExtension_fn _XSSQueryExtension = nullptr;
+static _XScreenSaverQueryVersion_fn _XSSQueryVersion = nullptr;
+static _XScreenSaverSuspend_fn _XSSSuspend = nullptr;
+
+/* static */
+bool WakeLockTopic::CheckXScreenSaverSupport() {
+ if (!sXssLib) {
+ sXssLib = PR_LoadLibrary("libXss.so.1");
+ if (!sXssLib) {
+ return false;
+ }
+ }
+
+ _XSSQueryExtension = (_XScreenSaverQueryExtension_fn)PR_FindFunctionSymbol(
+ sXssLib, "XScreenSaverQueryExtension");
+ _XSSQueryVersion = (_XScreenSaverQueryVersion_fn)PR_FindFunctionSymbol(
+ sXssLib, "XScreenSaverQueryVersion");
+ _XSSSuspend = (_XScreenSaverSuspend_fn)PR_FindFunctionSymbol(
+ sXssLib, "XScreenSaverSuspend");
+ if (!_XSSQueryExtension || !_XSSQueryVersion || !_XSSSuspend) {
+ return false;
+ }
+
+ GdkDisplay* gDisplay = gdk_display_get_default();
+ if (!GdkIsX11Display(gDisplay)) {
+ return false;
+ }
+ Display* display = GDK_DISPLAY_XDISPLAY(gDisplay);
+
+ int throwaway;
+ if (!_XSSQueryExtension(display, &throwaway, &throwaway)) return false;
+
+ int major, minor;
+ if (!_XSSQueryVersion(display, &major, &minor)) return false;
+ // Needs to be compatible with version 1.1
+ if (major != 1) return false;
+ if (minor < 1) return false;
+
+ WAKE_LOCK_LOG("XScreenSaver supported.");
+ return true;
+}
+
+/* static */
+bool WakeLockTopic::InhibitXScreenSaver(bool inhibit) {
+ WAKE_LOCK_LOG("InhibitXScreenSaver %d", inhibit);
+
+ // Should only be called if CheckXScreenSaverSupport returns true.
+ // There's a couple of safety checks here nonetheless.
+ if (!_XSSSuspend) {
+ return false;
+ }
+ GdkDisplay* gDisplay = gdk_display_get_default();
+ if (!GdkIsX11Display(gDisplay)) {
+ return false;
+ }
+ Display* display = GDK_DISPLAY_XDISPLAY(gDisplay);
+ _XSSSuspend(display, inhibit);
+
+ WAKE_LOCK_LOG("InhibitXScreenSaver %d succeeded", inhibit);
+ mInhibited = inhibit;
+ return true;
+}
+#endif
+
+#if defined(MOZ_WAYLAND)
+/* static */
+bool WakeLockTopic::CheckWaylandIdleInhibitSupport() {
+ nsWaylandDisplay* waylandDisplay = WaylandDisplayGet();
+ return waylandDisplay && waylandDisplay->GetIdleInhibitManager() != nullptr;
+}
+
+bool WakeLockTopic::InhibitWaylandIdle() {
+ WAKE_LOCK_LOG("InhibitWaylandIdle()");
+
+ nsWaylandDisplay* waylandDisplay = WaylandDisplayGet();
+ if (!waylandDisplay) {
+ return false;
+ }
+
+ nsWindow* focusedWindow = nsWindow::GetFocusedWindow();
+ if (!focusedWindow) {
+ return false;
+ }
+
+ UninhibitWaylandIdle();
+
+ MozContainerSurfaceLock lock(focusedWindow->GetMozContainer());
+ struct wl_surface* waylandSurface = lock.GetSurface();
+ if (waylandSurface) {
+ mWaylandInhibitor = zwp_idle_inhibit_manager_v1_create_inhibitor(
+ waylandDisplay->GetIdleInhibitManager(), waylandSurface);
+ mInhibited = true;
+ }
+
+ WAKE_LOCK_LOG("InhibitWaylandIdle() %s",
+ !!mWaylandInhibitor ? "succeeded" : "failed");
+ return !!mWaylandInhibitor;
+}
+
+bool WakeLockTopic::UninhibitWaylandIdle() {
+ WAKE_LOCK_LOG("UninhibitWaylandIdle() mWaylandInhibitor %p",
+ mWaylandInhibitor);
+
+ mInhibited = false;
+ if (!mWaylandInhibitor) {
+ return false;
+ }
+ zwp_idle_inhibitor_v1_destroy(mWaylandInhibitor);
+ mWaylandInhibitor = nullptr;
+ return true;
+}
+#endif
+
+bool WakeLockTopic::SendInhibit() {
+ WAKE_LOCK_LOG("WakeLockTopic::SendInhibit() WakeLockType %s",
+ WakeLockTypeNames[sWakeLockType]);
+ MOZ_ASSERT(sWakeLockType != Initial);
+
+ switch (sWakeLockType) {
+#if defined(MOZ_ENABLE_DBUS)
+ case FreeDesktopPortal:
+ InhibitFreeDesktopPortal();
+ break;
+ case FreeDesktopScreensaver:
+ InhibitFreeDesktopScreensaver();
+ break;
+ case FreeDesktopPower:
+ InhibitFreeDesktopPower();
+ break;
+ case GNOME:
+ InhibitGNOME();
+ break;
+#endif
+#if defined(MOZ_X11)
+ case XScreenSaver:
+ return InhibitXScreenSaver(true);
+#endif
+#if defined(MOZ_WAYLAND)
+ case WaylandIdleInhibit:
+ return InhibitWaylandIdle();
+#endif
+ default:
+ return false;
+ }
+ return true;
+}
+
+bool WakeLockTopic::SendUninhibit() {
+ WAKE_LOCK_LOG("WakeLockTopic::SendUninhibit() WakeLockType %s",
+ WakeLockTypeNames[sWakeLockType]);
+ MOZ_ASSERT(sWakeLockType != Initial);
+ switch (sWakeLockType) {
+#if defined(MOZ_ENABLE_DBUS)
+ case FreeDesktopPortal:
+ UninhibitFreeDesktopPortal();
+ break;
+ case FreeDesktopScreensaver:
+ UninhibitFreeDesktopScreensaver();
+ break;
+ case FreeDesktopPower:
+ UninhibitFreeDesktopPower();
+ break;
+ case GNOME:
+ UninhibitGNOME();
+ break;
+#endif
+#if defined(MOZ_X11)
+ case XScreenSaver:
+ return InhibitXScreenSaver(false);
+#endif
+#if defined(MOZ_WAYLAND)
+ case WaylandIdleInhibit:
+ return UninhibitWaylandIdle();
+#endif
+ default:
+ return false;
+ }
+ return true;
+}
+
+nsresult WakeLockTopic::InhibitScreensaver() {
+ WAKE_LOCK_LOG("WakeLockTopic::InhibitScreensaver() Inhibited %d", mInhibited);
+
+ if (mInhibited) {
+ // Screensaver is inhibited. Nothing to do here.
+ return NS_OK;
+ }
+ mShouldInhibit = true;
+
+ // Iterate through wake lock types in case of failure.
+ while (!SendInhibit() && SwitchToNextWakeLockType()) {
+ ;
+ }
+
+ return (sWakeLockType != Unsupported) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+void WakeLockTopic::Shutdown() {
+ WAKE_LOCK_LOG("WakeLockTopic::Shutdown() state %d", mInhibited);
+#ifdef MOZ_ENABLE_DBUS
+ if (mWaitingForDBusUninhibit) {
+ return;
+ }
+ g_cancellable_cancel(mCancellable);
+#endif
+ if (mInhibited) {
+ UninhibitScreensaver();
+ }
+}
+
+nsresult WakeLockTopic::UninhibitScreensaver() {
+ WAKE_LOCK_LOG("WakeLockTopic::UninhibitScreensaver() Inhibited %d",
+ mInhibited);
+
+ if (!mInhibited) {
+ // Screensaver isn't inhibited. Nothing to do here.
+ return NS_OK;
+ }
+ mShouldInhibit = false;
+
+ // Don't switch wake lock type in case of failure.
+ // We need to use the same lock/unlock type.
+ return SendUninhibit() ? NS_OK : NS_ERROR_FAILURE;
+}
+
+bool WakeLockTopic::IsWakeLockTypeAvailable(int aWakeLockType) {
+ switch (aWakeLockType) {
+#if defined(MOZ_ENABLE_DBUS)
+ case FreeDesktopPortal:
+ case FreeDesktopScreensaver:
+ case FreeDesktopPower:
+ case GNOME:
+ return true;
+#endif
+#if defined(MOZ_X11)
+ case XScreenSaver:
+ if (!GdkIsX11Display()) {
+ return false;
+ }
+ if (!CheckXScreenSaverSupport()) {
+ WAKE_LOCK_LOG(" XScreenSaverSupport is missing!");
+ return false;
+ }
+ return true;
+#endif
+#if defined(MOZ_WAYLAND)
+ case WaylandIdleInhibit:
+ if (!GdkIsWaylandDisplay()) {
+ return false;
+ }
+ if (!CheckWaylandIdleInhibitSupport()) {
+ WAKE_LOCK_LOG(" WaylandIdleInhibitSupport is missing!");
+ return false;
+ }
+ return true;
+#endif
+ default:
+ return false;
+ }
+}
+
+bool WakeLockTopic::SwitchToNextWakeLockType() {
+ WAKE_LOCK_LOG("WakeLockTopic::SwitchToNextWakeLockType() WakeLockType %s",
+ WakeLockTypeNames[sWakeLockType]);
+
+ if (sWakeLockType == Unsupported) {
+ return false;
+ }
+
+#ifdef MOZ_LOGGING
+ auto printWakeLocktype = MakeScopeExit([&] {
+ WAKE_LOCK_LOG(" switched to WakeLockType %s",
+ WakeLockTypeNames[sWakeLockType]);
+ });
+#endif
+
+#if defined(MOZ_ENABLE_DBUS)
+ if (IsDBusWakeLock(sWakeLockType)) {
+ // We're switching out of DBus wakelock - clear our recent DBus states.
+ mWaitingForDBusInhibit = false;
+ mWaitingForDBusUninhibit = false;
+ mInhibited = false;
+ ClearDBusInhibitToken();
+ }
+#endif
+
+ while (sWakeLockType != Unsupported) {
+ sWakeLockType++;
+ if (IsWakeLockTypeAvailable(sWakeLockType)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+WakeLockListener::WakeLockListener() = default;
+
+WakeLockListener::~WakeLockListener() {
+ for (const auto& topic : mTopics.Values()) {
+ topic->Shutdown();
+ }
+}
+
+nsresult WakeLockListener::Callback(const nsAString& topic,
+ const nsAString& state) {
+ if (!topic.Equals(u"screen"_ns) && !topic.Equals(u"video-playing"_ns) &&
+ !topic.Equals(u"autoscroll"_ns)) {
+ return NS_OK;
+ }
+
+ RefPtr<WakeLockTopic> topicLock = mTopics.LookupOrInsertWith(
+ topic, [&] { return MakeRefPtr<WakeLockTopic>(topic); });
+
+ // Treat "locked-background" the same as "unlocked" on desktop linux.
+ bool shouldLock = state.EqualsLiteral("locked-foreground");
+ WAKE_LOCK_LOG("WakeLockListener topic %s state %s request lock %d",
+ NS_ConvertUTF16toUTF8(topic).get(),
+ NS_ConvertUTF16toUTF8(state).get(), shouldLock);
+
+ return shouldLock ? topicLock->InhibitScreensaver()
+ : topicLock->UninhibitScreensaver();
+}
diff --git a/widget/gtk/WakeLockListener.h b/widget/gtk/WakeLockListener.h
new file mode 100644
index 0000000000..1c587d2302
--- /dev/null
+++ b/widget/gtk/WakeLockListener.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __WakeLockListener_h__
+#define __WakeLockListener_h__
+
+#include "nsHashKeys.h"
+#include "nsRefPtrHashtable.h"
+
+#include "nsIDOMWakeLockListener.h"
+
+class WakeLockTopic;
+
+/**
+ * Receives WakeLock events and simply passes it on to the right WakeLockTopic
+ * to inhibit the screensaver.
+ */
+class WakeLockListener final : public nsIDOMMozWakeLockListener {
+ public:
+ NS_DECL_ISUPPORTS;
+
+ nsresult Callback(const nsAString& topic, const nsAString& state) override;
+
+ WakeLockListener();
+
+ private:
+ ~WakeLockListener();
+
+ // Map of topic names to |WakeLockTopic|s.
+ // We assume a small, finite-sized set of topics.
+ nsRefPtrHashtable<nsStringHashKey, WakeLockTopic> mTopics;
+};
+
+#endif // __WakeLockListener_h__
diff --git a/widget/gtk/WaylandBuffer.cpp b/widget/gtk/WaylandBuffer.cpp
new file mode 100644
index 0000000000..f3fc409362
--- /dev/null
+++ b/widget/gtk/WaylandBuffer.cpp
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 4; 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 "WaylandBuffer.h"
+
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include "gfx2DGlue.h"
+#include "gfxPlatform.h"
+#include "mozilla/WidgetUtilsGtk.h"
+#include "mozilla/gfx/Tools.h"
+#include "nsGtkUtils.h"
+#include "nsPrintfCString.h"
+#include "prenv.h" // For PR_GetEnv
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "mozilla/ScopeExit.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gWidgetWaylandLog;
+# define LOGWAYLAND(...) \
+ MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# define LOGWAYLAND(...)
+#endif /* MOZ_LOGGING */
+
+using namespace mozilla::gl;
+
+namespace mozilla::widget {
+
+#define BUFFER_BPP 4
+gfx::SurfaceFormat WaylandBuffer::mFormat = gfx::SurfaceFormat::B8G8R8A8;
+
+#ifdef MOZ_LOGGING
+int WaylandBufferSHM::mDumpSerial =
+ PR_GetEnv("MOZ_WAYLAND_DUMP_WL_BUFFERS") ? 1 : 0;
+char* WaylandBufferSHM::mDumpDir = PR_GetEnv("MOZ_WAYLAND_DUMP_DIR");
+#endif
+
+/* static */
+RefPtr<WaylandShmPool> WaylandShmPool::Create(nsWaylandDisplay* aWaylandDisplay,
+ int aSize) {
+ if (!aWaylandDisplay->GetShm()) {
+ NS_WARNING("WaylandShmPool: Missing Wayland shm interface!");
+ return nullptr;
+ }
+
+ RefPtr<WaylandShmPool> shmPool = new WaylandShmPool();
+
+ shmPool->mShm = MakeUnique<base::SharedMemory>();
+ if (!shmPool->mShm->Create(aSize)) {
+ NS_WARNING("WaylandShmPool: Unable to allocate shared memory!");
+ return nullptr;
+ }
+
+ shmPool->mSize = aSize;
+ shmPool->mShmPool = wl_shm_create_pool(
+ aWaylandDisplay->GetShm(), shmPool->mShm->CloneHandle().get(), aSize);
+ if (!shmPool->mShmPool) {
+ NS_WARNING("WaylandShmPool: Unable to allocate shared memory pool!");
+ return nullptr;
+ }
+
+ return shmPool;
+}
+
+void* WaylandShmPool::GetImageData() {
+ if (mImageData) {
+ return mImageData;
+ }
+ if (!mShm->Map(mSize)) {
+ NS_WARNING("WaylandShmPool: Failed to map Shm!");
+ return nullptr;
+ }
+ mImageData = mShm->memory();
+ return mImageData;
+}
+
+WaylandShmPool::~WaylandShmPool() {
+ MozClearPointer(mShmPool, wl_shm_pool_destroy);
+}
+
+static const struct wl_buffer_listener sBufferListenerWaylandBuffer = {
+ WaylandBuffer::BufferReleaseCallbackHandler};
+
+WaylandBuffer::WaylandBuffer(const LayoutDeviceIntSize& aSize) : mSize(aSize) {}
+
+void WaylandBuffer::AttachAndCommit(wl_surface* aSurface) {
+ LOGWAYLAND(
+ "WaylandBuffer::AttachAndCommit [%p] wl_surface %p ID %d wl_buffer "
+ "%p ID %d\n",
+ (void*)this, (void*)aSurface,
+ aSurface ? wl_proxy_get_id((struct wl_proxy*)aSurface) : -1,
+ (void*)GetWlBuffer(),
+ GetWlBuffer() ? wl_proxy_get_id((struct wl_proxy*)GetWlBuffer()) : -1);
+
+ wl_buffer* buffer = GetWlBuffer();
+ if (buffer) {
+ mAttached = true;
+ wl_surface_attach(aSurface, buffer, 0, 0);
+ wl_surface_commit(aSurface);
+ }
+}
+
+void WaylandBuffer::BufferReleaseCallbackHandler(wl_buffer* aBuffer) {
+ mAttached = false;
+
+ if (mBufferReleaseFunc) {
+ mBufferReleaseFunc(mBufferReleaseData, aBuffer);
+ }
+}
+
+void WaylandBuffer::BufferReleaseCallbackHandler(void* aData,
+ wl_buffer* aBuffer) {
+ auto* buffer = reinterpret_cast<WaylandBuffer*>(aData);
+ buffer->BufferReleaseCallbackHandler(aBuffer);
+}
+
+/* static */
+RefPtr<WaylandBufferSHM> WaylandBufferSHM::Create(
+ const LayoutDeviceIntSize& aSize) {
+ RefPtr<WaylandBufferSHM> buffer = new WaylandBufferSHM(aSize);
+ nsWaylandDisplay* waylandDisplay = WaylandDisplayGet();
+
+ int size = aSize.width * aSize.height * BUFFER_BPP;
+ buffer->mShmPool = WaylandShmPool::Create(waylandDisplay, size);
+ if (!buffer->mShmPool) {
+ return nullptr;
+ }
+
+ buffer->mWLBuffer = wl_shm_pool_create_buffer(
+ buffer->mShmPool->GetShmPool(), 0, aSize.width, aSize.height,
+ aSize.width * BUFFER_BPP, WL_SHM_FORMAT_ARGB8888);
+ if (!buffer->mWLBuffer) {
+ return nullptr;
+ }
+
+ wl_buffer_add_listener(buffer->GetWlBuffer(), &sBufferListenerWaylandBuffer,
+ buffer.get());
+
+ LOGWAYLAND("WaylandBufferSHM Created [%p] WaylandDisplay [%p]\n",
+ buffer.get(), waylandDisplay);
+
+ return buffer;
+}
+
+WaylandBufferSHM::WaylandBufferSHM(const LayoutDeviceIntSize& aSize)
+ : WaylandBuffer(aSize) {}
+
+WaylandBufferSHM::~WaylandBufferSHM() {
+ MozClearPointer(mWLBuffer, wl_buffer_destroy);
+}
+
+already_AddRefed<gfx::DrawTarget> WaylandBufferSHM::Lock() {
+ return gfxPlatform::CreateDrawTargetForData(
+ static_cast<unsigned char*>(mShmPool->GetImageData()),
+ mSize.ToUnknownSize(), BUFFER_BPP * mSize.width, GetSurfaceFormat());
+}
+
+void WaylandBufferSHM::Clear() {
+ memset(mShmPool->GetImageData(), 0, mSize.height * mSize.width * BUFFER_BPP);
+}
+
+#ifdef MOZ_LOGGING
+void WaylandBufferSHM::DumpToFile(const char* aHint) {
+ if (!mDumpSerial) {
+ return;
+ }
+
+ cairo_surface_t* surface = nullptr;
+ auto unmap = MakeScopeExit([&] {
+ if (surface) {
+ cairo_surface_destroy(surface);
+ }
+ });
+ surface = cairo_image_surface_create_for_data(
+ (unsigned char*)mShmPool->GetImageData(), CAIRO_FORMAT_ARGB32,
+ mSize.width, mSize.height, BUFFER_BPP * mSize.width);
+ if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) {
+ nsCString filename;
+ if (mDumpDir) {
+ filename.Append(mDumpDir);
+ filename.Append('/');
+ }
+ filename.Append(
+ nsPrintfCString("firefox-wl-buffer-%.5d-%s.png", mDumpSerial++, aHint));
+ cairo_surface_write_to_png(surface, filename.get());
+ LOGWAYLAND("Dumped wl_buffer to %s\n", filename.get());
+ }
+}
+#endif
+
+/* static */
+RefPtr<WaylandBufferDMABUF> WaylandBufferDMABUF::Create(
+ const LayoutDeviceIntSize& aSize, GLContext* aGL) {
+ RefPtr<WaylandBufferDMABUF> buffer = new WaylandBufferDMABUF(aSize);
+
+ const auto flags =
+ static_cast<DMABufSurfaceFlags>(DMABUF_TEXTURE | DMABUF_ALPHA);
+ buffer->mDMABufSurface =
+ DMABufSurfaceRGBA::CreateDMABufSurface(aSize.width, aSize.height, flags);
+ if (!buffer->mDMABufSurface || !buffer->mDMABufSurface->CreateTexture(aGL)) {
+ return nullptr;
+ }
+
+ if (!buffer->mDMABufSurface->CreateWlBuffer()) {
+ return nullptr;
+ }
+
+ wl_buffer_add_listener(buffer->GetWlBuffer(), &sBufferListenerWaylandBuffer,
+ buffer.get());
+
+ return buffer;
+}
+
+WaylandBufferDMABUF::WaylandBufferDMABUF(const LayoutDeviceIntSize& aSize)
+ : WaylandBuffer(aSize) {}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/WaylandBuffer.h b/widget/gtk/WaylandBuffer.h
new file mode 100644
index 0000000000..34bdb87ff5
--- /dev/null
+++ b/widget/gtk/WaylandBuffer.h
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef _MOZILLA_WIDGET_GTK_WAYLAND_BUFFER_H
+#define _MOZILLA_WIDGET_GTK_WAYLAND_BUFFER_H
+
+#include "DMABufSurface.h"
+#include "GLContext.h"
+#include "MozFramebuffer.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/Mutex.h"
+#include "nsTArray.h"
+#include "nsWaylandDisplay.h"
+#include "base/shared_memory.h"
+
+namespace mozilla::widget {
+
+// Allocates and owns shared memory for Wayland drawing surface
+class WaylandShmPool {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaylandShmPool);
+
+ static RefPtr<WaylandShmPool> Create(nsWaylandDisplay* aWaylandDisplay,
+ int aSize);
+
+ wl_shm_pool* GetShmPool() { return mShmPool; };
+ void* GetImageData();
+
+ private:
+ WaylandShmPool() = default;
+ ~WaylandShmPool();
+
+ wl_shm_pool* mShmPool = nullptr;
+ void* mImageData = nullptr;
+ UniquePtr<base::SharedMemory> mShm;
+ int mSize = 0;
+};
+
+class WaylandBuffer {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaylandBuffer);
+
+ void AttachAndCommit(wl_surface* aSurface);
+ virtual wl_buffer* GetWlBuffer() = 0;
+ virtual already_AddRefed<gfx::DrawTarget> Lock() { return nullptr; };
+ virtual void* GetImageData() { return nullptr; }
+ virtual GLuint GetTexture() { return 0; }
+ virtual void DestroyGLResources(){};
+
+ LayoutDeviceIntSize GetSize() { return mSize; };
+ bool IsMatchingSize(const LayoutDeviceIntSize& aSize) {
+ return aSize == mSize;
+ }
+ bool IsAttached() { return mAttached; }
+ static gfx::SurfaceFormat GetSurfaceFormat() { return mFormat; }
+
+ static void BufferReleaseCallbackHandler(void* aData, wl_buffer* aBuffer);
+ void SetBufferReleaseFunc(void (*aBufferReleaseFunc)(void* aData,
+ wl_buffer* aBuffer)) {
+ mBufferReleaseFunc = aBufferReleaseFunc;
+ }
+ void SetBufferReleaseData(void* aBufferReleaseData) {
+ mBufferReleaseData = aBufferReleaseData;
+ }
+
+ protected:
+ explicit WaylandBuffer(const LayoutDeviceIntSize& aSize);
+ virtual ~WaylandBuffer() = default;
+
+ void BufferReleaseCallbackHandler(wl_buffer* aBuffer);
+
+ LayoutDeviceIntSize mSize;
+ bool mAttached = false;
+ void (*mBufferReleaseFunc)(void* aData, wl_buffer* aBuffer) = nullptr;
+ void* mBufferReleaseData = nullptr;
+ static gfx::SurfaceFormat mFormat;
+};
+
+// Holds actual graphics data for wl_surface
+class WaylandBufferSHM final : public WaylandBuffer {
+ public:
+ static RefPtr<WaylandBufferSHM> Create(const LayoutDeviceIntSize& aSize);
+
+ wl_buffer* GetWlBuffer() override { return mWLBuffer; };
+ already_AddRefed<gfx::DrawTarget> Lock() override;
+ void* GetImageData() override { return mShmPool->GetImageData(); }
+
+ void Clear();
+ size_t GetBufferAge() { return mBufferAge; };
+ RefPtr<WaylandShmPool> GetShmPool() { return mShmPool; }
+
+ void IncrementBufferAge() { mBufferAge++; };
+ void ResetBufferAge() { mBufferAge = 0; };
+
+#ifdef MOZ_LOGGING
+ void DumpToFile(const char* aHint);
+#endif
+
+ private:
+ explicit WaylandBufferSHM(const LayoutDeviceIntSize& aSize);
+ ~WaylandBufferSHM() override;
+
+ // WaylandShmPoolMB provides actual shared memory we draw into
+ RefPtr<WaylandShmPool> mShmPool;
+
+ // wl_buffer is a wayland object that encapsulates the shared memory
+ // and passes it to wayland compositor by wl_surface object.
+ wl_buffer* mWLBuffer = nullptr;
+
+ size_t mBufferAge = 0;
+
+#ifdef MOZ_LOGGING
+ static int mDumpSerial;
+ static char* mDumpDir;
+#endif
+};
+
+class WaylandBufferDMABUF final : public WaylandBuffer {
+ public:
+ static RefPtr<WaylandBufferDMABUF> Create(const LayoutDeviceIntSize& aSize,
+ gl::GLContext* aGL);
+
+ wl_buffer* GetWlBuffer() override { return mDMABufSurface->GetWlBuffer(); };
+ GLuint GetTexture() override { return mDMABufSurface->GetTexture(); };
+ void DestroyGLResources() override { mDMABufSurface->ReleaseTextures(); };
+
+ private:
+ explicit WaylandBufferDMABUF(const LayoutDeviceIntSize& aSize);
+ ~WaylandBufferDMABUF() = default;
+
+ RefPtr<DMABufSurfaceRGBA> mDMABufSurface;
+};
+
+} // namespace mozilla::widget
+
+#endif // _MOZILLA_WIDGET_GTK_WAYLAND_BUFFER_H
diff --git a/widget/gtk/WaylandVsyncSource.cpp b/widget/gtk/WaylandVsyncSource.cpp
new file mode 100644
index 0000000000..e5bca04837
--- /dev/null
+++ b/widget/gtk/WaylandVsyncSource.cpp
@@ -0,0 +1,431 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifdef MOZ_WAYLAND
+
+# include "WaylandVsyncSource.h"
+# include "mozilla/UniquePtr.h"
+# include "nsThreadUtils.h"
+# include "nsISupportsImpl.h"
+# include "MainThreadUtils.h"
+# include "mozilla/ScopeExit.h"
+# include "nsGtkUtils.h"
+# include "mozilla/StaticPrefs_layout.h"
+# include "mozilla/StaticPrefs_widget.h"
+# include "nsWindow.h"
+
+# include <gdk/gdkwayland.h>
+
+# ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gWidgetVsync;
+# undef LOG
+# define LOG(str, ...) \
+ MOZ_LOG(gWidgetVsync, mozilla::LogLevel::Debug, \
+ ("[nsWindow %p]: " str, GetWindowForLogging(), ##__VA_ARGS__))
+# else
+# define LOG(...)
+# endif /* MOZ_LOGGING */
+
+using namespace mozilla::widget;
+
+namespace mozilla {
+
+static void WaylandVsyncSourceCallbackHandler(void* aData,
+ struct wl_callback* aCallback,
+ uint32_t aTime) {
+ RefPtr context = static_cast<WaylandVsyncSource*>(aData);
+ context->FrameCallback(aCallback, aTime);
+}
+
+static const struct wl_callback_listener WaylandVsyncSourceCallbackListener = {
+ WaylandVsyncSourceCallbackHandler};
+
+static void NativeLayerRootWaylandVsyncCallback(void* aData, uint32_t aTime) {
+ RefPtr context = static_cast<WaylandVsyncSource*>(aData);
+ context->FrameCallback(nullptr, aTime);
+}
+
+static float GetFPS(TimeDuration aVsyncRate) {
+ return 1000.0f / float(aVsyncRate.ToMilliseconds());
+}
+
+static nsTArray<WaylandVsyncSource*> gWaylandVsyncSources;
+
+Maybe<TimeDuration> WaylandVsyncSource::GetFastestVsyncRate() {
+ Maybe<TimeDuration> retVal;
+ for (auto* source : gWaylandVsyncSources) {
+ auto rate = source->GetVsyncRateIfEnabled();
+ if (!rate) {
+ continue;
+ }
+ if (!retVal.isSome()) {
+ retVal.emplace(*rate);
+ } else if (*rate < *retVal) {
+ retVal.ref() = *rate;
+ }
+ }
+
+ return retVal;
+}
+
+WaylandVsyncSource::WaylandVsyncSource(nsWindow* aWindow)
+ : mMutex("WaylandVsyncSource"),
+ mVsyncRate(TimeDuration::FromMilliseconds(1000.0 / 60.0)),
+ mLastVsyncTimeStamp(TimeStamp::Now()),
+ mWindow(aWindow),
+ mIdleTimeout(1000 / StaticPrefs::layout_throttled_frame_rate()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ gWaylandVsyncSources.AppendElement(this);
+}
+
+WaylandVsyncSource::~WaylandVsyncSource() {
+ gWaylandVsyncSources.RemoveElement(this);
+}
+
+void WaylandVsyncSource::MaybeUpdateSource(MozContainer* aContainer) {
+ MutexAutoLock lock(mMutex);
+
+ LOG("WaylandVsyncSource::MaybeUpdateSource fps %f", GetFPS(mVsyncRate));
+
+ if (aContainer == mContainer) {
+ LOG(" mContainer is the same, quit.");
+ return;
+ }
+
+ mNativeLayerRoot = nullptr;
+ mContainer = aContainer;
+
+ if (mMonitorEnabled) {
+ LOG(" monitor enabled, ask for Refresh()");
+ mCallbackRequested = false;
+ Refresh(lock);
+ }
+}
+
+void WaylandVsyncSource::MaybeUpdateSource(
+ const RefPtr<NativeLayerRootWayland>& aNativeLayerRoot) {
+ MutexAutoLock lock(mMutex);
+
+ LOG("WaylandVsyncSource::MaybeUpdateSource aNativeLayerRoot fps %f",
+ GetFPS(mVsyncRate));
+
+ if (aNativeLayerRoot == mNativeLayerRoot) {
+ LOG(" mNativeLayerRoot is the same, quit.");
+ return;
+ }
+
+ mNativeLayerRoot = aNativeLayerRoot;
+ mContainer = nullptr;
+
+ if (mMonitorEnabled) {
+ LOG(" monitor enabled, ask for Refresh()");
+ mCallbackRequested = false;
+ Refresh(lock);
+ }
+}
+
+void WaylandVsyncSource::Refresh(const MutexAutoLock& aProofOfLock) {
+ mMutex.AssertCurrentThreadOwns();
+
+ LOG("WaylandVsyncSource::Refresh fps %f\n", GetFPS(mVsyncRate));
+
+ if (!(mContainer || mNativeLayerRoot) || !mMonitorEnabled || !mVsyncEnabled ||
+ mCallbackRequested) {
+ // We don't need to do anything because:
+ // * We are unwanted by our widget or monitor, or
+ // * The last frame callback hasn't yet run to see that it had been shut
+ // down, so we can reuse it after having set mVsyncEnabled to true.
+ LOG(" quit mContainer %d mNativeLayerRoot %d mMonitorEnabled %d "
+ "mVsyncEnabled %d mCallbackRequested %d",
+ !!mContainer, !!mNativeLayerRoot, mMonitorEnabled, mVsyncEnabled,
+ !!mCallbackRequested);
+ return;
+ }
+
+ if (mContainer) {
+ MozContainerSurfaceLock lock(mContainer);
+ struct wl_surface* surface = lock.GetSurface();
+ LOG(" refresh from mContainer, wl_surface %p", surface);
+ if (!surface) {
+ LOG(" we're missing wl_surface, register Refresh() callback");
+ // The surface hasn't been created yet. Try again when the surface is
+ // ready.
+ RefPtr<WaylandVsyncSource> self(this);
+ moz_container_wayland_add_initial_draw_callback_locked(
+ mContainer, [self]() -> void {
+ MutexAutoLock lock(self->mMutex);
+ self->Refresh(lock);
+ });
+ return;
+ }
+ }
+
+ // Vsync is enabled, but we don't have a callback configured. Set one up so
+ // we can get to work.
+ SetupFrameCallback(aProofOfLock);
+ const TimeStamp lastVSync = TimeStamp::Now();
+ mLastVsyncTimeStamp = lastVSync;
+ TimeStamp outputTimestamp = mLastVsyncTimeStamp + mVsyncRate;
+
+ {
+ MutexAutoUnlock unlock(mMutex);
+ NotifyVsync(lastVSync, outputTimestamp);
+ }
+}
+
+void WaylandVsyncSource::EnableMonitor() {
+ LOG("WaylandVsyncSource::EnableMonitor");
+ MutexAutoLock lock(mMutex);
+ if (mMonitorEnabled) {
+ return;
+ }
+ mMonitorEnabled = true;
+ Refresh(lock);
+}
+
+void WaylandVsyncSource::DisableMonitor() {
+ LOG("WaylandVsyncSource::DisableMonitor");
+ MutexAutoLock lock(mMutex);
+ if (!mMonitorEnabled) {
+ return;
+ }
+ mMonitorEnabled = false;
+ mCallbackRequested = false;
+}
+
+void WaylandVsyncSource::SetupFrameCallback(const MutexAutoLock& aProofOfLock) {
+ mMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(!mCallbackRequested);
+
+ LOG("WaylandVsyncSource::SetupFrameCallback");
+
+ if (mNativeLayerRoot) {
+ LOG(" use mNativeLayerRoot");
+ mNativeLayerRoot->RequestFrameCallback(&NativeLayerRootWaylandVsyncCallback,
+ this);
+ } else {
+ MozContainerSurfaceLock lock(mContainer);
+ struct wl_surface* surface = lock.GetSurface();
+ LOG(" use mContainer, wl_surface %p", surface);
+ if (!surface) {
+ // We don't have a surface, either due to being called before it was made
+ // available in the mozcontainer, or after it was destroyed. We're all
+ // done regardless.
+ LOG(" missing wl_surface, quit.");
+ return;
+ }
+
+ LOG(" register frame callback");
+ MozClearPointer(mCallback, wl_callback_destroy);
+ mCallback = wl_surface_frame(surface);
+ wl_callback_add_listener(mCallback, &WaylandVsyncSourceCallbackListener,
+ this);
+ wl_surface_commit(surface);
+ wl_display_flush(WaylandDisplayGet()->GetDisplay());
+
+ if (!mIdleTimerID) {
+ mIdleTimerID = g_timeout_add(
+ mIdleTimeout,
+ [](void* data) -> gint {
+ RefPtr vsync = static_cast<WaylandVsyncSource*>(data);
+ if (vsync->IdleCallback()) {
+ // We want to fire again, so don't clear mIdleTimerID
+ return G_SOURCE_CONTINUE;
+ }
+ // No need for g_source_remove, caller does it for us.
+ vsync->mIdleTimerID = 0;
+ return G_SOURCE_REMOVE;
+ },
+ this);
+ }
+ }
+
+ mCallbackRequested = true;
+}
+
+bool WaylandVsyncSource::IdleCallback() {
+ LOG("WaylandVsyncSource::IdleCallback");
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ RefPtr<nsWindow> window;
+ TimeStamp lastVSync;
+ TimeStamp outputTimestamp;
+ {
+ MutexAutoLock lock(mMutex);
+
+ if (!mVsyncEnabled || !mMonitorEnabled) {
+ // We are unwanted by either our creator or our consumer, so we just stop
+ // here without setting up a new frame callback.
+ LOG(" quit, mVsyncEnabled %d mMonitorEnabled %d", mVsyncEnabled,
+ mMonitorEnabled);
+ return false;
+ }
+
+ const auto now = TimeStamp::Now();
+ const auto timeSinceLastVSync = now - mLastVsyncTimeStamp;
+ if (timeSinceLastVSync.ToMilliseconds() < mIdleTimeout) {
+ // We're not idle, we want to fire the timer again.
+ return true;
+ }
+
+ LOG(" fire idle vsync");
+ CalculateVsyncRate(lock, now);
+ mLastVsyncTimeStamp = lastVSync = now;
+
+ outputTimestamp = mLastVsyncTimeStamp + mVsyncRate;
+ window = mWindow;
+ }
+
+ // This could disable vsync.
+ window->NotifyOcclusionState(OcclusionState::OCCLUDED);
+
+ if (window->IsDestroyed()) {
+ return false;
+ }
+ // Make sure to fire vsync now even if we get disabled afterwards.
+ // This gives an opportunity to clean up after the visibility state change.
+ // FIXME: Do we really need to do this?
+ NotifyVsync(lastVSync, outputTimestamp);
+ return StaticPrefs::widget_wayland_vsync_keep_firing_at_idle();
+}
+
+void WaylandVsyncSource::FrameCallback(wl_callback* aCallback, uint32_t aTime) {
+ LOG("WaylandVsyncSource::FrameCallback");
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ {
+ // This might enable vsync.
+ RefPtr window = mWindow;
+ window->NotifyOcclusionState(OcclusionState::VISIBLE);
+ // NotifyOcclusionState can destroy us.
+ if (window->IsDestroyed()) {
+ return;
+ }
+ }
+
+ MutexAutoLock lock(mMutex);
+ mCallbackRequested = false;
+
+ // NotifyOcclusionState() can clear and create new mCallback by
+ // EnableVsync()/Refresh(). So don't delete newly created frame callback.
+ if (aCallback && aCallback == mCallback) {
+ MozClearPointer(mCallback, wl_callback_destroy);
+ }
+
+ if (!mVsyncEnabled || !mMonitorEnabled) {
+ // We are unwanted by either our creator or our consumer, so we just stop
+ // here without setting up a new frame callback.
+ LOG(" quit, mVsyncEnabled %d mMonitorEnabled %d", mVsyncEnabled,
+ mMonitorEnabled);
+ return;
+ }
+
+ // Configure our next frame callback.
+ SetupFrameCallback(lock);
+
+ int64_t tick = BaseTimeDurationPlatformUtils::TicksFromMilliseconds(aTime);
+ const auto callbackTimeStamp = TimeStamp::FromSystemTime(tick);
+ const auto now = TimeStamp::Now();
+
+ // If the callback timestamp is close enough to our timestamp, use it,
+ // otherwise use the current time.
+ const TimeStamp& vsyncTimestamp =
+ std::abs((now - callbackTimeStamp).ToMilliseconds()) < 50.0
+ ? callbackTimeStamp
+ : now;
+
+ CalculateVsyncRate(lock, vsyncTimestamp);
+ mLastVsyncTimeStamp = vsyncTimestamp;
+ const TimeStamp outputTimestamp = vsyncTimestamp + mVsyncRate;
+
+ {
+ MutexAutoUnlock unlock(mMutex);
+ NotifyVsync(vsyncTimestamp, outputTimestamp);
+ }
+}
+
+TimeDuration WaylandVsyncSource::GetVsyncRate() {
+ MutexAutoLock lock(mMutex);
+ return mVsyncRate;
+}
+
+Maybe<TimeDuration> WaylandVsyncSource::GetVsyncRateIfEnabled() {
+ MutexAutoLock lock(mMutex);
+ if (!mVsyncEnabled) {
+ return Nothing();
+ }
+ return Some(mVsyncRate);
+}
+
+void WaylandVsyncSource::CalculateVsyncRate(const MutexAutoLock& aProofOfLock,
+ TimeStamp aVsyncTimestamp) {
+ mMutex.AssertCurrentThreadOwns();
+
+ double duration = (aVsyncTimestamp - mLastVsyncTimeStamp).ToMilliseconds();
+ double curVsyncRate = mVsyncRate.ToMilliseconds();
+
+ LOG("WaylandVsyncSource::CalculateVsyncRate start fps %f\n",
+ GetFPS(mVsyncRate));
+
+ double correction;
+ if (duration > curVsyncRate) {
+ correction = fmin(curVsyncRate, (duration - curVsyncRate) / 10);
+ mVsyncRate += TimeDuration::FromMilliseconds(correction);
+ } else {
+ correction = fmin(curVsyncRate / 2, (curVsyncRate - duration) / 10);
+ mVsyncRate -= TimeDuration::FromMilliseconds(correction);
+ }
+
+ LOG(" new fps %f correction %f\n", GetFPS(mVsyncRate), correction);
+}
+
+void WaylandVsyncSource::EnableVsync() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(mMutex);
+
+ LOG("WaylandVsyncSource::EnableVsync fps %f\n", GetFPS(mVsyncRate));
+ if (mVsyncEnabled || mIsShutdown) {
+ LOG(" early quit");
+ return;
+ }
+ mVsyncEnabled = true;
+ Refresh(lock);
+}
+
+void WaylandVsyncSource::DisableVsync() {
+ MutexAutoLock lock(mMutex);
+
+ LOG("WaylandVsyncSource::DisableVsync fps %f\n", GetFPS(mVsyncRate));
+ mVsyncEnabled = false;
+ mCallbackRequested = false;
+}
+
+bool WaylandVsyncSource::IsVsyncEnabled() {
+ MutexAutoLock lock(mMutex);
+ return mVsyncEnabled;
+}
+
+void WaylandVsyncSource::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+
+ LOG("WaylandVsyncSource::Shutdown fps %f\n", GetFPS(mVsyncRate));
+ mContainer = nullptr;
+ mNativeLayerRoot = nullptr;
+ mIsShutdown = true;
+ mVsyncEnabled = false;
+ mCallbackRequested = false;
+ MozClearHandleID(mIdleTimerID, g_source_remove);
+ MozClearPointer(mCallback, wl_callback_destroy);
+}
+
+} // namespace mozilla
+
+#endif // MOZ_WAYLAND
diff --git a/widget/gtk/WaylandVsyncSource.h b/widget/gtk/WaylandVsyncSource.h
new file mode 100644
index 0000000000..78fe72e445
--- /dev/null
+++ b/widget/gtk/WaylandVsyncSource.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 20; 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/. */
+
+#ifndef _WaylandVsyncSource_h_
+#define _WaylandVsyncSource_h_
+
+#include "base/thread.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/layers/NativeLayerWayland.h"
+#include "MozContainer.h"
+#include "nsWaylandDisplay.h"
+#include "VsyncSource.h"
+
+namespace mozilla {
+
+using layers::NativeLayerRootWayland;
+
+/*
+ * WaylandVsyncSource
+ *
+ * This class provides a per-widget VsyncSource under Wayland, emulated using
+ * frame callbacks on the widget surface with empty surface commits.
+ *
+ * Wayland does not expose vsync/vblank, as it considers that an implementation
+ * detail the clients should not concern themselves with. Instead, frame
+ * callbacks are provided whenever the compositor believes it is a good time to
+ * start drawing the next frame for a particular surface, giving us as much
+ * time as possible to do so.
+ *
+ * Note that the compositor sends frame callbacks only when it sees fit, and
+ * when that may be is entirely up to the compositor. One cannot expect a
+ * certain rate of callbacks, or any callbacks at all. Examples of common
+ * variations would be surfaces moved between outputs with different refresh
+ * rates, and surfaces that are hidden and therefore do not receieve any
+ * callbacks at all. Other hypothetical scenarios of variation could be
+ * throttling to conserve power, or because a user has requested it.
+ *
+ */
+class WaylandVsyncSource final : public gfx::VsyncSource {
+ public:
+ explicit WaylandVsyncSource(nsWindow* aWindow);
+ virtual ~WaylandVsyncSource();
+
+ static Maybe<TimeDuration> GetFastestVsyncRate();
+
+ void MaybeUpdateSource(MozContainer* aContainer);
+ void MaybeUpdateSource(
+ const RefPtr<NativeLayerRootWayland>& aNativeLayerRoot);
+
+ void EnableMonitor();
+ void DisableMonitor();
+
+ void FrameCallback(wl_callback* aCallback, uint32_t aTime);
+ // Returns whether we should keep firing.
+ bool IdleCallback();
+
+ TimeDuration GetVsyncRate() override;
+
+ void EnableVsync() override;
+
+ void DisableVsync() override;
+
+ bool IsVsyncEnabled() override;
+
+ void Shutdown() override;
+
+ private:
+ Maybe<TimeDuration> GetVsyncRateIfEnabled();
+
+ void Refresh(const MutexAutoLock& aProofOfLock);
+ void SetupFrameCallback(const MutexAutoLock& aProofOfLock);
+ void CalculateVsyncRate(const MutexAutoLock& aProofOfLock,
+ TimeStamp aVsyncTimestamp);
+ void* GetWindowForLogging() { return mWindow; };
+
+ Mutex mMutex;
+ bool mIsShutdown MOZ_GUARDED_BY(mMutex) = false;
+ bool mVsyncEnabled MOZ_GUARDED_BY(mMutex) = false;
+ bool mMonitorEnabled MOZ_GUARDED_BY(mMutex) = false;
+ bool mCallbackRequested MOZ_GUARDED_BY(mMutex) = false;
+ MozContainer* mContainer MOZ_GUARDED_BY(mMutex) = nullptr;
+ RefPtr<NativeLayerRootWayland> mNativeLayerRoot MOZ_GUARDED_BY(mMutex);
+ TimeDuration mVsyncRate MOZ_GUARDED_BY(mMutex);
+ TimeStamp mLastVsyncTimeStamp MOZ_GUARDED_BY(mMutex);
+ wl_callback* mCallback MOZ_GUARDED_BY(mMutex) = nullptr;
+
+ guint mIdleTimerID = 0; // Main thread only.
+ nsWindow* const mWindow; // Main thread only, except for logging.
+ const guint mIdleTimeout;
+};
+
+} // namespace mozilla
+
+#endif // _WaylandVsyncSource_h_
diff --git a/widget/gtk/WidgetStyleCache.cpp b/widget/gtk/WidgetStyleCache.cpp
new file mode 100644
index 0000000000..b7c1cf8a9f
--- /dev/null
+++ b/widget/gtk/WidgetStyleCache.cpp
@@ -0,0 +1,1434 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 <dlfcn.h>
+#include <gtk/gtk.h>
+#include "WidgetStyleCache.h"
+#include "gtkdrawing.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/ScopeExit.h"
+#include "nsDebug.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+
+#define STATE_FLAG_DIR_LTR (1U << 7)
+#define STATE_FLAG_DIR_RTL (1U << 8)
+static_assert(GTK_STATE_FLAG_DIR_LTR == STATE_FLAG_DIR_LTR &&
+ GTK_STATE_FLAG_DIR_RTL == STATE_FLAG_DIR_RTL,
+ "incorrect direction state flags");
+
+enum class CSDStyle {
+ Unknown,
+ Solid,
+ Normal,
+};
+
+static bool gHeaderBarShouldDrawContainer = false;
+static bool gMaximizedHeaderBarShouldDrawContainer = false;
+static CSDStyle gCSDStyle = CSDStyle::Unknown;
+static GtkWidget* sWidgetStorage[MOZ_GTK_WIDGET_NODE_COUNT];
+static GtkStyleContext* sStyleStorage[MOZ_GTK_WIDGET_NODE_COUNT];
+
+static GtkStyleContext* GetWidgetRootStyle(WidgetNodeType aNodeType);
+static GtkStyleContext* GetCssNodeStyleInternal(WidgetNodeType aNodeType);
+
+static GtkWidget* CreateWindowWidget() {
+ GtkWidget* widget = gtk_window_new(GTK_WINDOW_POPUP);
+ MOZ_RELEASE_ASSERT(widget, "We're missing GtkWindow widget!");
+ gtk_widget_set_name(widget, "MozillaGtkWidget");
+ return widget;
+}
+
+static GtkWidget* CreateWindowContainerWidget() {
+ GtkWidget* widget = gtk_fixed_new();
+ gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_WINDOW)), widget);
+ return widget;
+}
+
+static void AddToWindowContainer(GtkWidget* widget) {
+ gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_WINDOW_CONTAINER)), widget);
+}
+
+static GtkWidget* CreateScrollbarWidget(WidgetNodeType aAppearance,
+ GtkOrientation aOrientation) {
+ GtkWidget* widget = gtk_scrollbar_new(aOrientation, nullptr);
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateCheckboxWidget() {
+ GtkWidget* widget = gtk_check_button_new_with_label("M");
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateRadiobuttonWidget() {
+ GtkWidget* widget = gtk_radio_button_new_with_label(nullptr, "M");
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateMenuPopupWidget() {
+ GtkWidget* widget = gtk_menu_new();
+ GtkStyleContext* style = gtk_widget_get_style_context(widget);
+ gtk_style_context_add_class(style, GTK_STYLE_CLASS_POPUP);
+ gtk_menu_attach_to_widget(GTK_MENU(widget), GetWidget(MOZ_GTK_WINDOW),
+ nullptr);
+ return widget;
+}
+
+static GtkWidget* CreateMenuBarWidget() {
+ GtkWidget* widget = gtk_menu_bar_new();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateProgressWidget() {
+ GtkWidget* widget = gtk_progress_bar_new();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateTooltipWidget() {
+ MOZ_ASSERT(gtk_check_version(3, 20, 0) != nullptr,
+ "CreateTooltipWidget should be used for Gtk < 3.20 only.");
+ GtkWidget* widget = CreateWindowWidget();
+ GtkStyleContext* style = gtk_widget_get_style_context(widget);
+ gtk_style_context_add_class(style, GTK_STYLE_CLASS_TOOLTIP);
+ return widget;
+}
+
+static GtkWidget* CreateExpanderWidget() {
+ GtkWidget* widget = gtk_expander_new("M");
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateFrameWidget() {
+ GtkWidget* widget = gtk_frame_new(nullptr);
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateButtonWidget() {
+ GtkWidget* widget = gtk_button_new_with_label("M");
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateToggleButtonWidget() {
+ GtkWidget* widget = gtk_toggle_button_new();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateButtonArrowWidget() {
+ GtkWidget* widget = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_OUT);
+ gtk_container_add(GTK_CONTAINER(GetWidget(MOZ_GTK_TOGGLE_BUTTON)), widget);
+ gtk_widget_show(widget);
+ return widget;
+}
+
+static GtkWidget* CreateSpinWidget() {
+ GtkWidget* widget = gtk_spin_button_new(nullptr, 1, 0);
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateEntryWidget() {
+ GtkWidget* widget = gtk_entry_new();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateComboBoxWidget() {
+ GtkWidget* widget = gtk_combo_box_new();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+typedef struct {
+ GType type;
+ GtkWidget** widget;
+} GtkInnerWidgetInfo;
+
+static void GetInnerWidget(GtkWidget* widget, gpointer client_data) {
+ auto info = static_cast<GtkInnerWidgetInfo*>(client_data);
+
+ if (G_TYPE_CHECK_INSTANCE_TYPE(widget, info->type)) {
+ *info->widget = widget;
+ }
+}
+
+static GtkWidget* CreateComboBoxButtonWidget() {
+ GtkWidget* comboBox = GetWidget(MOZ_GTK_COMBOBOX);
+ GtkWidget* comboBoxButton = nullptr;
+
+ /* Get its inner Button */
+ GtkInnerWidgetInfo info = {GTK_TYPE_TOGGLE_BUTTON, &comboBoxButton};
+ gtk_container_forall(GTK_CONTAINER(comboBox), GetInnerWidget, &info);
+
+ if (!comboBoxButton) {
+ /* Shouldn't be reached with current internal gtk implementation; we
+ * use a generic toggle button as last resort fallback to avoid
+ * crashing. */
+ comboBoxButton = GetWidget(MOZ_GTK_TOGGLE_BUTTON);
+ } else {
+ /* We need to have pointers to the inner widgets (button, separator, arrow)
+ * of the ComboBox to get the correct rendering from theme engines which
+ * special cases their look. Since the inner layout can change, we ask GTK
+ * to NULL our pointers when they are about to become invalid because the
+ * corresponding widgets don't exist anymore. It's the role of
+ * g_object_add_weak_pointer().
+ * Note that if we don't find the inner widgets (which shouldn't happen), we
+ * fallback to use generic "non-inner" widgets, and they don't need that
+ * kind of weak pointer since they are explicit children of gProtoLayout and
+ * as such GTK holds a strong reference to them. */
+ g_object_add_weak_pointer(
+ G_OBJECT(comboBoxButton),
+ reinterpret_cast<gpointer*>(sWidgetStorage) + MOZ_GTK_COMBOBOX_BUTTON);
+ }
+
+ return comboBoxButton;
+}
+
+static GtkWidget* CreateComboBoxArrowWidget() {
+ GtkWidget* comboBoxButton = GetWidget(MOZ_GTK_COMBOBOX_BUTTON);
+ GtkWidget* comboBoxArrow = nullptr;
+
+ /* Get the widgets inside the Button */
+ GtkWidget* buttonChild = gtk_bin_get_child(GTK_BIN(comboBoxButton));
+ if (GTK_IS_BOX(buttonChild)) {
+ /* appears-as-list = FALSE, cell-view = TRUE; the button
+ * contains an hbox. This hbox is there because the ComboBox
+ * needs to place a cell renderer, a separator, and an arrow in
+ * the button when appears-as-list is FALSE. */
+ GtkInnerWidgetInfo info = {GTK_TYPE_ARROW, &comboBoxArrow};
+ gtk_container_forall(GTK_CONTAINER(buttonChild), GetInnerWidget, &info);
+ } else if (GTK_IS_ARROW(buttonChild)) {
+ /* appears-as-list = TRUE, or cell-view = FALSE;
+ * the button only contains an arrow */
+ comboBoxArrow = buttonChild;
+ }
+
+ if (!comboBoxArrow) {
+ /* Shouldn't be reached with current internal gtk implementation;
+ * we gButtonArrowWidget as last resort fallback to avoid
+ * crashing. */
+ comboBoxArrow = GetWidget(MOZ_GTK_BUTTON_ARROW);
+ } else {
+ g_object_add_weak_pointer(
+ G_OBJECT(comboBoxArrow),
+ reinterpret_cast<gpointer*>(sWidgetStorage) + MOZ_GTK_COMBOBOX_ARROW);
+ }
+
+ return comboBoxArrow;
+}
+
+static GtkWidget* CreateComboBoxSeparatorWidget() {
+ // Ensure to search for separator only once as it can fail
+ // TODO - it won't initialize after ResetWidgetCache() call
+ static bool isMissingSeparator = false;
+ if (isMissingSeparator) return nullptr;
+
+ /* Get the widgets inside the Button */
+ GtkWidget* comboBoxSeparator = nullptr;
+ GtkWidget* buttonChild =
+ gtk_bin_get_child(GTK_BIN(GetWidget(MOZ_GTK_COMBOBOX_BUTTON)));
+ if (GTK_IS_BOX(buttonChild)) {
+ /* appears-as-list = FALSE, cell-view = TRUE; the button
+ * contains an hbox. This hbox is there because the ComboBox
+ * needs to place a cell renderer, a separator, and an arrow in
+ * the button when appears-as-list is FALSE. */
+ GtkInnerWidgetInfo info = {GTK_TYPE_SEPARATOR, &comboBoxSeparator};
+ gtk_container_forall(GTK_CONTAINER(buttonChild), GetInnerWidget, &info);
+ }
+
+ if (comboBoxSeparator) {
+ g_object_add_weak_pointer(G_OBJECT(comboBoxSeparator),
+ reinterpret_cast<gpointer*>(sWidgetStorage) +
+ MOZ_GTK_COMBOBOX_SEPARATOR);
+ } else {
+ /* comboBoxSeparator may be NULL
+ * when "appears-as-list" = TRUE or "cell-view" = FALSE;
+ * if there is no separator, then we just won't paint it. */
+ isMissingSeparator = true;
+ }
+
+ return comboBoxSeparator;
+}
+
+static GtkWidget* CreateComboBoxEntryWidget() {
+ GtkWidget* widget = gtk_combo_box_new_with_entry();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateComboBoxEntryTextareaWidget() {
+ GtkWidget* comboBoxTextarea = nullptr;
+
+ /* Get its inner Entry and Button */
+ GtkInnerWidgetInfo info = {GTK_TYPE_ENTRY, &comboBoxTextarea};
+ gtk_container_forall(GTK_CONTAINER(GetWidget(MOZ_GTK_COMBOBOX_ENTRY)),
+ GetInnerWidget, &info);
+
+ if (!comboBoxTextarea) {
+ comboBoxTextarea = GetWidget(MOZ_GTK_ENTRY);
+ } else {
+ g_object_add_weak_pointer(
+ G_OBJECT(comboBoxTextarea),
+ reinterpret_cast<gpointer*>(sWidgetStorage) + MOZ_GTK_COMBOBOX_ENTRY);
+ }
+
+ return comboBoxTextarea;
+}
+
+static GtkWidget* CreateComboBoxEntryButtonWidget() {
+ GtkWidget* comboBoxButton = nullptr;
+
+ /* Get its inner Entry and Button */
+ GtkInnerWidgetInfo info = {GTK_TYPE_TOGGLE_BUTTON, &comboBoxButton};
+ gtk_container_forall(GTK_CONTAINER(GetWidget(MOZ_GTK_COMBOBOX_ENTRY)),
+ GetInnerWidget, &info);
+
+ if (!comboBoxButton) {
+ comboBoxButton = GetWidget(MOZ_GTK_TOGGLE_BUTTON);
+ } else {
+ g_object_add_weak_pointer(G_OBJECT(comboBoxButton),
+ reinterpret_cast<gpointer*>(sWidgetStorage) +
+ MOZ_GTK_COMBOBOX_ENTRY_BUTTON);
+ }
+
+ return comboBoxButton;
+}
+
+static GtkWidget* CreateComboBoxEntryArrowWidget() {
+ GtkWidget* comboBoxArrow = nullptr;
+
+ /* Get the Arrow inside the Button */
+ GtkWidget* buttonChild =
+ gtk_bin_get_child(GTK_BIN(GetWidget(MOZ_GTK_COMBOBOX_ENTRY_BUTTON)));
+
+ if (GTK_IS_BOX(buttonChild)) {
+ /* appears-as-list = FALSE, cell-view = TRUE; the button
+ * contains an hbox. This hbox is there because the ComboBox
+ * needs to place a cell renderer, a separator, and an arrow in
+ * the button when appears-as-list is FALSE. */
+ GtkInnerWidgetInfo info = {GTK_TYPE_ARROW, &comboBoxArrow};
+ gtk_container_forall(GTK_CONTAINER(buttonChild), GetInnerWidget, &info);
+ } else if (GTK_IS_ARROW(buttonChild)) {
+ /* appears-as-list = TRUE, or cell-view = FALSE;
+ * the button only contains an arrow */
+ comboBoxArrow = buttonChild;
+ }
+
+ if (!comboBoxArrow) {
+ /* Shouldn't be reached with current internal gtk implementation;
+ * we gButtonArrowWidget as last resort fallback to avoid
+ * crashing. */
+ comboBoxArrow = GetWidget(MOZ_GTK_BUTTON_ARROW);
+ } else {
+ g_object_add_weak_pointer(G_OBJECT(comboBoxArrow),
+ reinterpret_cast<gpointer*>(sWidgetStorage) +
+ MOZ_GTK_COMBOBOX_ENTRY_ARROW);
+ }
+
+ return comboBoxArrow;
+}
+
+static GtkWidget* CreateScrolledWindowWidget() {
+ GtkWidget* widget = gtk_scrolled_window_new(nullptr, nullptr);
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateTreeViewWidget() {
+ GtkWidget* widget = gtk_tree_view_new();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateTreeHeaderCellWidget() {
+ /*
+ * Some GTK engines paint the first and last cell
+ * of a TreeView header with a highlight.
+ * Since we do not know where our widget will be relative
+ * to the other buttons in the TreeView header, we must
+ * paint it as a button that is between two others,
+ * thus ensuring it is neither the first or last button
+ * in the header.
+ * GTK doesn't give us a way to do this explicitly,
+ * so we must paint with a button that is between two
+ * others.
+ */
+ GtkTreeViewColumn* firstTreeViewColumn;
+ GtkTreeViewColumn* middleTreeViewColumn;
+ GtkTreeViewColumn* lastTreeViewColumn;
+
+ GtkWidget* treeView = GetWidget(MOZ_GTK_TREEVIEW);
+
+ /* Create and append our three columns */
+ firstTreeViewColumn = gtk_tree_view_column_new();
+ gtk_tree_view_column_set_title(firstTreeViewColumn, "M");
+ gtk_tree_view_append_column(GTK_TREE_VIEW(treeView), firstTreeViewColumn);
+
+ middleTreeViewColumn = gtk_tree_view_column_new();
+ gtk_tree_view_column_set_title(middleTreeViewColumn, "M");
+ gtk_tree_view_append_column(GTK_TREE_VIEW(treeView), middleTreeViewColumn);
+
+ lastTreeViewColumn = gtk_tree_view_column_new();
+ gtk_tree_view_column_set_title(lastTreeViewColumn, "M");
+ gtk_tree_view_append_column(GTK_TREE_VIEW(treeView), lastTreeViewColumn);
+
+ /* Use the middle column's header for our button */
+ return gtk_tree_view_column_get_button(middleTreeViewColumn);
+}
+
+static GtkWidget* CreateHPanedWidget() {
+ GtkWidget* widget = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateVPanedWidget() {
+ GtkWidget* widget = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateScaleWidget(GtkOrientation aOrientation) {
+ GtkWidget* widget = gtk_scale_new(aOrientation, nullptr);
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static GtkWidget* CreateNotebookWidget() {
+ GtkWidget* widget = gtk_notebook_new();
+ AddToWindowContainer(widget);
+ return widget;
+}
+
+static bool HasBackground(GtkStyleContext* aStyle) {
+ GdkRGBA gdkColor;
+ gtk_style_context_get_background_color(aStyle, GTK_STATE_FLAG_NORMAL,
+ &gdkColor);
+ if (gdkColor.alpha != 0.0) {
+ return true;
+ }
+
+ GValue value = G_VALUE_INIT;
+ gtk_style_context_get_property(aStyle, "background-image",
+ GTK_STATE_FLAG_NORMAL, &value);
+ auto cleanup = mozilla::MakeScopeExit([&] { g_value_unset(&value); });
+ return g_value_get_boxed(&value);
+}
+
+static void CreateHeaderBarWidget(WidgetNodeType aAppearance) {
+ GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ GtkStyleContext* windowStyle = gtk_widget_get_style_context(window);
+
+ // Headerbar has to be placed to window with csd or solid-csd style
+ // to properly draw the decorated.
+ gtk_style_context_add_class(windowStyle,
+ IsSolidCSDStyleUsed() ? "solid-csd" : "csd");
+
+ GtkWidget* fixed = gtk_fixed_new();
+ GtkStyleContext* fixedStyle = gtk_widget_get_style_context(fixed);
+ gtk_style_context_add_class(fixedStyle, "titlebar");
+
+ GtkWidget* headerBar = gtk_header_bar_new();
+
+ // Emulate what create_titlebar() at gtkwindow.c does.
+ GtkStyleContext* headerBarStyle = gtk_widget_get_style_context(headerBar);
+ gtk_style_context_add_class(headerBarStyle, "titlebar");
+
+ // TODO: Define default-decoration titlebar style as workaround
+ // to ensure the titlebar buttons does not overflow outside.
+ // Recently the titlebar size is calculated as
+ // tab size + titlebar border/padding (default-decoration has 6px padding
+ // at default Adwaita theme).
+ // We need to fix titlebar size calculation to also include
+ // titlebar button sizes. (Bug 1419442)
+ gtk_style_context_add_class(headerBarStyle, "default-decoration");
+
+ sWidgetStorage[aAppearance] = headerBar;
+ if (aAppearance == MOZ_GTK_HEADER_BAR_MAXIMIZED) {
+ MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED],
+ "Window widget is already created!");
+ MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED],
+ "Fixed widget is already created!");
+
+ gtk_style_context_add_class(windowStyle, "maximized");
+
+ sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED] = window;
+ sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED] = fixed;
+ } else {
+ MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW],
+ "Window widget is already created!");
+ MOZ_ASSERT(!sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED],
+ "Fixed widget is already created!");
+ sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW] = window;
+ sWidgetStorage[MOZ_GTK_HEADERBAR_FIXED] = fixed;
+ }
+
+ gtk_container_add(GTK_CONTAINER(window), fixed);
+ gtk_container_add(GTK_CONTAINER(fixed), headerBar);
+
+ gtk_style_context_invalidate(headerBarStyle);
+ gtk_style_context_invalidate(fixedStyle);
+
+ // Some themes like Elementary's style the container of the headerbar rather
+ // than the header bar itself.
+ bool& shouldDrawContainer = aAppearance == MOZ_GTK_HEADER_BAR
+ ? gHeaderBarShouldDrawContainer
+ : gMaximizedHeaderBarShouldDrawContainer;
+ shouldDrawContainer = [&] {
+ const bool headerBarHasBackground = HasBackground(headerBarStyle);
+ if (headerBarHasBackground && GetBorderRadius(headerBarStyle)) {
+ return false;
+ }
+ if (HasBackground(fixedStyle) &&
+ (GetBorderRadius(fixedStyle) || !headerBarHasBackground)) {
+ return true;
+ }
+ return false;
+ }();
+}
+
+#define ICON_SCALE_VARIANTS 2
+
+static void LoadWidgetIconPixbuf(GtkWidget* aWidgetIcon) {
+ GtkStyleContext* style = gtk_widget_get_style_context(aWidgetIcon);
+
+ const gchar* iconName;
+ GtkIconSize gtkIconSize;
+ gtk_image_get_icon_name(GTK_IMAGE(aWidgetIcon), &iconName, &gtkIconSize);
+
+ gint iconWidth, iconHeight;
+ gtk_icon_size_lookup(gtkIconSize, &iconWidth, &iconHeight);
+
+ /* Those are available since Gtk+ 3.10 as well as GtkHeaderBar */
+ for (int scale = 1; scale < ICON_SCALE_VARIANTS + 1; scale++) {
+ GtkIconInfo* gtkIconInfo = gtk_icon_theme_lookup_icon_for_scale(
+ gtk_icon_theme_get_default(), iconName, iconWidth, scale,
+ (GtkIconLookupFlags)0);
+
+ if (!gtkIconInfo) {
+ // We miss the icon, nothing to do here.
+ return;
+ }
+
+ gboolean unused;
+ GdkPixbuf* iconPixbuf = gtk_icon_info_load_symbolic_for_context(
+ gtkIconInfo, style, &unused, nullptr);
+ g_object_unref(G_OBJECT(gtkIconInfo));
+
+ cairo_surface_t* iconSurface =
+ gdk_cairo_surface_create_from_pixbuf(iconPixbuf, scale, nullptr);
+ g_object_unref(iconPixbuf);
+
+ nsPrintfCString surfaceName("MozillaIconSurface%d", scale);
+ g_object_set_data_full(G_OBJECT(aWidgetIcon), surfaceName.get(),
+ iconSurface, (GDestroyNotify)cairo_surface_destroy);
+ }
+}
+
+cairo_surface_t* GetWidgetIconSurface(GtkWidget* aWidgetIcon, int aScale) {
+ if (aScale > ICON_SCALE_VARIANTS) {
+ aScale = ICON_SCALE_VARIANTS;
+ }
+
+ nsPrintfCString surfaceName("MozillaIconSurface%d", aScale);
+ return (cairo_surface_t*)g_object_get_data(G_OBJECT(aWidgetIcon),
+ surfaceName.get());
+}
+
+static void CreateHeaderBarButton(GtkWidget* aParentWidget,
+ WidgetNodeType aAppearance) {
+ GtkWidget* widget = gtk_button_new();
+
+ // We have to add button to widget hierarchy now to pick
+ // right icon style at LoadWidgetIconPixbuf().
+ if (GTK_IS_BOX(aParentWidget)) {
+ gtk_box_pack_start(GTK_BOX(aParentWidget), widget, FALSE, FALSE, 0);
+ } else {
+ gtk_container_add(GTK_CONTAINER(aParentWidget), widget);
+ }
+
+ // We bypass GetWidget() here because we create all titlebar
+ // buttons at once when a first one is requested.
+ NS_ASSERTION(!sWidgetStorage[aAppearance],
+ "Titlebar button is already created!");
+ sWidgetStorage[aAppearance] = widget;
+
+ // We need to show the button widget now as GtkBox does not
+ // place invisible widgets and we'll miss first-child/last-child
+ // css selectors at the buttons otherwise.
+ gtk_widget_show(widget);
+
+ GtkStyleContext* style = gtk_widget_get_style_context(widget);
+ gtk_style_context_add_class(style, "titlebutton");
+
+ GtkWidget* image = nullptr;
+ switch (aAppearance) {
+ case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE:
+ gtk_style_context_add_class(style, "close");
+ image = gtk_image_new_from_icon_name("window-close-symbolic",
+ GTK_ICON_SIZE_MENU);
+ break;
+ case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE:
+ gtk_style_context_add_class(style, "minimize");
+ image = gtk_image_new_from_icon_name("window-minimize-symbolic",
+ GTK_ICON_SIZE_MENU);
+ break;
+
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE:
+ gtk_style_context_add_class(style, "maximize");
+ image = gtk_image_new_from_icon_name("window-maximize-symbolic",
+ GTK_ICON_SIZE_MENU);
+ break;
+
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE:
+ gtk_style_context_add_class(style, "maximize");
+ image = gtk_image_new_from_icon_name("window-restore-symbolic",
+ GTK_ICON_SIZE_MENU);
+ break;
+ default:
+ break;
+ }
+
+ gtk_widget_set_valign(widget, GTK_ALIGN_CENTER);
+ g_object_set(image, "use-fallback", TRUE, NULL);
+ gtk_container_add(GTK_CONTAINER(widget), image);
+
+ // We bypass GetWidget() here by explicit sWidgetStorage[] update so
+ // invalidate the style as well as GetWidget() does.
+ style = gtk_widget_get_style_context(image);
+ gtk_style_context_invalidate(style);
+
+ LoadWidgetIconPixbuf(image);
+}
+
+static bool IsToolbarButtonEnabled(ButtonLayout* aButtonLayout,
+ size_t aButtonNums,
+ WidgetNodeType aAppearance) {
+ for (size_t i = 0; i < aButtonNums; i++) {
+ if (aButtonLayout[i].mType == aAppearance) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool IsSolidCSDStyleUsed() {
+ if (gCSDStyle == CSDStyle::Unknown) {
+ bool solid;
+ {
+ GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_titlebar(GTK_WINDOW(window), gtk_header_bar_new());
+ gtk_widget_realize(window);
+ GtkStyleContext* windowStyle = gtk_widget_get_style_context(window);
+ solid = gtk_style_context_has_class(windowStyle, "solid-csd");
+ gtk_widget_destroy(window);
+ }
+ gCSDStyle = solid ? CSDStyle::Solid : CSDStyle::Normal;
+ }
+ return gCSDStyle == CSDStyle::Solid;
+}
+
+static void CreateHeaderBarButtons() {
+ GtkWidget* headerBar = sWidgetStorage[MOZ_GTK_HEADER_BAR];
+ MOZ_ASSERT(headerBar, "We're missing header bar widget!");
+
+ gint buttonSpacing = 6;
+ g_object_get(headerBar, "spacing", &buttonSpacing, nullptr);
+
+ GtkWidget* buttonBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, buttonSpacing);
+ gtk_container_add(GTK_CONTAINER(headerBar), buttonBox);
+ // We support only LTR headerbar layout for now.
+ gtk_style_context_add_class(gtk_widget_get_style_context(buttonBox),
+ GTK_STYLE_CLASS_LEFT);
+
+ ButtonLayout buttonLayout[TOOLBAR_BUTTONS];
+
+ size_t activeButtons =
+ GetGtkHeaderBarButtonLayout(mozilla::Span(buttonLayout), nullptr);
+
+ if (IsToolbarButtonEnabled(buttonLayout, activeButtons,
+ MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE)) {
+ CreateHeaderBarButton(buttonBox, MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE);
+ }
+ if (IsToolbarButtonEnabled(buttonLayout, activeButtons,
+ MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE)) {
+ CreateHeaderBarButton(buttonBox, MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE);
+ // We don't pack "restore" headerbar button to box as it's an icon
+ // placeholder. Pack it only to header bar to get correct style.
+ CreateHeaderBarButton(GetWidget(MOZ_GTK_HEADER_BAR_MAXIMIZED),
+ MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE);
+ }
+ if (IsToolbarButtonEnabled(buttonLayout, activeButtons,
+ MOZ_GTK_HEADER_BAR_BUTTON_CLOSE)) {
+ CreateHeaderBarButton(buttonBox, MOZ_GTK_HEADER_BAR_BUTTON_CLOSE);
+ }
+}
+
+static void CreateHeaderBar() {
+ CreateHeaderBarWidget(MOZ_GTK_HEADER_BAR);
+ CreateHeaderBarWidget(MOZ_GTK_HEADER_BAR_MAXIMIZED);
+ CreateHeaderBarButtons();
+}
+
+static GtkWidget* CreateWidget(WidgetNodeType aAppearance) {
+ switch (aAppearance) {
+ case MOZ_GTK_WINDOW:
+ return CreateWindowWidget();
+ case MOZ_GTK_WINDOW_CONTAINER:
+ return CreateWindowContainerWidget();
+ case MOZ_GTK_CHECKBUTTON_CONTAINER:
+ return CreateCheckboxWidget();
+ case MOZ_GTK_PROGRESSBAR:
+ return CreateProgressWidget();
+ case MOZ_GTK_RADIOBUTTON_CONTAINER:
+ return CreateRadiobuttonWidget();
+ case MOZ_GTK_SCROLLBAR_VERTICAL:
+ return CreateScrollbarWidget(aAppearance, GTK_ORIENTATION_VERTICAL);
+ case MOZ_GTK_MENUPOPUP:
+ return CreateMenuPopupWidget();
+ case MOZ_GTK_MENUBAR:
+ return CreateMenuBarWidget();
+ case MOZ_GTK_EXPANDER:
+ return CreateExpanderWidget();
+ case MOZ_GTK_FRAME:
+ return CreateFrameWidget();
+ case MOZ_GTK_SPINBUTTON:
+ return CreateSpinWidget();
+ case MOZ_GTK_BUTTON:
+ return CreateButtonWidget();
+ case MOZ_GTK_TOGGLE_BUTTON:
+ return CreateToggleButtonWidget();
+ case MOZ_GTK_BUTTON_ARROW:
+ return CreateButtonArrowWidget();
+ case MOZ_GTK_ENTRY:
+ case MOZ_GTK_DROPDOWN_ENTRY:
+ return CreateEntryWidget();
+ case MOZ_GTK_SCROLLED_WINDOW:
+ return CreateScrolledWindowWidget();
+ case MOZ_GTK_TREEVIEW:
+ return CreateTreeViewWidget();
+ case MOZ_GTK_TREE_HEADER_CELL:
+ return CreateTreeHeaderCellWidget();
+ case MOZ_GTK_SPLITTER_HORIZONTAL:
+ return CreateHPanedWidget();
+ case MOZ_GTK_SPLITTER_VERTICAL:
+ return CreateVPanedWidget();
+ case MOZ_GTK_SCALE_HORIZONTAL:
+ return CreateScaleWidget(GTK_ORIENTATION_HORIZONTAL);
+ case MOZ_GTK_SCALE_VERTICAL:
+ return CreateScaleWidget(GTK_ORIENTATION_VERTICAL);
+ case MOZ_GTK_NOTEBOOK:
+ return CreateNotebookWidget();
+ case MOZ_GTK_COMBOBOX:
+ return CreateComboBoxWidget();
+ case MOZ_GTK_COMBOBOX_BUTTON:
+ return CreateComboBoxButtonWidget();
+ case MOZ_GTK_COMBOBOX_ARROW:
+ return CreateComboBoxArrowWidget();
+ case MOZ_GTK_COMBOBOX_SEPARATOR:
+ return CreateComboBoxSeparatorWidget();
+ case MOZ_GTK_COMBOBOX_ENTRY:
+ return CreateComboBoxEntryWidget();
+ case MOZ_GTK_COMBOBOX_ENTRY_TEXTAREA:
+ return CreateComboBoxEntryTextareaWidget();
+ case MOZ_GTK_COMBOBOX_ENTRY_BUTTON:
+ return CreateComboBoxEntryButtonWidget();
+ case MOZ_GTK_COMBOBOX_ENTRY_ARROW:
+ return CreateComboBoxEntryArrowWidget();
+ case MOZ_GTK_HEADERBAR_WINDOW:
+ case MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED:
+ case MOZ_GTK_HEADERBAR_FIXED:
+ case MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED:
+ case MOZ_GTK_HEADER_BAR:
+ case MOZ_GTK_HEADER_BAR_MAXIMIZED:
+ case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE:
+ /* Create header bar widgets once and fill with child elements as we need
+ the header bar fully configured to get a correct style */
+ CreateHeaderBar();
+ return sWidgetStorage[aAppearance];
+ default:
+ /* Not implemented */
+ return nullptr;
+ }
+}
+
+GtkWidget* GetWidget(WidgetNodeType aAppearance) {
+ GtkWidget* widget = sWidgetStorage[aAppearance];
+ if (!widget) {
+ widget = CreateWidget(aAppearance);
+ // Some widgets (MOZ_GTK_COMBOBOX_SEPARATOR for instance) may not be
+ // available or implemented.
+ if (!widget) {
+ return nullptr;
+ }
+ // In GTK versions prior to 3.18, automatic invalidation of style contexts
+ // for widgets was delayed until the next resize event. Gecko however,
+ // typically uses the style context before the resize event runs and so an
+ // explicit invalidation may be required. This is necessary if a style
+ // property was retrieved before all changes were made to the style
+ // context. One such situation is where gtk_button_construct_child()
+ // retrieves the style property "image-spacing" during construction of the
+ // GtkButton, before its parent is set to provide inheritance of ancestor
+ // properties. More recent GTK versions do not need this, but do not
+ // re-resolve until required and so invalidation does not trigger
+ // unnecessary resolution in general.
+ GtkStyleContext* style = gtk_widget_get_style_context(widget);
+ gtk_style_context_invalidate(style);
+
+ sWidgetStorage[aAppearance] = widget;
+ }
+ return widget;
+}
+
+static void AddStyleClassesFromStyle(GtkStyleContext* aDest,
+ GtkStyleContext* aSrc) {
+ GList* classes = gtk_style_context_list_classes(aSrc);
+ for (GList* link = classes; link; link = link->next) {
+ gtk_style_context_add_class(aDest, static_cast<gchar*>(link->data));
+ }
+ g_list_free(classes);
+}
+
+GtkStyleContext* CreateStyleForWidget(GtkWidget* aWidget,
+ GtkStyleContext* aParentStyle) {
+ static auto sGtkWidgetClassGetCSSName =
+ reinterpret_cast<const char* (*)(GtkWidgetClass*)>(
+ dlsym(RTLD_DEFAULT, "gtk_widget_class_get_css_name"));
+
+ GtkWidgetClass* widgetClass = GTK_WIDGET_GET_CLASS(aWidget);
+ const gchar* name = sGtkWidgetClassGetCSSName
+ ? sGtkWidgetClassGetCSSName(widgetClass)
+ : nullptr;
+
+ GtkStyleContext* context =
+ CreateCSSNode(name, aParentStyle, G_TYPE_FROM_CLASS(widgetClass));
+
+ // Classes are stored on the style context instead of the path so that any
+ // future gtk_style_context_save() will inherit classes on the head CSS
+ // node, in the same way as happens when called on a style context owned by
+ // a widget.
+ //
+ // Classes can be stored on a GtkCssNodeDeclaration and/or the path.
+ // gtk_style_context_save() reuses the GtkCssNodeDeclaration, and appends a
+ // new object to the path, without copying the classes from the old path
+ // head. The new head picks up classes from the GtkCssNodeDeclaration, but
+ // not the path. GtkWidgets store their classes on the
+ // GtkCssNodeDeclaration, so make sure to add classes there.
+ //
+ // Picking up classes from the style context also means that
+ // https://bugzilla.gnome.org/show_bug.cgi?id=767312, which can stop
+ // gtk_widget_path_append_for_widget() from finding classes in GTK 3.20,
+ // is not a problem.
+ GtkStyleContext* widgetStyle = gtk_widget_get_style_context(aWidget);
+ AddStyleClassesFromStyle(context, widgetStyle);
+
+ // Release any floating reference on aWidget.
+ g_object_ref_sink(aWidget);
+ g_object_unref(aWidget);
+
+ return context;
+}
+
+static GtkStyleContext* CreateStyleForWidget(GtkWidget* aWidget,
+ WidgetNodeType aParentType) {
+ return CreateStyleForWidget(aWidget, GetWidgetRootStyle(aParentType));
+}
+
+GtkStyleContext* CreateCSSNode(const char* aName, GtkStyleContext* aParentStyle,
+ GType aType) {
+ static auto sGtkWidgetPathIterSetObjectName =
+ reinterpret_cast<void (*)(GtkWidgetPath*, gint, const char*)>(
+ dlsym(RTLD_DEFAULT, "gtk_widget_path_iter_set_object_name"));
+
+ GtkWidgetPath* path;
+ if (aParentStyle) {
+ path = gtk_widget_path_copy(gtk_style_context_get_path(aParentStyle));
+ // Copy classes from the parent style context to its corresponding node in
+ // the path, because GTK will only match against ancestor classes if they
+ // are on the path.
+ GList* classes = gtk_style_context_list_classes(aParentStyle);
+ for (GList* link = classes; link; link = link->next) {
+ gtk_widget_path_iter_add_class(path, -1, static_cast<gchar*>(link->data));
+ }
+ g_list_free(classes);
+ } else {
+ path = gtk_widget_path_new();
+ }
+
+ gtk_widget_path_append_type(path, aType);
+
+ if (sGtkWidgetPathIterSetObjectName) {
+ (*sGtkWidgetPathIterSetObjectName)(path, -1, aName);
+ }
+
+ GtkStyleContext* context = gtk_style_context_new();
+ gtk_style_context_set_path(context, path);
+ gtk_style_context_set_parent(context, aParentStyle);
+ gtk_widget_path_unref(path);
+
+ // In GTK 3.4, gtk_render_* functions use |theming_engine| on the style
+ // context without ensuring any style resolution sets it appropriately
+ // in style_data_lookup(). e.g.
+ // https://git.gnome.org/browse/gtk+/tree/gtk/gtkstylecontext.c?h=3.4.4#n3847
+ //
+ // That can result in incorrect drawing on first draw. To work around this,
+ // force a style look-up to set |theming_engine|. It is sufficient to do
+ // this only on context creation, instead of after every modification to the
+ // context, because themes typically (Ambiance and oxygen-gtk, at least) set
+ // the "engine" property with the '*' selector.
+ if (GTK_MAJOR_VERSION == 3 && gtk_get_minor_version() < 6) {
+ GdkRGBA unused;
+ gtk_style_context_get_color(context, GTK_STATE_FLAG_NORMAL, &unused);
+ }
+
+ return context;
+}
+
+// Return a style context matching that of the root CSS node of a widget.
+// This is used by all GTK versions.
+static GtkStyleContext* GetWidgetRootStyle(WidgetNodeType aNodeType) {
+ GtkStyleContext* style = sStyleStorage[aNodeType];
+ if (style) return style;
+
+ switch (aNodeType) {
+ case MOZ_GTK_MENUITEM:
+ style = CreateStyleForWidget(gtk_menu_item_new(), MOZ_GTK_MENUPOPUP);
+ break;
+ case MOZ_GTK_MENUBARITEM:
+ style = CreateStyleForWidget(gtk_menu_item_new(), MOZ_GTK_MENUBAR);
+ break;
+ case MOZ_GTK_TEXT_VIEW:
+ style =
+ CreateStyleForWidget(gtk_text_view_new(), MOZ_GTK_SCROLLED_WINDOW);
+ break;
+ case MOZ_GTK_TOOLTIP:
+ if (gtk_check_version(3, 20, 0) != nullptr) {
+ // The tooltip style class is added first in CreateTooltipWidget()
+ // and transfered to style in CreateStyleForWidget().
+ GtkWidget* tooltipWindow = CreateTooltipWidget();
+ style = CreateStyleForWidget(tooltipWindow, nullptr);
+ gtk_widget_destroy(tooltipWindow); // Release GtkWindow self-reference.
+ } else {
+ // We create this from the path because GtkTooltipWindow is not public.
+ style = CreateCSSNode("tooltip", nullptr, GTK_TYPE_TOOLTIP);
+ gtk_style_context_add_class(style, GTK_STYLE_CLASS_BACKGROUND);
+ }
+ break;
+ case MOZ_GTK_TOOLTIP_BOX:
+ style = CreateStyleForWidget(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0),
+ MOZ_GTK_TOOLTIP);
+ break;
+ case MOZ_GTK_TOOLTIP_BOX_LABEL:
+ style = CreateStyleForWidget(gtk_label_new(nullptr), MOZ_GTK_TOOLTIP_BOX);
+ break;
+ default:
+ GtkWidget* widget = GetWidget(aNodeType);
+ MOZ_ASSERT(widget);
+ return gtk_widget_get_style_context(widget);
+ }
+
+ MOZ_ASSERT(style);
+ sStyleStorage[aNodeType] = style;
+ return style;
+}
+
+static GtkStyleContext* CreateChildCSSNode(const char* aName,
+ WidgetNodeType aParentNodeType) {
+ return CreateCSSNode(aName, GetCssNodeStyleInternal(aParentNodeType));
+}
+
+// Create a style context equivalent to a saved root style context of
+// |aAppearance| with |aStyleClass| as an additional class. This is used to
+// produce a context equivalent to what GTK versions < 3.20 use for many
+// internal parts of widgets.
+static GtkStyleContext* CreateSubStyleWithClass(WidgetNodeType aAppearance,
+ const gchar* aStyleClass) {
+ static auto sGtkWidgetPathIterGetObjectName =
+ reinterpret_cast<const char* (*)(const GtkWidgetPath*, gint)>(
+ dlsym(RTLD_DEFAULT, "gtk_widget_path_iter_get_object_name"));
+
+ GtkStyleContext* parentStyle = GetWidgetRootStyle(aAppearance);
+
+ // Create a new context that behaves like |parentStyle| would after
+ // gtk_style_context_save(parentStyle).
+ //
+ // Avoiding gtk_style_context_save() avoids the need to manage the
+ // restore, and a new context permits caching style resolution.
+ //
+ // gtk_style_context_save(context) changes the node hierarchy of |context|
+ // to add a new GtkCssNodeDeclaration that is a copy of its original node.
+ // The new node is a child of the original node, and so the new heirarchy is
+ // one level deeper. The new node receives the same classes as the
+ // original, but any changes to the classes on |context| will change only
+ // the new node. The new node inherits properties from the original node
+ // (which retains the original heirarchy and classes) and matches CSS rules
+ // with the new heirarchy and any changes to the classes.
+ //
+ // The change in hierarchy can produce some surprises in matching theme CSS
+ // rules (e.g. https://bugzilla.gnome.org/show_bug.cgi?id=761870#c2), but it
+ // is important here to produce the same behavior so that rules match the
+ // same widget parts in Gecko as they do in GTK.
+ //
+ // When using public GTK API to construct style contexts, a widget path is
+ // required. CSS rules are not matched against the style context heirarchy
+ // but according to the heirarchy in the widget path. The path that matches
+ // the same CSS rules as a saved context is like the path of |parentStyle|
+ // but with an extra copy of the head (last) object appended. Setting
+ // |parentStyle| as the parent context provides the same inheritance of
+ // properties from the widget root node.
+ const GtkWidgetPath* parentPath = gtk_style_context_get_path(parentStyle);
+ const gchar* name = sGtkWidgetPathIterGetObjectName
+ ? sGtkWidgetPathIterGetObjectName(parentPath, -1)
+ : nullptr;
+ GType objectType = gtk_widget_path_get_object_type(parentPath);
+
+ GtkStyleContext* style = CreateCSSNode(name, parentStyle, objectType);
+
+ // Start with the same classes on the new node as were on |parentStyle|.
+ // GTK puts no regions or junction_sides on widget root nodes, and so there
+ // is no need to copy these.
+ AddStyleClassesFromStyle(style, parentStyle);
+
+ gtk_style_context_add_class(style, aStyleClass);
+ return style;
+}
+
+/* GetCssNodeStyleInternal is used by Gtk >= 3.20 */
+static GtkStyleContext* GetCssNodeStyleInternal(WidgetNodeType aNodeType) {
+ GtkStyleContext* style = sStyleStorage[aNodeType];
+ if (style) return style;
+
+ switch (aNodeType) {
+ case MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL:
+ style = CreateChildCSSNode("contents", MOZ_GTK_SCROLLBAR_VERTICAL);
+ break;
+ case MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH,
+ MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL);
+ break;
+ case MOZ_GTK_SCROLLBAR_THUMB_VERTICAL:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_SLIDER,
+ MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL);
+ break;
+ case MOZ_GTK_RADIOBUTTON:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_RADIO,
+ MOZ_GTK_RADIOBUTTON_CONTAINER);
+ break;
+ case MOZ_GTK_CHECKBUTTON:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_CHECK,
+ MOZ_GTK_CHECKBUTTON_CONTAINER);
+ break;
+ case MOZ_GTK_PROGRESS_TROUGH:
+ /* Progress bar background (trough) */
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH, MOZ_GTK_PROGRESSBAR);
+ break;
+ case MOZ_GTK_PROGRESS_CHUNK:
+ style = CreateChildCSSNode("progress", MOZ_GTK_PROGRESS_TROUGH);
+ break;
+ case MOZ_GTK_SPINBUTTON_ENTRY:
+ // TODO - create from CSS node
+ style =
+ CreateSubStyleWithClass(MOZ_GTK_SPINBUTTON, GTK_STYLE_CLASS_ENTRY);
+ break;
+ case MOZ_GTK_SCROLLED_WINDOW:
+ // TODO - create from CSS node
+ style = CreateSubStyleWithClass(MOZ_GTK_SCROLLED_WINDOW,
+ GTK_STYLE_CLASS_FRAME);
+ break;
+ case MOZ_GTK_TEXT_VIEW_TEXT_SELECTION:
+ style = CreateChildCSSNode("selection", MOZ_GTK_TEXT_VIEW_TEXT);
+ break;
+ case MOZ_GTK_TEXT_VIEW_TEXT:
+ case MOZ_GTK_RESIZER:
+ style = CreateChildCSSNode("text", MOZ_GTK_TEXT_VIEW);
+ if (aNodeType == MOZ_GTK_RESIZER) {
+ // The "grip" class provides the correct builtin icon from
+ // gtk_render_handle(). The icon is drawn with shaded variants of
+ // the background color, and so a transparent background would lead to
+ // a transparent resizer. gtk_render_handle() also uses the
+ // background color to draw a background, and so this style otherwise
+ // matches what is used in GtkTextView to match the background with
+ // textarea elements.
+ GdkRGBA color;
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL,
+ &color);
+ if (color.alpha == 0.0) {
+ g_object_unref(style);
+ style = CreateStyleForWidget(gtk_text_view_new(),
+ MOZ_GTK_SCROLLED_WINDOW);
+ }
+ gtk_style_context_add_class(style, GTK_STYLE_CLASS_GRIP);
+ }
+ break;
+ case MOZ_GTK_FRAME_BORDER:
+ style = CreateChildCSSNode("border", MOZ_GTK_FRAME);
+ break;
+ case MOZ_GTK_TREEVIEW_VIEW:
+ // TODO - create from CSS node
+ style = CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_VIEW);
+ break;
+ case MOZ_GTK_TREEVIEW_EXPANDER:
+ // TODO - create from CSS node
+ style =
+ CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_EXPANDER);
+ break;
+ case MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL:
+ style = CreateChildCSSNode("separator", MOZ_GTK_SPLITTER_HORIZONTAL);
+ break;
+ case MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL:
+ style = CreateChildCSSNode("separator", MOZ_GTK_SPLITTER_VERTICAL);
+ break;
+ case MOZ_GTK_SCALE_CONTENTS_HORIZONTAL:
+ style = CreateChildCSSNode("contents", MOZ_GTK_SCALE_HORIZONTAL);
+ break;
+ case MOZ_GTK_SCALE_CONTENTS_VERTICAL:
+ style = CreateChildCSSNode("contents", MOZ_GTK_SCALE_VERTICAL);
+ break;
+ case MOZ_GTK_SCALE_TROUGH_HORIZONTAL:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH,
+ MOZ_GTK_SCALE_CONTENTS_HORIZONTAL);
+ break;
+ case MOZ_GTK_SCALE_TROUGH_VERTICAL:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_TROUGH,
+ MOZ_GTK_SCALE_CONTENTS_VERTICAL);
+ break;
+ case MOZ_GTK_SCALE_THUMB_HORIZONTAL:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_SLIDER,
+ MOZ_GTK_SCALE_TROUGH_HORIZONTAL);
+ break;
+ case MOZ_GTK_SCALE_THUMB_VERTICAL:
+ style = CreateChildCSSNode(GTK_STYLE_CLASS_SLIDER,
+ MOZ_GTK_SCALE_TROUGH_VERTICAL);
+ break;
+ case MOZ_GTK_TAB_TOP: {
+ // TODO - create from CSS node
+ style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_TOP);
+ gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB,
+ static_cast<GtkRegionFlags>(0));
+ break;
+ }
+ case MOZ_GTK_TAB_BOTTOM: {
+ // TODO - create from CSS node
+ style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_BOTTOM);
+ gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB,
+ static_cast<GtkRegionFlags>(0));
+ break;
+ }
+ case MOZ_GTK_NOTEBOOK:
+ case MOZ_GTK_NOTEBOOK_HEADER:
+ case MOZ_GTK_TABPANELS:
+ case MOZ_GTK_TAB_SCROLLARROW: {
+ // TODO - create from CSS node
+ GtkWidget* widget = GetWidget(MOZ_GTK_NOTEBOOK);
+ return gtk_widget_get_style_context(widget);
+ }
+ case MOZ_GTK_WINDOW_DECORATION: {
+ GtkStyleContext* parentStyle =
+ CreateSubStyleWithClass(MOZ_GTK_WINDOW, "csd");
+ style = CreateCSSNode("decoration", parentStyle);
+ g_object_unref(parentStyle);
+ break;
+ }
+ case MOZ_GTK_WINDOW_DECORATION_SOLID: {
+ GtkStyleContext* parentStyle =
+ CreateSubStyleWithClass(MOZ_GTK_WINDOW, "solid-csd");
+ style = CreateCSSNode("decoration", parentStyle);
+ g_object_unref(parentStyle);
+ break;
+ }
+ default:
+ return GetWidgetRootStyle(aNodeType);
+ }
+
+ MOZ_ASSERT(style, "missing style context for node type");
+ sStyleStorage[aNodeType] = style;
+ return style;
+}
+
+/* GetWidgetStyleInternal is used by Gtk < 3.20 */
+static GtkStyleContext* GetWidgetStyleInternal(WidgetNodeType aNodeType) {
+ GtkStyleContext* style = sStyleStorage[aNodeType];
+ if (style) return style;
+
+ switch (aNodeType) {
+ case MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SCROLLBAR_VERTICAL,
+ GTK_STYLE_CLASS_TROUGH);
+ break;
+ case MOZ_GTK_SCROLLBAR_THUMB_VERTICAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SCROLLBAR_VERTICAL,
+ GTK_STYLE_CLASS_SLIDER);
+ break;
+ case MOZ_GTK_RADIOBUTTON:
+ style = CreateSubStyleWithClass(MOZ_GTK_RADIOBUTTON_CONTAINER,
+ GTK_STYLE_CLASS_RADIO);
+ break;
+ case MOZ_GTK_CHECKBUTTON:
+ style = CreateSubStyleWithClass(MOZ_GTK_CHECKBUTTON_CONTAINER,
+ GTK_STYLE_CLASS_CHECK);
+ break;
+ case MOZ_GTK_PROGRESS_TROUGH:
+ style =
+ CreateSubStyleWithClass(MOZ_GTK_PROGRESSBAR, GTK_STYLE_CLASS_TROUGH);
+ break;
+ case MOZ_GTK_PROGRESS_CHUNK:
+ style = CreateSubStyleWithClass(MOZ_GTK_PROGRESSBAR,
+ GTK_STYLE_CLASS_PROGRESSBAR);
+ gtk_style_context_remove_class(style, GTK_STYLE_CLASS_TROUGH);
+ break;
+ case MOZ_GTK_SPINBUTTON_ENTRY:
+ style =
+ CreateSubStyleWithClass(MOZ_GTK_SPINBUTTON, GTK_STYLE_CLASS_ENTRY);
+ break;
+ case MOZ_GTK_SCROLLED_WINDOW:
+ style = CreateSubStyleWithClass(MOZ_GTK_SCROLLED_WINDOW,
+ GTK_STYLE_CLASS_FRAME);
+ break;
+ case MOZ_GTK_TEXT_VIEW_TEXT:
+ case MOZ_GTK_RESIZER:
+ // GTK versions prior to 3.20 do not have the view class on the root
+ // node, but add this to determine the background for the text window.
+ style = CreateSubStyleWithClass(MOZ_GTK_TEXT_VIEW, GTK_STYLE_CLASS_VIEW);
+ if (aNodeType == MOZ_GTK_RESIZER) {
+ // The "grip" class provides the correct builtin icon from
+ // gtk_render_handle(). The icon is drawn with shaded variants of
+ // the background color, and so a transparent background would lead to
+ // a transparent resizer. gtk_render_handle() also uses the
+ // background color to draw a background, and so this style otherwise
+ // matches MOZ_GTK_TEXT_VIEW_TEXT to match the background with
+ // textarea elements. GtkTextView creates a separate text window and
+ // so the background should not be transparent.
+ gtk_style_context_add_class(style, GTK_STYLE_CLASS_GRIP);
+ }
+ break;
+ case MOZ_GTK_FRAME_BORDER:
+ return GetWidgetRootStyle(MOZ_GTK_FRAME);
+ case MOZ_GTK_TREEVIEW_VIEW:
+ style = CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_VIEW);
+ break;
+ case MOZ_GTK_TREEVIEW_EXPANDER:
+ style =
+ CreateSubStyleWithClass(MOZ_GTK_TREEVIEW, GTK_STYLE_CLASS_EXPANDER);
+ break;
+ case MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SPLITTER_HORIZONTAL,
+ GTK_STYLE_CLASS_PANE_SEPARATOR);
+ break;
+ case MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SPLITTER_VERTICAL,
+ GTK_STYLE_CLASS_PANE_SEPARATOR);
+ break;
+ case MOZ_GTK_SCALE_TROUGH_HORIZONTAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SCALE_HORIZONTAL,
+ GTK_STYLE_CLASS_TROUGH);
+ break;
+ case MOZ_GTK_SCALE_TROUGH_VERTICAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SCALE_VERTICAL,
+ GTK_STYLE_CLASS_TROUGH);
+ break;
+ case MOZ_GTK_SCALE_THUMB_HORIZONTAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SCALE_HORIZONTAL,
+ GTK_STYLE_CLASS_SLIDER);
+ break;
+ case MOZ_GTK_SCALE_THUMB_VERTICAL:
+ style = CreateSubStyleWithClass(MOZ_GTK_SCALE_VERTICAL,
+ GTK_STYLE_CLASS_SLIDER);
+ break;
+ case MOZ_GTK_TAB_TOP:
+ style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_TOP);
+ gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB,
+ static_cast<GtkRegionFlags>(0));
+ break;
+ case MOZ_GTK_TAB_BOTTOM:
+ style = CreateSubStyleWithClass(MOZ_GTK_NOTEBOOK, GTK_STYLE_CLASS_BOTTOM);
+ gtk_style_context_add_region(style, GTK_STYLE_REGION_TAB,
+ static_cast<GtkRegionFlags>(0));
+ break;
+ case MOZ_GTK_NOTEBOOK:
+ case MOZ_GTK_NOTEBOOK_HEADER:
+ case MOZ_GTK_TABPANELS:
+ case MOZ_GTK_TAB_SCROLLARROW: {
+ GtkWidget* widget = GetWidget(MOZ_GTK_NOTEBOOK);
+ return gtk_widget_get_style_context(widget);
+ }
+ default:
+ return GetWidgetRootStyle(aNodeType);
+ }
+
+ MOZ_ASSERT(style);
+ sStyleStorage[aNodeType] = style;
+ return style;
+}
+
+void ResetWidgetCache() {
+ for (int i = 0; i < MOZ_GTK_WIDGET_NODE_COUNT; i++) {
+ if (sStyleStorage[i]) g_object_unref(sStyleStorage[i]);
+ }
+ mozilla::PodArrayZero(sStyleStorage);
+
+ gCSDStyle = CSDStyle::Unknown;
+
+ /* This will destroy all of our widgets */
+ if (sWidgetStorage[MOZ_GTK_WINDOW]) {
+ gtk_widget_destroy(sWidgetStorage[MOZ_GTK_WINDOW]);
+ }
+ if (sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW]) {
+ gtk_widget_destroy(sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW]);
+ }
+ if (sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED]) {
+ gtk_widget_destroy(sWidgetStorage[MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED]);
+ }
+
+ /* Clear already freed arrays */
+ mozilla::PodArrayZero(sWidgetStorage);
+}
+
+GtkStyleContext* GetStyleContext(WidgetNodeType aNodeType, int aScale,
+ GtkTextDirection aDirection,
+ GtkStateFlags aStateFlags) {
+ GtkStyleContext* style;
+ if (gtk_check_version(3, 20, 0) != nullptr) {
+ style = GetWidgetStyleInternal(aNodeType);
+ } else {
+ style = GetCssNodeStyleInternal(aNodeType);
+ StyleContextSetScale(style, aScale);
+ }
+ bool stateChanged = false;
+ bool stateHasDirection = gtk_get_minor_version() >= 8;
+ GtkStateFlags oldState = gtk_style_context_get_state(style);
+ MOZ_ASSERT(!(aStateFlags & (STATE_FLAG_DIR_LTR | STATE_FLAG_DIR_RTL)));
+ unsigned newState = aStateFlags;
+ if (stateHasDirection) {
+ switch (aDirection) {
+ case GTK_TEXT_DIR_LTR:
+ newState |= STATE_FLAG_DIR_LTR;
+ break;
+ case GTK_TEXT_DIR_RTL:
+ newState |= STATE_FLAG_DIR_RTL;
+ break;
+ default:
+ MOZ_FALLTHROUGH_ASSERT("Bad GtkTextDirection");
+ case GTK_TEXT_DIR_NONE:
+ // GtkWidget uses a default direction if neither is explicitly
+ // specified, but here DIR_NONE is interpreted as meaning the
+ // direction is not important, so don't change the direction
+ // unnecessarily.
+ newState |= oldState & (STATE_FLAG_DIR_LTR | STATE_FLAG_DIR_RTL);
+ }
+ } else if (aDirection != GTK_TEXT_DIR_NONE) {
+ GtkTextDirection oldDirection = gtk_style_context_get_direction(style);
+ if (aDirection != oldDirection) {
+ gtk_style_context_set_direction(style, aDirection);
+ stateChanged = true;
+ }
+ }
+ if (oldState != newState) {
+ gtk_style_context_set_state(style, static_cast<GtkStateFlags>(newState));
+ stateChanged = true;
+ }
+ // This invalidate is necessary for unsaved style contexts from GtkWidgets
+ // in pre-3.18 GTK, because automatic invalidation of such contexts
+ // was delayed until a resize event runs.
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1272194#c7
+ //
+ // Avoid calling invalidate on contexts that are not owned and constructed
+ // by widgets to avoid performing build_properties() (in 3.16 stylecontext.c)
+ // unnecessarily early.
+ if (stateChanged && sWidgetStorage[aNodeType]) {
+ gtk_style_context_invalidate(style);
+ }
+ return style;
+}
+
+GtkStyleContext* CreateStyleContextWithStates(WidgetNodeType aNodeType,
+ int aScale,
+ GtkTextDirection aDirection,
+ GtkStateFlags aStateFlags) {
+ GtkStyleContext* style =
+ GetStyleContext(aNodeType, aScale, aDirection, aStateFlags);
+ GtkWidgetPath* path = gtk_widget_path_copy(gtk_style_context_get_path(style));
+
+ static auto sGtkWidgetPathIterGetState =
+ (GtkStateFlags(*)(const GtkWidgetPath*, gint))dlsym(
+ RTLD_DEFAULT, "gtk_widget_path_iter_get_state");
+ static auto sGtkWidgetPathIterSetState =
+ (void (*)(GtkWidgetPath*, gint, GtkStateFlags))dlsym(
+ RTLD_DEFAULT, "gtk_widget_path_iter_set_state");
+
+ int pathLength = gtk_widget_path_length(path);
+ for (int i = 0; i < pathLength; i++) {
+ unsigned state = aStateFlags | sGtkWidgetPathIterGetState(path, i);
+ sGtkWidgetPathIterSetState(path, i, GtkStateFlags(state));
+ }
+
+ style = gtk_style_context_new();
+ gtk_style_context_set_path(style, path);
+ gtk_widget_path_unref(path);
+
+ return style;
+}
+
+void StyleContextSetScale(GtkStyleContext* style, gint aScaleFactor) {
+ // Support HiDPI styles on Gtk 3.20+
+ static auto sGtkStyleContextSetScalePtr =
+ (void (*)(GtkStyleContext*, gint))dlsym(RTLD_DEFAULT,
+ "gtk_style_context_set_scale");
+ if (sGtkStyleContextSetScalePtr && style) {
+ sGtkStyleContextSetScalePtr(style, aScaleFactor);
+ }
+}
+
+bool HeaderBarShouldDrawContainer(WidgetNodeType aNodeType) {
+ MOZ_ASSERT(aNodeType == MOZ_GTK_HEADER_BAR ||
+ aNodeType == MOZ_GTK_HEADER_BAR_MAXIMIZED);
+ mozilla::Unused << GetWidget(aNodeType);
+ return aNodeType == MOZ_GTK_HEADER_BAR
+ ? gHeaderBarShouldDrawContainer
+ : gMaximizedHeaderBarShouldDrawContainer;
+}
+
+gint GetBorderRadius(GtkStyleContext* aStyle) {
+ GValue value = G_VALUE_INIT;
+ // NOTE(emilio): In an ideal world, we'd query the two longhands
+ // (border-top-left-radius and border-top-right-radius) separately. However,
+ // that doesn't work (GTK rejects the query with:
+ //
+ // Style property "border-top-left-radius" is not gettable
+ //
+ // However! Getting border-radius does work, and it does return the
+ // border-top-left-radius as a gint:
+ //
+ // https://docs.gtk.org/gtk3/const.STYLE_PROPERTY_BORDER_RADIUS.html
+ // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-20/gtk/gtkcssshorthandpropertyimpl.c#L961-977
+ //
+ // So we abuse this fact, and make the assumption here that the
+ // border-top-{left,right}-radius are the same, and roll with it.
+ gtk_style_context_get_property(aStyle, "border-radius", GTK_STATE_FLAG_NORMAL,
+ &value);
+ gint result = 0;
+ auto type = G_VALUE_TYPE(&value);
+ if (type == G_TYPE_INT) {
+ result = g_value_get_int(&value);
+ } else {
+ NS_WARNING(nsPrintfCString("Unknown value type %lu for border-radius", type)
+ .get());
+ }
+ g_value_unset(&value);
+ return result;
+}
diff --git a/widget/gtk/WidgetStyleCache.h b/widget/gtk/WidgetStyleCache.h
new file mode 100644
index 0000000000..f38b75fae6
--- /dev/null
+++ b/widget/gtk/WidgetStyleCache.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef WidgetStyleCache_h
+#define WidgetStyleCache_h
+
+#include <gtk/gtk.h>
+#include "gtkdrawing.h"
+
+GtkWidget* GetWidget(WidgetNodeType aNodeType);
+
+cairo_surface_t* GetWidgetIconSurface(GtkWidget* aWidgetIcon, int aScale);
+
+/*
+ * Return a new style context based on aWidget, as a child of aParentStyle.
+ * If aWidget still has a floating reference, then it is sunk and released.
+ */
+GtkStyleContext* CreateStyleForWidget(GtkWidget* aWidget,
+ GtkStyleContext* aParentStyle);
+
+GtkStyleContext* CreateCSSNode(const char* aName, GtkStyleContext* aParentStyle,
+ GType aType = G_TYPE_NONE);
+
+/*
+ * Returns a pointer to a style context for the specified node and state.
+ * aStateFlags is applied only to last widget in css style path,
+ * for instance GetStyleContext(MOZ_GTK_BUTTON, .., GTK_STATE_FLAG_HOVER)
+ * you get "window button:hover" css selector.
+ * If you want aStateFlags applied to all path elements use
+ * CreateStyleContextWithStates().
+ *
+ * The context is owned by WidgetStyleCache. Do not unref.
+ */
+GtkStyleContext* GetStyleContext(
+ WidgetNodeType aNodeType, int aScale = 1,
+ GtkTextDirection aDirection = GTK_TEXT_DIR_NONE,
+ GtkStateFlags aStateFlags = GTK_STATE_FLAG_NORMAL);
+
+/*
+ * Returns a pointer to a style context for the specified node
+ * and state applied to all elements at widget style path.
+ *
+ * The context is owned by caller and must be released by g_object_unref().
+ */
+GtkStyleContext* CreateStyleContextWithStates(
+ WidgetNodeType aNodeType, int aScale = 1,
+ GtkTextDirection aDirection = GTK_TEXT_DIR_NONE,
+ GtkStateFlags aStateFlags = GTK_STATE_FLAG_NORMAL);
+
+void ResetWidgetCache();
+
+bool IsSolidCSDStyleUsed();
+
+void StyleContextSetScale(GtkStyleContext* style, gint aScaleFactor);
+
+gint GetBorderRadius(GtkStyleContext* aStyle);
+
+bool HeaderBarShouldDrawContainer(WidgetNodeType);
+
+#endif // WidgetStyleCache_h
diff --git a/widget/gtk/WidgetTraceEvent.cpp b/widget/gtk/WidgetTraceEvent.cpp
new file mode 100644
index 0000000000..161e5302cc
--- /dev/null
+++ b/widget/gtk/WidgetTraceEvent.cpp
@@ -0,0 +1,68 @@
+/* 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 "mozilla/WidgetTraceEvent.h"
+
+#include <glib.h>
+#include <mozilla/CondVar.h>
+#include <mozilla/Mutex.h>
+#include <stdio.h>
+
+using mozilla::CondVar;
+using mozilla::Mutex;
+using mozilla::MutexAutoLock;
+
+namespace {
+
+Mutex* sMutex = nullptr;
+CondVar* sCondVar = nullptr;
+bool sTracerProcessed = false;
+
+// This function is called from the main (UI) thread.
+gboolean TracerCallback(gpointer data) {
+ mozilla::SignalTracerThread();
+ return FALSE;
+}
+
+} // namespace
+
+namespace mozilla {
+
+bool InitWidgetTracing() {
+ sMutex = new Mutex("Event tracer thread mutex");
+ sCondVar = new CondVar(*sMutex, "Event tracer thread condvar");
+ return true;
+}
+
+void CleanUpWidgetTracing() {
+ delete sMutex;
+ delete sCondVar;
+ sMutex = nullptr;
+ sCondVar = nullptr;
+}
+
+// This function is called from the background tracer thread.
+bool FireAndWaitForTracerEvent() {
+ MOZ_ASSERT(sMutex && sCondVar, "Tracing not initialized!");
+
+ // Send a default-priority idle event through the
+ // event loop, and wait for it to finish.
+ MutexAutoLock lock(*sMutex);
+ MOZ_ASSERT(!sTracerProcessed, "Tracer synchronization state is wrong");
+ g_idle_add_full(G_PRIORITY_DEFAULT, TracerCallback, nullptr, nullptr);
+ while (!sTracerProcessed) sCondVar->Wait();
+ sTracerProcessed = false;
+ return true;
+}
+
+void SignalTracerThread() {
+ if (!sMutex || !sCondVar) return;
+ MutexAutoLock lock(*sMutex);
+ if (!sTracerProcessed) {
+ sTracerProcessed = true;
+ sCondVar->Notify();
+ }
+}
+
+} // namespace mozilla
diff --git a/widget/gtk/WidgetUtilsGtk.cpp b/widget/gtk/WidgetUtilsGtk.cpp
new file mode 100644
index 0000000000..2ce81dedb7
--- /dev/null
+++ b/widget/gtk/WidgetUtilsGtk.cpp
@@ -0,0 +1,500 @@
+/* -*- 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 "WidgetUtilsGtk.h"
+
+#include "MainThreadUtils.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/UniquePtr.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsWindow.h"
+#include "nsIGfxInfo.h"
+#include "mozilla/Components.h"
+#include "nsCOMPtr.h"
+#include "nsIProperties.h"
+#include "nsIFile.h"
+#include "nsXULAppAPI.h"
+#include "nsXPCOMCID.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsString.h"
+#include "nsGtkKeyUtils.h"
+#include "nsGtkUtils.h"
+
+#include <gtk/gtk.h>
+#include <dlfcn.h>
+#include <glib.h>
+
+#undef LOGW
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+extern mozilla::LazyLogModule gWidgetLog;
+# define LOGW(...) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# define LOGW(...)
+#endif /* MOZ_LOGGING */
+
+namespace mozilla::widget {
+
+int32_t WidgetUtilsGTK::IsTouchDeviceSupportPresent() {
+ int32_t result = 0;
+ GdkDisplay* display = gdk_display_get_default();
+ if (!display) {
+ return 0;
+ }
+
+ GdkDeviceManager* manager = gdk_display_get_device_manager(display);
+ if (!manager) {
+ return 0;
+ }
+
+ GList* devices =
+ gdk_device_manager_list_devices(manager, GDK_DEVICE_TYPE_SLAVE);
+ GList* list = devices;
+
+ while (devices) {
+ GdkDevice* device = static_cast<GdkDevice*>(devices->data);
+ if (gdk_device_get_source(device) == GDK_SOURCE_TOUCHSCREEN) {
+ result = 1;
+ break;
+ }
+ devices = devices->next;
+ }
+
+ if (list) {
+ g_list_free(list);
+ }
+
+ return result;
+}
+
+bool IsMainWindowTransparent() {
+ return nsWindow::IsToplevelWindowTransparent();
+}
+
+// We avoid linking gdk_*_display_get_type directly in order to avoid a runtime
+// dependency on GTK built with both backends. Other X11- and Wayland-specific
+// functions get stubbed out by libmozgtk and crash when called, but those
+// should only be called when the matching backend is already in use.
+
+bool GdkIsWaylandDisplay(GdkDisplay* display) {
+ static auto sGdkWaylandDisplayGetType =
+ (GType(*)())dlsym(RTLD_DEFAULT, "gdk_wayland_display_get_type");
+ return sGdkWaylandDisplayGetType &&
+ G_TYPE_CHECK_INSTANCE_TYPE(display, sGdkWaylandDisplayGetType());
+}
+
+bool GdkIsX11Display(GdkDisplay* display) {
+ static auto sGdkX11DisplayGetType =
+ (GType(*)())dlsym(RTLD_DEFAULT, "gdk_x11_display_get_type");
+ return sGdkX11DisplayGetType &&
+ G_TYPE_CHECK_INSTANCE_TYPE(display, sGdkX11DisplayGetType());
+}
+
+bool IsXWaylandProtocol() {
+ static bool isXwayland = [] {
+ return !GdkIsWaylandDisplay() && !!getenv("WAYLAND_DISPLAY");
+ }();
+ return isXwayland;
+}
+
+bool GdkIsWaylandDisplay() {
+ static bool isWaylandDisplay = gdk_display_get_default() &&
+ GdkIsWaylandDisplay(gdk_display_get_default());
+ return isWaylandDisplay;
+}
+
+bool GdkIsX11Display() {
+ static bool isX11Display = gdk_display_get_default()
+ ? GdkIsX11Display(gdk_display_get_default())
+ : false;
+ return isX11Display;
+}
+
+GdkDevice* GdkGetPointer() {
+ GdkDisplay* display = gdk_display_get_default();
+ GdkDeviceManager* deviceManager = gdk_display_get_device_manager(display);
+ return gdk_device_manager_get_client_pointer(deviceManager);
+}
+
+static GdkEvent* sLastMousePressEvent = nullptr;
+GdkEvent* GetLastMousePressEvent() { return sLastMousePressEvent; }
+
+void SetLastMousePressEvent(GdkEvent* aEvent) {
+ if (sLastMousePressEvent) {
+ GUniquePtr<GdkEvent> event(sLastMousePressEvent);
+ sLastMousePressEvent = nullptr;
+ }
+ if (!aEvent) {
+ return;
+ }
+ GUniquePtr<GdkEvent> event(gdk_event_copy(aEvent));
+ sLastMousePressEvent = event.release();
+}
+
+bool IsRunningUnderSnap() { return !!GetSnapInstanceName(); }
+
+bool IsRunningUnderFlatpak() {
+ // https://gitlab.gnome.org/GNOME/gtk/-/blob/4300a5c609306ce77cbc8a3580c19201dccd8d13/gdk/gdk.c#L472
+ static bool sRunning = [] {
+ return g_file_test("/.flatpak-info", G_FILE_TEST_EXISTS);
+ }();
+ return sRunning;
+}
+
+bool IsPackagedAppFileExists() {
+ static bool sRunning = [] {
+ nsresult rv;
+ nsCString path;
+ nsCOMPtr<nsIFile> file;
+ nsCOMPtr<nsIProperties> directoryService;
+
+ directoryService = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(directoryService, FALSE);
+
+ rv = directoryService->Get(NS_GRE_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, FALSE);
+
+ rv = file->AppendNative("is-packaged-app"_ns);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+
+ rv = file->GetNativePath(path);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+
+ return g_file_test(path.get(), G_FILE_TEST_EXISTS);
+ }();
+ return sRunning;
+}
+
+const char* GetSnapInstanceName() {
+ static const char* sInstanceName = []() -> const char* {
+ const char* snapName = g_getenv("SNAP_NAME");
+ if (!snapName) {
+ return nullptr;
+ }
+ if (g_strcmp0(snapName, MOZ_APP_NAME)) {
+ return nullptr;
+ }
+ // Intentionally leaked, as keeping a pointer to the environment forever
+ // is a bit suspicious.
+ if (const char* instanceName = g_getenv("SNAP_INSTANCE_NAME")) {
+ return g_strdup(instanceName);
+ }
+ // Instance name didn't exist for snapd <= 2.35:
+ return g_strdup(snapName);
+ }();
+ return sInstanceName;
+}
+
+bool ShouldUsePortal(PortalKind aPortalKind) {
+ static bool sPortalEnv = [] {
+ if (IsRunningUnderFlatpakOrSnap()) {
+ return true;
+ }
+ const char* portalEnvString = g_getenv("GTK_USE_PORTAL");
+ return portalEnvString && atoi(portalEnvString) != 0;
+ }();
+
+ bool autoBehavior = sPortalEnv;
+ const int32_t pref = [&] {
+ switch (aPortalKind) {
+ case PortalKind::FilePicker:
+ return StaticPrefs::widget_use_xdg_desktop_portal_file_picker();
+ case PortalKind::MimeHandler:
+ // Mime portal breaks default browser handling, see bug 1516290.
+ autoBehavior = IsRunningUnderFlatpakOrSnap();
+ return StaticPrefs::widget_use_xdg_desktop_portal_mime_handler();
+ case PortalKind::Settings:
+ autoBehavior = true;
+ return StaticPrefs::widget_use_xdg_desktop_portal_settings();
+ case PortalKind::Location:
+ return StaticPrefs::widget_use_xdg_desktop_portal_location();
+ case PortalKind::OpenUri:
+ return StaticPrefs::widget_use_xdg_desktop_portal_open_uri();
+ }
+ return 2;
+ }();
+
+ switch (pref) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return autoBehavior;
+ }
+}
+
+nsTArray<nsCString> ParseTextURIList(const nsACString& aData) {
+ UniquePtr<char[]> data(ToNewCString(aData));
+ gchar** uris = g_uri_list_extract_uris(data.get());
+
+ nsTArray<nsCString> result;
+ for (size_t i = 0; i < g_strv_length(uris); i++) {
+ result.AppendElement(nsCString(uris[i]));
+ }
+
+ g_strfreev(uris);
+ return result;
+}
+
+#ifdef MOZ_WAYLAND
+static gboolean token_failed(gpointer aData);
+
+class XDGTokenRequest {
+ public:
+ void SetTokenID(const char* aTokenID) {
+ LOGW("RequestWaylandFocusPromise() SetTokenID %s", aTokenID);
+ mTransferPromise->Resolve(aTokenID, __func__);
+ }
+ void Cancel() {
+ LOGW("RequestWaylandFocusPromise() canceled");
+ mTransferPromise->Reject(false, __func__);
+ mActivationTimeoutID = 0;
+ }
+
+ XDGTokenRequest(xdg_activation_token_v1* aXdgToken,
+ RefPtr<FocusRequestPromise::Private> aTransferPromise)
+ : mXdgToken(aXdgToken), mTransferPromise(std::move(aTransferPromise)) {
+ mActivationTimeoutID =
+ g_timeout_add(sActivationTimeout, token_failed, this);
+ }
+ ~XDGTokenRequest() {
+ MozClearPointer(mXdgToken, xdg_activation_token_v1_destroy);
+ if (mActivationTimeoutID) {
+ g_source_remove(mActivationTimeoutID);
+ }
+ }
+
+ private:
+ xdg_activation_token_v1* mXdgToken;
+ RefPtr<FocusRequestPromise::Private> mTransferPromise;
+ guint mActivationTimeoutID;
+ // Reject FocusRequestPromise if we don't get XDG token in 0.5 sec.
+ static constexpr int sActivationTimeout = 500;
+};
+
+// Failed to get token in time
+static gboolean token_failed(gpointer data) {
+ UniquePtr<XDGTokenRequest> request(static_cast<XDGTokenRequest*>(data));
+ request->Cancel();
+ return false;
+}
+
+// We've got activation token from Wayland compositor so it's time to use it.
+static void token_done(gpointer data, struct xdg_activation_token_v1* provider,
+ const char* tokenID) {
+ UniquePtr<XDGTokenRequest> request(static_cast<XDGTokenRequest*>(data));
+ request->SetTokenID(tokenID);
+}
+
+static const struct xdg_activation_token_v1_listener token_listener = {
+ token_done,
+};
+#endif
+
+RefPtr<FocusRequestPromise> RequestWaylandFocusPromise() {
+#ifdef MOZ_WAYLAND
+ if (!GdkIsWaylandDisplay() || !KeymapWrapper::GetSeat()) {
+ LOGW("RequestWaylandFocusPromise() failed.");
+ return nullptr;
+ }
+
+ RefPtr<nsWindow> sourceWindow = nsWindow::GetFocusedWindow();
+ if (!sourceWindow || sourceWindow->IsDestroyed()) {
+ LOGW("RequestWaylandFocusPromise() missing source window");
+ return nullptr;
+ }
+
+ xdg_activation_v1* xdg_activation = WaylandDisplayGet()->GetXdgActivation();
+ if (!xdg_activation) {
+ LOGW("RequestWaylandFocusPromise() missing xdg_activation");
+ return nullptr;
+ }
+
+ wl_surface* focusSurface;
+ uint32_t focusSerial;
+ KeymapWrapper::GetFocusInfo(&focusSurface, &focusSerial);
+ if (!focusSurface) {
+ LOGW("RequestWaylandFocusPromise() missing focusSurface");
+ return nullptr;
+ }
+
+ GdkWindow* gdkWindow = sourceWindow->GetToplevelGdkWindow();
+ if (!gdkWindow) {
+ return nullptr;
+ }
+ wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWindow);
+ if (focusSurface != surface) {
+ LOGW("RequestWaylandFocusPromise() missing wl_surface");
+ return nullptr;
+ }
+
+ RefPtr<FocusRequestPromise::Private> transferPromise =
+ new FocusRequestPromise::Private(__func__);
+
+ xdg_activation_token_v1* aXdgToken =
+ xdg_activation_v1_get_activation_token(xdg_activation);
+ xdg_activation_token_v1_add_listener(
+ aXdgToken, &token_listener,
+ new XDGTokenRequest(aXdgToken, transferPromise));
+ xdg_activation_token_v1_set_serial(aXdgToken, focusSerial,
+ KeymapWrapper::GetSeat());
+ xdg_activation_token_v1_set_surface(aXdgToken, focusSurface);
+ xdg_activation_token_v1_commit(aXdgToken);
+
+ LOGW("RequestWaylandFocusPromise() XDG Token sent");
+
+ return transferPromise.forget();
+#else // !defined(MOZ_WAYLAND)
+ return nullptr;
+#endif
+}
+
+// https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html
+static nsCString GetWindowManagerName() {
+ if (!GdkIsX11Display()) {
+ return {};
+ }
+
+#ifdef MOZ_X11
+ Display* xdisplay = gdk_x11_get_default_xdisplay();
+ Window root_win =
+ GDK_WINDOW_XID(gdk_screen_get_root_window(gdk_screen_get_default()));
+
+ int actual_format_return;
+ Atom actual_type_return;
+ unsigned long nitems_return;
+ unsigned long bytes_after_return;
+ unsigned char* prop_return = nullptr;
+ auto releaseXProperty = MakeScopeExit([&] {
+ if (prop_return) {
+ XFree(prop_return);
+ }
+ });
+
+ Atom property = XInternAtom(xdisplay, "_NET_SUPPORTING_WM_CHECK", true);
+ Atom req_type = XInternAtom(xdisplay, "WINDOW", true);
+ if (!property || !req_type) {
+ return {};
+ }
+ int result =
+ XGetWindowProperty(xdisplay, root_win, property,
+ 0L, // offset
+ sizeof(Window) / 4, // length
+ false, // delete
+ req_type, &actual_type_return, &actual_format_return,
+ &nitems_return, &bytes_after_return, &prop_return);
+
+ if (result != Success || bytes_after_return != 0 || nitems_return != 1) {
+ return {};
+ }
+
+ Window wmWindow = reinterpret_cast<Window*>(prop_return)[0];
+ if (!wmWindow) {
+ return {};
+ }
+
+ XFree(prop_return);
+ prop_return = nullptr;
+
+ property = XInternAtom(xdisplay, "_NET_WM_NAME", true);
+ req_type = XInternAtom(xdisplay, "UTF8_STRING", true);
+ if (!property || !req_type) {
+ return {};
+ }
+ result =
+ XGetWindowProperty(xdisplay, wmWindow, property,
+ 0L, // offset
+ INT32_MAX, // length
+ false, // delete
+ req_type, &actual_type_return, &actual_format_return,
+ &nitems_return, &bytes_after_return, &prop_return);
+ if (result != Success || bytes_after_return != 0) {
+ return {};
+ }
+
+ return nsCString(reinterpret_cast<const char*>(prop_return));
+#else
+ return {};
+#endif
+}
+
+// Getting a reliable identifier is quite tricky. We try to use the standard
+// XDG_CURRENT_DESKTOP environment first, _NET_WM_NAME later, and a set of
+// legacy / non-standard environment variables otherwise.
+//
+// Documentation for some of those can be found in:
+//
+// https://wiki.archlinux.org/title/Environment_variables#Examples
+// https://wiki.archlinux.org/title/Xdg-utils#Environment_variables
+const nsCString& GetDesktopEnvironmentIdentifier() {
+ MOZ_ASSERT(NS_IsMainThread());
+ static const nsDependentCString sIdentifier = [] {
+ nsCString ident = [] {
+ auto Env = [](const char* aKey) -> const char* {
+ const char* v = getenv(aKey);
+ return v && *v ? v : nullptr;
+ };
+ if (const char* currentDesktop = Env("XDG_CURRENT_DESKTOP")) {
+ return nsCString(currentDesktop);
+ }
+ if (auto wm = GetWindowManagerName(); !wm.IsEmpty()) {
+ return wm;
+ }
+ if (const char* sessionDesktop = Env("XDG_SESSION_DESKTOP")) {
+ // This is not really standardized in freedesktop.org, but it is
+ // documented here, and should be set in systemd systems.
+ // https://www.freedesktop.org/software/systemd/man/pam_systemd.html#%24XDG_SESSION_DESKTOP
+ return nsCString(sessionDesktop);
+ }
+ // We try first the DE-specific variables, then SESSION_DESKTOP, to match
+ // the documented order in:
+ // https://wiki.archlinux.org/title/Xdg-utils#Environment_variables
+ if (getenv("GNOME_DESKTOP_SESSION_ID")) {
+ return nsCString("gnome"_ns);
+ }
+ if (getenv("KDE_FULL_SESSION")) {
+ return nsCString("kde"_ns);
+ }
+ if (getenv("MATE_DESKTOP_SESSION_ID")) {
+ return nsCString("mate"_ns);
+ }
+ if (getenv("LXQT_SESSION_CONFIG")) {
+ return nsCString("lxqt"_ns);
+ }
+ if (const char* desktopSession = Env("DESKTOP_SESSION")) {
+ // Try the legacy DESKTOP_SESSION as a last resort.
+ return nsCString(desktopSession);
+ }
+ return nsCString();
+ }();
+ ToLowerCase(ident);
+ // Intentionally put into a ToNewCString copy, rather than just making a
+ // static nsCString to avoid leakchecking errors, since we really want to
+ // leak this string.
+ return nsDependentCString(ToNewCString(ident), ident.Length());
+ }();
+ return sIdentifier;
+}
+
+bool IsGnomeDesktopEnvironment() {
+ static bool sIsGnome =
+ !!FindInReadable("gnome"_ns, GetDesktopEnvironmentIdentifier());
+ return sIsGnome;
+}
+
+bool IsKdeDesktopEnvironment() {
+ static bool sIsKde = GetDesktopEnvironmentIdentifier().EqualsLiteral("kde");
+ return sIsKde;
+}
+
+bool IsCancelledGError(GError* aGError) {
+ return g_error_matches(aGError, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/WidgetUtilsGtk.h b/widget/gtk/WidgetUtilsGtk.h
new file mode 100644
index 0000000000..ffd7425ae5
--- /dev/null
+++ b/widget/gtk/WidgetUtilsGtk.h
@@ -0,0 +1,83 @@
+/* -*- 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/. */
+
+#ifndef WidgetUtilsGtk_h__
+#define WidgetUtilsGtk_h__
+
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mozilla/MozPromise.h"
+
+#include <stdint.h>
+
+typedef struct _GdkDisplay GdkDisplay;
+typedef struct _GdkDevice GdkDevice;
+typedef struct _GError GError;
+typedef union _GdkEvent GdkEvent;
+class nsWindow;
+
+namespace mozilla::widget {
+
+class WidgetUtilsGTK {
+ public:
+ /* See WidgetUtils::IsTouchDeviceSupportPresent(). */
+ static int32_t IsTouchDeviceSupportPresent();
+};
+
+bool IsMainWindowTransparent();
+
+bool GdkIsWaylandDisplay(GdkDisplay* display);
+bool GdkIsX11Display(GdkDisplay* display);
+
+bool GdkIsWaylandDisplay();
+bool GdkIsX11Display();
+
+bool IsXWaylandProtocol();
+
+GdkDevice* GdkGetPointer();
+
+// Sets / returns the last mouse press event we processed.
+void SetLastMousePressEvent(GdkEvent*);
+GdkEvent* GetLastMousePressEvent();
+
+// Return the snap's instance name, or null when not running as a snap.
+const char* GetSnapInstanceName();
+bool IsRunningUnderSnap();
+bool IsRunningUnderFlatpak();
+bool IsPackagedAppFileExists();
+inline bool IsRunningUnderFlatpakOrSnap() {
+ return IsRunningUnderFlatpak() || IsRunningUnderSnap();
+}
+
+enum class PortalKind {
+ FilePicker,
+ MimeHandler,
+ Settings,
+ Location,
+ OpenUri,
+};
+bool ShouldUsePortal(PortalKind);
+
+// Tries to get a descriptive identifier for the desktop environment that the
+// program is running under. Always normalized to lowercase.
+// See the implementation for the different environment variables and desktop
+// information we try to use.
+//
+// If we can't find a reasonable environment, the empty string is returned.
+const nsCString& GetDesktopEnvironmentIdentifier();
+bool IsGnomeDesktopEnvironment();
+bool IsKdeDesktopEnvironment();
+
+// Parse text/uri-list
+nsTArray<nsCString> ParseTextURIList(const nsACString& data);
+
+using FocusRequestPromise = MozPromise<nsCString, bool, false>;
+RefPtr<FocusRequestPromise> RequestWaylandFocusPromise();
+
+bool IsCancelledGError(GError* aGError);
+
+} // namespace mozilla::widget
+
+#endif // WidgetUtilsGtk_h__
diff --git a/widget/gtk/WindowSurface.h b/widget/gtk/WindowSurface.h
new file mode 100644
index 0000000000..858ddde59e
--- /dev/null
+++ b/widget/gtk/WindowSurface.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef _MOZILLA_WIDGET_WINDOW_SURFACE_H
+#define _MOZILLA_WIDGET_WINDOW_SURFACE_H
+
+#include "mozilla/gfx/2D.h"
+#include "Units.h"
+
+namespace mozilla {
+namespace widget {
+
+// A class for drawing to double-buffered windows.
+class WindowSurface {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowSurface);
+
+ // Locks a region of the window for drawing, returning a draw target
+ // capturing the bounds of the provided region.
+ // Implementations must permit invocation from any thread.
+ virtual already_AddRefed<gfx::DrawTarget> Lock(
+ const LayoutDeviceIntRegion& aRegion) = 0;
+
+ // Swaps the provided invalid region from the back buffer to the window.
+ // Implementations must permit invocation from any thread.
+ virtual void Commit(const LayoutDeviceIntRegion& aInvalidRegion) = 0;
+
+ // Whether the window surface represents a fallback method.
+ virtual bool IsFallback() const { return false; }
+
+ protected:
+ virtual ~WindowSurface() = default;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // _MOZILLA_WIDGET_WINDOW_SURFACE_H
diff --git a/widget/gtk/WindowSurfaceProvider.cpp b/widget/gtk/WindowSurfaceProvider.cpp
new file mode 100644
index 0000000000..b346f0783d
--- /dev/null
+++ b/widget/gtk/WindowSurfaceProvider.cpp
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 4; 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 "WindowSurfaceProvider.h"
+
+#include "gfxPlatformGtk.h"
+#include "GtkCompositorWidget.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "nsWindow.h"
+
+#ifdef MOZ_WAYLAND
+# include "mozilla/StaticPrefs_widget.h"
+# include "WindowSurfaceWaylandMultiBuffer.h"
+#endif
+#ifdef MOZ_X11
+# include "mozilla/X11Util.h"
+# include "WindowSurfaceX11Image.h"
+# include "WindowSurfaceX11SHM.h"
+#endif
+
+#undef LOG
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gWidgetLog;
+# define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args)
+#else
+# define LOG(args)
+#endif /* MOZ_LOGGING */
+
+namespace mozilla {
+namespace widget {
+
+using namespace mozilla::layers;
+
+WindowSurfaceProvider::WindowSurfaceProvider()
+ : mWindowSurface(nullptr),
+ mMutex("WindowSurfaceProvider"),
+ mWindowSurfaceValid(false)
+#ifdef MOZ_X11
+ ,
+ mIsShaped(false),
+ mXDepth(0),
+ mXWindow(0),
+ mXVisual(nullptr)
+#endif
+{
+}
+
+WindowSurfaceProvider::~WindowSurfaceProvider() {
+#ifdef MOZ_WAYLAND
+ MOZ_DIAGNOSTIC_ASSERT(!mWidget,
+ "nsWindow reference is still live, we're leaking it!");
+#endif
+#ifdef MOZ_X11
+ MOZ_DIAGNOSTIC_ASSERT(!mXWindow, "mXWindow should be released on quit!");
+#endif
+}
+
+#ifdef MOZ_WAYLAND
+void WindowSurfaceProvider::Initialize(RefPtr<nsWindow> aWidget) {
+ mWindowSurfaceValid = false;
+ mWidget = std::move(aWidget);
+}
+void WindowSurfaceProvider::Initialize(GtkCompositorWidget* aCompositorWidget) {
+ mWindowSurfaceValid = false;
+ mCompositorWidget = aCompositorWidget;
+ mWidget = static_cast<nsWindow*>(aCompositorWidget->RealWidget());
+}
+#endif
+#ifdef MOZ_X11
+void WindowSurfaceProvider::Initialize(Window aWindow, Visual* aVisual,
+ int aDepth, bool aIsShaped) {
+ mWindowSurfaceValid = false;
+ mXWindow = aWindow;
+ mXVisual = aVisual;
+ mXDepth = aDepth;
+ mIsShaped = aIsShaped;
+}
+#endif
+
+void WindowSurfaceProvider::CleanupResources() {
+ MutexAutoLock lock(mMutex);
+ mWindowSurfaceValid = false;
+#ifdef MOZ_WAYLAND
+ mWidget = nullptr;
+#endif
+#ifdef MOZ_X11
+ mXWindow = 0;
+ mXVisual = 0;
+ mXDepth = 0;
+ mIsShaped = false;
+#endif
+}
+
+RefPtr<WindowSurface> WindowSurfaceProvider::CreateWindowSurface() {
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ // We're called too early or we're unmapped.
+ if (!mWidget) {
+ return nullptr;
+ }
+ return MakeRefPtr<WindowSurfaceWaylandMB>(mWidget, mCompositorWidget);
+ }
+#endif
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ // We're called too early or we're unmapped.
+ if (!mXWindow) {
+ return nullptr;
+ }
+ // Blit to the window with the following priority:
+ // 1. MIT-SHM
+ // 2. XPutImage
+# ifdef MOZ_HAVE_SHMIMAGE
+ if (!mIsShaped && nsShmImage::UseShm()) {
+ LOG(("Drawing to Window 0x%lx will use MIT-SHM\n", mXWindow));
+ return MakeRefPtr<WindowSurfaceX11SHM>(DefaultXDisplay(), mXWindow,
+ mXVisual, mXDepth);
+ }
+# endif // MOZ_HAVE_SHMIMAGE
+
+ LOG(("Drawing to Window 0x%lx will use XPutImage\n", mXWindow));
+ return MakeRefPtr<WindowSurfaceX11Image>(DefaultXDisplay(), mXWindow,
+ mXVisual, mXDepth, mIsShaped);
+ }
+#endif
+ MOZ_RELEASE_ASSERT(false);
+}
+
+already_AddRefed<gfx::DrawTarget>
+WindowSurfaceProvider::StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) {
+ if (aInvalidRegion.IsEmpty()) {
+ return nullptr;
+ }
+
+ MutexAutoLock lock(mMutex);
+
+ if (!mWindowSurfaceValid) {
+ mWindowSurface = nullptr;
+ mWindowSurfaceValid = true;
+ }
+
+ if (!mWindowSurface) {
+ mWindowSurface = CreateWindowSurface();
+ if (!mWindowSurface) {
+ return nullptr;
+ }
+ }
+
+ *aBufferMode = BufferMode::BUFFER_NONE;
+ RefPtr<gfx::DrawTarget> dt = mWindowSurface->Lock(aInvalidRegion);
+#ifdef MOZ_X11
+ if (!dt && GdkIsX11Display() && !mWindowSurface->IsFallback()) {
+ // We can't use WindowSurfaceX11Image fallback on Wayland but
+ // Lock() call on WindowSurfaceWayland should never fail.
+ gfxWarningOnce()
+ << "Failed to lock WindowSurface, falling back to XPutImage backend.";
+ mWindowSurface = MakeRefPtr<WindowSurfaceX11Image>(
+ DefaultXDisplay(), mXWindow, mXVisual, mXDepth, mIsShaped);
+ dt = mWindowSurface->Lock(aInvalidRegion);
+ }
+#endif
+ return dt.forget();
+}
+
+void WindowSurfaceProvider::EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
+ MutexAutoLock lock(mMutex);
+ // Commit to mWindowSurface only if we have a valid one.
+ if (!mWindowSurface || !mWindowSurfaceValid) {
+ return;
+ }
+#if defined(MOZ_WAYLAND)
+ if (GdkIsWaylandDisplay()) {
+ // We're called too early or we're unmapped.
+ // Don't draw anything.
+ if (!mWidget || !mWidget->IsMapped()) {
+ return;
+ }
+ if (moz_container_wayland_is_commiting_to_parent(
+ mWidget->GetMozContainer())) {
+ // If we're drawing directly to wl_surface owned by Gtk we need to use it
+ // in main thread to sync with Gtk access to it.
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WindowSurfaceProvider::EndRemoteDrawingInRegion",
+ [widget = RefPtr{mWidget}, this, aInvalidRegion]() {
+ if (!widget->IsMapped()) {
+ return;
+ }
+ MutexAutoLock lock(mMutex);
+ // Commit to mWindowSurface only when we have a valid one.
+ if (mWindowSurface && mWindowSurfaceValid) {
+ mWindowSurface->Commit(aInvalidRegion);
+ }
+ }));
+ return;
+ }
+ }
+#endif
+ mWindowSurface->Commit(aInvalidRegion);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/WindowSurfaceProvider.h b/widget/gtk/WindowSurfaceProvider.h
new file mode 100644
index 0000000000..a040bbe395
--- /dev/null
+++ b/widget/gtk/WindowSurfaceProvider.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_PROVIDER_H
+#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_PROVIDER_H
+
+#include <gdk/gdk.h>
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/widget/WindowSurface.h"
+#include "Units.h"
+#include "mozilla/ScopeExit.h"
+
+#ifdef MOZ_X11
+# include <X11/Xlib.h> // for Window, Display, Visual, etc.
+# include "X11UndefineNone.h"
+#endif
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+class GtkCompositorWidget;
+
+/*
+ * Holds the logic for creating WindowSurface's for a GTK nsWindow.
+ * The main purpose of this class is to allow sharing of logic between
+ * nsWindow and GtkCompositorWidget, for when OMTC is enabled or disabled.
+ */
+class WindowSurfaceProvider final {
+ public:
+ WindowSurfaceProvider();
+ ~WindowSurfaceProvider();
+
+ /**
+ * Initializes the WindowSurfaceProvider by giving it the window
+ * handle and display to attach to. WindowSurfaceProvider doesn't
+ * own the Display, Window, etc, and they must continue to exist
+ * while WindowSurfaceProvider is used.
+ */
+#ifdef MOZ_WAYLAND
+ void Initialize(RefPtr<nsWindow> aWidget);
+ void Initialize(GtkCompositorWidget* aCompositorWidget);
+#endif
+#ifdef MOZ_X11
+ void Initialize(Window aWindow, Visual* aVisual, int aDepth, bool aIsShaped);
+#endif
+
+ /**
+ * Releases any surfaces created by this provider.
+ * This is used by GtkCompositorWidget to get rid
+ * of resources.
+ */
+ void CleanupResources();
+
+ already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode);
+ void EndRemoteDrawingInRegion(gfx::DrawTarget* aDrawTarget,
+ const LayoutDeviceIntRegion& aInvalidRegion);
+
+ private:
+ RefPtr<WindowSurface> CreateWindowSurface();
+ void CleanupWindowSurface();
+
+ RefPtr<WindowSurface> mWindowSurface;
+
+ /* While CleanupResources() can be called from Main thread when nsWindow is
+ * destroyed/hidden, StartRemoteDrawingInRegion()/EndRemoteDrawingInRegion()
+ * is called from Compositor thread during rendering.
+ *
+ * As nsWindow CleanupResources() call comes from Gtk/X11 we can't synchronize
+ * that with WebRender so we use lock to synchronize the access.
+ */
+ mozilla::Mutex mMutex MOZ_UNANNOTATED;
+ // WindowSurface needs to be re-created as underlying window was changed.
+ mozilla::Atomic<bool> mWindowSurfaceValid;
+#ifdef MOZ_WAYLAND
+ RefPtr<nsWindow> mWidget;
+ // WindowSurfaceProvider is owned by GtkCompositorWidget so we don't need
+ // to reference it.
+ GtkCompositorWidget* mCompositorWidget = nullptr;
+#endif
+#ifdef MOZ_X11
+ bool mIsShaped;
+ int mXDepth;
+ Window mXWindow;
+ Visual* mXVisual;
+#endif
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_PROVIDER_H
diff --git a/widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp b/widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp
new file mode 100644
index 0000000000..96f478b726
--- /dev/null
+++ b/widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp
@@ -0,0 +1,418 @@
+/* -*- Mode: C++; tab-width: 4; 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 "WindowSurfaceWaylandMultiBuffer.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <prenv.h>
+
+#include "gfx2DGlue.h"
+#include "gfxPlatform.h"
+#include "MozContainer.h"
+#include "GtkCompositorWidget.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/Tools.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/WidgetUtils.h"
+
+#undef LOG
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gWidgetWaylandLog;
+# define LOGWAYLAND(...) \
+ MOZ_LOG(gWidgetWaylandLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# define LOGWAYLAND(...)
+#endif /* MOZ_LOGGING */
+
+namespace mozilla::widget {
+
+/*
+ Wayland multi-thread rendering scheme
+
+ Every rendering thread (main thread, compositor thread) contains its own
+ nsWaylandDisplay object connected to Wayland compositor (Mutter, Weston, etc.)
+
+ WindowSurfaceWayland implements WindowSurface class and draws nsWindow by
+ WindowSurface interface (Lock, Commit) to screen through nsWaylandDisplay.
+
+ ----------------------
+ | Wayland compositor |
+ ----------------------
+ ^
+ |
+ ----------------------
+ | nsWaylandDisplay |
+ ----------------------
+ ^ ^
+ | |
+ | |
+ | --------------------------------- ------------------
+ | | WindowSurfaceWayland |<------>| nsWindow |
+ | | | ------------------
+ | | ----------------------- |
+ | | | WaylandBufferSHM | |
+ | | | | |
+ | | | ------------------- | |
+ | | | | WaylandShmPool | | |
+ | | | ------------------- | |
+ | | ----------------------- |
+ | | |
+ | | ----------------------- |
+ | | | WaylandBufferSHM | |
+ | | | | |
+ | | | ------------------- | |
+ | | | | WaylandShmPool | | |
+ | | | ------------------- | |
+ | | ----------------------- |
+ | ---------------------------------
+ |
+ |
+ --------------------------------- ------------------
+ | WindowSurfaceWayland |<------>| nsWindow |
+ | | ------------------
+ | ----------------------- |
+ | | WaylandBufferSHM | |
+ | | | |
+ | | ------------------- | |
+ | | | WaylandShmPool | | |
+ | | ------------------- | |
+ | ----------------------- |
+ | |
+ | ----------------------- |
+ | | WaylandBufferSHM | |
+ | | | |
+ | | ------------------- | |
+ | | | WaylandShmPool | | |
+ | | ------------------- | |
+ | ----------------------- |
+ ---------------------------------
+
+
+nsWaylandDisplay
+
+Is our connection to Wayland display server,
+holds our display connection (wl_display) and event queue (wl_event_queue).
+
+nsWaylandDisplay is created for every thread which sends data to Wayland
+compositor. Wayland events for main thread is served by default Gtk+ loop,
+for other threads (compositor) we must create wl_event_queue and run event loop.
+
+
+WindowSurfaceWayland
+
+Is a Wayland implementation of WindowSurface class for WindowSurfaceProvider,
+we implement Lock() and Commit() interfaces from WindowSurface
+for actual drawing.
+
+One WindowSurfaceWayland draws one nsWindow so those are tied 1:1.
+At Wayland level it holds one wl_surface object.
+
+To perform visualiation of nsWindow, WindowSurfaceWayland contains one
+wl_surface and two wl_buffer objects (owned by WaylandBufferSHM)
+as we use double buffering. When nsWindow drawing is finished to wl_buffer,
+the wl_buffer is attached to wl_surface and it's sent to Wayland compositor.
+
+When there's no wl_buffer available for drawing (all wl_buffers are locked in
+compositor for instance) we store the drawing to WindowImageSurface object
+and draw later when wl_buffer becomes available or discard the
+WindowImageSurface cache when whole screen is invalidated.
+
+WaylandBufferSHM
+
+Is a class which provides a wl_buffer for drawing.
+Wl_buffer is a main Wayland object with actual graphics data.
+Wl_buffer basically represent one complete window screen.
+When double buffering is involved every window (GdkWindow for instance)
+utilises two wl_buffers which are cycled. One is filed with data by application
+and one is rendered by compositor.
+
+WaylandBufferSHM is implemented by shared memory (shm).
+It owns wl_buffer object, owns WaylandShmPool
+(which provides the shared memory) and ties them together.
+
+WaylandShmPool
+
+WaylandShmPool acts as a manager of shared memory for WaylandBufferSHM.
+Allocates it, holds reference to it and releases it.
+
+We allocate shared memory (shm) by mmap(..., MAP_SHARED,...) as an interface
+between us and wayland compositor. We draw our graphics data to the shm and
+handle to wayland compositor by WaylandBufferSHM/WindowSurfaceWayland
+(wl_buffer/wl_surface).
+*/
+
+using gfx::DataSourceSurface;
+
+#define BACK_BUFFER_NUM 3
+
+WindowSurfaceWaylandMB::WindowSurfaceWaylandMB(
+ RefPtr<nsWindow> aWindow, GtkCompositorWidget* aCompositorWidget)
+ : mSurfaceLock("WindowSurfaceWayland lock"),
+ mWindow(std::move(aWindow)),
+ mCompositorWidget(aCompositorWidget),
+ mFrameInProcess(false),
+ mCallbackRequested(false) {}
+
+bool WindowSurfaceWaylandMB::MaybeUpdateWindowSize() {
+ // We want to get window size from compositor widget as it matches window
+ // size used by parent RenderCompositorSWGL rendrer.
+ // For main thread rendering mCompositorWidget is not available so get
+ // window size directly from nsWindow.
+ LayoutDeviceIntSize newWindowSize = mCompositorWidget
+ ? mCompositorWidget->GetClientSize()
+ : mWindow->GetClientSize();
+ if (mWindowSize != newWindowSize) {
+ mWindowSize = newWindowSize;
+ return true;
+ }
+ return false;
+}
+
+already_AddRefed<DrawTarget> WindowSurfaceWaylandMB::Lock(
+ const LayoutDeviceIntRegion& aInvalidRegion) {
+ MutexAutoLock lock(mSurfaceLock);
+
+#ifdef MOZ_LOGGING
+ gfx::IntRect lockRect = aInvalidRegion.GetBounds().ToUnknownRect();
+ LOGWAYLAND("WindowSurfaceWaylandMB::Lock [%p] [%d,%d] -> [%d x %d] rects %d",
+ (void*)mWindow.get(), lockRect.x, lockRect.y, lockRect.width,
+ lockRect.height, aInvalidRegion.GetNumRects());
+#endif
+
+ if (mWindow->GetWindowType() == WindowType::Invisible) {
+ return nullptr;
+ }
+ mFrameInProcess = true;
+
+ CollectPendingSurfaces(lock);
+
+ if (MaybeUpdateWindowSize()) {
+ LOGWAYLAND(" new window size [%d x %d]", mWindowSize.width,
+ mWindowSize.height);
+ if (mInProgressBuffer) {
+ ReturnBufferToPool(lock, mInProgressBuffer);
+ mInProgressBuffer = nullptr;
+ }
+ if (mFrontBuffer) {
+ ReturnBufferToPool(lock, mFrontBuffer);
+ mFrontBuffer = nullptr;
+ }
+ mAvailableBuffers.Clear();
+ }
+
+ if (!mInProgressBuffer) {
+ if (mFrontBuffer && !mFrontBuffer->IsAttached()) {
+ mInProgressBuffer = mFrontBuffer;
+ } else {
+ mInProgressBuffer = ObtainBufferFromPool(lock, mWindowSize);
+ if (!mInProgressBuffer) {
+ return nullptr;
+ }
+ if (mFrontBuffer) {
+ HandlePartialUpdate(lock, aInvalidRegion);
+ ReturnBufferToPool(lock, mFrontBuffer);
+ }
+ }
+ mFrontBuffer = nullptr;
+ mFrontBufferInvalidRegion.SetEmpty();
+ }
+
+ RefPtr<DrawTarget> dt = mInProgressBuffer->Lock();
+ return dt.forget();
+}
+
+void WindowSurfaceWaylandMB::HandlePartialUpdate(
+ const MutexAutoLock& aProofOfLock,
+ const LayoutDeviceIntRegion& aInvalidRegion) {
+ LayoutDeviceIntRegion copyRegion;
+ if (mInProgressBuffer->GetBufferAge() == 2) {
+ copyRegion.Sub(mFrontBufferInvalidRegion, aInvalidRegion);
+ } else {
+ LayoutDeviceIntSize frontBufferSize = mFrontBuffer->GetSize();
+ copyRegion = LayoutDeviceIntRegion(LayoutDeviceIntRect(
+ 0, 0, frontBufferSize.width, frontBufferSize.height));
+ copyRegion.SubOut(aInvalidRegion);
+ }
+
+ if (!copyRegion.IsEmpty()) {
+ RefPtr<DataSourceSurface> dataSourceSurface =
+ mozilla::gfx::CreateDataSourceSurfaceFromData(
+ mFrontBuffer->GetSize().ToUnknownSize(),
+ mFrontBuffer->GetSurfaceFormat(),
+ (const uint8_t*)mFrontBuffer->GetShmPool()->GetImageData(),
+ mFrontBuffer->GetSize().width *
+ BytesPerPixel(mFrontBuffer->GetSurfaceFormat()));
+ RefPtr<DrawTarget> dt = mInProgressBuffer->Lock();
+
+ for (auto iter = copyRegion.RectIter(); !iter.Done(); iter.Next()) {
+ LayoutDeviceIntRect r = iter.Get();
+ dt->CopySurface(dataSourceSurface, r.ToUnknownRect(),
+ gfx::IntPoint(r.x, r.y));
+ }
+ }
+}
+
+void WindowSurfaceWaylandMB::Commit(
+ const LayoutDeviceIntRegion& aInvalidRegion) {
+ MutexAutoLock lock(mSurfaceLock);
+ Commit(lock, aInvalidRegion);
+}
+
+void WindowSurfaceWaylandMB::Commit(
+ const MutexAutoLock& aProofOfLock,
+ const LayoutDeviceIntRegion& aInvalidRegion) {
+#ifdef MOZ_LOGGING
+ gfx::IntRect invalidRect = aInvalidRegion.GetBounds().ToUnknownRect();
+ LOGWAYLAND(
+ "WindowSurfaceWaylandMB::Commit [%p] damage rect [%d, %d] -> [%d x %d] "
+ "Window [%d x %d]\n",
+ (void*)mWindow.get(), invalidRect.x, invalidRect.y, invalidRect.width,
+ invalidRect.height, mWindowSize.width, mWindowSize.height);
+#endif
+
+ if (!mInProgressBuffer) {
+ // invisible window
+ return;
+ }
+ mFrameInProcess = false;
+
+ MozContainer* container = mWindow->GetMozContainer();
+ MozContainerSurfaceLock MozContainerLock(container);
+ struct wl_surface* waylandSurface = MozContainerLock.GetSurface();
+ if (!waylandSurface) {
+ LOGWAYLAND(
+ "WindowSurfaceWaylandMB::Commit [%p] frame queued: can't lock "
+ "wl_surface\n",
+ (void*)mWindow.get());
+ if (!mCallbackRequested) {
+ RefPtr<WindowSurfaceWaylandMB> self(this);
+ moz_container_wayland_add_initial_draw_callback_locked(
+ container, [self, aInvalidRegion]() -> void {
+ MutexAutoLock lock(self->mSurfaceLock);
+ if (!self->mFrameInProcess) {
+ self->Commit(lock, aInvalidRegion);
+ }
+ self->mCallbackRequested = false;
+ });
+ mCallbackRequested = true;
+ }
+ return;
+ }
+
+ if (moz_container_wayland_is_commiting_to_parent(container)) {
+ // When committing to parent surface we must use wl_surface_damage().
+ // A parent surface is created as v.3 object which does not support
+ // wl_surface_damage_buffer().
+ wl_surface_damage(waylandSurface, 0, 0, INT32_MAX, INT32_MAX);
+ } else {
+ for (auto iter = aInvalidRegion.RectIter(); !iter.Done(); iter.Next()) {
+ LayoutDeviceIntRect r = iter.Get();
+ wl_surface_damage_buffer(waylandSurface, r.x, r.y, r.width, r.height);
+ }
+ }
+
+ // aProofOfLock is a kind of substitution of MozContainerSurfaceLock.
+ // MozContainer is locked but MozContainerSurfaceLock doen't convert to
+ // MutexAutoLock& so we use aProofOfLock here.
+ moz_container_wayland_set_scale_factor_locked(
+ aProofOfLock, container, mWindow->GdkCeiledScaleFactor());
+
+ // It's possible that scale factor changed between Lock() and Commit()
+ // but window size is the same.
+ // Don't attach such buffer as it may have incorrect size,
+ // we'll paint new content soon.
+ if (moz_container_wayland_size_matches_scale_factor_locked(
+ aProofOfLock, container, mWindowSize.width, mWindowSize.height)) {
+ mInProgressBuffer->AttachAndCommit(waylandSurface);
+ }
+
+ mInProgressBuffer->ResetBufferAge();
+ mFrontBuffer = mInProgressBuffer;
+ mFrontBufferInvalidRegion = aInvalidRegion;
+ mInProgressBuffer = nullptr;
+
+ EnforcePoolSizeLimit(aProofOfLock);
+ IncrementBufferAge(aProofOfLock);
+
+ if (wl_display_flush(WaylandDisplayGet()->GetDisplay()) == -1) {
+ LOGWAYLAND("WindowSurfaceWaylandMB::Commit [%p] flush failed\n",
+ (void*)mWindow.get());
+ }
+}
+
+RefPtr<WaylandBufferSHM> WindowSurfaceWaylandMB::ObtainBufferFromPool(
+ const MutexAutoLock& aProofOfLock, const LayoutDeviceIntSize& aSize) {
+ if (!mAvailableBuffers.IsEmpty()) {
+ RefPtr<WaylandBufferSHM> buffer = mAvailableBuffers.PopLastElement();
+ mInUseBuffers.AppendElement(buffer);
+ return buffer;
+ }
+
+ RefPtr<WaylandBufferSHM> buffer = WaylandBufferSHM::Create(aSize);
+ if (buffer) {
+ mInUseBuffers.AppendElement(buffer);
+ }
+
+ return buffer;
+}
+
+void WindowSurfaceWaylandMB::ReturnBufferToPool(
+ const MutexAutoLock& aProofOfLock,
+ const RefPtr<WaylandBufferSHM>& aBuffer) {
+ if (aBuffer->IsAttached()) {
+ mPendingBuffers.AppendElement(aBuffer);
+ } else if (aBuffer->IsMatchingSize(mWindowSize)) {
+ mAvailableBuffers.AppendElement(aBuffer);
+ }
+ mInUseBuffers.RemoveElement(aBuffer);
+}
+
+void WindowSurfaceWaylandMB::EnforcePoolSizeLimit(
+ const MutexAutoLock& aProofOfLock) {
+ // Enforce the pool size limit, removing least-recently-used entries as
+ // necessary.
+ while (mAvailableBuffers.Length() > BACK_BUFFER_NUM) {
+ mAvailableBuffers.RemoveElementAt(0);
+ }
+
+ NS_WARNING_ASSERTION(mPendingBuffers.Length() < BACK_BUFFER_NUM,
+ "Are we leaking pending buffers?");
+ NS_WARNING_ASSERTION(mInUseBuffers.Length() < BACK_BUFFER_NUM,
+ "Are we leaking in-use buffers?");
+}
+
+void WindowSurfaceWaylandMB::CollectPendingSurfaces(
+ const MutexAutoLock& aProofOfLock) {
+ mPendingBuffers.RemoveElementsBy([&](auto& buffer) {
+ if (!buffer->IsAttached()) {
+ if (buffer->IsMatchingSize(mWindowSize)) {
+ mAvailableBuffers.AppendElement(std::move(buffer));
+ }
+ return true;
+ }
+ return false;
+ });
+}
+
+void WindowSurfaceWaylandMB::IncrementBufferAge(
+ const MutexAutoLock& aProofOfLock) {
+ for (const RefPtr<WaylandBufferSHM>& buffer : mInUseBuffers) {
+ buffer->IncrementBufferAge();
+ }
+ for (const RefPtr<WaylandBufferSHM>& buffer : mPendingBuffers) {
+ buffer->IncrementBufferAge();
+ }
+ for (const RefPtr<WaylandBufferSHM>& buffer : mAvailableBuffers) {
+ buffer->IncrementBufferAge();
+ }
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/WindowSurfaceWaylandMultiBuffer.h b/widget/gtk/WindowSurfaceWaylandMultiBuffer.h
new file mode 100644
index 0000000000..e14a626de0
--- /dev/null
+++ b/widget/gtk/WindowSurfaceWaylandMultiBuffer.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_MULTI_BUFFER_H
+#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_MULTI_BUFFER_H
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/Mutex.h"
+#include "nsTArray.h"
+#include "nsWaylandDisplay.h"
+#include "nsWindow.h"
+#include "WaylandBuffer.h"
+#include "WindowSurface.h"
+
+namespace mozilla::widget {
+
+using gfx::DrawTarget;
+
+// WindowSurfaceWaylandMB is an abstraction for wl_surface
+// and related management
+class WindowSurfaceWaylandMB : public WindowSurface {
+ public:
+ WindowSurfaceWaylandMB(RefPtr<nsWindow> aWindow,
+ GtkCompositorWidget* aCompositorWidget);
+ ~WindowSurfaceWaylandMB() = default;
+
+ // Lock() / Commit() are called by gecko when Firefox
+ // wants to display something. Lock() returns a DrawTarget
+ // where gecko paints. When gecko is done it calls Commit()
+ // and we try to send the DrawTarget (backed by wl_buffer)
+ // to wayland compositor.
+ //
+ // If we fail (wayland compositor is busy,
+ // wl_surface is not created yet) we queue the painting
+ // and we send it to wayland compositor in FrameCallbackHandler()/
+ // FlushPendingCommits().
+ already_AddRefed<DrawTarget> Lock(
+ const LayoutDeviceIntRegion& aInvalidRegion) override;
+ void Commit(const LayoutDeviceIntRegion& aInvalidRegion) final;
+
+ private:
+ void Commit(const MutexAutoLock& aProofOfLock,
+ const LayoutDeviceIntRegion& aInvalidRegion);
+ RefPtr<WaylandBufferSHM> ObtainBufferFromPool(
+ const MutexAutoLock& aProofOfLock, const LayoutDeviceIntSize& aSize);
+ void ReturnBufferToPool(const MutexAutoLock& aProofOfLock,
+ const RefPtr<WaylandBufferSHM>& aBuffer);
+ void EnforcePoolSizeLimit(const MutexAutoLock& aProofOfLock);
+ void CollectPendingSurfaces(const MutexAutoLock& aProofOfLock);
+ void HandlePartialUpdate(const MutexAutoLock& aProofOfLock,
+ const LayoutDeviceIntRegion& aInvalidRegion);
+ void IncrementBufferAge(const MutexAutoLock& aProofOfLock);
+ // Return true if window size was updated.
+ bool MaybeUpdateWindowSize();
+
+ mozilla::Mutex mSurfaceLock MOZ_UNANNOTATED;
+
+ RefPtr<nsWindow> mWindow;
+ // WindowSurfaceWaylandMB is owned by GtkCompositorWidget so we can't
+ // reference it.
+ GtkCompositorWidget* mCompositorWidget;
+ LayoutDeviceIntSize mWindowSize;
+
+ RefPtr<WaylandBufferSHM> mInProgressBuffer;
+ RefPtr<WaylandBufferSHM> mFrontBuffer;
+ LayoutDeviceIntRegion mFrontBufferInvalidRegion;
+
+ // buffer pool
+ nsTArray<RefPtr<WaylandBufferSHM>> mInUseBuffers;
+ nsTArray<RefPtr<WaylandBufferSHM>> mPendingBuffers;
+ nsTArray<RefPtr<WaylandBufferSHM>> mAvailableBuffers;
+
+ // delayed commits
+ bool mFrameInProcess;
+ bool mCallbackRequested;
+};
+
+} // namespace mozilla::widget
+
+#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_WAYLAND_MULTI_BUFFER_H
diff --git a/widget/gtk/WindowSurfaceX11.cpp b/widget/gtk/WindowSurfaceX11.cpp
new file mode 100644
index 0000000000..36b238a98b
--- /dev/null
+++ b/widget/gtk/WindowSurfaceX11.cpp
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 4; 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 "WindowSurfaceX11.h"
+#include "gfxPlatform.h"
+
+namespace mozilla::widget {
+
+WindowSurfaceX11::WindowSurfaceX11(Display* aDisplay, Window aWindow,
+ Visual* aVisual, unsigned int aDepth)
+ : mDisplay(aDisplay),
+ mWindow(aWindow),
+ mVisual(aVisual),
+ mDepth(aDepth),
+ mFormat(GetVisualFormat(aVisual, aDepth)) {}
+
+/* static */
+gfx::SurfaceFormat WindowSurfaceX11::GetVisualFormat(const Visual* aVisual,
+ unsigned int aDepth) {
+ switch (aDepth) {
+ case 32:
+ if (aVisual->red_mask == 0xff0000 && aVisual->green_mask == 0xff00 &&
+ aVisual->blue_mask == 0xff) {
+ return gfx::SurfaceFormat::B8G8R8A8;
+ }
+ break;
+ case 24:
+ if (aVisual->red_mask == 0xff0000 && aVisual->green_mask == 0xff00 &&
+ aVisual->blue_mask == 0xff) {
+ return gfx::SurfaceFormat::B8G8R8X8;
+ }
+ break;
+ case 16:
+ if (aVisual->red_mask == 0xf800 && aVisual->green_mask == 0x07e0 &&
+ aVisual->blue_mask == 0x1f) {
+ return gfx::SurfaceFormat::R5G6B5_UINT16;
+ }
+ break;
+ }
+
+ return gfx::SurfaceFormat::UNKNOWN;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/WindowSurfaceX11.h b/widget/gtk/WindowSurfaceX11.h
new file mode 100644
index 0000000000..dda17bf0d0
--- /dev/null
+++ b/widget/gtk/WindowSurfaceX11.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_H
+#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_H
+
+#ifdef MOZ_X11
+
+# include "mozilla/widget/WindowSurface.h"
+# include "mozilla/gfx/Types.h"
+
+# include <X11/Xlib.h>
+# include "X11UndefineNone.h"
+
+namespace mozilla::widget {
+
+class WindowSurfaceX11 : public WindowSurface {
+ public:
+ WindowSurfaceX11(Display* aDisplay, Window aWindow, Visual* aVisual,
+ unsigned int aDepth);
+
+ protected:
+ static gfx::SurfaceFormat GetVisualFormat(const Visual* aVisual,
+ unsigned int aDepth);
+
+ Display* const mDisplay;
+ const Window mWindow;
+ Visual* const mVisual;
+ const unsigned int mDepth;
+ const gfx::SurfaceFormat mFormat;
+};
+
+} // namespace mozilla::widget
+
+#endif // MOZ_X11
+#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_H
diff --git a/widget/gtk/WindowSurfaceX11Image.cpp b/widget/gtk/WindowSurfaceX11Image.cpp
new file mode 100644
index 0000000000..f797bfafad
--- /dev/null
+++ b/widget/gtk/WindowSurfaceX11Image.cpp
@@ -0,0 +1,272 @@
+/* -*- Mode: C++; tab-width: 4; 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 "WindowSurfaceX11Image.h"
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Tools.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "gfxPlatform.h"
+#include "gfx2DGlue.h"
+
+#include <X11/extensions/shape.h>
+
+namespace mozilla {
+namespace widget {
+
+using namespace mozilla::gfx;
+
+// gfxImageSurface pixel format configuration.
+#define SHAPED_IMAGE_SURFACE_BPP 4
+#ifdef IS_BIG_ENDIAN
+# define SHAPED_IMAGE_SURFACE_ALPHA_INDEX 0
+#else
+# define SHAPED_IMAGE_SURFACE_ALPHA_INDEX 3
+#endif
+
+WindowSurfaceX11Image::WindowSurfaceX11Image(Display* aDisplay, Window aWindow,
+ Visual* aVisual,
+ unsigned int aDepth,
+ bool aIsShaped)
+ : WindowSurfaceX11(aDisplay, aWindow, aVisual, aDepth),
+ mTransparencyBitmap(nullptr),
+ mTransparencyBitmapWidth(0),
+ mTransparencyBitmapHeight(0),
+ mIsShaped(aIsShaped),
+ mWindowParent(0) {
+ if (!mIsShaped) {
+ return;
+ }
+
+ Window root, *children = nullptr;
+ unsigned int childrenNum;
+ if (XQueryTree(mDisplay, mWindow, &root, &mWindowParent, &children,
+ &childrenNum)) {
+ if (children) {
+ XFree((char*)children);
+ }
+ }
+}
+
+WindowSurfaceX11Image::~WindowSurfaceX11Image() {
+ if (mTransparencyBitmap) {
+ delete[] mTransparencyBitmap;
+ }
+}
+
+already_AddRefed<gfx::DrawTarget> WindowSurfaceX11Image::Lock(
+ const LayoutDeviceIntRegion& aRegion) {
+ gfx::IntRect bounds = aRegion.GetBounds().ToUnknownRect();
+ gfx::IntSize size(bounds.XMost(), bounds.YMost());
+
+ if (!mWindowSurface || mWindowSurface->CairoStatus() ||
+ !(size <= mWindowSurface->GetSize())) {
+ mWindowSurface = new gfxXlibSurface(mDisplay, mWindow, mVisual, size);
+ }
+ if (mWindowSurface->CairoStatus()) {
+ return nullptr;
+ }
+
+ if (!mImageSurface || mImageSurface->CairoStatus() ||
+ !(size <= mImageSurface->GetSize())) {
+ gfxImageFormat format = SurfaceFormatToImageFormat(mFormat);
+ if (format == gfx::SurfaceFormat::UNKNOWN) {
+ format = mDepth == 32 ? gfx::SurfaceFormat::A8R8G8B8_UINT32
+ : gfx::SurfaceFormat::X8R8G8B8_UINT32;
+ }
+
+ // Use alpha image format for shaped window as we derive
+ // the shape bitmap from alpha channel. Must match SHAPED_IMAGE_SURFACE_BPP
+ // and SHAPED_IMAGE_SURFACE_ALPHA_INDEX.
+ if (mIsShaped) {
+ format = gfx::SurfaceFormat::A8R8G8B8_UINT32;
+ }
+
+ mImageSurface = new gfxImageSurface(size, format);
+ if (mImageSurface->CairoStatus()) {
+ return nullptr;
+ }
+ }
+
+ gfxImageFormat format = mImageSurface->Format();
+ // Cairo prefers compositing to BGRX instead of BGRA where possible.
+ // Cairo/pixman lacks some fast paths for compositing BGRX onto BGRA, so
+ // just report it as BGRX directly in that case.
+ // Otherwise, for Skia, report it as BGRA to the compositor. The alpha
+ // channel will be discarded when we put the image.
+ if (format == gfx::SurfaceFormat::X8R8G8B8_UINT32) {
+ gfx::BackendType backend = gfxVars::ContentBackend();
+ if (!gfx::Factory::DoesBackendSupportDataDrawtarget(backend)) {
+ backend = gfx::BackendType::SKIA;
+ }
+ if (backend != gfx::BackendType::CAIRO) {
+ format = gfx::SurfaceFormat::A8R8G8B8_UINT32;
+ }
+ }
+
+ return gfxPlatform::CreateDrawTargetForData(
+ mImageSurface->Data(), mImageSurface->GetSize(), mImageSurface->Stride(),
+ ImageFormatToSurfaceFormat(format));
+}
+
+// The transparency bitmap routines are derived form the ones at nsWindow.cpp.
+// The difference here is that we compose to RGBA image and then create
+// the shape mask from final image alpha channel.
+static inline int32_t GetBitmapStride(int32_t width) { return (width + 7) / 8; }
+
+static bool ChangedMaskBits(gchar* aMaskBits, int32_t aMaskWidth,
+ int32_t aMaskHeight, const nsIntRect& aRect,
+ uint8_t* aImageData) {
+ int32_t stride = aMaskWidth * SHAPED_IMAGE_SURFACE_BPP;
+ int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost();
+ int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth);
+ for (y = aRect.y; y < yMax; y++) {
+ gchar* maskBytes = aMaskBits + y * maskBytesPerRow;
+ uint8_t* alphas = aImageData;
+ for (x = aRect.x; x < xMax; x++) {
+ bool newBit = *(alphas + SHAPED_IMAGE_SURFACE_ALPHA_INDEX) > 0x7f;
+ alphas += SHAPED_IMAGE_SURFACE_BPP;
+
+ gchar maskByte = maskBytes[x >> 3];
+ bool maskBit = (maskByte & (1 << (x & 7))) != 0;
+
+ if (maskBit != newBit) {
+ return true;
+ }
+ }
+ aImageData += stride;
+ }
+
+ return false;
+}
+
+static void UpdateMaskBits(gchar* aMaskBits, int32_t aMaskWidth,
+ int32_t aMaskHeight, const nsIntRect& aRect,
+ uint8_t* aImageData) {
+ int32_t stride = aMaskWidth * SHAPED_IMAGE_SURFACE_BPP;
+ int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost();
+ int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth);
+ for (y = aRect.y; y < yMax; y++) {
+ gchar* maskBytes = aMaskBits + y * maskBytesPerRow;
+ uint8_t* alphas = aImageData;
+ for (x = aRect.x; x < xMax; x++) {
+ bool newBit = *(alphas + SHAPED_IMAGE_SURFACE_ALPHA_INDEX) > 0x7f;
+ alphas += SHAPED_IMAGE_SURFACE_BPP;
+
+ gchar mask = 1 << (x & 7);
+ gchar maskByte = maskBytes[x >> 3];
+ // Note: '-newBit' turns 0 into 00...00 and 1 into 11...11
+ maskBytes[x >> 3] = (maskByte & ~mask) | (-newBit & mask);
+ }
+ aImageData += stride;
+ }
+}
+
+void WindowSurfaceX11Image::ResizeTransparencyBitmap(int aWidth, int aHeight) {
+ int32_t actualSize =
+ GetBitmapStride(mTransparencyBitmapWidth) * mTransparencyBitmapHeight;
+ int32_t newSize = GetBitmapStride(aWidth) * aHeight;
+
+ if (actualSize < newSize) {
+ delete[] mTransparencyBitmap;
+ mTransparencyBitmap = new gchar[newSize];
+ }
+
+ mTransparencyBitmapWidth = aWidth;
+ mTransparencyBitmapHeight = aHeight;
+}
+
+void WindowSurfaceX11Image::ApplyTransparencyBitmap() {
+ gfx::IntSize size = mWindowSurface->GetSize();
+ bool maskChanged = true;
+
+ if (!mTransparencyBitmap) {
+ mTransparencyBitmapWidth = size.width;
+ mTransparencyBitmapHeight = size.height;
+
+ int32_t byteSize =
+ GetBitmapStride(mTransparencyBitmapWidth) * mTransparencyBitmapHeight;
+ mTransparencyBitmap = new gchar[byteSize];
+ } else {
+ bool sizeChanged = (size.width != mTransparencyBitmapWidth ||
+ size.height != mTransparencyBitmapHeight);
+
+ if (sizeChanged) {
+ ResizeTransparencyBitmap(size.width, size.height);
+ } else {
+ maskChanged = ChangedMaskBits(
+ mTransparencyBitmap, mTransparencyBitmapWidth,
+ mTransparencyBitmapHeight, nsIntRect(0, 0, size.width, size.height),
+ (uint8_t*)mImageSurface->Data());
+ }
+ }
+
+ if (maskChanged) {
+ UpdateMaskBits(mTransparencyBitmap, mTransparencyBitmapWidth,
+ mTransparencyBitmapHeight,
+ nsIntRect(0, 0, size.width, size.height),
+ (uint8_t*)mImageSurface->Data());
+
+ // We use X11 calls where possible, because GDK handles expose events
+ // for shaped windows in a way that's incompatible with us (Bug 635903).
+ // It doesn't occur when the shapes are set through X.
+ Display* xDisplay = mWindowSurface->XDisplay();
+ Window xDrawable = mWindowSurface->XDrawable();
+ Pixmap maskPixmap = XCreateBitmapFromData(
+ xDisplay, xDrawable, mTransparencyBitmap, mTransparencyBitmapWidth,
+ mTransparencyBitmapHeight);
+ XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap,
+ ShapeSet);
+ if (mWindowParent) {
+ XShapeCombineMask(mDisplay, mWindowParent, ShapeBounding, 0, 0,
+ maskPixmap, ShapeSet);
+ }
+ XFreePixmap(xDisplay, maskPixmap);
+ }
+}
+
+void WindowSurfaceX11Image::Commit(
+ const LayoutDeviceIntRegion& aInvalidRegion) {
+ RefPtr<gfx::DrawTarget> dt = gfx::Factory::CreateDrawTargetForCairoSurface(
+ mWindowSurface->CairoSurface(), mWindowSurface->GetSize());
+ RefPtr<gfx::SourceSurface> surf =
+ gfx::Factory::CreateSourceSurfaceForCairoSurface(
+ mImageSurface->CairoSurface(), mImageSurface->GetSize(),
+ mImageSurface->Format());
+ if (!dt || !surf) {
+ return;
+ }
+
+ gfx::IntRect bounds = aInvalidRegion.GetBounds().ToUnknownRect();
+ if (bounds.IsEmpty()) {
+ return;
+ }
+
+ if (mIsShaped) {
+ ApplyTransparencyBitmap();
+ }
+
+ uint32_t numRects = aInvalidRegion.GetNumRects();
+ if (numRects == 1) {
+ dt->CopySurface(surf, bounds, bounds.TopLeft());
+ } else {
+ AutoTArray<IntRect, 32> rects;
+ rects.SetCapacity(numRects);
+ for (auto iter = aInvalidRegion.RectIter(); !iter.Done(); iter.Next()) {
+ rects.AppendElement(iter.Get().ToUnknownRect());
+ }
+ dt->PushDeviceSpaceClipRects(rects.Elements(), rects.Length());
+
+ dt->DrawSurface(surf, gfx::Rect(bounds), gfx::Rect(bounds),
+ DrawSurfaceOptions(),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+
+ dt->PopClip();
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/WindowSurfaceX11Image.h b/widget/gtk/WindowSurfaceX11Image.h
new file mode 100644
index 0000000000..281199b5dd
--- /dev/null
+++ b/widget/gtk/WindowSurfaceX11Image.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_IMAGE_H
+#define _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_IMAGE_H
+
+#ifdef MOZ_X11
+
+# include <glib.h>
+# include "WindowSurfaceX11.h"
+# include "gfxXlibSurface.h"
+# include "gfxImageSurface.h"
+
+namespace mozilla {
+namespace widget {
+
+class WindowSurfaceX11Image : public WindowSurfaceX11 {
+ public:
+ WindowSurfaceX11Image(Display* aDisplay, Window aWindow, Visual* aVisual,
+ unsigned int aDepth, bool aIsShaped);
+ ~WindowSurfaceX11Image();
+
+ already_AddRefed<gfx::DrawTarget> Lock(
+ const LayoutDeviceIntRegion& aRegion) override;
+ void Commit(const LayoutDeviceIntRegion& aInvalidRegion) override;
+ bool IsFallback() const override { return true; }
+
+ private:
+ void ResizeTransparencyBitmap(int aWidth, int aHeight);
+ void ApplyTransparencyBitmap();
+
+ RefPtr<gfxXlibSurface> mWindowSurface;
+ RefPtr<gfxImageSurface> mImageSurface;
+
+ gchar* mTransparencyBitmap;
+ int32_t mTransparencyBitmapWidth;
+ int32_t mTransparencyBitmapHeight;
+ bool mIsShaped;
+ Window mWindowParent;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // MOZ_X11
+#endif // _MOZILLA_WIDGET_GTK_WINDOW_SURFACE_X11_IMAGE_H
diff --git a/widget/gtk/WindowSurfaceX11SHM.cpp b/widget/gtk/WindowSurfaceX11SHM.cpp
new file mode 100644
index 0000000000..889881d22d
--- /dev/null
+++ b/widget/gtk/WindowSurfaceX11SHM.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 4; 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 "WindowSurfaceX11SHM.h"
+
+namespace mozilla::widget {
+
+WindowSurfaceX11SHM::WindowSurfaceX11SHM(Display* aDisplay, Drawable aWindow,
+ Visual* aVisual, unsigned int aDepth) {
+ mFrontImage = new nsShmImage(aDisplay, aWindow, aVisual, aDepth);
+ mBackImage = new nsShmImage(aDisplay, aWindow, aVisual, aDepth);
+}
+
+already_AddRefed<gfx::DrawTarget> WindowSurfaceX11SHM::Lock(
+ const LayoutDeviceIntRegion& aRegion) {
+ mBackImage.swap(mFrontImage);
+ return mBackImage->CreateDrawTarget(aRegion);
+}
+
+void WindowSurfaceX11SHM::Commit(const LayoutDeviceIntRegion& aInvalidRegion) {
+ mBackImage->Put(aInvalidRegion);
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/WindowSurfaceX11SHM.h b/widget/gtk/WindowSurfaceX11SHM.h
new file mode 100644
index 0000000000..5d12137f7b
--- /dev/null
+++ b/widget/gtk/WindowSurfaceX11SHM.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef _MOZILLA_WIDGET_WINDOW_SURFACE_X11_SHM_H
+#define _MOZILLA_WIDGET_WINDOW_SURFACE_X11_SHM_H
+
+#ifdef MOZ_X11
+
+# include "mozilla/widget/WindowSurface.h"
+# include "nsShmImage.h"
+
+namespace mozilla {
+namespace widget {
+
+class WindowSurfaceX11SHM : public WindowSurface {
+ public:
+ WindowSurfaceX11SHM(Display* aDisplay, Drawable aWindow, Visual* aVisual,
+ unsigned int aDepth);
+
+ already_AddRefed<gfx::DrawTarget> Lock(
+ const LayoutDeviceIntRegion& aRegion) override;
+ void Commit(const LayoutDeviceIntRegion& aInvalidRegion) override;
+
+ private:
+ RefPtr<nsShmImage> mFrontImage;
+ RefPtr<nsShmImage> mBackImage;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // MOZ_X11
+#endif // _MOZILLA_WIDGET_WINDOW_SURFACE_X11_SHM_H
diff --git a/widget/gtk/compat/gdk/gdkdnd.h b/widget/gtk/compat/gdk/gdkdnd.h
new file mode 100644
index 0000000000..bf9888d84f
--- /dev/null
+++ b/widget/gtk/compat/gdk/gdkdnd.h
@@ -0,0 +1,29 @@
+/* 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/. */
+
+#ifndef GDKDND_WRAPPER_H
+#define GDKDND_WRAPPER_H
+
+#define gdk_drag_context_get_actions gdk_drag_context_get_actions_
+#define gdk_drag_context_list_targets gdk_drag_context_list_targets_
+#define gdk_drag_context_get_dest_window gdk_drag_context_get_dest_window_
+#include_next <gdk/gdkdnd.h>
+#undef gdk_drag_context_get_actions
+#undef gdk_drag_context_list_targets
+#undef gdk_drag_context_get_dest_window
+
+static inline GdkDragAction gdk_drag_context_get_actions(
+ GdkDragContext* context) {
+ return context->actions;
+}
+
+static inline GList* gdk_drag_context_list_targets(GdkDragContext* context) {
+ return context->targets;
+}
+
+static inline GdkWindow* gdk_drag_context_get_dest_window(
+ GdkDragContext* context) {
+ return context->dest_window;
+}
+#endif /* GDKDND_WRAPPER_H */
diff --git a/widget/gtk/compat/gdk/gdkkeysyms.h b/widget/gtk/compat/gdk/gdkkeysyms.h
new file mode 100644
index 0000000000..2f48d61fcc
--- /dev/null
+++ b/widget/gtk/compat/gdk/gdkkeysyms.h
@@ -0,0 +1,266 @@
+/* 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/. */
+
+#ifndef GDKKEYSYMS_WRAPPER_H
+#define GDKKEYSYMS_WRAPPER_H
+
+#include_next <gdk/gdkkeysyms.h>
+
+#ifndef GDK_ISO_Level5_Shift
+# define GDK_ISO_Level5_Shift 0xFE11
+#endif
+
+#ifndef GDK_ISO_Level5_Latch
+# define GDK_ISO_Level5_Latch 0xFE12
+#endif
+
+#ifndef GDK_ISO_Level5_Lock
+# define GDK_ISO_Level5_Lock 0xFE13
+#endif
+
+#ifndef GDK_dead_greek
+# define GDK_dead_greek 0xFE8C
+#endif
+
+#ifndef GDK_ch
+# define GDK_ch 0xFEA0
+#endif
+
+#ifndef GDK_Ch
+# define GDK_Ch 0xFEA1
+#endif
+
+#ifndef GDK_CH
+# define GDK_CH 0xFEA2
+#endif
+
+#ifndef GDK_c_h
+# define GDK_c_h 0xFEA3
+#endif
+
+#ifndef GDK_C_h
+# define GDK_C_h 0xFEA4
+#endif
+
+#ifndef GDK_C_H
+# define GDK_C_H 0xFEA5
+#endif
+
+#ifndef GDK_MonBrightnessUp
+# define GDK_MonBrightnessUp 0x1008FF02
+#endif
+
+#ifndef GDK_MonBrightnessDown
+# define GDK_MonBrightnessDown 0x1008FF03
+#endif
+
+#ifndef GDK_AudioLowerVolume
+# define GDK_AudioLowerVolume 0x1008FF11
+#endif
+
+#ifndef GDK_AudioMute
+# define GDK_AudioMute 0x1008FF12
+#endif
+
+#ifndef GDK_AudioRaiseVolume
+# define GDK_AudioRaiseVolume 0x1008FF13
+#endif
+
+#ifndef GDK_AudioPlay
+# define GDK_AudioPlay 0x1008FF14
+#endif
+
+#ifndef GDK_AudioStop
+# define GDK_AudioStop 0x1008FF15
+#endif
+
+#ifndef GDK_AudioPrev
+# define GDK_AudioPrev 0x1008FF16
+#endif
+
+#ifndef GDK_AudioNext
+# define GDK_AudioNext 0x1008FF17
+#endif
+
+#ifndef GDK_HomePage
+# define GDK_HomePage 0x1008FF18
+#endif
+
+#ifndef GDK_Mail
+# define GDK_Mail 0x1008FF19
+#endif
+
+#ifndef GDK_Search
+# define GDK_Search 0x1008FF1B
+#endif
+
+#ifndef GDK_AudioRecord
+# define GDK_AudioRecord 0x1008FF1C
+#endif
+
+#ifndef GDK_Back
+# define GDK_Back 0x1008FF26
+#endif
+
+#ifndef GDK_Forward
+# define GDK_Forward 0x1008FF27
+#endif
+
+#ifndef GDK_Stop
+# define GDK_Stop 0x1008FF28
+#endif
+
+#ifndef GDK_Refresh
+# define GDK_Refresh 0x1008FF29
+#endif
+
+#ifndef GDK_PowerOff
+# define GDK_PowerOff 0x1008FF2A
+#endif
+
+#ifndef GDK_Eject
+# define GDK_Eject 0x1008FF2C
+#endif
+
+#ifndef GDK_AudioPause
+# define GDK_AudioPause 0x1008FF31
+#endif
+
+#ifndef GDK_BrightnessAdjust
+# define GDK_BrightnessAdjust 0x1008FF3B
+#endif
+
+#ifndef GDK_AudioRewind
+# define GDK_AudioRewind 0x1008FF3E
+#endif
+
+#ifndef GDK_Launch0
+# define GDK_Launch0 0x1008FF40
+#endif
+
+#ifndef GDK_Launch1
+# define GDK_Launch1 0x1008FF41
+#endif
+
+#ifndef GDK_Launch2
+# define GDK_Launch2 0x1008FF42
+#endif
+
+#ifndef GDK_Launch3
+# define GDK_Launch3 0x1008FF43
+#endif
+
+#ifndef GDK_Launch4
+# define GDK_Launch4 0x1008FF44
+#endif
+
+#ifndef GDK_Launch5
+# define GDK_Launch5 0x1008FF45
+#endif
+
+#ifndef GDK_Launch6
+# define GDK_Launch6 0x1008FF46
+#endif
+
+#ifndef GDK_Launch7
+# define GDK_Launch7 0x1008FF47
+#endif
+
+#ifndef GDK_Launch8
+# define GDK_Launch8 0x1008FF48
+#endif
+
+#ifndef GDK_Launch9
+# define GDK_Launch9 0x1008FF49
+#endif
+
+#ifndef GDK_LaunchA
+# define GDK_LaunchA 0x1008FF4A
+#endif
+
+#ifndef GDK_LaunchB
+# define GDK_LaunchB 0x1008FF4B
+#endif
+
+#ifndef GDK_LaunchC
+# define GDK_LaunchC 0x1008FF4C
+#endif
+
+#ifndef GDK_LaunchD
+# define GDK_LaunchD 0x1008FF4D
+#endif
+
+#ifndef GDK_LaunchE
+# define GDK_LaunchE 0x1008FF4E
+#endif
+
+#ifndef GDK_LaunchF
+# define GDK_LaunchF 0x1008FF4F
+#endif
+
+#ifndef GDK_Copy
+# define GDK_Copy 0x1008FF57
+#endif
+
+#ifndef GDK_Cut
+# define GDK_Cut 0x1008FF58
+#endif
+
+#ifndef GDK_Paste
+# define GDK_Paste 0x1008FF6D
+#endif
+
+#ifndef GDK_Reload
+# define GDK_Reload 0x1008FF73
+#endif
+
+#ifndef GDK_AudioRandomPlay
+# define GDK_AudioRandomPlay 0x1008FF99
+#endif
+
+#ifndef GDK_Subtitle
+# define GDK_Subtitle 0x1008FF9A
+#endif
+
+#ifndef GDK_Red
+# define GDK_Red 0x1008FFA3
+#endif
+
+#ifndef GDK_Green
+# define GDK_Green 0x1008FFA4
+#endif
+
+#ifndef GDK_Yellow
+# define GDK_Yellow 0x1008FFA5
+#endif
+
+#ifndef GDK_Blue
+# define GDK_Blue 0x1008FFA6
+#endif
+
+#ifndef GDK_TouchpadToggle
+# define GDK_TouchpadToggle 0x1008FFA9
+#endif
+
+#ifndef GDK_TouchpadOn
+# define GDK_TouchpadOn 0x1008FFB0
+#endif
+
+#ifndef GDK_TouchpadOff
+# define GDK_TouchpadOff 0x1008ffb1
+#endif
+
+#ifndef GDK_LogWindowTree
+# define GDK_LogWindowTree 0x1008FE24
+#endif
+
+#ifndef GDK_LogGrabInfo
+# define GDK_LogGrabInfo 0x1008FE25
+#endif
+
+#ifndef GDK_Sleep
+# define GDK_Sleep 0x1008FF2F
+#endif
+
+#endif /* GDKKEYSYMS_WRAPPER_H */
diff --git a/widget/gtk/compat/gdk/gdkvisual.h b/widget/gtk/compat/gdk/gdkvisual.h
new file mode 100644
index 0000000000..e5187a78da
--- /dev/null
+++ b/widget/gtk/compat/gdk/gdkvisual.h
@@ -0,0 +1,15 @@
+/* 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/. */
+
+#ifndef GDKVISUAL_WRAPPER_H
+#define GDKVISUAL_WRAPPER_H
+
+#define gdk_visual_get_depth gdk_visual_get_depth_
+#include_next <gdk/gdkvisual.h>
+#undef gdk_visual_get_depth
+
+static inline gint gdk_visual_get_depth(GdkVisual* visual) {
+ return visual->depth;
+}
+#endif /* GDKVISUAL_WRAPPER_H */
diff --git a/widget/gtk/compat/gdk/gdkwindow.h b/widget/gtk/compat/gdk/gdkwindow.h
new file mode 100644
index 0000000000..a4d2efbc89
--- /dev/null
+++ b/widget/gtk/compat/gdk/gdkwindow.h
@@ -0,0 +1,27 @@
+/* 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/. */
+
+#ifndef GDKWINDOW_WRAPPER_H
+#define GDKWINDOW_WRAPPER_H
+
+#define gdk_window_get_display gdk_window_get_display_
+#define gdk_window_get_screen gdk_window_get_screen_
+#include_next <gdk/gdkwindow.h>
+#undef gdk_window_get_display
+#undef gdk_window_get_screen
+
+static inline GdkDisplay* gdk_window_get_display(GdkWindow* window) {
+ return gdk_drawable_get_display(GDK_DRAWABLE(window));
+}
+
+static inline GdkScreen* gdk_window_get_screen(GdkWindow* window) {
+ return gdk_drawable_get_screen(window);
+}
+
+#if GDK_PIXBUF_MAJOR == 2 && GDK_PIXBUF_MINOR < 18
+static inline gboolean gdk_window_is_destroyed(GdkWindow* window) {
+ return GDK_WINDOW_OBJECT(window)->destroyed;
+}
+#endif
+#endif /* GDKWINDOW_WRAPPER_H */
diff --git a/widget/gtk/compat/gdk/gdkx.h b/widget/gtk/compat/gdk/gdkx.h
new file mode 100644
index 0000000000..7b0718f3cb
--- /dev/null
+++ b/widget/gtk/compat/gdk/gdkx.h
@@ -0,0 +1,42 @@
+/* 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/. */
+
+#ifndef GDKX_WRAPPER_H
+#define GDKX_WRAPPER_H
+
+#include <gtk/gtkversion.h>
+
+#define gdk_x11_window_foreign_new_for_display \
+ gdk_x11_window_foreign_new_for_display_
+#define gdk_x11_window_lookup_for_display gdk_x11_window_lookup_for_display_
+#define gdk_x11_window_get_xid gdk_x11_window_get_xid_
+#if !GTK_CHECK_VERSION(2, 24, 0)
+# define gdk_x11_set_sm_client_id gdk_x11_set_sm_client_id_
+#endif
+#include_next <gdk/gdkx.h>
+#undef gdk_x11_window_foreign_new_for_display
+#undef gdk_x11_window_lookup_for_display
+#undef gdk_x11_window_get_xid
+
+static inline GdkWindow* gdk_x11_window_foreign_new_for_display(
+ GdkDisplay* display, Window window) {
+ return gdk_window_foreign_new_for_display(display, window);
+}
+
+static inline GdkWindow* gdk_x11_window_lookup_for_display(GdkDisplay* display,
+ Window window) {
+ return gdk_window_lookup_for_display(display, window);
+}
+
+static inline Window gdk_x11_window_get_xid(GdkWindow* window) {
+ return (GDK_WINDOW_XWINDOW(window));
+}
+
+#if !GTK_CHECK_VERSION(2, 24, 0)
+# undef gdk_x11_set_sm_client_id
+static inline void gdk_x11_set_sm_client_id(const gchar* sm_client_id) {
+ gdk_set_sm_client_id(sm_client_id);
+}
+#endif
+#endif /* GDKX_WRAPPER_H */
diff --git a/widget/gtk/compat/glib/gmem.h b/widget/gtk/compat/glib/gmem.h
new file mode 100644
index 0000000000..78f24bbd8d
--- /dev/null
+++ b/widget/gtk/compat/glib/gmem.h
@@ -0,0 +1,48 @@
+/* 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/. */
+
+#ifndef GMEM_WRAPPER_H
+#define GMEM_WRAPPER_H
+
+#define g_malloc_n g_malloc_n_
+#define g_malloc0_n g_malloc0_n_
+#define g_realloc_n g_realloc_n_
+#include_next <glib/gmem.h>
+#undef g_malloc_n
+#undef g_malloc0_n
+#undef g_realloc_n
+
+#include <glib/gmessages.h>
+
+#undef g_new
+#define g_new(type, num) ((type*)g_malloc_n((num), sizeof(type)))
+
+#undef g_new0
+#define g_new0(type, num) ((type*)g_malloc0_n((num), sizeof(type)))
+
+#undef g_renew
+#define g_renew(type, ptr, num) ((type*)g_realloc_n(ptr, (num), sizeof(type)))
+
+#define _CHECK_OVERFLOW(num, type_size) \
+ if (G_UNLIKELY(type_size > 0 && num > G_MAXSIZE / type_size)) { \
+ g_error("%s: overflow allocating %" G_GSIZE_FORMAT "*%" G_GSIZE_FORMAT \
+ " bytes", \
+ G_STRLOC, num, type_size); \
+ }
+
+static inline gpointer g_malloc_n(gsize num, gsize type_size) {
+ _CHECK_OVERFLOW(num, type_size)
+ return g_malloc(num * type_size);
+}
+
+static inline gpointer g_malloc0_n(gsize num, gsize type_size) {
+ _CHECK_OVERFLOW(num, type_size)
+ return g_malloc0(num * type_size);
+}
+
+static inline gpointer g_realloc_n(gpointer ptr, gsize num, gsize type_size) {
+ _CHECK_OVERFLOW(num, type_size)
+ return g_realloc(ptr, num * type_size);
+}
+#endif /* GMEM_WRAPPER_H */
diff --git a/widget/gtk/compat/gtk/gtkwidget.h b/widget/gtk/compat/gtk/gtkwidget.h
new file mode 100644
index 0000000000..21165d61fa
--- /dev/null
+++ b/widget/gtk/compat/gtk/gtkwidget.h
@@ -0,0 +1,43 @@
+/* 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/. */
+
+#ifndef GTKWIDGET_WRAPPER_H
+#define GTKWIDGET_WRAPPER_H
+
+#define gtk_widget_set_mapped gtk_widget_set_mapped_
+#define gtk_widget_get_mapped gtk_widget_get_mapped_
+#define gtk_widget_set_realized gtk_widget_set_realized_
+#define gtk_widget_get_realized gtk_widget_get_realized_
+#include_next <gtk/gtkwidget.h>
+#undef gtk_widget_set_mapped
+#undef gtk_widget_get_mapped
+#undef gtk_widget_set_realized
+#undef gtk_widget_get_realized
+
+#include <gtk/gtkversion.h>
+
+static inline void gtk_widget_set_mapped(GtkWidget* widget, gboolean mapped) {
+ if (mapped)
+ GTK_WIDGET_SET_FLAGS(widget, GTK_MAPPED);
+ else
+ GTK_WIDGET_UNSET_FLAGS(widget, GTK_MAPPED);
+}
+
+static inline gboolean gtk_widget_get_mapped(GtkWidget* widget) {
+ return GTK_WIDGET_MAPPED(widget);
+}
+
+static inline void gtk_widget_set_realized(GtkWidget* widget,
+ gboolean realized) {
+ if (realized)
+ GTK_WIDGET_SET_FLAGS(widget, GTK_REALIZED);
+ else
+ GTK_WIDGET_UNSET_FLAGS(widget, GTK_REALIZED);
+}
+
+static inline gboolean gtk_widget_get_realized(GtkWidget* widget) {
+ return GTK_WIDGET_REALIZED(widget);
+}
+
+#endif /* GTKWIDGET_WRAPPER_H */
diff --git a/widget/gtk/compat/gtk/gtkwindow.h b/widget/gtk/compat/gtk/gtkwindow.h
new file mode 100644
index 0000000000..7c3d5873bd
--- /dev/null
+++ b/widget/gtk/compat/gtk/gtkwindow.h
@@ -0,0 +1,26 @@
+/* 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/. */
+
+#ifndef GTKWINDOW_WRAPPER_H
+#define GTKWINDOW_WRAPPER_H
+
+#define gtk_window_group_get_current_grab gtk_window_group_get_current_grab_
+#define gtk_window_get_window_type gtk_window_get_window_type_
+#include_next <gtk/gtkwindow.h>
+#undef gtk_window_group_get_current_grab
+#undef gtk_window_get_window_type
+
+static inline GtkWidget* gtk_window_group_get_current_grab(
+ GtkWindowGroup* window_group) {
+ if (!window_group->grabs) return NULL;
+
+ return GTK_WIDGET(window_group->grabs->data);
+}
+
+static inline GtkWindowType gtk_window_get_window_type(GtkWindow* window) {
+ gint type;
+ g_object_get(window, "type", &type, (void*)NULL);
+ return (GtkWindowType)type;
+}
+#endif /* GTKWINDOW_WRAPPER_H */
diff --git a/widget/gtk/components.conf b/widget/gtk/components.conf
new file mode 100644
index 0000000000..541df93378
--- /dev/null
+++ b/widget/gtk/components.conf
@@ -0,0 +1,151 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Headers = [
+ '/widget/gtk/nsWidgetFactory.h',
+]
+
+InitFunc = 'nsWidgetGtk2ModuleCtor'
+UnloadFunc = 'nsWidgetGtk2ModuleDtor'
+
+Classes = [
+ {
+ 'cid': '{2d96b3df-c051-11d1-a827-0040959a28c9}',
+ 'contract_ids': ['@mozilla.org/widget/appshell/gtk;1'],
+ 'legacy_constructor': 'nsAppShellConstructor',
+ 'headers': ['/widget/gtk/nsWidgetFactory.h'],
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_UTILITY_AND_GMPLUGIN_PROCESS,
+ },
+ {
+ 'cid': '{e9537f8f-c07e-4435-8ab3-83f1ad6e3bbf}',
+ 'contract_ids': ['@mozilla.org/gfx/parent/screenmanager;1'],
+ 'singleton': True,
+ 'type': 'mozilla::widget::ScreenManager',
+ 'headers': ['mozilla/StaticPtr.h', 'mozilla/widget/ScreenManager.h'],
+ 'constructor': 'mozilla::widget::ScreenManager::GetAddRefedSingleton',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_AND_MAIN_PROCESS,
+ },
+ {
+ 'cid': '{a9339876-0027-430f-b953-84c9c11c2da3}',
+ 'contract_ids': ['@mozilla.org/widget/taskbarprogress/gtk;1'],
+ 'type': 'TaskbarProgress',
+ 'headers': ['/widget/gtk/TaskbarProgress.h'],
+ },
+ {
+ 'cid': '{4364de1a-798e-419c-a6f5-ca28866b6d5f}',
+ 'contract_ids': ['@mozilla.org/parent/colorpicker;1'],
+ 'type': 'nsColorPicker',
+ 'headers': ['/widget/gtk/nsColorPicker.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{1940fed5-7d02-4122-8acf-7abaac698983}',
+ 'contract_ids': ['@mozilla.org/parent/filepicker;1'],
+ 'type': 'nsFilePicker',
+ 'headers': ['/widget/gtk/nsFilePicker.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{948a0023-e3a7-11d2-96cf-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/htmlformatconverter;1'],
+ 'type': 'nsHTMLFormatConverter',
+ 'headers': ['/widget/nsHTMLFormatConverter.h'],
+ },
+ {
+ 'cid': '{b148eed2-236d-11d3-b35c-00a0cc3c1cde}',
+ 'contract_ids': ['@mozilla.org/sound;1'],
+ 'singleton': True,
+ 'type': 'nsISound',
+ 'constructor': 'nsSound::GetInstance',
+ 'headers': ['/widget/gtk/nsSound.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{8b5314bc-db01-11d2-96ce-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/transferable;1'],
+ 'type': 'nsTransferable',
+ 'headers': ['/widget/nsTransferable.h'],
+ },
+ {
+ 'cid': '{e221df9b-3d66-4045-9a66-5720949f8d10}',
+ 'contract_ids': ['@mozilla.org/applicationchooser;1'],
+ 'type': 'nsApplicationChooser',
+ 'headers': ['/widget/gtk/nsApplicationChooser.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{f55f5d31-dbb7-4d0d-9f6f-a4f4cd8e8ef1}',
+ 'contract_ids': ['@mozilla.org/widget/parent/clipboard;1'],
+ 'interfaces': ['nsIClipboard'],
+ 'type': 'nsIClipboard',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}',
+ 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'],
+ 'type': 'nsClipboardHelper',
+ 'headers': ['/widget/nsClipboardHelper.h'],
+ },
+ {
+ 'cid': '{0ba77e04-2adb-422f-af01-5a57b8013100}',
+ 'contract_ids': ['@mozilla.org/widget/parent/dragservice;1'],
+ 'singleton': True,
+ 'type': 'nsDragService',
+ 'headers': ['/widget/gtk/nsDragService.h'],
+ 'constructor': 'nsDragService::GetInstance',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'name': 'GfxInfo',
+ 'cid': '{d755a760-9f27-11df-0800-200c9a664242}',
+ 'contract_ids': ['@mozilla.org/gfx/info;1'],
+ 'type': 'mozilla::widget::GfxInfo',
+ 'headers': ['/widget/gtk/GfxInfo.h'],
+ 'init_method': 'Init',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_PROCESS,
+ },
+ {
+ 'cid': '{6987230e-0098-4e78-bc5f-1493ee7519fa}',
+ 'contract_ids': ['@mozilla.org/widget/useridleservice;1'],
+ 'singleton': True,
+ 'type': 'nsUserIdleService',
+ 'headers': ['/widget/gtk/nsUserIdleServiceGTK.h'],
+ 'constructor': 'nsUserIdleServiceGTK::GetInstance',
+ },
+]
+
+if defined('NS_PRINTING'):
+ Classes += [
+ {
+ 'cid': '{d3f69889-e13a-4321-980c-a39332e21f34}',
+ 'contract_ids': ['@mozilla.org/gfx/devicecontextspec;1'],
+ 'type': 'nsDeviceContextSpecGTK',
+ 'headers': ['/widget/gtk/nsDeviceContextSpecG.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{06beec76-a183-4d9f-85dd-085f26da565a}',
+ 'contract_ids': ['@mozilla.org/widget/printdialog-service;1'],
+ 'type': 'nsPrintDialogServiceGTK',
+ 'headers': ['/widget/gtk/nsPrintDialogGTK.h'],
+ 'init_method': 'Init',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{841387c8-72e6-484b-9296-bf6eea80d58a}',
+ 'contract_ids': ['@mozilla.org/gfx/printsettings-service;1'],
+ 'type': 'nsPrintSettingsServiceGTK',
+ 'headers': ['/widget/gtk/nsPrintSettingsServiceGTK.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'cid': '{a6cf9129-15b3-11d2-932e-00805f8add32}',
+ 'contract_ids': ['@mozilla.org/gfx/printerlist;1'],
+ 'type': 'nsPrinterListCUPS',
+ 'headers': ['/widget/nsPrinterListCUPS.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ ]
diff --git a/widget/gtk/crashtests/540078-1.xhtml b/widget/gtk/crashtests/540078-1.xhtml
new file mode 100644
index 0000000000..f5de1b9aee
--- /dev/null
+++ b/widget/gtk/crashtests/540078-1.xhtml
@@ -0,0 +1 @@
+<html xmlns="http://www.w3.org/1999/xhtml"><hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><scrollcorner class="zebra"/></hbox><style style="display: none;">.zebra { -moz-appearance: checkbox; }</style></html>
diff --git a/widget/gtk/crashtests/673390-1.html b/widget/gtk/crashtests/673390-1.html
new file mode 100644
index 0000000000..8463f67f05
--- /dev/null
+++ b/widget/gtk/crashtests/673390-1.html
@@ -0,0 +1 @@
+<div style="-moz-appearance: progresschunk; position: fixed"></div>
diff --git a/widget/gtk/crashtests/crashtests.list b/widget/gtk/crashtests/crashtests.list
new file mode 100644
index 0000000000..9ae47c2233
--- /dev/null
+++ b/widget/gtk/crashtests/crashtests.list
@@ -0,0 +1,2 @@
+load 540078-1.xhtml
+load 673390-1.html
diff --git a/widget/gtk/gbm.h b/widget/gtk/gbm.h
new file mode 100644
index 0000000000..bd94fa8967
--- /dev/null
+++ b/widget/gtk/gbm.h
@@ -0,0 +1,480 @@
+/*
+ * Copyright © 2011 Intel Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * Authors:
+ * Benjamin Franzke <benjaminfranzke@googlemail.com>
+ */
+
+#ifndef _GBM_H_
+#define _GBM_H_
+
+#define __GBM__ 1
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \file gbm.h
+ * \brief Generic Buffer Manager
+ */
+
+struct gbm_device;
+struct gbm_bo;
+struct gbm_surface;
+
+/**
+ * \mainpage The Generic Buffer Manager
+ *
+ * This module provides an abstraction that the caller can use to request a
+ * buffer from the underlying memory management system for the platform.
+ *
+ * This allows the creation of portable code whilst still allowing access to
+ * the underlying memory manager.
+ */
+
+/**
+ * Abstraction representing the handle to a buffer allocated by the
+ * manager
+ */
+union gbm_bo_handle {
+ void* ptr;
+ int32_t s32;
+ uint32_t u32;
+ int64_t s64;
+ uint64_t u64;
+};
+
+/** Format of the allocated buffer */
+enum gbm_bo_format {
+ /** RGB with 8 bits per channel in a 32 bit value */
+ GBM_BO_FORMAT_XRGB8888,
+ /** ARGB with 8 bits per channel in a 32 bit value */
+ GBM_BO_FORMAT_ARGB8888
+};
+
+/**
+ * The FourCC format codes are taken from the drm_fourcc.h definition, and
+ * re-namespaced. New GBM formats must not be added, unless they are
+ * identical ports from drm_fourcc.
+ */
+#define __gbm_fourcc_code(a, b, c, d) \
+ ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | \
+ ((uint32_t)(d) << 24))
+
+#define GBM_FORMAT_BIG_ENDIAN \
+ (1 << 31) /* format is big endian instead of little endian */
+
+/* color index */
+#define GBM_FORMAT_C8 __gbm_fourcc_code('C', '8', ' ', ' ') /* [7:0] C */
+
+/* 8 bpp Red */
+#define GBM_FORMAT_R8 __gbm_fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
+
+/* 16 bpp RG */
+#define GBM_FORMAT_GR88 \
+ __gbm_fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
+
+/* 8 bpp RGB */
+#define GBM_FORMAT_RGB332 \
+ __gbm_fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */
+#define GBM_FORMAT_BGR233 \
+ __gbm_fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */
+
+/* 16 bpp RGB */
+#define GBM_FORMAT_XRGB4444 \
+ __gbm_fourcc_code('X', 'R', '1', \
+ '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */
+#define GBM_FORMAT_XBGR4444 \
+ __gbm_fourcc_code('X', 'B', '1', \
+ '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */
+#define GBM_FORMAT_RGBX4444 \
+ __gbm_fourcc_code('R', 'X', '1', \
+ '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */
+#define GBM_FORMAT_BGRX4444 \
+ __gbm_fourcc_code('B', 'X', '1', \
+ '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */
+
+#define GBM_FORMAT_ARGB4444 \
+ __gbm_fourcc_code('A', 'R', '1', \
+ '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */
+#define GBM_FORMAT_ABGR4444 \
+ __gbm_fourcc_code('A', 'B', '1', \
+ '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */
+#define GBM_FORMAT_RGBA4444 \
+ __gbm_fourcc_code('R', 'A', '1', \
+ '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */
+#define GBM_FORMAT_BGRA4444 \
+ __gbm_fourcc_code('B', 'A', '1', \
+ '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */
+
+#define GBM_FORMAT_XRGB1555 \
+ __gbm_fourcc_code('X', 'R', '1', \
+ '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */
+#define GBM_FORMAT_XBGR1555 \
+ __gbm_fourcc_code('X', 'B', '1', \
+ '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */
+#define GBM_FORMAT_RGBX5551 \
+ __gbm_fourcc_code('R', 'X', '1', \
+ '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */
+#define GBM_FORMAT_BGRX5551 \
+ __gbm_fourcc_code('B', 'X', '1', \
+ '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */
+
+#define GBM_FORMAT_ARGB1555 \
+ __gbm_fourcc_code('A', 'R', '1', \
+ '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */
+#define GBM_FORMAT_ABGR1555 \
+ __gbm_fourcc_code('A', 'B', '1', \
+ '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */
+#define GBM_FORMAT_RGBA5551 \
+ __gbm_fourcc_code('R', 'A', '1', \
+ '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */
+#define GBM_FORMAT_BGRA5551 \
+ __gbm_fourcc_code('B', 'A', '1', \
+ '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */
+
+#define GBM_FORMAT_RGB565 \
+ __gbm_fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */
+#define GBM_FORMAT_BGR565 \
+ __gbm_fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */
+
+/* 24 bpp RGB */
+#define GBM_FORMAT_RGB888 \
+ __gbm_fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */
+#define GBM_FORMAT_BGR888 \
+ __gbm_fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */
+
+/* 32 bpp RGB */
+#define GBM_FORMAT_XRGB8888 \
+ __gbm_fourcc_code('X', 'R', '2', \
+ '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
+#define GBM_FORMAT_XBGR8888 \
+ __gbm_fourcc_code('X', 'B', '2', \
+ '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
+#define GBM_FORMAT_RGBX8888 \
+ __gbm_fourcc_code('R', 'X', '2', \
+ '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */
+#define GBM_FORMAT_BGRX8888 \
+ __gbm_fourcc_code('B', 'X', '2', \
+ '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */
+
+#define GBM_FORMAT_ARGB8888 \
+ __gbm_fourcc_code('A', 'R', '2', \
+ '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
+#define GBM_FORMAT_ABGR8888 \
+ __gbm_fourcc_code('A', 'B', '2', \
+ '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */
+#define GBM_FORMAT_RGBA8888 \
+ __gbm_fourcc_code('R', 'A', '2', \
+ '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */
+#define GBM_FORMAT_BGRA8888 \
+ __gbm_fourcc_code('B', 'A', '2', \
+ '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */
+
+#define GBM_FORMAT_XRGB2101010 \
+ __gbm_fourcc_code('X', 'R', '3', \
+ '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */
+#define GBM_FORMAT_XBGR2101010 \
+ __gbm_fourcc_code('X', 'B', '3', \
+ '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */
+#define GBM_FORMAT_RGBX1010102 \
+ __gbm_fourcc_code('R', 'X', '3', \
+ '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */
+#define GBM_FORMAT_BGRX1010102 \
+ __gbm_fourcc_code('B', 'X', '3', \
+ '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */
+
+#define GBM_FORMAT_ARGB2101010 \
+ __gbm_fourcc_code('A', 'R', '3', \
+ '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */
+#define GBM_FORMAT_ABGR2101010 \
+ __gbm_fourcc_code('A', 'B', '3', \
+ '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */
+#define GBM_FORMAT_RGBA1010102 \
+ __gbm_fourcc_code('R', 'A', '3', \
+ '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */
+#define GBM_FORMAT_BGRA1010102 \
+ __gbm_fourcc_code('B', 'A', '3', \
+ '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */
+
+/* packed YCbCr */
+#define GBM_FORMAT_YUYV \
+ __gbm_fourcc_code('Y', 'U', 'Y', \
+ 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */
+#define GBM_FORMAT_YVYU \
+ __gbm_fourcc_code('Y', 'V', 'Y', \
+ 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */
+#define GBM_FORMAT_UYVY \
+ __gbm_fourcc_code('U', 'Y', 'V', \
+ 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */
+#define GBM_FORMAT_VYUY \
+ __gbm_fourcc_code('V', 'Y', 'U', \
+ 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */
+
+#define GBM_FORMAT_AYUV \
+ __gbm_fourcc_code('A', 'Y', 'U', \
+ 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */
+
+/*
+ * 2 plane YCbCr
+ * index 0 = Y plane, [7:0] Y
+ * index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian
+ * or
+ * index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian
+ */
+#define GBM_FORMAT_NV12 \
+ __gbm_fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */
+#define GBM_FORMAT_NV21 \
+ __gbm_fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */
+#define GBM_FORMAT_NV16 \
+ __gbm_fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */
+#define GBM_FORMAT_NV61 \
+ __gbm_fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */
+
+/*
+ * 3 plane YCbCr
+ * index 0: Y plane, [7:0] Y
+ * index 1: Cb plane, [7:0] Cb
+ * index 2: Cr plane, [7:0] Cr
+ * or
+ * index 1: Cr plane, [7:0] Cr
+ * index 2: Cb plane, [7:0] Cb
+ */
+#define GBM_FORMAT_YUV410 \
+ __gbm_fourcc_code('Y', 'U', 'V', \
+ '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */
+#define GBM_FORMAT_YVU410 \
+ __gbm_fourcc_code('Y', 'V', 'U', \
+ '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */
+#define GBM_FORMAT_YUV411 \
+ __gbm_fourcc_code('Y', 'U', '1', \
+ '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */
+#define GBM_FORMAT_YVU411 \
+ __gbm_fourcc_code('Y', 'V', '1', \
+ '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */
+#define GBM_FORMAT_YUV420 \
+ __gbm_fourcc_code('Y', 'U', '1', \
+ '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */
+#define GBM_FORMAT_YVU420 \
+ __gbm_fourcc_code('Y', 'V', '1', \
+ '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */
+#define GBM_FORMAT_YUV422 \
+ __gbm_fourcc_code('Y', 'U', '1', \
+ '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */
+#define GBM_FORMAT_YVU422 \
+ __gbm_fourcc_code('Y', 'V', '1', \
+ '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */
+#define GBM_FORMAT_YUV444 \
+ __gbm_fourcc_code('Y', 'U', '2', \
+ '4') /* non-subsampled Cb (1) and Cr (2) planes */
+#define GBM_FORMAT_YVU444 \
+ __gbm_fourcc_code('Y', 'V', '2', \
+ '4') /* non-subsampled Cr (1) and Cb (2) planes */
+
+struct gbm_format_name_desc {
+ char name[5];
+};
+
+/**
+ * Flags to indicate the intended use for the buffer - these are passed into
+ * gbm_bo_create(). The caller must set the union of all the flags that are
+ * appropriate
+ *
+ * \sa Use gbm_device_is_format_supported() to check if the combination of
+ * format and use flags are supported
+ */
+enum gbm_bo_flags {
+ /**
+ * Buffer is going to be presented to the screen using an API such as KMS
+ */
+ GBM_BO_USE_SCANOUT = (1 << 0),
+ /**
+ * Buffer is going to be used as cursor
+ */
+ GBM_BO_USE_CURSOR = (1 << 1),
+ /**
+ * Deprecated
+ */
+ GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
+ /**
+ * Buffer is to be used for rendering - for example it is going to be used
+ * as the storage for a color buffer
+ */
+ GBM_BO_USE_RENDERING = (1 << 2),
+ /**
+ * Buffer can be used for gbm_bo_write. This is guaranteed to work
+ * with GBM_BO_USE_CURSOR, but may not work for other combinations.
+ */
+ GBM_BO_USE_WRITE = (1 << 3),
+ /**
+ * Buffer is linear, i.e. not tiled.
+ */
+ GBM_BO_USE_LINEAR = (1 << 4),
+};
+
+int gbm_device_get_fd(struct gbm_device* gbm);
+
+const char* gbm_device_get_backend_name(struct gbm_device* gbm);
+
+int gbm_device_is_format_supported(struct gbm_device* gbm, uint32_t format,
+ uint32_t usage);
+
+int gbm_device_get_format_modifier_plane_count(struct gbm_device* gbm,
+ uint32_t format,
+ uint64_t modifier);
+
+void gbm_device_destroy(struct gbm_device* gbm);
+
+struct gbm_device* gbm_create_device(int fd);
+
+struct gbm_bo* gbm_bo_create(struct gbm_device* gbm, uint32_t width,
+ uint32_t height, uint32_t format, uint32_t flags);
+
+struct gbm_bo* gbm_bo_create_with_modifiers(struct gbm_device* gbm,
+ uint32_t width, uint32_t height,
+ uint32_t format,
+ const uint64_t* modifiers,
+ const unsigned int count);
+#define GBM_BO_IMPORT_WL_BUFFER 0x5501
+#define GBM_BO_IMPORT_EGL_IMAGE 0x5502
+#define GBM_BO_IMPORT_FD 0x5503
+#define GBM_BO_IMPORT_FD_MODIFIER 0x5504
+
+struct gbm_import_fd_data {
+ int fd;
+ uint32_t width;
+ uint32_t height;
+ uint32_t stride;
+ uint32_t format;
+};
+
+struct gbm_import_fd_modifier_data {
+ uint32_t width;
+ uint32_t height;
+ uint32_t format;
+ uint32_t num_fds;
+ int fds[4];
+ int strides[4];
+ int offsets[4];
+ uint64_t modifier;
+};
+
+struct gbm_bo* gbm_bo_import(struct gbm_device* gbm, uint32_t type,
+ void* buffer, uint32_t usage);
+
+/**
+ * Flags to indicate the type of mapping for the buffer - these are
+ * passed into gbm_bo_map(). The caller must set the union of all the
+ * flags that are appropriate.
+ *
+ * These flags are independent of the GBM_BO_USE_* creation flags. However,
+ * mapping the buffer may require copying to/from a staging buffer.
+ *
+ * See also: pipe_transfer_usage
+ */
+enum gbm_bo_transfer_flags {
+ /**
+ * Buffer contents read back (or accessed directly) at transfer
+ * create time.
+ */
+ GBM_BO_TRANSFER_READ = (1 << 0),
+ /**
+ * Buffer contents will be written back at unmap time
+ * (or modified as a result of being accessed directly).
+ */
+ GBM_BO_TRANSFER_WRITE = (1 << 1),
+ /**
+ * Read/modify/write
+ */
+ GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE),
+};
+
+void* gbm_bo_map(struct gbm_bo* bo, uint32_t x, uint32_t y, uint32_t width,
+ uint32_t height, uint32_t flags, uint32_t* stride,
+ void** map_data);
+
+void gbm_bo_unmap(struct gbm_bo* bo, void* map_data);
+
+uint32_t gbm_bo_get_width(struct gbm_bo* bo);
+
+uint32_t gbm_bo_get_height(struct gbm_bo* bo);
+
+uint32_t gbm_bo_get_stride(struct gbm_bo* bo);
+
+uint32_t gbm_bo_get_stride_for_plane(struct gbm_bo* bo, int plane);
+
+uint32_t gbm_bo_get_format(struct gbm_bo* bo);
+
+uint32_t gbm_bo_get_bpp(struct gbm_bo* bo);
+
+uint32_t gbm_bo_get_offset(struct gbm_bo* bo, int plane);
+
+struct gbm_device* gbm_bo_get_device(struct gbm_bo* bo);
+
+union gbm_bo_handle gbm_bo_get_handle(struct gbm_bo* bo);
+
+int gbm_bo_get_fd(struct gbm_bo* bo);
+
+uint64_t gbm_bo_get_modifier(struct gbm_bo* bo);
+
+int gbm_bo_get_plane_count(struct gbm_bo* bo);
+
+union gbm_bo_handle gbm_bo_get_handle_for_plane(struct gbm_bo* bo, int plane);
+
+int gbm_bo_write(struct gbm_bo* bo, const void* buf, size_t count);
+
+void gbm_bo_set_user_data(struct gbm_bo* bo, void* data,
+ void (*destroy_user_data)(struct gbm_bo*, void*));
+
+void* gbm_bo_get_user_data(struct gbm_bo* bo);
+
+void gbm_bo_destroy(struct gbm_bo* bo);
+
+struct gbm_surface* gbm_surface_create(struct gbm_device* gbm, uint32_t width,
+ uint32_t height, uint32_t format,
+ uint32_t flags);
+
+struct gbm_surface* gbm_surface_create_with_modifiers(
+ struct gbm_device* gbm, uint32_t width, uint32_t height, uint32_t format,
+ const uint64_t* modifiers, const unsigned int count);
+
+struct gbm_bo* gbm_surface_lock_front_buffer(struct gbm_surface* surface);
+
+void gbm_surface_release_buffer(struct gbm_surface* surface, struct gbm_bo* bo);
+
+int gbm_surface_has_free_buffers(struct gbm_surface* surface);
+
+void gbm_surface_destroy(struct gbm_surface* surface);
+
+char* gbm_format_get_name(uint32_t gbm_format,
+ struct gbm_format_name_desc* desc);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/gtk3drawing.cpp b/widget/gtk/gtk3drawing.cpp
new file mode 100644
index 0000000000..1fa8b95606
--- /dev/null
+++ b/widget/gtk/gtk3drawing.cpp
@@ -0,0 +1,2179 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+/*
+ * This file contains painting functions for each of the gtk2 widgets.
+ * Adapted from the gtkdrawing.c, and gtk+2.0 source.
+ */
+
+#include <gtk/gtk.h>
+#include <gdk/gdkprivate.h>
+#include <string.h>
+#include "gtkdrawing.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ScopeExit.h"
+#include "prinrval.h"
+#include "WidgetStyleCache.h"
+#include "nsString.h"
+#include "nsDebug.h"
+#include "WidgetUtilsGtk.h"
+
+#include <math.h>
+#include <dlfcn.h>
+
+static gboolean checkbox_check_state;
+static gboolean notebook_has_tab_gap;
+
+static ToggleGTKMetrics sCheckboxMetrics;
+static ToggleGTKMetrics sRadioMetrics;
+static ToolbarGTKMetrics sToolbarMetrics;
+static CSDWindowDecorationSize sToplevelWindowDecorationSize;
+static CSDWindowDecorationSize sPopupWindowDecorationSize;
+
+using mozilla::Span;
+
+#define ARROW_UP 0
+#define ARROW_DOWN G_PI
+#define ARROW_RIGHT G_PI_2
+#define ARROW_LEFT (G_PI + G_PI_2)
+
+#if 0
+// It's used for debugging only to compare Gecko widget style with
+// the ones used by Gtk+ applications.
+static void
+style_path_print(GtkStyleContext *context)
+{
+ const GtkWidgetPath* path = gtk_style_context_get_path(context);
+
+ static auto sGtkWidgetPathToStringPtr =
+ (char * (*)(const GtkWidgetPath *))
+ dlsym(RTLD_DEFAULT, "gtk_widget_path_to_string");
+
+ fprintf(stderr, "Style path:\n%s\n\n", sGtkWidgetPathToStringPtr(path));
+}
+#endif
+
+static GtkBorder operator+=(GtkBorder& first, const GtkBorder& second) {
+ first.left += second.left;
+ first.right += second.right;
+ first.top += second.top;
+ first.bottom += second.bottom;
+ return first;
+}
+
+static gint moz_gtk_get_tab_thickness(GtkStyleContext* style);
+
+static void Inset(GdkRectangle*, const GtkBorder&);
+
+static void InsetByMargin(GdkRectangle*, GtkStyleContext* style);
+
+static void moz_gtk_add_style_margin(GtkStyleContext* style, gint* left,
+ gint* top, gint* right, gint* bottom) {
+ GtkBorder margin;
+
+ gtk_style_context_get_margin(style, gtk_style_context_get_state(style),
+ &margin);
+ *left += margin.left;
+ *right += margin.right;
+ *top += margin.top;
+ *bottom += margin.bottom;
+}
+
+static void moz_gtk_add_style_border(GtkStyleContext* style, gint* left,
+ gint* top, gint* right, gint* bottom) {
+ GtkBorder border;
+
+ gtk_style_context_get_border(style, gtk_style_context_get_state(style),
+ &border);
+
+ *left += border.left;
+ *right += border.right;
+ *top += border.top;
+ *bottom += border.bottom;
+}
+
+static void moz_gtk_add_style_padding(GtkStyleContext* style, gint* left,
+ gint* top, gint* right, gint* bottom) {
+ GtkBorder padding;
+
+ gtk_style_context_get_padding(style, gtk_style_context_get_state(style),
+ &padding);
+
+ *left += padding.left;
+ *right += padding.right;
+ *top += padding.top;
+ *bottom += padding.bottom;
+}
+
+static void moz_gtk_add_margin_border_padding(GtkStyleContext* style,
+ gint* left, gint* top,
+ gint* right, gint* bottom) {
+ moz_gtk_add_style_margin(style, left, top, right, bottom);
+ moz_gtk_add_style_border(style, left, top, right, bottom);
+ moz_gtk_add_style_padding(style, left, top, right, bottom);
+}
+
+static void moz_gtk_add_border_padding(GtkStyleContext* style, gint* left,
+ gint* top, gint* right, gint* bottom) {
+ moz_gtk_add_style_border(style, left, top, right, bottom);
+ moz_gtk_add_style_padding(style, left, top, right, bottom);
+}
+
+// In case there's an error in Gtk theme and preferred size is zero,
+// return some sane values to pass mozilla automation tests.
+// It should not happen in real-life.
+#define MIN_WIDGET_SIZE 10
+static void moz_gtk_sanity_preferred_size(GtkRequisition* requisition) {
+ if (requisition->width <= 0) {
+ requisition->width = MIN_WIDGET_SIZE;
+ }
+ if (requisition->height <= 0) {
+ requisition->height = MIN_WIDGET_SIZE;
+ }
+}
+
+// GetStateFlagsFromGtkWidgetState() can be safely used for the specific
+// GtkWidgets that set both prelight and active flags. For other widgets,
+// either the GtkStateFlags or Gecko's GtkWidgetState need to be carefully
+// adjusted to match GTK behavior. Although GTK sets insensitive and focus
+// flags in the generic GtkWidget base class, GTK adds prelight and active
+// flags only to widgets that are expected to demonstrate prelight or active
+// states. This contrasts with HTML where any element may have :active and
+// :hover states, and so Gecko's GtkStateFlags do not necessarily map to GTK
+// flags. Failure to restrict the flags in the same way as GTK can cause
+// generic CSS selectors from some themes to unintentionally match elements
+// that are not expected to change appearance on hover or mouse-down.
+static GtkStateFlags GetStateFlagsFromGtkWidgetState(GtkWidgetState* state) {
+ GtkStateFlags stateFlags = GTK_STATE_FLAG_NORMAL;
+
+ if (state->disabled)
+ stateFlags = GTK_STATE_FLAG_INSENSITIVE;
+ else {
+ if (state->depressed || state->active)
+ stateFlags =
+ static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_ACTIVE);
+ if (state->inHover)
+ stateFlags =
+ static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_PRELIGHT);
+ if (state->focused)
+ stateFlags =
+ static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_FOCUSED);
+ if (state->backdrop)
+ stateFlags =
+ static_cast<GtkStateFlags>(stateFlags | GTK_STATE_FLAG_BACKDROP);
+ }
+
+ return stateFlags;
+}
+
+static GtkStateFlags GetStateFlagsFromGtkTabFlags(GtkTabFlags flags) {
+ return ((flags & MOZ_GTK_TAB_SELECTED) == 0) ? GTK_STATE_FLAG_NORMAL
+ : GTK_STATE_FLAG_ACTIVE;
+}
+
+gint moz_gtk_init() {
+ if (gtk_major_version > 3 ||
+ (gtk_major_version == 3 && gtk_minor_version >= 14))
+ checkbox_check_state = GTK_STATE_FLAG_CHECKED;
+ else
+ checkbox_check_state = GTK_STATE_FLAG_ACTIVE;
+
+ moz_gtk_refresh();
+
+ return MOZ_GTK_SUCCESS;
+}
+
+void moz_gtk_refresh() {
+ if (gtk_check_version(3, 20, 0) != nullptr) {
+ // Deprecated for Gtk >= 3.20+
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_TAB_TOP);
+ gtk_style_context_get_style(style, "has-tab-gap", &notebook_has_tab_gap,
+ NULL);
+ } else {
+ notebook_has_tab_gap = true;
+ }
+
+ sCheckboxMetrics.initialized = false;
+ sRadioMetrics.initialized = false;
+ sToolbarMetrics.initialized = false;
+ sToplevelWindowDecorationSize.initialized = false;
+ sPopupWindowDecorationSize.initialized = false;
+
+ /* This will destroy all of our widgets */
+ ResetWidgetCache();
+}
+
+gint moz_gtk_button_get_default_overflow(gint* border_top, gint* border_left,
+ gint* border_bottom,
+ gint* border_right) {
+ GtkBorder* default_outside_border;
+
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_BUTTON);
+ gtk_style_context_get_style(style, "default-outside-border",
+ &default_outside_border, NULL);
+
+ if (default_outside_border) {
+ *border_top = default_outside_border->top;
+ *border_left = default_outside_border->left;
+ *border_bottom = default_outside_border->bottom;
+ *border_right = default_outside_border->right;
+ gtk_border_free(default_outside_border);
+ } else {
+ *border_top = *border_left = *border_bottom = *border_right = 0;
+ }
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_button_get_default_border(gint* border_top,
+ gint* border_left,
+ gint* border_bottom,
+ gint* border_right) {
+ GtkBorder* default_border;
+
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_BUTTON);
+ gtk_style_context_get_style(style, "default-border", &default_border, NULL);
+
+ if (default_border) {
+ *border_top = default_border->top;
+ *border_left = default_border->left;
+ *border_bottom = default_border->bottom;
+ *border_right = default_border->right;
+ gtk_border_free(default_border);
+ } else {
+ /* see gtkbutton.c */
+ *border_top = *border_left = *border_bottom = *border_right = 1;
+ }
+ return MOZ_GTK_SUCCESS;
+}
+
+gint moz_gtk_splitter_get_metrics(gint orientation, gint* size) {
+ GtkStyleContext* style;
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ style = GetStyleContext(MOZ_GTK_SPLITTER_HORIZONTAL);
+ } else {
+ style = GetStyleContext(MOZ_GTK_SPLITTER_VERTICAL);
+ }
+ gtk_style_context_get_style(style, "handle_size", size, NULL);
+ return MOZ_GTK_SUCCESS;
+}
+
+static void CalculateToolbarButtonMetrics(WidgetNodeType aAppearance,
+ ToolbarButtonGTKMetrics* aMetrics) {
+ gint iconWidth, iconHeight;
+ if (!gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &iconWidth, &iconHeight)) {
+ NS_WARNING("Failed to get Gtk+ icon size for titlebar button!");
+
+ // Use some reasonable fallback size
+ iconWidth = 16;
+ iconHeight = 16;
+ }
+
+ GtkStyleContext* style = GetStyleContext(aAppearance);
+ gint width = 0, height = 0;
+ if (!gtk_check_version(3, 20, 0)) {
+ gtk_style_context_get(style, gtk_style_context_get_state(style),
+ "min-width", &width, "min-height", &height, NULL);
+ }
+
+ // Cover cases when min-width/min-height is not set, it's invalid
+ // or we're running on Gtk+ < 3.20.
+ if (width < iconWidth) width = iconWidth;
+ if (height < iconHeight) height = iconHeight;
+
+ gint left = 0, top = 0, right = 0, bottom = 0;
+ moz_gtk_add_border_padding(style, &left, &top, &right, &bottom);
+
+ // Button size is calculated as min-width/height + border/padding.
+ width += left + right;
+ height += top + bottom;
+
+ // Place icon at button center.
+ aMetrics->iconXPosition = (width - iconWidth) / 2;
+ aMetrics->iconYPosition = (height - iconHeight) / 2;
+
+ aMetrics->minSizeWithBorderMargin.width = width;
+ aMetrics->minSizeWithBorderMargin.height = height;
+}
+
+// We support LTR layout only here for now.
+static void CalculateToolbarButtonSpacing(WidgetNodeType aAppearance,
+ ToolbarButtonGTKMetrics* aMetrics) {
+ GtkStyleContext* style = GetStyleContext(aAppearance);
+ gtk_style_context_get_margin(style, gtk_style_context_get_state(style),
+ &aMetrics->buttonMargin);
+
+ // Get titlebar spacing, a default one is 6 pixels (gtk/gtkheaderbar.c)
+ gint buttonSpacing = 6;
+ g_object_get(GetWidget(MOZ_GTK_HEADER_BAR), "spacing", &buttonSpacing,
+ nullptr);
+
+ // We apply spacing as a margin equally to both adjacent buttons.
+ buttonSpacing /= 2;
+
+ if (!aMetrics->firstButton) {
+ aMetrics->buttonMargin.left += buttonSpacing;
+ }
+ if (!aMetrics->lastButton) {
+ aMetrics->buttonMargin.right += buttonSpacing;
+ }
+
+ aMetrics->minSizeWithBorderMargin.width +=
+ aMetrics->buttonMargin.right + aMetrics->buttonMargin.left;
+ aMetrics->minSizeWithBorderMargin.height +=
+ aMetrics->buttonMargin.top + aMetrics->buttonMargin.bottom;
+}
+
+size_t GetGtkHeaderBarButtonLayout(Span<ButtonLayout> aButtonLayout,
+ bool* aReversedButtonsPlacement) {
+ gchar* decorationLayoutSetting = nullptr;
+ GtkSettings* settings = gtk_settings_get_default();
+ g_object_get(settings, "gtk-decoration-layout", &decorationLayoutSetting,
+ nullptr);
+ auto free = mozilla::MakeScopeExit([&] { g_free(decorationLayoutSetting); });
+
+ // Use a default layout
+ const gchar* decorationLayout = "menu:minimize,maximize,close";
+ if (decorationLayoutSetting) {
+ decorationLayout = decorationLayoutSetting;
+ }
+
+ // "minimize,maximize,close:" layout means buttons are on the opposite
+ // titlebar side. close button is always there.
+ if (aReversedButtonsPlacement) {
+ const char* closeButton = strstr(decorationLayout, "close");
+ const char* separator = strchr(decorationLayout, ':');
+ *aReversedButtonsPlacement =
+ closeButton && separator && closeButton < separator;
+ }
+
+ // We check what position a button string is stored in decorationLayout.
+ //
+ // decorationLayout gets its value from the GNOME preference:
+ // org.gnome.desktop.vm.preferences.button-layout via the
+ // gtk-decoration-layout property.
+ //
+ // Documentation of the gtk-decoration-layout property can be found here:
+ // https://developer.gnome.org/gtk3/stable/GtkSettings.html#GtkSettings--gtk-decoration-layout
+ if (aButtonLayout.IsEmpty()) {
+ return 0;
+ }
+
+ nsDependentCSubstring layout(decorationLayout, strlen(decorationLayout));
+
+ size_t activeButtons = 0;
+ for (const auto& part : layout.Split(':')) {
+ for (const auto& button : part.Split(',')) {
+ if (button.EqualsLiteral("close")) {
+ aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_CLOSE};
+ } else if (button.EqualsLiteral("minimize")) {
+ aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE};
+ } else if (button.EqualsLiteral("maximize")) {
+ aButtonLayout[activeButtons++] = {MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE};
+ }
+ if (activeButtons == aButtonLayout.Length()) {
+ return activeButtons;
+ }
+ }
+ }
+ return activeButtons;
+}
+
+static void EnsureToolbarMetrics() {
+ if (sToolbarMetrics.initialized) {
+ return;
+ }
+ // Make sure we have clean cache after theme reset, etc.
+ memset(&sToolbarMetrics, 0, sizeof(sToolbarMetrics));
+
+ // Calculate titlebar button visibility and positions.
+ ButtonLayout aButtonLayout[TOOLBAR_BUTTONS];
+ size_t activeButtonNums =
+ GetGtkHeaderBarButtonLayout(Span(aButtonLayout), nullptr);
+
+ for (size_t i = 0; i < activeButtonNums; i++) {
+ int buttonIndex =
+ (aButtonLayout[i].mType - MOZ_GTK_HEADER_BAR_BUTTON_CLOSE);
+ ToolbarButtonGTKMetrics* metrics = sToolbarMetrics.button + buttonIndex;
+ metrics->visible = true;
+ // Mark first button
+ if (!i) {
+ metrics->firstButton = true;
+ }
+ // Mark last button.
+ if (i == (activeButtonNums - 1)) {
+ metrics->lastButton = true;
+ }
+
+ CalculateToolbarButtonMetrics(aButtonLayout[i].mType, metrics);
+ CalculateToolbarButtonSpacing(aButtonLayout[i].mType, metrics);
+ }
+
+ sToolbarMetrics.initialized = true;
+}
+
+const ToolbarButtonGTKMetrics* GetToolbarButtonMetrics(
+ WidgetNodeType aAppearance) {
+ EnsureToolbarMetrics();
+
+ int buttonIndex = (aAppearance - MOZ_GTK_HEADER_BAR_BUTTON_CLOSE);
+ NS_ASSERTION(buttonIndex >= 0 && buttonIndex <= TOOLBAR_BUTTONS,
+ "GetToolbarButtonMetrics(): Wrong titlebar button!");
+ return sToolbarMetrics.button + buttonIndex;
+}
+
+static gint moz_gtk_window_decoration_paint(cairo_t* cr,
+ const GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ if (mozilla::widget::GdkIsWaylandDisplay()) {
+ // Doesn't seem to be needed.
+ return MOZ_GTK_SUCCESS;
+ }
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ GtkStyleContext* windowStyle =
+ GetStyleContext(MOZ_GTK_HEADERBAR_WINDOW, state->image_scale);
+ const bool solidDecorations =
+ gtk_style_context_has_class(windowStyle, "solid-csd");
+ GtkStyleContext* decorationStyle =
+ GetStyleContext(solidDecorations ? MOZ_GTK_WINDOW_DECORATION_SOLID
+ : MOZ_GTK_WINDOW_DECORATION,
+ state->image_scale, GTK_TEXT_DIR_LTR, state_flags);
+
+ gtk_render_background(decorationStyle, cr, rect->x, rect->y, rect->width,
+ rect->height);
+ gtk_render_frame(decorationStyle, cr, rect->x, rect->y, rect->width,
+ rect->height);
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_button_paint(cairo_t* cr, const GdkRectangle* rect,
+ GtkWidgetState* state, GtkReliefStyle relief,
+ GtkWidget* widget,
+ GtkTextDirection direction) {
+ if (!widget) {
+ return MOZ_GTK_UNKNOWN_WIDGET;
+ }
+
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ GtkStyleContext* style = gtk_widget_get_style_context(widget);
+ gint x = rect->x, y = rect->y, width = rect->width, height = rect->height;
+
+ gtk_widget_set_direction(widget, direction);
+
+ gtk_style_context_save(style);
+ StyleContextSetScale(style, state->image_scale);
+ gtk_style_context_set_state(style, state_flags);
+
+ if (state->isDefault && relief == GTK_RELIEF_NORMAL && !state->focused &&
+ !(state_flags & GTK_STATE_FLAG_PRELIGHT)) {
+ /* handle default borders both outside and inside the button */
+ gint default_top, default_left, default_bottom, default_right;
+ moz_gtk_button_get_default_overflow(&default_top, &default_left,
+ &default_bottom, &default_right);
+ x -= default_left;
+ y -= default_top;
+ width += default_left + default_right;
+ height += default_top + default_bottom;
+ gtk_render_background(style, cr, x, y, width, height);
+ gtk_render_frame(style, cr, x, y, width, height);
+ moz_gtk_button_get_default_border(&default_top, &default_left,
+ &default_bottom, &default_right);
+ x += default_left;
+ y += default_top;
+ width -= (default_left + default_right);
+ height -= (default_top + default_bottom);
+ } else if (relief != GTK_RELIEF_NONE || state->depressed ||
+ (state_flags & GTK_STATE_FLAG_PRELIGHT)) {
+ /* the following line can trigger an assertion (Crux theme)
+ file ../../gdk/gdkwindow.c: line 1846 (gdk_window_clear_area):
+ assertion `GDK_IS_WINDOW (window)' failed */
+ gtk_render_background(style, cr, x, y, width, height);
+ gtk_render_frame(style, cr, x, y, width, height);
+ }
+
+ if (state->focused) {
+ GtkBorder border;
+ gtk_style_context_get_border(style, state_flags, &border);
+ x += border.left;
+ y += border.top;
+ width -= (border.left + border.right);
+ height -= (border.top + border.bottom);
+ gtk_render_focus(style, cr, x, y, width, height);
+ }
+ gtk_style_context_restore(style);
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_header_bar_button_paint(cairo_t* cr,
+ const GdkRectangle* aRect,
+ GtkWidgetState* state,
+ GtkReliefStyle relief,
+ WidgetNodeType aIconWidgetType,
+ GtkTextDirection direction) {
+ GdkRectangle rect = *aRect;
+ // We need to inset our calculated margin because it also
+ // contains titlebar button spacing.
+ const ToolbarButtonGTKMetrics* metrics = GetToolbarButtonMetrics(
+ aIconWidgetType == MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE
+ ? MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE
+ : aIconWidgetType);
+ Inset(&rect, metrics->buttonMargin);
+
+ GtkWidget* buttonWidget = GetWidget(aIconWidgetType);
+ if (!buttonWidget) {
+ return MOZ_GTK_UNKNOWN_WIDGET;
+ }
+ moz_gtk_button_paint(cr, &rect, state, relief, buttonWidget, direction);
+
+ GtkWidget* iconWidget =
+ gtk_bin_get_child(GTK_BIN(GetWidget(aIconWidgetType)));
+ if (!iconWidget) {
+ return MOZ_GTK_UNKNOWN_WIDGET;
+ }
+ cairo_surface_t* surface =
+ GetWidgetIconSurface(iconWidget, state->image_scale);
+
+ if (surface) {
+ GtkStyleContext* style = gtk_widget_get_style_context(buttonWidget);
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+
+ gtk_style_context_save(style);
+ StyleContextSetScale(style, state->image_scale);
+ gtk_style_context_set_state(style, state_flags);
+
+ /* This is available since Gtk+ 3.10 as well as GtkHeaderBar */
+ gtk_render_icon_surface(style, cr, surface, rect.x + metrics->iconXPosition,
+ rect.y + metrics->iconYPosition);
+ gtk_style_context_restore(style);
+ }
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_toggle_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state, gboolean selected,
+ gboolean inconsistent, gboolean isradio,
+ GtkTextDirection direction) {
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ gint x, y, width, height;
+ GtkStyleContext* style;
+
+ // We need to call this before GetStyleContext, because otherwise we would
+ // reset state flags
+ const ToggleGTKMetrics* metrics =
+ GetToggleMetrics(isradio ? MOZ_GTK_RADIOBUTTON : MOZ_GTK_CHECKBUTTON);
+ // Clamp the rect and paint it center aligned in the rect.
+ x = rect->x;
+ y = rect->y;
+ width = rect->width;
+ height = rect->height;
+
+ if (rect->width < rect->height) {
+ y = rect->y + (rect->height - rect->width) / 2;
+ height = rect->width;
+ }
+
+ if (rect->height < rect->width) {
+ x = rect->x + (rect->width - rect->height) / 2;
+ width = rect->height;
+ }
+
+ if (selected)
+ state_flags =
+ static_cast<GtkStateFlags>(state_flags | checkbox_check_state);
+
+ if (inconsistent)
+ state_flags =
+ static_cast<GtkStateFlags>(state_flags | GTK_STATE_FLAG_INCONSISTENT);
+
+ style = GetStyleContext(isradio ? MOZ_GTK_RADIOBUTTON : MOZ_GTK_CHECKBUTTON,
+ state->image_scale, direction, state_flags);
+
+ if (gtk_check_version(3, 20, 0) == nullptr) {
+ gtk_render_background(style, cr, x, y, width, height);
+ gtk_render_frame(style, cr, x, y, width, height);
+ // Indicator is inset by the toggle's padding and border.
+ gint indicator_x = x + metrics->borderAndPadding.left;
+ gint indicator_y = y + metrics->borderAndPadding.top;
+ gint indicator_width = metrics->minSizeWithBorder.width -
+ metrics->borderAndPadding.left -
+ metrics->borderAndPadding.right;
+ gint indicator_height = metrics->minSizeWithBorder.height -
+ metrics->borderAndPadding.top -
+ metrics->borderAndPadding.bottom;
+ if (isradio) {
+ gtk_render_option(style, cr, indicator_x, indicator_y, indicator_width,
+ indicator_height);
+ } else {
+ gtk_render_check(style, cr, indicator_x, indicator_y, indicator_width,
+ indicator_height);
+ }
+ } else {
+ if (isradio) {
+ gtk_render_option(style, cr, x, y, width, height);
+ } else {
+ gtk_render_check(style, cr, x, y, width, height);
+ }
+ }
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint calculate_button_inner_rect(GtkWidget* button,
+ const GdkRectangle* rect,
+ GdkRectangle* inner_rect,
+ GtkTextDirection direction) {
+ GtkStyleContext* style;
+ GtkBorder border;
+ GtkBorder padding = {0, 0, 0, 0};
+
+ style = gtk_widget_get_style_context(button);
+
+ /* This mirrors gtkbutton's child positioning */
+ gtk_style_context_get_border(style, gtk_style_context_get_state(style),
+ &border);
+ gtk_style_context_get_padding(style, gtk_style_context_get_state(style),
+ &padding);
+
+ inner_rect->x = rect->x + border.left + padding.left;
+ inner_rect->y = rect->y + padding.top + border.top;
+ inner_rect->width =
+ MAX(1, rect->width - padding.left - padding.right - border.left * 2);
+ inner_rect->height =
+ MAX(1, rect->height - padding.top - padding.bottom - border.top * 2);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint calculate_arrow_rect(GtkWidget* arrow, GdkRectangle* rect,
+ GdkRectangle* arrow_rect,
+ GtkTextDirection direction) {
+ /* defined in gtkarrow.c */
+ gfloat arrow_scaling = 0.7;
+ gfloat xalign, xpad;
+ gint extent;
+ gint mxpad, mypad;
+ gfloat mxalign, myalign;
+ GtkMisc* misc = GTK_MISC(arrow);
+
+ gtk_style_context_get_style(gtk_widget_get_style_context(arrow),
+ "arrow_scaling", &arrow_scaling, NULL);
+
+ gtk_misc_get_padding(misc, &mxpad, &mypad);
+ extent = MIN((rect->width - mxpad * 2), (rect->height - mypad * 2)) *
+ arrow_scaling;
+
+ gtk_misc_get_alignment(misc, &mxalign, &myalign);
+
+ xalign = direction == GTK_TEXT_DIR_LTR ? mxalign : 1.0 - mxalign;
+ xpad = mxpad + (rect->width - extent) * xalign;
+
+ arrow_rect->x = direction == GTK_TEXT_DIR_LTR ? floor(rect->x + xpad)
+ : ceil(rect->x + xpad);
+ arrow_rect->y = floor(rect->y + mypad + ((rect->height - extent) * myalign));
+
+ arrow_rect->width = arrow_rect->height = extent;
+
+ return MOZ_GTK_SUCCESS;
+}
+
+/**
+ * Get minimum widget size as sum of margin, padding, border and
+ * min-width/min-height.
+ */
+static void moz_gtk_get_widget_min_size(GtkStyleContext* style, int* width,
+ int* height) {
+ GtkStateFlags state_flags = gtk_style_context_get_state(style);
+ gtk_style_context_get(style, state_flags, "min-height", height, "min-width",
+ width, nullptr);
+
+ GtkBorder border, padding, margin;
+ gtk_style_context_get_border(style, state_flags, &border);
+ gtk_style_context_get_padding(style, state_flags, &padding);
+ gtk_style_context_get_margin(style, state_flags, &margin);
+
+ *width += border.left + border.right + margin.left + margin.right +
+ padding.left + padding.right;
+ *height += border.top + border.bottom + margin.top + margin.bottom +
+ padding.top + padding.bottom;
+}
+
+static void Inset(GdkRectangle* rect, const GtkBorder& aBorder) {
+ rect->x += aBorder.left;
+ rect->y += aBorder.top;
+ rect->width -= aBorder.left + aBorder.right;
+ rect->height -= aBorder.top + aBorder.bottom;
+}
+
+// Inset a rectangle by the margins specified in a style context.
+static void InsetByMargin(GdkRectangle* rect, GtkStyleContext* style) {
+ GtkBorder margin;
+ gtk_style_context_get_margin(style, gtk_style_context_get_state(style),
+ &margin);
+ Inset(rect, margin);
+}
+
+// Inset a rectangle by the border and padding specified in a style context.
+static void InsetByBorderPadding(GdkRectangle* rect, GtkStyleContext* style) {
+ GtkStateFlags state = gtk_style_context_get_state(style);
+ GtkBorder padding, border;
+
+ gtk_style_context_get_padding(style, state, &padding);
+ Inset(rect, padding);
+ gtk_style_context_get_border(style, state, &border);
+ Inset(rect, border);
+}
+
+static void moz_gtk_draw_styled_frame(GtkStyleContext* style, cairo_t* cr,
+ const GdkRectangle* aRect,
+ bool drawFocus) {
+ GdkRectangle rect = *aRect;
+
+ InsetByMargin(&rect, style);
+
+ gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height);
+ gtk_render_frame(style, cr, rect.x, rect.y, rect.width, rect.height);
+ if (drawFocus) {
+ gtk_render_focus(style, cr, rect.x, rect.y, rect.width, rect.height);
+ }
+}
+
+static gint moz_gtk_inner_spin_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_SPINBUTTON, state->image_scale, direction,
+ GetStateFlagsFromGtkWidgetState(state));
+
+ gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+ /* hard code these values */
+ GdkRectangle arrow_rect;
+ arrow_rect.width = 6;
+ arrow_rect.height = 6;
+
+ // align spin to the left
+ arrow_rect.x = rect->x;
+
+ // up button
+ arrow_rect.y = rect->y + (rect->height - arrow_rect.height) / 2 - 3;
+ gtk_render_arrow(style, cr, ARROW_UP, arrow_rect.x, arrow_rect.y,
+ arrow_rect.width);
+
+ // down button
+ arrow_rect.y = rect->y + (rect->height - arrow_rect.height) / 2 + 3;
+ gtk_render_arrow(style, cr, ARROW_DOWN, arrow_rect.x, arrow_rect.y,
+ arrow_rect.width);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_spin_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_SPINBUTTON, state->image_scale, direction);
+ gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_spin_updown_paint(cairo_t* cr, GdkRectangle* rect,
+ gboolean isDown, GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_SPINBUTTON, state->image_scale, direction,
+ GetStateFlagsFromGtkWidgetState(state));
+
+ gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+ /* hard code these values */
+ GdkRectangle arrow_rect;
+ arrow_rect.width = 6;
+ arrow_rect.height = 6;
+ arrow_rect.x = rect->x + (rect->width - arrow_rect.width) / 2;
+ arrow_rect.y = rect->y + (rect->height - arrow_rect.height) / 2;
+ arrow_rect.y += isDown ? -1 : 1;
+
+ gtk_render_arrow(style, cr, isDown ? ARROW_DOWN : ARROW_UP, arrow_rect.x,
+ arrow_rect.y, arrow_rect.width);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+/* See gtk_range_draw() for reference.
+ */
+static gint moz_gtk_scale_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state, GtkOrientation flags,
+ GtkTextDirection direction) {
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ gint x, y, width, height, min_width, min_height;
+ GtkStyleContext* style;
+ GtkBorder margin;
+
+ moz_gtk_get_scale_metrics(flags, &min_width, &min_height);
+
+ WidgetNodeType widget = (flags == GTK_ORIENTATION_HORIZONTAL)
+ ? MOZ_GTK_SCALE_TROUGH_HORIZONTAL
+ : MOZ_GTK_SCALE_TROUGH_VERTICAL;
+ style = GetStyleContext(widget, state->image_scale, direction, state_flags);
+ gtk_style_context_get_margin(style, state_flags, &margin);
+
+ // Clamp the dimension perpendicular to the direction that the slider crosses
+ // to the minimum size.
+ if (flags == GTK_ORIENTATION_HORIZONTAL) {
+ width = rect->width - (margin.left + margin.right);
+ height = min_height - (margin.top + margin.bottom);
+ x = rect->x + margin.left;
+ y = rect->y + (rect->height - height) / 2;
+ } else {
+ width = min_width - (margin.left + margin.right);
+ height = rect->height - (margin.top + margin.bottom);
+ x = rect->x + (rect->width - width) / 2;
+ y = rect->y + margin.top;
+ }
+
+ gtk_render_background(style, cr, x, y, width, height);
+ gtk_render_frame(style, cr, x, y, width, height);
+
+ if (state->focused)
+ gtk_render_focus(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_scale_thumb_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkOrientation flags,
+ GtkTextDirection direction) {
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ GtkStyleContext* style;
+ gint thumb_width, thumb_height, x, y;
+
+ /* determine the thumb size, and position the thumb in the center in the
+ * opposite axis
+ */
+ if (flags == GTK_ORIENTATION_HORIZONTAL) {
+ moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_HORIZONTAL, &thumb_width,
+ &thumb_height);
+ x = rect->x;
+ y = rect->y + (rect->height - thumb_height) / 2;
+ } else {
+ moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_VERTICAL, &thumb_height,
+ &thumb_width);
+ x = rect->x + (rect->width - thumb_width) / 2;
+ y = rect->y;
+ }
+
+ WidgetNodeType widget = (flags == GTK_ORIENTATION_HORIZONTAL)
+ ? MOZ_GTK_SCALE_THUMB_HORIZONTAL
+ : MOZ_GTK_SCALE_THUMB_VERTICAL;
+ style = GetStyleContext(widget, state->image_scale, direction, state_flags);
+ gtk_render_slider(style, cr, x, y, thumb_width, thumb_height, flags);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_hpaned_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL, state->image_scale,
+ GTK_TEXT_DIR_LTR, GetStateFlagsFromGtkWidgetState(state));
+ gtk_render_handle(style, cr, rect->x, rect->y, rect->width, rect->height);
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_vpaned_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL, state->image_scale,
+ GTK_TEXT_DIR_LTR, GetStateFlagsFromGtkWidgetState(state));
+ gtk_render_handle(style, cr, rect->x, rect->y, rect->width, rect->height);
+ return MOZ_GTK_SUCCESS;
+}
+
+// See gtk_entry_draw() for reference.
+static gint moz_gtk_entry_paint(cairo_t* cr, const GdkRectangle* aRect,
+ GtkWidgetState* state, GtkStyleContext* style,
+ WidgetNodeType widget) {
+ GdkRectangle rect = *aRect;
+ gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height);
+
+ // Paint the border, except for 'menulist-textfield' that isn't focused:
+ if (widget != MOZ_GTK_DROPDOWN_ENTRY || state->focused) {
+ gtk_render_frame(style, cr, rect.x, rect.y, rect.width, rect.height);
+ }
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_text_view_paint(cairo_t* cr, GdkRectangle* aRect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ // GtkTextView and GtkScrolledWindow do not set active and prelight flags.
+ // The use of focus with MOZ_GTK_SCROLLED_WINDOW here is questionable
+ // because a parent widget will not have focus when its child GtkTextView
+ // has focus, but perhaps this may help identify a focused textarea with
+ // some themes as GtkTextView backgrounds do not typically render
+ // differently with focus.
+ GtkStateFlags state_flags = state->disabled ? GTK_STATE_FLAG_INSENSITIVE
+ : state->focused ? GTK_STATE_FLAG_FOCUSED
+ : GTK_STATE_FLAG_NORMAL;
+
+ GtkStyleContext* style_frame = GetStyleContext(
+ MOZ_GTK_SCROLLED_WINDOW, state->image_scale, direction, state_flags);
+ gtk_render_frame(style_frame, cr, aRect->x, aRect->y, aRect->width,
+ aRect->height);
+
+ GdkRectangle rect = *aRect;
+ InsetByBorderPadding(&rect, style_frame);
+
+ GtkStyleContext* style = GetStyleContext(
+ MOZ_GTK_TEXT_VIEW, state->image_scale, direction, state_flags);
+ gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height);
+ // There is a separate "text" window, which usually provides the
+ // background behind the text. However, this is transparent in Ambiance
+ // for GTK 3.20, in which case the MOZ_GTK_TEXT_VIEW background is
+ // visible.
+ style = GetStyleContext(MOZ_GTK_TEXT_VIEW_TEXT, state->image_scale, direction,
+ state_flags);
+ gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_treeview_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ gint xthickness, ythickness;
+ GtkStyleContext* style;
+ GtkStyleContext* style_tree;
+ GtkStateFlags state_flags;
+ GtkBorder border;
+
+ /* only handle disabled and normal states, otherwise the whole background
+ * area will be painted differently with other states */
+ state_flags =
+ state->disabled ? GTK_STATE_FLAG_INSENSITIVE : GTK_STATE_FLAG_NORMAL;
+
+ style =
+ GetStyleContext(MOZ_GTK_SCROLLED_WINDOW, state->image_scale, direction);
+ gtk_style_context_get_border(style, state_flags, &border);
+ xthickness = border.left;
+ ythickness = border.top;
+
+ style_tree =
+ GetStyleContext(MOZ_GTK_TREEVIEW_VIEW, state->image_scale, direction);
+ gtk_render_background(style_tree, cr, rect->x + xthickness,
+ rect->y + ythickness, rect->width - 2 * xthickness,
+ rect->height - 2 * ythickness);
+
+ style =
+ GetStyleContext(MOZ_GTK_SCROLLED_WINDOW, state->image_scale, direction);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_tree_header_cell_paint(cairo_t* cr,
+ const GdkRectangle* aRect,
+ GtkWidgetState* state,
+ gboolean isSorted,
+ GtkTextDirection direction) {
+ moz_gtk_button_paint(cr, aRect, state, GTK_RELIEF_NORMAL,
+ GetWidget(MOZ_GTK_TREE_HEADER_CELL), direction);
+ return MOZ_GTK_SUCCESS;
+}
+
+/* See gtk_expander_paint() for reference.
+ */
+static gint moz_gtk_treeview_expander_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkExpanderStyle expander_state,
+ GtkTextDirection direction) {
+ /* Because the frame we get is of the entire treeview, we can't get the
+ * precise event state of one expander, thus rendering hover and active
+ * feedback useless. */
+ GtkStateFlags state_flags =
+ state->disabled ? GTK_STATE_FLAG_INSENSITIVE : GTK_STATE_FLAG_NORMAL;
+
+ if (state->inHover)
+ state_flags =
+ static_cast<GtkStateFlags>(state_flags | GTK_STATE_FLAG_PRELIGHT);
+ if (state->selected)
+ state_flags =
+ static_cast<GtkStateFlags>(state_flags | GTK_STATE_FLAG_SELECTED);
+
+ /* GTK_STATE_FLAG_ACTIVE controls expanded/colapsed state rendering
+ * in gtk_render_expander()
+ */
+ if (expander_state == GTK_EXPANDER_EXPANDED)
+ state_flags =
+ static_cast<GtkStateFlags>(state_flags | checkbox_check_state);
+ else
+ state_flags =
+ static_cast<GtkStateFlags>(state_flags & ~(checkbox_check_state));
+
+ GtkStyleContext* style = GetStyleContext(
+ MOZ_GTK_TREEVIEW_EXPANDER, state->image_scale, direction, state_flags);
+ gtk_render_expander(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+/* See gtk_separator_draw() for reference.
+ */
+static gint moz_gtk_combo_box_paint(cairo_t* cr, const GdkRectangle* aRect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GdkRectangle arrow_rect, real_arrow_rect;
+ gint separator_width;
+ gboolean wide_separators;
+ GtkStyleContext* style;
+ GtkRequisition arrow_req;
+
+ GtkWidget* comboBoxButton = GetWidget(MOZ_GTK_COMBOBOX_BUTTON);
+ GtkWidget* comboBoxArrow = GetWidget(MOZ_GTK_COMBOBOX_ARROW);
+ if (!comboBoxButton || !comboBoxArrow) {
+ return MOZ_GTK_UNKNOWN_WIDGET;
+ }
+
+ /* Also sets the direction on gComboBoxButtonWidget, which is then
+ * inherited by the separator and arrow */
+ moz_gtk_button_paint(cr, aRect, state, GTK_RELIEF_NORMAL, comboBoxButton,
+ direction);
+
+ calculate_button_inner_rect(comboBoxButton, aRect, &arrow_rect, direction);
+ /* Now arrow_rect contains the inner rect ; we want to correct the width
+ * to what the arrow needs (see gtk_combo_box_size_allocate) */
+ gtk_widget_get_preferred_size(comboBoxArrow, NULL, &arrow_req);
+ moz_gtk_sanity_preferred_size(&arrow_req);
+
+ if (direction == GTK_TEXT_DIR_LTR)
+ arrow_rect.x += arrow_rect.width - arrow_req.width;
+ arrow_rect.width = arrow_req.width;
+
+ calculate_arrow_rect(comboBoxArrow, &arrow_rect, &real_arrow_rect, direction);
+
+ style = GetStyleContext(MOZ_GTK_COMBOBOX_ARROW, state->image_scale);
+ gtk_render_arrow(style, cr, ARROW_DOWN, real_arrow_rect.x, real_arrow_rect.y,
+ real_arrow_rect.width);
+
+ /* If there is no separator in the theme, there's nothing left to do. */
+ GtkWidget* widget = GetWidget(MOZ_GTK_COMBOBOX_SEPARATOR);
+ if (!widget) {
+ return MOZ_GTK_SUCCESS;
+ }
+ style = gtk_widget_get_style_context(widget);
+ StyleContextSetScale(style, state->image_scale);
+ gtk_style_context_get_style(style, "wide-separators", &wide_separators,
+ "separator-width", &separator_width, NULL);
+
+ if (wide_separators) {
+ if (direction == GTK_TEXT_DIR_LTR)
+ arrow_rect.x -= separator_width;
+ else
+ arrow_rect.x += arrow_rect.width;
+
+ gtk_render_frame(style, cr, arrow_rect.x, arrow_rect.y, separator_width,
+ arrow_rect.height);
+ } else {
+ if (direction == GTK_TEXT_DIR_LTR) {
+ GtkBorder padding;
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ gtk_style_context_get_padding(style, state_flags, &padding);
+ arrow_rect.x -= padding.left;
+ } else
+ arrow_rect.x += arrow_rect.width;
+
+ gtk_render_line(style, cr, arrow_rect.x, arrow_rect.y, arrow_rect.x,
+ arrow_rect.y + arrow_rect.height);
+ }
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_arrow_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state, GtkArrowType arrow_type,
+ GtkTextDirection direction) {
+ GdkRectangle arrow_rect;
+ gdouble arrow_angle;
+
+ if (direction == GTK_TEXT_DIR_RTL) {
+ if (arrow_type == GTK_ARROW_LEFT) {
+ arrow_type = GTK_ARROW_RIGHT;
+ } else if (arrow_type == GTK_ARROW_RIGHT) {
+ arrow_type = GTK_ARROW_LEFT;
+ }
+ }
+ switch (arrow_type) {
+ case GTK_ARROW_LEFT:
+ arrow_angle = ARROW_LEFT;
+ break;
+ case GTK_ARROW_RIGHT:
+ arrow_angle = ARROW_RIGHT;
+ break;
+ case GTK_ARROW_DOWN:
+ arrow_angle = ARROW_DOWN;
+ break;
+ default:
+ arrow_angle = ARROW_UP;
+ break;
+ }
+ if (arrow_type == GTK_ARROW_NONE) return MOZ_GTK_SUCCESS;
+
+ GtkWidget* widget = GetWidget(MOZ_GTK_BUTTON_ARROW);
+ if (!widget) {
+ return MOZ_GTK_UNKNOWN_WIDGET;
+ }
+ calculate_arrow_rect(widget, rect, &arrow_rect, direction);
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ GtkStyleContext* style = GetStyleContext(
+ MOZ_GTK_BUTTON_ARROW, state->image_scale, direction, state_flags);
+ gtk_render_arrow(style, cr, arrow_angle, arrow_rect.x, arrow_rect.y,
+ arrow_rect.width);
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_tooltip_paint(cairo_t* cr, const GdkRectangle* aRect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ // Tooltip widget is made in GTK3 as following tree:
+ // Tooltip window
+ // Horizontal Box
+ // Icon (not supported by Firefox)
+ // Label
+ // Each element can be fully styled by CSS of GTK theme.
+ // We have to draw all elements with appropriate offset and right dimensions.
+
+ // Tooltip drawing
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_TOOLTIP, state->image_scale, direction);
+ GdkRectangle rect = *aRect;
+ gtk_render_background(style, cr, rect.x, rect.y, rect.width, rect.height);
+ gtk_render_frame(style, cr, rect.x, rect.y, rect.width, rect.height);
+
+ // Horizontal Box drawing
+ //
+ // The box element has hard-coded 6px margin-* GtkWidget properties, which
+ // are added between the window dimensions and the CSS margin box of the
+ // horizontal box. The frame of the tooltip window is drawn in the
+ // 6px margin.
+ // For drawing Horizontal Box we have to inset drawing area by that 6px
+ // plus its CSS margin.
+ GtkStyleContext* boxStyle =
+ GetStyleContext(MOZ_GTK_TOOLTIP_BOX, state->image_scale, direction);
+
+ rect.x += 6;
+ rect.y += 6;
+ rect.width -= 12;
+ rect.height -= 12;
+
+ InsetByMargin(&rect, boxStyle);
+ gtk_render_background(boxStyle, cr, rect.x, rect.y, rect.width, rect.height);
+ gtk_render_frame(boxStyle, cr, rect.x, rect.y, rect.width, rect.height);
+
+ // Label drawing
+ InsetByBorderPadding(&rect, boxStyle);
+
+ GtkStyleContext* labelStyle =
+ GetStyleContext(MOZ_GTK_TOOLTIP_BOX_LABEL, state->image_scale, direction);
+ moz_gtk_draw_styled_frame(labelStyle, cr, &rect, false);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_resizer_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_RESIZER, state->image_scale, GTK_TEXT_DIR_LTR,
+ GetStateFlagsFromGtkWidgetState(state));
+
+ // Workaround unico not respecting the text direction for resizers.
+ // See bug 1174248.
+ cairo_save(cr);
+ if (direction == GTK_TEXT_DIR_RTL) {
+ cairo_matrix_t mat;
+ cairo_matrix_init_translate(&mat, 2 * rect->x + rect->width, 0);
+ cairo_matrix_scale(&mat, -1, 1);
+ cairo_transform(cr, &mat);
+ }
+
+ gtk_render_handle(style, cr, rect->x, rect->y, rect->width, rect->height);
+ cairo_restore(cr);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_frame_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_FRAME, state->image_scale, direction);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_progressbar_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_PROGRESS_TROUGH, state->image_scale, direction);
+ gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_progress_chunk_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction,
+ WidgetNodeType widget) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_PROGRESS_CHUNK, state->image_scale, direction);
+
+ if (widget == MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE ||
+ widget == MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE) {
+ /**
+ * The bar's size and the bar speed are set depending of the progress'
+ * size. These could also be constant for all progress bars easily.
+ */
+ gboolean vertical =
+ (widget == MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE);
+
+ /* The size of the dimension we are going to use for the animation. */
+ const gint progressSize = vertical ? rect->height : rect->width;
+
+ /* The bar is using a fifth of the element size, based on GtkProgressBar
+ * activity-blocks property. */
+ const gint barSize = MAX(1, progressSize / 5);
+
+ /* Represents the travel that has to be done for a complete cycle. */
+ const gint travel = 2 * (progressSize - barSize);
+
+ /* period equals to travel / pixelsPerMillisecond
+ * where pixelsPerMillisecond equals progressSize / 1000.0.
+ * This is equivalent to 1600. */
+ static const guint period = 1600;
+ const gint t = PR_IntervalToMilliseconds(PR_IntervalNow()) % period;
+ const gint dx = travel * t / period;
+
+ if (vertical) {
+ rect->y += (dx < travel / 2) ? dx : travel - dx;
+ rect->height = barSize;
+ } else {
+ rect->x += (dx < travel / 2) ? dx : travel - dx;
+ rect->width = barSize;
+ }
+ }
+
+ gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_get_tab_thickness(GtkStyleContext* style) {
+ if (!notebook_has_tab_gap)
+ return 0; /* tabs do not overdraw the tabpanel border with "no gap" style */
+
+ GtkBorder border;
+ gtk_style_context_get_border(style, gtk_style_context_get_state(style),
+ &border);
+ if (border.top < 2) return 2; /* some themes don't set ythickness correctly */
+
+ return border.top;
+}
+
+gint moz_gtk_get_tab_thickness(WidgetNodeType aNodeType) {
+ GtkStyleContext* style = GetStyleContext(aNodeType);
+ int thickness = moz_gtk_get_tab_thickness(style);
+ return thickness;
+}
+
+/* actual small tabs */
+static gint moz_gtk_tab_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state, GtkTabFlags flags,
+ GtkTextDirection direction,
+ WidgetNodeType widget) {
+ /* When the tab isn't selected, we just draw a notebook extension.
+ * When it is selected, we overwrite the adjacent border of the tabpanel
+ * touching the tab with a pierced border (called "the gap") to make the
+ * tab appear physically attached to the tabpanel; see details below. */
+
+ GtkStyleContext* style;
+ GdkRectangle tabRect;
+ GdkRectangle focusRect;
+ GdkRectangle backRect;
+ int initial_gap = 0;
+ bool isBottomTab = (widget == MOZ_GTK_TAB_BOTTOM);
+
+ style = GetStyleContext(widget, state->image_scale, direction,
+ GetStateFlagsFromGtkTabFlags(flags));
+ tabRect = *rect;
+
+ if (flags & MOZ_GTK_TAB_FIRST) {
+ gtk_style_context_get_style(style, "initial-gap", &initial_gap, NULL);
+ tabRect.width -= initial_gap;
+
+ if (direction != GTK_TEXT_DIR_RTL) {
+ tabRect.x += initial_gap;
+ }
+ }
+
+ focusRect = backRect = tabRect;
+
+ if (notebook_has_tab_gap) {
+ if ((flags & MOZ_GTK_TAB_SELECTED) == 0) {
+ /* Only draw the tab */
+ gtk_render_extension(style, cr, tabRect.x, tabRect.y, tabRect.width,
+ tabRect.height,
+ isBottomTab ? GTK_POS_TOP : GTK_POS_BOTTOM);
+ } else {
+ /* Draw the tab and the gap
+ * We want the gap to be positioned exactly on the tabpanel top
+ * border; since tabbox.css may set a negative margin so that the tab
+ * frame rect already overlaps the tabpanel frame rect, we need to take
+ * that into account when drawing. To that effect, nsNativeThemeGTK
+ * passes us this negative margin (bmargin in the graphic below) in the
+ * lowest bits of |flags|. We use it to set gap_voffset, the distance
+ * between the top of the gap and the bottom of the tab (resp. the
+ * bottom of the gap and the top of the tab when we draw a bottom tab),
+ * while ensuring that the gap always touches the border of the tab,
+ * i.e. 0 <= gap_voffset <= gap_height, to avoid surprinsing results
+ * with big negative or positive margins.
+ * Here is a graphical explanation in the case of top tabs:
+ * ___________________________
+ * / \
+ * | T A B |
+ * ----------|. . . . . . . . . . . . . . .|----- top of tabpanel
+ * : ^ bmargin : ^
+ * : | (-negative margin, : |
+ * bottom : v passed in flags) : | gap_height
+ * of -> :.............................: | (the size of the
+ * the tab . part of the gap . | tabpanel top border)
+ * . outside of the tab . v
+ * ----------------------------------------------
+ *
+ * To draw the gap, we use gtk_render_frame_gap(), see comment in
+ * moz_gtk_tabpanels_paint(). This gap is made 3 * gap_height tall,
+ * which should suffice to ensure that the only visible border is the
+ * pierced one. If the tab is in the middle, we make the box_gap begin
+ * a bit to the left of the tab and end a bit to the right, adjusting
+ * the gap position so it still is under the tab, because we want the
+ * rendering of a gap in the middle of a tabpanel. This is the role of
+ * the gints gap_{l,r}_offset. On the contrary, if the tab is the
+ * first, we align the start border of the box_gap with the start
+ * border of the tab (left if LTR, right if RTL), by setting the
+ * appropriate offset to 0.*/
+ gint gap_loffset, gap_roffset, gap_voffset, gap_height;
+
+ /* Get height needed by the gap */
+ gap_height = moz_gtk_get_tab_thickness(style);
+
+ /* Extract gap_voffset from the first bits of flags */
+ gap_voffset = flags & MOZ_GTK_TAB_MARGIN_MASK;
+ if (gap_voffset > gap_height) gap_voffset = gap_height;
+
+ /* Set gap_{l,r}_offset to appropriate values */
+ gap_loffset = gap_roffset = 20; /* should be enough */
+ if (flags & MOZ_GTK_TAB_FIRST) {
+ if (direction == GTK_TEXT_DIR_RTL)
+ gap_roffset = initial_gap;
+ else
+ gap_loffset = initial_gap;
+ }
+
+ GtkStyleContext* panelStyle =
+ GetStyleContext(MOZ_GTK_TABPANELS, state->image_scale, direction);
+
+ if (isBottomTab) {
+ /* Draw the tab on bottom */
+ focusRect.y += gap_voffset;
+ focusRect.height -= gap_voffset;
+
+ gtk_render_extension(style, cr, tabRect.x, tabRect.y + gap_voffset,
+ tabRect.width, tabRect.height - gap_voffset,
+ GTK_POS_TOP);
+
+ backRect.y += (gap_voffset - gap_height);
+ backRect.height = gap_height;
+
+ /* Draw the gap; erase with background color before painting in
+ * case theme does not */
+ gtk_render_background(panelStyle, cr, backRect.x, backRect.y,
+ backRect.width, backRect.height);
+ cairo_save(cr);
+ cairo_rectangle(cr, backRect.x, backRect.y, backRect.width,
+ backRect.height);
+ cairo_clip(cr);
+
+ gtk_render_frame_gap(panelStyle, cr, tabRect.x - gap_loffset,
+ tabRect.y + gap_voffset - 3 * gap_height,
+ tabRect.width + gap_loffset + gap_roffset,
+ 3 * gap_height, GTK_POS_BOTTOM, gap_loffset,
+ gap_loffset + tabRect.width);
+ cairo_restore(cr);
+ } else {
+ /* Draw the tab on top */
+ focusRect.height -= gap_voffset;
+ gtk_render_extension(style, cr, tabRect.x, tabRect.y, tabRect.width,
+ tabRect.height - gap_voffset, GTK_POS_BOTTOM);
+
+ backRect.y += (tabRect.height - gap_voffset);
+ backRect.height = gap_height;
+
+ /* Draw the gap; erase with background color before painting in
+ * case theme does not */
+ gtk_render_background(panelStyle, cr, backRect.x, backRect.y,
+ backRect.width, backRect.height);
+
+ cairo_save(cr);
+ cairo_rectangle(cr, backRect.x, backRect.y, backRect.width,
+ backRect.height);
+ cairo_clip(cr);
+
+ gtk_render_frame_gap(panelStyle, cr, tabRect.x - gap_loffset,
+ tabRect.y + tabRect.height - gap_voffset,
+ tabRect.width + gap_loffset + gap_roffset,
+ 3 * gap_height, GTK_POS_TOP, gap_loffset,
+ gap_loffset + tabRect.width);
+ cairo_restore(cr);
+ }
+ }
+ } else {
+ gtk_render_background(style, cr, tabRect.x, tabRect.y, tabRect.width,
+ tabRect.height);
+ gtk_render_frame(style, cr, tabRect.x, tabRect.y, tabRect.width,
+ tabRect.height);
+ }
+
+ if (state->focused) {
+ /* Paint the focus ring */
+ GtkBorder padding;
+ gtk_style_context_get_padding(style, GetStateFlagsFromGtkWidgetState(state),
+ &padding);
+
+ focusRect.x += padding.left;
+ focusRect.width -= (padding.left + padding.right);
+ focusRect.y += padding.top;
+ focusRect.height -= (padding.top + padding.bottom);
+
+ gtk_render_focus(style, cr, focusRect.x, focusRect.y, focusRect.width,
+ focusRect.height);
+ }
+
+ return MOZ_GTK_SUCCESS;
+}
+
+/* tab area*/
+static gint moz_gtk_tabpanels_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkTextDirection direction) {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_TABPANELS, state->image_scale, direction);
+ gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height);
+ /*
+ * The gap size is not needed in moz_gtk_tabpanels_paint because
+ * the gap will be painted with the foreground tab in moz_gtk_tab_paint.
+ *
+ * However, if moz_gtk_tabpanels_paint just uses gtk_render_frame(),
+ * the theme will think that there are no tabs and may draw something
+ * different.Hence the trick of using two clip regions, and drawing the
+ * gap outside each clip region, to get the correct frame for
+ * a tabpanel with tabs.
+ */
+ /* left side */
+ cairo_save(cr);
+ cairo_rectangle(cr, rect->x, rect->y, rect->x + rect->width / 2,
+ rect->y + rect->height);
+ cairo_clip(cr);
+ gtk_render_frame_gap(style, cr, rect->x, rect->y, rect->width, rect->height,
+ GTK_POS_TOP, rect->width - 1, rect->width);
+ cairo_restore(cr);
+
+ /* right side */
+ cairo_save(cr);
+ cairo_rectangle(cr, rect->x + rect->width / 2, rect->y, rect->x + rect->width,
+ rect->y + rect->height);
+ cairo_clip(cr);
+ gtk_render_frame_gap(style, cr, rect->x, rect->y, rect->width, rect->height,
+ GTK_POS_TOP, 0, 1);
+ cairo_restore(cr);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_tab_scroll_arrow_paint(cairo_t* cr, GdkRectangle* rect,
+ GtkWidgetState* state,
+ GtkArrowType arrow_type,
+ GtkTextDirection direction) {
+ GtkStyleContext* style;
+ gdouble arrow_angle;
+ gint arrow_size = MIN(rect->width, rect->height);
+ gint x = rect->x + (rect->width - arrow_size) / 2;
+ gint y = rect->y + (rect->height - arrow_size) / 2;
+
+ if (direction == GTK_TEXT_DIR_RTL) {
+ arrow_type =
+ (arrow_type == GTK_ARROW_LEFT) ? GTK_ARROW_RIGHT : GTK_ARROW_LEFT;
+ }
+ switch (arrow_type) {
+ case GTK_ARROW_LEFT:
+ arrow_angle = ARROW_LEFT;
+ break;
+ case GTK_ARROW_RIGHT:
+ arrow_angle = ARROW_RIGHT;
+ break;
+ case GTK_ARROW_DOWN:
+ arrow_angle = ARROW_DOWN;
+ break;
+ default:
+ arrow_angle = ARROW_UP;
+ break;
+ }
+ if (arrow_type != GTK_ARROW_NONE) {
+ style = GetStyleContext(MOZ_GTK_TAB_SCROLLARROW, state->image_scale,
+ direction, GetStateFlagsFromGtkWidgetState(state));
+ gtk_render_arrow(style, cr, arrow_angle, x, y, arrow_size);
+ }
+ return MOZ_GTK_SUCCESS;
+}
+
+static gint moz_gtk_header_bar_paint(WidgetNodeType widgetType, cairo_t* cr,
+ GdkRectangle* rect,
+ GtkWidgetState* state) {
+ GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state);
+ GtkStyleContext* style = GetStyleContext(widgetType, state->image_scale,
+ GTK_TEXT_DIR_NONE, state_flags);
+
+ // Some themes like Elementary's style the container of the headerbar rather
+ // than the header bar itself.
+ if (HeaderBarShouldDrawContainer(widgetType)) {
+ auto containerType = widgetType == MOZ_GTK_HEADER_BAR
+ ? MOZ_GTK_HEADERBAR_FIXED
+ : MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED;
+ style = GetStyleContext(containerType, state->image_scale,
+ GTK_TEXT_DIR_NONE, state_flags);
+ }
+
+ gtk_render_background(style, cr, rect->x, rect->y, rect->width, rect->height);
+ gtk_render_frame(style, cr, rect->x, rect->y, rect->width, rect->height);
+
+ return MOZ_GTK_SUCCESS;
+}
+
+gint moz_gtk_get_widget_border(WidgetNodeType widget, gint* left, gint* top,
+ gint* right, gint* bottom,
+ // NOTE: callers depend on direction being used
+ // only for MOZ_GTK_DROPDOWN widgets.
+ GtkTextDirection direction) {
+ GtkWidget* w;
+ GtkStyleContext* style;
+ *left = *top = *right = *bottom = 0;
+
+ switch (widget) {
+ case MOZ_GTK_BUTTON:
+ case MOZ_GTK_TOOLBAR_BUTTON: {
+ style = GetStyleContext(MOZ_GTK_BUTTON);
+
+ *left = *top = *right = *bottom = gtk_container_get_border_width(
+ GTK_CONTAINER(GetWidget(MOZ_GTK_BUTTON)));
+
+ if (widget == MOZ_GTK_TOOLBAR_BUTTON) {
+ gtk_style_context_save(style);
+ gtk_style_context_add_class(style, "image-button");
+ }
+
+ moz_gtk_add_style_padding(style, left, top, right, bottom);
+
+ if (widget == MOZ_GTK_TOOLBAR_BUTTON) gtk_style_context_restore(style);
+
+ moz_gtk_add_style_border(style, left, top, right, bottom);
+
+ return MOZ_GTK_SUCCESS;
+ }
+ case MOZ_GTK_ENTRY:
+ case MOZ_GTK_DROPDOWN_ENTRY: {
+ style = GetStyleContext(widget);
+
+ // XXX: Subtract 1 pixel from the padding to account for the default
+ // padding in forms.css. See bug 1187385.
+ *left = *top = *right = *bottom = -1;
+ moz_gtk_add_border_padding(style, left, top, right, bottom);
+
+ return MOZ_GTK_SUCCESS;
+ }
+ case MOZ_GTK_TEXT_VIEW:
+ case MOZ_GTK_TREEVIEW: {
+ style = GetStyleContext(MOZ_GTK_SCROLLED_WINDOW);
+ moz_gtk_add_style_border(style, left, top, right, bottom);
+ return MOZ_GTK_SUCCESS;
+ }
+ case MOZ_GTK_TREE_HEADER_CELL: {
+ /* A Tree Header in GTK is just a different styled button
+ * It must be placed in a TreeView for getting the correct style
+ * assigned.
+ * That is why the following code is the same as for MOZ_GTK_BUTTON.
+ * */
+ *left = *top = *right = *bottom = gtk_container_get_border_width(
+ GTK_CONTAINER(GetWidget(MOZ_GTK_TREE_HEADER_CELL)));
+ style = GetStyleContext(MOZ_GTK_TREE_HEADER_CELL);
+ moz_gtk_add_border_padding(style, left, top, right, bottom);
+ return MOZ_GTK_SUCCESS;
+ }
+ case MOZ_GTK_DROPDOWN: {
+ /* We need to account for the arrow on the dropdown, so text
+ * doesn't come too close to the arrow, or in some cases spill
+ * into the arrow. */
+ gboolean wide_separators;
+ gint separator_width;
+ GtkRequisition arrow_req;
+ GtkBorder border;
+
+ *left = *top = *right = *bottom = gtk_container_get_border_width(
+ GTK_CONTAINER(GetWidget(MOZ_GTK_COMBOBOX_BUTTON)));
+ style = GetStyleContext(MOZ_GTK_COMBOBOX_BUTTON);
+ moz_gtk_add_border_padding(style, left, top, right, bottom);
+
+ /* If there is no separator, don't try to count its width. */
+ separator_width = 0;
+ GtkWidget* comboBoxSeparator = GetWidget(MOZ_GTK_COMBOBOX_SEPARATOR);
+ if (comboBoxSeparator) {
+ style = gtk_widget_get_style_context(comboBoxSeparator);
+ gtk_style_context_get_style(style, "wide-separators", &wide_separators,
+ "separator-width", &separator_width, NULL);
+
+ if (!wide_separators) {
+ gtk_style_context_get_border(
+ style, gtk_style_context_get_state(style), &border);
+ separator_width = border.left;
+ }
+ }
+
+ gtk_widget_get_preferred_size(GetWidget(MOZ_GTK_COMBOBOX_ARROW), NULL,
+ &arrow_req);
+ moz_gtk_sanity_preferred_size(&arrow_req);
+
+ if (direction == GTK_TEXT_DIR_RTL)
+ *left += separator_width + arrow_req.width;
+ else
+ *right += separator_width + arrow_req.width;
+
+ return MOZ_GTK_SUCCESS;
+ }
+ case MOZ_GTK_TABPANELS:
+ w = GetWidget(MOZ_GTK_TABPANELS);
+ break;
+ case MOZ_GTK_PROGRESSBAR:
+ w = GetWidget(MOZ_GTK_PROGRESSBAR);
+ break;
+ case MOZ_GTK_SPINBUTTON_ENTRY:
+ case MOZ_GTK_SPINBUTTON_UP:
+ case MOZ_GTK_SPINBUTTON_DOWN:
+ w = GetWidget(MOZ_GTK_SPINBUTTON);
+ break;
+ case MOZ_GTK_SCALE_HORIZONTAL:
+ case MOZ_GTK_SCALE_VERTICAL:
+ w = GetWidget(widget);
+ break;
+ case MOZ_GTK_FRAME:
+ w = GetWidget(MOZ_GTK_FRAME);
+ break;
+ case MOZ_GTK_TOOLTIP: {
+ // In GTK 3 there are 6 pixels of additional margin around the box.
+ // See details there:
+ // https://github.com/GNOME/gtk/blob/5ea69a136bd7e4970b3a800390e20314665aaed2/gtk/ui/gtktooltipwindow.ui#L11
+ *left = *right = *top = *bottom = 6;
+
+ // We also need to add margin/padding/borders from Tooltip content.
+ // Tooltip contains horizontal box, where icon and label is put.
+ // We ignore icon as long as we don't have support for it.
+ GtkStyleContext* boxStyle = GetStyleContext(MOZ_GTK_TOOLTIP_BOX);
+ moz_gtk_add_margin_border_padding(boxStyle, left, top, right, bottom);
+
+ GtkStyleContext* labelStyle = GetStyleContext(MOZ_GTK_TOOLTIP_BOX_LABEL);
+ moz_gtk_add_margin_border_padding(labelStyle, left, top, right, bottom);
+
+ return MOZ_GTK_SUCCESS;
+ }
+ case MOZ_GTK_HEADER_BAR_BUTTON_BOX: {
+ style = GetStyleContext(MOZ_GTK_HEADER_BAR);
+ moz_gtk_add_border_padding(style, left, top, right, bottom);
+ *top = *bottom = 0;
+ bool leftButtonsPlacement = false;
+ GetGtkHeaderBarButtonLayout({}, &leftButtonsPlacement);
+ if (direction == GTK_TEXT_DIR_RTL) {
+ leftButtonsPlacement = !leftButtonsPlacement;
+ }
+ if (leftButtonsPlacement) {
+ *right = 0;
+ } else {
+ *left = 0;
+ }
+ return MOZ_GTK_SUCCESS;
+ }
+ /* These widgets have no borders, since they are not containers. */
+ case MOZ_GTK_SPLITTER_HORIZONTAL:
+ case MOZ_GTK_SPLITTER_VERTICAL:
+ case MOZ_GTK_CHECKBUTTON:
+ case MOZ_GTK_RADIOBUTTON:
+ case MOZ_GTK_SCALE_THUMB_HORIZONTAL:
+ case MOZ_GTK_SCALE_THUMB_VERTICAL:
+ case MOZ_GTK_PROGRESS_CHUNK:
+ case MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE:
+ case MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE:
+ case MOZ_GTK_TREEVIEW_EXPANDER:
+ case MOZ_GTK_HEADER_BAR:
+ case MOZ_GTK_HEADER_BAR_MAXIMIZED:
+ case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE:
+ /* These widgets have no borders.*/
+ case MOZ_GTK_INNER_SPIN_BUTTON:
+ case MOZ_GTK_SPINBUTTON:
+ case MOZ_GTK_WINDOW_DECORATION:
+ case MOZ_GTK_WINDOW_DECORATION_SOLID:
+ case MOZ_GTK_RESIZER:
+ case MOZ_GTK_TOOLBARBUTTON_ARROW:
+ case MOZ_GTK_TAB_SCROLLARROW:
+ return MOZ_GTK_SUCCESS;
+ default:
+ g_warning("Unsupported widget type: %d", widget);
+ return MOZ_GTK_UNKNOWN_WIDGET;
+ }
+ /* TODO - we're still missing some widget implementations */
+ if (w) {
+ moz_gtk_add_style_border(gtk_widget_get_style_context(w), left, top, right,
+ bottom);
+ }
+ return MOZ_GTK_SUCCESS;
+}
+
+gint moz_gtk_get_tab_border(gint* left, gint* top, gint* right, gint* bottom,
+ GtkTextDirection direction, GtkTabFlags flags,
+ WidgetNodeType widget) {
+ GtkStyleContext* style = GetStyleContext(widget, 1, direction,
+ GetStateFlagsFromGtkTabFlags(flags));
+
+ *left = *top = *right = *bottom = 0;
+ moz_gtk_add_style_padding(style, left, top, right, bottom);
+
+ // Gtk >= 3.20 does not use those styles
+ if (gtk_check_version(3, 20, 0) != nullptr) {
+ int tab_curvature;
+
+ gtk_style_context_get_style(style, "tab-curvature", &tab_curvature, NULL);
+ *left += tab_curvature;
+ *right += tab_curvature;
+
+ if (flags & MOZ_GTK_TAB_FIRST) {
+ int initial_gap = 0;
+ gtk_style_context_get_style(style, "initial-gap", &initial_gap, NULL);
+ if (direction == GTK_TEXT_DIR_RTL)
+ *right += initial_gap;
+ else
+ *left += initial_gap;
+ }
+ } else {
+ GtkBorder margin;
+
+ gtk_style_context_get_margin(style, gtk_style_context_get_state(style),
+ &margin);
+ *left += margin.left;
+ *right += margin.right;
+
+ if (flags & MOZ_GTK_TAB_FIRST) {
+ style = GetStyleContext(MOZ_GTK_NOTEBOOK_HEADER, direction);
+ gtk_style_context_get_margin(style, gtk_style_context_get_state(style),
+ &margin);
+ *left += margin.left;
+ *right += margin.right;
+ }
+ }
+
+ return MOZ_GTK_SUCCESS;
+}
+
+gint moz_gtk_get_tab_scroll_arrow_size(gint* width, gint* height) {
+ gint arrow_size;
+
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_TABPANELS);
+ gtk_style_context_get_style(style, "scroll-arrow-hlength", &arrow_size, NULL);
+
+ *height = *width = arrow_size;
+
+ return MOZ_GTK_SUCCESS;
+}
+
+void moz_gtk_get_arrow_size(WidgetNodeType widgetType, gint* width,
+ gint* height) {
+ GtkWidget* widget;
+ switch (widgetType) {
+ case MOZ_GTK_DROPDOWN:
+ widget = GetWidget(MOZ_GTK_COMBOBOX_ARROW);
+ break;
+ default:
+ widget = GetWidget(MOZ_GTK_BUTTON_ARROW);
+ break;
+ }
+ if (widget) {
+ GtkRequisition requisition;
+ gtk_widget_get_preferred_size(widget, NULL, &requisition);
+ moz_gtk_sanity_preferred_size(&requisition);
+
+ *width = requisition.width;
+ *height = requisition.height;
+ } else {
+ *width = 0;
+ *height = 0;
+ }
+}
+
+gint moz_gtk_get_expander_size(gint* size) {
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_EXPANDER);
+ gtk_style_context_get_style(style, "expander-size", size, NULL);
+ return MOZ_GTK_SUCCESS;
+}
+
+gint moz_gtk_get_treeview_expander_size(gint* size) {
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_TREEVIEW);
+ gtk_style_context_get_style(style, "expander-size", size, NULL);
+ return MOZ_GTK_SUCCESS;
+}
+
+void moz_gtk_get_entry_min_height(gint* min_content_height,
+ gint* border_padding_height) {
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_ENTRY);
+ if (!gtk_check_version(3, 20, 0)) {
+ gtk_style_context_get(style, gtk_style_context_get_state(style),
+ "min-height", min_content_height, nullptr);
+ } else {
+ *min_content_height = 0;
+ }
+
+ GtkBorder border;
+ GtkBorder padding;
+ gtk_style_context_get_border(style, gtk_style_context_get_state(style),
+ &border);
+ gtk_style_context_get_padding(style, gtk_style_context_get_state(style),
+ &padding);
+
+ *border_padding_height =
+ (border.top + border.bottom + padding.top + padding.bottom);
+}
+
+void moz_gtk_get_scale_metrics(GtkOrientation orient, gint* scale_width,
+ gint* scale_height) {
+ if (gtk_check_version(3, 20, 0) != nullptr) {
+ WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL)
+ ? MOZ_GTK_SCALE_HORIZONTAL
+ : MOZ_GTK_SCALE_VERTICAL;
+
+ gint thumb_length, thumb_height, trough_border;
+ moz_gtk_get_scalethumb_metrics(orient, &thumb_length, &thumb_height);
+
+ GtkStyleContext* style = GetStyleContext(widget);
+ gtk_style_context_get_style(style, "trough-border", &trough_border, NULL);
+
+ if (orient == GTK_ORIENTATION_HORIZONTAL) {
+ *scale_width = thumb_length + trough_border * 2;
+ *scale_height = thumb_height + trough_border * 2;
+ } else {
+ *scale_width = thumb_height + trough_border * 2;
+ *scale_height = thumb_length + trough_border * 2;
+ }
+ } else {
+ WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL)
+ ? MOZ_GTK_SCALE_TROUGH_HORIZONTAL
+ : MOZ_GTK_SCALE_TROUGH_VERTICAL;
+ moz_gtk_get_widget_min_size(GetStyleContext(widget), scale_width,
+ scale_height);
+ }
+}
+
+gint moz_gtk_get_scalethumb_metrics(GtkOrientation orient, gint* thumb_length,
+ gint* thumb_height) {
+ if (gtk_check_version(3, 20, 0) != nullptr) {
+ WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL)
+ ? MOZ_GTK_SCALE_HORIZONTAL
+ : MOZ_GTK_SCALE_VERTICAL;
+ GtkStyleContext* style = GetStyleContext(widget);
+ gtk_style_context_get_style(style, "slider_length", thumb_length,
+ "slider_width", thumb_height, NULL);
+ } else {
+ WidgetNodeType widget = (orient == GTK_ORIENTATION_HORIZONTAL)
+ ? MOZ_GTK_SCALE_THUMB_HORIZONTAL
+ : MOZ_GTK_SCALE_THUMB_VERTICAL;
+ GtkStyleContext* style = GetStyleContext(widget);
+
+ gint min_width, min_height;
+ GtkStateFlags state = gtk_style_context_get_state(style);
+ gtk_style_context_get(style, state, "min-width", &min_width, "min-height",
+ &min_height, nullptr);
+ GtkBorder margin;
+ gtk_style_context_get_margin(style, state, &margin);
+ gint margin_width = margin.left + margin.right;
+ gint margin_height = margin.top + margin.bottom;
+
+ // Negative margin of slider element also determines its minimal size
+ // so use bigger of those two values.
+ if (min_width < -margin_width) min_width = -margin_width;
+ if (min_height < -margin_height) min_height = -margin_height;
+
+ *thumb_length = min_width;
+ *thumb_height = min_height;
+ }
+
+ return MOZ_GTK_SUCCESS;
+}
+
+const ToggleGTKMetrics* GetToggleMetrics(WidgetNodeType aWidgetType) {
+ ToggleGTKMetrics* metrics;
+
+ switch (aWidgetType) {
+ case MOZ_GTK_RADIOBUTTON:
+ metrics = &sRadioMetrics;
+ break;
+ case MOZ_GTK_CHECKBUTTON:
+ metrics = &sCheckboxMetrics;
+ break;
+ default:
+ MOZ_CRASH("Unsupported widget type for getting metrics");
+ return nullptr;
+ }
+
+ metrics->initialized = true;
+ if (gtk_check_version(3, 20, 0) == nullptr) {
+ GtkStyleContext* style = GetStyleContext(aWidgetType);
+ GtkStateFlags state_flags = gtk_style_context_get_state(style);
+ gtk_style_context_get(style, state_flags, "min-height",
+ &(metrics->minSizeWithBorder.height), "min-width",
+ &(metrics->minSizeWithBorder.width), nullptr);
+ // Fallback to indicator size if min dimensions are zero
+ if (metrics->minSizeWithBorder.height == 0 ||
+ metrics->minSizeWithBorder.width == 0) {
+ gint indicator_size = 0;
+ gtk_widget_style_get(GetWidget(MOZ_GTK_CHECKBUTTON_CONTAINER),
+ "indicator_size", &indicator_size, nullptr);
+ if (metrics->minSizeWithBorder.height == 0) {
+ metrics->minSizeWithBorder.height = indicator_size;
+ }
+ if (metrics->minSizeWithBorder.width == 0) {
+ metrics->minSizeWithBorder.width = indicator_size;
+ }
+ }
+
+ GtkBorder border, padding;
+ gtk_style_context_get_border(style, state_flags, &border);
+ gtk_style_context_get_padding(style, state_flags, &padding);
+ metrics->borderAndPadding.left = border.left + padding.left;
+ metrics->borderAndPadding.right = border.right + padding.right;
+ metrics->borderAndPadding.top = border.top + padding.top;
+ metrics->borderAndPadding.bottom = border.bottom + padding.bottom;
+ metrics->minSizeWithBorder.width +=
+ metrics->borderAndPadding.left + metrics->borderAndPadding.right;
+ metrics->minSizeWithBorder.height +=
+ metrics->borderAndPadding.top + metrics->borderAndPadding.bottom;
+ } else {
+ gint indicator_size = 0, indicator_spacing = 0;
+ gtk_widget_style_get(GetWidget(MOZ_GTK_CHECKBUTTON_CONTAINER),
+ "indicator_size", &indicator_size, "indicator_spacing",
+ &indicator_spacing, nullptr);
+ metrics->minSizeWithBorder.width = metrics->minSizeWithBorder.height =
+ indicator_size;
+ }
+ return metrics;
+}
+
+/*
+ * get_shadow_width() from gtkwindow.c is not public so we need
+ * to implement it.
+ */
+void InitWindowDecorationSize(CSDWindowDecorationSize* sWindowDecorationSize,
+ bool aPopupWindow) {
+ bool solidDecorations = gtk_style_context_has_class(
+ GetStyleContext(MOZ_GTK_HEADERBAR_WINDOW, 1), "solid-csd");
+ // solid-csd does not use frame extents, quit now.
+ if (solidDecorations) {
+ sWindowDecorationSize->decorationSize = {0, 0, 0, 0};
+ return;
+ }
+
+ // Scale factor is applied later when decoration size is used for actual
+ // gtk windows.
+ GtkStyleContext* context = GetStyleContext(MOZ_GTK_WINDOW_DECORATION);
+
+ /* Always sum border + padding */
+ GtkBorder padding;
+ GtkStateFlags state = gtk_style_context_get_state(context);
+ gtk_style_context_get_border(context, state,
+ &sWindowDecorationSize->decorationSize);
+ gtk_style_context_get_padding(context, state, &padding);
+ sWindowDecorationSize->decorationSize += padding;
+
+ // Available on GTK 3.20+.
+ static auto sGtkRenderBackgroundGetClip = (void (*)(
+ GtkStyleContext*, gdouble, gdouble, gdouble, gdouble,
+ GdkRectangle*))dlsym(RTLD_DEFAULT, "gtk_render_background_get_clip");
+
+ if (!sGtkRenderBackgroundGetClip) {
+ return;
+ }
+
+ GdkRectangle clip;
+ sGtkRenderBackgroundGetClip(context, 0, 0, 0, 0, &clip);
+
+ GtkBorder extents;
+ extents.top = -clip.y;
+ extents.right = clip.width + clip.x;
+ extents.bottom = clip.height + clip.y;
+ extents.left = -clip.x;
+
+ // Get shadow extents but combine with style margin; use the bigger value.
+ // Margin is used for resize grip size - it's not present on
+ // popup windows.
+ if (!aPopupWindow) {
+ GtkBorder margin;
+ gtk_style_context_get_margin(context, state, &margin);
+
+ extents.top = MAX(extents.top, margin.top);
+ extents.right = MAX(extents.right, margin.right);
+ extents.bottom = MAX(extents.bottom, margin.bottom);
+ extents.left = MAX(extents.left, margin.left);
+ }
+
+ sWindowDecorationSize->decorationSize += extents;
+}
+
+GtkBorder GetCSDDecorationSize(bool aIsPopup) {
+ auto metrics =
+ aIsPopup ? &sPopupWindowDecorationSize : &sToplevelWindowDecorationSize;
+ if (!metrics->initialized) {
+ InitWindowDecorationSize(metrics, aIsPopup);
+ metrics->initialized = true;
+ }
+ return metrics->decorationSize;
+}
+
+/* cairo_t *cr argument has to be a system-cairo. */
+gint moz_gtk_widget_paint(WidgetNodeType widget, cairo_t* cr,
+ GdkRectangle* rect, GtkWidgetState* state, gint flags,
+ GtkTextDirection direction) {
+ /* A workaround for https://bugzilla.gnome.org/show_bug.cgi?id=694086
+ */
+ cairo_new_path(cr);
+
+ switch (widget) {
+ case MOZ_GTK_BUTTON:
+ case MOZ_GTK_TOOLBAR_BUTTON:
+ if (state->depressed) {
+ return moz_gtk_button_paint(cr, rect, state, (GtkReliefStyle)flags,
+ GetWidget(MOZ_GTK_TOGGLE_BUTTON),
+ direction);
+ }
+ return moz_gtk_button_paint(cr, rect, state, (GtkReliefStyle)flags,
+ GetWidget(MOZ_GTK_BUTTON), direction);
+ case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE:
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE:
+ return moz_gtk_header_bar_button_paint(
+ cr, rect, state, (GtkReliefStyle)flags, widget, direction);
+ case MOZ_GTK_CHECKBUTTON:
+ case MOZ_GTK_RADIOBUTTON:
+ return moz_gtk_toggle_paint(cr, rect, state,
+ !!(flags & MOZ_GTK_WIDGET_CHECKED),
+ !!(flags & MOZ_GTK_WIDGET_INCONSISTENT),
+ (widget == MOZ_GTK_RADIOBUTTON), direction);
+ case MOZ_GTK_SCALE_HORIZONTAL:
+ case MOZ_GTK_SCALE_VERTICAL:
+ return moz_gtk_scale_paint(cr, rect, state, (GtkOrientation)flags,
+ direction);
+ case MOZ_GTK_SCALE_THUMB_HORIZONTAL:
+ case MOZ_GTK_SCALE_THUMB_VERTICAL:
+ return moz_gtk_scale_thumb_paint(cr, rect, state, (GtkOrientation)flags,
+ direction);
+ case MOZ_GTK_INNER_SPIN_BUTTON:
+ return moz_gtk_inner_spin_paint(cr, rect, state, direction);
+ case MOZ_GTK_SPINBUTTON:
+ return moz_gtk_spin_paint(cr, rect, state, direction);
+ case MOZ_GTK_SPINBUTTON_UP:
+ case MOZ_GTK_SPINBUTTON_DOWN:
+ return moz_gtk_spin_updown_paint(
+ cr, rect, (widget == MOZ_GTK_SPINBUTTON_DOWN), state, direction);
+ case MOZ_GTK_SPINBUTTON_ENTRY: {
+ GtkStyleContext* style =
+ GetStyleContext(MOZ_GTK_SPINBUTTON_ENTRY, state->image_scale,
+ direction, GetStateFlagsFromGtkWidgetState(state));
+ return moz_gtk_entry_paint(cr, rect, state, style, widget);
+ }
+ case MOZ_GTK_TREEVIEW:
+ return moz_gtk_treeview_paint(cr, rect, state, direction);
+ case MOZ_GTK_TREE_HEADER_CELL:
+ return moz_gtk_tree_header_cell_paint(cr, rect, state, flags, direction);
+ case MOZ_GTK_TREEVIEW_EXPANDER:
+ return moz_gtk_treeview_expander_paint(
+ cr, rect, state, (GtkExpanderStyle)flags, direction);
+ case MOZ_GTK_ENTRY:
+ case MOZ_GTK_DROPDOWN_ENTRY: {
+ GtkStyleContext* style =
+ GetStyleContext(widget, state->image_scale, direction,
+ GetStateFlagsFromGtkWidgetState(state));
+ gint ret = moz_gtk_entry_paint(cr, rect, state, style, widget);
+ return ret;
+ }
+ case MOZ_GTK_TEXT_VIEW:
+ return moz_gtk_text_view_paint(cr, rect, state, direction);
+ case MOZ_GTK_DROPDOWN:
+ return moz_gtk_combo_box_paint(cr, rect, state, direction);
+ case MOZ_GTK_TOOLTIP:
+ return moz_gtk_tooltip_paint(cr, rect, state, direction);
+ case MOZ_GTK_FRAME:
+ return moz_gtk_frame_paint(cr, rect, state, direction);
+ case MOZ_GTK_RESIZER:
+ return moz_gtk_resizer_paint(cr, rect, state, direction);
+ case MOZ_GTK_PROGRESSBAR:
+ return moz_gtk_progressbar_paint(cr, rect, state, direction);
+ case MOZ_GTK_PROGRESS_CHUNK:
+ case MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE:
+ case MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE:
+ return moz_gtk_progress_chunk_paint(cr, rect, state, direction, widget);
+ case MOZ_GTK_TAB_TOP:
+ case MOZ_GTK_TAB_BOTTOM:
+ return moz_gtk_tab_paint(cr, rect, state, (GtkTabFlags)flags, direction,
+ widget);
+ case MOZ_GTK_TABPANELS:
+ return moz_gtk_tabpanels_paint(cr, rect, state, direction);
+ case MOZ_GTK_TAB_SCROLLARROW:
+ return moz_gtk_tab_scroll_arrow_paint(cr, rect, state,
+ (GtkArrowType)flags, direction);
+ case MOZ_GTK_TOOLBARBUTTON_ARROW:
+ return moz_gtk_arrow_paint(cr, rect, state, (GtkArrowType)flags,
+ direction);
+ case MOZ_GTK_SPLITTER_HORIZONTAL:
+ return moz_gtk_vpaned_paint(cr, rect, state);
+ case MOZ_GTK_SPLITTER_VERTICAL:
+ return moz_gtk_hpaned_paint(cr, rect, state);
+ case MOZ_GTK_WINDOW_DECORATION:
+ return moz_gtk_window_decoration_paint(cr, rect, state, direction);
+ case MOZ_GTK_HEADER_BAR:
+ case MOZ_GTK_HEADER_BAR_MAXIMIZED:
+ return moz_gtk_header_bar_paint(widget, cr, rect, state);
+ default:
+ g_warning("Unknown widget type: %d", widget);
+ }
+
+ return MOZ_GTK_UNKNOWN_WIDGET;
+}
+
+gint moz_gtk_shutdown() {
+ /* This will destroy all of our widgets */
+ ResetWidgetCache();
+
+ return MOZ_GTK_SUCCESS;
+}
diff --git a/widget/gtk/gtkdrawing.h b/widget/gtk/gtkdrawing.h
new file mode 100644
index 0000000000..e751dc38c9
--- /dev/null
+++ b/widget/gtk/gtkdrawing.h
@@ -0,0 +1,524 @@
+/* -*- 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/. */
+
+/**
+ * gtkdrawing.h: GTK widget rendering utilities
+ *
+ * gtkdrawing provides an API for rendering GTK widgets in the
+ * current theme to a pixmap or window, without requiring an actual
+ * widget instantiation, similar to the Macintosh Appearance Manager
+ * or Windows XP's DrawThemeBackground() API.
+ */
+
+#ifndef _GTK_DRAWING_H_
+#define _GTK_DRAWING_H_
+
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+#include <algorithm>
+#include "mozilla/Span.h"
+
+/*** type definitions ***/
+typedef struct {
+ guint8 active;
+ guint8 focused;
+ guint8 selected;
+ guint8 inHover;
+ guint8 disabled;
+ guint8 isDefault;
+ guint8 canDefault;
+ /* The depressed state is for buttons which remain active for a longer period:
+ * activated toggle buttons or buttons showing a popup menu. */
+ guint8 depressed;
+ guint8 backdrop;
+ gint32 curpos; /* curpos and maxpos are used for scrollbars */
+ gint32 maxpos;
+ gint32 image_scale; /* image scale */
+} GtkWidgetState;
+
+/**
+ * A size in the same GTK pixel units as GtkBorder and GdkRectangle.
+ */
+struct MozGtkSize {
+ gint width;
+ gint height;
+
+ MozGtkSize& operator+=(const GtkBorder& aBorder) {
+ width += aBorder.left + aBorder.right;
+ height += aBorder.top + aBorder.bottom;
+ return *this;
+ }
+ MozGtkSize operator+(const GtkBorder& aBorder) const {
+ MozGtkSize result = *this;
+ return result += aBorder;
+ }
+ bool operator<(const MozGtkSize& aOther) const {
+ return (width < aOther.width && height <= aOther.height) ||
+ (width <= aOther.width && height < aOther.height);
+ }
+ void Include(MozGtkSize aOther) {
+ width = std::max(width, aOther.width);
+ height = std::max(height, aOther.height);
+ }
+ void Rotate() {
+ gint tmp = width;
+ width = height;
+ height = tmp;
+ }
+};
+
+typedef struct {
+ bool initialized;
+ MozGtkSize minSizeWithBorder;
+ GtkBorder borderAndPadding;
+} ToggleGTKMetrics;
+
+typedef struct {
+ MozGtkSize minSizeWithBorderMargin;
+ GtkBorder buttonMargin;
+ gint iconXPosition;
+ gint iconYPosition;
+ bool visible;
+ bool firstButton;
+ bool lastButton;
+} ToolbarButtonGTKMetrics;
+
+#define TOOLBAR_BUTTONS 3
+typedef struct {
+ bool initialized;
+ ToolbarButtonGTKMetrics button[TOOLBAR_BUTTONS];
+} ToolbarGTKMetrics;
+
+typedef struct {
+ bool initialized;
+ GtkBorder decorationSize;
+} CSDWindowDecorationSize;
+
+/** flags for tab state **/
+typedef enum {
+ /* first eight bits are used to pass a margin */
+ MOZ_GTK_TAB_MARGIN_MASK = 0xFF,
+ /* the first tab in the group */
+ MOZ_GTK_TAB_FIRST = 1 << 9,
+ /* the selected tab */
+ MOZ_GTK_TAB_SELECTED = 1 << 10
+} GtkTabFlags;
+
+/*** result/error codes ***/
+#define MOZ_GTK_SUCCESS 0
+#define MOZ_GTK_UNKNOWN_WIDGET -1
+#define MOZ_GTK_UNSAFE_THEME -2
+
+/*** checkbox/radio flags ***/
+#define MOZ_GTK_WIDGET_CHECKED 1
+#define MOZ_GTK_WIDGET_INCONSISTENT (1 << 1)
+
+/*** widget type constants ***/
+enum WidgetNodeType : int {
+ /* Paints a GtkButton. flags is a GtkReliefStyle. */
+ MOZ_GTK_BUTTON,
+ /* Paints a button with image and no text */
+ MOZ_GTK_TOOLBAR_BUTTON,
+ /* Paints a toggle button */
+ MOZ_GTK_TOGGLE_BUTTON,
+ /* Paints a button arrow */
+ MOZ_GTK_BUTTON_ARROW,
+
+ /* Paints the container part of a GtkCheckButton. */
+ MOZ_GTK_CHECKBUTTON_CONTAINER,
+ /* Paints a GtkCheckButton. flags is a boolean, 1=checked, 0=not checked. */
+ MOZ_GTK_CHECKBUTTON,
+
+ /* Paints the container part of a GtkRadioButton. */
+ MOZ_GTK_RADIOBUTTON_CONTAINER,
+ /* Paints a GtkRadioButton. flags is a boolean, 1=checked, 0=not checked. */
+ MOZ_GTK_RADIOBUTTON,
+ /* Vertical GtkScrollbar counterparts */
+ MOZ_GTK_SCROLLBAR_VERTICAL,
+ MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL,
+ MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL,
+ MOZ_GTK_SCROLLBAR_THUMB_VERTICAL,
+
+ /* Paints a GtkScale. */
+ MOZ_GTK_SCALE_HORIZONTAL,
+ MOZ_GTK_SCALE_VERTICAL,
+ /* Paints a GtkScale trough. */
+ MOZ_GTK_SCALE_CONTENTS_HORIZONTAL,
+ MOZ_GTK_SCALE_CONTENTS_VERTICAL,
+ MOZ_GTK_SCALE_TROUGH_HORIZONTAL,
+ MOZ_GTK_SCALE_TROUGH_VERTICAL,
+ /* Paints a GtkScale thumb. */
+ MOZ_GTK_SCALE_THUMB_HORIZONTAL,
+ MOZ_GTK_SCALE_THUMB_VERTICAL,
+ /* Paints a GtkSpinButton */
+ MOZ_GTK_INNER_SPIN_BUTTON,
+ MOZ_GTK_SPINBUTTON,
+ MOZ_GTK_SPINBUTTON_UP,
+ MOZ_GTK_SPINBUTTON_DOWN,
+ MOZ_GTK_SPINBUTTON_ENTRY,
+ /* Paints a GtkEntry. */
+ MOZ_GTK_ENTRY,
+ /* Paints a GtkExpander. */
+ MOZ_GTK_EXPANDER,
+ /* Paints a GtkTextView or gets the style context corresponding to the
+ root node of a GtkTextView. */
+ MOZ_GTK_TEXT_VIEW,
+ /* The "text" window or node of a GtkTextView */
+ MOZ_GTK_TEXT_VIEW_TEXT,
+ /* The "selection" node of a GtkTextView.text */
+ MOZ_GTK_TEXT_VIEW_TEXT_SELECTION,
+ /* Paints a GtkOptionMenu. */
+ MOZ_GTK_DROPDOWN,
+ /* Paints an entry in an editable option menu */
+ MOZ_GTK_DROPDOWN_ENTRY,
+
+ /* Paints a GtkToolTip */
+ MOZ_GTK_TOOLTIP,
+ /* Paints a GtkBox from GtkToolTip */
+ MOZ_GTK_TOOLTIP_BOX,
+ /* Paints a GtkLabel of GtkToolTip */
+ MOZ_GTK_TOOLTIP_BOX_LABEL,
+ /* Paints a GtkFrame (e.g. a status bar panel). */
+ MOZ_GTK_FRAME,
+ /* Paints the border of a GtkFrame */
+ MOZ_GTK_FRAME_BORDER,
+ /* Paints a resize grip for a GtkTextView */
+ MOZ_GTK_RESIZER,
+ /* Paints a GtkProgressBar. */
+ MOZ_GTK_PROGRESSBAR,
+ /* Paints a trough (track) of a GtkProgressBar */
+ MOZ_GTK_PROGRESS_TROUGH,
+ /* Paints a progress chunk of a GtkProgressBar. */
+ MOZ_GTK_PROGRESS_CHUNK,
+ /* Paints a progress chunk of an indeterminated GtkProgressBar. */
+ MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE,
+ /* Paints a progress chunk of a vertical indeterminated GtkProgressBar. */
+ MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE,
+ /* Used as root style of whole GtkNotebook widget */
+ MOZ_GTK_NOTEBOOK,
+ /* Used as root style of active GtkNotebook area which contains tabs and
+ arrows. */
+ MOZ_GTK_NOTEBOOK_HEADER,
+ /* Paints a tab of a GtkNotebook. flags is a GtkTabFlags, defined above. */
+ MOZ_GTK_TAB_TOP,
+ /* Paints a tab of a GtkNotebook. flags is a GtkTabFlags, defined above. */
+ MOZ_GTK_TAB_BOTTOM,
+ /* Paints the background and border of a GtkNotebook. */
+ MOZ_GTK_TABPANELS,
+ /* Paints a GtkArrow for a GtkNotebook. flags is a GtkArrowType. */
+ MOZ_GTK_TAB_SCROLLARROW,
+ /* Paints the expander and border of a GtkTreeView */
+ MOZ_GTK_TREEVIEW,
+ /* Paints the border of a GtkTreeView */
+ MOZ_GTK_TREEVIEW_VIEW,
+ /* Paints treeheader cells */
+ MOZ_GTK_TREE_HEADER_CELL,
+ /* Paints an expander for a GtkTreeView */
+ MOZ_GTK_TREEVIEW_EXPANDER,
+ /* Paints the background of menus, context menus. */
+ MOZ_GTK_MENUPOPUP,
+ /* Menubar for -moz-headerbar colors */
+ MOZ_GTK_MENUBAR,
+ /* Paints an arrow in a toolbar button. flags is a GtkArrowType. */
+ MOZ_GTK_TOOLBARBUTTON_ARROW,
+ /* Paints items of popup menus. */
+ MOZ_GTK_MENUITEM,
+ /* Menubar menuitem for foreground colors. */
+ MOZ_GTK_MENUBARITEM,
+ /* GtkVPaned base class */
+ MOZ_GTK_SPLITTER_HORIZONTAL,
+ /* GtkHPaned base class */
+ MOZ_GTK_SPLITTER_VERTICAL,
+ /* Paints a GtkVPaned separator */
+ MOZ_GTK_SPLITTER_SEPARATOR_HORIZONTAL,
+ /* Paints a GtkHPaned separator */
+ MOZ_GTK_SPLITTER_SEPARATOR_VERTICAL,
+ /* Paints the background of a window, dialog or page. */
+ MOZ_GTK_WINDOW,
+ /* Used only as a container for MOZ_GTK_HEADER_BAR. */
+ MOZ_GTK_HEADERBAR_WINDOW,
+ /* Used only as a container for MOZ_GTK_HEADER_BAR_MAXIMIZED. */
+ MOZ_GTK_HEADERBAR_WINDOW_MAXIMIZED,
+ /* Used only as a container for MOZ_GTK_HEADER_BAR. */
+ MOZ_GTK_HEADERBAR_FIXED,
+ /* Used only as a container for MOZ_GTK_HEADER_BAR_MAXIMIZED. */
+ MOZ_GTK_HEADERBAR_FIXED_MAXIMIZED,
+ /* Window container for all widgets */
+ MOZ_GTK_WINDOW_CONTAINER,
+ /* Used for widget tree construction. */
+ MOZ_GTK_COMBOBOX,
+ /* Paints a GtkComboBox button widget. */
+ MOZ_GTK_COMBOBOX_BUTTON,
+ /* Paints a GtkComboBox arrow widget. */
+ MOZ_GTK_COMBOBOX_ARROW,
+ /* Paints a GtkComboBox separator widget. */
+ MOZ_GTK_COMBOBOX_SEPARATOR,
+ /* Used for widget tree construction. */
+ MOZ_GTK_COMBOBOX_ENTRY,
+ /* Paints a GtkComboBox entry widget. */
+ MOZ_GTK_COMBOBOX_ENTRY_TEXTAREA,
+ /* Paints a GtkComboBox entry button widget. */
+ MOZ_GTK_COMBOBOX_ENTRY_BUTTON,
+ /* Paints a GtkComboBox entry arrow widget. */
+ MOZ_GTK_COMBOBOX_ENTRY_ARROW,
+ /* Used for scrolled window shell. */
+ MOZ_GTK_SCROLLED_WINDOW,
+ /* Paints a GtkHeaderBar */
+ MOZ_GTK_HEADER_BAR,
+ /* Paints a GtkHeaderBar in maximized state */
+ MOZ_GTK_HEADER_BAR_MAXIMIZED,
+ /* Container for GtkHeaderBar buttons */
+ MOZ_GTK_HEADER_BAR_BUTTON_BOX,
+ /* Paints GtkHeaderBar title buttons.
+ * Keep the order here as MOZ_GTK_HEADER_BAR_BUTTON_* are processed
+ * as an array from MOZ_GTK_HEADER_BAR_BUTTON_CLOSE to the last one.
+ */
+ MOZ_GTK_HEADER_BAR_BUTTON_CLOSE,
+ MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE,
+ MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE,
+
+ /* MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE is a state of
+ * MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE button and it's used as
+ * an icon placeholder only.
+ */
+ MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE,
+
+ /* Client-side window decoration node. Available on GTK 3.20+. */
+ MOZ_GTK_WINDOW_DECORATION,
+ MOZ_GTK_WINDOW_DECORATION_SOLID,
+
+ MOZ_GTK_WIDGET_NODE_COUNT
+};
+
+/* ButtonLayout represents a GTK CSD button and whether its on the left or
+ * right side of the tab bar */
+struct ButtonLayout {
+ WidgetNodeType mType;
+};
+
+/*** General library functions ***/
+/**
+ * Initializes the drawing library. You must call this function
+ * prior to using any other functionality.
+ * returns: MOZ_GTK_SUCCESS if there were no errors
+ * MOZ_GTK_UNSAFE_THEME if the current theme engine is known
+ * to crash with gtkdrawing.
+ */
+gint moz_gtk_init();
+
+/**
+ * Updates the drawing library when the theme changes.
+ */
+void moz_gtk_refresh();
+
+/**
+ * Perform cleanup of the drawing library. You should call this function
+ * when your program exits, or you no longer need the library.
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_shutdown();
+
+/*** Widget drawing ***/
+/**
+ * Paint a widget in the current theme.
+ * widget: a constant giving the widget to paint
+ * drawable: the drawable to paint to;
+ * it's colormap must be moz_gtk_widget_get_colormap().
+ * rect: the bounding rectangle for the widget
+ * state: the state of the widget. ignored for some widgets.
+ * flags: widget-dependant flags; see the WidgetNodeType definition.
+ * direction: the text direction, to draw the widget correctly LTR and RTL.
+ */
+gint moz_gtk_widget_paint(WidgetNodeType widget, cairo_t* cr,
+ GdkRectangle* rect, GtkWidgetState* state, gint flags,
+ GtkTextDirection direction);
+
+/*** Widget metrics ***/
+/**
+ * Get the border size of a widget
+ * left/right: [OUT] the widget's left/right border
+ * top/bottom: [OUT] the widget's top/bottom border
+ * direction: the text direction for the widget. Callers depend on this
+ * being used only for MOZ_GTK_DROPDOWN widgets, and cache
+ * results for other widget types across direction values.
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_get_widget_border(WidgetNodeType widget, gint* left, gint* top,
+ gint* right, gint* bottom,
+ GtkTextDirection direction);
+
+/**
+ * Get the border size of a notebook tab
+ * left/right: [OUT] the tab's left/right border
+ * top/bottom: [OUT] the tab's top/bottom border
+ * direction: the text direction for the widget
+ * flags: tab-dependant flags; see the GtkTabFlags definition.
+ * widget: tab widget
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_get_tab_border(gint* left, gint* top, gint* right, gint* bottom,
+ GtkTextDirection direction, GtkTabFlags flags,
+ WidgetNodeType widget);
+
+/**
+ * Get the desired size of a GtkCheckButton
+ * indicator_size: [OUT] the indicator size
+ * indicator_spacing: [OUT] the spacing between the indicator and its
+ * container
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_checkbox_get_metrics(gint* indicator_size,
+ gint* indicator_spacing);
+
+/**
+ * Get metrics of the toggle (radio or checkbox)
+ * isRadio: [IN] true when requesting metrics for the radio button
+ * returns: pointer to ToggleGTKMetrics struct
+ */
+const ToggleGTKMetrics* GetToggleMetrics(WidgetNodeType aWidgetType);
+
+/**
+ * Get the desired size of a GtkRadioButton
+ * indicator_size: [OUT] the indicator size
+ * indicator_spacing: [OUT] the spacing between the indicator and its
+ * container
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_radio_get_metrics(gint* indicator_size, gint* indicator_spacing);
+
+/**
+ * Some GTK themes draw their indication for the default button outside
+ * the button (e.g. the glow in New Wave). This gets the extra space necessary.
+ *
+ * border_top: [OUT] extra space to add above
+ * border_left: [OUT] extra space to add to the left
+ * border_bottom: [OUT] extra space to add underneath
+ * border_right: [OUT] extra space to add to the right
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_button_get_default_overflow(gint* border_top, gint* border_left,
+ gint* border_bottom,
+ gint* border_right);
+
+/**
+ * Gets the minimum size of a GtkScale.
+ * orient: [IN] the scale orientation
+ * scale_width: [OUT] the width of the scale
+ * scale_height: [OUT] the height of the scale
+ */
+void moz_gtk_get_scale_metrics(GtkOrientation orient, gint* scale_width,
+ gint* scale_height);
+
+/**
+ * Get the desired size of a GtkScale thumb
+ * orient: [IN] the scale orientation
+ * thumb_length: [OUT] the length of the thumb
+ * thumb_height: [OUT] the height of the thumb
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_get_scalethumb_metrics(GtkOrientation orient, gint* thumb_length,
+ gint* thumb_height);
+
+/**
+ * Get the desired size of a scroll arrow widget
+ * width: [OUT] the desired width
+ * height: [OUT] the desired height
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_get_tab_scroll_arrow_size(gint* width, gint* height);
+
+/**
+ * Get the desired size of an arrow in a button
+ *
+ * widgetType: [IN] the widget for which to get the arrow size
+ * width: [OUT] the desired width
+ * height: [OUT] the desired height
+ */
+void moz_gtk_get_arrow_size(WidgetNodeType widgetType, gint* width,
+ gint* height);
+
+/**
+ * Get the minimum height of a entry widget
+ * min_content_height: [OUT] the minimum height of the content box.
+ * border_padding_height: [OUT] the size of borders and paddings.
+ */
+void moz_gtk_get_entry_min_height(gint* min_content_height,
+ gint* border_padding_height);
+
+/**
+ * Get the desired size of a toolbar separator
+ * size: [OUT] the desired width
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_get_toolbar_separator_width(gint* size);
+
+/**
+ * Get the size of a regular GTK expander that shows/hides content
+ * size: [OUT] the size of the GTK expander, size = width = height.
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_get_expander_size(gint* size);
+
+/**
+ * Get the size of a treeview's expander (we call them twisties)
+ * size: [OUT] the size of the GTK expander, size = width = height.
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_get_treeview_expander_size(gint* size);
+
+/**
+ * Get the desired size of a splitter
+ * orientation: [IN] GTK_ORIENTATION_HORIZONTAL or GTK_ORIENTATION_VERTICAL
+ * size: [OUT] width or height of the splitter handle
+ *
+ * returns: MOZ_GTK_SUCCESS if there was no error, an error code otherwise
+ */
+gint moz_gtk_splitter_get_metrics(gint orientation, gint* size);
+
+/**
+ * Get the YTHICKNESS of a tab (notebook extension).
+ */
+gint moz_gtk_get_tab_thickness(WidgetNodeType aNodeType);
+
+/**
+ * Get ToolbarButtonGTKMetrics for recent theme.
+ */
+const ToolbarButtonGTKMetrics* GetToolbarButtonMetrics(
+ WidgetNodeType aAppearance);
+
+/**
+ * Get toolbar button layout.
+ * aButtonLayout: [OUT] An array which will be filled by ButtonLayout
+ * references to visible titlebar buttons. Must contain at
+ * least TOOLBAR_BUTTONS entries if non-empty.
+ * aReversedButtonsPlacement: [OUT] True if the buttons are placed in opposite
+ * titlebar corner.
+ *
+ * returns: Number of returned entries at aButtonLayout.
+ */
+size_t GetGtkHeaderBarButtonLayout(mozilla::Span<ButtonLayout>,
+ bool* aReversedButtonsPlacement);
+
+/**
+ * Get size of CSD window extents.
+ *
+ * aIsPopup: [IN] Get decoration size for popup or toplevel window.
+ *
+ * returns: Calculated (or estimated) decoration size of given aGtkWindow.
+ */
+GtkBorder GetCSDDecorationSize(bool aIsPopup);
+
+#endif
diff --git a/widget/gtk/moz.build b/widget/gtk/moz.build
new file mode 100644
index 0000000000..0d3916853c
--- /dev/null
+++ b/widget/gtk/moz.build
@@ -0,0 +1,179 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Gtk")
+
+with Files("*CompositorWidget*"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+with Files("*WindowSurface*"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+with Files("*IMContextWrapper*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*nsGtkKeyUtils*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ DIRS += ["mozgtk"]
+
+if CONFIG["MOZ_WAYLAND"]:
+ DIRS += ["wayland", "mozwayland", "../../third_party/wayland-proxy"]
+
+if CONFIG["MOZ_ENABLE_VAAPI"]:
+ DIRS += ["vaapitest"]
+
+if CONFIG["MOZ_ENABLE_V4L2"]:
+ DIRS += ["v4l2test"]
+
+EXPORTS += [
+ "MozContainer.h",
+ "nsGTKToolkit.h",
+ "nsGtkUtils.h",
+ "nsImageToPixbuf.h",
+]
+
+EXPORTS.mozilla += [
+ "GfxInfo.h",
+ "GfxInfoUtils.h",
+ "GRefPtr.h",
+ "GUniquePtr.h",
+ "WidgetUtilsGtk.h",
+]
+
+EXPORTS.mozilla.widget += [
+ "CompositorWidgetChild.h",
+ "CompositorWidgetParent.h",
+ "DMABufLibWrapper.h",
+ "DMABufSurface.h",
+ "gbm.h",
+ "GtkCompositorWidget.h",
+ "InProcessGtkCompositorWidget.h",
+ "va_drmcommon.h",
+ "WindowSurface.h",
+ "WindowSurfaceProvider.h",
+]
+
+UNIFIED_SOURCES += [
+ "AsyncGtkClipboardRequest.cpp",
+ "CompositorWidgetChild.cpp",
+ "CompositorWidgetParent.cpp",
+ "DMABufLibWrapper.cpp",
+ "DMABufSurface.cpp",
+ "GfxInfo.cpp",
+ "gtk3drawing.cpp",
+ "GtkCompositorWidget.cpp",
+ "IMContextWrapper.cpp",
+ "InProcessGtkCompositorWidget.cpp",
+ "MozContainer.cpp",
+ "MPRISServiceHandler.cpp",
+ "NativeKeyBindings.cpp",
+ "NativeMenuGtk.cpp",
+ "NativeMenuSupport.cpp",
+ "nsApplicationChooser.cpp",
+ "nsAppShell.cpp",
+ "nsBidiKeyboard.cpp",
+ "nsClipboard.cpp",
+ "nsColorPicker.cpp",
+ "nsDragService.cpp",
+ "nsFilePicker.cpp",
+ "nsGtkKeyUtils.cpp",
+ "nsImageToPixbuf.cpp",
+ "nsLookAndFeel.cpp",
+ "nsSound.cpp",
+ "nsToolkit.cpp",
+ "nsUserIdleServiceGTK.cpp",
+ "nsWidgetFactory.cpp",
+ "ScreenHelperGTK.cpp",
+ "TaskbarProgress.cpp",
+ "WakeLockListener.cpp",
+ "WidgetStyleCache.cpp",
+ "WidgetTraceEvent.cpp",
+ "WidgetUtilsGtk.cpp",
+ "WindowSurfaceProvider.cpp",
+]
+
+SOURCES += [
+ "MediaKeysEventSourceFactory.cpp",
+ "nsNativeThemeGTK.cpp", # conflicts with X11 headers
+ "nsWindow.cpp", # conflicts with X11 headers
+ "WaylandVsyncSource.cpp", # conflicts with X11 headers
+]
+
+if CONFIG["MOZ_WAYLAND"]:
+ UNIFIED_SOURCES += [
+ "MozContainerWayland.cpp",
+ "nsClipboardWayland.cpp",
+ "nsWaylandDisplay.cpp",
+ "WaylandBuffer.cpp",
+ "WindowSurfaceWaylandMultiBuffer.cpp",
+ ]
+ EXPORTS.mozilla.widget += [
+ "MozContainerWayland.h",
+ "nsWaylandDisplay.h",
+ "WaylandBuffer.h",
+ ]
+
+if CONFIG["MOZ_X11"]:
+ UNIFIED_SOURCES += [
+ "nsClipboardX11.cpp",
+ "nsShmImage.cpp",
+ "WindowSurfaceX11.cpp",
+ "WindowSurfaceX11Image.cpp",
+ "WindowSurfaceX11SHM.cpp",
+ ]
+
+if CONFIG["NS_PRINTING"]:
+ UNIFIED_SOURCES += [
+ "nsDeviceContextSpecG.cpp",
+ "nsPrintDialogGTK.cpp",
+ "nsPrintSettingsGTK.cpp",
+ "nsPrintSettingsServiceGTK.cpp",
+ ]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/layout/base",
+ "/layout/forms",
+ "/layout/generic",
+ "/layout/xul",
+ "/other-licenses/atk-1.0",
+ "/third_party/cups/include",
+ "/widget",
+ "/widget/headless",
+ "/widget/x11",
+]
+
+DEFINES["CAIRO_GFX"] = True
+
+DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"]
+
+CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+if CONFIG["MOZ_WAYLAND"]:
+ CFLAGS += CONFIG["MOZ_WAYLAND_CFLAGS"]
+ CXXFLAGS += CONFIG["MOZ_WAYLAND_CFLAGS"]
+
+if CONFIG["MOZ_ENABLE_DBUS"]:
+ EXPORTS.mozilla.widget += [
+ "AsyncDBus.h",
+ ]
+ UNIFIED_SOURCES += [
+ "AsyncDBus.cpp",
+ ]
+ CXXFLAGS += CONFIG["MOZ_DBUS_CFLAGS"]
+
+CXXFLAGS += ["-Werror=switch"]
diff --git a/widget/gtk/mozgtk/moz.build b/widget/gtk/mozgtk/moz.build
new file mode 100644
index 0000000000..d5e78d0032
--- /dev/null
+++ b/widget/gtk/mozgtk/moz.build
@@ -0,0 +1,37 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+SharedLibrary("mozgtk")
+
+SOURCES += [
+ "mozgtk.c",
+]
+
+CFLAGS += CONFIG["MOZ_X11_CFLAGS"]
+CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+# If LDFLAGS contains -Wl,--as-needed or if it's the default for the toolchain,
+# we need to add -Wl,--no-as-needed before the gtk libraries, otherwise the
+# linker will drop those dependencies because no symbols are used from them.
+# But those dependencies need to be kept for things to work properly.
+# Ideally, we'd only add -Wl,--no-as-needed if necessary, but it's just simpler
+# to add it unconditionally. This library is also simple enough that forcing
+# -Wl,--as-needed after the gtk libraries is not going to make a significant
+# difference.
+if CONFIG["GCC_USE_GNU_LD"]:
+ no_as_needed = ["-Wl,--no-as-needed"]
+ as_needed = ["-Wl,--as-needed"]
+else:
+ no_as_needed = []
+ as_needed = []
+
+OS_LIBS += [f for f in CONFIG["MOZ_GTK3_LIBS"] if f.startswith("-L")]
+OS_LIBS += no_as_needed
+OS_LIBS += [
+ "gtk-3",
+ "gdk-3",
+]
+OS_LIBS += as_needed
diff --git a/widget/gtk/mozgtk/mozgtk.c b/widget/gtk/mozgtk/mozgtk.c
new file mode 100644
index 0000000000..d95746fc0b
--- /dev/null
+++ b/widget/gtk/mozgtk/mozgtk.c
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/Types.h"
+
+#include <gdk/gdk.h>
+
+// Dummy call to gtk3 library to prevent the linker from removing
+// the gtk3 dependency with --as-needed.
+// see toolkit/library/moz.build for details.
+MOZ_EXPORT void mozgtk_linker_holder() { gdk_display_get_default(); }
+
+#ifdef MOZ_X11
+# include <X11/Xlib.h>
+// Bug 1271100
+// We need to trick system Cairo into not using the XShm extension due to
+// a race condition in it that results in frequent BadAccess errors. Cairo
+// relies upon XShmQueryExtension to initially detect if XShm is available.
+// So we define our own stub that always indicates XShm not being present.
+// mozgtk loads before libXext/libcairo and so this stub will take priority.
+// Our tree usage goes through xcb and remains unaffected by this.
+//
+// This is also used to force libxul to depend on the mozgtk library. If we
+// ever can remove this workaround for system Cairo, we'll need something
+// to replace it for that purpose.
+MOZ_EXPORT Bool XShmQueryExtension(Display* aDisplay) { return False; }
+#endif
diff --git a/widget/gtk/mozwayland/moz.build b/widget/gtk/mozwayland/moz.build
new file mode 100644
index 0000000000..0df3f0f685
--- /dev/null
+++ b/widget/gtk/mozwayland/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "mozwayland.c",
+]
+EXPORTS.mozilla.widget += [
+ "mozwayland.h",
+]
+
+SharedLibrary("mozwayland")
+
+CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
diff --git a/widget/gtk/mozwayland/mozwayland.c b/widget/gtk/mozwayland/mozwayland.c
new file mode 100644
index 0000000000..eb17d3227e
--- /dev/null
+++ b/widget/gtk/mozwayland/mozwayland.c
@@ -0,0 +1,230 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 <stdlib.h>
+#include "mozilla/Types.h"
+#include <gtk/gtk.h>
+#include <gtk/gtkx.h>
+#include <gdk/gdkwayland.h>
+
+union wl_argument;
+
+/* Those structures are just placeholders and will be replaced by
+ * real symbols from libwayland during run-time linking. We need to make
+ * them explicitly visible.
+ */
+#pragma GCC visibility push(default)
+const struct wl_interface wl_buffer_interface;
+const struct wl_interface wl_callback_interface;
+const struct wl_interface wl_data_device_interface;
+const struct wl_interface wl_data_device_manager_interface;
+const struct wl_interface wl_keyboard_interface;
+const struct wl_interface wl_pointer_interface;
+const struct wl_interface wl_region_interface;
+const struct wl_interface wl_registry_interface;
+const struct wl_interface wl_shm_interface;
+const struct wl_interface wl_shm_pool_interface;
+const struct wl_interface wl_seat_interface;
+const struct wl_interface wl_surface_interface;
+const struct wl_interface wl_subsurface_interface;
+const struct wl_interface wl_compositor_interface;
+const struct wl_interface wl_subcompositor_interface;
+const struct wl_interface wl_output_interface;
+#pragma GCC visibility pop
+
+MOZ_EXPORT void wl_event_queue_destroy(struct wl_event_queue* queue) {}
+
+MOZ_EXPORT void wl_proxy_marshal(struct wl_proxy* p, uint32_t opcode, ...) {}
+
+MOZ_EXPORT void wl_proxy_marshal_array(struct wl_proxy* p, uint32_t opcode,
+ union wl_argument* args) {}
+
+MOZ_EXPORT struct wl_proxy* wl_proxy_create(
+ struct wl_proxy* factory, const struct wl_interface* interface) {
+ return NULL;
+}
+
+MOZ_EXPORT void* wl_proxy_create_wrapper(void* proxy) { return NULL; }
+
+MOZ_EXPORT void wl_proxy_wrapper_destroy(void* proxy_wrapper) {}
+
+MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor(
+ struct wl_proxy* proxy, uint32_t opcode,
+ const struct wl_interface* interface, ...) {
+ return NULL;
+}
+
+MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor_versioned(
+ struct wl_proxy* proxy, uint32_t opcode,
+ const struct wl_interface* interface, uint32_t version, ...) {
+ return NULL;
+}
+
+MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_array_constructor(
+ struct wl_proxy* proxy, uint32_t opcode, union wl_argument* args,
+ const struct wl_interface* interface) {
+ return NULL;
+}
+
+MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_array_constructor_versioned(
+ struct wl_proxy* proxy, uint32_t opcode, union wl_argument* args,
+ const struct wl_interface* interface, uint32_t version) {
+ return NULL;
+}
+
+MOZ_EXPORT void wl_proxy_destroy(struct wl_proxy* proxy) {}
+
+MOZ_EXPORT int wl_proxy_add_listener(struct wl_proxy* proxy,
+ void (**implementation)(void),
+ void* data) {
+ return -1;
+}
+
+MOZ_EXPORT const void* wl_proxy_get_listener(struct wl_proxy* proxy) {
+ return NULL;
+}
+
+typedef int (*wl_dispatcher_func_t)(const void*, void*, uint32_t,
+ const struct wl_message*,
+ union wl_argument*);
+
+MOZ_EXPORT int wl_proxy_add_dispatcher(struct wl_proxy* proxy,
+ wl_dispatcher_func_t dispatcher_func,
+ const void* dispatcher_data,
+ void* data) {
+ return -1;
+}
+
+MOZ_EXPORT void wl_proxy_set_user_data(struct wl_proxy* proxy,
+ void* user_data) {}
+
+MOZ_EXPORT void* wl_proxy_get_user_data(struct wl_proxy* proxy) { return NULL; }
+
+MOZ_EXPORT uint32_t wl_proxy_get_version(struct wl_proxy* proxy) { return -1; }
+
+MOZ_EXPORT uint32_t wl_proxy_get_id(struct wl_proxy* proxy) { return -1; }
+
+MOZ_EXPORT const char* wl_proxy_get_class(struct wl_proxy* proxy) {
+ return NULL;
+}
+
+MOZ_EXPORT void wl_proxy_set_queue(struct wl_proxy* proxy,
+ struct wl_event_queue* queue) {}
+
+MOZ_EXPORT struct wl_display* wl_display_connect(const char* name) {
+ return NULL;
+}
+
+MOZ_EXPORT struct wl_display* wl_display_connect_to_fd(int fd) { return NULL; }
+
+MOZ_EXPORT void wl_display_disconnect(struct wl_display* display) {}
+
+MOZ_EXPORT int wl_display_get_fd(struct wl_display* display) { return -1; }
+
+MOZ_EXPORT int wl_display_dispatch(struct wl_display* display) { return -1; }
+
+MOZ_EXPORT int wl_display_dispatch_queue(struct wl_display* display,
+ struct wl_event_queue* queue) {
+ return -1;
+}
+
+MOZ_EXPORT int wl_display_dispatch_queue_pending(struct wl_display* display,
+ struct wl_event_queue* queue) {
+ return -1;
+}
+
+MOZ_EXPORT int wl_display_dispatch_pending(struct wl_display* display) {
+ return -1;
+}
+
+MOZ_EXPORT int wl_display_get_error(struct wl_display* display) { return -1; }
+
+MOZ_EXPORT uint32_t wl_display_get_protocol_error(
+ struct wl_display* display, const struct wl_interface** interface,
+ uint32_t* id) {
+ return -1;
+}
+
+MOZ_EXPORT int wl_display_flush(struct wl_display* display) { return -1; }
+
+MOZ_EXPORT int wl_display_roundtrip_queue(struct wl_display* display,
+ struct wl_event_queue* queue) {
+ return -1;
+}
+
+MOZ_EXPORT int wl_display_roundtrip(struct wl_display* display) { return -1; }
+
+MOZ_EXPORT struct wl_event_queue* wl_display_create_queue(
+ struct wl_display* display) {
+ return NULL;
+}
+
+MOZ_EXPORT int wl_display_prepare_read_queue(struct wl_display* display,
+ struct wl_event_queue* queue) {
+ return -1;
+}
+
+MOZ_EXPORT int wl_display_prepare_read(struct wl_display* display) {
+ return -1;
+}
+
+MOZ_EXPORT void wl_display_cancel_read(struct wl_display* display) {}
+
+MOZ_EXPORT int wl_display_read_events(struct wl_display* display) { return -1; }
+
+MOZ_EXPORT void wl_log_set_handler_client(wl_log_func_t handler) {}
+
+MOZ_EXPORT struct wl_egl_window* wl_egl_window_create(
+ struct wl_surface* surface, int width, int height) {
+ return NULL;
+}
+
+MOZ_EXPORT void wl_egl_window_destroy(struct wl_egl_window* egl_window) {}
+
+MOZ_EXPORT void wl_egl_window_resize(struct wl_egl_window* egl_window,
+ int width, int height, int dx, int dy) {}
+
+MOZ_EXPORT void wl_egl_window_get_attached_size(
+ struct wl_egl_window* egl_window, int* width, int* height) {}
+
+MOZ_EXPORT void wl_list_init(struct wl_list* list) {}
+
+MOZ_EXPORT void wl_list_insert(struct wl_list* list, struct wl_list* elm) {}
+
+MOZ_EXPORT void wl_list_remove(struct wl_list* elm) {}
+
+MOZ_EXPORT int wl_list_length(const struct wl_list* list) { return -1; }
+
+MOZ_EXPORT int wl_list_empty(const struct wl_list* list) { return -1; }
+
+MOZ_EXPORT void wl_list_insert_list(struct wl_list* list,
+ struct wl_list* other) {}
+
+MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_flags(
+ struct wl_proxy* proxy, uint32_t opcode,
+ const struct wl_interface* interface, uint32_t version, uint32_t flags,
+ ...) {
+ return NULL;
+}
+
+MOZ_EXPORT struct wl_compositor* gdk_wayland_display_get_wl_compositor(
+ GdkDisplay* display) {
+ return NULL;
+}
+MOZ_EXPORT struct wl_surface* gdk_wayland_window_get_wl_surface(
+ GdkWindow* window) {
+ return NULL;
+}
+
+MOZ_EXPORT struct wl_pointer* gdk_wayland_device_get_wl_pointer(
+ GdkDevice* device) {
+ return NULL;
+}
+MOZ_EXPORT struct wl_display* gdk_wayland_display_get_wl_display(
+ GdkDisplay* display) {
+ return NULL;
+}
diff --git a/widget/gtk/mozwayland/mozwayland.h b/widget/gtk/mozwayland/mozwayland.h
new file mode 100644
index 0000000000..22f80d6315
--- /dev/null
+++ b/widget/gtk/mozwayland/mozwayland.h
@@ -0,0 +1,134 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+/* Wayland compatibility header, it makes Firefox build with
+ wayland-1.2 and Gtk+ 3.10.
+*/
+
+#ifndef __MozWayland_h_
+#define __MozWayland_h_
+
+#include "mozilla/Types.h"
+#include <gtk/gtk.h>
+#include <gdk/gdkwayland.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+MOZ_EXPORT struct wl_display* wl_display_connect(const char* name);
+MOZ_EXPORT int wl_display_roundtrip_queue(struct wl_display* display,
+ struct wl_event_queue* queue);
+MOZ_EXPORT uint32_t wl_proxy_get_version(struct wl_proxy* proxy);
+MOZ_EXPORT void wl_proxy_marshal(struct wl_proxy* p, uint32_t opcode, ...);
+MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor(
+ struct wl_proxy* proxy, uint32_t opcode,
+ const struct wl_interface* interface, ...);
+MOZ_EXPORT struct wl_proxy* wl_proxy_marshal_constructor_versioned(
+ struct wl_proxy* proxy, uint32_t opcode,
+ const struct wl_interface* interface, uint32_t version, ...);
+MOZ_EXPORT void wl_proxy_destroy(struct wl_proxy* proxy);
+MOZ_EXPORT void* wl_proxy_create_wrapper(void* proxy);
+MOZ_EXPORT void wl_proxy_wrapper_destroy(void* proxy_wrapper);
+
+/* We need implement some missing functions from wayland-client-protocol.h
+ */
+#ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM
+enum wl_data_device_manager_dnd_action {
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE = 0,
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY = 1,
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE = 2,
+ WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK = 4
+};
+#endif
+
+#ifndef WL_DATA_OFFER_SET_ACTIONS
+# define WL_DATA_OFFER_SET_ACTIONS 4
+
+struct moz_wl_data_offer_listener {
+ void (*offer)(void* data, struct wl_data_offer* wl_data_offer,
+ const char* mime_type);
+ void (*source_actions)(void* data, struct wl_data_offer* wl_data_offer,
+ uint32_t source_actions);
+ void (*action)(void* data, struct wl_data_offer* wl_data_offer,
+ uint32_t dnd_action);
+};
+
+static inline void wl_data_offer_set_actions(
+ struct wl_data_offer* wl_data_offer, uint32_t dnd_actions,
+ uint32_t preferred_action) {
+ wl_proxy_marshal((struct wl_proxy*)wl_data_offer, WL_DATA_OFFER_SET_ACTIONS,
+ dnd_actions, preferred_action);
+}
+#else
+typedef struct wl_data_offer_listener moz_wl_data_offer_listener;
+#endif
+
+#ifndef WL_SUBCOMPOSITOR_GET_SUBSURFACE
+# define WL_SUBCOMPOSITOR_GET_SUBSURFACE 1
+struct wl_subcompositor;
+
+// Emulate what mozilla header wrapper does - make the
+// wl_subcompositor_interface always visible.
+# pragma GCC visibility push(default)
+extern const struct wl_interface wl_subsurface_interface;
+extern const struct wl_interface wl_subcompositor_interface;
+# pragma GCC visibility pop
+
+# define WL_SUBSURFACE_DESTROY 0
+# define WL_SUBSURFACE_SET_POSITION 1
+# define WL_SUBSURFACE_PLACE_ABOVE 2
+# define WL_SUBSURFACE_PLACE_BELOW 3
+# define WL_SUBSURFACE_SET_SYNC 4
+# define WL_SUBSURFACE_SET_DESYNC 5
+
+static inline struct wl_subsurface* wl_subcompositor_get_subsurface(
+ struct wl_subcompositor* wl_subcompositor, struct wl_surface* surface,
+ struct wl_surface* parent) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor(
+ (struct wl_proxy*)wl_subcompositor, WL_SUBCOMPOSITOR_GET_SUBSURFACE,
+ &wl_subsurface_interface, NULL, surface, parent);
+
+ return (struct wl_subsurface*)id;
+}
+
+static inline void wl_subsurface_set_position(
+ struct wl_subsurface* wl_subsurface, int32_t x, int32_t y) {
+ wl_proxy_marshal((struct wl_proxy*)wl_subsurface, WL_SUBSURFACE_SET_POSITION,
+ x, y);
+}
+
+static inline void wl_subsurface_set_desync(
+ struct wl_subsurface* wl_subsurface) {
+ wl_proxy_marshal((struct wl_proxy*)wl_subsurface, WL_SUBSURFACE_SET_DESYNC);
+}
+
+static inline void wl_subsurface_destroy(struct wl_subsurface* wl_subsurface) {
+ wl_proxy_marshal((struct wl_proxy*)wl_subsurface, WL_SUBSURFACE_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)wl_subsurface);
+}
+#endif
+
+#ifndef WL_SURFACE_DAMAGE_BUFFER
+# define WL_SURFACE_DAMAGE_BUFFER 9
+
+static inline void wl_surface_damage_buffer(struct wl_surface* wl_surface,
+ int32_t x, int32_t y, int32_t width,
+ int32_t height) {
+ wl_proxy_marshal((struct wl_proxy*)wl_surface, WL_SURFACE_DAMAGE_BUFFER, x, y,
+ width, height);
+}
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __MozWayland_h_ */
diff --git a/widget/gtk/nsAppShell.cpp b/widget/gtk/nsAppShell.cpp
new file mode 100644
index 0000000000..d26b43737d
--- /dev/null
+++ b/widget/gtk/nsAppShell.cpp
@@ -0,0 +1,494 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 <sys/types.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <gdk/gdk.h>
+#include "nsAppShell.h"
+#include "nsBaseAppShell.h"
+#include "nsWindow.h"
+#include "mozilla/Logging.h"
+#include "prenv.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/Hal.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerThreadSleep.h"
+#include "mozilla/Unused.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/WidgetUtils.h"
+#include "nsIPowerManagerService.h"
+#ifdef MOZ_ENABLE_DBUS
+# include <gio/gio.h>
+# include "nsIObserverService.h"
+# include "WidgetUtilsGtk.h"
+#endif
+#include "WakeLockListener.h"
+#include "gfxPlatform.h"
+#include "nsAppRunner.h"
+#include "mozilla/XREAppData.h"
+#include "ScreenHelperGTK.h"
+#include "HeadlessScreenHelper.h"
+#include "mozilla/widget/ScreenManager.h"
+#ifdef MOZ_WAYLAND
+# include "nsWaylandDisplay.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::widget;
+using mozilla::widget::HeadlessScreenHelper;
+using mozilla::widget::ScreenHelperGTK;
+using mozilla::widget::ScreenManager;
+
+#define NOTIFY_TOKEN 0xFA
+#define QUIT_TOKEN 0xFB
+
+LazyLogModule gWidgetLog("Widget");
+LazyLogModule gWidgetDragLog("WidgetDrag");
+LazyLogModule gWidgetWaylandLog("WidgetWayland");
+LazyLogModule gWidgetPopupLog("WidgetPopup");
+LazyLogModule gWidgetVsync("WidgetVsync");
+LazyLogModule gDmabufLog("Dmabuf");
+LazyLogModule gClipboardLog("WidgetClipboard");
+
+static GPollFunc sPollFunc;
+
+nsAppShell* sAppShell = nullptr;
+
+// Wrapper function to disable hang monitoring while waiting in poll().
+static gint PollWrapper(GPollFD* aUfds, guint aNfsd, gint aTimeout) {
+ if (aTimeout == 0) {
+ // When the timeout is 0, there is no wait, so no point in notifying
+ // the BackgroundHangMonitor and the profiler.
+ return (*sPollFunc)(aUfds, aNfsd, aTimeout);
+ }
+
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ gint result;
+ {
+ gint timeout = aTimeout;
+ gint64 begin = 0;
+ if (aTimeout != -1) {
+ begin = g_get_monotonic_time();
+ }
+
+ AUTO_PROFILER_LABEL("PollWrapper", IDLE);
+ AUTO_PROFILER_THREAD_SLEEP;
+ do {
+ result = (*sPollFunc)(aUfds, aNfsd, timeout);
+
+ // The result will be -1 with the EINTR error if the poll was interrupted
+ // by a signal, typically the signal sent by the profiler to sample the
+ // process. We are only done waiting if we are not in that case.
+ if (result != -1 || errno != EINTR) {
+ break;
+ }
+
+ if (aTimeout != -1) {
+ // Adjust the timeout to account for the time already spent waiting.
+ gint elapsedSinceBegin = (g_get_monotonic_time() - begin) / 1000;
+ if (elapsedSinceBegin < aTimeout) {
+ timeout = aTimeout - elapsedSinceBegin;
+ } else {
+ // poll returns 0 to indicate the call timed out before any fd
+ // became ready.
+ result = 0;
+ break;
+ }
+ }
+ } while (true);
+ }
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+ return result;
+}
+
+// Emit resume-events on GdkFrameClock if flush-events has not been
+// balanced by resume-events at dispose.
+// For https://bugzilla.gnome.org/show_bug.cgi?id=742636
+static decltype(GObjectClass::constructed) sRealGdkFrameClockConstructed;
+static decltype(GObjectClass::dispose) sRealGdkFrameClockDispose;
+static GQuark sPendingResumeQuark;
+
+static void OnFlushEvents(GObject* clock, gpointer) {
+ g_object_set_qdata(clock, sPendingResumeQuark, GUINT_TO_POINTER(1));
+}
+
+static void OnResumeEvents(GObject* clock, gpointer) {
+ g_object_set_qdata(clock, sPendingResumeQuark, nullptr);
+}
+
+static void WrapGdkFrameClockConstructed(GObject* object) {
+ sRealGdkFrameClockConstructed(object);
+
+ g_signal_connect(object, "flush-events", G_CALLBACK(OnFlushEvents), nullptr);
+ g_signal_connect(object, "resume-events", G_CALLBACK(OnResumeEvents),
+ nullptr);
+}
+
+static void WrapGdkFrameClockDispose(GObject* object) {
+ if (g_object_get_qdata(object, sPendingResumeQuark)) {
+ g_signal_emit_by_name(object, "resume-events");
+ }
+
+ sRealGdkFrameClockDispose(object);
+}
+
+/*static*/
+gboolean nsAppShell::EventProcessorCallback(GIOChannel* source,
+ GIOCondition condition,
+ gpointer data) {
+ nsAppShell* self = static_cast<nsAppShell*>(data);
+
+ unsigned char c;
+ Unused << read(self->mPipeFDs[0], &c, 1);
+ switch (c) {
+ case NOTIFY_TOKEN:
+ self->NativeEventCallback();
+ break;
+ case QUIT_TOKEN:
+ self->Exit();
+ break;
+ default:
+ NS_ASSERTION(false, "wrong token");
+ break;
+ }
+ return TRUE;
+}
+
+nsAppShell::~nsAppShell() {
+ sAppShell = nullptr;
+
+#ifdef MOZ_ENABLE_DBUS
+ StopDBusListening();
+#endif
+ mozilla::hal::Shutdown();
+
+ if (mTag) g_source_remove(mTag);
+ if (mPipeFDs[0]) close(mPipeFDs[0]);
+ if (mPipeFDs[1]) close(mPipeFDs[1]);
+}
+
+mozilla::StaticRefPtr<WakeLockListener> sWakeLockListener;
+static void AddScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> powerManager =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (powerManager) {
+ sWakeLockListener = new WakeLockListener();
+ powerManager->AddWakeLockListener(sWakeLockListener);
+ } else {
+ NS_WARNING(
+ "Failed to retrieve PowerManagerService, wakelocks will be broken!");
+ }
+}
+
+static void RemoveScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> powerManager =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (powerManager) {
+ powerManager->RemoveWakeLockListener(sWakeLockListener);
+ sWakeLockListener = nullptr;
+ }
+}
+
+#ifdef MOZ_ENABLE_DBUS
+void nsAppShell::DBusSessionSleepCallback(GDBusProxy* aProxy,
+ gchar* aSenderName,
+ gchar* aSignalName,
+ GVariant* aParameters,
+ gpointer aUserData) {
+ if (g_strcmp0(aSignalName, "PrepareForSleep")) {
+ return;
+ }
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) {
+ return;
+ }
+ if (!g_variant_is_of_type(aParameters, G_VARIANT_TYPE_TUPLE) ||
+ g_variant_n_children(aParameters) != 1) {
+ NS_WARNING(
+ nsPrintfCString("Unexpected location updated signal params type: %s\n",
+ g_variant_get_type_string(aParameters))
+ .get());
+ return;
+ }
+
+ RefPtr<GVariant> variant =
+ dont_AddRef(g_variant_get_child_value(aParameters, 0));
+ if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN)) {
+ NS_WARNING(
+ nsPrintfCString("Unexpected location updated signal params type: %s\n",
+ g_variant_get_type_string(aParameters))
+ .get());
+ return;
+ }
+
+ gboolean suspend = g_variant_get_boolean(variant);
+ if (suspend) {
+ // Post sleep_notification
+ observerService->NotifyObservers(nullptr, NS_WIDGET_SLEEP_OBSERVER_TOPIC,
+ nullptr);
+ } else {
+ // Post wake_notification
+ observerService->NotifyObservers(nullptr, NS_WIDGET_WAKE_OBSERVER_TOPIC,
+ nullptr);
+ }
+}
+
+void nsAppShell::DBusTimedatePropertiesChangedCallback(GDBusProxy* aProxy,
+ gchar* aSenderName,
+ gchar* aSignalName,
+ GVariant* aParameters,
+ gpointer aUserData) {
+ if (g_strcmp0(aSignalName, "PropertiesChanged")) {
+ return;
+ }
+ nsBaseAppShell::OnSystemTimezoneChange();
+}
+
+void nsAppShell::DBusConnectClientResponse(GObject* aObject,
+ GAsyncResult* aResult,
+ gpointer aUserData) {
+ GUniquePtr<GError> error;
+ RefPtr<GDBusProxy> proxyClient =
+ dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error)));
+ if (!proxyClient) {
+ if (!IsCancelledGError(error.get())) {
+ NS_WARNING(
+ nsPrintfCString("Failed to connect to client: %s\n", error->message)
+ .get());
+ }
+ return;
+ }
+
+ RefPtr self = static_cast<nsAppShell*>(aUserData);
+ if (!strcmp(g_dbus_proxy_get_name(proxyClient), "org.freedesktop.login1")) {
+ self->mLogin1Proxy = std::move(proxyClient);
+ g_signal_connect(self->mLogin1Proxy, "g-signal",
+ G_CALLBACK(DBusSessionSleepCallback), self);
+ } else {
+ self->mTimedate1Proxy = std::move(proxyClient);
+ g_signal_connect(self->mTimedate1Proxy, "g-signal",
+ G_CALLBACK(DBusTimedatePropertiesChangedCallback), self);
+ }
+}
+
+// Based on
+// https://github.com/lcp/NetworkManager/blob/240f47c892b4e935a3e92fc09eb15163d1fa28d8/src/nm-sleep-monitor-systemd.c
+// Use login1 to signal sleep and wake notifications.
+void nsAppShell::StartDBusListening() {
+ MOZ_DIAGNOSTIC_ASSERT(!mLogin1Proxy, "Already configured?");
+ MOZ_DIAGNOSTIC_ASSERT(!mTimedate1Proxy, "Already configured?");
+ MOZ_DIAGNOSTIC_ASSERT(!mLogin1ProxyCancellable, "Already configured?");
+ MOZ_DIAGNOSTIC_ASSERT(!mTimedate1ProxyCancellable, "Already configured?");
+
+ mLogin1ProxyCancellable = dont_AddRef(g_cancellable_new());
+ mTimedate1ProxyCancellable = dont_AddRef(g_cancellable_new());
+
+ g_dbus_proxy_new_for_bus(
+ G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr,
+ "org.freedesktop.login1", "/org/freedesktop/login1",
+ "org.freedesktop.login1.Manager", mLogin1ProxyCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(DBusConnectClientResponse), this);
+
+ g_dbus_proxy_new_for_bus(
+ G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr,
+ "org.freedesktop.timedate1", "/org/freedesktop/timedate1",
+ "org.freedesktop.DBus.Properties", mTimedate1ProxyCancellable,
+ reinterpret_cast<GAsyncReadyCallback>(DBusConnectClientResponse), this);
+}
+
+void nsAppShell::StopDBusListening() {
+ if (mLogin1Proxy) {
+ g_signal_handlers_disconnect_matched(mLogin1Proxy, G_SIGNAL_MATCH_DATA, 0,
+ 0, nullptr, nullptr, this);
+ }
+ if (mLogin1ProxyCancellable) {
+ g_cancellable_cancel(mLogin1ProxyCancellable);
+ mLogin1ProxyCancellable = nullptr;
+ }
+ mLogin1Proxy = nullptr;
+
+ if (mTimedate1Proxy) {
+ g_signal_handlers_disconnect_matched(mTimedate1Proxy, G_SIGNAL_MATCH_DATA,
+ 0, 0, nullptr, nullptr, this);
+ }
+ if (mTimedate1ProxyCancellable) {
+ g_cancellable_cancel(mTimedate1ProxyCancellable);
+ mTimedate1ProxyCancellable = nullptr;
+ }
+ mTimedate1Proxy = nullptr;
+}
+#endif
+
+void nsAppShell::TermSignalHandler(int signo) {
+ if (signo != SIGTERM) {
+ NS_WARNING("Wrong signal!");
+ return;
+ }
+ sAppShell->ScheduleQuitEvent();
+}
+
+void nsAppShell::InstallTermSignalHandler() {
+ if (!XRE_IsParentProcess() || PR_GetEnv("MOZ_DISABLE_SIG_HANDLER") ||
+ !sAppShell) {
+ return;
+ }
+
+ struct sigaction act = {}, oldact;
+ act.sa_handler = TermSignalHandler;
+ sigfillset(&act.sa_mask);
+
+ if (NS_WARN_IF(sigaction(SIGTERM, nullptr, &oldact) != 0)) {
+ return;
+ }
+ if (oldact.sa_handler != SIG_DFL) {
+ NS_WARNING("SIGTERM signal handler is already set?");
+ }
+
+ sigaction(SIGTERM, &act, nullptr);
+}
+
+nsresult nsAppShell::Init() {
+ mozilla::hal::Init();
+
+#ifdef MOZ_ENABLE_DBUS
+ if (XRE_IsParentProcess()) {
+ StartDBusListening();
+ }
+#endif
+
+ if (!sPollFunc) {
+ sPollFunc = g_main_context_get_poll_func(nullptr);
+ g_main_context_set_poll_func(nullptr, &PollWrapper);
+ }
+
+ if (XRE_IsParentProcess()) {
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+ if (gfxPlatform::IsHeadless()) {
+ screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>());
+ } else {
+ screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperGTK>());
+ }
+
+ if (gtk_check_version(3, 16, 3) == nullptr) {
+ // Before 3.16.3, GDK cannot override classname by --class command line
+ // option when program uses gdk_set_program_class().
+ //
+ // See https://bugzilla.gnome.org/show_bug.cgi?id=747634
+ //
+ // Only bother doing this for the parent process, since it's the one
+ // creating top-level windows.
+ if (gAppData) {
+ gdk_set_program_class(gAppData->remotingName);
+ }
+ }
+ }
+
+ if (!sPendingResumeQuark &&
+ gtk_check_version(3, 14, 7) != nullptr) { // GTK 3.0 to GTK 3.14.7.
+ // GTK 3.8 - 3.14 registered this type when creating the frame clock
+ // for the root window of the display when the display was opened.
+ GType gdkFrameClockIdleType = g_type_from_name("GdkFrameClockIdle");
+ if (gdkFrameClockIdleType) { // not in versions prior to 3.8
+ sPendingResumeQuark = g_quark_from_string("moz-resume-is-pending");
+ auto gdk_frame_clock_idle_class =
+ G_OBJECT_CLASS(g_type_class_peek_static(gdkFrameClockIdleType));
+ auto constructed = &gdk_frame_clock_idle_class->constructed;
+ sRealGdkFrameClockConstructed = *constructed;
+ *constructed = WrapGdkFrameClockConstructed;
+ auto dispose = &gdk_frame_clock_idle_class->dispose;
+ sRealGdkFrameClockDispose = *dispose;
+ *dispose = WrapGdkFrameClockDispose;
+ }
+ }
+
+ // Workaround for bug 1209659 which is fixed by Gtk3.20
+ if (gtk_check_version(3, 20, 0) != nullptr) {
+ unsetenv("GTK_CSD");
+ }
+
+ // Whitelist of only common, stable formats - see bugs 1197059 and 1203078
+ GSList* pixbufFormats = gdk_pixbuf_get_formats();
+ for (GSList* iter = pixbufFormats; iter; iter = iter->next) {
+ GdkPixbufFormat* format = static_cast<GdkPixbufFormat*>(iter->data);
+ gchar* name = gdk_pixbuf_format_get_name(format);
+ if (strcmp(name, "jpeg") && strcmp(name, "png") && strcmp(name, "gif") &&
+ strcmp(name, "bmp") && strcmp(name, "ico") && strcmp(name, "xpm") &&
+ strcmp(name, "svg") && strcmp(name, "webp") && strcmp(name, "avif")) {
+ gdk_pixbuf_format_set_disabled(format, TRUE);
+ }
+ g_free(name);
+ }
+ g_slist_free(pixbufFormats);
+
+ int err = pipe(mPipeFDs);
+ if (err) return NS_ERROR_OUT_OF_MEMORY;
+
+ GIOChannel* ioc;
+ GSource* source;
+
+ // make the pipe nonblocking
+
+ int flags = fcntl(mPipeFDs[0], F_GETFL, 0);
+ if (flags == -1) goto failed;
+ err = fcntl(mPipeFDs[0], F_SETFL, flags | O_NONBLOCK);
+ if (err == -1) goto failed;
+ flags = fcntl(mPipeFDs[1], F_GETFL, 0);
+ if (flags == -1) goto failed;
+ err = fcntl(mPipeFDs[1], F_SETFL, flags | O_NONBLOCK);
+ if (err == -1) goto failed;
+
+ ioc = g_io_channel_unix_new(mPipeFDs[0]);
+ source = g_io_create_watch(ioc, G_IO_IN);
+ g_io_channel_unref(ioc);
+ g_source_set_callback(source, (GSourceFunc)EventProcessorCallback, this,
+ nullptr);
+ g_source_set_can_recurse(source, TRUE);
+ mTag = g_source_attach(source, nullptr);
+ g_source_unref(source);
+
+ sAppShell = this;
+
+ return nsBaseAppShell::Init();
+failed:
+ close(mPipeFDs[0]);
+ close(mPipeFDs[1]);
+ mPipeFDs[0] = mPipeFDs[1] = 0;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsAppShell::Run() {
+ if (XRE_IsParentProcess()) {
+ AddScreenWakeLockListener();
+ }
+
+ nsresult rv = nsBaseAppShell::Run();
+
+ if (XRE_IsParentProcess()) {
+ RemoveScreenWakeLockListener();
+ }
+ return rv;
+}
+
+void nsAppShell::ScheduleNativeEventCallback() {
+ unsigned char buf[] = {NOTIFY_TOKEN};
+ Unused << write(mPipeFDs[1], buf, 1);
+}
+
+void nsAppShell::ScheduleQuitEvent() {
+ unsigned char buf[] = {QUIT_TOKEN};
+ Unused << write(mPipeFDs[1], buf, 1);
+}
+
+bool nsAppShell::ProcessNextNativeEvent(bool mayWait) {
+ if (mSuspendNativeCount) {
+ return false;
+ }
+ bool didProcessEvent = g_main_context_iteration(nullptr, mayWait);
+ return didProcessEvent;
+}
diff --git a/widget/gtk/nsAppShell.h b/widget/gtk/nsAppShell.h
new file mode 100644
index 0000000000..0b628033cc
--- /dev/null
+++ b/widget/gtk/nsAppShell.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef nsAppShell_h__
+#define nsAppShell_h__
+
+#ifdef MOZ_ENABLE_DBUS
+# include <gio/gio.h>
+# include "mozilla/RefPtr.h"
+# include "mozilla/GRefPtr.h"
+#endif
+#include <glib.h>
+#include "nsBaseAppShell.h"
+
+class nsAppShell : public nsBaseAppShell {
+ public:
+ nsAppShell() = default;
+
+ // nsBaseAppShell overrides:
+ nsresult Init();
+ NS_IMETHOD Run() override;
+
+ void ScheduleNativeEventCallback() override;
+ bool ProcessNextNativeEvent(bool mayWait) override;
+
+#ifdef MOZ_ENABLE_DBUS
+ void StartDBusListening();
+ void StopDBusListening();
+
+ static void DBusSessionSleepCallback(GDBusProxy* aProxy, gchar* aSenderName,
+ gchar* aSignalName,
+ GVariant* aParameters,
+ gpointer aUserData);
+ static void DBusTimedatePropertiesChangedCallback(GDBusProxy* aProxy,
+ gchar* aSenderName,
+ gchar* aSignalName,
+ GVariant* aParameters,
+ gpointer aUserData);
+ static void DBusConnectClientResponse(GObject* aObject, GAsyncResult* aResult,
+ gpointer aUserData);
+#endif
+
+ static void InstallTermSignalHandler();
+
+ private:
+ virtual ~nsAppShell();
+
+ static gboolean EventProcessorCallback(GIOChannel* source,
+ GIOCondition condition, gpointer data);
+ static void TermSignalHandler(int signo);
+
+ void ScheduleQuitEvent();
+
+ int mPipeFDs[2] = {0, 0};
+ unsigned mTag = 0;
+
+#ifdef MOZ_ENABLE_DBUS
+ RefPtr<GDBusProxy> mLogin1Proxy;
+ RefPtr<GCancellable> mLogin1ProxyCancellable;
+ RefPtr<GDBusProxy> mTimedate1Proxy;
+ RefPtr<GCancellable> mTimedate1ProxyCancellable;
+#endif
+};
+
+#endif /* nsAppShell_h__ */
diff --git a/widget/gtk/nsApplicationChooser.cpp b/widget/gtk/nsApplicationChooser.cpp
new file mode 100644
index 0000000000..6752c52caf
--- /dev/null
+++ b/widget/gtk/nsApplicationChooser.cpp
@@ -0,0 +1,132 @@
+/* -*- 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 "mozilla/Types.h"
+
+#include <gtk/gtk.h>
+
+#include "nsApplicationChooser.h"
+#include "WidgetUtils.h"
+#include "nsIMIMEInfo.h"
+#include "nsIWidget.h"
+#include "nsIFile.h"
+#include "nsCExternalHandlerService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsGtkUtils.h"
+#include "nsPIDOMWindow.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsApplicationChooser, nsIApplicationChooser)
+
+nsApplicationChooser::nsApplicationChooser() = default;
+
+nsApplicationChooser::~nsApplicationChooser() = default;
+
+NS_IMETHODIMP
+nsApplicationChooser::Init(mozIDOMWindowProxy* aParent,
+ const nsACString& aTitle) {
+ NS_ENSURE_TRUE(aParent, NS_ERROR_FAILURE);
+ auto* parent = nsPIDOMWindowOuter::From(aParent);
+ mParentWidget = widget::WidgetUtils::DOMWindowToWidget(parent);
+ mWindowTitle.Assign(aTitle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsApplicationChooser::Open(const nsACString& aContentType,
+ nsIApplicationChooserFinishedCallback* aCallback) {
+ MOZ_ASSERT(aCallback);
+ if (mCallback) {
+ NS_WARNING("Chooser is already in progress.");
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+ mCallback = aCallback;
+ NS_ENSURE_TRUE(mParentWidget, NS_ERROR_FAILURE);
+ GtkWindow* parent_widget =
+ GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET));
+
+ GtkWidget* chooser = gtk_app_chooser_dialog_new_for_content_type(
+ parent_widget,
+ (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
+ PromiseFlatCString(aContentType).get());
+ gtk_app_chooser_dialog_set_heading(GTK_APP_CHOOSER_DIALOG(chooser),
+ mWindowTitle.BeginReading());
+ NS_ADDREF_THIS();
+ g_signal_connect(chooser, "response", G_CALLBACK(OnResponse), this);
+ g_signal_connect(chooser, "destroy", G_CALLBACK(OnDestroy), this);
+ gtk_widget_show(chooser);
+ return NS_OK;
+}
+
+/* static */
+void nsApplicationChooser::OnResponse(GtkWidget* chooser, gint response_id,
+ gpointer user_data) {
+ static_cast<nsApplicationChooser*>(user_data)->Done(chooser, response_id);
+}
+
+/* static */
+void nsApplicationChooser::OnDestroy(GtkWidget* chooser, gpointer user_data) {
+ static_cast<nsApplicationChooser*>(user_data)->Done(chooser,
+ GTK_RESPONSE_CANCEL);
+}
+
+void nsApplicationChooser::Done(GtkWidget* chooser, gint response) {
+ nsCOMPtr<nsILocalHandlerApp> localHandler;
+ nsresult rv;
+ switch (response) {
+ case GTK_RESPONSE_OK:
+ case GTK_RESPONSE_ACCEPT: {
+ localHandler = do_CreateInstance(NS_LOCALHANDLERAPP_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Out of memory.");
+ break;
+ }
+ GAppInfo* app_info =
+ gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(chooser));
+
+ nsCOMPtr<nsIFile> localExecutable;
+ gchar* fileWithFullPath =
+ g_find_program_in_path(g_app_info_get_executable(app_info));
+ if (!fileWithFullPath) {
+ g_object_unref(app_info);
+ NS_WARNING("Cannot find program in path.");
+ break;
+ }
+ rv = NS_NewNativeLocalFile(nsDependentCString(fileWithFullPath), false,
+ getter_AddRefs(localExecutable));
+ g_free(fileWithFullPath);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Cannot create local filename.");
+ localHandler = nullptr;
+ } else {
+ localHandler->SetExecutable(localExecutable);
+ localHandler->SetName(
+ NS_ConvertUTF8toUTF16(g_app_info_get_display_name(app_info)));
+ }
+ g_object_unref(app_info);
+ }
+
+ break;
+ case GTK_RESPONSE_CANCEL:
+ case GTK_RESPONSE_CLOSE:
+ case GTK_RESPONSE_DELETE_EVENT:
+ break;
+ default:
+ NS_WARNING("Unexpected response");
+ break;
+ }
+
+ // A "response" signal won't be sent again but "destroy" will be.
+ g_signal_handlers_disconnect_by_func(chooser, FuncToGpointer(OnDestroy),
+ this);
+ gtk_widget_destroy(chooser);
+
+ if (mCallback) {
+ mCallback->Done(localHandler);
+ mCallback = nullptr;
+ }
+ NS_RELEASE_THIS();
+}
diff --git a/widget/gtk/nsApplicationChooser.h b/widget/gtk/nsApplicationChooser.h
new file mode 100644
index 0000000000..22f9a808c0
--- /dev/null
+++ b/widget/gtk/nsApplicationChooser.h
@@ -0,0 +1,32 @@
+/* -*- 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/. */
+
+#ifndef nsApplicationChooser_h__
+#define nsApplicationChooser_h__
+
+#include <gtk/gtk.h>
+#include "nsCOMPtr.h"
+#include "nsIApplicationChooser.h"
+#include "nsString.h"
+
+class nsIWidget;
+
+class nsApplicationChooser final : public nsIApplicationChooser {
+ public:
+ nsApplicationChooser();
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAPPLICATIONCHOOSER
+ void Done(GtkWidget* chooser, gint response);
+
+ private:
+ ~nsApplicationChooser();
+ nsCOMPtr<nsIWidget> mParentWidget;
+ nsCString mWindowTitle;
+ nsCOMPtr<nsIApplicationChooserFinishedCallback> mCallback;
+ static void OnResponse(GtkWidget* chooser, gint response_id,
+ gpointer user_data);
+ static void OnDestroy(GtkWidget* chooser, gpointer user_data);
+};
+#endif
diff --git a/widget/gtk/nsBidiKeyboard.cpp b/widget/gtk/nsBidiKeyboard.cpp
new file mode 100644
index 0000000000..a57235195b
--- /dev/null
+++ b/widget/gtk/nsBidiKeyboard.cpp
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 "prlink.h"
+
+#include "nsBidiKeyboard.h"
+#include "nsIWidget.h"
+#include <gtk/gtk.h>
+
+NS_IMPL_ISUPPORTS(nsBidiKeyboard, nsIBidiKeyboard)
+
+nsBidiKeyboard::nsBidiKeyboard() { Reset(); }
+
+NS_IMETHODIMP
+nsBidiKeyboard::Reset() {
+ // NB: The default keymap can be null (e.g. in xpcshell). In that case,
+ // simply assume that we don't have bidi keyboards.
+ mHaveBidiKeyboards = false;
+
+ GdkDisplay* display = gdk_display_get_default();
+ if (!display) return NS_OK;
+
+ GdkKeymap* keymap = gdk_keymap_get_for_display(display);
+ mHaveBidiKeyboards = keymap && gdk_keymap_have_bidi_layouts(keymap);
+ return NS_OK;
+}
+
+nsBidiKeyboard::~nsBidiKeyboard() = default;
+
+NS_IMETHODIMP
+nsBidiKeyboard::IsLangRTL(bool* aIsRTL) {
+ if (!mHaveBidiKeyboards) return NS_ERROR_FAILURE;
+
+ *aIsRTL = (gdk_keymap_get_direction(gdk_keymap_get_default()) ==
+ PANGO_DIRECTION_RTL);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBidiKeyboard::GetHaveBidiKeyboards(bool* aResult) {
+ // not implemented yet
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// static
+already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() {
+ return do_AddRef(new nsBidiKeyboard());
+}
diff --git a/widget/gtk/nsBidiKeyboard.h b/widget/gtk/nsBidiKeyboard.h
new file mode 100644
index 0000000000..1bc1c7e0a2
--- /dev/null
+++ b/widget/gtk/nsBidiKeyboard.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsBidiKeyboard
+#define __nsBidiKeyboard
+#include "nsIBidiKeyboard.h"
+
+class nsBidiKeyboard : public nsIBidiKeyboard {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIBIDIKEYBOARD
+
+ nsBidiKeyboard();
+
+ protected:
+ virtual ~nsBidiKeyboard();
+
+ bool mHaveBidiKeyboards;
+};
+
+#endif // __nsBidiKeyboard
diff --git a/widget/gtk/nsClipboard.cpp b/widget/gtk/nsClipboard.cpp
new file mode 100644
index 0000000000..571b43f1cc
--- /dev/null
+++ b/widget/gtk/nsClipboard.cpp
@@ -0,0 +1,1425 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "mozilla/ArrayUtils.h"
+
+#include "nsArrayUtils.h"
+#include "nsClipboard.h"
+#if defined(MOZ_X11)
+# include "nsClipboardX11.h"
+#endif
+#if defined(MOZ_WAYLAND)
+# include "nsClipboardWayland.h"
+# include "nsWaylandDisplay.h"
+#endif
+#include "nsGtkUtils.h"
+#include "nsIURI.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsContentUtils.h"
+#include "HeadlessClipboard.h"
+#include "nsSupportsPrimitives.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsImageToPixbuf.h"
+#include "nsStringStream.h"
+#include "nsIFileURL.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/GRefPtr.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/TimeStamp.h"
+#include "GRefPtr.h"
+#include "WidgetUtilsGtk.h"
+
+#include "imgIContainer.h"
+
+#include <gtk/gtk.h>
+#if defined(MOZ_X11)
+# include <gtk/gtkx.h>
+#endif
+
+#include "mozilla/Encoding.h"
+
+using namespace mozilla;
+
+// Idle timeout for receiving selection and property notify events (microsec)
+// Right now it's set to 1 sec.
+const int kClipboardTimeout = 1000000;
+
+// Defines how many event loop iterations will be done without sleep.
+// We ususally get data in first 2-3 iterations unless some large object
+// (an image for instance) is transferred through clipboard.
+const int kClipboardFastIterationNum = 3;
+
+// We add this prefix to HTML markup, so that GetHTMLCharset can correctly
+// detect the HTML as UTF-8 encoded.
+static const char kHTMLMarkupPrefix[] =
+ R"(<meta http-equiv="content-type" content="text/html; charset=utf-8">)";
+
+static const char kURIListMime[] = "text/uri-list";
+
+ClipboardTargets nsRetrievalContext::sClipboardTargets;
+ClipboardTargets nsRetrievalContext::sPrimaryTargets;
+
+// Callback when someone asks us for the data
+static void clipboard_get_cb(GtkClipboard* aGtkClipboard,
+ GtkSelectionData* aSelectionData, guint info,
+ gpointer user_data);
+
+// Callback when someone asks us to clear a clipboard
+static void clipboard_clear_cb(GtkClipboard* aGtkClipboard, gpointer user_data);
+
+// Callback when owner of clipboard data is changed
+static void clipboard_owner_change_cb(GtkClipboard* aGtkClipboard,
+ GdkEventOwnerChange* aEvent,
+ gpointer aUserData);
+
+static bool GetHTMLCharset(Span<const char> aData, nsCString& str);
+
+static void SetTransferableData(nsITransferable* aTransferable,
+ const nsACString& aFlavor,
+ const char* aClipboardData,
+ uint32_t aClipboardDataLength) {
+ LOGCLIP("SetTransferableData MIME %s\n", PromiseFlatCString(aFlavor).get());
+ nsCOMPtr<nsISupports> wrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ aFlavor, aClipboardData, aClipboardDataLength, getter_AddRefs(wrapper));
+ aTransferable->SetTransferData(PromiseFlatCString(aFlavor).get(), wrapper);
+}
+
+ClipboardTargets ClipboardTargets::Clone() {
+ ClipboardTargets ret;
+ ret.mCount = mCount;
+ if (mCount) {
+ ret.mTargets.reset(
+ reinterpret_cast<GdkAtom*>(g_malloc(sizeof(GdkAtom) * mCount)));
+ memcpy(ret.mTargets.get(), mTargets.get(), sizeof(GdkAtom) * mCount);
+ }
+ return ret;
+}
+
+void ClipboardTargets::Set(ClipboardTargets aTargets) {
+ mCount = aTargets.mCount;
+ mTargets = std::move(aTargets.mTargets);
+}
+
+void ClipboardData::SetData(Span<const uint8_t> aData) {
+ mData = nullptr;
+ mLength = aData.Length();
+ if (mLength) {
+ mData.reset(reinterpret_cast<char*>(g_malloc(sizeof(char) * mLength)));
+ memcpy(mData.get(), aData.data(), sizeof(char) * mLength);
+ }
+}
+
+void ClipboardData::SetText(Span<const char> aData) {
+ mData = nullptr;
+ mLength = aData.Length();
+ if (mLength) {
+ mData.reset(
+ reinterpret_cast<char*>(g_malloc(sizeof(char) * (mLength + 1))));
+ memcpy(mData.get(), aData.data(), sizeof(char) * mLength);
+ mData.get()[mLength] = '\0';
+ }
+}
+
+void ClipboardData::SetTargets(ClipboardTargets aTargets) {
+ mLength = aTargets.mCount;
+ mData.reset(reinterpret_cast<char*>(aTargets.mTargets.release()));
+}
+
+ClipboardTargets ClipboardData::ExtractTargets() {
+ GUniquePtr<GdkAtom> targets(reinterpret_cast<GdkAtom*>(mData.release()));
+ uint32_t length = std::exchange(mLength, 0);
+ return ClipboardTargets{std::move(targets), length};
+}
+
+GdkAtom GetSelectionAtom(int32_t aWhichClipboard) {
+ if (aWhichClipboard == nsIClipboard::kGlobalClipboard)
+ return GDK_SELECTION_CLIPBOARD;
+
+ return GDK_SELECTION_PRIMARY;
+}
+
+int GetGeckoClipboardType(GtkClipboard* aGtkClipboard) {
+ if (aGtkClipboard == gtk_clipboard_get(GDK_SELECTION_PRIMARY))
+ return nsClipboard::kSelectionClipboard;
+ else if (aGtkClipboard == gtk_clipboard_get(GDK_SELECTION_CLIPBOARD))
+ return nsClipboard::kGlobalClipboard;
+
+ return -1; // THAT AIN'T NO CLIPBOARD I EVER HEARD OF
+}
+
+void nsRetrievalContext::ClearCachedTargetsClipboard(GtkClipboard* aClipboard,
+ GdkEvent* aEvent,
+ gpointer data) {
+ LOGCLIP("nsRetrievalContext::ClearCachedTargetsClipboard()");
+ sClipboardTargets.Clear();
+}
+
+void nsRetrievalContext::ClearCachedTargetsPrimary(GtkClipboard* aClipboard,
+ GdkEvent* aEvent,
+ gpointer data) {
+ LOGCLIP("nsRetrievalContext::ClearCachedTargetsPrimary()");
+ sPrimaryTargets.Clear();
+}
+
+ClipboardTargets nsRetrievalContext::GetTargets(int32_t aWhichClipboard) {
+ LOGCLIP("nsRetrievalContext::GetTargets(%s)\n",
+ aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+ ClipboardTargets& storedTargets =
+ (aWhichClipboard == nsClipboard::kSelectionClipboard) ? sPrimaryTargets
+ : sClipboardTargets;
+ if (!storedTargets) {
+ LOGCLIP(" getting targets from system");
+ storedTargets.Set(GetTargetsImpl(aWhichClipboard));
+ } else {
+ LOGCLIP(" using cached targets");
+ }
+ return storedTargets.Clone();
+}
+
+nsRetrievalContext::~nsRetrievalContext() {
+ sClipboardTargets.Clear();
+ sPrimaryTargets.Clear();
+}
+
+nsClipboard::nsClipboard()
+ : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(
+#ifdef MOZ_WAYLAND
+ widget::GdkIsWaylandDisplay()
+ ? widget::WaylandDisplayGet()->IsPrimarySelectionEnabled()
+ : true,
+#else
+ true, /* supportsSelectionClipboard */
+#endif
+ false /* supportsFindClipboard */,
+ false /* supportsSelectionCache */)) {
+ g_signal_connect(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), "owner-change",
+ G_CALLBACK(clipboard_owner_change_cb), this);
+ g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY), "owner-change",
+ G_CALLBACK(clipboard_owner_change_cb), this);
+}
+
+nsClipboard::~nsClipboard() {
+ g_signal_handlers_disconnect_by_func(
+ gtk_clipboard_get(GDK_SELECTION_CLIPBOARD),
+ FuncToGpointer(clipboard_owner_change_cb), this);
+ g_signal_handlers_disconnect_by_func(
+ gtk_clipboard_get(GDK_SELECTION_PRIMARY),
+ FuncToGpointer(clipboard_owner_change_cb), this);
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsClipboard, nsBaseClipboard, nsIObserver)
+
+nsresult nsClipboard::Init(void) {
+#if defined(MOZ_X11)
+ if (widget::GdkIsX11Display()) {
+ mContext = new nsRetrievalContextX11();
+ }
+#endif
+#if defined(MOZ_WAYLAND)
+ if (widget::GdkIsWaylandDisplay()) {
+ mContext = new nsRetrievalContextWayland();
+ }
+#endif
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->AddObserver(this, "xpcom-shutdown", false);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsClipboard::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ // Save global clipboard content to CLIPBOARD_MANAGER.
+ // gtk_clipboard_store() can run an event loop, so call from a dedicated
+ // runnable.
+ return SchedulerGroup::Dispatch(
+ NS_NewRunnableFunction("gtk_clipboard_store()", []() {
+ LOGCLIP("nsClipboard storing clipboard content\n");
+ gtk_clipboard_store(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD));
+ }));
+}
+
+NS_IMETHODIMP
+nsClipboard::SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ // See if we can short cut
+ if ((aWhichClipboard == kGlobalClipboard &&
+ aTransferable == mGlobalTransferable.get()) ||
+ (aWhichClipboard == kSelectionClipboard &&
+ aTransferable == mSelectionTransferable.get())) {
+ return NS_OK;
+ }
+
+ LOGCLIP("nsClipboard::SetNativeClipboardData (%s)\n",
+ aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+
+ // List of suported targets
+ GtkTargetList* list = gtk_target_list_new(nullptr, 0);
+
+ // Get the types of supported flavors
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanExport(flavors);
+ if (NS_FAILED(rv)) {
+ LOGCLIP(" FlavorsTransferableCanExport failed!\n");
+ // Fall through. |gtkTargets| will be null below.
+ }
+
+ // Add all the flavors to this widget's supported type.
+ bool imagesAdded = false;
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+ LOGCLIP(" processing target %s\n", flavorStr.get());
+
+ // Special case text/plain since we can handle all of the string types.
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ LOGCLIP(" adding TEXT targets\n");
+ gtk_target_list_add_text_targets(list, 0);
+ continue;
+ }
+
+ if (nsContentUtils::IsFlavorImage(flavorStr)) {
+ // Don't bother adding image targets twice
+ if (!imagesAdded) {
+ // accept any writable image type
+ LOGCLIP(" adding IMAGE targets\n");
+ gtk_target_list_add_image_targets(list, 0, TRUE);
+ imagesAdded = true;
+ }
+ continue;
+ }
+
+ if (flavorStr.EqualsLiteral(kFileMime)) {
+ LOGCLIP(" adding text/uri-list target\n");
+ GdkAtom atom = gdk_atom_intern(kURIListMime, FALSE);
+ gtk_target_list_add(list, atom, 0, 0);
+ continue;
+ }
+
+ // Add this to our list of valid targets
+ LOGCLIP(" adding OTHER target %s\n", flavorStr.get());
+ GdkAtom atom = gdk_atom_intern(flavorStr.get(), FALSE);
+ gtk_target_list_add(list, atom, 0, 0);
+ }
+
+ // Get GTK clipboard (CLIPBOARD or PRIMARY)
+ GtkClipboard* gtkClipboard =
+ gtk_clipboard_get(GetSelectionAtom(aWhichClipboard));
+
+ gint numTargets = 0;
+ GtkTargetEntry* gtkTargets =
+ gtk_target_table_new_from_list(list, &numTargets);
+ if (!gtkTargets || numTargets == 0) {
+ LOGCLIP(
+ " gtk_target_table_new_from_list() failed or empty list of "
+ "targets!\n");
+ // Clear references to the any old data and let GTK know that it is no
+ // longer available.
+ EmptyNativeClipboardData(aWhichClipboard);
+ return NS_ERROR_FAILURE;
+ }
+
+ ClearCachedTargets(aWhichClipboard);
+
+ // Set getcallback and request to store data after an application exit
+ if (gtk_clipboard_set_with_data(gtkClipboard, gtkTargets, numTargets,
+ clipboard_get_cb, clipboard_clear_cb, this)) {
+ // We managed to set-up the clipboard so update internal state
+ // We have to set it now because gtk_clipboard_set_with_data() calls
+ // clipboard_clear_cb() which reset our internal state
+ if (aWhichClipboard == kSelectionClipboard) {
+ mSelectionSequenceNumber++;
+ mSelectionTransferable = aTransferable;
+ } else {
+ mGlobalSequenceNumber++;
+ mGlobalTransferable = aTransferable;
+ gtk_clipboard_set_can_store(gtkClipboard, gtkTargets, numTargets);
+ }
+
+ rv = NS_OK;
+ } else {
+ LOGCLIP(" gtk_clipboard_set_with_data() failed!\n");
+ EmptyNativeClipboardData(aWhichClipboard);
+ rv = NS_ERROR_FAILURE;
+ }
+
+ gtk_target_table_free(gtkTargets, numTargets);
+ gtk_target_list_unref(list);
+
+ return rv;
+}
+
+mozilla::Result<int32_t, nsresult>
+nsClipboard::GetNativeClipboardSequenceNumber(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+ return aWhichClipboard == kSelectionClipboard ? mSelectionSequenceNumber
+ : mGlobalSequenceNumber;
+}
+
+static bool IsMIMEAtFlavourList(const nsTArray<nsCString>& aFlavourList,
+ const char* aMime) {
+ for (const auto& flavorStr : aFlavourList) {
+ if (flavorStr.Equals(aMime)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// When clipboard contains only images, X11/Gtk tries to convert them
+// to text when we request text instead of just fail to provide the data.
+// So if clipboard contains images only remove text MIME offer.
+bool nsClipboard::FilterImportedFlavors(int32_t aWhichClipboard,
+ nsTArray<nsCString>& aFlavors) {
+ LOGCLIP("nsClipboard::FilterImportedFlavors");
+
+ auto targets = mContext->GetTargets(aWhichClipboard);
+ if (!targets) {
+ LOGCLIP(" X11: no targes at clipboard (null), quit.\n");
+ return true;
+ }
+
+ for (const auto& atom : targets.AsSpan()) {
+ GUniquePtr<gchar> atom_name(gdk_atom_name(atom));
+ if (!atom_name) {
+ continue;
+ }
+ // Filter out system MIME types.
+ if (strcmp(atom_name.get(), "TARGETS") == 0 ||
+ strcmp(atom_name.get(), "TIMESTAMP") == 0 ||
+ strcmp(atom_name.get(), "SAVE_TARGETS") == 0 ||
+ strcmp(atom_name.get(), "MULTIPLE") == 0) {
+ continue;
+ }
+ // Filter out types which can't be converted to text.
+ if (strncmp(atom_name.get(), "image/", 6) == 0 ||
+ strncmp(atom_name.get(), "application/", 12) == 0 ||
+ strncmp(atom_name.get(), "audio/", 6) == 0 ||
+ strncmp(atom_name.get(), "video/", 6) == 0) {
+ continue;
+ }
+ // We have some other MIME type on clipboard which can be hopefully
+ // converted to text without any problem.
+ LOGCLIP(" X11: text types in clipboard, no need to filter them.\n");
+ return true;
+ }
+
+ // So make sure we offer only types we have at clipboard.
+ nsTArray<nsCString> clipboardFlavors;
+ for (const auto& atom : targets.AsSpan()) {
+ GUniquePtr<gchar> atom_name(gdk_atom_name(atom));
+ if (!atom_name) {
+ continue;
+ }
+ if (IsMIMEAtFlavourList(aFlavors, atom_name.get())) {
+ clipboardFlavors.AppendElement(nsCString(atom_name.get()));
+ }
+ }
+ aFlavors.SwapElements(clipboardFlavors);
+#ifdef MOZ_LOGGING
+ LOGCLIP(" X11: Flavors which match clipboard content:\n");
+ for (uint32_t i = 0; i < aFlavors.Length(); i++) {
+ LOGCLIP(" %s\n", aFlavors[i].get());
+ }
+#endif
+ return true;
+}
+
+static nsresult GetTransferableFlavors(nsITransferable* aTransferable,
+ nsTArray<nsCString>& aFlavors) {
+ if (!aTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+ // Get a list of flavors this transferable can import
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(aFlavors);
+ if (NS_FAILED(rv)) {
+ LOGCLIP(" FlavorsTransferableCanImport falied!\n");
+ return rv;
+ }
+#ifdef MOZ_LOGGING
+ LOGCLIP(" Flavors which can be imported:");
+ for (const auto& flavor : aFlavors) {
+ LOGCLIP(" %s", flavor.get());
+ }
+#endif
+ return NS_OK;
+}
+
+static bool TransferableSetFile(nsITransferable* aTransferable,
+ const nsACString& aURIList) {
+ nsresult rv;
+ nsTArray<nsCString> uris = mozilla::widget::ParseTextURIList(aURIList);
+ if (!uris.IsEmpty()) {
+ nsCOMPtr<nsIURI> fileURI;
+ NS_NewURI(getter_AddRefs(fileURI), uris[0]);
+ if (nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(fileURI, &rv)) {
+ nsCOMPtr<nsIFile> file;
+ rv = fileURL->GetFile(getter_AddRefs(file));
+ if (NS_SUCCEEDED(rv)) {
+ aTransferable->SetTransferData(kFileMime, file);
+ LOGCLIP(" successfully set file to clipboard\n");
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+static bool TransferableSetHTML(nsITransferable* aTransferable,
+ Span<const char> aData) {
+ nsLiteralCString mimeType(kHTMLMime);
+
+ // Convert text/html into our text format
+ nsAutoCString charset;
+ if (!GetHTMLCharset(aData, charset)) {
+ // Fall back to utf-8 in case html/data is missing kHTMLMarkupPrefix.
+ LOGCLIP("Failed to get html/text encoding, fall back to utf-8.\n");
+ charset.AssignLiteral("utf-8");
+ }
+
+ LOGCLIP("TransferableSetHTML: HTML detected charset %s", charset.get());
+ // app which use "text/html" to copy&paste
+ // get the decoder
+ auto encoding = Encoding::ForLabelNoReplacement(charset);
+ if (!encoding) {
+ LOGCLIP("TransferableSetHTML: get unicode decoder error (charset: %s)",
+ charset.get());
+ return false;
+ }
+
+ // According to spec html UTF-16BE/LE should be switched to UTF-8
+ // https://html.spec.whatwg.org/#determining-the-character-encoding:utf-16-encoding-2
+ if (encoding == UTF_16LE_ENCODING || encoding == UTF_16BE_ENCODING) {
+ encoding = UTF_8_ENCODING;
+ }
+
+ // Remove kHTMLMarkupPrefix again, it won't necessarily cause any
+ // issues, but might confuse other users.
+ const size_t prefixLen = ArrayLength(kHTMLMarkupPrefix) - 1;
+ if (aData.Length() >= prefixLen && nsDependentCSubstring(aData.To(prefixLen))
+ .EqualsLiteral(kHTMLMarkupPrefix)) {
+ aData = aData.From(prefixLen);
+ }
+
+ nsAutoString unicodeData;
+ auto [rv, enc] = encoding->Decode(AsBytes(aData), unicodeData);
+#if MOZ_LOGGING
+ if (enc != UTF_8_ENCODING &&
+ MOZ_LOG_TEST(gClipboardLog, mozilla::LogLevel::Debug)) {
+ nsCString decoderName;
+ enc->Name(decoderName);
+ LOGCLIP("TransferableSetHTML: expected UTF-8 decoder but got %s",
+ decoderName.get());
+ }
+#endif
+ if (NS_FAILED(rv)) {
+ LOGCLIP("TransferableSetHTML: failed to decode HTML");
+ return false;
+ }
+ SetTransferableData(aTransferable, mimeType,
+ (const char*)unicodeData.BeginReading(),
+ unicodeData.Length() * sizeof(char16_t));
+ return true;
+}
+
+NS_IMETHODIMP
+nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ LOGCLIP("nsClipboard::GetNativeClipboardData (%s)\n",
+ aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+
+ // TODO: Ensure we don't re-enter here.
+ if (!mContext) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = GetTransferableFlavors(aTransferable, flavors);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Filter out MIME types on X11 to prevent unwanted conversions,
+ // see Bug 1611407
+ if (widget::GdkIsX11Display() &&
+ !FilterImportedFlavors(aWhichClipboard, flavors)) {
+ LOGCLIP(" Missing suitable clipboard data, quit.");
+ return NS_OK;
+ }
+
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ if (flavorStr.EqualsLiteral(kJPEGImageMime) ||
+ flavorStr.EqualsLiteral(kJPGImageMime) ||
+ flavorStr.EqualsLiteral(kPNGImageMime) ||
+ flavorStr.EqualsLiteral(kGIFImageMime)) {
+ // Emulate support for image/jpg
+ if (flavorStr.EqualsLiteral(kJPGImageMime)) {
+ flavorStr.Assign(kJPEGImageMime);
+ }
+
+ LOGCLIP(" Getting image %s MIME clipboard data\n", flavorStr.get());
+
+ auto clipboardData =
+ mContext->GetClipboardData(flavorStr.get(), aWhichClipboard);
+ if (!clipboardData) {
+ LOGCLIP(" %s type is missing\n", flavorStr.get());
+ continue;
+ }
+
+ nsCOMPtr<nsIInputStream> byteStream;
+ NS_NewByteInputStream(getter_AddRefs(byteStream), clipboardData.AsSpan(),
+ NS_ASSIGNMENT_COPY);
+ aTransferable->SetTransferData(flavorStr.get(), byteStream);
+ LOGCLIP(" got %s MIME data\n", flavorStr.get());
+ return NS_OK;
+ }
+
+ // Special case text/plain since we can convert any
+ // string into text/plain
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ LOGCLIP(" Getting text %s MIME clipboard data\n", flavorStr.get());
+
+ auto clipboardData = mContext->GetClipboardText(aWhichClipboard);
+ if (!clipboardData) {
+ LOGCLIP(" failed to get text data\n");
+ // If the type was text/plain and we couldn't get
+ // text off the clipboard, run the next loop
+ // iteration.
+ continue;
+ }
+
+ // Convert utf-8 into our text format.
+ NS_ConvertUTF8toUTF16 ucs2string(clipboardData.get());
+ SetTransferableData(aTransferable, flavorStr,
+ (const char*)ucs2string.BeginReading(),
+ ucs2string.Length() * 2);
+
+ LOGCLIP(" got text data, length %zd\n", ucs2string.Length());
+ return NS_OK;
+ }
+
+ if (flavorStr.EqualsLiteral(kFileMime)) {
+ LOGCLIP(" Getting %s file clipboard data\n", flavorStr.get());
+
+ auto clipboardData =
+ mContext->GetClipboardData(kURIListMime, aWhichClipboard);
+ if (!clipboardData) {
+ LOGCLIP(" text/uri-list type is missing\n");
+ continue;
+ }
+
+ nsDependentCSubstring fileName(clipboardData.AsSpan());
+ if (!TransferableSetFile(aTransferable, fileName)) {
+ continue;
+ }
+ return NS_OK;
+ }
+
+ LOGCLIP(" Getting %s MIME clipboard data\n", flavorStr.get());
+
+ auto clipboardData =
+ mContext->GetClipboardData(flavorStr.get(), aWhichClipboard);
+
+#ifdef MOZ_LOGGING
+ if (!clipboardData) {
+ LOGCLIP(" %s type is missing\n", flavorStr.get());
+ }
+#endif
+
+ if (clipboardData) {
+ LOGCLIP(" got %s mime type data.\n", flavorStr.get());
+
+ // Special case text/html since we can convert into UCS2
+ if (flavorStr.EqualsLiteral(kHTMLMime)) {
+ if (!TransferableSetHTML(aTransferable, clipboardData.AsSpan())) {
+ continue;
+ }
+ } else {
+ auto span = clipboardData.AsSpan();
+ SetTransferableData(aTransferable, flavorStr, span.data(),
+ span.Length());
+ }
+ return NS_OK;
+ }
+ }
+
+ LOGCLIP(" failed to get clipboard content.\n");
+ return NS_OK;
+}
+
+enum DataType {
+ DATATYPE_IMAGE,
+ DATATYPE_FILE,
+ DATATYPE_HTML,
+ DATATYPE_RAW,
+};
+
+struct DataCallbackHandler {
+ RefPtr<nsITransferable> mTransferable;
+ nsBaseClipboard::GetDataCallback mDataCallback;
+ nsCString mMimeType;
+ DataType mDataType;
+
+ explicit DataCallbackHandler(RefPtr<nsITransferable> aTransferable,
+ nsBaseClipboard::GetDataCallback&& aDataCallback,
+ const char* aMimeType,
+ DataType aDataType = DATATYPE_RAW)
+ : mTransferable(std::move(aTransferable)),
+ mDataCallback(std::move(aDataCallback)),
+ mMimeType(aMimeType),
+ mDataType(aDataType) {
+ MOZ_COUNT_CTOR(DataCallbackHandler);
+ LOGCLIP("DataCallbackHandler created [%p] MIME %s type %d", this,
+ mMimeType.get(), mDataType);
+ }
+ ~DataCallbackHandler() {
+ LOGCLIP("DataCallbackHandler deleted [%p]", this);
+ MOZ_COUNT_DTOR(DataCallbackHandler);
+ }
+};
+
+static void AsyncGetTextImpl(nsITransferable* aTransferable,
+ int32_t aWhichClipboard,
+ nsBaseClipboard::GetDataCallback&& aCallback) {
+ LOGCLIP("AsyncGetText() type '%s'",
+ aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+
+ gtk_clipboard_request_text(
+ gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)),
+ [](GtkClipboard* aClipboard, const gchar* aText, gpointer aData) -> void {
+ UniquePtr<DataCallbackHandler> ref(
+ static_cast<DataCallbackHandler*>(aData));
+ LOGCLIP("AsyncGetText async handler of [%p]", aData);
+
+ size_t dataLength = aText ? strlen(aText) : 0;
+ if (dataLength <= 0) {
+ LOGCLIP(" quit, text is not available");
+ ref->mDataCallback(NS_OK);
+ return;
+ }
+
+ // Convert utf-8 into our unicode format.
+ NS_ConvertUTF8toUTF16 utf16string(aText, dataLength);
+ nsLiteralCString flavor(kTextMime);
+ SetTransferableData(ref->mTransferable, flavor,
+ (const char*)utf16string.BeginReading(),
+ utf16string.Length() * 2);
+ LOGCLIP(" text is set, length = %d", (int)dataLength);
+ ref->mDataCallback(NS_OK);
+ },
+ new DataCallbackHandler(aTransferable, std::move(aCallback), kTextMime));
+}
+
+static void AsyncGetDataImpl(nsITransferable* aTransferable,
+ int32_t aWhichClipboard, const char* aMimeType,
+ DataType aDataType,
+ nsBaseClipboard::GetDataCallback&& aCallback) {
+ LOGCLIP("AsyncGetData() type '%s'",
+ aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+
+ const char* gtkMIMEType = nullptr;
+ switch (aDataType) {
+ case DATATYPE_FILE:
+ // Don't ask Gtk for application/x-moz-file
+ gtkMIMEType = kURIListMime;
+ break;
+ case DATATYPE_IMAGE:
+ case DATATYPE_HTML:
+ case DATATYPE_RAW:
+ gtkMIMEType = aMimeType;
+ break;
+ }
+
+ gtk_clipboard_request_contents(
+ gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)),
+ gdk_atom_intern(gtkMIMEType, FALSE),
+ [](GtkClipboard* aClipboard, GtkSelectionData* aSelection,
+ gpointer aData) -> void {
+ UniquePtr<DataCallbackHandler> ref(
+ static_cast<DataCallbackHandler*>(aData));
+ LOGCLIP("AsyncGetData async handler [%p] MIME %s type %d", aData,
+ ref->mMimeType.get(), ref->mDataType);
+
+ int dataLength = gtk_selection_data_get_length(aSelection);
+ if (dataLength <= 0) {
+ ref->mDataCallback(NS_OK);
+ return;
+ }
+ const char* data = (const char*)gtk_selection_data_get_data(aSelection);
+ if (!data) {
+ ref->mDataCallback(NS_OK);
+ return;
+ }
+ switch (ref->mDataType) {
+ case DATATYPE_IMAGE: {
+ LOGCLIP(" set image clipboard data");
+ nsCOMPtr<nsIInputStream> byteStream;
+ NS_NewByteInputStream(getter_AddRefs(byteStream),
+ Span(data, dataLength), NS_ASSIGNMENT_COPY);
+ ref->mTransferable->SetTransferData(ref->mMimeType.get(),
+ byteStream);
+ break;
+ }
+ case DATATYPE_FILE: {
+ LOGCLIP(" set file clipboard data");
+ nsDependentCSubstring file(data, dataLength);
+ TransferableSetFile(ref->mTransferable, file);
+ break;
+ }
+ case DATATYPE_HTML: {
+ LOGCLIP(" html clipboard data");
+ Span dataSpan(data, dataLength);
+ TransferableSetHTML(ref->mTransferable, dataSpan);
+ break;
+ }
+ case DATATYPE_RAW: {
+ LOGCLIP(" raw clipboard data %s", ref->mMimeType.get());
+ SetTransferableData(ref->mTransferable, ref->mMimeType, data,
+ dataLength);
+ break;
+ }
+ }
+ ref->mDataCallback(NS_OK);
+ },
+ new DataCallbackHandler(aTransferable, std::move(aCallback), aMimeType,
+ aDataType));
+}
+
+static void AsyncGetDataFlavor(nsITransferable* aTransferable,
+ int32_t aWhichClipboard, nsCString& aFlavorStr,
+ nsBaseClipboard::GetDataCallback&& aCallback) {
+ if (aFlavorStr.EqualsLiteral(kJPEGImageMime) ||
+ aFlavorStr.EqualsLiteral(kJPGImageMime) ||
+ aFlavorStr.EqualsLiteral(kPNGImageMime) ||
+ aFlavorStr.EqualsLiteral(kGIFImageMime)) {
+ // Emulate support for image/jpg
+ if (aFlavorStr.EqualsLiteral(kJPGImageMime)) {
+ aFlavorStr.Assign(kJPEGImageMime);
+ }
+ LOGCLIP(" Getting image %s MIME clipboard data", aFlavorStr.get());
+ AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(),
+ DATATYPE_IMAGE, std::move(aCallback));
+ return;
+ }
+ // Special case text/plain since we can convert any
+ // string into text/plain
+ if (aFlavorStr.EqualsLiteral(kTextMime)) {
+ LOGCLIP(" Getting unicode clipboard data");
+ AsyncGetTextImpl(aTransferable, aWhichClipboard, std::move(aCallback));
+ return;
+ }
+ if (aFlavorStr.EqualsLiteral(kFileMime)) {
+ LOGCLIP(" Getting file clipboard data\n");
+ AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(),
+ DATATYPE_FILE, std::move(aCallback));
+ return;
+ }
+ if (aFlavorStr.EqualsLiteral(kHTMLMime)) {
+ LOGCLIP(" Getting HTML clipboard data");
+ AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(),
+ DATATYPE_HTML, std::move(aCallback));
+ return;
+ }
+ LOGCLIP(" Getting raw %s MIME clipboard data\n", aFlavorStr.get());
+ AsyncGetDataImpl(aTransferable, aWhichClipboard, aFlavorStr.get(),
+ DATATYPE_RAW, std::move(aCallback));
+}
+
+void nsClipboard::AsyncGetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard,
+ GetDataCallback&& aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ LOGCLIP("nsClipboard::AsyncGetNativeClipboardData (%s)",
+ aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+ nsTArray<nsCString> importedFlavors;
+ nsresult rv = GetTransferableFlavors(aTransferable, importedFlavors);
+ if (NS_FAILED(rv)) {
+ aCallback(rv);
+ return;
+ }
+
+ auto flavorsNum = importedFlavors.Length();
+ if (!flavorsNum) {
+ aCallback(NS_OK);
+ return;
+ }
+#ifdef MOZ_LOGGING
+ if (flavorsNum > 1) {
+ LOGCLIP(" Only first MIME type (%s) will be imported from clipboard!",
+ importedFlavors[0].get());
+ }
+#endif
+
+ // Filter out MIME types on X11 to prevent unwanted conversions,
+ // see Bug 1611407
+ if (widget::GdkIsX11Display()) {
+ AsyncHasNativeClipboardDataMatchingFlavors(
+ importedFlavors, aWhichClipboard,
+ [aWhichClipboard, transferable = nsCOMPtr{aTransferable},
+ callback = std::move(aCallback)](auto aResultOrError) mutable {
+ if (aResultOrError.isErr()) {
+ callback(aResultOrError.unwrapErr());
+ return;
+ }
+
+ nsTArray<nsCString> clipboardFlavors =
+ std::move(aResultOrError.unwrap());
+ if (!clipboardFlavors.Length()) {
+ LOGCLIP(" no flavors in clipboard, quit.");
+ callback(NS_OK);
+ return;
+ }
+
+ AsyncGetDataFlavor(transferable, aWhichClipboard, clipboardFlavors[0],
+ std::move(callback));
+ });
+ return;
+ }
+
+ // Read clipboard directly on Wayland
+ AsyncGetDataFlavor(aTransferable, aWhichClipboard, importedFlavors[0],
+ std::move(aCallback));
+}
+
+nsresult nsClipboard::EmptyNativeClipboardData(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ LOGCLIP("nsClipboard::EmptyNativeClipboardData (%s)\n",
+ aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+ if (aWhichClipboard == kSelectionClipboard) {
+ if (mSelectionTransferable) {
+ gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_PRIMARY));
+ MOZ_ASSERT(!mSelectionTransferable);
+ }
+ } else {
+ if (mGlobalTransferable) {
+ gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD));
+ MOZ_ASSERT(!mGlobalTransferable);
+ }
+ }
+ ClearCachedTargets(aWhichClipboard);
+ return NS_OK;
+}
+
+void nsClipboard::ClearTransferable(int32_t aWhichClipboard) {
+ if (aWhichClipboard == kSelectionClipboard) {
+ mSelectionSequenceNumber++;
+ mSelectionTransferable = nullptr;
+ } else {
+ mGlobalSequenceNumber++;
+ mGlobalTransferable = nullptr;
+ }
+}
+
+static bool FlavorMatchesTarget(const nsACString& aFlavor, GdkAtom aTarget) {
+ GUniquePtr<gchar> atom_name(gdk_atom_name(aTarget));
+ if (!atom_name) {
+ return false;
+ }
+ if (aFlavor.Equals(atom_name.get())) {
+ LOGCLIP(" has %s\n", atom_name.get());
+ return true;
+ }
+ // X clipboard supports image/jpeg, but we want to emulate support
+ // for image/jpg as well
+ if (aFlavor.EqualsLiteral(kJPGImageMime) &&
+ !strcmp(atom_name.get(), kJPEGImageMime)) {
+ LOGCLIP(" has image/jpg\n");
+ return true;
+ }
+ // application/x-moz-file should be treated like text/uri-list
+ if (aFlavor.EqualsLiteral(kFileMime) &&
+ !strcmp(atom_name.get(), kURIListMime)) {
+ LOGCLIP(" has text/uri-list treating as application/x-moz-file");
+ return true;
+ }
+ return false;
+}
+
+mozilla::Result<bool, nsresult>
+nsClipboard::HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ LOGCLIP("nsClipboard::HasNativeClipboardDataMatchingFlavors (%s)\n",
+ aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+
+ if (!mContext) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto targets = mContext->GetTargets(aWhichClipboard);
+ if (!targets) {
+ LOGCLIP(" no targes at clipboard (null)\n");
+ return false;
+ }
+
+#ifdef MOZ_LOGGING
+ if (LOGCLIP_ENABLED()) {
+ LOGCLIP(" Asking for content:\n");
+ for (auto& flavor : aFlavorList) {
+ LOGCLIP(" MIME %s\n", flavor.get());
+ }
+ LOGCLIP(" Clipboard content (target nums %zu):\n",
+ targets.AsSpan().Length());
+ for (const auto& target : targets.AsSpan()) {
+ GUniquePtr<gchar> atom_name(gdk_atom_name(target));
+ if (!atom_name) {
+ LOGCLIP(" failed to get MIME\n");
+ continue;
+ }
+ LOGCLIP(" MIME %s\n", atom_name.get());
+ }
+ }
+#endif
+
+ // Walk through the provided types and try to match it to a
+ // provided type.
+ for (auto& flavor : aFlavorList) {
+ // We special case text/plain here.
+ if (flavor.EqualsLiteral(kTextMime) &&
+ gtk_targets_include_text(targets.AsSpan().data(),
+ targets.AsSpan().Length())) {
+ LOGCLIP(" has kTextMime\n");
+ return true;
+ }
+ for (const auto& target : targets.AsSpan()) {
+ if (FlavorMatchesTarget(flavor, target)) {
+ return true;
+ }
+ }
+ }
+
+ LOGCLIP(" no targes at clipboard (bad match)\n");
+ return false;
+}
+
+struct TragetCallbackHandler {
+ TragetCallbackHandler(const nsTArray<nsCString>& aAcceptedFlavorList,
+ nsBaseClipboard::HasMatchingFlavorsCallback&& aCallback)
+ : mAcceptedFlavorList(aAcceptedFlavorList.Clone()),
+ mCallback(std::move(aCallback)) {
+ LOGCLIP("TragetCallbackHandler(%p) created", this);
+ }
+ ~TragetCallbackHandler() {
+ LOGCLIP("TragetCallbackHandler(%p) deleted", this);
+ }
+ nsTArray<nsCString> mAcceptedFlavorList;
+ nsBaseClipboard::HasMatchingFlavorsCallback mCallback;
+};
+
+void nsClipboard::AsyncHasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ nsBaseClipboard::HasMatchingFlavorsCallback&& aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ LOGCLIP("nsClipboard::AsyncHasNativeClipboardDataMatchingFlavors (%s)",
+ aWhichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+
+ gtk_clipboard_request_contents(
+ gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)),
+ gdk_atom_intern("TARGETS", FALSE),
+ [](GtkClipboard* aClipboard, GtkSelectionData* aSelection,
+ gpointer aData) -> void {
+ LOGCLIP("gtk_clipboard_request_contents async handler (%p)", aData);
+ UniquePtr<TragetCallbackHandler> handler(
+ static_cast<TragetCallbackHandler*>(aData));
+
+ GdkAtom* targets = nullptr;
+ gint targetsNum = 0;
+ if (gtk_selection_data_get_length(aSelection) > 0) {
+ gtk_selection_data_get_targets(aSelection, &targets, &targetsNum);
+ }
+ nsTArray<nsCString> results;
+ if (targetsNum) {
+ for (auto& flavor : handler->mAcceptedFlavorList) {
+ LOGCLIP(" looking for %s", flavor.get());
+ if (flavor.EqualsLiteral(kTextMime) &&
+ gtk_targets_include_text(targets, targetsNum)) {
+ results.AppendElement(flavor);
+ LOGCLIP(" has kTextMime\n");
+ continue;
+ }
+ for (int i = 0; i < targetsNum; i++) {
+ if (FlavorMatchesTarget(flavor, targets[i])) {
+ results.AppendElement(flavor);
+ }
+ }
+ }
+ }
+ handler->mCallback(std::move(results));
+ },
+ new TragetCallbackHandler(aFlavorList, std::move(aCallback)));
+}
+
+nsITransferable* nsClipboard::GetTransferable(int32_t aWhichClipboard) {
+ nsITransferable* retval;
+
+ if (aWhichClipboard == kSelectionClipboard)
+ retval = mSelectionTransferable.get();
+ else
+ retval = mGlobalTransferable.get();
+
+ return retval;
+}
+
+void nsClipboard::SelectionGetEvent(GtkClipboard* aClipboard,
+ GtkSelectionData* aSelectionData) {
+ // Someone has asked us to hand them something. The first thing
+ // that we want to do is see if that something includes text. If
+ // it does, try to give it text/plain after converting it to
+ // utf-8.
+
+ int32_t whichClipboard;
+
+ // which clipboard?
+ GdkAtom selection = gtk_selection_data_get_selection(aSelectionData);
+ if (selection == GDK_SELECTION_PRIMARY)
+ whichClipboard = kSelectionClipboard;
+ else if (selection == GDK_SELECTION_CLIPBOARD)
+ whichClipboard = kGlobalClipboard;
+ else
+ return; // THAT AIN'T NO CLIPBOARD I EVER HEARD OF
+
+ LOGCLIP("nsClipboard::SelectionGetEvent (%s)\n",
+ whichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+
+ nsCOMPtr<nsITransferable> trans = GetTransferable(whichClipboard);
+ if (!trans) {
+ // We have nothing to serve
+ LOGCLIP("nsClipboard::SelectionGetEvent() - %s clipboard is empty!\n",
+ whichClipboard == kSelectionClipboard ? "Primary" : "Clipboard");
+ return;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsISupports> item;
+
+ GdkAtom selectionTarget = gtk_selection_data_get_target(aSelectionData);
+ LOGCLIP(" selection target %s\n",
+ GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get());
+
+ // Check to see if the selection data is some text type.
+ if (gtk_targets_include_text(&selectionTarget, 1)) {
+ LOGCLIP(" providing text/plain data\n");
+ // Try to convert our internal type into a text string. Get
+ // the transferable for this clipboard and try to get the
+ // text/plain type for it.
+ rv = trans->GetTransferData("text/plain", getter_AddRefs(item));
+ if (NS_FAILED(rv) || !item) {
+ LOGCLIP(" GetTransferData() failed to get text/plain!\n");
+ return;
+ }
+
+ nsCOMPtr<nsISupportsString> wideString;
+ wideString = do_QueryInterface(item);
+ if (!wideString) return;
+
+ nsAutoString ucs2string;
+ wideString->GetData(ucs2string);
+ NS_ConvertUTF16toUTF8 utf8string(ucs2string);
+
+ LOGCLIP(" sent %zd bytes of utf-8 data\n", utf8string.Length());
+ if (selectionTarget == gdk_atom_intern("text/plain;charset=utf-8", FALSE)) {
+ LOGCLIP(
+ " using gtk_selection_data_set for 'text/plain;charset=utf-8'\n");
+ // Bypass gtk_selection_data_set_text, which will convert \n to \r\n
+ // in some versions of GTK.
+ gtk_selection_data_set(aSelectionData, selectionTarget, 8,
+ reinterpret_cast<const guchar*>(utf8string.get()),
+ utf8string.Length());
+ } else {
+ gtk_selection_data_set_text(aSelectionData, utf8string.get(),
+ utf8string.Length());
+ }
+ return;
+ }
+
+ // Check to see if the selection data is an image type
+ if (gtk_targets_include_image(&selectionTarget, 1, TRUE)) {
+ LOGCLIP(" providing image data\n");
+ // Look through our transfer data for the image
+ static const char* const imageMimeTypes[] = {kNativeImageMime,
+ kPNGImageMime, kJPEGImageMime,
+ kJPGImageMime, kGIFImageMime};
+ nsCOMPtr<nsISupports> imageItem;
+ nsCOMPtr<imgIContainer> image;
+ for (uint32_t i = 0; i < ArrayLength(imageMimeTypes); i++) {
+ rv = trans->GetTransferData(imageMimeTypes[i], getter_AddRefs(imageItem));
+ if (NS_FAILED(rv)) {
+ LOGCLIP(" %s is missing at GetTransferData()\n", imageMimeTypes[i]);
+ continue;
+ }
+
+ image = do_QueryInterface(imageItem);
+ if (image) {
+ LOGCLIP(" %s is available at GetTransferData()\n",
+ imageMimeTypes[i]);
+ break;
+ }
+ }
+
+ if (!image) { // Not getting an image for an image mime type!?
+ LOGCLIP(" Failed to get any image mime from GetTransferData()!\n");
+ return;
+ }
+
+ RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(image);
+ if (!pixbuf) {
+ LOGCLIP(" nsImageToPixbuf::ImageToPixbuf() failed!\n");
+ return;
+ }
+
+ LOGCLIP(" Setting pixbuf image data as %s\n",
+ GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get());
+ gtk_selection_data_set_pixbuf(aSelectionData, pixbuf);
+ return;
+ }
+
+ if (selectionTarget == gdk_atom_intern(kHTMLMime, FALSE)) {
+ LOGCLIP(" providing %s data\n", kHTMLMime);
+ rv = trans->GetTransferData(kHTMLMime, getter_AddRefs(item));
+ if (NS_FAILED(rv) || !item) {
+ LOGCLIP(" failed to get %s data by GetTransferData()!\n", kHTMLMime);
+ return;
+ }
+
+ nsCOMPtr<nsISupportsString> wideString;
+ wideString = do_QueryInterface(item);
+ if (!wideString) {
+ LOGCLIP(" failed to get wideString interface!");
+ return;
+ }
+
+ nsAutoString ucs2string;
+ wideString->GetData(ucs2string);
+
+ nsAutoCString html;
+ // Add the prefix so the encoding is correctly detected.
+ html.AppendLiteral(kHTMLMarkupPrefix);
+ AppendUTF16toUTF8(ucs2string, html);
+
+ LOGCLIP(" Setting %zd bytes of %s data\n", html.Length(),
+ GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get());
+ gtk_selection_data_set(aSelectionData, selectionTarget, 8,
+ (const guchar*)html.get(), html.Length());
+ return;
+ }
+
+ // We put kFileMime onto the clipboard as kURIListMime.
+ if (selectionTarget == gdk_atom_intern(kURIListMime, FALSE)) {
+ LOGCLIP(" providing %s data\n", kURIListMime);
+ rv = trans->GetTransferData(kFileMime, getter_AddRefs(item));
+ if (NS_FAILED(rv) || !item) {
+ LOGCLIP(" failed to get %s data by GetTransferData()!\n", kFileMime);
+ return;
+ }
+
+ nsCOMPtr<nsIFile> file = do_QueryInterface(item);
+ if (!file) {
+ LOGCLIP(" failed to get nsIFile interface!");
+ return;
+ }
+
+ nsCOMPtr<nsIURI> fileURI;
+ rv = NS_NewFileURI(getter_AddRefs(fileURI), file);
+ if (NS_FAILED(rv)) {
+ LOGCLIP(" failed to get fileURI\n");
+ return;
+ }
+
+ nsAutoCString uri;
+ if (NS_FAILED(fileURI->GetSpec(uri))) {
+ LOGCLIP(" failed to get fileURI spec\n");
+ return;
+ }
+
+ LOGCLIP(" Setting %zd bytes of data\n", uri.Length());
+ gtk_selection_data_set(aSelectionData, selectionTarget, 8,
+ (const guchar*)uri.get(), uri.Length());
+ return;
+ }
+
+ LOGCLIP(" Try if we have anything at GetTransferData() for %s\n",
+ GUniquePtr<gchar>(gdk_atom_name(selectionTarget)).get());
+
+ // Try to match up the selection data target to something our
+ // transferable provides.
+ GUniquePtr<gchar> target_name(gdk_atom_name(selectionTarget));
+ if (!target_name) {
+ LOGCLIP(" Failed to get target name!\n");
+ return;
+ }
+
+ rv = trans->GetTransferData(target_name.get(), getter_AddRefs(item));
+ // nothing found?
+ if (NS_FAILED(rv) || !item) {
+ LOGCLIP(" Failed to get anything from GetTransferData()!\n");
+ return;
+ }
+
+ void* primitive_data = nullptr;
+ uint32_t dataLen = 0;
+ nsPrimitiveHelpers::CreateDataFromPrimitive(
+ nsDependentCString(target_name.get()), item, &primitive_data, &dataLen);
+ if (!primitive_data) {
+ LOGCLIP(" Failed to get primitive data!\n");
+ return;
+ }
+
+ LOGCLIP(" Setting %s as a primitive data type, %d bytes\n",
+ target_name.get(), dataLen);
+ gtk_selection_data_set(aSelectionData, selectionTarget,
+ 8, /* 8 bits in a unit */
+ (const guchar*)primitive_data, dataLen);
+ free(primitive_data);
+}
+
+void nsClipboard::ClearCachedTargets(int32_t aWhichClipboard) {
+ if (aWhichClipboard == kSelectionClipboard) {
+ nsRetrievalContext::ClearCachedTargetsPrimary(nullptr, nullptr, nullptr);
+ } else {
+ nsRetrievalContext::ClearCachedTargetsClipboard(nullptr, nullptr, nullptr);
+ }
+}
+
+void nsClipboard::SelectionClearEvent(GtkClipboard* aGtkClipboard) {
+ int32_t whichClipboard = GetGeckoClipboardType(aGtkClipboard);
+ if (whichClipboard < 0) {
+ return;
+ }
+ LOGCLIP("nsClipboard::SelectionClearEvent (%s)\n",
+ whichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+ ClearCachedTargets(whichClipboard);
+ ClearTransferable(whichClipboard);
+ ClearClipboardCache(whichClipboard);
+}
+
+void nsClipboard::OwnerChangedEvent(GtkClipboard* aGtkClipboard,
+ GdkEventOwnerChange* aEvent) {
+ int32_t whichClipboard = GetGeckoClipboardType(aGtkClipboard);
+ if (whichClipboard < 0) {
+ return;
+ }
+ LOGCLIP("nsClipboard::OwnerChangedEvent (%s)\n",
+ whichClipboard == kSelectionClipboard ? "primary" : "clipboard");
+ GtkWidget* gtkWidget = [aEvent]() -> GtkWidget* {
+ if (!aEvent->owner) {
+ return nullptr;
+ }
+ gpointer user_data = nullptr;
+ gdk_window_get_user_data(aEvent->owner, &user_data);
+ return GTK_WIDGET(user_data);
+ }();
+ // If we can get GtkWidget from the current clipboard owner, this
+ // owner-changed event must be triggered by ourself via calling
+ // gtk_clipboard_set_with_data, the sequence number should already be handled.
+ if (!gtkWidget) {
+ if (whichClipboard == kSelectionClipboard) {
+ mSelectionSequenceNumber++;
+ } else {
+ mGlobalSequenceNumber++;
+ }
+ }
+
+ ClearCachedTargets(whichClipboard);
+}
+
+void clipboard_get_cb(GtkClipboard* aGtkClipboard,
+ GtkSelectionData* aSelectionData, guint info,
+ gpointer user_data) {
+ LOGCLIP("clipboard_get_cb() callback\n");
+ nsClipboard* clipboard = static_cast<nsClipboard*>(user_data);
+ clipboard->SelectionGetEvent(aGtkClipboard, aSelectionData);
+}
+
+void clipboard_clear_cb(GtkClipboard* aGtkClipboard, gpointer user_data) {
+ LOGCLIP("clipboard_clear_cb() callback\n");
+ nsClipboard* clipboard = static_cast<nsClipboard*>(user_data);
+ clipboard->SelectionClearEvent(aGtkClipboard);
+}
+
+void clipboard_owner_change_cb(GtkClipboard* aGtkClipboard,
+ GdkEventOwnerChange* aEvent,
+ gpointer aUserData) {
+ LOGCLIP("clipboard_owner_change_cb() callback\n");
+ nsClipboard* clipboard = static_cast<nsClipboard*>(aUserData);
+ clipboard->OwnerChangedEvent(aGtkClipboard, aEvent);
+}
+
+/*
+ * This function extracts the encoding label from the subset of HTML internal
+ * encoding declaration syntax that uses the old long form with double quotes
+ * and without spaces around the equals sign between the "content" attribute
+ * name and the attribute value.
+ *
+ * This was added for the sake of an ancient version of StarOffice
+ * in the pre-UTF-8 era in bug 123389. It is unclear if supporting
+ * non-UTF-8 encodings is still necessary and if this function
+ * still needs to exist.
+ *
+ * As of December 2022, both Gecko and LibreOffice emit an UTF-8
+ * declaration that this function successfully extracts "UTF-8" from,
+ * but that's also the default that we fall back on if this function
+ * fails to extract a label.
+ */
+bool GetHTMLCharset(Span<const char> aData, nsCString& aFoundCharset) {
+ // Assume ASCII first to find "charset" info
+ const nsDependentCSubstring htmlStr(aData);
+ nsACString::const_iterator start, end;
+ htmlStr.BeginReading(start);
+ htmlStr.EndReading(end);
+ nsACString::const_iterator valueStart(start), valueEnd(start);
+
+ if (CaseInsensitiveFindInReadable("CONTENT=\"text/html;"_ns, start, end)) {
+ start = end;
+ htmlStr.EndReading(end);
+
+ if (CaseInsensitiveFindInReadable("charset="_ns, start, end)) {
+ valueStart = end;
+ start = end;
+ htmlStr.EndReading(end);
+
+ if (FindCharInReadable('"', start, end)) valueEnd = start;
+ }
+ }
+ // find "charset" in HTML
+ if (valueStart != valueEnd) {
+ aFoundCharset = Substring(valueStart, valueEnd);
+ ToUpperCase(aFoundCharset);
+ return true;
+ }
+ return false;
+}
diff --git a/widget/gtk/nsClipboard.h b/widget/gtk/nsClipboard.h
new file mode 100644
index 0000000000..0ddbb59827
--- /dev/null
+++ b/widget/gtk/nsClipboard.h
@@ -0,0 +1,177 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsClipboard_h_
+#define __nsClipboard_h_
+
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Span.h"
+#include "nsBaseClipboard.h"
+#include "nsIClipboard.h"
+#include "nsIObserver.h"
+#include "nsCOMPtr.h"
+#include "GUniquePtr.h"
+#include <gtk/gtk.h>
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gClipboardLog;
+# define LOGCLIP(...) \
+ MOZ_LOG(gClipboardLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define LOGCLIP_ENABLED() \
+ MOZ_LOG_TEST(gClipboardLog, mozilla::LogLevel::Debug)
+#else
+# define LOGCLIP(...)
+# define LOGCLIP_ENABLED() false
+#endif /* MOZ_LOGGING */
+
+class ClipboardTargets {
+ friend class ClipboardData;
+
+ mozilla::GUniquePtr<GdkAtom> mTargets;
+ uint32_t mCount = 0;
+
+ public:
+ ClipboardTargets() = default;
+ ClipboardTargets(mozilla::GUniquePtr<GdkAtom> aTargets, uint32_t aCount)
+ : mTargets(std::move(aTargets)), mCount(aCount) {}
+
+ void Set(ClipboardTargets);
+ ClipboardTargets Clone();
+ void Clear() {
+ mTargets = nullptr;
+ mCount = 0;
+ };
+
+ mozilla::Span<GdkAtom> AsSpan() const { return {mTargets.get(), mCount}; }
+ explicit operator bool() const { return bool(mTargets); }
+};
+
+class ClipboardData {
+ mozilla::GUniquePtr<char> mData;
+ uint32_t mLength = 0;
+
+ public:
+ ClipboardData() = default;
+
+ void SetData(mozilla::Span<const uint8_t>);
+ void SetText(mozilla::Span<const char>);
+ void SetTargets(ClipboardTargets);
+
+ ClipboardTargets ExtractTargets();
+ mozilla::GUniquePtr<char> ExtractText() {
+ mLength = 0;
+ return std::move(mData);
+ }
+
+ mozilla::Span<char> AsSpan() const { return {mData.get(), mLength}; }
+ explicit operator bool() const { return bool(mData); }
+};
+
+enum class ClipboardDataType { Data, Text, Targets };
+
+class nsRetrievalContext {
+ public:
+ // We intentionally use unsafe thread refcount as clipboard is used in
+ // main thread only.
+ NS_INLINE_DECL_REFCOUNTING(nsRetrievalContext)
+
+ // Get actual clipboard content (GetClipboardData/GetClipboardText).
+ virtual ClipboardData GetClipboardData(const char* aMimeType,
+ int32_t aWhichClipboard) = 0;
+ virtual mozilla::GUniquePtr<char> GetClipboardText(
+ int32_t aWhichClipboard) = 0;
+
+ // Get data mime types which can be obtained from clipboard.
+ ClipboardTargets GetTargets(int32_t aWhichClipboard);
+
+ // Clipboard/Primary selection owner changed. Clear internal cached data.
+ static void ClearCachedTargetsClipboard(GtkClipboard* aClipboard,
+ GdkEvent* aEvent, gpointer data);
+ static void ClearCachedTargetsPrimary(GtkClipboard* aClipboard,
+ GdkEvent* aEvent, gpointer data);
+
+ nsRetrievalContext() = default;
+
+ protected:
+ virtual ClipboardTargets GetTargetsImpl(int32_t aWhichClipboard) = 0;
+ virtual ~nsRetrievalContext();
+
+ static ClipboardTargets sClipboardTargets;
+ static ClipboardTargets sPrimaryTargets;
+};
+
+class nsClipboard : public nsBaseClipboard, public nsIObserver {
+ public:
+ nsClipboard();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIOBSERVER
+
+ // Make sure we are initialized, called from the factory
+ // constructor
+ nsresult Init(void);
+
+ // Someone requested the selection
+ void SelectionGetEvent(GtkClipboard* aGtkClipboard,
+ GtkSelectionData* aSelectionData);
+ void SelectionClearEvent(GtkClipboard* aGtkClipboard);
+
+ // Clipboard owner changed
+ void OwnerChangedEvent(GtkClipboard* aGtkClipboard,
+ GdkEventOwnerChange* aEvent);
+
+ protected:
+ // Implement the native clipboard behavior.
+ NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ void AsyncGetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard,
+ GetDataCallback&& aCallback) override;
+ nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) override;
+ mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber(
+ int32_t aWhichClipboard) override;
+ mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override;
+ void AsyncHasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ HasMatchingFlavorsCallback&& aCallback) override;
+
+ private:
+ virtual ~nsClipboard();
+
+ // Get our hands on the correct transferable, given a specific
+ // clipboard
+ nsITransferable* GetTransferable(int32_t aWhichClipboard);
+
+ void ClearTransferable(int32_t aWhichClipboard);
+ void ClearCachedTargets(int32_t aWhichClipboard);
+
+ bool FilterImportedFlavors(int32_t aWhichClipboard,
+ nsTArray<nsCString>& aFlavors);
+
+ // Hang on to our transferables so we can transfer data when asked.
+ nsCOMPtr<nsITransferable> mSelectionTransferable;
+ nsCOMPtr<nsITransferable> mGlobalTransferable;
+ RefPtr<nsRetrievalContext> mContext;
+
+ // Sequence number of the system clipboard data.
+ int32_t mSelectionSequenceNumber = 0;
+ int32_t mGlobalSequenceNumber = 0;
+};
+
+extern const int kClipboardTimeout;
+extern const int kClipboardFastIterationNum;
+
+GdkAtom GetSelectionAtom(int32_t aWhichClipboard);
+int GetGeckoClipboardType(GtkClipboard* aGtkClipboard);
+
+#endif /* __nsClipboard_h_ */
diff --git a/widget/gtk/nsClipboardWayland.cpp b/widget/gtk/nsClipboardWayland.cpp
new file mode 100644
index 0000000000..d179f242dd
--- /dev/null
+++ b/widget/gtk/nsClipboardWayland.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "nsClipboardWayland.h"
+
+#include "AsyncGtkClipboardRequest.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/ScopeExit.h"
+#include "prtime.h"
+
+#include <gtk/gtk.h>
+#include <stdlib.h>
+#include <string.h>
+
+using namespace mozilla;
+
+nsRetrievalContextWayland::nsRetrievalContextWayland() = default;
+
+ClipboardTargets nsRetrievalContextWayland::GetTargetsImpl(
+ int32_t aWhichClipboard) {
+ LOGCLIP("nsRetrievalContextWayland::GetTargetsImpl()\n");
+
+ return WaitForClipboardData(ClipboardDataType::Targets, aWhichClipboard)
+ .ExtractTargets();
+}
+
+ClipboardData nsRetrievalContextWayland::GetClipboardData(
+ const char* aMimeType, int32_t aWhichClipboard) {
+ LOGCLIP("nsRetrievalContextWayland::GetClipboardData() mime %s\n", aMimeType);
+
+ return WaitForClipboardData(ClipboardDataType::Data, aWhichClipboard,
+ aMimeType);
+}
+
+GUniquePtr<char> nsRetrievalContextWayland::GetClipboardText(
+ int32_t aWhichClipboard) {
+ GdkAtom selection = GetSelectionAtom(aWhichClipboard);
+
+ LOGCLIP("nsRetrievalContextWayland::GetClipboardText(), clipboard %s\n",
+ (selection == GDK_SELECTION_PRIMARY) ? "Primary" : "Selection");
+
+ return WaitForClipboardData(ClipboardDataType::Text, aWhichClipboard)
+ .ExtractText();
+}
+
+ClipboardData nsRetrievalContextWayland::WaitForClipboardData(
+ ClipboardDataType aDataType, int32_t aWhichClipboard,
+ const char* aMimeType) {
+ LOGCLIP("nsRetrievalContextWayland::WaitForClipboardData, MIME %s\n",
+ aMimeType);
+
+ AsyncGtkClipboardRequest request(aDataType, aWhichClipboard, aMimeType);
+ int iteration = 1;
+
+ PRTime entryTime = PR_Now();
+ while (!request.HasCompleted()) {
+ if (iteration++ > kClipboardFastIterationNum) {
+ /* sleep for 10 ms/iteration */
+ PR_Sleep(PR_MillisecondsToInterval(10));
+ if (PR_Now() - entryTime > kClipboardTimeout) {
+ LOGCLIP(" failed to get async clipboard data in time limit\n");
+ break;
+ }
+ }
+ LOGCLIP("doing iteration %d msec %ld ...\n", (iteration - 1),
+ (long)((PR_Now() - entryTime) / 1000));
+ gtk_main_iteration();
+ }
+
+ return request.TakeResult();
+}
diff --git a/widget/gtk/nsClipboardWayland.h b/widget/gtk/nsClipboardWayland.h
new file mode 100644
index 0000000000..cd8dda0772
--- /dev/null
+++ b/widget/gtk/nsClipboardWayland.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsClipboardWayland_h_
+#define __nsClipboardWayland_h_
+
+#include "mozilla/Mutex.h"
+#include "nsClipboard.h"
+
+class nsRetrievalContextWayland final : public nsRetrievalContext {
+ public:
+ nsRetrievalContextWayland();
+
+ ClipboardData GetClipboardData(const char* aMimeType,
+ int32_t aWhichClipboard) override;
+ mozilla::GUniquePtr<char> GetClipboardText(int32_t aWhichClipboard) override;
+ ClipboardTargets GetTargetsImpl(int32_t aWhichClipboard) override;
+
+ private:
+ ClipboardData WaitForClipboardData(ClipboardDataType, int32_t aWhichClipboard,
+ const char* aMimeType = nullptr);
+};
+
+#endif /* __nsClipboardWayland_h_ */
diff --git a/widget/gtk/nsClipboardX11.cpp b/widget/gtk/nsClipboardX11.cpp
new file mode 100644
index 0000000000..db12d1e69c
--- /dev/null
+++ b/widget/gtk/nsClipboardX11.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "mozilla/ArrayUtils.h"
+
+#include "AsyncGtkClipboardRequest.h"
+#include "nsClipboardX11.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/WidgetUtilsGtk.h"
+
+#include <gtk/gtk.h>
+
+// For manipulation of the X event queue
+#include <X11/Xlib.h>
+#include <poll.h>
+#include <gdk/gdkx.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <unistd.h>
+#include "X11UndefineNone.h"
+
+using namespace mozilla;
+
+nsRetrievalContextX11::nsRetrievalContextX11() = default;
+
+static void DispatchSelectionNotifyEvent(GtkWidget* widget, XEvent* xevent) {
+ GdkWindow* window = gtk_widget_get_window(widget);
+ if (window) {
+ GdkEvent event = {};
+ event.selection.type = GDK_SELECTION_NOTIFY;
+ event.selection.window = window;
+ event.selection.selection =
+ gdk_x11_xatom_to_atom(xevent->xselection.selection);
+ event.selection.target = gdk_x11_xatom_to_atom(xevent->xselection.target);
+ event.selection.property =
+ gdk_x11_xatom_to_atom(xevent->xselection.property);
+ event.selection.time = xevent->xselection.time;
+
+ gtk_widget_event(widget, &event);
+ }
+}
+
+static void DispatchPropertyNotifyEvent(GtkWidget* widget, XEvent* xevent) {
+ GdkWindow* window = gtk_widget_get_window(widget);
+ if (window && ((gdk_window_get_events(window)) & GDK_PROPERTY_CHANGE_MASK)) {
+ GdkEvent event = {};
+ event.property.type = GDK_PROPERTY_NOTIFY;
+ event.property.window = window;
+ event.property.atom = gdk_x11_xatom_to_atom(xevent->xproperty.atom);
+ event.property.time = xevent->xproperty.time;
+ event.property.state = xevent->xproperty.state;
+
+ gtk_widget_event(widget, &event);
+ }
+}
+
+struct checkEventContext {
+ GtkWidget* cbWidget;
+ Atom selAtom;
+};
+
+static Bool checkEventProc(Display* display, XEvent* event, XPointer arg) {
+ checkEventContext* context = (checkEventContext*)arg;
+
+ if (event->xany.type == SelectionNotify ||
+ (event->xany.type == PropertyNotify &&
+ event->xproperty.atom == context->selAtom)) {
+ GdkWindow* cbWindow = gdk_x11_window_lookup_for_display(
+ gdk_x11_lookup_xdisplay(display), event->xany.window);
+ if (cbWindow) {
+ GtkWidget* cbWidget = nullptr;
+ gdk_window_get_user_data(cbWindow, (gpointer*)&cbWidget);
+ if (cbWidget && GTK_IS_WIDGET(cbWidget)) {
+ context->cbWidget = cbWidget;
+ return X11True;
+ }
+ }
+ }
+
+ return X11False;
+}
+
+ClipboardData nsRetrievalContextX11::WaitForClipboardData(
+ ClipboardDataType aDataType, int32_t aWhichClipboard,
+ const char* aMimeType) {
+ AsyncGtkClipboardRequest request(aDataType, aWhichClipboard, aMimeType);
+ if (request.HasCompleted()) {
+ // the request completed synchronously
+ return request.TakeResult();
+ }
+
+ GdkDisplay* gdkDisplay = gdk_display_get_default();
+ // gdk_display_get_default() returns null on headless
+ if (widget::GdkIsX11Display(gdkDisplay)) {
+ Display* xDisplay = GDK_DISPLAY_XDISPLAY(gdkDisplay);
+ checkEventContext context;
+ context.cbWidget = nullptr;
+ context.selAtom =
+ gdk_x11_atom_to_xatom(gdk_atom_intern("GDK_SELECTION", FALSE));
+
+ // Send X events which are relevant to the ongoing selection retrieval
+ // to the clipboard widget. Wait until either the operation completes, or
+ // we hit our timeout. All other X events remain queued.
+
+ int poll_result;
+
+ struct pollfd pfd;
+ pfd.fd = ConnectionNumber(xDisplay);
+ pfd.events = POLLIN;
+ TimeStamp start = TimeStamp::Now();
+
+ do {
+ XEvent xevent;
+
+ while (XCheckIfEvent(xDisplay, &xevent, checkEventProc,
+ (XPointer)&context)) {
+ if (xevent.xany.type == SelectionNotify)
+ DispatchSelectionNotifyEvent(context.cbWidget, &xevent);
+ else
+ DispatchPropertyNotifyEvent(context.cbWidget, &xevent);
+
+ if (request.HasCompleted()) {
+ return request.TakeResult();
+ }
+ }
+
+ TimeStamp now = TimeStamp::Now();
+ int timeout = std::max<int>(
+ 0, kClipboardTimeout / 1000 - (now - start).ToMilliseconds());
+ poll_result = poll(&pfd, 1, timeout);
+ } while ((poll_result == 1 && (pfd.revents & (POLLHUP | POLLERR)) == 0) ||
+ (poll_result == -1 && errno == EINTR));
+ }
+
+ LOGCLIP("exceeded clipboard timeout");
+ return {};
+}
+
+ClipboardTargets nsRetrievalContextX11::GetTargetsImpl(
+ int32_t aWhichClipboard) {
+ LOGCLIP("nsRetrievalContextX11::GetTargetsImpl(%s)\n",
+ aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+ return WaitForClipboardData(ClipboardDataType::Targets, aWhichClipboard)
+ .ExtractTargets();
+}
+
+ClipboardData nsRetrievalContextX11::GetClipboardData(const char* aMimeType,
+ int32_t aWhichClipboard) {
+ LOGCLIP("nsRetrievalContextX11::GetClipboardData(%s) MIME %s\n",
+ aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard",
+ aMimeType);
+
+ return WaitForClipboardData(ClipboardDataType::Data, aWhichClipboard,
+ aMimeType);
+}
+
+GUniquePtr<char> nsRetrievalContextX11::GetClipboardText(
+ int32_t aWhichClipboard) {
+ LOGCLIP("nsRetrievalContextX11::GetClipboardText(%s)\n",
+ aWhichClipboard == nsClipboard::kSelectionClipboard ? "primary"
+ : "clipboard");
+
+ return WaitForClipboardData(ClipboardDataType::Text, aWhichClipboard)
+ .ExtractText();
+}
diff --git a/widget/gtk/nsClipboardX11.h b/widget/gtk/nsClipboardX11.h
new file mode 100644
index 0000000000..c143222d3a
--- /dev/null
+++ b/widget/gtk/nsClipboardX11.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsClipboardX11_h_
+#define __nsClipboardX11_h_
+
+#include <gtk/gtk.h>
+
+#include "mozilla/Maybe.h"
+#include "nsClipboard.h"
+
+class nsRetrievalContextX11 : public nsRetrievalContext {
+ public:
+ ClipboardData GetClipboardData(const char* aMimeType,
+ int32_t aWhichClipboard) override;
+ mozilla::GUniquePtr<char> GetClipboardText(int32_t aWhichClipboard) override;
+ ClipboardTargets GetTargetsImpl(int32_t aWhichClipboard) override;
+
+ nsRetrievalContextX11();
+
+ private:
+ // Spins X event loop until timing out or being completed.
+ ClipboardData WaitForClipboardData(ClipboardDataType aDataType,
+ int32_t aWhichClipboard,
+ const char* aMimeType = nullptr);
+};
+
+#endif /* __nsClipboardX11_h_ */
diff --git a/widget/gtk/nsColorPicker.cpp b/widget/gtk/nsColorPicker.cpp
new file mode 100644
index 0000000000..4719710d9e
--- /dev/null
+++ b/widget/gtk/nsColorPicker.cpp
@@ -0,0 +1,253 @@
+/* -*- 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 <gtk/gtk.h>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "nsColor.h"
+#include "nsColorPicker.h"
+#include "nsGtkUtils.h"
+#include "nsIWidget.h"
+#include "WidgetUtils.h"
+#include "nsPIDOMWindow.h"
+
+using mozilla::dom::HTMLInputElement;
+
+NS_IMPL_ISUPPORTS(nsColorPicker, nsIColorPicker)
+
+#if defined(ACTIVATE_GTK3_COLOR_PICKER)
+int nsColorPicker::convertGdkRgbaComponent(gdouble color_component) {
+ // GdkRGBA value is in range [0.0..1.0]. We need something in range [0..255]
+ return color_component * 255 + 0.5;
+}
+
+gdouble nsColorPicker::convertToGdkRgbaComponent(int color_component) {
+ return color_component / 255.0;
+}
+
+GdkRGBA nsColorPicker::convertToRgbaColor(nscolor color) {
+ GdkRGBA result = {convertToGdkRgbaComponent(NS_GET_R(color)),
+ convertToGdkRgbaComponent(NS_GET_G(color)),
+ convertToGdkRgbaComponent(NS_GET_B(color)),
+ convertToGdkRgbaComponent(NS_GET_A(color))};
+
+ return result;
+}
+#else
+int nsColorPicker::convertGdkColorComponent(guint16 color_component) {
+ // GdkColor value is in range [0..65535]. We need something in range [0..255]
+ return (color_component * 255 + 127) / 65535;
+}
+
+guint16 nsColorPicker::convertToGdkColorComponent(int color_component) {
+ return color_component * 65535 / 255;
+}
+
+GdkColor nsColorPicker::convertToGdkColor(nscolor color) {
+ GdkColor result = {0 /* obsolete, unused 'pixel' value */,
+ convertToGdkColorComponent(NS_GET_R(color)),
+ convertToGdkColorComponent(NS_GET_G(color)),
+ convertToGdkColorComponent(NS_GET_B(color))};
+
+ return result;
+}
+
+GtkColorSelection* nsColorPicker::WidgetGetColorSelection(GtkWidget* widget) {
+ return GTK_COLOR_SELECTION(gtk_color_selection_dialog_get_color_selection(
+ GTK_COLOR_SELECTION_DIALOG(widget)));
+}
+#endif
+
+NS_IMETHODIMP nsColorPicker::Init(mozIDOMWindowProxy* aParent,
+ const nsAString& title,
+ const nsAString& initialColor,
+ const nsTArray<nsString>& aDefaultColors) {
+ auto* parent = nsPIDOMWindowOuter::From(aParent);
+ mParentWidget = mozilla::widget::WidgetUtils::DOMWindowToWidget(parent);
+ mTitle = title;
+ mInitialColor = initialColor;
+ mDefaultColors.Assign(aDefaultColors);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsColorPicker::Open(
+ nsIColorPickerShownCallback* aColorPickerShownCallback) {
+ auto maybeColor = HTMLInputElement::ParseSimpleColor(mInitialColor);
+ if (maybeColor.isNothing()) {
+ return NS_ERROR_FAILURE;
+ }
+ nscolor color = maybeColor.value();
+
+ if (mCallback) {
+ // It means Open has already been called: this is not allowed
+ NS_WARNING("mCallback is already set. Open called twice?");
+ return NS_ERROR_FAILURE;
+ }
+ mCallback = aColorPickerShownCallback;
+
+ NS_ConvertUTF16toUTF8 title(mTitle);
+ GtkWindow* parent_window =
+ GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET));
+
+#if defined(ACTIVATE_GTK3_COLOR_PICKER)
+ GtkWidget* color_chooser =
+ gtk_color_chooser_dialog_new(title.get(), parent_window);
+
+ if (parent_window) {
+ GtkWindow* window = GTK_WINDOW(color_chooser);
+ gtk_window_set_destroy_with_parent(window, TRUE);
+ if (gtk_window_get_modal(parent_window)) {
+ gtk_window_set_modal(window, TRUE);
+ }
+ }
+
+ gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(color_chooser), FALSE);
+
+ // Setting the default colors will put them into "Custom" colors list.
+ for (const nsString& defaultColor : mDefaultColors) {
+ if (auto color = HTMLInputElement::ParseSimpleColor(defaultColor)) {
+ GdkRGBA color_rgba = convertToRgbaColor(*color);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(color_chooser), &color_rgba);
+ }
+ }
+
+ // The initial color needs to be set last.
+ GdkRGBA color_rgba = convertToRgbaColor(color);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(color_chooser), &color_rgba);
+
+ g_signal_connect(GTK_COLOR_CHOOSER(color_chooser), "color-activated",
+ G_CALLBACK(OnColorChanged), this);
+#else
+ GtkWidget* color_chooser = gtk_color_selection_dialog_new(title.get());
+
+ if (parent_window) {
+ GtkWindow* window = GTK_WINDOW(color_chooser);
+ gtk_window_set_transient_for(window, parent_window);
+ gtk_window_set_destroy_with_parent(window, TRUE);
+ if (gtk_window_get_modal(parent_window)) {
+ gtk_window_set_modal(window, TRUE);
+ }
+ }
+
+ GdkColor color_gdk = convertToGdkColor(color);
+ gtk_color_selection_set_current_color(WidgetGetColorSelection(color_chooser),
+ &color_gdk);
+
+ g_signal_connect(WidgetGetColorSelection(color_chooser), "color-changed",
+ G_CALLBACK(OnColorChanged), this);
+#endif
+
+ NS_ADDREF_THIS();
+
+ g_signal_connect(color_chooser, "response", G_CALLBACK(OnResponse), this);
+ g_signal_connect(color_chooser, "destroy", G_CALLBACK(OnDestroy), this);
+ gtk_widget_show(color_chooser);
+
+ return NS_OK;
+}
+
+#if defined(ACTIVATE_GTK3_COLOR_PICKER)
+/* static */
+void nsColorPicker::OnColorChanged(GtkColorChooser* color_chooser,
+ GdkRGBA* color, gpointer user_data) {
+ static_cast<nsColorPicker*>(user_data)->Update(color);
+}
+
+void nsColorPicker::Update(GdkRGBA* color) {
+ SetColor(color);
+ if (mCallback) {
+ mCallback->Update(mColor);
+ }
+}
+
+void nsColorPicker::SetColor(const GdkRGBA* color) {
+ mColor.Assign('#');
+ mColor += ToHexString(convertGdkRgbaComponent(color->red));
+ mColor += ToHexString(convertGdkRgbaComponent(color->green));
+ mColor += ToHexString(convertGdkRgbaComponent(color->blue));
+}
+#else
+/* static */
+void nsColorPicker::OnColorChanged(GtkColorSelection* colorselection,
+ gpointer user_data) {
+ static_cast<nsColorPicker*>(user_data)->Update(colorselection);
+}
+
+void nsColorPicker::Update(GtkColorSelection* colorselection) {
+ ReadValueFromColorSelection(colorselection);
+ if (mCallback) {
+ mCallback->Update(mColor);
+ }
+}
+
+void nsColorPicker::ReadValueFromColorSelection(
+ GtkColorSelection* colorselection) {
+ GdkColor rgba;
+ gtk_color_selection_get_current_color(colorselection, &rgba);
+
+ mColor.Assign('#');
+ mColor += ToHexString(convertGdkColorComponent(rgba.red));
+ mColor += ToHexString(convertGdkColorComponent(rgba.green));
+ mColor += ToHexString(convertGdkColorComponent(rgba.blue));
+}
+#endif
+
+/* static */
+void nsColorPicker::OnResponse(GtkWidget* color_chooser, gint response_id,
+ gpointer user_data) {
+ static_cast<nsColorPicker*>(user_data)->Done(color_chooser, response_id);
+}
+
+/* static */
+void nsColorPicker::OnDestroy(GtkWidget* color_chooser, gpointer user_data) {
+ static_cast<nsColorPicker*>(user_data)->Done(color_chooser,
+ GTK_RESPONSE_CANCEL);
+}
+
+void nsColorPicker::Done(GtkWidget* color_chooser, gint response) {
+ switch (response) {
+ case GTK_RESPONSE_OK:
+ case GTK_RESPONSE_ACCEPT:
+#if defined(ACTIVATE_GTK3_COLOR_PICKER)
+ GdkRGBA color;
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(color_chooser), &color);
+ SetColor(&color);
+#else
+ ReadValueFromColorSelection(WidgetGetColorSelection(color_chooser));
+#endif
+ break;
+ case GTK_RESPONSE_CANCEL:
+ case GTK_RESPONSE_CLOSE:
+ case GTK_RESPONSE_DELETE_EVENT:
+ mColor = mInitialColor;
+ break;
+ default:
+ NS_WARNING("Unexpected response");
+ break;
+ }
+
+ // A "response" signal won't be sent again but "destroy" will be.
+ g_signal_handlers_disconnect_by_func(color_chooser, FuncToGpointer(OnDestroy),
+ this);
+
+ gtk_widget_destroy(color_chooser);
+ if (mCallback) {
+ mCallback->Done(mColor);
+ mCallback = nullptr;
+ }
+
+ NS_RELEASE_THIS();
+}
+
+nsString nsColorPicker::ToHexString(int n) {
+ nsString result;
+ if (n <= 0x0F) {
+ result.Append('0');
+ }
+ result.AppendInt(n, 16);
+ return result;
+}
diff --git a/widget/gtk/nsColorPicker.h b/widget/gtk/nsColorPicker.h
new file mode 100644
index 0000000000..d693175717
--- /dev/null
+++ b/widget/gtk/nsColorPicker.h
@@ -0,0 +1,71 @@
+/* -*- 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/. */
+
+#ifndef nsColorPicker_h__
+#define nsColorPicker_h__
+
+#include <gtk/gtk.h>
+
+#include "nsCOMPtr.h"
+#include "nsIColorPicker.h"
+#include "nsString.h"
+
+// Enable Gtk3 system color picker.
+#define ACTIVATE_GTK3_COLOR_PICKER 1
+
+class nsIWidget;
+
+class nsColorPicker final : public nsIColorPicker {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOLORPICKER
+
+ nsColorPicker() = default;
+
+ private:
+ ~nsColorPicker() = default;
+
+ static nsString ToHexString(int n);
+
+ static void OnResponse(GtkWidget* dialog, gint response_id,
+ gpointer user_data);
+ static void OnDestroy(GtkWidget* dialog, gpointer user_data);
+
+#if defined(ACTIVATE_GTK3_COLOR_PICKER)
+ static void OnColorChanged(GtkColorChooser* color_chooser, GdkRGBA* color,
+ gpointer user_data);
+
+ static int convertGdkRgbaComponent(gdouble color_component);
+ static gdouble convertToGdkRgbaComponent(int color_component);
+ static GdkRGBA convertToRgbaColor(nscolor color);
+
+ void Update(GdkRGBA* color);
+ void SetColor(const GdkRGBA* color);
+#else
+ static void OnColorChanged(GtkColorSelection* colorselection,
+ gpointer user_data);
+
+ // Conversion functions for color
+ static int convertGdkColorComponent(guint16 color_component);
+ static guint16 convertToGdkColorComponent(int color_component);
+ static GdkColor convertToGdkColor(nscolor color);
+
+ static GtkColorSelection* WidgetGetColorSelection(GtkWidget* widget);
+
+ void Update(GtkColorSelection* colorselection);
+ void ReadValueFromColorSelection(GtkColorSelection* colorselection);
+#endif
+
+ void Done(GtkWidget* dialog, gint response_id);
+
+ nsCOMPtr<nsIWidget> mParentWidget;
+ nsCOMPtr<nsIColorPickerShownCallback> mCallback;
+ nsString mTitle;
+ nsString mColor;
+ nsString mInitialColor;
+ nsTArray<nsString> mDefaultColors;
+};
+
+#endif // nsColorPicker_h__
diff --git a/widget/gtk/nsDeviceContextSpecG.cpp b/widget/gtk/nsDeviceContextSpecG.cpp
new file mode 100644
index 0000000000..3dfa816408
--- /dev/null
+++ b/widget/gtk/nsDeviceContextSpecG.cpp
@@ -0,0 +1,422 @@
+/* -*- 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 "nsDeviceContextSpecG.h"
+
+#include "mozilla/gfx/PrintPromise.h"
+#include "mozilla/gfx/PrintTargetPDF.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/WidgetUtilsGtk.h"
+
+#include "prenv.h" /* for PR_GetEnv */
+
+#include "nsComponentManagerUtils.h"
+#include "nsIObserverService.h"
+#include "nsPrintfCString.h"
+#include "nsQueryObject.h"
+#include "nsReadableUtils.h"
+#include "nsThreadUtils.h"
+
+#include "nsCUPSShim.h"
+#include "nsPrinterCUPS.h"
+
+#include "nsPrintSettingsGTK.h"
+
+#include "nsIFileStreams.h"
+#include "nsIFile.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_print.h"
+
+#include <dlfcn.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+// To check if we need to use flatpak portal for printing
+#include "nsIGIOService.h"
+
+using namespace mozilla;
+
+using mozilla::gfx::IntSize;
+using mozilla::gfx::PrintEndDocumentPromise;
+using mozilla::gfx::PrintTarget;
+using mozilla::gfx::PrintTargetPDF;
+
+nsDeviceContextSpecGTK::nsDeviceContextSpecGTK()
+ : mGtkPrintSettings(nullptr), mGtkPageSetup(nullptr) {}
+
+nsDeviceContextSpecGTK::~nsDeviceContextSpecGTK() {
+ if (mGtkPageSetup) {
+ g_object_unref(mGtkPageSetup);
+ }
+
+ if (mGtkPrintSettings) {
+ g_object_unref(mGtkPrintSettings);
+ }
+
+ if (mSpoolFile) {
+ mSpoolFile->Remove(false);
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsDeviceContextSpecGTK, nsIDeviceContextSpec)
+
+already_AddRefed<PrintTarget> nsDeviceContextSpecGTK::MakePrintTarget() {
+ double width, height;
+ mPrintSettings->GetEffectiveSheetSize(&width, &height);
+
+ // convert twips to points
+ width /= TWIPS_PER_POINT_FLOAT;
+ height /= TWIPS_PER_POINT_FLOAT;
+
+ // We shouldn't be attempting to get a surface if we've already got a spool
+ // file.
+ MOZ_ASSERT(!mSpoolFile);
+
+ auto stream = [&]() -> nsCOMPtr<nsIOutputStream> {
+ if (mPrintSettings->GetOutputDestination() ==
+ nsIPrintSettings::kOutputDestinationStream) {
+ nsCOMPtr<nsIOutputStream> out;
+ mPrintSettings->GetOutputStream(getter_AddRefs(out));
+ return out;
+ }
+ // Spool file. Use Glib's temporary file function since we're
+ // already dependent on the gtk software stack.
+ gchar* buf;
+ gint fd = g_file_open_tmp("XXXXXX.tmp", &buf, nullptr);
+ if (-1 == fd) {
+ return nullptr;
+ }
+ close(fd);
+ if (NS_FAILED(NS_NewNativeLocalFile(nsDependentCString(buf), false,
+ getter_AddRefs(mSpoolFile)))) {
+ unlink(buf);
+ g_free(buf);
+ return nullptr;
+ }
+ mSpoolName = buf;
+ g_free(buf);
+ mSpoolFile->SetPermissions(0600);
+ nsCOMPtr<nsIFileOutputStream> stream =
+ do_CreateInstance("@mozilla.org/network/file-output-stream;1");
+ if (NS_FAILED(stream->Init(mSpoolFile, -1, -1, 0))) {
+ return nullptr;
+ }
+ return stream;
+ }();
+
+ return PrintTargetPDF::CreateOrNull(stream, IntSize::Ceil(width, height));
+}
+
+#define DECLARE_KNOWN_MONOCHROME_SETTING(key_, value_) {"cups-" key_, value_},
+
+struct {
+ const char* mKey;
+ const char* mValue;
+} kKnownMonochromeSettings[] = {
+ CUPS_EACH_MONOCHROME_PRINTER_SETTING(DECLARE_KNOWN_MONOCHROME_SETTING)};
+
+#undef DECLARE_KNOWN_MONOCHROME_SETTING
+
+// https://developer.gnome.org/gtk3/stable/GtkPaperSize.html#gtk-paper-size-new-from-ipp
+static GUniquePtr<GtkPaperSize> GtkPaperSizeFromIpp(const gchar* aIppName,
+ gdouble aWidth,
+ gdouble aHeight) {
+ static auto sPtr = (GtkPaperSize * (*)(const gchar*, gdouble, gdouble))
+ dlsym(RTLD_DEFAULT, "gtk_paper_size_new_from_ipp");
+ if (gtk_check_version(3, 16, 0)) {
+ return nullptr;
+ }
+ return GUniquePtr<GtkPaperSize>(sPtr(aIppName, aWidth, aHeight));
+}
+
+static bool PaperSizeAlmostEquals(GtkPaperSize* aSize,
+ GtkPaperSize* aOtherSize) {
+ const double kEpsilon = 1.0; // millimetres
+ // GTK stores sizes internally in millimetres so just use that.
+ if (fabs(gtk_paper_size_get_height(aSize, GTK_UNIT_MM) -
+ gtk_paper_size_get_height(aOtherSize, GTK_UNIT_MM)) > kEpsilon) {
+ return false;
+ }
+ if (fabs(gtk_paper_size_get_width(aSize, GTK_UNIT_MM) -
+ gtk_paper_size_get_width(aOtherSize, GTK_UNIT_MM)) > kEpsilon) {
+ return false;
+ }
+ return true;
+}
+
+// Prefer the ppd name because some printers don't deal well even with standard
+// ipp names.
+static GUniquePtr<GtkPaperSize> PpdSizeFromIppName(const gchar* aIppName) {
+ static constexpr struct {
+ const char* mCups;
+ const char* mGtk;
+ } kMap[] = {
+ {CUPS_MEDIA_A3, GTK_PAPER_NAME_A3},
+ {CUPS_MEDIA_A4, GTK_PAPER_NAME_A4},
+ {CUPS_MEDIA_A5, GTK_PAPER_NAME_A5},
+ {CUPS_MEDIA_LETTER, GTK_PAPER_NAME_LETTER},
+ {CUPS_MEDIA_LEGAL, GTK_PAPER_NAME_LEGAL},
+ // Other gtk sizes with no standard CUPS constant: _EXECUTIVE and _B5
+ };
+
+ for (const auto& entry : kMap) {
+ if (!strcmp(entry.mCups, aIppName)) {
+ return GUniquePtr<GtkPaperSize>(gtk_paper_size_new(entry.mGtk));
+ }
+ }
+
+ return nullptr;
+}
+
+// This is a horrible workaround for some printer driver bugs that treat custom
+// page sizes different to standard ones. If our paper object matches one of a
+// standard one, use a standard paper size object instead.
+//
+// We prefer ppd to ipp to custom sizes.
+//
+// See bug 414314, bug 1691798, and bug 1717292 for more info.
+static GUniquePtr<GtkPaperSize> GetStandardGtkPaperSize(
+ GtkPaperSize* aGeckoPaperSize) {
+ // We should get an ipp name from cups, try to get a ppd from that first.
+ const gchar* geckoName = gtk_paper_size_get_name(aGeckoPaperSize);
+ if (auto ppd = PpdSizeFromIppName(geckoName)) {
+ return ppd;
+ }
+
+ // We try gtk_paper_size_new_from_ipp next, because even though
+ // gtk_paper_size_new tries to deal with ipp, it has some rounding issues that
+ // the ipp equivalent doesn't have, see
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/3685.
+ if (auto ipp = GtkPaperSizeFromIpp(
+ geckoName, gtk_paper_size_get_width(aGeckoPaperSize, GTK_UNIT_POINTS),
+ gtk_paper_size_get_height(aGeckoPaperSize, GTK_UNIT_POINTS))) {
+ if (!gtk_paper_size_is_custom(ipp.get())) {
+ if (auto ppd = PpdSizeFromIppName(gtk_paper_size_get_name(ipp.get()))) {
+ return ppd;
+ }
+ return ipp;
+ }
+ }
+
+ GUniquePtr<GtkPaperSize> size(gtk_paper_size_new(geckoName));
+ // gtk_paper_size_is_equal compares just paper names. The name in Gecko
+ // might come from CUPS, which is an ipp size, and gets normalized by gtk.
+ // So check also for the same actual paper size.
+ if (gtk_paper_size_is_equal(size.get(), aGeckoPaperSize) ||
+ PaperSizeAlmostEquals(aGeckoPaperSize, size.get())) {
+ return size;
+ }
+
+ // Not the same after all, so use our custom paper sizes instead.
+ return nullptr;
+}
+
+/** -------------------------------------------------------
+ * Initialize the nsDeviceContextSpecGTK
+ * @update dc 2/15/98
+ * @update syd 3/2/99
+ */
+NS_IMETHODIMP nsDeviceContextSpecGTK::Init(nsIPrintSettings* aPS,
+ bool aIsPrintPreview) {
+ RefPtr<nsPrintSettingsGTK> settings = do_QueryObject(aPS);
+ if (!settings) {
+ return NS_ERROR_NO_INTERFACE;
+ }
+ mPrintSettings = aPS;
+
+ mGtkPrintSettings = settings->GetGtkPrintSettings();
+ mGtkPageSetup = settings->GetGtkPageSetup();
+
+ GtkPaperSize* geckoPaperSize = gtk_page_setup_get_paper_size(mGtkPageSetup);
+ GUniquePtr<GtkPaperSize> gtkPaperSize =
+ GetStandardGtkPaperSize(geckoPaperSize);
+
+ mGtkPageSetup = gtk_page_setup_copy(mGtkPageSetup);
+ mGtkPrintSettings = gtk_print_settings_copy(mGtkPrintSettings);
+
+ if (!aPS->GetPrintInColor() && StaticPrefs::print_cups_monochrome_enabled()) {
+ for (const auto& setting : kKnownMonochromeSettings) {
+ gtk_print_settings_set(mGtkPrintSettings, setting.mKey, setting.mValue);
+ }
+ auto applySetting = [&](const nsACString& aKey, const nsACString& aVal) {
+ nsAutoCString extra;
+ extra.AppendASCII("cups-");
+ extra.Append(aKey);
+ gtk_print_settings_set(mGtkPrintSettings, extra.get(),
+ nsAutoCString(aVal).get());
+ };
+ nsPrinterCUPS::ForEachExtraMonochromeSetting(applySetting);
+ }
+
+ GtkPaperSize* properPaperSize =
+ gtkPaperSize ? gtkPaperSize.get() : geckoPaperSize;
+ gtk_print_settings_set_paper_size(mGtkPrintSettings, properPaperSize);
+ gtk_page_setup_set_paper_size_and_default_margins(mGtkPageSetup,
+ properPaperSize);
+ return NS_OK;
+}
+
+static void print_callback(GtkPrintJob* aJob, gpointer aData,
+ const GError* aError) {
+ g_object_unref(aJob);
+ ((nsIFile*)aData)->Remove(false);
+}
+
+/* static */
+gboolean nsDeviceContextSpecGTK::PrinterEnumerator(GtkPrinter* aPrinter,
+ gpointer aData) {
+ nsDeviceContextSpecGTK* spec = (nsDeviceContextSpecGTK*)aData;
+
+ if (spec->mHasEnumerationFoundAMatch) {
+ // We're already done, but we're letting the enumeration run its course,
+ // to avoid a GTK bug.
+ return FALSE;
+ }
+
+ // Find the printer whose name matches the one inside the settings.
+ nsString printerName;
+ nsresult rv = spec->mPrintSettings->GetPrinterName(printerName);
+ if (NS_SUCCEEDED(rv) && !printerName.IsVoid()) {
+ NS_ConvertUTF16toUTF8 requestedName(printerName);
+ const char* currentName = gtk_printer_get_name(aPrinter);
+ if (requestedName.Equals(currentName)) {
+ nsPrintSettingsGTK::From(spec->mPrintSettings)->SetGtkPrinter(aPrinter);
+
+ // Bug 1145916 - attempting to kick off a print job for this printer
+ // during this tick of the event loop will result in the printer backend
+ // misunderstanding what the capabilities of the printer are due to a
+ // GTK bug (https://bugzilla.gnome.org/show_bug.cgi?id=753041). We
+ // sidestep this by deferring the print to the next tick.
+ NS_DispatchToCurrentThread(
+ NewRunnableMethod("nsDeviceContextSpecGTK::StartPrintJob", spec,
+ &nsDeviceContextSpecGTK::StartPrintJob));
+
+ // We're already done, but we need to let the enumeration run its course,
+ // to avoid a GTK bug. So we record that we've found a match and
+ // then return FALSE.
+ // TODO: If/when we can be sure that GTK handles this OK, we could
+ // return TRUE to avoid some needless enumeration.
+ spec->mHasEnumerationFoundAMatch = true;
+ return FALSE;
+ }
+ }
+
+ // We haven't found it yet - keep searching...
+ return FALSE;
+}
+
+void nsDeviceContextSpecGTK::StartPrintJob() {
+ GtkPrintJob* job = gtk_print_job_new(
+ mTitle.get(), nsPrintSettingsGTK::From(mPrintSettings)->GetGtkPrinter(),
+ mGtkPrintSettings, mGtkPageSetup);
+
+ if (!gtk_print_job_set_source_file(job, mSpoolName.get(), nullptr)) return;
+
+ // Now gtk owns the print job, and will be released via our callback.
+ gtk_print_job_send(job, print_callback, mSpoolFile.forget().take(),
+ [](gpointer aData) {
+ auto* spoolFile = static_cast<nsIFile*>(aData);
+ NS_RELEASE(spoolFile);
+ });
+}
+
+void nsDeviceContextSpecGTK::EnumeratePrinters() {
+ mHasEnumerationFoundAMatch = false;
+ gtk_enumerate_printers(&nsDeviceContextSpecGTK::PrinterEnumerator, this,
+ nullptr, TRUE);
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecGTK::BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) {
+ // Print job names exceeding 255 bytes are safe with GTK version 3.18.2 or
+ // newer. This is a workaround for old GTK.
+ if (gtk_check_version(3, 18, 2) != nullptr) {
+ PrintTarget::AdjustPrintJobNameForIPP(aTitle, mTitle);
+ } else {
+ CopyUTF16toUTF8(aTitle, mTitle);
+ }
+
+ return NS_OK;
+}
+
+RefPtr<PrintEndDocumentPromise> nsDeviceContextSpecGTK::EndDocument() {
+ switch (mPrintSettings->GetOutputDestination()) {
+ case nsIPrintSettings::kOutputDestinationPrinter: {
+ // At this point, we might have a GtkPrinter set up in nsPrintSettingsGTK,
+ // or we might not. In the single-process case, we probably will, as this
+ // is populated by the print settings dialog, or set to the default
+ // printer.
+ // In the multi-process case, we proxy the print settings dialog over to
+ // the parent process, and only get the name of the printer back on the
+ // content process side. In that case, we need to enumerate the printers
+ // on the content side, and find a printer with a matching name.
+
+ if (nsPrintSettingsGTK::From(mPrintSettings)->GetGtkPrinter()) {
+ // We have a printer, so we can print right away.
+ StartPrintJob();
+ } else {
+ // We don't have a printer. We have to enumerate the printers and find
+ // one with a matching name.
+ NS_DispatchToCurrentThread(
+ NewRunnableMethod("nsDeviceContextSpecGTK::EnumeratePrinters", this,
+ &nsDeviceContextSpecGTK::EnumeratePrinters));
+ }
+ break;
+ }
+ case nsIPrintSettings::kOutputDestinationFile: {
+ // Handle print-to-file ourselves for the benefit of embedders
+ nsString targetPath;
+ nsCOMPtr<nsIFile> destFile;
+ mPrintSettings->GetToFileName(targetPath);
+
+ nsresult rv =
+ NS_NewLocalFile(targetPath, false, getter_AddRefs(destFile));
+ if (NS_FAILED(rv)) {
+ return PrintEndDocumentPromise::CreateAndReject(rv, __func__);
+ }
+
+ return nsIDeviceContextSpec::EndDocumentAsync(
+ __func__,
+ [destFile = std::move(destFile),
+ spoolFile = std::move(mSpoolFile)]() -> nsresult {
+ nsAutoString destLeafName;
+ auto rv = destFile->GetLeafName(destLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> destDir;
+ rv = destFile->GetParent(getter_AddRefs(destDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = spoolFile->MoveTo(destDir, destLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This is the standard way to get the UNIX umask. Ugh.
+ mode_t mask = umask(0);
+ umask(mask);
+ // If you're not familiar with umasks, they contain the bits of what
+ // NOT to set in the permissions (thats because files and
+ // directories have different numbers of bits for their permissions)
+ destFile->SetPermissions(0666 & ~(mask));
+ return NS_OK;
+ });
+
+ break;
+ }
+ case nsIPrintSettings::kOutputDestinationStream:
+ // Nothing to do, handled in MakePrintTarget.
+ MOZ_ASSERT(!mSpoolFile);
+ break;
+ }
+ return PrintEndDocumentPromise::CreateAndResolve(true, __func__);
+}
diff --git a/widget/gtk/nsDeviceContextSpecG.h b/widget/gtk/nsDeviceContextSpecG.h
new file mode 100644
index 0000000000..df5552c36c
--- /dev/null
+++ b/widget/gtk/nsDeviceContextSpecG.h
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+
+#ifndef nsDeviceContextSpecGTK_h___
+#define nsDeviceContextSpecGTK_h___
+
+struct JSContext;
+
+#include "nsIDeviceContextSpec.h"
+#include "nsIPrinterList.h"
+#include "nsIPrintSettings.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/gfx/PrintPromise.h"
+
+#include "nsCRT.h" /* should be <limits.h>? */
+
+#include <gtk/gtk.h>
+#include <gtk/gtkunixprint.h>
+
+#define NS_PORTRAIT 0
+#define NS_LANDSCAPE 1
+
+class nsPrintSettingsGTK;
+
+class nsDeviceContextSpecGTK : public nsIDeviceContextSpec {
+ public:
+ nsDeviceContextSpecGTK();
+
+ NS_DECL_ISUPPORTS
+
+ already_AddRefed<PrintTarget> MakePrintTarget() final;
+
+ NS_IMETHOD Init(nsIPrintSettings* aPS, bool aIsPrintPreview) override;
+ NS_IMETHOD BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) override;
+ RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() override;
+ NS_IMETHOD BeginPage(const IntSize& aSizeInPoints) override { return NS_OK; }
+ NS_IMETHOD EndPage() override { return NS_OK; }
+
+ protected:
+ virtual ~nsDeviceContextSpecGTK();
+ GtkPrintSettings* mGtkPrintSettings;
+ GtkPageSetup* mGtkPageSetup;
+
+ nsCString mSpoolName;
+ nsCOMPtr<nsIFile> mSpoolFile;
+ nsCString mTitle;
+ // Helper for EnumeratePrinters / PrinterEnumerator:
+ bool mHasEnumerationFoundAMatch = false;
+
+ private:
+ void EnumeratePrinters();
+ void StartPrintJob();
+ static gboolean PrinterEnumerator(GtkPrinter* aPrinter, gpointer aData);
+};
+
+#endif /* !nsDeviceContextSpecGTK_h___ */
diff --git a/widget/gtk/nsDragService.cpp b/widget/gtk/nsDragService.cpp
new file mode 100644
index 0000000000..df0965b5e4
--- /dev/null
+++ b/widget/gtk/nsDragService.cpp
@@ -0,0 +1,2756 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=4 et sw=2 tw=80: */
+/* 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 "nsDragService.h"
+#include "nsArrayUtils.h"
+#include "nsIObserverService.h"
+#include "nsWidgetsCID.h"
+#include "nsWindow.h"
+#include "nsSystemInfo.h"
+#include "nsXPCOM.h"
+#include "nsICookieJarSettings.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIIOService.h"
+#include "nsIFileURL.h"
+#include "nsNetUtil.h"
+#include "mozilla/Logging.h"
+#include "nsTArray.h"
+#include "nsPrimitiveHelpers.h"
+#include "prtime.h"
+#include "prthread.h"
+#include <dlfcn.h>
+#include <gtk/gtk.h>
+#include "nsCRT.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/Services.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/WidgetUtilsGtk.h"
+#include "GRefPtr.h"
+#include "nsAppShell.h"
+
+#ifdef MOZ_X11
+# include "gfxXlibSurface.h"
+#endif
+#include "gfxContext.h"
+#include "nsImageToPixbuf.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsViewManager.h"
+#include "nsIFrame.h"
+#include "nsGtkUtils.h"
+#include "nsGtkKeyUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "gfxPlatform.h"
+#include "ScreenHelperGTK.h"
+#include "nsArrayUtils.h"
+#include "nsStringStream.h"
+#include "nsDirectoryService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsEscape.h"
+#include "nsString.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+// The maximum time to wait for a "drag_received" arrived in microseconds.
+#define NS_DND_TIMEOUT 1000000
+
+// The maximum time to wait before temporary files resulting
+// from drag'n'drop events will be removed in miliseconds.
+// It's set to 5 min as the file has to be present some time after drop
+// event to give target application time to get the data.
+// (A target application can throw a dialog to ask user what to do with
+// the data and will access the tmp file after user action.)
+#define NS_DND_TMP_CLEANUP_TIMEOUT (1000 * 60 * 5)
+
+#define NS_SYSTEMINFO_CONTRACTID "@mozilla.org/system-info;1"
+
+// This sets how opaque the drag image is
+#define DRAG_IMAGE_ALPHA_LEVEL 0.5
+
+#ifdef MOZ_LOGGING
+extern mozilla::LazyLogModule gWidgetDragLog;
+# define LOGDRAGSERVICE(str, ...) \
+ MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, \
+ ("[Depth %d]: " str, GetLoopDepth(), ##__VA_ARGS__))
+# define LOGDRAGSERVICESTATIC(str, ...) \
+ MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__))
+#else
+# define LOGDRAGSERVICE(...)
+#endif
+
+// Helper class to block native events processing.
+class MOZ_STACK_CLASS AutoSuspendNativeEvents {
+ public:
+ AutoSuspendNativeEvents() {
+ mAppShell = do_GetService(NS_APPSHELL_CID);
+ mAppShell->SuspendNative();
+ }
+ ~AutoSuspendNativeEvents() { mAppShell->ResumeNative(); }
+
+ private:
+ nsCOMPtr<nsIAppShell> mAppShell;
+};
+
+// data used for synthetic periodic motion events sent to the source widget
+// grabbing real events for the drag.
+static guint sMotionEventTimerID;
+
+static GdkEvent* sMotionEvent;
+static GUniquePtr<GdkEvent> TakeMotionEvent() {
+ GUniquePtr<GdkEvent> event(sMotionEvent);
+ sMotionEvent = nullptr;
+ return event;
+}
+static void SetMotionEvent(GUniquePtr<GdkEvent> aEvent) {
+ TakeMotionEvent();
+ sMotionEvent = aEvent.release();
+}
+
+static GtkWidget* sGrabWidget;
+
+static constexpr nsLiteralString kDisallowedExportedSchemes[] = {
+ u"about"_ns, u"blob"_ns, u"cached-favicon"_ns,
+ u"chrome"_ns, u"imap"_ns, u"javascript"_ns,
+ u"mailbox"_ns, u"news"_ns, u"page-icon"_ns,
+ u"resource"_ns, u"view-source"_ns, u"moz-extension"_ns,
+ u"moz-page-thumb"_ns,
+};
+
+// _NETSCAPE_URL is similar to text/uri-list type.
+// Format is UTF8: URL + "\n" + title.
+// While text/uri-list tells target application to fetch, copy and store data
+// from URL, _NETSCAPE_URL suggest to create a link to the target.
+// Also _NETSCAPE_URL points to only one item while text/uri-list can point to
+// multiple ones.
+static const char gMozUrlType[] = "_NETSCAPE_URL";
+static const char gMimeListType[] = "application/x-moz-internal-item-list";
+static const char gTextUriListType[] = "text/uri-list";
+static const char gTextPlainUTF8Type[] = "text/plain;charset=utf-8";
+static const char gXdndDirectSaveType[] = "XdndDirectSave0";
+static const char gTabDropType[] = "application/x-moz-tabbrowser-tab";
+static const char gPortalFile[] = "application/vnd.portal.files";
+static const char gPortalFileTransfer[] = "application/vnd.portal.filetransfer";
+
+// See https://docs.gtk.org/gtk3/enum.DragResult.html
+static const char kGtkDragResults[][100]{
+ "GTK_DRAG_RESULT_SUCCESS", "GTK_DRAG_RESULT_NO_TARGET",
+ "GTK_DRAG_RESULT_USER_CANCELLED", "GTK_DRAG_RESULT_TIMEOUT_EXPIRED",
+ "GTK_DRAG_RESULT_GRAB_BROKEN", "GTK_DRAG_RESULT_ERROR"};
+
+static void invisibleSourceDragBegin(GtkWidget* aWidget,
+ GdkDragContext* aContext, gpointer aData);
+
+static void invisibleSourceDragEnd(GtkWidget* aWidget, GdkDragContext* aContext,
+ gpointer aData);
+
+static gboolean invisibleSourceDragFailed(GtkWidget* aWidget,
+ GdkDragContext* aContext,
+ gint aResult, gpointer aData);
+
+static void invisibleSourceDragDataGet(GtkWidget* aWidget,
+ GdkDragContext* aContext,
+ GtkSelectionData* aSelectionData,
+ guint aInfo, guint32 aTime,
+ gpointer aData);
+
+nsDragService::nsDragService()
+ : mScheduledTask(eDragTaskNone),
+ mTaskSource(0),
+ mScheduledTaskIsRunning(false),
+ mCachedDragContext() {
+ // We have to destroy the hidden widget before the event loop stops
+ // running.
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ obsServ->AddObserver(this, "quit-application", false);
+
+ // our hidden source widget
+ // Using an offscreen window works around bug 983843.
+ mHiddenWidget = gtk_offscreen_window_new();
+ // make sure that the widget is realized so that
+ // we can use it as a drag source.
+ gtk_widget_realize(mHiddenWidget);
+ // hook up our internal signals so that we can get some feedback
+ // from our drag source
+ g_signal_connect(mHiddenWidget, "drag_begin",
+ G_CALLBACK(invisibleSourceDragBegin), this);
+ g_signal_connect(mHiddenWidget, "drag_data_get",
+ G_CALLBACK(invisibleSourceDragDataGet), this);
+ g_signal_connect(mHiddenWidget, "drag_end",
+ G_CALLBACK(invisibleSourceDragEnd), this);
+ // drag-failed is available from GTK+ version 2.12
+ guint dragFailedID =
+ g_signal_lookup("drag-failed", G_TYPE_FROM_INSTANCE(mHiddenWidget));
+ if (dragFailedID) {
+ g_signal_connect_closure_by_id(
+ mHiddenWidget, dragFailedID, 0,
+ g_cclosure_new(G_CALLBACK(invisibleSourceDragFailed), this, nullptr),
+ FALSE);
+ }
+
+ // set up our logging module
+ mCanDrop = false;
+ mTargetDragDataReceived = false;
+ mTargetDragUris = nullptr;
+ mTargetDragData = 0;
+ mTargetDragDataLen = 0;
+ mTempFileTimerID = 0;
+ mEventLoopDepth = 0;
+ LOGDRAGSERVICE("nsDragService::nsDragService");
+}
+
+nsDragService::~nsDragService() {
+ LOGDRAGSERVICE("nsDragService::~nsDragService");
+ if (mTaskSource) g_source_remove(mTaskSource);
+ if (mTempFileTimerID) {
+ g_source_remove(mTempFileTimerID);
+ RemoveTempFiles();
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsDragService, nsBaseDragService, nsIObserver)
+
+mozilla::StaticRefPtr<nsDragService> sDragServiceInstance;
+/* static */
+already_AddRefed<nsDragService> nsDragService::GetInstance() {
+ if (gfxPlatform::IsHeadless()) {
+ return nullptr;
+ }
+ if (!sDragServiceInstance) {
+ sDragServiceInstance = new nsDragService();
+ ClearOnShutdown(&sDragServiceInstance);
+ }
+
+ RefPtr<nsDragService> service = sDragServiceInstance.get();
+ return service.forget();
+}
+
+// nsIObserver
+
+NS_IMETHODIMP
+nsDragService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!nsCRT::strcmp(aTopic, "quit-application")) {
+ LOGDRAGSERVICE("nsDragService::Observe(\"quit-application\")");
+ if (mHiddenWidget) {
+ gtk_widget_destroy(mHiddenWidget);
+ mHiddenWidget = 0;
+ }
+ TargetResetData();
+ } else {
+ MOZ_ASSERT_UNREACHABLE("unexpected topic");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+// Support for periodic drag events
+
+// http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model
+// and the Xdnd protocol both recommend that drag events are sent periodically,
+// but GTK does not normally provide this.
+//
+// Here GTK is periodically stimulated by copies of the most recent mouse
+// motion events so as to send drag position messages to the destination when
+// appropriate (after it has received a status event from the previous
+// message).
+//
+// (If events were sent only on the destination side then the destination
+// would have no message to which it could reply with a drag status. Without
+// sending a drag status to the source, the destination would not be able to
+// change its feedback re whether it could accept the drop, and so the
+// source's behavior on drop will not be consistent.)
+
+static gboolean DispatchMotionEventCopy(gpointer aData) {
+ // Clear the timer id before OnSourceGrabEventAfter is called during event
+ // dispatch.
+ sMotionEventTimerID = 0;
+
+ GUniquePtr<GdkEvent> event = TakeMotionEvent();
+ // If there is no longer a grab on the widget, then the drag is over and
+ // there is no need to continue drag motion.
+ if (gtk_widget_has_grab(sGrabWidget)) {
+ gtk_propagate_event(sGrabWidget, event.get());
+ }
+
+ // Cancel this timer;
+ // We've already started another if the motion event was dispatched.
+ return FALSE;
+}
+
+static void OnSourceGrabEventAfter(GtkWidget* widget, GdkEvent* event,
+ gpointer user_data) {
+ // If there is no longer a grab on the widget, then the drag motion is
+ // over (though the data may not be fetched yet).
+ if (!gtk_widget_has_grab(sGrabWidget)) return;
+
+ if (event->type == GDK_MOTION_NOTIFY) {
+ SetMotionEvent(GUniquePtr<GdkEvent>(gdk_event_copy(event)));
+
+ // Update the cursor position. The last of these recorded gets used for
+ // the eDragEnd event.
+ nsDragService* dragService = static_cast<nsDragService*>(user_data);
+ gint scale = mozilla::widget::ScreenHelperGTK::GetGTKMonitorScaleFactor();
+ auto p = LayoutDeviceIntPoint::Round(event->motion.x_root * scale,
+ event->motion.y_root * scale);
+ dragService->SetDragEndPoint(p);
+ } else if (sMotionEvent &&
+ (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) {
+ // Update modifier state from key events.
+ sMotionEvent->motion.state = event->key.state;
+ } else {
+ return;
+ }
+
+ if (sMotionEventTimerID) {
+ g_source_remove(sMotionEventTimerID);
+ }
+
+ // G_PRIORITY_DEFAULT_IDLE is lower priority than GDK's redraw idle source
+ // and lower than GTK's idle source that sends drag position messages after
+ // motion-notify signals.
+ //
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model
+ // recommends an interval of 350ms +/- 200ms.
+ sMotionEventTimerID = g_timeout_add_full(
+ G_PRIORITY_DEFAULT_IDLE, 350, DispatchMotionEventCopy, nullptr, nullptr);
+}
+
+static GtkWindow* GetGtkWindow(dom::Document* aDocument) {
+ if (!aDocument) return nullptr;
+
+ PresShell* presShell = aDocument->GetPresShell();
+ if (!presShell) {
+ return nullptr;
+ }
+
+ RefPtr<nsViewManager> vm = presShell->GetViewManager();
+ if (!vm) return nullptr;
+
+ nsCOMPtr<nsIWidget> widget = vm->GetRootWidget();
+ if (!widget) return nullptr;
+
+ GtkWidget* gtkWidget = static_cast<nsWindow*>(widget.get())->GetGtkWidget();
+ if (!gtkWidget) return nullptr;
+
+ GtkWidget* toplevel = nullptr;
+ toplevel = gtk_widget_get_toplevel(gtkWidget);
+ if (!GTK_IS_WINDOW(toplevel)) return nullptr;
+
+ return GTK_WINDOW(toplevel);
+}
+
+// nsIDragService
+
+NS_IMETHODIMP
+nsDragService::InvokeDragSession(
+ nsINode* aDOMNode, nsIPrincipal* aPrincipal, nsIContentSecurityPolicy* aCsp,
+ nsICookieJarSettings* aCookieJarSettings, nsIArray* aArrayTransferables,
+ uint32_t aActionType,
+ nsContentPolicyType aContentPolicyType = nsIContentPolicy::TYPE_OTHER) {
+ LOGDRAGSERVICE("nsDragService::InvokeDragSession");
+
+ // If the previous source drag has not yet completed, signal handlers need
+ // to be removed from sGrabWidget and dragend needs to be dispatched to
+ // the source node, but we can't call EndDragSession yet because we don't
+ // know whether or not the drag succeeded.
+ if (mSourceNode) return NS_ERROR_NOT_AVAILABLE;
+
+ return nsBaseDragService::InvokeDragSession(
+ aDOMNode, aPrincipal, aCsp, aCookieJarSettings, aArrayTransferables,
+ aActionType, aContentPolicyType);
+}
+
+// nsBaseDragService
+nsresult nsDragService::InvokeDragSessionImpl(
+ nsIArray* aArrayTransferables, const Maybe<CSSIntRegion>& aRegion,
+ uint32_t aActionType) {
+ // make sure that we have an array of transferables to use
+ if (!aArrayTransferables) return NS_ERROR_INVALID_ARG;
+ // set our reference to the transferables. this will also addref
+ // the transferables since we're going to hang onto this beyond the
+ // length of this call
+ mSourceDataItems = aArrayTransferables;
+
+ LOGDRAGSERVICE("nsDragService::InvokeDragSessionImpl");
+
+ GdkDevice* device = widget::GdkGetPointer();
+ GdkWindow* originGdkWindow = nullptr;
+ if (widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol()) {
+ originGdkWindow =
+ gdk_device_get_window_at_position(device, nullptr, nullptr);
+ // Workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/6385
+ // Check we have GdkWindow drag source.
+ if (!originGdkWindow) {
+ NS_WARNING(
+ "nsDragService::InvokeDragSessionImpl(): Missing origin GdkWindow!");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // get the list of items we offer for drags
+ GtkTargetList* sourceList = GetSourceList();
+
+ if (!sourceList) return NS_OK;
+
+ // save our action type
+ GdkDragAction action = GDK_ACTION_DEFAULT;
+
+ if (aActionType & DRAGDROP_ACTION_COPY)
+ action = (GdkDragAction)(action | GDK_ACTION_COPY);
+ if (aActionType & DRAGDROP_ACTION_MOVE)
+ action = (GdkDragAction)(action | GDK_ACTION_MOVE);
+ if (aActionType & DRAGDROP_ACTION_LINK)
+ action = (GdkDragAction)(action | GDK_ACTION_LINK);
+
+ GdkEvent* existingEvent = widget::GetLastMousePressEvent();
+ GdkEvent fakeEvent;
+ if (!existingEvent) {
+ // Create a fake event for the drag so we can pass the time (so to speak).
+ // If we don't do this, then, when the timestamp for the pending button
+ // release event is used for the ungrab, the ungrab can fail due to the
+ // timestamp being _earlier_ than CurrentTime.
+ memset(&fakeEvent, 0, sizeof(GdkEvent));
+ fakeEvent.type = GDK_BUTTON_PRESS;
+ fakeEvent.button.window = gtk_widget_get_window(mHiddenWidget);
+ fakeEvent.button.time = nsWindow::GetLastUserInputTime();
+ fakeEvent.button.device = device;
+ }
+
+ // Put the drag widget in the window group of the source node so that the
+ // gtk_grab_add during gtk_drag_begin is effective.
+ // gtk_window_get_group(nullptr) returns the default window group.
+ GtkWindowGroup* window_group =
+ gtk_window_get_group(GetGtkWindow(mSourceDocument));
+ gtk_window_group_add_window(window_group, GTK_WINDOW(mHiddenWidget));
+
+ // start our drag.
+ GdkDragContext* context = gtk_drag_begin_with_coordinates(
+ mHiddenWidget, sourceList, action, 1,
+ existingEvent ? existingEvent : &fakeEvent, -1, -1);
+
+ if (originGdkWindow) {
+ mSourceWindow = nsWindow::GetWindow(originGdkWindow);
+ if (mSourceWindow) {
+ mSourceWindow->SetDragSource(context);
+ }
+ }
+
+ LOGDRAGSERVICE(" GdkDragContext [%p] nsWindow [%p]", context,
+ mSourceWindow.get());
+
+ nsresult rv;
+ if (context) {
+ StartDragSession();
+
+ // GTK uses another hidden window for receiving mouse events.
+ sGrabWidget = gtk_window_group_get_current_grab(window_group);
+ if (sGrabWidget) {
+ g_object_ref(sGrabWidget);
+ // Only motion and key events are required but connect to
+ // "event-after" as this is never blocked by other handlers.
+ g_signal_connect(sGrabWidget, "event-after",
+ G_CALLBACK(OnSourceGrabEventAfter), this);
+ }
+ // We don't have a drag end point yet.
+ mEndDragPoint = LayoutDeviceIntPoint(-1, -1);
+ rv = NS_OK;
+ } else {
+ rv = NS_ERROR_FAILURE;
+ }
+
+ gtk_target_list_unref(sourceList);
+
+ return rv;
+}
+
+bool nsDragService::SetAlphaPixmap(SourceSurface* aSurface,
+ GdkDragContext* aContext, int32_t aXOffset,
+ int32_t aYOffset,
+ const LayoutDeviceIntRect& dragRect) {
+ GdkScreen* screen = gtk_widget_get_screen(mHiddenWidget);
+
+ // Transparent drag icons need, like a lot of transparency-related things,
+ // a compositing X window manager
+ if (!gdk_screen_is_composited(screen)) {
+ return false;
+ }
+
+#ifdef cairo_image_surface_create
+# error "Looks like we're including Mozilla's cairo instead of system cairo"
+#endif
+
+ // TODO: grab X11 pixmap or image data instead of expensive readback.
+ cairo_surface_t* surf = cairo_image_surface_create(
+ CAIRO_FORMAT_ARGB32, dragRect.width, dragRect.height);
+ if (!surf) return false;
+
+ RefPtr<DrawTarget> dt = gfxPlatform::CreateDrawTargetForData(
+ cairo_image_surface_get_data(surf),
+ nsIntSize(dragRect.width, dragRect.height),
+ cairo_image_surface_get_stride(surf), SurfaceFormat::B8G8R8A8);
+ if (!dt) return false;
+
+ dt->ClearRect(Rect(0, 0, dragRect.width, dragRect.height));
+ dt->DrawSurface(
+ aSurface, Rect(0, 0, dragRect.width, dragRect.height),
+ Rect(0, 0, dragRect.width, dragRect.height), DrawSurfaceOptions(),
+ DrawOptions(DRAG_IMAGE_ALPHA_LEVEL, CompositionOp::OP_SOURCE));
+
+ cairo_surface_mark_dirty(surf);
+ cairo_surface_set_device_offset(surf, -aXOffset, -aYOffset);
+
+ // Ensure that the surface is drawn at the correct scale on HiDPI displays.
+ static auto sCairoSurfaceSetDeviceScalePtr =
+ (void (*)(cairo_surface_t*, double, double))dlsym(
+ RTLD_DEFAULT, "cairo_surface_set_device_scale");
+ if (sCairoSurfaceSetDeviceScalePtr) {
+ gint scale = mozilla::widget::ScreenHelperGTK::GetGTKMonitorScaleFactor();
+ sCairoSurfaceSetDeviceScalePtr(surf, scale, scale);
+ }
+
+ gtk_drag_set_icon_surface(aContext, surf);
+ cairo_surface_destroy(surf);
+ return true;
+}
+
+NS_IMETHODIMP
+nsDragService::StartDragSession() {
+ LOGDRAGSERVICE("nsDragService::StartDragSession");
+ mTempFileUrls.Clear();
+ return nsBaseDragService::StartDragSession();
+}
+
+bool nsDragService::RemoveTempFiles() {
+ LOGDRAGSERVICE("nsDragService::RemoveTempFiles");
+
+ // We can not delete the temporary files immediately after the
+ // drag has finished, because the target application might have not
+ // copied the temporary file yet. The Qt toolkit does not provide a
+ // way to mark a drop as finished in an asynchronous way, so most
+ // Qt based applications do send the dnd_finished signal before they
+ // have actually accessed the data from the temporary file.
+ // (https://bugreports.qt.io/browse/QTBUG-5441)
+ //
+ // To work also with these applications we collect all temporary
+ // files in mTemporaryFiles array and remove them here in the timer event.
+ auto files = std::move(mTemporaryFiles);
+ for (nsIFile* file : files) {
+#ifdef MOZ_LOGGING
+ if (MOZ_LOG_TEST(gWidgetDragLog, LogLevel::Debug)) {
+ nsAutoCString path;
+ if (NS_SUCCEEDED(file->GetNativePath(path))) {
+ LOGDRAGSERVICE(" removing %s", path.get());
+ }
+ }
+#endif
+ file->Remove(/* recursive = */ true);
+ }
+ MOZ_ASSERT(mTemporaryFiles.IsEmpty());
+ mTempFileTimerID = 0;
+ // Return false to remove the timer added by g_timeout_add_full().
+ return false;
+}
+
+gboolean nsDragService::TaskRemoveTempFiles(gpointer data) {
+ RefPtr<nsDragService> dragService = static_cast<nsDragService*>(data);
+ return dragService->RemoveTempFiles();
+}
+
+NS_IMETHODIMP
+nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
+ LOGDRAGSERVICE("nsDragService::EndDragSession(%p) %d",
+ mTargetDragContext.get(), aDoneDrag);
+
+ if (sGrabWidget) {
+ g_signal_handlers_disconnect_by_func(
+ sGrabWidget, FuncToGpointer(OnSourceGrabEventAfter), this);
+ g_object_unref(sGrabWidget);
+ sGrabWidget = nullptr;
+
+ if (sMotionEventTimerID) {
+ g_source_remove(sMotionEventTimerID);
+ sMotionEventTimerID = 0;
+ }
+ if (sMotionEvent) {
+ TakeMotionEvent();
+ }
+ }
+
+ // unset our drag action
+ SetDragAction(DRAGDROP_ACTION_NONE);
+
+ // start timer to remove temporary files
+ if (mTemporaryFiles.Count() > 0 && !mTempFileTimerID) {
+ LOGDRAGSERVICE(" queue removing of temporary files");
+ // |this| won't be used after nsDragService delete because the timer is
+ // removed in the nsDragService destructor.
+ mTempFileTimerID =
+ g_timeout_add(NS_DND_TMP_CLEANUP_TIMEOUT, TaskRemoveTempFiles, this);
+ mTempFileUrls.Clear();
+ }
+
+ // We're done with the drag context.
+ if (mSourceWindow) {
+ mSourceWindow->SetDragSource(nullptr);
+ mSourceWindow = nullptr;
+ }
+ mTargetDragContextForRemote = nullptr;
+ mTargetWindow = nullptr;
+ mPendingWindow = nullptr;
+ mCachedDragContext = 0;
+
+ return nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers);
+}
+
+// nsIDragSession
+NS_IMETHODIMP
+nsDragService::SetCanDrop(bool aCanDrop) {
+ LOGDRAGSERVICE("nsDragService::SetCanDrop %d", aCanDrop);
+ mCanDrop = aCanDrop;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDragService::GetCanDrop(bool* aCanDrop) {
+ LOGDRAGSERVICE("nsDragService::GetCanDrop");
+ *aCanDrop = mCanDrop;
+ return NS_OK;
+}
+
+static void UTF16ToNewUTF8(const char16_t* aUTF16, uint32_t aUTF16Len,
+ char** aUTF8, uint32_t* aUTF8Len) {
+ nsDependentSubstring utf16(aUTF16, aUTF16Len);
+ *aUTF8 = ToNewUTF8String(utf16, aUTF8Len);
+}
+
+static void UTF8ToNewUTF16(const char* aUTF8, uint32_t aUTF8Len,
+ char16_t** aUTF16, uint32_t* aUTF16Len) {
+ nsDependentCSubstring utf8(aUTF8, aUTF8Len);
+ *aUTF16 = UTF8ToNewUnicode(utf8, aUTF16Len);
+}
+
+// extract an item from text/uri-list formatted data and convert it to
+// unicode.
+static void GetTextUriListItem(const char* data, uint32_t datalen,
+ uint32_t aItemIndex, char16_t** convertedText,
+ uint32_t* convertedTextLen) {
+ const char* p = data;
+ const char* endPtr = p + datalen;
+ unsigned int count = 0;
+
+ *convertedText = nullptr;
+ while (p < endPtr) {
+ // skip whitespace (if any)
+ while (p < endPtr && *p != '\0' && isspace(*p)) p++;
+ // if we aren't at the end of the line, we have a url
+ if (p != endPtr && *p != '\0' && *p != '\n' && *p != '\r') count++;
+ // this is the item we are after ...
+ if (aItemIndex + 1 == count) {
+ const char* q = p;
+ while (q < endPtr && *q != '\0' && *q != '\n' && *q != '\r') q++;
+ UTF8ToNewUTF16(p, q - p, convertedText, convertedTextLen);
+ break;
+ }
+ // skip to the end of the line
+ while (p < endPtr && *p != '\0' && *p != '\n') p++;
+ p++; // skip the actual newline as well.
+ }
+
+ // didn't find the desired item, so just pass the whole lot
+ if (!*convertedText) {
+ UTF8ToNewUTF16(data, datalen, convertedText, convertedTextLen);
+ }
+}
+
+// Spins event loop, called from JS.
+// Can lead to another round of drag_motion events.
+NS_IMETHODIMP
+nsDragService::GetNumDropItems(uint32_t* aNumItems) {
+ LOGDRAGSERVICE("nsDragService::GetNumDropItems");
+
+ if (!mTargetWidget) {
+ LOGDRAGSERVICE(
+ "*** warning: GetNumDropItems \
+ called without a valid target widget!\n");
+ *aNumItems = 0;
+ return NS_OK;
+ }
+
+ bool isList = IsTargetContextList();
+ if (isList) {
+ if (!mSourceDataItems) {
+ *aNumItems = 0;
+ return NS_OK;
+ }
+ mSourceDataItems->GetLength(aNumItems);
+ } else {
+ // text/uri-list
+ GdkAtom gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE);
+ if (!gdkFlavor) {
+ *aNumItems = 0;
+ return NS_OK;
+ }
+
+ nsTArray<nsCString> dragFlavors;
+ GetDragFlavors(dragFlavors);
+ GetTargetDragData(gdkFlavor, dragFlavors);
+
+ // application/vnd.portal.files
+ if (!mTargetDragUris) {
+ gdkFlavor = gdk_atom_intern(gPortalFile, FALSE);
+ if (!gdkFlavor) {
+ *aNumItems = 0;
+ return NS_OK;
+ }
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ }
+
+ // application/vnd.portal.filetransfer
+ if (!mTargetDragUris) {
+ gdkFlavor = gdk_atom_intern(gPortalFileTransfer, FALSE);
+ if (!gdkFlavor) {
+ *aNumItems = 0;
+ return NS_OK;
+ }
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ }
+
+ if (mTargetDragUris) {
+ *aNumItems = g_strv_length(mTargetDragUris.get());
+ } else
+ *aNumItems = 1;
+ }
+ LOGDRAGSERVICE(" NumOfDropItems %d", *aNumItems);
+ return NS_OK;
+}
+
+void nsDragService::GetDragFlavors(nsTArray<nsCString>& aFlavors) {
+ for (GList* tmp = gdk_drag_context_list_targets(mTargetDragContext); tmp;
+ tmp = tmp->next) {
+ GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data);
+ GUniquePtr<gchar> name(gdk_atom_name(atom));
+ if (!name) {
+ continue;
+ }
+ aFlavors.AppendElement(nsCString(name.get()));
+ }
+}
+
+static nsresult GetFileFromUri(const nsCString& aUri,
+ nsCOMPtr<nsIFile>& aFile) {
+ nsresult rv;
+ nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
+ nsCOMPtr<nsIURI> fileURI;
+ rv = ioService->NewURI(aUri, nullptr, nullptr, getter_AddRefs(fileURI));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(fileURI, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = fileURL->GetFile(getter_AddRefs(aFile));
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+ }
+ }
+ return rv;
+}
+
+nsresult GetReachableFileFromUriList(char** aFileUriList, uint32_t aItemIndex,
+ nsCOMPtr<nsIFile>& aFile) {
+ if (!aFileUriList || !(g_strv_length(aFileUriList) > aItemIndex) ||
+ !aFileUriList[aItemIndex]) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult rv;
+ rv = GetFileFromUri(nsDependentCString(aFileUriList[aItemIndex]), aFile);
+ if (NS_SUCCEEDED(rv)) {
+ bool fileExists = false;
+ aFile->Exists(&fileExists);
+ if (fileExists) {
+ LOGDRAG(" good, file %s exists\n", aFileUriList[aItemIndex]);
+ return NS_OK;
+ }
+ }
+ LOGDRAG(" uri %s not reachable/not found\n", aFileUriList[aItemIndex]);
+ aFile = nullptr;
+ return NS_ERROR_FILE_NOT_FOUND;
+}
+
+// Spins event loop, called from JS.
+// Can lead to another round of drag_motion events.
+NS_IMETHODIMP
+nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) {
+ LOGDRAGSERVICE("nsDragService::GetData(), index %d", aItemIndex);
+
+ // make sure that we have a transferable
+ if (!aTransferable) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!mTargetWidget) {
+ LOGDRAGSERVICE(
+ "*** failed: GetData called without a valid target widget!\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ // get flavor list that includes all acceptable flavors (including
+ // ones obtained through conversion).
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" failed to get flavors, quit.");
+ return rv;
+ }
+
+ // check to see if this is an internal list
+ bool isList = IsTargetContextList();
+
+ if (isList) {
+ LOGDRAGSERVICE(" Process as a list...");
+ // find a matching flavor
+ for (uint32_t i = 0; i < flavors.Length(); ++i) {
+ nsCString& flavorStr = flavors[i];
+ LOGDRAGSERVICE(" [%d] flavor is %s\n", i, flavorStr.get());
+ // get the item with the right index
+ nsCOMPtr<nsITransferable> item =
+ do_QueryElementAt(mSourceDataItems, aItemIndex);
+ if (!item) continue;
+
+ nsCOMPtr<nsISupports> data;
+ LOGDRAGSERVICE(" trying to get transfer data for %s\n", flavorStr.get());
+ rv = item->GetTransferData(flavorStr.get(), getter_AddRefs(data));
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" failed.\n");
+ continue;
+ }
+ rv = aTransferable->SetTransferData(flavorStr.get(), data);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" fail to set transfer data into transferable!\n");
+ continue;
+ }
+ LOGDRAGSERVICE(" succeeded\n");
+ // ok, we got the data
+ return NS_OK;
+ }
+ // if we got this far, we failed
+ LOGDRAGSERVICE(" failed to match flavors\n");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<nsCString> dragFlavors;
+ GetDragFlavors(dragFlavors);
+
+ // Now walk down the list of flavors. When we find one that is
+ // actually present, copy out the data into the transferable in that
+ // format. SetTransferData() implicitly handles conversions.
+ for (uint32_t i = 0; i < flavors.Length(); ++i) {
+ nsCString& flavorStr = flavors[i];
+
+ GdkAtom gdkFlavor;
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ gdkFlavor = gdk_atom_intern(gTextPlainUTF8Type, FALSE);
+ } else {
+ gdkFlavor = gdk_atom_intern(flavorStr.get(), FALSE);
+ }
+ LOGDRAGSERVICE(" we're getting data %s (gdk flavor %p)\n", flavorStr.get(),
+ gdkFlavor);
+ bool dataFound = false;
+ nsCOMPtr<nsIFile> file;
+ if (gdkFlavor) {
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ GetReachableFileFromUriList(mTargetDragUris.get(), aItemIndex, file);
+ }
+
+ // application/vnd.portal.files
+ if (!file || !mTargetDragUris) {
+ LOGDRAGSERVICE(" file not found, proceed with %s flavor\n", gPortalFile);
+ gdkFlavor = gdk_atom_intern(gPortalFile, FALSE);
+ if (gdkFlavor) {
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ GetReachableFileFromUriList(mTargetDragUris.get(), aItemIndex, file);
+ }
+ }
+
+ // application/vnd.portal.filetransfer
+ if (!file || !mTargetDragUris) {
+ LOGDRAGSERVICE(" file not found, proceed with %s flavor\n",
+ gPortalFileTransfer);
+ gdkFlavor = gdk_atom_intern(gPortalFileTransfer, FALSE);
+ if (gdkFlavor) {
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ GetReachableFileFromUriList(mTargetDragUris.get(), aItemIndex, file);
+ }
+ }
+
+ // Conversion from application/x-moz-file to text/uri-list
+ if ((!file || !mTargetDragUris) && (flavorStr.EqualsLiteral(kFileMime))) {
+ LOGDRAGSERVICE(
+ " file not found, proceed with conversion %s => %s flavor\n",
+ kFileMime, gTextUriListType);
+
+ gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE);
+ if (gdkFlavor) {
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ GetReachableFileFromUriList(mTargetDragUris.get(), aItemIndex, file);
+ }
+ }
+
+ if (file) {
+ LOGDRAGSERVICE(" from drag uris set as file %s - flavor: %s",
+ mTargetDragUris.get()[aItemIndex], flavorStr.get());
+ aTransferable->SetTransferData(flavorStr.get(), file);
+ return NS_OK;
+ }
+
+ if (mTargetDragData) {
+ LOGDRAGSERVICE(" dataFound = true\n");
+ dataFound = true;
+ } else {
+ LOGDRAGSERVICE(" dataFound = false, try conversions\n");
+ // If we are looking for text/plain, try again with non utf-8 text.
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ LOGDRAGSERVICE(" conversion %s => %s", kTextMime, kTextMime);
+ gdkFlavor = gdk_atom_intern(kTextMime, FALSE);
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ if (mTargetDragData) {
+ dataFound = true;
+ } // if plain text flavor present
+ } // if looking for text/plain
+
+ // if we are looking for text/x-moz-url and we failed to find
+ // it on the clipboard, try again with text/uri-list, and then
+ // _NETSCAPE_URL
+ if (flavorStr.EqualsLiteral(kURLMime)) {
+ LOGDRAGSERVICE(" conversion %s => %s", kURLMime, gTextUriListType);
+ gdkFlavor = gdk_atom_intern(gTextUriListType, FALSE);
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ if (mTargetDragData) {
+ const char* data = reinterpret_cast<char*>(mTargetDragData);
+ char16_t* convertedText = nullptr;
+ uint32_t convertedTextLen = 0;
+
+ GetTextUriListItem(data, mTargetDragDataLen, aItemIndex,
+ &convertedText, &convertedTextLen);
+
+ if (convertedText) {
+ // out with the old, in with the new
+ g_free(mTargetDragData);
+ mTargetDragData = convertedText;
+ mTargetDragDataLen = convertedTextLen * 2;
+ dataFound = true;
+ }
+ }
+ if (!dataFound) {
+ LOGDRAGSERVICE(" conversion %s => %s", kURLMime, gMozUrlType);
+ gdkFlavor = gdk_atom_intern(gMozUrlType, FALSE);
+ GetTargetDragData(gdkFlavor, dragFlavors);
+ if (mTargetDragData) {
+ const char* castedText = reinterpret_cast<char*>(mTargetDragData);
+ char16_t* convertedText = nullptr;
+ uint32_t convertedTextLen = 0;
+ UTF8ToNewUTF16(castedText, mTargetDragDataLen, &convertedText,
+ &convertedTextLen);
+ if (convertedText) {
+ // out with the old, in with the new
+ g_free(mTargetDragData);
+ mTargetDragData = convertedText;
+ mTargetDragDataLen = convertedTextLen * 2;
+ dataFound = true;
+ }
+ }
+ }
+ }
+
+ } // else we try one last ditch effort to find our data
+
+ if (dataFound) {
+ LOGDRAGSERVICE(" actual data found %s\n",
+ GUniquePtr<gchar>(gdk_atom_name(gdkFlavor)).get());
+
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ // The text is in UTF-8, so convert the text into UTF-16
+ const char* text = static_cast<char*>(mTargetDragData);
+ NS_ConvertUTF8toUTF16 ucs2string(text, mTargetDragDataLen);
+ char16_t* convertedText = ToNewUnicode(ucs2string, mozilla::fallible);
+ if (convertedText) {
+ g_free(mTargetDragData);
+ mTargetDragData = convertedText;
+ mTargetDragDataLen = ucs2string.Length() * 2;
+ }
+ }
+
+ if (flavorStr.EqualsLiteral(kJPEGImageMime) ||
+ flavorStr.EqualsLiteral(kJPGImageMime) ||
+ flavorStr.EqualsLiteral(kPNGImageMime) ||
+ flavorStr.EqualsLiteral(kGIFImageMime)) {
+ LOGDRAGSERVICE(" saving as image %s\n", flavorStr.get());
+
+ nsCOMPtr<nsIInputStream> byteStream;
+ NS_NewByteInputStream(getter_AddRefs(byteStream),
+ Span((char*)mTargetDragData, mTargetDragDataLen),
+ NS_ASSIGNMENT_COPY);
+ aTransferable->SetTransferData(flavorStr.get(), byteStream);
+ continue;
+ }
+
+ if (!flavorStr.EqualsLiteral(kCustomTypesMime)) {
+ // the DOM only wants LF, so convert from MacOS line endings
+ // to DOM line endings.
+ nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(
+ flavorStr.EqualsLiteral(kRTFMime), &mTargetDragData,
+ reinterpret_cast<int*>(&mTargetDragDataLen));
+ }
+
+ // put it into the transferable.
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ flavorStr, mTargetDragData, mTargetDragDataLen,
+ getter_AddRefs(genericDataWrapper));
+ aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
+ // we found one, get out of this loop!
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) {
+ LOGDRAGSERVICE("nsDragService::IsDataFlavorSupported(%p) %s",
+ mTargetDragContext.get(), aDataFlavor);
+ if (!_retval) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // set this to no by default
+ *_retval = false;
+
+ // check to make sure that we have a drag object set, here
+ if (!mTargetWidget) {
+ LOGDRAGSERVICE(
+ "*** warning: IsDataFlavorSupported called without a valid target "
+ "widget!\n");
+ return NS_OK;
+ }
+
+ // check to see if the target context is a list.
+ bool isList = IsTargetContextList();
+ // if it is, just look in the internal data since we are the source
+ // for it.
+ if (isList) {
+ LOGDRAGSERVICE(" It's a list");
+ uint32_t numDragItems = 0;
+ // if we don't have mDataItems we didn't start this drag so it's
+ // an external client trying to fool us.
+ if (!mSourceDataItems) {
+ LOGDRAGSERVICE(" quit");
+ return NS_OK;
+ }
+ mSourceDataItems->GetLength(&numDragItems);
+ LOGDRAGSERVICE(" drag items %d", numDragItems);
+ for (uint32_t itemIndex = 0; itemIndex < numDragItems; ++itemIndex) {
+ nsCOMPtr<nsITransferable> currItem =
+ do_QueryElementAt(mSourceDataItems, itemIndex);
+ if (currItem) {
+ nsTArray<nsCString> flavors;
+ currItem->FlavorsTransferableCanExport(flavors);
+ for (uint32_t i = 0; i < flavors.Length(); ++i) {
+ LOGDRAGSERVICE(" checking %s against %s\n", flavors[i].get(),
+ aDataFlavor);
+ if (flavors[i].Equals(aDataFlavor)) {
+ LOGDRAGSERVICE(" found.\n");
+ *_retval = true;
+ }
+ }
+ }
+ }
+ return NS_OK;
+ }
+
+ // check the target context vs. this flavor, one at a time
+ GList* tmp = nullptr;
+ if (mTargetDragContext) {
+ tmp = gdk_drag_context_list_targets(mTargetDragContext);
+ }
+
+ for (; tmp; tmp = tmp->next) {
+ /* Bug 331198 */
+ GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data);
+ GUniquePtr<gchar> name(gdk_atom_name(atom));
+ if (!name) {
+ continue;
+ }
+
+ if (strcmp(name.get(), aDataFlavor) == 0) {
+ *_retval = true;
+ }
+ // check for automatic text/uri-list -> text/x-moz-url mapping
+ else if (strcmp(name.get(), gTextUriListType) == 0 &&
+ (strcmp(aDataFlavor, kURLMime) == 0 ||
+ strcmp(aDataFlavor, kFileMime) == 0)) {
+ *_retval = true;
+ }
+ // check for automatic _NETSCAPE_URL -> text/x-moz-url mapping
+ else if (strcmp(name.get(), gMozUrlType) == 0 &&
+ (strcmp(aDataFlavor, kURLMime) == 0)) {
+ *_retval = true;
+ }
+ // check for file portal
+ // If we're asked for kURLMime/kFileMime we can convert gPortalFile
+ // or gPortalFileTransfer to it.
+ else if ((strcmp(name.get(), gPortalFile) == 0 ||
+ strcmp(name.get(), gPortalFileTransfer) == 0) &&
+ (strcmp(aDataFlavor, kURLMime) == 0 ||
+ strcmp(aDataFlavor, kFileMime) == 0)) {
+ *_retval = true;
+ }
+ if (*_retval) {
+ LOGDRAGSERVICE(" supported, with converting %s => %s", name.get(),
+ aDataFlavor);
+ }
+ }
+
+ if (!*_retval) {
+ LOGDRAGSERVICE(" %s is not supported", aDataFlavor);
+ }
+
+ return NS_OK;
+}
+
+void nsDragService::ReplyToDragMotion(GdkDragContext* aDragContext,
+ guint aTime) {
+ LOGDRAGSERVICE("nsDragService::ReplyToDragMotion(%p) can drop %d",
+ aDragContext, mCanDrop);
+
+ GdkDragAction action = (GdkDragAction)0;
+ if (mCanDrop) {
+ // notify the dragger if we can drop
+ switch (mDragAction) {
+ case DRAGDROP_ACTION_COPY:
+ LOGDRAGSERVICE(" set explicit action copy");
+ action = GDK_ACTION_COPY;
+ break;
+ case DRAGDROP_ACTION_LINK:
+ LOGDRAGSERVICE(" set explicit action link");
+ action = GDK_ACTION_LINK;
+ break;
+ case DRAGDROP_ACTION_NONE:
+ LOGDRAGSERVICE(" set explicit action none");
+ action = (GdkDragAction)0;
+ break;
+ default:
+ LOGDRAGSERVICE(" set explicit action move");
+ action = GDK_ACTION_MOVE;
+ break;
+ }
+ } else {
+ LOGDRAGSERVICE(" mCanDrop is false, disable drop");
+ }
+
+ // gdk_drag_status() is a kind of red herring here.
+ // It does not control final D&D operation type (copy/move) but controls
+ // drop/no-drop D&D state and default cursor type (copy/move).
+
+ // Actual D&D operation is determined by mDragAction which is set by
+ // SetDragAction() from UpdateDragAction() or gecko/layout.
+
+ // State passed to gdk_drag_status() sets default D&D cursor type
+ // which can be switched by key control (CTRL/SHIFT).
+ // If user changes D&D cursor (and D&D operation) we're notified by
+ // gdk_drag_context_get_selected_action() and update mDragAction.
+
+ // But if we pass mDragAction back to gdk_drag_status() the D&D operation
+ // becames locked and won't be returned when D&D modifiers (CTRL/SHIFT)
+ // are released.
+
+ // This gdk_drag_context_get_selected_action() -> gdk_drag_status() ->
+ // gdk_drag_context_get_selected_action() cycle happens on Wayland.
+ if (widget::GdkIsWaylandDisplay() && action == GDK_ACTION_COPY) {
+ LOGDRAGSERVICE(" Wayland: switch copy to move");
+ action = GDK_ACTION_MOVE;
+ }
+
+ LOGDRAGSERVICE(" gdk_drag_status() action %d", action);
+ gdk_drag_status(aDragContext, action, aTime);
+}
+
+void nsDragService::EnsureCachedDataValidForContext(
+ GdkDragContext* aDragContext) {
+ if (mCachedDragContext != (uintptr_t)aDragContext) {
+ mCachedUris.Clear();
+ mCachedData.Clear();
+ mCachedDragContext = (uintptr_t)aDragContext;
+ }
+}
+
+void nsDragService::TargetDataReceived(GtkWidget* aWidget,
+ GdkDragContext* aContext, gint aX,
+ gint aY,
+ GtkSelectionData* aSelectionData,
+ guint aInfo, guint32 aTime) {
+ LOGDRAGSERVICE("nsDragService::TargetDataReceived(%p)", aContext);
+ TargetResetData();
+
+ EnsureCachedDataValidForContext(aContext);
+
+ mTargetDragDataReceived = true;
+
+ GdkAtom target = gtk_selection_data_get_target(aSelectionData);
+ GUniquePtr<gchar> name(gdk_atom_name(target));
+ nsDependentCString flavor(name.get());
+
+ if (gtk_targets_include_uri(&target, 1)) {
+ GUniquePtr<gchar*> uris(gtk_selection_data_get_uris(aSelectionData));
+#ifdef MOZ_LOGGING
+ if (MOZ_LOG_TEST(gWidgetDragLog, mozilla::LogLevel::Debug)) {
+ gchar** uri = uris.get();
+ while (uri && *uri) {
+ LOGDRAGSERVICE(" got uri %s, MIME %s", *uri, flavor.get());
+ uri++;
+ }
+ }
+#endif
+ uris.swap(mTargetDragUris);
+ if (mTargetDragUris) {
+ mCachedUris.InsertOrUpdate(
+ flavor, GUniquePtr<gchar*>(g_strdupv(mTargetDragUris.get())));
+ } else {
+ LOGDRAGSERVICE("Failed to get uri list\n");
+ mCachedUris.InsertOrUpdate(flavor, GUniquePtr<gchar*>(nullptr));
+ }
+ return;
+ }
+
+ const guchar* data = gtk_selection_data_get_data(aSelectionData);
+ gint len = gtk_selection_data_get_length(aSelectionData);
+ if (len > 0 && data) {
+ mTargetDragDataLen = len;
+ mTargetDragData = g_malloc(mTargetDragDataLen);
+ memcpy(mTargetDragData, data, mTargetDragDataLen);
+
+ LOGDRAGSERVICE(" got data, len = %d", mTargetDragDataLen);
+
+ nsTArray<uint8_t> copy;
+ if (!copy.SetLength(len, fallible)) {
+ return;
+ }
+ memcpy(copy.Elements(), data, len);
+
+ mCachedData.InsertOrUpdate(flavor, std::move(copy));
+ } else {
+ LOGDRAGSERVICE("Failed to get data. selection data len was %d\n",
+ mTargetDragDataLen);
+ mCachedData.InsertOrUpdate(flavor, nsTArray<uint8_t>());
+ }
+ LOGDRAGSERVICE(" got data, MIME %s", flavor.get());
+}
+
+bool nsDragService::IsTargetContextList(void) {
+ bool retval = false;
+
+ // gMimeListType drags only work for drags within a single process. The
+ // gtk_drag_get_source_widget() function will return nullptr if the source
+ // of the drag is another app, so we use it to check if a gMimeListType
+ // drop will work or not.
+ if (mTargetDragContext &&
+ gtk_drag_get_source_widget(mTargetDragContext) == nullptr) {
+ return retval;
+ }
+
+ GList* tmp = nullptr;
+ if (mTargetDragContext) {
+ tmp = gdk_drag_context_list_targets(mTargetDragContext);
+ }
+
+ // walk the list of context targets and see if one of them is a list
+ // of items.
+ for (; tmp; tmp = tmp->next) {
+ /* Bug 331198 */
+ GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data);
+ GUniquePtr<gchar> name(gdk_atom_name(atom));
+ if (name && !strcmp(name.get(), gMimeListType)) {
+ return true;
+ }
+ }
+
+ return retval;
+}
+
+// Spins event loop, called from eDragTaskMotion handler by
+// DispatchMotionEvents().
+// Can lead to another round of drag_motion events.
+void nsDragService::GetTargetDragData(GdkAtom aFlavor,
+ nsTArray<nsCString>& aDropFlavors) {
+ LOGDRAGSERVICE("nsDragService::GetTargetDragData(%p) '%s'\n",
+ mTargetDragContext.get(),
+ GUniquePtr<gchar>(gdk_atom_name(aFlavor)).get());
+
+ // reset our target data areas
+ TargetResetData();
+
+ GUniquePtr<gchar> name(gdk_atom_name(aFlavor));
+ nsDependentCString flavor(name.get());
+
+ // Return early when requested MIME is not offered by D&D.
+ if (!aDropFlavors.Contains(flavor)) {
+ LOGDRAGSERVICE(" %s is missing", flavor.get());
+ return;
+ }
+
+ if (mTargetDragContext) {
+ // We keep a copy of the requested data with the same life-time
+ // as mTargetDragContext.
+ // Especially with multiple items the same data is requested
+ // very often.
+ EnsureCachedDataValidForContext(mTargetDragContext);
+ if (auto cached = mCachedUris.Lookup(flavor)) {
+ LOGDRAGSERVICE(" using cached uri list for %s", flavor.get());
+
+ mTargetDragUris = GUniquePtr<gchar*>(g_strdupv(cached->get()));
+ mTargetDragDataReceived = true;
+ LOGDRAGSERVICE(" %s found in cache", flavor.get());
+ return;
+ }
+ if (auto cached = mCachedData.Lookup(flavor)) {
+ mTargetDragDataLen = cached->Length();
+ LOGDRAGSERVICE(" using cached data for %s, length is %d", flavor.get(),
+ mTargetDragDataLen);
+
+ if (mTargetDragDataLen) {
+ mTargetDragData = g_malloc(mTargetDragDataLen);
+ memcpy(mTargetDragData, cached->Elements(), mTargetDragDataLen);
+ }
+
+ mTargetDragDataReceived = true;
+ LOGDRAGSERVICE(" %s found in cache", flavor.get());
+ return;
+ }
+
+ gtk_drag_get_data(mTargetWidget, mTargetDragContext, aFlavor, mTargetTime);
+
+ LOGDRAGSERVICE(" about to start inner iteration.");
+ gtk_main_iteration();
+
+ PRTime entryTime = PR_Now();
+ while (!mTargetDragDataReceived && mDoingDrag) {
+ // check the number of iterations
+ LOGDRAGSERVICE(" doing iteration...\n");
+ PR_Sleep(PR_MillisecondsToInterval(10)); /* sleep for 10 ms/iteration */
+ if (PR_Now() - entryTime > NS_DND_TIMEOUT) {
+ LOGDRAGSERVICE(" failed to get D&D data in time!\n");
+ break;
+ }
+ gtk_main_iteration();
+ }
+ }
+
+#ifdef MOZ_LOGGING
+ if (mTargetDragUris || (mTargetDragDataLen && mTargetDragData)) {
+ LOGDRAGSERVICE(" %s got from system", flavor.get());
+ } else {
+ LOGDRAGSERVICE(" %s failed to get from system", flavor.get());
+ }
+#endif
+}
+
+void nsDragService::TargetResetData(void) {
+ mTargetDragDataReceived = false;
+ // make sure to free old data if we have to
+ mTargetDragUris = nullptr;
+ g_free(mTargetDragData);
+ mTargetDragData = 0;
+ mTargetDragDataLen = 0;
+}
+
+static void TargetArrayAddTarget(nsTArray<GtkTargetEntry*>& aTargetArray,
+ const char* aTarget) {
+ GtkTargetEntry* target = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry));
+ target->target = g_strdup(aTarget);
+ target->flags = 0;
+ aTargetArray.AppendElement(target);
+ LOGDRAGSERVICESTATIC("adding target %s\n", aTarget);
+}
+
+static bool CanExportAsURLTarget(const char16_t* aURLData, uint32_t aURLLen) {
+ for (const nsLiteralString& disallowed : kDisallowedExportedSchemes) {
+ auto len = disallowed.AsString().Length();
+ if (len < aURLLen) {
+ if (!memcmp(disallowed.get(), aURLData,
+ /* len is char16_t char count */ len * 2)) {
+ LOGDRAGSERVICESTATIC("rejected URL scheme %s\n",
+ NS_ConvertUTF16toUTF8(disallowed).get());
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+GtkTargetList* nsDragService::GetSourceList(void) {
+ if (!mSourceDataItems) {
+ return nullptr;
+ }
+
+ nsTArray<GtkTargetEntry*> targetArray;
+ GtkTargetEntry* targets;
+ GtkTargetList* targetList = 0;
+ uint32_t targetCount = 0;
+ unsigned int numDragItems = 0;
+
+ mSourceDataItems->GetLength(&numDragItems);
+ LOGDRAGSERVICE(" numDragItems = %d", numDragItems);
+
+ // Check to see if we're dragging > 1 item.
+ if (numDragItems > 1) {
+ // as the Xdnd protocol only supports a single item (or is it just
+ // gtk's implementation?), we don't advertise all flavours listed
+ // in the nsITransferable.
+
+ // the application/x-moz-internal-item-list format, which preserves
+ // all information for drags within the same mozilla instance.
+ TargetArrayAddTarget(targetArray, gMimeListType);
+
+ // check what flavours are supported so we can decide what other
+ // targets to advertise.
+ nsCOMPtr<nsITransferable> currItem = do_QueryElementAt(mSourceDataItems, 0);
+
+ if (currItem) {
+ nsTArray<nsCString> flavors;
+ currItem->FlavorsTransferableCanExport(flavors);
+ for (uint32_t i = 0; i < flavors.Length(); ++i) {
+ // check if text/x-moz-url is supported.
+ // If so, advertise
+ // text/uri-list.
+ if (flavors[i].EqualsLiteral(kURLMime)) {
+ TargetArrayAddTarget(targetArray, gTextUriListType);
+ break;
+ }
+ }
+ } // if item is a transferable
+ } else if (numDragItems == 1) {
+ nsCOMPtr<nsITransferable> currItem = do_QueryElementAt(mSourceDataItems, 0);
+ if (currItem) {
+ nsTArray<nsCString> flavors;
+ currItem->FlavorsTransferableCanExport(flavors);
+ for (uint32_t i = 0; i < flavors.Length(); ++i) {
+ nsCString& flavorStr = flavors[i];
+
+ TargetArrayAddTarget(targetArray, flavorStr.get());
+
+ // If there is a file, add the text/uri-list type.
+ if (flavorStr.EqualsLiteral(kFileMime)) {
+ TargetArrayAddTarget(targetArray, gTextUriListType);
+ }
+ // Check to see if this is text/plain.
+ else if (flavorStr.EqualsLiteral(kTextMime)) {
+ TargetArrayAddTarget(targetArray, gTextPlainUTF8Type);
+ }
+ // Check to see if this is the x-moz-url type.
+ // If it is, add _NETSCAPE_URL
+ // this is a type used by everybody.
+ else if (flavorStr.EqualsLiteral(kURLMime)) {
+ nsCOMPtr<nsISupports> data;
+ if (NS_SUCCEEDED(currItem->GetTransferData(flavorStr.get(),
+ getter_AddRefs(data)))) {
+ void* tmpData = nullptr;
+ uint32_t tmpDataLen = 0;
+ nsPrimitiveHelpers::CreateDataFromPrimitive(
+ nsDependentCString(flavorStr.get()), data, &tmpData,
+ &tmpDataLen);
+ if (tmpData) {
+ if (CanExportAsURLTarget(reinterpret_cast<char16_t*>(tmpData),
+ tmpDataLen / 2)) {
+ TargetArrayAddTarget(targetArray, gMozUrlType);
+ }
+ free(tmpData);
+ }
+ }
+ }
+ // check if application/x-moz-file-promise url is supported.
+ // If so, advertise text/uri-list.
+ else if (flavorStr.EqualsLiteral(kFilePromiseURLMime)) {
+ TargetArrayAddTarget(targetArray, gTextUriListType);
+ }
+ // XdndDirectSave, use on X.org only.
+ else if (widget::GdkIsX11Display() && !widget::IsXWaylandProtocol() &&
+ flavorStr.EqualsLiteral(kFilePromiseMime)) {
+ TargetArrayAddTarget(targetArray, gXdndDirectSaveType);
+ }
+ // kNativeImageMime
+ else if (flavorStr.EqualsLiteral(kNativeImageMime)) {
+ TargetArrayAddTarget(targetArray, kPNGImageMime);
+ TargetArrayAddTarget(targetArray, kJPEGImageMime);
+ TargetArrayAddTarget(targetArray, kJPGImageMime);
+ TargetArrayAddTarget(targetArray, kGIFImageMime);
+ }
+ }
+ }
+ }
+
+ // get all the elements that we created.
+ targetCount = targetArray.Length();
+ if (targetCount) {
+ // allocate space to create the list of valid targets
+ targets = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry) * targetCount);
+ uint32_t targetIndex;
+ for (targetIndex = 0; targetIndex < targetCount; ++targetIndex) {
+ GtkTargetEntry* disEntry = targetArray.ElementAt(targetIndex);
+ // this is a string reference but it will be freed later.
+ targets[targetIndex].target = disEntry->target;
+ targets[targetIndex].flags = disEntry->flags;
+ targets[targetIndex].info = 0;
+ }
+ targetList = gtk_target_list_new(targets, targetCount);
+ // clean up the target list
+ for (uint32_t cleanIndex = 0; cleanIndex < targetCount; ++cleanIndex) {
+ GtkTargetEntry* thisTarget = targetArray.ElementAt(cleanIndex);
+ g_free(thisTarget->target);
+ g_free(thisTarget);
+ }
+ g_free(targets);
+ } else {
+ // We need to create a dummy target list to be able initialize dnd.
+ targetList = gtk_target_list_new(nullptr, 0);
+ }
+ return targetList;
+}
+
+void nsDragService::SourceEndDragSession(GdkDragContext* aContext,
+ gint aResult) {
+ LOGDRAGSERVICE("SourceEndDragSession(%p) result %s\n", aContext,
+ kGtkDragResults[aResult]);
+
+ // this just releases the list of data items that we provide
+ mSourceDataItems = nullptr;
+
+ // Remove this property, if it exists, to satisfy the Direct Save Protocol.
+ GdkAtom property = gdk_atom_intern(gXdndDirectSaveType, FALSE);
+ gdk_property_delete(gdk_drag_context_get_source_window(aContext), property);
+
+ if (!mDoingDrag || mScheduledTask == eDragTaskSourceEnd)
+ // EndDragSession() was already called on drop
+ // or SourceEndDragSession on drag-failed
+ return;
+
+ if (mEndDragPoint.x < 0) {
+ // We don't have a drag end point, so guess
+ gint x, y;
+ GdkDisplay* display = gdk_display_get_default();
+ GdkScreen* screen = gdk_display_get_default_screen(display);
+ GtkWindow* window = GetGtkWindow(mSourceDocument);
+ GdkWindow* gdkWindow = window ? gtk_widget_get_window(GTK_WIDGET(window))
+ : gdk_screen_get_root_window(screen);
+ if (!gdkWindow) {
+ return;
+ }
+ gdk_window_get_device_position(
+ gdkWindow, gdk_drag_context_get_device(aContext), &x, &y, nullptr);
+ gint scale = gdk_window_get_scale_factor(gdkWindow);
+ SetDragEndPoint(LayoutDeviceIntPoint(x * scale, y * scale));
+ LOGDRAGSERVICE(" guess drag end point %d %d\n", x * scale, y * scale);
+ }
+
+ // Either the drag was aborted or the drop occurred outside the app.
+ // The dropEffect of mDataTransfer is not updated for motion outside the
+ // app, but is needed for the dragend event, so set it now.
+
+ uint32_t dropEffect;
+
+ if (aResult == GTK_DRAG_RESULT_SUCCESS) {
+ LOGDRAGSERVICE(" drop is accepted");
+ // With GTK+ versions 2.10.x and prior the drag may have been
+ // cancelled (but no drag-failed signal would have been sent).
+ // aContext->dest_window will be non-nullptr only if the drop was
+ // sent.
+ GdkDragAction action = gdk_drag_context_get_dest_window(aContext)
+ ? gdk_drag_context_get_actions(aContext)
+ : (GdkDragAction)0;
+
+ // Only one bit of action should be set, but, just in case someone
+ // does something funny, erring away from MOVE, and not recording
+ // unusual action combinations as NONE.
+ if (!action) {
+ LOGDRAGSERVICE(" drop action is none");
+ dropEffect = DRAGDROP_ACTION_NONE;
+ } else if (action & GDK_ACTION_COPY) {
+ LOGDRAGSERVICE(" drop action is copy");
+ dropEffect = DRAGDROP_ACTION_COPY;
+ } else if (action & GDK_ACTION_LINK) {
+ LOGDRAGSERVICE(" drop action is link");
+ dropEffect = DRAGDROP_ACTION_LINK;
+ } else if (action & GDK_ACTION_MOVE) {
+ LOGDRAGSERVICE(" drop action is move");
+ dropEffect = DRAGDROP_ACTION_MOVE;
+ } else {
+ LOGDRAGSERVICE(" drop action is copy");
+ dropEffect = DRAGDROP_ACTION_COPY;
+ }
+ } else {
+ LOGDRAGSERVICE(" drop action is none");
+ dropEffect = DRAGDROP_ACTION_NONE;
+ if (aResult != GTK_DRAG_RESULT_NO_TARGET) {
+ LOGDRAGSERVICE(" drop is user chancelled\n");
+ mUserCancelled = true;
+ }
+ }
+
+ if (mDataTransfer) {
+ mDataTransfer->SetDropEffectInt(dropEffect);
+ }
+
+ // Schedule the appropriate drag end dom events.
+ Schedule(eDragTaskSourceEnd, nullptr, nullptr, LayoutDeviceIntPoint(), 0);
+}
+
+static nsresult GetDownloadDetails(nsITransferable* aTransferable,
+ nsIURI** aSourceURI, nsAString& aFilename) {
+ *aSourceURI = nullptr;
+ MOZ_ASSERT(aTransferable != nullptr, "aTransferable must not be null");
+
+ // get the URI from the kFilePromiseURLMime flavor
+ nsCOMPtr<nsISupports> urlPrimitive;
+ nsresult rv = aTransferable->GetTransferData(kFilePromiseURLMime,
+ getter_AddRefs(urlPrimitive));
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsISupportsString> srcUrlPrimitive = do_QueryInterface(urlPrimitive);
+ if (!srcUrlPrimitive) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString srcUri;
+ srcUrlPrimitive->GetData(srcUri);
+ if (srcUri.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIURI> sourceURI;
+ NS_NewURI(getter_AddRefs(sourceURI), srcUri);
+
+ nsAutoString srcFileName;
+ nsCOMPtr<nsISupports> fileNamePrimitive;
+ rv = aTransferable->GetTransferData(kFilePromiseDestFilename,
+ getter_AddRefs(fileNamePrimitive));
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsISupportsString> srcFileNamePrimitive =
+ do_QueryInterface(fileNamePrimitive);
+ if (srcFileNamePrimitive) {
+ srcFileNamePrimitive->GetData(srcFileName);
+ } else {
+ nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI);
+ if (!sourceURL) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoCString urlFileName;
+ sourceURL->GetFileName(urlFileName);
+ NS_UnescapeURL(urlFileName);
+ CopyUTF8toUTF16(urlFileName, srcFileName);
+ }
+ if (srcFileName.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ sourceURI.swap(*aSourceURI);
+ aFilename = srcFileName;
+ return NS_OK;
+}
+
+// See nsContentAreaDragDropDataProvider::GetFlavorData() for reference.
+nsresult nsDragService::CreateTempFile(nsITransferable* aItem,
+ nsACString& aURI) {
+ LOGDRAGSERVICE("nsDragService::CreateTempFile()");
+
+ nsCOMPtr<nsIFile> tmpDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir));
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to get temp directory\n");
+ return rv;
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCOMPtr<nsIChannel> channel;
+
+ // extract the file name and source uri of the promise-file data
+ nsAutoString wideFileName;
+ nsCOMPtr<nsIURI> sourceURI;
+ rv = GetDownloadDetails(aItem, getter_AddRefs(sourceURI), wideFileName);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(
+ " Failed to extract file name and source uri from download url");
+ return rv;
+ }
+
+ // Check if the file is already stored at /tmp.
+ // It happens when drop destination is changed and SourceDataGet() is caled
+ // more than once.
+ nsAutoCString fileName;
+ CopyUTF16toUTF8(wideFileName, fileName);
+ auto fileLen = fileName.Length();
+ for (const auto& url : mTempFileUrls) {
+ auto URLLen = url.Length();
+ if (URLLen > fileLen &&
+ fileName.Equals(nsDependentCString(url, URLLen - fileLen))) {
+ aURI = url;
+ LOGDRAGSERVICE(" recycle file %s", PromiseFlatCString(aURI).get());
+ return NS_OK;
+ }
+ }
+
+ // create and open channel for source uri
+ nsCOMPtr<nsIPrincipal> principal = aItem->GetRequestingPrincipal();
+ nsContentPolicyType contentPolicyType = aItem->GetContentPolicyType();
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ aItem->GetCookieJarSettings();
+ rv = NS_NewChannel(getter_AddRefs(channel), sourceURI, principal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType, cookieJarSettings);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to create new channel for source uri");
+ return rv;
+ }
+
+ rv = channel->Open(getter_AddRefs(inputStream));
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to open channel for source uri");
+ return rv;
+ }
+
+ // build the file:///tmp/dnd_file URL
+ tmpDir->Append(NS_LITERAL_STRING_FROM_CSTRING("dnd_file"));
+ rv = tmpDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed create tmp dir");
+ return rv;
+ }
+
+ // store a copy of that temporary directory so we can
+ // clean them up when nsDragService is destructed
+ nsCOMPtr<nsIFile> tempFile;
+ tmpDir->Clone(getter_AddRefs(tempFile));
+ mTemporaryFiles.AppendObject(tempFile);
+ if (mTempFileTimerID) {
+ g_source_remove(mTempFileTimerID);
+ mTempFileTimerID = 0;
+ }
+
+ // extend file:///tmp/dnd_file/<filename> URL
+ tmpDir->Append(wideFileName);
+
+ nsCOMPtr<nsIOutputStream> outputStream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), tmpDir);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to open output stream for temporary file");
+ return rv;
+ }
+
+ char buffer[8192];
+ uint32_t readCount = 0;
+ uint32_t writeCount = 0;
+ while (1) {
+ rv = inputStream->Read(buffer, sizeof(buffer), &readCount);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to read data from source uri");
+ return rv;
+ }
+
+ if (readCount == 0) break;
+
+ rv = outputStream->Write(buffer, readCount, &writeCount);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to write data to temporary file");
+ return rv;
+ }
+ }
+
+ inputStream->Close();
+ rv = outputStream->Close();
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to write data to temporary file");
+ return rv;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewFileURI(getter_AddRefs(uri), tmpDir);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to get file URI");
+ return rv;
+ }
+ nsCOMPtr<nsIURL> fileURL(do_QueryInterface(uri));
+ if (!fileURL) {
+ LOGDRAGSERVICE(" Failed to query file interface");
+ return NS_ERROR_FAILURE;
+ }
+ rv = fileURL->GetSpec(aURI);
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" Failed to get filepath");
+ return rv;
+ }
+
+ // store url of temporary file
+ mTempFileUrls.AppendElement()->Assign(aURI);
+ LOGDRAGSERVICE(" storing tmp file as %s", PromiseFlatCString(aURI).get());
+ return NS_OK;
+}
+
+bool nsDragService::SourceDataAppendURLFileItem(nsACString& aURI,
+ nsITransferable* aItem) {
+ // If there is a file available, create a URI from the file.
+ nsCOMPtr<nsISupports> data;
+ nsresult rv = aItem->GetTransferData(kFileMime, getter_AddRefs(data));
+ NS_ENSURE_SUCCESS(rv, false);
+ if (nsCOMPtr<nsIFile> file = do_QueryInterface(data)) {
+ nsCOMPtr<nsIURI> fileURI;
+ NS_NewFileURI(getter_AddRefs(fileURI), file);
+ if (fileURI) {
+ fileURI->GetSpec(aURI);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool nsDragService::SourceDataAppendURLItem(nsITransferable* aItem,
+ bool aExternalDrop,
+ nsACString& aURI) {
+ nsCOMPtr<nsISupports> data;
+ nsresult rv = aItem->GetTransferData(kURLMime, getter_AddRefs(data));
+ if (NS_FAILED(rv)) {
+ return SourceDataAppendURLFileItem(aURI, aItem);
+ }
+
+ nsCOMPtr<nsISupportsString> string = do_QueryInterface(data);
+ if (!string) {
+ return false;
+ }
+
+ nsAutoString text;
+ string->GetData(text);
+ if (!aExternalDrop || CanExportAsURLTarget(text.get(), text.Length())) {
+ AppendUTF16toUTF8(text, aURI);
+ return true;
+ }
+
+ // We're dropping to another application and the URL can't be exported
+ // as it's internal one (mailbox:// etc.)
+ // Try to get file target directly.
+ if (SourceDataAppendURLFileItem(aURI, aItem)) {
+ return true;
+ }
+
+ // We can't get the file directly so try to download it and save to tmp.
+ // The desktop or file manager expects for drags of promise-file data
+ // the text/uri-list flavor set to a temporary file that contains the
+ // promise-file data.
+ // We open a stream on the <protocol>:// url here and save the content
+ // to file:///tmp/dnd_file/<filename> and pass this url
+ // as text/uri-list flavor.
+
+ // check whether transferable contains FilePromiseUrl flavor...
+ nsCOMPtr<nsISupports> promiseData;
+ rv = aItem->GetTransferData(kFilePromiseURLMime, getter_AddRefs(promiseData));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // ... if so, create a temporary file and pass its url
+ return NS_SUCCEEDED(CreateTempFile(aItem, aURI));
+}
+
+void nsDragService::SourceDataGetUriList(GdkDragContext* aContext,
+ GtkSelectionData* aSelectionData,
+ uint32_t aDragItems) {
+ // Check if we're transfering data to another application.
+ // gdk_drag_context_get_dest_window() on X11 returns GdkWindow even for
+ // different application so use nsWindow::GetWindow() to check if that's
+ // our window.
+ const bool isExternalDrop =
+ widget::GdkIsX11Display()
+ ? !nsWindow::GetWindow(gdk_drag_context_get_dest_window(aContext))
+ : !gdk_drag_context_get_dest_window(aContext);
+
+ LOGDRAGSERVICE("nsDragService::SourceDataGetUriLists() len %d external %d",
+ aDragItems, isExternalDrop);
+
+ // Disable processing of native events until we store all files to /tmp.
+ // Otherwise user can quit drop before we have all files saved
+ // and that cancels whole D&D.
+ AutoSuspendNativeEvents suspend;
+
+ nsAutoCString uriList;
+ for (uint32_t i = 0; i < aDragItems; i++) {
+ nsCOMPtr<nsITransferable> item = do_QueryElementAt(mSourceDataItems, i);
+ if (!item) {
+ continue;
+ }
+ nsAutoCString uri;
+ if (!SourceDataAppendURLItem(item, isExternalDrop, uri)) {
+ continue;
+ }
+ // text/x-moz-url is of form url + "\n" + title.
+ // We just want the url.
+ int32_t separatorPos = uri.FindChar(u'\n');
+ if (separatorPos >= 0) {
+ uri.Truncate(separatorPos);
+ }
+ uriList.Append(uri);
+ uriList.AppendLiteral("\r\n");
+ }
+
+ LOGDRAGSERVICE("URI list\n%s", uriList.get());
+ GdkAtom target = gtk_selection_data_get_target(aSelectionData);
+ gtk_selection_data_set(aSelectionData, target, 8, (guchar*)uriList.get(),
+ uriList.Length());
+}
+
+void nsDragService::SourceDataGetImage(nsITransferable* aItem,
+ GtkSelectionData* aSelectionData) {
+ LOGDRAGSERVICE("nsDragService::SourceDataGetImage()");
+
+ nsresult rv;
+ nsCOMPtr<nsISupports> data;
+ rv = aItem->GetTransferData(kNativeImageMime, getter_AddRefs(data));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ LOGDRAGSERVICE(" posting image\n");
+ nsCOMPtr<imgIContainer> image = do_QueryInterface(data);
+ if (!image) {
+ LOGDRAGSERVICE(" do_QueryInterface failed\n");
+ return;
+ }
+ RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(image);
+ if (!pixbuf) {
+ LOGDRAGSERVICE(" ImageToPixbuf failed\n");
+ return;
+ }
+ gtk_selection_data_set_pixbuf(aSelectionData, pixbuf);
+ LOGDRAGSERVICE(" image data set\n");
+ return;
+}
+
+void nsDragService::SourceDataGetXDND(nsITransferable* aItem,
+ GdkDragContext* aContext,
+ GtkSelectionData* aSelectionData) {
+ LOGDRAGSERVICE("nsDragService::SourceDataGetXDND");
+
+ // Indicate failure by default.
+ GdkAtom target = gtk_selection_data_get_target(aSelectionData);
+ gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"E", 1);
+
+ GdkAtom property = gdk_atom_intern(gXdndDirectSaveType, FALSE);
+ GdkAtom type = gdk_atom_intern(kTextMime, FALSE);
+
+ GdkWindow* srcWindow = gdk_drag_context_get_source_window(aContext);
+ if (!srcWindow) {
+ LOGDRAGSERVICE(" failed to get source GdkWindow!");
+ return;
+ }
+
+ // Ensure null termination.
+ nsAutoCString data;
+ {
+ GUniquePtr<guchar> gdata;
+ gint length = 0;
+ if (!gdk_property_get(srcWindow, property, type, 0, INT32_MAX, FALSE,
+ nullptr, nullptr, &length, getter_Transfers(gdata))) {
+ LOGDRAGSERVICE(" failed to get gXdndDirectSaveType GdkWindow property.");
+ return;
+ }
+ data.Assign(nsDependentCSubstring((const char*)gdata.get(), length));
+ }
+
+ GUniquePtr<char> hostname;
+ GUniquePtr<char> fullpath(
+ g_filename_from_uri(data.get(), getter_Transfers(hostname), nullptr));
+ if (!fullpath) {
+ LOGDRAGSERVICE(" failed to get file from uri.");
+ return;
+ }
+
+ // If there is no hostname in the URI, NULL will be stored.
+ // We should not accept uris with from a different host.
+ if (hostname) {
+ nsCOMPtr<nsIPropertyBag2> infoService =
+ do_GetService(NS_SYSTEMINFO_CONTRACTID);
+ if (!infoService) {
+ return;
+ }
+ nsAutoCString host;
+ if (NS_SUCCEEDED(infoService->GetPropertyAsACString(u"host"_ns, host))) {
+ if (!host.Equals(hostname.get())) {
+ LOGDRAGSERVICE(" ignored drag because of different host.");
+ // Special error code "F" for this case.
+ gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"F", 1);
+ return;
+ }
+ }
+ }
+
+ LOGDRAGSERVICE(" XdndDirectSave filepath is %s", fullpath.get());
+
+ nsCOMPtr<nsIFile> file;
+ if (NS_FAILED(NS_NewNativeLocalFile(nsDependentCString(fullpath.get()), false,
+ getter_AddRefs(file)))) {
+ LOGDRAGSERVICE(" failed to get local file");
+ return;
+ }
+
+ // We have to split the path into a directory and filename,
+ // because our internal file-promise API is based on these.
+ nsCOMPtr<nsIFile> directory;
+ file->GetParent(getter_AddRefs(directory));
+
+ aItem->SetTransferData(kFilePromiseDirectoryMime, directory);
+
+ nsCOMPtr<nsISupportsString> filenamePrimitive =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ if (!filenamePrimitive) {
+ return;
+ }
+
+ nsAutoString leafName;
+ file->GetLeafName(leafName);
+ filenamePrimitive->SetData(leafName);
+
+ aItem->SetTransferData(kFilePromiseDestFilename, filenamePrimitive);
+
+ nsCOMPtr<nsISupports> promiseData;
+ nsresult rv =
+ aItem->GetTransferData(kFilePromiseMime, getter_AddRefs(promiseData));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Indicate success.
+ gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"S", 1);
+ return;
+}
+
+bool nsDragService::SourceDataGetText(nsITransferable* aItem,
+ const nsACString& aMIMEType,
+ bool aNeedToDoConversionToPlainText,
+ GtkSelectionData* aSelectionData) {
+ LOGDRAGSERVICE("nsDragService::SourceDataGetPlain()");
+
+ nsresult rv;
+ nsCOMPtr<nsISupports> data;
+ rv = aItem->GetTransferData(PromiseFlatCString(aMIMEType).get(),
+ getter_AddRefs(data));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ void* tmpData = nullptr;
+ uint32_t tmpDataLen = 0;
+
+ nsPrimitiveHelpers::CreateDataFromPrimitive(aMIMEType, data, &tmpData,
+ &tmpDataLen);
+ // if required, do the extra work to convert unicode to plain
+ // text and replace the output values with the plain text.
+ if (aNeedToDoConversionToPlainText) {
+ char* plainTextData = nullptr;
+ char16_t* castedUnicode = reinterpret_cast<char16_t*>(tmpData);
+ uint32_t plainTextLen = 0;
+ UTF16ToNewUTF8(castedUnicode, tmpDataLen / 2, &plainTextData,
+ &plainTextLen);
+ if (tmpData) {
+ // this was not allocated using glib
+ free(tmpData);
+ tmpData = plainTextData;
+ tmpDataLen = plainTextLen;
+ }
+ }
+ if (tmpData) {
+ // this copies the data
+ GdkAtom target = gtk_selection_data_get_target(aSelectionData);
+ gtk_selection_data_set(aSelectionData, target, 8, (guchar*)tmpData,
+ tmpDataLen);
+ // this wasn't allocated with glib
+ free(tmpData);
+ }
+
+ return true;
+}
+
+// We're asked to get data from mSourceDataItems and pass it to
+// GtkSelectionData (Gtk D&D interface).
+// We need to check mSourceDataItems data type and try to convert it
+// to data type accepted by Gtk.
+void nsDragService::SourceDataGet(GtkWidget* aWidget, GdkDragContext* aContext,
+ GtkSelectionData* aSelectionData,
+ guint32 aTime) {
+ LOGDRAGSERVICE("nsDragService::SourceDataGet(%p)", aContext);
+
+ GdkAtom target = gtk_selection_data_get_target(aSelectionData);
+ GUniquePtr<gchar> requestedTypeName(gdk_atom_name(target));
+ if (!requestedTypeName) {
+ LOGDRAGSERVICE(" failed to get atom name.\n");
+ return;
+ }
+
+ LOGDRAGSERVICE(" Gtk asks us for %s data type\n", requestedTypeName.get());
+ // check to make sure that we have data items to return.
+ if (!mSourceDataItems) {
+ LOGDRAGSERVICE(" Failed to get our data items\n");
+ return;
+ }
+
+ uint32_t dragItems;
+ mSourceDataItems->GetLength(&dragItems);
+ LOGDRAGSERVICE(" source data items %d", dragItems);
+
+ nsDependentCString mimeFlavor(requestedTypeName.get());
+ if (mimeFlavor.EqualsLiteral(gTextUriListType)) {
+ SourceDataGetUriList(aContext, aSelectionData, dragItems);
+ return;
+ }
+
+#ifdef MOZ_LOGGING
+ if (dragItems > 1) {
+ LOGDRAGSERVICE(
+ " There are %d data items but we're asked for %s MIME type. Only "
+ "first data element can be transfered!",
+ dragItems, mimeFlavor.get());
+ }
+#endif
+
+ nsCOMPtr<nsITransferable> item = do_QueryElementAt(mSourceDataItems, 0);
+ if (!item) {
+ LOGDRAGSERVICE(" Failed to get SourceDataItems!");
+ return;
+ }
+
+ if (mimeFlavor.EqualsLiteral(kTextMime) ||
+ mimeFlavor.EqualsLiteral(gTextPlainUTF8Type)) {
+ SourceDataGetText(item, nsDependentCString(kTextMime),
+ /* aNeedToDoConversionToPlainText */ true,
+ aSelectionData);
+ // no fallback for text mime types
+ return;
+ }
+ // Someone is asking for the special Direct Save Protocol type.
+ else if (mimeFlavor.EqualsLiteral(gXdndDirectSaveType)) {
+ SourceDataGetXDND(item, aContext, aSelectionData);
+ // no fallback for XDND mime types
+ return;
+ } else if (mimeFlavor.EqualsLiteral(kPNGImageMime) ||
+ mimeFlavor.EqualsLiteral(kJPEGImageMime) ||
+ mimeFlavor.EqualsLiteral(kJPGImageMime) ||
+ mimeFlavor.EqualsLiteral(kGIFImageMime)) {
+ // no fallback for image mime types
+ SourceDataGetImage(item, aSelectionData);
+ return;
+ } else if (mimeFlavor.EqualsLiteral(gMozUrlType)) {
+ // Someone was asking for _NETSCAPE_URL. We need to get it from
+ // transferable as x-moz-url and convert it to plain text.
+ // No need to check
+ if (SourceDataGetText(item, nsDependentCString(kURLMime),
+ /* aNeedToDoConversionToPlainText */ true,
+ aSelectionData)) {
+ return;
+ }
+ }
+ // Just try to get and set whatever we're asked for.
+ SourceDataGetText(item, mimeFlavor,
+ /* aNeedToDoConversionToPlainText */ false, aSelectionData);
+}
+
+void nsDragService::SourceBeginDrag(GdkDragContext* aContext) {
+ LOGDRAGSERVICE("nsDragService::SourceBeginDrag(%p)\n", aContext);
+
+ nsCOMPtr<nsITransferable> transferable =
+ do_QueryElementAt(mSourceDataItems, 0);
+ if (!transferable) return;
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = transferable->FlavorsTransferableCanImport(flavors);
+ NS_ENSURE_SUCCESS(rv, );
+
+ for (uint32_t i = 0; i < flavors.Length(); ++i) {
+ if (flavors[i].EqualsLiteral(kFilePromiseDestFilename)) {
+ nsCOMPtr<nsISupports> data;
+ rv = transferable->GetTransferData(kFilePromiseDestFilename,
+ getter_AddRefs(data));
+ if (NS_FAILED(rv)) {
+ LOGDRAGSERVICE(" transferable doesn't contain '%s",
+ kFilePromiseDestFilename);
+ return;
+ }
+
+ nsCOMPtr<nsISupportsString> fileName = do_QueryInterface(data);
+ if (!fileName) {
+ LOGDRAGSERVICE(" failed to get file name");
+ return;
+ }
+
+ nsAutoString fileNameStr;
+ fileName->GetData(fileNameStr);
+
+ nsCString fileNameCStr;
+ CopyUTF16toUTF8(fileNameStr, fileNameCStr);
+
+ GdkAtom property = gdk_atom_intern(gXdndDirectSaveType, FALSE);
+ GdkAtom type = gdk_atom_intern(kTextMime, FALSE);
+
+ gdk_property_change(gdk_drag_context_get_source_window(aContext),
+ property, type, 8, GDK_PROP_MODE_REPLACE,
+ (const guchar*)fileNameCStr.get(),
+ fileNameCStr.Length());
+ break;
+ }
+ }
+}
+
+void nsDragService::SetDragIcon(GdkDragContext* aContext) {
+ if (!mHasImage && !mSelection) return;
+
+ LOGDRAGSERVICE("nsDragService::SetDragIcon(%p)", aContext);
+
+ LayoutDeviceIntRect dragRect;
+ nsPresContext* pc;
+ RefPtr<SourceSurface> surface;
+ DrawDrag(mSourceNode, mRegion, mScreenPosition, &dragRect, &surface, &pc);
+ if (!pc) {
+ LOGDRAGSERVICE(" PresContext is missing!");
+ return;
+ }
+
+ const auto screenPoint =
+ LayoutDeviceIntPoint::Round(mScreenPosition * pc->CSSToDevPixelScale());
+ int32_t offsetX = screenPoint.x - dragRect.x;
+ int32_t offsetY = screenPoint.y - dragRect.y;
+
+ // If a popup is set as the drag image, use its widget. Otherwise, use
+ // the surface that DrawDrag created.
+ //
+ // XXX: Disable drag popups on GTK 3.19.4 and above: see bug 1264454.
+ // Fix this once a new GTK version ships that does not destroy our
+ // widget in gtk_drag_set_icon_widget.
+ // This is fixed in GTK 3.24
+ // by
+ // https://gitlab.gnome.org/GNOME/gtk/-/commit/c27c4e2048acb630feb24c31288f802345e99f4c
+ bool gtk_drag_set_icon_widget_is_working =
+ gtk_check_version(3, 19, 4) != nullptr ||
+ gtk_check_version(3, 24, 0) == nullptr;
+ if (mDragPopup && gtk_drag_set_icon_widget_is_working) {
+ GtkWidget* gtkWidget = nullptr;
+ nsIFrame* frame = mDragPopup->GetPrimaryFrame();
+ if (frame) {
+ // DrawDrag ensured that this is a popup frame.
+ nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget();
+ if (widget) {
+ gtkWidget = (GtkWidget*)widget->GetNativeData(NS_NATIVE_SHELLWIDGET);
+ if (gtkWidget) {
+ // When mDragPopup has a parent it's already attached to D&D context.
+ // That may happens when D&D operation is aborted but not finished
+ // on Gtk side yet so let's remove it now.
+ if (GtkWidget* parent = gtk_widget_get_parent(gtkWidget)) {
+ gtk_container_remove(GTK_CONTAINER(parent), gtkWidget);
+ }
+ LOGDRAGSERVICE(" set drag popup [%p]", widget.get());
+ OpenDragPopup();
+ gtk_drag_set_icon_widget(aContext, gtkWidget, offsetX, offsetY);
+ return;
+ } else {
+ LOGDRAGSERVICE(" NS_NATIVE_SHELLWIDGET is missing!");
+ }
+ } else {
+ LOGDRAGSERVICE(" NearestWidget is missing!");
+ }
+ } else {
+ LOGDRAGSERVICE(" PrimaryFrame is missing!");
+ }
+ }
+
+ if (surface) {
+ LOGDRAGSERVICE(" We have a surface");
+ if (!SetAlphaPixmap(surface, aContext, offsetX, offsetY, dragRect)) {
+ RefPtr<GdkPixbuf> dragPixbuf = nsImageToPixbuf::SourceSurfaceToPixbuf(
+ surface, dragRect.width, dragRect.height);
+ if (dragPixbuf) {
+ LOGDRAGSERVICE(" set drag pixbuf");
+ gtk_drag_set_icon_pixbuf(aContext, dragPixbuf, offsetX, offsetY);
+ } else {
+ LOGDRAGSERVICE(" SourceSurfaceToPixbuf failed!");
+ }
+ }
+ } else {
+ LOGDRAGSERVICE(" Surface is missing!");
+ }
+}
+
+static void invisibleSourceDragBegin(GtkWidget* aWidget,
+ GdkDragContext* aContext, gpointer aData) {
+ LOGDRAGSERVICESTATIC("invisibleSourceDragBegin (%p)", aContext);
+ nsDragService* dragService = (nsDragService*)aData;
+
+ dragService->SourceBeginDrag(aContext);
+ dragService->SetDragIcon(aContext);
+}
+
+static void invisibleSourceDragDataGet(GtkWidget* aWidget,
+ GdkDragContext* aContext,
+ GtkSelectionData* aSelectionData,
+ guint aInfo, guint32 aTime,
+ gpointer aData) {
+ LOGDRAGSERVICESTATIC("invisibleSourceDragDataGet (%p)", aContext);
+ nsDragService* dragService = (nsDragService*)aData;
+ dragService->SourceDataGet(aWidget, aContext, aSelectionData, aTime);
+}
+
+static gboolean invisibleSourceDragFailed(GtkWidget* aWidget,
+ GdkDragContext* aContext,
+ gint aResult, gpointer aData) {
+#ifdef MOZ_WAYLAND
+ // Wayland and X11 uses different drag results here. When drag target is
+ // missing X11 passes GDK_DRAG_CANCEL_NO_TARGET
+ // (from gdk_dnd_handle_button_event()/gdkdnd-x11.c)
+ // as backend X11 has info about other application windows.
+ // Wayland does not have such info so it always passes
+ // GDK_DRAG_CANCEL_ERROR error code
+ // (see data_source_cancelled/gdkselection-wayland.c).
+ // Bug 1527976
+ if (widget::GdkIsWaylandDisplay() && aResult == GTK_DRAG_RESULT_ERROR) {
+ for (GList* tmp = gdk_drag_context_list_targets(aContext); tmp;
+ tmp = tmp->next) {
+ GdkAtom atom = GDK_POINTER_TO_ATOM(tmp->data);
+ GUniquePtr<gchar> name(gdk_atom_name(atom));
+ if (name && !strcmp(name.get(), gTabDropType)) {
+ aResult = GTK_DRAG_RESULT_NO_TARGET;
+ LOGDRAGSERVICESTATIC("invisibleSourceDragFailed(%p): Wayland tab drop",
+ aContext);
+ break;
+ }
+ }
+ }
+#endif
+ LOGDRAGSERVICESTATIC("invisibleSourceDragFailed(%p) %s", aContext,
+ kGtkDragResults[aResult]);
+ nsDragService* dragService = (nsDragService*)aData;
+ // End the drag session now (rather than waiting for the drag-end signal)
+ // so that operations performed on dropEffect == none can start immediately
+ // rather than waiting for the drag-failed animation to finish.
+ dragService->SourceEndDragSession(aContext, aResult);
+
+ // We should return TRUE to disable the drag-failed animation iff the
+ // source performed an operation when dropEffect was none, but the handler
+ // of the dragend DOM event doesn't provide this information.
+ return FALSE;
+}
+
+static void invisibleSourceDragEnd(GtkWidget* aWidget, GdkDragContext* aContext,
+ gpointer aData) {
+ LOGDRAGSERVICESTATIC("invisibleSourceDragEnd(%p)", aContext);
+ nsDragService* dragService = (nsDragService*)aData;
+
+ // The drag has ended. Release the hostages!
+ dragService->SourceEndDragSession(aContext, GTK_DRAG_RESULT_SUCCESS);
+}
+
+// The following methods handle responding to GTK drag signals and
+// tracking state between these signals.
+//
+// In general, GTK does not expect us to run the event loop while handling its
+// drag signals, however our drag event handlers may run the
+// event loop, most often to fetch information about the drag data.
+//
+// GTK, for example, uses the return value from drag-motion signals to
+// determine whether drag-leave signals should be sent. If an event loop is
+// run during drag-motion the XdndLeave message can get processed but when GTK
+// receives the message it does not yet know that it needs to send the
+// drag-leave signal to our widget.
+//
+// After a drag-drop signal, we need to reply with gtk_drag_finish().
+// However, gtk_drag_finish should happen after the drag-drop signal handler
+// returns so that when the Motif drag protocol is used, the
+// XmTRANSFER_SUCCESS during gtk_drag_finish is sent after the XmDROP_START
+// reply sent on return from the drag-drop signal handler.
+//
+// Similarly drag-end for a successful drag and drag-failed are not good
+// times to run a nested event loop as gtk_drag_drop_finished() and
+// gtk_drag_source_info_destroy() don't gtk_drag_clear_source_info() or remove
+// drop_timeout until after at least the first of these signals is sent.
+// Processing other events (e.g. a slow GDK_DROP_FINISHED reply, or the drop
+// timeout) could cause gtk_drag_drop_finished to be called again with the
+// same GtkDragSourceInfo, which won't like being destroyed twice.
+//
+// Therefore we reply to the signals immediately and schedule a task to
+// dispatch the Gecko events, which may run the event loop.
+//
+// Action in response to drag-leave signals is also delayed until the event
+// loop runs again so that we find out whether a drag-drop signal follows.
+//
+// A single task is scheduled to manage responses to all three GTK signals.
+// If further signals are received while the task is scheduled, the scheduled
+// response is updated, sometimes effectively compressing successive signals.
+//
+// No Gecko drag events are dispatched (during nested event loops) while other
+// Gecko drag events are in flight. This helps event handlers that may not
+// expect nested events, while accessing an event's dataTransfer for example.
+
+gboolean nsDragService::ScheduleMotionEvent(nsWindow* aWindow,
+ GdkDragContext* aDragContext,
+ LayoutDeviceIntPoint aWindowPoint,
+ guint aTime) {
+ if (aDragContext && mScheduledTask == eDragTaskMotion) {
+ // The drag source has sent another motion message before we've
+ // replied to the previous. That shouldn't happen with Xdnd. The
+ // spec for Motif drags is less clear, but we'll just update the
+ // scheduled task with the new position reply only to the most
+ // recent message.
+ NS_WARNING("Drag Motion message received before previous reply was sent");
+ }
+
+ // Returning TRUE means we'll reply with a status message, unless we first
+ // get a leave.
+ return Schedule(eDragTaskMotion, aWindow, aDragContext, aWindowPoint, aTime);
+}
+
+void nsDragService::ScheduleLeaveEvent() {
+ // We don't know at this stage whether a drop signal will immediately
+ // follow. If the drop signal gets sent it will happen before we return
+ // to the main loop and the scheduled leave task will be replaced.
+ if (!Schedule(eDragTaskLeave, nullptr, nullptr, LayoutDeviceIntPoint(), 0)) {
+ NS_WARNING("Drag leave after drop");
+ }
+}
+
+gboolean nsDragService::ScheduleDropEvent(nsWindow* aWindow,
+ GdkDragContext* aDragContext,
+ LayoutDeviceIntPoint aWindowPoint,
+ guint aTime) {
+ if (!Schedule(eDragTaskDrop, aWindow, aDragContext, aWindowPoint, aTime)) {
+ NS_WARNING("Additional drag drop ignored");
+ return FALSE;
+ }
+
+ SetDragEndPoint(aWindowPoint);
+
+ // We'll reply with gtk_drag_finish().
+ return TRUE;
+}
+
+#ifdef MOZ_LOGGING
+const char* nsDragService::GetDragServiceTaskName(DragTask aTask) {
+ static const char* taskNames[] = {"eDragTaskNone", "eDragTaskMotion",
+ "eDragTaskLeave", "eDragTaskDrop",
+ "eDragTaskSourceEnd"};
+ MOZ_ASSERT(size_t(aTask) < ArrayLength(taskNames));
+ return taskNames[aTask];
+}
+#endif
+
+gboolean nsDragService::Schedule(DragTask aTask, nsWindow* aWindow,
+ GdkDragContext* aDragContext,
+ LayoutDeviceIntPoint aWindowPoint,
+ guint aTime) {
+ // If there is an existing leave or motion task scheduled, then that
+ // will be replaced. When the new task is run, it will dispatch
+ // any necessary leave or motion events.
+
+ // If aTask is eDragTaskSourceEnd, then it will replace even a scheduled
+ // drop event (which could happen if the drop event has not been processed
+ // within the allowed time). Otherwise, if we haven't yet run a scheduled
+ // drop or end task, just say that we are not ready to receive another
+ // drop.
+ LOGDRAGSERVICE("nsDragService::Schedule(%p) task %s window %p\n",
+ aDragContext, GetDragServiceTaskName(aTask), aWindow);
+
+ if (mScheduledTask == eDragTaskSourceEnd ||
+ (mScheduledTask == eDragTaskDrop && aTask != eDragTaskSourceEnd)) {
+ LOGDRAGSERVICE(" task does not fit recent task %s, quit!\n",
+ GetDragServiceTaskName(mScheduledTask));
+ return FALSE;
+ }
+
+ mScheduledTask = aTask;
+ mPendingWindow = aWindow;
+ mPendingDragContext = aDragContext;
+ mPendingWindowPoint = aWindowPoint;
+ mPendingTime = aTime;
+
+ if (!mTaskSource) {
+ // High priority is used here because we want to process motion events
+ // right after drag_motion event handler which is called by Gtk.
+ // An ideal scenario is to call TaskDispatchCallback() directly here
+ // but we can't do that. TaskDispatchCallback() spins gtk event loop
+ // while nsDragService::Schedule() is already called from event loop
+ // (by drag_motion* gtk_widget events) so that direct call will cause
+ // nested recursion.
+ mTaskSource = g_timeout_add_full(G_PRIORITY_HIGH, 0, TaskDispatchCallback,
+ this, nullptr);
+ }
+
+ // We need to reply to drag_motion event on Wayland immediately,
+ // see Bug 1730203.
+ if (widget::GdkIsWaylandDisplay() && mScheduledTask == eDragTaskMotion) {
+ UpdateDragAction(aDragContext);
+ ReplyToDragMotion(aDragContext, aTime);
+ }
+
+ return TRUE;
+}
+
+gboolean nsDragService::TaskDispatchCallback(gpointer data) {
+ RefPtr<nsDragService> dragService = static_cast<nsDragService*>(data);
+ AutoEventLoop loop(dragService);
+ return dragService->RunScheduledTask();
+}
+
+gboolean nsDragService::RunScheduledTask() {
+ LOGDRAGSERVICE(
+ "nsDragService::RunScheduledTask() task %s mTargetWindow %p "
+ "mPendingWindow %p\n",
+ GetDragServiceTaskName(mScheduledTask), mTargetWindow.get(),
+ mPendingWindow.get());
+
+ // Don't run RunScheduledTask() twice. As we use it in main thread only
+ // we don't need to be thread safe here.
+ if (mScheduledTaskIsRunning) {
+ LOGDRAGSERVICE(" sheduled task is already running, quit.");
+ return FALSE;
+ }
+ AutoRestore<bool> guard(mScheduledTaskIsRunning);
+ mScheduledTaskIsRunning = true;
+
+ if (mTargetWindow && mTargetWindow != mPendingWindow) {
+ LOGDRAGSERVICE(" dispatch eDragExit (%p)\n", mTargetWindow.get());
+ mTargetWindow->DispatchDragEvent(eDragExit, mTargetWindowPoint, 0);
+
+ if (!mSourceNode) {
+ // The drag that was initiated in a different app. End the drag
+ // session, since we're done with it for now (until the user drags
+ // back into this app).
+ EndDragSession(false, GetCurrentModifiers());
+ }
+ }
+
+ // It is possible that the pending state has been updated during dispatch
+ // of the leave event. That's fine.
+
+ // Now we collect the pending state because, from this point on, we want
+ // to use the same state for all events dispatched. All state is updated
+ // so that when other tasks are scheduled during dispatch here, this
+ // task is considered to have already been run.
+ bool positionHasChanged = mPendingWindow != mTargetWindow ||
+ mPendingWindowPoint != mTargetWindowPoint;
+ DragTask task = mScheduledTask;
+ mScheduledTask = eDragTaskNone;
+ mTargetWindow = std::move(mPendingWindow);
+ mTargetWindowPoint = mPendingWindowPoint;
+
+ if (task == eDragTaskLeave || task == eDragTaskSourceEnd) {
+ LOGDRAGSERVICE(" quit, selected task %s\n", GetDragServiceTaskName(task));
+ if (task == eDragTaskSourceEnd) {
+ // Dispatch drag end events.
+ EndDragSession(true, GetCurrentModifiers());
+ }
+
+ // Nothing more to do
+ // Returning false removes the task source from the event loop.
+ mTaskSource = 0;
+ return FALSE;
+ }
+
+ // This may be the start of a destination drag session.
+ StartDragSession();
+
+ // mTargetWidget may be nullptr if the window has been destroyed.
+ // (The leave event is not scheduled if a drop task is still scheduled.)
+ // We still reply appropriately to indicate that the drop will or didn't
+ // succeeed.
+ mTargetWidget = mTargetWindow ? mTargetWindow->GetGtkWidget() : nullptr;
+ LOGDRAGSERVICE(" start drag session mTargetWindow %p mTargetWidget %p\n",
+ mTargetWindow.get(), mTargetWidget.get());
+ LOGDRAGSERVICE(" mPendingDragContext %p => mTargetDragContext %p\n",
+ mPendingDragContext.get(), mTargetDragContext.get());
+ mTargetDragContext = std::move(mPendingDragContext);
+ mTargetTime = mPendingTime;
+
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model
+ // (as at 27 December 2010) indicates that a "drop" event should only be
+ // fired (at the current target element) if the current drag operation is
+ // not none. The current drag operation will only be set to a non-none
+ // value during a "dragover" event.
+ //
+ // If the user has ended the drag before any dragover events have been
+ // sent, then the spec recommends skipping the drop (because the current
+ // drag operation is none). However, here we assume that, by releasing
+ // the mouse button, the user has indicated that they want to drop, so we
+ // proceed with the drop where possible.
+ //
+ // In order to make the events appear to content in the same way as if the
+ // spec is being followed we make sure to dispatch a "dragover" event with
+ // appropriate coordinates and check canDrop before the "drop" event.
+ //
+ // When the Xdnd protocol is used for source/destination communication (as
+ // should be the case with GTK source applications) a dragover event
+ // should have already been sent during the drag-motion signal, which
+ // would have already been received because XdndDrop messages do not
+ // contain a position. However, we can't assume the same when the Motif
+ // protocol is used.
+ if (task == eDragTaskMotion || positionHasChanged) {
+ LOGDRAGSERVICE(" process motion event\n");
+ UpdateDragAction();
+ TakeDragEventDispatchedToChildProcess(); // Clear the old value.
+ DispatchMotionEvents();
+ if (task == eDragTaskMotion) {
+ if (TakeDragEventDispatchedToChildProcess()) {
+ mTargetDragContextForRemote = mTargetDragContext;
+ } else {
+ // Reply to tell the source whether we can drop and what
+ // action would be taken.
+ ReplyToDragMotion();
+ }
+ }
+ }
+
+ if (task == eDragTaskDrop) {
+ LOGDRAGSERVICE(" process drop task\n");
+ gboolean success = DispatchDropEvent();
+
+ // Perhaps we should set the del parameter to TRUE when the drag
+ // action is move, but we don't know whether the data was successfully
+ // transferred.
+ if (mTargetDragContext) {
+ LOGDRAGSERVICE(" drag finished\n");
+ gtk_drag_finish(mTargetDragContext, success,
+ /* del = */ FALSE, mTargetTime);
+ }
+ // Make sure to end the drag session. If this drag started in a
+ // different app, we won't get a drag_end signal to end it from.
+ EndDragSession(true, GetCurrentModifiers());
+ }
+
+ // We're done with the drag context.
+ LOGDRAGSERVICE(" clear mTargetWindow mTargetWidget and other data\n");
+ mTargetWidget = nullptr;
+ mTargetDragContext = nullptr;
+
+ // If we got another drag signal while running the sheduled task, that
+ // must have happened while running a nested event loop. Leave the task
+ // source on the event loop.
+ if (mScheduledTask != eDragTaskNone) return TRUE;
+
+ // We have no task scheduled.
+ // Returning false removes the task source from the event loop.
+ LOGDRAGSERVICE(" remove task source\n");
+ mTaskSource = 0;
+ return FALSE;
+}
+
+// This will update the drag action based on the information in the
+// drag context. Gtk gets this from a combination of the key settings
+// and what the source is offering.
+
+void nsDragService::UpdateDragAction(GdkDragContext* aDragContext) {
+ // This doesn't look right. dragSession.dragAction is used by
+ // nsContentUtils::SetDataTransferInEvent() to set the initial
+ // dataTransfer.dropEffect, so GdkDragContext::suggested_action would be
+ // more appropriate. GdkDragContext::actions should be used to set
+ // dataTransfer.effectAllowed, which doesn't currently happen with
+ // external sources.
+ LOGDRAGSERVICE("nsDragService::UpdateDragAction(%p)", aDragContext);
+
+ // default is to do nothing
+ int action = nsIDragService::DRAGDROP_ACTION_NONE;
+ GdkDragAction gdkAction = GDK_ACTION_DEFAULT;
+ if (aDragContext) {
+ gdkAction = gdk_drag_context_get_actions(aDragContext);
+ LOGDRAGSERVICE(" gdk_drag_context_get_actions() returns 0x%X", gdkAction);
+
+ // When D&D modifiers (CTRL/SHIFT) are involved,
+ // gdk_drag_context_get_actions() on X11 returns selected action but
+ // Wayland returns all allowed actions.
+
+ // So we need to call gdk_drag_context_get_selected_action() on Wayland
+ // to get potential D&D modifier.
+ // gdk_drag_context_get_selected_action() is also affected by
+ // gdk_drag_status(), see nsDragService::ReplyToDragMotion().
+ if (widget::GdkIsWaylandDisplay()) {
+ GdkDragAction gdkActionSelected =
+ gdk_drag_context_get_selected_action(aDragContext);
+ LOGDRAGSERVICE(" gdk_drag_context_get_selected_action() returns 0x%X",
+ gdkActionSelected);
+ if (gdkActionSelected) {
+ gdkAction = gdkActionSelected;
+ }
+ }
+ }
+
+ // set the default just in case nothing matches below
+ if (gdkAction & GDK_ACTION_DEFAULT) {
+ LOGDRAGSERVICE(" set default move");
+ action = nsIDragService::DRAGDROP_ACTION_MOVE;
+ }
+ // first check to see if move is set
+ if (gdkAction & GDK_ACTION_MOVE) {
+ LOGDRAGSERVICE(" set explicit move");
+ action = nsIDragService::DRAGDROP_ACTION_MOVE;
+ } else if (gdkAction & GDK_ACTION_LINK) {
+ // then fall to the others
+ LOGDRAGSERVICE(" set explicit link");
+ action = nsIDragService::DRAGDROP_ACTION_LINK;
+ } else if (gdkAction & GDK_ACTION_COPY) {
+ // copy is ctrl
+ LOGDRAGSERVICE(" set explicit copy");
+ action = nsIDragService::DRAGDROP_ACTION_COPY;
+ }
+
+ // update the drag information
+ SetDragAction(action);
+}
+
+void nsDragService::UpdateDragAction() { UpdateDragAction(mTargetDragContext); }
+
+NS_IMETHODIMP
+nsDragService::UpdateDragEffect() {
+ LOGDRAGSERVICE("nsDragService::UpdateDragEffect() from e10s child process");
+ if (mTargetDragContextForRemote) {
+ ReplyToDragMotion(mTargetDragContextForRemote, mTargetTime);
+ mTargetDragContextForRemote = nullptr;
+ }
+ return NS_OK;
+}
+
+void nsDragService::ReplyToDragMotion() {
+ if (mTargetDragContext) {
+ ReplyToDragMotion(mTargetDragContext, mTargetTime);
+ }
+}
+
+void nsDragService::DispatchMotionEvents() {
+ FireDragEventAtSource(eDrag, GetCurrentModifiers());
+ if (mTargetWindow) {
+ mTargetWindow->DispatchDragEvent(eDragOver, mTargetWindowPoint,
+ mTargetTime);
+ }
+}
+
+// Returns true if the drop was successful
+gboolean nsDragService::DispatchDropEvent() {
+ // We need to check IsDestroyed here because the nsRefPtr
+ // only protects this from being deleted, it does NOT protect
+ // against nsView::~nsView() calling Destroy() on it, bug 378273.
+ if (!mTargetWindow || mTargetWindow->IsDestroyed()) {
+ return FALSE;
+ }
+
+ EventMessage msg = mCanDrop ? eDrop : eDragExit;
+
+ mTargetWindow->DispatchDragEvent(msg, mTargetWindowPoint, mTargetTime);
+
+ return mCanDrop;
+}
+
+/* static */
+uint32_t nsDragService::GetCurrentModifiers() {
+ return mozilla::widget::KeymapWrapper::ComputeCurrentKeyModifiers();
+}
+
+#undef LOGDRAGSERVICE
diff --git a/widget/gtk/nsDragService.h b/widget/gtk/nsDragService.h
new file mode 100644
index 0000000000..8a45b7b8ed
--- /dev/null
+++ b/widget/gtk/nsDragService.h
@@ -0,0 +1,268 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=4 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsDragService_h__
+#define nsDragService_h__
+
+#include "mozilla/RefPtr.h"
+#include "nsBaseDragService.h"
+#include "nsCOMArray.h"
+#include "nsIObserver.h"
+#include <gtk/gtk.h>
+#include "nsITimer.h"
+#include "GUniquePtr.h"
+
+class nsICookieJarSettings;
+class nsWindow;
+
+namespace mozilla {
+namespace gfx {
+class SourceSurface;
+}
+} // namespace mozilla
+
+/**
+ * Native GTK DragService wrapper
+ */
+
+class nsDragService final : public nsBaseDragService, public nsIObserver {
+ public:
+ nsDragService();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIOBSERVER
+
+ // nsBaseDragService
+ MOZ_CAN_RUN_SCRIPT virtual nsresult InvokeDragSessionImpl(
+ nsIArray* anArrayTransferables,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ uint32_t aActionType) override;
+ // nsIDragService
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD InvokeDragSession(
+ nsINode* aDOMNode, nsIPrincipal* aPrincipal,
+ nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings,
+ nsIArray* anArrayTransferables, uint32_t aActionType,
+ nsContentPolicyType aContentPolicyType) override;
+ NS_IMETHOD StartDragSession() override;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndDragSession(bool aDoneDrag,
+ uint32_t aKeyModifiers) override;
+
+ // nsIDragSession
+ NS_IMETHOD SetCanDrop(bool aCanDrop) override;
+ NS_IMETHOD GetCanDrop(bool* aCanDrop) override;
+ NS_IMETHOD GetNumDropItems(uint32_t* aNumItems) override;
+ NS_IMETHOD GetData(nsITransferable* aTransferable,
+ uint32_t aItemIndex) override;
+ NS_IMETHOD IsDataFlavorSupported(const char* aDataFlavor,
+ bool* _retval) override;
+
+ // Update Drag&Drop state according child process state.
+ // UpdateDragEffect() is called by IPC bridge when child process
+ // accepts/denies D&D operation and uses stored
+ // mTargetDragContextForRemote context.
+ NS_IMETHOD UpdateDragEffect() override;
+
+ // Methods called from nsWindow to handle responding to GTK drag
+ // destination signals
+
+ static already_AddRefed<nsDragService> GetInstance();
+
+ void TargetDataReceived(GtkWidget* aWidget, GdkDragContext* aContext, gint aX,
+ gint aY, GtkSelectionData* aSelection_data,
+ guint aInfo, guint32 aTime);
+
+ gboolean ScheduleMotionEvent(nsWindow* aWindow, GdkDragContext* aDragContext,
+ mozilla::LayoutDeviceIntPoint aWindowPoint,
+ guint aTime);
+ void ScheduleLeaveEvent();
+ gboolean ScheduleDropEvent(nsWindow* aWindow, GdkDragContext* aDragContext,
+ mozilla::LayoutDeviceIntPoint aWindowPoint,
+ guint aTime);
+
+ nsWindow* GetMostRecentDestWindow() {
+ return mScheduledTask == eDragTaskNone ? mTargetWindow : mPendingWindow;
+ }
+
+ // END PUBLIC API
+
+ // These methods are public only so that they can be called from functions
+ // with C calling conventions. They are called for drags started with the
+ // invisible widget.
+ void SourceEndDragSession(GdkDragContext* aContext, gint aResult);
+ void SourceDataGet(GtkWidget* widget, GdkDragContext* context,
+ GtkSelectionData* selection_data, guint32 aTime);
+ bool SourceDataGetText(nsITransferable* aItem, const nsACString& aMIMEType,
+ bool aNeedToDoConversionToPlainText,
+ GtkSelectionData* aSelectionData);
+ void SourceDataGetImage(nsITransferable* aItem,
+ GtkSelectionData* aSelectionData);
+ void SourceDataGetXDND(nsITransferable* aItem, GdkDragContext* aContext,
+ GtkSelectionData* aSelectionData);
+ void SourceDataGetUriList(GdkDragContext* aContext,
+ GtkSelectionData* aSelectionData,
+ uint32_t aDragItems);
+ bool SourceDataAppendURLFileItem(nsACString& aURI, nsITransferable* aItem);
+ bool SourceDataAppendURLItem(nsITransferable* aItem, bool aExternalDrop,
+ nsACString& aURI);
+
+ void SourceBeginDrag(GdkDragContext* aContext);
+
+ // set the drag icon during drag-begin
+ void SetDragIcon(GdkDragContext* aContext);
+
+ class AutoEventLoop {
+ RefPtr<nsDragService> mService;
+
+ public:
+ explicit AutoEventLoop(RefPtr<nsDragService> aService)
+ : mService(std::move(aService)) {
+ mService->mEventLoopDepth++;
+ }
+ ~AutoEventLoop() { mService->mEventLoopDepth--; }
+ };
+ int GetLoopDepth() const { return mEventLoopDepth; };
+
+ protected:
+ virtual ~nsDragService();
+
+ private:
+ // mScheduledTask indicates what signal has been received from GTK and
+ // so what needs to be dispatched when the scheduled task is run. It is
+ // eDragTaskNone when there is no task scheduled (but the
+ // previous task may still not have finished running).
+ enum DragTask {
+ eDragTaskNone,
+ eDragTaskMotion,
+ eDragTaskLeave,
+ eDragTaskDrop,
+ eDragTaskSourceEnd
+ };
+ DragTask mScheduledTask;
+ // mTaskSource is the GSource id for the task that is either scheduled
+ // or currently running. It is 0 if no task is scheduled or running.
+ guint mTaskSource;
+ bool mScheduledTaskIsRunning;
+
+ // Where the drag begins. We need to keep it open on Wayland.
+ RefPtr<nsWindow> mSourceWindow;
+
+ // target/destination side vars
+ // These variables keep track of the state of the current drag.
+
+ // mPendingWindow, mPendingWindowPoint, mPendingDragContext, and
+ // mPendingTime, carry information from the GTK signal that will be used
+ // when the scheduled task is run. mPendingWindow and mPendingDragContext
+ // will be nullptr if the scheduled task is eDragTaskLeave.
+ RefPtr<nsWindow> mPendingWindow;
+ mozilla::LayoutDeviceIntPoint mPendingWindowPoint;
+ RefPtr<GdkDragContext> mPendingDragContext;
+
+ // We cache all data for the current drag context,
+ // because waiting for the data in GetTargetDragData can be very slow.
+ nsTHashMap<nsCStringHashKey, nsTArray<uint8_t>> mCachedData;
+ // mCachedData are tied to mCachedDragContext. mCachedDragContext is not
+ // ref counted and may be already deleted on Gtk side.
+ // We used it for mCachedData invalidation only and can't be used for
+ // any D&D operation.
+ uintptr_t mCachedDragContext;
+
+ nsTHashMap<nsCStringHashKey, mozilla::GUniquePtr<gchar*>> mCachedUris;
+
+ guint mPendingTime;
+
+ // mTargetWindow and mTargetWindowPoint record the position of the last
+ // eDragTaskMotion or eDragTaskDrop task that was run or is still running.
+ // mTargetWindow is cleared once the drag has completed or left.
+ RefPtr<nsWindow> mTargetWindow;
+ mozilla::LayoutDeviceIntPoint mTargetWindowPoint;
+ // mTargetWidget and mTargetDragContext are set only while dispatching
+ // motion or drop events. mTime records the corresponding timestamp.
+ RefPtr<GtkWidget> mTargetWidget;
+ RefPtr<GdkDragContext> mTargetDragContext;
+
+ // When we route D'n'D request to child process
+ // (by EventStateManager::DispatchCrossProcessEvent)
+ // we save GdkDragContext to mTargetDragContextForRemote.
+ // When we get a reply from child process we use
+ // the stored GdkDragContext to send reply to OS.
+ //
+ // We need to store GdkDragContext because mTargetDragContext is cleared
+ // after every D'n'D event.
+ RefPtr<GdkDragContext> mTargetDragContextForRemote;
+ guint mTargetTime;
+
+ // is it OK to drop on us?
+ bool mCanDrop;
+
+ // have we received our drag data?
+ bool mTargetDragDataReceived;
+ // last data received and its length
+ void* mTargetDragData;
+ uint32_t mTargetDragDataLen;
+ mozilla::GUniquePtr<gchar*> mTargetDragUris;
+ // is the current target drag context contain a list?
+ bool IsTargetContextList(void);
+ // this will get the native data from the last target given a
+ // specific flavor
+ void GetTargetDragData(GdkAtom aFlavor, nsTArray<nsCString>& aDropFlavors);
+ // this will reset all of the target vars
+ void TargetResetData(void);
+ // Ensure our data cache belongs to aDragContext and clear the cache if
+ // aDragContext is different than mCachedDragContext.
+ void EnsureCachedDataValidForContext(GdkDragContext* aDragContext);
+
+ // source side vars
+
+ // the source of our drags
+ GtkWidget* mHiddenWidget;
+ // our source data items
+ nsCOMPtr<nsIArray> mSourceDataItems;
+
+ // get a list of the sources in gtk's format
+ GtkTargetList* GetSourceList(void);
+
+ // attempts to create a semi-transparent drag image. Returns TRUE if
+ // successful, FALSE if not
+ bool SetAlphaPixmap(SourceSurface* aPixbuf, GdkDragContext* aContext,
+ int32_t aXOffset, int32_t aYOffset,
+ const mozilla::LayoutDeviceIntRect& dragRect);
+
+ gboolean Schedule(DragTask aTask, nsWindow* aWindow,
+ GdkDragContext* aDragContext,
+ mozilla::LayoutDeviceIntPoint aWindowPoint, guint aTime);
+
+ // Callback for g_idle_add_full() to run mScheduledTask.
+ MOZ_CAN_RUN_SCRIPT static gboolean TaskDispatchCallback(gpointer data);
+ MOZ_CAN_RUN_SCRIPT gboolean RunScheduledTask();
+ MOZ_CAN_RUN_SCRIPT void DispatchMotionEvents();
+ void ReplyToDragMotion(GdkDragContext* aDragContext, guint aTime);
+ void ReplyToDragMotion();
+ void UpdateDragAction(GdkDragContext* aDragContext);
+ void UpdateDragAction();
+
+#ifdef MOZ_LOGGING
+ const char* GetDragServiceTaskName(nsDragService::DragTask aTask);
+#endif
+ void GetDragFlavors(nsTArray<nsCString>& aFlavors);
+ gboolean DispatchDropEvent();
+ static uint32_t GetCurrentModifiers();
+
+ nsresult CreateTempFile(nsITransferable* aItem, nsACString& aURI);
+ bool RemoveTempFiles();
+ static gboolean TaskRemoveTempFiles(gpointer data);
+
+ // the url of the temporary file that has been created in the current drag
+ // session
+ nsTArray<nsCString> mTempFileUrls;
+ // stores all temporary files
+ nsCOMArray<nsIFile> mTemporaryFiles;
+ // timer to trigger deletion of temporary files
+ guint mTempFileTimerID;
+ // How deep we're nested in event loops
+ int mEventLoopDepth;
+};
+
+#endif // nsDragService_h__
diff --git a/widget/gtk/nsFilePicker.cpp b/widget/gtk/nsFilePicker.cpp
new file mode 100644
index 0000000000..751da3dc6b
--- /dev/null
+++ b/widget/gtk/nsFilePicker.cpp
@@ -0,0 +1,785 @@
+/* -*- 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 <dlfcn.h>
+#include <gtk/gtk.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "mozilla/Types.h"
+#include "AsyncDBus.h"
+#include "nsGtkUtils.h"
+#include "nsIFileURL.h"
+#include "nsIGIOService.h"
+#include "nsIURI.h"
+#include "nsIWidget.h"
+#include "nsIFile.h"
+#include "nsIStringBundle.h"
+#include "mozilla/Components.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/Promise.h"
+
+#include "nsArrayEnumerator.h"
+#include "nsEnumeratorUtils.h"
+#include "nsNetUtil.h"
+#include "nsReadableUtils.h"
+#include "MozContainer.h"
+#include "WidgetUtilsGtk.h"
+
+#include "nsFilePicker.h"
+
+#undef LOG
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+extern mozilla::LazyLogModule gWidgetLog;
+# define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args)
+#else
+# define LOG(args)
+#endif /* MOZ_LOGGING */
+
+using namespace mozilla;
+using mozilla::dom::Promise;
+
+#define MAX_PREVIEW_SIZE 180
+// bug 1184009
+#define MAX_PREVIEW_SOURCE_SIZE 4096
+
+nsIFile* nsFilePicker::mPrevDisplayDirectory = nullptr;
+
+void nsFilePicker::Shutdown() { NS_IF_RELEASE(mPrevDisplayDirectory); }
+
+static GtkFileChooserAction GetGtkFileChooserAction(nsIFilePicker::Mode aMode) {
+ GtkFileChooserAction action;
+
+ switch (aMode) {
+ case nsIFilePicker::modeSave:
+ action = GTK_FILE_CHOOSER_ACTION_SAVE;
+ break;
+
+ case nsIFilePicker::modeGetFolder:
+ action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
+ break;
+
+ case nsIFilePicker::modeOpen:
+ case nsIFilePicker::modeOpenMultiple:
+ action = GTK_FILE_CHOOSER_ACTION_OPEN;
+ break;
+
+ default:
+ NS_WARNING("Unknown nsIFilePicker mode");
+ action = GTK_FILE_CHOOSER_ACTION_OPEN;
+ break;
+ }
+
+ return action;
+}
+
+static void UpdateFilePreviewWidget(GtkFileChooser* file_chooser,
+ gpointer preview_widget_voidptr) {
+ GtkImage* preview_widget = GTK_IMAGE(preview_widget_voidptr);
+ char* image_filename = gtk_file_chooser_get_preview_filename(file_chooser);
+ struct stat st_buf;
+
+ if (!image_filename) {
+ gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
+ return;
+ }
+
+ gint preview_width = 0;
+ gint preview_height = 0;
+ /* check type of file
+ * if file is named pipe, Open is blocking which may lead to UI
+ * nonresponsiveness; if file is directory/socket, it also isn't
+ * likely to get preview */
+ if (stat(image_filename, &st_buf) || (!S_ISREG(st_buf.st_mode))) {
+ g_free(image_filename);
+ gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
+ return; /* stat failed or file is not regular */
+ }
+
+ GdkPixbufFormat* preview_format =
+ gdk_pixbuf_get_file_info(image_filename, &preview_width, &preview_height);
+ if (!preview_format || preview_width <= 0 || preview_height <= 0 ||
+ preview_width > MAX_PREVIEW_SOURCE_SIZE ||
+ preview_height > MAX_PREVIEW_SOURCE_SIZE) {
+ g_free(image_filename);
+ gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
+ return;
+ }
+
+ GdkPixbuf* preview_pixbuf = nullptr;
+ // Only scale down images that are too big
+ if (preview_width > MAX_PREVIEW_SIZE || preview_height > MAX_PREVIEW_SIZE) {
+ preview_pixbuf = gdk_pixbuf_new_from_file_at_size(
+ image_filename, MAX_PREVIEW_SIZE, MAX_PREVIEW_SIZE, nullptr);
+ } else {
+ preview_pixbuf = gdk_pixbuf_new_from_file(image_filename, nullptr);
+ }
+
+ g_free(image_filename);
+
+ if (!preview_pixbuf) {
+ gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
+ return;
+ }
+
+ GdkPixbuf* preview_pixbuf_temp = preview_pixbuf;
+ preview_pixbuf = gdk_pixbuf_apply_embedded_orientation(preview_pixbuf_temp);
+ g_object_unref(preview_pixbuf_temp);
+
+ // This is the easiest way to do center alignment without worrying about
+ // containers Minimum 3px padding each side (hence the 6) just to make things
+ // nice
+ gint x_padding =
+ (MAX_PREVIEW_SIZE + 6 - gdk_pixbuf_get_width(preview_pixbuf)) / 2;
+ gtk_misc_set_padding(GTK_MISC(preview_widget), x_padding, 0);
+
+ gtk_image_set_from_pixbuf(preview_widget, preview_pixbuf);
+ g_object_unref(preview_pixbuf);
+ gtk_file_chooser_set_preview_widget_active(file_chooser, TRUE);
+}
+
+static nsAutoCString MakeCaseInsensitiveShellGlob(const char* aPattern) {
+ // aPattern is UTF8
+ nsAutoCString result;
+ unsigned int len = strlen(aPattern);
+
+ for (unsigned int i = 0; i < len; i++) {
+ if (!g_ascii_isalpha(aPattern[i])) {
+ // non-ASCII characters will also trigger this path, so unicode
+ // is safely handled albeit case-sensitively
+ result.Append(aPattern[i]);
+ continue;
+ }
+
+ // add the lowercase and uppercase version of a character to a bracket
+ // match, so it matches either the lowercase or uppercase char.
+ result.Append('[');
+ result.Append(g_ascii_tolower(aPattern[i]));
+ result.Append(g_ascii_toupper(aPattern[i]));
+ result.Append(']');
+ }
+
+ return result;
+}
+
+NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
+
+nsFilePicker::nsFilePicker()
+ : mSelectedType(0), mAllowURLs(false), mFileChooserDelegate(nullptr) {
+ mUseNativeFileChooser =
+ widget::ShouldUsePortal(widget::PortalKind::FilePicker);
+}
+
+nsFilePicker::~nsFilePicker() = default;
+
+void ReadMultipleFiles(gpointer filename, gpointer array) {
+ nsCOMPtr<nsIFile> localfile;
+ nsresult rv =
+ NS_NewNativeLocalFile(nsDependentCString(static_cast<char*>(filename)),
+ false, getter_AddRefs(localfile));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMArray<nsIFile>& files = *static_cast<nsCOMArray<nsIFile>*>(array);
+ files.AppendObject(localfile);
+ }
+
+ g_free(filename);
+}
+
+void nsFilePicker::ReadValuesFromFileChooser(void* file_chooser) {
+ mFiles.Clear();
+
+ if (mMode == nsIFilePicker::modeOpenMultiple) {
+ mFileURL.Truncate();
+
+ GSList* list =
+ gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser));
+ g_slist_foreach(list, ReadMultipleFiles, static_cast<gpointer>(&mFiles));
+ g_slist_free(list);
+ } else {
+ gchar* filename = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser));
+ mFileURL.Assign(filename);
+ g_free(filename);
+ }
+
+ GtkFileFilter* filter =
+ gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(file_chooser));
+ GSList* filter_list =
+ gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(file_chooser));
+
+ mSelectedType = static_cast<int16_t>(g_slist_index(filter_list, filter));
+ g_slist_free(filter_list);
+
+ // Remember last used directory.
+ nsCOMPtr<nsIFile> file;
+ GetFile(getter_AddRefs(file));
+ if (file) {
+ nsCOMPtr<nsIFile> dir;
+ file->GetParent(getter_AddRefs(dir));
+ if (dir) {
+ dir.swap(mPrevDisplayDirectory);
+ }
+ }
+}
+
+void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
+ mParentWidget = aParent;
+ mTitle.Assign(aTitle);
+}
+
+NS_IMETHODIMP
+nsFilePicker::IsModeSupported(nsIFilePicker::Mode aMode, JSContext* aCx,
+ Promise** aRetPromise) {
+#ifdef MOZ_ENABLE_DBUS
+ if (!widget::ShouldUsePortal(widget::PortalKind::FilePicker) ||
+ aMode != nsIFilePicker::modeGetFolder) {
+ return nsBaseFilePicker::IsModeSupported(aMode, aCx, aRetPromise);
+ }
+
+ const char kFreedesktopPortalName[] = "org.freedesktop.portal.Desktop";
+ const char kFreedesktopPortalPath[] = "/org/freedesktop/portal/desktop";
+ const char kFreedesktopPortalFileChooser[] =
+ "org.freedesktop.portal.FileChooser";
+
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aRetPromise);
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> retPromise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ widget::CreateDBusProxyForBus(
+ G_BUS_TYPE_SESSION,
+ GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS),
+ /* aInterfaceInfo = */ nullptr, kFreedesktopPortalName,
+ kFreedesktopPortalPath, kFreedesktopPortalFileChooser)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [retPromise](RefPtr<GDBusProxy>&& aProxy) {
+ const char kFreedesktopPortalVersionProperty[] = "version";
+ // Folder selection was added in version 3 of xdg-desktop-portal
+ const uint32_t kFreedesktopPortalMinimumVersion = 3;
+ uint32_t foundVersion = 0;
+
+ RefPtr<GVariant> property =
+ dont_AddRef(g_dbus_proxy_get_cached_property(
+ aProxy, kFreedesktopPortalVersionProperty));
+
+ if (property) {
+ foundVersion = g_variant_get_uint32(property);
+ LOG(("Found portal version: %u", foundVersion));
+ }
+
+ retPromise->MaybeResolve(foundVersion >=
+ kFreedesktopPortalMinimumVersion);
+ },
+ [retPromise](GUniquePtr<GError>&& aError) {
+ g_printerr("Failed to create DBUS proxy: %s\n", aError->message);
+ retPromise->MaybeReject(NS_ERROR_FAILURE);
+ });
+
+ retPromise.forget(aRetPromise);
+ return NS_OK;
+#else
+ return nsBaseFilePicker::IsModeSupported(aMode, aCx, aRetPromise);
+#endif
+}
+
+NS_IMETHODIMP
+nsFilePicker::AppendFilters(int32_t aFilterMask) {
+ mAllowURLs = !!(aFilterMask & filterAllowURLs);
+ return nsBaseFilePicker::AppendFilters(aFilterMask);
+}
+
+NS_IMETHODIMP
+nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
+ if (aFilter.EqualsLiteral("..apps")) {
+ // No platform specific thing we can do here, really....
+ return NS_OK;
+ }
+
+ nsAutoCString filter, name;
+ CopyUTF16toUTF8(aFilter, filter);
+ CopyUTF16toUTF8(aTitle, name);
+
+ mFilters.AppendElement(filter);
+ mFilterNames.AppendElement(name);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::SetDefaultString(const nsAString& aString) {
+ mDefault = aString;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetDefaultString(nsAString& aString) {
+ // Per API...
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsFilePicker::SetDefaultExtension(const nsAString& aExtension) {
+ mDefaultExtension = aExtension;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetDefaultExtension(nsAString& aExtension) {
+ aExtension = mDefaultExtension;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
+ *aFilterIndex = mSelectedType;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
+ mSelectedType = aFilterIndex;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+
+ *aFile = nullptr;
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = GetFileURL(getter_AddRefs(uri));
+ if (!uri) return rv;
+
+ nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(uri, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> file;
+ rv = fileURL->GetFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ file.forget(aFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetFileURL(nsIURI** aFileURL) {
+ *aFileURL = nullptr;
+ return NS_NewURI(aFileURL, mFileURL);
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
+ NS_ENSURE_ARG_POINTER(aFiles);
+
+ if (mMode == nsIFilePicker::modeOpenMultiple) {
+ return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsFilePicker::Show(nsIFilePicker::ResultCode* aReturn) {
+ NS_ENSURE_ARG_POINTER(aReturn);
+
+ nsresult rv = Open(nullptr);
+ if (NS_FAILED(rv)) return rv;
+
+ while (mFileChooser) {
+ g_main_context_iteration(nullptr, TRUE);
+ }
+
+ *aReturn = mResult;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
+ // Can't show two dialogs concurrently with the same filepicker
+ if (mFileChooser) return NS_ERROR_NOT_AVAILABLE;
+
+ if (MaybeBlockFilePicker(aCallback)) {
+ return NS_OK;
+ }
+
+ NS_ConvertUTF16toUTF8 title(mTitle);
+
+ GtkWindow* parent_widget =
+ GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET));
+
+ GtkFileChooserAction action = GetGtkFileChooserAction(mMode);
+
+ const gchar* accept_button;
+ NS_ConvertUTF16toUTF8 buttonLabel(mOkButtonLabel);
+ if (!mOkButtonLabel.IsEmpty()) {
+ accept_button = buttonLabel.get();
+ } else {
+ accept_button = nullptr;
+ }
+
+ void* file_chooser =
+ GtkFileChooserNew(title.get(), parent_widget, action, accept_button);
+
+ // If we have --enable-proxy-bypass-protection, then don't allow
+ // remote URLs to be used.
+#ifndef MOZ_PROXY_BYPASS_PROTECTION
+ if (mAllowURLs) {
+ gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser), FALSE);
+ }
+#endif
+
+ if (action == GTK_FILE_CHOOSER_ACTION_OPEN ||
+ action == GTK_FILE_CHOOSER_ACTION_SAVE) {
+ GtkWidget* img_preview = gtk_image_new();
+ gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(file_chooser),
+ img_preview);
+ g_signal_connect(file_chooser, "update-preview",
+ G_CALLBACK(UpdateFilePreviewWidget), img_preview);
+ }
+
+ GtkFileChooserSetModal(file_chooser, parent_widget, TRUE);
+
+ NS_ConvertUTF16toUTF8 defaultName(mDefault);
+ switch (mMode) {
+ case nsIFilePicker::modeOpenMultiple:
+ gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser),
+ TRUE);
+ break;
+ case nsIFilePicker::modeSave:
+ gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser),
+ defaultName.get());
+ break;
+
+ default:
+ /* no additional setup needed */
+ break;
+ }
+
+ nsCOMPtr<nsIFile> defaultPath;
+ if (mDisplayDirectory) {
+ mDisplayDirectory->Clone(getter_AddRefs(defaultPath));
+ } else if (mPrevDisplayDirectory) {
+ mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath));
+ }
+
+ if (defaultPath) {
+ if (!defaultName.IsEmpty() && mMode != nsIFilePicker::modeSave) {
+ // Try to select the intended file. Even if it doesn't exist, GTK still
+ // switches directories.
+ defaultPath->AppendNative(defaultName);
+ nsAutoCString path;
+ defaultPath->GetNativePath(path);
+ gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser), path.get());
+ } else {
+ nsAutoCString directory;
+ defaultPath->GetNativePath(directory);
+
+ // Workaround for problematic refcounting in GTK3 before 3.16.
+ // We need to keep a reference to the dialog's internal delegate.
+ // Otherwise, if our dialog gets destroyed, we'll lose the dialog's
+ // delegate by the time this gets processed in the event loop.
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1166741
+ if (GTK_IS_DIALOG(file_chooser)) {
+ GtkDialog* dialog = GTK_DIALOG(file_chooser);
+ GtkContainer* area = GTK_CONTAINER(gtk_dialog_get_content_area(dialog));
+ gtk_container_forall(
+ area,
+ [](GtkWidget* widget, gpointer data) {
+ if (GTK_IS_FILE_CHOOSER_WIDGET(widget)) {
+ auto result = static_cast<GtkFileChooserWidget**>(data);
+ *result = GTK_FILE_CHOOSER_WIDGET(widget);
+ }
+ },
+ &mFileChooserDelegate);
+
+ if (mFileChooserDelegate != nullptr) {
+ g_object_ref(mFileChooserDelegate);
+ }
+ }
+ gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser),
+ directory.get());
+ }
+ }
+
+ if (GTK_IS_DIALOG(file_chooser)) {
+ gtk_dialog_set_default_response(GTK_DIALOG(file_chooser),
+ GTK_RESPONSE_ACCEPT);
+ }
+
+ int32_t count = mFilters.Length();
+ for (int32_t i = 0; i < count; ++i) {
+ // This is fun... the GTK file picker does not accept a list of filters
+ // so we need to split out each string, and add it manually.
+
+ char** patterns = g_strsplit(mFilters[i].get(), ";", -1);
+ if (!patterns) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ GtkFileFilter* filter = gtk_file_filter_new();
+ for (int j = 0; patterns[j] != nullptr; ++j) {
+ nsAutoCString caseInsensitiveFilter =
+ MakeCaseInsensitiveShellGlob(g_strstrip(patterns[j]));
+ gtk_file_filter_add_pattern(filter, caseInsensitiveFilter.get());
+ }
+
+ g_strfreev(patterns);
+
+ if (!mFilterNames[i].IsEmpty()) {
+ // If we have a name for our filter, let's use that.
+ const char* filter_name = mFilterNames[i].get();
+ gtk_file_filter_set_name(filter, filter_name);
+ } else {
+ // If we don't have a name, let's just use the filter pattern.
+ const char* filter_pattern = mFilters[i].get();
+ gtk_file_filter_set_name(filter, filter_pattern);
+ }
+
+ gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
+
+ // Set the initially selected filter
+ if (mSelectedType == i) {
+ gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter);
+ }
+ }
+
+ gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser),
+ TRUE);
+
+ mFileChooser = file_chooser;
+ mCallback = aCallback;
+ NS_ADDREF_THIS();
+ g_signal_connect(file_chooser, "response", G_CALLBACK(OnResponse), this);
+ GtkFileChooserShow(file_chooser);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::Close() {
+ if (mFileChooser) {
+ // Call ourself as done.
+ Done(mFileChooser, GTK_RESPONSE_CLOSE);
+ }
+
+ return NS_OK;
+}
+
+/* static */
+void nsFilePicker::OnResponse(void* file_chooser, gint response_id,
+ gpointer user_data) {
+ static_cast<nsFilePicker*>(user_data)->Done(file_chooser, response_id);
+}
+
+/* static */
+void nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data) {
+ static_cast<nsFilePicker*>(user_data)->Done(file_chooser,
+ GTK_RESPONSE_CANCEL);
+}
+
+bool nsFilePicker::WarnForNonReadableFile(void* file_chooser) {
+ nsCOMPtr<nsIFile> file;
+ GetFile(getter_AddRefs(file));
+ if (!file) {
+ return false;
+ }
+
+ bool isReadable = false;
+ file->IsReadable(&isReadable);
+ if (isReadable) {
+ return false;
+ }
+
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (!stringService) {
+ return false;
+ }
+
+ nsCOMPtr<nsIStringBundle> filepickerBundle;
+ nsresult rv = stringService->CreateBundle(
+ "chrome://global/locale/filepicker.properties",
+ getter_AddRefs(filepickerBundle));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsAutoString errorMessage;
+ rv = filepickerBundle->GetStringFromName("selectedFileNotReadableError",
+ errorMessage);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ GtkDialogFlags flags = GTK_DIALOG_DESTROY_WITH_PARENT;
+ auto* cancel_dialog = gtk_message_dialog_new(
+ GTK_WINDOW(file_chooser), flags, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
+ "%s", NS_ConvertUTF16toUTF8(errorMessage).get());
+ gtk_dialog_run(GTK_DIALOG(cancel_dialog));
+ gtk_widget_destroy(cancel_dialog);
+
+ return true;
+}
+
+void nsFilePicker::Done(void* file_chooser, gint response) {
+ mFileChooser = nullptr;
+
+ nsIFilePicker::ResultCode result;
+ switch (response) {
+ case GTK_RESPONSE_OK:
+ case GTK_RESPONSE_ACCEPT:
+ ReadValuesFromFileChooser(file_chooser);
+ result = nsIFilePicker::returnOK;
+ if (mMode == nsIFilePicker::modeSave) {
+ nsCOMPtr<nsIFile> file;
+ GetFile(getter_AddRefs(file));
+ if (file) {
+ bool exists = false;
+ file->Exists(&exists);
+ if (exists) result = nsIFilePicker::returnReplace;
+ }
+ } else if (mMode == nsIFilePicker::modeOpen) {
+ if (WarnForNonReadableFile(file_chooser)) {
+ result = nsIFilePicker::returnCancel;
+ }
+ }
+ break;
+
+ case GTK_RESPONSE_CANCEL:
+ case GTK_RESPONSE_CLOSE:
+ case GTK_RESPONSE_DELETE_EVENT:
+ result = nsIFilePicker::returnCancel;
+ break;
+
+ default:
+ NS_WARNING("Unexpected response");
+ result = nsIFilePicker::returnCancel;
+ break;
+ }
+
+ // A "response" signal won't be sent again but "destroy" will be.
+ g_signal_handlers_disconnect_by_func(file_chooser, FuncToGpointer(OnDestroy),
+ this);
+
+ // When response_id is GTK_RESPONSE_DELETE_EVENT or when called from
+ // OnDestroy, the widget would be destroyed anyway but it is fine if
+ // gtk_widget_destroy is called more than once. gtk_widget_destroy has
+ // requests that any remaining references be released, but the reference
+ // count will not be decremented again if GtkWindow's reference has already
+ // been released.
+ GtkFileChooserDestroy(file_chooser);
+
+ if (mFileChooserDelegate) {
+ // Properly deref our acquired reference. We call this after
+ // gtk_widget_destroy() to try and ensure that pending file info
+ // queries caused by updating the current folder have been cancelled.
+ // However, we do not know for certain when the callback will run after
+ // cancelled.
+ g_idle_add(
+ [](gpointer data) -> gboolean {
+ g_object_unref(data);
+ return G_SOURCE_REMOVE;
+ },
+ mFileChooserDelegate);
+ mFileChooserDelegate = nullptr;
+ }
+
+ if (mCallback) {
+ mCallback->Done(result);
+ mCallback = nullptr;
+ } else {
+ mResult = result;
+ }
+ NS_RELEASE_THIS();
+}
+
+// All below functions available as of GTK 3.20+
+void* nsFilePicker::GtkFileChooserNew(const gchar* title, GtkWindow* parent,
+ GtkFileChooserAction action,
+ const gchar* accept_label) {
+ static auto sGtkFileChooserNativeNewPtr =
+ (void* (*)(const gchar*, GtkWindow*, GtkFileChooserAction, const gchar*,
+ const gchar*))dlsym(RTLD_DEFAULT,
+ "gtk_file_chooser_native_new");
+ if (mUseNativeFileChooser && sGtkFileChooserNativeNewPtr != nullptr) {
+ return (*sGtkFileChooserNativeNewPtr)(title, parent, action, accept_label,
+ nullptr);
+ }
+ if (accept_label == nullptr) {
+ accept_label = (action == GTK_FILE_CHOOSER_ACTION_SAVE) ? GTK_STOCK_SAVE
+ : GTK_STOCK_OPEN;
+ }
+ GtkWidget* file_chooser = gtk_file_chooser_dialog_new(
+ title, parent, action, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ accept_label, GTK_RESPONSE_ACCEPT, nullptr);
+ gtk_dialog_set_alternative_button_order(
+ GTK_DIALOG(file_chooser), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_CANCEL, -1);
+ return file_chooser;
+}
+
+void nsFilePicker::GtkFileChooserShow(void* file_chooser) {
+ static auto sGtkNativeDialogShowPtr =
+ (void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_show");
+ if (mUseNativeFileChooser && sGtkNativeDialogShowPtr != nullptr) {
+ const char* portalEnvString = g_getenv("GTK_USE_PORTAL");
+ bool setPortalEnv =
+ (portalEnvString && *portalEnvString == '0') || !portalEnvString;
+ if (setPortalEnv) {
+ setenv("GTK_USE_PORTAL", "1", true);
+ }
+ (*sGtkNativeDialogShowPtr)(file_chooser);
+ if (setPortalEnv) {
+ unsetenv("GTK_USE_PORTAL");
+ }
+ } else {
+ g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this);
+ gtk_widget_show(GTK_WIDGET(file_chooser));
+ }
+}
+
+void nsFilePicker::GtkFileChooserDestroy(void* file_chooser) {
+ static auto sGtkNativeDialogDestroyPtr =
+ (void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_destroy");
+ if (mUseNativeFileChooser && sGtkNativeDialogDestroyPtr != nullptr) {
+ (*sGtkNativeDialogDestroyPtr)(file_chooser);
+ } else {
+ gtk_widget_destroy(GTK_WIDGET(file_chooser));
+ }
+}
+
+void nsFilePicker::GtkFileChooserSetModal(void* file_chooser,
+ GtkWindow* parent_widget,
+ gboolean modal) {
+ static auto sGtkNativeDialogSetModalPtr = (void (*)(void*, gboolean))dlsym(
+ RTLD_DEFAULT, "gtk_native_dialog_set_modal");
+ if (mUseNativeFileChooser && sGtkNativeDialogSetModalPtr != nullptr) {
+ (*sGtkNativeDialogSetModalPtr)(file_chooser, modal);
+ } else {
+ GtkWindow* window = GTK_WINDOW(file_chooser);
+ gtk_window_set_modal(window, modal);
+ if (parent_widget != nullptr) {
+ gtk_window_set_destroy_with_parent(window, modal);
+ }
+ }
+}
+
+#undef LOG
diff --git a/widget/gtk/nsFilePicker.h b/widget/gtk/nsFilePicker.h
new file mode 100644
index 0000000000..f8fc22bc97
--- /dev/null
+++ b/widget/gtk/nsFilePicker.h
@@ -0,0 +1,96 @@
+/* -*- 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/. */
+
+#ifndef nsFilePicker_h__
+#define nsFilePicker_h__
+
+#include <gtk/gtk.h>
+
+#include "nsBaseFilePicker.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsCOMArray.h"
+
+class nsIWidget;
+class nsIFile;
+
+class nsFilePicker : public nsBaseFilePicker {
+ public:
+ nsFilePicker();
+
+ NS_DECL_ISUPPORTS
+
+ // nsIFilePicker (less what's in nsBaseFilePicker)
+ NS_IMETHOD Open(nsIFilePickerShownCallback* aCallback) override;
+ NS_IMETHOD Close() override;
+ NS_IMETHOD IsModeSupported(nsIFilePicker::Mode, JSContext*,
+ mozilla::dom::Promise**) override;
+ NS_IMETHOD AppendFilters(int32_t aFilterMask) override;
+ NS_IMETHOD AppendFilter(const nsAString& aTitle,
+ const nsAString& aFilter) override;
+ NS_IMETHOD SetDefaultString(const nsAString& aString) override;
+ NS_IMETHOD GetDefaultString(nsAString& aString) override;
+ NS_IMETHOD SetDefaultExtension(const nsAString& aExtension) override;
+ NS_IMETHOD GetDefaultExtension(nsAString& aExtension) override;
+ NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override;
+ NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override;
+ NS_IMETHOD GetFile(nsIFile** aFile) override;
+ NS_IMETHOD GetFileURL(nsIURI** aFileURL) override;
+ NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override;
+
+ // nsBaseFilePicker
+ virtual void InitNative(nsIWidget* aParent, const nsAString& aTitle) override;
+
+ static void Shutdown();
+
+ protected:
+ virtual ~nsFilePicker();
+
+ nsresult Show(nsIFilePicker::ResultCode* aReturn) override;
+ void ReadValuesFromFileChooser(void* file_chooser);
+
+ bool WarnForNonReadableFile(void* file_chooser);
+
+ static void OnResponse(void* file_chooser, gint response_id,
+ gpointer user_data);
+ static void OnDestroy(GtkWidget* file_chooser, gpointer user_data);
+ void Done(void* file_chooser, gint response_id);
+
+ nsCOMPtr<nsIWidget> mParentWidget;
+ nsCOMPtr<nsIFilePickerShownCallback> mCallback;
+ nsCOMArray<nsIFile> mFiles;
+
+ int16_t mSelectedType;
+ nsIFilePicker::ResultCode mResult;
+ bool mAllowURLs;
+ nsCString mFileURL;
+ nsString mTitle;
+ nsString mDefault;
+ nsString mDefaultExtension;
+
+ nsTArray<nsCString> mFilters;
+ nsTArray<nsCString> mFilterNames;
+
+ private:
+ static nsIFile* mPrevDisplayDirectory;
+
+ void* GtkFileChooserNew(const gchar* title, GtkWindow* parent,
+ GtkFileChooserAction action,
+ const gchar* accept_label);
+ void GtkFileChooserShow(void* file_chooser);
+ void GtkFileChooserDestroy(void* file_chooser);
+ void GtkFileChooserSetModal(void* file_chooser, GtkWindow* parent_widget,
+ gboolean modal);
+
+ GtkFileChooserWidget* mFileChooserDelegate;
+ bool mUseNativeFileChooser;
+
+ /**
+ * mFileChooser is non-null while open.
+ */
+ void* mFileChooser = nullptr;
+};
+
+#endif
diff --git a/widget/gtk/nsGTKToolkit.h b/widget/gtk/nsGTKToolkit.h
new file mode 100644
index 0000000000..5129ba419d
--- /dev/null
+++ b/widget/gtk/nsGTKToolkit.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef GTKTOOLKIT_H
+#define GTKTOOLKIT_H
+
+#include "nsString.h"
+#include <gtk/gtk.h>
+
+/**
+ * Wrapper around the thread running the message pump.
+ * The toolkit abstraction is necessary because the message pump must
+ * execute within the same thread that created the widget under Win32.
+ */
+
+class nsGTKToolkit final {
+ public:
+ nsGTKToolkit() = default;
+
+ static nsGTKToolkit* GetToolkit();
+ static void Shutdown() {
+ delete gToolkit;
+ gToolkit = nullptr;
+ }
+
+ /**
+ * Get/set our startup token value. (XDG_ACTIVATION_TOKEN/DESKTOP_STARTUP_ID)
+ * When non-empty, this is applied to the next toplevel window to be shown or
+ * focused (and then immediately cleared).
+ */
+ void SetStartupToken(const nsACString& aToken) { mStartupToken = aToken; }
+ const nsCString& GetStartupToken() const { return mStartupToken; }
+
+ /**
+ * Get/set the timestamp value to be used, if non-zero, to focus the
+ * next top-level window to be shown or focused (upon which it is cleared).
+ */
+ void SetFocusTimestamp(uint32_t aTimestamp) { mFocusTimestamp = aTimestamp; }
+ uint32_t GetFocusTimestamp() const { return mFocusTimestamp; }
+
+ private:
+ static nsGTKToolkit* gToolkit;
+
+ nsCString mStartupToken;
+ uint32_t mFocusTimestamp = 0;
+};
+
+#endif // GTKTOOLKIT_H
diff --git a/widget/gtk/nsGtkCursors.h b/widget/gtk/nsGtkCursors.h
new file mode 100644
index 0000000000..f30ed593a9
--- /dev/null
+++ b/widget/gtk/nsGtkCursors.h
@@ -0,0 +1,416 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef nsGtkCursors_h__
+#define nsGtkCursors_h__
+
+typedef struct {
+ const unsigned char* bits;
+ const unsigned char* mask_bits;
+ int hot_x;
+ int hot_y;
+ const char* hash;
+} nsGtkCursor;
+
+/* MOZ_CURSOR_HAND_GRAB */
+static const unsigned char moz_hand_grab_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00,
+ 0x60, 0x39, 0x00, 0x00, 0x90, 0x49, 0x00, 0x00, 0x90, 0x49, 0x01, 0x00,
+ 0x20, 0xc9, 0x02, 0x00, 0x20, 0x49, 0x02, 0x00, 0x58, 0x40, 0x02, 0x00,
+ 0x64, 0x00, 0x02, 0x00, 0x44, 0x00, 0x01, 0x00, 0x08, 0x00, 0x01, 0x00,
+ 0x10, 0x00, 0x01, 0x00, 0x10, 0x80, 0x00, 0x00, 0x20, 0x80, 0x00, 0x00,
+ 0x40, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_hand_grab_mask_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x60, 0x3f, 0x00, 0x00,
+ 0xf0, 0x7f, 0x00, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf8, 0xff, 0x03, 0x00,
+ 0xf0, 0xff, 0x07, 0x00, 0xf8, 0xff, 0x07, 0x00, 0xfc, 0xff, 0x07, 0x00,
+ 0xfe, 0xff, 0x07, 0x00, 0xfe, 0xff, 0x03, 0x00, 0xfc, 0xff, 0x03, 0x00,
+ 0xf8, 0xff, 0x03, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x01, 0x00,
+ 0xe0, 0xff, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_HAND_GRABBING */
+static const unsigned char moz_hand_grabbing_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xc0, 0x36, 0x00, 0x00, 0x20, 0xc9, 0x00, 0x00, 0x20, 0x40, 0x01, 0x00,
+ 0x40, 0x00, 0x01, 0x00, 0x60, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, 0x00,
+ 0x10, 0x00, 0x01, 0x00, 0x10, 0x80, 0x00, 0x00, 0x20, 0x80, 0x00, 0x00,
+ 0x40, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00, 0x80, 0x40, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_hand_grabbing_mask_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x36, 0x00, 0x00,
+ 0xe0, 0xff, 0x00, 0x00, 0xf0, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x03, 0x00,
+ 0xe0, 0xff, 0x03, 0x00, 0xf0, 0xff, 0x03, 0x00, 0xf8, 0xff, 0x03, 0x00,
+ 0xf8, 0xff, 0x03, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x01, 0x00,
+ 0xe0, 0xff, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00, 0xc0, 0x7f, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_COPY */
+static const unsigned char moz_copy_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+ 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00,
+ 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00,
+ 0xfc, 0x03, 0x00, 0x00, 0x7c, 0x30, 0x00, 0x00, 0x6c, 0x30, 0x00, 0x00,
+ 0xc4, 0xfc, 0x00, 0x00, 0xc0, 0xfc, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00,
+ 0x80, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_copy_mask_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00,
+ 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00,
+ 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00,
+ 0xfe, 0x37, 0x00, 0x00, 0xfe, 0x7b, 0x00, 0x00, 0xfe, 0xfc, 0x00, 0x00,
+ 0xee, 0xff, 0x01, 0x00, 0xe4, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x00, 0x00,
+ 0xc0, 0x7b, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_ALIAS */
+static const unsigned char moz_alias_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+ 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00,
+ 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00,
+ 0xfc, 0x03, 0x00, 0x00, 0x7c, 0xf0, 0x00, 0x00, 0x6c, 0xe0, 0x00, 0x00,
+ 0xc4, 0xf0, 0x00, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00,
+ 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_alias_mask_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00,
+ 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00,
+ 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00,
+ 0xfe, 0xf7, 0x00, 0x00, 0xfe, 0xfb, 0x01, 0x00, 0xfe, 0xf0, 0x01, 0x00,
+ 0xee, 0xf9, 0x01, 0x00, 0xe4, 0xf9, 0x01, 0x00, 0xc0, 0xbf, 0x00, 0x00,
+ 0xc0, 0x3f, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_CONTEXT_MENU */
+static const unsigned char moz_menu_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+ 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00,
+ 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0xfd, 0x00, 0x00,
+ 0xfc, 0xff, 0x00, 0x00, 0x7c, 0x84, 0x00, 0x00, 0x6c, 0xfc, 0x00, 0x00,
+ 0xc4, 0x84, 0x00, 0x00, 0xc0, 0xfc, 0x00, 0x00, 0x80, 0x85, 0x00, 0x00,
+ 0x80, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_menu_mask_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00,
+ 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00,
+ 0xfe, 0x00, 0x00, 0x00, 0xfe, 0xfd, 0x00, 0x00, 0xfe, 0xff, 0x01, 0x00,
+ 0xfe, 0xff, 0x01, 0x00, 0xfe, 0xff, 0x01, 0x00, 0xfe, 0xfe, 0x01, 0x00,
+ 0xee, 0xff, 0x01, 0x00, 0xe4, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00,
+ 0xc0, 0xff, 0x01, 0x00, 0x80, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_SPINNING */
+static const unsigned char moz_spinning_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+ 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00,
+ 0x7c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00,
+ 0xfc, 0x3b, 0x00, 0x00, 0x7c, 0x38, 0x00, 0x00, 0x6c, 0x54, 0x00, 0x00,
+ 0xc4, 0xdc, 0x00, 0x00, 0xc0, 0x44, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00,
+ 0x80, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_spinning_mask_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00,
+ 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00,
+ 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfe, 0x3b, 0x00, 0x00,
+ 0xfe, 0x7f, 0x00, 0x00, 0xfe, 0x7f, 0x00, 0x00, 0xfe, 0xfe, 0x00, 0x00,
+ 0xee, 0xff, 0x01, 0x00, 0xe4, 0xff, 0x00, 0x00, 0xc0, 0x7f, 0x00, 0x00,
+ 0xc0, 0x7f, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_ZOOM_IN */
+static const unsigned char moz_zoom_in_bits[] = {
+ 0xf0, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00,
+ 0x62, 0x04, 0x00, 0x00, 0x61, 0x08, 0x00, 0x00, 0xf9, 0x09, 0x00, 0x00,
+ 0xf9, 0x09, 0x00, 0x00, 0x61, 0x08, 0x00, 0x00, 0x62, 0x04, 0x00, 0x00,
+ 0x02, 0x04, 0x00, 0x00, 0x0c, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00,
+ 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_zoom_in_mask_bits[] = {
+ 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00,
+ 0xfe, 0x07, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00,
+ 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00,
+ 0xfe, 0x07, 0x00, 0x00, 0xfc, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00,
+ 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_ZOOM_OUT */
+static const unsigned char moz_zoom_out_bits[] = {
+ 0xf0, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00,
+ 0x02, 0x04, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0xf9, 0x09, 0x00, 0x00,
+ 0xf9, 0x09, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00,
+ 0x02, 0x04, 0x00, 0x00, 0x0c, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00,
+ 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_zoom_out_mask_bits[] = {
+ 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00,
+ 0xfe, 0x07, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00,
+ 0xff, 0x0f, 0x00, 0x00, 0xff, 0x0f, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00,
+ 0xfe, 0x07, 0x00, 0x00, 0xfc, 0x0f, 0x00, 0x00, 0xf0, 0x1c, 0x00, 0x00,
+ 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00,
+ 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_NOT_ALLOWED */
+static const unsigned char moz_not_allowed_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x80, 0x1f, 0x00, 0x00, 0xe0, 0x7f, 0x00, 0x00,
+ 0xf0, 0xf0, 0x00, 0x00, 0x38, 0xc0, 0x01, 0x00, 0x7c, 0x80, 0x03, 0x00,
+ 0xec, 0x00, 0x03, 0x00, 0xce, 0x01, 0x07, 0x00, 0x86, 0x03, 0x06, 0x00,
+ 0x06, 0x07, 0x06, 0x00, 0x06, 0x0e, 0x06, 0x00, 0x06, 0x1c, 0x06, 0x00,
+ 0x0e, 0x38, 0x07, 0x00, 0x0c, 0x70, 0x03, 0x00, 0x1c, 0xe0, 0x03, 0x00,
+ 0x38, 0xc0, 0x01, 0x00, 0xf0, 0xf0, 0x00, 0x00, 0xe0, 0x7f, 0x00, 0x00,
+ 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_not_allowed_mask_bits[] = {
+ 0x80, 0x1f, 0x00, 0x00, 0xe0, 0x7f, 0x00, 0x00, 0xf0, 0xff, 0x00, 0x00,
+ 0xf8, 0xff, 0x01, 0x00, 0xfc, 0xf0, 0x03, 0x00, 0xfe, 0xc0, 0x07, 0x00,
+ 0xfe, 0x81, 0x07, 0x00, 0xff, 0x83, 0x0f, 0x00, 0xcf, 0x07, 0x0f, 0x00,
+ 0x8f, 0x0f, 0x0f, 0x00, 0x0f, 0x1f, 0x0f, 0x00, 0x0f, 0x3e, 0x0f, 0x00,
+ 0x1f, 0xfc, 0x0f, 0x00, 0x1e, 0xf8, 0x07, 0x00, 0x3e, 0xf0, 0x07, 0x00,
+ 0xfc, 0xf0, 0x03, 0x00, 0xf8, 0xff, 0x01, 0x00, 0xf0, 0xff, 0x00, 0x00,
+ 0xe0, 0x7f, 0x00, 0x00, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_VERTICAL_TEXT */
+static const unsigned char moz_vertical_text_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00,
+ 0x06, 0x60, 0x00, 0x00, 0xfc, 0x3f, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00,
+ 0x02, 0x40, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_vertical_text_mask_bits[] = {
+ 0x07, 0xe0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00,
+ 0xff, 0xff, 0x00, 0x00, 0xfe, 0x7f, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00,
+ 0x0f, 0xf0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_NESW_RESIZE */
+static const unsigned char moz_nesw_resize_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
+ 0x00, 0xbe, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00,
+ 0x02, 0xb4, 0x00, 0x00, 0x02, 0xa2, 0x00, 0x00, 0x02, 0x81, 0x00, 0x00,
+ 0x8a, 0x80, 0x00, 0x00, 0x5a, 0x80, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00,
+ 0x7a, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
+ 0xfe, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_nesw_resize_mask_bits[] = {
+ 0xc0, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00,
+ 0x00, 0xff, 0x01, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00,
+ 0x07, 0xfe, 0x01, 0x00, 0x07, 0xf7, 0x01, 0x00, 0x8f, 0xe3, 0x01, 0x00,
+ 0xdf, 0xc1, 0x01, 0x00, 0xff, 0xc0, 0x01, 0x00, 0x7f, 0x00, 0x00, 0x00,
+ 0xff, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00,
+ 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_NWSE_RESIZE */
+static const unsigned char moz_nwse_resize_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
+ 0xfa, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00,
+ 0x5a, 0x80, 0x00, 0x00, 0x8a, 0x80, 0x00, 0x00, 0x02, 0x81, 0x00, 0x00,
+ 0x02, 0xa2, 0x00, 0x00, 0x02, 0xb4, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00,
+ 0x00, 0xbc, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
+ 0xc0, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_nwse_resize_mask_bits[] = {
+ 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00,
+ 0xff, 0x01, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00,
+ 0xff, 0xc0, 0x01, 0x00, 0xdf, 0xc1, 0x01, 0x00, 0x8f, 0xe3, 0x01, 0x00,
+ 0x07, 0xf7, 0x01, 0x00, 0x07, 0xfe, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00,
+ 0x00, 0xfe, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00,
+ 0xc0, 0xff, 0x01, 0x00, 0xc0, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+/* MOZ_CURSOR_NONE */
+static const unsigned char moz_none_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const unsigned char moz_none_mask_bits[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+enum {
+ MOZ_CURSOR_HAND_GRAB,
+ MOZ_CURSOR_HAND_GRABBING,
+ MOZ_CURSOR_COPY,
+ MOZ_CURSOR_ALIAS,
+ MOZ_CURSOR_CONTEXT_MENU,
+ MOZ_CURSOR_SPINNING,
+ MOZ_CURSOR_ZOOM_IN,
+ MOZ_CURSOR_ZOOM_OUT,
+ MOZ_CURSOR_NOT_ALLOWED,
+ MOZ_CURSOR_VERTICAL_TEXT,
+ MOZ_CURSOR_NESW_RESIZE,
+ MOZ_CURSOR_NWSE_RESIZE,
+ MOZ_CURSOR_NONE
+};
+
+// create custom pixmap cursor. The hash values must stay in sync with the
+// bitmap data above. To see the hash function, have a look at XcursorImageHash
+// in libXcursor
+static const nsGtkCursor GtkCursors[] = {
+ {moz_hand_grab_bits, moz_hand_grab_mask_bits, 10, 10,
+ "5aca4d189052212118709018842178c0"},
+ {moz_hand_grabbing_bits, moz_hand_grabbing_mask_bits, 10, 10,
+ "208530c400c041818281048008011002"},
+ {moz_copy_bits, moz_copy_mask_bits, 2, 2,
+ "08ffe1cb5fe6fc01f906f1c063814ccf"},
+ {moz_alias_bits, moz_alias_mask_bits, 2, 2,
+ "0876e1c15ff2fc01f906f1c363074c0f"},
+ {moz_menu_bits, moz_menu_mask_bits, 2, 2,
+ "08ffe1e65f80fcfdf9fff11263e74c48"},
+ {moz_spinning_bits, moz_spinning_mask_bits, 2, 2,
+ "08e8e1c95fe2fc01f976f1e063a24ccd"},
+ {moz_zoom_in_bits, moz_zoom_in_mask_bits, 6, 6,
+ "f41c0e382c94c0958e07017e42b00462"},
+ {moz_zoom_out_bits, moz_zoom_out_mask_bits, 6, 6,
+ "f41c0e382c97c0938e07017e42800402"},
+ {moz_not_allowed_bits, moz_not_allowed_mask_bits, 9, 9,
+ "03b6e0fcb3499374a867d041f52298f0"},
+ {moz_vertical_text_bits, moz_vertical_text_mask_bits, 8, 4,
+ "048008013003cff3c00c801001200000"},
+ {moz_nesw_resize_bits, moz_nesw_resize_mask_bits, 8, 8,
+ "50585d75b494802d0151028115016902"},
+ {moz_nwse_resize_bits, moz_nwse_resize_mask_bits, 8, 8,
+ "38c5dff7c7b8962045400281044508d2"},
+ {moz_none_bits, moz_none_mask_bits, 0, 0, nullptr}};
+
+#endif /* nsGtkCursors_h__ */
diff --git a/widget/gtk/nsGtkKeyUtils.cpp b/widget/gtk/nsGtkKeyUtils.cpp
new file mode 100644
index 0000000000..7157a09664
--- /dev/null
+++ b/widget/gtk/nsGtkKeyUtils.cpp
@@ -0,0 +1,2541 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 "mozilla/Logging.h"
+
+#include "nsGtkKeyUtils.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <algorithm>
+#include <gdk/gdk.h>
+#include <dlfcn.h>
+#include <gdk/gdkkeysyms-compat.h>
+#ifdef MOZ_X11
+# include <gdk/gdkx.h>
+# include <X11/XKBlib.h>
+# include "X11UndefineNone.h"
+#endif
+#include "IMContextWrapper.h"
+#include "WidgetUtils.h"
+#include "WidgetUtilsGtk.h"
+#include "x11/keysym2ucs.h"
+#include "nsContentUtils.h"
+#include "nsGtkUtils.h"
+#include "nsIBidiKeyboard.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsWindow.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+
+#ifdef MOZ_WAYLAND
+# include <sys/mman.h>
+# include "nsWaylandDisplay.h"
+#endif
+
+// For collecting other people's log, tell them `MOZ_LOG=KeyboardHandler:4,sync`
+// rather than `MOZ_LOG=KeyboardHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gKeyLog("KeyboardHandler");
+
+namespace mozilla {
+namespace widget {
+
+#define IS_ASCII_ALPHABETICAL(key) \
+ ((('a' <= key) && (key <= 'z')) || (('A' <= key) && (key <= 'Z')))
+
+#define MOZ_MODIFIER_KEYS "MozKeymapWrapper"
+
+KeymapWrapper* KeymapWrapper::sInstance = nullptr;
+guint KeymapWrapper::sLastRepeatableHardwareKeyCode = 0;
+#ifdef MOZ_X11
+Time KeymapWrapper::sLastRepeatableKeyTime = 0;
+#endif
+KeymapWrapper::RepeatState KeymapWrapper::sRepeatState =
+ KeymapWrapper::NOT_PRESSED;
+
+#ifdef MOZ_WAYLAND
+wl_seat* KeymapWrapper::sSeat = nullptr;
+int KeymapWrapper::sSeatID = -1;
+wl_keyboard* KeymapWrapper::sKeyboard = nullptr;
+#endif
+
+static const char* GetBoolName(bool aBool) { return aBool ? "TRUE" : "FALSE"; }
+
+static const char* GetStatusName(nsEventStatus aStatus) {
+ switch (aStatus) {
+ case nsEventStatus_eConsumeDoDefault:
+ return "nsEventStatus_eConsumeDoDefault";
+ case nsEventStatus_eConsumeNoDefault:
+ return "nsEventStatus_eConsumeNoDefault";
+ case nsEventStatus_eIgnore:
+ return "nsEventStatus_eIgnore";
+ case nsEventStatus_eSentinel:
+ return "nsEventStatus_eSentinel";
+ default:
+ return "Illegal value";
+ }
+}
+
+static const nsCString GetKeyLocationName(uint32_t aLocation) {
+ switch (aLocation) {
+ case eKeyLocationLeft:
+ return "KEY_LOCATION_LEFT"_ns;
+ case eKeyLocationRight:
+ return "KEY_LOCATION_RIGHT"_ns;
+ case eKeyLocationStandard:
+ return "KEY_LOCATION_STANDARD"_ns;
+ case eKeyLocationNumpad:
+ return "KEY_LOCATION_NUMPAD"_ns;
+ default:
+ return nsPrintfCString("Unknown (0x%04X)", aLocation);
+ }
+}
+
+static const nsCString GetCharacterCodeName(char16_t aChar) {
+ switch (aChar) {
+ case 0x0000:
+ return "NULL (0x0000)"_ns;
+ case 0x0008:
+ return "BACKSPACE (0x0008)"_ns;
+ case 0x0009:
+ return "CHARACTER TABULATION (0x0009)"_ns;
+ case 0x000A:
+ return "LINE FEED (0x000A)"_ns;
+ case 0x000B:
+ return "LINE TABULATION (0x000B)"_ns;
+ case 0x000C:
+ return "FORM FEED (0x000C)"_ns;
+ case 0x000D:
+ return "CARRIAGE RETURN (0x000D)"_ns;
+ case 0x0018:
+ return "CANCEL (0x0018)"_ns;
+ case 0x001B:
+ return "ESCAPE (0x001B)"_ns;
+ case 0x0020:
+ return "SPACE (0x0020)"_ns;
+ case 0x007F:
+ return "DELETE (0x007F)"_ns;
+ case 0x00A0:
+ return "NO-BREAK SPACE (0x00A0)"_ns;
+ case 0x00AD:
+ return "SOFT HYPHEN (0x00AD)"_ns;
+ case 0x2000:
+ return "EN QUAD (0x2000)"_ns;
+ case 0x2001:
+ return "EM QUAD (0x2001)"_ns;
+ case 0x2002:
+ return "EN SPACE (0x2002)"_ns;
+ case 0x2003:
+ return "EM SPACE (0x2003)"_ns;
+ case 0x2004:
+ return "THREE-PER-EM SPACE (0x2004)"_ns;
+ case 0x2005:
+ return "FOUR-PER-EM SPACE (0x2005)"_ns;
+ case 0x2006:
+ return "SIX-PER-EM SPACE (0x2006)"_ns;
+ case 0x2007:
+ return "FIGURE SPACE (0x2007)"_ns;
+ case 0x2008:
+ return "PUNCTUATION SPACE (0x2008)"_ns;
+ case 0x2009:
+ return "THIN SPACE (0x2009)"_ns;
+ case 0x200A:
+ return "HAIR SPACE (0x200A)"_ns;
+ case 0x200B:
+ return "ZERO WIDTH SPACE (0x200B)"_ns;
+ case 0x200C:
+ return "ZERO WIDTH NON-JOINER (0x200C)"_ns;
+ case 0x200D:
+ return "ZERO WIDTH JOINER (0x200D)"_ns;
+ case 0x200E:
+ return "LEFT-TO-RIGHT MARK (0x200E)"_ns;
+ case 0x200F:
+ return "RIGHT-TO-LEFT MARK (0x200F)"_ns;
+ case 0x2029:
+ return "PARAGRAPH SEPARATOR (0x2029)"_ns;
+ case 0x202A:
+ return "LEFT-TO-RIGHT EMBEDDING (0x202A)"_ns;
+ case 0x202B:
+ return "RIGHT-TO-LEFT EMBEDDING (0x202B)"_ns;
+ case 0x202D:
+ return "LEFT-TO-RIGHT OVERRIDE (0x202D)"_ns;
+ case 0x202E:
+ return "RIGHT-TO-LEFT OVERRIDE (0x202E)"_ns;
+ case 0x202F:
+ return "NARROW NO-BREAK SPACE (0x202F)"_ns;
+ case 0x205F:
+ return "MEDIUM MATHEMATICAL SPACE (0x205F)"_ns;
+ case 0x2060:
+ return "WORD JOINER (0x2060)"_ns;
+ case 0x2066:
+ return "LEFT-TO-RIGHT ISOLATE (0x2066)"_ns;
+ case 0x2067:
+ return "RIGHT-TO-LEFT ISOLATE (0x2067)"_ns;
+ case 0x3000:
+ return "IDEOGRAPHIC SPACE (0x3000)"_ns;
+ case 0xFEFF:
+ return "ZERO WIDTH NO-BREAK SPACE (0xFEFF)"_ns;
+ default: {
+ if (aChar < ' ' || (aChar >= 0x80 && aChar < 0xA0)) {
+ return nsPrintfCString("control (0x%04X)", aChar);
+ }
+ if (NS_IS_HIGH_SURROGATE(aChar)) {
+ return nsPrintfCString("high surrogate (0x%04X)", aChar);
+ }
+ if (NS_IS_LOW_SURROGATE(aChar)) {
+ return nsPrintfCString("low surrogate (0x%04X)", aChar);
+ }
+ return nsPrintfCString("'%s' (0x%04X)",
+ NS_ConvertUTF16toUTF8(nsAutoString(aChar)).get(),
+ aChar);
+ }
+ }
+}
+
+static const nsCString GetCharacterCodeNames(const char16_t* aChars,
+ uint32_t aLength) {
+ if (!aLength) {
+ return "\"\""_ns;
+ }
+ nsCString result;
+ result.AssignLiteral("\"");
+ StringJoinAppend(result, ", "_ns, Span{aChars, aLength},
+ [](nsACString& dest, const char16_t charValue) {
+ dest.Append(GetCharacterCodeName(charValue));
+ });
+ result.AppendLiteral("\"");
+ return result;
+}
+
+static const nsCString GetCharacterCodeNames(const nsAString& aString) {
+ return GetCharacterCodeNames(aString.BeginReading(), aString.Length());
+}
+
+/* static */
+const char* KeymapWrapper::GetModifierName(MappedModifier aModifier) {
+ switch (aModifier) {
+ case CAPS_LOCK:
+ return "CapsLock";
+ case NUM_LOCK:
+ return "NumLock";
+ case SCROLL_LOCK:
+ return "ScrollLock";
+ case SHIFT:
+ return "Shift";
+ case CTRL:
+ return "Ctrl";
+ case ALT:
+ return "Alt";
+ case SUPER:
+ return "Super";
+ case HYPER:
+ return "Hyper";
+ case META:
+ return "Meta";
+ case LEVEL3:
+ return "Level3";
+ case LEVEL5:
+ return "Level5";
+ case NOT_MODIFIER:
+ return "NotModifier";
+ default:
+ return "InvalidValue";
+ }
+}
+
+/* static */
+KeymapWrapper::MappedModifier KeymapWrapper::GetModifierForGDKKeyval(
+ guint aGdkKeyval) {
+ switch (aGdkKeyval) {
+ case GDK_Caps_Lock:
+ return CAPS_LOCK;
+ case GDK_Num_Lock:
+ return NUM_LOCK;
+ case GDK_Scroll_Lock:
+ return SCROLL_LOCK;
+ case GDK_Shift_Lock:
+ case GDK_Shift_L:
+ case GDK_Shift_R:
+ return SHIFT;
+ case GDK_Control_L:
+ case GDK_Control_R:
+ return CTRL;
+ case GDK_Alt_L:
+ case GDK_Alt_R:
+ return ALT;
+ case GDK_Super_L:
+ case GDK_Super_R:
+ return SUPER;
+ case GDK_Hyper_L:
+ case GDK_Hyper_R:
+ return HYPER;
+ case GDK_Meta_L:
+ case GDK_Meta_R:
+ return META;
+ case GDK_ISO_Level3_Shift:
+ case GDK_Mode_switch:
+ return LEVEL3;
+ case GDK_ISO_Level5_Shift:
+ return LEVEL5;
+ default:
+ return NOT_MODIFIER;
+ }
+}
+
+guint KeymapWrapper::GetGdkModifierMask(MappedModifier aModifier) const {
+ switch (aModifier) {
+ case CAPS_LOCK:
+ return GDK_LOCK_MASK;
+ case NUM_LOCK:
+ return mModifierMasks[INDEX_NUM_LOCK];
+ case SCROLL_LOCK:
+ return mModifierMasks[INDEX_SCROLL_LOCK];
+ case SHIFT:
+ return GDK_SHIFT_MASK;
+ case CTRL:
+ return GDK_CONTROL_MASK;
+ case ALT:
+ return mModifierMasks[INDEX_ALT];
+ case SUPER:
+ return GDK_SUPER_MASK;
+ case HYPER:
+ return mModifierMasks[INDEX_HYPER];
+ case META:
+ return mModifierMasks[INDEX_META];
+ case LEVEL3:
+ return mModifierMasks[INDEX_LEVEL3];
+ case LEVEL5:
+ return mModifierMasks[INDEX_LEVEL5];
+ default:
+ return 0;
+ }
+}
+
+KeymapWrapper::ModifierKey* KeymapWrapper::GetModifierKey(
+ guint aHardwareKeycode) {
+ for (uint32_t i = 0; i < mModifierKeys.Length(); i++) {
+ ModifierKey& key = mModifierKeys[i];
+ if (key.mHardwareKeycode == aHardwareKeycode) {
+ return &key;
+ }
+ }
+ return nullptr;
+}
+
+/* static */
+KeymapWrapper* KeymapWrapper::GetInstance() {
+ if (!sInstance) {
+ sInstance = new KeymapWrapper();
+ sInstance->Init();
+ }
+ return sInstance;
+}
+
+#ifdef MOZ_WAYLAND
+void KeymapWrapper::EnsureInstance() { (void)GetInstance(); }
+
+void KeymapWrapper::InitBySystemSettingsWayland() {
+ MOZ_UNUSED(WaylandDisplayGet());
+}
+#endif
+
+/* static */
+void KeymapWrapper::Shutdown() {
+ if (sInstance) {
+ delete sInstance;
+ sInstance = nullptr;
+ }
+}
+
+KeymapWrapper::KeymapWrapper()
+ : mInitialized(false),
+ mGdkKeymap(gdk_keymap_get_default()),
+ mXKBBaseEventCode(0),
+ mOnKeysChangedSignalHandle(0),
+ mOnDirectionChangedSignalHandle(0) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p Constructor, mGdkKeymap=%p", this, mGdkKeymap));
+
+ g_object_ref(mGdkKeymap);
+
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ InitXKBExtension();
+ }
+#endif
+}
+
+void KeymapWrapper::Init() {
+ if (mInitialized) {
+ return;
+ }
+ mInitialized = true;
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p Init, mGdkKeymap=%p", this, mGdkKeymap));
+
+ mModifierKeys.Clear();
+ memset(mModifierMasks, 0, sizeof(mModifierMasks));
+
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ InitBySystemSettingsX11();
+ }
+#endif
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ InitBySystemSettingsWayland();
+ }
+#endif
+
+#ifdef MOZ_X11
+ gdk_window_add_filter(nullptr, FilterEvents, this);
+#endif
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p Init, CapsLock=0x%X, NumLock=0x%X, "
+ "ScrollLock=0x%X, Level3=0x%X, Level5=0x%X, "
+ "Shift=0x%X, Ctrl=0x%X, Alt=0x%X, Meta=0x%X, Super=0x%X, Hyper=0x%X",
+ this, GetGdkModifierMask(CAPS_LOCK), GetGdkModifierMask(NUM_LOCK),
+ GetGdkModifierMask(SCROLL_LOCK), GetGdkModifierMask(LEVEL3),
+ GetGdkModifierMask(LEVEL5), GetGdkModifierMask(SHIFT),
+ GetGdkModifierMask(CTRL), GetGdkModifierMask(ALT),
+ GetGdkModifierMask(META), GetGdkModifierMask(SUPER),
+ GetGdkModifierMask(HYPER)));
+}
+
+#ifdef MOZ_X11
+void KeymapWrapper::InitXKBExtension() {
+ PodZero(&mKeyboardState);
+
+ int xkbMajorVer = XkbMajorVersion;
+ int xkbMinorVer = XkbMinorVersion;
+ if (!XkbLibraryVersion(&xkbMajorVer, &xkbMinorVer)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitXKBExtension failed due to failure of "
+ "XkbLibraryVersion()",
+ this));
+ return;
+ }
+
+ Display* display = gdk_x11_display_get_xdisplay(gdk_display_get_default());
+
+ // XkbLibraryVersion() set xkbMajorVer and xkbMinorVer to that of the
+ // library, which may be newer than what is required of the server in
+ // XkbQueryExtension(), so these variables should be reset to
+ // XkbMajorVersion and XkbMinorVersion before the XkbQueryExtension call.
+ xkbMajorVer = XkbMajorVersion;
+ xkbMinorVer = XkbMinorVersion;
+ int opcode, baseErrorCode;
+ if (!XkbQueryExtension(display, &opcode, &mXKBBaseEventCode, &baseErrorCode,
+ &xkbMajorVer, &xkbMinorVer)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitXKBExtension failed due to failure of "
+ "XkbQueryExtension(), display=0x%p",
+ this, display));
+ return;
+ }
+
+ if (!XkbSelectEventDetails(display, XkbUseCoreKbd, XkbStateNotify,
+ XkbModifierStateMask, XkbModifierStateMask)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitXKBExtension failed due to failure of "
+ "XkbSelectEventDetails() for XModifierStateMask, display=0x%p",
+ this, display));
+ return;
+ }
+
+ if (!XkbSelectEventDetails(display, XkbUseCoreKbd, XkbControlsNotify,
+ XkbPerKeyRepeatMask, XkbPerKeyRepeatMask)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitXKBExtension failed due to failure of "
+ "XkbSelectEventDetails() for XkbControlsNotify, display=0x%p",
+ this, display));
+ return;
+ }
+
+ if (!XGetKeyboardControl(display, &mKeyboardState)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitXKBExtension failed due to failure of "
+ "XGetKeyboardControl(), display=0x%p",
+ this, display));
+ return;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info, ("%p InitXKBExtension, Succeeded", this));
+}
+
+void KeymapWrapper::InitBySystemSettingsX11() {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitBySystemSettingsX11, mGdkKeymap=%p", this, mGdkKeymap));
+
+ if (!mOnKeysChangedSignalHandle) {
+ mOnKeysChangedSignalHandle = g_signal_connect(
+ mGdkKeymap, "keys-changed", (GCallback)OnKeysChanged, this);
+ }
+ if (!mOnDirectionChangedSignalHandle) {
+ mOnDirectionChangedSignalHandle = g_signal_connect(
+ mGdkKeymap, "direction-changed", (GCallback)OnDirectionChanged, this);
+ }
+
+ Display* display = gdk_x11_display_get_xdisplay(gdk_display_get_default());
+
+ int min_keycode = 0;
+ int max_keycode = 0;
+ XDisplayKeycodes(display, &min_keycode, &max_keycode);
+
+ int keysyms_per_keycode = 0;
+ KeySym* xkeymap =
+ XGetKeyboardMapping(display, min_keycode, max_keycode - min_keycode + 1,
+ &keysyms_per_keycode);
+ if (!xkeymap) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitBySystemSettings, "
+ "Failed due to null xkeymap",
+ this));
+ return;
+ }
+
+ XModifierKeymap* xmodmap = XGetModifierMapping(display);
+ if (!xmodmap) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitBySystemSettings, "
+ "Failed due to null xmodmap",
+ this));
+ XFree(xkeymap);
+ return;
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitBySystemSettings, min_keycode=%d, "
+ "max_keycode=%d, keysyms_per_keycode=%d, max_keypermod=%d",
+ this, min_keycode, max_keycode, keysyms_per_keycode,
+ xmodmap->max_keypermod));
+
+ // The modifiermap member of the XModifierKeymap structure contains 8 sets
+ // of max_keypermod KeyCodes, one for each modifier in the order Shift,
+ // Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5.
+ // Only nonzero KeyCodes have meaning in each set, and zero KeyCodes are
+ // ignored.
+
+ // Note that two or more modifiers may use one modifier flag. E.g.,
+ // on Ubuntu 10.10, Alt and Meta share the Mod1 in default settings.
+ // And also Super and Hyper share the Mod4. In such cases, we need to
+ // decide which modifier flag means one of DOM modifiers.
+
+ // mod[0] is Modifier introduced by Mod1.
+ MappedModifier mod[5];
+ int32_t foundLevel[5];
+ for (uint32_t i = 0; i < ArrayLength(mod); i++) {
+ mod[i] = NOT_MODIFIER;
+ foundLevel[i] = INT32_MAX;
+ }
+ const uint32_t map_size = 8 * xmodmap->max_keypermod;
+ for (uint32_t i = 0; i < map_size; i++) {
+ KeyCode keycode = xmodmap->modifiermap[i];
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitBySystemSettings, "
+ " i=%d, keycode=0x%08X",
+ this, i, keycode));
+ if (!keycode || keycode < min_keycode || keycode > max_keycode) {
+ continue;
+ }
+
+ ModifierKey* modifierKey = GetModifierKey(keycode);
+ if (!modifierKey) {
+ modifierKey = mModifierKeys.AppendElement(ModifierKey(keycode));
+ }
+
+ const KeySym* syms =
+ xkeymap + (keycode - min_keycode) * keysyms_per_keycode;
+ const uint32_t bit = i / xmodmap->max_keypermod;
+ modifierKey->mMask |= 1 << bit;
+
+ // We need to know the meaning of Mod1, Mod2, Mod3, Mod4 and Mod5.
+ // Let's skip if current map is for others.
+ if (bit < 3) {
+ continue;
+ }
+
+ const int32_t modIndex = bit - 3;
+ for (int32_t j = 0; j < keysyms_per_keycode; j++) {
+ MappedModifier modifier = GetModifierForGDKKeyval(syms[j]);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p InitBySystemSettings, "
+ " Mod%d, j=%d, syms[j]=%s(0x%lX), modifier=%s",
+ this, modIndex + 1, j, gdk_keyval_name(syms[j]), syms[j],
+ GetModifierName(modifier)));
+
+ switch (modifier) {
+ case NOT_MODIFIER:
+ // Don't overwrite the stored information with
+ // NOT_MODIFIER.
+ break;
+ case CAPS_LOCK:
+ case SHIFT:
+ case CTRL:
+ case SUPER:
+ // Ignore the modifiers defined in GDK spec. They shouldn't
+ // be mapped to Mod1-5 because they must not work on native
+ // GTK applications.
+ break;
+ default:
+ // If new modifier is found in higher level than stored
+ // value, we don't need to overwrite it.
+ if (j > foundLevel[modIndex]) {
+ break;
+ }
+ // If new modifier is more important than stored value,
+ // we should overwrite it with new modifier.
+ if (j == foundLevel[modIndex]) {
+ mod[modIndex] = std::min(modifier, mod[modIndex]);
+ break;
+ }
+ foundLevel[modIndex] = j;
+ mod[modIndex] = modifier;
+ break;
+ }
+ }
+ }
+
+ for (uint32_t i = 0; i < COUNT_OF_MODIFIER_INDEX; i++) {
+ MappedModifier modifier;
+ switch (i) {
+ case INDEX_NUM_LOCK:
+ modifier = NUM_LOCK;
+ break;
+ case INDEX_SCROLL_LOCK:
+ modifier = SCROLL_LOCK;
+ break;
+ case INDEX_ALT:
+ modifier = ALT;
+ break;
+ case INDEX_META:
+ modifier = META;
+ break;
+ case INDEX_HYPER:
+ modifier = HYPER;
+ break;
+ case INDEX_LEVEL3:
+ modifier = LEVEL3;
+ break;
+ case INDEX_LEVEL5:
+ modifier = LEVEL5;
+ break;
+ default:
+ MOZ_CRASH("All indexes must be handled here");
+ }
+ for (uint32_t j = 0; j < ArrayLength(mod); j++) {
+ if (modifier == mod[j]) {
+ mModifierMasks[i] |= 1 << (j + 3);
+ }
+ }
+ }
+
+ XFreeModifiermap(xmodmap);
+ XFree(xkeymap);
+}
+#endif
+
+#ifdef MOZ_WAYLAND
+void KeymapWrapper::SetModifierMask(xkb_keymap* aKeymap,
+ ModifierIndex aModifierIndex,
+ const char* aModifierName) {
+ static auto sXkbKeymapModGetIndex =
+ (xkb_mod_index_t(*)(struct xkb_keymap*, const char*))dlsym(
+ RTLD_DEFAULT, "xkb_keymap_mod_get_index");
+
+ xkb_mod_index_t index = sXkbKeymapModGetIndex(aKeymap, aModifierName);
+ if (index != XKB_MOD_INVALID) {
+ mModifierMasks[aModifierIndex] = (1 << index);
+ }
+}
+
+void KeymapWrapper::SetModifierMasks(xkb_keymap* aKeymap) {
+ KeymapWrapper* keymapWrapper = GetInstance();
+
+ // This mapping is derived from get_xkb_modifiers() at gdkkeys-wayland.c
+ keymapWrapper->SetModifierMask(aKeymap, INDEX_NUM_LOCK, XKB_MOD_NAME_NUM);
+ keymapWrapper->SetModifierMask(aKeymap, INDEX_ALT, XKB_MOD_NAME_ALT);
+ keymapWrapper->SetModifierMask(aKeymap, INDEX_META, "Meta");
+ keymapWrapper->SetModifierMask(aKeymap, INDEX_HYPER, "Hyper");
+
+ keymapWrapper->SetModifierMask(aKeymap, INDEX_SCROLL_LOCK, "ScrollLock");
+ keymapWrapper->SetModifierMask(aKeymap, INDEX_LEVEL3, "Level3");
+ keymapWrapper->SetModifierMask(aKeymap, INDEX_LEVEL5, "Level5");
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p KeymapWrapper::SetModifierMasks, CapsLock=0x%X, NumLock=0x%X, "
+ "ScrollLock=0x%X, Level3=0x%X, Level5=0x%X, "
+ "Shift=0x%X, Ctrl=0x%X, Alt=0x%X, Meta=0x%X, Super=0x%X, Hyper=0x%X",
+ keymapWrapper, keymapWrapper->GetGdkModifierMask(CAPS_LOCK),
+ keymapWrapper->GetGdkModifierMask(NUM_LOCK),
+ keymapWrapper->GetGdkModifierMask(SCROLL_LOCK),
+ keymapWrapper->GetGdkModifierMask(LEVEL3),
+ keymapWrapper->GetGdkModifierMask(LEVEL5),
+ keymapWrapper->GetGdkModifierMask(SHIFT),
+ keymapWrapper->GetGdkModifierMask(CTRL),
+ keymapWrapper->GetGdkModifierMask(ALT),
+ keymapWrapper->GetGdkModifierMask(META),
+ keymapWrapper->GetGdkModifierMask(SUPER),
+ keymapWrapper->GetGdkModifierMask(HYPER)));
+}
+
+/* This keymap routine is derived from weston-2.0.0/clients/simple-im.c
+ */
+static void keyboard_handle_keymap(void* data, struct wl_keyboard* wl_keyboard,
+ uint32_t format, int fd, uint32_t size) {
+ KeymapWrapper::ResetKeyboard();
+
+ if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
+ close(fd);
+ return;
+ }
+
+ char* mapString = (char*)mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+ if (mapString == MAP_FAILED) {
+ close(fd);
+ return;
+ }
+
+ static auto sXkbContextNew =
+ (struct xkb_context * (*)(enum xkb_context_flags))
+ dlsym(RTLD_DEFAULT, "xkb_context_new");
+ static auto sXkbKeymapNewFromString =
+ (struct xkb_keymap * (*)(struct xkb_context*, const char*,
+ enum xkb_keymap_format,
+ enum xkb_keymap_compile_flags))
+ dlsym(RTLD_DEFAULT, "xkb_keymap_new_from_string");
+
+ struct xkb_context* xkb_context = sXkbContextNew(XKB_CONTEXT_NO_FLAGS);
+ struct xkb_keymap* keymap =
+ sXkbKeymapNewFromString(xkb_context, mapString, XKB_KEYMAP_FORMAT_TEXT_V1,
+ XKB_KEYMAP_COMPILE_NO_FLAGS);
+
+ munmap(mapString, size);
+ close(fd);
+
+ if (!keymap) {
+ NS_WARNING("keyboard_handle_keymap(): Failed to compile keymap!\n");
+ return;
+ }
+
+ KeymapWrapper::SetModifierMasks(keymap);
+
+ static auto sXkbKeymapUnRef =
+ (void (*)(struct xkb_keymap*))dlsym(RTLD_DEFAULT, "xkb_keymap_unref");
+ sXkbKeymapUnRef(keymap);
+
+ static auto sXkbContextUnref =
+ (void (*)(struct xkb_context*))dlsym(RTLD_DEFAULT, "xkb_context_unref");
+ sXkbContextUnref(xkb_context);
+}
+
+static void keyboard_handle_enter(void* data, struct wl_keyboard* keyboard,
+ uint32_t serial, struct wl_surface* surface,
+ struct wl_array* keys) {
+ KeymapWrapper::SetFocusIn(surface, serial);
+}
+
+static void keyboard_handle_leave(void* data, struct wl_keyboard* keyboard,
+ uint32_t serial, struct wl_surface* surface) {
+ KeymapWrapper::SetFocusOut(surface);
+}
+
+static void keyboard_handle_key(void* data, struct wl_keyboard* keyboard,
+ uint32_t serial, uint32_t time, uint32_t key,
+ uint32_t state) {}
+static void keyboard_handle_modifiers(void* data, struct wl_keyboard* keyboard,
+ uint32_t serial, uint32_t mods_depressed,
+ uint32_t mods_latched,
+ uint32_t mods_locked, uint32_t group) {}
+static void keyboard_handle_repeat_info(void* data,
+ struct wl_keyboard* keyboard,
+ int32_t rate, int32_t delay) {}
+
+static const struct wl_keyboard_listener keyboard_listener = {
+ keyboard_handle_keymap, keyboard_handle_enter,
+ keyboard_handle_leave, keyboard_handle_key,
+ keyboard_handle_modifiers, keyboard_handle_repeat_info};
+
+static void seat_handle_capabilities(void* data, struct wl_seat* seat,
+ unsigned int caps) {
+ wl_keyboard* keyboard = KeymapWrapper::GetKeyboard();
+ if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !keyboard) {
+ keyboard = wl_seat_get_keyboard(seat);
+ wl_keyboard_add_listener(keyboard, &keyboard_listener, nullptr);
+ KeymapWrapper::SetKeyboard(keyboard);
+ } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && keyboard) {
+ KeymapWrapper::ClearKeyboard();
+ }
+}
+
+static const struct wl_seat_listener seat_listener = {
+ seat_handle_capabilities,
+};
+
+#endif
+
+KeymapWrapper::~KeymapWrapper() {
+#ifdef MOZ_X11
+ gdk_window_remove_filter(nullptr, FilterEvents, this);
+#endif
+ if (mOnKeysChangedSignalHandle) {
+ g_signal_handler_disconnect(mGdkKeymap, mOnKeysChangedSignalHandle);
+ }
+ if (mOnDirectionChangedSignalHandle) {
+ g_signal_handler_disconnect(mGdkKeymap, mOnDirectionChangedSignalHandle);
+ }
+ g_object_unref(mGdkKeymap);
+ MOZ_LOG(gKeyLog, LogLevel::Info, ("%p Destructor", this));
+}
+
+#ifdef MOZ_X11
+/* static */
+GdkFilterReturn KeymapWrapper::FilterEvents(GdkXEvent* aXEvent,
+ GdkEvent* aGdkEvent,
+ gpointer aData) {
+ XEvent* xEvent = static_cast<XEvent*>(aXEvent);
+ switch (xEvent->type) {
+ case KeyPress: {
+ // If the key doesn't support auto repeat, ignore the event because
+ // even if such key (e.g., Shift) is pressed during auto repeat of
+ // anoter key, it doesn't stop the auto repeat.
+ KeymapWrapper* self = static_cast<KeymapWrapper*>(aData);
+ if (!self->IsAutoRepeatableKey(xEvent->xkey.keycode)) {
+ break;
+ }
+ if (sRepeatState == NOT_PRESSED) {
+ sRepeatState = FIRST_PRESS;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("FilterEvents(aXEvent={ type=KeyPress, "
+ "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, "
+ "aGdkEvent={ state=0x%08X }), "
+ "detected first keypress",
+ xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time,
+ reinterpret_cast<GdkEventKey*>(aGdkEvent)->state));
+ } else if (sLastRepeatableHardwareKeyCode == xEvent->xkey.keycode) {
+ if (sLastRepeatableKeyTime == xEvent->xkey.time &&
+ sLastRepeatableHardwareKeyCode ==
+ IMContextWrapper::
+ GetWaitingSynthesizedKeyPressHardwareKeyCode()) {
+ // On some environment, IM may generate duplicated KeyPress event
+ // without any special state flags. In such case, we shouldn't
+ // treat the event as "repeated".
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("FilterEvents(aXEvent={ type=KeyPress, "
+ "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, "
+ "aGdkEvent={ state=0x%08X }), "
+ "igored keypress since it must be synthesized by IME",
+ xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time,
+ reinterpret_cast<GdkEventKey*>(aGdkEvent)->state));
+ break;
+ }
+ sRepeatState = REPEATING;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("FilterEvents(aXEvent={ type=KeyPress, "
+ "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, "
+ "aGdkEvent={ state=0x%08X }), "
+ "detected repeating keypress",
+ xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time,
+ reinterpret_cast<GdkEventKey*>(aGdkEvent)->state));
+ } else {
+ // If a different key is pressed while another key is pressed,
+ // auto repeat system repeats only the last pressed key.
+ // So, setting new keycode and setting repeat state as first key
+ // press should work fine.
+ sRepeatState = FIRST_PRESS;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("FilterEvents(aXEvent={ type=KeyPress, "
+ "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, "
+ "aGdkEvent={ state=0x%08X }), "
+ "detected different keypress",
+ xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time,
+ reinterpret_cast<GdkEventKey*>(aGdkEvent)->state));
+ }
+ sLastRepeatableHardwareKeyCode = xEvent->xkey.keycode;
+ sLastRepeatableKeyTime = xEvent->xkey.time;
+ break;
+ }
+ case KeyRelease: {
+ if (sLastRepeatableHardwareKeyCode != xEvent->xkey.keycode) {
+ // This case means the key release event is caused by
+ // a non-repeatable key such as Shift or a repeatable key that
+ // was pressed before sLastRepeatableHardwareKeyCode was
+ // pressed.
+ break;
+ }
+ sRepeatState = NOT_PRESSED;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("FilterEvents(aXEvent={ type=KeyRelease, "
+ "xkey={ keycode=0x%08X, state=0x%08X, time=%lu } }, "
+ "aGdkEvent={ state=0x%08X }), "
+ "detected key release",
+ xEvent->xkey.keycode, xEvent->xkey.state, xEvent->xkey.time,
+ reinterpret_cast<GdkEventKey*>(aGdkEvent)->state));
+ break;
+ }
+ case FocusOut: {
+ // At moving focus, we should reset keyboard repeat state.
+ // Strictly, this causes incorrect behavior. However, this
+ // correctness must be enough for web applications.
+ sRepeatState = NOT_PRESSED;
+ break;
+ }
+ default: {
+ KeymapWrapper* self = static_cast<KeymapWrapper*>(aData);
+ if (xEvent->type != self->mXKBBaseEventCode) {
+ break;
+ }
+ XkbEvent* xkbEvent = (XkbEvent*)xEvent;
+ if (xkbEvent->any.xkb_type != XkbControlsNotify ||
+ !(xkbEvent->ctrls.changed_ctrls & XkbPerKeyRepeatMask)) {
+ break;
+ }
+ if (!XGetKeyboardControl(xkbEvent->any.display, &self->mKeyboardState)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p FilterEvents failed due to failure "
+ "of XGetKeyboardControl(), display=0x%p",
+ self, xkbEvent->any.display));
+ }
+ break;
+ }
+ }
+
+ return GDK_FILTER_CONTINUE;
+}
+#endif
+
+static void ResetBidiKeyboard() {
+ // Reset the bidi keyboard settings for the new GdkKeymap
+ nsCOMPtr<nsIBidiKeyboard> bidiKeyboard = nsContentUtils::GetBidiKeyboard();
+ if (bidiKeyboard) {
+ bidiKeyboard->Reset();
+ }
+ WidgetUtils::SendBidiKeyboardInfoToContent();
+}
+
+/* static */
+void KeymapWrapper::ResetKeyboard() {
+ if (sInstance) {
+ sInstance->mInitialized = false;
+ ResetBidiKeyboard();
+ }
+}
+
+/* static */
+void KeymapWrapper::OnKeysChanged(GdkKeymap* aGdkKeymap,
+ KeymapWrapper* aKeymapWrapper) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("OnKeysChanged, aGdkKeymap=%p, aKeymapWrapper=%p", aGdkKeymap,
+ aKeymapWrapper));
+
+ MOZ_ASSERT(sInstance == aKeymapWrapper,
+ "This instance must be the singleton instance");
+
+ // We cannot reintialize here becasue we don't have GdkWindow which is using
+ // the GdkKeymap. We'll reinitialize it when next GetInstance() is called.
+ ResetKeyboard();
+}
+
+// static
+void KeymapWrapper::OnDirectionChanged(GdkKeymap* aGdkKeymap,
+ KeymapWrapper* aKeymapWrapper) {
+ // XXX
+ // A lot of diretion-changed signal might be fired on switching bidi
+ // keyboard when using both ibus (with arabic layout) and fcitx (with IME).
+ // See https://github.com/fcitx/fcitx/issues/257
+ //
+ // Also, when using ibus, switching to IM might not cause this signal.
+ // See https://github.com/ibus/ibus/issues/1848
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("OnDirectionChanged, aGdkKeymap=%p, aKeymapWrapper=%p", aGdkKeymap,
+ aKeymapWrapper));
+
+ ResetBidiKeyboard();
+}
+
+/* static */
+guint KeymapWrapper::GetCurrentModifierState() {
+ GdkModifierType modifiers;
+ GdkDisplay* display = gdk_display_get_default();
+ GdkScreen* screen = gdk_display_get_default_screen(display);
+ GdkWindow* window = gdk_screen_get_root_window(screen);
+ gdk_window_get_device_position(window, GdkGetPointer(), nullptr, nullptr,
+ &modifiers);
+ return static_cast<guint>(modifiers);
+}
+
+/* static */
+bool KeymapWrapper::AreModifiersActive(MappedModifiers aModifiers,
+ guint aGdkModifierState) {
+ NS_ENSURE_TRUE(aModifiers, false);
+
+ KeymapWrapper* keymapWrapper = GetInstance();
+ for (uint32_t i = 0; i < sizeof(MappedModifier) * 8 && aModifiers; i++) {
+ MappedModifier modifier = static_cast<MappedModifier>(1 << i);
+ // Is the binary position used by modifier?
+ if (!(aModifiers & modifier)) {
+ continue;
+ }
+ // Is the modifier active?
+ if (!(aGdkModifierState & keymapWrapper->GetGdkModifierMask(modifier))) {
+ return false;
+ }
+ aModifiers &= ~modifier;
+ }
+ return true;
+}
+
+/* static */
+uint32_t KeymapWrapper::ComputeCurrentKeyModifiers() {
+ return ComputeKeyModifiers(GetCurrentModifierState());
+}
+
+/* static */
+uint32_t KeymapWrapper::ComputeKeyModifiers(guint aGdkModifierState) {
+ uint32_t keyModifiers = 0;
+ if (!aGdkModifierState) {
+ return keyModifiers;
+ }
+
+ // DOM Meta key should be TRUE only on Mac. We need to discuss this
+ // issue later.
+ KeymapWrapper* keymapWrapper = GetInstance();
+ if (keymapWrapper->AreModifiersActive(SHIFT, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_SHIFT;
+ }
+ if (keymapWrapper->AreModifiersActive(CTRL, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_CONTROL;
+ }
+ if (keymapWrapper->AreModifiersActive(ALT, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_ALT;
+ }
+ if (keymapWrapper->AreModifiersActive(SUPER, aGdkModifierState) ||
+ keymapWrapper->AreModifiersActive(HYPER, aGdkModifierState) ||
+ // "Meta" state is typically mapped to `Alt` + `Shift`, but we ignore the
+ // state if `Alt` is mapped to "Alt" state. Additionally it's mapped to
+ // `Win` in Sun/Solaris keyboard layout. In this case, we want to treat
+ // them as DOM Meta modifier keys like "Super" state in the major Linux
+ // environments.
+ keymapWrapper->AreModifiersActive(META, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_META;
+ }
+ if (keymapWrapper->AreModifiersActive(LEVEL3, aGdkModifierState) ||
+ keymapWrapper->AreModifiersActive(LEVEL5, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_ALTGRAPH;
+ }
+ if (keymapWrapper->AreModifiersActive(CAPS_LOCK, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_CAPSLOCK;
+ }
+ if (keymapWrapper->AreModifiersActive(NUM_LOCK, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_NUMLOCK;
+ }
+ if (keymapWrapper->AreModifiersActive(SCROLL_LOCK, aGdkModifierState)) {
+ keyModifiers |= MODIFIER_SCROLLLOCK;
+ }
+ return keyModifiers;
+}
+
+/* static */
+guint KeymapWrapper::ConvertWidgetModifierToGdkState(
+ nsIWidget::Modifiers aNativeModifiers) {
+ if (!aNativeModifiers) {
+ return 0;
+ }
+ struct ModifierMapEntry {
+ nsIWidget::Modifiers mWidgetModifier;
+ MappedModifier mModifier;
+ };
+ // TODO: Currently, we don't treat L/R of each modifier on Linux.
+ // TODO: No proper native modifier for Level5.
+ static constexpr ModifierMapEntry sModifierMap[] = {
+ {nsIWidget::CAPS_LOCK, MappedModifier::CAPS_LOCK},
+ {nsIWidget::NUM_LOCK, MappedModifier::NUM_LOCK},
+ {nsIWidget::SHIFT_L, MappedModifier::SHIFT},
+ {nsIWidget::SHIFT_R, MappedModifier::SHIFT},
+ {nsIWidget::CTRL_L, MappedModifier::CTRL},
+ {nsIWidget::CTRL_R, MappedModifier::CTRL},
+ {nsIWidget::ALT_L, MappedModifier::ALT},
+ {nsIWidget::ALT_R, MappedModifier::ALT},
+ {nsIWidget::ALTGRAPH, MappedModifier::LEVEL3},
+ {nsIWidget::COMMAND_L, MappedModifier::SUPER},
+ {nsIWidget::COMMAND_R, MappedModifier::SUPER}};
+
+ guint state = 0;
+ KeymapWrapper* instance = GetInstance();
+ for (const ModifierMapEntry& entry : sModifierMap) {
+ if (aNativeModifiers & entry.mWidgetModifier) {
+ state |= instance->GetGdkModifierMask(entry.mModifier);
+ }
+ }
+ return state;
+}
+
+/* static */
+void KeymapWrapper::InitInputEvent(WidgetInputEvent& aInputEvent,
+ guint aGdkModifierState) {
+ KeymapWrapper* keymapWrapper = GetInstance();
+
+ aInputEvent.mModifiers = ComputeKeyModifiers(aGdkModifierState);
+
+ // Don't log this method for non-important events because e.g., eMouseMove is
+ // just noisy and there is no reason to log it.
+ bool doLog = aInputEvent.mMessage != eMouseMove;
+ if (doLog) {
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p InitInputEvent, aGdkModifierState=0x%08X, "
+ "aInputEvent={ mMessage=%s, mModifiers=0x%04X (Shift: %s, "
+ "Control: %s, Alt: %s, Meta: %s, AltGr: %s, "
+ "CapsLock: %s, NumLock: %s, ScrollLock: %s })",
+ keymapWrapper, aGdkModifierState, ToChar(aInputEvent.mMessage),
+ aInputEvent.mModifiers,
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_SHIFT),
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_CONTROL),
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_ALT),
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_META),
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_ALTGRAPH),
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_CAPSLOCK),
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_NUMLOCK),
+ GetBoolName(aInputEvent.mModifiers & MODIFIER_SCROLLLOCK)));
+ }
+
+ switch (aInputEvent.mClass) {
+ case eMouseEventClass:
+ case eMouseScrollEventClass:
+ case eWheelEventClass:
+ case eDragEventClass:
+ case eSimpleGestureEventClass:
+ break;
+ default:
+ return;
+ }
+
+ WidgetMouseEventBase& mouseEvent = *aInputEvent.AsMouseEventBase();
+ mouseEvent.mButtons = 0;
+ if (aGdkModifierState & GDK_BUTTON1_MASK) {
+ mouseEvent.mButtons |= MouseButtonsFlag::ePrimaryFlag;
+ }
+ if (aGdkModifierState & GDK_BUTTON3_MASK) {
+ mouseEvent.mButtons |= MouseButtonsFlag::eSecondaryFlag;
+ }
+ if (aGdkModifierState & GDK_BUTTON2_MASK) {
+ mouseEvent.mButtons |= MouseButtonsFlag::eMiddleFlag;
+ }
+
+ if (doLog) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Debug,
+ ("%p InitInputEvent, aInputEvent has mButtons, "
+ "aInputEvent.mButtons=0x%04X (Left: %s, Right: %s, Middle: %s, "
+ "4th (BACK): %s, 5th (FORWARD): %s)",
+ keymapWrapper, mouseEvent.mButtons,
+ GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::ePrimaryFlag),
+ GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::eSecondaryFlag),
+ GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::eMiddleFlag),
+ GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::e4thFlag),
+ GetBoolName(mouseEvent.mButtons & MouseButtonsFlag::e5thFlag)));
+ }
+}
+
+/* static */
+uint32_t KeymapWrapper::ComputeDOMKeyCode(const GdkEventKey* aGdkKeyEvent) {
+ // If the keyval indicates it's a modifier key, we should use unshifted
+ // key's modifier keyval.
+ guint keyval = aGdkKeyEvent->keyval;
+ if (GetModifierForGDKKeyval(keyval)) {
+ // But if the keyval without modifiers isn't a modifier key, we
+ // shouldn't use it. E.g., Japanese keyboard layout's
+ // Shift + Eisu-Toggle key is CapsLock. This is an actual rare case,
+ // Windows uses different keycode for a physical key for different
+ // shift key state.
+ guint keyvalWithoutModifier = GetGDKKeyvalWithoutModifier(aGdkKeyEvent);
+ if (GetModifierForGDKKeyval(keyvalWithoutModifier)) {
+ keyval = keyvalWithoutModifier;
+ }
+ // Note that the modifier keycode and activating or deactivating
+ // modifier flag may be mismatched, but it's okay. If a DOM key
+ // event handler is testing a keydown event, it's more likely being
+ // used to test which key is being pressed than to test which
+ // modifier will become active. So, if we computed DOM keycode
+ // from modifier flag which were changing by the physical key, then
+ // there would be no other way for the user to generate the original
+ // keycode.
+ uint32_t DOMKeyCode = GetDOMKeyCodeFromKeyPairs(keyval);
+ NS_ASSERTION(DOMKeyCode, "All modifier keys must have a DOM keycode");
+ return DOMKeyCode;
+ }
+
+ // If the key isn't printable, let's look at the key pairs.
+ uint32_t charCode = GetCharCodeFor(aGdkKeyEvent);
+ if (!charCode) {
+ // Note that any key may be a function key because of some unusual keyboard
+ // layouts. I.e., even if the pressed key is a printable key of en-US
+ // keyboard layout, we should expose the function key's keyCode value to
+ // web apps because web apps should handle the keydown/keyup events as
+ // inputted by usual keyboard layout. For example, Hatchak keyboard
+ // maps Tab key to "Digit3" key and Level3 Shift makes it "Backspace".
+ // In this case, we should expose DOM_VK_BACK_SPACE (8).
+ uint32_t DOMKeyCode = GetDOMKeyCodeFromKeyPairs(keyval);
+ if (DOMKeyCode) {
+ // XXX If DOMKeyCode is a function key's keyCode value, it might be
+ // better to consume necessary modifiers. For example, if there is
+ // no Control Pad section on keyboard like notebook, Delete key is
+ // available only with Level3 Shift+"Backspace" key if using Hatchak.
+ // If web apps accept Delete key operation only when no modifiers are
+ // active, such users cannot use Delete key to do it. However,
+ // Chromium doesn't consume such necessary modifiers. So, our default
+ // behavior should keep not touching modifiers for compatibility, but
+ // it might be better to add a pref to consume necessary modifiers.
+ return DOMKeyCode;
+ }
+ // If aGdkKeyEvent cannot be mapped to a DOM keyCode value, we should
+ // refer keyCode value without modifiers because web apps should be
+ // able to identify the key as far as possible.
+ guint keyvalWithoutModifier = GetGDKKeyvalWithoutModifier(aGdkKeyEvent);
+ return GetDOMKeyCodeFromKeyPairs(keyvalWithoutModifier);
+ }
+
+ // printable numpad keys should be resolved here.
+ switch (keyval) {
+ case GDK_KP_Multiply:
+ return NS_VK_MULTIPLY;
+ case GDK_KP_Add:
+ return NS_VK_ADD;
+ case GDK_KP_Separator:
+ return NS_VK_SEPARATOR;
+ case GDK_KP_Subtract:
+ return NS_VK_SUBTRACT;
+ case GDK_KP_Decimal:
+ return NS_VK_DECIMAL;
+ case GDK_KP_Divide:
+ return NS_VK_DIVIDE;
+ case GDK_KP_0:
+ return NS_VK_NUMPAD0;
+ case GDK_KP_1:
+ return NS_VK_NUMPAD1;
+ case GDK_KP_2:
+ return NS_VK_NUMPAD2;
+ case GDK_KP_3:
+ return NS_VK_NUMPAD3;
+ case GDK_KP_4:
+ return NS_VK_NUMPAD4;
+ case GDK_KP_5:
+ return NS_VK_NUMPAD5;
+ case GDK_KP_6:
+ return NS_VK_NUMPAD6;
+ case GDK_KP_7:
+ return NS_VK_NUMPAD7;
+ case GDK_KP_8:
+ return NS_VK_NUMPAD8;
+ case GDK_KP_9:
+ return NS_VK_NUMPAD9;
+ }
+
+ KeymapWrapper* keymapWrapper = GetInstance();
+
+ // Ignore all modifier state except NumLock.
+ guint baseState =
+ (aGdkKeyEvent->state & keymapWrapper->GetGdkModifierMask(NUM_LOCK));
+
+ // Basically, we should use unmodified character for deciding our keyCode.
+ uint32_t unmodifiedChar = keymapWrapper->GetCharCodeFor(
+ aGdkKeyEvent, baseState, aGdkKeyEvent->group);
+ if (IsBasicLatinLetterOrNumeral(unmodifiedChar)) {
+ // If the unmodified character is an ASCII alphabet or an ASCII
+ // numeric, it's the best hint for deciding our keyCode.
+ return WidgetUtils::ComputeKeyCodeFromChar(unmodifiedChar);
+ }
+
+ // If the unmodified character is not an ASCII character, that means we
+ // couldn't find the hint. We should reset it.
+ if (!IsPrintableASCIICharacter(unmodifiedChar)) {
+ unmodifiedChar = 0;
+ }
+
+ // Retry with shifted keycode.
+ guint shiftState = (baseState | keymapWrapper->GetGdkModifierMask(SHIFT));
+ uint32_t shiftedChar = keymapWrapper->GetCharCodeFor(aGdkKeyEvent, shiftState,
+ aGdkKeyEvent->group);
+ if (IsBasicLatinLetterOrNumeral(shiftedChar)) {
+ // A shifted character can be an ASCII alphabet on Hebrew keyboard
+ // layout. And also shifted character can be an ASCII numeric on
+ // AZERTY keyboad layout. Then, it's a good hint for deciding our
+ // keyCode.
+ return WidgetUtils::ComputeKeyCodeFromChar(shiftedChar);
+ }
+
+ // If the shifted unmodified character isn't an ASCII character, we should
+ // discard it too.
+ if (!IsPrintableASCIICharacter(shiftedChar)) {
+ shiftedChar = 0;
+ }
+
+ // If current keyboard layout isn't ASCII alphabet inputtable layout,
+ // look for ASCII alphabet inputtable keyboard layout. If the key
+ // inputs an ASCII alphabet or an ASCII numeric, we should use it
+ // for deciding our keyCode.
+ uint32_t unmodCharLatin = 0;
+ uint32_t shiftedCharLatin = 0;
+ if (!keymapWrapper->IsLatinGroup(aGdkKeyEvent->group)) {
+ gint minGroup = keymapWrapper->GetFirstLatinGroup();
+ if (minGroup >= 0) {
+ unmodCharLatin =
+ keymapWrapper->GetCharCodeFor(aGdkKeyEvent, baseState, minGroup);
+ if (IsBasicLatinLetterOrNumeral(unmodCharLatin)) {
+ // If the unmodified character is an ASCII alphabet or
+ // an ASCII numeric, we should use it for the keyCode.
+ return WidgetUtils::ComputeKeyCodeFromChar(unmodCharLatin);
+ }
+ // If the unmodified character in the alternative ASCII capable
+ // keyboard layout isn't an ASCII character, that means we couldn't
+ // find the hint. We should reset it.
+ if (!IsPrintableASCIICharacter(unmodCharLatin)) {
+ unmodCharLatin = 0;
+ }
+ shiftedCharLatin =
+ keymapWrapper->GetCharCodeFor(aGdkKeyEvent, shiftState, minGroup);
+ if (IsBasicLatinLetterOrNumeral(shiftedCharLatin)) {
+ // If the shifted character is an ASCII alphabet or an ASCII
+ // numeric, we should use it for the keyCode.
+ return WidgetUtils::ComputeKeyCodeFromChar(shiftedCharLatin);
+ }
+ // If the shifted unmodified character in the alternative ASCII
+ // capable keyboard layout isn't an ASCII character, we should
+ // discard it too.
+ if (!IsPrintableASCIICharacter(shiftedCharLatin)) {
+ shiftedCharLatin = 0;
+ }
+ }
+ }
+
+ // If the key itself or with Shift state on active keyboard layout produces
+ // an ASCII punctuation character, we should decide keyCode value with it.
+ if (unmodifiedChar || shiftedChar) {
+ return WidgetUtils::ComputeKeyCodeFromChar(unmodifiedChar ? unmodifiedChar
+ : shiftedChar);
+ }
+
+ // If the key itself or with Shift state on alternative ASCII capable
+ // keyboard layout produces an ASCII punctuation character, we should
+ // decide keyCode value with it. Note that We've returned 0 for long
+ // time if keyCode isn't for an alphabet keys or a numeric key even in
+ // alternative ASCII capable keyboard layout because we decided that we
+ // should avoid setting same keyCode value to 2 or more keys since active
+ // keyboard layout may have a key to input the punctuation with different
+ // key. However, setting keyCode to 0 makes some web applications which
+ // are aware of neither KeyboardEvent.key nor KeyboardEvent.code not work
+ // with Firefox when user selects non-ASCII capable keyboard layout such
+ // as Russian and Thai. So, if alternative ASCII capable keyboard layout
+ // has keyCode value for the key, we should use it. In other words, this
+ // behavior means that non-ASCII capable keyboard layout overrides some
+ // keys' keyCode value only if the key produces ASCII character by itself
+ // or with Shift key.
+ if (unmodCharLatin || shiftedCharLatin) {
+ return WidgetUtils::ComputeKeyCodeFromChar(
+ unmodCharLatin ? unmodCharLatin : shiftedCharLatin);
+ }
+
+ // Otherwise, let's decide keyCode value from the hardware_keycode
+ // value on major keyboard layout.
+ CodeNameIndex code = ComputeDOMCodeNameIndex(aGdkKeyEvent);
+ return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code);
+}
+
+KeyNameIndex KeymapWrapper::ComputeDOMKeyNameIndex(
+ const GdkEventKey* aGdkKeyEvent) {
+ switch (aGdkKeyEvent->keyval) {
+#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ break;
+ }
+
+ return KEY_NAME_INDEX_Unidentified;
+}
+
+/* static */
+CodeNameIndex KeymapWrapper::ComputeDOMCodeNameIndex(
+ const GdkEventKey* aGdkKeyEvent) {
+ switch (aGdkKeyEvent->hardware_keycode) {
+#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \
+ case aNativeKey: \
+ return aCodeNameIndex;
+
+#include "NativeKeyToDOMCodeName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX
+
+ default:
+ break;
+ }
+
+ return CODE_NAME_INDEX_UNKNOWN;
+}
+
+/* static */
+bool KeymapWrapper::DispatchKeyDownOrKeyUpEvent(nsWindow* aWindow,
+ GdkEventKey* aGdkKeyEvent,
+ bool aIsProcessedByIME,
+ bool* aIsCancelled) {
+ MOZ_ASSERT(aIsCancelled, "aIsCancelled must not be nullptr");
+
+ *aIsCancelled = false;
+
+ if (aGdkKeyEvent->type == GDK_KEY_PRESS && aGdkKeyEvent->keyval == GDK_Tab &&
+ AreModifiersActive(CTRL | ALT, aGdkKeyEvent->state)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" DispatchKeyDownOrKeyUpEvent(), didn't dispatch keyboard events "
+ "because it's Ctrl + Alt + Tab"));
+ return false;
+ }
+
+ EventMessage message =
+ aGdkKeyEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp;
+ WidgetKeyboardEvent keyEvent(true, message, aWindow);
+ KeymapWrapper::InitKeyEvent(keyEvent, aGdkKeyEvent, aIsProcessedByIME);
+ return DispatchKeyDownOrKeyUpEvent(aWindow, keyEvent, aIsCancelled);
+}
+
+/* static */
+bool KeymapWrapper::DispatchKeyDownOrKeyUpEvent(
+ nsWindow* aWindow, WidgetKeyboardEvent& aKeyboardEvent,
+ bool* aIsCancelled) {
+ MOZ_ASSERT(aIsCancelled, "aIsCancelled must not be nullptr");
+
+ *aIsCancelled = false;
+
+ RefPtr<TextEventDispatcher> dispatcher = aWindow->GetTextEventDispatcher();
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ (" DispatchKeyDownOrKeyUpEvent(), stopped dispatching %s event "
+ "because of failed to initialize TextEventDispatcher",
+ ToChar(aKeyboardEvent.mMessage)));
+ return FALSE;
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ bool dispatched = dispatcher->DispatchKeyboardEvent(
+ aKeyboardEvent.mMessage, aKeyboardEvent, status, nullptr);
+ *aIsCancelled = (status == nsEventStatus_eConsumeNoDefault);
+ return dispatched;
+}
+
+/* static */
+bool KeymapWrapper::MaybeDispatchContextMenuEvent(nsWindow* aWindow,
+ const GdkEventKey* aEvent) {
+ KeyNameIndex keyNameIndex = ComputeDOMKeyNameIndex(aEvent);
+
+ // Shift+F10 and ContextMenu should cause eContextMenu event.
+ if (keyNameIndex != KEY_NAME_INDEX_F10 &&
+ keyNameIndex != KEY_NAME_INDEX_ContextMenu) {
+ return false;
+ }
+
+ WidgetMouseEvent contextMenuEvent(true, eContextMenu, aWindow,
+ WidgetMouseEvent::eReal,
+ WidgetMouseEvent::eContextMenuKey);
+
+ contextMenuEvent.mRefPoint = LayoutDeviceIntPoint(0, 0);
+ contextMenuEvent.AssignEventTime(aWindow->GetWidgetEventTime(aEvent->time));
+ contextMenuEvent.mClickCount = 1;
+ KeymapWrapper::InitInputEvent(contextMenuEvent, aEvent->state);
+
+ if (contextMenuEvent.IsControl() || contextMenuEvent.IsMeta() ||
+ contextMenuEvent.IsAlt()) {
+ return false;
+ }
+
+ // If the key is ContextMenu, then an eContextMenu mouse event is
+ // dispatched regardless of the state of the Shift modifier. When it is
+ // pressed without the Shift modifier, a web page can prevent the default
+ // context menu action. When pressed with the Shift modifier, the web page
+ // cannot prevent the default context menu action.
+ // (PresShell::HandleEventInternal() sets mOnlyChromeDispatch to true.)
+
+ // If the key is F10, it needs Shift state because Shift+F10 is well-known
+ // shortcut key on Linux. However, eContextMenu with Shift state is
+ // special. It won't fire "contextmenu" event in the web content for
+ // blocking web page to prevent its default. Therefore, this combination
+ // should work same as ContextMenu key.
+ // XXX Should we allow to block web page to prevent its default with
+ // Ctrl+Shift+F10 or Alt+Shift+F10 instead?
+ if (keyNameIndex == KEY_NAME_INDEX_F10) {
+ if (!contextMenuEvent.IsShift()) {
+ return false;
+ }
+ contextMenuEvent.mModifiers &= ~MODIFIER_SHIFT;
+ }
+
+ aWindow->DispatchInputEvent(&contextMenuEvent);
+ return true;
+}
+
+/* static*/
+void KeymapWrapper::HandleKeyPressEvent(nsWindow* aWindow,
+ GdkEventKey* aGdkKeyEvent) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("HandleKeyPressEvent(aWindow=%p, aGdkKeyEvent={ type=%s, "
+ "keyval=%s(0x%X), state=0x%08X, hardware_keycode=0x%08X, "
+ "time=%u, is_modifier=%s })",
+ aWindow,
+ ((aGdkKeyEvent->type == GDK_KEY_PRESS) ? "GDK_KEY_PRESS"
+ : "GDK_KEY_RELEASE"),
+ gdk_keyval_name(aGdkKeyEvent->keyval), aGdkKeyEvent->keyval,
+ aGdkKeyEvent->state, aGdkKeyEvent->hardware_keycode,
+ aGdkKeyEvent->time, GetBoolName(aGdkKeyEvent->is_modifier)));
+
+ // if we are in the middle of composing text, XIM gets to see it
+ // before mozilla does.
+ // FYI: Don't dispatch keydown event before notifying IME of the event
+ // because IME may send a key event synchronously and consume the
+ // original event.
+ bool IMEWasEnabled = false;
+ KeyHandlingState handlingState = KeyHandlingState::eNotHandled;
+ RefPtr<IMContextWrapper> imContext = aWindow->GetIMContext();
+ if (imContext) {
+ IMEWasEnabled = imContext->IsEnabled();
+ handlingState = imContext->OnKeyEvent(aWindow, aGdkKeyEvent);
+ if (handlingState == KeyHandlingState::eHandled) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), the event was handled by "
+ "IMContextWrapper"));
+ return;
+ }
+ }
+
+ // work around for annoying things.
+ if (aGdkKeyEvent->keyval == GDK_Tab &&
+ AreModifiersActive(CTRL | ALT, aGdkKeyEvent->state)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), didn't dispatch keyboard events "
+ "because it's Ctrl + Alt + Tab"));
+ return;
+ }
+
+ // Dispatch keydown event always. At auto repeating, we should send
+ // KEYDOWN -> KEYPRESS -> KEYDOWN -> KEYPRESS ... -> KEYUP
+ // However, old distributions (e.g., Ubuntu 9.10) sent native key
+ // release event, so, on such platform, the DOM events will be:
+ // KEYDOWN -> KEYPRESS -> KEYUP -> KEYDOWN -> KEYPRESS -> KEYUP...
+
+ bool isKeyDownCancelled = false;
+ if (handlingState == KeyHandlingState::eNotHandled) {
+ if (DispatchKeyDownOrKeyUpEvent(aWindow, aGdkKeyEvent, false,
+ &isKeyDownCancelled) &&
+ (MOZ_UNLIKELY(aWindow->IsDestroyed()) || isKeyDownCancelled)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched eKeyDown event and "
+ "stopped handling the event because %s",
+ aWindow->IsDestroyed() ? "the window has been destroyed"
+ : "the event was consumed"));
+ return;
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched eKeyDown event and "
+ "it wasn't consumed"));
+ handlingState = KeyHandlingState::eNotHandledButEventDispatched;
+ }
+
+ // If a keydown event handler causes to enable IME, i.e., it moves
+ // focus from IME unusable content to IME usable editor, we should
+ // send the native key event to IME for the first input on the editor.
+ imContext = aWindow->GetIMContext();
+ if (!IMEWasEnabled && imContext && imContext->IsEnabled()) {
+ // Notice our keydown event was already dispatched. This prevents
+ // unnecessary DOM keydown event in the editor.
+ handlingState = imContext->OnKeyEvent(aWindow, aGdkKeyEvent, true);
+ if (handlingState == KeyHandlingState::eHandled) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), the event was handled by "
+ "IMContextWrapper which was enabled by the preceding eKeyDown "
+ "event"));
+ return;
+ }
+ }
+
+ // Look for specialized app-command keys
+ switch (aGdkKeyEvent->keyval) {
+ case GDK_Back:
+ aWindow->DispatchCommandEvent(nsGkAtoms::Back);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Back\" command event"));
+ return;
+ case GDK_Forward:
+ aWindow->DispatchCommandEvent(nsGkAtoms::Forward);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Forward\" command "
+ "event"));
+ return;
+ case GDK_Reload:
+ case GDK_Refresh:
+ aWindow->DispatchCommandEvent(nsGkAtoms::Reload);
+ return;
+ case GDK_Stop:
+ aWindow->DispatchCommandEvent(nsGkAtoms::Stop);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Stop\" command event"));
+ return;
+ case GDK_Search:
+ aWindow->DispatchCommandEvent(nsGkAtoms::Search);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Search\" command event"));
+ return;
+ case GDK_Favorites:
+ aWindow->DispatchCommandEvent(nsGkAtoms::Bookmarks);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Bookmarks\" command "
+ "event"));
+ return;
+ case GDK_HomePage:
+ aWindow->DispatchCommandEvent(nsGkAtoms::Home);
+ return;
+ case GDK_Copy:
+ case GDK_F16: // F16, F20, F18, F14 are old keysyms for Copy Cut Paste Undo
+ aWindow->DispatchContentCommandEvent(eContentCommandCopy);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Copy\" content command "
+ "event"));
+ return;
+ case GDK_Cut:
+ case GDK_F20:
+ aWindow->DispatchContentCommandEvent(eContentCommandCut);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Cut\" content command "
+ "event"));
+ return;
+ case GDK_Paste:
+ case GDK_F18:
+ aWindow->DispatchContentCommandEvent(eContentCommandPaste);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Paste\" content command "
+ "event"));
+ return;
+ case GDK_Redo:
+ aWindow->DispatchContentCommandEvent(eContentCommandRedo);
+ return;
+ case GDK_Undo:
+ case GDK_F14:
+ aWindow->DispatchContentCommandEvent(eContentCommandUndo);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched \"Undo\" content command "
+ "event"));
+ return;
+ default:
+ break;
+ }
+
+ // before we dispatch a key, check if it's the context menu key.
+ // If so, send a context menu key event instead.
+ if (MaybeDispatchContextMenuEvent(aWindow, aGdkKeyEvent)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), stopped dispatching eKeyPress event "
+ "because eContextMenu event was dispatched"));
+ return;
+ }
+
+ RefPtr<TextEventDispatcher> textEventDispatcher =
+ aWindow->GetTextEventDispatcher();
+ nsresult rv = textEventDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ (" HandleKeyPressEvent(), stopped dispatching eKeyPress event "
+ "because of failed to initialize TextEventDispatcher"));
+ return;
+ }
+
+ // If the character code is in the BMP, send the key press event.
+ // Otherwise, send a compositionchange event with the equivalent UTF-16
+ // string.
+ // TODO: Investigate other browser's behavior in this case because
+ // this hack is odd for UI Events.
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, aWindow);
+ KeymapWrapper::InitKeyEvent(keypressEvent, aGdkKeyEvent, false);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ if (keypressEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING ||
+ keypressEvent.mKeyValue.Length() == 1) {
+ if (textEventDispatcher->MaybeDispatchKeypressEvents(keypressEvent, status,
+ aGdkKeyEvent)) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched eKeyPress event "
+ "(status=%s)",
+ GetStatusName(status)));
+ } else {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), didn't dispatch eKeyPress event "
+ "(status=%s)",
+ GetStatusName(status)));
+ }
+ } else {
+ WidgetEventTime eventTime = aWindow->GetWidgetEventTime(aGdkKeyEvent->time);
+ textEventDispatcher->CommitComposition(status, &keypressEvent.mKeyValue,
+ &eventTime);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyPressEvent(), dispatched a set of composition "
+ "events"));
+ }
+}
+
+/* static */
+bool KeymapWrapper::HandleKeyReleaseEvent(nsWindow* aWindow,
+ GdkEventKey* aGdkKeyEvent) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("HandleKeyReleaseEvent(aWindow=%p, aGdkKeyEvent={ type=%s, "
+ "keyval=%s(0x%X), state=0x%08X, hardware_keycode=0x%08X, "
+ "time=%u, is_modifier=%s })",
+ aWindow,
+ ((aGdkKeyEvent->type == GDK_KEY_PRESS) ? "GDK_KEY_PRESS"
+ : "GDK_KEY_RELEASE"),
+ gdk_keyval_name(aGdkKeyEvent->keyval), aGdkKeyEvent->keyval,
+ aGdkKeyEvent->state, aGdkKeyEvent->hardware_keycode,
+ aGdkKeyEvent->time, GetBoolName(aGdkKeyEvent->is_modifier)));
+
+ RefPtr<IMContextWrapper> imContext = aWindow->GetIMContext();
+ if (imContext) {
+ KeyHandlingState handlingState =
+ imContext->OnKeyEvent(aWindow, aGdkKeyEvent);
+ if (handlingState != KeyHandlingState::eNotHandled) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyReleaseEvent(), the event was handled by "
+ "IMContextWrapper"));
+ return true;
+ }
+ }
+
+ bool isCancelled = false;
+ if (NS_WARN_IF(!DispatchKeyDownOrKeyUpEvent(aWindow, aGdkKeyEvent, false,
+ &isCancelled))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ (" HandleKeyReleaseEvent(), didn't dispatch eKeyUp event"));
+ return false;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" HandleKeyReleaseEvent(), dispatched eKeyUp event "
+ "(isCancelled=%s)",
+ GetBoolName(isCancelled)));
+ return true;
+}
+
+guint KeymapWrapper::GetModifierState(GdkEventKey* aGdkKeyEvent,
+ KeymapWrapper* aWrapper) {
+ guint state = aGdkKeyEvent->state;
+ if (!aGdkKeyEvent->is_modifier) {
+ return state;
+ }
+#ifdef MOZ_X11
+ // NOTE: The state of given key event indicates adjacent state of
+ // modifier keys. E.g., even if the event is Shift key press event,
+ // the bit for Shift is still false. By the same token, even if the
+ // event is Shift key release event, the bit for Shift is still true.
+ // Unfortunately, gdk_keyboard_get_modifiers() returns current modifier
+ // state. It means if there're some pending modifier key press or
+ // key release events, the result isn't what we want.
+ GdkDisplay* gdkDisplay = gdk_display_get_default();
+ if (GdkIsX11Display(gdkDisplay)) {
+ GdkDisplay* gdkDisplay = gdk_display_get_default();
+ Display* display = gdk_x11_display_get_xdisplay(gdkDisplay);
+ if (XEventsQueued(display, QueuedAfterReading)) {
+ XEvent nextEvent;
+ XPeekEvent(display, &nextEvent);
+ if (nextEvent.type == aWrapper->mXKBBaseEventCode) {
+ XkbEvent* XKBEvent = (XkbEvent*)&nextEvent;
+ if (XKBEvent->any.xkb_type == XkbStateNotify) {
+ XkbStateNotifyEvent* stateNotifyEvent =
+ (XkbStateNotifyEvent*)XKBEvent;
+ state &= ~0xFF;
+ state |= stateNotifyEvent->lookup_mods;
+ }
+ }
+ }
+ return state;
+ }
+#endif
+#ifdef MOZ_WAYLAND
+ int mask = 0;
+ switch (aGdkKeyEvent->keyval) {
+ case GDK_Shift_L:
+ case GDK_Shift_R:
+ mask = aWrapper->GetGdkModifierMask(SHIFT);
+ break;
+ case GDK_Control_L:
+ case GDK_Control_R:
+ mask = aWrapper->GetGdkModifierMask(CTRL);
+ break;
+ case GDK_Alt_L:
+ case GDK_Alt_R:
+ mask = aWrapper->GetGdkModifierMask(ALT);
+ break;
+ case GDK_Super_L:
+ case GDK_Super_R:
+ mask = aWrapper->GetGdkModifierMask(SUPER);
+ break;
+ case GDK_Hyper_L:
+ case GDK_Hyper_R:
+ mask = aWrapper->GetGdkModifierMask(HYPER);
+ break;
+ case GDK_Meta_L:
+ case GDK_Meta_R:
+ mask = aWrapper->GetGdkModifierMask(META);
+ break;
+ default:
+ break;
+ }
+ if (aGdkKeyEvent->type == GDK_KEY_PRESS) {
+ state |= mask;
+ } else {
+ state &= ~mask;
+ }
+#endif
+ return state;
+}
+
+/* static */
+void KeymapWrapper::InitKeyEvent(WidgetKeyboardEvent& aKeyEvent,
+ GdkEventKey* aGdkKeyEvent,
+ bool aIsProcessedByIME) {
+ MOZ_ASSERT(
+ !aIsProcessedByIME || aKeyEvent.mMessage != eKeyPress,
+ "If the key event is handled by IME, keypress event shouldn't be fired");
+
+ KeymapWrapper* keymapWrapper = GetInstance();
+
+ aKeyEvent.mCodeNameIndex = ComputeDOMCodeNameIndex(aGdkKeyEvent);
+ MOZ_ASSERT(aKeyEvent.mCodeNameIndex != CODE_NAME_INDEX_USE_STRING);
+ aKeyEvent.mKeyNameIndex =
+ aIsProcessedByIME ? KEY_NAME_INDEX_Process
+ : keymapWrapper->ComputeDOMKeyNameIndex(aGdkKeyEvent);
+ if (aKeyEvent.mKeyNameIndex == KEY_NAME_INDEX_Unidentified) {
+ uint32_t charCode = GetCharCodeFor(aGdkKeyEvent);
+ if (!charCode) {
+ charCode = keymapWrapper->GetUnmodifiedCharCodeFor(aGdkKeyEvent);
+ }
+ if (charCode) {
+ aKeyEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ MOZ_ASSERT(aKeyEvent.mKeyValue.IsEmpty(),
+ "Uninitialized mKeyValue must be empty");
+ AppendUCS4ToUTF16(charCode, aKeyEvent.mKeyValue);
+ }
+ }
+
+ if (aIsProcessedByIME) {
+ aKeyEvent.mKeyCode = NS_VK_PROCESSKEY;
+ } else if (aKeyEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING ||
+ aKeyEvent.mMessage != eKeyPress) {
+ aKeyEvent.mKeyCode = ComputeDOMKeyCode(aGdkKeyEvent);
+ } else {
+ aKeyEvent.mKeyCode = 0;
+ }
+
+ const guint modifierState = GetModifierState(aGdkKeyEvent, keymapWrapper);
+ InitInputEvent(aKeyEvent, modifierState);
+
+ switch (aGdkKeyEvent->keyval) {
+ case GDK_Shift_L:
+ case GDK_Control_L:
+ case GDK_Alt_L:
+ case GDK_Super_L:
+ case GDK_Hyper_L:
+ case GDK_Meta_L:
+ aKeyEvent.mLocation = eKeyLocationLeft;
+ break;
+
+ case GDK_Shift_R:
+ case GDK_Control_R:
+ case GDK_Alt_R:
+ case GDK_Super_R:
+ case GDK_Hyper_R:
+ case GDK_Meta_R:
+ aKeyEvent.mLocation = eKeyLocationRight;
+ break;
+
+ case GDK_KP_0:
+ case GDK_KP_1:
+ case GDK_KP_2:
+ case GDK_KP_3:
+ case GDK_KP_4:
+ case GDK_KP_5:
+ case GDK_KP_6:
+ case GDK_KP_7:
+ case GDK_KP_8:
+ case GDK_KP_9:
+ case GDK_KP_Space:
+ case GDK_KP_Tab:
+ case GDK_KP_Enter:
+ case GDK_KP_F1:
+ case GDK_KP_F2:
+ case GDK_KP_F3:
+ case GDK_KP_F4:
+ case GDK_KP_Home:
+ case GDK_KP_Left:
+ case GDK_KP_Up:
+ case GDK_KP_Right:
+ case GDK_KP_Down:
+ case GDK_KP_Prior: // same as GDK_KP_Page_Up
+ case GDK_KP_Next: // same as GDK_KP_Page_Down
+ case GDK_KP_End:
+ case GDK_KP_Begin:
+ case GDK_KP_Insert:
+ case GDK_KP_Delete:
+ case GDK_KP_Equal:
+ case GDK_KP_Multiply:
+ case GDK_KP_Add:
+ case GDK_KP_Separator:
+ case GDK_KP_Subtract:
+ case GDK_KP_Decimal:
+ case GDK_KP_Divide:
+ aKeyEvent.mLocation = eKeyLocationNumpad;
+ break;
+
+ default:
+ aKeyEvent.mLocation = eKeyLocationStandard;
+ break;
+ }
+
+ // The transformations above and in gdk for the keyval are not invertible
+ // so link to the GdkEvent (which will vanish soon after return from the
+ // event callback) to give plugins access to hardware_keycode and state.
+ // (An XEvent would be nice but the GdkEvent is good enough.)
+ aKeyEvent.mNativeKeyEvent = static_cast<void*>(aGdkKeyEvent);
+ aKeyEvent.mIsRepeat =
+ sRepeatState == REPEATING &&
+ aGdkKeyEvent->hardware_keycode == sLastRepeatableHardwareKeyCode;
+
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p InitKeyEvent, modifierState=0x%08X "
+ "aKeyEvent={ mMessage=%s, isShift=%s, isControl=%s, "
+ "isAlt=%s, isMeta=%s , mKeyCode=0x%02X, mCharCode=%s, "
+ "mKeyNameIndex=%s, mKeyValue=%s, mCodeNameIndex=%s, mCodeValue=%s, "
+ "mLocation=%s, mIsRepeat=%s }",
+ keymapWrapper, modifierState, ToChar(aKeyEvent.mMessage),
+ GetBoolName(aKeyEvent.IsShift()), GetBoolName(aKeyEvent.IsControl()),
+ GetBoolName(aKeyEvent.IsAlt()), GetBoolName(aKeyEvent.IsMeta()),
+ aKeyEvent.mKeyCode,
+ GetCharacterCodeName(static_cast<char16_t>(aKeyEvent.mCharCode)).get(),
+ ToString(aKeyEvent.mKeyNameIndex).get(),
+ GetCharacterCodeNames(aKeyEvent.mKeyValue).get(),
+ ToString(aKeyEvent.mCodeNameIndex).get(),
+ GetCharacterCodeNames(aKeyEvent.mCodeValue).get(),
+ GetKeyLocationName(aKeyEvent.mLocation).get(),
+ GetBoolName(aKeyEvent.mIsRepeat)));
+}
+
+/* static */
+uint32_t KeymapWrapper::GetCharCodeFor(const GdkEventKey* aGdkKeyEvent) {
+ // Anything above 0xf000 is considered a non-printable
+ // Exception: directly encoded UCS characters
+ if (aGdkKeyEvent->keyval > 0xf000 &&
+ (aGdkKeyEvent->keyval & 0xff000000) != 0x01000000) {
+ // Keypad keys are an exception: they return a value different
+ // from their non-keypad equivalents, but mozilla doesn't distinguish.
+ switch (aGdkKeyEvent->keyval) {
+ case GDK_KP_Space:
+ return ' ';
+ case GDK_KP_Equal:
+ return '=';
+ case GDK_KP_Multiply:
+ return '*';
+ case GDK_KP_Add:
+ return '+';
+ case GDK_KP_Separator:
+ return ',';
+ case GDK_KP_Subtract:
+ return '-';
+ case GDK_KP_Decimal:
+ return '.';
+ case GDK_KP_Divide:
+ return '/';
+ case GDK_KP_0:
+ return '0';
+ case GDK_KP_1:
+ return '1';
+ case GDK_KP_2:
+ return '2';
+ case GDK_KP_3:
+ return '3';
+ case GDK_KP_4:
+ return '4';
+ case GDK_KP_5:
+ return '5';
+ case GDK_KP_6:
+ return '6';
+ case GDK_KP_7:
+ return '7';
+ case GDK_KP_8:
+ return '8';
+ case GDK_KP_9:
+ return '9';
+ default:
+ return 0; // non-printables
+ }
+ }
+
+ static const long MAX_UNICODE = 0x10FFFF;
+
+ // we're supposedly printable, let's try to convert
+ long ucs = keysym2ucs(aGdkKeyEvent->keyval);
+ if ((ucs != -1) && (ucs < MAX_UNICODE)) {
+ return ucs;
+ }
+
+ // I guess we couldn't convert
+ return 0;
+}
+
+uint32_t KeymapWrapper::GetCharCodeFor(const GdkEventKey* aGdkKeyEvent,
+ guint aGdkModifierState, gint aGroup) {
+ guint keyval;
+ if (!gdk_keymap_translate_keyboard_state(
+ mGdkKeymap, aGdkKeyEvent->hardware_keycode,
+ GdkModifierType(aGdkModifierState), aGroup, &keyval, nullptr, nullptr,
+ nullptr)) {
+ return 0;
+ }
+ GdkEventKey tmpEvent = *aGdkKeyEvent;
+ tmpEvent.state = aGdkModifierState;
+ tmpEvent.keyval = keyval;
+ tmpEvent.group = aGroup;
+ return GetCharCodeFor(&tmpEvent);
+}
+
+uint32_t KeymapWrapper::GetUnmodifiedCharCodeFor(
+ const GdkEventKey* aGdkKeyEvent) {
+ guint state =
+ aGdkKeyEvent->state &
+ (GetGdkModifierMask(SHIFT) | GetGdkModifierMask(CAPS_LOCK) |
+ GetGdkModifierMask(NUM_LOCK) | GetGdkModifierMask(SCROLL_LOCK) |
+ GetGdkModifierMask(LEVEL3) | GetGdkModifierMask(LEVEL5));
+ uint32_t charCode =
+ GetCharCodeFor(aGdkKeyEvent, GdkModifierType(state), aGdkKeyEvent->group);
+ if (charCode) {
+ return charCode;
+ }
+ // If no character is mapped to the key when Level3 Shift or Level5 Shift
+ // is active, let's return a character which is inputted by the key without
+ // Level3 nor Level5 Shift.
+ guint stateWithoutAltGraph =
+ state & ~(GetGdkModifierMask(LEVEL3) | GetGdkModifierMask(LEVEL5));
+ if (state == stateWithoutAltGraph) {
+ return 0;
+ }
+ return GetCharCodeFor(aGdkKeyEvent, GdkModifierType(stateWithoutAltGraph),
+ aGdkKeyEvent->group);
+}
+
+gint KeymapWrapper::GetKeyLevel(GdkEventKey* aGdkKeyEvent) {
+ gint level;
+ if (!gdk_keymap_translate_keyboard_state(
+ mGdkKeymap, aGdkKeyEvent->hardware_keycode,
+ GdkModifierType(aGdkKeyEvent->state), aGdkKeyEvent->group, nullptr,
+ nullptr, &level, nullptr)) {
+ return -1;
+ }
+ return level;
+}
+
+gint KeymapWrapper::GetFirstLatinGroup() {
+ GdkKeymapKey* keys;
+ gint count;
+ gint minGroup = -1;
+ if (gdk_keymap_get_entries_for_keyval(mGdkKeymap, GDK_a, &keys, &count)) {
+ // find the minimum number group for latin inputtable layout
+ for (gint i = 0; i < count && minGroup != 0; ++i) {
+ if (keys[i].level != 0 && keys[i].level != 1) {
+ continue;
+ }
+ if (minGroup >= 0 && keys[i].group > minGroup) {
+ continue;
+ }
+ minGroup = keys[i].group;
+ }
+ g_free(keys);
+ }
+ return minGroup;
+}
+
+bool KeymapWrapper::IsLatinGroup(guint8 aGroup) {
+ GdkKeymapKey* keys;
+ gint count;
+ bool result = false;
+ if (gdk_keymap_get_entries_for_keyval(mGdkKeymap, GDK_a, &keys, &count)) {
+ for (gint i = 0; i < count; ++i) {
+ if (keys[i].level != 0 && keys[i].level != 1) {
+ continue;
+ }
+ if (keys[i].group == aGroup) {
+ result = true;
+ break;
+ }
+ }
+ g_free(keys);
+ }
+ return result;
+}
+
+bool KeymapWrapper::IsAutoRepeatableKey(guint aHardwareKeyCode) {
+#ifdef MOZ_X11
+ uint8_t indexOfArray = aHardwareKeyCode / 8;
+ MOZ_ASSERT(indexOfArray < ArrayLength(mKeyboardState.auto_repeats),
+ "invalid index");
+ char bitMask = 1 << (aHardwareKeyCode % 8);
+ return (mKeyboardState.auto_repeats[indexOfArray] & bitMask) != 0;
+#else
+ return false;
+#endif
+}
+
+/* static */
+bool KeymapWrapper::IsBasicLatinLetterOrNumeral(uint32_t aCharCode) {
+ return (aCharCode >= 'a' && aCharCode <= 'z') ||
+ (aCharCode >= 'A' && aCharCode <= 'Z') ||
+ (aCharCode >= '0' && aCharCode <= '9');
+}
+
+/* static */
+guint KeymapWrapper::GetGDKKeyvalWithoutModifier(
+ const GdkEventKey* aGdkKeyEvent) {
+ KeymapWrapper* keymapWrapper = GetInstance();
+ guint state =
+ (aGdkKeyEvent->state & keymapWrapper->GetGdkModifierMask(NUM_LOCK));
+ guint keyval;
+ if (!gdk_keymap_translate_keyboard_state(
+ keymapWrapper->mGdkKeymap, aGdkKeyEvent->hardware_keycode,
+ GdkModifierType(state), aGdkKeyEvent->group, &keyval, nullptr,
+ nullptr, nullptr)) {
+ return 0;
+ }
+ return keyval;
+}
+
+/* static */
+uint32_t KeymapWrapper::GetDOMKeyCodeFromKeyPairs(guint aGdkKeyval) {
+ switch (aGdkKeyval) {
+ case GDK_Cancel:
+ return NS_VK_CANCEL;
+ case GDK_BackSpace:
+ return NS_VK_BACK;
+ case GDK_Tab:
+ case GDK_ISO_Left_Tab:
+ return NS_VK_TAB;
+ case GDK_Clear:
+ return NS_VK_CLEAR;
+ case GDK_Return:
+ return NS_VK_RETURN;
+ case GDK_Shift_L:
+ case GDK_Shift_R:
+ case GDK_Shift_Lock:
+ return NS_VK_SHIFT;
+ case GDK_Control_L:
+ case GDK_Control_R:
+ return NS_VK_CONTROL;
+ case GDK_Alt_L:
+ case GDK_Alt_R:
+ return NS_VK_ALT;
+ case GDK_Meta_L:
+ case GDK_Meta_R:
+ return NS_VK_META;
+
+ // Assume that Super or Hyper is always mapped to physical Win key.
+ case GDK_Super_L:
+ case GDK_Super_R:
+ case GDK_Hyper_L:
+ case GDK_Hyper_R:
+ return NS_VK_WIN;
+
+ // GTK's AltGraph key is similar to Mac's Option (Alt) key. However,
+ // unfortunately, browsers on Mac are using NS_VK_ALT for it even though
+ // it's really different from Alt key on Windows.
+ // On the other hand, GTK's AltGrapsh keys are really different from
+ // Alt key. However, there is no AltGrapsh key on Windows. On Windows,
+ // both Ctrl and Alt keys are pressed internally when AltGr key is
+ // pressed. For some languages' users, AltGraph key is important, so,
+ // web applications on such locale may want to know AltGraph key press.
+ // Therefore, we should map AltGr keycode for them only on GTK.
+ case GDK_ISO_Level3_Shift:
+ case GDK_ISO_Level5_Shift:
+ // We assume that Mode_switch is always used for level3 shift.
+ case GDK_Mode_switch:
+ return NS_VK_ALTGR;
+
+ case GDK_Pause:
+ return NS_VK_PAUSE;
+ case GDK_Caps_Lock:
+ return NS_VK_CAPS_LOCK;
+ case GDK_Kana_Lock:
+ case GDK_Kana_Shift:
+ return NS_VK_KANA;
+ case GDK_Hangul:
+ return NS_VK_HANGUL;
+ // case GDK_XXX: return NS_VK_JUNJA;
+ // case GDK_XXX: return NS_VK_FINAL;
+ case GDK_Hangul_Hanja:
+ return NS_VK_HANJA;
+ case GDK_Kanji:
+ return NS_VK_KANJI;
+ case GDK_Escape:
+ return NS_VK_ESCAPE;
+ case GDK_Henkan:
+ return NS_VK_CONVERT;
+ case GDK_Muhenkan:
+ return NS_VK_NONCONVERT;
+ // case GDK_XXX: return NS_VK_ACCEPT;
+ // case GDK_XXX: return NS_VK_MODECHANGE;
+ case GDK_Page_Up:
+ return NS_VK_PAGE_UP;
+ case GDK_Page_Down:
+ return NS_VK_PAGE_DOWN;
+ case GDK_End:
+ return NS_VK_END;
+ case GDK_Home:
+ return NS_VK_HOME;
+ case GDK_Left:
+ return NS_VK_LEFT;
+ case GDK_Up:
+ return NS_VK_UP;
+ case GDK_Right:
+ return NS_VK_RIGHT;
+ case GDK_Down:
+ return NS_VK_DOWN;
+ case GDK_Select:
+ return NS_VK_SELECT;
+ case GDK_Print:
+ return NS_VK_PRINT;
+ case GDK_Execute:
+ return NS_VK_EXECUTE;
+ case GDK_Insert:
+ return NS_VK_INSERT;
+ case GDK_Delete:
+ return NS_VK_DELETE;
+ case GDK_Help:
+ return NS_VK_HELP;
+
+ // keypad keys
+ case GDK_KP_Left:
+ return NS_VK_LEFT;
+ case GDK_KP_Right:
+ return NS_VK_RIGHT;
+ case GDK_KP_Up:
+ return NS_VK_UP;
+ case GDK_KP_Down:
+ return NS_VK_DOWN;
+ case GDK_KP_Page_Up:
+ return NS_VK_PAGE_UP;
+ // Not sure what these are
+ // case GDK_KP_Prior: return NS_VK_;
+ // case GDK_KP_Next: return NS_VK_;
+ case GDK_KP_Begin:
+ return NS_VK_CLEAR; // Num-unlocked 5
+ case GDK_KP_Page_Down:
+ return NS_VK_PAGE_DOWN;
+ case GDK_KP_Home:
+ return NS_VK_HOME;
+ case GDK_KP_End:
+ return NS_VK_END;
+ case GDK_KP_Insert:
+ return NS_VK_INSERT;
+ case GDK_KP_Delete:
+ return NS_VK_DELETE;
+ case GDK_KP_Enter:
+ return NS_VK_RETURN;
+
+ case GDK_Num_Lock:
+ return NS_VK_NUM_LOCK;
+ case GDK_Scroll_Lock:
+ return NS_VK_SCROLL_LOCK;
+
+ // Function keys
+ case GDK_F1:
+ return NS_VK_F1;
+ case GDK_F2:
+ return NS_VK_F2;
+ case GDK_F3:
+ return NS_VK_F3;
+ case GDK_F4:
+ return NS_VK_F4;
+ case GDK_F5:
+ return NS_VK_F5;
+ case GDK_F6:
+ return NS_VK_F6;
+ case GDK_F7:
+ return NS_VK_F7;
+ case GDK_F8:
+ return NS_VK_F8;
+ case GDK_F9:
+ return NS_VK_F9;
+ case GDK_F10:
+ return NS_VK_F10;
+ case GDK_F11:
+ return NS_VK_F11;
+ case GDK_F12:
+ return NS_VK_F12;
+ case GDK_F13:
+ return NS_VK_F13;
+ case GDK_F14:
+ return NS_VK_F14;
+ case GDK_F15:
+ return NS_VK_F15;
+ case GDK_F16:
+ return NS_VK_F16;
+ case GDK_F17:
+ return NS_VK_F17;
+ case GDK_F18:
+ return NS_VK_F18;
+ case GDK_F19:
+ return NS_VK_F19;
+ case GDK_F20:
+ return NS_VK_F20;
+ case GDK_F21:
+ return NS_VK_F21;
+ case GDK_F22:
+ return NS_VK_F22;
+ case GDK_F23:
+ return NS_VK_F23;
+ case GDK_F24:
+ return NS_VK_F24;
+
+ // context menu key, keysym 0xff67, typically keycode 117 on 105-key
+ // (Microsoft) x86 keyboards, located between right 'Windows' key and
+ // right Ctrl key
+ case GDK_Menu:
+ return NS_VK_CONTEXT_MENU;
+ case GDK_Sleep:
+ return NS_VK_SLEEP;
+
+ case GDK_3270_Attn:
+ return NS_VK_ATTN;
+ case GDK_3270_CursorSelect:
+ return NS_VK_CRSEL;
+ case GDK_3270_ExSelect:
+ return NS_VK_EXSEL;
+ case GDK_3270_EraseEOF:
+ return NS_VK_EREOF;
+ case GDK_3270_Play:
+ return NS_VK_PLAY;
+ // case GDK_XXX: return NS_VK_ZOOM;
+ case GDK_3270_PA1:
+ return NS_VK_PA1;
+
+ // map Sun Keyboard special keysyms on to NS_VK keys
+
+ // Sun F11 key generates SunF36(0x1005ff10) keysym
+ case 0x1005ff10:
+ return NS_VK_F11;
+ // Sun F12 key generates SunF37(0x1005ff11) keysym
+ case 0x1005ff11:
+ return NS_VK_F12;
+ default:
+ return 0;
+ }
+}
+
+void KeymapWrapper::WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyEvent,
+ GdkEventKey* aGdkKeyEvent) {
+ GetInstance()->WillDispatchKeyboardEventInternal(aKeyEvent, aGdkKeyEvent);
+}
+
+void KeymapWrapper::WillDispatchKeyboardEventInternal(
+ WidgetKeyboardEvent& aKeyEvent, GdkEventKey* aGdkKeyEvent) {
+ if (!aGdkKeyEvent) {
+ // If aGdkKeyEvent is nullptr, we're trying to dispatch a fake keyboard
+ // event in such case, we don't need to set alternative char codes.
+ // So, we don't need to do nothing here. This case is typically we're
+ // dispatching eKeyDown or eKeyUp event during composition.
+ return;
+ }
+
+ uint32_t charCode = GetCharCodeFor(aGdkKeyEvent);
+ if (!charCode) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p WillDispatchKeyboardEventInternal, "
+ "mKeyCode=0x%02X, charCode=0x%08X",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode));
+ return;
+ }
+
+ // The mCharCode was set from mKeyValue. However, for example, when Ctrl key
+ // is pressed, its value should indicate an ASCII character for backward
+ // compatibility rather than inputting character without the modifiers.
+ // Therefore, we need to modify mCharCode value here.
+ aKeyEvent.SetCharCode(charCode);
+
+ gint level = GetKeyLevel(aGdkKeyEvent);
+ if (level != 0 && level != 1) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p WillDispatchKeyboardEventInternal, "
+ "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level));
+ return;
+ }
+
+ guint baseState = aGdkKeyEvent->state &
+ ~(GetGdkModifierMask(SHIFT) | GetGdkModifierMask(CTRL) |
+ GetGdkModifierMask(ALT) | GetGdkModifierMask(META) |
+ GetGdkModifierMask(SUPER) | GetGdkModifierMask(HYPER));
+
+ // We shold send both shifted char and unshifted char, all keyboard layout
+ // users can use all keys. Don't change event.mCharCode. On some keyboard
+ // layouts, Ctrl/Alt/Meta keys are used for inputting some characters.
+ AlternativeCharCode altCharCodes(0, 0);
+ // unshifted charcode of current keyboard layout.
+ altCharCodes.mUnshiftedCharCode =
+ GetCharCodeFor(aGdkKeyEvent, baseState, aGdkKeyEvent->group);
+ bool isLatin = (altCharCodes.mUnshiftedCharCode <= 0xFF);
+ // shifted charcode of current keyboard layout.
+ altCharCodes.mShiftedCharCode = GetCharCodeFor(
+ aGdkKeyEvent, baseState | GetGdkModifierMask(SHIFT), aGdkKeyEvent->group);
+ isLatin = isLatin && (altCharCodes.mShiftedCharCode <= 0xFF);
+ if (altCharCodes.mUnshiftedCharCode || altCharCodes.mShiftedCharCode) {
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altCharCodes);
+ }
+
+ bool needLatinKeyCodes = !isLatin;
+ if (!needLatinKeyCodes) {
+ needLatinKeyCodes =
+ (IS_ASCII_ALPHABETICAL(altCharCodes.mUnshiftedCharCode) !=
+ IS_ASCII_ALPHABETICAL(altCharCodes.mShiftedCharCode));
+ }
+
+ // If current keyboard layout can input Latin characters, we don't need
+ // more information.
+ if (!needLatinKeyCodes) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p WillDispatchKeyboardEventInternal, "
+ "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d, altCharCodes={ "
+ "mUnshiftedCharCode=0x%08X, mShiftedCharCode=0x%08X }",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level,
+ altCharCodes.mUnshiftedCharCode, altCharCodes.mShiftedCharCode));
+ return;
+ }
+
+ // Next, find Latin inputtable keyboard layout.
+ gint minGroup = GetFirstLatinGroup();
+ if (minGroup < 0) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p WillDispatchKeyboardEventInternal, "
+ "Latin keyboard layout isn't found: "
+ "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d, "
+ "altCharCodes={ mUnshiftedCharCode=0x%08X, "
+ "mShiftedCharCode=0x%08X }",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level,
+ altCharCodes.mUnshiftedCharCode, altCharCodes.mShiftedCharCode));
+ return;
+ }
+
+ AlternativeCharCode altLatinCharCodes(0, 0);
+ uint32_t unmodifiedCh = aKeyEvent.IsShift() ? altCharCodes.mShiftedCharCode
+ : altCharCodes.mUnshiftedCharCode;
+
+ // unshifted charcode of found keyboard layout.
+ uint32_t ch = GetCharCodeFor(aGdkKeyEvent, baseState, minGroup);
+ altLatinCharCodes.mUnshiftedCharCode =
+ IsBasicLatinLetterOrNumeral(ch) ? ch : 0;
+ // shifted charcode of found keyboard layout.
+ ch = GetCharCodeFor(aGdkKeyEvent, baseState | GetGdkModifierMask(SHIFT),
+ minGroup);
+ altLatinCharCodes.mShiftedCharCode = IsBasicLatinLetterOrNumeral(ch) ? ch : 0;
+ if (altLatinCharCodes.mUnshiftedCharCode ||
+ altLatinCharCodes.mShiftedCharCode) {
+ aKeyEvent.mAlternativeCharCodes.AppendElement(altLatinCharCodes);
+ }
+ // If the mCharCode is not Latin, and the level is 0 or 1, we should
+ // replace the mCharCode to Latin char if Alt keys are not pressed. (Alt
+ // should be sent the localized char for accesskey like handling of Web
+ // Applications.)
+ ch = aKeyEvent.IsShift() ? altLatinCharCodes.mShiftedCharCode
+ : altLatinCharCodes.mUnshiftedCharCode;
+ if (ch && !aKeyEvent.IsAlt() && charCode == unmodifiedCh) {
+ aKeyEvent.SetCharCode(ch);
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p WillDispatchKeyboardEventInternal, "
+ "mKeyCode=0x%02X, mCharCode=0x%08X, level=%d, minGroup=%d, "
+ "altCharCodes={ mUnshiftedCharCode=0x%08X, "
+ "mShiftedCharCode=0x%08X } "
+ "altLatinCharCodes={ mUnshiftedCharCode=0x%08X, "
+ "mShiftedCharCode=0x%08X }",
+ this, aKeyEvent.mKeyCode, aKeyEvent.mCharCode, level, minGroup,
+ altCharCodes.mUnshiftedCharCode, altCharCodes.mShiftedCharCode,
+ altLatinCharCodes.mUnshiftedCharCode,
+ altLatinCharCodes.mShiftedCharCode));
+}
+
+#ifdef MOZ_WAYLAND
+void KeymapWrapper::SetFocusIn(wl_surface* aFocusSurface,
+ uint32_t aFocusSerial) {
+ LOGW("KeymapWrapper::SetFocusIn() surface %p ID %d serial %d", aFocusSurface,
+ aFocusSurface ? wl_proxy_get_id((struct wl_proxy*)aFocusSurface) : 0,
+ aFocusSerial);
+
+ KeymapWrapper* keymapWrapper = KeymapWrapper::GetInstance();
+ keymapWrapper->mFocusSurface = aFocusSurface;
+ keymapWrapper->mFocusSerial = aFocusSerial;
+}
+
+// aFocusSurface can be null in case that focused surface is already destroyed.
+void KeymapWrapper::SetFocusOut(wl_surface* aFocusSurface) {
+ KeymapWrapper* keymapWrapper = KeymapWrapper::GetInstance();
+ LOGW("KeymapWrapper::SetFocusOut surface %p ID %d", aFocusSurface,
+ aFocusSurface ? wl_proxy_get_id((struct wl_proxy*)aFocusSurface) : 0);
+
+ keymapWrapper->mFocusSurface = nullptr;
+ keymapWrapper->mFocusSerial = 0;
+}
+
+void KeymapWrapper::GetFocusInfo(wl_surface** aFocusSurface,
+ uint32_t* aFocusSerial) {
+ KeymapWrapper* keymapWrapper = KeymapWrapper::GetInstance();
+ *aFocusSurface = keymapWrapper->mFocusSurface;
+ *aFocusSerial = keymapWrapper->mFocusSerial;
+}
+
+void KeymapWrapper::SetSeat(wl_seat* aSeat, int aId) {
+ sSeat = aSeat;
+ sSeatID = aId;
+ wl_seat_add_listener(aSeat, &seat_listener, nullptr);
+}
+
+void KeymapWrapper::ClearSeat(int aId) {
+ if (sSeatID == aId) {
+ ClearKeyboard();
+ sSeat = nullptr;
+ sSeatID = -1;
+ }
+}
+
+wl_seat* KeymapWrapper::GetSeat() { return sSeat; }
+
+void KeymapWrapper::SetKeyboard(wl_keyboard* aKeyboard) {
+ sKeyboard = aKeyboard;
+}
+
+wl_keyboard* KeymapWrapper::GetKeyboard() { return sKeyboard; }
+
+void KeymapWrapper::ClearKeyboard() {
+ if (sKeyboard) {
+ wl_keyboard_destroy(sKeyboard);
+ sKeyboard = nullptr;
+ }
+}
+#endif
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/gtk/nsGtkKeyUtils.h b/widget/gtk/nsGtkKeyUtils.h
new file mode 100644
index 0000000000..aac9d446f3
--- /dev/null
+++ b/widget/gtk/nsGtkKeyUtils.h
@@ -0,0 +1,509 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsGdkKeyUtils_h__
+#define __nsGdkKeyUtils_h__
+
+#include "mozilla/EventForwards.h"
+#include "nsIWidget.h"
+#include "nsTArray.h"
+
+#include <gdk/gdk.h>
+#ifdef MOZ_X11
+# include <X11/XKBlib.h>
+#endif
+#ifdef MOZ_WAYLAND
+# include <gdk/gdkwayland.h>
+# include <xkbcommon/xkbcommon.h>
+#endif
+#include "X11UndefineNone.h"
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * KeymapWrapper is a wrapper class of GdkKeymap. GdkKeymap doesn't support
+ * all our needs, therefore, we need to access lower level APIs.
+ * But such code is usually complex and might be slow. Against such issues,
+ * we should cache some information.
+ *
+ * This class provides only static methods. The methods is using internal
+ * singleton instance which is initialized by default GdkKeymap. When the
+ * GdkKeymap is destroyed, the singleton instance will be destroyed.
+ */
+
+class KeymapWrapper {
+ public:
+ /**
+ * Compute an our DOM keycode from a GDK keyval.
+ */
+ static uint32_t ComputeDOMKeyCode(const GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * Compute a DOM key name index from aGdkKeyEvent.
+ */
+ static KeyNameIndex ComputeDOMKeyNameIndex(const GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * Compute a DOM code name index from aGdkKeyEvent.
+ */
+ static CodeNameIndex ComputeDOMCodeNameIndex(const GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * We need to translate modifiers masks from Gdk to Gecko.
+ * MappedModifier is a table of mapped modifiers, we ignore other
+ * Gdk ones.
+ */
+ enum MappedModifier {
+ NOT_MODIFIER = 0x0000,
+ CAPS_LOCK = 0x0001,
+ NUM_LOCK = 0x0002,
+ SCROLL_LOCK = 0x0004,
+ SHIFT = 0x0008,
+ CTRL = 0x0010,
+ ALT = 0x0020,
+ META = 0x0040,
+ SUPER = 0x0080,
+ HYPER = 0x0100,
+ LEVEL3 = 0x0200,
+ LEVEL5 = 0x0400
+ };
+
+ /**
+ * MappedModifiers is used for combination of MappedModifier.
+ * E.g., |MappedModifiers modifiers = (SHIFT | CTRL);| means Shift and Ctrl.
+ */
+ typedef uint32_t MappedModifiers;
+
+ /**
+ * GetCurrentModifierState() returns current modifier key state.
+ * The "current" means actual state of hardware keyboard when this is
+ * called. I.e., if some key events are not still dispatched by GDK,
+ * the state may mismatch with GdkEventKey::state.
+ *
+ * @return Current modifier key state.
+ */
+ static guint GetCurrentModifierState();
+
+ /**
+ * Utility function to compute current keyboard modifiers for
+ * WidgetInputEvent
+ */
+ static uint32_t ComputeCurrentKeyModifiers();
+
+ /**
+ * Utility function to covert platform modifier state to keyboard modifiers
+ * of WidgetInputEvent
+ */
+ static uint32_t ComputeKeyModifiers(guint aGdkModifierState);
+
+ /**
+ * Convert native modifiers for `nsIWidget::SynthesizeNative*()` to
+ * GDK's state.
+ */
+ static guint ConvertWidgetModifierToGdkState(
+ nsIWidget::Modifiers aNativeModifiers);
+
+ /**
+ * InitInputEvent() initializes the aInputEvent with aModifierState.
+ */
+ static void InitInputEvent(WidgetInputEvent& aInputEvent,
+ guint aGdkModifierState);
+
+ /**
+ * InitKeyEvent() intializes aKeyEvent's modifier key related members
+ * and keycode related values.
+ *
+ * @param aKeyEvent It's an WidgetKeyboardEvent which needs to be
+ * initialized.
+ * @param aGdkKeyEvent A native GDK key event.
+ * @param aIsProcessedByIME true if aGdkKeyEvent is handled by IME.
+ */
+ static void InitKeyEvent(WidgetKeyboardEvent& aKeyEvent,
+ GdkEventKey* aGdkKeyEvent, bool aIsProcessedByIME);
+
+ /**
+ * DispatchKeyDownOrKeyUpEvent() dispatches eKeyDown or eKeyUp event.
+ *
+ * @param aWindow The window to dispatch a keyboard event.
+ * @param aGdkKeyEvent A native GDK_KEY_PRESS or GDK_KEY_RELEASE
+ * event.
+ * @param aIsProcessedByIME true if the event is handled by IME.
+ * @param aIsCancelled [Out] true if the default is prevented.
+ * @return true if eKeyDown event is actually dispatched.
+ * Otherwise, false.
+ */
+ static bool DispatchKeyDownOrKeyUpEvent(nsWindow* aWindow,
+ GdkEventKey* aGdkKeyEvent,
+ bool aIsProcessedByIME,
+ bool* aIsCancelled);
+
+ /**
+ * DispatchKeyDownOrKeyUpEvent() dispatches eKeyDown or eKeyUp event.
+ *
+ * @param aWindow The window to dispatch aKeyboardEvent.
+ * @param aKeyboardEvent An eKeyDown or eKeyUp event. This will be
+ * dispatched as is.
+ * @param aIsCancelled [Out] true if the default is prevented.
+ * @return true if eKeyDown event is actually dispatched.
+ * Otherwise, false.
+ */
+ static bool DispatchKeyDownOrKeyUpEvent(nsWindow* aWindow,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ bool* aIsCancelled);
+
+ /**
+ * GDK_KEY_PRESS event handler.
+ *
+ * @param aWindow The window to dispatch eKeyDown event (and maybe
+ * eKeyPress events).
+ * @param aGdkKeyEvent Receivied GDK_KEY_PRESS event.
+ */
+ static void HandleKeyPressEvent(nsWindow* aWindow, GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * GDK_KEY_RELEASE event handler.
+ *
+ * @param aWindow The window to dispatch eKeyUp event.
+ * @param aGdkKeyEvent Receivied GDK_KEY_RELEASE event.
+ * @return true if an event is dispatched. Otherwise, false.
+ */
+ static bool HandleKeyReleaseEvent(nsWindow* aWindow,
+ GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * WillDispatchKeyboardEvent() is called via
+ * TextEventDispatcherListener::WillDispatchKeyboardEvent().
+ *
+ * @param aKeyEvent An instance of KeyboardEvent which will be
+ * dispatched. This method should set charCode
+ * and alternative char codes if it's necessary.
+ * @param aGdkKeyEvent A GdkEventKey instance which caused the
+ * aKeyEvent.
+ */
+ static void WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyEvent,
+ GdkEventKey* aGdkKeyEvent);
+
+#ifdef MOZ_WAYLAND
+ /**
+ * Utility function to set all supported modifier masks
+ * from xkb_keymap. We call that from Wayland backend routines.
+ */
+ static void SetModifierMasks(xkb_keymap* aKeymap);
+
+ /**
+ * Wayland global focus handlers
+ */
+ static void SetFocusIn(wl_surface* aFocusSurface, uint32_t aFocusSerial);
+ static void SetFocusOut(wl_surface* aFocusSurface);
+ static void GetFocusInfo(wl_surface** aFocusSurface, uint32_t* aFocusSerial);
+
+ static void SetSeat(wl_seat* aSeat, int aId);
+ static void ClearSeat(int aId);
+ static wl_seat* GetSeat();
+
+ static void SetKeyboard(wl_keyboard* aKeyboard);
+ static wl_keyboard* GetKeyboard();
+ static void ClearKeyboard();
+
+ /**
+ * EnsureInstance() is provided on Wayland to register Wayland callbacks
+ * early.
+ */
+ static void EnsureInstance();
+#endif
+
+ /**
+ * ResetKeyboard is called on keymap changes from OnKeysChanged and
+ * keyboard_handle_keymap to prepare for keymap changes.
+ */
+ static void ResetKeyboard();
+
+ /**
+ * Destroys the singleton KeymapWrapper instance, if it exists.
+ */
+ static void Shutdown();
+
+ private:
+ /**
+ * GetInstance() returns a KeymapWrapper instance.
+ *
+ * @return A singleton instance of KeymapWrapper.
+ */
+ static KeymapWrapper* GetInstance();
+
+ KeymapWrapper();
+ ~KeymapWrapper();
+
+ bool mInitialized;
+
+ /**
+ * Initializing methods.
+ */
+ void Init();
+#ifdef MOZ_X11
+ void InitXKBExtension();
+ void InitBySystemSettingsX11();
+#endif
+#ifdef MOZ_WAYLAND
+ void InitBySystemSettingsWayland();
+#endif
+
+ /**
+ * mModifierKeys stores each hardware key information.
+ */
+ struct ModifierKey {
+ guint mHardwareKeycode;
+ guint mMask;
+
+ explicit ModifierKey(guint aHardwareKeycode)
+ : mHardwareKeycode(aHardwareKeycode), mMask(0) {}
+ };
+ nsTArray<ModifierKey> mModifierKeys;
+
+ /**
+ * GetModifierKey() returns modifier key information of the hardware
+ * keycode. If the key isn't a modifier key, returns nullptr.
+ */
+ ModifierKey* GetModifierKey(guint aHardwareKeycode);
+
+ /**
+ * mModifierMasks is bit masks for each modifier. The index should be one
+ * of ModifierIndex values.
+ */
+ enum ModifierIndex {
+ INDEX_NUM_LOCK,
+ INDEX_SCROLL_LOCK,
+ INDEX_ALT,
+ INDEX_META,
+ INDEX_HYPER,
+ INDEX_LEVEL3,
+ INDEX_LEVEL5,
+ COUNT_OF_MODIFIER_INDEX
+ };
+ guint mModifierMasks[COUNT_OF_MODIFIER_INDEX];
+
+ guint GetGdkModifierMask(MappedModifier aModifier) const;
+
+ /**
+ * @param aGdkKeyval A GDK defined modifier key value such as
+ * GDK_Shift_L.
+ * @return Returns MappedModifier values for aGdkKeyval.
+ * If the given key code isn't a modifier key,
+ * returns NOT_MODIFIER.
+ */
+ static MappedModifier GetModifierForGDKKeyval(guint aGdkKeyval);
+
+ static const char* GetModifierName(MappedModifier aModifier);
+
+ /**
+ * AreModifiersActive() just checks whether aGdkModifierState indicates
+ * all modifiers in aModifiers are active or not.
+ *
+ * @param aModifiers One or more of MappedModifier values except
+ * NOT_MODIFIER.
+ * @param aGdkModifierState GDK's modifier states.
+ * @return TRUE if aGdkModifierType indicates all of
+ * modifiers in aModifier are active.
+ * Otherwise, FALSE.
+ */
+ static bool AreModifiersActive(MappedModifiers aModifiers,
+ guint aGdkModifierState);
+
+ /**
+ * mGdkKeymap is a wrapped instance by this class.
+ */
+ GdkKeymap* mGdkKeymap;
+
+ /**
+ * The base event code of XKB extension.
+ */
+ int mXKBBaseEventCode;
+
+#ifdef MOZ_X11
+ /**
+ * Only auto_repeats[] stores valid value. If you need to use other
+ * members, you need to listen notification events for them.
+ * See a call of XkbSelectEventDetails() with XkbControlsNotify in
+ * InitXKBExtension().
+ */
+ XKeyboardState mKeyboardState;
+#endif
+
+ /**
+ * Pointer of the singleton instance.
+ */
+ static KeymapWrapper* sInstance;
+
+ /**
+ * Auto key repeat management.
+ */
+ static guint sLastRepeatableHardwareKeyCode;
+#ifdef MOZ_X11
+ static Time sLastRepeatableKeyTime;
+#endif
+ enum RepeatState { NOT_PRESSED, FIRST_PRESS, REPEATING };
+ static RepeatState sRepeatState;
+
+ /**
+ * IsAutoRepeatableKey() returns true if the key supports auto repeat.
+ * Otherwise, false.
+ */
+ bool IsAutoRepeatableKey(guint aHardwareKeyCode);
+
+ /**
+ * Signal handlers.
+ */
+ static void OnKeysChanged(GdkKeymap* aKeymap, KeymapWrapper* aKeymapWrapper);
+ static void OnDirectionChanged(GdkKeymap* aGdkKeymap,
+ KeymapWrapper* aKeymapWrapper);
+
+ gulong mOnKeysChangedSignalHandle;
+ gulong mOnDirectionChangedSignalHandle;
+
+ /**
+ * GetCharCodeFor() Computes what character is inputted by the key event
+ * with aModifierState and aGroup.
+ *
+ * @param aGdkKeyEvent Native key event, must not be nullptr.
+ * @param aModifierState Combination of GdkModifierType which you
+ * want to test with aGdkKeyEvent.
+ * @param aGroup Set group in the mGdkKeymap.
+ * @return charCode which is inputted by aGdkKeyEvent.
+ * If failed, this returns 0.
+ */
+ static uint32_t GetCharCodeFor(const GdkEventKey* aGdkKeyEvent);
+ uint32_t GetCharCodeFor(const GdkEventKey* aGdkKeyEvent,
+ guint aGdkModifierState, gint aGroup);
+
+ /**
+ * GetUnmodifiedCharCodeFor() computes what character is inputted by the
+ * key event without Ctrl/Alt/Meta/Super/Hyper modifiers.
+ * If Level3 or Level5 Shift causes no character input, this also ignores
+ * them.
+ *
+ * @param aGdkKeyEvent Native key event, must not be nullptr.
+ * @return charCode which is computed without modifiers
+ * which prevent text input.
+ */
+ uint32_t GetUnmodifiedCharCodeFor(const GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * GetKeyLevel() returns level of the aGdkKeyEvent in mGdkKeymap.
+ *
+ * @param aGdkKeyEvent Native key event, must not be nullptr.
+ * @return Using level. Typically, this is 0 or 1.
+ * If failed, this returns -1.
+ */
+ gint GetKeyLevel(GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * GetFirstLatinGroup() returns group of mGdkKeymap which can input an
+ * ASCII character by GDK_A.
+ *
+ * @return group value of GdkEventKey.
+ */
+ gint GetFirstLatinGroup();
+
+ /**
+ * IsLatinGroup() checkes whether the keyboard layout of aGroup is
+ * ASCII alphabet inputtable or not.
+ *
+ * @param aGroup The group value of GdkEventKey.
+ * @return TRUE if the keyboard layout can input
+ * ASCII alphabet. Otherwise, FALSE.
+ */
+ bool IsLatinGroup(guint8 aGroup);
+
+ /**
+ * IsBasicLatinLetterOrNumeral() Checks whether the aCharCode is an
+ * alphabet or a numeric character in ASCII.
+ *
+ * @param aCharCode Charcode which you want to test.
+ * @return TRUE if aCharCode is an alphabet or a numeric
+ * in ASCII range. Otherwise, FALSE.
+ */
+ static bool IsBasicLatinLetterOrNumeral(uint32_t aCharCode);
+
+ /**
+ * IsPrintableASCIICharacter() checks whether the aCharCode is a printable
+ * ASCII character. I.e., returns false if aCharCode is a control
+ * character even in an ASCII character.
+ */
+ static bool IsPrintableASCIICharacter(uint32_t aCharCode) {
+ return aCharCode >= 0x20 && aCharCode <= 0x7E;
+ }
+
+ /**
+ * GetGDKKeyvalWithoutModifier() returns the keyval for aGdkKeyEvent when
+ * ignoring the modifier state except NumLock. (NumLock is a key to change
+ * some key's meaning.)
+ */
+ static guint GetGDKKeyvalWithoutModifier(const GdkEventKey* aGdkKeyEvent);
+
+ /**
+ * GetDOMKeyCodeFromKeyPairs() returns DOM keycode for aGdkKeyval if
+ * it's in KeyPair table.
+ */
+ static uint32_t GetDOMKeyCodeFromKeyPairs(guint aGdkKeyval);
+
+#ifdef MOZ_X11
+ /**
+ * FilterEvents() listens all events on all our windows.
+ * Be careful, this may make damage to performance if you add expensive
+ * code in this method.
+ */
+ static GdkFilterReturn FilterEvents(GdkXEvent* aXEvent, GdkEvent* aGdkEvent,
+ gpointer aData);
+#endif
+
+ /**
+ * MaybeDispatchContextMenuEvent() may dispatch eContextMenu event if
+ * the given key combination should cause opening context menu.
+ *
+ * @param aWindow The window to dispatch a contextmenu event.
+ * @param aEvent The native key event.
+ * @return true if this method dispatched eContextMenu
+ * event. Otherwise, false.
+ * Be aware, when this returns true, the
+ * widget may have been destroyed.
+ */
+ static bool MaybeDispatchContextMenuEvent(nsWindow* aWindow,
+ const GdkEventKey* aEvent);
+
+ /**
+ * See the document of WillDispatchKeyboardEvent().
+ */
+ void WillDispatchKeyboardEventInternal(WidgetKeyboardEvent& aKeyEvent,
+ GdkEventKey* aGdkKeyEvent);
+
+ static guint GetModifierState(GdkEventKey* aGdkKeyEvent,
+ KeymapWrapper* aWrapper);
+
+#ifdef MOZ_WAYLAND
+ /**
+ * Utility function to set Xkb modifier key mask.
+ */
+ void SetModifierMask(xkb_keymap* aKeymap, ModifierIndex aModifierIndex,
+ const char* aModifierName);
+#endif
+
+#ifdef MOZ_WAYLAND
+ static wl_seat* sSeat;
+ static int sSeatID;
+ static wl_keyboard* sKeyboard;
+ wl_surface* mFocusSurface = nullptr;
+ uint32_t mFocusSerial = 0;
+#endif
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __nsGdkKeyUtils_h__ */
diff --git a/widget/gtk/nsGtkUtils.h b/widget/gtk/nsGtkUtils.h
new file mode 100644
index 0000000000..ae26049e7f
--- /dev/null
+++ b/widget/gtk/nsGtkUtils.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef nsGtkUtils_h__
+#define nsGtkUtils_h__
+
+#include <glib.h>
+
+// Some gobject functions expect functions for gpointer arguments.
+// gpointer is void* but C++ doesn't like casting functions to void*.
+template <class T>
+static inline gpointer FuncToGpointer(T aFunction) {
+ return reinterpret_cast<gpointer>(
+ reinterpret_cast<uintptr_t>
+ // This cast just provides a warning if T is not a function.
+ (reinterpret_cast<void (*)()>(aFunction)));
+}
+
+// Type-safe alternative to glib's g_clear_pointer.
+//
+// Using `g_clear_pointer` itself causes UBSan to report undefined
+// behavior. The function-based definition of `g_clear_pointer` (as
+// opposed to the older preprocessor macro) treats the `destroy`
+// function as a `void (*)(void *)`, but the actual destroy functions
+// that are used (say `wl_buffer_destroy`) usually have more specific
+// pointer types.
+//
+// C++ draft n4901 [expr.call] para 6:
+//
+// Calling a function through an expression whose function type E
+// is different from the function type F of the called function’s
+// definition results in undefined behavior unless the type
+// “pointer to F” can be converted to the type “pointer to E” via
+// a function pointer conversion (7.3.14).
+//
+// §7.3.14 only talks about converting between noexcept and ordinary
+// function pointers.
+template <class T>
+static inline void MozClearPointer(T*& pointer, void (*destroy)(T*)) {
+ T* hold = pointer;
+ pointer = nullptr;
+ if (hold) {
+ destroy(hold);
+ }
+}
+
+template <class T>
+static inline void MozClearHandleID(T& handle, gboolean (*destroy)(T)) {
+ if (handle) {
+ destroy(handle);
+ handle = 0;
+ }
+}
+
+#endif // nsGtkUtils_h__
diff --git a/widget/gtk/nsImageToPixbuf.cpp b/widget/gtk/nsImageToPixbuf.cpp
new file mode 100644
index 0000000000..73094b74ef
--- /dev/null
+++ b/widget/gtk/nsImageToPixbuf.cpp
@@ -0,0 +1,121 @@
+/* vim:set sw=2 sts=2 et cin: */
+/* 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 <gdk-pixbuf/gdk-pixbuf.h>
+
+#include "nsImageToPixbuf.h"
+
+#include "imgIContainer.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/RefPtr.h"
+#include "GRefPtr.h"
+#include "nsCOMPtr.h"
+
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::SurfaceFormat;
+
+inline unsigned char unpremultiply(unsigned char color, unsigned char alpha) {
+ if (alpha == 0) return 0;
+ // plus alpha/2 to round instead of truncate
+ return (color * 255 + alpha / 2) / alpha;
+}
+
+already_AddRefed<GdkPixbuf> nsImageToPixbuf::ImageToPixbuf(
+ imgIContainer* aImage, const mozilla::Maybe<nsIntSize>& aOverrideSize) {
+ RefPtr<SourceSurface> surface;
+
+ const uint32_t flags =
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY;
+ if (aOverrideSize) {
+ surface = aImage->GetFrameAtSize(*aOverrideSize,
+ imgIContainer::FRAME_CURRENT, flags);
+ } else {
+ surface = aImage->GetFrame(imgIContainer::FRAME_CURRENT, flags);
+ }
+
+ // If the last call failed, it was probably because our call stack originates
+ // in an imgINotificationObserver event, meaning that we're not allowed
+ // request a sync decode. Presumably the originating event is something
+ // sensible like OnStopFrame(), so we can just retry the call without a sync
+ // decode.
+ if (!surface) {
+ if (aOverrideSize) {
+ surface =
+ aImage->GetFrameAtSize(*aOverrideSize, imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_NONE);
+ } else {
+ surface = aImage->GetFrame(imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_NONE);
+ }
+ }
+
+ NS_ENSURE_TRUE(surface, nullptr);
+
+ return SourceSurfaceToPixbuf(surface, surface->GetSize().width,
+ surface->GetSize().height);
+}
+
+already_AddRefed<GdkPixbuf> nsImageToPixbuf::SourceSurfaceToPixbuf(
+ SourceSurface* aSurface, int32_t aWidth, int32_t aHeight) {
+ MOZ_ASSERT(aWidth <= aSurface->GetSize().width &&
+ aHeight <= aSurface->GetSize().height,
+ "Requested rect is bigger than the supplied surface");
+
+ RefPtr<GdkPixbuf> pixbuf =
+ dont_AddRef(gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, aWidth, aHeight));
+ if (!pixbuf) {
+ return nullptr;
+ }
+
+ uint32_t destStride = gdk_pixbuf_get_rowstride(pixbuf);
+ guchar* destPixels = gdk_pixbuf_get_pixels(pixbuf);
+
+ RefPtr<DataSourceSurface> dataSurface = aSurface->GetDataSurface();
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) {
+ return nullptr;
+ }
+
+ uint8_t* srcData = map.mData;
+ int32_t srcStride = map.mStride;
+
+ SurfaceFormat format = dataSurface->GetFormat();
+
+ for (int32_t row = 0; row < aHeight; ++row) {
+ for (int32_t col = 0; col < aWidth; ++col) {
+ guchar* destPixel = destPixels + row * destStride + 4 * col;
+
+ uint32_t* srcPixel =
+ reinterpret_cast<uint32_t*>((srcData + row * srcStride + 4 * col));
+
+ if (format == SurfaceFormat::B8G8R8A8) {
+ const uint8_t a = (*srcPixel >> 24) & 0xFF;
+ const uint8_t r = unpremultiply((*srcPixel >> 16) & 0xFF, a);
+ const uint8_t g = unpremultiply((*srcPixel >> 8) & 0xFF, a);
+ const uint8_t b = unpremultiply((*srcPixel >> 0) & 0xFF, a);
+
+ *destPixel++ = r;
+ *destPixel++ = g;
+ *destPixel++ = b;
+ *destPixel++ = a;
+ } else {
+ MOZ_ASSERT(format == SurfaceFormat::B8G8R8X8);
+
+ const uint8_t r = (*srcPixel >> 16) & 0xFF;
+ const uint8_t g = (*srcPixel >> 8) & 0xFF;
+ const uint8_t b = (*srcPixel >> 0) & 0xFF;
+
+ *destPixel++ = r;
+ *destPixel++ = g;
+ *destPixel++ = b;
+ *destPixel++ = 0xFF; // A
+ }
+ }
+ }
+
+ dataSurface->Unmap();
+
+ return pixbuf.forget();
+}
diff --git a/widget/gtk/nsImageToPixbuf.h b/widget/gtk/nsImageToPixbuf.h
new file mode 100644
index 0000000000..500489d7eb
--- /dev/null
+++ b/widget/gtk/nsImageToPixbuf.h
@@ -0,0 +1,37 @@
+/* vim:set sw=2 sts=2 et cin: */
+/* 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/. */
+
+#ifndef NSIMAGETOPIXBUF_H_
+#define NSIMAGETOPIXBUF_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "nsSize.h"
+
+class imgIContainer;
+typedef struct _GdkPixbuf GdkPixbuf;
+
+namespace mozilla::gfx {
+class SourceSurface;
+} // namespace mozilla::gfx
+
+class nsImageToPixbuf final {
+ using SourceSurface = mozilla::gfx::SourceSurface;
+
+ public:
+ // Friendlier version of ConvertImageToPixbuf for callers inside of
+ // widget
+ static already_AddRefed<GdkPixbuf> ImageToPixbuf(
+ imgIContainer* aImage,
+ const mozilla::Maybe<nsIntSize>& aOverrideSize = mozilla::Nothing());
+ static already_AddRefed<GdkPixbuf> SourceSurfaceToPixbuf(
+ SourceSurface* aSurface, int32_t aWidth, int32_t aHeight);
+
+ private:
+ ~nsImageToPixbuf() = default;
+};
+
+#endif
diff --git a/widget/gtk/nsLookAndFeel.cpp b/widget/gtk/nsLookAndFeel.cpp
new file mode 100644
index 0000000000..c4b430d2eb
--- /dev/null
+++ b/widget/gtk/nsLookAndFeel.cpp
@@ -0,0 +1,2329 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+// for strtod()
+#include <stdlib.h>
+#include <dlfcn.h>
+
+#include "nsLookAndFeel.h"
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+
+#include <pango/pango.h>
+#include <pango/pango-fontmap.h>
+#include <fontconfig/fontconfig.h>
+
+#include "GRefPtr.h"
+#include "GUniquePtr.h"
+#include "nsGtkUtils.h"
+#include "gfxPlatformGtk.h"
+#include "mozilla/FontPropertyTypes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/WidgetUtilsGtk.h"
+#include "ScreenHelperGTK.h"
+#include "ScrollbarDrawing.h"
+
+#include "gtkdrawing.h"
+#include "nsString.h"
+#include "nsStyleConsts.h"
+#include "gfxFontConstants.h"
+#include "WidgetUtils.h"
+#include "nsWindow.h"
+
+#include "mozilla/gfx/2D.h"
+
+#include <cairo-gobject.h>
+#include <dlfcn.h>
+#include "WidgetStyleCache.h"
+#include "prenv.h"
+#include "nsCSSColorUtils.h"
+#include "mozilla/Preferences.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#ifdef MOZ_LOGGING
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+static LazyLogModule gLnfLog("LookAndFeel");
+# define LOGLNF(...) MOZ_LOG(gLnfLog, LogLevel::Debug, (__VA_ARGS__))
+# define LOGLNF_ENABLED() MOZ_LOG_TEST(gLnfLog, LogLevel::Debug)
+#else
+# define LOGLNF(args)
+# define LOGLNF_ENABLED() false
+#endif /* MOZ_LOGGING */
+
+#define GDK_COLOR_TO_NS_RGB(c) \
+ ((nscolor)NS_RGB(c.red >> 8, c.green >> 8, c.blue >> 8))
+#define GDK_RGBA_TO_NS_RGBA(c) \
+ ((nscolor)NS_RGBA((int)((c).red * 255), (int)((c).green * 255), \
+ (int)((c).blue * 255), (int)((c).alpha * 255)))
+
+static bool sIgnoreChangedSettings = false;
+
+static void OnSettingsChange() {
+ if (sIgnoreChangedSettings) {
+ return;
+ }
+ // TODO: We could be more granular here, but for now assume everything
+ // changed.
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
+ widget::IMContextWrapper::OnThemeChanged();
+}
+
+static void settings_changed_cb(GtkSettings*, GParamSpec*, void*) {
+ OnSettingsChange();
+}
+
+static bool sCSDAvailable;
+
+static nsCString GVariantToString(GVariant* aVariant) {
+ nsCString ret;
+ gchar* s = g_variant_print(aVariant, TRUE);
+ if (s) {
+ ret.Assign(s);
+ g_free(s);
+ }
+ return ret;
+}
+
+static nsDependentCString GVariantGetString(GVariant* aVariant) {
+ gsize len = 0;
+ const gchar* v = g_variant_get_string(aVariant, &len);
+ return nsDependentCString(v, len);
+}
+
+static void settings_changed_signal_cb(GDBusProxy* proxy, gchar* sender_name,
+ gchar* signal_name, GVariant* parameters,
+ gpointer user_data) {
+ LOGLNF("Settings Change sender=%s signal=%s params=%s\n", sender_name,
+ signal_name, GVariantToString(parameters).get());
+ if (strcmp(signal_name, "SettingChanged")) {
+ NS_WARNING("Unknown change signal for settings");
+ return;
+ }
+ RefPtr<GVariant> ns = dont_AddRef(g_variant_get_child_value(parameters, 0));
+ RefPtr<GVariant> key = dont_AddRef(g_variant_get_child_value(parameters, 1));
+ // Third parameter is the value, but we don't care about it.
+ if (!ns || !key || !g_variant_is_of_type(ns, G_VARIANT_TYPE_STRING) ||
+ !g_variant_is_of_type(key, G_VARIANT_TYPE_STRING)) {
+ MOZ_ASSERT(false, "Unexpected setting change signal parameters");
+ return;
+ }
+
+ auto* lnf = static_cast<nsLookAndFeel*>(user_data);
+
+ auto nsStr = GVariantGetString(ns);
+ auto keyStr = GVariantGetString(key);
+ if (nsStr.Equals("org.freedesktop.appearance"_ns) &&
+ keyStr.Equals("color-scheme"_ns)) {
+ lnf->OnColorSchemeSettingChanged();
+ }
+}
+
+void nsLookAndFeel::WatchDBus() {
+ GUniquePtr<GError> error;
+ mDBusSettingsProxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync(
+ G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
+ "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Settings", nullptr, getter_Transfers(error)));
+ if (mDBusSettingsProxy) {
+ g_signal_connect(mDBusSettingsProxy, "g-signal",
+ G_CALLBACK(settings_changed_signal_cb), this);
+ } else {
+ LOGLNF("Can't create DBus proxy for settings: %s\n", error->message);
+ return;
+ }
+
+ // DBus interface was started after L&F init so we need to load
+ // our settings from DBus explicitly.
+ if (!sIgnoreChangedSettings) {
+ OnColorSchemeSettingChanged();
+ }
+}
+
+void nsLookAndFeel::UnwatchDBus() {
+ if (mDBusSettingsProxy) {
+ g_signal_handlers_disconnect_by_func(
+ mDBusSettingsProxy, FuncToGpointer(settings_changed_signal_cb), this);
+ mDBusSettingsProxy = nullptr;
+ }
+}
+
+nsLookAndFeel::nsLookAndFeel() {
+ static constexpr nsLiteralCString kObservedSettings[] = {
+ // Affects system font sizes.
+ "notify::gtk-xft-dpi"_ns,
+ // Affects mSystemTheme and mAltTheme as expected.
+ "notify::gtk-theme-name"_ns,
+ // System fonts?
+ "notify::gtk-font-name"_ns,
+ // prefers-reduced-motion
+ "notify::gtk-enable-animations"_ns,
+ // CSD media queries, etc.
+ "notify::gtk-decoration-layout"_ns,
+ // Text resolution affects system font and widget sizes.
+ "notify::resolution"_ns,
+ // These three Affect mCaretBlinkTime
+ "notify::gtk-cursor-blink"_ns,
+ "notify::gtk-cursor-blink-time"_ns,
+ "notify::gtk-cursor-blink-timeout"_ns,
+ // Affects SelectTextfieldsOnKeyFocus
+ "notify::gtk-entry-select-on-focus"_ns,
+ // Affects ScrollToClick
+ "notify::gtk-primary-button-warps-slider"_ns,
+ // Affects SubmenuDelay
+ "notify::gtk-menu-popup-delay"_ns,
+ // Affects DragThresholdX/Y
+ "notify::gtk-dnd-drag-threshold"_ns,
+ // Affects titlebar actions loaded at moz_gtk_refresh().
+ "notify::gtk-titlebar-double-click"_ns,
+ "notify::gtk-titlebar-middle-click"_ns,
+ };
+
+ GtkSettings* settings = gtk_settings_get_default();
+ for (const auto& setting : kObservedSettings) {
+ g_signal_connect_after(settings, setting.get(),
+ G_CALLBACK(settings_changed_cb), nullptr);
+ }
+
+ sCSDAvailable =
+ nsWindow::GetSystemGtkWindowDecoration() != nsWindow::GTK_DECORATION_NONE;
+
+ if (ShouldUsePortal(PortalKind::Settings)) {
+ mDBusID = g_bus_watch_name(
+ G_BUS_TYPE_SESSION, "org.freedesktop.portal.Desktop",
+ G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+ [](GDBusConnection*, const gchar*, const gchar*,
+ gpointer data) -> void {
+ auto* lnf = static_cast<nsLookAndFeel*>(data);
+ lnf->WatchDBus();
+ },
+ [](GDBusConnection*, const gchar*, gpointer data) -> void {
+ auto* lnf = static_cast<nsLookAndFeel*>(data);
+ lnf->UnwatchDBus();
+ },
+ this, nullptr);
+ }
+ if (IsKdeDesktopEnvironment()) {
+ GUniquePtr<gchar> path(
+ g_strconcat(g_get_user_config_dir(), "/gtk-3.0/colors.css", NULL));
+ mKdeColors = dont_AddRef(g_file_new_for_path(path.get()));
+ mKdeColorsMonitor = dont_AddRef(
+ g_file_monitor_file(mKdeColors.get(), G_FILE_MONITOR_NONE, NULL, NULL));
+ if (mKdeColorsMonitor) {
+ g_signal_connect(mKdeColorsMonitor.get(), "changed",
+ G_CALLBACK(settings_changed_cb), NULL);
+ }
+ }
+}
+
+nsLookAndFeel::~nsLookAndFeel() {
+ ClearRoundedCornerProvider();
+ if (mDBusID) {
+ g_bus_unwatch_name(mDBusID);
+ mDBusID = 0;
+ }
+ UnwatchDBus();
+ g_signal_handlers_disconnect_by_func(
+ gtk_settings_get_default(), FuncToGpointer(settings_changed_cb), nullptr);
+}
+
+#if 0
+static void DumpStyleContext(GtkStyleContext* aStyle) {
+ static auto sGtkStyleContextToString =
+ reinterpret_cast<char* (*)(GtkStyleContext*, gint)>(
+ dlsym(RTLD_DEFAULT, "gtk_style_context_to_string"));
+ char* str = sGtkStyleContextToString(aStyle, ~0);
+ printf("%s\n", str);
+ g_free(str);
+ str = gtk_widget_path_to_string(gtk_style_context_get_path(aStyle));
+ printf("%s\n", str);
+ g_free(str);
+}
+#endif
+
+// Modifies color |*aDest| as if a pattern of color |aSource| was painted with
+// CAIRO_OPERATOR_OVER to a surface with color |*aDest|.
+static void ApplyColorOver(const GdkRGBA& aSource, GdkRGBA* aDest) {
+ gdouble sourceCoef = aSource.alpha;
+ gdouble destCoef = aDest->alpha * (1.0 - sourceCoef);
+ gdouble resultAlpha = sourceCoef + destCoef;
+ if (resultAlpha != 0.0) { // don't divide by zero
+ destCoef /= resultAlpha;
+ sourceCoef /= resultAlpha;
+ aDest->red = sourceCoef * aSource.red + destCoef * aDest->red;
+ aDest->green = sourceCoef * aSource.green + destCoef * aDest->green;
+ aDest->blue = sourceCoef * aSource.blue + destCoef * aDest->blue;
+ aDest->alpha = resultAlpha;
+ }
+}
+
+static void GetLightAndDarkness(const GdkRGBA& aColor, double* aLightness,
+ double* aDarkness) {
+ double sum = aColor.red + aColor.green + aColor.blue;
+ *aLightness = sum * aColor.alpha;
+ *aDarkness = (3.0 - sum) * aColor.alpha;
+}
+
+static bool GetGradientColors(const GValue* aValue, GdkRGBA* aLightColor,
+ GdkRGBA* aDarkColor) {
+ if (!G_TYPE_CHECK_VALUE_TYPE(aValue, CAIRO_GOBJECT_TYPE_PATTERN)) {
+ return false;
+ }
+
+ auto pattern = static_cast<cairo_pattern_t*>(g_value_get_boxed(aValue));
+ if (!pattern) {
+ return false;
+ }
+
+ // Just picking the lightest and darkest colors as simple samples rather
+ // than trying to blend, which could get messy if there are many stops.
+ if (CAIRO_STATUS_SUCCESS !=
+ cairo_pattern_get_color_stop_rgba(pattern, 0, nullptr, &aDarkColor->red,
+ &aDarkColor->green, &aDarkColor->blue,
+ &aDarkColor->alpha)) {
+ return false;
+ }
+
+ double maxLightness, maxDarkness;
+ GetLightAndDarkness(*aDarkColor, &maxLightness, &maxDarkness);
+ *aLightColor = *aDarkColor;
+
+ GdkRGBA stop;
+ for (int index = 1;
+ CAIRO_STATUS_SUCCESS ==
+ cairo_pattern_get_color_stop_rgba(pattern, index, nullptr, &stop.red,
+ &stop.green, &stop.blue, &stop.alpha);
+ ++index) {
+ double lightness, darkness;
+ GetLightAndDarkness(stop, &lightness, &darkness);
+ if (lightness > maxLightness) {
+ maxLightness = lightness;
+ *aLightColor = stop;
+ }
+ if (darkness > maxDarkness) {
+ maxDarkness = darkness;
+ *aDarkColor = stop;
+ }
+ }
+
+ return true;
+}
+
+static bool GetColorFromImagePattern(const GValue* aValue, nscolor* aColor) {
+ if (!G_TYPE_CHECK_VALUE_TYPE(aValue, CAIRO_GOBJECT_TYPE_PATTERN)) {
+ return false;
+ }
+
+ auto* pattern = static_cast<cairo_pattern_t*>(g_value_get_boxed(aValue));
+ if (!pattern) {
+ return false;
+ }
+
+ cairo_surface_t* surface;
+ if (cairo_pattern_get_surface(pattern, &surface) != CAIRO_STATUS_SUCCESS) {
+ return false;
+ }
+
+ cairo_format_t format = cairo_image_surface_get_format(surface);
+ if (format == CAIRO_FORMAT_INVALID) {
+ return false;
+ }
+ int width = cairo_image_surface_get_width(surface);
+ int height = cairo_image_surface_get_height(surface);
+ int stride = cairo_image_surface_get_stride(surface);
+ if (!width || !height) {
+ return false;
+ }
+
+ // Guesstimate the central pixel would have a sensible color.
+ int x = width / 2;
+ int y = height / 2;
+
+ unsigned char* data = cairo_image_surface_get_data(surface);
+ switch (format) {
+ // Most (all?) GTK images / patterns / etc use ARGB32.
+ case CAIRO_FORMAT_ARGB32: {
+ size_t offset = x * 4 + y * stride;
+ uint32_t* pixel = reinterpret_cast<uint32_t*>(data + offset);
+ *aColor = gfx::sRGBColor::UnusualFromARGB(*pixel).ToABGR();
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+static bool GetUnicoBorderGradientColors(GtkStyleContext* aContext,
+ GdkRGBA* aLightColor,
+ GdkRGBA* aDarkColor) {
+ // Ubuntu 12.04 has GTK engine Unico-1.0.2, which overrides render_frame,
+ // providing its own border code. Ubuntu 14.04 has
+ // Unico-1.0.3+14.04.20140109, which does not override render_frame, and
+ // so does not need special attention. The earlier Unico can be detected
+ // by the -unico-border-gradient style property it registers.
+ // gtk_style_properties_lookup_property() is checked first to avoid the
+ // warning from gtk_style_context_get_property() when the property does
+ // not exist. (gtk_render_frame() of GTK+ 3.16 no longer uses the
+ // engine.)
+ const char* propertyName = "-unico-border-gradient";
+ if (!gtk_style_properties_lookup_property(propertyName, nullptr, nullptr))
+ return false;
+
+ // -unico-border-gradient is used only when the CSS node's engine is Unico.
+ GtkThemingEngine* engine;
+ GtkStateFlags state = gtk_style_context_get_state(aContext);
+ gtk_style_context_get(aContext, state, "engine", &engine, nullptr);
+ if (strcmp(g_type_name(G_TYPE_FROM_INSTANCE(engine)), "UnicoEngine") != 0)
+ return false;
+
+ // draw_border() of Unico engine uses -unico-border-gradient
+ // in preference to border-color.
+ GValue value = G_VALUE_INIT;
+ gtk_style_context_get_property(aContext, propertyName, state, &value);
+
+ bool result = GetGradientColors(&value, aLightColor, aDarkColor);
+
+ g_value_unset(&value);
+ return result;
+}
+
+// Sets |aLightColor| and |aDarkColor| to colors from |aContext|. Returns
+// true if |aContext| uses these colors to render a visible border.
+// If returning false, then the colors returned are a fallback from the
+// border-color value even though |aContext| does not use these colors to
+// render a border.
+static bool GetBorderColors(GtkStyleContext* aContext, GdkRGBA* aLightColor,
+ GdkRGBA* aDarkColor) {
+ // Determine whether the border on this style context is visible.
+ GtkStateFlags state = gtk_style_context_get_state(aContext);
+ GtkBorderStyle borderStyle;
+ gtk_style_context_get(aContext, state, GTK_STYLE_PROPERTY_BORDER_STYLE,
+ &borderStyle, nullptr);
+ bool visible = borderStyle != GTK_BORDER_STYLE_NONE &&
+ borderStyle != GTK_BORDER_STYLE_HIDDEN;
+ if (visible) {
+ // GTK has an initial value of zero for border-widths, and so themes
+ // need to explicitly set border-widths to make borders visible.
+ GtkBorder border;
+ gtk_style_context_get_border(aContext, state, &border);
+ visible = border.top != 0 || border.right != 0 || border.bottom != 0 ||
+ border.left != 0;
+ }
+
+ if (visible &&
+ GetUnicoBorderGradientColors(aContext, aLightColor, aDarkColor))
+ return true;
+
+ // The initial value for the border-color is the foreground color, and so
+ // this will usually return a color distinct from the background even if
+ // there is no visible border detected.
+ gtk_style_context_get_border_color(aContext, state, aDarkColor);
+ // TODO GTK3 - update aLightColor
+ // for GTK_BORDER_STYLE_INSET/OUTSET/GROVE/RIDGE border styles.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=978172#c25
+ *aLightColor = *aDarkColor;
+ return visible;
+}
+
+static bool GetBorderColors(GtkStyleContext* aContext, nscolor* aLightColor,
+ nscolor* aDarkColor) {
+ GdkRGBA lightColor, darkColor;
+ bool ret = GetBorderColors(aContext, &lightColor, &darkColor);
+ *aLightColor = GDK_RGBA_TO_NS_RGBA(lightColor);
+ *aDarkColor = GDK_RGBA_TO_NS_RGBA(darkColor);
+ return ret;
+}
+
+// Finds ideal cell highlight colors used for unfocused+selected cells distinct
+// from both Highlight, used as focused+selected background, and the listbox
+// background which is assumed to be similar to -moz-field
+void nsLookAndFeel::PerThemeData::InitCellHighlightColors() {
+ int32_t minLuminosityDifference = NS_SUFFICIENT_LUMINOSITY_DIFFERENCE_BG;
+ int32_t backLuminosityDifference =
+ NS_LUMINOSITY_DIFFERENCE(mWindow.mBg, mField.mBg);
+ if (backLuminosityDifference >= minLuminosityDifference) {
+ mCellHighlight = mWindow;
+ return;
+ }
+
+ uint16_t hue, sat, luminance;
+ uint8_t alpha;
+ mCellHighlight = mField;
+
+ NS_RGB2HSV(mCellHighlight.mBg, hue, sat, luminance, alpha);
+
+ uint16_t step = 30;
+ // Lighten the color if the color is very dark
+ if (luminance <= step) {
+ luminance += step;
+ }
+ // Darken it if it is very light
+ else if (luminance >= 255 - step) {
+ luminance -= step;
+ }
+ // Otherwise, compute what works best depending on the text luminance.
+ else {
+ uint16_t textHue, textSat, textLuminance;
+ uint8_t textAlpha;
+ NS_RGB2HSV(mCellHighlight.mFg, textHue, textSat, textLuminance, textAlpha);
+ // Text is darker than background, use a lighter shade
+ if (textLuminance < luminance) {
+ luminance += step;
+ }
+ // Otherwise, use a darker shade
+ else {
+ luminance -= step;
+ }
+ }
+ NS_HSV2RGB(mCellHighlight.mBg, hue, sat, luminance, alpha);
+}
+
+void nsLookAndFeel::NativeInit() { EnsureInit(); }
+
+void nsLookAndFeel::RefreshImpl() {
+ mInitialized = false;
+ moz_gtk_refresh();
+
+ nsXPLookAndFeel::RefreshImpl();
+}
+
+nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
+ nscolor& aColor) {
+ EnsureInit();
+
+ const auto& theme =
+ aScheme == ColorScheme::Light ? LightTheme() : DarkTheme();
+ return theme.GetColor(aID, aColor);
+}
+
+static bool ShouldUseColorForActiveDarkScrollbarThumb(nscolor aColor) {
+ auto IsDifferentEnough = [](int32_t aChannel, int32_t aOtherChannel) {
+ return std::abs(aChannel - aOtherChannel) > 10;
+ };
+ return IsDifferentEnough(NS_GET_R(aColor), NS_GET_G(aColor)) ||
+ IsDifferentEnough(NS_GET_R(aColor), NS_GET_B(aColor));
+}
+
+static bool ShouldUseThemedScrollbarColor(StyleSystemColor aID, nscolor aColor,
+ bool aIsDark) {
+ if (!aIsDark) {
+ return true;
+ }
+ if (StaticPrefs::widget_non_native_theme_scrollbar_dark_themed()) {
+ return true;
+ }
+ return aID == StyleSystemColor::ThemedScrollbarThumbActive &&
+ StaticPrefs::widget_non_native_theme_scrollbar_active_always_themed();
+}
+
+nsresult nsLookAndFeel::PerThemeData::GetColor(ColorID aID,
+ nscolor& aColor) const {
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ // These colors don't seem to be used for anything anymore in Mozilla
+ // The CSS2 colors below are used.
+ case ColorID::Appworkspace: // MDI background color
+ case ColorID::Background: // desktop background
+ case ColorID::Window:
+ case ColorID::Windowframe:
+ case ColorID::MozCombobox:
+ aColor = mWindow.mBg;
+ break;
+ case ColorID::Windowtext:
+ aColor = mWindow.mFg;
+ break;
+ case ColorID::MozDialog:
+ aColor = mDialog.mBg;
+ break;
+ case ColorID::MozDialogtext:
+ aColor = mDialog.mFg;
+ break;
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::Highlight: // preference selected item,
+ aColor = mSelectedText.mBg;
+ break;
+ case ColorID::Highlighttext:
+ if (NS_GET_A(mSelectedText.mBg) < 155) {
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ }
+ [[fallthrough]];
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ aColor = mSelectedText.mFg;
+ break;
+ case ColorID::Selecteditem:
+ aColor = mSelectedItem.mBg;
+ break;
+ case ColorID::Selecteditemtext:
+ aColor = mSelectedItem.mFg;
+ break;
+ case ColorID::Accentcolor:
+ aColor = mAccent.mBg;
+ break;
+ case ColorID::Accentcolortext:
+ aColor = mAccent.mFg;
+ break;
+ case ColorID::MozCellhighlight:
+ aColor = mCellHighlight.mBg;
+ break;
+ case ColorID::MozCellhighlighttext:
+ aColor = mCellHighlight.mFg;
+ break;
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::Scrollbar:
+ aColor = mThemedScrollbar;
+ break;
+ case ColorID::ThemedScrollbar:
+ aColor = mThemedScrollbar;
+ if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ case ColorID::ThemedScrollbarInactive:
+ aColor = mThemedScrollbarInactive;
+ if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ case ColorID::ThemedScrollbarThumb:
+ aColor = mThemedScrollbarThumb;
+ if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ case ColorID::ThemedScrollbarThumbHover:
+ aColor = mThemedScrollbarThumbHover;
+ if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ case ColorID::ThemedScrollbarThumbActive:
+ aColor = mThemedScrollbarThumbActive;
+ if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ case ColorID::ThemedScrollbarThumbInactive:
+ aColor = mThemedScrollbarThumbInactive;
+ if (!ShouldUseThemedScrollbarColor(aID, aColor, mIsDark)) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+
+ // css2 http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
+ case ColorID::Activeborder:
+ // active window border
+ aColor = mMozWindowActiveBorder;
+ break;
+ case ColorID::Inactiveborder:
+ // inactive window border
+ aColor = mMozWindowInactiveBorder;
+ break;
+ case ColorID::Graytext: // disabled text in windows, menus, etc.
+ aColor = mGrayText;
+ break;
+ case ColorID::Activecaption:
+ aColor = mTitlebar.mBg;
+ break;
+ case ColorID::Captiontext: // text in active window caption (titlebar)
+ aColor = mTitlebar.mFg;
+ break;
+ case ColorID::Inactivecaption:
+ // inactive window caption
+ aColor = mTitlebarInactive.mBg;
+ break;
+ case ColorID::Inactivecaptiontext:
+ aColor = mTitlebarInactive.mFg;
+ break;
+ case ColorID::Infobackground:
+ aColor = mInfo.mBg;
+ break;
+ case ColorID::Infotext:
+ aColor = mInfo.mFg;
+ break;
+ case ColorID::Menu:
+ aColor = mMenu.mBg;
+ break;
+ case ColorID::Menutext:
+ aColor = mMenu.mFg;
+ break;
+ case ColorID::MozHeaderbar:
+ aColor = mHeaderBar.mBg;
+ break;
+ case ColorID::MozHeaderbartext:
+ aColor = mHeaderBar.mFg;
+ break;
+ case ColorID::MozHeaderbarinactive:
+ aColor = mHeaderBarInactive.mBg;
+ break;
+ case ColorID::MozHeaderbarinactivetext:
+ aColor = mHeaderBarInactive.mFg;
+ break;
+ case ColorID::Threedface:
+ case ColorID::Buttonface:
+ case ColorID::MozButtondisabledface:
+ // 3-D face color
+ aColor = mWindow.mBg;
+ break;
+
+ case ColorID::Buttontext:
+ // text on push buttons
+ aColor = mButton.mFg;
+ break;
+
+ case ColorID::Buttonhighlight:
+ // 3-D highlighted edge color
+ case ColorID::Threedhighlight:
+ // 3-D highlighted outer edge color
+ aColor = mThreeDHighlight;
+ break;
+
+ case ColorID::Buttonshadow:
+ // 3-D shadow edge color
+ case ColorID::Threedshadow:
+ // 3-D shadow inner edge color
+ aColor = mThreeDShadow;
+ break;
+ case ColorID::Buttonborder:
+ aColor = mButtonBorder;
+ break;
+ case ColorID::Threedlightshadow:
+ case ColorID::MozDisabledfield:
+ aColor = mIsDark ? *GenericDarkColor(aID) : NS_RGB(0xE0, 0xE0, 0xE0);
+ break;
+ case ColorID::Threeddarkshadow:
+ aColor = mIsDark ? *GenericDarkColor(aID) : NS_RGB(0xDC, 0xDC, 0xDC);
+ break;
+
+ case ColorID::MozEventreerow:
+ case ColorID::Field:
+ aColor = mField.mBg;
+ break;
+ case ColorID::Fieldtext:
+ aColor = mField.mFg;
+ break;
+ case ColorID::MozSidebar:
+ aColor = mSidebar.mBg;
+ break;
+ case ColorID::MozSidebartext:
+ aColor = mSidebar.mFg;
+ break;
+ case ColorID::MozSidebarborder:
+ aColor = mSidebarBorder;
+ break;
+ case ColorID::MozButtonhoverface:
+ aColor = mButtonHover.mBg;
+ break;
+ case ColorID::MozButtonhovertext:
+ aColor = mButtonHover.mFg;
+ break;
+ case ColorID::MozButtonactiveface:
+ aColor = mButtonActive.mBg;
+ break;
+ case ColorID::MozButtonactivetext:
+ aColor = mButtonActive.mFg;
+ break;
+ case ColorID::MozMenuhover:
+ aColor = mMenuHover.mBg;
+ break;
+ case ColorID::MozMenuhovertext:
+ aColor = mMenuHover.mFg;
+ break;
+ case ColorID::MozMenuhoverdisabled:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::MozOddtreerow:
+ aColor = mOddCellBackground;
+ break;
+ case ColorID::MozNativehyperlinktext:
+ aColor = mNativeHyperLinkText;
+ break;
+ case ColorID::MozNativevisitedhyperlinktext:
+ aColor = mNativeVisitedHyperLinkText;
+ break;
+ case ColorID::MozComboboxtext:
+ aColor = mComboBoxText;
+ break;
+ case ColorID::MozColheader:
+ aColor = mMozColHeader.mBg;
+ break;
+ case ColorID::MozColheadertext:
+ aColor = mMozColHeader.mFg;
+ break;
+ case ColorID::MozColheaderhover:
+ aColor = mMozColHeaderHover.mBg;
+ break;
+ case ColorID::MozColheaderhovertext:
+ aColor = mMozColHeaderHover.mFg;
+ break;
+ case ColorID::MozColheaderactive:
+ aColor = mMozColHeaderActive.mBg;
+ break;
+ case ColorID::MozColheaderactivetext:
+ aColor = mMozColHeaderActive.mFg;
+ break;
+ case ColorID::SpellCheckerUnderline:
+ case ColorID::Mark:
+ case ColorID::Marktext:
+ aColor = GetStandinForNativeColor(
+ aID, mIsDark ? ColorScheme::Dark : ColorScheme::Light);
+ break;
+ default:
+ /* default color is BLACK */
+ aColor = 0;
+ res = NS_ERROR_FAILURE;
+ break;
+ }
+
+ return res;
+}
+
+static int32_t CheckWidgetStyle(GtkWidget* aWidget, const char* aStyle,
+ int32_t aResult) {
+ gboolean value = FALSE;
+ gtk_widget_style_get(aWidget, aStyle, &value, nullptr);
+ return value ? aResult : 0;
+}
+
+static int32_t ConvertGTKStepperStyleToMozillaScrollArrowStyle(
+ GtkWidget* aWidget) {
+ if (!aWidget) return mozilla::LookAndFeel::eScrollArrowStyle_Single;
+
+ return CheckWidgetStyle(aWidget, "has-backward-stepper",
+ mozilla::LookAndFeel::eScrollArrow_StartBackward) |
+ CheckWidgetStyle(aWidget, "has-forward-stepper",
+ mozilla::LookAndFeel::eScrollArrow_EndForward) |
+ CheckWidgetStyle(aWidget, "has-secondary-backward-stepper",
+ mozilla::LookAndFeel::eScrollArrow_EndBackward) |
+ CheckWidgetStyle(aWidget, "has-secondary-forward-stepper",
+ mozilla::LookAndFeel::eScrollArrow_StartForward);
+}
+
+nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ nsresult res = NS_OK;
+
+ // We use delayed initialization by EnsureInit() here
+ // to make sure mozilla::Preferences is available (Bug 115807).
+ // IntID::UseAccessibilityTheme is requested before user preferences
+ // are read, and so EnsureInit(), which depends on preference values,
+ // is deliberately delayed until required.
+ switch (aID) {
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ break;
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ aResult = 1;
+ break;
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 2;
+ break;
+ case IntID::CaretBlinkTime:
+ EnsureInit();
+ aResult = mCaretBlinkTime;
+ break;
+ case IntID::CaretBlinkCount:
+ EnsureInit();
+ aResult = mCaretBlinkCount;
+ break;
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+ case IntID::SelectTextfieldsOnKeyFocus: {
+ GtkSettings* settings;
+ gboolean select_on_focus;
+
+ settings = gtk_settings_get_default();
+ g_object_get(settings, "gtk-entry-select-on-focus", &select_on_focus,
+ nullptr);
+
+ if (select_on_focus)
+ aResult = 1;
+ else
+ aResult = 0;
+
+ } break;
+ case IntID::ScrollToClick: {
+ GtkSettings* settings;
+ gboolean warps_slider = FALSE;
+
+ settings = gtk_settings_get_default();
+ if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
+ "gtk-primary-button-warps-slider")) {
+ g_object_get(settings, "gtk-primary-button-warps-slider", &warps_slider,
+ nullptr);
+ }
+
+ if (warps_slider)
+ aResult = 1;
+ else
+ aResult = 0;
+ } break;
+ case IntID::SubmenuDelay: {
+ GtkSettings* settings;
+ gint delay;
+
+ settings = gtk_settings_get_default();
+ g_object_get(settings, "gtk-menu-popup-delay", &delay, nullptr);
+ aResult = (int32_t)delay;
+ break;
+ }
+ case IntID::TooltipDelay: {
+ aResult = 500;
+ break;
+ }
+ case IntID::MenusCanOverlapOSBar:
+ // we want XUL popups to be able to overlap the task bar.
+ aResult = 1;
+ break;
+ case IntID::SkipNavigatingDisabledMenuItem:
+ aResult = 1;
+ break;
+ case IntID::DragThresholdX:
+ case IntID::DragThresholdY: {
+ gint threshold = 0;
+ g_object_get(gtk_settings_get_default(), "gtk-dnd-drag-threshold",
+ &threshold, nullptr);
+
+ aResult = threshold;
+ } break;
+ case IntID::ScrollArrowStyle: {
+ GtkWidget* scrollbar = GetWidget(MOZ_GTK_SCROLLBAR_VERTICAL);
+ aResult = ConvertGTKStepperStyleToMozillaScrollArrowStyle(scrollbar);
+ break;
+ }
+ case IntID::TreeOpenDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeCloseDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeLazyScrollDelay:
+ aResult = 150;
+ break;
+ case IntID::TreeScrollDelay:
+ aResult = 100;
+ break;
+ case IntID::TreeScrollLinesMax:
+ aResult = 3;
+ break;
+ case IntID::AlertNotificationOrigin:
+ aResult = NS_ALERT_TOP;
+ break;
+ case IntID::IMERawInputUnderlineStyle:
+ case IntID::IMEConvertedTextUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
+ break;
+ case IntID::IMESelectedRawTextUnderlineStyle:
+ case IntID::IMESelectedConvertedTextUnderline:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::None);
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Wavy);
+ break;
+ case IntID::MenuBarDrag:
+ EnsureInit();
+ aResult = mSystemTheme.mMenuSupportsDrag;
+ break;
+ case IntID::ScrollbarButtonAutoRepeatBehavior:
+ aResult = 1;
+ break;
+ case IntID::SwipeAnimationEnabled:
+ aResult = 1;
+ break;
+ case IntID::ContextMenuOffsetVertical:
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 2;
+ break;
+ case IntID::GTKCSDAvailable:
+ aResult = sCSDAvailable;
+ break;
+ case IntID::GTKCSDMaximizeButton:
+ EnsureInit();
+ aResult = mCSDMaximizeButton;
+ break;
+ case IntID::GTKCSDMinimizeButton:
+ EnsureInit();
+ aResult = mCSDMinimizeButton;
+ break;
+ case IntID::GTKCSDCloseButton:
+ EnsureInit();
+ aResult = mCSDCloseButton;
+ break;
+ case IntID::GTKCSDReversedPlacement:
+ EnsureInit();
+ aResult = mCSDReversedPlacement;
+ break;
+ case IntID::PrefersReducedMotion: {
+ EnsureInit();
+ aResult = mPrefersReducedMotion;
+ break;
+ }
+ case IntID::SystemUsesDarkTheme: {
+ EnsureInit();
+ if (mColorSchemePreference) {
+ aResult = *mColorSchemePreference == ColorScheme::Dark;
+ } else {
+ aResult = mSystemTheme.mIsDark;
+ }
+ break;
+ }
+ case IntID::GTKCSDMaximizeButtonPosition:
+ aResult = mCSDMaximizeButtonPosition;
+ break;
+ case IntID::GTKCSDMinimizeButtonPosition:
+ aResult = mCSDMinimizeButtonPosition;
+ break;
+ case IntID::GTKCSDCloseButtonPosition:
+ aResult = mCSDCloseButtonPosition;
+ break;
+ case IntID::GTKThemeFamily: {
+ EnsureInit();
+ aResult = int32_t(EffectiveTheme().mFamily);
+ break;
+ }
+ case IntID::UseAccessibilityTheme:
+ // If high contrast is enabled, enable prefers-reduced-transparency media
+ // query as well as there is no dedicated option.
+ case IntID::PrefersReducedTransparency:
+ EnsureInit();
+ aResult = mSystemTheme.mHighContrast;
+ break;
+ case IntID::InvertedColors:
+ // No GTK API for checking if inverted colors is enabled
+ aResult = 0;
+ break;
+ case IntID::TitlebarRadius: {
+ EnsureInit();
+ aResult = EffectiveTheme().mTitlebarRadius;
+ break;
+ }
+ case IntID::AllowOverlayScrollbarsOverlap: {
+ aResult = 1;
+ break;
+ }
+ case IntID::ScrollbarFadeBeginDelay: {
+ aResult = 1000;
+ break;
+ }
+ case IntID::ScrollbarFadeDuration: {
+ aResult = 400;
+ break;
+ }
+ case IntID::ScrollbarDisplayOnMouseMove: {
+ aResult = 1;
+ break;
+ }
+ case IntID::PanelAnimations:
+ aResult = [&]() -> bool {
+ if (!sCSDAvailable) {
+ // Disabled on systems without CSD, see bug 1385079.
+ return false;
+ }
+ if (GdkIsWaylandDisplay()) {
+ // Disabled on wayland, see bug 1800442 and bug 1800368.
+ return false;
+ }
+ return true;
+ }();
+ break;
+ case IntID::UseOverlayScrollbars: {
+ aResult = StaticPrefs::widget_gtk_overlay_scrollbars_enabled();
+ break;
+ }
+ case IntID::HideCursorWhileTyping: {
+ aResult = StaticPrefs::widget_gtk_hide_pointer_while_typing_enabled();
+ break;
+ }
+ case IntID::TouchDeviceSupportPresent:
+ aResult = widget::WidgetUtilsGTK::IsTouchDeviceSupportPresent();
+ break;
+ default:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ }
+
+ return res;
+}
+
+nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ nsresult rv = NS_OK;
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::CaretAspectRatio:
+ EnsureInit();
+ aResult = mSystemTheme.mCaretRatio;
+ break;
+ case FloatID::TextScaleFactor:
+ aResult = gfxPlatformGtk::GetFontScaleFactor();
+ break;
+ default:
+ aResult = -1.0;
+ rv = NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+
+static void GetSystemFontInfo(GtkStyleContext* aStyle, nsString* aFontName,
+ gfxFontStyle* aFontStyle) {
+ aFontStyle->style = FontSlantStyle::NORMAL;
+
+ // As in
+ // https://git.gnome.org/browse/gtk+/tree/gtk/gtkwidget.c?h=3.22.19#n10333
+ PangoFontDescription* desc;
+ gtk_style_context_get(aStyle, gtk_style_context_get_state(aStyle), "font",
+ &desc, nullptr);
+
+ aFontStyle->systemFont = true;
+
+ constexpr auto quote = u"\""_ns;
+ NS_ConvertUTF8toUTF16 family(pango_font_description_get_family(desc));
+ *aFontName = quote + family + quote;
+
+ aFontStyle->weight =
+ FontWeight::FromInt(pango_font_description_get_weight(desc));
+
+ // FIXME: Set aFontStyle->stretch correctly!
+ aFontStyle->stretch = FontStretch::NORMAL;
+
+ float size = float(pango_font_description_get_size(desc)) / PANGO_SCALE;
+
+ // |size| is now either pixels or pango-points (not Mozilla-points!)
+
+ if (!pango_font_description_get_size_is_absolute(desc)) {
+ // |size| is in pango-points, so convert to pixels.
+ size *= float(gfxPlatformGtk::GetFontScaleDPI()) / POINTS_PER_INCH_FLOAT;
+ }
+
+ // |size| is now pixels but not scaled for the hidpi displays,
+ aFontStyle->size = size;
+
+ pango_font_description_free(desc);
+}
+
+bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ return mSystemTheme.GetFont(aID, aFontName, aFontStyle);
+}
+
+bool nsLookAndFeel::PerThemeData::GetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) const {
+ switch (aID) {
+ case FontID::Menu: // css2
+ case FontID::MozPullDownMenu: // css3
+ aFontName = mMenuFontName;
+ aFontStyle = mMenuFontStyle;
+ break;
+
+ case FontID::MozField: // css3
+ case FontID::MozList: // css3
+ aFontName = mFieldFontName;
+ aFontStyle = mFieldFontStyle;
+ break;
+
+ case FontID::MozButton: // css3
+ aFontName = mButtonFontName;
+ aFontStyle = mButtonFontStyle;
+ break;
+
+ case FontID::Caption: // css2
+ case FontID::Icon: // css2
+ case FontID::MessageBox: // css2
+ case FontID::SmallCaption: // css2
+ case FontID::StatusBar: // css2
+ default:
+ aFontName = mDefaultFontName;
+ aFontStyle = mDefaultFontStyle;
+ break;
+ }
+
+ // Convert GDK pixels to CSS pixels.
+ // When "layout.css.devPixelsPerPx" > 0, this is not a direct conversion.
+ // The difference produces a scaling of system fonts in proportion with
+ // other scaling from the change in CSS pixel sizes.
+ aFontStyle.size /= LookAndFeel::GetTextScaleFactor();
+ return true;
+}
+
+static nsCString GetGtkSettingsStringKey(const char* aKey) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ nsCString ret;
+ GtkSettings* settings = gtk_settings_get_default();
+ char* value = nullptr;
+ g_object_get(settings, aKey, &value, nullptr);
+ if (value) {
+ ret.Assign(value);
+ g_free(value);
+ }
+ return ret;
+}
+
+static nsCString GetGtkTheme() {
+ return GetGtkSettingsStringKey("gtk-theme-name");
+}
+
+static bool GetPreferDarkTheme() {
+ GtkSettings* settings = gtk_settings_get_default();
+ gboolean preferDarkTheme = FALSE;
+ g_object_get(settings, "gtk-application-prefer-dark-theme", &preferDarkTheme,
+ nullptr);
+ return preferDarkTheme == TRUE;
+}
+
+// It seems GTK doesn't have an API to query if the current theme is "light" or
+// "dark", so we synthesize it from the CSS2 Window/WindowText colors instead,
+// by comparing their luminosity.
+static bool GetThemeIsDark() {
+ GdkRGBA bg, fg;
+ GtkStyleContext* style = GetStyleContext(MOZ_GTK_WINDOW);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &bg);
+ gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &fg);
+ return RelativeLuminanceUtils::Compute(GDK_RGBA_TO_NS_RGBA(bg)) <
+ RelativeLuminanceUtils::Compute(GDK_RGBA_TO_NS_RGBA(fg));
+}
+
+void nsLookAndFeel::RestoreSystemTheme() {
+ LOGLNF("RestoreSystemTheme(%s, %d, %d)\n", mSystemTheme.mName.get(),
+ mSystemTheme.mPreferDarkTheme, mSystemThemeOverridden);
+
+ if (!mSystemThemeOverridden) {
+ return;
+ }
+
+ // Available on Gtk 3.20+.
+ static auto sGtkSettingsResetProperty =
+ (void (*)(GtkSettings*, const gchar*))dlsym(
+ RTLD_DEFAULT, "gtk_settings_reset_property");
+
+ GtkSettings* settings = gtk_settings_get_default();
+ if (sGtkSettingsResetProperty) {
+ sGtkSettingsResetProperty(settings, "gtk-theme-name");
+ sGtkSettingsResetProperty(settings, "gtk-application-prefer-dark-theme");
+ } else {
+ g_object_set(settings, "gtk-theme-name", mSystemTheme.mName.get(),
+ "gtk-application-prefer-dark-theme",
+ mSystemTheme.mPreferDarkTheme, nullptr);
+ }
+ mSystemThemeOverridden = false;
+ UpdateRoundedBottomCornerStyles();
+ moz_gtk_refresh();
+}
+
+static bool AnyColorChannelIsDifferent(nscolor aColor) {
+ return NS_GET_R(aColor) != NS_GET_G(aColor) ||
+ NS_GET_R(aColor) != NS_GET_B(aColor);
+}
+
+bool nsLookAndFeel::ConfigureAltTheme() {
+ GtkSettings* settings = gtk_settings_get_default();
+ // Toggling gtk-application-prefer-dark-theme is not enough generally to
+ // switch from dark to light theme. If the theme didn't change, and we have
+ // a dark theme, try to first remove -Dark{,er,est} from the theme name to
+ // find the light variant.
+ if (mSystemTheme.mIsDark) {
+ nsCString potentialLightThemeName = mSystemTheme.mName;
+ // clang-format off
+ constexpr nsLiteralCString kSubstringsToRemove[] = {
+ "-darkest"_ns, "-darker"_ns, "-dark"_ns,
+ "-Darkest"_ns, "-Darker"_ns, "-Dark"_ns,
+ "_darkest"_ns, "_darker"_ns, "_dark"_ns,
+ "_Darkest"_ns, "_Darker"_ns, "_Dark"_ns,
+ };
+ // clang-format on
+ bool found = false;
+ for (const auto& s : kSubstringsToRemove) {
+ potentialLightThemeName = mSystemTheme.mName;
+ potentialLightThemeName.ReplaceSubstring(s, ""_ns);
+ if (potentialLightThemeName.Length() != mSystemTheme.mName.Length()) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ LOGLNF(" found potential light variant of %s: %s",
+ mSystemTheme.mName.get(), potentialLightThemeName.get());
+ g_object_set(settings, "gtk-theme-name", potentialLightThemeName.get(),
+ "gtk-application-prefer-dark-theme", !mSystemTheme.mIsDark,
+ nullptr);
+ moz_gtk_refresh();
+
+ if (!GetThemeIsDark()) {
+ return true; // Success!
+ }
+ }
+ }
+
+ LOGLNF(" toggling gtk-application-prefer-dark-theme");
+ g_object_set(settings, "gtk-application-prefer-dark-theme",
+ !mSystemTheme.mIsDark, nullptr);
+ moz_gtk_refresh();
+ if (mSystemTheme.mIsDark != GetThemeIsDark()) {
+ return true; // Success!
+ }
+
+ LOGLNF(" didn't work, falling back to default theme");
+ // If the theme still didn't change enough, fall back to Adwaita with the
+ // appropriate color preference.
+ g_object_set(settings, "gtk-theme-name", "Adwaita",
+ "gtk-application-prefer-dark-theme", !mSystemTheme.mIsDark,
+ nullptr);
+ moz_gtk_refresh();
+
+ // If it _still_ didn't change enough, and we're looking for a dark theme,
+ // try to set Adwaita-dark as a theme name. This might be needed in older GTK
+ // versions.
+ if (!mSystemTheme.mIsDark && !GetThemeIsDark()) {
+ LOGLNF(" last resort Adwaita-dark fallback");
+ g_object_set(settings, "gtk-theme-name", "Adwaita-dark", nullptr);
+ moz_gtk_refresh();
+ }
+
+ return false;
+}
+
+// We override some adwaita colors from GTK3 to LibAdwaita, see:
+// https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/named-colors.html
+void nsLookAndFeel::MaybeApplyAdwaitaOverrides() {
+ auto& dark = mSystemTheme.mIsDark ? mSystemTheme : mAltTheme;
+ auto& light = mSystemTheme.mIsDark ? mAltTheme : mSystemTheme;
+
+ // Unconditional special case for Adwaita-dark: In GTK3 we don't have more
+ // proper accent colors, so we use the selected background colors. Those
+ // colors, however, don't have much contrast in dark mode (see bug 1741293).
+ if (dark.mFamily == ThemeFamily::Adwaita) {
+ dark.mAccent = {NS_RGB(0x35, 0x84, 0xe4), NS_RGB(0xff, 0xff, 0xff)};
+ dark.mSelectedText = dark.mAccent;
+ }
+
+ if (light.mFamily == ThemeFamily::Adwaita) {
+ light.mAccent = {NS_RGB(0x35, 0x84, 0xe4), NS_RGB(0xff, 0xff, 0xff)};
+ light.mSelectedText = light.mAccent;
+ }
+
+ if (!StaticPrefs::widget_gtk_libadwaita_colors_enabled()) {
+ return;
+ }
+
+ if (light.mFamily == ThemeFamily::Adwaita) {
+ // #323232 is rgba(0,0,0,.8) over #fafafa.
+ light.mWindow =
+ light.mDialog = {NS_RGB(0xfa, 0xfa, 0xfa), NS_RGB(0x32, 0x32, 0x32)};
+ light.mField = {NS_RGB(0xff, 0xff, 0xff), NS_RGB(0x32, 0x32, 0x32)};
+
+ // We use the sidebar colors for the headerbar in light mode background
+ // because it creates much better contrast. GTK headerbar colors are white,
+ // and meant to "blend" with the contents otherwise.
+ // #2f2f2f is rgba(0,0,0,.8) over #ebebeb.
+ light.mSidebar = light.mHeaderBar =
+ light.mTitlebar = {NS_RGB(0xeb, 0xeb, 0xeb), NS_RGB(0x2f, 0x2f, 0x2f)};
+ light.mHeaderBarInactive = light.mTitlebarInactive = {
+ NS_RGB(0xf2, 0xf2, 0xf2), NS_RGB(0x2f, 0x2f, 0x2f)};
+ light.mThreeDShadow = NS_RGB(0xe0, 0xe0, 0xe0);
+ light.mSidebarBorder = NS_RGBA(0, 0, 0, 18);
+ }
+
+ if (dark.mFamily == ThemeFamily::Adwaita) {
+ dark.mWindow = {NS_RGB(0x24, 0x24, 0x24), NS_RGB(0xff, 0xff, 0xff)};
+ dark.mDialog = {NS_RGB(0x38, 0x38, 0x38), NS_RGB(0xff, 0xff, 0xff)};
+ dark.mField = {NS_RGB(0x3a, 0x3a, 0x3a), NS_RGB(0xff, 0xff, 0xff)};
+ dark.mSidebar = dark.mHeaderBar =
+ dark.mTitlebar = {NS_RGB(0x30, 0x30, 0x30), NS_RGB(0xff, 0xff, 0xff)};
+ dark.mHeaderBarInactive = dark.mTitlebarInactive = {
+ NS_RGB(0x24, 0x24, 0x24), NS_RGB(0xff, 0xff, 0xff)};
+ // headerbar_shade_color
+ dark.mThreeDShadow = NS_RGB(0x1f, 0x1f, 0x1f);
+ dark.mSidebarBorder = NS_RGBA(0, 0, 0, 92);
+ }
+}
+
+void nsLookAndFeel::ConfigureAndInitializeAltTheme() {
+ const bool fellBackToDefaultTheme = !ConfigureAltTheme();
+
+ mAltTheme.Init();
+
+ MaybeApplyAdwaitaOverrides();
+
+ // Some of the alt theme colors we can grab from the system theme, if we fell
+ // back to the default light / dark themes.
+ if (fellBackToDefaultTheme) {
+ if (StaticPrefs::widget_gtk_alt_theme_selection()) {
+ mAltTheme.mSelectedText = mSystemTheme.mSelectedText;
+ }
+
+ if (StaticPrefs::widget_gtk_alt_theme_scrollbar_active() &&
+ (!mAltTheme.mIsDark || ShouldUseColorForActiveDarkScrollbarThumb(
+ mSystemTheme.mThemedScrollbarThumbActive))) {
+ mAltTheme.mThemedScrollbarThumbActive =
+ mSystemTheme.mThemedScrollbarThumbActive;
+ }
+
+ if (StaticPrefs::widget_gtk_alt_theme_accent()) {
+ mAltTheme.mAccent = mSystemTheme.mAccent;
+ }
+ }
+
+ // Right now we're using the opposite color-scheme theme, make sure to record
+ // it.
+ mSystemThemeOverridden = true;
+ UpdateRoundedBottomCornerStyles();
+}
+
+void nsLookAndFeel::ClearRoundedCornerProvider() {
+ if (mRoundedCornerProvider) {
+ gtk_style_context_remove_provider_for_screen(
+ gdk_screen_get_default(),
+ GTK_STYLE_PROVIDER(mRoundedCornerProvider.get()));
+ mRoundedCornerProvider = nullptr;
+ }
+}
+
+void nsLookAndFeel::UpdateRoundedBottomCornerStyles() {
+ ClearRoundedCornerProvider();
+ if (!StaticPrefs::widget_gtk_rounded_bottom_corners_enabled()) {
+ return;
+ }
+ int32_t radius = EffectiveTheme().mTitlebarRadius;
+ if (!radius) {
+ return;
+ }
+ mRoundedCornerProvider = dont_AddRef(gtk_css_provider_new());
+ nsPrintfCString string(
+ "window.csd decoration {"
+ "border-bottom-right-radius: %dpx;"
+ "border-bottom-left-radius: %dpx;"
+ "}\n",
+ radius, radius);
+ GUniquePtr<GError> error;
+ if (!gtk_css_provider_load_from_data(mRoundedCornerProvider.get(),
+ string.get(), string.Length(),
+ getter_Transfers(error))) {
+ NS_WARNING(nsPrintfCString("Failed to load provider: %s - %s\n",
+ string.get(), error ? error->message : nullptr)
+ .get());
+ }
+ gtk_style_context_add_provider_for_screen(
+ gdk_screen_get_default(),
+ GTK_STYLE_PROVIDER(mRoundedCornerProvider.get()),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
+
+Maybe<ColorScheme> nsLookAndFeel::ComputeColorSchemeSetting() {
+ {
+ // Check the pref explicitly here. Usually this shouldn't be needed, but
+ // since we can only load one GTK theme at a time, and the pref will
+ // override the effective value that the rest of gecko assumes for the
+ // "system" color scheme, we need to factor it in our GTK theme decisions.
+ int32_t pref = 0;
+ if (NS_SUCCEEDED(Preferences::GetInt("ui.systemUsesDarkTheme", &pref))) {
+ return Some(pref ? ColorScheme::Dark : ColorScheme::Light);
+ }
+ }
+
+ if (!mDBusSettingsProxy) {
+ return Nothing();
+ }
+ GUniquePtr<GError> error;
+ RefPtr<GVariant> variant = dont_AddRef(g_dbus_proxy_call_sync(
+ mDBusSettingsProxy, "Read",
+ g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
+ G_DBUS_CALL_FLAGS_NONE,
+ StaticPrefs::widget_gtk_settings_portal_timeout_ms(), nullptr,
+ getter_Transfers(error)));
+ if (!variant) {
+ LOGLNF("color-scheme query error: %s\n", error->message);
+ return Nothing();
+ }
+ LOGLNF("color-scheme query result: %s\n", GVariantToString(variant).get());
+ variant = dont_AddRef(g_variant_get_child_value(variant, 0));
+ while (variant && g_variant_is_of_type(variant, G_VARIANT_TYPE_VARIANT)) {
+ // Unbox the return value.
+ variant = dont_AddRef(g_variant_get_variant(variant));
+ }
+ if (!variant || !g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32)) {
+ MOZ_ASSERT(false, "Unexpected color-scheme query return value");
+ return Nothing();
+ }
+ switch (g_variant_get_uint32(variant)) {
+ default:
+ MOZ_FALLTHROUGH_ASSERT("Unexpected color-scheme query return value");
+ case 0:
+ break;
+ case 1:
+ return Some(ColorScheme::Dark);
+ case 2:
+ return Some(ColorScheme::Light);
+ }
+ return Nothing();
+}
+
+void nsLookAndFeel::Initialize() {
+ LOGLNF("nsLookAndFeel::Initialize");
+ MOZ_DIAGNOSTIC_ASSERT(!mInitialized);
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(),
+ "LookAndFeel init should be done on the main thread");
+
+ mInitialized = true;
+
+ GtkSettings* settings = gtk_settings_get_default();
+ if (MOZ_UNLIKELY(!settings)) {
+ NS_WARNING("EnsureInit: No settings");
+ return;
+ }
+
+ AutoRestore<bool> restoreIgnoreSettings(sIgnoreChangedSettings);
+ sIgnoreChangedSettings = true;
+
+ // Our current theme may be different from the system theme if we're matching
+ // the Firefox theme or using the alt theme intentionally due to the
+ // color-scheme preference. Make sure to restore the original system theme.
+ RestoreSystemTheme();
+
+ // First initialize global settings.
+ InitializeGlobalSettings();
+
+ // Record our system theme settings now.
+ mSystemTheme.Init();
+
+ // Find the alternative-scheme theme (light if the system theme is dark, or
+ // vice versa), configure it and initialize it.
+ ConfigureAndInitializeAltTheme();
+
+ LOGLNF("System Theme: %s. Alt Theme: %s\n", mSystemTheme.mName.get(),
+ mAltTheme.mName.get());
+
+ // Go back to the system theme or keep the alt theme configured, depending on
+ // Firefox theme or user color-scheme preference.
+ ConfigureFinalEffectiveTheme();
+
+ RecordTelemetry();
+}
+
+void nsLookAndFeel::OnColorSchemeSettingChanged() {
+ if (NS_WARN_IF(mColorSchemePreference == ComputeColorSchemeSetting())) {
+ // We sometimes get duplicate color-scheme changes from dbus, avoid doing
+ // extra work if not needed.
+ return;
+ }
+ OnSettingsChange();
+}
+
+void nsLookAndFeel::InitializeGlobalSettings() {
+ GtkSettings* settings = gtk_settings_get_default();
+
+ mColorSchemePreference = ComputeColorSchemeSetting();
+
+ gboolean enableAnimations = false;
+ g_object_get(settings, "gtk-enable-animations", &enableAnimations, nullptr);
+ mPrefersReducedMotion = !enableAnimations;
+
+ gint blink_time = 0; // In milliseconds
+ gint blink_timeout = 0; // in seconds
+ gboolean blink;
+ g_object_get(settings, "gtk-cursor-blink-time", &blink_time,
+ "gtk-cursor-blink-timeout", &blink_timeout, "gtk-cursor-blink",
+ &blink, nullptr);
+ // From
+ // https://docs.gtk.org/gtk3/property.Settings.gtk-cursor-blink-timeout.html:
+ //
+ // Setting this to zero has the same effect as setting
+ // GtkSettings:gtk-cursor-blink to FALSE.
+ //
+ mCaretBlinkTime = blink && blink_timeout ? (int32_t)blink_time : 0;
+
+ if (mCaretBlinkTime) {
+ // blink_time * 2 because blink count is a full blink cycle.
+ mCaretBlinkCount =
+ std::max(1, int32_t(std::ceil(float(blink_timeout * 1000) /
+ (float(blink_time) * 2.0f))));
+ } else {
+ mCaretBlinkCount = -1;
+ }
+
+ mCSDCloseButton = false;
+ mCSDMinimizeButton = false;
+ mCSDMaximizeButton = false;
+ mCSDCloseButtonPosition = 0;
+ mCSDMinimizeButtonPosition = 0;
+ mCSDMaximizeButtonPosition = 0;
+
+ // We need to initialize whole CSD config explicitly because it's queried
+ // as -moz-gtk* media features.
+ ButtonLayout buttonLayout[TOOLBAR_BUTTONS];
+
+ size_t activeButtons =
+ GetGtkHeaderBarButtonLayout(Span(buttonLayout), &mCSDReversedPlacement);
+ for (size_t i = 0; i < activeButtons; i++) {
+ // We check if a button is represented on the right side of the tabbar.
+ // Then we assign it a value from 3 to 5, instead of 0 to 2 when it is on
+ // the left side.
+ const ButtonLayout& layout = buttonLayout[i];
+ int32_t* pos = nullptr;
+ switch (layout.mType) {
+ case MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE:
+ mCSDMinimizeButton = true;
+ pos = &mCSDMinimizeButtonPosition;
+ break;
+ case MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE:
+ mCSDMaximizeButton = true;
+ pos = &mCSDMaximizeButtonPosition;
+ break;
+ case MOZ_GTK_HEADER_BAR_BUTTON_CLOSE:
+ mCSDCloseButton = true;
+ pos = &mCSDCloseButtonPosition;
+ break;
+ default:
+ break;
+ }
+
+ if (pos) {
+ *pos = i;
+ }
+ }
+
+ struct actionMapping {
+ TitlebarAction action;
+ char name[100];
+ } ActionMapping[] = {
+ {TitlebarAction::None, "none"},
+ {TitlebarAction::WindowLower, "lower"},
+ {TitlebarAction::WindowMenu, "menu"},
+ {TitlebarAction::WindowMinimize, "minimize"},
+ {TitlebarAction::WindowMaximize, "maximize"},
+ {TitlebarAction::WindowMaximizeToggle, "toggle-maximize"},
+ };
+
+ auto GetWindowAction = [&](const char* eventName) -> TitlebarAction {
+ gchar* action = nullptr;
+ g_object_get(settings, eventName, &action, nullptr);
+ if (!action) {
+ return TitlebarAction::None;
+ }
+ auto free = mozilla::MakeScopeExit([&] { g_free(action); });
+ for (auto const& mapping : ActionMapping) {
+ if (!strncmp(action, mapping.name, strlen(mapping.name))) {
+ return mapping.action;
+ }
+ }
+ return TitlebarAction::None;
+ };
+
+ mDoubleClickAction = GetWindowAction("gtk-titlebar-double-click");
+ mMiddleClickAction = GetWindowAction("gtk-titlebar-middle-click");
+}
+
+void nsLookAndFeel::ConfigureFinalEffectiveTheme() {
+ MOZ_ASSERT(mSystemThemeOverridden,
+ "By this point, the alt theme should be configured");
+ const bool shouldUseSystemTheme = [&] {
+ using ChromeSetting = PreferenceSheet::ChromeColorSchemeSetting;
+ // NOTE: We can't call ColorSchemeForChrome directly because this might run
+ // while we're computing it.
+ switch (PreferenceSheet::ColorSchemeSettingForChrome()) {
+ case ChromeSetting::Light:
+ return !mSystemTheme.mIsDark;
+ case ChromeSetting::Dark:
+ return mSystemTheme.mIsDark;
+ case ChromeSetting::System:
+ break;
+ };
+ if (!mColorSchemePreference) {
+ return true;
+ }
+ const bool preferenceIsDark = *mColorSchemePreference == ColorScheme::Dark;
+ return preferenceIsDark == mSystemTheme.mIsDark;
+ }();
+
+ const bool usingSystem = !mSystemThemeOverridden;
+ LOGLNF("OverrideSystemThemeIfNeeded(matchesSystem=%d, usingSystem=%d)\n",
+ shouldUseSystemTheme, usingSystem);
+
+ if (shouldUseSystemTheme) {
+ RestoreSystemTheme();
+ } else if (usingSystem) {
+ LOGLNF("Setting theme %s, %d\n", mAltTheme.mName.get(),
+ mAltTheme.mPreferDarkTheme);
+
+ GtkSettings* settings = gtk_settings_get_default();
+ if (mSystemTheme.mName == mAltTheme.mName) {
+ // Prefer setting only gtk-application-prefer-dark-theme, so we can still
+ // get notified from notify::gtk-theme-name if the user changes the theme.
+ g_object_set(settings, "gtk-application-prefer-dark-theme",
+ mAltTheme.mPreferDarkTheme, nullptr);
+ } else {
+ g_object_set(settings, "gtk-theme-name", mAltTheme.mName.get(),
+ "gtk-application-prefer-dark-theme",
+ mAltTheme.mPreferDarkTheme, nullptr);
+ }
+ mSystemThemeOverridden = true;
+ UpdateRoundedBottomCornerStyles();
+ moz_gtk_refresh();
+ }
+}
+
+static bool GetColorFromBackgroundImage(GtkStyleContext* aStyle,
+ nscolor aForForegroundColor,
+ GtkStateFlags aState, nscolor* aColor) {
+ GValue value = G_VALUE_INIT;
+ gtk_style_context_get_property(aStyle, "background-image", aState, &value);
+ auto cleanup = MakeScopeExit([&] { g_value_unset(&value); });
+ if (GetColorFromImagePattern(&value, aColor)) {
+ return true;
+ }
+
+ {
+ GdkRGBA light, dark;
+ if (GetGradientColors(&value, &light, &dark)) {
+ nscolor l = GDK_RGBA_TO_NS_RGBA(light);
+ nscolor d = GDK_RGBA_TO_NS_RGBA(dark);
+ // Return the one with more contrast.
+ // TODO(emilio): This could do interpolation or what not but seems
+ // overkill.
+ if (NS_LUMINOSITY_DIFFERENCE(l, aForForegroundColor) >
+ NS_LUMINOSITY_DIFFERENCE(d, aForForegroundColor)) {
+ *aColor = l;
+ } else {
+ *aColor = d;
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static nscolor GetBackgroundColor(
+ GtkStyleContext* aStyle, nscolor aForForegroundColor,
+ GtkStateFlags aState = GTK_STATE_FLAG_NORMAL,
+ nscolor aOverBackgroundColor = NS_TRANSPARENT) {
+ // Try to synthesize a color from a background-image.
+ nscolor imageColor = NS_TRANSPARENT;
+ if (GetColorFromBackgroundImage(aStyle, aForForegroundColor, aState,
+ &imageColor)) {
+ if (NS_GET_A(imageColor) == 255) {
+ return imageColor;
+ }
+ }
+
+ GdkRGBA gdkColor;
+ gtk_style_context_get_background_color(aStyle, aState, &gdkColor);
+ nscolor bgColor = GDK_RGBA_TO_NS_RGBA(gdkColor);
+ // background-image paints over background-color.
+ const nscolor finalColor = NS_ComposeColors(bgColor, imageColor);
+ if (finalColor != aOverBackgroundColor) {
+ return finalColor;
+ }
+ return NS_TRANSPARENT;
+}
+
+static nscolor GetTextColor(GtkStyleContext* aStyle,
+ GtkStateFlags aState = GTK_STATE_FLAG_NORMAL) {
+ GdkRGBA color;
+ gtk_style_context_get_color(aStyle, aState, &color);
+ return GDK_RGBA_TO_NS_RGBA(color);
+}
+
+using ColorPair = nsLookAndFeel::ColorPair;
+static ColorPair GetColorPair(GtkStyleContext* aStyle,
+ GtkStateFlags aState = GTK_STATE_FLAG_NORMAL) {
+ ColorPair result;
+ result.mFg = GetTextColor(aStyle, aState);
+ result.mBg = GetBackgroundColor(aStyle, result.mFg, aState);
+ return result;
+}
+
+static bool GetNamedColorPair(GtkStyleContext* aStyle, const char* aBgName,
+ const char* aFgName, ColorPair* aPair) {
+ GdkRGBA bg, fg;
+ if (!gtk_style_context_lookup_color(aStyle, aBgName, &bg) ||
+ !gtk_style_context_lookup_color(aStyle, aFgName, &fg)) {
+ return false;
+ }
+
+ aPair->mBg = GDK_RGBA_TO_NS_RGBA(bg);
+ aPair->mFg = GDK_RGBA_TO_NS_RGBA(fg);
+
+ // If the colors are semi-transparent and the theme provides a
+ // background color, blend with them to get the "final" color, see
+ // bug 1717077.
+ if (NS_GET_A(aPair->mBg) != 255 &&
+ (gtk_style_context_lookup_color(aStyle, "bg_color", &bg) ||
+ gtk_style_context_lookup_color(aStyle, "theme_bg_color", &bg))) {
+ aPair->mBg = NS_ComposeColors(GDK_RGBA_TO_NS_RGBA(bg), aPair->mBg);
+ }
+
+ // A semi-transparent foreground color would be kinda silly, but is done
+ // for symmetry.
+ if (NS_GET_A(aPair->mFg) != 255) {
+ aPair->mFg = NS_ComposeColors(aPair->mBg, aPair->mFg);
+ }
+
+ return true;
+}
+
+static void EnsureColorPairIsOpaque(ColorPair& aPair) {
+ // Blend with white, ensuring the color is opaque, so that the UI doesn't have
+ // to care about alpha.
+ aPair.mBg = NS_ComposeColors(NS_RGB(0xff, 0xff, 0xff), aPair.mBg);
+ aPair.mFg = NS_ComposeColors(aPair.mBg, aPair.mFg);
+}
+
+static void PreferDarkerBackground(ColorPair& aPair) {
+ // We use the darker one unless the foreground isn't really a color (is all
+ // white / black / gray) and the background is, in which case we stick to what
+ // we have.
+ if (RelativeLuminanceUtils::Compute(aPair.mBg) >
+ RelativeLuminanceUtils::Compute(aPair.mFg) &&
+ (AnyColorChannelIsDifferent(aPair.mFg) ||
+ !AnyColorChannelIsDifferent(aPair.mBg))) {
+ std::swap(aPair.mBg, aPair.mFg);
+ }
+}
+
+void nsLookAndFeel::PerThemeData::Init() {
+ mName = GetGtkTheme();
+
+ mFamily = [&] {
+ if (mName.EqualsLiteral("Adwaita") || mName.EqualsLiteral("Adwaita-dark")) {
+ return ThemeFamily::Adwaita;
+ }
+ if (mName.EqualsLiteral("Breeze") || mName.EqualsLiteral("Breeze-Dark")) {
+ return ThemeFamily::Breeze;
+ }
+ if (StringBeginsWith(mName, "Yaru"_ns)) {
+ return ThemeFamily::Yaru;
+ }
+ return ThemeFamily::Unknown;
+ }();
+
+ GtkStyleContext* style;
+
+ mHighContrast = StaticPrefs::widget_content_gtk_high_contrast_enabled() &&
+ mName.Find("HighContrast"_ns) >= 0;
+
+ mPreferDarkTheme = GetPreferDarkTheme();
+
+ mIsDark = GetThemeIsDark();
+
+ GdkRGBA color;
+ // Some themes style the <trough>, while others style the <scrollbar>
+ // itself, so we look at both and compose the colors.
+ style = GetStyleContext(MOZ_GTK_SCROLLBAR_VERTICAL);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mThemedScrollbar = GDK_RGBA_TO_NS_RGBA(color);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_BACKDROP,
+ &color);
+ mThemedScrollbarInactive = GDK_RGBA_TO_NS_RGBA(color);
+
+ style = GetStyleContext(MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mThemedScrollbar =
+ NS_ComposeColors(mThemedScrollbar, GDK_RGBA_TO_NS_RGBA(color));
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_BACKDROP,
+ &color);
+ mThemedScrollbarInactive =
+ NS_ComposeColors(mThemedScrollbarInactive, GDK_RGBA_TO_NS_RGBA(color));
+
+ style = GetStyleContext(MOZ_GTK_SCROLLBAR_THUMB_VERTICAL);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mThemedScrollbarThumb = GDK_RGBA_TO_NS_RGBA(color);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_PRELIGHT,
+ &color);
+ mThemedScrollbarThumbHover = GDK_RGBA_TO_NS_RGBA(color);
+ gtk_style_context_get_background_color(
+ style, GtkStateFlags(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE),
+ &color);
+ mThemedScrollbarThumbActive = GDK_RGBA_TO_NS_RGBA(color);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_BACKDROP,
+ &color);
+ mThemedScrollbarThumbInactive = GDK_RGBA_TO_NS_RGBA(color);
+
+ // Make sure that the thumb is visible, at least.
+ const bool fallbackToUnthemedColors = [&] {
+ if (!StaticPrefs::widget_gtk_theme_scrollbar_colors_enabled()) {
+ return true;
+ }
+
+ if (!ShouldHonorThemeScrollbarColors()) {
+ return true;
+ }
+ // If any of the scrollbar thumb colors are fully transparent, fall back to
+ // non-native ones.
+ if (!NS_GET_A(mThemedScrollbarThumb) ||
+ !NS_GET_A(mThemedScrollbarThumbHover) ||
+ !NS_GET_A(mThemedScrollbarThumbActive)) {
+ return true;
+ }
+ // If the thumb and track are the same color and opaque, fall back to
+ // non-native colors as well.
+ if (mThemedScrollbar == mThemedScrollbarThumb &&
+ NS_GET_A(mThemedScrollbar) == 0xff) {
+ return true;
+ }
+ return false;
+ }();
+
+ if (fallbackToUnthemedColors) {
+ if (mIsDark) {
+ // Taken from Adwaita-dark.
+ mThemedScrollbar = NS_RGB(0x31, 0x31, 0x31);
+ mThemedScrollbarInactive = NS_RGB(0x2d, 0x2d, 0x2d);
+ mThemedScrollbarThumb = NS_RGB(0xa3, 0xa4, 0xa4);
+ mThemedScrollbarThumbInactive = NS_RGB(0x59, 0x5a, 0x5a);
+ } else {
+ // Taken from Adwaita.
+ mThemedScrollbar = NS_RGB(0xce, 0xce, 0xce);
+ mThemedScrollbarInactive = NS_RGB(0xec, 0xed, 0xef);
+ mThemedScrollbarThumb = NS_RGB(0x82, 0x81, 0x7e);
+ mThemedScrollbarThumbInactive = NS_RGB(0xce, 0xcf, 0xce);
+ }
+
+ mThemedScrollbarThumbHover = ThemeColors::AdjustUnthemedScrollbarThumbColor(
+ mThemedScrollbarThumb, dom::ElementState::HOVER);
+ mThemedScrollbarThumbActive =
+ ThemeColors::AdjustUnthemedScrollbarThumbColor(
+ mThemedScrollbarThumb, dom::ElementState::ACTIVE);
+ }
+
+ // The label is not added to a parent widget, but shared for constructing
+ // different style contexts. The node hierarchy is constructed only on
+ // the label style context.
+ GtkWidget* labelWidget = gtk_label_new("M");
+ g_object_ref_sink(labelWidget);
+
+ // Window colors
+ style = GetStyleContext(MOZ_GTK_WINDOW);
+ mWindow = mDialog = GetColorPair(style);
+
+ gtk_style_context_get_border_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mMozWindowActiveBorder = GDK_RGBA_TO_NS_RGBA(color);
+
+ gtk_style_context_get_border_color(style, GTK_STATE_FLAG_INSENSITIVE, &color);
+ mMozWindowInactiveBorder = GDK_RGBA_TO_NS_RGBA(color);
+
+ style = GetStyleContext(MOZ_GTK_WINDOW_CONTAINER);
+ {
+ GtkStyleContext* labelStyle = CreateStyleForWidget(labelWidget, style);
+ GetSystemFontInfo(labelStyle, &mDefaultFontName, &mDefaultFontStyle);
+ g_object_unref(labelStyle);
+ }
+
+ // tooltip foreground and background
+ style = GetStyleContext(MOZ_GTK_TOOLTIP_BOX_LABEL);
+ mInfo.mFg = GetTextColor(style);
+ style = GetStyleContext(MOZ_GTK_TOOLTIP);
+ mInfo.mBg = GetBackgroundColor(style, mInfo.mFg);
+
+ style = GetStyleContext(MOZ_GTK_MENUITEM);
+ {
+ GtkStyleContext* accelStyle =
+ CreateStyleForWidget(gtk_accel_label_new("M"), style);
+
+ GetSystemFontInfo(accelStyle, &mMenuFontName, &mMenuFontStyle);
+
+ gtk_style_context_get_color(accelStyle, GTK_STATE_FLAG_NORMAL, &color);
+ mMenu.mFg = GetTextColor(accelStyle);
+ mGrayText = GetTextColor(accelStyle, GTK_STATE_FLAG_INSENSITIVE);
+ g_object_unref(accelStyle);
+ }
+
+ const auto effectiveTitlebarStyle =
+ HeaderBarShouldDrawContainer(MOZ_GTK_HEADER_BAR) ? MOZ_GTK_HEADERBAR_FIXED
+ : MOZ_GTK_HEADER_BAR;
+ style = GetStyleContext(effectiveTitlebarStyle);
+ {
+ mTitlebar = GetColorPair(style, GTK_STATE_FLAG_NORMAL);
+ mTitlebarInactive = GetColorPair(style, GTK_STATE_FLAG_BACKDROP);
+ mTitlebarRadius = IsSolidCSDStyleUsed() ? 0 : GetBorderRadius(style);
+ }
+
+ // We special-case the header bar color in Adwaita, Yaru and Breeze to be the
+ // titlebar color, because it looks better and matches what apps do by
+ // default, see bug 1838460.
+ //
+ // We only do this in the relevant desktop environments, however, since in
+ // other cases we don't really know if the DE's titlebars are going to match.
+ //
+ // For breeze, additionally we read the KDE colors directly, if available,
+ // since these are user-configurable.
+ //
+ // For most other themes or those in unknown DEs, we use the menubar colors.
+ //
+ // FIXME(emilio): Can we do something a bit less special-case-y?
+ const bool shouldUseTitlebarColorsForHeaderBar = [&] {
+ if (mFamily == ThemeFamily::Adwaita || mFamily == ThemeFamily::Yaru) {
+ return IsGnomeDesktopEnvironment();
+ }
+ if (mFamily == ThemeFamily::Breeze) {
+ return IsKdeDesktopEnvironment();
+ }
+ return false;
+ }();
+
+ if (shouldUseTitlebarColorsForHeaderBar) {
+ mHeaderBar = mTitlebar;
+ mHeaderBarInactive = mTitlebarInactive;
+ if (mFamily == ThemeFamily::Breeze) {
+ GetNamedColorPair(style, "theme_header_background_breeze",
+ "theme_header_foreground_breeze", &mHeaderBar);
+ GetNamedColorPair(style, "theme_header_background_backdrop_breeze",
+ "theme_header_foreground_backdrop_breeze",
+ &mHeaderBarInactive);
+ }
+ } else {
+ style = GetStyleContext(MOZ_GTK_MENUBARITEM);
+ mHeaderBar.mFg = GetTextColor(style);
+ mHeaderBarInactive.mFg = GetTextColor(style, GTK_STATE_FLAG_BACKDROP);
+
+ style = GetStyleContext(MOZ_GTK_MENUBAR);
+ mHeaderBar.mBg = GetBackgroundColor(style, mHeaderBar.mFg);
+ mHeaderBarInactive.mBg = GetBackgroundColor(style, mHeaderBarInactive.mFg,
+ GTK_STATE_FLAG_BACKDROP);
+ }
+
+ style = GetStyleContext(MOZ_GTK_MENUPOPUP);
+ mMenu.mBg = [&] {
+ nscolor color = GetBackgroundColor(style, mMenu.mFg);
+ if (NS_GET_A(color)) {
+ return color;
+ }
+ // Some themes only style menupopups with the backdrop pseudo-class. Since a
+ // context / popup menu always seems to match that, try that before giving
+ // up.
+ color = GetBackgroundColor(style, mMenu.mFg, GTK_STATE_FLAG_BACKDROP);
+ if (NS_GET_A(color)) {
+ return color;
+ }
+ // If we get here we couldn't figure out the right color to use. Rather than
+ // falling back to transparent, fall back to the window background.
+ NS_WARNING(
+ "Couldn't find menu background color, falling back to window "
+ "background");
+ return mWindow.mBg;
+ }();
+
+ style = GetStyleContext(MOZ_GTK_MENUITEM);
+ gtk_style_context_get_color(style, GTK_STATE_FLAG_PRELIGHT, &color);
+ mMenuHover.mFg = GDK_RGBA_TO_NS_RGBA(color);
+ mMenuHover.mBg = NS_ComposeColors(
+ mMenu.mBg,
+ GetBackgroundColor(style, mMenu.mFg, GTK_STATE_FLAG_PRELIGHT, mMenu.mBg));
+
+ GtkWidget* parent = gtk_fixed_new();
+ GtkWidget* window = gtk_window_new(GTK_WINDOW_POPUP);
+ GtkWidget* treeView = gtk_tree_view_new();
+ GtkWidget* linkButton = gtk_link_button_new("http://example.com/");
+ GtkWidget* menuBar = gtk_menu_bar_new();
+ GtkWidget* menuBarItem = gtk_menu_item_new();
+ GtkWidget* entry = gtk_entry_new();
+ GtkWidget* textView = gtk_text_view_new();
+
+ gtk_container_add(GTK_CONTAINER(parent), treeView);
+ gtk_container_add(GTK_CONTAINER(parent), linkButton);
+ gtk_container_add(GTK_CONTAINER(parent), menuBar);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), menuBarItem);
+ gtk_container_add(GTK_CONTAINER(window), parent);
+ gtk_container_add(GTK_CONTAINER(parent), entry);
+ gtk_container_add(GTK_CONTAINER(parent), textView);
+
+ // Text colors
+ GdkRGBA bgColor;
+ // If the text window background is translucent, then the background of
+ // the textview root node is visible.
+ style = GetStyleContext(MOZ_GTK_TEXT_VIEW);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL,
+ &bgColor);
+
+ style = GetStyleContext(MOZ_GTK_TEXT_VIEW_TEXT);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ ApplyColorOver(color, &bgColor);
+ mField.mBg = GDK_RGBA_TO_NS_RGBA(bgColor);
+ gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mField.mFg = GDK_RGBA_TO_NS_RGBA(color);
+ mSidebar = mField;
+
+ // Selected text and background
+ {
+ GtkStyleContext* selectionStyle =
+ GetStyleContext(MOZ_GTK_TEXT_VIEW_TEXT_SELECTION);
+ auto GrabSelectionColors = [&](GtkStyleContext* style) {
+ gtk_style_context_get_background_color(
+ style,
+ static_cast<GtkStateFlags>(GTK_STATE_FLAG_FOCUSED |
+ GTK_STATE_FLAG_SELECTED),
+ &color);
+ mSelectedText.mBg = GDK_RGBA_TO_NS_RGBA(color);
+ gtk_style_context_get_color(
+ style,
+ static_cast<GtkStateFlags>(GTK_STATE_FLAG_FOCUSED |
+ GTK_STATE_FLAG_SELECTED),
+ &color);
+ mSelectedText.mFg = GDK_RGBA_TO_NS_RGBA(color);
+ };
+ GrabSelectionColors(selectionStyle);
+ if (mSelectedText.mBg == mSelectedText.mFg) {
+ // Some old distros/themes don't properly use the .selection style, so
+ // fall back to the regular text view style.
+ GrabSelectionColors(style);
+ }
+
+ // Default selected item color is the selection background / foreground
+ // colors, but we prefer named colors, as those are more general purpose
+ // than the actual selection style, which might e.g. be too-transparent.
+ //
+ // NOTE(emilio): It's unclear which one of the theme_selected_* or the
+ // selected_* pairs should we prefer, in all themes that define both that
+ // I've found, they're always the same.
+ if (!GetNamedColorPair(style, "selected_bg_color", "selected_fg_color",
+ &mSelectedItem) &&
+ !GetNamedColorPair(style, "theme_selected_bg_color",
+ "theme_selected_fg_color", &mSelectedItem)) {
+ mSelectedItem = mSelectedText;
+ }
+
+ EnsureColorPairIsOpaque(mSelectedItem);
+
+ // In a similar fashion, default accent color is the selected item/text
+ // pair, but we also prefer named colors, if available.
+ //
+ // accent_{bg,fg}_color is not _really_ a gtk3 thing (it's a gtk4 thing),
+ // but if gtk 3 themes want to specify these we let them, see:
+ //
+ // https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/named-colors.html#accent-colors
+ if (!GetNamedColorPair(style, "accent_bg_color", "accent_fg_color",
+ &mAccent)) {
+ mAccent = mSelectedItem;
+ }
+
+ EnsureColorPairIsOpaque(mAccent);
+ PreferDarkerBackground(mAccent);
+ }
+
+ // Button text color
+ style = GetStyleContext(MOZ_GTK_BUTTON);
+ {
+ GtkStyleContext* labelStyle = CreateStyleForWidget(labelWidget, style);
+ GetSystemFontInfo(labelStyle, &mButtonFontName, &mButtonFontStyle);
+ g_object_unref(labelStyle);
+ }
+
+ gtk_style_context_get_border_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mButtonBorder = GDK_RGBA_TO_NS_RGBA(color);
+ mButton = GetColorPair(style);
+ mButtonHover = GetColorPair(style, GTK_STATE_FLAG_PRELIGHT);
+ mButtonActive = GetColorPair(style, GTK_STATE_FLAG_ACTIVE);
+ if (!NS_GET_A(mButtonHover.mBg)) {
+ mButtonHover.mBg = mWindow.mBg;
+ }
+ if (!NS_GET_A(mButtonActive.mBg)) {
+ mButtonActive.mBg = mWindow.mBg;
+ }
+
+ // Combobox text color
+ style = GetStyleContext(MOZ_GTK_COMBOBOX_ENTRY_TEXTAREA);
+ gtk_style_context_get_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mComboBoxText = GDK_RGBA_TO_NS_RGBA(color);
+
+ // GTK's guide to fancy odd row background colors:
+ // 1) Check if a theme explicitly defines an odd row color
+ // 2) If not, check if it defines an even row color, and darken it
+ // slightly by a hardcoded value (gtkstyle.c)
+ // 3) If neither are defined, take the base background color and
+ // darken that by a hardcoded value
+ style = GetStyleContext(MOZ_GTK_TREEVIEW);
+
+ // Get odd row background color
+ gtk_style_context_save(style);
+ gtk_style_context_add_region(style, GTK_STYLE_REGION_ROW, GTK_REGION_ODD);
+ gtk_style_context_get_background_color(style, GTK_STATE_FLAG_NORMAL, &color);
+ mOddCellBackground = GDK_RGBA_TO_NS_RGBA(color);
+ gtk_style_context_restore(style);
+
+ // Column header colors
+ style = GetStyleContext(MOZ_GTK_TREE_HEADER_CELL);
+ mMozColHeader = GetColorPair(style, GTK_STATE_FLAG_NORMAL);
+ mMozColHeaderHover = GetColorPair(style, GTK_STATE_FLAG_NORMAL);
+ mMozColHeaderActive = GetColorPair(style, GTK_STATE_FLAG_ACTIVE);
+
+ // Compute cell highlight colors
+ InitCellHighlightColors();
+
+ // GtkFrame has a "border" subnode on which Adwaita draws the border.
+ // Some themes do not draw on this node but draw a border on the widget
+ // root node, so check the root node if no border is found on the border
+ // node.
+ style = GetStyleContext(MOZ_GTK_FRAME_BORDER);
+ bool themeUsesColors =
+ GetBorderColors(style, &mThreeDHighlight, &mThreeDShadow);
+ if (!themeUsesColors) {
+ style = GetStyleContext(MOZ_GTK_FRAME);
+ GetBorderColors(style, &mThreeDHighlight, &mThreeDShadow);
+ }
+ mSidebarBorder = mThreeDShadow;
+
+ // Some themes have a unified menu bar, and support window dragging on it
+ gboolean supports_menubar_drag = FALSE;
+ GParamSpec* param_spec = gtk_widget_class_find_style_property(
+ GTK_WIDGET_GET_CLASS(menuBar), "window-dragging");
+ if (param_spec) {
+ if (g_type_is_a(G_PARAM_SPEC_VALUE_TYPE(param_spec), G_TYPE_BOOLEAN)) {
+ gtk_widget_style_get(menuBar, "window-dragging", &supports_menubar_drag,
+ nullptr);
+ }
+ }
+ mMenuSupportsDrag = supports_menubar_drag;
+
+ // TODO: It returns wrong color for themes which
+ // sets link color for GtkLabel only as we query
+ // GtkLinkButton style here.
+ style = gtk_widget_get_style_context(linkButton);
+ gtk_style_context_get_color(style, GTK_STATE_FLAG_LINK, &color);
+ mNativeHyperLinkText = GDK_RGBA_TO_NS_RGBA(color);
+
+ gtk_style_context_get_color(style, GTK_STATE_FLAG_VISITED, &color);
+ mNativeVisitedHyperLinkText = GDK_RGBA_TO_NS_RGBA(color);
+
+ // invisible character styles
+ guint value;
+ g_object_get(entry, "invisible-char", &value, nullptr);
+ mInvisibleCharacter = char16_t(value);
+
+ // caret styles
+ gtk_widget_style_get(entry, "cursor-aspect-ratio", &mCaretRatio, nullptr);
+
+ GetSystemFontInfo(gtk_widget_get_style_context(entry), &mFieldFontName,
+ &mFieldFontStyle);
+
+ gtk_widget_destroy(window);
+ g_object_unref(labelWidget);
+
+ if (LOGLNF_ENABLED()) {
+ LOGLNF("Initialized theme %s (%d)\n", mName.get(), mPreferDarkTheme);
+ for (auto id : MakeEnumeratedRange(ColorID::End)) {
+ nscolor color;
+ nsresult rv = GetColor(id, color);
+ LOGLNF(" * color %d: pref=%s success=%d value=%x\n", int(id),
+ GetColorPrefName(id), NS_SUCCEEDED(rv),
+ NS_SUCCEEDED(rv) ? color : 0);
+ }
+ LOGLNF(" * titlebar-radius: %d\n", mTitlebarRadius);
+ }
+}
+
+// virtual
+char16_t nsLookAndFeel::GetPasswordCharacterImpl() {
+ EnsureInit();
+ return mSystemTheme.mInvisibleCharacter;
+}
+
+bool nsLookAndFeel::GetEchoPasswordImpl() { return false; }
+
+bool nsLookAndFeel::GetDefaultDrawInTitlebar() { return sCSDAvailable; }
+
+nsXPLookAndFeel::TitlebarAction nsLookAndFeel::GetTitlebarAction(
+ TitlebarEvent aEvent) {
+ return aEvent == TitlebarEvent::Double_Click ? mDoubleClickAction
+ : mMiddleClickAction;
+}
+
+void nsLookAndFeel::GetThemeInfo(nsACString& aInfo) {
+ aInfo.Append(mSystemTheme.mName);
+ aInfo.Append(" / ");
+ aInfo.Append(mAltTheme.mName);
+}
+
+bool nsLookAndFeel::WidgetUsesImage(WidgetNodeType aNodeType) {
+ static constexpr GtkStateFlags sFlagsToCheck[]{
+ GTK_STATE_FLAG_NORMAL, GTK_STATE_FLAG_PRELIGHT,
+ GtkStateFlags(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE),
+ GTK_STATE_FLAG_BACKDROP, GTK_STATE_FLAG_INSENSITIVE};
+
+ GtkStyleContext* style = GetStyleContext(aNodeType);
+
+ GValue value = G_VALUE_INIT;
+ for (GtkStateFlags state : sFlagsToCheck) {
+ gtk_style_context_get_property(style, "background-image", state, &value);
+ bool hasPattern = G_VALUE_TYPE(&value) == CAIRO_GOBJECT_TYPE_PATTERN &&
+ g_value_get_boxed(&value);
+ g_value_unset(&value);
+ if (hasPattern) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void nsLookAndFeel::RecordLookAndFeelSpecificTelemetry() {
+ // Gtk version we're on.
+ nsString version;
+ version.AppendPrintf("%d.%d", gtk_major_version, gtk_minor_version);
+ Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_GTK_VERSION, version);
+
+ // Whether the current Gtk theme has scrollbar buttons.
+ bool hasScrollbarButtons =
+ GetInt(LookAndFeel::IntID::ScrollArrowStyle) != eScrollArrow_None;
+ mozilla::Telemetry::ScalarSet(
+ mozilla::Telemetry::ScalarID::WIDGET_GTK_THEME_HAS_SCROLLBAR_BUTTONS,
+ hasScrollbarButtons);
+
+ // Whether the current Gtk theme uses something other than a solid color
+ // background for scrollbar parts.
+ bool scrollbarUsesImage = !ShouldHonorThemeScrollbarColors();
+ mozilla::Telemetry::ScalarSet(
+ mozilla::Telemetry::ScalarID::WIDGET_GTK_THEME_SCROLLBAR_USES_IMAGES,
+ scrollbarUsesImage);
+}
+
+bool nsLookAndFeel::ShouldHonorThemeScrollbarColors() {
+ // If the Gtk theme uses anything other than solid color backgrounds for Gtk
+ // scrollbar parts, this is a good indication that painting XUL scrollbar part
+ // elements using colors extracted from the theme won't provide good results.
+ return !WidgetUsesImage(MOZ_GTK_SCROLLBAR_VERTICAL) &&
+ !WidgetUsesImage(MOZ_GTK_SCROLLBAR_CONTENTS_VERTICAL) &&
+ !WidgetUsesImage(MOZ_GTK_SCROLLBAR_TROUGH_VERTICAL) &&
+ !WidgetUsesImage(MOZ_GTK_SCROLLBAR_THUMB_VERTICAL);
+}
+
+#undef LOGLNF
+#undef LOGLNF_ENABLED
diff --git a/widget/gtk/nsLookAndFeel.h b/widget/gtk/nsLookAndFeel.h
new file mode 100644
index 0000000000..56608d331f
--- /dev/null
+++ b/widget/gtk/nsLookAndFeel.h
@@ -0,0 +1,211 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsLookAndFeel
+#define __nsLookAndFeel
+
+#include "X11UndefineNone.h"
+#include "nsXPLookAndFeel.h"
+#include "nsCOMPtr.h"
+#include "gfxFont.h"
+
+enum WidgetNodeType : int;
+struct _GtkStyle;
+typedef struct _GDBusProxy GDBusProxy;
+typedef struct _GtkCssProvider GtkCssProvider;
+typedef struct _GFile GFile;
+typedef struct _GFileMonitor GFileMonitor;
+
+namespace mozilla {
+enum class StyleGtkThemeFamily : uint8_t;
+}
+
+class nsLookAndFeel final : public nsXPLookAndFeel {
+ public:
+ nsLookAndFeel();
+ virtual ~nsLookAndFeel();
+
+ void NativeInit() final;
+ void RefreshImpl() override;
+ nsresult NativeGetInt(IntID aID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID aID, float& aResult) override;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override;
+ bool NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) override;
+
+ char16_t GetPasswordCharacterImpl() override;
+ bool GetEchoPasswordImpl() override;
+
+ bool GetDefaultDrawInTitlebar() override;
+
+ nsXPLookAndFeel::TitlebarAction GetTitlebarAction(
+ TitlebarEvent aEvent) override;
+
+ void GetThemeInfo(nsACString&) override;
+
+ static const nscolor kBlack = NS_RGB(0, 0, 0);
+ static const nscolor kWhite = NS_RGB(255, 255, 255);
+ void OnColorSchemeSettingChanged();
+
+ struct ColorPair {
+ nscolor mBg = kWhite;
+ nscolor mFg = kBlack;
+ };
+
+ using ThemeFamily = mozilla::StyleGtkThemeFamily;
+
+ protected:
+ static bool WidgetUsesImage(WidgetNodeType aNodeType);
+ void RecordLookAndFeelSpecificTelemetry() override;
+ static bool ShouldHonorThemeScrollbarColors();
+ mozilla::Maybe<ColorScheme> ComputeColorSchemeSetting();
+
+ void WatchDBus();
+ void UnwatchDBus();
+
+ // We use up to two themes (one light, one dark), which might have different
+ // sets of fonts and colors.
+ struct PerThemeData {
+ nsCString mName;
+ bool mIsDark = false;
+ bool mHighContrast = false;
+ bool mPreferDarkTheme = false;
+
+ ThemeFamily mFamily{0};
+
+ // Cached fonts
+ nsString mDefaultFontName;
+ nsString mButtonFontName;
+ nsString mFieldFontName;
+ nsString mMenuFontName;
+ gfxFontStyle mDefaultFontStyle;
+ gfxFontStyle mButtonFontStyle;
+ gfxFontStyle mFieldFontStyle;
+ gfxFontStyle mMenuFontStyle;
+
+ // Cached colors
+ nscolor mGrayText = kBlack;
+ ColorPair mInfo;
+ ColorPair mMenu;
+ ColorPair mMenuHover;
+ ColorPair mHeaderBar;
+ ColorPair mHeaderBarInactive;
+ ColorPair mButton;
+ ColorPair mButtonHover;
+ ColorPair mButtonActive;
+ nscolor mButtonBorder = kBlack;
+ nscolor mThreeDHighlight = kBlack;
+ nscolor mThreeDShadow = kBlack;
+ nscolor mOddCellBackground = kWhite;
+ nscolor mNativeHyperLinkText = kBlack;
+ nscolor mNativeVisitedHyperLinkText = kBlack;
+ // FIXME: This doesn't seem like it'd be sound since we use Window for
+ // -moz-Combobox... But I guess we rely on chrome code not setting
+ // appearance: none on selects or overriding the color if they do.
+ nscolor mComboBoxText = kBlack;
+ ColorPair mField;
+ ColorPair mWindow;
+ ColorPair mDialog;
+ ColorPair mSidebar;
+ nscolor mSidebarBorder = kBlack;
+
+ nscolor mMozWindowActiveBorder = kBlack;
+ nscolor mMozWindowInactiveBorder = kBlack;
+
+ ColorPair mCellHighlight;
+ ColorPair mSelectedText;
+ ColorPair mAccent;
+ ColorPair mSelectedItem;
+
+ ColorPair mMozColHeader;
+ ColorPair mMozColHeaderHover;
+ ColorPair mMozColHeaderActive;
+
+ ColorPair mTitlebar;
+ ColorPair mTitlebarInactive;
+
+ nscolor mThemedScrollbar = kWhite;
+ nscolor mThemedScrollbarInactive = kWhite;
+ nscolor mThemedScrollbarThumb = kBlack;
+ nscolor mThemedScrollbarThumbHover = kBlack;
+ nscolor mThemedScrollbarThumbActive = kBlack;
+ nscolor mThemedScrollbarThumbInactive = kBlack;
+
+ float mCaretRatio = 0.0f;
+ int32_t mTitlebarRadius = 0;
+ char16_t mInvisibleCharacter = 0;
+ bool mMenuSupportsDrag = false;
+
+ void Init();
+ nsresult GetColor(ColorID, nscolor&) const;
+ bool GetFont(FontID, nsString& aFontName, gfxFontStyle&) const;
+ void InitCellHighlightColors();
+ };
+
+ PerThemeData mSystemTheme;
+
+ // If the system theme is light, a dark theme. Otherwise, a light theme. The
+ // alternative theme to the current one is preferred, but otherwise we fall
+ // back to Adwaita / Adwaita Dark, respectively.
+ PerThemeData mAltTheme;
+
+ const PerThemeData& LightTheme() const {
+ return mSystemTheme.mIsDark ? mAltTheme : mSystemTheme;
+ }
+
+ const PerThemeData& DarkTheme() const {
+ return mSystemTheme.mIsDark ? mSystemTheme : mAltTheme;
+ }
+
+ const PerThemeData& EffectiveTheme() const {
+ return mSystemThemeOverridden ? mAltTheme : mSystemTheme;
+ }
+
+ uint32_t mDBusID = 0;
+ RefPtr<GDBusProxy> mDBusSettingsProxy;
+ RefPtr<GFile> mKdeColors;
+ RefPtr<GFileMonitor> mKdeColorsMonitor;
+ mozilla::Maybe<ColorScheme> mColorSchemePreference;
+ int32_t mCaretBlinkTime = 0;
+ int32_t mCaretBlinkCount = -1;
+ bool mCSDMaximizeButton = false;
+ bool mCSDMinimizeButton = false;
+ bool mCSDCloseButton = false;
+ bool mCSDReversedPlacement = false;
+ bool mPrefersReducedMotion = false;
+ bool mInitialized = false;
+ bool mSystemThemeOverridden = false;
+ int32_t mCSDMaximizeButtonPosition = 0;
+ int32_t mCSDMinimizeButtonPosition = 0;
+ int32_t mCSDCloseButtonPosition = 0;
+ TitlebarAction mDoubleClickAction = TitlebarAction::None;
+ TitlebarAction mMiddleClickAction = TitlebarAction::None;
+
+ RefPtr<GtkCssProvider> mRoundedCornerProvider;
+ void UpdateRoundedBottomCornerStyles();
+
+ void ClearRoundedCornerProvider();
+
+ void EnsureInit() {
+ if (mInitialized) {
+ return;
+ }
+ Initialize();
+ }
+
+ void Initialize();
+
+ void RestoreSystemTheme();
+ void InitializeGlobalSettings();
+ // Returns whether we found an alternative theme.
+ bool ConfigureAltTheme();
+ void ConfigureAndInitializeAltTheme();
+ void ConfigureFinalEffectiveTheme();
+ void MaybeApplyAdwaitaOverrides();
+};
+
+#endif
diff --git a/widget/gtk/nsNativeThemeGTK.cpp b/widget/gtk/nsNativeThemeGTK.cpp
new file mode 100644
index 0000000000..06d9b48007
--- /dev/null
+++ b/widget/gtk/nsNativeThemeGTK.cpp
@@ -0,0 +1,1369 @@
+/* -*- 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 "nsNativeThemeGTK.h"
+#include "nsPresContext.h"
+#include "nsStyleConsts.h"
+#include "gtkdrawing.h"
+#include "ScreenHelperGTK.h"
+#include "WidgetUtilsGtk.h"
+
+#include "gfx2DGlue.h"
+#include "nsIObserverService.h"
+#include "nsIFrame.h"
+#include "nsIContent.h"
+#include "nsViewManager.h"
+#include "nsNameSpaceManager.h"
+#include "nsGfxCIID.h"
+#include "nsTransform2D.h"
+#include "nsXULPopupManager.h"
+#include "tree/nsTreeBodyFrame.h"
+#include "prlink.h"
+#include "nsGkAtoms.h"
+#include "nsAttrValueInlines.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Services.h"
+
+#include <gdk/gdkprivate.h>
+#include <gtk/gtk.h>
+
+#include "gfxContext.h"
+#include "mozilla/dom/XULButtonElement.h"
+#include "mozilla/gfx/BorrowedContext.h"
+#include "mozilla/gfx/HelpersCairo.h"
+#include "mozilla/gfx/PathHelpers.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsWindow.h"
+#include "nsLayoutUtils.h"
+#include "Theme.h"
+
+#ifdef MOZ_X11
+# ifdef CAIRO_HAS_XLIB_SURFACE
+# include "cairo-xlib.h"
+# endif
+#endif
+
+#include <algorithm>
+#include <dlfcn.h>
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+
+static int gLastGdkError;
+
+// Return widget scale factor of the monitor where the window is located by the
+// most part. We intentionally honor the text scale factor here in order to
+// have consistent scaling with other UI elements.
+static inline CSSToLayoutDeviceScale GetWidgetScaleFactor(nsIFrame* aFrame) {
+ return aFrame->PresContext()->CSSToDevPixelScale();
+}
+
+nsNativeThemeGTK::nsNativeThemeGTK() : Theme(ScrollbarStyle()) {
+ if (moz_gtk_init() != MOZ_GTK_SUCCESS) {
+ memset(mDisabledWidgetTypes, 0xff, sizeof(mDisabledWidgetTypes));
+ return;
+ }
+
+ ThemeChanged();
+}
+
+nsNativeThemeGTK::~nsNativeThemeGTK() { moz_gtk_shutdown(); }
+
+void nsNativeThemeGTK::RefreshWidgetWindow(nsIFrame* aFrame) {
+ MOZ_ASSERT(aFrame);
+ MOZ_ASSERT(aFrame->PresShell());
+
+ nsViewManager* vm = aFrame->PresShell()->GetViewManager();
+ if (!vm) {
+ return;
+ }
+ vm->InvalidateAllViews();
+}
+
+static bool IsFrameContentNodeInNamespace(nsIFrame* aFrame,
+ uint32_t aNamespace) {
+ nsIContent* content = aFrame ? aFrame->GetContent() : nullptr;
+ if (!content) return false;
+ return content->IsInNamespace(aNamespace);
+}
+
+static bool IsWidgetTypeDisabled(const uint8_t* aDisabledVector,
+ StyleAppearance aAppearance) {
+ auto type = static_cast<size_t>(aAppearance);
+ MOZ_ASSERT(type < static_cast<size_t>(StyleAppearance::Count));
+ return (aDisabledVector[type >> 3] & (1 << (type & 7))) != 0;
+}
+
+static void SetWidgetTypeDisabled(uint8_t* aDisabledVector,
+ StyleAppearance aAppearance) {
+ auto type = static_cast<size_t>(aAppearance);
+ MOZ_ASSERT(type < static_cast<size_t>(mozilla::StyleAppearance::Count));
+ aDisabledVector[type >> 3] |= (1 << (type & 7));
+}
+
+static inline uint16_t GetWidgetStateKey(StyleAppearance aAppearance,
+ GtkWidgetState* aWidgetState) {
+ return (aWidgetState->active | aWidgetState->focused << 1 |
+ aWidgetState->inHover << 2 | aWidgetState->disabled << 3 |
+ aWidgetState->isDefault << 4 |
+ static_cast<uint16_t>(aAppearance) << 5);
+}
+
+static bool IsWidgetStateSafe(uint8_t* aSafeVector, StyleAppearance aAppearance,
+ GtkWidgetState* aWidgetState) {
+ MOZ_ASSERT(static_cast<size_t>(aAppearance) <
+ static_cast<size_t>(mozilla::StyleAppearance::Count));
+ uint16_t key = GetWidgetStateKey(aAppearance, aWidgetState);
+ return (aSafeVector[key >> 3] & (1 << (key & 7))) != 0;
+}
+
+static void SetWidgetStateSafe(uint8_t* aSafeVector,
+ StyleAppearance aAppearance,
+ GtkWidgetState* aWidgetState) {
+ MOZ_ASSERT(static_cast<size_t>(aAppearance) <
+ static_cast<size_t>(mozilla::StyleAppearance::Count));
+ uint16_t key = GetWidgetStateKey(aAppearance, aWidgetState);
+ aSafeVector[key >> 3] |= (1 << (key & 7));
+}
+
+/* static */
+GtkTextDirection nsNativeThemeGTK::GetTextDirection(nsIFrame* aFrame) {
+ // IsFrameRTL() treats vertical-rl modes as right-to-left (in addition to
+ // horizontal text with direction=RTL), rather than just considering the
+ // text direction. GtkTextDirection does not have distinct values for
+ // vertical writing modes, but considering the block flow direction is
+ // important for resizers and scrollbar elements, at least.
+ return IsFrameRTL(aFrame) ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR;
+}
+
+// Returns positive for negative margins (otherwise 0).
+gint nsNativeThemeGTK::GetTabMarginPixels(nsIFrame* aFrame) {
+ nscoord margin = IsBottomTab(aFrame) ? aFrame->GetUsedMargin().top
+ : aFrame->GetUsedMargin().bottom;
+
+ return std::min<gint>(
+ MOZ_GTK_TAB_MARGIN_MASK,
+ std::max(0, aFrame->PresContext()->AppUnitsToDevPixels(-margin)));
+}
+
+bool nsNativeThemeGTK::GetGtkWidgetAndState(StyleAppearance aAppearance,
+ nsIFrame* aFrame,
+ WidgetNodeType& aGtkWidgetType,
+ GtkWidgetState* aState,
+ gint* aWidgetFlags) {
+ if (aWidgetFlags) {
+ *aWidgetFlags = 0;
+ }
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ if (aState) {
+ memset(aState, 0, sizeof(GtkWidgetState));
+
+ // For XUL checkboxes and radio buttons, the state of the parent
+ // determines our state.
+ if (aWidgetFlags) {
+ if (elementState.HasState(ElementState::CHECKED)) {
+ *aWidgetFlags |= MOZ_GTK_WIDGET_CHECKED;
+ }
+ if (elementState.HasState(ElementState::INDETERMINATE)) {
+ *aWidgetFlags |= MOZ_GTK_WIDGET_INCONSISTENT;
+ }
+ }
+
+ aState->disabled =
+ elementState.HasState(ElementState::DISABLED) || IsReadOnly(aFrame);
+ aState->active = elementState.HasState(ElementState::ACTIVE);
+ aState->focused = elementState.HasState(ElementState::FOCUS);
+ aState->inHover = elementState.HasState(ElementState::HOVER);
+ aState->isDefault = IsDefaultButton(aFrame);
+ aState->canDefault = FALSE; // XXX fix me
+
+ if (aAppearance == StyleAppearance::Button ||
+ aAppearance == StyleAppearance::Toolbarbutton ||
+ aAppearance == StyleAppearance::Dualbutton ||
+ aAppearance == StyleAppearance::ToolbarbuttonDropdown ||
+ aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton) {
+ aState->active &= aState->inHover;
+ } else if (aAppearance == StyleAppearance::Treetwisty ||
+ aAppearance == StyleAppearance::Treetwistyopen) {
+ if (nsTreeBodyFrame* treeBodyFrame = do_QueryFrame(aFrame)) {
+ const mozilla::AtomArray& atoms =
+ treeBodyFrame->GetPropertyArrayForCurrentDrawingItem();
+ aState->selected = atoms.Contains(nsGkAtoms::selected);
+ aState->inHover = atoms.Contains(nsGkAtoms::hover);
+ }
+ }
+
+ if (IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL)) {
+ // For these widget types, some element (either a child or parent)
+ // actually has element focus, so we check the focused attribute
+ // to see whether to draw in the focused state.
+ aState->focused = elementState.HasState(ElementState::FOCUSRING);
+ if (aAppearance == StyleAppearance::Radio ||
+ aAppearance == StyleAppearance::Checkbox) {
+ // In XUL, checkboxes and radios shouldn't have focus rings, their
+ // labels do
+ aState->focused = FALSE;
+ }
+
+ // A button with drop down menu open or an activated toggle button
+ // should always appear depressed.
+ if (aAppearance == StyleAppearance::Button ||
+ aAppearance == StyleAppearance::Toolbarbutton ||
+ aAppearance == StyleAppearance::Dualbutton ||
+ aAppearance == StyleAppearance::ToolbarbuttonDropdown ||
+ aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton) {
+ bool menuOpen = IsOpenButton(aFrame);
+ aState->depressed = IsCheckedButton(aFrame) || menuOpen;
+ // we must not highlight buttons with open drop down menus on hover.
+ aState->inHover = aState->inHover && !menuOpen;
+ }
+ }
+
+ if (aAppearance == StyleAppearance::MozWindowTitlebar ||
+ aAppearance == StyleAppearance::MozWindowTitlebarMaximized ||
+ aAppearance == StyleAppearance::MozWindowButtonClose ||
+ aAppearance == StyleAppearance::MozWindowButtonMinimize ||
+ aAppearance == StyleAppearance::MozWindowButtonMaximize ||
+ aAppearance == StyleAppearance::MozWindowButtonRestore) {
+ aState->backdrop = !nsWindow::GetTopLevelWindowActiveState(aFrame);
+ }
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ if (aWidgetFlags) *aWidgetFlags = GTK_RELIEF_NORMAL;
+ aGtkWidgetType = MOZ_GTK_BUTTON;
+ break;
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Dualbutton:
+ if (aWidgetFlags) *aWidgetFlags = GTK_RELIEF_NONE;
+ aGtkWidgetType = MOZ_GTK_TOOLBAR_BUTTON;
+ break;
+ case StyleAppearance::Checkbox:
+ aGtkWidgetType = MOZ_GTK_CHECKBUTTON;
+ break;
+ case StyleAppearance::Radio:
+ aGtkWidgetType = MOZ_GTK_RADIOBUTTON;
+ break;
+ case StyleAppearance::Spinner:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON;
+ break;
+ case StyleAppearance::SpinnerUpbutton:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON_UP;
+ break;
+ case StyleAppearance::SpinnerDownbutton:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON_DOWN;
+ break;
+ case StyleAppearance::SpinnerTextfield:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON_ENTRY;
+ break;
+ case StyleAppearance::Range: {
+ if (IsRangeHorizontal(aFrame)) {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_HORIZONTAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_HORIZONTAL;
+ } else {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_VERTICAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_VERTICAL;
+ }
+ break;
+ }
+ case StyleAppearance::RangeThumb: {
+ if (IsRangeHorizontal(aFrame)) {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_HORIZONTAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_THUMB_HORIZONTAL;
+ } else {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_VERTICAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_THUMB_VERTICAL;
+ }
+ break;
+ }
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ aGtkWidgetType = MOZ_GTK_ENTRY;
+ break;
+ case StyleAppearance::Textarea:
+ aGtkWidgetType = MOZ_GTK_TEXT_VIEW;
+ break;
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ aGtkWidgetType = MOZ_GTK_TREEVIEW;
+ break;
+ case StyleAppearance::Treetwisty:
+ aGtkWidgetType = MOZ_GTK_TREEVIEW_EXPANDER;
+ if (aWidgetFlags) *aWidgetFlags = GTK_EXPANDER_COLLAPSED;
+ break;
+ case StyleAppearance::Treetwistyopen:
+ aGtkWidgetType = MOZ_GTK_TREEVIEW_EXPANDER;
+ if (aWidgetFlags) *aWidgetFlags = GTK_EXPANDER_EXPANDED;
+ break;
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist:
+ aGtkWidgetType = MOZ_GTK_DROPDOWN;
+ if (aWidgetFlags)
+ *aWidgetFlags =
+ IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XHTML);
+ break;
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ aGtkWidgetType = MOZ_GTK_TOOLBARBUTTON_ARROW;
+ if (aWidgetFlags) {
+ *aWidgetFlags = GTK_ARROW_DOWN;
+
+ if (aAppearance == StyleAppearance::ButtonArrowUp)
+ *aWidgetFlags = GTK_ARROW_UP;
+ else if (aAppearance == StyleAppearance::ButtonArrowNext)
+ *aWidgetFlags = GTK_ARROW_RIGHT;
+ else if (aAppearance == StyleAppearance::ButtonArrowPrevious)
+ *aWidgetFlags = GTK_ARROW_LEFT;
+ }
+ break;
+ case StyleAppearance::Tooltip:
+ aGtkWidgetType = MOZ_GTK_TOOLTIP;
+ break;
+ case StyleAppearance::ProgressBar:
+ aGtkWidgetType = MOZ_GTK_PROGRESSBAR;
+ break;
+ case StyleAppearance::Progresschunk: {
+ nsIFrame* stateFrame = aFrame->GetParent();
+ ElementState elementState = GetContentState(stateFrame, aAppearance);
+
+ aGtkWidgetType = elementState.HasState(ElementState::INDETERMINATE)
+ ? IsVerticalProgress(stateFrame)
+ ? MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE
+ : MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE
+ : MOZ_GTK_PROGRESS_CHUNK;
+ } break;
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ if (aWidgetFlags)
+ *aWidgetFlags = aAppearance == StyleAppearance::TabScrollArrowBack
+ ? GTK_ARROW_LEFT
+ : GTK_ARROW_RIGHT;
+ aGtkWidgetType = MOZ_GTK_TAB_SCROLLARROW;
+ break;
+ case StyleAppearance::Tabpanels:
+ aGtkWidgetType = MOZ_GTK_TABPANELS;
+ break;
+ case StyleAppearance::Tab: {
+ if (IsBottomTab(aFrame)) {
+ aGtkWidgetType = MOZ_GTK_TAB_BOTTOM;
+ } else {
+ aGtkWidgetType = MOZ_GTK_TAB_TOP;
+ }
+
+ if (aWidgetFlags) {
+ /* First bits will be used to store max(0,-bmargin) where bmargin
+ * is the bottom margin of the tab in pixels (resp. top margin,
+ * for bottom tabs). */
+ *aWidgetFlags = GetTabMarginPixels(aFrame);
+
+ if (IsSelectedTab(aFrame)) *aWidgetFlags |= MOZ_GTK_TAB_SELECTED;
+
+ if (IsFirstTab(aFrame)) *aWidgetFlags |= MOZ_GTK_TAB_FIRST;
+ }
+ } break;
+ case StyleAppearance::Splitter:
+ if (IsHorizontal(aFrame))
+ aGtkWidgetType = MOZ_GTK_SPLITTER_VERTICAL;
+ else
+ aGtkWidgetType = MOZ_GTK_SPLITTER_HORIZONTAL;
+ break;
+ case StyleAppearance::MozWindowTitlebar:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR;
+ break;
+ case StyleAppearance::MozWindowDecorations:
+ aGtkWidgetType = MOZ_GTK_WINDOW_DECORATION;
+ break;
+ case StyleAppearance::MozWindowTitlebarMaximized:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_MAXIMIZED;
+ break;
+ case StyleAppearance::MozWindowButtonBox:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_BOX;
+ break;
+ case StyleAppearance::MozWindowButtonClose:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_CLOSE;
+ break;
+ case StyleAppearance::MozWindowButtonMinimize:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE;
+ break;
+ case StyleAppearance::MozWindowButtonMaximize:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE;
+ break;
+ case StyleAppearance::MozWindowButtonRestore:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE;
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+class SystemCairoClipper : public ClipExporter {
+ public:
+ explicit SystemCairoClipper(cairo_t* aContext, gint aScaleFactor = 1)
+ : mContext(aContext), mScaleFactor(aScaleFactor) {}
+
+ void BeginClip(const Matrix& aTransform) override {
+ cairo_matrix_t mat;
+ GfxMatrixToCairoMatrix(aTransform, mat);
+ // We also need to remove the scale factor effect from the matrix
+ mat.y0 = mat.y0 / mScaleFactor;
+ mat.x0 = mat.x0 / mScaleFactor;
+ cairo_set_matrix(mContext, &mat);
+
+ cairo_new_path(mContext);
+ }
+
+ void MoveTo(const Point& aPoint) override {
+ cairo_move_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor);
+ mBeginPoint = aPoint;
+ mCurrentPoint = aPoint;
+ }
+
+ void LineTo(const Point& aPoint) override {
+ cairo_line_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor);
+ mCurrentPoint = aPoint;
+ }
+
+ void BezierTo(const Point& aCP1, const Point& aCP2,
+ const Point& aCP3) override {
+ cairo_curve_to(mContext, aCP1.x / mScaleFactor, aCP1.y / mScaleFactor,
+ aCP2.x / mScaleFactor, aCP2.y / mScaleFactor,
+ aCP3.x / mScaleFactor, aCP3.y / mScaleFactor);
+ mCurrentPoint = aCP3;
+ }
+
+ void QuadraticBezierTo(const Point& aCP1, const Point& aCP2) override {
+ Point CP0 = CurrentPoint();
+ Point CP1 = (CP0 + aCP1 * 2.0) / 3.0;
+ Point CP2 = (aCP2 + aCP1 * 2.0) / 3.0;
+ Point CP3 = aCP2;
+ cairo_curve_to(mContext, CP1.x / mScaleFactor, CP1.y / mScaleFactor,
+ CP2.x / mScaleFactor, CP2.y / mScaleFactor,
+ CP3.x / mScaleFactor, CP3.y / mScaleFactor);
+ mCurrentPoint = aCP2;
+ }
+
+ void Arc(const Point& aOrigin, float aRadius, float aStartAngle,
+ float aEndAngle, bool aAntiClockwise) override {
+ ArcToBezier(this, aOrigin, Size(aRadius, aRadius), aStartAngle, aEndAngle,
+ aAntiClockwise);
+ }
+
+ void Close() override {
+ cairo_close_path(mContext);
+ mCurrentPoint = mBeginPoint;
+ }
+
+ void EndClip() override { cairo_clip(mContext); }
+
+ private:
+ cairo_t* mContext;
+ gint mScaleFactor;
+};
+
+static void DrawThemeWithCairo(gfxContext* aContext, DrawTarget* aDrawTarget,
+ GtkWidgetState aState,
+ WidgetNodeType aGTKWidgetType, gint aFlags,
+ GtkTextDirection aDirection, double aScaleFactor,
+ bool aSnapped, const Point& aDrawOrigin,
+ const nsIntSize& aDrawSize,
+ GdkRectangle& aGDKRect,
+ nsITheme::Transparency aTransparency) {
+ static auto sCairoSurfaceSetDeviceScalePtr =
+ (void (*)(cairo_surface_t*, double, double))dlsym(
+ RTLD_DEFAULT, "cairo_surface_set_device_scale");
+ const bool useHiDPIWidgets =
+ aScaleFactor != 1.0 && sCairoSurfaceSetDeviceScalePtr;
+
+ Point drawOffsetScaled;
+ Point drawOffsetOriginal;
+ Matrix transform;
+ if (!aSnapped) {
+ // If we are not snapped, we depend on the DT for translation.
+ drawOffsetOriginal = aDrawOrigin;
+ drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor
+ : drawOffsetOriginal;
+ transform = aDrawTarget->GetTransform().PreTranslate(drawOffsetScaled);
+ } else {
+ // Otherwise, we only need to take the device offset into account.
+ drawOffsetOriginal = aDrawOrigin - aContext->GetDeviceOffset();
+ drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor
+ : drawOffsetOriginal;
+ transform = Matrix::Translation(drawOffsetScaled);
+ }
+
+ if (!useHiDPIWidgets && aScaleFactor != 1) {
+ transform.PreScale(aScaleFactor, aScaleFactor);
+ }
+
+ cairo_matrix_t mat;
+ GfxMatrixToCairoMatrix(transform, mat);
+
+ Size clipSize((aDrawSize.width + aScaleFactor - 1) / aScaleFactor,
+ (aDrawSize.height + aScaleFactor - 1) / aScaleFactor);
+
+ // A direct Cairo draw target is not available, so we need to create a
+ // temporary one.
+#if defined(MOZ_X11) && defined(CAIRO_HAS_XLIB_SURFACE)
+ if (GdkIsX11Display()) {
+ // If using a Cairo xlib surface, then try to reuse it.
+ BorrowedXlibDrawable borrow(aDrawTarget);
+ if (Drawable drawable = borrow.GetDrawable()) {
+ nsIntSize size = borrow.GetSize();
+ cairo_surface_t* surf = cairo_xlib_surface_create(
+ borrow.GetDisplay(), drawable, borrow.GetVisual(), size.width,
+ size.height);
+ if (!NS_WARN_IF(!surf)) {
+ Point offset = borrow.GetOffset();
+ if (offset != Point()) {
+ cairo_surface_set_device_offset(surf, offset.x, offset.y);
+ }
+ cairo_t* cr = cairo_create(surf);
+ if (!NS_WARN_IF(!cr)) {
+ RefPtr<SystemCairoClipper> clipper = new SystemCairoClipper(cr);
+ aContext->ExportClip(*clipper);
+
+ cairo_set_matrix(cr, &mat);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height);
+ cairo_clip(cr);
+
+ moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags,
+ aDirection);
+
+ cairo_destroy(cr);
+ }
+ cairo_surface_destroy(surf);
+ }
+ borrow.Finish();
+ return;
+ }
+ }
+#endif
+
+ // Check if the widget requires complex masking that must be composited.
+ // Try to directly write to the draw target's pixels if possible.
+ uint8_t* data;
+ nsIntSize size;
+ int32_t stride;
+ SurfaceFormat format;
+ IntPoint origin;
+ if (aDrawTarget->LockBits(&data, &size, &stride, &format, &origin)) {
+ // Create a Cairo image surface context the device rectangle.
+ cairo_surface_t* surf = cairo_image_surface_create_for_data(
+ data, GfxFormatToCairoFormat(format), size.width, size.height, stride);
+ if (!NS_WARN_IF(!surf)) {
+ if (useHiDPIWidgets) {
+ sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor);
+ }
+ if (origin != IntPoint()) {
+ cairo_surface_set_device_offset(surf, -origin.x, -origin.y);
+ }
+ cairo_t* cr = cairo_create(surf);
+ if (!NS_WARN_IF(!cr)) {
+ RefPtr<SystemCairoClipper> clipper =
+ new SystemCairoClipper(cr, useHiDPIWidgets ? aScaleFactor : 1);
+ aContext->ExportClip(*clipper);
+
+ cairo_set_matrix(cr, &mat);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height);
+ cairo_clip(cr);
+
+ moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags,
+ aDirection);
+
+ cairo_destroy(cr);
+ }
+ cairo_surface_destroy(surf);
+ }
+ aDrawTarget->ReleaseBits(data);
+ } else {
+ // If the widget has any transparency, make sure to choose an alpha format.
+ format = aTransparency != nsITheme::eOpaque ? SurfaceFormat::B8G8R8A8
+ : aDrawTarget->GetFormat();
+ // Create a temporary data surface to render the widget into.
+ RefPtr<DataSourceSurface> dataSurface = Factory::CreateDataSourceSurface(
+ aDrawSize, format, aTransparency != nsITheme::eOpaque);
+ DataSourceSurface::MappedSurface map;
+ if (!NS_WARN_IF(
+ !(dataSurface &&
+ dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)))) {
+ // Create a Cairo image surface wrapping the data surface.
+ cairo_surface_t* surf = cairo_image_surface_create_for_data(
+ map.mData, GfxFormatToCairoFormat(format), aDrawSize.width,
+ aDrawSize.height, map.mStride);
+ cairo_t* cr = nullptr;
+ if (!NS_WARN_IF(!surf)) {
+ cr = cairo_create(surf);
+ if (!NS_WARN_IF(!cr)) {
+ if (aScaleFactor != 1) {
+ if (useHiDPIWidgets) {
+ sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor);
+ } else {
+ cairo_scale(cr, aScaleFactor, aScaleFactor);
+ }
+ }
+
+ moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags,
+ aDirection);
+ }
+ }
+
+ // Unmap the surface before using it as a source
+ dataSurface->Unmap();
+
+ if (cr) {
+ // The widget either needs to be masked or has transparency, so use the
+ // slower drawing path.
+ aDrawTarget->DrawSurface(
+ dataSurface,
+ Rect(aSnapped ? drawOffsetOriginal -
+ aDrawTarget->GetTransform().GetTranslation()
+ : drawOffsetOriginal,
+ Size(aDrawSize)),
+ Rect(0, 0, aDrawSize.width, aDrawSize.height));
+ cairo_destroy(cr);
+ }
+
+ if (surf) {
+ cairo_surface_destroy(surf);
+ }
+ }
+ }
+}
+
+CSSIntMargin nsNativeThemeGTK::GetExtraSizeForWidget(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ CSSIntMargin extra;
+ // Allow an extra one pixel above and below the thumb for certain
+ // GTK2 themes (Ximian Industrial, Bluecurve, Misty, at least);
+ // We modify the frame's overflow area. See bug 297508.
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ if (IsDefaultButton(aFrame)) {
+ // Some themes draw a default indicator outside the widget,
+ // include that in overflow
+ moz_gtk_button_get_default_overflow(&extra.top.value, &extra.left.value,
+ &extra.bottom.value,
+ &extra.right.value);
+ break;
+ }
+ return {};
+ }
+ default:
+ return {};
+ }
+ return extra;
+}
+
+bool nsNativeThemeGTK::IsWidgetVisible(StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::MozWindowButtonBox:
+ return false;
+ default:
+ break;
+ }
+ return true;
+}
+
+NS_IMETHODIMP
+nsNativeThemeGTK::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect,
+ DrawOverflow aDrawOverflow) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect,
+ aDirtyRect, aDrawOverflow);
+ }
+
+ GtkWidgetState state;
+ WidgetNodeType gtkWidgetType;
+ GtkTextDirection direction = GetTextDirection(aFrame);
+ gint flags;
+
+ if (!IsWidgetVisible(aAppearance) ||
+ !GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, &state,
+ &flags)) {
+ return NS_OK;
+ }
+
+ gfxContext* ctx = aContext;
+ nsPresContext* presContext = aFrame->PresContext();
+
+ gfxRect rect = presContext->AppUnitsToGfxUnits(aRect);
+ gfxRect dirtyRect = presContext->AppUnitsToGfxUnits(aDirtyRect);
+
+ // Align to device pixels where sensible
+ // to provide crisper and faster drawing.
+ // Don't snap if it's a non-unit scale factor. We're going to have to take
+ // slow paths then in any case.
+ // We prioritize the size when snapping in order to avoid distorting widgets
+ // that should be square, which can occur if edges are snapped independently.
+ bool snapped = ctx->UserToDevicePixelSnapped(
+ rect, gfxContext::SnapOption::PrioritizeSize);
+ if (snapped) {
+ // Leave rect in device coords but make dirtyRect consistent.
+ dirtyRect = ctx->UserToDevice(dirtyRect);
+ }
+
+ // Translate the dirty rect so that it is wrt the widget top-left.
+ dirtyRect.MoveBy(-rect.TopLeft());
+ // Round out the dirty rect to gdk pixels to ensure that gtk draws
+ // enough pixels for interpolation to device pixels.
+ dirtyRect.RoundOut();
+
+ // GTK themes can only draw an integer number of pixels
+ // (even when not snapped).
+ LayoutDeviceIntRect widgetRect(0, 0, NS_lround(rect.Width()),
+ NS_lround(rect.Height()));
+
+ // This is the rectangle that will actually be drawn, in gdk pixels
+ LayoutDeviceIntRect drawingRect(
+ int32_t(dirtyRect.X()), int32_t(dirtyRect.Y()),
+ int32_t(dirtyRect.Width()), int32_t(dirtyRect.Height()));
+ if (widgetRect.IsEmpty() ||
+ !drawingRect.IntersectRect(widgetRect, drawingRect)) {
+ return NS_OK;
+ }
+
+ NS_ASSERTION(!IsWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance),
+ "Trying to render an unsafe widget!");
+
+ bool safeState = IsWidgetStateSafe(mSafeWidgetStates, aAppearance, &state);
+ if (!safeState) {
+ gLastGdkError = 0;
+ gdk_error_trap_push();
+ }
+
+ Transparency transparency = GetWidgetTransparency(aFrame, aAppearance);
+
+ // gdk rectangles are wrt the drawing rect.
+ auto scaleFactor = GetWidgetScaleFactor(aFrame);
+ LayoutDeviceIntRect gdkDevRect(-drawingRect.TopLeft(), widgetRect.Size());
+
+ auto gdkCssRect = CSSIntRect::RoundIn(gdkDevRect / scaleFactor);
+ GdkRectangle gdk_rect = {gdkCssRect.x, gdkCssRect.y, gdkCssRect.width,
+ gdkCssRect.height};
+
+ // Save actual widget scale to GtkWidgetState as we don't provide
+ // the frame to gtk3drawing routines.
+ state.image_scale = std::ceil(scaleFactor.scale);
+
+ // translate everything so (0,0) is the top left of the drawingRect
+ gfxPoint origin = rect.TopLeft() + drawingRect.TopLeft().ToUnknownPoint();
+
+ DrawThemeWithCairo(ctx, aContext->GetDrawTarget(), state, gtkWidgetType,
+ flags, direction, scaleFactor.scale, snapped,
+ ToPoint(origin), drawingRect.Size().ToUnknownSize(),
+ gdk_rect, transparency);
+
+ if (!safeState) {
+ // gdk_flush() call from expose event crashes Gtk+ on Wayland
+ // (Gnome BZ #773307)
+ if (GdkIsX11Display()) {
+ gdk_flush();
+ }
+ gLastGdkError = gdk_error_trap_pop();
+
+ if (gLastGdkError) {
+#ifdef DEBUG
+ printf(
+ "GTK theme failed for widget type %d, error was %d, state was "
+ "[active=%d,focused=%d,inHover=%d,disabled=%d]\n",
+ static_cast<int>(aAppearance), gLastGdkError, state.active,
+ state.focused, state.inHover, state.disabled);
+#endif
+ NS_WARNING("GTK theme failed; disabling unsafe widget");
+ SetWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance);
+ // force refresh of the window, because the widget was not
+ // successfully drawn it must be redrawn using the default look
+ RefreshWidgetWindow(aFrame);
+ } else {
+ SetWidgetStateSafe(mSafeWidgetStates, aAppearance, &state);
+ }
+ }
+
+ // Indeterminate progress bar are animated.
+ if (gtkWidgetType == MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE ||
+ gtkWidgetType == MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE) {
+ if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) {
+ NS_WARNING("unable to animate widget!");
+ }
+ }
+
+ return NS_OK;
+}
+
+bool nsNativeThemeGTK::CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::CreateWebRenderCommandsForWidget(
+ aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect);
+ }
+ return false;
+}
+
+WidgetNodeType nsNativeThemeGTK::NativeThemeToGtkTheme(
+ StyleAppearance aAppearance, nsIFrame* aFrame) {
+ WidgetNodeType gtkWidgetType;
+ gint unusedFlags;
+
+ if (!GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr,
+ &unusedFlags)) {
+ MOZ_ASSERT_UNREACHABLE("Unknown native widget to gtk widget mapping");
+ return MOZ_GTK_WINDOW;
+ }
+ return gtkWidgetType;
+}
+
+static void FixupForVerticalWritingMode(WritingMode aWritingMode,
+ CSSIntMargin* aResult) {
+ if (aWritingMode.IsVertical()) {
+ bool rtl = aWritingMode.IsBidiRTL();
+ LogicalMargin logical(aWritingMode, aResult->top,
+ rtl ? aResult->left : aResult->right, aResult->bottom,
+ rtl ? aResult->right : aResult->left);
+ nsMargin physical = logical.GetPhysicalMargin(aWritingMode);
+ aResult->top = physical.top;
+ aResult->right = physical.right;
+ aResult->bottom = physical.bottom;
+ aResult->left = physical.left;
+ }
+}
+
+CSSIntMargin nsNativeThemeGTK::GetCachedWidgetBorder(
+ nsIFrame* aFrame, StyleAppearance aAppearance,
+ GtkTextDirection aDirection) {
+ CSSIntMargin result;
+
+ WidgetNodeType gtkWidgetType;
+ gint unusedFlags;
+ if (GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr,
+ &unusedFlags)) {
+ MOZ_ASSERT(0 <= gtkWidgetType && gtkWidgetType < MOZ_GTK_WIDGET_NODE_COUNT);
+ uint8_t cacheIndex = gtkWidgetType / 8;
+ uint8_t cacheBit = 1u << (gtkWidgetType % 8);
+
+ if (mBorderCacheValid[cacheIndex] & cacheBit) {
+ result = mBorderCache[gtkWidgetType];
+ } else {
+ moz_gtk_get_widget_border(gtkWidgetType, &result.left.value,
+ &result.top.value, &result.right.value,
+ &result.bottom.value, aDirection);
+ if (gtkWidgetType != MOZ_GTK_DROPDOWN) { // depends on aDirection
+ mBorderCacheValid[cacheIndex] |= cacheBit;
+ mBorderCache[gtkWidgetType] = result;
+ }
+ }
+ }
+ FixupForVerticalWritingMode(aFrame->GetWritingMode(), &result);
+ return result;
+}
+
+LayoutDeviceIntMargin nsNativeThemeGTK::GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetWidgetBorder(aContext, aFrame, aAppearance);
+ }
+
+ CSSIntMargin result;
+ GtkTextDirection direction = GetTextDirection(aFrame);
+ switch (aAppearance) {
+ case StyleAppearance::Toolbox:
+ // gtk has no toolbox equivalent. So, although we map toolbox to
+ // gtk's 'toolbar' for purposes of painting the widget background,
+ // we don't use the toolbar border for toolbox.
+ break;
+ case StyleAppearance::Dualbutton:
+ // TOOLBAR_DUAL_BUTTON is an interesting case. We want a border to draw
+ // around the entire button + dropdown, and also an inner border if you're
+ // over the button part. But, we want the inner button to be right up
+ // against the edge of the outer button so that the borders overlap.
+ // To make this happen, we draw a button border for the outer button,
+ // but don't reserve any space for it.
+ break;
+ case StyleAppearance::Tab: {
+ WidgetNodeType gtkWidgetType;
+ gint flags;
+
+ if (!GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr,
+ &flags)) {
+ return {};
+ }
+ moz_gtk_get_tab_border(&result.left.value, &result.top.value,
+ &result.right.value, &result.bottom.value,
+ direction, (GtkTabFlags)flags, gtkWidgetType);
+ } break;
+ default: {
+ result = GetCachedWidgetBorder(aFrame, aAppearance, direction);
+ }
+ }
+
+ return (CSSMargin(result) * GetWidgetScaleFactor(aFrame)).Rounded();
+}
+
+bool nsNativeThemeGTK::GetWidgetPadding(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetWidgetPadding(aContext, aFrame, aAppearance, aResult);
+ }
+ switch (aAppearance) {
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::MozWindowButtonBox:
+ case StyleAppearance::MozWindowButtonClose:
+ case StyleAppearance::MozWindowButtonMinimize:
+ case StyleAppearance::MozWindowButtonMaximize:
+ case StyleAppearance::MozWindowButtonRestore:
+ case StyleAppearance::Dualbutton:
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ case StyleAppearance::RangeThumb:
+ // Radios and checkboxes return a fixed size in GetMinimumWidgetSize
+ // and have a meaningful baseline, so they can't have
+ // author-specified padding.
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ aResult->SizeTo(0, 0, 0, 0);
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool nsNativeThemeGTK::GetWidgetOverflow(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::GetWidgetOverflow(aContext, aFrame, aAppearance,
+ aOverflowRect);
+ }
+ auto overflow = GetExtraSizeForWidget(aFrame, aAppearance);
+ if (overflow == CSSIntMargin()) {
+ return false;
+ }
+ aOverflowRect->Inflate(CSSIntMargin::ToAppUnits(overflow));
+ return true;
+}
+
+auto nsNativeThemeGTK::IsWidgetNonNative(nsIFrame* aFrame,
+ StyleAppearance aAppearance)
+ -> NonNative {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return NonNative::Always;
+ }
+
+ // If the current GTK theme color scheme matches our color-scheme, then we
+ // can draw a native widget.
+ if (LookAndFeel::ColorSchemeForFrame(aFrame) ==
+ PreferenceSheet::ColorSchemeForChrome()) {
+ return NonNative::No;
+ }
+
+ // As an special-case, for tooltips, we check if the tooltip color is the
+ // same between the light and dark themes. If so we can get away with drawing
+ // the native widget, see bug 1817396.
+ if (aAppearance == StyleAppearance::Tooltip) {
+ auto darkColor =
+ LookAndFeel::Color(StyleSystemColor::Infotext, ColorScheme::Dark,
+ LookAndFeel::UseStandins::No);
+ auto lightColor =
+ LookAndFeel::Color(StyleSystemColor::Infotext, ColorScheme::Light,
+ LookAndFeel::UseStandins::No);
+ if (darkColor == lightColor) {
+ return NonNative::No;
+ }
+ }
+
+ // If the non-native theme doesn't support the widget then oh well...
+ if (!Theme::ThemeSupportsWidget(aFrame->PresContext(), aFrame, aAppearance)) {
+ return NonNative::No;
+ }
+
+ return NonNative::BecauseColorMismatch;
+}
+
+bool nsNativeThemeGTK::IsWidgetAlwaysNonNative(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ return Theme::IsWidgetAlwaysNonNative(aFrame, aAppearance) ||
+ aAppearance == StyleAppearance::MozMenulistArrowButton;
+}
+
+LayoutDeviceIntSize nsNativeThemeGTK::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance);
+ }
+
+ CSSIntSize result;
+ switch (aAppearance) {
+ case StyleAppearance::Splitter: {
+ if (IsHorizontal(aFrame)) {
+ moz_gtk_splitter_get_metrics(GTK_ORIENTATION_HORIZONTAL, &result.width);
+ } else {
+ moz_gtk_splitter_get_metrics(GTK_ORIENTATION_VERTICAL, &result.height);
+ }
+ } break;
+ case StyleAppearance::RangeThumb: {
+ if (IsRangeHorizontal(aFrame)) {
+ moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_HORIZONTAL,
+ &result.width, &result.height);
+ } else {
+ moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_VERTICAL, &result.width,
+ &result.width);
+ }
+ } break;
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward: {
+ moz_gtk_get_tab_scroll_arrow_size(&result.width, &result.height);
+ } break;
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio: {
+ const ToggleGTKMetrics* metrics = GetToggleMetrics(
+ aAppearance == StyleAppearance::Radio ? MOZ_GTK_RADIOBUTTON
+ : MOZ_GTK_CHECKBUTTON);
+ result.width = metrics->minSizeWithBorder.width;
+ result.height = metrics->minSizeWithBorder.height;
+ } break;
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious: {
+ moz_gtk_get_arrow_size(MOZ_GTK_TOOLBARBUTTON_ARROW, &result.width,
+ &result.height);
+ } break;
+ case StyleAppearance::MozWindowButtonClose: {
+ const ToolbarButtonGTKMetrics* metrics =
+ GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_CLOSE);
+ result.width = metrics->minSizeWithBorderMargin.width;
+ result.height = metrics->minSizeWithBorderMargin.height;
+ break;
+ }
+ case StyleAppearance::MozWindowButtonMinimize: {
+ const ToolbarButtonGTKMetrics* metrics =
+ GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE);
+ result.width = metrics->minSizeWithBorderMargin.width;
+ result.height = metrics->minSizeWithBorderMargin.height;
+ break;
+ }
+ case StyleAppearance::MozWindowButtonMaximize:
+ case StyleAppearance::MozWindowButtonRestore: {
+ const ToolbarButtonGTKMetrics* metrics =
+ GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE);
+ result.width = metrics->minSizeWithBorderMargin.width;
+ result.height = metrics->minSizeWithBorderMargin.height;
+ break;
+ }
+ case StyleAppearance::Button:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton: {
+ if (aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton) {
+ // Include the arrow size.
+ moz_gtk_get_arrow_size(MOZ_GTK_DROPDOWN, &result.width, &result.height);
+ }
+ // else the minimum size is missing consideration of container
+ // descendants; the value returned here will not be helpful, but the
+ // box model may consider border and padding with child minimum sizes.
+
+ CSSIntMargin border =
+ GetCachedWidgetBorder(aFrame, aAppearance, GetTextDirection(aFrame));
+ result.width += border.LeftRight();
+ result.height += border.TopBottom();
+ } break;
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield: {
+ gint contentHeight = 0;
+ gint borderPaddingHeight = 0;
+ moz_gtk_get_entry_min_height(&contentHeight, &borderPaddingHeight);
+
+ // Scale the min content height proportionately with the font-size if it's
+ // smaller than the default one. This prevents <input type=text
+ // style="font-size: .5em"> from keeping a ridiculously large size, for
+ // example.
+ const gfxFloat fieldFontSizeInCSSPixels = [] {
+ gfxFontStyle fieldFontStyle;
+ nsAutoString unusedFontName;
+ DebugOnly<bool> result = LookAndFeel::GetFont(
+ LookAndFeel::FontID::MozField, unusedFontName, fieldFontStyle);
+ MOZ_ASSERT(result, "GTK look and feel supports the field font");
+ // NOTE: GetFont returns font sizes in CSS pixels, and we want just
+ // that.
+ return fieldFontStyle.size;
+ }();
+
+ const gfxFloat fontSize = aFrame->StyleFont()->mFont.size.ToCSSPixels();
+ if (fieldFontSizeInCSSPixels > fontSize) {
+ contentHeight =
+ std::round(contentHeight * fontSize / fieldFontSizeInCSSPixels);
+ }
+
+ gint height = contentHeight + borderPaddingHeight;
+ if (aFrame->GetWritingMode().IsVertical()) {
+ result.width = height;
+ } else {
+ result.height = height;
+ }
+ } break;
+ case StyleAppearance::Spinner:
+ // hard code these sizes
+ result.width = 14;
+ result.height = 26;
+ break;
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ // hard code these sizes
+ result.width = 14;
+ result.height = 13;
+ break;
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen: {
+ gint expander_size;
+ moz_gtk_get_treeview_expander_size(&expander_size);
+ result.width = result.height = expander_size;
+ } break;
+ default:
+ break;
+ }
+
+ return LayoutDeviceIntSize::Round(CSSSize(result) *
+ GetWidgetScaleFactor(aFrame));
+}
+
+NS_IMETHODIMP
+nsNativeThemeGTK::WidgetStateChanged(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) {
+ *aShouldRepaint = false;
+
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::WidgetStateChanged(aFrame, aAppearance, aAttribute,
+ aShouldRepaint, aOldValue);
+ }
+
+ // Some widget types just never change state.
+ if (aAppearance == StyleAppearance::Toolbox ||
+ aAppearance == StyleAppearance::Toolbar ||
+ aAppearance == StyleAppearance::Progresschunk ||
+ aAppearance == StyleAppearance::ProgressBar ||
+ aAppearance == StyleAppearance::Tooltip ||
+ aAppearance == StyleAppearance::MozWindowDecorations) {
+ return NS_OK;
+ }
+
+ if (aAppearance == StyleAppearance::MozWindowTitlebar ||
+ aAppearance == StyleAppearance::MozWindowTitlebarMaximized ||
+ aAppearance == StyleAppearance::MozWindowButtonClose ||
+ aAppearance == StyleAppearance::MozWindowButtonMinimize ||
+ aAppearance == StyleAppearance::MozWindowButtonMaximize ||
+ aAppearance == StyleAppearance::MozWindowButtonRestore) {
+ *aShouldRepaint = true;
+ return NS_OK;
+ }
+
+ // XXXdwh Not sure what can really be done here. Can at least guess for
+ // specific widgets that they're highly unlikely to have certain states.
+ // For example, a toolbar doesn't care about any states.
+ if (!aAttribute) {
+ // Hover/focus/active changed. Always repaint.
+ *aShouldRepaint = true;
+ return NS_OK;
+ }
+
+ // Check the attribute to see if it's relevant.
+ // disabled, checked, dlgtype, default, etc.
+ *aShouldRepaint = false;
+ if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
+ aAttribute == nsGkAtoms::selected ||
+ aAttribute == nsGkAtoms::visuallyselected ||
+ aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::readonly ||
+ aAttribute == nsGkAtoms::_default ||
+ aAttribute == nsGkAtoms::menuactive || aAttribute == nsGkAtoms::open) {
+ *aShouldRepaint = true;
+ return NS_OK;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNativeThemeGTK::ThemeChanged() {
+ memset(mDisabledWidgetTypes, 0, sizeof(mDisabledWidgetTypes));
+ memset(mSafeWidgetStates, 0, sizeof(mSafeWidgetStates));
+ memset(mBorderCacheValid, 0, sizeof(mBorderCacheValid));
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(bool)
+nsNativeThemeGTK::ThemeSupportsWidget(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance)) {
+ return false;
+ }
+
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::ThemeSupportsWidget(aPresContext, aFrame, aAppearance);
+ }
+
+ switch (aAppearance) {
+ // Combobox dropdowns don't support native theming in vertical mode.
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ if (aFrame && aFrame->GetWritingMode().IsVertical()) {
+ return false;
+ }
+ [[fallthrough]];
+
+ case StyleAppearance::Button:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Toolbox: // N/A
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Dualbutton: // so we can override the border with 0
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ // case StyleAppearance::Treeitem:
+ case StyleAppearance::Treetwisty:
+ // case StyleAppearance::Treeline:
+ // case StyleAppearance::Treeheader:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Tab:
+ // case StyleAppearance::Tabpanel:
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::SpinnerTextfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Range:
+ case StyleAppearance::RangeThumb:
+ case StyleAppearance::Splitter:
+ case StyleAppearance::MozWindowButtonBox:
+ case StyleAppearance::MozWindowButtonClose:
+ case StyleAppearance::MozWindowButtonMinimize:
+ case StyleAppearance::MozWindowButtonMaximize:
+ case StyleAppearance::MozWindowButtonRestore:
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::MozWindowTitlebarMaximized:
+ case StyleAppearance::MozWindowDecorations:
+ return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+ default:
+ break;
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP_(bool)
+nsNativeThemeGTK::WidgetIsContainer(StyleAppearance aAppearance) {
+ // XXXdwh At some point flesh all of this out.
+ if (aAppearance == StyleAppearance::Radio ||
+ aAppearance == StyleAppearance::RangeThumb ||
+ aAppearance == StyleAppearance::Checkbox ||
+ aAppearance == StyleAppearance::TabScrollArrowBack ||
+ aAppearance == StyleAppearance::TabScrollArrowForward ||
+ aAppearance == StyleAppearance::ButtonArrowUp ||
+ aAppearance == StyleAppearance::ButtonArrowDown ||
+ aAppearance == StyleAppearance::ButtonArrowNext ||
+ aAppearance == StyleAppearance::ButtonArrowPrevious)
+ return false;
+ return true;
+}
+
+bool nsNativeThemeGTK::ThemeDrawsFocusForWidget(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::ThemeDrawsFocusForWidget(aFrame, aAppearance);
+ }
+ switch (aAppearance) {
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ // These are drawn only for non-XUL elements, but in XUL the label has
+ // the focus ring.
+ return true;
+ case StyleAppearance::Button:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool nsNativeThemeGTK::ThemeNeedsComboboxDropmarker() { return false; }
+
+nsITheme::Transparency nsNativeThemeGTK::GetWidgetTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::GetWidgetTransparency(aFrame, aAppearance);
+ }
+
+ switch (aAppearance) {
+ // Tooltips use gtk_paint_flat_box() on Gtk2
+ // but are shaped on Gtk3
+ case StyleAppearance::Tooltip:
+ return eTransparent;
+ default:
+ return eUnknownTransparency;
+ }
+}
+
+already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() {
+ if (gfxPlatform::IsHeadless()) {
+ return do_AddRef(new Theme(Theme::ScrollbarStyle()));
+ }
+ return do_AddRef(new nsNativeThemeGTK());
+}
diff --git a/widget/gtk/nsNativeThemeGTK.h b/widget/gtk/nsNativeThemeGTK.h
new file mode 100644
index 0000000000..2d0878290e
--- /dev/null
+++ b/widget/gtk/nsNativeThemeGTK.h
@@ -0,0 +1,119 @@
+/* -*- 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/. */
+
+#ifndef _GTK_NSNATIVETHEMEGTK_H_
+#define _GTK_NSNATIVETHEMEGTK_H_
+
+#include "nsITheme.h"
+#include "nsCOMPtr.h"
+#include "nsAtom.h"
+#include "Theme.h"
+
+#include <gtk/gtk.h>
+#include "gtkdrawing.h"
+
+class nsNativeThemeGTK final : public mozilla::widget::Theme {
+ using Theme = mozilla::widget::Theme;
+
+ public:
+ // The nsITheme interface.
+ NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect, const nsRect& aDirtyRect,
+ DrawOverflow) override;
+
+ bool CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame*,
+ StyleAppearance, const nsRect& aRect) override;
+
+ [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) override;
+
+ bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) override;
+
+ // Whether we draw a non-native widget.
+ //
+ // We always draw scrollbars as non-native so that all of Firefox has
+ // consistent scrollbar styles both in chrome and content (plus, the
+ // non-native scrollbars support scrollbar-width, auto-darkening...).
+ //
+ // We draw other widgets as non-native when their color-scheme doesn't match
+ // the current GTK theme's color-scheme. We do that because frequently
+ // switching GTK themes at runtime is prohibitively expensive. In that case
+ // (`BecauseColorMismatch`) we don't call into the non-native theme for sizing
+ // information (GetWidgetPadding/Border and GetMinimumWidgetSize), to avoid
+ // subtle sizing changes. The non-native theme can basically draw at any size,
+ // so we prefer to have consistent sizing information.
+ enum class NonNative { No, Always, BecauseColorMismatch };
+ static bool IsWidgetAlwaysNonNative(nsIFrame*, StyleAppearance);
+ NonNative IsWidgetNonNative(nsIFrame*, StyleAppearance);
+
+ mozilla::LayoutDeviceIntSize GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) override;
+
+ NS_IMETHOD ThemeChanged() override;
+
+ NS_IMETHOD_(bool)
+ ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ NS_IMETHOD_(bool) WidgetIsContainer(StyleAppearance aAppearance) override;
+
+ bool ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) override;
+
+ bool ThemeNeedsComboboxDropmarker() override;
+ Transparency GetWidgetTransparency(nsIFrame*, StyleAppearance) override;
+
+ nsNativeThemeGTK();
+
+ protected:
+ virtual ~nsNativeThemeGTK();
+
+ private:
+ GtkTextDirection GetTextDirection(nsIFrame* aFrame);
+ gint GetTabMarginPixels(nsIFrame* aFrame);
+ bool GetGtkWidgetAndState(StyleAppearance aAppearance, nsIFrame* aFrame,
+ WidgetNodeType& aGtkWidgetType,
+ GtkWidgetState* aState, gint* aWidgetFlags);
+ mozilla::CSSIntMargin GetExtraSizeForWidget(nsIFrame*, StyleAppearance);
+ bool IsWidgetVisible(StyleAppearance aAppearance);
+
+ void RefreshWidgetWindow(nsIFrame* aFrame);
+ WidgetNodeType NativeThemeToGtkTheme(StyleAppearance aAppearance,
+ nsIFrame* aFrame);
+
+ uint8_t mDisabledWidgetTypes
+ [(static_cast<size_t>(mozilla::StyleAppearance::Count) + 7) / 8];
+ uint8_t
+ mSafeWidgetStates[static_cast<size_t>(mozilla::StyleAppearance::Count) *
+ 4]; // 32 bits per widget
+ static const char* sDisabledEngines[];
+
+ // Because moz_gtk_get_widget_border can be slow, we cache its results
+ // by widget type. Each bit in mBorderCacheValid says whether the
+ // corresponding entry in mBorderCache is valid.
+ mozilla::CSSIntMargin GetCachedWidgetBorder(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ GtkTextDirection aDirection);
+ uint8_t mBorderCacheValid[(MOZ_GTK_WIDGET_NODE_COUNT + 7) / 8];
+ mozilla::CSSIntMargin mBorderCache[MOZ_GTK_WIDGET_NODE_COUNT];
+};
+
+#endif
diff --git a/widget/gtk/nsPrintDialogGTK.cpp b/widget/gtk/nsPrintDialogGTK.cpp
new file mode 100644
index 0000000000..3885a94c24
--- /dev/null
+++ b/widget/gtk/nsPrintDialogGTK.cpp
@@ -0,0 +1,621 @@
+/* -*- 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 <gtk/gtk.h>
+#include <gtk/gtkunixprint.h>
+#include <stdlib.h>
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Services.h"
+
+#include "MozContainer.h"
+#include "nsIPrintSettings.h"
+#include "nsIWidget.h"
+#include "nsPrintDialogGTK.h"
+#include "nsPrintSettingsGTK.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsIStringBundle.h"
+#include "nsIPrintSettingsService.h"
+#include "nsPIDOMWindow.h"
+#include "nsPrintfCString.h"
+#include "nsIGIOService.h"
+#include "nsServiceManagerUtils.h"
+#include "WidgetUtils.h"
+#include "WidgetUtilsGtk.h"
+#include "nsIObserverService.h"
+
+// for gdk_x11_window_get_xid
+#include <gdk/gdk.h>
+#ifdef MOZ_X11
+# include <gdk/gdkx.h>
+#endif
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <gio/gunixfdlist.h>
+
+// for dlsym
+#include <dlfcn.h>
+#include "MainThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+static const char header_footer_tags[][4] = {"", "&T", "&U", "&D", "&P", "&PT"};
+
+#define CUSTOM_VALUE_INDEX gint(ArrayLength(header_footer_tags))
+
+static GtkWindow* get_gtk_window_for_nsiwidget(nsIWidget* widget) {
+ return GTK_WINDOW(widget->GetNativeData(NS_NATIVE_SHELLWIDGET));
+}
+
+static void ShowCustomDialog(GtkComboBox* changed_box, gpointer user_data) {
+ if (gtk_combo_box_get_active(changed_box) != CUSTOM_VALUE_INDEX) {
+ g_object_set_data(G_OBJECT(changed_box), "previous-active",
+ GINT_TO_POINTER(gtk_combo_box_get_active(changed_box)));
+ return;
+ }
+
+ GtkWindow* printDialog = GTK_WINDOW(user_data);
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+
+ nsCOMPtr<nsIStringBundle> printBundle;
+ bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties",
+ getter_AddRefs(printBundle));
+ nsAutoString intlString;
+
+ printBundle->GetStringFromName("headerFooterCustom", intlString);
+ GtkWidget* prompt_dialog = gtk_dialog_new_with_buttons(
+ NS_ConvertUTF16toUTF8(intlString).get(), printDialog,
+ (GtkDialogFlags)(GTK_DIALOG_MODAL), GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT,
+ GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, nullptr);
+ gtk_dialog_set_default_response(GTK_DIALOG(prompt_dialog),
+ GTK_RESPONSE_ACCEPT);
+ gtk_dialog_set_alternative_button_order(
+ GTK_DIALOG(prompt_dialog), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_REJECT, -1);
+
+ printBundle->GetStringFromName("customHeaderFooterPrompt", intlString);
+ GtkWidget* custom_label =
+ gtk_label_new(NS_ConvertUTF16toUTF8(intlString).get());
+ GtkWidget* custom_entry = gtk_entry_new();
+ GtkWidget* question_icon =
+ gtk_image_new_from_stock(GTK_STOCK_DIALOG_QUESTION, GTK_ICON_SIZE_DIALOG);
+
+ // To be convenient, prefill the textbox with the existing value, if any, and
+ // select it all so they can easily both edit it and type in a new one.
+ const char* current_text =
+ (const char*)g_object_get_data(G_OBJECT(changed_box), "custom-text");
+ if (current_text) {
+ gtk_entry_set_text(GTK_ENTRY(custom_entry), current_text);
+ gtk_editable_select_region(GTK_EDITABLE(custom_entry), 0, -1);
+ }
+ gtk_entry_set_activates_default(GTK_ENTRY(custom_entry), TRUE);
+
+ GtkWidget* custom_vbox = gtk_vbox_new(TRUE, 2);
+ gtk_box_pack_start(GTK_BOX(custom_vbox), custom_label, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(custom_vbox), custom_entry, FALSE, FALSE,
+ 5); // Make entry 5px underneath label
+ GtkWidget* custom_hbox = gtk_hbox_new(FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(custom_hbox), question_icon, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(custom_hbox), custom_vbox, FALSE, FALSE,
+ 10); // Make question icon 10px away from content
+ gtk_container_set_border_width(GTK_CONTAINER(custom_hbox), 2);
+ gtk_widget_show_all(custom_hbox);
+
+ gtk_box_pack_start(
+ GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(prompt_dialog))),
+ custom_hbox, FALSE, FALSE, 0);
+ gint diag_response = gtk_dialog_run(GTK_DIALOG(prompt_dialog));
+
+ if (diag_response == GTK_RESPONSE_ACCEPT) {
+ const gchar* response_text = gtk_entry_get_text(GTK_ENTRY(custom_entry));
+ g_object_set_data_full(G_OBJECT(changed_box), "custom-text",
+ strdup(response_text), (GDestroyNotify)free);
+ g_object_set_data(G_OBJECT(changed_box), "previous-active",
+ GINT_TO_POINTER(CUSTOM_VALUE_INDEX));
+ } else {
+ // Go back to the previous index
+ gint previous_active = GPOINTER_TO_INT(
+ g_object_get_data(G_OBJECT(changed_box), "previous-active"));
+ gtk_combo_box_set_active(changed_box, previous_active);
+ }
+
+ gtk_widget_destroy(prompt_dialog);
+}
+
+class nsPrintDialogWidgetGTK {
+ public:
+ nsPrintDialogWidgetGTK(nsPIDOMWindowOuter* aParent, bool aHaveSelection,
+ nsIPrintSettings* aPrintSettings);
+ ~nsPrintDialogWidgetGTK() { gtk_widget_destroy(dialog); }
+ NS_ConvertUTF16toUTF8 GetUTF8FromBundle(const char* aKey);
+ gint Run();
+
+ nsresult ImportSettings(nsIPrintSettings* aNSSettings);
+ nsresult ExportSettings(nsIPrintSettings* aNSSettings);
+
+ private:
+ GtkWidget* dialog;
+ GtkWidget* shrink_to_fit_toggle;
+ GtkWidget* print_bg_colors_toggle;
+ GtkWidget* print_bg_images_toggle;
+ GtkWidget* selection_only_toggle;
+ GtkWidget* header_dropdown[3]; // {left, center, right}
+ GtkWidget* footer_dropdown[3];
+
+ nsCOMPtr<nsIStringBundle> printBundle;
+
+ bool useNativeSelection;
+
+ GtkWidget* ConstructHeaderFooterDropdown(const char16_t* currentString);
+ const char* OptionWidgetToString(GtkWidget* dropdown);
+
+ /* Code to copy between GTK and NS print settings structures.
+ * In the following,
+ * "Import" means to copy from NS to GTK
+ * "Export" means to copy from GTK to NS
+ */
+ void ExportHeaderFooter(nsIPrintSettings* aNS);
+};
+
+nsPrintDialogWidgetGTK::nsPrintDialogWidgetGTK(nsPIDOMWindowOuter* aParent,
+ bool aHaveSelection,
+ nsIPrintSettings* aSettings) {
+ nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(aParent);
+ NS_ASSERTION(widget, "Need a widget for dialog to be modal.");
+ GtkWindow* gtkParent = get_gtk_window_for_nsiwidget(widget);
+ NS_ASSERTION(gtkParent, "Need a GTK window for dialog to be modal.");
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ bundleSvc->CreateBundle("chrome://global/locale/printdialog.properties",
+ getter_AddRefs(printBundle));
+
+ dialog = gtk_print_unix_dialog_new(GetUTF8FromBundle("printTitleGTK").get(),
+ gtkParent);
+
+ gtk_print_unix_dialog_set_manual_capabilities(
+ GTK_PRINT_UNIX_DIALOG(dialog),
+ GtkPrintCapabilities(
+ GTK_PRINT_CAPABILITY_COPIES | GTK_PRINT_CAPABILITY_COLLATE |
+ GTK_PRINT_CAPABILITY_REVERSE | GTK_PRINT_CAPABILITY_SCALE |
+ GTK_PRINT_CAPABILITY_GENERATE_PDF));
+
+ // The vast majority of magic numbers in this widget construction are padding.
+ // e.g. for the set_border_width below, 12px matches that of just about every
+ // other window.
+ GtkWidget* custom_options_tab = gtk_vbox_new(FALSE, 0);
+ gtk_container_set_border_width(GTK_CONTAINER(custom_options_tab), 12);
+ GtkWidget* tab_label =
+ gtk_label_new(GetUTF8FromBundle("optionsTabLabelGTK").get());
+
+ // Check buttons for shrink-to-fit and print selection
+ GtkWidget* check_buttons_container = gtk_vbox_new(TRUE, 2);
+ shrink_to_fit_toggle = gtk_check_button_new_with_mnemonic(
+ GetUTF8FromBundle("shrinkToFit").get());
+ gtk_box_pack_start(GTK_BOX(check_buttons_container), shrink_to_fit_toggle,
+ FALSE, FALSE, 0);
+
+ // GTK+2.18 and above allow us to add a "Selection" option to the main
+ // settings screen, rather than adding an option on a custom tab like we must
+ // do on older versions.
+ if (gtk_major_version > 2 ||
+ (gtk_major_version == 2 && gtk_minor_version >= 18)) {
+ useNativeSelection = true;
+ g_object_set(dialog, "support-selection", TRUE, "has-selection",
+ aHaveSelection, "embed-page-setup", TRUE, nullptr);
+ } else {
+ useNativeSelection = false;
+ selection_only_toggle = gtk_check_button_new_with_mnemonic(
+ GetUTF8FromBundle("selectionOnly").get());
+ gtk_widget_set_sensitive(selection_only_toggle, aHaveSelection);
+ gtk_box_pack_start(GTK_BOX(check_buttons_container), selection_only_toggle,
+ FALSE, FALSE, 0);
+ }
+
+ // Check buttons for printing background
+ GtkWidget* appearance_buttons_container = gtk_vbox_new(TRUE, 2);
+ print_bg_colors_toggle = gtk_check_button_new_with_mnemonic(
+ GetUTF8FromBundle("printBGColors").get());
+ print_bg_images_toggle = gtk_check_button_new_with_mnemonic(
+ GetUTF8FromBundle("printBGImages").get());
+ gtk_box_pack_start(GTK_BOX(appearance_buttons_container),
+ print_bg_colors_toggle, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(appearance_buttons_container),
+ print_bg_images_toggle, FALSE, FALSE, 0);
+
+ // "Appearance" options label, bold and center-aligned
+ GtkWidget* appearance_label = gtk_label_new(nullptr);
+ char* pangoMarkup = g_markup_printf_escaped(
+ "<b>%s</b>", GetUTF8FromBundle("printBGOptions").get());
+ gtk_label_set_markup(GTK_LABEL(appearance_label), pangoMarkup);
+ g_free(pangoMarkup);
+ gtk_misc_set_alignment(GTK_MISC(appearance_label), 0, 0);
+
+ GtkWidget* appearance_container = gtk_alignment_new(0, 0, 0, 0);
+ gtk_alignment_set_padding(GTK_ALIGNMENT(appearance_container), 8, 0, 12, 0);
+ gtk_container_add(GTK_CONTAINER(appearance_container),
+ appearance_buttons_container);
+
+ GtkWidget* appearance_vertical_squasher = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(appearance_vertical_squasher), appearance_label,
+ FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(appearance_vertical_squasher),
+ appearance_container, FALSE, FALSE, 0);
+
+ // "Header & Footer" options label, bold and center-aligned
+ GtkWidget* header_footer_label = gtk_label_new(nullptr);
+ pangoMarkup = g_markup_printf_escaped(
+ "<b>%s</b>", GetUTF8FromBundle("headerFooter").get());
+ gtk_label_set_markup(GTK_LABEL(header_footer_label), pangoMarkup);
+ g_free(pangoMarkup);
+ gtk_misc_set_alignment(GTK_MISC(header_footer_label), 0, 0);
+
+ GtkWidget* header_footer_container = gtk_alignment_new(0, 0, 0, 0);
+ gtk_alignment_set_padding(GTK_ALIGNMENT(header_footer_container), 8, 0, 12,
+ 0);
+
+ // --- Table for making the header and footer options ---
+ GtkWidget* header_footer_table = gtk_table_new(3, 3, FALSE); // 3x3 table
+ nsString header_footer_str[3];
+
+ aSettings->GetHeaderStrLeft(header_footer_str[0]);
+ aSettings->GetHeaderStrCenter(header_footer_str[1]);
+ aSettings->GetHeaderStrRight(header_footer_str[2]);
+
+ for (unsigned int i = 0; i < ArrayLength(header_dropdown); i++) {
+ header_dropdown[i] =
+ ConstructHeaderFooterDropdown(header_footer_str[i].get());
+ // Those 4 magic numbers in the middle provide the position in the table.
+ // The last two numbers mean 2 px padding on every side.
+ gtk_table_attach(GTK_TABLE(header_footer_table), header_dropdown[i], i,
+ (i + 1), 0, 1, (GtkAttachOptions)0, (GtkAttachOptions)0, 2,
+ 2);
+ }
+
+ const char labelKeys[][7] = {"left", "center", "right"};
+ for (unsigned int i = 0; i < ArrayLength(labelKeys); i++) {
+ gtk_table_attach(GTK_TABLE(header_footer_table),
+ gtk_label_new(GetUTF8FromBundle(labelKeys[i]).get()), i,
+ (i + 1), 1, 2, (GtkAttachOptions)0, (GtkAttachOptions)0, 2,
+ 2);
+ }
+
+ aSettings->GetFooterStrLeft(header_footer_str[0]);
+ aSettings->GetFooterStrCenter(header_footer_str[1]);
+ aSettings->GetFooterStrRight(header_footer_str[2]);
+
+ for (unsigned int i = 0; i < ArrayLength(footer_dropdown); i++) {
+ footer_dropdown[i] =
+ ConstructHeaderFooterDropdown(header_footer_str[i].get());
+ gtk_table_attach(GTK_TABLE(header_footer_table), footer_dropdown[i], i,
+ (i + 1), 2, 3, (GtkAttachOptions)0, (GtkAttachOptions)0, 2,
+ 2);
+ }
+ // ---
+
+ gtk_container_add(GTK_CONTAINER(header_footer_container),
+ header_footer_table);
+
+ GtkWidget* header_footer_vertical_squasher = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(header_footer_vertical_squasher),
+ header_footer_label, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(header_footer_vertical_squasher),
+ header_footer_container, FALSE, FALSE, 0);
+
+ // Construction of everything
+ gtk_box_pack_start(GTK_BOX(custom_options_tab), check_buttons_container,
+ FALSE, FALSE, 10); // 10px padding
+ gtk_box_pack_start(GTK_BOX(custom_options_tab), appearance_vertical_squasher,
+ FALSE, FALSE, 10);
+ gtk_box_pack_start(GTK_BOX(custom_options_tab),
+ header_footer_vertical_squasher, FALSE, FALSE, 0);
+
+ gtk_print_unix_dialog_add_custom_tab(GTK_PRINT_UNIX_DIALOG(dialog),
+ custom_options_tab, tab_label);
+ gtk_widget_show_all(custom_options_tab);
+}
+
+NS_ConvertUTF16toUTF8 nsPrintDialogWidgetGTK::GetUTF8FromBundle(
+ const char* aKey) {
+ nsAutoString intlString;
+ printBundle->GetStringFromName(aKey, intlString);
+ return NS_ConvertUTF16toUTF8(
+ intlString); // Return the actual object so we don't lose reference
+}
+
+const char* nsPrintDialogWidgetGTK::OptionWidgetToString(GtkWidget* dropdown) {
+ gint index = gtk_combo_box_get_active(GTK_COMBO_BOX(dropdown));
+
+ NS_ASSERTION(index <= CUSTOM_VALUE_INDEX,
+ "Index of dropdown is higher than expected!");
+
+ if (index == CUSTOM_VALUE_INDEX)
+ return (const char*)g_object_get_data(G_OBJECT(dropdown), "custom-text");
+ return header_footer_tags[index];
+}
+
+gint nsPrintDialogWidgetGTK::Run() {
+ const gint response = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_hide(dialog);
+ return response;
+}
+
+void nsPrintDialogWidgetGTK::ExportHeaderFooter(nsIPrintSettings* aNS) {
+ const char* header_footer_str;
+ header_footer_str = OptionWidgetToString(header_dropdown[0]);
+ aNS->SetHeaderStrLeft(NS_ConvertUTF8toUTF16(header_footer_str));
+
+ header_footer_str = OptionWidgetToString(header_dropdown[1]);
+ aNS->SetHeaderStrCenter(NS_ConvertUTF8toUTF16(header_footer_str));
+
+ header_footer_str = OptionWidgetToString(header_dropdown[2]);
+ aNS->SetHeaderStrRight(NS_ConvertUTF8toUTF16(header_footer_str));
+
+ header_footer_str = OptionWidgetToString(footer_dropdown[0]);
+ aNS->SetFooterStrLeft(NS_ConvertUTF8toUTF16(header_footer_str));
+
+ header_footer_str = OptionWidgetToString(footer_dropdown[1]);
+ aNS->SetFooterStrCenter(NS_ConvertUTF8toUTF16(header_footer_str));
+
+ header_footer_str = OptionWidgetToString(footer_dropdown[2]);
+ aNS->SetFooterStrRight(NS_ConvertUTF8toUTF16(header_footer_str));
+}
+
+nsresult nsPrintDialogWidgetGTK::ImportSettings(nsIPrintSettings* aNSSettings) {
+ MOZ_ASSERT(aNSSettings, "aSettings must not be null");
+ NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsPrintSettingsGTK> aNSSettingsGTK(do_QueryInterface(aNSSettings));
+ if (!aNSSettingsGTK) return NS_ERROR_FAILURE;
+
+ GtkPrintSettings* settings = aNSSettingsGTK->GetGtkPrintSettings();
+ GtkPageSetup* setup = aNSSettingsGTK->GetGtkPageSetup();
+
+ // Set our custom fields:
+
+ bool geckoBool;
+ aNSSettings->GetShrinkToFit(&geckoBool);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(shrink_to_fit_toggle),
+ geckoBool);
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_bg_colors_toggle),
+ aNSSettings->GetPrintBGColors());
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(print_bg_images_toggle),
+ aNSSettings->GetPrintBGImages());
+
+ // Temporarily set the pages-per-sheet on the GtkPrintSettings:
+ int32_t pagesPerSide;
+ aNSSettings->GetNumPagesPerSheet(&pagesPerSide);
+ gtk_print_settings_set_number_up(settings, pagesPerSide);
+
+ gtk_print_unix_dialog_set_settings(GTK_PRINT_UNIX_DIALOG(dialog), settings);
+ gtk_print_unix_dialog_set_page_setup(GTK_PRINT_UNIX_DIALOG(dialog), setup);
+
+ return NS_OK;
+}
+
+nsresult nsPrintDialogWidgetGTK::ExportSettings(nsIPrintSettings* aNSSettings) {
+ MOZ_ASSERT(aNSSettings, "aSettings must not be null");
+ NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE);
+
+ GtkPrintSettings* settings =
+ gtk_print_unix_dialog_get_settings(GTK_PRINT_UNIX_DIALOG(dialog));
+ GtkPageSetup* setup =
+ gtk_print_unix_dialog_get_page_setup(GTK_PRINT_UNIX_DIALOG(dialog));
+ GtkPrinter* printer =
+ gtk_print_unix_dialog_get_selected_printer(GTK_PRINT_UNIX_DIALOG(dialog));
+ if (settings && setup && printer) {
+ ExportHeaderFooter(aNSSettings);
+
+ aNSSettings->SetOutputFormat(nsIPrintSettings::kOutputFormatNative);
+
+ // Print-to-file is true by default. This must be turned off or else
+ // printing won't occur! (We manually copy the spool file when this flag is
+ // set, because we love our embedders) Even if it is print-to-file in GTK's
+ // case, GTK does The Right Thing when we send the job.
+ aNSSettings->SetOutputDestination(
+ nsIPrintSettings::kOutputDestinationPrinter);
+
+ aNSSettings->SetShrinkToFit(
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(shrink_to_fit_toggle)));
+
+ aNSSettings->SetPrintBGColors(gtk_toggle_button_get_active(
+ GTK_TOGGLE_BUTTON(print_bg_colors_toggle)));
+ aNSSettings->SetPrintBGImages(gtk_toggle_button_get_active(
+ GTK_TOGGLE_BUTTON(print_bg_images_toggle)));
+
+ // Move any pages-per-sheet value from the GtkPrintSettings to our
+ // nsIPrintSettings. (We handle pages-per-sheet internally and don't want
+ // the native Linux printing code to doubly apply that value!)
+ int32_t pagesPerSide = gtk_print_settings_get_number_up(settings);
+ gtk_print_settings_set_number_up(settings, 1);
+ aNSSettings->SetNumPagesPerSheet(pagesPerSide);
+
+ // Try to save native settings in the session object
+ nsCOMPtr<nsPrintSettingsGTK> aNSSettingsGTK(do_QueryInterface(aNSSettings));
+ if (aNSSettingsGTK) {
+ aNSSettingsGTK->SetGtkPrintSettings(settings);
+ aNSSettingsGTK->SetGtkPageSetup(setup);
+ aNSSettingsGTK->SetGtkPrinter(printer);
+ bool printSelectionOnly;
+ if (useNativeSelection) {
+ _GtkPrintPages pageSetting =
+ (_GtkPrintPages)gtk_print_settings_get_print_pages(settings);
+ printSelectionOnly = (pageSetting == _GTK_PRINT_PAGES_SELECTION);
+ } else {
+ printSelectionOnly = gtk_toggle_button_get_active(
+ GTK_TOGGLE_BUTTON(selection_only_toggle));
+ }
+ aNSSettingsGTK->SetPrintSelectionOnly(printSelectionOnly);
+ }
+ }
+
+ if (settings) g_object_unref(settings);
+ return NS_OK;
+}
+
+GtkWidget* nsPrintDialogWidgetGTK::ConstructHeaderFooterDropdown(
+ const char16_t* currentString) {
+ GtkWidget* dropdown = gtk_combo_box_text_new();
+ const char hf_options[][22] = {"headerFooterBlank", "headerFooterTitle",
+ "headerFooterURL", "headerFooterDate",
+ "headerFooterPage", "headerFooterPageTotal",
+ "headerFooterCustom"};
+
+ for (unsigned int i = 0; i < ArrayLength(hf_options); i++) {
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(dropdown), nullptr,
+ GetUTF8FromBundle(hf_options[i]).get());
+ }
+
+ bool shouldBeCustom = true;
+ NS_ConvertUTF16toUTF8 currentStringUTF8(currentString);
+
+ for (unsigned int i = 0; i < ArrayLength(header_footer_tags); i++) {
+ if (!strcmp(currentStringUTF8.get(), header_footer_tags[i])) {
+ gtk_combo_box_set_active(GTK_COMBO_BOX(dropdown), i);
+ g_object_set_data(G_OBJECT(dropdown), "previous-active",
+ GINT_TO_POINTER(i));
+ shouldBeCustom = false;
+ break;
+ }
+ }
+
+ if (shouldBeCustom) {
+ gtk_combo_box_set_active(GTK_COMBO_BOX(dropdown), CUSTOM_VALUE_INDEX);
+ g_object_set_data(G_OBJECT(dropdown), "previous-active",
+ GINT_TO_POINTER(CUSTOM_VALUE_INDEX));
+ char* custom_string = strdup(currentStringUTF8.get());
+ g_object_set_data_full(G_OBJECT(dropdown), "custom-text", custom_string,
+ (GDestroyNotify)free);
+ }
+
+ g_signal_connect(dropdown, "changed", (GCallback)ShowCustomDialog, dialog);
+ return dropdown;
+}
+
+NS_IMPL_ISUPPORTS(nsPrintDialogServiceGTK, nsIPrintDialogService)
+
+nsPrintDialogServiceGTK::nsPrintDialogServiceGTK() = default;
+
+nsPrintDialogServiceGTK::~nsPrintDialogServiceGTK() = default;
+
+NS_IMETHODIMP
+nsPrintDialogServiceGTK::Init() { return NS_OK; }
+
+NS_IMETHODIMP
+nsPrintDialogServiceGTK::ShowPrintDialog(mozIDOMWindowProxy* aParent,
+ bool aHaveSelection,
+ nsIPrintSettings* aSettings) {
+ MOZ_ASSERT(aParent, "aParent must not be null");
+ MOZ_ASSERT(aSettings, "aSettings must not be null");
+
+ nsPrintDialogWidgetGTK printDialog(nsPIDOMWindowOuter::From(aParent),
+ aHaveSelection, aSettings);
+ nsresult rv = printDialog.ImportSettings(aSettings);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const gint response = printDialog.Run();
+
+ // Handle the result
+ switch (response) {
+ case GTK_RESPONSE_OK: // Proceed
+ rv = printDialog.ExportSettings(aSettings);
+ break;
+
+ case GTK_RESPONSE_CANCEL:
+ case GTK_RESPONSE_CLOSE:
+ case GTK_RESPONSE_DELETE_EVENT:
+ case GTK_RESPONSE_NONE:
+ rv = NS_ERROR_ABORT;
+ break;
+
+ case GTK_RESPONSE_APPLY: // Print preview
+ default:
+ NS_WARNING("Unexpected response");
+ rv = NS_ERROR_ABORT;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsPrintDialogServiceGTK::ShowPageSetupDialog(mozIDOMWindowProxy* aParent,
+ nsIPrintSettings* aNSSettings) {
+ MOZ_ASSERT(aParent, "aParent must not be null");
+ MOZ_ASSERT(aNSSettings, "aSettings must not be null");
+ NS_ENSURE_TRUE(aNSSettings, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIWidget> widget =
+ WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(aParent));
+ NS_ASSERTION(widget, "Need a widget for dialog to be modal.");
+ GtkWindow* gtkParent = get_gtk_window_for_nsiwidget(widget);
+ NS_ASSERTION(gtkParent, "Need a GTK window for dialog to be modal.");
+
+ nsCOMPtr<nsPrintSettingsGTK> aNSSettingsGTK(do_QueryInterface(aNSSettings));
+ if (!aNSSettingsGTK) return NS_ERROR_FAILURE;
+
+ // We need to init the prefs here because aNSSettings in its current form is a
+ // dummy in both uses of the word
+ nsCOMPtr<nsIPrintSettingsService> psService =
+ do_GetService("@mozilla.org/gfx/printsettings-service;1");
+ if (psService) {
+ nsString printName;
+ aNSSettings->GetPrinterName(printName);
+ if (printName.IsVoid()) {
+ psService->GetLastUsedPrinterName(printName);
+ aNSSettings->SetPrinterName(printName);
+ }
+ psService->InitPrintSettingsFromPrefs(aNSSettings, true,
+ nsIPrintSettings::kInitSaveAll);
+ }
+
+ // Frustratingly, gtk_print_run_page_setup_dialog doesn't tell us whether
+ // the user cancelled or confirmed the dialog! So to avoid needlessly
+ // refreshing the preview when Page Setup was cancelled, we compare the
+ // serializations of old and new settings; if they're the same, bail out.
+ GtkPrintSettings* gtkSettings = aNSSettingsGTK->GetGtkPrintSettings();
+ GtkPageSetup* oldPageSetup = aNSSettingsGTK->GetGtkPageSetup();
+ GKeyFile* oldKeyFile = g_key_file_new();
+ gtk_page_setup_to_key_file(oldPageSetup, oldKeyFile, nullptr);
+ gsize oldLength;
+ gchar* oldData = g_key_file_to_data(oldKeyFile, &oldLength, nullptr);
+ g_key_file_free(oldKeyFile);
+
+ GtkPageSetup* newPageSetup =
+ gtk_print_run_page_setup_dialog(gtkParent, oldPageSetup, gtkSettings);
+
+ GKeyFile* newKeyFile = g_key_file_new();
+ gtk_page_setup_to_key_file(newPageSetup, newKeyFile, nullptr);
+ gsize newLength;
+ gchar* newData = g_key_file_to_data(newKeyFile, &newLength, nullptr);
+ g_key_file_free(newKeyFile);
+
+ bool unchanged =
+ (oldLength == newLength && !memcmp(oldData, newData, oldLength));
+ g_free(oldData);
+ g_free(newData);
+ if (unchanged) {
+ g_object_unref(newPageSetup);
+ return NS_ERROR_ABORT;
+ }
+
+ aNSSettingsGTK->SetGtkPageSetup(newPageSetup);
+
+ // Now newPageSetup has a refcount of 2 (SetGtkPageSetup will addref), put it
+ // to 1 so if this gets replaced we don't leak.
+ g_object_unref(newPageSetup);
+
+ if (psService)
+ psService->MaybeSavePrintSettingsToPrefs(
+ aNSSettings, nsIPrintSettings::kInitSaveOrientation |
+ nsIPrintSettings::kInitSavePaperSize |
+ nsIPrintSettings::kInitSaveUnwriteableMargins);
+
+ return NS_OK;
+}
diff --git a/widget/gtk/nsPrintDialogGTK.h b/widget/gtk/nsPrintDialogGTK.h
new file mode 100644
index 0000000000..4388962858
--- /dev/null
+++ b/widget/gtk/nsPrintDialogGTK.h
@@ -0,0 +1,35 @@
+/* -*- 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/. */
+
+#ifndef nsPrintDialog_h__
+#define nsPrintDialog_h__
+
+#include "nsIPrintDialogService.h"
+
+class nsIPrintSettings;
+
+// Copy the print pages enum here because not all versions
+// have SELECTION, which we will use
+typedef enum {
+ _GTK_PRINT_PAGES_ALL,
+ _GTK_PRINT_PAGES_CURRENT,
+ _GTK_PRINT_PAGES_RANGES,
+ _GTK_PRINT_PAGES_SELECTION
+} _GtkPrintPages;
+
+class nsPrintDialogServiceGTK final : public nsIPrintDialogService {
+ virtual ~nsPrintDialogServiceGTK();
+
+ public:
+ nsPrintDialogServiceGTK();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRINTDIALOGSERVICE
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintDialogServiceGTK,
+ NS_IPRINTDIALOGSERVICE_IID)
+
+#endif
diff --git a/widget/gtk/nsPrintSettingsGTK.cpp b/widget/gtk/nsPrintSettingsGTK.cpp
new file mode 100644
index 0000000000..a101f44fcb
--- /dev/null
+++ b/widget/gtk/nsPrintSettingsGTK.cpp
@@ -0,0 +1,681 @@
+/* -*- 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 "nsPrintSettingsGTK.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include <stdlib.h>
+#include <algorithm>
+
+// These constants are the the strings that GTK expects as key-value pairs for
+// setting CUPS duplex modes. These are not universal to all CUPS systems, which
+// is why they are local to this file.
+static constexpr gchar kCupsDuplex[] = "cups-Duplex";
+static constexpr gchar kCupsDuplexNone[] = "None";
+static constexpr gchar kCupsDuplexNoTumble[] = "DuplexNoTumble";
+static constexpr gchar kCupsDuplexTumble[] = "DuplexTumble";
+
+static GtkPaperSize* moz_gtk_paper_size_copy_to_new_custom(
+ GtkPaperSize* oldPaperSize) {
+ // We make a "custom-ified" copy of the paper size so it can be changed later.
+ return gtk_paper_size_new_custom(
+ gtk_paper_size_get_name(oldPaperSize),
+ gtk_paper_size_get_display_name(oldPaperSize),
+ gtk_paper_size_get_width(oldPaperSize, GTK_UNIT_INCH),
+ gtk_paper_size_get_height(oldPaperSize, GTK_UNIT_INCH), GTK_UNIT_INCH);
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsGTK, nsPrintSettings,
+ nsPrintSettingsGTK)
+
+/** ---------------------------------------------------
+ */
+nsPrintSettingsGTK::nsPrintSettingsGTK()
+ : mPageSetup(nullptr), mPrintSettings(nullptr), mGTKPrinter(nullptr) {
+ // The aim here is to set up the objects enough that silent printing works
+ // well. These will be replaced anyway if the print dialog is used.
+ mPrintSettings = gtk_print_settings_new();
+ GtkPageSetup* pageSetup = gtk_page_setup_new();
+ SetGtkPageSetup(pageSetup);
+ g_object_unref(pageSetup);
+
+ SetOutputFormat(nsIPrintSettings::kOutputFormatNative);
+}
+
+already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings(
+ const mozilla::PrintSettingsInitializer& aSettings) {
+ RefPtr<nsPrintSettings> settings = new nsPrintSettingsGTK();
+ settings->InitWithInitializer(aSettings);
+ settings->SetDefaultFileName();
+ return settings.forget();
+}
+
+/** ---------------------------------------------------
+ */
+nsPrintSettingsGTK::~nsPrintSettingsGTK() {
+ if (mPageSetup) {
+ g_object_unref(mPageSetup);
+ mPageSetup = nullptr;
+ }
+ if (mPrintSettings) {
+ g_object_unref(mPrintSettings);
+ mPrintSettings = nullptr;
+ }
+ if (mGTKPrinter) {
+ g_object_unref(mGTKPrinter);
+ mGTKPrinter = nullptr;
+ }
+}
+
+/** ---------------------------------------------------
+ */
+nsPrintSettingsGTK::nsPrintSettingsGTK(const nsPrintSettingsGTK& aPS)
+ : mPageSetup(nullptr), mPrintSettings(nullptr), mGTKPrinter(nullptr) {
+ *this = aPS;
+}
+
+/** ---------------------------------------------------
+ */
+nsPrintSettingsGTK& nsPrintSettingsGTK::operator=(
+ const nsPrintSettingsGTK& rhs) {
+ if (this == &rhs) {
+ return *this;
+ }
+
+ nsPrintSettings::operator=(rhs);
+
+ if (mPageSetup) g_object_unref(mPageSetup);
+ mPageSetup = gtk_page_setup_copy(rhs.mPageSetup);
+ // NOTE: No need to re-initialize mUnwriteableMargin here (even
+ // though mPageSetup is changing). It'll be copied correctly by
+ // nsPrintSettings::operator=.
+
+ if (mPrintSettings) g_object_unref(mPrintSettings);
+ mPrintSettings = gtk_print_settings_copy(rhs.mPrintSettings);
+
+ if (mGTKPrinter) g_object_unref(mGTKPrinter);
+
+ if (rhs.mGTKPrinter) {
+ g_object_ref(rhs.mGTKPrinter);
+ }
+ mGTKPrinter = rhs.mGTKPrinter;
+
+ return *this;
+}
+
+/** -------------------------------------------
+ */
+nsresult nsPrintSettingsGTK::_Clone(nsIPrintSettings** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = nullptr;
+
+ nsPrintSettingsGTK* newSettings = new nsPrintSettingsGTK(*this);
+ if (!newSettings) return NS_ERROR_FAILURE;
+ *_retval = newSettings;
+ NS_ADDREF(*_retval);
+ return NS_OK;
+}
+
+/** -------------------------------------------
+ */
+NS_IMETHODIMP
+nsPrintSettingsGTK::_Assign(nsIPrintSettings* aPS) {
+ nsPrintSettingsGTK* printSettingsGTK = static_cast<nsPrintSettingsGTK*>(aPS);
+ if (!printSettingsGTK) return NS_ERROR_UNEXPECTED;
+ *this = *printSettingsGTK;
+ return NS_OK;
+}
+
+/** ---------------------------------------------------
+ */
+void nsPrintSettingsGTK::SetGtkPageSetup(GtkPageSetup* aPageSetup) {
+ if (mPageSetup) g_object_unref(mPageSetup);
+
+ mPageSetup = (GtkPageSetup*)g_object_ref(aPageSetup);
+ InitUnwriteableMargin();
+
+ // If the paper size is not custom, then we make a custom copy of the
+ // GtkPaperSize, so it can be mutable. If a GtkPaperSize wasn't made as
+ // custom, its properties are immutable.
+ GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(aPageSetup);
+ if (!gtk_paper_size_is_custom(paperSize)) {
+ GtkPaperSize* customPaperSize =
+ moz_gtk_paper_size_copy_to_new_custom(paperSize);
+ gtk_page_setup_set_paper_size(mPageSetup, customPaperSize);
+ gtk_paper_size_free(customPaperSize);
+ }
+ SaveNewPageSize();
+}
+
+/** ---------------------------------------------------
+ */
+void nsPrintSettingsGTK::SetGtkPrintSettings(GtkPrintSettings* aPrintSettings) {
+ if (mPrintSettings) g_object_unref(mPrintSettings);
+
+ mPrintSettings = (GtkPrintSettings*)g_object_ref(aPrintSettings);
+
+ GtkPaperSize* paperSize = gtk_print_settings_get_paper_size(aPrintSettings);
+ if (paperSize) {
+ GtkPaperSize* customPaperSize =
+ moz_gtk_paper_size_copy_to_new_custom(paperSize);
+ gtk_paper_size_free(paperSize);
+ gtk_page_setup_set_paper_size(mPageSetup, customPaperSize);
+ gtk_paper_size_free(customPaperSize);
+ } else {
+ // paperSize was null, and so we add the paper size in the GtkPageSetup to
+ // the settings.
+ SaveNewPageSize();
+ }
+}
+
+/** ---------------------------------------------------
+ */
+void nsPrintSettingsGTK::SetGtkPrinter(GtkPrinter* aPrinter) {
+ if (mGTKPrinter) g_object_unref(mGTKPrinter);
+
+ mGTKPrinter = (GtkPrinter*)g_object_ref(aPrinter);
+}
+
+NS_IMETHODIMP nsPrintSettingsGTK::GetOutputFormat(int16_t* aOutputFormat) {
+ NS_ENSURE_ARG_POINTER(aOutputFormat);
+
+ int16_t format;
+ nsresult rv = nsPrintSettings::GetOutputFormat(&format);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (format == nsIPrintSettings::kOutputFormatNative &&
+ GTK_IS_PRINTER(mGTKPrinter)) {
+ format = nsIPrintSettings::kOutputFormatPDF;
+ }
+
+ *aOutputFormat = format;
+ return NS_OK;
+}
+
+/**
+ * Reimplementation of nsPrintSettings functions so that we get the values
+ * from the GTK objects rather than our own variables.
+ */
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPageRanges(const nsTArray<int32_t>& aRanges) {
+ if (aRanges.Length() % 2 != 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ gtk_print_settings_set_print_pages(
+ mPrintSettings,
+ aRanges.IsEmpty() ? GTK_PRINT_PAGES_ALL : GTK_PRINT_PAGES_RANGES);
+
+ nsTArray<GtkPageRange> ranges;
+ ranges.SetCapacity(aRanges.Length() / 2);
+ for (size_t i = 0; i < aRanges.Length(); i += 2) {
+ GtkPageRange* gtkRange = ranges.AppendElement();
+ gtkRange->start = aRanges[i] - 1;
+ gtkRange->end = aRanges[i + 1] - 1;
+ }
+
+ gtk_print_settings_set_page_ranges(mPrintSettings, ranges.Elements(),
+ ranges.Length());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetPrintReversed(bool* aPrintReversed) {
+ *aPrintReversed = gtk_print_settings_get_reverse(mPrintSettings);
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPrintReversed(bool aPrintReversed) {
+ gtk_print_settings_set_reverse(mPrintSettings, aPrintReversed);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetPrintInColor(bool* aPrintInColor) {
+ *aPrintInColor = gtk_print_settings_get_use_color(mPrintSettings);
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPrintInColor(bool aPrintInColor) {
+ gtk_print_settings_set_use_color(mPrintSettings, aPrintInColor);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetOrientation(int32_t* aOrientation) {
+ NS_ENSURE_ARG_POINTER(aOrientation);
+
+ GtkPageOrientation gtkOrient = gtk_page_setup_get_orientation(mPageSetup);
+ switch (gtkOrient) {
+ case GTK_PAGE_ORIENTATION_LANDSCAPE:
+ case GTK_PAGE_ORIENTATION_REVERSE_LANDSCAPE:
+ *aOrientation = kLandscapeOrientation;
+ break;
+
+ case GTK_PAGE_ORIENTATION_PORTRAIT:
+ case GTK_PAGE_ORIENTATION_REVERSE_PORTRAIT:
+ default:
+ *aOrientation = kPortraitOrientation;
+ }
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetOrientation(int32_t aOrientation) {
+ GtkPageOrientation gtkOrient;
+ if (aOrientation == kLandscapeOrientation)
+ gtkOrient = GTK_PAGE_ORIENTATION_LANDSCAPE;
+ else
+ gtkOrient = GTK_PAGE_ORIENTATION_PORTRAIT;
+
+ gtk_print_settings_set_orientation(mPrintSettings, gtkOrient);
+ gtk_page_setup_set_orientation(mPageSetup, gtkOrient);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetToFileName(nsAString& aToFileName) {
+ // Get the gtk output filename
+ const char* gtk_output_uri =
+ gtk_print_settings_get(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_URI);
+ if (!gtk_output_uri) {
+ aToFileName = mToFileName;
+ return NS_OK;
+ }
+
+ // Convert to an nsIFile
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_GetFileFromURLSpec(nsDependentCString(gtk_output_uri),
+ getter_AddRefs(file));
+ if (NS_FAILED(rv)) return rv;
+
+ // Extract the path
+ return file->GetPath(aToFileName);
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetToFileName(const nsAString& aToFileName) {
+ if (aToFileName.IsEmpty()) {
+ mToFileName.SetLength(0);
+ gtk_print_settings_set(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_URI,
+ nullptr);
+ return NS_OK;
+ }
+
+ gtk_print_settings_set(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_FILE_FORMAT,
+ "pdf");
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(aToFileName, true, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Convert the nsIFile to a URL
+ nsAutoCString url;
+ rv = NS_GetURLSpecFromFile(file, url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ gtk_print_settings_set(mPrintSettings, GTK_PRINT_SETTINGS_OUTPUT_URI,
+ url.get());
+ mToFileName = aToFileName;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetPrinterName(nsAString& aPrinter) {
+ const char* gtkPrintName = gtk_print_settings_get_printer(mPrintSettings);
+ if (!gtkPrintName) {
+ if (GTK_IS_PRINTER(mGTKPrinter)) {
+ gtkPrintName = gtk_printer_get_name(mGTKPrinter);
+ } else {
+ // This mimics what nsPrintSettingsImpl does when we try to Get before we
+ // Set
+ aPrinter.Truncate();
+ return NS_OK;
+ }
+ }
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(gtkPrintName), aPrinter);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPrinterName(const nsAString& aPrinter) {
+ NS_ConvertUTF16toUTF8 gtkPrinter(aPrinter);
+
+ if (StringBeginsWith(gtkPrinter, "CUPS/"_ns)) {
+ // Strip off "CUPS/"; GTK might recognize the rest
+ gtkPrinter.Cut(0, strlen("CUPS/"));
+ }
+
+ // Give mPrintSettings the passed-in printer name if either...
+ // - it has no printer name stored yet
+ // - it has an existing printer name that's different from
+ // the name passed to this function.
+ const char* oldPrinterName = gtk_print_settings_get_printer(mPrintSettings);
+ if (!oldPrinterName || !gtkPrinter.Equals(oldPrinterName)) {
+ mIsInitedFromPrinter = false;
+ mIsInitedFromPrefs = false;
+ gtk_print_settings_set_printer(mPrintSettings, gtkPrinter.get());
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetNumCopies(int32_t* aNumCopies) {
+ NS_ENSURE_ARG_POINTER(aNumCopies);
+ *aNumCopies = gtk_print_settings_get_n_copies(mPrintSettings);
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetNumCopies(int32_t aNumCopies) {
+ gtk_print_settings_set_n_copies(mPrintSettings, aNumCopies);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetScaling(double* aScaling) {
+ *aScaling = gtk_print_settings_get_scale(mPrintSettings) / 100.0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetScaling(double aScaling) {
+ gtk_print_settings_set_scale(mPrintSettings, aScaling * 100.0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetPaperId(nsAString& aPaperId) {
+ const gchar* name =
+ gtk_paper_size_get_name(gtk_page_setup_get_paper_size(mPageSetup));
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(name), aPaperId);
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPaperId(const nsAString& aPaperId) {
+ NS_ConvertUTF16toUTF8 gtkPaperName(aPaperId);
+
+ // Convert these Gecko names to GTK names
+ // XXX (jfkthame): is this still relevant?
+ if (gtkPaperName.EqualsIgnoreCase("letter"))
+ gtkPaperName.AssignLiteral(GTK_PAPER_NAME_LETTER);
+ else if (gtkPaperName.EqualsIgnoreCase("legal"))
+ gtkPaperName.AssignLiteral(GTK_PAPER_NAME_LEGAL);
+
+ GtkPaperSize* oldPaperSize = gtk_page_setup_get_paper_size(mPageSetup);
+ gdouble width = gtk_paper_size_get_width(oldPaperSize, GTK_UNIT_INCH);
+ gdouble height = gtk_paper_size_get_height(oldPaperSize, GTK_UNIT_INCH);
+
+ // Try to get the display name from the name so our paper size fits in the
+ // Page Setup dialog.
+ GtkPaperSize* paperSize = gtk_paper_size_new(gtkPaperName.get());
+ GtkPaperSize* customPaperSize = gtk_paper_size_new_custom(
+ gtkPaperName.get(), gtk_paper_size_get_display_name(paperSize), width,
+ height, GTK_UNIT_INCH);
+ gtk_paper_size_free(paperSize);
+
+ gtk_page_setup_set_paper_size(mPageSetup, customPaperSize);
+ gtk_paper_size_free(customPaperSize);
+ SaveNewPageSize();
+ return NS_OK;
+}
+
+GtkUnit nsPrintSettingsGTK::GetGTKUnit(int16_t aGeckoUnit) {
+ if (aGeckoUnit == kPaperSizeMillimeters)
+ return GTK_UNIT_MM;
+ else
+ return GTK_UNIT_INCH;
+}
+
+void nsPrintSettingsGTK::SaveNewPageSize() {
+ gtk_print_settings_set_paper_size(mPrintSettings,
+ gtk_page_setup_get_paper_size(mPageSetup));
+}
+
+void nsPrintSettingsGTK::InitUnwriteableMargin() {
+ mUnwriteableMargin.SizeTo(
+ NS_INCHES_TO_INT_TWIPS(
+ gtk_page_setup_get_top_margin(mPageSetup, GTK_UNIT_INCH)),
+ NS_INCHES_TO_INT_TWIPS(
+ gtk_page_setup_get_right_margin(mPageSetup, GTK_UNIT_INCH)),
+ NS_INCHES_TO_INT_TWIPS(
+ gtk_page_setup_get_bottom_margin(mPageSetup, GTK_UNIT_INCH)),
+ NS_INCHES_TO_INT_TWIPS(
+ gtk_page_setup_get_left_margin(mPageSetup, GTK_UNIT_INCH)));
+}
+
+/**
+ * NOTE: Need a custom set of SetUnwriteableMargin functions, because
+ * whenever we change mUnwriteableMargin, we must pass the change
+ * down to our GTKPageSetup object. (This is needed in order for us
+ * to give the correct default values in nsPrintDialogGTK.)
+ *
+ * It's important that the following functions pass
+ * mUnwriteableMargin values rather than aUnwriteableMargin values
+ * to gtk_page_setup_set_[blank]_margin, because the two may not be
+ * the same. (Specifically, negative values of aUnwriteableMargin
+ * are ignored by the nsPrintSettings::SetUnwriteableMargin functions.)
+ */
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetUnwriteableMarginInTwips(
+ nsIntMargin& aUnwriteableMargin) {
+ nsPrintSettings::SetUnwriteableMarginInTwips(aUnwriteableMargin);
+ gtk_page_setup_set_top_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.top), GTK_UNIT_INCH);
+ gtk_page_setup_set_left_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.left), GTK_UNIT_INCH);
+ gtk_page_setup_set_bottom_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.bottom), GTK_UNIT_INCH);
+ gtk_page_setup_set_right_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.right), GTK_UNIT_INCH);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetUnwriteableMarginTop(double aUnwriteableMarginTop) {
+ nsPrintSettings::SetUnwriteableMarginTop(aUnwriteableMarginTop);
+ gtk_page_setup_set_top_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.top), GTK_UNIT_INCH);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetUnwriteableMarginLeft(double aUnwriteableMarginLeft) {
+ nsPrintSettings::SetUnwriteableMarginLeft(aUnwriteableMarginLeft);
+ gtk_page_setup_set_left_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.left), GTK_UNIT_INCH);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetUnwriteableMarginBottom(
+ double aUnwriteableMarginBottom) {
+ nsPrintSettings::SetUnwriteableMarginBottom(aUnwriteableMarginBottom);
+ gtk_page_setup_set_bottom_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.bottom), GTK_UNIT_INCH);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetUnwriteableMarginRight(double aUnwriteableMarginRight) {
+ nsPrintSettings::SetUnwriteableMarginRight(aUnwriteableMarginRight);
+ gtk_page_setup_set_right_margin(
+ mPageSetup, NS_TWIPS_TO_INCHES(mUnwriteableMargin.right), GTK_UNIT_INCH);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetPaperWidth(double* aPaperWidth) {
+ NS_ENSURE_ARG_POINTER(aPaperWidth);
+ GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup);
+ *aPaperWidth =
+ gtk_paper_size_get_width(paperSize, GetGTKUnit(mPaperSizeUnit));
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPaperWidth(double aPaperWidth) {
+ GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup);
+ gtk_paper_size_set_size(
+ paperSize, aPaperWidth,
+ gtk_paper_size_get_height(paperSize, GetGTKUnit(mPaperSizeUnit)),
+ GetGTKUnit(mPaperSizeUnit));
+ SaveNewPageSize();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetPaperHeight(double* aPaperHeight) {
+ NS_ENSURE_ARG_POINTER(aPaperHeight);
+ GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup);
+ *aPaperHeight =
+ gtk_paper_size_get_height(paperSize, GetGTKUnit(mPaperSizeUnit));
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPaperHeight(double aPaperHeight) {
+ GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup);
+ gtk_paper_size_set_size(
+ paperSize,
+ gtk_paper_size_get_width(paperSize, GetGTKUnit(mPaperSizeUnit)),
+ aPaperHeight, GetGTKUnit(mPaperSizeUnit));
+ SaveNewPageSize();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetPaperSizeUnit(int16_t aPaperSizeUnit) {
+ // Convert units internally. e.g. they might have set the values while we're
+ // still in mm but they change to inch just afterwards, expecting that their
+ // sizes are in inches.
+ GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup);
+ gtk_paper_size_set_size(
+ paperSize,
+ gtk_paper_size_get_width(paperSize, GetGTKUnit(mPaperSizeUnit)),
+ gtk_paper_size_get_height(paperSize, GetGTKUnit(mPaperSizeUnit)),
+ GetGTKUnit(aPaperSizeUnit));
+ SaveNewPageSize();
+
+ mPaperSizeUnit = aPaperSizeUnit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetEffectivePageSize(double* aWidth, double* aHeight) {
+ GtkPaperSize* paperSize = gtk_page_setup_get_paper_size(mPageSetup);
+ if (mPaperSizeUnit == kPaperSizeInches) {
+ *aWidth =
+ NS_INCHES_TO_TWIPS(gtk_paper_size_get_width(paperSize, GTK_UNIT_INCH));
+ *aHeight =
+ NS_INCHES_TO_TWIPS(gtk_paper_size_get_height(paperSize, GTK_UNIT_INCH));
+ } else {
+ MOZ_ASSERT(mPaperSizeUnit == kPaperSizeMillimeters,
+ "unexpected paper size unit");
+ *aWidth = NS_MILLIMETERS_TO_TWIPS(
+ gtk_paper_size_get_width(paperSize, GTK_UNIT_MM));
+ *aHeight = NS_MILLIMETERS_TO_TWIPS(
+ gtk_paper_size_get_height(paperSize, GTK_UNIT_MM));
+ }
+ GtkPageOrientation gtkOrient = gtk_page_setup_get_orientation(mPageSetup);
+
+ if (gtkOrient == GTK_PAGE_ORIENTATION_LANDSCAPE ||
+ gtkOrient == GTK_PAGE_ORIENTATION_REVERSE_LANDSCAPE) {
+ double temp = *aWidth;
+ *aWidth = *aHeight;
+ *aHeight = temp;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetPageRanges(nsTArray<int32_t>& aPages) {
+ GtkPrintPages gtkRange = gtk_print_settings_get_print_pages(mPrintSettings);
+ if (gtkRange != GTK_PRINT_PAGES_RANGES) {
+ aPages.Clear();
+ return NS_OK;
+ }
+
+ gint ctRanges;
+ GtkPageRange* lstRanges =
+ gtk_print_settings_get_page_ranges(mPrintSettings, &ctRanges);
+
+ aPages.Clear();
+
+ for (gint i = 0; i < ctRanges; i++) {
+ aPages.AppendElement(lstRanges[i].start + 1);
+ aPages.AppendElement(lstRanges[i].end + 1);
+ }
+
+ g_free(lstRanges);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetResolution(int32_t* aResolution) {
+ *aResolution = gtk_print_settings_get_resolution(mPrintSettings);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetResolution(int32_t aResolution) {
+ gtk_print_settings_set_resolution(mPrintSettings, aResolution);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::GetDuplex(int32_t* aDuplex) {
+ NS_ENSURE_ARG_POINTER(aDuplex);
+
+ // Default to DuplexNone.
+ *aDuplex = kDuplexNone;
+
+ if (!gtk_print_settings_has_key(mPrintSettings, GTK_PRINT_SETTINGS_DUPLEX)) {
+ return NS_OK;
+ }
+
+ switch (gtk_print_settings_get_duplex(mPrintSettings)) {
+ case GTK_PRINT_DUPLEX_SIMPLEX:
+ *aDuplex = kDuplexNone;
+ break;
+ case GTK_PRINT_DUPLEX_HORIZONTAL:
+ *aDuplex = kDuplexFlipOnLongEdge;
+ break;
+ case GTK_PRINT_DUPLEX_VERTICAL:
+ *aDuplex = kDuplexFlipOnShortEdge;
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsGTK::SetDuplex(int32_t aDuplex) {
+ uint32_t duplex = static_cast<uint32_t>(aDuplex);
+ MOZ_ASSERT(duplex <= kDuplexFlipOnShortEdge,
+ "value is out of bounds for duplex enum");
+
+ // We want to set the GTK CUPS Duplex setting in addition to calling
+ // gtk_print_settings_set_duplex(). Some systems may look for one, or the
+ // other, so it is best to set them both consistently.
+ switch (duplex) {
+ case kDuplexNone:
+ gtk_print_settings_set(mPrintSettings, kCupsDuplex, kCupsDuplexNone);
+ gtk_print_settings_set_duplex(mPrintSettings, GTK_PRINT_DUPLEX_SIMPLEX);
+ break;
+ case kDuplexFlipOnLongEdge:
+ gtk_print_settings_set(mPrintSettings, kCupsDuplex, kCupsDuplexNoTumble);
+ gtk_print_settings_set_duplex(mPrintSettings,
+ GTK_PRINT_DUPLEX_HORIZONTAL);
+ break;
+ case kDuplexFlipOnShortEdge:
+ gtk_print_settings_set(mPrintSettings, kCupsDuplex, kCupsDuplexTumble);
+ gtk_print_settings_set_duplex(mPrintSettings, GTK_PRINT_DUPLEX_VERTICAL);
+ break;
+ }
+
+ return NS_OK;
+}
diff --git a/widget/gtk/nsPrintSettingsGTK.h b/widget/gtk/nsPrintSettingsGTK.h
new file mode 100644
index 0000000000..c2d97a6208
--- /dev/null
+++ b/widget/gtk/nsPrintSettingsGTK.h
@@ -0,0 +1,147 @@
+/* -*- Mode: IDL; tab-width: 4; 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/. */
+
+#ifndef nsPrintSettingsGTK_h_
+#define nsPrintSettingsGTK_h_
+
+#include "nsPrintSettingsImpl.h"
+
+extern "C" {
+#include <gtk/gtk.h>
+#include <gtk/gtkunixprint.h>
+}
+
+#define NS_PRINTSETTINGSGTK_IID \
+ { \
+ 0x758df520, 0xc7c3, 0x11dc, { \
+ 0x95, 0xff, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 \
+ } \
+ }
+
+//*****************************************************************************
+//*** nsPrintSettingsGTK
+//*****************************************************************************
+
+class nsPrintSettingsGTK : public nsPrintSettings {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_PRINTSETTINGSGTK_IID)
+
+ nsPrintSettingsGTK();
+ explicit nsPrintSettingsGTK(const PrintSettingsInitializer& aSettings);
+
+ static nsPrintSettingsGTK* From(nsIPrintSettings* aPrintSettings) {
+ return static_cast<nsPrintSettingsGTK*>(aPrintSettings);
+ }
+
+ // We're overriding these methods because we want to read/write with GTK
+ // objects, not local variables. This allows a simpler settings implementation
+ // between Gecko and GTK.
+
+ GtkPageSetup* GetGtkPageSetup() { return mPageSetup; };
+ void SetGtkPageSetup(GtkPageSetup* aPageSetup);
+
+ GtkPrintSettings* GetGtkPrintSettings() { return mPrintSettings; };
+ void SetGtkPrintSettings(GtkPrintSettings* aPrintSettings);
+
+ GtkPrinter* GetGtkPrinter() { return mGTKPrinter; };
+ void SetGtkPrinter(GtkPrinter* aPrinter);
+
+ // Reversed, color, orientation and file name are all stored in the
+ // GtkPrintSettings. Orientation is also stored in the GtkPageSetup and its
+ // setting takes priority when getting the orientation.
+ NS_IMETHOD GetPrintReversed(bool* aPrintReversed) override;
+ NS_IMETHOD SetPrintReversed(bool aPrintReversed) override;
+
+ NS_IMETHOD GetPrintInColor(bool* aPrintInColor) override;
+ NS_IMETHOD SetPrintInColor(bool aPrintInColor) override;
+
+ NS_IMETHOD GetOrientation(int32_t* aOrientation) override;
+ NS_IMETHOD SetOrientation(int32_t aOrientation) override;
+
+ NS_IMETHOD GetToFileName(nsAString& aToFileName) override;
+ NS_IMETHOD SetToFileName(const nsAString& aToFileName) override;
+
+ // Gets/Sets the printer name in the GtkPrintSettings. If no printer name is
+ // specified there, you will get back the name of the current internal
+ // GtkPrinter.
+ NS_IMETHOD GetPrinterName(nsAString& Printer) override;
+ NS_IMETHOD SetPrinterName(const nsAString& aPrinter) override;
+
+ // Number of copies is stored/gotten from the GtkPrintSettings.
+ NS_IMETHOD GetNumCopies(int32_t* aNumCopies) override;
+ NS_IMETHOD SetNumCopies(int32_t aNumCopies) override;
+
+ NS_IMETHOD GetScaling(double* aScaling) override;
+ NS_IMETHOD SetScaling(double aScaling) override;
+
+ // A name recognised by GTK is strongly advised here, as this is used to
+ // create a GtkPaperSize.
+ NS_IMETHOD GetPaperId(nsAString& aPaperId) override;
+ NS_IMETHOD SetPaperId(const nsAString& aPaperId) override;
+
+ NS_IMETHOD SetUnwriteableMarginInTwips(
+ nsIntMargin& aUnwriteableMargin) override;
+ NS_IMETHOD SetUnwriteableMarginTop(double aUnwriteableMarginTop) override;
+ NS_IMETHOD SetUnwriteableMarginLeft(double aUnwriteableMarginLeft) override;
+ NS_IMETHOD SetUnwriteableMarginBottom(
+ double aUnwriteableMarginBottom) override;
+ NS_IMETHOD SetUnwriteableMarginRight(double aUnwriteableMarginRight) override;
+
+ NS_IMETHOD GetPaperWidth(double* aPaperWidth) override;
+ NS_IMETHOD SetPaperWidth(double aPaperWidth) override;
+
+ NS_IMETHOD GetPaperHeight(double* aPaperHeight) override;
+ NS_IMETHOD SetPaperHeight(double aPaperHeight) override;
+
+ NS_IMETHOD SetPaperSizeUnit(int16_t aPaperSizeUnit) override;
+
+ NS_IMETHOD GetEffectivePageSize(double* aWidth, double* aHeight) override;
+
+ NS_IMETHOD SetPageRanges(const nsTArray<int32_t>&) override;
+ NS_IMETHOD GetPageRanges(nsTArray<int32_t>&) override;
+
+ NS_IMETHOD GetResolution(int32_t* aResolution) override;
+ NS_IMETHOD SetResolution(int32_t aResolution) override;
+
+ NS_IMETHOD GetDuplex(int32_t* aDuplex) override;
+ NS_IMETHOD SetDuplex(int32_t aDuplex) override;
+
+ NS_IMETHOD GetOutputFormat(int16_t* aOutputFormat) override;
+
+ protected:
+ virtual ~nsPrintSettingsGTK();
+
+ nsPrintSettingsGTK(const nsPrintSettingsGTK& src);
+ nsPrintSettingsGTK& operator=(const nsPrintSettingsGTK& rhs);
+
+ virtual nsresult _Clone(nsIPrintSettings** _retval) override;
+ virtual nsresult _Assign(nsIPrintSettings* aPS) override;
+
+ GtkUnit GetGTKUnit(int16_t aGeckoUnit);
+ void SaveNewPageSize();
+
+ /**
+ * Re-initialize mUnwriteableMargin with values from mPageSetup.
+ * Should be called whenever mPageSetup is initialized or overwritten.
+ */
+ void InitUnwriteableMargin();
+
+ /**
+ * On construction:
+ * - mPrintSettings and mPageSetup are just new objects with defaults
+ * determined by GTK.
+ * - mGTKPrinter is nullptr!!! Remember to be careful when accessing this
+ * property.
+ */
+ GtkPageSetup* mPageSetup;
+ GtkPrintSettings* mPrintSettings;
+ GtkPrinter* mGTKPrinter;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintSettingsGTK, NS_PRINTSETTINGSGTK_IID)
+
+#endif // nsPrintSettingsGTK_h_
diff --git a/widget/gtk/nsPrintSettingsServiceGTK.cpp b/widget/gtk/nsPrintSettingsServiceGTK.cpp
new file mode 100644
index 0000000000..9d0dfc9379
--- /dev/null
+++ b/widget/gtk/nsPrintSettingsServiceGTK.cpp
@@ -0,0 +1,80 @@
+/* -*- 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 "nsPrintSettingsServiceGTK.h"
+
+#include "mozilla/embedding/PPrintingTypes.h"
+#include "nsPrintSettingsGTK.h"
+
+using namespace mozilla::embedding;
+
+static void serialize_gtk_printsettings_to_printdata(const gchar* key,
+ const gchar* value,
+ gpointer aData) {
+ PrintData* data = (PrintData*)aData;
+ CStringKeyValue pair;
+ pair.key() = key;
+ pair.value() = value;
+ data->GTKPrintSettings().AppendElement(pair);
+}
+
+NS_IMETHODIMP
+nsPrintSettingsServiceGTK::SerializeToPrintData(nsIPrintSettings* aSettings,
+ PrintData* data) {
+ nsresult rv = nsPrintSettingsService::SerializeToPrintData(aSettings, data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsPrintSettingsGTK> settingsGTK(do_QueryInterface(aSettings));
+ NS_ENSURE_STATE(settingsGTK);
+
+ GtkPrintSettings* gtkPrintSettings = settingsGTK->GetGtkPrintSettings();
+ NS_ENSURE_STATE(gtkPrintSettings);
+
+ gtk_print_settings_foreach(gtkPrintSettings,
+ serialize_gtk_printsettings_to_printdata, data);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsServiceGTK::DeserializeToPrintSettings(
+ const PrintData& data, nsIPrintSettings* settings) {
+ nsCOMPtr<nsPrintSettingsGTK> settingsGTK(do_QueryInterface(settings));
+ NS_ENSURE_STATE(settingsGTK);
+
+ nsresult rv =
+ nsPrintSettingsService::DeserializeToPrintSettings(data, settings);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Instead of re-using the GtkPrintSettings that nsIPrintSettings is
+ // wrapping, we'll create a new one to deserialize to and replace it
+ // within nsIPrintSettings.
+ GtkPrintSettings* newGtkPrintSettings = gtk_print_settings_new();
+
+ for (uint32_t i = 0; i < data.GTKPrintSettings().Length(); ++i) {
+ CStringKeyValue pair = data.GTKPrintSettings()[i];
+ gtk_print_settings_set(newGtkPrintSettings, pair.key().get(),
+ pair.value().get());
+ }
+
+ settingsGTK->SetGtkPrintSettings(newGtkPrintSettings);
+
+ // nsPrintSettingsGTK is holding a reference to newGtkPrintSettings
+ g_object_unref(newGtkPrintSettings);
+ newGtkPrintSettings = nullptr;
+ return NS_OK;
+}
+
+nsresult nsPrintSettingsServiceGTK::_CreatePrintSettings(
+ nsIPrintSettings** _retval) {
+ *_retval = nullptr;
+ nsPrintSettingsGTK* printSettings =
+ new nsPrintSettingsGTK(); // does not initially ref count
+ NS_ENSURE_TRUE(printSettings, NS_ERROR_OUT_OF_MEMORY);
+
+ NS_ADDREF(*_retval = printSettings); // ref count
+
+ return NS_OK;
+}
diff --git a/widget/gtk/nsPrintSettingsServiceGTK.h b/widget/gtk/nsPrintSettingsServiceGTK.h
new file mode 100644
index 0000000000..d26f543110
--- /dev/null
+++ b/widget/gtk/nsPrintSettingsServiceGTK.h
@@ -0,0 +1,33 @@
+/* -*- Mode: IDL; tab-width: 4; 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/. */
+
+#ifndef nsPrintSettingsServiceGTK_h
+#define nsPrintSettingsServiceGTK_h
+
+#include "nsPrintSettingsService.h"
+
+namespace mozilla {
+namespace embedding {
+class PrintData;
+} // namespace embedding
+} // namespace mozilla
+
+class nsPrintSettingsServiceGTK final : public nsPrintSettingsService {
+ public:
+ nsPrintSettingsServiceGTK() = default;
+
+ NS_IMETHODIMP SerializeToPrintData(
+ nsIPrintSettings* aSettings,
+ mozilla::embedding::PrintData* data) override;
+
+ NS_IMETHODIMP DeserializeToPrintSettings(
+ const mozilla::embedding::PrintData& data,
+ nsIPrintSettings* settings) override;
+
+ virtual nsresult _CreatePrintSettings(nsIPrintSettings** _retval) override;
+};
+
+#endif // nsPrintSettingsServiceGTK_h
diff --git a/widget/gtk/nsShmImage.cpp b/widget/gtk/nsShmImage.cpp
new file mode 100644
index 0000000000..7208b7075f
--- /dev/null
+++ b/widget/gtk/nsShmImage.cpp
@@ -0,0 +1,326 @@
+/* -*- 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 "nsShmImage.h"
+
+#ifdef MOZ_HAVE_SHMIMAGE
+# include "mozilla/X11Util.h"
+# include "mozilla/gfx/gfxVars.h"
+# include "mozilla/ipc/SharedMemory.h"
+# include "gfxPlatform.h"
+# include "nsPrintfCString.h"
+# include "nsTArray.h"
+
+# include <dlfcn.h>
+# include <errno.h>
+# include <string.h>
+# include <sys/ipc.h>
+# include <sys/shm.h>
+
+extern "C" {
+# include <X11/ImUtil.h>
+}
+
+using namespace mozilla::ipc;
+using namespace mozilla::gfx;
+
+nsShmImage::nsShmImage(Display* aDisplay, Drawable aWindow, Visual* aVisual,
+ unsigned int aDepth)
+ : mDisplay(aDisplay),
+ mConnection(XGetXCBConnection(aDisplay)),
+ mWindow(aWindow),
+ mVisual(aVisual),
+ mDepth(aDepth),
+ mFormat(mozilla::gfx::SurfaceFormat::UNKNOWN),
+ mSize(0, 0),
+ mStride(0),
+ mPixmap(XCB_NONE),
+ mGC(XCB_NONE),
+ mRequestPending(false),
+ mShmSeg(XCB_NONE),
+ mShmId(-1),
+ mShmAddr(nullptr) {
+ mozilla::PodZero(&mSyncRequest);
+}
+
+nsShmImage::~nsShmImage() { DestroyImage(); }
+
+// If XShm isn't available to our client, we'll try XShm once, fail,
+// set this to false and then never try again.
+static bool gShmAvailable = true;
+bool nsShmImage::UseShm() { return gShmAvailable; }
+
+bool nsShmImage::CreateShmSegment() {
+ size_t size = SharedMemory::PageAlignedSize(mStride * mSize.height);
+
+# if defined(__OpenBSD__) && defined(MOZ_SANDBOX)
+ static mozilla::LazyLogModule sPledgeLog("SandboxPledge");
+ MOZ_LOG(sPledgeLog, mozilla::LogLevel::Debug,
+ ("%s called when pledged, returning false\n", __func__));
+ return false;
+# endif
+ mShmId = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
+ if (mShmId == -1) {
+ return false;
+ }
+ mShmAddr = (uint8_t*)shmat(mShmId, nullptr, 0);
+ mShmSeg = xcb_generate_id(mConnection);
+
+ // Mark the handle removed so that it will destroy the segment when unmapped.
+ shmctl(mShmId, IPC_RMID, nullptr);
+
+ if (mShmAddr == (void*)-1) {
+ // Since mapping failed, the segment is already destroyed.
+ mShmId = -1;
+
+ nsPrintfCString warning("shmat(): %s (%d)\n", strerror(errno), errno);
+ NS_WARNING(warning.get());
+ return false;
+ }
+
+# ifdef DEBUG
+ struct shmid_ds info;
+ if (shmctl(mShmId, IPC_STAT, &info) < 0) {
+ return false;
+ }
+
+ MOZ_ASSERT(size <= info.shm_segsz, "Segment doesn't have enough space!");
+# endif
+
+ return true;
+}
+
+void nsShmImage::DestroyShmSegment() {
+ if (mShmId != -1) {
+ shmdt(mShmAddr);
+ mShmId = -1;
+ }
+}
+
+static bool gShmInitialized = false;
+static bool gUseShmPixmaps = false;
+
+bool nsShmImage::InitExtension() {
+ if (gShmInitialized) {
+ return gShmAvailable;
+ }
+
+ gShmInitialized = true;
+
+ // Bugs 1397918, 1293474 - race condition in libxcb fixed upstream as of
+ // version 1.11. Since we can't query libxcb's version directly, the only
+ // other option is to check for symbols that were added after 1.11.
+ // xcb_discard_reply64 was added in 1.11.1, so check for existence of
+ // that to verify we are using a version of libxcb with the bug fixed.
+ // Otherwise, we can't risk using libxcb due to aforementioned crashes.
+ if (!dlsym(RTLD_DEFAULT, "xcb_discard_reply64")) {
+ gShmAvailable = false;
+ return false;
+ }
+
+ const xcb_query_extension_reply_t* extReply;
+ extReply = xcb_get_extension_data(mConnection, &xcb_shm_id);
+ if (!extReply || !extReply->present) {
+ gShmAvailable = false;
+ return false;
+ }
+
+ xcb_shm_query_version_reply_t* shmReply = xcb_shm_query_version_reply(
+ mConnection, xcb_shm_query_version(mConnection), nullptr);
+
+ if (!shmReply) {
+ gShmAvailable = false;
+ return false;
+ }
+
+ gUseShmPixmaps = shmReply->shared_pixmaps &&
+ shmReply->pixmap_format == XCB_IMAGE_FORMAT_Z_PIXMAP;
+
+ free(shmReply);
+
+ return true;
+}
+
+bool nsShmImage::CreateImage(const IntSize& aSize) {
+ MOZ_ASSERT(mConnection && mVisual);
+
+ if (!InitExtension()) {
+ return false;
+ }
+
+ mSize = aSize;
+
+ BackendType backend = gfxVars::ContentBackend();
+
+ mFormat = SurfaceFormat::UNKNOWN;
+ switch (mDepth) {
+ case 32:
+ if (mVisual->red_mask == 0xff0000 && mVisual->green_mask == 0xff00 &&
+ mVisual->blue_mask == 0xff) {
+ mFormat = SurfaceFormat::B8G8R8A8;
+ }
+ break;
+ case 24:
+ // Only support the BGRX layout, and report it as BGRA to the compositor.
+ // The alpha channel will be discarded when we put the image.
+ // Cairo/pixman lacks some fast paths for compositing BGRX onto BGRA, so
+ // just report it as BGRX directly in that case.
+ if (mVisual->red_mask == 0xff0000 && mVisual->green_mask == 0xff00 &&
+ mVisual->blue_mask == 0xff) {
+ mFormat = backend == BackendType::CAIRO ? SurfaceFormat::B8G8R8X8
+ : SurfaceFormat::B8G8R8A8;
+ }
+ break;
+ case 16:
+ if (mVisual->red_mask == 0xf800 && mVisual->green_mask == 0x07e0 &&
+ mVisual->blue_mask == 0x1f) {
+ mFormat = SurfaceFormat::R5G6B5_UINT16;
+ }
+ break;
+ }
+
+ if (mFormat == SurfaceFormat::UNKNOWN) {
+ NS_WARNING("Unsupported XShm Image format!");
+ gShmAvailable = false;
+ return false;
+ }
+
+ // Round up stride to the display's scanline pad (in bits) as XShm expects.
+ int scanlinePad = _XGetScanlinePad(mDisplay, mDepth);
+ int bitsPerPixel = _XGetBitsPerPixel(mDisplay, mDepth);
+ int bitsPerLine =
+ ((bitsPerPixel * aSize.width + scanlinePad - 1) / scanlinePad) *
+ scanlinePad;
+ mStride = bitsPerLine / 8;
+
+ if (!CreateShmSegment()) {
+ DestroyImage();
+ return false;
+ }
+
+ xcb_generic_error_t* error;
+ xcb_void_cookie_t cookie;
+
+ cookie = xcb_shm_attach_checked(mConnection, mShmSeg, mShmId, 0);
+
+ if ((error = xcb_request_check(mConnection, cookie))) {
+ NS_WARNING("Failed to attach MIT-SHM segment.");
+ DestroyImage();
+ gShmAvailable = false;
+ free(error);
+ return false;
+ }
+
+ if (gUseShmPixmaps) {
+ mPixmap = xcb_generate_id(mConnection);
+ cookie = xcb_shm_create_pixmap_checked(mConnection, mPixmap, mWindow,
+ aSize.width, aSize.height, mDepth,
+ mShmSeg, 0);
+
+ if ((error = xcb_request_check(mConnection, cookie))) {
+ // Disable shared pixmaps permanently if creation failed.
+ mPixmap = XCB_NONE;
+ gUseShmPixmaps = false;
+ free(error);
+ }
+ }
+
+ return true;
+}
+
+void nsShmImage::DestroyImage() {
+ if (mGC) {
+ xcb_free_gc(mConnection, mGC);
+ mGC = XCB_NONE;
+ }
+ if (mPixmap != XCB_NONE) {
+ xcb_free_pixmap(mConnection, mPixmap);
+ mPixmap = XCB_NONE;
+ }
+ if (mShmSeg != XCB_NONE) {
+ xcb_shm_detach_checked(mConnection, mShmSeg);
+ mShmSeg = XCB_NONE;
+ }
+ DestroyShmSegment();
+ // Avoid leaking any pending reply. No real need to wait but CentOS 6 build
+ // machines don't have xcb_discard_reply().
+ WaitIfPendingReply();
+}
+
+// Wait for any in-flight shm-affected requests to complete.
+// Typically X clients would wait for a XShmCompletionEvent to be received,
+// but this works as it's sent immediately after the request is sent.
+void nsShmImage::WaitIfPendingReply() {
+ if (mRequestPending) {
+ xcb_get_input_focus_reply_t* reply =
+ xcb_get_input_focus_reply(mConnection, mSyncRequest, nullptr);
+ free(reply);
+ mRequestPending = false;
+ }
+}
+
+already_AddRefed<DrawTarget> nsShmImage::CreateDrawTarget(
+ const mozilla::LayoutDeviceIntRegion& aRegion) {
+ WaitIfPendingReply();
+
+ // Due to bug 1205045, we must avoid making GTK calls off the main thread to
+ // query window size. Instead we just track the largest offset within the
+ // image we are drawing to and grow the image to accomodate it. Since usually
+ // the entire window is invalidated on the first paint to it, this should grow
+ // the image to the necessary size quickly without many intermediate
+ // reallocations.
+ IntRect bounds = aRegion.GetBounds().ToUnknownRect();
+ IntSize size(bounds.XMost(), bounds.YMost());
+ if (size.width > mSize.width || size.height > mSize.height) {
+ DestroyImage();
+ if (!CreateImage(size)) {
+ return nullptr;
+ }
+ }
+
+ return gfxPlatform::CreateDrawTargetForData(
+ reinterpret_cast<unsigned char*>(mShmAddr) + bounds.y * mStride +
+ bounds.x * BytesPerPixel(mFormat),
+ bounds.Size(), mStride, mFormat);
+}
+
+void nsShmImage::Put(const mozilla::LayoutDeviceIntRegion& aRegion) {
+ AutoTArray<xcb_rectangle_t, 32> xrects;
+ xrects.SetCapacity(aRegion.GetNumRects());
+
+ for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) {
+ const mozilla::LayoutDeviceIntRect& r = iter.Get();
+ xcb_rectangle_t xrect = {(short)r.x, (short)r.y, (unsigned short)r.width,
+ (unsigned short)r.height};
+ xrects.AppendElement(xrect);
+ }
+
+ if (!mGC) {
+ mGC = xcb_generate_id(mConnection);
+ xcb_create_gc(mConnection, mGC, mWindow, 0, nullptr);
+ }
+
+ xcb_set_clip_rectangles(mConnection, XCB_CLIP_ORDERING_YX_BANDED, mGC, 0, 0,
+ xrects.Length(), xrects.Elements());
+
+ if (mPixmap != XCB_NONE) {
+ xcb_copy_area(mConnection, mPixmap, mWindow, mGC, 0, 0, 0, 0, mSize.width,
+ mSize.height);
+ } else {
+ xcb_shm_put_image(mConnection, mWindow, mGC, mSize.width, mSize.height, 0,
+ 0, mSize.width, mSize.height, 0, 0, mDepth,
+ XCB_IMAGE_FORMAT_Z_PIXMAP, 0, mShmSeg, 0);
+ }
+
+ // Send a request that returns a response so that we don't have to start a
+ // sync in nsShmImage::CreateDrawTarget.
+ mSyncRequest = xcb_get_input_focus(mConnection);
+ mRequestPending = true;
+
+ xcb_flush(mConnection);
+}
+
+#endif // MOZ_HAVE_SHMIMAGE
diff --git a/widget/gtk/nsShmImage.h b/widget/gtk/nsShmImage.h
new file mode 100644
index 0000000000..2d92c39e0d
--- /dev/null
+++ b/widget/gtk/nsShmImage.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef __mozilla_widget_nsShmImage_h__
+#define __mozilla_widget_nsShmImage_h__
+
+#if defined(MOZ_X11)
+# define MOZ_HAVE_SHMIMAGE
+#endif
+
+#ifdef MOZ_HAVE_SHMIMAGE
+
+# include "mozilla/gfx/2D.h"
+# include "nsIWidget.h"
+# include "Units.h"
+
+# include <X11/Xlib-xcb.h>
+# include <xcb/shm.h>
+
+class nsShmImage {
+ // bug 1168843, compositor thread may create shared memory instances that are
+ // destroyed by main thread on shutdown, so this must use thread-safe RC to
+ // avoid hitting assertion
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsShmImage)
+
+ public:
+ static bool UseShm();
+
+ already_AddRefed<mozilla::gfx::DrawTarget> CreateDrawTarget(
+ const mozilla::LayoutDeviceIntRegion& aRegion);
+
+ void Put(const mozilla::LayoutDeviceIntRegion& aRegion);
+
+ nsShmImage(Display* aDisplay, Drawable aWindow, Visual* aVisual,
+ unsigned int aDepth);
+
+ private:
+ ~nsShmImage();
+
+ bool InitExtension();
+
+ bool CreateShmSegment();
+ void DestroyShmSegment();
+
+ bool CreateImage(const mozilla::gfx::IntSize& aSize);
+ void DestroyImage();
+
+ void WaitIfPendingReply();
+
+ Display* mDisplay;
+ xcb_connection_t* mConnection;
+ Window mWindow;
+ Visual* mVisual;
+ unsigned int mDepth;
+
+ mozilla::gfx::SurfaceFormat mFormat;
+ mozilla::gfx::IntSize mSize;
+ int mStride;
+
+ xcb_pixmap_t mPixmap;
+ xcb_gcontext_t mGC;
+ xcb_get_input_focus_cookie_t mSyncRequest;
+ bool mRequestPending;
+
+ xcb_shm_seg_t mShmSeg;
+ int mShmId;
+ uint8_t* mShmAddr;
+};
+
+#endif // MOZ_HAVE_SHMIMAGE
+
+#endif
diff --git a/widget/gtk/nsSound.cpp b/widget/gtk/nsSound.cpp
new file mode 100644
index 0000000000..0530c73224
--- /dev/null
+++ b/widget/gtk/nsSound.cpp
@@ -0,0 +1,397 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 <string.h>
+
+#include "nscore.h"
+#include "prlink.h"
+
+#include "nsSound.h"
+
+#include "HeadlessSound.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "nsIChannel.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsDirectoryService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/Unused.h"
+#include "mozilla/WidgetUtils.h"
+#include "nsIXULAppInfo.h"
+#include "nsContentUtils.h"
+#include "gfxPlatform.h"
+#include "mozilla/ClearOnShutdown.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include <gtk/gtk.h>
+static PRLibrary* libcanberra = nullptr;
+
+/* used to play sounds with libcanberra. */
+typedef struct _ca_context ca_context;
+typedef struct _ca_proplist ca_proplist;
+
+typedef void (*ca_finish_callback_t)(ca_context* c, uint32_t id, int error_code,
+ void* userdata);
+
+typedef int (*ca_context_create_fn)(ca_context**);
+typedef int (*ca_context_destroy_fn)(ca_context*);
+typedef int (*ca_context_play_fn)(ca_context* c, uint32_t id, ...);
+typedef int (*ca_context_change_props_fn)(ca_context* c, ...);
+typedef int (*ca_proplist_create_fn)(ca_proplist**);
+typedef int (*ca_proplist_destroy_fn)(ca_proplist*);
+typedef int (*ca_proplist_sets_fn)(ca_proplist* c, const char* key,
+ const char* value);
+typedef int (*ca_context_play_full_fn)(ca_context* c, uint32_t id,
+ ca_proplist* p, ca_finish_callback_t cb,
+ void* userdata);
+
+static ca_context_create_fn ca_context_create;
+static ca_context_destroy_fn ca_context_destroy;
+static ca_context_play_fn ca_context_play;
+static ca_context_change_props_fn ca_context_change_props;
+static ca_proplist_create_fn ca_proplist_create;
+static ca_proplist_destroy_fn ca_proplist_destroy;
+static ca_proplist_sets_fn ca_proplist_sets;
+static ca_context_play_full_fn ca_context_play_full;
+
+struct ScopedCanberraFile {
+ explicit ScopedCanberraFile(nsIFile* file) : mFile(file){};
+
+ ~ScopedCanberraFile() {
+ if (mFile) {
+ mFile->Remove(false);
+ }
+ }
+
+ void forget() { mozilla::Unused << mFile.forget(); }
+ nsIFile* operator->() { return mFile; }
+ operator nsIFile*() { return mFile; }
+
+ nsCOMPtr<nsIFile> mFile;
+};
+
+static ca_context* ca_context_get_default() {
+ // This allows us to avoid race conditions with freeing the context by handing
+ // that responsibility to Glib, and still use one context at a time
+ static GPrivate ctx_private =
+ G_PRIVATE_INIT((GDestroyNotify)ca_context_destroy);
+
+ ca_context* ctx = (ca_context*)g_private_get(&ctx_private);
+
+ if (ctx) {
+ return ctx;
+ }
+
+ ca_context_create(&ctx);
+ if (!ctx) {
+ return nullptr;
+ }
+
+ g_private_set(&ctx_private, ctx);
+
+ GtkSettings* settings = gtk_settings_get_default();
+ if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
+ "gtk-sound-theme-name")) {
+ gchar* sound_theme_name = nullptr;
+ g_object_get(settings, "gtk-sound-theme-name", &sound_theme_name, nullptr);
+
+ if (sound_theme_name) {
+ ca_context_change_props(ctx, "canberra.xdg-theme.name", sound_theme_name,
+ nullptr);
+ g_free(sound_theme_name);
+ }
+ }
+
+ nsAutoString wbrand;
+ mozilla::widget::WidgetUtils::GetBrandShortName(wbrand);
+ ca_context_change_props(ctx, "application.name",
+ NS_ConvertUTF16toUTF8(wbrand).get(), nullptr);
+
+ nsCOMPtr<nsIXULAppInfo> appInfo =
+ do_GetService("@mozilla.org/xre/app-info;1");
+ if (appInfo) {
+ nsAutoCString version;
+ appInfo->GetVersion(version);
+
+ ca_context_change_props(ctx, "application.version", version.get(), nullptr);
+ }
+
+ ca_context_change_props(ctx, "application.icon_name", MOZ_APP_NAME, nullptr);
+
+ return ctx;
+}
+
+static void ca_finish_cb(ca_context* c, uint32_t id, int error_code,
+ void* userdata) {
+ nsIFile* file = reinterpret_cast<nsIFile*>(userdata);
+ if (file) {
+ file->Remove(false);
+ NS_RELEASE(file);
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver)
+
+////////////////////////////////////////////////////////////////////////
+nsSound::nsSound() { mInited = false; }
+
+nsSound::~nsSound() = default;
+
+NS_IMETHODIMP
+nsSound::Init() {
+ // This function is designed so that no library is compulsory, and
+ // one library missing doesn't cause the other(s) to not be used.
+ if (mInited) return NS_OK;
+
+ mInited = true;
+
+ if (!libcanberra) {
+ libcanberra = PR_LoadLibrary("libcanberra.so.0");
+ if (libcanberra) {
+ ca_context_create = (ca_context_create_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_context_create");
+ if (!ca_context_create) {
+#ifdef MOZ_TSAN
+ // With TSan, we cannot unload libcanberra once we have loaded it
+ // because TSan does not support unloading libraries that are matched
+ // from its suppression list. Hence we just keep the library loaded in
+ // TSan builds.
+ libcanberra = nullptr;
+ return NS_OK;
+#endif
+ PR_UnloadLibrary(libcanberra);
+ libcanberra = nullptr;
+ } else {
+ // at this point we know we have a good libcanberra library
+ ca_context_destroy = (ca_context_destroy_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_context_destroy");
+ ca_context_play = (ca_context_play_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_context_play");
+ ca_context_change_props =
+ (ca_context_change_props_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_context_change_props");
+ ca_proplist_create = (ca_proplist_create_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_proplist_create");
+ ca_proplist_destroy = (ca_proplist_destroy_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_proplist_destroy");
+ ca_proplist_sets = (ca_proplist_sets_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_proplist_sets");
+ ca_context_play_full = (ca_context_play_full_fn)PR_FindFunctionSymbol(
+ libcanberra, "ca_context_play_full");
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+/* static */
+void nsSound::Shutdown() {
+#ifndef MOZ_TSAN
+ if (libcanberra) {
+ PR_UnloadLibrary(libcanberra);
+ libcanberra = nullptr;
+ }
+#endif
+}
+
+namespace mozilla {
+namespace sound {
+StaticRefPtr<nsISound> sInstance;
+}
+} // namespace mozilla
+/* static */
+already_AddRefed<nsISound> nsSound::GetInstance() {
+ using namespace mozilla::sound;
+
+ if (!sInstance) {
+ if (gfxPlatform::IsHeadless()) {
+ sInstance = new mozilla::widget::HeadlessSound();
+ } else {
+ sInstance = new nsSound();
+ }
+ ClearOnShutdown(&sInstance);
+ }
+
+ RefPtr<nsISound> service = sInstance.get();
+ return service.forget();
+}
+
+NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader* aLoader,
+ nsISupports* context, nsresult aStatus,
+ uint32_t dataLen, const uint8_t* data) {
+ // print a load error on bad status, and return
+ if (NS_FAILED(aStatus)) {
+#ifdef DEBUG
+ if (aLoader) {
+ nsCOMPtr<nsIRequest> request;
+ aLoader->GetRequest(getter_AddRefs(request));
+ if (request) {
+ nsCOMPtr<nsIURI> uri;
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
+ if (channel) {
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ printf("Failed to load %s\n", uri->GetSpecOrDefault().get());
+ }
+ }
+ }
+ }
+#endif
+ return aStatus;
+ }
+
+ nsCOMPtr<nsIFile> tmpFile;
+ nsDirectoryService::gService->Get(NS_OS_TEMP_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(tmpFile));
+
+ nsresult rv =
+ tmpFile->AppendNative(nsDependentCString("mozilla_audio_sample"));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, PR_IRUSR | PR_IWUSR);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ScopedCanberraFile canberraFile(tmpFile);
+
+ mozilla::AutoFDClose fd;
+ rv = canberraFile->OpenNSPRFileDesc(PR_WRONLY, PR_IRUSR | PR_IWUSR,
+ getter_Transfers(fd));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // XXX: Should we do this on another thread?
+ uint32_t length = dataLen;
+ while (length > 0) {
+ int32_t amount = PR_Write(fd.get(), data, length);
+ if (amount < 0) {
+ return NS_ERROR_FAILURE;
+ }
+ length -= amount;
+ data += amount;
+ }
+
+ ca_context* ctx = ca_context_get_default();
+ if (!ctx) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ ca_proplist* p;
+ ca_proplist_create(&p);
+ if (!p) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsAutoCString path;
+ rv = canberraFile->GetNativePath(path);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ca_proplist_sets(p, "media.filename", path.get());
+ if (ca_context_play_full(ctx, 0, p, ca_finish_cb, canberraFile) >= 0) {
+ // Don't delete the temporary file here if ca_context_play_full succeeds
+ canberraFile.forget();
+ }
+ ca_proplist_destroy(p);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSound::Beep() {
+ ::gdk_beep();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSound::Play(nsIURL* aURL) {
+ if (!mInited) Init();
+
+ if (!libcanberra) return NS_ERROR_NOT_AVAILABLE;
+
+ nsresult rv;
+ if (aURL->SchemeIs("file")) {
+ ca_context* ctx = ca_context_get_default();
+ if (!ctx) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsAutoCString spec;
+ rv = aURL->GetSpec(spec);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ gchar* path = g_filename_from_uri(spec.get(), nullptr, nullptr);
+ if (!path) {
+ return NS_ERROR_FILE_UNRECOGNIZED_PATH;
+ }
+
+ ca_context_play(ctx, 0, "media.filename", path, nullptr);
+ g_free(path);
+ } else {
+ nsCOMPtr<nsIStreamLoader> loader;
+ rv = NS_NewStreamLoader(
+ getter_AddRefs(loader), aURL,
+ this, // aObserver
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsSound::PlayEventSound(uint32_t aEventId) {
+ if (!mInited) Init();
+
+ if (!libcanberra) return NS_OK;
+
+ // Do we even want alert sounds?
+ GtkSettings* settings = gtk_settings_get_default();
+
+ if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
+ "gtk-enable-event-sounds")) {
+ gboolean enable_sounds = TRUE;
+ g_object_get(settings, "gtk-enable-event-sounds", &enable_sounds, nullptr);
+
+ if (!enable_sounds) {
+ return NS_OK;
+ }
+ }
+
+ ca_context* ctx = ca_context_get_default();
+ if (!ctx) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ switch (aEventId) {
+ case EVENT_ALERT_DIALOG_OPEN:
+ ca_context_play(ctx, 0, "event.id", "dialog-warning", nullptr);
+ break;
+ case EVENT_CONFIRM_DIALOG_OPEN:
+ ca_context_play(ctx, 0, "event.id", "dialog-question", nullptr);
+ break;
+ case EVENT_NEW_MAIL_RECEIVED:
+ ca_context_play(ctx, 0, "event.id", "message-new-email", nullptr);
+ break;
+ case EVENT_MENU_EXECUTE:
+ ca_context_play(ctx, 0, "event.id", "menu-click", nullptr);
+ break;
+ case EVENT_MENU_POPUP:
+ ca_context_play(ctx, 0, "event.id", "menu-popup", nullptr);
+ break;
+ }
+ return NS_OK;
+}
diff --git a/widget/gtk/nsSound.h b/widget/gtk/nsSound.h
new file mode 100644
index 0000000000..8f4fe5a04b
--- /dev/null
+++ b/widget/gtk/nsSound.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsSound_h__
+#define __nsSound_h__
+
+#include "nsISound.h"
+#include "nsIStreamLoader.h"
+
+#include <gtk/gtk.h>
+
+class nsSound : public nsISound, public nsIStreamLoaderObserver {
+ public:
+ nsSound();
+
+ static void Shutdown();
+ static already_AddRefed<nsISound> GetInstance();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOUND
+ NS_DECL_NSISTREAMLOADEROBSERVER
+
+ private:
+ virtual ~nsSound();
+
+ bool mInited;
+};
+
+#endif /* __nsSound_h__ */
diff --git a/widget/gtk/nsToolkit.cpp b/widget/gtk/nsToolkit.cpp
new file mode 100644
index 0000000000..aaf13239e0
--- /dev/null
+++ b/widget/gtk/nsToolkit.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 "nscore.h" // needed for 'nullptr'
+#include "nsGTKToolkit.h"
+
+nsGTKToolkit* nsGTKToolkit::gToolkit = nullptr;
+
+//-------------------------------------------------------------------------------
+// Return the toolkit. If a toolkit does not yet exist, then one will be
+// created.
+//-------------------------------------------------------------------------------
+// static
+nsGTKToolkit* nsGTKToolkit::GetToolkit() {
+ if (!gToolkit) {
+ gToolkit = new nsGTKToolkit();
+ }
+
+ return gToolkit;
+}
diff --git a/widget/gtk/nsUserIdleServiceGTK.cpp b/widget/gtk/nsUserIdleServiceGTK.cpp
new file mode 100644
index 0000000000..4cdc5ba13d
--- /dev/null
+++ b/widget/gtk/nsUserIdleServiceGTK.cpp
@@ -0,0 +1,317 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 <gtk/gtk.h>
+
+#include "nsUserIdleServiceGTK.h"
+#include "nsDebug.h"
+#include "prlink.h"
+#include "mozilla/Logging.h"
+#include "WidgetUtilsGtk.h"
+#ifdef MOZ_X11
+# include <X11/Xlib.h>
+# include <X11/Xutil.h>
+# include <gdk/gdkx.h>
+#endif
+#ifdef MOZ_ENABLE_DBUS
+# include <gio/gio.h>
+# include "AsyncDBus.h"
+# include "WakeLockListener.h"
+# include "nsIObserverService.h"
+# include "mozilla/UniquePtrExtensions.h"
+#endif
+
+using mozilla::LogLevel;
+static mozilla::LazyLogModule sIdleLog("nsIUserIdleService");
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#ifdef MOZ_X11
+typedef struct {
+ Window window; // Screen saver window
+ int state; // ScreenSaver(Off,On,Disabled)
+ int kind; // ScreenSaver(Blanked,Internal,External)
+ unsigned long til_or_since; // milliseconds since/til screensaver kicks in
+ unsigned long idle; // milliseconds idle
+ unsigned long event_mask; // event stuff
+} XScreenSaverInfo;
+
+typedef bool (*_XScreenSaverQueryExtension_fn)(Display* dpy, int* event_base,
+ int* error_base);
+typedef XScreenSaverInfo* (*_XScreenSaverAllocInfo_fn)(void);
+typedef void (*_XScreenSaverQueryInfo_fn)(Display* dpy, Drawable drw,
+ XScreenSaverInfo* info);
+
+class UserIdleServiceX11 : public UserIdleServiceImpl {
+ public:
+ bool PollIdleTime(uint32_t* aIdleTime) override {
+ // Ask xscreensaver about idle time:
+ *aIdleTime = 0;
+
+ // We might not have a display (cf. in xpcshell)
+ Display* dplay = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
+ if (!dplay) {
+ MOZ_LOG(sIdleLog, LogLevel::Warning, ("No display found!\n"));
+ return false;
+ }
+
+ int event_base, error_base;
+ if (mXSSQueryExtension(dplay, &event_base, &error_base)) {
+ if (!mXssInfo) mXssInfo = mXSSAllocInfo();
+ if (!mXssInfo) return false;
+ mXSSQueryInfo(dplay, GDK_ROOT_WINDOW(), mXssInfo);
+ *aIdleTime = mXssInfo->idle;
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("UserIdleServiceX11::PollIdleTime() %d\n", *aIdleTime));
+ return true;
+ }
+ // If we get here, we couldn't get to XScreenSaver:
+ MOZ_LOG(sIdleLog, LogLevel::Warning,
+ ("XSSQueryExtension returned false!\n"));
+ return false;
+ }
+
+ bool ProbeImplementation() override {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("UserIdleServiceX11::UserIdleServiceX11()\n"));
+
+ if (!mozilla::widget::GdkIsX11Display()) {
+ return false;
+ }
+
+ // This will leak - See comments in ~UserIdleServiceX11().
+ PRLibrary* xsslib = PR_LoadLibrary("libXss.so.1");
+ if (!xsslib) // ouch.
+ {
+ MOZ_LOG(sIdleLog, LogLevel::Warning, ("Failed to find libXss.so!\n"));
+ return false;
+ }
+
+ mXSSQueryExtension = (_XScreenSaverQueryExtension_fn)PR_FindFunctionSymbol(
+ xsslib, "XScreenSaverQueryExtension");
+ mXSSAllocInfo = (_XScreenSaverAllocInfo_fn)PR_FindFunctionSymbol(
+ xsslib, "XScreenSaverAllocInfo");
+ mXSSQueryInfo = (_XScreenSaverQueryInfo_fn)PR_FindFunctionSymbol(
+ xsslib, "XScreenSaverQueryInfo");
+
+ if (!mXSSQueryExtension) {
+ MOZ_LOG(sIdleLog, LogLevel::Warning,
+ ("Failed to get XSSQueryExtension!\n"));
+ }
+ if (!mXSSAllocInfo) {
+ MOZ_LOG(sIdleLog, LogLevel::Warning, ("Failed to get XSSAllocInfo!\n"));
+ }
+ if (!mXSSQueryInfo) {
+ MOZ_LOG(sIdleLog, LogLevel::Warning, ("Failed to get XSSQueryInfo!\n"));
+ }
+ if (!mXSSQueryExtension || !mXSSAllocInfo || !mXSSQueryInfo) {
+ // We're missing X11 symbols
+ return false;
+ }
+
+ // UserIdleServiceX11 uses sync init, confirm it now.
+ mUserIdleServiceGTK->AcceptServiceCallback();
+ return true;
+ }
+
+ explicit UserIdleServiceX11(nsUserIdleServiceGTK* aUserIdleService)
+ : UserIdleServiceImpl(aUserIdleService){};
+
+ ~UserIdleServiceX11() {
+# ifdef MOZ_X11
+ if (mXssInfo) {
+ XFree(mXssInfo);
+ }
+# endif
+
+// It is not safe to unload libXScrnSaver until each display is closed because
+// the library registers callbacks through XESetCloseDisplay (Bug 397607).
+// (Also the library and its functions are scoped for the file not the object.)
+# if 0
+ if (xsslib) {
+ PR_UnloadLibrary(xsslib);
+ xsslib = nullptr;
+ }
+# endif
+ }
+
+ private:
+ XScreenSaverInfo* mXssInfo = nullptr;
+ _XScreenSaverQueryExtension_fn mXSSQueryExtension = nullptr;
+ _XScreenSaverAllocInfo_fn mXSSAllocInfo = nullptr;
+ _XScreenSaverQueryInfo_fn mXSSQueryInfo = nullptr;
+};
+#endif
+
+#ifdef MOZ_ENABLE_DBUS
+class UserIdleServiceMutter : public UserIdleServiceImpl {
+ public:
+ bool PollIdleTime(uint32_t* aIdleTime) override {
+ MOZ_LOG(sIdleLog, LogLevel::Info, ("PollIdleTime() request\n"));
+
+ // We're not ready yet
+ if (!mProxy) {
+ return false;
+ }
+
+ if (!mPollInProgress) {
+ mPollInProgress = true;
+ DBusProxyCall(mProxy, "GetIdletime", nullptr, G_DBUS_CALL_FLAGS_NONE, -1,
+ mCancellable)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ // It's safe to capture this as we use mCancellable to stop
+ // listening.
+ [this](RefPtr<GVariant>&& aResult) {
+ if (!g_variant_is_of_type(aResult, G_VARIANT_TYPE_TUPLE) ||
+ g_variant_n_children(aResult) != 1) {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("PollIdleTime() Unexpected params type: %s\n",
+ g_variant_get_type_string(aResult)));
+ mLastIdleTime = 0;
+ return;
+ }
+ RefPtr<GVariant> iTime =
+ dont_AddRef(g_variant_get_child_value(aResult, 0));
+ if (!g_variant_is_of_type(iTime, G_VARIANT_TYPE_UINT64)) {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("PollIdleTime() Unexpected params type: %s\n",
+ g_variant_get_type_string(aResult)));
+ mLastIdleTime = 0;
+ return;
+ }
+ uint64_t idleTime = g_variant_get_uint64(iTime);
+ if (idleTime > std::numeric_limits<uint32_t>::max()) {
+ idleTime = std::numeric_limits<uint32_t>::max();
+ }
+ mLastIdleTime = idleTime;
+ mPollInProgress = false;
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("Async handler got %d\n", mLastIdleTime));
+ },
+ [this](GUniquePtr<GError>&& aError) {
+ mPollInProgress = false;
+ if (!IsCancelledGError(aError.get())) {
+ MOZ_LOG(
+ sIdleLog, LogLevel::Warning,
+ ("Failed to call GetIdletime(): %s\n", aError->message));
+ mUserIdleServiceGTK->RejectAndTryNextServiceCallback();
+ }
+ });
+ }
+
+ *aIdleTime = mLastIdleTime;
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("PollIdleTime() returns %d\n", *aIdleTime));
+ return true;
+ }
+
+ bool ProbeImplementation() override {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("UserIdleServiceMutter::UserIdleServiceMutter()\n"));
+
+ mCancellable = dont_AddRef(g_cancellable_new());
+ CreateDBusProxyForBus(
+ G_BUS_TYPE_SESSION,
+ GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
+ G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES),
+ nullptr, "org.gnome.Mutter.IdleMonitor",
+ "/org/gnome/Mutter/IdleMonitor/Core", "org.gnome.Mutter.IdleMonitor",
+ mCancellable)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this](RefPtr<GDBusProxy>&& aProxy) {
+ mProxy = std::move(aProxy);
+ mUserIdleServiceGTK->AcceptServiceCallback();
+ },
+ [this](GUniquePtr<GError>&& aError) {
+ if (!IsCancelledGError(aError.get())) {
+ mUserIdleServiceGTK->RejectAndTryNextServiceCallback();
+ }
+ });
+ return true;
+ }
+
+ explicit UserIdleServiceMutter(nsUserIdleServiceGTK* aUserIdleService)
+ : UserIdleServiceImpl(aUserIdleService){};
+
+ ~UserIdleServiceMutter() {
+ if (mCancellable) {
+ g_cancellable_cancel(mCancellable);
+ mCancellable = nullptr;
+ }
+ mProxy = nullptr;
+ }
+
+ private:
+ RefPtr<GDBusProxy> mProxy;
+ RefPtr<GCancellable> mCancellable;
+ uint32_t mLastIdleTime = 0;
+ bool mPollInProgress = false;
+};
+#endif
+
+void nsUserIdleServiceGTK::ProbeService() {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("nsUserIdleServiceGTK::ProbeService() mIdleServiceType %d\n",
+ mIdleServiceType));
+ MOZ_ASSERT(!mIdleService);
+
+ switch (mIdleServiceType) {
+#ifdef MOZ_ENABLE_DBUS
+ case IDLE_SERVICE_MUTTER:
+ mIdleService = MakeUnique<UserIdleServiceMutter>(this);
+ break;
+#endif
+#ifdef MOZ_X11
+ case IDLE_SERVICE_XSCREENSAVER:
+ mIdleService = MakeUnique<UserIdleServiceX11>(this);
+ break;
+#endif
+ default:
+ return;
+ }
+
+ if (!mIdleService->ProbeImplementation()) {
+ RejectAndTryNextServiceCallback();
+ }
+}
+
+void nsUserIdleServiceGTK::AcceptServiceCallback() {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("nsUserIdleServiceGTK::AcceptServiceCallback() type %d\n",
+ mIdleServiceType));
+ mIdleServiceInitialized = true;
+}
+
+void nsUserIdleServiceGTK::RejectAndTryNextServiceCallback() {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("nsUserIdleServiceGTK::RejectAndTryNextServiceCallback() type %d\n",
+ mIdleServiceType));
+
+ // Delete recent non-working service
+ MOZ_ASSERT(mIdleService, "Nothing to reject?");
+ mIdleService = nullptr;
+ mIdleServiceInitialized = false;
+
+ mIdleServiceType++;
+ if (mIdleServiceType < IDLE_SERVICE_NONE) {
+ MOZ_LOG(sIdleLog, LogLevel::Info,
+ ("nsUserIdleServiceGTK try next idle service\n"));
+ ProbeService();
+ } else {
+ MOZ_LOG(sIdleLog, LogLevel::Info, ("nsUserIdleServiceGTK failed\n"));
+ }
+}
+
+bool nsUserIdleServiceGTK::PollIdleTime(uint32_t* aIdleTime) {
+ if (!mIdleServiceInitialized) {
+ return false;
+ }
+ return mIdleService->PollIdleTime(aIdleTime);
+}
diff --git a/widget/gtk/nsUserIdleServiceGTK.h b/widget/gtk/nsUserIdleServiceGTK.h
new file mode 100644
index 0000000000..1a5ed97840
--- /dev/null
+++ b/widget/gtk/nsUserIdleServiceGTK.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef nsUserIdleServiceGTK_h__
+#define nsUserIdleServiceGTK_h__
+
+#include "nsUserIdleService.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/UniquePtr.h"
+
+class nsUserIdleServiceGTK;
+
+class UserIdleServiceImpl {
+ public:
+ explicit UserIdleServiceImpl(nsUserIdleServiceGTK* aUserIdleService)
+ : mUserIdleServiceGTK(aUserIdleService){};
+
+ virtual bool PollIdleTime(uint32_t* aIdleTime) = 0;
+ virtual bool ProbeImplementation() = 0;
+
+ virtual ~UserIdleServiceImpl() = default;
+
+ protected:
+ nsUserIdleServiceGTK* mUserIdleServiceGTK;
+};
+
+#define IDLE_SERVICE_MUTTER 0
+#define IDLE_SERVICE_XSCREENSAVER 1
+#define IDLE_SERVICE_NONE 2
+
+class nsUserIdleServiceGTK : public nsUserIdleService {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsUserIdleServiceGTK, nsUserIdleService)
+
+ // The idle time in ms
+ virtual bool PollIdleTime(uint32_t* aIdleTime) override;
+
+ static already_AddRefed<nsUserIdleServiceGTK> GetInstance() {
+ RefPtr<nsUserIdleServiceGTK> idleService =
+ nsUserIdleService::GetInstance().downcast<nsUserIdleServiceGTK>();
+ if (!idleService) {
+ // Avoid late instantiation or resurrection during shutdown.
+ if (mozilla::AppShutdown::IsInOrBeyond(
+ mozilla::ShutdownPhase::AppShutdownConfirmed)) {
+ return nullptr;
+ }
+ idleService = new nsUserIdleServiceGTK();
+ idleService->ProbeService();
+ }
+
+ return idleService.forget();
+ }
+
+ void ProbeService();
+ void AcceptServiceCallback();
+ void RejectAndTryNextServiceCallback();
+
+ protected:
+ nsUserIdleServiceGTK() = default;
+
+ private:
+ ~nsUserIdleServiceGTK() = default;
+
+ mozilla::UniquePtr<UserIdleServiceImpl> mIdleService;
+#ifdef MOZ_ENABLE_DBUS
+ int mIdleServiceType = IDLE_SERVICE_MUTTER;
+#else
+ int mIdleServiceType = IDLE_SERVICE_XSCREENSAVER;
+#endif
+ // We have a working idle service.
+ bool mIdleServiceInitialized = false;
+};
+
+#endif // nsUserIdleServiceGTK_h__
diff --git a/widget/gtk/nsWaylandDisplay.cpp b/widget/gtk/nsWaylandDisplay.cpp
new file mode 100644
index 0000000000..2a1021457a
--- /dev/null
+++ b/widget/gtk/nsWaylandDisplay.cpp
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "nsWaylandDisplay.h"
+
+#include <dlfcn.h>
+
+#include "base/message_loop.h" // for MessageLoop
+#include "base/task.h" // for NewRunnableMethod, etc
+#include "mozilla/gfx/Logging.h" // for gfxCriticalNote
+#include "mozilla/StaticMutex.h"
+#include "mozilla/Array.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/ThreadLocal.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/Sprintf.h"
+#include "WidgetUtilsGtk.h"
+#include "nsGtkKeyUtils.h"
+
+namespace mozilla::widget {
+
+static nsWaylandDisplay* gWaylandDisplay;
+
+void WaylandDisplayRelease() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread(),
+ "WaylandDisplay can be released in main thread only!");
+ if (!gWaylandDisplay) {
+ NS_WARNING("WaylandDisplayRelease(): Wayland display is missing!");
+ return;
+ }
+ delete gWaylandDisplay;
+ gWaylandDisplay = nullptr;
+}
+
+wl_display* WaylandDisplayGetWLDisplay() {
+ GdkDisplay* disp = gdk_display_get_default();
+ if (!GdkIsWaylandDisplay(disp)) {
+ return nullptr;
+ }
+ return gdk_wayland_display_get_wl_display(disp);
+}
+
+nsWaylandDisplay* WaylandDisplayGet() {
+ if (!gWaylandDisplay) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread(),
+ "WaylandDisplay can be created in main thread only!");
+ wl_display* waylandDisplay = WaylandDisplayGetWLDisplay();
+ if (!waylandDisplay) {
+ return nullptr;
+ }
+ gWaylandDisplay = new nsWaylandDisplay(waylandDisplay);
+ }
+ return gWaylandDisplay;
+}
+
+void nsWaylandDisplay::SetShm(wl_shm* aShm) { mShm = aShm; }
+
+void nsWaylandDisplay::SetCompositor(wl_compositor* aCompositor) {
+ mCompositor = aCompositor;
+}
+
+void nsWaylandDisplay::SetSubcompositor(wl_subcompositor* aSubcompositor) {
+ mSubcompositor = aSubcompositor;
+}
+
+void nsWaylandDisplay::SetIdleInhibitManager(
+ zwp_idle_inhibit_manager_v1* aIdleInhibitManager) {
+ mIdleInhibitManager = aIdleInhibitManager;
+}
+
+void nsWaylandDisplay::SetViewporter(wp_viewporter* aViewporter) {
+ mViewporter = aViewporter;
+}
+
+void nsWaylandDisplay::SetRelativePointerManager(
+ zwp_relative_pointer_manager_v1* aRelativePointerManager) {
+ mRelativePointerManager = aRelativePointerManager;
+}
+
+void nsWaylandDisplay::SetPointerConstraints(
+ zwp_pointer_constraints_v1* aPointerConstraints) {
+ mPointerConstraints = aPointerConstraints;
+}
+
+void nsWaylandDisplay::SetDmabuf(zwp_linux_dmabuf_v1* aDmabuf) {
+ mDmabuf = aDmabuf;
+}
+
+void nsWaylandDisplay::SetXdgActivation(xdg_activation_v1* aXdgActivation) {
+ mXdgActivation = aXdgActivation;
+}
+
+static void global_registry_handler(void* data, wl_registry* registry,
+ uint32_t id, const char* interface,
+ uint32_t version) {
+ auto* display = static_cast<nsWaylandDisplay*>(data);
+ if (!display) {
+ return;
+ }
+
+ nsDependentCString iface(interface);
+ if (iface.EqualsLiteral("wl_shm")) {
+ auto* shm = WaylandRegistryBind<wl_shm>(registry, id, &wl_shm_interface, 1);
+ display->SetShm(shm);
+ } else if (iface.EqualsLiteral("zwp_idle_inhibit_manager_v1")) {
+ auto* idle_inhibit_manager =
+ WaylandRegistryBind<zwp_idle_inhibit_manager_v1>(
+ registry, id, &zwp_idle_inhibit_manager_v1_interface, 1);
+ display->SetIdleInhibitManager(idle_inhibit_manager);
+ } else if (iface.EqualsLiteral("zwp_relative_pointer_manager_v1")) {
+ auto* relative_pointer_manager =
+ WaylandRegistryBind<zwp_relative_pointer_manager_v1>(
+ registry, id, &zwp_relative_pointer_manager_v1_interface, 1);
+ display->SetRelativePointerManager(relative_pointer_manager);
+ } else if (iface.EqualsLiteral("zwp_pointer_constraints_v1")) {
+ auto* pointer_constraints = WaylandRegistryBind<zwp_pointer_constraints_v1>(
+ registry, id, &zwp_pointer_constraints_v1_interface, 1);
+ display->SetPointerConstraints(pointer_constraints);
+ } else if (iface.EqualsLiteral("wl_compositor")) {
+ // Requested wl_compositor version 4 as we need wl_surface_damage_buffer().
+ auto* compositor = WaylandRegistryBind<wl_compositor>(
+ registry, id, &wl_compositor_interface, 4);
+ display->SetCompositor(compositor);
+ } else if (iface.EqualsLiteral("wl_subcompositor")) {
+ auto* subcompositor = WaylandRegistryBind<wl_subcompositor>(
+ registry, id, &wl_subcompositor_interface, 1);
+ display->SetSubcompositor(subcompositor);
+ } else if (iface.EqualsLiteral("wp_viewporter")) {
+ auto* viewporter = WaylandRegistryBind<wp_viewporter>(
+ registry, id, &wp_viewporter_interface, 1);
+ display->SetViewporter(viewporter);
+ } else if (iface.EqualsLiteral("zwp_linux_dmabuf_v1") && version > 2) {
+ auto* dmabuf = WaylandRegistryBind<zwp_linux_dmabuf_v1>(
+ registry, id, &zwp_linux_dmabuf_v1_interface, 3);
+ display->SetDmabuf(dmabuf);
+ } else if (iface.EqualsLiteral("xdg_activation_v1")) {
+ auto* activation = WaylandRegistryBind<xdg_activation_v1>(
+ registry, id, &xdg_activation_v1_interface, 1);
+ display->SetXdgActivation(activation);
+ } else if (iface.EqualsLiteral("wl_seat")) {
+ // Install keyboard handlers for main thread only
+ auto* seat =
+ WaylandRegistryBind<wl_seat>(registry, id, &wl_seat_interface, 1);
+ KeymapWrapper::SetSeat(seat, id);
+ } else if (iface.EqualsLiteral("wp_fractional_scale_manager_v1")) {
+ auto* manager = WaylandRegistryBind<wp_fractional_scale_manager_v1>(
+ registry, id, &wp_fractional_scale_manager_v1_interface, 1);
+ display->SetFractionalScaleManager(manager);
+ } else if (iface.EqualsLiteral("gtk_primary_selection_device_manager") ||
+ iface.EqualsLiteral("zwp_primary_selection_device_manager_v1")) {
+ display->EnablePrimarySelection();
+ }
+}
+
+static void global_registry_remover(void* data, wl_registry* registry,
+ uint32_t id) {
+ KeymapWrapper::ClearSeat(id);
+}
+
+static const struct wl_registry_listener registry_listener = {
+ global_registry_handler, global_registry_remover};
+
+nsWaylandDisplay::~nsWaylandDisplay() {}
+
+static void WlLogHandler(const char* format, va_list args) {
+ char error[1000];
+ VsprintfLiteral(error, format, args);
+ gfxCriticalNote << "Wayland protocol error: " << error;
+
+ // See Bug 1826583 and Bug 1844653 for reference.
+ // "warning: queue %p destroyed while proxies still attached" and variants
+ // like "zwp_linux_dmabuf_feedback_v1@%d still attached" are exceptions on
+ // Wayland and non-fatal. They are triggered in certain versions of Mesa or
+ // the proprietary Nvidia driver and we don't want to crash because of them.
+ if (strstr(error, "still attached")) {
+ return;
+ }
+
+ MOZ_CRASH_UNSAFE(error);
+}
+
+nsWaylandDisplay::nsWaylandDisplay(wl_display* aDisplay)
+ : mThreadId(PR_GetCurrentThread()), mDisplay(aDisplay) {
+ // GTK sets the log handler on display creation, thus we overwrite it here
+ // in a similar fashion
+ wl_log_set_handler_client(WlLogHandler);
+
+ mRegistry = wl_display_get_registry(mDisplay);
+ wl_registry_add_listener(mRegistry, &registry_listener, this);
+ wl_display_roundtrip(mDisplay);
+ wl_display_roundtrip(mDisplay);
+
+ // Check we have critical Wayland interfaces.
+ // Missing ones indicates a compositor bug and we can't continue.
+ MOZ_DIAGNOSTIC_ASSERT(GetShm(), "We're missing shm interface!");
+ MOZ_DIAGNOSTIC_ASSERT(GetCompositor(), "We're missing compositor interface!");
+ MOZ_DIAGNOSTIC_ASSERT(GetSubcompositor(),
+ "We're missing subcompositor interface!");
+}
+
+} // namespace mozilla::widget
diff --git a/widget/gtk/nsWaylandDisplay.h b/widget/gtk/nsWaylandDisplay.h
new file mode 100644
index 0000000000..cd8124d97f
--- /dev/null
+++ b/widget/gtk/nsWaylandDisplay.h
@@ -0,0 +1,120 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __MOZ_WAYLAND_DISPLAY_H__
+#define __MOZ_WAYLAND_DISPLAY_H__
+
+#include "DMABufLibWrapper.h"
+
+#include "mozilla/widget/mozwayland.h"
+#include "mozilla/widget/gbm.h"
+#include "mozilla/widget/fractional-scale-v1-client-protocol.h"
+#include "mozilla/widget/idle-inhibit-unstable-v1-client-protocol.h"
+#include "mozilla/widget/relative-pointer-unstable-v1-client-protocol.h"
+#include "mozilla/widget/pointer-constraints-unstable-v1-client-protocol.h"
+#include "mozilla/widget/linux-dmabuf-unstable-v1-client-protocol.h"
+#include "mozilla/widget/viewporter-client-protocol.h"
+#include "mozilla/widget/xdg-activation-v1-client-protocol.h"
+#include "mozilla/widget/xdg-output-unstable-v1-client-protocol.h"
+
+namespace mozilla::widget {
+
+// Our general connection to Wayland display server,
+// holds our display connection and runs event loop.
+// We have a global nsWaylandDisplay object for each thread.
+class nsWaylandDisplay {
+ public:
+ // Create nsWaylandDisplay object on top of native Wayland wl_display
+ // connection.
+ explicit nsWaylandDisplay(wl_display* aDisplay);
+
+ wl_display* GetDisplay() { return mDisplay; };
+ wl_compositor* GetCompositor() { return mCompositor; };
+ wl_subcompositor* GetSubcompositor() { return mSubcompositor; };
+ wl_shm* GetShm() { return mShm; };
+ zwp_idle_inhibit_manager_v1* GetIdleInhibitManager() {
+ return mIdleInhibitManager;
+ }
+ wp_viewporter* GetViewporter() { return mViewporter; };
+ zwp_relative_pointer_manager_v1* GetRelativePointerManager() {
+ return mRelativePointerManager;
+ }
+ zwp_pointer_constraints_v1* GetPointerConstraints() {
+ return mPointerConstraints;
+ }
+ zwp_linux_dmabuf_v1* GetDmabuf() { return mDmabuf; };
+ xdg_activation_v1* GetXdgActivation() { return mXdgActivation; };
+ wp_fractional_scale_manager_v1* GetFractionalScaleManager() {
+ return mFractionalScaleManager;
+ }
+ bool IsPrimarySelectionEnabled() { return mIsPrimarySelectionEnabled; }
+
+ void SetShm(wl_shm* aShm);
+ void SetCompositor(wl_compositor* aCompositor);
+ void SetSubcompositor(wl_subcompositor* aSubcompositor);
+ void SetDataDeviceManager(wl_data_device_manager* aDataDeviceManager);
+ void SetIdleInhibitManager(zwp_idle_inhibit_manager_v1* aIdleInhibitManager);
+ void SetViewporter(wp_viewporter* aViewporter);
+ void SetRelativePointerManager(
+ zwp_relative_pointer_manager_v1* aRelativePointerManager);
+ void SetPointerConstraints(zwp_pointer_constraints_v1* aPointerConstraints);
+ void SetDmabuf(zwp_linux_dmabuf_v1* aDmabuf);
+ void SetXdgActivation(xdg_activation_v1* aXdgActivation);
+ void SetFractionalScaleManager(wp_fractional_scale_manager_v1* aManager) {
+ mFractionalScaleManager = aManager;
+ }
+ void EnablePrimarySelection() { mIsPrimarySelectionEnabled = true; }
+
+ ~nsWaylandDisplay();
+
+ private:
+ PRThread* mThreadId = nullptr;
+ wl_registry* mRegistry = nullptr;
+ wl_display* mDisplay = nullptr;
+ wl_compositor* mCompositor = nullptr;
+ wl_subcompositor* mSubcompositor = nullptr;
+ wl_shm* mShm = nullptr;
+ zwp_idle_inhibit_manager_v1* mIdleInhibitManager = nullptr;
+ zwp_relative_pointer_manager_v1* mRelativePointerManager = nullptr;
+ zwp_pointer_constraints_v1* mPointerConstraints = nullptr;
+ wp_viewporter* mViewporter = nullptr;
+ zwp_linux_dmabuf_v1* mDmabuf = nullptr;
+ xdg_activation_v1* mXdgActivation = nullptr;
+ wp_fractional_scale_manager_v1* mFractionalScaleManager = nullptr;
+ bool mExplicitSync = false;
+ bool mIsPrimarySelectionEnabled = false;
+};
+
+wl_display* WaylandDisplayGetWLDisplay();
+nsWaylandDisplay* WaylandDisplayGet();
+void WaylandDisplayRelease();
+
+} // namespace mozilla::widget
+
+template <class T>
+static inline T* WaylandRegistryBind(struct wl_registry* wl_registry,
+ uint32_t name,
+ const struct wl_interface* interface,
+ uint32_t version) {
+ struct wl_proxy* id;
+
+ // When libwayland-client does not provide this symbol, it will be
+ // linked to the fallback in libmozwayland, which returns NULL.
+ id = wl_proxy_marshal_constructor_versioned(
+ (struct wl_proxy*)wl_registry, WL_REGISTRY_BIND, interface, version, name,
+ interface->name, version, nullptr);
+
+ if (id == nullptr) {
+ id = wl_proxy_marshal_constructor((struct wl_proxy*)wl_registry,
+ WL_REGISTRY_BIND, interface, name,
+ interface->name, version, nullptr);
+ }
+
+ return reinterpret_cast<T*>(id);
+}
+
+#endif // __MOZ_WAYLAND_DISPLAY_H__
diff --git a/widget/gtk/nsWidgetFactory.cpp b/widget/gtk/nsWidgetFactory.cpp
new file mode 100644
index 0000000000..7f06e8f9f3
--- /dev/null
+++ b/widget/gtk/nsWidgetFactory.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 "nsWidgetFactory.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/WidgetUtils.h"
+#include "NativeKeyBindings.h"
+#include "nsWidgetsCID.h"
+#include "nsAppShell.h"
+#include "nsAppShellSingleton.h"
+#include "nsBaseWidget.h"
+#include "nsGtkKeyUtils.h"
+#include "nsLookAndFeel.h"
+#include "nsWindow.h"
+#include "nsHTMLFormatConverter.h"
+#include "HeadlessClipboard.h"
+#include "IMContextWrapper.h"
+#include "nsClipboard.h"
+#include "TaskbarProgress.h"
+#include "nsFilePicker.h"
+#include "nsSound.h"
+#include "nsGTKToolkit.h"
+#include "WakeLockListener.h"
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/widget/ScreenManager.h"
+#include <gtk/gtk.h>
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+NS_IMPL_COMPONENT_FACTORY(nsIClipboard) {
+ nsCOMPtr<nsIClipboard> inst;
+ if (gfxPlatform::IsHeadless()) {
+ inst = new HeadlessClipboard();
+ } else {
+ auto clipboard = MakeRefPtr<nsClipboard>();
+ if (NS_FAILED(clipboard->Init())) {
+ return nullptr;
+ }
+ inst = std::move(clipboard);
+ }
+
+ return inst.forget().downcast<nsISupports>();
+}
+
+nsresult nsWidgetGtk2ModuleCtor() { return nsAppShellInit(); }
+
+void nsWidgetGtk2ModuleDtor() {
+ // Shutdown all XP level widget classes.
+ WidgetUtils::Shutdown();
+
+ NativeKeyBindings::Shutdown();
+ nsLookAndFeel::Shutdown();
+ nsFilePicker::Shutdown();
+ nsSound::Shutdown();
+ nsWindow::ReleaseGlobals();
+ IMContextWrapper::Shutdown();
+ KeymapWrapper::Shutdown();
+ nsGTKToolkit::Shutdown();
+ nsAppShellShutdown();
+}
diff --git a/widget/gtk/nsWidgetFactory.h b/widget/gtk/nsWidgetFactory.h
new file mode 100644
index 0000000000..f9312f6274
--- /dev/null
+++ b/widget/gtk/nsWidgetFactory.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef widget_gtk_nsWidgetFactory_h
+#define widget_gtk_nsWidgetFactory_h
+
+#include "nscore.h"
+#include "nsID.h"
+
+class nsISupports;
+
+nsresult nsAppShellConstructor(const nsIID& iid, void** result);
+
+nsresult nsWidgetGtk2ModuleCtor();
+void nsWidgetGtk2ModuleDtor();
+
+#endif // defined widget_gtk_nsWidgetFactory_h
diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp
new file mode 100644
index 0000000000..8185c7bda9
--- /dev/null
+++ b/widget/gtk/nsWindow.cpp
@@ -0,0 +1,10028 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "nsWindow.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <dlfcn.h>
+#include <gdk/gdkkeysyms.h>
+#include <wchar.h>
+
+#include "VsyncSource.h"
+#include "gfx2DGlue.h"
+#include "gfxContext.h"
+#include "gfxImageSurface.h"
+#include "gfxPlatformGtk.h"
+#include "gfxUtils.h"
+#include "GLContextProvider.h"
+#include "GLContext.h"
+#include "GtkCompositorWidget.h"
+#include "gtkdrawing.h"
+#include "imgIContainer.h"
+#include "InputData.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Components.h"
+#include "mozilla/GRefPtr.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/WheelEventBinding.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/gfx/HelpersCairo.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_mozilla.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/SwipeTracker.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/WidgetUtils.h"
+#include "mozilla/WritingModes.h"
+#ifdef MOZ_X11
+# include "mozilla/X11Util.h"
+#endif
+#include "mozilla/XREAppData.h"
+#include "NativeKeyBindings.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsAppRunner.h"
+#include "nsDragService.h"
+#include "nsGTKToolkit.h"
+#include "nsGtkKeyUtils.h"
+#include "nsGtkCursors.h"
+#include "nsGfxCIID.h"
+#include "nsGtkUtils.h"
+#include "nsIFile.h"
+#include "nsIGSettingsService.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsImageToPixbuf.h"
+#include "nsINode.h"
+#include "nsIRollupListener.h"
+#include "nsIScreenManager.h"
+#include "nsIUserIdleServiceInternal.h"
+#include "nsIWidgetListener.h"
+#include "nsLayoutUtils.h"
+#include "nsMenuPopupFrame.h"
+#include "nsPresContext.h"
+#include "nsShmImage.h"
+#include "nsString.h"
+#include "nsWidgetsCID.h"
+#include "nsViewManager.h"
+#include "nsXPLookAndFeel.h"
+#include "prlink.h"
+#include "Screen.h"
+#include "ScreenHelperGTK.h"
+#include "SystemTimeConverter.h"
+#include "WidgetUtilsGtk.h"
+
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/LocalAccessible.h"
+# include "mozilla/a11y/Platform.h"
+# include "nsAccessibilityService.h"
+#endif
+
+#ifdef MOZ_X11
+# include <gdk/gdkkeysyms-compat.h>
+# include <X11/Xatom.h>
+# include <X11/extensions/XShm.h>
+# include <X11/extensions/shape.h>
+# include "gfxXlibSurface.h"
+# include "GLContextGLX.h" // for GLContextGLX::FindVisual()
+# include "GLContextEGL.h" // for GLContextEGL::FindVisual()
+# include "WindowSurfaceX11Image.h"
+# include "WindowSurfaceX11SHM.h"
+#endif
+#ifdef MOZ_WAYLAND
+# include <gdk/gdkkeysyms-compat.h>
+# include "nsIClipboard.h"
+# include "nsView.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+#ifdef MOZ_X11
+using mozilla::gl::GLContextEGL;
+using mozilla::gl::GLContextGLX;
+#endif
+
+// Don't put more than this many rects in the dirty region, just fluff
+// out to the bounding-box if there are more
+#define MAX_RECTS_IN_REGION 100
+
+#if !GTK_CHECK_VERSION(3, 18, 0)
+
+struct _GdkEventTouchpadPinch {
+ GdkEventType type;
+ GdkWindow* window;
+ gint8 send_event;
+ gint8 phase;
+ gint8 n_fingers;
+ guint32 time;
+ gdouble x;
+ gdouble y;
+ gdouble dx;
+ gdouble dy;
+ gdouble angle_delta;
+ gdouble scale;
+ gdouble x_root, y_root;
+ guint state;
+};
+
+typedef enum {
+ GDK_TOUCHPAD_GESTURE_PHASE_BEGIN,
+ GDK_TOUCHPAD_GESTURE_PHASE_UPDATE,
+ GDK_TOUCHPAD_GESTURE_PHASE_END,
+ GDK_TOUCHPAD_GESTURE_PHASE_CANCEL
+} GdkTouchpadGesturePhase;
+
+gint GDK_TOUCHPAD_GESTURE_MASK = 1 << 24;
+GdkEventType GDK_TOUCHPAD_PINCH = static_cast<GdkEventType>(42);
+
+#endif
+
+const gint kEvents = GDK_TOUCHPAD_GESTURE_MASK | GDK_EXPOSURE_MASK |
+ GDK_STRUCTURE_MASK | GDK_VISIBILITY_NOTIFY_MASK |
+ GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_SMOOTH_SCROLL_MASK | GDK_TOUCH_MASK | GDK_SCROLL_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_PROPERTY_CHANGE_MASK;
+
+/* utility functions */
+static bool is_mouse_in_window(GdkWindow* aWindow, gdouble aMouseX,
+ gdouble aMouseY);
+static nsWindow* get_window_for_gtk_widget(GtkWidget* widget);
+static nsWindow* get_window_for_gdk_window(GdkWindow* window);
+static GtkWidget* get_gtk_widget_for_gdk_window(GdkWindow* window);
+static GdkCursor* get_gtk_cursor(nsCursor aCursor);
+static GdkWindow* get_inner_gdk_window(GdkWindow* aWindow, gint x, gint y,
+ gint* retx, gint* rety);
+
+/* callbacks from widgets */
+static gboolean expose_event_cb(GtkWidget* widget, cairo_t* cr);
+static gboolean configure_event_cb(GtkWidget* widget, GdkEventConfigure* event);
+static void widget_map_cb(GtkWidget* widget);
+static void widget_unmap_cb(GtkWidget* widget);
+static void size_allocate_cb(GtkWidget* widget, GtkAllocation* allocation);
+static void toplevel_window_size_allocate_cb(GtkWidget* widget,
+ GtkAllocation* allocation);
+static gboolean delete_event_cb(GtkWidget* widget, GdkEventAny* event);
+static gboolean enter_notify_event_cb(GtkWidget* widget,
+ GdkEventCrossing* event);
+static gboolean leave_notify_event_cb(GtkWidget* widget,
+ GdkEventCrossing* event);
+static gboolean motion_notify_event_cb(GtkWidget* widget,
+ GdkEventMotion* event);
+MOZ_CAN_RUN_SCRIPT static gboolean button_press_event_cb(GtkWidget* widget,
+ GdkEventButton* event);
+static gboolean button_release_event_cb(GtkWidget* widget,
+ GdkEventButton* event);
+static gboolean focus_in_event_cb(GtkWidget* widget, GdkEventFocus* event);
+static gboolean focus_out_event_cb(GtkWidget* widget, GdkEventFocus* event);
+static gboolean key_press_event_cb(GtkWidget* widget, GdkEventKey* event);
+static gboolean key_release_event_cb(GtkWidget* widget, GdkEventKey* event);
+static gboolean property_notify_event_cb(GtkWidget* widget,
+ GdkEventProperty* event);
+static gboolean scroll_event_cb(GtkWidget* widget, GdkEventScroll* event);
+static gboolean visibility_notify_event_cb(GtkWidget* widget,
+ GdkEventVisibility* event);
+static void hierarchy_changed_cb(GtkWidget* widget,
+ GtkWidget* previous_toplevel);
+static gboolean window_state_event_cb(GtkWidget* widget,
+ GdkEventWindowState* event);
+static void settings_xft_dpi_changed_cb(GtkSettings* settings,
+ GParamSpec* pspec, nsWindow* data);
+static void check_resize_cb(GtkContainer* container, gpointer user_data);
+static void screen_composited_changed_cb(GdkScreen* screen, gpointer user_data);
+static void widget_composited_changed_cb(GtkWidget* widget, gpointer user_data);
+
+static void scale_changed_cb(GtkWidget* widget, GParamSpec* aPSpec,
+ gpointer aPointer);
+static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent);
+static gboolean generic_event_cb(GtkWidget* widget, GdkEvent* aEvent);
+
+static nsWindow* GetFirstNSWindowForGDKWindow(GdkWindow* aGdkWindow);
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+#ifdef MOZ_X11
+static GdkFilterReturn popup_take_focus_filter(GdkXEvent* gdk_xevent,
+ GdkEvent* event, gpointer data);
+#endif /* MOZ_X11 */
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+static gboolean drag_motion_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX,
+ gint aY, guint aTime, gpointer aData);
+static void drag_leave_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, guint aTime,
+ gpointer aData);
+static gboolean drag_drop_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX,
+ gint aY, guint aTime, gpointer aData);
+static void drag_data_received_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX,
+ gint aY,
+ GtkSelectionData* aSelectionData,
+ guint aInfo, guint32 aTime,
+ gpointer aData);
+
+/* initialization static functions */
+static nsresult initialize_prefs(void);
+
+static guint32 sLastUserInputTime = GDK_CURRENT_TIME;
+
+static SystemTimeConverter<guint32>& TimeConverter() {
+ static SystemTimeConverter<guint32> sTimeConverterSingleton;
+ return sTimeConverterSingleton;
+}
+
+bool nsWindow::sTransparentMainWindow = false;
+
+// forward declare from mozgtk
+extern "C" MOZ_EXPORT void mozgtk_linker_holder();
+
+namespace mozilla {
+
+#ifdef MOZ_X11
+class CurrentX11TimeGetter {
+ public:
+ explicit CurrentX11TimeGetter(GdkWindow* aWindow) : mWindow(aWindow) {}
+
+ guint32 GetCurrentTime() const { return gdk_x11_get_server_time(mWindow); }
+
+ void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) {
+ // Check for in-flight request
+ if (!mAsyncUpdateStart.IsNull()) {
+ return;
+ }
+ mAsyncUpdateStart = aNow;
+
+ Display* xDisplay = GDK_WINDOW_XDISPLAY(mWindow);
+ Window xWindow = GDK_WINDOW_XID(mWindow);
+ unsigned char c = 'a';
+ Atom timeStampPropAtom = TimeStampPropAtom();
+ XChangeProperty(xDisplay, xWindow, timeStampPropAtom, timeStampPropAtom, 8,
+ PropModeReplace, &c, 1);
+ XFlush(xDisplay);
+ }
+
+ gboolean PropertyNotifyHandler(GtkWidget* aWidget, GdkEventProperty* aEvent) {
+ if (aEvent->atom != gdk_x11_xatom_to_atom(TimeStampPropAtom())) {
+ return FALSE;
+ }
+
+ guint32 eventTime = aEvent->time;
+ TimeStamp lowerBound = mAsyncUpdateStart;
+
+ TimeConverter().CompensateForBackwardsSkew(eventTime, lowerBound);
+ mAsyncUpdateStart = TimeStamp();
+ return TRUE;
+ }
+
+ private:
+ static Atom TimeStampPropAtom() {
+ return gdk_x11_get_xatom_by_name_for_display(gdk_display_get_default(),
+ "GDK_TIMESTAMP_PROP");
+ }
+
+ // This is safe because this class is stored as a member of mWindow and
+ // won't outlive it.
+ GdkWindow* mWindow;
+ TimeStamp mAsyncUpdateStart;
+};
+#endif
+
+} // namespace mozilla
+
+// The window from which the focus manager asks us to dispatch key events.
+static nsWindow* gFocusWindow = nullptr;
+static bool gBlockActivateEvent = false;
+static bool gGlobalsInitialized = false;
+static bool gUseAspectRatio = true;
+static uint32_t gLastTouchID = 0;
+// See Bug 1777269 for details. We don't know if the suspected leave notify
+// event is a correct one when we get it.
+// Store it and issue it later from enter notify event if it's correct,
+// throw it away otherwise.
+static GUniquePtr<GdkEventCrossing> sStoredLeaveNotifyEvent;
+
+#define NS_WINDOW_TITLE_MAX_LENGTH 4095
+
+// cursor cache
+static GdkCursor* gCursorCache[eCursorCount];
+
+// Sometimes this actually also includes the state of the modifier keys, but
+// only the button state bits are used.
+static guint gButtonState;
+
+static inline int32_t GetBitmapStride(int32_t width) {
+#if defined(MOZ_X11)
+ return (width + 7) / 8;
+#else
+ return cairo_format_stride_for_width(CAIRO_FORMAT_A1, width);
+#endif
+}
+
+static inline bool TimestampIsNewerThan(guint32 a, guint32 b) {
+ // Timestamps are just the least significant bits of a monotonically
+ // increasing function, and so the use of unsigned overflow arithmetic.
+ return a - b <= G_MAXUINT32 / 2;
+}
+
+static void UpdateLastInputEventTime(void* aGdkEvent) {
+ nsCOMPtr<nsIUserIdleServiceInternal> idleService =
+ do_GetService("@mozilla.org/widget/useridleservice;1");
+ if (idleService) {
+ idleService->ResetIdleTimeOut(0);
+ }
+
+ guint timestamp = gdk_event_get_time(static_cast<GdkEvent*>(aGdkEvent));
+ if (timestamp == GDK_CURRENT_TIME) {
+ return;
+ }
+
+ sLastUserInputTime = timestamp;
+}
+
+// Don't set parent (transient for) if nothing changes.
+// gtk_window_set_transient_for() blows up wl_subsurfaces used by aWindow
+// even if aParent is the same.
+static void GtkWindowSetTransientFor(GtkWindow* aWindow, GtkWindow* aParent) {
+ GtkWindow* parent = gtk_window_get_transient_for(aWindow);
+ if (parent != aParent) {
+ gtk_window_set_transient_for(aWindow, aParent);
+ }
+}
+
+#define gtk_window_set_transient_for(a, b) \
+ { \
+ MOZ_ASSERT_UNREACHABLE( \
+ "gtk_window_set_transient_for() can't be used directly."); \
+ }
+
+nsWindow::nsWindow()
+ : mTitlebarRectMutex("nsWindow::mTitlebarRectMutex"),
+ mDestroyMutex("nsWindow::mDestroyMutex"),
+ mIsDestroyed(false),
+ mIsShown(false),
+ mNeedsShow(false),
+ mIsMapped(false),
+ mEnabled(true),
+ mCreated(false),
+ mHandleTouchEvent(false),
+ mIsDragPopup(false),
+ mCompositedScreen(gdk_screen_is_composited(gdk_screen_get_default())),
+ mIsAccelerated(false),
+ mWindowShouldStartDragging(false),
+ mHasMappedToplevel(false),
+ mRetryPointerGrab(false),
+ mPanInProgress(false),
+ mTitlebarBackdropState(false),
+ mIsChildWindow(false),
+ mAlwaysOnTop(false),
+ mNoAutoHide(false),
+ mIsTransparent(false),
+ mHasReceivedSizeAllocate(false),
+ mWidgetCursorLocked(false),
+ mUndecorated(false),
+ mPopupTrackInHierarchy(false),
+ mPopupTrackInHierarchyConfigured(false),
+ mHiddenPopupPositioned(false),
+ mTransparencyBitmapForTitlebar(false),
+ mHasAlphaVisual(false),
+ mPopupAnchored(false),
+ mPopupContextMenu(false),
+ mPopupMatchesLayout(false),
+ mPopupChanged(false),
+ mPopupTemporaryHidden(false),
+ mPopupClosed(false),
+ mPopupUseMoveToRect(false),
+ mWaitingForMoveToRectCallback(false),
+ mMovedAfterMoveToRect(false),
+ mResizedAfterMoveToRect(false),
+ mConfiguredClearColor(false),
+ mGotNonBlankPaint(false),
+ mNeedsToRetryCapturingMouse(false) {
+ mWindowType = WindowType::Child;
+ mSizeConstraints.mMaxSize = GetSafeWindowSize(mSizeConstraints.mMaxSize);
+
+ if (!gGlobalsInitialized) {
+ gGlobalsInitialized = true;
+
+ // It's OK if either of these fail, but it may not be one day.
+ initialize_prefs();
+
+#ifdef MOZ_WAYLAND
+ // Wayland provides clipboard data to application on focus-in event
+ // so we need to init our clipboard hooks before we create window
+ // and get focus.
+ if (GdkIsWaylandDisplay()) {
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1");
+ NS_ASSERTION(clipboard, "Failed to init clipboard!");
+ }
+#endif
+ }
+ // Dummy call to mozgtk to prevent the linker from removing
+ // the dependency with --as-needed.
+ // see toolkit/library/moz.build for details.
+ mozgtk_linker_holder();
+}
+
+nsWindow::~nsWindow() {
+ LOG("nsWindow::~nsWindow()");
+ Destroy();
+}
+
+/* static */
+void nsWindow::ReleaseGlobals() {
+ for (auto& cursor : gCursorCache) {
+ if (cursor) {
+ g_object_unref(cursor);
+ cursor = nullptr;
+ }
+ }
+}
+
+void nsWindow::DispatchActivateEvent(void) {
+#ifdef ACCESSIBILITY
+ DispatchActivateEventAccessible();
+#endif // ACCESSIBILITY
+
+ if (mWidgetListener) mWidgetListener->WindowActivated();
+}
+
+void nsWindow::DispatchDeactivateEvent() {
+ if (mWidgetListener) {
+ mWidgetListener->WindowDeactivated();
+ }
+
+#ifdef ACCESSIBILITY
+ DispatchDeactivateEventAccessible();
+#endif // ACCESSIBILITY
+}
+
+void nsWindow::DispatchResized() {
+ LOG("nsWindow::DispatchResized() size [%d, %d]", (int)(mBounds.width),
+ (int)(mBounds.height));
+
+ mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1);
+ if (mWidgetListener) {
+ mWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
+ }
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
+ }
+}
+
+void nsWindow::MaybeDispatchResized() {
+ if (mNeedsDispatchSize != LayoutDeviceIntSize(-1, -1) && !mIsDestroyed) {
+ mBounds.SizeTo(mNeedsDispatchSize);
+ // Check mBounds size
+ if (mCompositorSession &&
+ !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) {
+ gfxCriticalNoteOnce << "Invalid mBounds in MaybeDispatchResized "
+ << mBounds << " size state " << mSizeMode;
+ }
+
+ if (IsTopLevelWindowType()) {
+ UpdateTopLevelOpaqueRegion();
+ }
+
+ // Notify the GtkCompositorWidget of a ClientSizeChange
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize());
+ }
+
+ DispatchResized();
+ }
+}
+
+nsIWidgetListener* nsWindow::GetListener() {
+ return mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener;
+}
+
+nsresult nsWindow::DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) {
+#ifdef DEBUG
+ debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "something", 0);
+#endif
+ aStatus = nsEventStatus_eIgnore;
+ nsIWidgetListener* listener = GetListener();
+ if (listener) {
+ aStatus = listener->HandleEvent(aEvent, mUseAttachedEvents);
+ }
+
+ return NS_OK;
+}
+
+void nsWindow::OnDestroy(void) {
+ if (mOnDestroyCalled) {
+ return;
+ }
+
+ mOnDestroyCalled = true;
+
+ // Prevent deletion.
+ nsCOMPtr<nsIWidget> kungFuDeathGrip = this;
+
+ // release references to children, device context, toolkit + app shell
+ nsBaseWidget::OnDestroy();
+
+ // Remove association between this object and its parent and siblings.
+ nsBaseWidget::Destroy();
+ mParent = nullptr;
+
+ NotifyWindowDestroyed();
+}
+
+bool nsWindow::AreBoundsSane() {
+ // Check requested size, as mBounds might not have been updated.
+ return !mLastSizeRequest.IsEmpty();
+}
+
+// Walk the list of child windows and call destroy on them.
+void nsWindow::DestroyChildWindows() {
+ LOG("nsWindow::DestroyChildWindows()");
+ if (!mGdkWindow) {
+ return;
+ }
+ while (GList* children = gdk_window_peek_children(mGdkWindow)) {
+ GdkWindow* child = GDK_WINDOW(children->data);
+ nsWindow* kid = get_window_for_gdk_window(child);
+ if (kid) {
+ kid->Destroy();
+ }
+ }
+}
+
+void nsWindow::Destroy() {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ if (mIsDestroyed || !mCreated) {
+ return;
+ }
+
+ LOG("nsWindow::Destroy\n");
+
+ MutexAutoLock lock(mDestroyMutex);
+ mIsDestroyed = true;
+ mCreated = false;
+
+ MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove);
+
+#ifdef MOZ_WAYLAND
+ // Shut down our local vsync source
+ if (mWaylandVsyncSource) {
+ mWaylandVsyncSource->Shutdown();
+ mWaylandVsyncSource = nullptr;
+ }
+ mWaylandVsyncDispatcher = nullptr;
+#endif
+
+ // dragService will be null after shutdown of the service manager.
+ RefPtr<nsDragService> dragService = nsDragService::GetInstance();
+ if (dragService && this == dragService->GetMostRecentDestWindow()) {
+ dragService->ScheduleLeaveEvent();
+ }
+
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ if (rollupListener) {
+ nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
+ if (static_cast<nsIWidget*>(this) == rollupWidget) {
+ rollupListener->Rollup({});
+ }
+ }
+
+ NativeShow(false);
+
+ ClearTransparencyBitmap();
+
+ DestroyLayerManager();
+
+ // mSurfaceProvider holds reference to this nsWindow so we need to explicitly
+ // clear it here to avoid nsWindow leak.
+ mSurfaceProvider.CleanupResources();
+
+ g_signal_handlers_disconnect_by_data(gtk_settings_get_default(), this);
+
+ if (mIMContext) {
+ mIMContext->OnDestroyWindow(this);
+ }
+
+ // make sure that we remove ourself as the focus window
+ if (gFocusWindow == this) {
+ LOG("automatically losing focus...\n");
+ gFocusWindow = nullptr;
+ }
+
+ if (sStoredLeaveNotifyEvent) {
+ nsWindow* window =
+ get_window_for_gdk_window(sStoredLeaveNotifyEvent->window);
+ if (window == this) {
+ sStoredLeaveNotifyEvent = nullptr;
+ }
+ }
+
+ // We need to detach accessible object here because mContainer is a custom
+ // widget and doesn't call gtk_widget_real_destroy() from destroy handler
+ // as regular widgets.
+ if (AtkObject* ac = gtk_widget_get_accessible(GTK_WIDGET(mContainer))) {
+ gtk_accessible_set_widget(GTK_ACCESSIBLE(ac), nullptr);
+ }
+
+ gtk_widget_destroy(mShell);
+ mShell = nullptr;
+ mContainer = nullptr;
+
+ MOZ_ASSERT(!mGdkWindow,
+ "mGdkWindow should be NULL when mContainer is destroyed");
+
+#ifdef ACCESSIBILITY
+ if (mRootAccessible) {
+ mRootAccessible = nullptr;
+ }
+#endif
+
+ // Save until last because OnDestroy() may cause us to be deleted.
+ OnDestroy();
+}
+
+nsIWidget* nsWindow::GetParent() { return mParent; }
+
+float nsWindow::GetDPI() {
+ float dpi = 96.0f;
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ screen->GetDpi(&dpi);
+ }
+ return dpi;
+}
+
+double nsWindow::GetDefaultScaleInternal() { return FractionalScaleFactor(); }
+
+DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScale() {
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ return DesktopToLayoutDeviceScale(FractionalScaleFactor());
+ }
+#endif
+
+ // In Gtk/X11, we manage windows using device pixels.
+ return DesktopToLayoutDeviceScale(1.0);
+}
+
+DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScaleByScreen() {
+#ifdef MOZ_WAYLAND
+ // In Wayland there's no way to get absolute position of the window and use it
+ // to determine the screen factor of the monitor on which the window is
+ // placed. The window is notified of the current scale factor but not at this
+ // point, so the GdkScaleFactor can return wrong value which can lead to wrong
+ // popup placement. We need to use parent's window scale factor for the new
+ // one.
+ if (GdkIsWaylandDisplay()) {
+ nsView* view = nsView::GetViewFor(this);
+ if (view) {
+ nsView* parentView = view->GetParent();
+ if (parentView) {
+ nsIWidget* parentWidget = parentView->GetNearestWidget(nullptr);
+ if (parentWidget) {
+ return DesktopToLayoutDeviceScale(
+ parentWidget->RoundsWidgetCoordinatesTo());
+ }
+ NS_WARNING("Widget has no parent");
+ }
+ } else {
+ NS_WARNING("Cannot find widget view");
+ }
+ }
+#endif
+ return nsBaseWidget::GetDesktopToDeviceScale();
+}
+
+// Reparent a child window to a new parent.
+void nsWindow::SetParent(nsIWidget* aNewParent) {
+ LOG("nsWindow::SetParent() new parent %p", aNewParent);
+ if (!mIsChildWindow) {
+ NS_WARNING("Used by child widgets only");
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> kungFuDeathGrip = this;
+ if (mParent) {
+ mParent->RemoveChild(this);
+ }
+ mParent = aNewParent;
+
+ // We're already deleted, quit.
+ if (!mGdkWindow || mIsDestroyed || !aNewParent) {
+ return;
+ }
+ aNewParent->AddChild(this);
+
+ auto* newParent = static_cast<nsWindow*>(aNewParent);
+
+ // New parent is deleted, quit.
+ if (newParent->mIsDestroyed) {
+ Destroy();
+ return;
+ }
+
+ GdkWindow* window = GetToplevelGdkWindow();
+ GdkWindow* parentWindow = newParent->GetToplevelGdkWindow();
+ LOG(" child GdkWindow %p set parent GdkWindow %p", window, parentWindow);
+ gdk_window_reparent(window, parentWindow, 0, 0);
+ SetHasMappedToplevel(newParent && newParent->mHasMappedToplevel);
+}
+
+bool nsWindow::WidgetTypeSupportsAcceleration() {
+ if (mWindowType == WindowType::Invisible) {
+ return false;
+ }
+
+ if (IsSmallPopup()) {
+ return false;
+ }
+ // Workaround for Bug 1479135
+ // We draw transparent popups on non-compositing screens by SW as we don't
+ // implement X shape masks in WebRender.
+ if (mWindowType == WindowType::Popup) {
+ return HasRemoteContent() && mCompositedScreen;
+ }
+
+ return true;
+}
+
+void nsWindow::ReparentNativeWidget(nsIWidget* aNewParent) {
+ MOZ_ASSERT(aNewParent, "null widget");
+ MOZ_ASSERT(!mIsDestroyed, "");
+ MOZ_ASSERT(!static_cast<nsWindow*>(aNewParent)->mIsDestroyed, "");
+ MOZ_ASSERT(
+ !mParent,
+ "nsWindow::ReparentNativeWidget() works on toplevel windows only.");
+
+ auto* newParent = static_cast<nsWindow*>(aNewParent);
+ GtkWindow* newParentWidget = GTK_WINDOW(newParent->GetGtkWidget());
+
+ LOG("nsWindow::ReparentNativeWidget new parent %p\n", newParent);
+ GtkWindowSetTransientFor(GTK_WINDOW(mShell), newParentWidget);
+}
+
+void nsWindow::SetModal(bool aModal) {
+ LOG("nsWindow::SetModal %d\n", aModal);
+ if (mIsDestroyed) {
+ return;
+ }
+
+ gtk_window_set_modal(GTK_WINDOW(mShell), aModal ? TRUE : FALSE);
+}
+
+// nsIWidget method, which means IsShown.
+bool nsWindow::IsVisible() const { return mIsShown; }
+
+bool nsWindow::IsMapped() const { return mIsMapped; }
+
+void nsWindow::RegisterTouchWindow() {
+ mHandleTouchEvent = true;
+ mTouches.Clear();
+}
+
+LayoutDeviceIntPoint nsWindow::GetScreenEdgeSlop() {
+ if (DrawsToCSDTitlebar()) {
+ return GetClientOffset();
+ }
+ return {};
+}
+
+void nsWindow::ConstrainPosition(DesktopIntPoint& aPoint) {
+ if (!mShell || GdkIsWaylandDisplay()) {
+ return;
+ }
+
+ double dpiScale = GetDefaultScale().scale;
+
+ // we need to use the window size in logical screen pixels
+ int32_t logWidth = std::max(NSToIntRound(mBounds.width / dpiScale), 1);
+ int32_t logHeight = std::max(NSToIntRound(mBounds.height / dpiScale), 1);
+
+ /* get our playing field. use the current screen, or failing that
+ for any reason, use device caps for the default screen. */
+ nsCOMPtr<nsIScreenManager> screenmgr =
+ do_GetService("@mozilla.org/gfx/screenmanager;1");
+ if (!screenmgr) {
+ return;
+ }
+ nsCOMPtr<nsIScreen> screen;
+ screenmgr->ScreenForRect(aPoint.x, aPoint.y, logWidth, logHeight,
+ getter_AddRefs(screen));
+ // We don't have any screen so leave the coordinates as is
+ if (!screen) {
+ return;
+ }
+
+ // For normalized windows, use the desktop work area.
+ // For full screen windows, use the desktop.
+ DesktopIntRect screenRect = mSizeMode == nsSizeMode_Fullscreen
+ ? screen->GetRectDisplayPix()
+ : screen->GetAvailRectDisplayPix();
+
+ // Expand for the decoration size if needed.
+ auto slop =
+ DesktopIntPoint::Round(GetScreenEdgeSlop() / GetDesktopToDeviceScale());
+ screenRect.Inflate(slop.x, slop.y);
+
+ if (aPoint.x < screenRect.x) {
+ aPoint.x = screenRect.x;
+ } else if (aPoint.x >= screenRect.XMost() - logWidth) {
+ aPoint.x = screenRect.XMost() - logWidth;
+ }
+
+ if (aPoint.y < screenRect.y) {
+ aPoint.y = screenRect.y;
+ } else if (aPoint.y >= screenRect.YMost() - logHeight) {
+ aPoint.y = screenRect.YMost() - logHeight;
+ }
+}
+
+void nsWindow::SetSizeConstraints(const SizeConstraints& aConstraints) {
+ mSizeConstraints.mMinSize = GetSafeWindowSize(aConstraints.mMinSize);
+ mSizeConstraints.mMaxSize = GetSafeWindowSize(aConstraints.mMaxSize);
+
+ ApplySizeConstraints();
+}
+
+bool nsWindow::DrawsToCSDTitlebar() const {
+ return mSizeMode == nsSizeMode_Normal &&
+ mGtkWindowDecoration == GTK_DECORATION_CLIENT && mDrawInTitlebar;
+}
+
+void nsWindow::AddCSDDecorationSize(int* aWidth, int* aHeight) {
+ if (mSizeMode != nsSizeMode_Normal || mUndecorated ||
+ mGtkWindowDecoration != GTK_DECORATION_CLIENT || !GdkIsWaylandDisplay() ||
+ !IsGnomeDesktopEnvironment()) {
+ return;
+ }
+
+ GtkBorder decorationSize = GetCSDDecorationSize(IsPopup());
+ *aWidth += decorationSize.left + decorationSize.right;
+ *aHeight += decorationSize.top + decorationSize.bottom;
+}
+
+#ifdef MOZ_WAYLAND
+bool nsWindow::GetCSDDecorationOffset(int* aDx, int* aDy) {
+ if (!DrawsToCSDTitlebar()) {
+ return false;
+ }
+ GtkBorder decorationSize = GetCSDDecorationSize(IsPopup());
+ *aDx = decorationSize.left;
+ *aDy = decorationSize.top;
+ return true;
+}
+#endif
+
+void nsWindow::ApplySizeConstraints() {
+ if (mShell) {
+ GdkGeometry geometry;
+ geometry.min_width =
+ DevicePixelsToGdkCoordRoundUp(mSizeConstraints.mMinSize.width);
+ geometry.min_height =
+ DevicePixelsToGdkCoordRoundUp(mSizeConstraints.mMinSize.height);
+ geometry.max_width =
+ DevicePixelsToGdkCoordRoundDown(mSizeConstraints.mMaxSize.width);
+ geometry.max_height =
+ DevicePixelsToGdkCoordRoundDown(mSizeConstraints.mMaxSize.height);
+
+ uint32_t hints = 0;
+ if (mSizeConstraints.mMinSize != LayoutDeviceIntSize()) {
+ if (GdkIsWaylandDisplay()) {
+ gtk_widget_set_size_request(GTK_WIDGET(mContainer), geometry.min_width,
+ geometry.min_height);
+ }
+ AddCSDDecorationSize(&geometry.min_width, &geometry.min_height);
+ hints |= GDK_HINT_MIN_SIZE;
+ }
+ if (mSizeConstraints.mMaxSize !=
+ LayoutDeviceIntSize(NS_MAXSIZE, NS_MAXSIZE)) {
+ AddCSDDecorationSize(&geometry.max_width, &geometry.max_height);
+ hints |= GDK_HINT_MAX_SIZE;
+ }
+
+ if (mAspectRatio != 0.0f && !mAspectResizer) {
+ geometry.min_aspect = mAspectRatio;
+ geometry.max_aspect = mAspectRatio;
+ hints |= GDK_HINT_ASPECT;
+ }
+
+ gtk_window_set_geometry_hints(GTK_WINDOW(mShell), nullptr, &geometry,
+ GdkWindowHints(hints));
+ }
+}
+
+void nsWindow::Show(bool aState) {
+ if (aState == mIsShown) {
+ return;
+ }
+
+ mIsShown = aState;
+
+#ifdef MOZ_LOGGING
+ LOG("nsWindow::Show state %d frame %s\n", aState, GetFrameTag().get());
+ if (!aState && mSourceDragContext && GdkIsWaylandDisplay()) {
+ LOG(" closing Drag&Drop source window, D&D will be canceled!");
+ }
+#endif
+
+ // Ok, someone called show on a window that isn't sized to a sane
+ // value. Mark this window as needing to have Show() called on it
+ // and return.
+ if ((aState && !AreBoundsSane()) || !mCreated) {
+ LOG("\tbounds are insane or window hasn't been created yet\n");
+ mNeedsShow = true;
+ return;
+ }
+
+ // If someone is hiding this widget, clear any needing show flag.
+ if (!aState) mNeedsShow = false;
+
+#ifdef ACCESSIBILITY
+ if (aState && a11y::ShouldA11yBeEnabled()) CreateRootAccessible();
+#endif
+
+ NativeShow(aState);
+ RefreshWindowClass();
+}
+
+void nsWindow::ResizeInt(const Maybe<LayoutDeviceIntPoint>& aMove,
+ LayoutDeviceIntSize aSize) {
+ LOG("nsWindow::ResizeInt w:%d h:%d\n", aSize.width, aSize.height);
+ const bool moved = aMove && *aMove != mBounds.TopLeft();
+ if (moved) {
+ mBounds.MoveTo(*aMove);
+ LOG(" with move to left:%d top:%d", aMove->x.value, aMove->y.value);
+ }
+
+ ConstrainSize(&aSize.width, &aSize.height);
+ LOG(" ConstrainSize: w:%d h;%d\n", aSize.width, aSize.height);
+
+ const bool resized = aSize != mLastSizeRequest || mBounds.Size() != aSize;
+#if MOZ_LOGGING
+ LOG(" resized %d aSize [%d, %d] mLastSizeRequest [%d, %d] mBounds [%d, %d]",
+ resized, aSize.width, aSize.height, mLastSizeRequest.width,
+ mLastSizeRequest.height, mBounds.width, mBounds.height);
+#endif
+
+ // For top-level windows, aSize should possibly be
+ // interpreted as frame bounds, but NativeMoveResize treats these as window
+ // bounds (Bug 581866).
+ mLastSizeRequest = aSize;
+ // Check size
+ if (mCompositorSession &&
+ !wr::WindowSizeSanityCheck(aSize.width, aSize.height)) {
+ gfxCriticalNoteOnce << "Invalid aSize in ResizeInt " << aSize
+ << " size state " << mSizeMode;
+ }
+
+ // Recalculate aspect ratio when resized from DOM
+ if (mAspectRatio != 0.0) {
+ LockAspectRatio(true);
+ }
+
+ if (!mCreated) {
+ return;
+ }
+
+ if (!moved && !resized) {
+ LOG(" not moved or resized, quit");
+ return;
+ }
+
+ NativeMoveResize(moved, resized);
+
+ // We optimistically assume size changes immediately in two cases:
+ // 1. Override-redirect window: Size is controlled by only us.
+ // 2. Managed window that has not not yet received a size-allocate event:
+ // Resize() Callers expect initial sizes to be applied synchronously.
+ // If the size request is not honored, then we'll correct in
+ // OnSizeAllocate().
+ //
+ // When a managed window has already received a size-allocate, we cannot
+ // assume we'll always get a notification if our request does not get
+ // honored: "If the configure request has not changed, we don't ever resend
+ // it, because it could mean fighting the user or window manager."
+ // https://gitlab.gnome.org/GNOME/gtk/-/blob/3.24.31/gtk/gtkwindow.c#L9782
+ // So we don't update mBounds until OnSizeAllocate() when we know the
+ // request is granted.
+ bool isOrWillBeVisible = mHasReceivedSizeAllocate || mNeedsShow || mIsShown;
+ if (!isOrWillBeVisible ||
+ gtk_window_get_window_type(GTK_WINDOW(mShell)) == GTK_WINDOW_POPUP) {
+ mBounds.SizeTo(aSize);
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyClientSizeChanged(aSize);
+ }
+ DispatchResized();
+ }
+}
+
+void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
+ LOG("nsWindow::Resize %f %f\n", aWidth, aHeight);
+
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ auto size = LayoutDeviceIntSize::Round(scale * aWidth, scale * aHeight);
+
+ ResizeInt(Nothing(), size);
+}
+
+void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ LOG("nsWindow::Resize [%f,%f] -> [%f x %f] repaint %d\n", aX, aY, aWidth,
+ aHeight, aRepaint);
+
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ auto size = LayoutDeviceIntSize::Round(scale * aWidth, scale * aHeight);
+ auto topLeft = LayoutDeviceIntPoint::Round(scale * aX, scale * aY);
+
+ ResizeInt(Some(topLeft), size);
+}
+
+void nsWindow::Enable(bool aState) { mEnabled = aState; }
+
+bool nsWindow::IsEnabled() const { return mEnabled; }
+
+void nsWindow::Move(double aX, double aY) {
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ int32_t x = NSToIntRound(aX * scale);
+ int32_t y = NSToIntRound(aY * scale);
+
+ LOG("nsWindow::Move to %d x %d\n", x, y);
+
+ if (mSizeMode != nsSizeMode_Normal && IsTopLevelWindowType()) {
+ LOG(" size state is not normal, bailing");
+ return;
+ }
+
+ // Since a popup window's x/y coordinates are in relation to to
+ // the parent, the parent might have moved so we always move a
+ // popup window.
+ LOG(" bounds %d x %d\n", mBounds.x, mBounds.y);
+ if (x == mBounds.x && y == mBounds.y && mWindowType != WindowType::Popup) {
+ LOG(" position is the same, return\n");
+ return;
+ }
+
+ // XXX Should we do some AreBoundsSane check here?
+
+ mBounds.x = x;
+ mBounds.y = y;
+
+ if (!mCreated) {
+ LOG(" is not created, return.\n");
+ return;
+ }
+
+ NativeMoveResize(/* move */ true, /* resize */ false);
+}
+
+bool nsWindow::IsPopup() const { return mWindowType == WindowType::Popup; }
+
+bool nsWindow::IsWaylandPopup() const {
+ return GdkIsWaylandDisplay() && IsPopup();
+}
+
+static nsMenuPopupFrame* GetMenuPopupFrame(nsIFrame* aFrame) {
+ return do_QueryFrame(aFrame);
+}
+
+void nsWindow::AppendPopupToHierarchyList(nsWindow* aToplevelWindow) {
+ mWaylandToplevel = aToplevelWindow;
+
+ nsWindow* popup = aToplevelWindow;
+ while (popup && popup->mWaylandPopupNext) {
+ popup = popup->mWaylandPopupNext;
+ }
+ popup->mWaylandPopupNext = this;
+
+ mWaylandPopupPrev = popup;
+ mWaylandPopupNext = nullptr;
+ mPopupChanged = true;
+ mPopupClosed = false;
+}
+
+void nsWindow::RemovePopupFromHierarchyList() {
+ // We're already removed from the popup hierarchy
+ if (!IsInPopupHierarchy()) {
+ return;
+ }
+ mWaylandPopupPrev->mWaylandPopupNext = mWaylandPopupNext;
+ if (mWaylandPopupNext) {
+ mWaylandPopupNext->mWaylandPopupPrev = mWaylandPopupPrev;
+ mWaylandPopupNext->mPopupChanged = true;
+ }
+ mWaylandPopupNext = mWaylandPopupPrev = nullptr;
+}
+
+// Gtk refuses to map popup window with x < 0 && y < 0 relative coordinates
+// see https://gitlab.gnome.org/GNOME/gtk/-/issues/4071
+// as a workaround just fool around and place the popup temporary to 0,0.
+bool nsWindow::WaylandPopupRemoveNegativePosition(int* aX, int* aY) {
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/4071 applies to temporary
+ // windows only
+ GdkWindow* window = GetToplevelGdkWindow();
+ if (!window || gdk_window_get_window_type(window) != GDK_WINDOW_TEMP) {
+ return false;
+ }
+
+ LOG("nsWindow::WaylandPopupRemoveNegativePosition()");
+
+ int x, y;
+ gtk_window_get_position(GTK_WINDOW(mShell), &x, &y);
+ bool moveBack = (x < 0 && y < 0);
+ if (moveBack) {
+ gtk_window_move(GTK_WINDOW(mShell), 0, 0);
+ if (aX) {
+ *aX = x;
+ }
+ if (aY) {
+ *aY = y;
+ }
+ }
+
+ gdk_window_get_geometry(window, &x, &y, nullptr, nullptr);
+ if (x < 0 && y < 0) {
+ gdk_window_move(window, 0, 0);
+ }
+
+ return moveBack;
+}
+
+void nsWindow::ShowWaylandPopupWindow() {
+ LOG("nsWindow::ShowWaylandPopupWindow. Expected to see visible.");
+ MOZ_ASSERT(IsWaylandPopup());
+
+ if (!mPopupTrackInHierarchy) {
+ LOG(" popup is not tracked in popup hierarchy, show it now");
+ gtk_widget_show(mShell);
+ return;
+ }
+
+ // Popup position was checked before gdk_window_move_to_rect() callback
+ // so just show it.
+ if (mPopupUseMoveToRect && mWaitingForMoveToRectCallback) {
+ LOG(" active move-to-rect callback, show it as is");
+ gtk_widget_show(mShell);
+ return;
+ }
+
+ if (gtk_widget_is_visible(mShell)) {
+ LOG(" is already visible, quit");
+ return;
+ }
+
+ int x, y;
+ bool moved = WaylandPopupRemoveNegativePosition(&x, &y);
+ gtk_widget_show(mShell);
+ if (moved) {
+ LOG(" move back to (%d, %d) and show", x, y);
+ gtk_window_move(GTK_WINDOW(mShell), x, y);
+ }
+}
+
+void nsWindow::WaylandPopupMarkAsClosed() {
+ LOG("nsWindow::WaylandPopupMarkAsClosed: [%p]\n", this);
+ mPopupClosed = true;
+ // If we have any child popup window notify it about
+ // parent switch.
+ if (mWaylandPopupNext) {
+ mWaylandPopupNext->mPopupChanged = true;
+ }
+}
+
+nsWindow* nsWindow::WaylandPopupFindLast(nsWindow* aPopup) {
+ while (aPopup && aPopup->mWaylandPopupNext) {
+ aPopup = aPopup->mWaylandPopupNext;
+ }
+ return aPopup;
+}
+
+// Hide and potentially removes popup from popup hierarchy.
+void nsWindow::HideWaylandPopupWindow(bool aTemporaryHide,
+ bool aRemoveFromPopupList) {
+ LOG("nsWindow::HideWaylandPopupWindow: remove from list %d\n",
+ aRemoveFromPopupList);
+ if (aRemoveFromPopupList) {
+ RemovePopupFromHierarchyList();
+ }
+
+ if (!mPopupClosed) {
+ mPopupClosed = !aTemporaryHide;
+ }
+
+ bool visible = gtk_widget_is_visible(mShell);
+ LOG(" gtk_widget_is_visible() = %d\n", visible);
+
+ // Restore only popups which are really visible
+ mPopupTemporaryHidden = aTemporaryHide && visible;
+
+ // Hide only visible popups or popups closed pernamently.
+ if (visible) {
+ gtk_widget_hide(mShell);
+
+ // If there's pending Move-To-Rect callback and we hide the popup
+ // the callback won't be called any more.
+ mWaitingForMoveToRectCallback = false;
+ }
+
+ if (mPopupClosed) {
+ LOG(" Clearing mMoveToRectPopupSize\n");
+ mMoveToRectPopupSize = {};
+#ifdef MOZ_WAYLAND
+ if (moz_container_wayland_is_waiting_to_show(mContainer)) {
+ // We need to clear rendering queue, see Bug 1782948.
+ LOG(" popup failed to show by Wayland compositor, clear rendering "
+ "queue.");
+ moz_container_wayland_clear_waiting_to_show_flag(mContainer);
+ ClearRenderingQueue();
+ }
+#endif
+ }
+}
+
+void nsWindow::HideWaylandToplevelWindow() {
+ LOG("nsWindow::HideWaylandToplevelWindow: [%p]\n", this);
+ if (mWaylandPopupNext) {
+ nsWindow* popup = WaylandPopupFindLast(mWaylandPopupNext);
+ while (popup->mWaylandToplevel != nullptr) {
+ nsWindow* prev = popup->mWaylandPopupPrev;
+ popup->HideWaylandPopupWindow(/* aTemporaryHide */ false,
+ /* aRemoveFromPopupList */ true);
+ popup = prev;
+ }
+ }
+ WaylandStopVsync();
+ gtk_widget_hide(mShell);
+}
+
+void nsWindow::ShowWaylandToplevelWindow() {
+ MOZ_ASSERT(!IsWaylandPopup());
+ LOG("nsWindow::ShowWaylandToplevelWindow");
+ gtk_widget_show(mShell);
+}
+
+void nsWindow::WaylandPopupRemoveClosedPopups() {
+ LOG("nsWindow::WaylandPopupRemoveClosedPopups()");
+ nsWindow* popup = this;
+ while (popup) {
+ nsWindow* next = popup->mWaylandPopupNext;
+ if (popup->mPopupClosed) {
+ popup->HideWaylandPopupWindow(/* aTemporaryHide */ false,
+ /* aRemoveFromPopupList */ true);
+ }
+ popup = next;
+ }
+}
+
+// Hide all tooltips except the latest one.
+void nsWindow::WaylandPopupHideTooltips() {
+ LOG("nsWindow::WaylandPopupHideTooltips");
+ MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");
+
+ nsWindow* popup = mWaylandPopupNext;
+ while (popup && popup->mWaylandPopupNext) {
+ if (popup->mPopupType == PopupType::Tooltip) {
+ LOG(" hidding tooltip [%p]", popup);
+ popup->WaylandPopupMarkAsClosed();
+ }
+ popup = popup->mWaylandPopupNext;
+ }
+}
+
+void nsWindow::WaylandPopupCloseOrphanedPopups() {
+#ifdef MOZ_WAYLAND
+ LOG("nsWindow::WaylandPopupCloseOrphanedPopups");
+ MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");
+
+ nsWindow* popup = mWaylandPopupNext;
+ bool dangling = false;
+ while (popup) {
+ if (!dangling &&
+ moz_container_wayland_is_waiting_to_show(popup->GetMozContainer())) {
+ LOG(" popup [%p] is waiting to show, close all child popups", popup);
+ dangling = true;
+ } else if (dangling) {
+ popup->WaylandPopupMarkAsClosed();
+ }
+ popup = popup->mWaylandPopupNext;
+ }
+#endif
+}
+
+// We can't show popups with remote content or overflow popups
+// on top of regular ones.
+// If there's any remote popup opened, close all parent popups of it.
+void nsWindow::CloseAllPopupsBeforeRemotePopup() {
+ LOG("nsWindow::CloseAllPopupsBeforeRemotePopup");
+ MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");
+
+ // Don't waste time when there's only one popup opened.
+ if (!mWaylandPopupNext || mWaylandPopupNext->mWaylandPopupNext == nullptr) {
+ return;
+ }
+
+ // Find the first opened remote content popup
+ nsWindow* remotePopup = mWaylandPopupNext;
+ while (remotePopup) {
+ if (remotePopup->HasRemoteContent() ||
+ remotePopup->IsWidgetOverflowWindow()) {
+ LOG(" remote popup [%p]", remotePopup);
+ break;
+ }
+ remotePopup = remotePopup->mWaylandPopupNext;
+ }
+
+ if (!remotePopup) {
+ return;
+ }
+
+ // ...hide opened popups before the remote one.
+ nsWindow* popup = mWaylandPopupNext;
+ while (popup && popup != remotePopup) {
+ LOG(" hidding popup [%p]", popup);
+ popup->WaylandPopupMarkAsClosed();
+ popup = popup->mWaylandPopupNext;
+ }
+}
+
+static void GetLayoutPopupWidgetChain(
+ nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ pm->GetSubmenuWidgetChain(aLayoutWidgetHierarchy);
+ aLayoutWidgetHierarchy->Reverse();
+}
+
+// Compare 'this' popup position in Wayland widget hierarchy
+// (mWaylandPopupPrev/mWaylandPopupNext) with
+// 'this' popup position in layout hierarchy.
+//
+// When aMustMatchParent is true we also request
+// 'this' parents match, i.e. 'this' has the same parent in
+// both layout and widget hierarchy.
+bool nsWindow::IsPopupInLayoutPopupChain(
+ nsTArray<nsIWidget*>* aLayoutWidgetHierarchy, bool aMustMatchParent) {
+ int len = (int)aLayoutWidgetHierarchy->Length();
+ for (int i = 0; i < len; i++) {
+ if (this == (*aLayoutWidgetHierarchy)[i]) {
+ if (!aMustMatchParent) {
+ return true;
+ }
+
+ // Find correct parent popup for 'this' according to widget
+ // hierarchy. That means we need to skip closed popups.
+ nsWindow* parentPopup = nullptr;
+ if (mWaylandPopupPrev != mWaylandToplevel) {
+ parentPopup = mWaylandPopupPrev;
+ while (parentPopup != mWaylandToplevel && parentPopup->mPopupClosed) {
+ parentPopup = parentPopup->mWaylandPopupPrev;
+ }
+ }
+
+ if (i == 0) {
+ // We found 'this' popups as a first popup in layout hierarchy.
+ // It matches layout hierarchy if it's first widget also in
+ // wayland widget hierarchy (i.e. parent is null).
+ return parentPopup == nullptr;
+ }
+
+ return parentPopup == (*aLayoutWidgetHierarchy)[i - 1];
+ }
+ }
+ return false;
+}
+
+// Hide popups which are not in popup chain.
+void nsWindow::WaylandPopupHierarchyHideByLayout(
+ nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) {
+ LOG("nsWindow::WaylandPopupHierarchyHideByLayout");
+ MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");
+
+ // Hide all popups which are not in layout popup chain
+ nsWindow* popup = mWaylandPopupNext;
+ while (popup) {
+ // Don't check closed popups and drag source popups and tooltips.
+ if (!popup->mPopupClosed && popup->mPopupType != PopupType::Tooltip &&
+ !popup->mSourceDragContext) {
+ if (!popup->IsPopupInLayoutPopupChain(aLayoutWidgetHierarchy,
+ /* aMustMatchParent */ false)) {
+ LOG(" hidding popup [%p]", popup);
+ popup->WaylandPopupMarkAsClosed();
+ }
+ }
+ popup = popup->mWaylandPopupNext;
+ }
+}
+
+// Mark popups outside of layout hierarchy
+void nsWindow::WaylandPopupHierarchyValidateByLayout(
+ nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) {
+ LOG("nsWindow::WaylandPopupHierarchyValidateByLayout");
+ nsWindow* popup = mWaylandPopupNext;
+ while (popup) {
+ if (popup->mPopupType == PopupType::Tooltip) {
+ popup->mPopupMatchesLayout = true;
+ } else if (!popup->mPopupClosed) {
+ popup->mPopupMatchesLayout = popup->IsPopupInLayoutPopupChain(
+ aLayoutWidgetHierarchy, /* aMustMatchParent */ true);
+ LOG(" popup [%p] parent window [%p] matches layout %d\n", (void*)popup,
+ (void*)popup->mWaylandPopupPrev, popup->mPopupMatchesLayout);
+ }
+ popup = popup->mWaylandPopupNext;
+ }
+}
+
+void nsWindow::WaylandPopupHierarchyHideTemporary() {
+ LOG("nsWindow::WaylandPopupHierarchyHideTemporary()");
+ nsWindow* popup = WaylandPopupFindLast(this);
+ while (popup && popup != this) {
+ LOG(" temporary hidding popup [%p]", popup);
+ nsWindow* prev = popup->mWaylandPopupPrev;
+ popup->HideWaylandPopupWindow(/* aTemporaryHide */ true,
+ /* aRemoveFromPopupList */ false);
+ popup = prev;
+ }
+}
+
+void nsWindow::WaylandPopupHierarchyShowTemporaryHidden() {
+ LOG("nsWindow::WaylandPopupHierarchyShowTemporaryHidden()");
+ nsWindow* popup = this;
+ while (popup) {
+ if (popup->mPopupTemporaryHidden) {
+ popup->mPopupTemporaryHidden = false;
+ LOG(" showing temporary hidden popup [%p]", popup);
+ popup->ShowWaylandPopupWindow();
+ }
+ popup = popup->mWaylandPopupNext;
+ }
+}
+
+void nsWindow::WaylandPopupHierarchyCalculatePositions() {
+ LOG("nsWindow::WaylandPopupHierarchyCalculatePositions()");
+
+ // Set widget hierarchy in Gtk
+ nsWindow* popup = mWaylandToplevel->mWaylandPopupNext;
+ while (popup) {
+ LOG(" popup [%p] set parent window [%p]", (void*)popup,
+ (void*)popup->mWaylandPopupPrev);
+ GtkWindowSetTransientFor(GTK_WINDOW(popup->mShell),
+ GTK_WINDOW(popup->mWaylandPopupPrev->mShell));
+ popup = popup->mWaylandPopupNext;
+ }
+
+ popup = this;
+ while (popup) {
+ // Anchored window has mPopupPosition already calculated against
+ // its parent, no need to recalculate.
+ LOG(" popup [%p] bounds [%d, %d] -> [%d x %d]", popup,
+ (int)(popup->mBounds.x / FractionalScaleFactor()),
+ (int)(popup->mBounds.y / FractionalScaleFactor()),
+ (int)(popup->mBounds.width / FractionalScaleFactor()),
+ (int)(popup->mBounds.height / FractionalScaleFactor()));
+#ifdef MOZ_LOGGING
+ if (LOG_ENABLED()) {
+ if (nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame())) {
+ auto r = LayoutDeviceRect::FromAppUnitsRounded(
+ popupFrame->GetRect(),
+ popupFrame->PresContext()->AppUnitsPerDevPixel());
+ LOG(" popup [%p] layout [%d, %d] -> [%d x %d]", popup, r.x, r.y,
+ r.width, r.height);
+ }
+ }
+#endif
+ if (popup->WaylandPopupIsFirst()) {
+ LOG(" popup [%p] has toplevel as parent", popup);
+ popup->mRelativePopupPosition = popup->mPopupPosition;
+ } else {
+ if (popup->mPopupAnchored) {
+ LOG(" popup [%p] is anchored", popup);
+ if (!popup->mPopupMatchesLayout) {
+ NS_WARNING("Anchored popup does not match layout!");
+ }
+ }
+ GdkPoint parent = popup->WaylandGetParentPosition();
+
+ LOG(" popup [%p] uses transformed coordinates\n", popup);
+ LOG(" parent position [%d, %d]\n", parent.x, parent.y);
+ LOG(" popup position [%d, %d]\n", popup->mPopupPosition.x,
+ popup->mPopupPosition.y);
+
+ popup->mRelativePopupPosition.x = popup->mPopupPosition.x - parent.x;
+ popup->mRelativePopupPosition.y = popup->mPopupPosition.y - parent.y;
+ }
+ LOG(" popup [%p] transformed popup coordinates from [%d, %d] to [%d, %d]",
+ popup, popup->mPopupPosition.x, popup->mPopupPosition.y,
+ popup->mRelativePopupPosition.x, popup->mRelativePopupPosition.y);
+ popup = popup->mWaylandPopupNext;
+ }
+}
+
+// The MenuList popups are used as dropdown menus for example in WebRTC
+// microphone/camera chooser or autocomplete widgets.
+bool nsWindow::WaylandPopupIsMenu() {
+ nsMenuPopupFrame* menuPopupFrame = GetMenuPopupFrame(GetFrame());
+ if (menuPopupFrame) {
+ return mPopupType == PopupType::Menu && !menuPopupFrame->IsMenuList();
+ }
+ return false;
+}
+
+bool nsWindow::WaylandPopupIsContextMenu() {
+ nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
+ if (!popupFrame) {
+ return false;
+ }
+ return popupFrame->IsContextMenu();
+}
+
+bool nsWindow::WaylandPopupIsPermanent() {
+ nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
+ if (!popupFrame) {
+ // We can always hide popups without frames.
+ return false;
+ }
+ return popupFrame->IsNoAutoHide();
+}
+
+bool nsWindow::WaylandPopupIsAnchored() {
+ nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
+ if (!popupFrame) {
+ // We can always hide popups without frames.
+ return false;
+ }
+ return !!popupFrame->GetAnchor();
+}
+
+bool nsWindow::IsWidgetOverflowWindow() {
+ if (this->GetFrame() && this->GetFrame()->GetContent()->GetID()) {
+ nsCString nodeId;
+ this->GetFrame()->GetContent()->GetID()->ToUTF8String(nodeId);
+ return nodeId.Equals("widget-overflow");
+ }
+ return false;
+}
+
+bool nsWindow::WaylandPopupIsFirst() {
+ return !mWaylandPopupPrev || !mWaylandPopupPrev->mWaylandToplevel;
+}
+
+nsWindow* nsWindow::GetEffectiveParent() {
+ GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell));
+ if (!parentGtkWindow || !GTK_IS_WIDGET(parentGtkWindow)) {
+ return nullptr;
+ }
+ return get_window_for_gtk_widget(GTK_WIDGET(parentGtkWindow));
+}
+
+GdkPoint nsWindow::WaylandGetParentPosition() {
+ GdkPoint topLeft = {0, 0};
+ nsWindow* window = GetEffectiveParent();
+ if (window->IsPopup()) {
+ topLeft = DevicePixelsToGdkPointRoundDown(window->mBounds.TopLeft());
+ }
+ LOG("nsWindow::WaylandGetParentPosition() [%d, %d]\n", topLeft.x, topLeft.y);
+ return topLeft;
+}
+
+#ifdef MOZ_LOGGING
+void nsWindow::LogPopupHierarchy() {
+ if (!LOG_ENABLED()) {
+ return;
+ }
+
+ LOG("Widget Popup Hierarchy:\n");
+ if (!mWaylandToplevel->mWaylandPopupNext) {
+ LOG(" Empty\n");
+ } else {
+ int indent = 4;
+ nsWindow* popup = mWaylandToplevel->mWaylandPopupNext;
+ while (popup) {
+ nsPrintfCString indentString("%*s", indent, " ");
+ LOG("%s %s %s nsWindow [%p] Menu %d Permanent %d ContextMenu %d "
+ "Anchored %d Visible %d MovedByRect %d\n",
+ indentString.get(), popup->GetFrameTag().get(),
+ popup->GetPopupTypeName().get(), popup, popup->WaylandPopupIsMenu(),
+ popup->WaylandPopupIsPermanent(), popup->mPopupContextMenu,
+ popup->mPopupAnchored, gtk_widget_is_visible(popup->mShell),
+ popup->mPopupUseMoveToRect);
+ indent += 4;
+ popup = popup->mWaylandPopupNext;
+ }
+ }
+
+ LOG("Layout Popup Hierarchy:\n");
+ AutoTArray<nsIWidget*, 5> widgetChain;
+ GetLayoutPopupWidgetChain(&widgetChain);
+ if (widgetChain.Length() == 0) {
+ LOG(" Empty\n");
+ } else {
+ for (unsigned long i = 0; i < widgetChain.Length(); i++) {
+ nsWindow* window = static_cast<nsWindow*>(widgetChain[i]);
+ nsPrintfCString indentString("%*s", (int)(i + 1) * 4, " ");
+ if (window) {
+ LOG("%s %s %s nsWindow [%p] Menu %d Permanent %d ContextMenu %d "
+ "Anchored %d Visible %d MovedByRect %d\n",
+ indentString.get(), window->GetFrameTag().get(),
+ window->GetPopupTypeName().get(), window,
+ window->WaylandPopupIsMenu(), window->WaylandPopupIsPermanent(),
+ window->mPopupContextMenu, window->mPopupAnchored,
+ gtk_widget_is_visible(window->mShell), window->mPopupUseMoveToRect);
+ } else {
+ LOG("%s null window\n", indentString.get());
+ }
+ }
+ }
+}
+#endif
+
+nsWindow* nsWindow::GetTopmostWindow() {
+ if (nsView* view = nsView::GetViewFor(this)) {
+ if (nsView* parentView = view->GetParent()) {
+ if (nsIWidget* parentWidget = parentView->GetNearestWidget(nullptr)) {
+ return static_cast<nsWindow*>(parentWidget);
+ }
+ }
+ }
+ return nullptr;
+}
+
+// Configure Wayland popup. If true is returned we need to track popup
+// in popup hierarchy. Otherwise we just show it as is.
+bool nsWindow::WaylandPopupConfigure() {
+ if (mIsDragPopup) {
+ return false;
+ }
+
+ // Don't track popups without frame
+ nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
+ if (!popupFrame) {
+ return false;
+ }
+
+ // Popup state can be changed, see Bug 1728952.
+ bool permanentStateMatches =
+ mPopupTrackInHierarchy == !WaylandPopupIsPermanent();
+
+ // Popup permanent state (noautohide attribute) can change during popup life.
+ if (mPopupTrackInHierarchyConfigured && permanentStateMatches) {
+ return mPopupTrackInHierarchy;
+ }
+
+ // Configure persistent popup params only once.
+ // WaylandPopupIsAnchored() can give it wrong value after
+ // nsMenuPopupFrame::MoveTo() call which we use in move-to-rect callback
+ // to position popup after wayland position change.
+ if (!mPopupTrackInHierarchyConfigured) {
+ mPopupAnchored = WaylandPopupIsAnchored();
+ mPopupContextMenu = WaylandPopupIsContextMenu();
+ }
+
+ LOG("nsWindow::WaylandPopupConfigure tracked %d anchored %d hint %d\n",
+ mPopupTrackInHierarchy, mPopupAnchored, int(mPopupType));
+
+ // Permanent state changed and popup is mapped.
+ // We need to switch popup type but that's done when popup is mapped
+ // by Gtk so we need to unmap the popup here.
+ // It will be mapped again by gtk_widget_show().
+ if (!permanentStateMatches && mIsMapped) {
+ LOG(" permanent state change from %d to %d, unmapping",
+ mPopupTrackInHierarchy, !WaylandPopupIsPermanent());
+ gtk_widget_unmap(mShell);
+ }
+
+ mPopupTrackInHierarchy = !WaylandPopupIsPermanent();
+ LOG(" tracked in hierarchy %d\n", mPopupTrackInHierarchy);
+
+ // See gdkwindow-wayland.c and
+ // should_map_as_popup()/should_map_as_subsurface()
+ GdkWindowTypeHint gtkTypeHint;
+ switch (mPopupType) {
+ case PopupType::Menu:
+ // GDK_WINDOW_TYPE_HINT_POPUP_MENU is mapped as xdg_popup by default.
+ // We use this type for all menu popups.
+ gtkTypeHint = GDK_WINDOW_TYPE_HINT_POPUP_MENU;
+ LOG(" popup type Menu");
+ break;
+ case PopupType::Tooltip:
+ gtkTypeHint = GDK_WINDOW_TYPE_HINT_TOOLTIP;
+ LOG(" popup type Tooltip");
+ break;
+ default:
+ gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY;
+ LOG(" popup type Utility");
+ break;
+ }
+
+ if (!mPopupTrackInHierarchy) {
+ // GDK_WINDOW_TYPE_HINT_UTILITY is mapped as wl_subsurface
+ // by default.
+ LOG(" not tracked in popup hierarchy, switch to Utility");
+ gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY;
+ }
+ gtk_window_set_type_hint(GTK_WINDOW(mShell), gtkTypeHint);
+
+ mPopupTrackInHierarchyConfigured = true;
+ return mPopupTrackInHierarchy;
+}
+
+bool nsWindow::IsInPopupHierarchy() {
+ return mPopupTrackInHierarchy && mWaylandToplevel && mWaylandPopupPrev;
+}
+
+void nsWindow::AddWindowToPopupHierarchy() {
+ LOG("nsWindow::AddWindowToPopupHierarchy\n");
+ if (!GetFrame()) {
+ LOG(" Window without frame cannot be added as popup!\n");
+ return;
+ }
+
+ // Check if we're already in the hierarchy
+ if (!IsInPopupHierarchy()) {
+ mWaylandToplevel = GetTopmostWindow();
+ AppendPopupToHierarchyList(mWaylandToplevel);
+ }
+}
+
+// Wayland keeps strong popup window hierarchy. We need to track active
+// (visible) popup windows and make sure we hide popup on the same level
+// before we open another one on that level. It means that every open
+// popup needs to have an unique parent.
+void nsWindow::UpdateWaylandPopupHierarchy() {
+ LOG("nsWindow::UpdateWaylandPopupHierarchy\n");
+
+ // This popup hasn't been added to popup hierarchy yet so no need to
+ // do any configurations.
+ if (!IsInPopupHierarchy()) {
+ LOG(" popup isn't in hierarchy\n");
+ return;
+ }
+
+#ifdef MOZ_LOGGING
+ LogPopupHierarchy();
+ auto printPopupHierarchy = MakeScopeExit([&] { LogPopupHierarchy(); });
+#endif
+
+ // Hide all tooltips without the last one. Tooltip can't be popup parent.
+ mWaylandToplevel->WaylandPopupHideTooltips();
+
+ // See Bug 1709254 / https://gitlab.gnome.org/GNOME/gtk/-/issues/5092
+ // It's possible that Wayland compositor refuses to show
+ // a popup although Gtk claims it's visible.
+ // We don't know if the popup is shown or not.
+ // To avoid application crash refuse to create any child of such invisible
+ // popup and close any child of it now.
+ mWaylandToplevel->WaylandPopupCloseOrphanedPopups();
+
+ // Check if we have any remote content / overflow window in hierarchy.
+ // We can't attach such widget on top of other popup.
+ mWaylandToplevel->CloseAllPopupsBeforeRemotePopup();
+
+ // Check if your popup hierarchy matches layout hierarchy.
+ // For instance we should not connect hamburger menu on top
+ // of context menu.
+ // Close all popups from different layout chains if possible.
+ AutoTArray<nsIWidget*, 5> layoutPopupWidgetChain;
+ GetLayoutPopupWidgetChain(&layoutPopupWidgetChain);
+
+ mWaylandToplevel->WaylandPopupHierarchyHideByLayout(&layoutPopupWidgetChain);
+ mWaylandToplevel->WaylandPopupHierarchyValidateByLayout(
+ &layoutPopupWidgetChain);
+
+ // Now we have Popup hierarchy complete.
+ // Find first unchanged (and still open) popup to start with hierarchy
+ // changes.
+ nsWindow* changedPopup = mWaylandToplevel->mWaylandPopupNext;
+ while (changedPopup) {
+ // Stop when parent of this popup was changed and we need to recalc
+ // popup position.
+ if (changedPopup->mPopupChanged) {
+ break;
+ }
+ // Stop when this popup is closed.
+ if (changedPopup->mPopupClosed) {
+ break;
+ }
+ changedPopup = changedPopup->mWaylandPopupNext;
+ }
+
+ // We don't need to recompute popup positions, quit now.
+ if (!changedPopup) {
+ LOG(" changed Popup is null, quit.\n");
+ return;
+ }
+
+ LOG(" first changed popup [%p]\n", (void*)changedPopup);
+
+ // Hide parent popups if necessary (there are layout discontinuity)
+ // reposition the popup and show them again.
+ changedPopup->WaylandPopupHierarchyHideTemporary();
+
+ nsWindow* parentOfchangedPopup = nullptr;
+ if (changedPopup->mPopupClosed) {
+ parentOfchangedPopup = changedPopup->mWaylandPopupPrev;
+ }
+ changedPopup->WaylandPopupRemoveClosedPopups();
+
+ // It's possible that changedPopup was removed from widget hierarchy,
+ // in such case use child popup of the removed one if there's any.
+ if (!changedPopup->IsInPopupHierarchy()) {
+ if (!parentOfchangedPopup || !parentOfchangedPopup->mWaylandPopupNext) {
+ LOG(" last popup was removed, quit.\n");
+ return;
+ }
+ changedPopup = parentOfchangedPopup->mWaylandPopupNext;
+ }
+
+ GetLayoutPopupWidgetChain(&layoutPopupWidgetChain);
+ mWaylandToplevel->WaylandPopupHierarchyValidateByLayout(
+ &layoutPopupWidgetChain);
+
+ changedPopup->WaylandPopupHierarchyCalculatePositions();
+
+ nsWindow* popup = changedPopup;
+ while (popup) {
+ const bool useMoveToRect = [&] {
+ if (!StaticPrefs::widget_wayland_use_move_to_rect_AtStartup()) {
+ return false; // Not available.
+ }
+ if (!popup->mPopupMatchesLayout) {
+ // We can use move_to_rect only when popups in popup hierarchy matches
+ // layout hierarchy as move_to_rect request that parent/child
+ // popups are adjacent.
+ return false;
+ }
+ if (popup->mPopupType == PopupType::Panel &&
+ popup->WaylandPopupIsFirst() &&
+ popup->WaylandPopupFitsToplevelWindow(/* aMove */ true)) {
+ // Workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/1986
+ //
+ // PopupType::Panel types are used for extension popups which may be
+ // resized. If such popup uses move-to-rect, we need to hide it before
+ // resize and show it again. That leads to massive flickering
+ // so use plain move if possible to avoid it.
+ //
+ // Bug 1760276 - don't use move-to-rect when popup is inside main
+ // Firefox window.
+ //
+ // Use it for first popups only due to another mutter bug
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/5089
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1784873
+ return false;
+ }
+ if (!popup->WaylandPopupIsFirst() &&
+ !popup->mWaylandPopupPrev->WaylandPopupIsFirst() &&
+ !popup->mWaylandPopupPrev->mPopupUseMoveToRect) {
+ // We can't use move-to-rect if there are more parents of
+ // wl_subsurface popups types.
+ //
+ // It's because wl_subsurface is ignored by xgd_popup
+ // (created by move-to-rect) so our popup scenario:
+ //
+ // toplevel -> xgd_popup(1) -> wl_subsurface(2) -> xgd_popup(3)
+ //
+ // looks for Wayland compositor as:
+ //
+ // toplevel -> xgd_popup(1) -> xgd_popup(3)
+ //
+ // If xgd_popup(1) and xgd_popup(3) are not connected
+ // move-to-rect applied to xgd_popup(3) fails and we get missing popup.
+ return false;
+ }
+ return true;
+ }();
+
+ LOG(" popup [%p] matches layout [%d] anchored [%d] first popup [%d] use "
+ "move-to-rect %d\n",
+ popup, popup->mPopupMatchesLayout, popup->mPopupAnchored,
+ popup->WaylandPopupIsFirst(), useMoveToRect);
+
+ popup->mPopupUseMoveToRect = useMoveToRect;
+ popup->WaylandPopupMoveImpl();
+ popup->mPopupChanged = false;
+ popup = popup->mWaylandPopupNext;
+ }
+
+ changedPopup->WaylandPopupHierarchyShowTemporaryHidden();
+}
+
+static void NativeMoveResizeCallback(GdkWindow* window,
+ const GdkRectangle* flipped_rect,
+ const GdkRectangle* final_rect,
+ gboolean flipped_x, gboolean flipped_y,
+ void* aWindow) {
+ LOG_POPUP("[%p] NativeMoveResizeCallback flipped_x %d flipped_y %d\n",
+ aWindow, flipped_x, flipped_y);
+ LOG_POPUP("[%p] new position [%d, %d] -> [%d x %d]", aWindow,
+ final_rect->x, final_rect->y, final_rect->width,
+ final_rect->height);
+ nsWindow* wnd = get_window_for_gdk_window(window);
+
+ wnd->NativeMoveResizeWaylandPopupCallback(final_rect, flipped_x, flipped_y);
+}
+
+// When popup is repositioned by widget code, we need to notify
+// layout about it. It's because we control popup placement
+// on widget on Wayland so layout may have old popup size/coordinates.
+void nsWindow::WaylandPopupPropagateChangesToLayout(bool aMove, bool aResize) {
+ LOG("nsWindow::WaylandPopupPropagateChangesToLayout()");
+
+ if (aResize) {
+ LOG(" needSizeUpdate\n");
+ if (nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame())) {
+ RefPtr<PresShell> presShell = popupFrame->PresShell();
+ presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::None,
+ NS_FRAME_IS_DIRTY);
+ }
+ }
+ if (aMove) {
+ LOG(" needPositionUpdate, bounds [%d, %d]", mBounds.x, mBounds.y);
+ NotifyWindowMoved(mBounds.x, mBounds.y, ByMoveToRect::Yes);
+ }
+}
+
+void nsWindow::NativeMoveResizeWaylandPopupCallback(
+ const GdkRectangle* aFinalSize, bool aFlippedX, bool aFlippedY) {
+ // We're getting move-to-rect callback without move-to-rect call.
+ // That indicates a compositor bug. It happens when a window is hidden and
+ // shown again before move-to-rect callback is fired.
+ // It may lead to incorrect popup placement as we may call
+ // gtk_window_move() between hide & show.
+ // See Bug 1777919, 1789581.
+#if MOZ_LOGGING
+ if (!mWaitingForMoveToRectCallback) {
+ LOG(" Bogus move-to-rect callback! Expect wrong popup coordinates.");
+ }
+#endif
+
+ mWaitingForMoveToRectCallback = false;
+
+ bool movedByLayout = mMovedAfterMoveToRect;
+ bool resizedByLayout = mResizedAfterMoveToRect;
+
+ // Popup was moved between move-to-rect call and move-to-rect callback
+ // and the coordinates from move-to-rect callback are outdated.
+ if (movedByLayout || resizedByLayout) {
+ LOG(" Another move/resize called during waiting for callback\n");
+ mMovedAfterMoveToRect = false;
+ mResizedAfterMoveToRect = false;
+ // Fire another round of move/resize to reflect latest request
+ // from layout.
+ NativeMoveResize(movedByLayout, resizedByLayout);
+ return;
+ }
+
+ LOG(" orig mBounds [%d, %d] -> [%d x %d]\n", mBounds.x, mBounds.y,
+ mBounds.width, mBounds.height);
+
+ LayoutDeviceIntRect newBounds = [&] {
+ GdkRectangle finalRect = *aFinalSize;
+ GdkPoint parent = WaylandGetParentPosition();
+ finalRect.x += parent.x;
+ finalRect.y += parent.y;
+ return GdkRectToDevicePixels(finalRect);
+ }();
+
+ LOG(" new mBounds [%d, %d] -> [%d x %d]", newBounds.x, newBounds.y,
+ newBounds.width, newBounds.height);
+
+ bool needsPositionUpdate = newBounds.TopLeft() != mBounds.TopLeft();
+ bool needsSizeUpdate = newBounds.Size() != mLastSizeRequest;
+
+ if (needsSizeUpdate) {
+ // Wayland compositor changed popup size request from layout.
+ // Set the constraints to use them in nsMenuPopupFrame::SetPopupPosition().
+ // Beware that gtk_window_resize() requests sizes asynchronously and so
+ // newBounds might not have the size from the most recent
+ // gtk_window_resize().
+ if (newBounds.width < mLastSizeRequest.width) {
+ mMoveToRectPopupSize.width = newBounds.width;
+ }
+ if (newBounds.height < mLastSizeRequest.height) {
+ mMoveToRectPopupSize.height = newBounds.height;
+ }
+ LOG(" mMoveToRectPopupSize set to [%d, %d]", mMoveToRectPopupSize.width,
+ mMoveToRectPopupSize.height);
+ }
+ mBounds = newBounds;
+ // Check mBounds size
+ if (mCompositorSession &&
+ !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) {
+ gfxCriticalNoteOnce << "Invalid mBounds in PopupCallback " << mBounds
+ << " size state " << mSizeMode;
+ }
+ WaylandPopupPropagateChangesToLayout(needsPositionUpdate, needsSizeUpdate);
+}
+
+static GdkGravity PopupAlignmentToGdkGravity(int8_t aAlignment) {
+ switch (aAlignment) {
+ case POPUPALIGNMENT_NONE:
+ return GDK_GRAVITY_NORTH_WEST;
+ case POPUPALIGNMENT_TOPLEFT:
+ return GDK_GRAVITY_NORTH_WEST;
+ case POPUPALIGNMENT_TOPRIGHT:
+ return GDK_GRAVITY_NORTH_EAST;
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ return GDK_GRAVITY_SOUTH_WEST;
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ return GDK_GRAVITY_SOUTH_EAST;
+ case POPUPALIGNMENT_LEFTCENTER:
+ return GDK_GRAVITY_WEST;
+ case POPUPALIGNMENT_RIGHTCENTER:
+ return GDK_GRAVITY_EAST;
+ case POPUPALIGNMENT_TOPCENTER:
+ return GDK_GRAVITY_NORTH;
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ return GDK_GRAVITY_SOUTH;
+ }
+ return GDK_GRAVITY_STATIC;
+}
+
+bool nsWindow::IsPopupDirectionRTL() {
+ nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
+ return popupFrame && popupFrame->IsDirectionRTL();
+}
+
+// Position the popup directly by gtk_window_move() and try to keep it
+// on screen by just moving it in scope of it's parent window.
+//
+// It's used when we position noautihode popup and we don't use xdg_positioner.
+// See Bug 1718867
+void nsWindow::WaylandPopupSetDirectPosition() {
+ GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft());
+ GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);
+
+ LOG("nsWindow::WaylandPopupSetDirectPosition %d,%d -> %d x %d\n", topLeft.x,
+ topLeft.y, size.width, size.height);
+
+ mPopupPosition = {topLeft.x, topLeft.y};
+
+ if (mIsDragPopup) {
+ gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y);
+ gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
+ // DND window is placed inside container so we need to make hard size
+ // request to ensure parent container is resized too.
+ gtk_widget_set_size_request(GTK_WIDGET(mShell), size.width, size.height);
+ return;
+ }
+
+ GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell));
+ nsWindow* window = get_window_for_gtk_widget(GTK_WIDGET(parentGtkWindow));
+ if (!window) {
+ return;
+ }
+ GdkWindow* gdkWindow = window->GetGdkWindow();
+ if (!gdkWindow) {
+ return;
+ }
+
+ int parentWidth = gdk_window_get_width(gdkWindow);
+ int popupWidth = size.width;
+
+ int x;
+ gdk_window_get_position(gdkWindow, &x, nullptr);
+
+ // If popup is bigger than main window just center it.
+ if (popupWidth > parentWidth) {
+ mPopupPosition.x = -(parentWidth - popupWidth) / 2 + x;
+ } else {
+ if (IsPopupDirectionRTL()) {
+ // Stick with right window edge
+ if (mPopupPosition.x < x) {
+ mPopupPosition.x = x;
+ }
+ } else {
+ // Stick with left window edge
+ if (mPopupPosition.x + popupWidth > parentWidth + x) {
+ mPopupPosition.x = parentWidth + x - popupWidth;
+ }
+ }
+ }
+
+ LOG(" set position [%d, %d]\n", mPopupPosition.x, mPopupPosition.y);
+ gtk_window_move(GTK_WINDOW(mShell), mPopupPosition.x, mPopupPosition.y);
+
+ LOG(" set size [%d, %d]\n", size.width, size.height);
+ gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
+
+ if (mPopupPosition.x != topLeft.x) {
+ mBounds.MoveTo(GdkPointToDevicePixels(mPopupPosition));
+ LOG(" setting new bounds [%d, %d]\n", mBounds.x, mBounds.y);
+ WaylandPopupPropagateChangesToLayout(/* move */ true, /* resize */ false);
+ }
+}
+
+bool nsWindow::WaylandPopupFitsToplevelWindow(bool aMove) {
+ LOG("nsWindow::WaylandPopupFitsToplevelWindow() move %d", aMove);
+
+ GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(mShell));
+ GtkWindow* tmp = parent;
+ while ((tmp = gtk_window_get_transient_for(GTK_WINDOW(parent)))) {
+ parent = tmp;
+ }
+ GdkWindow* toplevelGdkWindow = gtk_widget_get_window(GTK_WIDGET(parent));
+ if (!toplevelGdkWindow) {
+ NS_WARNING("Toplevel widget without GdkWindow?");
+ return false;
+ }
+
+ int parentWidth = gdk_window_get_width(toplevelGdkWindow);
+ int parentHeight = gdk_window_get_height(toplevelGdkWindow);
+ LOG(" parent size %d x %d", parentWidth, parentHeight);
+
+ GdkPoint topLeft = aMove ? mPopupPosition
+ : DevicePixelsToGdkPointRoundDown(mBounds.TopLeft());
+ GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);
+ LOG(" popup topleft %d, %d size %d x %d", topLeft.x, topLeft.y, size.width,
+ size.height);
+ int fits = topLeft.x >= 0 && topLeft.y >= 0 &&
+ topLeft.x + size.width <= parentWidth &&
+ topLeft.y + size.height <= parentHeight;
+
+ LOG(" fits %d", fits);
+ return fits;
+}
+
+void nsWindow::NativeMoveResizeWaylandPopup(bool aMove, bool aResize) {
+ GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft());
+ GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);
+
+ LOG("nsWindow::NativeMoveResizeWaylandPopup Bounds %d,%d -> %d x %d move %d "
+ "resize %d\n",
+ topLeft.x, topLeft.y, size.width, size.height, aMove, aResize);
+
+ // Compositor may be confused by windows with width/height = 0
+ // and positioning such windows leads to Bug 1555866.
+ if (!AreBoundsSane()) {
+ LOG(" Bounds are not sane (width: %d height: %d)\n",
+ mLastSizeRequest.width, mLastSizeRequest.height);
+ return;
+ }
+
+ if (mWaitingForMoveToRectCallback) {
+ LOG(" waiting for move to rect, scheduling");
+ // mBounds position must not be overwritten before it is applied.
+ // OnConfigureEvent() will not set mBounds to an old position for
+ // GTK_WINDOW_POPUP.
+ MOZ_ASSERT(gtk_window_get_window_type(GTK_WINDOW(mShell)) ==
+ GTK_WINDOW_POPUP);
+ mMovedAfterMoveToRect = aMove;
+ mResizedAfterMoveToRect = aResize;
+ return;
+ }
+
+ mMovedAfterMoveToRect = false;
+ mResizedAfterMoveToRect = false;
+
+ bool trackedInHierarchy = WaylandPopupConfigure();
+
+ // Read popup position from layout if it was moved or newly created.
+ // This position is used by move-to-rect method as we need anchor and other
+ // info to place popup correctly.
+ // We need WaylandPopupConfigure() to be called before to have all needed
+ // popup info in place (mainly the anchored flag).
+ if (aMove) {
+ mPopupMoveToRectParams = WaylandPopupGetPositionFromLayout();
+ }
+
+ if (!trackedInHierarchy) {
+ WaylandPopupSetDirectPosition();
+ return;
+ }
+
+ if (aResize) {
+ LOG(" set size [%d, %d]\n", size.width, size.height);
+ gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
+ }
+
+ if (!aMove && WaylandPopupFitsToplevelWindow(aMove)) {
+ // Popup position has not been changed and its position/size fits
+ // parent window so no need to reposition the window.
+ LOG(" fits parent window size, just resize\n");
+ return;
+ }
+
+ // Mark popup as changed as we're updating position/size.
+ mPopupChanged = true;
+
+ // Save popup position for former re-calculations when popup hierarchy
+ // is changed.
+ LOG(" popup position changed from [%d, %d] to [%d, %d]\n", mPopupPosition.x,
+ mPopupPosition.y, topLeft.x, topLeft.y);
+ mPopupPosition = {topLeft.x, topLeft.y};
+
+ UpdateWaylandPopupHierarchy();
+}
+
+struct PopupSides {
+ Maybe<Side> mVertical;
+ Maybe<Side> mHorizontal;
+};
+
+static PopupSides SidesForPopupAlignment(int8_t aAlignment) {
+ switch (aAlignment) {
+ case POPUPALIGNMENT_NONE:
+ break;
+ case POPUPALIGNMENT_TOPLEFT:
+ return {Some(eSideTop), Some(eSideLeft)};
+ case POPUPALIGNMENT_TOPRIGHT:
+ return {Some(eSideTop), Some(eSideRight)};
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ return {Some(eSideBottom), Some(eSideLeft)};
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ return {Some(eSideBottom), Some(eSideRight)};
+ case POPUPALIGNMENT_LEFTCENTER:
+ return {Nothing(), Some(eSideLeft)};
+ case POPUPALIGNMENT_RIGHTCENTER:
+ return {Nothing(), Some(eSideRight)};
+ case POPUPALIGNMENT_TOPCENTER:
+ return {Some(eSideTop), Nothing()};
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ return {Some(eSideBottom), Nothing()};
+ }
+ return {};
+}
+
+// We want to apply margins based on popup alignment (which would generally be
+// just an offset to apply to the popup). However, to deal with flipping
+// correctly, we apply the margin to the anchor when possible.
+struct ResolvedPopupMargin {
+ // A margin to be applied to the anchor.
+ nsMargin mAnchorMargin;
+ // An offset in app units to be applied to the popup for when we need to tell
+ // GTK to center inside the anchor precisely (so we can't really do better in
+ // presence of flips).
+ nsPoint mPopupOffset;
+};
+
+static ResolvedPopupMargin ResolveMargin(nsMenuPopupFrame* aFrame,
+ int8_t aPopupAlign,
+ int8_t aAnchorAlign,
+ bool aAnchoredToPoint,
+ bool aIsContextMenu) {
+ nsMargin margin = aFrame->GetMargin();
+ nsPoint offset;
+
+ if (aAnchoredToPoint) {
+ // Since GTK doesn't allow us to specify margins itself, when anchored to a
+ // point we can just assume we'll be aligned correctly... This is kind of
+ // annoying but alas.
+ //
+ // This calculation must match the relevant unanchored popup calculation in
+ // nsMenuPopupFrame::SetPopupPosition(), which should itself be the inverse
+ // inverse of nsMenuPopupFrame::MoveTo().
+ if (aIsContextMenu && aFrame->IsDirectionRTL()) {
+ offset.x = -margin.right;
+ } else {
+ offset.x = margin.left;
+ }
+ offset.y = margin.top;
+ return {nsMargin(), offset};
+ }
+
+ auto popupSides = SidesForPopupAlignment(aPopupAlign);
+ auto anchorSides = SidesForPopupAlignment(aAnchorAlign);
+ // Matched sides: Invert the margin, so that we pull in the right direction.
+ // Popup not aligned to any anchor side: We give up and use the offset,
+ // applying the margin from the popup side.
+ // Mismatched sides: We swap the margins so that we pull in the right
+ // direction, e.g. margin-left: -10px should shrink 10px the _right_ of the
+ // box, not the left of the box.
+ if (popupSides.mHorizontal == anchorSides.mHorizontal) {
+ margin.left = -margin.left;
+ margin.right = -margin.right;
+ } else if (!anchorSides.mHorizontal) {
+ auto popupSide = *popupSides.mHorizontal;
+ offset.x += popupSide == eSideRight ? -margin.Side(popupSide)
+ : margin.Side(popupSide);
+ margin.left = margin.right = 0;
+ } else {
+ std::swap(margin.left, margin.right);
+ }
+
+ // Same logic as above, but in the vertical direction.
+ if (popupSides.mVertical == anchorSides.mVertical) {
+ margin.top = -margin.top;
+ margin.bottom = -margin.bottom;
+ } else if (!anchorSides.mVertical) {
+ auto popupSide = *popupSides.mVertical;
+ offset.y += popupSide == eSideBottom ? -margin.Side(popupSide)
+ : margin.Side(popupSide);
+ margin.top = margin.bottom = 0;
+ } else {
+ std::swap(margin.top, margin.bottom);
+ }
+
+ return {margin, offset};
+}
+
+#ifdef MOZ_LOGGING
+void nsWindow::LogPopupAnchorHints(int aHints) {
+ static struct hints_ {
+ int hint;
+ char name[100];
+ } hints[] = {
+ {GDK_ANCHOR_FLIP_X, "GDK_ANCHOR_FLIP_X"},
+ {GDK_ANCHOR_FLIP_Y, "GDK_ANCHOR_FLIP_Y"},
+ {GDK_ANCHOR_SLIDE_X, "GDK_ANCHOR_SLIDE_X"},
+ {GDK_ANCHOR_SLIDE_Y, "GDK_ANCHOR_SLIDE_Y"},
+ {GDK_ANCHOR_RESIZE_X, "GDK_ANCHOR_RESIZE_X"},
+ {GDK_ANCHOR_RESIZE_Y, "GDK_ANCHOR_RESIZE_X"},
+ };
+
+ LOG(" PopupAnchorHints");
+ for (const auto& hint : hints) {
+ if (hint.hint & aHints) {
+ LOG(" %s", hint.name);
+ }
+ }
+}
+
+void nsWindow::LogPopupGravity(GdkGravity aGravity) {
+ static char gravity[][100]{"NONE",
+ "GDK_GRAVITY_NORTH_WEST",
+ "GDK_GRAVITY_NORTH",
+ "GDK_GRAVITY_NORTH_EAST",
+ "GDK_GRAVITY_WEST",
+ "GDK_GRAVITY_CENTER",
+ "GDK_GRAVITY_EAST",
+ "GDK_GRAVITY_SOUTH_WEST",
+ "GDK_GRAVITY_SOUTH",
+ "GDK_GRAVITY_SOUTH_EAST",
+ "GDK_GRAVITY_STATIC"};
+ LOG(" %s", gravity[aGravity]);
+}
+#endif
+
+const nsWindow::WaylandPopupMoveToRectParams
+nsWindow::WaylandPopupGetPositionFromLayout() {
+ LOG("nsWindow::WaylandPopupGetPositionFromLayout\n");
+
+ nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
+
+ const bool isTopContextMenu = mPopupContextMenu && !mPopupAnchored;
+ const bool isRTL = IsPopupDirectionRTL();
+ const bool anchored = popupFrame->IsAnchored();
+ int8_t popupAlign = POPUPALIGNMENT_TOPLEFT;
+ int8_t anchorAlign = POPUPALIGNMENT_BOTTOMRIGHT;
+ if (anchored) {
+ // See nsMenuPopupFrame::AdjustPositionForAnchorAlign.
+ popupAlign = popupFrame->GetPopupAlignment();
+ anchorAlign = popupFrame->GetPopupAnchor();
+ }
+ if (isRTL && (anchored || isTopContextMenu)) {
+ popupAlign = -popupAlign;
+ anchorAlign = -anchorAlign;
+ }
+
+ // Although we have mPopupPosition / mRelativePopupPosition here
+ // we can't use it. move-to-rect needs anchor rectangle to position a popup
+ // but we have only a point from Resize().
+ //
+ // So we need to extract popup position from nsMenuPopupFrame() and duplicate
+ // the layout work here.
+ LayoutDeviceIntRect anchorRect;
+ ResolvedPopupMargin popupMargin;
+ {
+ nsRect anchorRectAppUnits = popupFrame->GetUntransformedAnchorRect();
+ // This is a somewhat hacky way of applying the popup margin. We don't know
+ // if GTK will end up flipping the popup, in which case the offset we
+ // compute is just wrong / applied to the wrong side.
+ //
+ // Instead, we tell it to anchor us at a smaller or bigger rect depending on
+ // the margin, which achieves the same result if the popup is positioned
+ // correctly, but doesn't misposition the popup when flipped across the
+ // anchor.
+ popupMargin = ResolveMargin(popupFrame, popupAlign, anchorAlign,
+ anchorRectAppUnits.IsEmpty(), isTopContextMenu);
+ LOG(" layout popup CSS anchor (%d, %d) %s, margin %s offset %s\n",
+ popupAlign, anchorAlign, ToString(anchorRectAppUnits).c_str(),
+ ToString(popupMargin.mAnchorMargin).c_str(),
+ ToString(popupMargin.mPopupOffset).c_str());
+ anchorRectAppUnits.Inflate(popupMargin.mAnchorMargin);
+ LOG(" after margins %s\n", ToString(anchorRectAppUnits).c_str());
+ nscoord auPerDev = popupFrame->PresContext()->AppUnitsPerDevPixel();
+ anchorRect = LayoutDeviceIntRect::FromAppUnitsToNearest(anchorRectAppUnits,
+ auPerDev);
+ if (anchorRect.width < 0) {
+ auto w = -anchorRect.width;
+ anchorRect.width += w + 1;
+ anchorRect.x += w;
+ }
+ LOG(" final %s\n", ToString(anchorRect).c_str());
+ }
+
+ LOG(" relative popup rect position [%d, %d] -> [%d x %d]\n", anchorRect.x,
+ anchorRect.y, anchorRect.width, anchorRect.height);
+
+ // Get gravity and flip type
+ GdkGravity rectAnchor = PopupAlignmentToGdkGravity(anchorAlign);
+ GdkGravity menuAnchor = PopupAlignmentToGdkGravity(popupAlign);
+
+ LOG(" parentRect gravity: %d anchor gravity: %d\n", rectAnchor, menuAnchor);
+
+ // Gtk default is: GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE | GDK_ANCHOR_RESIZE.
+ // We want to SLIDE_X menu on the dual monitor setup rather than resize it
+ // on the other monitor.
+ GdkAnchorHints hints =
+ GdkAnchorHints(GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_RESIZE);
+
+ // slideHorizontal from nsMenuPopupFrame::SetPopupPosition
+ int8_t position = popupFrame->GetAlignmentPosition();
+ if (position >= POPUPPOSITION_BEFORESTART &&
+ position <= POPUPPOSITION_AFTEREND) {
+ hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE_X);
+ }
+ // slideVertical from nsMenuPopupFrame::SetPopupPosition
+ if (position >= POPUPPOSITION_STARTBEFORE &&
+ position <= POPUPPOSITION_ENDAFTER) {
+ hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE_Y);
+ }
+
+ FlipType flipType = popupFrame->GetFlipType();
+ if (rectAnchor == GDK_GRAVITY_CENTER && menuAnchor == GDK_GRAVITY_CENTER) {
+ // only slide
+ hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE);
+ } else {
+ switch (flipType) {
+ case FlipType_Both:
+ hints = GdkAnchorHints(hints | GDK_ANCHOR_FLIP);
+ break;
+ case FlipType_Slide:
+ hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE);
+ break;
+ case FlipType_Default:
+ hints = GdkAnchorHints(hints | GDK_ANCHOR_FLIP);
+ break;
+ default:
+ break;
+ }
+ }
+ if (!WaylandPopupIsMenu()) {
+ // we don't want to slide menus to fit the screen rather resize them
+ hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE);
+ }
+
+ // We want tooltips to flip verticaly or slide only.
+ // See nsMenuPopupFrame::SetPopupPosition().
+ // https://searchfox.org/mozilla-central/rev/d0f5bc50aff3462c9d1546b88d60c5cb020eb15c/layout/xul/nsMenuPopupFrame.cpp#1603
+ if (mPopupType == PopupType::Tooltip) {
+ hints = GdkAnchorHints(GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE);
+ }
+
+ return {
+ anchorRect,
+ rectAnchor,
+ menuAnchor,
+ hints,
+ DevicePixelsToGdkPointRoundDown(LayoutDevicePoint::FromAppUnitsToNearest(
+ popupMargin.mPopupOffset,
+ popupFrame->PresContext()->AppUnitsPerDevPixel())),
+ true};
+}
+
+bool nsWindow::WaylandPopupAnchorAdjustForParentPopup(
+ GdkRectangle* aPopupAnchor, GdkPoint* aOffset) {
+ LOG("nsWindow::WaylandPopupAnchorAdjustForParentPopup");
+
+ GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell));
+ if (!parentGtkWindow || !GTK_IS_WIDGET(parentGtkWindow)) {
+ NS_WARNING("Popup has no parent!");
+ return false;
+ }
+ GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(parentGtkWindow));
+ if (!window) {
+ NS_WARNING("Popup parrent is not mapped!");
+ return false;
+ }
+
+ GdkRectangle parentWindowRect = {0, 0, gdk_window_get_width(window),
+ gdk_window_get_height(window)};
+ LOG(" parent window size %d x %d", parentWindowRect.width,
+ parentWindowRect.height);
+
+ // We can't have rectangle anchor with zero width/height.
+ if (!aPopupAnchor->width) {
+ aPopupAnchor->width = 1;
+ }
+ if (!aPopupAnchor->height) {
+ aPopupAnchor->height = 1;
+ }
+
+ GdkRectangle finalRect;
+ if (!gdk_rectangle_intersect(aPopupAnchor, &parentWindowRect, &finalRect)) {
+ return false;
+ }
+ *aPopupAnchor = finalRect;
+ LOG(" anchor is correct %d,%d -> %d x %d", finalRect.x, finalRect.y,
+ finalRect.width, finalRect.height);
+
+ *aOffset = mPopupMoveToRectParams.mOffset;
+ LOG(" anchor offset %d, %d", aOffset->x, aOffset->y);
+ return true;
+}
+
+bool nsWindow::WaylandPopupCheckAndGetAnchor(GdkRectangle* aPopupAnchor,
+ GdkPoint* aOffset) {
+ LOG("nsWindow::WaylandPopupCheckAndGetAnchor");
+
+ GdkWindow* gdkWindow = GetToplevelGdkWindow();
+ nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
+ if (!gdkWindow || !popupFrame) {
+ LOG(" can't use move-to-rect due missing gdkWindow or popupFrame");
+ return false;
+ }
+
+ if (popupFrame->IsConstrainedByLayout()) {
+ LOG(" can't use move-to-rect, flipped / constrained by layout");
+ return false;
+ }
+
+ if (!mPopupMoveToRectParams.mAnchorSet) {
+ LOG(" can't use move-to-rect due missing anchor");
+ return false;
+ }
+ // Update popup layout coordinates from layout by recent popup hierarchy
+ // (calculate correct position according to parent window)
+ // and convert to Gtk coordinates.
+ LayoutDeviceIntRect anchorRect = mPopupMoveToRectParams.mAnchorRect;
+ if (!WaylandPopupIsFirst()) {
+ GdkPoint parent = WaylandGetParentPosition();
+ LOG(" subtract parent position from anchor [%d, %d]\n", parent.x,
+ parent.y);
+ anchorRect.MoveBy(-GdkPointToDevicePixels(parent));
+ }
+
+ *aPopupAnchor = DevicePixelsToGdkRectRoundOut(anchorRect);
+ LOG(" anchored to rectangle [%d, %d] -> [%d x %d]", aPopupAnchor->x,
+ aPopupAnchor->y, aPopupAnchor->width, aPopupAnchor->height);
+
+ if (!WaylandPopupAnchorAdjustForParentPopup(aPopupAnchor, aOffset)) {
+ LOG(" can't use move-to-rect, anchor is not placed inside of parent "
+ "window");
+ return false;
+ }
+
+ return true;
+}
+
+void nsWindow::WaylandPopupPrepareForMove() {
+ LOG("nsWindow::WaylandPopupPrepareForMove()");
+
+ if (mPopupType == PopupType::Tooltip) {
+ // Don't fiddle with tooltips type, just hide it before move-to-rect
+ if (mPopupUseMoveToRect && gtk_widget_is_visible(mShell)) {
+ HideWaylandPopupWindow(/* aTemporaryHide */ true,
+ /* aRemoveFromPopupList */ false);
+ }
+ LOG(" it's tooltip, quit");
+ return;
+ }
+
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1785185#c8
+ // gtk_window_move() needs GDK_WINDOW_TYPE_HINT_UTILITY popup type.
+ // move-to-rect requires GDK_WINDOW_TYPE_HINT_POPUP_MENU popups type.
+ // We need to set it before map event when popup is hidden.
+ const GdkWindowTypeHint currentType =
+ gtk_window_get_type_hint(GTK_WINDOW(mShell));
+ const GdkWindowTypeHint requiredType = mPopupUseMoveToRect
+ ? GDK_WINDOW_TYPE_HINT_POPUP_MENU
+ : GDK_WINDOW_TYPE_HINT_UTILITY;
+
+ if (!mPopupUseMoveToRect && currentType == requiredType) {
+ LOG(" type matches and we're not forced to hide it, quit.");
+ return;
+ }
+
+ if (gtk_widget_is_visible(mShell)) {
+ HideWaylandPopupWindow(/* aTemporaryHide */ true,
+ /* aRemoveFromPopupList */ false);
+ }
+
+ if (currentType != requiredType) {
+ LOG(" set type %s",
+ requiredType == GDK_WINDOW_TYPE_HINT_POPUP_MENU ? "MENU" : "UTILITY");
+ gtk_window_set_type_hint(GTK_WINDOW(mShell), requiredType);
+ }
+}
+
+// Plain popup move on Wayland - simply place popup on given location.
+// We can't just call gtk_window_move() as it's not effective on visible
+// popups.
+void nsWindow::WaylandPopupMovePlain(int aX, int aY) {
+ LOG("nsWindow::WaylandPopupMovePlain(%d, %d)", aX, aY);
+
+ // We can directly move only popups based on wl_subsurface type.
+ MOZ_DIAGNOSTIC_ASSERT(gtk_window_get_type_hint(GTK_WINDOW(mShell)) ==
+ GDK_WINDOW_TYPE_HINT_UTILITY ||
+ gtk_window_get_type_hint(GTK_WINDOW(mShell)) ==
+ GDK_WINDOW_TYPE_HINT_TOOLTIP);
+
+ gtk_window_move(GTK_WINDOW(mShell), aX, aY);
+
+ // gtk_window_move() can trick us. When widget is hidden gtk_window_move()
+ // does not move the widget but sets new widget coordinates when widget
+ // is mapped again.
+ //
+ // If popup used move-to-rect before
+ // (GdkWindow has POSITION_METHOD_MOVE_TO_RECT set), popup will use
+ // move-to-rect again when it's mapped and we'll get bogus move-to-rect
+ // callback.
+ //
+ // gdk_window_move() sets position_method to POSITION_METHOD_MOVE_RESIZE
+ // so we'll use plain move when popup is shown.
+ if (!gtk_widget_get_mapped(mShell)) {
+ if (GdkWindow* window = GetToplevelGdkWindow()) {
+ gdk_window_move(window, aX, aY);
+ }
+ }
+}
+
+void nsWindow::WaylandPopupMoveImpl() {
+ // Available as of GTK 3.24+
+ static auto sGdkWindowMoveToRect = (void (*)(
+ GdkWindow*, const GdkRectangle*, GdkGravity, GdkGravity, GdkAnchorHints,
+ gint, gint))dlsym(RTLD_DEFAULT, "gdk_window_move_to_rect");
+
+ if (mPopupUseMoveToRect && !sGdkWindowMoveToRect) {
+ LOG("can't use move-to-rect due missing gdk_window_move_to_rect()");
+ mPopupUseMoveToRect = false;
+ }
+
+ GdkRectangle gtkAnchorRect;
+ GdkPoint offset;
+ if (mPopupUseMoveToRect) {
+ mPopupUseMoveToRect =
+ WaylandPopupCheckAndGetAnchor(&gtkAnchorRect, &offset);
+ }
+
+ LOG("nsWindow::WaylandPopupMove");
+ LOG(" original widget popup position [%d, %d]\n", mPopupPosition.x,
+ mPopupPosition.y);
+ LOG(" relative widget popup position [%d, %d]\n", mRelativePopupPosition.x,
+ mRelativePopupPosition.y);
+ LOG(" popup use move to rect %d", mPopupUseMoveToRect);
+
+ WaylandPopupPrepareForMove();
+
+ if (!mPopupUseMoveToRect) {
+ WaylandPopupMovePlain(mRelativePopupPosition.x, mRelativePopupPosition.y);
+ // Layout already should be aware of our bounds, since we didn't change it
+ // from the widget side for flipping or so.
+ return;
+ }
+
+ // Correct popup position now. It will be updated by gdk_window_move_to_rect()
+ // anyway but we need to set it now to avoid a race condition here.
+ WaylandPopupRemoveNegativePosition();
+
+ GdkWindow* gdkWindow = GetToplevelGdkWindow();
+ if (!g_signal_handler_find(gdkWindow, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr,
+ FuncToGpointer(NativeMoveResizeCallback), this)) {
+ g_signal_connect(gdkWindow, "moved-to-rect",
+ G_CALLBACK(NativeMoveResizeCallback), this);
+ }
+ mWaitingForMoveToRectCallback = true;
+
+#ifdef MOZ_LOGGING
+ if (LOG_ENABLED()) {
+ LOG(" Call move-to-rect");
+ LOG(" Anchor rect [%d, %d] -> [%d x %d]", gtkAnchorRect.x, gtkAnchorRect.y,
+ gtkAnchorRect.width, gtkAnchorRect.height);
+ LOG(" Offset [%d, %d]", offset.x, offset.y);
+ LOG(" AnchorType");
+ LogPopupGravity(mPopupMoveToRectParams.mAnchorRectType);
+ LOG(" PopupAnchorType");
+ LogPopupGravity(mPopupMoveToRectParams.mPopupAnchorType);
+ LogPopupAnchorHints(mPopupMoveToRectParams.mHints);
+ }
+#endif
+
+ sGdkWindowMoveToRect(gdkWindow, &gtkAnchorRect,
+ mPopupMoveToRectParams.mAnchorRectType,
+ mPopupMoveToRectParams.mPopupAnchorType,
+ mPopupMoveToRectParams.mHints, offset.x, offset.y);
+}
+
+void nsWindow::SetZIndex(int32_t aZIndex) {
+ nsIWidget* oldPrev = GetPrevSibling();
+
+ nsBaseWidget::SetZIndex(aZIndex);
+
+ if (GetPrevSibling() == oldPrev) {
+ return;
+ }
+
+ // We skip the nsWindows that don't have mGdkWindows.
+ // These are probably in the process of being destroyed.
+ if (!mGdkWindow) {
+ return;
+ }
+
+ if (!GetNextSibling()) {
+ // We're to be on top.
+ if (mGdkWindow) {
+ gdk_window_raise(mGdkWindow);
+ }
+ } else {
+ // All the siblings before us need to be below our widget.
+ for (nsWindow* w = this; w;
+ w = static_cast<nsWindow*>(w->GetPrevSibling())) {
+ if (w->mGdkWindow) {
+ gdk_window_lower(w->mGdkWindow);
+ }
+ }
+ }
+}
+
+void nsWindow::SetSizeMode(nsSizeMode aMode) {
+ LOG("nsWindow::SetSizeMode %d\n", aMode);
+
+ // Return if there's no shell or our current state is the same as the mode we
+ // were just set to.
+ if (!mShell) {
+ LOG(" no shell");
+ return;
+ }
+
+ if (mSizeMode == aMode && mLastSizeModeRequest == aMode) {
+ LOG(" already set");
+ return;
+ }
+
+ // It is tempting to try to optimize calls below based only on current
+ // mSizeMode, but that wouldn't work if there's a size-request in flight
+ // (specially before show). See bug 1789823.
+ const auto SizeModeMightBe = [&](nsSizeMode aModeToTest) {
+ if (mSizeMode != mLastSizeModeRequest) {
+ // Arbitrary size mode requests might be ongoing.
+ return true;
+ }
+ return mSizeMode == aModeToTest;
+ };
+
+ if (aMode != nsSizeMode_Fullscreen && aMode != nsSizeMode_Minimized) {
+ // Fullscreen and minimized are compatible.
+ if (SizeModeMightBe(nsSizeMode_Fullscreen)) {
+ MakeFullScreen(false);
+ }
+ }
+
+ switch (aMode) {
+ case nsSizeMode_Maximized:
+ LOG(" set maximized");
+ gtk_window_maximize(GTK_WINDOW(mShell));
+ break;
+ case nsSizeMode_Minimized:
+ LOG(" set minimized");
+ gtk_window_iconify(GTK_WINDOW(mShell));
+ break;
+ case nsSizeMode_Fullscreen:
+ LOG(" set fullscreen");
+ MakeFullScreen(true);
+ break;
+ default:
+ MOZ_FALLTHROUGH_ASSERT("Unknown size mode");
+ case nsSizeMode_Normal:
+ LOG(" set normal");
+ if (SizeModeMightBe(nsSizeMode_Maximized)) {
+ gtk_window_unmaximize(GTK_WINDOW(mShell));
+ }
+ if (SizeModeMightBe(nsSizeMode_Minimized)) {
+ gtk_window_deiconify(GTK_WINDOW(mShell));
+ // We need this for actual deiconification on mutter.
+ gtk_window_present(GTK_WINDOW(mShell));
+ }
+ break;
+ }
+ mLastSizeModeRequest = aMode;
+}
+
+#define kDesktopMutterSchema "org.gnome.mutter"_ns
+#define kDesktopDynamicWorkspacesKey "dynamic-workspaces"_ns
+
+static bool WorkspaceManagementDisabled(GdkScreen* screen) {
+ if (Preferences::GetBool("widget.disable-workspace-management", false)) {
+ return true;
+ }
+ if (Preferences::HasUserValue("widget.workspace-management")) {
+ return Preferences::GetBool("widget.workspace-management");
+ }
+
+ if (IsGnomeDesktopEnvironment()) {
+ // Gnome uses dynamic workspaces by default so disable workspace management
+ // in that case.
+ bool usesDynamicWorkspaces = true;
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ if (gsettings) {
+ nsCOMPtr<nsIGSettingsCollection> mutterSettings;
+ gsettings->GetCollectionForSchema(kDesktopMutterSchema,
+ getter_AddRefs(mutterSettings));
+ if (mutterSettings) {
+ mutterSettings->GetBoolean(kDesktopDynamicWorkspacesKey,
+ &usesDynamicWorkspaces);
+ }
+ }
+ return usesDynamicWorkspaces;
+ }
+
+ const auto& desktop = GetDesktopEnvironmentIdentifier();
+ return desktop.EqualsLiteral("bspwm") || desktop.EqualsLiteral("i3");
+}
+
+void nsWindow::GetWorkspaceID(nsAString& workspaceID) {
+ workspaceID.Truncate();
+
+ if (!GdkIsX11Display() || !mShell) {
+ return;
+ }
+
+#ifdef MOZ_X11
+ LOG("nsWindow::GetWorkspaceID()\n");
+
+ // Get the gdk window for this widget.
+ GdkWindow* gdk_window = GetToplevelGdkWindow();
+ if (!gdk_window) {
+ LOG(" missing Gdk window, quit.");
+ return;
+ }
+
+ if (WorkspaceManagementDisabled(gdk_window_get_screen(gdk_window))) {
+ LOG(" WorkspaceManagementDisabled, quit.");
+ return;
+ }
+
+ GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL);
+ GdkAtom type_returned;
+ int format_returned;
+ int length_returned;
+ long* wm_desktop;
+
+ if (!gdk_property_get(gdk_window, gdk_atom_intern("_NET_WM_DESKTOP", FALSE),
+ cardinal_atom,
+ 0, // offset
+ INT32_MAX, // length
+ FALSE, // delete
+ &type_returned, &format_returned, &length_returned,
+ (guchar**)&wm_desktop)) {
+ LOG(" gdk_property_get() failed, quit.");
+ return;
+ }
+
+ LOG(" got workspace ID %d", (int32_t)wm_desktop[0]);
+ workspaceID.AppendInt((int32_t)wm_desktop[0]);
+ g_free(wm_desktop);
+#endif
+}
+
+void nsWindow::MoveToWorkspace(const nsAString& workspaceIDStr) {
+ nsresult rv = NS_OK;
+ int32_t workspaceID = workspaceIDStr.ToInteger(&rv);
+
+ LOG("nsWindow::MoveToWorkspace() ID %d", workspaceID);
+ if (NS_FAILED(rv) || !workspaceID || !GdkIsX11Display() || !mShell) {
+ LOG(" MoveToWorkspace disabled, quit");
+ return;
+ }
+
+#ifdef MOZ_X11
+ // Get the gdk window for this widget.
+ GdkWindow* gdk_window = GetToplevelGdkWindow();
+ if (!gdk_window) {
+ LOG(" failed to get GdkWindow, quit.");
+ return;
+ }
+
+ // This code is inspired by some found in the 'gxtuner' project.
+ // https://github.com/brummer10/gxtuner/blob/792d453da0f3a599408008f0f1107823939d730d/deskpager.cpp#L50
+ XEvent xevent;
+ Display* xdisplay = gdk_x11_get_default_xdisplay();
+ GdkScreen* screen = gdk_window_get_screen(gdk_window);
+ Window root_win = GDK_WINDOW_XID(gdk_screen_get_root_window(screen));
+ GdkDisplay* display = gdk_window_get_display(gdk_window);
+ Atom type = gdk_x11_get_xatom_by_name_for_display(display, "_NET_WM_DESKTOP");
+
+ xevent.type = ClientMessage;
+ xevent.xclient.type = ClientMessage;
+ xevent.xclient.serial = 0;
+ xevent.xclient.send_event = TRUE;
+ xevent.xclient.display = xdisplay;
+ xevent.xclient.window = GDK_WINDOW_XID(gdk_window);
+ xevent.xclient.message_type = type;
+ xevent.xclient.format = 32;
+ xevent.xclient.data.l[0] = workspaceID;
+ xevent.xclient.data.l[1] = X11CurrentTime;
+ xevent.xclient.data.l[2] = 0;
+ xevent.xclient.data.l[3] = 0;
+ xevent.xclient.data.l[4] = 0;
+
+ XSendEvent(xdisplay, root_win, FALSE,
+ SubstructureNotifyMask | SubstructureRedirectMask, &xevent);
+
+ XFlush(xdisplay);
+ LOG(" moved to workspace");
+#endif
+}
+
+void nsWindow::SetUserTimeAndStartupTokenForActivatedWindow() {
+ nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit();
+ if (!toolkit || MOZ_UNLIKELY(mWindowType == WindowType::Invisible)) {
+ return;
+ }
+
+ mWindowActivationTokenFromEnv = toolkit->GetStartupToken();
+ if (!mWindowActivationTokenFromEnv.IsEmpty()) {
+ if (!GdkIsWaylandDisplay()) {
+ gtk_window_set_startup_id(GTK_WINDOW(mShell),
+ mWindowActivationTokenFromEnv.get());
+ // In the case of X11, the above call is all we need. For wayland we need
+ // to keep the token around until we take it in
+ // TransferFocusToWaylandWindow.
+ mWindowActivationTokenFromEnv.Truncate();
+ }
+ } else if (uint32_t timestamp = toolkit->GetFocusTimestamp()) {
+ // We don't have the data we need. Fall back to an
+ // approximation ... using the timestamp of the remote command
+ // being received as a guess for the timestamp of the user event
+ // that triggered it.
+ gdk_window_focus(GetToplevelGdkWindow(), timestamp);
+ }
+
+ // If we used the startup ID, that already contains the focus timestamp;
+ // we don't want to reuse the timestamp next time we raise the window
+ toolkit->SetFocusTimestamp(0);
+ toolkit->SetStartupToken(""_ns);
+}
+
+/* static */
+guint32 nsWindow::GetLastUserInputTime() {
+ // gdk_x11_display_get_user_time/gtk_get_current_event_time tracks
+ // button and key presses, DESKTOP_STARTUP_ID used to start the app,
+ // drop events from external drags,
+ // WM_DELETE_WINDOW delete events, but not usually mouse motion nor
+ // button and key releases. Therefore use the most recent of
+ // gdk_x11_display_get_user_time and the last time that we have seen.
+#ifdef MOZ_X11
+ GdkDisplay* gdkDisplay = gdk_display_get_default();
+ guint32 timestamp = GdkIsX11Display(gdkDisplay)
+ ? gdk_x11_display_get_user_time(gdkDisplay)
+ : gtk_get_current_event_time();
+#else
+ guint32 timestamp = gtk_get_current_event_time();
+#endif
+
+ if (sLastUserInputTime != GDK_CURRENT_TIME &&
+ TimestampIsNewerThan(sLastUserInputTime, timestamp)) {
+ return sLastUserInputTime;
+ }
+
+ return timestamp;
+}
+
+#ifdef MOZ_WAYLAND
+void nsWindow::FocusWaylandWindow(const char* aTokenID) {
+ MOZ_DIAGNOSTIC_ASSERT(aTokenID);
+
+ LOG("nsWindow::FocusWaylandWindow(%s)", aTokenID);
+ if (IsDestroyed()) {
+ LOG(" already destroyed, quit.");
+ return;
+ }
+ wl_surface* surface =
+ mGdkWindow ? gdk_wayland_window_get_wl_surface(mGdkWindow) : nullptr;
+ if (!surface) {
+ LOG(" mGdkWindow is not visible, quit.");
+ return;
+ }
+
+ LOG(" requesting xdg-activation, surface ID %d",
+ wl_proxy_get_id((struct wl_proxy*)surface));
+ xdg_activation_v1* xdg_activation = WaylandDisplayGet()->GetXdgActivation();
+ if (!xdg_activation) {
+ return;
+ }
+ xdg_activation_v1_activate(xdg_activation, aTokenID, surface);
+}
+
+// Transfer focus from gFocusWindow to aWindow and use xdg_activation
+// protocol for it.
+void nsWindow::TransferFocusToWaylandWindow(nsWindow* aWindow) {
+ LOGW("nsWindow::TransferFocusToWaylandWindow(%p) gFocusWindow %p", aWindow,
+ gFocusWindow);
+ auto promise = mozilla::widget::RequestWaylandFocusPromise();
+ if (NS_WARN_IF(!promise)) {
+ LOGW(" quit, failed to create TransferFocusToWaylandWindow [%p]", aWindow);
+ return;
+ }
+ promise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ /* resolve */
+ [window = RefPtr{aWindow}](nsCString token) {
+ window->FocusWaylandWindow(token.get());
+ },
+ /* reject */
+ [window = RefPtr{aWindow}](bool state) {
+ LOGW("TransferFocusToWaylandWindow [%p] failed", window.get());
+ });
+}
+#endif
+
+// Request activation of this window or give focus to this widget.
+// aRaise means whether we should request activation of this widget's
+// toplevel window.
+//
+// nsWindow::SetFocus(Raise::Yes) - Raise and give focus to toplevel window.
+// nsWindow::SetFocus(Raise::No) - Give focus to this window.
+void nsWindow::SetFocus(Raise aRaise, mozilla::dom::CallerType aCallerType) {
+ LOG("nsWindow::SetFocus Raise %d\n", aRaise == Raise::Yes);
+
+ // Raise the window if someone passed in true and the prefs are
+ // set properly.
+ GtkWidget* toplevelWidget = gtk_widget_get_toplevel(GTK_WIDGET(mContainer));
+
+ LOG(" gFocusWindow [%p]\n", gFocusWindow);
+ LOG(" mContainer [%p]\n", GTK_WIDGET(mContainer));
+ LOG(" Toplevel widget [%p]\n", toplevelWidget);
+
+ // Make sure that our owning widget has focus. If it doesn't try to
+ // grab it. Note that we don't set our focus flag in this case.
+ if (StaticPrefs::mozilla_widget_raise_on_setfocus_AtStartup() &&
+ aRaise == Raise::Yes && toplevelWidget &&
+ !gtk_widget_has_focus(toplevelWidget)) {
+ if (gtk_widget_get_visible(mShell)) {
+ LOG(" toplevel is not focused");
+ gdk_window_show_unraised(GetToplevelGdkWindow());
+ // Unset the urgency hint if possible.
+ SetUrgencyHint(mShell, false);
+ }
+ }
+
+ RefPtr<nsWindow> toplevelWindow = get_window_for_gtk_widget(toplevelWidget);
+ if (!toplevelWindow) {
+ LOG(" missing toplevel nsWindow, quit\n");
+ return;
+ }
+
+ if (aRaise == Raise::Yes) {
+ // means request toplevel activation.
+
+ // This is asynchronous. If and when the window manager accepts the request,
+ // then the focus widget will get a focus-in-event signal.
+ if (StaticPrefs::mozilla_widget_raise_on_setfocus_AtStartup() &&
+ toplevelWindow->mIsShown && toplevelWindow->mShell &&
+ !gtk_window_is_active(GTK_WINDOW(toplevelWindow->mShell))) {
+ LOG(" toplevel is visible but not active, requesting activation [%p]",
+ toplevelWindow.get());
+
+ // Take the time here explicitly for the call below.
+ const uint32_t timestamp = [&] {
+ if (nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit()) {
+ if (uint32_t t = toolkit->GetFocusTimestamp()) {
+ toolkit->SetFocusTimestamp(0);
+ return t;
+ }
+ }
+ return GetLastUserInputTime();
+ }();
+
+ toplevelWindow->SetUserTimeAndStartupTokenForActivatedWindow();
+ gtk_window_present_with_time(GTK_WINDOW(toplevelWindow->mShell),
+ timestamp);
+
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ auto existingToken =
+ std::move(toplevelWindow->mWindowActivationTokenFromEnv);
+ if (!existingToken.IsEmpty()) {
+ LOG(" has existing activation token.");
+ toplevelWindow->FocusWaylandWindow(existingToken.get());
+ } else {
+ LOG(" missing activation token, try to transfer from focused "
+ "window");
+ TransferFocusToWaylandWindow(toplevelWindow);
+ }
+ }
+#endif
+ }
+ return;
+ }
+
+ // aRaise == No means that keyboard events should be dispatched from this
+ // widget.
+
+ // Ensure GTK_WIDGET(mContainer) is the focused GtkWidget within its toplevel
+ // window.
+ //
+ // For WindowType::Popup, this GtkWidget may not actually be the one that
+ // receives the key events as it may be the parent window that is active.
+ if (!gtk_widget_is_focus(GTK_WIDGET(mContainer))) {
+ // This is synchronous. It takes focus from a plugin or from a widget
+ // in an embedder. The focus manager already knows that this window
+ // is active so gBlockActivateEvent avoids another (unnecessary)
+ // activate notification.
+ gBlockActivateEvent = true;
+ gtk_widget_grab_focus(GTK_WIDGET(mContainer));
+ gBlockActivateEvent = false;
+ }
+
+ // If this is the widget that already has focus, return.
+ if (gFocusWindow == this) {
+ LOG(" already have focus");
+ return;
+ }
+
+ // Set this window to be the focused child window
+ gFocusWindow = this;
+
+ if (mIMContext) {
+ mIMContext->OnFocusWindow(this);
+ }
+
+ LOG(" widget now has focus in SetFocus()");
+}
+
+LayoutDeviceIntRect nsWindow::GetScreenBounds() {
+ if (!mGdkWindow) {
+ return mBounds;
+ }
+
+ const LayoutDeviceIntPoint origin = [&] {
+ gint x, y;
+ gdk_window_get_root_origin(mGdkWindow, &x, &y);
+
+ // Workaround for https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4820
+ // Bug 1775017 Gtk < 3.24.35 returns scaled values for
+ // override redirected window on X11.
+ if (gtk_check_version(3, 24, 35) != nullptr && GdkIsX11Display() &&
+ gdk_window_get_window_type(mGdkWindow) == GDK_WINDOW_TEMP) {
+ return LayoutDeviceIntPoint(x, y);
+ }
+ return GdkPointToDevicePixels({x, y});
+ }();
+
+ // mBounds.Size() is the window bounds, not the window-manager frame
+ // bounds (bug 581863). gdk_window_get_frame_extents would give the
+ // frame bounds, but mBounds.Size() is returned here for consistency
+ // with Resize.
+ const LayoutDeviceIntRect rect(origin, mBounds.Size());
+#if MOZ_LOGGING
+ if (MOZ_LOG_TEST(IsPopup() ? gWidgetPopupLog : gWidgetLog,
+ LogLevel::Verbose)) {
+ gint scale = GdkCeiledScaleFactor();
+ if (mLastLoggedScale != scale || !(mLastLoggedBoundSize == rect)) {
+ mLastLoggedScale = scale;
+ mLastLoggedBoundSize = rect;
+ LOG("GetScreenBounds %d,%d -> %d x %d, unscaled %d,%d -> %d x %d\n",
+ rect.x, rect.y, rect.width, rect.height, rect.x / scale,
+ rect.y / scale, rect.width / scale, rect.height / scale);
+ }
+ }
+#endif
+ return rect;
+}
+
+LayoutDeviceIntSize nsWindow::GetClientSize() {
+ return LayoutDeviceIntSize(mBounds.width, mBounds.height);
+}
+
+LayoutDeviceIntRect nsWindow::GetClientBounds() {
+ // GetBounds returns a rect whose top left represents the top left of the
+ // outer bounds, but whose width/height represent the size of the inner
+ // bounds (which is messed up).
+ LayoutDeviceIntRect rect = GetBounds();
+ rect.MoveBy(GetClientOffset());
+ return rect;
+}
+
+void nsWindow::RecomputeClientOffset(bool aNotify) {
+ if (!IsTopLevelWindowType()) {
+ return;
+ }
+
+ auto oldOffset = mClientOffset;
+
+ mClientOffset = WidgetToScreenOffset() - mBounds.TopLeft();
+
+ if (aNotify && mClientOffset != oldOffset) {
+ // Send a WindowMoved notification. This ensures that BrowserParent picks up
+ // the new client offset and sends it to the child process if appropriate.
+ NotifyWindowMoved(mBounds.x, mBounds.y);
+ }
+}
+
+gboolean nsWindow::OnPropertyNotifyEvent(GtkWidget* aWidget,
+ GdkEventProperty* aEvent) {
+ if (aEvent->atom == gdk_atom_intern("_NET_FRAME_EXTENTS", FALSE)) {
+ RecomputeClientOffset(/* aNotify = */ true);
+ return FALSE;
+ }
+ if (!mGdkWindow) {
+ return FALSE;
+ }
+#ifdef MOZ_X11
+ if (GetCurrentTimeGetter()->PropertyNotifyHandler(aWidget, aEvent)) {
+ return TRUE;
+ }
+#endif
+ return FALSE;
+}
+
+static GdkCursor* GetCursorForImage(const nsIWidget::Cursor& aCursor,
+ int32_t aWidgetScaleFactor) {
+ if (!aCursor.IsCustom()) {
+ return nullptr;
+ }
+ nsIntSize size = nsIWidget::CustomCursorSize(aCursor);
+
+ // NOTE: GTK only allows integer scale factors, so we ceil to the larger scale
+ // factor and then tell gtk to scale it down. We ensure to scale at least to
+ // the GDK scale factor, so that cursors aren't downsized in HiDPI on wayland,
+ // see bug 1707533.
+ int32_t gtkScale = std::max(
+ aWidgetScaleFactor, int32_t(std::ceil(std::max(aCursor.mResolution.mX,
+ aCursor.mResolution.mY))));
+
+ // Reject cursors greater than 128 pixels in some direction, to prevent
+ // spoofing.
+ // XXX ideally we should rescale. Also, we could modify the API to
+ // allow trusted content to set larger cursors.
+ //
+ // TODO(emilio, bug 1445844): Unify the solution for this with other
+ // platforms.
+ if (size.width > 128 || size.height > 128) {
+ return nullptr;
+ }
+
+ nsIntSize rasterSize = size * gtkScale;
+ RefPtr<GdkPixbuf> pixbuf =
+ nsImageToPixbuf::ImageToPixbuf(aCursor.mContainer, Some(rasterSize));
+ if (!pixbuf) {
+ return nullptr;
+ }
+
+ // Looks like all cursors need an alpha channel (tested on Gtk 2.4.4). This
+ // is of course not documented anywhere...
+ // So add one if there isn't one yet
+ if (!gdk_pixbuf_get_has_alpha(pixbuf)) {
+ RefPtr<GdkPixbuf> alphaBuf =
+ dont_AddRef(gdk_pixbuf_add_alpha(pixbuf, FALSE, 0, 0, 0));
+ pixbuf = std::move(alphaBuf);
+ if (!pixbuf) {
+ return nullptr;
+ }
+ }
+
+ cairo_surface_t* surface =
+ gdk_cairo_surface_create_from_pixbuf(pixbuf, gtkScale, nullptr);
+ if (!surface) {
+ return nullptr;
+ }
+
+ auto CleanupSurface =
+ MakeScopeExit([&]() { cairo_surface_destroy(surface); });
+
+ return gdk_cursor_new_from_surface(gdk_display_get_default(), surface,
+ aCursor.mHotspotX, aCursor.mHotspotY);
+}
+
+void nsWindow::SetCursor(const Cursor& aCursor) {
+ if (mWidgetCursorLocked || !mGdkWindow) {
+ return;
+ }
+
+ // Only change cursor if it's actually been changed
+ if (!mUpdateCursor && mCursor == aCursor) {
+ return;
+ }
+
+ mUpdateCursor = false;
+ mCursor = aCursor;
+
+ // Try to set the cursor image first, and fall back to the numeric cursor.
+ GdkCursor* imageCursor = nullptr;
+ if (mCustomCursorAllowed) {
+ imageCursor = GetCursorForImage(aCursor, GdkCeiledScaleFactor());
+ }
+
+ // When using a custom cursor, clear the cursor first using eCursor_none, in
+ // order to work around https://gitlab.gnome.org/GNOME/gtk/-/issues/6242
+ GdkCursor* nonImageCursor =
+ get_gtk_cursor(imageCursor ? eCursor_none : aCursor.mDefaultCursor);
+ auto CleanupCursor = mozilla::MakeScopeExit([&]() {
+ // get_gtk_cursor returns a weak reference, which we shouldn't unref.
+ if (imageCursor) {
+ g_object_unref(imageCursor);
+ }
+ });
+
+ gdk_window_set_cursor(mGdkWindow, nonImageCursor);
+ if (imageCursor) {
+ gdk_window_set_cursor(mGdkWindow, imageCursor);
+ }
+}
+
+void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) {
+ if (!mGdkWindow) {
+ return;
+ }
+
+ GdkRectangle rect = DevicePixelsToGdkRectRoundOut(aRect);
+ gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE);
+
+ LOG("Invalidate (rect): %d %d %d %d\n", rect.x, rect.y, rect.width,
+ rect.height);
+}
+
+void* nsWindow::GetNativeData(uint32_t aDataType) {
+ switch (aDataType) {
+ case NS_NATIVE_WINDOW:
+ case NS_NATIVE_WIDGET: {
+ return mGdkWindow;
+ }
+
+ case NS_NATIVE_SHELLWIDGET:
+ return GetToplevelWidget();
+
+ case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID:
+ if (!mGdkWindow) {
+ return nullptr;
+ }
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ return (void*)GDK_WINDOW_XID(gdk_window_get_toplevel(mGdkWindow));
+ }
+#endif
+ NS_WARNING(
+ "nsWindow::GetNativeData(): NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID is not "
+ "handled on Wayland!");
+ return nullptr;
+ case NS_RAW_NATIVE_IME_CONTEXT: {
+ void* pseudoIMEContext = GetPseudoIMEContext();
+ if (pseudoIMEContext) {
+ return pseudoIMEContext;
+ }
+ // If IME context isn't available on this widget, we should set |this|
+ // instead of nullptr.
+ if (!mIMContext) {
+ return this;
+ }
+ return mIMContext.get();
+ }
+ case NS_NATIVE_OPENGL_CONTEXT:
+ return nullptr;
+ case NS_NATIVE_EGL_WINDOW: {
+ void* eglWindow = nullptr;
+
+ // We can't block on mutex here as it leads to a deadlock:
+ // 1) mutex is taken at nsWindow::Destroy()
+ // 2) NS_NATIVE_EGL_WINDOW is called from compositor/rendering thread,
+ // blocking on mutex.
+ // 3) DestroyCompositor() is called by nsWindow::Destroy(). As a sync
+ // call it waits to compositor/rendering threads,
+ // but they're blocked at 2).
+ // It's fine if we return null EGL window during DestroyCompositor(),
+ // in such case compositor painting is skipped.
+ if (mDestroyMutex.TryLock()) {
+ if (mGdkWindow && !mIsDestroyed) {
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ eglWindow = (void*)GDK_WINDOW_XID(mGdkWindow);
+ }
+#endif
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ bool hiddenWindow =
+ mCompositorWidgetDelegate &&
+ mCompositorWidgetDelegate->AsGtkCompositorWidget() &&
+ mCompositorWidgetDelegate->AsGtkCompositorWidget()->IsHidden();
+ if (!hiddenWindow) {
+ eglWindow = moz_container_wayland_get_egl_window(
+ mContainer, FractionalScaleFactor());
+ }
+ }
+#endif
+ }
+ mDestroyMutex.Unlock();
+ }
+
+ LOG("Get NS_NATIVE_EGL_WINDOW mGdkWindow %p returned eglWindow %p",
+ mGdkWindow, eglWindow);
+ return eglWindow;
+ }
+ default:
+ NS_WARNING("nsWindow::GetNativeData called with bad value");
+ return nullptr;
+ }
+}
+
+nsresult nsWindow::SetTitle(const nsAString& aTitle) {
+ if (!mShell) {
+ return NS_OK;
+ }
+
+ // convert the string into utf8 and set the title.
+#define UTF8_FOLLOWBYTE(ch) (((ch) & 0xC0) == 0x80)
+ NS_ConvertUTF16toUTF8 titleUTF8(aTitle);
+ if (titleUTF8.Length() > NS_WINDOW_TITLE_MAX_LENGTH) {
+ // Truncate overlong titles (bug 167315). Make sure we chop after a
+ // complete sequence by making sure the next char isn't a follow-byte.
+ uint32_t len = NS_WINDOW_TITLE_MAX_LENGTH;
+ while (UTF8_FOLLOWBYTE(titleUTF8[len])) --len;
+ titleUTF8.Truncate(len);
+ }
+ gtk_window_set_title(GTK_WINDOW(mShell), (const char*)titleUTF8.get());
+
+ return NS_OK;
+}
+
+void nsWindow::SetIcon(const nsAString& aIconSpec) {
+ if (!mShell) {
+ return;
+ }
+
+ nsAutoCString iconName;
+
+ if (aIconSpec.EqualsLiteral("default")) {
+ nsAutoString brandName;
+ WidgetUtils::GetBrandShortName(brandName);
+ if (brandName.IsEmpty()) {
+ brandName.AssignLiteral(u"Mozilla");
+ }
+ AppendUTF16toUTF8(brandName, iconName);
+ ToLowerCase(iconName);
+ } else {
+ AppendUTF16toUTF8(aIconSpec, iconName);
+ }
+
+ nsCOMPtr<nsIFile> iconFile;
+ nsAutoCString path;
+
+ gint* iconSizes = gtk_icon_theme_get_icon_sizes(gtk_icon_theme_get_default(),
+ iconName.get());
+ bool foundIcon = (iconSizes[0] != 0);
+ g_free(iconSizes);
+
+ if (!foundIcon) {
+ // Look for icons with the following suffixes appended to the base name
+ // The last two entries (for the old XPM format) will be ignored unless
+ // no icons are found using other suffixes. XPM icons are deprecated.
+
+ const char16_t extensions[9][8] = {u".png", u"16.png", u"32.png",
+ u"48.png", u"64.png", u"128.png",
+ u"256.png", u".xpm", u"16.xpm"};
+
+ for (uint32_t i = 0; i < ArrayLength(extensions); i++) {
+ // Don't bother looking for XPM versions if we found a PNG.
+ if (i == ArrayLength(extensions) - 2 && foundIcon) break;
+
+ ResolveIconName(aIconSpec, nsDependentString(extensions[i]),
+ getter_AddRefs(iconFile));
+ if (iconFile) {
+ iconFile->GetNativePath(path);
+ GdkPixbuf* icon = gdk_pixbuf_new_from_file(path.get(), nullptr);
+ if (icon) {
+ gtk_icon_theme_add_builtin_icon(iconName.get(),
+ gdk_pixbuf_get_height(icon), icon);
+ g_object_unref(icon);
+ foundIcon = true;
+ }
+ }
+ }
+ }
+
+ // leave the default icon intact if no matching icons were found
+ if (foundIcon) {
+ gtk_window_set_icon_name(GTK_WINDOW(mShell), iconName.get());
+ }
+}
+
+/* TODO(bug 1655924): gdk_window_get_origin is can block waiting for the X
+ server for a long time, we would like to use the implementation below
+ instead. However, removing the synchronous x server queries causes a race
+ condition to surface, causing issues such as bug 1652743 and bug 1653711.
+
+
+ This code can be used instead of gdk_window_get_origin() but it cuases
+ such issues:
+
+ *aX = 0;
+ *aY = 0;
+ if (!aWindow) {
+ return;
+ }
+
+ GdkWindow* current = aWindow;
+ while (GdkWindow* parent = gdk_window_get_parent(current)) {
+ if (parent == current) {
+ break;
+ }
+
+ int x = 0;
+ int y = 0;
+ gdk_window_get_position(current, &x, &y);
+ *aX += x;
+ *aY += y;
+
+ current = parent;
+ }
+*/
+LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() {
+ // Don't use gdk_window_get_origin() on wl_subsurface Wayland popups
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/5287
+ if (IsWaylandPopup() && !mPopupUseMoveToRect) {
+ return mBounds.TopLeft();
+ }
+ nsIntPoint origin(0, 0);
+ if (mGdkWindow) {
+ gdk_window_get_origin(mGdkWindow, &origin.x.value, &origin.y.value);
+ }
+ return GdkPointToDevicePixels({origin.x, origin.y});
+}
+
+void nsWindow::CaptureRollupEvents(bool aDoCapture) {
+ LOG("CaptureRollupEvents(%d)\n", aDoCapture);
+ if (mIsDestroyed) {
+ return;
+ }
+
+ static constexpr auto kCaptureEventsMask =
+ GdkEventMask(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK | GDK_TOUCH_MASK);
+
+ static bool sSystemNeedsPointerGrab = [&] {
+ if (GdkIsWaylandDisplay()) {
+ return false;
+ }
+ // We only need to grab the pointer for X servers that move the focus with
+ // the pointer (like twm, sawfish...). Since we roll up popups on focus out,
+ // not grabbing the pointer triggers rollup when the mouse enters the popup
+ // and leaves the main window, see bug 1807482.
+ //
+ // FVWM is also affected but less severely: the pointer can enter the
+ // popup, but if it briefly moves out of the popup and over the main window
+ // then we see a focus change and roll up the popup.
+ //
+ // We don't do it for most common desktops, if only because it causes X11
+ // crashes like bug 1607713.
+ const auto& desktop = GetDesktopEnvironmentIdentifier();
+ return desktop.EqualsLiteral("twm") || desktop.EqualsLiteral("sawfish") ||
+ StringBeginsWith(desktop, "fvwm"_ns);
+ }();
+
+ const bool grabPointer = [] {
+ switch (StaticPrefs::widget_gtk_grab_pointer()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return sSystemNeedsPointerGrab;
+ }
+ }();
+
+ if (!grabPointer) {
+ return;
+ }
+
+ mNeedsToRetryCapturingMouse = false;
+ if (aDoCapture) {
+ if (mIsDragPopup || DragInProgress()) {
+ // Don't add a grab if a drag is in progress, or if the widget is a drag
+ // feedback popup. (panels with type="drag").
+ return;
+ }
+
+ if (!mHasMappedToplevel) {
+ // On X, capturing an unmapped window is pointless (returns
+ // GDK_GRAB_NOT_VIEWABLE). Avoid the X server round-trip and just retry
+ // when we're mapped.
+ mNeedsToRetryCapturingMouse = true;
+ return;
+ }
+
+ // gdk_pointer_grab is deprecated in favor of gdk_device_grab, but that
+ // causes a strange bug on X11, most obviously with nested popup menus:
+ // we somehow take the pointer position relative to the top left of the
+ // outer menu and use it as if it were relative to the submenu. This
+ // doesn't happen with gdk_pointer_grab even though the code is very
+ // similar. See the video attached to bug 1750721 for a demonstration,
+ // and see also bug 1820542 for when the same thing happened with
+ // another attempt to use gdk_device_grab.
+ //
+ // (gdk_device_grab is deprecated in favor of gdk_seat_grab as of 3.20,
+ // but at the time of this writing we still support older versions of
+ // GTK 3.)
+ GdkGrabStatus status =
+ gdk_pointer_grab(GetToplevelGdkWindow(),
+ /* owner_events = */ true, kCaptureEventsMask,
+ /* confine_to = */ nullptr,
+ /* cursor = */ nullptr, GetLastUserInputTime());
+ Unused << NS_WARN_IF(status != GDK_GRAB_SUCCESS);
+ LOG(" > pointer grab with status %d", int(status));
+ gtk_grab_add(GTK_WIDGET(mContainer));
+ } else {
+ // There may not have been a drag in process when aDoCapture was set,
+ // so make sure to remove any added grab. This is a no-op if the grab
+ // was not added to this widget.
+ gtk_grab_remove(GTK_WIDGET(mContainer));
+ gdk_pointer_ungrab(GetLastUserInputTime());
+ }
+}
+
+nsresult nsWindow::GetAttention(int32_t aCycleCount) {
+ LOG("nsWindow::GetAttention");
+
+ GtkWidget* top_window = GetToplevelWidget();
+ GtkWidget* top_focused_window =
+ gFocusWindow ? gFocusWindow->GetToplevelWidget() : nullptr;
+
+ // Don't get attention if the window is focused anyway.
+ if (top_window && (gtk_widget_get_visible(top_window)) &&
+ top_window != top_focused_window) {
+ SetUrgencyHint(top_window, true);
+ }
+
+ return NS_OK;
+}
+
+bool nsWindow::HasPendingInputEvent() {
+ // This sucks, but gtk/gdk has no way to answer the question we want while
+ // excluding paint events, and there's no X API that will let us peek
+ // without blocking or removing. To prevent event reordering, peek
+ // anything except expose events. Reordering expose and others should be
+ // ok, hopefully.
+ bool haveEvent = false;
+#ifdef MOZ_X11
+ XEvent ev;
+ if (GdkIsX11Display()) {
+ Display* display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
+ haveEvent = XCheckMaskEvent(
+ display,
+ KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask |
+ EnterWindowMask | LeaveWindowMask | PointerMotionMask |
+ PointerMotionHintMask | Button1MotionMask | Button2MotionMask |
+ Button3MotionMask | Button4MotionMask | Button5MotionMask |
+ ButtonMotionMask | KeymapStateMask | VisibilityChangeMask |
+ StructureNotifyMask | ResizeRedirectMask | SubstructureNotifyMask |
+ SubstructureRedirectMask | FocusChangeMask | PropertyChangeMask |
+ ColormapChangeMask | OwnerGrabButtonMask,
+ &ev);
+ if (haveEvent) {
+ XPutBackEvent(display, &ev);
+ }
+ }
+#endif
+ return haveEvent;
+}
+
+#ifdef cairo_copy_clip_rectangle_list
+# error "Looks like we're including Mozilla's cairo instead of system cairo"
+#endif
+static bool ExtractExposeRegion(LayoutDeviceIntRegion& aRegion, cairo_t* cr) {
+ cairo_rectangle_list_t* rects = cairo_copy_clip_rectangle_list(cr);
+ if (rects->status != CAIRO_STATUS_SUCCESS) {
+ NS_WARNING("Failed to obtain cairo rectangle list.");
+ return false;
+ }
+
+ for (int i = 0; i < rects->num_rectangles; i++) {
+ const cairo_rectangle_t& r = rects->rectangles[i];
+ aRegion.Or(aRegion,
+ LayoutDeviceIntRect::Truncate((float)r.x, (float)r.y,
+ (float)r.width, (float)r.height));
+ }
+
+ cairo_rectangle_list_destroy(rects);
+ return true;
+}
+
+#ifdef MOZ_WAYLAND
+void nsWindow::CreateCompositorVsyncDispatcher() {
+ LOG_VSYNC("nsWindow::CreateCompositorVsyncDispatcher()");
+ if (!mWaylandVsyncSource) {
+ LOG_VSYNC(
+ " mWaylandVsyncSource is missing, create "
+ "nsBaseWidget::CompositorVsyncDispatcher()");
+ nsBaseWidget::CreateCompositorVsyncDispatcher();
+ return;
+ }
+ if (!mCompositorVsyncDispatcherLock) {
+ mCompositorVsyncDispatcherLock =
+ MakeUnique<Mutex>("mCompositorVsyncDispatcherLock");
+ }
+ MutexAutoLock lock(*mCompositorVsyncDispatcherLock);
+ if (!mCompositorVsyncDispatcher) {
+ LOG_VSYNC(" create CompositorVsyncDispatcher()");
+ mCompositorVsyncDispatcher =
+ new CompositorVsyncDispatcher(mWaylandVsyncDispatcher);
+ }
+}
+#endif
+
+void nsWindow::RequestRepaint(LayoutDeviceIntRegion& aRepaintRegion) {
+ WindowRenderer* renderer = GetWindowRenderer();
+ WebRenderLayerManager* layerManager = renderer->AsWebRender();
+ KnowsCompositor* knowsCompositor = renderer->AsKnowsCompositor();
+
+ if (knowsCompositor && layerManager && mCompositorSession) {
+ if (!mConfiguredClearColor && !IsPopup()) {
+ layerManager->WrBridge()->SendSetDefaultClearColor(LookAndFeel::Color(
+ LookAndFeel::ColorID::Window, PreferenceSheet::ColorSchemeForChrome(),
+ LookAndFeel::UseStandins::No));
+ mConfiguredClearColor = true;
+ }
+
+ // We need to paint to the screen even if nothing changed, since if we
+ // don't have a compositing window manager, our pixels could be stale.
+ layerManager->SetNeedsComposite(true);
+ layerManager->SendInvalidRegion(aRepaintRegion.ToUnknownRegion());
+ }
+}
+
+gboolean nsWindow::OnExposeEvent(cairo_t* cr) {
+ // This might destroy us.
+ NotifyOcclusionState(OcclusionState::VISIBLE);
+ if (mIsDestroyed) {
+ return FALSE;
+ }
+
+ // Send any pending resize events so that layout can update.
+ // May run event loop and destroy us.
+ MaybeDispatchResized();
+ if (mIsDestroyed) {
+ return FALSE;
+ }
+
+ // Windows that are not visible will be painted after they become visible.
+ if (!mGdkWindow || !mHasMappedToplevel) {
+ return FALSE;
+ }
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay() && !moz_container_wayland_can_draw(mContainer)) {
+ return FALSE;
+ }
+#endif
+
+ if (!GetListener()) {
+ return FALSE;
+ }
+
+ LOG("nsWindow::OnExposeEvent GdkWindow [%p] XID [0x%lx]", mGdkWindow,
+ GetX11Window());
+
+ LayoutDeviceIntRegion exposeRegion;
+ if (!ExtractExposeRegion(exposeRegion, cr)) {
+ LOG(" no rects, quit");
+ return FALSE;
+ }
+
+ gint scale = GdkCeiledScaleFactor();
+ LayoutDeviceIntRegion region = exposeRegion;
+ region.ScaleRoundOut(scale, scale);
+
+ RequestRepaint(region);
+
+ RefPtr<nsWindow> strongThis(this);
+
+ // Dispatch WillPaintWindow notification to allow scripts etc. to run
+ // before we paint. It also spins event loop which may show/hide the window
+ // so we may have new renderer etc.
+ GetListener()->WillPaintWindow(this);
+
+ // If the window has been destroyed during the will paint notification,
+ // there is nothing left to do.
+ if (!mGdkWindow || mIsDestroyed) {
+ return TRUE;
+ }
+
+ // Re-get all rendering components since the will paint notification
+ // might have killed it.
+ nsIWidgetListener* listener = GetListener();
+ if (!listener) return FALSE;
+
+ WindowRenderer* renderer = GetWindowRenderer();
+ WebRenderLayerManager* layerManager = renderer->AsWebRender();
+ KnowsCompositor* knowsCompositor = renderer->AsKnowsCompositor();
+
+ if (knowsCompositor && layerManager && layerManager->NeedsComposite()) {
+ layerManager->ScheduleComposite(wr::RenderReasons::WIDGET);
+ layerManager->SetNeedsComposite(false);
+ }
+
+ // Our bounds may have changed after calling WillPaintWindow. Clip
+ // to the new bounds here. The region is relative to this
+ // window.
+ region.And(region, LayoutDeviceIntRect(0, 0, mBounds.width, mBounds.height));
+
+ bool shaped = false;
+ if (TransparencyMode::Transparent == GetTransparencyMode()) {
+ auto* window = static_cast<nsWindow*>(GetTopLevelWidget());
+ if (mTransparencyBitmapForTitlebar) {
+ if (mSizeMode == nsSizeMode_Normal) {
+ window->UpdateTitlebarTransparencyBitmap();
+ } else {
+ window->ClearTransparencyBitmap();
+ }
+ } else {
+ if (mHasAlphaVisual) {
+ // Remove possible shape mask from when window manger was not
+ // previously compositing.
+ window->ClearTransparencyBitmap();
+ } else {
+ shaped = true;
+ }
+ }
+ }
+
+ if (region.IsEmpty()) {
+ return TRUE;
+ }
+
+ // If this widget uses OMTC...
+ if (renderer->GetBackendType() == LayersBackend::LAYERS_WR) {
+ listener->PaintWindow(this, region);
+
+ // Re-get the listener since the will paint notification might have
+ // killed it.
+ listener = GetListener();
+ if (!listener) {
+ return TRUE;
+ }
+
+ listener->DidPaintWindow();
+ return TRUE;
+ }
+
+ BufferMode layerBuffering = BufferMode::BUFFERED;
+ RefPtr<DrawTarget> dt = StartRemoteDrawingInRegion(region, &layerBuffering);
+ if (!dt || !dt->IsValid()) {
+ return FALSE;
+ }
+ Maybe<gfxContext> ctx;
+ IntRect boundsRect = region.GetBounds().ToUnknownRect();
+ IntPoint offset(0, 0);
+ if (dt->GetSize() == boundsRect.Size()) {
+ offset = boundsRect.TopLeft();
+ dt->SetTransform(Matrix::Translation(-offset));
+ }
+
+#ifdef MOZ_X11
+ if (shaped) {
+ // Collapse update area to the bounding box. This is so we only have to
+ // call UpdateTranslucentWindowAlpha once. After we have dropped
+ // support for non-Thebes graphics, UpdateTranslucentWindowAlpha will be
+ // our private interface so we can rework things to avoid this.
+ dt->PushClipRect(Rect(boundsRect));
+
+ // The double buffering is done here to extract the shape mask.
+ // (The shape mask won't be necessary when a visual with an alpha
+ // channel is used on compositing window managers.)
+ layerBuffering = BufferMode::BUFFER_NONE;
+ RefPtr<DrawTarget> destDT =
+ dt->CreateSimilarDrawTarget(boundsRect.Size(), SurfaceFormat::B8G8R8A8);
+ if (!destDT || !destDT->IsValid()) {
+ return FALSE;
+ }
+ destDT->SetTransform(Matrix::Translation(-boundsRect.TopLeft()));
+ ctx.emplace(destDT, /* aPreserveTransform */ true);
+ } else {
+ gfxUtils::ClipToRegion(dt, region.ToUnknownRegion());
+ ctx.emplace(dt, /* aPreserveTransform */ true);
+ }
+
+# if 0
+ // NOTE: Paint flashing region would be wrong for cairo, since
+ // cairo inflates the update region, etc. So don't paint flash
+ // for cairo.
+# ifdef DEBUG
+ // XXX aEvent->region may refer to a newly-invalid area. FIXME
+ if (0 && WANT_PAINT_FLASHING && gtk_widget_get_window(aEvent))
+ gdk_window_flash(mGdkWindow, 1, 100, aEvent->region);
+# endif
+# endif
+
+#endif // MOZ_X11
+
+ bool painted = false;
+ {
+ if (renderer->GetBackendType() == LayersBackend::LAYERS_NONE) {
+ if (GetTransparencyMode() == TransparencyMode::Transparent &&
+ layerBuffering == BufferMode::BUFFER_NONE && mHasAlphaVisual) {
+ // If our draw target is unbuffered and we use an alpha channel,
+ // clear the image beforehand to ensure we don't get artifacts from a
+ // reused SHM image. See bug 1258086.
+ dt->ClearRect(Rect(boundsRect));
+ }
+ AutoLayerManagerSetup setupLayerManager(
+ this, ctx.isNothing() ? nullptr : &ctx.ref(), layerBuffering);
+ painted = listener->PaintWindow(this, region);
+
+ // Re-get the listener since the will paint notification might have
+ // killed it.
+ listener = GetListener();
+ if (!listener) {
+ return TRUE;
+ }
+ }
+ }
+
+#ifdef MOZ_X11
+ // PaintWindow can Destroy us (bug 378273), avoid doing any paint
+ // operations below if that happened - it will lead to XError and exit().
+ if (shaped) {
+ if (MOZ_LIKELY(!mIsDestroyed)) {
+ if (painted) {
+ RefPtr<SourceSurface> surf = ctx->GetDrawTarget()->Snapshot();
+
+ UpdateAlpha(surf, boundsRect);
+
+ dt->DrawSurface(surf, Rect(boundsRect),
+ Rect(0, 0, boundsRect.width, boundsRect.height),
+ DrawSurfaceOptions(SamplingFilter::POINT),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+ }
+ }
+ }
+
+ ctx.reset();
+ dt->PopClip();
+
+#endif // MOZ_X11
+
+ EndRemoteDrawingInRegion(dt, region);
+
+ listener->DidPaintWindow();
+
+ // Synchronously flush any new dirty areas
+ cairo_region_t* dirtyArea = gdk_window_get_update_area(mGdkWindow);
+
+ if (dirtyArea) {
+ gdk_window_invalidate_region(mGdkWindow, dirtyArea, false);
+ cairo_region_destroy(dirtyArea);
+ gdk_window_process_updates(mGdkWindow, false);
+ }
+
+ // check the return value!
+ return TRUE;
+}
+
+void nsWindow::UpdateAlpha(SourceSurface* aSourceSurface,
+ nsIntRect aBoundsRect) {
+ // We need to create our own buffer to force the stride to match the
+ // expected stride.
+ int32_t stride =
+ GetAlignedStride<4>(aBoundsRect.width, BytesPerPixel(SurfaceFormat::A8));
+ if (stride == 0) {
+ return;
+ }
+ int32_t bufferSize = stride * aBoundsRect.height;
+ auto imageBuffer = MakeUniqueFallible<uint8_t[]>(bufferSize);
+ {
+ RefPtr<DrawTarget> drawTarget = gfxPlatform::CreateDrawTargetForData(
+ imageBuffer.get(), aBoundsRect.Size(), stride, SurfaceFormat::A8);
+
+ if (drawTarget) {
+ drawTarget->DrawSurface(aSourceSurface,
+ Rect(0, 0, aBoundsRect.width, aBoundsRect.height),
+ Rect(0, 0, aSourceSurface->GetSize().width,
+ aSourceSurface->GetSize().height),
+ DrawSurfaceOptions(SamplingFilter::POINT),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+ }
+ }
+ UpdateTranslucentWindowAlphaInternal(aBoundsRect, imageBuffer.get(), stride);
+}
+
+gboolean nsWindow::OnConfigureEvent(GtkWidget* aWidget,
+ GdkEventConfigure* aEvent) {
+ // These events are only received on toplevel windows.
+ //
+ // GDK ensures that the coordinates are the client window top-left wrt the
+ // root window.
+ //
+ // GDK calculates the cordinates for real ConfigureNotify events on
+ // managed windows (that would normally be relative to the parent
+ // window).
+ //
+ // Synthetic ConfigureNotify events are from the window manager and
+ // already relative to the root window. GDK creates all X windows with
+ // border_width = 0, so synthetic events also indicate the top-left of
+ // the client window.
+ //
+ // Override-redirect windows are children of the root window so parent
+ // coordinates are root coordinates.
+
+#ifdef MOZ_LOGGING
+ int scale = mGdkWindow ? gdk_window_get_scale_factor(mGdkWindow) : -1;
+ LOG("configure event %d,%d -> %d x %d direct mGdkWindow scale %d (scaled "
+ "size %d x %d)\n",
+ aEvent->x, aEvent->y, aEvent->width, aEvent->height, scale,
+ aEvent->width * scale, aEvent->height * scale);
+#endif
+
+ if (mPendingConfigures > 0) {
+ mPendingConfigures--;
+ }
+
+ // Don't fire configure event for scale changes, we handle that
+ // OnScaleChanged event. Skip that for toplevel windows only.
+ if (mGdkWindow && IsTopLevelWindowType()) {
+ if (mCeiledScaleFactor != gdk_window_get_scale_factor(mGdkWindow)) {
+ LOG(" scale factor changed to %d,return early",
+ gdk_window_get_scale_factor(mGdkWindow));
+ return FALSE;
+ }
+ }
+
+ LayoutDeviceIntRect screenBounds = GetScreenBounds();
+
+ if (IsTopLevelWindowType()) {
+ // This check avoids unwanted rollup on spurious configure events from
+ // Cygwin/X (bug 672103).
+ if (mBounds.x != screenBounds.x || mBounds.y != screenBounds.y) {
+ RollupAllMenus();
+ }
+ }
+
+ NS_ASSERTION(GTK_IS_WINDOW(aWidget),
+ "Configure event on widget that is not a GtkWindow");
+ if (mGdkWindow &&
+ gtk_window_get_window_type(GTK_WINDOW(aWidget)) == GTK_WINDOW_POPUP) {
+ // Override-redirect window
+ //
+ // These windows should not be moved by the window manager, and so any
+ // change in position is a result of our direction. mBounds has
+ // already been set in std::move() or Resize(), and that is more
+ // up-to-date than the position in the ConfigureNotify event if the
+ // event is from an earlier window move.
+ //
+ // Skipping the WindowMoved call saves context menus from an infinite
+ // loop when nsXULPopupManager::PopupMoved moves the window to the new
+ // position and nsMenuPopupFrame::SetPopupPosition adds
+ // offsetForContextMenu on each iteration.
+
+ // Our back buffer might have been invalidated while we drew the last
+ // frame, and its contents might be incorrect. See bug 1280653 comment 7
+ // and comment 10. Specifically we must ensure we recomposite the frame
+ // as soon as possible to avoid the corrupted frame being displayed.
+ GetWindowRenderer()->FlushRendering(wr::RenderReasons::WIDGET);
+ return FALSE;
+ }
+
+ mBounds.MoveTo(screenBounds.TopLeft());
+ RecomputeClientOffset(/* aNotify = */ false);
+
+ // XXX mozilla will invalidate the entire window after this move
+ // complete. wtf?
+ NotifyWindowMoved(mBounds.x, mBounds.y);
+
+ return FALSE;
+}
+
+void nsWindow::OnMap() {
+ LOG("nsWindow::OnMap");
+ // Gtk mapped widget to screen. Configure underlying GdkWindow properly
+ // as our rendering target.
+ // This call means we have X11 (or Wayland) window we can render to by GL
+ // so we need to notify compositor about it.
+ mIsMapped = true;
+ ConfigureGdkWindow();
+}
+
+void nsWindow::OnUnmap() {
+ LOG("nsWindow::OnUnmap");
+
+ mIsMapped = false;
+
+ if (mSourceDragContext) {
+ static auto sGtkDragCancel =
+ (void (*)(GdkDragContext*))dlsym(RTLD_DEFAULT, "gtk_drag_cancel");
+ if (sGtkDragCancel) {
+ sGtkDragCancel(mSourceDragContext);
+ mSourceDragContext = nullptr;
+ }
+ }
+}
+
+void nsWindow::OnSizeAllocate(GtkAllocation* aAllocation) {
+ LOG("nsWindow::OnSizeAllocate %d,%d -> %d x %d\n", aAllocation->x,
+ aAllocation->y, aAllocation->width, aAllocation->height);
+
+ // Client offset are updated by _NET_FRAME_EXTENTS on X11 when system titlebar
+ // is enabled. In either cases (Wayland or system titlebar is off on X11)
+ // we don't get _NET_FRAME_EXTENTS X11 property notification so we derive
+ // it from mContainer position.
+ RecomputeClientOffset(/* aNotify = */ true);
+
+ mHasReceivedSizeAllocate = true;
+
+ LayoutDeviceIntSize size = GdkRectToDevicePixels(*aAllocation).Size();
+
+ // Sometimes the window manager gives us garbage sizes (way past the maximum
+ // texture size) causing crashes if we don't enforce size constraints again
+ // here.
+ ConstrainSize(&size.width, &size.height);
+
+ if (mBounds.Size() == size) {
+ LOG(" Already the same size");
+ // mBounds was set at Create() or Resize().
+ if (mNeedsDispatchSize != LayoutDeviceIntSize(-1, -1)) {
+ LOG(" No longer needs to dispatch %dx%d", mNeedsDispatchSize.width,
+ mNeedsDispatchSize.height);
+ mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1);
+ }
+ return;
+ }
+
+ // Invalidate the new part of the window now for the pending paint to
+ // minimize background flashes (GDK does not do this for external resizes
+ // of toplevels.)
+ if (mGdkWindow) {
+ if (mBounds.width < size.width) {
+ GdkRectangle rect = DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect(
+ mBounds.width, 0, size.width - mBounds.width, size.height));
+ gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE);
+ }
+ if (mBounds.height < size.height) {
+ GdkRectangle rect = DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect(
+ 0, mBounds.height, size.width, size.height - mBounds.height));
+ gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE);
+ }
+ }
+
+ // If we update mBounds here, then inner/outerHeight are out of sync until
+ // we call WindowResized.
+ mNeedsDispatchSize = size;
+
+ // Gecko permits running nested event loops during processing of events,
+ // GtkWindow callers of gtk_widget_size_allocate expect the signal
+ // handlers to return sometime in the near future.
+ NS_DispatchToCurrentThread(NewRunnableMethod(
+ "nsWindow::MaybeDispatchResized", this, &nsWindow::MaybeDispatchResized));
+}
+
+void nsWindow::OnDeleteEvent() {
+ if (mWidgetListener) mWidgetListener->RequestWindowClose(this);
+}
+
+void nsWindow::OnEnterNotifyEvent(GdkEventCrossing* aEvent) {
+ LOG("enter notify (win=%p, sub=%p): %f, %f mode %d, detail %d\n",
+ aEvent->window, aEvent->subwindow, aEvent->x, aEvent->y, aEvent->mode,
+ aEvent->detail);
+ // This skips NotifyVirtual and NotifyNonlinearVirtual enter notify events
+ // when the pointer enters a child window. If the destination window is a
+ // Gecko window then we'll catch the corresponding event on that window,
+ // but we won't notice when the pointer directly enters a foreign (plugin)
+ // child window without passing over a visible portion of a Gecko window.
+ if (aEvent->subwindow) {
+ return;
+ }
+
+ // Check before checking for ungrab as the button state may have
+ // changed while a non-Gecko ancestor window had a pointer grab.
+ DispatchMissedButtonReleases(aEvent);
+
+ WidgetMouseEvent event(true, eMouseEnterIntoWidget, this,
+ WidgetMouseEvent::eReal);
+
+ event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y);
+ event.AssignEventTime(GetWidgetEventTime(aEvent->time));
+
+ LOG("OnEnterNotify");
+
+ DispatchInputEvent(&event);
+}
+
+// Some window managers send a bogus top-level leave-notify event on every
+// click. That confuses our event handling code in ways that can break websites,
+// see bug 1805939 for details.
+//
+// Make sure to only check this on bogus environments, since for environments
+// with CSD, gdk_device_get_window_at_position could return the window even when
+// the pointer is in the decoration area.
+static bool IsBogusLeaveNotifyEvent(GdkWindow* aWindow,
+ GdkEventCrossing* aEvent) {
+ static bool sBogusWm = [] {
+ if (GdkIsWaylandDisplay()) {
+ return false;
+ }
+ const auto& desktopEnv = GetDesktopEnvironmentIdentifier();
+ return desktopEnv.EqualsLiteral("fluxbox") || // Bug 1805939 comment 0.
+ desktopEnv.EqualsLiteral("blackbox") || // Bug 1805939 comment 32.
+ desktopEnv.EqualsLiteral("lg3d") || // Bug 1820405.
+ desktopEnv.EqualsLiteral("pekwm") || // Bug 1822911.
+ StringBeginsWith(desktopEnv, "fvwm"_ns);
+ }();
+
+ const bool shouldCheck = [] {
+ switch (StaticPrefs::widget_gtk_ignore_bogus_leave_notify()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return sBogusWm;
+ }
+ }();
+
+ if (!shouldCheck || !aWindow) {
+ return false;
+ }
+ GdkDevice* pointer = GdkGetPointer();
+ GdkWindow* winAtPt =
+ gdk_device_get_window_at_position(pointer, nullptr, nullptr);
+ if (!winAtPt) {
+ return false;
+ }
+ // We're still in the same top level window, ignore this leave notify event.
+ GdkWindow* topLevelAtPt = gdk_window_get_toplevel(winAtPt);
+ GdkWindow* topLevelWidget = gdk_window_get_toplevel(aWindow);
+ return topLevelAtPt == topLevelWidget;
+}
+
+void nsWindow::OnLeaveNotifyEvent(GdkEventCrossing* aEvent) {
+ LOG("leave notify (win=%p, sub=%p): %f, %f mode %d, detail %d\n",
+ aEvent->window, aEvent->subwindow, aEvent->x, aEvent->y, aEvent->mode,
+ aEvent->detail);
+
+ // This ignores NotifyVirtual and NotifyNonlinearVirtual leave notify
+ // events when the pointer leaves a child window. If the destination
+ // window is a Gecko window then we'll catch the corresponding event on
+ // that window.
+ //
+ // XXXkt However, we will miss toplevel exits when the pointer directly
+ // leaves a foreign (plugin) child window without passing over a visible
+ // portion of a Gecko window.
+ if (aEvent->subwindow) {
+ return;
+ }
+
+ // The filter out for subwindows should make sure that this is targeted to
+ // this nsWindow.
+ const bool leavingTopLevel = IsTopLevelWindowType();
+ if (leavingTopLevel && IsBogusLeaveNotifyEvent(mGdkWindow, aEvent)) {
+ return;
+ }
+
+ WidgetMouseEvent event(true, eMouseExitFromWidget, this,
+ WidgetMouseEvent::eReal);
+
+ event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y);
+ event.AssignEventTime(GetWidgetEventTime(aEvent->time));
+ event.mExitFrom = Some(leavingTopLevel ? WidgetMouseEvent::ePlatformTopLevel
+ : WidgetMouseEvent::ePlatformChild);
+
+ LOG("OnLeaveNotify");
+
+ DispatchInputEvent(&event);
+}
+
+Maybe<GdkWindowEdge> nsWindow::CheckResizerEdge(
+ const LayoutDeviceIntPoint& aPoint) {
+ const bool canResize = [&] {
+ // Don't allow resizing maximized/fullscreen windows.
+ if (mSizeMode != nsSizeMode_Normal) {
+ return false;
+ }
+ if (mIsPIPWindow) {
+ // Note that since we do show resizers on left/right sides on PIP windows,
+ // we still want the resizers there, even when tiled.
+ return true;
+ }
+ if (!mDrawInTitlebar) {
+ return false;
+ }
+ // On KDE, allow for 1 extra pixel at the top of regular windows when
+ // drawing to the titlebar. This matches the native titlebar behavior on
+ // that environment. See bug 1813554.
+ //
+ // Don't do that on GNOME (see bug 1822764). If we wanted to do this on
+ // GNOME we'd need an extra check for mIsTiled, since the window is "stuck"
+ // to the top and bottom.
+ //
+ // Other DEs are untested.
+ return mDrawInTitlebar && IsKdeDesktopEnvironment();
+ }();
+
+ if (!canResize) {
+ return Nothing();
+ }
+
+ // If we're not in a PiP window, allow 1px resizer edge from the top edge,
+ // and nothing else.
+ // This is to allow resizes of tiled windows on KDE, see bug 1813554.
+ const int resizerHeight = (mIsPIPWindow ? 15 : 1) * GdkCeiledScaleFactor();
+ const int resizerWidth = resizerHeight * 4;
+
+ const int topDist = aPoint.y;
+ const int leftDist = aPoint.x;
+ const int rightDist = mBounds.width - aPoint.x;
+ const int bottomDist = mBounds.height - aPoint.y;
+
+ // We can't emulate resize of North/West edges on Wayland as we can't shift
+ // toplevel window.
+ bool waylandLimitedResize = mAspectRatio != 0.0f && GdkIsWaylandDisplay();
+
+ if (topDist <= resizerHeight) {
+ if (rightDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_NORTH_EAST);
+ }
+ if (leftDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_NORTH_WEST);
+ }
+ return waylandLimitedResize ? Nothing() : Some(GDK_WINDOW_EDGE_NORTH);
+ }
+
+ if (!mIsPIPWindow) {
+ return Nothing();
+ }
+
+ if (bottomDist <= resizerHeight) {
+ if (leftDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_SOUTH_WEST);
+ }
+ if (rightDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_SOUTH_EAST);
+ }
+ return Some(GDK_WINDOW_EDGE_SOUTH);
+ }
+
+ if (leftDist <= resizerHeight) {
+ if (topDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_NORTH_WEST);
+ }
+ if (bottomDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_SOUTH_WEST);
+ }
+ return waylandLimitedResize ? Nothing() : Some(GDK_WINDOW_EDGE_WEST);
+ }
+
+ if (rightDist <= resizerHeight) {
+ if (topDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_NORTH_EAST);
+ }
+ if (bottomDist <= resizerWidth) {
+ return Some(GDK_WINDOW_EDGE_SOUTH_EAST);
+ }
+ return Some(GDK_WINDOW_EDGE_EAST);
+ }
+ return Nothing();
+}
+
+template <typename Event>
+static LayoutDeviceIntPoint GetRefPoint(nsWindow* aWindow, Event* aEvent) {
+ if (aEvent->window == aWindow->GetGdkWindow()) {
+ // we are the window that the event happened on so no need for expensive
+ // WidgetToScreenOffset
+ return aWindow->GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y);
+ }
+ // XXX we're never quite sure which GdkWindow the event came from due to our
+ // custom bubbling in scroll_event_cb(), so use ScreenToWidget to translate
+ // the screen root coordinates into coordinates relative to this widget.
+ return aWindow->GdkEventCoordsToDevicePixels(aEvent->x_root, aEvent->y_root) -
+ aWindow->WidgetToScreenOffset();
+}
+
+void nsWindow::EmulateResizeDrag(GdkEventMotion* aEvent) {
+ auto newPoint = LayoutDeviceIntPoint::Floor(aEvent->x, aEvent->y);
+ LayoutDeviceIntPoint diff = newPoint - mLastResizePoint;
+ mLastResizePoint = newPoint;
+
+ GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size());
+ LayoutDeviceIntSize newSize(size.width + diff.x, size.height + diff.y);
+
+ if (mAspectResizer.value() == GTK_ORIENTATION_VERTICAL) {
+ newSize.width = int(newSize.height * mAspectRatio);
+ } else { // GTK_ORIENTATION_HORIZONTAL
+ newSize.height = int(newSize.width / mAspectRatio);
+ }
+ LOG(" aspect ratio correction %d x %d aspect %f\n", newSize.width,
+ newSize.height, mAspectRatio);
+ gtk_window_resize(GTK_WINDOW(mShell), newSize.width, newSize.height);
+}
+
+void nsWindow::OnMotionNotifyEvent(GdkEventMotion* aEvent) {
+ if (!mGdkWindow) {
+ return;
+ }
+
+ // Emulate gdk_window_begin_resize_drag() for windows
+ // with fixed aspect ratio on Wayland.
+ if (mAspectResizer && mAspectRatio != 0.0f) {
+ EmulateResizeDrag(aEvent);
+ return;
+ }
+
+ if (mWindowShouldStartDragging) {
+ mWindowShouldStartDragging = false;
+ GdkWindow* dragWindow = nullptr;
+
+ // find the top-level window
+ if (mGdkWindow) {
+ dragWindow = gdk_window_get_toplevel(mGdkWindow);
+ MOZ_ASSERT(dragWindow, "gdk_window_get_toplevel should not return null");
+ }
+
+#ifdef MOZ_X11
+ if (dragWindow && GdkIsX11Display()) {
+ // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=789054
+ // To avoid crashes disable double-click on WM without _NET_WM_MOVERESIZE.
+ // See _should_perform_ewmh_drag() at gdkwindow-x11.c
+ GdkScreen* screen = gdk_window_get_screen(dragWindow);
+ GdkAtom atom = gdk_atom_intern("_NET_WM_MOVERESIZE", FALSE);
+ if (!gdk_x11_screen_supports_net_wm_hint(screen, atom)) {
+ dragWindow = nullptr;
+ }
+ }
+#endif
+
+ if (dragWindow) {
+ gdk_window_begin_move_drag(dragWindow, 1, aEvent->x_root, aEvent->y_root,
+ aEvent->time);
+ return;
+ }
+ }
+
+ mWidgetCursorLocked = false;
+ const auto refPoint = GetRefPoint(this, aEvent);
+ if (auto edge = CheckResizerEdge(refPoint)) {
+ nsCursor cursor = eCursor_none;
+ switch (*edge) {
+ case GDK_WINDOW_EDGE_SOUTH:
+ case GDK_WINDOW_EDGE_NORTH:
+ cursor = eCursor_ns_resize;
+ break;
+ case GDK_WINDOW_EDGE_WEST:
+ case GDK_WINDOW_EDGE_EAST:
+ cursor = eCursor_ew_resize;
+ break;
+ case GDK_WINDOW_EDGE_NORTH_WEST:
+ case GDK_WINDOW_EDGE_SOUTH_EAST:
+ cursor = eCursor_nwse_resize;
+ break;
+ case GDK_WINDOW_EDGE_NORTH_EAST:
+ case GDK_WINDOW_EDGE_SOUTH_WEST:
+ cursor = eCursor_nesw_resize;
+ break;
+ }
+ SetCursor(Cursor{cursor});
+ // If we set resize cursor on widget level keep it locked and prevent layout
+ // to switch it back to default (by synthetic mouse events for instance)
+ // until resize is finished. This affects PIP windows only.
+ if (mIsPIPWindow) {
+ mWidgetCursorLocked = true;
+ }
+ return;
+ }
+
+ WidgetMouseEvent event(true, eMouseMove, this, WidgetMouseEvent::eReal);
+
+ gdouble pressure = 0;
+ gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure);
+ // Sometime gdk generate 0 pressure value between normal values
+ // We have to ignore that and use last valid value
+ if (pressure) {
+ mLastMotionPressure = pressure;
+ }
+ event.mPressure = mLastMotionPressure;
+ event.mRefPoint = refPoint;
+ event.AssignEventTime(GetWidgetEventTime(aEvent->time));
+
+ KeymapWrapper::InitInputEvent(event, aEvent->state);
+
+ DispatchInputEvent(&event);
+}
+
+// If the automatic pointer grab on ButtonPress has deactivated before
+// ButtonRelease, and the mouse button is released while the pointer is not
+// over any a Gecko window, then the ButtonRelease event will not be received.
+// (A similar situation exists when the pointer is grabbed with owner_events
+// True as the ButtonRelease may be received on a foreign [plugin] window).
+// Use this method to check for released buttons when the pointer returns to a
+// Gecko window.
+void nsWindow::DispatchMissedButtonReleases(GdkEventCrossing* aGdkEvent) {
+ guint changed = aGdkEvent->state ^ gButtonState;
+ // Only consider button releases.
+ // (Ignore button presses that occurred outside Gecko.)
+ guint released = changed & gButtonState;
+ gButtonState = aGdkEvent->state;
+
+ // Loop over each button, excluding mouse wheel buttons 4 and 5 for which
+ // GDK ignores releases.
+ for (guint buttonMask = GDK_BUTTON1_MASK; buttonMask <= GDK_BUTTON3_MASK;
+ buttonMask <<= 1) {
+ if (released & buttonMask) {
+ int16_t buttonType;
+ switch (buttonMask) {
+ case GDK_BUTTON1_MASK:
+ buttonType = MouseButton::ePrimary;
+ break;
+ case GDK_BUTTON2_MASK:
+ buttonType = MouseButton::eMiddle;
+ break;
+ default:
+ NS_ASSERTION(buttonMask == GDK_BUTTON3_MASK,
+ "Unexpected button mask");
+ buttonType = MouseButton::eSecondary;
+ }
+
+ LOG("Synthesized button %u release", guint(buttonType + 1));
+
+ // Dispatch a synthesized button up event to tell Gecko about the
+ // change in state. This event is marked as synthesized so that
+ // it is not dispatched as a DOM event, because we don't know the
+ // position, widget, modifiers, or time/order.
+ WidgetMouseEvent synthEvent(true, eMouseUp, this,
+ WidgetMouseEvent::eSynthesized);
+ synthEvent.mButton = buttonType;
+ DispatchInputEvent(&synthEvent);
+ }
+ }
+}
+
+void nsWindow::InitButtonEvent(WidgetMouseEvent& aEvent,
+ GdkEventButton* aGdkEvent,
+ const LayoutDeviceIntPoint& aRefPoint) {
+ aEvent.mRefPoint = aRefPoint;
+
+ guint modifierState = aGdkEvent->state;
+ // aEvent's state includes the button state from immediately before this
+ // event. If aEvent is a mousedown or mouseup event, we need to update
+ // the button state.
+ guint buttonMask = 0;
+ switch (aGdkEvent->button) {
+ case 1:
+ buttonMask = GDK_BUTTON1_MASK;
+ break;
+ case 2:
+ buttonMask = GDK_BUTTON2_MASK;
+ break;
+ case 3:
+ buttonMask = GDK_BUTTON3_MASK;
+ break;
+ }
+ if (aGdkEvent->type == GDK_BUTTON_RELEASE) {
+ modifierState &= ~buttonMask;
+ } else {
+ modifierState |= buttonMask;
+ }
+
+ KeymapWrapper::InitInputEvent(aEvent, modifierState);
+
+ aEvent.AssignEventTime(GetWidgetEventTime(aGdkEvent->time));
+
+ switch (aGdkEvent->type) {
+ case GDK_2BUTTON_PRESS:
+ aEvent.mClickCount = 2;
+ break;
+ case GDK_3BUTTON_PRESS:
+ aEvent.mClickCount = 3;
+ break;
+ // default is one click
+ default:
+ aEvent.mClickCount = 1;
+ }
+}
+
+static guint ButtonMaskFromGDKButton(guint button) {
+ return GDK_BUTTON1_MASK << (button - 1);
+}
+
+void nsWindow::DispatchContextMenuEventFromMouseEvent(
+ uint16_t domButton, GdkEventButton* aEvent,
+ const LayoutDeviceIntPoint& aRefPoint) {
+ if (domButton == MouseButton::eSecondary && MOZ_LIKELY(!mIsDestroyed)) {
+ WidgetMouseEvent contextMenuEvent(true, eContextMenu, this,
+ WidgetMouseEvent::eReal);
+ InitButtonEvent(contextMenuEvent, aEvent, aRefPoint);
+ contextMenuEvent.mPressure = mLastMotionPressure;
+ DispatchInputEvent(&contextMenuEvent);
+ }
+}
+
+void nsWindow::TryToShowNativeWindowMenu(GdkEventButton* aEvent) {
+ if (!gdk_window_show_window_menu(GetToplevelGdkWindow(), (GdkEvent*)aEvent)) {
+ NS_WARNING("Native context menu wasn't shown");
+ }
+}
+
+bool nsWindow::DoTitlebarAction(LookAndFeel::TitlebarEvent aEvent,
+ GdkEventButton* aButtonEvent) {
+ LOG("DoTitlebarAction %s click",
+ aEvent == LookAndFeel::TitlebarEvent::Double_Click ? "double" : "middle");
+ switch (LookAndFeel::GetTitlebarAction(aEvent)) {
+ case LookAndFeel::TitlebarAction::WindowMenu:
+ // Titlebar app menu
+ LOG(" action menu");
+ TryToShowNativeWindowMenu(aButtonEvent);
+ break;
+ // Lower is part of gtk_surface1 protocol which we can't support
+ // as Gtk keeps it private. So emulate it by minimize.
+ case LookAndFeel::TitlebarAction::WindowLower:
+ case LookAndFeel::TitlebarAction::WindowMinimize:
+ LOG(" action minimize");
+ SetSizeMode(nsSizeMode_Minimized);
+ break;
+ case LookAndFeel::TitlebarAction::WindowMaximize:
+ LOG(" action maximize");
+ SetSizeMode(nsSizeMode_Maximized);
+ break;
+ case LookAndFeel::TitlebarAction::WindowMaximizeToggle:
+ LOG(" action toggle maximize");
+ if (mSizeMode == nsSizeMode_Maximized) {
+ SetSizeMode(nsSizeMode_Normal);
+ } else if (mSizeMode == nsSizeMode_Normal) {
+ SetSizeMode(nsSizeMode_Maximized);
+ }
+ break;
+ case LookAndFeel::TitlebarAction::None:
+ default:
+ LOG(" action none");
+ return false;
+ }
+ return true;
+}
+
+void nsWindow::OnButtonPressEvent(GdkEventButton* aEvent) {
+ LOG("Button %u press\n", aEvent->button);
+
+ SetLastMousePressEvent((GdkEvent*)aEvent);
+
+ // If you double click in GDK, it will actually generate a second
+ // GDK_BUTTON_PRESS before sending the GDK_2BUTTON_PRESS, and this is
+ // different than the DOM spec. GDK puts this in the queue
+ // programatically, so it's safe to assume that if there's a
+ // double click in the queue, it was generated so we can just drop
+ // this click.
+ GUniquePtr<GdkEvent> peekedEvent(gdk_event_peek());
+ if (peekedEvent) {
+ GdkEventType type = peekedEvent->any.type;
+ if (type == GDK_2BUTTON_PRESS || type == GDK_3BUTTON_PRESS) {
+ return;
+ }
+ }
+
+ nsWindow* containerWindow = GetContainerWindow();
+ if (!gFocusWindow && containerWindow) {
+ containerWindow->DispatchActivateEvent();
+ }
+
+ const auto refPoint = GetRefPoint(this, aEvent);
+
+ // check to see if we should rollup
+ if (CheckForRollup(aEvent->x_root, aEvent->y_root, false, false)) {
+ if (aEvent->button == 3 && mDraggableRegion.Contains(refPoint)) {
+ GUniquePtr<GdkEvent> eventCopy;
+ if (aEvent->type != GDK_BUTTON_PRESS) {
+ // If the user double-clicks too fast we'll get a 2BUTTON_PRESS event
+ // instead, and that isn't handled by open_window_menu, so coerce it
+ // into a regular press.
+ eventCopy.reset(gdk_event_copy((GdkEvent*)aEvent));
+ eventCopy->type = GDK_BUTTON_PRESS;
+ }
+ TryToShowNativeWindowMenu(eventCopy ? &eventCopy->button : aEvent);
+ }
+ return;
+ }
+
+ // Check to see if the event is within our window's resize region
+ if (auto edge = CheckResizerEdge(refPoint)) {
+ // On Wayland Gtk fails to vertically/horizontally resize windows
+ // with fixed aspect ratio. We need to emulate
+ // gdk_window_begin_resize_drag() at OnMotionNotifyEvent().
+ if (mAspectRatio != 0.0f && GdkIsWaylandDisplay()) {
+ mLastResizePoint = LayoutDeviceIntPoint::Floor(aEvent->x, aEvent->y);
+ switch (*edge) {
+ case GDK_WINDOW_EDGE_SOUTH:
+ mAspectResizer = Some(GTK_ORIENTATION_VERTICAL);
+ break;
+ case GDK_WINDOW_EDGE_EAST:
+ mAspectResizer = Some(GTK_ORIENTATION_HORIZONTAL);
+ break;
+ default:
+ mAspectResizer.reset();
+ break;
+ }
+ ApplySizeConstraints();
+ }
+ if (!mAspectResizer) {
+ gdk_window_begin_resize_drag(GetToplevelGdkWindow(), *edge,
+ aEvent->button, aEvent->x_root,
+ aEvent->y_root, aEvent->time);
+ }
+ return;
+ }
+
+ gdouble pressure = 0;
+ gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure);
+ mLastMotionPressure = pressure;
+
+ uint16_t domButton;
+ switch (aEvent->button) {
+ case 1:
+ domButton = MouseButton::ePrimary;
+ break;
+ case 2:
+ domButton = MouseButton::eMiddle;
+ break;
+ case 3:
+ domButton = MouseButton::eSecondary;
+ break;
+ // These are mapped to horizontal scroll
+ case 6:
+ case 7:
+ NS_WARNING("We're not supporting legacy horizontal scroll event");
+ return;
+ // Map buttons 8-9(10) to back/forward
+ case 8:
+ if (!Preferences::GetBool("mousebutton.4th.enabled", true)) {
+ return;
+ }
+ DispatchCommandEvent(nsGkAtoms::Back);
+ return;
+ case 9:
+ case 10:
+ if (!Preferences::GetBool("mousebutton.5th.enabled", true)) {
+ return;
+ }
+ DispatchCommandEvent(nsGkAtoms::Forward);
+ return;
+ default:
+ return;
+ }
+
+ gButtonState |= ButtonMaskFromGDKButton(aEvent->button);
+
+ WidgetMouseEvent event(true, eMouseDown, this, WidgetMouseEvent::eReal);
+ event.mButton = domButton;
+ InitButtonEvent(event, aEvent, refPoint);
+ event.mPressure = mLastMotionPressure;
+
+ nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event);
+
+ const bool defaultPrevented =
+ eventStatus.mContentStatus == nsEventStatus_eConsumeNoDefault;
+
+ if (!defaultPrevented && mDraggableRegion.Contains(refPoint)) {
+ if (domButton == MouseButton::ePrimary) {
+ mWindowShouldStartDragging = true;
+ } else if (domButton == MouseButton::eMiddle &&
+ StaticPrefs::widget_gtk_titlebar_action_middle_click_enabled()) {
+ DoTitlebarAction(nsXPLookAndFeel::TitlebarEvent::Middle_Click, aEvent);
+ }
+ }
+
+ // right menu click on linux should also pop up a context menu
+ if (!StaticPrefs::ui_context_menus_after_mouseup() &&
+ eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
+ DispatchContextMenuEventFromMouseEvent(domButton, aEvent, refPoint);
+ }
+}
+
+void nsWindow::OnButtonReleaseEvent(GdkEventButton* aEvent) {
+ LOG("Button %u release\n", aEvent->button);
+
+ SetLastMousePressEvent(nullptr);
+
+ if (!mGdkWindow) {
+ return;
+ }
+
+ if (mAspectResizer) {
+ mAspectResizer = Nothing();
+ return;
+ }
+
+ if (mWindowShouldStartDragging) {
+ mWindowShouldStartDragging = false;
+ }
+
+ uint16_t domButton;
+ switch (aEvent->button) {
+ case 1:
+ domButton = MouseButton::ePrimary;
+ break;
+ case 2:
+ domButton = MouseButton::eMiddle;
+ break;
+ case 3:
+ domButton = MouseButton::eSecondary;
+ break;
+ default:
+ return;
+ }
+
+ gButtonState &= ~ButtonMaskFromGDKButton(aEvent->button);
+
+ const auto refPoint = GetRefPoint(this, aEvent);
+
+ WidgetMouseEvent event(true, eMouseUp, this, WidgetMouseEvent::eReal);
+ event.mButton = domButton;
+ InitButtonEvent(event, aEvent, refPoint);
+ gdouble pressure = 0;
+ gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure);
+ event.mPressure = pressure ? (float)pressure : (float)mLastMotionPressure;
+
+ // The mRefPoint is manipulated in DispatchInputEvent, we're saving it
+ // to use it for the doubleclick position check.
+ const LayoutDeviceIntPoint pos = event.mRefPoint;
+
+ nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event);
+
+ const bool defaultPrevented =
+ eventStatus.mContentStatus == nsEventStatus_eConsumeNoDefault;
+ // Check if mouse position in titlebar and doubleclick happened to
+ // trigger defined action.
+ if (!defaultPrevented && mDrawInTitlebar &&
+ event.mButton == MouseButton::ePrimary && event.mClickCount == 2 &&
+ mDraggableRegion.Contains(pos)) {
+ DoTitlebarAction(nsXPLookAndFeel::TitlebarEvent::Double_Click, aEvent);
+ }
+ mLastMotionPressure = pressure;
+
+ // right menu click on linux should also pop up a context menu
+ if (StaticPrefs::ui_context_menus_after_mouseup() &&
+ eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
+ DispatchContextMenuEventFromMouseEvent(domButton, aEvent, refPoint);
+ }
+
+ // Open window manager menu on PIP window to allow user
+ // to place it on top / all workspaces.
+ if (mAlwaysOnTop && aEvent->button == 3) {
+ TryToShowNativeWindowMenu(aEvent);
+ }
+}
+
+void nsWindow::OnContainerFocusInEvent(GdkEventFocus* aEvent) {
+ LOG("OnContainerFocusInEvent");
+
+ // Unset the urgency hint, if possible
+ GtkWidget* top_window = GetToplevelWidget();
+ if (top_window && (gtk_widget_get_visible(top_window))) {
+ SetUrgencyHint(top_window, false);
+ }
+
+ // Return if being called within SetFocus because the focus manager
+ // already knows that the window is active.
+ if (gBlockActivateEvent) {
+ LOG("activated notification is blocked");
+ return;
+ }
+
+ // If keyboard input will be accepted, the focus manager will call
+ // SetFocus to set the correct window.
+ gFocusWindow = nullptr;
+
+ DispatchActivateEvent();
+
+ if (!gFocusWindow) {
+ // We don't really have a window for dispatching key events, but
+ // setting a non-nullptr value here prevents OnButtonPressEvent() from
+ // dispatching an activation notification if the widget is already
+ // active.
+ gFocusWindow = this;
+ }
+
+ LOG("Events sent from focus in event");
+}
+
+void nsWindow::OnContainerFocusOutEvent(GdkEventFocus* aEvent) {
+ LOG("OnContainerFocusOutEvent");
+
+ if (IsTopLevelWindowType()) {
+ // Rollup menus when a window is focused out unless a drag is occurring.
+ // This check is because drags grab the keyboard and cause a focus out on
+ // versions of GTK before 2.18.
+ const bool shouldRollupMenus = [&] {
+ nsCOMPtr<nsIDragService> dragService =
+ do_GetService("@mozilla.org/widget/dragservice;1");
+ nsCOMPtr<nsIDragSession> dragSession;
+ dragService->GetCurrentSession(getter_AddRefs(dragSession));
+ if (!dragSession) {
+ return true;
+ }
+ // We also roll up when a drag is from a different application
+ nsCOMPtr<nsINode> sourceNode;
+ dragSession->GetSourceNode(getter_AddRefs(sourceNode));
+ return !sourceNode;
+ }();
+
+ if (shouldRollupMenus) {
+ RollupAllMenus();
+ }
+
+ if (RefPtr pm = nsXULPopupManager::GetInstance()) {
+ pm->RollupTooltips();
+ }
+ }
+
+ if (gFocusWindow) {
+ RefPtr<nsWindow> kungFuDeathGrip = gFocusWindow;
+ if (gFocusWindow->mIMContext) {
+ gFocusWindow->mIMContext->OnBlurWindow(gFocusWindow);
+ }
+ gFocusWindow = nullptr;
+ }
+
+ DispatchDeactivateEvent();
+
+ if (IsChromeWindowTitlebar()) {
+ // DispatchDeactivateEvent() ultimately results in a call to
+ // BrowsingContext::SetIsActiveBrowserWindow(), which resets
+ // the state. We call UpdateMozWindowActive() to keep it in
+ // sync with GDK_WINDOW_STATE_FOCUSED.
+ UpdateMozWindowActive();
+ }
+
+ LOG("Done with container focus out");
+}
+
+bool nsWindow::DispatchCommandEvent(nsAtom* aCommand) {
+ nsEventStatus status;
+ WidgetCommandEvent appCommandEvent(true, aCommand, this);
+ DispatchEvent(&appCommandEvent, status);
+ return TRUE;
+}
+
+bool nsWindow::DispatchContentCommandEvent(EventMessage aMsg) {
+ nsEventStatus status;
+ WidgetContentCommandEvent event(true, aMsg, this);
+ DispatchEvent(&event, status);
+ return TRUE;
+}
+
+WidgetEventTime nsWindow::GetWidgetEventTime(guint32 aEventTime) {
+ return WidgetEventTime(GetEventTimeStamp(aEventTime));
+}
+
+TimeStamp nsWindow::GetEventTimeStamp(guint32 aEventTime) {
+ if (MOZ_UNLIKELY(!mGdkWindow)) {
+ // nsWindow has been Destroy()ed.
+ return TimeStamp::Now();
+ }
+ if (aEventTime == 0) {
+ // Some X11 and GDK events may be received with a time of 0 to indicate
+ // that they are synthetic events. Some input method editors do this.
+ // In this case too, just return the current timestamp.
+ return TimeStamp::Now();
+ }
+
+ TimeStamp eventTimeStamp;
+
+ if (GdkIsWaylandDisplay()) {
+ // Wayland compositors use monotonic time to set timestamps.
+ int64_t timestampTime = g_get_monotonic_time() / 1000;
+ guint32 refTimeTruncated = guint32(timestampTime);
+
+ timestampTime -= refTimeTruncated - aEventTime;
+ int64_t tick =
+ BaseTimeDurationPlatformUtils::TicksFromMilliseconds(timestampTime);
+ eventTimeStamp = TimeStamp::FromSystemTime(tick);
+ } else {
+#ifdef MOZ_X11
+ CurrentX11TimeGetter* getCurrentTime = GetCurrentTimeGetter();
+ MOZ_ASSERT(getCurrentTime,
+ "Null current time getter despite having a window");
+ eventTimeStamp =
+ TimeConverter().GetTimeStampFromSystemTime(aEventTime, *getCurrentTime);
+#endif
+ }
+ return eventTimeStamp;
+}
+
+#ifdef MOZ_X11
+mozilla::CurrentX11TimeGetter* nsWindow::GetCurrentTimeGetter() {
+ MOZ_ASSERT(mGdkWindow, "Expected mGdkWindow to be set");
+ if (MOZ_UNLIKELY(!mCurrentTimeGetter)) {
+ mCurrentTimeGetter = MakeUnique<CurrentX11TimeGetter>(mGdkWindow);
+ }
+ return mCurrentTimeGetter.get();
+}
+#endif
+
+gboolean nsWindow::OnKeyPressEvent(GdkEventKey* aEvent) {
+ LOG("OnKeyPressEvent");
+
+ KeymapWrapper::HandleKeyPressEvent(this, aEvent);
+ return TRUE;
+}
+
+gboolean nsWindow::OnKeyReleaseEvent(GdkEventKey* aEvent) {
+ LOG("OnKeyReleaseEvent");
+ if (NS_WARN_IF(!KeymapWrapper::HandleKeyReleaseEvent(this, aEvent))) {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void nsWindow::OnScrollEvent(GdkEventScroll* aEvent) {
+ LOG("OnScrollEvent");
+
+ // check to see if we should rollup
+ if (CheckForRollup(aEvent->x_root, aEvent->y_root, true, false)) {
+ return;
+ }
+
+ // check for duplicate legacy scroll event, see GNOME bug 726878
+ if (aEvent->direction != GDK_SCROLL_SMOOTH &&
+ mLastScrollEventTime == aEvent->time) {
+ LOG("[%d] duplicate legacy scroll event %d\n", aEvent->time,
+ aEvent->direction);
+ return;
+ }
+ WidgetWheelEvent wheelEvent(true, eWheel, this);
+ wheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_LINE;
+ switch (aEvent->direction) {
+ case GDK_SCROLL_SMOOTH: {
+ // As of GTK 3.4, all directional scroll events are provided by
+ // the GDK_SCROLL_SMOOTH direction on XInput2 and Wayland devices.
+ mLastScrollEventTime = aEvent->time;
+
+ // Special handling for touchpads to support flings
+ // (also known as kinetic/inertial/momentum scrolling)
+ GdkDevice* device = gdk_event_get_source_device((GdkEvent*)aEvent);
+ GdkInputSource source = gdk_device_get_source(device);
+ if (source == GDK_SOURCE_TOUCHSCREEN || source == GDK_SOURCE_TOUCHPAD ||
+ mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase.isSome()) {
+ if (StaticPrefs::apz_gtk_pangesture_enabled() &&
+ gtk_check_version(3, 20, 0) == nullptr) {
+ static auto sGdkEventIsScrollStopEvent =
+ (gboolean(*)(const GdkEvent*))dlsym(
+ RTLD_DEFAULT, "gdk_event_is_scroll_stop_event");
+
+ LOG("[%d] pan smooth event dx=%f dy=%f inprogress=%d\n", aEvent->time,
+ aEvent->delta_x, aEvent->delta_y, mPanInProgress);
+ auto eventType = PanGestureInput::PANGESTURE_PAN;
+ if (sGdkEventIsScrollStopEvent((GdkEvent*)aEvent)) {
+ eventType = PanGestureInput::PANGESTURE_END;
+ mPanInProgress = false;
+ } else if (!mPanInProgress) {
+ eventType = PanGestureInput::PANGESTURE_START;
+ mPanInProgress = true;
+ } else if (mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase
+ .isSome()) {
+ switch (*mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase) {
+ case PHASE_BEGIN:
+ // we should never hit this because we'll hit the above case
+ // before this.
+ MOZ_ASSERT_UNREACHABLE();
+ eventType = PanGestureInput::PANGESTURE_START;
+ mPanInProgress = true;
+ break;
+ case PHASE_UPDATE:
+ // nothing to do here, eventtype should already be set
+ MOZ_ASSERT(mPanInProgress);
+ MOZ_ASSERT(eventType == PanGestureInput::PANGESTURE_PAN);
+ eventType = PanGestureInput::PANGESTURE_PAN;
+ break;
+ case PHASE_END:
+ MOZ_ASSERT(mPanInProgress);
+ eventType = PanGestureInput::PANGESTURE_END;
+ mPanInProgress = false;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE();
+ break;
+ }
+ }
+
+ mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase.reset();
+
+ const bool isPageMode =
+#ifdef NIGHTLY_BUILD
+ StaticPrefs::apz_gtk_pangesture_delta_mode() == 1;
+#else
+ StaticPrefs::apz_gtk_pangesture_delta_mode() != 2;
+#endif
+ const double multiplier =
+ isPageMode
+ ? StaticPrefs::apz_gtk_pangesture_page_delta_mode_multiplier()
+ : StaticPrefs::
+ apz_gtk_pangesture_pixel_delta_mode_multiplier() *
+ FractionalScaleFactor();
+
+ ScreenPoint deltas(float(aEvent->delta_x * multiplier),
+ float(aEvent->delta_y * multiplier));
+
+ LayoutDeviceIntPoint touchPoint = GetRefPoint(this, aEvent);
+ PanGestureInput panEvent(
+ eventType, GetEventTimeStamp(aEvent->time),
+ ScreenPoint(touchPoint.x, touchPoint.y), deltas,
+ KeymapWrapper::ComputeKeyModifiers(aEvent->state));
+ panEvent.mDeltaType = isPageMode ? PanGestureInput::PANDELTA_PAGE
+ : PanGestureInput::PANDELTA_PIXEL;
+ panEvent.mSimulateMomentum =
+ StaticPrefs::apz_gtk_kinetic_scroll_enabled();
+
+ DispatchPanGesture(panEvent);
+
+ if (mCurrentSynthesizedTouchpadPan.mSavedObserver != 0) {
+ mozilla::widget::AutoObserverNotifier::NotifySavedObserver(
+ mCurrentSynthesizedTouchpadPan.mSavedObserver,
+ "touchpadpanevent");
+ mCurrentSynthesizedTouchpadPan.mSavedObserver = 0;
+ }
+
+ return;
+ }
+
+ // Older GTK doesn't support stop events, so we can't support fling
+ wheelEvent.mScrollType = WidgetWheelEvent::SCROLL_ASYNCHRONOUSLY;
+ }
+
+ // TODO - use a more appropriate scrolling unit than lines.
+ // Multiply event deltas by 3 to emulate legacy behaviour.
+ wheelEvent.mDeltaX = aEvent->delta_x * 3;
+ wheelEvent.mDeltaY = aEvent->delta_y * 3;
+ wheelEvent.mWheelTicksX = aEvent->delta_x;
+ wheelEvent.mWheelTicksY = aEvent->delta_y;
+ wheelEvent.mIsNoLineOrPageDelta = true;
+
+ break;
+ }
+ case GDK_SCROLL_UP:
+ wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = -3;
+ wheelEvent.mWheelTicksY = -1;
+ break;
+ case GDK_SCROLL_DOWN:
+ wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = 3;
+ wheelEvent.mWheelTicksY = 1;
+ break;
+ case GDK_SCROLL_LEFT:
+ wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = -1;
+ wheelEvent.mWheelTicksX = -1;
+ break;
+ case GDK_SCROLL_RIGHT:
+ wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = 1;
+ wheelEvent.mWheelTicksX = 1;
+ break;
+ }
+
+ wheelEvent.mRefPoint = GetRefPoint(this, aEvent);
+
+ KeymapWrapper::InitInputEvent(wheelEvent, aEvent->state);
+
+ wheelEvent.AssignEventTime(GetWidgetEventTime(aEvent->time));
+
+ DispatchInputEvent(&wheelEvent);
+}
+
+void nsWindow::DispatchPanGesture(PanGestureInput& aPanInput) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mSwipeTracker) {
+ // Give the swipe tracker a first pass at the event. If a new pan gesture
+ // has been started since the beginning of the swipe, the swipe tracker
+ // will know to ignore the event.
+ nsEventStatus status = mSwipeTracker->ProcessEvent(aPanInput);
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ }
+
+ APZEventResult result;
+ if (mAPZC) {
+ MOZ_ASSERT(APZThreadUtils::IsControllerThread());
+
+ result = mAPZC->InputBridge()->ReceiveInputEvent(aPanInput);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ }
+
+ WidgetWheelEvent event = aPanInput.ToWidgetEvent(this);
+ if (!mAPZC) {
+ if (MayStartSwipeForNonAPZ(aPanInput)) {
+ return;
+ }
+ } else {
+ event = MayStartSwipeForAPZ(aPanInput, result);
+ }
+
+ ProcessUntransformedAPZEvent(&event, result);
+}
+
+void nsWindow::OnVisibilityNotifyEvent(GdkVisibilityState aState) {
+ LOG("nsWindow::OnVisibilityNotifyEvent [%p] state 0x%x\n", this, aState);
+ auto state = aState == GDK_VISIBILITY_FULLY_OBSCURED
+ ? OcclusionState::OCCLUDED
+ : OcclusionState::UNKNOWN;
+ NotifyOcclusionState(state);
+}
+
+void nsWindow::OnWindowStateEvent(GtkWidget* aWidget,
+ GdkEventWindowState* aEvent) {
+ LOG("nsWindow::OnWindowStateEvent for %p changed 0x%x new_window_state "
+ "0x%x\n",
+ aWidget, aEvent->changed_mask, aEvent->new_window_state);
+
+ if (IS_MOZ_CONTAINER(aWidget)) {
+ // This event is notifying the container widget of changes to the
+ // toplevel window. Just detect changes affecting whether windows are
+ // viewable.
+ //
+ // (A visibility notify event is sent to each window that becomes
+ // viewable when the toplevel is mapped, but we can't rely on that for
+ // setting mHasMappedToplevel because these toplevel window state
+ // events are asynchronous. The windows in the hierarchy now may not
+ // be the same windows as when the toplevel was mapped, so they may
+ // not get VisibilityNotify events.)
+ bool mapped = !(aEvent->new_window_state &
+ (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_WITHDRAWN));
+ SetHasMappedToplevel(mapped);
+ LOG("\tquick return because IS_MOZ_CONTAINER(aWidget) is true\n");
+ return;
+ }
+ // else the widget is a shell widget.
+
+ // The block below is a bit evil.
+ //
+ // When a window is resized before it is shown, gtk_window_resize() delays
+ // resizes until the window is shown. If gtk_window_state_event() sees a
+ // GDK_WINDOW_STATE_MAXIMIZED change [1] before the window is shown, then
+ // gtk_window_compute_configure_request_size() ignores the values from the
+ // resize [2]. See bug 1449166 for an example of how this could happen.
+ //
+ // [1] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L7967
+ // [2] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L9377
+ //
+ // In order to provide a sensible size for the window when the user exits
+ // maximized state, we hide the GDK_WINDOW_STATE_MAXIMIZED change from
+ // gtk_window_state_event() so as to trick GTK into using the values from
+ // gtk_window_resize() in its configure request.
+ //
+ // We instead notify gtk_window_state_event() of the maximized state change
+ // once the window is shown.
+ //
+ // See https://gitlab.gnome.org/GNOME/gtk/issues/1044
+ //
+ // This may be fixed in Gtk 3.24+ but it's still live and kicking
+ // (Bug 1791779).
+ if (!mIsShown) {
+ aEvent->changed_mask = static_cast<GdkWindowState>(
+ aEvent->changed_mask & ~GDK_WINDOW_STATE_MAXIMIZED);
+ } else if (aEvent->changed_mask & GDK_WINDOW_STATE_WITHDRAWN &&
+ aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) {
+ aEvent->changed_mask = static_cast<GdkWindowState>(
+ aEvent->changed_mask | GDK_WINDOW_STATE_MAXIMIZED);
+ }
+
+ // This is a workaround for https://gitlab.gnome.org/GNOME/gtk/issues/1395
+ // Gtk+ controls window active appearance by window-state-event signal.
+ if (IsChromeWindowTitlebar() &&
+ (aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED)) {
+ // Emulate what Gtk+ does at gtk_window_state_event().
+ // We can't check GTK_STATE_FLAG_BACKDROP directly as it's set by Gtk+
+ // *after* this window-state-event handler.
+ mTitlebarBackdropState =
+ !(aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED);
+
+ // keep IsActiveBrowserWindow in sync with GDK_WINDOW_STATE_FOCUSED
+ UpdateMozWindowActive();
+
+ ForceTitlebarRedraw();
+ }
+
+ // We don't care about anything but changes in the maximized/icon/fullscreen
+ // states but we need a workaround for bug in Wayland:
+ // https://gitlab.gnome.org/GNOME/gtk/issues/67
+ // Under wayland the gtk_window_iconify implementation does NOT synthetize
+ // window_state_event where the GDK_WINDOW_STATE_ICONIFIED is set.
+ // During restore we won't get aEvent->changed_mask with
+ // the GDK_WINDOW_STATE_ICONIFIED so to detect that change we use the stored
+ // mSizeMode and obtaining a focus.
+ bool waylandWasIconified =
+ (GdkIsWaylandDisplay() &&
+ aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED &&
+ aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED &&
+ mSizeMode == nsSizeMode_Minimized);
+ if (!waylandWasIconified &&
+ (aEvent->changed_mask &
+ (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_MAXIMIZED |
+ GDK_WINDOW_STATE_TILED | GDK_WINDOW_STATE_FULLSCREEN)) == 0) {
+ LOG("\tearly return because no interesting bits changed\n");
+ return;
+ }
+
+ auto oldSizeMode = mSizeMode;
+ if (aEvent->new_window_state & GDK_WINDOW_STATE_ICONIFIED) {
+ LOG("\tIconified\n");
+ mSizeMode = nsSizeMode_Minimized;
+#ifdef ACCESSIBILITY
+ DispatchMinimizeEventAccessible();
+#endif // ACCESSIBILITY
+ } else if (aEvent->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) {
+ LOG("\tFullscreen\n");
+ mSizeMode = nsSizeMode_Fullscreen;
+ } else if (aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) {
+ LOG("\tMaximized\n");
+ mSizeMode = nsSizeMode_Maximized;
+#ifdef ACCESSIBILITY
+ DispatchMaximizeEventAccessible();
+#endif // ACCESSIBILITY
+ } else {
+ LOG("\tNormal\n");
+ mSizeMode = nsSizeMode_Normal;
+#ifdef ACCESSIBILITY
+ DispatchRestoreEventAccessible();
+#endif // ACCESSIBILITY
+ }
+
+ mIsTiled = aEvent->new_window_state & GDK_WINDOW_STATE_TILED;
+ LOG("\tTiled: %d\n", int(mIsTiled));
+
+ if (mWidgetListener && mSizeMode != oldSizeMode) {
+ mWidgetListener->SizeModeChanged(mSizeMode);
+ }
+
+ if (mDrawInTitlebar && mTransparencyBitmapForTitlebar) {
+ if (mSizeMode == nsSizeMode_Normal && !mIsTiled) {
+ UpdateTitlebarTransparencyBitmap();
+ } else {
+ ClearTransparencyBitmap();
+ }
+ } else {
+ SetTitlebarRect();
+ }
+}
+
+void nsWindow::OnDPIChanged() {
+ // Update menu's font size etc.
+ // This affects style / layout because it affects system font sizes.
+ if (mWidgetListener) {
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ mWidgetListener->UIResolutionChanged();
+ }
+}
+
+void nsWindow::OnCheckResize() { mPendingConfigures++; }
+
+void nsWindow::OnCompositedChanged() {
+ // Update CSD after the change in alpha visibility. This only affects
+ // system metrics, not other theme shenanigans.
+ NotifyThemeChanged(ThemeChangeKind::MediaQueriesOnly);
+ mCompositedScreen = gdk_screen_is_composited(gdk_screen_get_default());
+}
+
+void nsWindow::OnScaleChanged(bool aNotify) {
+ if (!IsTopLevelWindowType()) {
+ return;
+ }
+ if (!mGdkWindow) {
+ return; // We'll get there again when we configure the window.
+ }
+ gint newCeiled = gdk_window_get_scale_factor(mGdkWindow);
+ double newFractional = [&] {
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ return moz_container_wayland_get_fractional_scale(mContainer);
+ }
+#endif
+ return 0.0;
+ }();
+
+ if (mCeiledScaleFactor == newCeiled &&
+ mFractionalScaleFactor == newFractional) {
+ return;
+ }
+
+ LOG("OnScaleChanged %d, %f -> %d, %f\n", int(mCeiledScaleFactor),
+ mFractionalScaleFactor, newCeiled, newFractional);
+
+ mCeiledScaleFactor = newCeiled;
+ mFractionalScaleFactor = newFractional;
+
+ if (!aNotify) {
+ return;
+ }
+
+ // We pause compositor to avoid rendering of obsoleted remote content which
+ // produces flickering.
+ // Re-enable compositor again when remote content is updated or timeout
+ // happens.
+ PauseCompositorFlickering();
+
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(mContainer), &allocation);
+ LayoutDeviceIntSize size = GdkRectToDevicePixels(allocation).Size();
+ mBounds.SizeTo(size);
+ // Check mBounds size
+ if (mCompositorSession &&
+ !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) {
+ gfxCriticalNoteOnce << "Invalid mBounds in OnScaleChanged " << mBounds;
+ }
+
+ if (mWidgetListener) {
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ }
+
+ DispatchResized();
+
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize());
+ }
+
+ if (mCursor.IsCustom()) {
+ mUpdateCursor = true;
+ SetCursor(Cursor{mCursor});
+ }
+}
+
+void nsWindow::DispatchDragEvent(EventMessage aMsg,
+ const LayoutDeviceIntPoint& aRefPoint,
+ guint aTime) {
+ LOGDRAG("nsWindow::DispatchDragEvent");
+ WidgetDragEvent event(true, aMsg, this);
+
+ InitDragEvent(event);
+
+ event.mRefPoint = aRefPoint;
+ event.AssignEventTime(GetWidgetEventTime(aTime));
+
+ DispatchInputEvent(&event);
+}
+
+void nsWindow::OnDragDataReceivedEvent(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX,
+ gint aY,
+ GtkSelectionData* aSelectionData,
+ guint aInfo, guint aTime,
+ gpointer aData) {
+ LOGDRAG("nsWindow::OnDragDataReceived");
+
+ RefPtr<nsDragService> dragService = nsDragService::GetInstance();
+ nsDragService::AutoEventLoop loop(dragService);
+ dragService->TargetDataReceived(aWidget, aDragContext, aX, aY, aSelectionData,
+ aInfo, aTime);
+}
+
+nsWindow* nsWindow::GetTransientForWindowIfPopup() {
+ if (mWindowType != WindowType::Popup) {
+ return nullptr;
+ }
+ GtkWindow* toplevel = gtk_window_get_transient_for(GTK_WINDOW(mShell));
+ if (toplevel) {
+ return get_window_for_gtk_widget(GTK_WIDGET(toplevel));
+ }
+ return nullptr;
+}
+
+bool nsWindow::IsHandlingTouchSequence(GdkEventSequence* aSequence) {
+ return mHandleTouchEvent && mTouches.Contains(aSequence);
+}
+
+gboolean nsWindow::OnTouchpadPinchEvent(GdkEventTouchpadPinch* aEvent) {
+ if (!StaticPrefs::apz_gtk_touchpad_pinch_enabled()) {
+ return TRUE;
+ }
+ // Do not respond to pinch gestures involving more than two fingers
+ // unless specifically preffed on. These are sometimes hooked up to other
+ // actions at the desktop environment level and having the browser also
+ // pinch can be undesirable.
+ if (aEvent->n_fingers > 2 &&
+ !StaticPrefs::apz_gtk_touchpad_pinch_three_fingers_enabled()) {
+ return FALSE;
+ }
+ auto pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
+ ScreenCoord currentSpan;
+ ScreenCoord previousSpan;
+
+ switch (aEvent->phase) {
+ case GDK_TOUCHPAD_GESTURE_PHASE_BEGIN:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_START;
+ currentSpan = aEvent->scale;
+ mCurrentTouchpadFocus = ViewAs<ScreenPixel>(
+ GetRefPoint(this, aEvent),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ // Assign PreviousSpan --> 0.999 to make mDeltaY field of the
+ // WidgetWheelEvent that this PinchGestureInput event will be converted
+ // to not equal Zero as our discussion because we observed that the
+ // scale of the PHASE_BEGIN event is 1.
+ previousSpan = 0.999;
+ break;
+
+ case GDK_TOUCHPAD_GESTURE_PHASE_UPDATE:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
+ mCurrentTouchpadFocus += ScreenPoint(aEvent->dx, aEvent->dy);
+ if (aEvent->scale == mLastPinchEventSpan) {
+ return FALSE;
+ }
+ currentSpan = aEvent->scale;
+ previousSpan = mLastPinchEventSpan;
+ break;
+
+ case GDK_TOUCHPAD_GESTURE_PHASE_END:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_END;
+ currentSpan = aEvent->scale;
+ previousSpan = mLastPinchEventSpan;
+ break;
+
+ default:
+ return FALSE;
+ }
+
+ PinchGestureInput event(
+ pinchGestureType, PinchGestureInput::TRACKPAD,
+ GetEventTimeStamp(aEvent->time), ExternalPoint(0, 0),
+ mCurrentTouchpadFocus,
+ 100.0 * ((aEvent->phase == GDK_TOUCHPAD_GESTURE_PHASE_END)
+ ? ScreenCoord(1.f)
+ : currentSpan),
+ 100.0 * ((aEvent->phase == GDK_TOUCHPAD_GESTURE_PHASE_END)
+ ? ScreenCoord(1.f)
+ : previousSpan),
+ KeymapWrapper::ComputeKeyModifiers(aEvent->state));
+
+ if (!event.SetLineOrPageDeltaY(this)) {
+ return FALSE;
+ }
+
+ mLastPinchEventSpan = aEvent->scale;
+ DispatchPinchGestureInput(event);
+ return TRUE;
+}
+
+gboolean nsWindow::OnTouchEvent(GdkEventTouch* aEvent) {
+ LOG("OnTouchEvent: x=%f y=%f type=%d\n", aEvent->x, aEvent->y, aEvent->type);
+ if (!mHandleTouchEvent) {
+ // If a popup window was spawned (e.g. as the result of a long-press)
+ // and touch events got diverted to that window within a touch sequence,
+ // ensure the touch event gets sent to the original window instead. We
+ // keep the checks here very conservative so that we only redirect
+ // events in this specific scenario.
+ nsWindow* targetWindow = GetTransientForWindowIfPopup();
+ if (targetWindow &&
+ targetWindow->IsHandlingTouchSequence(aEvent->sequence)) {
+ return targetWindow->OnTouchEvent(aEvent);
+ }
+
+ return FALSE;
+ }
+
+ EventMessage msg;
+ switch (aEvent->type) {
+ case GDK_TOUCH_BEGIN:
+ // check to see if we should rollup
+ if (CheckForRollup(aEvent->x_root, aEvent->y_root, false, false)) {
+ return FALSE;
+ }
+ msg = eTouchStart;
+ break;
+ case GDK_TOUCH_UPDATE:
+ msg = eTouchMove;
+ // Start dragging when motion events happens in the dragging area
+ if (mWindowShouldStartDragging) {
+ mWindowShouldStartDragging = false;
+ if (mGdkWindow) {
+ GdkWindow* gdk_window = gdk_window_get_toplevel(mGdkWindow);
+ MOZ_ASSERT(gdk_window,
+ "gdk_window_get_toplevel should not return null");
+
+ LOG(" start window dragging window\n");
+ gdk_window_begin_move_drag(gdk_window, 1, aEvent->x_root,
+ aEvent->y_root, aEvent->time);
+
+ // Cancel the event sequence. gdk will steal all subsequent events
+ // (including TOUCH_END).
+ msg = eTouchCancel;
+ }
+ }
+ break;
+ case GDK_TOUCH_END:
+ msg = eTouchEnd;
+ if (mWindowShouldStartDragging) {
+ LOG(" end of window dragging window\n");
+ mWindowShouldStartDragging = false;
+ }
+ break;
+ case GDK_TOUCH_CANCEL:
+ msg = eTouchCancel;
+ break;
+ default:
+ return FALSE;
+ }
+
+ const LayoutDeviceIntPoint touchPoint = GetRefPoint(this, aEvent);
+
+ int32_t id;
+ RefPtr<dom::Touch> touch;
+ if (mTouches.Remove(aEvent->sequence, getter_AddRefs(touch))) {
+ id = touch->mIdentifier;
+ } else {
+ id = ++gLastTouchID & 0x7FFFFFFF;
+ }
+
+ touch =
+ new dom::Touch(id, touchPoint, LayoutDeviceIntPoint(1, 1), 0.0f, 0.0f);
+
+ WidgetTouchEvent event(true, msg, this);
+ KeymapWrapper::InitInputEvent(event, aEvent->state);
+
+ if (msg == eTouchStart || msg == eTouchMove) {
+ mTouches.InsertOrUpdate(aEvent->sequence, std::move(touch));
+ // add all touch points to event object
+ for (const auto& data : mTouches.Values()) {
+ event.mTouches.AppendElement(new dom::Touch(*data));
+ }
+ } else if (msg == eTouchEnd || msg == eTouchCancel) {
+ *event.mTouches.AppendElement() = std::move(touch);
+ }
+
+ nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event);
+
+ // There's a chance that we are in drag area and the event is not consumed
+ // by something on it.
+ if (msg == eTouchStart && mDraggableRegion.Contains(touchPoint) &&
+ eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
+ mWindowShouldStartDragging = true;
+ }
+ return TRUE;
+}
+
+// Return true if toplevel window is transparent.
+// It's transparent when we're running on composited screens
+// and we can draw main window without system titlebar.
+bool nsWindow::IsToplevelWindowTransparent() {
+ static bool transparencyConfigured = false;
+
+ if (!transparencyConfigured) {
+ if (gdk_screen_is_composited(gdk_screen_get_default())) {
+ // Some Gtk+ themes use non-rectangular toplevel windows. To fully
+ // support such themes we need to make toplevel window transparent
+ // with ARGB visual.
+ // It may cause performanance issue so make it configurable
+ // and enable it by default for selected window managers.
+ if (Preferences::HasUserValue("mozilla.widget.use-argb-visuals")) {
+ // argb visual is explicitly required so use it
+ sTransparentMainWindow =
+ Preferences::GetBool("mozilla.widget.use-argb-visuals");
+ } else {
+ // Enable transparent toplevel window if we can draw main window
+ // without system titlebar as Gtk+ themes use titlebar round corners.
+ sTransparentMainWindow =
+ GetSystemGtkWindowDecoration() != GTK_DECORATION_NONE;
+ }
+ }
+ transparencyConfigured = true;
+ }
+
+ return sTransparentMainWindow;
+}
+
+#ifdef MOZ_X11
+// Configure GL visual on X11.
+bool nsWindow::ConfigureX11GLVisual() {
+ auto* screen = gtk_widget_get_screen(mShell);
+ int visualId = 0;
+ bool haveVisual = false;
+
+ if (gfxVars::UseEGL()) {
+ haveVisual = GLContextEGL::FindVisual(&visualId);
+ }
+
+ // We are on GLX or use it as a fallback on Mesa, see
+ // https://gitlab.freedesktop.org/mesa/mesa/-/issues/149
+ if (!haveVisual) {
+ auto* display = GDK_DISPLAY_XDISPLAY(gtk_widget_get_display(mShell));
+ int screenNumber = GDK_SCREEN_XNUMBER(screen);
+ haveVisual = GLContextGLX::FindVisual(display, screenNumber, &visualId);
+ }
+
+ GdkVisual* gdkVisual = nullptr;
+ if (haveVisual) {
+ // If we're using CSD, rendering will go through mContainer, but
+ // it will inherit this visual as it is a child of mShell.
+ gdkVisual = gdk_x11_screen_lookup_visual(screen, visualId);
+ }
+ if (!gdkVisual) {
+ NS_WARNING("We're missing X11 Visual!");
+ // We try to use a fallback alpha visual
+ GdkScreen* screen = gtk_widget_get_screen(mShell);
+ gdkVisual = gdk_screen_get_rgba_visual(screen);
+ }
+ if (gdkVisual) {
+ gtk_widget_set_visual(mShell, gdkVisual);
+ mHasAlphaVisual = true;
+ return true;
+ }
+
+ return false;
+}
+#endif
+
+nsAutoCString nsWindow::GetFrameTag() const {
+ if (nsIFrame* frame = GetFrame()) {
+#ifdef DEBUG_FRAME_DUMP
+ return frame->ListTag();
+#else
+ nsAutoCString buf;
+ buf.AppendPrintf("Frame(%p)", frame);
+ if (nsIContent* content = frame->GetContent()) {
+ buf.Append(' ');
+ AppendUTF16toUTF8(content->NodeName(), buf);
+ }
+ return buf;
+#endif
+ }
+ return nsAutoCString("(no frame)");
+}
+
+nsCString nsWindow::GetPopupTypeName() {
+ switch (mPopupType) {
+ case PopupType::Menu:
+ return nsCString("Menu");
+ case PopupType::Tooltip:
+ return nsCString("Tooltip");
+ case PopupType::Panel:
+ return nsCString("Panel/Utility");
+ default:
+ return nsCString("Unknown");
+ }
+}
+
+// Disables all rendering of GtkWidget from Gtk side.
+// We do our best to persuade Gtk/Gdk to ignore all painting
+// to the widget.
+static void GtkWidgetDisableUpdates(GtkWidget* aWidget) {
+ // Clear exposure mask - it disabled synthesized events.
+ GdkWindow* window = gtk_widget_get_window(aWidget);
+ if (!window) {
+ return;
+ }
+ gdk_window_set_events(window, (GdkEventMask)(gdk_window_get_events(window) &
+ (~GDK_EXPOSURE_MASK)));
+
+ // Remove before/after paint handles from frame clock.
+ // It disables widget content updates.
+ GdkFrameClock* frame_clock = gdk_window_get_frame_clock(window);
+ g_signal_handlers_disconnect_by_data(frame_clock, window);
+}
+
+Window nsWindow::GetX11Window() {
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ return mGdkWindow ? gdk_x11_window_get_xid(mGdkWindow) : X11None;
+ }
+#endif
+ return (Window) nullptr;
+}
+
+void nsWindow::EnsureGdkWindow() {
+ MOZ_DIAGNOSTIC_ASSERT(mIsMapped);
+ if (!mGdkWindow) {
+ mGdkWindow = gtk_widget_get_window(GTK_WIDGET(mContainer));
+ g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", this);
+ if (mIMContext) {
+ mIMContext->SetGdkWindow(mGdkWindow);
+ }
+ }
+}
+
+bool nsWindow::GetShapedState() {
+ return mIsTransparent && !mHasAlphaVisual && !mTransparencyBitmapForTitlebar;
+}
+
+void nsWindow::ConfigureCompositor() {
+ MOZ_DIAGNOSTIC_ASSERT(mCompositorState == COMPOSITOR_ENABLED);
+ MOZ_DIAGNOSTIC_ASSERT(mIsMapped);
+
+ LOG("nsWindow::ConfigureCompositor()");
+ auto startCompositing = [self = RefPtr{this}, this]() -> void {
+ LOG(" moz_container_wayland_add_or_fire_initial_draw_callback "
+ "ConfigureCompositor");
+
+ // too late
+ if (mIsDestroyed || !mIsMapped) {
+ LOG(" quit, mIsDestroyed = %d mIsMapped = %d", !!mIsDestroyed,
+ mIsMapped);
+ return;
+ }
+ // Compositor will be resumed later by ResumeCompositorFlickering().
+ if (mCompositorState == COMPOSITOR_PAUSED_FLICKERING) {
+ LOG(" quit, will be resumed by ResumeCompositorFlickering.");
+ return;
+ }
+ // Compositor will be resumed at nsWindow::SetCompositorWidgetDelegate().
+ if (!mCompositorWidgetDelegate) {
+ LOG(" quit, missing mCompositorWidgetDelegate");
+ return;
+ }
+
+ ResumeCompositorImpl();
+ };
+
+ if (GdkIsWaylandDisplay()) {
+#ifdef MOZ_WAYLAND
+ moz_container_wayland_add_or_fire_initial_draw_callback(mContainer,
+ startCompositing);
+#endif
+ } else {
+ startCompositing();
+ }
+}
+
+void nsWindow::ConfigureGdkWindow() {
+ LOG("nsWindow::ConfigureGdkWindow()");
+
+ EnsureGdkWindow();
+ OnScaleChanged(/* aNotify = */ false);
+
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ GdkVisual* gdkVisual = gdk_window_get_visual(mGdkWindow);
+ Visual* visual = gdk_x11_visual_get_xvisual(gdkVisual);
+ int depth = gdk_visual_get_depth(gdkVisual);
+ mSurfaceProvider.Initialize(GetX11Window(), visual, depth,
+ GetShapedState());
+
+ // Set window manager hint to keep fullscreen windows composited.
+ //
+ // If the window were to get unredirected, there could be visible
+ // tearing because Gecko does not align its framebuffer updates with
+ // vblank.
+ SetCompositorHint(GTK_WIDGET_COMPOSIDED_ENABLED);
+ }
+#endif
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ mSurfaceProvider.Initialize(this);
+ }
+#endif
+
+ if (mIsDragPopup) {
+ if (GdkIsWaylandDisplay()) {
+ // Disable painting to the widget on Wayland as we paint directly to the
+ // widget. Wayland compositors does not paint wl_subsurface
+ // of D&D widget.
+ if (GtkWidget* parent = gtk_widget_get_parent(mShell)) {
+ GtkWidgetDisableUpdates(parent);
+ }
+ GtkWidgetDisableUpdates(mShell);
+ GtkWidgetDisableUpdates(GTK_WIDGET(mContainer));
+ } else {
+ // Disable rendering of parent container on X11 to avoid flickering.
+ if (GtkWidget* parent = gtk_widget_get_parent(mShell)) {
+ gtk_widget_set_opacity(parent, 0.0);
+ }
+ }
+ }
+
+ if (mWindowType == WindowType::Popup) {
+ if (mNoAutoHide) {
+ gint wmd = ConvertBorderStyles(mBorderStyle);
+ if (wmd != -1) {
+ gdk_window_set_decorations(mGdkWindow, (GdkWMDecoration)wmd);
+ }
+ }
+ // If the popup ignores mouse events, set an empty input shape.
+ SetInputRegion(mInputRegion);
+ }
+
+ RefreshWindowClass();
+
+ // We're not mapped yet but we have already created compositor.
+ if (mCompositorWidgetDelegate) {
+ ConfigureCompositor();
+ }
+
+ LOG(" finished, new GdkWindow %p XID 0x%lx\n", mGdkWindow, GetX11Window());
+}
+
+nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ LOG("nsWindow::Create\n");
+
+ // only set the base parent if we're going to be a dialog or a
+ // toplevel
+ nsIWidget* baseParent =
+ aInitData && (aInitData->mWindowType == WindowType::Dialog ||
+ aInitData->mWindowType == WindowType::TopLevel ||
+ aInitData->mWindowType == WindowType::Invisible)
+ ? nullptr
+ : aParent;
+
+#ifdef ACCESSIBILITY
+ // Send a DBus message to check whether a11y is enabled
+ a11y::PreInit();
+#endif
+
+#ifdef MOZ_WAYLAND
+ // Ensure that KeymapWrapper is created on Wayland as we need it for
+ // keyboard focus tracking.
+ if (GdkIsWaylandDisplay()) {
+ KeymapWrapper::EnsureInstance();
+ }
+#endif
+
+ // Ensure that the toolkit is created.
+ nsGTKToolkit::GetToolkit();
+
+ // initialize all the common bits of this class
+ BaseCreate(baseParent, aInitData);
+
+ // and do our common creation
+ mParent = aParent;
+ // save our bounds
+ mBounds = aRect;
+ LOG(" mBounds: x:%d y:%d w:%d h:%d\n", mBounds.x, mBounds.y, mBounds.width,
+ mBounds.height);
+
+ ConstrainSize(&mBounds.width, &mBounds.height);
+ mLastSizeRequest = mBounds.Size();
+
+ bool popupNeedsAlphaVisual = mWindowType == WindowType::Popup &&
+ (aInitData && aInitData->mTransparencyMode ==
+ TransparencyMode::Transparent);
+
+ // Figure out our parent window - only used for WindowType::Child
+ nsWindow* parentnsWindow = nullptr;
+
+ if (aParent) {
+ parentnsWindow = static_cast<nsWindow*>(aParent);
+ } else if (aNativeParent && GDK_IS_WINDOW(aNativeParent)) {
+ parentnsWindow = get_window_for_gdk_window(GDK_WINDOW(aNativeParent));
+ if (!parentnsWindow) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (mWindowType == WindowType::Child) {
+ // We don't support WindowType::Child directly but emulate it by popup
+ // windows.
+ mWindowType = WindowType::Popup;
+ if (!parentnsWindow) {
+ if (aNativeParent && GTK_IS_CONTAINER(aNativeParent)) {
+ parentnsWindow = get_window_for_gtk_widget(GTK_WIDGET(aNativeParent));
+ }
+ }
+ mIsChildWindow = true;
+ LOG(" child widget, switch to popup. parent nsWindow %p", parentnsWindow);
+ }
+
+ MOZ_ASSERT_IF(mWindowType == WindowType::Popup, parentnsWindow);
+
+ if (mWindowType != WindowType::Dialog && mWindowType != WindowType::Popup &&
+ mWindowType != WindowType::TopLevel &&
+ mWindowType != WindowType::Invisible) {
+ MOZ_ASSERT_UNREACHABLE("Unexpected eWindowType");
+ return NS_ERROR_FAILURE;
+ }
+
+ mAlwaysOnTop = aInitData && aInitData->mAlwaysOnTop;
+ // mNoAutoHide seems to be always false here.
+ // The mNoAutoHide state is set later on nsMenuPopupFrame level
+ // and can be changed so we use WaylandPopupIsPermanent() to get
+ // recent popup config (Bug 1728952).
+ mNoAutoHide = aInitData && aInitData->mNoAutoHide;
+
+ // Popups that are not noautohide are only temporary. The are used
+ // for menus and the like and disappear when another window is used.
+ // For most popups, use the standard GtkWindowType GTK_WINDOW_POPUP,
+ // which will use a Window with the override-redirect attribute
+ // (for temporary windows).
+ // For long-lived windows, their stacking order is managed by the
+ // window manager, as indicated by GTK_WINDOW_TOPLEVEL.
+ // For Wayland we have to always use GTK_WINDOW_POPUP to control
+ // popup window position.
+ GtkWindowType type = GTK_WINDOW_TOPLEVEL;
+ if (mWindowType == WindowType::Popup) {
+ MOZ_ASSERT(aInitData);
+ type = GTK_WINDOW_POPUP;
+ if (GdkIsX11Display() && mNoAutoHide) {
+ type = GTK_WINDOW_TOPLEVEL;
+ }
+ }
+ mShell = gtk_window_new(type);
+
+ // Ensure gfxPlatform is initialized, since that is what initializes
+ // gfxVars, used below.
+ Unused << gfxPlatform::GetPlatform();
+
+ if (IsTopLevelWindowType()) {
+ mGtkWindowDecoration = GetSystemGtkWindowDecoration();
+ // Inherit initial scale from our parent, or use the default monitor scale
+ // otherwise.
+ mCeiledScaleFactor = parentnsWindow
+ ? int32_t(parentnsWindow->mCeiledScaleFactor)
+ : ScreenHelperGTK::GetGTKMonitorScaleFactor();
+ }
+
+ // Don't use transparency for PictureInPicture windows.
+ bool toplevelNeedsAlphaVisual = false;
+ if (mWindowType == WindowType::TopLevel && !mIsPIPWindow) {
+ toplevelNeedsAlphaVisual = IsToplevelWindowTransparent();
+ }
+
+ bool isGLVisualSet = false;
+ mIsAccelerated = ComputeShouldAccelerate();
+#ifdef MOZ_X11
+ if (GdkIsX11Display() && mIsAccelerated) {
+ isGLVisualSet = ConfigureX11GLVisual();
+ }
+#endif
+ if (!isGLVisualSet && (popupNeedsAlphaVisual || toplevelNeedsAlphaVisual)) {
+ // We're running on composited screen so we can use alpha visual
+ // for both toplevel and popups.
+ if (mCompositedScreen) {
+ GdkVisual* visual =
+ gdk_screen_get_rgba_visual(gtk_widget_get_screen(mShell));
+ if (visual) {
+ gtk_widget_set_visual(mShell, visual);
+ mHasAlphaVisual = true;
+ }
+ }
+ }
+
+ // Use X shape mask to draw round corners of Firefox titlebar.
+ // We don't use shape masks any more as we switched to ARGB visual
+ // by default and non-compositing screens use solid-csd decorations
+ // without round corners.
+ // Leave the shape mask code here as it can be used to draw round
+ // corners on EGL (https://gitlab.freedesktop.org/mesa/mesa/-/issues/149)
+ // or when custom titlebar theme is used.
+ mTransparencyBitmapForTitlebar = TitlebarUseShapeMask();
+
+ // We have a toplevel window with transparency.
+ // Calls to UpdateTitlebarTransparencyBitmap() from OnExposeEvent()
+ // occur before SetTransparencyMode() receives TransparencyMode::Transparent
+ // from layout, so set mIsTransparent here.
+ if (mWindowType == WindowType::TopLevel &&
+ (mHasAlphaVisual || mTransparencyBitmapForTitlebar)) {
+ mIsTransparent = true;
+ }
+
+ // We only move a general managed toplevel window if someone has
+ // actually placed the window somewhere. If no placement has taken
+ // place, we just let the window manager Do The Right Thing.
+ if (AreBoundsSane()) {
+ GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size());
+ LOG("nsWindow::Create() Initial resize to %d x %d\n", size.width,
+ size.height);
+ gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
+ }
+ if (mIsPIPWindow) {
+ LOG(" Is PIP window\n");
+ gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_UTILITY);
+ } else if (aInitData && aInitData->mIsAlert) {
+ LOG(" Is alert window\n");
+ gtk_window_set_type_hint(GTK_WINDOW(mShell),
+ GDK_WINDOW_TYPE_HINT_NOTIFICATION);
+ } else if (mWindowType == WindowType::Dialog) {
+ mGtkWindowRoleName = "Dialog";
+
+ SetDefaultIcon();
+ gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_DIALOG);
+ LOG("nsWindow::Create(): dialog");
+ if (parentnsWindow) {
+ GtkWindowSetTransientFor(GTK_WINDOW(mShell),
+ GTK_WINDOW(parentnsWindow->GetGtkWidget()));
+ LOG(" set parent window [%p]\n", parentnsWindow);
+ }
+ } else if (mWindowType == WindowType::Popup) {
+ MOZ_ASSERT(aInitData);
+ mGtkWindowRoleName = "Popup";
+
+ LOG("nsWindow::Create() Popup");
+
+ if (mNoAutoHide) {
+ // ... but the window manager does not decorate this window,
+ // nor provide a separate taskbar icon.
+ if (mBorderStyle == BorderStyle::Default) {
+ gtk_window_set_decorated(GTK_WINDOW(mShell), FALSE);
+ } else {
+ bool decorate = bool(mBorderStyle & BorderStyle::Title);
+ gtk_window_set_decorated(GTK_WINDOW(mShell), decorate);
+ if (decorate) {
+ gtk_window_set_deletable(GTK_WINDOW(mShell),
+ bool(mBorderStyle & BorderStyle::Close));
+ }
+ }
+ gtk_window_set_skip_taskbar_hint(GTK_WINDOW(mShell), TRUE);
+ // Element focus is managed by the parent window so the
+ // WM_HINTS input field is set to False to tell the window
+ // manager not to set input focus to this window ...
+ gtk_window_set_accept_focus(GTK_WINDOW(mShell), FALSE);
+#ifdef MOZ_X11
+ // ... but when the window manager offers focus through
+ // WM_TAKE_FOCUS, focus is requested on the parent window.
+ if (GdkIsX11Display()) {
+ gtk_widget_realize(mShell);
+ gdk_window_add_filter(GetToplevelGdkWindow(), popup_take_focus_filter,
+ nullptr);
+ }
+#endif
+ }
+
+ if (aInitData->mIsDragPopup) {
+ gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_DND);
+ mIsDragPopup = true;
+ LOG("nsWindow::Create() Drag popup\n");
+ } else if (GdkIsX11Display()) {
+ // Set the window hints on X11 only. Wayland popups are configured
+ // at WaylandPopupConfigure().
+ GdkWindowTypeHint gtkTypeHint;
+ switch (mPopupType) {
+ case PopupType::Menu:
+ gtkTypeHint = GDK_WINDOW_TYPE_HINT_POPUP_MENU;
+ break;
+ case PopupType::Tooltip:
+ gtkTypeHint = GDK_WINDOW_TYPE_HINT_TOOLTIP;
+ break;
+ default:
+ gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY;
+ break;
+ }
+ gtk_window_set_type_hint(GTK_WINDOW(mShell), gtkTypeHint);
+ LOG("nsWindow::Create() popup type %s", GetPopupTypeName().get());
+ }
+ if (parentnsWindow) {
+ LOG(" set parent window [%p] %s", parentnsWindow,
+ parentnsWindow->mGtkWindowRoleName.get());
+ GtkWindow* parentWidget = GTK_WINDOW(parentnsWindow->GetGtkWidget());
+ GtkWindowSetTransientFor(GTK_WINDOW(mShell), parentWidget);
+
+ // If popup parent is modal, we need to make popup modal too.
+ if (mPopupType != PopupType::Tooltip &&
+ gtk_window_get_modal(parentWidget)) {
+ gtk_window_set_modal(GTK_WINDOW(mShell), true);
+ }
+ }
+
+ // We need realized mShell at NativeMoveResize().
+ gtk_widget_realize(mShell);
+
+ // With popup windows, we want to set their position.
+ // Place them immediately on X11 and save initial popup position
+ // on Wayland as we place Wayland popup on show.
+ if (GdkIsX11Display()) {
+ NativeMoveResize(/* move */ true, /* resize */ false);
+ } else if (AreBoundsSane()) {
+ GdkRectangle rect = DevicePixelsToGdkRectRoundOut(mBounds);
+ mPopupPosition = {rect.x, rect.y};
+ }
+ } else { // must be WindowType::TopLevel
+ mGtkWindowRoleName = "Toplevel";
+ SetDefaultIcon();
+
+ LOG("nsWindow::Create() Toplevel\n");
+
+ // each toplevel window gets its own window group
+ GtkWindowGroup* group = gtk_window_group_new();
+ gtk_window_group_add_window(group, GTK_WINDOW(mShell));
+ g_object_unref(group);
+ }
+
+ if (mAlwaysOnTop) {
+ gtk_window_set_keep_above(GTK_WINDOW(mShell), TRUE);
+ }
+
+ // It is important that this happens before the realize() call below, so that
+ // we don't get bogus CSD margins on Wayland, see bug 1794577.
+ mUndecorated = IsAlwaysUndecoratedWindow();
+ if (mUndecorated) {
+ LOG(" Is undecorated Window\n");
+ gtk_window_set_titlebar(GTK_WINDOW(mShell), gtk_fixed_new());
+ gtk_window_set_decorated(GTK_WINDOW(mShell), false);
+ }
+
+ // Create a container to hold child windows and child GtkWidgets.
+ GtkWidget* container = moz_container_new();
+ mContainer = MOZ_CONTAINER(container);
+
+ // Prevent GtkWindow from painting a background to avoid flickering.
+ gtk_widget_set_app_paintable(
+ GTK_WIDGET(mContainer),
+ StaticPrefs::widget_transparent_windows_AtStartup());
+
+ gtk_widget_add_events(GTK_WIDGET(mContainer), kEvents);
+ gtk_widget_add_events(mShell, GDK_PROPERTY_CHANGE_MASK);
+ gtk_widget_set_app_paintable(
+ mShell, StaticPrefs::widget_transparent_windows_AtStartup());
+
+ if (mTransparencyBitmapForTitlebar) {
+ moz_container_force_default_visual(mContainer);
+ }
+
+ // If we draw to mContainer window then configure it now because
+ // gtk_container_add() realizes the child widget.
+ gtk_widget_set_has_window(container, true);
+ gtk_container_add(GTK_CONTAINER(mShell), container);
+
+ // alwaysontop windows are generally used for peripheral indicators,
+ // so we don't focus them by default.
+ if (mAlwaysOnTop) {
+ gtk_window_set_focus_on_map(GTK_WINDOW(mShell), FALSE);
+ }
+
+ gtk_widget_realize(container);
+
+ // make sure this is the focus widget in the container
+ gtk_widget_show(container);
+
+ if (!mAlwaysOnTop) {
+ gtk_widget_grab_focus(container);
+ }
+
+#ifdef MOZ_WAYLAND
+ if (mIsDragPopup && GdkIsWaylandDisplay()) {
+ LOG(" set commit to parent");
+ moz_container_wayland_set_commit_to_parent(mContainer);
+ }
+#endif
+
+ if (mWindowType == WindowType::TopLevel && gKioskMode) {
+ if (gKioskMonitor != -1) {
+ mKioskMonitor = Some(gKioskMonitor);
+ LOG(" set kiosk mode monitor %d", mKioskMonitor.value());
+ } else {
+ LOG(" set kiosk mode");
+ }
+ // Kiosk mode always use fullscreen.
+ MakeFullScreen(/* aFullScreen */ true);
+ }
+
+ if (mWindowType == WindowType::Popup) {
+ MOZ_ASSERT(aInitData);
+ // gdk does not automatically set the cursor for "temporary"
+ // windows, which are what gtk uses for popups.
+
+ // force SetCursor to actually set the cursor, even though our internal
+ // state indicates that we already have the standard cursor.
+ mUpdateCursor = true;
+ SetCursor(Cursor{eCursor_standard});
+ }
+
+ if (mIsChildWindow && parentnsWindow) {
+ GdkWindow* window = GetToplevelGdkWindow();
+ GdkWindow* parentWindow = parentnsWindow->GetToplevelGdkWindow();
+ LOG(" child GdkWindow %p set parent GdkWindow %p", window, parentWindow);
+ gdk_window_reparent(window, parentWindow,
+ DevicePixelsToGdkCoordRoundDown(mBounds.x),
+ DevicePixelsToGdkCoordRoundDown(mBounds.y));
+ }
+
+ // Also label mShell toplevel window,
+ // property_notify_event_cb callback also needs to find its way home
+ g_object_set_data(G_OBJECT(GetToplevelGdkWindow()), "nsWindow", this);
+ g_object_set_data(G_OBJECT(mContainer), "nsWindow", this);
+ g_object_set_data(G_OBJECT(mShell), "nsWindow", this);
+
+ // attach listeners for events
+ g_signal_connect(mShell, "configure_event", G_CALLBACK(configure_event_cb),
+ nullptr);
+ g_signal_connect(mShell, "delete_event", G_CALLBACK(delete_event_cb),
+ nullptr);
+ g_signal_connect(mShell, "window_state_event",
+ G_CALLBACK(window_state_event_cb), nullptr);
+ g_signal_connect(mShell, "visibility-notify-event",
+ G_CALLBACK(visibility_notify_event_cb), nullptr);
+ g_signal_connect(mShell, "check-resize", G_CALLBACK(check_resize_cb),
+ nullptr);
+ g_signal_connect(mShell, "composited-changed",
+ G_CALLBACK(widget_composited_changed_cb), nullptr);
+ g_signal_connect(mShell, "property-notify-event",
+ G_CALLBACK(property_notify_event_cb), nullptr);
+
+ if (mWindowType == WindowType::TopLevel) {
+ g_signal_connect_after(mShell, "size_allocate",
+ G_CALLBACK(toplevel_window_size_allocate_cb),
+ nullptr);
+ }
+
+ GdkScreen* screen = gtk_widget_get_screen(mShell);
+ if (!g_signal_handler_find(screen, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr,
+ FuncToGpointer(screen_composited_changed_cb),
+ nullptr)) {
+ g_signal_connect(screen, "composited-changed",
+ G_CALLBACK(screen_composited_changed_cb), nullptr);
+ }
+
+ gtk_drag_dest_set((GtkWidget*)mShell, (GtkDestDefaults)0, nullptr, 0,
+ (GdkDragAction)0);
+ g_signal_connect(mShell, "drag_motion", G_CALLBACK(drag_motion_event_cb),
+ nullptr);
+ g_signal_connect(mShell, "drag_leave", G_CALLBACK(drag_leave_event_cb),
+ nullptr);
+ g_signal_connect(mShell, "drag_drop", G_CALLBACK(drag_drop_event_cb),
+ nullptr);
+ g_signal_connect(mShell, "drag_data_received",
+ G_CALLBACK(drag_data_received_event_cb), nullptr);
+
+ GtkSettings* default_settings = gtk_settings_get_default();
+ g_signal_connect_after(default_settings, "notify::gtk-xft-dpi",
+ G_CALLBACK(settings_xft_dpi_changed_cb), this);
+
+ // Widget signals
+ g_signal_connect(mContainer, "map", G_CALLBACK(widget_map_cb), nullptr);
+ g_signal_connect(mContainer, "unmap", G_CALLBACK(widget_unmap_cb), nullptr);
+ g_signal_connect_after(mContainer, "size_allocate",
+ G_CALLBACK(size_allocate_cb), nullptr);
+ g_signal_connect(mContainer, "hierarchy-changed",
+ G_CALLBACK(hierarchy_changed_cb), nullptr);
+ g_signal_connect(mContainer, "notify::scale-factor",
+ G_CALLBACK(scale_changed_cb), nullptr);
+ // Initialize mHasMappedToplevel.
+ hierarchy_changed_cb(GTK_WIDGET(mContainer), nullptr);
+ // Expose, focus, key, and drag events are sent even to GTK_NO_WINDOW
+ // widgets.
+ g_signal_connect(G_OBJECT(mContainer), "draw", G_CALLBACK(expose_event_cb),
+ nullptr);
+ g_signal_connect(mContainer, "focus_in_event", G_CALLBACK(focus_in_event_cb),
+ nullptr);
+ g_signal_connect(mContainer, "focus_out_event",
+ G_CALLBACK(focus_out_event_cb), nullptr);
+ g_signal_connect(mContainer, "key_press_event",
+ G_CALLBACK(key_press_event_cb), nullptr);
+ g_signal_connect(mContainer, "key_release_event",
+ G_CALLBACK(key_release_event_cb), nullptr);
+
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ gtk_widget_set_double_buffered(GTK_WIDGET(mContainer), FALSE);
+ }
+#endif
+#ifdef MOZ_WAYLAND
+ // Initialize the window specific VsyncSource early in order to avoid races
+ // with BrowserParent::UpdateVsyncParentVsyncDispatcher().
+ // Only use for toplevel windows for now, see bug 1619246.
+ if (GdkIsWaylandDisplay() &&
+ StaticPrefs::widget_wayland_vsync_enabled_AtStartup() &&
+ IsTopLevelWindowType()) {
+ mWaylandVsyncSource = new WaylandVsyncSource(this);
+ mWaylandVsyncDispatcher = new VsyncDispatcher(mWaylandVsyncSource);
+ LOG_VSYNC(" created WaylandVsyncSource");
+ }
+#endif
+
+ // We create input contexts for all containers, except for
+ // toplevel popup windows
+ if (mWindowType != WindowType::Popup) {
+ mIMContext = new IMContextWrapper(this);
+ }
+
+ // These events are sent to the owning widget of the relevant window
+ // and propagate up to the first widget that handles the events, so we
+ // need only connect on mShell, if it exists, to catch events on its
+ // window and windows of mContainer.
+ g_signal_connect(mContainer, "enter-notify-event",
+ G_CALLBACK(enter_notify_event_cb), nullptr);
+ g_signal_connect(mContainer, "leave-notify-event",
+ G_CALLBACK(leave_notify_event_cb), nullptr);
+ g_signal_connect(mContainer, "motion-notify-event",
+ G_CALLBACK(motion_notify_event_cb), nullptr);
+ g_signal_connect(mContainer, "button-press-event",
+ G_CALLBACK(button_press_event_cb), nullptr);
+ g_signal_connect(mContainer, "button-release-event",
+ G_CALLBACK(button_release_event_cb), nullptr);
+ g_signal_connect(mContainer, "scroll-event", G_CALLBACK(scroll_event_cb),
+ nullptr);
+ if (gtk_check_version(3, 18, 0) == nullptr) {
+ g_signal_connect(mContainer, "event", G_CALLBACK(generic_event_cb),
+ nullptr);
+ }
+ g_signal_connect(mContainer, "touch-event", G_CALLBACK(touch_event_cb),
+ nullptr);
+
+ LOG(" nsWindow type %d %s\n", int(mWindowType),
+ mIsPIPWindow ? "PIP window" : "");
+ LOG(" mShell %p (window %p) mContainer %p mGdkWindow %p XID 0x%lx\n", mShell,
+ GetToplevelGdkWindow(), mContainer, mGdkWindow, GetX11Window());
+
+ // Set default application name when it's empty.
+ if (mGtkWindowAppName.IsEmpty()) {
+ mGtkWindowAppName = gAppData->name;
+ }
+
+ mCreated = true;
+ return NS_OK;
+}
+
+void nsWindow::RefreshWindowClass(void) {
+ GdkWindow* gdkWindow = GetToplevelGdkWindow();
+ if (!gdkWindow) {
+ return;
+ }
+
+ if (!mGtkWindowRoleName.IsEmpty()) {
+ gdk_window_set_role(gdkWindow, mGtkWindowRoleName.get());
+ }
+
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ XClassHint* class_hint = XAllocClassHint();
+ if (!class_hint) {
+ return;
+ }
+
+ const char* res_name =
+ !mGtkWindowAppName.IsEmpty() ? mGtkWindowAppName.get() : gAppData->name;
+
+ const char* res_class = !mGtkWindowAppClass.IsEmpty()
+ ? mGtkWindowAppClass.get()
+ : gdk_get_program_class();
+
+ if (!res_name || !res_class) {
+ XFree(class_hint);
+ return;
+ }
+
+ class_hint->res_name = const_cast<char*>(res_name);
+ class_hint->res_class = const_cast<char*>(res_class);
+
+ // Can't use gtk_window_set_wmclass() for this; it prints
+ // a warning & refuses to make the change.
+ GdkDisplay* display = gdk_display_get_default();
+ XSetClassHint(GDK_DISPLAY_XDISPLAY(display),
+ gdk_x11_window_get_xid(gdkWindow), class_hint);
+ XFree(class_hint);
+ }
+#endif /* MOZ_X11 */
+
+#ifdef MOZ_WAYLAND
+ static auto sGdkWaylandWindowSetApplicationId =
+ (void (*)(GdkWindow*, const char*))dlsym(
+ RTLD_DEFAULT, "gdk_wayland_window_set_application_id");
+
+ if (GdkIsWaylandDisplay() && sGdkWaylandWindowSetApplicationId &&
+ !mGtkWindowAppClass.IsEmpty()) {
+ sGdkWaylandWindowSetApplicationId(gdkWindow, mGtkWindowAppClass.get());
+ }
+#endif /* MOZ_WAYLAND */
+}
+
+void nsWindow::SetWindowClass(const nsAString& xulWinType,
+ const nsAString& xulWinClass,
+ const nsAString& xulWinName) {
+ if (!mShell) {
+ return;
+ }
+
+ // If window type attribute is set, parse it into name and role
+ if (!xulWinType.IsEmpty()) {
+ char* res_name = ToNewCString(xulWinType, mozilla::fallible);
+ const char* role = nullptr;
+
+ if (res_name) {
+ // Parse res_name into a name and role. Characters other than
+ // [A-Za-z0-9_-] are converted to '_'. Anything after the first
+ // colon is assigned to role; if there's no colon, assign the
+ // whole thing to both role and res_name.
+ for (char* c = res_name; *c; c++) {
+ if (':' == *c) {
+ *c = 0;
+ role = c + 1;
+ } else if (!isascii(*c) ||
+ (!isalnum(*c) && ('_' != *c) && ('-' != *c))) {
+ *c = '_';
+ }
+ }
+ res_name[0] = (char)toupper(res_name[0]);
+ if (!role) role = res_name;
+
+ mGtkWindowAppName = res_name;
+ mGtkWindowRoleName = role;
+ free(res_name);
+ }
+ }
+
+ // If window class attribute is set, store it as app class
+ // If this attribute is not set, reset app class to default
+ if (!xulWinClass.IsEmpty()) {
+ CopyUTF16toUTF8(xulWinClass, mGtkWindowAppClass);
+ } else {
+ mGtkWindowAppClass = nullptr;
+ }
+
+ // If window class attribute is set, store it as app name
+ // If both name and type are not set, reset app name to default
+ if (!xulWinName.IsEmpty()) {
+ CopyUTF16toUTF8(xulWinName, mGtkWindowAppName);
+ } else if (xulWinType.IsEmpty()) {
+ mGtkWindowAppClass = nullptr;
+ }
+
+ RefreshWindowClass();
+}
+
+nsAutoCString nsWindow::GetDebugTag() const {
+ nsAutoCString tag;
+ tag.AppendPrintf("[%p]", this);
+ return tag;
+}
+
+void nsWindow::NativeMoveResize(bool aMoved, bool aResized) {
+ GdkPoint topLeft = [&] {
+ auto target = mBounds.TopLeft();
+ // gtk_window_move will undo the csd offset, but nothing else, so only add
+ // the client offset if drawing to the csd titlebar.
+ if (DrawsToCSDTitlebar()) {
+ target += mClientOffset;
+ }
+ return DevicePixelsToGdkPointRoundDown(target);
+ }();
+ GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);
+
+ LOG("nsWindow::NativeMoveResize move %d resize %d to %d,%d -> %d x %d\n",
+ aMoved, aResized, topLeft.x, topLeft.y, size.width, size.height);
+
+ if (aResized && !AreBoundsSane()) {
+ LOG(" bounds are insane, hidding the window");
+ // We have been resized but to incorrect size.
+ // If someone has set this so that the needs show flag is false
+ // and it needs to be hidden, update the flag and hide the
+ // window. This flag will be cleared the next time someone
+ // hides the window or shows it. It also prevents us from
+ // calling NativeShow(false) excessively on the window which
+ // causes unneeded X traffic.
+ if (!mNeedsShow && mIsShown) {
+ mNeedsShow = true;
+ NativeShow(false);
+ }
+ if (aMoved) {
+ LOG(" moving to %d x %d", topLeft.x, topLeft.y);
+ gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y);
+ }
+ return;
+ }
+
+ // Set position to hidden window on X11 may fail, so save the position
+ // and move it when it's shown.
+ if (aMoved && GdkIsX11Display() && IsPopup() &&
+ !gtk_widget_get_visible(GTK_WIDGET(mShell))) {
+ LOG(" store position of hidden popup window");
+ mHiddenPopupPositioned = true;
+ mPopupPosition = {topLeft.x, topLeft.y};
+ }
+
+ if (IsWaylandPopup()) {
+ NativeMoveResizeWaylandPopup(aMoved, aResized);
+ } else {
+ // x and y give the position of the window manager frame top-left.
+ if (aMoved) {
+ gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y);
+ }
+ if (aResized) {
+ gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
+ if (mIsDragPopup) {
+ // DND window is placed inside container so we need to make hard size
+ // request to ensure parent container is resized too.
+ gtk_widget_set_size_request(GTK_WIDGET(mShell), size.width,
+ size.height);
+ }
+ }
+ }
+
+ if (aResized) {
+ // Recompute the input region, in case the window grew or shrunk.
+ SetInputRegion(mInputRegion);
+ }
+
+ // Does it need to be shown because bounds were previously insane?
+ if (mNeedsShow && mIsShown && aResized) {
+ NativeShow(true);
+ }
+}
+
+// We pause compositor to avoid rendering of obsoleted remote content which
+// produces flickering.
+// Re-enable compositor again when remote content is updated or
+// timeout happens.
+
+// Define maximal compositor pause when it's paused to avoid flickering,
+// in milliseconds.
+#define COMPOSITOR_PAUSE_TIMEOUT (1000)
+
+void nsWindow::PauseCompositorFlickering() {
+ bool pauseCompositor = IsTopLevelWindowType() &&
+ mCompositorState == COMPOSITOR_ENABLED &&
+ mCompositorWidgetDelegate && !mIsDestroyed;
+ if (!pauseCompositor) {
+ return;
+ }
+
+ LOG("nsWindow::PauseCompositorFlickering()");
+
+ MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove);
+
+ CompositorBridgeChild* remoteRenderer = GetRemoteRenderer();
+ if (remoteRenderer) {
+ remoteRenderer->SendPause();
+ mCompositorState = COMPOSITOR_PAUSED_FLICKERING;
+ mCompositorPauseTimeoutID = (int)g_timeout_add(
+ COMPOSITOR_PAUSE_TIMEOUT,
+ [](void* data) -> gint {
+ nsWindow* window = static_cast<nsWindow*>(data);
+ if (!window->IsDestroyed()) {
+ window->ResumeCompositorFlickering();
+ }
+ return true;
+ },
+ this);
+ }
+}
+
+bool nsWindow::IsWaitingForCompositorResume() {
+ return mCompositorState == COMPOSITOR_PAUSED_FLICKERING;
+}
+
+void nsWindow::ResumeCompositorFlickering() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ LOG("nsWindow::ResumeCompositorFlickering()\n");
+
+ if (mIsDestroyed || !IsWaitingForCompositorResume()) {
+ LOG(" early quit\n");
+ return;
+ }
+
+ MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove);
+
+ // mCompositorWidgetDelegate can be deleted during timeout.
+ // In such case just flip compositor back to enabled and let
+ // SetCompositorWidgetDelegate() or Map event resume it.
+ if (!mCompositorWidgetDelegate) {
+ mCompositorState = COMPOSITOR_ENABLED;
+ return;
+ }
+
+ ResumeCompositorImpl();
+}
+
+void nsWindow::ResumeCompositorFromCompositorThread() {
+ nsCOMPtr<nsIRunnable> event =
+ NewRunnableMethod("nsWindow::ResumeCompositorFlickering", this,
+ &nsWindow::ResumeCompositorFlickering);
+ NS_DispatchToMainThread(event.forget());
+}
+
+void nsWindow::ResumeCompositorImpl() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ LOG("nsWindow::ResumeCompositorImpl()\n");
+
+ MOZ_DIAGNOSTIC_ASSERT(mCompositorWidgetDelegate);
+
+ // DisableRendering() clears stored X11Window so we're sure EnableRendering()
+ // really updates it.
+ mCompositorWidgetDelegate->DisableRendering();
+ mCompositorWidgetDelegate->EnableRendering(GetX11Window(), GetShapedState());
+
+ // As WaylandStartVsync needs mCompositorWidgetDelegate this is the right
+ // time to start it.
+ WaylandStartVsync();
+
+ CompositorBridgeChild* remoteRenderer = GetRemoteRenderer();
+ MOZ_RELEASE_ASSERT(remoteRenderer);
+ remoteRenderer->SendResume();
+ remoteRenderer->SendForcePresent(wr::RenderReasons::WIDGET);
+ mCompositorState = COMPOSITOR_ENABLED;
+}
+
+void nsWindow::WaylandStartVsync() {
+#ifdef MOZ_WAYLAND
+ if (!mWaylandVsyncSource) {
+ return;
+ }
+
+ LOG_VSYNC("nsWindow::WaylandStartVsync");
+
+ MOZ_DIAGNOSTIC_ASSERT(mCompositorWidgetDelegate);
+ if (mCompositorWidgetDelegate->AsGtkCompositorWidget() &&
+ mCompositorWidgetDelegate->AsGtkCompositorWidget()
+ ->GetNativeLayerRoot()) {
+ LOG_VSYNC(" use source NativeLayerRootWayland");
+ mWaylandVsyncSource->MaybeUpdateSource(
+ mCompositorWidgetDelegate->AsGtkCompositorWidget()
+ ->GetNativeLayerRoot()
+ ->AsNativeLayerRootWayland());
+ } else {
+ LOG_VSYNC(" use source mContainer");
+ mWaylandVsyncSource->MaybeUpdateSource(mContainer);
+ }
+
+ mWaylandVsyncSource->EnableMonitor();
+#endif
+}
+
+void nsWindow::WaylandStopVsync() {
+#ifdef MOZ_WAYLAND
+ if (!mWaylandVsyncSource) {
+ return;
+ }
+
+ LOG_VSYNC("nsWindow::WaylandStopVsync");
+
+ // The widget is going to be hidden, so clear the surface of our
+ // vsync source.
+ mWaylandVsyncSource->DisableMonitor();
+ mWaylandVsyncSource->MaybeUpdateSource(nullptr);
+#endif
+}
+
+void nsWindow::NativeShow(bool aAction) {
+ if (aAction) {
+ // unset our flag now that our window has been shown
+ mNeedsShow = true;
+ auto removeShow = MakeScopeExit([&] { mNeedsShow = false; });
+
+ LOG("nsWindow::NativeShow show\n");
+
+ if (IsWaylandPopup()) {
+ mPopupClosed = false;
+ if (WaylandPopupConfigure()) {
+ AddWindowToPopupHierarchy();
+ UpdateWaylandPopupHierarchy();
+ if (mPopupClosed) {
+ return;
+ }
+ }
+ }
+ // Set up usertime/startupID metadata for the created window.
+ // On X11 we use gtk_window_set_startup_id() so we need to call it
+ // before show.
+ if (GdkIsX11Display()) {
+ SetUserTimeAndStartupTokenForActivatedWindow();
+ }
+ if (GdkIsWaylandDisplay()) {
+ if (IsWaylandPopup()) {
+ ShowWaylandPopupWindow();
+ } else {
+ ShowWaylandToplevelWindow();
+ }
+ } else {
+ LOG(" calling gtk_widget_show(mShell)\n");
+ gtk_widget_show(mShell);
+ }
+ if (GdkIsWaylandDisplay()) {
+ SetUserTimeAndStartupTokenForActivatedWindow();
+#ifdef MOZ_WAYLAND
+ auto token = std::move(mWindowActivationTokenFromEnv);
+ if (!token.IsEmpty()) {
+ FocusWaylandWindow(token.get());
+ }
+#endif
+ }
+ if (mHiddenPopupPositioned && IsPopup()) {
+ LOG(" re-position hidden popup window");
+ gtk_window_move(GTK_WINDOW(mShell), mPopupPosition.x, mPopupPosition.y);
+ mHiddenPopupPositioned = false;
+ }
+ } else {
+ LOG("nsWindow::NativeShow hide\n");
+ if (GdkIsWaylandDisplay()) {
+ if (IsWaylandPopup()) {
+ // We can't close tracked popups directly as they may have visible
+ // child popups. Just mark is as closed and let
+ // UpdateWaylandPopupHierarchy() do the job.
+ if (IsInPopupHierarchy()) {
+ WaylandPopupMarkAsClosed();
+ UpdateWaylandPopupHierarchy();
+ } else {
+ // Close untracked popups directly.
+ HideWaylandPopupWindow(/* aTemporaryHide */ false,
+ /* aRemoveFromPopupList */ true);
+ }
+ } else {
+ HideWaylandToplevelWindow();
+ }
+ } else {
+ // Workaround window freezes on GTK versions before 3.21.2 by
+ // ensuring that configure events get dispatched to windows before
+ // they are unmapped. See bug 1225044.
+ if (gtk_check_version(3, 21, 2) != nullptr && mPendingConfigures > 0) {
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(mShell), &allocation);
+
+ GdkEventConfigure event;
+ PodZero(&event);
+ event.type = GDK_CONFIGURE;
+ event.window = mGdkWindow;
+ event.send_event = TRUE;
+ event.x = allocation.x;
+ event.y = allocation.y;
+ event.width = allocation.width;
+ event.height = allocation.height;
+
+ auto* shellClass = GTK_WIDGET_GET_CLASS(mShell);
+ for (unsigned int i = 0; i < mPendingConfigures; i++) {
+ Unused << shellClass->configure_event(mShell, &event);
+ }
+ mPendingConfigures = 0;
+ }
+ gtk_widget_hide(mShell);
+
+ ClearTransparencyBitmap(); // Release some resources
+ }
+ }
+}
+
+void nsWindow::SetHasMappedToplevel(bool aState) {
+ LOG("nsWindow::SetHasMappedToplevel(%d)", aState);
+ if (aState == mHasMappedToplevel) {
+ return;
+ }
+ // Even when aState == mHasMappedToplevel (as when this method is called
+ // from Show()), child windows need to have their state checked, so don't
+ // return early.
+ mHasMappedToplevel = aState;
+ if (aState && mNeedsToRetryCapturingMouse) {
+ CaptureRollupEvents(true);
+ MOZ_ASSERT(!mNeedsToRetryCapturingMouse);
+ }
+}
+
+LayoutDeviceIntSize nsWindow::GetSafeWindowSize(LayoutDeviceIntSize aSize) {
+ // The X protocol uses CARD32 for window sizes, but the server (1.11.3)
+ // reads it as CARD16. Sizes of pixmaps, used for drawing, are (unsigned)
+ // CARD16 in the protocol, but the server's ProcCreatePixmap returns
+ // BadAlloc if dimensions cannot be represented by signed shorts.
+ // Because we are creating Cairo surfaces to represent window buffers,
+ // we also must ensure that the window can fit in a Cairo surface.
+ LayoutDeviceIntSize result = aSize;
+ int32_t maxSize = 32767;
+ if (mWindowRenderer && mWindowRenderer->AsKnowsCompositor()) {
+ maxSize = std::min(
+ maxSize, mWindowRenderer->AsKnowsCompositor()->GetMaxTextureSize());
+ }
+ if (result.width > maxSize) {
+ result.width = maxSize;
+ }
+ if (result.height > maxSize) {
+ result.height = maxSize;
+ }
+ return result;
+}
+
+void nsWindow::SetTransparencyMode(TransparencyMode aMode) {
+ bool isTransparent = aMode == TransparencyMode::Transparent;
+
+ if (mIsTransparent == isTransparent) {
+ return;
+ }
+
+ if (mWindowType != WindowType::Popup) {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1344839 reported
+ // problems cleaning the layer manager for toplevel windows.
+ // Ignore the request so as to workaround that.
+ // mIsTransparent is set in Create() if transparency may be required.
+ if (isTransparent) {
+ NS_WARNING("Transparent mode not supported on non-popup windows.");
+ }
+ return;
+ }
+
+ if (!isTransparent) {
+ ClearTransparencyBitmap();
+ } // else the new default alpha values are "all 1", so we don't
+ // need to change anything yet
+
+ mIsTransparent = isTransparent;
+
+ if (!mHasAlphaVisual) {
+ // The choice of layer manager depends on
+ // GtkCompositorWidgetInitData::Shaped(), which will need to change, so
+ // clean out the old layer manager.
+ DestroyLayerManager();
+ }
+}
+
+TransparencyMode nsWindow::GetTransparencyMode() {
+ return mIsTransparent ? TransparencyMode::Transparent
+ : TransparencyMode::Opaque;
+}
+
+gint nsWindow::GetInputRegionMarginInGdkCoords() {
+ return DevicePixelsToGdkCoordRoundDown(mInputRegion.mMargin);
+}
+
+void nsWindow::SetInputRegion(const InputRegion& aInputRegion) {
+ mInputRegion = aInputRegion;
+
+ GdkWindow* window = GetToplevelGdkWindow();
+ if (!window) {
+ return;
+ }
+
+ LOG("nsWindow::SetInputRegion(%d, %d)", aInputRegion.mFullyTransparent,
+ int(aInputRegion.mMargin));
+
+ cairo_rectangle_int_t rect = {0, 0, 0, 0};
+ cairo_region_t* region = nullptr;
+ auto releaseRegion = MakeScopeExit([&] {
+ if (region) {
+ cairo_region_destroy(region);
+ }
+ });
+
+ if (aInputRegion.mFullyTransparent) {
+ region = cairo_region_create_rectangle(&rect);
+ } else if (aInputRegion.mMargin != 0) {
+ LayoutDeviceIntRect inputRegion(LayoutDeviceIntPoint(), mLastSizeRequest);
+ inputRegion.Deflate(aInputRegion.mMargin);
+ GdkRectangle gdkRect = DevicePixelsToGdkRectRoundOut(inputRegion);
+ rect = {gdkRect.x, gdkRect.y, gdkRect.width, gdkRect.height};
+ region = cairo_region_create_rectangle(&rect);
+ }
+
+ gdk_window_input_shape_combine_region(window, region, 0, 0);
+
+ // On Wayland gdk_window_input_shape_combine_region() call is cached and
+ // applied to underlying wl_surface when GdkWindow is repainted.
+ // Force repaint of GdkWindow to apply the change immediately.
+ if (GdkIsWaylandDisplay()) {
+ gdk_window_invalidate_rect(window, nullptr, false);
+ }
+}
+
+// For setting the draggable titlebar region from CSS
+// with -moz-window-dragging: drag.
+void nsWindow::UpdateWindowDraggingRegion(
+ const LayoutDeviceIntRegion& aRegion) {
+ if (mDraggableRegion != aRegion) {
+ mDraggableRegion = aRegion;
+ }
+}
+
+LayoutDeviceIntCoord nsWindow::GetTitlebarRadius() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ int32_t cssCoord = LookAndFeel::GetInt(LookAndFeel::IntID::TitlebarRadius);
+ return GdkCoordToDevicePixels(cssCoord);
+}
+
+// See subtract_corners_from_region() at gtk/gtkwindow.c
+// We need to subtract corners from toplevel window opaque region
+// to draw transparent corners of default Gtk titlebar.
+// Both implementations (cairo_region_t and wl_region) needs to be synced.
+static void SubtractTitlebarCorners(cairo_region_t* aRegion, int aX, int aY,
+ int aWindowWidth, int aWindowHeight,
+ int aTitlebarRadius) {
+ if (!aTitlebarRadius) {
+ return;
+ }
+ cairo_rectangle_int_t rect = {aX, aY, aTitlebarRadius, aTitlebarRadius};
+ cairo_region_subtract_rectangle(aRegion, &rect);
+ rect = {
+ aX + aWindowWidth - aTitlebarRadius,
+ aY,
+ aTitlebarRadius,
+ aTitlebarRadius,
+ };
+ cairo_region_subtract_rectangle(aRegion, &rect);
+ rect = {
+ aX,
+ aY + aWindowHeight - aTitlebarRadius,
+ aTitlebarRadius,
+ aTitlebarRadius,
+ };
+ cairo_region_subtract_rectangle(aRegion, &rect);
+ rect = {
+ aX + aWindowWidth - aTitlebarRadius,
+ aY + aWindowHeight - aTitlebarRadius,
+ aTitlebarRadius,
+ aTitlebarRadius,
+ };
+ cairo_region_subtract_rectangle(aRegion, &rect);
+}
+
+void nsWindow::UpdateTopLevelOpaqueRegion() {
+ if (!mCompositedScreen) {
+ return;
+ }
+
+ GdkWindow* window = GetToplevelGdkWindow();
+ if (!window) {
+ return;
+ }
+ MOZ_ASSERT(gdk_window_get_window_type(window) == GDK_WINDOW_TOPLEVEL);
+
+ int x = 0;
+ int y = 0;
+
+ gdk_window_get_position(mGdkWindow, &x, &y);
+
+ int width = DevicePixelsToGdkCoordRoundDown(mBounds.width);
+ int height = DevicePixelsToGdkCoordRoundDown(mBounds.height);
+
+ cairo_region_t* region = cairo_region_create();
+ cairo_rectangle_int_t rect = {x, y, width, height};
+ cairo_region_union_rectangle(region, &rect);
+
+ // TODO: We actually could get a proper opaque region from layout, see
+ // nsIWidget::UpdateOpaqueRegion. This could simplify titlebar drawing.
+ int radius = DoDrawTilebarCorners() ? int(GetTitlebarRadius()) : 0;
+ SubtractTitlebarCorners(region, x, y, width, height, radius);
+
+ gdk_window_set_opaque_region(window, region);
+
+ cairo_region_destroy(region);
+
+#ifdef MOZ_WAYLAND
+ if (GdkIsWaylandDisplay()) {
+ moz_container_wayland_update_opaque_region(mContainer, radius);
+ }
+#endif
+
+ SetTitlebarRect();
+}
+
+bool nsWindow::IsChromeWindowTitlebar() {
+ return mDrawInTitlebar && !mIsPIPWindow &&
+ mWindowType == WindowType::TopLevel;
+}
+
+bool nsWindow::DoDrawTilebarCorners() {
+ return IsChromeWindowTitlebar() && mSizeMode == nsSizeMode_Normal &&
+ !mIsTiled;
+}
+
+void nsWindow::ResizeTransparencyBitmap() {
+ if (!mTransparencyBitmap) {
+ return;
+ }
+
+ if (mBounds.width == mTransparencyBitmapWidth &&
+ mBounds.height == mTransparencyBitmapHeight) {
+ return;
+ }
+
+ int32_t newRowBytes = GetBitmapStride(mBounds.width);
+ int32_t newSize = newRowBytes * mBounds.height;
+ auto* newBits = new gchar[newSize];
+ // fill new mask with "transparent", first
+ memset(newBits, 0, newSize);
+
+ // Now copy the intersection of the old and new areas into the new mask
+ int32_t copyWidth = std::min(mBounds.width, mTransparencyBitmapWidth);
+ int32_t copyHeight = std::min(mBounds.height, mTransparencyBitmapHeight);
+ int32_t oldRowBytes = GetBitmapStride(mTransparencyBitmapWidth);
+ int32_t copyBytes = GetBitmapStride(copyWidth);
+
+ int32_t i;
+ gchar* fromPtr = mTransparencyBitmap;
+ gchar* toPtr = newBits;
+ for (i = 0; i < copyHeight; i++) {
+ memcpy(toPtr, fromPtr, copyBytes);
+ fromPtr += oldRowBytes;
+ toPtr += newRowBytes;
+ }
+
+ delete[] mTransparencyBitmap;
+ mTransparencyBitmap = newBits;
+ mTransparencyBitmapWidth = mBounds.width;
+ mTransparencyBitmapHeight = mBounds.height;
+}
+
+static bool ChangedMaskBits(gchar* aMaskBits, int32_t aMaskWidth,
+ int32_t aMaskHeight, const nsIntRect& aRect,
+ uint8_t* aAlphas, int32_t aStride) {
+ int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost();
+ int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth);
+ for (y = aRect.y; y < yMax; y++) {
+ gchar* maskBytes = aMaskBits + y * maskBytesPerRow;
+ uint8_t* alphas = aAlphas;
+ for (x = aRect.x; x < xMax; x++) {
+ bool newBit = *alphas > 0x7f;
+ alphas++;
+
+ gchar maskByte = maskBytes[x >> 3];
+ bool maskBit = (maskByte & (1 << (x & 7))) != 0;
+
+ if (maskBit != newBit) {
+ return true;
+ }
+ }
+ aAlphas += aStride;
+ }
+
+ return false;
+}
+
+static void UpdateMaskBits(gchar* aMaskBits, int32_t aMaskWidth,
+ int32_t aMaskHeight, const nsIntRect& aRect,
+ uint8_t* aAlphas, int32_t aStride) {
+ int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost();
+ int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth);
+ for (y = aRect.y; y < yMax; y++) {
+ gchar* maskBytes = aMaskBits + y * maskBytesPerRow;
+ uint8_t* alphas = aAlphas;
+ for (x = aRect.x; x < xMax; x++) {
+ bool newBit = *alphas > 0x7f;
+ alphas++;
+
+ gchar mask = 1 << (x & 7);
+ gchar maskByte = maskBytes[x >> 3];
+ // Note: '-newBit' turns 0 into 00...00 and 1 into 11...11
+ maskBytes[x >> 3] = (maskByte & ~mask) | (-newBit & mask);
+ }
+ aAlphas += aStride;
+ }
+}
+
+void nsWindow::ApplyTransparencyBitmap() {
+#ifdef MOZ_X11
+ // We use X11 calls where possible, because GDK handles expose events
+ // for shaped windows in a way that's incompatible with us (Bug 635903).
+ // It doesn't occur when the shapes are set through X.
+ Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow);
+ Window xDrawable = GDK_WINDOW_XID(mGdkWindow);
+ Pixmap maskPixmap = XCreateBitmapFromData(
+ xDisplay, xDrawable, mTransparencyBitmap, mTransparencyBitmapWidth,
+ mTransparencyBitmapHeight);
+ XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap,
+ ShapeSet);
+ XFreePixmap(xDisplay, maskPixmap);
+#endif // MOZ_X11
+}
+
+void nsWindow::ClearTransparencyBitmap() {
+ if (!mTransparencyBitmap) {
+ return;
+ }
+
+ delete[] mTransparencyBitmap;
+ mTransparencyBitmap = nullptr;
+ mTransparencyBitmapWidth = 0;
+ mTransparencyBitmapHeight = 0;
+
+ if (!mShell) {
+ return;
+ }
+
+#ifdef MOZ_X11
+ if (MOZ_UNLIKELY(!mGdkWindow)) {
+ return;
+ }
+
+ Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow);
+ Window xWindow = gdk_x11_window_get_xid(mGdkWindow);
+
+ XShapeCombineMask(xDisplay, xWindow, ShapeBounding, 0, 0, X11None, ShapeSet);
+#endif
+}
+
+nsresult nsWindow::UpdateTranslucentWindowAlphaInternal(const nsIntRect& aRect,
+ uint8_t* aAlphas,
+ int32_t aStride) {
+ NS_ASSERTION(mIsTransparent, "Window is not transparent");
+ NS_ASSERTION(!mTransparencyBitmapForTitlebar,
+ "Transparency bitmap is already used for titlebar rendering");
+
+ if (mTransparencyBitmap == nullptr) {
+ int32_t size = GetBitmapStride(mBounds.width) * mBounds.height;
+ mTransparencyBitmap = new gchar[size];
+ memset(mTransparencyBitmap, 255, size);
+ mTransparencyBitmapWidth = mBounds.width;
+ mTransparencyBitmapHeight = mBounds.height;
+ } else {
+ ResizeTransparencyBitmap();
+ }
+
+ nsIntRect rect;
+ rect.IntersectRect(aRect, nsIntRect(0, 0, mBounds.width, mBounds.height));
+
+ if (!ChangedMaskBits(mTransparencyBitmap, mBounds.width, mBounds.height, rect,
+ aAlphas, aStride)) {
+ // skip the expensive stuff if the mask bits haven't changed; hopefully
+ // this is the common case
+ return NS_OK;
+ }
+
+ UpdateMaskBits(mTransparencyBitmap, mBounds.width, mBounds.height, rect,
+ aAlphas, aStride);
+
+ if (!mNeedsShow) {
+ ApplyTransparencyBitmap();
+ }
+ return NS_OK;
+}
+
+#define TITLEBAR_HEIGHT 10
+
+void nsWindow::SetTitlebarRect() {
+ MutexAutoLock lock(mTitlebarRectMutex);
+
+ if (!mGdkWindow || !DoDrawTilebarCorners()) {
+ mTitlebarRect = LayoutDeviceIntRect();
+ return;
+ }
+ mTitlebarRect = LayoutDeviceIntRect(0, 0, mBounds.width,
+ GdkCeiledScaleFactor() * TITLEBAR_HEIGHT);
+}
+
+LayoutDeviceIntRect nsWindow::GetTitlebarRect() {
+ MutexAutoLock lock(mTitlebarRectMutex);
+ return mTitlebarRect;
+}
+
+void nsWindow::UpdateTitlebarTransparencyBitmap() {
+ NS_ASSERTION(mTransparencyBitmapForTitlebar,
+ "Transparency bitmap is already used to draw window shape");
+
+ if (!mGdkWindow || !mDrawInTitlebar ||
+ (mBounds.width == mTransparencyBitmapWidth &&
+ mBounds.height == mTransparencyBitmapHeight)) {
+ return;
+ }
+
+ bool maskCreate =
+ !mTransparencyBitmap || mBounds.width > mTransparencyBitmapWidth;
+
+ bool maskUpdate =
+ !mTransparencyBitmap || mBounds.width != mTransparencyBitmapWidth;
+
+ LayoutDeviceIntCoord radius = GetTitlebarRadius();
+ if (maskCreate) {
+ delete[] mTransparencyBitmap;
+ int32_t size = GetBitmapStride(mBounds.width) * radius;
+ mTransparencyBitmap = new gchar[size];
+ mTransparencyBitmapWidth = mBounds.width;
+ } else {
+ mTransparencyBitmapWidth = mBounds.width;
+ }
+ mTransparencyBitmapHeight = mBounds.height;
+
+ if (maskUpdate) {
+ cairo_surface_t* surface = cairo_image_surface_create(
+ CAIRO_FORMAT_A8, mTransparencyBitmapWidth, radius);
+ if (!surface) {
+ return;
+ }
+
+ cairo_t* cr = cairo_create(surface);
+
+ GtkWidgetState state;
+ memset((void*)&state, 0, sizeof(state));
+ GdkRectangle rect = {0, 0, mTransparencyBitmapWidth, radius};
+
+ moz_gtk_widget_paint(MOZ_GTK_HEADER_BAR, cr, &rect, &state, 0,
+ GTK_TEXT_DIR_NONE);
+
+ cairo_destroy(cr);
+ cairo_surface_mark_dirty(surface);
+ cairo_surface_flush(surface);
+
+ UpdateMaskBits(mTransparencyBitmap, mTransparencyBitmapWidth, radius,
+ nsIntRect(0, 0, mTransparencyBitmapWidth, radius),
+ cairo_image_surface_get_data(surface),
+ cairo_format_stride_for_width(CAIRO_FORMAT_A8,
+ mTransparencyBitmapWidth));
+
+ cairo_surface_destroy(surface);
+ }
+
+#ifdef MOZ_X11
+ if (!mNeedsShow) {
+ Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow);
+ Window xDrawable = GDK_WINDOW_XID(mGdkWindow);
+
+ Pixmap maskPixmap =
+ XCreateBitmapFromData(xDisplay, xDrawable, mTransparencyBitmap,
+ mTransparencyBitmapWidth, radius);
+
+ XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap,
+ ShapeSet);
+
+ if (mTransparencyBitmapHeight > radius) {
+ XRectangle rect = {0, 0, (unsigned short)mTransparencyBitmapWidth,
+ (unsigned short)(mTransparencyBitmapHeight - radius)};
+ XShapeCombineRectangles(xDisplay, xDrawable, ShapeBounding, 0, radius,
+ &rect, 1, ShapeUnion, 0);
+ }
+
+ XFreePixmap(xDisplay, maskPixmap);
+ }
+#endif
+}
+
+GtkWidget* nsWindow::GetToplevelWidget() const { return mShell; }
+
+GdkWindow* nsWindow::GetToplevelGdkWindow() const {
+ return gtk_widget_get_window(mShell);
+}
+
+nsWindow* nsWindow::GetContainerWindow() const {
+ GtkWidget* owningWidget = GTK_WIDGET(mContainer);
+ if (!owningWidget) {
+ return nullptr;
+ }
+
+ nsWindow* window = get_window_for_gtk_widget(owningWidget);
+ NS_ASSERTION(window, "No nsWindow for container widget");
+ return window;
+}
+
+void nsWindow::SetUrgencyHint(GtkWidget* top_window, bool state) {
+ LOG(" nsWindow::SetUrgencyHint widget %p\n", top_window);
+ if (!top_window) {
+ return;
+ }
+ GdkWindow* window = gtk_widget_get_window(top_window);
+ if (!window) {
+ return;
+ }
+ // TODO: Use xdg-activation on Wayland?
+ gdk_window_set_urgency_hint(window, state);
+}
+
+void nsWindow::SetDefaultIcon(void) { SetIcon(u"default"_ns); }
+
+gint nsWindow::ConvertBorderStyles(BorderStyle aStyle) {
+ gint w = 0;
+
+ if (aStyle == BorderStyle::Default) {
+ return -1;
+ }
+
+ // note that we don't handle BorderStyle::Close yet
+ if (aStyle & BorderStyle::All) w |= GDK_DECOR_ALL;
+ if (aStyle & BorderStyle::Border) w |= GDK_DECOR_BORDER;
+ if (aStyle & BorderStyle::ResizeH) w |= GDK_DECOR_RESIZEH;
+ if (aStyle & BorderStyle::Title) w |= GDK_DECOR_TITLE;
+ if (aStyle & BorderStyle::Menu) w |= GDK_DECOR_MENU;
+ if (aStyle & BorderStyle::Minimize) w |= GDK_DECOR_MINIMIZE;
+ if (aStyle & BorderStyle::Maximize) w |= GDK_DECOR_MAXIMIZE;
+
+ return w;
+}
+
+class FullscreenTransitionWindow final : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit FullscreenTransitionWindow(GtkWidget* aWidget);
+
+ GtkWidget* mWindow;
+
+ private:
+ ~FullscreenTransitionWindow();
+};
+
+NS_IMPL_ISUPPORTS0(FullscreenTransitionWindow)
+
+FullscreenTransitionWindow::FullscreenTransitionWindow(GtkWidget* aWidget) {
+ mWindow = gtk_window_new(GTK_WINDOW_POPUP);
+ GtkWindow* gtkWin = GTK_WINDOW(mWindow);
+
+ gtk_window_set_type_hint(gtkWin, GDK_WINDOW_TYPE_HINT_SPLASHSCREEN);
+ GtkWindowSetTransientFor(gtkWin, GTK_WINDOW(aWidget));
+ gtk_window_set_decorated(gtkWin, false);
+
+ GdkWindow* gdkWin = gtk_widget_get_window(aWidget);
+ GdkScreen* screen = gtk_widget_get_screen(aWidget);
+ gint monitorNum = gdk_screen_get_monitor_at_window(screen, gdkWin);
+ GdkRectangle monitorRect;
+ gdk_screen_get_monitor_geometry(screen, monitorNum, &monitorRect);
+ gtk_window_set_screen(gtkWin, screen);
+ gtk_window_move(gtkWin, monitorRect.x, monitorRect.y);
+ MOZ_ASSERT(monitorRect.width > 0 && monitorRect.height > 0,
+ "Can't resize window smaller than 1x1.");
+ gtk_window_resize(gtkWin, monitorRect.width, monitorRect.height);
+
+ GdkRGBA bgColor;
+ bgColor.red = bgColor.green = bgColor.blue = 0.0;
+ bgColor.alpha = 1.0;
+ gtk_widget_override_background_color(mWindow, GTK_STATE_FLAG_NORMAL,
+ &bgColor);
+
+ gtk_widget_set_opacity(mWindow, 0.0);
+ gtk_widget_show(mWindow);
+}
+
+FullscreenTransitionWindow::~FullscreenTransitionWindow() {
+ gtk_widget_destroy(mWindow);
+}
+
+class FullscreenTransitionData {
+ public:
+ FullscreenTransitionData(nsIWidget::FullscreenTransitionStage aStage,
+ uint16_t aDuration, nsIRunnable* aCallback,
+ FullscreenTransitionWindow* aWindow)
+ : mStage(aStage),
+ mStartTime(TimeStamp::Now()),
+ mDuration(TimeDuration::FromMilliseconds(aDuration)),
+ mCallback(aCallback),
+ mWindow(aWindow) {}
+
+ static const guint sInterval = 1000 / 30; // 30fps
+ static gboolean TimeoutCallback(gpointer aData);
+
+ private:
+ nsIWidget::FullscreenTransitionStage mStage;
+ TimeStamp mStartTime;
+ TimeDuration mDuration;
+ nsCOMPtr<nsIRunnable> mCallback;
+ RefPtr<FullscreenTransitionWindow> mWindow;
+};
+
+/* static */
+gboolean FullscreenTransitionData::TimeoutCallback(gpointer aData) {
+ bool finishing = false;
+ auto* data = static_cast<FullscreenTransitionData*>(aData);
+ gdouble opacity = (TimeStamp::Now() - data->mStartTime) / data->mDuration;
+ if (opacity >= 1.0) {
+ opacity = 1.0;
+ finishing = true;
+ }
+ if (data->mStage == nsIWidget::eAfterFullscreenToggle) {
+ opacity = 1.0 - opacity;
+ }
+ gtk_widget_set_opacity(data->mWindow->mWindow, opacity);
+
+ if (!finishing) {
+ return TRUE;
+ }
+ NS_DispatchToMainThread(data->mCallback.forget());
+ delete data;
+ return FALSE;
+}
+
+/* virtual */
+bool nsWindow::PrepareForFullscreenTransition(nsISupports** aData) {
+ if (!mCompositedScreen) {
+ return false;
+ }
+ *aData = do_AddRef(new FullscreenTransitionWindow(mShell)).take();
+ return true;
+}
+
+/* virtual */
+void nsWindow::PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration,
+ nsISupports* aData,
+ nsIRunnable* aCallback) {
+ auto* data = static_cast<FullscreenTransitionWindow*>(aData);
+ // This will be released at the end of the last timeout callback for it.
+ auto* transitionData =
+ new FullscreenTransitionData(aStage, aDuration, aCallback, data);
+ g_timeout_add_full(G_PRIORITY_HIGH, FullscreenTransitionData::sInterval,
+ FullscreenTransitionData::TimeoutCallback, transitionData,
+ nullptr);
+}
+
+already_AddRefed<widget::Screen> nsWindow::GetWidgetScreen() {
+ // Wayland can read screen directly
+ if (GdkIsWaylandDisplay()) {
+ if (RefPtr<Screen> screen = ScreenHelperGTK::GetScreenForWindow(this)) {
+ return screen.forget();
+ }
+ }
+
+ // GetScreenBounds() is slow for the GTK port so we override and use
+ // mBounds directly.
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+ LayoutDeviceIntRect bounds = mBounds;
+ DesktopIntRect deskBounds = RoundedToInt(bounds / GetDesktopToDeviceScale());
+ return screenManager.ScreenForRect(deskBounds);
+}
+
+RefPtr<VsyncDispatcher> nsWindow::GetVsyncDispatcher() {
+#ifdef MOZ_WAYLAND
+ if (mWaylandVsyncDispatcher) {
+ return mWaylandVsyncDispatcher;
+ }
+#endif
+ return nullptr;
+}
+
+bool nsWindow::SynchronouslyRepaintOnResize() {
+ if (GdkIsWaylandDisplay()) {
+ // See Bug 1734368
+ // Don't request synchronous repaint on HW accelerated backend - mesa can be
+ // deadlocked when it's missing back buffer and main event loop is blocked.
+ return false;
+ }
+
+ // default is synced repaint.
+ return true;
+}
+
+void nsWindow::KioskLockOnMonitor() {
+ // Available as of GTK 3.18+
+ static auto sGdkWindowFullscreenOnMonitor =
+ (void (*)(GdkWindow* window, gint monitor))dlsym(
+ RTLD_DEFAULT, "gdk_window_fullscreen_on_monitor");
+
+ if (!sGdkWindowFullscreenOnMonitor) {
+ return;
+ }
+
+ int monitor = mKioskMonitor.value();
+ if (monitor < 0 || monitor >= ScreenHelperGTK::GetMonitorCount()) {
+ LOG("nsWindow::KioskLockOnMonitor() wrong monitor number! (%d)\n", monitor);
+ return;
+ }
+
+ LOG("nsWindow::KioskLockOnMonitor() locked on %d\n", monitor);
+ sGdkWindowFullscreenOnMonitor(GetToplevelGdkWindow(), monitor);
+}
+
+static bool IsFullscreenSupported(GtkWidget* aShell) {
+#ifdef MOZ_X11
+ GdkScreen* screen = gtk_widget_get_screen(aShell);
+ GdkAtom atom = gdk_atom_intern("_NET_WM_STATE_FULLSCREEN", FALSE);
+ return gdk_x11_screen_supports_net_wm_hint(screen, atom);
+#else
+ return true;
+#endif
+}
+
+nsresult nsWindow::MakeFullScreen(bool aFullScreen) {
+ LOG("nsWindow::MakeFullScreen aFullScreen %d\n", aFullScreen);
+
+ if (GdkIsX11Display() && !IsFullscreenSupported(mShell)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aFullScreen) {
+ if (mSizeMode != nsSizeMode_Fullscreen &&
+ mSizeMode != nsSizeMode_Minimized) {
+ mLastSizeModeBeforeFullscreen = mSizeMode;
+ }
+ if (mIsPIPWindow) {
+ gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_NORMAL);
+ if (gUseAspectRatio) {
+ mAspectRatioSaved = mAspectRatio;
+ mAspectRatio = 0.0f;
+ ApplySizeConstraints();
+ }
+ }
+
+ if (mKioskMonitor.isSome()) {
+ KioskLockOnMonitor();
+ } else {
+ gtk_window_fullscreen(GTK_WINDOW(mShell));
+ }
+ } else {
+ // Kiosk mode always use fullscreen mode.
+ if (gKioskMode) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ gtk_window_unfullscreen(GTK_WINDOW(mShell));
+
+ if (mIsPIPWindow && gUseAspectRatio) {
+ mAspectRatio = mAspectRatioSaved;
+ // ApplySizeConstraints();
+ }
+ }
+
+ MOZ_ASSERT(mLastSizeModeBeforeFullscreen != nsSizeMode_Fullscreen);
+ return NS_OK;
+}
+
+void nsWindow::SetWindowDecoration(BorderStyle aStyle) {
+ LOG("nsWindow::SetWindowDecoration() Border style %x\n", int(aStyle));
+
+ // We can't use mGdkWindow directly here as it can be
+ // derived from mContainer which is not a top-level GdkWindow.
+ GdkWindow* window = GetToplevelGdkWindow();
+
+ // Sawfish, metacity, and presumably other window managers get
+ // confused if we change the window decorations while the window
+ // is visible.
+ bool wasVisible = false;
+ if (gdk_window_is_visible(window)) {
+ gdk_window_hide(window);
+ wasVisible = true;
+ }
+
+ gint wmd = ConvertBorderStyles(aStyle);
+ if (wmd != -1) gdk_window_set_decorations(window, (GdkWMDecoration)wmd);
+
+ if (wasVisible) gdk_window_show(window);
+
+ // For some window managers, adding or removing window decorations
+ // requires unmapping and remapping our toplevel window. Go ahead
+ // and flush the queue here so that we don't end up with a BadWindow
+ // error later when this happens (when the persistence timer fires
+ // and GetWindowPos is called)
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ XSync(GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), X11False);
+ } else
+#endif /* MOZ_X11 */
+ {
+ gdk_flush();
+ }
+}
+
+void nsWindow::HideWindowChrome(bool aShouldHide) {
+ SetWindowDecoration(aShouldHide ? BorderStyle::None : mBorderStyle);
+}
+
+bool nsWindow::CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel,
+ bool aAlwaysRollup) {
+ LOG("nsWindow::CheckForRollup() aAlwaysRollup %d", aAlwaysRollup);
+ nsIRollupListener* rollupListener = GetActiveRollupListener();
+ nsCOMPtr<nsIWidget> rollupWidget;
+ if (rollupListener) {
+ rollupWidget = rollupListener->GetRollupWidget();
+ }
+ if (!rollupWidget) {
+ return false;
+ }
+
+ auto* rollupWindow =
+ (GdkWindow*)rollupWidget->GetNativeData(NS_NATIVE_WINDOW);
+ if (!aAlwaysRollup && is_mouse_in_window(rollupWindow, aMouseX, aMouseY)) {
+ return false;
+ }
+ bool retVal = false;
+ if (aIsWheel) {
+ retVal = rollupListener->ShouldConsumeOnMouseWheelEvent();
+ if (!rollupListener->ShouldRollupOnMouseWheelEvent()) {
+ return retVal;
+ }
+ }
+ LayoutDeviceIntPoint point;
+ nsIRollupListener::RollupOptions options{0,
+ nsIRollupListener::FlushViews::Yes};
+ // if we're dealing with menus, we probably have submenus and
+ // we don't want to rollup if the click is in a parent menu of
+ // the current submenu
+ if (!aAlwaysRollup) {
+ AutoTArray<nsIWidget*, 5> widgetChain;
+ uint32_t sameTypeCount =
+ rollupListener->GetSubmenuWidgetChain(&widgetChain);
+ for (unsigned long i = 0; i < widgetChain.Length(); ++i) {
+ nsIWidget* widget = widgetChain[i];
+ auto* currWindow = (GdkWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (is_mouse_in_window(currWindow, aMouseX, aMouseY)) {
+ // Don't roll up if the mouse event occurred within a menu of the same
+ // type.
+ // If the mouse event occurred in a menu higher than that, roll up, but
+ // pass the number of popups to Rollup so that only those of the same
+ // type close up.
+ if (i < sameTypeCount) {
+ return retVal;
+ }
+ options.mCount = sameTypeCount;
+ break;
+ }
+ } // foreach parent menu widget
+ if (!aIsWheel) {
+ point = GdkEventCoordsToDevicePixels(aMouseX, aMouseY);
+ options.mPoint = &point;
+ }
+ }
+
+ if (mSizeMode == nsSizeMode_Minimized) {
+ // When we try to rollup in a minimized window, transitionend events for
+ // panels might not fire and thus we might not hide the popup after all,
+ // see bug 1810797.
+ options.mAllowAnimations = nsIRollupListener::AllowAnimations::No;
+ }
+
+ if (rollupListener->Rollup(options)) {
+ retVal = true;
+ }
+ return retVal;
+}
+
+/* static */
+bool nsWindow::DragInProgress() {
+ nsCOMPtr<nsIDragService> dragService =
+ do_GetService("@mozilla.org/widget/dragservice;1");
+ if (!dragService) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDragSession> currentDragSession;
+ dragService->GetCurrentSession(getter_AddRefs(currentDragSession));
+ return !!currentDragSession;
+}
+
+// This is an ugly workaround for
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1622107
+// We try to detect when Wayland compositor / gtk fails to deliver
+// info about finished D&D operations and cancel it on our own.
+MOZ_CAN_RUN_SCRIPT static void WaylandDragWorkaround(GdkEventButton* aEvent) {
+ static int buttonPressCountWithDrag = 0;
+
+ // We track only left button state as Firefox performs D&D on left
+ // button only.
+ if (aEvent->button != 1 || aEvent->type != GDK_BUTTON_PRESS) {
+ return;
+ }
+
+ nsCOMPtr<nsIDragService> dragService =
+ do_GetService("@mozilla.org/widget/dragservice;1");
+ if (!dragService) {
+ return;
+ }
+ nsCOMPtr<nsIDragSession> currentDragSession;
+ dragService->GetCurrentSession(getter_AddRefs(currentDragSession));
+
+ if (!currentDragSession) {
+ buttonPressCountWithDrag = 0;
+ return;
+ }
+
+ buttonPressCountWithDrag++;
+ if (buttonPressCountWithDrag > 1) {
+ NS_WARNING(
+ "Quit unfinished Wayland Drag and Drop operation. Buggy Wayland "
+ "compositor?");
+ buttonPressCountWithDrag = 0;
+ dragService->EndDragSession(false, 0);
+ }
+}
+
+static nsWindow* get_window_for_gtk_widget(GtkWidget* widget) {
+ gpointer user_data = g_object_get_data(G_OBJECT(widget), "nsWindow");
+ return static_cast<nsWindow*>(user_data);
+}
+
+static nsWindow* get_window_for_gdk_window(GdkWindow* window) {
+ gpointer user_data = g_object_get_data(G_OBJECT(window), "nsWindow");
+ return static_cast<nsWindow*>(user_data);
+}
+
+static bool is_mouse_in_window(GdkWindow* aWindow, gdouble aMouseX,
+ gdouble aMouseY) {
+ GdkWindow* window = aWindow;
+ if (!window) {
+ return false;
+ }
+
+ gint x = 0;
+ gint y = 0;
+
+ {
+ gint offsetX = 0;
+ gint offsetY = 0;
+
+ while (window) {
+ gint tmpX = 0;
+ gint tmpY = 0;
+
+ gdk_window_get_position(window, &tmpX, &tmpY);
+ GtkWidget* widget = get_gtk_widget_for_gdk_window(window);
+
+ // if this is a window, compute x and y given its origin and our
+ // offset
+ if (GTK_IS_WINDOW(widget)) {
+ x = tmpX + offsetX;
+ y = tmpY + offsetY;
+ break;
+ }
+
+ offsetX += tmpX;
+ offsetY += tmpY;
+ window = gdk_window_get_parent(window);
+ }
+ }
+
+ gint margin = 0;
+ if (nsWindow* w = get_window_for_gdk_window(aWindow)) {
+ margin = w->GetInputRegionMarginInGdkCoords();
+ }
+
+ x += margin;
+ y += margin;
+
+ gint w = gdk_window_get_width(aWindow) - margin;
+ gint h = gdk_window_get_height(aWindow) - margin;
+
+ return aMouseX > x && aMouseX < x + w && aMouseY > y && aMouseY < y + h;
+}
+
+static GtkWidget* get_gtk_widget_for_gdk_window(GdkWindow* window) {
+ gpointer user_data = nullptr;
+ gdk_window_get_user_data(window, &user_data);
+
+ return GTK_WIDGET(user_data);
+}
+
+static GdkCursor* get_gtk_cursor(nsCursor aCursor) {
+ GdkCursor* gdkcursor = nullptr;
+ uint8_t newType = 0xff;
+
+ if ((gdkcursor = gCursorCache[aCursor])) {
+ return gdkcursor;
+ }
+
+ GdkDisplay* defaultDisplay = gdk_display_get_default();
+
+ switch (aCursor) {
+ case eCursor_standard:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "default");
+ break;
+ case eCursor_wait:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "wait");
+ break;
+ case eCursor_select:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "text");
+ break;
+ case eCursor_hyperlink:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "pointer");
+ break;
+ case eCursor_n_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "n-resize");
+ break;
+ case eCursor_s_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "s-resize");
+ break;
+ case eCursor_w_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "w-resize");
+ break;
+ case eCursor_e_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "e-resize");
+ break;
+ case eCursor_nw_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "nw-resize");
+ break;
+ case eCursor_se_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "se-resize");
+ break;
+ case eCursor_ne_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "ne-resize");
+ break;
+ case eCursor_sw_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "sw-resize");
+ break;
+ case eCursor_crosshair:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "crosshair");
+ break;
+ case eCursor_move:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "move");
+ break;
+ case eCursor_help:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "help");
+ break;
+ case eCursor_copy:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "copy");
+ if (!gdkcursor) newType = MOZ_CURSOR_COPY;
+ break;
+ case eCursor_alias:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "alias");
+ if (!gdkcursor) newType = MOZ_CURSOR_ALIAS;
+ break;
+ case eCursor_context_menu:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "context-menu");
+ if (!gdkcursor) newType = MOZ_CURSOR_CONTEXT_MENU;
+ break;
+ case eCursor_cell:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "cell");
+ break;
+ case eCursor_grab:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "grab");
+ if (!gdkcursor) newType = MOZ_CURSOR_HAND_GRAB;
+ break;
+ case eCursor_grabbing:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "grabbing");
+ if (!gdkcursor) newType = MOZ_CURSOR_HAND_GRABBING;
+ break;
+ case eCursor_spinning:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "progress");
+ if (!gdkcursor) newType = MOZ_CURSOR_SPINNING;
+ break;
+ case eCursor_zoom_in:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "zoom-in");
+ if (!gdkcursor) newType = MOZ_CURSOR_ZOOM_IN;
+ break;
+ case eCursor_zoom_out:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "zoom-out");
+ if (!gdkcursor) newType = MOZ_CURSOR_ZOOM_OUT;
+ break;
+ case eCursor_not_allowed:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "not-allowed");
+ if (!gdkcursor) newType = MOZ_CURSOR_NOT_ALLOWED;
+ break;
+ case eCursor_no_drop:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "no-drop");
+ if (!gdkcursor) { // this nonstandard sequence makes it work on KDE and
+ // GNOME
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "forbidden");
+ }
+ if (!gdkcursor) {
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "circle");
+ }
+ if (!gdkcursor) newType = MOZ_CURSOR_NOT_ALLOWED;
+ break;
+ case eCursor_vertical_text:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "vertical-text");
+ if (!gdkcursor) {
+ newType = MOZ_CURSOR_VERTICAL_TEXT;
+ }
+ break;
+ case eCursor_all_scroll:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "move");
+ break;
+ case eCursor_nesw_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "nesw-resize");
+ if (!gdkcursor) newType = MOZ_CURSOR_NESW_RESIZE;
+ break;
+ case eCursor_nwse_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "nwse-resize");
+ if (!gdkcursor) newType = MOZ_CURSOR_NWSE_RESIZE;
+ break;
+ case eCursor_ns_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "ns-resize");
+ break;
+ case eCursor_ew_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "ew-resize");
+ break;
+ case eCursor_row_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "row-resize");
+ break;
+ case eCursor_col_resize:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "col-resize");
+ break;
+ case eCursor_none:
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "none");
+ if (!gdkcursor) newType = MOZ_CURSOR_NONE;
+ break;
+ default:
+ NS_ASSERTION(aCursor, "Invalid cursor type");
+ gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "default");
+ break;
+ }
+
+ // If by now we don't have a xcursor, this means we have to make a custom
+ // one. First, we try creating a named cursor based on the hash of our
+ // custom bitmap, as libXcursor has some magic to convert bitmapped cursors
+ // to themed cursors
+ if (newType != 0xFF && GtkCursors[newType].hash) {
+ gdkcursor =
+ gdk_cursor_new_from_name(defaultDisplay, GtkCursors[newType].hash);
+ }
+
+ // If we still don't have a xcursor, we now really create a bitmap cursor
+ if (newType != 0xff && !gdkcursor) {
+ GdkPixbuf* cursor_pixbuf =
+ gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 32, 32);
+ if (!cursor_pixbuf) {
+ return nullptr;
+ }
+
+ guchar* data = gdk_pixbuf_get_pixels(cursor_pixbuf);
+
+ // Read data from GtkCursors and compose RGBA surface from 1bit bitmap and
+ // mask GtkCursors bits and mask are 32x32 monochrome bitmaps (1 bit for
+ // each pixel) so it's 128 byte array (4 bytes for are one bitmap row and
+ // there are 32 rows here).
+ const unsigned char* bits = GtkCursors[newType].bits;
+ const unsigned char* mask_bits = GtkCursors[newType].mask_bits;
+
+ for (int i = 0; i < 128; i++) {
+ char bit = (char)*bits++;
+ char mask = (char)*mask_bits++;
+ for (int j = 0; j < 8; j++) {
+ unsigned char pix = ~(((bit >> j) & 0x01) * 0xff);
+ *data++ = pix;
+ *data++ = pix;
+ *data++ = pix;
+ *data++ = (((mask >> j) & 0x01) * 0xff);
+ }
+ }
+
+ gdkcursor = gdk_cursor_new_from_pixbuf(
+ gdk_display_get_default(), cursor_pixbuf, GtkCursors[newType].hot_x,
+ GtkCursors[newType].hot_y);
+
+ g_object_unref(cursor_pixbuf);
+ }
+
+ gCursorCache[aCursor] = gdkcursor;
+
+ return gdkcursor;
+}
+
+// gtk callbacks
+
+void draw_window_of_widget(GtkWidget* widget, GdkWindow* aWindow, cairo_t* cr) {
+ if (gtk_cairo_should_draw_window(cr, aWindow)) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ NS_WARNING("Cannot get nsWindow from GtkWidget");
+ } else {
+ cairo_save(cr);
+ gtk_cairo_transform_to_window(cr, widget, aWindow);
+ // TODO - window->OnExposeEvent() can destroy this or other windows,
+ // do we need to handle it somehow?
+ window->OnExposeEvent(cr);
+ cairo_restore(cr);
+ }
+ }
+}
+
+/* static */
+gboolean expose_event_cb(GtkWidget* widget, cairo_t* cr) {
+ draw_window_of_widget(widget, gtk_widget_get_window(widget), cr);
+
+ // A strong reference is already held during "draw" signal emission,
+ // but GTK+ 3.4 wants the object to live a little longer than that
+ // (bug 1225970).
+ g_object_ref(widget);
+ g_idle_add(
+ [](gpointer data) -> gboolean {
+ g_object_unref(data);
+ return G_SOURCE_REMOVE;
+ },
+ widget);
+
+ return FALSE;
+}
+
+static gboolean configure_event_cb(GtkWidget* widget,
+ GdkEventConfigure* event) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return FALSE;
+ }
+
+ return window->OnConfigureEvent(widget, event);
+}
+
+// Some Gtk widget code may call gtk_widget_unrealize() which destroys
+// mGdkWindow. We need to listen on this signal and re-create
+// mGdkWindow when we're already mapped.
+static void widget_map_cb(GtkWidget* widget) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return;
+ }
+ window->OnMap();
+}
+
+static void widget_unmap_cb(GtkWidget* widget) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return;
+ }
+ window->OnUnmap();
+}
+
+static void size_allocate_cb(GtkWidget* widget, GtkAllocation* allocation) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return;
+ }
+
+ window->OnSizeAllocate(allocation);
+}
+
+static void toplevel_window_size_allocate_cb(GtkWidget* widget,
+ GtkAllocation* allocation) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return;
+ }
+
+ window->UpdateTopLevelOpaqueRegion();
+}
+
+static gboolean delete_event_cb(GtkWidget* widget, GdkEventAny* event) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return FALSE;
+ }
+
+ window->OnDeleteEvent();
+
+ return TRUE;
+}
+
+static gboolean enter_notify_event_cb(GtkWidget* widget,
+ GdkEventCrossing* event) {
+ RefPtr<nsWindow> window = get_window_for_gdk_window(event->window);
+ if (!window) {
+ return TRUE;
+ }
+
+ // We have stored leave notify - check if it's the correct one and
+ // fire it before enter notify in such case.
+ if (sStoredLeaveNotifyEvent) {
+ auto clearNofityEvent =
+ MakeScopeExit([&] { sStoredLeaveNotifyEvent = nullptr; });
+ if (event->x_root == sStoredLeaveNotifyEvent->x_root &&
+ event->y_root == sStoredLeaveNotifyEvent->y_root &&
+ window->ApplyEnterLeaveMutterWorkaround()) {
+ // Enter/Leave notify events has the same coordinates
+ // and uses know buggy window config.
+ // Consider it as a bogus one.
+ return TRUE;
+ }
+ RefPtr<nsWindow> leftWindow =
+ get_window_for_gdk_window(sStoredLeaveNotifyEvent->window);
+ if (leftWindow) {
+ leftWindow->OnLeaveNotifyEvent(sStoredLeaveNotifyEvent.get());
+ }
+ }
+
+ window->OnEnterNotifyEvent(event);
+ return TRUE;
+}
+
+static gboolean leave_notify_event_cb(GtkWidget* widget,
+ GdkEventCrossing* event) {
+ RefPtr<nsWindow> window = get_window_for_gdk_window(event->window);
+ if (!window) {
+ return TRUE;
+ }
+
+ if (window->ApplyEnterLeaveMutterWorkaround()) {
+ // The leave event is potentially wrong, don't fire it now but store
+ // it for further check at enter_notify_event_cb().
+ sStoredLeaveNotifyEvent.reset(reinterpret_cast<GdkEventCrossing*>(
+ gdk_event_copy(reinterpret_cast<GdkEvent*>(event))));
+ } else {
+ sStoredLeaveNotifyEvent = nullptr;
+ window->OnLeaveNotifyEvent(event);
+ }
+
+ return TRUE;
+}
+
+static nsWindow* GetFirstNSWindowForGDKWindow(GdkWindow* aGdkWindow) {
+ nsWindow* window;
+ while (!(window = get_window_for_gdk_window(aGdkWindow))) {
+ // The event has bubbled to the moz_container widget as passed into each
+ // caller's *widget parameter, but its corresponding nsWindow is an ancestor
+ // of the window that we need. Instead, look at event->window and find the
+ // first ancestor nsWindow of it because event->window may be in a plugin.
+ aGdkWindow = gdk_window_get_parent(aGdkWindow);
+ if (!aGdkWindow) {
+ window = nullptr;
+ break;
+ }
+ }
+ return window;
+}
+
+static gboolean motion_notify_event_cb(GtkWidget* widget,
+ GdkEventMotion* event) {
+ UpdateLastInputEventTime(event);
+
+ RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
+ if (!window) {
+ return FALSE;
+ }
+
+ window->OnMotionNotifyEvent(event);
+
+ return TRUE;
+}
+
+static gboolean button_press_event_cb(GtkWidget* widget,
+ GdkEventButton* event) {
+ UpdateLastInputEventTime(event);
+
+ RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
+ if (!window) {
+ return FALSE;
+ }
+
+ window->OnButtonPressEvent(event);
+
+ if (GdkIsWaylandDisplay()) {
+ WaylandDragWorkaround(event);
+ }
+
+ return TRUE;
+}
+
+static gboolean button_release_event_cb(GtkWidget* widget,
+ GdkEventButton* event) {
+ UpdateLastInputEventTime(event);
+
+ RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
+ if (!window) {
+ return FALSE;
+ }
+
+ window->OnButtonReleaseEvent(event);
+
+ return TRUE;
+}
+
+static gboolean focus_in_event_cb(GtkWidget* widget, GdkEventFocus* event) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return FALSE;
+ }
+
+ window->OnContainerFocusInEvent(event);
+
+ return FALSE;
+}
+
+static gboolean focus_out_event_cb(GtkWidget* widget, GdkEventFocus* event) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return FALSE;
+ }
+
+ window->OnContainerFocusOutEvent(event);
+
+ return FALSE;
+}
+
+#ifdef MOZ_X11
+// For long-lived popup windows that don't really take focus themselves but
+// may have elements that accept keyboard input when the parent window is
+// active, focus is handled specially. These windows include noautohide
+// panels. (This special handling is not necessary for temporary popups where
+// the keyboard is grabbed.)
+//
+// Mousing over or clicking on these windows should not cause them to steal
+// focus from their parent windows, so, the input field of WM_HINTS is set to
+// False to request that the window manager not set the input focus to this
+// window. http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.7
+//
+// However, these windows can still receive WM_TAKE_FOCUS messages from the
+// window manager, so they can still detect when the user has indicated that
+// they wish to direct keyboard input at these windows. When the window
+// manager offers focus to these windows (after a mouse over or click, for
+// example), a request to make the parent window active is issued. When the
+// parent window becomes active, keyboard events will be received.
+
+static GdkFilterReturn popup_take_focus_filter(GdkXEvent* gdk_xevent,
+ GdkEvent* event, gpointer data) {
+ auto* xevent = static_cast<XEvent*>(gdk_xevent);
+ if (xevent->type != ClientMessage) {
+ return GDK_FILTER_CONTINUE;
+ }
+
+ XClientMessageEvent& xclient = xevent->xclient;
+ if (xclient.message_type != gdk_x11_get_xatom_by_name("WM_PROTOCOLS")) {
+ return GDK_FILTER_CONTINUE;
+ }
+
+ Atom atom = xclient.data.l[0];
+ if (atom != gdk_x11_get_xatom_by_name("WM_TAKE_FOCUS")) {
+ return GDK_FILTER_CONTINUE;
+ }
+
+ guint32 timestamp = xclient.data.l[1];
+
+ GtkWidget* widget = get_gtk_widget_for_gdk_window(event->any.window);
+ if (!widget) {
+ return GDK_FILTER_CONTINUE;
+ }
+
+ GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(widget));
+ if (!parent) {
+ return GDK_FILTER_CONTINUE;
+ }
+
+ if (gtk_window_is_active(parent)) {
+ return GDK_FILTER_REMOVE; // leave input focus on the parent
+ }
+
+ GdkWindow* parent_window = gtk_widget_get_window(GTK_WIDGET(parent));
+ if (!parent_window) {
+ return GDK_FILTER_CONTINUE;
+ }
+
+ // In case the parent has not been deiconified.
+ gdk_window_show_unraised(parent_window);
+
+ // Request focus on the parent window.
+ // Use gdk_window_focus rather than gtk_window_present to avoid
+ // raising the parent window.
+ gdk_window_focus(parent_window, timestamp);
+ return GDK_FILTER_REMOVE;
+}
+#endif /* MOZ_X11 */
+
+static gboolean key_press_event_cb(GtkWidget* widget, GdkEventKey* event) {
+ LOGW("key_press_event_cb\n");
+
+ UpdateLastInputEventTime(event);
+
+ // find the window with focus and dispatch this event to that widget
+ nsWindow* window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return FALSE;
+ }
+
+ RefPtr<nsWindow> focusWindow = gFocusWindow ? gFocusWindow : window;
+
+#ifdef MOZ_X11
+ // Keyboard repeat can cause key press events to queue up when there are
+ // slow event handlers (bug 301029). Throttle these events by removing
+ // consecutive pending duplicate KeyPress events to the same window.
+ // We use the event time of the last one.
+ // Note: GDK calls XkbSetDetectableAutorepeat so that KeyRelease events
+ // are generated only when the key is physically released.
+# define NS_GDKEVENT_MATCH_MASK 0x1FFF // GDK_SHIFT_MASK .. GDK_BUTTON5_MASK
+ // Our headers undefine X11 KeyPress - let's redefine it here.
+# ifndef KeyPress
+# define KeyPress 2
+# endif
+ GdkDisplay* gdkDisplay = gtk_widget_get_display(widget);
+ if (GdkIsX11Display(gdkDisplay)) {
+ Display* dpy = GDK_DISPLAY_XDISPLAY(gdkDisplay);
+ while (XPending(dpy)) {
+ XEvent next_event;
+ XPeekEvent(dpy, &next_event);
+ GdkWindow* nextGdkWindow =
+ gdk_x11_window_lookup_for_display(gdkDisplay, next_event.xany.window);
+ if (nextGdkWindow != event->window || next_event.type != KeyPress ||
+ next_event.xkey.keycode != event->hardware_keycode ||
+ next_event.xkey.state != (event->state & NS_GDKEVENT_MATCH_MASK)) {
+ break;
+ }
+ XNextEvent(dpy, &next_event);
+ event->time = next_event.xkey.time;
+ }
+ }
+#endif
+
+ return focusWindow->OnKeyPressEvent(event);
+}
+
+static gboolean key_release_event_cb(GtkWidget* widget, GdkEventKey* event) {
+ LOGW("key_release_event_cb\n");
+
+ UpdateLastInputEventTime(event);
+
+ // find the window with focus and dispatch this event to that widget
+ nsWindow* window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return FALSE;
+ }
+
+ RefPtr<nsWindow> focusWindow = gFocusWindow ? gFocusWindow : window;
+ return focusWindow->OnKeyReleaseEvent(event);
+}
+
+static gboolean property_notify_event_cb(GtkWidget* aWidget,
+ GdkEventProperty* aEvent) {
+ RefPtr<nsWindow> window = get_window_for_gdk_window(aEvent->window);
+ if (!window) {
+ return FALSE;
+ }
+
+ return window->OnPropertyNotifyEvent(aWidget, aEvent);
+}
+
+static gboolean scroll_event_cb(GtkWidget* widget, GdkEventScroll* event) {
+ RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
+ if (NS_WARN_IF(!window)) {
+ return FALSE;
+ }
+
+ window->OnScrollEvent(event);
+
+ return TRUE;
+}
+
+static gboolean visibility_notify_event_cb(GtkWidget* widget,
+ GdkEventVisibility* event) {
+ RefPtr<nsWindow> window = get_window_for_gdk_window(event->window);
+ if (!window) {
+ return FALSE;
+ }
+ window->OnVisibilityNotifyEvent(event->state);
+ return TRUE;
+}
+
+static void hierarchy_changed_cb(GtkWidget* widget,
+ GtkWidget* previous_toplevel) {
+ GtkWidget* toplevel = gtk_widget_get_toplevel(widget);
+ GdkWindowState old_window_state = GDK_WINDOW_STATE_WITHDRAWN;
+ GdkEventWindowState event;
+
+ event.new_window_state = GDK_WINDOW_STATE_WITHDRAWN;
+
+ if (GTK_IS_WINDOW(previous_toplevel)) {
+ g_signal_handlers_disconnect_by_func(
+ previous_toplevel, FuncToGpointer(window_state_event_cb), widget);
+ GdkWindow* win = gtk_widget_get_window(previous_toplevel);
+ if (win) {
+ old_window_state = gdk_window_get_state(win);
+ }
+ }
+
+ if (GTK_IS_WINDOW(toplevel)) {
+ g_signal_connect_swapped(toplevel, "window-state-event",
+ G_CALLBACK(window_state_event_cb), widget);
+ GdkWindow* win = gtk_widget_get_window(toplevel);
+ if (win) {
+ event.new_window_state = gdk_window_get_state(win);
+ }
+ }
+
+ event.changed_mask =
+ static_cast<GdkWindowState>(old_window_state ^ event.new_window_state);
+
+ if (event.changed_mask) {
+ event.type = GDK_WINDOW_STATE;
+ event.window = nullptr;
+ event.send_event = TRUE;
+ window_state_event_cb(widget, &event);
+ }
+}
+
+static gboolean window_state_event_cb(GtkWidget* widget,
+ GdkEventWindowState* event) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return FALSE;
+ }
+
+ window->OnWindowStateEvent(widget, event);
+
+ return FALSE;
+}
+
+static void settings_xft_dpi_changed_cb(GtkSettings* gtk_settings,
+ GParamSpec* pspec, nsWindow* data) {
+ RefPtr<nsWindow> window = data;
+ window->OnDPIChanged();
+ // Even though the window size in screen pixels has not changed,
+ // nsViewManager stores the dimensions in app units.
+ // DispatchResized() updates those.
+ window->DispatchResized();
+}
+
+static void check_resize_cb(GtkContainer* container, gpointer user_data) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(GTK_WIDGET(container));
+ if (!window) {
+ return;
+ }
+ window->OnCheckResize();
+}
+
+static void screen_composited_changed_cb(GdkScreen* screen,
+ gpointer user_data) {
+ // This callback can run before gfxPlatform::Init() in rare
+ // cases involving the profile manager. When this happens,
+ // we have no reason to reset any compositors as graphics
+ // hasn't been initialized yet.
+ if (GPUProcessManager::Get()) {
+ GPUProcessManager::Get()->ResetCompositors();
+ }
+}
+
+static void widget_composited_changed_cb(GtkWidget* widget,
+ gpointer user_data) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return;
+ }
+ window->OnCompositedChanged();
+}
+
+static void scale_changed_cb(GtkWidget* widget, GParamSpec* aPSpec,
+ gpointer aPointer) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
+ if (!window) {
+ return;
+ }
+
+ window->OnScaleChanged(/* aNotify = */ true);
+}
+
+static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent) {
+ UpdateLastInputEventTime(aEvent);
+
+ RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(aEvent->window);
+ if (!window) {
+ return FALSE;
+ }
+
+ return window->OnTouchEvent(aEvent);
+}
+
+// This function called generic because there is no signal specific to touchpad
+// pinch events.
+static gboolean generic_event_cb(GtkWidget* widget, GdkEvent* aEvent) {
+ if (aEvent->type != GDK_TOUCHPAD_PINCH) {
+ return FALSE;
+ }
+ // Using reinterpret_cast because the touchpad_pinch field of GdkEvent is not
+ // available in GTK+ versions lower than v3.18
+ GdkEventTouchpadPinch* event =
+ reinterpret_cast<GdkEventTouchpadPinch*>(aEvent);
+
+ RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
+
+ if (!window) {
+ return FALSE;
+ }
+ return window->OnTouchpadPinchEvent(event);
+}
+
+//////////////////////////////////////////////////////////////////////
+// These are all of our drag and drop operations
+
+void nsWindow::InitDragEvent(WidgetDragEvent& aEvent) {
+ // set the keyboard modifiers
+ guint modifierState = KeymapWrapper::GetCurrentModifierState();
+ KeymapWrapper::InitInputEvent(aEvent, modifierState);
+}
+
+static LayoutDeviceIntPoint GetWindowDropPosition(nsWindow* aWindow, int aX,
+ int aY) {
+ // Workaround for Bug 1710344
+ // Caused by Gtk issue https://gitlab.gnome.org/GNOME/gtk/-/issues/4437
+ if (aWindow->IsWaylandPopup()) {
+ int tx = 0, ty = 0;
+ gdk_window_get_position(aWindow->GetToplevelGdkWindow(), &tx, &ty);
+ aX += tx;
+ aY += ty;
+ }
+ LOGDRAG("WindowDropPosition [%d, %d]", aX, aY);
+ return aWindow->GdkPointToDevicePixels({aX, aY});
+}
+
+gboolean WindowDragMotionHandler(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX, gint aY,
+ guint aTime) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
+ if (!window) {
+ return FALSE;
+ }
+
+ // figure out which internal widget this drag motion actually happened on
+ nscoord retx = 0;
+ nscoord rety = 0;
+
+ GdkWindow* innerWindow = get_inner_gdk_window(gtk_widget_get_window(aWidget),
+ aX, aY, &retx, &rety);
+ RefPtr<nsWindow> innerMostWindow = get_window_for_gdk_window(innerWindow);
+ if (!innerMostWindow) {
+ innerMostWindow = window;
+ }
+ LOGDRAG("WindowDragMotionHandler target nsWindow [%p]",
+ innerMostWindow.get());
+
+ RefPtr<nsDragService> dragService = nsDragService::GetInstance();
+ nsDragService::AutoEventLoop loop(dragService);
+ if (!dragService->ScheduleMotionEvent(
+ innerMostWindow, aDragContext,
+ GetWindowDropPosition(innerMostWindow, retx, rety), aTime)) {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean drag_motion_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX,
+ gint aY, guint aTime, gpointer aData) {
+ return WindowDragMotionHandler(aWidget, aDragContext, aX, aY, aTime);
+}
+
+void WindowDragLeaveHandler(GtkWidget* aWidget) {
+ LOGDRAG("WindowDragLeaveHandler()\n");
+
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
+ if (!window) {
+ LOGDRAG(" Failed - can't find nsWindow!\n");
+ return;
+ }
+
+ RefPtr<nsDragService> dragService = nsDragService::GetInstance();
+ nsDragService::AutoEventLoop loop(dragService);
+
+ nsWindow* mostRecentDragWindow = dragService->GetMostRecentDestWindow();
+ if (!mostRecentDragWindow) {
+ // This can happen when the target will not accept a drop. A GTK drag
+ // source sends the leave message to the destination before the
+ // drag-failed signal on the source widget, but the leave message goes
+ // via the X server, and so doesn't get processed at least until the
+ // event loop runs again.
+ LOGDRAG(" Failed - GetMostRecentDestWindow()!\n");
+ return;
+ }
+
+ if (aWidget != window->GetGtkWidget()) {
+ // When the drag moves between widgets, GTK can send leave signal for
+ // the old widget after the motion or drop signal for the new widget.
+ // We'll send the leave event when the motion or drop event is run.
+ LOGDRAG(" Failed - GtkWidget mismatch!\n");
+ return;
+ }
+
+ LOGDRAG("WindowDragLeaveHandler nsWindow %p\n", (void*)mostRecentDragWindow);
+ dragService->ScheduleLeaveEvent();
+}
+
+static void drag_leave_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, guint aTime,
+ gpointer aData) {
+ WindowDragLeaveHandler(aWidget);
+}
+
+gboolean WindowDragDropHandler(GtkWidget* aWidget, GdkDragContext* aDragContext,
+ gint aX, gint aY, guint aTime) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
+ if (!window) {
+ return FALSE;
+ }
+
+ // figure out which internal widget this drag motion actually happened on
+ nscoord retx = 0;
+ nscoord rety = 0;
+
+ GdkWindow* innerWindow = get_inner_gdk_window(gtk_widget_get_window(aWidget),
+ aX, aY, &retx, &rety);
+ RefPtr<nsWindow> innerMostWindow = get_window_for_gdk_window(innerWindow);
+
+ if (!innerMostWindow) {
+ innerMostWindow = window;
+ }
+
+ LOGDRAG("WindowDragDropHandler nsWindow [%p]", innerMostWindow.get());
+ RefPtr<nsDragService> dragService = nsDragService::GetInstance();
+ nsDragService::AutoEventLoop loop(dragService);
+ return dragService->ScheduleDropEvent(
+ innerMostWindow, aDragContext,
+ GetWindowDropPosition(innerMostWindow, retx, rety), aTime);
+}
+
+static gboolean drag_drop_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX,
+ gint aY, guint aTime, gpointer aData) {
+ return WindowDragDropHandler(aWidget, aDragContext, aX, aY, aTime);
+}
+
+static void drag_data_received_event_cb(GtkWidget* aWidget,
+ GdkDragContext* aDragContext, gint aX,
+ gint aY,
+ GtkSelectionData* aSelectionData,
+ guint aInfo, guint aTime,
+ gpointer aData) {
+ RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
+ if (!window) {
+ return;
+ }
+
+ window->OnDragDataReceivedEvent(aWidget, aDragContext, aX, aY, aSelectionData,
+ aInfo, aTime, aData);
+}
+
+static nsresult initialize_prefs(void) {
+ if (Preferences::HasUserValue("widget.use-aspect-ratio")) {
+ gUseAspectRatio = Preferences::GetBool("widget.use-aspect-ratio", true);
+ } else {
+ gUseAspectRatio = IsGnomeDesktopEnvironment() || IsKdeDesktopEnvironment();
+ }
+ return NS_OK;
+}
+
+// TODO: Can we simplify it for mShell/mContainer only scenario?
+static GdkWindow* get_inner_gdk_window(GdkWindow* aWindow, gint x, gint y,
+ gint* retx, gint* rety) {
+ gint cx, cy, cw, ch;
+ GList* children = gdk_window_peek_children(aWindow);
+ for (GList* child = g_list_last(children); child;
+ child = g_list_previous(child)) {
+ auto* childWindow = (GdkWindow*)child->data;
+ if (get_window_for_gdk_window(childWindow)) {
+ gdk_window_get_geometry(childWindow, &cx, &cy, &cw, &ch);
+ if ((cx < x) && (x < (cx + cw)) && (cy < y) && (y < (cy + ch)) &&
+ gdk_window_is_visible(childWindow)) {
+ return get_inner_gdk_window(childWindow, x - cx, y - cy, retx, rety);
+ }
+ }
+ }
+ *retx = x;
+ *rety = y;
+ return aWindow;
+}
+
+#ifdef ACCESSIBILITY
+void nsWindow::CreateRootAccessible() {
+ if (!mRootAccessible) {
+ LOG("nsWindow:: Create Toplevel Accessibility\n");
+ mRootAccessible = GetRootAccessible();
+ }
+}
+
+void nsWindow::DispatchEventToRootAccessible(uint32_t aEventType) {
+ if (!a11y::ShouldA11yBeEnabled()) {
+ return;
+ }
+
+ nsAccessibilityService* accService = GetOrCreateAccService();
+ if (!accService) {
+ return;
+ }
+
+ // Get the root document accessible and fire event to it.
+ a11y::LocalAccessible* acc = GetRootAccessible();
+ if (acc) {
+ accService->FireAccessibleEvent(aEventType, acc);
+ }
+}
+
+void nsWindow::DispatchActivateEventAccessible(void) {
+ DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE);
+}
+
+void nsWindow::DispatchDeactivateEventAccessible(void) {
+ DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_DEACTIVATE);
+}
+
+void nsWindow::DispatchMaximizeEventAccessible(void) {
+ DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE);
+}
+
+void nsWindow::DispatchMinimizeEventAccessible(void) {
+ DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE);
+}
+
+void nsWindow::DispatchRestoreEventAccessible(void) {
+ DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_RESTORE);
+}
+
+#endif /* #ifdef ACCESSIBILITY */
+
+void nsWindow::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ if (!mIMContext) {
+ return;
+ }
+ mIMContext->SetInputContext(this, &aContext, &aAction);
+}
+
+InputContext nsWindow::GetInputContext() {
+ InputContext context;
+ if (!mIMContext) {
+ context.mIMEState.mEnabled = IMEEnabled::Disabled;
+ context.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
+ } else {
+ context = mIMContext->GetInputContext();
+ }
+ return context;
+}
+
+TextEventDispatcherListener* nsWindow::GetNativeTextEventDispatcherListener() {
+ if (NS_WARN_IF(!mIMContext)) {
+ return nullptr;
+ }
+ return mIMContext;
+}
+
+bool nsWindow::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+
+ Maybe<WritingMode> writingMode;
+ if (aEvent.NeedsToRemapNavigationKey()) {
+ if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+ writingMode = dispatcher->MaybeQueryWritingModeAtSelection();
+ }
+ }
+
+ NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
+ keyBindings->GetEditCommands(aEvent, writingMode, aCommands);
+ return true;
+}
+
+already_AddRefed<DrawTarget> nsWindow::StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion, BufferMode* aBufferMode) {
+ return mSurfaceProvider.StartRemoteDrawingInRegion(aInvalidRegion,
+ aBufferMode);
+}
+
+void nsWindow::EndRemoteDrawingInRegion(
+ DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
+ mSurfaceProvider.EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion);
+}
+
+bool nsWindow::GetDragInfo(WidgetMouseEvent* aMouseEvent, GdkWindow** aWindow,
+ gint* aButton, gint* aRootX, gint* aRootY) {
+ if (aMouseEvent->mButton != MouseButton::ePrimary) {
+ // we can only begin a move drag with the left mouse button
+ return false;
+ }
+ *aButton = 1;
+
+ // get the gdk window for this widget
+ GdkWindow* gdk_window = mGdkWindow;
+ if (!gdk_window) {
+ return false;
+ }
+#ifdef DEBUG
+ // GDK_IS_WINDOW(...) expands to a statement-expression, and
+ // statement-expressions are not allowed in template-argument lists. So we
+ // have to make the MOZ_ASSERT condition indirect.
+ if (!GDK_IS_WINDOW(gdk_window)) {
+ MOZ_ASSERT(false, "must really be window");
+ }
+#endif
+
+ // find the top-level window
+ gdk_window = gdk_window_get_toplevel(gdk_window);
+ MOZ_ASSERT(gdk_window, "gdk_window_get_toplevel should not return null");
+ *aWindow = gdk_window;
+
+ if (!aMouseEvent->mWidget) {
+ return false;
+ }
+
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=789054
+ // To avoid crashes disable double-click on WM without _NET_WM_MOVERESIZE.
+ // See _should_perform_ewmh_drag() at gdkwindow-x11.c
+ // XXXsmaug remove this old hack. gtk should be fixed now.
+ GdkScreen* screen = gdk_window_get_screen(gdk_window);
+ GdkAtom atom = gdk_atom_intern("_NET_WM_MOVERESIZE", FALSE);
+ if (!gdk_x11_screen_supports_net_wm_hint(screen, atom)) {
+ static TimeStamp lastTimeStamp;
+ if (lastTimeStamp != aMouseEvent->mTimeStamp) {
+ lastTimeStamp = aMouseEvent->mTimeStamp;
+ } else {
+ return false;
+ }
+ }
+ }
+#endif
+
+ // FIXME: It would be nice to have the widget position at the time
+ // of the event, but it's relatively unlikely that the widget has
+ // moved since the mousedown. (On the other hand, it's quite likely
+ // that the mouse has moved, which is why we use the mouse position
+ // from the event.)
+ LayoutDeviceIntPoint offset = aMouseEvent->mWidget->WidgetToScreenOffset();
+ *aRootX = aMouseEvent->mRefPoint.x + offset.x;
+ *aRootY = aMouseEvent->mRefPoint.y + offset.y;
+
+ return true;
+}
+
+nsIWidget::WindowRenderer* nsWindow::GetWindowRenderer() {
+ if (mIsDestroyed) {
+ // Prevent external code from triggering the re-creation of the
+ // LayerManager/Compositor during shutdown. Just return what we currently
+ // have, which is most likely null.
+ return mWindowRenderer;
+ }
+
+ return nsBaseWidget::GetWindowRenderer();
+}
+
+void nsWindow::DidGetNonBlankPaint() {
+ if (mGotNonBlankPaint) {
+ return;
+ }
+ mGotNonBlankPaint = true;
+ if (!mConfiguredClearColor) {
+ // Nothing to do, we hadn't overridden the clear color.
+ mConfiguredClearColor = true;
+ return;
+ }
+ // Reset the clear color set in the expose event to transparent.
+ GetWindowRenderer()->AsWebRender()->WrBridge()->SendSetDefaultClearColor(
+ NS_TRANSPARENT);
+}
+
+/* nsWindow::SetCompositorWidgetDelegate() sets remote GtkCompositorWidget
+ * to render into with compositor.
+ *
+ * SetCompositorWidgetDelegate(delegate) is called from
+ * nsBaseWidget::CreateCompositor(), i.e. nsWindow::GetWindowRenderer().
+ *
+ * SetCompositorWidgetDelegate(null) is called from
+ * nsBaseWidget::DestroyCompositor().
+ */
+void nsWindow::SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) {
+ LOG("nsWindow::SetCompositorWidgetDelegate %p mIsMapped %d "
+ "mCompositorWidgetDelegate %p\n",
+ delegate, mIsMapped, mCompositorWidgetDelegate);
+
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ if (delegate) {
+ mCompositorWidgetDelegate = delegate->AsPlatformSpecificDelegate();
+ MOZ_ASSERT(mCompositorWidgetDelegate,
+ "nsWindow::SetCompositorWidgetDelegate called with a "
+ "non-PlatformCompositorWidgetDelegate");
+ if (mIsMapped) {
+ ConfigureCompositor();
+ }
+ } else {
+ mCompositorWidgetDelegate = nullptr;
+ }
+}
+
+nsresult nsWindow::SetNonClientMargins(const LayoutDeviceIntMargin& aMargins) {
+ SetDrawsInTitlebar(aMargins.top == 0);
+ return NS_OK;
+}
+
+bool nsWindow::IsAlwaysUndecoratedWindow() const {
+ if (mIsPIPWindow || gKioskMode) {
+ return true;
+ }
+ if (mWindowType == WindowType::Dialog &&
+ mBorderStyle != BorderStyle::Default &&
+ mBorderStyle != BorderStyle::All &&
+ !(mBorderStyle & BorderStyle::Title) &&
+ !(mBorderStyle & BorderStyle::ResizeH)) {
+ return true;
+ }
+ return false;
+}
+
+void nsWindow::SetDrawsInTitlebar(bool aState) {
+ LOG("nsWindow::SetDrawsInTitlebar() State %d mGtkWindowDecoration %d\n",
+ aState, (int)mGtkWindowDecoration);
+
+ if (mGtkWindowDecoration == GTK_DECORATION_NONE ||
+ aState == mDrawInTitlebar) {
+ LOG(" already set, quit");
+ return;
+ }
+
+ if (mUndecorated) {
+ MOZ_ASSERT(aState, "Unexpected decoration request");
+ MOZ_ASSERT(!gtk_window_get_decorated(GTK_WINDOW(mShell)));
+ return;
+ }
+
+ mDrawInTitlebar = aState;
+
+ if (mGtkWindowDecoration == GTK_DECORATION_SYSTEM) {
+ SetWindowDecoration(aState ? BorderStyle::Border : mBorderStyle);
+ } else if (mGtkWindowDecoration == GTK_DECORATION_CLIENT) {
+ LOG(" Using CSD mode\n");
+
+ if (!gtk_widget_get_realized(GTK_WIDGET(mShell))) {
+ LOG(" Using CSD mode fast path\n");
+ gtk_window_set_titlebar(GTK_WINDOW(mShell),
+ aState ? gtk_fixed_new() : nullptr);
+ return;
+ }
+
+ /* Window manager does not support GDK_DECOR_BORDER,
+ * emulate it by CSD.
+ *
+ * gtk_window_set_titlebar() works on unrealized widgets only,
+ * we need to handle mShell carefully here.
+ * When CSD is enabled mGdkWindow is owned by mContainer which is good
+ * as we can't delete our mGdkWindow. To make mShell unrealized while
+ * mContainer is preserved we temporary reparent mContainer to an
+ * invisible GtkWindow.
+ */
+ bool visible = !mNeedsShow && mIsShown;
+ if (visible) {
+ NativeShow(false);
+ }
+
+ // Using GTK_WINDOW_POPUP rather than
+ // GTK_WINDOW_TOPLEVEL in the hope that POPUP results in less
+ // initialization and window manager interaction.
+ GtkWidget* tmpWindow = gtk_window_new(GTK_WINDOW_POPUP);
+ gtk_widget_realize(tmpWindow);
+
+ gtk_widget_reparent(GTK_WIDGET(mContainer), tmpWindow);
+ gtk_widget_unrealize(GTK_WIDGET(mShell));
+
+ // Add a hidden titlebar widget to trigger CSD, but disable the default
+ // titlebar. GtkFixed is a somewhat random choice for a simple unused
+ // widget. gtk_window_set_titlebar() takes ownership of the titlebar
+ // widget.
+ gtk_window_set_titlebar(GTK_WINDOW(mShell),
+ aState ? gtk_fixed_new() : nullptr);
+
+ /* A workaround for https://bugzilla.gnome.org/show_bug.cgi?id=791081
+ * gtk_widget_realize() throws:
+ * "In pixman_region32_init_rect: Invalid rectangle passed"
+ * when mShell has default 1x1 size.
+ */
+ GtkAllocation allocation = {0, 0, 0, 0};
+ gtk_widget_get_preferred_width(GTK_WIDGET(mShell), nullptr,
+ &allocation.width);
+ gtk_widget_get_preferred_height(GTK_WIDGET(mShell), nullptr,
+ &allocation.height);
+ gtk_widget_size_allocate(GTK_WIDGET(mShell), &allocation);
+
+ gtk_widget_realize(GTK_WIDGET(mShell));
+ gtk_widget_reparent(GTK_WIDGET(mContainer), GTK_WIDGET(mShell));
+
+ // Label mShell toplevel window so property_notify_event_cb callback
+ // can find its way home.
+ g_object_set_data(G_OBJECT(GetToplevelGdkWindow()), "nsWindow", this);
+
+ if (AreBoundsSane()) {
+ GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size());
+ LOG(" resize to %d x %d\n", size.width, size.height);
+ gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
+ }
+
+ if (visible) {
+ mNeedsShow = true;
+ NativeShow(true);
+ }
+
+ gtk_widget_destroy(tmpWindow);
+ }
+
+ if (mTransparencyBitmapForTitlebar) {
+ if (mDrawInTitlebar && mSizeMode == nsSizeMode_Normal && !mIsTiled) {
+ UpdateTitlebarTransparencyBitmap();
+ } else {
+ ClearTransparencyBitmap();
+ }
+ } else {
+ SetTitlebarRect();
+ }
+}
+
+GtkWindow* nsWindow::GetCurrentTopmostWindow() const {
+ GtkWindow* parentWindow = GTK_WINDOW(GetGtkWidget());
+ GtkWindow* topmostParentWindow = nullptr;
+ while (parentWindow) {
+ topmostParentWindow = parentWindow;
+ parentWindow = gtk_window_get_transient_for(parentWindow);
+ }
+ return topmostParentWindow;
+}
+
+gint nsWindow::GdkCeiledScaleFactor() {
+ if (IsTopLevelWindowType()) {
+ return mCeiledScaleFactor;
+ }
+ if (nsWindow* topmost = GetTopmostWindow()) {
+ return topmost->mCeiledScaleFactor;
+ }
+ return ScreenHelperGTK::GetGTKMonitorScaleFactor();
+}
+
+double nsWindow::FractionalScaleFactor() {
+#ifdef MOZ_WAYLAND
+ double fractional_scale = [&] {
+ if (IsTopLevelWindowType()) {
+ return mFractionalScaleFactor;
+ }
+ if (nsWindow* topmost = GetTopmostWindow()) {
+ return topmost->mFractionalScaleFactor;
+ }
+ return 0.0;
+ }();
+ if (fractional_scale != 0.0) {
+ return fractional_scale;
+ }
+#endif
+ return GdkCeiledScaleFactor();
+}
+
+gint nsWindow::DevicePixelsToGdkCoordRoundUp(int aPixels) {
+ double scale = FractionalScaleFactor();
+ return ceil(aPixels / scale);
+}
+
+gint nsWindow::DevicePixelsToGdkCoordRoundDown(int aPixels) {
+ double scale = FractionalScaleFactor();
+ return floor(aPixels / scale);
+}
+
+GdkPoint nsWindow::DevicePixelsToGdkPointRoundDown(
+ const LayoutDeviceIntPoint& aPoint) {
+ double scale = FractionalScaleFactor();
+ return {int(aPoint.x / scale), int(aPoint.y / scale)};
+}
+
+GdkRectangle nsWindow::DevicePixelsToGdkRectRoundOut(
+ const LayoutDeviceIntRect& aRect) {
+ double scale = FractionalScaleFactor();
+ int x = floor(aRect.x / scale);
+ int y = floor(aRect.y / scale);
+ int right = ceil((aRect.x + aRect.width) / scale);
+ int bottom = ceil((aRect.y + aRect.height) / scale);
+ return {x, y, right - x, bottom - y};
+}
+
+GdkRectangle nsWindow::DevicePixelsToGdkSizeRoundUp(
+ const LayoutDeviceIntSize& aSize) {
+ double scale = FractionalScaleFactor();
+ gint width = ceil(aSize.width / scale);
+ gint height = ceil(aSize.height / scale);
+ return {0, 0, width, height};
+}
+
+int nsWindow::GdkCoordToDevicePixels(gint aCoord) {
+ return (int)(aCoord * FractionalScaleFactor());
+}
+
+LayoutDeviceIntPoint nsWindow::GdkEventCoordsToDevicePixels(gdouble aX,
+ gdouble aY) {
+ double scale = FractionalScaleFactor();
+ return LayoutDeviceIntPoint::Floor((float)(aX * scale), (float)(aY * scale));
+}
+
+LayoutDeviceIntPoint nsWindow::GdkPointToDevicePixels(const GdkPoint& aPoint) {
+ double scale = FractionalScaleFactor();
+ return LayoutDeviceIntPoint::Floor((float)(aPoint.x * scale),
+ (float)(aPoint.y * scale));
+}
+
+LayoutDeviceIntRect nsWindow::GdkRectToDevicePixels(const GdkRectangle& aRect) {
+ double scale = FractionalScaleFactor();
+ return LayoutDeviceIntRect::RoundIn(
+ (float)(aRect.x * scale), (float)(aRect.y * scale),
+ (float)(aRect.width * scale), (float)(aRect.height * scale));
+}
+
+nsresult nsWindow::SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ LOG("SynthesizeNativeMouseEvent(%d, %d, %d, %d, %d)", aPoint.x.value,
+ aPoint.y.value, int(aNativeMessage), int(aButton), int(aModifierFlags));
+
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+
+ if (!mGdkWindow) {
+ return NS_OK;
+ }
+
+ // When a button-press/release event is requested, create it here and put it
+ // in the event queue. This will not emit a motion event - this needs to be
+ // done explicitly *before* requesting a button-press/release. You will also
+ // need to wait for the motion event to be dispatched before requesting a
+ // button-press/release event to maintain the desired event order.
+ switch (aNativeMessage) {
+ case NativeMouseMessage::ButtonDown:
+ case NativeMouseMessage::ButtonUp: {
+ GdkEvent event;
+ memset(&event, 0, sizeof(GdkEvent));
+ event.type = aNativeMessage == NativeMouseMessage::ButtonDown
+ ? GDK_BUTTON_PRESS
+ : GDK_BUTTON_RELEASE;
+ switch (aButton) {
+ case MouseButton::ePrimary:
+ case MouseButton::eMiddle:
+ case MouseButton::eSecondary:
+ case MouseButton::eX1:
+ case MouseButton::eX2:
+ event.button.button = aButton + 1;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+ event.button.state =
+ KeymapWrapper::ConvertWidgetModifierToGdkState(aModifierFlags);
+ event.button.window = mGdkWindow;
+ event.button.time = GDK_CURRENT_TIME;
+
+ // Get device for event source
+ event.button.device = GdkGetPointer();
+
+ event.button.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x);
+ event.button.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y);
+
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ event.button.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x);
+ event.button.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y);
+
+ gdk_event_put(&event);
+ return NS_OK;
+ }
+ case NativeMouseMessage::Move: {
+ // We don't support specific events other than button-press/release. In
+ // all other cases we'll synthesize a motion event that will be emitted by
+ // gdk_display_warp_pointer().
+ // XXX How to activate native modifier for the other events?
+#ifdef MOZ_WAYLAND
+ // Impossible to warp the pointer on Wayland.
+ // For pointer lock, pointer-constraints and relative-pointer are used.
+ if (GdkIsWaylandDisplay()) {
+ return NS_OK;
+ }
+#endif
+ GdkScreen* screen = gdk_window_get_screen(mGdkWindow);
+ GdkPoint point = DevicePixelsToGdkPointRoundDown(aPoint);
+ gdk_device_warp(GdkGetPointer(), screen, point.x, point.y);
+ return NS_OK;
+ }
+ case NativeMouseMessage::EnterWindow:
+ case NativeMouseMessage::LeaveWindow:
+ MOZ_ASSERT_UNREACHABLE("Non supported mouse event on Linux");
+ return NS_ERROR_INVALID_ARG;
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+void nsWindow::CreateAndPutGdkScrollEvent(mozilla::LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY) {
+ GdkEvent event;
+ memset(&event, 0, sizeof(GdkEvent));
+ event.type = GDK_SCROLL;
+ event.scroll.window = mGdkWindow;
+ event.scroll.time = GDK_CURRENT_TIME;
+ // Get device for event source
+ GdkDisplay* display = gdk_window_get_display(mGdkWindow);
+ GdkDeviceManager* device_manager = gdk_display_get_device_manager(display);
+ // See note in nsWindow::SynthesizeNativeTouchpadPan about the device we use
+ // here.
+ event.scroll.device = gdk_device_manager_get_client_pointer(device_manager);
+ event.scroll.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x);
+ event.scroll.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y);
+
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ event.scroll.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x);
+ event.scroll.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y);
+
+ // The delta values are backwards on Linux compared to Windows and Cocoa,
+ // hence the negation.
+ event.scroll.direction = GDK_SCROLL_SMOOTH;
+ event.scroll.delta_x = -aDeltaX;
+ event.scroll.delta_y = -aDeltaY;
+
+ gdk_event_put(&event);
+}
+
+nsresult nsWindow::SynthesizeNativeMouseScrollEvent(
+ mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage,
+ double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+
+ if (!mGdkWindow) {
+ return NS_OK;
+ }
+
+ CreateAndPutGdkScrollEvent(aPoint, aDeltaX, aDeltaY);
+
+ return NS_OK;
+}
+
+nsresult nsWindow::SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpoint");
+
+ if (!mGdkWindow) {
+ return NS_OK;
+ }
+
+ GdkEvent event;
+ memset(&event, 0, sizeof(GdkEvent));
+
+ static std::map<uint32_t, GdkEventSequence*> sKnownPointers;
+
+ auto result = sKnownPointers.find(aPointerId);
+ switch (aPointerState) {
+ case TOUCH_CONTACT:
+ if (result == sKnownPointers.end()) {
+ // GdkEventSequence isn't a thing we can instantiate, and never gets
+ // dereferenced in the gtk code. It's an opaque pointer, the only
+ // requirement is that it be distinct from other instances of
+ // GdkEventSequence*.
+ event.touch.sequence = (GdkEventSequence*)((uintptr_t)aPointerId);
+ sKnownPointers[aPointerId] = event.touch.sequence;
+ event.type = GDK_TOUCH_BEGIN;
+ } else {
+ event.touch.sequence = result->second;
+ event.type = GDK_TOUCH_UPDATE;
+ }
+ break;
+ case TOUCH_REMOVE:
+ event.type = GDK_TOUCH_END;
+ if (result == sKnownPointers.end()) {
+ NS_WARNING("Tried to synthesize touch-end for unknown pointer!");
+ return NS_ERROR_UNEXPECTED;
+ }
+ event.touch.sequence = result->second;
+ sKnownPointers.erase(result);
+ break;
+ case TOUCH_CANCEL:
+ event.type = GDK_TOUCH_CANCEL;
+ if (result == sKnownPointers.end()) {
+ NS_WARNING("Tried to synthesize touch-cancel for unknown pointer!");
+ return NS_ERROR_UNEXPECTED;
+ }
+ event.touch.sequence = result->second;
+ sKnownPointers.erase(result);
+ break;
+ case TOUCH_HOVER:
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ event.touch.window = mGdkWindow;
+ event.touch.time = GDK_CURRENT_TIME;
+
+ GdkDisplay* display = gdk_window_get_display(mGdkWindow);
+ GdkDeviceManager* device_manager = gdk_display_get_device_manager(display);
+ event.touch.device = gdk_device_manager_get_client_pointer(device_manager);
+
+ event.touch.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x);
+ event.touch.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y);
+
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ event.touch.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x);
+ event.touch.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y);
+
+ gdk_event_put(&event);
+
+ return NS_OK;
+}
+
+nsresult nsWindow::SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint,
+ int32_t aModifierFlags) {
+ if (!mGdkWindow) {
+ return NS_OK;
+ }
+ GdkEvent event;
+ memset(&event, 0, sizeof(GdkEvent));
+
+ GdkEventTouchpadPinch* touchpad_event =
+ reinterpret_cast<GdkEventTouchpadPinch*>(&event);
+ touchpad_event->type = GDK_TOUCHPAD_PINCH;
+
+ const ScreenIntPoint widgetToScreenOffset = ViewAs<ScreenPixel>(
+ WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ ScreenPoint pointInWindow =
+ ViewAs<ScreenPixel>(
+ aPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent) -
+ widgetToScreenOffset;
+
+ gdouble dx = 0, dy = 0;
+
+ switch (aEventPhase) {
+ case PHASE_BEGIN:
+ touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_BEGIN;
+ mCurrentSynthesizedTouchpadPinch = {pointInWindow, pointInWindow};
+ break;
+ case PHASE_UPDATE:
+ dx = pointInWindow.x - mCurrentSynthesizedTouchpadPinch.mCurrentFocus.x;
+ dy = pointInWindow.y - mCurrentSynthesizedTouchpadPinch.mCurrentFocus.y;
+ mCurrentSynthesizedTouchpadPinch.mCurrentFocus = pointInWindow;
+ touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_UPDATE;
+ break;
+ case PHASE_END:
+ touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_END;
+ break;
+
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ touchpad_event->window = mGdkWindow;
+ // We only set the fields of GdkEventTouchpadPinch which are
+ // actually used in OnTouchpadPinchEvent().
+ // GdkEventTouchpadPinch has additional fields.
+ // If OnTouchpadPinchEvent() is changed to use other fields, this function
+ // will need to change to set them as well.
+ touchpad_event->time = GDK_CURRENT_TIME;
+ touchpad_event->scale = aScale;
+ touchpad_event->x_root = DevicePixelsToGdkCoordRoundDown(
+ mCurrentSynthesizedTouchpadPinch.mBeginFocus.x +
+ ScreenCoord(widgetToScreenOffset.x));
+ touchpad_event->y_root = DevicePixelsToGdkCoordRoundDown(
+ mCurrentSynthesizedTouchpadPinch.mBeginFocus.y +
+ ScreenCoord(widgetToScreenOffset.y));
+
+ touchpad_event->x = DevicePixelsToGdkCoordRoundDown(
+ mCurrentSynthesizedTouchpadPinch.mBeginFocus.x);
+ touchpad_event->y = DevicePixelsToGdkCoordRoundDown(
+ mCurrentSynthesizedTouchpadPinch.mBeginFocus.y);
+
+ touchpad_event->dx = dx;
+ touchpad_event->dy = dy;
+
+ touchpad_event->state = aModifierFlags;
+
+ gdk_event_put(&event);
+
+ return NS_OK;
+}
+
+nsresult nsWindow::SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpadpanevent");
+
+ if (!mGdkWindow) {
+ return NS_OK;
+ }
+
+ // This should/could maybe send GdkEventTouchpadSwipe events, however we don't
+ // currently consume those (either real user input or testing events). So we
+ // send gdk scroll events to be more like what we do for real user input. If
+ // we start consuming GdkEventTouchpadSwipe and get those hooked up to swipe
+ // to nav, then maybe we should test those too.
+
+ mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase = Some(aEventPhase);
+ MOZ_ASSERT(mCurrentSynthesizedTouchpadPan.mSavedObserver == 0);
+ mCurrentSynthesizedTouchpadPan.mSavedObserver = notifier.SaveObserver();
+
+ // Note that CreateAndPutGdkScrollEvent sets the device source for the created
+ // event as the "client pointer" (a kind of default device) which will
+ // probably be of type mouse. We would ideally want to set the device of the
+ // created event to be a touchpad, but the system might not have a touchpad.
+ // To get around this we use
+ // mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase being something to
+ // indicate that we should treat the source of the event as touchpad in
+ // OnScrollEvent.
+ CreateAndPutGdkScrollEvent(aPoint, aDeltaX, aDeltaY);
+
+ return NS_OK;
+}
+
+nsWindow::GtkWindowDecoration nsWindow::GetSystemGtkWindowDecoration() {
+ static GtkWindowDecoration sGtkWindowDecoration = [] {
+ // Allow MOZ_GTK_TITLEBAR_DECORATION to override our heuristics
+ if (const char* decorationOverride =
+ getenv("MOZ_GTK_TITLEBAR_DECORATION")) {
+ if (strcmp(decorationOverride, "none") == 0) {
+ return GTK_DECORATION_NONE;
+ }
+ if (strcmp(decorationOverride, "client") == 0) {
+ return GTK_DECORATION_CLIENT;
+ }
+ if (strcmp(decorationOverride, "system") == 0) {
+ return GTK_DECORATION_SYSTEM;
+ }
+ }
+
+ // nsWindow::GetSystemGtkWindowDecoration can be called from various
+ // threads so we can't use gfxPlatformGtk here.
+ if (GdkIsWaylandDisplay()) {
+ return GTK_DECORATION_CLIENT;
+ }
+
+ // GTK_CSD forces CSD mode - use also CSD because window manager
+ // decorations does not work with CSD.
+ // We check GTK_CSD as well as gtk_window_should_use_csd() does.
+ if (const char* csdOverride = getenv("GTK_CSD")) {
+ return *csdOverride == '0' ? GTK_DECORATION_NONE : GTK_DECORATION_CLIENT;
+ }
+
+ // TODO: Consider switching this to GetDesktopEnvironmentIdentifier().
+ const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP");
+ if (!currentDesktop) {
+ return GTK_DECORATION_NONE;
+ }
+ if (strstr(currentDesktop, "i3")) {
+ return GTK_DECORATION_NONE;
+ }
+
+ // Tested desktops: pop:GNOME, KDE, Enlightenment, LXDE, openbox, MATE,
+ // X-Cinnamon, Pantheon, Deepin, GNOME, LXQt, Unity.
+ return GTK_DECORATION_CLIENT;
+ }();
+ return sGtkWindowDecoration;
+}
+
+bool nsWindow::TitlebarUseShapeMask() {
+ static int useShapeMask = []() {
+ // Don't use titlebar shape mask on Wayland
+ if (!GdkIsX11Display()) {
+ return false;
+ }
+
+ // We can't use shape masks on Mutter/X.org as we can't resize Firefox
+ // window there (Bug 1530252).
+ if (IsGnomeDesktopEnvironment()) {
+ return false;
+ }
+
+ return Preferences::GetBool("widget.titlebar-x11-use-shape-mask", false);
+ }();
+ return useShapeMask;
+}
+
+int32_t nsWindow::RoundsWidgetCoordinatesTo() { return GdkCeiledScaleFactor(); }
+
+void nsWindow::GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) {
+ nsCString displayName;
+
+ LOG("nsWindow::GetCompositorWidgetInitData");
+
+ *aInitData = mozilla::widget::GtkCompositorWidgetInitData(
+ GetX11Window(), displayName, GetShapedState(), GdkIsX11Display(),
+ GetClientSize());
+
+#ifdef MOZ_X11
+ if (GdkIsX11Display()) {
+ // Make sure the window XID is propagated to X server, we can fail otherwise
+ // in GPU process (Bug 1401634).
+ Display* display = DefaultXDisplay();
+ XFlush(display);
+ displayName = nsCString(XDisplayString(display));
+ }
+#endif
+}
+
+#ifdef MOZ_X11
+/* XApp progress support currently works by setting a property
+ * on a window with this Atom name. A supporting window manager
+ * will notice this and pass it along to whatever handling has
+ * been implemented on that end (e.g. passing it on to a taskbar
+ * widget.) There is no issue if WM support is lacking, this is
+ * simply ignored in that case.
+ *
+ * See https://github.com/linuxmint/xapps/blob/master/libxapp/xapp-gtk-window.c
+ * for further details.
+ */
+
+# define PROGRESS_HINT "_NET_WM_XAPP_PROGRESS"
+
+static void set_window_hint_cardinal(Window xid, const gchar* atom_name,
+ gulong cardinal) {
+ GdkDisplay* display;
+
+ display = gdk_display_get_default();
+
+ if (cardinal > 0) {
+ XChangeProperty(GDK_DISPLAY_XDISPLAY(display), xid,
+ gdk_x11_get_xatom_by_name_for_display(display, atom_name),
+ XA_CARDINAL, 32, PropModeReplace, (guchar*)&cardinal, 1);
+ } else {
+ XDeleteProperty(GDK_DISPLAY_XDISPLAY(display), xid,
+ gdk_x11_get_xatom_by_name_for_display(display, atom_name));
+ }
+}
+#endif // MOZ_X11
+
+void nsWindow::SetProgress(unsigned long progressPercent) {
+#ifdef MOZ_X11
+
+ if (!GdkIsX11Display()) {
+ return;
+ }
+
+ if (!mShell) {
+ return;
+ }
+
+ progressPercent = MIN(progressPercent, 100);
+
+ set_window_hint_cardinal(GDK_WINDOW_XID(GetToplevelGdkWindow()),
+ PROGRESS_HINT, progressPercent);
+#endif // MOZ_X11
+}
+
+#ifdef MOZ_X11
+void nsWindow::SetCompositorHint(WindowComposeRequest aState) {
+ if (!GdkIsX11Display()) {
+ return;
+ }
+
+ gulong value = aState;
+ GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL);
+ gdk_property_change(GetToplevelGdkWindow(),
+ gdk_atom_intern("_NET_WM_BYPASS_COMPOSITOR", FALSE),
+ cardinal_atom,
+ 32, // format
+ GDK_PROP_MODE_REPLACE, (guchar*)&value, 1);
+}
+#endif
+
+nsresult nsWindow::SetSystemFont(const nsCString& aFontName) {
+ GtkSettings* settings = gtk_settings_get_default();
+ g_object_set(settings, "gtk-font-name", aFontName.get(), nullptr);
+ return NS_OK;
+}
+
+nsresult nsWindow::GetSystemFont(nsCString& aFontName) {
+ GtkSettings* settings = gtk_settings_get_default();
+ gchar* fontName = nullptr;
+ g_object_get(settings, "gtk-font-name", &fontName, nullptr);
+ if (fontName) {
+ aFontName.Assign(fontName);
+ g_free(fontName);
+ }
+ return NS_OK;
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow();
+ return window.forget();
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow();
+ return window.forget();
+}
+
+#ifdef MOZ_WAYLAND
+static void relative_pointer_handle_relative_motion(
+ void* data, struct zwp_relative_pointer_v1* pointer, uint32_t time_hi,
+ uint32_t time_lo, wl_fixed_t dx_w, wl_fixed_t dy_w, wl_fixed_t dx_unaccel_w,
+ wl_fixed_t dy_unaccel_w) {
+ RefPtr<nsWindow> window(reinterpret_cast<nsWindow*>(data));
+
+ WidgetMouseEvent event(true, eMouseMove, window, WidgetMouseEvent::eReal);
+
+ double scale = window->FractionalScaleFactor();
+ event.mRefPoint = window->GetNativePointerLockCenter();
+ event.mRefPoint.x += int(wl_fixed_to_double(dx_w) * scale);
+ event.mRefPoint.y += int(wl_fixed_to_double(dy_w) * scale);
+
+ event.AssignEventTime(window->GetWidgetEventTime(time_lo));
+ window->DispatchInputEvent(&event);
+}
+
+static const struct zwp_relative_pointer_v1_listener relative_pointer_listener =
+ {
+ relative_pointer_handle_relative_motion,
+};
+
+void nsWindow::SetNativePointerLockCenter(
+ const LayoutDeviceIntPoint& aLockCenter) {
+ mNativePointerLockCenter = aLockCenter;
+}
+
+void nsWindow::LockNativePointer() {
+ if (!GdkIsWaylandDisplay()) {
+ return;
+ }
+
+ auto* waylandDisplay = WaylandDisplayGet();
+
+ auto* pointerConstraints = waylandDisplay->GetPointerConstraints();
+ if (!pointerConstraints) {
+ return;
+ }
+
+ auto* relativePointerMgr = waylandDisplay->GetRelativePointerManager();
+ if (!relativePointerMgr) {
+ return;
+ }
+
+ GdkDisplay* display = gdk_display_get_default();
+
+ GdkDeviceManager* manager = gdk_display_get_device_manager(display);
+ MOZ_ASSERT(manager);
+
+ GdkDevice* device = gdk_device_manager_get_client_pointer(manager);
+ if (!device) {
+ NS_WARNING("Could not find Wayland pointer to lock");
+ return;
+ }
+ wl_pointer* pointer = gdk_wayland_device_get_wl_pointer(device);
+ MOZ_ASSERT(pointer);
+
+ wl_surface* surface =
+ gdk_wayland_window_get_wl_surface(GetToplevelGdkWindow());
+ if (!surface) {
+ /* Can be null when the window is hidden.
+ * Though it's unlikely that a lock request comes in that case, be
+ * defensive. */
+ return;
+ }
+
+ if (mLockedPointer || mRelativePointer) {
+ UnlockNativePointer();
+ }
+
+ mLockedPointer = zwp_pointer_constraints_v1_lock_pointer(
+ pointerConstraints, surface, pointer, nullptr,
+ ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
+ if (!mLockedPointer) {
+ NS_WARNING("Could not lock Wayland pointer");
+ return;
+ }
+
+ mRelativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer(
+ relativePointerMgr, pointer);
+ if (!mRelativePointer) {
+ NS_WARNING("Could not create relative Wayland pointer");
+ zwp_locked_pointer_v1_destroy(mLockedPointer);
+ mLockedPointer = nullptr;
+ return;
+ }
+
+ zwp_relative_pointer_v1_add_listener(mRelativePointer,
+ &relative_pointer_listener, this);
+}
+
+void nsWindow::UnlockNativePointer() {
+ if (!GdkIsWaylandDisplay()) {
+ return;
+ }
+ if (mRelativePointer) {
+ zwp_relative_pointer_v1_destroy(mRelativePointer);
+ mRelativePointer = nullptr;
+ }
+ if (mLockedPointer) {
+ zwp_locked_pointer_v1_destroy(mLockedPointer);
+ mLockedPointer = nullptr;
+ }
+}
+#endif
+
+bool nsWindow::GetTopLevelWindowActiveState(nsIFrame* aFrame) {
+ // Used by window frame and button box rendering. We can end up in here in
+ // the content process when rendering one of these moz styles freely in a
+ // page. Fail in this case, there is no applicable window focus state.
+ if (!XRE_IsParentProcess()) {
+ return false;
+ }
+ // All headless windows are considered active so they are painted.
+ if (gfxPlatform::IsHeadless()) {
+ return true;
+ }
+ // Get the widget. nsIFrame's GetNearestWidget walks up the view chain
+ // until it finds a real window.
+ nsWindow* window = static_cast<nsWindow*>(aFrame->GetNearestWidget());
+ if (!window) {
+ return false;
+ }
+ return !window->mTitlebarBackdropState;
+}
+
+static nsIFrame* FindTitlebarFrame(nsIFrame* aFrame) {
+ for (nsIFrame* childFrame : aFrame->PrincipalChildList()) {
+ StyleAppearance appearance =
+ childFrame->StyleDisplay()->EffectiveAppearance();
+ if (appearance == StyleAppearance::MozWindowTitlebar ||
+ appearance == StyleAppearance::MozWindowTitlebarMaximized) {
+ return childFrame;
+ }
+
+ if (nsIFrame* foundFrame = FindTitlebarFrame(childFrame)) {
+ return foundFrame;
+ }
+ }
+ return nullptr;
+}
+
+nsIFrame* nsWindow::GetFrame() const {
+ nsView* view = nsView::GetViewFor(this);
+ if (!view) {
+ return nullptr;
+ }
+ return view->GetFrame();
+}
+
+void nsWindow::UpdateMozWindowActive() {
+ // Update activation state for the :-moz-window-inactive pseudoclass.
+ // Normally, this follows focus; we override it here to follow
+ // GDK_WINDOW_STATE_FOCUSED.
+ if (mozilla::dom::Document* document = GetDocument()) {
+ if (nsPIDOMWindowOuter* window = document->GetWindow()) {
+ if (RefPtr<mozilla::dom::BrowsingContext> bc =
+ window->GetBrowsingContext()) {
+ bc->SetIsActiveBrowserWindow(!mTitlebarBackdropState);
+ }
+ }
+ }
+}
+
+void nsWindow::ForceTitlebarRedraw() {
+ MOZ_ASSERT(mDrawInTitlebar, "We should not redraw invisible titlebar.");
+
+ if (!mWidgetListener || !mWidgetListener->GetPresShell()) {
+ return;
+ }
+
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ return;
+ }
+
+ frame = FindTitlebarFrame(frame);
+ if (frame) {
+ nsIContent* content = frame->GetContent();
+ if (content) {
+ nsLayoutUtils::PostRestyleEvent(content->AsElement(), RestyleHint{0},
+ nsChangeHint_RepaintFrame);
+ }
+ }
+}
+
+void nsWindow::LockAspectRatio(bool aShouldLock) {
+ if (!gUseAspectRatio) {
+ return;
+ }
+
+ if (aShouldLock) {
+ int decWidth = 0, decHeight = 0;
+ AddCSDDecorationSize(&decWidth, &decHeight);
+
+ float width =
+ DevicePixelsToGdkCoordRoundDown(mLastSizeRequest.width) + decWidth;
+ float height =
+ DevicePixelsToGdkCoordRoundDown(mLastSizeRequest.height) + decHeight;
+
+ mAspectRatio = width / height;
+ LOG("nsWindow::LockAspectRatio() width %f height %f aspect %f", width,
+ height, mAspectRatio);
+ } else {
+ mAspectRatio = 0.0;
+ LOG("nsWindow::LockAspectRatio() removed aspect ratio");
+ }
+
+ ApplySizeConstraints();
+}
+
+nsWindow* nsWindow::GetFocusedWindow() { return gFocusWindow; }
+
+#ifdef MOZ_WAYLAND
+bool nsWindow::SetEGLNativeWindowSize(
+ const LayoutDeviceIntSize& aEGLWindowSize) {
+ if (!GdkIsWaylandDisplay()) {
+ return true;
+ }
+
+ // SetEGLNativeWindowSize() is called from renderer/compositor thread,
+ // make sure nsWindow is not destroyed.
+ bool paint = false;
+
+ // See NS_NATIVE_EGL_WINDOW why we can't block here.
+ if (mDestroyMutex.TryLock()) {
+ if (!mIsDestroyed) {
+ gint scale = GdkCeiledScaleFactor();
+# ifdef MOZ_LOGGING
+ if (LOG_ENABLED()) {
+ static uintptr_t lastSizeLog = 0;
+ uintptr_t sizeLog = uintptr_t(this) + aEGLWindowSize.width +
+ aEGLWindowSize.height + scale +
+ aEGLWindowSize.width / scale +
+ aEGLWindowSize.height / scale;
+ if (lastSizeLog != sizeLog) {
+ lastSizeLog = sizeLog;
+ LOG("nsWindow::SetEGLNativeWindowSize() %d x %d scale %d (unscaled "
+ "%d x "
+ "%d)",
+ aEGLWindowSize.width, aEGLWindowSize.height, scale,
+ aEGLWindowSize.width / scale, aEGLWindowSize.height / scale);
+ }
+ }
+# endif
+ paint = moz_container_wayland_egl_window_set_size(
+ mContainer, aEGLWindowSize.ToUnknownSize(), scale);
+ }
+ mDestroyMutex.Unlock();
+ }
+ return paint;
+}
+#endif
+
+nsWindow* nsWindow::GetWindow(GdkWindow* window) {
+ return get_window_for_gdk_window(window);
+}
+
+void nsWindow::ClearRenderingQueue() {
+ LOG("nsWindow::ClearRenderingQueue()");
+
+ if (mWidgetListener) {
+ mWidgetListener->RequestWindowClose(this);
+ }
+ DestroyLayerManager();
+}
+
+void nsWindow::DisableRendering() {
+ LOG("nsWindow::DisableRendering()");
+
+ if (mGdkWindow) {
+ if (mIMContext) {
+ mIMContext->SetGdkWindow(nullptr);
+ }
+ g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", nullptr);
+ mGdkWindow = nullptr;
+ }
+
+#ifdef MOZ_WAYLAND
+ // Widget is backed by OpenGL EGLSurface created over wl_surface
+ // owned by mContainer.
+ // RenderCompositorEGL::Resume() deletes recent EGLSurface based on
+ // wl_surface owned by mContainer and creates a new fallback EGLSurface.
+ // Then we can delete wl_surface in moz_container_wayland_unmap().
+ // We don't want to pause compositor as it may lead to whole
+ // browser freeze (Bug 1777664).
+ ///
+ // We don't need to do such operation for SW backend as
+ // WindowSurfaceWaylandMB::Commit() gets wl_surface from
+ // MozContainer every commit.
+ if (moz_container_wayland_has_egl_window(mContainer) &&
+ mCompositorWidgetDelegate) {
+ if (CompositorBridgeChild* remoteRenderer = GetRemoteRenderer()) {
+ // Call DisableRendering() to make GtkCompositorWidget hidden.
+ // Then SendResume() will create fallback EGLSurface, see
+ // GLContextEGL::CreateEGLSurfaceForCompositorWidget().
+ mCompositorWidgetDelegate->DisableRendering();
+ remoteRenderer->SendResume();
+ mCompositorWidgetDelegate->EnableRendering(GetX11Window(),
+ GetShapedState());
+ }
+ }
+#endif
+}
+
+// Apply workaround for Mutter compositor bug (mzbz#1777269).
+//
+// When we open a popup window (tooltip for instance) attached to
+// GDK_WINDOW_TYPE_HINT_UTILITY parent popup, Mutter compositor sends bogus
+// leave/enter events to the GDK_WINDOW_TYPE_HINT_UTILITY popup.
+// That leads to immediate tooltip close. As a workaround ignore these
+// bogus events.
+//
+// We need to check two affected window types:
+//
+// - toplevel window with at least two child popups where the first one is
+// GDK_WINDOW_TYPE_HINT_UTILITY.
+// - GDK_WINDOW_TYPE_HINT_UTILITY popup with a child popup
+//
+// We need to mask two bogus leave/enter sequences:
+// 1) Leave (popup) -> Enter (toplevel)
+// 2) Leave (toplevel) -> Enter (popup)
+//
+// TODO: persistent (non-tracked) popups with tooltip/child popups?
+//
+bool nsWindow::ApplyEnterLeaveMutterWorkaround() {
+ // Leave (toplevel) case
+ if (mWindowType == WindowType::TopLevel && mWaylandPopupNext &&
+ mWaylandPopupNext->mWaylandPopupNext &&
+ gtk_window_get_type_hint(GTK_WINDOW(mWaylandPopupNext->GetGtkWidget())) ==
+ GDK_WINDOW_TYPE_HINT_UTILITY) {
+ LOG("nsWindow::ApplyEnterLeaveMutterWorkaround(): leave toplevel");
+ return true;
+ }
+ // Leave (popup) case
+ if (IsWaylandPopup() && mWaylandPopupNext &&
+ gtk_window_get_type_hint(GTK_WINDOW(mShell)) ==
+ GDK_WINDOW_TYPE_HINT_UTILITY) {
+ LOG("nsWindow::ApplyEnterLeaveMutterWorkaround(): leave popup");
+ return true;
+ }
+ return false;
+}
+
+void nsWindow::NotifyOcclusionState(OcclusionState aState) {
+ if (!IsTopLevelWindowType()) {
+ return;
+ }
+
+ bool isFullyOccluded = aState == OcclusionState::OCCLUDED;
+ if (mIsFullyOccluded == isFullyOccluded) {
+ return;
+ }
+ mIsFullyOccluded = isFullyOccluded;
+
+ LOG("nsWindow::NotifyOcclusionState() mIsFullyOccluded %d", mIsFullyOccluded);
+ if (mWidgetListener) {
+ mWidgetListener->OcclusionStateChanged(mIsFullyOccluded);
+ }
+}
+
+void nsWindow::SetDragSource(GdkDragContext* aSourceDragContext) {
+ mSourceDragContext = aSourceDragContext;
+ if (IsPopup() &&
+ (widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol())) {
+ if (auto* menuPopupFrame = GetMenuPopupFrame(GetFrame())) {
+ menuPopupFrame->SetIsDragSource(!!aSourceDragContext);
+ }
+ }
+}
diff --git a/widget/gtk/nsWindow.h b/widget/gtk/nsWindow.h
new file mode 100644
index 0000000000..e235d12c08
--- /dev/null
+++ b/widget/gtk/nsWindow.h
@@ -0,0 +1,1016 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsWindow_h__
+#define __nsWindow_h__
+
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+
+#include "CompositorWidget.h"
+#include "MozContainer.h"
+#include "VsyncSource.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/widget/WindowSurface.h"
+#include "mozilla/widget/WindowSurfaceProvider.h"
+#include "nsBaseWidget.h"
+#include "nsGkAtoms.h"
+#include "nsIDragService.h"
+#include "nsRefPtrHashtable.h"
+#include "IMContextWrapper.h"
+#include "LookAndFeel.h"
+
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/LocalAccessible.h"
+#endif
+
+#ifdef MOZ_X11
+# include <gdk/gdkx.h>
+# include "X11UndefineNone.h"
+#endif
+#ifdef MOZ_WAYLAND
+# include <gdk/gdkwayland.h>
+# include "base/thread.h"
+# include "WaylandVsyncSource.h"
+# include "nsClipboardWayland.h"
+#endif
+
+#ifdef MOZ_LOGGING
+
+# include "mozilla/Logging.h"
+# include "nsTArray.h"
+# include "Units.h"
+
+extern mozilla::LazyLogModule gWidgetLog;
+extern mozilla::LazyLogModule gWidgetDragLog;
+extern mozilla::LazyLogModule gWidgetPopupLog;
+extern mozilla::LazyLogModule gWidgetVsync;
+
+# define LOG(str, ...) \
+ MOZ_LOG(IsPopup() ? gWidgetPopupLog : gWidgetLog, \
+ mozilla::LogLevel::Debug, \
+ ("%s: " str, GetDebugTag().get(), ##__VA_ARGS__))
+# define LOGW(...) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define LOGDRAG(...) \
+ MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define LOG_POPUP(...) \
+ MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define LOG_VSYNC(...) \
+ MOZ_LOG(gWidgetVsync, mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define LOG_ENABLED() \
+ (MOZ_LOG_TEST(gWidgetPopupLog, mozilla::LogLevel::Debug) || \
+ MOZ_LOG_TEST(gWidgetLog, mozilla::LogLevel::Debug))
+
+#else
+
+# define LOG(...)
+# define LOGW(...)
+# define LOGDRAG(...)
+# define LOG_POPUP(...)
+# define LOG_ENABLED() false
+
+#endif /* MOZ_LOGGING */
+
+#if defined(MOZ_WAYLAND) && !defined(MOZ_X11)
+typedef uintptr_t Window;
+#endif
+
+class gfxPattern;
+class nsIFrame;
+#if !GTK_CHECK_VERSION(3, 18, 0)
+struct _GdkEventTouchpadPinch;
+typedef struct _GdkEventTouchpadPinch GdkEventTouchpadPinch;
+#endif
+
+#if !GTK_CHECK_VERSION(3, 22, 0)
+typedef enum {
+ GDK_ANCHOR_FLIP_X = 1 << 0,
+ GDK_ANCHOR_FLIP_Y = 1 << 1,
+ GDK_ANCHOR_SLIDE_X = 1 << 2,
+ GDK_ANCHOR_SLIDE_Y = 1 << 3,
+ GDK_ANCHOR_RESIZE_X = 1 << 4,
+ GDK_ANCHOR_RESIZE_Y = 1 << 5,
+ GDK_ANCHOR_FLIP = GDK_ANCHOR_FLIP_X | GDK_ANCHOR_FLIP_Y,
+ GDK_ANCHOR_SLIDE = GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_SLIDE_Y,
+ GDK_ANCHOR_RESIZE = GDK_ANCHOR_RESIZE_X | GDK_ANCHOR_RESIZE_Y
+} GdkAnchorHints;
+#endif
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class TimeStamp;
+#ifdef MOZ_X11
+class CurrentX11TimeGetter;
+#endif
+
+namespace widget {
+class Screen;
+} // namespace widget
+} // namespace mozilla
+
+class nsWindow final : public nsBaseWidget {
+ public:
+ typedef mozilla::gfx::DrawTarget DrawTarget;
+ typedef mozilla::WidgetEventTime WidgetEventTime;
+ typedef mozilla::WidgetKeyboardEvent WidgetKeyboardEvent;
+ typedef mozilla::widget::PlatformCompositorWidgetDelegate
+ PlatformCompositorWidgetDelegate;
+
+ nsWindow();
+
+ static void ReleaseGlobals();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsWindow, nsBaseWidget)
+
+ nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+
+ // called when we are destroyed
+ void OnDestroy() override;
+
+ // called to check and see if a widget's dimensions are sane
+ bool AreBoundsSane(void);
+
+ // nsIWidget
+ using nsBaseWidget::Create; // for Create signature not overridden here
+ [[nodiscard]] nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* aInitData) override;
+ void Destroy() override;
+ nsIWidget* GetParent() override;
+ float GetDPI() override;
+ double GetDefaultScaleInternal() override;
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() override;
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScaleByScreen()
+ override;
+ void SetParent(nsIWidget* aNewParent) override;
+ void SetModal(bool aModal) override;
+ bool IsVisible() const override;
+ bool IsMapped() const override;
+ void ConstrainPosition(DesktopIntPoint&) override;
+ void SetSizeConstraints(const SizeConstraints& aConstraints) override;
+ void LockAspectRatio(bool aShouldLock) override;
+ void Move(double aX, double aY) override;
+ void Show(bool aState) override;
+ void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+ bool IsEnabled() const override;
+
+ void SetZIndex(int32_t aZIndex) override;
+ nsSizeMode SizeMode() override { return mSizeMode; }
+ void SetSizeMode(nsSizeMode aMode) override;
+ void GetWorkspaceID(nsAString& workspaceID) override;
+ void MoveToWorkspace(const nsAString& workspaceID) override;
+ void Enable(bool aState) override;
+ void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ LayoutDeviceIntRect GetScreenBounds() override;
+ LayoutDeviceIntRect GetClientBounds() override;
+ LayoutDeviceIntSize GetClientSize() override;
+ LayoutDeviceIntPoint GetClientOffset() override { return mClientOffset; }
+ LayoutDeviceIntPoint GetScreenEdgeSlop() override;
+
+ // Recomputes the client offset according to our current window position.
+ // If aNotify is true, NotifyWindowMoved will be called on client offset
+ // changes.
+ //
+ // NOTE(emilio): It seems that as long any change here update either the size
+ // or the position of the window, we should be doing fine without notifying,
+ // but this is done to preserve existing behavior.
+ void RecomputeClientOffset(bool aNotify);
+
+ void SetCursor(const Cursor&) override;
+ void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ void* GetNativeData(uint32_t aDataType) override;
+ nsresult SetTitle(const nsAString& aTitle) override;
+ void SetIcon(const nsAString& aIconSpec) override;
+ void SetWindowClass(const nsAString& xulWinType, const nsAString& xulWinClass,
+ const nsAString& xulWinName) override;
+ LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ void CaptureRollupEvents(bool aDoCapture) override;
+ [[nodiscard]] nsresult GetAttention(int32_t aCycleCount) override;
+ bool HasPendingInputEvent() override;
+
+ bool PrepareForFullscreenTransition(nsISupports** aData) override;
+ void PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration, nsISupports* aData,
+ nsIRunnable* aCallback) override;
+ already_AddRefed<Screen> GetWidgetScreen() override;
+ nsresult MakeFullScreen(bool aFullScreen) override;
+ void HideWindowChrome(bool aShouldHide) override;
+
+ /**
+ * GetLastUserInputTime returns a timestamp for the most recent user input
+ * event. This is intended for pointer grab requests (including drags).
+ */
+ static guint32 GetLastUserInputTime();
+
+ // utility method, -1 if no change should be made, otherwise returns a
+ // value that can be passed to gdk_window_set_decorations
+ gint ConvertBorderStyles(BorderStyle aStyle);
+
+ mozilla::widget::IMContextWrapper* GetIMContext() const { return mIMContext; }
+
+ bool DispatchCommandEvent(nsAtom* aCommand);
+ bool DispatchContentCommandEvent(mozilla::EventMessage aMsg);
+
+ // event callbacks
+ gboolean OnExposeEvent(cairo_t* cr);
+ gboolean OnConfigureEvent(GtkWidget* aWidget, GdkEventConfigure* aEvent);
+ void OnMap();
+ void OnUnmap();
+ void OnSizeAllocate(GtkAllocation* aAllocation);
+ void OnDeleteEvent();
+ void OnEnterNotifyEvent(GdkEventCrossing* aEvent);
+ void OnLeaveNotifyEvent(GdkEventCrossing* aEvent);
+ void OnMotionNotifyEvent(GdkEventMotion* aEvent);
+ void OnButtonPressEvent(GdkEventButton* aEvent);
+ void OnButtonReleaseEvent(GdkEventButton* aEvent);
+ void OnContainerFocusInEvent(GdkEventFocus* aEvent);
+ void OnContainerFocusOutEvent(GdkEventFocus* aEvent);
+ gboolean OnKeyPressEvent(GdkEventKey* aEvent);
+ gboolean OnKeyReleaseEvent(GdkEventKey* aEvent);
+
+ void OnScrollEvent(GdkEventScroll* aEvent);
+
+ void OnVisibilityNotifyEvent(GdkVisibilityState aState);
+ void OnWindowStateEvent(GtkWidget* aWidget, GdkEventWindowState* aEvent);
+ void OnDragDataReceivedEvent(GtkWidget* aWidget, GdkDragContext* aDragContext,
+ gint aX, gint aY,
+ GtkSelectionData* aSelectionData, guint aInfo,
+ guint aTime, gpointer aData);
+ gboolean OnPropertyNotifyEvent(GtkWidget* aWidget, GdkEventProperty* aEvent);
+ gboolean OnTouchEvent(GdkEventTouch* aEvent);
+ gboolean OnTouchpadPinchEvent(GdkEventTouchpadPinch* aEvent);
+
+ gint GetInputRegionMarginInGdkCoords();
+
+ void UpdateTopLevelOpaqueRegion();
+
+ already_AddRefed<mozilla::gfx::DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ mozilla::layers::BufferMode* aBufferMode) override;
+ void EndRemoteDrawingInRegion(
+ mozilla::gfx::DrawTarget* aDrawTarget,
+ const LayoutDeviceIntRegion& aInvalidRegion) override;
+
+ void SetProgress(unsigned long progressPercent);
+
+ RefPtr<mozilla::VsyncDispatcher> GetVsyncDispatcher() override;
+ bool SynchronouslyRepaintOnResize() override;
+
+ void OnDPIChanged();
+ void OnCheckResize();
+ void OnCompositedChanged();
+ void OnScaleChanged(bool aNotify);
+ void DispatchResized();
+
+ static guint32 sLastButtonPressTime;
+
+ MozContainer* GetMozContainer() { return mContainer; }
+ GdkWindow* GetGdkWindow() const { return mGdkWindow; };
+ GdkWindow* GetToplevelGdkWindow() const;
+ GtkWidget* GetGtkWidget() const { return mShell; }
+ nsIFrame* GetFrame() const;
+ nsWindow* GetEffectiveParent();
+ bool IsDestroyed() const { return mIsDestroyed; }
+ bool IsPopup() const;
+ bool IsWaylandPopup() const;
+ bool IsDragPopup() { return mIsDragPopup; };
+
+ nsAutoCString GetDebugTag() const;
+
+ void DispatchDragEvent(mozilla::EventMessage aMsg,
+ const LayoutDeviceIntPoint& aRefPoint, guint aTime);
+ static void UpdateDragStatus(GdkDragContext* aDragContext,
+ nsIDragService* aDragService);
+ void SetDragSource(GdkDragContext* aSourceDragContext);
+
+ WidgetEventTime GetWidgetEventTime(guint32 aEventTime);
+ mozilla::TimeStamp GetEventTimeStamp(guint32 aEventTime);
+#ifdef MOZ_X11
+ mozilla::CurrentX11TimeGetter* GetCurrentTimeGetter();
+#endif
+
+ void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ InputContext GetInputContext() override;
+ TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override;
+ MOZ_CAN_RUN_SCRIPT bool GetEditCommands(
+ mozilla::NativeKeyBindingsType aType,
+ const mozilla::WidgetKeyboardEvent& aEvent,
+ nsTArray<mozilla::CommandInt>& aCommands) override;
+
+ // These methods are for toplevel windows only.
+ void ResizeTransparencyBitmap();
+ void ApplyTransparencyBitmap();
+ void ClearTransparencyBitmap();
+
+ void SetTransparencyMode(TransparencyMode aMode) override;
+ TransparencyMode GetTransparencyMode() override;
+ void SetInputRegion(const InputRegion&) override;
+ nsresult UpdateTranslucentWindowAlphaInternal(const nsIntRect& aRect,
+ uint8_t* aAlphas,
+ int32_t aStride);
+ void ReparentNativeWidget(nsIWidget* aNewParent) override;
+
+ void UpdateTitlebarTransparencyBitmap();
+
+ nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ return SynthesizeNativeMouseEvent(
+ aPoint, NativeMouseMessage::Move, mozilla::MouseButton::eNotPressed,
+ nsIWidget::Modifiers::NO_MODIFIERS, aObserver);
+ }
+
+ nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override;
+
+ nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+
+ nsresult SynthesizeNativeTouchPadPinch(TouchpadGesturePhase aEventPhase,
+ float aScale,
+ LayoutDeviceIntPoint aPoint,
+ int32_t aModifierFlags) override;
+
+ nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ void GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) override;
+
+ nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) override;
+ void SetDrawsInTitlebar(bool aState);
+ void SetTitlebarRect();
+ mozilla::LayoutDeviceIntCoord GetTitlebarRadius();
+ LayoutDeviceIntRect GetTitlebarRect();
+ void UpdateWindowDraggingRegion(
+ const LayoutDeviceIntRegion& aRegion) override;
+
+ // HiDPI scale conversion
+ gint GdkCeiledScaleFactor();
+ double FractionalScaleFactor();
+
+ // To GDK
+ gint DevicePixelsToGdkCoordRoundUp(int);
+ gint DevicePixelsToGdkCoordRoundDown(int);
+ GdkPoint DevicePixelsToGdkPointRoundDown(const LayoutDeviceIntPoint&);
+ GdkRectangle DevicePixelsToGdkSizeRoundUp(const LayoutDeviceIntSize&);
+ GdkRectangle DevicePixelsToGdkRectRoundOut(const LayoutDeviceIntRect&);
+
+ // From GDK
+ int GdkCoordToDevicePixels(gint);
+ LayoutDeviceIntPoint GdkPointToDevicePixels(const GdkPoint&);
+ LayoutDeviceIntPoint GdkEventCoordsToDevicePixels(gdouble aX, gdouble aY);
+ LayoutDeviceIntRect GdkRectToDevicePixels(const GdkRectangle&);
+
+ bool WidgetTypeSupportsAcceleration() override;
+
+ nsresult SetSystemFont(const nsCString& aFontName) override;
+ nsresult GetSystemFont(nsCString& aFontName) override;
+
+ typedef enum {
+ GTK_DECORATION_SYSTEM, // CSD including shadows
+ GTK_DECORATION_CLIENT, // CSD without shadows
+ GTK_DECORATION_NONE, // WM does not support CSD at all
+ } GtkWindowDecoration;
+ /**
+ * Get the support of Client Side Decoration by checking the desktop
+ * environment.
+ */
+ static GtkWindowDecoration GetSystemGtkWindowDecoration();
+
+ static bool GetTopLevelWindowActiveState(nsIFrame* aFrame);
+ static bool TitlebarUseShapeMask();
+ bool IsRemoteContent() { return HasRemoteContent(); }
+ void NativeMoveResizeWaylandPopupCallback(const GdkRectangle* aFinalSize,
+ bool aFlippedX, bool aFlippedY);
+ static bool IsToplevelWindowTransparent();
+
+ static nsWindow* GetFocusedWindow();
+
+#ifdef MOZ_WAYLAND
+ // Use xdg-activation protocol to transfer focus from gFocusWindow to aWindow.
+ static void TransferFocusToWaylandWindow(nsWindow* aWindow);
+ void FocusWaylandWindow(const char* aTokenID);
+
+ bool GetCSDDecorationOffset(int* aDx, int* aDy);
+ bool SetEGLNativeWindowSize(const LayoutDeviceIntSize& aEGLWindowSize);
+ void WaylandDragWorkaround(GdkEventButton* aEvent);
+
+ void CreateCompositorVsyncDispatcher() override;
+ LayoutDeviceIntPoint GetNativePointerLockCenter() {
+ return mNativePointerLockCenter;
+ }
+ void SetNativePointerLockCenter(
+ const LayoutDeviceIntPoint& aLockCenter) override;
+ void LockNativePointer() override;
+ void UnlockNativePointer() override;
+ LayoutDeviceIntSize GetMoveToRectPopupSize() const override {
+ return mMoveToRectPopupSize;
+ };
+#endif
+
+ typedef enum {
+ // WebRender compositor is enabled
+ COMPOSITOR_ENABLED,
+ // WebRender compositor is paused as we're repainting whole window and
+ // we're waiting for content process to update page content.
+ COMPOSITOR_PAUSED_FLICKERING
+ } WindowCompositorState;
+
+ // Pause compositor to avoid rendering artifacts from content process.
+ void ResumeCompositorImpl();
+ void ResumeCompositorFlickering();
+ void ResumeCompositorFromCompositorThread();
+ void PauseCompositorFlickering();
+ bool IsWaitingForCompositorResume();
+
+ // Force hide this window, remove compositor etc. to avoid
+ // rendering queue blocking (see Bug 1782948).
+ void ClearRenderingQueue();
+
+ void DisableRendering();
+
+ bool ApplyEnterLeaveMutterWorkaround();
+
+ void NotifyOcclusionState(mozilla::widget::OcclusionState aState) override;
+
+ static nsWindow* GetWindow(GdkWindow* window);
+
+ protected:
+ virtual ~nsWindow();
+
+ // event handling code
+ void DispatchActivateEvent(void);
+ void DispatchDeactivateEvent(void);
+ void MaybeDispatchResized();
+ void DispatchPanGesture(mozilla::PanGestureInput& aPanInput);
+
+ void RegisterTouchWindow() override;
+
+ nsCOMPtr<nsIWidget> mParent;
+ mozilla::Atomic<int, mozilla::Relaxed> mCeiledScaleFactor{1};
+ double mFractionalScaleFactor = 0.0;
+
+ void UpdateAlpha(mozilla::gfx::SourceSurface* aSourceSurface,
+ nsIntRect aBoundsRect);
+
+ void NativeMoveResize(bool aMoved, bool aResized);
+
+ void NativeShow(bool aAction);
+ void SetHasMappedToplevel(bool aState);
+ LayoutDeviceIntSize GetSafeWindowSize(LayoutDeviceIntSize aSize);
+
+ void DispatchContextMenuEventFromMouseEvent(
+ uint16_t domButton, GdkEventButton* aEvent,
+ const mozilla::LayoutDeviceIntPoint& aRefPoint);
+
+ void TryToShowNativeWindowMenu(GdkEventButton* aEvent);
+
+ bool DoTitlebarAction(mozilla::LookAndFeel::TitlebarEvent aEvent,
+ GdkEventButton* aButtonEvent);
+
+ void WaylandStartVsync();
+ void WaylandStopVsync();
+ void DestroyChildWindows();
+ GtkWidget* GetToplevelWidget() const;
+ nsWindow* GetContainerWindow() const;
+ Window GetX11Window();
+ bool GetShapedState();
+ void EnsureGdkWindow();
+ void SetUrgencyHint(GtkWidget* top_window, bool state);
+ void SetDefaultIcon(void);
+ void SetWindowDecoration(BorderStyle aStyle);
+ void InitButtonEvent(mozilla::WidgetMouseEvent& aEvent,
+ GdkEventButton* aGdkEvent,
+ const mozilla::LayoutDeviceIntPoint& aRefPoint);
+ bool CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel,
+ bool aAlwaysRollup);
+ void RollupAllMenus() { CheckForRollup(0, 0, false, true); }
+ void CheckForRollupDuringGrab() { RollupAllMenus(); }
+
+ bool GetDragInfo(mozilla::WidgetMouseEvent* aMouseEvent, GdkWindow** aWindow,
+ gint* aButton, gint* aRootX, gint* aRootY);
+ nsIWidgetListener* GetListener();
+
+ nsWindow* GetTransientForWindowIfPopup();
+ bool IsHandlingTouchSequence(GdkEventSequence* aSequence);
+
+ void ResizeInt(const mozilla::Maybe<LayoutDeviceIntPoint>& aMove,
+ LayoutDeviceIntSize aSize);
+ void NativeMoveResizeWaylandPopup(bool aMove, bool aResize);
+
+ // Returns a window edge if the given point (in device pixels) is within a
+ // resizer region of the window.
+ // Only used when drawing decorations client side.
+ mozilla::Maybe<GdkWindowEdge> CheckResizerEdge(const LayoutDeviceIntPoint&);
+
+ GtkTextDirection GetTextDirection();
+
+ bool DrawsToCSDTitlebar() const;
+ void AddCSDDecorationSize(int* aWidth, int* aHeight);
+
+ void CreateAndPutGdkScrollEvent(mozilla::LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY);
+
+ nsCString mGtkWindowAppClass;
+ nsCString mGtkWindowAppName;
+ nsCString mGtkWindowRoleName;
+ void RefreshWindowClass();
+
+ GtkWidget* mShell = nullptr;
+ MozContainer* mContainer = nullptr;
+ GdkWindow* mGdkWindow = nullptr;
+ PlatformCompositorWidgetDelegate* mCompositorWidgetDelegate = nullptr;
+ mozilla::Atomic<WindowCompositorState, mozilla::Relaxed> mCompositorState{
+ COMPOSITOR_ENABLED};
+ // This is used in COMPOSITOR_PAUSED_FLICKERING mode only to resume compositor
+ // in some reasonable time when page content is not updated.
+ guint mCompositorPauseTimeoutID = 0;
+
+ // The actual size mode that's in effect.
+ nsSizeMode mSizeMode = nsSizeMode_Normal;
+ // The last size mode we've requested. This might not match mSizeMode if
+ // there's a request to change the size mode in progress.
+ nsSizeMode mLastSizeModeRequest = nsSizeMode_Normal;
+ nsSizeMode mLastSizeModeBeforeFullscreen = nsSizeMode_Normal;
+
+ float mAspectRatio = 0.0f;
+ float mAspectRatioSaved = 0.0f;
+ mozilla::Maybe<GtkOrientation> mAspectResizer;
+ LayoutDeviceIntPoint mLastResizePoint;
+
+ // The size requested, which might not be reflected in mBounds. Used in
+ // WaylandPopupSetDirectPosition() to remember intended size for popup
+ // positioning, in LockAspect() to remember the intended aspect ratio, and
+ // to remember a size requested while waiting for moved-to-rect when
+ // OnSizeAllocate() might change mBounds.Size().
+ LayoutDeviceIntSize mLastSizeRequest;
+ LayoutDeviceIntPoint mClientOffset;
+ // Indicates a new size that still needs to be dispatched.
+ LayoutDeviceIntSize mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1);
+
+ // This field omits duplicate scroll events caused by GNOME bug 726878.
+ guint32 mLastScrollEventTime = GDK_CURRENT_TIME;
+ mozilla::ScreenCoord mLastPinchEventSpan;
+
+ struct TouchpadPinchGestureState {
+ // Focus point of the PHASE_BEGIN event
+ ScreenPoint mBeginFocus;
+
+ // Focus point of the most recent PHASE_UPDATE event
+ ScreenPoint mCurrentFocus;
+ };
+
+ // Used for handling touchpad pinch gestures
+ ScreenPoint mCurrentTouchpadFocus;
+
+ // Used for synthesizing touchpad pinch gestures
+ TouchpadPinchGestureState mCurrentSynthesizedTouchpadPinch;
+
+ // Used for synthesizing touchpad pan gestures
+ struct TouchpadPanGestureState {
+ mozilla::Maybe<TouchpadGesturePhase> mTouchpadGesturePhase;
+ uint64_t mSavedObserver = 0;
+ };
+
+ // Used for synthesizing touchpad pan gestures
+ TouchpadPanGestureState mCurrentSynthesizedTouchpadPan;
+
+ // for touch event handling
+ nsRefPtrHashtable<nsPtrHashKey<GdkEventSequence>, mozilla::dom::Touch>
+ mTouches;
+
+ // Upper bound on pending ConfigureNotify events to be dispatched to the
+ // window. See bug 1225044.
+ unsigned int mPendingConfigures = 0;
+
+ // Window titlebar rendering mode, GTK_DECORATION_NONE if it's disabled
+ // for this window.
+ GtkWindowDecoration mGtkWindowDecoration = GTK_DECORATION_NONE;
+
+ // Draggable titlebar region maintained by UpdateWindowDraggingRegion
+ LayoutDeviceIntRegion mDraggableRegion;
+
+ // The cursor cache
+ static GdkCursor* gsGtkCursorCache[eCursorCount];
+
+ // If true, draw our own window titlebar.
+ bool mDrawInTitlebar = false;
+
+ mozilla::Mutex mTitlebarRectMutex;
+ LayoutDeviceIntRect mTitlebarRect MOZ_GUARDED_BY(mTitlebarRectMutex);
+
+ mozilla::Mutex mDestroyMutex;
+
+ // Has this widget been destroyed yet?
+ bool mIsDestroyed : 1;
+ // mIsShown tracks requested visible status from browser perspective, i.e.
+ // if the window should be visible or now.
+ bool mIsShown : 1;
+ // mNeedsShow is set when browser requested to show this window but we failed
+ // to do so for some reason (wrong window size for instance).
+ // In such case we set mIsShown = true and mNeedsShow = true to indicate
+ // that the window is not actually visible but we report to browser that
+ // it is visible (mIsShown == true).
+ bool mNeedsShow : 1;
+ // This track real window visibility from OS perspective.
+ // It's set by OnMap/OnUnmap which is based on Gtk events.
+ bool mIsMapped : 1;
+ // is this widget enabled?
+ bool mEnabled : 1;
+ // has the native window for this been created yet?
+ bool mCreated : 1;
+ // whether we handle touch event
+ bool mHandleTouchEvent : 1;
+ // true if this is a drag and drop feedback popup
+ bool mIsDragPopup : 1;
+ bool mCompositedScreen : 1;
+ bool mIsAccelerated : 1;
+ bool mWindowShouldStartDragging : 1;
+ bool mHasMappedToplevel : 1;
+ bool mRetryPointerGrab : 1;
+ bool mPanInProgress : 1;
+ // Draw titlebar with :backdrop css state (inactive/unfocused).
+ bool mTitlebarBackdropState : 1;
+ // It's child window, i.e. window which is nested in parent window.
+ // This is obsoleted and should not be used.
+ // We use GdkWindow hierarchy for such windows.
+ bool mIsChildWindow : 1;
+ bool mAlwaysOnTop : 1;
+ bool mNoAutoHide : 1;
+ bool mIsTransparent : 1;
+ // We can expect at least one size-allocate event after early resizes.
+ bool mHasReceivedSizeAllocate : 1;
+ bool mWidgetCursorLocked : 1;
+ bool mUndecorated : 1;
+
+ /* Gkt creates popup in two incarnations - wl_subsurface and xdg_popup.
+ * Kind of popup is choosen before GdkWindow is mapped so we can change
+ * it only when GdkWindow is hidden.
+ *
+ * Relevant Gtk code is at gdkwindow-wayland.c
+ * in should_map_as_popup() and should_map_as_subsurface()
+ *
+ * wl_subsurface:
+ * - can't be positioned by move-to-rect
+ * - can stand outside popup widget hierarchy (has toplevel as parent)
+ * - don't have child popup widgets
+ *
+ * xdg_popup:
+ * - can be positioned by move-to-rect
+ * - aligned in popup widget hierarchy, first one is attached to toplevel
+ * - has child (popup) widgets
+ *
+ * Thus we need to map Firefox popup type to desired Gtk one:
+ *
+ * wl_subsurface:
+ * - pernament panels
+ *
+ * xdg_popup:
+ * - menus
+ * - autohide popups (hamburger menu)
+ * - extension popups
+ * - tooltips
+ *
+ * We set mPopupTrackInHierarchy = false for pernament panels which
+ * are always mapped to toplevel and painted as wl_surfaces.
+ */
+ bool mPopupTrackInHierarchy : 1;
+ bool mPopupTrackInHierarchyConfigured : 1;
+
+ /* On X11 Gtk tends to ignore window position requests when gtk_window
+ * is hidden. Save the position requests at mPopupPosition and apply
+ * when the widget is shown.
+ */
+ bool mHiddenPopupPositioned : 1;
+
+ // The transparency bitmap is used instead of ARGB visual for toplevel
+ // window to draw titlebar.
+ bool mTransparencyBitmapForTitlebar : 1;
+
+ // True when we're on compositing window manager and this
+ // window is using visual with alpha channel.
+ bool mHasAlphaVisual : 1;
+
+ // When popup is anchored, mPopupPosition is relative to its parent popup.
+ bool mPopupAnchored : 1;
+
+ // When popup is context menu.
+ bool mPopupContextMenu : 1;
+
+ // Indicates that this popup matches layout setup so we can use parent popup
+ // coordinates reliably.
+ bool mPopupMatchesLayout : 1;
+
+ /* Indicates that popup setup was changed and
+ * we need to recalculate popup coordinates.
+ */
+ bool mPopupChanged : 1;
+
+ // Popup is hidden only as a part of hierarchy tree update.
+ bool mPopupTemporaryHidden : 1;
+
+ // Popup is going to be closed and removed.
+ bool mPopupClosed : 1;
+
+ // Popup is positioned by gdk_window_move_to_rect()
+ bool mPopupUseMoveToRect : 1;
+
+ /* mWaitingForMoveToRectCallback is set when move-to-rect is called
+ * and we're waiting for move-to-rect callback.
+ *
+ * If another position/resize request comes between move-to-rect call and
+ * move-to-rect callback we set mMovedAfterMoveToRect/mResizedAfterMoveToRect.
+ */
+ bool mWaitingForMoveToRectCallback : 1;
+ bool mMovedAfterMoveToRect : 1;
+ bool mResizedAfterMoveToRect : 1;
+
+ // Params used for popup placemend by GdkWindowMoveToRect.
+ // When popup is only resized and not positioned,
+ // we need to reuse last GdkWindowMoveToRect params to avoid
+ // popup movement.
+ struct WaylandPopupMoveToRectParams {
+ LayoutDeviceIntRect mAnchorRect = {0, 0, 0, 0};
+ GdkGravity mAnchorRectType = GDK_GRAVITY_NORTH_WEST;
+ GdkGravity mPopupAnchorType = GDK_GRAVITY_NORTH_WEST;
+ GdkAnchorHints mHints = GDK_ANCHOR_SLIDE;
+ GdkPoint mOffset = {0, 0};
+ bool mAnchorSet = false;
+ };
+
+ WaylandPopupMoveToRectParams mPopupMoveToRectParams;
+
+ // Whether we've configured default clear color already.
+ bool mConfiguredClearColor : 1;
+ // Whether we've received a non-blank paint in which case we can reset the
+ // clear color to transparent.
+ bool mGotNonBlankPaint : 1;
+
+ // Whether we need to retry capturing the mouse because we' re not mapped yet.
+ bool mNeedsToRetryCapturingMouse : 1;
+
+ // This bitmap tracks which pixels are transparent. We don't support
+ // full translucency at this time; each pixel is either fully opaque
+ // or fully transparent.
+ gchar* mTransparencyBitmap = nullptr;
+ int32_t mTransparencyBitmapWidth = 0;
+ int32_t mTransparencyBitmapHeight = 0;
+
+ // all of our DND stuff
+ void InitDragEvent(mozilla::WidgetDragEvent& aEvent);
+
+ float mLastMotionPressure = 0.0f;
+
+ InputRegion mInputRegion;
+
+ static bool DragInProgress(void);
+
+ void DispatchMissedButtonReleases(GdkEventCrossing* aGdkEvent);
+
+ // When window widget gets mapped/unmapped we need to configure
+ // underlying GdkWindow properly. Otherwise we'll end up with
+ // rendering to released window.
+ void ConfigureGdkWindow();
+ void ReleaseGdkWindow();
+ void ConfigureCompositor();
+
+ bool IsAlwaysUndecoratedWindow() const;
+
+ // nsBaseWidget
+ WindowRenderer* GetWindowRenderer() override;
+ void DidGetNonBlankPaint() override;
+
+ void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override;
+
+ int32_t RoundsWidgetCoordinatesTo() override;
+
+ void UpdateMozWindowActive();
+
+ void ForceTitlebarRedraw();
+ bool DoDrawTilebarCorners();
+ bool IsChromeWindowTitlebar();
+
+ void SetPopupWindowDecoration(bool aShowOnTaskbar);
+
+ void ApplySizeConstraints();
+
+ // Wayland Popup section
+ GdkPoint WaylandGetParentPosition();
+ bool WaylandPopupConfigure();
+ bool WaylandPopupIsAnchored();
+ bool WaylandPopupIsMenu();
+ bool WaylandPopupIsContextMenu();
+ bool WaylandPopupIsPermanent();
+ // First popup means it's attached directly to toplevel window
+ bool WaylandPopupIsFirst();
+ bool IsWidgetOverflowWindow();
+ void RemovePopupFromHierarchyList();
+ void ShowWaylandPopupWindow();
+ void HideWaylandPopupWindow(bool aTemporaryHidden, bool aRemoveFromPopupList);
+ void ShowWaylandToplevelWindow();
+ void HideWaylandToplevelWindow();
+ void WaylandPopupHideTooltips();
+ void WaylandPopupCloseOrphanedPopups();
+ void AppendPopupToHierarchyList(nsWindow* aToplevelWindow);
+ void WaylandPopupHierarchyHideTemporary();
+ void WaylandPopupHierarchyShowTemporaryHidden();
+ void WaylandPopupHierarchyCalculatePositions();
+ bool IsInPopupHierarchy();
+ void AddWindowToPopupHierarchy();
+ void UpdateWaylandPopupHierarchy();
+ void WaylandPopupHierarchyHideByLayout(
+ nsTArray<nsIWidget*>* aLayoutWidgetHierarchy);
+ void WaylandPopupHierarchyValidateByLayout(
+ nsTArray<nsIWidget*>* aLayoutWidgetHierarchy);
+ void CloseAllPopupsBeforeRemotePopup();
+ void WaylandPopupHideClosedPopups();
+ void WaylandPopupPrepareForMove();
+ void WaylandPopupMoveImpl();
+ void WaylandPopupMovePlain(int aX, int aY);
+ bool WaylandPopupRemoveNegativePosition(int* aX = nullptr, int* aY = nullptr);
+ bool WaylandPopupCheckAndGetAnchor(GdkRectangle* aPopupAnchor,
+ GdkPoint* aOffset);
+ bool WaylandPopupAnchorAdjustForParentPopup(GdkRectangle* aPopupAnchor,
+ GdkPoint* aOffset);
+ nsWindow* GetTopmostWindow();
+ bool IsPopupInLayoutPopupChain(nsTArray<nsIWidget*>* aLayoutWidgetHierarchy,
+ bool aMustMatchParent);
+ void WaylandPopupMarkAsClosed();
+ void WaylandPopupRemoveClosedPopups();
+ void WaylandPopupSetDirectPosition();
+ bool WaylandPopupFitsToplevelWindow(bool aMove);
+ const WaylandPopupMoveToRectParams WaylandPopupGetPositionFromLayout();
+ void WaylandPopupPropagateChangesToLayout(bool aMove, bool aResize);
+ nsWindow* WaylandPopupFindLast(nsWindow* aPopup);
+ GtkWindow* GetCurrentTopmostWindow() const;
+ nsAutoCString GetFrameTag() const;
+ nsCString GetPopupTypeName();
+ bool IsPopupDirectionRTL();
+
+#ifdef MOZ_LOGGING
+ void LogPopupHierarchy();
+ void LogPopupAnchorHints(int aHints);
+ void LogPopupGravity(GdkGravity aGravity);
+#endif
+
+ bool IsTopLevelWindowType() const {
+ return mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog;
+ }
+
+ // mPopupPosition is the original popup position/size from layout, set by
+ // nsWindow::Move() or nsWindow::Resize().
+ // Popup position is relative to main (toplevel) window.
+ GdkPoint mPopupPosition{};
+
+ // mRelativePopupPosition is popup position calculated against
+ // recent popup parent window.
+ GdkPoint mRelativePopupPosition{};
+
+ // Toplevel window (first element) of linked list of Wayland popups. It's null
+ // if we're the toplevel.
+ RefPtr<nsWindow> mWaylandToplevel;
+
+ // Next/Previous popups in Wayland popup hierarchy.
+ RefPtr<nsWindow> mWaylandPopupNext;
+ RefPtr<nsWindow> mWaylandPopupPrev;
+
+ // When popup is resized by Gtk by move-to-rect callback,
+ // we store final popup size here. Then we use mMoveToRectPopupSize size
+ // in following popup operations unless mLayoutPopupSizeCleared is set.
+ LayoutDeviceIntSize mMoveToRectPopupSize;
+
+ /**
+ * |mIMContext| takes all IME related stuff.
+ *
+ * This is owned by the top-level nsWindow or the topmost child
+ * nsWindow embedded in a non-Gecko widget.
+ *
+ * The instance is created when the top level widget is created. And when
+ * the widget is destroyed, it's released. All child windows refer its
+ * ancestor widget's instance. So, one set of IM contexts is created for
+ * all windows in a hierarchy. If the children are released after the top
+ * level window is released, the children still have a valid pointer,
+ * however, IME doesn't work at that time.
+ */
+ RefPtr<mozilla::widget::IMContextWrapper> mIMContext;
+
+#ifdef MOZ_X11
+ mozilla::UniquePtr<mozilla::CurrentX11TimeGetter> mCurrentTimeGetter;
+#endif
+ static GtkWindowDecoration sGtkWindowDecoration;
+
+ static bool sTransparentMainWindow;
+
+#ifdef ACCESSIBILITY
+ RefPtr<mozilla::a11y::LocalAccessible> mRootAccessible;
+
+ /**
+ * Request to create the accessible for this window if it is top level.
+ */
+ void CreateRootAccessible();
+
+ /**
+ * Dispatch accessible event for the top level window accessible.
+ *
+ * @param aEventType [in] the accessible event type to dispatch
+ */
+ void DispatchEventToRootAccessible(uint32_t aEventType);
+
+ /**
+ * Dispatch accessible window activate event for the top level window
+ * accessible.
+ */
+ void DispatchActivateEventAccessible();
+
+ /**
+ * Dispatch accessible window deactivate event for the top level window
+ * accessible.
+ */
+ void DispatchDeactivateEventAccessible();
+
+ /**
+ * Dispatch accessible window maximize event for the top level window
+ * accessible.
+ */
+ void DispatchMaximizeEventAccessible();
+
+ /**
+ * Dispatch accessible window minize event for the top level window
+ * accessible.
+ */
+ void DispatchMinimizeEventAccessible();
+
+ /**
+ * Dispatch accessible window restore event for the top level window
+ * accessible.
+ */
+ void DispatchRestoreEventAccessible();
+#endif
+
+ void SetUserTimeAndStartupTokenForActivatedWindow();
+
+ void KioskLockOnMonitor();
+
+ void EmulateResizeDrag(GdkEventMotion* aEvent);
+
+ void RequestRepaint(LayoutDeviceIntRegion& aRepaintRegion);
+
+#ifdef MOZ_X11
+ typedef enum {GTK_WIDGET_COMPOSIDED_DEFAULT = 0,
+ GTK_WIDGET_COMPOSIDED_DISABLED = 1,
+ GTK_WIDGET_COMPOSIDED_ENABLED = 2} WindowComposeRequest;
+ void SetCompositorHint(WindowComposeRequest aState);
+ bool ConfigureX11GLVisual();
+#endif
+#ifdef MOZ_WAYLAND
+ RefPtr<mozilla::WaylandVsyncSource> mWaylandVsyncSource;
+ RefPtr<mozilla::VsyncDispatcher> mWaylandVsyncDispatcher;
+ LayoutDeviceIntPoint mNativePointerLockCenter;
+ zwp_locked_pointer_v1* mLockedPointer = nullptr;
+ zwp_relative_pointer_v1* mRelativePointer = nullptr;
+#endif
+ // An activation token from our environment (see handling of the
+ // XDG_ACTIVATION_TOKEN/DESKTOP_STARTUP_ID) env vars.
+ nsCString mWindowActivationTokenFromEnv;
+ mozilla::widget::WindowSurfaceProvider mSurfaceProvider;
+ GdkDragContext* mSourceDragContext = nullptr;
+#if MOZ_LOGGING
+ LayoutDeviceIntRect mLastLoggedBoundSize;
+ int mLastLoggedScale = -1;
+#endif
+ // Running in kiosk mode and requested to stay on specified monitor.
+ // If monitor is removed minimize the window.
+ mozilla::Maybe<int> mKioskMonitor;
+};
+
+#endif /* __nsWindow_h__ */
diff --git a/widget/gtk/v4l2test/moz.build b/widget/gtk/v4l2test/moz.build
new file mode 100644
index 0000000000..da22288f17
--- /dev/null
+++ b/widget/gtk/v4l2test/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Startup and Profile System")
+
+Program("v4l2test")
+SOURCES += [
+ "v4l2test.cpp",
+]
+CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+CXXFLAGS += CONFIG["MOZ_PANGO_CFLAGS"]
+OS_LIBS += CONFIG["MOZ_X11_LIBS"]
+OS_LIBS += CONFIG["MOZ_GTK3_LIBS"]
diff --git a/widget/gtk/v4l2test/v4l2test.cpp b/widget/gtk/v4l2test/v4l2test.cpp
new file mode 100644
index 0000000000..6ee685d9ed
--- /dev/null
+++ b/widget/gtk/v4l2test/v4l2test.cpp
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 <cstdio>
+#include <cstdlib>
+#include <errno.h>
+#include <fcntl.h>
+#if defined(__NetBSD__) || defined(__OpenBSD__)
+# include <sys/videoio.h>
+#elif defined(__sun)
+# include <sys/videodev2.h>
+#else
+# include <linux/videodev2.h>
+#endif
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <string.h>
+#include <getopt.h>
+#include <stdint.h>
+#include <stdarg.h>
+
+#if defined(MOZ_ASAN) || defined(FUZZING)
+# include <signal.h>
+#endif
+
+#include "mozilla/ScopeExit.h"
+
+#ifdef __SUNPRO_CC
+# include <stdio.h>
+#endif
+
+#include "mozilla/GfxInfoUtils.h"
+
+// Print test results to stdout and logging to stderr
+#define OUTPUT_PIPE 1
+
+// Convert an integer pixfmt to a 4-character string. str must have a length
+// of at least 5 to include null-termination.
+static void v4l2_pixfmt_to_str(uint32_t pixfmt, char* str) {
+ for (int i = 0; i < 4; i++) {
+ str[i] = (pixfmt >> (i * 8)) & 0xff;
+ }
+ str[4] = 0;
+}
+
+// Enumerate the buffer formats supported on a V4L2 buffer queue. aTypeStr
+// is the queue type, i.e. CAPTURE or OUTPUT.
+static void v4l2_enumfmt(int aFd, int aType, const char* aTypeStr) {
+ struct v4l2_fmtdesc fmt {};
+ char pix_fmt_str[5];
+ fmt.type = aType;
+ record_value("V4L2_%s_FMTS\n", aTypeStr);
+ for (fmt.index = 0;; fmt.index++) {
+ int result = ioctl(aFd, VIDIOC_ENUM_FMT, &fmt);
+ if (result < 0) {
+ break;
+ }
+ v4l2_pixfmt_to_str(fmt.pixelformat, pix_fmt_str);
+ record_value(" %s", pix_fmt_str);
+ }
+ record_value("\n");
+}
+
+// Probe a V4L2 device to work out what it supports
+static void v4l2_check_device(const char* aVideoDevice) {
+ int fd = -1;
+ int result = -1;
+
+ log("v4l2test probing device '%s'\n", aVideoDevice);
+
+ auto autoRelease = mozilla::MakeScopeExit([&] {
+ if (fd >= 0) {
+ close(fd);
+ }
+ });
+
+ fd = open(aVideoDevice, O_RDWR | O_NONBLOCK, 0);
+ if (fd < 0) {
+ record_value("ERROR\nV4L2 failed to open device %s: %s\n", aVideoDevice,
+ strerror(errno));
+ return;
+ }
+
+ struct v4l2_capability cap {};
+ result = ioctl(fd, VIDIOC_QUERYCAP, &cap);
+ if (result < 0) {
+ record_value("ERROR\nV4L2 device %s failed to query capabilities\n",
+ aVideoDevice);
+ return;
+ }
+ log("v4l2test driver %s card %s bus_info %s version %d\n", cap.driver,
+ cap.card, cap.bus_info, cap.version);
+
+ if (!(cap.capabilities & V4L2_CAP_DEVICE_CAPS)) {
+ record_value("ERROR\nV4L2 device %s does not support DEVICE_CAPS\n",
+ aVideoDevice);
+ return;
+ }
+
+ if (!(cap.device_caps & V4L2_CAP_STREAMING)) {
+ record_value("ERROR\nV4L2 device %s does not support V4L2_CAP_STREAMING\n",
+ aVideoDevice);
+ return;
+ }
+
+ // Work out whether the device supports planar or multiplaner bitbuffers and
+ // framebuffers
+ bool splane = cap.device_caps & V4L2_CAP_VIDEO_M2M;
+ bool mplane = cap.device_caps & V4L2_CAP_VIDEO_M2M_MPLANE;
+ if (!splane && !mplane) {
+ record_value("ERROR\nV4L2 device %s does not support M2M modes\n",
+ aVideoDevice);
+ // (It's probably a webcam!)
+ return;
+ }
+ record_value("V4L2_SPLANE\n%s\n", splane ? "TRUE" : "FALSE");
+ record_value("V4L2_MPLANE\n%s\n", mplane ? "TRUE" : "FALSE");
+
+ // Now check the formats supported for CAPTURE and OUTPUT buffers.
+ // For a V4L2-M2M decoder, OUTPUT is actually the bitbuffers we put in and
+ // CAPTURE is the framebuffers we get out.
+ v4l2_enumfmt(
+ fd,
+ mplane ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE : V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ "CAPTURE");
+ v4l2_enumfmt(
+ fd,
+ mplane ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE : V4L2_BUF_TYPE_VIDEO_OUTPUT,
+ "OUTPUT");
+
+ record_value("V4L2_SUPPORTED\nTRUE\n");
+}
+
+static void PrintUsage() {
+ printf(
+ "Firefox V4L2-M2M probe utility\n"
+ "\n"
+ "usage: v4l2test [options]\n"
+ "\n"
+ "Options:\n"
+ "\n"
+ " -h --help show this message\n"
+ " -d --device device Probe a v4l2 device (e.g. /dev/video10)\n"
+ "\n");
+}
+
+int main(int argc, char** argv) {
+ struct option longOptions[] = {{"help", no_argument, nullptr, 'h'},
+ {"device", required_argument, nullptr, 'd'},
+ {nullptr, 0, nullptr, 0}};
+ const char* shortOptions = "hd:";
+ int c;
+ const char* device = nullptr;
+ while ((c = getopt_long(argc, argv, shortOptions, longOptions, nullptr)) !=
+ -1) {
+ switch (c) {
+ case 'd':
+ device = optarg;
+ break;
+ case 'h':
+ default:
+ break;
+ }
+ }
+
+ if (device) {
+#if defined(MOZ_ASAN) || defined(FUZZING)
+ // If handle_segv=1 (default), then glxtest crash will print a sanitizer
+ // report which can confuse the harness in fuzzing automation.
+ signal(SIGSEGV, SIG_DFL);
+#endif
+ const char* env = getenv("MOZ_GFX_DEBUG");
+ enable_logging = env && *env == '1';
+ output_pipe = OUTPUT_PIPE;
+ if (!enable_logging) {
+ close_logging();
+ }
+ v4l2_check_device(device);
+ record_flush();
+ return EXIT_SUCCESS;
+ }
+ PrintUsage();
+ return 0;
+}
diff --git a/widget/gtk/va_drmcommon.h b/widget/gtk/va_drmcommon.h
new file mode 100644
index 0000000000..e16f244a46
--- /dev/null
+++ b/widget/gtk/va_drmcommon.h
@@ -0,0 +1,156 @@
+/*
+ * va_drmcommon.h - Common utilities for DRM-based drivers
+ *
+ * Copyright (c) 2012 Intel Corporation. All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sub license, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the
+ * next paragraph) shall be included in all copies or substantial portions
+ * of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
+ * IN NO EVENT SHALL INTEL AND/OR ITS SUPPLIERS BE LIABLE FOR
+ * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef VA_DRM_COMMON_H
+#define VA_DRM_COMMON_H
+
+#include <stdint.h>
+
+/** \brief DRM authentication type. */
+enum {
+ /** \brief Disconnected. */
+ VA_DRM_AUTH_NONE = 0,
+ /**
+ * \brief Connected. Authenticated with DRI1 protocol.
+ *
+ * @deprecated
+ * This is a deprecated authentication type. All DRI-based drivers have
+ * been migrated to use the DRI2 protocol. Newly written drivers shall
+ * use DRI2 protocol only, or a custom authentication means. e.g. opt
+ * for authenticating on the VA driver side, instead of libva side.
+ */
+ VA_DRM_AUTH_DRI1 = 1,
+ /**
+ * \brief Connected. Authenticated with DRI2 protocol.
+ *
+ * This is only useful to VA/X11 drivers. The libva-x11 library provides
+ * a helper function VA_DRI2Authenticate() for authenticating the
+ * connection. However, DRI2 conformant drivers don't need to call that
+ * function since authentication happens on the libva side, implicitly.
+ */
+ VA_DRM_AUTH_DRI2 = 2,
+ /**
+ * \brief Connected. Authenticated with some alternate raw protocol.
+ *
+ * This authentication mode is mainly used in non-VA/X11 drivers.
+ * Authentication happens through some alternative method, at the
+ * discretion of the VA driver implementation.
+ */
+ VA_DRM_AUTH_CUSTOM = 3
+};
+
+/** \brief Base DRM state. */
+struct drm_state {
+ /** \brief DRM connection descriptor. */
+ int fd;
+ /** \brief DRM authentication type. */
+ int auth_type;
+ /** \brief Reserved bytes for future use, must be zero */
+ int va_reserved[8];
+};
+
+/** \brief Kernel DRM buffer memory type. */
+#define VA_SURFACE_ATTRIB_MEM_TYPE_KERNEL_DRM 0x10000000
+/** \brief DRM PRIME memory type (old version)
+ *
+ * This supports only single objects with restricted memory layout.
+ * Used with VASurfaceAttribExternalBuffers.
+ */
+#define VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME 0x20000000
+/** \brief DRM PRIME memory type
+ *
+ * Used with VADRMPRIMESurfaceDescriptor.
+ */
+#define VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 0x40000000
+
+/**
+ * \brief External buffer descriptor for a DRM PRIME surface.
+ *
+ * For export, call vaExportSurfaceHandle() with mem_type set to
+ * VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 and pass a pointer to an
+ * instance of this structure to fill.
+ * If VA_EXPORT_SURFACE_SEPARATE_LAYERS is specified on export, each
+ * layer will contain exactly one plane. For example, an NV12
+ * surface will be exported as two layers, one of DRM_FORMAT_R8 and
+ * one of DRM_FORMAT_GR88.
+ * If VA_EXPORT_SURFACE_COMPOSED_LAYERS is specified on export,
+ * there will be exactly one layer.
+ *
+ * For import, call vaCreateSurfaces() with the MemoryType attribute
+ * set to VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 and the
+ * ExternalBufferDescriptor attribute set to point to an array of
+ * num_surfaces instances of this structure.
+ * The number of planes which need to be provided for a given layer
+ * is dependent on both the format and the format modifier used for
+ * the objects containing it. For example, the format DRM_FORMAT_RGBA
+ * normally requires one plane, but with the format modifier
+ * I915_FORMAT_MOD_Y_TILED_CCS it requires two planes - the first
+ * being the main data plane and the second containing the color
+ * control surface.
+ * Note that a given driver may only support a subset of possible
+ * representations of a particular format. For example, it may only
+ * support NV12 surfaces when they are contained within a single DRM
+ * object, and therefore fail to create such surfaces if the two
+ * planes are in different DRM objects.
+ */
+typedef struct _VADRMPRIMESurfaceDescriptor {
+ /** Pixel format fourcc of the whole surface (VA_FOURCC_*). */
+ uint32_t fourcc;
+ /** Width of the surface in pixels. */
+ uint32_t width;
+ /** Height of the surface in pixels. */
+ uint32_t height;
+ /** Number of distinct DRM objects making up the surface. */
+ uint32_t num_objects;
+ /** Description of each object. */
+ struct {
+ /** DRM PRIME file descriptor for this object. */
+ int fd;
+ /** Total size of this object (may include regions which are
+ * not part of the surface). */
+ uint32_t size;
+ /** Format modifier applied to this object. */
+ uint64_t drm_format_modifier;
+ } objects[4];
+ /** Number of layers making up the surface. */
+ uint32_t num_layers;
+ /** Description of each layer in the surface. */
+ struct {
+ /** DRM format fourcc of this layer (DRM_FOURCC_*). */
+ uint32_t drm_format;
+ /** Number of planes in this layer. */
+ uint32_t num_planes;
+ /** Index in the objects array of the object containing each
+ * plane. */
+ uint32_t object_index[4];
+ /** Offset within the object of each plane. */
+ uint32_t offset[4];
+ /** Pitch of each plane. */
+ uint32_t pitch[4];
+ } layers[4];
+} VADRMPRIMESurfaceDescriptor;
+
+#endif /* VA_DRM_COMMON_H */
diff --git a/widget/gtk/vaapitest/moz.build b/widget/gtk/vaapitest/moz.build
new file mode 100644
index 0000000000..d9ea481ff1
--- /dev/null
+++ b/widget/gtk/vaapitest/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Startup and Profile System")
+
+Program("vaapitest")
+SOURCES += [
+ "vaapitest.cpp",
+]
+CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+CXXFLAGS += CONFIG["MOZ_PANGO_CFLAGS"]
+OS_LIBS += CONFIG["MOZ_X11_LIBS"]
+OS_LIBS += CONFIG["MOZ_GTK3_LIBS"]
+
+USE_LIBS += ["mozva"]
+LOCAL_INCLUDES += ["/media/mozva"]
diff --git a/widget/gtk/vaapitest/vaapitest.cpp b/widget/gtk/vaapitest/vaapitest.cpp
new file mode 100644
index 0000000000..e2c0e0a742
--- /dev/null
+++ b/widget/gtk/vaapitest/vaapitest.cpp
@@ -0,0 +1,255 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 <cstdio>
+#include <cstdlib>
+#include <dlfcn.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <stdint.h>
+#include <stdarg.h>
+
+#if defined(MOZ_ASAN) || defined(FUZZING)
+# include <signal.h>
+#endif
+
+#include "mozilla/ScopeExit.h"
+
+#ifdef __SUNPRO_CC
+# include <stdio.h>
+#endif
+
+#include "prlink.h"
+#include "va/va.h"
+
+#include "mozilla/GfxInfoUtils.h"
+
+// Print VA-API test results to stdout and logging to stderr
+#define OUTPUT_PIPE 1
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+// bits to use decoding vaapitest() return values.
+constexpr int CODEC_HW_H264 = 1 << 4;
+constexpr int CODEC_HW_VP8 = 1 << 5;
+constexpr int CODEC_HW_VP9 = 1 << 6;
+constexpr int CODEC_HW_AV1 = 1 << 7;
+
+// childgltest is declared inside extern "C" so that the name is not mangled.
+// The name is used in build/valgrind/x86_64-pc-linux-gnu.sup to suppress
+// memory leak errors because we run it inside a short lived fork and we don't
+// care about leaking memory
+extern "C" {
+
+static constexpr struct {
+ VAProfile mVAProfile;
+ const char* mName;
+} kVAAPiProfileName[] = {
+#define MAP(v) \
+ { VAProfile##v, #v }
+ MAP(H264ConstrainedBaseline),
+ MAP(H264Main),
+ MAP(H264High),
+ MAP(VP8Version0_3),
+ MAP(VP9Profile0),
+ MAP(VP9Profile2),
+ MAP(AV1Profile0),
+ MAP(AV1Profile1),
+#undef MAP
+};
+
+static const char* VAProfileName(VAProfile aVAProfile) {
+ for (const auto& profile : kVAAPiProfileName) {
+ if (profile.mVAProfile == aVAProfile) {
+ return profile.mName;
+ }
+ }
+ return nullptr;
+}
+
+static void vaapitest(const char* aRenderDevicePath) {
+ int renderDeviceFD = -1;
+ VAProfile* profiles = nullptr;
+ VAEntrypoint* entryPoints = nullptr;
+ VADisplay display = nullptr;
+ void* libDrm = nullptr;
+
+ log("vaapitest start, device %s\n", aRenderDevicePath);
+
+ auto autoRelease = mozilla::MakeScopeExit([&] {
+ free(profiles);
+ free(entryPoints);
+ if (display) {
+ vaTerminate(display);
+ }
+ if (libDrm) {
+ dlclose(libDrm);
+ }
+ if (renderDeviceFD > -1) {
+ close(renderDeviceFD);
+ }
+ });
+
+ renderDeviceFD = open(aRenderDevicePath, O_RDWR);
+ if (renderDeviceFD == -1) {
+ record_error("VA-API test failed: failed to open renderDeviceFD.");
+ return;
+ }
+
+ libDrm = dlopen("libva-drm.so.2", RTLD_LAZY);
+ if (!libDrm) {
+ log("vaapitest failed: libva-drm.so.2 is missing\n");
+ return;
+ }
+
+ static auto sVaGetDisplayDRM =
+ (void* (*)(int fd))dlsym(libDrm, "vaGetDisplayDRM");
+ if (!sVaGetDisplayDRM) {
+ record_error("VA-API test failed: sVaGetDisplayDRM is missing.");
+ return;
+ }
+
+ display = sVaGetDisplayDRM(renderDeviceFD);
+ if (!display) {
+ record_error("VA-API test failed: vaGetDisplayDRM failed.");
+ return;
+ }
+
+ int major, minor;
+ VAStatus status = vaInitialize(display, &major, &minor);
+ if (status != VA_STATUS_SUCCESS) {
+ log("vaInitialize failed %d\n", status);
+ return;
+ } else {
+ log("vaInitialize finished\n");
+ }
+
+ int maxProfiles = vaMaxNumProfiles(display);
+ int maxEntryPoints = vaMaxNumEntrypoints(display);
+ if (maxProfiles <= 0 || maxEntryPoints <= 0) {
+ record_error("VA-API test failed: wrong VAAPI profiles/entry point nums.");
+ return;
+ }
+
+ profiles = (VAProfile*)malloc(sizeof(VAProfile) * maxProfiles);
+ int numProfiles = 0;
+ status = vaQueryConfigProfiles(display, profiles, &numProfiles);
+ if (status != VA_STATUS_SUCCESS) {
+ record_error("VA-API test failed: vaQueryConfigProfiles() failed.");
+ return;
+ }
+ numProfiles = MIN(numProfiles, maxProfiles);
+
+ entryPoints = (VAEntrypoint*)malloc(sizeof(VAEntrypoint) * maxEntryPoints);
+ int codecs = 0;
+ bool foundProfile = false;
+ for (int p = 0; p < numProfiles; p++) {
+ VAProfile profile = profiles[p];
+
+ // Check only supported profiles
+ if (!VAProfileName(profile)) {
+ continue;
+ }
+
+ int numEntryPoints = 0;
+ status = vaQueryConfigEntrypoints(display, profile, entryPoints,
+ &numEntryPoints);
+ if (status != VA_STATUS_SUCCESS) {
+ continue;
+ }
+ numEntryPoints = MIN(numEntryPoints, maxEntryPoints);
+ for (int entry = 0; entry < numEntryPoints; entry++) {
+ if (entryPoints[entry] != VAEntrypointVLD) {
+ continue;
+ }
+ VAConfigID config = VA_INVALID_ID;
+ status = vaCreateConfig(display, profile, entryPoints[entry], nullptr, 0,
+ &config);
+ if (status == VA_STATUS_SUCCESS) {
+ const char* profstr = VAProfileName(profile);
+ log("Profile: %s\n", profstr);
+ // VAProfileName returns null on failure, making the below calls safe
+ if (!strncmp(profstr, "H264", 4)) {
+ codecs |= CODEC_HW_H264;
+ } else if (!strncmp(profstr, "VP8", 3)) {
+ codecs |= CODEC_HW_VP8;
+ } else if (!strncmp(profstr, "VP9", 3)) {
+ codecs |= CODEC_HW_VP9;
+ } else if (!strncmp(profstr, "AV1", 3)) {
+ codecs |= CODEC_HW_AV1;
+ } else {
+ record_warning("VA-API test unknown profile.");
+ }
+ vaDestroyConfig(display, config);
+ foundProfile = true;
+ }
+ }
+ }
+ if (foundProfile) {
+ record_value("VAAPI_SUPPORTED\nTRUE\n");
+ record_value("VAAPI_HWCODECS\n%d\n", codecs);
+ } else {
+ record_value("VAAPI_SUPPORTED\nFALSE\n");
+ }
+ log("vaapitest finished\n");
+}
+
+} // extern "C"
+
+static void PrintUsage() {
+ printf(
+ "Firefox VA-API probe utility\n"
+ "\n"
+ "usage: vaapitest [options]\n"
+ "\n"
+ "Options:\n"
+ "\n"
+ " -h --help show this message\n"
+ " -d --drm drm_device probe VA-API on drm_device (may be "
+ "/dev/dri/renderD128)\n"
+ "\n");
+}
+
+int main(int argc, char** argv) {
+ struct option longOptions[] = {{"help", no_argument, NULL, 'h'},
+ {"drm", required_argument, NULL, 'd'},
+ {NULL, 0, NULL, 0}};
+ const char* shortOptions = "hd:";
+ int c;
+ const char* drmDevice = nullptr;
+ while ((c = getopt_long(argc, argv, shortOptions, longOptions, NULL)) != -1) {
+ switch (c) {
+ case 'd':
+ drmDevice = optarg;
+ break;
+ case 'h':
+ default:
+ break;
+ }
+ }
+ if (drmDevice) {
+#if defined(MOZ_ASAN) || defined(FUZZING)
+ // If handle_segv=1 (default), then glxtest crash will print a sanitizer
+ // report which can confuse the harness in fuzzing automation.
+ signal(SIGSEGV, SIG_DFL);
+#endif
+ const char* env = getenv("MOZ_GFX_DEBUG");
+ enable_logging = env && *env == '1';
+ output_pipe = OUTPUT_PIPE;
+ if (!enable_logging) {
+ close_logging();
+ }
+ vaapitest(drmDevice);
+ record_flush();
+ return EXIT_SUCCESS;
+ }
+ PrintUsage();
+ return 0;
+}
diff --git a/widget/gtk/wayland/fractional-scale-v1-client-protocol.h b/widget/gtk/wayland/fractional-scale-v1-client-protocol.h
new file mode 100644
index 0000000000..3db2eaad6b
--- /dev/null
+++ b/widget/gtk/wayland/fractional-scale-v1-client-protocol.h
@@ -0,0 +1,268 @@
+/* Generated by wayland-scanner 1.19.0 */
+
+#ifndef FRACTIONAL_SCALE_V1_CLIENT_PROTOCOL_H
+#define FRACTIONAL_SCALE_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_fractional_scale_v1 The fractional_scale_v1 protocol
+ * Protocol for requesting fractional surface scales
+ *
+ * @section page_desc_fractional_scale_v1 Description
+ *
+ * This protocol allows a compositor to suggest for surfaces to render at
+ * fractional scales.
+ *
+ * A client can submit scaled content by utilizing wp_viewport. This is done by
+ * creating a wp_viewport object for the surface and setting the destination
+ * rectangle to the surface size before the scale factor is applied.
+ *
+ * The buffer size is calculated by multiplying the surface size by the
+ * intended scale.
+ *
+ * The wl_surface buffer scale should remain set to 1.
+ *
+ * If a surface has a surface-local size of 100 px by 50 px and wishes to
+ * submit buffers with a scale of 1.5, then a buffer of 150px by 75 px should
+ * be used and the wp_viewport destination rectangle should be 100 px by 50 px.
+ *
+ * For toplevel surfaces, the size is rounded halfway away from zero. The
+ * rounding algorithm for subsurface position and size is not defined.
+ *
+ * @section page_ifaces_fractional_scale_v1 Interfaces
+ * - @subpage page_iface_wp_fractional_scale_manager_v1 - fractional surface
+ * scale information
+ * - @subpage page_iface_wp_fractional_scale_v1 - fractional scale interface to
+ * a wl_surface
+ * @section page_copyright_fractional_scale_v1 Copyright
+ * <pre>
+ *
+ * Copyright © 2022 Kenny Levinsen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_surface;
+struct wp_fractional_scale_manager_v1;
+struct wp_fractional_scale_v1;
+
+#ifndef WP_FRACTIONAL_SCALE_MANAGER_V1_INTERFACE
+# define WP_FRACTIONAL_SCALE_MANAGER_V1_INTERFACE
+/**
+ * @page page_iface_wp_fractional_scale_manager_v1
+ * wp_fractional_scale_manager_v1
+ * @section page_iface_wp_fractional_scale_manager_v1_desc Description
+ *
+ * A global interface for requesting surfaces to use fractional scales.
+ * @section page_iface_wp_fractional_scale_manager_v1_api API
+ * See @ref iface_wp_fractional_scale_manager_v1.
+ */
+/**
+ * @defgroup iface_wp_fractional_scale_manager_v1 The
+ * wp_fractional_scale_manager_v1 interface
+ *
+ * A global interface for requesting surfaces to use fractional scales.
+ */
+extern const struct wl_interface wp_fractional_scale_manager_v1_interface;
+#endif
+#ifndef WP_FRACTIONAL_SCALE_V1_INTERFACE
+# define WP_FRACTIONAL_SCALE_V1_INTERFACE
+/**
+ * @page page_iface_wp_fractional_scale_v1 wp_fractional_scale_v1
+ * @section page_iface_wp_fractional_scale_v1_desc Description
+ *
+ * An additional interface to a wl_surface object which allows the compositor
+ * to inform the client of the preferred scale.
+ * @section page_iface_wp_fractional_scale_v1_api API
+ * See @ref iface_wp_fractional_scale_v1.
+ */
+/**
+ * @defgroup iface_wp_fractional_scale_v1 The wp_fractional_scale_v1 interface
+ *
+ * An additional interface to a wl_surface object which allows the compositor
+ * to inform the client of the preferred scale.
+ */
+extern const struct wl_interface wp_fractional_scale_v1_interface;
+#endif
+
+#ifndef WP_FRACTIONAL_SCALE_MANAGER_V1_ERROR_ENUM
+# define WP_FRACTIONAL_SCALE_MANAGER_V1_ERROR_ENUM
+enum wp_fractional_scale_manager_v1_error {
+ /**
+ * the surface already has a fractional_scale object associated
+ */
+ WP_FRACTIONAL_SCALE_MANAGER_V1_ERROR_FRACTIONAL_SCALE_EXISTS = 0,
+};
+#endif /* WP_FRACTIONAL_SCALE_MANAGER_V1_ERROR_ENUM */
+
+#define WP_FRACTIONAL_SCALE_MANAGER_V1_DESTROY 0
+#define WP_FRACTIONAL_SCALE_MANAGER_V1_GET_FRACTIONAL_SCALE 1
+
+/**
+ * @ingroup iface_wp_fractional_scale_manager_v1
+ */
+#define WP_FRACTIONAL_SCALE_MANAGER_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_wp_fractional_scale_manager_v1
+ */
+#define WP_FRACTIONAL_SCALE_MANAGER_V1_GET_FRACTIONAL_SCALE_SINCE_VERSION 1
+
+/** @ingroup iface_wp_fractional_scale_manager_v1 */
+static inline void wp_fractional_scale_manager_v1_set_user_data(
+ struct wp_fractional_scale_manager_v1* wp_fractional_scale_manager_v1,
+ void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)wp_fractional_scale_manager_v1,
+ user_data);
+}
+
+/** @ingroup iface_wp_fractional_scale_manager_v1 */
+static inline void* wp_fractional_scale_manager_v1_get_user_data(
+ struct wp_fractional_scale_manager_v1* wp_fractional_scale_manager_v1) {
+ return wl_proxy_get_user_data(
+ (struct wl_proxy*)wp_fractional_scale_manager_v1);
+}
+
+static inline uint32_t wp_fractional_scale_manager_v1_get_version(
+ struct wp_fractional_scale_manager_v1* wp_fractional_scale_manager_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)wp_fractional_scale_manager_v1);
+}
+
+/**
+ * @ingroup iface_wp_fractional_scale_manager_v1
+ *
+ * Informs the server that the client will not be using this protocol
+ * object anymore. This does not affect any other objects,
+ * wp_fractional_scale_v1 objects included.
+ */
+static inline void wp_fractional_scale_manager_v1_destroy(
+ struct wp_fractional_scale_manager_v1* wp_fractional_scale_manager_v1) {
+ wl_proxy_marshal((struct wl_proxy*)wp_fractional_scale_manager_v1,
+ WP_FRACTIONAL_SCALE_MANAGER_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)wp_fractional_scale_manager_v1);
+}
+
+/**
+ * @ingroup iface_wp_fractional_scale_manager_v1
+ *
+ * Create an add-on object for the the wl_surface to let the compositor
+ * request fractional scales. If the given wl_surface already has a
+ * wp_fractional_scale_v1 object associated, the fractional_scale_exists
+ * protocol error is raised.
+ */
+static inline struct wp_fractional_scale_v1*
+wp_fractional_scale_manager_v1_get_fractional_scale(
+ struct wp_fractional_scale_manager_v1* wp_fractional_scale_manager_v1,
+ struct wl_surface* surface) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor(
+ (struct wl_proxy*)wp_fractional_scale_manager_v1,
+ WP_FRACTIONAL_SCALE_MANAGER_V1_GET_FRACTIONAL_SCALE,
+ &wp_fractional_scale_v1_interface, NULL, surface);
+
+ return (struct wp_fractional_scale_v1*)id;
+}
+
+/**
+ * @ingroup iface_wp_fractional_scale_v1
+ * @struct wp_fractional_scale_v1_listener
+ */
+struct wp_fractional_scale_v1_listener {
+ /**
+ * notify of new preferred scale
+ *
+ * Notification of a new preferred scale for this surface that
+ * the compositor suggests that the client should use.
+ *
+ * The sent scale is the numerator of a fraction with a denominator
+ * of 120.
+ * @param scale the new preferred scale
+ */
+ void (*preferred_scale)(void* data,
+ struct wp_fractional_scale_v1* wp_fractional_scale_v1,
+ uint32_t scale);
+};
+
+/**
+ * @ingroup iface_wp_fractional_scale_v1
+ */
+static inline int wp_fractional_scale_v1_add_listener(
+ struct wp_fractional_scale_v1* wp_fractional_scale_v1,
+ const struct wp_fractional_scale_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)wp_fractional_scale_v1,
+ (void (**)(void))listener, data);
+}
+
+#define WP_FRACTIONAL_SCALE_V1_DESTROY 0
+
+/**
+ * @ingroup iface_wp_fractional_scale_v1
+ */
+#define WP_FRACTIONAL_SCALE_V1_PREFERRED_SCALE_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_wp_fractional_scale_v1
+ */
+#define WP_FRACTIONAL_SCALE_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_wp_fractional_scale_v1 */
+static inline void wp_fractional_scale_v1_set_user_data(
+ struct wp_fractional_scale_v1* wp_fractional_scale_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)wp_fractional_scale_v1, user_data);
+}
+
+/** @ingroup iface_wp_fractional_scale_v1 */
+static inline void* wp_fractional_scale_v1_get_user_data(
+ struct wp_fractional_scale_v1* wp_fractional_scale_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)wp_fractional_scale_v1);
+}
+
+static inline uint32_t wp_fractional_scale_v1_get_version(
+ struct wp_fractional_scale_v1* wp_fractional_scale_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)wp_fractional_scale_v1);
+}
+
+/**
+ * @ingroup iface_wp_fractional_scale_v1
+ *
+ * Destroy the fractional scale object. When this object is destroyed,
+ * preferred_scale events will no longer be sent.
+ */
+static inline void wp_fractional_scale_v1_destroy(
+ struct wp_fractional_scale_v1* wp_fractional_scale_v1) {
+ wl_proxy_marshal((struct wl_proxy*)wp_fractional_scale_v1,
+ WP_FRACTIONAL_SCALE_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)wp_fractional_scale_v1);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/fractional-scale-v1-protocol.c b/widget/gtk/wayland/fractional-scale-v1-protocol.c
new file mode 100644
index 0000000000..4841533289
--- /dev/null
+++ b/widget/gtk/wayland/fractional-scale-v1-protocol.c
@@ -0,0 +1,73 @@
+/* Generated by wayland-scanner 1.19.0 */
+
+/*
+ * Copyright © 2022 Kenny Levinsen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+# define WL_PRIVATE __attribute__((visibility("hidden")))
+#else
+# define WL_PRIVATE
+#endif
+
+#pragma GCC visibility push(default)
+extern const struct wl_interface wl_surface_interface;
+#pragma GCC visibility pop
+extern const struct wl_interface wp_fractional_scale_v1_interface;
+
+static const struct wl_interface* fractional_scale_v1_types[] = {
+ NULL,
+ &wp_fractional_scale_v1_interface,
+ &wl_surface_interface,
+};
+
+static const struct wl_message wp_fractional_scale_manager_v1_requests[] = {
+ {"destroy", "", fractional_scale_v1_types + 0},
+ {"get_fractional_scale", "no", fractional_scale_v1_types + 1},
+};
+
+WL_PRIVATE const struct wl_interface wp_fractional_scale_manager_v1_interface =
+ {
+ "wp_fractional_scale_manager_v1", 1, 2,
+ wp_fractional_scale_manager_v1_requests, 0, NULL,
+};
+
+static const struct wl_message wp_fractional_scale_v1_requests[] = {
+ {"destroy", "", fractional_scale_v1_types + 0},
+};
+
+static const struct wl_message wp_fractional_scale_v1_events[] = {
+ {"preferred_scale", "u", fractional_scale_v1_types + 0},
+};
+
+WL_PRIVATE const struct wl_interface wp_fractional_scale_v1_interface = {
+ "wp_fractional_scale_v1", 1, 1,
+ wp_fractional_scale_v1_requests, 1, wp_fractional_scale_v1_events,
+};
diff --git a/widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h b/widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h
new file mode 100644
index 0000000000..3ae2303b3c
--- /dev/null
+++ b/widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h
@@ -0,0 +1,228 @@
+/* Generated by wayland-scanner 1.16.0 */
+
+#ifndef IDLE_INHIBIT_UNSTABLE_V1_CLIENT_PROTOCOL_H
+#define IDLE_INHIBIT_UNSTABLE_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_idle_inhibit_unstable_v1 The idle_inhibit_unstable_v1 protocol
+ * @section page_ifaces_idle_inhibit_unstable_v1 Interfaces
+ * - @subpage page_iface_zwp_idle_inhibit_manager_v1 - control behavior when
+ * display idles
+ * - @subpage page_iface_zwp_idle_inhibitor_v1 - context object for inhibiting
+ * idle behavior
+ * @section page_copyright_idle_inhibit_unstable_v1 Copyright
+ * <pre>
+ *
+ * Copyright © 2015 Samsung Electronics Co., Ltd
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_surface;
+struct zwp_idle_inhibit_manager_v1;
+struct zwp_idle_inhibitor_v1;
+
+/**
+ * @page page_iface_zwp_idle_inhibit_manager_v1 zwp_idle_inhibit_manager_v1
+ * @section page_iface_zwp_idle_inhibit_manager_v1_desc Description
+ *
+ * This interface permits inhibiting the idle behavior such as screen
+ * blanking, locking, and screensaving. The client binds the idle manager
+ * globally, then creates idle-inhibitor objects for each surface.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible changes
+ * may be added together with the corresponding interface version bump.
+ * Backward incompatible changes are done by bumping the version number in
+ * the protocol and interface names and resetting the interface version.
+ * Once the protocol is to be declared stable, the 'z' prefix and the
+ * version number in the protocol and interface names are removed and the
+ * interface version number is reset.
+ * @section page_iface_zwp_idle_inhibit_manager_v1_api API
+ * See @ref iface_zwp_idle_inhibit_manager_v1.
+ */
+/**
+ * @defgroup iface_zwp_idle_inhibit_manager_v1 The zwp_idle_inhibit_manager_v1
+ * interface
+ *
+ * This interface permits inhibiting the idle behavior such as screen
+ * blanking, locking, and screensaving. The client binds the idle manager
+ * globally, then creates idle-inhibitor objects for each surface.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible changes
+ * may be added together with the corresponding interface version bump.
+ * Backward incompatible changes are done by bumping the version number in
+ * the protocol and interface names and resetting the interface version.
+ * Once the protocol is to be declared stable, the 'z' prefix and the
+ * version number in the protocol and interface names are removed and the
+ * interface version number is reset.
+ */
+extern const struct wl_interface zwp_idle_inhibit_manager_v1_interface;
+/**
+ * @page page_iface_zwp_idle_inhibitor_v1 zwp_idle_inhibitor_v1
+ * @section page_iface_zwp_idle_inhibitor_v1_desc Description
+ *
+ * An idle inhibitor prevents the output that the associated surface is
+ * visible on from being set to a state where it is not visually usable due
+ * to lack of user interaction (e.g. blanked, dimmed, locked, set to power
+ * save, etc.) Any screensaver processes are also blocked from displaying.
+ *
+ * If the surface is destroyed, unmapped, becomes occluded, loses
+ * visibility, or otherwise becomes not visually relevant for the user, the
+ * idle inhibitor will not be honored by the compositor; if the surface
+ * subsequently regains visibility the inhibitor takes effect once again.
+ * Likewise, the inhibitor isn't honored if the system was already idled at
+ * the time the inhibitor was established, although if the system later
+ * de-idles and re-idles the inhibitor will take effect.
+ * @section page_iface_zwp_idle_inhibitor_v1_api API
+ * See @ref iface_zwp_idle_inhibitor_v1.
+ */
+/**
+ * @defgroup iface_zwp_idle_inhibitor_v1 The zwp_idle_inhibitor_v1 interface
+ *
+ * An idle inhibitor prevents the output that the associated surface is
+ * visible on from being set to a state where it is not visually usable due
+ * to lack of user interaction (e.g. blanked, dimmed, locked, set to power
+ * save, etc.) Any screensaver processes are also blocked from displaying.
+ *
+ * If the surface is destroyed, unmapped, becomes occluded, loses
+ * visibility, or otherwise becomes not visually relevant for the user, the
+ * idle inhibitor will not be honored by the compositor; if the surface
+ * subsequently regains visibility the inhibitor takes effect once again.
+ * Likewise, the inhibitor isn't honored if the system was already idled at
+ * the time the inhibitor was established, although if the system later
+ * de-idles and re-idles the inhibitor will take effect.
+ */
+extern const struct wl_interface zwp_idle_inhibitor_v1_interface;
+
+#define ZWP_IDLE_INHIBIT_MANAGER_V1_DESTROY 0
+#define ZWP_IDLE_INHIBIT_MANAGER_V1_CREATE_INHIBITOR 1
+
+/**
+ * @ingroup iface_zwp_idle_inhibit_manager_v1
+ */
+#define ZWP_IDLE_INHIBIT_MANAGER_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_idle_inhibit_manager_v1
+ */
+#define ZWP_IDLE_INHIBIT_MANAGER_V1_CREATE_INHIBITOR_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_idle_inhibit_manager_v1 */
+static inline void zwp_idle_inhibit_manager_v1_set_user_data(
+ struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1,
+ void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_idle_inhibit_manager_v1,
+ user_data);
+}
+
+/** @ingroup iface_zwp_idle_inhibit_manager_v1 */
+static inline void* zwp_idle_inhibit_manager_v1_get_user_data(
+ struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_idle_inhibit_manager_v1);
+}
+
+static inline uint32_t zwp_idle_inhibit_manager_v1_get_version(
+ struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_idle_inhibit_manager_v1);
+}
+
+/**
+ * @ingroup iface_zwp_idle_inhibit_manager_v1
+ *
+ * Destroy the inhibit manager.
+ */
+static inline void zwp_idle_inhibit_manager_v1_destroy(
+ struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_idle_inhibit_manager_v1,
+ ZWP_IDLE_INHIBIT_MANAGER_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_idle_inhibit_manager_v1);
+}
+
+/**
+ * @ingroup iface_zwp_idle_inhibit_manager_v1
+ *
+ * Create a new inhibitor object associated with the given surface.
+ */
+static inline struct zwp_idle_inhibitor_v1*
+zwp_idle_inhibit_manager_v1_create_inhibitor(
+ struct zwp_idle_inhibit_manager_v1* zwp_idle_inhibit_manager_v1,
+ struct wl_surface* surface) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor(
+ (struct wl_proxy*)zwp_idle_inhibit_manager_v1,
+ ZWP_IDLE_INHIBIT_MANAGER_V1_CREATE_INHIBITOR,
+ &zwp_idle_inhibitor_v1_interface, NULL, surface);
+
+ return (struct zwp_idle_inhibitor_v1*)id;
+}
+
+#define ZWP_IDLE_INHIBITOR_V1_DESTROY 0
+
+/**
+ * @ingroup iface_zwp_idle_inhibitor_v1
+ */
+#define ZWP_IDLE_INHIBITOR_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_idle_inhibitor_v1 */
+static inline void zwp_idle_inhibitor_v1_set_user_data(
+ struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_idle_inhibitor_v1, user_data);
+}
+
+/** @ingroup iface_zwp_idle_inhibitor_v1 */
+static inline void* zwp_idle_inhibitor_v1_get_user_data(
+ struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_idle_inhibitor_v1);
+}
+
+static inline uint32_t zwp_idle_inhibitor_v1_get_version(
+ struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_idle_inhibitor_v1);
+}
+
+/**
+ * @ingroup iface_zwp_idle_inhibitor_v1
+ *
+ * Remove the inhibitor effect from the associated wl_surface.
+ */
+static inline void zwp_idle_inhibitor_v1_destroy(
+ struct zwp_idle_inhibitor_v1* zwp_idle_inhibitor_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_idle_inhibitor_v1,
+ ZWP_IDLE_INHIBITOR_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_idle_inhibitor_v1);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c b/widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c
new file mode 100644
index 0000000000..579095e003
--- /dev/null
+++ b/widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c
@@ -0,0 +1,60 @@
+/* Generated by wayland-scanner 1.16.0 */
+
+/*
+ * Copyright © 2015 Samsung Electronics Co., Ltd
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#pragma GCC visibility push(default)
+extern const struct wl_interface wl_surface_interface;
+extern const struct wl_interface zwp_idle_inhibitor_v1_interface;
+#pragma GCC visibility pop
+
+static const struct wl_interface* types[] = {
+ &zwp_idle_inhibitor_v1_interface,
+ &wl_surface_interface,
+};
+
+static const struct wl_message zwp_idle_inhibit_manager_v1_requests[] = {
+ {"destroy", "", types + 0},
+ {"create_inhibitor", "no", types + 0},
+};
+
+const struct wl_interface zwp_idle_inhibit_manager_v1_interface = {
+ "zwp_idle_inhibit_manager_v1", 1, 2,
+ zwp_idle_inhibit_manager_v1_requests, 0, NULL,
+};
+
+static const struct wl_message zwp_idle_inhibitor_v1_requests[] = {
+ {"destroy", "", types + 0},
+};
+
+const struct wl_interface zwp_idle_inhibitor_v1_interface = {
+ "zwp_idle_inhibitor_v1", 1, 1, zwp_idle_inhibitor_v1_requests, 0, NULL,
+};
diff --git a/widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h b/widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h
new file mode 100644
index 0000000000..cff0426a9c
--- /dev/null
+++ b/widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h
@@ -0,0 +1,650 @@
+/* Generated by wayland-scanner 1.17.0 */
+
+#ifndef LINUX_DMABUF_UNSTABLE_V1_CLIENT_PROTOCOL_H
+#define LINUX_DMABUF_UNSTABLE_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_linux_dmabuf_unstable_v1 The linux_dmabuf_unstable_v1 protocol
+ * @section page_ifaces_linux_dmabuf_unstable_v1 Interfaces
+ * - @subpage page_iface_zwp_linux_dmabuf_v1 - factory for creating dmabuf-based
+ * wl_buffers
+ * - @subpage page_iface_zwp_linux_buffer_params_v1 - parameters for creating a
+ * dmabuf-based wl_buffer
+ * @section page_copyright_linux_dmabuf_unstable_v1 Copyright
+ * <pre>
+ *
+ * Copyright © 2014, 2015 Collabora, Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_buffer;
+struct zwp_linux_buffer_params_v1;
+struct zwp_linux_dmabuf_v1;
+
+/**
+ * @page page_iface_zwp_linux_dmabuf_v1 zwp_linux_dmabuf_v1
+ * @section page_iface_zwp_linux_dmabuf_v1_desc Description
+ *
+ * Following the interfaces from:
+ * https://www.khronos.org/registry/egl/extensions/EXT/EGL_EXT_image_dma_buf_import.txt
+ * and the Linux DRM sub-system's AddFb2 ioctl.
+ *
+ * This interface offers ways to create generic dmabuf-based
+ * wl_buffers. Immediately after a client binds to this interface,
+ * the set of supported formats and format modifiers is sent with
+ * 'format' and 'modifier' events.
+ *
+ * The following are required from clients:
+ *
+ * - Clients must ensure that either all data in the dma-buf is
+ * coherent for all subsequent read access or that coherency is
+ * correctly handled by the underlying kernel-side dma-buf
+ * implementation.
+ *
+ * - Don't make any more attachments after sending the buffer to the
+ * compositor. Making more attachments later increases the risk of
+ * the compositor not being able to use (re-import) an existing
+ * dmabuf-based wl_buffer.
+ *
+ * The underlying graphics stack must ensure the following:
+ *
+ * - The dmabuf file descriptors relayed to the server will stay valid
+ * for the whole lifetime of the wl_buffer. This means the server may
+ * at any time use those fds to import the dmabuf into any kernel
+ * sub-system that might accept it.
+ *
+ * To create a wl_buffer from one or more dmabufs, a client creates a
+ * zwp_linux_dmabuf_params_v1 object with a zwp_linux_dmabuf_v1.create_params
+ * request. All planes required by the intended format are added with
+ * the 'add' request. Finally, a 'create' or 'create_immed' request is
+ * issued, which has the following outcome depending on the import success.
+ *
+ * The 'create' request,
+ * - on success, triggers a 'created' event which provides the final
+ * wl_buffer to the client.
+ * - on failure, triggers a 'failed' event to convey that the server
+ * cannot use the dmabufs received from the client.
+ *
+ * For the 'create_immed' request,
+ * - on success, the server immediately imports the added dmabufs to
+ * create a wl_buffer. No event is sent from the server in this case.
+ * - on failure, the server can choose to either:
+ * - terminate the client by raising a fatal error.
+ * - mark the wl_buffer as failed, and send a 'failed' event to the
+ * client. If the client uses a failed wl_buffer as an argument to any
+ * request, the behaviour is compositor implementation-defined.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible changes
+ * may be added together with the corresponding interface version bump.
+ * Backward incompatible changes are done by bumping the version number in
+ * the protocol and interface names and resetting the interface version.
+ * Once the protocol is to be declared stable, the 'z' prefix and the
+ * version number in the protocol and interface names are removed and the
+ * interface version number is reset.
+ * @section page_iface_zwp_linux_dmabuf_v1_api API
+ * See @ref iface_zwp_linux_dmabuf_v1.
+ */
+/**
+ * @defgroup iface_zwp_linux_dmabuf_v1 The zwp_linux_dmabuf_v1 interface
+ *
+ * Following the interfaces from:
+ * https://www.khronos.org/registry/egl/extensions/EXT/EGL_EXT_image_dma_buf_import.txt
+ * and the Linux DRM sub-system's AddFb2 ioctl.
+ *
+ * This interface offers ways to create generic dmabuf-based
+ * wl_buffers. Immediately after a client binds to this interface,
+ * the set of supported formats and format modifiers is sent with
+ * 'format' and 'modifier' events.
+ *
+ * The following are required from clients:
+ *
+ * - Clients must ensure that either all data in the dma-buf is
+ * coherent for all subsequent read access or that coherency is
+ * correctly handled by the underlying kernel-side dma-buf
+ * implementation.
+ *
+ * - Don't make any more attachments after sending the buffer to the
+ * compositor. Making more attachments later increases the risk of
+ * the compositor not being able to use (re-import) an existing
+ * dmabuf-based wl_buffer.
+ *
+ * The underlying graphics stack must ensure the following:
+ *
+ * - The dmabuf file descriptors relayed to the server will stay valid
+ * for the whole lifetime of the wl_buffer. This means the server may
+ * at any time use those fds to import the dmabuf into any kernel
+ * sub-system that might accept it.
+ *
+ * To create a wl_buffer from one or more dmabufs, a client creates a
+ * zwp_linux_dmabuf_params_v1 object with a zwp_linux_dmabuf_v1.create_params
+ * request. All planes required by the intended format are added with
+ * the 'add' request. Finally, a 'create' or 'create_immed' request is
+ * issued, which has the following outcome depending on the import success.
+ *
+ * The 'create' request,
+ * - on success, triggers a 'created' event which provides the final
+ * wl_buffer to the client.
+ * - on failure, triggers a 'failed' event to convey that the server
+ * cannot use the dmabufs received from the client.
+ *
+ * For the 'create_immed' request,
+ * - on success, the server immediately imports the added dmabufs to
+ * create a wl_buffer. No event is sent from the server in this case.
+ * - on failure, the server can choose to either:
+ * - terminate the client by raising a fatal error.
+ * - mark the wl_buffer as failed, and send a 'failed' event to the
+ * client. If the client uses a failed wl_buffer as an argument to any
+ * request, the behaviour is compositor implementation-defined.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible changes
+ * may be added together with the corresponding interface version bump.
+ * Backward incompatible changes are done by bumping the version number in
+ * the protocol and interface names and resetting the interface version.
+ * Once the protocol is to be declared stable, the 'z' prefix and the
+ * version number in the protocol and interface names are removed and the
+ * interface version number is reset.
+ */
+extern const struct wl_interface zwp_linux_dmabuf_v1_interface;
+/**
+ * @page page_iface_zwp_linux_buffer_params_v1 zwp_linux_buffer_params_v1
+ * @section page_iface_zwp_linux_buffer_params_v1_desc Description
+ *
+ * This temporary object is a collection of dmabufs and other
+ * parameters that together form a single logical buffer. The temporary
+ * object may eventually create one wl_buffer unless cancelled by
+ * destroying it before requesting 'create'.
+ *
+ * Single-planar formats only require one dmabuf, however
+ * multi-planar formats may require more than one dmabuf. For all
+ * formats, an 'add' request must be called once per plane (even if the
+ * underlying dmabuf fd is identical).
+ *
+ * You must use consecutive plane indices ('plane_idx' argument for 'add')
+ * from zero to the number of planes used by the drm_fourcc format code.
+ * All planes required by the format must be given exactly once, but can
+ * be given in any order. Each plane index can be set only once.
+ * @section page_iface_zwp_linux_buffer_params_v1_api API
+ * See @ref iface_zwp_linux_buffer_params_v1.
+ */
+/**
+ * @defgroup iface_zwp_linux_buffer_params_v1 The zwp_linux_buffer_params_v1
+ * interface
+ *
+ * This temporary object is a collection of dmabufs and other
+ * parameters that together form a single logical buffer. The temporary
+ * object may eventually create one wl_buffer unless cancelled by
+ * destroying it before requesting 'create'.
+ *
+ * Single-planar formats only require one dmabuf, however
+ * multi-planar formats may require more than one dmabuf. For all
+ * formats, an 'add' request must be called once per plane (even if the
+ * underlying dmabuf fd is identical).
+ *
+ * You must use consecutive plane indices ('plane_idx' argument for 'add')
+ * from zero to the number of planes used by the drm_fourcc format code.
+ * All planes required by the format must be given exactly once, but can
+ * be given in any order. Each plane index can be set only once.
+ */
+extern const struct wl_interface zwp_linux_buffer_params_v1_interface;
+
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ * @struct zwp_linux_dmabuf_v1_listener
+ */
+struct zwp_linux_dmabuf_v1_listener {
+ /**
+ * supported buffer format
+ *
+ * This event advertises one buffer format that the server
+ * supports. All the supported formats are advertised once when the
+ * client binds to this interface. A roundtrip after binding
+ * guarantees that the client has received all supported formats.
+ *
+ * For the definition of the format codes, see the
+ * zwp_linux_buffer_params_v1::create request.
+ *
+ * Warning: the 'format' event is likely to be deprecated and
+ * replaced with the 'modifier' event introduced in
+ * zwp_linux_dmabuf_v1 version 3, described below. Please refrain
+ * from using the information received from this event.
+ * @param format DRM_FORMAT code
+ */
+ void (*format)(void* data, struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1,
+ uint32_t format);
+ /**
+ * supported buffer format modifier
+ *
+ * This event advertises the formats that the server supports,
+ * along with the modifiers supported for each format. All the
+ * supported modifiers for all the supported formats are advertised
+ * once when the client binds to this interface. A roundtrip after
+ * binding guarantees that the client has received all supported
+ * format-modifier pairs.
+ *
+ * For the definition of the format and modifier codes, see the
+ * zwp_linux_buffer_params_v1::create request.
+ * @param format DRM_FORMAT code
+ * @param modifier_hi high 32 bits of layout modifier
+ * @param modifier_lo low 32 bits of layout modifier
+ * @since 3
+ */
+ void (*modifier)(void* data, struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1,
+ uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo);
+};
+
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ */
+static inline int zwp_linux_dmabuf_v1_add_listener(
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1,
+ const struct zwp_linux_dmabuf_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)zwp_linux_dmabuf_v1,
+ (void (**)(void))listener, data);
+}
+
+#define ZWP_LINUX_DMABUF_V1_DESTROY 0
+#define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS 1
+
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ */
+#define ZWP_LINUX_DMABUF_V1_FORMAT_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ */
+#define ZWP_LINUX_DMABUF_V1_MODIFIER_SINCE_VERSION 3
+
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ */
+#define ZWP_LINUX_DMABUF_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ */
+#define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_linux_dmabuf_v1 */
+static inline void zwp_linux_dmabuf_v1_set_user_data(
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_linux_dmabuf_v1, user_data);
+}
+
+/** @ingroup iface_zwp_linux_dmabuf_v1 */
+static inline void* zwp_linux_dmabuf_v1_get_user_data(
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_linux_dmabuf_v1);
+}
+
+static inline uint32_t zwp_linux_dmabuf_v1_get_version(
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_linux_dmabuf_v1);
+}
+
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ *
+ * Objects created through this interface, especially wl_buffers, will
+ * remain valid.
+ */
+static inline void zwp_linux_dmabuf_v1_destroy(
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_linux_dmabuf_v1,
+ ZWP_LINUX_DMABUF_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_linux_dmabuf_v1);
+}
+
+/**
+ * @ingroup iface_zwp_linux_dmabuf_v1
+ *
+ * This temporary object is used to collect multiple dmabuf handles into
+ * a single batch to create a wl_buffer. It can only be used once and
+ * should be destroyed after a 'created' or 'failed' event has been
+ * received.
+ */
+static inline struct zwp_linux_buffer_params_v1*
+zwp_linux_dmabuf_v1_create_params(
+ struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf_v1) {
+ struct wl_proxy* params_id;
+
+ params_id = wl_proxy_marshal_constructor(
+ (struct wl_proxy*)zwp_linux_dmabuf_v1, ZWP_LINUX_DMABUF_V1_CREATE_PARAMS,
+ &zwp_linux_buffer_params_v1_interface, NULL);
+
+ return (struct zwp_linux_buffer_params_v1*)params_id;
+}
+
+#ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM
+# define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM
+enum zwp_linux_buffer_params_v1_error {
+ /**
+ * the dmabuf_batch object has already been used to create a wl_buffer
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ALREADY_USED = 0,
+ /**
+ * plane index out of bounds
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_IDX = 1,
+ /**
+ * the plane index was already set
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_SET = 2,
+ /**
+ * missing or too many planes to create a buffer
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INCOMPLETE = 3,
+ /**
+ * format not supported
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_FORMAT = 4,
+ /**
+ * invalid width or height
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DIMENSIONS = 5,
+ /**
+ * offset + stride * height goes out of dmabuf bounds
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_OUT_OF_BOUNDS = 6,
+ /**
+ * invalid wl_buffer resulted from importing dmabufs via the
+ * create_immed request on given buffer_params
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_WL_BUFFER = 7,
+};
+#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM */
+
+#ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM
+# define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM
+enum zwp_linux_buffer_params_v1_flags {
+ /**
+ * contents are y-inverted
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT = 1,
+ /**
+ * content is interlaced
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_INTERLACED = 2,
+ /**
+ * bottom field first
+ */
+ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST = 4,
+};
+#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM */
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ * @struct zwp_linux_buffer_params_v1_listener
+ */
+struct zwp_linux_buffer_params_v1_listener {
+ /**
+ * buffer creation succeeded
+ *
+ * This event indicates that the attempted buffer creation was
+ * successful. It provides the new wl_buffer referencing the
+ * dmabuf(s).
+ *
+ * Upon receiving this event, the client should destroy the
+ * zlinux_dmabuf_params object.
+ * @param buffer the newly created wl_buffer
+ */
+ void (*created)(void* data,
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1,
+ struct wl_buffer* buffer);
+ /**
+ * buffer creation failed
+ *
+ * This event indicates that the attempted buffer creation has
+ * failed. It usually means that one of the dmabuf constraints has
+ * not been fulfilled.
+ *
+ * Upon receiving this event, the client should destroy the
+ * zlinux_buffer_params object.
+ */
+ void (*failed)(void* data,
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1);
+};
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ */
+static inline int zwp_linux_buffer_params_v1_add_listener(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1,
+ const struct zwp_linux_buffer_params_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)zwp_linux_buffer_params_v1,
+ (void (**)(void))listener, data);
+}
+
+#define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY 0
+#define ZWP_LINUX_BUFFER_PARAMS_V1_ADD 1
+#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE 2
+#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED 3
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ */
+#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ */
+#define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ */
+#define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ */
+#define ZWP_LINUX_BUFFER_PARAMS_V1_ADD_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ */
+#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ */
+#define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED_SINCE_VERSION 2
+
+/** @ingroup iface_zwp_linux_buffer_params_v1 */
+static inline void zwp_linux_buffer_params_v1_set_user_data(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1,
+ void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_linux_buffer_params_v1,
+ user_data);
+}
+
+/** @ingroup iface_zwp_linux_buffer_params_v1 */
+static inline void* zwp_linux_buffer_params_v1_get_user_data(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_linux_buffer_params_v1);
+}
+
+static inline uint32_t zwp_linux_buffer_params_v1_get_version(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_linux_buffer_params_v1);
+}
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ *
+ * Cleans up the temporary data sent to the server for dmabuf-based
+ * wl_buffer creation.
+ */
+static inline void zwp_linux_buffer_params_v1_destroy(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_linux_buffer_params_v1,
+ ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_linux_buffer_params_v1);
+}
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ *
+ * This request adds one dmabuf to the set in this
+ * zwp_linux_buffer_params_v1.
+ *
+ * The 64-bit unsigned value combined from modifier_hi and modifier_lo
+ * is the dmabuf layout modifier. DRM AddFB2 ioctl calls this the
+ * fb modifier, which is defined in drm_mode.h of Linux UAPI.
+ * This is an opaque token. Drivers use this token to express tiling,
+ * compression, etc. driver-specific modifications to the base format
+ * defined by the DRM fourcc code.
+ *
+ * This request raises the PLANE_IDX error if plane_idx is too large.
+ * The error PLANE_SET is raised if attempting to set a plane that
+ * was already set.
+ */
+static inline void zwp_linux_buffer_params_v1_add(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1, int32_t fd,
+ uint32_t plane_idx, uint32_t offset, uint32_t stride, uint32_t modifier_hi,
+ uint32_t modifier_lo) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_linux_buffer_params_v1,
+ ZWP_LINUX_BUFFER_PARAMS_V1_ADD, fd, plane_idx, offset,
+ stride, modifier_hi, modifier_lo);
+}
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ *
+ * This asks for creation of a wl_buffer from the added dmabuf
+ * buffers. The wl_buffer is not created immediately but returned via
+ * the 'created' event if the dmabuf sharing succeeds. The sharing
+ * may fail at runtime for reasons a client cannot predict, in
+ * which case the 'failed' event is triggered.
+ *
+ * The 'format' argument is a DRM_FORMAT code, as defined by the
+ * libdrm's drm_fourcc.h. The Linux kernel's DRM sub-system is the
+ * authoritative source on how the format codes should work.
+ *
+ * The 'flags' is a bitfield of the flags defined in enum "flags".
+ * 'y_invert' means the that the image needs to be y-flipped.
+ *
+ * Flag 'interlaced' means that the frame in the buffer is not
+ * progressive as usual, but interlaced. An interlaced buffer as
+ * supported here must always contain both top and bottom fields.
+ * The top field always begins on the first pixel row. The temporal
+ * ordering between the two fields is top field first, unless
+ * 'bottom_first' is specified. It is undefined whether 'bottom_first'
+ * is ignored if 'interlaced' is not set.
+ *
+ * This protocol does not convey any information about field rate,
+ * duration, or timing, other than the relative ordering between the
+ * two fields in one buffer. A compositor may have to estimate the
+ * intended field rate from the incoming buffer rate. It is undefined
+ * whether the time of receiving wl_surface.commit with a new buffer
+ * attached, applying the wl_surface state, wl_surface.frame callback
+ * trigger, presentation, or any other point in the compositor cycle
+ * is used to measure the frame or field times. There is no support
+ * for detecting missed or late frames/fields/buffers either, and
+ * there is no support whatsoever for cooperating with interlaced
+ * compositor output.
+ *
+ * The composited image quality resulting from the use of interlaced
+ * buffers is explicitly undefined. A compositor may use elaborate
+ * hardware features or software to deinterlace and create progressive
+ * output frames from a sequence of interlaced input buffers, or it
+ * may produce substandard image quality. However, compositors that
+ * cannot guarantee reasonable image quality in all cases are recommended
+ * to just reject all interlaced buffers.
+ *
+ * Any argument errors, including non-positive width or height,
+ * mismatch between the number of planes and the format, bad
+ * format, bad offset or stride, may be indicated by fatal protocol
+ * errors: INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS,
+ * OUT_OF_BOUNDS.
+ *
+ * Dmabuf import errors in the server that are not obvious client
+ * bugs are returned via the 'failed' event as non-fatal. This
+ * allows attempting dmabuf sharing and falling back in the client
+ * if it fails.
+ *
+ * This request can be sent only once in the object's lifetime, after
+ * which the only legal request is destroy. This object should be
+ * destroyed after issuing a 'create' request. Attempting to use this
+ * object after issuing 'create' raises ALREADY_USED protocol error.
+ *
+ * It is not mandatory to issue 'create'. If a client wants to
+ * cancel the buffer creation, it can just destroy this object.
+ */
+static inline void zwp_linux_buffer_params_v1_create(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1,
+ int32_t width, int32_t height, uint32_t format, uint32_t flags) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_linux_buffer_params_v1,
+ ZWP_LINUX_BUFFER_PARAMS_V1_CREATE, width, height, format,
+ flags);
+}
+
+/**
+ * @ingroup iface_zwp_linux_buffer_params_v1
+ *
+ * This asks for immediate creation of a wl_buffer by importing the
+ * added dmabufs.
+ *
+ * In case of import success, no event is sent from the server, and the
+ * wl_buffer is ready to be used by the client.
+ *
+ * Upon import failure, either of the following may happen, as seen fit
+ * by the implementation:
+ * - the client is terminated with one of the following fatal protocol
+ * errors:
+ * - INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, OUT_OF_BOUNDS,
+ * in case of argument errors such as mismatch between the number
+ * of planes and the format, bad format, non-positive width or
+ * height, or bad offset or stride.
+ * - INVALID_WL_BUFFER, in case the cause for failure is unknown or
+ * plaform specific.
+ * - the server creates an invalid wl_buffer, marks it as failed and
+ * sends a 'failed' event to the client. The result of using this
+ * invalid wl_buffer as an argument in any request by the client is
+ * defined by the compositor implementation.
+ *
+ * This takes the same arguments as a 'create' request, and obeys the
+ * same restrictions.
+ */
+static inline struct wl_buffer* zwp_linux_buffer_params_v1_create_immed(
+ struct zwp_linux_buffer_params_v1* zwp_linux_buffer_params_v1,
+ int32_t width, int32_t height, uint32_t format, uint32_t flags) {
+ struct wl_proxy* buffer_id;
+
+ buffer_id = wl_proxy_marshal_constructor(
+ (struct wl_proxy*)zwp_linux_buffer_params_v1,
+ ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED, &wl_buffer_interface, NULL,
+ width, height, format, flags);
+
+ return (struct wl_buffer*)buffer_id;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c b/widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c
new file mode 100644
index 0000000000..51c1e8e575
--- /dev/null
+++ b/widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c
@@ -0,0 +1,81 @@
+/* Generated by wayland-scanner 1.17.0 */
+
+/*
+ * Copyright © 2014, 2015 Collabora, Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#pragma GCC visibility push(default)
+extern const struct wl_interface wl_buffer_interface;
+extern const struct wl_interface zwp_linux_buffer_params_v1_interface;
+#pragma GCC visibility pop
+
+static const struct wl_interface* types[] = {
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ &zwp_linux_buffer_params_v1_interface,
+ &wl_buffer_interface,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ &wl_buffer_interface,
+};
+
+static const struct wl_message zwp_linux_dmabuf_v1_requests[] = {
+ {"destroy", "", types + 0},
+ {"create_params", "n", types + 6},
+};
+
+static const struct wl_message zwp_linux_dmabuf_v1_events[] = {
+ {"format", "u", types + 0},
+ {"modifier", "3uuu", types + 0},
+};
+
+const struct wl_interface zwp_linux_dmabuf_v1_interface = {
+ "zwp_linux_dmabuf_v1", 3, 2,
+ zwp_linux_dmabuf_v1_requests, 2, zwp_linux_dmabuf_v1_events,
+};
+
+static const struct wl_message zwp_linux_buffer_params_v1_requests[] = {
+ {"destroy", "", types + 0},
+ {"add", "huuuuu", types + 0},
+ {"create", "iiuu", types + 0},
+ {"create_immed", "2niiuu", types + 7},
+};
+
+static const struct wl_message zwp_linux_buffer_params_v1_events[] = {
+ {"created", "n", types + 12},
+ {"failed", "", types + 0},
+};
+
+const struct wl_interface zwp_linux_buffer_params_v1_interface = {
+ "zwp_linux_buffer_params_v1", 3, 4,
+ zwp_linux_buffer_params_v1_requests, 2, zwp_linux_buffer_params_v1_events,
+};
diff --git a/widget/gtk/wayland/moz.build b/widget/gtk/wayland/moz.build
new file mode 100644
index 0000000000..e033187a3b
--- /dev/null
+++ b/widget/gtk/wayland/moz.build
@@ -0,0 +1,37 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Gtk")
+
+SOURCES += [
+ "fractional-scale-v1-protocol.c",
+ "idle-inhibit-unstable-v1-protocol.c",
+ "linux-dmabuf-unstable-v1-protocol.c",
+ "pointer-constraints-unstable-v1-protocol.c",
+ "relative-pointer-unstable-v1-protocol.c",
+ "viewporter-protocol.c",
+ "xdg-activation-v1-protocol.c",
+ "xdg-output-unstable-v1-protocol.c",
+]
+
+EXPORTS.mozilla.widget += [
+ "fractional-scale-v1-client-protocol.h",
+ "idle-inhibit-unstable-v1-client-protocol.h",
+ "linux-dmabuf-unstable-v1-client-protocol.h",
+ "pointer-constraints-unstable-v1-client-protocol.h",
+ "relative-pointer-unstable-v1-client-protocol.h",
+ "viewporter-client-protocol.h",
+ "xdg-activation-v1-client-protocol.h",
+ "xdg-output-unstable-v1-client-protocol.h",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
diff --git a/widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h b/widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h
new file mode 100644
index 0000000000..0f38dee226
--- /dev/null
+++ b/widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h
@@ -0,0 +1,650 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+#ifndef POINTER_CONSTRAINTS_UNSTABLE_V1_CLIENT_PROTOCOL_H
+#define POINTER_CONSTRAINTS_UNSTABLE_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_pointer_constraints_unstable_v1 The
+ * pointer_constraints_unstable_v1 protocol protocol for constraining pointer
+ * motions
+ *
+ * @section page_desc_pointer_constraints_unstable_v1 Description
+ *
+ * This protocol specifies a set of interfaces used for adding constraints to
+ * the motion of a pointer. Possible constraints include confining pointer
+ * motions to a given region, or locking it to its current position.
+ *
+ * In order to constrain the pointer, a client must first bind the global
+ * interface "wp_pointer_constraints" which, if a compositor supports pointer
+ * constraints, is exposed by the registry. Using the bound global object, the
+ * client uses the request that corresponds to the type of constraint it wants
+ * to make. See wp_pointer_constraints for more details.
+ *
+ * Warning! The protocol described in this file is experimental and backward
+ * incompatible changes may be made. Backward compatible changes may be added
+ * together with the corresponding interface version bump. Backward
+ * incompatible changes are done by bumping the version number in the protocol
+ * and interface names and resetting the interface version. Once the protocol
+ * is to be declared stable, the 'z' prefix and the version number in the
+ * protocol and interface names are removed and the interface version number is
+ * reset.
+ *
+ * @section page_ifaces_pointer_constraints_unstable_v1 Interfaces
+ * - @subpage page_iface_zwp_pointer_constraints_v1 - constrain the movement of
+ * a pointer
+ * - @subpage page_iface_zwp_locked_pointer_v1 - receive relative pointer motion
+ * events
+ * - @subpage page_iface_zwp_confined_pointer_v1 - confined pointer object
+ * @section page_copyright_pointer_constraints_unstable_v1 Copyright
+ * <pre>
+ *
+ * Copyright © 2014 Jonas Ådahl
+ * Copyright © 2015 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_pointer;
+struct wl_region;
+struct wl_surface;
+struct zwp_confined_pointer_v1;
+struct zwp_locked_pointer_v1;
+struct zwp_pointer_constraints_v1;
+
+/**
+ * @page page_iface_zwp_pointer_constraints_v1 zwp_pointer_constraints_v1
+ * @section page_iface_zwp_pointer_constraints_v1_desc Description
+ *
+ * The global interface exposing pointer constraining functionality. It
+ * exposes two requests: lock_pointer for locking the pointer to its
+ * position, and confine_pointer for locking the pointer to a region.
+ *
+ * The lock_pointer and confine_pointer requests create the objects
+ * wp_locked_pointer and wp_confined_pointer respectively, and the client can
+ * use these objects to interact with the lock.
+ *
+ * For any surface, only one lock or confinement may be active across all
+ * wl_pointer objects of the same seat. If a lock or confinement is requested
+ * when another lock or confinement is active or requested on the same surface
+ * and with any of the wl_pointer objects of the same seat, an
+ * 'already_constrained' error will be raised.
+ * @section page_iface_zwp_pointer_constraints_v1_api API
+ * See @ref iface_zwp_pointer_constraints_v1.
+ */
+/**
+ * @defgroup iface_zwp_pointer_constraints_v1 The zwp_pointer_constraints_v1
+ * interface
+ *
+ * The global interface exposing pointer constraining functionality. It
+ * exposes two requests: lock_pointer for locking the pointer to its
+ * position, and confine_pointer for locking the pointer to a region.
+ *
+ * The lock_pointer and confine_pointer requests create the objects
+ * wp_locked_pointer and wp_confined_pointer respectively, and the client can
+ * use these objects to interact with the lock.
+ *
+ * For any surface, only one lock or confinement may be active across all
+ * wl_pointer objects of the same seat. If a lock or confinement is requested
+ * when another lock or confinement is active or requested on the same surface
+ * and with any of the wl_pointer objects of the same seat, an
+ * 'already_constrained' error will be raised.
+ */
+extern const struct wl_interface zwp_pointer_constraints_v1_interface;
+/**
+ * @page page_iface_zwp_locked_pointer_v1 zwp_locked_pointer_v1
+ * @section page_iface_zwp_locked_pointer_v1_desc Description
+ *
+ * The wp_locked_pointer interface represents a locked pointer state.
+ *
+ * While the lock of this object is active, the wl_pointer objects of the
+ * associated seat will not emit any wl_pointer.motion events.
+ *
+ * This object will send the event 'locked' when the lock is activated.
+ * Whenever the lock is activated, it is guaranteed that the locked surface
+ * will already have received pointer focus and that the pointer will be
+ * within the region passed to the request creating this object.
+ *
+ * To unlock the pointer, send the destroy request. This will also destroy
+ * the wp_locked_pointer object.
+ *
+ * If the compositor decides to unlock the pointer the unlocked event is
+ * sent. See wp_locked_pointer.unlock for details.
+ *
+ * When unlocking, the compositor may warp the cursor position to the set
+ * cursor position hint. If it does, it will not result in any relative
+ * motion events emitted via wp_relative_pointer.
+ *
+ * If the surface the lock was requested on is destroyed and the lock is not
+ * yet activated, the wp_locked_pointer object is now defunct and must be
+ * destroyed.
+ * @section page_iface_zwp_locked_pointer_v1_api API
+ * See @ref iface_zwp_locked_pointer_v1.
+ */
+/**
+ * @defgroup iface_zwp_locked_pointer_v1 The zwp_locked_pointer_v1 interface
+ *
+ * The wp_locked_pointer interface represents a locked pointer state.
+ *
+ * While the lock of this object is active, the wl_pointer objects of the
+ * associated seat will not emit any wl_pointer.motion events.
+ *
+ * This object will send the event 'locked' when the lock is activated.
+ * Whenever the lock is activated, it is guaranteed that the locked surface
+ * will already have received pointer focus and that the pointer will be
+ * within the region passed to the request creating this object.
+ *
+ * To unlock the pointer, send the destroy request. This will also destroy
+ * the wp_locked_pointer object.
+ *
+ * If the compositor decides to unlock the pointer the unlocked event is
+ * sent. See wp_locked_pointer.unlock for details.
+ *
+ * When unlocking, the compositor may warp the cursor position to the set
+ * cursor position hint. If it does, it will not result in any relative
+ * motion events emitted via wp_relative_pointer.
+ *
+ * If the surface the lock was requested on is destroyed and the lock is not
+ * yet activated, the wp_locked_pointer object is now defunct and must be
+ * destroyed.
+ */
+extern const struct wl_interface zwp_locked_pointer_v1_interface;
+/**
+ * @page page_iface_zwp_confined_pointer_v1 zwp_confined_pointer_v1
+ * @section page_iface_zwp_confined_pointer_v1_desc Description
+ *
+ * The wp_confined_pointer interface represents a confined pointer state.
+ *
+ * This object will send the event 'confined' when the confinement is
+ * activated. Whenever the confinement is activated, it is guaranteed that
+ * the surface the pointer is confined to will already have received pointer
+ * focus and that the pointer will be within the region passed to the request
+ * creating this object. It is up to the compositor to decide whether this
+ * requires some user interaction and if the pointer will warp to within the
+ * passed region if outside.
+ *
+ * To unconfine the pointer, send the destroy request. This will also destroy
+ * the wp_confined_pointer object.
+ *
+ * If the compositor decides to unconfine the pointer the unconfined event is
+ * sent. The wp_confined_pointer object is at this point defunct and should
+ * be destroyed.
+ * @section page_iface_zwp_confined_pointer_v1_api API
+ * See @ref iface_zwp_confined_pointer_v1.
+ */
+/**
+ * @defgroup iface_zwp_confined_pointer_v1 The zwp_confined_pointer_v1 interface
+ *
+ * The wp_confined_pointer interface represents a confined pointer state.
+ *
+ * This object will send the event 'confined' when the confinement is
+ * activated. Whenever the confinement is activated, it is guaranteed that
+ * the surface the pointer is confined to will already have received pointer
+ * focus and that the pointer will be within the region passed to the request
+ * creating this object. It is up to the compositor to decide whether this
+ * requires some user interaction and if the pointer will warp to within the
+ * passed region if outside.
+ *
+ * To unconfine the pointer, send the destroy request. This will also destroy
+ * the wp_confined_pointer object.
+ *
+ * If the compositor decides to unconfine the pointer the unconfined event is
+ * sent. The wp_confined_pointer object is at this point defunct and should
+ * be destroyed.
+ */
+extern const struct wl_interface zwp_confined_pointer_v1_interface;
+
+#ifndef ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM
+# define ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ * wp_pointer_constraints error values
+ *
+ * These errors can be emitted in response to wp_pointer_constraints
+ * requests.
+ */
+enum zwp_pointer_constraints_v1_error {
+ /**
+ * pointer constraint already requested on that surface
+ */
+ ZWP_POINTER_CONSTRAINTS_V1_ERROR_ALREADY_CONSTRAINED = 1,
+};
+#endif /* ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM */
+
+#ifndef ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM
+# define ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ * the pointer constraint may reactivate
+ *
+ * A persistent pointer constraint may again reactivate once it has
+ * been deactivated. See the corresponding deactivation event
+ * (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) for
+ * details.
+ */
+enum zwp_pointer_constraints_v1_lifetime {
+ ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT = 1,
+ ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT = 2,
+};
+#endif /* ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM */
+
+#define ZWP_POINTER_CONSTRAINTS_V1_DESTROY 0
+#define ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER 1
+#define ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER 2
+
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ */
+#define ZWP_POINTER_CONSTRAINTS_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ */
+#define ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ */
+#define ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_pointer_constraints_v1 */
+static inline void zwp_pointer_constraints_v1_set_user_data(
+ struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1,
+ void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_pointer_constraints_v1,
+ user_data);
+}
+
+/** @ingroup iface_zwp_pointer_constraints_v1 */
+static inline void* zwp_pointer_constraints_v1_get_user_data(
+ struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_pointer_constraints_v1);
+}
+
+static inline uint32_t zwp_pointer_constraints_v1_get_version(
+ struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_pointer_constraints_v1);
+}
+
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ *
+ * Used by the client to notify the server that it will no longer use this
+ * pointer constraints object.
+ */
+static inline void zwp_pointer_constraints_v1_destroy(
+ struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_pointer_constraints_v1,
+ ZWP_POINTER_CONSTRAINTS_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_pointer_constraints_v1);
+}
+
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ *
+ * The lock_pointer request lets the client request to disable movements of
+ * the virtual pointer (i.e. the cursor), effectively locking the pointer
+ * to a position. This request may not take effect immediately; in the
+ * future, when the compositor deems implementation-specific constraints
+ * are satisfied, the pointer lock will be activated and the compositor
+ * sends a locked event.
+ *
+ * The protocol provides no guarantee that the constraints are ever
+ * satisfied, and does not require the compositor to send an error if the
+ * constraints cannot ever be satisfied. It is thus possible to request a
+ * lock that will never activate.
+ *
+ * There may not be another pointer constraint of any kind requested or
+ * active on the surface for any of the wl_pointer objects of the seat of
+ * the passed pointer when requesting a lock. If there is, an error will be
+ * raised. See general pointer lock documentation for more details.
+ *
+ * The intersection of the region passed with this request and the input
+ * region of the surface is used to determine where the pointer must be
+ * in order for the lock to activate. It is up to the compositor whether to
+ * warp the pointer or require some kind of user interaction for the lock
+ * to activate. If the region is null the surface input region is used.
+ *
+ * A surface may receive pointer focus without the lock being activated.
+ *
+ * The request creates a new object wp_locked_pointer which is used to
+ * interact with the lock as well as receive updates about its state. See
+ * the the description of wp_locked_pointer for further information.
+ *
+ * Note that while a pointer is locked, the wl_pointer objects of the
+ * corresponding seat will not emit any wl_pointer.motion events, but
+ * relative motion events will still be emitted via wp_relative_pointer
+ * objects of the same seat. wl_pointer.axis and wl_pointer.button events
+ * are unaffected.
+ */
+static inline struct zwp_locked_pointer_v1*
+zwp_pointer_constraints_v1_lock_pointer(
+ struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1,
+ struct wl_surface* surface, struct wl_pointer* pointer,
+ struct wl_region* region, uint32_t lifetime) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor(
+ (struct wl_proxy*)zwp_pointer_constraints_v1,
+ ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER, &zwp_locked_pointer_v1_interface,
+ NULL, surface, pointer, region, lifetime);
+
+ return (struct zwp_locked_pointer_v1*)id;
+}
+
+/**
+ * @ingroup iface_zwp_pointer_constraints_v1
+ *
+ * The confine_pointer request lets the client request to confine the
+ * pointer cursor to a given region. This request may not take effect
+ * immediately; in the future, when the compositor deems implementation-
+ * specific constraints are satisfied, the pointer confinement will be
+ * activated and the compositor sends a confined event.
+ *
+ * The intersection of the region passed with this request and the input
+ * region of the surface is used to determine where the pointer must be
+ * in order for the confinement to activate. It is up to the compositor
+ * whether to warp the pointer or require some kind of user interaction for
+ * the confinement to activate. If the region is null the surface input
+ * region is used.
+ *
+ * The request will create a new object wp_confined_pointer which is used
+ * to interact with the confinement as well as receive updates about its
+ * state. See the the description of wp_confined_pointer for further
+ * information.
+ */
+static inline struct zwp_confined_pointer_v1*
+zwp_pointer_constraints_v1_confine_pointer(
+ struct zwp_pointer_constraints_v1* zwp_pointer_constraints_v1,
+ struct wl_surface* surface, struct wl_pointer* pointer,
+ struct wl_region* region, uint32_t lifetime) {
+ struct wl_proxy* id;
+
+ id =
+ wl_proxy_marshal_constructor((struct wl_proxy*)zwp_pointer_constraints_v1,
+ ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER,
+ &zwp_confined_pointer_v1_interface, NULL,
+ surface, pointer, region, lifetime);
+
+ return (struct zwp_confined_pointer_v1*)id;
+}
+
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ * @struct zwp_locked_pointer_v1_listener
+ */
+struct zwp_locked_pointer_v1_listener {
+ /**
+ * lock activation event
+ *
+ * Notification that the pointer lock of the seat's pointer is
+ * activated.
+ */
+ void (*locked)(void* data,
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1);
+ /**
+ * lock deactivation event
+ *
+ * Notification that the pointer lock of the seat's pointer is no
+ * longer active. If this is a oneshot pointer lock (see
+ * wp_pointer_constraints.lifetime) this object is now defunct and
+ * should be destroyed. If this is a persistent pointer lock (see
+ * wp_pointer_constraints.lifetime) this pointer lock may again
+ * reactivate in the future.
+ */
+ void (*unlocked)(void* data,
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1);
+};
+
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ */
+static inline int zwp_locked_pointer_v1_add_listener(
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1,
+ const struct zwp_locked_pointer_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)zwp_locked_pointer_v1,
+ (void (**)(void))listener, data);
+}
+
+#define ZWP_LOCKED_POINTER_V1_DESTROY 0
+#define ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT 1
+#define ZWP_LOCKED_POINTER_V1_SET_REGION 2
+
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ */
+#define ZWP_LOCKED_POINTER_V1_LOCKED_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ */
+#define ZWP_LOCKED_POINTER_V1_UNLOCKED_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ */
+#define ZWP_LOCKED_POINTER_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ */
+#define ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ */
+#define ZWP_LOCKED_POINTER_V1_SET_REGION_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_locked_pointer_v1 */
+static inline void zwp_locked_pointer_v1_set_user_data(
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_locked_pointer_v1, user_data);
+}
+
+/** @ingroup iface_zwp_locked_pointer_v1 */
+static inline void* zwp_locked_pointer_v1_get_user_data(
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_locked_pointer_v1);
+}
+
+static inline uint32_t zwp_locked_pointer_v1_get_version(
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_locked_pointer_v1);
+}
+
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ *
+ * Destroy the locked pointer object. If applicable, the compositor will
+ * unlock the pointer.
+ */
+static inline void zwp_locked_pointer_v1_destroy(
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_locked_pointer_v1,
+ ZWP_LOCKED_POINTER_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_locked_pointer_v1);
+}
+
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ *
+ * Set the cursor position hint relative to the top left corner of the
+ * surface.
+ *
+ * If the client is drawing its own cursor, it should update the position
+ * hint to the position of its own cursor. A compositor may use this
+ * information to warp the pointer upon unlock in order to avoid pointer
+ * jumps.
+ *
+ * The cursor position hint is double buffered. The new hint will only take
+ * effect when the associated surface gets it pending state applied. See
+ * wl_surface.commit for details.
+ */
+static inline void zwp_locked_pointer_v1_set_cursor_position_hint(
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1, wl_fixed_t surface_x,
+ wl_fixed_t surface_y) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_locked_pointer_v1,
+ ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT, surface_x,
+ surface_y);
+}
+
+/**
+ * @ingroup iface_zwp_locked_pointer_v1
+ *
+ * Set a new region used to lock the pointer.
+ *
+ * The new lock region is double-buffered. The new lock region will
+ * only take effect when the associated surface gets its pending state
+ * applied. See wl_surface.commit for details.
+ *
+ * For details about the lock region, see wp_locked_pointer.
+ */
+static inline void zwp_locked_pointer_v1_set_region(
+ struct zwp_locked_pointer_v1* zwp_locked_pointer_v1,
+ struct wl_region* region) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_locked_pointer_v1,
+ ZWP_LOCKED_POINTER_V1_SET_REGION, region);
+}
+
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ * @struct zwp_confined_pointer_v1_listener
+ */
+struct zwp_confined_pointer_v1_listener {
+ /**
+ * pointer confined
+ *
+ * Notification that the pointer confinement of the seat's
+ * pointer is activated.
+ */
+ void (*confined)(void* data,
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1);
+ /**
+ * pointer unconfined
+ *
+ * Notification that the pointer confinement of the seat's
+ * pointer is no longer active. If this is a oneshot pointer
+ * confinement (see wp_pointer_constraints.lifetime) this object is
+ * now defunct and should be destroyed. If this is a persistent
+ * pointer confinement (see wp_pointer_constraints.lifetime) this
+ * pointer confinement may again reactivate in the future.
+ */
+ void (*unconfined)(void* data,
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1);
+};
+
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ */
+static inline int zwp_confined_pointer_v1_add_listener(
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1,
+ const struct zwp_confined_pointer_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)zwp_confined_pointer_v1,
+ (void (**)(void))listener, data);
+}
+
+#define ZWP_CONFINED_POINTER_V1_DESTROY 0
+#define ZWP_CONFINED_POINTER_V1_SET_REGION 1
+
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ */
+#define ZWP_CONFINED_POINTER_V1_CONFINED_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ */
+#define ZWP_CONFINED_POINTER_V1_UNCONFINED_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ */
+#define ZWP_CONFINED_POINTER_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ */
+#define ZWP_CONFINED_POINTER_V1_SET_REGION_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_confined_pointer_v1 */
+static inline void zwp_confined_pointer_v1_set_user_data(
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_confined_pointer_v1, user_data);
+}
+
+/** @ingroup iface_zwp_confined_pointer_v1 */
+static inline void* zwp_confined_pointer_v1_get_user_data(
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_confined_pointer_v1);
+}
+
+static inline uint32_t zwp_confined_pointer_v1_get_version(
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_confined_pointer_v1);
+}
+
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ *
+ * Destroy the confined pointer object. If applicable, the compositor will
+ * unconfine the pointer.
+ */
+static inline void zwp_confined_pointer_v1_destroy(
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_confined_pointer_v1,
+ ZWP_CONFINED_POINTER_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_confined_pointer_v1);
+}
+
+/**
+ * @ingroup iface_zwp_confined_pointer_v1
+ *
+ * Set a new region used to confine the pointer.
+ *
+ * The new confine region is double-buffered. The new confine region will
+ * only take effect when the associated surface gets its pending state
+ * applied. See wl_surface.commit for details.
+ *
+ * If the confinement is active when the new confinement region is applied
+ * and the pointer ends up outside of newly applied region, the pointer may
+ * warped to a position within the new confinement region. If warped, a
+ * wl_pointer.motion event will be emitted, but no
+ * wp_relative_pointer.relative_motion event.
+ *
+ * The compositor may also, instead of using the new region, unconfine the
+ * pointer.
+ *
+ * For details about the confine region, see wp_confined_pointer.
+ */
+static inline void zwp_confined_pointer_v1_set_region(
+ struct zwp_confined_pointer_v1* zwp_confined_pointer_v1,
+ struct wl_region* region) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_confined_pointer_v1,
+ ZWP_CONFINED_POINTER_V1_SET_REGION, region);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c b/widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c
new file mode 100644
index 0000000000..c28ddf6918
--- /dev/null
+++ b/widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c
@@ -0,0 +1,97 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+/*
+ * Copyright © 2014 Jonas Ådahl
+ * Copyright © 2015 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#pragma GCC visibility push(default)
+extern const struct wl_interface wl_pointer_interface;
+extern const struct wl_interface wl_region_interface;
+extern const struct wl_interface wl_surface_interface;
+extern const struct wl_interface zwp_confined_pointer_v1_interface;
+extern const struct wl_interface zwp_locked_pointer_v1_interface;
+#pragma GCC visibility pop
+
+static const struct wl_interface* pointer_constraints_unstable_v1_types[] = {
+ NULL,
+ NULL,
+ &zwp_locked_pointer_v1_interface,
+ &wl_surface_interface,
+ &wl_pointer_interface,
+ &wl_region_interface,
+ NULL,
+ &zwp_confined_pointer_v1_interface,
+ &wl_surface_interface,
+ &wl_pointer_interface,
+ &wl_region_interface,
+ NULL,
+ &wl_region_interface,
+ &wl_region_interface,
+};
+
+static const struct wl_message zwp_pointer_constraints_v1_requests[] = {
+ {"destroy", "", pointer_constraints_unstable_v1_types + 0},
+ {"lock_pointer", "noo?ou", pointer_constraints_unstable_v1_types + 2},
+ {"confine_pointer", "noo?ou", pointer_constraints_unstable_v1_types + 7},
+};
+
+WL_EXPORT const struct wl_interface zwp_pointer_constraints_v1_interface = {
+ "zwp_pointer_constraints_v1", 1, 3,
+ zwp_pointer_constraints_v1_requests, 0, NULL,
+};
+
+static const struct wl_message zwp_locked_pointer_v1_requests[] = {
+ {"destroy", "", pointer_constraints_unstable_v1_types + 0},
+ {"set_cursor_position_hint", "ff",
+ pointer_constraints_unstable_v1_types + 0},
+ {"set_region", "?o", pointer_constraints_unstable_v1_types + 12},
+};
+
+static const struct wl_message zwp_locked_pointer_v1_events[] = {
+ {"locked", "", pointer_constraints_unstable_v1_types + 0},
+ {"unlocked", "", pointer_constraints_unstable_v1_types + 0},
+};
+
+WL_EXPORT const struct wl_interface zwp_locked_pointer_v1_interface = {
+ "zwp_locked_pointer_v1", 1, 3,
+ zwp_locked_pointer_v1_requests, 2, zwp_locked_pointer_v1_events,
+};
+
+static const struct wl_message zwp_confined_pointer_v1_requests[] = {
+ {"destroy", "", pointer_constraints_unstable_v1_types + 0},
+ {"set_region", "?o", pointer_constraints_unstable_v1_types + 13},
+};
+
+static const struct wl_message zwp_confined_pointer_v1_events[] = {
+ {"confined", "", pointer_constraints_unstable_v1_types + 0},
+ {"unconfined", "", pointer_constraints_unstable_v1_types + 0},
+};
+
+WL_EXPORT const struct wl_interface zwp_confined_pointer_v1_interface = {
+ "zwp_confined_pointer_v1", 1, 2,
+ zwp_confined_pointer_v1_requests, 2, zwp_confined_pointer_v1_events,
+};
diff --git a/widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h b/widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h
new file mode 100644
index 0000000000..dbae8081f4
--- /dev/null
+++ b/widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h
@@ -0,0 +1,293 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+#ifndef RELATIVE_POINTER_UNSTABLE_V1_CLIENT_PROTOCOL_H
+#define RELATIVE_POINTER_UNSTABLE_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_relative_pointer_unstable_v1 The relative_pointer_unstable_v1
+ * protocol protocol for relative pointer motion events
+ *
+ * @section page_desc_relative_pointer_unstable_v1 Description
+ *
+ * This protocol specifies a set of interfaces used for making clients able to
+ * receive relative pointer events not obstructed by barriers (such as the
+ * monitor edge or other pointer barriers).
+ *
+ * To start receiving relative pointer events, a client must first bind the
+ * global interface "wp_relative_pointer_manager" which, if a compositor
+ * supports relative pointer motion events, is exposed by the registry. After
+ * having created the relative pointer manager proxy object, the client uses
+ * it to create the actual relative pointer object using the
+ * "get_relative_pointer" request given a wl_pointer. The relative pointer
+ * motion events will then, when applicable, be transmitted via the proxy of
+ * the newly created relative pointer object. See the documentation of the
+ * relative pointer interface for more details.
+ *
+ * Warning! The protocol described in this file is experimental and backward
+ * incompatible changes may be made. Backward compatible changes may be added
+ * together with the corresponding interface version bump. Backward
+ * incompatible changes are done by bumping the version number in the protocol
+ * and interface names and resetting the interface version. Once the protocol
+ * is to be declared stable, the 'z' prefix and the version number in the
+ * protocol and interface names are removed and the interface version number is
+ * reset.
+ *
+ * @section page_ifaces_relative_pointer_unstable_v1 Interfaces
+ * - @subpage page_iface_zwp_relative_pointer_manager_v1 - get relative pointer
+ * objects
+ * - @subpage page_iface_zwp_relative_pointer_v1 - relative pointer object
+ * @section page_copyright_relative_pointer_unstable_v1 Copyright
+ * <pre>
+ *
+ * Copyright © 2014 Jonas Ådahl
+ * Copyright © 2015 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_pointer;
+struct zwp_relative_pointer_manager_v1;
+struct zwp_relative_pointer_v1;
+
+/**
+ * @page page_iface_zwp_relative_pointer_manager_v1
+ * zwp_relative_pointer_manager_v1
+ * @section page_iface_zwp_relative_pointer_manager_v1_desc Description
+ *
+ * A global interface used for getting the relative pointer object for a
+ * given pointer.
+ * @section page_iface_zwp_relative_pointer_manager_v1_api API
+ * See @ref iface_zwp_relative_pointer_manager_v1.
+ */
+/**
+ * @defgroup iface_zwp_relative_pointer_manager_v1 The
+ * zwp_relative_pointer_manager_v1 interface
+ *
+ * A global interface used for getting the relative pointer object for a
+ * given pointer.
+ */
+extern const struct wl_interface zwp_relative_pointer_manager_v1_interface;
+/**
+ * @page page_iface_zwp_relative_pointer_v1 zwp_relative_pointer_v1
+ * @section page_iface_zwp_relative_pointer_v1_desc Description
+ *
+ * A wp_relative_pointer object is an extension to the wl_pointer interface
+ * used for emitting relative pointer events. It shares the same focus as
+ * wl_pointer objects of the same seat and will only emit events when it has
+ * focus.
+ * @section page_iface_zwp_relative_pointer_v1_api API
+ * See @ref iface_zwp_relative_pointer_v1.
+ */
+/**
+ * @defgroup iface_zwp_relative_pointer_v1 The zwp_relative_pointer_v1 interface
+ *
+ * A wp_relative_pointer object is an extension to the wl_pointer interface
+ * used for emitting relative pointer events. It shares the same focus as
+ * wl_pointer objects of the same seat and will only emit events when it has
+ * focus.
+ */
+extern const struct wl_interface zwp_relative_pointer_v1_interface;
+
+#define ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY 0
+#define ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER 1
+
+/**
+ * @ingroup iface_zwp_relative_pointer_manager_v1
+ */
+#define ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_relative_pointer_manager_v1
+ */
+#define ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_relative_pointer_manager_v1 */
+static inline void zwp_relative_pointer_manager_v1_set_user_data(
+ struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1,
+ void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_relative_pointer_manager_v1,
+ user_data);
+}
+
+/** @ingroup iface_zwp_relative_pointer_manager_v1 */
+static inline void* zwp_relative_pointer_manager_v1_get_user_data(
+ struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1) {
+ return wl_proxy_get_user_data(
+ (struct wl_proxy*)zwp_relative_pointer_manager_v1);
+}
+
+static inline uint32_t zwp_relative_pointer_manager_v1_get_version(
+ struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1) {
+ return wl_proxy_get_version(
+ (struct wl_proxy*)zwp_relative_pointer_manager_v1);
+}
+
+/**
+ * @ingroup iface_zwp_relative_pointer_manager_v1
+ *
+ * Used by the client to notify the server that it will no longer use this
+ * relative pointer manager object.
+ */
+static inline void zwp_relative_pointer_manager_v1_destroy(
+ struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_relative_pointer_manager_v1,
+ ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_relative_pointer_manager_v1);
+}
+
+/**
+ * @ingroup iface_zwp_relative_pointer_manager_v1
+ *
+ * Create a relative pointer interface given a wl_pointer object. See the
+ * wp_relative_pointer interface for more details.
+ */
+static inline struct zwp_relative_pointer_v1*
+zwp_relative_pointer_manager_v1_get_relative_pointer(
+ struct zwp_relative_pointer_manager_v1* zwp_relative_pointer_manager_v1,
+ struct wl_pointer* pointer) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor(
+ (struct wl_proxy*)zwp_relative_pointer_manager_v1,
+ ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER,
+ &zwp_relative_pointer_v1_interface, NULL, pointer);
+
+ return (struct zwp_relative_pointer_v1*)id;
+}
+
+/**
+ * @ingroup iface_zwp_relative_pointer_v1
+ * @struct zwp_relative_pointer_v1_listener
+ */
+struct zwp_relative_pointer_v1_listener {
+ /**
+ * relative pointer motion
+ *
+ * Relative x/y pointer motion from the pointer of the seat
+ * associated with this object.
+ *
+ * A relative motion is in the same dimension as regular wl_pointer
+ * motion events, except they do not represent an absolute
+ * position. For example, moving a pointer from (x, y) to (x', y')
+ * would have the equivalent relative motion (x' - x, y' - y). If a
+ * pointer motion caused the absolute pointer position to be
+ * clipped by for example the edge of the monitor, the relative
+ * motion is unaffected by the clipping and will represent the
+ * unclipped motion.
+ *
+ * This event also contains non-accelerated motion deltas. The
+ * non-accelerated delta is, when applicable, the regular pointer
+ * motion delta as it was before having applied motion acceleration
+ * and other transformations such as normalization.
+ *
+ * Note that the non-accelerated delta does not represent 'raw'
+ * events as they were read from some device. Pointer motion
+ * acceleration is device- and configuration-specific and
+ * non-accelerated deltas and accelerated deltas may have the same
+ * value on some devices.
+ *
+ * Relative motions are not coupled to wl_pointer.motion events,
+ * and can be sent in combination with such events, but also
+ * independently. There may also be scenarios where
+ * wl_pointer.motion is sent, but there is no relative motion. The
+ * order of an absolute and relative motion event originating from
+ * the same physical motion is not guaranteed.
+ *
+ * If the client needs button events or focus state, it can receive
+ * them from a wl_pointer object of the same seat that the
+ * wp_relative_pointer object is associated with.
+ * @param utime_hi high 32 bits of a 64 bit timestamp with microsecond
+ * granularity
+ * @param utime_lo low 32 bits of a 64 bit timestamp with microsecond
+ * granularity
+ * @param dx the x component of the motion vector
+ * @param dy the y component of the motion vector
+ * @param dx_unaccel the x component of the unaccelerated motion vector
+ * @param dy_unaccel the y component of the unaccelerated motion vector
+ */
+ void (*relative_motion)(
+ void* data, struct zwp_relative_pointer_v1* zwp_relative_pointer_v1,
+ uint32_t utime_hi, uint32_t utime_lo, wl_fixed_t dx, wl_fixed_t dy,
+ wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel);
+};
+
+/**
+ * @ingroup iface_zwp_relative_pointer_v1
+ */
+static inline int zwp_relative_pointer_v1_add_listener(
+ struct zwp_relative_pointer_v1* zwp_relative_pointer_v1,
+ const struct zwp_relative_pointer_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)zwp_relative_pointer_v1,
+ (void (**)(void))listener, data);
+}
+
+#define ZWP_RELATIVE_POINTER_V1_DESTROY 0
+
+/**
+ * @ingroup iface_zwp_relative_pointer_v1
+ */
+#define ZWP_RELATIVE_POINTER_V1_RELATIVE_MOTION_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_relative_pointer_v1
+ */
+#define ZWP_RELATIVE_POINTER_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_relative_pointer_v1 */
+static inline void zwp_relative_pointer_v1_set_user_data(
+ struct zwp_relative_pointer_v1* zwp_relative_pointer_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zwp_relative_pointer_v1, user_data);
+}
+
+/** @ingroup iface_zwp_relative_pointer_v1 */
+static inline void* zwp_relative_pointer_v1_get_user_data(
+ struct zwp_relative_pointer_v1* zwp_relative_pointer_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zwp_relative_pointer_v1);
+}
+
+static inline uint32_t zwp_relative_pointer_v1_get_version(
+ struct zwp_relative_pointer_v1* zwp_relative_pointer_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zwp_relative_pointer_v1);
+}
+
+/**
+ * @ingroup iface_zwp_relative_pointer_v1
+ */
+static inline void zwp_relative_pointer_v1_destroy(
+ struct zwp_relative_pointer_v1* zwp_relative_pointer_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zwp_relative_pointer_v1,
+ ZWP_RELATIVE_POINTER_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zwp_relative_pointer_v1);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c b/widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c
new file mode 100644
index 0000000000..3534686a3d
--- /dev/null
+++ b/widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c
@@ -0,0 +1,69 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+/*
+ * Copyright © 2014 Jonas Ådahl
+ * Copyright © 2015 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#pragma GCC visibility push(default)
+extern const struct wl_interface wl_pointer_interface;
+extern const struct wl_interface zwp_relative_pointer_v1_interface;
+#pragma GCC visibility pop
+
+static const struct wl_interface* relative_pointer_unstable_v1_types[] = {
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ &zwp_relative_pointer_v1_interface,
+ &wl_pointer_interface,
+};
+
+static const struct wl_message zwp_relative_pointer_manager_v1_requests[] = {
+ {"destroy", "", relative_pointer_unstable_v1_types + 0},
+ {"get_relative_pointer", "no", relative_pointer_unstable_v1_types + 6},
+};
+
+WL_EXPORT const struct wl_interface zwp_relative_pointer_manager_v1_interface =
+ {
+ "zwp_relative_pointer_manager_v1", 1, 2,
+ zwp_relative_pointer_manager_v1_requests, 0, NULL,
+};
+
+static const struct wl_message zwp_relative_pointer_v1_requests[] = {
+ {"destroy", "", relative_pointer_unstable_v1_types + 0},
+};
+
+static const struct wl_message zwp_relative_pointer_v1_events[] = {
+ {"relative_motion", "uuffff", relative_pointer_unstable_v1_types + 0},
+};
+
+WL_EXPORT const struct wl_interface zwp_relative_pointer_v1_interface = {
+ "zwp_relative_pointer_v1", 1, 1,
+ zwp_relative_pointer_v1_requests, 1, zwp_relative_pointer_v1_events,
+};
diff --git a/widget/gtk/wayland/viewporter-client-protocol.h b/widget/gtk/wayland/viewporter-client-protocol.h
new file mode 100644
index 0000000000..4c6c5bd910
--- /dev/null
+++ b/widget/gtk/wayland/viewporter-client-protocol.h
@@ -0,0 +1,392 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+#ifndef VIEWPORTER_CLIENT_PROTOCOL_H
+#define VIEWPORTER_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_viewporter The viewporter protocol
+ * @section page_ifaces_viewporter Interfaces
+ * - @subpage page_iface_wp_viewporter - surface cropping and scaling
+ * - @subpage page_iface_wp_viewport - crop and scale interface to a wl_surface
+ * @section page_copyright_viewporter Copyright
+ * <pre>
+ *
+ * Copyright © 2013-2016 Collabora, Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_surface;
+struct wp_viewport;
+struct wp_viewporter;
+
+/**
+ * @page page_iface_wp_viewporter wp_viewporter
+ * @section page_iface_wp_viewporter_desc Description
+ *
+ * The global interface exposing surface cropping and scaling
+ * capabilities is used to instantiate an interface extension for a
+ * wl_surface object. This extended interface will then allow
+ * cropping and scaling the surface contents, effectively
+ * disconnecting the direct relationship between the buffer and the
+ * surface size.
+ * @section page_iface_wp_viewporter_api API
+ * See @ref iface_wp_viewporter.
+ */
+/**
+ * @defgroup iface_wp_viewporter The wp_viewporter interface
+ *
+ * The global interface exposing surface cropping and scaling
+ * capabilities is used to instantiate an interface extension for a
+ * wl_surface object. This extended interface will then allow
+ * cropping and scaling the surface contents, effectively
+ * disconnecting the direct relationship between the buffer and the
+ * surface size.
+ */
+extern const struct wl_interface wp_viewporter_interface;
+/**
+ * @page page_iface_wp_viewport wp_viewport
+ * @section page_iface_wp_viewport_desc Description
+ *
+ * An additional interface to a wl_surface object, which allows the
+ * client to specify the cropping and scaling of the surface
+ * contents.
+ *
+ * This interface works with two concepts: the source rectangle (src_x,
+ * src_y, src_width, src_height), and the destination size (dst_width,
+ * dst_height). The contents of the source rectangle are scaled to the
+ * destination size, and content outside the source rectangle is ignored.
+ * This state is double-buffered, and is applied on the next
+ * wl_surface.commit.
+ *
+ * The two parts of crop and scale state are independent: the source
+ * rectangle, and the destination size. Initially both are unset, that
+ * is, no scaling is applied. The whole of the current wl_buffer is
+ * used as the source, and the surface size is as defined in
+ * wl_surface.attach.
+ *
+ * If the destination size is set, it causes the surface size to become
+ * dst_width, dst_height. The source (rectangle) is scaled to exactly
+ * this size. This overrides whatever the attached wl_buffer size is,
+ * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
+ * has no content and therefore no size. Otherwise, the size is always
+ * at least 1x1 in surface local coordinates.
+ *
+ * If the source rectangle is set, it defines what area of the wl_buffer is
+ * taken as the source. If the source rectangle is set and the destination
+ * size is not set, then src_width and src_height must be integers, and the
+ * surface size becomes the source rectangle size. This results in cropping
+ * without scaling. If src_width or src_height are not integers and
+ * destination size is not set, the bad_size protocol error is raised when
+ * the surface state is applied.
+ *
+ * The coordinate transformations from buffer pixel coordinates up to
+ * the surface-local coordinates happen in the following order:
+ * 1. buffer_transform (wl_surface.set_buffer_transform)
+ * 2. buffer_scale (wl_surface.set_buffer_scale)
+ * 3. crop and scale (wp_viewport.set*)
+ * This means, that the source rectangle coordinates of crop and scale
+ * are given in the coordinates after the buffer transform and scale,
+ * i.e. in the coordinates that would be the surface-local coordinates
+ * if the crop and scale was not applied.
+ *
+ * If src_x or src_y are negative, the bad_value protocol error is raised.
+ * Otherwise, if the source rectangle is partially or completely outside of
+ * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
+ * when the surface state is applied. A NULL wl_buffer does not raise the
+ * out_of_buffer error.
+ *
+ * The x, y arguments of wl_surface.attach are applied as normal to
+ * the surface. They indicate how many pixels to remove from the
+ * surface size from the left and the top. In other words, they are
+ * still in the surface-local coordinate system, just like dst_width
+ * and dst_height are.
+ *
+ * If the wl_surface associated with the wp_viewport is destroyed,
+ * all wp_viewport requests except 'destroy' raise the protocol error
+ * no_surface.
+ *
+ * If the wp_viewport object is destroyed, the crop and scale
+ * state is removed from the wl_surface. The change will be applied
+ * on the next wl_surface.commit.
+ * @section page_iface_wp_viewport_api API
+ * See @ref iface_wp_viewport.
+ */
+/**
+ * @defgroup iface_wp_viewport The wp_viewport interface
+ *
+ * An additional interface to a wl_surface object, which allows the
+ * client to specify the cropping and scaling of the surface
+ * contents.
+ *
+ * This interface works with two concepts: the source rectangle (src_x,
+ * src_y, src_width, src_height), and the destination size (dst_width,
+ * dst_height). The contents of the source rectangle are scaled to the
+ * destination size, and content outside the source rectangle is ignored.
+ * This state is double-buffered, and is applied on the next
+ * wl_surface.commit.
+ *
+ * The two parts of crop and scale state are independent: the source
+ * rectangle, and the destination size. Initially both are unset, that
+ * is, no scaling is applied. The whole of the current wl_buffer is
+ * used as the source, and the surface size is as defined in
+ * wl_surface.attach.
+ *
+ * If the destination size is set, it causes the surface size to become
+ * dst_width, dst_height. The source (rectangle) is scaled to exactly
+ * this size. This overrides whatever the attached wl_buffer size is,
+ * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
+ * has no content and therefore no size. Otherwise, the size is always
+ * at least 1x1 in surface local coordinates.
+ *
+ * If the source rectangle is set, it defines what area of the wl_buffer is
+ * taken as the source. If the source rectangle is set and the destination
+ * size is not set, then src_width and src_height must be integers, and the
+ * surface size becomes the source rectangle size. This results in cropping
+ * without scaling. If src_width or src_height are not integers and
+ * destination size is not set, the bad_size protocol error is raised when
+ * the surface state is applied.
+ *
+ * The coordinate transformations from buffer pixel coordinates up to
+ * the surface-local coordinates happen in the following order:
+ * 1. buffer_transform (wl_surface.set_buffer_transform)
+ * 2. buffer_scale (wl_surface.set_buffer_scale)
+ * 3. crop and scale (wp_viewport.set*)
+ * This means, that the source rectangle coordinates of crop and scale
+ * are given in the coordinates after the buffer transform and scale,
+ * i.e. in the coordinates that would be the surface-local coordinates
+ * if the crop and scale was not applied.
+ *
+ * If src_x or src_y are negative, the bad_value protocol error is raised.
+ * Otherwise, if the source rectangle is partially or completely outside of
+ * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
+ * when the surface state is applied. A NULL wl_buffer does not raise the
+ * out_of_buffer error.
+ *
+ * The x, y arguments of wl_surface.attach are applied as normal to
+ * the surface. They indicate how many pixels to remove from the
+ * surface size from the left and the top. In other words, they are
+ * still in the surface-local coordinate system, just like dst_width
+ * and dst_height are.
+ *
+ * If the wl_surface associated with the wp_viewport is destroyed,
+ * all wp_viewport requests except 'destroy' raise the protocol error
+ * no_surface.
+ *
+ * If the wp_viewport object is destroyed, the crop and scale
+ * state is removed from the wl_surface. The change will be applied
+ * on the next wl_surface.commit.
+ */
+extern const struct wl_interface wp_viewport_interface;
+
+#ifndef WP_VIEWPORTER_ERROR_ENUM
+# define WP_VIEWPORTER_ERROR_ENUM
+enum wp_viewporter_error {
+ /**
+ * the surface already has a viewport object associated
+ */
+ WP_VIEWPORTER_ERROR_VIEWPORT_EXISTS = 0,
+};
+#endif /* WP_VIEWPORTER_ERROR_ENUM */
+
+#define WP_VIEWPORTER_DESTROY 0
+#define WP_VIEWPORTER_GET_VIEWPORT 1
+
+/**
+ * @ingroup iface_wp_viewporter
+ */
+#define WP_VIEWPORTER_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_wp_viewporter
+ */
+#define WP_VIEWPORTER_GET_VIEWPORT_SINCE_VERSION 1
+
+/** @ingroup iface_wp_viewporter */
+static inline void wp_viewporter_set_user_data(
+ struct wp_viewporter* wp_viewporter, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)wp_viewporter, user_data);
+}
+
+/** @ingroup iface_wp_viewporter */
+static inline void* wp_viewporter_get_user_data(
+ struct wp_viewporter* wp_viewporter) {
+ return wl_proxy_get_user_data((struct wl_proxy*)wp_viewporter);
+}
+
+static inline uint32_t wp_viewporter_get_version(
+ struct wp_viewporter* wp_viewporter) {
+ return wl_proxy_get_version((struct wl_proxy*)wp_viewporter);
+}
+
+/**
+ * @ingroup iface_wp_viewporter
+ *
+ * Informs the server that the client will not be using this
+ * protocol object anymore. This does not affect any other objects,
+ * wp_viewport objects included.
+ */
+static inline void wp_viewporter_destroy(struct wp_viewporter* wp_viewporter) {
+ wl_proxy_marshal((struct wl_proxy*)wp_viewporter, WP_VIEWPORTER_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)wp_viewporter);
+}
+
+/**
+ * @ingroup iface_wp_viewporter
+ *
+ * Instantiate an interface extension for the given wl_surface to
+ * crop and scale its content. If the given wl_surface already has
+ * a wp_viewport object associated, the viewport_exists
+ * protocol error is raised.
+ */
+static inline struct wp_viewport* wp_viewporter_get_viewport(
+ struct wp_viewporter* wp_viewporter, struct wl_surface* surface) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor((struct wl_proxy*)wp_viewporter,
+ WP_VIEWPORTER_GET_VIEWPORT,
+ &wp_viewport_interface, NULL, surface);
+
+ return (struct wp_viewport*)id;
+}
+
+#ifndef WP_VIEWPORT_ERROR_ENUM
+# define WP_VIEWPORT_ERROR_ENUM
+enum wp_viewport_error {
+ /**
+ * negative or zero values in width or height
+ */
+ WP_VIEWPORT_ERROR_BAD_VALUE = 0,
+ /**
+ * destination size is not integer
+ */
+ WP_VIEWPORT_ERROR_BAD_SIZE = 1,
+ /**
+ * source rectangle extends outside of the content area
+ */
+ WP_VIEWPORT_ERROR_OUT_OF_BUFFER = 2,
+ /**
+ * the wl_surface was destroyed
+ */
+ WP_VIEWPORT_ERROR_NO_SURFACE = 3,
+};
+#endif /* WP_VIEWPORT_ERROR_ENUM */
+
+#define WP_VIEWPORT_DESTROY 0
+#define WP_VIEWPORT_SET_SOURCE 1
+#define WP_VIEWPORT_SET_DESTINATION 2
+
+/**
+ * @ingroup iface_wp_viewport
+ */
+#define WP_VIEWPORT_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_wp_viewport
+ */
+#define WP_VIEWPORT_SET_SOURCE_SINCE_VERSION 1
+/**
+ * @ingroup iface_wp_viewport
+ */
+#define WP_VIEWPORT_SET_DESTINATION_SINCE_VERSION 1
+
+/** @ingroup iface_wp_viewport */
+static inline void wp_viewport_set_user_data(struct wp_viewport* wp_viewport,
+ void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)wp_viewport, user_data);
+}
+
+/** @ingroup iface_wp_viewport */
+static inline void* wp_viewport_get_user_data(struct wp_viewport* wp_viewport) {
+ return wl_proxy_get_user_data((struct wl_proxy*)wp_viewport);
+}
+
+static inline uint32_t wp_viewport_get_version(
+ struct wp_viewport* wp_viewport) {
+ return wl_proxy_get_version((struct wl_proxy*)wp_viewport);
+}
+
+/**
+ * @ingroup iface_wp_viewport
+ *
+ * The associated wl_surface's crop and scale state is removed.
+ * The change is applied on the next wl_surface.commit.
+ */
+static inline void wp_viewport_destroy(struct wp_viewport* wp_viewport) {
+ wl_proxy_marshal((struct wl_proxy*)wp_viewport, WP_VIEWPORT_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)wp_viewport);
+}
+
+/**
+ * @ingroup iface_wp_viewport
+ *
+ * Set the source rectangle of the associated wl_surface. See
+ * wp_viewport for the description, and relation to the wl_buffer
+ * size.
+ *
+ * If all of x, y, width and height are -1.0, the source rectangle is
+ * unset instead. Any other set of values where width or height are zero
+ * or negative, or x or y are negative, raise the bad_value protocol
+ * error.
+ *
+ * The crop and scale state is double-buffered state, and will be
+ * applied on the next wl_surface.commit.
+ */
+static inline void wp_viewport_set_source(struct wp_viewport* wp_viewport,
+ wl_fixed_t x, wl_fixed_t y,
+ wl_fixed_t width, wl_fixed_t height) {
+ wl_proxy_marshal((struct wl_proxy*)wp_viewport, WP_VIEWPORT_SET_SOURCE, x, y,
+ width, height);
+}
+
+/**
+ * @ingroup iface_wp_viewport
+ *
+ * Set the destination size of the associated wl_surface. See
+ * wp_viewport for the description, and relation to the wl_buffer
+ * size.
+ *
+ * If width is -1 and height is -1, the destination size is unset
+ * instead. Any other pair of values for width and height that
+ * contains zero or negative values raises the bad_value protocol
+ * error.
+ *
+ * The crop and scale state is double-buffered state, and will be
+ * applied on the next wl_surface.commit.
+ */
+static inline void wp_viewport_set_destination(struct wp_viewport* wp_viewport,
+ int32_t width, int32_t height) {
+ wl_proxy_marshal((struct wl_proxy*)wp_viewport, WP_VIEWPORT_SET_DESTINATION,
+ width, height);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/viewporter-protocol.c b/widget/gtk/wayland/viewporter-protocol.c
new file mode 100644
index 0000000000..06b7901426
--- /dev/null
+++ b/widget/gtk/wayland/viewporter-protocol.c
@@ -0,0 +1,56 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+/*
+ * Copyright © 2013-2016 Collabora, Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#pragma GCC visibility push(default)
+extern const struct wl_interface wl_surface_interface;
+extern const struct wl_interface wp_viewport_interface;
+#pragma GCC visibility pop
+
+static const struct wl_interface* viewporter_types[] = {
+ NULL, NULL, NULL, NULL, &wp_viewport_interface, &wl_surface_interface,
+};
+
+static const struct wl_message wp_viewporter_requests[] = {
+ {"destroy", "", viewporter_types + 0},
+ {"get_viewport", "no", viewporter_types + 4},
+};
+
+const struct wl_interface wp_viewporter_interface = {
+ "wp_viewporter", 1, 2, wp_viewporter_requests, 0, NULL,
+};
+
+static const struct wl_message wp_viewport_requests[] = {
+ {"destroy", "", viewporter_types + 0},
+ {"set_source", "ffff", viewporter_types + 0},
+ {"set_destination", "ii", viewporter_types + 0},
+};
+
+const struct wl_interface wp_viewport_interface = {
+ "wp_viewport", 1, 3, wp_viewport_requests, 0, NULL,
+};
diff --git a/widget/gtk/wayland/xdg-activation-v1-client-protocol.h b/widget/gtk/wayland/xdg-activation-v1-client-protocol.h
new file mode 100644
index 0000000000..9bebcb6ec2
--- /dev/null
+++ b/widget/gtk/wayland/xdg-activation-v1-client-protocol.h
@@ -0,0 +1,409 @@
+/* Generated by wayland-scanner 1.19.0 */
+
+#ifndef XDG_ACTIVATION_V1_CLIENT_PROTOCOL_H
+#define XDG_ACTIVATION_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_xdg_activation_v1 The xdg_activation_v1 protocol
+ * Protocol for requesting activation of surfaces
+ *
+ * @section page_desc_xdg_activation_v1 Description
+ *
+ * The way for a client to pass focus to another toplevel is as follows.
+ *
+ * The client that intends to activate another toplevel uses the
+ * xdg_activation_v1.get_activation_token request to get an activation token.
+ * This token is then forwarded to the client, which is supposed to activate
+ * one of its surfaces, through a separate band of communication.
+ *
+ * One established way of doing this is through the XDG_ACTIVATION_TOKEN
+ * environment variable of a newly launched child process. The child process
+ * should unset the environment variable again right after reading it out in
+ * order to avoid propagating it to other child processes.
+ *
+ * Another established way exists for Applications implementing the D-Bus
+ * interface org.freedesktop.Application, which should get their token under
+ * XDG_ACTIVATION_TOKEN on their platform_data.
+ *
+ * In general activation tokens may be transferred across clients through
+ * means not described in this protocol.
+ *
+ * The client to be activated will then pass the token
+ * it received to the xdg_activation_v1.activate request. The compositor can
+ * then use this token to decide how to react to the activation request.
+ *
+ * The token the activating client gets may be ineffective either already at
+ * the time it receives it, for example if it was not focused, for focus
+ * stealing prevention. The activating client will have no way to discover
+ * the validity of the token, and may still forward it to the to be activated
+ * client.
+ *
+ * The created activation token may optionally get information attached to it
+ * that can be used by the compositor to identify the application that we
+ * intend to activate. This can for example be used to display a visual hint
+ * about what application is being started.
+ *
+ * Warning! The protocol described in this file is currently in the testing
+ * phase. Backward compatible changes may be added together with the
+ * corresponding interface version bump. Backward incompatible changes can
+ * only be done by creating a new major version of the extension.
+ *
+ * @section page_ifaces_xdg_activation_v1 Interfaces
+ * - @subpage page_iface_xdg_activation_v1 - interface for activating surfaces
+ * - @subpage page_iface_xdg_activation_token_v1 - an exported activation handle
+ * @section page_copyright_xdg_activation_v1 Copyright
+ * <pre>
+ *
+ * Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
+ * Copyright © 2020 Carlos Garnacho <carlosg@gnome.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_seat;
+struct wl_surface;
+struct xdg_activation_token_v1;
+struct xdg_activation_v1;
+
+#ifndef XDG_ACTIVATION_V1_INTERFACE
+# define XDG_ACTIVATION_V1_INTERFACE
+/**
+ * @page page_iface_xdg_activation_v1 xdg_activation_v1
+ * @section page_iface_xdg_activation_v1_desc Description
+ *
+ * A global interface used for informing the compositor about applications
+ * being activated or started, or for applications to request to be
+ * activated.
+ * @section page_iface_xdg_activation_v1_api API
+ * See @ref iface_xdg_activation_v1.
+ */
+/**
+ * @defgroup iface_xdg_activation_v1 The xdg_activation_v1 interface
+ *
+ * A global interface used for informing the compositor about applications
+ * being activated or started, or for applications to request to be
+ * activated.
+ */
+extern const struct wl_interface xdg_activation_v1_interface;
+#endif
+#ifndef XDG_ACTIVATION_TOKEN_V1_INTERFACE
+# define XDG_ACTIVATION_TOKEN_V1_INTERFACE
+/**
+ * @page page_iface_xdg_activation_token_v1 xdg_activation_token_v1
+ * @section page_iface_xdg_activation_token_v1_desc Description
+ *
+ * An object for setting up a token and receiving a token handle that can
+ * be passed as an activation token to another client.
+ *
+ * The object is created using the xdg_activation_v1.get_activation_token
+ * request. This object should then be populated with the app_id, surface
+ * and serial information and committed. The compositor shall then issue a
+ * done event with the token. In case the request's parameters are invalid,
+ * the compositor will provide an invalid token.
+ * @section page_iface_xdg_activation_token_v1_api API
+ * See @ref iface_xdg_activation_token_v1.
+ */
+/**
+ * @defgroup iface_xdg_activation_token_v1 The xdg_activation_token_v1 interface
+ *
+ * An object for setting up a token and receiving a token handle that can
+ * be passed as an activation token to another client.
+ *
+ * The object is created using the xdg_activation_v1.get_activation_token
+ * request. This object should then be populated with the app_id, surface
+ * and serial information and committed. The compositor shall then issue a
+ * done event with the token. In case the request's parameters are invalid,
+ * the compositor will provide an invalid token.
+ */
+extern const struct wl_interface xdg_activation_token_v1_interface;
+#endif
+
+#define XDG_ACTIVATION_V1_DESTROY 0
+#define XDG_ACTIVATION_V1_GET_ACTIVATION_TOKEN 1
+#define XDG_ACTIVATION_V1_ACTIVATE 2
+
+/**
+ * @ingroup iface_xdg_activation_v1
+ */
+#define XDG_ACTIVATION_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_activation_v1
+ */
+#define XDG_ACTIVATION_V1_GET_ACTIVATION_TOKEN_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_activation_v1
+ */
+#define XDG_ACTIVATION_V1_ACTIVATE_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_activation_v1 */
+static inline void xdg_activation_v1_set_user_data(
+ struct xdg_activation_v1* xdg_activation_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)xdg_activation_v1, user_data);
+}
+
+/** @ingroup iface_xdg_activation_v1 */
+static inline void* xdg_activation_v1_get_user_data(
+ struct xdg_activation_v1* xdg_activation_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)xdg_activation_v1);
+}
+
+static inline uint32_t xdg_activation_v1_get_version(
+ struct xdg_activation_v1* xdg_activation_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)xdg_activation_v1);
+}
+
+/**
+ * @ingroup iface_xdg_activation_v1
+ *
+ * Notify the compositor that the xdg_activation object will no longer be
+ * used.
+ *
+ * The child objects created via this interface are unaffected and should
+ * be destroyed separately.
+ */
+static inline void xdg_activation_v1_destroy(
+ struct xdg_activation_v1* xdg_activation_v1) {
+ wl_proxy_marshal((struct wl_proxy*)xdg_activation_v1,
+ XDG_ACTIVATION_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)xdg_activation_v1);
+}
+
+/**
+ * @ingroup iface_xdg_activation_v1
+ *
+ * Creates an xdg_activation_token_v1 object that will provide
+ * the initiating client with a unique token for this activation. This
+ * token should be offered to the clients to be activated.
+ */
+static inline struct xdg_activation_token_v1*
+xdg_activation_v1_get_activation_token(
+ struct xdg_activation_v1* xdg_activation_v1) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor((struct wl_proxy*)xdg_activation_v1,
+ XDG_ACTIVATION_V1_GET_ACTIVATION_TOKEN,
+ &xdg_activation_token_v1_interface, NULL);
+
+ return (struct xdg_activation_token_v1*)id;
+}
+
+/**
+ * @ingroup iface_xdg_activation_v1
+ *
+ * Requests surface activation. It's up to the compositor to display
+ * this information as desired, for example by placing the surface above
+ * the rest.
+ *
+ * The compositor may know who requested this by checking the activation
+ * token and might decide not to follow through with the activation if it's
+ * considered unwanted.
+ *
+ * Compositors can ignore unknown activation tokens when an invalid
+ * token is passed.
+ */
+static inline void xdg_activation_v1_activate(
+ struct xdg_activation_v1* xdg_activation_v1, const char* token,
+ struct wl_surface* surface) {
+ wl_proxy_marshal((struct wl_proxy*)xdg_activation_v1,
+ XDG_ACTIVATION_V1_ACTIVATE, token, surface);
+}
+
+#ifndef XDG_ACTIVATION_TOKEN_V1_ERROR_ENUM
+# define XDG_ACTIVATION_TOKEN_V1_ERROR_ENUM
+enum xdg_activation_token_v1_error {
+ /**
+ * The token has already been used previously
+ */
+ XDG_ACTIVATION_TOKEN_V1_ERROR_ALREADY_USED = 0,
+};
+#endif /* XDG_ACTIVATION_TOKEN_V1_ERROR_ENUM */
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ * @struct xdg_activation_token_v1_listener
+ */
+struct xdg_activation_token_v1_listener {
+ /**
+ * the exported activation token
+ *
+ * The 'done' event contains the unique token of this activation
+ * request and notifies that the provider is done.
+ * @param token the exported activation token
+ */
+ void (*done)(void* data,
+ struct xdg_activation_token_v1* xdg_activation_token_v1,
+ const char* token);
+};
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ */
+static inline int xdg_activation_token_v1_add_listener(
+ struct xdg_activation_token_v1* xdg_activation_token_v1,
+ const struct xdg_activation_token_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)xdg_activation_token_v1,
+ (void (**)(void))listener, data);
+}
+
+#define XDG_ACTIVATION_TOKEN_V1_SET_SERIAL 0
+#define XDG_ACTIVATION_TOKEN_V1_SET_APP_ID 1
+#define XDG_ACTIVATION_TOKEN_V1_SET_SURFACE 2
+#define XDG_ACTIVATION_TOKEN_V1_COMMIT 3
+#define XDG_ACTIVATION_TOKEN_V1_DESTROY 4
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ */
+#define XDG_ACTIVATION_TOKEN_V1_DONE_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ */
+#define XDG_ACTIVATION_TOKEN_V1_SET_SERIAL_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ */
+#define XDG_ACTIVATION_TOKEN_V1_SET_APP_ID_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ */
+#define XDG_ACTIVATION_TOKEN_V1_SET_SURFACE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ */
+#define XDG_ACTIVATION_TOKEN_V1_COMMIT_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ */
+#define XDG_ACTIVATION_TOKEN_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_activation_token_v1 */
+static inline void xdg_activation_token_v1_set_user_data(
+ struct xdg_activation_token_v1* xdg_activation_token_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)xdg_activation_token_v1, user_data);
+}
+
+/** @ingroup iface_xdg_activation_token_v1 */
+static inline void* xdg_activation_token_v1_get_user_data(
+ struct xdg_activation_token_v1* xdg_activation_token_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)xdg_activation_token_v1);
+}
+
+static inline uint32_t xdg_activation_token_v1_get_version(
+ struct xdg_activation_token_v1* xdg_activation_token_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)xdg_activation_token_v1);
+}
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ *
+ * Provides information about the seat and serial event that requested the
+ * token.
+ *
+ * The serial can come from an input or focus event. For instance, if a
+ * click triggers the launch of a third-party client, the launcher client
+ * should send a set_serial request with the serial and seat from the
+ * wl_pointer.button event.
+ *
+ * Some compositors might refuse to activate toplevels when the token
+ * doesn't have a valid and recent enough event serial.
+ *
+ * Must be sent before commit. This information is optional.
+ */
+static inline void xdg_activation_token_v1_set_serial(
+ struct xdg_activation_token_v1* xdg_activation_token_v1, uint32_t serial,
+ struct wl_seat* seat) {
+ wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1,
+ XDG_ACTIVATION_TOKEN_V1_SET_SERIAL, serial, seat);
+}
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ *
+ * The requesting client can specify an app_id to associate the token
+ * being created with it.
+ *
+ * Must be sent before commit. This information is optional.
+ */
+static inline void xdg_activation_token_v1_set_app_id(
+ struct xdg_activation_token_v1* xdg_activation_token_v1,
+ const char* app_id) {
+ wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1,
+ XDG_ACTIVATION_TOKEN_V1_SET_APP_ID, app_id);
+}
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ *
+ * This request sets the surface requesting the activation. Note, this is
+ * different from the surface that will be activated.
+ *
+ * Some compositors might refuse to activate toplevels when the token
+ * doesn't have a requesting surface.
+ *
+ * Must be sent before commit. This information is optional.
+ */
+static inline void xdg_activation_token_v1_set_surface(
+ struct xdg_activation_token_v1* xdg_activation_token_v1,
+ struct wl_surface* surface) {
+ wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1,
+ XDG_ACTIVATION_TOKEN_V1_SET_SURFACE, surface);
+}
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ *
+ * Requests an activation token based on the different parameters that
+ * have been offered through set_serial, set_surface and set_app_id.
+ */
+static inline void xdg_activation_token_v1_commit(
+ struct xdg_activation_token_v1* xdg_activation_token_v1) {
+ wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1,
+ XDG_ACTIVATION_TOKEN_V1_COMMIT);
+}
+
+/**
+ * @ingroup iface_xdg_activation_token_v1
+ *
+ * Notify the compositor that the xdg_activation_token_v1 object will no
+ * longer be used.
+ */
+static inline void xdg_activation_token_v1_destroy(
+ struct xdg_activation_token_v1* xdg_activation_token_v1) {
+ wl_proxy_marshal((struct wl_proxy*)xdg_activation_token_v1,
+ XDG_ACTIVATION_TOKEN_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)xdg_activation_token_v1);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/xdg-activation-v1-protocol.c b/widget/gtk/wayland/xdg-activation-v1-protocol.c
new file mode 100644
index 0000000000..1fad6dbf97
--- /dev/null
+++ b/widget/gtk/wayland/xdg-activation-v1-protocol.c
@@ -0,0 +1,82 @@
+/* Generated by wayland-scanner 1.19.0 */
+
+/*
+ * Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
+ * Copyright © 2020 Carlos Garnacho <carlosg@gnome.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+# define WL_PRIVATE __attribute__((visibility("hidden")))
+#else
+# define WL_PRIVATE
+#endif
+
+#pragma GCC visibility push(default)
+extern const struct wl_interface wl_seat_interface;
+extern const struct wl_interface wl_surface_interface;
+#pragma GCC visibility pop
+extern const struct wl_interface xdg_activation_token_v1_interface;
+
+static const struct wl_interface* xdg_activation_v1_types[] = {
+ NULL,
+ &xdg_activation_token_v1_interface,
+ NULL,
+ &wl_surface_interface,
+ NULL,
+ &wl_seat_interface,
+ &wl_surface_interface,
+};
+
+static const struct wl_message xdg_activation_v1_requests[] = {
+ {"destroy", "", xdg_activation_v1_types + 0},
+ {"get_activation_token", "n", xdg_activation_v1_types + 1},
+ {"activate", "so", xdg_activation_v1_types + 2},
+};
+
+WL_PRIVATE const struct wl_interface xdg_activation_v1_interface = {
+ "xdg_activation_v1", 1, 3, xdg_activation_v1_requests, 0, NULL,
+};
+
+static const struct wl_message xdg_activation_token_v1_requests[] = {
+ {"set_serial", "uo", xdg_activation_v1_types + 4},
+ {"set_app_id", "s", xdg_activation_v1_types + 0},
+ {"set_surface", "o", xdg_activation_v1_types + 6},
+ {"commit", "", xdg_activation_v1_types + 0},
+ {"destroy", "", xdg_activation_v1_types + 0},
+};
+
+static const struct wl_message xdg_activation_token_v1_events[] = {
+ {"done", "s", xdg_activation_v1_types + 0},
+};
+
+WL_PRIVATE const struct wl_interface xdg_activation_token_v1_interface = {
+ "xdg_activation_token_v1", 1, 5,
+ xdg_activation_token_v1_requests, 1, xdg_activation_token_v1_events,
+};
diff --git a/widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h b/widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h
new file mode 100644
index 0000000000..432057ccef
--- /dev/null
+++ b/widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h
@@ -0,0 +1,392 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+#ifndef XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H
+#define XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_xdg_output_unstable_v1 The xdg_output_unstable_v1 protocol
+ * Protocol to describe output regions
+ *
+ * @section page_desc_xdg_output_unstable_v1 Description
+ *
+ * This protocol aims at describing outputs in a way which is more in line
+ * with the concept of an output on desktop oriented systems.
+ *
+ * Some information are more specific to the concept of an output for
+ * a desktop oriented system and may not make sense in other applications,
+ * such as IVI systems for example.
+ *
+ * Typically, the global compositor space on a desktop system is made of
+ * a contiguous or overlapping set of rectangular regions.
+ *
+ * Some of the information provided in this protocol might be identical
+ * to their counterparts already available from wl_output, in which case
+ * the information provided by this protocol should be preferred to their
+ * equivalent in wl_output. The goal is to move the desktop specific
+ * concepts (such as output location within the global compositor space,
+ * the connector name and types, etc.) out of the core wl_output protocol.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible
+ * changes may be added together with the corresponding interface
+ * version bump.
+ * Backward incompatible changes are done by bumping the version
+ * number in the protocol and interface names and resetting the
+ * interface version. Once the protocol is to be declared stable,
+ * the 'z' prefix and the version number in the protocol and
+ * interface names are removed and the interface version number is
+ * reset.
+ *
+ * @section page_ifaces_xdg_output_unstable_v1 Interfaces
+ * - @subpage page_iface_zxdg_output_manager_v1 - manage xdg_output objects
+ * - @subpage page_iface_zxdg_output_v1 - compositor logical output region
+ * @section page_copyright_xdg_output_unstable_v1 Copyright
+ * <pre>
+ *
+ * Copyright © 2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </pre>
+ */
+struct wl_output;
+struct zxdg_output_manager_v1;
+struct zxdg_output_v1;
+
+/**
+ * @page page_iface_zxdg_output_manager_v1 zxdg_output_manager_v1
+ * @section page_iface_zxdg_output_manager_v1_desc Description
+ *
+ * A global factory interface for xdg_output objects.
+ * @section page_iface_zxdg_output_manager_v1_api API
+ * See @ref iface_zxdg_output_manager_v1.
+ */
+/**
+ * @defgroup iface_zxdg_output_manager_v1 The zxdg_output_manager_v1 interface
+ *
+ * A global factory interface for xdg_output objects.
+ */
+extern const struct wl_interface zxdg_output_manager_v1_interface;
+/**
+ * @page page_iface_zxdg_output_v1 zxdg_output_v1
+ * @section page_iface_zxdg_output_v1_desc Description
+ *
+ * An xdg_output describes part of the compositor geometry.
+ *
+ * This typically corresponds to a monitor that displays part of the
+ * compositor space.
+ *
+ * For objects version 3 onwards, after all xdg_output properties have been
+ * sent (when the object is created and when properties are updated), a
+ * wl_output.done event is sent. This allows changes to the output
+ * properties to be seen as atomic, even if they happen via multiple events.
+ * @section page_iface_zxdg_output_v1_api API
+ * See @ref iface_zxdg_output_v1.
+ */
+/**
+ * @defgroup iface_zxdg_output_v1 The zxdg_output_v1 interface
+ *
+ * An xdg_output describes part of the compositor geometry.
+ *
+ * This typically corresponds to a monitor that displays part of the
+ * compositor space.
+ *
+ * For objects version 3 onwards, after all xdg_output properties have been
+ * sent (when the object is created and when properties are updated), a
+ * wl_output.done event is sent. This allows changes to the output
+ * properties to be seen as atomic, even if they happen via multiple events.
+ */
+extern const struct wl_interface zxdg_output_v1_interface;
+
+#define ZXDG_OUTPUT_MANAGER_V1_DESTROY 0
+#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT 1
+
+/**
+ * @ingroup iface_zxdg_output_manager_v1
+ */
+#define ZXDG_OUTPUT_MANAGER_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zxdg_output_manager_v1
+ */
+#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT_SINCE_VERSION 1
+
+/** @ingroup iface_zxdg_output_manager_v1 */
+static inline void zxdg_output_manager_v1_set_user_data(
+ struct zxdg_output_manager_v1* zxdg_output_manager_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zxdg_output_manager_v1, user_data);
+}
+
+/** @ingroup iface_zxdg_output_manager_v1 */
+static inline void* zxdg_output_manager_v1_get_user_data(
+ struct zxdg_output_manager_v1* zxdg_output_manager_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zxdg_output_manager_v1);
+}
+
+static inline uint32_t zxdg_output_manager_v1_get_version(
+ struct zxdg_output_manager_v1* zxdg_output_manager_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zxdg_output_manager_v1);
+}
+
+/**
+ * @ingroup iface_zxdg_output_manager_v1
+ *
+ * Using this request a client can tell the server that it is not
+ * going to use the xdg_output_manager object anymore.
+ *
+ * Any objects already created through this instance are not affected.
+ */
+static inline void zxdg_output_manager_v1_destroy(
+ struct zxdg_output_manager_v1* zxdg_output_manager_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zxdg_output_manager_v1,
+ ZXDG_OUTPUT_MANAGER_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zxdg_output_manager_v1);
+}
+
+/**
+ * @ingroup iface_zxdg_output_manager_v1
+ *
+ * This creates a new xdg_output object for the given wl_output.
+ */
+static inline struct zxdg_output_v1* zxdg_output_manager_v1_get_xdg_output(
+ struct zxdg_output_manager_v1* zxdg_output_manager_v1,
+ struct wl_output* output) {
+ struct wl_proxy* id;
+
+ id = wl_proxy_marshal_constructor((struct wl_proxy*)zxdg_output_manager_v1,
+ ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT,
+ &zxdg_output_v1_interface, NULL, output);
+
+ return (struct zxdg_output_v1*)id;
+}
+
+/**
+ * @ingroup iface_zxdg_output_v1
+ * @struct zxdg_output_v1_listener
+ */
+struct zxdg_output_v1_listener {
+ /**
+ * position of the output within the global compositor space
+ *
+ * The position event describes the location of the wl_output
+ * within the global compositor space.
+ *
+ * The logical_position event is sent after creating an xdg_output
+ * (see xdg_output_manager.get_xdg_output) and whenever the
+ * location of the output changes within the global compositor
+ * space.
+ * @param x x position within the global compositor space
+ * @param y y position within the global compositor space
+ */
+ void (*logical_position)(void* data, struct zxdg_output_v1* zxdg_output_v1,
+ int32_t x, int32_t y);
+ /**
+ * size of the output in the global compositor space
+ *
+ * The logical_size event describes the size of the output in the
+ * global compositor space.
+ *
+ * For example, a surface without any buffer scale, transformation
+ * nor rotation set, with the size matching the logical_size will
+ * have the same size as the corresponding output when displayed.
+ *
+ * Most regular Wayland clients should not pay attention to the
+ * logical size and would rather rely on xdg_shell interfaces.
+ *
+ * Some clients such as Xwayland, however, need this to configure
+ * their surfaces in the global compositor space as the compositor
+ * may apply a different scale from what is advertised by the
+ * output scaling property (to achieve fractional scaling, for
+ * example).
+ *
+ * For example, for a wl_output mode 3840×2160 and a scale factor
+ * 2:
+ *
+ * - A compositor not scaling the surface buffers will advertise a
+ * logical size of 3840×2160,
+ *
+ * - A compositor automatically scaling the surface buffers will
+ * advertise a logical size of 1920×1080,
+ *
+ * - A compositor using a fractional scale of 1.5 will advertise a
+ * logical size to 2560×1620.
+ *
+ * For example, for a wl_output mode 1920×1080 and a 90 degree
+ * rotation, the compositor will advertise a logical size of
+ * 1080x1920.
+ *
+ * The logical_size event is sent after creating an xdg_output (see
+ * xdg_output_manager.get_xdg_output) and whenever the logical size
+ * of the output changes, either as a result of a change in the
+ * applied scale or because of a change in the corresponding output
+ * mode(see wl_output.mode) or transform (see wl_output.transform).
+ * @param width width in global compositor space
+ * @param height height in global compositor space
+ */
+ void (*logical_size)(void* data, struct zxdg_output_v1* zxdg_output_v1,
+ int32_t width, int32_t height);
+ /**
+ * all information about the output have been sent
+ *
+ * This event is sent after all other properties of an xdg_output
+ * have been sent.
+ *
+ * This allows changes to the xdg_output properties to be seen as
+ * atomic, even if they happen via multiple events.
+ *
+ * For objects version 3 onwards, this event is deprecated.
+ * Compositors are not required to send it anymore and must send
+ * wl_output.done instead.
+ */
+ void (*done)(void* data, struct zxdg_output_v1* zxdg_output_v1);
+ /**
+ * name of this output
+ *
+ * Many compositors will assign names to their outputs, show them
+ * to the user, allow them to be configured by name, etc. The
+ * client may wish to know this name as well to offer the user
+ * similar behaviors.
+ *
+ * The naming convention is compositor defined, but limited to
+ * alphanumeric characters and dashes (-). Each name is unique
+ * among all wl_output globals, but if a wl_output global is
+ * destroyed the same name may be reused later. The names will also
+ * remain consistent across sessions with the same hardware and
+ * software configuration.
+ *
+ * Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc.
+ * However, do not assume that the name is a reflection of an
+ * underlying DRM connector, X11 connection, etc.
+ *
+ * The name event is sent after creating an xdg_output (see
+ * xdg_output_manager.get_xdg_output). This event is only sent once
+ * per xdg_output, and the name does not change over the lifetime
+ * of the wl_output global.
+ * @param name output name
+ * @since 2
+ */
+ void (*name)(void* data, struct zxdg_output_v1* zxdg_output_v1,
+ const char* name);
+ /**
+ * human-readable description of this output
+ *
+ * Many compositors can produce human-readable descriptions of
+ * their outputs. The client may wish to know this description as
+ * well, to communicate the user for various purposes.
+ *
+ * The description is a UTF-8 string with no convention defined for
+ * its contents. Examples might include 'Foocorp 11" Display' or
+ * 'Virtual X11 output via :1'.
+ *
+ * The description event is sent after creating an xdg_output (see
+ * xdg_output_manager.get_xdg_output) and whenever the description
+ * changes. The description is optional, and may not be sent at
+ * all.
+ *
+ * For objects of version 2 and lower, this event is only sent once
+ * per xdg_output, and the description does not change over the
+ * lifetime of the wl_output global.
+ * @param description output description
+ * @since 2
+ */
+ void (*description)(void* data, struct zxdg_output_v1* zxdg_output_v1,
+ const char* description);
+};
+
+/**
+ * @ingroup iface_zxdg_output_v1
+ */
+static inline int zxdg_output_v1_add_listener(
+ struct zxdg_output_v1* zxdg_output_v1,
+ const struct zxdg_output_v1_listener* listener, void* data) {
+ return wl_proxy_add_listener((struct wl_proxy*)zxdg_output_v1,
+ (void (**)(void))listener, data);
+}
+
+#define ZXDG_OUTPUT_V1_DESTROY 0
+
+/**
+ * @ingroup iface_zxdg_output_v1
+ */
+#define ZXDG_OUTPUT_V1_LOGICAL_POSITION_SINCE_VERSION 1
+/**
+ * @ingroup iface_zxdg_output_v1
+ */
+#define ZXDG_OUTPUT_V1_LOGICAL_SIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zxdg_output_v1
+ */
+#define ZXDG_OUTPUT_V1_DONE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zxdg_output_v1
+ */
+#define ZXDG_OUTPUT_V1_NAME_SINCE_VERSION 2
+/**
+ * @ingroup iface_zxdg_output_v1
+ */
+#define ZXDG_OUTPUT_V1_DESCRIPTION_SINCE_VERSION 2
+
+/**
+ * @ingroup iface_zxdg_output_v1
+ */
+#define ZXDG_OUTPUT_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_zxdg_output_v1 */
+static inline void zxdg_output_v1_set_user_data(
+ struct zxdg_output_v1* zxdg_output_v1, void* user_data) {
+ wl_proxy_set_user_data((struct wl_proxy*)zxdg_output_v1, user_data);
+}
+
+/** @ingroup iface_zxdg_output_v1 */
+static inline void* zxdg_output_v1_get_user_data(
+ struct zxdg_output_v1* zxdg_output_v1) {
+ return wl_proxy_get_user_data((struct wl_proxy*)zxdg_output_v1);
+}
+
+static inline uint32_t zxdg_output_v1_get_version(
+ struct zxdg_output_v1* zxdg_output_v1) {
+ return wl_proxy_get_version((struct wl_proxy*)zxdg_output_v1);
+}
+
+/**
+ * @ingroup iface_zxdg_output_v1
+ *
+ * Using this request a client can tell the server that it is not
+ * going to use the xdg_output object anymore.
+ */
+static inline void zxdg_output_v1_destroy(
+ struct zxdg_output_v1* zxdg_output_v1) {
+ wl_proxy_marshal((struct wl_proxy*)zxdg_output_v1, ZXDG_OUTPUT_V1_DESTROY);
+
+ wl_proxy_destroy((struct wl_proxy*)zxdg_output_v1);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/widget/gtk/wayland/xdg-output-unstable-v1-protocol.c b/widget/gtk/wayland/xdg-output-unstable-v1-protocol.c
new file mode 100644
index 0000000000..f80133f357
--- /dev/null
+++ b/widget/gtk/wayland/xdg-output-unstable-v1-protocol.c
@@ -0,0 +1,74 @@
+/* Generated by wayland-scanner 1.18.0 */
+
+/*
+ * Copyright © 2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <gdk/gdkwayland.h>
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+# define WL_PRIVATE __attribute__((visibility("hidden")))
+#else
+# define WL_PRIVATE
+#endif
+
+extern const struct wl_interface wl_output_interface;
+extern const struct wl_interface zxdg_output_v1_interface;
+
+static const struct wl_interface* xdg_output_unstable_v1_types[] = {
+ NULL,
+ NULL,
+ &zxdg_output_v1_interface,
+ &wl_output_interface,
+};
+
+static const struct wl_message zxdg_output_manager_v1_requests[] = {
+ {"destroy", "", xdg_output_unstable_v1_types + 0},
+ {"get_xdg_output", "no", xdg_output_unstable_v1_types + 2},
+};
+
+WL_PRIVATE const struct wl_interface zxdg_output_manager_v1_interface = {
+ "zxdg_output_manager_v1", 3, 2, zxdg_output_manager_v1_requests, 0, NULL,
+};
+
+static const struct wl_message zxdg_output_v1_requests[] = {
+ {"destroy", "", xdg_output_unstable_v1_types + 0},
+};
+
+static const struct wl_message zxdg_output_v1_events[] = {
+ {"logical_position", "ii", xdg_output_unstable_v1_types + 0},
+ {"logical_size", "ii", xdg_output_unstable_v1_types + 0},
+ {"done", "", xdg_output_unstable_v1_types + 0},
+ {"name", "2s", xdg_output_unstable_v1_types + 0},
+ {"description", "2s", xdg_output_unstable_v1_types + 0},
+};
+
+WL_PRIVATE const struct wl_interface zxdg_output_v1_interface = {
+ "zxdg_output_v1", 3, 1, zxdg_output_v1_requests, 5, zxdg_output_v1_events,
+};
diff --git a/widget/headless/HeadlessClipboard.cpp b/widget/headless/HeadlessClipboard.cpp
new file mode 100644
index 0000000000..be419af523
--- /dev/null
+++ b/widget/headless/HeadlessClipboard.cpp
@@ -0,0 +1,154 @@
+/* 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 "HeadlessClipboard.h"
+
+#include "nsISupportsPrimitives.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla::widget {
+
+NS_IMPL_ISUPPORTS_INHERITED0(HeadlessClipboard, nsBaseClipboard)
+
+HeadlessClipboard::HeadlessClipboard()
+ : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(
+ true /* supportsSelectionClipboard */,
+ true /* supportsFindClipboard */,
+ true /* supportsSelectionCache */)) {
+ for (auto& clipboard : mClipboards) {
+ clipboard = MakeUnique<HeadlessClipboardData>();
+ }
+}
+
+NS_IMETHODIMP
+HeadlessClipboard::SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ // Clear out the clipboard in order to set the new data.
+ EmptyNativeClipboardData(aWhichClipboard);
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanExport(flavors);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ auto& clipboard = mClipboards[aWhichClipboard];
+ MOZ_ASSERT(clipboard);
+
+ for (const auto& flavor : flavors) {
+ if (!flavor.EqualsLiteral(kTextMime) && !flavor.EqualsLiteral(kHTMLMime)) {
+ continue;
+ }
+
+ nsCOMPtr<nsISupports> data;
+ rv = aTransferable->GetTransferData(flavor.get(), getter_AddRefs(data));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsCOMPtr<nsISupportsString> wideString = do_QueryInterface(data);
+ if (!wideString) {
+ continue;
+ }
+
+ nsAutoString utf16string;
+ wideString->GetData(utf16string);
+ flavor.EqualsLiteral(kTextMime) ? clipboard->SetText(utf16string)
+ : clipboard->SetHTML(utf16string);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessClipboard::GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto& clipboard = mClipboards[aWhichClipboard];
+ MOZ_ASSERT(clipboard);
+
+ for (const auto& flavor : flavors) {
+ if (!flavor.EqualsLiteral(kTextMime) && !flavor.EqualsLiteral(kHTMLMime)) {
+ continue;
+ }
+
+ bool isText = flavor.EqualsLiteral(kTextMime);
+ if (!(isText ? clipboard->HasText() : clipboard->HasHTML())) {
+ continue;
+ }
+
+ nsCOMPtr<nsISupportsString> dataWrapper =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv);
+ rv = dataWrapper->SetData(isText ? clipboard->GetText()
+ : clipboard->GetHTML());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ nsCOMPtr<nsISupports> genericDataWrapper = do_QueryInterface(dataWrapper);
+ rv = aTransferable->SetTransferData(flavor.get(), genericDataWrapper);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ // XXX Other platforms only fill the first available type, too.
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult HeadlessClipboard::EmptyNativeClipboardData(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+ auto& clipboard = mClipboards[aWhichClipboard];
+ MOZ_ASSERT(clipboard);
+ clipboard->Clear();
+ return NS_OK;
+}
+
+mozilla::Result<int32_t, nsresult>
+HeadlessClipboard::GetNativeClipboardSequenceNumber(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+ auto& clipboard = mClipboards[aWhichClipboard];
+ MOZ_ASSERT(clipboard);
+ return clipboard->GetChangeCount();
+ ;
+}
+
+mozilla::Result<bool, nsresult>
+HeadlessClipboard::HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ auto& clipboard = mClipboards[aWhichClipboard];
+ MOZ_ASSERT(clipboard);
+
+ // Retrieve the union of all aHasType in aFlavorList
+ for (auto& flavor : aFlavorList) {
+ if ((flavor.EqualsLiteral(kTextMime) && clipboard->HasText()) ||
+ (flavor.EqualsLiteral(kHTMLMime) && clipboard->HasHTML())) {
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/headless/HeadlessClipboard.h b/widget/headless/HeadlessClipboard.h
new file mode 100644
index 0000000000..697fc78f8b
--- /dev/null
+++ b/widget/headless/HeadlessClipboard.h
@@ -0,0 +1,45 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_HeadlessClipboard_h
+#define mozilla_widget_HeadlessClipboard_h
+
+#include "nsBaseClipboard.h"
+#include "nsIClipboard.h"
+#include "mozilla/UniquePtr.h"
+#include "HeadlessClipboardData.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessClipboard final : public nsBaseClipboard {
+ public:
+ HeadlessClipboard();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ protected:
+ ~HeadlessClipboard() = default;
+
+ // Implement the native clipboard behavior.
+ NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) override;
+ mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber(
+ int32_t aWhichClipboard) override;
+ mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override;
+
+ private:
+ UniquePtr<HeadlessClipboardData>
+ mClipboards[nsIClipboard::kClipboardTypeCount];
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/headless/HeadlessClipboardData.cpp b/widget/headless/HeadlessClipboardData.cpp
new file mode 100644
index 0000000000..8a10488403
--- /dev/null
+++ b/widget/headless/HeadlessClipboardData.cpp
@@ -0,0 +1,35 @@
+/* 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 "HeadlessClipboardData.h"
+
+namespace mozilla::widget {
+
+void HeadlessClipboardData::SetText(const nsAString& aText) {
+ mPlain = aText;
+ mChangeCount++;
+}
+
+bool HeadlessClipboardData::HasText() const { return !mPlain.IsVoid(); }
+
+const nsAString& HeadlessClipboardData::GetText() const { return mPlain; }
+
+void HeadlessClipboardData::SetHTML(const nsAString& aHTML) {
+ mHTML = aHTML;
+ mChangeCount++;
+}
+
+bool HeadlessClipboardData::HasHTML() const { return !mHTML.IsVoid(); }
+
+const nsAString& HeadlessClipboardData::GetHTML() const { return mHTML; }
+
+int32_t HeadlessClipboardData::GetChangeCount() const { return mChangeCount; }
+
+void HeadlessClipboardData::Clear() {
+ mPlain.SetIsVoid(true);
+ mHTML.SetIsVoid(true);
+ mChangeCount++;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/headless/HeadlessClipboardData.h b/widget/headless/HeadlessClipboardData.h
new file mode 100644
index 0000000000..6e9a0109e7
--- /dev/null
+++ b/widget/headless/HeadlessClipboardData.h
@@ -0,0 +1,44 @@
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessClipboardData_h
+#define mozilla_widget_HeadlessClipboardData_h
+
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessClipboardData final {
+ public:
+ HeadlessClipboardData() : mPlain(VoidString()), mHTML(VoidString()) {}
+ ~HeadlessClipboardData() = default;
+
+ // For text/plain
+ void SetText(const nsAString& aText);
+ bool HasText() const;
+ const nsAString& GetText() const;
+
+ // For text/html
+ void SetHTML(const nsAString& aHTML);
+ bool HasHTML() const;
+ const nsAString& GetHTML() const;
+
+ int32_t GetChangeCount() const;
+
+ // For other APIs
+ void Clear();
+
+ private:
+ nsString mPlain;
+ nsString mHTML;
+
+ int32_t mChangeCount = 0;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessClipboardData_h
diff --git a/widget/headless/HeadlessCompositorWidget.cpp b/widget/headless/HeadlessCompositorWidget.cpp
new file mode 100644
index 0000000000..bb4ee9175e
--- /dev/null
+++ b/widget/headless/HeadlessCompositorWidget.cpp
@@ -0,0 +1,46 @@
+/* -*- 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 "mozilla/widget/PlatformWidgetTypes.h"
+#include "HeadlessCompositorWidget.h"
+#include "VsyncDispatcher.h"
+
+namespace mozilla {
+namespace widget {
+
+HeadlessCompositorWidget::HeadlessCompositorWidget(
+ const HeadlessCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, HeadlessWidget* aWindow)
+ : CompositorWidget(aOptions),
+ mWidget(aWindow),
+ mClientSize(LayoutDeviceIntSize(aInitData.InitialClientSize()),
+ "HeadlessCompositorWidget::mClientSize") {}
+
+void HeadlessCompositorWidget::ObserveVsync(VsyncObserver* aObserver) {
+ if (RefPtr<CompositorVsyncDispatcher> cvd =
+ mWidget->GetCompositorVsyncDispatcher()) {
+ cvd->SetCompositorVsyncObserver(aObserver);
+ }
+}
+
+nsIWidget* HeadlessCompositorWidget::RealWidget() { return mWidget; }
+
+void HeadlessCompositorWidget::NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ auto size = mClientSize.Lock();
+ *size = aClientSize;
+}
+
+LayoutDeviceIntSize HeadlessCompositorWidget::GetClientSize() {
+ auto size = mClientSize.Lock();
+ return *size;
+}
+
+uintptr_t HeadlessCompositorWidget::GetWidgetKey() {
+ return reinterpret_cast<uintptr_t>(mWidget);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessCompositorWidget.h b/widget/headless/HeadlessCompositorWidget.h
new file mode 100644
index 0000000000..facd2bc65a
--- /dev/null
+++ b/widget/headless/HeadlessCompositorWidget.h
@@ -0,0 +1,54 @@
+/* -*- 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/. */
+
+#ifndef widget_headless_HeadlessCompositorWidget_h
+#define widget_headless_HeadlessCompositorWidget_h
+
+#include "mozilla/widget/CompositorWidget.h"
+
+#include "HeadlessWidget.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessCompositorWidgetInitData;
+
+class HeadlessCompositorWidget final : public CompositorWidget,
+ public CompositorWidgetDelegate {
+ public:
+ HeadlessCompositorWidget(const HeadlessCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions,
+ HeadlessWidget* aWindow);
+
+ void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize);
+
+ // CompositorWidget Overrides
+
+ uintptr_t GetWidgetKey() override;
+
+ LayoutDeviceIntSize GetClientSize() override;
+
+ nsIWidget* RealWidget() override;
+ CompositorWidgetDelegate* AsDelegate() override { return this; }
+
+ void ObserveVsync(VsyncObserver* aObserver) override;
+
+ // CompositorWidgetDelegate Overrides
+
+ HeadlessCompositorWidget* AsHeadlessCompositorWidget() override {
+ return this;
+ }
+
+ private:
+ HeadlessWidget* mWidget;
+
+ // See GtkCompositorWidget for the justification for this mutex.
+ DataMutex<LayoutDeviceIntSize> mClientSize;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_headless_HeadlessCompositor_h
diff --git a/widget/headless/HeadlessKeyBindings.cpp b/widget/headless/HeadlessKeyBindings.cpp
new file mode 100644
index 0000000000..13fcd4c6ca
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindings.cpp
@@ -0,0 +1,37 @@
+/* -*- 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 "HeadlessKeyBindings.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/WritingModes.h"
+
+namespace mozilla {
+namespace widget {
+
+HeadlessKeyBindings& HeadlessKeyBindings::GetInstance() {
+ static UniquePtr<HeadlessKeyBindings> sInstance;
+ if (!sInstance) {
+ sInstance.reset(new HeadlessKeyBindings());
+ ClearOnShutdown(&sInstance);
+ }
+ return *sInstance;
+}
+
+nsresult HeadlessKeyBindings::AttachNativeKeyEvent(
+ WidgetKeyboardEvent& aEvent) {
+ // Stub for non-mac platforms.
+ return NS_OK;
+}
+
+void HeadlessKeyBindings::GetEditCommands(
+ NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
+ // Stub for non-mac platforms.
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessKeyBindings.h b/widget/headless/HeadlessKeyBindings.h
new file mode 100644
index 0000000000..5eff30c4f5
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindings.h
@@ -0,0 +1,42 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_HeadlessKeyBindings_h
+#define mozilla_widget_HeadlessKeyBindings_h
+
+#include "mozilla/TextEvents.h"
+#include "nsIWidget.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
+namespace widget {
+
+/**
+ * Helper to emulate native key bindings. Currently only MacOS is supported.
+ */
+
+class HeadlessKeyBindings final {
+ public:
+ HeadlessKeyBindings() = default;
+
+ static HeadlessKeyBindings& GetInstance();
+
+ void GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+ [[nodiscard]] nsresult AttachNativeKeyEvent(WidgetKeyboardEvent& aEvent);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessKeyBindings_h
diff --git a/widget/headless/HeadlessKeyBindingsCocoa.mm b/widget/headless/HeadlessKeyBindingsCocoa.mm
new file mode 100644
index 0000000000..27ed07c461
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindingsCocoa.mm
@@ -0,0 +1,53 @@
+/* -*- 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 "HeadlessKeyBindings.h"
+#import <Cocoa/Cocoa.h>
+#include "nsCocoaUtils.h"
+#include "NativeKeyBindings.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/WritingModes.h"
+
+namespace mozilla {
+namespace widget {
+
+HeadlessKeyBindings& HeadlessKeyBindings::GetInstance() {
+ static UniquePtr<HeadlessKeyBindings> sInstance;
+ if (!sInstance) {
+ sInstance.reset(new HeadlessKeyBindings());
+ ClearOnShutdown(&sInstance);
+ }
+ return *sInstance;
+}
+
+nsresult HeadlessKeyBindings::AttachNativeKeyEvent(
+ WidgetKeyboardEvent& aEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ aEvent.mNativeKeyEvent =
+ nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aEvent, 0, nil);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void HeadlessKeyBindings::GetEditCommands(
+ NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
+ // Convert the widget keyboard into a cocoa event so it can be translated
+ // into commands in the NativeKeyBindings.
+ WidgetKeyboardEvent modifiedEvent(aEvent);
+ modifiedEvent.mNativeKeyEvent =
+ nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aEvent, 0, nil);
+
+ NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
+ keyBindings->GetEditCommands(modifiedEvent, aWritingMode, aCommands);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessLookAndFeel.h b/widget/headless/HeadlessLookAndFeel.h
new file mode 100644
index 0000000000..4e61b9e95d
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeel.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessLookAndFeel_h
+#define mozilla_widget_HeadlessLookAndFeel_h
+
+#include "nsXPLookAndFeel.h"
+#include "nsLookAndFeel.h"
+
+namespace mozilla {
+namespace widget {
+
+#if defined(MOZ_WIDGET_GTK)
+
+// Our nsLookAndFeel for Gtk relies on APIs that aren't available in headless
+// mode, so for processes that are unable to connect to a display server, we use
+// an implementation with hardcoded values.
+//
+// HeadlessLookAndFeel is used:
+//
+// * in the parent process, when full headless mode (MOZ_HEADLESS=1) is
+// enabled
+//
+// The result of this is that when headless content mode is enabled, content
+// processes use values derived from the parent's nsLookAndFeel (i.e., values
+// derived from Gtk APIs) while still refraining from making any display server
+// connections.
+
+class HeadlessLookAndFeel : public nsXPLookAndFeel {
+ public:
+ explicit HeadlessLookAndFeel();
+ virtual ~HeadlessLookAndFeel();
+
+ void NativeInit() final{};
+ nsresult NativeGetInt(IntID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID, float& aResult) override;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override;
+ bool NativeGetFont(FontID, nsString& aFontName, gfxFontStyle&) override;
+
+ char16_t GetPasswordCharacterImpl() override;
+};
+
+#else
+
+// When possible, we simply reuse the platform's existing nsLookAndFeel
+// implementation in headless mode.
+
+typedef nsLookAndFeel HeadlessLookAndFeel;
+
+#endif
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/headless/HeadlessLookAndFeelGTK.cpp b/widget/headless/HeadlessLookAndFeelGTK.cpp
new file mode 100644
index 0000000000..f8f6270cd7
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeelGTK.cpp
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "HeadlessLookAndFeel.h"
+#include "mozilla/FontPropertyTypes.h"
+#include "nsIContent.h"
+
+namespace mozilla::widget {
+
+static const char16_t UNICODE_BULLET = 0x2022;
+
+HeadlessLookAndFeel::HeadlessLookAndFeel() = default;
+
+HeadlessLookAndFeel::~HeadlessLookAndFeel() = default;
+
+nsresult HeadlessLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
+ nscolor& aResult) {
+ aResult = GetStandinForNativeColor(aID, aScheme);
+ return NS_OK;
+}
+
+nsresult HeadlessLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ nsresult res = NS_OK;
+ // These values should be sane defaults for headless mode under GTK.
+ switch (aID) {
+ case IntID::CaretBlinkTime:
+ aResult = 567;
+ break;
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+ case IntID::SelectTextfieldsOnKeyFocus:
+ aResult = 1;
+ break;
+ case IntID::SubmenuDelay:
+ aResult = 200;
+ break;
+ case IntID::MenusCanOverlapOSBar:
+ aResult = 0;
+ break;
+ case IntID::UseOverlayScrollbars:
+ aResult = 0;
+ break;
+ case IntID::AllowOverlayScrollbarsOverlap:
+ aResult = 0;
+ break;
+ case IntID::SkipNavigatingDisabledMenuItem:
+ aResult = 1;
+ break;
+ case IntID::DragThresholdX:
+ case IntID::DragThresholdY:
+ aResult = 4;
+ break;
+ case IntID::UseAccessibilityTheme:
+ aResult = 0;
+ break;
+ case IntID::ScrollArrowStyle:
+ aResult = eScrollArrow_None;
+ break;
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ return NS_OK;
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ aResult = 3;
+ return NS_OK;
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 3;
+ return NS_OK;
+ case IntID::TreeOpenDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeCloseDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeLazyScrollDelay:
+ aResult = 150;
+ break;
+ case IntID::TreeScrollDelay:
+ aResult = 100;
+ break;
+ case IntID::TreeScrollLinesMax:
+ aResult = 3;
+ break;
+ case IntID::TabFocusModel:
+ aResult = nsIContent::eTabFocus_textControlsMask;
+ break;
+ case IntID::ChosenMenuItemsShouldBlink:
+ aResult = 1;
+ break;
+ case IntID::WindowsAccentColorInTitlebar:
+ aResult = 0;
+ res = NS_ERROR_NOT_IMPLEMENTED;
+ break;
+ case IntID::AlertNotificationOrigin:
+ aResult = NS_ALERT_TOP;
+ break;
+ case IntID::ScrollToClick:
+ aResult = 0;
+ break;
+ case IntID::IMERawInputUnderlineStyle:
+ case IntID::IMESelectedRawTextUnderlineStyle:
+ case IntID::IMEConvertedTextUnderlineStyle:
+ case IntID::IMESelectedConvertedTextUnderline:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
+ break;
+ case IntID::MenuBarDrag:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarButtonAutoRepeatBehavior:
+ aResult = 0;
+ break;
+ case IntID::TooltipDelay:
+ aResult = 500;
+ break;
+ case IntID::SwipeAnimationEnabled:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarDisplayOnMouseMove:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarFadeBeginDelay:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarFadeDuration:
+ aResult = 0;
+ break;
+ case IntID::ContextMenuOffsetVertical:
+ aResult = -6;
+ break;
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 1;
+ break;
+ case IntID::GTKCSDAvailable:
+ aResult = 0;
+ break;
+ case IntID::GTKCSDMinimizeButton:
+ aResult = 0;
+ break;
+ case IntID::GTKCSDMaximizeButton:
+ aResult = 0;
+ break;
+ case IntID::GTKCSDCloseButton:
+ aResult = 1;
+ break;
+ case IntID::GTKCSDReversedPlacement:
+ aResult = 0;
+ break;
+ case IntID::SystemUsesDarkTheme:
+ aResult = 0;
+ break;
+ case IntID::PrefersReducedMotion:
+ case IntID::PrefersReducedTransparency:
+ aResult = 0;
+ break;
+ case IntID::InvertedColors:
+ aResult = 0;
+ break;
+ case IntID::PrimaryPointerCapabilities:
+ aResult = 0;
+ break;
+ case IntID::AllPointerCapabilities:
+ aResult = 0;
+ break;
+ default:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ break;
+ }
+ return res;
+}
+
+nsresult HeadlessLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ nsresult res = NS_OK;
+
+ // Hardcoded values for GTK.
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::CaretAspectRatio:
+ // Intentionally failing to quietly indicate lack of support.
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ break;
+ default:
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ break;
+ }
+
+ return res;
+}
+
+bool HeadlessLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ // Default to san-serif for everything.
+ aFontStyle.style = FontSlantStyle::NORMAL;
+ aFontStyle.weight = FontWeight::NORMAL;
+ aFontStyle.stretch = FontStretch::NORMAL;
+ aFontStyle.size = 14;
+ aFontStyle.systemFont = true;
+
+ aFontName.AssignLiteral("sans-serif");
+ return true;
+}
+
+char16_t HeadlessLookAndFeel::GetPasswordCharacterImpl() {
+ return UNICODE_BULLET;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/headless/HeadlessScreenHelper.cpp b/widget/headless/HeadlessScreenHelper.cpp
new file mode 100644
index 0000000000..4d8dbfa0d1
--- /dev/null
+++ b/widget/headless/HeadlessScreenHelper.cpp
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "HeadlessScreenHelper.h"
+
+#include "prenv.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/RefPtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace widget {
+
+/* static */
+LayoutDeviceIntRect HeadlessScreenHelper::GetScreenRect() {
+ char* ev = PR_GetEnv("MOZ_HEADLESS_WIDTH");
+ int width = 1366;
+ if (ev) {
+ width = atoi(ev);
+ }
+ ev = PR_GetEnv("MOZ_HEADLESS_HEIGHT");
+ int height = 768;
+ if (ev) {
+ height = atoi(ev);
+ }
+ return LayoutDeviceIntRect(0, 0, width, height);
+}
+
+HeadlessScreenHelper::HeadlessScreenHelper() {
+ AutoTArray<RefPtr<Screen>, 1> screenList;
+ LayoutDeviceIntRect rect = GetScreenRect();
+ auto ret = MakeRefPtr<Screen>(
+ rect, rect, 24, 24, 0, DesktopToLayoutDeviceScale(),
+ CSSToLayoutDeviceScale(), 96.0f, Screen::IsPseudoDisplay::No);
+ screenList.AppendElement(ret.forget());
+ ScreenManager::Refresh(std::move(screenList));
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessScreenHelper.h b/widget/headless/HeadlessScreenHelper.h
new file mode 100644
index 0000000000..f86677af52
--- /dev/null
+++ b/widget/headless/HeadlessScreenHelper.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessScreenHelper_h
+#define mozilla_widget_HeadlessScreenHelper_h
+
+#include "mozilla/widget/ScreenManager.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessScreenHelper final : public ScreenManager::Helper {
+ public:
+ HeadlessScreenHelper();
+ ~HeadlessScreenHelper() override = default;
+
+ private:
+ static LayoutDeviceIntRect GetScreenRect();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessScreenHelper_h
diff --git a/widget/headless/HeadlessSound.cpp b/widget/headless/HeadlessSound.cpp
new file mode 100644
index 0000000000..278d31b355
--- /dev/null
+++ b/widget/headless/HeadlessSound.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "HeadlessSound.h"
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(HeadlessSound, nsISound, nsIStreamLoaderObserver)
+
+HeadlessSound::HeadlessSound() = default;
+
+HeadlessSound::~HeadlessSound() = default;
+
+NS_IMETHODIMP
+HeadlessSound::Init() { return NS_OK; }
+
+NS_IMETHODIMP HeadlessSound::OnStreamComplete(nsIStreamLoader* aLoader,
+ nsISupports* context,
+ nsresult aStatus,
+ uint32_t dataLen,
+ const uint8_t* data) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP HeadlessSound::Beep() { return NS_OK; }
+
+NS_IMETHODIMP HeadlessSound::Play(nsIURL* aURL) { return NS_OK; }
+
+NS_IMETHODIMP HeadlessSound::PlayEventSound(uint32_t aEventId) { return NS_OK; }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessSound.h b/widget/headless/HeadlessSound.h
new file mode 100644
index 0000000000..a481acc61e
--- /dev/null
+++ b/widget/headless/HeadlessSound.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessSound_h
+#define mozilla_widget_HeadlessSound_h
+
+#include "nsISound.h"
+#include "nsIStreamLoader.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessSound : public nsISound, public nsIStreamLoaderObserver {
+ public:
+ HeadlessSound();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOUND
+ NS_DECL_NSISTREAMLOADEROBSERVER
+
+ private:
+ virtual ~HeadlessSound();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessSound_h
diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp
new file mode 100644
index 0000000000..083d026d3c
--- /dev/null
+++ b/widget/headless/HeadlessWidget.cpp
@@ -0,0 +1,625 @@
+/* -*- 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 "HeadlessWidget.h"
+#include "ErrorList.h"
+#include "HeadlessCompositorWidget.h"
+#include "BasicEvents.h"
+#include "MouseEvents.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/widget/HeadlessWidgetTypes.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "mozilla/widget/Screen.h"
+#include "nsIScreen.h"
+#include "HeadlessKeyBindings.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+
+using mozilla::LogLevel;
+
+#ifdef MOZ_LOGGING
+
+# include "mozilla/Logging.h"
+static mozilla::LazyLogModule sWidgetLog("Widget");
+static mozilla::LazyLogModule sWidgetFocusLog("WidgetFocus");
+# define LOG(args) MOZ_LOG(sWidgetLog, mozilla::LogLevel::Debug, args)
+# define LOGFOCUS(args) \
+ MOZ_LOG(sWidgetFocusLog, mozilla::LogLevel::Debug, args)
+
+#else
+
+# define LOG(args)
+# define LOGFOCUS(args)
+
+#endif /* MOZ_LOGGING */
+
+/*static*/
+already_AddRefed<nsIWidget> nsIWidget::CreateHeadlessWidget() {
+ nsCOMPtr<nsIWidget> widget = new mozilla::widget::HeadlessWidget();
+ return widget.forget();
+}
+
+namespace mozilla {
+namespace widget {
+
+StaticAutoPtr<nsTArray<HeadlessWidget*>> HeadlessWidget::sActiveWindows;
+
+already_AddRefed<HeadlessWidget> HeadlessWidget::GetActiveWindow() {
+ if (!sActiveWindows) {
+ return nullptr;
+ }
+ auto length = sActiveWindows->Length();
+ if (length == 0) {
+ return nullptr;
+ }
+ RefPtr<HeadlessWidget> widget = sActiveWindows->ElementAt(length - 1);
+ return widget.forget();
+}
+
+HeadlessWidget::HeadlessWidget()
+ : mEnabled(true),
+ mVisible(false),
+ mDestroyed(false),
+ mAlwaysOnTop(false),
+ mTopLevel(nullptr),
+ mCompositorWidget(nullptr),
+ mSizeMode(nsSizeMode_Normal),
+ mLastSizeMode(nsSizeMode_Normal),
+ mEffectiveSizeMode(nsSizeMode_Normal),
+ mRestoreBounds(0, 0, 0, 0) {
+ if (!sActiveWindows) {
+ sActiveWindows = new nsTArray<HeadlessWidget*>();
+ ClearOnShutdown(&sActiveWindows);
+ }
+}
+
+HeadlessWidget::~HeadlessWidget() {
+ LOG(("HeadlessWidget::~HeadlessWidget() [%p]\n", (void*)this));
+
+ Destroy();
+}
+
+void HeadlessWidget::Destroy() {
+ if (mDestroyed) {
+ return;
+ }
+ LOG(("HeadlessWidget::Destroy [%p]\n", (void*)this));
+ mDestroyed = true;
+
+ if (sActiveWindows) {
+ int32_t index = sActiveWindows->IndexOf(this);
+ if (index != -1) {
+ RefPtr<HeadlessWidget> activeWindow = GetActiveWindow();
+ sActiveWindows->RemoveElementAt(index);
+ // If this is the currently active widget and there's a previously active
+ // widget, activate the previous widget.
+ RefPtr<HeadlessWidget> previousActiveWindow = GetActiveWindow();
+ if (this == activeWindow && previousActiveWindow &&
+ previousActiveWindow->mWidgetListener) {
+ previousActiveWindow->mWidgetListener->WindowActivated();
+ }
+ }
+ }
+
+ nsBaseWidget::OnDestroy();
+
+ nsBaseWidget::Destroy();
+}
+
+nsresult HeadlessWidget::Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ MOZ_ASSERT(!aNativeParent, "No native parents for headless widgets.");
+
+ BaseCreate(nullptr, aInitData);
+
+ mBounds = aRect;
+ mRestoreBounds = aRect;
+
+ mAlwaysOnTop = aInitData && aInitData->mAlwaysOnTop;
+
+ if (aParent) {
+ mTopLevel = aParent->GetTopLevelWidget();
+ } else {
+ mTopLevel = this;
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<nsIWidget> HeadlessWidget::CreateChild(
+ const LayoutDeviceIntRect& aRect, widget::InitData* aInitData,
+ bool aForceUseIWidgetParent) {
+ nsCOMPtr<nsIWidget> widget = nsIWidget::CreateHeadlessWidget();
+ if (!widget) {
+ return nullptr;
+ }
+ if (NS_FAILED(widget->Create(this, nullptr, aRect, aInitData))) {
+ return nullptr;
+ }
+ return widget.forget();
+}
+
+void HeadlessWidget::GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) {
+ *aInitData =
+ mozilla::widget::HeadlessCompositorWidgetInitData(GetClientSize());
+}
+
+nsIWidget* HeadlessWidget::GetTopLevelWidget() { return mTopLevel; }
+
+void HeadlessWidget::RaiseWindow() {
+ MOZ_ASSERT(mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog ||
+ mWindowType == WindowType::Sheet,
+ "Raising a non-toplevel window.");
+
+ // Do nothing if this is the currently active window.
+ RefPtr<HeadlessWidget> activeWindow = GetActiveWindow();
+ if (activeWindow == this) {
+ return;
+ }
+
+ // Raise the window to the top of the stack.
+ nsWindowZ placement = nsWindowZTop;
+ nsCOMPtr<nsIWidget> actualBelow;
+ if (mWidgetListener)
+ mWidgetListener->ZLevelChanged(true, &placement, nullptr,
+ getter_AddRefs(actualBelow));
+
+ // Deactivate the last active window.
+ if (activeWindow && activeWindow->mWidgetListener) {
+ activeWindow->mWidgetListener->WindowDeactivated();
+ }
+
+ // Remove this window if it's already tracked.
+ int32_t index = sActiveWindows->IndexOf(this);
+ if (index != -1) {
+ sActiveWindows->RemoveElementAt(index);
+ }
+
+ // Activate this window.
+ sActiveWindows->AppendElement(this);
+ if (mWidgetListener) mWidgetListener->WindowActivated();
+}
+
+void HeadlessWidget::Show(bool aState) {
+ mVisible = aState;
+
+ LOG(("HeadlessWidget::Show [%p] state %d\n", (void*)this, aState));
+
+ // Top-level window and dialogs are activated/raised when shown.
+ // NB: alwaysontop windows are generally used for peripheral indicators,
+ // so we don't focus them by default.
+ if (aState && !mAlwaysOnTop &&
+ (mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog || mWindowType == WindowType::Sheet)) {
+ RaiseWindow();
+ }
+
+ ApplySizeModeSideEffects();
+}
+
+bool HeadlessWidget::IsVisible() const { return mVisible; }
+
+void HeadlessWidget::SetFocus(Raise aRaise,
+ mozilla::dom::CallerType aCallerType) {
+ LOGFOCUS((" SetFocus %d [%p]\n", aRaise == Raise::Yes, (void*)this));
+
+ // This means we request activation of our toplevel window.
+ if (aRaise == Raise::Yes) {
+ HeadlessWidget* topLevel = (HeadlessWidget*)GetTopLevelWidget();
+
+ // The toplevel only becomes active if it's currently visible; otherwise, it
+ // will be activated anyway when it's shown.
+ if (topLevel->IsVisible()) topLevel->RaiseWindow();
+ }
+}
+
+void HeadlessWidget::Enable(bool aState) { mEnabled = aState; }
+
+bool HeadlessWidget::IsEnabled() const { return mEnabled; }
+
+void HeadlessWidget::Move(double aX, double aY) {
+ LOG(("HeadlessWidget::Move [%p] %f %f\n", (void*)this, aX, aY));
+
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ int32_t x = NSToIntRound(aX * scale);
+ int32_t y = NSToIntRound(aY * scale);
+
+ if (mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog) {
+ SetSizeMode(nsSizeMode_Normal);
+ }
+
+ MoveInternal(x, y);
+}
+
+void HeadlessWidget::MoveInternal(int32_t aX, int32_t aY) {
+ // Since a popup window's x/y coordinates are in relation to
+ // the parent, the parent might have moved so we always move a
+ // popup window.
+ if (mBounds.IsEqualXY(aX, aY) && mWindowType != WindowType::Popup) {
+ return;
+ }
+
+ mBounds.MoveTo(aX, aY);
+ NotifyWindowMoved(aX, aY);
+}
+
+LayoutDeviceIntPoint HeadlessWidget::WidgetToScreenOffset() {
+ return mTopLevel->GetBounds().TopLeft();
+}
+
+WindowRenderer* HeadlessWidget::GetWindowRenderer() {
+ return nsBaseWidget::GetWindowRenderer();
+}
+
+void HeadlessWidget::SetCompositorWidgetDelegate(
+ CompositorWidgetDelegate* delegate) {
+ if (delegate) {
+ mCompositorWidget = delegate->AsHeadlessCompositorWidget();
+ MOZ_ASSERT(mCompositorWidget,
+ "HeadlessWidget::SetCompositorWidgetDelegate called with a "
+ "non-HeadlessCompositorWidget");
+ } else {
+ mCompositorWidget = nullptr;
+ }
+}
+
+void HeadlessWidget::Resize(double aWidth, double aHeight, bool aRepaint) {
+ int32_t width = NSToIntRound(aWidth);
+ int32_t height = NSToIntRound(aHeight);
+ ResizeInternal(width, height, aRepaint);
+}
+
+void HeadlessWidget::ResizeInternal(int32_t aWidth, int32_t aHeight,
+ bool aRepaint) {
+ ConstrainSize(&aWidth, &aHeight);
+ mBounds.SizeTo(LayoutDeviceIntSize(aWidth, aHeight));
+
+ if (mCompositorWidget) {
+ mCompositorWidget->NotifyClientSizeChanged(
+ LayoutDeviceIntSize(mBounds.Width(), mBounds.Height()));
+ }
+ if (mWidgetListener) {
+ mWidgetListener->WindowResized(this, mBounds.Width(), mBounds.Height());
+ }
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->WindowResized(this, mBounds.Width(),
+ mBounds.Height());
+ }
+}
+
+void HeadlessWidget::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ MoveInternal(NSToIntRound(aX), NSToIntRound(aY));
+ Resize(aWidth, aHeight, aRepaint);
+}
+
+void HeadlessWidget::SetSizeMode(nsSizeMode aMode) {
+ LOG(("HeadlessWidget::SetSizeMode [%p] %d\n", (void*)this, aMode));
+
+ if (aMode == mSizeMode) {
+ return;
+ }
+
+ if (aMode == nsSizeMode_Normal && mSizeMode == nsSizeMode_Fullscreen) {
+ MakeFullScreen(false);
+ return;
+ }
+
+ mSizeMode = aMode;
+
+ // Normally in real widget backends a window event would be triggered that
+ // would cause the window manager to handle resizing the window. In headless
+ // the window must manually be resized.
+ ApplySizeModeSideEffects();
+}
+
+void HeadlessWidget::ApplySizeModeSideEffects() {
+ if (!mVisible || mEffectiveSizeMode == mSizeMode) {
+ return;
+ }
+
+ if (mEffectiveSizeMode == nsSizeMode_Normal) {
+ // Store the last normal size bounds so it can be restored when entering
+ // normal mode again.
+ mRestoreBounds = mBounds;
+ }
+
+ switch (mSizeMode) {
+ case nsSizeMode_Normal: {
+ MoveInternal(mRestoreBounds.X(), mRestoreBounds.Y());
+ ResizeInternal(mRestoreBounds.Width(), mRestoreBounds.Height(), false);
+ break;
+ }
+ case nsSizeMode_Minimized:
+ break;
+ case nsSizeMode_Maximized: {
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ int32_t left, top, width, height;
+ if (NS_SUCCEEDED(
+ screen->GetRectDisplayPix(&left, &top, &width, &height))) {
+ MoveInternal(0, 0);
+ ResizeInternal(width, height, true);
+ }
+ }
+ break;
+ }
+ case nsSizeMode_Fullscreen:
+ // This will take care of resizing the window.
+ nsBaseWidget::InfallibleMakeFullScreen(true);
+ break;
+ default:
+ break;
+ }
+
+ mEffectiveSizeMode = mSizeMode;
+ if (mWidgetListener) {
+ mWidgetListener->SizeModeChanged(mSizeMode);
+ }
+}
+
+nsresult HeadlessWidget::MakeFullScreen(bool aFullScreen) {
+ // Directly update the size mode here so a later call SetSizeMode does
+ // nothing.
+ if (aFullScreen) {
+ if (mSizeMode != nsSizeMode_Fullscreen) {
+ mLastSizeMode = mSizeMode;
+ }
+ mSizeMode = nsSizeMode_Fullscreen;
+ } else {
+ mSizeMode = mLastSizeMode;
+ }
+
+ // Notify the listener first so size mode change events are triggered before
+ // resize events.
+ if (mWidgetListener) {
+ mWidgetListener->SizeModeChanged(mSizeMode);
+ }
+
+ // Real widget backends don't seem to follow a common approach for
+ // when and how many resize events are triggered during fullscreen
+ // transitions. InfallibleMakeFullScreen will trigger a resize, but it
+ // will be ignored if still transitioning to fullscreen, so it must be
+ // triggered on the next tick.
+ RefPtr<HeadlessWidget> self(this);
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(
+ "HeadlessWidget::MakeFullScreen", [self, aFullScreen]() -> void {
+ self->InfallibleMakeFullScreen(aFullScreen);
+ }));
+
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::AttachNativeKeyEvent(WidgetKeyboardEvent& aEvent) {
+ HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance();
+ return bindings.AttachNativeKeyEvent(aEvent);
+}
+
+bool HeadlessWidget::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+
+ Maybe<WritingMode> writingMode;
+ if (aEvent.NeedsToRemapNavigationKey()) {
+ if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+ writingMode = dispatcher->MaybeQueryWritingModeAtSelection();
+ }
+ }
+
+ HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance();
+ bindings.GetEditCommands(aType, aEvent, writingMode, aCommands);
+ return true;
+}
+
+nsresult HeadlessWidget::DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) {
+#ifdef DEBUG
+ debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "HeadlessWidget", 0);
+#endif
+
+ aStatus = nsEventStatus_eIgnore;
+
+ if (mAttachedWidgetListener) {
+ aStatus = mAttachedWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+ } else if (mWidgetListener) {
+ aStatus = mWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+ }
+
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+ EventMessage msg;
+ switch (aNativeMessage) {
+ case NativeMouseMessage::Move:
+ msg = eMouseMove;
+ break;
+ case NativeMouseMessage::ButtonDown:
+ msg = eMouseDown;
+ break;
+ case NativeMouseMessage::ButtonUp:
+ msg = eMouseUp;
+ break;
+ case NativeMouseMessage::EnterWindow:
+ case NativeMouseMessage::LeaveWindow:
+ MOZ_ASSERT_UNREACHABLE("Unsupported synthesized mouse event");
+ return NS_ERROR_UNEXPECTED;
+ }
+ WidgetMouseEvent event(true, msg, this, WidgetMouseEvent::eReal);
+ event.mRefPoint = aPoint - WidgetToScreenOffset();
+ if (msg == eMouseDown || msg == eMouseUp) {
+ event.mButton = aButton;
+ }
+ if (msg == eMouseDown) {
+ event.mClickCount = 1;
+ }
+ event.AssignEventTime(WidgetEventTime());
+ DispatchInputEvent(&event);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeMouseScrollEvent(
+ mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage,
+ double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+ printf(">>> DEBUG_ME: Synth: aDeltaY=%f\n", aDeltaY);
+ // The various platforms seem to handle scrolling deltas differently,
+ // but the following seems to emulate it well enough.
+ WidgetWheelEvent event(true, eWheel, this);
+ event.mDeltaMode = MOZ_HEADLESS_SCROLL_DELTA_MODE;
+ event.mIsNoLineOrPageDelta = true;
+ event.mDeltaX = -aDeltaX * MOZ_HEADLESS_SCROLL_MULTIPLIER;
+ event.mDeltaY = -aDeltaY * MOZ_HEADLESS_SCROLL_MULTIPLIER;
+ event.mDeltaZ = -aDeltaZ * MOZ_HEADLESS_SCROLL_MULTIPLIER;
+ event.mRefPoint = aPoint - WidgetToScreenOffset();
+ event.AssignEventTime(WidgetEventTime());
+ DispatchInputEvent(&event);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeTouchPoint(
+ uint32_t aPointerId, TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPointerPressure,
+ uint32_t aPointerOrientation, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpoint");
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aPointerState == TOUCH_HOVER) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!mSynthesizedTouchInput) {
+ mSynthesizedTouchInput = MakeUnique<MultiTouchInput>();
+ }
+
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState(
+ mSynthesizedTouchInput.get(), TimeStamp::Now(), aPointerId, aPointerState,
+ pointInWindow, aPointerPressure, aPointerOrientation);
+ DispatchTouchInput(inputToDispatch);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint,
+ int32_t aModifierFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ PinchGestureInput::PinchGestureType pinchGestureType =
+ PinchGestureInput::PINCHGESTURE_SCALE;
+ ScreenCoord CurrentSpan;
+ ScreenCoord PreviousSpan;
+ switch (aEventPhase) {
+ case PHASE_BEGIN:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_START;
+ CurrentSpan = aScale;
+ PreviousSpan = 0.999;
+ break;
+
+ case PHASE_UPDATE:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
+ if (aScale == mLastPinchSpan) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ CurrentSpan = aScale;
+ PreviousSpan = mLastPinchSpan;
+ break;
+
+ case PHASE_END:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_END;
+ CurrentSpan = aScale;
+ PreviousSpan = mLastPinchSpan;
+ break;
+
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ScreenPoint touchpadPoint = ViewAs<ScreenPixel>(
+ aPoint - WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+ // The headless widget does not support modifiers.
+ // Do not pass `aModifierFlags` because it contains native modifier values.
+ PinchGestureInput inputToDispatch(
+ pinchGestureType, PinchGestureInput::TRACKPAD, TimeStamp::Now(),
+ ExternalPoint(0, 0), touchpadPoint,
+ 100.0 * ((aEventPhase == PHASE_END) ? ScreenCoord(1.f) : CurrentSpan),
+ 100.0 * ((aEventPhase == PHASE_END) ? ScreenCoord(1.f) : PreviousSpan),
+ 0);
+
+ if (!inputToDispatch.SetLineOrPageDeltaY(this)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mLastPinchSpan = aScale;
+ DispatchPinchGestureInput(inputToDispatch);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeTouchpadPan(
+ TouchpadGesturePhase aEventPhase, LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY, int32_t aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpadpanevent");
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ PanGestureInput::PanGestureType eventType = PanGestureInput::PANGESTURE_PAN;
+ switch (aEventPhase) {
+ case PHASE_BEGIN:
+ eventType = PanGestureInput::PANGESTURE_START;
+ break;
+ case PHASE_UPDATE:
+ eventType = PanGestureInput::PANGESTURE_PAN;
+ break;
+ case PHASE_END:
+ eventType = PanGestureInput::PANGESTURE_END;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ScreenPoint touchpadPoint = ViewAs<ScreenPixel>(
+ aPoint - WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+ PanGestureInput input(eventType, TimeStamp::Now(), touchpadPoint,
+ ScreenPoint(float(aDeltaX), float(aDeltaY)),
+ // Same as SynthesizeNativeTouchPadPinch case we ignore
+ // aModifierFlags.
+ 0);
+
+ input.mSimulateMomentum =
+ Preferences::GetBool("apz.test.headless.simulate_momentum");
+
+ DispatchPanGestureInput(input);
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessWidget.h b/widget/headless/HeadlessWidget.h
new file mode 100644
index 0000000000..9856991ef3
--- /dev/null
+++ b/widget/headless/HeadlessWidget.h
@@ -0,0 +1,182 @@
+/* -*- 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/. */
+
+#ifndef HEADLESSWIDGET_H
+#define HEADLESSWIDGET_H
+
+#include "mozilla/widget/InProcessCompositorWidget.h"
+#include "nsBaseWidget.h"
+#include "CompositorWidget.h"
+#include "mozilla/dom/WheelEventBinding.h"
+
+// The various synthesized event values are hardcoded to avoid pulling
+// in the platform specific widget code.
+#if defined(MOZ_WIDGET_GTK)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER 3
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_LINE
+#elif defined(XP_WIN)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER \
+ .025 // default scroll lines (3) / WHEEL_DELTA (120)
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_LINE
+#elif defined(XP_MACOSX)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER 1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_PIXEL
+#elif defined(ANDROID)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER 1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_LINE
+#else
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER -1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE -1
+#endif
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+namespace widget {
+
+class HeadlessWidget : public nsBaseWidget {
+ public:
+ HeadlessWidget();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HeadlessWidget, nsBaseWidget)
+
+ void* GetNativeData(uint32_t aDataType) override {
+ // Headless widgets have no native data.
+ return nullptr;
+ }
+
+ virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData = nullptr) override;
+ using nsBaseWidget::Create; // for Create signature not overridden here
+ virtual already_AddRefed<nsIWidget> CreateChild(
+ const LayoutDeviceIntRect& aRect, widget::InitData* aInitData = nullptr,
+ bool aForceUseIWidgetParent = false) override;
+
+ virtual nsIWidget* GetTopLevelWidget() override;
+
+ virtual void GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) override;
+
+ virtual void Destroy() override;
+ virtual void Show(bool aState) override;
+ virtual bool IsVisible() const override;
+ virtual void Move(double aX, double aY) override;
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override;
+ virtual nsresult MakeFullScreen(bool aFullScreen) override;
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override {
+ // TODO: see if we need to do anything here.
+ }
+ virtual nsresult SetTitle(const nsAString& title) override {
+ // Headless widgets have no title, so just ignore it.
+ return NS_OK;
+ }
+ virtual nsresult SetNonClientMargins(
+ const LayoutDeviceIntMargin& margins) override {
+ // Headless widgets have no chrome margins, so just ignore the call.
+ return NS_OK;
+ }
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override {
+ mInputContext = aContext;
+ }
+ virtual InputContext GetInputContext() override { return mInputContext; }
+
+ virtual WindowRenderer* GetWindowRenderer() override;
+
+ void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override;
+
+ [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
+ WidgetKeyboardEvent& aEvent) override;
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) override;
+
+ virtual nsresult DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+
+ virtual nsresult SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ return SynthesizeNativeMouseEvent(
+ aPoint, NativeMouseMessage::Move, mozilla::MouseButton::eNotPressed,
+ nsIWidget::Modifiers::NO_MODIFIERS, aObserver);
+ };
+
+ virtual nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale,
+ LayoutDeviceIntPoint aPoint, int32_t aModifierFlags) override;
+
+ virtual nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ private:
+ ~HeadlessWidget();
+ bool mEnabled;
+ bool mVisible;
+ bool mDestroyed;
+ bool mAlwaysOnTop;
+ nsIWidget* mTopLevel;
+ HeadlessCompositorWidget* mCompositorWidget;
+ nsSizeMode mSizeMode;
+ // The size mode before entering fullscreen mode.
+ nsSizeMode mLastSizeMode;
+ // The last size mode set while the window was visible.
+ nsSizeMode mEffectiveSizeMode;
+ mozilla::ScreenCoord mLastPinchSpan;
+ InputContext mInputContext;
+ mozilla::UniquePtr<mozilla::MultiTouchInput> mSynthesizedTouchInput;
+ // In headless there is no window manager to track window bounds
+ // across size mode changes, so we must track it to emulate.
+ LayoutDeviceIntRect mRestoreBounds;
+ void ApplySizeModeSideEffects();
+ // Move while maintaining size mode.
+ void MoveInternal(int32_t aX, int32_t aY);
+ // Resize while maintaining size mode.
+ void ResizeInternal(int32_t aWidth, int32_t aHeight, bool aRepaint);
+ // Similarly, we must track the active window ourselves in order
+ // to dispatch (de)activation events properly.
+ void RaiseWindow();
+ // The top level widgets are tracked for window ordering. They are
+ // stored in order of activation where the last element is always the
+ // currently active widget.
+ static StaticAutoPtr<nsTArray<HeadlessWidget*>> sActiveWindows;
+ // Get the most recently activated widget or null if there are none.
+ static already_AddRefed<HeadlessWidget> GetActiveWindow();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/headless/HeadlessWidgetTypes.ipdlh b/widget/headless/HeadlessWidgetTypes.ipdlh
new file mode 100644
index 0000000000..ac7e0e6142
--- /dev/null
+++ b/widget/headless/HeadlessWidgetTypes.ipdlh
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 "mozilla/GfxMessageUtils.h";
+
+using mozilla::LayoutDeviceIntSize from "Units.h";
+
+namespace mozilla {
+namespace widget {
+
+struct HeadlessCompositorWidgetInitData
+{
+ LayoutDeviceIntSize InitialClientSize;
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/moz.build b/widget/headless/moz.build
new file mode 100644
index 0000000000..a46c3a10e3
--- /dev/null
+++ b/widget/headless/moz.build
@@ -0,0 +1,48 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Headless")
+
+DIRS += ["tests"]
+
+LOCAL_INCLUDES += [
+ "/widget",
+ "/widget/headless",
+]
+
+widget_dir = CONFIG["MOZ_WIDGET_TOOLKIT"]
+
+LOCAL_INCLUDES += [
+ "/widget/%s" % widget_dir,
+]
+
+UNIFIED_SOURCES += [
+ "HeadlessClipboard.cpp",
+ "HeadlessClipboardData.cpp",
+ "HeadlessCompositorWidget.cpp",
+ "HeadlessScreenHelper.cpp",
+ "HeadlessSound.cpp",
+ "HeadlessWidget.cpp",
+]
+
+if widget_dir == "gtk":
+ UNIFIED_SOURCES += [
+ "HeadlessLookAndFeelGTK.cpp",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ UNIFIED_SOURCES += [
+ "HeadlessKeyBindingsCocoa.mm",
+ ]
+else:
+ UNIFIED_SOURCES += [
+ "HeadlessKeyBindings.cpp",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/widget/headless/tests/headless.html b/widget/headless/tests/headless.html
new file mode 100644
index 0000000000..bbde895077
--- /dev/null
+++ b/widget/headless/tests/headless.html
@@ -0,0 +1,6 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 255)">
+Hi
+</body>
+</html>
diff --git a/widget/headless/tests/headless_button.html b/widget/headless/tests/headless_button.html
new file mode 100644
index 0000000000..5641066bfe
--- /dev/null
+++ b/widget/headless/tests/headless_button.html
@@ -0,0 +1,6 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body>
+<button id="btn">button</button>
+</body>
+</html>
diff --git a/widget/headless/tests/moz.build b/widget/headless/tests/moz.build
new file mode 100644
index 0000000000..9656ad382b
--- /dev/null
+++ b/widget/headless/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell.toml"]
diff --git a/widget/headless/tests/test_headless.js b/widget/headless/tests/test_headless.js
new file mode 100644
index 0000000000..f9183245d2
--- /dev/null
+++ b/widget/headless/tests/test_headless.js
@@ -0,0 +1,224 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+do_get_profile();
+const server = new HttpServer();
+server.registerDirectory("/", do_get_cwd());
+server.start(-1);
+const ROOT = `http://localhost:${server.identity.primaryPort}`;
+const BASE = `${ROOT}/`;
+const HEADLESS_URL = Services.io.newURI(`${BASE}/headless.html`);
+const HEADLESS_BUTTON_URL = Services.io.newURI(`${BASE}/headless_button.html`);
+registerCleanupFunction(() => {
+ server.stop(() => {});
+});
+
+// Refrences to the progress listeners to keep them from being gc'ed
+// before they are called.
+const progressListeners = new Map();
+
+function loadContentWindow(windowlessBrowser, uri) {
+ return new Promise((resolve, reject) => {
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ windowlessBrowser.loadURI(uri, loadURIOptions);
+ let docShell = windowlessBrowser.docShell;
+ let webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ let progressListener = {
+ onLocationChange(progress, request, location, flags) {
+ // Ignore inner-frame events
+ if (progress != webProgress) {
+ return;
+ }
+ // Ignore events that don't change the document
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ return;
+ }
+ let contentWindow = docShell.domWindow;
+ webProgress.removeProgressListener(progressListener);
+ progressListeners.delete(progressListener);
+ contentWindow.addEventListener(
+ "load",
+ event => {
+ resolve(contentWindow);
+ },
+ { once: true }
+ );
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ progressListeners.set(progressListener, progressListener);
+ webProgress.addProgressListener(
+ progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+ });
+}
+
+add_setup(function () {
+ Services.prefs.setBoolPref("security.allow_unsafe_parent_loads", true);
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("security.allow_unsafe_parent_loads");
+});
+
+add_task(async function test_snapshot() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
+ const contentWidth = 400;
+ const contentHeight = 300;
+ // Verify dimensions.
+ contentWindow.resizeTo(contentWidth, contentHeight);
+ equal(contentWindow.innerWidth, contentWidth);
+ equal(contentWindow.innerHeight, contentHeight);
+
+ // Snapshot the test page.
+ let canvas = contentWindow.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+ let width = contentWindow.innerWidth;
+ let height = contentWindow.innerHeight;
+ canvas.width = width;
+ canvas.height = height;
+ context.drawWindow(contentWindow, 0, 0, width, height, "rgb(255, 255, 255)");
+ let imageData = context.getImageData(0, 0, width, height).data;
+ ok(
+ imageData[0] === 0 &&
+ imageData[1] === 255 &&
+ imageData[2] === 0 &&
+ imageData[3] === 255,
+ "Page is green."
+ );
+
+ // Search for a blue pixel (a quick and dirty check to see if the blue text is
+ // on the page)
+ let found = false;
+ for (let i = 0; i < imageData.length; i += 4) {
+ if (imageData[i + 2] === 255) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, "Found blue text on page.");
+
+ windowlessBrowser.close();
+});
+
+add_task(async function test_snapshot_widget_layers() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ // nsIWindowlessBrowser inherits from nsIWebNavigation.
+ let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
+ const contentWidth = 1;
+ const contentHeight = 2;
+ // Verify dimensions.
+ contentWindow.resizeTo(contentWidth, contentHeight);
+ equal(contentWindow.innerWidth, contentWidth);
+ equal(contentWindow.innerHeight, contentHeight);
+
+ // Snapshot the test page.
+ let canvas = contentWindow.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+ let width = contentWindow.innerWidth;
+ let height = contentWindow.innerHeight;
+ canvas.width = width;
+ canvas.height = height;
+ context.drawWindow(
+ contentWindow,
+ 0,
+ 0,
+ width,
+ height,
+ "rgb(255, 255, 255)",
+ context.DRAWWINDOW_DRAW_CARET |
+ context.DRAWWINDOW_DRAW_VIEW |
+ context.DRAWWINDOW_USE_WIDGET_LAYERS
+ );
+ ok(true, "Snapshot with widget layers didn't crash.");
+
+ windowlessBrowser.close();
+});
+
+// Ensure keydown events are triggered on the windowless browser.
+add_task(async function test_keydown() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ // nsIWindowlessBrowser inherits from nsIWebNavigation.
+ let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
+
+ let keydown = new Promise(resolve => {
+ contentWindow.addEventListener(
+ "keydown",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ let tip = Cc["@mozilla.org/text-input-processor;1"].createInstance(
+ Ci.nsITextInputProcessor
+ );
+ let begun = tip.beginInputTransactionForTests(contentWindow);
+ ok(
+ begun,
+ "nsITextInputProcessor.beginInputTransactionForTests() should succeed"
+ );
+ tip.keydown(
+ new contentWindow.KeyboardEvent("", {
+ key: "a",
+ code: "KeyA",
+ keyCode: contentWindow.KeyboardEvent.DOM_VK_A,
+ })
+ );
+
+ await keydown;
+ ok(true, "Send keydown didn't crash");
+
+ windowlessBrowser.close();
+});
+
+// Test dragging the mouse on a button to ensure the creation of the drag
+// service doesn't crash in headless.
+add_task(async function test_mouse_drag() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ // nsIWindowlessBrowser inherits from nsIWebNavigation.
+ let contentWindow = await loadContentWindow(
+ windowlessBrowser,
+ HEADLESS_BUTTON_URL
+ );
+ contentWindow.resizeTo(400, 400);
+
+ let target = contentWindow.document.getElementById("btn");
+ let rect = target.getBoundingClientRect();
+ let left = rect.left;
+ let top = rect.top;
+
+ let utils = contentWindow.windowUtils;
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mousemove", left, top, 0, 1, 0, false, 0, 0);
+ // Wait for a turn of the event loop since the synthetic mouse event
+ // that creates the drag service is processed during the refresh driver.
+ await new Promise(r => {
+ executeSoon(r);
+ });
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+
+ ok(true, "Send mouse event didn't crash");
+
+ windowlessBrowser.close();
+});
diff --git a/widget/headless/tests/test_headless_clipboard.js b/widget/headless/tests/test_headless_clipboard.js
new file mode 100644
index 0000000000..862e343001
--- /dev/null
+++ b/widget/headless/tests/test_headless_clipboard.js
@@ -0,0 +1,45 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function getString(clipboard) {
+ var str = "";
+
+ // Create transferable that will transfer the text.
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor("text/plain");
+
+ clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
+
+ try {
+ var data = {};
+ trans.getTransferData("text/plain", data);
+
+ if (data) {
+ data = data.value.QueryInterface(Ci.nsISupportsString);
+ str = data.data;
+ }
+ } catch (ex) {
+ // If the clipboard is empty getTransferData will throw.
+ }
+
+ return str;
+}
+
+add_task(async function test_clipboard() {
+ let clipboard = Services.clipboard;
+
+ // Test copy.
+ const data = "random number: " + Math.random();
+ let helper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ helper.copyString(data);
+ equal(getString(clipboard), data, "Data was successfully copied.");
+
+ clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
+ equal(getString(clipboard), "", "Data was successfully cleared.");
+});
diff --git a/widget/headless/tests/xpcshell.toml b/widget/headless/tests/xpcshell.toml
new file mode 100644
index 0000000000..29c5fd100e
--- /dev/null
+++ b/widget/headless/tests/xpcshell.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+run-if = [
+ "os == 'linux'",
+ "os == 'win'",
+]
+headless = true
+
+["test_headless.js"]
+support-files = [
+ "headless.html",
+ "headless_button.html",
+]
+
+["test_headless_clipboard.js"]
diff --git a/widget/moz.build b/widget/moz.build
new file mode 100644
index 0000000000..54a231c67d
--- /dev/null
+++ b/widget/moz.build
@@ -0,0 +1,396 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget")
+
+with Files("crashtests/*1128214*"):
+ BUG_COMPONENT = ("Core", "Layout")
+
+with Files("crashtests/*303901*"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+with Files("crashtests/*380359*"):
+ BUG_COMPONENT = ("Core", "Widget")
+
+with Files("reftests/**"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("reftests/*fallback*"):
+ BUG_COMPONENT = ("Core", "Layout: Form Controls")
+
+with Files("*CompositorWidget*"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+with Files("*ContentCache*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*ContentData*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*Events.h"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*FontRange*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*Gfx*"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+with Files("*IMEData*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*TextEventDispatcher*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*NativeKeyBindings*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+if toolkit in ("android", "cocoa", "gtk", "uikit", "windows"):
+ DIRS += [toolkit]
+
+if toolkit == "cocoa":
+ XPIDL_SOURCES += [
+ "nsIMacDockSupport.idl",
+ "nsIMacFinderProgress.idl",
+ "nsIMacSharingService.idl",
+ "nsIMacUserActivityUpdater.idl",
+ "nsIMacWebAppUtils.idl",
+ "nsIStandaloneNativeMenu.idl",
+ "nsITaskbarProgress.idl",
+ "nsITouchBarHelper.idl",
+ "nsITouchBarInput.idl",
+ "nsITouchBarUpdater.idl",
+ ]
+elif toolkit == "gtk":
+ XPIDL_SOURCES += [
+ "nsIApplicationChooser.idl",
+ "nsIGtkTaskbarProgress.idl",
+ "nsITaskbarProgress.idl",
+ ]
+elif toolkit == "windows":
+ XPIDL_SOURCES += [
+ "nsIJumpListBuilder.idl",
+ "nsILegacyJumpListBuilder.idl",
+ "nsILegacyJumpListItem.idl",
+ "nsIPrintSettingsWin.idl",
+ "nsITaskbarOverlayIconController.idl",
+ "nsITaskbarPreview.idl",
+ "nsITaskbarPreviewButton.idl",
+ "nsITaskbarPreviewController.idl",
+ "nsITaskbarProgress.idl",
+ "nsITaskbarTabPreview.idl",
+ "nsITaskbarWindowPreview.idl",
+ "nsIWindowsUIUtils.idl",
+ "nsIWinTaskbar.idl",
+ ]
+
+if CONFIG["NS_PRINTING"]:
+ # Needs to go before the XPIDL_MODULE line.
+ XPIDL_SOURCES += [
+ "nsIPrintDialogService.idl",
+ ]
+
+TEST_DIRS += ["tests", "tests/gtest"]
+
+DIRS += ["headless"]
+
+# Don't build the DSO under the 'build' directory as windows does.
+#
+# The DSOs get built in the toolkit dir itself. Do this so that
+# multiple implementations of widget can be built on the same
+# source tree.
+#
+
+XPIDL_SOURCES += [
+ "nsIAppShell.idl",
+ "nsIBaseWindow.idl",
+ "nsIBidiKeyboard.idl",
+ "nsIClipboard.idl",
+ "nsIClipboardHelper.idl",
+ "nsIClipboardOwner.idl",
+ "nsIColorPicker.idl",
+ "nsIDisplayInfo.idl",
+ "nsIDragService.idl",
+ "nsIDragSession.idl",
+ "nsIFilePicker.idl",
+ "nsIFormatConverter.idl",
+ "nsIGfxInfo.idl",
+ "nsIGfxInfoDebug.idl",
+ "nsIPaper.idl",
+ "nsIPaperMargin.idl",
+ "nsIPrinter.idl",
+ "nsIPrinterList.idl",
+ "nsIPrintSettings.idl",
+ "nsIPrintSettingsService.idl",
+ "nsIScreen.idl",
+ "nsIScreenManager.idl",
+ "nsISharePicker.idl",
+ "nsISound.idl",
+ "nsISystemStatusBar.idl",
+ "nsITransferable.idl",
+ "nsIUserIdleService.idl",
+ "nsIUserIdleServiceInternal.idl",
+]
+
+XPIDL_MODULE = "widget"
+
+EXPORTS += [
+ "GfxDriverInfo.h",
+ "GfxInfoBase.h",
+ "GfxInfoCollector.h",
+ "InputData.h",
+ "nsBaseAppShell.h",
+ "nsBaseClipboard.h",
+ "nsBaseDragService.h",
+ "nsBaseFilePicker.h",
+ "nsBaseWidget.h",
+ "nsIDeviceContextSpec.h",
+ "nsIRollupListener.h",
+ "nsIWidget.h",
+ "nsIWidgetListener.h",
+ "nsPaper.h",
+ "nsPrinterListBase.h",
+ "nsUserIdleService.h",
+ "nsWidgetsCID.h",
+ "PuppetWidget.h",
+]
+
+EXPORTS.mozilla += [
+ "BasicEvents.h",
+ "ClipboardReadRequestChild.h",
+ "ClipboardReadRequestParent.h",
+ "ClipboardWriteRequestChild.h",
+ "ClipboardWriteRequestParent.h",
+ "ColorScheme.h",
+ "CommandList.h",
+ "ContentCache.h",
+ "ContentData.h",
+ "ContentEvents.h",
+ "DimensionRequest.h",
+ "EventClassList.h",
+ "EventForwards.h",
+ "EventMessageList.h",
+ "FontRange.h",
+ "LookAndFeel.h",
+ "MiscEvents.h",
+ "MouseEvents.h",
+ "NativeKeyBindingsType.h",
+ "SwipeTracker.h",
+ "TextEventDispatcher.h",
+ "TextEventDispatcherListener.h",
+ "TextEvents.h",
+ "TextRange.h",
+ "TouchEvents.h",
+ "VsyncDispatcher.h",
+ "WidgetUtils.h",
+ "WindowButtonType.h",
+]
+
+EXPORTS.mozilla.widget += [
+ "CompositorWidget.h",
+ "IconLoader.h",
+ "IMEData.h",
+ "InitData.h",
+ "InProcessCompositorWidget.h",
+ "MediaKeysEventSourceFactory.h",
+ "NativeMenu.h",
+ "NativeMenuSupport.h",
+ "nsAutoRollup.h",
+ "nsXPLookAndFeel.h",
+ "PuppetBidiKeyboard.h",
+ "RemoteLookAndFeel.h",
+ "Screen.h",
+ "ScreenManager.h",
+ "TextRecognition.h",
+ "ThemeChangeKind.h",
+ "WidgetMessageUtils.h",
+ "WindowOcclusionState.h",
+]
+
+UNIFIED_SOURCES += [
+ "ClipboardReadRequestParent.cpp",
+ "ClipboardWriteRequestChild.cpp",
+ "ClipboardWriteRequestParent.cpp",
+ "CompositorWidget.cpp",
+ "ContentCache.cpp",
+ "ContentData.cpp",
+ "DimensionRequest.cpp",
+ "GfxDriverInfo.cpp",
+ "GfxInfoBase.cpp",
+ "GfxInfoCollector.cpp",
+ "IconLoader.cpp",
+ "IMEData.cpp",
+ "InProcessCompositorWidget.cpp",
+ "InputData.cpp",
+ "nsAutoRollup.cpp",
+ "nsBaseAppShell.cpp",
+ "nsBaseClipboard.cpp",
+ "nsClipboardHelper.cpp",
+ "nsClipboardProxy.cpp",
+ "nsColorPickerProxy.cpp",
+ "nsDragServiceProxy.cpp",
+ "nsFilePickerProxy.cpp",
+ "nsHTMLFormatConverter.cpp",
+ "nsIBaseWindow.cpp",
+ "nsIDeviceContextSpec.cpp",
+ "nsIWidgetListener.cpp",
+ "nsPrimitiveHelpers.cpp",
+ "nsPrintSettingsImpl.cpp",
+ "nsTransferable.cpp",
+ "nsUserIdleService.cpp",
+ "nsXPLookAndFeel.cpp",
+ "PuppetBidiKeyboard.cpp",
+ "PuppetWidget.cpp",
+ "RemoteLookAndFeel.cpp",
+ "Screen.cpp",
+ "ScrollbarDrawing.cpp",
+ "ScrollbarDrawingAndroid.cpp",
+ "ScrollbarDrawingCocoa.cpp",
+ "ScrollbarDrawingGTK.cpp",
+ "ScrollbarDrawingWin.cpp",
+ "ScrollbarDrawingWin11.cpp",
+ "SharedWidgetUtils.cpp",
+ "SwipeTracker.cpp",
+ "TextEventDispatcher.cpp",
+ "TextRecognition.cpp",
+ "Theme.cpp",
+ "ThemeCocoa.cpp",
+ "ThemeColors.cpp",
+ "ThemeDrawing.cpp",
+ "TouchResampler.cpp",
+ "VsyncDispatcher.cpp",
+ "WidgetEventImpl.cpp",
+ "WidgetUtils.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "Linux":
+ EXPORTS.mozilla.widget += ["LSBUtils.h"]
+ SOURCES += ["LSBUtils.cpp"]
+
+if CONFIG["NS_PRINTING"]:
+ EXPORTS += [
+ "nsDeviceContextSpecProxy.h",
+ "nsPrintSettingsService.h",
+ ]
+ UNIFIED_SOURCES += [
+ "nsDeviceContextSpecProxy.cpp",
+ "nsPaper.cpp",
+ "nsPaperMargin.cpp",
+ "nsPrinterBase.cpp",
+ "nsPrinterListBase.cpp",
+ "nsPrintSettingsService.cpp",
+ ]
+
+ if toolkit in ("cocoa", "gtk"):
+ UNIFIED_SOURCES += [
+ "nsCUPSShim.cpp",
+ "nsPrinterCUPS.cpp",
+ "nsPrinterListCUPS.cpp",
+ ]
+
+# nsBaseWidget.cpp needs to be built separately because of name clashes in the OS X headers
+# nsBaseDragService.cpp moved out of UNIFIED to fix xgill crash (bug 1259850) after moving widget/ContentHelper -> apz/util/TouchActionHelper
+SOURCES += [
+ "nsBaseDragService.cpp",
+ "nsBaseWidget.cpp",
+ "ScreenManager.cpp",
+]
+
+if CONFIG["MOZ_INSTRUMENT_EVENT_LOOP"]:
+ EXPORTS.mozilla += [
+ "WidgetTraceEvent.h",
+ ]
+
+EXPORTS.ipc = [
+ "nsGUIEventIPC.h",
+]
+
+if CONFIG["MOZ_X11"] or CONFIG["MOZ_WAYLAND"]:
+ DIRS += ["x11"]
+
+if toolkit in {"gtk", "cocoa", "windows", "android", "uikit"}:
+ UNIFIED_SOURCES += [
+ "nsBaseFilePicker.cpp",
+ ]
+
+if toolkit in ("gtk", "windows", "cocoa", "android"):
+ UNIFIED_SOURCES += [
+ "nsNativeTheme.cpp",
+ ]
+
+DEFINES["MOZ_CROSS_PROCESS_IME"] = True
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/dom/ipc",
+ "/gfx/2d",
+ "/layout/base",
+ "/layout/forms",
+ "/layout/generic",
+ "/layout/painting",
+ "/layout/xul",
+ "/layout/xul/tree/",
+ "/view",
+ "/widget",
+ "/widget/headless",
+]
+
+# We use the CUPS headers on Cocoa and GTK, but on GTK we don't depend on there being system headers.
+if toolkit == "gtk":
+ LOCAL_INCLUDES += ["/third_party/cups/include"]
+
+if toolkit == "windows":
+ IPDL_SOURCES = [
+ "headless/HeadlessWidgetTypes.ipdlh",
+ "windows/PCompositorWidget.ipdl",
+ "windows/PlatformWidgetTypes.ipdlh",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ IPDL_SOURCES = [
+ "gtk/PCompositorWidget.ipdl",
+ "gtk/PlatformWidgetTypes.ipdlh",
+ "headless/HeadlessWidgetTypes.ipdlh",
+ ]
+elif toolkit == "android":
+ IPDL_SOURCES = [
+ "android/PCompositorWidget.ipdl",
+ "android/PlatformWidgetTypes.ipdlh",
+ "headless/HeadlessWidgetTypes.ipdlh",
+ ]
+else:
+ IPDL_SOURCES = [
+ "generic/PCompositorWidget.ipdl",
+ "generic/PlatformWidgetTypes.ipdlh",
+ "headless/HeadlessWidgetTypes.ipdlh",
+ ]
+
+IPDL_SOURCES += [
+ "LookAndFeelTypes.ipdlh",
+ "PClipboardReadRequest.ipdl",
+ "PClipboardWriteRequest.ipdl",
+]
+
+LOCAL_INCLUDES += [
+ "/widget/%s" % toolkit,
+]
+FINAL_LIBRARY = "xul"
+
+if CONFIG["MOZ_ENABLE_D3D10_LAYER"]:
+ DEFINES["MOZ_ENABLE_D3D10_LAYER"] = True
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+if CONFIG["MOZ_WAYLAND"]:
+ CXXFLAGS += CONFIG["MOZ_WAYLAND_CFLAGS"]
diff --git a/widget/nsAppShellSingleton.h b/widget/nsAppShellSingleton.h
new file mode 100644
index 0000000000..cd554951c6
--- /dev/null
+++ b/widget/nsAppShellSingleton.h
@@ -0,0 +1,60 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsAppShellSingleton_h__
+#define nsAppShellSingleton_h__
+
+/**
+ * This file is designed to be included into the file that provides the
+ * XPCOM module implementation for a particular widget toolkit.
+ *
+ * The following functions are defined:
+ * nsAppShellInit
+ * nsAppShellShutdown
+ * nsAppShellConstructor
+ *
+ * The nsAppShellInit function is designed to be used as a module constructor.
+ * If you already have a module constructor, then call nsAppShellInit from your
+ * module constructor.
+ *
+ * The nsAppShellShutdown function is designed to be used as a module
+ * destructor. If you already have a module destructor, then call
+ * nsAppShellShutdown from your module destructor.
+ *
+ * The nsAppShellConstructor function is designed to be used as a factory
+ * method for the nsAppShell class.
+ */
+
+#include "nsXULAppAPI.h"
+
+static nsIAppShell* sAppShell;
+
+static nsresult nsAppShellInit() {
+ NS_ASSERTION(!sAppShell, "already initialized");
+
+ sAppShell = new nsAppShell();
+ if (!sAppShell) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(sAppShell);
+
+ nsresult rv = static_cast<nsAppShell*>(sAppShell)->Init();
+ // If we somehow failed to initialize the appshell, it's extremely likely
+ // that we are sufficiently hosed that continuing on is just going to lead
+ // to bad things later. By crashing early here, the crash report will
+ // potentially contain a little more insight into what's going wrong than
+ // if we waited for a crash further down the line. See also bug 1545381.
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+
+ return NS_OK;
+}
+
+static void nsAppShellShutdown() { NS_RELEASE(sAppShell); }
+
+nsresult nsAppShellConstructor(const nsIID& iid, void** result) {
+ NS_ENSURE_TRUE(sAppShell, NS_ERROR_NOT_INITIALIZED);
+
+ return sAppShell->QueryInterface(iid, result);
+}
+
+#endif // nsAppShellSingleton_h__
diff --git a/widget/nsAutoRollup.cpp b/widget/nsAutoRollup.cpp
new file mode 100644
index 0000000000..9a043576df
--- /dev/null
+++ b/widget/nsAutoRollup.cpp
@@ -0,0 +1,50 @@
+/* -*- 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 "mozilla/widget/nsAutoRollup.h"
+
+namespace mozilla {
+namespace widget {
+
+/*static*/
+uint32_t nsAutoRollup::sCount = 0;
+/*static*/
+StaticRefPtr<nsIContent> nsAutoRollup::sLastRollup;
+
+nsAutoRollup::nsAutoRollup() {
+ // remember if sLastRollup was null, and only clear it upon destruction
+ // if so. This prevents recursive usage of nsAutoRollup from clearing
+ // sLastRollup when it shouldn't.
+ mWasClear = !sLastRollup;
+ sCount++;
+}
+
+nsAutoRollup::nsAutoRollup(nsIContent* aRollup) {
+ MOZ_ASSERT(!sLastRollup);
+ mWasClear = true;
+ sCount++;
+ SetLastRollup(aRollup);
+}
+
+nsAutoRollup::~nsAutoRollup() {
+ if (sLastRollup && mWasClear) {
+ sLastRollup = nullptr;
+ }
+ sCount--;
+}
+
+/*static*/
+void nsAutoRollup::SetLastRollup(nsIContent* aLastRollup) {
+ // There must be at least one nsAutoRollup on the stack.
+ MOZ_ASSERT(sCount);
+
+ sLastRollup = aLastRollup;
+}
+
+/*static*/
+nsIContent* nsAutoRollup::GetLastRollup() { return sLastRollup.get(); }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/nsAutoRollup.h b/widget/nsAutoRollup.h
new file mode 100644
index 0000000000..f6f3685887
--- /dev/null
+++ b/widget/nsAutoRollup.h
@@ -0,0 +1,54 @@
+/* -*- 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/. */
+#ifndef nsAutoRollup_h__
+#define nsAutoRollup_h__
+
+#include "mozilla/Attributes.h" // for MOZ_RAII
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "nsIContent.h"
+
+namespace mozilla {
+namespace widget {
+
+// A situation can occur when a mouse event occurs over a menu label while the
+// menu popup is already open. The expected behaviour is to close the popup.
+// This happens by calling nsIRollupListener::Rollup before the mouse event is
+// processed. However, in cases where the mouse event is not consumed, this
+// event will then get targeted at the menu label causing the menu to open
+// again. To prevent this, we store in sLastRollup a reference to the popup
+// that was closed during the Rollup call, and prevent this popup from
+// reopening while processing the mouse event.
+// sLastRollup can only be set while an nsAutoRollup is in scope;
+// when it goes out of scope sLastRollup is cleared automatically.
+// As sLastRollup is static, it can be retrieved by calling
+// nsAutoRollup::GetLastRollup.
+class MOZ_RAII nsAutoRollup {
+ public:
+ nsAutoRollup();
+ ~nsAutoRollup();
+
+ // Convenience constructor that creates a nsAutoRollup and also sets
+ // the last rollup.
+ explicit nsAutoRollup(nsIContent* aRollup);
+
+ static void SetLastRollup(nsIContent* aLastRollup);
+ // Return the popup that was last rolled up, or null if there isn't one.
+ static nsIContent* GetLastRollup();
+
+ private:
+ // Whether sLastRollup was clear when this nsAutoRollup
+ // was created.
+ bool mWasClear;
+
+ // The number of nsAutoRollup instances active.
+ static uint32_t sCount;
+ // The last rolled up popup.
+ static StaticRefPtr<nsIContent> sLastRollup;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // nsAutoRollup_h__
diff --git a/widget/nsBaseAppShell.cpp b/widget/nsBaseAppShell.cpp
new file mode 100644
index 0000000000..632f38478c
--- /dev/null
+++ b/widget/nsBaseAppShell.cpp
@@ -0,0 +1,313 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 "base/message_loop.h"
+
+#include "nsBaseAppShell.h"
+#include "nsExceptionHandler.h"
+#include "nsJSUtils.h"
+#include "nsThreadUtils.h"
+#include "nsIAppShell.h"
+#include "nsIObserverService.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Services.h"
+#include "nsXULAppAPI.h"
+
+// When processing the next thread event, the appshell may process native
+// events (if not in performance mode), which can result in suppressing the
+// next thread event for at most this many ticks:
+#define THREAD_EVENT_STARVATION_LIMIT PR_MillisecondsToInterval(10)
+
+NS_IMPL_ISUPPORTS(nsBaseAppShell, nsIAppShell, nsIThreadObserver, nsIObserver)
+
+nsBaseAppShell::nsBaseAppShell()
+ : mSuspendNativeCount(0),
+ mEventloopNestingLevel(0),
+ mBlockedWait(nullptr),
+ mNativeEventPending(false),
+ mGeckoTaskBurstStartTime(0),
+ mLastNativeEventTime(0),
+ mEventloopNestingState(eEventloopNone),
+ mRunning(false),
+ mExiting(false),
+ mBlockNativeEvent(false),
+ mProcessedGeckoEvents(false) {}
+
+nsBaseAppShell::~nsBaseAppShell() = default;
+
+nsresult nsBaseAppShell::Init() {
+ // Configure ourselves as an observer for the current thread:
+
+ if (XRE_UseNativeEventProcessing()) {
+ nsCOMPtr<nsIThreadInternal> threadInt =
+ do_QueryInterface(NS_GetCurrentThread());
+ NS_ENSURE_STATE(threadInt);
+
+ threadInt->SetObserver(this);
+ }
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ if (obsSvc) obsSvc->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ return NS_OK;
+}
+
+// Called by nsAppShell's native event callback
+void nsBaseAppShell::NativeEventCallback() {
+ if (!mNativeEventPending.exchange(false)) return;
+
+ // If DoProcessNextNativeEvent is on the stack, then we assume that we can
+ // just unwind and let nsThread::ProcessNextEvent process the next event.
+ // However, if we are called from a nested native event loop (maybe via some
+ // plug-in or library function), then go ahead and process Gecko events now.
+ if (mEventloopNestingState == eEventloopXPCOM) {
+ mEventloopNestingState = eEventloopOther;
+ // XXX there is a tiny risk we will never get a new NativeEventCallback,
+ // XXX see discussion in bug 389931.
+ return;
+ }
+
+ // nsBaseAppShell::Run is not being used to pump events, so this may be
+ // our only opportunity to process pending gecko events.
+
+ nsIThread* thread = NS_GetCurrentThread();
+ bool prevBlockNativeEvent = mBlockNativeEvent;
+ if (mEventloopNestingState == eEventloopOther) {
+ if (!NS_HasPendingEvents(thread)) return;
+ // We're in a nested native event loop and have some gecko events to
+ // process. While doing that we block processing native events from the
+ // appshell - instead, we want to get back to the nested native event
+ // loop ASAP (bug 420148).
+ mBlockNativeEvent = true;
+ }
+
+ IncrementEventloopNestingLevel();
+ EventloopNestingState prevVal = mEventloopNestingState;
+ NS_ProcessPendingEvents(thread, THREAD_EVENT_STARVATION_LIMIT);
+ mProcessedGeckoEvents = true;
+ mEventloopNestingState = prevVal;
+ mBlockNativeEvent = prevBlockNativeEvent;
+
+ // Continue processing pending events later (we don't want to starve the
+ // embedders event loop).
+ if (NS_HasPendingEvents(thread)) DoProcessMoreGeckoEvents();
+
+ DecrementEventloopNestingLevel();
+}
+
+void nsBaseAppShell::OnSystemTimezoneChange() {
+ nsJSUtils::ResetTimeZone();
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ if (obsSvc) {
+ // Timezone changed notification
+ obsSvc->NotifyObservers(nullptr, DEFAULT_TIMEZONE_CHANGED_OBSERVER_TOPIC,
+ nullptr);
+ }
+}
+
+// Note, this is currently overidden on windows, see comments in nsAppShell for
+// details.
+void nsBaseAppShell::DoProcessMoreGeckoEvents() { OnDispatchedEvent(); }
+
+// Main thread via OnProcessNextEvent below
+bool nsBaseAppShell::DoProcessNextNativeEvent(bool mayWait) {
+ // The next native event to be processed may trigger our NativeEventCallback,
+ // in which case we do not want it to process any thread events since we'll
+ // do that when this function returns.
+ //
+ // If the next native event is not our NativeEventCallback, then we may end
+ // up recursing into this function.
+ //
+ // However, if the next native event is not our NativeEventCallback, but it
+ // results in another native event loop, then our NativeEventCallback could
+ // fire and it will see mEventloopNestingState as eEventloopOther.
+ //
+ EventloopNestingState prevVal = mEventloopNestingState;
+ mEventloopNestingState = eEventloopXPCOM;
+
+ IncrementEventloopNestingLevel();
+ bool result = ProcessNextNativeEvent(mayWait);
+ DecrementEventloopNestingLevel();
+
+ mEventloopNestingState = prevVal;
+ return result;
+}
+
+//-------------------------------------------------------------------------
+// nsIAppShell methods:
+
+NS_IMETHODIMP
+nsBaseAppShell::Run(void) {
+ NS_ENSURE_STATE(!mRunning); // should not call Run twice
+ mRunning = true;
+
+ nsIThread* thread = NS_GetCurrentThread();
+
+ MessageLoop::current()->Run();
+
+ NS_ProcessPendingEvents(thread);
+
+ mRunning = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseAppShell::Exit(void) {
+ if (mRunning && !mExiting) {
+ MessageLoop::current()->Quit();
+ }
+ mExiting = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseAppShell::GeckoTaskBurst() {
+ if (mGeckoTaskBurstStartTime == 0) {
+ mGeckoTaskBurstStartTime = PR_IntervalNow();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseAppShell::SuspendNative() {
+ ++mSuspendNativeCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseAppShell::ResumeNative() {
+ --mSuspendNativeCount;
+ NS_ASSERTION(mSuspendNativeCount >= 0,
+ "Unbalanced call to nsBaseAppShell::ResumeNative!");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseAppShell::GetEventloopNestingLevel(uint32_t* aNestingLevelResult) {
+ NS_ENSURE_ARG_POINTER(aNestingLevelResult);
+
+ *aNestingLevelResult = mEventloopNestingLevel;
+
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+// nsIThreadObserver methods:
+
+// Called from any thread
+NS_IMETHODIMP
+nsBaseAppShell::OnDispatchedEvent() {
+ if (mBlockNativeEvent) return NS_OK;
+
+ if (mNativeEventPending.exchange(true)) return NS_OK;
+
+ // Returns on the main thread in NativeEventCallback above
+ ScheduleNativeEventCallback();
+ return NS_OK;
+}
+
+// Called from the main thread
+NS_IMETHODIMP
+nsBaseAppShell::OnProcessNextEvent(nsIThreadInternal* thr, bool mayWait) {
+ if (mBlockNativeEvent) {
+ if (!mayWait) return NS_OK;
+ // Hmm, we're in a nested native event loop and would like to get
+ // back to it ASAP, but it seems a gecko event has caused us to
+ // spin up a nested XPCOM event loop (eg. modal window), so we
+ // really must start processing native events here again.
+ mBlockNativeEvent = false;
+ if (NS_HasPendingEvents(thr))
+ OnDispatchedEvent(); // in case we blocked it earlier
+ }
+
+ PRIntervalTime start = PR_IntervalNow();
+ PRIntervalTime limit = THREAD_EVENT_STARVATION_LIMIT;
+
+ // Unblock outer nested wait loop (below).
+ if (mBlockedWait) *mBlockedWait = false;
+
+ bool* oldBlockedWait = mBlockedWait;
+ mBlockedWait = &mayWait;
+
+ // When mayWait is true, we need to make sure that there is an event in the
+ // thread's event queue before we return. Otherwise, the thread will block
+ // on its event queue waiting for an event.
+ bool needEvent = mayWait;
+ // Reset prior to invoking DoProcessNextNativeEvent which might cause
+ // NativeEventCallback to process gecko events.
+ mProcessedGeckoEvents = false;
+
+ // Content processes always priorize gecko events.
+ if (!XRE_IsContentProcess() && (start > (mGeckoTaskBurstStartTime + limit))) {
+ mGeckoTaskBurstStartTime = 0;
+ // Favor pending native events
+ PRIntervalTime now = start;
+ bool keepGoing;
+ do {
+ mLastNativeEventTime = now;
+ keepGoing = DoProcessNextNativeEvent(false);
+ } while (keepGoing && ((now = PR_IntervalNow()) - start) < limit);
+ } else {
+ // Avoid starving native events completely when in performance mode
+ if (start - mLastNativeEventTime > limit) {
+ mLastNativeEventTime = start;
+ DoProcessNextNativeEvent(false);
+ }
+ }
+
+ while (!NS_HasPendingEvents(thr) && !mProcessedGeckoEvents) {
+ // If we have been asked to exit from Run, then we should not wait for
+ // events to process. Note that an inner nested event loop causes
+ // 'mayWait' to become false too, through 'mBlockedWait'.
+ if (mExiting) mayWait = false;
+
+ mLastNativeEventTime = PR_IntervalNow();
+ if (!DoProcessNextNativeEvent(mayWait) || !mayWait) break;
+ }
+
+ mBlockedWait = oldBlockedWait;
+
+ // Make sure that the thread event queue does not block on its monitor, as
+ // it normally would do if it did not have any pending events. To avoid
+ // that, we simply insert a dummy event into its queue during shutdown.
+ if (needEvent && !mExiting && !NS_HasPendingEvents(thr)) {
+ DispatchDummyEvent(thr);
+ }
+
+ return NS_OK;
+}
+
+bool nsBaseAppShell::DispatchDummyEvent(nsIThread* aTarget) {
+ NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+
+ if (!mDummyEvent) mDummyEvent = new mozilla::Runnable("DummyEvent");
+
+ return NS_SUCCEEDED(aTarget->Dispatch(mDummyEvent, NS_DISPATCH_NORMAL));
+}
+
+void nsBaseAppShell::IncrementEventloopNestingLevel() {
+ ++mEventloopNestingLevel;
+ CrashReporter::SetEventloopNestingLevel(mEventloopNestingLevel);
+}
+
+void nsBaseAppShell::DecrementEventloopNestingLevel() {
+ --mEventloopNestingLevel;
+ CrashReporter::SetEventloopNestingLevel(mEventloopNestingLevel);
+}
+
+// Called from the main thread
+NS_IMETHODIMP
+nsBaseAppShell::AfterProcessNextEvent(nsIThreadInternal* thr,
+ bool eventWasProcessed) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseAppShell::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ NS_ASSERTION(!strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID), "oops");
+ Exit();
+ return NS_OK;
+}
diff --git a/widget/nsBaseAppShell.h b/widget/nsBaseAppShell.h
new file mode 100644
index 0000000000..fabc0e188a
--- /dev/null
+++ b/widget/nsBaseAppShell.h
@@ -0,0 +1,140 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsBaseAppShell_h__
+#define nsBaseAppShell_h__
+
+#include "mozilla/Atomics.h"
+#include "nsIAppShell.h"
+#include "nsIThreadInternal.h"
+#include "nsIObserver.h"
+#include "nsIRunnable.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "prinrval.h"
+
+/**
+ * A singleton that manages the UI thread's event queue. Subclass this class
+ * to enable platform-specific event queue support.
+ */
+class nsBaseAppShell : public nsIAppShell,
+ public nsIThreadObserver,
+ public nsIObserver {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIAPPSHELL
+
+ NS_DECL_NSITHREADOBSERVER
+ NS_DECL_NSIOBSERVER
+
+ nsBaseAppShell();
+
+ /**
+ * Called by subclasses. Reset the internal timezone when the user's system
+ * timezone changes.
+ */
+ static void OnSystemTimezoneChange();
+
+ protected:
+ virtual ~nsBaseAppShell();
+
+ /**
+ * This method is called by subclasses when the app shell singleton is
+ * instantiated.
+ */
+ nsresult Init();
+
+ /**
+ * Called by subclasses from a native event. See ScheduleNativeEventCallback.
+ */
+ void NativeEventCallback();
+
+ /**
+ * Make a decision as to whether or not NativeEventCallback will
+ * trigger gecko event processing when there are pending gecko
+ * events.
+ */
+ virtual void DoProcessMoreGeckoEvents();
+
+ /**
+ * Implemented by subclasses. Invoke NativeEventCallback from a native
+ * event. This method may be called on any thread.
+ */
+ virtual void ScheduleNativeEventCallback() = 0;
+
+ /**
+ * Implemented by subclasses. Process the next native event. Only wait for
+ * the next native event if mayWait is true. This method is only called on
+ * the main application thread.
+ *
+ * @param mayWait
+ * If "true", then this method may wait if necessary for the next available
+ * native event. DispatchNativeEvent may be called to unblock a call to
+ * ProcessNextNativeEvent that is waiting.
+ * @return
+ * This method returns "true" if a native event was processed.
+ */
+ virtual bool ProcessNextNativeEvent(bool mayWait) = 0;
+
+ int32_t mSuspendNativeCount;
+ uint32_t mEventloopNestingLevel;
+
+ private:
+ bool DoProcessNextNativeEvent(bool mayWait);
+
+ bool DispatchDummyEvent(nsIThread* target);
+
+ void IncrementEventloopNestingLevel();
+ void DecrementEventloopNestingLevel();
+
+ nsCOMPtr<nsIRunnable> mDummyEvent;
+ /**
+ * mBlockedWait points back to a slot that controls the wait loop in
+ * an outer OnProcessNextEvent invocation. Nested calls always set
+ * it to false to unblock an outer loop, since all events may
+ * have been consumed by the inner event loop(s).
+ */
+ bool* mBlockedWait;
+ mozilla::Atomic<bool> mNativeEventPending;
+ PRIntervalTime mGeckoTaskBurstStartTime;
+ PRIntervalTime mLastNativeEventTime;
+ enum EventloopNestingState {
+ eEventloopNone, // top level thread execution
+ eEventloopXPCOM, // innermost native event loop is ProcessNextNativeEvent
+ eEventloopOther // innermost native event loop is a native library/plugin
+ // etc
+ };
+ EventloopNestingState mEventloopNestingState;
+ bool mRunning;
+ bool mExiting;
+ /**
+ * mBlockNativeEvent blocks the appshell from processing native events.
+ * It is set to true while a nested native event loop (eEventloopOther)
+ * is processing gecko events in NativeEventCallback(), thus queuing up
+ * native events until we return to that loop (bug 420148).
+ * We force mBlockNativeEvent to false in case handling one of the gecko
+ * events spins up a nested XPCOM event loop (eg. modal window) which would
+ * otherwise lead to a "deadlock" where native events aren't processed at all.
+ */
+ bool mBlockNativeEvent;
+ /**
+ * Tracks whether we have processed any gecko events in NativeEventCallback so
+ * that we can avoid erroneously entering a blocking loop waiting for gecko
+ * events to show up during OnProcessNextEvent. This is required because on
+ * OS X ProcessGeckoEvents may be invoked inside the context of
+ * ProcessNextNativeEvent and may result in NativeEventCallback being invoked
+ * and in turn invoking NS_ProcessPendingEvents. Because
+ * ProcessNextNativeEvent may be invoked prior to the NS_HasPendingEvents
+ * waiting loop, this is the only way to make the loop aware that events may
+ * have been processed.
+ *
+ * This variable is set to false in OnProcessNextEvent prior to the first
+ * call to DoProcessNextNativeEvent. It is set to true by
+ * NativeEventCallback after calling NS_ProcessPendingEvents.
+ */
+ bool mProcessedGeckoEvents;
+};
+
+#endif // nsBaseAppShell_h__
diff --git a/widget/nsBaseClipboard.cpp b/widget/nsBaseClipboard.cpp
new file mode 100644
index 0000000000..40af1de388
--- /dev/null
+++ b/widget/nsBaseClipboard.cpp
@@ -0,0 +1,1313 @@
+/* -*- 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 "nsBaseClipboard.h"
+
+#include "ContentAnalysis.h"
+#include "mozilla/Components.h"
+#include "mozilla/contentanalysis/ContentAnalysisIPCTypes.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MoveOnlyFunction.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Services.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsContentUtils.h"
+#include "nsFocusManager.h"
+#include "nsIClipboardOwner.h"
+#include "nsIPromptService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsError.h"
+#include "nsXPCOM.h"
+
+using mozilla::GenericPromise;
+using mozilla::LogLevel;
+using mozilla::UniquePtr;
+using mozilla::dom::BrowsingContext;
+using mozilla::dom::CanonicalBrowsingContext;
+using mozilla::dom::ClipboardCapabilities;
+using mozilla::dom::Document;
+
+static const int32_t kGetAvailableFlavorsRetryCount = 5;
+
+namespace {
+
+struct ClipboardGetRequest {
+ ClipboardGetRequest(const nsTArray<nsCString>& aFlavorList,
+ nsIAsyncClipboardGetCallback* aCallback)
+ : mFlavorList(aFlavorList.Clone()), mCallback(aCallback) {}
+
+ const nsTArray<nsCString> mFlavorList;
+ const nsCOMPtr<nsIAsyncClipboardGetCallback> mCallback;
+};
+
+class UserConfirmationRequest final
+ : public mozilla::dom::PromiseNativeHandler {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(UserConfirmationRequest)
+
+ UserConfirmationRequest(int32_t aClipboardType,
+ Document* aRequestingChromeDocument,
+ nsIPrincipal* aRequestingPrincipal,
+ nsBaseClipboard* aClipboard,
+ mozilla::dom::WindowContext* aRequestingWindowContext)
+ : mClipboardType(aClipboardType),
+ mRequestingChromeDocument(aRequestingChromeDocument),
+ mRequestingPrincipal(aRequestingPrincipal),
+ mClipboard(aClipboard),
+ mRequestingWindowContext(aRequestingWindowContext) {
+ MOZ_ASSERT(
+ mClipboard->nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+ }
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ mozilla::ErrorResult& aRv) override;
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ mozilla::ErrorResult& aRv) override;
+
+ bool IsEqual(int32_t aClipboardType, Document* aRequestingChromeDocument,
+ nsIPrincipal* aRequestingPrincipal,
+ mozilla::dom::WindowContext* aRequestingWindowContext) const {
+ if (!(ClipboardType() == aClipboardType &&
+ RequestingChromeDocument() == aRequestingChromeDocument &&
+ RequestingPrincipal()->Equals(aRequestingPrincipal) &&
+ (mRequestingWindowContext && aRequestingWindowContext))) {
+ return false;
+ }
+ // Only check requesting window contexts if content analysis is active
+ nsCOMPtr<nsIContentAnalysis> contentAnalysis =
+ mozilla::components::nsIContentAnalysis::Service();
+ if (!contentAnalysis) {
+ return false;
+ }
+
+ bool contentAnalysisIsActive;
+ nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
+ if (MOZ_LIKELY(NS_FAILED(rv) || !contentAnalysisIsActive)) {
+ return true;
+ }
+ return mRequestingWindowContext->Id() == aRequestingWindowContext->Id();
+ }
+
+ int32_t ClipboardType() const { return mClipboardType; }
+
+ Document* RequestingChromeDocument() const {
+ return mRequestingChromeDocument;
+ }
+
+ nsIPrincipal* RequestingPrincipal() const { return mRequestingPrincipal; }
+
+ void AddClipboardGetRequest(const nsTArray<nsCString>& aFlavorList,
+ nsIAsyncClipboardGetCallback* aCallback) {
+ MOZ_ASSERT(!aFlavorList.IsEmpty());
+ MOZ_ASSERT(aCallback);
+ mPendingClipboardGetRequests.AppendElement(
+ mozilla::MakeUnique<ClipboardGetRequest>(aFlavorList, aCallback));
+ }
+
+ void RejectPendingClipboardGetRequests(nsresult aError) {
+ MOZ_ASSERT(NS_FAILED(aError));
+ auto requests = std::move(mPendingClipboardGetRequests);
+ for (const auto& request : requests) {
+ MOZ_ASSERT(request);
+ MOZ_ASSERT(request->mCallback);
+ request->mCallback->OnError(aError);
+ }
+ }
+
+ void ProcessPendingClipboardGetRequests() {
+ auto requests = std::move(mPendingClipboardGetRequests);
+ for (const auto& request : requests) {
+ MOZ_ASSERT(request);
+ MOZ_ASSERT(!request->mFlavorList.IsEmpty());
+ MOZ_ASSERT(request->mCallback);
+ mClipboard->AsyncGetDataInternal(request->mFlavorList, mClipboardType,
+ mRequestingWindowContext,
+ request->mCallback);
+ }
+ }
+
+ nsTArray<UniquePtr<ClipboardGetRequest>>& GetPendingClipboardGetRequests() {
+ return mPendingClipboardGetRequests;
+ }
+
+ private:
+ ~UserConfirmationRequest() = default;
+
+ const int32_t mClipboardType;
+ RefPtr<Document> mRequestingChromeDocument;
+ const nsCOMPtr<nsIPrincipal> mRequestingPrincipal;
+ const RefPtr<nsBaseClipboard> mClipboard;
+ const RefPtr<mozilla::dom::WindowContext> mRequestingWindowContext;
+ // Track the pending read requests that wait for user confirmation.
+ nsTArray<UniquePtr<ClipboardGetRequest>> mPendingClipboardGetRequests;
+};
+
+NS_IMPL_CYCLE_COLLECTION(UserConfirmationRequest, mRequestingChromeDocument)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UserConfirmationRequest)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(UserConfirmationRequest)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(UserConfirmationRequest)
+
+static mozilla::StaticRefPtr<UserConfirmationRequest> sUserConfirmationRequest;
+
+void UserConfirmationRequest::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ mozilla::ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(sUserConfirmationRequest == this);
+ sUserConfirmationRequest = nullptr;
+
+ JS::Rooted<JSObject*> detailObj(aCx, &aValue.toObject());
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ nsresult rv = mozilla::dom::UnwrapArg<nsIPropertyBag2>(
+ aCx, detailObj, getter_AddRefs(propBag));
+ if (NS_FAILED(rv)) {
+ RejectPendingClipboardGetRequests(rv);
+ return;
+ }
+
+ bool result = false;
+ rv = propBag->GetPropertyAsBool(u"ok"_ns, &result);
+ if (NS_FAILED(rv)) {
+ RejectPendingClipboardGetRequests(rv);
+ return;
+ }
+
+ if (!result) {
+ RejectPendingClipboardGetRequests(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ return;
+ }
+
+ ProcessPendingClipboardGetRequests();
+}
+
+void UserConfirmationRequest::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ mozilla::ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(sUserConfirmationRequest == this);
+ sUserConfirmationRequest = nullptr;
+ RejectPendingClipboardGetRequests(NS_ERROR_FAILURE);
+}
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncSetClipboardData,
+ nsIAsyncSetClipboardData)
+
+nsBaseClipboard::AsyncSetClipboardData::AsyncSetClipboardData(
+ int32_t aClipboardType, nsBaseClipboard* aClipboard,
+ nsIAsyncClipboardRequestCallback* aCallback)
+ : mClipboardType(aClipboardType),
+ mClipboard(aClipboard),
+ mCallback(aCallback) {
+ MOZ_ASSERT(mClipboard);
+ MOZ_ASSERT(
+ mClipboard->nsIClipboard::IsClipboardTypeSupported(mClipboardType));
+}
+
+NS_IMETHODIMP
+nsBaseClipboard::AsyncSetClipboardData::SetData(nsITransferable* aTransferable,
+ nsIClipboardOwner* aOwner) {
+ MOZ_CLIPBOARD_LOG("AsyncSetClipboardData::SetData (%p): clipboard=%d", this,
+ mClipboardType);
+
+ if (!IsValid()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ nsTArray<nsCString> flavors;
+ if (NS_SUCCEEDED(aTransferable->FlavorsTransferableCanImport(flavors))) {
+ for (const auto& flavor : flavors) {
+ MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get());
+ }
+ }
+ }
+
+ MOZ_ASSERT(mClipboard);
+ MOZ_ASSERT(
+ mClipboard->nsIClipboard::IsClipboardTypeSupported(mClipboardType));
+ MOZ_DIAGNOSTIC_ASSERT(mClipboard->mPendingWriteRequests[mClipboardType] ==
+ this);
+
+ RefPtr<AsyncSetClipboardData> request =
+ std::move(mClipboard->mPendingWriteRequests[mClipboardType]);
+ nsresult rv = mClipboard->SetData(aTransferable, aOwner, mClipboardType);
+ MaybeNotifyCallback(rv);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsBaseClipboard::AsyncSetClipboardData::Abort(nsresult aReason) {
+ // Note: This may be called during destructor, so it should not attempt to
+ // take a reference to mClipboard.
+
+ if (!IsValid() || !NS_FAILED(aReason)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MaybeNotifyCallback(aReason);
+ return NS_OK;
+}
+
+void nsBaseClipboard::AsyncSetClipboardData::MaybeNotifyCallback(
+ nsresult aResult) {
+ // Note: This may be called during destructor, so it should not attempt to
+ // take a reference to mClipboard.
+
+ MOZ_ASSERT(IsValid());
+ if (nsCOMPtr<nsIAsyncClipboardRequestCallback> callback =
+ mCallback.forget()) {
+ callback->OnComplete(aResult);
+ }
+ // Once the callback is notified, setData should not be allowed, so invalidate
+ // this request.
+ mClipboard = nullptr;
+}
+
+void nsBaseClipboard::RejectPendingAsyncSetDataRequestIfAny(
+ int32_t aClipboardType) {
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+ auto& request = mPendingWriteRequests[aClipboardType];
+ if (request) {
+ request->Abort(NS_ERROR_ABORT);
+ request = nullptr;
+ }
+}
+
+NS_IMETHODIMP nsBaseClipboard::AsyncSetData(
+ int32_t aWhichClipboard, nsIAsyncClipboardRequestCallback* aCallback,
+ nsIAsyncSetClipboardData** _retval) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
+
+ *_retval = nullptr;
+ if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
+ aWhichClipboard);
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+
+ // Reject existing pending AsyncSetData request if any.
+ RejectPendingAsyncSetDataRequestIfAny(aWhichClipboard);
+
+ // Create a new AsyncSetClipboardData.
+ RefPtr<AsyncSetClipboardData> request =
+ mozilla::MakeRefPtr<AsyncSetClipboardData>(aWhichClipboard, this,
+ aCallback);
+ mPendingWriteRequests[aWhichClipboard] = request;
+ request.forget(_retval);
+ return NS_OK;
+}
+
+namespace {
+class SafeContentAnalysisResultCallback final
+ : public nsIContentAnalysisCallback {
+ public:
+ explicit SafeContentAnalysisResultCallback(
+ std::function<void(RefPtr<nsIContentAnalysisResult>&&)> aResolver)
+ : mResolver(std::move(aResolver)) {}
+ void Callback(RefPtr<nsIContentAnalysisResult>&& aResult) {
+ MOZ_ASSERT(mResolver, "Called SafeContentAnalysisResultCallback twice!");
+ if (auto resolver = std::move(mResolver)) {
+ resolver(std::move(aResult));
+ }
+ }
+
+ NS_IMETHODIMP ContentResult(nsIContentAnalysisResponse* aResponse) override {
+ using namespace mozilla::contentanalysis;
+ RefPtr<ContentAnalysisResult> result =
+ ContentAnalysisResult::FromContentAnalysisResponse(aResponse);
+ Callback(result);
+ return NS_OK;
+ }
+
+ NS_IMETHODIMP Error(nsresult aError) override {
+ using namespace mozilla::contentanalysis;
+ Callback(ContentAnalysisResult::FromNoResult(
+ NoContentAnalysisResult::ERROR_OTHER));
+ return NS_OK;
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ private:
+ // Private destructor to force this to be allocated in a RefPtr, which is
+ // necessary for safe usage.
+ ~SafeContentAnalysisResultCallback() {
+ MOZ_ASSERT(!mResolver, "SafeContentAnalysisResultCallback never called!");
+ }
+ mozilla::MoveOnlyFunction<void(RefPtr<nsIContentAnalysisResult>&&)> mResolver;
+};
+NS_IMPL_ISUPPORTS(SafeContentAnalysisResultCallback,
+ nsIContentAnalysisCallback);
+} // namespace
+
+// Returning:
+// - true means a content analysis request was fired
+// - false means there is no text data in the transferable
+// - NoContentAnalysisResult means there was an error
+static mozilla::Result<bool, mozilla::contentanalysis::NoContentAnalysisResult>
+CheckClipboardContentAnalysisAsText(
+ uint64_t aInnerWindowId, SafeContentAnalysisResultCallback* aResolver,
+ nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis,
+ nsITransferable* aTextTrans) {
+ using namespace mozilla::contentanalysis;
+
+ nsCOMPtr<nsISupports> transferData;
+ if (NS_FAILED(aTextTrans->GetTransferData(kTextMime,
+ getter_AddRefs(transferData)))) {
+ return false;
+ }
+ nsCOMPtr<nsISupportsString> textData = do_QueryInterface(transferData);
+ if (MOZ_UNLIKELY(!textData)) {
+ return false;
+ }
+ nsString text;
+ if (NS_FAILED(textData->GetData(text))) {
+ return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER);
+ }
+ RefPtr<mozilla::dom::WindowGlobalParent> window =
+ mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId);
+ if (!window) {
+ // The window has gone away in the meantime
+ return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER);
+ }
+ nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
+ new ContentAnalysisRequest(
+ nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry,
+ std::move(text), false, EmptyCString(), aDocumentURI,
+ nsIContentAnalysisRequest::OperationType::eClipboard, window);
+ nsresult rv = aContentAnalysis->AnalyzeContentRequestCallback(
+ contentAnalysisRequest, /* aAutoAcknowledge */ true, aResolver);
+ if (NS_FAILED(rv)) {
+ return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER);
+ }
+ return true;
+}
+
+// Returning:
+// - true means a content analysis request was fired
+// - false means there is no file data in the transferable
+// - NoContentAnalysisResult means there was an error
+static mozilla::Result<bool, mozilla::contentanalysis::NoContentAnalysisResult>
+CheckClipboardContentAnalysisAsFile(
+ uint64_t aInnerWindowId, SafeContentAnalysisResultCallback* aResolver,
+ nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis,
+ nsITransferable* aFileTrans) {
+ using namespace mozilla::contentanalysis;
+
+ nsCOMPtr<nsISupports> transferData;
+ nsresult rv =
+ aFileTrans->GetTransferData(kFileMime, getter_AddRefs(transferData));
+ nsString filePath;
+ if (NS_SUCCEEDED(rv)) {
+ if (nsCOMPtr<nsIFile> file = do_QueryInterface(transferData)) {
+ rv = file->GetPath(filePath);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("clipboard data had kFileMime but no nsIFile!");
+ return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER);
+ }
+ }
+ if (NS_FAILED(rv) || filePath.IsEmpty()) {
+ return false;
+ }
+ RefPtr<mozilla::dom::WindowGlobalParent> window =
+ mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId);
+ if (!window) {
+ // The window has gone away in the meantime
+ return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER);
+ }
+ // Let the content analysis code calculate the digest
+ nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
+ new ContentAnalysisRequest(
+ nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry,
+ std::move(filePath), true, EmptyCString(), aDocumentURI,
+ nsIContentAnalysisRequest::OperationType::eCustomDisplayString,
+ window);
+ rv = aContentAnalysis->AnalyzeContentRequestCallback(
+ contentAnalysisRequest,
+ /* aAutoAcknowledge */ true, aResolver);
+ if (NS_FAILED(rv)) {
+ return mozilla::Err(NoContentAnalysisResult::ERROR_OTHER);
+ }
+ return true;
+}
+
+static void CheckClipboardContentAnalysis(
+ mozilla::dom::WindowGlobalParent* aWindow, nsITransferable* aTransferable,
+ SafeContentAnalysisResultCallback* aResolver) {
+ using namespace mozilla::contentanalysis;
+
+ // Content analysis is only needed if an outside webpage has access to
+ // the data. So, skip content analysis if there is:
+ // - no associated window (for example, scripted clipboard read by system
+ // code)
+ // - the window is a chrome docshell
+ // - the window is being rendered in the parent process (for example,
+ // about:support and the like)
+ if (!aWindow || aWindow->GetBrowsingContext()->IsChrome() ||
+ aWindow->IsInProcess()) {
+ aResolver->Callback(ContentAnalysisResult::FromNoResult(
+ NoContentAnalysisResult::CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS));
+ return;
+ }
+ nsCOMPtr<nsIContentAnalysis> contentAnalysis =
+ mozilla::components::nsIContentAnalysis::Service();
+ if (!contentAnalysis) {
+ aResolver->Callback(ContentAnalysisResult::FromNoResult(
+ NoContentAnalysisResult::ERROR_OTHER));
+ return;
+ }
+
+ bool contentAnalysisIsActive;
+ nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
+ if (MOZ_LIKELY(NS_FAILED(rv) || !contentAnalysisIsActive)) {
+ aResolver->Callback(ContentAnalysisResult::FromNoResult(
+ NoContentAnalysisResult::CONTENT_ANALYSIS_NOT_ACTIVE));
+ return;
+ }
+
+ nsCOMPtr<nsIURI> currentURI = aWindow->Canonical()->GetDocumentURI();
+ uint64_t innerWindowId = aWindow->InnerWindowId();
+ nsTArray<nsCString> flavors;
+ rv = aTransferable->FlavorsTransferableCanExport(flavors);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aResolver->Callback(ContentAnalysisResult::FromNoResult(
+ NoContentAnalysisResult::ERROR_OTHER));
+ return;
+ }
+ bool keepChecking = true;
+ if (flavors.Contains(kFileMime)) {
+ auto fileResult = CheckClipboardContentAnalysisAsFile(
+ innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable);
+
+ if (fileResult.isErr()) {
+ aResolver->Callback(
+ ContentAnalysisResult::FromNoResult(fileResult.unwrapErr()));
+ return;
+ }
+ keepChecking = !fileResult.unwrap();
+ }
+ if (keepChecking) {
+ // Failed to get the clipboard data as a file, so try as text
+ auto textResult = CheckClipboardContentAnalysisAsText(
+ innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable);
+ if (textResult.isErr()) {
+ aResolver->Callback(
+ ContentAnalysisResult::FromNoResult(textResult.unwrapErr()));
+ return;
+ }
+ if (!textResult.unwrap()) {
+ // Couldn't get file or text data from this
+ aResolver->Callback(ContentAnalysisResult::FromNoResult(
+ NoContentAnalysisResult::ERROR_COULD_NOT_GET_DATA));
+ return;
+ }
+ }
+}
+
+static bool CheckClipboardContentAnalysisSync(
+ mozilla::dom::WindowGlobalParent* aWindow,
+ const nsCOMPtr<nsITransferable>& trans) {
+ bool requestDone = false;
+ RefPtr<nsIContentAnalysisResult> result;
+ auto callback = mozilla::MakeRefPtr<SafeContentAnalysisResultCallback>(
+ [&requestDone, &result](RefPtr<nsIContentAnalysisResult>&& aResult) {
+ result = std::move(aResult);
+ requestDone = true;
+ });
+ CheckClipboardContentAnalysis(aWindow, trans, callback);
+ mozilla::SpinEventLoopUntil("CheckClipboardContentAnalysisSync"_ns,
+ [&requestDone]() -> bool { return requestDone; });
+ return result->GetShouldAllowContent();
+}
+
+nsBaseClipboard::nsBaseClipboard(const ClipboardCapabilities& aClipboardCaps)
+ : mClipboardCaps(aClipboardCaps) {
+ using mozilla::MakeUnique;
+ // Initialize clipboard cache.
+ mCaches[kGlobalClipboard] = MakeUnique<ClipboardCache>();
+ if (mClipboardCaps.supportsSelectionClipboard()) {
+ mCaches[kSelectionClipboard] = MakeUnique<ClipboardCache>();
+ }
+ if (mClipboardCaps.supportsFindClipboard()) {
+ mCaches[kFindClipboard] = MakeUnique<ClipboardCache>();
+ }
+ if (mClipboardCaps.supportsSelectionCache()) {
+ mCaches[kSelectionCache] = MakeUnique<ClipboardCache>();
+ }
+}
+
+nsBaseClipboard::~nsBaseClipboard() {
+ for (auto& request : mPendingWriteRequests) {
+ if (request) {
+ request->Abort(NS_ERROR_ABORT);
+ request = nullptr;
+ }
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsBaseClipboard, nsIClipboard)
+
+/**
+ * Sets the transferable object
+ *
+ */
+NS_IMETHODIMP nsBaseClipboard::SetData(nsITransferable* aTransferable,
+ nsIClipboardOwner* aOwner,
+ int32_t aWhichClipboard) {
+ NS_ASSERTION(aTransferable, "clipboard given a null transferable");
+
+ MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
+
+ if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
+ aWhichClipboard);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ nsTArray<nsCString> flavors;
+ if (NS_SUCCEEDED(aTransferable->FlavorsTransferableCanImport(flavors))) {
+ for (const auto& flavor : flavors) {
+ MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get());
+ }
+ }
+ }
+
+ const auto& clipboardCache = mCaches[aWhichClipboard];
+ MOZ_ASSERT(clipboardCache);
+ if (aTransferable == clipboardCache->GetTransferable() &&
+ aOwner == clipboardCache->GetClipboardOwner()) {
+ MOZ_CLIPBOARD_LOG("%s: skipping update.", __FUNCTION__);
+ return NS_OK;
+ }
+
+ clipboardCache->Clear();
+
+ nsresult rv = NS_ERROR_FAILURE;
+ if (aTransferable) {
+ mIgnoreEmptyNotification = true;
+ // Reject existing pending asyncSetData request if any.
+ RejectPendingAsyncSetDataRequestIfAny(aWhichClipboard);
+ rv = SetNativeClipboardData(aTransferable, aWhichClipboard);
+ mIgnoreEmptyNotification = false;
+ }
+ if (NS_FAILED(rv)) {
+ MOZ_CLIPBOARD_LOG("%s: setting native clipboard data failed.",
+ __FUNCTION__);
+ return rv;
+ }
+
+ auto result = GetNativeClipboardSequenceNumber(aWhichClipboard);
+ if (result.isErr()) {
+ MOZ_CLIPBOARD_LOG("%s: getting native clipboard change count failed.",
+ __FUNCTION__);
+ return result.unwrapErr();
+ }
+
+ clipboardCache->Update(aTransferable, aOwner, result.unwrap());
+ return NS_OK;
+}
+
+nsresult nsBaseClipboard::GetDataFromClipboardCache(
+ nsITransferable* aTransferable, int32_t aClipboardType) {
+ MOZ_ASSERT(aTransferable);
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+ MOZ_ASSERT(mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled());
+
+ const auto* clipboardCache = GetClipboardCacheIfValid(aClipboardType);
+ if (!clipboardCache) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return clipboardCache->GetData(aTransferable);
+}
+
+/**
+ * Gets the transferable object from system clipboard.
+ */
+NS_IMETHODIMP nsBaseClipboard::GetData(
+ nsITransferable* aTransferable, int32_t aWhichClipboard,
+ mozilla::dom::WindowContext* aWindowContext) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
+
+ if (!aTransferable) {
+ NS_ASSERTION(false, "clipboard given a null transferable");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
+ aWhichClipboard);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) {
+ // If we were the last ones to put something on the native clipboard, then
+ // just use the cached transferable. Otherwise clear it because it isn't
+ // relevant any more.
+ if (NS_SUCCEEDED(
+ GetDataFromClipboardCache(aTransferable, aWhichClipboard))) {
+ // maybe try to fill in more types? Is there a point?
+ if (!CheckClipboardContentAnalysisSync(aWindowContext->Canonical(),
+ aTransferable)) {
+ aTransferable->ClearAllData();
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ return NS_OK;
+ }
+
+ // at this point we can't satisfy the request from cache data so let's look
+ // for things other people put on the system clipboard
+ }
+ nsresult rv = GetNativeClipboardData(aTransferable, aWhichClipboard);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!CheckClipboardContentAnalysisSync(aWindowContext->Canonical(),
+ aTransferable)) {
+ aTransferable->ClearAllData();
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+ return NS_OK;
+}
+
+void nsBaseClipboard::MaybeRetryGetAvailableFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ nsIAsyncClipboardGetCallback* aCallback, int32_t aRetryCount,
+ mozilla::dom::WindowContext* aRequestingWindowContext) {
+ // Note we have to get the clipboard sequence number first before the actual
+ // read. This is to use it to verify the clipboard data is still the one we
+ // try to read, instead of the later state.
+ auto sequenceNumberOrError =
+ GetNativeClipboardSequenceNumber(aWhichClipboard);
+ if (sequenceNumberOrError.isErr()) {
+ MOZ_CLIPBOARD_LOG("%s: unable to get sequence number for clipboard %d.",
+ __FUNCTION__, aWhichClipboard);
+ aCallback->OnError(sequenceNumberOrError.unwrapErr());
+ return;
+ }
+
+ int32_t sequenceNumber = sequenceNumberOrError.unwrap();
+ AsyncHasNativeClipboardDataMatchingFlavors(
+ aFlavorList, aWhichClipboard,
+ [self = RefPtr{this}, callback = nsCOMPtr{aCallback}, aWhichClipboard,
+ aRetryCount, flavorList = aFlavorList.Clone(), sequenceNumber,
+ requestingWindowContext =
+ RefPtr{aRequestingWindowContext}](auto aFlavorsOrError) {
+ if (aFlavorsOrError.isErr()) {
+ MOZ_CLIPBOARD_LOG(
+ "%s: unable to get available flavors for clipboard %d.",
+ __FUNCTION__, aWhichClipboard);
+ callback->OnError(aFlavorsOrError.unwrapErr());
+ return;
+ }
+
+ auto sequenceNumberOrError =
+ self->GetNativeClipboardSequenceNumber(aWhichClipboard);
+ if (sequenceNumberOrError.isErr()) {
+ MOZ_CLIPBOARD_LOG(
+ "%s: unable to get sequence number for clipboard %d.",
+ __FUNCTION__, aWhichClipboard);
+ callback->OnError(sequenceNumberOrError.unwrapErr());
+ return;
+ }
+
+ if (sequenceNumber == sequenceNumberOrError.unwrap()) {
+ auto asyncGetClipboardData =
+ mozilla::MakeRefPtr<AsyncGetClipboardData>(
+ aWhichClipboard, sequenceNumber,
+ std::move(aFlavorsOrError.unwrap()), false, self,
+ requestingWindowContext);
+ callback->OnSuccess(asyncGetClipboardData);
+ return;
+ }
+
+ if (aRetryCount > 0) {
+ MOZ_CLIPBOARD_LOG(
+ "%s: clipboard=%d, ignore the data due to the sequence number "
+ "doesn't match, retry (%d) ..",
+ __FUNCTION__, aWhichClipboard, aRetryCount);
+ self->MaybeRetryGetAvailableFlavors(flavorList, aWhichClipboard,
+ callback, aRetryCount - 1,
+ requestingWindowContext);
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(false, "How can this happen?!?");
+ callback->OnError(NS_ERROR_FAILURE);
+ });
+}
+
+NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ mozilla::dom::WindowContext* aRequestingWindowContext,
+ nsIPrincipal* aRequestingPrincipal,
+ nsIAsyncClipboardGetCallback* aCallback) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
+
+ if (!aCallback || !aRequestingPrincipal || aFlavorList.IsEmpty()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
+ aWhichClipboard);
+ return NS_ERROR_FAILURE;
+ }
+
+ // We want to disable security check for automated tests that have the pref
+ // set to true, or extension that have clipboard read permission.
+ if (mozilla::StaticPrefs::
+ dom_events_testing_asyncClipboard_DoNotUseDirectly() ||
+ nsContentUtils::PrincipalHasPermission(*aRequestingPrincipal,
+ nsGkAtoms::clipboardRead)) {
+ AsyncGetDataInternal(aFlavorList, aWhichClipboard, aRequestingWindowContext,
+ aCallback);
+ return NS_OK;
+ }
+
+ // If cache data is valid, we are the last ones to put something on the native
+ // clipboard, then check if the data is from the same-origin page,
+ if (auto* clipboardCache = GetClipboardCacheIfValid(aWhichClipboard)) {
+ nsCOMPtr<nsITransferable> trans = clipboardCache->GetTransferable();
+ MOZ_ASSERT(trans);
+
+ if (nsCOMPtr<nsIPrincipal> principal = trans->GetRequestingPrincipal()) {
+ if (aRequestingPrincipal->Subsumes(principal)) {
+ MOZ_CLIPBOARD_LOG("%s: native clipboard data is from same-origin page.",
+ __FUNCTION__);
+ AsyncGetDataInternal(aFlavorList, aWhichClipboard,
+ aRequestingWindowContext, aCallback);
+ return NS_OK;
+ }
+ }
+ }
+
+ // TODO: enable showing the "Paste" button in this case; see bug 1773681.
+ if (aRequestingPrincipal->GetIsAddonOrExpandedAddonPrincipal()) {
+ MOZ_CLIPBOARD_LOG("%s: Addon without read permission.", __FUNCTION__);
+ return aCallback->OnError(NS_ERROR_FAILURE);
+ }
+
+ RequestUserConfirmation(aWhichClipboard, aFlavorList,
+ aRequestingWindowContext, aRequestingPrincipal,
+ aCallback);
+ return NS_OK;
+}
+
+void nsBaseClipboard::AsyncGetDataInternal(
+ const nsTArray<nsCString>& aFlavorList, int32_t aClipboardType,
+ mozilla::dom::WindowContext* aRequestingWindowContext,
+ nsIAsyncClipboardGetCallback* aCallback) {
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+
+ if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) {
+ // If we were the last ones to put something on the native clipboard, then
+ // just use the cached transferable. Otherwise clear it because it isn't
+ // relevant any more.
+ if (auto* clipboardCache = GetClipboardCacheIfValid(aClipboardType)) {
+ nsITransferable* cachedTransferable = clipboardCache->GetTransferable();
+ MOZ_ASSERT(cachedTransferable);
+
+ nsTArray<nsCString> transferableFlavors;
+ if (NS_SUCCEEDED(cachedTransferable->FlavorsTransferableCanExport(
+ transferableFlavors))) {
+ nsTArray<nsCString> results;
+ for (const auto& transferableFlavor : transferableFlavors) {
+ for (const auto& flavor : aFlavorList) {
+ // XXX We need special check for image as we always put the
+ // image as "native" on the clipboard.
+ if (transferableFlavor.Equals(flavor) ||
+ (transferableFlavor.Equals(kNativeImageMime) &&
+ nsContentUtils::IsFlavorImage(flavor))) {
+ MOZ_CLIPBOARD_LOG(" has %s", flavor.get());
+ results.AppendElement(flavor);
+ }
+ }
+ }
+
+ // XXX Do we need to check system clipboard for the flavors that cannot
+ // be found in cache?
+ auto asyncGetClipboardData = mozilla::MakeRefPtr<AsyncGetClipboardData>(
+ aClipboardType, clipboardCache->GetSequenceNumber(),
+ std::move(results), true, this, aRequestingWindowContext);
+ aCallback->OnSuccess(asyncGetClipboardData);
+ return;
+ }
+ }
+
+ // At this point we can't satisfy the request from cache data so let's look
+ // for things other people put on the system clipboard.
+ }
+
+ MaybeRetryGetAvailableFlavors(aFlavorList, aClipboardType, aCallback,
+ kGetAvailableFlavorsRetryCount,
+ aRequestingWindowContext);
+}
+
+NS_IMETHODIMP nsBaseClipboard::EmptyClipboard(int32_t aWhichClipboard) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
+
+ if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
+ aWhichClipboard);
+ return NS_ERROR_FAILURE;
+ }
+
+ EmptyNativeClipboardData(aWhichClipboard);
+
+ const auto& clipboardCache = mCaches[aWhichClipboard];
+ MOZ_ASSERT(clipboardCache);
+
+ if (mIgnoreEmptyNotification) {
+ MOZ_DIAGNOSTIC_ASSERT(!clipboardCache->GetTransferable() &&
+ !clipboardCache->GetClipboardOwner() &&
+ clipboardCache->GetSequenceNumber() == -1,
+ "How did we have data in clipboard cache here?");
+ return NS_OK;
+ }
+
+ clipboardCache->Clear();
+
+ return NS_OK;
+}
+
+mozilla::Result<nsTArray<nsCString>, nsresult>
+nsBaseClipboard::GetFlavorsFromClipboardCache(int32_t aClipboardType) {
+ MOZ_ASSERT(mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled());
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+
+ const auto* clipboardCache = GetClipboardCacheIfValid(aClipboardType);
+ if (!clipboardCache) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+
+ nsITransferable* cachedTransferable = clipboardCache->GetTransferable();
+ MOZ_ASSERT(cachedTransferable);
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = cachedTransferable->FlavorsTransferableCanExport(flavors);
+ if (NS_FAILED(rv)) {
+ return mozilla::Err(rv);
+ }
+
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ MOZ_CLIPBOARD_LOG(" Cached transferable types (nums %zu)\n",
+ flavors.Length());
+ for (const auto& flavor : flavors) {
+ MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get());
+ }
+ }
+
+ return std::move(flavors);
+}
+
+NS_IMETHODIMP
+nsBaseClipboard::HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList,
+ int32_t aWhichClipboard,
+ bool* aOutResult) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ MOZ_CLIPBOARD_LOG(" Asking for content clipboard=%i:\n",
+ aWhichClipboard);
+ for (const auto& flavor : aFlavorList) {
+ MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get());
+ }
+ }
+
+ *aOutResult = false;
+
+ if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) {
+ // First, check if we have valid data in our cached transferable.
+ auto flavorsOrError = GetFlavorsFromClipboardCache(aWhichClipboard);
+ if (flavorsOrError.isOk()) {
+ for (const auto& transferableFlavor : flavorsOrError.unwrap()) {
+ for (const auto& flavor : aFlavorList) {
+ if (transferableFlavor.Equals(flavor)) {
+ MOZ_CLIPBOARD_LOG(" has %s", flavor.get());
+ *aOutResult = true;
+ return NS_OK;
+ }
+ }
+ }
+ }
+ }
+
+ auto resultOrError =
+ HasNativeClipboardDataMatchingFlavors(aFlavorList, aWhichClipboard);
+ if (resultOrError.isErr()) {
+ MOZ_CLIPBOARD_LOG(
+ "%s: checking native clipboard data matching flavors falied.",
+ __FUNCTION__);
+ return resultOrError.unwrapErr();
+ }
+
+ *aOutResult = resultOrError.unwrap();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseClipboard::IsClipboardTypeSupported(int32_t aWhichClipboard,
+ bool* aRetval) {
+ NS_ENSURE_ARG_POINTER(aRetval);
+ switch (aWhichClipboard) {
+ case kGlobalClipboard:
+ // We always support the global clipboard.
+ *aRetval = true;
+ return NS_OK;
+ case kSelectionClipboard:
+ *aRetval = mClipboardCaps.supportsSelectionClipboard();
+ return NS_OK;
+ case kFindClipboard:
+ *aRetval = mClipboardCaps.supportsFindClipboard();
+ return NS_OK;
+ case kSelectionCache:
+ *aRetval = mClipboardCaps.supportsSelectionCache();
+ return NS_OK;
+ default:
+ *aRetval = false;
+ return NS_OK;
+ }
+}
+
+void nsBaseClipboard::AsyncHasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ HasMatchingFlavorsCallback&& aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ MOZ_CLIPBOARD_LOG(
+ "nsBaseClipboard::AsyncHasNativeClipboardDataMatchingFlavors: "
+ "clipboard=%d",
+ aWhichClipboard);
+
+ nsTArray<nsCString> results;
+ for (const auto& flavor : aFlavorList) {
+ auto resultOrError = HasNativeClipboardDataMatchingFlavors(
+ AutoTArray<nsCString, 1>{flavor}, aWhichClipboard);
+ if (resultOrError.isOk() && resultOrError.unwrap()) {
+ results.AppendElement(flavor);
+ }
+ }
+ aCallback(std::move(results));
+}
+
+void nsBaseClipboard::AsyncGetNativeClipboardData(
+ nsITransferable* aTransferable, int32_t aWhichClipboard,
+ GetDataCallback&& aCallback) {
+ aCallback(GetNativeClipboardData(aTransferable, aWhichClipboard));
+}
+
+void nsBaseClipboard::ClearClipboardCache(int32_t aClipboardType) {
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+ const mozilla::UniquePtr<ClipboardCache>& cache = mCaches[aClipboardType];
+ MOZ_ASSERT(cache);
+ cache->Clear();
+}
+
+void nsBaseClipboard::RequestUserConfirmation(
+ int32_t aClipboardType, const nsTArray<nsCString>& aFlavorList,
+ mozilla::dom::WindowContext* aWindowContext,
+ nsIPrincipal* aRequestingPrincipal,
+ nsIAsyncClipboardGetCallback* aCallback) {
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+ MOZ_ASSERT(aCallback);
+
+ if (!aWindowContext) {
+ aCallback->OnError(NS_ERROR_FAILURE);
+ return;
+ }
+
+ CanonicalBrowsingContext* cbc =
+ CanonicalBrowsingContext::Cast(aWindowContext->GetBrowsingContext());
+ MOZ_ASSERT(
+ cbc->IsContent(),
+ "Should not require user confirmation when access from chrome window");
+
+ RefPtr<CanonicalBrowsingContext> chromeTop = cbc->TopCrossChromeBoundary();
+ Document* chromeDoc = chromeTop ? chromeTop->GetDocument() : nullptr;
+ if (!chromeDoc || !chromeDoc->HasFocus(mozilla::IgnoreErrors())) {
+ MOZ_CLIPBOARD_LOG("%s: reject due to not in the focused window",
+ __FUNCTION__);
+ aCallback->OnError(NS_ERROR_FAILURE);
+ return;
+ }
+
+ mozilla::dom::Element* activeElementInChromeDoc =
+ chromeDoc->GetActiveElement();
+ if (activeElementInChromeDoc != cbc->Top()->GetEmbedderElement()) {
+ // Reject if the request is not from web content that is in the focused tab.
+ MOZ_CLIPBOARD_LOG("%s: reject due to not in the focused tab", __FUNCTION__);
+ aCallback->OnError(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // If there is a pending user confirmation request, check if we could reuse
+ // it. If not, reject the request.
+ if (sUserConfirmationRequest) {
+ if (sUserConfirmationRequest->IsEqual(
+ aClipboardType, chromeDoc, aRequestingPrincipal, aWindowContext)) {
+ sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback);
+ return;
+ }
+
+ aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ return;
+ }
+
+ nsresult rv = NS_ERROR_FAILURE;
+ nsCOMPtr<nsIPromptService> promptService =
+ do_GetService("@mozilla.org/prompter;1", &rv);
+ if (NS_FAILED(rv)) {
+ aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ return;
+ }
+
+ RefPtr<mozilla::dom::Promise> promise;
+ if (NS_FAILED(promptService->ConfirmUserPaste(aWindowContext->Canonical(),
+ getter_AddRefs(promise)))) {
+ aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ return;
+ }
+
+ sUserConfirmationRequest = new UserConfirmationRequest(
+ aClipboardType, chromeDoc, aRequestingPrincipal, this, aWindowContext);
+ sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback);
+ promise->AppendNativeHandler(sUserConfirmationRequest);
+}
+
+NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncGetClipboardData,
+ nsIAsyncGetClipboardData)
+
+nsBaseClipboard::AsyncGetClipboardData::AsyncGetClipboardData(
+ int32_t aClipboardType, int32_t aSequenceNumber,
+ nsTArray<nsCString>&& aFlavors, bool aFromCache,
+ nsBaseClipboard* aClipboard,
+ mozilla::dom::WindowContext* aRequestingWindowContext)
+ : mClipboardType(aClipboardType),
+ mSequenceNumber(aSequenceNumber),
+ mFlavors(std::move(aFlavors)),
+ mFromCache(aFromCache),
+ mClipboard(aClipboard),
+ mRequestingWindowContext(aRequestingWindowContext) {
+ MOZ_ASSERT(mClipboard);
+ MOZ_ASSERT(
+ mClipboard->nsIClipboard::IsClipboardTypeSupported(mClipboardType));
+}
+
+NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetValid(
+ bool* aOutResult) {
+ *aOutResult = IsValid();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetFlavorList(
+ nsTArray<nsCString>& aFlavors) {
+ aFlavors.AppendElements(mFlavors);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetData(
+ nsITransferable* aTransferable,
+ nsIAsyncClipboardRequestCallback* aCallback) {
+ MOZ_CLIPBOARD_LOG("AsyncGetClipboardData::GetData: %p", this);
+
+ if (!aTransferable || !aCallback) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // If the requested flavor is not in the list, throw an error.
+ for (const auto& flavor : flavors) {
+ if (!mFlavors.Contains(flavor)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (!IsValid()) {
+ aCallback->OnComplete(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mClipboard);
+
+ auto contentAnalysisCallback =
+ mozilla::MakeRefPtr<SafeContentAnalysisResultCallback>(
+ [transferable = nsCOMPtr{aTransferable},
+ callback = nsCOMPtr{aCallback}](
+ RefPtr<nsIContentAnalysisResult>&& aResult) {
+ if (aResult->GetShouldAllowContent()) {
+ callback->OnComplete(NS_OK);
+ } else {
+ transferable->ClearAllData();
+ callback->OnComplete(NS_ERROR_CONTENT_BLOCKED);
+ }
+ });
+
+ if (mFromCache) {
+ const auto* clipboardCache =
+ mClipboard->GetClipboardCacheIfValid(mClipboardType);
+ // `IsValid()` above ensures we should get a valid cache and matched
+ // sequence number here.
+ MOZ_DIAGNOSTIC_ASSERT(clipboardCache);
+ MOZ_DIAGNOSTIC_ASSERT(clipboardCache->GetSequenceNumber() ==
+ mSequenceNumber);
+ if (NS_SUCCEEDED(clipboardCache->GetData(aTransferable))) {
+ CheckClipboardContentAnalysis(mRequestingWindowContext
+ ? mRequestingWindowContext->Canonical()
+ : nullptr,
+ aTransferable, contentAnalysisCallback);
+ return NS_OK;
+ }
+
+ // At this point we can't satisfy the request from cache data so let's look
+ // for things other people put on the system clipboard.
+ }
+
+ // Since this is an async operation, we need to check if the data is still
+ // valid after we get the result.
+ mClipboard->AsyncGetNativeClipboardData(
+ aTransferable, mClipboardType,
+ [callback = nsCOMPtr{aCallback}, self = RefPtr{this},
+ transferable = nsCOMPtr{aTransferable},
+ contentAnalysisCallback =
+ std::move(contentAnalysisCallback)](nsresult aResult) mutable {
+ if (NS_FAILED(aResult)) {
+ callback->OnComplete(aResult);
+ return;
+ }
+ // `IsValid()` checks the clipboard sequence number to ensure the data
+ // we are requesting is still valid.
+ if (!self->IsValid()) {
+ callback->OnComplete(NS_ERROR_FAILURE);
+ return;
+ }
+ CheckClipboardContentAnalysis(
+ self->mRequestingWindowContext
+ ? self->mRequestingWindowContext->Canonical()
+ : nullptr,
+ transferable, contentAnalysisCallback);
+ });
+ return NS_OK;
+}
+
+bool nsBaseClipboard::AsyncGetClipboardData::IsValid() {
+ if (!mClipboard) {
+ return false;
+ }
+
+ // If the data should from cache, check if cache is still valid or the
+ // sequence numbers are matched.
+ if (mFromCache) {
+ const auto* clipboardCache =
+ mClipboard->GetClipboardCacheIfValid(mClipboardType);
+ if (!clipboardCache) {
+ mClipboard = nullptr;
+ return false;
+ }
+
+ return mSequenceNumber == clipboardCache->GetSequenceNumber();
+ }
+
+ auto resultOrError =
+ mClipboard->GetNativeClipboardSequenceNumber(mClipboardType);
+ if (resultOrError.isErr()) {
+ mClipboard = nullptr;
+ return false;
+ }
+
+ if (mSequenceNumber != resultOrError.unwrap()) {
+ mClipboard = nullptr;
+ return false;
+ }
+
+ return true;
+}
+
+nsBaseClipboard::ClipboardCache* nsBaseClipboard::GetClipboardCacheIfValid(
+ int32_t aClipboardType) {
+ MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
+
+ const mozilla::UniquePtr<ClipboardCache>& cache = mCaches[aClipboardType];
+ MOZ_ASSERT(cache);
+
+ if (!cache->GetTransferable()) {
+ MOZ_ASSERT(cache->GetSequenceNumber() == -1);
+ return nullptr;
+ }
+
+ auto changeCountOrError = GetNativeClipboardSequenceNumber(aClipboardType);
+ if (changeCountOrError.isErr()) {
+ return nullptr;
+ }
+
+ if (changeCountOrError.unwrap() != cache->GetSequenceNumber()) {
+ // Clipboard cache is invalid, clear it.
+ cache->Clear();
+ return nullptr;
+ }
+
+ return cache.get();
+}
+
+void nsBaseClipboard::ClipboardCache::Clear() {
+ if (mClipboardOwner) {
+ mClipboardOwner->LosingOwnership(mTransferable);
+ mClipboardOwner = nullptr;
+ }
+ mTransferable = nullptr;
+ mSequenceNumber = -1;
+}
+
+nsresult nsBaseClipboard::ClipboardCache::GetData(
+ nsITransferable* aTransferable) const {
+ MOZ_ASSERT(aTransferable);
+ MOZ_ASSERT(mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled());
+
+ // get flavor list that includes all acceptable flavors (including ones
+ // obtained through conversion)
+ nsTArray<nsCString> flavors;
+ if (NS_FAILED(aTransferable->FlavorsTransferableCanImport(flavors))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(mTransferable);
+ for (const auto& flavor : flavors) {
+ nsCOMPtr<nsISupports> dataSupports;
+ // XXX Maybe we need special check for image as we always put the image as
+ // "native" on the clipboard.
+ if (NS_SUCCEEDED(mTransferable->GetTransferData(
+ flavor.get(), getter_AddRefs(dataSupports)))) {
+ MOZ_CLIPBOARD_LOG("%s: getting %s from cache.", __FUNCTION__,
+ flavor.get());
+ aTransferable->SetTransferData(flavor.get(), dataSupports);
+ // XXX we only read the first available type from native clipboard, so
+ // make cache behave the same.
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
diff --git a/widget/nsBaseClipboard.h b/widget/nsBaseClipboard.h
new file mode 100644
index 0000000000..8f90be725a
--- /dev/null
+++ b/widget/nsBaseClipboard.h
@@ -0,0 +1,220 @@
+/* -*- 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/. */
+
+#ifndef nsBaseClipboard_h__
+#define nsBaseClipboard_h__
+
+#include "mozilla/dom/PContent.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MoveOnlyFunction.h"
+#include "mozilla/Result.h"
+#include "nsIClipboard.h"
+#include "nsITransferable.h"
+#include "nsCOMPtr.h"
+
+static mozilla::LazyLogModule sWidgetClipboardLog("WidgetClipboard");
+#define MOZ_CLIPBOARD_LOG(...) \
+ MOZ_LOG(sWidgetClipboardLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#define MOZ_CLIPBOARD_LOG_ENABLED() \
+ MOZ_LOG_TEST(sWidgetClipboardLog, mozilla::LogLevel::Debug)
+
+class nsITransferable;
+class nsIClipboardOwner;
+class nsIPrincipal;
+class nsIWidget;
+
+namespace mozilla::dom {
+class WindowContext;
+} // namespace mozilla::dom
+
+/**
+ * A base clipboard class for all platform, so that they can share the same
+ * implementation.
+ */
+class nsBaseClipboard : public nsIClipboard {
+ public:
+ explicit nsBaseClipboard(
+ const mozilla::dom::ClipboardCapabilities& aClipboardCaps);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+
+ // nsIClipboard
+ NS_IMETHOD SetData(nsITransferable* aTransferable, nsIClipboardOwner* aOwner,
+ int32_t aWhichClipboard) override final;
+ NS_IMETHOD AsyncSetData(int32_t aWhichClipboard,
+ nsIAsyncClipboardRequestCallback* aCallback,
+ nsIAsyncSetClipboardData** _retval) override final;
+ NS_IMETHOD GetData(
+ nsITransferable* aTransferable, int32_t aWhichClipboard,
+ mozilla::dom::WindowContext* aWindowContext) override final;
+ NS_IMETHOD AsyncGetData(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ mozilla::dom::WindowContext* aRequestingWindowContext,
+ nsIPrincipal* aRequestingPrincipal,
+ nsIAsyncClipboardGetCallback* aCallback) override final;
+ NS_IMETHOD EmptyClipboard(int32_t aWhichClipboard) override final;
+ NS_IMETHOD HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList,
+ int32_t aWhichClipboard,
+ bool* aOutResult) override final;
+ NS_IMETHOD IsClipboardTypeSupported(int32_t aWhichClipboard,
+ bool* aRetval) override final;
+
+ void AsyncGetDataInternal(
+ const nsTArray<nsCString>& aFlavorList, int32_t aClipboardType,
+ mozilla::dom::WindowContext* aRequestingWindowContext,
+ nsIAsyncClipboardGetCallback* aCallback);
+
+ using GetDataCallback = mozilla::MoveOnlyFunction<void(nsresult)>;
+ using HasMatchingFlavorsCallback = mozilla::MoveOnlyFunction<void(
+ mozilla::Result<nsTArray<nsCString>, nsresult>)>;
+
+ protected:
+ virtual ~nsBaseClipboard();
+
+ // Implement the native clipboard behavior.
+ NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) = 0;
+ NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) = 0;
+ virtual void AsyncGetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard,
+ GetDataCallback&& aCallback);
+ virtual nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) = 0;
+ virtual mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber(
+ int32_t aWhichClipboard) = 0;
+ virtual mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) = 0;
+ virtual void AsyncHasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ HasMatchingFlavorsCallback&& aCallback);
+
+ void ClearClipboardCache(int32_t aClipboardType);
+
+ private:
+ void RejectPendingAsyncSetDataRequestIfAny(int32_t aClipboardType);
+
+ class AsyncSetClipboardData final : public nsIAsyncSetClipboardData {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCSETCLIPBOARDDATA
+
+ AsyncSetClipboardData(int32_t aClipboardType, nsBaseClipboard* aClipboard,
+ nsIAsyncClipboardRequestCallback* aCallback);
+
+ private:
+ virtual ~AsyncSetClipboardData() = default;
+ bool IsValid() const {
+ // If this request is no longer valid, the callback should be notified.
+ MOZ_ASSERT_IF(!mClipboard, !mCallback);
+ return !!mClipboard;
+ }
+ void MaybeNotifyCallback(nsresult aResult);
+
+ // The clipboard type defined in nsIClipboard.
+ int32_t mClipboardType;
+ // It is safe to use a raw pointer as it will be nullified (by calling
+ // NotifyCallback()) once nsBaseClipboard stops tracking us. This is
+ // also used to indicate whether this request is valid.
+ nsBaseClipboard* mClipboard;
+ // mCallback will be nullified once the callback is notified to ensure the
+ // callback is only notified once.
+ nsCOMPtr<nsIAsyncClipboardRequestCallback> mCallback;
+ };
+
+ class AsyncGetClipboardData final : public nsIAsyncGetClipboardData {
+ public:
+ AsyncGetClipboardData(
+ int32_t aClipboardType, int32_t aSequenceNumber,
+ nsTArray<nsCString>&& aFlavors, bool aFromCache,
+ nsBaseClipboard* aClipboard,
+ mozilla::dom::WindowContext* aRequestingWindowContext);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCGETCLIPBOARDDATA
+
+ private:
+ virtual ~AsyncGetClipboardData() = default;
+ bool IsValid();
+
+ // The clipboard type defined in nsIClipboard.
+ const int32_t mClipboardType;
+ // The sequence number associated with the clipboard content for this
+ // request. If it doesn't match with the current sequence number in system
+ // clipboard, this request targets stale data and is deemed invalid.
+ const int32_t mSequenceNumber;
+ // List of available data types for clipboard content.
+ const nsTArray<nsCString> mFlavors;
+ // Data should be read from cache.
+ const bool mFromCache;
+ // This is also used to indicate whether this request is still valid.
+ RefPtr<nsBaseClipboard> mClipboard;
+ // The requesting window, which is used for Content Analysis purposes.
+ RefPtr<mozilla::dom::WindowContext> mRequestingWindowContext;
+ };
+
+ class ClipboardCache final {
+ public:
+ ~ClipboardCache() {
+ // In order to notify the old clipboard owner.
+ Clear();
+ }
+
+ /**
+ * Clear the cached transferable and notify the original clipboard owner
+ * that it has lost ownership.
+ */
+ void Clear();
+ void Update(nsITransferable* aTransferable,
+ nsIClipboardOwner* aClipboardOwner, int32_t aSequenceNumber) {
+ // Clear first to notify the old clipboard owner.
+ Clear();
+ mTransferable = aTransferable;
+ mClipboardOwner = aClipboardOwner;
+ mSequenceNumber = aSequenceNumber;
+ }
+ nsITransferable* GetTransferable() const { return mTransferable; }
+ nsIClipboardOwner* GetClipboardOwner() const { return mClipboardOwner; }
+ int32_t GetSequenceNumber() const { return mSequenceNumber; }
+ nsresult GetData(nsITransferable* aTransferable) const;
+
+ private:
+ nsCOMPtr<nsITransferable> mTransferable;
+ nsCOMPtr<nsIClipboardOwner> mClipboardOwner;
+ int32_t mSequenceNumber = -1;
+ };
+
+ void MaybeRetryGetAvailableFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ nsIAsyncClipboardGetCallback* aCallback, int32_t aRetryCount,
+ mozilla::dom::WindowContext* aRequestingWindowContext);
+
+ // Return clipboard cache if the cached data is valid, otherwise clear the
+ // cached data and returns null.
+ ClipboardCache* GetClipboardCacheIfValid(int32_t aClipboardType);
+
+ mozilla::Result<nsTArray<nsCString>, nsresult> GetFlavorsFromClipboardCache(
+ int32_t aClipboardType);
+ nsresult GetDataFromClipboardCache(nsITransferable* aTransferable,
+ int32_t aClipboardType);
+
+ void RequestUserConfirmation(int32_t aClipboardType,
+ const nsTArray<nsCString>& aFlavorList,
+ mozilla::dom::WindowContext* aWindowContext,
+ nsIPrincipal* aRequestingPrincipal,
+ nsIAsyncClipboardGetCallback* aCallback);
+
+ // Track the pending request for each clipboard type separately. And only need
+ // to track the latest request for each clipboard type as the prior pending
+ // request will be canceled when a new request is made.
+ RefPtr<AsyncSetClipboardData>
+ mPendingWriteRequests[nsIClipboard::kClipboardTypeCount];
+
+ mozilla::UniquePtr<ClipboardCache> mCaches[nsIClipboard::kClipboardTypeCount];
+ const mozilla::dom::ClipboardCapabilities mClipboardCaps;
+ bool mIgnoreEmptyNotification = false;
+};
+
+#endif // nsBaseClipboard_h__
diff --git a/widget/nsBaseDragService.cpp b/widget/nsBaseDragService.cpp
new file mode 100644
index 0000000000..22ed844595
--- /dev/null
+++ b/widget/nsBaseDragService.cpp
@@ -0,0 +1,1044 @@
+/* -*- 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 "nsBaseDragService.h"
+#include "nsITransferable.h"
+
+#include "nsArrayUtils.h"
+#include "nsITransferable.h"
+#include "nsSize.h"
+#include "nsXPCOM.h"
+#include "nsCOMPtr.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIFrame.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsIContent.h"
+#include "nsViewManager.h"
+#include "nsINode.h"
+#include "nsPresContext.h"
+#include "nsIImageLoadingContent.h"
+#include "imgIContainer.h"
+#include "imgIRequest.h"
+#include "ImageRegion.h"
+#include "nsQueryObject.h"
+#include "nsRegion.h"
+#include "nsXULPopupManager.h"
+#include "nsMenuPopupFrame.h"
+#include "nsTreeBodyFrame.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/SVGImageContext.h"
+#include "mozilla/TextControlElement.h"
+#include "mozilla/Unused.h"
+#include "mozilla/ViewportUtils.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/DataTransferItemList.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/DragEvent.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/gfx/2D.h"
+#include "nsFrameLoader.h"
+#include "BrowserParent.h"
+#include "nsIMutableArray.h"
+#include "gfxContext.h"
+#include "gfxPlatform.h"
+#include "nscore.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+LazyLogModule sWidgetDragServiceLog("WidgetDragService");
+
+#define DRAGIMAGES_PREF "nglayout.enable_drag_images"
+
+nsBaseDragService::nsBaseDragService()
+ : mCanDrop(false),
+ mOnlyChromeDrop(false),
+ mDoingDrag(false),
+ mSessionIsSynthesizedForTests(false),
+ mIsDraggingTextInTextControl(false),
+ mEndingSession(false),
+ mHasImage(false),
+ mUserCancelled(false),
+ mDragEventDispatchedToChildProcess(false),
+ mDragAction(DRAGDROP_ACTION_NONE),
+ mDragActionFromChildProcess(DRAGDROP_ACTION_UNINITIALIZED),
+ mEffectAllowedForTests(DRAGDROP_ACTION_UNINITIALIZED),
+ mContentPolicyType(nsIContentPolicy::TYPE_OTHER),
+ mSuppressLevel(0),
+ mInputSource(MouseEvent_Binding::MOZ_SOURCE_MOUSE) {}
+
+nsBaseDragService::~nsBaseDragService() = default;
+
+NS_IMPL_ISUPPORTS(nsBaseDragService, nsIDragService, nsIDragSession)
+
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::SetCanDrop(bool aCanDrop) {
+ mCanDrop = aCanDrop;
+ return NS_OK;
+}
+
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::GetCanDrop(bool* aCanDrop) {
+ *aCanDrop = mCanDrop;
+ return NS_OK;
+}
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::SetOnlyChromeDrop(bool aOnlyChrome) {
+ mOnlyChromeDrop = aOnlyChrome;
+ return NS_OK;
+}
+
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::GetOnlyChromeDrop(bool* aOnlyChrome) {
+ *aOnlyChrome = mOnlyChromeDrop;
+ return NS_OK;
+}
+
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::SetDragAction(uint32_t anAction) {
+ mDragAction = anAction;
+ return NS_OK;
+}
+
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::GetDragAction(uint32_t* anAction) {
+ *anAction = mDragAction;
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsBaseDragService::GetNumDropItems(uint32_t* aNumItems) {
+ *aNumItems = 0;
+ return NS_ERROR_FAILURE;
+}
+
+//
+// GetSourceWindowContext
+//
+// Returns the window context where the drag was initiated. This will be
+// nullptr if the drag began outside of our application.
+//
+NS_IMETHODIMP
+nsBaseDragService::GetSourceWindowContext(
+ WindowContext** aSourceWindowContext) {
+ *aSourceWindowContext = mSourceWindowContext.get();
+ NS_IF_ADDREF(*aSourceWindowContext);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::SetSourceWindowContext(WindowContext* aSourceWindowContext) {
+ // This should only be called in a child process.
+ MOZ_ASSERT(!XRE_IsParentProcess());
+ mSourceWindowContext = aSourceWindowContext;
+ return NS_OK;
+}
+
+//
+// GetSourceTopWindowContext
+//
+// Returns the top-level window context where the drag was initiated. This will
+// be nullptr if the drag began outside of our application.
+//
+NS_IMETHODIMP
+nsBaseDragService::GetSourceTopWindowContext(
+ WindowContext** aSourceTopWindowContext) {
+ *aSourceTopWindowContext = mSourceTopWindowContext.get();
+ NS_IF_ADDREF(*aSourceTopWindowContext);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::SetSourceTopWindowContext(
+ WindowContext* aSourceTopWindowContext) {
+ // This should only be called in a child process.
+ MOZ_ASSERT(!XRE_IsParentProcess());
+ mSourceTopWindowContext = aSourceTopWindowContext;
+ return NS_OK;
+}
+
+//
+// GetSourceNode
+//
+// Returns the DOM node where the drag was initiated. This will be
+// nullptr if the drag began outside of our application.
+//
+NS_IMETHODIMP
+nsBaseDragService::GetSourceNode(nsINode** aSourceNode) {
+ *aSourceNode = do_AddRef(mSourceNode).take();
+ return NS_OK;
+}
+
+void nsBaseDragService::UpdateSource(nsINode* aNewSourceNode,
+ Selection* aNewSelection) {
+ MOZ_ASSERT(mSourceNode);
+ MOZ_ASSERT(aNewSourceNode);
+ MOZ_ASSERT(mSourceNode->IsInNativeAnonymousSubtree() ||
+ aNewSourceNode->IsInNativeAnonymousSubtree());
+ MOZ_ASSERT(mSourceDocument == aNewSourceNode->OwnerDoc());
+ mSourceNode = aNewSourceNode;
+ // Don't set mSelection if the session was invoked without selection or
+ // making it becomes nullptr. The latter occurs when the old frame is
+ // being destroyed.
+ if (mSelection && aNewSelection) {
+ // XXX If the dragging image is created once (e.g., at drag start), the
+ // image won't be updated unless we notify `DrawDrag` callers.
+ // However, it must be okay for now to keep using older image of
+ // Selection.
+ mSelection = aNewSelection;
+ }
+}
+
+NS_IMETHODIMP
+nsBaseDragService::GetTriggeringPrincipal(nsIPrincipal** aPrincipal) {
+ NS_IF_ADDREF(*aPrincipal = mTriggeringPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::SetTriggeringPrincipal(nsIPrincipal* aPrincipal) {
+ mTriggeringPrincipal = aPrincipal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::GetCsp(nsIContentSecurityPolicy** aCsp) {
+ NS_IF_ADDREF(*aCsp = mCsp);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::SetCsp(nsIContentSecurityPolicy* aCsp) {
+ mCsp = aCsp;
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsBaseDragService::GetData(nsITransferable* aTransferable,
+ uint32_t aItemIndex) {
+ return NS_ERROR_FAILURE;
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::IsDataFlavorSupported(const char* aDataFlavor,
+ bool* _retval) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::GetDataTransferXPCOM(DataTransfer** aDataTransfer) {
+ *aDataTransfer = mDataTransfer;
+ NS_IF_ADDREF(*aDataTransfer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::SetDataTransferXPCOM(DataTransfer* aDataTransfer) {
+ NS_ENSURE_STATE(aDataTransfer);
+ mDataTransfer = aDataTransfer;
+ return NS_OK;
+}
+
+DataTransfer* nsBaseDragService::GetDataTransfer() { return mDataTransfer; }
+
+void nsBaseDragService::SetDataTransfer(DataTransfer* aDataTransfer) {
+ mDataTransfer = aDataTransfer;
+}
+
+bool nsBaseDragService::IsSynthesizedForTests() {
+ return mSessionIsSynthesizedForTests;
+}
+
+bool nsBaseDragService::IsDraggingTextInTextControl() {
+ return mIsDraggingTextInTextControl;
+}
+
+uint32_t nsBaseDragService::GetEffectAllowedForTests() {
+ MOZ_ASSERT(mSessionIsSynthesizedForTests);
+ return mEffectAllowedForTests;
+}
+
+NS_IMETHODIMP nsBaseDragService::SetDragEndPointForTests(int32_t aScreenX,
+ int32_t aScreenY) {
+ MOZ_ASSERT(mDoingDrag);
+ MOZ_ASSERT(mSourceDocument);
+ MOZ_ASSERT(mSessionIsSynthesizedForTests);
+
+ if (!mDoingDrag || !mSourceDocument || !mSessionIsSynthesizedForTests) {
+ return NS_ERROR_FAILURE;
+ }
+ nsPresContext* pc = mSourceDocument->GetPresContext();
+ if (NS_WARN_IF(!pc)) {
+ return NS_ERROR_FAILURE;
+ }
+ auto p = LayoutDeviceIntPoint::Round(CSSIntPoint(aScreenX, aScreenY) *
+ pc->CSSToDevPixelScale());
+ // p is screen-relative, and we want them to be top-level-widget-relative.
+ if (nsCOMPtr<nsIWidget> widget = pc->GetRootWidget()) {
+ p -= widget->WidgetToScreenOffset();
+ p += widget->WidgetToTopLevelWidgetOffset();
+ }
+ SetDragEndPoint(p);
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::InvokeDragSession(
+ nsINode* aDOMNode, nsIPrincipal* aPrincipal, nsIContentSecurityPolicy* aCsp,
+ nsICookieJarSettings* aCookieJarSettings, nsIArray* aTransferableArray,
+ uint32_t aActionType,
+ nsContentPolicyType aContentPolicyType = nsIContentPolicy::TYPE_OTHER) {
+ AUTO_PROFILER_LABEL("nsBaseDragService::InvokeDragSession", OTHER);
+
+ NS_ENSURE_TRUE(aDOMNode, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
+
+ // stash the document of the dom node
+ mSourceDocument = aDOMNode->OwnerDoc();
+ mTriggeringPrincipal = aPrincipal;
+ mCsp = aCsp;
+ mSourceNode = aDOMNode;
+ mIsDraggingTextInTextControl =
+ mSourceNode->IsInNativeAnonymousSubtree() &&
+ TextControlElement::FromNodeOrNull(
+ mSourceNode->GetClosestNativeAnonymousSubtreeRootParentOrHost());
+ mContentPolicyType = aContentPolicyType;
+ mEndDragPoint = LayoutDeviceIntPoint(0, 0);
+
+ // When the mouse goes down, the selection code starts a mouse
+ // capture. However, this gets in the way of determining drag
+ // feedback for things like trees because the event coordinates
+ // are in the wrong coord system, so turn off mouse capture.
+ PresShell::ClearMouseCapture();
+
+ if (mSessionIsSynthesizedForTests) {
+ mDoingDrag = true;
+ mDragAction = aActionType;
+ mEffectAllowedForTests = aActionType;
+ return NS_OK;
+ }
+
+ // If you're hitting this, a test is causing the browser to attempt to enter
+ // the drag-drop native nested event loop, which will put the browser in a
+ // state that won't run tests properly until there's manual intervention
+ // to exit the drag-drop loop (either by moving the mouse or hitting escape),
+ // which can't be done from script since we're in the nested loop.
+ //
+ // The best way to avoid this is to catch the dragstart event on the item
+ // being dragged, and then to call preventDefault() and stopPropagating() on
+ // it.
+ if (XRE_IsParentProcess()) {
+ MOZ_ASSERT(
+ !xpc::IsInAutomation(),
+ "About to start drag-drop native loop on which will prevent later "
+ "tests from running properly.");
+ }
+
+ uint32_t length = 0;
+ mozilla::Unused << aTransferableArray->GetLength(&length);
+ if (!length) {
+ nsCOMPtr<nsIMutableArray> mutableArray =
+ do_QueryInterface(aTransferableArray);
+ if (mutableArray) {
+ // In order to be able trigger dnd, we need to have some transferable
+ // object.
+ nsCOMPtr<nsITransferable> trans =
+ do_CreateInstance("@mozilla.org/widget/transferable;1");
+ trans->Init(nullptr);
+ trans->SetRequestingPrincipal(mSourceNode->NodePrincipal());
+ trans->SetContentPolicyType(mContentPolicyType);
+ trans->SetCookieJarSettings(aCookieJarSettings);
+ mutableArray->AppendElement(trans);
+ }
+ } else {
+ for (uint32_t i = 0; i < length; ++i) {
+ nsCOMPtr<nsITransferable> trans =
+ do_QueryElementAt(aTransferableArray, i);
+ if (trans) {
+ // Set the requestingPrincipal on the transferable.
+ trans->SetRequestingPrincipal(mSourceNode->NodePrincipal());
+ trans->SetContentPolicyType(mContentPolicyType);
+ trans->SetCookieJarSettings(aCookieJarSettings);
+ }
+ }
+ }
+
+ nsresult rv = InvokeDragSessionImpl(aTransferableArray, mRegion, aActionType);
+
+ if (NS_FAILED(rv)) {
+ // Set mDoingDrag so that EndDragSession cleans up and sends the dragend
+ // event after the aborted drag.
+ mDoingDrag = true;
+ EndDragSession(true, 0);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::InvokeDragSessionWithImage(
+ nsINode* aDOMNode, nsIPrincipal* aPrincipal, nsIContentSecurityPolicy* aCsp,
+ nsICookieJarSettings* aCookieJarSettings, nsIArray* aTransferableArray,
+ uint32_t aActionType, nsINode* aImage, int32_t aImageX, int32_t aImageY,
+ DragEvent* aDragEvent, DataTransfer* aDataTransfer) {
+ NS_ENSURE_TRUE(aDragEvent, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(aDataTransfer, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
+
+ mSessionIsSynthesizedForTests =
+ aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
+ mDataTransfer = aDataTransfer;
+ mSelection = nullptr;
+ mHasImage = true;
+ mDragPopup = nullptr;
+ mImage = aImage;
+ mImageOffset = CSSIntPoint(aImageX, aImageY);
+ mDragStartData = nullptr;
+ mSourceWindowContext =
+ aDOMNode ? aDOMNode->OwnerDoc()->GetWindowContext() : nullptr;
+ mSourceTopWindowContext =
+ mSourceWindowContext ? mSourceWindowContext->TopWindowContext() : nullptr;
+
+ mScreenPosition = aDragEvent->ScreenPoint(CallerType::System);
+ mInputSource = aDragEvent->InputSource();
+
+ // If dragging within a XUL tree and no custom drag image was
+ // set, the region argument to InvokeDragSessionWithImage needs
+ // to be set to the area encompassing the selected rows of the
+ // tree to ensure that the drag feedback gets clipped to those
+ // rows. For other content, region should be null.
+ mRegion = Nothing();
+ if (aDOMNode && aDOMNode->IsContent() && !aImage) {
+ if (aDOMNode->NodeInfo()->Equals(nsGkAtoms::treechildren,
+ kNameSpaceID_XUL)) {
+ nsTreeBodyFrame* treeBody =
+ do_QueryFrame(aDOMNode->AsContent()->GetPrimaryFrame());
+ if (treeBody) {
+ mRegion = treeBody->GetSelectionRegion();
+ }
+ }
+ }
+
+ nsresult rv = InvokeDragSession(
+ aDOMNode, aPrincipal, aCsp, aCookieJarSettings, aTransferableArray,
+ aActionType, nsIContentPolicy::TYPE_INTERNAL_IMAGE);
+ mRegion = Nothing();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::InvokeDragSessionWithRemoteImage(
+ nsINode* aDOMNode, nsIPrincipal* aPrincipal, nsIContentSecurityPolicy* aCsp,
+ nsICookieJarSettings* aCookieJarSettings, nsIArray* aTransferableArray,
+ uint32_t aActionType, RemoteDragStartData* aDragStartData,
+ DragEvent* aDragEvent, DataTransfer* aDataTransfer) {
+ NS_ENSURE_TRUE(aDragEvent, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(aDataTransfer, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
+
+ mSessionIsSynthesizedForTests =
+ aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
+ mDataTransfer = aDataTransfer;
+ mSelection = nullptr;
+ mHasImage = true;
+ mDragPopup = nullptr;
+ mImage = nullptr;
+ mDragStartData = aDragStartData;
+ mImageOffset = CSSIntPoint(0, 0);
+ mSourceWindowContext = mDragStartData->GetSourceWindowContext();
+ mSourceTopWindowContext = mDragStartData->GetSourceTopWindowContext();
+
+ mScreenPosition = aDragEvent->ScreenPoint(CallerType::System);
+ mInputSource = aDragEvent->InputSource();
+
+ nsresult rv = InvokeDragSession(
+ aDOMNode, aPrincipal, aCsp, aCookieJarSettings, aTransferableArray,
+ aActionType, nsIContentPolicy::TYPE_INTERNAL_IMAGE);
+ mRegion = Nothing();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::InvokeDragSessionWithSelection(
+ Selection* aSelection, nsIPrincipal* aPrincipal,
+ nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings,
+ nsIArray* aTransferableArray, uint32_t aActionType, DragEvent* aDragEvent,
+ DataTransfer* aDataTransfer) {
+ NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(aDragEvent, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
+
+ mSessionIsSynthesizedForTests =
+ aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
+ mDataTransfer = aDataTransfer;
+ mSelection = aSelection;
+ mHasImage = true;
+ mDragPopup = nullptr;
+ mImage = nullptr;
+ mImageOffset = CSSIntPoint();
+ mDragStartData = nullptr;
+ mRegion = Nothing();
+
+ mScreenPosition.x = aDragEvent->ScreenX(CallerType::System);
+ mScreenPosition.y = aDragEvent->ScreenY(CallerType::System);
+ mInputSource = aDragEvent->InputSource();
+
+ // just get the focused node from the selection
+ // XXXndeakin this should actually be the deepest node that contains both
+ // endpoints of the selection
+ nsCOMPtr<nsINode> node = aSelection->GetFocusNode();
+ mSourceWindowContext = node ? node->OwnerDoc()->GetWindowContext() : nullptr;
+ mSourceTopWindowContext =
+ mSourceWindowContext ? mSourceWindowContext->TopWindowContext() : nullptr;
+
+ return InvokeDragSession(node, aPrincipal, aCsp, aCookieJarSettings,
+ aTransferableArray, aActionType,
+ nsIContentPolicy::TYPE_OTHER);
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::GetCurrentSession(nsIDragSession** aSession) {
+ if (!aSession) return NS_ERROR_INVALID_ARG;
+
+ // "this" also implements a drag session, so say we are one but only
+ // if there is currently a drag going on.
+ if (!mSuppressLevel && mDoingDrag) {
+ *aSession = this;
+ NS_ADDREF(*aSession); // addRef because we're a "getter"
+ } else
+ *aSession = nullptr;
+
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::StartDragSession() {
+ if (mDoingDrag) {
+ return NS_ERROR_FAILURE;
+ }
+ mDoingDrag = true;
+ // By default dispatch drop also to content.
+ mOnlyChromeDrop = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseDragService::StartDragSessionForTests(
+ uint32_t aAllowedEffect) {
+ if (NS_WARN_IF(NS_FAILED(StartDragSession()))) {
+ return NS_ERROR_FAILURE;
+ }
+ mDragAction = aAllowedEffect;
+ mEffectAllowedForTests = aAllowedEffect;
+ mSessionIsSynthesizedForTests = true;
+ return NS_OK;
+}
+
+void nsBaseDragService::OpenDragPopup() {
+ if (mDragPopup) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ pm->ShowPopupAtScreen(mDragPopup, mScreenPosition.x - mImageOffset.x,
+ mScreenPosition.y - mImageOffset.y, false, nullptr);
+ }
+ }
+}
+
+int32_t nsBaseDragService::TakeChildProcessDragAction() {
+ // If the last event was dispatched to the child process, use the drag action
+ // assigned from it instead and return it. DRAGDROP_ACTION_UNINITIALIZED is
+ // returned otherwise.
+ int32_t retval = DRAGDROP_ACTION_UNINITIALIZED;
+ if (TakeDragEventDispatchedToChildProcess() &&
+ mDragActionFromChildProcess != DRAGDROP_ACTION_UNINITIALIZED) {
+ retval = mDragActionFromChildProcess;
+ }
+
+ return retval;
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsBaseDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
+ if (!mDoingDrag || mEndingSession) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mEndingSession = true;
+
+ if (aDoneDrag && !mSuppressLevel) {
+ FireDragEventAtSource(eDragEnd, aKeyModifiers);
+ }
+
+ if (mDragPopup) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ pm->HidePopup(mDragPopup, {HidePopupOption::DeselectMenu});
+ }
+ }
+
+ uint32_t dropEffect = nsIDragService::DRAGDROP_ACTION_NONE;
+ if (mDataTransfer) {
+ dropEffect = mDataTransfer->DropEffectInt();
+ }
+
+ for (uint32_t i = 0; i < mChildProcesses.Length(); ++i) {
+ mozilla::Unused << mChildProcesses[i]->SendEndDragSession(
+ aDoneDrag, mUserCancelled, mEndDragPoint, aKeyModifiers, dropEffect);
+ // Continue sending input events with input priority when stopping the dnd
+ // session.
+ mChildProcesses[i]->SetInputPriorityEventEnabled(true);
+ }
+ mChildProcesses.Clear();
+
+ // mDataTransfer and the items it owns are going to die anyway, but we
+ // explicitly deref the contained data here so that we don't have to wait for
+ // CC to reclaim the memory.
+ if (XRE_IsParentProcess()) {
+ DiscardInternalTransferData();
+ }
+
+ mDoingDrag = false;
+ mSessionIsSynthesizedForTests = false;
+ mIsDraggingTextInTextControl = false;
+ mEffectAllowedForTests = nsIDragService::DRAGDROP_ACTION_UNINITIALIZED;
+ mEndingSession = false;
+ mCanDrop = false;
+
+ // release the source we've been holding on to.
+ mSourceDocument = nullptr;
+ mSourceNode = nullptr;
+ mSourceWindowContext = nullptr;
+ mSourceTopWindowContext = nullptr;
+ mTriggeringPrincipal = nullptr;
+ mCsp = nullptr;
+ mSelection = nullptr;
+ mDataTransfer = nullptr;
+ mHasImage = false;
+ mUserCancelled = false;
+ mDragPopup = nullptr;
+ mDragStartData = nullptr;
+ mImage = nullptr;
+ mImageOffset = CSSIntPoint();
+ mScreenPosition = CSSIntPoint();
+ mEndDragPoint = LayoutDeviceIntPoint(0, 0);
+ mInputSource = MouseEvent_Binding::MOZ_SOURCE_MOUSE;
+ mRegion = Nothing();
+
+ return NS_OK;
+}
+
+void nsBaseDragService::DiscardInternalTransferData() {
+ if (mDataTransfer && mSourceNode) {
+ MOZ_ASSERT(mDataTransfer);
+
+ DataTransferItemList* items = mDataTransfer->Items();
+ for (size_t i = 0; i < items->Length(); i++) {
+ bool found;
+ DataTransferItem* item = items->IndexedGetter(i, found);
+
+ // Non-OTHER items may still be needed by JS. Skip them.
+ if (!found || item->Kind() != DataTransferItem::KIND_OTHER) {
+ continue;
+ }
+
+ nsCOMPtr<nsIVariant> variant = item->DataNoSecurityCheck();
+ nsCOMPtr<nsIWritableVariant> writable = do_QueryInterface(variant);
+
+ if (writable) {
+ writable->SetAsEmpty();
+ }
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsBaseDragService::FireDragEventAtSource(EventMessage aEventMessage,
+ uint32_t aKeyModifiers) {
+ if (!mSourceNode || !mSourceDocument || mSuppressLevel) {
+ return NS_OK;
+ }
+ RefPtr<PresShell> presShell = mSourceDocument->GetPresShell();
+ if (!presShell) {
+ return NS_OK;
+ }
+
+ RefPtr<nsPresContext> pc = presShell->GetPresContext();
+ nsCOMPtr<nsIWidget> widget = pc ? pc->GetRootWidget() : nullptr;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetDragEvent event(true, aEventMessage, widget);
+ event.mFlags.mIsSynthesizedForTests = mSessionIsSynthesizedForTests;
+ event.mInputSource = mInputSource;
+ if (aEventMessage == eDragEnd) {
+ event.mRefPoint = mEndDragPoint;
+ if (widget) {
+ event.mRefPoint -= widget->WidgetToTopLevelWidgetOffset();
+ }
+ event.mUserCancelled = mUserCancelled;
+ }
+ event.mModifiers = aKeyModifiers;
+
+ // Most drag events aren't able to converted to MouseEvent except to
+ // eDragStart and eDragEnd.
+ if (widget && event.CanConvertToInputData()) {
+ // Send the drag event to APZ, which needs to know about them to be
+ // able to accurately detect the end of a drag gesture.
+ widget->DispatchEventToAPZOnly(&event);
+ }
+
+ nsCOMPtr<nsIContent> content = do_QueryInterface(mSourceNode);
+ return presShell->HandleDOMEventWithTarget(content, &event, &status);
+}
+
+/* This is used by Windows and Mac to update the position of a popup being
+ * used as a drag image during the drag. This isn't used on GTK as it manages
+ * the drag popup itself.
+ */
+NS_IMETHODIMP
+nsBaseDragService::DragMoved(int32_t aX, int32_t aY) {
+ if (mDragPopup) {
+ nsIFrame* frame = mDragPopup->GetPrimaryFrame();
+ if (frame && frame->IsMenuPopupFrame()) {
+ CSSIntPoint cssPos =
+ RoundedToInt(LayoutDeviceIntPoint(aX, aY) /
+ frame->PresContext()->CSSToDevPixelScale()) -
+ mImageOffset;
+ static_cast<nsMenuPopupFrame*>(frame)->MoveTo(cssPos, true);
+ }
+ }
+
+ return NS_OK;
+}
+
+static PresShell* GetPresShellForContent(nsINode* aDOMNode) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aDOMNode);
+ if (!content) return nullptr;
+
+ RefPtr<Document> document = content->GetComposedDoc();
+ if (document) {
+ document->FlushPendingNotifications(FlushType::Display);
+ return document->GetPresShell();
+ }
+
+ return nullptr;
+}
+
+nsresult nsBaseDragService::DrawDrag(nsINode* aDOMNode,
+ const Maybe<CSSIntRegion>& aRegion,
+ CSSIntPoint aScreenPosition,
+ LayoutDeviceIntRect* aScreenDragRect,
+ RefPtr<SourceSurface>* aSurface,
+ nsPresContext** aPresContext) {
+ *aSurface = nullptr;
+ *aPresContext = nullptr;
+
+ // use a default size, in case of an error.
+ aScreenDragRect->SetRect(aScreenPosition.x - mImageOffset.x,
+ aScreenPosition.y - mImageOffset.y, 1, 1);
+
+ // if a drag image was specified, use that, otherwise, use the source node
+ nsCOMPtr<nsINode> dragNode = mImage ? mImage.get() : aDOMNode;
+
+ // get the presshell for the node being dragged. If the drag image is not in
+ // a document or has no frame, get the presshell from the source drag node
+ PresShell* presShell = GetPresShellForContent(dragNode);
+ if (!presShell && mImage) {
+ presShell = GetPresShellForContent(aDOMNode);
+ }
+ if (!presShell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aPresContext = presShell->GetPresContext();
+
+ if (mDragStartData) {
+ if (mImage) {
+ // Just clear the surface if chrome has overridden it with an image.
+ *aSurface = nullptr;
+ } else {
+ *aSurface = mDragStartData->TakeVisualization(aScreenDragRect);
+ }
+
+ mDragStartData = nullptr;
+ return NS_OK;
+ }
+
+ // convert mouse position to dev pixels of the prescontext
+ const CSSIntPoint screenPosition = aScreenPosition - mImageOffset;
+ const auto screenPoint = LayoutDeviceIntPoint::Round(
+ screenPosition * (*aPresContext)->CSSToDevPixelScale());
+ aScreenDragRect->MoveTo(screenPoint.x, screenPoint.y);
+
+ // check if drag images are disabled
+ bool enableDragImages = Preferences::GetBool(DRAGIMAGES_PREF, true);
+
+ // didn't want an image, so just set the screen rectangle to the frame size
+ if (!enableDragImages || !mHasImage) {
+ // This holds a quantity in RelativeTo{presShell->GetRootFrame(),
+ // ViewportType::Layout} space.
+ nsRect presLayoutRect;
+ if (aRegion) {
+ // if a region was specified, set the screen rectangle to the area that
+ // the region occupies
+ presLayoutRect = ToAppUnits(aRegion->GetBounds(), AppUnitsPerCSSPixel());
+ } else {
+ // otherwise, there was no region so just set the rectangle to
+ // the size of the primary frame of the content.
+ nsCOMPtr<nsIContent> content = do_QueryInterface(dragNode);
+ if (nsIFrame* frame = content->GetPrimaryFrame()) {
+ presLayoutRect = frame->GetBoundingClientRect();
+ }
+ }
+
+ LayoutDeviceRect screenVisualRect = ViewportUtils::ToScreenRelativeVisual(
+ LayoutDeviceRect::FromAppUnits(presLayoutRect,
+ (*aPresContext)->AppUnitsPerDevPixel()),
+ *aPresContext);
+ aScreenDragRect->SizeTo(screenVisualRect.Width(),
+ screenVisualRect.Height());
+ return NS_OK;
+ }
+
+ // draw the image for selections
+ if (mSelection) {
+ LayoutDeviceIntPoint pnt(aScreenDragRect->TopLeft());
+ *aSurface = presShell->RenderSelection(
+ mSelection, pnt, aScreenDragRect,
+ mImage ? RenderImageFlags::None : RenderImageFlags::AutoScale);
+ return NS_OK;
+ }
+
+ // if a custom image was specified, check if it is an image node and draw
+ // using the source rather than the displayed image. But if mImage isn't
+ // an image or canvas, fall through to RenderNode below.
+ if (mImage) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(dragNode);
+ HTMLCanvasElement* canvas = HTMLCanvasElement::FromNodeOrNull(content);
+ if (canvas) {
+ return DrawDragForImage(*aPresContext, nullptr, canvas, aScreenDragRect,
+ aSurface);
+ }
+
+ nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(dragNode);
+ // for image nodes, create the drag image from the actual image data
+ if (imageLoader) {
+ return DrawDragForImage(*aPresContext, imageLoader, nullptr,
+ aScreenDragRect, aSurface);
+ }
+
+ // If the image is a popup, use that as the image. This allows custom drag
+ // images that can change during the drag, but means that any platform
+ // default image handling won't occur.
+ // XXXndeakin this should be chrome-only
+
+ nsIFrame* frame = content->GetPrimaryFrame();
+ if (frame && frame->IsMenuPopupFrame()) {
+ mDragPopup = content->AsElement();
+ }
+ }
+
+ if (!mDragPopup) {
+ // otherwise, just draw the node
+ RenderImageFlags renderFlags =
+ mImage ? RenderImageFlags::None : RenderImageFlags::AutoScale;
+ if (renderFlags != RenderImageFlags::None) {
+ // check if the dragged node itself is an img element
+ if (dragNode->NodeName().LowerCaseEqualsLiteral("img")) {
+ renderFlags = renderFlags | RenderImageFlags::IsImage;
+ } else {
+ nsINodeList* childList = dragNode->ChildNodes();
+ uint32_t length = childList->Length();
+ // check every childnode for being an img element
+ // XXXbz why don't we need to check descendants recursively?
+ for (uint32_t count = 0; count < length; ++count) {
+ if (childList->Item(count)->NodeName().LowerCaseEqualsLiteral(
+ "img")) {
+ // if the dragnode contains an image, set RenderImageFlags::IsImage
+ // flag
+ renderFlags = renderFlags | RenderImageFlags::IsImage;
+ break;
+ }
+ }
+ }
+ }
+ LayoutDeviceIntPoint pnt(aScreenDragRect->TopLeft());
+ *aSurface = presShell->RenderNode(dragNode, aRegion, pnt, aScreenDragRect,
+ renderFlags);
+ }
+
+ // If an image was specified, reset the position from the offset that was
+ // supplied.
+ if (mImage) {
+ aScreenDragRect->MoveTo(screenPoint.x, screenPoint.y);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsBaseDragService::DrawDragForImage(
+ nsPresContext* aPresContext, nsIImageLoadingContent* aImageLoader,
+ HTMLCanvasElement* aCanvas, LayoutDeviceIntRect* aScreenDragRect,
+ RefPtr<SourceSurface>* aSurface) {
+ nsCOMPtr<imgIContainer> imgContainer;
+ if (aImageLoader) {
+ nsCOMPtr<imgIRequest> imgRequest;
+ nsresult rv = aImageLoader->GetRequest(
+ nsIImageLoadingContent::CURRENT_REQUEST, getter_AddRefs(imgRequest));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!imgRequest) return NS_ERROR_NOT_AVAILABLE;
+
+ rv = imgRequest->GetImage(getter_AddRefs(imgContainer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!imgContainer) return NS_ERROR_NOT_AVAILABLE;
+
+ // use the size of the image as the size of the drag image
+ int32_t imageWidth, imageHeight;
+ rv = imgContainer->GetWidth(&imageWidth);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = imgContainer->GetHeight(&imageHeight);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aScreenDragRect->SizeTo(aPresContext->CSSPixelsToDevPixels(imageWidth),
+ aPresContext->CSSPixelsToDevPixels(imageHeight));
+ } else {
+ // XXX The canvas size should be converted to dev pixels.
+ NS_ASSERTION(aCanvas, "both image and canvas are null");
+ nsIntSize sz = aCanvas->GetSize();
+ aScreenDragRect->SizeTo(sz.width, sz.height);
+ }
+
+ nsIntSize destSize;
+ destSize.width = aScreenDragRect->Width();
+ destSize.height = aScreenDragRect->Height();
+ if (destSize.width == 0 || destSize.height == 0) return NS_ERROR_FAILURE;
+
+ nsresult result = NS_OK;
+ if (aImageLoader) {
+ RefPtr<DrawTarget> dt =
+ gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
+ destSize, SurfaceFormat::B8G8R8A8);
+ if (!dt || !dt->IsValid()) return NS_ERROR_FAILURE;
+
+ gfxContext ctx(dt);
+
+ ImgDrawResult res = imgContainer->Draw(
+ &ctx, destSize, ImageRegion::Create(destSize),
+ imgIContainer::FRAME_CURRENT, SamplingFilter::GOOD, SVGImageContext(),
+ imgIContainer::FLAG_SYNC_DECODE, 1.0);
+ if (res == ImgDrawResult::BAD_IMAGE || res == ImgDrawResult::BAD_ARGS ||
+ res == ImgDrawResult::NOT_SUPPORTED) {
+ return NS_ERROR_FAILURE;
+ }
+ *aSurface = dt->Snapshot();
+ } else {
+ *aSurface = aCanvas->GetSurfaceSnapshot();
+ }
+
+ return result;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::Suppress() {
+ EndDragSession(false, 0);
+ ++mSuppressLevel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::Unsuppress() {
+ --mSuppressLevel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::UserCancelled() {
+ mUserCancelled = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::UpdateDragEffect() {
+ mDragActionFromChildProcess = mDragAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::UpdateDragImage(nsINode* aImage, int32_t aImageX,
+ int32_t aImageY) {
+ // Don't change the image if this is a drag from another source or if there
+ // is a drag popup.
+ if (!mSourceNode || mDragPopup) return NS_OK;
+
+ mImage = aImage;
+ mImageOffset = CSSIntPoint(aImageX, aImageY);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::DragEventDispatchedToChildProcess() {
+ mDragEventDispatchedToChildProcess = true;
+ return NS_OK;
+}
+
+bool nsBaseDragService::MaybeAddChildProcess(
+ mozilla::dom::ContentParent* aChild) {
+ if (!mChildProcesses.Contains(aChild)) {
+ mChildProcesses.AppendElement(aChild);
+ return true;
+ }
+ return false;
+}
+
+bool nsBaseDragService::RemoveAllChildProcesses() {
+ for (uint32_t c = 0; c < mChildProcesses.Length(); c++) {
+ mozilla::Unused << mChildProcesses[c]->SendEndDragSession(
+ true, false, LayoutDeviceIntPoint(), 0,
+ nsIDragService::DRAGDROP_ACTION_NONE);
+ }
+ mChildProcesses.Clear();
+ return true;
+}
+
+bool nsBaseDragService::MustUpdateDataTransfer(EventMessage aMessage) {
+ return false;
+}
+
+NS_IMETHODIMP
+nsBaseDragService::MaybeEditorDeletedSourceNode(Element* aEditingHost) {
+ // If builtin editor of Blink and WebKit deletes the source node,they retarget
+ // the source node to the editing host.
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/page/drag_controller.cc;l=724;drc=d9ba13b8cd8ac0faed7afc3d1f7e4b67ebac2a0b
+ // That allows editor apps listens to "dragend" event in editing host or its
+ // ancestors. Therefore, we should follow them for compatibility.
+ if (mSourceNode && !mSourceNode->IsInComposedDoc()) {
+ mSourceNode = aEditingHost;
+ }
+ return NS_OK;
+}
diff --git a/widget/nsBaseDragService.h b/widget/nsBaseDragService.h
new file mode 100644
index 0000000000..7e76a23604
--- /dev/null
+++ b/widget/nsBaseDragService.h
@@ -0,0 +1,221 @@
+/* -*- 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/. */
+
+#ifndef nsBaseDragService_h__
+#define nsBaseDragService_h__
+
+#include "nsIDragService.h"
+#include "nsIDragSession.h"
+#include "nsCOMPtr.h"
+#include "nsRect.h"
+#include "nsPoint.h"
+#include "nsString.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/RemoteDragStartData.h"
+#include "mozilla/Logging.h"
+#include "nsTArray.h"
+#include "nsRegion.h"
+#include "Units.h"
+
+extern mozilla::LazyLogModule sWidgetDragServiceLog;
+#define MOZ_DRAGSERVICE_LOG(...) \
+ MOZ_LOG(sWidgetDragServiceLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#define MOZ_DRAGSERVICE_LOG_ENABLED() \
+ MOZ_LOG_TEST(sWidgetDragServiceLog, mozilla::LogLevel::Debug)
+
+// translucency level for drag images
+#define DRAG_TRANSLUCENCY 0.65
+
+class nsIContent;
+
+class nsINode;
+class nsPresContext;
+class nsIImageLoadingContent;
+
+namespace mozilla {
+namespace gfx {
+class SourceSurface;
+} // namespace gfx
+
+namespace dom {
+class DataTransfer;
+class Selection;
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * XP DragService wrapper base class
+ */
+
+class nsBaseDragService : public nsIDragService, public nsIDragSession {
+ public:
+ typedef mozilla::gfx::SourceSurface SourceSurface;
+
+ nsBaseDragService();
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+
+ // nsIDragSession and nsIDragService
+ NS_DECL_NSIDRAGSERVICE
+ NS_DECL_NSIDRAGSESSION
+
+ void SetDragEndPoint(nsIntPoint aEndDragPoint) {
+ mEndDragPoint =
+ mozilla::LayoutDeviceIntPoint::FromUnknownPoint(aEndDragPoint);
+ }
+ void SetDragEndPoint(mozilla::LayoutDeviceIntPoint aEndDragPoint) {
+ mEndDragPoint = aEndDragPoint;
+ }
+
+ uint16_t GetInputSource() { return mInputSource; }
+
+ int32_t TakeChildProcessDragAction();
+
+ protected:
+ virtual ~nsBaseDragService();
+
+ /**
+ * Called from nsBaseDragService to initiate a platform drag from a source
+ * in this process. This is expected to ensure that StartDragSession() and
+ * EndDragSession() get called if the platform drag is successfully invoked.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult InvokeDragSessionImpl(
+ nsIArray* aTransferableArray,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ uint32_t aActionType) = 0;
+
+ /**
+ * Draw the drag image, if any, to a surface and return it. The drag image
+ * is constructed from mImage if specified, or aDOMNode if mImage is null.
+ *
+ * aRegion may be used to draw only a subset of the element. This region
+ * should be supplied using x and y coordinates measured in css pixels
+ * that are relative to the upper-left corner of the window.
+ *
+ * aScreenPosition should be the screen coordinates of the mouse click
+ * for the drag. These are in CSS pixels.
+ *
+ * On return, aScreenDragRect will contain the screen coordinates of the
+ * area being dragged. This is used by the platform-specific part of the
+ * drag service to determine the drag feedback. This rect will be in the
+ * device pixels of the presContext.
+ *
+ * If there is no drag image, the returned surface will be null, but
+ * aScreenDragRect will still be set to the drag area.
+ *
+ * aPresContext will be set to the nsPresContext used determined from
+ * whichever of mImage or aDOMNode is used.
+ */
+ nsresult DrawDrag(nsINode* aDOMNode,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ mozilla::CSSIntPoint aScreenPosition,
+ mozilla::LayoutDeviceIntRect* aScreenDragRect,
+ RefPtr<SourceSurface>* aSurface,
+ nsPresContext** aPresContext);
+
+ /**
+ * Draw a drag image for an image node specified by aImageLoader or aCanvas.
+ * This is called by DrawDrag.
+ */
+ nsresult DrawDragForImage(nsPresContext* aPresContext,
+ nsIImageLoadingContent* aImageLoader,
+ mozilla::dom::HTMLCanvasElement* aCanvas,
+ mozilla::LayoutDeviceIntRect* aScreenDragRect,
+ RefPtr<SourceSurface>* aSurface);
+
+ /**
+ * If the drag image is a popup, open the popup when the drag begins.
+ */
+ void OpenDragPopup();
+
+ /**
+ * Free resources contained in DataTransferItems that aren't needed by JS.
+ */
+ void DiscardInternalTransferData();
+
+ // Returns true if a drag event was dispatched to a child process after
+ // the previous TakeDragEventDispatchedToChildProcess() call.
+ bool TakeDragEventDispatchedToChildProcess() {
+ bool retval = mDragEventDispatchedToChildProcess;
+ mDragEventDispatchedToChildProcess = false;
+ return retval;
+ }
+
+ bool mCanDrop;
+ bool mOnlyChromeDrop;
+ bool mDoingDrag;
+ bool mSessionIsSynthesizedForTests;
+ bool mIsDraggingTextInTextControl;
+
+ // true if in EndDragSession
+ bool mEndingSession;
+ // true if mImage should be used to set a drag image
+ bool mHasImage;
+ // true if the user cancelled the drag operation
+ bool mUserCancelled;
+
+ bool mDragEventDispatchedToChildProcess;
+
+ uint32_t mDragAction;
+ uint32_t mDragActionFromChildProcess;
+
+ // mEffectAllowedForTests stores allowed effects at invoking the drag
+ // for tests.
+ uint32_t mEffectAllowedForTests;
+
+ nsCOMPtr<nsINode> mSourceNode;
+ nsCOMPtr<nsIPrincipal> mTriggeringPrincipal;
+ nsCOMPtr<nsIContentSecurityPolicy> mCsp;
+
+ // the document at the drag source. will be null if it came from outside the
+ // app.
+ RefPtr<mozilla::dom::Document> mSourceDocument;
+
+ RefPtr<mozilla::dom::WindowContext> mSourceWindowContext;
+ RefPtr<mozilla::dom::WindowContext> mSourceTopWindowContext;
+
+ // the contentpolicy type passed to the channel when initiating the drag
+ // session
+ nsContentPolicyType mContentPolicyType;
+
+ RefPtr<mozilla::dom::DataTransfer> mDataTransfer;
+
+ // used to determine the image to appear on the cursor while dragging
+ nsCOMPtr<nsINode> mImage;
+ // offset of cursor within the image
+ mozilla::CSSIntPoint mImageOffset;
+
+ // set if a selection is being dragged
+ RefPtr<mozilla::dom::Selection> mSelection;
+
+ // remote drag data
+ RefPtr<mozilla::dom::RemoteDragStartData> mDragStartData;
+
+ // set if the image in mImage is a popup. If this case, the popup will be
+ // opened and moved instead of using a drag image.
+ nsCOMPtr<mozilla::dom::Element> mDragPopup;
+
+ // the screen position where drag gesture occurred, used for positioning the
+ // drag image.
+ mozilla::CSSIntPoint mScreenPosition;
+
+ // The position relative to the top level widget where the drag ended.
+ mozilla::LayoutDeviceIntPoint mEndDragPoint;
+
+ uint32_t mSuppressLevel;
+
+ // The input source of the drag event. Possible values are from MouseEvent.
+ uint16_t mInputSource;
+
+ nsTArray<RefPtr<mozilla::dom::ContentParent>> mChildProcesses;
+
+ // Sub-region for tree-selections.
+ mozilla::Maybe<mozilla::CSSIntRegion> mRegion;
+};
+
+#endif // nsBaseDragService_h__
diff --git a/widget/nsBaseFilePicker.cpp b/widget/nsBaseFilePicker.cpp
new file mode 100644
index 0000000000..53be3d80b7
--- /dev/null
+++ b/widget/nsBaseFilePicker.cpp
@@ -0,0 +1,496 @@
+/* -*- 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 "nsCOMPtr.h"
+#include "nsPIDOMWindow.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIWidget.h"
+
+#include "nsIStringBundle.h"
+#include "nsString.h"
+#include "nsCOMArray.h"
+#include "nsIFile.h"
+#include "nsEnumeratorUtils.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/Components.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "WidgetUtils.h"
+#include "nsSimpleEnumerator.h"
+#include "nsThreadUtils.h"
+#include "nsContentUtils.h"
+
+#include "nsBaseFilePicker.h"
+
+using namespace mozilla::widget;
+using namespace mozilla::dom;
+using mozilla::ErrorResult;
+
+#define FILEPICKER_TITLES "chrome://global/locale/filepicker.properties"
+#define FILEPICKER_FILTERS "chrome://global/content/filepicker.properties"
+
+namespace {
+
+nsresult LocalFileToDirectoryOrBlob(nsPIDOMWindowInner* aWindow,
+ bool aIsDirectory, nsIFile* aFile,
+ nsISupports** aResult) {
+ MOZ_ASSERT(aWindow);
+
+ if (aIsDirectory) {
+#ifdef DEBUG
+ bool isDir;
+ aFile->IsDirectory(&isDir);
+ MOZ_ASSERT(isDir);
+#endif
+
+ RefPtr<Directory> directory = Directory::Create(aWindow->AsGlobal(), aFile);
+ MOZ_ASSERT(directory);
+
+ directory.forget(aResult);
+ return NS_OK;
+ }
+
+ RefPtr<File> file = File::CreateFromFile(aWindow->AsGlobal(), aFile);
+ if (NS_WARN_IF(!file)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ file.forget(aResult);
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+#ifndef XP_WIN
+/**
+ * A runnable to dispatch from the main thread to the main thread to display
+ * the file picker while letting the showAsync method return right away.
+ *
+ * Not needed on Windows, where nsFilePicker::Open() is fully async.
+ */
+class nsBaseFilePicker::AsyncShowFilePicker : public mozilla::Runnable {
+ public:
+ AsyncShowFilePicker(nsBaseFilePicker* aFilePicker,
+ nsIFilePickerShownCallback* aCallback)
+ : mozilla::Runnable("AsyncShowFilePicker"),
+ mFilePicker(aFilePicker),
+ mCallback(aCallback) {}
+
+ NS_IMETHOD Run() override {
+ NS_ASSERTION(NS_IsMainThread(),
+ "AsyncShowFilePicker should be on the main thread!");
+
+ // It's possible that some widget implementations require GUI operations
+ // to be on the main thread, so that's why we're not dispatching to another
+ // thread and calling back to the main after it's done.
+ nsIFilePicker::ResultCode result = nsIFilePicker::returnCancel;
+ nsresult rv = mFilePicker->Show(&result);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("FilePicker's Show() implementation failed!");
+ }
+
+ if (mCallback) {
+ mCallback->Done(result);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<nsBaseFilePicker> mFilePicker;
+ RefPtr<nsIFilePickerShownCallback> mCallback;
+};
+#endif
+
+class nsBaseFilePickerEnumerator : public nsSimpleEnumerator {
+ public:
+ nsBaseFilePickerEnumerator(nsPIDOMWindowOuter* aParent,
+ nsISimpleEnumerator* iterator,
+ nsIFilePicker::Mode aMode)
+ : mIterator(iterator),
+ mParent(aParent->GetCurrentInnerWindow()),
+ mMode(aMode) {}
+
+ const nsID& DefaultInterface() override { return NS_GET_IID(nsIFile); }
+
+ NS_IMETHOD
+ GetNext(nsISupports** aResult) override {
+ nsCOMPtr<nsISupports> tmp;
+ nsresult rv = mIterator->GetNext(getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!tmp) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> localFile = do_QueryInterface(tmp);
+ if (!localFile) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mParent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return LocalFileToDirectoryOrBlob(
+ mParent, mMode == nsIFilePicker::modeGetFolder, localFile, aResult);
+ }
+
+ NS_IMETHOD
+ HasMoreElements(bool* aResult) override {
+ return mIterator->HasMoreElements(aResult);
+ }
+
+ private:
+ nsCOMPtr<nsISimpleEnumerator> mIterator;
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+ nsIFilePicker::Mode mMode;
+};
+
+nsBaseFilePicker::nsBaseFilePicker()
+ : mAddToRecentDocs(true), mMode(nsIFilePicker::modeOpen) {}
+
+nsBaseFilePicker::~nsBaseFilePicker() = default;
+
+NS_IMETHODIMP nsBaseFilePicker::Init(
+ mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ nsIFilePicker::Mode aMode,
+ mozilla::dom::BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(aParent,
+ "Null parent passed to filepicker, no file "
+ "picker for you!");
+
+ mParent = nsPIDOMWindowOuter::From(aParent);
+
+ nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(mParent);
+ NS_ENSURE_TRUE(widget, NS_ERROR_FAILURE);
+
+ mBrowsingContext = aBrowsingContext;
+ mMode = aMode;
+ InitNative(widget, aTitle);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::IsModeSupported(nsIFilePicker::Mode aMode, JSContext* aCx,
+ Promise** aPromise) {
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aPromise);
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ promise->MaybeResolve(true);
+ promise.forget(aPromise);
+
+ return NS_OK;
+}
+
+#ifndef XP_WIN
+NS_IMETHODIMP
+nsBaseFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
+ if (MaybeBlockFilePicker(aCallback)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIRunnable> filePickerEvent =
+ new AsyncShowFilePicker(this, aCallback);
+ return NS_DispatchToMainThread(filePickerEvent);
+}
+#endif
+
+NS_IMETHODIMP
+nsBaseFilePicker::Close() {
+ NS_WARNING("Unimplemented nsFilePicker::Close");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::AppendFilters(int32_t aFilterMask) {
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (!stringService) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIStringBundle> titleBundle, filterBundle;
+
+ nsresult rv = stringService->CreateBundle(FILEPICKER_TITLES,
+ getter_AddRefs(titleBundle));
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ rv = stringService->CreateBundle(FILEPICKER_FILTERS,
+ getter_AddRefs(filterBundle));
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ nsAutoString title;
+ nsAutoString filter;
+
+ if (aFilterMask & filterAll) {
+ titleBundle->GetStringFromName("allTitle", title);
+ filterBundle->GetStringFromName("allFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterHTML) {
+ titleBundle->GetStringFromName("htmlTitle", title);
+ filterBundle->GetStringFromName("htmlFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterText) {
+ titleBundle->GetStringFromName("textTitle", title);
+ filterBundle->GetStringFromName("textFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterImages) {
+ titleBundle->GetStringFromName("imageTitle", title);
+ filterBundle->GetStringFromName("imageFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterAudio) {
+ titleBundle->GetStringFromName("audioTitle", title);
+ filterBundle->GetStringFromName("audioFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterVideo) {
+ titleBundle->GetStringFromName("videoTitle", title);
+ filterBundle->GetStringFromName("videoFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterXML) {
+ titleBundle->GetStringFromName("xmlTitle", title);
+ filterBundle->GetStringFromName("xmlFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterXUL) {
+ titleBundle->GetStringFromName("xulTitle", title);
+ filterBundle->GetStringFromName("xulFilter", filter);
+ AppendFilter(title, filter);
+ }
+ if (aFilterMask & filterApps) {
+ titleBundle->GetStringFromName("appsTitle", title);
+ // Pass the magic string "..apps" to the platform filepicker, which it
+ // should recognize and do the correct platform behavior for.
+ AppendFilter(title, u"..apps"_ns);
+ }
+ if (aFilterMask & filterPDF) {
+ titleBundle->GetStringFromName("pdfTitle", title);
+ filterBundle->GetStringFromName("pdfFilter", filter);
+ AppendFilter(title, filter);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseFilePicker::AppendRawFilter(const nsAString& aFilter) {
+ mRawFilters.AppendElement(aFilter);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseFilePicker::GetCapture(
+ nsIFilePicker::CaptureTarget* aCapture) {
+ *aCapture = nsIFilePicker::CaptureTarget::captureNone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseFilePicker::SetCapture(
+ nsIFilePicker::CaptureTarget aCapture) {
+ return NS_OK;
+}
+
+// Set the filter index
+NS_IMETHODIMP nsBaseFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
+ *aFilterIndex = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseFilePicker::SetFilterIndex(int32_t aFilterIndex) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
+ NS_ENSURE_ARG_POINTER(aFiles);
+ nsCOMArray<nsIFile> files;
+ nsresult rv;
+
+ // if we get into the base class, the platform
+ // doesn't implement GetFiles() yet.
+ // so we fake it.
+ nsCOMPtr<nsIFile> file;
+ rv = GetFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ files.AppendObject(file);
+
+ return NS_NewArrayEnumerator(aFiles, files, NS_GET_IID(nsIFile));
+}
+
+// Set the display directory
+NS_IMETHODIMP nsBaseFilePicker::SetDisplayDirectory(nsIFile* aDirectory) {
+ // if displaySpecialDirectory has been previously called, let's abort this
+ // operation.
+ if (!mDisplaySpecialDirectory.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (!aDirectory) {
+ mDisplayDirectory = nullptr;
+ return NS_OK;
+ }
+ nsCOMPtr<nsIFile> directory;
+ nsresult rv = aDirectory->Clone(getter_AddRefs(directory));
+ if (NS_FAILED(rv)) return rv;
+ mDisplayDirectory = directory;
+ return NS_OK;
+}
+
+// Get the display directory
+NS_IMETHODIMP nsBaseFilePicker::GetDisplayDirectory(nsIFile** aDirectory) {
+ *aDirectory = nullptr;
+
+ // if displaySpecialDirectory has been previously called, let's abort this
+ // operation.
+ if (!mDisplaySpecialDirectory.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (!mDisplayDirectory) return NS_OK;
+ nsCOMPtr<nsIFile> directory;
+ nsresult rv = mDisplayDirectory->Clone(getter_AddRefs(directory));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ directory.forget(aDirectory);
+ return NS_OK;
+}
+
+// Set the display special directory
+NS_IMETHODIMP nsBaseFilePicker::SetDisplaySpecialDirectory(
+ const nsAString& aDirectory) {
+ // if displayDirectory has been previously called, let's abort this operation.
+ if (mDisplayDirectory && mDisplaySpecialDirectory.IsEmpty()) {
+ return NS_OK;
+ }
+
+ mDisplaySpecialDirectory = aDirectory;
+ if (mDisplaySpecialDirectory.IsEmpty()) {
+ mDisplayDirectory = nullptr;
+ return NS_OK;
+ }
+
+ return ResolveSpecialDirectory(aDirectory);
+}
+
+bool nsBaseFilePicker::MaybeBlockFilePicker(
+ nsIFilePickerShownCallback* aCallback) {
+ if (!mozilla::StaticPrefs::widget_disable_file_pickers()) {
+ return false;
+ }
+
+ if (aCallback) {
+ // File pickers are disabled, so we answer the callback with returnCancel.
+ aCallback->Done(nsIFilePicker::returnCancel);
+ }
+ if (mBrowsingContext) {
+ RefPtr<Element> topFrameElement = mBrowsingContext->GetTopFrameElement();
+ if (topFrameElement) {
+ // Dispatch an event that the frontend may use.
+ nsContentUtils::DispatchEventOnlyToChrome(
+ topFrameElement->OwnerDoc(), topFrameElement, u"FilePickerBlocked"_ns,
+ mozilla::CanBubble::eYes, mozilla::Cancelable::eNo);
+ }
+ }
+
+ return true;
+}
+
+nsresult nsBaseFilePicker::ResolveSpecialDirectory(
+ const nsAString& aSpecialDirectory) {
+ // Only perform special-directory name resolution in the parent process.
+ // (Subclasses of `nsBaseFilePicker` used in other processes must override
+ // this function.)
+ MOZ_ASSERT(XRE_IsParentProcess());
+ return NS_GetSpecialDirectory(NS_ConvertUTF16toUTF8(aSpecialDirectory).get(),
+ getter_AddRefs(mDisplayDirectory));
+}
+
+// Get the display special directory
+NS_IMETHODIMP nsBaseFilePicker::GetDisplaySpecialDirectory(
+ nsAString& aDirectory) {
+ aDirectory = mDisplaySpecialDirectory;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::GetAddToRecentDocs(bool* aFlag) {
+ *aFlag = mAddToRecentDocs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::SetAddToRecentDocs(bool aFlag) {
+ mAddToRecentDocs = aFlag;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::GetMode(nsIFilePicker::Mode* aMode) {
+ *aMode = mMode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::SetOkButtonLabel(const nsAString& aLabel) {
+ mOkButtonLabel = aLabel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::GetOkButtonLabel(nsAString& aLabel) {
+ aLabel = mOkButtonLabel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::GetDomFileOrDirectory(nsISupports** aValue) {
+ nsCOMPtr<nsIFile> localFile;
+ nsresult rv = GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!localFile) {
+ *aValue = nullptr;
+ return NS_OK;
+ }
+
+ auto* innerParent = mParent ? mParent->GetCurrentInnerWindow() : nullptr;
+
+ if (!innerParent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return LocalFileToDirectoryOrBlob(
+ innerParent, mMode == nsIFilePicker::modeGetFolder, localFile, aValue);
+}
+
+NS_IMETHODIMP
+nsBaseFilePicker::GetDomFileOrDirectoryEnumerator(
+ nsISimpleEnumerator** aValue) {
+ nsCOMPtr<nsISimpleEnumerator> iter;
+ nsresult rv = GetFiles(getter_AddRefs(iter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsBaseFilePickerEnumerator> retIter =
+ new nsBaseFilePickerEnumerator(mParent, iter, mMode);
+
+ retIter.forget(aValue);
+ return NS_OK;
+}
diff --git a/widget/nsBaseFilePicker.h b/widget/nsBaseFilePicker.h
new file mode 100644
index 0000000000..3d26a1822e
--- /dev/null
+++ b/widget/nsBaseFilePicker.h
@@ -0,0 +1,82 @@
+/* -*- 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/. */
+
+#ifndef nsBaseFilePicker_h__
+#define nsBaseFilePicker_h__
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "nsISupports.h"
+#include "nsIFilePicker.h"
+#include "nsISimpleEnumerator.h"
+#include "nsArrayEnumerator.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsPIDOMWindowOuter;
+class nsIWidget;
+
+class nsBaseFilePicker : public nsIFilePicker {
+#ifndef XP_WIN
+ class AsyncShowFilePicker;
+#endif
+
+ public:
+ nsBaseFilePicker();
+ virtual ~nsBaseFilePicker();
+
+ NS_IMETHOD Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ nsIFilePicker::Mode aMode,
+ mozilla::dom::BrowsingContext* aBrowsingContext) override;
+ NS_IMETHOD IsModeSupported(nsIFilePicker::Mode aMode, JSContext* aCx,
+ mozilla::dom::Promise** aPromise) override;
+#ifndef XP_WIN
+ NS_IMETHOD Open(nsIFilePickerShownCallback* aCallback) override;
+#endif
+ NS_IMETHOD Close() override;
+ NS_IMETHOD AppendFilters(int32_t filterMask) override;
+ NS_IMETHOD AppendRawFilter(const nsAString& aFilter) override;
+ NS_IMETHOD GetCapture(nsIFilePicker::CaptureTarget* aCapture) override;
+ NS_IMETHOD SetCapture(nsIFilePicker::CaptureTarget aCapture) override;
+ NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override;
+ NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override;
+ NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override;
+ NS_IMETHOD GetDisplayDirectory(nsIFile** aDisplayDirectory) override;
+ NS_IMETHOD SetDisplayDirectory(nsIFile* aDisplayDirectory) override;
+ NS_IMETHOD GetDisplaySpecialDirectory(nsAString& aDisplayDirectory) override;
+ NS_IMETHOD SetDisplaySpecialDirectory(
+ const nsAString& aDisplayDirectory) override;
+ NS_IMETHOD GetAddToRecentDocs(bool* aFlag) override;
+ NS_IMETHOD SetAddToRecentDocs(bool aFlag) override;
+ NS_IMETHOD GetMode(nsIFilePicker::Mode* aMode) override;
+ NS_IMETHOD SetOkButtonLabel(const nsAString& aLabel) override;
+ NS_IMETHOD GetOkButtonLabel(nsAString& aLabel) override;
+
+ NS_IMETHOD GetDomFileOrDirectory(nsISupports** aValue) override;
+ NS_IMETHOD GetDomFileOrDirectoryEnumerator(
+ nsISimpleEnumerator** aValue) override;
+
+ protected:
+ virtual void InitNative(nsIWidget* aParent, const nsAString& aTitle) = 0;
+ virtual nsresult Show(nsIFilePicker::ResultCode* _retval) = 0;
+
+ virtual nsresult ResolveSpecialDirectory(const nsAString& aSpecialDirectory);
+ bool MaybeBlockFilePicker(nsIFilePickerShownCallback* aCallback);
+
+ bool mAddToRecentDocs;
+ nsCOMPtr<nsIFile> mDisplayDirectory;
+ nsString mDisplaySpecialDirectory;
+
+ nsCOMPtr<nsPIDOMWindowOuter> mParent;
+ // The BrowsingContext from which the file picker is being opened.
+ // Used for content analysis.
+ RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext;
+ nsIFilePicker::Mode mMode;
+ nsString mOkButtonLabel;
+ nsTArray<nsString> mRawFilters;
+};
+
+#endif // nsBaseFilePicker_h__
diff --git a/widget/nsBaseWidget.cpp b/widget/nsBaseWidget.cpp
new file mode 100644
index 0000000000..35c580c106
--- /dev/null
+++ b/widget/nsBaseWidget.cpp
@@ -0,0 +1,3513 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "nsBaseWidget.h"
+
+#include <utility>
+
+#include "GLConsts.h"
+#include "InputData.h"
+#include "LiveResizeListener.h"
+#include "SwipeTracker.h"
+#include "TouchEvents.h"
+#include "X11UndefineNone.h"
+#include "base/thread.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/GlobalKeyListener.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEventDispatcherListener.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/VsyncDispatcher.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/SimpleGestureEventBinding.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/TouchActionHelper.h"
+#include "mozilla/layers/APZEventState.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/ChromeProcessController.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorOptions.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDeviceContext.h"
+#include "nsGfxCIID.h"
+#include "nsIAppWindow.h"
+#include "nsIBaseWindow.h"
+#include "nsIContent.h"
+#include "nsIScreenManager.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIWidgetListener.h"
+#include "nsRefPtrHashtable.h"
+#include "nsServiceManagerUtils.h"
+#include "nsWidgetsCID.h"
+#include "nsXULPopupManager.h"
+#include "prdtoa.h"
+#include "prenv.h"
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+#endif
+#include "gfxConfig.h"
+#include "gfxUtils.h" // for ToDeviceColor
+#include "mozilla/layers/CompositorSession.h"
+#include "VRManagerChild.h"
+#include "gfxConfig.h"
+#include "nsView.h"
+#include "nsViewManager.h"
+
+static mozilla::LazyLogModule sBaseWidgetLog("BaseWidget");
+
+#ifdef DEBUG
+# include "nsIObserver.h"
+
+static void debug_RegisterPrefCallbacks();
+
+#endif
+
+#ifdef NOISY_WIDGET_LEAKS
+static int32_t gNumWidgets;
+#endif
+
+using namespace mozilla::dom;
+using namespace mozilla::layers;
+using namespace mozilla::ipc;
+using namespace mozilla::widget;
+using namespace mozilla;
+
+// Async pump timer during injected long touch taps
+#define TOUCH_INJECT_PUMP_TIMER_MSEC 50
+#define TOUCH_INJECT_LONG_TAP_DEFAULT_MSEC 1500
+int32_t nsIWidget::sPointerIdCounter = 0;
+
+// Some statics from nsIWidget.h
+/*static*/
+uint64_t AutoObserverNotifier::sObserverId = 0;
+/*static*/ nsTHashMap<uint64_t, nsCOMPtr<nsIObserver>>
+ AutoObserverNotifier::sSavedObservers;
+
+// The maximum amount of time to let the EnableDragDrop runnable wait in the
+// idle queue before timing out and moving it to the regular queue. Value is in
+// milliseconds.
+const uint32_t kAsyncDragDropTimeout = 1000;
+
+NS_IMPL_ISUPPORTS(nsBaseWidget, nsIWidget, nsISupportsWeakReference)
+
+//-------------------------------------------------------------------------
+//
+// nsBaseWidget constructor
+//
+//-------------------------------------------------------------------------
+
+nsBaseWidget::nsBaseWidget() : nsBaseWidget(BorderStyle::None) {}
+
+nsBaseWidget::nsBaseWidget(BorderStyle aBorderStyle)
+ : mWidgetListener(nullptr),
+ mAttachedWidgetListener(nullptr),
+ mPreviouslyAttachedWidgetListener(nullptr),
+ mCompositorVsyncDispatcher(nullptr),
+ mBorderStyle(aBorderStyle),
+ mBounds(0, 0, 0, 0),
+ mIsTiled(false),
+ mPopupLevel(PopupLevel::Top),
+ mPopupType(PopupType::Any),
+ mHasRemoteContent(false),
+ mUpdateCursor(true),
+ mUseAttachedEvents(false),
+ mIMEHasFocus(false),
+ mIMEHasQuit(false),
+ mIsFullyOccluded(false),
+ mNeedFastSnaphot(false),
+ mCurrentPanGestureBelongsToSwipe(false),
+ mIsPIPWindow(false) {
+#ifdef NOISY_WIDGET_LEAKS
+ gNumWidgets++;
+ printf("WIDGETS+ = %d\n", gNumWidgets);
+#endif
+
+#ifdef DEBUG
+ debug_RegisterPrefCallbacks();
+#endif
+
+ mShutdownObserver = new WidgetShutdownObserver(this);
+}
+
+NS_IMPL_ISUPPORTS(WidgetShutdownObserver, nsIObserver)
+
+WidgetShutdownObserver::WidgetShutdownObserver(nsBaseWidget* aWidget)
+ : mWidget(aWidget), mRegistered(false) {
+ Register();
+}
+
+WidgetShutdownObserver::~WidgetShutdownObserver() {
+ // No need to call Unregister(), we can't be destroyed until nsBaseWidget
+ // gets torn down. The observer service and nsBaseWidget have a ref on us
+ // so nsBaseWidget has to call Unregister and then clear its ref.
+}
+
+NS_IMETHODIMP
+WidgetShutdownObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!mWidget) {
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ RefPtr<nsBaseWidget> widget(mWidget);
+ widget->Shutdown();
+ } else if (!strcmp(aTopic, "quit-application")) {
+ RefPtr<nsBaseWidget> widget(mWidget);
+ widget->QuitIME();
+ }
+ return NS_OK;
+}
+
+void WidgetShutdownObserver::Register() {
+ if (!mRegistered) {
+ mRegistered = true;
+ nsContentUtils::RegisterShutdownObserver(this);
+
+#ifndef MOZ_WIDGET_ANDROID
+ // The primary purpose of observing quit-application is
+ // to avoid leaking a widget on Windows when nothing else
+ // breaks the circular reference between the widget and
+ // TSFTextStore. However, our Android IME code crashes if
+ // doing this on Android, so let's not do this on Android.
+ // Doing this on Gtk and Mac just in case.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "quit-application", false);
+ }
+#endif
+ }
+}
+
+void WidgetShutdownObserver::Unregister() {
+ if (mRegistered) {
+ mWidget = nullptr;
+
+#ifndef MOZ_WIDGET_ANDROID
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "quit-application");
+ }
+#endif
+
+ nsContentUtils::UnregisterShutdownObserver(this);
+ mRegistered = false;
+ }
+}
+
+#define INTL_APP_LOCALES_CHANGED "intl:app-locales-changed"
+
+NS_IMPL_ISUPPORTS(LocalesChangedObserver, nsIObserver)
+
+LocalesChangedObserver::LocalesChangedObserver(nsBaseWidget* aWidget)
+ : mWidget(aWidget), mRegistered(false) {
+ Register();
+}
+
+LocalesChangedObserver::~LocalesChangedObserver() {
+ // No need to call Unregister(), we can't be destroyed until nsBaseWidget
+ // gets torn down. The observer service and nsBaseWidget have a ref on us
+ // so nsBaseWidget has to call Unregister and then clear its ref.
+}
+
+NS_IMETHODIMP
+LocalesChangedObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!mWidget) {
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, INTL_APP_LOCALES_CHANGED)) {
+ RefPtr<nsBaseWidget> widget(mWidget);
+ widget->LocalesChanged();
+ }
+ return NS_OK;
+}
+
+void LocalesChangedObserver::Register() {
+ if (mRegistered) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, INTL_APP_LOCALES_CHANGED, true);
+ }
+
+ // Locale might be update before registering
+ RefPtr<nsBaseWidget> widget(mWidget);
+ widget->LocalesChanged();
+
+ mRegistered = true;
+}
+
+void LocalesChangedObserver::Unregister() {
+ if (!mRegistered) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, INTL_APP_LOCALES_CHANGED);
+ }
+
+ mWidget = nullptr;
+ mRegistered = false;
+}
+
+void nsBaseWidget::Shutdown() {
+ NotifyLiveResizeStopped();
+ DestroyCompositor();
+ FreeLocalesChangedObserver();
+ FreeShutdownObserver();
+}
+
+void nsBaseWidget::QuitIME() {
+ IMEStateManager::WidgetOnQuit(this);
+ this->mIMEHasQuit = true;
+}
+
+void nsBaseWidget::DestroyCompositor() {
+ RevokeTransactionIdAllocator();
+
+ // We release this before releasing the compositor, since it may hold the
+ // last reference to our ClientLayerManager. ClientLayerManager's dtor can
+ // trigger a paint, creating a new compositor, and we don't want to re-use
+ // the old vsync dispatcher.
+ if (mCompositorVsyncDispatcher) {
+ MOZ_ASSERT(mCompositorVsyncDispatcherLock.get());
+
+ MutexAutoLock lock(*mCompositorVsyncDispatcherLock.get());
+ mCompositorVsyncDispatcher->Shutdown();
+ mCompositorVsyncDispatcher = nullptr;
+ }
+
+ // The compositor shutdown sequence looks like this:
+ // 1. CompositorSession calls CompositorBridgeChild::Destroy.
+ // 2. CompositorBridgeChild synchronously sends WillClose.
+ // 3. CompositorBridgeParent releases some resources (such as the layer
+ // manager, compositor, and widget).
+ // 4. CompositorBridgeChild::Destroy returns.
+ // 5. Asynchronously, CompositorBridgeParent::ActorDestroy will fire on the
+ // compositor thread when the I/O thread closes the IPC channel.
+ // 6. Step 5 will schedule DeferredDestroy on the compositor thread, which
+ // releases the reference CompositorBridgeParent holds to itself.
+ //
+ // When CompositorSession::Shutdown returns, we assume the compositor is gone
+ // or will be gone very soon.
+ if (mCompositorSession) {
+ ReleaseContentController();
+ mAPZC = nullptr;
+ SetCompositorWidgetDelegate(nullptr);
+ mCompositorBridgeChild = nullptr;
+ mCompositorSession->Shutdown();
+ mCompositorSession = nullptr;
+ }
+}
+
+// This prevents the layer manager from starting a new transaction during
+// shutdown.
+void nsBaseWidget::RevokeTransactionIdAllocator() {
+ if (!mWindowRenderer || !mWindowRenderer->AsWebRender()) {
+ return;
+ }
+ mWindowRenderer->AsWebRender()->SetTransactionIdAllocator(nullptr);
+}
+
+void nsBaseWidget::ReleaseContentController() {
+ if (mRootContentController) {
+ mRootContentController->Destroy();
+ mRootContentController = nullptr;
+ }
+}
+
+void nsBaseWidget::DestroyLayerManager() {
+ if (mWindowRenderer) {
+ mWindowRenderer->Destroy();
+ mWindowRenderer = nullptr;
+ }
+ DestroyCompositor();
+}
+
+void nsBaseWidget::OnRenderingDeviceReset() { DestroyLayerManager(); }
+
+void nsBaseWidget::FreeShutdownObserver() {
+ if (mShutdownObserver) {
+ mShutdownObserver->Unregister();
+ }
+ mShutdownObserver = nullptr;
+}
+
+void nsBaseWidget::FreeLocalesChangedObserver() {
+ if (mLocalesChangedObserver) {
+ mLocalesChangedObserver->Unregister();
+ }
+ mLocalesChangedObserver = nullptr;
+}
+
+//-------------------------------------------------------------------------
+//
+// nsBaseWidget destructor
+//
+//-------------------------------------------------------------------------
+
+nsBaseWidget::~nsBaseWidget() {
+ if (mSwipeTracker) {
+ mSwipeTracker->Destroy();
+ mSwipeTracker = nullptr;
+ }
+
+ IMEStateManager::WidgetDestroyed(this);
+
+ FreeLocalesChangedObserver();
+ FreeShutdownObserver();
+ DestroyLayerManager();
+
+#ifdef NOISY_WIDGET_LEAKS
+ gNumWidgets--;
+ printf("WIDGETS- = %d\n", gNumWidgets);
+#endif
+}
+
+//-------------------------------------------------------------------------
+//
+// Basic create.
+//
+//-------------------------------------------------------------------------
+void nsBaseWidget::BaseCreate(nsIWidget* aParent, widget::InitData* aInitData) {
+ if (aInitData) {
+ mWindowType = aInitData->mWindowType;
+ mBorderStyle = aInitData->mBorderStyle;
+ mPopupLevel = aInitData->mPopupLevel;
+ mPopupType = aInitData->mPopupHint;
+ mHasRemoteContent = aInitData->mHasRemoteContent;
+ mIsPIPWindow = aInitData->mPIPWindow;
+ }
+
+ if (aParent) {
+ aParent->AddChild(this);
+ }
+}
+
+//-------------------------------------------------------------------------
+//
+// Accessor functions to get/set the client data
+//
+//-------------------------------------------------------------------------
+
+nsIWidgetListener* nsBaseWidget::GetWidgetListener() const {
+ return mWidgetListener;
+}
+
+void nsBaseWidget::SetWidgetListener(nsIWidgetListener* aWidgetListener) {
+ mWidgetListener = aWidgetListener;
+}
+
+already_AddRefed<nsIWidget> nsBaseWidget::CreateChild(
+ const LayoutDeviceIntRect& aRect, widget::InitData* aInitData,
+ bool aForceUseIWidgetParent) {
+ nsIWidget* parent = this;
+ nsNativeWidget nativeParent = nullptr;
+
+ if (!aForceUseIWidgetParent) {
+ // Use only either parent or nativeParent, not both, to match
+ // existing code. Eventually Create() should be divested of its
+ // nativeWidget parameter.
+ nativeParent = parent ? parent->GetNativeData(NS_NATIVE_WIDGET) : nullptr;
+ parent = nativeParent ? nullptr : parent;
+ MOZ_ASSERT(!parent || !nativeParent, "messed up logic");
+ }
+
+ nsCOMPtr<nsIWidget> widget;
+ if (aInitData && aInitData->mWindowType == WindowType::Popup) {
+ widget = AllocateChildPopupWidget();
+ } else {
+ widget = nsIWidget::CreateChildWindow();
+ }
+
+ if (widget && mNeedFastSnaphot) {
+ widget->SetNeedFastSnaphot();
+ }
+
+ if (widget &&
+ NS_SUCCEEDED(widget->Create(parent, nativeParent, aRect, aInitData))) {
+ return widget.forget();
+ }
+
+ return nullptr;
+}
+
+// Attach a view to our widget which we'll send events to.
+void nsBaseWidget::AttachViewToTopLevel(bool aUseAttachedEvents) {
+ NS_ASSERTION((mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog ||
+ mWindowType == WindowType::Invisible ||
+ mWindowType == WindowType::Child),
+ "Can't attach to window of that type");
+
+ mUseAttachedEvents = aUseAttachedEvents;
+}
+
+nsIWidgetListener* nsBaseWidget::GetAttachedWidgetListener() const {
+ return mAttachedWidgetListener;
+}
+
+nsIWidgetListener* nsBaseWidget::GetPreviouslyAttachedWidgetListener() {
+ return mPreviouslyAttachedWidgetListener;
+}
+
+void nsBaseWidget::SetPreviouslyAttachedWidgetListener(
+ nsIWidgetListener* aListener) {
+ mPreviouslyAttachedWidgetListener = aListener;
+}
+
+void nsBaseWidget::SetAttachedWidgetListener(nsIWidgetListener* aListener) {
+ mAttachedWidgetListener = aListener;
+}
+
+//-------------------------------------------------------------------------
+//
+// Close this nsBaseWidget
+//
+//-------------------------------------------------------------------------
+void nsBaseWidget::Destroy() {
+ DestroyCompositor();
+
+ // Just in case our parent is the only ref to us
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+ // disconnect from the parent
+ nsIWidget* parent = GetParent();
+ if (parent) {
+ parent->RemoveChild(this);
+ }
+}
+
+//-------------------------------------------------------------------------
+//
+// Get this nsBaseWidget parent
+//
+//-------------------------------------------------------------------------
+nsIWidget* nsBaseWidget::GetParent(void) { return nullptr; }
+
+//-------------------------------------------------------------------------
+//
+// Get this nsBaseWidget top level widget
+//
+//-------------------------------------------------------------------------
+nsIWidget* nsBaseWidget::GetTopLevelWidget() {
+ nsIWidget *topLevelWidget = nullptr, *widget = this;
+ while (widget) {
+ topLevelWidget = widget;
+ widget = widget->GetParent();
+ }
+ return topLevelWidget;
+}
+
+//-------------------------------------------------------------------------
+//
+// Get this nsBaseWidget's top (non-sheet) parent (if it's a sheet)
+//
+//-------------------------------------------------------------------------
+nsIWidget* nsBaseWidget::GetSheetWindowParent(void) { return nullptr; }
+
+float nsBaseWidget::GetDPI() { return 96.0f; }
+
+CSSToLayoutDeviceScale nsIWidget::GetDefaultScale() {
+ double devPixelsPerCSSPixel = StaticPrefs::layout_css_devPixelsPerPx();
+
+ if (devPixelsPerCSSPixel <= 0.0) {
+ devPixelsPerCSSPixel = GetDefaultScaleInternal();
+ }
+
+ return CSSToLayoutDeviceScale(devPixelsPerCSSPixel);
+}
+
+nsIntSize nsIWidget::CustomCursorSize(const Cursor& aCursor) {
+ MOZ_ASSERT(aCursor.IsCustom());
+ int32_t width = 0;
+ int32_t height = 0;
+ aCursor.mContainer->GetWidth(&width);
+ aCursor.mContainer->GetHeight(&height);
+ aCursor.mResolution.ApplyTo(width, height);
+ return {width, height};
+}
+
+LayoutDeviceIntSize nsIWidget::ClientToWindowSizeDifference() {
+ auto margin = ClientToWindowMargin();
+ MOZ_ASSERT(margin.top >= 0, "Window should be bigger than client area");
+ MOZ_ASSERT(margin.left >= 0, "Window should be bigger than client area");
+ MOZ_ASSERT(margin.right >= 0, "Window should be bigger than client area");
+ MOZ_ASSERT(margin.bottom >= 0, "Window should be bigger than client area");
+ return {margin.LeftRight(), margin.TopBottom()};
+}
+
+RefPtr<mozilla::VsyncDispatcher> nsIWidget::GetVsyncDispatcher() {
+ return nullptr;
+}
+
+//-------------------------------------------------------------------------
+//
+// Add a child to the list of children
+//
+//-------------------------------------------------------------------------
+void nsBaseWidget::AddChild(nsIWidget* aChild) {
+ MOZ_ASSERT(!aChild->GetNextSibling() && !aChild->GetPrevSibling(),
+ "aChild not properly removed from its old child list");
+
+ if (!mFirstChild) {
+ mFirstChild = mLastChild = aChild;
+ } else {
+ // append to the list
+ MOZ_ASSERT(mLastChild);
+ MOZ_ASSERT(!mLastChild->GetNextSibling());
+ mLastChild->SetNextSibling(aChild);
+ aChild->SetPrevSibling(mLastChild);
+ mLastChild = aChild;
+ }
+}
+
+//-------------------------------------------------------------------------
+//
+// Remove a child from the list of children
+//
+//-------------------------------------------------------------------------
+void nsBaseWidget::RemoveChild(nsIWidget* aChild) {
+#ifdef DEBUG
+# ifdef XP_MACOSX
+ // nsCocoaWindow doesn't implement GetParent, so in that case parent will be
+ // null and we'll just have to do without this assertion.
+ nsIWidget* parent = aChild->GetParent();
+ NS_ASSERTION(!parent || parent == this, "Not one of our kids!");
+# else
+ MOZ_RELEASE_ASSERT(aChild->GetParent() == this, "Not one of our kids!");
+# endif
+#endif
+
+ if (mLastChild == aChild) {
+ mLastChild = mLastChild->GetPrevSibling();
+ }
+ if (mFirstChild == aChild) {
+ mFirstChild = mFirstChild->GetNextSibling();
+ }
+
+ // Now remove from the list. Make sure that we pass ownership of the tail
+ // of the list correctly before we have aChild let go of it.
+ nsIWidget* prev = aChild->GetPrevSibling();
+ nsIWidget* next = aChild->GetNextSibling();
+ if (prev) {
+ prev->SetNextSibling(next);
+ }
+ if (next) {
+ next->SetPrevSibling(prev);
+ }
+
+ aChild->SetNextSibling(nullptr);
+ aChild->SetPrevSibling(nullptr);
+}
+
+//-------------------------------------------------------------------------
+//
+// Sets widget's position within its parent's child list.
+//
+//-------------------------------------------------------------------------
+void nsBaseWidget::SetZIndex(int32_t aZIndex) {
+ // Hold a ref to ourselves just in case, since we're going to remove
+ // from our parent.
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ mZIndex = aZIndex;
+
+ // reorder this child in its parent's list.
+ auto* parent = static_cast<nsBaseWidget*>(GetParent());
+ if (parent) {
+ parent->RemoveChild(this);
+ // Scope sib outside the for loop so we can check it afterward
+ nsIWidget* sib = parent->GetFirstChild();
+ for (; sib; sib = sib->GetNextSibling()) {
+ int32_t childZIndex = GetZIndex();
+ if (aZIndex < childZIndex) {
+ // Insert ourselves before sib
+ nsIWidget* prev = sib->GetPrevSibling();
+ mNextSibling = sib;
+ mPrevSibling = prev;
+ sib->SetPrevSibling(this);
+ if (prev) {
+ prev->SetNextSibling(this);
+ } else {
+ NS_ASSERTION(sib == parent->mFirstChild, "Broken child list");
+ // We've taken ownership of sib, so it's safe to have parent let
+ // go of it
+ parent->mFirstChild = this;
+ }
+ PlaceBehind(eZPlacementBelow, sib, false);
+ break;
+ }
+ }
+ // were we added to the list?
+ if (!sib) {
+ parent->AddChild(this);
+ }
+ }
+}
+
+void nsBaseWidget::GetWorkspaceID(nsAString& workspaceID) {
+ workspaceID.Truncate();
+}
+
+void nsBaseWidget::MoveToWorkspace(const nsAString& workspaceID) {
+ // Noop.
+}
+
+//-------------------------------------------------------------------------
+//
+// Get this component cursor
+//
+//-------------------------------------------------------------------------
+
+void nsBaseWidget::SetCursor(const Cursor& aCursor) { mCursor = aCursor; }
+
+void nsBaseWidget::SetCustomCursorAllowed(bool aIsAllowed) {
+ if (aIsAllowed != mCustomCursorAllowed) {
+ mCustomCursorAllowed = aIsAllowed;
+ mUpdateCursor = true;
+ SetCursor(mCursor);
+ }
+}
+
+//-------------------------------------------------------------------------
+//
+// Window transparency methods
+//
+//-------------------------------------------------------------------------
+
+void nsBaseWidget::SetTransparencyMode(TransparencyMode aMode) {}
+
+TransparencyMode nsBaseWidget::GetTransparencyMode() {
+ return TransparencyMode::Opaque;
+}
+
+/* virtual */
+void nsBaseWidget::PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration,
+ nsISupports* aData,
+ nsIRunnable* aCallback) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Should never call PerformFullscreenTransition on nsBaseWidget");
+}
+
+//-------------------------------------------------------------------------
+//
+// Put the window into full-screen mode
+//
+//-------------------------------------------------------------------------
+void nsBaseWidget::InfallibleMakeFullScreen(bool aFullScreen) {
+#define MOZ_FORMAT_RECT(fmtstr) "[" fmtstr "," fmtstr " " fmtstr "x" fmtstr "]"
+#define MOZ_SPLAT_RECT(rect) \
+ (rect).X(), (rect).Y(), (rect).Width(), (rect).Height()
+
+ // Windows which can be made fullscreen are exactly those which are located on
+ // the desktop, rather than being a child of some other window.
+ MOZ_DIAGNOSTIC_ASSERT(BoundsUseDesktopPixels(),
+ "non-desktop windows cannot be made fullscreen");
+
+ // Ensure that the OS chrome is hidden/shown before we resize and/or exit the
+ // function.
+ //
+ // HideWindowChrome() may (depending on platform, implementation details, and
+ // OS-level user preferences) alter the reported size of the window. The
+ // obvious and principled solution is socks-and-shoes:
+ // - On entering fullscreen mode: hide window chrome, then perform resize.
+ // - On leaving fullscreen mode: unperform resize, then show window chrome.
+ //
+ // ... unfortunately, HideWindowChrome() requires Resize() to be called
+ // afterwards (see bug 498835), which prevents this from being done in a
+ // straightforward way.
+ //
+ // Instead, we always call HideWindowChrome() just before we call Resize().
+ // This at least ensures that our measurements are consistently taken in a
+ // pre-transition state.
+ //
+ // ... unfortunately again, coupling HideWindowChrome() to Resize() means that
+ // we have to worry about the possibility of control flows that don't call
+ // Resize() at all. (That shouldn't happen, but it's not trivial to rule out.)
+ // We therefore set up a fallback to fix up the OS chrome if it hasn't been
+ // done at exit time.
+ bool hasAdjustedOSChrome = false;
+ const auto adjustOSChrome = [&]() {
+ if (hasAdjustedOSChrome) {
+ MOZ_ASSERT_UNREACHABLE("window chrome should only be adjusted once");
+ return;
+ }
+ HideWindowChrome(aFullScreen);
+ hasAdjustedOSChrome = true;
+ };
+ const auto adjustChromeOnScopeExit = MakeScopeExit([&]() {
+ if (hasAdjustedOSChrome) {
+ return;
+ }
+
+ MOZ_LOG(sBaseWidgetLog, LogLevel::Warning,
+ ("window was not resized within InfallibleMakeFullScreen()"));
+
+ // Hide chrome and "resize" the window to its current size.
+ auto rect = GetBounds();
+ adjustOSChrome();
+ Resize(rect.X(), rect.Y(), rect.Width(), rect.Height(), true);
+ });
+
+ // Attempt to resize to `rect`.
+ //
+ // Returns the actual rectangle resized to. (This may differ from `rect`, if
+ // the OS is unhappy with it. See bug 1786226.)
+ const auto doReposition = [&](auto rect) -> void {
+ static_assert(std::is_base_of_v<DesktopPixel,
+ std::remove_reference_t<decltype(rect)>>,
+ "doReposition requires a rectangle using desktop pixels");
+
+ if (MOZ_LOG_TEST(sBaseWidgetLog, LogLevel::Debug)) {
+ const DesktopRect previousSize =
+ GetScreenBounds() / GetDesktopToDeviceScale();
+ MOZ_LOG(sBaseWidgetLog, LogLevel::Debug,
+ ("before resize: " MOZ_FORMAT_RECT("%f"),
+ MOZ_SPLAT_RECT(previousSize)));
+ }
+
+ adjustOSChrome();
+ Resize(rect.X(), rect.Y(), rect.Width(), rect.Height(), true);
+
+ if (MOZ_LOG_TEST(sBaseWidgetLog, LogLevel::Warning)) {
+ // `rect` may have any underlying data type; coerce to float to
+ // simplify printf-style logging
+ const gfx::RectTyped<DesktopPixel, float> rectAsFloat{rect};
+
+ // The OS may have objected to the target position. That's not necessarily
+ // a problem -- it'll happen regularly on Macs with camera notches in the
+ // monitor, for instance (see bug 1786226) -- but it probably deserves to
+ // be called out.
+ //
+ // Since there's floating-point math involved, the actual values may be
+ // off by a few ulps -- as an upper bound, perhaps 8 * FLT_EPSILON *
+ // max(MOZ_SPLAT_RECT(rect)) -- but 0.01 should be several orders of
+ // magnitude bigger than that.
+
+ const auto postResizeRectRaw = GetScreenBounds();
+ const auto postResizeRect = postResizeRectRaw / GetDesktopToDeviceScale();
+ const bool succeeded = postResizeRect.WithinEpsilonOf(rectAsFloat, 0.01);
+
+ if (succeeded) {
+ MOZ_LOG(sBaseWidgetLog, LogLevel::Debug,
+ ("resized to: " MOZ_FORMAT_RECT("%f"),
+ MOZ_SPLAT_RECT(rectAsFloat)));
+ } else {
+ MOZ_LOG(sBaseWidgetLog, LogLevel::Warning,
+ ("attempted to resize to: " MOZ_FORMAT_RECT("%f"),
+ MOZ_SPLAT_RECT(rectAsFloat)));
+ MOZ_LOG(sBaseWidgetLog, LogLevel::Warning,
+ ("... but ended up at: " MOZ_FORMAT_RECT("%f"),
+ MOZ_SPLAT_RECT(postResizeRect)));
+ }
+
+ MOZ_LOG(
+ sBaseWidgetLog, LogLevel::Verbose,
+ ("(... which, before DPI adjustment, is:" MOZ_FORMAT_RECT("%d") ")",
+ MOZ_SPLAT_RECT(postResizeRectRaw)));
+ }
+ };
+
+ if (aFullScreen) {
+ if (!mSavedBounds) {
+ mSavedBounds = Some(FullscreenSavedState());
+ }
+ // save current position
+ mSavedBounds->windowRect = GetScreenBounds() / GetDesktopToDeviceScale();
+
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (!screen) {
+ return;
+ }
+
+ // Move to fill the screen.
+ doReposition(screen->GetRectDisplayPix());
+ // Save off the new position. (This may differ from GetRectDisplayPix(), if
+ // the OS was unhappy with it. See bug 1786226.)
+ mSavedBounds->screenRect = GetScreenBounds() / GetDesktopToDeviceScale();
+ } else {
+ if (!mSavedBounds) {
+ // This should never happen, at present, since we don't make windows
+ // fullscreen at their creation time; but it's not logically impossible.
+ MOZ_ASSERT(false, "fullscreen window did not have saved position");
+ return;
+ }
+
+ // Figure out where to go from here.
+ //
+ // Fortunately, since we're currently fullscreen (and other code should be
+ // handling _keeping_ us fullscreen even after display-layout changes),
+ // there's an obvious choice for which display we should attach to; all we
+ // need to determine is where on that display we should go.
+
+ const DesktopRect currentWinRect =
+ GetScreenBounds() / GetDesktopToDeviceScale();
+
+ // Optimization: if where we are is where we were, then where we originally
+ // came from is where we're going to go.
+ if (currentWinRect == DesktopRect(mSavedBounds->screenRect)) {
+ MOZ_LOG(sBaseWidgetLog, LogLevel::Debug,
+ ("no location change detected; returning to saved location"));
+ doReposition(mSavedBounds->windowRect);
+ return;
+ }
+
+ /*
+ General case: figure out where we're going to go by dividing where we are
+ by where we were, and then multiplying by where we originally came from.
+
+ Less abstrusely: resize so that we occupy the same proportional position
+ on our current display after leaving fullscreen as we occupied on our
+ previous display before entering fullscreen.
+
+ (N.B.: We do not clamp. If we were only partially on the old display,
+ we'll be only partially on the new one, too.)
+ */
+
+ MOZ_LOG(sBaseWidgetLog, LogLevel::Debug,
+ ("location change detected; computing new destination"));
+
+ // splat: convert an arbitrary Rect into a tuple, for syntactic convenience.
+ const auto splat = [](auto rect) {
+ return std::tuple(rect.X(), rect.Y(), rect.Width(), rect.Height());
+ };
+
+ // remap: find the unique affine mapping which transforms `src` to `dst`,
+ // and apply it to `val`.
+ using Range = std::pair<float, float>;
+ const auto remap = [](Range dst, Range src, float val) {
+ // linear interpolation and its inverse: lerp(a, b, invlerp(a, b, t)) == t
+ const auto lerp = [](float lo, float hi, float t) {
+ return lo + t * (hi - lo);
+ };
+ const auto invlerp = [](float lo, float hi, float mid) {
+ return (mid - lo) / (hi - lo);
+ };
+
+ const auto [dst_a, dst_b] = dst;
+ const auto [src_a, src_b] = src;
+ return lerp(dst_a, dst_b, invlerp(src_a, src_b, val));
+ };
+
+ // original position
+ const auto [px, py, pw, ph] = splat(mSavedBounds->windowRect);
+ // source desktop rect
+ const auto [sx, sy, sw, sh] = splat(mSavedBounds->screenRect);
+ // target desktop rect
+ const auto [tx, ty, tw, th] = splat(currentWinRect);
+
+ const float nx = remap({tx, tx + tw}, {sx, sx + sw}, px);
+ const float ny = remap({ty, ty + th}, {sy, sy + sh}, py);
+ const float nw = remap({0, tw}, {0, sw}, pw);
+ const float nh = remap({0, th}, {0, sh}, ph);
+
+ doReposition(DesktopRect{nx, ny, nw, nh});
+ }
+
+#undef MOZ_SPLAT_RECT
+#undef MOZ_FORMAT_RECT
+}
+
+nsresult nsBaseWidget::MakeFullScreen(bool aFullScreen) {
+ InfallibleMakeFullScreen(aFullScreen);
+ return NS_OK;
+}
+
+nsBaseWidget::AutoLayerManagerSetup::AutoLayerManagerSetup(
+ nsBaseWidget* aWidget, gfxContext* aTarget, BufferMode aDoubleBuffering)
+ : mWidget(aWidget) {
+ WindowRenderer* renderer = mWidget->GetWindowRenderer();
+ if (renderer->AsFallback()) {
+ mRenderer = renderer->AsFallback();
+ mRenderer->SetTarget(aTarget, aDoubleBuffering);
+ }
+}
+
+nsBaseWidget::AutoLayerManagerSetup::~AutoLayerManagerSetup() {
+ if (mRenderer) {
+ mRenderer->SetTarget(nullptr, mozilla::layers::BufferMode::BUFFER_NONE);
+ }
+}
+
+bool nsBaseWidget::IsSmallPopup() const {
+ return mWindowType == WindowType::Popup && mPopupType != PopupType::Panel;
+}
+
+bool nsBaseWidget::ComputeShouldAccelerate() {
+ return gfx::gfxConfig::IsEnabled(gfx::Feature::HW_COMPOSITING) &&
+ (WidgetTypeSupportsAcceleration() ||
+ StaticPrefs::gfx_webrender_unaccelerated_widget_force());
+}
+
+bool nsBaseWidget::UseAPZ() {
+ return (gfxPlatform::AsyncPanZoomEnabled() &&
+ (mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Child ||
+ ((mWindowType == WindowType::Popup ||
+ mWindowType == WindowType::Dialog) &&
+ HasRemoteContent() && StaticPrefs::apz_popups_enabled())));
+}
+
+void nsBaseWidget::CreateCompositor() {
+ LayoutDeviceIntRect rect = GetBounds();
+ CreateCompositor(rect.Width(), rect.Height());
+}
+
+void nsIWidget::PauseOrResumeCompositor(bool aPause) {
+ auto* renderer = GetRemoteRenderer();
+ if (!renderer) {
+ return;
+ }
+ if (aPause) {
+ renderer->SendPause();
+ } else {
+ renderer->SendResume();
+ }
+}
+
+already_AddRefed<GeckoContentController>
+nsBaseWidget::CreateRootContentController() {
+ RefPtr<GeckoContentController> controller =
+ new ChromeProcessController(this, mAPZEventState, mAPZC);
+ return controller.forget();
+}
+
+void nsBaseWidget::ConfigureAPZCTreeManager() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mAPZC);
+
+ mAPZC->SetDPI(GetDPI());
+
+ if (StaticPrefs::apz_keyboard_enabled_AtStartup()) {
+ KeyboardMap map = RootWindowGlobalKeyListener::CollectKeyboardShortcuts();
+ mAPZC->SetKeyboardMap(map);
+ }
+
+ ContentReceivedInputBlockCallback callback(
+ [treeManager = RefPtr{mAPZC.get()}](uint64_t aInputBlockId,
+ bool aPreventDefault) {
+ MOZ_ASSERT(NS_IsMainThread());
+ treeManager->ContentReceivedInputBlock(aInputBlockId, aPreventDefault);
+ });
+ mAPZEventState = new APZEventState(this, std::move(callback));
+
+ mRootContentController = CreateRootContentController();
+ if (mRootContentController) {
+ mCompositorSession->SetContentController(mRootContentController);
+ }
+
+ // When APZ is enabled, we can actually enable raw touch events because we
+ // have code that can deal with them properly. If APZ is not enabled, this
+ // function doesn't get called.
+ if (StaticPrefs::dom_w3c_touch_events_enabled()) {
+ RegisterTouchWindow();
+ }
+}
+
+void nsBaseWidget::ConfigureAPZControllerThread() {
+ // By default the controller thread is the main thread.
+ APZThreadUtils::SetControllerThread(NS_GetCurrentThread());
+}
+
+void nsBaseWidget::SetConfirmedTargetAPZC(
+ uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) const {
+ mAPZC->SetTargetAPZC(aInputBlockId, aTargets);
+}
+
+void nsBaseWidget::UpdateZoomConstraints(
+ const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
+ const Maybe<ZoomConstraints>& aConstraints) {
+ if (!mCompositorSession || !mAPZC) {
+ MOZ_ASSERT_IF(mInitialZoomConstraints,
+ mInitialZoomConstraints->mViewID == aViewId);
+ if (aConstraints) {
+ // We have some constraints, but the compositor and APZC aren't
+ // created yet. Save these so we can use them later.
+ mInitialZoomConstraints = Some(
+ InitialZoomConstraints(aPresShellId, aViewId, aConstraints.ref()));
+ } else {
+ mInitialZoomConstraints.reset();
+ }
+ return;
+ }
+ LayersId layersId = mCompositorSession->RootLayerTreeId();
+ mAPZC->UpdateZoomConstraints(
+ ScrollableLayerGuid(layersId, aPresShellId, aViewId), aConstraints);
+}
+
+bool nsBaseWidget::AsyncPanZoomEnabled() const { return !!mAPZC; }
+
+nsEventStatus nsBaseWidget::ProcessUntransformedAPZEvent(
+ WidgetInputEvent* aEvent, const APZEventResult& aApzResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ ScrollableLayerGuid targetGuid = aApzResult.mTargetGuid;
+ uint64_t inputBlockId = aApzResult.mInputBlockId;
+ InputAPZContext context(aApzResult.mTargetGuid, inputBlockId,
+ aApzResult.GetStatus());
+
+ // Make a copy of the original event for the APZCCallbackHelper helpers that
+ // we call later, because the event passed to DispatchEvent can get mutated in
+ // ways that we don't want (i.e. touch points can get stripped out).
+ nsEventStatus status;
+ UniquePtr<WidgetEvent> original(aEvent->Duplicate());
+ DispatchEvent(aEvent, status);
+
+ if (mAPZC && !InputAPZContext::WasRoutedToChildProcess() &&
+ !InputAPZContext::WasDropped() && inputBlockId) {
+ // EventStateManager did not route the event into the child process and
+ // the event was dispatched in the parent process.
+ // It's safe to communicate to APZ that the event has been processed.
+ // Note that here aGuid.mLayersId might be different from
+ // mCompositorSession->RootLayerTreeId() because the event might have gotten
+ // hit-tested by APZ to be targeted at a child process, but a parent process
+ // event listener called preventDefault on it. In that case aGuid.mLayersId
+ // would still be the layers id for the child process, but the event would
+ // not have actually gotten routed to the child process. The main-thread
+ // hit-test result therefore needs to use the parent process layers id.
+ LayersId rootLayersId = mCompositorSession->RootLayerTreeId();
+
+ RefPtr<DisplayportSetListener> postLayerization;
+ if (WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent()) {
+ nsTArray<TouchBehaviorFlags> allowedTouchBehaviors;
+ if (touchEvent->mMessage == eTouchStart) {
+ auto& originalEvent = *original->AsTouchEvent();
+ MOZ_ASSERT(NS_IsMainThread());
+ allowedTouchBehaviors = TouchActionHelper::GetAllowedTouchBehavior(
+ this, GetDocument(), originalEvent);
+ if (!allowedTouchBehaviors.IsEmpty()) {
+ mAPZC->SetAllowedTouchBehavior(inputBlockId, allowedTouchBehaviors);
+ }
+ postLayerization = APZCCallbackHelper::SendSetTargetAPZCNotification(
+ this, GetDocument(), originalEvent, rootLayersId, inputBlockId);
+ }
+ mAPZEventState->ProcessTouchEvent(*touchEvent, targetGuid, inputBlockId,
+ aApzResult.GetStatus(), status,
+ std::move(allowedTouchBehaviors));
+ } else if (WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent()) {
+ MOZ_ASSERT(wheelEvent->mFlags.mHandledByAPZ);
+ postLayerization = APZCCallbackHelper::SendSetTargetAPZCNotification(
+ this, GetDocument(), *original->AsWheelEvent(), rootLayersId,
+ inputBlockId);
+ if (wheelEvent->mCanTriggerSwipe) {
+ ReportSwipeStarted(inputBlockId, wheelEvent->TriggersSwipe());
+ }
+ mAPZEventState->ProcessWheelEvent(*wheelEvent, inputBlockId);
+ } else if (WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent()) {
+ MOZ_ASSERT(mouseEvent->mFlags.mHandledByAPZ);
+ postLayerization = APZCCallbackHelper::SendSetTargetAPZCNotification(
+ this, GetDocument(), *original->AsMouseEvent(), rootLayersId,
+ inputBlockId);
+ mAPZEventState->ProcessMouseEvent(*mouseEvent, inputBlockId);
+ }
+ if (postLayerization) {
+ postLayerization->Register();
+ }
+ }
+
+ return status;
+}
+
+template <class InputType, class EventType>
+class DispatchEventOnMainThread : public Runnable {
+ public:
+ DispatchEventOnMainThread(const InputType& aInput, nsBaseWidget* aWidget,
+ const APZEventResult& aAPZResult)
+ : mozilla::Runnable("DispatchEventOnMainThread"),
+ mInput(aInput),
+ mWidget(aWidget),
+ mAPZResult(aAPZResult) {}
+
+ NS_IMETHOD Run() override {
+ EventType event = mInput.ToWidgetEvent(mWidget);
+ mWidget->ProcessUntransformedAPZEvent(&event, mAPZResult);
+ return NS_OK;
+ }
+
+ private:
+ InputType mInput;
+ nsBaseWidget* mWidget;
+ APZEventResult mAPZResult;
+};
+
+template <class InputType, class EventType>
+class DispatchInputOnControllerThread : public Runnable {
+ public:
+ enum class APZOnly { Yes, No };
+ DispatchInputOnControllerThread(const EventType& aEvent,
+ IAPZCTreeManager* aAPZC,
+ nsBaseWidget* aWidget,
+ APZOnly aAPZOnly = APZOnly::No)
+ : mozilla::Runnable("DispatchInputOnControllerThread"),
+ mMainMessageLoop(MessageLoop::current()),
+ mInput(aEvent),
+ mAPZC(aAPZC),
+ mWidget(aWidget),
+ mAPZOnly(aAPZOnly) {}
+
+ NS_IMETHOD Run() override {
+ APZEventResult result = mAPZC->InputBridge()->ReceiveInputEvent(mInput);
+ if (mAPZOnly == APZOnly::Yes ||
+ result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return NS_OK;
+ }
+ RefPtr<Runnable> r = new DispatchEventOnMainThread<InputType, EventType>(
+ mInput, mWidget, result);
+ mMainMessageLoop->PostTask(r.forget());
+ return NS_OK;
+ }
+
+ private:
+ MessageLoop* mMainMessageLoop;
+ InputType mInput;
+ RefPtr<IAPZCTreeManager> mAPZC;
+ nsBaseWidget* mWidget;
+ const APZOnly mAPZOnly;
+};
+
+void nsBaseWidget::DispatchTouchInput(MultiTouchInput& aInput,
+ uint16_t aInputSource) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aInputSource ==
+ mozilla::dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH ||
+ aInputSource == mozilla::dom::MouseEvent_Binding::MOZ_SOURCE_PEN);
+ if (mAPZC) {
+ MOZ_ASSERT(APZThreadUtils::IsControllerThread());
+
+ APZEventResult result = mAPZC->InputBridge()->ReceiveInputEvent(aInput);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+
+ WidgetTouchEvent event = aInput.ToWidgetEvent(this, aInputSource);
+ ProcessUntransformedAPZEvent(&event, result);
+ } else {
+ WidgetTouchEvent event = aInput.ToWidgetEvent(this, aInputSource);
+
+ nsEventStatus status;
+ DispatchEvent(&event, status);
+ }
+}
+
+void nsBaseWidget::DispatchPanGestureInput(PanGestureInput& aInput) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAPZC) {
+ MOZ_ASSERT(APZThreadUtils::IsControllerThread());
+
+ APZEventResult result = mAPZC->InputBridge()->ReceiveInputEvent(aInput);
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+
+ WidgetWheelEvent event = aInput.ToWidgetEvent(this);
+ ProcessUntransformedAPZEvent(&event, result);
+ } else {
+ WidgetWheelEvent event = aInput.ToWidgetEvent(this);
+ nsEventStatus status;
+ DispatchEvent(&event, status);
+ }
+}
+
+void nsBaseWidget::DispatchPinchGestureInput(PinchGestureInput& aInput) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAPZC) {
+ MOZ_ASSERT(APZThreadUtils::IsControllerThread());
+ APZEventResult result = mAPZC->InputBridge()->ReceiveInputEvent(aInput);
+
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ WidgetWheelEvent event = aInput.ToWidgetEvent(this);
+ ProcessUntransformedAPZEvent(&event, result);
+ } else {
+ WidgetWheelEvent event = aInput.ToWidgetEvent(this);
+ nsEventStatus status;
+ DispatchEvent(&event, status);
+ }
+}
+
+nsIWidget::ContentAndAPZEventStatus nsBaseWidget::DispatchInputEvent(
+ WidgetInputEvent* aEvent) {
+ nsIWidget::ContentAndAPZEventStatus status;
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mAPZC) {
+ if (APZThreadUtils::IsControllerThread()) {
+ APZEventResult result = mAPZC->InputBridge()->ReceiveInputEvent(*aEvent);
+ status.mApzStatus = result.GetStatus();
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return status;
+ }
+ status.mContentStatus = ProcessUntransformedAPZEvent(aEvent, result);
+ return status;
+ }
+ // Most drag events aren't able to converted to MouseEvent except to
+ // eDragStart and eDragEnd.
+ const bool canDispatchToApzc =
+ !aEvent->AsDragEvent() ||
+ aEvent->AsDragEvent()->CanConvertToInputData();
+ if (canDispatchToApzc) {
+ if (WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent()) {
+ RefPtr<Runnable> r =
+ new DispatchInputOnControllerThread<ScrollWheelInput,
+ WidgetWheelEvent>(*wheelEvent,
+ mAPZC, this);
+ APZThreadUtils::RunOnControllerThread(std::move(r));
+ status.mContentStatus = nsEventStatus_eConsumeDoDefault;
+ return status;
+ }
+ if (WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent()) {
+ RefPtr<Runnable> r =
+ new DispatchInputOnControllerThread<MouseInput, WidgetMouseEvent>(
+ *mouseEvent, mAPZC, this);
+ APZThreadUtils::RunOnControllerThread(std::move(r));
+ status.mContentStatus = nsEventStatus_eConsumeDoDefault;
+ return status;
+ }
+ if (WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent()) {
+ RefPtr<Runnable> r =
+ new DispatchInputOnControllerThread<MultiTouchInput,
+ WidgetTouchEvent>(*touchEvent,
+ mAPZC, this);
+ APZThreadUtils::RunOnControllerThread(std::move(r));
+ status.mContentStatus = nsEventStatus_eConsumeDoDefault;
+ return status;
+ }
+ // Allow dispatching keyboard/drag events on Gecko thread
+ // without sending them to APZ
+
+ // FIXME: APZ can handle keyboard events now, we should
+ // be sending them to APZ here
+ MOZ_ASSERT(aEvent->AsKeyboardEvent() || aEvent->AsDragEvent());
+ }
+ }
+
+ DispatchEvent(aEvent, status.mContentStatus);
+ return status;
+}
+
+void nsBaseWidget::DispatchEventToAPZOnly(mozilla::WidgetInputEvent* aEvent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAPZC) {
+ if (APZThreadUtils::IsControllerThread()) {
+ mAPZC->InputBridge()->ReceiveInputEvent(*aEvent);
+ return;
+ }
+
+ if (WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent()) {
+ RefPtr<Runnable> r =
+ new DispatchInputOnControllerThread<MouseInput, WidgetMouseEvent>(
+ *mouseEvent, mAPZC, this,
+ DispatchInputOnControllerThread<MouseInput,
+ WidgetMouseEvent>::APZOnly::Yes);
+ APZThreadUtils::RunOnControllerThread(std::move(r));
+ return;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Not implemented yet");
+ }
+}
+
+bool nsBaseWidget::DispatchWindowEvent(WidgetGUIEvent& event) {
+ nsEventStatus status;
+ DispatchEvent(&event, status);
+ return ConvertStatus(status);
+}
+
+Document* nsBaseWidget::GetDocument() const {
+ if (mWidgetListener) {
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ return presShell->GetDocument();
+ }
+ }
+ return nullptr;
+}
+
+void nsBaseWidget::CreateCompositorVsyncDispatcher() {
+ // Parent directly listens to the vsync source whereas
+ // child process communicate via IPC
+ // Should be called AFTER gfxPlatform is initialized
+ if (XRE_IsParentProcess()) {
+ if (!mCompositorVsyncDispatcherLock) {
+ mCompositorVsyncDispatcherLock =
+ MakeUnique<Mutex>("mCompositorVsyncDispatcherLock");
+ }
+ MutexAutoLock lock(*mCompositorVsyncDispatcherLock.get());
+ if (!mCompositorVsyncDispatcher) {
+ RefPtr<VsyncDispatcher> vsyncDispatcher =
+ gfxPlatform::GetPlatform()->GetGlobalVsyncDispatcher();
+ mCompositorVsyncDispatcher =
+ new CompositorVsyncDispatcher(std::move(vsyncDispatcher));
+ }
+ }
+}
+
+already_AddRefed<CompositorVsyncDispatcher>
+nsBaseWidget::GetCompositorVsyncDispatcher() {
+ MOZ_ASSERT(mCompositorVsyncDispatcherLock.get());
+
+ MutexAutoLock lock(*mCompositorVsyncDispatcherLock.get());
+ RefPtr<CompositorVsyncDispatcher> dispatcher = mCompositorVsyncDispatcher;
+ return dispatcher.forget();
+}
+
+already_AddRefed<WebRenderLayerManager> nsBaseWidget::CreateCompositorSession(
+ int aWidth, int aHeight, CompositorOptions* aOptionsOut) {
+ MOZ_ASSERT(aOptionsOut);
+
+ do {
+ CreateCompositorVsyncDispatcher();
+
+ gfx::GPUProcessManager* gpu = gfx::GPUProcessManager::Get();
+ // Make sure GPU process is ready for use.
+ // If it failed to connect to GPU process, GPU process usage is disabled in
+ // EnsureGPUReady(). It could update gfxVars and gfxConfigs.
+ nsresult rv = gpu->EnsureGPUReady();
+ if (NS_WARN_IF(rv == NS_ERROR_ILLEGAL_DURING_SHUTDOWN)) {
+ return nullptr;
+ }
+
+ // If widget type does not supports acceleration, we may be allowed to use
+ // software WebRender instead.
+ bool supportsAcceleration = WidgetTypeSupportsAcceleration();
+ bool enableSWWR = true;
+ if (supportsAcceleration ||
+ StaticPrefs::gfx_webrender_unaccelerated_widget_force()) {
+ enableSWWR = gfx::gfxVars::UseSoftwareWebRender();
+ }
+ bool enableAPZ = UseAPZ();
+ CompositorOptions options(enableAPZ, enableSWWR);
+
+#ifdef XP_WIN
+ if (supportsAcceleration) {
+ options.SetAllowSoftwareWebRenderD3D11(
+ gfx::gfxVars::AllowSoftwareWebRenderD3D11());
+ }
+ if (mNeedFastSnaphot) {
+ options.SetNeedFastSnaphot(true);
+ }
+#elif defined(MOZ_WIDGET_ANDROID)
+ MOZ_ASSERT(supportsAcceleration);
+ options.SetAllowSoftwareWebRenderOGL(
+ gfx::gfxVars::AllowSoftwareWebRenderOGL());
+#elif defined(MOZ_WIDGET_GTK)
+ if (supportsAcceleration) {
+ options.SetAllowSoftwareWebRenderOGL(
+ gfx::gfxVars::AllowSoftwareWebRenderOGL());
+ }
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+ // Unconditionally set the compositor as initially paused, as we have not
+ // yet had a chance to send the compositor surface to the GPU process. We
+ // will do so shortly once we have returned to nsWindow::CreateLayerManager,
+ // where we will also resume the compositor if required.
+ options.SetInitiallyPaused(true);
+#else
+ options.SetInitiallyPaused(CompositorInitiallyPaused());
+#endif
+
+ RefPtr<WebRenderLayerManager> lm = new WebRenderLayerManager(this);
+
+ uint64_t innerWindowId = 0;
+ if (Document* doc = GetDocument()) {
+ innerWindowId = doc->InnerWindowID();
+ }
+
+ bool retry = false;
+ mCompositorSession = gpu->CreateTopLevelCompositor(
+ this, lm, GetDefaultScale(), options, UseExternalCompositingSurface(),
+ gfx::IntSize(aWidth, aHeight), innerWindowId, &retry);
+
+ if (mCompositorSession) {
+ TextureFactoryIdentifier textureFactoryIdentifier;
+ nsCString error;
+ lm->Initialize(mCompositorSession->GetCompositorBridgeChild(),
+ wr::AsPipelineId(mCompositorSession->RootLayerTreeId()),
+ &textureFactoryIdentifier, error);
+ if (textureFactoryIdentifier.mParentBackend != LayersBackend::LAYERS_WR) {
+ retry = true;
+ DestroyCompositor();
+ // gfxVars::UseDoubleBufferingWithCompositor() is also disabled.
+ gfx::GPUProcessManager::Get()->DisableWebRender(
+ wr::WebRenderError::INITIALIZE, error);
+ }
+ }
+
+ // We need to retry in a loop because the act of failing to create the
+ // compositor can change our state (e.g. disable WebRender).
+ if (mCompositorSession || !retry) {
+ *aOptionsOut = options;
+ return lm.forget();
+ }
+ } while (true);
+}
+
+void nsBaseWidget::CreateCompositor(int aWidth, int aHeight) {
+ // This makes sure that gfxPlatforms gets initialized if it hasn't by now.
+ gfxPlatform::GetPlatform();
+
+ MOZ_ASSERT(gfxPlatform::UsesOffMainThreadCompositing(),
+ "This function assumes OMTC");
+
+ MOZ_ASSERT(!mCompositorSession && !mCompositorBridgeChild,
+ "Should have properly cleaned up the previous PCompositor pair "
+ "beforehand");
+
+ if (mCompositorBridgeChild) {
+ mCompositorBridgeChild->Destroy();
+ }
+
+ // Recreating this is tricky, as we may still have an old and we need
+ // to make sure it's properly destroyed by calling DestroyCompositor!
+
+ // If we've already received a shutdown notification, don't try
+ // create a new compositor.
+ if (!mShutdownObserver) {
+ return;
+ }
+
+ // The controller thread must be configured before the compositor
+ // session is created, so that the input bridge runs on the right
+ // thread.
+ ConfigureAPZControllerThread();
+
+ CompositorOptions options;
+ RefPtr<WebRenderLayerManager> lm =
+ CreateCompositorSession(aWidth, aHeight, &options);
+ if (!lm) {
+ return;
+ }
+
+ MOZ_ASSERT(mCompositorSession);
+ mCompositorBridgeChild = mCompositorSession->GetCompositorBridgeChild();
+ SetCompositorWidgetDelegate(
+ mCompositorSession->GetCompositorWidgetDelegate());
+
+ if (options.UseAPZ()) {
+ mAPZC = mCompositorSession->GetAPZCTreeManager();
+ ConfigureAPZCTreeManager();
+ } else {
+ mAPZC = nullptr;
+ }
+
+ if (mInitialZoomConstraints) {
+ UpdateZoomConstraints(mInitialZoomConstraints->mPresShellID,
+ mInitialZoomConstraints->mViewID,
+ Some(mInitialZoomConstraints->mConstraints));
+ mInitialZoomConstraints.reset();
+ }
+
+ TextureFactoryIdentifier textureFactoryIdentifier =
+ lm->GetTextureFactoryIdentifier();
+ MOZ_ASSERT(textureFactoryIdentifier.mParentBackend ==
+ LayersBackend::LAYERS_WR);
+ ImageBridgeChild::IdentifyCompositorTextureHost(textureFactoryIdentifier);
+ gfx::VRManagerChild::IdentifyTextureHost(textureFactoryIdentifier);
+
+ WindowUsesOMTC();
+
+ mWindowRenderer = std::move(lm);
+
+ // Only track compositors for top-level windows, since other window types
+ // may use the basic compositor. Except on the OS X - see bug 1306383
+#if defined(XP_MACOSX)
+ bool getCompositorFromThisWindow = true;
+#else
+ bool getCompositorFromThisWindow = mWindowType == WindowType::TopLevel;
+#endif
+
+ if (getCompositorFromThisWindow) {
+ gfxPlatform::GetPlatform()->NotifyCompositorCreated(
+ mWindowRenderer->GetCompositorBackendType());
+ }
+}
+
+void nsBaseWidget::NotifyCompositorSessionLost(CompositorSession* aSession) {
+ MOZ_ASSERT(aSession == mCompositorSession);
+ DestroyLayerManager();
+}
+
+bool nsBaseWidget::ShouldUseOffMainThreadCompositing() {
+ return gfxPlatform::UsesOffMainThreadCompositing();
+}
+
+WindowRenderer* nsBaseWidget::GetWindowRenderer() {
+ if (!mWindowRenderer) {
+ if (!mShutdownObserver) {
+ // We are shutting down, do not try to re-create a LayerManager
+ return nullptr;
+ }
+ // Try to use an async compositor first, if possible
+ if (ShouldUseOffMainThreadCompositing()) {
+ CreateCompositor();
+ }
+
+ if (!mWindowRenderer) {
+ mWindowRenderer = CreateFallbackRenderer();
+ }
+ }
+ return mWindowRenderer;
+}
+
+WindowRenderer* nsBaseWidget::CreateFallbackRenderer() {
+ return new FallbackRenderer;
+}
+
+CompositorBridgeChild* nsBaseWidget::GetRemoteRenderer() {
+ return mCompositorBridgeChild;
+}
+
+void nsBaseWidget::ClearCachedWebrenderResources() {
+ if (!mWindowRenderer || !mWindowRenderer->AsWebRender()) {
+ return;
+ }
+ mWindowRenderer->AsWebRender()->ClearCachedResources();
+}
+
+void nsBaseWidget::ClearWebrenderAnimationResources() {
+ if (!mWindowRenderer || !mWindowRenderer->AsWebRender()) {
+ return;
+ }
+ mWindowRenderer->AsWebRender()->ClearAnimationResources();
+}
+
+bool nsBaseWidget::SetNeedFastSnaphot() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(!mCompositorSession);
+
+ if (!XRE_IsParentProcess() || mCompositorSession) {
+ return false;
+ }
+
+ mNeedFastSnaphot = true;
+ return true;
+}
+
+already_AddRefed<gfx::DrawTarget> nsBaseWidget::StartRemoteDrawing() {
+ return nullptr;
+}
+
+uint32_t nsBaseWidget::GetGLFrameBufferFormat() { return LOCAL_GL_RGBA; }
+
+//-------------------------------------------------------------------------
+//
+// Destroy the window
+//
+//-------------------------------------------------------------------------
+void nsBaseWidget::OnDestroy() {
+ if (mTextEventDispatcher) {
+ mTextEventDispatcher->OnDestroyWidget();
+ // Don't release it until this widget actually released because after this
+ // is called, TextEventDispatcher() may create it again.
+ }
+
+ // If this widget is being destroyed, let the APZ code know to drop references
+ // to this widget. Callers of this function all should be holding a deathgrip
+ // on this widget already.
+ ReleaseContentController();
+}
+
+void nsBaseWidget::MoveClient(const DesktopPoint& aOffset) {
+ LayoutDeviceIntPoint clientOffset(GetClientOffset());
+
+ // GetClientOffset returns device pixels; scale back to desktop pixels
+ // if that's what this widget uses for the Move/Resize APIs
+ if (BoundsUseDesktopPixels()) {
+ DesktopPoint desktopOffset = clientOffset / GetDesktopToDeviceScale();
+ Move(aOffset.x - desktopOffset.x, aOffset.y - desktopOffset.y);
+ } else {
+ LayoutDevicePoint layoutOffset = aOffset * GetDesktopToDeviceScale();
+ Move(layoutOffset.x - LayoutDeviceCoord(clientOffset.x),
+ layoutOffset.y - LayoutDeviceCoord(clientOffset.y));
+ }
+}
+
+void nsBaseWidget::ResizeClient(const DesktopSize& aSize, bool aRepaint) {
+ NS_ASSERTION((aSize.width >= 0), "Negative width passed to ResizeClient");
+ NS_ASSERTION((aSize.height >= 0), "Negative height passed to ResizeClient");
+
+ LayoutDeviceIntRect clientBounds = GetClientBounds();
+
+ // GetClientBounds and mBounds are device pixels; scale back to desktop pixels
+ // if that's what this widget uses for the Move/Resize APIs
+ if (BoundsUseDesktopPixels()) {
+ DesktopSize desktopDelta =
+ (LayoutDeviceIntSize(mBounds.Width(), mBounds.Height()) -
+ clientBounds.Size()) /
+ GetDesktopToDeviceScale();
+ Resize(aSize.width + desktopDelta.width, aSize.height + desktopDelta.height,
+ aRepaint);
+ } else {
+ LayoutDeviceSize layoutSize = aSize * GetDesktopToDeviceScale();
+ Resize(mBounds.Width() + (layoutSize.width - clientBounds.Width()),
+ mBounds.Height() + (layoutSize.height - clientBounds.Height()),
+ aRepaint);
+ }
+}
+
+void nsBaseWidget::ResizeClient(const DesktopRect& aRect, bool aRepaint) {
+ NS_ASSERTION((aRect.Width() >= 0), "Negative width passed to ResizeClient");
+ NS_ASSERTION((aRect.Height() >= 0), "Negative height passed to ResizeClient");
+
+ LayoutDeviceIntRect clientBounds = GetClientBounds();
+ LayoutDeviceIntPoint clientOffset = GetClientOffset();
+ DesktopToLayoutDeviceScale scale = GetDesktopToDeviceScale();
+
+ if (BoundsUseDesktopPixels()) {
+ DesktopPoint desktopOffset = clientOffset / scale;
+ DesktopSize desktopDelta =
+ (LayoutDeviceIntSize(mBounds.Width(), mBounds.Height()) -
+ clientBounds.Size()) /
+ scale;
+ Resize(aRect.X() - desktopOffset.x, aRect.Y() - desktopOffset.y,
+ aRect.Width() + desktopDelta.width,
+ aRect.Height() + desktopDelta.height, aRepaint);
+ } else {
+ LayoutDeviceRect layoutRect = aRect * scale;
+ Resize(layoutRect.X() - clientOffset.x, layoutRect.Y() - clientOffset.y,
+ layoutRect.Width() + mBounds.Width() - clientBounds.Width(),
+ layoutRect.Height() + mBounds.Height() - clientBounds.Height(),
+ aRepaint);
+ }
+}
+
+//-------------------------------------------------------------------------
+//
+// Bounds
+//
+//-------------------------------------------------------------------------
+
+/**
+ * If the implementation of nsWindow supports borders this method MUST be
+ * overridden
+ *
+ **/
+LayoutDeviceIntRect nsBaseWidget::GetClientBounds() { return GetBounds(); }
+
+/**
+ * If the implementation of nsWindow supports borders this method MUST be
+ * overridden
+ *
+ **/
+LayoutDeviceIntRect nsBaseWidget::GetBounds() { return mBounds; }
+
+/**
+ * If the implementation of nsWindow uses a local coordinate system within the
+ *window, this method must be overridden
+ *
+ **/
+LayoutDeviceIntRect nsBaseWidget::GetScreenBounds() { return GetBounds(); }
+
+nsresult nsBaseWidget::GetRestoredBounds(LayoutDeviceIntRect& aRect) {
+ if (SizeMode() != nsSizeMode_Normal) {
+ return NS_ERROR_FAILURE;
+ }
+ aRect = GetScreenBounds();
+ return NS_OK;
+}
+
+LayoutDeviceIntPoint nsBaseWidget::GetClientOffset() {
+ return LayoutDeviceIntPoint(0, 0);
+}
+
+nsresult nsBaseWidget::SetNonClientMargins(const LayoutDeviceIntMargin&) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+void nsBaseWidget::SetResizeMargin(LayoutDeviceIntCoord aResizeMargin) {}
+
+uint32_t nsBaseWidget::GetMaxTouchPoints() const { return 0; }
+
+bool nsBaseWidget::HasPendingInputEvent() { return false; }
+
+bool nsBaseWidget::ShowsResizeIndicator(LayoutDeviceIntRect* aResizerRect) {
+ return false;
+}
+
+/**
+ * Modifies aFile to point at an icon file with the given name and suffix. The
+ * suffix may correspond to a file extension with leading '.' if appropriate.
+ * Returns true if the icon file exists and can be read.
+ */
+static bool ResolveIconNameHelper(nsIFile* aFile, const nsAString& aIconName,
+ const nsAString& aIconSuffix) {
+ aFile->Append(u"icons"_ns);
+ aFile->Append(u"default"_ns);
+ aFile->Append(aIconName + aIconSuffix);
+
+ bool readable;
+ return NS_SUCCEEDED(aFile->IsReadable(&readable)) && readable;
+}
+
+/**
+ * Resolve the given icon name into a local file object. This method is
+ * intended to be called by subclasses of nsBaseWidget. aIconSuffix is a
+ * platform specific icon file suffix (e.g., ".ico" under Win32).
+ *
+ * If no file is found matching the given parameters, then null is returned.
+ */
+void nsBaseWidget::ResolveIconName(const nsAString& aIconName,
+ const nsAString& aIconSuffix,
+ nsIFile** aResult) {
+ *aResult = nullptr;
+
+ nsCOMPtr<nsIProperties> dirSvc =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
+ if (!dirSvc) return;
+
+ // first check auxilary chrome directories
+
+ nsCOMPtr<nsISimpleEnumerator> dirs;
+ dirSvc->Get(NS_APP_CHROME_DIR_LIST, NS_GET_IID(nsISimpleEnumerator),
+ getter_AddRefs(dirs));
+ if (dirs) {
+ bool hasMore;
+ while (NS_SUCCEEDED(dirs->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> element;
+ dirs->GetNext(getter_AddRefs(element));
+ if (!element) continue;
+ nsCOMPtr<nsIFile> file = do_QueryInterface(element);
+ if (!file) continue;
+ if (ResolveIconNameHelper(file, aIconName, aIconSuffix)) {
+ NS_ADDREF(*aResult = file);
+ return;
+ }
+ }
+ }
+
+ // then check the main app chrome directory
+
+ nsCOMPtr<nsIFile> file;
+ dirSvc->Get(NS_APP_CHROME_DIR, NS_GET_IID(nsIFile), getter_AddRefs(file));
+ if (file && ResolveIconNameHelper(file, aIconName, aIconSuffix))
+ NS_ADDREF(*aResult = file);
+}
+
+void nsBaseWidget::SetSizeConstraints(const SizeConstraints& aConstraints) {
+ mSizeConstraints = aConstraints;
+
+ // Popups are constrained during layout, and we don't want to synchronously
+ // paint from reflow, so bail out... This is not great, but it's no worse than
+ // what we used to do.
+ //
+ // The right fix here is probably making constraint changes go through the
+ // view manager and such.
+ if (mWindowType == WindowType::Popup) {
+ return;
+ }
+
+ // If the current size doesn't meet the new constraints, trigger a
+ // resize to apply it. Note that, we don't want to invoke Resize if
+ // the new constraints don't affect the current size, because Resize
+ // implementation on some platforms may touch other geometry even if
+ // the size don't need to change.
+ LayoutDeviceIntSize curSize = mBounds.Size();
+ LayoutDeviceIntSize clampedSize =
+ Max(aConstraints.mMinSize, Min(aConstraints.mMaxSize, curSize));
+ if (clampedSize != curSize) {
+ gfx::Size size;
+ if (BoundsUseDesktopPixels()) {
+ DesktopSize desktopSize = clampedSize / GetDesktopToDeviceScale();
+ size = desktopSize.ToUnknownSize();
+ } else {
+ size = gfx::Size(clampedSize.ToUnknownSize());
+ }
+ Resize(size.width, size.height, true);
+ }
+}
+
+const widget::SizeConstraints nsBaseWidget::GetSizeConstraints() {
+ return mSizeConstraints;
+}
+
+// static
+nsIRollupListener* nsBaseWidget::GetActiveRollupListener() {
+ // TODO: Simplify this.
+ return nsXULPopupManager::GetInstance();
+}
+
+void nsBaseWidget::NotifyWindowDestroyed() {
+ if (!mWidgetListener) return;
+
+ nsCOMPtr<nsIAppWindow> window = mWidgetListener->GetAppWindow();
+ nsCOMPtr<nsIBaseWindow> appWindow(do_QueryInterface(window));
+ if (appWindow) {
+ appWindow->Destroy();
+ }
+}
+
+void nsBaseWidget::NotifyWindowMoved(int32_t aX, int32_t aY,
+ ByMoveToRect aByMoveToRect) {
+ if (mWidgetListener) {
+ mWidgetListener->WindowMoved(this, aX, aY, aByMoveToRect);
+ }
+
+ if (mIMEHasFocus && IMENotificationRequestsRef().WantPositionChanged()) {
+ NotifyIME(IMENotification(IMEMessage::NOTIFY_IME_OF_POSITION_CHANGE));
+ }
+}
+
+void nsBaseWidget::NotifySizeMoveDone() {
+ if (!mWidgetListener) {
+ return;
+ }
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->WindowSizeMoveDone();
+ }
+}
+
+void nsBaseWidget::NotifyThemeChanged(ThemeChangeKind aKind) {
+ LookAndFeel::NotifyChangedAllWindows(aKind);
+}
+
+nsresult nsBaseWidget::NotifyIME(const IMENotification& aIMENotification) {
+ if (mIMEHasQuit) {
+ return NS_OK;
+ }
+ switch (aIMENotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ // We should send request to IME only when there is a TextEventDispatcher
+ // instance (this means that this widget has dispatched at least one
+ // composition event or keyboard event) and the it has composition.
+ // Otherwise, there is nothing to do.
+ // Note that if current input transaction is for native input events,
+ // TextEventDispatcher::NotifyIME() will call
+ // TextEventDispatcherListener::NotifyIME().
+ if (mTextEventDispatcher && mTextEventDispatcher->IsComposing()) {
+ return mTextEventDispatcher->NotifyIME(aIMENotification);
+ }
+ return NS_OK;
+ default: {
+ if (aIMENotification.mMessage == NOTIFY_IME_OF_FOCUS) {
+ mIMEHasFocus = true;
+ }
+ EnsureTextEventDispatcher();
+ // TextEventDispatcher::NotifyIME() will always call
+ // TextEventDispatcherListener::NotifyIME(). I.e., even if current
+ // input transaction is for synthesized events for automated tests,
+ // notifications will be sent to native IME.
+ nsresult rv = mTextEventDispatcher->NotifyIME(aIMENotification);
+ if (aIMENotification.mMessage == NOTIFY_IME_OF_BLUR) {
+ mIMEHasFocus = false;
+ }
+ return rv;
+ }
+ }
+}
+
+void nsBaseWidget::EnsureTextEventDispatcher() {
+ if (mTextEventDispatcher) {
+ return;
+ }
+ mTextEventDispatcher = new TextEventDispatcher(this);
+}
+
+nsIWidget::NativeIMEContext nsBaseWidget::GetNativeIMEContext() {
+ if (mTextEventDispatcher && mTextEventDispatcher->GetPseudoIMEContext()) {
+ // If we already have a TextEventDispatcher and it's working with
+ // a TextInputProcessor, we need to return pseudo IME context since
+ // TextCompositionArray::IndexOf(nsIWidget*) should return a composition
+ // on the pseudo IME context in such case.
+ NativeIMEContext pseudoIMEContext;
+ pseudoIMEContext.InitWithRawNativeIMEContext(
+ mTextEventDispatcher->GetPseudoIMEContext());
+ return pseudoIMEContext;
+ }
+ return NativeIMEContext(this);
+}
+
+nsIWidget::TextEventDispatcher* nsBaseWidget::GetTextEventDispatcher() {
+ EnsureTextEventDispatcher();
+ return mTextEventDispatcher;
+}
+
+void* nsBaseWidget::GetPseudoIMEContext() {
+ TextEventDispatcher* dispatcher = GetTextEventDispatcher();
+ if (!dispatcher) {
+ return nullptr;
+ }
+ return dispatcher->GetPseudoIMEContext();
+}
+
+TextEventDispatcherListener*
+nsBaseWidget::GetNativeTextEventDispatcherListener() {
+ // TODO: If all platforms supported use of TextEventDispatcher for handling
+ // native IME and keyboard events, this method should be removed since
+ // in such case, this is overridden by all the subclasses.
+ return nullptr;
+}
+
+void nsBaseWidget::ZoomToRect(const uint32_t& aPresShellId,
+ const ScrollableLayerGuid::ViewID& aViewId,
+ const CSSRect& aRect, const uint32_t& aFlags) {
+ if (!mCompositorSession || !mAPZC) {
+ return;
+ }
+ LayersId layerId = mCompositorSession->RootLayerTreeId();
+ mAPZC->ZoomToRect(ScrollableLayerGuid(layerId, aPresShellId, aViewId),
+ ZoomTarget{aRect}, aFlags);
+}
+
+#ifdef ACCESSIBILITY
+
+a11y::LocalAccessible* nsBaseWidget::GetRootAccessible() {
+ NS_ENSURE_TRUE(mWidgetListener, nullptr);
+
+ PresShell* presShell = mWidgetListener->GetPresShell();
+ NS_ENSURE_TRUE(presShell, nullptr);
+
+ // If container is null then the presshell is not active. This often happens
+ // when a preshell is being held onto for fastback.
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE(presContext->GetContainerWeak(), nullptr);
+
+ // LocalAccessible creation might be not safe so use IsSafeToRunScript to
+ // make sure it's not created at unsafe times.
+ nsAccessibilityService* accService = GetOrCreateAccService();
+ if (accService) {
+ return accService->GetRootDocumentAccessible(
+ presShell, nsContentUtils::IsSafeToRunScript());
+ }
+
+ return nullptr;
+}
+
+#endif // ACCESSIBILITY
+
+void nsBaseWidget::StartAsyncScrollbarDrag(
+ const AsyncDragMetrics& aDragMetrics) {
+ if (!AsyncPanZoomEnabled()) {
+ return;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess() && mCompositorSession);
+
+ LayersId layersId = mCompositorSession->RootLayerTreeId();
+ ScrollableLayerGuid guid(layersId, aDragMetrics.mPresShellId,
+ aDragMetrics.mViewId);
+
+ mAPZC->StartScrollbarDrag(guid, aDragMetrics);
+}
+
+bool nsBaseWidget::StartAsyncAutoscroll(const ScreenPoint& aAnchorLocation,
+ const ScrollableLayerGuid& aGuid) {
+ MOZ_ASSERT(XRE_IsParentProcess() && AsyncPanZoomEnabled());
+
+ return mAPZC->StartAutoscroll(aGuid, aAnchorLocation);
+}
+
+void nsBaseWidget::StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) {
+ MOZ_ASSERT(XRE_IsParentProcess() && AsyncPanZoomEnabled());
+
+ mAPZC->StopAutoscroll(aGuid);
+}
+
+LayersId nsBaseWidget::GetRootLayerTreeId() {
+ return mCompositorSession ? mCompositorSession->RootLayerTreeId()
+ : LayersId{0};
+}
+
+already_AddRefed<widget::Screen> nsBaseWidget::GetWidgetScreen() {
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+ LayoutDeviceIntRect bounds = GetScreenBounds();
+ DesktopIntRect deskBounds = RoundedToInt(bounds / GetDesktopToDeviceScale());
+ return screenManager.ScreenForRect(deskBounds);
+}
+
+mozilla::DesktopToLayoutDeviceScale
+nsBaseWidget::GetDesktopToDeviceScaleByScreen() {
+ return (nsView::GetViewFor(this)->GetViewManager()->GetDeviceContext())
+ ->GetDesktopToDeviceScale();
+}
+
+nsresult nsIWidget::SynthesizeNativeTouchTap(LayoutDeviceIntPoint aPoint,
+ bool aLongTap,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchtap");
+
+ if (sPointerIdCounter > TOUCH_INJECT_MAX_POINTS) {
+ sPointerIdCounter = 0;
+ }
+ int pointerId = sPointerIdCounter;
+ sPointerIdCounter++;
+ nsresult rv = SynthesizeNativeTouchPoint(pointerId, TOUCH_CONTACT, aPoint,
+ 1.0, 90, nullptr);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!aLongTap) {
+ return SynthesizeNativeTouchPoint(pointerId, TOUCH_REMOVE, aPoint, 0, 0,
+ nullptr);
+ }
+
+ // initiate a long tap
+ int elapse = Preferences::GetInt("ui.click_hold_context_menus.delay",
+ TOUCH_INJECT_LONG_TAP_DEFAULT_MSEC);
+ if (!mLongTapTimer) {
+ mLongTapTimer = NS_NewTimer();
+ if (!mLongTapTimer) {
+ SynthesizeNativeTouchPoint(pointerId, TOUCH_CANCEL, aPoint, 0, 0,
+ nullptr);
+ return NS_ERROR_UNEXPECTED;
+ }
+ // Windows requires recuring events, so we set this to a smaller window
+ // than the pref value.
+ int timeout = elapse;
+ if (timeout > TOUCH_INJECT_PUMP_TIMER_MSEC) {
+ timeout = TOUCH_INJECT_PUMP_TIMER_MSEC;
+ }
+ mLongTapTimer->InitWithNamedFuncCallback(
+ OnLongTapTimerCallback, this, timeout, nsITimer::TYPE_REPEATING_SLACK,
+ "nsIWidget::SynthesizeNativeTouchTap");
+ }
+
+ // If we already have a long tap pending, cancel it. We only allow one long
+ // tap to be active at a time.
+ if (mLongTapTouchPoint) {
+ SynthesizeNativeTouchPoint(mLongTapTouchPoint->mPointerId, TOUCH_CANCEL,
+ mLongTapTouchPoint->mPosition, 0, 0, nullptr);
+ }
+
+ mLongTapTouchPoint = MakeUnique<LongTapInfo>(
+ pointerId, aPoint, TimeDuration::FromMilliseconds(elapse), aObserver);
+ notifier.SkipNotification(); // we'll do it in the long-tap callback
+ return NS_OK;
+}
+
+// static
+void nsIWidget::OnLongTapTimerCallback(nsITimer* aTimer, void* aClosure) {
+ auto* self = static_cast<nsIWidget*>(aClosure);
+
+ if ((self->mLongTapTouchPoint->mStamp + self->mLongTapTouchPoint->mDuration) >
+ TimeStamp::Now()) {
+#ifdef XP_WIN
+ // Windows needs us to keep pumping feedback to the digitizer, so update
+ // the pointer id with the same position.
+ self->SynthesizeNativeTouchPoint(
+ self->mLongTapTouchPoint->mPointerId, TOUCH_CONTACT,
+ self->mLongTapTouchPoint->mPosition, 1.0, 90, nullptr);
+#endif
+ return;
+ }
+
+ AutoObserverNotifier notifier(self->mLongTapTouchPoint->mObserver,
+ "touchtap");
+
+ // finished, remove the touch point
+ self->mLongTapTimer->Cancel();
+ self->mLongTapTimer = nullptr;
+ self->SynthesizeNativeTouchPoint(
+ self->mLongTapTouchPoint->mPointerId, TOUCH_REMOVE,
+ self->mLongTapTouchPoint->mPosition, 0, 0, nullptr);
+ self->mLongTapTouchPoint = nullptr;
+}
+
+float nsIWidget::GetFallbackDPI() {
+ RefPtr<const Screen> primaryScreen =
+ ScreenManager::GetSingleton().GetPrimaryScreen();
+ return primaryScreen->GetDPI();
+}
+
+CSSToLayoutDeviceScale nsIWidget::GetFallbackDefaultScale() {
+ RefPtr<const Screen> s = ScreenManager::GetSingleton().GetPrimaryScreen();
+ return s->GetCSSToLayoutDeviceScale(Screen::IncludeOSZoom::No);
+}
+
+nsresult nsIWidget::ClearNativeTouchSequence(nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "cleartouch");
+
+ // XXX This is odd. This is called by the constructor of nsIWidget. However,
+ // at that point, nsIWidget::mLongTapTimer must be nullptr. Therefore,
+ // this must do nothing at initializing the instance.
+ if (!mLongTapTimer) {
+ return NS_OK;
+ }
+ mLongTapTimer->Cancel();
+ mLongTapTimer = nullptr;
+ SynthesizeNativeTouchPoint(mLongTapTouchPoint->mPointerId, TOUCH_CANCEL,
+ mLongTapTouchPoint->mPosition, 0, 0, nullptr);
+ mLongTapTouchPoint = nullptr;
+ return NS_OK;
+}
+
+MultiTouchInput nsBaseWidget::UpdateSynthesizedTouchState(
+ MultiTouchInput* aState, mozilla::TimeStamp aTimeStamp, uint32_t aPointerId,
+ TouchPointerState aPointerState, LayoutDeviceIntPoint aPoint,
+ double aPointerPressure, uint32_t aPointerOrientation) {
+ ScreenIntPoint pointerScreenPoint = ViewAs<ScreenPixel>(
+ aPoint, PixelCastJustification::LayoutDeviceIsScreenForBounds);
+
+ // We can't dispatch *aState directly because (a) dispatching
+ // it might inadvertently modify it and (b) in the case of touchend or
+ // touchcancel events aState will hold the touches that are
+ // still down whereas the input dispatched needs to hold the removed
+ // touch(es). We use |inputToDispatch| for this purpose.
+ MultiTouchInput inputToDispatch;
+ inputToDispatch.mInputType = MULTITOUCH_INPUT;
+ inputToDispatch.mTimeStamp = aTimeStamp;
+
+ int32_t index = aState->IndexOfTouch((int32_t)aPointerId);
+ if (aPointerState == TOUCH_CONTACT) {
+ if (index >= 0) {
+ // found an existing touch point, update it
+ SingleTouchData& point = aState->mTouches[index];
+ point.mScreenPoint = pointerScreenPoint;
+ point.mRotationAngle = (float)aPointerOrientation;
+ point.mForce = (float)aPointerPressure;
+ inputToDispatch.mType = MultiTouchInput::MULTITOUCH_MOVE;
+ } else {
+ // new touch point, add it
+ aState->mTouches.AppendElement(SingleTouchData(
+ (int32_t)aPointerId, pointerScreenPoint, ScreenSize(0, 0),
+ (float)aPointerOrientation, (float)aPointerPressure));
+ inputToDispatch.mType = MultiTouchInput::MULTITOUCH_START;
+ }
+ inputToDispatch.mTouches = aState->mTouches;
+ } else {
+ MOZ_ASSERT(aPointerState == TOUCH_REMOVE || aPointerState == TOUCH_CANCEL);
+ // a touch point is being lifted, so remove it from the stored list
+ if (index >= 0) {
+ aState->mTouches.RemoveElementAt(index);
+ }
+ inputToDispatch.mType =
+ (aPointerState == TOUCH_REMOVE ? MultiTouchInput::MULTITOUCH_END
+ : MultiTouchInput::MULTITOUCH_CANCEL);
+ inputToDispatch.mTouches.AppendElement(SingleTouchData(
+ (int32_t)aPointerId, pointerScreenPoint, ScreenSize(0, 0),
+ (float)aPointerOrientation, (float)aPointerPressure));
+ }
+
+ return inputToDispatch;
+}
+
+void nsBaseWidget::NotifyLiveResizeStarted() {
+ // If we have mLiveResizeListeners already non-empty, we should notify those
+ // listeners that the resize stopped before starting anew. In theory this
+ // should never happen because we shouldn't get nested live resize actions.
+ NotifyLiveResizeStopped();
+ MOZ_ASSERT(mLiveResizeListeners.IsEmpty());
+
+ // If we can get the active remote tab for the current widget, suppress
+ // the displayport on it during the live resize.
+ if (!mWidgetListener) {
+ return;
+ }
+ nsCOMPtr<nsIAppWindow> appWindow = mWidgetListener->GetAppWindow();
+ if (!appWindow) {
+ return;
+ }
+ mLiveResizeListeners = appWindow->GetLiveResizeListeners();
+ for (uint32_t i = 0; i < mLiveResizeListeners.Length(); i++) {
+ mLiveResizeListeners[i]->LiveResizeStarted();
+ }
+}
+
+void nsBaseWidget::NotifyLiveResizeStopped() {
+ if (!mLiveResizeListeners.IsEmpty()) {
+ for (uint32_t i = 0; i < mLiveResizeListeners.Length(); i++) {
+ mLiveResizeListeners[i]->LiveResizeStopped();
+ }
+ mLiveResizeListeners.Clear();
+ }
+}
+
+nsresult nsBaseWidget::AsyncEnableDragDrop(bool aEnable) {
+ RefPtr<nsBaseWidget> kungFuDeathGrip = this;
+ return NS_DispatchToCurrentThreadQueue(
+ NS_NewRunnableFunction(
+ "AsyncEnableDragDropFn",
+ [this, aEnable, kungFuDeathGrip]() { EnableDragDrop(aEnable); }),
+ kAsyncDragDropTimeout, EventQueuePriority::Idle);
+}
+
+void nsBaseWidget::SwipeFinished() {
+ if (mSwipeTracker) {
+ mSwipeTracker->Destroy();
+ mSwipeTracker = nullptr;
+ }
+}
+
+void nsBaseWidget::ReportSwipeStarted(uint64_t aInputBlockId,
+ bool aStartSwipe) {
+ if (mSwipeEventQueue && mSwipeEventQueue->inputBlockId == aInputBlockId) {
+ if (aStartSwipe) {
+ PanGestureInput& startEvent = mSwipeEventQueue->queuedEvents[0];
+ TrackScrollEventAsSwipe(startEvent, mSwipeEventQueue->allowedDirections,
+ aInputBlockId);
+ for (size_t i = 1; i < mSwipeEventQueue->queuedEvents.Length(); i++) {
+ mSwipeTracker->ProcessEvent(mSwipeEventQueue->queuedEvents[i]);
+ }
+ } else if (mAPZC) {
+ // If the event wasn't start swipe, we need to notify it to APZ.
+ mAPZC->SetBrowserGestureResponse(aInputBlockId,
+ BrowserGestureResponse::NotConsumed);
+ }
+ mSwipeEventQueue = nullptr;
+ }
+}
+
+void nsBaseWidget::TrackScrollEventAsSwipe(
+ const mozilla::PanGestureInput& aSwipeStartEvent,
+ uint32_t aAllowedDirections, uint64_t aInputBlockId) {
+ // If a swipe is currently being tracked kill it -- it's been interrupted
+ // by another gesture event.
+ if (mSwipeTracker) {
+ mSwipeTracker->CancelSwipe(aSwipeStartEvent.mTimeStamp);
+ mSwipeTracker->Destroy();
+ mSwipeTracker = nullptr;
+ }
+
+ uint32_t direction =
+ (aSwipeStartEvent.mPanDisplacement.x > 0.0)
+ ? (uint32_t)dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
+ : (uint32_t)dom::SimpleGestureEvent_Binding::DIRECTION_LEFT;
+
+ mSwipeTracker =
+ new SwipeTracker(*this, aSwipeStartEvent, aAllowedDirections, direction);
+
+ if (!mAPZC) {
+ mCurrentPanGestureBelongsToSwipe = true;
+ } else {
+ // Now SwipeTracker has started consuming pan events, notify it to APZ so
+ // that APZ can discard queued events.
+ mAPZC->SetBrowserGestureResponse(aInputBlockId,
+ BrowserGestureResponse::Consumed);
+ }
+}
+
+nsBaseWidget::SwipeInfo nsBaseWidget::SendMayStartSwipe(
+ const mozilla::PanGestureInput& aSwipeStartEvent) {
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ uint32_t direction =
+ (aSwipeStartEvent.mPanDisplacement.x > 0.0)
+ ? (uint32_t)dom::SimpleGestureEvent_Binding::DIRECTION_RIGHT
+ : (uint32_t)dom::SimpleGestureEvent_Binding::DIRECTION_LEFT;
+
+ // We're ready to start the animation. Tell Gecko about it, and at the same
+ // time ask it if it really wants to start an animation for this event.
+ // This event also reports back the directions that we can swipe in.
+ LayoutDeviceIntPoint position = RoundedToInt(aSwipeStartEvent.mPanStartPoint *
+ ScreenToLayoutDeviceScale(1));
+ WidgetSimpleGestureEvent geckoEvent = SwipeTracker::CreateSwipeGestureEvent(
+ eSwipeGestureMayStart, this, position, aSwipeStartEvent.mTimeStamp);
+ geckoEvent.mDirection = direction;
+ geckoEvent.mDelta = 0.0;
+ geckoEvent.mAllowedDirections = 0;
+ bool shouldStartSwipe =
+ DispatchWindowEvent(geckoEvent); // event cancelled == swipe should start
+
+ SwipeInfo result = {shouldStartSwipe, geckoEvent.mAllowedDirections};
+ return result;
+}
+
+WidgetWheelEvent nsBaseWidget::MayStartSwipeForAPZ(
+ const PanGestureInput& aPanInput, const APZEventResult& aApzResult) {
+ WidgetWheelEvent event = aPanInput.ToWidgetEvent(this);
+
+ // Ignore swipe-to-navigation in PiP window.
+ if (mIsPIPWindow) {
+ return event;
+ }
+
+ if (aPanInput.AllowsSwipe()) {
+ SwipeInfo swipeInfo = SendMayStartSwipe(aPanInput);
+ event.mCanTriggerSwipe = swipeInfo.wantsSwipe;
+ if (swipeInfo.wantsSwipe) {
+ if (aApzResult.GetStatus() == nsEventStatus_eIgnore) {
+ // APZ has determined and that scrolling horizontally in the
+ // requested direction is impossible, so it didn't do any
+ // scrolling for the event.
+ // We know now that MayStartSwipe wants a swipe, so we can start
+ // the swipe now.
+ TrackScrollEventAsSwipe(aPanInput, swipeInfo.allowedDirections,
+ aApzResult.mInputBlockId);
+ } else if (!aApzResult.GetHandledResult() ||
+ !aApzResult.GetHandledResult()->IsHandledByRoot()) {
+ // We don't know whether this event can start a swipe, so we need
+ // to queue up events and wait for a call to ReportSwipeStarted.
+ // APZ might already have started scrolling in response to the
+ // event if it knew that it's the right thing to do. In that case
+ // we'll still get a call to ReportSwipeStarted, and we will
+ // discard the queued events at that point.
+ mSwipeEventQueue = MakeUnique<SwipeEventQueue>(
+ swipeInfo.allowedDirections, aApzResult.mInputBlockId);
+ }
+ } else {
+ // Inform that the browser gesture didn't use the pan event (pan-start
+ // precisely), so that APZ can now start using the event for
+ // scrolling/overscrolling.
+ mAPZC->SetBrowserGestureResponse(aApzResult.mInputBlockId,
+ BrowserGestureResponse::NotConsumed);
+ }
+ }
+
+ if (mSwipeEventQueue &&
+ mSwipeEventQueue->inputBlockId == aApzResult.mInputBlockId) {
+ mSwipeEventQueue->queuedEvents.AppendElement(aPanInput);
+ }
+
+ return event;
+}
+
+bool nsBaseWidget::MayStartSwipeForNonAPZ(const PanGestureInput& aPanInput) {
+ // Ignore swipe-to-navigation in PiP window.
+ if (mIsPIPWindow) {
+ return false;
+ }
+
+ if (aPanInput.mType == PanGestureInput::PANGESTURE_MAYSTART ||
+ aPanInput.mType == PanGestureInput::PANGESTURE_START) {
+ mCurrentPanGestureBelongsToSwipe = false;
+ }
+ if (mCurrentPanGestureBelongsToSwipe) {
+ // Ignore this event. It's a momentum event from a scroll gesture
+ // that was processed as a swipe, and the swipe animation has
+ // already finished (so mSwipeTracker is already null).
+ MOZ_ASSERT(aPanInput.IsMomentum(),
+ "If the fingers are still on the touchpad, we should still have "
+ "a SwipeTracker, "
+ "and it should have consumed this event.");
+ return true;
+ }
+
+ if (!aPanInput.MayTriggerSwipe()) {
+ return false;
+ }
+
+ SwipeInfo swipeInfo = SendMayStartSwipe(aPanInput);
+
+ // We're in the non-APZ case here, but we still want to know whether
+ // the event was routed to a child process, so we use InputAPZContext
+ // to get that piece of information.
+ ScrollableLayerGuid guid;
+ uint64_t blockId = 0;
+ InputAPZContext context(guid, blockId, nsEventStatus_eIgnore);
+
+ WidgetWheelEvent event = aPanInput.ToWidgetEvent(this);
+ event.mCanTriggerSwipe = swipeInfo.wantsSwipe;
+ nsEventStatus status;
+ DispatchEvent(&event, status);
+ if (swipeInfo.wantsSwipe) {
+ if (context.WasRoutedToChildProcess()) {
+ // We don't know whether this event can start a swipe, so we need
+ // to queue up events and wait for a call to ReportSwipeStarted.
+ mSwipeEventQueue =
+ MakeUnique<SwipeEventQueue>(swipeInfo.allowedDirections, blockId);
+ } else if (event.TriggersSwipe()) {
+ TrackScrollEventAsSwipe(aPanInput, swipeInfo.allowedDirections, blockId);
+ }
+ }
+
+ if (mSwipeEventQueue && mSwipeEventQueue->inputBlockId == 0) {
+ mSwipeEventQueue->queuedEvents.AppendElement(aPanInput);
+ }
+
+ return true;
+}
+
+const IMENotificationRequests& nsIWidget::IMENotificationRequestsRef() {
+ TextEventDispatcher* dispatcher = GetTextEventDispatcher();
+ return dispatcher->IMENotificationRequestsRef();
+}
+
+void nsIWidget::PostHandleKeyEvent(mozilla::WidgetKeyboardEvent* aEvent) {}
+
+bool nsIWidget::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ MOZ_ASSERT(aEvent.IsTrusted());
+ MOZ_ASSERT(aCommands.IsEmpty());
+ return true;
+}
+
+already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboard() {
+ if (XRE_IsContentProcess()) {
+ return CreateBidiKeyboardContentProcess();
+ }
+ return CreateBidiKeyboardInner();
+}
+
+#ifdef ANDROID
+already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() {
+ // no bidi keyboard implementation
+ return nullptr;
+}
+#endif
+
+namespace mozilla::widget {
+
+const char* ToChar(InputContext::Origin aOrigin) {
+ switch (aOrigin) {
+ case InputContext::ORIGIN_MAIN:
+ return "ORIGIN_MAIN";
+ case InputContext::ORIGIN_CONTENT:
+ return "ORIGIN_CONTENT";
+ default:
+ return "Unexpected value";
+ }
+}
+
+const char* ToChar(IMEMessage aIMEMessage) {
+ switch (aIMEMessage) {
+ case NOTIFY_IME_OF_NOTHING:
+ return "NOTIFY_IME_OF_NOTHING";
+ case NOTIFY_IME_OF_FOCUS:
+ return "NOTIFY_IME_OF_FOCUS";
+ case NOTIFY_IME_OF_BLUR:
+ return "NOTIFY_IME_OF_BLUR";
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ return "NOTIFY_IME_OF_SELECTION_CHANGE";
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ return "NOTIFY_IME_OF_TEXT_CHANGE";
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ return "NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED";
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ return "NOTIFY_IME_OF_POSITION_CHANGE";
+ case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ return "NOTIFY_IME_OF_MOUSE_BUTTON_EVENT";
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ return "REQUEST_TO_COMMIT_COMPOSITION";
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ return "REQUEST_TO_CANCEL_COMPOSITION";
+ default:
+ return "Unexpected value";
+ }
+}
+
+void NativeIMEContext::Init(nsIWidget* aWidget) {
+ if (!aWidget) {
+ mRawNativeIMEContext = reinterpret_cast<uintptr_t>(nullptr);
+ mOriginProcessID = static_cast<uint64_t>(-1);
+ return;
+ }
+ if (!XRE_IsContentProcess()) {
+ mRawNativeIMEContext = reinterpret_cast<uintptr_t>(
+ aWidget->GetNativeData(NS_RAW_NATIVE_IME_CONTEXT));
+ mOriginProcessID = 0;
+ return;
+ }
+ // If this is created in a child process, aWidget is an instance of
+ // PuppetWidget which doesn't support NS_RAW_NATIVE_IME_CONTEXT.
+ // Instead of that PuppetWidget::GetNativeIMEContext() returns cached
+ // native IME context of the parent process.
+ *this = aWidget->GetNativeIMEContext();
+}
+
+void NativeIMEContext::InitWithRawNativeIMEContext(void* aRawNativeIMEContext) {
+ if (NS_WARN_IF(!aRawNativeIMEContext)) {
+ mRawNativeIMEContext = reinterpret_cast<uintptr_t>(nullptr);
+ mOriginProcessID = static_cast<uint64_t>(-1);
+ return;
+ }
+ mRawNativeIMEContext = reinterpret_cast<uintptr_t>(aRawNativeIMEContext);
+ mOriginProcessID =
+ XRE_IsContentProcess() ? ContentChild::GetSingleton()->GetID() : 0;
+}
+
+void IMENotification::TextChangeDataBase::MergeWith(
+ const IMENotification::TextChangeDataBase& aOther) {
+ MOZ_ASSERT(aOther.IsValid(), "Merging data must store valid data");
+ MOZ_ASSERT(aOther.mStartOffset <= aOther.mRemovedEndOffset,
+ "end of removed text must be same or larger than start");
+ MOZ_ASSERT(aOther.mStartOffset <= aOther.mAddedEndOffset,
+ "end of added text must be same or larger than start");
+
+ if (!IsValid()) {
+ *this = aOther;
+ return;
+ }
+
+ // |mStartOffset| and |mRemovedEndOffset| represent all replaced or removed
+ // text ranges. I.e., mStartOffset should be the smallest offset of all
+ // modified text ranges in old text. |mRemovedEndOffset| should be the
+ // largest end offset in old text of all modified text ranges.
+ // |mAddedEndOffset| represents the end offset of all inserted text ranges.
+ // I.e., only this is an offset in new text.
+ // In other words, between mStartOffset and |mRemovedEndOffset| of the
+ // premodified text was already removed. And some text whose length is
+ // |mAddedEndOffset - mStartOffset| is inserted to |mStartOffset|. I.e.,
+ // this allows IME to mark dirty the modified text range with |mStartOffset|
+ // and |mRemovedEndOffset| if IME stores all text of the focused editor and
+ // to compute new text length with |mAddedEndOffset| and |mRemovedEndOffset|.
+ // Additionally, IME can retrieve only the text between |mStartOffset| and
+ // |mAddedEndOffset| for updating stored text.
+
+ // For comparing new and old |mStartOffset|/|mRemovedEndOffset| values, they
+ // should be adjusted to be in same text. The |newData.mStartOffset| and
+ // |newData.mRemovedEndOffset| should be computed as in old text because
+ // |mStartOffset| and |mRemovedEndOffset| represent the modified text range
+ // in the old text but even if some text before the values of the newData
+ // has already been modified, the values don't include the changes.
+
+ // For comparing new and old |mAddedEndOffset| values, they should be
+ // adjusted to be in same text. The |oldData.mAddedEndOffset| should be
+ // computed as in the new text because |mAddedEndOffset| indicates the end
+ // offset of inserted text in the new text but |oldData.mAddedEndOffset|
+ // doesn't include any changes of the text before |newData.mAddedEndOffset|.
+
+ const TextChangeDataBase& newData = aOther;
+ const TextChangeDataBase oldData = *this;
+
+ // mCausedOnlyByComposition should be true only when all changes are caused
+ // by composition.
+ mCausedOnlyByComposition =
+ newData.mCausedOnlyByComposition && oldData.mCausedOnlyByComposition;
+
+ // mIncludingChangesWithoutComposition should be true if at least one of
+ // merged changes occurred without composition.
+ mIncludingChangesWithoutComposition =
+ newData.mIncludingChangesWithoutComposition ||
+ oldData.mIncludingChangesWithoutComposition;
+
+ // mIncludingChangesDuringComposition should be true when at least one of
+ // the merged non-composition changes occurred during the latest composition.
+ if (!newData.mCausedOnlyByComposition &&
+ !newData.mIncludingChangesDuringComposition) {
+ MOZ_ASSERT(newData.mIncludingChangesWithoutComposition);
+ MOZ_ASSERT(mIncludingChangesWithoutComposition);
+ // If new change is neither caused by composition nor occurred during
+ // composition, set mIncludingChangesDuringComposition to false because
+ // IME doesn't want outdated text changes as text change during current
+ // composition.
+ mIncludingChangesDuringComposition = false;
+ } else {
+ // Otherwise, set mIncludingChangesDuringComposition to true if either
+ // oldData or newData includes changes during composition.
+ mIncludingChangesDuringComposition =
+ newData.mIncludingChangesDuringComposition ||
+ oldData.mIncludingChangesDuringComposition;
+ }
+
+ if (newData.mStartOffset >= oldData.mAddedEndOffset) {
+ // Case 1:
+ // If new start is after old end offset of added text, it means that text
+ // after the modified range is modified. Like:
+ // added range of old change: +----------+
+ // removed range of new change: +----------+
+ // So, the old start offset is always the smaller offset.
+ mStartOffset = oldData.mStartOffset;
+ // The new end offset of removed text is moved by the old change and we
+ // need to cancel the move of the old change for comparing the offsets in
+ // same text because it doesn't make sensce to compare offsets in different
+ // text.
+ uint32_t newRemovedEndOffsetInOldText =
+ newData.mRemovedEndOffset - oldData.Difference();
+ mRemovedEndOffset =
+ std::max(newRemovedEndOffsetInOldText, oldData.mRemovedEndOffset);
+ // The new end offset of added text is always the larger offset.
+ mAddedEndOffset = newData.mAddedEndOffset;
+ return;
+ }
+
+ if (newData.mStartOffset >= oldData.mStartOffset) {
+ // If new start is in the modified range, it means that new data changes
+ // a part or all of the range.
+ mStartOffset = oldData.mStartOffset;
+ if (newData.mRemovedEndOffset >= oldData.mAddedEndOffset) {
+ // Case 2:
+ // If new end of removed text is greater than old end of added text, it
+ // means that all or a part of modified range modified again and text
+ // after the modified range is also modified. Like:
+ // added range of old change: +----------+
+ // removed range of new change: +----------+
+ // So, the new removed end offset is moved by the old change and we need
+ // to cancel the move of the old change for comparing the offsets in the
+ // same text because it doesn't make sense to compare the offsets in
+ // different text.
+ uint32_t newRemovedEndOffsetInOldText =
+ newData.mRemovedEndOffset - oldData.Difference();
+ mRemovedEndOffset =
+ std::max(newRemovedEndOffsetInOldText, oldData.mRemovedEndOffset);
+ // The old end of added text is replaced by new change. So, it should be
+ // same as the new start. On the other hand, the new added end offset is
+ // always same or larger. Therefore, the merged end offset of added
+ // text should be the new end offset of added text.
+ mAddedEndOffset = newData.mAddedEndOffset;
+ return;
+ }
+
+ // Case 3:
+ // If new end of removed text is less than old end of added text, it means
+ // that only a part of the modified range is modified again. Like:
+ // added range of old change: +------------+
+ // removed range of new change: +-----+
+ // So, the new end offset of removed text should be same as the old end
+ // offset of removed text. Therefore, the merged end offset of removed
+ // text should be the old text change's |mRemovedEndOffset|.
+ mRemovedEndOffset = oldData.mRemovedEndOffset;
+ // The old end of added text is moved by new change. So, we need to cancel
+ // the move of the new change for comparing the offsets in same text.
+ uint32_t oldAddedEndOffsetInNewText =
+ oldData.mAddedEndOffset + newData.Difference();
+ mAddedEndOffset =
+ std::max(newData.mAddedEndOffset, oldAddedEndOffsetInNewText);
+ return;
+ }
+
+ if (newData.mRemovedEndOffset >= oldData.mStartOffset) {
+ // If new end of removed text is greater than old start (and new start is
+ // less than old start), it means that a part of modified range is modified
+ // again and some new text before the modified range is also modified.
+ MOZ_ASSERT(newData.mStartOffset < oldData.mStartOffset,
+ "new start offset should be less than old one here");
+ mStartOffset = newData.mStartOffset;
+ if (newData.mRemovedEndOffset >= oldData.mAddedEndOffset) {
+ // Case 4:
+ // If new end of removed text is greater than old end of added text, it
+ // means that all modified text and text after the modified range is
+ // modified. Like:
+ // added range of old change: +----------+
+ // removed range of new change: +------------------+
+ // So, the new end of removed text is moved by the old change. Therefore,
+ // we need to cancel the move of the old change for comparing the offsets
+ // in same text because it doesn't make sense to compare the offsets in
+ // different text.
+ uint32_t newRemovedEndOffsetInOldText =
+ newData.mRemovedEndOffset - oldData.Difference();
+ mRemovedEndOffset =
+ std::max(newRemovedEndOffsetInOldText, oldData.mRemovedEndOffset);
+ // The old end of added text is replaced by new change. So, the old end
+ // offset of added text is same as new text change's start offset. Then,
+ // new change's end offset of added text is always same or larger than
+ // it. Therefore, merged end offset of added text is always the new end
+ // offset of added text.
+ mAddedEndOffset = newData.mAddedEndOffset;
+ return;
+ }
+
+ // Case 5:
+ // If new end of removed text is less than old end of added text, it
+ // means that only a part of the modified range is modified again. Like:
+ // added range of old change: +----------+
+ // removed range of new change: +----------+
+ // So, the new end of removed text should be same as old end of removed
+ // text for preventing end of removed text to be modified. Therefore,
+ // merged end offset of removed text is always the old end offset of removed
+ // text.
+ mRemovedEndOffset = oldData.mRemovedEndOffset;
+ // The old end of added text is moved by this change. So, we need to
+ // cancel the move of the new change for comparing the offsets in same text
+ // because it doesn't make sense to compare the offsets in different text.
+ uint32_t oldAddedEndOffsetInNewText =
+ oldData.mAddedEndOffset + newData.Difference();
+ mAddedEndOffset =
+ std::max(newData.mAddedEndOffset, oldAddedEndOffsetInNewText);
+ return;
+ }
+
+ // Case 6:
+ // Otherwise, i.e., both new end of added text and new start are less than
+ // old start, text before the modified range is modified. Like:
+ // added range of old change: +----------+
+ // removed range of new change: +----------+
+ MOZ_ASSERT(newData.mStartOffset < oldData.mStartOffset,
+ "new start offset should be less than old one here");
+ mStartOffset = newData.mStartOffset;
+ MOZ_ASSERT(newData.mRemovedEndOffset < oldData.mRemovedEndOffset,
+ "new removed end offset should be less than old one here");
+ mRemovedEndOffset = oldData.mRemovedEndOffset;
+ // The end of added text should be adjusted with the new difference.
+ uint32_t oldAddedEndOffsetInNewText =
+ oldData.mAddedEndOffset + newData.Difference();
+ mAddedEndOffset =
+ std::max(newData.mAddedEndOffset, oldAddedEndOffsetInNewText);
+}
+
+#ifdef DEBUG
+
+// Let's test the code of merging multiple text change data in debug build
+// and crash if one of them fails because this feature is very complex but
+// cannot be tested with mochitest.
+void IMENotification::TextChangeDataBase::Test() {
+ static bool gTestTextChangeEvent = true;
+ if (!gTestTextChangeEvent) {
+ return;
+ }
+ gTestTextChangeEvent = false;
+
+ /****************************************************************************
+ * Case 1
+ ****************************************************************************/
+
+ // Appending text
+ MergeWith(TextChangeData(10, 10, 20, false, false));
+ MergeWith(TextChangeData(20, 20, 35, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 1-1-1: mStartOffset should be the first offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 10, // 20 - (20 - 10)
+ "Test 1-1-2: mRemovedEndOffset should be the first end of removed text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 35,
+ "Test 1-1-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Removing text (longer line -> shorter line)
+ MergeWith(TextChangeData(10, 20, 10, false, false));
+ MergeWith(TextChangeData(10, 30, 10, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 1-2-1: mStartOffset should be the first offset");
+ MOZ_ASSERT(mRemovedEndOffset == 40, // 30 + (10 - 20)
+ "Test 1-2-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "with already removed length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 10,
+ "Test 1-2-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Removing text (shorter line -> longer line)
+ MergeWith(TextChangeData(10, 20, 10, false, false));
+ MergeWith(TextChangeData(10, 15, 10, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 1-3-1: mStartOffset should be the first offset");
+ MOZ_ASSERT(mRemovedEndOffset == 25, // 15 + (10 - 20)
+ "Test 1-3-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "with already removed length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 10,
+ "Test 1-3-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Appending text at different point (not sure if actually occurs)
+ MergeWith(TextChangeData(10, 10, 20, false, false));
+ MergeWith(TextChangeData(55, 55, 60, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 1-4-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 45, // 55 - (10 - 20)
+ "Test 1-4-2: mRemovedEndOffset should be the the largest end of removed "
+ "text without already added length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 60,
+ "Test 1-4-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Removing text at different point (not sure if actually occurs)
+ MergeWith(TextChangeData(10, 20, 10, false, false));
+ MergeWith(TextChangeData(55, 68, 55, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 1-5-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 78, // 68 - (10 - 20)
+ "Test 1-5-2: mRemovedEndOffset should be the the largest end of removed "
+ "text with already removed length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 55,
+ "Test 1-5-3: mAddedEndOffset should be the largest end of added text");
+ Clear();
+
+ // Replacing text and append text (becomes longer)
+ MergeWith(TextChangeData(30, 35, 32, false, false));
+ MergeWith(TextChangeData(32, 32, 40, false, false));
+ MOZ_ASSERT(mStartOffset == 30,
+ "Test 1-6-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 35, // 32 - (32 - 35)
+ "Test 1-6-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 40,
+ "Test 1-6-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Replacing text and append text (becomes shorter)
+ MergeWith(TextChangeData(30, 35, 32, false, false));
+ MergeWith(TextChangeData(32, 32, 33, false, false));
+ MOZ_ASSERT(mStartOffset == 30,
+ "Test 1-7-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 35, // 32 - (32 - 35)
+ "Test 1-7-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 33,
+ "Test 1-7-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Removing text and replacing text after first range (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(30, 35, 30, false, false));
+ MergeWith(TextChangeData(32, 34, 48, false, false));
+ MOZ_ASSERT(mStartOffset == 30,
+ "Test 1-8-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 39, // 34 - (30 - 35)
+ "Test 1-8-2: mRemovedEndOffset should be the the first end of "
+ "removed text "
+ "without already removed text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 48,
+ "Test 1-8-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Removing text and replacing text after first range (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(30, 35, 30, false, false));
+ MergeWith(TextChangeData(32, 38, 36, false, false));
+ MOZ_ASSERT(mStartOffset == 30,
+ "Test 1-9-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 43, // 38 - (30 - 35)
+ "Test 1-9-2: mRemovedEndOffset should be the the first end of "
+ "removed text "
+ "without already removed text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 36,
+ "Test 1-9-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ /****************************************************************************
+ * Case 2
+ ****************************************************************************/
+
+ // Replacing text in around end of added text (becomes shorter) (not sure
+ // if actually occurs)
+ MergeWith(TextChangeData(50, 50, 55, false, false));
+ MergeWith(TextChangeData(53, 60, 54, false, false));
+ MOZ_ASSERT(mStartOffset == 50,
+ "Test 2-1-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 55, // 60 - (55 - 50)
+ "Test 2-1-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already added text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 54,
+ "Test 2-1-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Replacing text around end of added text (becomes longer) (not sure
+ // if actually occurs)
+ MergeWith(TextChangeData(50, 50, 55, false, false));
+ MergeWith(TextChangeData(54, 62, 68, false, false));
+ MOZ_ASSERT(mStartOffset == 50,
+ "Test 2-2-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 57, // 62 - (55 - 50)
+ "Test 2-2-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already added text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 68,
+ "Test 2-2-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Replacing text around end of replaced text (became shorter) (not sure if
+ // actually occurs)
+ MergeWith(TextChangeData(36, 48, 45, false, false));
+ MergeWith(TextChangeData(43, 50, 49, false, false));
+ MOZ_ASSERT(mStartOffset == 36,
+ "Test 2-3-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 53, // 50 - (45 - 48)
+ "Test 2-3-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already removed text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 49,
+ "Test 2-3-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Replacing text around end of replaced text (became longer) (not sure if
+ // actually occurs)
+ MergeWith(TextChangeData(36, 52, 53, false, false));
+ MergeWith(TextChangeData(43, 68, 61, false, false));
+ MOZ_ASSERT(mStartOffset == 36,
+ "Test 2-4-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 67, // 68 - (53 - 52)
+ "Test 2-4-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already added text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 61,
+ "Test 2-4-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ /****************************************************************************
+ * Case 3
+ ****************************************************************************/
+
+ // Appending text in already added text (not sure if actually occurs)
+ MergeWith(TextChangeData(10, 10, 20, false, false));
+ MergeWith(TextChangeData(15, 15, 30, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 3-1-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 10,
+ "Test 3-1-2: mRemovedEndOffset should be the the first end of "
+ "removed text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 35, // 20 + (30 - 15)
+ "Test 3-1-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ // Replacing text in added text (not sure if actually occurs)
+ MergeWith(TextChangeData(50, 50, 55, false, false));
+ MergeWith(TextChangeData(52, 53, 56, false, false));
+ MOZ_ASSERT(mStartOffset == 50,
+ "Test 3-2-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 50,
+ "Test 3-2-2: mRemovedEndOffset should be the the first end of "
+ "removed text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 58, // 55 + (56 - 53)
+ "Test 3-2-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ // Replacing text in replaced text (became shorter) (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(36, 48, 45, false, false));
+ MergeWith(TextChangeData(37, 38, 50, false, false));
+ MOZ_ASSERT(mStartOffset == 36,
+ "Test 3-3-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 48,
+ "Test 3-3-2: mRemovedEndOffset should be the the first end of "
+ "removed text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 57, // 45 + (50 - 38)
+ "Test 3-3-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ // Replacing text in replaced text (became longer) (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(32, 48, 53, false, false));
+ MergeWith(TextChangeData(43, 50, 52, false, false));
+ MOZ_ASSERT(mStartOffset == 32,
+ "Test 3-4-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 48,
+ "Test 3-4-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already added text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 55, // 53 + (52 - 50)
+ "Test 3-4-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ // Replacing text in replaced text (became shorter) (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(36, 48, 50, false, false));
+ MergeWith(TextChangeData(37, 49, 47, false, false));
+ MOZ_ASSERT(mStartOffset == 36,
+ "Test 3-5-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 48,
+ "Test 3-5-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(mAddedEndOffset == 48, // 50 + (47 - 49)
+ "Test 3-5-3: mAddedEndOffset should be the first end of added "
+ "text without "
+ "removed text length by the new change");
+ Clear();
+
+ // Replacing text in replaced text (became longer) (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(32, 48, 53, false, false));
+ MergeWith(TextChangeData(43, 50, 47, false, false));
+ MOZ_ASSERT(mStartOffset == 32,
+ "Test 3-6-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 48,
+ "Test 3-6-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already added text length");
+ MOZ_ASSERT(mAddedEndOffset == 50, // 53 + (47 - 50)
+ "Test 3-6-3: mAddedEndOffset should be the first end of added "
+ "text without "
+ "removed text length by the new change");
+ Clear();
+
+ /****************************************************************************
+ * Case 4
+ ****************************************************************************/
+
+ // Replacing text all of already append text (not sure if actually occurs)
+ MergeWith(TextChangeData(50, 50, 55, false, false));
+ MergeWith(TextChangeData(44, 66, 68, false, false));
+ MOZ_ASSERT(mStartOffset == 44,
+ "Test 4-1-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 61, // 66 - (55 - 50)
+ "Test 4-1-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already added text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 68,
+ "Test 4-1-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Replacing text around a point in which text was removed (not sure if
+ // actually occurs)
+ MergeWith(TextChangeData(50, 62, 50, false, false));
+ MergeWith(TextChangeData(44, 66, 68, false, false));
+ MOZ_ASSERT(mStartOffset == 44,
+ "Test 4-2-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 78, // 66 - (50 - 62)
+ "Test 4-2-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already removed text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 68,
+ "Test 4-2-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Replacing text all replaced text (became shorter) (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(50, 62, 60, false, false));
+ MergeWith(TextChangeData(49, 128, 130, false, false));
+ MOZ_ASSERT(mStartOffset == 49,
+ "Test 4-3-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 130, // 128 - (60 - 62)
+ "Test 4-3-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "without already removed text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 130,
+ "Test 4-3-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ // Replacing text all replaced text (became longer) (not sure if actually
+ // occurs)
+ MergeWith(TextChangeData(50, 61, 73, false, false));
+ MergeWith(TextChangeData(44, 100, 50, false, false));
+ MOZ_ASSERT(mStartOffset == 44,
+ "Test 4-4-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(mRemovedEndOffset == 88, // 100 - (73 - 61)
+ "Test 4-4-2: mRemovedEndOffset should be the the last end of "
+ "removed text "
+ "with already added text length");
+ MOZ_ASSERT(
+ mAddedEndOffset == 50,
+ "Test 4-4-3: mAddedEndOffset should be the last end of added text");
+ Clear();
+
+ /****************************************************************************
+ * Case 5
+ ****************************************************************************/
+
+ // Replacing text around start of added text (not sure if actually occurs)
+ MergeWith(TextChangeData(50, 50, 55, false, false));
+ MergeWith(TextChangeData(48, 52, 49, false, false));
+ MOZ_ASSERT(mStartOffset == 48,
+ "Test 5-1-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 50,
+ "Test 5-1-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 52, // 55 + (52 - 49)
+ "Test 5-1-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ // Replacing text around start of replaced text (became shorter) (not sure if
+ // actually occurs)
+ MergeWith(TextChangeData(50, 60, 58, false, false));
+ MergeWith(TextChangeData(43, 50, 48, false, false));
+ MOZ_ASSERT(mStartOffset == 43,
+ "Test 5-2-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 60,
+ "Test 5-2-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(mAddedEndOffset == 56, // 58 + (48 - 50)
+ "Test 5-2-3: mAddedEndOffset should be the first end of added "
+ "text without "
+ "removed text length by the new change");
+ Clear();
+
+ // Replacing text around start of replaced text (became longer) (not sure if
+ // actually occurs)
+ MergeWith(TextChangeData(50, 60, 68, false, false));
+ MergeWith(TextChangeData(43, 55, 53, false, false));
+ MOZ_ASSERT(mStartOffset == 43,
+ "Test 5-3-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 60,
+ "Test 5-3-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(mAddedEndOffset == 66, // 68 + (53 - 55)
+ "Test 5-3-3: mAddedEndOffset should be the first end of added "
+ "text without "
+ "removed text length by the new change");
+ Clear();
+
+ // Replacing text around start of replaced text (became shorter) (not sure if
+ // actually occurs)
+ MergeWith(TextChangeData(50, 60, 58, false, false));
+ MergeWith(TextChangeData(43, 50, 128, false, false));
+ MOZ_ASSERT(mStartOffset == 43,
+ "Test 5-4-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 60,
+ "Test 5-4-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 136, // 58 + (128 - 50)
+ "Test 5-4-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ // Replacing text around start of replaced text (became longer) (not sure if
+ // actually occurs)
+ MergeWith(TextChangeData(50, 60, 68, false, false));
+ MergeWith(TextChangeData(43, 55, 65, false, false));
+ MOZ_ASSERT(mStartOffset == 43,
+ "Test 5-5-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 60,
+ "Test 5-5-2: mRemovedEndOffset should be the the first end of removed "
+ "text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 78, // 68 + (65 - 55)
+ "Test 5-5-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ /****************************************************************************
+ * Case 6
+ ****************************************************************************/
+
+ // Appending text before already added text (not sure if actually occurs)
+ MergeWith(TextChangeData(30, 30, 45, false, false));
+ MergeWith(TextChangeData(10, 10, 20, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 6-1-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 30,
+ "Test 6-1-2: mRemovedEndOffset should be the the largest end of removed "
+ "text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 55, // 45 + (20 - 10)
+ "Test 6-1-3: mAddedEndOffset should be the first end of added text with "
+ "added text length by the new change");
+ Clear();
+
+ // Removing text before already removed text (not sure if actually occurs)
+ MergeWith(TextChangeData(30, 35, 30, false, false));
+ MergeWith(TextChangeData(10, 25, 10, false, false));
+ MOZ_ASSERT(mStartOffset == 10,
+ "Test 6-2-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 35,
+ "Test 6-2-2: mRemovedEndOffset should be the the largest end of removed "
+ "text");
+ MOZ_ASSERT(
+ mAddedEndOffset == 15, // 30 - (25 - 10)
+ "Test 6-2-3: mAddedEndOffset should be the first end of added text with "
+ "removed text length by the new change");
+ Clear();
+
+ // Replacing text before already replaced text (not sure if actually occurs)
+ MergeWith(TextChangeData(50, 65, 70, false, false));
+ MergeWith(TextChangeData(13, 24, 15, false, false));
+ MOZ_ASSERT(mStartOffset == 13,
+ "Test 6-3-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 65,
+ "Test 6-3-2: mRemovedEndOffset should be the the largest end of removed "
+ "text");
+ MOZ_ASSERT(mAddedEndOffset == 61, // 70 + (15 - 24)
+ "Test 6-3-3: mAddedEndOffset should be the first end of added "
+ "text without "
+ "removed text length by the new change");
+ Clear();
+
+ // Replacing text before already replaced text (not sure if actually occurs)
+ MergeWith(TextChangeData(50, 65, 70, false, false));
+ MergeWith(TextChangeData(13, 24, 36, false, false));
+ MOZ_ASSERT(mStartOffset == 13,
+ "Test 6-4-1: mStartOffset should be the smallest offset");
+ MOZ_ASSERT(
+ mRemovedEndOffset == 65,
+ "Test 6-4-2: mRemovedEndOffset should be the the largest end of removed "
+ "text");
+ MOZ_ASSERT(mAddedEndOffset == 82, // 70 + (36 - 24)
+ "Test 6-4-3: mAddedEndOffset should be the first end of added "
+ "text without "
+ "removed text length by the new change");
+ Clear();
+}
+
+#endif // #ifdef DEBUG
+
+} // namespace mozilla::widget
+
+#ifdef DEBUG
+//////////////////////////////////////////////////////////////
+//
+// Convert a GUI event message code to a string.
+// Makes it a lot easier to debug events.
+//
+// See gtk/nsWidget.cpp and windows/nsWindow.cpp
+// for a DebugPrintEvent() function that uses
+// this.
+//
+//////////////////////////////////////////////////////////////
+/* static */
+nsAutoString nsBaseWidget::debug_GuiEventToString(WidgetGUIEvent* aGuiEvent) {
+ NS_ASSERTION(nullptr != aGuiEvent, "cmon, null gui event.");
+
+ nsAutoString eventName(u"UNKNOWN"_ns);
+
+# define _ASSIGN_eventName(_value, _name) \
+ case _value: \
+ eventName.AssignLiteral(_name); \
+ break
+
+ switch (aGuiEvent->mMessage) {
+ _ASSIGN_eventName(eBlur, "eBlur");
+ _ASSIGN_eventName(eDrop, "eDrop");
+ _ASSIGN_eventName(eDragEnter, "eDragEnter");
+ _ASSIGN_eventName(eDragExit, "eDragExit");
+ _ASSIGN_eventName(eDragOver, "eDragOver");
+ _ASSIGN_eventName(eEditorInput, "eEditorInput");
+ _ASSIGN_eventName(eFocus, "eFocus");
+ _ASSIGN_eventName(eFocusIn, "eFocusIn");
+ _ASSIGN_eventName(eFocusOut, "eFocusOut");
+ _ASSIGN_eventName(eFormSelect, "eFormSelect");
+ _ASSIGN_eventName(eFormChange, "eFormChange");
+ _ASSIGN_eventName(eFormReset, "eFormReset");
+ _ASSIGN_eventName(eFormSubmit, "eFormSubmit");
+ _ASSIGN_eventName(eImageAbort, "eImageAbort");
+ _ASSIGN_eventName(eLoadError, "eLoadError");
+ _ASSIGN_eventName(eKeyDown, "eKeyDown");
+ _ASSIGN_eventName(eKeyPress, "eKeyPress");
+ _ASSIGN_eventName(eKeyUp, "eKeyUp");
+ _ASSIGN_eventName(eMouseEnterIntoWidget, "eMouseEnterIntoWidget");
+ _ASSIGN_eventName(eMouseExitFromWidget, "eMouseExitFromWidget");
+ _ASSIGN_eventName(eMouseDown, "eMouseDown");
+ _ASSIGN_eventName(eMouseUp, "eMouseUp");
+ _ASSIGN_eventName(eMouseClick, "eMouseClick");
+ _ASSIGN_eventName(eMouseAuxClick, "eMouseAuxClick");
+ _ASSIGN_eventName(eMouseDoubleClick, "eMouseDoubleClick");
+ _ASSIGN_eventName(eMouseMove, "eMouseMove");
+ _ASSIGN_eventName(eLoad, "eLoad");
+ _ASSIGN_eventName(ePopState, "ePopState");
+ _ASSIGN_eventName(eBeforeScriptExecute, "eBeforeScriptExecute");
+ _ASSIGN_eventName(eAfterScriptExecute, "eAfterScriptExecute");
+ _ASSIGN_eventName(eUnload, "eUnload");
+ _ASSIGN_eventName(eHashChange, "eHashChange");
+ _ASSIGN_eventName(eReadyStateChange, "eReadyStateChange");
+ _ASSIGN_eventName(eXULBroadcast, "eXULBroadcast");
+ _ASSIGN_eventName(eXULCommandUpdate, "eXULCommandUpdate");
+
+# undef _ASSIGN_eventName
+
+ default: {
+ eventName.AssignLiteral("UNKNOWN: ");
+ eventName.AppendInt(aGuiEvent->mMessage);
+ } break;
+ }
+
+ return nsAutoString(eventName);
+}
+//////////////////////////////////////////////////////////////
+//
+// Code to deal with paint and event debug prefs.
+//
+//////////////////////////////////////////////////////////////
+struct PrefPair {
+ const char* name;
+ bool value;
+};
+
+static PrefPair debug_PrefValues[] = {
+ {"nglayout.debug.crossing_event_dumping", false},
+ {"nglayout.debug.event_dumping", false},
+ {"nglayout.debug.invalidate_dumping", false},
+ {"nglayout.debug.motion_event_dumping", false},
+ {"nglayout.debug.paint_dumping", false}};
+
+//////////////////////////////////////////////////////////////
+bool nsBaseWidget::debug_GetCachedBoolPref(const char* aPrefName) {
+ NS_ASSERTION(nullptr != aPrefName, "cmon, pref name is null.");
+
+ for (uint32_t i = 0; i < ArrayLength(debug_PrefValues); i++) {
+ if (strcmp(debug_PrefValues[i].name, aPrefName) == 0) {
+ return debug_PrefValues[i].value;
+ }
+ }
+
+ return false;
+}
+//////////////////////////////////////////////////////////////
+static void debug_SetCachedBoolPref(const char* aPrefName, bool aValue) {
+ NS_ASSERTION(nullptr != aPrefName, "cmon, pref name is null.");
+
+ for (uint32_t i = 0; i < ArrayLength(debug_PrefValues); i++) {
+ if (strcmp(debug_PrefValues[i].name, aPrefName) == 0) {
+ debug_PrefValues[i].value = aValue;
+
+ return;
+ }
+ }
+
+ NS_ASSERTION(false, "cmon, this code is not reached dude.");
+}
+
+//////////////////////////////////////////////////////////////
+class Debug_PrefObserver final : public nsIObserver {
+ ~Debug_PrefObserver() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+};
+
+NS_IMPL_ISUPPORTS(Debug_PrefObserver, nsIObserver)
+
+NS_IMETHODIMP
+Debug_PrefObserver::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ NS_ConvertUTF16toUTF8 prefName(data);
+
+ bool value = Preferences::GetBool(prefName.get(), false);
+ debug_SetCachedBoolPref(prefName.get(), value);
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////
+/* static */ void debug_RegisterPrefCallbacks() {
+ static bool once = true;
+
+ if (!once) {
+ return;
+ }
+
+ once = false;
+
+ nsCOMPtr<nsIObserver> obs(new Debug_PrefObserver());
+ for (uint32_t i = 0; i < ArrayLength(debug_PrefValues); i++) {
+ // Initialize the pref values
+ debug_PrefValues[i].value =
+ Preferences::GetBool(debug_PrefValues[i].name, false);
+
+ if (obs) {
+ // Register callbacks for when these change
+ nsCString name;
+ name.AssignLiteral(debug_PrefValues[i].name,
+ strlen(debug_PrefValues[i].name));
+ Preferences::AddStrongObserver(obs, name);
+ }
+ }
+}
+//////////////////////////////////////////////////////////////
+static int32_t _GetPrintCount() {
+ static int32_t sCount = 0;
+
+ return ++sCount;
+}
+//////////////////////////////////////////////////////////////
+/* static */
+void nsBaseWidget::debug_DumpEvent(FILE* aFileOut, nsIWidget* aWidget,
+ WidgetGUIEvent* aGuiEvent,
+ const char* aWidgetName, int32_t aWindowID) {
+ if (aGuiEvent->mMessage == eMouseMove) {
+ if (!debug_GetCachedBoolPref("nglayout.debug.motion_event_dumping")) return;
+ }
+
+ if (aGuiEvent->mMessage == eMouseEnterIntoWidget ||
+ aGuiEvent->mMessage == eMouseExitFromWidget) {
+ if (!debug_GetCachedBoolPref("nglayout.debug.crossing_event_dumping"))
+ return;
+ }
+
+ if (!debug_GetCachedBoolPref("nglayout.debug.event_dumping")) return;
+
+ NS_LossyConvertUTF16toASCII tempString(
+ debug_GuiEventToString(aGuiEvent).get());
+
+ fprintf(aFileOut, "%4d %-26s widget=%-8p name=%-12s id=0x%-6x refpt=%d,%d\n",
+ _GetPrintCount(), tempString.get(), (void*)aWidget, aWidgetName,
+ aWindowID, aGuiEvent->mRefPoint.x.value,
+ aGuiEvent->mRefPoint.y.value);
+}
+//////////////////////////////////////////////////////////////
+/* static */
+void nsBaseWidget::debug_DumpPaintEvent(FILE* aFileOut, nsIWidget* aWidget,
+ const nsIntRegion& aRegion,
+ const char* aWidgetName,
+ int32_t aWindowID) {
+ NS_ASSERTION(nullptr != aFileOut, "cmon, null output FILE");
+ NS_ASSERTION(nullptr != aWidget, "cmon, the widget is null");
+
+ if (!debug_GetCachedBoolPref("nglayout.debug.paint_dumping")) return;
+
+ nsIntRect rect = aRegion.GetBounds();
+ fprintf(aFileOut,
+ "%4d PAINT widget=%p name=%-12s id=0x%-6x bounds-rect=%3d,%-3d "
+ "%3d,%-3d",
+ _GetPrintCount(), (void*)aWidget, aWidgetName, aWindowID, rect.X(),
+ rect.Y(), rect.Width(), rect.Height());
+
+ fprintf(aFileOut, "\n");
+}
+//////////////////////////////////////////////////////////////
+/* static */
+void nsBaseWidget::debug_DumpInvalidate(FILE* aFileOut, nsIWidget* aWidget,
+ const LayoutDeviceIntRect* aRect,
+ const char* aWidgetName,
+ int32_t aWindowID) {
+ if (!debug_GetCachedBoolPref("nglayout.debug.invalidate_dumping")) return;
+
+ NS_ASSERTION(nullptr != aFileOut, "cmon, null output FILE");
+ NS_ASSERTION(nullptr != aWidget, "cmon, the widget is null");
+
+ fprintf(aFileOut, "%4d Invalidate widget=%p name=%-12s id=0x%-6x",
+ _GetPrintCount(), (void*)aWidget, aWidgetName, aWindowID);
+
+ if (aRect) {
+ fprintf(aFileOut, " rect=%3d,%-3d %3d,%-3d", aRect->X(), aRect->Y(),
+ aRect->Width(), aRect->Height());
+ } else {
+ fprintf(aFileOut, " rect=%-15s", "none");
+ }
+
+ fprintf(aFileOut, "\n");
+}
+//////////////////////////////////////////////////////////////
+
+#endif // DEBUG
diff --git a/widget/nsBaseWidget.h b/widget/nsBaseWidget.h
new file mode 100644
index 0000000000..8b6e8ad0eb
--- /dev/null
+++ b/widget/nsBaseWidget.h
@@ -0,0 +1,777 @@
+/* -*- 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/. */
+#ifndef nsBaseWidget_h__
+#define nsBaseWidget_h__
+
+#include "InputData.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WidgetUtils.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/CompositorOptions.h"
+#include "mozilla/layers/NativeLayer.h"
+#include "mozilla/widget/ThemeChangeKind.h"
+#include "mozilla/widget/WindowOcclusionState.h"
+#include "nsRect.h"
+#include "nsIWidget.h"
+#include "nsWidgetsCID.h"
+#include "nsIFile.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIRollupListener.h"
+#include "nsIObserver.h"
+#include "nsIWidgetListener.h"
+#include "nsPIDOMWindow.h"
+#include "nsWeakReference.h"
+
+#include <algorithm>
+
+class nsIContent;
+class gfxContext;
+
+namespace mozilla {
+class CompositorVsyncDispatcher;
+class LiveResizeListener;
+class FallbackRenderer;
+class SwipeTracker;
+struct SwipeEventQueue;
+class WidgetWheelEvent;
+
+#ifdef ACCESSIBILITY
+namespace a11y {
+class LocalAccessible;
+}
+#endif
+
+namespace gfx {
+class DrawTarget;
+class SourceSurface;
+} // namespace gfx
+
+namespace layers {
+class CompositorBridgeChild;
+class CompositorBridgeParent;
+class IAPZCTreeManager;
+class GeckoContentController;
+class APZEventState;
+struct APZEventResult;
+class CompositorSession;
+class ImageContainer;
+class WebRenderLayerManager;
+struct ScrollableLayerGuid;
+class RemoteCompositorSession;
+} // namespace layers
+
+namespace widget {
+class CompositorWidgetDelegate;
+class InProcessCompositorWidget;
+class WidgetRenderingContext;
+} // namespace widget
+
+class CompositorVsyncDispatcher;
+} // namespace mozilla
+
+namespace base {
+class Thread;
+} // namespace base
+
+// Windows specific constant indicating the maximum number of touch points the
+// inject api will allow. This also sets the maximum numerical value for touch
+// ids we can use when injecting touch points on Windows.
+#define TOUCH_INJECT_MAX_POINTS 256
+
+class nsBaseWidget;
+
+// Helper class used in shutting down gfx related code.
+class WidgetShutdownObserver final : public nsIObserver {
+ ~WidgetShutdownObserver();
+
+ public:
+ explicit WidgetShutdownObserver(nsBaseWidget* aWidget);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ void Register();
+ void Unregister();
+
+ nsBaseWidget* mWidget;
+ bool mRegistered;
+};
+
+// Helper class used for observing locales change.
+class LocalesChangedObserver final : public nsIObserver {
+ ~LocalesChangedObserver();
+
+ public:
+ explicit LocalesChangedObserver(nsBaseWidget* aWidget);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ void Register();
+ void Unregister();
+
+ nsBaseWidget* mWidget;
+ bool mRegistered;
+};
+
+/**
+ * Common widget implementation used as base class for native
+ * or crossplatform implementations of Widgets.
+ * All cross-platform behavior that all widgets need to implement
+ * should be placed in this class.
+ * (Note: widget implementations are not required to use this
+ * class, but it gives them a head start.)
+ */
+
+class nsBaseWidget : public nsIWidget, public nsSupportsWeakReference {
+ template <class EventType, class InputType>
+ friend class DispatchEventOnMainThread;
+ friend class mozilla::widget::InProcessCompositorWidget;
+ friend class mozilla::layers::RemoteCompositorSession;
+
+ protected:
+ typedef base::Thread Thread;
+ typedef mozilla::gfx::DrawTarget DrawTarget;
+ typedef mozilla::gfx::SourceSurface SourceSurface;
+ typedef mozilla::layers::BufferMode BufferMode;
+ typedef mozilla::layers::CompositorBridgeChild CompositorBridgeChild;
+ typedef mozilla::layers::CompositorBridgeParent CompositorBridgeParent;
+ typedef mozilla::layers::IAPZCTreeManager IAPZCTreeManager;
+ typedef mozilla::layers::GeckoContentController GeckoContentController;
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+ typedef mozilla::layers::APZEventState APZEventState;
+ typedef mozilla::CSSIntRect CSSIntRect;
+ typedef mozilla::CSSRect CSSRect;
+ typedef mozilla::ScreenRotation ScreenRotation;
+ typedef mozilla::widget::CompositorWidgetDelegate CompositorWidgetDelegate;
+ typedef mozilla::layers::CompositorSession CompositorSession;
+ typedef mozilla::layers::ImageContainer ImageContainer;
+
+ virtual ~nsBaseWidget();
+
+ public:
+ nsBaseWidget();
+
+ explicit nsBaseWidget(BorderStyle aBorderStyle);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsIWidget interface
+ void CaptureRollupEvents(bool aDoCapture) override {}
+ nsIWidgetListener* GetWidgetListener() const override;
+ void SetWidgetListener(nsIWidgetListener* alistener) override;
+ void Destroy() override;
+ void SetParent(nsIWidget* aNewParent) override{};
+ nsIWidget* GetParent() override;
+ nsIWidget* GetTopLevelWidget() override;
+ nsIWidget* GetSheetWindowParent(void) override;
+ float GetDPI() override;
+ void AddChild(nsIWidget* aChild) override;
+ void RemoveChild(nsIWidget* aChild) override;
+
+ void SetZIndex(int32_t aZIndex) override;
+ void PlaceBehind(nsTopLevelWidgetZPlacement aPlacement, nsIWidget* aWidget,
+ bool aActivate) override {}
+
+ void GetWorkspaceID(nsAString& workspaceID) override;
+ void MoveToWorkspace(const nsAString& workspaceID) override;
+ bool IsTiled() const override { return mIsTiled; }
+
+ bool IsFullyOccluded() const override { return mIsFullyOccluded; }
+
+ void SetCursor(const Cursor&) override;
+ void SetCustomCursorAllowed(bool) override;
+ void ClearCachedCursor() final {
+ mCursor = {};
+ mUpdateCursor = true;
+ }
+ void SetTransparencyMode(TransparencyMode aMode) override;
+ TransparencyMode GetTransparencyMode() override;
+ void SetWindowShadowStyle(mozilla::WindowShadow) override {}
+ void SetShowsToolbarButton(bool aShow) override {}
+ void SetSupportsNativeFullscreen(bool aSupportsNativeFullscreen) override {}
+ void SetWindowAnimationType(WindowAnimationType aType) override {}
+ void HideWindowChrome(bool aShouldHide) override {}
+ bool PrepareForFullscreenTransition(nsISupports** aData) override {
+ return false;
+ }
+ void PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration, nsISupports* aData,
+ nsIRunnable* aCallback) override;
+ void CleanupFullscreenTransition() override {}
+ already_AddRefed<Screen> GetWidgetScreen() override;
+ nsresult MakeFullScreen(bool aFullScreen) override;
+ void InfallibleMakeFullScreen(bool aFullScreen);
+
+ WindowRenderer* GetWindowRenderer() override;
+ bool HasWindowRenderer() const final { return !!mWindowRenderer; }
+
+ // A remote compositor session tied to this window has been lost and IPC
+ // messages will no longer work. The widget must clean up any lingering
+ // resources and possibly schedule another paint.
+ //
+ // A reference to the session object is held until this function has
+ // returned. Callers should hold a reference to the widget, since this
+ // function could deallocate the widget if it is unparented.
+ virtual void NotifyCompositorSessionLost(
+ mozilla::layers::CompositorSession* aSession);
+
+ already_AddRefed<mozilla::CompositorVsyncDispatcher>
+ GetCompositorVsyncDispatcher();
+ virtual void CreateCompositorVsyncDispatcher();
+ virtual void CreateCompositor();
+ virtual void CreateCompositor(int aWidth, int aHeight);
+ virtual void SetCompositorWidgetDelegate(CompositorWidgetDelegate*) {}
+ void PrepareWindowEffects() override {}
+ void UpdateThemeGeometries(
+ const nsTArray<ThemeGeometry>& aThemeGeometries) override {}
+ void SetModal(bool aModal) override {}
+ uint32_t GetMaxTouchPoints() const override;
+ void SetWindowClass(const nsAString& xulWinType, const nsAString& xulWinClass,
+ const nsAString& xulWinName) override {}
+ // Return whether this widget interprets parameters to Move and Resize APIs
+ // as "desktop pixels" rather than "device pixels", and therefore
+ // applies its GetDefaultScale() value to them before using them as mBounds
+ // etc (which are always stored in device pixels).
+ // Note that APIs that -get- the widget's position/size/bounds, rather than
+ // -setting- them (i.e. moving or resizing the widget) will always return
+ // values in the widget's device pixels.
+ bool BoundsUseDesktopPixels() const {
+ return mWindowType <= WindowType::Popup;
+ }
+ // Default implementation, to be overridden by platforms where desktop coords
+ // are virtualized and may not correspond to device pixels on the screen.
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() override {
+ return mozilla::DesktopToLayoutDeviceScale(1.0);
+ }
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScaleByScreen()
+ override;
+
+ void ConstrainPosition(DesktopIntPoint&) override {}
+ void MoveClient(const DesktopPoint& aOffset) override;
+ void ResizeClient(const DesktopSize& aSize, bool aRepaint) override;
+ void ResizeClient(const DesktopRect& aRect, bool aRepaint) override;
+ LayoutDeviceIntRect GetBounds() override;
+ LayoutDeviceIntRect GetClientBounds() override;
+ LayoutDeviceIntRect GetScreenBounds() override;
+ [[nodiscard]] nsresult GetRestoredBounds(LayoutDeviceIntRect& aRect) override;
+ nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) override;
+ LayoutDeviceIntPoint GetClientOffset() override;
+ void EnableDragDrop(bool aEnable) override{};
+ nsresult AsyncEnableDragDrop(bool aEnable) override;
+ void SetResizeMargin(mozilla::LayoutDeviceIntCoord aResizeMargin) override;
+ [[nodiscard]] nsresult GetAttention(int32_t aCycleCount) override {
+ return NS_OK;
+ }
+ bool HasPendingInputEvent() override;
+ void SetIcon(const nsAString& aIconSpec) override {}
+ bool ShowsResizeIndicator(LayoutDeviceIntRect* aResizerRect) override;
+ void FreeNativeData(void* data, uint32_t aDataType) override {}
+ nsresult ActivateNativeMenuItemAt(const nsAString& indexString) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ nsresult ForceUpdateNativeMenuAt(const nsAString& indexString) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ nsresult NotifyIME(const IMENotification& aIMENotification) final;
+ [[nodiscard]] nsresult AttachNativeKeyEvent(
+ mozilla::WidgetKeyboardEvent& aEvent) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ bool ComputeShouldAccelerate();
+ virtual bool WidgetTypeSupportsAcceleration() { return true; }
+ [[nodiscard]] nsresult OnDefaultButtonLoaded(
+ const LayoutDeviceIntRect& aButtonRect) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ already_AddRefed<nsIWidget> CreateChild(
+ const LayoutDeviceIntRect& aRect, InitData* aInitData = nullptr,
+ bool aForceUseIWidgetParent = false) override;
+ void AttachViewToTopLevel(bool aUseAttachedEvents) override;
+ nsIWidgetListener* GetAttachedWidgetListener() const override;
+ void SetAttachedWidgetListener(nsIWidgetListener* aListener) override;
+ nsIWidgetListener* GetPreviouslyAttachedWidgetListener() override;
+ void SetPreviouslyAttachedWidgetListener(nsIWidgetListener*) override;
+ NativeIMEContext GetNativeIMEContext() override;
+ TextEventDispatcher* GetTextEventDispatcher() final;
+ TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override;
+ void ZoomToRect(const uint32_t& aPresShellId,
+ const ScrollableLayerGuid::ViewID& aViewId,
+ const CSSRect& aRect, const uint32_t& aFlags) override;
+
+ // Dispatch an event that must be first be routed through APZ.
+ ContentAndAPZEventStatus DispatchInputEvent(
+ mozilla::WidgetInputEvent* aEvent) override;
+ void DispatchEventToAPZOnly(mozilla::WidgetInputEvent* aEvent) override;
+
+ bool DispatchWindowEvent(mozilla::WidgetGUIEvent& event) override;
+
+ void SetConfirmedTargetAPZC(
+ uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) const override;
+
+ void UpdateZoomConstraints(
+ const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
+ const mozilla::Maybe<ZoomConstraints>& aConstraints) override;
+
+ bool AsyncPanZoomEnabled() const override;
+
+ void SwipeFinished() override;
+ void ReportSwipeStarted(uint64_t aInputBlockId, bool aStartSwipe) override;
+ void TrackScrollEventAsSwipe(const mozilla::PanGestureInput& aSwipeStartEvent,
+ uint32_t aAllowedDirections,
+ uint64_t aInputBlockId);
+ struct SwipeInfo {
+ bool wantsSwipe;
+ uint32_t allowedDirections;
+ };
+ SwipeInfo SendMayStartSwipe(const mozilla::PanGestureInput& aSwipeStartEvent);
+ // Returns a WidgetWheelEvent which needs to be handled by APZ regardless of
+ // whether |aPanInput| event was used for SwipeTracker or not.
+ mozilla::WidgetWheelEvent MayStartSwipeForAPZ(
+ const mozilla::PanGestureInput& aPanInput,
+ const mozilla::layers::APZEventResult& aApzResult);
+
+ // Returns true if |aPanInput| event was used for SwipeTracker, false
+ // otherwise.
+ bool MayStartSwipeForNonAPZ(const mozilla::PanGestureInput& aPanInput);
+
+ void NotifyWindowDestroyed();
+ void NotifySizeMoveDone();
+
+ using ByMoveToRect = nsIWidgetListener::ByMoveToRect;
+ void NotifyWindowMoved(int32_t aX, int32_t aY,
+ ByMoveToRect = ByMoveToRect::No);
+
+ // Should be called by derived implementations to notify on system color and
+ // theme changes.
+ void NotifyThemeChanged(mozilla::widget::ThemeChangeKind);
+
+#ifdef ACCESSIBILITY
+ // Get the accessible for the window.
+ mozilla::a11y::LocalAccessible* GetRootAccessible();
+#endif
+
+ // Return true if this is a simple widget (that is typically not worth
+ // accelerating)
+ bool IsSmallPopup() const;
+
+ PopupLevel GetPopupLevel() { return mPopupLevel; }
+
+ // return true if this is a popup widget with a native titlebar
+ bool IsPopupWithTitleBar() const {
+ return (mWindowType == WindowType::Popup &&
+ mBorderStyle != BorderStyle::Default &&
+ mBorderStyle & BorderStyle::Title);
+ }
+
+ void ReparentNativeWidget(nsIWidget* aNewParent) override {}
+
+ const SizeConstraints GetSizeConstraints() override;
+ void SetSizeConstraints(const SizeConstraints& aConstraints) override;
+
+ void StartAsyncScrollbarDrag(const AsyncDragMetrics& aDragMetrics) override;
+
+ bool StartAsyncAutoscroll(const ScreenPoint& aAnchorLocation,
+ const ScrollableLayerGuid& aGuid) override;
+
+ void StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ mozilla::layers::LayersId GetRootLayerTreeId() override;
+
+ /**
+ * Use this when GetLayerManager() returns a BasicLayerManager
+ * (nsBaseWidget::GetLayerManager() does). This sets up the widget's
+ * layer manager to temporarily render into aTarget.
+ *
+ * |aNaturalWidgetBounds| is the un-rotated bounds of |aWidget|.
+ * |aRotation| is the "virtual rotation" to apply when rendering to
+ * the target. When |aRotation| is ROTATION_0,
+ * |aNaturalWidgetBounds| is not used.
+ */
+ class AutoLayerManagerSetup {
+ public:
+ AutoLayerManagerSetup(nsBaseWidget* aWidget, gfxContext* aTarget,
+ BufferMode aDoubleBuffering);
+ ~AutoLayerManagerSetup();
+
+ private:
+ nsBaseWidget* mWidget;
+ mozilla::FallbackRenderer* mRenderer = nullptr;
+ };
+ friend class AutoLayerManagerSetup;
+
+ virtual bool ShouldUseOffMainThreadCompositing();
+
+ static nsIRollupListener* GetActiveRollupListener();
+
+ void Shutdown();
+
+ void QuitIME();
+
+ // These functions should be called at the start and end of a "live" widget
+ // resize (i.e. when the window contents are repainting during the resize,
+ // such as when the user drags a window border). It will suppress the
+ // displayport during the live resize to avoid unneccessary overpainting.
+ void NotifyLiveResizeStarted();
+ void NotifyLiveResizeStopped();
+
+#if defined(MOZ_WIDGET_ANDROID)
+ void RecvToolbarAnimatorMessageFromCompositor(int32_t) override{};
+ void UpdateRootFrameMetrics(const ScreenPoint& aScrollOffset,
+ const CSSToScreenScale& aZoom) override{};
+ void RecvScreenPixels(mozilla::ipc::Shmem&& aMem, const ScreenIntSize& aSize,
+ bool aNeedsYFlip) override{};
+#endif
+
+ virtual void LocalesChanged() {}
+
+ virtual void NotifyOcclusionState(mozilla::widget::OcclusionState aState) {}
+
+ protected:
+ // These are methods for CompositorWidgetWrapper, and should only be
+ // accessed from that class. Derived widgets can choose which methods to
+ // implement, or none if supporting out-of-process compositing.
+ virtual bool PreRender(mozilla::widget::WidgetRenderingContext* aContext) {
+ return true;
+ }
+ virtual void PostRender(mozilla::widget::WidgetRenderingContext* aContext) {}
+ virtual RefPtr<mozilla::layers::NativeLayerRoot> GetNativeLayerRoot() {
+ return nullptr;
+ }
+ virtual already_AddRefed<DrawTarget> StartRemoteDrawing();
+ virtual already_AddRefed<DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion, BufferMode* aBufferMode) {
+ return StartRemoteDrawing();
+ }
+ virtual void EndRemoteDrawing() {}
+ virtual void EndRemoteDrawingInRegion(
+ DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
+ EndRemoteDrawing();
+ }
+ virtual void CleanupRemoteDrawing() {}
+ virtual void CleanupWindowEffects() {}
+ virtual bool InitCompositor(mozilla::layers::Compositor* aCompositor) {
+ return true;
+ }
+ virtual uint32_t GetGLFrameBufferFormat();
+ virtual bool CompositorInitiallyPaused() { return false; }
+
+ protected:
+ void ResolveIconName(const nsAString& aIconName, const nsAString& aIconSuffix,
+ nsIFile** aResult);
+ virtual void OnDestroy();
+ void BaseCreate(nsIWidget* aParent, InitData* aInitData);
+
+ virtual void ConfigureAPZCTreeManager();
+ virtual void ConfigureAPZControllerThread();
+ virtual already_AddRefed<GeckoContentController>
+ CreateRootContentController();
+
+ // Dispatch an event that has already been routed through APZ.
+ nsEventStatus ProcessUntransformedAPZEvent(
+ mozilla::WidgetInputEvent* aEvent,
+ const mozilla::layers::APZEventResult& aApzResult);
+
+ nsresult SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout,
+ int32_t aNativeKeyCode,
+ uint32_t aModifierFlags,
+ const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters,
+ nsIObserver* aObserver) override {
+ mozilla::widget::AutoObserverNotifier notifier(aObserver, "keyevent");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override {
+ mozilla::widget::AutoObserverNotifier notifier(aObserver, "mouseevent");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ mozilla::widget::AutoObserverNotifier notifier(aObserver, "mouseevent");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override {
+ mozilla::widget::AutoObserverNotifier notifier(aObserver,
+ "mousescrollevent");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override {
+ mozilla::widget::AutoObserverNotifier notifier(aObserver, "touchpoint");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativeTouchPadPinch(TouchpadGesturePhase aEventPhase,
+ float aScale,
+ LayoutDeviceIntPoint aPoint,
+ int32_t aModifierFlags) override {
+ MOZ_RELEASE_ASSERT(
+ false, "This method is not implemented on the current platform");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativePenInput(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPressure, uint32_t aRotation,
+ int32_t aTiltX, int32_t aTiltY,
+ int32_t aButton,
+ nsIObserver* aObserver) override {
+ MOZ_RELEASE_ASSERT(
+ false, "This method is not implemented on the current platform");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativeTouchpadDoubleTap(LayoutDeviceIntPoint aPoint,
+ uint32_t aModifierFlags) override {
+ MOZ_RELEASE_ASSERT(
+ false, "This method is not implemented on the current platform");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) override {
+ MOZ_RELEASE_ASSERT(
+ false, "This method is not implemented on the current platform");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ /**
+ * GetPseudoIMEContext() returns pseudo IME context when TextEventDispatcher
+ * has non-native input transaction. Otherwise, returns nullptr.
+ */
+ void* GetPseudoIMEContext();
+
+ protected:
+ virtual already_AddRefed<nsIWidget> AllocateChildPopupWidget() {
+ return nsIWidget::CreateChildWindow();
+ }
+
+ WindowRenderer* CreateFallbackRenderer();
+
+ PopupType GetPopupType() const { return mPopupType; }
+
+ bool HasRemoteContent() const { return mHasRemoteContent; }
+
+ /**
+ * Apply the current size constraints to the given size.
+ *
+ * @param aWidth width to constrain
+ * @param aHeight height to constrain
+ */
+ void ConstrainSize(int32_t* aWidth, int32_t* aHeight) override {
+ SizeConstraints c = GetSizeConstraints();
+ *aWidth = std::max(c.mMinSize.width, std::min(c.mMaxSize.width, *aWidth));
+ *aHeight =
+ std::max(c.mMinSize.height, std::min(c.mMaxSize.height, *aHeight));
+ }
+
+ CompositorBridgeChild* GetRemoteRenderer() override;
+
+ void ClearCachedWebrenderResources() override;
+
+ void ClearWebrenderAnimationResources() override;
+
+ bool SetNeedFastSnaphot() override;
+
+ /**
+ * Notify the widget that this window is being used with OMTC.
+ */
+ virtual void WindowUsesOMTC() {}
+ virtual void RegisterTouchWindow() {}
+
+ mozilla::dom::Document* GetDocument() const;
+
+ void EnsureTextEventDispatcher();
+
+ // Notify the compositor that a device reset has occurred.
+ void OnRenderingDeviceReset();
+
+ bool UseAPZ();
+
+ bool AllowWebRenderForThisWindow();
+
+ /**
+ * For widgets that support synthesizing native touch events, this function
+ * can be used to manage the current state of synthetic pointers. Each widget
+ * must maintain its own MultiTouchInput instance and pass it in as the state,
+ * along with the desired parameters for the changes. This function returns
+ * a new MultiTouchInput object that is ready to be dispatched.
+ */
+ mozilla::MultiTouchInput UpdateSynthesizedTouchState(
+ mozilla::MultiTouchInput* aState, mozilla::TimeStamp aTimeStamp,
+ uint32_t aPointerId, TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPointerPressure,
+ uint32_t aPointerOrientation);
+
+ /**
+ * Dispatch the given MultiTouchInput through APZ to Gecko (if APZ is enabled)
+ * or directly to gecko (if APZ is not enabled). This function must only
+ * be called from the main thread, and if APZ is enabled, that must also be
+ * the APZ controller thread.
+ */
+ void DispatchTouchInput(
+ mozilla::MultiTouchInput& aInput,
+ uint16_t aInputSource =
+ mozilla::dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH);
+
+ /**
+ * Dispatch the given PanGestureInput through APZ to Gecko (if APZ is enabled)
+ * or directly to gecko (if APZ is not enabled). This function must only
+ * be called from the main thread, and if APZ is enabled, that must also be
+ * the APZ controller thread.
+ */
+ void DispatchPanGestureInput(mozilla::PanGestureInput& aInput);
+ void DispatchPinchGestureInput(mozilla::PinchGestureInput& aInput);
+
+ static bool ConvertStatus(nsEventStatus aStatus) {
+ return aStatus == nsEventStatus_eConsumeNoDefault;
+ }
+
+ protected:
+ // Returns whether compositing should use an external surface size.
+ virtual bool UseExternalCompositingSurface() const { return false; }
+
+ /**
+ * Starts the OMTC compositor destruction sequence.
+ *
+ * When this function returns, the compositor should not be
+ * able to access the opengl context anymore.
+ * It is safe to call it several times if platform implementations
+ * require the compositor to be destroyed before ~nsBaseWidget is
+ * reached (This is the case with gtk2 for instance).
+ */
+ virtual void DestroyCompositor();
+ void DestroyLayerManager();
+ void ReleaseContentController();
+ void RevokeTransactionIdAllocator();
+
+ void FreeShutdownObserver();
+ void FreeLocalesChangedObserver();
+
+ bool IsPIPWindow() const { return mIsPIPWindow; };
+
+ nsIWidgetListener* mWidgetListener;
+ nsIWidgetListener* mAttachedWidgetListener;
+ nsIWidgetListener* mPreviouslyAttachedWidgetListener;
+ RefPtr<WindowRenderer> mWindowRenderer;
+ RefPtr<CompositorSession> mCompositorSession;
+ RefPtr<CompositorBridgeChild> mCompositorBridgeChild;
+
+ mozilla::UniquePtr<mozilla::Mutex> mCompositorVsyncDispatcherLock;
+ RefPtr<mozilla::CompositorVsyncDispatcher> mCompositorVsyncDispatcher;
+
+ RefPtr<IAPZCTreeManager> mAPZC;
+ RefPtr<GeckoContentController> mRootContentController;
+ RefPtr<APZEventState> mAPZEventState;
+ RefPtr<WidgetShutdownObserver> mShutdownObserver;
+ RefPtr<LocalesChangedObserver> mLocalesChangedObserver;
+ RefPtr<TextEventDispatcher> mTextEventDispatcher;
+ RefPtr<mozilla::SwipeTracker> mSwipeTracker;
+ mozilla::UniquePtr<mozilla::SwipeEventQueue> mSwipeEventQueue;
+ Cursor mCursor;
+ bool mCustomCursorAllowed = true;
+ BorderStyle mBorderStyle;
+ LayoutDeviceIntRect mBounds;
+ bool mIsTiled;
+ PopupLevel mPopupLevel;
+ PopupType mPopupType;
+ SizeConstraints mSizeConstraints;
+ bool mHasRemoteContent;
+
+ struct FullscreenSavedState {
+ DesktopRect windowRect;
+ DesktopRect screenRect;
+ };
+ mozilla::Maybe<FullscreenSavedState> mSavedBounds;
+
+ bool mUpdateCursor;
+ bool mUseAttachedEvents;
+ bool mIMEHasFocus;
+ bool mIMEHasQuit;
+ bool mIsFullyOccluded;
+ bool mNeedFastSnaphot;
+ // This flag is only used when APZ is off. It indicates that the current pan
+ // gesture was processed as a swipe. Sometimes the swipe animation can finish
+ // before momentum events of the pan gesture have stopped firing, so this
+ // flag tells us that we shouldn't allow the remaining events to cause
+ // scrolling. It is reset to false once a new gesture starts (as indicated by
+ // a PANGESTURE_(MAY)START event).
+ bool mCurrentPanGestureBelongsToSwipe;
+
+ // It's PictureInPicture window.
+ bool mIsPIPWindow : 1;
+
+ struct InitialZoomConstraints {
+ InitialZoomConstraints(const uint32_t& aPresShellID,
+ const ScrollableLayerGuid::ViewID& aViewID,
+ const ZoomConstraints& aConstraints)
+ : mPresShellID(aPresShellID),
+ mViewID(aViewID),
+ mConstraints(aConstraints) {}
+
+ uint32_t mPresShellID;
+ ScrollableLayerGuid::ViewID mViewID;
+ ZoomConstraints mConstraints;
+ };
+
+ mozilla::Maybe<InitialZoomConstraints> mInitialZoomConstraints;
+
+ // This points to the resize listeners who have been notified that a live
+ // resize is in progress. This should always be empty when a live-resize is
+ // not in progress.
+ nsTArray<RefPtr<mozilla::LiveResizeListener>> mLiveResizeListeners;
+
+#ifdef DEBUG
+ protected:
+ static nsAutoString debug_GuiEventToString(
+ mozilla::WidgetGUIEvent* aGuiEvent);
+
+ static void debug_DumpInvalidate(FILE* aFileOut, nsIWidget* aWidget,
+ const LayoutDeviceIntRect* aRect,
+ const char* aWidgetName, int32_t aWindowID);
+
+ static void debug_DumpEvent(FILE* aFileOut, nsIWidget* aWidget,
+ mozilla::WidgetGUIEvent* aGuiEvent,
+ const char* aWidgetName, int32_t aWindowID);
+
+ static void debug_DumpPaintEvent(FILE* aFileOut, nsIWidget* aWidget,
+ const nsIntRegion& aPaintEvent,
+ const char* aWidgetName, int32_t aWindowID);
+
+ static bool debug_GetCachedBoolPref(const char* aPrefName);
+#endif
+
+ private:
+ already_AddRefed<mozilla::layers::WebRenderLayerManager>
+ CreateCompositorSession(int aWidth, int aHeight,
+ mozilla::layers::CompositorOptions* aOptionsOut);
+};
+
+#endif // nsBaseWidget_h__
diff --git a/widget/nsCUPSShim.cpp b/widget/nsCUPSShim.cpp
new file mode 100644
index 0000000000..d999a1be17
--- /dev/null
+++ b/widget/nsCUPSShim.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ex: set tabstop=8 softtabstop=2 shiftwidth=2 expandtab: */
+/* 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 "nsDebug.h"
+#include "nsString.h"
+#include "nsCUPSShim.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Logging.h"
+#include "prlink.h"
+
+#ifdef CUPS_SHIM_RUNTIME_LINK
+
+mozilla::LazyLogModule gCupsLinkLog("CupsLink");
+
+# define DEBUG_LOG(...) \
+ MOZ_LOG(gCupsLinkLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+// TODO: This is currently pointless as we always use the compile-time linked
+// version of CUPS, but in the future this may become a configure option.
+// We also cannot use NSPR's library suffix support, since that cannot handle
+// version number suffixes.
+# ifdef XP_MACOSX
+static const char gCUPSLibraryName[] = "libcups.2.dylib";
+# else
+static const char gCUPSLibraryName[] = "libcups.so.2";
+# endif
+
+static bool LoadCupsFunc(PRLibrary* aLib, void** aDest, const char* const aName,
+ nsCUPSShim::Optional aOptional) {
+ *aDest = PR_FindSymbol(aLib, aName);
+ if (!*aDest) {
+ DEBUG_LOG("%s not found in CUPS library", aName);
+ return bool(aOptional);
+ }
+ return true;
+}
+
+nsCUPSShim::nsCUPSShim() {
+ mCupsLib = PR_LoadLibrary(gCUPSLibraryName);
+ if (!mCupsLib) {
+ DEBUG_LOG("CUPS library not found");
+ return;
+ }
+
+ bool success = true;
+
+ // This is a macro so that it could also load from libcups if we are
+ // configured to use it as a compile-time dependency.
+ //
+ // We try to load all functions even if some fail so that we get the debug log
+ // unconditionally.
+# define CUPS_SHIM_LOAD(opt_, fn_) \
+ success |= \
+ LoadCupsFunc(mCupsLib, reinterpret_cast<void**>(&fn_), #fn_, opt_);
+ CUPS_SHIM_ALL_FUNCS(CUPS_SHIM_LOAD)
+# undef CUPS_SHIM_LOAD
+
+ if (!success) {
+# ifndef MOZ_TSAN
+ // With TSan, we cannot unload libcups once we have loaded it because
+ // TSan does not support unloading libraries that are matched from its
+ // suppression list. Hence we just keep the library loaded in TSan builds.
+ PR_UnloadLibrary(mCupsLib);
+# endif
+ mCupsLib = nullptr;
+ return;
+ }
+
+ // Set mInitOkay only if all cups functions are loaded successfully.
+ mInitOkay = true;
+}
+
+#endif
diff --git a/widget/nsCUPSShim.h b/widget/nsCUPSShim.h
new file mode 100644
index 0000000000..8ab00a48ad
--- /dev/null
+++ b/widget/nsCUPSShim.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ex: set tabstop=8 softtabstop=2 shiftwidth=2 expandtab: */
+/* 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/. */
+
+#ifndef nsCUPSShim_h___
+#define nsCUPSShim_h___
+
+#include <cups/cups.h>
+#include "mozilla/Atomics.h"
+#include "mozilla/Mutex.h"
+
+// TODO: This should be a configure option, ideally.
+#ifndef XP_MACOSX
+# define CUPS_SHIM_RUNTIME_LINK
+#endif
+
+struct PRLibrary;
+
+class nsCUPSShim {
+ public:
+#ifdef CUPS_SHIM_RUNTIME_LINK
+ nsCUPSShim();
+ bool InitOkay() { return mInitOkay; }
+#else
+ nsCUPSShim() = default;
+ bool InitOkay() { return true; }
+#endif
+
+ // We allow some functions to be missing, to degrade as gracefully as possible
+ // for older versions of CUPS.
+ //
+ // The current target is CUPS 1.6 (bug 1701019).
+ enum class Optional : bool { No, Yes };
+
+ /**
+ * Function pointers for supported functions. These are only
+ * valid after successful initialization.
+ */
+#define CUPS_SHIM_ALL_FUNCS(X) \
+ X(Optional::No, cupsAddOption) \
+ X(Optional::No, cupsCheckDestSupported) \
+ X(Optional::No, cupsConnectDest) \
+ X(Optional::No, cupsCopyDest) \
+ X(Optional::No, cupsCopyDestInfo) \
+ X(Optional::No, cupsDoRequest) \
+ X(Optional::No, cupsEnumDests) \
+ X(Optional::No, cupsFreeDestInfo) \
+ X(Optional::No, cupsFreeDests) \
+ X(Optional::No, cupsGetDestMediaByName) \
+ X(Optional::Yes, cupsFindDestDefault) \
+ X(Optional::Yes, cupsGetDestMediaDefault) \
+ X(Optional::Yes, cupsGetDestMediaCount) \
+ X(Optional::Yes, cupsGetDestMediaByIndex) \
+ X(Optional::Yes, cupsLocalizeDestMedia) \
+ X(Optional::No, cupsGetDest) \
+ X(Optional::No, cupsGetDests) \
+ X(Optional::No, cupsGetDests2) \
+ X(Optional::No, cupsGetNamedDest) \
+ X(Optional::No, cupsGetOption) \
+ X(Optional::No, cupsServer) \
+ X(Optional::Yes, httpAddrPort) \
+ X(Optional::No, httpClose) \
+ X(Optional::No, httpGetHostname) \
+ X(Optional::Yes, httpGetAddress) \
+ X(Optional::No, ippAddString) \
+ X(Optional::No, ippAddStrings) \
+ X(Optional::No, ippDelete) \
+ X(Optional::No, ippFindAttribute) \
+ X(Optional::No, ippGetCount) \
+ X(Optional::No, ippGetString) \
+ X(Optional::No, ippNewRequest) \
+ X(Optional::No, ippPort)
+
+#ifdef CUPS_SHIM_RUNTIME_LINK
+ // Define a single field which holds a function pointer.
+# define CUPS_SHIM_FUNC_DECL(opt_, fn_) decltype(::fn_)* fn_ = nullptr;
+#else
+ // Define a static constexpr function pointer. GCC can sometimes optimize
+ // away the pointer fetch for this.
+# define CUPS_SHIM_FUNC_DECL(opt_, fn_) \
+ static constexpr decltype(::fn_)* const fn_ = ::fn_;
+#endif
+
+ CUPS_SHIM_ALL_FUNCS(CUPS_SHIM_FUNC_DECL)
+#undef CUPS_SHIM_FUNC_DECL
+
+#ifdef CUPS_SHIM_RUNTIME_LINK
+ private:
+ bool mInitOkay = false;
+ PRLibrary* mCupsLib = nullptr;
+#endif
+};
+
+#endif /* nsCUPSShim_h___ */
diff --git a/widget/nsClipboardHelper.cpp b/widget/nsClipboardHelper.cpp
new file mode 100644
index 0000000000..3439adef48
--- /dev/null
+++ b/widget/nsClipboardHelper.cpp
@@ -0,0 +1,123 @@
+/* -*- 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 "nsClipboardHelper.h"
+
+// basics
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsServiceManagerUtils.h"
+
+// helpers
+#include "nsIClipboard.h"
+#include "mozilla/dom/Document.h"
+#include "nsITransferable.h"
+#include "nsReadableUtils.h"
+
+NS_IMPL_ISUPPORTS(nsClipboardHelper, nsIClipboardHelper)
+
+/*****************************************************************************
+ * nsClipboardHelper ctor / dtor
+ *****************************************************************************/
+
+nsClipboardHelper::nsClipboardHelper() = default;
+
+nsClipboardHelper::~nsClipboardHelper() {
+ // no members, nothing to destroy
+}
+
+/*****************************************************************************
+ * nsIClipboardHelper methods
+ *****************************************************************************/
+
+NS_IMETHODIMP
+nsClipboardHelper::CopyStringToClipboard(const nsAString& aString,
+ int32_t aClipboardID,
+ SensitiveData aSensitive) {
+ nsresult rv;
+
+ // get the clipboard
+ nsCOMPtr<nsIClipboard> clipboard(
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(clipboard, NS_ERROR_FAILURE);
+
+ // don't go any further if they're asking for the selection
+ // clipboard on a platform which doesn't support it (i.e., unix)
+ if (nsIClipboard::kSelectionClipboard == aClipboardID &&
+ !clipboard->IsClipboardTypeSupported(nsIClipboard::kSelectionClipboard)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // don't go any further if they're asking for the find clipboard on a platform
+ // which doesn't support it (i.e., non-osx)
+ if (nsIClipboard::kFindClipboard == aClipboardID &&
+ !clipboard->IsClipboardTypeSupported(nsIClipboard::kFindClipboard)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // create a transferable for putting data on the clipboard
+ nsCOMPtr<nsITransferable> trans(
+ do_CreateInstance("@mozilla.org/widget/transferable;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
+
+ trans->Init(nullptr);
+ if (aSensitive == SensitiveData::Sensitive) {
+ trans->SetIsPrivateData(true);
+ }
+
+ // Add the text data flavor to the transferable
+ rv = trans->AddDataFlavor(kTextMime);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get wStrings to hold clip data
+ nsCOMPtr<nsISupportsString> data(
+ do_CreateInstance("@mozilla.org/supports-string;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(data, NS_ERROR_FAILURE);
+
+ // populate the string
+ rv = data->SetData(aString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Pass the data object as |nsISupports| so that when the transferable holds
+ // onto it, it will addref the correct interface.
+ rv = trans->SetTransferData(kTextMime, ToSupports(data));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // put the transferable on the clipboard
+ rv = clipboard->SetData(trans, nullptr, aClipboardID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsClipboardHelper::CopyString(const nsAString& aString,
+ SensitiveData aSensitive) {
+ nsresult rv;
+
+ // copy to the global clipboard. it's bad if this fails in any way.
+ rv = CopyStringToClipboard(aString, nsIClipboard::kGlobalClipboard,
+ aSensitive);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // unix also needs us to copy to the selection clipboard. this will
+ // fail in CopyStringToClipboard if we're not on a platform that
+ // supports the selection clipboard. (this could have been #ifdef
+ // XP_UNIX, but using the IsClipboardTypeSupported call is the more correct
+ // thing to do.
+ //
+ // if this fails in any way other than "not being unix", we'll get
+ // the assertion we need in CopyStringToClipboard, and we needn't
+ // assert again here.
+ CopyStringToClipboard(aString, nsIClipboard::kSelectionClipboard, aSensitive);
+
+ return NS_OK;
+}
diff --git a/widget/nsClipboardHelper.h b/widget/nsClipboardHelper.h
new file mode 100644
index 0000000000..b9954e7bd4
--- /dev/null
+++ b/widget/nsClipboardHelper.h
@@ -0,0 +1,30 @@
+/* -*- 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/. */
+
+#ifndef nsClipboardHelper_h__
+#define nsClipboardHelper_h__
+
+// interfaces
+#include "nsIClipboardHelper.h"
+
+// basics
+#include "nsString.h"
+
+/**
+ * impl class for nsIClipboardHelper, a helper for common uses of nsIClipboard.
+ */
+
+class nsClipboardHelper : public nsIClipboardHelper {
+ virtual ~nsClipboardHelper();
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICLIPBOARDHELPER
+
+ nsClipboardHelper();
+};
+
+#endif // nsClipboardHelper_h__
diff --git a/widget/nsClipboardProxy.cpp b/widget/nsClipboardProxy.cpp
new file mode 100644
index 0000000000..41f285461b
--- /dev/null
+++ b/widget/nsClipboardProxy.cpp
@@ -0,0 +1,271 @@
+/* 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 "nsClipboardProxy.h"
+
+#if defined(ACCESSIBILITY) && defined(XP_WIN)
+# include "mozilla/a11y/Compatibility.h"
+#endif
+#include "mozilla/ClipboardReadRequestChild.h"
+#include "mozilla/ClipboardWriteRequestChild.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/Unused.h"
+#include "nsArrayUtils.h"
+#include "nsBaseClipboard.h"
+#include "nsISupportsPrimitives.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsContentUtils.h"
+#include "PermissionMessageUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_ISUPPORTS(nsClipboardProxy, nsIClipboard, nsIClipboardProxy)
+
+nsClipboardProxy::nsClipboardProxy() : mClipboardCaps(false, false, false) {}
+
+NS_IMETHODIMP
+nsClipboardProxy::SetData(nsITransferable* aTransferable,
+ nsIClipboardOwner* anOwner, int32_t aWhichClipboard) {
+#if defined(ACCESSIBILITY) && defined(XP_WIN)
+ a11y::Compatibility::SuppressA11yForClipboardCopy();
+#endif
+
+ ContentChild* child = ContentChild::GetSingleton();
+ IPCTransferable ipcTransferable;
+ nsContentUtils::TransferableToIPCTransferable(aTransferable, &ipcTransferable,
+ false, nullptr);
+ child->SendSetClipboard(std::move(ipcTransferable), aWhichClipboard);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsClipboardProxy::AsyncSetData(
+ int32_t aWhichClipboard, nsIAsyncClipboardRequestCallback* aCallback,
+ nsIAsyncSetClipboardData** _retval) {
+ RefPtr<ClipboardWriteRequestChild> request =
+ MakeRefPtr<ClipboardWriteRequestChild>(aCallback);
+ ContentChild::GetSingleton()->SendPClipboardWriteRequestConstructor(
+ request, aWhichClipboard);
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsClipboardProxy::GetData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard,
+ mozilla::dom::WindowContext* aWindowContext) {
+ MOZ_DIAGNOSTIC_ASSERT(aWindowContext && aWindowContext->IsInProcess(),
+ "content clipboard reads must be associated with an "
+ "in-process WindowContext");
+ if (aWindowContext->IsDiscarded()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ nsTArray<nsCString> types;
+ aTransferable->FlavorsTransferableCanImport(types);
+
+ IPCTransferableData transferable;
+ ContentChild::GetSingleton()->SendGetClipboard(types, aWhichClipboard,
+ aWindowContext, &transferable);
+ return nsContentUtils::IPCTransferableDataToTransferable(
+ transferable, false /* aAddDataFlavor */, aTransferable,
+ false /* aFilterUnknownFlavors */);
+}
+
+namespace {
+
+class AsyncGetClipboardDataProxy final : public nsIAsyncGetClipboardData {
+ public:
+ explicit AsyncGetClipboardDataProxy(ClipboardReadRequestChild* aActor)
+ : mActor(aActor) {
+ MOZ_ASSERT(mActor);
+ }
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCGETCLIPBOARDDATA
+
+ private:
+ virtual ~AsyncGetClipboardDataProxy() {
+ MOZ_ASSERT(mActor);
+ if (mActor->CanSend()) {
+ PClipboardReadRequestChild::Send__delete__(mActor);
+ }
+ };
+
+ RefPtr<ClipboardReadRequestChild> mActor;
+};
+
+NS_IMPL_ISUPPORTS(AsyncGetClipboardDataProxy, nsIAsyncGetClipboardData)
+
+NS_IMETHODIMP AsyncGetClipboardDataProxy::GetValid(bool* aOutResult) {
+ MOZ_ASSERT(mActor);
+ *aOutResult = mActor->CanSend();
+ return NS_OK;
+}
+
+NS_IMETHODIMP AsyncGetClipboardDataProxy::GetFlavorList(
+ nsTArray<nsCString>& aFlavorList) {
+ MOZ_ASSERT(mActor);
+ aFlavorList.AppendElements(mActor->FlavorList());
+ return NS_OK;
+}
+
+NS_IMETHODIMP AsyncGetClipboardDataProxy::GetData(
+ nsITransferable* aTransferable,
+ nsIAsyncClipboardRequestCallback* aCallback) {
+ if (!aTransferable || !aCallback) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Get a list of flavors this transferable can import
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ MOZ_ASSERT(mActor);
+ // If the requested flavor is not in the list, throw an error.
+ for (const auto& flavor : flavors) {
+ if (!mActor->FlavorList().Contains(flavor)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (!mActor->CanSend()) {
+ return aCallback->OnComplete(NS_ERROR_FAILURE);
+ }
+
+ mActor->SendGetData(flavors)->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ /* resolve */
+ [self = RefPtr{this}, callback = nsCOMPtr{aCallback},
+ transferable = nsCOMPtr{aTransferable}](
+ const IPCTransferableDataOrError& aIpcTransferableDataOrError) {
+ if (aIpcTransferableDataOrError.type() ==
+ IPCTransferableDataOrError::Tnsresult) {
+ MOZ_ASSERT(NS_FAILED(aIpcTransferableDataOrError.get_nsresult()));
+ callback->OnComplete(aIpcTransferableDataOrError.get_nsresult());
+ return;
+ }
+
+ nsresult rv = nsContentUtils::IPCTransferableDataToTransferable(
+ aIpcTransferableDataOrError.get_IPCTransferableData(),
+ false /* aAddDataFlavor */, transferable,
+ false /* aFilterUnknownFlavors */);
+ if (NS_FAILED(rv)) {
+ callback->OnComplete(rv);
+ return;
+ }
+
+ callback->OnComplete(NS_OK);
+ },
+ /* reject */
+ [callback =
+ nsCOMPtr{aCallback}](mozilla::ipc::ResponseRejectReason aReason) {
+ callback->OnComplete(NS_ERROR_FAILURE);
+ });
+
+ return NS_OK;
+}
+
+} // namespace
+
+NS_IMETHODIMP nsClipboardProxy::AsyncGetData(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ mozilla::dom::WindowContext* aRequestingWindowContext,
+ nsIPrincipal* aRequestingPrincipal,
+ nsIAsyncClipboardGetCallback* aCallback) {
+ if (!aCallback || !aRequestingPrincipal || aFlavorList.IsEmpty()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
+ MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
+ aWhichClipboard);
+ return NS_ERROR_FAILURE;
+ }
+
+ ContentChild::GetSingleton()
+ ->SendGetClipboardAsync(aFlavorList, aWhichClipboard,
+ aRequestingWindowContext,
+ WrapNotNull(aRequestingPrincipal))
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ /* resolve */
+ [callback = nsCOMPtr{aCallback}](const PClipboardReadRequestOrError&
+ aClipboardReadRequestOrError) {
+ if (aClipboardReadRequestOrError.type() ==
+ PClipboardReadRequestOrError::Tnsresult) {
+ MOZ_ASSERT(
+ NS_FAILED(aClipboardReadRequestOrError.get_nsresult()));
+ callback->OnError(aClipboardReadRequestOrError.get_nsresult());
+ return;
+ }
+
+ auto asyncGetClipboardData = MakeRefPtr<AsyncGetClipboardDataProxy>(
+ static_cast<ClipboardReadRequestChild*>(
+ aClipboardReadRequestOrError.get_PClipboardReadRequest()
+ .AsChild()
+ .get()));
+
+ callback->OnSuccess(asyncGetClipboardData);
+ },
+ /* reject */
+ [callback = nsCOMPtr{aCallback}](
+ mozilla::ipc::ResponseRejectReason aReason) {
+ callback->OnError(NS_ERROR_FAILURE);
+ });
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsClipboardProxy::EmptyClipboard(int32_t aWhichClipboard) {
+ ContentChild::GetSingleton()->SendEmptyClipboard(aWhichClipboard);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsClipboardProxy::HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList,
+ int32_t aWhichClipboard,
+ bool* aHasType) {
+ *aHasType = false;
+
+ ContentChild::GetSingleton()->SendClipboardHasType(aFlavorList,
+ aWhichClipboard, aHasType);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsClipboardProxy::IsClipboardTypeSupported(int32_t aWhichClipboard,
+ bool* aIsSupported) {
+ switch (aWhichClipboard) {
+ case kGlobalClipboard:
+ // We always support the global clipboard.
+ *aIsSupported = true;
+ return NS_OK;
+ case kSelectionClipboard:
+ *aIsSupported = mClipboardCaps.supportsSelectionClipboard();
+ return NS_OK;
+ case kFindClipboard:
+ *aIsSupported = mClipboardCaps.supportsFindClipboard();
+ return NS_OK;
+ case kSelectionCache:
+ *aIsSupported = mClipboardCaps.supportsSelectionCache();
+ return NS_OK;
+ }
+
+ *aIsSupported = false;
+ return NS_OK;
+}
+
+void nsClipboardProxy::SetCapabilities(
+ const ClipboardCapabilities& aClipboardCaps) {
+ mClipboardCaps = aClipboardCaps;
+}
diff --git a/widget/nsClipboardProxy.h b/widget/nsClipboardProxy.h
new file mode 100644
index 0000000000..33b47d90bb
--- /dev/null
+++ b/widget/nsClipboardProxy.h
@@ -0,0 +1,48 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef NS_CLIPBOARD_PROXY_H
+#define NS_CLIPBOARD_PROXY_H
+
+#include "mozilla/dom/PContent.h"
+#include "nsIClipboard.h"
+
+#define NS_CLIPBOARDPROXY_IID \
+ { \
+ 0xa64c82da, 0x7326, 0x4681, { \
+ 0xa0, 0x95, 0x81, 0x2c, 0xc9, 0x86, 0xe6, 0xde \
+ } \
+ }
+
+// Hack for ContentChild to be able to know that we're an nsClipboardProxy.
+class nsIClipboardProxy : public nsIClipboard {
+ protected:
+ typedef mozilla::dom::ClipboardCapabilities ClipboardCapabilities;
+
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_CLIPBOARDPROXY_IID)
+
+ virtual void SetCapabilities(const ClipboardCapabilities& aClipboardCaps) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIClipboardProxy, NS_CLIPBOARDPROXY_IID)
+
+class nsClipboardProxy final : public nsIClipboardProxy {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICLIPBOARD
+
+ nsClipboardProxy();
+
+ virtual void SetCapabilities(
+ const ClipboardCapabilities& aClipboardCaps) override;
+
+ private:
+ ~nsClipboardProxy() = default;
+
+ ClipboardCapabilities mClipboardCaps;
+};
+
+#endif
diff --git a/widget/nsColorPickerProxy.cpp b/widget/nsColorPickerProxy.cpp
new file mode 100644
index 0000000000..0db2a7bd81
--- /dev/null
+++ b/widget/nsColorPickerProxy.cpp
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsColorPickerProxy.h"
+
+#include "mozilla/dom/BrowserChild.h"
+
+using namespace mozilla::dom;
+
+NS_IMPL_ISUPPORTS(nsColorPickerProxy, nsIColorPicker)
+
+NS_IMETHODIMP
+nsColorPickerProxy::Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ const nsAString& aInitialColor,
+ const nsTArray<nsString>& aDefaultColors) {
+ BrowserChild* browserChild = BrowserChild::GetFrom(aParent);
+ if (!browserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ browserChild->SendPColorPickerConstructor(this, aTitle, aInitialColor,
+ aDefaultColors);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsColorPickerProxy::Open(
+ nsIColorPickerShownCallback* aColorPickerShownCallback) {
+ NS_ENSURE_STATE(!mCallback);
+ mCallback = aColorPickerShownCallback;
+
+ SendOpen();
+ return NS_OK;
+}
+
+mozilla::ipc::IPCResult nsColorPickerProxy::RecvUpdate(
+ const nsAString& aColor) {
+ if (mCallback) {
+ mCallback->Update(aColor);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult nsColorPickerProxy::Recv__delete__(
+ const nsAString& aColor) {
+ if (mCallback) {
+ mCallback->Done(aColor);
+ mCallback = nullptr;
+ }
+ return IPC_OK();
+}
diff --git a/widget/nsColorPickerProxy.h b/widget/nsColorPickerProxy.h
new file mode 100644
index 0000000000..a06e69aafe
--- /dev/null
+++ b/widget/nsColorPickerProxy.h
@@ -0,0 +1,33 @@
+/* -*- 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/. */
+
+#ifndef nsColorPickerProxy_h
+#define nsColorPickerProxy_h
+
+#include "nsIColorPicker.h"
+
+#include "mozilla/dom/PColorPickerChild.h"
+
+class nsColorPickerProxy final : public nsIColorPicker,
+ public mozilla::dom::PColorPickerChild {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOLORPICKER
+
+ nsColorPickerProxy() = default;
+
+ virtual mozilla::ipc::IPCResult RecvUpdate(const nsAString& aColor) override;
+ virtual mozilla::ipc::IPCResult Recv__delete__(
+ const nsAString& aColor) override;
+
+ private:
+ ~nsColorPickerProxy() = default;
+
+ nsCOMPtr<nsIColorPickerShownCallback> mCallback;
+ nsString mTitle;
+ nsString mInitialColor;
+};
+
+#endif // nsColorPickerProxy_h
diff --git a/widget/nsContentProcessWidgetFactory.h b/widget/nsContentProcessWidgetFactory.h
new file mode 100644
index 0000000000..eb2d5a042d
--- /dev/null
+++ b/widget/nsContentProcessWidgetFactory.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef nsContentProcessWidgetFactory_h
+#define nsContentProcessWidgetFactory_h
+
+#include "nsISupports.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsXULAppAPI.h"
+
+#define MAKE_COMPONENT_CHOOSER(name_, parent_, content_, constructor_) \
+ static already_AddRefed<nsISupports> name_() { \
+ nsCOMPtr<nsISupports> inst; \
+ if (XRE_IsContentProcess()) { \
+ inst = constructor_(content_); \
+ } else { \
+ inst = constructor_(parent_); \
+ } \
+ return inst.forget(); \
+ }
+
+#ifdef MOZ_WIDGET_COCOA
+// This should be `do_GetService`, but test_bug466599.xhtml erroneously uses
+// `createInstance` rather than `getService`, and passes only because doing so
+// bypasses the clipboard contents cache, which does not have the expected
+// wrapping.
+MAKE_COMPONENT_CHOOSER(nsClipboardSelector,
+ "@mozilla.org/widget/parent/clipboard;1",
+ "@mozilla.org/widget/content/clipboard;1",
+ do_CreateInstance)
+#else
+MAKE_COMPONENT_CHOOSER(nsClipboardSelector,
+ "@mozilla.org/widget/parent/clipboard;1",
+ "@mozilla.org/widget/content/clipboard;1", do_GetService)
+#endif
+MAKE_COMPONENT_CHOOSER(nsColorPickerSelector,
+ "@mozilla.org/parent/colorpicker;1",
+ "@mozilla.org/content/colorpicker;1", do_CreateInstance)
+MAKE_COMPONENT_CHOOSER(nsFilePickerSelector, "@mozilla.org/parent/filepicker;1",
+ "@mozilla.org/content/filepicker;1", do_CreateInstance)
+MAKE_COMPONENT_CHOOSER(nsScreenManagerSelector,
+ "@mozilla.org/gfx/parent/screenmanager;1",
+ "@mozilla.org/gfx/content/screenmanager;1",
+ do_GetService)
+MAKE_COMPONENT_CHOOSER(nsDragServiceSelector,
+ "@mozilla.org/widget/parent/dragservice;1",
+ "@mozilla.org/widget/content/dragservice;1",
+ do_GetService)
+
+#undef MAKE_COMPONENT_CHOOSER
+
+#endif // defined nsContentProcessWidgetFactory_h
diff --git a/widget/nsDeviceContextSpecProxy.cpp b/widget/nsDeviceContextSpecProxy.cpp
new file mode 100644
index 0000000000..17a0664ede
--- /dev/null
+++ b/widget/nsDeviceContextSpecProxy.cpp
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "nsDeviceContextSpecProxy.h"
+
+#include "gfxASurface.h"
+#include "gfxPlatform.h"
+#include "mozilla/gfx/DrawEventRecorder.h"
+#include "mozilla/gfx/PrintTargetThebes.h"
+#include "mozilla/layout/RemotePrintJobChild.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIPrintSettings.h"
+#include "private/pprio.h"
+
+using mozilla::Unused;
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+NS_IMPL_ISUPPORTS(nsDeviceContextSpecProxy, nsIDeviceContextSpec)
+
+nsDeviceContextSpecProxy::nsDeviceContextSpecProxy(
+ RemotePrintJobChild* aRemotePrintJob)
+ : mRemotePrintJob(aRemotePrintJob) {}
+nsDeviceContextSpecProxy::~nsDeviceContextSpecProxy() = default;
+
+NS_IMETHODIMP
+nsDeviceContextSpecProxy::Init(nsIPrintSettings* aPrintSettings,
+ bool aIsPrintPreview) {
+ mPrintSettings = aPrintSettings;
+
+ if (aIsPrintPreview) {
+ return NS_OK;
+ }
+
+ if (!mRemotePrintJob) {
+ NS_WARNING("We can't print via the parent without a RemotePrintJobChild.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<PrintTarget> nsDeviceContextSpecProxy::MakePrintTarget() {
+ double width, height;
+ mPrintSettings->GetEffectiveSheetSize(&width, &height);
+ if (width <= 0 || height <= 0) {
+ return nullptr;
+ }
+
+ // convert twips to points
+ width /= TWIPS_PER_POINT_FLOAT;
+ height /= TWIPS_PER_POINT_FLOAT;
+
+ RefPtr<gfxASurface> surface =
+ gfxPlatform::GetPlatform()->CreateOffscreenSurface(
+ mozilla::gfx::IntSize::Ceil(width, height),
+ mozilla::gfx::SurfaceFormat::A8R8G8B8_UINT32);
+ if (!surface) {
+ return nullptr;
+ }
+
+ // The type of PrintTarget that we return here shouldn't really matter since
+ // our implementation of GetDrawEventRecorder returns an object, which means
+ // the DrawTarget returned by the PrintTarget will be a
+ // DrawTargetWrapAndRecord. The recording will be serialized and sent over to
+ // the parent process where PrintTranslator::TranslateRecording will call
+ // MakePrintTarget (indirectly via PrintTranslator::CreateDrawTarget) on
+ // whatever type of nsIDeviceContextSpecProxy is created for the platform that
+ // we are running on. It is that DrawTarget that the recording will be
+ // replayed on to print.
+ // XXX(jwatt): The above isn't quite true. We do want to use a
+ // PrintTargetRecording here, but we can't until bug 1280324 is figured out
+ // and fixed otherwise we will cause bug 1280181 to happen again.
+ RefPtr<PrintTarget> target = PrintTargetThebes::CreateOrNull(surface);
+
+ return target.forget();
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecProxy::GetDrawEventRecorder(
+ mozilla::gfx::DrawEventRecorder** aDrawEventRecorder) {
+ MOZ_ASSERT(aDrawEventRecorder);
+ RefPtr<mozilla::gfx::DrawEventRecorder> result = mRecorder;
+ result.forget(aDrawEventRecorder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecProxy::BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) {
+ if (!mRemotePrintJob || mRemotePrintJob->IsDestroyed()) {
+ mRemotePrintJob = nullptr;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mRecorder = new mozilla::layout::DrawEventRecorderPRFileDesc();
+ nsresult rv =
+ mRemotePrintJob->InitializePrint(nsString(aTitle), aStartPage, aEndPage);
+ if (NS_FAILED(rv)) {
+ // The parent process will send a 'delete' message to tell this process to
+ // delete our RemotePrintJobChild. As soon as we return to the event loop
+ // and evaluate that message we will crash if we try to access
+ // mRemotePrintJob. We must not try to use it again.
+ mRemotePrintJob = nullptr;
+ }
+ return rv;
+}
+
+RefPtr<mozilla::gfx::PrintEndDocumentPromise>
+nsDeviceContextSpecProxy::EndDocument() {
+ if (!mRemotePrintJob || mRemotePrintJob->IsDestroyed()) {
+ mRemotePrintJob = nullptr;
+ return mozilla::gfx::PrintEndDocumentPromise::CreateAndReject(
+ NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ Unused << mRemotePrintJob->SendFinalizePrint();
+
+ if (mRecorder) {
+ MOZ_ASSERT(!mRecorder->IsOpen());
+ mRecorder->DetachResources();
+ mRecorder = nullptr;
+ }
+
+ return mozilla::gfx::PrintEndDocumentPromise::CreateAndResolve(true,
+ __func__);
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecProxy::BeginPage(const IntSize& aSizeInPoints) {
+ if (!mRemotePrintJob || mRemotePrintJob->IsDestroyed()) {
+ mRemotePrintJob = nullptr;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mRecorder->OpenFD(mRemotePrintJob->GetNextPageFD());
+ mCurrentPageSizeInPoints = aSizeInPoints;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDeviceContextSpecProxy::EndPage() {
+ if (!mRemotePrintJob || mRemotePrintJob->IsDestroyed()) {
+ mRemotePrintJob = nullptr;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Send the page recording to the parent.
+ mRecorder->Close();
+ mRemotePrintJob->ProcessPage(mCurrentPageSizeInPoints,
+ std::move(mRecorder->TakeDependentSurfaces()));
+
+ return NS_OK;
+}
diff --git a/widget/nsDeviceContextSpecProxy.h b/widget/nsDeviceContextSpecProxy.h
new file mode 100644
index 0000000000..843811c34b
--- /dev/null
+++ b/widget/nsDeviceContextSpecProxy.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsDeviceContextSpecProxy_h
+#define nsDeviceContextSpecProxy_h
+
+#include "nsIDeviceContextSpec.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "mozilla/layout/printing/DrawEventRecorder.h"
+#include "mozilla/gfx/PrintPromise.h"
+
+class nsIFile;
+class nsIUUIDGenerator;
+
+namespace mozilla {
+namespace layout {
+class RemotePrintJobChild;
+}
+} // namespace mozilla
+
+class nsDeviceContextSpecProxy final : public nsIDeviceContextSpec {
+ public:
+ using IntSize = mozilla::gfx::IntSize;
+ using RemotePrintJobChild = mozilla::layout::RemotePrintJobChild;
+
+ explicit nsDeviceContextSpecProxy(RemotePrintJobChild* aRemotePrintJob);
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Init(nsIPrintSettings* aPrintSettings, bool aIsPrintPreview) final;
+
+ already_AddRefed<PrintTarget> MakePrintTarget() final;
+
+ NS_IMETHOD GetDrawEventRecorder(
+ mozilla::gfx::DrawEventRecorder** aDrawEventRecorder) final;
+
+ NS_IMETHOD BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) final;
+
+ RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() final;
+
+ NS_IMETHOD BeginPage(const IntSize& aSizeInPoints) final;
+
+ NS_IMETHOD EndPage() final;
+
+ private:
+ ~nsDeviceContextSpecProxy();
+
+ RefPtr<RemotePrintJobChild> mRemotePrintJob;
+ RefPtr<mozilla::layout::DrawEventRecorderPRFileDesc> mRecorder;
+ IntSize mCurrentPageSizeInPoints;
+};
+
+#endif // nsDeviceContextSpecProxy_h
diff --git a/widget/nsDragServiceProxy.cpp b/widget/nsDragServiceProxy.cpp
new file mode 100644
index 0000000000..cb273a89fa
--- /dev/null
+++ b/widget/nsDragServiceProxy.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsDragServiceProxy.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+
+using mozilla::CSSIntRegion;
+using mozilla::LayoutDeviceIntRect;
+using mozilla::Maybe;
+using mozilla::Nothing;
+using mozilla::Some;
+using mozilla::dom::BrowserChild;
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::SourceSurface;
+using mozilla::gfx::SurfaceFormat;
+using mozilla::ipc::Shmem;
+
+nsDragServiceProxy::nsDragServiceProxy() = default;
+
+nsDragServiceProxy::~nsDragServiceProxy() = default;
+
+nsresult nsDragServiceProxy::InvokeDragSessionImpl(
+ nsIArray* aArrayTransferables, const Maybe<CSSIntRegion>& aRegion,
+ uint32_t aActionType) {
+ NS_ENSURE_STATE(mSourceDocument->GetDocShell());
+ BrowserChild* child = BrowserChild::GetFrom(mSourceDocument->GetDocShell());
+ NS_ENSURE_STATE(child);
+ nsTArray<mozilla::dom::IPCTransferableData> transferables;
+ nsContentUtils::TransferablesToIPCTransferableDatas(
+ aArrayTransferables, transferables, false, nullptr);
+
+ nsCOMPtr<nsIPrincipal> principal;
+ if (mSourceNode) {
+ principal = mSourceNode->NodePrincipal();
+ }
+
+ nsCOMPtr<nsIContentSecurityPolicy> csp;
+ if (mSourceDocument) {
+ csp = mSourceDocument->GetCsp();
+ // XXX why do we need this here? Shouldn't they be set properly in
+ // nsBaseDragService already?
+ mSourceWindowContext = mSourceDocument->GetWindowContext();
+ mSourceTopWindowContext = mSourceWindowContext
+ ? mSourceWindowContext->TopWindowContext()
+ : nullptr;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ cookieJarSettings = mSourceDocument->CookieJarSettings();
+ mozilla::net::CookieJarSettingsArgs csArgs;
+ mozilla::net::CookieJarSettings::Cast(cookieJarSettings)->Serialize(csArgs);
+
+ LayoutDeviceIntRect dragRect;
+ if (mHasImage || mSelection) {
+ nsPresContext* pc;
+ RefPtr<SourceSurface> surface;
+ DrawDrag(mSourceNode, aRegion, mScreenPosition, &dragRect, &surface, &pc);
+
+ if (surface) {
+ RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
+ if (dataSurface) {
+ size_t length;
+ int32_t stride;
+ auto surfaceData =
+ nsContentUtils::GetSurfaceData(*dataSurface, &length, &stride);
+ if (surfaceData.isNothing()) {
+ NS_WARNING("Failed to create shared memory for drag session.");
+ return NS_ERROR_FAILURE;
+ }
+
+ mozilla::Unused << child->SendInvokeDragSession(
+ std::move(transferables), aActionType, std::move(surfaceData),
+ stride, dataSurface->GetFormat(), dragRect, principal, csp, csArgs,
+ mSourceWindowContext, mSourceTopWindowContext);
+ StartDragSession();
+ return NS_OK;
+ }
+ }
+ }
+
+ mozilla::Unused << child->SendInvokeDragSession(
+ std::move(transferables), aActionType, Nothing(), 0,
+ static_cast<SurfaceFormat>(0), dragRect, principal, csp, csArgs,
+ mSourceWindowContext, mSourceTopWindowContext);
+ StartDragSession();
+ return NS_OK;
+}
diff --git a/widget/nsDragServiceProxy.h b/widget/nsDragServiceProxy.h
new file mode 100644
index 0000000000..15aa6fa23e
--- /dev/null
+++ b/widget/nsDragServiceProxy.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+#ifndef NSDRAGSERVICEPROXY_H
+#define NSDRAGSERVICEPROXY_H
+
+#include "nsBaseDragService.h"
+
+class nsDragServiceProxy : public nsBaseDragService {
+ public:
+ nsDragServiceProxy();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsDragServiceProxy, nsBaseDragService)
+
+ // nsBaseDragService
+ virtual nsresult InvokeDragSessionImpl(
+ nsIArray* anArrayTransferables,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ uint32_t aActionType) override;
+
+ private:
+ virtual ~nsDragServiceProxy();
+};
+
+#endif // NSDRAGSERVICEPROXY_H
diff --git a/widget/nsFilePickerProxy.cpp b/widget/nsFilePickerProxy.cpp
new file mode 100644
index 0000000000..8777f338cb
--- /dev/null
+++ b/widget/nsFilePickerProxy.cpp
@@ -0,0 +1,296 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsFilePickerProxy.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIFile.h"
+#include "nsSimpleEnumerator.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+
+using namespace mozilla::dom;
+
+NS_IMPL_ISUPPORTS(nsFilePickerProxy, nsIFilePicker)
+
+nsFilePickerProxy::nsFilePickerProxy()
+ : mSelectedType(0), mCapture(captureNone), mIPCActive(false) {}
+
+nsFilePickerProxy::~nsFilePickerProxy() = default;
+
+NS_IMETHODIMP
+nsFilePickerProxy::Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ nsIFilePicker::Mode aMode,
+ BrowsingContext* aBrowsingContext) {
+ BrowserChild* browserChild = BrowserChild::GetFrom(aParent);
+ if (!browserChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mParent = nsPIDOMWindowOuter::From(aParent);
+
+ mMode = aMode;
+
+ browserChild->SendPFilePickerConstructor(this, aTitle, aMode);
+
+ mIPCActive = true;
+ return NS_OK;
+}
+
+void nsFilePickerProxy::InitNative(nsIWidget* aParent,
+ const nsAString& aTitle) {}
+
+NS_IMETHODIMP
+nsFilePickerProxy::AppendFilter(const nsAString& aTitle,
+ const nsAString& aFilter) {
+ mFilterNames.AppendElement(aTitle);
+ mFilters.AppendElement(aFilter);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetCapture(nsIFilePicker::CaptureTarget* aCapture) {
+ *aCapture = mCapture;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::SetCapture(nsIFilePicker::CaptureTarget aCapture) {
+ mCapture = aCapture;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetDefaultString(nsAString& aDefaultString) {
+ aDefaultString = mDefault;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::SetDefaultString(const nsAString& aDefaultString) {
+ mDefault = aDefaultString;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetDefaultExtension(nsAString& aDefaultExtension) {
+ aDefaultExtension = mDefaultExtension;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::SetDefaultExtension(const nsAString& aDefaultExtension) {
+ mDefaultExtension = aDefaultExtension;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetFilterIndex(int32_t* aFilterIndex) {
+ *aFilterIndex = mSelectedType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::SetFilterIndex(int32_t aFilterIndex) {
+ mSelectedType = aFilterIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetFile(nsIFile** aFile) {
+ MOZ_ASSERT(false, "GetFile is unimplemented; use GetDomFileOrDirectory");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetFileURL(nsIURI** aFileURL) {
+ MOZ_ASSERT(false, "GetFileURL is unimplemented; use GetDomFileOrDirectory");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetFiles(nsISimpleEnumerator** aFiles) {
+ MOZ_ASSERT(false,
+ "GetFiles is unimplemented; use GetDomFileOrDirectoryEnumerator");
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsFilePickerProxy::Show(nsIFilePicker::ResultCode* aReturn) {
+ MOZ_ASSERT(false, "Show is unimplemented; use Open");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::Open(nsIFilePickerShownCallback* aCallback) {
+ mCallback = aCallback;
+
+ nsString displayDirectory;
+ if (mDisplayDirectory) {
+ mDisplayDirectory->GetPath(displayDirectory);
+ }
+
+ if (!mIPCActive) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SendOpen(mSelectedType, mAddToRecentDocs, mDefault, mDefaultExtension,
+ mFilters, mFilterNames, mRawFilters, displayDirectory,
+ mDisplaySpecialDirectory, mOkButtonLabel, mCapture);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::Close() {
+ SendClose();
+
+ return NS_OK;
+}
+
+mozilla::ipc::IPCResult nsFilePickerProxy::Recv__delete__(
+ const MaybeInputData& aData, const nsIFilePicker::ResultCode& aResult) {
+ nsPIDOMWindowInner* inner =
+ mParent ? mParent->GetCurrentInnerWindow() : nullptr;
+
+ if (NS_WARN_IF(!inner)) {
+ return IPC_OK();
+ }
+
+ if (aData.type() == MaybeInputData::TInputBlobs) {
+ const nsTArray<IPCBlob>& blobs = aData.get_InputBlobs().blobs();
+ for (uint32_t i = 0; i < blobs.Length(); ++i) {
+ RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(blobs[i]);
+ NS_ENSURE_TRUE(blobImpl, IPC_OK());
+
+ if (!blobImpl->IsFile()) {
+ return IPC_OK();
+ }
+
+ RefPtr<File> file = File::Create(inner->AsGlobal(), blobImpl);
+ if (NS_WARN_IF(!file)) {
+ return IPC_OK();
+ }
+
+ OwningFileOrDirectory* element = mFilesOrDirectories.AppendElement();
+ element->SetAsFile() = file;
+ }
+ } else if (aData.type() == MaybeInputData::TInputDirectory) {
+ nsCOMPtr<nsIFile> file;
+ const nsAString& path(aData.get_InputDirectory().directoryPath());
+ nsresult rv = NS_NewLocalFile(path, true, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IPC_OK();
+ }
+
+ RefPtr<Directory> directory = Directory::Create(inner->AsGlobal(), file);
+ MOZ_ASSERT(directory);
+
+ OwningFileOrDirectory* element = mFilesOrDirectories.AppendElement();
+ element->SetAsDirectory() = directory;
+ }
+
+ if (mCallback) {
+ mCallback->Done(aResult);
+ mCallback = nullptr;
+ }
+
+ return IPC_OK();
+}
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetDomFileOrDirectory(nsISupports** aValue) {
+ *aValue = nullptr;
+ if (mFilesOrDirectories.IsEmpty()) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mFilesOrDirectories.Length() == 1);
+
+ if (mFilesOrDirectories[0].IsFile()) {
+ nsCOMPtr<nsISupports> blob = ToSupports(mFilesOrDirectories[0].GetAsFile());
+ blob.forget(aValue);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mFilesOrDirectories[0].IsDirectory());
+ RefPtr<Directory> directory = mFilesOrDirectories[0].GetAsDirectory();
+ directory.forget(aValue);
+ return NS_OK;
+}
+
+namespace {
+
+class SimpleEnumerator final : public nsSimpleEnumerator {
+ public:
+ explicit SimpleEnumerator(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories)
+ : mFilesOrDirectories(aFilesOrDirectories.Clone()), mIndex(0) {}
+
+ NS_IMETHOD
+ HasMoreElements(bool* aRetvalue) override {
+ MOZ_ASSERT(aRetvalue);
+ *aRetvalue = mIndex < mFilesOrDirectories.Length();
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ GetNext(nsISupports** aValue) override {
+ NS_ENSURE_TRUE(mIndex < mFilesOrDirectories.Length(), NS_ERROR_FAILURE);
+
+ uint32_t index = mIndex++;
+
+ if (mFilesOrDirectories[index].IsFile()) {
+ nsCOMPtr<nsISupports> blob =
+ ToSupports(mFilesOrDirectories[index].GetAsFile());
+ blob.forget(aValue);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mFilesOrDirectories[index].IsDirectory());
+ RefPtr<Directory> directory = mFilesOrDirectories[index].GetAsDirectory();
+ directory.forget(aValue);
+ return NS_OK;
+ }
+
+ private:
+ nsTArray<mozilla::dom::OwningFileOrDirectory> mFilesOrDirectories;
+ uint32_t mIndex;
+};
+
+} // namespace
+
+NS_IMETHODIMP
+nsFilePickerProxy::GetDomFileOrDirectoryEnumerator(
+ nsISimpleEnumerator** aDomfiles) {
+ RefPtr<SimpleEnumerator> enumerator =
+ new SimpleEnumerator(mFilesOrDirectories);
+ enumerator.forget(aDomfiles);
+ return NS_OK;
+}
+
+void nsFilePickerProxy::ActorDestroy(ActorDestroyReason aWhy) {
+ mIPCActive = false;
+
+ if (mCallback) {
+ mCallback->Done(nsIFilePicker::returnCancel);
+ mCallback = nullptr;
+ }
+}
+
+nsresult nsFilePickerProxy::ResolveSpecialDirectory(
+ const nsAString& aSpecialDirectory) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+ // Resolving the special-directory name to a path in both the child and parent
+ // processes is redundant -- and sandboxing may prevent us from doing so in
+ // the child process, anyway. (See bugs 1357846 and 1838244.)
+ //
+ // Unfortunately we can't easily verify that `aSpecialDirectory` is usable or
+ // even meaningful here, so we just accept anything.
+ return NS_OK;
+}
diff --git a/widget/nsFilePickerProxy.h b/widget/nsFilePickerProxy.h
new file mode 100644
index 0000000000..1b6aef13ed
--- /dev/null
+++ b/widget/nsFilePickerProxy.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+#ifndef NSFILEPICKERPROXY_H
+#define NSFILEPICKERPROXY_H
+
+#include "nsBaseFilePicker.h"
+#include "nsString.h"
+#include "nsIURI.h"
+#include "nsTArray.h"
+#include "nsCOMArray.h"
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/PFilePickerChild.h"
+#include "mozilla/dom/UnionTypes.h"
+
+class nsIWidget;
+class nsIFile;
+class nsPIDOMWindowInner;
+
+/**
+ This class creates a proxy file picker to be used in content processes.
+ The file picker just collects the initialization data and when Show() is
+ called, remotes everything to the chrome process which in turn can show a
+ platform specific file picker.
+*/
+class nsFilePickerProxy : public nsBaseFilePicker,
+ public mozilla::dom::PFilePickerChild {
+ public:
+ nsFilePickerProxy();
+
+ NS_DECL_ISUPPORTS
+
+ // nsIFilePicker (less what's in nsBaseFilePicker)
+ NS_IMETHOD Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ nsIFilePicker::Mode aMode,
+ mozilla::dom::BrowsingContext* aBrowsingContext) override;
+ NS_IMETHOD AppendFilter(const nsAString& aTitle,
+ const nsAString& aFilter) override;
+ NS_IMETHOD GetCapture(nsIFilePicker::CaptureTarget* aCapture) override;
+ NS_IMETHOD SetCapture(nsIFilePicker::CaptureTarget aCapture) override;
+ NS_IMETHOD GetDefaultString(nsAString& aDefaultString) override;
+ NS_IMETHOD SetDefaultString(const nsAString& aDefaultString) override;
+ NS_IMETHOD GetDefaultExtension(nsAString& aDefaultExtension) override;
+ NS_IMETHOD SetDefaultExtension(const nsAString& aDefaultExtension) override;
+ NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override;
+ NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override;
+ NS_IMETHOD GetFile(nsIFile** aFile) override;
+ NS_IMETHOD GetFileURL(nsIURI** aFileURL) override;
+ NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override;
+
+ NS_IMETHOD GetDomFileOrDirectory(nsISupports** aValue) override;
+ NS_IMETHOD GetDomFileOrDirectoryEnumerator(
+ nsISimpleEnumerator** aValue) override;
+
+ NS_IMETHOD Open(nsIFilePickerShownCallback* aCallback) override;
+ NS_IMETHOD Close() override;
+
+ // PFilePickerChild
+ virtual mozilla::ipc::IPCResult Recv__delete__(
+ const MaybeInputData& aData,
+ const nsIFilePicker::ResultCode& aResult) override;
+
+ private:
+ ~nsFilePickerProxy();
+ void InitNative(nsIWidget*, const nsAString&) override;
+ nsresult Show(nsIFilePicker::ResultCode* aReturn) override;
+ nsresult ResolveSpecialDirectory(const nsAString& aSpecialDirectory) override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ nsTArray<mozilla::dom::OwningFileOrDirectory> mFilesOrDirectories;
+ nsCOMPtr<nsIFilePickerShownCallback> mCallback;
+
+ int16_t mSelectedType;
+ nsString mFile;
+ nsString mDefault;
+ nsString mDefaultExtension;
+ nsIFilePicker::CaptureTarget mCapture;
+
+ bool mIPCActive;
+
+ nsTArray<nsString> mFilters;
+ nsTArray<nsString> mFilterNames;
+};
+
+#endif // NSFILEPICKERPROXY_H
diff --git a/widget/nsGUIEventIPC.h b/widget/nsGUIEventIPC.h
new file mode 100644
index 0000000000..8ba4682935
--- /dev/null
+++ b/widget/nsGUIEventIPC.h
@@ -0,0 +1,1406 @@
+/* -*- 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/. */
+
+#ifndef nsGUIEventIPC_h__
+#define nsGUIEventIPC_h__
+
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/ContentCache.h"
+#include "mozilla/GfxMessageUtils.h"
+#include "mozilla/dom/Touch.h"
+#include "mozilla/ipc/IPDLParamTraits.h" // for ReadIPDLParam and WriteIPDLParam
+#include "mozilla/ipc/URIUtils.h" // for IPDLParamTraits<nsIURI*>
+#include "mozilla/layers/LayersMessageUtils.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaAdjustmentStrategy
+#include "mozilla/dom/Selection.h"
+#include "InputData.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::EventMessage>
+ : public ContiguousEnumSerializer<
+ mozilla::EventMessage, mozilla::EventMessage(0),
+ mozilla::EventMessage::eEventMessage_MaxValue> {};
+
+template <>
+struct ParamTraits<mozilla::BaseEventFlags> {
+ using paramType = mozilla::BaseEventFlags;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ aWriter->WriteBytes(&aParam, sizeof(aParam));
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return aReader->ReadBytesInto(aResult, sizeof(*aResult));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetEvent> {
+ using paramType = mozilla::WidgetEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ // Mark the event as posted to another process.
+ const_cast<mozilla::WidgetEvent&>(aParam).MarkAsPostedToRemoteProcess();
+
+ WriteParam(aWriter, static_cast<mozilla::EventClassIDType>(aParam.mClass));
+ WriteParam(aWriter, aParam.mMessage);
+ WriteParam(aWriter, aParam.mRefPoint);
+ WriteParam(aWriter, aParam.mFocusSequenceNumber);
+ WriteParam(aWriter, aParam.mTimeStamp);
+ WriteParam(aWriter, aParam.mFlags);
+ WriteParam(aWriter, aParam.mLayersId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ mozilla::EventClassIDType eventClassID = 0;
+ bool ret = ReadParam(aReader, &eventClassID) &&
+ ReadParam(aReader, &aResult->mMessage) &&
+ ReadParam(aReader, &aResult->mRefPoint) &&
+ ReadParam(aReader, &aResult->mFocusSequenceNumber) &&
+ ReadParam(aReader, &aResult->mTimeStamp) &&
+ ReadParam(aReader, &aResult->mFlags) &&
+ ReadParam(aReader, &aResult->mLayersId);
+ aResult->mClass = static_cast<mozilla::EventClassID>(eventClassID);
+ if (ret) {
+ // Reset cross process dispatching state here because the event has not
+ // been dispatched to different process from current process.
+ aResult->ResetCrossProcessDispatchingState();
+ // Mark the event comes from another process.
+ aResult->MarkAsComingFromAnotherProcess();
+ }
+ return ret;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetGUIEvent> {
+ using paramType = mozilla::WidgetGUIEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetEvent&>(aParam));
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::WidgetEvent*>(aResult));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetInputEvent> {
+ using paramType = mozilla::WidgetInputEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetGUIEvent&>(aParam));
+ WriteParam(aWriter, aParam.mModifiers);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::WidgetGUIEvent*>(aResult)) &&
+ ReadParam(aReader, &aResult->mModifiers);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetMouseEventBase> {
+ using paramType = mozilla::WidgetMouseEventBase;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetInputEvent&>(aParam));
+ WriteParam(aWriter, aParam.mButton);
+ WriteParam(aWriter, aParam.mButtons);
+ WriteParam(aWriter, aParam.mPressure);
+ WriteParam(aWriter, aParam.mInputSource);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader,
+ static_cast<mozilla::WidgetInputEvent*>(aResult)) &&
+ ReadParam(aReader, &aResult->mButton) &&
+ ReadParam(aReader, &aResult->mButtons) &&
+ ReadParam(aReader, &aResult->mPressure) &&
+ ReadParam(aReader, &aResult->mInputSource);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetWheelEvent> {
+ using paramType = mozilla::WidgetWheelEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter,
+ static_cast<const mozilla::WidgetMouseEventBase&>(aParam));
+ WriteParam(aWriter, aParam.mDeltaX);
+ WriteParam(aWriter, aParam.mDeltaY);
+ WriteParam(aWriter, aParam.mDeltaZ);
+ WriteParam(aWriter, aParam.mDeltaMode);
+ WriteParam(aWriter, aParam.mWheelTicksX);
+ WriteParam(aWriter, aParam.mWheelTicksY);
+ WriteParam(aWriter, aParam.mCustomizedByUserPrefs);
+ WriteParam(aWriter, aParam.mMayHaveMomentum);
+ WriteParam(aWriter, aParam.mIsMomentum);
+ WriteParam(aWriter, aParam.mIsNoLineOrPageDelta);
+ WriteParam(aWriter, aParam.mLineOrPageDeltaX);
+ WriteParam(aWriter, aParam.mLineOrPageDeltaY);
+ WriteParam(aWriter, static_cast<uint8_t>(aParam.mScrollType));
+ WriteParam(aWriter, aParam.mOverflowDeltaX);
+ WriteParam(aWriter, aParam.mOverflowDeltaY);
+ WriteParam(aWriter, aParam.mViewPortIsOverscrolled);
+ WriteParam(aWriter, aParam.mCanTriggerSwipe);
+ WriteParam(aWriter, aParam.mAllowToOverrideSystemScrollSpeed);
+ WriteParam(aWriter, aParam.mDeltaValuesHorizontalizedForDefaultHandler);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ uint8_t scrollType = 0;
+ bool rv = ReadParam(aReader,
+ static_cast<mozilla::WidgetMouseEventBase*>(aResult)) &&
+ ReadParam(aReader, &aResult->mDeltaX) &&
+ ReadParam(aReader, &aResult->mDeltaY) &&
+ ReadParam(aReader, &aResult->mDeltaZ) &&
+ ReadParam(aReader, &aResult->mDeltaMode) &&
+ ReadParam(aReader, &aResult->mWheelTicksX) &&
+ ReadParam(aReader, &aResult->mWheelTicksY) &&
+ ReadParam(aReader, &aResult->mCustomizedByUserPrefs) &&
+ ReadParam(aReader, &aResult->mMayHaveMomentum) &&
+ ReadParam(aReader, &aResult->mIsMomentum) &&
+ ReadParam(aReader, &aResult->mIsNoLineOrPageDelta) &&
+ ReadParam(aReader, &aResult->mLineOrPageDeltaX) &&
+ ReadParam(aReader, &aResult->mLineOrPageDeltaY) &&
+ ReadParam(aReader, &scrollType) &&
+ ReadParam(aReader, &aResult->mOverflowDeltaX) &&
+ ReadParam(aReader, &aResult->mOverflowDeltaY) &&
+ ReadParam(aReader, &aResult->mViewPortIsOverscrolled) &&
+ ReadParam(aReader, &aResult->mCanTriggerSwipe) &&
+ ReadParam(aReader, &aResult->mAllowToOverrideSystemScrollSpeed) &&
+ ReadParam(aReader,
+ &aResult->mDeltaValuesHorizontalizedForDefaultHandler);
+ aResult->mScrollType =
+ static_cast<mozilla::WidgetWheelEvent::ScrollType>(scrollType);
+ return rv;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetPointerHelper> {
+ using paramType = mozilla::WidgetPointerHelper;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.pointerId);
+ WriteParam(aWriter, aParam.tiltX);
+ WriteParam(aWriter, aParam.tiltY);
+ WriteParam(aWriter, aParam.twist);
+ WriteParam(aWriter, aParam.tangentialPressure);
+ // We don't serialize convertToPointer since it's temporarily variable and
+ // should be reset to default.
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ bool rv;
+ rv = ReadParam(aReader, &aResult->pointerId) &&
+ ReadParam(aReader, &aResult->tiltX) &&
+ ReadParam(aReader, &aResult->tiltY) &&
+ ReadParam(aReader, &aResult->twist) &&
+ ReadParam(aReader, &aResult->tangentialPressure);
+ return rv;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetMouseEvent> {
+ using paramType = mozilla::WidgetMouseEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter,
+ static_cast<const mozilla::WidgetMouseEventBase&>(aParam));
+ WriteParam(aWriter,
+ static_cast<const mozilla::WidgetPointerHelper&>(aParam));
+ WriteParam(aWriter, aParam.mIgnoreRootScrollFrame);
+ WriteParam(aWriter, aParam.mClickEventPrevented);
+ WriteParam(aWriter, static_cast<paramType::ReasonType>(aParam.mReason));
+ WriteParam(aWriter, static_cast<paramType::ContextMenuTriggerType>(
+ aParam.mContextMenuTrigger));
+ WriteParam(aWriter, aParam.mExitFrom.isSome());
+ if (aParam.mExitFrom.isSome()) {
+ WriteParam(aWriter, static_cast<paramType::ExitFromType>(
+ aParam.mExitFrom.value()));
+ }
+ WriteParam(aWriter, aParam.mClickCount);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ bool rv;
+ paramType::ReasonType reason = 0;
+ paramType::ContextMenuTriggerType contextMenuTrigger = 0;
+ bool hasExitFrom = false;
+ rv = ReadParam(aReader,
+ static_cast<mozilla::WidgetMouseEventBase*>(aResult)) &&
+ ReadParam(aReader,
+ static_cast<mozilla::WidgetPointerHelper*>(aResult)) &&
+ ReadParam(aReader, &aResult->mIgnoreRootScrollFrame) &&
+ ReadParam(aReader, &aResult->mClickEventPrevented) &&
+ ReadParam(aReader, &reason) && ReadParam(aReader, &contextMenuTrigger);
+ aResult->mReason = static_cast<paramType::Reason>(reason);
+ aResult->mContextMenuTrigger =
+ static_cast<paramType::ContextMenuTrigger>(contextMenuTrigger);
+ rv = rv && ReadParam(aReader, &hasExitFrom);
+ if (hasExitFrom) {
+ paramType::ExitFromType exitFrom = 0;
+ rv = rv && ReadParam(aReader, &exitFrom);
+ aResult->mExitFrom = Some(static_cast<paramType::ExitFrom>(exitFrom));
+ }
+ rv = rv && ReadParam(aReader, &aResult->mClickCount);
+ return rv;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetDragEvent> {
+ using paramType = mozilla::WidgetDragEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetMouseEvent&>(aParam));
+ WriteParam(aWriter, aParam.mUserCancelled);
+ WriteParam(aWriter, aParam.mDefaultPreventedOnContent);
+ WriteParam(aWriter, aParam.mInHTMLEditorEventListener);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ bool rv =
+ ReadParam(aReader, static_cast<mozilla::WidgetMouseEvent*>(aResult)) &&
+ ReadParam(aReader, &aResult->mUserCancelled) &&
+ ReadParam(aReader, &aResult->mDefaultPreventedOnContent) &&
+ ReadParam(aReader, &aResult->mInHTMLEditorEventListener);
+ return rv;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetPointerEvent> {
+ using paramType = mozilla::WidgetPointerEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetMouseEvent&>(aParam));
+ WriteParam(aWriter, aParam.mWidth);
+ WriteParam(aWriter, aParam.mHeight);
+ WriteParam(aWriter, aParam.mIsPrimary);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ bool rv =
+ ReadParam(aReader, static_cast<mozilla::WidgetMouseEvent*>(aResult)) &&
+ ReadParam(aReader, &aResult->mWidth) &&
+ ReadParam(aReader, &aResult->mHeight) &&
+ ReadParam(aReader, &aResult->mIsPrimary);
+ return rv;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetTouchEvent> {
+ using paramType = mozilla::WidgetTouchEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetInputEvent&>(aParam));
+ WriteParam(aWriter, aParam.mInputSource);
+ WriteParam(aWriter, aParam.mButton);
+ WriteParam(aWriter, aParam.mButtons);
+ // Sigh, Touch bites us again! We want to be able to do
+ // WriteParam(aWriter, aParam.mTouches);
+ const paramType::TouchArray& touches = aParam.mTouches;
+ WriteParam(aWriter, touches.Length());
+ for (uint32_t i = 0; i < touches.Length(); ++i) {
+ mozilla::dom::Touch* touch = touches[i];
+ WriteParam(aWriter, touch->mIdentifier);
+ WriteParam(aWriter, touch->mRefPoint);
+ WriteParam(aWriter, touch->mRadius);
+ WriteParam(aWriter, touch->mRotationAngle);
+ WriteParam(aWriter, touch->mForce);
+ WriteParam(aWriter, touch->tiltX);
+ WriteParam(aWriter, touch->tiltY);
+ WriteParam(aWriter, touch->twist);
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ paramType::TouchArray::size_type numTouches;
+ if (!ReadParam(aReader, static_cast<mozilla::WidgetInputEvent*>(aResult)) ||
+ !ReadParam(aReader, &aResult->mInputSource) ||
+ !ReadParam(aReader, &aResult->mButton) ||
+ !ReadParam(aReader, &aResult->mButtons) ||
+ !ReadParam(aReader, &numTouches)) {
+ return false;
+ }
+ for (uint32_t i = 0; i < numTouches; ++i) {
+ int32_t identifier;
+ mozilla::LayoutDeviceIntPoint refPoint;
+ mozilla::LayoutDeviceIntPoint radius;
+ float rotationAngle;
+ float force;
+ uint32_t tiltX;
+ uint32_t tiltY;
+ uint32_t twist;
+ if (!ReadParam(aReader, &identifier) || !ReadParam(aReader, &refPoint) ||
+ !ReadParam(aReader, &radius) || !ReadParam(aReader, &rotationAngle) ||
+ !ReadParam(aReader, &force) || !ReadParam(aReader, &tiltX) ||
+ !ReadParam(aReader, &tiltY) || !ReadParam(aReader, &twist)) {
+ return false;
+ }
+ auto* touch = new mozilla::dom::Touch(identifier, refPoint, radius,
+ rotationAngle, force);
+ touch->tiltX = tiltX;
+ touch->tiltY = tiltY;
+ touch->twist = twist;
+ aResult->mTouches.AppendElement(touch);
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::AlternativeCharCode> {
+ using paramType = mozilla::AlternativeCharCode;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mUnshiftedCharCode);
+ WriteParam(aWriter, aParam.mShiftedCharCode);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mUnshiftedCharCode) &&
+ ReadParam(aReader, &aResult->mShiftedCharCode);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::ShortcutKeyCandidate::ShiftState>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ShortcutKeyCandidate::ShiftState,
+ mozilla::ShortcutKeyCandidate::ShiftState::Ignorable,
+ mozilla::ShortcutKeyCandidate::ShiftState::MatchExactly> {};
+
+template <>
+struct ParamTraits<mozilla::ShortcutKeyCandidate::SkipIfEarlierHandlerDisabled>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ShortcutKeyCandidate::SkipIfEarlierHandlerDisabled,
+ mozilla::ShortcutKeyCandidate::SkipIfEarlierHandlerDisabled::No,
+ mozilla::ShortcutKeyCandidate::SkipIfEarlierHandlerDisabled::Yes> {};
+
+template <>
+struct ParamTraits<mozilla::ShortcutKeyCandidate> {
+ using paramType = mozilla::ShortcutKeyCandidate;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mCharCode);
+ WriteParam(aWriter, aParam.mShiftState);
+ WriteParam(aWriter, aParam.mSkipIfEarlierHandlerDisabled);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mCharCode) &&
+ ReadParam(aReader, &aResult->mShiftState) &&
+ ReadParam(aReader, &aResult->mSkipIfEarlierHandlerDisabled);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetKeyboardEvent> {
+ using paramType = mozilla::WidgetKeyboardEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetInputEvent&>(aParam));
+ WriteParam(aWriter,
+ static_cast<mozilla::KeyNameIndexType>(aParam.mKeyNameIndex));
+ WriteParam(aWriter,
+ static_cast<mozilla::CodeNameIndexType>(aParam.mCodeNameIndex));
+ WriteParam(aWriter, aParam.mKeyValue);
+ WriteParam(aWriter, aParam.mCodeValue);
+ WriteParam(aWriter, aParam.mKeyCode);
+ WriteParam(aWriter, aParam.mCharCode);
+ WriteParam(aWriter, aParam.mPseudoCharCode);
+ WriteParam(aWriter, aParam.mAlternativeCharCodes);
+ WriteParam(aWriter, aParam.mIsRepeat);
+ WriteParam(aWriter, aParam.mLocation);
+ WriteParam(aWriter, aParam.mUniqueId);
+ WriteParam(aWriter, aParam.mIsSynthesizedByTIP);
+ WriteParam(aWriter, aParam.mMaybeSkippableInRemoteProcess);
+
+ // An OS-specific native event might be attached in |mNativeKeyEvent|, but
+ // that cannot be copied across process boundaries.
+
+ WriteParam(aWriter, aParam.mEditCommandsForSingleLineEditor);
+ WriteParam(aWriter, aParam.mEditCommandsForMultiLineEditor);
+ WriteParam(aWriter, aParam.mEditCommandsForRichTextEditor);
+ WriteParam(aWriter, aParam.mEditCommandsForSingleLineEditorInitialized);
+ WriteParam(aWriter, aParam.mEditCommandsForMultiLineEditorInitialized);
+ WriteParam(aWriter, aParam.mEditCommandsForRichTextEditorInitialized);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ mozilla::KeyNameIndexType keyNameIndex = 0;
+ mozilla::CodeNameIndexType codeNameIndex = 0;
+ if (ReadParam(aReader, static_cast<mozilla::WidgetInputEvent*>(aResult)) &&
+ ReadParam(aReader, &keyNameIndex) &&
+ ReadParam(aReader, &codeNameIndex) &&
+ ReadParam(aReader, &aResult->mKeyValue) &&
+ ReadParam(aReader, &aResult->mCodeValue) &&
+ ReadParam(aReader, &aResult->mKeyCode) &&
+ ReadParam(aReader, &aResult->mCharCode) &&
+ ReadParam(aReader, &aResult->mPseudoCharCode) &&
+ ReadParam(aReader, &aResult->mAlternativeCharCodes) &&
+ ReadParam(aReader, &aResult->mIsRepeat) &&
+ ReadParam(aReader, &aResult->mLocation) &&
+ ReadParam(aReader, &aResult->mUniqueId) &&
+ ReadParam(aReader, &aResult->mIsSynthesizedByTIP) &&
+ ReadParam(aReader, &aResult->mMaybeSkippableInRemoteProcess) &&
+ ReadParam(aReader, &aResult->mEditCommandsForSingleLineEditor) &&
+ ReadParam(aReader, &aResult->mEditCommandsForMultiLineEditor) &&
+ ReadParam(aReader, &aResult->mEditCommandsForRichTextEditor) &&
+ ReadParam(aReader,
+ &aResult->mEditCommandsForSingleLineEditorInitialized) &&
+ ReadParam(aReader,
+ &aResult->mEditCommandsForMultiLineEditorInitialized) &&
+ ReadParam(aReader,
+ &aResult->mEditCommandsForRichTextEditorInitialized)) {
+ aResult->mKeyNameIndex = static_cast<mozilla::KeyNameIndex>(keyNameIndex);
+ aResult->mCodeNameIndex =
+ static_cast<mozilla::CodeNameIndex>(codeNameIndex);
+ aResult->mNativeKeyEvent = nullptr;
+ return true;
+ }
+ return false;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::TextRangeStyle> {
+ using paramType = mozilla::TextRangeStyle;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mDefinedStyles);
+ WriteParam(aWriter, static_cast<mozilla::TextRangeStyle::LineStyleType>(
+ aParam.mLineStyle));
+ WriteParam(aWriter, aParam.mIsBoldLine);
+ WriteParam(aWriter, aParam.mForegroundColor);
+ WriteParam(aWriter, aParam.mBackgroundColor);
+ WriteParam(aWriter, aParam.mUnderlineColor);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ mozilla::TextRangeStyle::LineStyleType lineStyle;
+ if (!ReadParam(aReader, &aResult->mDefinedStyles) ||
+ !ReadParam(aReader, &lineStyle) ||
+ !ReadParam(aReader, &aResult->mIsBoldLine) ||
+ !ReadParam(aReader, &aResult->mForegroundColor) ||
+ !ReadParam(aReader, &aResult->mBackgroundColor) ||
+ !ReadParam(aReader, &aResult->mUnderlineColor)) {
+ return false;
+ }
+ aResult->mLineStyle = mozilla::TextRangeStyle::ToLineStyle(lineStyle);
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::TextRange> {
+ using paramType = mozilla::TextRange;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mStartOffset);
+ WriteParam(aWriter, aParam.mEndOffset);
+ WriteParam(aWriter, mozilla::ToRawTextRangeType(aParam.mRangeType));
+ WriteParam(aWriter, aParam.mRangeStyle);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ mozilla::RawTextRangeType rawTextRangeType;
+ if (ReadParam(aReader, &aResult->mStartOffset) &&
+ ReadParam(aReader, &aResult->mEndOffset) &&
+ ReadParam(aReader, &rawTextRangeType) &&
+ ReadParam(aReader, &aResult->mRangeStyle)) {
+ aResult->mRangeType = mozilla::ToTextRangeType(rawTextRangeType);
+ return true;
+ }
+ return false;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::TextRangeArray> {
+ using paramType = mozilla::TextRangeArray;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.Length());
+ for (uint32_t index = 0; index < aParam.Length(); index++) {
+ WriteParam(aWriter, aParam[index]);
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ paramType::size_type length;
+ if (!ReadParam(aReader, &length)) {
+ return false;
+ }
+ for (uint32_t index = 0; index < length; index++) {
+ mozilla::TextRange textRange;
+ if (!ReadParam(aReader, &textRange)) {
+ aResult->Clear();
+ return false;
+ }
+ aResult->AppendElement(textRange);
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetCompositionEvent> {
+ using paramType = mozilla::WidgetCompositionEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetGUIEvent&>(aParam));
+ WriteParam(aWriter, aParam.mData);
+ WriteParam(aWriter, aParam.mNativeIMEContext);
+ WriteParam(aWriter, aParam.mCompositionId);
+ bool hasRanges = !!aParam.mRanges;
+ WriteParam(aWriter, hasRanges);
+ if (hasRanges) {
+ WriteParam(aWriter, *aParam.mRanges.get());
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ bool hasRanges;
+ if (!ReadParam(aReader, static_cast<mozilla::WidgetGUIEvent*>(aResult)) ||
+ !ReadParam(aReader, &aResult->mData) ||
+ !ReadParam(aReader, &aResult->mNativeIMEContext) ||
+ !ReadParam(aReader, &aResult->mCompositionId) ||
+ !ReadParam(aReader, &hasRanges)) {
+ return false;
+ }
+
+ if (!hasRanges) {
+ aResult->mRanges = nullptr;
+ } else {
+ aResult->mRanges = new mozilla::TextRangeArray();
+ if (!ReadParam(aReader, aResult->mRanges.get())) {
+ return false;
+ }
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::FontRange> {
+ using paramType = mozilla::FontRange;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mStartOffset);
+ WriteParam(aWriter, aParam.mFontName);
+ WriteParam(aWriter, aParam.mFontSize);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mStartOffset) &&
+ ReadParam(aReader, &aResult->mFontName) &&
+ ReadParam(aReader, &aResult->mFontSize);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WidgetSelectionEvent> {
+ using paramType = mozilla::WidgetSelectionEvent;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::WidgetGUIEvent&>(aParam));
+ WriteParam(aWriter, aParam.mOffset);
+ WriteParam(aWriter, aParam.mLength);
+ WriteParam(aWriter, aParam.mReversed);
+ WriteParam(aWriter, aParam.mExpandToClusterBoundary);
+ WriteParam(aWriter, aParam.mSucceeded);
+ WriteParam(aWriter, aParam.mUseNativeLineBreak);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::WidgetGUIEvent*>(aResult)) &&
+ ReadParam(aReader, &aResult->mOffset) &&
+ ReadParam(aReader, &aResult->mLength) &&
+ ReadParam(aReader, &aResult->mReversed) &&
+ ReadParam(aReader, &aResult->mExpandToClusterBoundary) &&
+ ReadParam(aReader, &aResult->mSucceeded) &&
+ ReadParam(aReader, &aResult->mUseNativeLineBreak);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::IMENotificationRequests> {
+ using paramType = mozilla::widget::IMENotificationRequests;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mWantUpdates);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mWantUpdates);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::NativeIMEContext> {
+ using paramType = mozilla::widget::NativeIMEContext;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mRawNativeIMEContext);
+ WriteParam(aWriter, aParam.mOriginProcessID);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mRawNativeIMEContext) &&
+ ReadParam(aReader, &aResult->mOriginProcessID);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::IMENotification::SelectionChangeDataBase> {
+ using paramType = mozilla::widget::IMENotification::SelectionChangeDataBase;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ MOZ_RELEASE_ASSERT(aParam.mString);
+ WriteParam(aWriter, aParam.mOffset);
+ WriteParam(aWriter, *aParam.mString);
+ WriteParam(aWriter, aParam.mWritingModeBits);
+ WriteParam(aWriter, aParam.mIsInitialized);
+ WriteParam(aWriter, aParam.mHasRange);
+ WriteParam(aWriter, aParam.mReversed);
+ WriteParam(aWriter, aParam.mCausedByComposition);
+ WriteParam(aWriter, aParam.mCausedBySelectionEvent);
+ WriteParam(aWriter, aParam.mOccurredDuringComposition);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ aResult->mString = new nsString();
+ return ReadParam(aReader, &aResult->mOffset) &&
+ ReadParam(aReader, aResult->mString) &&
+ ReadParam(aReader, &aResult->mWritingModeBits) &&
+ ReadParam(aReader, &aResult->mIsInitialized) &&
+ ReadParam(aReader, &aResult->mHasRange) &&
+ ReadParam(aReader, &aResult->mReversed) &&
+ ReadParam(aReader, &aResult->mCausedByComposition) &&
+ ReadParam(aReader, &aResult->mCausedBySelectionEvent) &&
+ ReadParam(aReader, &aResult->mOccurredDuringComposition);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::IMENotification::TextChangeDataBase> {
+ using paramType = mozilla::widget::IMENotification::TextChangeDataBase;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mStartOffset);
+ WriteParam(aWriter, aParam.mRemovedEndOffset);
+ WriteParam(aWriter, aParam.mAddedEndOffset);
+ WriteParam(aWriter, aParam.mCausedOnlyByComposition);
+ WriteParam(aWriter, aParam.mIncludingChangesDuringComposition);
+ WriteParam(aWriter, aParam.mIncludingChangesWithoutComposition);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mStartOffset) &&
+ ReadParam(aReader, &aResult->mRemovedEndOffset) &&
+ ReadParam(aReader, &aResult->mAddedEndOffset) &&
+ ReadParam(aReader, &aResult->mCausedOnlyByComposition) &&
+ ReadParam(aReader, &aResult->mIncludingChangesDuringComposition) &&
+ ReadParam(aReader, &aResult->mIncludingChangesWithoutComposition);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::IMENotification::MouseButtonEventData> {
+ using paramType = mozilla::widget::IMENotification::MouseButtonEventData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mEventMessage);
+ WriteParam(aWriter, aParam.mOffset);
+ WriteParam(aWriter, aParam.mCursorPos);
+ WriteParam(aWriter, aParam.mCharRect);
+ WriteParam(aWriter, aParam.mButton);
+ WriteParam(aWriter, aParam.mButtons);
+ WriteParam(aWriter, aParam.mModifiers);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mEventMessage) &&
+ ReadParam(aReader, &aResult->mOffset) &&
+ ReadParam(aReader, &aResult->mCursorPos) &&
+ ReadParam(aReader, &aResult->mCharRect) &&
+ ReadParam(aReader, &aResult->mButton) &&
+ ReadParam(aReader, &aResult->mButtons) &&
+ ReadParam(aReader, &aResult->mModifiers);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::IMENotification> {
+ using paramType = mozilla::widget::IMENotification;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter,
+ static_cast<mozilla::widget::IMEMessageType>(aParam.mMessage));
+ switch (aParam.mMessage) {
+ case mozilla::widget::NOTIFY_IME_OF_SELECTION_CHANGE:
+ WriteParam(aWriter, aParam.mSelectionChangeData);
+ return;
+ case mozilla::widget::NOTIFY_IME_OF_TEXT_CHANGE:
+ WriteParam(aWriter, aParam.mTextChangeData);
+ return;
+ case mozilla::widget::NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ WriteParam(aWriter, aParam.mMouseButtonEventData);
+ return;
+ default:
+ return;
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ mozilla::widget::IMEMessageType IMEMessage = 0;
+ if (!ReadParam(aReader, &IMEMessage)) {
+ return false;
+ }
+ aResult->mMessage = static_cast<mozilla::widget::IMEMessage>(IMEMessage);
+ switch (aResult->mMessage) {
+ case mozilla::widget::NOTIFY_IME_OF_SELECTION_CHANGE:
+ return ReadParam(aReader, &aResult->mSelectionChangeData);
+ case mozilla::widget::NOTIFY_IME_OF_TEXT_CHANGE:
+ return ReadParam(aReader, &aResult->mTextChangeData);
+ case mozilla::widget::NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ return ReadParam(aReader, &aResult->mMouseButtonEventData);
+ default:
+ return true;
+ }
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::IMEEnabled>
+ : ContiguousEnumSerializer<mozilla::widget::IMEEnabled,
+ mozilla::widget::IMEEnabled::Disabled,
+ mozilla::widget::IMEEnabled::Unknown> {};
+
+template <>
+struct ParamTraits<mozilla::widget::IMEState::Open>
+ : ContiguousEnumSerializerInclusive<
+ mozilla::widget::IMEState::Open,
+ mozilla::widget::IMEState::Open::OPEN_STATE_NOT_SUPPORTED,
+ mozilla::widget::IMEState::Open::CLOSED> {};
+
+template <>
+struct ParamTraits<mozilla::widget::IMEState> {
+ using paramType = mozilla::widget::IMEState;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mEnabled);
+ WriteParam(aWriter, aParam.mOpen);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mEnabled) &&
+ ReadParam(aReader, &aResult->mOpen);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::InputContext::Origin>
+ : ContiguousEnumSerializerInclusive<
+ mozilla::widget::InputContext::Origin,
+ mozilla::widget::InputContext::Origin::ORIGIN_MAIN,
+ mozilla::widget::InputContext::Origin::ORIGIN_CONTENT> {};
+
+template <>
+struct ParamTraits<mozilla::widget::InputContext> {
+ using paramType = mozilla::widget::InputContext;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mIMEState);
+ WriteParam(aWriter, aParam.mHTMLInputType);
+ WriteParam(aWriter, aParam.mHTMLInputMode);
+ WriteParam(aWriter, aParam.mActionHint);
+ WriteParam(aWriter, aParam.mAutocapitalize);
+ WriteParam(aWriter, aParam.mOrigin);
+ WriteParam(aWriter, aParam.mHasHandledUserInput);
+ WriteParam(aWriter, aParam.mInPrivateBrowsing);
+ mozilla::ipc::WriteIPDLParam(aWriter, aWriter->GetActor(), aParam.mURI);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mIMEState) &&
+ ReadParam(aReader, &aResult->mHTMLInputType) &&
+ ReadParam(aReader, &aResult->mHTMLInputMode) &&
+ ReadParam(aReader, &aResult->mActionHint) &&
+ ReadParam(aReader, &aResult->mAutocapitalize) &&
+ ReadParam(aReader, &aResult->mOrigin) &&
+ ReadParam(aReader, &aResult->mHasHandledUserInput) &&
+ ReadParam(aReader, &aResult->mInPrivateBrowsing) &&
+ mozilla::ipc::ReadIPDLParam(aReader, aReader->GetActor(),
+ address_of(aResult->mURI));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::InputContextAction::Cause>
+ : ContiguousEnumSerializerInclusive<
+ mozilla::widget::InputContextAction::Cause,
+ mozilla::widget::InputContextAction::Cause::CAUSE_UNKNOWN,
+ mozilla::widget::InputContextAction::Cause::
+ CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT> {};
+
+template <>
+struct ParamTraits<mozilla::widget::InputContextAction::FocusChange>
+ : ContiguousEnumSerializerInclusive<
+ mozilla::widget::InputContextAction::FocusChange,
+ mozilla::widget::InputContextAction::FocusChange::FOCUS_NOT_CHANGED,
+ mozilla::widget::InputContextAction::FocusChange::WIDGET_CREATED> {};
+
+template <>
+struct ParamTraits<mozilla::widget::InputContextAction> {
+ using paramType = mozilla::widget::InputContextAction;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mCause);
+ WriteParam(aWriter, aParam.mFocusChange);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mCause) &&
+ ReadParam(aReader, &aResult->mFocusChange);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WritingMode> {
+ using paramType = mozilla::WritingMode;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mWritingMode._0);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mWritingMode._0);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::ContentCache::Selection> {
+ using paramType = mozilla::ContentCache::Selection;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mAnchor);
+ WriteParam(aWriter, aParam.mFocus);
+ WriteParam(aWriter, aParam.mWritingMode);
+ WriteParam(aWriter, aParam.mHasRange);
+ WriteParam(aWriter, aParam.mAnchorCharRects[0]);
+ WriteParam(aWriter, aParam.mAnchorCharRects[1]);
+ WriteParam(aWriter, aParam.mFocusCharRects[0]);
+ WriteParam(aWriter, aParam.mFocusCharRects[1]);
+ WriteParam(aWriter, aParam.mRect);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mAnchor) &&
+ ReadParam(aReader, &aResult->mFocus) &&
+ ReadParam(aReader, &aResult->mWritingMode) &&
+ ReadParam(aReader, &aResult->mHasRange) &&
+ ReadParam(aReader, &aResult->mAnchorCharRects[0]) &&
+ ReadParam(aReader, &aResult->mAnchorCharRects[1]) &&
+ ReadParam(aReader, &aResult->mFocusCharRects[0]) &&
+ ReadParam(aReader, &aResult->mFocusCharRects[1]) &&
+ ReadParam(aReader, &aResult->mRect);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::ContentCache::Caret> {
+ using paramType = mozilla::ContentCache::Caret;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mOffset);
+ WriteParam(aWriter, aParam.mRect);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mOffset) &&
+ ReadParam(aReader, &aResult->mRect);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::ContentCache::TextRectArray> {
+ using paramType = mozilla::ContentCache::TextRectArray;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mStart);
+ WriteParam(aWriter, aParam.mRects);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mStart) &&
+ ReadParam(aReader, &aResult->mRects);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::ContentCache> {
+ using paramType = mozilla::ContentCache;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mCompositionStart);
+ WriteParam(aWriter, aParam.mText);
+ WriteParam(aWriter, aParam.mSelection);
+ WriteParam(aWriter, aParam.mFirstCharRect);
+ WriteParam(aWriter, aParam.mCaret);
+ WriteParam(aWriter, aParam.mTextRectArray);
+ WriteParam(aWriter, aParam.mLastCommitStringTextRectArray);
+ WriteParam(aWriter, aParam.mEditorRect);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mCompositionStart) &&
+ ReadParam(aReader, &aResult->mText) &&
+ ReadParam(aReader, &aResult->mSelection) &&
+ ReadParam(aReader, &aResult->mFirstCharRect) &&
+ ReadParam(aReader, &aResult->mCaret) &&
+ ReadParam(aReader, &aResult->mTextRectArray) &&
+ ReadParam(aReader, &aResult->mLastCommitStringTextRectArray) &&
+ ReadParam(aReader, &aResult->mEditorRect);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::widget::CandidateWindowPosition> {
+ using paramType = mozilla::widget::CandidateWindowPosition;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mPoint);
+ WriteParam(aWriter, aParam.mRect);
+ WriteParam(aWriter, aParam.mExcludeRect);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mPoint) &&
+ ReadParam(aReader, &aResult->mRect) &&
+ ReadParam(aReader, &aResult->mExcludeRect);
+ }
+};
+
+// InputData.h
+
+template <>
+struct ParamTraits<mozilla::InputType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::InputType, mozilla::InputType::MULTITOUCH_INPUT,
+ mozilla::kHighestInputType> {};
+
+template <>
+struct ParamTraits<mozilla::InputData> {
+ using paramType = mozilla::InputData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mInputType);
+ WriteParam(aWriter, aParam.mTimeStamp);
+ WriteParam(aWriter, aParam.modifiers);
+ WriteParam(aWriter, aParam.mFocusSequenceNumber);
+ WriteParam(aWriter, aParam.mLayersId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mInputType) &&
+ ReadParam(aReader, &aResult->mTimeStamp) &&
+ ReadParam(aReader, &aResult->modifiers) &&
+ ReadParam(aReader, &aResult->mFocusSequenceNumber) &&
+ ReadParam(aReader, &aResult->mLayersId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::SingleTouchData::HistoricalTouchData> {
+ using paramType = mozilla::SingleTouchData::HistoricalTouchData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mTimeStamp);
+ WriteParam(aWriter, aParam.mScreenPoint);
+ WriteParam(aWriter, aParam.mLocalScreenPoint);
+ WriteParam(aWriter, aParam.mRadius);
+ WriteParam(aWriter, aParam.mRotationAngle);
+ WriteParam(aWriter, aParam.mForce);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mTimeStamp) &&
+ ReadParam(aReader, &aResult->mScreenPoint) &&
+ ReadParam(aReader, &aResult->mLocalScreenPoint) &&
+ ReadParam(aReader, &aResult->mRadius) &&
+ ReadParam(aReader, &aResult->mRotationAngle) &&
+ ReadParam(aReader, &aResult->mForce));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::SingleTouchData> {
+ using paramType = mozilla::SingleTouchData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mHistoricalData);
+ WriteParam(aWriter, aParam.mIdentifier);
+ WriteParam(aWriter, aParam.mScreenPoint);
+ WriteParam(aWriter, aParam.mLocalScreenPoint);
+ WriteParam(aWriter, aParam.mRadius);
+ WriteParam(aWriter, aParam.mRotationAngle);
+ WriteParam(aWriter, aParam.mForce);
+ WriteParam(aWriter, aParam.mTiltX);
+ WriteParam(aWriter, aParam.mTiltY);
+ WriteParam(aWriter, aParam.mTwist);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mHistoricalData) &&
+ ReadParam(aReader, &aResult->mIdentifier) &&
+ ReadParam(aReader, &aResult->mScreenPoint) &&
+ ReadParam(aReader, &aResult->mLocalScreenPoint) &&
+ ReadParam(aReader, &aResult->mRadius) &&
+ ReadParam(aReader, &aResult->mRotationAngle) &&
+ ReadParam(aReader, &aResult->mForce) &&
+ ReadParam(aReader, &aResult->mTiltX) &&
+ ReadParam(aReader, &aResult->mTiltY) &&
+ ReadParam(aReader, &aResult->mTwist));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::MultiTouchInput::MultiTouchType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::MultiTouchInput::MultiTouchType,
+ mozilla::MultiTouchInput::MultiTouchType::MULTITOUCH_START,
+ mozilla::MultiTouchInput::sHighestMultiTouchType> {};
+
+template <>
+struct ParamTraits<mozilla::MultiTouchInput> {
+ using paramType = mozilla::MultiTouchInput;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::InputData&>(aParam));
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mTouches);
+ WriteParam(aWriter, aParam.mHandledByAPZ);
+ WriteParam(aWriter, aParam.mScreenOffset);
+ WriteParam(aWriter, aParam.mButton);
+ WriteParam(aWriter, aParam.mButtons);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::InputData*>(aResult)) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mTouches) &&
+ ReadParam(aReader, &aResult->mHandledByAPZ) &&
+ ReadParam(aReader, &aResult->mScreenOffset) &&
+ ReadParam(aReader, &aResult->mButton) &&
+ ReadParam(aReader, &aResult->mButtons);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::MouseInput::MouseType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::MouseInput::MouseType,
+ mozilla::MouseInput::MouseType::MOUSE_NONE,
+ mozilla::MouseInput::sHighestMouseType> {};
+
+template <>
+struct ParamTraits<mozilla::MouseInput::ButtonType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::MouseInput::ButtonType,
+ mozilla::MouseInput::ButtonType::PRIMARY_BUTTON,
+ mozilla::MouseInput::sHighestButtonType> {};
+
+template <>
+struct ParamTraits<mozilla::MouseInput> {
+ using paramType = mozilla::MouseInput;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::InputData&>(aParam));
+ WriteParam(aWriter, aParam.mButtonType);
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mInputSource);
+ WriteParam(aWriter, aParam.mButtons);
+ WriteParam(aWriter, aParam.mOrigin);
+ WriteParam(aWriter, aParam.mLocalOrigin);
+ WriteParam(aWriter, aParam.mHandledByAPZ);
+ WriteParam(aWriter, aParam.mPreventClickEvent);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::InputData*>(aResult)) &&
+ ReadParam(aReader, &aResult->mButtonType) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mInputSource) &&
+ ReadParam(aReader, &aResult->mButtons) &&
+ ReadParam(aReader, &aResult->mOrigin) &&
+ ReadParam(aReader, &aResult->mLocalOrigin) &&
+ ReadParam(aReader, &aResult->mHandledByAPZ) &&
+ ReadParam(aReader, &aResult->mPreventClickEvent);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::PanGestureInput::PanGestureType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::PanGestureInput::PanGestureType,
+ mozilla::PanGestureInput::PanGestureType::PANGESTURE_MAYSTART,
+ mozilla::PanGestureInput::sHighestPanGestureType> {};
+
+template <>
+struct ParamTraits<mozilla::PanGestureInput::PanDeltaType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::PanGestureInput::PanDeltaType,
+ mozilla::PanGestureInput::PanDeltaType::PANDELTA_PAGE,
+ mozilla::PanGestureInput::sHighestPanDeltaType> {};
+
+template <>
+struct ParamTraits<mozilla::PanGestureInput>
+ : BitfieldHelper<mozilla::PanGestureInput> {
+ using paramType = mozilla::PanGestureInput;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::InputData&>(aParam));
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mPanStartPoint);
+ WriteParam(aWriter, aParam.mPanDisplacement);
+ WriteParam(aWriter, aParam.mLocalPanStartPoint);
+ WriteParam(aWriter, aParam.mLocalPanDisplacement);
+ WriteParam(aWriter, aParam.mLineOrPageDeltaX);
+ WriteParam(aWriter, aParam.mLineOrPageDeltaY);
+ WriteParam(aWriter, aParam.mUserDeltaMultiplierX);
+ WriteParam(aWriter, aParam.mUserDeltaMultiplierY);
+ WriteParam(aWriter, aParam.mDeltaType);
+ WriteParam(aWriter, aParam.mHandledByAPZ);
+ WriteParam(aWriter, aParam.mMayTriggerSwipe);
+ WriteParam(aWriter, aParam.mOverscrollBehaviorAllowsSwipe);
+ WriteParam(aWriter, aParam.mSimulateMomentum);
+ WriteParam(aWriter, aParam.mIsNoLineOrPageDelta);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::InputData*>(aResult)) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mPanStartPoint) &&
+ ReadParam(aReader, &aResult->mPanDisplacement) &&
+ ReadParam(aReader, &aResult->mLocalPanStartPoint) &&
+ ReadParam(aReader, &aResult->mLocalPanDisplacement) &&
+ ReadParam(aReader, &aResult->mLineOrPageDeltaX) &&
+ ReadParam(aReader, &aResult->mLineOrPageDeltaY) &&
+ ReadParam(aReader, &aResult->mUserDeltaMultiplierX) &&
+ ReadParam(aReader, &aResult->mUserDeltaMultiplierY) &&
+ ReadParam(aReader, &aResult->mDeltaType) &&
+ ReadBoolForBitfield(aReader, aResult, &paramType::SetHandledByAPZ) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetMayTriggerSwipe) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetOverscrollBehaviorAllowsSwipe) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetSimulateMomentum) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsNoLineOrPageDelta);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::PinchGestureInput::PinchGestureType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::PinchGestureInput::PinchGestureType,
+ mozilla::PinchGestureInput::PinchGestureType::PINCHGESTURE_START,
+ mozilla::PinchGestureInput::sHighestPinchGestureType> {};
+
+template <>
+struct ParamTraits<mozilla::PinchGestureInput::PinchGestureSource>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::PinchGestureInput::PinchGestureSource,
+ // Set the min to TOUCH, to ensure UNKNOWN is never sent over IPC
+ mozilla::PinchGestureInput::PinchGestureSource::TOUCH,
+ mozilla::PinchGestureInput::sHighestPinchGestureSource> {};
+
+template <>
+struct ParamTraits<mozilla::PinchGestureInput> {
+ using paramType = mozilla::PinchGestureInput;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::InputData&>(aParam));
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mSource);
+ WriteParam(aWriter, aParam.mScreenOffset);
+ WriteParam(aWriter, aParam.mFocusPoint);
+ WriteParam(aWriter, aParam.mLocalFocusPoint);
+ WriteParam(aWriter, aParam.mCurrentSpan);
+ WriteParam(aWriter, aParam.mPreviousSpan);
+ WriteParam(aWriter, aParam.mLineOrPageDeltaY);
+ WriteParam(aWriter, aParam.mHandledByAPZ);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::InputData*>(aResult)) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mSource) &&
+ ReadParam(aReader, &aResult->mScreenOffset) &&
+ ReadParam(aReader, &aResult->mFocusPoint) &&
+ ReadParam(aReader, &aResult->mLocalFocusPoint) &&
+ ReadParam(aReader, &aResult->mCurrentSpan) &&
+ ReadParam(aReader, &aResult->mPreviousSpan) &&
+ ReadParam(aReader, &aResult->mLineOrPageDeltaY) &&
+ ReadParam(aReader, &aResult->mHandledByAPZ);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::TapGestureInput::TapGestureType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::TapGestureInput::TapGestureType,
+ mozilla::TapGestureInput::TapGestureType::TAPGESTURE_LONG,
+ mozilla::TapGestureInput::sHighestTapGestureType> {};
+
+template <>
+struct ParamTraits<mozilla::TapGestureInput> {
+ using paramType = mozilla::TapGestureInput;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::InputData&>(aParam));
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mPoint);
+ WriteParam(aWriter, aParam.mLocalPoint);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::InputData*>(aResult)) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mPoint) &&
+ ReadParam(aReader, &aResult->mLocalPoint);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::ScrollWheelInput::ScrollDeltaType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ScrollWheelInput::ScrollDeltaType,
+ mozilla::ScrollWheelInput::ScrollDeltaType::SCROLLDELTA_LINE,
+ mozilla::ScrollWheelInput::sHighestScrollDeltaType> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollWheelInput::ScrollMode>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ScrollWheelInput::ScrollMode,
+ mozilla::ScrollWheelInput::ScrollMode::SCROLLMODE_INSTANT,
+ mozilla::ScrollWheelInput::sHighestScrollMode> {};
+
+template <>
+struct ParamTraits<mozilla::WheelDeltaAdjustmentStrategy>
+ : public ContiguousEnumSerializer<
+ mozilla::WheelDeltaAdjustmentStrategy,
+ mozilla::WheelDeltaAdjustmentStrategy(0),
+ mozilla::WheelDeltaAdjustmentStrategy::eSentinel> {};
+
+template <>
+struct ParamTraits<mozilla::layers::APZWheelAction>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::APZWheelAction,
+ mozilla::layers::APZWheelAction::Scroll,
+ mozilla::layers::kHighestAPZWheelAction> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollWheelInput> {
+ using paramType = mozilla::ScrollWheelInput;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::InputData&>(aParam));
+ WriteParam(aWriter, aParam.mDeltaType);
+ WriteParam(aWriter, aParam.mScrollMode);
+ WriteParam(aWriter, aParam.mOrigin);
+ WriteParam(aWriter, aParam.mHandledByAPZ);
+ WriteParam(aWriter, aParam.mDeltaX);
+ WriteParam(aWriter, aParam.mDeltaY);
+ WriteParam(aWriter, aParam.mWheelTicksX);
+ WriteParam(aWriter, aParam.mWheelTicksY);
+ WriteParam(aWriter, aParam.mLocalOrigin);
+ WriteParam(aWriter, aParam.mLineOrPageDeltaX);
+ WriteParam(aWriter, aParam.mLineOrPageDeltaY);
+ WriteParam(aWriter, aParam.mScrollSeriesNumber);
+ WriteParam(aWriter, aParam.mUserDeltaMultiplierX);
+ WriteParam(aWriter, aParam.mUserDeltaMultiplierY);
+ WriteParam(aWriter, aParam.mMayHaveMomentum);
+ WriteParam(aWriter, aParam.mIsMomentum);
+ WriteParam(aWriter, aParam.mAllowToOverrideSystemScrollSpeed);
+ WriteParam(aWriter, aParam.mWheelDeltaAdjustmentStrategy);
+ WriteParam(aWriter, aParam.mAPZAction);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::InputData*>(aResult)) &&
+ ReadParam(aReader, &aResult->mDeltaType) &&
+ ReadParam(aReader, &aResult->mScrollMode) &&
+ ReadParam(aReader, &aResult->mOrigin) &&
+ ReadParam(aReader, &aResult->mHandledByAPZ) &&
+ ReadParam(aReader, &aResult->mDeltaX) &&
+ ReadParam(aReader, &aResult->mDeltaY) &&
+ ReadParam(aReader, &aResult->mWheelTicksX) &&
+ ReadParam(aReader, &aResult->mWheelTicksY) &&
+ ReadParam(aReader, &aResult->mLocalOrigin) &&
+ ReadParam(aReader, &aResult->mLineOrPageDeltaX) &&
+ ReadParam(aReader, &aResult->mLineOrPageDeltaY) &&
+ ReadParam(aReader, &aResult->mScrollSeriesNumber) &&
+ ReadParam(aReader, &aResult->mUserDeltaMultiplierX) &&
+ ReadParam(aReader, &aResult->mUserDeltaMultiplierY) &&
+ ReadParam(aReader, &aResult->mMayHaveMomentum) &&
+ ReadParam(aReader, &aResult->mIsMomentum) &&
+ ReadParam(aReader, &aResult->mAllowToOverrideSystemScrollSpeed) &&
+ ReadParam(aReader, &aResult->mWheelDeltaAdjustmentStrategy) &&
+ ReadParam(aReader, &aResult->mAPZAction);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::KeyboardInput::KeyboardEventType>
+ : public ContiguousEnumSerializer<
+ mozilla::KeyboardInput::KeyboardEventType,
+ mozilla::KeyboardInput::KeyboardEventType::KEY_DOWN,
+ mozilla::KeyboardInput::KeyboardEventType::KEY_SENTINEL> {};
+
+template <>
+struct ParamTraits<mozilla::KeyboardInput> {
+ using paramType = mozilla::KeyboardInput;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, static_cast<const mozilla::InputData&>(aParam));
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mKeyCode);
+ WriteParam(aWriter, aParam.mCharCode);
+ WriteParam(aWriter, aParam.mShortcutCandidates);
+ WriteParam(aWriter, aParam.mHandledByAPZ);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, static_cast<mozilla::InputData*>(aResult)) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mKeyCode) &&
+ ReadParam(aReader, &aResult->mCharCode) &&
+ ReadParam(aReader, &aResult->mShortcutCandidates) &&
+ ReadParam(aReader, &aResult->mHandledByAPZ);
+ }
+};
+
+} // namespace IPC
+
+#endif // nsGUIEventIPC_h__
diff --git a/widget/nsHTMLFormatConverter.cpp b/widget/nsHTMLFormatConverter.cpp
new file mode 100644
index 0000000000..4898fd2c8b
--- /dev/null
+++ b/widget/nsHTMLFormatConverter.cpp
@@ -0,0 +1,184 @@
+/* -*- 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 "nsHTMLFormatConverter.h"
+
+#include "nsArray.h"
+#include "nsCRT.h"
+#include "nsCOMPtr.h"
+#include "nsITransferable.h"
+#include "nsLiteralString.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+
+// HTML convertor stuff
+#include "nsPrimitiveHelpers.h"
+#include "nsIDocumentEncoder.h"
+#include "nsContentUtils.h"
+
+nsHTMLFormatConverter::nsHTMLFormatConverter() = default;
+
+nsHTMLFormatConverter::~nsHTMLFormatConverter() = default;
+
+NS_IMPL_ISUPPORTS(nsHTMLFormatConverter, nsIFormatConverter)
+
+//
+// GetInputDataFlavors
+//
+// Creates a new list and returns the list of all the flavors this converter
+// knows how to import. In this case, it's just HTML.
+//
+NS_IMETHODIMP
+nsHTMLFormatConverter::GetInputDataFlavors(nsTArray<nsCString>& aFlavors) {
+ aFlavors.AppendElement(nsLiteralCString(kHTMLMime));
+ return NS_OK;
+}
+
+//
+// GetOutputDataFlavors
+//
+// Creates a new list and returns the list of all the flavors this converter
+// knows how to export (convert). In this case, it's all sorts of things that
+// HTML can be converted to.
+//
+NS_IMETHODIMP
+nsHTMLFormatConverter::GetOutputDataFlavors(nsTArray<nsCString>& aFlavors) {
+ aFlavors.AppendElement(nsLiteralCString(kHTMLMime));
+ aFlavors.AppendElement(nsLiteralCString(kTextMime));
+ return NS_OK;
+}
+
+//
+// CanConvert
+//
+// Determines if we support the given conversion. Currently, this method only
+// converts from HTML to others.
+//
+NS_IMETHODIMP
+nsHTMLFormatConverter::CanConvert(const char* aFromDataFlavor,
+ const char* aToDataFlavor, bool* _retval) {
+ if (!_retval) return NS_ERROR_INVALID_ARG;
+
+ *_retval = false;
+ if (!nsCRT::strcmp(aFromDataFlavor, kHTMLMime)) {
+ if (!nsCRT::strcmp(aToDataFlavor, kHTMLMime)) {
+ *_retval = true;
+ } else if (!nsCRT::strcmp(aToDataFlavor, kTextMime)) {
+ *_retval = true;
+ }
+#if NOT_NOW
+ // pinkerton
+ // no one uses this flavor right now, so it's just slowing things down. If
+ // anyone cares I can put it back in.
+ else if (toFlavor.Equals(kAOLMailMime))
+ *_retval = true;
+#endif
+ }
+ return NS_OK;
+
+} // CanConvert
+
+//
+// Convert
+//
+// Convert data from one flavor to another. The data is wrapped in primitive
+// objects so that it is accessible from JS. Currently, this only accepts HTML
+// input, so anything else is invalid.
+//
+// XXX This method copies the data WAAAAY too many time for my liking. Grrrrrr.
+// Mostly it's because
+// XXX we _must_ put things into nsStrings so that the parser will accept it.
+// Lame lame lame lame. We
+// XXX also can't just get raw unicode out of the nsString, so we have to
+// allocate heap to get
+// XXX unicode out of the string. Lame lame lame.
+//
+NS_IMETHODIMP
+nsHTMLFormatConverter::Convert(const char* aFromDataFlavor,
+ nsISupports* aFromData,
+ const char* aToDataFlavor,
+ nsISupports** aToData) {
+ if (!aToData) return NS_ERROR_INVALID_ARG;
+
+ nsresult rv = NS_OK;
+ *aToData = nullptr;
+
+ if (!nsCRT::strcmp(aFromDataFlavor, kHTMLMime)) {
+ nsAutoCString toFlavor(aToDataFlavor);
+
+ // HTML on clipboard is going to always be double byte so it will be in a
+ // primitive class of nsISupportsString. Also, since the data is in two byte
+ // chunks the length represents the length in 1-byte chars, so we need to
+ // divide by two.
+ nsCOMPtr<nsISupportsString> dataWrapper0(do_QueryInterface(aFromData));
+ if (!dataWrapper0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoString dataStr;
+ dataWrapper0->GetData(dataStr); // COPY #1
+ // note: conversion to text/plain is done inside the clipboard. we do not
+ // need to worry about it here.
+ if (toFlavor.Equals(kHTMLMime) || toFlavor.Equals(kTextMime)) {
+ nsresult res;
+ if (toFlavor.Equals(kHTMLMime)) {
+ int32_t dataLen = dataStr.Length() * 2;
+ nsPrimitiveHelpers::CreatePrimitiveForData(toFlavor, dataStr.get(),
+ dataLen, aToData);
+ } else {
+ nsAutoString outStr;
+ res = ConvertFromHTMLToUnicode(dataStr, outStr);
+ if (NS_SUCCEEDED(res)) {
+ int32_t dataLen = outStr.Length() * 2;
+ nsPrimitiveHelpers::CreatePrimitiveForData(toFlavor, outStr.get(),
+ dataLen, aToData);
+ }
+ }
+ } // else if HTML or Unicode
+ else if (toFlavor.Equals(kAOLMailMime)) {
+ nsAutoString outStr;
+ if (NS_SUCCEEDED(ConvertFromHTMLToAOLMail(dataStr, outStr))) {
+ int32_t dataLen = outStr.Length() * 2;
+ nsPrimitiveHelpers::CreatePrimitiveForData(toFlavor, outStr.get(),
+ dataLen, aToData);
+ }
+ } // else if AOL mail
+ else {
+ rv = NS_ERROR_FAILURE;
+ }
+ } // if we got html mime
+ else
+ rv = NS_ERROR_FAILURE;
+
+ return rv;
+
+} // Convert
+
+//
+// ConvertFromHTMLToUnicode
+//
+// Takes HTML and converts it to plain text but in unicode.
+//
+NS_IMETHODIMP
+nsHTMLFormatConverter::ConvertFromHTMLToUnicode(const nsAutoString& aFromStr,
+ nsAutoString& aToStr) {
+ return nsContentUtils::ConvertToPlainText(
+ aFromStr, aToStr,
+ nsIDocumentEncoder::OutputSelectionOnly |
+ nsIDocumentEncoder::OutputAbsoluteLinks |
+ nsIDocumentEncoder::OutputNoScriptContent |
+ nsIDocumentEncoder::OutputNoFramesContent,
+ 0);
+} // ConvertFromHTMLToUnicode
+
+NS_IMETHODIMP
+nsHTMLFormatConverter::ConvertFromHTMLToAOLMail(const nsAutoString& aFromStr,
+ nsAutoString& aToStr) {
+ aToStr.AssignLiteral("<HTML>");
+ aToStr.Append(aFromStr);
+ aToStr.AppendLiteral("</HTML>");
+
+ return NS_OK;
+}
diff --git a/widget/nsHTMLFormatConverter.h b/widget/nsHTMLFormatConverter.h
new file mode 100644
index 0000000000..f01373233b
--- /dev/null
+++ b/widget/nsHTMLFormatConverter.h
@@ -0,0 +1,32 @@
+/* -*- 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/. */
+
+#ifndef nsHTMLFormatConverter_h__
+#define nsHTMLFormatConverter_h__
+
+#include "nsCOMPtr.h"
+#include "nsIFormatConverter.h"
+#include "nsString.h"
+
+class nsIMutableArray;
+
+class nsHTMLFormatConverter : public nsIFormatConverter {
+ public:
+ nsHTMLFormatConverter();
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFORMATCONVERTER
+
+ protected:
+ virtual ~nsHTMLFormatConverter();
+
+ NS_IMETHOD ConvertFromHTMLToUnicode(const nsAutoString& aFromStr,
+ nsAutoString& aToStr);
+ NS_IMETHOD ConvertFromHTMLToAOLMail(const nsAutoString& aFromStr,
+ nsAutoString& aToStr);
+};
+
+#endif // nsHTMLFormatConverter_h__
diff --git a/widget/nsIAppShell.idl b/widget/nsIAppShell.idl
new file mode 100644
index 0000000000..f2ea8ab166
--- /dev/null
+++ b/widget/nsIAppShell.idl
@@ -0,0 +1,71 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIRunnable;
+%{ C++
+template <class T> struct already_AddRefed;
+
+/**
+ * After the default timezone changes, this topic is notified. Some systems may
+ * not support monitoring timezone.
+ */
+#define DEFAULT_TIMEZONE_CHANGED_OBSERVER_TOPIC "default-timezone-changed"
+%}
+
+/**
+ * Interface for the native event system layer. This interface is designed
+ * to be used on the main application thread only.
+ */
+[uuid(7cd5c71d-223b-4afe-931d-5eedb1f2b01f)]
+interface nsIAppShell : nsISupports
+{
+ /**
+ * Enter an event loop. Don't leave until exit() is called.
+ */
+ void run();
+
+ /**
+ * Exit the handle event loop
+ */
+ void exit();
+
+ /**
+ * Ask the native event queue notification mechanism to favor Gecko tasks
+ * (instead of native tasks) for a short while. (Content processes always
+ * favor Gecko tasks.)
+ */
+ void geckoTaskBurst();
+
+ /**
+ * Suspends the use of additional platform-specific methods (besides the
+ * nsIAppShell->run() event loop) to run Gecko events on the main
+ * application thread. Under some circumstances these "additional methods"
+ * can cause Gecko event handlers to be re-entered, sometimes leading to
+ * hangs and crashes. Calls to suspendNative() and resumeNative() may be
+ * nested. On some platforms (those that don't use any "additional
+ * methods") this will be a no-op. Does not (in itself) stop Gecko events
+ * from being processed on the main application thread. But if the
+ * nsIAppShell->run() event loop is blocked when this call is made, Gecko
+ * events will stop being processed until resumeNative() is called (even
+ * if a plugin or library is temporarily processing events on a nested
+ * event loop).
+ */
+ void suspendNative();
+
+ /**
+ * Resumes the use of additional platform-specific methods to run Gecko
+ * events on the main application thread. Calls to suspendNative() and
+ * resumeNative() may be nested. On some platforms this will be a no-op.
+ */
+ void resumeNative();
+
+ /**
+ * The current event loop nesting level.
+ */
+ readonly attribute unsigned long eventloopNestingLevel;
+};
diff --git a/widget/nsIApplicationChooser.idl b/widget/nsIApplicationChooser.idl
new file mode 100644
index 0000000000..f3da903a10
--- /dev/null
+++ b/widget/nsIApplicationChooser.idl
@@ -0,0 +1,39 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIMIMEInfo.idl"
+
+interface mozIDOMWindowProxy;
+
+[scriptable, function, uuid(8144404d-e6c7-4861-bcca-47de912ee811)]
+interface nsIApplicationChooserFinishedCallback : nsISupports
+{
+ void done(in nsIHandlerApp handlerApp);
+};
+
+[scriptable, uuid(f7a149da-612a-46ba-8a2f-54786fc28791)]
+interface nsIApplicationChooser : nsISupports
+{
+ /**
+ * Initialize the application chooser picker widget. The application chooser
+ * is not valid until this method is called.
+ *
+ * @param parent nsIDOMWindow parent. This dialog will be dependent
+ * on this parent. parent must be non-null.
+ * @param title The title for the file widget
+ *
+ */
+ void init(in mozIDOMWindowProxy parent, in ACString title);
+
+ /**
+ * Open application chooser dialog.
+ *
+ * @param contentType content type of file to open
+ * @param applicationChooserFinishedCallback callback fuction to run when dialog is closed
+ */
+ void open(in ACString contentType, in nsIApplicationChooserFinishedCallback applicationChooserFinishedCallback);
+};
diff --git a/widget/nsIBaseWindow.cpp b/widget/nsIBaseWindow.cpp
new file mode 100644
index 0000000000..a9ebdcc860
--- /dev/null
+++ b/widget/nsIBaseWindow.cpp
@@ -0,0 +1,14 @@
+/* -*- 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 "nsIBaseWindow.h"
+#include "mozilla/LookAndFeel.h"
+
+using namespace mozilla;
+
+CSSToLayoutDeviceScale nsIBaseWindow::UnscaledDevicePixelsPerCSSPixel() {
+ return CSSToLayoutDeviceScale(GetWidgetCSSToDeviceScale() *
+ LookAndFeel::SystemZoomSettings().mFullZoom);
+}
diff --git a/widget/nsIBaseWindow.idl b/widget/nsIBaseWindow.idl
new file mode 100644
index 0000000000..3cf7193788
--- /dev/null
+++ b/widget/nsIBaseWindow.idl
@@ -0,0 +1,296 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsrootidl.idl"
+/*#include "nsIWidget.idl" Boy this would be nice.*/
+
+[ptr] native nsIWidget(nsIWidget);
+%{C++
+#include "Units.h"
+#include "mozilla/DimensionRequest.h"
+
+class nsIWidget;
+%}
+
+typedef voidPtr nativeWindow;
+native DimensionRequest(mozilla::DimensionRequest&&);
+native DimensionKind(mozilla::DimensionKind);
+
+/**
+ * The nsIBaseWindow describes a generic window and basic operations that
+ * can be performed on it. This is not to be a complete windowing interface
+ * but rather a common set that nearly all windowed objects support.
+ */
+
+[scriptable, builtinclass, uuid(ca635529-a977-4552-9b8a-66187e54d882)]
+interface nsIBaseWindow : nsISupports
+{
+ /*
+ Allows a client to initialize an object implementing this interface with
+ the usually required window setup information.
+ It is possible to pass null for both parentNativeWindow and parentWidget,
+ but only docshells support this.
+
+ @param parentNativeWindow - This allows a system to pass in the parenting
+ window as a native reference rather than relying on the calling
+ application to have created the parent window as an nsIWidget. This
+ value will be ignored (should be nullptr) if an nsIWidget is passed in to
+ the parentWidget parameter.
+
+ @param parentWidget - This allows a system to pass in the parenting widget.
+ This allows some objects to optimize themselves and rely on the view
+ system for event flow rather than creating numerous native windows. If
+ one of these is not available, nullptr should be passed.
+
+ @param x - This is the x co-ordinate relative to the parent to place the
+ window.
+
+ @param y - This is the y co-ordinate relative to the parent to place the
+ window.
+
+ @param cx - This is the width for the window to be.
+
+ @param cy - This is the height for the window to be.
+
+ @return NS_OK - Window Init succeeded without a problem.
+ NS_ERROR_UNEXPECTED - Call was unexpected at this time. Perhaps
+ initWindow() had already been called.
+ NS_ERROR_INVALID_ARG - controls that require either a parentNativeWindow
+ or a parentWidget may return invalid arg when they do not
+ receive what they are needing.
+ */
+ [noscript]void initWindow(in nativeWindow parentNativeWindow,
+ in nsIWidget parentWidget, in long x, in long y, in long cx, in long cy);
+
+ /*
+ Tell the window that it should destroy itself. This call should not be
+ necessary as it will happen implictly when final release occurs on the
+ object. If for some reaons you want the window destroyed prior to release
+ due to cycle or ordering issues, then this call provides that ability.
+
+ @return NS_OK - Everything destroyed properly.
+ NS_ERROR_UNEXPECTED - This call was unexpected at this time.
+ Perhaps create() has not been called yet.
+ */
+ void destroy();
+
+ /*
+ Sets the current x and y coordinates of the control. This is relative to
+ the parent window.
+ */
+ void setPosition(in long x, in long y);
+
+ /*
+ Ditto, with arguments in global desktop pixels rather than (potentially
+ ambiguous) device pixels
+ */
+ void setPositionDesktopPix(in long x, in long y);
+
+ /*
+ Gets the current x and y coordinates of the control. This is relative to the
+ parent window.
+ */
+ void getPosition(out long x, out long y);
+
+%{C++
+ mozilla::LayoutDeviceIntPoint GetPosition() {
+ int32_t x = 0, y = 0;
+ GetPosition(&x, &y);
+ return mozilla::LayoutDeviceIntPoint(x, y);
+ }
+%}
+
+ /*
+ Sets the width and height of the control.
+ */
+ void setSize(in long cx, in long cy, in boolean fRepaint);
+
+ /*
+ Gets the width and height of the control.
+ */
+ void getSize(out long cx, out long cy);
+
+%{C++
+ mozilla::LayoutDeviceIntSize GetSize() {
+ int32_t w = 0, h = 0;
+ GetSize(&w, &h);
+ return mozilla::LayoutDeviceIntSize(w, h);
+ }
+%}
+
+ /**
+ * The 'flags' argument to setPositionAndSize is a set of these bits.
+ */
+ const unsigned long eRepaint = 1;
+ const unsigned long eDelayResize = 2;
+
+ /*
+ Convenience function combining the SetPosition and SetSize into one call.
+ Also is more efficient than calling both.
+ */
+ void setPositionAndSize(in long x, in long y, in long cx, in long cy,
+ in unsigned long flags);
+
+ /*
+ Convenience function combining the GetPosition and GetSize into one call.
+ Also is more efficient than calling both.
+ */
+ void getPositionAndSize(out long x, out long y, out long cx, out long cy);
+
+%{C++
+ mozilla::LayoutDeviceIntRect GetPositionAndSize() {
+ int32_t x = 0, y = 0, w = 0, h = 0;
+ GetPositionAndSize(&x, &y, &w, &h);
+ return mozilla::LayoutDeviceIntRect(x, y, w, h);
+ }
+%}
+
+ /**
+ * Allows to request the change of individual dimensions without specifying
+ * the other components.
+ *
+ * @param aRequest - The requested change. A request to change only the width
+ * may look like:
+ * {DimensionKind::Outer, Nothing(), Nothing(), Some(20), Nothing()}
+ *
+ * Note: Inner position is not supported.
+ *
+ * @see DimensionRequest
+ */
+ void setDimensions(in DimensionRequest aRequest);
+
+ /**
+ * Gets the dimensions of the window. The caller may pass nullptr for any
+ * value it is uninterested in receiving.
+ *
+ * @param aDimensionKind Specifies whether the dimensions are in reference
+ * to the inner or outer dimensions.
+ * @param aX Left hand corner of the outer area; or nullptr.
+ * @param aY Top corner of the outer area; or nullptr.
+ * @param aCX Width of the inner or outer area; or nullptr.
+ * @param aCY Height of the inner or outer area; or nullptr.
+ *
+ * Note: Inner position is not supported.
+ *
+ * @see DimensionRequest
+ */
+ void getDimensions(in DimensionKind aDimensionKind, out long aX, out long aY, out long aCX, out long aCY);
+
+ /**
+ * Tell the window to repaint itself
+ * @param aForce - if true, repaint immediately
+ * if false, the window may defer repainting as it sees fit.
+ */
+ void repaint(in boolean force);
+
+ /*
+ This is the parenting widget for the control. This may be null if the
+ native window was handed in for the parent during initialization.
+ If this is returned, it should refer to the same object as
+ parentNativeWindow.
+
+ Setting this after Create() has been called may not be supported by some
+ implementations.
+
+ On controls that don't support widgets, setting this will return a
+ NS_ERROR_NOT_IMPLEMENTED error.
+ */
+ [noscript] attribute nsIWidget parentWidget;
+
+ /*
+ This is the native window parent of the control.
+
+ Setting this after Create() has been called may not be supported by some
+ implementations.
+
+ On controls that don't support setting nativeWindow parents, setting this
+ will return a NS_ERROR_NOT_IMPLEMENTED error.
+ */
+ attribute nativeWindow parentNativeWindow;
+
+ /*
+ This is the handle (HWND, GdkWindow*, ...) to the native window of the
+ control, exposed as an AString.
+
+ @return AString in hex format with "0x" prepended, or empty string if
+ mainWidget undefined
+
+ @throws NS_ERROR_NOT_IMPLEMENTED for non-XULWindows
+ */
+ readonly attribute AString nativeHandle;
+
+ /*
+ Attribute controls the visibility of the object behind this interface.
+ Setting this attribute to false will hide the control. Setting it to
+ true will show it.
+ */
+ attribute boolean visibility;
+
+ /*
+ a disabled window should accept no user interaction; it's a dead window,
+ like the parent of a modal window.
+ */
+ attribute boolean enabled;
+
+ /*
+ Allows you to find out what the widget is of a given object. Depending
+ on the object, this may return the parent widget in which this object
+ lives if it has not had to create its own widget.
+ */
+ [noscript] readonly attribute nsIWidget mainWidget;
+
+ /*
+ The number of device pixels per CSS pixel used by this window's widget at the
+ default full zoom level.
+ This is the value returned by GetDefaultScale() of the underlying widget.
+ Note that this may change if the window is moved between screens with
+ differing resolutions.
+ NOTE: This is mostly an implementation detail of
+ UnscaledDevicePixelsPerCSSPixel, which is what you probably want to use.
+ */
+ [noscript, notxpcom, nostdcall] readonly attribute double widgetCSSToDeviceScale;
+
+%{C++
+ // The number of device pixels per CSS pixel used on this window's current
+ // screen at the default full zoom level.
+ //
+ // This is the widget scale _plus_ the OS zoom scale if appropriate.
+ // Implemented in AppWindow.cpp
+ mozilla::CSSToLayoutDeviceScale UnscaledDevicePixelsPerCSSPixel();
+%}
+
+ /*
+ The number of device pixels per display pixel on this window's current
+ screen. (The meaning of "display pixel" varies across OS environments;
+ it is the pixel units used by the desktop environment to manage screen
+ real estate and window positioning, which may correspond to (per-screen)
+ device pixels, or may be a virtual coordinate space that covers a multi-
+ monitor, mixed-dpi desktop space.)
+ This is the value returned by GetDesktopToDeviceScale() of the underlying
+ widget.
+ Note that this may change if the window is moved between screens with
+ differing resolutions.
+ */
+ readonly attribute double devicePixelsPerDesktopPixel;
+
+%{C++
+ mozilla::DesktopToLayoutDeviceScale DevicePixelsPerDesktopPixel() {
+ double s = 1.0;
+ GetDevicePixelsPerDesktopPixel(&s);
+ return mozilla::DesktopToLayoutDeviceScale(s);
+ }
+
+ mozilla::CSSToDesktopScale GetUnscaledCSSToDesktopScale() {
+ return UnscaledDevicePixelsPerCSSPixel() / DevicePixelsPerDesktopPixel();
+ }
+%}
+
+ /*
+ Title of the window.
+ */
+ attribute AString title;
+};
diff --git a/widget/nsIBidiKeyboard.idl b/widget/nsIBidiKeyboard.idl
new file mode 100644
index 0000000000..1d41b2e79e
--- /dev/null
+++ b/widget/nsIBidiKeyboard.idl
@@ -0,0 +1,31 @@
+/* -*- 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 "nsISupports.idl"
+
+[builtinclass, scriptable, uuid(288dae24-76e2-43a3-befe-9d9fabe8014e)]
+interface nsIBidiKeyboard : nsISupports
+{
+ /**
+ * Inspects the installed keyboards and resets the bidi keyboard state
+ */
+ void reset();
+
+ /**
+ * Determines if the current keyboard language is right-to-left
+ * @throws NS_ERROR_FAILURE if no right-to-left keyboards are installed
+ */
+ boolean isLangRTL();
+
+ /**
+ * Determines whether the system has at least one keyboard of each direction
+ * installed.
+ *
+ * @throws NS_ERROR_NOT_IMPLEMENTED if the widget layer does not provide this
+ * information.
+ */
+ readonly attribute boolean haveBidiKeyboards;
+};
diff --git a/widget/nsIClipboard.idl b/widget/nsIClipboard.idl
new file mode 100644
index 0000000000..5ed2d22600
--- /dev/null
+++ b/widget/nsIClipboard.idl
@@ -0,0 +1,215 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+#include "nsITransferable.idl"
+#include "nsIClipboardOwner.idl"
+
+interface nsIArray;
+
+webidl WindowContext;
+
+[scriptable, builtinclass, uuid(801e2318-c8fa-11ed-afa1-0242ac120002)]
+interface nsIAsyncSetClipboardData : nsISupports {
+ /**
+ * Provide the data for the set request.
+ *
+ * @param aTransferable
+ * The transferable contains the data to be written.
+ * @param aOwner [optional]
+ * The owner of the transferable.
+ */
+ void setData(in nsITransferable aTransferable, [optional] in nsIClipboardOwner aOwner);
+
+ /**
+ * Abort the request to set data.
+ *
+ * @param aReason
+ * The reason for the abort, can not be NS_OK.
+ */
+ void abort(in nsresult aReason);
+};
+
+[scriptable, function, uuid(78f7c18e-c8fa-11ed-afa1-0242ac120002)]
+interface nsIAsyncClipboardRequestCallback : nsISupports
+{
+ /**
+ * Indicates that the clipboard request has either succeeded, been canceled or
+ * rejected.
+ *
+ * @param aResult
+ * The result of the request. NS_OK if successful, or another value
+ * that indicates the reason for failure or cancellation.
+ */
+ void onComplete(in nsresult aResult);
+};
+
+[scriptable, builtinclass, uuid(c18ea2f7-6b6f-4a38-9ab3-a8781fdfcc39)]
+interface nsIAsyncGetClipboardData : nsISupports {
+ /**
+ * Determines whether this request is still valid (e.g., the clipboard content
+ * associated with this request is not stale).
+ */
+ readonly attribute boolean valid;
+
+ /**
+ * The available flavors in the clipboard.
+ */
+ readonly attribute Array<ACString> flavorList;
+
+ /**
+ * Filters the flavors that `aTransferable` can import (see
+ * `nsITransferable::flavorsTransferableCanImport`). Every specified flavors
+ * must exist in `flavorList`, or the request will be rejected. If the request
+ * remains valid, it retrieves the data for the first flavor. The data is then
+ * set for `aTransferable`.
+ *
+ * @param aTransferable
+ * The transferable which contains the flavors to be read.
+ * @param aCallback
+ * The nsIAsyncClipboardRequestCallback to be invoked once the get
+ * request is either successfully completed or rejected.
+ * @result NS_OK if no errors
+ */
+ void getData(in nsITransferable aTransferable,
+ in nsIAsyncClipboardRequestCallback aCallback);
+};
+
+[scriptable, uuid(ce23c1c4-58fd-4c33-8579-fa0796d9652c)]
+interface nsIAsyncClipboardGetCallback : nsISupports
+{
+ /**
+ * Indicates that the clipboard get request has succeeded.
+ */
+ void onSuccess(in nsIAsyncGetClipboardData aAsyncGetClipboardData);
+
+ /**
+ * Indicates that the clipboard get request has rejected.
+ *
+ * @param aResult
+ * The reason for the rejection, can not be NS_OK.
+ */
+ void onError(in nsresult aResult);
+};
+
+[scriptable, builtinclass, uuid(ceaa0047-647f-4b8e-ad1c-aff9fa62aa51)]
+interface nsIClipboard : nsISupports
+{
+ const long kSelectionClipboard = 0;
+ const long kGlobalClipboard = 1;
+ const long kFindClipboard = 2;
+ // Used to cache current selection on (nsClipboard) for macOS service menu.
+ const long kSelectionCache = 3;
+
+%{ C++
+ static const uint32_t kClipboardTypeCount = kSelectionCache + 1;
+%}
+
+ /**
+ * Given a transferable, set the data on the native clipboard
+ *
+ * @param aTransferable The transferable
+ * @param anOwner The owner of the transferable
+ * @param aWhichClipboard Specifies the clipboard to which this operation applies.
+ * @result NS_Ok if no errors
+ */
+
+ void setData ( in nsITransferable aTransferable, in nsIClipboardOwner anOwner,
+ in long aWhichClipboard ) ;
+
+ /**
+ * Requests setting data to the native clipboard. The acutal set occur
+ * when the data is provided by calling nsIAsyncSetClipboardData::setData().
+ * The result will be notified by nsIClipboardCallback. A new set request
+ * will cancel any prior pending requests, if any exist.
+ *
+ * @param aWhichClipboard
+ * Specifies the clipboard to which this operation applies.
+ * @param aCallback [optional]
+ * The callback object that will be notified upon completion.
+ * @return nsIAsyncSetClipboardData
+ * The write request object. The actual write will occur when the
+ * data is provided by calling nsIAsyncSetClipboardData::setData().
+ */
+ nsIAsyncSetClipboardData asyncSetData(in long aWhichClipboard,
+ [optional] in nsIAsyncClipboardRequestCallback aCallback);
+
+ /**
+ * Filters the flavors aTransferable can import (see
+ * `nsITransferable::flavorsTransferableCanImport`) and gets the data for the
+ * first flavor. That data is set for aTransferable.
+ *
+ * @param aTransferable The transferable
+ * @param aWhichClipboard Specifies the clipboard to which this operation applies.
+ * @param aRequestingWindowContext [optional]
+ * The window context window that is requesting the clipboard, which is
+ * used for content analysis. Passing null means that the content is
+ * exempt from content analysis. (for example, scripted clipboard read by
+ * system code) This parameter should not be null when calling this from a
+ * content process.
+ * @result NS_OK if no errors
+ */
+
+ void getData ( in nsITransferable aTransferable, in long aWhichClipboard, [optional] in WindowContext aRequestingWindowContext) ;
+
+ /**
+ * Requests getting data asynchronously from the native clipboard. This does
+ * not actually retrieve the data, but returns a nsIAsyncGetClipboardData
+ * contains current avaiable data formats. If the native clipboard is
+ * updated, either by us or other application, the existing
+ * nsIAsyncGetClipboardData becomes invalid.
+ *
+ * @param aFlavorList
+ * Specific data formats ('flavors') that can be retrieved from the
+ * clipboard.
+ * @param aWhichClipboard
+ * Specifies the clipboard to which this operation applies.
+ * @param aCallback
+ * The callback object that will be notified upon completion.
+ * @result NS_OK if no errors
+ */
+ void asyncGetData(in Array<ACString> aFlavorList,
+ in long aWhichClipboard,
+ in WindowContext aRequestingWindowContext,
+ in nsIPrincipal aRequestingPrincipal,
+ in nsIAsyncClipboardGetCallback aCallback);
+
+ /**
+ * This empties the clipboard and notifies the clipboard owner.
+ * This empties the "logical" clipboard. It does not clear the native clipboard.
+ *
+ * @param aWhichClipboard Specifies the clipboard to which this operation applies.
+ * @result NS_OK if successful.
+ */
+
+ void emptyClipboard ( in long aWhichClipboard ) ;
+
+ /**
+ * This provides a way to give correct UI feedback about, for instance, a paste
+ * should be allowed. It does _NOT_ actually retreive the data and should be a very
+ * inexpensive call. All it does is check if there is data on the clipboard matching
+ * any of the flavors in the given list.
+ *
+ * @param aFlavorList An array of ASCII strings.
+ * @param aWhichClipboard Specifies the clipboard to which this operation applies.
+ * @outResult - if data is present matching one of
+ * @result NS_OK if successful.
+ */
+ boolean hasDataMatchingFlavors ( in Array<ACString> aFlavorList,
+ in long aWhichClipboard ) ;
+
+ /**
+ * Allows clients to determine if the implementation supports the concept of a
+ * separate clipboard.
+ *
+ * @param aWhichClipboard Specifies the clipboard to which this operation applies.
+ * @outResult true if the implementaion supports specific clipboard type.
+ * @result NS_OK if successful.
+ */
+ [infallible]
+ boolean isClipboardTypeSupported(in long aWhichClipboard);
+};
diff --git a/widget/nsIClipboardHelper.idl b/widget/nsIClipboardHelper.idl
new file mode 100644
index 0000000000..ed4af112f1
--- /dev/null
+++ b/widget/nsIClipboardHelper.idl
@@ -0,0 +1,45 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+#include "nsIClipboard.idl"
+
+%{ C++
+#include "nsString.h" // needed for AString -> nsAString, unfortunately
+%}
+
+/**
+ * helper service for common uses of nsIClipboard.
+ */
+
+[scriptable, uuid(438307fd-0c68-4d79-922a-f6cc9550cd02)]
+interface nsIClipboardHelper : nsISupports
+{
+ cenum SensitiveData : 8 {
+ NotSensitive = 0,
+ Sensitive = 1,
+ };
+
+ /**
+ * copy string to given clipboard
+ *
+ * @param aString, the string to copy to the clipboard
+ * @param aClipboardID, the ID of the clipboard to copy to
+ * (eg. kSelectionClipboard -- see nsIClipboard.idl)
+ * @param aSensitive, optional flag to indicate that data is sensitive, like a password.
+ * That will exclude data from Cloud Clipboard/Clipboard History on Windows.
+ */
+ void copyStringToClipboard(in AString aString, in long aClipboardID,
+ [optional, default(NotSensitive)] in nsIClipboardHelper_SensitiveData aSensitive);
+
+ /**
+ * copy string to (default) clipboard
+ *
+ * @param aString, the string to copy to the clipboard
+ */
+ void copyString(in AString aString,
+ [optional, default(NotSensitive)] in nsIClipboardHelper_SensitiveData aSensitive);
+};
diff --git a/widget/nsIClipboardOwner.idl b/widget/nsIClipboardOwner.idl
new file mode 100644
index 0000000000..df9dfd1049
--- /dev/null
+++ b/widget/nsIClipboardOwner.idl
@@ -0,0 +1,29 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+#include "nsITransferable.idl"
+
+
+[scriptable, uuid(5A31C7A1-E122-11d2-9A57-000064657374)]
+interface nsIClipboardOwner : nsISupports
+{
+ /**
+ * Notifies the owner of the clipboard transferable that the
+ * transferable is being removed from the clipboard
+ *
+ * @param aTransferable The transferable
+ * @result NS_Ok if no errors
+ */
+
+ void LosingOwnership ( in nsITransferable aTransferable ) ;
+};
+
+
+%{ C++
+
+%}
diff --git a/widget/nsIColorPicker.idl b/widget/nsIColorPicker.idl
new file mode 100644
index 0000000000..aa24bf73cc
--- /dev/null
+++ b/widget/nsIColorPicker.idl
@@ -0,0 +1,72 @@
+/* -*- 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 "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+
+/**
+ * nsIColorPicker is representing colors as strings because the internal
+ * representation will depend on the underlying backend.
+ * The format of the colors taken in input and returned will always follow the
+ * format of the <input type='color'> value as described in the HTML
+ * specifications.
+ */
+
+[scriptable, uuid(d2ce78d1-40b5-49d1-b66d-5801fcb9a385)]
+interface nsIColorPickerShownCallback : nsISupports
+{
+ /**
+ * Callback called when the color picker requests a color update.
+ * This callback can not be called after done() was called.
+ * When this callback is used, the consumer can assume that the color value has
+ * changed.
+ *
+ * @param color The new selected color value following the format specifed on
+ * top of this file.
+ */
+ void update(in AString color);
+
+ /**
+ * Callback called when the color picker is dismissed.
+ * When this callback is used, the color might have changed or could stay the
+ * same.
+ * If the color has not changed, the color parameter will be the empty string.
+ *
+ * @param color The new selected color value following the format specifed on
+ * top of this file or the empty string.
+ */
+ void done(in AString color);
+};
+
+[scriptable, uuid(de229d37-a8a6-46f1-969a-0c1de33d0ad7)]
+interface nsIColorPicker : nsISupports
+{
+ /**
+ * Initialize the color picker widget. The color picker will not be shown until
+ * open() is called.
+ * If the backend doesn't support setting a title to the native color picker
+ * widget, the title parameter might be ignored.
+ * If the initialColor parameter does not follow the format specified on top of
+ * this file, the behavior will be unspecified. The initialColor could be the
+ * one used by the underlying backend or an arbitrary one. The backend could
+ * also assert.
+ *
+ * @param parent nsIDOMWindow parent. This dialog will be dependent
+ * on this parent. parent must be non-null.
+ * @param title The title for the color picker widget.
+ * @param initialColor The color to show when the widget is opened. The
+ * parameter has to follow the format specified on top
+ * of this file.
+ */
+ void init(in mozIDOMWindowProxy parent, in AString title, in AString initialColor, in Array<AString> defaultColors);
+
+ /**
+ * Opens the color dialog asynchrounously.
+ * The results are provided via the callback object.
+ */
+ void open(in nsIColorPickerShownCallback aColorPickerShownCallback);
+};
diff --git a/widget/nsIDeviceContextSpec.cpp b/widget/nsIDeviceContextSpec.cpp
new file mode 100644
index 0000000000..5ab442415d
--- /dev/null
+++ b/widget/nsIDeviceContextSpec.cpp
@@ -0,0 +1,87 @@
+/* -*- 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 "nsIDeviceContextSpec.h"
+
+#include "gfxPoint.h"
+#include "mozilla/gfx/PrintPromise.h"
+#include "nsError.h"
+#include "nsIPrintSettings.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/TaskQueue.h"
+
+using mozilla::MakeRefPtr;
+using mozilla::gfx::PrintEndDocumentPromise;
+
+// We have some platform specific code here rather than in the appropriate
+// nsIDeviceContextSpec subclass. We structure the code this way so that
+// nsIDeviceContextSpecProxy gets the correct behavior without us having to
+// instantiate a platform specific nsIDeviceContextSpec subclass in content
+// processes. That is necessary for sandboxing.
+
+float nsIDeviceContextSpec::GetPrintingScale() {
+#ifdef XP_WIN
+ if (mPrintSettings->GetOutputFormat() != nsIPrintSettings::kOutputFormatPDF
+# ifdef MOZ_ENABLE_SKIA_PDF
+ && !mPrintViaSkPDF
+# endif
+ ) {
+ // The print settings will have the resolution stored from the real device.
+ int32_t resolution;
+ mPrintSettings->GetResolution(&resolution);
+ return float(resolution) / GetDPI();
+ }
+#endif
+
+ return 72.0f / GetDPI();
+}
+
+gfxPoint nsIDeviceContextSpec::GetPrintingTranslate() {
+#ifdef XP_WIN
+ // The underlying surface on windows is the size of the printable region. When
+ // the region is smaller than the actual paper size the (0, 0) coordinate
+ // refers top-left of that unwritable region. To instead have (0, 0) become
+ // the top-left of the actual paper, translate it's coordinate system by the
+ // unprintable region's width.
+ double marginTop, marginLeft;
+ mPrintSettings->GetUnwriteableMarginTop(&marginTop);
+ mPrintSettings->GetUnwriteableMarginLeft(&marginLeft);
+ int32_t resolution;
+ mPrintSettings->GetResolution(&resolution);
+ return gfxPoint(-marginLeft * resolution, -marginTop * resolution);
+#else
+ return gfxPoint(0, 0);
+#endif
+}
+
+RefPtr<PrintEndDocumentPromise>
+nsIDeviceContextSpec::EndDocumentPromiseFromResult(nsresult aResult,
+ const char* aSite) {
+ return NS_SUCCEEDED(aResult)
+ ? PrintEndDocumentPromise::CreateAndResolve(true, aSite)
+ : PrintEndDocumentPromise::CreateAndReject(aResult, aSite);
+}
+
+RefPtr<PrintEndDocumentPromise> nsIDeviceContextSpec::EndDocumentAsync(
+ const char* aCallSite, AsyncEndDocumentFunction aFunction) {
+ auto promise =
+ MakeRefPtr<PrintEndDocumentPromise::Private>("PrintEndDocumentPromise");
+
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "EndDocumentAsync",
+ [promise, function = std::move(aFunction)]() mutable {
+ const auto result = function();
+ if (NS_SUCCEEDED(result)) {
+ promise->Resolve(true, __func__);
+ } else {
+ promise->Reject(result, __func__);
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return promise;
+}
diff --git a/widget/nsIDeviceContextSpec.h b/widget/nsIDeviceContextSpec.h
new file mode 100644
index 0000000000..6afe4ea850
--- /dev/null
+++ b/widget/nsIDeviceContextSpec.h
@@ -0,0 +1,111 @@
+/* -*- 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/. */
+
+#ifndef nsIDeviceContextSpec_h___
+#define nsIDeviceContextSpec_h___
+
+#include "gfxPoint.h"
+#include "nsISupports.h"
+#include "mozilla/StaticPrefs_print.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/PrintPromise.h"
+#include "mozilla/MoveOnlyFunction.h"
+
+class nsIWidget;
+class nsIPrintSettings;
+
+namespace mozilla {
+namespace gfx {
+class DrawEventRecorder;
+class PrintTarget;
+} // namespace gfx
+} // namespace mozilla
+
+#define NS_IDEVICE_CONTEXT_SPEC_IID \
+ { \
+ 0xf407cfba, 0xbe28, 0x46c9, { \
+ 0x8a, 0xba, 0x04, 0x2d, 0xae, 0xbb, 0x4f, 0x23 \
+ } \
+ }
+
+class nsIDeviceContextSpec : public nsISupports {
+ public:
+ typedef mozilla::gfx::PrintTarget PrintTarget;
+ using IntSize = mozilla::gfx::IntSize;
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IDEVICE_CONTEXT_SPEC_IID)
+
+ /**
+ * Initialize the device context spec.
+ * @param aWidget A widget a dialog can be hosted in
+ * @param aPrintSettings Print settings for the print operation
+ * @param aIsPrintPreview True if creating Spec for PrintPreview
+ * @return NS_OK or a suitable error code.
+ */
+ NS_IMETHOD Init(nsIPrintSettings* aPrintSettings, bool aIsPrintPreview) = 0;
+
+ virtual already_AddRefed<PrintTarget> MakePrintTarget() = 0;
+
+ /**
+ * If required override to return a recorder to record the print.
+ *
+ * @param aDrawEventRecorder out param for the recorder to use
+ * @return NS_OK or a suitable error code
+ */
+ NS_IMETHOD GetDrawEventRecorder(
+ mozilla::gfx::DrawEventRecorder** aDrawEventRecorder) {
+ MOZ_ASSERT(aDrawEventRecorder);
+ *aDrawEventRecorder = nullptr;
+ return NS_OK;
+ }
+
+ /**
+ * @return DPI for printing.
+ */
+ float GetDPI() { return mozilla::StaticPrefs::print_default_dpi(); }
+
+ /**
+ * @return the printing scale to be applied to the context for printing.
+ */
+ float GetPrintingScale();
+
+ /**
+ * @return the point to translate the context to for printing.
+ */
+ gfxPoint GetPrintingTranslate();
+
+ NS_IMETHOD BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) = 0;
+
+ virtual RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() = 0;
+ /**
+ * Note: not all print devices implement mixed page sizing. Internally,
+ * aSizeInPoints gets handed off to a PrintTarget, and most PrintTarget
+ * subclasses will ignore `aSizeInPoints`.
+ */
+ NS_IMETHOD BeginPage(const IntSize& aSizeInPoints) = 0;
+ NS_IMETHOD EndPage() = 0;
+
+ protected:
+ using AsyncEndDocumentFunction = mozilla::MoveOnlyFunction<nsresult()>;
+ static RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocumentAsync(
+ const char* aCallSite, AsyncEndDocumentFunction aFunction);
+
+ static RefPtr<mozilla::gfx::PrintEndDocumentPromise>
+ EndDocumentPromiseFromResult(nsresult aResult, const char* aSite);
+
+ nsCOMPtr<nsIPrintSettings> mPrintSettings;
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ // This variable is independant of nsIPrintSettings::kOutputFormatPDF (i.e.
+ // save-to-PDF). If set to true, then even when we print to a printer we
+ // output and send it PDF.
+ bool mPrintViaSkPDF = false;
+#endif
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIDeviceContextSpec, NS_IDEVICE_CONTEXT_SPEC_IID)
+#endif
diff --git a/widget/nsIDisplayInfo.idl b/widget/nsIDisplayInfo.idl
new file mode 100644
index 0000000000..69d5064e4e
--- /dev/null
+++ b/widget/nsIDisplayInfo.idl
@@ -0,0 +1,14 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+[scriptable, uuid(615bc23d-6346-4b15-9c10-add002f140b6)]
+interface nsIDisplayInfo : nsISupports
+{
+ readonly attribute long id;
+ readonly attribute boolean connected;
+};
diff --git a/widget/nsIDragService.idl b/widget/nsIDragService.idl
new file mode 100644
index 0000000000..a35d3128a8
--- /dev/null
+++ b/widget/nsIDragService.idl
@@ -0,0 +1,213 @@
+/* -*- Mode: IDL; 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 "nsIArray.idl"
+#include "nsISupports.idl"
+#include "nsIDragSession.idl"
+#include "nsIContentPolicy.idl"
+
+webidl DragEvent;
+webidl Element;
+webidl Node;
+webidl Selection;
+
+interface nsICookieJarSettings;
+
+%{C++
+#include "mozilla/EventForwards.h"
+
+namespace mozilla {
+namespace dom {
+class ContentParent;
+class DataTransfer;
+class RemoteDragStartData;
+} // namespace dom
+} // namespace mozilla
+%}
+
+[ptr] native ContentParentPtr(mozilla::dom::ContentParent);
+[ptr] native DataTransferPtr(mozilla::dom::DataTransfer);
+[ptr] native RemoteDragStartDataPtr(mozilla::dom::RemoteDragStartData);
+native EventMessage(mozilla::EventMessage);
+
+[scriptable, uuid(ebd6b3a2-af16-43af-a698-3091a087dd62), builtinclass]
+interface nsIDragService : nsISupports
+{
+ const long DRAGDROP_ACTION_NONE = 0;
+ const long DRAGDROP_ACTION_COPY = 1;
+ const long DRAGDROP_ACTION_MOVE = 2;
+ const long DRAGDROP_ACTION_LINK = 4;
+ const long DRAGDROP_ACTION_UNINITIALIZED = 64;
+
+ /**
+ * Starts a modal drag session with an array of transaferables.
+ *
+ * Note: This method is deprecated for non-native code.
+ *
+ * @param aPrincipal - the triggering principal of the drag, or null if
+ * it's from browser chrome or OS
+ * @param aCsp - The csp of the triggering Document
+ * @param aTransferables - an array of transferables to be dragged
+ * @param aActionType - specified which of copy/move/link are allowed
+ * @param aContentPolicyType - the contentPolicyType that will be
+ * passed to the loadInfo when creating a new channel
+ * (defaults to TYPE_OTHER)
+ */
+ [can_run_script]
+ void invokeDragSession (in Node aDOMNode,
+ in nsIPrincipal aPrincipal,
+ in nsIContentSecurityPolicy aCsp,
+ in nsICookieJarSettings aCookieJarSettings,
+ in nsIArray aTransferables,
+ in unsigned long aActionType,
+ [optional] in nsContentPolicyType aContentPolicyType);
+
+ /**
+ * Starts a modal drag session using an image. The first four arguments are
+ * the same as invokeDragSession.
+ *
+ * Note: This method is deprecated for non-native code.
+ *
+ * A custom image may be specified using the aImage argument. If this is
+ * supplied, the aImageX and aImageY arguments specify the offset within
+ * the image where the cursor would be positioned. That is, when the image
+ * is drawn, it is offset up and left the amount so that the cursor appears
+ * at that location within the image.
+ *
+ * If aImage is null, aImageX and aImageY are not used and the image is instead
+ * determined from the source node aDOMNode, and the offset calculated so that
+ * the initial location for the image appears in the same screen position as
+ * where the element is located. The node must be within a document.
+ *
+ * Currently, supported images are all DOM nodes. If this is an HTML <image> or
+ * <canvas>, the drag image is taken from the image data. If the element is in
+ * a document, it will be rendered at its displayed size, othewise, it will be
+ * rendered at its real size. For other types of elements, the element is
+ * rendered into an offscreen buffer in the same manner as it is currently
+ * displayed. The document selection is hidden while drawing.
+ *
+ * The aDragEvent must be supplied as the current screen coordinates of the
+ * event are needed to calculate the image location.
+ */
+ [noscript, can_run_script]
+ void invokeDragSessionWithImage(in Node aDOMNode,
+ in nsIPrincipal aPrincipal,
+ in nsIContentSecurityPolicy aCsp,
+ in nsICookieJarSettings aCookieJarSettings,
+ in nsIArray aTransferableArray,
+ in unsigned long aActionType,
+ in Node aImage,
+ in long aImageX,
+ in long aImageY,
+ in DragEvent aDragEvent,
+ in DataTransferPtr aDataTransfer);
+
+ /** Start a drag session with the data in aDragStartData from a child process.
+ * Other arguments are the same as invokeDragSessionWithImage.
+ */
+ [noscript, can_run_script]
+ void invokeDragSessionWithRemoteImage(in Node aDOMNode,
+ in nsIPrincipal aPrincipal,
+ in nsIContentSecurityPolicy aCsp,
+ in nsICookieJarSettings aCookieJarSettings,
+ in nsIArray aTransferableArray,
+ in unsigned long aActionType,
+ in RemoteDragStartDataPtr aDragStartData,
+ in DragEvent aDragEvent,
+ in DataTransferPtr aDataTransfer);
+
+ /**
+ * Start a modal drag session using the selection as the drag image.
+ * The aDragEvent must be supplied as the current screen coordinates of the
+ * event are needed to calculate the image location.
+ *
+ * Note: This method is deprecated for non-native code.
+ */
+ [can_run_script]
+ void invokeDragSessionWithSelection(in Selection aSelection,
+ in nsIPrincipal aPrincipal,
+ in nsIContentSecurityPolicy aCsp,
+ in nsICookieJarSettings aCookieJarSettings,
+ in nsIArray aTransferableArray,
+ in unsigned long aActionType,
+ in DragEvent aDragEvent,
+ in DataTransferPtr aDataTransfer);
+
+ /**
+ * Returns the current Drag Session
+ */
+ nsIDragSession getCurrentSession();
+
+ /**
+ * Tells the Drag Service to start a drag session. This is called when
+ * an external drag occurs
+ */
+ void startDragSession() ;
+
+ /**
+ * Similar to startDragSession(), automated tests may want to start
+ * session for emulating an external drag. At that time, this should
+ * be used instead of startDragSession().
+ *
+ * @param aAllowedEffect Set default drag action which means allowed effects
+ * in the session and every DnD events are initialized
+ * with one of specified value. So, the value can be
+ * DRAGDROP_ACTION_NONE, or one or more values of
+ * DRAGDROP_ACTION_(MOVE|COPY|LINK).
+ */
+ void startDragSessionForTests(in unsigned long aAllowedEffect);
+
+ /**
+ * Tells the Drag Service to end a drag session. This is called when
+ * an external drag occurs
+ *
+ * If aDoneDrag is true, the drag has finished, otherwise the drag has
+ * just left the window.
+ */
+ [can_run_script]
+ void endDragSession(in boolean aDoneDrag,
+ [optional] in unsigned long aKeyModifiers);
+
+ /**
+ * Fire a drag event at the source of the drag
+ */
+ [noscript, can_run_script]
+ void fireDragEventAtSource(in EventMessage aEventMessage,
+ in unsigned long aKeyModifiers);
+
+ /**
+ * Increase/decrease dragging suppress level by one.
+ * If level is greater than one, dragging is disabled.
+ */
+ [can_run_script]
+ void suppress();
+ void unsuppress();
+
+ /**
+ * aX and aY are in LayoutDevice pixels.
+ */
+ [noscript] void dragMoved(in long aX, in long aY);
+
+ [notxpcom, nostdcall] boolean maybeAddChildProcess(in ContentParentPtr aChild);
+ [notxpcom, nostdcall] boolean removeAllChildProcesses();
+
+ /**
+ * Retrun true if nsIDragSession's data is updated.
+ */
+ [notxpcom, nostdcall] boolean mustUpdateDataTransfer(in EventMessage aMessage);
+
+ /**
+ * Called when HTMLEditor maybe deleted the source node from the document.
+ *
+ * @param aEditingHost The editing host when the editor deletes selection.
+ */
+ [noscript] void maybeEditorDeletedSourceNode(in Element aEditingHost);
+};
+
+
+%{ C++
+
+%}
diff --git a/widget/nsIDragSession.idl b/widget/nsIDragSession.idl
new file mode 100644
index 0000000000..e96e6fc1c7
--- /dev/null
+++ b/widget/nsIDragSession.idl
@@ -0,0 +1,151 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+#include "nsITransferable.idl"
+
+
+
+%{ C++
+#include "nsSize.h"
+%}
+interface nsIContentSecurityPolicy;
+
+native nsSize(nsSize);
+
+webidl DataTransfer;
+webidl WindowContext;
+webidl Node;
+webidl Selection;
+
+[scriptable, builtinclass, uuid(25bce737-73f0-43c7-bc20-c71044a73c5a)]
+interface nsIDragSession : nsISupports
+{
+ /**
+ * Set the current state of the drag, whether it can be dropped or not.
+ * usually the target "frame" sets this so the native system can render the correct feedback
+ */
+ attribute boolean canDrop;
+
+ /**
+ * Indicates if the drop event should be dispatched only to chrome.
+ */
+ attribute boolean onlyChromeDrop;
+
+ /**
+ * Sets the action (copy, move, link, et.c) for the current drag
+ */
+ attribute unsigned long dragAction;
+
+ /**
+ * Get the number of items that were dropped
+ */
+ readonly attribute unsigned long numDropItems;
+
+ /**
+ * The window context where the drag was started, which will be null if the
+ * drag originated outside the application. Useful for determining if a drop
+ * originated in the same window context.
+ */
+ [infallible]
+ attribute WindowContext sourceWindowContext;
+
+ /**
+ * The top-level window context where the drag was started, which will be
+ * null if the drag originated outside the application. Useful for
+ * determining if a drop originated in the same top-level window context.
+ */
+ [infallible]
+ attribute WindowContext sourceTopWindowContext;
+
+ /**
+ * The dom node that was originally dragged to start the session, which will be null if the
+ * drag originated outside the application.
+ */
+ readonly attribute Node sourceNode;
+
+ /**
+ * Replace source node and selection with new ones.
+ * If sourceNode is a native anonymous node, it may be replaced at reframing.
+ * If sourceNode is disconnected from the document, we cannot dispatch
+ * `dragend` event properly.
+ * When this is called, sourceNode or aNewSourceNode should be a native
+ * anonymous node.
+ */
+ [notxpcom, nostdcall] void updateSource(in Node aNewSourceNode,
+ in Selection aNewSelection);
+
+ /**
+ * the triggering principal. This may be different than sourceNode's
+ * principal when sourceNode is xul:browser and the drag is
+ * triggered in a browsing context inside it.
+ */
+ attribute nsIPrincipal triggeringPrincipal;
+
+ /**
+ * the triggering csp. This may be different than sourceNode's
+ * csp when sourceNode is xul:browser and the drag is
+ * triggered in a browsing context inside it.
+ */
+ attribute nsIContentSecurityPolicy csp;
+
+ /**
+ * The data transfer object for the current drag.
+ */
+ [binaryname(DataTransferXPCOM)]
+ attribute DataTransfer dataTransfer;
+ [notxpcom, nostdcall] DataTransfer getDataTransfer();
+ [notxpcom, nostdcall] void setDataTransfer(in DataTransfer aDataTransfer);
+
+ /**
+ * Get data from a Drag&Drop. Can be called while the drag is in process
+ * or after the drop has completed.
+ *
+ * @param aTransferable the transferable for the data to be put into
+ * @param aItemIndex which of multiple drag items, zero-based
+ */
+ void getData ( in nsITransferable aTransferable, in unsigned long aItemIndex ) ;
+
+ /**
+ * Check to set if any of the native data on the clipboard matches this data flavor
+ */
+ boolean isDataFlavorSupported ( in string aDataFlavor ) ;
+
+ void userCancelled();
+
+ void dragEventDispatchedToChildProcess();
+
+ // Called when nsIDragSession implementation should update the UI for the
+ // drag-and-drop based on the data got from the child process in response to
+ // NS_DRAGDROP_OVER sent from parent process to child process.
+ void updateDragEffect();
+
+ // Change the drag image, using similar arguments as
+ // nsIDragService::InvokeDragSessionWithImage.
+ void updateDragImage(in Node aImage, in long aImageX, in long aImageY);
+
+ /**
+ * Returns effects allowed at starting the session for tests.
+ */
+ [notxpcom, nostdcall] unsigned long getEffectAllowedForTests();
+
+ /**
+ * Returns true if current session was started with synthesized drag start.
+ */
+ [notxpcom, nostdcall] bool isSynthesizedForTests();
+
+ /**
+ * Sets drag end point of synthesized session when the test does not dispatch
+ * "drop" event.
+ */
+ void setDragEndPointForTests(in long aScreenX, in long aScreenY);
+
+ /**
+ * Returns true if the session is for dragging text in a text in text control
+ * element.
+ */
+ [notxpcom, nostdcall] bool isDraggingTextInTextControl();
+};
diff --git a/widget/nsIFilePicker.idl b/widget/nsIFilePicker.idl
new file mode 100644
index 0000000000..e9bdedfd42
--- /dev/null
+++ b/widget/nsIFilePicker.idl
@@ -0,0 +1,255 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIFile;
+interface nsIURI;
+interface mozIDOMWindowProxy;
+interface nsISimpleEnumerator;
+webidl BrowsingContext;
+
+// Declared in this file, below.
+interface nsIFilePickerShownCallback;
+
+[scriptable, uuid(9285b984-02d3-46b4-9514-7da8c471a747)]
+interface nsIFilePicker : nsISupports
+{
+ cenum Mode: 16 {
+ modeOpen = 0, // Load a file or directory
+ modeSave = 1, // Save a file or directory
+ modeGetFolder = 2, // Select a folder/directory
+ modeOpenMultiple = 3, // Load multiple files
+ };
+
+ cenum ResultCode: 16 {
+ returnOK = 0, // User hit Ok, process selection
+ returnCancel = 1, // User hit cancel, ignore selection
+ returnReplace = 2, // User acknowledged file already exists so ok to replace, process selection
+ };
+
+ const long filterAll = 0x001; // *.*
+ const long filterHTML = 0x002; // *.html; *.htm
+ const long filterText = 0x004; // *.txt
+ const long filterImages = 0x008; // *.jpe; *.jpg; *.jpeg; *.gif;
+ // *.png; *.bmp; *.ico; *.svg;
+ // *.svgz; *.tif; *.tiff; *.ai;
+ // *.drw; *.pct; *.psp; *.xcf;
+ // *.psd; *.raw; *.webp; *.heic
+ const long filterXML = 0x010; // *.xml
+ const long filterXUL = 0x020; // *.xul
+ const long filterApps = 0x040; // Applications (per-platform implementation)
+ const long filterAllowURLs = 0x080; // Allow URLs
+ const long filterAudio = 0x100; // *.aac; *.aif; *.flac; *.iff;
+ // *.m4a; *.m4b; *.mid; *.midi;
+ // *.mp3; *.mpa; *.mpc; *.oga;
+ // *.ogg; *.ra; *.ram; *.snd;
+ // *.wav; *.wma
+ const long filterVideo = 0x200; // *.avi; *.divx; *.flv; *.m4v;
+ // *.mkv; *.mov; *.mp4; *.mpeg;
+ // *.mpg; *.ogm; *.ogv; *.ogx;
+ // *.rm; *.rmvb; *.smil; *.webm;
+ // *.wmv; *.xvid
+ const long filterPDF = 0x400; // *.pdf;
+
+ cenum CaptureTarget: 8 {
+ captureNone = 0, // No capture target specified.
+ captureDefault = 1, // Missing/invalid value default.
+ captureUser = 2, // "user" capture target specified.
+ captureEnv = 3, // "environment" capture target specified.
+ };
+
+ /**
+ * Initialize the file picker widget. The file picker is not valid until this
+ * method is called.
+ *
+ * @param parent mozIDOMWindow parent. This dialog will be dependent
+ * on this parent. parent must be non-null.
+ * @param title The title for the file widget
+ * @param mode load, save, or get folder
+ * @param browsingContext [optional]
+ * The context in which the file picker is being shown. This is
+ * used for content analysis and can be omitted if chrome is
+ * showing the file picker.
+ */
+ void init(in mozIDOMWindowProxy parent,
+ in AString title,
+ in nsIFilePicker_Mode mode,
+ [optional] in BrowsingContext browsingContext);
+
+ /**
+ * Returns a Promise that resolves to true if the passed nsIFilePicker mode
+ * is supported on the current platform, and false otherwise. The Promise may
+ * reject if something unexpected occurs while trying to determine if the mode
+ * is supported.
+ *
+ * @param mode
+ * @return Promise<boolean>
+ */
+ [implicit_jscontext]
+ Promise isModeSupported(in nsIFilePicker_Mode mode);
+
+ /**
+ * Append to the filter list with things from the predefined list
+ *
+ * @param filters mask of filters i.e. (filterAll | filterHTML)
+ *
+ */
+ void appendFilters(in long filterMask);
+
+ /**
+ * Add a filter
+ *
+ * @param title name of the filter
+ * @param filter extensions to filter -- semicolon and space separated
+ *
+ */
+ void appendFilter(in AString title,
+ in AString filter);
+
+ /**
+ * Add a raw filter (eg, add a MIME type without transforming it to a list of
+ * extensions).
+ *
+ * @param filter a filter taken directly from the accept attribute
+ * without processing
+ *
+ */
+ void appendRawFilter(in AString filter);
+
+ /**
+ * The filename that should be suggested to the user as a default. This should
+ * include the extension.
+ *
+ * @throws NS_ERROR_FAILURE on attempts to get
+ */
+ attribute AString defaultString;
+
+ /**
+ * The extension that should be associated with files of the type we
+ * want to work with. On some platforms, this extension will be
+ * automatically appended to filenames the user enters, if needed.
+ */
+ attribute AString defaultExtension;
+
+ /**
+ * The filter which is currently selected in the File Picker dialog
+ *
+ * @return Returns the index (0 based) of the selected filter in the filter list.
+ */
+ attribute long filterIndex;
+
+ /**
+ * Set the directory that the file open/save dialog initially displays
+ * Note that, if displaySpecialDirectory has been already set, this value will
+ * be ignored.
+ *
+ * @param displayDirectory the name of the directory
+ *
+ */
+ attribute nsIFile displayDirectory;
+
+ /**
+ * Set the directory that the file open/save dialog initially displays using
+ * one of the special name as such as 'Desk', 'TmpD', and so on.
+ * Note that, if displayDirectory has been already set, this value will be
+ * ignored.
+ *
+ * @param displaySpecialDirectory the name of the special directory
+ *
+ */
+ attribute AString displaySpecialDirectory;
+
+
+ /**
+ * Get the nsIFile for the file or directory. A different file object
+ * may be returned by each invocation.
+ *
+ * @return Returns the file currently selected
+ */
+ readonly attribute nsIFile file;
+
+ /**
+ * Get the nsIURI for the file or directory.
+ *
+ * @return Returns the file currently selected
+ */
+ readonly attribute nsIURI fileURL;
+
+ /**
+ * Get the enumerator for the selected files
+ * only works in the modeOpenMultiple mode
+ *
+ * @return Returns the files currently selected
+ */
+ readonly attribute nsISimpleEnumerator files;
+
+ /**
+ * Get the DOM File or the DOM Directory
+ *
+ * @return Returns the file or directory currently selected DOM object.
+ */
+ readonly attribute nsISupports domFileOrDirectory;
+
+ /**
+ * Get the enumerator for the selected files or directories
+ * only works in the modeOpenMultiple mode
+ *
+ * @return Returns the files/directories currently selected as DOM object.
+ */
+ readonly attribute nsISimpleEnumerator domFileOrDirectoryEnumerator;
+
+ /**
+ * Controls whether the chosen file(s) should be added to the system's recent
+ * documents list. This attribute will be ignored if the system has no "Recent
+ * Docs" concept, or if the application is in private browsing mode (in which
+ * case the file will not be added). Defaults to true.
+ */
+ attribute boolean addToRecentDocs;
+
+ /**
+ * Opens the file dialog asynchrounously.
+ * The passed in object's done method will be called upon completion.
+ */
+ void open(in nsIFilePickerShownCallback aFilePickerShownCallback);
+
+ /**
+ * Closes the file dialog if open.
+ */
+ void close();
+
+ /**
+ * The picker's mode, as set by the 'mode' argument passed to init()
+ * (one of the modeOpen et. al. constants specified above).
+ */
+ readonly attribute nsIFilePicker_Mode mode;
+
+ /**
+ * If set to non-empty string, the nsIFilePicker implementation
+ * may use okButtonLabel as the label for the button the user uses to accept
+ * file selection.
+ */
+ attribute AString okButtonLabel;
+
+ /**
+ * Implementation of HTMLInputElement's `capture` property.
+ *
+ * Not used by Firefox Desktop.
+ */
+ attribute nsIFilePicker_CaptureTarget capture;
+};
+
+[scriptable, function, uuid(0d79adad-b244-49A5-9997-2a8cad93fc44)]
+interface nsIFilePickerShownCallback : nsISupports
+{
+ /**
+ * Callback which is called when a filepicker is shown and a result
+ * is returned.
+ *
+ * @param aResult One of returnOK, returnCancel, or returnReplace
+ */
+ void done(in nsIFilePicker_ResultCode aResult);
+};
diff --git a/widget/nsIFormatConverter.idl b/widget/nsIFormatConverter.idl
new file mode 100644
index 0000000000..0d72f49d2a
--- /dev/null
+++ b/widget/nsIFormatConverter.idl
@@ -0,0 +1,50 @@
+/* -*- 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 "nsISupports.idl"
+
+[scriptable, uuid(948A0023-E3A7-11d2-96CF-0060B0FB9956)]
+interface nsIFormatConverter : nsISupports
+{
+ /**
+ * Get the list of the "input" data flavors (mime types as nsISupportsCString),
+ * in otherwords, the flavors that this converter can convert "from" (the
+ * incoming data to the converter).
+ */
+ Array<ACString> getInputDataFlavors ( ) ;
+
+ /**
+ * Get the list of the "output" data flavors (mime types as nsISupportsCString),
+ * in otherwords, the flavors that this converter can convert "to" (the
+ * outgoing data to the converter).
+ *
+ * @param aDataFlavorList fills list with supported flavors
+ */
+ Array<ACString> getOutputDataFlavors ( ) ;
+
+ /**
+ * Determines whether a conversion from one flavor to another is supported
+ *
+ * @param aFromFormatConverter flavor to convert from
+ * @param aFromFormatConverter flavor to convert to
+ */
+ boolean canConvert ( in string aFromDataFlavor, in string aToDataFlavor ) ;
+
+ /**
+ * Converts from one flavor to another.
+ *
+ * @param aFromFormatConverter flavor to convert from
+ * @param aFromFormatConverter flavor to convert to (destination own the memory)
+ * @returns returns NS_OK if it was converted
+ */
+ void convert ( in string aFromDataFlavor, in nsISupports aFromData,
+ in string aToDataFlavor, out nsISupports aToData ) ;
+
+};
+
+
+%{ C++
+
+%}
diff --git a/widget/nsIGfxInfo.idl b/widget/nsIGfxInfo.idl
new file mode 100644
index 0000000000..4f4f0b7e1e
--- /dev/null
+++ b/widget/nsIGfxInfo.idl
@@ -0,0 +1,366 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+/* NOTE: this interface is completely undesigned, not stable and likely to change */
+[scriptable, builtinclass, uuid(1accd618-4c80-4703-9d29-ecf257d397c8)]
+interface nsIGfxInfo : nsISupports
+{
+ /*
+ * These are win32-specific
+ */
+ readonly attribute boolean D2DEnabled;
+ readonly attribute boolean DWriteEnabled;
+ readonly attribute boolean EmbeddedInFirefoxReality;
+ readonly attribute AString AzureCanvasBackend;
+ readonly attribute AString AzureContentBackend;
+ readonly attribute boolean usingGPUProcess;
+ readonly attribute boolean hasBattery;
+ readonly attribute AString DWriteVersion;
+ readonly attribute AString cleartypeParameters;
+
+ /*
+ * These are non-Android linux-specific
+ */
+ readonly attribute AString windowProtocol;
+ readonly attribute AString testType;
+
+ /*
+ * These are valid across all platforms.
+ */
+ readonly attribute AString ContentBackend;
+ readonly attribute boolean isHeadless;
+ readonly attribute unsigned long TargetFrameRate;
+ readonly attribute ACString CodecSupportInfo;
+
+ // XXX: Switch to a list of devices, rather than explicitly numbering them.
+
+ // Present on all platforms, but only useful on Android
+ cenum FontVisibilityDeviceDetermination : 8 {
+ Unassigned = 0,
+ Unknown_Platform = 1,
+ Windows_Platform = 2,
+ MacOS_Platform = 3,
+ Android_Unknown_Release_Version = 4,
+ Android_Unknown_Peloton = 5,
+ Android_Unknown_vbox = 6,
+ Android_Unknown_mitv = 7,
+ Android_Chromebook = 8,
+ Android_Amazon = 9,
+ Android_sub_9 = 10,
+ Android_9_11 = 11,
+ Android_12_plus = 12,
+ Linux_Unknown = 13,
+ Linux_Ubuntu_any = 14,
+ Linux_Ubuntu_20 = 15,
+ Linux_Ubuntu_22 = 16,
+ Linux_Fedora_any = 17,
+ Linux_Fedora_38 = 18,
+ Linux_Fedora_39 = 19
+ };
+ readonly attribute nsIGfxInfo_FontVisibilityDeviceDetermination fontVisibilityDetermination;
+ readonly attribute AString fontVisibilityDeterminationStr;
+
+ /**
+ * The name of the display adapter.
+ */
+ readonly attribute AString adapterDescription;
+ readonly attribute AString adapterDescription2;
+
+ readonly attribute AString adapterDriver;
+ readonly attribute AString adapterDriver2;
+
+ /* These types are inspired by DXGI_ADAPTER_DESC */
+ readonly attribute AString adapterVendorID;
+ readonly attribute AString adapterVendorID2;
+
+ readonly attribute AString adapterDeviceID;
+ readonly attribute AString adapterDeviceID2;
+
+ readonly attribute AString adapterSubsysID;
+ readonly attribute AString adapterSubsysID2;
+
+ /**
+ * The amount of RAM in MB in the display adapter.
+ */
+ readonly attribute unsigned long adapterRAM;
+ readonly attribute unsigned long adapterRAM2;
+
+ readonly attribute AString adapterDriverVendor;
+ readonly attribute AString adapterDriverVendor2;
+
+ readonly attribute AString adapterDriverVersion;
+ readonly attribute AString adapterDriverVersion2;
+
+ readonly attribute AString adapterDriverDate;
+ readonly attribute AString adapterDriverDate2;
+
+ readonly attribute boolean isGPU2Active;
+
+ readonly attribute ACString drmRenderDevice;
+
+ /**
+ * Returns an array of objects describing each monitor. Guaranteed properties
+ * are "screenWidth" and "screenHeight". This is only implemented on Desktop.
+ *
+ * Windows additionally supplies "refreshRate" and "pseudoDisplay".
+ *
+ * OS X additionally supplies "scale".
+ */
+ [implicit_jscontext]
+ jsval getMonitors();
+
+ Array<ACString> getFailures(out Array<long> indices);
+
+ [noscript, notxpcom] void logFailure(in ACString failure);
+ /*
+ * A set of constants for features that we can ask this GfxInfo object
+ * about via GetFeatureStatus
+ */
+ /* Don't assign any value <= 0 */
+ /* Values must be contiguous */
+ /* Whether Direct2D is supported for content rendering. */
+ const long FEATURE_DIRECT2D = 1;
+ /* Whether Direct3D 9 is supported for layers. */
+ const long FEATURE_DIRECT3D_9_LAYERS = 2;
+ /* Whether Direct3D 10.0 is supported for layers. */
+ const long FEATURE_DIRECT3D_10_LAYERS = 3;
+ /* Whether Direct3D 10.1 is supported for layers. */
+ const long FEATURE_DIRECT3D_10_1_LAYERS = 4;
+ /* Whether OpenGL is supported for layers */
+ const long FEATURE_OPENGL_LAYERS = 5;
+ /* Whether WebGL is supported via OpenGL. */
+ const long FEATURE_WEBGL_OPENGL = 6;
+ /* Whether WebGL is supported via ANGLE (D3D9 -- does not check for the presence of ANGLE libs). */
+ const long FEATURE_WEBGL_ANGLE = 7;
+ /* (Unused) Whether WebGL antialiasing is supported. */
+ const long UNUSED_FEATURE_WEBGL_MSAA = 8;
+ /* Whether Stagefright is supported, starting in 17. */
+ const long FEATURE_STAGEFRIGHT = 9;
+ /* Whether Webrtc Hardware H.264 acceleration is supported, starting in 71. */
+ const long FEATURE_WEBRTC_HW_ACCELERATION_H264 = 10;
+ /* Whether Direct3D 11 is supported for layers, starting in 32. */
+ const long FEATURE_DIRECT3D_11_LAYERS = 11;
+ /* Whether hardware accelerated video decoding is supported, starting in 36. */
+ const long FEATURE_HARDWARE_VIDEO_DECODING = 12;
+ /* Whether Direct3D 11 is supported for ANGLE, starting in 38. */
+ const long FEATURE_DIRECT3D_11_ANGLE = 13;
+ /* Whether Webrtc Hardware acceleration is supported, starting in 42. */
+ const long FEATURE_WEBRTC_HW_ACCELERATION_ENCODE = 14;
+ /* Whether Webrtc Hardware acceleration is supported, starting in 42. */
+ const long FEATURE_WEBRTC_HW_ACCELERATION_DECODE = 15;
+ /* Whether Canvas acceleration is supported, starting in 45 */
+ const long FEATURE_CANVAS2D_ACCELERATION = 16;
+ /* Whether hardware VP8 decoding is supported, starting in 48; not for downloadable blocking. */
+ const long FEATURE_VP8_HW_DECODE = 17;
+ /* Whether hardware VP9 decoding is supported, starting in 48; not for downloadable blocking. */
+ const long FEATURE_VP9_HW_DECODE = 18;
+ /* Whether NV_dx_interop2 is supported, starting in 50; downloadable blocking in 58. */
+ const long FEATURE_DX_INTEROP2 = 19;
+ /* Whether the GPU process is supported, starting in 52; downloadable blocking in 58. */
+ const long FEATURE_GPU_PROCESS = 20;
+ /* Whether the WebGL2 is supported, starting in 54 */
+ const long FEATURE_WEBGL2 = 21;
+ /* Whether D3D11 keyed mutex is supported, starting in 56 */
+ const long FEATURE_D3D11_KEYED_MUTEX = 22;
+ /* Whether WebRender is supported, starting in 62 */
+ const long FEATURE_WEBRENDER = 23;
+ /* Whether WebRender is supported, starting in 62 */
+ const long FEATURE_DX_NV12 = 24;
+ const long FEATURE_DX_P010 = 25;
+ const long FEATURE_DX_P016 = 26;
+ /* Whether OpenGL swizzle configuration of texture units is supported, starting in 70 */
+ const long FEATURE_GL_SWIZZLE = 27;
+ /* Whether WebRender native compositor is supported, starting in 73 */
+ const long FEATURE_WEBRENDER_COMPOSITOR = 28;
+ /* Whether WebRender can use scissored clears for cached surfaces, staring in 79 */
+ const long FEATURE_WEBRENDER_SCISSORED_CACHE_CLEARS = 29;
+ /* Support webgl.out-of-process: true (starting in 83) */
+ const long FEATURE_ALLOW_WEBGL_OUT_OF_PROCESS = 30;
+ /* Is OpenGL threadsafe (starting in 83) */
+ const long FEATURE_THREADSAFE_GL = 31;
+ /* Whether webrender uses pre-optimized shaders, starting in 87. */
+ const long FEATURE_WEBRENDER_OPTIMIZED_SHADERS = 32;
+ /* Whether we prefer EGL over GLX with X11, starting in 88. */
+ const long FEATURE_X11_EGL = 33;
+ /* Whether DMABUF is supported, starting in 88. */
+ const long FEATURE_DMABUF = 34;
+ /* Whether webrender caches shader program binaries to disk, starting in 89. */
+ const long FEATURE_WEBRENDER_SHADER_CACHE = 35;
+ /* Whether partial present is allowed with WebRender, starting in 98. */
+ const long FEATURE_WEBRENDER_PARTIAL_PRESENT = 36;
+ /* Whether WebGPU is supported, starting in 100. */
+ const long FEATURE_WEBGPU = 37;
+ /* Whether video overlay of hardware decoded video is supported, starting in 100. */
+ const long FEATURE_VIDEO_OVERLAY = 38;
+ /* Whether hardware decoded video zero copy is supported, starting in 101. */
+ const long FEATURE_HW_DECODED_VIDEO_ZERO_COPY = 39;
+ /* Whether DMABUF export is supported, starting in 103. */
+ const long FEATURE_DMABUF_SURFACE_EXPORT = 40;
+ /* Whether reuse decoder device is supported, starting in 104. */
+ const long FEATURE_REUSE_DECODER_DEVICE = 41;
+ /* Whether to allow backdrop filter, starting in 105. */
+ const long FEATURE_BACKDROP_FILTER = 42;
+ /* Whether to use Accelerated Canvas2D, starting in 110. */
+ const long FEATURE_ACCELERATED_CANVAS2D = 43;
+ /* Whether hardware H264 decoding is supported, starting in 114; not for downloadable blocking. */
+ const long FEATURE_H264_HW_DECODE = 44;
+ /* Whether hardware AV1 decoding is supported, starting in 114; not for downloadable blocking. */
+ const long FEATURE_AV1_HW_DECODE = 45;
+ /* Whether video overlay of software decoded video is supported, starting in 116. */
+ const long FEATURE_VIDEO_SOFTWARE_OVERLAY = 46;
+ /* Whether WebGL is allowed to use hardware rendering, otherwise software fallbacks. */
+ const long FEATURE_WEBGL_USE_HARDWARE = 47;
+ /* the maximum feature value. */
+ const long FEATURE_MAX_VALUE = FEATURE_WEBGL_USE_HARDWARE;
+
+ /*
+ * A set of return values from GetFeatureStatus
+ */
+
+ /* The driver is safe to the best of our knowledge */
+ const long FEATURE_STATUS_OK = 1;
+ /* We don't know the status of the feature yet. The analysis probably hasn't finished yet. */
+ const long FEATURE_STATUS_UNKNOWN = 2;
+ /* This feature is blocked on this driver version. Updating driver will typically unblock it. */
+ const long FEATURE_BLOCKED_DRIVER_VERSION = 3;
+ /* This feature is blocked on this device, regardless of driver version.
+ * Typically means we hit too many driver crashes without a good reason to hope for them to
+ * get fixed soon. */
+ const long FEATURE_BLOCKED_DEVICE = 4;
+ /* This feature is available and can be used, but is not suggested (e.g. shouldn't be used by default */
+ const long FEATURE_DISCOURAGED = 5;
+ /* This feature is blocked on this OS version. */
+ const long FEATURE_BLOCKED_OS_VERSION = 6;
+ /* This feature is blocked because of mismatched driver versions. */
+ const long FEATURE_BLOCKED_MISMATCHED_VERSION = 7;
+ /* This feature is blocked due to not being on the allowlist. */
+ const long FEATURE_DENIED = 8;
+ /* This feature is safe to be on this device due to the allowlist. */
+ const long FEATURE_ALLOW_ALWAYS = 9;
+ /* This feature is safe to be on this device due to the allowlist, depending on qualified/experiment status. */
+ const long FEATURE_ALLOW_QUALIFIED = 10;
+ /* This feature failed in a startup test, e.g. due to a crashing driver. */
+ const long FEATURE_BLOCKED_PLATFORM_TEST = 11;
+
+ /**
+ * Ask about a feature, and return the status of that feature.
+ * If the feature is not ok then aFailureId will give a unique failure Id
+ * otherwise it will be empty.
+ */
+ long getFeatureStatus(in long aFeature, [optional] out ACString aFailureId);
+
+ /*
+ * Ask about a feature, return the minimum driver version required for it if its status is
+ * FEATURE_BLOCKED_DRIVER_VERSION, otherwise return an empty string.
+ */
+ AString getFeatureSuggestedDriverVersion(in long aFeature);
+
+ // only useful on X11
+ [noscript, notxpcom] void GetData();
+
+ /**
+ * Maximum refresh rate among detected monitors. -1 if unknown. aMixed is set
+ * to true if we know there are multiple displays and they have different
+ * refresh rates, else false. The returned value is in Hz.
+ */
+ [noscript, notxpcom] long GetMaxRefreshRate(out boolean aMixed);
+
+ [implicit_jscontext]
+ jsval getInfo();
+
+ // Return an object describing all features that have been configured:
+ //
+ // "features": [
+ // // For each feature:
+ // {
+ // "name": <string>,
+ // "description": <string>,
+ // "status": <string>,
+ // "log": [
+ // // One or more log entries, the first denotes the default value.
+ // {
+ // "type": <string>, // "base", "user", "env", or "runtime"
+ // "status": <string>,
+ // "message": <string> // Set unless type is "base" and status is "available".
+ // }
+ // ]
+ // }
+ // ]
+ // "fallbacks": [
+ // // For each workaround:
+ // {
+ // "name:" <string>,
+ // "description": <string>,
+ // "message": <string>
+ // ]
+ // }
+ //
+ // When a message is prefixed with a '#', it is a special status code. Status
+ // codes are unique identifiers that can be searched in the codebase to find
+ // which line of code caused the message. Some codes are standardized to
+ // improve about:support messaging:
+ //
+ // "[CONTEXT_]FEATURE_FAILURE_BUG_<number>"
+ // CONTEXT is optional and can currently only be "BLOCKLIST".
+ // <number> refers to a bug number in Bugzilla.
+ //
+ [implicit_jscontext]
+ jsval getFeatureLog();
+
+ // Returns an object containing information about graphics features. It is
+ // intended to be directly included into the Telemetry environment.
+ //
+ // "layers":
+ // {
+ // "compositor": "d3d9", "d3d11", "opengl", "basic", or "none"
+ // // ("none" indicates no compositors have been created)
+ // // Feature is one of "d3d9", "d3d11", "opengl", "basic", or "d2d".
+ // "<feature>": {
+ // // Each backend can have one of the following statuses:
+ // // "unused" - This feature has not been requested.
+ // // "unavailable" - OS version or restriction prevents use.
+ // // "blocked" - An internal condition (such as safe mode) prevents use.
+ // // "blocklisted" - Blocked due to a blocklist restriction.
+ // // "denied" - Blocked due to allowlist restrictions.
+ // // "disabled" - User explicitly disabled this default feature.
+ // // "failed" - Feature failed to initialize.
+ // // "available" - User has this feature available by default.
+ // "status": "<status>",
+ // "version": "<version>",
+ // "warp": true|false, // D3D11 only.
+ // "textureSharing": true|false, // D3D11 only.
+ // }
+ // }
+ [implicit_jscontext]
+ jsval getFeatures();
+
+ // Returns an array listing any active crash guards.
+ //
+ // [
+ // {
+ // // Type is one of "d3d11layers", or "glcontext".
+ // "type": "<identifier>",
+ //
+ // // Preference that must be deleted/reset to retrigger the guard.
+ // "prefName": "<preference>",
+ // }
+ // ]
+ [implicit_jscontext]
+ jsval getActiveCrashGuards();
+
+ // Forces the GPU process to start or shutdown. This is intended only for
+ // xpcshell-tests.
+ boolean controlGPUProcessForXPCShell(in boolean aEnable);
+
+ // Kills the GPU process cleanly, without generating a crash dump.
+ // This is intended only for use by tests.
+ void killGPUProcessForTests();
+
+ // Causes the GPU process to crash. This is intended only for use by tests.
+ void crashGPUProcessForTests();
+};
diff --git a/widget/nsIGfxInfoDebug.idl b/widget/nsIGfxInfoDebug.idl
new file mode 100644
index 0000000000..894dfa97ae
--- /dev/null
+++ b/widget/nsIGfxInfoDebug.idl
@@ -0,0 +1,19 @@
+/* -*- 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 "nsISupports.idl"
+
+/* NOTE: this interface is only implemented in debug builds */
+
+[scriptable, uuid(ca7b0bc7-c67c-4b79-8270-ed7ba002af08)]
+interface nsIGfxInfoDebug : nsISupports
+{
+ void spoofVendorID(in AString aVendorID);
+ void spoofDeviceID(in AString aDeviceID);
+
+ void spoofDriverVersion(in AString aDriverVersion);
+
+ void spoofOSVersion(in unsigned long aVersion);
+};
diff --git a/widget/nsIGtkTaskbarProgress.idl b/widget/nsIGtkTaskbarProgress.idl
new file mode 100644
index 0000000000..ebefe5e3a1
--- /dev/null
+++ b/widget/nsIGtkTaskbarProgress.idl
@@ -0,0 +1,22 @@
+/* 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 "nsITaskbarProgress.idl"
+
+interface mozIDOMWindowProxy;
+
+/**
+ * Allow the TaskbarProgress instance to set a new target window.
+ */
+
+[scriptable, uuid(39f6fc5a-2386-4bc6-941c-d7479253bc3f)]
+interface nsIGtkTaskbarProgress : nsITaskbarProgress
+{
+ /**
+ * Sets the window that is considered primary for purposes of
+ * setting the XApp progress property.
+ */
+
+ void setPrimaryWindow(in mozIDOMWindowProxy aWindow);
+};
diff --git a/widget/nsIJumpListBuilder.idl b/widget/nsIJumpListBuilder.idl
new file mode 100644
index 0000000000..1b7b93e5e7
--- /dev/null
+++ b/widget/nsIJumpListBuilder.idl
@@ -0,0 +1,119 @@
+/* -*- 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 "nsISupports.idl"
+interface nsIURI;
+
+[scriptable, uuid(5769F08D-0303-4E38-8FE6-86B5473022F6)]
+interface nsIJumpListBuilder : nsISupports
+{
+ /**
+ * Returns the local filesystem path for a favicon for a page hosted at
+ * faviconURL if we happen to have written one to disk before. If we have not,
+ * then a background thread retrieves the favicon and will write it to disk
+ * and NS_ERROR_NOT_AVAILABLE will be thrown.
+ *
+ * @param {nsIURI} faviconURL
+ * The URL for the web page for which we would like a filesystem path for
+ * the favicon.
+ * @returns {AString}
+ * The local filesystem path for the favicon if it has been cached before.
+ * If it has not been cached before, this method will throw
+ * NS_ERROR_NOT_AVAILABLE.
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * In the event that the favicon has never been cached to disk before.
+ */
+ AString obtainAndCacheFavicon(in nsIURI faviconURL);
+
+ /**
+ * Returns a Promise that resolves with whether or not the Jump List backend
+ * on the background thread is up and running.
+ *
+ * @returns {Promise<boolean>}
+ * Resolves to true if the backend is ready to accept
+ * WindowsJumpListShortcutDescriptions. False, otherwise.
+ * @throws NS_ERROR_FAILURE
+ * If an attempt to communicate with the background thread fails.
+ */
+ [implicit_jscontext]
+ Promise isAvailable();
+
+ /**
+ * Asks the Windows Jump List API for any items that might have been removed
+ * by the user from the Jump List UI.
+ *
+ * Important: This should be called prior to any attempt to call
+ * `populateJumpList` to ensure that any passed in
+ * WindowsJumpListShortcutDescriptions do not describe items that the user has
+ * just removed. Failing to do so will cause the Promise returned from
+ * `populateJumpList` to reject. This is a constraint of the underlying win32
+ * API. Please see
+ * https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
+ * for more details.
+ *
+ * @returns {Promise<string[], nsresult>}
+ * On success, will return an array of strings for URLs of history that
+ * have been removed by the user via the Windows Jump List. These items will
+ * also have had their cached favicons removed from the disk off of the
+ * main thread. On failure, this will reject with the nsresult failure code.
+ * @throws NS_ERROR_FAILURE
+ * If an attempt to communicate with the background thread fails.
+ */
+ [implicit_jscontext]
+ Promise checkForRemovals();
+
+ /**
+ * Writes a new set of items to the Windows Jump List. This occurs
+ * asynchronously, off of the main thread.
+ *
+ * Important: Callers should first call `checkForRemovals` to remove any
+ * browsing history items that the user chose to remove in the Jump List
+ * Only then should any WindowsJumpListShortcutDescriptions be created
+ * and passed to this method. Any attempt to add
+ * WindowsJumpListShortcutDescriptions matching items that have been removed
+ * by the user will result in the returned Promise rejecting. This is a
+ * constraint of the underlying win32 API. Please see
+ * https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
+ * for more details.
+ *
+ * @param {WindowsJumpListShortcutDescription[]} aTaskDescriptions
+ * 0 or more WindowsJumpListShortcutDescriptions to place items within the
+ * "tasks" section of the Jump List.
+ * @param {AString} aCustomTitle
+ * An optional title for a custom sub-list within the Jump List that will be
+ * populated via aCustomDescriptions. This must be supplied if
+ * aCustomDescriptions is not empty.
+ * @param {WindowsJumpListShortcutDescription[]} aCustomDescriptions
+ * 0 or more WindowsJumpListShortcutDescriptions to place items within the
+ * custom section of the Jump List. aCustomTitle must be supplied if this
+ * array is non-empty.
+ * @returns {Promise<undefined, nsresult>}
+ * Returns a Promise that resolves if the Jump List was properly written
+ * to, and rejects otherwise with the nsresult of the failure.
+ * @throws NS_ERROR_INVALID_ARG
+ * If any of the passed arguments do not meet the requirements set out
+ * above.
+ * @throws NS_ERROR_FAILURE
+ * If an attempt to communicate with the background thread fails.
+ */
+ [implicit_jscontext]
+ Promise populateJumpList(
+ in Array<jsval> aTaskDescriptions,
+ in AString aCustomTitle,
+ in Array<jsval> aCustomDescriptions
+ );
+
+ /**
+ * Removes all items from the Jump List.
+ *
+ * @returns {Promise<undefined, nsresult>}
+ * Resolves with undefined on successfully clearing the Jump List. If it
+ * fails to do so, it will reject with the failure code.
+ * @throws NS_ERROR_FAILURE
+ * If an attempt to communicate with the background thread fails.
+ */
+ [implicit_jscontext]
+ Promise clearJumpList();
+};
diff --git a/widget/nsILegacyJumpListBuilder.idl b/widget/nsILegacyJumpListBuilder.idl
new file mode 100644
index 0000000000..999e658707
--- /dev/null
+++ b/widget/nsILegacyJumpListBuilder.idl
@@ -0,0 +1,161 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIArray;
+
+[scriptable, function, uuid(5131a62a-e99f-4631-9138-751f8aad1ae4)]
+interface nsILegacyJumpListCommittedCallback : nsISupports
+{
+ void done(in boolean result);
+};
+
+[scriptable, uuid(1FE6A9CD-2B18-4dd5-A176-C2B32FA4F683)]
+interface nsILegacyJumpListBuilder : nsISupports
+{
+ /**
+ * JumpLists
+ *
+ * Jump lists are built and then applied. Modifying an applied jump list is not
+ * permitted. Callers should begin the creation of a new jump list using
+ * initListBuild, add sub lists using addListToBuild, then commit the jump list
+ * using commitListBuild. Lists are built in real-time during the sequence of
+ * build calls, make sure to check for errors on each individual step.
+ *
+ * The default number of allowed items in a jump list is ten. Users can change
+ * the number through system preferences. User may also pin items to jump lists,
+ * which take up additional slots. Applications do not have control over the
+ * number of items allowed in jump lists; excess items added are dropped by the
+ * system. Item insertion priority is defined as first to last added.
+ *
+ * Users may remove items from jump lists after they are commited. The system
+ * tracks removed items between commits. A list of these items is returned by
+ * a call to initListBuild. nsILegacyJumpListBuilder does not filter entries added that
+ * have been removed since the last commit. To prevent repeatedly adding entries
+ * users have removed, applications are encoraged to track removed items
+ * internally.
+ *
+ * Each list is made up of an array of nsILegacyJumpListItem representing items
+ * such as shortcuts, links, and separators. See nsILegacyJumpListItem for information
+ * on adding additional jump list types.
+ */
+
+ /**
+ * List Types
+ */
+
+ /**
+ * Task List
+ *
+ * Tasks are common actions performed by users within the application. A task
+ * can be represented by an application shortcut and associated command line
+ * parameters or a URI. Task lists should generally be static lists that do not
+ * change often, if at all - similar to an application menu.
+ *
+ * Tasks are given the highest priority of all lists when space is limited.
+ */
+ const short JUMPLIST_CATEGORY_TASKS = 0;
+
+ /**
+ * Recent or Frequent list
+ *
+ * Recent and frequent lists are based on Window's recent document lists. The
+ * lists are generated automatically by Windows. Applications that use recent
+ * or frequent lists should keep document use tracking up to date by calling
+ * the SHAddToRecentDocs shell api.
+ */
+ const short JUMPLIST_CATEGORY_RECENT = 1;
+ const short JUMPLIST_CATEGORY_FREQUENT = 2;
+
+ /**
+ * Custom Lists
+ *
+ * Custom lists can be made up of tasks, links, and separators. The title of
+ * of the list is passed through the optional string parameter of addBuildList.
+ */
+ const short JUMPLIST_CATEGORY_CUSTOMLIST = 3;
+
+ /**
+ * Indicates whether jump list taskbar features are supported by the current
+ * host.
+ */
+ readonly attribute short available;
+
+ /**
+ * JumpList management
+ *
+ * @throw NS_ERROR_NOT_AVAILABLE on all calls if taskbar functionality
+ * is not supported by the operating system.
+ */
+
+ /**
+ * Indicates if a commit has already occurred in this session.
+ */
+ readonly attribute boolean isListCommitted;
+
+ /**
+ * The maximum number of jump list items the current desktop can support.
+ */
+ readonly attribute short maxListItems;
+
+ /**
+ * Initializes a jump list build and returns a promise with the list of
+ * items the user removed since the last time a jump list was committed.
+ * Removed items can become state after initListBuild is called, lists
+ * should be built in single-shot fasion.
+ *
+ * @returns a promise with the list of items that were removed by the user
+ * since the last commit.
+ */
+ [implicit_jscontext]
+ Promise initListBuild();
+
+ /**
+ * Adds a list and if required, a set of items for the list.
+ *
+ * @param aCatType
+ * The type of list to add.
+ * @param items
+ * An array of nsILegacyJumpListItem items to add to the list.
+ * @param catName
+ * For custom lists, the title of the list.
+ *
+ * @returns true if the operation completed successfully.
+ *
+ * @throw NS_ERROR_INVALID_ARG if incorrect parameters are passed for
+ * a particular category or item type.
+ * @throw NS_ERROR_ILLEGAL_VALUE if an item is added that was removed
+ * since the last commit.
+ * @throw NS_ERROR_UNEXPECTED on internal errors.
+ */
+ boolean addListToBuild(in short aCatType, [optional] in nsIArray items, [optional] in AString catName);
+
+ /**
+ * Aborts and clears the current jump list build.
+ */
+ void abortListBuild();
+
+ /**
+ * Commits the current jump list build to the Taskbar.
+ *
+ * @param callback
+ * Receives one argument, which is true if the operation completed
+ * successfully, otherwise it is false.
+ */
+ void commitListBuild([optional] in nsILegacyJumpListCommittedCallback callback);
+
+ /**
+ * Deletes any currently applied taskbar jump list for this application.
+ * Common uses would be the enabling of a privacy mode and uninstallation.
+ *
+ * @returns true if the operation completed successfully.
+ *
+ * @throw NS_ERROR_UNEXPECTED on internal errors.
+ */
+ boolean deleteActiveList();
+
+ void setAppUserModelID(in AString aAppUserModelId);
+};
diff --git a/widget/nsILegacyJumpListItem.idl b/widget/nsILegacyJumpListItem.idl
new file mode 100644
index 0000000000..29011215ac
--- /dev/null
+++ b/widget/nsILegacyJumpListItem.idl
@@ -0,0 +1,120 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURI;
+interface nsILocalHandlerApp;
+interface nsIMutableArray;
+
+/**
+ * Implements Win7 Taskbar jump list item interfaces.
+ *
+ * Note to consumers: it's reasonable to expect we'll need support for other types
+ * of jump list items (an audio file, an email message, etc.). To add types,
+ * create the specific interface here, add an implementation class to WinJumpListItem,
+ * and add support to addListBuild & removed items processing.
+ *
+ */
+
+[scriptable, uuid(ACB8FB3C-E1B0-4044-8A50-E52C3E7C1057)]
+interface nsILegacyJumpListItem : nsISupports
+{
+ const short JUMPLIST_ITEM_EMPTY = 0; // Empty list item
+ const short JUMPLIST_ITEM_SEPARATOR = 1; // Separator
+ const short JUMPLIST_ITEM_LINK = 2; // Web link item
+ const short JUMPLIST_ITEM_SHORTCUT = 3; // Application shortcut
+
+ /**
+ * Retrieves the jump list item type.
+ */
+ readonly attribute short type;
+
+ /**
+ * Compare this item to another.
+ *
+ * Compares the type and other properties specific to this item's
+ * type.
+ *
+ * separator: type
+ * link: type, uri, title
+ * shortcut: type, handler app
+ */
+ boolean equals(in nsILegacyJumpListItem item);
+};
+
+/**
+ * A menu separator.
+ */
+
+[scriptable, uuid(69A2D5C5-14DC-47da-925D-869E0BD64D27)]
+interface nsILegacyJumpListSeparator : nsILegacyJumpListItem
+{
+ /* nothing needed here */
+};
+
+/**
+ * A URI link jump list item.
+ *
+ * Note the application must be the registered protocol
+ * handler for the protocol of the link.
+ */
+
+[scriptable, uuid(76EA47B1-C797-49b3-9F18-5E740A688524)]
+interface nsILegacyJumpListLink : nsILegacyJumpListItem
+{
+ /**
+ * Set or get the uri for this link item.
+ */
+ attribute nsIURI uri;
+
+ /**
+ * Set or get the title for a link item.
+ */
+ attribute AString uriTitle;
+};
+
+/**
+ * A generic application shortcut with command line support.
+ */
+
+[scriptable, uuid(CBE3A37C-BCE1-4fec-80A5-5FFBC7F33EEA)]
+interface nsILegacyJumpListShortcut : nsILegacyJumpListItem
+{
+ /**
+ * Set or get the handler app for this shortcut item.
+ *
+ * The handler app may also be used along with iconIndex to generate an icon
+ * for the jump list item.
+ *
+ * @throw NS_ERROR_FILE_NOT_FOUND if the handler app can
+ * not be found on the system.
+ *
+ * @see faviconPageUri
+ */
+ attribute nsILocalHandlerApp app;
+
+ /**
+ * Set or get the icon displayed with the jump list item.
+ *
+ * Indicates the resource index of the icon contained within the handler
+ * executable which may be used as the jump list icon.
+ *
+ * @see faviconPageUri
+ */
+ attribute long iconIndex;
+
+ /**
+ * Set or get the URI of a page whose favicon may be used as the icon.
+ *
+ * When a jump list build occurs, the favicon to be used for the item is
+ * obtained using the following steps:
+ * - First, attempt to use the asynchronously retrieved and scaled favicon
+ * associated with the faviconPageUri.
+ * - If faviconPageUri is null, or if retrieving the favicon fails, fall
+ * back to using the handler executable and iconIndex.
+ */
+ attribute nsIURI faviconPageUri;
+};
diff --git a/widget/nsIMacDockSupport.idl b/widget/nsIMacDockSupport.idl
new file mode 100644
index 0000000000..04d6dca774
--- /dev/null
+++ b/widget/nsIMacDockSupport.idl
@@ -0,0 +1,71 @@
+/* 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 "nsISupports.idl"
+
+interface nsIStandaloneNativeMenu;
+
+/**
+ * Allow applications to interface with the Mac OS X Dock.
+ *
+ * Applications may indicate progress on their Dock icon. Only one such
+ * progress indicator is available to the entire application.
+ */
+
+[scriptable, uuid(8BE66B0C-5F71-4B74-98CF-6C2551B999B1)]
+interface nsIMacDockSupport : nsISupports
+{
+ /**
+ * Menu to use for application-specific dock menu items.
+ */
+ attribute nsIStandaloneNativeMenu dockMenu;
+
+ /**
+ * Activate the application. This should be used by an application to
+ * activate itself when a dock menu is selected as selection of a dock menu
+ * item does not automatically activate the application.
+ *
+ * @param aIgnoreOtherApplications If false, the application is activated
+ * only if no other application is currently active. If true, the
+ * application activates regardless.
+ */
+ void activateApplication(in boolean aIgnoreOtherApplications);
+
+ /**
+ * Text used to badge the dock tile.
+ */
+ attribute AString badgeText;
+
+ /**
+ * True if this app is in the list of apps that are persisted to the macOS
+ * Dock (as if the user selected "Keep in Dock").
+ */
+ readonly attribute bool isAppInDock;
+
+ /**
+ * Ensure that there is a tile for this app in the list of apps that are
+ * persisted to the macOS Dock (equivalent to the user selected "Keep in
+ * Dock").
+ *
+ * The position for the [new] app tile is:
+ *
+ * - its current position if it already exists, else
+ * - the position of `aAppToReplacePath` if it exists, else
+ * - directly after the last app with the same .app name, else
+ * - directly after the last known browser app (see `browserAppNames` in
+ * nsMacDockSupport.mm), else
+ * - at the end of the persisted app list.
+ *
+ * @param aAppPath [optional] The path of the .app to persist to the Dock
+ * (defaults to the path of the current app).
+ * @param aAppToReplacePath [optional] The path of a .app that should be
+ * replaced if it is in the list of persisted apps. This is useful when we
+ * prompt the user to install the app when the app is being run directly
+ * from a .dmg and the user may have dragged that .app file to the dock.
+ * @return true if the app was already in the list of persisted apps or if it
+ * was successfully added, else returns false.
+ */
+ bool ensureAppIsPinnedToDock([optional] in AString aAppPath,
+ [optional] in AString aAppToReplacePath);
+};
diff --git a/widget/nsIMacFinderProgress.idl b/widget/nsIMacFinderProgress.idl
new file mode 100644
index 0000000000..3c568bf1b5
--- /dev/null
+++ b/widget/nsIMacFinderProgress.idl
@@ -0,0 +1,43 @@
+/* -*- 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 "nsISupports.idl"
+
+
+[scriptable, function, uuid(6BAE6D1C-7FFD-4354-8D7B-64697E98A801)]
+interface nsIMacFinderProgressCanceledCallback : nsISupports
+{
+ void canceled();
+};
+
+
+[scriptable, uuid(25A0B01F-54D4-4AEF-B2BF-C5764CDC68A8)]
+interface nsIMacFinderProgress : nsISupports
+{
+ /**
+ * Initialize and display a new Finder progressbar on the given file
+ *
+ * @param path The path of the file
+ *
+ * @param canceledCallback Callback which is called when cancelation is requested
+ */
+ void init(in AString path, in nsIMacFinderProgressCanceledCallback canceledCallback);
+
+ /**
+ * Update the current and total progess. If currentProgress and totalProgress are both 0,
+ * the progress is indetermined
+ *
+ * @param currentProgress The current progress
+ *
+ * @param totalProgress The total progress
+ */
+ void updateProgress(in unsigned long long currentProgress, in unsigned long long totalProgress);
+
+ /**
+ * End displaying the progressbar on the file
+ */
+ void end();
+};
diff --git a/widget/nsIMacSharingService.idl b/widget/nsIMacSharingService.idl
new file mode 100644
index 0000000000..cec0dae2d7
--- /dev/null
+++ b/widget/nsIMacSharingService.idl
@@ -0,0 +1,30 @@
+/* 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 "nsISupports.idl"
+
+/**
+ * Allow applications to interface with the Mac OS X Sharing APIs.
+ */
+
+[scriptable, uuid(de59fe1a-46c8-490f-b04d-34545acb06c9)]
+interface nsIMacSharingService : nsISupports
+{
+ /**
+ * Get list of sharing providers
+ */
+ [implicit_jscontext] jsval getSharingProviders(in AString pageUrl);
+
+ /**
+ * Launch service with shareTitle with given url
+ */
+ void shareUrl(in AString serviceName,
+ in AString pageUrl,
+ in AString pageTitle);
+
+ /**
+ * Open the MacOS preferences window to the sharing panel
+ */
+ void openSharingPreferences();
+};
diff --git a/widget/nsIMacUserActivityUpdater.idl b/widget/nsIMacUserActivityUpdater.idl
new file mode 100644
index 0000000000..c5de948a43
--- /dev/null
+++ b/widget/nsIMacUserActivityUpdater.idl
@@ -0,0 +1,24 @@
+/* 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 "nsISupports.idl"
+
+interface nsIBaseWindow;
+
+/**
+ * Updates macOS widget code with the current URI and page title. Widget code
+ * can use this information to update NSUserActivity, enabling Handoff
+ * functionality.
+ */
+
+[scriptable, uuid(29046c8f-cba6-4ffa-9141-1685e96c4ea0)]
+interface nsIMacUserActivityUpdater : nsISupports
+{
+ /**
+ * Update active URL and page title for the given window.
+ */
+ void updateLocation(in AString pageUrl,
+ in AString pageTitle,
+ in nsIBaseWindow window);
+};
diff --git a/widget/nsIMacWebAppUtils.idl b/widget/nsIMacWebAppUtils.idl
new file mode 100644
index 0000000000..4d570a8bf0
--- /dev/null
+++ b/widget/nsIMacWebAppUtils.idl
@@ -0,0 +1,35 @@
+/* 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 "nsISupports.idl"
+
+interface nsIMacWebAppUtils;
+
+[scriptable, function, uuid(8c899c4f-58c1-4b74-9034-3bb64e484b68)]
+interface nsITrashAppCallback : nsISupports
+{
+ void trashAppFinished(in nsresult rv);
+};
+
+/**
+ * Allow MozApps API to locate and manipulate natively installed apps
+ */
+
+[scriptable, uuid(c69cf343-ea41-428b-b161-4655fd54d8e7)]
+interface nsIMacWebAppUtils : nsISupports {
+ /**
+ * Find the path for an app with the given signature.
+ */
+ AString pathForAppWithIdentifier(in AString bundleIdentifier);
+
+ /**
+ * Launch the app with the given identifier, if it exists.
+ */
+ void launchAppWithIdentifier(in AString bundleIdentifier);
+
+ /**
+ * Move the app from the given directory to the Trash.
+ */
+ void trashApp(in AString path, in nsITrashAppCallback callback);
+};
diff --git a/widget/nsIPaper.idl b/widget/nsIPaper.idl
new file mode 100644
index 0000000000..0ec957d221
--- /dev/null
+++ b/widget/nsIPaper.idl
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+[scriptable, uuid(a4dd9675-6311-45a9-a547-44e0127304a6)]
+interface nsIPaper : nsISupports
+{
+ /**
+ * The internal name of the paper (a fixed, non-localized ID).
+ * (For CUPS, this is the PWG-standardized name as used internally by CUPS;
+ * on Windows, it is the integer paper ID as a string.)
+ */
+ readonly attribute AString id;
+
+ /**
+ * The human-readable (potentially localized) name of the paper.
+ */
+ readonly attribute AString name;
+
+ /**
+ * The width of the paper assuming portrait orientation, in points.
+ * That is, the length of the shorter edges of the paper.
+ */
+ readonly attribute double width;
+
+ /**
+ * The height of the paper assuming portrait orientation, in points.
+ * That is, the length of the longer edges of the paper.
+ */
+ readonly attribute double height;
+
+ /**
+ * The Promise resolves with an nsIPaperMargin object. The margin widths contained
+ * in that object's top/bottom/left/right properties are relative to the paper in
+ * portrait orientation. That is, top and bottom are the margins for the short edges,
+ * and left and right are the margins for the long edges.
+ */
+ [implicit_jscontext] readonly attribute Promise unwriteableMargin;
+};
diff --git a/widget/nsIPaperMargin.idl b/widget/nsIPaperMargin.idl
new file mode 100644
index 0000000000..80242f63b3
--- /dev/null
+++ b/widget/nsIPaperMargin.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+// A margin for a sheet of paper.
+[builtinclass, scriptable, uuid(0858d1a7-b646-4b15-a1e8-7eb5ab572d0a)]
+interface nsIPaperMargin : nsISupports
+{
+ [infallible] readonly attribute double top;
+ [infallible] readonly attribute double right;
+ [infallible] readonly attribute double bottom;
+ [infallible] readonly attribute double left;
+};
diff --git a/widget/nsIPrintDialogService.idl b/widget/nsIPrintDialogService.idl
new file mode 100644
index 0000000000..1c79ddd76b
--- /dev/null
+++ b/widget/nsIPrintDialogService.idl
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/* Doc interface here */
+
+#include "nsISupports.idl"
+#include "nsIWebProgressListener.idl"
+#include "nsIPrintSettings.idl"
+#include "nsIObserver.idl"
+
+interface mozIDOMWindowProxy;
+
+/**
+ * Service for opening native print dialogs provided by the operating system.
+ * (The widget code may customize the dialog.)
+ */
+[scriptable, uuid(88af6712-a9fd-4393-9af3-3ffbb1f2caaf)]
+interface nsIPrintDialogService : nsISupports
+{
+ /**
+ * Initialize the service.
+ */
+ void init();
+
+ /**
+ * Show the print dialog.
+ * @param aParent A DOM window the dialog will be parented to.
+ * @param aHaveSelection A boolean indicating whether the document to be
+ * printed has some selected text, which is used to
+ * determine whether the "Print selection only" radio
+ * button is enabled in the print settings dialog.
+ * @param aSettings On entry, this contains initial settings for the
+ * print dialog. On return, if the print operation should
+ * proceed, then this has been updated with the settings
+ * that the user selected in the dialog.
+ * @return NS_OK if the print operation should proceed
+ * @return NS_ERROR_ABORT if the user indicated not to proceed
+ * @return a suitable error for failures to show the print dialog.
+ */
+ void showPrintDialog(in mozIDOMWindowProxy aParent,
+ in boolean aHaveSelection,
+ in nsIPrintSettings aPrintSettings);
+
+ /**
+ * Show the page setup dialog. Note that there is no way to tell whether the
+ * user clicked OK or Cancel on the dialog.
+ * @param aParent A DOM window the dialog will be parented to.
+ * @param aSettings On entry, this contains initial settings for the
+ * page setup dialog. On return, if the dialog wasn't
+ * cancelled, then this has been updated with the settings
+ * that the user selected in the dialog.
+ */
+ void showPageSetupDialog(in mozIDOMWindowProxy aParent,
+ in nsIPrintSettings aPrintSettings);
+
+};
+
+%{C++
+#define NS_PRINTDIALOGSERVICE_IID \
+ {0x88af6712, 0xa9fd, 0x4393, { 0x9a, 0xf3, 0x3f, 0xfb, 0xb1, 0xf2, 0xca, 0xaf}}
+
+#define NS_PRINTDIALOGSERVICE_CONTRACTID \
+ ("@mozilla.org/widget/printdialog-service;1")
+%}
diff --git a/widget/nsIPrintSettings.idl b/widget/nsIPrintSettings.idl
new file mode 100644
index 0000000000..efed9efce6
--- /dev/null
+++ b/widget/nsIPrintSettings.idl
@@ -0,0 +1,405 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+%{ C++
+#include "nsMargin.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+struct PrintSettingsInitializer;
+}
+%}
+
+/**
+ * Native types
+ */
+native nsNativeIntMargin(nsIntMargin);
+[ref] native nsNativeIntMarginRef(nsIntMargin);
+native PrintSettingsInitializer(mozilla::PrintSettingsInitializer);
+
+interface nsIOutputStream;
+
+/**
+ * Simplified graphics interface for JS rendering.
+ */
+[scriptable, builtinclass, uuid(ecc5cbad-57fc-4731-b0bd-09e865bd62ad)]
+interface nsIPrintSettings : nsISupports
+{
+ /**
+ * PrintSettings to be Saved Navigation Constants
+ */
+ /* Flag 0x00000001 is unused */
+ const unsigned long kInitSaveHeaderLeft = 0x00000002;
+ const unsigned long kInitSaveHeaderCenter = 0x00000004;
+ const unsigned long kInitSaveHeaderRight = 0x00000008;
+ const unsigned long kInitSaveFooterLeft = 0x00000010;
+ const unsigned long kInitSaveFooterCenter = 0x00000020;
+ const unsigned long kInitSaveFooterRight = 0x00000040;
+ const unsigned long kInitSaveBGColors = 0x00000080;
+ const unsigned long kInitSaveBGImages = 0x00000100;
+ const unsigned long kInitSavePaperSize = 0x00000200;
+ /* Flag 0x00000400 is unused */
+ const unsigned long kInitSaveDuplex = 0x00000800;
+ /* Flag 0x00001000 is unused */
+ /* Flag 0x00002000 is unused */
+ const unsigned long kInitSaveUnwriteableMargins = 0x00004000;
+ const unsigned long kInitSaveEdges = 0x00008000;
+
+ const unsigned long kInitSaveReversed = 0x00010000;
+ const unsigned long kInitSaveInColor = 0x00020000;
+ const unsigned long kInitSaveOrientation = 0x00040000;
+
+ const unsigned long kInitSavePrinterName = 0x00100000;
+ const unsigned long kInitSavePrintToFile = 0x00200000;
+ const unsigned long kInitSaveToFileName = 0x00400000;
+ const unsigned long kInitSavePageDelay = 0x00800000;
+ const unsigned long kInitSaveMargins = 0x01000000;
+ /* Flag 0x02000000 is unused */
+
+ const unsigned long kInitSaveShrinkToFit = 0x08000000;
+ const unsigned long kInitSaveScaling = 0x10000000;
+
+ const unsigned long kInitSaveAll = 0xFFFFFFFF;
+
+ // These settings should be read from global prefs. Other settings should be
+ // read only from printer-specific prefs.
+ const unsigned long kGlobalSettings =
+ kInitSaveHeaderLeft | kInitSaveHeaderCenter | kInitSaveHeaderRight |
+ kInitSaveFooterLeft | kInitSaveFooterCenter | kInitSaveFooterRight |
+ kInitSaveEdges | kInitSaveReversed | kInitSaveInColor |
+ kInitSaveBGColors | kInitSaveBGImages | kInitSaveShrinkToFit;
+
+ // These settings may be changed by native print dialog and should be
+ // persisted when changed.
+ const unsigned long kPrintDialogPersistSettings =
+ kGlobalSettings | kInitSavePaperSize | kInitSaveDuplex |
+ kInitSaveEdges | kInitSaveReversed | kInitSaveInColor |
+ kInitSaveOrientation | kInitSavePageDelay | kInitSaveMargins |
+ kInitSaveShrinkToFit | kInitSaveScaling;
+
+ /* Justification Enums */
+ const long kJustLeft = 0;
+ const long kJustCenter = 1;
+ const long kJustRight = 2;
+
+ /** Page Size Unit Constants */
+ const short kPaperSizeInches = 0;
+ const short kPaperSizeMillimeters = 1;
+
+ /** Orientation Constants */
+ const short kPortraitOrientation = 0;
+ const short kLandscapeOrientation = 1;
+
+ /** Output file format */
+ const short kOutputFormatNative = 0;
+ const short kOutputFormatPDF = 2;
+
+ /** Output destination */
+ cenum OutputDestinationType : 8 {
+ kOutputDestinationPrinter = 0,
+ kOutputDestinationFile = 1,
+ kOutputDestinationStream = 2,
+ };
+
+ /**
+ * Duplex printing options.
+ *
+ * Note that other libraries refer to equivalent duplex settings using
+ * various sets of terminology. This can be confusing and inconsistent both
+ * with other libraries, and with the behavior that these terms intend to describe.
+ *
+ * kDuplexNone is equivalent to Simplex. Thankfully, both of these terms are
+ * consistent with the behavior that they describe, which is to have single-sided
+ * printing per sheet.
+ *
+ * kDuplexFlipOnLongEdge is equivalent to the following platform-specific constants:
+ * CUPS/macOS: NoTumble
+ * Windows: DMDUP_VERTICAL
+ * GTK: GTK_PRINT_DUPLEX_HORIZONTAL
+ *
+ * kDuplexFlipOnShortEdge is equivalent to the following platform-specific constants:
+ * CUPS/macOS: Tumble
+ * Windows: DMDUP_HORIZONTAL
+ * GTK: GTK_PRINT_DUPLEX_VERTICAL
+ *
+ *
+ * Notice that the GTK and Windows constants have opposite meanings for
+ * VERTICAL and HORIZONTAL.
+ *
+ * To make matters more confusing, these platform-specific terms describe different
+ * behavior (from the user's perspective) depending on whether the sheet is in
+ * portrait vs. landscape orientation.
+ *
+ * For example, the generic term "tumble" describes behavior where a sheet flips over
+ * a binding on the top edge (like a calendar). This requires that the back side of
+ * the sheet is printed upside down with respect to the front side of the sheet,
+ * so that its content appears upright to the reader when they tumble-flip the
+ * sheet over the top-edge binding.
+ *
+ * However, the CUPS/macOS Tumble setting only inverts the back side of the
+ * sheet in portrait orientation. When you switch to landscape orientation, the
+ * Tumble setting behaves like a book-like sheet flip, where the front and back
+ * sides of the sheet are both printed upright with respect to each other.
+ *
+ * This is why it is more consistent and more clear to think of these terms
+ * with regard to sheets being bound on the long edge or the short edge.
+ *
+ * kDuplexFlipOnLongEdge + Portrait = book-like flip (front/back same direction)
+ * kDuplexFlipOnLongEdge + Landscape = calendar-like flip (front/back inverted)
+ *
+ * kDuplexFlipOnShortEdge + Portrait = calendar-like flip (front/back inverted)
+ * kDuplexFlipOnShortEdge + Landscape = book-like flip (front/back same direction)
+ *
+ * The long-edge and short-edge terminology unfortunately breaks down when printing
+ * with square sheet dimensions. Thankfully this edge case (hah) is quite uncommon,
+ * since most standard printing paper dimensions are not square. Such a paper size
+ * would even break the uniformly used portrait and landscape terminology.
+ */
+ const short kDuplexNone = 0;
+ const short kDuplexFlipOnLongEdge = 1;
+ const short kDuplexFlipOnShortEdge = 2;
+
+ /**
+ * Get the page size in twips, considering the
+ * orientation (portrait or landscape).
+ */
+ void GetEffectivePageSize(out double aWidth, out double aHeight);
+
+ /**
+ * Get the printed sheet size in twips, considering both the user-specified
+ * orientation (portrait or landscape) *as well as* the fact that we might be
+ * inverting the orientation to account for 2 or 6 pages-per-sheet.
+ *
+ * This API will usually behave the same (& return the same thing) as
+ * GetEffectivePageSize, *except for* when we are printing with 2 or 6
+ * pages-per-sheet, in which case the return values (aWidth & aHeight) will
+ * be swapped with respect to what GetEffectivePageSize would return.
+ *
+ * Callers should use this method rather than GetEffectivePageSize when they
+ * really do want the size of the sheet of paper to be printed, rather than
+ * the possibly-"virtualized"-via-pages-per-sheet page size.
+ */
+ [noscript, notxpcom, nostdcall] void GetEffectiveSheetSize(out double aWidth,
+ out double aHeight);
+
+ /**
+ * Get the orientation of a printed sheet. This is usually the same as the
+ * 'orientation' attribute (which is the orientation of individual pages),
+ * except when we're printing with 2 or 6 pages-per-sheet, in which case
+ * it'll be the opposite value.
+ *
+ * Note that this value is not independently settable. Its value is fully
+ * determined by the 'orientation' and 'numPagesPerSheet' attributes.
+ */
+ [noscript, notxpcom, nostdcall] long GetSheetOrientation();
+
+ /**
+ * Convenience getter, which returns true IFF the value of `numPagesPerSheet`
+ * would require us to orient the pages orthogonally to the sheet in order
+ * to make best use of the space on the sheet. Specifically, this returns
+ * true IFF `numPagesPerSheet` is set to 2 or 6 pages-per-sheet.
+ */
+ [noscript, notxpcom, nostdcall] bool HasOrthogonalPagesPerSheet();
+
+ /**
+ * Makes a new copy
+ */
+ nsIPrintSettings clone();
+
+ /**
+ * Assigns the internal values from the "in" arg to the current object
+ */
+ void assign(in nsIPrintSettings aPS);
+
+ /**
+ * Returns true if the settings will result in an equivalent preview and
+ * therefore print. The printer name is ignored and it allows for a small
+ * delta in sizes to allow for rounding differences.
+ */
+ bool equivalentTo(in nsIPrintSettings aPrintSettings);
+
+ /**
+ * The edge measurements define the positioning of the headers
+ * and footers on the page. They're treated as an offset from the edges of
+ * the page, but are forced to be at least the "unwriteable margin"
+ * (described below).
+ */
+ attribute double edgeTop; /* these are in inches */
+ attribute double edgeLeft;
+ attribute double edgeBottom;
+ attribute double edgeRight;
+
+ /**
+ * The margins define the positioning of the content on the page.
+ * and footers on the page. They're treated as an offset from the edges of
+ * the page, but are forced to be at least the "unwriteable margin," unless
+ * set to be ignored (described below).
+ */
+ attribute double marginTop; /* these are in inches */
+ attribute double marginLeft;
+ attribute double marginBottom;
+ attribute double marginRight;
+ /**
+ * The unwriteable margin defines the printable region of the paper.
+ */
+ attribute double unwriteableMarginTop; /* these are in inches */
+ attribute double unwriteableMarginLeft;
+ attribute double unwriteableMarginBottom;
+ attribute double unwriteableMarginRight;
+
+ attribute double scaling; /* values 0.0 - 1.0 */
+ [infallible] attribute boolean printBGColors; /* Print Background Colors */
+ [infallible] attribute boolean printBGImages; /* Print Background Images */
+
+ /**
+ * Whether @page rule margins should be honored or not. If the @page
+ * rule sets its margins to zero, we automatically ignore unwriteable
+ * margins, but nonzero values will be clamped to unwriteable margins.
+ */
+ [infallible] attribute boolean honorPageRuleMargins;
+
+ /**
+ * Whether @page rule size should be used for the output paper size.
+ */
+ [infallible] attribute boolean usePageRuleSizeAsPaperSize;
+
+ /**
+ * Whether unwritable margins should be ignored. This should be set when
+ * when the user explicitly requests "Margins: None", e.g. for documents
+ * where accurate scaling matters. Note: While `honorPageRuleMargins` and
+ * this flag can't be set at the same time through the UI, doing so will
+ * cause even the nonzero @page rule margins to ignore unwriteable margins.
+ */
+ [infallible] attribute boolean ignoreUnwriteableMargins;
+
+ /** Whether to draw guidelines showing the margin settings */
+ [infallible] attribute boolean showMarginGuides;
+
+ /** Whether to only print the selected nodes */
+ [infallible] attribute boolean printSelectionOnly;
+
+ attribute AString title;
+ attribute AString docURL;
+
+ attribute AString headerStrLeft;
+ attribute AString headerStrCenter;
+ attribute AString headerStrRight;
+
+ attribute AString footerStrLeft;
+ attribute AString footerStrCenter;
+ attribute AString footerStrRight;
+
+ attribute boolean printSilent; /* print without putting up the dialog */
+ [infallible] attribute boolean shrinkToFit; /* shrinks content to fit on page */
+
+ /* Additional XP Related */
+ attribute AString paperId; /* identifier of paper (not display name) */
+ attribute double paperWidth; /* width of the paper in inches or mm */
+ attribute double paperHeight; /* height of the paper in inches or mm */
+ attribute short paperSizeUnit; /* paper is in inches or mm */
+
+ attribute boolean printReversed;
+ [infallible] attribute boolean printInColor; /* a false means grayscale */
+ attribute long orientation; /* see orientation consts */
+ attribute long numCopies;
+
+ /**
+ * For numPagesPerSheet, we support these values: 1, 2, 4, 6, 9, 16.
+ *
+ * Unsupported values will be treated internally as 1 page per sheet, and
+ * will trigger assertion failures in debug builds.
+ */
+ attribute long numPagesPerSheet;
+
+ /** Output device information */
+ [infallible] attribute nsIPrintSettings_OutputDestinationType outputDestination;
+ [infallible] attribute short outputFormat;
+
+ /**
+ * If outputDestination==kOutputDestinationPrinter, this is set to the name
+ * of the printer that the print output should be saved to, but only in the
+ * parent process (we don't want to leak printer names to potentially
+ * compromised content processes).
+ */
+ attribute AString printerName;
+
+ /**
+ * If outputDestination==kOutputDestinationFile, this is set to the path
+ * of the file that the print output should be saved to, but only in the
+ * parent process (we don't want to leak system paths to potentially
+ * compromised content processes).
+ */
+ attribute AString toFileName;
+
+ attribute nsIOutputStream outputStream; /* for kOutputDestinationPrinter */
+
+ [infallible] attribute long printPageDelay; /* in milliseconds */
+
+ [infallible] attribute long resolution; /* print resolution (dpi) */
+
+ [infallible] attribute long duplex; /* duplex mode */
+
+ /* initialize helpers */
+ /**
+ * This attribute tracks whether the PS has been initialized
+ * from a printer specified by the "printerName" attr.
+ * If a different name is set into the "printerName"
+ * attribute than the one it was initialized with the PS
+ * will then get initialized from that printer.
+ */
+ attribute boolean isInitializedFromPrinter;
+
+ /**
+ * This attribute tracks whether the PS has been initialized
+ * from prefs. If a different name is set into the "printerName"
+ * attribute than the one it was initialized with the PS
+ * will then get initialized from prefs again.
+ */
+ attribute boolean isInitializedFromPrefs;
+
+ /* C++ Helper Functions */
+ [noscript] void SetMarginInTwips(in nsNativeIntMarginRef aMargin);
+ [noscript] void SetEdgeInTwips(in nsNativeIntMarginRef aEdge);
+ [noscript, notxpcom, nostdcall] nsNativeIntMargin GetMarginInTwips();
+ [noscript, notxpcom, nostdcall] nsNativeIntMargin GetEdgeInTwips();
+
+ /**
+ * Sets/Gets the "unwriteable margin" for the page format. This defines
+ * the boundary from which we'll measure the EdgeInTwips and MarginInTwips
+ * attributes, to place the headers and content, respectively.
+ *
+ * Note: Implementations of SetUnwriteableMarginInTwips should handle
+ * negative margin values by falling back on the system default for
+ * that margin.
+ */
+ [noscript] void SetUnwriteableMarginInTwips(in nsNativeIntMarginRef aEdge);
+ [noscript, notxpcom, nostdcall] nsNativeIntMargin GetUnwriteableMarginInTwips();
+
+ /**
+ * Get more accurate print ranges from the superior interval
+ * (startPageRange, endPageRange). The aPages array is populated with a
+ * list of pairs (start, end), where the endpoints are included. The print
+ * ranges (start, end), must not overlap and must be in the
+ * (startPageRange, endPageRange) scope.
+ *
+ * If there are no print ranges the aPages array is empty.
+ */
+ attribute Array<long> pageRanges;
+
+ /**
+ * Get a PrintSettingsInitializer populated with the relevant current settings.
+ */
+ [notxpcom, nostdcall] PrintSettingsInitializer getSettingsInitializer();
+
+%{C++
+ static bool IsPageSkipped(int32_t aPageNum, const nsTArray<int32_t>& aRanges);
+%}
+};
+
+%{ C++
+already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings(const mozilla::PrintSettingsInitializer&);
+%}
diff --git a/widget/nsIPrintSettingsService.idl b/widget/nsIPrintSettingsService.idl
new file mode 100644
index 0000000000..3b33724c34
--- /dev/null
+++ b/widget/nsIPrintSettingsService.idl
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/* Interface to the Service for gwetting the Global PrintSettings object
+ or a unique PrintSettings object
+*/
+
+#include "nsISupports.idl"
+
+interface nsIPrintSettings;
+
+%{ C++
+namespace mozilla {
+namespace embedding {
+ class PrintData;
+}
+}
+%}
+
+/**
+ * Native types
+ */
+[ref] native PrintDataRef(const mozilla::embedding::PrintData);
+[ptr] native PrintDataPtr(mozilla::embedding::PrintData);
+
+[scriptable, uuid(841387C8-72E6-484b-9296-BF6EEA80D58A)]
+interface nsIPrintSettingsService : nsISupports
+{
+ /**
+ * Returns the default print settings as used for printing.
+ */
+ [noscript] readonly attribute nsIPrintSettings defaultPrintSettingsForPrinting;
+
+ /**
+ * Creates a new nsIPrintSettings object.
+ *
+ * Initializes the settings object from the unprefixed printer
+ * (Note: this may not happen if there is an OS specific implementation.)
+ */
+ nsIPrintSettings createNewPrintSettings();
+
+ /**
+ * The name of the last printer used. Note that this may not be set, or may
+ * no longer be a valid printer. The caller is responsible for checking and
+ * falling back to some other printer (such as the system default printer).
+ *
+ * XXX: make it [infallible] when AString supports that (bug 1491187).
+ */
+ readonly attribute AString lastUsedPrinterName;
+
+ /**
+ * Initializes certain settings from the native printer into the PrintSettings
+ * if aPrinterName is null then it uses the default printer name if it can
+ * These settings include, but are not limited to:
+ * Page Orientation
+ * Page Size
+ * Number of Copies
+ */
+ void initPrintSettingsFromPrinter(in AString aPrinterName,
+ in nsIPrintSettings aPrintSettings);
+
+ /**
+ * Reads PrintSettings values from Prefs,
+ * the values to be read are indicated by the "flags" arg.
+ *
+ * aPrintSettings should be initialized with the name of a printer. First
+ * it reads in the PrintSettings from the last print job. Then it uses the
+ * PrinterName in the PrinterSettings to read any settings that were saved
+ * just for that printer.
+ *
+ * aPS - PrintSettings to have its settings read
+ * aUsePrinterNamePrefix - indicates whether to use the printer name as a prefix
+ * aFlags - indicates which prefs to read, see nsIPrintSettings.idl for the
+ * const values.
+ *
+ * Items not read:
+ * startPageRange, endPageRange, scaling, printRange, title
+ * docURL, isCancelled,
+ * printSilent, shrinkToFit, numCopies,
+ * printerName
+ *
+ */
+ void initPrintSettingsFromPrefs(in nsIPrintSettings aPrintSettings, in boolean aUsePrinterNamePrefix, in unsigned long aFlags);
+
+ /**
+ * As long as the pref print.save_print_settings isn't set to false, this
+ * saves to prefs the settings from aPrintSettings that are indicated by
+ * aFlags.
+ *
+ * If there is no PrinterName in the PrinterSettings
+ * the values are saved as the "generic" values not associated with any printer.
+ * If a PrinterName is there, then it saves the items qualified for that Printer
+ *
+ * aPS - PrintSettings to have its settings saved
+ * aFlags - indicates which prefs to save, see nsIPrintSettings.idl for the const values.
+ *
+ * Items not written:
+ * startPageRange, endPageRange, scaling, printRange, title
+ * docURL, isCancelled,
+ * printSilent, shrinkToFit, numCopies
+ *
+ */
+ void maybeSavePrintSettingsToPrefs(in nsIPrintSettings aPrintSettings, in unsigned long aFlags);
+
+ /**
+ * As long as the pref print.save_print_settings isn't set to false, this
+ * saves the given printer name as the last used printer name.
+ */
+ void maybeSaveLastUsedPrinterNameToPrefs(in AString aPrinterName);
+
+ /**
+ * Given some nsIPrintSettings,
+ * populates a PrintData representing them which can be sent over IPC. Values
+ * are only ever read from aSettings and aWBP.
+ *
+ * @param aSettings
+ * An nsIPrintSettings for a print job.
+ * @param data
+ * Pointer to a pre-existing PrintData to populate.
+ *
+ * @return nsresult
+ */
+ [noscript]
+ void SerializeToPrintData(in nsIPrintSettings aPrintSettings,
+ in PrintDataPtr data);
+
+ /**
+ * This function is the opposite of SerializeToPrintData, in that it takes
+ * a PrintData, and populates a pre-existing nsIPrintSettings with the data
+ * from PrintData.
+ *
+ * @param PrintData
+ * Printing information sent through IPC.
+ * @param settings
+ * A pre-existing nsIPrintSettings to populate with the PrintData.
+ *
+ * @return nsresult
+ */
+ [noscript]
+ void DeserializeToPrintSettings(in PrintDataRef data,
+ in nsIPrintSettings aPrintSettings);
+
+};
+
+%{C++
+// {841387C8-72E6-484b-9296-BF6EEA80D58A}
+#define NS_PRINTSETTINGSSERVICE_IID \
+ {0x841387c8, 0x72e6, 0x484b, { 0x92, 0x96, 0xbf, 0x6e, 0xea, 0x80, 0xd5, 0x8a}}
+%}
diff --git a/widget/nsIPrintSettingsWin.idl b/widget/nsIPrintSettingsWin.idl
new file mode 100644
index 0000000000..fae68a9fdd
--- /dev/null
+++ b/widget/nsIPrintSettingsWin.idl
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+%{ C++
+#include "windows.h"
+%}
+
+/**
+ * Native types
+ */
+ [ptr] native nsDevMode(DEVMODEW);
+ native nsHdc(HDC);
+
+/**
+ * Simplified PrintSettings for Windows interface
+ */
+[uuid(c63eed41-6ac5-459e-8a64-033eb9ad770a)]
+interface nsIPrintSettingsWin : nsISupports
+{
+ /**
+ * Data Members
+ *
+ * Each of these data members make a copy
+ * of the contents. If you get the value,
+ * you own the memory.
+ *
+ * The following three pieces of data are needed
+ * to create a DC for printing. These are typcially
+ * gotten via the PrintDLG call ro can be obtained
+ * via the "m_pd" data member of the CPrintDialog
+ * in MFC.
+ */
+ [noscript] attribute AString deviceName;
+ [noscript] attribute AString driverName;
+
+ [noscript] attribute nsDevMode devMode;
+
+ /**
+ * Copy relevant print settings from native Windows device.
+ *
+ * @param hdc HDC to copy from
+ * @param devMode DEVMODE to copy from
+ */
+ [notxpcom] void copyFromNative(in nsHdc hdc, in nsDevMode devMode);
+
+ /**
+ * Copy relevant print settings to native windows structures.
+ *
+ * @param devMode DEVMODE to be populated.
+ */
+ [notxpcom] void copyToNative(in nsDevMode devMode);
+};
diff --git a/widget/nsIPrinter.idl b/widget/nsIPrinter.idl
new file mode 100644
index 0000000000..d95671f681
--- /dev/null
+++ b/widget/nsIPrinter.idl
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsIPaper.idl"
+#include "nsIPrintSettings.idl"
+#include "nsISupports.idl"
+
+[scriptable, uuid(855ae9dd-62a4-64aa-9c60-b1078ff028f1)]
+interface nsIPrinterInfo : nsISupports
+{
+ /**
+ * An array of nsIPaper instances that represents the available paper sizes.
+ */
+ readonly attribute Array<nsIPaper> paperList;
+ /**
+ * nsIPrintSettings object containing the default settings for a printer.
+ */
+ readonly attribute nsIPrintSettings defaultSettings;
+};
+
+[scriptable, uuid(d2dde9bb-df86-469c-bfcc-fd95a44b1db8)]
+interface nsIPrinter : nsISupports
+{
+ /**
+ * The name of the printer.
+ */
+ readonly attribute AString name;
+
+ /**
+ * The system name of the printer.
+ *
+ * This may be faster for lookup in nsIPrinterList functions, but will only
+ * work for functions that will accept the system name.
+ */
+ readonly attribute AString systemName;
+
+ /**
+ * Returns a Promise that resolves to a nsIPrinterInfo.
+ * This will contain the default printer settings, and the list of paper
+ * sizes supported by the printer.
+ */
+ [implicit_jscontext]
+ readonly attribute Promise printerInfo;
+
+ /**
+ * Returns a Promise that resolves to a new settings object that contains all
+ * of the settings from aSettingsToCopyFrom that are valid for this printer.
+ * Any settings that are not valid for the printer are set to default/fallback
+ * values.
+ */
+ [implicit_jscontext]
+ Promise copyFromWithValidation(in nsIPrintSettings aSettingsToCopyFrom);
+
+ /**
+ * Returns a Promise that resolves to true or false to indicate whether this
+ * printer supports duplex printing.
+ */
+ [implicit_jscontext]
+ readonly attribute Promise supportsDuplex;
+
+ /**
+ * Returns a Promise that resolves to true or false to indicate whether this
+ * printer supports color printing.
+ */
+ [implicit_jscontext]
+ readonly attribute Promise supportsColor;
+
+ /**
+ * Returns a Promise that resolves to true or false to indicate whether this
+ * printer supports monochrome printing.
+ */
+ [implicit_jscontext]
+ readonly attribute Promise supportsMonochrome;
+
+ /**
+ * Returns a Promise that resolves to true or false to indicate whether this
+ * printer supports collation.
+ */
+ [implicit_jscontext]
+ readonly attribute Promise supportsCollation;
+};
diff --git a/widget/nsIPrinterList.idl b/widget/nsIPrinterList.idl
new file mode 100644
index 0000000000..d49cedc16e
--- /dev/null
+++ b/widget/nsIPrinterList.idl
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+#include "nsIPrinter.idl"
+#include "nsIPrintSettings.idl"
+
+[scriptable, uuid(5e738fff-404c-4c94-9189-e8f2cce93e94)]
+interface nsIPrinterList : nsISupports
+{
+ /**
+ * Initializes certain settings from the native printer into the PrintSettings
+ * These settings include, but are not limited to:
+ * Page Orientation
+ * Page Size
+ * Number of Copies
+ */
+ void initPrintSettingsFromPrinter(in AString aPrinterName,
+ in nsIPrintSettings aPrintSettings);
+
+ /**
+ * The system default printer name. This is not necessarily gecko's
+ * default printer; see nsIPrintSettingsService.lastUsedPrinterName
+ * for that.
+ */
+ readonly attribute AString systemDefaultPrinterName;
+
+ /**
+ * Returns a promise that resolves to the printer of a given name, or is
+ * rejected if there is no such printer.
+ */
+ [implicit_jscontext] Promise getPrinterByName(in AString aPrinterName);
+
+ /**
+ * Returns a promise that resolves to the printer of a given system name, or
+ * is rejected if there is no such printer.
+ * This may be more efficient than using getNamedPrinter, but requires the
+ * caller to know the system name of the printer they want to find.
+ */
+ [implicit_jscontext] Promise getPrinterBySystemName(in AString aPrinterName);
+
+ /**
+ * Returns a promise that resolves to the printer of the given name, or
+ * the default system printer, or is rejected if there are no printers
+ * available.
+ */
+ [implicit_jscontext] Promise getNamedOrDefaultPrinter(in AString aPrinterName);
+
+ /**
+ * Returns a promise that resolves to an array of printers.
+ */
+ [implicit_jscontext] readonly attribute Promise printers;
+
+ /**
+ * Returns a Promise that resolves to an array of nsIPaper instances
+ * for common paper sizes suitable to be supported for Save to PDF.
+ */
+ [implicit_jscontext] readonly attribute Promise fallbackPaperList;
+};
diff --git a/widget/nsIRollupListener.h b/widget/nsIRollupListener.h
new file mode 100644
index 0000000000..b52e768a14
--- /dev/null
+++ b/widget/nsIRollupListener.h
@@ -0,0 +1,81 @@
+/* -*- 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/. */
+
+#ifndef __nsIRollupListener_h__
+#define __nsIRollupListener_h__
+
+#include "nsTArray.h"
+#include "nsPoint.h"
+#include "Units.h"
+
+class nsIContent;
+class nsIWidget;
+
+class nsIRollupListener {
+ public:
+ enum class FlushViews : bool { No, Yes };
+ enum class AllowAnimations : bool { No, Yes };
+ struct RollupOptions {
+ // aCount is the number of popups in a chain to close. If this is
+ // zero, then all popups are closed.
+ uint32_t mCount = 0;
+ // If this is true, then views should be flushed after the rollup.
+ FlushViews mFlush = FlushViews::No;
+ // This is the mouse pointer position where the event that triggered the
+ // rollup occurred, which may be nullptr.
+ const mozilla::LayoutDeviceIntPoint* mPoint = nullptr;
+ // Whether to allow panel animations.
+ AllowAnimations mAllowAnimations = AllowAnimations::Yes;
+ };
+
+ /**
+ * Notifies the object to rollup, optionally returning the node that
+ * was just rolled up in aLastRolledUp, if non-null.
+ *
+ * aLastRolledUp is not addrefed.
+ *
+ * Returns true if the event that the caller is processing should be consumed.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual bool Rollup(const RollupOptions&,
+ nsIContent** aLastRolledUp = nullptr) = 0;
+
+ /**
+ * Asks the RollupListener if it should rollup on mouse wheel events
+ */
+ virtual bool ShouldRollupOnMouseWheelEvent() = 0;
+
+ /**
+ * Asks the RollupListener if it should consume mouse wheel events
+ */
+ virtual bool ShouldConsumeOnMouseWheelEvent() = 0;
+
+ /**
+ * Asks the RollupListener if it should rollup on mouse activate, eg. X-Mouse
+ */
+ virtual bool ShouldRollupOnMouseActivate() = 0;
+
+ /*
+ * Retrieve the widgets for open menus and store them in the array
+ * aWidgetChain. The number of menus of the same type should be returned,
+ * for example, if a context menu is open, return only the number of menus
+ * that are part of the context menu chain. This allows closing up only
+ * those menus in different situations. The returned value should be exactly
+ * the same number of widgets added to aWidgetChain.
+ */
+ virtual uint32_t GetSubmenuWidgetChain(
+ nsTArray<nsIWidget*>* aWidgetChain) = 0;
+
+ virtual nsIWidget* GetRollupWidget() = 0;
+
+ /**
+ * If a native menu is currently shown, closes the menu.
+ * Returns true if a native menu was open.
+ */
+ virtual bool RollupNativeMenu() { return false; }
+};
+
+#endif /* __nsIRollupListener_h__ */
diff --git a/widget/nsIScreen.idl b/widget/nsIScreen.idl
new file mode 100644
index 0000000000..af8371edb3
--- /dev/null
+++ b/widget/nsIScreen.idl
@@ -0,0 +1,127 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+%{C++
+#include "Units.h"
+
+namespace mozilla::dom {
+// TODO(zrhoffman, bug 1444515): ScreenColorGamut should be forward-declared
+// using `webidl` once the webidl identifier supports enums.
+enum class ScreenColorGamut : uint8_t;
+} // namespace mozilla::dom
+
+/**
+ * The display type of nsIScreen belongs to.
+ */
+enum class DisplayType: int32_t {
+ DISPLAY_PRIMARY, // primary screen
+ DISPLAY_EXTERNAL, // wired displays, such as HDMI, DisplayPort, etc.
+ DISPLAY_VIRTUAL // wireless displays, such as Chromecast, WiFi-Display, etc.
+};
+%}
+
+native ScreenColorGamut(mozilla::dom::ScreenColorGamut);
+
+[scriptable, builtinclass, uuid(826e80c8-d70f-42e2-8aa9-82c05f2a370a)]
+interface nsIScreen : nsISupports
+{
+ /**
+ * These report screen dimensions in (screen-specific) device pixels
+ */
+ void GetRect(out long left, out long top, out long width, out long height);
+ void GetAvailRect(out long left, out long top, out long width, out long height);
+
+%{C++
+ mozilla::LayoutDeviceIntRect GetRect() {
+ int32_t left = 0, top = 0, width = 0, height = 0;
+ GetRect(&left, &top, &width, &height);
+ return {left, top, width, height};
+ }
+
+ mozilla::LayoutDeviceIntRect GetAvailRect() {
+ int32_t left = 0, top = 0, width = 0, height = 0;
+ GetAvailRect(&left, &top, &width, &height);
+ return {left, top, width, height};
+ }
+%}
+
+ /**
+ * And these report in desktop pixels
+ */
+ void GetRectDisplayPix(out long left, out long top, out long width, out long height);
+ void GetAvailRectDisplayPix(out long left, out long top, out long width, out long height);
+
+%{C++
+ mozilla::DesktopIntRect GetRectDisplayPix() {
+ int32_t left = 0, top = 0, width = 0, height = 0;
+ GetRectDisplayPix(&left, &top, &width, &height);
+ return {left, top, width, height};
+ }
+
+ mozilla::DesktopIntRect GetAvailRectDisplayPix() {
+ int32_t left = 0, top = 0, width = 0, height = 0;
+ GetAvailRectDisplayPix(&left, &top, &width, &height);
+ return {left, top, width, height};
+ }
+%}
+
+ [infallible] readonly attribute long pixelDepth;
+ [infallible] readonly attribute long colorDepth;
+ /**
+ * ScreenColorGamut is native type, which cannot be declared [infallible].
+ */
+ readonly attribute ScreenColorGamut colorGamut;
+
+ /**
+ * The number of device pixels per desktop pixel for this screen (for
+ * hidpi configurations where there may be multiple device pixels per
+ * desktop px and/or per CSS px).
+ *
+ * This seems poorly named (something like devicePixelsPerDesktopPixel
+ * would be more accurate/explicit), but given that it is exposed to
+ * front-end code and may also be used by add-ons, it's probably not
+ * worth the disruption of changing it.
+ *
+ * Returns 1.0 if HiDPI mode is disabled or unsupported, or if the
+ * host OS uses device pixels as its desktop pixel units (e.g. Windows 7 or
+ * GTK/X11). Per-monitor DPI is available in Windows 8.1+, GTK/Wayland or
+ * macOS.
+ */
+ [infallible] readonly attribute double contentsScaleFactor;
+
+ /**
+ * The default number of device pixels per unscaled CSS pixel for this
+ * screen. This is probably what contentsScaleFactor originally meant
+ * to be, prior to confusion between CSS pixels and desktop pixel units.
+ */
+ [infallible] readonly attribute double defaultCSSScaleFactor;
+
+%{C++
+
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToLayoutDeviceScale() {
+ return mozilla::DesktopToLayoutDeviceScale(GetContentsScaleFactor());
+ }
+
+ mozilla::CSSToLayoutDeviceScale GetCSSToLayoutDeviceScale() {
+ return mozilla::CSSToLayoutDeviceScale(GetDefaultCSSScaleFactor());
+ }
+
+ mozilla::CSSToDesktopScale GetCSSToDesktopScale() {
+ return GetCSSToLayoutDeviceScale() / GetDesktopToLayoutDeviceScale();
+ }
+
+%}
+ /**
+ * The DPI of the screen.
+ */
+ [infallible] readonly attribute float dpi;
+
+ /** The target screen refresh rate, in Hz, or 0 if unknown */
+ [infallible] readonly attribute long refreshRate;
+ [infallible] readonly attribute boolean isPseudoDisplay;
+};
diff --git a/widget/nsIScreenManager.idl b/widget/nsIScreenManager.idl
new file mode 100644
index 0000000000..9203d7891f
--- /dev/null
+++ b/widget/nsIScreenManager.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+#include "nsIScreen.idl"
+
+%{C++
+#include "nsCOMPtr.h"
+%}
+
+[scriptable, uuid(e8a96e60-6b61-4a14-bacc-53891604b502)]
+interface nsIScreenManager : nsISupports
+{
+ // Returns the screen that contains the rectangle. If the rect overlaps
+ // multiple screens, it picks the screen with the greatest area of intersection.
+ //
+ // The coordinates are in pixels (not twips) and in screen coordinates.
+ nsIScreen screenForRect(in long left, in long top, in long width, in long height) ;
+
+%{C++
+ already_AddRefed<nsIScreen> ScreenForRect(const mozilla::DesktopIntRect& aRect) {
+ nsCOMPtr<nsIScreen> screen;
+ ScreenForRect(aRect.x, aRect.y, aRect.width, aRect.height, getter_AddRefs(screen));
+ return screen.forget();
+ }
+%}
+
+ // The screen with the menubar/taskbar. This shouldn't be needed very often.
+ readonly attribute nsIScreen primaryScreen;
+
+ // The total number of pixels across all monitors.
+ readonly attribute int64_t totalScreenPixels;
+};
diff --git a/widget/nsISharePicker.idl b/widget/nsISharePicker.idl
new file mode 100644
index 0000000000..34c7f18d0e
--- /dev/null
+++ b/widget/nsISharePicker.idl
@@ -0,0 +1,32 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURI;
+
+interface mozIDOMWindowProxy;
+
+[scriptable, uuid(1201d357-8417-4926-a694-e6408fbedcf8)]
+interface nsISharePicker : nsISupports
+{
+ /**
+ * Initialize the share picker widget.
+ * @param nsIDOMWindow openerWindow.
+ */
+ void init(in mozIDOMWindowProxy openerWindow);
+
+ /**
+ * Returns the parent window this was initialized with.
+ */
+ readonly attribute mozIDOMWindowProxy openerWindow;
+
+ /**
+ * XPCOM Analog of navigator.share() as per:
+ * https://w3c.github.io/web-share/#share-method
+ */
+ Promise share(in AUTF8String title, in AUTF8String text, in nsIURI url);
+};
diff --git a/widget/nsISound.idl b/widget/nsISound.idl
new file mode 100644
index 0000000000..8b4a2f29c0
--- /dev/null
+++ b/widget/nsISound.idl
@@ -0,0 +1,40 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURL;
+
+[scriptable, uuid(C3C28D92-A17F-43DF-976D-4EEAE6F995FC)]
+interface nsISound : nsISupports
+{
+ void play(in nsIURL aURL);
+ void beep();
+
+ /**
+ * Not strictly necessary, but avoids delay before first sound.
+ * The various methods on nsISound call Init() if they need to.
+ */
+ void init();
+
+ /**
+ * In some situations, playEventSound will be called. Then, each
+ * implementations will play a system sound for the event if it's necessary.
+ *
+ * NOTE: Don't change these values because they are used in
+ * nsPIPromptService.idl. So, if they are changed, that makes big impact for
+ * the embedders.
+ */
+ const unsigned long EVENT_NEW_MAIL_RECEIVED = 0;
+ const unsigned long EVENT_ALERT_DIALOG_OPEN = 1;
+ const unsigned long EVENT_CONFIRM_DIALOG_OPEN = 2;
+ const unsigned long EVENT_PROMPT_DIALOG_OPEN = 3;
+ const unsigned long EVENT_SELECT_DIALOG_OPEN = 4;
+ const unsigned long EVENT_MENU_EXECUTE = 5;
+ const unsigned long EVENT_MENU_POPUP = 6;
+ const unsigned long EVENT_EDITOR_MAX_LEN = 7;
+ void playEventSound(in unsigned long aEventId);
+};
diff --git a/widget/nsIStandaloneNativeMenu.idl b/widget/nsIStandaloneNativeMenu.idl
new file mode 100644
index 0000000000..b995f1f8b9
--- /dev/null
+++ b/widget/nsIStandaloneNativeMenu.idl
@@ -0,0 +1,55 @@
+/* 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 "nsISupports.idl"
+
+webidl Element;
+
+/**
+ * Platform-independent interface to platform native menu objects.
+ */
+
+[scriptable, uuid(7F7201EB-510C-4CEF-BDF0-04A15A7A4A8C)]
+interface nsIStandaloneNativeMenu : nsISupports
+{
+ /**
+ * Initialize the native menu using given XUL DOM element.
+ *
+ * @param aDOMElement A XUL DOM element of tag type |menu| or |menupopup|.
+ */
+ void init(in Element aElement);
+
+ /**
+ * This method must be called before the menu is opened and displayed to the
+ * user. It allows the platform code to update the menu and also determine
+ * whether the menu should even be shown.
+ *
+ * @return true if the menu can be shown, false if it should not be shown
+ */
+ boolean menuWillOpen();
+
+ /**
+ * Activate the native menu item specified by |anIndexString|. This method
+ * is intended to be used by the test suite.
+ *
+ * @param anIndexString string containing a list of indices separated by
+ * pipe ('|') characters
+ */
+ void activateNativeMenuItemAt(in AString anIndexString);
+
+ /**
+ * Force an update of the native menu item specified by |anIndexString|. This
+ * method is intended to be used by the test suite.
+ *
+ * @param anIndexString string containing a list of indices separated by
+ * pipe ('|') characters
+ */
+ void forceUpdateNativeMenuAt(in AString anIndexString);
+
+ /**
+ * Print information about the menu structure to stdout. Only used for
+ * debugging.
+ */
+ void dump();
+};
diff --git a/widget/nsISystemStatusBar.idl b/widget/nsISystemStatusBar.idl
new file mode 100644
index 0000000000..51aa457a08
--- /dev/null
+++ b/widget/nsISystemStatusBar.idl
@@ -0,0 +1,36 @@
+/* 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 "nsISupports.idl"
+
+webidl Element;
+
+/**
+ * Allow applications to interface with the Mac OS X system status bar.
+ */
+
+[scriptable, uuid(24493180-ee81-4b7c-8b17-9e69480b7b8a)]
+interface nsISystemStatusBar : nsISupports
+{
+ /**
+ * Add an item to the system status bar. Each item can only be present once,
+ * subsequent addItem calls with the same element will be ignored.
+ * The system status bar holds a strong reference to the added XUL menu
+ * element and the item will stay in the status bar until it is removed via
+ * a call to removeItem, or until the process shuts down.
+ * @param aDOMMenuElement A XUL menu element that contains a XUL menupopup
+ * with regular menu content. The menu's icon is put
+ * into the system status bar; clicking it will open
+ * a menu with the contents of the menupopup.
+ * The menu label is not shown.
+ */
+ void addItem(in Element aMenuElement);
+
+ /**
+ * Remove a previously-added item from the menu bar. Calling this with an
+ * element that has not been added before will be silently ignored.
+ * @param aDOMMenuElement The XUL menu element that you called addItem with.
+ */
+ void removeItem(in Element aMenuElement);
+};
diff --git a/widget/nsITaskbarOverlayIconController.idl b/widget/nsITaskbarOverlayIconController.idl
new file mode 100644
index 0000000000..f11cf16ed0
--- /dev/null
+++ b/widget/nsITaskbarOverlayIconController.idl
@@ -0,0 +1,39 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+interface imgIContainer;
+
+/**
+ * Starting in Windows 7, applications can display an overlay on the icon in
+ * the taskbar. This class wraps around the native functionality to do this.
+ */
+[scriptable, uuid(b1858889-a698-428a-a14b-b5d60cff6de2)]
+interface nsITaskbarOverlayIconController : nsISupports
+{
+ /**
+ * Sets the overlay icon and its corresponding alt text.
+ *
+ * @param statusIcon The handle to the overlay icon. The icon will be scaled
+ * to the small icon size (16x16 at 96 dpi). Can be null, in
+ * which case if the taskbar button represents a single window
+ * the icon is removed.
+ * @param statusDescription The alt text version of the information
+ * conveyed by the overlay, for accessibility
+ * purposes.
+ *
+ * @note The behavior for window groups is managed by Windows.
+ * - If an overlay icon is set for any window in a window group and another
+ * overlay icon is already applied to the corresponding taskbar button, that
+ * existing overlay is replaced.
+ * - If null is passed in to replace the overlay currently being displayed,
+ * and if a previous overlay set for a different window in the group is
+ * still available, then that previous overlay is displayed.
+ */
+ void setOverlayIcon(in imgIContainer statusIcon,
+ in AString statusDescription);
+};
diff --git a/widget/nsITaskbarPreview.idl b/widget/nsITaskbarPreview.idl
new file mode 100644
index 0000000000..9bb5f079e7
--- /dev/null
+++ b/widget/nsITaskbarPreview.idl
@@ -0,0 +1,70 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "nsISupports.idl"
+#include "nsIBaseWindow.idl"
+
+interface nsITaskbarPreviewController;
+
+/**
+ * nsITaskbarPreview
+ *
+ * Common interface for both window and tab taskbar previews. This interface
+ * cannot be instantiated directly.
+ *
+ */
+[scriptable, uuid(CBFDF766-D002-403B-A3D9-B091C9AD465B)]
+interface nsITaskbarPreview : nsISupports
+{
+ /**
+ * The controller for this preview. A controller is required to provide
+ * the behavior and appearance of the taskbar previews. It is responsible for
+ * determining the size and contents of the preview, which buttons are
+ * displayed and how the application responds to user actions on the preview.
+ *
+ * Neither preview makes full use of the controller. See the documentation
+ * for nsITaskbarWindowPreview and nsITaskbarTabPreview for details on which
+ * controller methods are used.
+ *
+ * The controller is not allowed to be null.
+ *
+ * @see nsITaskbarPreviewController
+ */
+ attribute nsITaskbarPreviewController controller;
+
+ /**
+ * The tooltip displayed above the preview when the user hovers over it
+ *
+ * Default: an empty string
+ */
+ attribute AString tooltip;
+
+ /**
+ * Whether or not the preview is visible.
+ *
+ * Changing this option is expensive for tab previews since toggling this
+ * option will destroy/create the proxy window and its registration with the
+ * taskbar. If any step of that fails, an exception will be thrown.
+ *
+ * For window previews, this operation is very cheap.
+ *
+ * Default: false
+ */
+ attribute boolean visible;
+
+ /**
+ * Gets/sets whether or not the preview is marked active (selected) in the
+ * taskbar.
+ */
+ attribute boolean active;
+
+ /**
+ * Invalidates the taskbar's cached image of this preview, forcing a redraw
+ * if necessary
+ */
+ void invalidate();
+};
diff --git a/widget/nsITaskbarPreviewButton.idl b/widget/nsITaskbarPreviewButton.idl
new file mode 100644
index 0000000000..79ad36699a
--- /dev/null
+++ b/widget/nsITaskbarPreviewButton.idl
@@ -0,0 +1,62 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "nsISupports.idl"
+
+interface imgIContainer;
+
+/**
+ * nsITaskbarPreviewButton
+ *
+ * Provides access to a window preview's toolbar button's properties.
+ */
+[scriptable, uuid(CED8842D-FE37-4767-9A8E-FDFA56510C75)]
+interface nsITaskbarPreviewButton : nsISupports
+{
+ /**
+ * The button's tooltip.
+ *
+ * Default: an empty string
+ */
+ attribute AString tooltip;
+
+ /**
+ * True if the array of previews should be dismissed when this button is clicked.
+ *
+ * Default: false
+ */
+ attribute boolean dismissOnClick;
+
+ /**
+ * True if the taskbar should draw a border around this button's image.
+ *
+ * Default: true
+ */
+ attribute boolean hasBorder;
+
+ /**
+ * True if the button is disabled. This is not the same as visible.
+ *
+ * Default: false
+ */
+ attribute boolean disabled;
+
+ /**
+ * The icon used for the button.
+ *
+ * Default: null
+ */
+ attribute imgIContainer image;
+
+ /**
+ * True if the button is shown. Buttons that are invisible do not
+ * participate in the layout of buttons underneath the preview.
+ *
+ * Default: false
+ */
+ attribute boolean visible;
+};
diff --git a/widget/nsITaskbarPreviewController.idl b/widget/nsITaskbarPreviewController.idl
new file mode 100644
index 0000000000..4920687dfb
--- /dev/null
+++ b/widget/nsITaskbarPreviewController.idl
@@ -0,0 +1,103 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "nsISupports.idl"
+
+interface nsIDocShell;
+interface nsITaskbarPreview;
+interface nsITaskbarPreviewButton;
+
+/**
+ * nsITaskbarPreviewCallback
+ *
+ * Provides an interface for async image result callbacks. See
+ * nsITaskbarPreviewController request apis below.
+ */
+[scriptable, function, uuid(f3744696-320d-4804-9c27-6a84c29acaa6)]
+interface nsITaskbarPreviewCallback : nsISupports
+{
+ void done(in nsISupports aCanvas, in boolean aDrawBorder);
+};
+
+/**
+ * nsITaskbarPreviewController
+ *
+ * nsITaskbarPreviewController provides the behavior for the taskbar previews.
+ * Its methods and properties are used by nsITaskbarPreview. Clients are
+ * intended to provide their own implementation of this interface. Depending on
+ * the interface the controller is attached to, only certain methods/attributes
+ * are required to be implemented.
+ */
+[scriptable, uuid(8b427646-e446-4941-ae0b-c1122a173a68)]
+interface nsITaskbarPreviewController : nsISupports
+{
+ /**
+ * The width of the preview image. This value is allowed to change at any
+ * time. See requestPreview for more information.
+ */
+ readonly attribute unsigned long width;
+
+ /**
+ * The height of the preview image. This value is allowed to change at any
+ * time. See requestPreview for more information.
+ */
+ readonly attribute unsigned long height;
+
+ /**
+ * The aspect ratio of the thumbnail - this does not need to match the ratio
+ * of the preview. This value is allowed to change at any time. See
+ * requestThumbnail for more information.
+ */
+ readonly attribute float thumbnailAspectRatio;
+
+ /**
+ * Invoked by nsITaskbarPreview when it needs to render the preview.
+ *
+ * @param aCallback Async callback the controller should invoke once
+ * the thumbnail is rendered. aCallback receives as its only parameter
+ * a canvas containing the preview image.
+ */
+ void requestPreview(in nsITaskbarPreviewCallback aCallback);
+
+ /**
+ * Note: it is guaranteed that width/height == thumbnailAspectRatio
+ * (modulo rounding errors)
+ *
+ * Also note that the context is not attached to a canvas element.
+ *
+ * @param aCallback Async callback the controller should invoke once
+ * the thumbnail is rendered. aCallback receives as its only parameter
+ * a canvas containing the thumbnail image. Canvas dimensions should
+ * match the requested width or height otherwise setting the thumbnail
+ * will fail.
+ * @param width The width of the requested thumbnail
+ * @param height The height of the requested thumbnail
+ */
+ void requestThumbnail(in nsITaskbarPreviewCallback aCallback,
+ in unsigned long width, in unsigned long height);
+
+ /**
+ * Invoked when the user presses the close button on the tab preview.
+ */
+ void onClose();
+
+ /**
+ * Invoked when the user clicks on the tab preview.
+ *
+ * @return true if the top level window corresponding to the preview should
+ * be activated, false if activation is not accepted.
+ */
+ boolean onActivate();
+
+ /**
+ * Invoked when one of the buttons on the window preview's toolbar is pressed.
+ *
+ * @param button The button that was pressed. This can be compared with the
+ * buttons returned by nsITaskbarWindowPreview.getButton.
+ */
+ void onClick(in nsITaskbarPreviewButton button);
+};
diff --git a/widget/nsITaskbarProgress.idl b/widget/nsITaskbarProgress.idl
new file mode 100644
index 0000000000..d81179d311
--- /dev/null
+++ b/widget/nsITaskbarProgress.idl
@@ -0,0 +1,58 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+#include "nsIBaseWindow.idl"
+
+typedef long nsTaskbarProgressState;
+
+/**
+ * Starting in Windows 7, applications can display a progress notification in
+ * the taskbar. This class wraps around the native functionality to do this.
+ */
+[scriptable, uuid(23ac257d-ef3c-4033-b424-be7fef91a86c)]
+interface nsITaskbarProgress : nsISupports
+{
+ /**
+ * Stop displaying progress on the taskbar button. This should be used when
+ * the operation is complete or cancelled.
+ */
+ const nsTaskbarProgressState STATE_NO_PROGRESS = 0;
+
+ /**
+ * Display a cycling, indeterminate progress bar.
+ */
+ const nsTaskbarProgressState STATE_INDETERMINATE = 1;
+
+ /**
+ * Display a determinate, normal progress bar.
+ */
+ const nsTaskbarProgressState STATE_NORMAL = 2;
+
+ /**
+ * Display a determinate, error progress bar.
+ */
+ const nsTaskbarProgressState STATE_ERROR = 3;
+
+ /**
+ * Display a determinate progress bar indicating that the operation has
+ * paused.
+ */
+ const nsTaskbarProgressState STATE_PAUSED = 4;
+
+ /**
+ * Sets the taskbar progress state and value for this window. The currentValue
+ * and maxValue parameters are optional and should be supplied when |state|
+ * is one of STATE_NORMAL, STATE_ERROR or STATE_PAUSED.
+ *
+ * @throws NS_ERROR_INVALID_ARG if state is STATE_NO_PROGRESS or
+ * STATE_INDETERMINATE, and either currentValue or maxValue is not 0.
+ * @throws NS_ERROR_ILLEGAL_VALUE if currentValue is greater than maxValue.
+ */
+ void setProgressState(in nsTaskbarProgressState state,
+ [optional] in unsigned long long currentValue,
+ [optional] in unsigned long long maxValue);
+};
diff --git a/widget/nsITaskbarTabPreview.idl b/widget/nsITaskbarTabPreview.idl
new file mode 100644
index 0000000000..47520e38a6
--- /dev/null
+++ b/widget/nsITaskbarTabPreview.idl
@@ -0,0 +1,62 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "nsITaskbarPreview.idl"
+interface imgIContainer;
+
+/*
+ * nsITaskbarTabPreview
+ *
+ * This interface controls tab preview-specific behavior. Creating an
+ * nsITaskbarTabPreview for a window will hide that window's
+ * nsITaskbarWindowPreview in the taskbar - the native API performs this
+ * unconditionally. When there are no more tab previews for a window, the
+ * nsITaskbarWindowPreview will automatically become visible again.
+ *
+ * An application may have as many tab previews per window as memory allows.
+ *
+ */
+[scriptable, builtinclass, uuid(11E4C8BD-5C2D-4E1A-A9A1-79DD5B0FE544)]
+interface nsITaskbarTabPreview : nsITaskbarPreview
+{
+ /**
+ * The title displayed above the thumbnail
+ *
+ * Default: an empty string
+ */
+ attribute AString title;
+
+ /**
+ * The icon displayed next to the title in the preview
+ *
+ * Default: null
+ */
+ attribute imgIContainer icon;
+
+ /**
+ * Rearranges the preview relative to another tab preview from the same window
+ * @param aNext The preview to the right of this one. A value of null
+ * indicates that the preview is the rightmost one.
+ */
+ void move(in nsITaskbarTabPreview aNext);
+
+ /**
+ * Used internally to grab the handle to the proxy window.
+ */
+ [notxpcom]
+ nativeWindow GetHWND();
+
+ /**
+ * Used internally to ensure that the taskbar knows about this preview. If a
+ * preview is not registered, then the API call to set its sibling (via move)
+ * will silently fail.
+ *
+ * This method is only invoked when it is safe to make taskbar API calls.
+ */
+ [notxpcom]
+ void EnsureRegistration();
+};
diff --git a/widget/nsITaskbarWindowPreview.idl b/widget/nsITaskbarWindowPreview.idl
new file mode 100644
index 0000000000..c73e8b696e
--- /dev/null
+++ b/widget/nsITaskbarWindowPreview.idl
@@ -0,0 +1,69 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "nsITaskbarPreview.idl"
+interface nsITaskbarPreviewButton;
+
+/*
+ * nsITaskbarWindowPreview
+ *
+ * This interface represents the preview for a window in the taskbar. By
+ * default, Windows implements much of the behavior for applications by
+ * default. The primary purpose of this interface is to allow Gecko
+ * applications to take control over parts of the preview. Some parts are not
+ * controlled through this interface: the title and icon of the preview match
+ * the title and icon of the window always.
+ *
+ * By default, Windows takes care of drawing the thumbnail and preview for the
+ * application however if enableCustomDrawing is set to true, then the
+ * controller will start to receive requestPreview and requestThumbnail calls
+ * as well as reads on the thumbnailAspectRatio, width and height properties.
+ *
+ * By default, nsITaskbarWindowPreviews are visible. When made invisible, the
+ * window disappears from the list of windows in the taskbar for the
+ * application.
+ *
+ * If the window has any visible nsITaskbarTabPreviews, then the
+ * nsITaskbarWindowPreview for the corresponding window is automatically
+ * hidden. This is not reflected in the visible property. Note that other parts
+ * of the system (such as alt-tab) may still request thumbnails and/or previews
+ * through the nsITaskbarWindowPreview's controller.
+ *
+ * nsITaskbarWindowPreview will never invoke the controller's onClose or
+ * onActivate methods since handling them may conflict with other internal
+ * Gecko state and there is existing infrastructure in place to allow clients
+ * to handle those events
+ *
+ * Window previews may have a toolbar with up to 7 buttons. See
+ * nsITaskbarPreviewButton for more information about button properties.
+ */
+[scriptable, uuid(EC67CC57-342D-4064-B4C6-74A375E07B10)]
+interface nsITaskbarWindowPreview : nsITaskbarPreview
+{
+ /**
+ * Max 7 buttons per preview per the Windows Taskbar API
+ */
+ const long NUM_TOOLBAR_BUTTONS = 7;
+
+ /**
+ * Gets the nth button for the preview image. By default, all of the buttons
+ * are invisible.
+ *
+ * @see nsITaskbarPreviewButton
+ *
+ * @param index The index into the button array. Must be >= 0 and <
+ * MAX_TOOLBAR_BUTTONS.
+ */
+ nsITaskbarPreviewButton getButton(in unsigned long index);
+
+ /**
+ * Enables/disables custom drawing of thumbnails and previews
+ *
+ * Default value: false
+ */
+ attribute boolean enableCustomDrawing;
+};
diff --git a/widget/nsITouchBarHelper.idl b/widget/nsITouchBarHelper.idl
new file mode 100644
index 0000000000..62d2e8593b
--- /dev/null
+++ b/widget/nsITouchBarHelper.idl
@@ -0,0 +1,65 @@
+/* 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 "nsIArray.idl"
+#include "nsISupports.idl"
+#include "nsITouchBarInput.idl"
+
+webidl Document;
+
+/**
+ * Back-to-frontend communication for the Touch Bar
+ */
+[scriptable, uuid(ea109912-3acc-48de-b679-c23b6a122da5)]
+interface nsITouchBarHelper : nsISupports
+{
+ /**
+ * Returns the active browser's URL.
+ */
+ readonly attribute AString activeUrl;
+
+ /**
+ * Return the active browser's page title.
+ */
+ readonly attribute AString activeTitle;
+
+ /**
+ * Return true if the Urlbar has focus.
+ */
+ readonly attribute boolean isUrlbarFocused;
+
+ /**
+ * Toggles Urlbar focus.
+ */
+ void toggleFocusUrlbar();
+
+ /**
+ * Unfocuses the Urlbar.
+ */
+ void unfocusUrlbar();
+
+ /**
+ * Returns all available Touch Bar Inputs in an nsIArray
+ * of nsITouchBarInput objects.
+ */
+ attribute nsIArray allItems;
+
+ /**
+ * The context in which this nsITouchBarHelper exists. Required to create
+ * an imgLoader to load our SVG icons.
+ */
+ readonly attribute Document document;
+
+ /**
+ * Returns the requested TouchBarInput.
+ * Exposed for testing.
+ */
+ nsITouchBarInput getTouchBarInput(in string aInputName);
+
+ /**
+ * Inserts a search restriction string in the Urlbar.
+ * Exposed for testing.
+ */
+ void insertRestrictionInUrlbar(in string aToken);
+};
diff --git a/widget/nsITouchBarInput.idl b/widget/nsITouchBarInput.idl
new file mode 100644
index 0000000000..0c9c3b0b1a
--- /dev/null
+++ b/widget/nsITouchBarInput.idl
@@ -0,0 +1,78 @@
+/* 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 "nsIArray.idl"
+#include "nsISupports.idl"
+#include "nsIURI.idl"
+
+[scriptable, function, uuid(001ab07c-1b3a-4dbf-a657-fada0065ff55)]
+interface nsITouchBarInputCallback : nsISupports
+{
+ void onCommand();
+};
+
+/**
+ * Implements an input to be registered on the Mac Touch Bar.
+ */
+
+[scriptable, uuid(77441d17-f29c-49d7-982f-f20a5ab5a900)]
+interface nsITouchBarInput : nsISupports
+{
+ readonly attribute AString key;
+
+ /**
+ * The lookup key for the button's localized text title.
+ */
+ attribute AString title;
+
+ /**
+ * The URI of an icon file.
+ */
+ attribute nsIURI image;
+
+ /**
+ * The type of the input.
+ * Takes one of:
+ * `button`:
+ * A standard button.
+ * If an image is available, only the image is displayed.
+ * `mainButton`:
+ * An extra-wide button. Displays both the image and title.
+ * `scrubber`:
+ * A Scrubber element. Not yet implemented, except in the case of Apple's
+ * pre-built Share scrubber.
+ * `popover`:
+ * An element that displays a new instance of nsTouchBar when tapped.
+ * The elements in the new Touch Bar should be defined in the
+ * input's `children` property.
+ * `label`:
+ * A text label.
+ * `scrollView`:
+ * Contains several buttons, defined in the input's `children` property.
+ * The user can scroll through the buttons.
+ */
+ attribute AString type;
+
+ /**
+ * A callback function to be invoked when an element is touched.
+ */
+ attribute nsITouchBarInputCallback callback;
+
+ /**
+ * A hexadecimal uint32_t specifying the input's
+ * background color. If omitted, the default background color is used.
+ */
+ attribute uint32_t color;
+
+ /**
+ * If `true`, the Touch Bar input is greyed out and inoperable.
+ */
+ attribute boolean disabled;
+
+ /**
+ * An array containing an input's children.
+ * Available for type = ("scrollView" || "popover").
+ */
+ attribute nsIArray children;
+};
diff --git a/widget/nsITouchBarUpdater.idl b/widget/nsITouchBarUpdater.idl
new file mode 100644
index 0000000000..f804191a08
--- /dev/null
+++ b/widget/nsITouchBarUpdater.idl
@@ -0,0 +1,42 @@
+/* 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 "nsIArray.idl"
+#include "nsIBaseWindow.idl"
+#include "nsISupports.idl"
+#include "nsITouchBarInput.idl"
+
+/**
+ * Front-to-backend communication to keep Touch Bar updated
+ */
+[scriptable, uuid(38f396e2-93c9-4a77-aaf7-2d50b9962186)]
+interface nsITouchBarUpdater : nsISupports
+{
+ /**
+ * Updates an array of nsITouchBarInputs in the specified window.
+ */
+ void updateTouchBarInputs(in nsIBaseWindow aWindow, in Array<nsITouchBarInput> aInputs);
+
+ /**
+ * Enter the native Touch Bar customization window.
+ */
+ void enterCustomizeMode();
+
+ /**
+ * Checks if the user is using a Touch Bar-compatible Mac.
+ */
+ boolean isTouchBarInitialized();
+
+ /**
+ * Sets whether the Touch Bar is initialized.
+ * NOTE: This method is for internal unit tests only! Normally, the system
+ * sets this value after a Touch Bar is initialized on compatible Macs.
+ */
+ void setTouchBarInitialized(in boolean aIsInitialized);
+
+ /**
+ * If aShowing is true, aPopover is shown. Otherwise, it is hidden.
+ */
+ void showPopover(in nsIBaseWindow aWindow, in nsITouchBarInput aPopover, in boolean aShowing);
+};
diff --git a/widget/nsITransferable.idl b/widget/nsITransferable.idl
new file mode 100644
index 0000000000..0a8419c4bb
--- /dev/null
+++ b/widget/nsITransferable.idl
@@ -0,0 +1,225 @@
+/* -*- Mode: IDL; 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 "nsIArray.idl"
+#include "nsISupports.idl"
+#include "nsIFormatConverter.idl"
+#include "nsIContentPolicy.idl"
+
+interface nsICookieJarSettings;
+interface nsIPrincipal;
+interface nsIReferrerInfo;
+
+%{ C++
+
+// Internal formats must have their second part starting with 'x-moz-',
+// for example text/x-moz-internaltype. These cannot be assigned by
+// unprivileged content but all other types can.
+#define kInternal_Mimetype_Prefix u"/x-moz-"_ns
+
+// these probably shouldn't live here, but in some central repository shared
+// by the entire app.
+#define kTextMime "text/plain"
+#define kRTFMime "text/rtf"
+#define kMozTextInternal "text/x-moz-text-internal" // text data which isn't suppoed to be parsed by other apps.
+#define kHTMLMime "text/html"
+#define kAOLMailMime "AOLMAIL"
+#define kPNGImageMime "image/png"
+#define kJPEGImageMime "image/jpeg"
+#define kJPGImageMime "image/jpg"
+#define kGIFImageMime "image/gif"
+#define kFileMime "application/x-moz-file"
+
+#define kURLMime "text/x-moz-url" // data contains url\ntitle
+#define kURLDataMime "text/x-moz-url-data" // data contains url only
+#define kURLDescriptionMime "text/x-moz-url-desc" // data contains description
+#define kURLPrivateMime "text/x-moz-url-priv" // same as kURLDataMime but for private uses
+#define kNativeImageMime "application/x-moz-nativeimage"
+#define kNativeHTMLMime "application/x-moz-nativehtml"
+
+// These are used to indicate the context for a fragment of HTML source, such
+// that some parent structure and style can be preserved. kHTMLContext
+// contains the serialized ancestor elements, whereas kHTMLInfo are numbers
+// identifying where in the context the fragment was from.
+#define kHTMLContext "text/_moz_htmlcontext"
+#define kHTMLInfo "text/_moz_htmlinfo"
+
+// Holds the MIME type from the image request. This is used to ensure the
+// local application handler for the request's MIME type accepts images with
+// the given filename extension (from kFilePromiseDestFilename). When the
+// image is dragged out, we replace the extension with a compatible extension.
+#define kImageRequestMime "text/x-moz-requestmime"
+
+// the source URL for a file promise
+#define kFilePromiseURLMime "application/x-moz-file-promise-url"
+// the destination filename for a file promise
+#define kFilePromiseDestFilename "application/x-moz-file-promise-dest-filename"
+// a dataless flavor used to interact with the OS during file drags
+#define kFilePromiseMime "application/x-moz-file-promise"
+// a synthetic flavor, put into the transferable once we know the destination directory of a file drag
+#define kFilePromiseDirectoryMime "application/x-moz-file-promise-dir"
+
+#define kCustomTypesMime "application/x-moz-custom-clipdata"
+
+#define kPDFJSMime "application/pdfjs"
+
+%}
+
+
+/**
+ * nsIFlavorDataProvider allows a flavor to 'promise' data later,
+ * supplying the data lazily.
+ *
+ * To use it, call setTransferData, passing the flavor string and
+ * a nsIFlavorDataProvider QI'd to nsISupports.
+ *
+ * When someone calls getTransferData later, if the data size is
+ * stored as 0, the nsISupports will be QI'd to nsIFlavorDataProvider,
+ * and its getFlavorData called.
+ *
+ */
+interface nsITransferable;
+interface nsILoadContext;
+
+[scriptable, uuid(7E225E5F-711C-11D7-9FAE-000393636592)]
+interface nsIFlavorDataProvider : nsISupports
+{
+
+ /**
+ * Retrieve the data from this data provider.
+ *
+ * @param aTransferable (in parameter) the transferable we're being called for.
+ * @param aFlavor (in parameter) the flavor of data to retrieve
+ * @param aData the data. Some variant of class in nsISupportsPrimitives.idl
+ */
+ void getFlavorData(in nsITransferable aTransferable, in string aFlavor, out nsISupports aData);
+};
+
+
+[scriptable, builtinclass, uuid(97e0c418-1c1e-4106-bad1-9fcb11dff2fe)]
+interface nsITransferable : nsISupports
+{
+ /**
+ * Initializes a transferable object. This should be called on all
+ * transferable objects. Failure to do so will result in fatal assertions in
+ * debug builds.
+ *
+ * The load context is used to track whether the transferable is storing privacy-
+ * sensitive information.
+ *
+ * To get the appropriate load context in Javascript callers, one needs to get
+ * to the document that the transferable corresponds to, and then get the load
+ * context from the document like this:
+ *
+ * var loadContext = doc.defaultView.docShell
+ * .QueryInterface(Ci.nsILoadContext);
+ *
+ * In C++ callers, if you have the corresponding document, you can just call
+ * Document::GetLoadContext to get to the load context object.
+ *
+ * @param aContext the load context associated with the transferable object.
+ * This can be set to null if a load context is not available.
+ */
+ void init(in nsILoadContext aContext);
+
+ /**
+ * Computes a list of flavors that the transferable can export, either
+ * through intrinsic knowledge or output data converters.
+ */
+ Array<ACString> flavorsTransferableCanExport();
+
+ /**
+ * Given a flavor retrieve the data.
+ *
+ * @param aFlavor (in parameter) the flavor of data to retrieve
+ * @param aData the data. Some variant of class in nsISupportsPrimitives.idl
+ */
+ [must_use] void getTransferData(in string aFlavor, out nsISupports aData);
+
+ /**
+ * Returns the first flavor which has data.
+ *
+ * @param aFlavor (out parameter) the flavor of data that was retrieved
+ * @param aData the data. Some variant of class in nsISupportsPrimitives.idl
+ */
+ void getAnyTransferData(out ACString aFlavor, out nsISupports aData);
+
+ ///////////////////////////////
+ // Setter part of interface
+ ///////////////////////////////
+
+ /**
+ * Computes a list of flavors that the transferable can
+ * accept into it, either through intrinsic knowledge or input data converters.
+ *
+ */
+ Array<ACString> flavorsTransferableCanImport();
+
+ /**
+ * Sets the data in the transferable with the specified flavor. The transferable
+ * will maintain its own copy the data, so it is not necessary to do that beforehand.
+ *
+ * @param aFlavor the flavor of data that is being set
+ * @param aData the data, either some variant of class in nsISupportsPrimitives.idl,
+ * an nsIFile, or an nsIFlavorDataProvider (see above)
+ */
+ void setTransferData(in string aFlavor, in nsISupports aData);
+
+ /**
+ * Removes the data from all flavors.
+ */
+ void clearAllData();
+
+ /**
+ * Add the data flavor, indicating that this transferable
+ * can receive this type of flavor
+ *
+ * @param aDataFlavor a new data flavor to handle
+ */
+ void addDataFlavor ( in string aDataFlavor ) ;
+
+ /**
+ * Removes the data flavor matching the given one (string compare) and the data
+ * that goes along with it.
+ *
+ * @param aDataFlavor a data flavor to remove
+ */
+ void removeDataFlavor ( in string aDataFlavor ) ;
+
+ attribute nsIFormatConverter converter;
+
+ /**
+ * Use of the SetIsPrivateData() method generated by isPrivateData attribute should
+ * be avoided as much as possible because the value set may not reflect the status
+ * of the context in which the transferable was created.
+ */
+ [notxpcom, nostdcall] attribute boolean isPrivateData;
+
+ /**
+ * The principal associated with this transferable. This could be either the
+ * node principal of the source DOM node from which this transferable was
+ * created, or the principal of the global from which this transferable was
+ * created.
+ * XXXedgar: Rename it to something more generic, bug 1867636.
+ */
+ [notxpcom, nostdcall] attribute nsIPrincipal requestingPrincipal;
+
+ /**
+ * the contentPolicyType for this transferable.
+ */
+ [notxpcom, nostdcall] attribute nsContentPolicyType contentPolicyType;
+
+ /**
+ * The cookieJarSettings of the source dom node this transferable was created
+ * from.
+ */
+ [notxpcom, nostdcall] attribute nsICookieJarSettings cookieJarSettings;
+
+ /**
+ * Used for initializing the referrer when downloading a file promise.
+ */
+ [notxpcom, nostdcall] attribute nsIReferrerInfo referrerInfo;
+};
diff --git a/widget/nsIUserIdleService.idl b/widget/nsIUserIdleService.idl
new file mode 100644
index 0000000000..3507232c36
--- /dev/null
+++ b/widget/nsIUserIdleService.idl
@@ -0,0 +1,86 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+interface nsIObserver;
+
+/**
+ * This interface lets you monitor how long the user has been 'idle',
+ * i.e. not used their mouse or keyboard. You can get the idle time directly,
+ * but in most cases you will want to register an observer for a predefined
+ * interval. The observer will get an 'idle' notification when the user is idle
+ * for that interval (or longer), and receive an 'active' notification when the
+ * user starts using their computer again.
+ */
+
+[scriptable, uuid(cc52f19a-63ae-4a1c-9cc3-e79eace0b471)]
+interface nsIUserIdleService : nsISupports
+{
+ /**
+ * The amount of time in milliseconds that has passed
+ * since the last user activity.
+ *
+ * If we do not have a valid idle time to report, 0 is returned
+ * (this can happen if the user never interacted with the browser
+ * at all, and if we are also unable to poll for idle time manually).
+ */
+ readonly attribute unsigned long idleTime;
+
+ /**
+ * Add an observer to be notified when the user idles for some period of
+ * time, and when they get back from that.
+ *
+ * @param observer the observer to be notified
+ * @param time the amount of time in seconds the user should be idle before
+ * the observer should be notified.
+ *
+ * @note
+ * The subject of the notification the observer will get is always the
+ * nsIUserIdleService itself.
+ * When the user goes idle, the observer topic is "idle" and when he gets
+ * back, the observer topic is "active".
+ * The data param for the notification contains the current user idle time.
+ *
+ * @note
+ * You can add the same observer twice.
+ * @note
+ * Most implementations need to poll the OS for idle info themselves,
+ * meaning your notifications could arrive with a delay up to the length
+ * of the polling interval in that implementation.
+ * Current implementations use a delay of 5 seconds.
+ */
+ void addIdleObserver(in nsIObserver observer, in unsigned long time);
+
+ /**
+ * Remove an observer registered with addIdleObserver.
+ * @param observer the observer that needs to be removed.
+ * @param time the amount of time they were listening for.
+ * @note
+ * Removing an observer will remove it once, for the idle time you specify.
+ * If you have added an observer multiple times, you will need to remove it
+ * just as many times.
+ */
+ void removeIdleObserver(in nsIObserver observer, in unsigned long time);
+
+ /**
+ * If true, the idle service is temporarily disabled, and all idle events
+ * will be ignored.
+ *
+ * This should only be used in automation.
+ */
+ attribute boolean disabled;
+};
+
+%{C++
+ /**
+ * Observer topic notification for idle window: OBSERVER_TOPIC_IDLE.
+ * Observer topic notification for active window: OBSERVER_TOPIC_ACTIVE.
+ */
+
+ #define OBSERVER_TOPIC_IDLE "idle"
+ #define OBSERVER_TOPIC_ACTIVE "active"
+ #define OBSERVER_TOPIC_IDLE_DAILY "idle-daily"
+%}
diff --git a/widget/nsIUserIdleServiceInternal.idl b/widget/nsIUserIdleServiceInternal.idl
new file mode 100644
index 0000000000..044b95de3b
--- /dev/null
+++ b/widget/nsIUserIdleServiceInternal.idl
@@ -0,0 +1,17 @@
+/* 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 "nsIUserIdleService.idl"
+
+[scriptable, uuid(7b89a2e7-ed12-42e0-b86d-4984239abd7b)]
+interface nsIUserIdleServiceInternal : nsIUserIdleService
+{
+ /**
+ * "Resets the idle time to the value specified."
+ *
+ * @param idleDelta the time (in milliseconds) since the last user inter
+ * action
+ **/
+ void resetIdleTimeOut(in unsigned long idleDeltaInMS);
+};
diff --git a/widget/nsIWidget.h b/widget/nsIWidget.h
new file mode 100644
index 0000000000..81e952a8c6
--- /dev/null
+++ b/widget/nsIWidget.h
@@ -0,0 +1,2144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsIWidget_h__
+#define nsIWidget_h__
+
+#include <cmath>
+#include <cstdint>
+#include "imgIContainer.h"
+#include "ErrorList.h"
+#include "Units.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/Matrix.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "mozilla/layers/ZoomConstraints.h"
+#include "mozilla/image/Resolution.h"
+#include "mozilla/widget/IMEData.h"
+#include "nsCOMPtr.h"
+#include "nsColor.h"
+#include "nsDebug.h"
+#include "nsID.h"
+#include "nsIObserver.h"
+#include "nsISupports.h"
+#include "nsITheme.h"
+#include "nsITimer.h"
+#include "nsIWidgetListener.h"
+#include "nsRect.h"
+#include "nsSize.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashMap.h"
+#include "mozilla/widget/InitData.h"
+#include "nsXULAppAPI.h"
+
+// forward declarations
+class nsIBidiKeyboard;
+class nsIRollupListener;
+class nsIContent;
+class ViewWrapper;
+class nsIRunnable;
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+class VsyncDispatcher;
+class WidgetGUIEvent;
+class WidgetInputEvent;
+class WidgetKeyboardEvent;
+struct FontRange;
+enum class ColorScheme : uint8_t;
+enum class WindowButtonType : uint8_t;
+
+enum class WindowShadow : uint8_t {
+ None,
+ Menu,
+ Panel,
+ Tooltip,
+};
+
+#if defined(MOZ_WIDGET_ANDROID)
+namespace ipc {
+class Shmem;
+}
+#endif // defined(MOZ_WIDGET_ANDROID)
+namespace dom {
+class BrowserChild;
+enum class CallerType : uint32_t;
+} // namespace dom
+class WindowRenderer;
+namespace layers {
+class AsyncDragMetrics;
+class Compositor;
+class CompositorBridgeChild;
+struct FrameMetrics;
+class LayerManager;
+class WebRenderBridgeChild;
+} // namespace layers
+namespace widget {
+class TextEventDispatcher;
+class TextEventDispatcherListener;
+class CompositorWidget;
+class CompositorWidgetInitData;
+class Screen;
+} // namespace widget
+namespace wr {
+class DisplayListBuilder;
+class IpcResourceUpdateQueue;
+enum class RenderRoot : uint8_t;
+} // namespace wr
+} // namespace mozilla
+
+/**
+ * Callback function that processes events.
+ *
+ * The argument is actually a subtype (subclass) of WidgetEvent which carries
+ * platform specific information about the event. Platform specific code
+ * knows how to deal with it.
+ *
+ * The return value determines whether or not the default action should take
+ * place.
+ */
+typedef nsEventStatus (*EVENT_CALLBACK)(mozilla::WidgetGUIEvent* aEvent);
+
+// Hide the native window system's real window type so as to avoid
+// including native window system types and APIs. This is necessary
+// to ensure cross-platform code.
+typedef void* nsNativeWidget;
+
+/**
+ * Values for the GetNativeData function
+ */
+#define NS_NATIVE_WINDOW 0
+#define NS_NATIVE_GRAPHIC 1
+#define NS_NATIVE_WIDGET 3
+#define NS_NATIVE_REGION 5
+#define NS_NATIVE_OFFSETX 6
+#define NS_NATIVE_OFFSETY 7
+#define NS_NATIVE_SCREEN 9
+// The toplevel GtkWidget containing this nsIWidget:
+#define NS_NATIVE_SHELLWIDGET 10
+#define NS_NATIVE_OPENGL_CONTEXT 12
+// This is available only with GetNativeData() in parent process. Anybody
+// shouldn't access this pointer as a valid pointer since the result may be
+// special value like NS_ONLY_ONE_NATIVE_IME_CONTEXT. So, the result is just
+// an identifier of distinguishing a text composition is caused by which native
+// IME context. Note that the result is only valid in the process. So,
+// XP code should use nsIWidget::GetNativeIMEContext() instead of using this.
+#define NS_RAW_NATIVE_IME_CONTEXT 14
+#define NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID 15
+#ifdef XP_WIN
+# define NS_NATIVE_TSF_THREAD_MGR 100
+# define NS_NATIVE_TSF_CATEGORY_MGR 101
+# define NS_NATIVE_TSF_DISPLAY_ATTR_MGR 102
+# define NS_NATIVE_ICOREWINDOW 103 // winrt specific
+#endif
+#if defined(MOZ_WIDGET_GTK)
+# define NS_NATIVE_EGL_WINDOW 106
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+# define NS_JAVA_SURFACE 100
+#endif
+
+#define MOZ_WIDGET_MAX_SIZE 16384
+#define MOZ_WIDGET_INVALID_SCALE 0.0
+
+// Must be kept in sync with xpcom/rust/xpcom/src/interfaces/nonidl.rs
+#define NS_IWIDGET_IID \
+ { \
+ 0x06396bf6, 0x2dd8, 0x45e5, { \
+ 0xac, 0x45, 0x75, 0x26, 0x53, 0xb1, 0xc9, 0x80 \
+ } \
+ }
+
+/**
+ * Cursor types.
+ */
+
+enum nsCursor { ///(normal cursor, usually rendered as an arrow)
+ eCursor_standard,
+ ///(system is busy, usually rendered as a hourglass or watch)
+ eCursor_wait,
+ ///(Selecting something, usually rendered as an IBeam)
+ eCursor_select,
+ ///(can hyper-link, usually rendered as a human hand)
+ eCursor_hyperlink,
+ ///(north/south/west/east edge sizing)
+ eCursor_n_resize,
+ eCursor_s_resize,
+ eCursor_w_resize,
+ eCursor_e_resize,
+ ///(corner sizing)
+ eCursor_nw_resize,
+ eCursor_se_resize,
+ eCursor_ne_resize,
+ eCursor_sw_resize,
+ eCursor_crosshair,
+ eCursor_move,
+ eCursor_help,
+ eCursor_copy, // CSS3
+ eCursor_alias,
+ eCursor_context_menu,
+ eCursor_cell,
+ eCursor_grab,
+ eCursor_grabbing,
+ eCursor_spinning,
+ eCursor_zoom_in,
+ eCursor_zoom_out,
+ eCursor_not_allowed,
+ eCursor_col_resize,
+ eCursor_row_resize,
+ eCursor_no_drop,
+ eCursor_vertical_text,
+ eCursor_all_scroll,
+ eCursor_nesw_resize,
+ eCursor_nwse_resize,
+ eCursor_ns_resize,
+ eCursor_ew_resize,
+ eCursor_none,
+ // This one is used for array sizing, and so better be the last
+ // one in this list...
+ eCursorCount,
+
+ // ...except for this one.
+ eCursorInvalid = eCursorCount + 1
+};
+
+enum nsTopLevelWidgetZPlacement { // for PlaceBehind()
+ eZPlacementBottom = 0, // bottom of the window stack
+ eZPlacementBelow, // just below another widget
+ eZPlacementTop // top of the window stack
+};
+
+/**
+ * Before the OS goes to sleep, this topic is notified.
+ */
+#define NS_WIDGET_SLEEP_OBSERVER_TOPIC "sleep_notification"
+
+/**
+ * After the OS wakes up, this topic is notified.
+ */
+#define NS_WIDGET_WAKE_OBSERVER_TOPIC "wake_notification"
+
+/**
+ * Before the OS suspends the current process, this topic is notified. Some
+ * OS will kill processes that are suspended instead of resuming them.
+ * For that reason this topic may be useful to safely close down resources.
+ */
+#define NS_WIDGET_SUSPEND_PROCESS_OBSERVER_TOPIC "suspend_process_notification"
+
+/**
+ * After the current process resumes from being suspended, this topic is
+ * notified.
+ */
+#define NS_WIDGET_RESUME_PROCESS_OBSERVER_TOPIC "resume_process_notification"
+
+/**
+ * When an app(-shell) is activated by the OS, this topic is notified.
+ * Currently, this only happens on Mac OSX.
+ */
+#define NS_WIDGET_MAC_APP_ACTIVATE_OBSERVER_TOPIC "mac_app_activate"
+
+namespace mozilla::widget {
+
+/**
+ * Size constraints for setting the minimum and maximum size of a widget.
+ * Values are in device pixels.
+ */
+struct SizeConstraints {
+ SizeConstraints()
+ : mMaxSize(MOZ_WIDGET_MAX_SIZE, MOZ_WIDGET_MAX_SIZE),
+ mScale(MOZ_WIDGET_INVALID_SCALE) {}
+
+ SizeConstraints(mozilla::LayoutDeviceIntSize aMinSize,
+ mozilla::LayoutDeviceIntSize aMaxSize,
+ mozilla::DesktopToLayoutDeviceScale aScale)
+ : mMinSize(aMinSize), mMaxSize(aMaxSize), mScale(aScale) {
+ if (mMaxSize.width > MOZ_WIDGET_MAX_SIZE) {
+ mMaxSize.width = MOZ_WIDGET_MAX_SIZE;
+ }
+ if (mMaxSize.height > MOZ_WIDGET_MAX_SIZE) {
+ mMaxSize.height = MOZ_WIDGET_MAX_SIZE;
+ }
+ }
+
+ mozilla::LayoutDeviceIntSize mMinSize;
+ mozilla::LayoutDeviceIntSize mMaxSize;
+
+ /*
+ * The scale used to convert from desktop to device dimensions.
+ * MOZ_WIDGET_INVALID_SCALE if the value is not known.
+ *
+ * Bug 1701109 is filed to revisit adding of 'mScale' and deal
+ * with multi-monitor scaling issues in more complete way across
+ * all widget implementations.
+ */
+ mozilla::DesktopToLayoutDeviceScale mScale;
+};
+
+struct AutoObserverNotifier {
+ AutoObserverNotifier(nsIObserver* aObserver, const char* aTopic)
+ : mObserver(aObserver), mTopic(aTopic) {}
+
+ void SkipNotification() { mObserver = nullptr; }
+
+ uint64_t SaveObserver() {
+ if (!mObserver) {
+ return 0;
+ }
+ uint64_t observerId = ++sObserverId;
+ sSavedObservers.InsertOrUpdate(observerId, mObserver);
+ SkipNotification();
+ return observerId;
+ }
+
+ ~AutoObserverNotifier() {
+ if (mObserver) {
+ mObserver->Observe(nullptr, mTopic, nullptr);
+ }
+ }
+
+ static void NotifySavedObserver(const uint64_t& aObserverId,
+ const char* aTopic) {
+ nsCOMPtr<nsIObserver> observer = sSavedObservers.Get(aObserverId);
+ if (!observer) {
+ MOZ_ASSERT(aObserverId == 0,
+ "We should always find a saved observer for nonzero IDs");
+ return;
+ }
+
+ sSavedObservers.Remove(aObserverId);
+ observer->Observe(nullptr, aTopic, nullptr);
+ }
+
+ private:
+ nsCOMPtr<nsIObserver> mObserver;
+ const char* mTopic;
+
+ private:
+ static uint64_t sObserverId;
+ static nsTHashMap<uint64_t, nsCOMPtr<nsIObserver>> sSavedObservers;
+};
+
+} // namespace mozilla::widget
+
+/**
+ * The base class for all the widgets. It provides the interface for
+ * all basic and necessary functionality.
+ */
+class nsIWidget : public nsISupports {
+ protected:
+ typedef mozilla::dom::BrowserChild BrowserChild;
+
+ public:
+ typedef mozilla::layers::CompositorBridgeChild CompositorBridgeChild;
+ typedef mozilla::layers::AsyncDragMetrics AsyncDragMetrics;
+ typedef mozilla::layers::FrameMetrics FrameMetrics;
+ typedef mozilla::layers::LayerManager LayerManager;
+ typedef mozilla::WindowRenderer WindowRenderer;
+ typedef mozilla::layers::LayersBackend LayersBackend;
+ typedef mozilla::layers::LayersId LayersId;
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+ typedef mozilla::layers::ZoomConstraints ZoomConstraints;
+ typedef mozilla::widget::IMEEnabled IMEEnabled;
+ typedef mozilla::widget::IMEMessage IMEMessage;
+ typedef mozilla::widget::IMENotification IMENotification;
+ typedef mozilla::widget::IMENotificationRequests IMENotificationRequests;
+ typedef mozilla::widget::IMEState IMEState;
+ typedef mozilla::widget::InputContext InputContext;
+ typedef mozilla::widget::InputContextAction InputContextAction;
+ typedef mozilla::widget::NativeIMEContext NativeIMEContext;
+ typedef mozilla::widget::SizeConstraints SizeConstraints;
+ typedef mozilla::widget::TextEventDispatcher TextEventDispatcher;
+ typedef mozilla::widget::TextEventDispatcherListener
+ TextEventDispatcherListener;
+ typedef mozilla::LayoutDeviceIntMargin LayoutDeviceIntMargin;
+ typedef mozilla::LayoutDeviceIntPoint LayoutDeviceIntPoint;
+ typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect;
+ typedef mozilla::LayoutDeviceIntRegion LayoutDeviceIntRegion;
+ typedef mozilla::LayoutDeviceIntSize LayoutDeviceIntSize;
+ typedef mozilla::ScreenIntPoint ScreenIntPoint;
+ typedef mozilla::ScreenIntMargin ScreenIntMargin;
+ typedef mozilla::ScreenIntSize ScreenIntSize;
+ typedef mozilla::ScreenPoint ScreenPoint;
+ typedef mozilla::CSSToScreenScale CSSToScreenScale;
+ typedef mozilla::DesktopIntRect DesktopIntRect;
+ typedef mozilla::DesktopPoint DesktopPoint;
+ typedef mozilla::DesktopIntPoint DesktopIntPoint;
+ typedef mozilla::DesktopRect DesktopRect;
+ typedef mozilla::DesktopSize DesktopSize;
+ typedef mozilla::CSSPoint CSSPoint;
+ typedef mozilla::CSSRect CSSRect;
+
+ using InitData = mozilla::widget::InitData;
+ using WindowType = mozilla::widget::WindowType;
+ using PopupType = mozilla::widget::PopupType;
+ using PopupLevel = mozilla::widget::PopupLevel;
+ using BorderStyle = mozilla::widget::BorderStyle;
+ using TransparencyMode = mozilla::widget::TransparencyMode;
+ using Screen = mozilla::widget::Screen;
+
+ // Used in UpdateThemeGeometries.
+ struct ThemeGeometry {
+ // The ThemeGeometryType value for the themed widget, see
+ // nsITheme::ThemeGeometryTypeForWidget.
+ nsITheme::ThemeGeometryType mType;
+ // The device-pixel rect within the window for the themed widget
+ LayoutDeviceIntRect mRect;
+
+ ThemeGeometry(nsITheme::ThemeGeometryType aType,
+ const LayoutDeviceIntRect& aRect)
+ : mType(aType), mRect(aRect) {}
+ };
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IWIDGET_IID)
+
+ nsIWidget()
+ : mLastChild(nullptr),
+ mPrevSibling(nullptr),
+ mOnDestroyCalled(false),
+ mWindowType(WindowType::Child),
+ mZIndex(0)
+
+ {
+ ClearNativeTouchSequence(nullptr);
+ }
+
+ /**
+ * Create and initialize a widget.
+ *
+ * All the arguments can be null in which case a top level window
+ * with size 0 is created. The event callback function has to be
+ * provided only if the caller wants to deal with the events this
+ * widget receives. The event callback is basically a preprocess
+ * hook called synchronously. The return value determines whether
+ * the event goes to the default window procedure or it is hidden
+ * to the os. The assumption is that if the event handler returns
+ * false the widget does not see the event. The widget should not
+ * automatically clear the window to the background color. The
+ * calling code must handle paint messages and clear the background
+ * itself.
+ *
+ * In practice at least one of aParent and aNativeParent will be null. If
+ * both are null the widget isn't parented (e.g. context menus or
+ * independent top level windows).
+ *
+ * The dimensions given in aRect are specified in the parent's
+ * device coordinate system.
+ * This must not be called for parentless widgets such as top-level
+ * windows, which use the desktop pixel coordinate system; a separate
+ * method is provided for these.
+ *
+ * @param aParent parent nsIWidget
+ * @param aNativeParent native parent widget
+ * @param aRect the widget dimension
+ * @param aInitData data that is used for widget initialization
+ *
+ */
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* = nullptr) = 0;
+
+ /*
+ * As above, but with aRect specified in DesktopPixel units (for top-level
+ * widgets).
+ * Default implementation just converts aRect to device pixels and calls
+ * through to device-pixel Create, but platforms may override this if the
+ * mapping is not straightforward or the native platform needs to use the
+ * desktop pixel values directly.
+ */
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const DesktopIntRect& aRect,
+ InitData* aInitData = nullptr) {
+ LayoutDeviceIntRect devPixRect =
+ RoundedToInt(aRect * GetDesktopToDeviceScale());
+ return Create(aParent, aNativeParent, devPixRect, aInitData);
+ }
+
+ /**
+ * Allocate, initialize, and return a widget that is a child of
+ * |this|. The returned widget (if nonnull) has gone through the
+ * equivalent of CreateInstance(widgetCID) + Create(...).
+ *
+ * |CreateChild()| lets widget backends decide whether to parent
+ * the new child widget to this, nonnatively parent it, or both.
+ * This interface exists to support the PuppetWidget backend,
+ * which is entirely non-native. All other params are the same as
+ * for |Create()|.
+ *
+ * |aForceUseIWidgetParent| forces |CreateChild()| to only use the
+ * |nsIWidget*| this, not its native widget (if it exists), when
+ * calling |Create()|. This is a timid hack around poorly
+ * understood code, and shouldn't be used in new code.
+ */
+ virtual already_AddRefed<nsIWidget> CreateChild(
+ const LayoutDeviceIntRect& aRect, InitData* = nullptr,
+ bool aForceUseIWidgetParent = false) = 0;
+
+ /**
+ * Attach to a top level widget.
+ *
+ * In cases where a top level chrome widget is being used as a content
+ * container, attach a secondary listener and update the device
+ * context. The primary widget listener will continue to be called for
+ * notifications relating to the top-level window, whereas other
+ * notifications such as painting and events will instead be called via
+ * the attached listener. SetAttachedWidgetListener should be used to
+ * assign the attached listener.
+ *
+ * aUseAttachedEvents if true, events are sent to the attached listener
+ * instead of the normal listener.
+ */
+ virtual void AttachViewToTopLevel(bool aUseAttachedEvents) = 0;
+
+ /**
+ * Accessor functions to get and set the attached listener. Used by
+ * nsView in connection with AttachViewToTopLevel above.
+ */
+ virtual void SetAttachedWidgetListener(nsIWidgetListener* aListener) = 0;
+ virtual nsIWidgetListener* GetAttachedWidgetListener() const = 0;
+ virtual void SetPreviouslyAttachedWidgetListener(
+ nsIWidgetListener* aListener) = 0;
+ virtual nsIWidgetListener* GetPreviouslyAttachedWidgetListener() = 0;
+
+ /**
+ * Notifies the root widget of a non-blank paint.
+ */
+ virtual void DidGetNonBlankPaint() {}
+
+ /**
+ * Accessor functions to get and set the listener which handles various
+ * actions for the widget.
+ */
+ //@{
+ virtual nsIWidgetListener* GetWidgetListener() const = 0;
+ virtual void SetWidgetListener(nsIWidgetListener* alistener) = 0;
+ //@}
+
+ /**
+ * Close and destroy the internal native window.
+ * This method does not delete the widget.
+ */
+
+ virtual void Destroy() = 0;
+
+ /**
+ * Destroyed() returns true if Destroy() has been called already.
+ * Otherwise, false.
+ */
+ bool Destroyed() const { return mOnDestroyCalled; }
+
+ /**
+ * Reparent a widget
+ *
+ * Change the widget's parent. Null parents are allowed.
+ *
+ * @param aNewParent new parent
+ */
+ virtual void SetParent(nsIWidget* aNewParent) = 0;
+
+ /**
+ * Return the parent Widget of this Widget or nullptr if this is a
+ * top level window
+ *
+ * @return the parent widget or nullptr if it does not have a parent
+ *
+ */
+ virtual nsIWidget* GetParent(void) = 0;
+
+ /**
+ * Return the top level Widget of this Widget
+ *
+ * @return the top level widget
+ */
+ virtual nsIWidget* GetTopLevelWidget() = 0;
+
+ /**
+ * Return the top (non-sheet) parent of this Widget if it's a sheet,
+ * or nullptr if this isn't a sheet (or some other error occurred).
+ * Sheets are only supported on some platforms (currently only macOS).
+ *
+ * @return the top (non-sheet) parent widget or nullptr
+ *
+ */
+ virtual nsIWidget* GetSheetWindowParent(void) = 0;
+
+ /**
+ * Return the physical DPI of the screen containing the window ...
+ * the number of device pixels per inch.
+ */
+ virtual float GetDPI() = 0;
+
+ /**
+ * Fallback DPI for when there's no widget available.
+ */
+ static float GetFallbackDPI();
+
+ /**
+ * Return the scaling factor between device pixels and the platform-
+ * dependent "desktop pixels" used to manage window positions on a
+ * potentially multi-screen, mixed-resolution desktop.
+ */
+ virtual mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() = 0;
+
+ /**
+ * Return the scaling factor between device pixels and the platform-
+ * dependent "desktop pixels" by looking up the screen by the position
+ * of the widget.
+ */
+ virtual mozilla::DesktopToLayoutDeviceScale
+ GetDesktopToDeviceScaleByScreen() = 0;
+
+ /**
+ * Return the default scale factor for the window. This is the
+ * default number of device pixels per CSS pixel to use. This should
+ * depend on OS/platform settings such as the Mac's "UI scale factor"
+ * or Windows' "font DPI". This will take into account Gecko preferences
+ * overriding the system setting.
+ */
+ mozilla::CSSToLayoutDeviceScale GetDefaultScale();
+
+ /**
+ * Fallback default scale for when there's no widget available.
+ */
+ static mozilla::CSSToLayoutDeviceScale GetFallbackDefaultScale();
+
+ /**
+ * Return the first child of this widget. Will return null if
+ * there are no children.
+ */
+ nsIWidget* GetFirstChild() const { return mFirstChild; }
+
+ /**
+ * Return the last child of this widget. Will return null if
+ * there are no children.
+ */
+ nsIWidget* GetLastChild() const { return mLastChild; }
+
+ /**
+ * Return the next sibling of this widget
+ */
+ nsIWidget* GetNextSibling() const { return mNextSibling; }
+
+ /**
+ * Set the next sibling of this widget
+ */
+ void SetNextSibling(nsIWidget* aSibling) { mNextSibling = aSibling; }
+
+ /**
+ * Return the previous sibling of this widget
+ */
+ nsIWidget* GetPrevSibling() const { return mPrevSibling; }
+
+ /**
+ * Set the previous sibling of this widget
+ */
+ void SetPrevSibling(nsIWidget* aSibling) { mPrevSibling = aSibling; }
+
+ /**
+ * Show or hide this widget
+ *
+ * @param aState true to show the Widget, false to hide it
+ *
+ */
+ virtual void Show(bool aState) = 0;
+
+ /**
+ * Whether or not a widget must be recreated after being hidden to show
+ * again properly.
+ */
+ virtual bool NeedsRecreateToReshow() { return false; }
+
+ /**
+ * Make the window modal.
+ */
+ virtual void SetModal(bool aModal) = 0;
+
+ /**
+ * Make the non-modal window opened by modal window fake-modal, that will
+ * call SetFakeModal(false) on destroy on Cocoa.
+ */
+ virtual void SetFakeModal(bool aModal) { SetModal(aModal); }
+
+ /**
+ * Are we app modal. Currently only implemented on Cocoa.
+ */
+ virtual bool IsRunningAppModal() { return false; }
+
+ /**
+ * The maximum number of simultaneous touch contacts supported by the device.
+ * In the case of devices with multiple digitizers (e.g. multiple touch
+ * screens), the value will be the maximum of the set of maximum supported
+ * contacts by each individual digitizer.
+ */
+ virtual uint32_t GetMaxTouchPoints() const = 0;
+
+ /**
+ * Returns whether the window is visible
+ *
+ */
+ virtual bool IsVisible() const = 0;
+
+ /**
+ * Returns whether the window has allocated resources so
+ * we can paint into it.
+ * Recently it's used on Linux/Gtk where we should not paint
+ * to invisible window.
+ */
+ virtual bool IsMapped() const { return true; }
+
+ /**
+ * Perform platform-dependent sanity check on a potential window position.
+ * This is guaranteed to work only for top-level windows.
+ */
+ virtual void ConstrainPosition(DesktopIntPoint&) = 0;
+
+ /**
+ * NOTE:
+ *
+ * For a top-level window widget, the "parent's coordinate system" is the
+ * "global" display pixel coordinate space, *not* device pixels (which
+ * may be inconsistent between multiple screens, at least in the Mac OS
+ * case with mixed hi-dpi and lo-dpi displays). This applies to all the
+ * following Move and Resize widget APIs.
+ *
+ * The display-/device-pixel distinction becomes important for (at least)
+ * macOS with Hi-DPI (retina) displays, and Windows when the UI scale factor
+ * is set to other than 100%.
+ *
+ * The Move and Resize methods take floating-point parameters, rather than
+ * integer ones. This is important when manipulating top-level widgets,
+ * where the coordinate system may not be an integral multiple of the
+ * device-pixel space.
+ **/
+
+ /**
+ * Move this widget.
+ *
+ * Coordinates refer to the top-left of the widget. For toplevel windows
+ * with decorations, this is the top-left of the titlebar and frame .
+ *
+ * @param aX the new x position expressed in the parent's coordinate system
+ * @param aY the new y position expressed in the parent's coordinate system
+ *
+ **/
+ virtual void Move(double aX, double aY) = 0;
+
+ /**
+ * Reposition this widget so that the client area has the given offset.
+ *
+ * @param aOffset the new offset of the client area expressed as an
+ * offset from the origin of the client area of the parent
+ * widget (for root widgets and popup widgets it is in
+ * screen coordinates)
+ **/
+ virtual void MoveClient(const DesktopPoint& aOffset) = 0;
+
+ /**
+ * Resize this widget. Any size constraints set for the window by a
+ * previous call to SetSizeConstraints will be applied.
+ *
+ * @param aWidth the new width expressed in the parent's coordinate system
+ * @param aHeight the new height expressed in the parent's coordinate
+ * system
+ * @param aRepaint whether the widget should be repainted
+ */
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) = 0;
+
+ /**
+ * Lock the aspect ratio of a Window
+ *
+ * @param aShouldLock bool
+ *
+ */
+ virtual void LockAspectRatio(bool aShouldLock){};
+
+ /**
+ * Move or resize this widget. Any size constraints set for the window by
+ * a previous call to SetSizeConstraints will be applied.
+ *
+ * @param aX the new x position expressed in the parent's coordinate
+ * system
+ * @param aY the new y position expressed in the parent's coordinate
+ * system
+ * @param aWidth the new width expressed in the parent's coordinate system
+ * @param aHeight the new height expressed in the parent's coordinate
+ * system
+ * @param aRepaint whether the widget should be repainted if the size
+ * changes
+ *
+ */
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) = 0;
+
+ virtual mozilla::Maybe<bool> IsResizingNativeWidget() {
+ return mozilla::Nothing();
+ }
+
+ /**
+ * Resize the widget so that the inner client area has the given size.
+ *
+ * @param aSize the new size of the client area.
+ * @param aRepaint whether the widget should be repainted
+ */
+ virtual void ResizeClient(const DesktopSize& aSize, bool aRepaint) = 0;
+
+ /**
+ * Resize and reposition the widget so tht inner client area has the given
+ * offset and size.
+ *
+ * @param aRect the new offset and size of the client area. The offset is
+ * expressed as an offset from the origin of the client area
+ * of the parent widget (for root widgets and popup widgets it
+ * is in screen coordinates).
+ * @param aRepaint whether the widget should be repainted
+ */
+ virtual void ResizeClient(const DesktopRect& aRect, bool aRepaint) = 0;
+
+ /**
+ * Sets the widget's z-index.
+ */
+ virtual void SetZIndex(int32_t aZIndex) = 0;
+
+ /**
+ * Gets the widget's z-index.
+ */
+ int32_t GetZIndex() { return mZIndex; }
+
+ /**
+ * Position this widget just behind the given widget. (Used to
+ * control z-order for top-level widgets. Get/SetZIndex by contrast
+ * control z-order for child widgets of other widgets.)
+ * @param aPlacement top, bottom, or below a widget
+ * (if top or bottom, param aWidget is ignored)
+ * @param aWidget widget to place this widget behind
+ * (only if aPlacement is eZPlacementBelow).
+ * null is equivalent to aPlacement of eZPlacementTop
+ * @param aActivate true to activate the widget after placing it
+ */
+ virtual void PlaceBehind(nsTopLevelWidgetZPlacement aPlacement,
+ nsIWidget* aWidget, bool aActivate) = 0;
+
+ /**
+ * Minimize, maximize or normalize the window size.
+ * Takes a value from nsSizeMode (see nsIWidgetListener.h)
+ */
+ virtual void SetSizeMode(nsSizeMode aMode) = 0;
+
+ virtual void GetWorkspaceID(nsAString& workspaceID) = 0;
+
+ virtual void MoveToWorkspace(const nsAString& workspaceID) = 0;
+
+ /**
+ * Suppress animations that are applied to a window by OS.
+ */
+ virtual void SuppressAnimation(bool aSuppress) {}
+
+ /**
+ * Return size mode (minimized, maximized, normalized).
+ * Returns a value from nsSizeMode (see nsIWidgetListener.h)
+ */
+ virtual nsSizeMode SizeMode() = 0;
+
+ /**
+ * Ask whether the window is tiled.
+ */
+ virtual bool IsTiled() const = 0;
+
+ /**
+ * Ask wether the widget is fully occluded
+ */
+ virtual bool IsFullyOccluded() const = 0;
+
+ /**
+ * Enable or disable this Widget
+ *
+ * @param aState true to enable the Widget, false to disable it.
+ */
+ virtual void Enable(bool aState) = 0;
+
+ /**
+ * Ask whether the widget is enabled
+ */
+ virtual bool IsEnabled() const = 0;
+
+ /*
+ * Whether we should request activation of this widget's toplevel window.
+ */
+ enum class Raise {
+ No,
+ Yes,
+ };
+
+ /**
+ * Request activation of this window or give focus to this widget.
+ */
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) = 0;
+
+ /**
+ * Get this widget's outside dimensions relative to its parent widget. For
+ * popup widgets the returned rect is in screen coordinates and not
+ * relative to its parent widget.
+ *
+ * @return the x, y, width and height of this widget.
+ */
+ virtual LayoutDeviceIntRect GetBounds() = 0;
+
+ /**
+ * Get this widget's outside dimensions in device coordinates. This
+ * includes any title bar on the window.
+ *
+ * @return the x, y, width and height of this widget.
+ */
+ virtual LayoutDeviceIntRect GetScreenBounds() = 0;
+
+ /**
+ * Similar to GetScreenBounds except that this function will always
+ * get the size when the widget is in the nsSizeMode_Normal size mode
+ * even if the current size mode is not nsSizeMode_Normal.
+ * This method will fail if the size mode is not nsSizeMode_Normal and
+ * the platform doesn't have the ability.
+ * This method will always succeed if the current size mode is
+ * nsSizeMode_Normal.
+ *
+ * @param aRect On return it holds the x, y, width and height of
+ * this widget.
+ */
+ [[nodiscard]] virtual nsresult GetRestoredBounds(
+ LayoutDeviceIntRect& aRect) = 0;
+
+ /**
+ * Get this widget's client area bounds, if the window has a 3D border
+ * appearance this returns the area inside the border. The position is the
+ * position of the client area relative to the client area of the parent
+ * widget (for root widgets and popup widgets it is in screen coordinates).
+ *
+ * @return the x, y, width and height of the client area of this widget.
+ */
+ virtual LayoutDeviceIntRect GetClientBounds() = 0;
+
+ /**
+ * Sets the non-client area dimensions of the window. Pass -1 to restore
+ * the system default frame size for that border. Pass zero to remove
+ * a border, or pass a specific value adjust a border. Units are in
+ * pixels. (DPI dependent)
+ *
+ * Platform notes:
+ * Windows: shrinking top non-client height will remove application
+ * icon and window title text. Glass desktops will refuse to set
+ * dimensions between zero and size < system default.
+ */
+ virtual nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) = 0;
+
+ /**
+ * Sets the region around the edges of the window that can be dragged to
+ * resize the window. All four sides of the window will get the same margin.
+ */
+ virtual void SetResizeMargin(mozilla::LayoutDeviceIntCoord aResizeMargin) = 0;
+ /**
+ * Get the client offset from the window origin.
+ *
+ * @return the x and y of the offset.
+ */
+ virtual LayoutDeviceIntPoint GetClientOffset() = 0;
+
+ /**
+ * Returns the slop from the screen edges in device pixels.
+ * @see Window.screenEdgeSlop{X,Y}
+ */
+ virtual LayoutDeviceIntPoint GetScreenEdgeSlop() { return {}; }
+
+ /**
+ * Equivalent to GetClientBounds but only returns the size.
+ */
+ virtual LayoutDeviceIntSize GetClientSize() {
+ // Depending on the backend, overloading this method may be useful if
+ // requesting the client offset is expensive.
+ return GetClientBounds().Size();
+ }
+
+ /**
+ * Set the background color for this widget
+ *
+ * @param aColor the new background color
+ *
+ */
+
+ virtual void SetBackgroundColor(const nscolor& aColor) {}
+
+ /**
+ * If a cursor type is currently cached locally for this widget, clear the
+ * cached cursor to force an update on the next SetCursor call.
+ */
+
+ virtual void ClearCachedCursor() = 0;
+
+ struct Cursor {
+ // The system cursor chosen by the page. This is used if there's no custom
+ // cursor, or if we fail to use the custom cursor in some way (if the image
+ // fails to load, for example).
+ nsCursor mDefaultCursor = eCursor_standard;
+ // May be null, to represent no custom cursor image.
+ nsCOMPtr<imgIContainer> mContainer;
+ uint32_t mHotspotX = 0;
+ uint32_t mHotspotY = 0;
+ mozilla::ImageResolution mResolution;
+
+ bool IsCustom() const { return !!mContainer; }
+
+ bool operator==(const Cursor& aOther) const {
+ return mDefaultCursor == aOther.mDefaultCursor &&
+ mContainer.get() == aOther.mContainer.get() &&
+ mHotspotX == aOther.mHotspotX && mHotspotY == aOther.mHotspotY &&
+ mResolution == aOther.mResolution;
+ }
+
+ bool operator!=(const Cursor& aOther) const { return !(*this == aOther); }
+ };
+
+ /**
+ * Sets the cursor for this widget.
+ */
+ virtual void SetCursor(const Cursor&) = 0;
+
+ virtual void SetCustomCursorAllowed(bool) = 0;
+
+ static nsIntSize CustomCursorSize(const Cursor&);
+
+ /**
+ * Get the window type of this widget.
+ */
+ WindowType GetWindowType() const { return mWindowType; }
+
+ /**
+ * Set the transparency mode of the top-level window containing this widget.
+ * So, e.g., if you call this on the widget for an IFRAME, the top level
+ * browser window containing the IFRAME actually gets set. Be careful.
+ *
+ * This can fail if the platform doesn't support
+ * transparency/glass. By default widgets are not
+ * transparent. This will also fail if the toplevel window is not
+ * a Mozilla window, e.g., if the widget is in an embedded
+ * context.
+ *
+ * After transparency/glass has been enabled, the initial alpha channel
+ * value for all pixels is 1, i.e., opaque.
+ * If the window is resized then the alpha channel values for
+ * all pixels are reset to 1.
+ * Pixel RGB color values are already premultiplied with alpha channel values.
+ */
+ virtual void SetTransparencyMode(TransparencyMode aMode) = 0;
+
+ /**
+ * Get the transparency mode of the top-level window that contains this
+ * widget.
+ */
+ virtual TransparencyMode GetTransparencyMode() = 0;
+
+ /**
+ * Set the shadow style of the window.
+ *
+ * Ignored on child widgets and on non-Mac platforms.
+ */
+ virtual void SetWindowShadowStyle(mozilla::WindowShadow aStyle) = 0;
+
+ /**
+ * Set the opacity of the window.
+ * Values need to be between 0.0f (invisible) and 1.0f (fully opaque).
+ *
+ * Ignored on child widgets and on non-Mac platforms.
+ */
+ virtual void SetWindowOpacity(float aOpacity) {}
+
+ /**
+ * Set the transform of the window. Values are in device pixels,
+ * the origin is the top left corner of the window.
+ *
+ * Ignored on child widgets and on non-Mac platforms.
+ */
+ virtual void SetWindowTransform(const mozilla::gfx::Matrix& aTransform) {}
+
+ /**
+ * Set the preferred color-scheme for the widget.
+ * Ignored on non-Mac platforms.
+ */
+ virtual void SetColorScheme(const mozilla::Maybe<mozilla::ColorScheme>&) {}
+
+ /**
+ * Set whether the window should ignore mouse events or not, and if it should
+ * not, what input margin should it use.
+ *
+ * This is only used on popup windows. The margin is only implemented on
+ * Linux.
+ */
+ struct InputRegion {
+ bool mFullyTransparent = false;
+ mozilla::LayoutDeviceIntCoord mMargin = 0;
+ };
+ virtual void SetInputRegion(const InputRegion&) {}
+
+ /*
+ * On macOS, this method shows or hides the pill button in the titlebar
+ * that's used to collapse the toolbar.
+ *
+ * Ignored on child widgets and on non-Mac platforms.
+ */
+ virtual void SetShowsToolbarButton(bool aShow) = 0;
+
+ /*
+ * On macOS, this method determines whether we tell cocoa that the window
+ * supports native full screen. If we do so, and another window is in
+ * native full screen, this window will also appear in native full screen.
+ *
+ * We generally only want to do this for primary application windows.
+ *
+ * Ignored on child widgets and on non-Mac platforms.
+ */
+ virtual void SetSupportsNativeFullscreen(bool aSupportsNativeFullscreen) = 0;
+
+ enum WindowAnimationType {
+ eGenericWindowAnimation,
+ eDocumentWindowAnimation
+ };
+
+ /**
+ * Sets the kind of top-level window animation this widget should have. On
+ * macOS, this causes a particular kind of animation to be shown when the
+ * window is first made visible.
+ *
+ * Ignored on child widgets and on non-Mac platforms.
+ */
+ virtual void SetWindowAnimationType(WindowAnimationType aType) = 0;
+
+ /**
+ * Specifies whether the window title should be drawn even if the window
+ * contents extend into the titlebar. Ignored on windows that don't draw
+ * in the titlebar. Only implemented on macOS.
+ */
+ virtual void SetDrawsTitle(bool aDrawTitle) {}
+
+ /**
+ * Hide window chrome (borders, buttons) for this widget.
+ *
+ */
+ virtual void HideWindowChrome(bool aShouldHide) = 0;
+
+ enum FullscreenTransitionStage {
+ eBeforeFullscreenToggle,
+ eAfterFullscreenToggle
+ };
+
+ /**
+ * Prepares for fullscreen transition and returns whether the widget
+ * supports fullscreen transition. If this method returns false,
+ * PerformFullscreenTransition() must never be called. Otherwise,
+ * caller should call that method twice with "before" and "after"
+ * stages respectively in order. In the latter case, this method may
+ * return some data via aData pointer. Caller must pass that data to
+ * PerformFullscreenTransition() if any, and caller is responsible
+ * for releasing that data.
+ */
+ virtual bool PrepareForFullscreenTransition(nsISupports** aData) = 0;
+
+ /**
+ * Performs fullscreen transition. This method returns immediately,
+ * and will post aCallback to the main thread when the transition
+ * finishes.
+ */
+ virtual void PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration,
+ nsISupports* aData,
+ nsIRunnable* aCallback) = 0;
+
+ /**
+ * Perform any actions needed after the fullscreen transition has ended.
+ */
+ virtual void CleanupFullscreenTransition() = 0;
+
+ /**
+ * Return the screen the widget is in, or null if we don't know.
+ */
+ virtual already_AddRefed<Screen> GetWidgetScreen() = 0;
+
+ /**
+ * Put the toplevel window into or out of fullscreen mode.
+ *
+ * @return NS_OK if the widget is setup properly for fullscreen and
+ * FullscreenChanged callback has been or will be called. If other
+ * value is returned, the caller should continue the change itself.
+ */
+ virtual nsresult MakeFullScreen(bool aFullScreen) = 0;
+
+ /**
+ * Same as MakeFullScreen, except that, on systems which natively
+ * support fullscreen transition, calling this method explicitly
+ * requests that behavior.
+ * It is currently only supported on macOS 10.7+.
+ */
+ virtual nsresult MakeFullScreenWithNativeTransition(bool aFullScreen) {
+ return MakeFullScreen(aFullScreen);
+ }
+
+ /**
+ * Invalidate a specified rect for a widget so that it will be repainted
+ * later.
+ */
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) = 0;
+
+ enum LayerManagerPersistence {
+ LAYER_MANAGER_CURRENT = 0,
+ LAYER_MANAGER_PERSISTENT
+ };
+
+ /**
+ * Return the widget's LayerManager. The layer tree for that LayerManager is
+ * what gets rendered to the widget.
+ *
+ * Note that this tries to create a renderer if it doesn't exist.
+ */
+ virtual WindowRenderer* GetWindowRenderer() = 0;
+
+ /**
+ * Returns whether there's an existing window renderer.
+ */
+ virtual bool HasWindowRenderer() const = 0;
+
+ /**
+ * Called before each layer manager transaction to allow any preparation
+ * for DrawWindowUnderlay/Overlay that needs to be on the main thread.
+ *
+ * Always called on the main thread.
+ */
+ virtual void PrepareWindowEffects() = 0;
+
+ /**
+ * Called when Gecko knows which themed widgets exist in this window.
+ * The passed array contains an entry for every themed widget of the right
+ * type (currently only StyleAppearance::Toolbar) within the window, except
+ * for themed widgets which are transformed or have effects applied to them
+ * (e.g. CSS opacity or filters).
+ * This could sometimes be called during display list construction
+ * outside of painting.
+ * If called during painting, it will be called before we actually
+ * paint anything.
+ */
+ virtual void UpdateThemeGeometries(
+ const nsTArray<ThemeGeometry>& aThemeGeometries) = 0;
+
+ /**
+ * Informs the widget about the region of the window that is opaque.
+ *
+ * @param aOpaqueRegion the region of the window that is opaque.
+ */
+ virtual void UpdateOpaqueRegion(const LayoutDeviceIntRegion& aOpaqueRegion) {}
+
+ /**
+ * Informs the widget about the region of the window that is draggable.
+ */
+ virtual void UpdateWindowDraggingRegion(
+ const LayoutDeviceIntRegion& aRegion) {}
+
+ /**
+ * Tells the widget whether the given input block results in a swipe.
+ * Should be called in response to a WidgetWheelEvent that has
+ * mFlags.mCanTriggerSwipe set on it.
+ */
+ virtual void ReportSwipeStarted(uint64_t aInputBlockId, bool aStartSwipe) {}
+
+ /**
+ * Internal methods
+ */
+
+ //@{
+ virtual void AddChild(nsIWidget* aChild) = 0;
+ virtual void RemoveChild(nsIWidget* aChild) = 0;
+ virtual void* GetNativeData(uint32_t aDataType) = 0;
+ virtual void FreeNativeData(void* data, uint32_t aDataType) = 0; //~~~
+
+ //@}
+
+ /**
+ * Set the widget's title.
+ * Must be called after Create.
+ *
+ * @param aTitle string displayed as the title of the widget
+ */
+ virtual nsresult SetTitle(const nsAString& aTitle) = 0;
+
+ /**
+ * Set the widget's icon.
+ * Must be called after Create.
+ *
+ * @param aIconSpec string specifying the icon to use; convention is to
+ * pass a resource: URL from which a platform-dependent
+ * resource file name will be constructed
+ */
+ virtual void SetIcon(const nsAString& aIconSpec) = 0;
+
+ /**
+ * Return this widget's origin in screen coordinates.
+ *
+ * @return screen coordinates stored in the x,y members
+ */
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() = 0;
+
+ /**
+ * The same as WidgetToScreenOffset(), except in the case of
+ * PuppetWidget where this method omits the chrome offset.
+ */
+ virtual LayoutDeviceIntPoint TopLevelWidgetToScreenOffset() {
+ return WidgetToScreenOffset();
+ }
+
+ /**
+ * For a PuppetWidget, returns the transform from the coordinate
+ * space of the PuppetWidget to the coordinate space of the
+ * top-level native widget.
+ *
+ * Identity transform in other cases.
+ */
+ virtual mozilla::LayoutDeviceToLayoutDeviceMatrix4x4
+ WidgetToTopLevelWidgetTransform() {
+ return mozilla::LayoutDeviceToLayoutDeviceMatrix4x4();
+ }
+
+ mozilla::LayoutDeviceIntPoint WidgetToTopLevelWidgetOffset() {
+ return mozilla::LayoutDeviceIntPoint::Round(
+ WidgetToTopLevelWidgetTransform().TransformPoint(
+ mozilla::LayoutDevicePoint()));
+ }
+
+ /**
+ * Returns the margins that are applied to go from client sizes to window
+ * sizes (which includes window borders and titlebar).
+ * This method should work even when the window is not yet visible.
+ */
+ virtual LayoutDeviceIntMargin ClientToWindowMargin() { return {}; }
+
+ LayoutDeviceIntSize ClientToWindowSizeDifference();
+
+ /**
+ * Dispatches an event to the widget
+ */
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* event,
+ nsEventStatus& aStatus) = 0;
+
+ /**
+ * Dispatches an event to APZ only.
+ * No-op in the child process.
+ */
+ virtual void DispatchEventToAPZOnly(mozilla::WidgetInputEvent* aEvent) = 0;
+
+ /*
+ * Dispatch a gecko event for this widget.
+ * Returns true if it's consumed. Otherwise, false.
+ */
+ virtual bool DispatchWindowEvent(mozilla::WidgetGUIEvent& event) = 0;
+
+ // A structure that groups the statuses from APZ dispatch and content
+ // dispatch.
+ struct ContentAndAPZEventStatus {
+ // Either of these may not be set if the event was not dispatched
+ // to APZ or to content.
+ nsEventStatus mApzStatus = nsEventStatus_eIgnore;
+ nsEventStatus mContentStatus = nsEventStatus_eIgnore;
+ };
+
+ /**
+ * Dispatches an event that must be handled by APZ first, when APZ is
+ * enabled. If invoked in the child process, it is forwarded to the
+ * parent process synchronously.
+ */
+ virtual ContentAndAPZEventStatus DispatchInputEvent(
+ mozilla::WidgetInputEvent* aEvent) = 0;
+
+ /**
+ * Confirm an APZ-aware event target. This should be used when APZ will
+ * not need a layers update to process the event.
+ */
+ virtual void SetConfirmedTargetAPZC(
+ uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) const = 0;
+
+ /**
+ * Returns true if APZ is in use, false otherwise.
+ */
+ virtual bool AsyncPanZoomEnabled() const = 0;
+
+ /**
+ */
+ virtual void SwipeFinished() = 0;
+
+ /**
+ * Enables the dropping of files to a widget.
+ */
+ virtual void EnableDragDrop(bool aEnable) = 0;
+ virtual nsresult AsyncEnableDragDrop(bool aEnable) = 0;
+
+ /**
+ * Classify the window for the window manager. Mostly for X11.
+ *
+ * @param xulWinType The window type. Characters other than [A-Za-z0-9_-] are
+ * converted to '_'. Anything before the first colon is
+ * assigned to name, anything after it to role. If there's
+ * no colon, assign the whole thing to both role and name.
+ *
+ * @param xulWinClass The window class. If set, overrides the normal value.
+ * Otherwise, the program class it used.
+ *
+ * @param xulWinName The window name. If set, overrides the value specified in
+ * window type. Otherwise, name from window type is used.
+ *
+ */
+ virtual void SetWindowClass(const nsAString& xulWinType,
+ const nsAString& xulWinClass,
+ const nsAString& xulWinName) = 0;
+
+ /**
+ * Enables/Disables system capture of any and all events that would cause a
+ * popup to be rolled up. aListener should be set to a non-null value for
+ * any popups that are not managed by the popup manager.
+ * @param aDoCapture true enables capture, false disables capture
+ *
+ */
+ virtual void CaptureRollupEvents(bool aDoCapture) = 0;
+
+ /**
+ * Bring this window to the user's attention. This is intended to be a more
+ * gentle notification than popping the window to the top or putting up an
+ * alert. See, for example, Win32 FlashWindow or the NotificationManager on
+ * the Mac. The notification should be suppressed if the window is already
+ * in the foreground and should be dismissed when the user brings this window
+ * to the foreground.
+ * @param aCycleCount Maximum number of times to animate the window per system
+ * conventions. If set to -1, cycles indefinitely until
+ * window is brought into the foreground.
+ */
+ [[nodiscard]] virtual nsresult GetAttention(int32_t aCycleCount) = 0;
+
+ /**
+ * Ask whether there user input events pending. All input events are
+ * included, including those not targeted at this nsIwidget instance.
+ */
+ virtual bool HasPendingInputEvent() = 0;
+
+ /*
+ * Determine whether the widget shows a resize widget. If it does,
+ * aResizerRect returns the resizer's rect.
+ *
+ * Returns false on any platform that does not support it.
+ *
+ * @param aResizerRect The resizer's rect in device pixels.
+ * @return Whether a resize widget is shown.
+ */
+ virtual bool ShowsResizeIndicator(LayoutDeviceIntRect* aResizerRect) = 0;
+
+ // TODO: Make this an enum class with MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS or
+ // EnumSet class.
+ enum Modifiers : uint32_t {
+ NO_MODIFIERS = 0x00000000,
+ CAPS_LOCK = 0x00000001, // when CapsLock is active
+ NUM_LOCK = 0x00000002, // when NumLock is active
+ SHIFT_L = 0x00000100,
+ SHIFT_R = 0x00000200,
+ CTRL_L = 0x00000400,
+ CTRL_R = 0x00000800,
+ ALT_L = 0x00001000, // includes Option
+ ALT_R = 0x00002000,
+ COMMAND_L = 0x00004000,
+ COMMAND_R = 0x00008000,
+ HELP = 0x00010000,
+ ALTGRAPH = 0x00020000, // AltGr key on Windows. This emulates
+ // AltRight key behavior of keyboard
+ // layouts which maps AltGr to AltRight
+ // key.
+ FUNCTION = 0x00100000,
+ NUMERIC_KEY_PAD = 0x01000000 // when the key is coming from the keypad
+ };
+ /**
+ * Utility method intended for testing. Dispatches native key events
+ * to this widget to simulate the press and release of a key.
+ * @param aNativeKeyboardLayout a *platform-specific* constant.
+ * On Mac, this is the resource ID for a 'uchr' or 'kchr' resource.
+ * On Windows, it is converted to a hex string and passed to
+ * LoadKeyboardLayout, see
+ * http://msdn.microsoft.com/en-us/library/ms646305(VS.85).aspx
+ * @param aNativeKeyCode a *platform-specific* keycode.
+ * On Windows, this is the virtual key code.
+ * @param aModifiers some combination of the above 'Modifiers' flags;
+ * not all flags will apply to all platforms. Mac ignores the _R
+ * modifiers. Windows ignores COMMAND, NUMERIC_KEY_PAD, HELP and
+ * FUNCTION.
+ * @param aCharacters characters that the OS would decide to generate
+ * from the event. On Windows, this is the charCode passed by
+ * WM_CHAR.
+ * @param aUnmodifiedCharacters characters that the OS would decide
+ * to generate from the event if modifier keys (other than shift)
+ * were assumed inactive. Needed on Mac, ignored on Windows.
+ * @param aObserver the observer that will get notified once the events
+ * have been dispatched.
+ * @return NS_ERROR_NOT_AVAILABLE to indicate that the keyboard
+ * layout is not supported and the event was not fired
+ */
+ virtual nsresult SynthesizeNativeKeyEvent(
+ int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters, nsIObserver* aObserver) = 0;
+
+ /**
+ * Utility method intended for testing. Dispatches native mouse events
+ * may even move the mouse cursor. On Mac the events are guaranteed to
+ * be sent to the window containing this widget, but on Windows they'll go
+ * to whatever's topmost on the screen at that position, so for
+ * cross-platform testing ensure that your window is at the top of the
+ * z-order.
+ * @param aPoint screen location of the mouse, in device
+ * pixels, with origin at the top left
+ * @param aNativeMessage abstract native message.
+ * @param aButton Mouse button defined by DOM UI Events.
+ * @param aModifierFlags Some values of nsIWidget::Modifiers.
+ * FYI: On Windows, Android and Headless widget on all
+ * platroms, this hasn't been handled yet.
+ * @param aObserver the observer that will get notified once the events
+ * have been dispatched.
+ */
+ enum class NativeMouseMessage : uint32_t {
+ ButtonDown, // button down
+ ButtonUp, // button up
+ Move, // mouse cursor move
+ EnterWindow, // mouse cursor comes into a window
+ LeaveWindow, // mouse cursor leaves from a window
+ };
+ virtual nsresult SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) = 0;
+
+ /**
+ * A shortcut to SynthesizeNativeMouseEvent, abstracting away the native
+ * message. aPoint is location in device pixels to which the mouse pointer
+ * moves to.
+ * @param aObserver the observer that will get notified once the events
+ * have been dispatched.
+ */
+ virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) = 0;
+
+ /**
+ * Utility method intended for testing. Dispatching native mouse scroll
+ * events may move the mouse cursor.
+ *
+ * @param aPoint Mouse cursor position in screen coordinates.
+ * In device pixels, the origin at the top left of
+ * the primary display.
+ * @param aNativeMessage Platform native message.
+ * @param aDeltaX The delta value for X direction. If the native
+ * message doesn't indicate X direction scrolling,
+ * this may be ignored.
+ * @param aDeltaY The delta value for Y direction. If the native
+ * message doesn't indicate Y direction scrolling,
+ * this may be ignored.
+ * @param aDeltaZ The delta value for Z direction. If the native
+ * message doesn't indicate Z direction scrolling,
+ * this may be ignored.
+ * @param aModifierFlags Must be values of Modifiers, or zero.
+ * @param aAdditionalFlags See nsIDOMWidnowUtils' consts and their
+ * document.
+ * @param aObserver The observer that will get notified once the
+ * events have been dispatched.
+ */
+ virtual nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) = 0;
+
+ /*
+ * TouchPointerState states for SynthesizeNativeTouchPoint. Match
+ * touch states in nsIDOMWindowUtils.idl.
+ */
+ enum TouchPointerState {
+ // The pointer is in a hover state above the digitizer
+ TOUCH_HOVER = (1 << 0),
+ // The pointer is in contact with the digitizer
+ TOUCH_CONTACT = (1 << 1),
+ // The pointer has been removed from the digitizer detection area
+ TOUCH_REMOVE = (1 << 2),
+ // The pointer has been canceled. Will cancel any pending os level
+ // gestures that would triggered as a result of completion of the
+ // input sequence. This may not cancel moz platform related events
+ // that might get tirggered by input already delivered.
+ TOUCH_CANCEL = (1 << 3),
+
+ // ALL_BITS used for validity checking during IPC serialization
+ ALL_BITS = (1 << 4) - 1
+ };
+ /*
+ * TouchpadGesturePhase states for SynthesizeNativeTouchPadPinch and
+ * SynthesizeNativeTouchpadPan. Match phase states in nsIDOMWindowUtils.idl.
+ */
+ enum TouchpadGesturePhase {
+ PHASE_BEGIN = 0,
+ PHASE_UPDATE = 1,
+ PHASE_END = 2
+ };
+ /*
+ * Create a new or update an existing touch pointer on the digitizer.
+ * To trigger os level gestures, individual touch points should
+ * transition through a complete set of touch states which should be
+ * sent as individual messages.
+ *
+ * @param aPointerId The touch point id to create or update.
+ * @param aPointerState one or more of the touch states listed above
+ * @param aPoint coords of this event
+ * @param aPressure 0.0 -> 1.0 float val indicating pressure
+ * @param aOrientation 0 -> 359 degree value indicating the
+ * orientation of the pointer. Use 90 for normal taps.
+ * @param aObserver The observer that will get notified once the events
+ * have been dispatched.
+ */
+ virtual nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) = 0;
+ /*
+ * See nsIDOMWindowUtils.sendNativeTouchpadPinch().
+ */
+ virtual nsresult SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale,
+ LayoutDeviceIntPoint aPoint, int32_t aModifierFlags) = 0;
+
+ /*
+ * Helper for simulating a simple tap event with one touch point. When
+ * aLongTap is true, simulates a native long tap with a duration equal to
+ * ui.click_hold_context_menus.delay. This pref is compatible with the
+ * apzc long tap duration. Defaults to 1.5 seconds.
+ * @param aObserver The observer that will get notified once the events
+ * have been dispatched.
+ */
+ virtual nsresult SynthesizeNativeTouchTap(LayoutDeviceIntPoint aPoint,
+ bool aLongTap,
+ nsIObserver* aObserver);
+
+ virtual nsresult SynthesizeNativePenInput(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPressure,
+ uint32_t aRotation, int32_t aTiltX,
+ int32_t aTiltY, int32_t aButton,
+ nsIObserver* aObserver) = 0;
+
+ /*
+ * Cancels all active simulated touch input points and pending long taps.
+ * Native widgets should track existing points such that they can clear the
+ * digitizer state when this call is made.
+ * @param aObserver The observer that will get notified once the touch
+ * sequence has been cleared.
+ */
+ virtual nsresult ClearNativeTouchSequence(nsIObserver* aObserver);
+
+ /*
+ * Send a native event as if the user double tapped the touchpad with two
+ * fingers.
+ */
+ virtual nsresult SynthesizeNativeTouchpadDoubleTap(
+ LayoutDeviceIntPoint aPoint, uint32_t aModifierFlags) = 0;
+
+ /*
+ * See nsIDOMWindowUtils.sendNativeTouchpadPan().
+ */
+ virtual nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) = 0;
+
+ virtual void StartAsyncScrollbarDrag(
+ const AsyncDragMetrics& aDragMetrics) = 0;
+
+ /**
+ * Notify APZ to start autoscrolling.
+ * @param aAnchorLocation the location of the autoscroll anchor
+ * @param aGuid identifies the scroll frame to be autoscrolled
+ * @return true if APZ has been successfully notified
+ */
+ virtual bool StartAsyncAutoscroll(const ScreenPoint& aAnchorLocation,
+ const ScrollableLayerGuid& aGuid) = 0;
+
+ /**
+ * Notify APZ to stop autoscrolling.
+ * @param aGuid identifies the scroll frame which is being autoscrolled.
+ */
+ virtual void StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) = 0;
+
+ virtual LayersId GetRootLayerTreeId() = 0;
+
+ // If this widget supports out-of-process compositing, it can override
+ // this method to provide additional information to the compositor.
+ virtual void GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) {}
+
+ /**
+ * Setter/Getter of the system font setting for testing.
+ */
+ virtual nsresult SetSystemFont(const nsCString& aFontName) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ virtual nsresult GetSystemFont(nsCString& aFontName) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ /**
+ * Wayland specific routines.
+ */
+ virtual LayoutDeviceIntSize GetMoveToRectPopupSize() const {
+ NS_WARNING("GetLayoutPopupRect implemented only for wayland");
+ return LayoutDeviceIntSize();
+ }
+
+ /**
+ * If this widget uses native pointer lock instead of warp-to-center
+ * (currently only GTK on Wayland), these methods provide access to that
+ * functionality.
+ */
+ virtual void SetNativePointerLockCenter(
+ const LayoutDeviceIntPoint& aLockCenter) {}
+ virtual void LockNativePointer() {}
+ virtual void UnlockNativePointer() {}
+
+ /*
+ * Get safe area insets except to cutout.
+ * See https://drafts.csswg.org/css-env-1/#safe-area-insets.
+ */
+ virtual mozilla::ScreenIntMargin GetSafeAreaInsets() const {
+ return mozilla::ScreenIntMargin();
+ }
+
+ private:
+ class LongTapInfo {
+ public:
+ LongTapInfo(int32_t aPointerId, LayoutDeviceIntPoint& aPoint,
+ mozilla::TimeDuration aDuration, nsIObserver* aObserver)
+ : mPointerId(aPointerId),
+ mPosition(aPoint),
+ mDuration(aDuration),
+ mObserver(aObserver),
+ mStamp(mozilla::TimeStamp::Now()) {}
+
+ int32_t mPointerId;
+ LayoutDeviceIntPoint mPosition;
+ mozilla::TimeDuration mDuration;
+ nsCOMPtr<nsIObserver> mObserver;
+ mozilla::TimeStamp mStamp;
+ };
+
+ static void OnLongTapTimerCallback(nsITimer* aTimer, void* aClosure);
+
+ static already_AddRefed<nsIBidiKeyboard> CreateBidiKeyboardContentProcess();
+ static already_AddRefed<nsIBidiKeyboard> CreateBidiKeyboardInner();
+
+ mozilla::UniquePtr<LongTapInfo> mLongTapTouchPoint;
+ nsCOMPtr<nsITimer> mLongTapTimer;
+ static int32_t sPointerIdCounter;
+
+ public:
+ /**
+ * If key events have not been handled by content or XBL handlers, they can
+ * be offered to the system (for custom application shortcuts set in system
+ * preferences, for example).
+ */
+ virtual void PostHandleKeyEvent(mozilla::WidgetKeyboardEvent* aEvent);
+
+ /**
+ * Activates a native menu item at the position specified by the index
+ * string. The index string is a string of positive integers separated
+ * by the "|" (pipe) character. The last integer in the string represents
+ * the item index in a submenu located using the integers preceding it.
+ *
+ * Example: 1|0|4
+ * In this string, the first integer represents the top-level submenu
+ * in the native menu bar. Since the integer is 1, it is the second submeu
+ * in the native menu bar. Within that, the first item (index 0) is a
+ * submenu, and we want to activate the 5th item within that submenu.
+ */
+ virtual nsresult ActivateNativeMenuItemAt(const nsAString& indexString) = 0;
+
+ /**
+ * This is used for native menu system testing.
+ *
+ * Updates a native menu at the position specified by the index string.
+ * The index string is a string of positive integers separated by the "|"
+ * (pipe) character.
+ *
+ * Example: 1|0|4
+ * In this string, the first integer represents the top-level submenu
+ * in the native menu bar. Since the integer is 1, it is the second submeu
+ * in the native menu bar. Within that, the first item (index 0) is a
+ * submenu, and we want to update submenu at index 4 within that submenu.
+ *
+ * If this is called with an empty string it forces a full reload of the
+ * menu system.
+ */
+ virtual nsresult ForceUpdateNativeMenuAt(const nsAString& indexString) = 0;
+
+ /**
+ * This is used for testing macOS service menu code.
+ *
+ * @param aResult - the current text selection. Is empty if no selection.
+ * @return nsresult - whether or not aResult was assigned the selected text.
+ */
+ [[nodiscard]] virtual nsresult GetSelectionAsPlaintext(nsAString& aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ /**
+ * Notify IME of the specified notification.
+ *
+ * @return If the notification is mouse button event and it's consumed by
+ * IME, this returns NS_SUCCESS_EVENT_CONSUMED.
+ */
+ virtual nsresult NotifyIME(const IMENotification& aIMENotification) = 0;
+
+ /**
+ * MaybeDispatchInitialFocusEvent will dispatch a focus event after creation
+ * of the widget, in the event that we were not able to observe and respond to
+ * the initial focus event. This is necessary for the early skeleton UI
+ * window, which is displayed and receives its initial focus event before we
+ * can actually respond to it.
+ */
+ virtual void MaybeDispatchInitialFocusEvent() {}
+
+ /*
+ * Notifies the input context changes.
+ */
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) = 0;
+
+ /*
+ * Get current input context.
+ */
+ virtual InputContext GetInputContext() = 0;
+
+ /**
+ * Get native IME context. This is different from GetNativeData() with
+ * NS_RAW_NATIVE_IME_CONTEXT, the result is unique even if in a remote
+ * process.
+ */
+ virtual NativeIMEContext GetNativeIMEContext() = 0;
+
+ /*
+ * Given a WidgetKeyboardEvent, this method synthesizes a corresponding
+ * native (OS-level) event for it. This method allows tests to simulate
+ * keystrokes that trigger native key bindings (which require a native
+ * event).
+ */
+ [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
+ mozilla::WidgetKeyboardEvent& aEvent) = 0;
+
+ /**
+ * Retrieve edit commands when the key combination of aEvent is used
+ * in platform native applications.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ mozilla::NativeKeyBindingsType aType,
+ const mozilla::WidgetKeyboardEvent& aEvent,
+ nsTArray<mozilla::CommandInt>& aCommands);
+
+ /*
+ * Retrieves a reference to notification requests of IME. Note that the
+ * reference is valid while the nsIWidget instance is alive. So, if you
+ * need to store the reference for a long time, you need to grab the widget
+ * instance too.
+ */
+ const IMENotificationRequests& IMENotificationRequestsRef();
+
+ /*
+ * Call this method when a dialog is opened which has a default button.
+ * The button's rectangle should be supplied in aButtonRect.
+ */
+ [[nodiscard]] virtual nsresult OnDefaultButtonLoaded(
+ const LayoutDeviceIntRect& aButtonRect) = 0;
+
+ /**
+ * Return true if this process shouldn't use platform widgets, and
+ * so should use PuppetWidgets instead. If this returns true, the
+ * result of creating and using a platform widget is undefined,
+ * and likely to end in crashes or other buggy behavior.
+ */
+ static bool UsePuppetWidgets() { return XRE_IsContentProcess(); }
+
+ static already_AddRefed<nsIWidget> CreateTopLevelWindow();
+
+ static already_AddRefed<nsIWidget> CreateChildWindow();
+
+ /**
+ * Allocate and return a "puppet widget" that doesn't directly
+ * correlate to a platform widget; platform events and data must
+ * be fed to it. Currently used in content processes. NULL is
+ * returned if puppet widgets aren't supported in this build
+ * config, on this platform, or for this process type.
+ *
+ * This function is called "Create" to match CreateInstance().
+ * The returned widget must still be nsIWidget::Create()d.
+ */
+ static already_AddRefed<nsIWidget> CreatePuppetWidget(
+ BrowserChild* aBrowserChild);
+
+ static already_AddRefed<nsIWidget> CreateHeadlessWidget();
+
+ /**
+ * Reparent this widget's native widget.
+ * @param aNewParent the native widget of aNewParent is the new native
+ * parent widget
+ */
+ virtual void ReparentNativeWidget(nsIWidget* aNewParent) = 0;
+
+ /**
+ * Return true if widget has it's own GL context
+ */
+ virtual bool HasGLContext() { return false; }
+
+ /**
+ * Returns true to indicate that this widget paints an opaque background
+ * that we want to be visible under the page, so layout should not force
+ * a default background.
+ */
+ virtual bool WidgetPaintsBackground() { return false; }
+
+ virtual bool NeedsPaint() { return IsVisible() && !GetBounds().IsEmpty(); }
+
+ /**
+ * Get the natural bounds of this widget. This method is only
+ * meaningful for widgets for which Gecko implements screen
+ * rotation natively. When this is the case, GetBounds() returns
+ * the widget bounds taking rotation into account, and
+ * GetNaturalBounds() returns the bounds *not* taking rotation
+ * into account.
+ *
+ * No code outside of the composition pipeline should know or care
+ * about this. If you're not an agent of the compositor, you
+ * probably shouldn't call this method.
+ */
+ virtual LayoutDeviceIntRect GetNaturalBounds() { return GetBounds(); }
+
+ /**
+ * Set size constraints on the window size such that it is never less than
+ * the specified minimum size and never larger than the specified maximum
+ * size. The size constraints are sizes of the outer rectangle including
+ * the window frame and title bar. Use 0 for an unconstrained minimum size
+ * and NS_MAXSIZE for an unconstrained maximum size. Note that this method
+ * does not necessarily change the size of a window to conform to this size,
+ * thus Resize should be called afterwards.
+ *
+ * @param aConstraints: the size constraints in device pixels
+ */
+ virtual void SetSizeConstraints(const SizeConstraints& aConstraints) = 0;
+
+ /**
+ * Return the size constraints currently observed by the widget.
+ *
+ * @return the constraints in device pixels
+ */
+ virtual const SizeConstraints GetSizeConstraints() = 0;
+
+ /**
+ * Apply the current size constraints to the given size.
+ *
+ * @param aWidth width to constrain
+ * @param aHeight height to constrain
+ */
+ virtual void ConstrainSize(int32_t* aWidth, int32_t* aHeight) = 0;
+
+ /**
+ * If this is owned by a BrowserChild, return that. Otherwise return
+ * null.
+ */
+ virtual BrowserChild* GetOwningBrowserChild() { return nullptr; }
+
+ /**
+ * If this isn't directly compositing to its window surface,
+ * return the compositor which is doing that on our behalf.
+ */
+ virtual CompositorBridgeChild* GetRemoteRenderer() { return nullptr; }
+
+ /**
+ * If there is a remote renderer, pause or resume it.
+ */
+ virtual void PauseOrResumeCompositor(bool aPause);
+
+ /**
+ * Clear WebRender resources
+ */
+ virtual void ClearCachedWebrenderResources() {}
+
+ /**
+ * Clear WebRender animation resources
+ */
+ virtual void ClearWebrenderAnimationResources() {}
+
+ /**
+ * Request fast snapshot at RenderCompositor of WebRender.
+ * Since readback of Windows DirectComposition is very slow.
+ */
+ virtual bool SetNeedFastSnaphot() { return false; }
+
+ /**
+ * If this widget has its own vsync dispatcher, return it, otherwise return
+ * nullptr. An example of such a local vsync dispatcher would be Wayland frame
+ * callbacks.
+ */
+ virtual RefPtr<mozilla::VsyncDispatcher> GetVsyncDispatcher();
+
+ /**
+ * Returns true if the widget requires synchronous repaints on resize,
+ * false otherwise.
+ */
+ virtual bool SynchronouslyRepaintOnResize() { return true; }
+
+ /**
+ * Some platforms (only cocoa right now) round widget coordinates to the
+ * nearest even pixels (see bug 892994), this function allows us to
+ * determine how widget coordinates will be rounded.
+ */
+ virtual int32_t RoundsWidgetCoordinatesTo() { return 1; }
+
+ virtual void UpdateZoomConstraints(
+ const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
+ const mozilla::Maybe<ZoomConstraints>& aConstraints){};
+
+ /**
+ * GetTextEventDispatcher() returns TextEventDispatcher belonging to the
+ * widget. Note that this never returns nullptr.
+ */
+ virtual TextEventDispatcher* GetTextEventDispatcher() = 0;
+
+ /**
+ * GetNativeTextEventDispatcherListener() returns a
+ * TextEventDispatcherListener instance which is used when the widget
+ * instance handles native IME and/or keyboard events.
+ */
+ virtual TextEventDispatcherListener*
+ GetNativeTextEventDispatcherListener() = 0;
+
+ /**
+ * Trigger an animation to zoom to the given |aRect|.
+ * |aRect| should be relative to the layout viewport of the widget's root
+ * document
+ */
+ virtual void ZoomToRect(const uint32_t& aPresShellId,
+ const ScrollableLayerGuid::ViewID& aViewId,
+ const CSSRect& aRect, const uint32_t& aFlags) = 0;
+
+ /**
+ * LookUpDictionary shows the dictionary for the word around current point.
+ *
+ * @param aText the word to look up dictiorary.
+ * @param aFontRangeArray text decoration of aText
+ * @param aIsVertical true if the word is vertical layout
+ * @param aPoint top-left point of aText
+ */
+ virtual void LookUpDictionary(
+ const nsAString& aText,
+ const nsTArray<mozilla::FontRange>& aFontRangeArray,
+ const bool aIsVertical, const LayoutDeviceIntPoint& aPoint) {}
+
+ virtual void RequestFxrOutput() {
+ MOZ_ASSERT(false, "This function should only execute in Windows");
+ }
+
+#if defined(MOZ_WIDGET_ANDROID)
+ /**
+ * RecvToolbarAnimatorMessageFromCompositor receive message from compositor
+ * thread.
+ *
+ * @param aMessage message being sent to Android UI thread.
+ */
+ virtual void RecvToolbarAnimatorMessageFromCompositor(int32_t aMessage) = 0;
+
+ /**
+ * UpdateRootFrameMetrics steady state frame metrics send from compositor
+ * thread
+ *
+ * @param aScrollOffset page scroll offset value in screen pixels.
+ * @param aZoom current page zoom.
+ */
+ virtual void UpdateRootFrameMetrics(const ScreenPoint& aScrollOffset,
+ const CSSToScreenScale& aZoom) = 0;
+
+ /**
+ * RecvScreenPixels Buffer containing the pixel from the frame buffer. Used
+ * for android robocop tests.
+ *
+ * @param aMem shared memory containing the frame buffer pixels.
+ * @param aSize size of the buffer in screen pixels.
+ */
+ virtual void RecvScreenPixels(mozilla::ipc::Shmem&& aMem,
+ const ScreenIntSize& aSize,
+ bool aNeedsYFlip) = 0;
+
+ virtual void UpdateDynamicToolbarMaxHeight(mozilla::ScreenIntCoord aHeight) {}
+ virtual mozilla::ScreenIntCoord GetDynamicToolbarMaxHeight() const {
+ return 0;
+ }
+#endif
+
+ static already_AddRefed<nsIBidiKeyboard> CreateBidiKeyboard();
+
+ /**
+ * Like GetDefaultScale, but taking into account only the system settings
+ * and ignoring Gecko preferences.
+ */
+ virtual double GetDefaultScaleInternal() { return 1.0; }
+
+ using WindowButtonType = mozilla::WindowButtonType;
+
+ /**
+ * Layout uses this to alert the widget to the client rect representing
+ * the window maximize button. An empty rect indicates there is no
+ * maximize button (for example, in fullscreen). This is only implemented
+ * on Windows.
+ */
+ virtual void SetWindowButtonRect(WindowButtonType aButtonType,
+ const LayoutDeviceIntRect& aClientRect) {}
+
+#ifdef DEBUG
+ virtual nsresult SetHiDPIMode(bool aHiDPI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ virtual nsresult RestoreHiDPIMode() { return NS_ERROR_NOT_IMPLEMENTED; }
+#endif
+
+ protected:
+ // keep the list of children. We also keep track of our siblings.
+ // The ownership model is as follows: parent holds a strong ref to
+ // the first element of the list, and each element holds a strong
+ // ref to the next element in the list. The prevsibling and
+ // lastchild pointers are weak, which is fine as long as they are
+ // maintained properly.
+ nsCOMPtr<nsIWidget> mFirstChild;
+ nsIWidget* MOZ_NON_OWNING_REF mLastChild;
+ nsCOMPtr<nsIWidget> mNextSibling;
+ nsIWidget* MOZ_NON_OWNING_REF mPrevSibling;
+ // When Destroy() is called, the sub class should set this true.
+ bool mOnDestroyCalled;
+ WindowType mWindowType;
+ int32_t mZIndex;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIWidget, NS_IWIDGET_IID)
+
+#endif // nsIWidget_h__
diff --git a/widget/nsIWidgetListener.cpp b/widget/nsIWidgetListener.cpp
new file mode 100644
index 0000000000..b9f236f159
--- /dev/null
+++ b/widget/nsIWidgetListener.cpp
@@ -0,0 +1,89 @@
+/* -*- 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 "nsIWidgetListener.h"
+
+#include "nsRegion.h"
+#include "nsView.h"
+#include "nsIWidget.h"
+#include "nsIAppWindow.h"
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/PresShell.h"
+
+using namespace mozilla;
+
+nsIAppWindow* nsIWidgetListener::GetAppWindow() { return nullptr; }
+
+nsView* nsIWidgetListener::GetView() { return nullptr; }
+
+PresShell* nsIWidgetListener::GetPresShell() { return nullptr; }
+
+bool nsIWidgetListener::WindowMoved(nsIWidget* aWidget, int32_t aX, int32_t aY,
+ ByMoveToRect) {
+ return false;
+}
+
+bool nsIWidgetListener::WindowResized(nsIWidget* aWidget, int32_t aWidth,
+ int32_t aHeight) {
+ return false;
+}
+
+void nsIWidgetListener::SizeModeChanged(nsSizeMode aSizeMode) {}
+
+void nsIWidgetListener::SafeAreaInsetsChanged(const mozilla::ScreenIntMargin&) {
+}
+
+void nsIWidgetListener::UIResolutionChanged() {}
+
+#if defined(MOZ_WIDGET_ANDROID)
+void nsIWidgetListener::DynamicToolbarMaxHeightChanged(ScreenIntCoord aHeight) {
+}
+void nsIWidgetListener::DynamicToolbarOffsetChanged(ScreenIntCoord aOffset) {}
+#endif
+
+void nsIWidgetListener::MacFullscreenMenubarOverlapChanged(
+ mozilla::DesktopCoord aOverlapAmount) {}
+
+bool nsIWidgetListener::ZLevelChanged(bool aImmediate, nsWindowZ* aPlacement,
+ nsIWidget* aRequestBelow,
+ nsIWidget** aActualBelow) {
+ return false;
+}
+
+void nsIWidgetListener::OcclusionStateChanged(bool aIsFullyOccluded) {}
+
+void nsIWidgetListener::WindowActivated() {}
+
+void nsIWidgetListener::WindowDeactivated() {}
+
+void nsIWidgetListener::OSToolbarButtonPressed() {}
+
+bool nsIWidgetListener::RequestWindowClose(nsIWidget* aWidget) { return false; }
+
+void nsIWidgetListener::WillPaintWindow(nsIWidget* aWidget) {}
+
+bool nsIWidgetListener::PaintWindow(nsIWidget* aWidget,
+ LayoutDeviceIntRegion aRegion) {
+ return false;
+}
+
+void nsIWidgetListener::DidPaintWindow() {}
+
+void nsIWidgetListener::DidCompositeWindow(
+ mozilla::layers::TransactionId aTransactionId,
+ const TimeStamp& aCompositeStart, const TimeStamp& aCompositeEnd) {}
+
+void nsIWidgetListener::RequestRepaint() {}
+
+bool nsIWidgetListener::ShouldNotBeVisible() {
+ // Returns false to assume that nothing should happen in most cases.
+ return false;
+}
+
+nsEventStatus nsIWidgetListener::HandleEvent(WidgetGUIEvent* aEvent,
+ bool aUseAttachedEvents) {
+ return nsEventStatus_eIgnore;
+}
diff --git a/widget/nsIWidgetListener.h b/widget/nsIWidgetListener.h
new file mode 100644
index 0000000000..aff753aaec
--- /dev/null
+++ b/widget/nsIWidgetListener.h
@@ -0,0 +1,194 @@
+/* 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/. */
+
+#ifndef nsIWidgetListener_h__
+#define nsIWidgetListener_h__
+
+#include <stdint.h>
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/TimeStamp.h"
+
+#include "nsRegionFwd.h"
+#include "Units.h"
+
+class nsView;
+class nsIWidget;
+class nsIAppWindow;
+
+namespace mozilla {
+class PresShell;
+} // namespace mozilla
+
+/**
+ * sizemode is an adjunct to widget size
+ */
+enum nsSizeMode {
+ nsSizeMode_Normal = 0,
+ nsSizeMode_Minimized,
+ nsSizeMode_Maximized,
+ nsSizeMode_Fullscreen,
+ nsSizeMode_Invalid
+};
+
+/**
+ * different types of (top-level) window z-level positioning
+ */
+enum nsWindowZ {
+ nsWindowZTop = 0, // on top
+ nsWindowZBottom, // on bottom
+ nsWindowZRelative // just below some specified widget
+};
+
+class nsIWidgetListener {
+ public:
+ /**
+ * If this listener is for an nsIAppWindow, return it. If this is null, then
+ * this is likely a listener for a view, which can be determined using
+ * GetView. If both methods return null, this will be an nsWebBrowser.
+ */
+ virtual nsIAppWindow* GetAppWindow();
+
+ /**
+ * If this listener is for an nsView, return it.
+ */
+ virtual nsView* GetView();
+
+ /**
+ * Return the presshell for this widget listener.
+ */
+ virtual mozilla::PresShell* GetPresShell();
+
+ /**
+ * Called when a window is moved to location (x, y). Returns true if the
+ * notification was handled. Coordinates are outer window screen coordinates.
+ */
+ enum class ByMoveToRect : bool { No, Yes };
+ virtual bool WindowMoved(nsIWidget* aWidget, int32_t aX, int32_t aY,
+ ByMoveToRect);
+
+ /**
+ * Called when a window is resized to (width, height). Returns true if the
+ * notification was handled. Coordinates are outer window screen coordinates.
+ */
+ virtual bool WindowResized(nsIWidget* aWidget, int32_t aWidth,
+ int32_t aHeight);
+
+ /**
+ * Called when the size mode (minimized, maximized, fullscreen) is changed.
+ */
+ virtual void SizeModeChanged(nsSizeMode aSizeMode);
+
+ /**
+ * Called when the DPI (device resolution scaling factor) is changed,
+ * such that UI elements may need to be rescaled.
+ */
+ virtual void UIResolutionChanged();
+
+#if defined(MOZ_WIDGET_ANDROID)
+ virtual void DynamicToolbarMaxHeightChanged(mozilla::ScreenIntCoord aHeight);
+ virtual void DynamicToolbarOffsetChanged(mozilla::ScreenIntCoord aOffset);
+#endif
+
+ /**
+ * Called when the z-order of the window is changed. Returns true if the
+ * notification was handled. aPlacement indicates the new z order. If
+ * placement is nsWindowZRelative, then aRequestBelow should be the
+ * window to place below. On return, aActualBelow will be set to the
+ * window actually behind. This generally only applies to Windows.
+ */
+ virtual bool ZLevelChanged(bool aImmediate, nsWindowZ* aPlacement,
+ nsIWidget* aRequestBelow,
+ nsIWidget** aActualBelow);
+
+ /**
+ * Called when the macOS titlebar is shown while in fullscreen.
+ */
+ virtual void MacFullscreenMenubarOverlapChanged(
+ mozilla::DesktopCoord aOverlapAmount);
+
+ /**
+ * Called when the occlusion state is changed.
+ */
+ virtual void OcclusionStateChanged(bool aIsFullyOccluded);
+
+ /**
+ * Called when the window is activated and focused.
+ */
+ virtual void WindowActivated();
+
+ /**
+ * Called when the window is deactivated and no longer focused.
+ */
+ virtual void WindowDeactivated();
+
+ /**
+ * Called when the show/hide toolbar button on the Mac titlebar is pressed.
+ */
+ virtual void OSToolbarButtonPressed();
+
+ /**
+ * Called when a request is made to close the window. Returns true if the
+ * notification was handled. Returns true if the notification was handled.
+ */
+ virtual bool RequestWindowClose(nsIWidget* aWidget);
+
+ /*
+ * Indicate that a paint is about to occur on this window. This is called
+ * at a time when it's OK to change the geometry of this widget or of
+ * other widgets. Must be called before every call to PaintWindow.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual void WillPaintWindow(nsIWidget* aWidget);
+
+ /**
+ * Paint the specified region of the window. Returns true if the
+ * notification was handled.
+ * This is called at a time when it is not OK to change the geometry of
+ * this widget or of other widgets.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual bool PaintWindow(nsIWidget* aWidget,
+ mozilla::LayoutDeviceIntRegion aRegion);
+
+ /**
+ * Indicates that a paint occurred.
+ * This is called at a time when it is OK to change the geometry of
+ * this widget or of other widgets.
+ * Must be called after every call to PaintWindow.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual void DidPaintWindow();
+
+ virtual void DidCompositeWindow(mozilla::layers::TransactionId aTransactionId,
+ const mozilla::TimeStamp& aCompositeStart,
+ const mozilla::TimeStamp& aCompositeEnd);
+
+ /**
+ * Request that layout schedules a repaint on the next refresh driver tick.
+ */
+ virtual void RequestRepaint();
+
+ /**
+ * Returns true if this is a popup that should not be visible. If this
+ * is a popup that is visible, not a popup or this state is unknown,
+ * returns false.
+ */
+ virtual bool ShouldNotBeVisible();
+
+ /**
+ * Handle an event.
+ */
+ virtual nsEventStatus HandleEvent(mozilla::WidgetGUIEvent* aEvent,
+ bool aUseAttachedEvents);
+
+ /**
+ * Called when safe area insets are changed.
+ */
+ virtual void SafeAreaInsetsChanged(
+ const mozilla::ScreenIntMargin& aSafeAreaInsets);
+};
+
+#endif
diff --git a/widget/nsIWinTaskbar.idl b/widget/nsIWinTaskbar.idl
new file mode 100644
index 0000000000..7d9b96a9d8
--- /dev/null
+++ b/widget/nsIWinTaskbar.idl
@@ -0,0 +1,196 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "nsISupports.idl"
+#include "nsIBaseWindow.idl"
+
+interface nsIDocShell;
+interface nsIJumpListBuilder;
+interface nsITaskbarTabPreview;
+interface nsITaskbarWindowPreview;
+interface nsITaskbarPreviewController;
+interface nsITaskbarProgress;
+interface nsITaskbarOverlayIconController;
+interface nsILegacyJumpListBuilder;
+interface mozIDOMWindow;
+
+/*
+ * nsIWinTaskbar
+ *
+ * This interface represents a service which exposes the APIs provided by the
+ * Windows taskbar to applications.
+ *
+ * Starting in Windows 7, applications gain some control over their appearance
+ * in the taskbar. By default, there is one taskbar preview per top level
+ * window (excluding popups). This preview is represented by an
+ * nsITaskbarWindowPreview object.
+ *
+ * An application can register its own "tab" previews. Such previews will hide
+ * the corresponding nsITaskbarWindowPreview automatically (though this is not
+ * reflected in the visible attribute of the nsITaskbarWindowPreview). These
+ * tab previews do not have to correspond to tabs in the application - they can
+ * vary in size, shape and location. They do not even need to be actual GUI
+ * elements on the window. Unlike window previews, tab previews require most of
+ * the functionality of the nsITaskbarPreviewController to be implemented.
+ *
+ * Applications can also show progress on their taskbar icon. This does not
+ * interact with the taskbar previews except if the nsITaskbarWindowPreview is
+ * made invisible in which case the progress is naturally not shown on that
+ * window.
+ *
+ * When taskbar icons are combined as is the default in Windows 7, the progress
+ * for those windows is also combined as defined here:
+ * http://msdn.microsoft.com/en-us/library/dd391697%28VS.85%29.aspx
+ *
+ * Applications may also define custom taskbar jump lists on application shortcuts.
+ * See nsILegacyJumpListBuilder for more information.
+ */
+
+[scriptable, uuid(11751471-9246-4c72-a80f-0c7df765d640)]
+interface nsIWinTaskbar : nsISupports
+{
+ /**
+ * Returns true if the operating system supports Win7+ taskbar features.
+ * This property acts as a replacement for in-place os version checking.
+ */
+ readonly attribute boolean available;
+
+ /**
+ * Returns the default application user model identity the application
+ * registers with the system. This id is used by the taskbar in grouping
+ * windows and in associating pinned shortcuts with running instances and
+ * jump lists.
+ */
+ readonly attribute AString defaultGroupId;
+
+ /**
+ * Same as above, but a different value so that Private Browsing windows
+ * can be separated in the Taskbar.
+ */
+ readonly attribute AString defaultPrivateGroupId;
+
+ /**
+ * Taskbar window and tab preview management
+ */
+
+ /**
+ * Creates a taskbar preview. The docshell should be a toplevel docshell and
+ * is used to find the toplevel window. See the documentation for
+ * nsITaskbarTabPreview for more information.
+ */
+ nsITaskbarTabPreview createTaskbarTabPreview(in nsIDocShell shell,
+ in nsITaskbarPreviewController controller);
+
+ /**
+ * Gets the taskbar preview for a window. The docshell is used to find the
+ * toplevel window. See the documentation for nsITaskbarTabPreview for more
+ * information.
+ *
+ * Note: to implement custom drawing or buttons, a controller is required.
+ */
+ nsITaskbarWindowPreview getTaskbarWindowPreview(in nsIDocShell shell);
+
+ /**
+ * Taskbar icon progress indicator
+ */
+
+ /**
+ * Gets the taskbar progress for a window. The docshell is used to find the
+ * toplevel window. See the documentation for nsITaskbarProgress for more
+ * information.
+ */
+ nsITaskbarProgress getTaskbarProgress(in nsIDocShell shell);
+
+ /**
+ * Taskbar icon overlay
+ */
+
+ /**
+ * Gets the taskbar icon overlay controller for a window. The docshell is used
+ * to find the toplevel window. See the documentation in
+ * nsITaskbarOverlayIconController for more details.
+ */
+ nsITaskbarOverlayIconController getOverlayIconController(in nsIDocShell shell);
+
+ /**
+ * Taskbar and start menu jump list management
+ */
+
+ /**
+ * Retrieve a legacy taskbar jump list builder. This jump list builder backend
+ * is in the process of being phased out.
+ *
+ * Fails if a jump list build operation has already been initiated, developers
+ * should make use of a single instance of nsILegacyJumpListBuilder for building lists
+ * within an application.
+ *
+ * @throws NS_ERROR_ALREADY_INITIALIZED if an nsILegacyJumpListBuilder instance is
+ * currently building a list.
+ */
+ nsILegacyJumpListBuilder createLegacyJumpListBuilder(in boolean aPrivateBrowsing);
+
+ /**
+ * Retrieves a Windows Jump List builder. This jump list builder can be used
+ * to asynchronously add, remove, and update items in the Windows Jump List.
+ *
+ * @throws NS_ERROR_UNEXPECTED if the builder failed to be created.
+ */
+ nsIJumpListBuilder createJumpListBuilder(in boolean aPrivateBrowsing);
+
+ /**
+ * Application window taskbar group settings
+ */
+
+ /**
+ * Get the grouping id for a window.
+ *
+ * The runtime sets a default, global grouping id for all windows on startup.
+ * getGroupIdForWindow allows finding the grouping of individual windows
+ * on the taskbar.
+ *
+ * @throw NS_ERROR_INVALID_ARG if the window is not a valid top level window
+ * associated with a widget.
+ * @throw NS_ERROR_FAILURE if the property on the window could not be set.
+ * @throw NS_ERROR_UNEXPECTED for general failures.
+ */
+ AString getGroupIdForWindow(in mozIDOMWindow aParent);
+
+ /**
+ * Set the grouping id for a window.
+ *
+ * The runtime sets a default, global grouping id for all windows on startup.
+ * setGroupIdForWindow allows individual windows to be grouped independently
+ * on the taskbar. Ids should be unique to the app and window to insure
+ * conflicts with other pinned applications do no arise.
+ *
+ * The default group id is based on application.ini vendor, application, and
+ * version values, with a format of 'vendor.app.version'. The default can be
+ * retrieved via defaultGroupId.
+ *
+ * Note, when a window changes taskbar window stacks, it is placed at the
+ * bottom of the new stack.
+ *
+ * @throw NS_ERROR_INVALID_ARG if the window is not a valid top level window
+ * associated with a widget.
+ * @throw NS_ERROR_FAILURE if the property on the window could not be set.
+ * @throw NS_ERROR_UNEXPECTED for general failures.
+ */
+ void setGroupIdForWindow(in mozIDOMWindow aParent, in AString aIdentifier);
+
+ /**
+ * Notify the taskbar that a window identified by its HWND is about to enter
+ * full screen mode.
+ *
+ * A Windows autohide taskbar will not behave correctly in all cases if
+ * it is not notified when full screen operations start and end.
+ *
+ * @throw NS_ERROR_INVALID_ARG if the window is not a valid top level window
+ * @throw NS_ERROR_UNEXPECTED for general failures.
+ * @throw NS_ERROR_NOT_AVAILABLE if the taskbar cannot be obtained.
+ */
+ [noscript] void prepareFullScreen(in voidPtr aHWND, in boolean aFullScreen);
+};
diff --git a/widget/nsIWindowsUIUtils.idl b/widget/nsIWindowsUIUtils.idl
new file mode 100644
index 0000000000..6c84e90afd
--- /dev/null
+++ b/widget/nsIWindowsUIUtils.idl
@@ -0,0 +1,34 @@
+/* -*- 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 "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+interface imgIContainer;
+
+[scriptable, builtinclass, uuid(aa8a0ecf-96a1-418c-b80e-f24ae18bbedc)]
+interface nsIWindowsUIUtils : nsISupports
+{
+ readonly attribute long systemSmallIconSize;
+ readonly attribute long systemLargeIconSize;
+
+ void setWindowIcon(in mozIDOMWindowProxy aWindow, in imgIContainer aSmallIcon, in imgIContainer aLargeIcon);
+
+ void setWindowIconFromExe(in mozIDOMWindowProxy aWindow, in AString aExe, in unsigned short aIndex);
+
+ 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
+ */
+ readonly attribute boolean inTabletMode;
+
+ /**
+ * Share URL
+ */
+ void shareUrl(in AString shareTitle, in AString urlToShare);
+};
diff --git a/widget/nsNativeTheme.cpp b/widget/nsNativeTheme.cpp
new file mode 100644
index 0000000000..2484f802cd
--- /dev/null
+++ b/widget/nsNativeTheme.cpp
@@ -0,0 +1,580 @@
+/* -*- 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 "nsNativeTheme.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+#include "nsIContent.h"
+#include "nsIFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsNumberControlFrame.h"
+#include "nsPresContext.h"
+#include "nsString.h"
+#include "nsNameSpaceManager.h"
+#include "nsStyleConsts.h"
+#include "nsPIDOMWindow.h"
+#include "nsProgressFrame.h"
+#include "nsMeterFrame.h"
+#include "nsRangeFrame.h"
+#include "nsCSSRendering.h"
+#include "ImageContainer.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLProgressElement.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsNativeTheme::nsNativeTheme() : mAnimatedContentTimeout(UINT32_MAX) {}
+
+NS_IMPL_ISUPPORTS(nsNativeTheme, nsITimerCallback, nsINamed)
+
+/* static */ ElementState nsNativeTheme::GetContentState(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (!aFrame) {
+ return ElementState();
+ }
+
+ nsIContent* frameContent = aFrame->GetContent();
+ if (!frameContent || !frameContent->IsElement()) {
+ return ElementState();
+ }
+
+ const bool isXULElement = frameContent->IsXULElement();
+ if (isXULElement) {
+ if (aAppearance == StyleAppearance::Checkbox ||
+ aAppearance == StyleAppearance::Radio ||
+ aAppearance == StyleAppearance::ToolbarbuttonDropdown ||
+ aAppearance == StyleAppearance::ButtonArrowPrevious ||
+ aAppearance == StyleAppearance::ButtonArrowNext ||
+ aAppearance == StyleAppearance::ButtonArrowUp ||
+ aAppearance == StyleAppearance::ButtonArrowDown) {
+ aFrame = aFrame->GetParent();
+ frameContent = aFrame->GetContent();
+ }
+ MOZ_ASSERT(frameContent && frameContent->IsElement());
+ }
+
+ ElementState flags = frameContent->AsElement()->StyleState();
+ nsNumberControlFrame* numberControlFrame =
+ nsNumberControlFrame::GetNumberControlFrameForSpinButton(aFrame);
+ if (numberControlFrame &&
+ numberControlFrame->GetContent()->AsElement()->StyleState().HasState(
+ ElementState::DISABLED)) {
+ flags |= ElementState::DISABLED;
+ }
+
+ if (!isXULElement) {
+ return flags;
+ }
+
+ if (CheckBooleanAttr(aFrame, nsGkAtoms::disabled)) {
+ flags |= ElementState::DISABLED;
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::Radio: {
+ if (CheckBooleanAttr(aFrame, nsGkAtoms::focused)) {
+ flags |= ElementState::FOCUS;
+ nsPIDOMWindowOuter* window =
+ aFrame->GetContent()->OwnerDoc()->GetWindow();
+ if (window && window->ShouldShowFocusRing()) {
+ flags |= ElementState::FOCUSRING;
+ }
+ }
+ if (CheckBooleanAttr(aFrame, nsGkAtoms::selected) ||
+ CheckBooleanAttr(aFrame, nsGkAtoms::checked)) {
+ flags |= ElementState::CHECKED;
+ }
+ break;
+ }
+ case StyleAppearance::Checkbox: {
+ if (CheckBooleanAttr(aFrame, nsGkAtoms::checked)) {
+ flags |= ElementState::CHECKED;
+ } else if (CheckBooleanAttr(aFrame, nsGkAtoms::indeterminate)) {
+ flags |= ElementState::INDETERMINATE;
+ }
+ break;
+ }
+ case StyleAppearance::Toolbarbutton:
+ if (CheckBooleanAttr(aFrame, nsGkAtoms::open)) {
+ flags |= ElementState::HOVER | ElementState::ACTIVE;
+ }
+ break;
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Searchfield:
+ case StyleAppearance::Textarea: {
+ if (CheckBooleanAttr(aFrame, nsGkAtoms::focused)) {
+ flags |= ElementState::FOCUS | ElementState::FOCUSRING;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return flags;
+}
+
+/* static */
+bool nsNativeTheme::CheckBooleanAttr(nsIFrame* aFrame, nsAtom* aAtom) {
+ if (!aFrame) return false;
+
+ nsIContent* content = aFrame->GetContent();
+ if (!content || !content->IsElement()) return false;
+
+ if (content->IsHTMLElement()) return content->AsElement()->HasAttr(aAtom);
+
+ // For XML/XUL elements, an attribute must be equal to the literal
+ // string "true" to be counted as true. An empty string should _not_
+ // be counted as true.
+ return content->AsElement()->AttrValueIs(kNameSpaceID_None, aAtom, u"true"_ns,
+ eCaseMatters);
+}
+
+/* static */
+int32_t nsNativeTheme::CheckIntAttr(nsIFrame* aFrame, nsAtom* aAtom,
+ int32_t defaultValue) {
+ if (!aFrame) return defaultValue;
+
+ nsIContent* content = aFrame->GetContent();
+ if (!content || !content->IsElement()) return defaultValue;
+
+ nsAutoString attr;
+ content->AsElement()->GetAttr(aAtom, attr);
+ nsresult err;
+ int32_t value = attr.ToInteger(&err);
+ if (attr.IsEmpty() || NS_FAILED(err)) return defaultValue;
+
+ return value;
+}
+
+/* static */
+double nsNativeTheme::GetProgressValue(nsIFrame* aFrame) {
+ if (!aFrame || !aFrame->GetContent()->IsHTMLElement(nsGkAtoms::progress)) {
+ return 0;
+ }
+
+ return static_cast<HTMLProgressElement*>(aFrame->GetContent())->Value();
+}
+
+/* static */
+double nsNativeTheme::GetProgressMaxValue(nsIFrame* aFrame) {
+ if (!aFrame || !aFrame->GetContent()->IsHTMLElement(nsGkAtoms::progress)) {
+ return 100;
+ }
+
+ return static_cast<HTMLProgressElement*>(aFrame->GetContent())->Max();
+}
+
+bool nsNativeTheme::IsButtonTypeMenu(nsIFrame* aFrame) {
+ if (!aFrame) return false;
+
+ nsIContent* content = aFrame->GetContent();
+ return content->IsXULElement(nsGkAtoms::button) &&
+ content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ u"menu"_ns, eCaseMatters);
+}
+
+bool nsNativeTheme::IsPressedButton(nsIFrame* aFrame) {
+ ElementState state = GetContentState(aFrame, StyleAppearance::Toolbarbutton);
+ if (state.HasState(ElementState::DISABLED)) {
+ return false;
+ }
+
+ return IsOpenButton(aFrame) ||
+ state.HasAllStates(ElementState::ACTIVE | ElementState::HOVER);
+}
+
+bool nsNativeTheme::IsWidgetStyled(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ // Check for specific widgets to see if HTML has overridden the style.
+ if (!aFrame) {
+ return false;
+ }
+
+ /**
+ * Progress bar appearance should be the same for the bar and the container
+ * frame. nsProgressFrame owns the logic and will tell us what we should do.
+ */
+ if (aAppearance == StyleAppearance::Progresschunk ||
+ aAppearance == StyleAppearance::ProgressBar) {
+ nsProgressFrame* progressFrame = do_QueryFrame(
+ aAppearance == StyleAppearance::Progresschunk ? aFrame->GetParent()
+ : aFrame);
+ if (progressFrame) {
+ return !progressFrame->ShouldUseNativeStyle();
+ }
+ }
+
+ /**
+ * Meter bar appearance should be the same for the bar and the container
+ * frame. nsMeterFrame owns the logic and will tell us what we should do.
+ */
+ if (aAppearance == StyleAppearance::Meterchunk ||
+ aAppearance == StyleAppearance::Meter) {
+ nsMeterFrame* meterFrame = do_QueryFrame(
+ aAppearance == StyleAppearance::Meterchunk ? aFrame->GetParent()
+ : aFrame);
+ if (meterFrame) {
+ return !meterFrame->ShouldUseNativeStyle();
+ }
+ }
+
+ /**
+ * An nsRangeFrame and its children are treated atomically when it
+ * comes to native theming (either all parts, or no parts, are themed).
+ * nsRangeFrame owns the logic and will tell us what we should do.
+ */
+ if (aAppearance == StyleAppearance::Range ||
+ aAppearance == StyleAppearance::RangeThumb) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(
+ aAppearance == StyleAppearance::RangeThumb ? aFrame->GetParent()
+ : aFrame);
+ if (rangeFrame) {
+ return !rangeFrame->ShouldUseNativeStyle();
+ }
+ }
+
+ return nsLayoutUtils::AuthorSpecifiedBorderBackgroundDisablesTheming(
+ aAppearance) &&
+ aFrame->GetContent()->IsHTMLElement() &&
+ aFrame->Style()->HasAuthorSpecifiedBorderOrBackground();
+}
+
+/* static */
+bool nsNativeTheme::IsFrameRTL(nsIFrame* aFrame) {
+ if (!aFrame) {
+ return false;
+ }
+ return aFrame->GetWritingMode().IsPhysicalRTL();
+}
+
+/* static */
+bool nsNativeTheme::IsHTMLContent(nsIFrame* aFrame) {
+ if (!aFrame) {
+ return false;
+ }
+ nsIContent* content = aFrame->GetContent();
+ return content && content->IsHTMLElement();
+}
+
+// treeheadercell:
+nsNativeTheme::TreeSortDirection nsNativeTheme::GetTreeSortDirection(
+ nsIFrame* aFrame) {
+ if (!aFrame || !aFrame->GetContent()) return eTreeSortDirection_Natural;
+
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::descending,
+ nsGkAtoms::ascending, nullptr};
+
+ nsIContent* content = aFrame->GetContent();
+ if (content->IsElement()) {
+ switch (content->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::sortDirection, strings, eCaseMatters)) {
+ case 0:
+ return eTreeSortDirection_Descending;
+ case 1:
+ return eTreeSortDirection_Ascending;
+ }
+ }
+
+ return eTreeSortDirection_Natural;
+}
+
+bool nsNativeTheme::IsLastTreeHeaderCell(nsIFrame* aFrame) {
+ if (!aFrame) {
+ return false;
+ }
+
+ // A tree column picker button is always the last header cell.
+ if (aFrame->GetContent()->IsXULElement(nsGkAtoms::button)) {
+ return true;
+ }
+
+ // Find the parent tree.
+ nsIContent* parent = aFrame->GetContent()->GetParent();
+ while (parent && !parent->IsXULElement(nsGkAtoms::tree)) {
+ parent = parent->GetParent();
+ }
+
+ // If the column picker is visible, this can't be the last column.
+ if (parent && !parent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::hidecolumnpicker,
+ u"true"_ns, eCaseMatters))
+ return false;
+
+ while ((aFrame = aFrame->GetNextSibling())) {
+ if (aFrame->GetRect().Width() > 0) return false;
+ }
+ return true;
+}
+
+// tab:
+bool nsNativeTheme::IsBottomTab(nsIFrame* aFrame) {
+ if (!aFrame) return false;
+
+ nsAutoString classStr;
+ if (aFrame->GetContent()->IsElement()) {
+ aFrame->GetContent()->AsElement()->GetAttr(nsGkAtoms::_class, classStr);
+ }
+ // FIXME: This looks bogus, shouldn't this be looking at GetClasses()?
+ return !classStr.IsEmpty() && classStr.Find(u"tab-bottom") != kNotFound;
+}
+
+bool nsNativeTheme::IsFirstTab(nsIFrame* aFrame) {
+ if (!aFrame) return false;
+
+ for (nsIFrame* first : aFrame->GetParent()->PrincipalChildList()) {
+ if (first->GetRect().Width() > 0 &&
+ first->GetContent()->IsXULElement(nsGkAtoms::tab))
+ return (first == aFrame);
+ }
+ return false;
+}
+
+bool nsNativeTheme::IsHorizontal(nsIFrame* aFrame) {
+ if (!aFrame) return false;
+
+ if (!aFrame->GetContent()->IsElement()) return true;
+
+ return !aFrame->GetContent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::orient, nsGkAtoms::vertical, eCaseMatters);
+}
+
+bool nsNativeTheme::IsNextToSelectedTab(nsIFrame* aFrame, int32_t aOffset) {
+ if (!aFrame) return false;
+
+ if (aOffset == 0) return IsSelectedTab(aFrame);
+
+ int32_t thisTabIndex = -1, selectedTabIndex = -1;
+
+ nsIFrame* currentTab = aFrame->GetParent()->PrincipalChildList().FirstChild();
+ for (int32_t i = 0; currentTab; currentTab = currentTab->GetNextSibling()) {
+ if (currentTab->GetRect().Width() == 0) continue;
+ if (aFrame == currentTab) thisTabIndex = i;
+ if (IsSelectedTab(currentTab)) selectedTabIndex = i;
+ ++i;
+ }
+
+ if (thisTabIndex == -1 || selectedTabIndex == -1) return false;
+
+ return (thisTabIndex - selectedTabIndex == aOffset);
+}
+
+bool nsNativeTheme::IsVerticalProgress(nsIFrame* aFrame) {
+ if (!aFrame) {
+ return false;
+ }
+ return IsVerticalMeter(aFrame);
+}
+
+bool nsNativeTheme::IsVerticalMeter(nsIFrame* aFrame) {
+ MOZ_ASSERT(aFrame, "You have to pass a non-null aFrame");
+ switch (aFrame->StyleDisplay()->mOrient) {
+ case StyleOrient::Horizontal:
+ return false;
+ case StyleOrient::Vertical:
+ return true;
+ case StyleOrient::Inline:
+ return aFrame->GetWritingMode().IsVertical();
+ case StyleOrient::Block:
+ return !aFrame->GetWritingMode().IsVertical();
+ }
+ MOZ_ASSERT_UNREACHABLE("unexpected -moz-orient value");
+ return false;
+}
+
+// menupopup:
+bool nsNativeTheme::IsSubmenu(nsIFrame* aFrame, bool* aLeftOfParent) {
+ if (!aFrame) return false;
+
+ nsIContent* parentContent = aFrame->GetContent()->GetParent();
+ if (!parentContent || !parentContent->IsXULElement(nsGkAtoms::menu))
+ return false;
+
+ nsIFrame* parent = aFrame;
+ while ((parent = parent->GetParent())) {
+ if (parent->GetContent() == parentContent) {
+ if (aLeftOfParent) {
+ LayoutDeviceIntRect selfBounds, parentBounds;
+ selfBounds = aFrame->GetNearestWidget()->GetScreenBounds();
+ parentBounds = parent->GetNearestWidget()->GetScreenBounds();
+ *aLeftOfParent = selfBounds.X() < parentBounds.X();
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool nsNativeTheme::QueueAnimatedContentForRefresh(nsIContent* aContent,
+ uint32_t aMinimumFrameRate) {
+ NS_ASSERTION(aContent, "Null pointer!");
+ NS_ASSERTION(aMinimumFrameRate, "aMinimumFrameRate must be non-zero!");
+ NS_ASSERTION(aMinimumFrameRate <= 1000,
+ "aMinimumFrameRate must be less than 1000!");
+
+ uint32_t timeout = 1000 / aMinimumFrameRate;
+ timeout = std::min(mAnimatedContentTimeout, timeout);
+
+ if (!mAnimatedContentTimer) {
+ mAnimatedContentTimer = NS_NewTimer();
+ NS_ENSURE_TRUE(mAnimatedContentTimer, false);
+ }
+
+ if (mAnimatedContentList.IsEmpty() || timeout != mAnimatedContentTimeout) {
+ nsresult rv;
+ if (!mAnimatedContentList.IsEmpty()) {
+ rv = mAnimatedContentTimer->Cancel();
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+
+ if (XRE_IsContentProcess() && NS_IsMainThread()) {
+ mAnimatedContentTimer->SetTarget(GetMainThreadSerialEventTarget());
+ }
+ rv = mAnimatedContentTimer->InitWithCallback(this, timeout,
+ nsITimer::TYPE_ONE_SHOT);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ mAnimatedContentTimeout = timeout;
+ }
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mAnimatedContentList.AppendElement(aContent);
+
+ return true;
+}
+
+NS_IMETHODIMP
+nsNativeTheme::Notify(nsITimer* aTimer) {
+ NS_ASSERTION(aTimer == mAnimatedContentTimer, "Wrong timer!");
+
+ // XXX Assumes that calling nsIFrame::Invalidate won't reenter
+ // QueueAnimatedContentForRefresh.
+
+ uint32_t count = mAnimatedContentList.Length();
+ for (uint32_t index = 0; index < count; index++) {
+ nsIFrame* frame = mAnimatedContentList[index]->GetPrimaryFrame();
+ if (frame) {
+ frame->InvalidateFrame();
+ }
+ }
+
+ mAnimatedContentList.Clear();
+ mAnimatedContentTimeout = UINT32_MAX;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNativeTheme::GetName(nsACString& aName) {
+ aName.AssignLiteral("nsNativeTheme");
+ return NS_OK;
+}
+
+nsIFrame* nsNativeTheme::GetAdjacentSiblingFrameWithSameAppearance(
+ nsIFrame* aFrame, bool aNextSibling) {
+ if (!aFrame) return nullptr;
+
+ // Find the next visible sibling.
+ nsIFrame* sibling = aFrame;
+ do {
+ sibling =
+ aNextSibling ? sibling->GetNextSibling() : sibling->GetPrevSibling();
+ } while (sibling && sibling->GetRect().Width() == 0);
+
+ // Check same appearance and adjacency.
+ if (!sibling ||
+ sibling->StyleDisplay()->EffectiveAppearance() !=
+ aFrame->StyleDisplay()->EffectiveAppearance() ||
+ (sibling->GetRect().XMost() != aFrame->GetRect().X() &&
+ aFrame->GetRect().XMost() != sibling->GetRect().X()))
+ return nullptr;
+ return sibling;
+}
+
+bool nsNativeTheme::IsRangeHorizontal(nsIFrame* aFrame) {
+ nsIFrame* rangeFrame = aFrame;
+ if (!rangeFrame->IsRangeFrame()) {
+ // If the thumb's frame is passed in, get its range parent:
+ rangeFrame = aFrame->GetParent();
+ }
+ if (rangeFrame->IsRangeFrame()) {
+ return static_cast<nsRangeFrame*>(rangeFrame)->IsHorizontal();
+ }
+ // Not actually a range frame - just use the ratio of the frame's size to
+ // decide:
+ return aFrame->GetSize().width >= aFrame->GetSize().height;
+}
+
+/* static */
+bool nsNativeTheme::IsDarkBackgroundForScrollbar(nsIFrame* aFrame) {
+ // Try to find the scrolled frame. Note that for stuff like xul <tree> there
+ // might be none.
+ {
+ nsIFrame* frame = aFrame;
+ nsIScrollableFrame* scrollFrame = nullptr;
+ while (!scrollFrame && frame) {
+ scrollFrame = frame->GetScrollTargetFrame();
+ frame = frame->GetParent();
+ }
+ if (scrollFrame) {
+ aFrame = scrollFrame->GetScrolledFrame();
+ } else {
+ // Leave aFrame untouched.
+ }
+ }
+
+ return IsDarkBackground(aFrame);
+}
+
+/* static */
+bool nsNativeTheme::IsDarkBackground(nsIFrame* aFrame) {
+ auto color =
+ nsCSSRendering::FindEffectiveBackgroundColor(
+ aFrame, /* aStopAtThemed = */ false, /* aPreferBodyToCanvas = */ true)
+ .mColor;
+ return LookAndFeel::IsDarkColor(color);
+}
+
+/*static*/
+bool nsNativeTheme::IsWidgetScrollbarPart(StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ case StyleAppearance::ScrollbarthumbVertical:
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ case StyleAppearance::ScrollbartrackHorizontal:
+ case StyleAppearance::ScrollbartrackVertical:
+ case StyleAppearance::Scrollcorner:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/*static*/
+bool nsNativeTheme::IsWidgetAlwaysNonNative(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ return IsWidgetScrollbarPart(aAppearance) ||
+ aAppearance == StyleAppearance::FocusOutline ||
+ (aFrame && aFrame->StyleUI()->mMozTheme == StyleMozTheme::NonNative);
+}
diff --git a/widget/nsNativeTheme.h b/widget/nsNativeTheme.h
new file mode 100644
index 0000000000..28a8c0cc3f
--- /dev/null
+++ b/widget/nsNativeTheme.h
@@ -0,0 +1,170 @@
+/* -*- 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/. */
+
+// This defines a common base class for nsITheme implementations, to reduce
+// code duplication.
+
+#ifndef _NSNATIVETHEME_H_
+#define _NSNATIVETHEME_H_
+
+#include "nsAlgorithm.h"
+#include "nsAtom.h"
+#include "nsColor.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsMargin.h"
+#include "nsGkAtoms.h"
+#include "nsTArray.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+#include "nsIContent.h"
+#include "mozilla/dom/RustTypes.h"
+
+class nsIFrame;
+class nsPresContext;
+
+namespace mozilla {
+class ComputedStyle;
+enum class StyleAppearance : uint8_t;
+} // namespace mozilla
+
+class nsNativeTheme : public nsITimerCallback, public nsINamed {
+ protected:
+ virtual ~nsNativeTheme() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ nsNativeTheme();
+
+ public:
+ enum ScrollbarButtonType {
+ eScrollbarButton_UpTop = 0,
+ eScrollbarButton_Down = 1 << 0,
+ eScrollbarButton_Bottom = 1 << 1
+ };
+
+ enum TreeSortDirection {
+ eTreeSortDirection_Descending,
+ eTreeSortDirection_Natural,
+ eTreeSortDirection_Ascending
+ };
+ // Returns the content state (hover, focus, etc), see EventStateManager.h
+ static mozilla::dom::ElementState GetContentState(
+ nsIFrame* aFrame, mozilla::StyleAppearance aAppearance);
+
+ // Returns whether the widget is already styled by content
+ // Normally called from ThemeSupportsWidget to turn off native theming
+ // for elements that are already styled.
+ bool IsWidgetStyled(nsPresContext* aPresContext, nsIFrame* aFrame,
+ mozilla::StyleAppearance aAppearance);
+
+ // RTL chrome direction
+ static bool IsFrameRTL(nsIFrame* aFrame);
+
+ static bool IsHTMLContent(nsIFrame* aFrame);
+
+ // button:
+ bool IsDefaultButton(nsIFrame* aFrame) {
+ return CheckBooleanAttr(aFrame, nsGkAtoms::_default);
+ }
+
+ bool IsButtonTypeMenu(nsIFrame* aFrame);
+
+ // tab:
+ bool IsSelectedTab(nsIFrame* aFrame) {
+ return CheckBooleanAttr(aFrame, nsGkAtoms::visuallyselected);
+ }
+
+ bool IsNextToSelectedTab(nsIFrame* aFrame, int32_t aOffset);
+
+ bool IsBeforeSelectedTab(nsIFrame* aFrame) {
+ return IsNextToSelectedTab(aFrame, -1);
+ }
+
+ bool IsAfterSelectedTab(nsIFrame* aFrame) {
+ return IsNextToSelectedTab(aFrame, 1);
+ }
+
+ bool IsLeftToSelectedTab(nsIFrame* aFrame) {
+ return IsFrameRTL(aFrame) ? IsAfterSelectedTab(aFrame)
+ : IsBeforeSelectedTab(aFrame);
+ }
+
+ bool IsRightToSelectedTab(nsIFrame* aFrame) {
+ return IsFrameRTL(aFrame) ? IsBeforeSelectedTab(aFrame)
+ : IsAfterSelectedTab(aFrame);
+ }
+
+ // button / toolbarbutton:
+ bool IsCheckedButton(nsIFrame* aFrame) {
+ return CheckBooleanAttr(aFrame, nsGkAtoms::checked);
+ }
+
+ bool IsSelectedButton(nsIFrame* aFrame) {
+ return CheckBooleanAttr(aFrame, nsGkAtoms::checked) ||
+ CheckBooleanAttr(aFrame, nsGkAtoms::selected);
+ }
+
+ bool IsOpenButton(nsIFrame* aFrame) {
+ return CheckBooleanAttr(aFrame, nsGkAtoms::open);
+ }
+
+ bool IsPressedButton(nsIFrame* aFrame);
+
+ // treeheadercell:
+ TreeSortDirection GetTreeSortDirection(nsIFrame* aFrame);
+ bool IsLastTreeHeaderCell(nsIFrame* aFrame);
+
+ // tab:
+ bool IsBottomTab(nsIFrame* aFrame);
+ bool IsFirstTab(nsIFrame* aFrame);
+
+ bool IsHorizontal(nsIFrame* aFrame);
+
+ // progressbar:
+ bool IsVerticalProgress(nsIFrame* aFrame);
+
+ // meter:
+ bool IsVerticalMeter(nsIFrame* aFrame);
+
+ // textfield:
+ bool IsReadOnly(nsIFrame* aFrame) {
+ return CheckBooleanAttr(aFrame, nsGkAtoms::readonly);
+ }
+
+ // menupopup:
+ bool IsSubmenu(nsIFrame* aFrame, bool* aLeftOfParent);
+
+ static bool CheckBooleanAttr(nsIFrame* aFrame, nsAtom* aAtom);
+ static int32_t CheckIntAttr(nsIFrame* aFrame, nsAtom* aAtom,
+ int32_t defaultValue);
+
+ // Helpers for progressbar.
+ static double GetProgressValue(nsIFrame* aFrame);
+ static double GetProgressMaxValue(nsIFrame* aFrame);
+
+ bool QueueAnimatedContentForRefresh(nsIContent* aContent,
+ uint32_t aMinimumFrameRate);
+
+ nsIFrame* GetAdjacentSiblingFrameWithSameAppearance(nsIFrame* aFrame,
+ bool aNextSibling);
+
+ bool IsRangeHorizontal(nsIFrame* aFrame);
+
+ static bool IsDarkBackgroundForScrollbar(nsIFrame*);
+ static bool IsDarkBackground(nsIFrame*);
+
+ static bool IsWidgetScrollbarPart(mozilla::StyleAppearance);
+ static bool IsWidgetAlwaysNonNative(nsIFrame*, mozilla::StyleAppearance);
+
+ private:
+ uint32_t mAnimatedContentTimeout;
+ nsCOMPtr<nsITimer> mAnimatedContentTimer;
+ AutoTArray<nsCOMPtr<nsIContent>, 20> mAnimatedContentList;
+};
+
+#endif // _NSNATIVETHEME_H_
diff --git a/widget/nsPaper.cpp b/widget/nsPaper.cpp
new file mode 100644
index 0000000000..a940d3c1cb
--- /dev/null
+++ b/widget/nsPaper.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsPaper.h"
+#include "nsPaperMargin.h"
+#include "nsPrinterBase.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/Promise.h"
+
+using mozilla::ErrorResult;
+using mozilla::PaperInfo;
+
+NS_IMPL_CYCLE_COLLECTION(nsPaper, mMarginPromise, mPrinter)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsPaper)
+ NS_INTERFACE_MAP_ENTRY(nsIPaper)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPaper)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsPaper)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsPaper)
+
+nsPaper::nsPaper(const mozilla::PaperInfo& aInfo)
+ : mPrinter(nullptr), mInfo(aInfo) {}
+
+nsPaper::nsPaper(nsPrinterBase& aPrinter, const mozilla::PaperInfo& aInfo)
+ : mPrinter(&aPrinter), mInfo(aInfo) {}
+
+nsPaper::~nsPaper() = default;
+
+NS_IMETHODIMP
+nsPaper::GetId(nsAString& aId) {
+ aId = mInfo.mId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPaper::GetName(nsAString& aName) {
+ aName = mInfo.mName.IsEmpty() ? mInfo.mId : mInfo.mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPaper::GetWidth(double* aWidth) {
+ NS_ENSURE_ARG_POINTER(aWidth);
+ *aWidth = mInfo.mSize.Width();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPaper::GetHeight(double* aHeight) {
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aHeight = mInfo.mSize.Height();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPaper::GetUnwriteableMargin(JSContext* aCx, Promise** aPromise) {
+ if (RefPtr<Promise> existing = mMarginPromise) {
+ existing.forget(aPromise);
+ return NS_OK;
+ }
+ ErrorResult rv;
+ RefPtr<Promise> promise = Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ mMarginPromise = promise;
+
+ if (mInfo.mUnwriteableMargin) {
+ auto margin = mozilla::MakeRefPtr<nsPaperMargin>(*mInfo.mUnwriteableMargin);
+ mMarginPromise->MaybeResolve(margin);
+ } else {
+ if (mPrinter) {
+ mPrinter->QueryMarginsForPaper(*mMarginPromise, mInfo.mId);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("common paper sizes should know their margins");
+ mMarginPromise->MaybeRejectWithNotSupportedError("Margins unavailable");
+ }
+ }
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
diff --git a/widget/nsPaper.h b/widget/nsPaper.h
new file mode 100644
index 0000000000..b0117b7306
--- /dev/null
+++ b/widget/nsPaper.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsPaper_h__
+#define nsPaper_h__
+
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/Maybe.h"
+#include "nsIPaper.h"
+#include "nsISupportsImpl.h"
+#include "js/TypeDecls.h"
+#include "nsString.h"
+
+struct JSContext;
+
+namespace mozilla {
+
+// Simple struct that can be used off the main thread to hold all the info from
+// an nsPaper instance.
+struct PaperInfo {
+ using MarginDouble = mozilla::gfx::MarginDouble;
+ using SizeDouble = mozilla::gfx::SizeDouble;
+
+ PaperInfo() = default;
+ PaperInfo(const nsAString& aId, const nsAString& aName,
+ const SizeDouble& aSize,
+ const Maybe<MarginDouble>& aUnwriteableMargin)
+ : mId(aId),
+ mName(aName),
+ mSize(aSize),
+ mUnwriteableMargin(aUnwriteableMargin) {}
+
+ nsString mId;
+ nsString mName;
+
+ SizeDouble mSize;
+
+ // The margins may not be known by some back-ends.
+ Maybe<MarginDouble> mUnwriteableMargin{Nothing()};
+};
+
+/**
+ * Plain struct used for commonly used, hard-coded paper sizes.
+ *
+ * Used to construct PaperInfo at runtime by localizing the name.
+ */
+struct CommonPaperSize final {
+ // The standardized PWG name, which should be used as the PaperInfo id
+ nsLiteralString mPWGName;
+ // The name key to localize the name of this paper size using strings in
+ // printUI.ftl
+ nsLiteralCString mLocalizableNameKey;
+ // Size is in points, same as PaperInfo
+ gfx::SizeDouble mSize;
+};
+
+} // namespace mozilla
+
+class nsPrinterBase;
+
+class nsPaper final : public nsIPaper {
+ using Promise = mozilla::dom::Promise;
+ using CommonPaperSize = mozilla::CommonPaperSize;
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsPaper)
+ NS_DECL_NSIPAPER
+
+ nsPaper() = delete;
+ explicit nsPaper(const mozilla::PaperInfo&);
+ nsPaper(nsPrinterBase&, const mozilla::PaperInfo&);
+
+ // This list is used for both our fallback paper sizes (used when a printer
+ // does not provide a list of available paper sizes), and for localizing
+ // common paper sizes to avoid querying the printer for extra information in
+ // the common case, as well as to provide more uniform paper names in the
+ // frontend.
+ // If we need to separate these two uses, we will need to either split this
+ // into two lists, or add a flag to indicate what the size is used for.
+#define mm *72.0 / 25.4
+#define in *72.0
+ static constexpr CommonPaperSize kCommonPaperSizes[] = {
+ CommonPaperSize{u"iso_a5"_ns, "a5"_ns, {148 mm, 210 mm}},
+ CommonPaperSize{u"iso_a4"_ns, "a4"_ns, {210 mm, 297 mm}},
+ CommonPaperSize{u"iso_a3"_ns, "a3"_ns, {297 mm, 420 mm}},
+ CommonPaperSize{u"iso_b5"_ns, "b5"_ns, {176 mm, 250 mm}},
+ CommonPaperSize{u"iso_b4"_ns, "b4"_ns, {250 mm, 353 mm}},
+ CommonPaperSize{u"jis_b5"_ns, "jis-b5"_ns, {182 mm, 257 mm}},
+ CommonPaperSize{u"jis_b4"_ns, "jis-b4"_ns, {257 mm, 364 mm}},
+ CommonPaperSize{u"na_letter"_ns, "letter"_ns, {8.5 in, 11 in}},
+ CommonPaperSize{u"na_legal"_ns, "legal"_ns, {8.5 in, 14 in}},
+ CommonPaperSize{u"na_ledger"_ns, "tabloid"_ns, {11 in, 17 in}}};
+#undef mm
+#undef in
+ static constexpr size_t kNumCommonPaperSizes =
+ mozilla::ArrayLength(kCommonPaperSizes);
+
+ private:
+ ~nsPaper();
+
+ // null if not associated with a printer (for "Save-to-PDF" paper sizes)
+ RefPtr<nsPrinterBase> mPrinter;
+
+ RefPtr<Promise> mMarginPromise;
+ const mozilla::PaperInfo mInfo;
+};
+
+namespace mozilla {
+
+// Used to allow fixed-sized arrays of PWG paper info to be ref-counted.
+class CommonPaperInfoArray
+ : public Array<PaperInfo, nsPaper::kNumCommonPaperSizes> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CommonPaperInfoArray);
+ CommonPaperInfoArray() = default;
+
+ private:
+ ~CommonPaperInfoArray() = default;
+};
+
+} // namespace mozilla
+
+#endif /* nsPaper_h__ */
diff --git a/widget/nsPaperMargin.cpp b/widget/nsPaperMargin.cpp
new file mode 100644
index 0000000000..1e6171cd16
--- /dev/null
+++ b/widget/nsPaperMargin.cpp
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsPaperMargin.h"
+
+NS_IMPL_ISUPPORTS(nsPaperMargin, nsIPaperMargin)
+
+NS_IMETHODIMP
+nsPaperMargin::GetTop(double* aTop) {
+ *aTop = mMargin.top;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPaperMargin::GetRight(double* aRight) {
+ *aRight = mMargin.right;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPaperMargin::GetBottom(double* aBottom) {
+ *aBottom = mMargin.bottom;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPaperMargin::GetLeft(double* aLeft) {
+ *aLeft = mMargin.left;
+ return NS_OK;
+}
diff --git a/widget/nsPaperMargin.h b/widget/nsPaperMargin.h
new file mode 100644
index 0000000000..594b266c00
--- /dev/null
+++ b/widget/nsPaperMargin.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsPaperMargin_h_
+#define nsPaperMargin_h_
+
+#include "nsISupportsImpl.h"
+#include "nsIPaperMargin.h"
+#include "mozilla/gfx/Rect.h"
+
+class nsPaperMargin final : public nsIPaperMargin {
+ using MarginDouble = mozilla::gfx::MarginDouble;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPAPERMARGIN
+
+ nsPaperMargin() = delete;
+
+ explicit nsPaperMargin(const MarginDouble& aMargin) : mMargin(aMargin) {}
+
+ private:
+ ~nsPaperMargin() = default;
+ const MarginDouble mMargin;
+};
+
+#endif
diff --git a/widget/nsPrimitiveHelpers.cpp b/widget/nsPrimitiveHelpers.cpp
new file mode 100644
index 0000000000..ea36f32d72
--- /dev/null
+++ b/widget/nsPrimitiveHelpers.cpp
@@ -0,0 +1,193 @@
+/* -*- 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/. */
+
+//
+// Part of the reason these routines are all in once place is so that as new
+// data flavors are added that are known to be one-byte or two-byte strings, or
+// even raw binary data, then we just have to go to one place to change how the
+// data moves into/out of the primitives and native line endings.
+//
+// If you add new flavors that have special consideration (binary data or
+// one-byte char* strings), please update all the helper classes in this file.
+//
+// For now, this is the assumption that we are making:
+// - text/plain is always a char*
+// - anything else is a char16_t*
+//
+
+#include "nsPrimitiveHelpers.h"
+
+#include "mozilla/UniquePtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+#include "nsLinebreakConverter.h"
+#include "nsReadableUtils.h"
+
+//
+// CreatePrimitiveForData
+//
+// Given some data and the flavor it corresponds to, creates the appropriate
+// nsISupports* wrapper for passing across IDL boundaries. Right now, everything
+// creates a two-byte |nsISupportsString|, except for "text/plain" and native
+// platform HTML (CF_HTML on win32)
+//
+void nsPrimitiveHelpers ::CreatePrimitiveForData(const nsACString& aFlavor,
+ const void* aDataBuff,
+ uint32_t aDataLen,
+ nsISupports** aPrimitive) {
+ if (!aPrimitive) return;
+
+ if (aFlavor.EqualsLiteral(kNativeHTMLMime) ||
+ aFlavor.EqualsLiteral(kRTFMime) ||
+ aFlavor.EqualsLiteral(kCustomTypesMime)) {
+ nsCOMPtr<nsISupportsCString> primitive =
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID);
+ if (primitive) {
+ const char* start = reinterpret_cast<const char*>(aDataBuff);
+ primitive->SetData(Substring(start, start + aDataLen));
+ NS_ADDREF(*aPrimitive = primitive);
+ }
+ } else {
+ nsCOMPtr<nsISupportsString> primitive =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ if (primitive) {
+ if (aDataLen % 2) {
+ auto buffer = mozilla::MakeUnique<char[]>(aDataLen + 1);
+ if (!MOZ_LIKELY(buffer)) return;
+
+ memcpy(buffer.get(), aDataBuff, aDataLen);
+ buffer[aDataLen] = 0;
+ const char16_t* start = reinterpret_cast<const char16_t*>(buffer.get());
+ // recall that length takes length as characters, not bytes
+ primitive->SetData(Substring(start, start + (aDataLen + 1) / 2));
+ } else {
+ const char16_t* start = reinterpret_cast<const char16_t*>(aDataBuff);
+ // recall that length takes length as characters, not bytes
+ primitive->SetData(Substring(start, start + (aDataLen / 2)));
+ }
+ NS_ADDREF(*aPrimitive = primitive);
+ }
+ }
+
+} // CreatePrimitiveForData
+
+//
+// CreatePrimitiveForCFHTML
+//
+// Platform specific CreatePrimitive, windows CF_HTML.
+//
+void nsPrimitiveHelpers ::CreatePrimitiveForCFHTML(const void* aDataBuff,
+ uint32_t* aDataLen,
+ nsISupports** aPrimitive) {
+ if (!aPrimitive) return;
+
+ nsCOMPtr<nsISupportsString> primitive =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ if (!primitive) return;
+
+ // We need to duplicate the input buffer, since the removal of linebreaks
+ // might reallocte it.
+ void* utf8 = moz_xmalloc(*aDataLen);
+ memcpy(utf8, aDataBuff, *aDataLen);
+ int32_t signedLen = static_cast<int32_t>(*aDataLen);
+ nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(true, &utf8, &signedLen);
+ *aDataLen = signedLen;
+
+ nsAutoString str(
+ NS_ConvertUTF8toUTF16(reinterpret_cast<const char*>(utf8), *aDataLen));
+ free(utf8);
+ *aDataLen = str.Length() * sizeof(char16_t);
+ primitive->SetData(str);
+ NS_ADDREF(*aPrimitive = primitive);
+}
+
+//
+// CreateDataFromPrimitive
+//
+// Given a nsISupports* primitive and the flavor it represents, creates a new
+// data buffer with the data in it. This data will be null terminated, but the
+// length parameter does not reflect that.
+//
+void nsPrimitiveHelpers::CreateDataFromPrimitive(const nsACString& aFlavor,
+ nsISupports* aPrimitive,
+ void** aDataBuff,
+ uint32_t* aDataLen) {
+ if (!aDataBuff) return;
+
+ *aDataBuff = nullptr;
+ *aDataLen = 0;
+
+ if (aFlavor.EqualsLiteral(kCustomTypesMime)) {
+ nsCOMPtr<nsISupportsCString> plainText(do_QueryInterface(aPrimitive));
+ if (plainText) {
+ nsAutoCString data;
+ plainText->GetData(data);
+ *aDataBuff = ToNewCString(data);
+ *aDataLen = data.Length() * sizeof(char);
+ }
+ } else {
+ nsCOMPtr<nsISupportsString> doubleByteText(do_QueryInterface(aPrimitive));
+ if (doubleByteText) {
+ nsAutoString data;
+ doubleByteText->GetData(data);
+ *aDataBuff = ToNewUnicode(data);
+ *aDataLen = data.Length() * sizeof(char16_t);
+ }
+ }
+}
+
+//
+// ConvertPlatformToDOMLinebreaks
+//
+// Given some data, convert from the platform linebreaks into the LF expected by
+// the DOM. This will attempt to convert the data in place, but the buffer may
+// still need to be reallocated regardless (disposing the old buffer is taken
+// care of internally, see the note below).
+//
+// NOTE: this assumes that it can use 'free' to dispose of the old buffer.
+//
+nsresult nsLinebreakHelpers ::ConvertPlatformToDOMLinebreaks(
+ bool aIsSingleByteChars, void** ioData, int32_t* ioLengthInBytes) {
+ NS_ASSERTION(ioData && *ioData && ioLengthInBytes, "Bad Params");
+ if (!(ioData && *ioData && ioLengthInBytes)) return NS_ERROR_INVALID_ARG;
+
+ nsresult retVal = NS_OK;
+
+ // RTF and CF_HTML on Windows are transfered as single-byte characters.
+ if (aIsSingleByteChars) {
+ char* buffAsChars = reinterpret_cast<char*>(*ioData);
+ char* oldBuffer = buffAsChars;
+ retVal = nsLinebreakConverter::ConvertLineBreaksInSitu(
+ &buffAsChars, nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent, *ioLengthInBytes,
+ ioLengthInBytes);
+ if (NS_SUCCEEDED(retVal)) {
+ if (buffAsChars != oldBuffer) // check if buffer was reallocated
+ free(oldBuffer);
+ *ioData = buffAsChars;
+ }
+ } else {
+ char16_t* buffAsUnichar = reinterpret_cast<char16_t*>(*ioData);
+ char16_t* oldBuffer = buffAsUnichar;
+ int32_t newLengthInChars;
+ retVal = nsLinebreakConverter::ConvertUnicharLineBreaksInSitu(
+ &buffAsUnichar, nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakContent,
+ *ioLengthInBytes / sizeof(char16_t), &newLengthInChars);
+ if (NS_SUCCEEDED(retVal)) {
+ if (buffAsUnichar != oldBuffer) // check if buffer was reallocated
+ free(oldBuffer);
+ *ioData = buffAsUnichar;
+ *ioLengthInBytes = newLengthInChars * sizeof(char16_t);
+ }
+ }
+
+ return retVal;
+
+} // ConvertPlatformToDOMLinebreaks
diff --git a/widget/nsPrimitiveHelpers.h b/widget/nsPrimitiveHelpers.h
new file mode 100644
index 0000000000..54a903b444
--- /dev/null
+++ b/widget/nsPrimitiveHelpers.h
@@ -0,0 +1,54 @@
+/* -*- 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/. */
+
+#ifndef nsPrimitiveHelpers_h___
+#define nsPrimitiveHelpers_h___
+
+#include "nsError.h"
+#include "nscore.h"
+#include "nsString.h"
+
+class nsISupports;
+
+class nsPrimitiveHelpers {
+ public:
+ // Given some data and the flavor it corresponds to, creates the appropriate
+ // nsISupports* wrapper for passing across IDL boundaries. The length
+ // parameter should not include the null if the data is null terminated.
+ static void CreatePrimitiveForData(const nsACString& aFlavor,
+ const void* aDataBuff, uint32_t aDataLen,
+ nsISupports** aPrimitive);
+
+ // A specific case of CreatePrimitive for windows CF_HTML handling in
+ // DataTransfer
+ static void CreatePrimitiveForCFHTML(const void* aDataBuff,
+ uint32_t* aDataLen,
+ nsISupports** aPrimitive);
+
+ // Given a nsISupports* primitive and the flavor it represents, creates a new
+ // data buffer with the data in it. This data will be null terminated, but the
+ // length parameter does not reflect that.
+ static void CreateDataFromPrimitive(const nsACString& aFlavor,
+ nsISupports* aPrimitive, void** aDataBuff,
+ uint32_t* aDataLen);
+
+}; // class nsPrimitiveHelpers
+
+class nsLinebreakHelpers {
+ public:
+ // Given some data, convert from the platform linebreaks into the LF expected
+ // by the DOM. This will attempt to convert the data in place, but the buffer
+ // may still need to be reallocated regardless (disposing the old buffer is
+ // taken care of internally, see the note below).
+ //
+ // NOTE: this assumes that it can use 'free' to dispose of the old buffer.
+ static nsresult ConvertPlatformToDOMLinebreaks(bool aIsSingleByteChars,
+ void** ioData,
+ int32_t* ioLengthInBytes);
+
+}; // class nsLinebreakHelpers
+
+#endif // nsPrimitiveHelpers_h___
diff --git a/widget/nsPrintSettingsImpl.cpp b/widget/nsPrintSettingsImpl.cpp
new file mode 100644
index 0000000000..45623af88a
--- /dev/null
+++ b/widget/nsPrintSettingsImpl.cpp
@@ -0,0 +1,941 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsPrintSettingsImpl.h"
+
+#include "prenv.h"
+#include "nsCoord.h"
+#include "nsPaper.h"
+#include "nsReadableUtils.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/RefPtr.h"
+
+using namespace mozilla;
+
+#define DEFAULT_MARGIN_WIDTH 0.5
+
+NS_IMPL_ISUPPORTS(nsPrintSettings, nsIPrintSettings)
+
+nsPrintSettings::nsPrintSettings() {
+ /* member initializers and constructor code */
+ int32_t marginWidth = NS_INCHES_TO_INT_TWIPS(DEFAULT_MARGIN_WIDTH);
+ mMargin.SizeTo(marginWidth, marginWidth, marginWidth, marginWidth);
+ mEdge.SizeTo(0, 0, 0, 0);
+ mUnwriteableMargin.SizeTo(0, 0, 0, 0);
+
+ mHeaderStrs[0].AssignLiteral("&T");
+ mHeaderStrs[2].AssignLiteral("&U");
+
+ mFooterStrs[0].AssignLiteral(
+ "&PT"); // Use &P (Page Num Only) or &PT (Page Num of Page Total)
+ mFooterStrs[2].AssignLiteral("&D");
+}
+
+void nsPrintSettings::InitWithInitializer(
+ const PrintSettingsInitializer& aSettings) {
+ const double kInchesPerPoint = 1.0 / 72.0;
+
+ SetPrinterName(aSettings.mPrinter);
+ SetPrintInColor(aSettings.mPrintInColor);
+ SetResolution(aSettings.mResolution);
+ SetNumCopies(aSettings.mNumCopies);
+ SetDuplex(aSettings.mDuplex);
+ // The paper ID used by nsPrintSettings is the non-localizable identifier
+ // exposed as "id" by the paper, not the potentially localized human-friendly
+ // "name", which could change, e.g. if the user changes their system locale.
+ SetPaperId(aSettings.mPaperInfo.mId);
+
+ // Set the paper sizes to match the unit.
+ SetPaperSizeUnit(aSettings.mPaperSizeUnit);
+ double sizeUnitsPerPoint =
+ aSettings.mPaperSizeUnit == kPaperSizeInches ? 1.0 / 72.0 : 25.4 / 72.0;
+ SetPaperWidth(aSettings.mPaperInfo.mSize.width * sizeUnitsPerPoint);
+ SetPaperHeight(aSettings.mPaperInfo.mSize.height * sizeUnitsPerPoint);
+
+ // If our initializer says that we're producing portrait-mode sheets of
+ // paper, then our page format must also be portrait-mode; unless we've got
+ // a pages-per-sheet value with orthogonal pages/sheets, in which case it's
+ // reversed.
+ const bool areSheetsOfPaperPortraitMode =
+ (aSettings.mSheetOrientation == kPortraitOrientation);
+ const bool arePagesPortraitMode =
+ (areSheetsOfPaperPortraitMode != HasOrthogonalPagesPerSheet());
+ SetOrientation(arePagesPortraitMode ? kPortraitOrientation
+ : kLandscapeOrientation);
+
+ if (aSettings.mPaperInfo.mUnwriteableMargin) {
+ const auto& margin = aSettings.mPaperInfo.mUnwriteableMargin.value();
+ // Margins are stored internally in TWIPS, but the setters expect inches.
+ SetUnwriteableMarginTop(margin.top * kInchesPerPoint);
+ SetUnwriteableMarginRight(margin.right * kInchesPerPoint);
+ SetUnwriteableMarginBottom(margin.bottom * kInchesPerPoint);
+ SetUnwriteableMarginLeft(margin.left * kInchesPerPoint);
+ }
+
+ // Set this last because other setters may overwrite its value.
+ SetIsInitializedFromPrinter(true);
+}
+
+nsPrintSettings::nsPrintSettings(const nsPrintSettings& aPS) { *this = aPS; }
+
+nsPrintSettings::~nsPrintSettings() = default;
+
+NS_IMETHODIMP nsPrintSettings::GetPrintReversed(bool* aPrintReversed) {
+ *aPrintReversed = mPrintReversed;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPrintReversed(bool aPrintReversed) {
+ mPrintReversed = aPrintReversed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPrintInColor(bool* aPrintInColor) {
+ *aPrintInColor = mPrintInColor;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPrintInColor(bool aPrintInColor) {
+ mPrintInColor = aPrintInColor;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetOrientation(int32_t* aOrientation) {
+ *aOrientation = mOrientation;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetOrientation(int32_t aOrientation) {
+ mOrientation = aOrientation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetResolution(int32_t* aResolution) {
+ NS_ENSURE_ARG_POINTER(aResolution);
+ *aResolution = mResolution;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetResolution(const int32_t aResolution) {
+ mResolution = aResolution;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetDuplex(int32_t* aDuplex) {
+ NS_ENSURE_ARG_POINTER(aDuplex);
+ *aDuplex = mDuplex;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetDuplex(const int32_t aDuplex) {
+ mDuplex = aDuplex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPrinterName(nsAString& aPrinter) {
+ aPrinter = mPrinter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetPrinterName(const nsAString& aPrinter) {
+ if (!mPrinter.Equals(aPrinter)) {
+ mIsInitedFromPrinter = false;
+ mIsInitedFromPrefs = false;
+ }
+
+ mPrinter.Assign(aPrinter);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetNumCopies(int32_t* aNumCopies) {
+ NS_ENSURE_ARG_POINTER(aNumCopies);
+ *aNumCopies = mNumCopies;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetNumCopies(int32_t aNumCopies) {
+ mNumCopies = aNumCopies;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetNumPagesPerSheet(int32_t* aNumPagesPerSheet) {
+ NS_ENSURE_ARG_POINTER(aNumPagesPerSheet);
+ *aNumPagesPerSheet = mNumPagesPerSheet;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetNumPagesPerSheet(int32_t aNumPagesPerSheet) {
+ mNumPagesPerSheet = aNumPagesPerSheet;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetOutputDestination(
+ OutputDestinationType* aDestination) {
+ *aDestination = mOutputDestination;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetOutputDestination(
+ OutputDestinationType aDestination) {
+ mOutputDestination = aDestination;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetOutputStream(nsIOutputStream* aStream) {
+ mOutputStream = aStream;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetOutputStream(nsIOutputStream** aStream) {
+ NS_IF_ADDREF(*aStream = mOutputStream.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetToFileName(nsAString& aToFileName) {
+ aToFileName = mToFileName;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetToFileName(const nsAString& aToFileName) {
+ mToFileName = aToFileName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetOutputFormat(int16_t* aOutputFormat) {
+ NS_ENSURE_ARG_POINTER(aOutputFormat);
+ *aOutputFormat = mOutputFormat;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetOutputFormat(int16_t aOutputFormat) {
+ mOutputFormat = aOutputFormat;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPrintPageDelay(int32_t* aPrintPageDelay) {
+ *aPrintPageDelay = mPrintPageDelay;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPrintPageDelay(int32_t aPrintPageDelay) {
+ mPrintPageDelay = aPrintPageDelay;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetIsInitializedFromPrinter(
+ bool* aIsInitializedFromPrinter) {
+ NS_ENSURE_ARG_POINTER(aIsInitializedFromPrinter);
+ *aIsInitializedFromPrinter = mIsInitedFromPrinter;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetIsInitializedFromPrinter(
+ bool aIsInitializedFromPrinter) {
+ mIsInitedFromPrinter = aIsInitializedFromPrinter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetIsInitializedFromPrefs(
+ bool* aInitializedFromPrefs) {
+ NS_ENSURE_ARG_POINTER(aInitializedFromPrefs);
+ *aInitializedFromPrefs = mIsInitedFromPrefs;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetIsInitializedFromPrefs(
+ bool aInitializedFromPrefs) {
+ mIsInitedFromPrefs = aInitializedFromPrefs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetMarginTop(double* aMarginTop) {
+ NS_ENSURE_ARG_POINTER(aMarginTop);
+ *aMarginTop = NS_TWIPS_TO_INCHES(mMargin.top);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetMarginTop(double aMarginTop) {
+ mMargin.top = NS_INCHES_TO_INT_TWIPS(float(aMarginTop));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetMarginLeft(double* aMarginLeft) {
+ NS_ENSURE_ARG_POINTER(aMarginLeft);
+ *aMarginLeft = NS_TWIPS_TO_INCHES(mMargin.left);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetMarginLeft(double aMarginLeft) {
+ mMargin.left = NS_INCHES_TO_INT_TWIPS(float(aMarginLeft));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetMarginBottom(double* aMarginBottom) {
+ NS_ENSURE_ARG_POINTER(aMarginBottom);
+ *aMarginBottom = NS_TWIPS_TO_INCHES(mMargin.bottom);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetMarginBottom(double aMarginBottom) {
+ mMargin.bottom = NS_INCHES_TO_INT_TWIPS(float(aMarginBottom));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetMarginRight(double* aMarginRight) {
+ NS_ENSURE_ARG_POINTER(aMarginRight);
+ *aMarginRight = NS_TWIPS_TO_INCHES(mMargin.right);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetMarginRight(double aMarginRight) {
+ mMargin.right = NS_INCHES_TO_INT_TWIPS(float(aMarginRight));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetEdgeTop(double* aEdgeTop) {
+ NS_ENSURE_ARG_POINTER(aEdgeTop);
+ *aEdgeTop = NS_TWIPS_TO_INCHES(mEdge.top);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetEdgeTop(double aEdgeTop) {
+ mEdge.top = NS_INCHES_TO_INT_TWIPS(float(aEdgeTop));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetEdgeLeft(double* aEdgeLeft) {
+ NS_ENSURE_ARG_POINTER(aEdgeLeft);
+ *aEdgeLeft = NS_TWIPS_TO_INCHES(mEdge.left);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetEdgeLeft(double aEdgeLeft) {
+ mEdge.left = NS_INCHES_TO_INT_TWIPS(float(aEdgeLeft));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetEdgeBottom(double* aEdgeBottom) {
+ NS_ENSURE_ARG_POINTER(aEdgeBottom);
+ *aEdgeBottom = NS_TWIPS_TO_INCHES(mEdge.bottom);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetEdgeBottom(double aEdgeBottom) {
+ mEdge.bottom = NS_INCHES_TO_INT_TWIPS(float(aEdgeBottom));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetEdgeRight(double* aEdgeRight) {
+ NS_ENSURE_ARG_POINTER(aEdgeRight);
+ *aEdgeRight = NS_TWIPS_TO_INCHES(mEdge.right);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetEdgeRight(double aEdgeRight) {
+ mEdge.right = NS_INCHES_TO_INT_TWIPS(float(aEdgeRight));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetUnwriteableMarginTop(
+ double* aUnwriteableMarginTop) {
+ NS_ENSURE_ARG_POINTER(aUnwriteableMarginTop);
+ *aUnwriteableMarginTop = NS_TWIPS_TO_INCHES(mUnwriteableMargin.top);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetUnwriteableMarginTop(
+ double aUnwriteableMarginTop) {
+ if (aUnwriteableMarginTop >= 0.0) {
+ mUnwriteableMargin.top = NS_INCHES_TO_INT_TWIPS(aUnwriteableMarginTop);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetUnwriteableMarginLeft(
+ double* aUnwriteableMarginLeft) {
+ NS_ENSURE_ARG_POINTER(aUnwriteableMarginLeft);
+ *aUnwriteableMarginLeft = NS_TWIPS_TO_INCHES(mUnwriteableMargin.left);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetUnwriteableMarginLeft(
+ double aUnwriteableMarginLeft) {
+ if (aUnwriteableMarginLeft >= 0.0) {
+ mUnwriteableMargin.left = NS_INCHES_TO_INT_TWIPS(aUnwriteableMarginLeft);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetUnwriteableMarginBottom(
+ double* aUnwriteableMarginBottom) {
+ NS_ENSURE_ARG_POINTER(aUnwriteableMarginBottom);
+ *aUnwriteableMarginBottom = NS_TWIPS_TO_INCHES(mUnwriteableMargin.bottom);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetUnwriteableMarginBottom(
+ double aUnwriteableMarginBottom) {
+ if (aUnwriteableMarginBottom >= 0.0) {
+ mUnwriteableMargin.bottom =
+ NS_INCHES_TO_INT_TWIPS(aUnwriteableMarginBottom);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetUnwriteableMarginRight(
+ double* aUnwriteableMarginRight) {
+ NS_ENSURE_ARG_POINTER(aUnwriteableMarginRight);
+ *aUnwriteableMarginRight = NS_TWIPS_TO_INCHES(mUnwriteableMargin.right);
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetUnwriteableMarginRight(
+ double aUnwriteableMarginRight) {
+ if (aUnwriteableMarginRight >= 0.0) {
+ mUnwriteableMargin.right = NS_INCHES_TO_INT_TWIPS(aUnwriteableMarginRight);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetScaling(double* aScaling) {
+ NS_ENSURE_ARG_POINTER(aScaling);
+ *aScaling = mScaling;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetScaling(double aScaling) {
+ mScaling = aScaling;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPrintBGColors(bool* aPrintBGColors) {
+ NS_ENSURE_ARG_POINTER(aPrintBGColors);
+ *aPrintBGColors = mPrintBGColors;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPrintBGColors(bool aPrintBGColors) {
+ mPrintBGColors = aPrintBGColors;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPrintBGImages(bool* aPrintBGImages) {
+ NS_ENSURE_ARG_POINTER(aPrintBGImages);
+ *aPrintBGImages = mPrintBGImages;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPrintBGImages(bool aPrintBGImages) {
+ mPrintBGImages = aPrintBGImages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetTitle(nsAString& aTitle) {
+ aTitle = mTitle;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetTitle(const nsAString& aTitle) {
+ mTitle = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetDocURL(nsAString& aDocURL) {
+ aDocURL = mURL;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetDocURL(const nsAString& aDocURL) {
+ mURL = aDocURL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetHeaderStrLeft(nsAString& aTitle) {
+ aTitle = mHeaderStrs[0];
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetHeaderStrLeft(const nsAString& aTitle) {
+ mHeaderStrs[0] = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetHeaderStrCenter(nsAString& aTitle) {
+ aTitle = mHeaderStrs[1];
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetHeaderStrCenter(const nsAString& aTitle) {
+ mHeaderStrs[1] = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetHeaderStrRight(nsAString& aTitle) {
+ aTitle = mHeaderStrs[2];
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetHeaderStrRight(const nsAString& aTitle) {
+ mHeaderStrs[2] = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetFooterStrLeft(nsAString& aTitle) {
+ aTitle = mFooterStrs[0];
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetFooterStrLeft(const nsAString& aTitle) {
+ mFooterStrs[0] = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetFooterStrCenter(nsAString& aTitle) {
+ aTitle = mFooterStrs[1];
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetFooterStrCenter(const nsAString& aTitle) {
+ mFooterStrs[1] = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetFooterStrRight(nsAString& aTitle) {
+ aTitle = mFooterStrs[2];
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetFooterStrRight(const nsAString& aTitle) {
+ mFooterStrs[2] = aTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPrintSilent(bool* aPrintSilent) {
+ NS_ENSURE_ARG_POINTER(aPrintSilent);
+ *aPrintSilent = mPrintSilent;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPrintSilent(bool aPrintSilent) {
+ mPrintSilent = aPrintSilent;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetShrinkToFit(bool* aShrinkToFit) {
+ NS_ENSURE_ARG_POINTER(aShrinkToFit);
+ *aShrinkToFit = mShrinkToFit;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetShrinkToFit(bool aShrinkToFit) {
+ mShrinkToFit = aShrinkToFit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetShowMarginGuides(bool* aShowMarginGuides) {
+ *aShowMarginGuides = mShowMarginGuides;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetShowMarginGuides(bool aShowMarginGuides) {
+ mShowMarginGuides = aShowMarginGuides;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetHonorPageRuleMargins(bool* aResult) {
+ *aResult = mHonorPageRuleMargins;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetHonorPageRuleMargins(bool aHonor) {
+ mHonorPageRuleMargins = aHonor;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetUsePageRuleSizeAsPaperSize(bool* aResult) {
+ *aResult = mUsePageRuleSizeAsPaperSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetUsePageRuleSizeAsPaperSize(bool aHonor) {
+ mUsePageRuleSizeAsPaperSize = aHonor;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetIgnoreUnwriteableMargins(bool* aResult) {
+ *aResult = mIgnoreUnwriteableMargins;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetIgnoreUnwriteableMargins(bool aIgnore) {
+ mIgnoreUnwriteableMargins = aIgnore;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPrintSelectionOnly(bool* aResult) {
+ *aResult = mPrintSelectionOnly;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::SetPrintSelectionOnly(bool aSelectionOnly) {
+ mPrintSelectionOnly = aSelectionOnly;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPaperId(nsAString& aPaperId) {
+ aPaperId = mPaperId;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPaperId(const nsAString& aPaperId) {
+ mPaperId = aPaperId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPaperWidth(double* aPaperWidth) {
+ NS_ENSURE_ARG_POINTER(aPaperWidth);
+ *aPaperWidth = mPaperWidth;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPaperWidth(double aPaperWidth) {
+ mPaperWidth = aPaperWidth;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPaperHeight(double* aPaperHeight) {
+ NS_ENSURE_ARG_POINTER(aPaperHeight);
+ *aPaperHeight = mPaperHeight;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPaperHeight(double aPaperHeight) {
+ mPaperHeight = aPaperHeight;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettings::GetPaperSizeUnit(int16_t* aPaperSizeUnit) {
+ NS_ENSURE_ARG_POINTER(aPaperSizeUnit);
+ *aPaperSizeUnit = mPaperSizeUnit;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettings::SetPaperSizeUnit(int16_t aPaperSizeUnit) {
+ mPaperSizeUnit = aPaperSizeUnit;
+ return NS_OK;
+}
+
+/** ---------------------------------------------------
+ * See documentation in nsPrintSettingsService.h
+ * @update 6/21/00 dwc
+ * @update 1/12/01 rods
+ */
+NS_IMETHODIMP
+nsPrintSettings::SetMarginInTwips(nsIntMargin& aMargin) {
+ mMargin = aMargin;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettings::SetEdgeInTwips(nsIntMargin& aEdge) {
+ mEdge = aEdge;
+ return NS_OK;
+}
+
+// NOTE: Any subclass implementation of this function should make sure
+// to check for negative margin values in aUnwriteableMargin (which
+// would indicate that we should use the system default unwriteable margin.)
+NS_IMETHODIMP
+nsPrintSettings::SetUnwriteableMarginInTwips(nsIntMargin& aUnwriteableMargin) {
+ if (aUnwriteableMargin.top >= 0) {
+ mUnwriteableMargin.top = aUnwriteableMargin.top;
+ }
+ if (aUnwriteableMargin.left >= 0) {
+ mUnwriteableMargin.left = aUnwriteableMargin.left;
+ }
+ if (aUnwriteableMargin.bottom >= 0) {
+ mUnwriteableMargin.bottom = aUnwriteableMargin.bottom;
+ }
+ if (aUnwriteableMargin.right >= 0) {
+ mUnwriteableMargin.right = aUnwriteableMargin.right;
+ }
+ return NS_OK;
+}
+
+nsIntMargin nsPrintSettings::GetMarginInTwips() { return mMargin; }
+
+nsIntMargin nsPrintSettings::GetEdgeInTwips() { return mEdge; }
+
+nsIntMargin nsPrintSettings::GetUnwriteableMarginInTwips() {
+ return mUnwriteableMargin;
+}
+
+/** ---------------------------------------------------
+ * See documentation in nsPrintSettingsService.h
+ */
+NS_IMETHODIMP
+nsPrintSettings::GetEffectivePageSize(double* aWidth, double* aHeight) {
+ if (mPaperSizeUnit == kPaperSizeInches) {
+ *aWidth = NS_INCHES_TO_TWIPS(float(mPaperWidth));
+ *aHeight = NS_INCHES_TO_TWIPS(float(mPaperHeight));
+ } else {
+ MOZ_ASSERT(mPaperSizeUnit == kPaperSizeMillimeters,
+ "unexpected paper size unit");
+ *aWidth = NS_MILLIMETERS_TO_TWIPS(float(mPaperWidth));
+ *aHeight = NS_MILLIMETERS_TO_TWIPS(float(mPaperHeight));
+ }
+ if (kLandscapeOrientation == mOrientation) {
+ double temp = *aWidth;
+ *aWidth = *aHeight;
+ *aHeight = temp;
+ }
+ return NS_OK;
+}
+
+bool nsPrintSettings::HasOrthogonalPagesPerSheet() {
+ return mNumPagesPerSheet == 2 || mNumPagesPerSheet == 6;
+}
+
+void nsPrintSettings::GetEffectiveSheetSize(double* aWidth, double* aHeight) {
+ mozilla::DebugOnly<nsresult> rv = GetEffectivePageSize(aWidth, aHeight);
+
+ // Our GetEffectivePageSize impls only return NS_OK, so this should hold:
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Uh oh, GetEffectivePageSize failed");
+
+ if (HasOrthogonalPagesPerSheet()) {
+ std::swap(*aWidth, *aHeight);
+ }
+}
+
+int32_t nsPrintSettings::GetSheetOrientation() {
+ if (HasOrthogonalPagesPerSheet()) {
+ // Sheet orientation is rotated with respect to the page orientation.
+ return kLandscapeOrientation == mOrientation ? kPortraitOrientation
+ : kLandscapeOrientation;
+ }
+
+ // Sheet orientation is the same as the page orientation.
+ return mOrientation;
+}
+
+NS_IMETHODIMP
+nsPrintSettings::SetPageRanges(const nsTArray<int32_t>& aPages) {
+ // Needs to be a set of (start, end) pairs.
+ if (aPages.Length() % 2 != 0) {
+ return NS_ERROR_FAILURE;
+ }
+ mPageRanges = aPages.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettings::GetPageRanges(nsTArray<int32_t>& aPages) {
+ aPages = mPageRanges.Clone();
+ return NS_OK;
+}
+
+bool nsIPrintSettings::IsPageSkipped(int32_t aPageNum,
+ const nsTArray<int32_t>& aRanges) {
+ MOZ_RELEASE_ASSERT(aRanges.Length() % 2 == 0);
+ if (aRanges.IsEmpty()) {
+ return false;
+ }
+ for (size_t i = 0; i < aRanges.Length(); i += 2) {
+ if (aRanges[i] <= aPageNum && aPageNum <= aRanges[i + 1]) {
+ // The page is included in this piece of the custom range,
+ // so it's not skipped.
+ return false;
+ }
+ }
+ return true;
+}
+
+nsresult nsPrintSettings::_Clone(nsIPrintSettings** _retval) {
+ RefPtr<nsPrintSettings> printSettings = new nsPrintSettings(*this);
+ printSettings.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettings::Clone(nsIPrintSettings** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ return _Clone(_retval);
+}
+
+nsresult nsPrintSettings::_Assign(nsIPrintSettings* aPS) {
+ nsPrintSettings* ps = static_cast<nsPrintSettings*>(aPS);
+ *this = *ps;
+ return NS_OK;
+}
+
+nsresult nsPrintSettings::EquivalentTo(nsIPrintSettings* aPrintSettings,
+ bool* _retval) {
+ MOZ_ASSERT(aPrintSettings);
+ *_retval = false;
+ auto* other = static_cast<nsPrintSettings*>(aPrintSettings);
+ if (GetMarginInTwips() != aPrintSettings->GetMarginInTwips()) {
+ return NS_OK;
+ }
+ if (GetEdgeInTwips() != aPrintSettings->GetEdgeInTwips()) {
+ return NS_OK;
+ }
+ if (GetUnwriteableMarginInTwips() !=
+ aPrintSettings->GetUnwriteableMarginInTwips()) {
+ return NS_OK;
+ }
+ nsTArray<int32_t> ourPageRanges, otherPageRanges;
+ if (NS_FAILED(GetPageRanges(ourPageRanges)) ||
+ NS_FAILED(aPrintSettings->GetPageRanges(otherPageRanges)) ||
+ ourPageRanges != otherPageRanges) {
+ return NS_OK;
+ }
+ double ourScaling, otherScaling;
+ if (NS_FAILED(GetScaling(&ourScaling)) ||
+ NS_FAILED(aPrintSettings->GetScaling(&otherScaling)) ||
+ ourScaling != otherScaling) {
+ return NS_OK;
+ }
+ if (GetPrintBGColors() != aPrintSettings->GetPrintBGColors()) {
+ return NS_OK;
+ }
+ if (GetPrintBGImages() != aPrintSettings->GetPrintBGImages()) {
+ return NS_OK;
+ }
+ if (GetPrintSelectionOnly() != aPrintSettings->GetPrintSelectionOnly()) {
+ return NS_OK;
+ }
+ if (GetShrinkToFit() != aPrintSettings->GetShrinkToFit()) {
+ return NS_OK;
+ }
+ if (GetShowMarginGuides() != aPrintSettings->GetShowMarginGuides()) {
+ return NS_OK;
+ }
+ if (GetHonorPageRuleMargins() != aPrintSettings->GetHonorPageRuleMargins()) {
+ return NS_OK;
+ }
+ if (GetUsePageRuleSizeAsPaperSize() !=
+ aPrintSettings->GetUsePageRuleSizeAsPaperSize()) {
+ return NS_OK;
+ }
+ if (GetIgnoreUnwriteableMargins() !=
+ aPrintSettings->GetIgnoreUnwriteableMargins()) {
+ return NS_OK;
+ }
+ nsAutoString ourTitle, otherTitle;
+ if (NS_FAILED(GetTitle(ourTitle)) ||
+ NS_FAILED(aPrintSettings->GetTitle(otherTitle)) ||
+ ourTitle != otherTitle) {
+ return NS_OK;
+ }
+ nsAutoString ourUrl, otherUrl;
+ if (NS_FAILED(GetDocURL(ourUrl)) ||
+ NS_FAILED(aPrintSettings->GetDocURL(otherUrl)) || ourUrl != otherUrl) {
+ return NS_OK;
+ }
+ if (!mozilla::ArrayEqual(mHeaderStrs, other->mHeaderStrs) ||
+ !mozilla::ArrayEqual(mFooterStrs, other->mFooterStrs)) {
+ return NS_OK;
+ }
+ nsAutoString ourPaperId, otherPaperId;
+ if (NS_FAILED(GetPaperId(ourPaperId)) ||
+ NS_FAILED(aPrintSettings->GetPaperId(otherPaperId)) ||
+ ourPaperId != otherPaperId) {
+ return NS_OK;
+ }
+ double ourWidth, ourHeight, otherWidth, otherHeight;
+ if (NS_FAILED(GetEffectivePageSize(&ourWidth, &ourHeight)) ||
+ NS_FAILED(other->GetEffectivePageSize(&otherWidth, &otherHeight)) ||
+ std::abs(ourWidth - otherWidth) >= 1 ||
+ std::abs(ourHeight - otherHeight) >= 1) {
+ return NS_OK;
+ }
+ int32_t ourOrientation, otherOrientation;
+ if (NS_FAILED(GetOrientation(&ourOrientation)) ||
+ NS_FAILED(aPrintSettings->GetOrientation(&otherOrientation)) ||
+ ourOrientation != otherOrientation) {
+ return NS_OK;
+ }
+ int32_t ourResolution, otherResolution;
+ if (NS_FAILED(GetResolution(&ourResolution)) ||
+ NS_FAILED(aPrintSettings->GetResolution(&otherResolution)) ||
+ ourResolution != otherResolution) {
+ return NS_OK;
+ }
+ int32_t ourNumPagesPerSheet, otherNumPagesPerSheet;
+ if (NS_FAILED(GetNumPagesPerSheet(&ourNumPagesPerSheet)) ||
+ NS_FAILED(aPrintSettings->GetNumPagesPerSheet(&otherNumPagesPerSheet)) ||
+ ourNumPagesPerSheet != otherNumPagesPerSheet) {
+ return NS_OK;
+ }
+
+ *_retval = true;
+ return NS_OK;
+}
+
+mozilla::PrintSettingsInitializer nsPrintSettings::GetSettingsInitializer() {
+ mozilla::PrintSettingsInitializer settingsInitializer;
+ settingsInitializer.mPrinter.Assign(mPrinter);
+ settingsInitializer.mPaperInfo.mId = mPaperId;
+
+ double pointsPerSizeUnit =
+ mPaperSizeUnit == kPaperSizeInches ? 72.0 : 72.0 / 25.4;
+ settingsInitializer.mPaperInfo.mSize = {mPaperWidth * pointsPerSizeUnit,
+ mPaperHeight * pointsPerSizeUnit};
+
+ // Unwritable margins are stored in TWIPS here and points in PaperInfo.
+ settingsInitializer.mPaperInfo.mUnwriteableMargin =
+ Some(mozilla::gfx::MarginDouble(
+ mUnwriteableMargin.top / 20.0, mUnwriteableMargin.right / 20.0,
+ mUnwriteableMargin.bottom / 20.0, mUnwriteableMargin.left / 20.0));
+
+ settingsInitializer.mPrintInColor = mPrintInColor;
+ settingsInitializer.mResolution = mResolution;
+ settingsInitializer.mSheetOrientation = GetSheetOrientation();
+ settingsInitializer.mNumCopies = mNumCopies;
+ settingsInitializer.mDuplex = mDuplex;
+ RefPtr<nsIPrintSettings> settingsToInitialize;
+ MOZ_ALWAYS_SUCCEEDS(Clone(getter_AddRefs(settingsToInitialize)));
+ settingsInitializer.mPrintSettings =
+ new nsMainThreadPtrHolder<nsPrintSettings>(
+ "PrintSettingsInitializer::mPrintSettings",
+ settingsToInitialize.forget().downcast<nsPrintSettings>());
+ return settingsInitializer;
+}
+
+NS_IMETHODIMP
+nsPrintSettings::Assign(nsIPrintSettings* aPS) {
+ NS_ENSURE_ARG(aPS);
+ return _Assign(aPS);
+}
+
+//-------------------------------------------
+nsPrintSettings& nsPrintSettings::operator=(const nsPrintSettings& rhs) {
+ if (this == &rhs) {
+ return *this;
+ }
+
+ mPageRanges = rhs.mPageRanges.Clone();
+ mMargin = rhs.mMargin;
+ mEdge = rhs.mEdge;
+ mUnwriteableMargin = rhs.mUnwriteableMargin;
+ mScaling = rhs.mScaling;
+ mPrintBGColors = rhs.mPrintBGColors;
+ mPrintBGImages = rhs.mPrintBGImages;
+ mTitle = rhs.mTitle;
+ mURL = rhs.mURL;
+ mPrintSilent = rhs.mPrintSilent;
+ mShrinkToFit = rhs.mShrinkToFit;
+ mShowMarginGuides = rhs.mShowMarginGuides;
+ mHonorPageRuleMargins = rhs.mHonorPageRuleMargins;
+ mUsePageRuleSizeAsPaperSize = rhs.mUsePageRuleSizeAsPaperSize;
+ mIgnoreUnwriteableMargins = rhs.mIgnoreUnwriteableMargins;
+ mPrintSelectionOnly = rhs.mPrintSelectionOnly;
+ mPaperId = rhs.mPaperId;
+ mPaperWidth = rhs.mPaperWidth;
+ mPaperHeight = rhs.mPaperHeight;
+ mPaperSizeUnit = rhs.mPaperSizeUnit;
+ mPrintReversed = rhs.mPrintReversed;
+ mPrintInColor = rhs.mPrintInColor;
+ mOrientation = rhs.mOrientation;
+ mResolution = rhs.mResolution;
+ mDuplex = rhs.mDuplex;
+ mNumCopies = rhs.mNumCopies;
+ mNumPagesPerSheet = rhs.mNumPagesPerSheet;
+ mPrinter = rhs.mPrinter;
+ mOutputDestination = rhs.mOutputDestination;
+ mOutputStream = rhs.mOutputStream;
+ mToFileName = rhs.mToFileName;
+ mOutputFormat = rhs.mOutputFormat;
+ mIsInitedFromPrinter = rhs.mIsInitedFromPrinter;
+ mIsInitedFromPrefs = rhs.mIsInitedFromPrefs;
+ mPrintPageDelay = rhs.mPrintPageDelay;
+
+ for (int32_t i = 0; i < NUM_HEAD_FOOT; i++) {
+ mHeaderStrs[i] = rhs.mHeaderStrs[i];
+ mFooterStrs[i] = rhs.mFooterStrs[i];
+ }
+
+ return *this;
+}
+
+void nsPrintSettings::SetDefaultFileName() {
+ nsAutoString filename;
+ nsresult rv = GetToFileName(filename);
+ if (NS_FAILED(rv) || filename.IsEmpty()) {
+ const char* path = PR_GetEnv("PWD");
+ if (!path) {
+ path = PR_GetEnv("HOME");
+ }
+
+ if (path) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(path), filename);
+ filename.AppendLiteral("/mozilla.pdf");
+ } else {
+ filename.AssignLiteral("mozilla.pdf");
+ }
+
+ SetToFileName(filename);
+ }
+}
diff --git a/widget/nsPrintSettingsImpl.h b/widget/nsPrintSettingsImpl.h
new file mode 100644
index 0000000000..f090cc5841
--- /dev/null
+++ b/widget/nsPrintSettingsImpl.h
@@ -0,0 +1,134 @@
+/* -*- Mode: IDL; tab-width: 4; 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/. */
+
+#ifndef nsPrintSettingsImpl_h__
+#define nsPrintSettingsImpl_h__
+
+#include "nsIPrintSettings.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsMargin.h"
+#include "nsPaper.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+
+#define NUM_HEAD_FOOT 3
+
+//*****************************************************************************
+//*** nsPrintSettings
+//*****************************************************************************
+
+class nsPrintSettings;
+
+namespace mozilla {
+
+/**
+ * A struct that can be used off the main thread to collect printer-specific
+ * info that can be used to initialized a default nsIPrintSettings object.
+ */
+struct PrintSettingsInitializer {
+ nsString mPrinter;
+ PaperInfo mPaperInfo;
+ int16_t mPaperSizeUnit = nsIPrintSettings::kPaperSizeInches;
+ // If we fail to obtain printer capabilities, being given the option to print
+ // in color to your monochrome printer is a lot less annoying than not being
+ // given the option to print in color to your color printer.
+ bool mPrintInColor = true;
+ int mResolution = 0;
+ int mSheetOrientation = nsIPrintSettings::kPortraitOrientation;
+ int mNumCopies = 1;
+ int mDuplex = nsIPrintSettings::kDuplexNone;
+
+ // This is to hold a reference to a newly cloned settings object that will
+ // then be initialized by the other values in the initializer that may have
+ // been changed on a background thread.
+ nsMainThreadPtrHandle<nsPrintSettings> mPrintSettings;
+
+#ifdef XP_WIN
+ CopyableTArray<uint8_t> mDevmodeWStorage;
+#endif
+};
+
+} // namespace mozilla
+
+class nsPrintSettings : public nsIPrintSettings {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRINTSETTINGS
+ using PrintSettingsInitializer = mozilla::PrintSettingsInitializer;
+
+ nsPrintSettings();
+ nsPrintSettings(const nsPrintSettings& aPS);
+
+ /**
+ * Initialize relevant members from the PrintSettingsInitializer.
+ * This is specifically not a constructor so that we can ensure that the
+ * relevant setters are dynamically dispatched to derived classes.
+ */
+ virtual void InitWithInitializer(const PrintSettingsInitializer& aSettings);
+
+ nsPrintSettings& operator=(const nsPrintSettings& rhs);
+
+ // Sets a default file name for the print settings.
+ void SetDefaultFileName();
+
+ protected:
+ virtual ~nsPrintSettings();
+
+ // May be implemented by the platform-specific derived class
+ virtual nsresult _Clone(nsIPrintSettings** _retval);
+ virtual nsresult _Assign(nsIPrintSettings* aPS);
+
+ // Members
+ nsWeakPtr mSession; // Should never be touched by Clone or Assign
+
+ // mMargin, mEdge, and mUnwriteableMargin are stored in twips
+ nsIntMargin mMargin;
+ nsIntMargin mEdge;
+ nsIntMargin mUnwriteableMargin;
+
+ nsTArray<int32_t> mPageRanges;
+
+ double mScaling = 1.0;
+ bool mPrintBGColors = false;
+ bool mPrintBGImages = false;
+
+ bool mPrintSilent = false;
+ bool mShrinkToFit = true;
+ bool mShowMarginGuides = false;
+ bool mHonorPageRuleMargins = true;
+ bool mUsePageRuleSizeAsPaperSize = false;
+ bool mIgnoreUnwriteableMargins = false;
+ bool mPrintSelectionOnly = false;
+
+ int32_t mPrintPageDelay = 50; // XXX Do we really want this?
+
+ nsString mTitle;
+ nsString mURL;
+ nsString mHeaderStrs[NUM_HEAD_FOOT];
+ nsString mFooterStrs[NUM_HEAD_FOOT];
+
+ nsString mPaperId;
+ double mPaperWidth = 8.5;
+ double mPaperHeight = 11.0;
+ int16_t mPaperSizeUnit = kPaperSizeInches;
+
+ bool mPrintReversed = false;
+ bool mPrintInColor = true;
+ int32_t mOrientation = kPortraitOrientation;
+ int32_t mResolution = 0;
+ int32_t mDuplex = kDuplexNone;
+ int32_t mNumCopies = 1;
+ int32_t mNumPagesPerSheet = 1;
+ int16_t mOutputFormat = kOutputFormatNative;
+ OutputDestinationType mOutputDestination = kOutputDestinationPrinter;
+ nsString mPrinter;
+ nsString mToFileName;
+ nsCOMPtr<nsIOutputStream> mOutputStream;
+ bool mIsInitedFromPrinter = false;
+ bool mIsInitedFromPrefs = false;
+};
+
+#endif /* nsPrintSettings_h__ */
diff --git a/widget/nsPrintSettingsService.cpp b/widget/nsPrintSettingsService.cpp
new file mode 100644
index 0000000000..5f2dcd7c0a
--- /dev/null
+++ b/widget/nsPrintSettingsService.cpp
@@ -0,0 +1,1090 @@
+/* -*- 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 "nsPrintSettingsService.h"
+
+#include "mozilla/embedding/PPrintingTypes.h"
+#include "mozilla/layout/RemotePrintJobChild.h"
+#include "mozilla/RefPtr.h"
+#include "nsCoord.h"
+#include "nsIPrinterList.h"
+#include "nsReadableUtils.h"
+#include "nsPrintSettingsImpl.h"
+#include "nsServiceManagerUtils.h"
+#include "nsSize.h"
+
+#include "nsArray.h"
+#include "nsXPCOM.h"
+#include "nsXULAppAPI.h"
+
+#include "nsIStringEnumerator.h"
+#include "stdlib.h"
+#include "mozilla/StaticPrefs_print.h"
+#include "mozilla/Preferences.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla;
+using namespace mozilla::embedding;
+
+typedef mozilla::layout::RemotePrintJobChild RemotePrintJobChild;
+
+NS_IMPL_ISUPPORTS(nsPrintSettingsService, nsIPrintSettingsService)
+
+// Pref Constants
+static const char kMarginTop[] = "print_margin_top";
+static const char kMarginLeft[] = "print_margin_left";
+static const char kMarginBottom[] = "print_margin_bottom";
+static const char kMarginRight[] = "print_margin_right";
+static const char kEdgeTop[] = "print_edge_top";
+static const char kEdgeLeft[] = "print_edge_left";
+static const char kEdgeBottom[] = "print_edge_bottom";
+static const char kEdgeRight[] = "print_edge_right";
+
+static const char kIgnoreUnwriteableMargins[] =
+ "print_ignore_unwriteable_margins";
+
+static const char kUnwriteableMarginTopTwips[] =
+ "print_unwriteable_margin_top_twips";
+static const char kUnwriteableMarginLeftTwips[] =
+ "print_unwriteable_margin_left_twips";
+static const char kUnwriteableMarginBottomTwips[] =
+ "print_unwriteable_margin_bottom_twips";
+static const char kUnwriteableMarginRightTwips[] =
+ "print_unwriteable_margin_right_twips";
+
+// These are legacy versions of the above UnwriteableMargin prefs. The new ones,
+// which are in twips, were introduced to more accurately record the values.
+static const char kUnwriteableMarginTop[] = "print_unwriteable_margin_top";
+static const char kUnwriteableMarginLeft[] = "print_unwriteable_margin_left";
+static const char kUnwriteableMarginBottom[] =
+ "print_unwriteable_margin_bottom";
+static const char kUnwriteableMarginRight[] = "print_unwriteable_margin_right";
+
+// Prefs for Print Options
+static const char kPrintHeaderStrLeft[] = "print_headerleft";
+static const char kPrintHeaderStrCenter[] = "print_headercenter";
+static const char kPrintHeaderStrRight[] = "print_headerright";
+static const char kPrintFooterStrLeft[] = "print_footerleft";
+static const char kPrintFooterStrCenter[] = "print_footercenter";
+static const char kPrintFooterStrRight[] = "print_footerright";
+
+// Additional Prefs
+static const char kPrintReversed[] = "print_reversed";
+static const char kPrintInColor[] = "print_in_color";
+static const char kPrintPaperId[] = "print_paper_id";
+static const char kPrintPaperSizeUnit[] = "print_paper_size_unit";
+static const char kPrintPaperWidth[] = "print_paper_width";
+static const char kPrintPaperHeight[] = "print_paper_height";
+static const char kPrintOrientation[] = "print_orientation";
+static const char kPrinterName[] = "print_printer";
+static const char kPrintToFile[] = "print_to_file";
+static const char kPrintToFileName[] = "print_to_filename";
+static const char kPrintPageDelay[] = "print_page_delay";
+static const char kPrintBGColors[] = "print_bgcolor";
+static const char kPrintBGImages[] = "print_bgimages";
+static const char kPrintShrinkToFit[] = "print_shrink_to_fit";
+static const char kPrintScaling[] = "print_scaling";
+static const char kPrintDuplex[] = "print_duplex";
+
+static const char kJustLeft[] = "left";
+static const char kJustCenter[] = "center";
+static const char kJustRight[] = "right";
+
+#define NS_PRINTER_LIST_CONTRACTID "@mozilla.org/gfx/printerlist;1"
+
+nsresult nsPrintSettingsService::Init() { return NS_OK; }
+
+NS_IMETHODIMP
+nsPrintSettingsService::SerializeToPrintData(nsIPrintSettings* aSettings,
+ PrintData* data) {
+ aSettings->GetPageRanges(data->pageRanges());
+
+ aSettings->GetEdgeTop(&data->edgeTop());
+ aSettings->GetEdgeLeft(&data->edgeLeft());
+ aSettings->GetEdgeBottom(&data->edgeBottom());
+ aSettings->GetEdgeRight(&data->edgeRight());
+
+ aSettings->GetMarginTop(&data->marginTop());
+ aSettings->GetMarginLeft(&data->marginLeft());
+ aSettings->GetMarginBottom(&data->marginBottom());
+ aSettings->GetMarginRight(&data->marginRight());
+ aSettings->GetUnwriteableMarginTop(&data->unwriteableMarginTop());
+ aSettings->GetUnwriteableMarginLeft(&data->unwriteableMarginLeft());
+ aSettings->GetUnwriteableMarginBottom(&data->unwriteableMarginBottom());
+ aSettings->GetUnwriteableMarginRight(&data->unwriteableMarginRight());
+
+ aSettings->GetScaling(&data->scaling());
+
+ data->printBGColors() = aSettings->GetPrintBGColors();
+ data->printBGImages() = aSettings->GetPrintBGImages();
+
+ data->ignoreUnwriteableMargins() = aSettings->GetIgnoreUnwriteableMargins();
+ data->honorPageRuleMargins() = aSettings->GetHonorPageRuleMargins();
+ data->usePageRuleSizeAsPaperSize() =
+ aSettings->GetUsePageRuleSizeAsPaperSize();
+ data->showMarginGuides() = aSettings->GetShowMarginGuides();
+ data->printSelectionOnly() = aSettings->GetPrintSelectionOnly();
+
+ aSettings->GetTitle(data->title());
+ aSettings->GetDocURL(data->docURL());
+
+ aSettings->GetHeaderStrLeft(data->headerStrLeft());
+ aSettings->GetHeaderStrCenter(data->headerStrCenter());
+ aSettings->GetHeaderStrRight(data->headerStrRight());
+
+ aSettings->GetFooterStrLeft(data->footerStrLeft());
+ aSettings->GetFooterStrCenter(data->footerStrCenter());
+ aSettings->GetFooterStrRight(data->footerStrRight());
+
+ aSettings->GetPrintSilent(&data->printSilent());
+ aSettings->GetShrinkToFit(&data->shrinkToFit());
+
+ aSettings->GetPaperId(data->paperId());
+ aSettings->GetPaperWidth(&data->paperWidth());
+ aSettings->GetPaperHeight(&data->paperHeight());
+ aSettings->GetPaperSizeUnit(&data->paperSizeUnit());
+
+ aSettings->GetPrintReversed(&data->printReversed());
+ aSettings->GetPrintInColor(&data->printInColor());
+ aSettings->GetOrientation(&data->orientation());
+
+ aSettings->GetNumCopies(&data->numCopies());
+ aSettings->GetNumPagesPerSheet(&data->numPagesPerSheet());
+
+ data->outputDestination() = aSettings->GetOutputDestination();
+
+ data->outputFormat() = aSettings->GetOutputFormat();
+ data->printPageDelay() = aSettings->GetPrintPageDelay();
+ data->resolution() = aSettings->GetResolution();
+ data->duplex() = aSettings->GetDuplex();
+
+ aSettings->GetIsInitializedFromPrinter(&data->isInitializedFromPrinter());
+ aSettings->GetIsInitializedFromPrefs(&data->isInitializedFromPrefs());
+
+ // Initialize the platform-specific values that don't
+ // default-initialize, so that we don't send uninitialized data over
+ // IPC (which leads to valgrind warnings, and, for bools, fatal
+ // assertions).
+ // data->driverName() default-initializes
+ // data->deviceName() default-initializes
+ // data->GTKPrintSettings() default-initializes
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsService::DeserializeToPrintSettings(const PrintData& data,
+ nsIPrintSettings* settings) {
+ settings->SetPageRanges(data.pageRanges());
+
+ settings->SetEdgeTop(data.edgeTop());
+ settings->SetEdgeLeft(data.edgeLeft());
+ settings->SetEdgeBottom(data.edgeBottom());
+ settings->SetEdgeRight(data.edgeRight());
+
+ settings->SetMarginTop(data.marginTop());
+ settings->SetMarginLeft(data.marginLeft());
+ settings->SetMarginBottom(data.marginBottom());
+ settings->SetMarginRight(data.marginRight());
+ settings->SetUnwriteableMarginTop(data.unwriteableMarginTop());
+ settings->SetUnwriteableMarginLeft(data.unwriteableMarginLeft());
+ settings->SetUnwriteableMarginBottom(data.unwriteableMarginBottom());
+ settings->SetUnwriteableMarginRight(data.unwriteableMarginRight());
+
+ settings->SetScaling(data.scaling());
+
+ settings->SetPrintBGColors(data.printBGColors());
+ settings->SetPrintBGImages(data.printBGImages());
+ settings->SetHonorPageRuleMargins(data.honorPageRuleMargins());
+ settings->SetUsePageRuleSizeAsPaperSize(data.usePageRuleSizeAsPaperSize());
+ settings->SetIgnoreUnwriteableMargins(data.ignoreUnwriteableMargins());
+ settings->SetShowMarginGuides(data.showMarginGuides());
+ settings->SetPrintSelectionOnly(data.printSelectionOnly());
+
+ settings->SetTitle(data.title());
+ settings->SetDocURL(data.docURL());
+
+ // Header strings...
+ settings->SetHeaderStrLeft(data.headerStrLeft());
+ settings->SetHeaderStrCenter(data.headerStrCenter());
+ settings->SetHeaderStrRight(data.headerStrRight());
+
+ // Footer strings...
+ settings->SetFooterStrLeft(data.footerStrLeft());
+ settings->SetFooterStrCenter(data.footerStrCenter());
+ settings->SetFooterStrRight(data.footerStrRight());
+
+ settings->SetPrintSilent(data.printSilent());
+ settings->SetShrinkToFit(data.shrinkToFit());
+
+ settings->SetPaperId(data.paperId());
+
+ settings->SetPaperWidth(data.paperWidth());
+ settings->SetPaperHeight(data.paperHeight());
+ settings->SetPaperSizeUnit(data.paperSizeUnit());
+
+ settings->SetPrintReversed(data.printReversed());
+ settings->SetPrintInColor(data.printInColor());
+ settings->SetOrientation(data.orientation());
+
+ settings->SetNumCopies(data.numCopies());
+ settings->SetNumPagesPerSheet(data.numPagesPerSheet());
+
+ settings->SetOutputDestination(
+ nsIPrintSettings::OutputDestinationType(data.outputDestination()));
+ // Output stream intentionally unset, child processes shouldn't care about it.
+
+ settings->SetOutputFormat(data.outputFormat());
+ settings->SetPrintPageDelay(data.printPageDelay());
+ settings->SetResolution(data.resolution());
+ settings->SetDuplex(data.duplex());
+ settings->SetIsInitializedFromPrinter(data.isInitializedFromPrinter());
+ settings->SetIsInitializedFromPrefs(data.isInitializedFromPrefs());
+
+ return NS_OK;
+}
+
+/** ---------------------------------------------------
+ * Helper function - Creates the "prefix" for the pref
+ * It is either "print."
+ * or "print.printer_<print name>."
+ */
+const char* nsPrintSettingsService::GetPrefName(const char* aPrefName,
+ const nsAString& aPrinterName) {
+ if (!aPrefName || !*aPrefName) {
+ NS_ERROR("Must have a valid pref name!");
+ return aPrefName;
+ }
+
+ mPrefName.AssignLiteral("print.");
+
+ if (aPrinterName.Length()) {
+ mPrefName.AppendLiteral("printer_");
+ AppendUTF16toUTF8(aPrinterName, mPrefName);
+ mPrefName.Append('.');
+ }
+ mPrefName += aPrefName;
+
+ return mPrefName.get();
+}
+
+/**
+ * This will either read in the generic prefs (not specific to a printer)
+ * or read the prefs in using the printer name to qualify.
+ * It is either "print.attr_name" or "print.printer_HPLasr5.attr_name"
+ */
+nsresult nsPrintSettingsService::ReadPrefs(nsIPrintSettings* aPS,
+ const nsAString& aPrinterName,
+ uint32_t aFlags) {
+ NS_ENSURE_ARG_POINTER(aPS);
+
+ bool noValidPrefsFound = true;
+ bool b;
+ nsAutoString str;
+ int32_t iVal;
+ double dbl;
+
+#define GETBOOLPREF(_prefname, _retval) \
+ NS_SUCCEEDED( \
+ Preferences::GetBool(GetPrefName(_prefname, aPrinterName), _retval))
+
+#define GETSTRPREF(_prefname, _retval) \
+ NS_SUCCEEDED( \
+ Preferences::GetString(GetPrefName(_prefname, aPrinterName), _retval))
+
+#define GETINTPREF(_prefname, _retval) \
+ NS_SUCCEEDED( \
+ Preferences::GetInt(GetPrefName(_prefname, aPrinterName), _retval))
+
+#define GETDBLPREF(_prefname, _retval) \
+ NS_SUCCEEDED(ReadPrefDouble(GetPrefName(_prefname, aPrinterName), _retval))
+
+ bool gotPaperSizeFromPrefs = false;
+ int16_t paperSizeUnit;
+ double paperWidth, paperHeight;
+
+ // Paper size prefs are read as a group
+ if (aFlags & nsIPrintSettings::kInitSavePaperSize) {
+ gotPaperSizeFromPrefs = GETINTPREF(kPrintPaperSizeUnit, &iVal) &&
+ GETDBLPREF(kPrintPaperWidth, paperWidth) &&
+ GETDBLPREF(kPrintPaperHeight, paperHeight) &&
+ GETSTRPREF(kPrintPaperId, str);
+ paperSizeUnit = (int16_t)iVal;
+
+ if (gotPaperSizeFromPrefs &&
+ paperSizeUnit != nsIPrintSettings::kPaperSizeInches &&
+ paperSizeUnit != nsIPrintSettings::kPaperSizeMillimeters) {
+ gotPaperSizeFromPrefs = false;
+ }
+
+ if (gotPaperSizeFromPrefs) {
+ // Bug 315687: Sanity check paper size to avoid paper size values in
+ // mm when the size unit flag is inches. The value 100 is arbitrary
+ // and can be changed.
+ gotPaperSizeFromPrefs =
+ (paperSizeUnit != nsIPrintSettings::kPaperSizeInches) ||
+ (paperWidth < 100.0) || (paperHeight < 100.0);
+ }
+
+ if (gotPaperSizeFromPrefs) {
+ aPS->SetPaperSizeUnit(paperSizeUnit);
+ aPS->SetPaperWidth(paperWidth);
+ aPS->SetPaperHeight(paperHeight);
+ aPS->SetPaperId(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ nsIntSize pageSizeInTwips; // to sanity check margins
+ if (!gotPaperSizeFromPrefs) {
+ aPS->GetPaperSizeUnit(&paperSizeUnit);
+ aPS->GetPaperWidth(&paperWidth);
+ aPS->GetPaperHeight(&paperHeight);
+ }
+ if (paperSizeUnit == nsIPrintSettings::kPaperSizeMillimeters) {
+ pageSizeInTwips = nsIntSize((int)NS_MILLIMETERS_TO_TWIPS(paperWidth),
+ (int)NS_MILLIMETERS_TO_TWIPS(paperHeight));
+ } else {
+ pageSizeInTwips = nsIntSize((int)NS_INCHES_TO_TWIPS(paperWidth),
+ (int)NS_INCHES_TO_TWIPS(paperHeight));
+ }
+
+ auto MarginIsOK = [&pageSizeInTwips](const nsIntMargin& aMargin) {
+ return aMargin.top >= 0 && aMargin.right >= 0 && aMargin.bottom >= 0 &&
+ aMargin.left >= 0 && aMargin.LeftRight() < pageSizeInTwips.width &&
+ aMargin.TopBottom() < pageSizeInTwips.height;
+ };
+
+ if (aFlags & nsIPrintSettings::kInitSaveUnwriteableMargins) {
+ nsIntMargin margin;
+ bool allPrefsRead =
+ GETINTPREF(kUnwriteableMarginTopTwips, &margin.top.value) &&
+ GETINTPREF(kUnwriteableMarginRightTwips, &margin.right.value) &&
+ GETINTPREF(kUnwriteableMarginBottomTwips, &margin.bottom.value) &&
+ GETINTPREF(kUnwriteableMarginLeftTwips, &margin.left.value);
+ if (!allPrefsRead) {
+ // We failed to read the new unwritable margin twips prefs. Try to read
+ // the old ones in case they exist.
+ allPrefsRead = ReadInchesIntToTwipsPref(
+ GetPrefName(kUnwriteableMarginTop, aPrinterName),
+ margin.top.value) &&
+ ReadInchesIntToTwipsPref(
+ GetPrefName(kUnwriteableMarginLeft, aPrinterName),
+ margin.left.value) &&
+ ReadInchesIntToTwipsPref(
+ GetPrefName(kUnwriteableMarginBottom, aPrinterName),
+ margin.bottom.value) &&
+ ReadInchesIntToTwipsPref(
+ GetPrefName(kUnwriteableMarginRight, aPrinterName),
+ margin.right.value);
+ }
+ // SetUnwriteableMarginInTwips does its own validation and drops negative
+ // values individually. We still want to block overly large values though,
+ // so we do that part of MarginIsOK manually.
+ if (allPrefsRead && margin.LeftRight() < pageSizeInTwips.width &&
+ margin.TopBottom() < pageSizeInTwips.height) {
+ aPS->SetUnwriteableMarginInTwips(margin);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveMargins) {
+ int32_t halfInch = NS_INCHES_TO_INT_TWIPS(0.5);
+ nsIntMargin margin(halfInch, halfInch, halfInch, halfInch);
+ bool prefRead = ReadInchesToTwipsPref(GetPrefName(kMarginTop, aPrinterName),
+ margin.top.value);
+ prefRead = ReadInchesToTwipsPref(GetPrefName(kMarginLeft, aPrinterName),
+ margin.left.value) ||
+ prefRead;
+ prefRead = ReadInchesToTwipsPref(GetPrefName(kMarginBottom, aPrinterName),
+ margin.bottom.value) ||
+ prefRead;
+
+ prefRead = ReadInchesToTwipsPref(GetPrefName(kMarginRight, aPrinterName),
+ margin.right.value) ||
+ prefRead;
+ if (prefRead && MarginIsOK(margin)) {
+ aPS->SetMarginInTwips(margin);
+ noValidPrefsFound = false;
+
+ prefRead = GETBOOLPREF(kIgnoreUnwriteableMargins, &b);
+ if (prefRead) {
+ aPS->SetIgnoreUnwriteableMargins(b);
+ }
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveEdges) {
+ nsIntMargin margin(0, 0, 0, 0);
+ bool prefRead = ReadInchesIntToTwipsPref(
+ GetPrefName(kEdgeTop, aPrinterName), margin.top.value);
+ prefRead = ReadInchesIntToTwipsPref(GetPrefName(kEdgeLeft, aPrinterName),
+ margin.left.value) ||
+ prefRead;
+
+ prefRead = ReadInchesIntToTwipsPref(GetPrefName(kEdgeBottom, aPrinterName),
+ margin.bottom.value) ||
+ prefRead;
+
+ prefRead = ReadInchesIntToTwipsPref(GetPrefName(kEdgeRight, aPrinterName),
+ margin.right.value) ||
+ prefRead;
+ ;
+ if (prefRead && MarginIsOK(margin)) {
+ aPS->SetEdgeInTwips(margin);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveHeaderLeft) {
+ if (GETSTRPREF(kPrintHeaderStrLeft, str)) {
+ aPS->SetHeaderStrLeft(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveHeaderCenter) {
+ if (GETSTRPREF(kPrintHeaderStrCenter, str)) {
+ aPS->SetHeaderStrCenter(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveHeaderRight) {
+ if (GETSTRPREF(kPrintHeaderStrRight, str)) {
+ aPS->SetHeaderStrRight(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveFooterLeft) {
+ if (GETSTRPREF(kPrintFooterStrLeft, str)) {
+ aPS->SetFooterStrLeft(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveFooterCenter) {
+ if (GETSTRPREF(kPrintFooterStrCenter, str)) {
+ aPS->SetFooterStrCenter(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveFooterRight) {
+ if (GETSTRPREF(kPrintFooterStrRight, str)) {
+ aPS->SetFooterStrRight(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveBGColors) {
+ if (GETBOOLPREF(kPrintBGColors, &b)) {
+ aPS->SetPrintBGColors(b);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveBGImages) {
+ if (GETBOOLPREF(kPrintBGImages, &b)) {
+ aPS->SetPrintBGImages(b);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveReversed) {
+ if (GETBOOLPREF(kPrintReversed, &b)) {
+ aPS->SetPrintReversed(b);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveInColor) {
+ if (GETBOOLPREF(kPrintInColor, &b)) {
+ aPS->SetPrintInColor(b);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveOrientation) {
+ if (GETINTPREF(kPrintOrientation, &iVal) &&
+ (iVal == nsIPrintSettings::kPortraitOrientation ||
+ iVal == nsIPrintSettings::kLandscapeOrientation)) {
+ aPS->SetOrientation(iVal);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSavePrintToFile) {
+ if (GETBOOLPREF(kPrintToFile, &b)) {
+ aPS->SetOutputDestination(
+ b ? nsIPrintSettings::kOutputDestinationFile
+ : nsIPrintSettings::kOutputDestinationPrinter);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveToFileName) {
+ if (GETSTRPREF(kPrintToFileName, str)) {
+ if (StringEndsWith(str, u".ps"_ns)) {
+ // We only support PDF since bug 1425188 landed. Users may still have
+ // prefs with .ps filenames if they last saved a file as Postscript
+ // though, so we fix that up here. (The pref values will be
+ // overwritten the next time they save to file as a PDF.)
+ str.Truncate(str.Length() - 2);
+ str.AppendLiteral("pdf");
+ }
+ aPS->SetToFileName(str);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSavePageDelay) {
+ // milliseconds
+ if (GETINTPREF(kPrintPageDelay, &iVal) && iVal >= 0 && iVal <= 1000) {
+ aPS->SetPrintPageDelay(iVal);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveShrinkToFit) {
+ if (GETBOOLPREF(kPrintShrinkToFit, &b)) {
+ aPS->SetShrinkToFit(b);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveScaling) {
+ // The limits imposed here are fairly arbitrary and mainly intended to
+ // purge bad values which tend to be negative and/or very large. If we
+ // get complaints from users that settings outside these values "aren't
+ // saved" then we can consider increasing them.
+ if (GETDBLPREF(kPrintScaling, dbl) && dbl >= 0.05 && dbl <= 20) {
+ aPS->SetScaling(dbl);
+ noValidPrefsFound = false;
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveDuplex) {
+ if (GETINTPREF(kPrintDuplex, &iVal)) {
+ aPS->SetDuplex(iVal);
+ noValidPrefsFound = false;
+ }
+ }
+
+ // Not Reading In:
+ // Number of Copies
+ // Print Resolution
+
+ return noValidPrefsFound ? NS_ERROR_NOT_AVAILABLE : NS_OK;
+}
+
+nsresult nsPrintSettingsService::WritePrefs(nsIPrintSettings* aPS,
+ const nsAString& aPrinterName,
+ uint32_t aFlags) {
+ NS_ENSURE_ARG_POINTER(aPS);
+
+ if (aFlags & nsIPrintSettings::kInitSaveMargins) {
+ nsIntMargin margin = aPS->GetMarginInTwips();
+ WriteInchesFromTwipsPref(GetPrefName(kMarginTop, aPrinterName), margin.top);
+ WriteInchesFromTwipsPref(GetPrefName(kMarginLeft, aPrinterName),
+ margin.left);
+ WriteInchesFromTwipsPref(GetPrefName(kMarginBottom, aPrinterName),
+ margin.bottom);
+ WriteInchesFromTwipsPref(GetPrefName(kMarginRight, aPrinterName),
+ margin.right);
+ Preferences::SetBool(GetPrefName(kIgnoreUnwriteableMargins, aPrinterName),
+ aPS->GetIgnoreUnwriteableMargins());
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveEdges) {
+ nsIntMargin edge = aPS->GetEdgeInTwips();
+ WriteInchesIntFromTwipsPref(GetPrefName(kEdgeTop, aPrinterName), edge.top);
+ WriteInchesIntFromTwipsPref(GetPrefName(kEdgeLeft, aPrinterName),
+ edge.left);
+ WriteInchesIntFromTwipsPref(GetPrefName(kEdgeBottom, aPrinterName),
+ edge.bottom);
+ WriteInchesIntFromTwipsPref(GetPrefName(kEdgeRight, aPrinterName),
+ edge.right);
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveUnwriteableMargins) {
+ nsIntMargin unwriteableMargin = aPS->GetUnwriteableMarginInTwips();
+ Preferences::SetInt(GetPrefName(kUnwriteableMarginTopTwips, aPrinterName),
+ unwriteableMargin.top);
+ Preferences::SetInt(GetPrefName(kUnwriteableMarginLeftTwips, aPrinterName),
+ unwriteableMargin.left);
+ Preferences::SetInt(
+ GetPrefName(kUnwriteableMarginBottomTwips, aPrinterName),
+ unwriteableMargin.bottom);
+ Preferences::SetInt(GetPrefName(kUnwriteableMarginRightTwips, aPrinterName),
+ unwriteableMargin.right);
+
+ // Remove the old unwriteableMargin prefs.
+ Preferences::ClearUser(GetPrefName(kUnwriteableMarginTop, aPrinterName));
+ Preferences::ClearUser(GetPrefName(kUnwriteableMarginRight, aPrinterName));
+ Preferences::ClearUser(GetPrefName(kUnwriteableMarginBottom, aPrinterName));
+ Preferences::ClearUser(GetPrefName(kUnwriteableMarginLeft, aPrinterName));
+ }
+
+ // Paper size prefs are saved as a group
+ if (aFlags & nsIPrintSettings::kInitSavePaperSize) {
+ int16_t sizeUnit;
+ double width, height;
+ nsString name;
+
+ if (NS_SUCCEEDED(aPS->GetPaperSizeUnit(&sizeUnit)) &&
+ NS_SUCCEEDED(aPS->GetPaperWidth(&width)) &&
+ NS_SUCCEEDED(aPS->GetPaperHeight(&height)) &&
+ NS_SUCCEEDED(aPS->GetPaperId(name))) {
+ Preferences::SetInt(GetPrefName(kPrintPaperSizeUnit, aPrinterName),
+ int32_t(sizeUnit));
+ WritePrefDouble(GetPrefName(kPrintPaperWidth, aPrinterName), width);
+ WritePrefDouble(GetPrefName(kPrintPaperHeight, aPrinterName), height);
+ Preferences::SetString(GetPrefName(kPrintPaperId, aPrinterName), name);
+ }
+ }
+
+ bool b;
+ nsString uStr;
+ int32_t iVal;
+ double dbl;
+
+ if (aFlags & nsIPrintSettings::kInitSaveHeaderLeft) {
+ if (NS_SUCCEEDED(aPS->GetHeaderStrLeft(uStr))) {
+ Preferences::SetString(GetPrefName(kPrintHeaderStrLeft, aPrinterName),
+ uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveHeaderCenter) {
+ if (NS_SUCCEEDED(aPS->GetHeaderStrCenter(uStr))) {
+ Preferences::SetString(GetPrefName(kPrintHeaderStrCenter, aPrinterName),
+ uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveHeaderRight) {
+ if (NS_SUCCEEDED(aPS->GetHeaderStrRight(uStr))) {
+ Preferences::SetString(GetPrefName(kPrintHeaderStrRight, aPrinterName),
+ uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveFooterLeft) {
+ if (NS_SUCCEEDED(aPS->GetFooterStrLeft(uStr))) {
+ Preferences::SetString(GetPrefName(kPrintFooterStrLeft, aPrinterName),
+ uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveFooterCenter) {
+ if (NS_SUCCEEDED(aPS->GetFooterStrCenter(uStr))) {
+ Preferences::SetString(GetPrefName(kPrintFooterStrCenter, aPrinterName),
+ uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveFooterRight) {
+ if (NS_SUCCEEDED(aPS->GetFooterStrRight(uStr))) {
+ Preferences::SetString(GetPrefName(kPrintFooterStrRight, aPrinterName),
+ uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveBGColors) {
+ b = aPS->GetPrintBGColors();
+ Preferences::SetBool(GetPrefName(kPrintBGColors, aPrinterName), b);
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveBGImages) {
+ b = aPS->GetPrintBGImages();
+ Preferences::SetBool(GetPrefName(kPrintBGImages, aPrinterName), b);
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveReversed) {
+ if (NS_SUCCEEDED(aPS->GetPrintReversed(&b))) {
+ Preferences::SetBool(GetPrefName(kPrintReversed, aPrinterName), b);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveInColor) {
+ if (NS_SUCCEEDED(aPS->GetPrintInColor(&b))) {
+ Preferences::SetBool(GetPrefName(kPrintInColor, aPrinterName), b);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveOrientation) {
+ if (NS_SUCCEEDED(aPS->GetOrientation(&iVal))) {
+ Preferences::SetInt(GetPrefName(kPrintOrientation, aPrinterName), iVal);
+ }
+ }
+
+ // Only the general version of this pref is saved
+ if ((aFlags & nsIPrintSettings::kInitSavePrinterName) &&
+ aPrinterName.IsEmpty()) {
+ if (NS_SUCCEEDED(aPS->GetPrinterName(uStr))) {
+ Preferences::SetString(kPrinterName, uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSavePrintToFile) {
+ Preferences::SetBool(GetPrefName(kPrintToFile, aPrinterName),
+ aPS->GetOutputDestination() ==
+ nsIPrintSettings::kOutputDestinationFile);
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveToFileName) {
+ if (NS_SUCCEEDED(aPS->GetToFileName(uStr))) {
+ Preferences::SetString(GetPrefName(kPrintToFileName, aPrinterName), uStr);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSavePageDelay) {
+ if (NS_SUCCEEDED(aPS->GetPrintPageDelay(&iVal))) {
+ Preferences::SetInt(GetPrefName(kPrintPageDelay, aPrinterName), iVal);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveShrinkToFit) {
+ if (NS_SUCCEEDED(aPS->GetShrinkToFit(&b))) {
+ Preferences::SetBool(GetPrefName(kPrintShrinkToFit, aPrinterName), b);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveScaling) {
+ if (NS_SUCCEEDED(aPS->GetScaling(&dbl))) {
+ WritePrefDouble(GetPrefName(kPrintScaling, aPrinterName), dbl);
+ }
+ }
+
+ if (aFlags & nsIPrintSettings::kInitSaveDuplex) {
+ if (NS_SUCCEEDED(aPS->GetDuplex(&iVal))) {
+ Preferences::SetInt(GetPrefName(kPrintDuplex, aPrinterName), iVal);
+ }
+ }
+
+ // Not Writing Out:
+ // Number of Copies
+ // Print Resolution
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsService::GetDefaultPrintSettingsForPrinting(
+ nsIPrintSettings** aPrintSettings) {
+ nsresult rv = CreateNewPrintSettings(aPrintSettings);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIPrintSettings* settings = *aPrintSettings;
+
+ // For security reasons, we don't pass the printer name to content processes.
+ // Once bug 1776169 is fixed, we can just assert that this is the parent
+ // process.
+ bool usePrinterName = XRE_IsParentProcess();
+
+ if (usePrinterName) {
+ nsAutoString printerName;
+ settings->GetPrinterName(printerName);
+ if (printerName.IsEmpty()) {
+ GetLastUsedPrinterName(printerName);
+ settings->SetPrinterName(printerName);
+ }
+ InitPrintSettingsFromPrinter(printerName, settings);
+ }
+
+ InitPrintSettingsFromPrefs(settings, usePrinterName,
+ nsIPrintSettings::kInitSaveAll);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsService::CreateNewPrintSettings(
+ nsIPrintSettings** aNewPrintSettings) {
+ return _CreatePrintSettings(aNewPrintSettings);
+}
+
+NS_IMETHODIMP
+nsPrintSettingsService::GetLastUsedPrinterName(
+ nsAString& aLastUsedPrinterName) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ aLastUsedPrinterName.Truncate();
+ Preferences::GetString(kPrinterName, aLastUsedPrinterName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsService::InitPrintSettingsFromPrinter(
+ const nsAString& aPrinterName, nsIPrintSettings* aPrintSettings) {
+ // Don't get print settings from the printer in the child when printing via
+ // parent, these will be retrieved in the parent later in the print process.
+ if (XRE_IsContentProcess()) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_ARG_POINTER(aPrintSettings);
+
+#ifdef DEBUG
+ nsString printerName;
+ aPrintSettings->GetPrinterName(printerName);
+ if (!printerName.Equals(aPrinterName)) {
+ NS_WARNING("Printer names should match!");
+ }
+#endif
+
+ bool isInitialized;
+ aPrintSettings->GetIsInitializedFromPrinter(&isInitialized);
+ if (isInitialized) return NS_OK;
+
+ nsresult rv;
+ nsCOMPtr<nsIPrinterList> printerList =
+ do_GetService(NS_PRINTER_LIST_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = printerList->InitPrintSettingsFromPrinter(aPrinterName, aPrintSettings);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aPrintSettings->SetIsInitializedFromPrinter(true);
+ return rv;
+}
+
+/** ---------------------------------------------------
+ * Helper function - Returns either the name or sets the length to zero
+ */
+static nsresult GetAdjustedPrinterName(nsIPrintSettings* aPS, bool aUsePNP,
+ nsAString& aPrinterName) {
+ NS_ENSURE_ARG_POINTER(aPS);
+
+ aPrinterName.Truncate();
+ if (!aUsePNP) return NS_OK;
+
+ // Get the Printer Name from the PrintSettings
+ // to use as a prefix for Pref Names
+ nsresult rv = aPS->GetPrinterName(aPrinterName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Convert any whitespaces, carriage returns or newlines to _
+ // The below algorithm is supposedly faster than using iterators
+ constexpr auto replSubstr = u"_"_ns;
+ const char* replaceStr = " \n\r";
+
+ int32_t x;
+ for (x = 0; x < (int32_t)strlen(replaceStr); x++) {
+ char16_t uChar = replaceStr[x];
+
+ int32_t i = 0;
+ while ((i = aPrinterName.FindChar(uChar, i)) != kNotFound) {
+ aPrinterName.Replace(i, 1, replSubstr);
+ i++;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsService::InitPrintSettingsFromPrefs(nsIPrintSettings* aPS,
+ bool aUsePNP,
+ uint32_t aFlags) {
+ NS_ENSURE_ARG_POINTER(aPS);
+
+ bool isInitialized;
+ aPS->GetIsInitializedFromPrefs(&isInitialized);
+
+ if (isInitialized) {
+ return NS_OK;
+ }
+
+ auto globalPrintSettings = aFlags;
+#ifndef MOZ_WIDGET_ANDROID
+ globalPrintSettings &= nsIPrintSettings::kGlobalSettings;
+#endif
+
+ nsAutoString prtName;
+ // read any non printer specific prefs
+ // with empty printer name
+ nsresult rv = ReadPrefs(aPS, prtName, globalPrintSettings);
+ if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) {
+ NS_WARNING("ReadPrefs failed");
+ }
+
+ // Get the Printer Name from the PrintSettings to use as a prefix for Pref
+ // Names
+ rv = GetAdjustedPrinterName(aPS, aUsePNP, prtName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (prtName.IsEmpty()) {
+ NS_WARNING("Caller should supply a printer name.");
+ return NS_OK;
+ }
+
+ // Now read any printer specific prefs
+ rv = ReadPrefs(aPS, prtName, aFlags);
+ if (NS_SUCCEEDED(rv)) {
+ aPS->SetIsInitializedFromPrefs(true);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Save all of the printer settings; if we can find a printer name, save
+ * printer-specific preferences. Otherwise, save generic ones.
+ */
+nsresult nsPrintSettingsService::MaybeSavePrintSettingsToPrefs(
+ nsIPrintSettings* aPS, uint32_t aFlags) {
+ NS_ENSURE_ARG_POINTER(aPS);
+ MOZ_DIAGNOSTIC_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
+ MOZ_ASSERT(!(aFlags & nsIPrintSettings::kInitSavePrinterName),
+ "Use SaveLastUsedPrintNameToPrefs");
+
+ if (!Preferences::GetBool("print.save_print_settings", false)) {
+ return NS_OK;
+ }
+
+ // Get the printer name from the PrinterSettings for an optional prefix.
+ nsAutoString prtName;
+ nsresult rv = GetAdjustedPrinterName(aPS, true, prtName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifndef MOZ_WIDGET_ANDROID
+ // On most platforms we should always use a prefix when saving print settings
+ // to prefs. Saving without a prefix risks breaking printing for users
+ // without a good way for us to fix things for them (unprefixed prefs act as
+ // defaults and can result in values being inappropriately propagated to
+ // prefixed prefs).
+ if (prtName.IsEmpty()) {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Print settings must be saved with a prefix");
+ return NS_ERROR_FAILURE;
+ }
+#endif
+
+ return WritePrefs(aPS, prtName, aFlags);
+}
+
+nsresult nsPrintSettingsService::MaybeSaveLastUsedPrinterNameToPrefs(
+ const nsAString& aPrinterName) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
+
+ if (!Preferences::GetBool("print.save_print_settings", false)) {
+ return NS_OK;
+ }
+
+ if (!aPrinterName.IsEmpty()) {
+ Preferences::SetString(kPrinterName, aPrinterName);
+ }
+ return NS_OK;
+}
+
+//-----------------------------------------------------
+//-- Protected Methods --------------------------------
+//-----------------------------------------------------
+nsresult nsPrintSettingsService::ReadPrefDouble(const char* aPrefId,
+ double& aVal) {
+ NS_ENSURE_ARG_POINTER(aPrefId);
+
+ nsAutoCString str;
+ nsresult rv = Preferences::GetCString(aPrefId, str);
+ if (NS_FAILED(rv) || str.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ double value = str.ToDouble(&rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aVal = value;
+ return NS_OK;
+}
+
+nsresult nsPrintSettingsService::WritePrefDouble(const char* aPrefId,
+ double aVal) {
+ NS_ENSURE_ARG_POINTER(aPrefId);
+
+ nsAutoCString str;
+ str.AppendFloat(aVal);
+ return Preferences::SetCString(aPrefId, str);
+}
+
+bool nsPrintSettingsService::ReadInchesToTwipsPref(const char* aPrefId,
+ int32_t& aTwips) {
+ nsAutoString str;
+ nsresult rv = Preferences::GetString(aPrefId, str);
+ if (NS_FAILED(rv) || str.IsEmpty()) {
+ return false;
+ }
+
+ float inches = str.ToFloat(&rv);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ aTwips = NS_INCHES_TO_INT_TWIPS(inches);
+ return true;
+}
+
+void nsPrintSettingsService::WriteInchesFromTwipsPref(const char* aPrefId,
+ int32_t aTwips) {
+ double inches = NS_TWIPS_TO_INCHES(aTwips);
+ nsAutoCString inchesStr;
+ inchesStr.AppendFloat(inches);
+
+ Preferences::SetCString(aPrefId, inchesStr);
+}
+
+bool nsPrintSettingsService::ReadInchesIntToTwipsPref(const char* aPrefId,
+ int32_t& aTwips) {
+ int32_t value;
+ nsresult rv = Preferences::GetInt(aPrefId, &value);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ aTwips = NS_INCHES_TO_INT_TWIPS(float(value) / 100.0f);
+ return true;
+}
+
+void nsPrintSettingsService::WriteInchesIntFromTwipsPref(const char* aPrefId,
+ int32_t aTwips) {
+ Preferences::SetInt(aPrefId,
+ int32_t(NS_TWIPS_TO_INCHES(aTwips) * 100.0f + 0.5f));
+}
+
+void nsPrintSettingsService::ReadJustification(const char* aPrefId,
+ int16_t& aJust,
+ int16_t aInitValue) {
+ aJust = aInitValue;
+ nsAutoString justStr;
+ if (NS_SUCCEEDED(Preferences::GetString(aPrefId, justStr))) {
+ if (justStr.EqualsASCII(kJustRight)) {
+ aJust = nsIPrintSettings::kJustRight;
+ } else if (justStr.EqualsASCII(kJustCenter)) {
+ aJust = nsIPrintSettings::kJustCenter;
+ } else {
+ aJust = nsIPrintSettings::kJustLeft;
+ }
+ }
+}
+
+//---------------------------------------------------
+void nsPrintSettingsService::WriteJustification(const char* aPrefId,
+ int16_t aJust) {
+ switch (aJust) {
+ case nsIPrintSettings::kJustLeft:
+ Preferences::SetCString(aPrefId, kJustLeft);
+ break;
+
+ case nsIPrintSettings::kJustCenter:
+ Preferences::SetCString(aPrefId, kJustCenter);
+ break;
+
+ case nsIPrintSettings::kJustRight:
+ Preferences::SetCString(aPrefId, kJustRight);
+ break;
+ } // switch
+}
diff --git a/widget/nsPrintSettingsService.h b/widget/nsPrintSettingsService.h
new file mode 100644
index 0000000000..43965455a1
--- /dev/null
+++ b/widget/nsPrintSettingsService.h
@@ -0,0 +1,87 @@
+/* -*- Mode: IDL; tab-width: 4; 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/. */
+
+#ifndef nsPrintSettingsService_h
+#define nsPrintSettingsService_h
+
+#include "nsCOMPtr.h"
+#include "nsIPrintSettings.h"
+#include "nsIPrintSettingsService.h"
+#include "nsString.h"
+#include "nsFont.h"
+
+/**
+ * Class nsPrintSettingsService. Base class for the platform specific widget
+ * subclasses to inherit from.
+ */
+class nsPrintSettingsService : public nsIPrintSettingsService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRINTSETTINGSSERVICE
+
+ nsPrintSettingsService() = default;
+
+ /**
+ * method Init
+ * Initializes member variables. Every consumer that does manual
+ * creation (instead of do_CreateInstance) needs to call this method
+ * immediately after instantiation.
+ */
+ virtual nsresult Init();
+
+ private:
+ // Copying is not supported.
+ nsPrintSettingsService(const nsPrintSettingsService& x) = delete;
+ nsPrintSettingsService& operator=(const nsPrintSettingsService& x) = delete;
+
+ protected:
+ virtual ~nsPrintSettingsService() = default;
+
+ void ReadBitFieldPref(const char* aPrefId, int32_t anOption);
+ void WriteBitFieldPref(const char* aPrefId, int32_t anOption);
+ void ReadJustification(const char* aPrefId, int16_t& aJust,
+ int16_t aInitValue);
+ void WriteJustification(const char* aPrefId, int16_t aJust);
+ bool ReadInchesToTwipsPref(const char* aPrefId, int32_t& aTwips);
+ void WriteInchesFromTwipsPref(const char* aPrefId, int32_t aTwips);
+ bool ReadInchesIntToTwipsPref(const char* aPrefId, int32_t& aTwips);
+ void WriteInchesIntFromTwipsPref(const char* aPrefId, int32_t aTwips);
+
+ nsresult ReadPrefDouble(const char* aPrefId, double& aVal);
+ nsresult WritePrefDouble(const char* aPrefId, double aVal);
+
+ /**
+ * method ReadPrefs
+ * @param aPS a pointer to the printer settings
+ * @param aPrinterName the name of the printer for which to read prefs
+ * @param aFlags flag specifying which prefs to read
+ */
+ virtual nsresult ReadPrefs(nsIPrintSettings* aPS,
+ const nsAString& aPrinterName, uint32_t aFlags);
+ /**
+ * method WritePrefs
+ * @param aPS a pointer to the printer settings
+ * @param aPrinterName the name of the printer for which to write prefs
+ * @param aFlags flag specifying which prefs to read
+ */
+ virtual nsresult WritePrefs(nsIPrintSettings* aPS,
+ const nsAString& aPrinterName, uint32_t aFlags);
+
+ const char* GetPrefName(const char* aPrefName, const nsAString& aPrinterName);
+
+ /**
+ * method _CreatePrintSettings
+ * May be implemented by the platform-specific derived class
+ *
+ * @return printer settings instance
+ */
+ virtual nsresult _CreatePrintSettings(nsIPrintSettings** _retval) = 0;
+
+ // Members
+ nsCString mPrefName;
+};
+
+#endif // nsPrintSettingsService_h
diff --git a/widget/nsPrinterBase.cpp b/widget/nsPrinterBase.cpp
new file mode 100644
index 0000000000..7e01b9e12f
--- /dev/null
+++ b/widget/nsPrinterBase.cpp
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsPrinterBase.h"
+#include "nsPaperMargin.h"
+#include <utility>
+#include "nsPaper.h"
+#include "nsIPrintSettings.h"
+#include "nsPrintSettingsService.h"
+#include "PrintBackgroundTask.h"
+#include "mozilla/dom/Promise.h"
+
+using namespace mozilla;
+using mozilla::dom::Promise;
+using mozilla::gfx::MarginDouble;
+
+// The maximum error when considering a paper size equal, in points.
+// There is some variance in the actual sizes returned by printer drivers and
+// print servers for paper sizes. This is a best-guess based on initial
+// telemetry which should catch most near-miss dimensions. This should let us
+// get consistent paper size names even when the size isn't quite exactly the
+// correct size.
+static constexpr double kPaperSizePointsEpsilon = 4.0;
+
+// Basic implementation of nsIPrinterInfo
+class nsPrinterInfo : public nsIPrinterInfo {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsPrinterInfo)
+ NS_DECL_NSIPRINTERINFO
+ nsPrinterInfo() = delete;
+ nsPrinterInfo(nsPrinterBase& aPrinter,
+ const nsPrinterBase::PrinterInfo& aPrinterInfo)
+ : mDefaultSettings(
+ CreatePlatformPrintSettings(aPrinterInfo.mDefaultSettings)) {
+ mPaperList.SetCapacity(aPrinterInfo.mPaperList.Length());
+ for (const PaperInfo& info : aPrinterInfo.mPaperList) {
+ mPaperList.AppendElement(MakeRefPtr<nsPaper>(aPrinter, info));
+ }
+
+ // Update the printer's default settings with the global settings.
+ nsCOMPtr<nsIPrintSettingsService> printSettingsSvc =
+ do_GetService("@mozilla.org/gfx/printsettings-service;1");
+ if (printSettingsSvc) {
+ // Passing false as the second parameter means we don't get the printer
+ // specific settings.
+ printSettingsSvc->InitPrintSettingsFromPrefs(
+ mDefaultSettings, false, nsIPrintSettings::kInitSaveAll);
+ }
+ }
+
+ private:
+ virtual ~nsPrinterInfo() = default;
+
+ nsTArray<RefPtr<nsIPaper>> mPaperList;
+ RefPtr<nsIPrintSettings> mDefaultSettings;
+};
+
+NS_IMETHODIMP
+nsPrinterInfo::GetPaperList(nsTArray<RefPtr<nsIPaper>>& aPaperList) {
+ aPaperList = mPaperList.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrinterInfo::GetDefaultSettings(nsIPrintSettings** aDefaultSettings) {
+ NS_ENSURE_ARG_POINTER(aDefaultSettings);
+ MOZ_ASSERT(mDefaultSettings);
+ RefPtr<nsIPrintSettings> settings = mDefaultSettings;
+ settings.forget(aDefaultSettings);
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION(nsPrinterInfo, mPaperList, mDefaultSettings)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsPrinterInfo)
+ NS_INTERFACE_MAP_ENTRY(nsIPrinterInfo)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPrinterInfo)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsPrinterInfo)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsPrinterInfo)
+
+template <typename Index, Index Size, typename Value>
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback,
+ EnumeratedArray<Index, Size, Value>& aArray, const char* aName,
+ uint32_t aFlags = 0) {
+ aFlags |= CycleCollectionEdgeNameArrayFlag;
+ for (Value& element : aArray) {
+ ImplCycleCollectionTraverse(aCallback, element, aName, aFlags);
+ }
+}
+
+template <typename Index, Index Size, typename Value>
+inline void ImplCycleCollectionUnlink(
+ EnumeratedArray<Index, Size, Value>& aArray) {
+ for (Value& element : aArray) {
+ ImplCycleCollectionUnlink(element);
+ }
+}
+
+namespace mozilla {
+
+template <>
+void ResolveOrReject(Promise& aPromise, nsPrinterBase&,
+ const MarginDouble& aResult) {
+ auto margin = MakeRefPtr<nsPaperMargin>(aResult);
+ aPromise.MaybeResolve(margin);
+}
+
+template <>
+void ResolveOrReject(Promise& aPromise, nsPrinterBase& aPrinter,
+ const nsTArray<PaperInfo>& aResult) {
+ nsTArray<RefPtr<nsPaper>> result;
+ result.SetCapacity(aResult.Length());
+ for (const PaperInfo& info : aResult) {
+ result.AppendElement(MakeRefPtr<nsPaper>(aPrinter, info));
+ }
+ aPromise.MaybeResolve(result);
+}
+
+template <>
+void ResolveOrReject(Promise& aPromise, nsPrinterBase& aPrinter,
+ const PrintSettingsInitializer& aResult) {
+ aPromise.MaybeResolve(
+ RefPtr<nsIPrintSettings>(CreatePlatformPrintSettings(aResult)));
+}
+
+template <>
+void ResolveOrReject(Promise& aPromise, nsPrinterBase& aPrinter,
+ const nsPrinterBase::PrinterInfo& aResult) {
+ aPromise.MaybeResolve(MakeRefPtr<nsPrinterInfo>(aPrinter, aResult));
+}
+
+} // namespace mozilla
+
+template <typename T, typename... Args>
+nsresult nsPrinterBase::AsyncPromiseAttributeGetter(
+ JSContext* aCx, Promise** aResultPromise, AsyncAttribute aAttribute,
+ BackgroundTask<T, Args...> aBackgroundTask, Args... aArgs) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ static constexpr EnumeratedArray<AsyncAttribute, AsyncAttribute::Last,
+ nsLiteralCString>
+ attributeKeys{"SupportsDuplex"_ns, "SupportsColor"_ns,
+ "SupportsMonochrome"_ns, "SupportsCollation"_ns,
+ "PrinterInfo"_ns};
+ return mozilla::AsyncPromiseAttributeGetter(
+ *this, mAsyncAttributePromises[aAttribute], aCx, aResultPromise,
+ attributeKeys[aAttribute], aBackgroundTask, std::forward<Args>(aArgs)...);
+}
+
+NS_IMETHODIMP nsPrinterBase::CopyFromWithValidation(
+ nsIPrintSettings* aSettingsToCopyFrom, JSContext* aCx,
+ Promise** aResultPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aResultPromise);
+
+ ErrorResult errorResult;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), errorResult);
+ if (MOZ_UNLIKELY(errorResult.Failed())) {
+ return errorResult.StealNSResult();
+ }
+
+ nsCOMPtr<nsIPrintSettings> settings;
+ MOZ_ALWAYS_SUCCEEDS(aSettingsToCopyFrom->Clone(getter_AddRefs(settings)));
+ nsString printerName;
+ MOZ_ALWAYS_SUCCEEDS(GetName(printerName));
+ MOZ_ALWAYS_SUCCEEDS(settings->SetPrinterName(printerName));
+ promise->MaybeResolve(settings);
+ promise.forget(aResultPromise);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrinterBase::GetSupportsDuplex(JSContext* aCx,
+ Promise** aResultPromise) {
+ return AsyncPromiseAttributeGetter(aCx, aResultPromise,
+ AsyncAttribute::SupportsDuplex,
+ &nsPrinterBase::SupportsDuplex);
+}
+
+NS_IMETHODIMP nsPrinterBase::GetSupportsColor(JSContext* aCx,
+ Promise** aResultPromise) {
+ return AsyncPromiseAttributeGetter(aCx, aResultPromise,
+ AsyncAttribute::SupportsColor,
+ &nsPrinterBase::SupportsColor);
+}
+
+NS_IMETHODIMP nsPrinterBase::GetSupportsMonochrome(JSContext* aCx,
+ Promise** aResultPromise) {
+ return AsyncPromiseAttributeGetter(aCx, aResultPromise,
+ AsyncAttribute::SupportsMonochrome,
+ &nsPrinterBase::SupportsMonochrome);
+}
+
+NS_IMETHODIMP nsPrinterBase::GetSupportsCollation(JSContext* aCx,
+ Promise** aResultPromise) {
+ return AsyncPromiseAttributeGetter(aCx, aResultPromise,
+ AsyncAttribute::SupportsCollation,
+ &nsPrinterBase::SupportsCollation);
+}
+
+NS_IMETHODIMP nsPrinterBase::GetPrinterInfo(JSContext* aCx,
+ Promise** aResultPromise) {
+ return AsyncPromiseAttributeGetter(aCx, aResultPromise,
+ AsyncAttribute::PrinterInfo,
+ &nsPrinterBase::CreatePrinterInfo);
+}
+
+void nsPrinterBase::QueryMarginsForPaper(Promise& aPromise,
+ const nsString& aPaperId) {
+ return SpawnPrintBackgroundTask(*this, aPromise, "MarginsForPaper"_ns,
+ &nsPrinterBase::GetMarginsForPaper, aPaperId);
+}
+
+NS_IMPL_CYCLE_COLLECTION(nsPrinterBase, mAsyncAttributePromises)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsPrinterBase)
+ NS_INTERFACE_MAP_ENTRY(nsIPrinter)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPrinter)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsPrinterBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsPrinterBase)
+
+nsPrinterBase::nsPrinterBase(const CommonPaperInfoArray* aPaperInfoArray)
+ : mCommonPaperInfo(aPaperInfoArray) {
+ MOZ_DIAGNOSTIC_ASSERT(aPaperInfoArray, "Localized paper info was null");
+}
+nsPrinterBase::~nsPrinterBase() = default;
+
+const PaperInfo* nsPrinterBase::FindCommonPaperSize(
+ const gfx::SizeDouble& aSize) const {
+ for (const PaperInfo& paper : *mCommonPaperInfo) {
+ if (std::abs(paper.mSize.width - aSize.width) <= kPaperSizePointsEpsilon &&
+ std::abs(paper.mSize.height - aSize.height) <=
+ kPaperSizePointsEpsilon) {
+ return &paper;
+ }
+ }
+ return nullptr;
+}
diff --git a/widget/nsPrinterBase.h b/widget/nsPrinterBase.h
new file mode 100644
index 0000000000..abd4f82705
--- /dev/null
+++ b/widget/nsPrinterBase.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsPrinterBase_h__
+#define nsPrinterBase_h__
+
+#include "mozilla/gfx/Rect.h"
+#include "nsIPrinter.h"
+#include "nsTArray.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsPrintSettingsImpl.h"
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/Result.h"
+
+namespace mozilla {
+
+struct PaperInfo;
+
+namespace dom {
+class Promise;
+}
+
+} // namespace mozilla
+
+class nsPrinterBase : public nsIPrinter {
+ public:
+ using Promise = mozilla::dom::Promise;
+ using MarginDouble = mozilla::gfx::MarginDouble;
+ using PrintSettingsInitializer = mozilla::PrintSettingsInitializer;
+
+ NS_IMETHOD CopyFromWithValidation(nsIPrintSettings*, JSContext*,
+ Promise**) override;
+ NS_IMETHOD GetSupportsDuplex(JSContext*, Promise**) final;
+ NS_IMETHOD GetSupportsColor(JSContext*, Promise**) final;
+ NS_IMETHOD GetSupportsMonochrome(JSContext*, Promise**) final;
+ NS_IMETHOD GetSupportsCollation(JSContext*, Promise**) final;
+ NS_IMETHOD GetPrinterInfo(JSContext*, Promise**) final;
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsPrinterBase)
+
+ // We require the paper info array
+ nsPrinterBase() = delete;
+
+ // No copy or move, we're an identity.
+ nsPrinterBase(const nsPrinterBase&) = delete;
+ nsPrinterBase(nsPrinterBase&&) = delete;
+
+ void QueryMarginsForPaper(Promise&, const nsString& aPaperId);
+
+ /**
+ * Caches the argument by copying it into mPrintSettingsInitializer.
+ * If mPrintSettingsInitializer is already populated this is a no-op.
+ */
+ void CachePrintSettingsInitializer(
+ const PrintSettingsInitializer& aInitializer);
+
+ // Data to pass through to create the nsPrinterInfo
+ struct PrinterInfo {
+ nsTArray<mozilla::PaperInfo> mPaperList;
+ PrintSettingsInitializer mDefaultSettings;
+ };
+
+ private:
+ enum class AsyncAttribute {
+ // If you change this list you must update attributeKeys in
+ // nsPrinterBase::AsyncPromiseAttributeGetter.
+ SupportsDuplex = 0,
+ SupportsColor,
+ SupportsMonochrome,
+ SupportsCollation,
+ PrinterInfo,
+ // Just a guard.
+ Last,
+ };
+
+ template <typename Result, typename... Args>
+ using BackgroundTask = Result (nsPrinterBase::*)(Args...) const;
+
+ // Resolves an async attribute via a background task.
+ template <typename T, typename... Args>
+ nsresult AsyncPromiseAttributeGetter(JSContext*, Promise**, AsyncAttribute,
+ BackgroundTask<T, Args...>,
+ Args... aArgs);
+
+ protected:
+ nsPrinterBase(const mozilla::CommonPaperInfoArray* aPaperInfoArray);
+ virtual ~nsPrinterBase();
+
+ // Implementation-specific methods. These must not make assumptions about
+ // which thread they run on.
+ virtual bool SupportsDuplex() const = 0;
+ virtual bool SupportsColor() const = 0;
+ virtual bool SupportsMonochrome() const = 0;
+ virtual bool SupportsCollation() const = 0;
+ virtual MarginDouble GetMarginsForPaper(nsString aPaperId) const = 0;
+ virtual PrinterInfo CreatePrinterInfo() const = 0;
+ // Searches our built-in list of commonly used PWG paper sizes for a matching,
+ // localized PaperInfo. Returns null if there is no matching size.
+ const mozilla::PaperInfo* FindCommonPaperSize(
+ const mozilla::gfx::SizeDouble& aSize) const;
+
+ private:
+ mozilla::EnumeratedArray<AsyncAttribute, AsyncAttribute::Last,
+ RefPtr<Promise>>
+ mAsyncAttributePromises;
+ // List of built-in, commonly used paper sizes.
+ const RefPtr<const mozilla::CommonPaperInfoArray> mCommonPaperInfo;
+};
+
+#endif
diff --git a/widget/nsPrinterCUPS.cpp b/widget/nsPrinterCUPS.cpp
new file mode 100644
index 0000000000..a8656de23f
--- /dev/null
+++ b/widget/nsPrinterCUPS.cpp
@@ -0,0 +1,476 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 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 "nsPrinterCUPS.h"
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/GkRustUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_print.h"
+#include "nsTHashtable.h"
+#include "nsPaper.h"
+#include "nsPrinterBase.h"
+#include "nsPrintSettingsImpl.h"
+
+using namespace mozilla;
+using MarginDouble = mozilla::gfx::MarginDouble;
+
+// Requested attributes for IPP requests, just the CUPS version now.
+static constexpr Array<const char* const, 1> requestedAttributes{
+ "cups-version"};
+
+static constexpr double kPointsPerHundredthMillimeter = 72.0 / 2540.0;
+
+static PaperInfo MakePaperInfo(const nsAString& aName,
+ const cups_size_t& aMedia) {
+ // XXX Do we actually have the guarantee that this is utf-8?
+ NS_ConvertUTF8toUTF16 paperId(aMedia.media); // internal paper name/ID
+ return PaperInfo(
+ paperId, aName,
+ {aMedia.width * kPointsPerHundredthMillimeter,
+ aMedia.length * kPointsPerHundredthMillimeter},
+ Some(gfx::MarginDouble{aMedia.top * kPointsPerHundredthMillimeter,
+ aMedia.right * kPointsPerHundredthMillimeter,
+ aMedia.bottom * kPointsPerHundredthMillimeter,
+ aMedia.left * kPointsPerHundredthMillimeter}));
+}
+
+// Fetches the CUPS version for the print server controlling the printer. This
+// will only modify the output arguments if the fetch succeeds.
+static void FetchCUPSVersionForPrinter(const nsCUPSShim& aShim,
+ const cups_dest_t* const aDest,
+ uint64_t& aOutMajor, uint64_t& aOutMinor,
+ uint64_t& aOutPatch) {
+ // Make an IPP request to the server for the printer.
+ const char* const uri = aShim.cupsGetOption(
+ "printer-uri-supported", aDest->num_options, aDest->options);
+ if (!uri) {
+ return;
+ }
+
+ ipp_t* const ippRequest = aShim.ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
+
+ // Set the URI we want to use.
+ aShim.ippAddString(ippRequest, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
+ nullptr, uri);
+
+ // Set the attributes to request.
+ aShim.ippAddStrings(ippRequest, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
+ "requested-attributes", requestedAttributes.Length,
+ nullptr, &(requestedAttributes[0]));
+
+ // Use the default HTTP connection to query the CUPS server itself to get
+ // the CUPS version.
+ // Note that cupsDoRequest will delete the request whether it succeeds or
+ // fails, so we should not use ippDelete on it.
+ if (ipp_t* const ippResponse =
+ aShim.cupsDoRequest(CUPS_HTTP_DEFAULT, ippRequest, "/")) {
+ ipp_attribute_t* const versionAttrib =
+ aShim.ippFindAttribute(ippResponse, "cups-version", IPP_TAG_TEXT);
+ if (versionAttrib && aShim.ippGetCount(versionAttrib) == 1) {
+ const char* versionString = aShim.ippGetString(versionAttrib, 0, nullptr);
+ MOZ_ASSERT(versionString);
+ // On error, GkRustUtils::ParseSemVer will not modify its arguments.
+ GkRustUtils::ParseSemVer(
+ nsDependentCSubstring{MakeStringSpan(versionString)}, aOutMajor,
+ aOutMinor, aOutPatch);
+ }
+ aShim.ippDelete(ippResponse);
+ }
+}
+
+nsPrinterCUPS::~nsPrinterCUPS() {
+ PrinterInfoLock lock = mPrinterInfoMutex.Lock();
+ if (lock->mPrinterInfo) {
+ mShim.cupsFreeDestInfo(lock->mPrinterInfo);
+ }
+ if (lock->mPrinter) {
+ mShim.cupsFreeDests(1, lock->mPrinter);
+ }
+}
+
+NS_IMETHODIMP
+nsPrinterCUPS::GetName(nsAString& aName) {
+ GetPrinterName(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrinterCUPS::GetSystemName(nsAString& aName) {
+ PrinterInfoLock lock = mPrinterInfoMutex.Lock();
+ CopyUTF8toUTF16(MakeStringSpan(lock->mPrinter->name), aName);
+ return NS_OK;
+}
+
+void nsPrinterCUPS::GetPrinterName(nsAString& aName) const {
+ if (mDisplayName.IsEmpty()) {
+ aName.Truncate();
+ PrinterInfoLock lock = mPrinterInfoMutex.Lock();
+ CopyUTF8toUTF16(MakeStringSpan(lock->mPrinter->name), aName);
+ } else {
+ aName = mDisplayName;
+ }
+}
+
+const char* nsPrinterCUPS::LocalizeMediaName(http_t& aConnection,
+ cups_size_t& aMedia) const {
+ // The returned string is owned by mPrinterInfo.
+ // https://www.cups.org/doc/cupspm.html#cupsLocalizeDestMedia
+ if (!mShim.cupsLocalizeDestMedia) {
+ return aMedia.media;
+ }
+ PrinterInfoLock lock = TryEnsurePrinterInfo();
+ return mShim.cupsLocalizeDestMedia(&aConnection, lock->mPrinter,
+ lock->mPrinterInfo,
+ CUPS_MEDIA_FLAGS_DEFAULT, &aMedia);
+}
+
+bool nsPrinterCUPS::SupportsDuplex() const {
+ return Supports(CUPS_SIDES, CUPS_SIDES_TWO_SIDED_PORTRAIT);
+}
+
+bool nsPrinterCUPS::SupportsMonochrome() const {
+ if (!SupportsColor()) {
+ return true;
+ }
+ return StaticPrefs::print_cups_monochrome_enabled();
+}
+
+bool nsPrinterCUPS::SupportsColor() const {
+ // CUPS 2.1 (particularly as used in Ubuntu 16) is known to have inaccurate
+ // results for CUPS_PRINT_COLOR_MODE.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1660658#c15
+ if (!IsCUPSVersionAtLeast(2, 2, 0)) {
+ return true; // See comment for PrintSettingsInitializer.mPrintInColor
+ }
+ return Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_AUTO) ||
+ Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_COLOR) ||
+ !Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_MONOCHROME);
+}
+
+bool nsPrinterCUPS::SupportsCollation() const {
+ // We can't depend on cupsGetIntegerOption existing.
+ PrinterInfoLock lock = mPrinterInfoMutex.Lock();
+ const char* const value = FindCUPSOption(lock, "printer-type");
+ if (!value) {
+ return false;
+ }
+ // If the value is non-numeric, then atoi will return 0, which will still
+ // cause this function to return false.
+ const int type = atoi(value);
+ return type & CUPS_PRINTER_COLLATE;
+}
+
+nsPrinterBase::PrinterInfo nsPrinterCUPS::CreatePrinterInfo() const {
+ Connection connection{mShim};
+ return PrinterInfo{PaperList(connection), DefaultSettings(connection)};
+}
+
+bool nsPrinterCUPS::Supports(const char* aOption, const char* aValue) const {
+ PrinterInfoLock lock = TryEnsurePrinterInfo();
+ return mShim.cupsCheckDestSupported(CUPS_HTTP_DEFAULT, lock->mPrinter,
+ lock->mPrinterInfo, aOption, aValue);
+}
+
+bool nsPrinterCUPS::IsCUPSVersionAtLeast(uint64_t aCUPSMajor,
+ uint64_t aCUPSMinor,
+ uint64_t aCUPSPatch) const {
+ PrinterInfoLock lock = TryEnsurePrinterInfo();
+ // Compare major version.
+ if (lock->mCUPSMajor > aCUPSMajor) {
+ return true;
+ }
+ if (lock->mCUPSMajor < aCUPSMajor) {
+ return false;
+ }
+
+ // Compare minor version.
+ if (lock->mCUPSMinor > aCUPSMinor) {
+ return true;
+ }
+ if (lock->mCUPSMinor < aCUPSMinor) {
+ return false;
+ }
+
+ // Compare patch.
+ return aCUPSPatch <= lock->mCUPSPatch;
+}
+
+http_t* nsPrinterCUPS::Connection::GetConnection(cups_dest_t* aDest) {
+ if (mWasInited) {
+ return mConnection;
+ }
+ mWasInited = true;
+
+ // blocking call
+ http_t* const connection = mShim.cupsConnectDest(aDest, CUPS_DEST_FLAGS_NONE,
+ /* timeout(ms) */ 5000,
+ /* cancel */ nullptr,
+ /* resource */ nullptr,
+ /* resourcesize */ 0,
+ /* callback */ nullptr,
+ /* user_data */ nullptr);
+ if (connection) {
+ mConnection = connection;
+ }
+ return mConnection;
+}
+
+nsPrinterCUPS::Connection::~Connection() {
+ if (mWasInited && mConnection) {
+ mShim.httpClose(mConnection);
+ }
+}
+
+PrintSettingsInitializer nsPrinterCUPS::DefaultSettings(
+ Connection& aConnection) const {
+ nsString printerName;
+ GetPrinterName(printerName);
+ PrinterInfoLock lock = TryEnsurePrinterInfo();
+
+ cups_size_t media;
+
+ bool hasDefaultMedia = false;
+// cupsGetDestMediaDefault appears to return more accurate defaults on macOS,
+// and the IPP attribute appears to return more accurate defaults on Linux.
+#ifdef XP_MACOSX
+ hasDefaultMedia = mShim.cupsGetDestMediaDefault(
+ CUPS_HTTP_DEFAULT, lock->mPrinter, lock->mPrinterInfo,
+ CUPS_MEDIA_FLAGS_DEFAULT, &media);
+#else
+ {
+ ipp_attribute_t* defaultMediaIPP =
+ mShim.cupsFindDestDefault
+ ? mShim.cupsFindDestDefault(CUPS_HTTP_DEFAULT, lock->mPrinter,
+ lock->mPrinterInfo, "media")
+ : nullptr;
+
+ const char* defaultMediaName =
+ defaultMediaIPP ? mShim.ippGetString(defaultMediaIPP, 0, nullptr)
+ : nullptr;
+
+ hasDefaultMedia = defaultMediaName &&
+ mShim.cupsGetDestMediaByName(
+ CUPS_HTTP_DEFAULT, lock->mPrinter, lock->mPrinterInfo,
+ defaultMediaName, CUPS_MEDIA_FLAGS_DEFAULT, &media);
+ }
+#endif
+
+ if (!hasDefaultMedia) {
+ return PrintSettingsInitializer{
+ std::move(printerName),
+ PaperInfo(),
+ SupportsColor(),
+ };
+ }
+
+ // Check if this is a localized fallback paper size, in which case we can
+ // avoid using the CUPS localization methods.
+ const gfx::SizeDouble sizeDouble{
+ media.width * kPointsPerHundredthMillimeter,
+ media.length * kPointsPerHundredthMillimeter};
+ if (const PaperInfo* const paperInfo = FindCommonPaperSize(sizeDouble)) {
+ return PrintSettingsInitializer{
+ std::move(printerName),
+ MakePaperInfo(paperInfo->mName, media),
+ SupportsColor(),
+ };
+ }
+
+ http_t* const connection = aConnection.GetConnection(lock->mPrinter);
+ // XXX Do we actually have the guarantee that this is utf-8?
+ NS_ConvertUTF8toUTF16 localizedName{
+ connection ? LocalizeMediaName(*connection, media) : ""};
+
+ return PrintSettingsInitializer{
+ std::move(printerName),
+ MakePaperInfo(localizedName, media),
+ SupportsColor(),
+ };
+}
+
+nsTArray<mozilla::PaperInfo> nsPrinterCUPS::PaperList(
+ Connection& aConnection) const {
+ PrinterInfoLock lock = mPrinterInfoMutex.Lock();
+ http_t* const connection = aConnection.GetConnection(lock->mPrinter);
+ TryEnsurePrinterInfo(lock, connection);
+
+ if (!lock->mPrinterInfo) {
+ return {};
+ }
+
+ const int paperCount = mShim.cupsGetDestMediaCount
+ ? mShim.cupsGetDestMediaCount(
+ connection, lock->mPrinter,
+ lock->mPrinterInfo, CUPS_MEDIA_FLAGS_DEFAULT)
+ : 0;
+ nsTArray<PaperInfo> paperList;
+ nsTHashtable<nsCharPtrHashKey> paperSet(std::max(paperCount, 0));
+
+ paperList.SetCapacity(paperCount);
+ for (int i = 0; i < paperCount; ++i) {
+ cups_size_t media;
+ const int getInfoSucceeded = mShim.cupsGetDestMediaByIndex(
+ connection, lock->mPrinter, lock->mPrinterInfo, i,
+ CUPS_MEDIA_FLAGS_DEFAULT, &media);
+
+ if (!getInfoSucceeded || !paperSet.EnsureInserted(media.media)) {
+ continue;
+ }
+ // Check if this is a PWG paper size, in which case we can avoid using the
+ // CUPS localization methods.
+ const gfx::SizeDouble sizeDouble{
+ media.width * kPointsPerHundredthMillimeter,
+ media.length * kPointsPerHundredthMillimeter};
+ if (const PaperInfo* const paperInfo = FindCommonPaperSize(sizeDouble)) {
+ paperList.AppendElement(MakePaperInfo(paperInfo->mName, media));
+ } else {
+ const char* const mediaName =
+ connection ? LocalizeMediaName(*connection, media) : media.media;
+ paperList.AppendElement(
+ MakePaperInfo(NS_ConvertUTF8toUTF16(mediaName), media));
+ }
+ }
+
+ return paperList;
+}
+
+void nsPrinterCUPS::TryEnsurePrinterInfo(PrinterInfoLock& aLock,
+ http_t* const aConnection) const {
+ if (aLock->mPrinterInfo ||
+ (aConnection == CUPS_HTTP_DEFAULT ? aLock->mTriedInitWithDefault
+ : aLock->mTriedInitWithConnection)) {
+ return;
+ }
+
+ if (aConnection == CUPS_HTTP_DEFAULT) {
+ aLock->mTriedInitWithDefault = true;
+ } else {
+ aLock->mTriedInitWithConnection = true;
+ }
+
+ MOZ_ASSERT(aLock->mPrinter);
+
+ // httpGetAddress was only added in CUPS 2.0, and some systems still use
+ // CUPS 1.7.
+ if (aConnection && MOZ_LIKELY(mShim.httpGetAddress && mShim.httpAddrPort)) {
+ // This is a workaround for the CUPS Bug seen in bug 1691347.
+ // This is to avoid a null string being passed to strstr in CUPS. The path
+ // in CUPS that leads to this is as follows:
+ //
+ // In cupsCopyDestInfo, CUPS_DEST_FLAG_DEVICE is set when the connection is
+ // not null (same as CUPS_HTTP_DEFAULT), the print server is not the same
+ // as our hostname and is not path-based (starts with a '/'), or the IPP
+ // port is different than the global server IPP port.
+ //
+ // https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718-L722
+ //
+ // In _cupsGetDestResource, CUPS fetches the IPP options "device-uri" and
+ // "printer-uri-supported". Note that IPP options are returned as null when
+ // missing.
+ //
+ // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1138-L1141
+ //
+ // If the CUPS_DEST_FLAG_DEVICE is set or the "printer-uri-supported"
+ // option is not set, CUPS checks for "._tcp" in the "device-uri" option
+ // without doing a NULL-check first.
+ //
+ // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
+ //
+ // If we find that those branches will be taken, don't actually fetch the
+ // CUPS data and instead just return an empty printer info.
+
+ const char* const serverNameBytes = mShim.cupsServer();
+
+ if (MOZ_LIKELY(serverNameBytes)) {
+ const nsDependentCString serverName{serverNameBytes};
+
+ // We only need enough characters to determine equality with serverName.
+ // + 2 because we need one byte for the null-character, and we also want
+ // to get more characters of the host name than the server name if
+ // possible. Otherwise, if the hostname starts with the same text as the
+ // entire server name, it would compare equal when it's not.
+ const size_t hostnameMemLength = serverName.Length() + 2;
+ auto hostnameMem = MakeUnique<char[]>(hostnameMemLength);
+
+ // We don't expect httpGetHostname to return null when a connection is
+ // passed, but it's better not to make assumptions.
+ const char* const hostnameBytes = mShim.httpGetHostname(
+ aConnection, hostnameMem.get(), hostnameMemLength);
+
+ if (MOZ_LIKELY(hostnameBytes)) {
+ const nsDependentCString hostname{hostnameBytes};
+
+ // Attempt to match the condional at
+ // https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718
+ //
+ // To find the result of the comparison CUPS performs of
+ // `strcmp(http->hostname, cg->server)`, we use httpGetHostname to try
+ // to get the value of `http->hostname`, but this isn't quite the same.
+ // For local addresses, httpGetHostName will normalize the result to be
+ // localhost", rather than the actual value of `http->hostname`.
+ //
+ // https://github.com/apple/cups/blob/2201569857f225c9874bfae19713ffb2f4bdfdeb/cups/http-addr.c#L794-L818
+ //
+ // Because of this, if both serverName and hostname equal "localhost",
+ // then the actual hostname might be a different local address that CUPS
+ // normalized in httpGetHostName, and `http->hostname` won't be equal to
+ // `cg->server` in CUPS.
+ const bool namesMightNotMatch =
+ hostname != serverName || hostname == "localhost";
+ const bool portsDiffer =
+ mShim.httpAddrPort(mShim.httpGetAddress(aConnection)) !=
+ mShim.ippPort();
+ const bool cupsDestDeviceFlag =
+ (namesMightNotMatch && serverName[0] != '/') || portsDiffer;
+
+ // Match the conditional at
+ // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
+ // but if device-uri is null do not call into CUPS.
+ if ((cupsDestDeviceFlag ||
+ !FindCUPSOption(aLock, "printer-uri-supported")) &&
+ !FindCUPSOption(aLock, "device-uri")) {
+ return;
+ }
+ }
+ }
+ }
+
+ // All CUPS calls that take the printer info do null-checks internally, so we
+ // can fetch this info and only worry about the result of the later CUPS
+ // functions.
+ aLock->mPrinterInfo = mShim.cupsCopyDestInfo(aConnection, aLock->mPrinter);
+
+ // Even if we failed to fetch printer info, it is still possible we can talk
+ // to the print server and get its CUPS version.
+ FetchCUPSVersionForPrinter(mShim, aLock->mPrinter, aLock->mCUPSMajor,
+ aLock->mCUPSMinor, aLock->mCUPSPatch);
+}
+
+void nsPrinterCUPS::ForEachExtraMonochromeSetting(
+ FunctionRef<void(const nsACString&, const nsACString&)> aCallback) {
+ nsAutoCString pref;
+ Preferences::GetCString("print.cups.monochrome.extra_settings", pref);
+ if (pref.IsEmpty()) {
+ return;
+ }
+
+ for (const auto& pair : pref.Split(',')) {
+ auto splitter = pair.Split(':');
+ auto end = splitter.end();
+
+ auto key = splitter.begin();
+ if (key == end) {
+ continue;
+ }
+
+ auto value = ++splitter.begin();
+ if (value == end) {
+ continue;
+ }
+
+ aCallback(*key, *value);
+ }
+}
diff --git a/widget/nsPrinterCUPS.h b/widget/nsPrinterCUPS.h
new file mode 100644
index 0000000000..3ecf1cb982
--- /dev/null
+++ b/widget/nsPrinterCUPS.h
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 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/. */
+
+#ifndef nsPrinterCUPS_h___
+#define nsPrinterCUPS_h___
+
+#include "nsPrinterBase.h"
+#include "nsPrintSettingsImpl.h"
+#include "nsCUPSShim.h"
+#include "nsString.h"
+
+#include "mozilla/DataMutex.h"
+#include "mozilla/FunctionRef.h"
+#include "mozilla/RecursiveMutex.h"
+
+/**
+ * @brief Interface to help implementing nsIPrinter using a CUPS printer.
+ */
+class nsPrinterCUPS final : public nsPrinterBase {
+ public:
+ NS_IMETHOD GetName(nsAString& aName) override;
+ NS_IMETHOD GetSystemName(nsAString& aName) override;
+ bool SupportsDuplex() const final;
+ bool SupportsColor() const final;
+ bool SupportsMonochrome() const final;
+ bool SupportsCollation() const final;
+ PrinterInfo CreatePrinterInfo() const final;
+ MarginDouble GetMarginsForPaper(nsString aPaperId) const final {
+ MOZ_ASSERT_UNREACHABLE(
+ "The CUPS API requires us to always get the margin when fetching the "
+ "paper list so there should be no need to query it separately");
+ return {};
+ }
+
+ nsPrinterCUPS() = delete;
+
+ nsPrinterCUPS(const mozilla::CommonPaperInfoArray* aArray,
+ const nsCUPSShim& aShim, nsString aDisplayName,
+ cups_dest_t* aPrinter)
+ : nsPrinterBase(aArray),
+ mShim(aShim),
+ mDisplayName(std::move(aDisplayName)),
+ mPrinterInfoMutex(CUPSPrinterInfo{aPrinter},
+ "nsPrinterCUPS::mPrinterInfoMutex") {}
+
+ static void ForEachExtraMonochromeSetting(
+ mozilla::FunctionRef<void(const nsACString&, const nsACString&)>);
+
+ private:
+ struct CUPSPrinterInfo {
+ explicit constexpr CUPSPrinterInfo(cups_dest_t* aPrinter)
+ : mPrinter(aPrinter) {}
+ cups_dest_t* mPrinter;
+ cups_dinfo_t* mPrinterInfo = nullptr;
+ uint64_t mCUPSMajor = 0;
+ uint64_t mCUPSMinor = 0;
+ uint64_t mCUPSPatch = 0;
+
+ // Whether we have attempted to fetch mPrinterInfo with CUPS_HTTP_DEFAULT.
+ bool mTriedInitWithDefault = false;
+ // Whether we have attempted to fetch mPrinterInfo with a connection.
+ bool mTriedInitWithConnection = false;
+ CUPSPrinterInfo() = delete;
+ CUPSPrinterInfo(const CUPSPrinterInfo&) = delete;
+ CUPSPrinterInfo(CUPSPrinterInfo&& aOther)
+ : mPrinter(aOther.mPrinter),
+ mPrinterInfo(aOther.mPrinterInfo),
+ mCUPSMajor(aOther.mCUPSMajor),
+ mCUPSMinor(aOther.mCUPSMinor),
+ mCUPSPatch(aOther.mCUPSPatch) {
+ aOther.mPrinter = nullptr;
+ aOther.mPrinterInfo = nullptr;
+ }
+ };
+
+ using PrinterInfoMutex =
+ mozilla::DataMutexBase<CUPSPrinterInfo, mozilla::RecursiveMutex>;
+
+ using PrinterInfoLock = PrinterInfoMutex::AutoLock;
+
+ ~nsPrinterCUPS();
+
+ /**
+ * Retrieves the localized name for a given media (paper).
+ * Returns nullptr if the name cannot be localized.
+ */
+ const char* LocalizeMediaName(http_t& aConnection, cups_size_t& aMedia) const;
+
+ void GetPrinterName(nsAString& aName) const;
+
+ // Little util for getting support flags using the direct CUPS names.
+ bool Supports(const char* aOption, const char* aValue) const;
+
+ // Returns support value if CUPS meets the minimum version, otherwise returns
+ // |aDefault|
+ bool IsCUPSVersionAtLeast(uint64_t aCUPSMajor, uint64_t aCUPSMinor,
+ uint64_t aCUPSPatch) const;
+
+ const char* FindCUPSOption(PrinterInfoLock& aLock, const char* name) const {
+ const cups_dest_t* const printer = aLock->mPrinter;
+ return mShim.cupsGetOption(name, printer->num_options, printer->options);
+ }
+
+ class Connection {
+ public:
+ http_t* GetConnection(cups_dest_t* aDest);
+
+ inline explicit Connection(const nsCUPSShim& aShim) : mShim(aShim) {}
+ Connection() = delete;
+ ~Connection();
+
+ protected:
+ const nsCUPSShim& mShim;
+ http_t* mConnection = CUPS_HTTP_DEFAULT;
+ bool mWasInited = false;
+ };
+
+ PrintSettingsInitializer DefaultSettings(Connection& aConnection) const;
+ nsTArray<mozilla::PaperInfo> PaperList(Connection& aConnection) const;
+ /**
+ * Attempts to populate the CUPSPrinterInfo object.
+ * This usually works with the CUPS default connection,
+ * but has been known to require an established connection
+ * on older versions of Ubuntu (18 and below).
+ */
+ PrinterInfoLock TryEnsurePrinterInfo(
+ http_t* const aConnection = CUPS_HTTP_DEFAULT) const {
+ PrinterInfoLock lock = mPrinterInfoMutex.Lock();
+ TryEnsurePrinterInfo(lock, aConnection);
+ return lock;
+ }
+
+ /**
+ * TryEnsurePrinterInfo that uses a caller-provided PrinterInfoLock.
+ *
+ * This can be used to avoid unnecessarily redundant locking of
+ * mPrinterInfoLock when getting a connection through
+ * Connection::GetConnection and then passing that into TryEnsurePrinterInfo.
+ */
+ void TryEnsurePrinterInfo(PrinterInfoLock& aLock,
+ http_t* const aConnection) const;
+
+ const nsCUPSShim& mShim;
+ nsString mDisplayName;
+ mutable PrinterInfoMutex mPrinterInfoMutex;
+};
+
+// There's no standard setting in Core Printing for monochrome. Or rather,
+// there is (PMSetColorMode) but it does nothing. Similarly, the relevant gtk
+// setting only works on Windows, yay.
+//
+// So on CUPS the right setting to use depends on the print driver. So we set /
+// look for a variety of driver-specific keys that are known to work across
+// printers.
+//
+// We set all the known settings, because the alternative to that is parsing ppd
+// files from the printer and find the relevant known choices that can apply,
+// and that is a lot more complex, similarly sketchy (requires the same amount
+// of driver-specific knowledge), and requires using deprecated CUPS APIs.
+#define CUPS_EACH_MONOCHROME_PRINTER_SETTING(macro_) \
+ macro_("ColorModel", "Gray") /* Generic */ \
+ macro_("BRMonoColor", "Mono") /* Brother */ \
+ macro_("BRPrintQuality", "Black") /* Brother */ \
+ macro_("CNIJGrayScale", "1") /* Canon */ \
+ macro_("CNGrayscale", "True") /* Canon */ \
+ macro_("INK", "MONO") /* Epson */ \
+ macro_("HPColorMode", "GrayscalePrint") /* HP */ \
+ macro_("ColorMode", "Mono") /* Samsung */ \
+ macro_("PrintoutMode", "Normal.Gray") /* Foomatic */ \
+ macro_("ProcessColorModel", "Mono") /* Samsung */ \
+ macro_("ARCMode", "CMBW") /* Sharp */ \
+ macro_("XRXColor", "BW") /* Xerox */ \
+ macro_("XROutputColor", "PrintAsGrayscale") /* Xerox, bug 1676191#c32 */ \
+ macro_("SelectColor", "Grayscale") /* Konica Minolta */ \
+ macro_("OKControl", "Gray") /* Oki */ \
+ macro_("BLW", "TrueM") /* Lexmark */ \
+ macro_("EPRendering", "None") /* Epson */
+
+#endif /* nsPrinterCUPS_h___ */
diff --git a/widget/nsPrinterListBase.cpp b/widget/nsPrinterListBase.cpp
new file mode 100644
index 0000000000..a362474cb5
--- /dev/null
+++ b/widget/nsPrinterListBase.cpp
@@ -0,0 +1,168 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsPrinterListBase.h"
+#include "PrintBackgroundTask.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/intl/Localization.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "xpcpublic.h"
+
+using namespace mozilla;
+
+using mozilla::ErrorResult;
+using mozilla::intl::Localization;
+using PrinterInfo = nsPrinterListBase::PrinterInfo;
+using MarginDouble = mozilla::gfx::MarginDouble;
+
+nsPrinterListBase::nsPrinterListBase() = default;
+nsPrinterListBase::~nsPrinterListBase() = default;
+
+NS_IMPL_CYCLE_COLLECTION(nsPrinterListBase, mPrintersPromise)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsPrinterListBase)
+ NS_INTERFACE_MAP_ENTRY(nsIPrinterList)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPrinterList)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsPrinterListBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsPrinterListBase)
+
+namespace mozilla {
+
+template <>
+void ResolveOrReject(dom::Promise& aPromise, nsPrinterListBase& aList,
+ const nsTArray<PrinterInfo>& aInfo) {
+ nsTArray<RefPtr<nsIPrinter>> printers;
+ printers.SetCapacity(aInfo.Length());
+ for (auto& info : aInfo) {
+ printers.AppendElement(aList.CreatePrinter(info));
+ }
+ aPromise.MaybeResolve(printers);
+}
+
+template <>
+void ResolveOrReject(dom::Promise& aPromise, nsPrinterListBase& aList,
+ const Maybe<PrinterInfo>& aInfo) {
+ if (aInfo) {
+ aPromise.MaybeResolve(aList.CreatePrinter(aInfo.value()));
+ } else {
+ aPromise.MaybeRejectWithNotFoundError("Printer not found");
+ }
+}
+
+} // namespace mozilla
+
+NS_IMETHODIMP nsPrinterListBase::GetPrinters(JSContext* aCx,
+ Promise** aResult) {
+ EnsureCommonPaperInfo(aCx);
+ return mozilla::AsyncPromiseAttributeGetter(*this, mPrintersPromise, aCx,
+ aResult, "Printers"_ns,
+ &nsPrinterListBase::Printers);
+}
+
+NS_IMETHODIMP nsPrinterListBase::GetPrinterByName(const nsAString& aPrinterName,
+ JSContext* aCx,
+ Promise** aResult) {
+ EnsureCommonPaperInfo(aCx);
+ return PrintBackgroundTaskPromise(*this, aCx, aResult, "PrinterByName"_ns,
+ &nsPrinterListBase::PrinterByName,
+ nsString{aPrinterName});
+}
+
+NS_IMETHODIMP nsPrinterListBase::GetPrinterBySystemName(
+ const nsAString& aPrinterName, JSContext* aCx, Promise** aResult) {
+ EnsureCommonPaperInfo(aCx);
+ return PrintBackgroundTaskPromise(
+ *this, aCx, aResult, "PrinterBySystemName"_ns,
+ &nsPrinterListBase::PrinterBySystemName, nsString{aPrinterName});
+}
+
+NS_IMETHODIMP nsPrinterListBase::GetNamedOrDefaultPrinter(
+ const nsAString& aPrinterName, JSContext* aCx, Promise** aResult) {
+ EnsureCommonPaperInfo(aCx);
+ return PrintBackgroundTaskPromise(
+ *this, aCx, aResult, "NamedOrDefaultPrinter"_ns,
+ &nsPrinterListBase::NamedOrDefaultPrinter, nsString{aPrinterName});
+}
+
+Maybe<PrinterInfo> nsPrinterListBase::NamedOrDefaultPrinter(
+ nsString aName) const {
+ if (Maybe<PrinterInfo> value = PrinterByName(std::move(aName))) {
+ return value;
+ }
+
+ // Since the name had to be passed by-value, we can re-use it to fetch the
+ // default printer name, potentially avoiding an extra string allocation.
+ if (NS_SUCCEEDED(SystemDefaultPrinterName(aName))) {
+ return PrinterByName(std::move(aName));
+ }
+
+ return Nothing();
+}
+
+NS_IMETHODIMP nsPrinterListBase::GetFallbackPaperList(JSContext* aCx,
+ Promise** aResult) {
+ ErrorResult rv;
+ nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx);
+ RefPtr<Promise> promise = Promise::Create(global, rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ *aResult = nullptr;
+ return rv.StealNSResult();
+ }
+
+ EnsureCommonPaperInfo(aCx);
+ nsTArray<RefPtr<nsPaper>> papers;
+ papers.SetCapacity(nsPaper::kNumCommonPaperSizes);
+ for (const auto& info : *mCommonPaperInfo) {
+ papers.AppendElement(MakeRefPtr<nsPaper>(info));
+ }
+
+ promise->MaybeResolve(papers);
+ promise.forget(aResult);
+ return NS_OK;
+}
+
+void nsPrinterListBase::EnsureCommonPaperInfo(JSContext* aCx) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ if (mCommonPaperInfo) {
+ return;
+ }
+ RefPtr<CommonPaperInfoArray> localizedPaperInfo =
+ MakeRefPtr<CommonPaperInfoArray>();
+ CommonPaperInfoArray& paperArray = *localizedPaperInfo;
+ // Apply localization to the names while constructing the PaperInfo, if
+ // available (otherwise leave them as the internal keys, which are at least
+ // somewhat recognizable).
+ IgnoredErrorResult rv;
+ nsTArray<nsCString> resIds = {
+ "toolkit/printing/printUI.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+
+ for (auto i : IntegerRange(nsPaper::kNumCommonPaperSizes)) {
+ const CommonPaperSize& size = nsPaper::kCommonPaperSizes[i];
+ PaperInfo& info = paperArray[i];
+
+ nsAutoCString key{"printui-paper-"};
+ key.Append(size.mLocalizableNameKey);
+ nsAutoCString name;
+ l10n->FormatValueSync(key, {}, name, rv);
+
+ // Fill out the info with our PWG size and the localized name.
+ info.mId = size.mPWGName;
+ CopyUTF8toUTF16(
+ (rv.Failed() || name.IsEmpty())
+ ? static_cast<const nsCString&>(size.mLocalizableNameKey)
+ : name,
+ info.mName);
+ info.mSize = size.mSize;
+ info.mUnwriteableMargin = Some(MarginDouble{});
+ }
+ mCommonPaperInfo = std::move(localizedPaperInfo);
+}
diff --git a/widget/nsPrinterListBase.h b/widget/nsPrinterListBase.h
new file mode 100644
index 0000000000..9f922d74b5
--- /dev/null
+++ b/widget/nsPrinterListBase.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsPrinterListBase_h__
+#define nsPrinterListBase_h__
+
+#include "nsIPrinterList.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsPaper.h"
+#include "nsString.h"
+
+class nsPrinterListBase : public nsIPrinterList {
+ public:
+ using Promise = mozilla::dom::Promise;
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsPrinterListBase)
+ NS_IMETHOD GetSystemDefaultPrinterName(nsAString& aName) final {
+ return SystemDefaultPrinterName(aName);
+ }
+ NS_IMETHOD GetPrinters(JSContext*, Promise**) final;
+ NS_IMETHOD GetPrinterByName(const nsAString& aPrinterName, JSContext* aCx,
+ Promise** aResult) final;
+ NS_IMETHOD GetPrinterBySystemName(const nsAString& aPrinterName,
+ JSContext* aCx, Promise** aResult) final;
+ NS_IMETHOD GetNamedOrDefaultPrinter(const nsAString& aPrinterName,
+ JSContext* aCx, Promise** aResult) final;
+ NS_IMETHOD GetFallbackPaperList(JSContext*, Promise**) final;
+
+ struct PrinterInfo {
+ // Both windows and CUPS: The name of the printer.
+ nsString mName;
+ // CUPS only: Handle to owned cups_dest_t.
+ void* mCupsHandle = nullptr;
+ };
+
+ // Called off the main thread, collect information to create an appropriate
+ // list of printers.
+ virtual nsTArray<PrinterInfo> Printers() const = 0;
+
+ // Create an nsIPrinter object given the information we obtained from the
+ // background thread.
+ virtual RefPtr<nsIPrinter> CreatePrinter(PrinterInfo) const = 0;
+
+ mozilla::Maybe<PrinterInfo> NamedOrDefaultPrinter(nsString aName) const;
+
+ // No copy or move, we're an identity.
+ nsPrinterListBase(const nsPrinterListBase&) = delete;
+ nsPrinterListBase(nsPrinterListBase&&) = delete;
+
+ protected:
+ nsPrinterListBase();
+ virtual ~nsPrinterListBase();
+
+ // This could be implemented in terms of Printers() and then searching the
+ // returned printer info for a printer of the given name, but we expect
+ // backends to have more efficient methods of implementing this.
+ virtual mozilla::Maybe<PrinterInfo> PrinterByName(nsString aName) const = 0;
+
+ // Same as NamedPrinter, but uses the system name.
+ // Depending on whether or not there is a more efficient way to address the
+ // printer for a given backend, this may or may not be equivalent to
+ // NamedPrinter.
+ virtual mozilla::Maybe<PrinterInfo> PrinterBySystemName(
+ nsString aName) const = 0;
+
+ // This is implemented separately from the IDL interface version so that it
+ // can be made const, which allows it to be used while resolving promises.
+ virtual nsresult SystemDefaultPrinterName(nsAString&) const = 0;
+
+ // Return "paper" sizes to be supported by the Save to PDF destination;
+ // for actual printer drivers the list is retrieved from nsIPrinter.
+ nsTArray<RefPtr<nsPaper>> FallbackPaperList() const;
+
+ // Constructs mCommonPaperInfo by localizing the sizes in
+ // nsPaper::kCommonPaperSizes and creating corresponding PaperInfo.
+ void EnsureCommonPaperInfo(JSContext* aCx);
+
+ RefPtr<Promise> mPrintersPromise;
+ // PaperInfo for our fallback sizes and common size localization.
+ // This field contains the same data for every instance of this class.
+ // It's unfortunate that this needs to be a member rather than static data
+ // like nsPaper::kCommonPaperSizes, but that's because PaperInfo contains
+ // localized data, and we need a JSContext to do the localization.
+ RefPtr<const mozilla::CommonPaperInfoArray> mCommonPaperInfo;
+};
+
+#endif
diff --git a/widget/nsPrinterListCUPS.cpp b/widget/nsPrinterListCUPS.cpp
new file mode 100644
index 0000000000..2629d7626e
--- /dev/null
+++ b/widget/nsPrinterListCUPS.cpp
@@ -0,0 +1,233 @@
+/* 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 "nsPrinterListCUPS.h"
+
+#include "mozilla/IntegerRange.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPrefs_print.h"
+#include "nsCUPSShim.h"
+#include "nsPrinterCUPS.h"
+#include "nsString.h"
+#include "prenv.h"
+
+// Use a local static to initialize the CUPS shim lazily, when it's needed.
+// This is used in order to avoid a global constructor.
+static nsCUPSShim& CupsShim() {
+ static nsCUPSShim sCupsShim;
+ return sCupsShim;
+}
+
+using PrinterInfo = nsPrinterListBase::PrinterInfo;
+
+/**
+ * Retrieves a human-readable name for the printer from CUPS.
+ * https://www.cups.org/doc/cupspm.html#basic-destination-information
+ */
+static void GetDisplayNameForPrinter(const cups_dest_t& aDest,
+ nsAString& aName) {
+// macOS clients expect prettified printer names
+// while GTK clients expect non-prettified names.
+// If you change this, please change NamedPrinter accordingly.
+#ifdef XP_MACOSX
+ const char* displayName = CupsShim().cupsGetOption(
+ "printer-info", aDest.num_options, aDest.options);
+ if (displayName) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(displayName), aName);
+ }
+#endif
+}
+
+NS_IMETHODIMP
+nsPrinterListCUPS::InitPrintSettingsFromPrinter(
+ const nsAString& aPrinterName, nsIPrintSettings* aPrintSettings) {
+ MOZ_ASSERT(aPrintSettings);
+
+ // Set a default file name.
+ nsAutoString filename;
+ nsresult rv = aPrintSettings->GetToFileName(filename);
+ if (NS_FAILED(rv) || filename.IsEmpty()) {
+ const char* path = PR_GetEnv("PWD");
+ if (!path) {
+ path = PR_GetEnv("HOME");
+ }
+
+ if (path) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(path), filename);
+ filename.AppendLiteral("/mozilla.pdf");
+ } else {
+ filename.AssignLiteral("mozilla.pdf");
+ }
+
+ aPrintSettings->SetToFileName(filename);
+ }
+
+ aPrintSettings->SetIsInitializedFromPrinter(true);
+ return NS_OK;
+}
+
+static int CupsDestCallback(void* user_data, unsigned aFlags,
+ cups_dest_t* aDest) {
+ MOZ_ASSERT(user_data);
+ auto* printerInfoList = static_cast<nsTArray<PrinterInfo>*>(user_data);
+
+ cups_dest_t* ownedDest = nullptr;
+ mozilla::DebugOnly<const int> numCopied =
+ CupsShim().cupsCopyDest(aDest, 0, &ownedDest);
+ MOZ_ASSERT(numCopied == 1);
+
+ nsString name;
+ GetDisplayNameForPrinter(*aDest, name);
+
+ printerInfoList->AppendElement(PrinterInfo{std::move(name), ownedDest});
+
+ return aFlags == CUPS_DEST_FLAGS_MORE ? 1 : 0;
+}
+
+nsTArray<PrinterInfo> nsPrinterListCUPS::Printers() const {
+ if (!CupsShim().InitOkay()) {
+ return {};
+ }
+
+ auto FreeDestsAndClear = [](nsTArray<PrinterInfo>& aArray) {
+ for (auto& info : aArray) {
+ CupsShim().cupsFreeDests(1, static_cast<cups_dest_t*>(info.mCupsHandle));
+ }
+ aArray.Clear();
+ };
+
+ nsTArray<PrinterInfo> printerInfoList;
+ // cupsGetDests2 returns list of found printers without duplicates, unlike
+ // cupsEnumDests
+ cups_dest_t* printers = nullptr;
+ const auto numPrinters = CupsShim().cupsGetDests2(nullptr, &printers);
+ if (numPrinters > 0) {
+ for (auto i : mozilla::IntegerRange(0, numPrinters)) {
+ cups_dest_t* ownedDest = nullptr;
+ mozilla::DebugOnly<const int> numCopied =
+ CupsShim().cupsCopyDest(printers + i, 0, &ownedDest);
+ MOZ_ASSERT(numCopied == 1);
+
+ nsString name;
+ GetDisplayNameForPrinter(*(printers + i), name);
+ printerInfoList.AppendElement(PrinterInfo{std::move(name), ownedDest});
+ }
+ CupsShim().cupsFreeDests(numPrinters, printers);
+ return printerInfoList;
+ }
+
+ // An error occurred - retry with CUPS_PRINTER_DISCOVERED masked out (since
+ // it looks like there are a lot of error cases for that in cupsEnumDests):
+ if (CupsShim().cupsEnumDests(
+ CUPS_DEST_FLAGS_NONE,
+ 0 /* 0 timeout should be okay when masking CUPS_PRINTER_DISCOVERED */,
+ nullptr /* cancel* */, CUPS_PRINTER_LOCAL,
+ CUPS_PRINTER_FAX | CUPS_PRINTER_SCANNER | CUPS_PRINTER_DISCOVERED,
+ &CupsDestCallback, &printerInfoList)) {
+ return printerInfoList;
+ }
+
+ // Another error occurred. Maybe printerInfoList could be partially
+ // populated, so perhaps we could return it without clearing it in the hope
+ // that there are some usable dests. However, presuambly CUPS doesn't
+ // guarantee that any dests that it added are complete and safe to use when
+ // an error occurs?
+ FreeDestsAndClear(printerInfoList);
+ return {};
+}
+
+RefPtr<nsIPrinter> nsPrinterListCUPS::CreatePrinter(PrinterInfo aInfo) const {
+ return mozilla::MakeRefPtr<nsPrinterCUPS>(
+ mCommonPaperInfo, CupsShim(), std::move(aInfo.mName),
+ static_cast<cups_dest_t*>(aInfo.mCupsHandle));
+}
+
+mozilla::Maybe<PrinterInfo> nsPrinterListCUPS::PrinterByName(
+ nsString aPrinterName) const {
+ mozilla::Maybe<PrinterInfo> rv;
+ if (!CupsShim().InitOkay()) {
+ return rv;
+ }
+
+ // Will contain the printer, if found. This must be fully owned, and not a
+ // member of another array of printers.
+ cups_dest_t* printer = nullptr;
+
+#ifdef XP_MACOSX
+ // On OS X the printer name given to this function is the readable/display
+ // name and not the CUPS name, so we iterate over all the printers for now.
+ // See bug 1659807 for one approach to improve perf here.
+ {
+ nsAutoCString printerName;
+ CopyUTF16toUTF8(aPrinterName, printerName);
+ cups_dest_t* printers = nullptr;
+ const auto numPrinters = CupsShim().cupsGetDests(&printers);
+ for (auto i : mozilla::IntegerRange(0, numPrinters)) {
+ const char* const displayName = CupsShim().cupsGetOption(
+ "printer-info", printers[i].num_options, printers[i].options);
+ if (printerName == displayName) {
+ // The second arg to CupsShim().cupsCopyDest is called num_dests, but
+ // it actually copies num_dests + 1 elements.
+ CupsShim().cupsCopyDest(printers + i, 0, &printer);
+ break;
+ }
+ }
+ CupsShim().cupsFreeDests(numPrinters, printers);
+ }
+#else
+ // On GTK, we only ever show the CUPS name of printers, so we can use
+ // cupsGetNamedDest directly.
+ {
+ const auto printerName = NS_ConvertUTF16toUTF8(aPrinterName);
+ printer = CupsShim().cupsGetNamedDest(CUPS_HTTP_DEFAULT, printerName.get(),
+ nullptr);
+ }
+#endif
+
+ if (printer) {
+ // Since the printer name had to be passed by-value, we can move the
+ // name from that.
+ rv.emplace(PrinterInfo{std::move(aPrinterName), printer});
+ }
+ return rv;
+}
+
+mozilla::Maybe<PrinterInfo> nsPrinterListCUPS::PrinterBySystemName(
+ nsString aPrinterName) const {
+ mozilla::Maybe<PrinterInfo> rv;
+ if (!CupsShim().InitOkay()) {
+ return rv;
+ }
+
+ const auto printerName = NS_ConvertUTF16toUTF8(aPrinterName);
+ if (cups_dest_t* const printer = CupsShim().cupsGetNamedDest(
+ CUPS_HTTP_DEFAULT, printerName.get(), nullptr)) {
+ rv.emplace(PrinterInfo{std::move(aPrinterName), printer});
+ }
+ return rv;
+}
+
+nsresult nsPrinterListCUPS::SystemDefaultPrinterName(nsAString& aName) const {
+ aName.Truncate();
+
+ if (!CupsShim().InitOkay()) {
+ return NS_OK;
+ }
+
+ // Passing in nullptr for the name will return the default, if any.
+ cups_dest_t* dest =
+ CupsShim().cupsGetNamedDest(CUPS_HTTP_DEFAULT, /* name */ nullptr,
+ /* instance */ nullptr);
+ if (!dest) {
+ return NS_OK;
+ }
+
+ GetDisplayNameForPrinter(*dest, aName);
+ if (aName.IsEmpty()) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(dest->name), aName);
+ }
+
+ CupsShim().cupsFreeDests(1, dest);
+ return NS_OK;
+}
diff --git a/widget/nsPrinterListCUPS.h b/widget/nsPrinterListCUPS.h
new file mode 100644
index 0000000000..77fff571c6
--- /dev/null
+++ b/widget/nsPrinterListCUPS.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsPrinterListCUPS_h__
+#define nsPrinterListCUPS_h__
+
+#include "nsPrinterListBase.h"
+#include "nsStringFwd.h"
+
+namespace mozilla {
+template <typename T>
+class Maybe;
+} // namespace mozilla
+
+class nsPrinterListCUPS final : public nsPrinterListBase {
+ NS_IMETHOD InitPrintSettingsFromPrinter(const nsAString&,
+ nsIPrintSettings*) final;
+
+ nsTArray<PrinterInfo> Printers() const final;
+ RefPtr<nsIPrinter> CreatePrinter(PrinterInfo) const final;
+ mozilla::Maybe<PrinterInfo> PrinterByName(nsString aPrinterName) const final;
+ mozilla::Maybe<PrinterInfo> PrinterBySystemName(
+ nsString aPrinterName) const final;
+ nsresult SystemDefaultPrinterName(nsAString&) const final;
+
+ private:
+ ~nsPrinterListCUPS() override = default;
+};
+
+#endif
diff --git a/widget/nsTransferable.cpp b/widget/nsTransferable.cpp
new file mode 100644
index 0000000000..58a8630801
--- /dev/null
+++ b/widget/nsTransferable.cpp
@@ -0,0 +1,561 @@
+/* -*- 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/. */
+
+/*
+Notes to self:
+
+- at some point, strings will be accessible from JS, so we won't have to wrap
+ flavors in an nsISupportsCString. Until then, we're kinda stuck with
+ this crappy API of nsIArrays.
+
+*/
+
+#include "nsTransferable.h"
+#include "nsAnonymousTemporaryFile.h"
+#include "nsArray.h"
+#include "nsArrayUtils.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsTArray.h"
+#include "nsIFormatConverter.h"
+#include "nsIContentPolicy.h"
+#include "nsCOMPtr.h"
+#include "nsXPCOM.h"
+#include "nsISupportsPrimitives.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryService.h"
+#include "nsCRT.h"
+#include "nsNetUtil.h"
+#include "nsILoadContext.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/UniquePtr.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsTransferable, nsITransferable)
+
+DataStruct::DataStruct(DataStruct&& aRHS)
+ : mData(aRHS.mData.forget()),
+ mCacheFD(aRHS.mCacheFD),
+ mFlavor(aRHS.mFlavor) {
+ aRHS.mCacheFD = nullptr;
+}
+
+//-------------------------------------------------------------------------
+DataStruct::~DataStruct() {
+ if (mCacheFD) {
+ PR_Close(mCacheFD);
+ }
+}
+
+//-------------------------------------------------------------------------
+
+void DataStruct::SetData(nsISupports* aData, bool aIsPrivateData) {
+ // Now, check to see if we consider the data to be "too large"
+ // as well as ensuring that private browsing mode is disabled.
+ // File IO is not allowed in content processes.
+ if (!aIsPrivateData && XRE_IsParentProcess()) {
+ void* data = nullptr;
+ uint32_t dataLen = 0;
+ nsPrimitiveHelpers::CreateDataFromPrimitive(mFlavor, aData, &data,
+ &dataLen);
+
+ if (dataLen > kLargeDatasetSize) {
+ // Too large, cache it to disk instead of memory.
+ if (NS_SUCCEEDED(WriteCache(data, dataLen))) {
+ free(data);
+ // Clear previously set small data.
+ mData = nullptr;
+ return;
+ }
+
+ NS_WARNING("Oh no, couldn't write data to the cache file");
+ }
+
+ free(data);
+ }
+
+ if (mCacheFD) {
+ // Clear previously set big data.
+ PR_Close(mCacheFD);
+ mCacheFD = nullptr;
+ }
+
+ mData = aData;
+}
+
+//-------------------------------------------------------------------------
+void DataStruct::GetData(nsISupports** aData) {
+ // check here to see if the data is cached on disk
+ if (mCacheFD) {
+ // if so, read it in and pass it back
+ // ReadCache creates memory and copies the data into it.
+ if (NS_SUCCEEDED(ReadCache(aData))) {
+ return;
+ }
+
+ // oh shit, something went horribly wrong here.
+ NS_WARNING("Oh no, couldn't read data in from the cache file");
+ *aData = nullptr;
+ PR_Close(mCacheFD);
+ mCacheFD = nullptr;
+ return;
+ }
+
+ nsCOMPtr<nsISupports> data = mData;
+ data.forget(aData);
+}
+
+//-------------------------------------------------------------------------
+void DataStruct::ClearData() {
+ if (mCacheFD) {
+ PR_Close(mCacheFD);
+ }
+ mData = nullptr;
+}
+
+//-------------------------------------------------------------------------
+nsresult DataStruct::WriteCache(void* aData, uint32_t aDataLen) {
+ MOZ_ASSERT(aData && aDataLen);
+ MOZ_ASSERT(aDataLen <= uint32_t(std::numeric_limits<int32_t>::max()),
+ "too large size for PR_Write");
+
+ nsresult rv;
+ if (!mCacheFD) {
+ rv = NS_OpenAnonymousTemporaryFile(&mCacheFD);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (PR_Seek64(mCacheFD, 0, PR_SEEK_SET) == -1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Write out the contents of the clipboard to the file.
+ int32_t written = PR_Write(mCacheFD, aData, aDataLen);
+ if (written == int32_t(aDataLen)) {
+ return NS_OK;
+ }
+
+ PR_Close(mCacheFD);
+ mCacheFD = nullptr;
+ return NS_ERROR_FAILURE;
+}
+
+//-------------------------------------------------------------------------
+nsresult DataStruct::ReadCache(nsISupports** aData) {
+ if (!mCacheFD) {
+ return NS_ERROR_FAILURE;
+ }
+
+ PRFileInfo fileInfo;
+ if (PR_GetOpenFileInfo(mCacheFD, &fileInfo) != PR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+ if (PR_Seek64(mCacheFD, 0, PR_SEEK_SET) == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ uint32_t fileSize = fileInfo.size;
+
+ auto data = MakeUnique<char[]>(fileSize);
+ if (!data) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ uint32_t actual = PR_Read(mCacheFD, data.get(), fileSize);
+ if (actual != fileSize) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsPrimitiveHelpers::CreatePrimitiveForData(mFlavor, data.get(), fileSize,
+ aData);
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+//
+// Transferable constructor
+//
+//-------------------------------------------------------------------------
+nsTransferable::nsTransferable()
+ : mPrivateData(false),
+ mContentPolicyType(nsIContentPolicy::TYPE_OTHER)
+#ifdef DEBUG
+ ,
+ mInitialized(false)
+#endif
+{
+}
+
+//-------------------------------------------------------------------------
+//
+// Transferable destructor
+//
+//-------------------------------------------------------------------------
+nsTransferable::~nsTransferable() = default;
+
+NS_IMETHODIMP
+nsTransferable::Init(nsILoadContext* aContext) {
+ MOZ_ASSERT(!mInitialized);
+
+ if (aContext) {
+ mPrivateData = aContext->UsePrivateBrowsing();
+ } else {
+ // without aContext here to provide PrivateBrowsing information, we defer to
+ // the active configured setting
+ mPrivateData = StaticPrefs::browser_privatebrowsing_autostart();
+ }
+#ifdef DEBUG
+ mInitialized = true;
+#endif
+ return NS_OK;
+}
+
+//
+// GetTransferDataFlavors
+//
+// Returns a copy of the internal list of flavors. This does NOT take into
+// account any converter that may be registered.
+//
+void nsTransferable::GetTransferDataFlavors(nsTArray<nsCString>& aFlavors) {
+ MOZ_ASSERT(mInitialized);
+
+ for (size_t i = 0; i < mDataArray.Length(); ++i) {
+ DataStruct& data = mDataArray.ElementAt(i);
+ aFlavors.AppendElement(data.GetFlavor());
+ }
+}
+
+Maybe<size_t> nsTransferable::FindDataFlavor(const char* aFlavor) {
+ nsDependentCString flavor(aFlavor);
+
+ for (size_t i = 0; i < mDataArray.Length(); ++i) {
+ if (mDataArray[i].GetFlavor().Equals(flavor)) {
+ return Some(i);
+ }
+ }
+
+ return Nothing();
+}
+
+//
+// GetTransferData
+//
+// Returns the data of the requested flavor, obtained from either having the
+// data on hand or using a converter to get it. The data is wrapped in a
+// nsISupports primitive so that it is accessible from JS.
+//
+NS_IMETHODIMP
+nsTransferable::GetTransferData(const char* aFlavor, nsISupports** aData) {
+ MOZ_ASSERT(mInitialized);
+
+ *aData = nullptr;
+
+ nsresult rv = NS_OK;
+
+ // First look and see if the data is present in one of the intrinsic flavors.
+ if (Maybe<size_t> index = FindDataFlavor(aFlavor)) {
+ nsCOMPtr<nsISupports> dataBytes;
+ mDataArray[index.value()].GetData(getter_AddRefs(dataBytes));
+
+ // Do we have a (lazy) data provider?
+ if (nsCOMPtr<nsIFlavorDataProvider> dataProvider =
+ do_QueryInterface(dataBytes)) {
+ rv =
+ dataProvider->GetFlavorData(this, aFlavor, getter_AddRefs(dataBytes));
+ if (NS_FAILED(rv)) {
+ dataBytes = nullptr;
+ // The provider failed, fall into the converter code below.
+ }
+ }
+
+ if (dataBytes) {
+ dataBytes.forget(aData);
+ return NS_OK;
+ }
+
+ // Empty data
+ }
+
+ // If not, try using a format converter to get the requested flavor.
+ if (mFormatConv) {
+ for (size_t i = 0; i < mDataArray.Length(); ++i) {
+ DataStruct& data = mDataArray.ElementAt(i);
+ bool canConvert = false;
+ mFormatConv->CanConvert(data.GetFlavor().get(), aFlavor, &canConvert);
+ if (canConvert) {
+ nsCOMPtr<nsISupports> dataBytes;
+ data.GetData(getter_AddRefs(dataBytes));
+
+ // Do we have a (lazy) data provider?
+ if (nsCOMPtr<nsIFlavorDataProvider> dataProvider =
+ do_QueryInterface(dataBytes)) {
+ rv = dataProvider->GetFlavorData(this, aFlavor,
+ getter_AddRefs(dataBytes));
+ if (NS_FAILED(rv)) {
+ // Give up.
+ return rv;
+ }
+ }
+
+ return mFormatConv->Convert(data.GetFlavor().get(), dataBytes, aFlavor,
+ aData);
+ }
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+//
+// GetAnyTransferData
+//
+// Returns the data of the first flavor found. Caller is responsible for
+// deleting the flavor string.
+//
+NS_IMETHODIMP
+nsTransferable::GetAnyTransferData(nsACString& aFlavor, nsISupports** aData) {
+ MOZ_ASSERT(mInitialized);
+
+ for (size_t i = 0; i < mDataArray.Length(); ++i) {
+ DataStruct& data = mDataArray.ElementAt(i);
+ if (data.IsDataAvailable()) {
+ aFlavor.Assign(data.GetFlavor());
+ data.GetData(aData);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+//
+// SetTransferData
+//
+//
+//
+NS_IMETHODIMP
+nsTransferable::SetTransferData(const char* aFlavor, nsISupports* aData) {
+ MOZ_ASSERT(mInitialized);
+
+ // first check our intrinsic flavors to see if one has been registered.
+ if (Maybe<size_t> index = FindDataFlavor(aFlavor)) {
+ DataStruct& data = mDataArray.ElementAt(index.value());
+ data.SetData(aData, mPrivateData);
+ return NS_OK;
+ }
+
+ // if not, try using a format converter to find a flavor to put the data in
+ if (mFormatConv) {
+ for (size_t i = 0; i < mDataArray.Length(); ++i) {
+ DataStruct& data = mDataArray.ElementAt(i);
+ bool canConvert = false;
+ mFormatConv->CanConvert(aFlavor, data.GetFlavor().get(), &canConvert);
+
+ if (canConvert) {
+ nsCOMPtr<nsISupports> ConvertedData;
+ mFormatConv->Convert(aFlavor, aData, data.GetFlavor().get(),
+ getter_AddRefs(ConvertedData));
+ data.SetData(ConvertedData, mPrivateData);
+ return NS_OK;
+ }
+ }
+ }
+
+ // Can't set data neither directly nor through converter. Just add this flavor
+ // and try again
+ if (NS_SUCCEEDED(AddDataFlavor(aFlavor))) {
+ return SetTransferData(aFlavor, aData);
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsTransferable::ClearAllData() {
+ for (auto& entry : mDataArray) {
+ entry.ClearData();
+ }
+ return NS_OK;
+}
+
+//
+// AddDataFlavor
+//
+// Adds a data flavor to our list with no data. Error if it already exists.
+//
+NS_IMETHODIMP
+nsTransferable::AddDataFlavor(const char* aDataFlavor) {
+ MOZ_ASSERT(mInitialized);
+
+ if (FindDataFlavor(aDataFlavor).isSome()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create a new "slot" for the data
+ mDataArray.AppendElement(DataStruct(aDataFlavor));
+ return NS_OK;
+}
+
+//
+// RemoveDataFlavor
+//
+// Removes a data flavor (and causes the data to be destroyed). Error if
+// the requested flavor is not present.
+//
+NS_IMETHODIMP
+nsTransferable::RemoveDataFlavor(const char* aDataFlavor) {
+ MOZ_ASSERT(mInitialized);
+
+ if (Maybe<size_t> index = FindDataFlavor(aDataFlavor)) {
+ mDataArray.RemoveElementAt(index.value());
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsTransferable::SetConverter(nsIFormatConverter* aConverter) {
+ MOZ_ASSERT(mInitialized);
+
+ mFormatConv = aConverter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransferable::GetConverter(nsIFormatConverter** aConverter) {
+ MOZ_ASSERT(mInitialized);
+
+ nsCOMPtr<nsIFormatConverter> converter = mFormatConv;
+ converter.forget(aConverter);
+ return NS_OK;
+}
+
+//
+// FlavorsTransferableCanImport
+//
+// Computes a list of flavors that the transferable can accept into it, either
+// through intrinsic knowledge or input data converters.
+//
+NS_IMETHODIMP
+nsTransferable::FlavorsTransferableCanImport(nsTArray<nsCString>& aFlavors) {
+ MOZ_ASSERT(mInitialized);
+
+ // Get the flavor list, and on to the end of it, append the list of flavors we
+ // can also get to through a converter. This is so that we can just walk the
+ // list in one go, looking for the desired flavor.
+ GetTransferDataFlavors(aFlavors);
+
+ if (mFormatConv) {
+ nsTArray<nsCString> convertedList;
+ mFormatConv->GetInputDataFlavors(convertedList);
+
+ for (uint32_t i = 0; i < convertedList.Length(); ++i) {
+ nsCString& flavorStr = convertedList[i];
+
+ // Don't append if already in intrinsic list
+ if (!aFlavors.Contains(flavorStr)) {
+ aFlavors.AppendElement(flavorStr);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+//
+// FlavorsTransferableCanExport
+//
+// Computes a list of flavors that the transferable can export, either through
+// intrinsic knowledge or output data converters.
+//
+NS_IMETHODIMP
+nsTransferable::FlavorsTransferableCanExport(nsTArray<nsCString>& aFlavors) {
+ MOZ_ASSERT(mInitialized);
+
+ // Get the flavor list, and on to the end of it, append the list of flavors we
+ // can also get to through a converter. This is so that we can just walk the
+ // list in one go, looking for the desired flavor.
+ GetTransferDataFlavors(aFlavors);
+
+ if (mFormatConv) {
+ nsTArray<nsCString> convertedList;
+ mFormatConv->GetOutputDataFlavors(convertedList);
+
+ for (uint32_t i = 0; i < convertedList.Length(); ++i) {
+ nsCString& flavorStr = convertedList[i];
+
+ // Don't append if already in intrinsic list
+ if (!aFlavors.Contains(flavorStr)) {
+ aFlavors.AppendElement(flavorStr);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+bool nsTransferable::GetIsPrivateData() {
+ MOZ_ASSERT(mInitialized);
+
+ return mPrivateData;
+}
+
+void nsTransferable::SetIsPrivateData(bool aIsPrivateData) {
+ MOZ_ASSERT(mInitialized);
+
+ mPrivateData = aIsPrivateData;
+}
+
+nsIPrincipal* nsTransferable::GetRequestingPrincipal() {
+ MOZ_ASSERT(mInitialized);
+
+ return mRequestingPrincipal;
+}
+
+void nsTransferable::SetRequestingPrincipal(
+ nsIPrincipal* aRequestingPrincipal) {
+ MOZ_ASSERT(mInitialized);
+
+ mRequestingPrincipal = aRequestingPrincipal;
+}
+
+nsContentPolicyType nsTransferable::GetContentPolicyType() {
+ MOZ_ASSERT(mInitialized);
+
+ return mContentPolicyType;
+}
+
+void nsTransferable::SetContentPolicyType(
+ nsContentPolicyType aContentPolicyType) {
+ MOZ_ASSERT(mInitialized);
+
+ mContentPolicyType = aContentPolicyType;
+}
+
+nsICookieJarSettings* nsTransferable::GetCookieJarSettings() {
+ MOZ_ASSERT(mInitialized);
+
+ return mCookieJarSettings;
+}
+
+void nsTransferable::SetCookieJarSettings(
+ nsICookieJarSettings* aCookieJarSettings) {
+ MOZ_ASSERT(mInitialized);
+
+ mCookieJarSettings = aCookieJarSettings;
+}
+
+nsIReferrerInfo* nsTransferable::GetReferrerInfo() {
+ MOZ_ASSERT(mInitialized);
+ return mReferrerInfo;
+}
+
+void nsTransferable::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) {
+ MOZ_ASSERT(mInitialized);
+ mReferrerInfo = aReferrerInfo;
+}
diff --git a/widget/nsTransferable.h b/widget/nsTransferable.h
new file mode 100644
index 0000000000..02f2b0459c
--- /dev/null
+++ b/widget/nsTransferable.h
@@ -0,0 +1,92 @@
+/* -*- 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/. */
+
+#ifndef nsTransferable_h__
+#define nsTransferable_h__
+
+#include "nsICookieJarSettings.h"
+#include "nsIFormatConverter.h"
+#include "nsITransferable.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsIPrincipal.h"
+#include "nsIReferrerInfo.h"
+#include "prio.h"
+#include "mozilla/Maybe.h"
+
+class nsIMutableArray;
+
+//
+// DataStruct
+//
+// Holds a flavor (a mime type) that describes the data and the associated data.
+//
+struct DataStruct {
+ explicit DataStruct(const char* aFlavor)
+ : mCacheFD(nullptr), mFlavor(aFlavor) {}
+ DataStruct(DataStruct&& aRHS);
+ ~DataStruct();
+
+ const nsCString& GetFlavor() const { return mFlavor; }
+ void SetData(nsISupports* aData, bool aIsPrivateData);
+ void GetData(nsISupports** aData);
+ void ClearData();
+ bool IsDataAvailable() const { return mData || mCacheFD; }
+
+ protected:
+ enum {
+ // The size of data over which we write the data to disk rather than
+ // keep it around in memory.
+ kLargeDatasetSize = 1000000 // 1 million bytes
+ };
+
+ nsresult WriteCache(void* aData, uint32_t aDataLen);
+ nsresult ReadCache(nsISupports** aData);
+
+ // mData OR mCacheFD should be used, not both.
+ nsCOMPtr<nsISupports> mData; // OWNER - some varient of primitive wrapper
+ PRFileDesc* mCacheFD;
+ const nsCString mFlavor;
+
+ private:
+ DataStruct(const DataStruct&) = delete;
+ DataStruct& operator=(const DataStruct&) = delete;
+};
+
+/**
+ * XP Transferable wrapper
+ */
+
+class nsTransferable : public nsITransferable {
+ public:
+ nsTransferable();
+
+ // nsISupports
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITRANSFERABLE
+
+ protected:
+ virtual ~nsTransferable();
+
+ // Get flavors w/out converter
+ void GetTransferDataFlavors(nsTArray<nsCString>& aFlavors);
+
+ // Find index for data with the matching flavor in mDataArray.
+ mozilla::Maybe<size_t> FindDataFlavor(const char* aFlavor);
+
+ nsTArray<DataStruct> mDataArray;
+ nsCOMPtr<nsIFormatConverter> mFormatConv;
+ bool mPrivateData;
+ nsCOMPtr<nsIPrincipal> mRequestingPrincipal;
+ nsContentPolicyType mContentPolicyType;
+ nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;
+ nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
+#if DEBUG
+ bool mInitialized;
+#endif
+};
+
+#endif // nsTransferable_h__
diff --git a/widget/nsUserIdleService.cpp b/widget/nsUserIdleService.cpp
new file mode 100644
index 0000000000..f70a4bb459
--- /dev/null
+++ b/widget/nsUserIdleService.cpp
@@ -0,0 +1,900 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "nsError.h"
+#include "nsIAsyncShutdown.h"
+#include "nsUserIdleService.h"
+#include "nsString.h"
+#include "nsIObserverService.h"
+#include "nsDebug.h"
+#include "nsCOMArray.h"
+#include "nsXULAppAPI.h"
+#include "prinrval.h"
+#include "mozilla/Logging.h"
+#include "prtime.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/Services.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Telemetry.h"
+#include <algorithm>
+
+#ifdef MOZ_WIDGET_ANDROID
+# include <android/log.h>
+#endif
+
+using namespace mozilla;
+
+// After the twenty four hour period expires for an idle daily, this is the
+// amount of idle time we wait for before actually firing the idle-daily
+// event.
+#define DAILY_SIGNIFICANT_IDLE_SERVICE_SEC (3 * 60)
+
+// In cases where it's been longer than twenty four hours since the last
+// idle-daily, this is the shortend amount of idle time we wait for before
+// firing the idle-daily event.
+#define DAILY_SHORTENED_IDLE_SERVICE_SEC 60
+
+// Pref for last time (seconds since epoch) daily notification was sent.
+#define PREF_LAST_DAILY "idle.lastDailyNotification"
+
+// Number of seconds in a day.
+#define SECONDS_PER_DAY 86400
+
+static LazyLogModule sLog("idleService");
+
+#define LOG_TAG "GeckoIdleService"
+#define LOG_LEVEL ANDROID_LOG_DEBUG
+
+// Use this to find previously added observers in our array:
+class IdleListenerComparator {
+ public:
+ bool Equals(IdleListener a, IdleListener b) const {
+ return (a.observer == b.observer) && (a.reqIdleTime == b.reqIdleTime);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsUserIdleServiceDaily
+
+NS_IMPL_ISUPPORTS(nsUserIdleServiceDaily, nsIObserver, nsISupportsWeakReference)
+
+NS_IMETHODIMP
+nsUserIdleServiceDaily::Observe(nsISupports*, const char* aTopic,
+ const char16_t*) {
+ auto shutdownInProgress =
+ AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed);
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Observe '%s' (%d)", aTopic,
+ shutdownInProgress));
+
+ if (shutdownInProgress || strcmp(aTopic, OBSERVER_TOPIC_ACTIVE) == 0) {
+ return NS_OK;
+ }
+ MOZ_ASSERT(strcmp(aTopic, OBSERVER_TOPIC_IDLE) == 0);
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Notifying idle-daily observers"));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG, "Notifying idle-daily observers");
+#endif
+
+ // Send the idle-daily observer event
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_STATE(observerService);
+ (void)observerService->NotifyObservers(nullptr, OBSERVER_TOPIC_IDLE_DAILY,
+ nullptr);
+
+ // Notify the category observers.
+ nsCOMArray<nsIObserver> entries;
+ mCategoryObservers.GetEntries(entries);
+ for (int32_t i = 0; i < entries.Count(); ++i) {
+ (void)entries[i]->Observe(nullptr, OBSERVER_TOPIC_IDLE_DAILY, nullptr);
+ }
+
+ // Stop observing idle for today.
+ (void)mIdleService->RemoveIdleObserver(this, mIdleDailyTriggerWait);
+
+ // Set the last idle-daily time pref.
+ int32_t nowSec = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC);
+ Preferences::SetInt(PREF_LAST_DAILY, nowSec);
+
+ // Force that to be stored so we don't retrigger twice a day under
+ // any circumstances.
+ nsIPrefService* prefs = Preferences::GetService();
+ if (prefs) {
+ prefs->SavePrefFile(nullptr);
+ }
+
+ MOZ_LOG(
+ sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Storing last idle time as %d sec.", nowSec));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG, "Storing last idle time as %d",
+ nowSec);
+#endif
+
+ // Note the moment we expect to get the next timer callback
+ mExpectedTriggerTime =
+ PR_Now() + ((PRTime)SECONDS_PER_DAY * (PRTime)PR_USEC_PER_SEC);
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Restarting daily timer"));
+
+ // Start timer for the next check in one day.
+ (void)mTimer->InitWithNamedFuncCallback(
+ DailyCallback, this, SECONDS_PER_DAY * PR_MSEC_PER_SEC,
+ nsITimer::TYPE_ONE_SHOT, "nsUserIdleServiceDaily::Observe");
+
+ return NS_OK;
+}
+
+nsUserIdleServiceDaily::nsUserIdleServiceDaily(nsIUserIdleService* aIdleService)
+ : mIdleService(aIdleService),
+ mTimer(NS_NewTimer()),
+ mCategoryObservers(OBSERVER_TOPIC_IDLE_DAILY),
+ mExpectedTriggerTime(0),
+ mIdleDailyTriggerWait(DAILY_SIGNIFICANT_IDLE_SERVICE_SEC) {}
+
+void nsUserIdleServiceDaily::Init() {
+ // First check the time of the last idle-daily event notification. If it
+ // has been 24 hours or higher, or if we have never sent an idle-daily,
+ // get ready to send an idle-daily event. Otherwise set a timer targeted
+ // at 24 hours past the last idle-daily we sent.
+
+ int32_t lastDaily = Preferences::GetInt(PREF_LAST_DAILY, 0);
+ // Setting the pref to -1 allows to disable idle-daily, and it's particularly
+ // useful in tests. Normally there should be no need for the user to set
+ // this value.
+ if (lastDaily == -1) {
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Init: disabled idle-daily"));
+ return;
+ }
+
+ int32_t nowSec = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC);
+ if (lastDaily < 0 || lastDaily > nowSec) {
+ // The time is bogus, use default.
+ lastDaily = 0;
+ }
+ int32_t secondsSinceLastDaily = nowSec - lastDaily;
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Init: seconds since last daily: %d",
+ secondsSinceLastDaily));
+
+ // If it has been twenty four hours or more or if we have never sent an
+ // idle-daily event get ready to send it during the next idle period.
+ if (secondsSinceLastDaily > SECONDS_PER_DAY) {
+ // Check for a "long wait", e.g. 48-hours or more.
+ bool hasBeenLongWait =
+ (lastDaily && (secondsSinceLastDaily > (SECONDS_PER_DAY * 2)));
+
+ MOZ_LOG(
+ sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: has been long wait? %d", hasBeenLongWait));
+
+ // StageIdleDaily sets up a wait for the user to become idle and then
+ // sends the idle-daily event.
+ StageIdleDaily(hasBeenLongWait);
+ } else {
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Setting timer a day from now"));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG, "Setting timer a day from now");
+#endif
+
+ // According to our last idle-daily pref, the last idle-daily was fired
+ // less then 24 hours ago. Set a wait for the amount of time remaining.
+ int32_t milliSecLeftUntilDaily =
+ (SECONDS_PER_DAY - secondsSinceLastDaily) * PR_MSEC_PER_SEC;
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Seconds till next timeout: %d",
+ (SECONDS_PER_DAY - secondsSinceLastDaily)));
+
+ // Mark the time at which we expect this to fire. On systems with faulty
+ // timers, we need to be able to cross check that the timer fired at the
+ // expected time.
+ mExpectedTriggerTime =
+ PR_Now() + (milliSecLeftUntilDaily * PR_USEC_PER_MSEC);
+
+ (void)mTimer->InitWithNamedFuncCallback(
+ DailyCallback, this, milliSecLeftUntilDaily, nsITimer::TYPE_ONE_SHOT,
+ "nsUserIdleServiceDaily::Init");
+ }
+}
+
+nsUserIdleServiceDaily::~nsUserIdleServiceDaily() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+void nsUserIdleServiceDaily::StageIdleDaily(bool aHasBeenLongWait) {
+ NS_ASSERTION(mIdleService, "No idle service available?");
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: Registering Idle observer callback "
+ "(short wait requested? %d)",
+ aHasBeenLongWait));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG, "Registering Idle observer callback");
+#endif
+ mIdleDailyTriggerWait =
+ (aHasBeenLongWait ? DAILY_SHORTENED_IDLE_SERVICE_SEC
+ : DAILY_SIGNIFICANT_IDLE_SERVICE_SEC);
+ (void)mIdleService->AddIdleObserver(this, mIdleDailyTriggerWait);
+}
+
+// static
+void nsUserIdleServiceDaily::DailyCallback(nsITimer* aTimer, void* aClosure) {
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: DailyCallback running"));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG, "DailyCallback running");
+#endif
+
+ nsUserIdleServiceDaily* self = static_cast<nsUserIdleServiceDaily*>(aClosure);
+
+ // Check to be sure the timer didn't fire early. This currently only
+ // happens on android.
+ PRTime now = PR_Now();
+ if (self->mExpectedTriggerTime && now < self->mExpectedTriggerTime) {
+ // Timer returned early, reschedule to the appropriate time.
+ PRTime delayTime = self->mExpectedTriggerTime - now;
+
+ // Add 10 ms to ensure we don't undershoot, and never get a "0" timer.
+ delayTime += 10 * PR_USEC_PER_MSEC;
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("nsUserIdleServiceDaily: DailyCallback resetting timer to %" PRId64
+ " msec",
+ delayTime / PR_USEC_PER_MSEC));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "DailyCallback resetting timer to %" PRId64 " msec",
+ delayTime / PR_USEC_PER_MSEC);
+#endif
+
+ (void)self->mTimer->InitWithNamedFuncCallback(
+ DailyCallback, self, delayTime / PR_USEC_PER_MSEC,
+ nsITimer::TYPE_ONE_SHOT, "nsUserIdleServiceDaily::DailyCallback");
+ return;
+ }
+
+ // Register for a short term wait for idle event. When this fires we fire
+ // our idle-daily event.
+ self->StageIdleDaily(false);
+}
+
+/**
+ * The idle services goal is to notify subscribers when a certain time has
+ * passed since the last user interaction with the system.
+ *
+ * On some platforms this is defined as the last time user events reached this
+ * application, on other platforms it is a system wide thing - the preferred
+ * implementation is to use the system idle time, rather than the application
+ * idle time, as the things depending on the idle service are likely to use
+ * significant resources (network, disk, memory, cpu, etc.).
+ *
+ * When the idle service needs to use the system wide idle timer, it typically
+ * needs to poll the idle time value by the means of a timer. It needs to
+ * poll fast when it is in active idle mode (when it has a listener in the idle
+ * mode) as it needs to detect if the user is active in other applications.
+ *
+ * When the service is waiting for the first listener to become idle, or when
+ * it is only monitoring application idle time, it only needs to have the timer
+ * expire at the time the next listener goes idle.
+ *
+ * The core state of the service is determined by:
+ *
+ * - A list of listeners.
+ *
+ * - A boolean that tells if any listeners are in idle mode.
+ *
+ * - A delta value that indicates when, measured from the last non-idle time,
+ * the next listener should switch to idle mode.
+ *
+ * - An absolute time of the last time idle mode was detected (this is used to
+ * judge if we have been out of idle mode since the last invocation of the
+ * service.
+ *
+ * There are four entry points into the system:
+ *
+ * - A new listener is registered.
+ *
+ * - An existing listener is deregistered.
+ *
+ * - User interaction is detected.
+ *
+ * - The timer expires.
+ *
+ * When a new listener is added its idle timeout, is compared with the next idle
+ * timeout, and if lower, that time is stored as the new timeout, and the timer
+ * is reconfigured to ensure a timeout around the time the new listener should
+ * timeout.
+ *
+ * If the next idle time is above the idle time requested by the new listener
+ * it won't be informed until the timer expires, this is to avoid recursive
+ * behavior and to simplify the code. In this case the timer will be set to
+ * about 10 ms.
+ *
+ * When an existing listener is deregistered, it is just removed from the list
+ * of active listeners, we don't stop the timer, we just let it expire.
+ *
+ * When user interaction is detected, either because it was directly detected or
+ * because we polled the system timer and found it to be unexpected low, then we
+ * check the flag that tells us if any listeners are in idle mode, if there are
+ * they are removed from idle mode and told so, and we reset our state
+ * caculating the next timeout and restart the timer if needed.
+ *
+ * ---- Build in logic
+ *
+ * In order to avoid restarting the timer endlessly, the timer function has
+ * logic that will only restart the timer, if the requested timeout is before
+ * the current timeout.
+ *
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsUserIdleService
+
+namespace {
+nsUserIdleService* gIdleService;
+} // namespace
+
+already_AddRefed<nsUserIdleService> nsUserIdleService::GetInstance() {
+ RefPtr<nsUserIdleService> instance(gIdleService);
+ return instance.forget();
+}
+
+class UserIdleBlocker final : public nsIAsyncShutdownBlocker {
+ ~UserIdleBlocker() = default;
+
+ public:
+ explicit UserIdleBlocker() = default;
+
+ NS_IMETHOD
+ GetName(nsAString& aNameOut) override {
+ aNameOut = nsLiteralString(u"UserIdleBlocker");
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ BlockShutdown(nsIAsyncShutdownClient* aClient) override {
+ if (gIdleService) {
+ gIdleService->SetDisabledForShutdown();
+ }
+ aClient->RemoveBlocker(this);
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ GetState(nsIPropertyBag**) override { return NS_OK; }
+
+ NS_DECL_ISUPPORTS
+};
+
+NS_IMPL_ISUPPORTS(UserIdleBlocker, nsIAsyncShutdownBlocker)
+
+nsUserIdleService::nsUserIdleService()
+ : mIdleObserverCount(0),
+ mDeltaToNextIdleSwitchInS(UINT32_MAX),
+ mLastUserInteraction(TimeStamp::Now()) {
+ MOZ_ASSERT(!gIdleService);
+ gIdleService = this;
+ if (XRE_IsParentProcess()) {
+ mDailyIdle = new nsUserIdleServiceDaily(this);
+ mDailyIdle->Init();
+ }
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ MOZ_ASSERT(svc);
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ auto rv = svc->GetQuitApplicationGranted(getter_AddRefs(client));
+ if (NS_FAILED(rv)) {
+ // quitApplicationGranted can be undefined in some environments.
+ rv = svc->GetXpcomWillShutdown(getter_AddRefs(client));
+ }
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ client->AddBlocker(new UserIdleBlocker(),
+ NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ u""_ns);
+}
+
+nsUserIdleService::~nsUserIdleService() {
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+
+ MOZ_ASSERT(gIdleService == this);
+ gIdleService = nullptr;
+}
+
+NS_IMPL_ISUPPORTS(nsUserIdleService, nsIUserIdleService,
+ nsIUserIdleServiceInternal)
+
+void nsUserIdleService::SetDisabledForShutdown() {
+ SetDisabled(true);
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+NS_IMETHODIMP
+nsUserIdleService::AddIdleObserver(nsIObserver* aObserver,
+ uint32_t aIdleTimeInS) {
+ NS_ENSURE_ARG_POINTER(aObserver);
+ // We don't accept idle time at 0, and we can't handle idle time that are too
+ // high either - no more than ~136 years.
+ NS_ENSURE_ARG_RANGE(aIdleTimeInS, 1, (UINT32_MAX / 10) - 1);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString timeCStr;
+ timeCStr.AppendInt(aIdleTimeInS);
+ PROFILER_MARKER_TEXT("UserIdle::AddObserver", OTHER, MarkerStack::Capture(),
+ timeCStr);
+ }
+
+ if (XRE_IsContentProcess()) {
+ dom::ContentChild* cpc = dom::ContentChild::GetSingleton();
+ cpc->AddIdleObserver(aObserver, aIdleTimeInS);
+ return NS_OK;
+ }
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Register idle observer %p for %d seconds", aObserver,
+ aIdleTimeInS));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Register idle observer %p for %d seconds", aObserver,
+ aIdleTimeInS);
+#endif
+
+ // Put the time + observer in a struct we can keep:
+ IdleListener listener(aObserver, aIdleTimeInS);
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mArrayListeners.AppendElement(listener);
+
+ // Create our timer callback if it's not there already.
+ if (!mTimer) {
+ mTimer = NS_NewTimer();
+ NS_ENSURE_TRUE(mTimer, NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ // Check if the newly added observer has a smaller wait time than what we
+ // are waiting for now.
+ if (mDeltaToNextIdleSwitchInS > aIdleTimeInS) {
+ // If it is, then this is the next to move to idle (at this point we
+ // don't care if it should have switched already).
+ MOZ_LOG(
+ sLog, LogLevel::Debug,
+ ("idleService: Register: adjusting next switch from %d to %d seconds",
+ mDeltaToNextIdleSwitchInS, aIdleTimeInS));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Register: adjusting next switch from %d to %d seconds",
+ mDeltaToNextIdleSwitchInS, aIdleTimeInS);
+#endif
+
+ mDeltaToNextIdleSwitchInS = aIdleTimeInS;
+ ReconfigureTimer();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserIdleService::RemoveIdleObserver(nsIObserver* aObserver,
+ uint32_t aTimeInS) {
+ NS_ENSURE_ARG_POINTER(aObserver);
+ NS_ENSURE_ARG(aTimeInS);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString timeCStr;
+ timeCStr.AppendInt(aTimeInS);
+ PROFILER_MARKER_TEXT("UserIdle::RemoveObserver", OTHER,
+ MarkerStack::Capture(), timeCStr);
+ }
+
+ if (XRE_IsContentProcess()) {
+ dom::ContentChild* cpc = dom::ContentChild::GetSingleton();
+ cpc->RemoveIdleObserver(aObserver, aTimeInS);
+ return NS_OK;
+ }
+
+ IdleListener listener(aObserver, aTimeInS);
+
+ // Find the entry and remove it, if it was the last entry, we just let the
+ // existing timer run to completion (there might be a new registration in a
+ // little while.
+ IdleListenerComparator c;
+ nsTArray<IdleListener>::index_type listenerIndex =
+ mArrayListeners.IndexOf(listener, 0, c);
+ if (listenerIndex != mArrayListeners.NoIndex) {
+ if (mArrayListeners.ElementAt(listenerIndex).isIdle) mIdleObserverCount--;
+ mArrayListeners.RemoveElementAt(listenerIndex);
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Remove observer %p (%d seconds), %d remain idle",
+ aObserver, aTimeInS, mIdleObserverCount));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Remove observer %p (%d seconds), %d remain idle",
+ aObserver, aTimeInS, mIdleObserverCount);
+#endif
+ return NS_OK;
+ }
+
+ // If we get here, we haven't removed anything:
+ MOZ_LOG(sLog, LogLevel::Warning,
+ ("idleService: Failed to remove idle observer %p (%d seconds)",
+ aObserver, aTimeInS));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Failed to remove idle observer %p (%d seconds)",
+ aObserver, aTimeInS);
+#endif
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsUserIdleService::ResetIdleTimeOut(uint32_t idleDeltaInMS) {
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Reset idle timeout (last interaction %u msec)",
+ idleDeltaInMS));
+
+ // Store the time
+ mLastUserInteraction =
+ TimeStamp::Now() - TimeDuration::FromMilliseconds(idleDeltaInMS);
+
+ // If no one is idle, then we are done, any existing timers can keep running.
+ if (mIdleObserverCount == 0) {
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Reset idle timeout: no idle observers"));
+ return NS_OK;
+ }
+
+ // Mark all idle services as non-idle, and calculate the next idle timeout.
+ nsCOMArray<nsIObserver> notifyList;
+ mDeltaToNextIdleSwitchInS = UINT32_MAX;
+
+ // Loop through all listeners, and find any that have detected idle.
+ for (uint32_t i = 0; i < mArrayListeners.Length(); i++) {
+ IdleListener& curListener = mArrayListeners.ElementAt(i);
+
+ // If the listener was idle, then he shouldn't be any longer.
+ if (curListener.isIdle) {
+ notifyList.AppendObject(curListener.observer);
+ curListener.isIdle = false;
+ }
+
+ // Check if the listener is the next one to timeout.
+ mDeltaToNextIdleSwitchInS =
+ std::min(mDeltaToNextIdleSwitchInS, curListener.reqIdleTime);
+ }
+
+ // When we are done, then we wont have anyone idle.
+ mIdleObserverCount = 0;
+
+ // Restart the idle timer, and do so before anyone can delay us.
+ ReconfigureTimer();
+
+ int32_t numberOfPendingNotifications = notifyList.Count();
+
+ // Bail if nothing to do.
+ if (!numberOfPendingNotifications) {
+ return NS_OK;
+ }
+
+ // Now send "active" events to all, if any should have timed out already,
+ // then they will be reawaken by the timer that is already running.
+
+ // We need a text string to send with any state change events.
+ nsAutoString timeStr;
+
+ timeStr.AppendInt((int32_t)(idleDeltaInMS / PR_MSEC_PER_SEC));
+
+ // Send the "non-idle" events.
+ while (numberOfPendingNotifications--) {
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Reset idle timeout: tell observer %p user is back",
+ notifyList[numberOfPendingNotifications]));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Reset idle timeout: tell observer %p user is back",
+ notifyList[numberOfPendingNotifications]);
+#endif
+ notifyList[numberOfPendingNotifications]->Observe(
+ this, OBSERVER_TOPIC_ACTIVE, timeStr.get());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserIdleService::GetIdleTime(uint32_t* idleTime) {
+ // Check sanity of in parameter.
+ if (!idleTime) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Polled idle time in ms.
+ uint32_t polledIdleTimeMS;
+
+ bool polledIdleTimeIsValid = PollIdleTime(&polledIdleTimeMS);
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Get idle time: polled %u msec, valid = %d",
+ polledIdleTimeMS, polledIdleTimeIsValid));
+
+ // timeSinceReset is in milliseconds.
+ TimeDuration timeSinceReset = TimeStamp::Now() - mLastUserInteraction;
+ uint32_t timeSinceResetInMS = timeSinceReset.ToMilliseconds();
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Get idle time: time since reset %u msec",
+ timeSinceResetInMS));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Get idle time: time since reset %u msec",
+ timeSinceResetInMS);
+#endif
+
+ // If we did't get pulled data, return the time since last idle reset.
+ if (!polledIdleTimeIsValid) {
+ // We need to convert to ms before returning the time.
+ *idleTime = timeSinceResetInMS;
+ return NS_OK;
+ }
+
+ // Otherwise return the shortest time detected (in ms).
+ *idleTime = std::min(timeSinceResetInMS, polledIdleTimeMS);
+
+ return NS_OK;
+}
+
+bool nsUserIdleService::PollIdleTime(uint32_t* /*aIdleTime*/) {
+ // Default behavior is not to have the ability to poll an idle time.
+ return false;
+}
+
+nsresult nsUserIdleService::GetDisabled(bool* aResult) {
+ *aResult = mDisabled;
+ return NS_OK;
+}
+
+nsresult nsUserIdleService::SetDisabled(bool aDisabled) {
+ mDisabled = aDisabled;
+ return NS_OK;
+}
+
+void nsUserIdleService::StaticIdleTimerCallback(nsITimer* aTimer,
+ void* aClosure) {
+ static_cast<nsUserIdleService*>(aClosure)->IdleTimerCallback();
+}
+
+void nsUserIdleService::IdleTimerCallback(void) {
+ // Remember that we no longer have a timer running.
+ mCurrentlySetToTimeoutAt = TimeStamp();
+
+ // Find the last detected idle time.
+ uint32_t lastIdleTimeInMS = static_cast<uint32_t>(
+ (TimeStamp::Now() - mLastUserInteraction).ToMilliseconds());
+ // Get the current idle time.
+ uint32_t currentIdleTimeInMS;
+
+ if (NS_FAILED(GetIdleTime(&currentIdleTimeInMS))) {
+ MOZ_LOG(sLog, LogLevel::Info,
+ ("idleService: Idle timer callback: failed to get idle time"));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Idle timer callback: failed to get idle time");
+#endif
+ return;
+ }
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: Idle timer callback: current idle time %u msec",
+ currentIdleTimeInMS));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Idle timer callback: current idle time %u msec",
+ currentIdleTimeInMS);
+#endif
+
+ // Check if we have had some user interaction we didn't handle previously
+ // we do the calculation in ms to lessen the chance for rounding errors to
+ // trigger wrong results.
+ if (lastIdleTimeInMS > currentIdleTimeInMS) {
+ // We had user activity, so handle that part first (to ensure the listeners
+ // don't risk getting an non-idle after they get a new idle indication.
+ ResetIdleTimeOut(currentIdleTimeInMS);
+
+ // NOTE: We can't bail here, as we might have something already timed out.
+ }
+
+ // Find the idle time in S.
+ uint32_t currentIdleTimeInS = currentIdleTimeInMS / PR_MSEC_PER_SEC;
+
+ // Restart timer and bail if no-one are expected to be in idle
+ if (mDeltaToNextIdleSwitchInS > currentIdleTimeInS) {
+ // If we didn't expect anyone to be idle, then just re-start the timer.
+ ReconfigureTimer();
+ return;
+ }
+
+ if (mDisabled) {
+ MOZ_LOG(sLog, LogLevel::Info,
+ ("idleService: Skipping idle callback while disabled"));
+
+ ReconfigureTimer();
+ return;
+ }
+
+ // Tell expired listeners they are expired,and find the next timeout
+ Telemetry::AutoTimer<Telemetry::IDLE_NOTIFY_IDLE_MS> timer;
+
+ // We need to initialise the time to the next idle switch.
+ mDeltaToNextIdleSwitchInS = UINT32_MAX;
+
+ // Create list of observers that should be notified.
+ nsCOMArray<nsIObserver> notifyList;
+
+ for (uint32_t i = 0; i < mArrayListeners.Length(); i++) {
+ IdleListener& curListener = mArrayListeners.ElementAt(i);
+
+ // We are only interested in items, that are not in the idle state.
+ if (!curListener.isIdle) {
+ // If they have an idle time smaller than the actual idle time.
+ if (curListener.reqIdleTime <= currentIdleTimeInS) {
+ // Then add the listener to the list of listeners that should be
+ // notified.
+ notifyList.AppendObject(curListener.observer);
+ // This listener is now idle.
+ curListener.isIdle = true;
+ // Remember we have someone idle.
+ mIdleObserverCount++;
+ } else {
+ // Listeners that are not timed out yet are candidates for timing out.
+ mDeltaToNextIdleSwitchInS =
+ std::min(mDeltaToNextIdleSwitchInS, curListener.reqIdleTime);
+ }
+ }
+ }
+
+ // Restart the timer before any notifications that could slow us down are
+ // done.
+ ReconfigureTimer();
+
+ int32_t numberOfPendingNotifications = notifyList.Count();
+
+ // Bail if nothing to do.
+ if (!numberOfPendingNotifications) {
+ MOZ_LOG(
+ sLog, LogLevel::Debug,
+ ("idleService: **** Idle timer callback: no observers to message."));
+ return;
+ }
+
+ // We need a text string to send with any state change events.
+ nsAutoString timeStr;
+ timeStr.AppendInt(currentIdleTimeInS);
+
+ // Notify all listeners that just timed out.
+ while (numberOfPendingNotifications--) {
+ MOZ_LOG(
+ sLog, LogLevel::Debug,
+ ("idleService: **** Idle timer callback: tell observer %p user is idle",
+ notifyList[numberOfPendingNotifications]));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "Idle timer callback: tell observer %p user is idle",
+ notifyList[numberOfPendingNotifications]);
+#endif
+ nsAutoCString timeCStr;
+ timeCStr.AppendInt(currentIdleTimeInS);
+ AUTO_PROFILER_MARKER_TEXT("UserIdle::IdleCallback", OTHER, {}, timeCStr);
+ notifyList[numberOfPendingNotifications]->Observe(this, OBSERVER_TOPIC_IDLE,
+ timeStr.get());
+ }
+}
+
+void nsUserIdleService::SetTimerExpiryIfBefore(TimeStamp aNextTimeout) {
+ TimeDuration nextTimeoutDuration = aNextTimeout - TimeStamp::Now();
+
+ MOZ_LOG(
+ sLog, LogLevel::Debug,
+ ("idleService: SetTimerExpiryIfBefore: next timeout %0.f msec from now",
+ nextTimeoutDuration.ToMilliseconds()));
+
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "SetTimerExpiryIfBefore: next timeout %0.f msec from now",
+ nextTimeoutDuration.ToMilliseconds());
+#endif
+
+ // Bail if we don't have a timer service.
+ if (!mTimer) {
+ return;
+ }
+
+ // If the new timeout is before the old one or we don't have a timer running,
+ // then restart the timer.
+ if (mCurrentlySetToTimeoutAt.IsNull() ||
+ mCurrentlySetToTimeoutAt > aNextTimeout) {
+ mCurrentlySetToTimeoutAt = aNextTimeout;
+
+ // Stop the current timer (it's ok to try'n stop it, even it isn't running).
+ mTimer->Cancel();
+
+ // Check that the timeout is actually in the future, otherwise make it so.
+ TimeStamp currentTime = TimeStamp::Now();
+ if (currentTime > mCurrentlySetToTimeoutAt) {
+ mCurrentlySetToTimeoutAt = currentTime;
+ }
+
+ // Add 10 ms to ensure we don't undershoot, and never get a "0" timer.
+ mCurrentlySetToTimeoutAt += TimeDuration::FromMilliseconds(10);
+
+ TimeDuration deltaTime = mCurrentlySetToTimeoutAt - currentTime;
+ MOZ_LOG(
+ sLog, LogLevel::Debug,
+ ("idleService: IdleService reset timer expiry to %0.f msec from now",
+ deltaTime.ToMilliseconds()));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "reset timer expiry to %0.f msec from now",
+ deltaTime.ToMilliseconds());
+#endif
+
+ // Start the timer
+ mTimer->InitWithNamedFuncCallback(
+ StaticIdleTimerCallback, this, deltaTime.ToMilliseconds(),
+ nsITimer::TYPE_ONE_SHOT, "nsUserIdleService::SetTimerExpiryIfBefore");
+ }
+}
+
+void nsUserIdleService::ReconfigureTimer(void) {
+ // Check if either someone is idle, or someone will become idle.
+ if ((mIdleObserverCount == 0) && UINT32_MAX == mDeltaToNextIdleSwitchInS) {
+ // If not, just let any existing timers run to completion
+ // And bail out.
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: ReconfigureTimer: no idle or waiting observers"));
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG,
+ "ReconfigureTimer: no idle or waiting observers");
+#endif
+ return;
+ }
+
+ // Find the next timeout value, assuming we are not polling.
+
+ // We need to store the current time, so we don't get artifacts from the time
+ // ticking while we are processing.
+ TimeStamp curTime = TimeStamp::Now();
+
+ TimeStamp nextTimeoutAt =
+ mLastUserInteraction +
+ TimeDuration::FromSeconds(mDeltaToNextIdleSwitchInS);
+
+ TimeDuration nextTimeoutDuration = nextTimeoutAt - curTime;
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("idleService: next timeout %0.f msec from now",
+ nextTimeoutDuration.ToMilliseconds()));
+
+#ifdef MOZ_WIDGET_ANDROID
+ __android_log_print(LOG_LEVEL, LOG_TAG, "next timeout %0.f msec from now",
+ nextTimeoutDuration.ToMilliseconds());
+#endif
+
+ SetTimerExpiryIfBefore(nextTimeoutAt);
+}
diff --git a/widget/nsUserIdleService.h b/widget/nsUserIdleService.h
new file mode 100644
index 0000000000..2ed35cd032
--- /dev/null
+++ b/widget/nsUserIdleService.h
@@ -0,0 +1,218 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef nsUserIdleService_h__
+#define nsUserIdleService_h__
+
+#include "nsIUserIdleServiceInternal.h"
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "nsIObserver.h"
+#include "nsIUserIdleService.h"
+#include "nsCategoryCache.h"
+#include "nsWeakReference.h"
+#include "mozilla/TimeStamp.h"
+
+/**
+ * Class we can use to store an observer with its associated idle time
+ * requirement and whether or not the observer thinks it's "idle".
+ */
+class IdleListener {
+ public:
+ nsCOMPtr<nsIObserver> observer;
+ uint32_t reqIdleTime;
+ bool isIdle;
+
+ IdleListener(nsIObserver* obs, uint32_t reqIT, bool aIsIdle = false)
+ : observer(obs), reqIdleTime(reqIT), isIdle(aIsIdle) {}
+ ~IdleListener() = default;
+};
+
+// This one will be declared later.
+class nsUserIdleService;
+
+/**
+ * Class to handle the daily idle timer.
+ */
+class nsUserIdleServiceDaily : public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ explicit nsUserIdleServiceDaily(nsIUserIdleService* aIdleService);
+
+ /**
+ * Initializes the daily idle observer.
+ * Keep this separated from the constructor, since it could cause pointer
+ * corruption due to AddRef/Release of "this".
+ */
+ void Init();
+
+ private:
+ virtual ~nsUserIdleServiceDaily();
+
+ /**
+ * StageIdleDaily is the interim call made when an idle-daily event is due.
+ * However we don't want to fire idle-daily until the user is idle for this
+ * session, so this sets up a short wait for an idle event which triggers
+ * the actual idle-daily event.
+ *
+ * @param aHasBeenLongWait Pass true indicating nsUserIdleServiceDaily is
+ * having trouble getting the idle-daily event fired. If true StageIdleDaily
+ * will use a shorter idle wait time before firing idle-daily.
+ */
+ void StageIdleDaily(bool aHasBeenLongWait);
+
+ /**
+ * @note This is a normal pointer, part to avoid creating a cycle with the
+ * idle service, part to avoid potential pointer corruption due to this class
+ * being instantiated in the constructor of the service itself.
+ */
+ nsIUserIdleService* mIdleService;
+
+ /**
+ * Place to hold the timer used by this class to determine when a day has
+ * passed, after that it will wait for idle time to be detected.
+ */
+ nsCOMPtr<nsITimer> mTimer;
+
+ /**
+ * Function that is called back once a day.
+ */
+ static void DailyCallback(nsITimer* aTimer, void* aClosure);
+
+ /**
+ * Cache of observers for the "idle-daily" category.
+ */
+ nsCategoryCache<nsIObserver> mCategoryObservers;
+
+ /**
+ * Next time we expect an idle-daily timer to fire, in case timers aren't
+ * very reliable on the platform. Value is in PR_Now microsecond units.
+ */
+ PRTime mExpectedTriggerTime;
+
+ /**
+ * Tracks which idle daily observer callback we ask for. There are two: a
+ * regular long idle wait and a shorter wait if we've been waiting to fire
+ * idle daily for an extended period. Set by StageIdleDaily.
+ */
+ int32_t mIdleDailyTriggerWait;
+};
+
+class nsUserIdleService : public nsIUserIdleServiceInternal {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIUSERIDLESERVICE NS_DECL_NSIUSERIDLESERVICEINTERNAL
+
+ protected : static already_AddRefed<nsUserIdleService>
+ GetInstance();
+
+ nsUserIdleService();
+ virtual ~nsUserIdleService();
+
+ /**
+ * If there is a platform specific function to poll the system idel time
+ * then that must be returned in this function, and the function MUST return
+ * true, otherwise then the function should be left unimplemented or made
+ * to return false (this can also be used for systems where it depends on
+ * the configuration of the system if the idle time can be determined)
+ *
+ * @param aIdleTime
+ * The idle time in ms.
+ *
+ * @return true if the idle time could be polled, false otherwise.
+ *
+ * @note The time returned by this function can be different than the one
+ * returned by GetIdleTime, as that is corrected by any calls to
+ * ResetIdleTimeOut(), unless you overwrite that function too...
+ */
+ virtual bool PollIdleTime(uint32_t* aIdleTime);
+
+ public:
+ void SetDisabledForShutdown();
+
+ private:
+ /**
+ * Ensure that the timer is expiring at least at the given time
+ *
+ * The function might not restart the timer if there is one running currently
+ *
+ * @param aNextTimeout
+ * The last absolute time the timer should expire
+ */
+ void SetTimerExpiryIfBefore(mozilla::TimeStamp aNextTimeout);
+
+ /**
+ * Stores the next timeout time, 0 means timer not running
+ */
+ mozilla::TimeStamp mCurrentlySetToTimeoutAt;
+
+ /**
+ * mTimer holds the internal timer used by this class to detect when to poll
+ * for idle time, when to check if idle timers should expire etc.
+ */
+ nsCOMPtr<nsITimer> mTimer;
+
+ /**
+ * Array of listeners that wants to be notified about idle time.
+ */
+ nsTArray<IdleListener> mArrayListeners;
+
+ /**
+ * Object keeping track of the daily idle thingy.
+ */
+ RefPtr<nsUserIdleServiceDaily> mDailyIdle;
+
+ /**
+ * Number of observers currently in idle mode.
+ */
+ uint32_t mIdleObserverCount;
+
+ /**
+ * Delta time from last non idle time to when the next observer should switch
+ * to idle mode
+ *
+ * Time in seconds
+ *
+ * If this value is 0 it means there are no active observers
+ */
+ uint32_t mDeltaToNextIdleSwitchInS;
+
+ /**
+ * If true, the idle service is temporarily disabled, and all idle events
+ * will be ignored.
+ */
+ bool mDisabled = false;
+
+ /**
+ * Absolute value for when the last user interaction took place.
+ */
+ mozilla::TimeStamp mLastUserInteraction;
+
+ /**
+ * Function that ensures the timer is running with at least the minimum time
+ * needed. It will kill the timer if there are no active observers.
+ */
+ void ReconfigureTimer(void);
+
+ /**
+ * Callback function that is called when the internal timer expires.
+ *
+ * This in turn calls the IdleTimerCallback that does the real processing
+ */
+ static void StaticIdleTimerCallback(nsITimer* aTimer, void* aClosure);
+
+ /**
+ * Function that handles when a timer has expired
+ */
+ void IdleTimerCallback(void);
+};
+
+#endif // nsUserIdleService_h__
diff --git a/widget/nsWidgetsCID.h b/widget/nsWidgetsCID.h
new file mode 100644
index 0000000000..95c425f75e
--- /dev/null
+++ b/widget/nsWidgetsCID.h
@@ -0,0 +1,332 @@
+/* -*- 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/. */
+
+/* bd57cee8-1dd1-11b2-9fe7-95cf4709aea3 */
+#define NS_FILEPICKER_CID \
+ { \
+ 0xbd57cee8, 0x1dd1, 0x11b2, { \
+ 0x9f, 0xe7, 0x95, 0xcf, 0x47, 0x09, 0xae, 0xa3 \
+ } \
+ }
+
+/* e221df9b-3d66-4045-9a66-5720949f8d10 */
+#define NS_APPLICATIONCHOOSER_CID \
+ { \
+ 0xe221df9b, 0x3d66, 0x4045, { \
+ 0x9a, 0x66, 0x57, 0x20, 0x94, 0x9f, 0x8d, 0x10 \
+ } \
+ }
+
+/* 0f872c8c-3ee6-46bd-92a2-69652c6b474e */
+#define NS_COLORPICKER_CID \
+ { \
+ 0x0f872c8c, 0x3ee6, 0x46bd, { \
+ 0x92, 0xa2, 0x69, 0x65, 0x2c, 0x6b, 0x47, 0x4e \
+ } \
+ }
+
+/* 2d96b3df-c051-11d1-a827-0040959a28c9 */
+#define NS_APPSHELL_CID \
+ { \
+ 0x2d96b3df, 0xc051, 0x11d1, { \
+ 0xa8, 0x27, 0x00, 0x40, 0x95, 0x9a, 0x28, 0xc9 \
+ } \
+ }
+
+/* 2d96b3e0-c051-11d1-a827-0040959a28c9 */
+#define NS_TOOLKIT_CID \
+ { \
+ 0x2d96b3e0, 0xc051, 0x11d1, { \
+ 0xa8, 0x27, 0x00, 0x40, 0x95, 0x9a, 0x28, 0xc9 \
+ } \
+ }
+
+/* 9A0CB62B-D638-4FAF-9588-AE96F5E29093 */
+#define NS_TASKBARPREVIEWCALLBACK_CID \
+ { \
+ 0x9a0cb62b, 0xd638, 0x4faf, { \
+ 0x95, 0x88, 0xae, 0x96, 0xf5, 0xe2, 0x90, 0x93 \
+ } \
+ }
+
+/*1201d357-8417-4926-a694-e6408fbedcf8*/
+#define NS_SHAREPICKER_CID \
+ { \
+ 0x1201d357, 0x8417, 0x4926, { \
+ 0xa6, 0x94, 0xe6, 0x40, 0x8f, 0xbe, 0xdc, 0xf8 \
+ } \
+ }
+
+/* XXX the following CID's are not in order. This needs
+ to be fixed. */
+
+//-----------------------------------------------------------
+// Menus
+//-----------------------------------------------------------
+
+// {F6CD4F21-53AF-11d2-8DC4-00609703C14E}
+#define NS_POPUPMENU_CID \
+ { \
+ 0xf6cd4f21, 0x53af, 0x11d2, { \
+ 0x8d, 0xc4, 0x0, 0x60, 0x97, 0x3, 0xc1, 0x4e \
+ } \
+ }
+
+// {1F39AE50-B6A0-4B37-90F4-60AF614193D8}
+#define NS_STANDALONENATIVEMENU_CID \
+ { \
+ 0x1F39AE50, 0xB6A0, 0x4B37, { \
+ 0x90, 0xF4, 0x60, 0xAF, 0x61, 0x41, 0x93, 0xD8 \
+ } \
+ }
+
+// {2451BAED-8DC3-46D9-9E30-96E1BAA03666}
+#define NS_MACDOCKSUPPORT_CID \
+ { \
+ 0x2451BAED, 0x8DC3, 0x46D9, { \
+ 0x9E, 0x30, 0x96, 0xE1, 0xBA, 0xA0, 0x36, 0x66 \
+ } \
+ }
+
+// {74EA4101-A5BB-49BC-9984-66DA8B225A37}
+#define NS_MACFINDERPROGRESS_CID \
+ { \
+ 0x74EA4101, 0xA5BB, 0x49BC, { \
+ 0x99, 0x84, 0x66, 0xDA, 0x8B, 0x22, 0x5A, 0x37 \
+ } \
+ }
+
+// {de59fe1a-46c8-490f-b04d-34545acb06c9}
+#define NS_MACSHARINGSERVICE_CID \
+ { \
+ 0xde59fe1a, 0x46c8, 0x490f, { \
+ 0xb0, 0x4d, 0x34, 0x54, 0x5a, 0xcb, 0x06, 0xc9 \
+ } \
+ }
+
+// {29046c8f-cba6-4ffa-9141-1685e96c4ea0}
+#define NS_MACUSERACTIVITYUPDATER_CID \
+ { \
+ 0x29046c8f, 0xcba6, 0x4ffa, { \
+ 0x91, 0x41, 0x16, 0x85, 0xe9, 0x6c, 0x4e, 0xa0 \
+ } \
+ }
+
+// {b6e1a890-b2b8-4883-a65f-9476f6185313}
+#define NS_SYSTEMSTATUSBAR_CID \
+ { \
+ 0xb6e1a890, 0xb2b8, 0x4883, { \
+ 0xa6, 0x5f, 0x94, 0x76, 0xf6, 0x18, 0x53, 0x13 \
+ } \
+ }
+
+// {ea109912-3acc-48de-b679-c23b6a122da5}
+#define NS_TOUCHBARHELPER_CID \
+ { \
+ 0xea109912, 0x3acc, 0x48de, { \
+ 0xb6, 0x79, 0xc2, 0x3b, 0x6a, 0x12, 0x2d, 0xa5 \
+ } \
+ }
+
+// {38f396e2-93c9-4a77-aaf7-2d50b9962186}
+#define NS_TOUCHBARUPDATER_CID \
+ { \
+ 0x38f396e2, 0x93c9, 0x4a77, { \
+ 0xaa, 0xf7, 0x2d, 0x50, 0xb9, 0x96, 0x21, 0x86 \
+ } \
+ }
+
+// {77441d17-f29c-49d7-982f-f20a5ab5a900}
+#define NS_TOUCHBARINPUT_CID \
+ { \
+ 0x77441d17, 0xf29c, 0x49d7, { \
+ 0x98, 0x2f, 0xf2, 0x0a, 0x5a, 0xb5, 0xa9, 0x00 \
+ } \
+ }
+
+//-----------------------------------------------------------
+// Drag & Drop & Clipboard
+//-----------------------------------------------------------
+// {8B5314BB-DB01-11d2-96CE-0060B0FB9956}
+#define NS_DRAGSERVICE_CID \
+ { \
+ 0x8b5314bb, 0xdb01, 0x11d2, { \
+ 0x96, 0xce, 0x0, 0x60, 0xb0, 0xfb, 0x99, 0x56 \
+ } \
+ }
+
+// {8B5314BC-DB01-11d2-96CE-0060B0FB9956}
+#define NS_TRANSFERABLE_CID \
+ { \
+ 0x8b5314bc, 0xdb01, 0x11d2, { \
+ 0x96, 0xce, 0x0, 0x60, 0xb0, 0xfb, 0x99, 0x56 \
+ } \
+ }
+
+// {8B5314BA-DB01-11d2-96CE-0060B0FB9956}
+#define NS_CLIPBOARD_CID \
+ { \
+ 0x8b5314ba, 0xdb01, 0x11d2, { \
+ 0x96, 0xce, 0x0, 0x60, 0xb0, 0xfb, 0x99, 0x56 \
+ } \
+ }
+
+// {77221D5A-1DD2-11B2-8C69-C710F15D2ED5}
+#define NS_CLIPBOARDHELPER_CID \
+ { \
+ 0x77221d5a, 0x1dd2, 0x11b2, { \
+ 0x8c, 0x69, 0xc7, 0x10, 0xf1, 0x5d, 0x2e, 0xd5 \
+ } \
+ }
+
+// {8B5314BD-DB01-11d2-96CE-0060B0FB9956}
+#define NS_DATAFLAVOR_CID \
+ { \
+ 0x8b5314bd, 0xdb01, 0x11d2, { \
+ 0x96, 0xce, 0x0, 0x60, 0xb0, 0xfb, 0x99, 0x56 \
+ } \
+ }
+
+// {948A0023-E3A7-11d2-96CF-0060B0FB9956}
+#define NS_HTMLFORMATCONVERTER_CID \
+ { \
+ 0x948a0023, 0xe3a7, 0x11d2, { \
+ 0x96, 0xcf, 0x0, 0x60, 0xb0, 0xfb, 0x99, 0x56 \
+ } \
+ }
+
+//-----------------------------------------------------------
+// Other
+//-----------------------------------------------------------
+// {B148EED2-236D-11d3-B35C-00A0CC3C1CDE}
+#define NS_SOUND_CID \
+ { \
+ 0xb148eed2, 0x236d, 0x11d3, { \
+ 0xb3, 0x5c, 0x0, 0xa0, 0xcc, 0x3c, 0x1c, 0xde \
+ } \
+ }
+
+#define NS_SCREENMANAGER_CID \
+ { \
+ 0xc401eb80, 0xf9ea, 0x11d3, { \
+ 0xbb, 0x6f, 0xe7, 0x32, 0xb7, 0x3e, 0xbe, 0x7c \
+ } \
+ }
+
+// {6987230e-0089-4e78-bc5f-1493ee7519fa}
+#define NS_IDLE_SERVICE_CID \
+ { \
+ 0x6987230e, 0x0098, 0x4e78, { \
+ 0xbc, 0x5f, 0x14, 0x93, 0xee, 0x75, 0x19, 0xfa \
+ } \
+ }
+
+#define NS_WIN_TASKBAR_CID \
+ { \
+ 0xb8e5bc54, 0xa22f, 0x4eb2, { \
+ 0xb0, 0x61, 0x24, 0xcb, 0x6d, 0x19, 0xc1, 0x5f \
+ } \
+ }
+
+// {73A5946F-608D-454f-9D33-0B8F8C7294B6}
+#define NS_WIN_LEGACYJUMPLISTBUILDER_CID \
+ { \
+ 0x73a5946f, 0x608d, 0x454f, { \
+ 0x9d, 0x33, 0xb, 0x8f, 0x8c, 0x72, 0x94, 0xb6 \
+ } \
+ }
+
+// {2B9A1F2C-27CE-45b6-8D4E-755D0E34F8DB}
+#define NS_WIN_LEGACYJUMPLISTITEM_CID \
+ { \
+ 0x2b9a1f2c, 0x27ce, 0x45b6, { \
+ 0x8d, 0x4e, 0x75, 0x5d, 0x0e, 0x34, 0xf8, 0xdb \
+ } \
+ }
+
+// {21F1F13B-F75A-42ad-867A-D91AD694447E}
+#define NS_WIN_LEGACYJUMPLISTSEPARATOR_CID \
+ { \
+ 0x21f1f13b, 0xf75a, 0x42ad, { \
+ 0x86, 0x7a, 0xd9, 0x1a, 0xd6, 0x94, 0x44, 0x7e \
+ } \
+ }
+
+// {F72C5DC4-5A12-47be-BE28-AB105F33B08F}
+#define NS_WIN_LEGACYJUMPLISTLINK_CID \
+ { \
+ 0xf72c5dc4, 0x5a12, 0x47be, { \
+ 0xbe, 0x28, 0xab, 0x10, 0x5f, 0x33, 0xb0, 0x8f \
+ } \
+ }
+
+// {B16656B2-5187-498f-ABF4-56346126BFDB}
+#define NS_WIN_LEGACYJUMPLISTSHORTCUT_CID \
+ { \
+ 0xb16656b2, 0x5187, 0x498f, { \
+ 0xab, 0xf4, 0x56, 0x34, 0x61, 0x26, 0xbf, 0xdb \
+ } \
+ }
+
+// {e9096367-ddd9-45e4-b762-49c0c18b7119}
+#define NS_MACWEBAPPUTILS_CID \
+ { \
+ 0xe9096367, 0xddd9, 0x45e4, { \
+ 0xb7, 0x62, 0x49, 0xc0, 0xc1, 0x8b, 0x71, 0x19 \
+ } \
+ }
+
+// {a9339876-0027-430f-b953-84c9c11c2da3}
+#define NS_GTK_TASKBARPROGRESS_CID \
+ { \
+ 0xa9339876, 0x0027, 0x430f, { \
+ 0xb9, 0x53, 0x84, 0xc9, 0xc1, 0x1c, 0x2d, 0xa3 \
+ } \
+ }
+
+//-----------------------------------------------------------
+// Printing
+//-----------------------------------------------------------
+#define NS_DEVICE_CONTEXT_SPEC_CID \
+ { \
+ 0xd3f69889, 0xe13a, 0x4321, { \
+ 0x98, 0x0c, 0xa3, 0x93, 0x32, 0xe2, 0x1f, 0x34 \
+ } \
+ }
+
+#define NS_PRINTSETTINGSSERVICE_CID \
+ { \
+ 0x841387c8, 0x72e6, 0x484b, { \
+ 0x92, 0x96, 0xbf, 0x6e, 0xea, 0x80, 0xd5, 0x8a \
+ } \
+ }
+
+#define NS_PRINTER_LIST_CID \
+ { \
+ 0xa6cf9129, 0x15b3, 0x11d2, { \
+ 0x93, 0x2e, 0x00, 0x80, 0x5f, 0x8a, 0xdd, 0x32 \
+ } \
+ }
+
+#define NS_PRINTDIALOGSERVICE_CID \
+ { \
+ 0x06beec76, 0xa183, 0x4d9f, { \
+ 0x85, 0xdd, 0x08, 0x5f, 0x26, 0xda, 0x56, 0x5a \
+ } \
+ }
+
+#define NS_GFXINFO_CID \
+ { \
+ 0xd755a760, 0x9f27, 0x11df, { \
+ 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66, 0x42, 0x42 \
+ } \
+ }
+
+#define NS_WINDOWS_UIUTILS_CID \
+ { \
+ 0xe04a55e8, 0xfee3, 0x4ea2, { \
+ 0xa9, 0x8b, 0x41, 0xd2, 0x62, 0x1a, 0xdc, 0x3c \
+ } \
+ }
diff --git a/widget/nsXPLookAndFeel.cpp b/widget/nsXPLookAndFeel.cpp
new file mode 100644
index 0000000000..05b8952b04
--- /dev/null
+++ b/widget/nsXPLookAndFeel.cpp
@@ -0,0 +1,1547 @@
+/* -*- mode: C++; tab-width: 4; 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 "mozilla/ArrayUtils.h"
+
+#include "mozilla/LookAndFeel.h"
+#include "nscore.h"
+
+#include "nsXPLookAndFeel.h"
+#include "nsLookAndFeel.h"
+#include "HeadlessLookAndFeel.h"
+#include "RemoteLookAndFeel.h"
+#include "nsContentUtils.h"
+#include "nsCRT.h"
+#include "nsFont.h"
+#include "nsIFrame.h"
+#include "nsIXULRuntime.h"
+#include "nsLayoutUtils.h"
+#include "Theme.h"
+#include "SurfaceCacheUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/ServoStyleSet.h"
+#include "mozilla/ServoCSSParser.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/PreferenceSheet.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/widget/WidgetMessageUtils.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryScalarEnums.h"
+#include "mozilla/Try.h"
+
+#include "gfxPlatform.h"
+#include "gfxFont.h"
+
+#include "qcms.h"
+
+#include <bitset>
+
+using namespace mozilla;
+
+using IntID = mozilla::LookAndFeel::IntID;
+using FloatID = mozilla::LookAndFeel::FloatID;
+using ColorID = mozilla::LookAndFeel::ColorID;
+using FontID = mozilla::LookAndFeel::FontID;
+
+template <typename Index, typename Value, Index kEnd>
+class EnumeratedCache {
+ mozilla::EnumeratedArray<Index, kEnd, Value> mEntries;
+ std::bitset<size_t(kEnd)> mValidity;
+
+ public:
+ constexpr EnumeratedCache() = default;
+
+ bool IsValid(Index aIndex) const { return mValidity[size_t(aIndex)]; }
+
+ const Value* Get(Index aIndex) const {
+ return IsValid(aIndex) ? &mEntries[aIndex] : nullptr;
+ }
+
+ void Insert(Index aIndex, Value aValue) {
+ mValidity[size_t(aIndex)] = true;
+ mEntries[aIndex] = aValue;
+ }
+
+ void Remove(Index aIndex) {
+ mValidity[size_t(aIndex)] = false;
+ mEntries[aIndex] = Value();
+ }
+
+ void Clear() {
+ mValidity.reset();
+ for (auto& entry : mEntries) {
+ entry = Value();
+ }
+ }
+};
+
+using ColorCache = EnumeratedCache<ColorID, Maybe<nscolor>, ColorID::End>;
+
+struct ColorCaches {
+ using UseStandins = LookAndFeel::UseStandins;
+
+ ColorCache mCaches[2][2];
+
+ constexpr ColorCaches() = default;
+
+ ColorCache& Get(ColorScheme aScheme, UseStandins aUseStandins) {
+ return mCaches[aScheme == ColorScheme::Dark]
+ [aUseStandins == UseStandins::Yes];
+ }
+
+ void Clear() {
+ for (auto& c : mCaches) {
+ for (auto& cache : c) {
+ cache.Clear();
+ }
+ }
+ }
+};
+
+static ColorCaches sColorCaches;
+
+static EnumeratedCache<FloatID, Maybe<float>, FloatID::End> sFloatCache;
+static EnumeratedCache<IntID, Maybe<int32_t>, IntID::End> sIntCache;
+static EnumeratedCache<FontID, widget::LookAndFeelFont, FontID::End> sFontCache;
+
+// To make one of these prefs toggleable from a reftest add a user
+// pref in testing/profiles/reftest/user.js. For example, to make
+// ui.useAccessibilityTheme toggleable, add:
+//
+// user_pref("ui.useAccessibilityTheme", 0);
+//
+// This needs to be of the same length and in the same order as
+// LookAndFeel::IntID values.
+static const char sIntPrefs[][45] = {
+ "ui.caretBlinkTime",
+ "ui.caretBlinkCount",
+ "ui.caretWidth",
+ "ui.caretVisibleWithSelection",
+ "ui.selectTextfieldsOnKeyFocus",
+ "ui.submenuDelay",
+ "ui.menusCanOverlapOSBar",
+ "ui.useOverlayScrollbars",
+ "ui.allowOverlayScrollbarsOverlap",
+ "ui.skipNavigatingDisabledMenuItem",
+ "ui.dragThresholdX",
+ "ui.dragThresholdY",
+ "ui.useAccessibilityTheme",
+ "ui.scrollArrowStyle",
+ "ui.scrollButtonLeftMouseButtonAction",
+ "ui.scrollButtonMiddleMouseButtonAction",
+ "ui.scrollButtonRightMouseButtonAction",
+ "ui.treeOpenDelay",
+ "ui.treeCloseDelay",
+ "ui.treeLazyScrollDelay",
+ "ui.treeScrollDelay",
+ "ui.treeScrollLinesMax",
+ "accessibility.tabfocus", // Weird one...
+ "ui.chosenMenuItemsShouldBlink",
+ "ui.windowsAccentColorInTitlebar",
+ "ui.macBigSurTheme",
+ "ui.macRTL",
+ "ui.alertNotificationOrigin",
+ "ui.scrollToClick",
+ "ui.IMERawInputUnderlineStyle",
+ "ui.IMESelectedRawTextUnderlineStyle",
+ "ui.IMEConvertedTextUnderlineStyle",
+ "ui.IMESelectedConvertedTextUnderlineStyle",
+ "ui.SpellCheckerUnderlineStyle",
+ "ui.menuBarDrag",
+ "ui.scrollbarButtonAutoRepeatBehavior",
+ "ui.tooltipDelay",
+ "ui.swipeAnimationEnabled",
+ "ui.scrollbarDisplayOnMouseMove",
+ "ui.scrollbarFadeBeginDelay",
+ "ui.scrollbarFadeDuration",
+ "ui.contextMenuOffsetVertical",
+ "ui.contextMenuOffsetHorizontal",
+ "ui.GtkCSDAvailable",
+ "ui.GtkCSDMinimizeButton",
+ "ui.GtkCSDMaximizeButton",
+ "ui.GtkCSDCloseButton",
+ "ui.GtkCSDMinimizeButtonPosition",
+ "ui.GtkCSDMaximizeButtonPosition",
+ "ui.GtkCSDCloseButtonPosition",
+ "ui.GtkCSDReversedPlacement",
+ "ui.systemUsesDarkTheme",
+ "ui.prefersReducedMotion",
+ "ui.prefersReducedTransparency",
+ "ui.invertedColors",
+ "ui.primaryPointerCapabilities",
+ "ui.allPointerCapabilities",
+ "ui.systemScrollbarSize",
+ "ui.touchDeviceSupportPresent",
+ "ui.titlebarRadius",
+ "ui.dynamicRange",
+ "ui.videoDynamicRange",
+ "ui.panelAnimations",
+ "ui.hideCursorWhileTyping",
+ "ui.gtkThemeFamily",
+};
+
+static_assert(ArrayLength(sIntPrefs) == size_t(LookAndFeel::IntID::End),
+ "Should have a pref for each int value");
+
+// This array MUST be kept in the same order as the float id list in
+// LookAndFeel.h
+// clang-format off
+static const char sFloatPrefs[][37] = {
+ "ui.IMEUnderlineRelativeSize",
+ "ui.SpellCheckerUnderlineRelativeSize",
+ "ui.caretAspectRatio",
+ "ui.textScaleFactor",
+ "ui.cursorScale",
+};
+// clang-format on
+
+static_assert(ArrayLength(sFloatPrefs) == size_t(LookAndFeel::FloatID::End),
+ "Should have a pref for each float value");
+
+// This array MUST be kept in the same order as the color list in
+// specified/color.rs
+static const char sColorPrefs[][41] = {
+ "ui.activeborder",
+ "ui.activecaption",
+ "ui.appworkspace",
+ "ui.background",
+ "ui.buttonface",
+ "ui.buttonhighlight",
+ "ui.buttonshadow",
+ "ui.buttontext",
+ "ui.buttonborder",
+ "ui.captiontext",
+ "ui.-moz-field",
+ "ui.-moz-disabledfield",
+ "ui.-moz-fieldtext",
+ "ui.mark",
+ "ui.marktext",
+ "ui.-moz-comboboxtext",
+ "ui.-moz-combobox",
+ "ui.graytext",
+ "ui.highlight",
+ "ui.highlighttext",
+ "ui.inactiveborder",
+ "ui.inactivecaption",
+ "ui.inactivecaptiontext",
+ "ui.infobackground",
+ "ui.infotext",
+ "ui.menu",
+ "ui.menutext",
+ "ui.scrollbar",
+ "ui.threeddarkshadow",
+ "ui.threedface",
+ "ui.threedhighlight",
+ "ui.threedlightshadow",
+ "ui.threedshadow",
+ "ui.window",
+ "ui.windowframe",
+ "ui.windowtext",
+ "ui.-moz-default-color",
+ "ui.-moz-default-background-color",
+ "ui.-moz-dialog",
+ "ui.-moz-dialogtext",
+ "ui.-moz-cellhighlight",
+ "ui.-moz_cellhighlighttext",
+ "ui.selecteditem",
+ "ui.selecteditemtext",
+ "ui.-moz-buttonhoverface",
+ "ui.-moz_buttonhovertext",
+ "ui.-moz_menuhover",
+ "ui.-moz_menuhoverdisabled",
+ "ui.-moz_menuhovertext",
+ "ui.-moz_menubarhovertext",
+ "ui.-moz_eventreerow",
+ "ui.-moz_oddtreerow",
+ "ui.-moz-buttonactivetext",
+ "ui.-moz-buttonactiveface",
+ "ui.-moz-buttondisabledface",
+ "ui.-moz-headerbar",
+ "ui.-moz-headerbartext",
+ "ui.-moz-headerbarinactive",
+ "ui.-moz-headerbarinactivetext",
+ "ui.-moz-mac-defaultbuttontext",
+ "ui.-moz-mac-focusring",
+ "ui.-moz_mac_disabledtoolbartext",
+ "ui.-moz-sidebar",
+ "ui.-moz-sidebartext",
+ "ui.-moz-sidebarborder",
+ "ui.accentcolor",
+ "ui.accentcolortext",
+ "ui.-moz-autofill-background",
+ "ui.-moz-nativehyperlinktext",
+ "ui.-moz-nativevisitedhyperlinktext",
+ "ui.-moz-hyperlinktext",
+ "ui.-moz-activehyperlinktext",
+ "ui.-moz-visitedhyperlinktext",
+ "ui.-moz-colheader",
+ "ui.-moz-colheadertext",
+ "ui.-moz-colheaderhover",
+ "ui.-moz-colheaderhovertext",
+ "ui.-moz-colheaderactive",
+ "ui.-moz-colheaderactivetext",
+ "ui.textSelectDisabledBackground",
+ "ui.textSelectAttentionBackground",
+ "ui.textSelectAttentionForeground",
+ "ui.textHighlightBackground",
+ "ui.textHighlightForeground",
+ "ui.IMERawInputBackground",
+ "ui.IMERawInputForeground",
+ "ui.IMERawInputUnderline",
+ "ui.IMESelectedRawTextBackground",
+ "ui.IMESelectedRawTextForeground",
+ "ui.IMESelectedRawTextUnderline",
+ "ui.IMEConvertedTextBackground",
+ "ui.IMEConvertedTextForeground",
+ "ui.IMEConvertedTextUnderline",
+ "ui.IMESelectedConvertedTextBackground",
+ "ui.IMESelectedConvertedTextForeground",
+ "ui.IMESelectedConvertedTextUnderline",
+ "ui.SpellCheckerUnderline",
+ "ui.themedScrollbar",
+ "ui.themedScrollbarInactive",
+ "ui.themedScrollbarThumb",
+ "ui.themedScrollbarThumbHover",
+ "ui.themedScrollbarThumbActive",
+ "ui.themedScrollbarThumbInactive",
+};
+
+static_assert(ArrayLength(sColorPrefs) == size_t(LookAndFeel::ColorID::End),
+ "Should have a pref for each color value");
+
+// This array MUST be kept in the same order as the SystemFont enum.
+static const char sFontPrefs[][41] = {
+ "ui.font.caption",
+ "ui.font.icon",
+ "ui.font.menu",
+ "ui.font.message-box",
+ "ui.font.small-caption",
+ "ui.font.status-bar",
+ "ui.font.-moz-pull-down-menu",
+ "ui.font.-moz-button",
+ "ui.font.-moz-list",
+ "ui.font.-moz-field",
+};
+
+static_assert(ArrayLength(sFontPrefs) == size_t(LookAndFeel::FontID::End),
+ "Should have a pref for each font value");
+
+const char* nsXPLookAndFeel::GetColorPrefName(ColorID aId) {
+ return sColorPrefs[size_t(aId)];
+}
+
+bool nsXPLookAndFeel::sInitialized = false;
+
+nsXPLookAndFeel* nsXPLookAndFeel::sInstance = nullptr;
+bool nsXPLookAndFeel::sShutdown = false;
+
+auto LookAndFeel::SystemZoomSettings() -> ZoomSettings {
+ ZoomSettings settings;
+ switch (StaticPrefs::browser_display_os_zoom_behavior()) {
+ case 0:
+ default:
+ break;
+ case 1:
+ settings.mFullZoom = GetTextScaleFactor();
+ break;
+ case 2:
+ settings.mTextZoom = GetTextScaleFactor();
+ break;
+ }
+ return settings;
+}
+
+// static
+nsXPLookAndFeel* nsXPLookAndFeel::GetInstance() {
+ if (sInstance) {
+ return sInstance;
+ }
+
+ NS_ENSURE_TRUE(!sShutdown, nullptr);
+
+ // If we're in a content process, then the parent process will have supplied
+ // us with an initial FullLookAndFeel object.
+ // We grab this data from the ContentChild,
+ // where it's been temporarily stashed, and initialize our new LookAndFeel
+ // object with it.
+
+ FullLookAndFeel* lnf = nullptr;
+
+ if (auto* cc = mozilla::dom::ContentChild::GetSingleton()) {
+ lnf = &cc->BorrowLookAndFeelData();
+ }
+
+ if (lnf) {
+ sInstance = new widget::RemoteLookAndFeel(std::move(*lnf));
+ } else if (gfxPlatform::IsHeadless()) {
+ sInstance = new widget::HeadlessLookAndFeel();
+ } else {
+ sInstance = new nsLookAndFeel();
+ }
+
+ // This is only ever used once during initialization, and can be cleared now.
+ if (lnf) {
+ *lnf = {};
+ }
+
+ widget::Theme::Init();
+ return sInstance;
+}
+
+// static
+void nsXPLookAndFeel::Shutdown() {
+ if (sShutdown) {
+ return;
+ }
+
+ sShutdown = true;
+ delete sInstance;
+ sInstance = nullptr;
+
+ // This keeps strings alive, so need to clear to make leak checking happy.
+ sFontCache.Clear();
+
+ widget::Theme::Shutdown();
+}
+
+static void IntPrefChanged(const nsACString& aPref) {
+ // Most Int prefs can't change our system colors or fonts, but
+ // ui.systemUsesDarkTheme can, since it affects the effective color-scheme
+ // (affecting system colors).
+ auto changeKind = aPref.EqualsLiteral("ui.systemUsesDarkTheme")
+ ? widget::ThemeChangeKind::Style
+ : widget::ThemeChangeKind::MediaQueriesOnly;
+ LookAndFeel::NotifyChangedAllWindows(changeKind);
+}
+
+static void FloatPrefChanged(const nsACString& aPref) {
+ // Most float prefs can't change our system colors or fonts, but
+ // textScaleFactor affects layout.
+ auto changeKind = aPref.EqualsLiteral("ui.textScaleFactor")
+ ? widget::ThemeChangeKind::StyleAndLayout
+ : widget::ThemeChangeKind::MediaQueriesOnly;
+ LookAndFeel::NotifyChangedAllWindows(changeKind);
+}
+
+static void ColorPrefChanged() {
+ // Color prefs affect style, because they by definition change system colors.
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
+}
+
+static void FontPrefChanged() {
+ // Color prefs affect style, because they by definition change system fonts.
+ LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
+}
+
+// static
+void nsXPLookAndFeel::OnPrefChanged(const char* aPref, void* aClosure) {
+ nsDependentCString prefName(aPref);
+ for (const char* pref : sIntPrefs) {
+ if (prefName.Equals(pref)) {
+ IntPrefChanged(prefName);
+ return;
+ }
+ }
+
+ for (const char* pref : sFloatPrefs) {
+ if (prefName.Equals(pref)) {
+ FloatPrefChanged(prefName);
+ return;
+ }
+ }
+
+ for (const char* pref : sColorPrefs) {
+ // We use StringBeginsWith to handle .dark prefs too.
+ if (StringBeginsWith(prefName, nsDependentCString(pref))) {
+ ColorPrefChanged();
+ return;
+ }
+ }
+
+ for (const char* pref : sFontPrefs) {
+ if (StringBeginsWith(prefName, nsDependentCString(pref))) {
+ FontPrefChanged();
+ return;
+ }
+ }
+}
+
+static constexpr struct {
+ nsLiteralCString mName;
+ widget::ThemeChangeKind mChangeKind =
+ widget::ThemeChangeKind::MediaQueriesOnly;
+} kMediaQueryPrefs[] = {
+ // Affects whether standins are used for the accent color.
+ {"widget.non-native-theme.use-theme-accent"_ns,
+ widget::ThemeChangeKind::Style},
+ // These three affect system colors on Windows.
+ {"widget.windows.uwp-system-colors.enabled"_ns,
+ widget::ThemeChangeKind::Style},
+ {"widget.windows.uwp-system-colors.highlight-accent"_ns,
+ widget::ThemeChangeKind::Style},
+ // Affects env().
+ {"layout.css.prefers-color-scheme.content-override"_ns,
+ widget::ThemeChangeKind::Style},
+ // Affects media queries and scrollbar sizes, so gotta relayout.
+ {"widget.gtk.overlay-scrollbars.enabled"_ns,
+ widget::ThemeChangeKind::StyleAndLayout},
+ // Affects zoom settings which includes text and full zoom.
+ {"browser.display.os-zoom-behavior"_ns,
+ widget::ThemeChangeKind::StyleAndLayout},
+ // This affects system colors on Linux.
+ {"widget.gtk.libadwaita-colors.enabled"_ns, widget::ThemeChangeKind::Style},
+ // This affects not only the media query, but also the native theme, so we
+ // need to re-layout.
+ {"browser.theme.toolbar-theme"_ns, widget::ThemeChangeKind::AllBits},
+ {"browser.theme.content-theme"_ns},
+};
+
+// Read values from the user's preferences.
+// This is done once at startup, but since the user's preferences
+// haven't actually been read yet at that time, we also have to
+// set a callback to inform us of changes to each pref.
+void nsXPLookAndFeel::Init() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ // Say we're already initialized, and take the chance that it might fail;
+ // protects against some other process writing to our static variables.
+ sInitialized = true;
+
+ if (XRE_IsParentProcess()) {
+ nsLayoutUtils::RecomputeSmoothScrollDefault();
+ }
+
+ // XXX If we could reorganize the pref names, we should separate the branch
+ // for each types. Then, we could reduce the unnecessary loop from
+ // nsXPLookAndFeel::OnPrefChanged().
+ Preferences::RegisterPrefixCallback(OnPrefChanged, "ui.");
+ // We really do just want the accessibility.tabfocus pref, not other prefs
+ // that start with that string.
+ Preferences::RegisterCallback(OnPrefChanged, "accessibility.tabfocus");
+
+ for (const auto& pref : kMediaQueryPrefs) {
+ Preferences::RegisterCallback(
+ [](const char*, void* aChangeKind) {
+ auto changeKind =
+ widget::ThemeChangeKind(reinterpret_cast<uintptr_t>(aChangeKind));
+ LookAndFeel::NotifyChangedAllWindows(changeKind);
+ },
+ pref.mName, reinterpret_cast<void*>(uintptr_t(pref.mChangeKind)));
+ }
+}
+
+nsXPLookAndFeel::~nsXPLookAndFeel() {
+ NS_ASSERTION(sInstance == this,
+ "This destroying instance isn't the singleton instance");
+ sInstance = nullptr;
+}
+
+static bool IsSpecialColor(LookAndFeel::ColorID aID, nscolor aColor) {
+ using ColorID = LookAndFeel::ColorID;
+
+ if (aColor == NS_SAME_AS_FOREGROUND_COLOR) {
+ return true;
+ }
+
+ switch (aID) {
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ case ColorID::SpellCheckerUnderline:
+ return NS_IS_SELECTION_SPECIAL_COLOR(aColor);
+ default:
+ break;
+ }
+ /*
+ * In GetColor(), every color that is not a special color is color
+ * corrected. Use false to make other colors color corrected.
+ */
+ return false;
+}
+
+nscolor nsXPLookAndFeel::GetStandinForNativeColor(ColorID aID,
+ ColorScheme aScheme) {
+ if (aScheme == ColorScheme::Dark) {
+ if (auto color = GenericDarkColor(aID)) {
+ return *color;
+ }
+ }
+
+ // The stand-in colors are taken from what the non-native theme needs (for
+ // field/button colors), the Windows 7 Aero theme except Mac-specific colors
+ // which are taken from Mac OS 10.7.
+
+#define COLOR(name_, r, g, b) \
+ case ColorID::name_: \
+ return NS_RGB(r, g, b);
+
+#define COLORA(name_, r, g, b, a) \
+ case ColorID::name_: \
+ return NS_RGBA(r, g, b, a);
+
+ switch (aID) {
+ // These are here for the purposes of headless mode.
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ return NS_TRANSPARENT;
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ return NS_SAME_AS_FOREGROUND_COLOR;
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ return NS_40PERCENT_FOREGROUND_COLOR;
+ case ColorID::Accentcolor:
+ return widget::sDefaultAccent.ToABGR();
+ case ColorID::Accentcolortext:
+ return widget::sDefaultAccentText.ToABGR();
+ COLOR(SpellCheckerUnderline, 0xff, 0x00, 0x00)
+ COLOR(TextSelectDisabledBackground, 0xAA, 0xAA, 0xAA)
+
+ // Titlebar colors
+ // deprecated in CSS Color Level 4, same as Buttonborder:
+ COLOR(Activeborder, 0xE3, 0xE3, 0xE3)
+ // deprecated in CSS Color Level 4, same as Buttonborder:
+ COLOR(Inactiveborder, 0xE3, 0xE3, 0xE3)
+ // deprecated in CSS Color Level 4, same as Canvas/Window:
+ COLOR(Activecaption, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Canvas/Window:
+ COLOR(Inactivecaption, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Canvastext/Windowtext:
+ COLOR(Captiontext, 0x00, 0x00, 0x00)
+ // deprecated in CSS Color Level 4, same as Graytext:
+ COLOR(Inactivecaptiontext, 0x6D, 0x6D, 0x6D)
+
+ // CSS 2 colors:
+ // deprecated in CSS Color Level 4, same as Canvas/Window:
+ COLOR(Appworkspace, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Canvas/Window:
+ COLOR(Background, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Buttonface:
+ COLOR(Buttonhighlight, 0xE9, 0xE9, 0xED)
+ // deprecated in CSS Color Level 4, same as Buttonface:
+ COLOR(Buttonshadow, 0xE9, 0xE9, 0xED)
+
+ // Buttons and comboboxes should be kept in sync since they are drawn with
+ // the same colors by the non-native theme.
+ COLOR(Buttonface, 0xE9, 0xE9, 0xED)
+ COLORA(MozButtondisabledface, 0xE9, 0xE9, 0xED, 128)
+
+ COLOR(MozCombobox, 0xE9, 0xE9, 0xED)
+
+ COLOR(Buttontext, 0x00, 0x00, 0x00)
+ COLOR(MozComboboxtext, 0x00, 0x00, 0x00)
+
+ COLOR(Graytext, 0x6D, 0x6D, 0x6D)
+ COLOR(Highlight, 0x33, 0x99, 0xFF)
+ COLOR(Highlighttext, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Canvas/Window:
+ COLOR(Infobackground, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Canvastext/Windowtext:
+ COLOR(Infotext, 0x00, 0x00, 0x00)
+ // deprecated in CSS Color Level 4, same as Canvas/Window:
+ COLOR(Menu, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Canvastext/Windowtext:
+ COLOR(Menutext, 0x00, 0x00, 0x00)
+ // deprecated in CSS Color Level 4, same as Canvas/Window:
+ COLOR(Scrollbar, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Buttonborder:
+ COLOR(Threeddarkshadow, 0xE3, 0xE3, 0xE3)
+ // deprecated in CSS Color Level 4, same as Buttonface:
+ COLOR(Threedface, 0xE9, 0xE9, 0xED)
+ // deprecated in CSS Color Level 4, same as Buttonborder:
+ COLOR(Threedhighlight, 0xE3, 0xE3, 0xE3)
+ COLOR(Threedlightshadow, 0xE3, 0xE3, 0xE3)
+ // deprecated in CSS Color Level 4, same as Buttonborder:
+ COLOR(Threedshadow, 0xE3, 0xE3, 0xE3)
+ COLOR(Buttonborder, 0xE3, 0xE3, 0xE3)
+ COLOR(Mark, 0xFF, 0xFF, 0x00)
+ COLOR(Marktext, 0x00, 0x00, 0x00)
+ COLOR(Window, 0xFF, 0xFF, 0xFF)
+ // deprecated in CSS Color Level 4, same as Buttonborder:
+ COLOR(Windowframe, 0xE3, 0xE3, 0xE3)
+ COLOR(Windowtext, 0x00, 0x00, 0x00)
+ COLOR(Field, 0xFF, 0xFF, 0xFF)
+ COLORA(MozDisabledfield, 0xFF, 0xFF, 0xFF, 128)
+ COLOR(Fieldtext, 0x00, 0x00, 0x00)
+ COLOR(MozDialog, 0xF0, 0xF0, 0xF0)
+ COLOR(MozDialogtext, 0x00, 0x00, 0x00)
+ COLOR(MozColheadertext, 0x00, 0x00, 0x00)
+ COLOR(MozColheaderhovertext, 0x00, 0x00, 0x00)
+ COLOR(MozCellhighlight, 0xF0, 0xF0, 0xF0)
+ COLOR(MozCellhighlighttext, 0x00, 0x00, 0x00)
+ COLOR(Selecteditem, 0x33, 0x99, 0xFF)
+ COLOR(Selecteditemtext, 0xFF, 0xFF, 0xFF)
+ COLOR(MozButtonhoverface, 0xd0, 0xd0, 0xd7)
+ COLOR(MozButtonhovertext, 0x00, 0x00, 0x00)
+ COLOR(MozButtonactiveface, 0xb1, 0xb1, 0xb9)
+ COLOR(MozButtonactivetext, 0x00, 0x00, 0x00)
+ COLOR(MozMenuhover, 0x33, 0x99, 0xFF)
+ COLOR(MozMenuhovertext, 0x00, 0x00, 0x00)
+ COLOR(MozMenubarhovertext, 0x00, 0x00, 0x00)
+ COLOR(MozMenuhoverdisabled, 0xF0, 0xF0, 0xF0)
+ COLOR(MozEventreerow, 0xFF, 0xFF, 0xFF)
+ COLOR(MozOddtreerow, 0xFF, 0xFF, 0xFF)
+ COLOR(MozMacFocusring, 0x60, 0x9D, 0xD7)
+ COLOR(MozMacDisabledtoolbartext, 0x3F, 0x3F, 0x3F)
+ // Seems to be the default color (hardcoded because of bug 1065998)
+ COLOR(MozNativehyperlinktext, 0x00, 0x66, 0xCC)
+ COLOR(MozNativevisitedhyperlinktext, 0x55, 0x1A, 0x8B)
+ default:
+ break;
+ }
+ return NS_RGB(0xFF, 0xFF, 0xFF);
+}
+
+#undef COLOR
+#undef COLORA
+
+// Taken from in-content/common.inc.css's dark theme.
+Maybe<nscolor> nsXPLookAndFeel::GenericDarkColor(ColorID aID) {
+ nscolor color = NS_RGB(0, 0, 0);
+ static constexpr nscolor kWindowBackground = NS_RGB(28, 27, 34);
+ static constexpr nscolor kWindowText = NS_RGB(251, 251, 254);
+ switch (aID) {
+ case ColorID::Window: // --in-content-page-background
+ case ColorID::Background:
+ case ColorID::Appworkspace:
+ case ColorID::Scrollbar:
+ case ColorID::Infobackground:
+ color = kWindowBackground;
+ break;
+
+ case ColorID::Menu:
+ color = NS_RGB(0x2b, 0x2a, 0x33);
+ break;
+
+ case ColorID::MozMenuhovertext:
+ case ColorID::MozMenubarhovertext:
+ case ColorID::Menutext:
+ color = NS_RGB(0xfb, 0xfb, 0xfe);
+ break;
+
+ case ColorID::MozMenuhover:
+ color = NS_RGB(0x52, 0x52, 0x5e);
+ break;
+
+ case ColorID::MozMenuhoverdisabled:
+ color = NS_RGB(0x3a, 0x39, 0x44);
+ break;
+
+ case ColorID::MozEventreerow:
+ case ColorID::MozOddtreerow:
+ case ColorID::MozDialog: // --in-content-box-background
+ color = NS_RGB(35, 34, 43);
+ break;
+ case ColorID::Windowtext: // --in-content-page-color
+ case ColorID::MozDialogtext:
+ case ColorID::MozSidebartext:
+ case ColorID::Fieldtext:
+ case ColorID::Infotext:
+ case ColorID::Buttontext: // --in-content-button-text-color (via
+ // --in-content-page-color)
+ case ColorID::MozComboboxtext:
+ case ColorID::MozButtonhovertext:
+ case ColorID::MozButtonactivetext:
+ case ColorID::MozHeaderbartext:
+ case ColorID::MozHeaderbarinactivetext:
+ case ColorID::Captiontext:
+ case ColorID::Inactivecaptiontext: // TODO(emilio): Maybe make
+ // Inactivecaptiontext Graytext?
+ case ColorID::MozColheadertext:
+ case ColorID::MozColheaderhovertext:
+ case ColorID::MozColheaderactivetext:
+ color = kWindowText;
+ break;
+ case ColorID::Buttonshadow:
+ case ColorID::Threedshadow:
+ case ColorID::MozSidebarborder:
+ case ColorID::Threedlightshadow:
+ case ColorID::Threedhighlight:
+ case ColorID::Windowframe:
+ case ColorID::Buttonborder: // --in-content-box-border-color computed
+ // with kWindowText above
+ // kWindowBackground.
+ case ColorID::Graytext: // opacity: 0.4 of kWindowText blended over the
+ // "Window" background color, which happens to be
+ // the same :-)
+ color = NS_ComposeColors(kWindowBackground, NS_RGBA(251, 251, 254, 102));
+ break;
+ case ColorID::MozCellhighlight:
+ case ColorID::Selecteditem: // --in-content-primary-button-background /
+ // --in-content-item-selected
+ color = NS_RGB(0, 221, 255);
+ break;
+ case ColorID::MozSidebar:
+ case ColorID::Field:
+ case ColorID::Buttonface: // --in-content-button-background
+ case ColorID::Buttonhighlight:
+ case ColorID::MozColheader:
+ case ColorID::Threedface:
+ case ColorID::MozCombobox:
+ case ColorID::MozCellhighlighttext:
+ case ColorID::Selecteditemtext: // --in-content-primary-button-text-color /
+ // --in-content-item-selected-text
+ color = NS_RGB(43, 42, 51);
+ break;
+ case ColorID::Threeddarkshadow: // Same as Threedlightshadow but with the
+ // background.
+ case ColorID::MozDisabledfield: // opacity: 0.4 of the face above blended
+ // over the "Window" background color.
+ case ColorID::MozButtondisabledface:
+ color = NS_ComposeColors(kWindowBackground, NS_RGBA(43, 42, 51, 102));
+ break;
+ case ColorID::MozButtonhoverface: // --in-content-button-background-hover
+ case ColorID::MozColheaderhover:
+ color = NS_RGB(82, 82, 94);
+ break;
+ case ColorID::MozButtonactiveface: // --in-content-button-background-active
+ case ColorID::MozColheaderactive:
+ color = NS_RGB(91, 91, 102);
+ break;
+ case ColorID::Highlight:
+ color = NS_RGBA(0, 221, 255, 78);
+ break;
+ case ColorID::Highlighttext:
+ color = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::MozNativehyperlinktext:
+ // If you change this color, you probably also want to change the default
+ // value of browser.anchor_color.dark.
+ color = NS_RGB(0x8c, 0x8c, 0xff);
+ break;
+ case ColorID::MozNativevisitedhyperlinktext:
+ // If you change this color, you probably also want to change the default
+ // value of browser.visited_color.dark.
+ color = NS_RGB(0xff, 0xad, 0xff);
+ break;
+ case ColorID::SpellCheckerUnderline:
+ // This is the default for active links in dark mode as well
+ // (browser.active_color.dark). See bug 1755564 for some analysis and
+ // other options too.
+ color = NS_RGB(0xff, 0x66, 0x66);
+ break;
+ case ColorID::Activeborder:
+ case ColorID::Inactiveborder:
+ color = NS_RGB(57, 57, 57);
+ break;
+ case ColorID::MozHeaderbar:
+ case ColorID::MozHeaderbarinactive:
+ case ColorID::Activecaption:
+ case ColorID::Inactivecaption:
+ color = NS_RGB(28, 27, 34);
+ break;
+ default:
+ return Nothing();
+ }
+ return Some(color);
+}
+
+// Uncomment the #define below if you want to debug system color use in a skin
+// that uses them. When set, it will make all system color pairs that are
+// appropriate for foreground/background pairing the same. This means if the
+// skin is using system colors correctly you will not be able to see *any* text.
+//
+// #define DEBUG_SYSTEM_COLOR_USE
+
+#ifdef DEBUG_SYSTEM_COLOR_USE
+static nsresult SystemColorUseDebuggingColor(LookAndFeel::ColorID aID,
+ nscolor& aResult) {
+ using ColorID = LookAndFeel::ColorID;
+
+ switch (aID) {
+ // css2 http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
+ case ColorID::Activecaption:
+ // active window caption background
+ case ColorID::Captiontext:
+ // text in active window caption
+ aResult = NS_RGB(0xff, 0x00, 0x00);
+ break;
+
+ case ColorID::Highlight:
+ // background of selected item
+ case ColorID::Highlighttext:
+ // text of selected item
+ aResult = NS_RGB(0xff, 0xff, 0x00);
+ break;
+
+ case ColorID::Inactivecaption:
+ // inactive window caption
+ case ColorID::Inactivecaptiontext:
+ // text in inactive window caption
+ aResult = NS_RGB(0x66, 0x66, 0x00);
+ break;
+
+ case ColorID::Infobackground:
+ // tooltip background color
+ case ColorID::Infotext:
+ // tooltip text color
+ aResult = NS_RGB(0x00, 0xff, 0x00);
+ break;
+
+ case ColorID::Menu:
+ // menu background
+ case ColorID::Menutext:
+ // menu text
+ aResult = NS_RGB(0x00, 0xff, 0xff);
+ break;
+
+ case ColorID::Threedface:
+ case ColorID::Buttonface:
+ // 3-D face color
+ case ColorID::Buttontext:
+ // text on push buttons
+ aResult = NS_RGB(0x00, 0x66, 0x66);
+ break;
+
+ case ColorID::Window:
+ case ColorID::Windowtext:
+ aResult = NS_RGB(0x00, 0x00, 0xff);
+ break;
+
+ // from the CSS3 working draft (not yet finalized)
+ // http://www.w3.org/tr/2000/wd-css3-userint-20000216.html#color
+
+ case ColorID::Field:
+ case ColorID::Fieldtext:
+ aResult = NS_RGB(0xff, 0x00, 0xff);
+ break;
+
+ case ColorID::MozDialog:
+ case ColorID::MozDialogtext:
+ aResult = NS_RGB(0x66, 0x00, 0x66);
+ break;
+
+ default:
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return NS_OK;
+}
+#endif
+
+static nsresult GetPrefColor(const char* aPref, nscolor& aResult) {
+ nsAutoCString colorStr;
+ MOZ_TRY(Preferences::GetCString(aPref, colorStr));
+ if (!ServoCSSParser::ComputeColor(nullptr, NS_RGB(0, 0, 0), colorStr,
+ &aResult)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+static nsresult GetColorFromPref(LookAndFeel::ColorID aID, ColorScheme aScheme,
+ nscolor& aResult) {
+ const char* prefName = sColorPrefs[size_t(aID)];
+ if (aScheme == ColorScheme::Dark) {
+ nsAutoCString darkPrefName(prefName);
+ darkPrefName.Append(".dark");
+ if (NS_SUCCEEDED(GetPrefColor(darkPrefName.get(), aResult))) {
+ return NS_OK;
+ }
+ }
+ return GetPrefColor(prefName, aResult);
+}
+
+// All these routines will return NS_OK if they have a value,
+// in which case the nsLookAndFeel should use that value;
+// otherwise we'll return NS_ERROR_NOT_AVAILABLE, in which case, the
+// platform-specific nsLookAndFeel should use its own values instead.
+nsresult nsXPLookAndFeel::GetColorValue(ColorID aID, ColorScheme aScheme,
+ UseStandins aUseStandins,
+ nscolor& aResult) {
+ if (!sInitialized) {
+ Init();
+ }
+
+#ifdef DEBUG_SYSTEM_COLOR_USE
+ if (NS_SUCCEEDED(SystemColorUseDebuggingColor(aID, aResult))) {
+ return NS_OK;
+ }
+#endif
+
+ auto& cache = sColorCaches.Get(aScheme, aUseStandins);
+ if (const auto* cached = cache.Get(aID)) {
+ if (cached->isNothing()) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult = cached->value();
+ return NS_OK;
+ }
+
+ // NOTE: Servo holds a lock and the main thread is paused, so writing to the
+ // global cache here is fine.
+ auto result = GetUncachedColor(aID, aScheme, aUseStandins);
+ cache.Insert(aID, result);
+ if (!result) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult = *result;
+ return NS_OK;
+}
+
+Maybe<nscolor> nsXPLookAndFeel::GetUncachedColor(ColorID aID,
+ ColorScheme aScheme,
+ UseStandins aUseStandins) {
+ if (aUseStandins == UseStandins::Yes) {
+ return Some(GetStandinForNativeColor(aID, aScheme));
+ }
+ nscolor r;
+ if (NS_SUCCEEDED(GetColorFromPref(aID, aScheme, r))) {
+ return Some(r);
+ }
+ if (NS_SUCCEEDED(NativeGetColor(aID, aScheme, r))) {
+ if (gfxPlatform::GetCMSMode() == CMSMode::All && !IsSpecialColor(aID, r)) {
+ qcms_transform* transform = gfxPlatform::GetCMSInverseRGBTransform();
+ if (transform) {
+ uint8_t color[4];
+ color[0] = NS_GET_R(r);
+ color[1] = NS_GET_G(r);
+ color[2] = NS_GET_B(r);
+ color[3] = NS_GET_A(r);
+ qcms_transform_data(transform, color, color, 1);
+ r = NS_RGBA(color[0], color[1], color[2], color[3]);
+ }
+ }
+
+ return Some(r);
+ }
+ return Nothing();
+}
+
+nsresult nsXPLookAndFeel::GetIntValue(IntID aID, int32_t& aResult) {
+ if (!sInitialized) {
+ Init();
+ }
+
+ if (const auto* cached = sIntCache.Get(aID)) {
+ if (cached->isNothing()) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult = cached->value();
+ return NS_OK;
+ }
+
+ if (NS_SUCCEEDED(Preferences::GetInt(sIntPrefs[size_t(aID)], &aResult))) {
+ sIntCache.Insert(aID, Some(aResult));
+ return NS_OK;
+ }
+
+ if (NS_FAILED(NativeGetInt(aID, aResult))) {
+ sIntCache.Insert(aID, Nothing());
+ return NS_ERROR_FAILURE;
+ }
+
+ sIntCache.Insert(aID, Some(aResult));
+ return NS_OK;
+}
+
+nsresult nsXPLookAndFeel::GetFloatValue(FloatID aID, float& aResult) {
+ if (!sInitialized) {
+ Init();
+ }
+
+ if (const auto* cached = sFloatCache.Get(aID)) {
+ if (cached->isNothing()) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult = cached->value();
+ return NS_OK;
+ }
+
+ int32_t pref = 0;
+ if (NS_SUCCEEDED(Preferences::GetInt(sFloatPrefs[size_t(aID)], &pref))) {
+ aResult = float(pref) / 100.0f;
+ sFloatCache.Insert(aID, Some(aResult));
+ return NS_OK;
+ }
+
+ if (NS_FAILED(NativeGetFloat(aID, aResult))) {
+ sFloatCache.Insert(aID, Nothing());
+ return NS_ERROR_FAILURE;
+ }
+
+ sFloatCache.Insert(aID, Some(aResult));
+ return NS_OK;
+}
+
+bool nsXPLookAndFeel::LookAndFeelFontToStyle(const LookAndFeelFont& aFont,
+ nsString& aName,
+ gfxFontStyle& aStyle) {
+ if (!aFont.haveFont()) {
+ return false;
+ }
+ aName = aFont.name();
+ aStyle = gfxFontStyle();
+ aStyle.size = aFont.size();
+ aStyle.weight = FontWeight::FromInt(aFont.weight());
+ aStyle.style =
+ aFont.italic() ? FontSlantStyle::ITALIC : FontSlantStyle::NORMAL;
+ aStyle.systemFont = true;
+ return true;
+}
+
+widget::LookAndFeelFont nsXPLookAndFeel::StyleToLookAndFeelFont(
+ const nsAString& aName, const gfxFontStyle& aStyle) {
+ LookAndFeelFont font;
+ font.haveFont() = true;
+ font.name() = aName;
+ font.size() = aStyle.size;
+ font.weight() = aStyle.weight.ToFloat();
+ font.italic() = aStyle.style.IsItalic();
+ MOZ_ASSERT(aStyle.style.IsNormal() || aStyle.style.IsItalic(),
+ "Cannot handle oblique font style");
+#ifdef DEBUG
+ {
+ // Assert that all the remaining font style properties have their
+ // default values.
+ gfxFontStyle candidate = aStyle;
+ gfxFontStyle defaults{};
+ candidate.size = defaults.size;
+ candidate.weight = defaults.weight;
+ candidate.style = defaults.style;
+ MOZ_ASSERT(candidate.Equals(defaults),
+ "Some font style properties not supported");
+ }
+#endif
+ return font;
+}
+
+bool nsXPLookAndFeel::GetFontValue(FontID aID, nsString& aName,
+ gfxFontStyle& aStyle) {
+ if (const LookAndFeelFont* cached = sFontCache.Get(aID)) {
+ return LookAndFeelFontToStyle(*cached, aName, aStyle);
+ }
+
+ LookAndFeelFont font;
+ auto GetFontsFromPrefs = [&]() -> bool {
+ nsDependentCString pref(sFontPrefs[size_t(aID)]);
+ if (NS_FAILED(Preferences::GetString(pref.get(), aName))) {
+ return false;
+ }
+ font.haveFont() = true;
+ font.name() = aName;
+ font.size() = Preferences::GetFloat(nsAutoCString(pref + ".size"_ns).get());
+ // This is written this way rather than using the fallback so that an empty
+ // pref (such like the one about:config creates) doesn't cause system fonts
+ // to have zero-size.
+ if (font.size() < 1.0f) {
+ font.size() = StyleFONT_MEDIUM_PX;
+ }
+ font.weight() = Preferences::GetFloat(
+ nsAutoCString(pref + ".weight"_ns).get(), FontWeight::NORMAL.ToFloat());
+ font.italic() =
+ Preferences::GetBool(nsAutoCString(pref + ".italic"_ns).get());
+ return true;
+ };
+
+ if (GetFontsFromPrefs()) {
+ LookAndFeelFontToStyle(font, aName, aStyle);
+ } else if (NativeGetFont(aID, aName, aStyle)) {
+ font = StyleToLookAndFeelFont(aName, aStyle);
+ } else {
+ MOZ_ASSERT(!font.haveFont());
+ }
+ bool success = font.haveFont();
+ sFontCache.Insert(aID, std::move(font));
+ return success;
+}
+
+void nsXPLookAndFeel::RefreshImpl() {
+ // Wipe out our caches.
+ sColorCaches.Clear();
+ sFontCache.Clear();
+ sFloatCache.Clear();
+ sIntCache.Clear();
+
+ if (XRE_IsParentProcess()) {
+ nsLayoutUtils::RecomputeSmoothScrollDefault();
+ // Clear any cached FullLookAndFeel data, which is now invalid.
+ widget::RemoteLookAndFeel::ClearCachedData();
+ }
+}
+
+static bool sRecordedLookAndFeelTelemetry = false;
+
+void nsXPLookAndFeel::RecordTelemetry() {
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (sRecordedLookAndFeelTelemetry) {
+ return;
+ }
+
+ sRecordedLookAndFeelTelemetry = true;
+
+ int32_t i;
+ Telemetry::ScalarSet(
+ Telemetry::ScalarID::WIDGET_DARK_MODE,
+ NS_SUCCEEDED(GetIntValue(IntID::SystemUsesDarkTheme, i)) && i != 0);
+
+ RecordLookAndFeelSpecificTelemetry();
+}
+
+namespace mozilla {
+
+bool LookAndFeel::sGlobalThemeChanged;
+static widget::ThemeChangeKind sGlobalThemeChangeKind{0};
+
+void LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind aKind) {
+ sGlobalThemeChanged = true;
+ sGlobalThemeChangeKind |= aKind;
+
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ const char16_t kind[] = {char16_t(aKind), 0};
+ obs->NotifyObservers(nullptr, "internal-look-and-feel-changed", kind);
+ }
+}
+
+void LookAndFeel::DoHandleGlobalThemeChange() {
+ MOZ_ASSERT(sGlobalThemeChanged);
+ sGlobalThemeChanged = false;
+ auto kind = std::exchange(sGlobalThemeChangeKind, widget::ThemeChangeKind(0));
+
+ // Tell the theme that it changed, so it can flush any handles to stale theme
+ // data.
+ //
+ // We can use the *DoNotUseDirectly functions directly here, because we want
+ // to notify all possible themes in a given process (but just once).
+ if (XRE_IsParentProcess() ||
+ !StaticPrefs::widget_non_native_theme_enabled()) {
+ if (nsCOMPtr<nsITheme> theme = do_GetNativeThemeDoNotUseDirectly()) {
+ theme->ThemeChanged();
+ }
+ }
+ if (nsCOMPtr<nsITheme> theme = do_GetBasicNativeThemeDoNotUseDirectly()) {
+ theme->ThemeChanged();
+ }
+
+ // Clear all cached LookAndFeel colors.
+ LookAndFeel::Refresh();
+
+ // Reset default background and foreground colors for the document since they
+ // may be using system colors, color scheme, etc.
+ PreferenceSheet::Refresh();
+
+ // Vector images (SVG) may be using theme colors so we discard all cached
+ // surfaces. (We could add a vector image only version of DiscardAll, but
+ // in bug 940625 we decided theme changes are rare enough not to bother.)
+ image::SurfaceCacheUtils::DiscardAll();
+
+ if (XRE_IsParentProcess()) {
+ dom::ContentParent::BroadcastThemeUpdate(kind);
+ }
+
+ nsContentUtils::AddScriptRunner(
+ NS_NewRunnableFunction("HandleGlobalThemeChange", [] {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "look-and-feel-changed", nullptr);
+ }
+ }));
+}
+
+#define BIT_FOR(_c) (1ull << size_t(ColorID::_c))
+
+// We want to use a non-native color scheme for the non-native theme (except in
+// high-contrast mode), so spoof some of the colors with stand-ins to prevent
+// lack of contrast.
+static constexpr std::bitset<size_t(ColorID::End)> sNonNativeThemeStandinColors{
+ // Used by default button styles.
+ BIT_FOR(Buttonface) | BIT_FOR(Buttontext) | BIT_FOR(MozButtonhoverface) |
+ BIT_FOR(MozButtonhovertext) | BIT_FOR(MozButtonactiveface) |
+ BIT_FOR(MozButtonactivetext) | BIT_FOR(MozButtondisabledface) |
+ BIT_FOR(Buttonborder) |
+ // Used by select elements.
+ BIT_FOR(MozCombobox) | BIT_FOR(MozComboboxtext) |
+ BIT_FOR(Threedlightshadow) |
+ // For symmetry with the above.
+ BIT_FOR(Threeddarkshadow) |
+ // Used by fieldset borders.
+ BIT_FOR(Threedface) |
+ // Used by input / textarea.
+ BIT_FOR(Field) | BIT_FOR(Fieldtext) |
+ // Used by disabled form controls.
+ BIT_FOR(MozDisabledfield) | BIT_FOR(Graytext) |
+ // Per spec, the following colors are deprecated, see
+ // https://drafts.csswg.org/css-color-4/#deprecated-system-colors
+ // should match ButtonFace:
+ BIT_FOR(Buttonhighlight) | BIT_FOR(Buttonshadow) | BIT_FOR(Threedface) |
+ // should match ButtonBorder:
+ BIT_FOR(Activeborder) | BIT_FOR(Inactiveborder) |
+ BIT_FOR(Threeddarkshadow) | BIT_FOR(Threedhighlight) |
+ BIT_FOR(Threedshadow) | BIT_FOR(Windowframe) |
+ // should match GrayText:
+ BIT_FOR(Inactivecaptiontext) |
+ // should match Canvas/Window:
+ BIT_FOR(Appworkspace) | BIT_FOR(Background) | BIT_FOR(Inactivecaption) |
+ BIT_FOR(Infobackground) | BIT_FOR(Menu) | BIT_FOR(Scrollbar) |
+ // should match CanvasText/WindowText:
+ BIT_FOR(Activecaption) | BIT_FOR(Captiontext) | BIT_FOR(Infotext) |
+ BIT_FOR(Menutext) |
+ // Some pages expect these to return windows-like colors, see bug 1773795.
+ // Also, per spec, these should match Canvas/CanvasText, see
+ // https://drafts.csswg.org/css-color-4/#valdef-color-window and
+ // https://drafts.csswg.org/css-color-4/#valdef-color-windowtext
+ BIT_FOR(Window) | BIT_FOR(Windowtext)};
+#undef BIT_FOR
+
+static bool ShouldUseStandinsForNativeColorForNonNativeTheme(
+ const dom::Document& aDoc, LookAndFeel::ColorID aColor,
+ const PreferenceSheet::Prefs& aPrefs) {
+ const bool shouldUseStandinsForColor = [&] {
+ if (sNonNativeThemeStandinColors[size_t(aColor)]) {
+ return true;
+ }
+ // There are platforms where we want the content-exposed accent color to be
+ // the windows blue rather than the system accent color, for now.
+ return !StaticPrefs::widget_non_native_theme_use_theme_accent() &&
+ (aColor == LookAndFeel::ColorID::Accentcolor ||
+ aColor == LookAndFeel::ColorID::Accentcolortext);
+ }();
+
+ return shouldUseStandinsForColor && aDoc.ShouldAvoidNativeTheme() &&
+ !aPrefs.NonNativeThemeShouldBeHighContrast();
+}
+
+bool LookAndFeel::IsDarkColor(nscolor aColor) {
+ // Given https://www.w3.org/TR/WCAG20/#contrast-ratiodef, this is the
+ // threshold that tells us whether contrast is better against white or black.
+ //
+ // Contrast ratio against black is: (L + 0.05) / 0.05
+ // Contrast ratio against white is: 1.05 / (L + 0.05)
+ //
+ // So the intersection is:
+ //
+ // (L + 0.05) / 0.05 = 1.05 / (L + 0.05)
+ //
+ // And the solution to that equation is:
+ //
+ // sqrt(1.05 * 0.05) - 0.05
+ //
+ // So we consider a color dark if the contrast is below this threshold, and
+ // it's at least half-opaque.
+ constexpr float kThreshold = 0.179129;
+ return NS_GET_A(aColor) > 127 &&
+ RelativeLuminanceUtils::Compute(aColor) < kThreshold;
+}
+
+ColorScheme LookAndFeel::ColorSchemeForStyle(
+ const dom::Document& aDoc, const StyleColorSchemeFlags& aFlags,
+ ColorSchemeMode aMode) {
+ const auto& prefs = PreferenceSheet::PrefsFor(aDoc);
+ StyleColorSchemeFlags style(aFlags);
+ if (!style) {
+ style._0 = aDoc.GetColorSchemeBits();
+ }
+ const bool supportsDark = bool(style & StyleColorSchemeFlags::DARK);
+ const bool supportsLight = bool(style & StyleColorSchemeFlags::LIGHT);
+ if (supportsLight && supportsDark) {
+ // Both color-schemes are explicitly supported, use the preferred one.
+ return aDoc.PreferredColorScheme();
+ }
+ if (supportsDark || supportsLight) {
+ // One color-scheme is explicitly supported and one isn't, so use the one
+ // the content supports.
+ return supportsDark ? ColorScheme::Dark : ColorScheme::Light;
+ }
+ // No value specified. Chrome docs, and forced-colors mode always supports
+ // both, so use the preferred color-scheme.
+ if (aMode == ColorSchemeMode::Preferred || aDoc.ChromeRulesEnabled() ||
+ !prefs.mUseDocumentColors) {
+ return aDoc.PreferredColorScheme();
+ }
+ // Otherwise default content to light.
+ return ColorScheme::Light;
+}
+
+LookAndFeel::ColorScheme LookAndFeel::ColorSchemeForFrame(
+ const nsIFrame* aFrame, ColorSchemeMode aMode) {
+ return ColorSchemeForStyle(*aFrame->PresContext()->Document(),
+ aFrame->StyleUI()->mColorScheme.bits, aMode);
+}
+
+// static
+Maybe<nscolor> LookAndFeel::GetColor(ColorID aId, ColorScheme aScheme,
+ UseStandins aUseStandins) {
+ nscolor result;
+ nsresult rv = nsLookAndFeel::GetInstance()->GetColorValue(
+ aId, aScheme, aUseStandins, result);
+ if (NS_FAILED(rv)) {
+ return Nothing();
+ }
+ return Some(result);
+}
+
+// Returns whether there is a CSS color name for this color.
+static bool ColorIsCSSAccessible(LookAndFeel::ColorID aId) {
+ using ColorID = LookAndFeel::ColorID;
+
+ switch (aId) {
+ case ColorID::TextSelectDisabledBackground:
+ case ColorID::TextSelectAttentionBackground:
+ case ColorID::TextSelectAttentionForeground:
+ case ColorID::TextHighlightBackground:
+ case ColorID::TextHighlightForeground:
+ case ColorID::ThemedScrollbar:
+ case ColorID::ThemedScrollbarInactive:
+ case ColorID::ThemedScrollbarThumb:
+ case ColorID::ThemedScrollbarThumbActive:
+ case ColorID::ThemedScrollbarThumbInactive:
+ case ColorID::ThemedScrollbarThumbHover:
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMEConvertedTextBackground:
+ case ColorID::IMEConvertedTextForeground:
+ case ColorID::IMEConvertedTextUnderline:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ case ColorID::SpellCheckerUnderline:
+ return false;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+LookAndFeel::UseStandins LookAndFeel::ShouldUseStandins(
+ const dom::Document& aDoc, ColorID aId) {
+ const auto& prefs = PreferenceSheet::PrefsFor(aDoc);
+ if (ShouldUseStandinsForNativeColorForNonNativeTheme(aDoc, aId, prefs)) {
+ return UseStandins::Yes;
+ }
+ if (prefs.mUseStandins && ColorIsCSSAccessible(aId)) {
+ return UseStandins::Yes;
+ }
+ return UseStandins::No;
+}
+
+Maybe<nscolor> LookAndFeel::GetColor(ColorID aId, const nsIFrame* aFrame) {
+ const auto* doc = aFrame->PresContext()->Document();
+ return GetColor(aId, ColorSchemeForFrame(aFrame),
+ ShouldUseStandins(*doc, aId));
+}
+
+// static
+nsresult LookAndFeel::GetInt(IntID aID, int32_t* aResult) {
+ return nsLookAndFeel::GetInstance()->GetIntValue(aID, *aResult);
+}
+
+// static
+nsresult LookAndFeel::GetFloat(FloatID aID, float* aResult) {
+ return nsLookAndFeel::GetInstance()->GetFloatValue(aID, *aResult);
+}
+
+// static
+bool LookAndFeel::GetFont(FontID aID, nsString& aName, gfxFontStyle& aStyle) {
+ return nsLookAndFeel::GetInstance()->GetFontValue(aID, aName, aStyle);
+}
+
+// static
+char16_t LookAndFeel::GetPasswordCharacter() {
+ return nsLookAndFeel::GetInstance()->GetPasswordCharacterImpl();
+}
+
+// static
+bool LookAndFeel::GetEchoPassword() {
+ if (StaticPrefs::editor_password_mask_delay() >= 0) {
+ return StaticPrefs::editor_password_mask_delay() > 0;
+ }
+ return nsLookAndFeel::GetInstance()->GetEchoPasswordImpl();
+}
+
+// static
+uint32_t LookAndFeel::GetPasswordMaskDelay() {
+ int32_t delay = StaticPrefs::editor_password_mask_delay();
+ if (delay < 0) {
+ return nsLookAndFeel::GetInstance()->GetPasswordMaskDelayImpl();
+ }
+ return delay;
+}
+
+bool LookAndFeel::DrawInTitlebar() {
+ switch (StaticPrefs::browser_tabs_inTitlebar()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ break;
+ }
+ return nsLookAndFeel::GetInstance()->GetDefaultDrawInTitlebar();
+}
+
+LookAndFeel::TitlebarAction LookAndFeel::GetTitlebarAction(
+ TitlebarEvent aEvent) {
+ return nsLookAndFeel::GetInstance()->GetTitlebarAction(aEvent);
+}
+
+void LookAndFeel::GetThemeInfo(nsACString& aOut) {
+ nsLookAndFeel::GetInstance()->GetThemeInfo(aOut);
+}
+
+uint32_t LookAndFeel::GetMenuAccessKey() {
+ return StaticPrefs::ui_key_menuAccessKey();
+}
+
+Modifiers LookAndFeel::GetMenuAccessKeyModifiers() {
+ switch (GetMenuAccessKey()) {
+ case dom::KeyboardEvent_Binding::DOM_VK_SHIFT:
+ return MODIFIER_SHIFT;
+ case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
+ return MODIFIER_CONTROL;
+ case dom::KeyboardEvent_Binding::DOM_VK_ALT:
+ return MODIFIER_ALT;
+ case dom::KeyboardEvent_Binding::DOM_VK_META:
+ case dom::KeyboardEvent_Binding::DOM_VK_WIN:
+ return MODIFIER_META;
+ default:
+ return 0;
+ }
+}
+
+// static
+void LookAndFeel::Refresh() {
+ nsLookAndFeel::GetInstance()->RefreshImpl();
+ widget::Theme::LookAndFeelChanged();
+}
+
+// static
+void LookAndFeel::NativeInit() { nsLookAndFeel::GetInstance()->NativeInit(); }
+
+// static
+void LookAndFeel::SetData(widget::FullLookAndFeel&& aTables) {
+ nsLookAndFeel::GetInstance()->SetDataImpl(std::move(aTables));
+}
+
+} // namespace mozilla
diff --git a/widget/nsXPLookAndFeel.h b/widget/nsXPLookAndFeel.h
new file mode 100644
index 0000000000..0f308f6935
--- /dev/null
+++ b/widget/nsXPLookAndFeel.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef __nsXPLookAndFeel
+#define __nsXPLookAndFeel
+
+#include "mozilla/Maybe.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/widget/LookAndFeelTypes.h"
+#include "nsTArray.h"
+
+class nsLookAndFeel;
+
+class nsXPLookAndFeel : public mozilla::LookAndFeel {
+ public:
+ using FullLookAndFeel = mozilla::widget::FullLookAndFeel;
+ using LookAndFeelFont = mozilla::widget::LookAndFeelFont;
+
+ virtual ~nsXPLookAndFeel();
+
+ static nsXPLookAndFeel* GetInstance();
+ static void Shutdown();
+
+ void Init();
+
+ // Gets the pref name for a given color, just for debugging purposes.
+ static const char* GetColorPrefName(ColorID);
+
+ // These functions will return a value specified by an override pref, if it
+ // exists, and otherwise will call into the NativeGetXxx function to get the
+ // platform-specific value.
+ //
+ // NS_ERROR_NOT_AVAILABLE is returned if there is neither an override pref or
+ // a platform-specific value.
+ nsresult GetColorValue(ColorID, ColorScheme, UseStandins, nscolor& aResult);
+ nsresult GetIntValue(IntID aID, int32_t& aResult);
+ nsresult GetFloatValue(FloatID aID, float& aResult);
+ // Same, but returns false if there is no platform-specific value.
+ // (There are no override prefs for font values.)
+ bool GetFontValue(FontID aID, nsString& aName, gfxFontStyle& aStyle);
+
+ virtual nsresult NativeGetInt(IntID aID, int32_t& aResult) = 0;
+ virtual nsresult NativeGetFloat(FloatID aID, float& aResult) = 0;
+ virtual nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) = 0;
+ virtual bool NativeGetFont(FontID aID, nsString& aName,
+ gfxFontStyle& aStyle) = 0;
+
+ virtual void RefreshImpl();
+
+ virtual char16_t GetPasswordCharacterImpl() { return char16_t('*'); }
+
+ virtual bool GetEchoPasswordImpl() { return false; }
+
+ virtual uint32_t GetPasswordMaskDelayImpl() { return 600; }
+
+ virtual bool GetDefaultDrawInTitlebar() { return true; }
+
+ virtual TitlebarAction GetTitlebarAction(TitlebarEvent aEvent) {
+ return TitlebarAction::None;
+ }
+
+ static bool LookAndFeelFontToStyle(const LookAndFeelFont&, nsString& aName,
+ gfxFontStyle&);
+ static LookAndFeelFont StyleToLookAndFeelFont(const nsAString& aName,
+ const gfxFontStyle&);
+
+ virtual void SetDataImpl(FullLookAndFeel&& aTables) {}
+
+ virtual void NativeInit() = 0;
+
+ virtual void GetThemeInfo(nsACString&) {}
+
+ protected:
+ nsXPLookAndFeel() = default;
+
+ static nscolor GetStandinForNativeColor(ColorID, ColorScheme);
+
+ // A platform-agnostic dark-color scheme, for platforms where we don't have
+ // "native" dark colors, like Windows and Android.
+ static mozilla::Maybe<nscolor> GenericDarkColor(ColorID);
+ mozilla::Maybe<nscolor> GetUncachedColor(ColorID, ColorScheme, UseStandins);
+
+ void RecordTelemetry();
+ virtual void RecordLookAndFeelSpecificTelemetry() {}
+
+ static void OnPrefChanged(const char* aPref, void* aClosure);
+
+ static bool sInitialized;
+
+ static nsXPLookAndFeel* sInstance;
+ static bool sShutdown;
+};
+
+#endif
diff --git a/widget/reftests/664925.xhtml b/widget/reftests/664925.xhtml
new file mode 100644
index 0000000000..b4a11a7386
--- /dev/null
+++ b/widget/reftests/664925.xhtml
@@ -0,0 +1 @@
+<html xmlns="http://www.w3.org/1999/xhtml"><div><td style="-moz-appearance: progressbar;"></td></div></html>
diff --git a/widget/reftests/meter-fallback-default-style-ref.html b/widget/reftests/meter-fallback-default-style-ref.html
new file mode 100644
index 0000000000..cf6c2e521e
--- /dev/null
+++ b/widget/reftests/meter-fallback-default-style-ref.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ div.meter-element {
+ /**
+ * The purpose of this test is to not show the native style.
+ * -moz-appearance: meterbar;
+ */
+ display: inline-block;
+ height: 1em;
+ width: 5em;
+ vertical-align: -0.2em;
+
+ background: linear-gradient(#e6e6e6, #e6e6e6, #eeeeee 20%, #cccccc 45%, #cccccc 55%);
+ }
+
+ div.meter-bar {
+ /**
+ * The purpose of this test is to not show the native style.
+ * -moz-appearance: meterchunk;
+ */
+
+ height: 100%;
+ width: 100%;
+
+ background: linear-gradient(#ad7, #ad7, #cea 20%, #7a3 45%, #7a3 55%);
+ }
+
+ div.meter-element { padding: 5px; }
+ body > div:nth-child(1) { -moz-appearance: none; }
+ body > div:nth-child(2) > .meter-bar { -moz-appearance: none; }
+ body > div:nth-child(3) { background: red; }
+ body > div:nth-child(4) > .meter-bar { background: red; }
+ body > div:nth-child(5) { border: 2px solid red; }
+ body > div:nth-child(6) > .meter-bar { border: 5px solid red; width: calc(100% - 10px); }
+ </style>
+ <body>
+ <div class="meter-element">
+ <div class="meter-bar"></div>
+ </div>
+ <div class="meter-element">
+ <div class="meter-bar"></div>
+ </div>
+ <div class="meter-element">
+ <div class="meter-bar"></div>
+ </div>
+ <div class="meter-element">
+ <div class="meter-bar"></div>
+ </div>
+ <div class="meter-element">
+ <div class="meter-bar"></div>
+ </div>
+ <div class="meter-element">
+ <div class="meter-bar"></div>
+ </div>
+ </body>
+</html>
diff --git a/widget/reftests/meter-fallback-default-style.html b/widget/reftests/meter-fallback-default-style.html
new file mode 100644
index 0000000000..c59fc265e4
--- /dev/null
+++ b/widget/reftests/meter-fallback-default-style.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ meter { padding: 5px }
+ body > meter:nth-child(1) { -moz-appearance: none; }
+ body > meter:nth-child(2)::-moz-meter-bar { -moz-appearance: none; }
+ body > meter:nth-child(3) { background: red; }
+ body > meter:nth-child(4)::-moz-meter-bar { background: red; }
+ body > meter:nth-child(5) { border: 2px solid red; }
+ body > meter:nth-child(6) { overflow: visible; }
+ body > meter:nth-child(6)::-moz-meter-bar { border: 5px solid red; }
+ </style>
+ <body>
+ <meter value=1></meter>
+ <meter value=1></meter>
+ <meter value=1></meter>
+ <meter value=1></meter>
+ <meter value=1></meter>
+ <meter value=1></meter>
+ </body>
+</html>
diff --git a/widget/reftests/meter-native-style-ref.html b/widget/reftests/meter-native-style-ref.html
new file mode 100644
index 0000000000..f54ec0bbac
--- /dev/null
+++ b/widget/reftests/meter-native-style-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <!-- Empty meter, no bar. -->
+ <meter></meter>
+ <!-- Full meter green colored. -->
+ <meter min=0 low=0 high=1 optimum=2 max=10 value=10></meter>
+ <!-- Full meter orange colored. -->
+ <meter min=0 low=0 high=1 optimum=1 max=10 value=10></meter>
+ <!-- Full meter red colored. -->
+ <meter min=0 low=1 high=2 optimum=0 max=10 value=10></meter>
+ <!-- Half-empty orange colored. -->
+ <meter min=0 low=0 high=1 optimum=0 max=10 value=5></meter>
+ <!-- Half-empty orange colored. -->
+ <meter min=0 low=0 high=1 optimum=0 max=10 value=5></meter>
+ <!-- With RTL, the bar should begin on the right. -->
+ <meter style="transform: scale(-1, 1);" value=0.5></meter>
+ </body>
+</html>
diff --git a/widget/reftests/meter-native-style.html b/widget/reftests/meter-native-style.html
new file mode 100644
index 0000000000..91342772fe
--- /dev/null
+++ b/widget/reftests/meter-native-style.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <meter vaue=0></meter>
+ <!-- Should be green. -->
+ <meter min=0 low=0 high=10 optimum=10 max=10 value=10></meter>
+ <!-- Should be orange. -->
+ <meter min=0 low=9 high=10 optimum=8 max=10 value=10></meter>
+ <!-- Should be red. -->
+ <meter min=0 low=8 high=9 optimum=0 max=10 value=10></meter>
+ <!-- Half-full orange. -->
+ <meter min=0 low=3 high=4 optimum=4 max=10 value=5></meter>
+ <!-- Half-full orange. -->
+ <meter min=0 low=9 high=10 optimum=10 max=10 value=5></meter>
+ <!-- Test RTL -->
+ <meter dir='rtl' value=0.5></meter>
+ </body>
+</html>
diff --git a/widget/reftests/meter-vertical-native-style-ref.html b/widget/reftests/meter-vertical-native-style-ref.html
new file mode 100644
index 0000000000..e771d3fd76
--- /dev/null
+++ b/widget/reftests/meter-vertical-native-style-ref.html
@@ -0,0 +1,14 @@
+<html>
+ <style>
+ meter:nth-child(1) { transform: rotate(-90deg) translate(-2em, -2em); }
+ meter:nth-child(2) { transform: rotate(-90deg) translate(-2em, -6em); }
+ meter:nth-child(3) { transform: rotate(-90deg) translate(-2em, -10em); }
+ meter:nth-child(4) { transform: rotate(-90deg) translate(-2em, -14em); }
+ meter:nth-child(5) { transform: rotate(-90deg) translate(-2em, -18em); }
+ meter:nth-child(6) { transform: rotate(-90deg) translate(-2em, -22em); }
+ meter:nth-child(7) { transform: rotate(-90deg) translate(-2em, -26em); }
+ </style>
+<body>
+<meter></meter><meter min=0 low=0 high=1 optimum=2 max=10 value=10></meter><meter min=0 low=0 high=1 optimum=1 max=10 value=10></meter><meter min=0 low=1 high=2 optimum=0 max=10 value=10></meter><meter min=0 low=0 high=1 optimum=0 max=10 value=5></meter><meter min=0 low=0 high=1 optimum=0 max=10 value=5></meter><meter value=0.5></meter>
+</body>
+</html>
diff --git a/widget/reftests/meter-vertical-native-style.html b/widget/reftests/meter-vertical-native-style.html
new file mode 100644
index 0000000000..ca0ca1b933
--- /dev/null
+++ b/widget/reftests/meter-vertical-native-style.html
@@ -0,0 +1,13 @@
+<html>
+ <style>
+ meter { -moz-orient: vertical; }
+ </style>
+ <body>
+<!-- For some reasons, the ref has a small offset when there are spaces between meters.
+ Given that we don't want to test transform and even a perfect match but just
+ the general rendering, we are going to keep this dirty test.
+ It's very similar to the non-vertical test with a difference, for the RTL: RTL
+ does not apply for vertical meters. -->
+<meter vaue=0></meter><meter min=0 low=0 high=10 optimum=10 max=10 value=10></meter><meter min=0 low=9 high=10 optimum=8 max=10 value=10></meter><meter min=0 low=8 high=9 optimum=0 max=10 value=10></meter><meter min=0 low=3 high=4 optimum=4 max=10 value=5></meter><meter min=0 low=9 high=10 optimum=10 max=10 value=5></meter><meter value=0.5 dir=rtl></meter>
+ </body>
+</html>
diff --git a/widget/reftests/progressbar-fallback-default-style-ref.html b/widget/reftests/progressbar-fallback-default-style-ref.html
new file mode 100644
index 0000000000..22950ec583
--- /dev/null
+++ b/widget/reftests/progressbar-fallback-default-style-ref.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <link rel='stylesheet' href='resource://reftest/progress.css'>
+ <style>
+ div.progress-element { padding: 5px; }
+ body > div:nth-child(1) { -moz-appearance: none; }
+ body > div:nth-child(2) > .progress-bar { -moz-appearance: none; }
+ body > div:nth-child(3) { background-color: red; }
+ body > div:nth-child(4) > .progress-bar { background-color: red; }
+ body > div:nth-child(5) { border: 2px solid red; }
+ body > div:nth-child(6) > .progress-bar { border: 5px solid red; }
+ </style>
+ <body>
+ <div class="progress-element">
+ <div class="progress-bar"></div>
+ </div>
+ <div class="progress-element">
+ <div class="progress-bar"></div>
+ </div>
+ <div class="progress-element">
+ <div class="progress-bar"></div>
+ </div>
+ <div class="progress-element">
+ <div class="progress-bar"></div>
+ </div>
+ <div class="progress-element">
+ <div class="progress-bar"></div>
+ </div>
+ <div class="progress-element">
+ <div class="progress-bar"></div>
+ </div>
+ </body>
+</html>
diff --git a/widget/reftests/progressbar-fallback-default-style.html b/widget/reftests/progressbar-fallback-default-style.html
new file mode 100644
index 0000000000..7594e65556
--- /dev/null
+++ b/widget/reftests/progressbar-fallback-default-style.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ progress { padding: 5px }
+ body > progress:nth-child(1) { -moz-appearance: none; }
+ body > progress:nth-child(2)::-moz-progress-bar { -moz-appearance: none; }
+ body > progress:nth-child(3) { background-color: red; }
+ body > progress:nth-child(4)::-moz-progress-bar { background-color: red; }
+ body > progress:nth-child(5) { border: 2px solid red; }
+ body > progress:nth-child(6)::-moz-progress-bar { border: 5px solid red; }
+ </style>
+ <body>
+ <progress></progress>
+ <progress></progress>
+ <progress></progress>
+ <progress></progress>
+ <progress></progress>
+ <progress></progress>
+ </body>
+</html>
diff --git a/widget/reftests/reftest.list b/widget/reftests/reftest.list
new file mode 100644
index 0000000000..dc788a31aa
--- /dev/null
+++ b/widget/reftests/reftest.list
@@ -0,0 +1,9 @@
+== progressbar-fallback-default-style.html progressbar-fallback-default-style-ref.html
+fuzzy(0-31,0-67) == meter-native-style.html meter-native-style-ref.html
+fuzzy(0-11,0-342) == meter-vertical-native-style.html meter-vertical-native-style-ref.html # dithering
+== meter-fallback-default-style.html meter-fallback-default-style-ref.html
+load 664925.xhtml
+pref(apz.allow_zooming,true) pref(ui.useOverlayScrollbars,0) skip-if(!cocoaWidget) != scaled-scrollbar.html about:blank
+
+# Test that scrollbar buttons are inhibited on Linux using the non-native theme.
+skip-if(!gtkWidget) pref(widget.non-native-theme.enabled,true) test-pref(ui.scrollArrowStyle,4097) ref-pref(ui.scrollArrowStyle,0) == scrollbar-buttons.html scrollbar-buttons.html
diff --git a/widget/reftests/scaled-scrollbar.html b/widget/reftests/scaled-scrollbar.html
new file mode 100644
index 0000000000..f94df78420
--- /dev/null
+++ b/widget/reftests/scaled-scrollbar.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html reftest-resolution="2.0" style="scrollbar-width: none;">
+<div style="overflow: scroll; width: 300px; height: 300px">
+ <div style="width: 1000px; height: 1000px"></div>
+</div>
+</html>
diff --git a/widget/reftests/scrollbar-buttons.html b/widget/reftests/scrollbar-buttons.html
new file mode 100644
index 0000000000..d54218272d
--- /dev/null
+++ b/widget/reftests/scrollbar-buttons.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<div style="width: 200px; height: 200px; overflow: scroll;">
+ <div style="width: 400px; height: 400px;"></div>
+</div>
diff --git a/widget/tests/TestChromeMargin.cpp b/widget/tests/TestChromeMargin.cpp
new file mode 100644
index 0000000000..0eed86b208
--- /dev/null
+++ b/widget/tests/TestChromeMargin.cpp
@@ -0,0 +1,130 @@
+/* -*- 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/. */
+
+/* This tests the margin parsing functionality in nsAttrValue.cpp, which
+ * is accessible via nsContentUtils, and is used in setting chromemargins
+ * to widget windows. It's located here due to linking issues in the
+ * content directory.
+ */
+
+/* This test no longer compiles now that we've removed nsIContentUtils (bug
+ * 647273). We need to be internal code in order to include nsContentUtils.h,
+ * but defining MOZILLA_INTERNAL_API is not enough to make us internal.
+ */
+
+#include "TestHarness.h"
+
+#ifndef MOZILLA_INTERNAL_API
+# error This test needs MOZILLA_INTERNAL_API (see bug 652123)
+#endif
+
+#include "nscore.h"
+#include "nsContentUtils.h"
+#include "nsString.h"
+
+struct DATA {
+ bool shouldfail;
+ const char* margins;
+ int top;
+ int right;
+ int bottom;
+ int left;
+};
+
+const bool SHOULD_FAIL = true;
+const int SHOULD_PASS = false;
+
+const DATA Data[] = {
+ {SHOULD_FAIL, "", 1, 2, 3, 4},
+ {SHOULD_FAIL, "1,0,0,0", 1, 2, 3, 4},
+ {SHOULD_FAIL, "1,2,0,0", 1, 2, 3, 4},
+ {SHOULD_FAIL, "1,2,3,0", 1, 2, 3, 4},
+ {SHOULD_FAIL, "4,3,2,1", 1, 2, 3, 4},
+ {SHOULD_FAIL, "azsasdasd", 0, 0, 0, 0},
+ {SHOULD_FAIL, ",azsasdasd", 0, 0, 0, 0},
+ {SHOULD_FAIL, " ", 1, 2, 3, 4},
+ {SHOULD_FAIL,
+ "azsdfsdfsdfsdfsdfsasdasd,asdasdasdasdasdasd,asdadasdasd,asdasdasdasd", 0,
+ 0, 0, 0},
+ {SHOULD_FAIL, "as,as,as,as", 0, 0, 0, 0},
+ {SHOULD_FAIL, "0,0,0", 0, 0, 0, 0},
+ {SHOULD_FAIL, "0,0", 0, 0, 0, 0},
+ {SHOULD_FAIL, "4.6,1,1,1", 0, 0, 0, 0},
+ {SHOULD_FAIL, ",,,,", 0, 0, 0, 0},
+ {SHOULD_FAIL, "1, , , ,", 0, 0, 0, 0},
+ {SHOULD_FAIL, "1, , ,", 0, 0, 0, 0},
+ {SHOULD_FAIL, "@!@%^&^*()", 1, 2, 3, 4},
+ {SHOULD_PASS, "4,3,2,1", 4, 3, 2, 1},
+ {SHOULD_PASS, "-4,-3,-2,-1", -4, -3, -2, -1},
+ {SHOULD_PASS, "10000,3,2,1", 10000, 3, 2, 1},
+ {SHOULD_PASS, "4 , 3 , 2 , 1", 4, 3, 2, 1},
+ {SHOULD_PASS, "4, 3 ,2,1", 4, 3, 2, 1},
+ {SHOULD_FAIL, "4,3,2,10000000000000 --", 4, 3, 2, 10000000000000},
+ {SHOULD_PASS, "4,3,2,1000", 4, 3, 2, 1000},
+ {SHOULD_PASS, "2147483647,3,2,1000", 2147483647, 3, 2, 1000},
+ {SHOULD_PASS, "2147483647,2147483647,2147483647,2147483647", 2147483647,
+ 2147483647, 2147483647, 2147483647},
+ {SHOULD_PASS, "-2147483647,3,2,1000", -2147483647, 3, 2, 1000},
+ {SHOULD_FAIL, "2147483648,3,2,1000", 1, 3, 2, 1000},
+ {0, nullptr, 0, 0, 0, 0}};
+
+void DoAttrValueTest() {
+ int idx = -1;
+ bool didFail = false;
+ while (Data[++idx].margins) {
+ nsAutoString str;
+ str.AssignLiteral(Data[idx].margins);
+ nsIntMargin values(99, 99, 99, 99);
+ bool result = nsContentUtils::ParseIntMarginValue(str, values);
+
+ // if the parse fails
+ if (!result) {
+ if (Data[idx].shouldfail) continue;
+ fail(Data[idx].margins);
+ didFail = true;
+ printf("*1\n");
+ continue;
+ }
+
+ if (Data[idx].shouldfail) {
+ if (Data[idx].top == values.top && Data[idx].right == values.right &&
+ Data[idx].bottom == values.bottom && Data[idx].left == values.left) {
+ // not likely
+ fail(Data[idx].margins);
+ didFail = true;
+ printf("*2\n");
+ continue;
+ }
+ // good failure, parse failed and that's what we expected.
+ continue;
+ }
+#if 0
+ printf("%d==%d %d==%d %d==%d %d==%d\n",
+ Data[idx].top, values.top,
+ Data[idx].right, values.right,
+ Data[idx].bottom, values.bottom,
+ Data[idx].left, values.left);
+#endif
+ if (Data[idx].top == values.top && Data[idx].right == values.right &&
+ Data[idx].bottom == values.bottom && Data[idx].left == values.left) {
+ // good parse results
+ continue;
+ } else {
+ fail(Data[idx].margins);
+ didFail = true;
+ printf("*3\n");
+ continue;
+ }
+ }
+
+ if (!didFail) passed("nsAttrValue margin parsing tests passed.");
+}
+
+int main(int argc, char** argv) {
+ ScopedXPCOM xpcom("");
+ if (xpcom.failed()) return 1;
+ DoAttrValueTest();
+ return 0;
+}
diff --git a/widget/tests/browser/browser.toml b/widget/tests/browser/browser.toml
new file mode 100644
index 0000000000..506fe1a998
--- /dev/null
+++ b/widget/tests/browser/browser.toml
@@ -0,0 +1,90 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+
+["browser_test_AZERTY_digit_shortcut.js"]
+skip-if = ["os == 'linux'"] # Linux build has not implemented sendNativeKeyEvent yet
+
+["browser_test_ContentCache.js"]
+
+["browser_test_InputContextURI.js"]
+
+["browser_test_clipboard_contextmenu.js"]
+
+["browser_test_clipboardcache.js"]
+skip-if = [
+ "os == 'win' && bits == 32 && !debug", # Bug 1759422
+ "os == 'linux'", # Bug 1792749
+]
+
+["browser_test_fullscreen_size.js"]
+
+["browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js"]
+support-files = [
+ "file_ime_state_tests.html",
+ "../file_ime_state_test_helper.js",
+ "../file_test_ime_state_on_focus_move.js",
+]
+
+["browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js"]
+support-files = [
+ "file_ime_state_tests.html",
+ "../file_ime_state_test_helper.js",
+ "../file_test_ime_state_in_contenteditable_on_readonly_change.js",
+]
+
+["browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js"]
+support-files = [
+ "file_ime_state_tests.html",
+ "../file_ime_state_test_helper.js",
+ "../file_test_ime_state_on_focus_move.js",
+]
+
+["browser_test_ime_state_in_plugin_in_remote_content.js"]
+support-files = ["../file_ime_state_test_helper.js"]
+
+["browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js"]
+support-files = [
+ "../file_ime_state_test_helper.js",
+ "../file_test_ime_state_in_text_control_on_reframe.js",
+]
+
+["browser_test_ime_state_on_editable_state_change_in_remote_content.js"]
+support-files = ["../file_ime_state_test_helper.js"]
+
+["browser_test_ime_state_on_focus_move_in_remote_content.js"]
+support-files = [
+ "file_ime_state_tests.html",
+ "../file_ime_state_test_helper.js",
+ "../file_test_ime_state_on_focus_move.js",
+]
+
+["browser_test_ime_state_on_input_type_change_in_remote_content.js"]
+skip-if = ["true"] # Bug 1817704
+support-files = [
+ "file_ime_state_tests.html",
+ "../file_ime_state_test_helper.js",
+ "../file_test_ime_state_on_input_type_change.js",
+]
+
+["browser_test_ime_state_on_readonly_change_in_remote_content.js"]
+support-files = [
+ "file_ime_state_tests.html",
+ "../file_ime_state_test_helper.js",
+ "../file_test_ime_state_on_readonly_change.js",
+]
+
+["browser_test_scrollbar_colors.js"]
+skip-if = ["os == 'linux'"] # bug 1460109
+support-files = ["helper_scrollbar_colors.html"]
+
+["browser_test_swipe_gesture.js"]
+skip-if = [
+ "win11_2009 && bits == 32 && !debug", # Bug 1759422
+ "verify", # Bug 1800022
+ "os == 'linux'", # Bug 1784772
+]
+support-files = [
+ "helper_swipe_gesture.html",
+ "!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ "!/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+]
diff --git a/widget/tests/browser/browser_test_AZERTY_digit_shortcut.js b/widget/tests/browser/browser_test_AZERTY_digit_shortcut.js
new file mode 100644
index 0000000000..ef5112ac01
--- /dev/null
+++ b/widget/tests/browser/browser_test_AZERTY_digit_shortcut.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ let tabs = [];
+ for (let i = 0; i < 10; i++) {
+ const tab = BrowserTestUtils.addTab(gBrowser);
+ tabs.push(tab);
+ }
+ const kIsMac = AppConstants.platform == "macosx";
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ let NativeKeyConstants = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js",
+ NativeKeyConstants
+ );
+
+ function promiseSynthesizeAccelHyphenMinusWithAZERTY() {
+ return new Promise(resolve =>
+ EventUtils.synthesizeNativeKey(
+ EventUtils.KEYBOARD_LAYOUT_FRENCH_PC,
+ kIsMac
+ ? NativeKeyConstants.MAC_VK_ANSI_6
+ : NativeKeyConstants.WIN_VK_6,
+ { accelKey: true },
+ kIsMac ? "-" : "",
+ kIsMac ? "-" : "",
+ resolve
+ )
+ );
+ }
+
+ async function waitForCondition(aFunc) {
+ for (let i = 0; i < 60; i++) {
+ await new Promise(resolve =>
+ requestAnimationFrame(() => requestAnimationFrame(resolve))
+ );
+ if (aFunc(ZoomManager.getFullZoomForBrowser(browser))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ const minZoomLevel = ZoomManager.MIN;
+ while (true) {
+ const currentZoom = ZoomManager.getFullZoomForBrowser(browser);
+ if (minZoomLevel == currentZoom) {
+ break;
+ }
+ info(`Trying to zoom out: ${currentZoom}`);
+ await promiseSynthesizeAccelHyphenMinusWithAZERTY();
+ if (!(await waitForCondition(aZoomLevel => aZoomLevel < currentZoom))) {
+ ok(false, `Failed to zoom out from ${currentZoom}`);
+ return;
+ }
+ }
+
+ await promiseSynthesizeAccelHyphenMinusWithAZERTY();
+ await waitForCondition(() => false);
+ is(
+ gBrowser.selectedBrowser,
+ browser,
+ "Tab shouldn't be changed by Ctrl+- of AZERTY keyboard layout"
+ );
+ // Reset the zoom before going to the next test.
+ EventUtils.synthesizeKey("0", { accelKey: true });
+ await waitForCondition(aZoomLevel => aZoomLevel == 1);
+ }
+ );
+
+ while (tabs.length) {
+ await new Promise(resolve => {
+ const tab = tabs.shift();
+ BrowserTestUtils.waitForTabClosing(tab).then(resolve);
+ BrowserTestUtils.removeTab(tab);
+ });
+ }
+});
diff --git a/widget/tests/browser/browser_test_ContentCache.js b/widget/tests/browser/browser_test_ContentCache.js
new file mode 100644
index 0000000000..7b64e00f13
--- /dev/null
+++ b/widget/tests/browser/browser_test_ContentCache.js
@@ -0,0 +1,296 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ const TIP = Cc["@mozilla.org/text-input-processor;1"].createInstance(
+ Ci.nsITextInputProcessor
+ );
+ let notifications = [];
+ const observer = (aTIP, aNotification) => {
+ switch (aNotification.type) {
+ case "request-to-commit":
+ aTIP.commitComposition();
+ break;
+ case "request-to-cancel":
+ aTIP.cancelComposition();
+ break;
+ case "notify-end-input-transaction":
+ case "notify-focus":
+ case "notify-blur":
+ case "notify-text-change":
+ case "notify-selection-change":
+ notifications.push(aNotification);
+ break;
+ }
+ return true;
+ };
+
+ function checkNotifications(aExpectedNotifications, aDescription) {
+ for (const expectedNotification of aExpectedNotifications) {
+ const notification = notifications.find(
+ element => element.type == expectedNotification.type
+ );
+ if (expectedNotification.expected) {
+ isnot(
+ notification,
+ undefined,
+ `"${expectedNotification.type}" should be notified ${aDescription}`
+ );
+ } else {
+ is(
+ notification,
+ undefined,
+ `"${expectedNotification.type}" should not be notified ${aDescription}`
+ );
+ }
+ }
+ }
+
+ ok(
+ TIP.beginInputTransaction(window, observer),
+ "nsITextInputProcessor.beingInputTransaction should return true"
+ );
+ ok(
+ TIP.beginInputTransactionForTests(window, observer),
+ "nsITextInputProcessor.beginInputTransactionForTests should return true"
+ );
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ ok(browser.isRemoteBrowser, "This test passes only in e10s mode");
+
+ // IMEContentObserver flushes pending IME notifications at next vsync
+ // after something happens. Therefore, after doing something in content
+ // process, we need to guarantee that IMEContentObserver has a change to
+ // send IME notifications to the main process with calling this function.
+ function waitForSendingIMENotificationsInContent() {
+ return SpecialPowers.spawn(browser, [], async () => {
+ await new Promise(resolve =>
+ content.requestAnimationFrame(() =>
+ content.requestAnimationFrame(resolve)
+ )
+ );
+ });
+ }
+
+ /**
+ * Test when empty editor gets focus
+ */
+ notifications = [];
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML = "<div contenteditable><br></div>";
+ const editor = content.document.querySelector("div[contenteditable]");
+ editor.focus();
+ });
+
+ await waitForSendingIMENotificationsInContent();
+
+ (function () {
+ checkNotifications(
+ [
+ { type: "notify-focus", expected: true },
+ { type: "notify-blur", expected: false },
+ { type: "notify-end-input-transaction", expected: false },
+ { type: "notify-text-change", expected: false },
+ { type: "notify-selection-change", expected: false },
+ ],
+ "after empty editor gets focus"
+ );
+ const text = EventUtils.synthesizeQueryTextContent(0, 1000);
+ ok(
+ text?.succeeded,
+ "query text content should succeed after empty editor gets focus"
+ );
+ if (text?.succeeded) {
+ is(
+ text.text.replace(/[\r\n]/g, ""),
+ "",
+ "text should be only line breaks after empty editor gets focus"
+ );
+ }
+ const selection = EventUtils.synthesizeQuerySelectedText();
+ ok(
+ selection?.succeeded,
+ "query selected text should succeed after empty editor gets focus"
+ );
+ if (selection?.succeeded) {
+ ok(
+ !selection.notFound,
+ "query selected text should find a selection range after empty editor gets focus"
+ );
+ if (!selection.notFound) {
+ is(
+ selection.text,
+ "",
+ "selection should be collapsed after empty editor gets focus"
+ );
+ }
+ }
+ })();
+
+ /**
+ * Test when there is non-collapsed selection
+ */
+ notifications = [];
+ await SpecialPowers.spawn(browser, [], () => {
+ const editor = content.document.querySelector("div[contenteditable]");
+ editor.innerHTML = "<p>abc</p><p>def</p>";
+ content
+ .getSelection()
+ .setBaseAndExtent(
+ editor.querySelector("p").firstChild,
+ 2,
+ editor.querySelector("p + p").firstChild,
+ 1
+ );
+ });
+
+ await waitForSendingIMENotificationsInContent();
+
+ (function () {
+ checkNotifications(
+ [
+ { type: "notify-focus", expected: false },
+ { type: "notify-blur", expected: false },
+ { type: "notify-end-input-transaction", expected: false },
+ { type: "notify-text-change", expected: true },
+ { type: "notify-selection-change", expected: true },
+ ],
+ "after modifying focused editor"
+ );
+ const text = EventUtils.synthesizeQueryTextContent(0, 1000);
+ ok(
+ text?.succeeded,
+ "query text content should succeed after modifying focused editor"
+ );
+ if (text?.succeeded) {
+ is(
+ text.text.trim().replace(/\r\n/g, "\n").replace(/\n\n+/g, "\n"),
+ "abc\ndef",
+ "text should include the both paragraph's text after modifying focused editor"
+ );
+ }
+ const selection = EventUtils.synthesizeQuerySelectedText();
+ ok(
+ selection?.succeeded,
+ "query selected text should succeed after modifying focused editor"
+ );
+ if (selection?.succeeded) {
+ ok(
+ !selection.notFound,
+ "query selected text should find a selection range after modifying focused editor"
+ );
+ if (!selection.notFound) {
+ is(
+ selection.text
+ .trim()
+ .replace(/\r\n/g, "\n")
+ .replace(/\n\n+/g, "\n"),
+ "c\nd",
+ "selection should have the selected characters in the both paragraphs after modifying focused editor"
+ );
+ }
+ }
+ })();
+
+ /**
+ * Test when there is no selection ranges
+ */
+ notifications = [];
+ await SpecialPowers.spawn(browser, [], () => {
+ content.getSelection().removeAllRanges();
+ });
+
+ await waitForSendingIMENotificationsInContent();
+
+ (function () {
+ checkNotifications(
+ [
+ { type: "notify-focus", expected: false },
+ { type: "notify-blur", expected: false },
+ { type: "notify-end-input-transaction", expected: false },
+ { type: "notify-text-change", expected: false },
+ { type: "notify-selection-change", expected: true },
+ ],
+ "after removing all selection ranges from the focused editor"
+ );
+ const text = EventUtils.synthesizeQueryTextContent(0, 1000);
+ ok(
+ text?.succeeded,
+ "query text content should succeed after removing all selection ranges from the focused editor"
+ );
+ if (text?.succeeded) {
+ is(
+ text.text.trim().replace(/\r\n/g, "\n").replace(/\n\n+/g, "\n"),
+ "abc\ndef",
+ "text should include the both paragraph's text after removing all selection ranges from the focused editor"
+ );
+ }
+ const selection = EventUtils.synthesizeQuerySelectedText();
+ ok(
+ selection?.succeeded,
+ "query selected text should succeed after removing all selection ranges from the focused editor"
+ );
+ if (selection?.succeeded) {
+ ok(
+ selection.notFound,
+ "query selected text should find no selection range after removing all selection ranges from the focused editor"
+ );
+ }
+ })();
+
+ /**
+ * Test when no editable element has focus.
+ */
+ notifications = [];
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML = "abcdef";
+ });
+
+ await waitForSendingIMENotificationsInContent();
+
+ (function () {
+ checkNotifications(
+ [
+ { type: "notify-focus", expected: false },
+ { type: "notify-blur", expected: true },
+ ],
+ "removing editor should make ContentCacheInParent not have any data"
+ );
+ const text = EventUtils.synthesizeQueryTextContent(0, 1000);
+ ok(
+ !text?.succeeded,
+ "query text content should fail because no editable element has focus"
+ );
+ const selection = EventUtils.synthesizeQuerySelectedText();
+ ok(
+ !selection?.succeeded,
+ "query selected text should fail because no editable element has focus"
+ );
+ const caret = EventUtils.synthesizeQueryCaretRect(0);
+ ok(
+ !caret?.succeeded,
+ "query caret rect should fail because no editable element has focus"
+ );
+ const textRect = EventUtils.synthesizeQueryTextRect(0, 5, false);
+ ok(
+ !textRect?.succeeded,
+ "query text rect should fail because no editable element has focus"
+ );
+ const textRectArray = EventUtils.synthesizeQueryTextRectArray(0, 5);
+ ok(
+ !textRectArray?.succeeded,
+ "query text rect array should fail because no editable element has focus"
+ );
+ const editorRect = EventUtils.synthesizeQueryEditorRect();
+ todo(
+ !editorRect?.succeeded,
+ "query editor rect should fail because no editable element has focus"
+ );
+ })();
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_InputContextURI.js b/widget/tests/browser/browser_test_InputContextURI.js
new file mode 100644
index 0000000000..52f05d90f9
--- /dev/null
+++ b/widget/tests/browser/browser_test_InputContextURI.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const gDOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+
+function promiseURLBarFocus() {
+ const waitForFocusInURLBar = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+ gURLBar.blur();
+ gURLBar.focus();
+ return Promise.all([
+ waitForFocusInURLBar,
+ TestUtils.waitForCondition(
+ () =>
+ gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED &&
+ gDOMWindowUtils.inputContextOrigin ===
+ Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_MAIN
+ ),
+ ]);
+}
+
+function promiseIMEStateEnabledByRemote() {
+ return TestUtils.waitForCondition(
+ () =>
+ gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED &&
+ gDOMWindowUtils.inputContextOrigin ===
+ Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_CONTENT
+ );
+}
+
+async function test_url_bar_url(aDesc) {
+ await promiseURLBarFocus();
+
+ is(
+ gDOMWindowUtils.inputContextURI,
+ null,
+ `When the search bar has focus, input context URI should be null because of in chrome document (${aDesc})`
+ );
+}
+
+async function test_input_in_http_or_https(aIsHTTPS) {
+ await promiseURLBarFocus();
+
+ const scheme = aIsHTTPS ? "https" : "http";
+ const url = `${scheme}://example.com/browser/toolkit/content/tests/browser/file_empty.html`;
+ await BrowserTestUtils.withNewTab(url, async browser => {
+ ok(browser.isRemoteBrowser, "This test passes only in e10s mode");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.body.innerHTML = "<input>";
+ const input = content.document.querySelector("input");
+ input.focus();
+
+ // Wait for a tick for flushing IMEContentObserver's pending notifications.
+ await new Promise(resolve =>
+ content.requestAnimationFrame(() =>
+ content.requestAnimationFrame(resolve)
+ )
+ );
+ });
+
+ await promiseIMEStateEnabledByRemote();
+ if (!gDOMWindowUtils.inputContextURI) {
+ ok(
+ false,
+ `Input context should have valid URI when the scheme of focused tab's URL is ${scheme}`
+ );
+ return;
+ }
+ is(
+ gDOMWindowUtils.inputContextURI.spec,
+ url,
+ `Input context should have the document URI when the scheme of focused tab's URL is ${scheme}`
+ );
+ });
+}
+
+add_task(async () => {
+ await test_url_bar_url("first check");
+});
+add_task(async () => {
+ await test_input_in_http_or_https(true);
+});
+add_task(async () => {
+ await test_url_bar_url("check after remote content sets the URI");
+});
+add_task(async () => {
+ await test_input_in_http_or_https(false);
+});
+
+add_task(async function test_input_in_data() {
+ await BrowserTestUtils.withNewTab("data:text/html,<input>", async browser => {
+ ok(browser.isRemoteBrowser, "This test passes only in e10s mode");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ const input = content.document.querySelector("input");
+ input.focus();
+
+ // Wait for a tick for flushing IMEContentObserver's pending notifications.
+ await new Promise(resolve =>
+ content.requestAnimationFrame(() =>
+ content.requestAnimationFrame(resolve)
+ )
+ );
+ });
+
+ await promiseIMEStateEnabledByRemote();
+ is(
+ gDOMWindowUtils.inputContextURI,
+ null,
+ "Input context should not have data URI"
+ );
+ });
+});
+
+add_task(async function test_omit_private_things_in_URL() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.auth.confirmAuth.enabled", false]],
+ });
+ await promiseURLBarFocus();
+
+ await BrowserTestUtils.withNewTab(
+ "https://username:password@example.com/browser/toolkit/content/tests/browser/file_empty.html?query=some#ref",
+ async browser => {
+ ok(browser.isRemoteBrowser, "This test passes only in e10s mode");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.body.innerHTML = "<input>";
+ const input = content.document.querySelector("input");
+ input.focus();
+
+ // Wait for a tick for flushing IMEContentObserver's pending notifications.
+ await new Promise(resolve =>
+ content.requestAnimationFrame(() =>
+ content.requestAnimationFrame(resolve)
+ )
+ );
+ });
+
+ await promiseIMEStateEnabledByRemote();
+ if (!gDOMWindowUtils.inputContextURI) {
+ ok(
+ false,
+ `Input context should have valid URI even when the URL contains some private things`
+ );
+ return;
+ }
+ is(
+ gDOMWindowUtils.inputContextURI.spec,
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ `Input context should have the document URI which omit some private things in the URL`
+ );
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_clipboard_contextmenu.js b/widget/tests/browser/browser_test_clipboard_contextmenu.js
new file mode 100644
index 0000000000..4d268d5be4
--- /dev/null
+++ b/widget/tests/browser/browser_test_clipboard_contextmenu.js
@@ -0,0 +1,127 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const clipboard = SpecialPowers.Services.clipboard;
+const clipboardTypes = [
+ clipboard.kGlobalClipboard,
+ clipboard.kSelectionClipboard,
+ clipboard.kFindClipboard,
+ clipboard.kSelectionCache,
+];
+const supportedClipboardTypes = clipboardTypes.filter(type =>
+ clipboard.isClipboardTypeSupported(type)
+);
+
+const kPasteMenuPopupId = "clipboardReadPasteMenuPopup";
+const kPasteMenuItemId = "clipboardReadPasteMenuItem";
+
+function waitForPasteMenuPopupEvent(aEventSuffix) {
+ // The element with id `kPasteMenuPopupId` is inserted dynamically, hence
+ // calling `BrowserTestUtils.waitForEvent` instead of
+ // `BrowserTestUtils.waitForPopupEvent`.
+ return BrowserTestUtils.waitForEvent(
+ document,
+ "popup" + aEventSuffix,
+ false /* capture */,
+ e => {
+ return e.target.getAttribute("id") == kPasteMenuPopupId;
+ }
+ );
+}
+
+async function waitForPasteContextMenu() {
+ await waitForPasteMenuPopupEvent("shown");
+ const pasteButton = document.getElementById(kPasteMenuItemId);
+ info("Wait for paste button enabled");
+ await BrowserTestUtils.waitForMutationCondition(
+ pasteButton,
+ { attributeFilter: ["disabled"] },
+ () => !pasteButton.disabled,
+ "Wait for paste button enabled"
+ );
+}
+
+function promiseClickPasteButton() {
+ info("Wait for clicking paste contextmenu");
+ const pasteButton = document.getElementById(kPasteMenuItemId);
+ let promise = BrowserTestUtils.waitForEvent(pasteButton, "click");
+ EventUtils.synthesizeMouseAtCenter(pasteButton, {});
+ return promise;
+}
+
+async function clipboardAsyncGetData(aBrowser, aClipboardType) {
+ await SpecialPowers.spawn(aBrowser, [aClipboardType], async type => {
+ return new Promise((resolve, reject) => {
+ SpecialPowers.Services.clipboard.asyncGetData(
+ ["text/plain"],
+ type,
+ content.browsingContext.currentWindowContext,
+ content.document.nodePrincipal,
+ {
+ QueryInterface: SpecialPowers.ChromeUtils.generateQI([
+ "nsIAsyncClipboardGetCallback",
+ ]),
+ // nsIAsyncClipboardGetCallback
+ onSuccess: aAsyncGetClipboardData => {
+ resolve();
+ },
+ onError: aResult => {
+ reject(aResult);
+ },
+ }
+ );
+ });
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Avoid paste button delay enabling making test too long.
+ ["security.dialog_enable_delay", 0],
+ ],
+ });
+});
+
+supportedClipboardTypes.forEach(type => {
+ add_task(async function test_clipboard_contextmenu() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ info(`Test clipboard contextmenu for clipboard type ${type}`);
+ let pasteContextMenuPromise = waitForPasteContextMenu();
+ const asyncRead = clipboardAsyncGetData(browser, type);
+ await pasteContextMenuPromise;
+
+ // We don't allow requests for different clipboard type to be
+ // consolidated, so when the context menu is shown for a specific
+ // clipboard type and has not yet got user response, any new requests
+ // for a different type should be rejected.
+ info(
+ "Test other clipboard types before interact with paste contextmenu"
+ );
+ for (let otherType of supportedClipboardTypes) {
+ if (type == otherType) {
+ continue;
+ }
+
+ info(`Test other clipboard type ${otherType}`);
+ await Assert.rejects(
+ clipboardAsyncGetData(browser, otherType),
+ ex => ex == Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR,
+ `Request for clipboard type ${otherType} should be rejected`
+ );
+ }
+
+ await promiseClickPasteButton();
+ try {
+ await asyncRead;
+ ok(true, `nsIClipboard.asyncGetData() should success`);
+ } catch (e) {
+ ok(false, `nsIClipboard.asyncGetData() should not fail with ${e}`);
+ }
+ }
+ );
+ });
+});
diff --git a/widget/tests/browser/browser_test_clipboardcache.js b/widget/tests/browser/browser_test_clipboardcache.js
new file mode 100644
index 0000000000..8cb6adb8b5
--- /dev/null
+++ b/widget/tests/browser/browser_test_clipboardcache.js
@@ -0,0 +1,137 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Note: widget/tests/test_bug1123480.xhtml checks whether nsTransferable behaves
+// as expected with regards to private browsing mode and the clipboard cache,
+// i.e. that the clipboard is not cached to the disk when private browsing mode
+// is enabled.
+//
+// This test tests that the clipboard is not cached to the disk by IPC,
+// as a regression test for bug 1396224.
+// It indirectly uses nsTransferable, via the async navigator.clipboard API.
+
+// Create over 1 MB of sample garbage text. JavaScript strings are represented
+// by UTF16 strings, so the size is twice as much as the actual string length.
+// This value is chosen such that the size of the memory for the string exceeds
+// the kLargeDatasetSize threshold in nsTransferable.h.
+// It is also not a round number to reduce the odds of having an accidental
+// collisions with another file (since the test below looks at the file size
+// to identify the file).
+var Ipsum = "0123456789".repeat(1234321);
+var IpsumByteLength = Ipsum.length * 2;
+var SHORT_STRING_NO_CACHE = "short string that will not be cached to the disk";
+
+// Get a list of open file descriptors that refer to a file with the same size
+// as the expected data (and assume that any mutations in file descriptor counts
+// are caused by our test).
+// TODO: This logic only counts file descriptors that are still open (e.g. when
+// data persists after a copy). It does not detect cache files that exist only
+// temporarily (e.g. after a paste).
+function getClipboardCacheFDCount() {
+ let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ if (AppConstants.platform === "win") {
+ // On Windows, nsAnonymousTemporaryFile does not immediately delete a file.
+ // Instead, the Windows-specific FILE_FLAG_DELETE_ON_CLOSE flag is used,
+ // which means that the file is deleted when the last handle is closed.
+ // Apparently, this flag is unreliable (e.g. when the application crashes),
+ // so nsAnonymousTemporaryFile stores the temporary files in a subdirectory,
+ // which is cleaned up some time after start-up.
+
+ // This is just a test, and during the test we deterministically close the
+ // handles, so if FILE_FLAG_DELETE_ON_CLOSE does the thing it promises, the
+ // file is actually removed when the handle is closed.
+
+ // Path from nsAnonymousTemporaryFile.cpp, GetTempDir.
+ dir.initWithPath(PathUtils.join(PathUtils.tempDir, "mozilla-temp-files"));
+ } else {
+ dir.initWithPath("/dev/fd");
+ }
+ let count = 0;
+ for (let fdFile of dir.directoryEntries) {
+ let fileSize;
+ try {
+ fileSize = fdFile.fileSize;
+ } catch (e) {
+ // This can happen on macOS.
+ continue;
+ }
+ if (fileSize === IpsumByteLength) {
+ // Assume that the file was created by us if the size matches.
+ ++count;
+ }
+ }
+ return count;
+}
+
+async function testCopyPaste(isPrivate) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: isPrivate });
+ let tab = await BrowserTestUtils.openNewForegroundTab(win);
+ let browser = tab.linkedBrowser;
+
+ // Sanitize environment
+ await ContentTask.spawn(browser, SHORT_STRING_NO_CACHE, async shortStr => {
+ await content.navigator.clipboard.writeText(shortStr);
+ });
+
+ let initialFdCount = getClipboardCacheFDCount();
+
+ await SpecialPowers.spawn(browser, [Ipsum], async largeString => {
+ await content.navigator.clipboard.writeText(largeString);
+ });
+
+ let fdCountAfterCopy = getClipboardCacheFDCount();
+ if (isPrivate) {
+ is(fdCountAfterCopy, initialFdCount, "Private write");
+ } else {
+ is(fdCountAfterCopy, initialFdCount + 1, "Cached write");
+ }
+
+ let readStr = await SpecialPowers.spawn(browser, [], async () => {
+ let { document } = content;
+ document.body.contentEditable = true;
+ document.body.focus();
+ let pastePromise = new Promise(resolve => {
+ document.addEventListener(
+ "paste",
+ e => {
+ resolve(e.clipboardData.getData("text/plain"));
+ },
+ { once: true }
+ );
+ });
+ document.execCommand("paste");
+ return pastePromise;
+ });
+ ok(readStr === Ipsum, "Read what we pasted");
+
+ if (isPrivate) {
+ is(getClipboardCacheFDCount(), fdCountAfterCopy, "Private read");
+ } else {
+ // Upon reading from the clipboard, a temporary nsTransferable is used, for
+ // which the cache is disabled. The content process does not cache clipboard
+ // data either. So the file descriptor count should be identical.
+ is(getClipboardCacheFDCount(), fdCountAfterCopy, "Read not cached");
+ }
+
+ // Cleanup.
+ await SpecialPowers.spawn(
+ browser,
+ [SHORT_STRING_NO_CACHE],
+ async shortStr => {
+ await content.navigator.clipboard.writeText(shortStr);
+ }
+ );
+ is(getClipboardCacheFDCount(), initialFdCount, "Drop clipboard cache if any");
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+}
+
+add_task(async function test_private() {
+ await testCopyPaste(true);
+});
+
+add_task(async function test_non_private() {
+ await testCopyPaste(false);
+});
diff --git a/widget/tests/browser/browser_test_fullscreen_size.js b/widget/tests/browser/browser_test_fullscreen_size.js
new file mode 100644
index 0000000000..dace46603c
--- /dev/null
+++ b/widget/tests/browser/browser_test_fullscreen_size.js
@@ -0,0 +1,66 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function waitForReflow(aWindow) {
+ return new Promise(resolve => {
+ aWindow.requestAnimationFrame(() => {
+ aWindow.requestAnimationFrame(resolve);
+ });
+ });
+}
+
+add_task(async function fullscreen_size() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({});
+ win.gBrowser.selectedBrowser.focus();
+
+ info("Enter browser fullscreen mode");
+ let promise = Promise.all([
+ BrowserTestUtils.waitForEvent(win, "fullscreen"),
+ BrowserTestUtils.waitForEvent(win, "resize"),
+ ]);
+ win.fullScreen = true;
+ await promise;
+
+ info("Await reflow of the chrome window");
+ await waitForReflow(win);
+
+ is(win.innerHeight, win.outerHeight, "Check height");
+ is(win.innerWidth, win.outerWidth, "Check width");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1830721
+add_task(async function fullscreen_size_moz_appearance() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({});
+ win.gBrowser.selectedBrowser.focus();
+
+ info("Add -moz-appearance style to chrome document");
+ const style = win.document.createElement("style");
+ style.innerHTML = `
+ #main-window {
+ -moz-appearance: button;
+ }
+ `;
+ win.document.head.appendChild(style);
+
+ info("Await reflow of the chrome window");
+ await waitForReflow(win);
+
+ info("Enter browser fullscreen mode");
+ let promise = Promise.all([
+ BrowserTestUtils.waitForEvent(win, "fullscreen"),
+ BrowserTestUtils.waitForEvent(win, "resize"),
+ ]);
+ win.fullScreen = true;
+ await promise;
+
+ info("Await reflow of the chrome window");
+ await waitForReflow(win);
+
+ is(win.innerHeight, win.outerHeight, "Check height");
+ is(win.innerWidth, win.outerWidth, `Check width`);
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js
new file mode 100644
index 0000000000..50b19f0cc3
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_on_focus_move.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_focus_move.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ // isnot is used in file_test_ime_state_on_focus_move.js, but it's not
+ // defined as the alias of Assert.notEqual in browser-chrome tests.
+ // Therefore, we need to define it here.
+ // eslint-disable-next-line no-unused-vars
+ const isnot = Assert.notEqual;
+
+ async function runIMEStateOnFocusMoveTests(aDescription) {
+ await (async function test_IMEState_without_focused_element() {
+ const checker = new IMEStateWhenNoActiveElementTester(aDescription);
+ const expectedData = await SpecialPowers.spawn(
+ browser,
+ [aDescription],
+ description => {
+ const runner =
+ content.wrappedJSObject.createIMEStateWhenNoActiveElementTester(
+ description
+ );
+ return runner.run(content.document, content.window);
+ }
+ );
+ checker.check(expectedData);
+ })();
+ for (
+ let index = 0;
+ index < IMEStateOnFocusMoveTester.numberOfTests;
+ ++index
+ ) {
+ const checker = new IMEStateOnFocusMoveTester(aDescription, index);
+ const expectedData = await SpecialPowers.spawn(
+ browser,
+ [aDescription, index],
+ (description, aIndex) => {
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateOnFocusMoveTester(
+ description,
+ aIndex,
+ content.window
+ );
+ return content.wrappedJSObject.runner.prepareToRun(
+ content.document.querySelector("div")
+ );
+ }
+ );
+ checker.prepareToCheck(expectedData, tipWrapper);
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.run();
+ });
+ checker.check(expectedData);
+
+ if (checker.canTestOpenCloseState(expectedData)) {
+ for (const defaultOpenState of [false, true]) {
+ const expectedOpenStateData = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.prepareToRunOpenCloseTest(
+ content.document.querySelector("div")
+ );
+ }
+ );
+ checker.prepareToCheckOpenCloseTest(
+ defaultOpenState,
+ expectedOpenStateData
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runOpenCloseTest();
+ });
+ checker.checkOpenCloseTest(expectedOpenStateData);
+ }
+ }
+ await SpecialPowers.spawn(browser, [], () => {
+ content.wrappedJSObject.runner.destroy();
+ content.wrappedJSObject.runner = undefined;
+ });
+ checker.destroy();
+ } // for loop iterating test of IMEStateOnFocusMoveTester
+ } // definition of runIMEStateOnFocusMoveTests
+
+ // test for contentEditable="true"
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document
+ .querySelector("div")
+ .setAttribute("contenteditable", "true");
+ });
+ await runIMEStateOnFocusMoveTests("in div[contenteditable]");
+
+ // test for contentEditable="false"
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document
+ .querySelector("div")
+ .setAttribute("contenteditable", "false");
+ });
+ await runIMEStateOnFocusMoveTests('in div[contenteditable="false"]');
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js
new file mode 100644
index 0000000000..33217d1d2c
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js
@@ -0,0 +1,261 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_in_contenteditable_on_readonly_change.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_in_contenteditable_on_readonly_change.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ await (async function test_ime_state_in_contenteditable_on_readonly_change() {
+ const expectedDataOfInitialization = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ content.document.body.innerHTML = "<div contenteditable><br></div>";
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateInContentEditableOnReadonlyChangeTester();
+ const editingHost = content.document.querySelector(
+ "div[contenteditable]"
+ );
+ return content.wrappedJSObject.runner.prepareToRun(
+ editingHost,
+ editingHost,
+ content.window
+ );
+ }
+ );
+ const tester = new IMEStateInContentEditableOnReadonlyChangeTester();
+ tester.checkResultOfPreparation(
+ expectedDataOfInitialization,
+ window,
+ tipWrapper
+ );
+ const expectedDataOfReadonly = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly();
+ }
+ );
+ tester.checkResultOfMakingHTMLEditorReadonly(expectedDataOfReadonly);
+ const expectedDataOfEditable = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable();
+ }
+ );
+ tester.checkResultOfMakingHTMLEditorEditable(expectedDataOfEditable);
+ const expectedDataOfFinalization = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.runToRemoveContentEditableAttribute();
+ }
+ );
+ tester.checkResultOfRemovingContentEditableAttribute(
+ expectedDataOfFinalization
+ );
+ tester.clear();
+ })();
+
+ await (async function test_ime_state_in_button_in_contenteditable_on_readonly_change() {
+ const expectedDataOfInitialization = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ content.document.body.innerHTML =
+ "<div contenteditable><br><button>button</button></div>";
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateInContentEditableOnReadonlyChangeTester();
+ const editingHost = content.document.querySelector(
+ "div[contenteditable]"
+ );
+ return content.wrappedJSObject.runner.prepareToRun(
+ editingHost,
+ editingHost.querySelector("button"),
+ content.window
+ );
+ }
+ );
+ const tester = new IMEStateInContentEditableOnReadonlyChangeTester();
+ tester.checkResultOfPreparation(
+ expectedDataOfInitialization,
+ window,
+ tipWrapper
+ );
+ const expectedDataOfReadonly = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly();
+ }
+ );
+ tester.checkResultOfMakingHTMLEditorReadonly(expectedDataOfReadonly);
+ const expectedDataOfEditable = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable();
+ }
+ );
+ tester.checkResultOfMakingHTMLEditorEditable(expectedDataOfEditable);
+ const expectedDataOfFinalization = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.runToRemoveContentEditableAttribute();
+ }
+ );
+ tester.checkResultOfRemovingContentEditableAttribute(
+ expectedDataOfFinalization
+ );
+ tester.clear();
+ })();
+
+ await (async function test_ime_state_of_text_controls_in_contenteditable_on_readonly_change() {
+ const tester =
+ new IMEStateOfTextControlInContentEditableOnReadonlyChangeTester();
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML = "<div contenteditable></div>";
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateOfTextControlInContentEditableOnReadonlyChangeTester();
+ });
+ for (
+ let index = 0;
+ index <
+ IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.numberOfTextControlTypes;
+ index++
+ ) {
+ const expectedDataOfInitialization = await SpecialPowers.spawn(
+ browser,
+ [index],
+ aIndex => {
+ const editingHost = content.document.querySelector("div");
+ return content.wrappedJSObject.runner.prepareToRun(
+ aIndex,
+ editingHost,
+ content.window
+ );
+ }
+ );
+ tester.checkResultOfPreparation(
+ expectedDataOfInitialization,
+ window,
+ tipWrapper
+ );
+ const expectedDataOfMakingParentEditingHost =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeParentEditingHost();
+ });
+ tester.checkResultOfMakingParentEditingHost(
+ expectedDataOfMakingParentEditingHost
+ );
+ const expectedDataOfMakingHTMLEditorReadonly =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly();
+ });
+ tester.checkResultOfMakingHTMLEditorReadonly(
+ expectedDataOfMakingHTMLEditorReadonly
+ );
+ const expectedDataOfMakingHTMLEditorEditable =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable();
+ });
+ tester.checkResultOfMakingHTMLEditorEditable(
+ expectedDataOfMakingHTMLEditorEditable
+ );
+ const expectedDataOfMakingParentNonEditable =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeParentNonEditingHost();
+ });
+ tester.checkResultOfMakingParentNonEditable(
+ expectedDataOfMakingParentNonEditable
+ );
+ tester.clear();
+ }
+ })();
+
+ await (async function test_ime_state_outside_contenteditable_on_readonly_change() {
+ const tester =
+ new IMEStateOutsideContentEditableOnReadonlyChangeTester();
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML = "<div contenteditable></div>";
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateOutsideContentEditableOnReadonlyChangeTester();
+ });
+ for (
+ let index = 0;
+ index <
+ IMEStateOutsideContentEditableOnReadonlyChangeTester.numberOfFocusTargets;
+ index++
+ ) {
+ const expectedDataOfInitialization = await SpecialPowers.spawn(
+ browser,
+ [index],
+ aIndex => {
+ const editingHost = content.document.querySelector("div");
+ return content.wrappedJSObject.runner.prepareToRun(
+ aIndex,
+ editingHost,
+ content.window
+ );
+ }
+ );
+ tester.checkResultOfPreparation(
+ expectedDataOfInitialization,
+ window,
+ tipWrapper
+ );
+ const expectedDataOfMakingParentEditingHost =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeParentEditingHost();
+ });
+ tester.checkResultOfMakingParentEditingHost(
+ expectedDataOfMakingParentEditingHost
+ );
+ const expectedDataOfMakingHTMLEditorReadonly =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly();
+ });
+ tester.checkResultOfMakingHTMLEditorReadonly(
+ expectedDataOfMakingHTMLEditorReadonly
+ );
+ const expectedDataOfMakingHTMLEditorEditable =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable();
+ });
+ tester.checkResultOfMakingHTMLEditorEditable(
+ expectedDataOfMakingHTMLEditorEditable
+ );
+ const expectedDataOfMakingParentNonEditable =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeParentNonEditingHost();
+ });
+ tester.checkResultOfMakingParentNonEditable(
+ expectedDataOfMakingParentNonEditable
+ );
+ tester.clear();
+ }
+ })();
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js
new file mode 100644
index 0000000000..5ea5990e96
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_on_focus_move.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_focus_move.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ // isnot is used in file_test_ime_state_on_focus_move.js, but it's not
+ // defined as the alias of Assert.notEqual in browser-chrome tests.
+ // Therefore, we need to define it here.
+ // eslint-disable-next-line no-unused-vars
+ const isnot = Assert.notEqual;
+
+ async function runIMEStateOnFocusMoveTests(aDescription) {
+ await (async function test_IMEState_without_focused_element() {
+ const checker = new IMEStateWhenNoActiveElementTester(aDescription);
+ const expectedData = await SpecialPowers.spawn(
+ browser,
+ [aDescription],
+ description => {
+ const runner =
+ content.wrappedJSObject.createIMEStateWhenNoActiveElementTester(
+ description
+ );
+ return runner.run(content.document, content.window);
+ }
+ );
+ checker.check(expectedData);
+ })();
+ for (
+ let index = 0;
+ index < IMEStateOnFocusMoveTester.numberOfTests;
+ ++index
+ ) {
+ const checker = new IMEStateOnFocusMoveTester(aDescription, index);
+ const expectedData = await SpecialPowers.spawn(
+ browser,
+ [aDescription, index],
+ (description, aIndex) => {
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateOnFocusMoveTester(
+ description,
+ aIndex,
+ content.window
+ );
+ return content.wrappedJSObject.runner.prepareToRun(
+ content.document.querySelector("div")
+ );
+ }
+ );
+ checker.prepareToCheck(expectedData, tipWrapper);
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.run();
+ });
+ checker.check(expectedData);
+
+ if (checker.canTestOpenCloseState(expectedData)) {
+ for (const defaultOpenState of [false, true]) {
+ const expectedOpenStateData = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.prepareToRunOpenCloseTest(
+ content.document.querySelector("div")
+ );
+ }
+ );
+ checker.prepareToCheckOpenCloseTest(
+ defaultOpenState,
+ expectedOpenStateData
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runOpenCloseTest();
+ });
+ checker.checkOpenCloseTest(expectedOpenStateData);
+ }
+ }
+ await SpecialPowers.spawn(browser, [], () => {
+ content.wrappedJSObject.runner.destroy();
+ content.wrappedJSObject.runner = undefined;
+ });
+ checker.destroy();
+ } // for loop iterating test of IMEStateOnFocusMoveTester
+ } // definition of runIMEStateOnFocusMoveTests
+
+ // test designMode
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.designMode = "on";
+ });
+ await runIMEStateOnFocusMoveTests('in designMode="on"');
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.designMode = "off";
+ });
+ await runIMEStateOnFocusMoveTests('in designMode="off"');
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_in_plugin_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_plugin_in_remote_content.js
new file mode 100644
index 0000000000..0862b51080
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_in_plugin_in_remote_content.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.wrappedJSObject.waitForIMEContentObserverSendingNotifications =
+ () => {
+ return new content.window.Promise(resolve =>
+ content.window.requestAnimationFrame(() =>
+ content.window.requestAnimationFrame(resolve)
+ )
+ );
+ };
+ content.document.body.innerHTML =
+ '<input><object type="application/x-test"></object>';
+ });
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.activeElement?.blur();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when no element has focus"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when no element has focus"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("object").focus();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when an <object> for plugin has focus"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME enabled state should not have focus when an <object> for plugin has focus"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("object").blur();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when an <object> for plugin gets blurred"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when an <object> for plugin gets blurred"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("object").focus();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when an <object> for plugin gets focused again"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when an <object> for plugin gets focused again"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("object").remove();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when focused <object> for plugin is removed from the document"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when focused <object> for plugin is removed from the document"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("input").focus();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "IME enabled state should be enabled after <input> gets focus"
+ );
+ ok(
+ tipWrapper.IMEHasFocus,
+ "IME should have focus after <input> gets focus"
+ );
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js
new file mode 100644
index 0000000000..4d8acab1ff
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_in_text_control_on_reframe.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_in_text_control_on_reframe.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ await (async function test_ime_state_outside_contenteditable_on_readonly_change() {
+ const tester = new IMEStateInTextControlOnReframeTester();
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.innerHTML = "<div contenteditable></div>";
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateInTextControlOnReframeTester();
+ });
+ for (
+ let index = 0;
+ index < IMEStateInTextControlOnReframeTester.numberOfTextControlTypes;
+ index++
+ ) {
+ tipWrapper.clearFocusBlurNotifications();
+ const expectedData1 = await SpecialPowers.spawn(
+ browser,
+ [index],
+ aIndex => {
+ return content.wrappedJSObject.runner.prepareToRun(
+ aIndex,
+ content.document,
+ content.window
+ );
+ }
+ );
+ tipWrapper.typeA();
+ await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve =>
+ content.window.requestAnimationFrame(() =>
+ content.window.requestAnimationFrame(resolve)
+ )
+ );
+ });
+ tester.checkResultAfterTypingA(expectedData1, window, tipWrapper);
+
+ const expectedData2 = await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.prepareToRun2();
+ });
+ tipWrapper.typeA();
+ await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve =>
+ content.window.requestAnimationFrame(() =>
+ content.window.requestAnimationFrame(resolve)
+ )
+ );
+ });
+ tester.checkResultAfterTypingA2(expectedData2);
+ tester.clear();
+ }
+ })();
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_on_editable_state_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_editable_state_change_in_remote_content.js
new file mode 100644
index 0000000000..8c38f97b72
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_on_editable_state_change_in_remote_content.js
@@ -0,0 +1,297 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.wrappedJSObject.waitForIMEContentObserverSendingNotifications =
+ () => {
+ return new content.window.Promise(resolve =>
+ content.window.requestAnimationFrame(() =>
+ content.window.requestAnimationFrame(resolve)
+ )
+ );
+ };
+ content.wrappedJSObject.resetIMEStateWithFocusMove = () => {
+ const input = content.document.createElement("input");
+ content.document.body.appendChild(input);
+ input.focus();
+ input.remove();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ };
+ content.document.body.innerHTML = "<div></div>";
+ });
+
+ function resetIMEStateWithFocusMove() {
+ return SpecialPowers.spawn(browser, [], () => {
+ const input = content.document.createElement("input");
+ content.document.body.appendChild(input);
+ input.focus();
+ input.remove();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ }
+
+ await (async function test_setting_contenteditable_of_focused_div() {
+ await SpecialPowers.spawn(browser, [], () => {
+ const div = content.document.querySelector("div");
+ div.setAttribute("tabindex", "0");
+ div.focus();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "test_setting_contenteditable_of_focused_div: IME should be disabled when non-editable <div> has focus"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .querySelector("div")
+ .setAttribute("contenteditable", "");
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "test_setting_contenteditable_of_focused_div: IME should be enabled when contenteditable of focused <div> is set"
+ );
+ ok(
+ tipWrapper.IMEHasFocus,
+ "test_setting_contenteditable_of_focused_div: IME should have focus when contenteditable of focused <div> is set"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .querySelector("div")
+ .removeAttribute("contenteditable");
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "test_setting_contenteditable_of_focused_div: IME should be disabled when contenteditable of focused <div> is removed"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "test_setting_contenteditable_of_focused_div: IME should not have focus when contenteditable of focused <div> is removed"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("div").removeAttribute("tabindex");
+ });
+ })();
+
+ await resetIMEStateWithFocusMove();
+
+ await (async function test_removing_contenteditable_of_non_last_editable_div() {
+ await SpecialPowers.spawn(browser, [], async () => {
+ const div = content.document.querySelector("div");
+ div.setAttribute("tabindex", "0");
+ div.setAttribute("contenteditable", "");
+ const anotherEditableDiv = content.document.createElement("div");
+ anotherEditableDiv.setAttribute("contenteditable", "");
+ div.parentElement.appendChild(anotherEditableDiv);
+ div.focus();
+ await content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ div.removeAttribute("contenteditable");
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "test_removing_contenteditable_of_non_last_editable_div: IME should be disabled when contenteditable of focused <div> is removed"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "test_removing_contenteditable_of_non_last_editable_div: IME should not have focus when contenteditable of focused <div> is removed"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ const divs = content.document.querySelectorAll("div");
+ divs[1].remove();
+ divs[0].removeAttribute("tabindex");
+ });
+ })();
+
+ await resetIMEStateWithFocusMove();
+
+ await (async function test_setting_designMode() {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.window.focus();
+ content.document.designMode = "on";
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ 'test_setting_designMode: IME should be enabled when designMode is set to "on"'
+ );
+ ok(
+ tipWrapper.IMEHasFocus,
+ 'test_setting_designMode: IME should have focus when designMode is set to "on"'
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.designMode = "off";
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ 'test_setting_designMode: IME should be disabled when designMode is set to "off"'
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ 'test_setting_designMode: IME should not have focus when designMode is set to "off"'
+ );
+ })();
+
+ await resetIMEStateWithFocusMove();
+
+ async function test_setting_content_editable_of_body_when_shadow_DOM_has_focus(
+ aMode
+ ) {
+ await SpecialPowers.spawn(browser, [aMode], mode => {
+ const div = content.document.querySelector("div");
+ const shadow = div.attachShadow({ mode });
+ content.wrappedJSObject.divInShadow =
+ content.document.createElement("div");
+ content.wrappedJSObject.divInShadow.setAttribute("tabindex", "0");
+ shadow.appendChild(content.wrappedJSObject.divInShadow);
+ content.wrappedJSObject.divInShadow.focus();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${aMode}): IME should be disabled when non-editable <div> in a shadow DOM has focus`
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.setAttribute("contenteditable", "");
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ // todo_is because of bug 1807597. Gecko does not update focus when focused
+ // element becomes an editable child. Therefore, cannot initialize
+ // HTMLEditor with the new editing host.
+ todo_is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${aMode}): IME should be enabled when the <body> becomes editable`
+ );
+ todo(
+ tipWrapper.IMEHasFocus,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${aMode}): IME should have focus when the <body> becomes editable`
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.removeAttribute("contenteditable");
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${aMode}): IME should be disabled when the <body> becomes not editable`
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${aMode}): IME should not have focus when the <body> becomes not editable`
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("div").remove();
+ content.document.body.appendChild(
+ content.document.createElement("div")
+ );
+ });
+ }
+
+ async function test_setting_designMode_when_shadow_DOM_has_focus(aMode) {
+ await SpecialPowers.spawn(browser, [aMode], mode => {
+ const div = content.document.querySelector("div");
+ const shadow = div.attachShadow({ mode });
+ content.wrappedJSObject.divInShadow =
+ content.document.createElement("div");
+ content.wrappedJSObject.divInShadow.setAttribute("tabindex", "0");
+ shadow.appendChild(content.wrappedJSObject.divInShadow);
+ content.wrappedJSObject.divInShadow.focus();
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should be disabled when non-editable <div> in a shadow DOM has focus`
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.designMode = "on";
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should stay disabled when designMode is set`
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should not have focus when designMode is set`
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.wrappedJSObject.divInShadow.setAttribute(
+ "contenteditable",
+ ""
+ );
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ // todo_is because of bug 1807597. Gecko does not update focus when focused
+ // document is into the design mode. Therefore, cannot initialize
+ // HTMLEditor with the document node properly.
+ todo_is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should be enabled when focused <div> in a shadow DOM becomes editable`
+ );
+ todo(
+ tipWrapper.IMEHasFocus,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should have focus when focused <div> in a shadow DOM becomes editable`
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.designMode = "off";
+ return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications();
+ });
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should be disabled when designMode is unset`
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should not have focus when designMode is unset`
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("div").remove();
+ content.document.body.appendChild(
+ content.document.createElement("div")
+ );
+ });
+ }
+
+ for (const mode of ["open", "closed"]) {
+ await test_setting_content_editable_of_body_when_shadow_DOM_has_focus(
+ mode
+ );
+ await resetIMEStateWithFocusMove();
+ await test_setting_designMode_when_shadow_DOM_has_focus(mode);
+ await resetIMEStateWithFocusMove();
+ }
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_on_focus_move_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_focus_move_in_remote_content.js
new file mode 100644
index 0000000000..3916d3d47c
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_on_focus_move_in_remote_content.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_on_focus_move.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_focus_move.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ // isnot is used in file_test_ime_state_on_focus_move.js, but it's not
+ // defined as the alias of Assert.notEqual in browser-chrome tests.
+ // Therefore, we need to define it here.
+ // eslint-disable-next-line no-unused-vars
+ const isnot = Assert.notEqual;
+
+ async function runIMEStateOnFocusMoveTests(aDescription) {
+ await (async function test_IMEState_without_focused_element() {
+ const checker = new IMEStateWhenNoActiveElementTester(aDescription);
+ const expectedData = await SpecialPowers.spawn(
+ browser,
+ [aDescription],
+ description => {
+ const runner =
+ content.wrappedJSObject.createIMEStateWhenNoActiveElementTester(
+ description
+ );
+ return runner.run(content.document, content.window);
+ }
+ );
+ checker.check(expectedData);
+ })();
+ for (
+ let index = 0;
+ index < IMEStateOnFocusMoveTester.numberOfTests;
+ ++index
+ ) {
+ const checker = new IMEStateOnFocusMoveTester(aDescription, index);
+ const expectedData = await SpecialPowers.spawn(
+ browser,
+ [aDescription, index],
+ (description, aIndex) => {
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateOnFocusMoveTester(
+ description,
+ aIndex,
+ content.window
+ );
+ return content.wrappedJSObject.runner.prepareToRun(
+ content.document.querySelector("div")
+ );
+ }
+ );
+ checker.prepareToCheck(expectedData, tipWrapper);
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.run();
+ });
+ checker.check(expectedData);
+
+ if (checker.canTestOpenCloseState(expectedData)) {
+ for (const defaultOpenState of [false, true]) {
+ const expectedOpenStateData = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return content.wrappedJSObject.runner.prepareToRunOpenCloseTest(
+ content.document.querySelector("div")
+ );
+ }
+ );
+ checker.prepareToCheckOpenCloseTest(
+ defaultOpenState,
+ expectedOpenStateData
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runOpenCloseTest();
+ });
+ checker.checkOpenCloseTest(expectedOpenStateData);
+ }
+ }
+ await SpecialPowers.spawn(browser, [], () => {
+ content.wrappedJSObject.runner.destroy();
+ content.wrappedJSObject.runner = undefined;
+ });
+ checker.destroy();
+ } // for loop iterating test of IMEStateOnFocusMoveTester
+ } // definition of runIMEStateOnFocusMoveTests
+
+ // test for normal contents.
+ await runIMEStateOnFocusMoveTests("in non-editable container");
+
+ // test for removing contentEditable
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document
+ .querySelector("div")
+ .setAttribute("contenteditable", "true");
+ content.document.querySelector("div").focus();
+ await new Promise(resolve =>
+ content.window.requestAnimationFrame(() =>
+ content.window.requestAnimationFrame(resolve)
+ )
+ );
+ content.document
+ .querySelector("div")
+ .removeAttribute("contenteditable");
+ });
+ await runIMEStateOnFocusMoveTests(
+ "after removing contenteditable from the container"
+ );
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_on_input_type_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_input_type_change_in_remote_content.js
new file mode 100644
index 0000000000..2a4bc4c332
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_on_input_type_change_in_remote_content.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_on_input_type_change.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_input_type_change.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ for (
+ let srcIndex = 0;
+ srcIndex < IMEStateOnInputTypeChangeTester.numberOfTests;
+ srcIndex++
+ ) {
+ const tester = new IMEStateOnInputTypeChangeTester(srcIndex);
+ for (
+ let destIndex = 0;
+ destIndex < IMEStateOnInputTypeChangeTester.numberOfTests;
+ destIndex++
+ ) {
+ const expectedResultBefore = await SpecialPowers.spawn(
+ browser,
+ [srcIndex, destIndex],
+ (aSrcIndex, aDestIndex) => {
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateOnInputTypeChangeTester(
+ aSrcIndex
+ );
+ return content.wrappedJSObject.runner.prepareToRun(
+ aDestIndex,
+ content.window,
+ content.document.body
+ );
+ }
+ );
+ if (expectedResultBefore === false) {
+ continue;
+ }
+ tester.checkBeforeRun(expectedResultBefore, tipWrapper);
+ const expectedResult = await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.run();
+ });
+ tester.checkResult(expectedResultBefore, expectedResult);
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.clear();
+ });
+ tipWrapper.clearFocusBlurNotifications();
+ }
+ tester.clear();
+ }
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_ime_state_on_readonly_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_readonly_change_in_remote_content.js
new file mode 100644
index 0000000000..a0c0019328
--- /dev/null
+++ b/widget/tests/browser/browser_test_ime_state_on_readonly_change_in_remote_content.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_on_readonly_change.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_readonly_change.js",
+ this
+);
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html",
+ async function (browser) {
+ const tipWrapper = new TIPWrapper(window);
+ ok(
+ tipWrapper.isAvailable(),
+ "TextInputProcessor should've been initialized"
+ );
+
+ const tester = new IMEStateOnReadonlyChangeTester();
+ for (
+ let i = 0;
+ i < IMEStateOnReadonlyChangeTester.numberOfTextControlTypes;
+ i++
+ ) {
+ const expectedResultBefore = await SpecialPowers.spawn(
+ browser,
+ [i],
+ aIndex => {
+ content.wrappedJSObject.runner =
+ content.wrappedJSObject.createIMEStateOnReadonlyChangeTester(
+ aIndex
+ );
+ return content.wrappedJSObject.runner.prepareToRun(
+ aIndex,
+ content.window,
+ content.document.body
+ );
+ }
+ );
+ tester.checkBeforeRun(expectedResultBefore, tipWrapper);
+ const expectedResultOfMakingTextControlReadonly =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeTextControlReadonly();
+ });
+ tester.checkResultOfMakingTextControlReadonly(
+ expectedResultOfMakingTextControlReadonly
+ );
+ const expectedResultOfMakingTextControlEditable =
+ await SpecialPowers.spawn(browser, [], () => {
+ return content.wrappedJSObject.runner.runToMakeTextControlEditable();
+ });
+ tester.checkResultOfMakingTextControlEditable(
+ expectedResultOfMakingTextControlEditable
+ );
+ tipWrapper.clearFocusBlurNotifications();
+ tester.clear();
+ }
+ }
+ );
+});
diff --git a/widget/tests/browser/browser_test_scrollbar_colors.js b/widget/tests/browser/browser_test_scrollbar_colors.js
new file mode 100644
index 0000000000..2152412071
--- /dev/null
+++ b/widget/tests/browser/browser_test_scrollbar_colors.js
@@ -0,0 +1,146 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+add_task(async () => {
+ const URL_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ URL_ROOT + "helper_scrollbar_colors.html"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ ChromeUtils.defineESModuleGetters(this, {
+ WindowsVersionInfo:
+ "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs",
+ });
+
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js",
+ this
+ );
+
+ // == Native theme ==
+
+ const WIN_REFERENCES = [
+ // Yellow background
+ ["255,255,0", 6889],
+ // Blue scrollbar face
+ ["0,0,255", 540],
+ // Cyan scrollbar track
+ ["0,255,255", 2487],
+ ];
+
+ const MAC_REFERENCES = [
+ // Yellow background
+ ["255,255,0", 7225],
+ // Blue scrollbar face
+ ["0,0,255", 416],
+ // Cyan scrollbar track
+ ["0,255,255", 1760],
+ ];
+
+ // Values have been updated from 8100, 720, 1180 for linux1804
+ const LINUX_REFERENCES = [
+ // Yellow background
+ ["255,255,0", 7744],
+ // Blue scrollbar face
+ ["0,0,255", 1104],
+ // Cyan scrollbar track
+ ["0,255,255", 1152],
+ ];
+
+ // == Non-native theme ==
+
+ const WIN10_NNT_REFERENCES = [
+ // Yellow background
+ ["255,255,0", 6889],
+ // Blue scrollbar face
+ ["0,0,255", 612],
+ // Cyan scrollbar track
+ ["0,255,255", 2355],
+ ];
+
+ const WIN11_NNT_REFERENCES = [
+ // Yellow background
+ ["255,255,0", 6889],
+ // Blue scrollbar face
+ ["0,0,255", 324],
+ // Cyan scrollbar track
+ ["0,255,255", 2787],
+ ];
+
+ const MAC_NNT_REFERENCES = MAC_REFERENCES;
+
+ const LINUX_NNT_REFERENCES = [
+ // Yellow background
+ ["255,255,0", 7744],
+ // Blue scrollbar face
+ ["0,0,255", 368],
+ // Cyan scrollbar track
+ ["0,255,255", 1852],
+ ];
+
+ function countPixels(canvas) {
+ let result = new Map();
+ let ctx = canvas.getContext("2d");
+ let image = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ let data = image.data;
+ let size = image.width * image.height;
+ for (let i = 0; i < size; i++) {
+ let key = data.subarray(i * 4, i * 4 + 3).toString();
+ let value = result.get(key);
+ value = value ? value : 0;
+ result.set(key, value + 1);
+ }
+ return result;
+ }
+
+ let outer = content.document.querySelector(".outer");
+ let outerRect = outer.getBoundingClientRect();
+ if (
+ outerRect.width == outer.clientWidth &&
+ outerRect.height == outer.clientHeight
+ ) {
+ ok(true, "Using overlay scrollbar, skip this test");
+ return;
+ }
+ content.document.querySelector("#style").textContent = `
+ .outer { scrollbar-color: blue cyan; }
+ `;
+
+ let canvas = snapshotRect(content.window, outerRect);
+ let stats = countPixels(canvas);
+ let isNNT = SpecialPowers.getBoolPref("widget.non-native-theme.enabled");
+
+ let references;
+ if (content.navigator.platform.startsWith("Win")) {
+ if (!isNNT) {
+ references = WIN_REFERENCES;
+ } else if (WindowsVersionInfo.get().buildNumber >= 22000) {
+ // Windows 11 NNT
+ references = WIN11_NNT_REFERENCES;
+ } else {
+ // Windows 10 NNT
+ references = WIN10_NNT_REFERENCES;
+ }
+ } else if (content.navigator.platform.startsWith("Mac")) {
+ references = isNNT ? MAC_NNT_REFERENCES : MAC_REFERENCES;
+ } else if (content.navigator.platform.startsWith("Linux")) {
+ references = isNNT ? LINUX_NNT_REFERENCES : LINUX_REFERENCES;
+ } else {
+ ok(false, "Unsupported platform");
+ }
+ for (let [color, count] of references) {
+ let value = stats.get(color);
+ is(value, count, `Pixel count of color ${color}`);
+ }
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/widget/tests/browser/browser_test_swipe_gesture.js b/widget/tests/browser/browser_test_swipe_gesture.js
new file mode 100644
index 0000000000..0ac85d80c8
--- /dev/null
+++ b/widget/tests/browser/browser_test_swipe_gesture.js
@@ -0,0 +1,1274 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+async function waitForWhile() {
+ await new Promise(resolve => {
+ requestIdleCallback(resolve, { timeout: 300 });
+ });
+ await new Promise(r => requestAnimationFrame(r));
+}
+
+requestLongerTimeout(2);
+
+add_task(async () => {
+ // Set the default values for an OS that supports swipe to nav, except for
+ // pixel-size which varies by OS, we vary it in differente tests in this file.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ // Set the velocity-contribution to 0 so we can exactly control the
+ // values in the swipe tracker via the delta in the events that we send.
+ ["widget.swipe.success-velocity-contribution", 0.0],
+ ["widget.swipe.pixel-size", 550.0],
+ ],
+ });
+
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!gBrowser.webNavigation.canGoForward);
+
+ let wheelEventCount = 0;
+ tab.linkedBrowser.addEventListener("wheel", () => {
+ wheelEventCount++;
+ });
+
+ // Send a pan that starts a navigate back but doesn't have enough delta to do
+ // anything. Don't send the pan end because we want to check the opacity
+ // before the MSD animation in SwipeTracker starts which can temporarily put
+ // us at 1 opacity.
+ await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 0.9);
+ await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 0.9);
+
+ // Check both getComputedStyle instead of element.style.opacity because we use a transition on the opacity.
+ let computedOpacity = window
+ .getComputedStyle(gHistorySwipeAnimation._prevBox)
+ .getPropertyValue("opacity");
+ is(computedOpacity, "1", "opacity of prevbox is 1");
+ let opacity = gHistorySwipeAnimation._prevBox.style.opacity;
+ is(opacity, "", "opacity style isn't explicitly set");
+
+ const isTranslatingIcon =
+ Services.prefs.getIntPref(
+ "browser.swipe.navigation-icon-start-position",
+ 0
+ ) != 0 ||
+ Services.prefs.getIntPref(
+ "browser.swipe.navigation-icon-end-position",
+ 0
+ ) != 0;
+ if (isTranslatingIcon != 0) {
+ isnot(
+ window
+ .getComputedStyle(gHistorySwipeAnimation._prevBox)
+ .getPropertyValue("translate"),
+ "none",
+ "translate of prevbox is not `none` during gestures"
+ );
+ }
+
+ await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 0.9);
+
+ // NOTE: We only get a wheel event for the beginPhase, rest of events have
+ // been captured by the swipe gesture module.
+ is(wheelEventCount, 1, "Received a wheel event");
+
+ await waitForWhile();
+ // Make sure any navigation didn't happen.
+ is(tab.linkedBrowser.currentURI.spec, secondPage);
+
+ // Try to navigate backward.
+ wheelEventCount = 0;
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 1);
+ // NOTE: We only get a wheel event for the beginPhase, rest of events have
+ // been captured by the swipe gesture module.
+ is(wheelEventCount, 1, "Received a wheel event");
+
+ // The element.style opacity will be 0 because we set it to 0 on successful navigation, however
+ // we have a tranisition on it so the computed style opacity will still be 1 because the transition hasn't started yet.
+ computedOpacity = window
+ .getComputedStyle(gHistorySwipeAnimation._prevBox)
+ .getPropertyValue("opacity");
+ ok(computedOpacity == 1, "computed opacity of prevbox is 1");
+ opacity = gHistorySwipeAnimation._prevBox.style.opacity;
+ ok(opacity == 0, "element.style opacity of prevbox 0");
+
+ if (isTranslatingIcon) {
+ // We don't have a transition for translate property so that we still have
+ // some amount of translate.
+ isnot(
+ window
+ .getComputedStyle(gHistorySwipeAnimation._prevBox)
+ .getPropertyValue("translate"),
+ "none",
+ "translate of prevbox is not `none` during the opacity transition"
+ );
+ }
+
+ // Make sure the gesture triggered going back to the previous page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Same test as above but pixel-size is increased and the multipliers passed to panLeftToRight correspondingly increased.
+add_task(async () => {
+ // Set the default values for an OS that supports swipe to nav, except for
+ // pixel-size which varies by OS, we vary it in differente tests
+ // in this file.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ // Set the velocity-contribution to 0 so we can exactly control the
+ // values in the swipe tracker via the delta in the events that we send.
+ ["widget.swipe.success-velocity-contribution", 0.0],
+ ["widget.swipe.pixel-size", 1100.0],
+ ],
+ });
+
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!gBrowser.webNavigation.canGoForward);
+
+ let wheelEventCount = 0;
+ tab.linkedBrowser.addEventListener("wheel", () => {
+ wheelEventCount++;
+ });
+
+ // Send a pan that starts a navigate back but doesn't have enough delta to do
+ // anything. Don't send the pan end because we want to check the opacity
+ // before the MSD animation in SwipeTracker starts which can temporarily put
+ // us at 1 opacity.
+ await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 1.8);
+ await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 1.8);
+
+ // Check both getComputedStyle instead of element.style.opacity because we use a transition on the opacity.
+ let computedOpacity = window
+ .getComputedStyle(gHistorySwipeAnimation._prevBox)
+ .getPropertyValue("opacity");
+ is(computedOpacity, "1", "opacity of prevbox is 1");
+ let opacity = gHistorySwipeAnimation._prevBox.style.opacity;
+ is(opacity, "", "opacity style isn't explicitly set");
+
+ await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 1.8);
+
+ // NOTE: We only get a wheel event for the beginPhase, rest of events have
+ // been captured by the swipe gesture module.
+ is(wheelEventCount, 1, "Received a wheel event");
+
+ await waitForWhile();
+ // Make sure any navigation didn't happen.
+ is(tab.linkedBrowser.currentURI.spec, secondPage);
+
+ // Try to navigate backward.
+ wheelEventCount = 0;
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 2);
+ // NOTE: We only get a wheel event for the beginPhase, rest of events have
+ // been captured by the swipe gesture module.
+ is(wheelEventCount, 1, "Received a wheel event");
+
+ // The element.style opacity will be 0 because we set it to 0 on successful navigation, however
+ // we have a tranisition on it so the computed style opacity will still be 1 because the transition hasn't started yet.
+ computedOpacity = window
+ .getComputedStyle(gHistorySwipeAnimation._prevBox)
+ .getPropertyValue("opacity");
+ ok(computedOpacity == 1, "computed opacity of prevbox is 1");
+ opacity = gHistorySwipeAnimation._prevBox.style.opacity;
+ ok(opacity == 0, "element.style opacity of prevbox 0");
+
+ // Make sure the gesture triggered going back to the previous page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ // Set the default values for an OS that supports swipe to nav, except for
+ // pixel-size which varies by OS, we vary it in different tests
+ // in this file.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ // Set the velocity-contribution to 1 (default 0.05f) so velocity is a
+ // large contribution to the success value in SwipeTracker.cpp so it
+ // pushes us into success territory without going into success territory
+ // purely from th deltas.
+ ["widget.swipe.success-velocity-contribution", 2.0],
+ ["widget.swipe.pixel-size", 550.0],
+ ],
+ });
+
+ async function runTest() {
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!gBrowser.webNavigation.canGoForward);
+
+ let wheelEventCount = 0;
+ tab.linkedBrowser.addEventListener("wheel", () => {
+ wheelEventCount++;
+ });
+
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let startTime = performance.now();
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 0.2);
+ let endTime = performance.now();
+
+ // If sending the events took too long then we might not have been able
+ // to generate enough velocity.
+ // The value 230 was picked based on try runs, in particular test verify
+ // runs on mac were the long pole, and when we get times near this we can
+ // still achieve the required velocity.
+ if (endTime - startTime > 230) {
+ BrowserTestUtils.removeTab(tab);
+ return false;
+ }
+
+ // NOTE: We only get a wheel event for the beginPhase, rest of events have
+ // been captured by the swipe gesture module.
+ is(wheelEventCount, 1, "Received a wheel event");
+
+ // The element.style opacity will be 0 because we set it to 0 on successful navigation, however
+ // we have a tranisition on it so the computed style opacity will still be 1 because the transition hasn't started yet.
+ let computedOpacity = window
+ .getComputedStyle(gHistorySwipeAnimation._prevBox)
+ .getPropertyValue("opacity");
+ ok(computedOpacity == 1, "computed opacity of prevbox is 1");
+ let opacity = gHistorySwipeAnimation._prevBox.style.opacity;
+ ok(opacity == 0, "element.style opacity of prevbox 0");
+
+ // Make sure the gesture triggered going back to the previous page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ BrowserTestUtils.removeTab(tab);
+
+ return true;
+ }
+
+ let numTries = 15;
+ while (numTries > 0) {
+ await new Promise(r => requestAnimationFrame(r));
+ await new Promise(resolve => requestIdleCallback(resolve));
+ await new Promise(r => requestAnimationFrame(r));
+
+ // runTest return value indicates if test was able to run to the end.
+ if (await runTest()) {
+ break;
+ }
+ numTries--;
+ }
+ ok(numTries > 0, "never ran the test");
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ // Set the default values for an OS that supports swipe to nav, except for
+ // pixel-size which varies by OS, we vary it in differente tests
+ // in this file.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ // Set the velocity-contribution to 0 so we can exactly control the
+ // values in the swipe tracker via the delta in the events that we send.
+ ["widget.swipe.success-velocity-contribution", 0.0],
+ ["widget.swipe.pixel-size", 550.0],
+ ],
+ });
+
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!gBrowser.webNavigation.canGoForward);
+
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 2);
+
+ // Make sure the gesture triggered going back to the previous page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ while (
+ gHistorySwipeAnimation._prevBox != null ||
+ gHistorySwipeAnimation._nextBox != null
+ ) {
+ await new Promise(r => requestAnimationFrame(r));
+ }
+
+ ok(
+ gHistorySwipeAnimation._prevBox == null &&
+ gHistorySwipeAnimation._nextBox == null
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ // Set the velocity-contribution to 0 so we can exactly control the
+ // values in the swipe tracker via the delta in the events that we send.
+ ["widget.swipe.success-velocity-contribution", 0.0],
+ ["widget.swipe.pixel-size", 550.0],
+ ],
+ });
+
+ function swipeGestureEndPromise() {
+ return new Promise(resolve => {
+ let promiseObserver = {
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "MozSwipeGestureEnd":
+ gBrowser.tabbox.removeEventListener(
+ "MozSwipeGestureEnd",
+ promiseObserver,
+ true
+ );
+ resolve();
+ break;
+ }
+ },
+ };
+ gBrowser.tabbox.addEventListener(
+ "MozSwipeGestureEnd",
+ promiseObserver,
+ true
+ );
+ });
+ }
+
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!gBrowser.webNavigation.canGoForward);
+
+ let numSwipeGestureEndEvents = 0;
+ var anObserver = {
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "MozSwipeGestureEnd":
+ numSwipeGestureEndEvents++;
+ break;
+ }
+ },
+ };
+
+ gBrowser.tabbox.addEventListener("MozSwipeGestureEnd", anObserver, true);
+
+ let gestureEndPromise = swipeGestureEndPromise();
+
+ is(
+ numSwipeGestureEndEvents,
+ 0,
+ "expected no MozSwipeGestureEnd got " + numSwipeGestureEndEvents
+ );
+
+ // Send a pan that starts a navigate back but doesn't have enough delta to do
+ // anything.
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 0.9);
+
+ await waitForWhile();
+ // Make sure any navigation didn't happen.
+ is(tab.linkedBrowser.currentURI.spec, secondPage);
+ // end event comes after a swipe that does not navigate
+ await gestureEndPromise;
+ is(
+ numSwipeGestureEndEvents,
+ 1,
+ "expected one MozSwipeGestureEnd got " + numSwipeGestureEndEvents
+ );
+
+ // Try to navigate backward.
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+
+ gestureEndPromise = swipeGestureEndPromise();
+
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 1);
+
+ // Make sure the gesture triggered going back to the previous page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ await gestureEndPromise;
+
+ is(
+ numSwipeGestureEndEvents,
+ 2,
+ "expected one MozSwipeGestureEnd got " + (numSwipeGestureEndEvents - 1)
+ );
+
+ gBrowser.tabbox.removeEventListener("MozSwipeGestureEnd", anObserver, true);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ // success-velocity-contribution is very high and pixel-size is
+ // very low so that one swipe goes over the threshold asap.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ ["widget.swipe.success-velocity-contribution", 999999.0],
+ ["widget.swipe.pixel-size", 1.0],
+ ],
+ });
+
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!gBrowser.webNavigation.canGoForward);
+
+ // Navigate backward.
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+
+ await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 100);
+
+ ok(gHistorySwipeAnimation._prevBox != null, "should have prevbox");
+ let transitionCancelPromise = new Promise(resolve => {
+ gHistorySwipeAnimation._prevBox.addEventListener(
+ "transitioncancel",
+ event => {
+ if (
+ event.propertyName == "opacity" &&
+ event.target == gHistorySwipeAnimation._prevBox
+ ) {
+ resolve();
+ }
+ },
+ { once: true }
+ );
+ });
+ let transitionStartPromise = new Promise(resolve => {
+ gHistorySwipeAnimation._prevBox.addEventListener(
+ "transitionstart",
+ event => {
+ if (
+ event.propertyName == "opacity" &&
+ event.target == gHistorySwipeAnimation._prevBox
+ ) {
+ resolve();
+ }
+ },
+ { once: true }
+ );
+ });
+
+ await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 100);
+ await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 100);
+
+ // Make sure the gesture triggered going back to the previous page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ await Promise.any([transitionStartPromise, transitionCancelPromise]);
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ gHistorySwipeAnimation._prevBox == null &&
+ gHistorySwipeAnimation._nextBox == null
+ );
+ });
+
+ // Navigate forward and check the forward navigation icon box state.
+ startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ secondPage
+ );
+ stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ secondPage
+ );
+
+ await panRightToLeftBegin(tab.linkedBrowser, 100, 100, 100);
+
+ ok(gHistorySwipeAnimation._nextBox != null, "should have nextbox");
+ transitionCancelPromise = new Promise(resolve => {
+ gHistorySwipeAnimation._nextBox.addEventListener(
+ "transitioncancel",
+ event => {
+ if (
+ event.propertyName == "opacity" &&
+ event.target == gHistorySwipeAnimation._nextBox
+ ) {
+ resolve();
+ }
+ }
+ );
+ });
+ transitionStartPromise = new Promise(resolve => {
+ gHistorySwipeAnimation._nextBox.addEventListener(
+ "transitionstart",
+ event => {
+ if (
+ event.propertyName == "opacity" &&
+ event.target == gHistorySwipeAnimation._nextBox
+ ) {
+ resolve();
+ }
+ }
+ );
+ });
+
+ await panRightToLeftUpdate(tab.linkedBrowser, 100, 100, 100);
+ await panRightToLeftEnd(tab.linkedBrowser, 100, 100, 100);
+
+ // Make sure the gesture triggered going forward to the next page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoBack);
+
+ await Promise.any([transitionStartPromise, transitionCancelPromise]);
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ gHistorySwipeAnimation._nextBox == null &&
+ gHistorySwipeAnimation._prevBox == null
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// A simple test case on RTL.
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ ["widget.swipe.success-velocity-contribution", 0.5],
+ ["intl.l10n.pseudo", "bidi"],
+ ],
+ });
+
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ newWin.gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(newWin.gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!newWin.gBrowser.webNavigation.canGoForward);
+
+ // Make sure that our gesture support stuff has been initialized in the new
+ // browser window.
+ await TestUtils.waitForCondition(() => {
+ return newWin.gHistorySwipeAnimation.active;
+ });
+
+ // Try to navigate backward.
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+ await panRightToLeft(tab.linkedBrowser, 100, 100, 1);
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(newWin.gBrowser.webNavigation.canGoForward);
+
+ // Now try to navigate forward again.
+ startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ secondPage
+ );
+ stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ secondPage
+ );
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 1);
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(newWin.gBrowser.webNavigation.canGoBack);
+
+ await BrowserTestUtils.closeWindow(newWin);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ ["widget.swipe.success-velocity-contribution", 0.5],
+ ["apz.overscroll.enabled", true],
+ ["apz.test.logging_enabled", true],
+ ],
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:about",
+ true /* waitForLoad */
+ );
+
+ const URL_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false /* includeSubFrames */,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // Set `overscroll-behavior-x: contain` and flush it.
+ content.document.documentElement.style.overscrollBehaviorX = "contain";
+ content.document.documentElement.getBoundingClientRect();
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ // Start a pan gesture but keep touching.
+ await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 2);
+
+ // Flush APZ pending requests to make sure the pan gesture has been processed.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const isOverscrolled = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ const scrollId = SpecialPowers.DOMWindowUtils.getViewId(
+ content.document.scrollingElement
+ );
+ const data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData();
+ return data.additionalData.some(entry => {
+ return (
+ entry.key == scrollId &&
+ entry.value.split(",").includes("overscrolled")
+ );
+ });
+ }
+ );
+
+ ok(isOverscrolled, "The root scroller should have overscrolled");
+
+ // Finish the pan gesture.
+ await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 2);
+ await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 2);
+
+ // And wait a while to give a chance to navigate.
+ await waitForWhile();
+
+ // Make sure any navigation didn't happen.
+ is(tab.linkedBrowser.currentURI.spec, URL_ROOT + "helper_swipe_gesture.html");
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// A test case to make sure the short circuit path for swipe-to-navigations in
+// APZ works, i.e. cases where we know for sure that the target APZC for a given
+// pan-start event isn't scrollable in the pan-start event direction.
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["apz.overscroll.enabled", true],
+ ],
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:about",
+ true /* waitForLoad */
+ );
+
+ const URL_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false /* includeSubFrames */,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+
+ // Make sure the content can allow both of overscrolling and
+ // swipe-to-navigations.
+ const overscrollBehaviorX = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return content.window.getComputedStyle(content.document.documentElement)
+ .overscrollBehaviorX;
+ }
+ );
+ is(overscrollBehaviorX, "auto");
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+
+ // Start a pan gesture but keep touching.
+ await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 2);
+
+ // The above pan event should invoke a SwipeGestureStart event immediately so
+ // that the swipe-to-navigation icon box should be uncollapsed to show it.
+ ok(!gHistorySwipeAnimation._prevBox.collapsed);
+
+ // Finish the pan gesture, i.e. sending a pan-end event, otherwise a new
+ // pan-start event in the next will also generate a pan-interrupt event which
+ // will break the test.
+ await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 2);
+ await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 2);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ ["widget.swipe.success-velocity-contribution", 0.5],
+ ["apz.overscroll.enabled", true],
+ ["apz.test.logging_enabled", true],
+ ],
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:about",
+ true /* waitForLoad */
+ );
+
+ const URL_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false /* includeSubFrames */,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+
+ // Start a pan gesture but keep touching.
+ await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 2);
+
+ // Flush APZ pending requests to make sure the pan gesture has been processed.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const isOverscrolled = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ const scrollId = SpecialPowers.DOMWindowUtils.getViewId(
+ content.document.scrollingElement
+ );
+ const data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData();
+ return data.additionalData.some(entry => {
+ return entry.key == scrollId && entry.value.includes("overscrolled");
+ });
+ }
+ );
+
+ ok(!isOverscrolled, "The root scroller should not have overscrolled");
+
+ await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 0);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ ["widget.swipe.success-velocity-contribution", 0.5],
+ ],
+ });
+
+ // Load three pages and go to the second page so that it can be navigated
+ // to both back and forward.
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:about",
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:mozilla");
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false /* includeSubFrames */,
+ "about:mozilla"
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:home");
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false /* includeSubFrames */,
+ "about:home"
+ );
+
+ gBrowser.goBack();
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false /* includeSubFrames */,
+ "about:mozilla"
+ );
+
+ // Make sure we can go back and go forward.
+ ok(gBrowser.webNavigation.canGoBack);
+ ok(gBrowser.webNavigation.canGoForward);
+
+ // Start a history back pan gesture but keep touching.
+ await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 1);
+
+ ok(
+ !gHistorySwipeAnimation._prevBox.collapsed,
+ "The icon box for the previous navigation should NOT be collapsed"
+ );
+ ok(
+ gHistorySwipeAnimation._nextBox.collapsed,
+ "The icon box for the next navigation should be collapsed"
+ );
+
+ // Pan back to the opposite direction so that the gesture should be cancelled.
+ // eslint-disable-next-line no-undef
+ await NativePanHandler.promiseNativePanEvent(
+ tab.linkedBrowser,
+ 100,
+ 100,
+ // eslint-disable-next-line no-undef
+ NativePanHandler.delta,
+ 0,
+ // eslint-disable-next-line no-undef
+ NativePanHandler.updatePhase
+ );
+
+ ok(
+ gHistorySwipeAnimation._prevBox.collapsed,
+ "The icon box for the previous navigation should be collapsed"
+ );
+ ok(
+ gHistorySwipeAnimation._nextBox.collapsed,
+ "The icon box for the next navigation should be collapsed"
+ );
+
+ await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 0);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ ["widget.swipe.success-velocity-contribution", 0.5],
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.damping", 5.0],
+ ["apz.content_response_timeout", 0],
+ ],
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:about",
+ true /* waitForLoad */
+ );
+
+ const URL_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+
+ // Load a horizontal scrollable content.
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false /* includeSubFrames */,
+ URL_ROOT + "helper_swipe_gesture.html"
+ );
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+
+ // Shift the horizontal scroll position slightly to make the content
+ // overscrollable.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.documentElement.scrollLeft = 1;
+ content.document.documentElement.getBoundingClientRect();
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ // Swipe horizontally to overscroll.
+ await panLeftToRight(tab.linkedBrowser, 1, 100, 1);
+
+ // Swipe again over the overscroll gutter.
+ await panLeftToRight(tab.linkedBrowser, 1, 100, 1);
+
+ // Wait the overscroll gutter is restored.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // For some reasons using functions in apz_test_native_event_utils.js
+ // sometimes causes "TypeError content.wrappedJSObject.XXXX is not a
+ // function" error, so we observe "APZ:TransformEnd" instead of using
+ // promiseTransformEnd().
+ await new Promise((resolve, reject) => {
+ SpecialPowers.Services.obs.addObserver(function observer(
+ subject,
+ topic,
+ data
+ ) {
+ try {
+ SpecialPowers.Services.obs.removeObserver(observer, topic);
+ resolve([subject, data]);
+ } catch (ex) {
+ SpecialPowers.Services.obs.removeObserver(observer, topic);
+ reject(ex);
+ }
+ },
+ "APZ:TransformEnd");
+ });
+ });
+
+ // Set up an APZ aware event listener and...
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.documentElement.addEventListener("wheel", e => {}, {
+ passive: false,
+ });
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ // Try to swipe back again without overscrolling to make sure swipe-navigation
+ // works with the APZ aware event listener.
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 1);
+
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ "about:about"
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ "about:about"
+ );
+
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// NOTE: This test listens wheel events so that it causes an overscroll issue
+// (bug 1800022). To avoid the bug, we need to run this test case at the end
+// of this file.
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
+ ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"],
+ ["widget.disable-swipe-tracker", false],
+ ["widget.swipe.velocity-twitch-tolerance", 0.0000001],
+ ["widget.swipe.success-velocity-contribution", 0.5],
+ ],
+ });
+
+ const firstPage = "about:about";
+ const secondPage = "about:mozilla";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ firstPage,
+ true /* waitForLoad */
+ );
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
+
+ // Make sure we can go back to the previous page.
+ ok(gBrowser.webNavigation.canGoBack);
+ // and we cannot go forward to the next page.
+ ok(!gBrowser.webNavigation.canGoForward);
+
+ let wheelEventCount = 0;
+ tab.linkedBrowser.addEventListener("wheel", () => {
+ wheelEventCount++;
+ });
+
+ // Try to navigate forward.
+ await panRightToLeft(tab.linkedBrowser, 100, 100, 1);
+ // NOTE: The last endPhase shouldn't fire a wheel event since
+ // its delta is zero.
+ is(wheelEventCount, 2, "Received 2 wheel events");
+
+ await waitForWhile();
+ // Make sure any navigation didn't happen.
+ is(tab.linkedBrowser.currentURI.spec, secondPage);
+
+ // Try to navigate backward.
+ wheelEventCount = 0;
+ let startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ firstPage
+ );
+ let stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ firstPage
+ );
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 1);
+ // NOTE: We only get a wheel event for the beginPhase, rest of events have
+ // been captured by the swipe gesture module.
+ is(wheelEventCount, 1, "Received a wheel event");
+
+ // Make sure the gesture triggered going back to the previous page.
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoForward);
+
+ // Now try to navigate forward again.
+ wheelEventCount = 0;
+ startLoadingPromise = BrowserTestUtils.browserStarted(
+ tab.linkedBrowser,
+ secondPage
+ );
+ stoppedLoadingPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ secondPage
+ );
+ await panRightToLeft(tab.linkedBrowser, 100, 100, 1);
+ is(wheelEventCount, 1, "Received a wheel event");
+
+ await Promise.all([startLoadingPromise, stoppedLoadingPromise]);
+
+ ok(gBrowser.webNavigation.canGoBack);
+
+ // Now try to navigate backward again but with preventDefault-ed event
+ // handler.
+ wheelEventCount = 0;
+ let wheelEventListener = event => {
+ event.preventDefault();
+ };
+ tab.linkedBrowser.addEventListener("wheel", wheelEventListener);
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 1);
+ is(wheelEventCount, 3, "Received all wheel events");
+
+ await waitForWhile();
+ // Make sure any navigation didn't happen.
+ is(tab.linkedBrowser.currentURI.spec, secondPage);
+
+ // Now drop the event handler and disable the swipe tracker and try to swipe
+ // again.
+ wheelEventCount = 0;
+ tab.linkedBrowser.removeEventListener("wheel", wheelEventListener);
+ await SpecialPowers.pushPrefEnv({
+ set: [["widget.disable-swipe-tracker", true]],
+ });
+
+ await panLeftToRight(tab.linkedBrowser, 100, 100, 1);
+ is(wheelEventCount, 3, "Received all wheel events");
+
+ await waitForWhile();
+ // Make sure any navigation didn't happen.
+ is(tab.linkedBrowser.currentURI.spec, secondPage);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/widget/tests/browser/file_ime_state_tests.html b/widget/tests/browser/file_ime_state_tests.html
new file mode 100644
index 0000000000..d6b63f1e52
--- /dev/null
+++ b/widget/tests/browser/file_ime_state_tests.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html style="ime-mode: disabled;">
+<head>
+<meta charset="utf-8">
+<script src="file_ime_state_test_helper.js"></script>
+<script src="file_test_ime_state_in_contenteditable_on_readonly_change.js"></script>
+<script src="file_test_ime_state_in_text_control_on_reframe.js"></script>
+<script src="file_test_ime_state_on_focus_move.js"></script>
+<script src="file_test_ime_state_on_input_type_change.js"></script>
+<script src="file_test_ime_state_on_readonly_change.js"></script>
+<script>
+"use strict";
+
+/* import-globals-from ../file_ime_state_test_helper.js */
+/* import-globals-from ../file_test_ime_state_in_contenteditable_on_readonly_change.js */
+/* import-globals-from ../file_test_ime_state_in_text_control_on_reframe.js */
+/* import-globals-from ../file_test_ime_state_on_focus_move.js */
+/* import-globals-from ../file_test_ime_state_on_input_type_change.js */
+/* import-globals-from ../file_test_ime_state_on_readonly_change.js */
+
+function createIMEStateInContentEditableOnReadonlyChangeTester() {
+ return new IMEStateInContentEditableOnReadonlyChangeTester();
+}
+function createIMEStateOfTextControlInContentEditableOnReadonlyChangeTester() {
+ return new IMEStateOfTextControlInContentEditableOnReadonlyChangeTester();
+}
+function createIMEStateOutsideContentEditableOnReadonlyChangeTester() {
+ return new IMEStateOutsideContentEditableOnReadonlyChangeTester();
+}
+function createIMEStateInTextControlOnReframeTester() {
+ return new IMEStateInTextControlOnReframeTester();
+}
+function createIMEStateWhenNoActiveElementTester(aDescription) {
+ return new IMEStateWhenNoActiveElementTester(aDescription);
+}
+function createIMEStateOnFocusMoveTester(aDescription, aIndex, aWindow = window) {
+ return new IMEStateOnFocusMoveTester(aDescription, aIndex, aWindow);
+}
+function createIMEStateOnInputTypeChangeTester(aSrcIndex) {
+ return new IMEStateOnInputTypeChangeTester(aSrcIndex);
+}
+function createIMEStateOnReadonlyChangeTester() {
+ return new IMEStateOnReadonlyChangeTester();
+}
+</script>
+</head>
+<body style="ime-mode: disabled;"><div style="ime-mode: disabled;"></div></body>
+</html>
diff --git a/widget/tests/browser/helper_scrollbar_colors.html b/widget/tests/browser/helper_scrollbar_colors.html
new file mode 100644
index 0000000000..e6001906e2
--- /dev/null
+++ b/widget/tests/browser/helper_scrollbar_colors.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<meta charset="UTF-8">
+<title>Test for scrollbar-*-color properties</title>
+<style>
+ .outer {
+ width: 100px;
+ height: 100px;
+ background: yellow;
+ overflow: scroll;
+ }
+ .inner {
+ width: 200px;
+ height: 200px;
+ }
+</style>
+<style id="style"></style>
+<div class="outer">
+ <div class="inner">
+ </div>
+</div>
+</html>
diff --git a/widget/tests/browser/helper_swipe_gesture.html b/widget/tests/browser/helper_swipe_gesture.html
new file mode 100644
index 0000000000..1fa79dbbf3
--- /dev/null
+++ b/widget/tests/browser/helper_swipe_gesture.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+<script src="/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script>
+<style>
+html {
+ overflow-x: scroll;
+}
+body {
+ margin: 0;
+}
+div {
+ height: 100vh;
+ width: 110vw;
+ background-color: blue;
+}
+</style>
+<div></div>
+</html>
diff --git a/widget/tests/bug586713_window.xhtml b/widget/tests/bug586713_window.xhtml
new file mode 100644
index 0000000000..c180c00235
--- /dev/null
+++ b/widget/tests/bug586713_window.xhtml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="bug586713_window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="300"
+ height="300"
+ onload="onLoad();"
+ title="Bug 586713 Test">
+
+ <menubar id="nativemenubar">
+ <menu id="foo" label="Foo">
+ <menupopup>
+ <menuitem label="FooItem0"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <script type="application/javascript"><![CDATA[
+ function ok(condition, message) {
+ window.arguments[0].SimpleTest.ok(condition, message);
+ }
+
+ function onTestsFinished() {
+ window.close();
+ window.arguments[0].SimpleTest.finish();
+ }
+
+ var fooCallCount = 0;
+ function foo() {
+ fooCallCount++;
+ let instruction = document.createProcessingInstruction("xml-stylesheet", 'href="chrome://foo.css" type="text/css"');
+ document.insertBefore(instruction, document.documentElement);
+ if (fooCallCount == 2) {
+ ok(true, "If we got here we didn't crash, excellent.");
+ onTestsFinished();
+ }
+ }
+
+ function onLoad() {
+ foo();
+ setTimeout(() => foo(), 0);
+ }
+ ]]></script>
+</window>
diff --git a/widget/tests/chrome.toml b/widget/tests/chrome.toml
new file mode 100644
index 0000000000..08d02d3c36
--- /dev/null
+++ b/widget/tests/chrome.toml
@@ -0,0 +1,200 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+support-files = [
+ "empty_window.xhtml",
+ "clipboard_helper.js",
+]
+
+["test_alwaysontop_focus.xhtml"]
+
+# Privacy relevant
+
+["test_bug1123480.xhtml"]
+skip-if = ["win11_2009 && bits == 32"]
+
+["test_bug343416.xhtml"]
+skip-if = ["debug"]
+
+["test_bug428405.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+
+["test_bug429954.xhtml"]
+support-files = ["window_bug429954.xhtml"]
+
+["test_bug444800.xhtml"]
+
+["test_bug466599.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+
+["test_bug478536.xhtml"]
+skip-if = ["true"] # Bug 561929
+support-files = ["window_bug478536.xhtml"]
+
+["test_bug485118.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+
+["test_bug517396.xhtml"]
+skip-if = ["verify && os == 'win'"]
+
+["test_bug522217.xhtml"]
+tags = "fullscreen"
+run-if = ["os == 'mac'"] # Cocoa widget test
+support-files = ["window_bug522217.xhtml"]
+
+["test_bug538242.xhtml"]
+support-files = ["window_bug538242.xhtml"]
+
+["test_bug565392.html"]
+run-if = ["os == 'win'"]
+
+["test_bug586713.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+support-files = ["bug586713_window.xhtml"]
+
+["test_bug593307.xhtml"]
+support-files = [
+ "window_bug593307_offscreen.xhtml",
+ "window_bug593307_centerscreen.xhtml",
+]
+
+["test_bug596600.xhtml"]
+support-files = "file_bug596600.html"
+run-if = ["os == 'mac'"] # Cocoa widget test
+
+["test_bug673301.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+
+["test_bug760802.xhtml"]
+
+["test_clipboard_chrome.html"]
+support-files = "file_test_clipboard.js"
+
+["test_clipboard_asyncGetData_chrome.html"]
+support-files = "file_test_clipboard_asyncGetData.js"
+
+["test_clipboard_asyncSetData_chrome.html"]
+support-files = "file_test_clipboard_asyncSetData.js"
+
+["test_clipboard_cache_chrome.html"]
+
+["test_clipboard_owner_chrome.html"]
+
+["test_composition_text_querycontent.xhtml"]
+support-files = ["window_composition_text_querycontent.xhtml"]
+
+["test_ime_state_in_contenteditable_on_readonly_change_in_parent.html"]
+support-files = [
+ "file_ime_state_test_helper.js",
+ "file_test_ime_state_in_contenteditable_on_readonly_change.js",
+]
+
+["test_ime_state_in_plugin_in_parent.html"]
+support-files = ["file_ime_state_test_helper.js"]
+
+["test_ime_state_in_text_control_on_reframe_in_parent.html"]
+support-files = [
+ "file_ime_state_test_helper.js",
+ "file_test_ime_state_in_text_control_on_reframe.js",
+]
+
+["test_ime_state_on_editable_state_change_in_parent.html"]
+support-files = ["file_ime_state_test_helper.js"]
+
+["test_ime_state_on_focus_move_in_parent.html"]
+support-files = [
+ "file_ime_state_test_helper.js",
+ "file_test_ime_state_on_focus_move.js",
+]
+
+["test_ime_state_on_input_type_change_in_parent.html"]
+skip-if = ["true"] # Bug 1817704
+support-files = [
+ "file_ime_state_test_helper.js",
+ "file_test_ime_state_on_input_type_change.js",
+]
+
+["test_ime_state_on_readonly_change_in_parent.html"]
+support-files = [
+ "file_ime_state_test_helper.js",
+ "file_test_ime_state_on_readonly_change.js",
+]
+
+["test_ime_state_others_in_parent.html"]
+support-files = ["window_imestate_iframes.html"]
+
+["test_input_events_on_deactive_window.xhtml"]
+support-files = ["file_input_events_on_deactive_window.html"]
+
+["test_key_event_counts.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+
+["test_keycodes.xhtml"]
+
+["test_mouse_scroll.xhtml"]
+run-if = ["os == 'win'"] # windows widget test
+support-files = [
+ "window_mouse_scroll_win.html",
+ "window_mouse_scroll_win_2.html",
+]
+
+["test_native_key_bindings_mac.html"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+skip-if = [
+ "verify",
+]
+
+["test_native_menus.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+support-files = ["native_menus_window.xhtml"]
+
+["test_panel_mouse_coords.xhtml"]
+skip-if = ["os == 'win'"] # bug 1009955
+
+["test_platform_colors.xhtml"]
+skip-if = ["true"] # Bug 1207190
+
+["test_position_on_resize.xhtml"]
+skip-if = [
+ "verify && os == 'win'",
+ "os == 'linux' && bits == 64", # Bug 1616760
+]
+
+["test_secure_input.html"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+support-files = ["file_secure_input.html"]
+
+["test_sizemode_events.xhtml"]
+
+["test_standalone_native_menu.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+support-files = ["standalone_native_menu_window.xhtml"]
+
+["test_surrogate_pair_native_key_handling.xhtml"]
+run-if = ["os == 'win'"] # Windows widget test
+
+["test_system_font_changes.xhtml"]
+support-files = ["system_font_changes.xhtml"]
+run-if = ["os == 'linux'"] # Currently the test works on only gtk3
+
+["test_system_status_bar.xhtml"]
+run-if = ["os == 'mac'"] # Cocoa widget test
+
+["test_taskbar_progress.xhtml"]
+skip-if = [
+ "os == 'linux'",
+ "os == 'android'",
+ "win10_2009 && !ccov", # Bug 1456811
+]
+
+["test_transferable_overflow.xhtml"]
+skip-if = [
+ "verify && apple_catalina",
+ "verify && os == 'linux'",
+]
+
+["test_wheeltransaction.xhtml"]
+support-files = ["window_wheeltransaction.xhtml"]
+
+# Windows
+# taskbar_previews.xhtml
+# window_state_windows.xhtml
diff --git a/widget/tests/clipboard_helper.js b/widget/tests/clipboard_helper.js
new file mode 100644
index 0000000000..96787468fb
--- /dev/null
+++ b/widget/tests/clipboard_helper.js
@@ -0,0 +1,230 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Cr = SpecialPowers.Cr;
+const clipboard = SpecialPowers.Services.clipboard;
+const clipboardTypes = [
+ clipboard.kGlobalClipboard,
+ clipboard.kSelectionClipboard,
+ clipboard.kFindClipboard,
+ clipboard.kSelectionCache,
+];
+
+function emptyClipboardData(aType) {
+ // XXX gtk doesn't support emptying clipboard data which is stored from
+ // other application (bug 1853884). As a workaround, we set dummy data
+ // to the clipboard first to ensure the subsequent emptyClipboard call
+ // works.
+ if (navigator.platform.includes("Linux")) {
+ writeStringToClipboard("foo", "text/plain", aType);
+ }
+
+ clipboard.emptyClipboard(aType);
+}
+
+function cleanupAllClipboard() {
+ clipboardTypes.forEach(function (type) {
+ if (clipboard.isClipboardTypeSupported(type)) {
+ info(`cleanup clipboard ${type}`);
+ emptyClipboardData(type);
+ }
+ });
+}
+
+function generateRandomString() {
+ return "random number: " + Math.random();
+}
+
+function generateNewTransferable(aFlavor, aStr) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor(aFlavor);
+
+ let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ supportsStr.data = aStr;
+ trans.setTransferData(aFlavor, supportsStr);
+
+ return trans;
+}
+
+function addStringToTransferable(aFlavor, aStr, aTrans) {
+ aTrans.addDataFlavor(aFlavor);
+
+ let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ supportsStr.data = aStr;
+ aTrans.setTransferData(aFlavor, supportsStr);
+}
+
+function updateStringToTransferable(aFlavor, aStr, aTrans) {
+ let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ supportsStr.data = aStr;
+ aTrans.setTransferData(aFlavor, supportsStr);
+}
+
+function writeStringToClipboard(
+ aStr,
+ aFlavor,
+ aClipboardType,
+ aClipboardOwner = null,
+ aAsync = false
+) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor(aFlavor);
+
+ let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ supportsStr.data = aStr;
+ trans.setTransferData(aFlavor, supportsStr);
+
+ if (aAsync) {
+ let request = clipboard.asyncSetData(aClipboardType);
+ request.setData(trans, aClipboardOwner);
+ return;
+ }
+
+ clipboard.setData(trans, aClipboardOwner, aClipboardType);
+ // XXX gtk doesn't support get empty text data from clipboard, bug 1852983.
+ if (aStr == "" && navigator.platform.includes("Linux")) {
+ todo_is(
+ getClipboardData(aFlavor, aClipboardType),
+ "",
+ `Should get empty string on clipboard type ${aClipboardType}`
+ );
+ } else {
+ is(
+ getClipboardData(aFlavor, aClipboardType),
+ // On Windows, widget adds extra data into HTML clipboard.
+ aFlavor == "text/html" && navigator.platform.includes("Win")
+ ? `<html><body>\n<!--StartFragment-->${aStr}<!--EndFragment-->\n</body>\n</html>`
+ : aStr,
+ "ensure clipboard data is set"
+ );
+ }
+}
+
+function writeRandomStringToClipboard(
+ aFlavor,
+ aClipboardType,
+ aClipboardOwner = null,
+ aAsync = false
+) {
+ let randomString = generateRandomString();
+ writeStringToClipboard(
+ randomString,
+ aFlavor,
+ aClipboardType,
+ aClipboardOwner,
+ aAsync
+ );
+ return randomString;
+}
+
+function getClipboardData(aFlavor, aClipboardType) {
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor(aFlavor);
+ clipboard.getData(
+ trans,
+ aClipboardType,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+
+ try {
+ var data = SpecialPowers.createBlankObject();
+ trans.getTransferData(aFlavor, data);
+ return data.value.QueryInterface(SpecialPowers.Ci.nsISupportsString).data;
+ } catch (ex) {
+ // If the clipboard is empty getTransferData will throw.
+ return null;
+ }
+}
+
+function asyncGetClipboardData(aClipboardType) {
+ return new Promise((resolve, reject) => {
+ try {
+ clipboard.asyncGetData(
+ ["text/plain", "text/html", "image/png"],
+ aClipboardType,
+ null,
+ SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
+ {
+ QueryInterface: SpecialPowers.ChromeUtils.generateQI([
+ "nsIAsyncClipboardGetCallback",
+ ]),
+ // nsIAsyncClipboardGetCallback
+ onSuccess: SpecialPowers.wrapCallback(function (
+ aAsyncGetClipboardData
+ ) {
+ resolve(aAsyncGetClipboardData);
+ }),
+ onError: SpecialPowers.wrapCallback(function (aResult) {
+ reject(aResult);
+ }),
+ }
+ );
+ } catch (e) {
+ ok(false, `asyncGetData should not throw`);
+ reject(e);
+ }
+ });
+}
+
+function asyncClipboardRequestGetData(aRequest, aFlavor, aThrows = false) {
+ return new Promise((resolve, reject) => {
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor(aFlavor);
+ try {
+ aRequest.getData(trans, aResult => {
+ if (aResult != Cr.NS_OK) {
+ reject(aResult);
+ return;
+ }
+
+ try {
+ var data = SpecialPowers.createBlankObject();
+ trans.getTransferData(aFlavor, data);
+ resolve(data.value.QueryInterface(Ci.nsISupportsString).data);
+ } catch (ex) {
+ // XXX: should widget set empty string to transferable when there no
+ // data in system clipboard?
+ resolve("");
+ }
+ });
+ ok(
+ !aThrows,
+ `nsIAsyncGetClipboardData.getData should ${
+ aThrows ? "throw" : "success"
+ }`
+ );
+ } catch (e) {
+ ok(
+ aThrows,
+ `nsIAsyncGetClipboardData.getData should ${
+ aThrows ? "throw" : "success"
+ }`
+ );
+ reject(e);
+ }
+ });
+}
diff --git a/widget/tests/empty_window.xhtml b/widget/tests/empty_window.xhtml
new file mode 100644
index 0000000000..f0e01761d2
--- /dev/null
+++ b/widget/tests/empty_window.xhtml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Empty window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
diff --git a/widget/tests/file_bug596600.html b/widget/tests/file_bug596600.html
new file mode 100644
index 0000000000..1b178a6b68
--- /dev/null
+++ b/widget/tests/file_bug596600.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<body>
+Content page
+</body>
diff --git a/widget/tests/file_ime_state_test_helper.js b/widget/tests/file_ime_state_test_helper.js
new file mode 100644
index 0000000000..0cee5c036f
--- /dev/null
+++ b/widget/tests/file_ime_state_test_helper.js
@@ -0,0 +1,197 @@
+"use strict";
+
+function IsIMEOpenStateSupported() {
+ // We support to control IME open state on Windows and Mac actually. However,
+ // we cannot test it on Mac if the current keyboard layout is not CJK. And also
+ // we cannot test it on Win32 if the system didn't be installed IME. So,
+ // currently we should not run the open state testing.
+ return false;
+}
+
+/**
+ * @param {Node} aNode
+ */
+function nodeIsInShadowDOM(aNode) {
+ for (let node = aNode; node; node = node.parentNode) {
+ if (node instanceof ShadowRoot) {
+ return true;
+ }
+ if (node == node.parentNode) {
+ break;
+ }
+ }
+ return false;
+}
+
+/**
+ * @param {Node} aNode
+ */
+function nodeIsInDesignMode(aNode) {
+ return (
+ aNode.isConnected &&
+ !nodeIsInShadowDOM(aNode) &&
+ aNode.ownerDocument.designMode == "on"
+ );
+}
+
+/**
+ * param {Node} aNode
+ */
+function getEditingHost(aNode) {
+ if (nodeIsInDesignMode(aNode)) {
+ return aNode.ownerDocument.documentElement;
+ }
+ for (
+ let element =
+ aNode.nodeType == Node.ELEMENT_NODE ? aNode : aNode.parentElement;
+ element;
+ element = element.parentElement
+ ) {
+ const contenteditable = element.getAttribute("contenteditable");
+ if (contenteditable === "true" || contenteditable === "") {
+ return element;
+ }
+ if (contenteditable === "false") {
+ return null;
+ }
+ }
+ return null;
+}
+
+/**
+ * @param {Node} aNode
+ */
+function nodeIsEditable(aNode) {
+ if (nodeIsInDesignMode(aNode)) {
+ return true;
+ }
+ if (!aNode.isConnected) {
+ return false;
+ }
+ return getEditingHost(aNode) != null;
+}
+
+/**
+ * @param {Element} aElement
+ */
+function elementIsEditingHost(aElement) {
+ return (
+ nodeIsEditable(aElement) &&
+ (!aElement.parentElement || !getEditingHost(aElement) == aElement)
+ );
+}
+
+/**
+ * @returns {Element} Retrieve focused element. If focused element is a element
+ * in UA widget, this returns its host element. E.g., when
+ * a button in the controls of <audio> or <video> has focus,
+ * this returns the <video> or <audio>.
+ */
+function getFocusedElementOrUAWidgetHost() {
+ const focusedElement = SpecialPowers.focusManager.focusedElement;
+ if (SpecialPowers.wrap(focusedElement)?.containingShadowRoot?.isUAWidget()) {
+ return focusedElement.containingShadowRoot.host;
+ }
+ return focusedElement;
+}
+
+class TIPWrapper {
+ #mTIP = null;
+ #mFocusBlurNotifications = [];
+ #mFocusBlurListener;
+ #mWindow;
+
+ constructor(aWindow) {
+ this.#mWindow = aWindow;
+ this.#mTIP = Cc["@mozilla.org/text-input-processor;1"].createInstance(
+ Ci.nsITextInputProcessor
+ );
+ if (!this.beginInputTransactionForTests()) {
+ this.#mTIP = null;
+ }
+ }
+
+ beginInputTransactionForTests() {
+ return this.#mTIP.beginInputTransactionForTests(
+ this.#mWindow,
+ this.#observer.bind(this)
+ );
+ }
+
+ typeA() {
+ const AKey = new this.#mWindow.KeyboardEvent("", {
+ key: "a",
+ code: "KeyA",
+ keyCode: this.#mWindow.KeyboardEvent.DOM_VK_A,
+ });
+ this.#mTIP.keydown(AKey);
+ this.#mTIP.keyup(AKey);
+ }
+
+ isAvailable() {
+ return this.#mTIP != null;
+ }
+
+ #observer(aTIP, aNotification) {
+ if (aTIP != this.#mTIP) {
+ return false;
+ }
+ switch (aNotification.type) {
+ case "request-to-commit":
+ this.#mTIP.commitComposition();
+ break;
+ case "request-to-cancel":
+ this.#mTIP.cancelComposition();
+ break;
+ case "notify-focus":
+ case "notify-blur":
+ this.#mFocusBlurNotifications.push(aNotification.type);
+ if (this.#mFocusBlurListener) {
+ this.#mFocusBlurListener(aNotification.type);
+ }
+ break;
+ }
+ return true;
+ }
+
+ get TIP() {
+ return this.#mTIP;
+ }
+
+ /**
+ * @param {Function} aListener
+ */
+ set onIMEFocusBlur(aListener) {
+ this.#mFocusBlurListener = aListener;
+ }
+
+ get focusBlurNotifications() {
+ return this.#mFocusBlurNotifications.concat();
+ }
+
+ get numberOfFocusNotifications() {
+ return this.#mFocusBlurNotifications.filter(t => t == "notify-focus")
+ .length;
+ }
+ get numberOfBlurNotifications() {
+ return this.#mFocusBlurNotifications.filter(t => t == "notify-blur").length;
+ }
+
+ get IMEHasFocus() {
+ return (
+ !!this.#mFocusBlurNotifications.length &&
+ this.#mFocusBlurNotifications[this.#mFocusBlurNotifications.length - 1] ==
+ "notify-focus"
+ );
+ }
+
+ clearFocusBlurNotifications() {
+ this.#mFocusBlurNotifications = [];
+ }
+
+ destroy() {
+ this.#mTIP = null;
+ this.#mFocusBlurListener = null;
+ this.#mFocusBlurNotifications = [];
+ }
+}
diff --git a/widget/tests/file_input_events_on_deactive_window.html b/widget/tests/file_input_events_on_deactive_window.html
new file mode 100644
index 0000000000..e733adbb9d
--- /dev/null
+++ b/widget/tests/file_input_events_on_deactive_window.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+ this is an active window.
+</body>
+</html>
diff --git a/widget/tests/file_secure_input.html b/widget/tests/file_secure_input.html
new file mode 100644
index 0000000000..28fec7b44b
--- /dev/null
+++ b/widget/tests/file_secure_input.html
@@ -0,0 +1 @@
+<input id="text" type"text"><input id="password" type"password">
diff --git a/widget/tests/file_test_clipboard.js b/widget/tests/file_test_clipboard.js
new file mode 100644
index 0000000000..76bdbaa84d
--- /dev/null
+++ b/widget/tests/file_test_clipboard.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from clipboard_helper.js */
+
+"use strict";
+
+function getLoadContext() {
+ return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+// Get clipboard data to paste.
+function paste(clipboard) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/plain");
+ clipboard.getData(
+ trans,
+ Ci.nsIClipboard.kGlobalClipboard,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+ let str = SpecialPowers.createBlankObject();
+ try {
+ trans.getTransferData("text/plain", str);
+ } catch (e) {
+ str = "";
+ }
+ if (str) {
+ str = str.value.QueryInterface(Ci.nsISupportsString);
+ if (str) {
+ str = str.data;
+ }
+ }
+ return str;
+}
+
+add_setup(function init() {
+ cleanupAllClipboard();
+});
+
+/* Test for bug 948065 */
+add_task(function test_copy() {
+ // Test copy.
+ const data = "random number: " + Math.random();
+ let helper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ helper.copyString(data);
+ is(paste(clipboard), data, "Data was successfully copied.");
+
+ clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
+ is(paste(clipboard), "", "Data was successfully cleared.");
+
+ cleanupAllClipboard();
+});
+
+/* Tests for bug 1834073 */
+clipboardTypes.forEach(function (clipboardType) {
+ if (clipboard.isClipboardTypeSupported(clipboardType)) {
+ add_task(function test_clipboard_apis() {
+ info(`Test clipboard apis for type ${clipboardType}`);
+
+ // Set clipboard data
+ let str;
+ try {
+ str = writeRandomStringToClipboard("text/plain", clipboardType);
+ } catch (e) {
+ ok(
+ false,
+ `setData should not throw error for clipboard type ${clipboardType}`
+ );
+ }
+
+ // Test hasDataMatchingFlavors
+ try {
+ ok(
+ clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType),
+ `Test hasDataMatchingFlavors for clipboard type ${clipboardType}`
+ );
+ } catch (e) {
+ ok(
+ false,
+ `hasDataMatchingFlavors should not throw error for clipboard type ${clipboardType}`
+ );
+ }
+
+ // Test getData
+ try {
+ is(
+ getClipboardData("text/plain", clipboardType),
+ str,
+ `Test getData for clipboard type ${clipboardType}`
+ );
+ } catch (e) {
+ ok(
+ false,
+ `getData should not throw error for clipboard type ${clipboardType}`
+ );
+ }
+ });
+
+ add_task(function test_clipboard_set_empty_string() {
+ info(`Test setting empty string to type ${clipboardType}`);
+
+ // Clear clipboard type.
+ clipboard.emptyClipboard(clipboardType);
+ is(
+ getClipboardData("text/plain", clipboardType),
+ null,
+ `Should get null data on clipboard type ${clipboardType}`
+ );
+ ok(
+ !clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType),
+ `Should not have text/plain flavor on clipboard ${clipboardType}`
+ );
+
+ // Set text/plain to empty string.
+ writeStringToClipboard("", "text/plain", clipboardType);
+ // XXX gtk doesn't support get empty text data from clipboard, bug 1852983.
+ if (navigator.platform.includes("Linux")) {
+ todo_is(
+ getClipboardData("text/plain", clipboardType),
+ "",
+ `Should get empty string on clipboard type ${clipboardType}`
+ );
+ } else {
+ is(
+ getClipboardData("text/plain", clipboardType),
+ "",
+ `Should get empty string on clipboard type ${clipboardType}`
+ );
+ }
+ // XXX android doesn't support setting empty text data to clipboard, bug 1841058.
+ if (navigator.userAgent.includes("Android")) {
+ todo_is(
+ clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType),
+ true,
+ `Should have text/plain flavor on clipboard ${clipboardType}`
+ );
+ } else {
+ ok(
+ clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType),
+ `Should have text/plain flavor on clipboard ${clipboardType}`
+ );
+ }
+
+ // Clear all clipboard data.
+ cleanupAllClipboard();
+ });
+ }
+});
diff --git a/widget/tests/file_test_clipboard_asyncGetData.js b/widget/tests/file_test_clipboard_asyncGetData.js
new file mode 100644
index 0000000000..e7f2e5f0f1
--- /dev/null
+++ b/widget/tests/file_test_clipboard_asyncGetData.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from clipboard_helper.js */
+
+"use strict";
+
+clipboardTypes.forEach(function (type) {
+ if (!clipboard.isClipboardTypeSupported(type)) {
+ add_task(async function test_clipboard_asyncGetData_not_support() {
+ info(`Test asyncGetData request throwing on ${type}`);
+ SimpleTest.doesThrow(
+ () => clipboard.asyncGetData(["text/plain"], type, {}),
+ "Passing unsupported clipboard type should throw"
+ );
+ });
+ return;
+ }
+
+ add_task(async function test_clipboard_asyncGetData_throw() {
+ info(`Test asyncGetData request throwing on ${type}`);
+ SimpleTest.doesThrow(
+ () => clipboard.asyncGetData([], type, {}),
+ "Passing empty flavor list should throw"
+ );
+
+ SimpleTest.doesThrow(
+ () => clipboard.asyncGetData(["text/plain"], type, null),
+ "Passing no callback should throw"
+ );
+ });
+
+ add_task(async function test_clipboard_asyncGetData_no_matched_flavor() {
+ info(`Test asyncGetData have no matched flavor on ${type}`);
+ cleanupAllClipboard();
+ is(getClipboardData("text/plain", type), null, "ensure clipboard is empty");
+
+ writeRandomStringToClipboard("text/plain", type);
+ let request = await new Promise(resolve => {
+ clipboard.asyncGetData(
+ ["text/html"],
+ type,
+ null,
+ SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
+ {
+ QueryInterface: SpecialPowers.ChromeUtils.generateQI([
+ "nsIAsyncClipboardGetCallback",
+ ]),
+ // nsIAsyncClipboardGetCallback
+ onSuccess: SpecialPowers.wrapCallback(function (
+ aAsyncGetClipboardData
+ ) {
+ resolve(aAsyncGetClipboardData);
+ }),
+ }
+ );
+ });
+ isDeeply(request.flavorList, [], "Check flavorList");
+ });
+
+ add_task(async function test_empty_data() {
+ info(`Test asyncGetData request with empty data on ${type}`);
+ cleanupAllClipboard();
+ is(getClipboardData("text/plain", type), null, "ensure clipboard is empty");
+
+ let request = await asyncGetClipboardData(type);
+ isDeeply(request.flavorList, [], "Check flavorList");
+ await asyncClipboardRequestGetData(request, "text/plain", true).catch(
+ () => {}
+ );
+ });
+
+ add_task(async function test_clipboard_asyncGetData_after_write() {
+ info(`Test asyncGetData request after write on ${type}`);
+
+ let str = writeRandomStringToClipboard("text/plain", type);
+ let request = await asyncGetClipboardData(type);
+ isDeeply(request.flavorList, ["text/plain"], "Check flavorList");
+ is(
+ await asyncClipboardRequestGetData(request, "text/plain"),
+ str,
+ "Check data"
+ );
+ ok(request.valid, "request should still be valid");
+ // Requesting a flavor that is not in the list should throw error.
+ await asyncClipboardRequestGetData(request, "text/html", true).catch(
+ () => {}
+ );
+ ok(request.valid, "request should still be valid");
+
+ // Writing a new data should invalid existing get request.
+ str = writeRandomStringToClipboard("text/plain", type);
+ await asyncClipboardRequestGetData(request, "text/plain").then(
+ () => {
+ ok(false, "asyncClipboardRequestGetData should not success");
+ },
+ e => {
+ ok(true, "asyncClipboardRequestGetData should reject");
+ }
+ );
+ ok(!request.valid, "request should no longer be valid");
+
+ info(`check clipboard data again`);
+ request = await asyncGetClipboardData(type);
+ isDeeply(request.flavorList, ["text/plain"], "Check flavorList");
+ is(
+ await asyncClipboardRequestGetData(request, "text/plain"),
+ str,
+ "Check data"
+ );
+
+ cleanupAllClipboard();
+ });
+
+ add_task(async function test_clipboard_asyncGetData_after_empty() {
+ info(`Test asyncGetData request after empty on ${type}`);
+
+ let str = writeRandomStringToClipboard("text/plain", type);
+ let request = await asyncGetClipboardData(type);
+ isDeeply(request.flavorList, ["text/plain"], "Check flavorList");
+ is(
+ await asyncClipboardRequestGetData(request, "text/plain"),
+ str,
+ "Check data"
+ );
+ ok(request.valid, "request should still be valid");
+
+ // Empty clipboard data
+ emptyClipboardData(type);
+ is(getClipboardData("text/plain", type), null, "ensure clipboard is empty");
+
+ await asyncClipboardRequestGetData(request, "text/plain").then(
+ () => {
+ ok(false, "asyncClipboardRequestGetData should not success");
+ },
+ e => {
+ ok(true, "asyncClipboardRequestGetData should reject");
+ }
+ );
+ ok(!request.valid, "request should no longer be valid");
+
+ info(`check clipboard data again`);
+ request = await asyncGetClipboardData(type);
+ isDeeply(request.flavorList, [], "Check flavorList");
+
+ cleanupAllClipboard();
+ });
+});
+
+add_task(async function test_html_data() {
+ info(`Test asyncGetData request with html data`);
+
+ const html_str = `<img src="https://example.com/oops">`;
+ writeStringToClipboard(html_str, "text/html", clipboard.kGlobalClipboard);
+
+ let request = await asyncGetClipboardData(clipboard.kGlobalClipboard);
+ isDeeply(request.flavorList, ["text/html"], "Check flavorList");
+ is(
+ await asyncClipboardRequestGetData(request, "text/html"),
+ // On Windows, widget adds extra data into HTML clipboard.
+ navigator.platform.includes("Win")
+ ? `<html><body>\n<!--StartFragment-->${html_str}<!--EndFragment-->\n</body>\n</html>`
+ : html_str,
+ "Check data"
+ );
+ // Requesting a flavor that is not in the list should throw error.
+ await asyncClipboardRequestGetData(request, "text/plain", true).catch(
+ () => {}
+ );
+});
diff --git a/widget/tests/file_test_clipboard_asyncSetData.js b/widget/tests/file_test_clipboard_asyncSetData.js
new file mode 100644
index 0000000000..cceecd2c44
--- /dev/null
+++ b/widget/tests/file_test_clipboard_asyncSetData.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from clipboard_helper.js */
+
+"use strict";
+
+clipboardTypes.forEach(function (type) {
+ if (clipboard.isClipboardTypeSupported(type)) {
+ clipboardTypes.forEach(function (otherType) {
+ if (clipboard.isClipboardTypeSupported(otherType)) {
+ [true, false].forEach(async function (async) {
+ add_task(async function test_clipboard_pending_asyncSetData() {
+ info(
+ `Test having a pending asyncSetData request on ${type} and then make a new ${
+ async ? "asyncSetData" : "setData"
+ } request on ${otherType}`
+ );
+
+ // Create a pending asyncSetData request
+ let priorResult;
+ let priorRequest;
+ let priorPromise = new Promise(resolve => {
+ priorRequest = clipboard.asyncSetData(type, {
+ QueryInterface: SpecialPowers.ChromeUtils.generateQI([
+ "nsIAsyncSetClipboardDataCallback",
+ ]),
+ onComplete(rv) {
+ priorResult = rv;
+ resolve();
+ },
+ });
+ });
+
+ // Create a new request
+ let str = writeRandomStringToClipboard(
+ "text/plain",
+ otherType,
+ null,
+ async
+ );
+
+ if (type === otherType) {
+ info(
+ "The new request to the same clipboard type should cancel the prior pending request"
+ );
+ await priorPromise;
+
+ is(
+ priorResult,
+ Cr.NS_ERROR_ABORT,
+ "The pending asyncSetData request should be canceled"
+ );
+ try {
+ priorRequest.setData(
+ generateNewTransferable("text/plain", generateRandomString())
+ );
+ ok(
+ false,
+ "An error should be thrown if setData is called on a canceled clipboard request"
+ );
+ } catch (e) {
+ is(
+ e.result,
+ Cr.NS_ERROR_FAILURE,
+ "An error should be thrown if setData is called on a canceled clipboard request"
+ );
+ }
+ } else {
+ info(
+ "The new request to the different clipboard type should not cancel the prior pending request"
+ );
+ str = generateRandomString();
+ priorRequest.setData(
+ generateNewTransferable("text/plain", str),
+ null
+ );
+ await priorPromise;
+
+ is(
+ priorResult,
+ Cr.NS_OK,
+ "The pending asyncSetData request should success"
+ );
+
+ try {
+ priorRequest.setData(
+ generateNewTransferable("text/plain", generateRandomString())
+ );
+ ok(
+ false,
+ "Calling setData multiple times should throw an error"
+ );
+ } catch (e) {
+ is(
+ e.result,
+ Cr.NS_ERROR_FAILURE,
+ "Calling setData multiple times should throw an error"
+ );
+ }
+ }
+
+ // Test clipboard data.
+ is(
+ getClipboardData("text/plain", type),
+ str,
+ `Test clipboard data for type ${type}`
+ );
+
+ // Clean clipboard data.
+ cleanupAllClipboard();
+ });
+ });
+ }
+ });
+
+ add_task(async function test_clipboard_asyncSetData_abort() {
+ info(`Test abort asyncSetData request on ${type}`);
+
+ // Create a pending asyncSetData request
+ let result;
+ let request = clipboard.asyncSetData(type, rv => {
+ result = rv;
+ });
+
+ // Abort with NS_OK.
+ try {
+ request.abort(Cr.NS_OK);
+ ok(false, "Throw an error when attempting to abort with NS_OK");
+ } catch (e) {
+ is(
+ e.result,
+ Cr.NS_ERROR_FAILURE,
+ "Should throw an error when attempting to abort with NS_OK"
+ );
+ }
+ is(result, undefined, "The asyncSetData request should not be canceled");
+
+ // Abort with NS_ERROR_ABORT.
+ request.abort(Cr.NS_ERROR_ABORT);
+ is(
+ result,
+ Cr.NS_ERROR_ABORT,
+ "The asyncSetData request should be canceled"
+ );
+ try {
+ request.abort(Cr.NS_ERROR_FAILURE);
+ ok(false, "Throw an error when attempting to abort again");
+ } catch (e) {
+ is(
+ e.result,
+ Cr.NS_ERROR_FAILURE,
+ "Should throw an error when attempting to abort again"
+ );
+ }
+ is(
+ result,
+ Cr.NS_ERROR_ABORT,
+ "The callback should not be notified again"
+ );
+
+ try {
+ request.setData(
+ generateNewTransferable("text/plain", generateRandomString())
+ );
+ ok(
+ false,
+ "An error should be thrown if setData is called on a canceled clipboard request"
+ );
+ } catch (e) {
+ is(
+ e.result,
+ Cr.NS_ERROR_FAILURE,
+ "An error should be thrown if setData is called on a canceled clipboard request"
+ );
+ }
+ });
+ }
+});
diff --git a/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js b/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js
new file mode 100644
index 0000000000..9f1ab2d305
--- /dev/null
+++ b/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js
@@ -0,0 +1,616 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+
+class IMEStateInContentEditableOnReadonlyChangeTester {
+ // Runner only fields.
+ #mEditingHost;
+ #mFocusElement;
+ #mWindow;
+
+ // Tester only fields.
+ #mTIPWrapper;
+ #mWindowUtils;
+
+ clear() {
+ this.#mTIPWrapper?.clearFocusBlurNotifications();
+ this.#mTIPWrapper = null;
+ }
+
+ #flushPendingIMENotifications() {
+ return new Promise(resolve =>
+ this.#mWindow.requestAnimationFrame(() =>
+ this.#mWindow.requestAnimationFrame(resolve)
+ )
+ );
+ }
+
+ #getExpectedIMEState() {
+ // Although if this.#mFocusElement is a <button>, its `.focus()` call
+ // focus it, but caret is not set into it and following typing is handled
+ // outside the <button>. Therefore, anyway the enabled state should be
+ // "enabled".
+ return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+ }
+
+ /**
+ * @param {Element} aEditingHost The editing host.
+ * @param {Element} aFocusElement Element which should have focus. This must
+ * be an inclusive descendant of the editing host and editable element.
+ * @param {Window} aWindow [optional] The window.
+ * @returns {object} Expected result of initial state.
+ */
+ async prepareToRun(aEditingHost, aFocusElement, aWindow = window) {
+ this.#mWindow = aWindow;
+ this.#mEditingHost = aEditingHost;
+ this.#mFocusElement = aFocusElement;
+
+ if (this.#mEditingHost.ownerDocument.activeElement) {
+ this.#mEditingHost.ownerDocument.activeElement.blur();
+ await this.#flushPendingIMENotifications();
+ }
+
+ this.#mWindow.focus();
+ this.#mEditingHost.setAttribute("contenteditable", "");
+ this.#mFocusElement.focus();
+
+ await this.#flushPendingIMENotifications();
+
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when initialized with setting focus to ${
+ this.#mFocusElement == this.#mEditingHost
+ ? "the editing host"
+ : `<${this.#mFocusElement.tagName.toLowerCase()}>`
+ }`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result of the test.
+ */
+ #checkResult(aExpectedResult) {
+ const description = `IMEStateInContentEditableOnReadonlyChangeTester`;
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedResult.expectedIMEState,
+ `${description}: IME enabled state should be expected one ${aExpectedResult.description}`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedResult.expectedIMEFocus,
+ `${description}: IME should ${
+ aExpectedResult.expectedIMEFocus ? "" : "not "
+ }have focus ${aExpectedResult.description}`
+ );
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result of prepareToRun().
+ * @param {Window} aWindow The window to check IME state.
+ * @param {TIPWrapper} aTIPWrapper The TIPWrapper for aWindow.
+ */
+ checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) {
+ this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
+ this.#mTIPWrapper = aTIPWrapper;
+ this.#checkResult(aExpectedResult);
+ }
+
+ /**
+ * @returns {object} The expected result.
+ */
+ async runToMakeHTMLEditorReadonly() {
+ const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
+ htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+
+ await this.#flushPendingIMENotifications();
+
+ return {
+ description:
+ this.#mFocusElement == this.#mEditingHost
+ ? "when the editing host has focus"
+ : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`,
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ expectedIMEFocus: false,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorReadonly().
+ */
+ checkResultOfMakingHTMLEditorReadonly(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ /**
+ * @returns {object} The expected result.
+ */
+ async runToMakeHTMLEditorEditable() {
+ const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
+ htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+
+ await this.#flushPendingIMENotifications();
+
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description:
+ this.#mFocusElement == this.#mEditingHost
+ ? "when the editing host has focus"
+ : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorEditable().
+ */
+ checkResultOfMakingHTMLEditorEditable(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToRemoveContentEditableAttribute() {
+ this.#mEditingHost.removeAttribute("contenteditable");
+
+ await this.#flushPendingIMENotifications();
+
+ return {
+ description:
+ this.#mFocusElement == this.#mEditingHost
+ ? "after removing contenteditable attribute when the editing host has focus"
+ : `after removing contenteditable attribute when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`,
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ expectedIMEFocus: false,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result of runToRemoveContentEditableAttribute().
+ */
+ checkResultOfRemovingContentEditableAttribute(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+}
+
+class IMEStateOfTextControlInContentEditableOnReadonlyChangeTester {
+ static #sTextControls = [
+ {
+ tag: "input",
+ type: "text",
+ readonly: false,
+ },
+ {
+ tag: "input",
+ type: "text",
+ readonly: true,
+ },
+ {
+ tag: "textarea",
+ readonly: false,
+ },
+ {
+ tag: "textarea",
+ readonly: true,
+ },
+ ];
+
+ static get numberOfTextControlTypes() {
+ return IMEStateOfTextControlInContentEditableOnReadonlyChangeTester
+ .#sTextControls.length;
+ }
+
+ static #createElement(aDocument, aTextControl) {
+ const textControl = aDocument.createElement(aTextControl.tag);
+ if (aTextControl.type !== undefined) {
+ textControl.setAttribute("type", aTextControl.type);
+ }
+ if (aTextControl.readonly) {
+ textControl.setAttribute("readonly", "");
+ }
+ return textControl;
+ }
+
+ #getDescription() {
+ return `<${this.#mTextControl.tag}${
+ this.#mTextControl.type !== undefined
+ ? ` type=${this.#mTextControl.type}`
+ : ""
+ }${this.#mTextControl.readonly ? " readonly" : ""}>`;
+ }
+
+ #getExpectedIMEState() {
+ return this.#mTextControl.readonly
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+ }
+
+ #flushPendingIMENotifications() {
+ return new Promise(resolve =>
+ this.#mWindow.requestAnimationFrame(() =>
+ this.#mWindow.requestAnimationFrame(resolve)
+ )
+ );
+ }
+
+ // Runner only fields.
+ #mEditingHost;
+ #mTextControl;
+ #mTextControlElement;
+ #mWindow;
+
+ // Checker only fields.
+ #mWindowUtils;
+ #mTIPWrapper;
+
+ clear() {
+ this.#mTIPWrapper?.clearFocusBlurNotifications();
+ this.#mTIPWrapper = null;
+ }
+
+ /**
+ * @param {number} aIndex Index of the test.
+ * @param {Element} aEditingHost The editing host which will have a text control.
+ * @param {Window} aWindow [optional] The DOM window containing aEditingHost.
+ * @returns {object} Expected result of initial state.
+ */
+ async prepareToRun(aIndex, aEditingHost, aWindow = window) {
+ this.#mWindow = aWindow;
+ this.#mEditingHost = aEditingHost;
+ this.#mEditingHost.ownerDocument.activeElement?.blur();
+ this.#mEditingHost.removeAttribute("contenteditable");
+ this.#mTextControlElement?.remove();
+ await this.#flushPendingIMENotifications();
+ this.#mTextControl =
+ IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#sTextControls[
+ aIndex
+ ];
+ this.#mTextControlElement =
+ IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#createElement(
+ this.#mEditingHost.ownerDocument,
+ this.#mTextControl
+ );
+ this.#mEditingHost.appendChild(this.#mTextControlElement);
+ this.#mTextControlElement.focus();
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when ${this.#getDescription()} simply has focus`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ #checkResult(aExpectedResult) {
+ const description =
+ "IMEStateOfTextControlInContentEditableOnReadonlyChangeTester";
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedResult.expectedIMEState,
+ `${description}: IME state should be proper one for the text control ${aExpectedResult.description}`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedResult.expectedIMEFocus,
+ `${description}: IME should ${
+ aExpectedResult.expectedIMEFocus ? "" : "not "
+ }have focus ${aExpectedResult.description}`
+ );
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result returned by prepareToRun().
+ * @param {Window} aWindow The window whose IME state should be checked.
+ * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow.
+ */
+ checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) {
+ this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
+ this.#mTIPWrapper = aTIPWrapper;
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeParentEditingHost() {
+ this.#mEditingHost.setAttribute("contenteditable", "");
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when parent of ${this.#getDescription()} becomes contenteditable`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingParentEditingHost(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeHTMLEditorReadonly() {
+ const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
+ editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when HTMLEditor for parent of ${this.#getDescription()} becomes readonly`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingHTMLEditorReadonly(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeHTMLEditorEditable() {
+ const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
+ editor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when HTMLEditor for parent of ${this.#getDescription()} becomes editable`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingHTMLEditorEditable(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeParentNonEditingHost() {
+ this.#mEditingHost.removeAttribute("contenteditable");
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when parent of ${this.#getDescription()} becomes non-editable`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingParentNonEditable(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+}
+
+class IMEStateOutsideContentEditableOnReadonlyChangeTester {
+ static #sFocusTargets = [
+ {
+ tag: "input",
+ type: "text",
+ readonly: false,
+ },
+ {
+ tag: "input",
+ type: "text",
+ readonly: true,
+ },
+ {
+ tag: "textarea",
+ readonly: false,
+ },
+ {
+ tag: "textarea",
+ readonly: true,
+ },
+ {
+ tag: "button",
+ },
+ {
+ tag: "body",
+ },
+ ];
+
+ static get numberOfFocusTargets() {
+ return IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets
+ .length;
+ }
+
+ static #maybeCreateElement(aDocument, aFocusTarget) {
+ if (aFocusTarget.tag == "body") {
+ return null;
+ }
+ const element = aDocument.createElement(aFocusTarget.tag);
+ if (aFocusTarget.type !== undefined) {
+ element.setAttribute("type", aFocusTarget.type);
+ }
+ if (aFocusTarget.readonly) {
+ element.setAttribute("readonly", "");
+ }
+ return element;
+ }
+
+ #getDescription() {
+ return `<${this.#mFocusTarget.tag}${
+ this.#mFocusTarget.type !== undefined
+ ? ` type=${this.#mFocusTarget.type}`
+ : ""
+ }${this.#mFocusTarget.readonly ? " readonly" : ""}>`;
+ }
+
+ #getExpectedIMEState() {
+ return this.#mFocusTarget.readonly ||
+ this.#mFocusTarget.tag == "button" ||
+ this.#mFocusTarget.tag == "body"
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+ }
+
+ #flushPendingIMENotifications() {
+ return new Promise(resolve =>
+ this.#mWindow.requestAnimationFrame(() =>
+ this.#mWindow.requestAnimationFrame(resolve)
+ )
+ );
+ }
+
+ // Runner only fields.
+ #mBody;
+ #mEditingHost;
+ #mFocusTarget;
+ #mFocusTargetElement;
+ #mWindow;
+
+ // Checker only fields.
+ #mWindowUtils;
+ #mTIPWrapper;
+
+ clear() {
+ this.#mTIPWrapper?.clearFocusBlurNotifications();
+ this.#mTIPWrapper = null;
+ }
+
+ /**
+ * @param {number} aIndex Index of the test.
+ * @param {Element} aEditingHost The editing host.
+ * @param {Window} aWindow [optional] The DOM window containing aEditingHost.
+ * @returns {object} Expected result of initial state.
+ */
+ async prepareToRun(aIndex, aEditingHost, aWindow = window) {
+ this.#mWindow = aWindow;
+ this.#mEditingHost = aEditingHost;
+ this.#mEditingHost.removeAttribute("contenteditable");
+ this.#mBody = this.#mEditingHost.ownerDocument.body;
+ this.#mBody.ownerDocument.activeElement?.blur();
+ if (this.#mFocusTargetElement != this.#mBody) {
+ this.#mFocusTargetElement?.remove();
+ }
+ await this.#flushPendingIMENotifications();
+ this.#mFocusTarget =
+ IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets[
+ aIndex
+ ];
+ this.#mFocusTargetElement =
+ IMEStateOutsideContentEditableOnReadonlyChangeTester.#maybeCreateElement(
+ this.#mBody.ownerDocument,
+ this.#mFocusTarget
+ );
+ if (this.#mFocusTargetElement) {
+ this.#mBody.appendChild(this.#mFocusTargetElement);
+ this.#mFocusTargetElement.focus();
+ }
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when ${this.#getDescription()} simply has focus`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ #checkResult(aExpectedResult) {
+ const description = "IMEStateOutsideContentEditableOnReadonlyChangeTester";
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedResult.expectedIMEState,
+ `${description}: IME state should be proper one for the focused element ${aExpectedResult.description}`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedResult.expectedIMEFocus,
+ `${description}: IME should ${
+ aExpectedResult.expectedIMEFocus ? "" : "not "
+ }have focus ${aExpectedResult.description}`
+ );
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result returned by prepareToRun().
+ * @param {Window} aWindow The window whose IME state should be checked.
+ * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow.
+ */
+ checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) {
+ this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
+ this.#mTIPWrapper = aTIPWrapper;
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeParentEditingHost() {
+ this.#mEditingHost.setAttribute("contenteditable", "");
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when parent of ${this.#getDescription()} becomes contenteditable`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingParentEditingHost(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeHTMLEditorReadonly() {
+ const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
+ editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when HTMLEditor for parent of ${this.#getDescription()} becomes readonly`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingHTMLEditorReadonly(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeHTMLEditorEditable() {
+ const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
+ editor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when HTMLEditor for parent of ${this.#getDescription()} becomes editable`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingHTMLEditorEditable(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+
+ async runToMakeParentNonEditingHost() {
+ this.#mEditingHost.removeAttribute("contenteditable");
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when parent of ${this.#getDescription()} becomes non-editable`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResultOfMakingParentNonEditable(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+ }
+}
diff --git a/widget/tests/file_test_ime_state_in_text_control_on_reframe.js b/widget/tests/file_test_ime_state_in_text_control_on_reframe.js
new file mode 100644
index 0000000000..719022b889
--- /dev/null
+++ b/widget/tests/file_test_ime_state_in_text_control_on_reframe.js
@@ -0,0 +1,190 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+
+// Bug 580388 and bug 808287
+class IMEStateInTextControlOnReframeTester {
+ static #sTextControls = [
+ {
+ tag: "input",
+ type: "text",
+ },
+ {
+ tag: "input",
+ type: "password",
+ },
+ {
+ tag: "textarea",
+ },
+ ];
+
+ static get numberOfTextControlTypes() {
+ return IMEStateInTextControlOnReframeTester.#sTextControls.length;
+ }
+
+ #createElement() {
+ const textControl = this.#mDocument.createElement(this.#mTextControl.tag);
+ if (this.#mTextControl.type !== undefined) {
+ textControl.setAttribute("type", this.#mTextControl.type);
+ }
+ return textControl;
+ }
+
+ #getDescription() {
+ return `<${this.#mTextControl.tag}${
+ this.#mTextControl.type !== undefined
+ ? ` type=${this.#mTextControl.type}`
+ : ""
+ }>`;
+ }
+
+ #getExpectedIMEState() {
+ return this.#mTextControl.type == "password"
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+ }
+
+ #flushPendingIMENotifications() {
+ return new Promise(resolve =>
+ this.#mWindow.requestAnimationFrame(() =>
+ this.#mWindow.requestAnimationFrame(resolve)
+ )
+ );
+ }
+
+ // Runner only fields.
+ #mTextControl;
+ #mTextControlElement;
+ #mWindow;
+ #mDocument;
+
+ // Checker only fields.
+ #mWindowUtils;
+ #mTIPWrapper;
+
+ clear() {
+ this.#mTIPWrapper?.clearFocusBlurNotifications();
+ this.#mTIPWrapper = null;
+ }
+
+ /**
+ * @param {number} aIndex Index of the test.
+ * @param {Element} aDocument The document to run the test.
+ * @param {Window} aWindow [optional] The DOM window for aDocument.
+ * @returns {object} Expected result of initial state.
+ */
+ async prepareToRun(aIndex, aDocument, aWindow = window) {
+ this.#mWindow = aWindow;
+ this.#mDocument = aDocument;
+ this.#mDocument.activeElement?.blur();
+ this.#mTextControlElement?.remove();
+ await this.#flushPendingIMENotifications();
+ this.#mTextControl =
+ IMEStateInTextControlOnReframeTester.#sTextControls[aIndex];
+ this.#mTextControlElement = this.#createElement();
+ this.#mDocument.body.appendChild(this.#mTextControlElement);
+ this.#mTextControlElement.focus();
+ this.#mTextControlElement.style.overflow = "visible";
+ this.#mTextControlElement.addEventListener(
+ "input",
+ aEvent => {
+ aEvent.target.style.overflow = "hidden";
+ },
+ {
+ capture: true,
+ }
+ );
+ await this.#flushPendingIMENotifications();
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when ${this.#getDescription()} has focus`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ expectedNumberOfFocusNotifications: 1,
+ };
+ }
+
+ #checkResult(aExpectedResult) {
+ const description = "IMEStateInTextControlOnReframeTester";
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedResult.expectedIMEState,
+ `${description}: IME state should be proper one for the text control ${aExpectedResult.description}`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedResult.expectedIMEFocus,
+ `${description}: IME should ${
+ aExpectedResult.expectedIMEFocus ? "" : "not "
+ }have focus ${aExpectedResult.description}`
+ );
+ if (aExpectedResult.numberOfFocusNotifications !== undefined) {
+ is(
+ this.#mTIPWrapper.numberOfFocusNotifications,
+ aExpectedResult.numberOfFocusNotifications,
+ `${description}: focus notifications should've been received ${
+ this.#mTIPWrapper.numberOfFocusNotifications
+ } times ${aExpectedResult.description}`
+ );
+ }
+ if (aExpectedResult.numberOfBlurNotifications !== undefined) {
+ is(
+ this.#mTIPWrapper.numberOfBlurNotifications,
+ aExpectedResult.numberOfBlurNotifications,
+ `${description}: blur notifications should've been received ${
+ this.#mTIPWrapper.numberOfBlurNotifications
+ } times ${aExpectedResult.description}`
+ );
+ }
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result returned by prepareToRun().
+ * @param {Window} aWindow The window whose IME state should be checked.
+ * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow.
+ */
+ checkResultAfterTypingA(aExpectedResult, aWindow, aTIPWrapper) {
+ this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
+ this.#mTIPWrapper = aTIPWrapper;
+ this.#checkResult(aExpectedResult);
+
+ this.#mTIPWrapper.clearFocusBlurNotifications();
+ }
+
+ async prepareToRun2() {
+ this.#mTextControlElement.addEventListener("focus", aEvent => {
+ // Perform a style change and flush it to trigger reframing.
+ aEvent.target.style.overflow = "visible";
+ aEvent.target.getBoundingClientRect();
+ });
+ this.#mTextControlElement.blur();
+ this.#mTextControlElement.focus();
+
+ await this.#flushPendingIMENotifications();
+
+ const expectedIMEState = this.#getExpectedIMEState();
+ return {
+ description: `when ${this.#getDescription()} is reframed by focus event listener`,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ expectedNumberOfFocusNotifications: 1,
+ expectedNumberOfBlurNotifications: 1,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedResult The expected result returned by prepareToRun().
+ */
+ checkResultAfterTypingA2(aExpectedResult) {
+ this.#checkResult(aExpectedResult);
+
+ this.#mTIPWrapper.clearFocusBlurNotifications();
+ }
+}
diff --git a/widget/tests/file_test_ime_state_on_focus_move.js b/widget/tests/file_test_ime_state_on_focus_move.js
new file mode 100644
index 0000000000..f7760d8a09
--- /dev/null
+++ b/widget/tests/file_test_ime_state_on_focus_move.js
@@ -0,0 +1,1588 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+
+class IMEStateWhenNoActiveElementTester {
+ #mDescription;
+
+ constructor(aDescription) {
+ this.#mDescription = aDescription;
+ }
+
+ async run(aDocument, aWindow = window) {
+ aWindow.focus();
+ aDocument.activeElement?.blur();
+
+ await new Promise(resolve =>
+ requestAnimationFrame(() => requestAnimationFrame(resolve))
+ ); // wait for sending IME notifications
+
+ return { designModeValue: aDocument.designMode };
+ }
+
+ check(aExpectedData, aWindow = window) {
+ const winUtils = SpecialPowers.wrap(aWindow).windowUtils;
+ if (aExpectedData.designModeValue == "on") {
+ is(
+ winUtils.IMEStatus,
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ `IMEStateWhenNoActiveElementTester(${
+ this.#mDescription
+ }): When no element has focus, IME should stay enabled in design mode`
+ );
+ } else {
+ is(
+ winUtils.IMEStatus,
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `IMEStateWhenNoActiveElementTester(${
+ this.#mDescription
+ }): When no element has focus, IME should be disabled`
+ );
+ }
+ }
+}
+
+class IMEStateOnFocusMoveTester {
+ // Common fields
+ #mDescription;
+ #mTest;
+ #mWindow;
+ #mWindowUtils;
+
+ // Only runner fields
+ #mCreatedElement;
+ #mCreatedElementForPreviousFocusedElement;
+ #mElementToSetFocus;
+ #mContainerIsEditable;
+
+ // Only checker fields
+ #mTIPWrapper;
+
+ constructor(aDescription, aIndex, aWindow = window) {
+ this.#mTest = IMEStateOnFocusMoveTester.#sTestList[aIndex];
+ this.#mDescription = `IMEStateOnFocusMoveTester(${aDescription}): ${
+ this.#mTest.description
+ }`;
+ this.#mWindow = aWindow;
+ this.#mWindowUtils = SpecialPowers.wrap(this.#mWindow).windowUtils;
+ }
+
+ /**
+ * prepareToRun should be called before run only in the process which will run the test.
+ */
+ async prepareToRun(aContainer) {
+ const doc = aContainer.ownerDocument;
+ this.#mTest = this.#resolveTest(this.#mTest, aContainer);
+ this.#mContainerIsEditable = nodeIsEditable(aContainer);
+ this.#mCreatedElement = this.#mTest.createElement(doc);
+ const waitForLoadIfIFrame = new Promise(resolve => {
+ if (this.#mCreatedElement.tagName == "IFRAME") {
+ this.#mCreatedElement.addEventListener("load", resolve, {
+ capture: true,
+ once: true,
+ });
+ } else {
+ resolve();
+ }
+ });
+ aContainer.appendChild(this.#mCreatedElement);
+ await waitForLoadIfIFrame;
+ this.#mElementToSetFocus = this.#mCreatedElement.contentDocument
+ ? this.#mCreatedElement.contentDocument.documentElement
+ : this.#mCreatedElement;
+ if (doc.designMode == "on") {
+ doc.activeElement?.blur();
+ } else if (this.#mContainerIsEditable) {
+ getEditingHost(aContainer).focus(); // FIXME: use editing host instead
+ } else {
+ this.#mCreatedElementForPreviousFocusedElement =
+ doc.createElement("input");
+ this.#mCreatedElementForPreviousFocusedElement.setAttribute(
+ "type",
+ this.#mTest.expectedEnabledValue ==
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED
+ ? "password"
+ : "text"
+ );
+ aContainer.appendChild(this.#mCreatedElementForPreviousFocusedElement);
+ this.#mCreatedElementForPreviousFocusedElement.focus();
+ }
+
+ await new Promise(resolve =>
+ requestAnimationFrame(() => requestAnimationFrame(resolve))
+ ); // wait for sending IME notifications
+
+ return {
+ designModeValue: doc.designMode,
+ containerIsEditable: this.#mContainerIsEditable,
+ isFocusable: this.#mTest.isFocusable,
+ focusEventFired: this.#mTest.focusEventIsExpected,
+ enabledValue: this.#mTest.expectedEnabledValue,
+ testedSubDocumentInDesignMode:
+ this.#mCreatedElement.contentDocument?.designMode == "on",
+ };
+ }
+
+ /**
+ * prepareToCheck should be called before calling run only in the process which will check the result.
+ */
+ prepareToCheck(aExpectedData, aTIPWrapper) {
+ info(`Starting ${this.#mDescription} (enable state check)...`);
+ this.#mTIPWrapper = aTIPWrapper;
+ this.#mTIPWrapper.onIMEFocusBlur = aNotificationType => {
+ switch (aNotificationType) {
+ case "notify-focus":
+ info(aNotificationType);
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.enabledValue,
+ `${
+ this.#mDescription
+ }, IME should receive a focus notification after IME state is updated`
+ );
+ break;
+ case "notify-blur":
+ info(aNotificationType);
+ const changingStatus = !(
+ aExpectedData.containerIsEditable &&
+ aExpectedData.enabledValue ==
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED
+ );
+ if (aExpectedData.designModeValue == "on") {
+ is(
+ // FIXME: This is odd, but #mWindowUtils.IMEStatus sometimes IME_STATUS_PASSWORD
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ aExpectedData.enabledValue,
+ `${
+ this.#mDescription
+ }, IME should receive a blur notification after IME state is updated`
+ );
+ } else if (changingStatus) {
+ isnot(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.enabledValue,
+ `${
+ this.#mDescription
+ }, IME should receive a blur notification BEFORE IME state is updated`
+ );
+ } else {
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.enabledValue,
+ `${
+ this.#mDescription
+ }, IME should receive a blur notification and its context has expected IME state if the state isn't being changed`
+ );
+ }
+ break;
+ }
+ };
+
+ this.#mTIPWrapper.clearFocusBlurNotifications();
+ }
+
+ /**
+ * @returns {bool} whether expected element has focus or not after moving focus.
+ */
+ async run() {
+ const previousFocusedElement = getFocusedElementOrUAWidgetHost();
+ if (this.#mTest.setFocusIntoUAWidget) {
+ this.#mTest.setFocusIntoUAWidget(this.#mElementToSetFocus);
+ } else {
+ this.#mElementToSetFocus.focus();
+ }
+
+ await new Promise(resolve =>
+ requestAnimationFrame(() => requestAnimationFrame(resolve))
+ ); // wait for sending IME notifications
+
+ const currentFocusedElement = getFocusedElementOrUAWidgetHost();
+ this.#mCreatedElementForPreviousFocusedElement?.remove();
+ if (this.#mTest.isFocusable) {
+ return this.#mElementToSetFocus == currentFocusedElement;
+ }
+ return previousFocusedElement == currentFocusedElement;
+ }
+
+ check(aExpectedData) {
+ this.#mTIPWrapper.onIMEFocusBlur = null;
+
+ if (aExpectedData.isFocusable) {
+ if (aExpectedData.focusEventFired) {
+ if (
+ aExpectedData.enabledValue ==
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED ||
+ aExpectedData.enabledValue ==
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD
+ ) {
+ ok(
+ this.#mTIPWrapper.numberOfFocusNotifications > 0,
+ `${this.#mDescription}, IME should receive a focus notification`
+ );
+ if (
+ aExpectedData.designModeValue == "on" &&
+ !aExpectedData.testedSubDocumentInDesignMode
+ ) {
+ is(
+ this.#mTIPWrapper.numberOfBlurNotifications,
+ 0,
+ `${
+ this.#mDescription
+ }, IME shouldn't receive a blur notification in designMode since focus isn't moved from another editor`
+ );
+ } else {
+ ok(
+ this.#mTIPWrapper.numberOfBlurNotifications > 0,
+ `${
+ this.#mDescription
+ }, IME should receive a blur notification for the previous focused editor`
+ );
+ }
+ ok(
+ this.#mTIPWrapper.IMEHasFocus,
+ `${this.#mDescription}, IME should have focus right now`
+ );
+ } else {
+ is(
+ this.#mTIPWrapper.numberOfFocusNotifications,
+ 0,
+ `${this.#mDescription}, IME shouldn't receive a focus notification`
+ );
+ ok(
+ this.#mTIPWrapper.numberOfBlurNotifications > 0,
+ `${this.#mDescription}, IME should receive a blur notification`
+ );
+ ok(
+ !this.#mTIPWrapper.IMEHasFocus,
+ `${this.#mDescription}, IME shouldn't have focus right now`
+ );
+ }
+ } else {
+ ok(true, `${this.#mDescription}, focus event should be fired`);
+ }
+ } else {
+ is(
+ this.#mTIPWrapper.numberOfFocusNotifications,
+ 0,
+ `${
+ this.#mDescription
+ }, IME shouldn't receive a focus notification at testing non-focusable element`
+ );
+ is(
+ this.#mTIPWrapper.numberOfBlurNotifications,
+ 0,
+ `${
+ this.#mDescription
+ }, IME shouldn't receive a blur notification at testing non-focusable element`
+ );
+ }
+
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.enabledValue,
+ `${this.#mDescription}, wrong enabled state`
+ );
+ if (
+ this.#mTest.expectedInputElementType &&
+ aExpectedData.designModeValue != "on"
+ ) {
+ is(
+ this.#mWindowUtils.focusedInputType,
+ this.#mTest.expectedInputElementType,
+ `${this.#mDescription}, wrong input type`
+ );
+ } else if (aExpectedData.designModeValue == "on") {
+ is(
+ this.#mWindowUtils.focusedInputType,
+ "",
+ `${this.#mDescription}, wrong input type`
+ );
+ }
+ }
+
+ destroy() {
+ this.#mCreatedElement?.remove();
+ this.#mCreatedElementForPreviousFocusedElement?.remove();
+ this.#mTIPWrapper?.clearFocusBlurNotifications();
+ this.#mTIPWrapper = null;
+ }
+
+ /**
+ * Open/Close state test check
+ * Note that these tests are not run now.
+ * If these tests should run between `run` and `cleanUp` call of the above
+ * tests.
+ */
+ canTestOpenCloseState(aExpectedData) {
+ return (
+ IsIMEOpenStateSupported() &&
+ this.#mWindowUtils.IMEStatus ==
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED &&
+ aExpectedData.enabledValue ==
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED
+ );
+ }
+ async prepareToRunOpenCloseTest(aContainer) {
+ const doc = aContainer.ownerDocument;
+ this.#mCreatedElementForPreviousFocusedElement?.remove();
+ this.#mCreatedElementForPreviousFocusedElement = doc.createElement("input");
+ this.#mCreatedElementForPreviousFocusedElement.setAttribute("type", "text");
+ aContainer.appendChild(this.#mCreatedElementForPreviousFocusedElement);
+
+ this.#mContainerIsEditable = nodeIsEditable(aContainer);
+ this.#mCreatedElement = this.#mTest.createElement(doc);
+ const waitForLoadIfIFrame = new Promise(resolve => {
+ if (this.#mCreatedElement.tagName == "IFRAME") {
+ this.#mCreatedElement.addEventListener("load", resolve, {
+ capture: true,
+ once: true,
+ });
+ } else {
+ resolve();
+ }
+ });
+ aContainer.appendChild(this.#mCreatedElement);
+ await waitForLoadIfIFrame;
+ this.#mElementToSetFocus = this.#mCreatedElement.contentDocument
+ ? this.#mCreatedElement.contentDocument.documentElement
+ : this.#mCreatedElement;
+
+ this.#mCreatedElementForPreviousFocusedElement.focus();
+
+ return {};
+ }
+ prepareToCheckOpenCloseTest(aPreviousOpenState, aExpectedData) {
+ info(`Starting ${this.#mDescription} (open/close state check)...`);
+ this.#mWindowUtils.IMEIsOpen = aPreviousOpenState;
+ aExpectedData.defaultOpenState = this.#mWindowUtils.IMEIsOpen;
+ }
+ async runOpenCloseTest() {
+ return this.run();
+ }
+ checkOpenCloseTest(aExpectedData) {
+ const expectedOpenState =
+ this.#mTest.expectedOpenState != undefined
+ ? this.#mTest.expectedOpenState
+ : aExpectedData.defaultOpenState;
+ is(
+ this.#mWindowUtils.IMEIsOpen,
+ expectedOpenState,
+ `${this.#mDescription}, IME should ${
+ expectedOpenState != aExpectedData.defaultOpenState ? "become" : "keep"
+ } ${expectedOpenState ? "open" : "closed"}`
+ );
+ }
+
+ /**
+ * Utility methods for defining kIMEStateTestList.
+ */
+
+ /**
+ * @param {Element} aElement
+ */
+ static #elementIsConnectedAndNotInDesignMode(aElement) {
+ return aElement.isConnected && !nodeIsInDesignMode(aElement);
+ }
+
+ /**
+ * @param {Element} aElementContainer
+ * @param {bool} aElementIsEditingHost
+ */
+ static #elementIsFocusableIfEditingHost(
+ aElementContainer,
+ aElementIsEditingHost
+ ) {
+ return !nodeIsEditable(aElementContainer) && aElementIsEditingHost;
+ }
+
+ static #IMEStateEnabledAlways() {
+ return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+ }
+
+ /**
+ * @param {Element} aElement
+ * @param {bool} aElementIsEditingHost
+ */
+ static #IMEStateEnabledIfEditable(aElement, aElementIsEditingHost) {
+ return nodeIsEditable(aElement) || aElementIsEditingHost
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED;
+ }
+
+ /**
+ * @param {Element} aElement
+ */
+ static #IMEStateEnabledIfInDesignMode(aElement) {
+ return IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode(
+ aElement
+ )
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+ }
+
+ /**
+ * @param {Element} aElement
+ */
+ static #IMEStatePasswordIfNotInDesignMode(aElement) {
+ return IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode(
+ aElement
+ )
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+ }
+
+ /**
+ * @param {Element} aElement
+ */
+ static #elementIsConnectedAndNotEditable(aElement) {
+ return aElement.isConnected && !nodeIsEditable(aElement);
+ }
+
+ /**
+ * @param {Element} aElementContainer
+ */
+ static #focusEventIsExpectedUnlessEditableChild(aElementContainer) {
+ return !nodeIsEditable(aElementContainer);
+ }
+
+ // Form controls except text editable elements are "disable" in normal
+ // condition, however, if they are editable, they are "enabled".
+ // XXX Probably there are some bugs: If the form controls editable, they
+ // shouldn't be focusable.
+ #resolveTest(aTest, aContainer) {
+ const isFocusable = aTest.isFocusable(
+ aContainer,
+ aTest.isNewElementEditingHost
+ );
+ return {
+ // Description of the new element
+ description: aTest.description,
+ // Create element to check IME state
+ createElement: aTest.createElement,
+ // Whether the new element is an editing host if container is not editable
+ isNewElementEditingHost: aTest.isNewElementEditingHost,
+ // If the test wants to move focus into an element in UA widget, define
+ // this and set focus in it.
+ setFocusIntoUAWidget: aTest.setFocusIntoUAWidget,
+ // Whether the element is focusable or not
+ isFocusable,
+ // Whether focus events are fired on the element if it's focusable
+ focusEventIsExpected:
+ isFocusable &&
+ aTest.focusEventIsExpected(aContainer, aTest.isNewElementEditingHost),
+ // Expected IME enabled state when the element has focus
+ expectedEnabledValue: aTest.expectedEnabledValue(
+ aContainer,
+ aTest.isNewElementEditingHost
+ ),
+ // Expected IME open state when the element gets focus
+ // "undefined" means that IME open state should not be changed
+ expectedOpenState: aTest.expectedOpenState,
+ // Expected type of input element if it's an <input>
+ expectedInputElementType: aTest.expectedInputElementType,
+ };
+ }
+ static #sTestList = [
+ {
+ description: "input[type=text]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "text");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedInputElementType: "text",
+ },
+ {
+ description: "input[type=text][readonly]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("readonly", "");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "input[type=password]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "password");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ expectedInputElementType: "password",
+ },
+ {
+ description: "input[type=password][readonly]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "password");
+ element.setAttribute("readonly", "");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "input[type=checkbox]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "checkbox");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected:
+ IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "input[type=radio]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "radio");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected:
+ IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "input[type=submit]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "submit");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "input[type=reset]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "reset");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "input[type=file]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "file");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected:
+ IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "input[type=button]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "button");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "input[type=image]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "image");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "input[type=url]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "url");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedInputElementType: "url",
+ },
+ {
+ description: "input[type=email]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "email");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedInputElementType: "email",
+ },
+ {
+ description: "input[type=search]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "search");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedInputElementType: "search",
+ },
+ {
+ description: "input[type=tel]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "tel");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedInputElementType: "tel",
+ },
+ {
+ description: "input[type=number]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "number");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedInputElementType: "number",
+ },
+ {
+ description: "input[type=date]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "date");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ expectedInputElementType: "date",
+ },
+ {
+ description: "input[type=datetime-local]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "datetime-local");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ expectedInputElementType: "datetime-local",
+ },
+ {
+ description: "input[type=time]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "time");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ expectedInputElementType: "time",
+ },
+ // TODO(bug 1283382, bug 1283382): month and week
+
+ // form controls
+ {
+ description: "button",
+ createElement: aDocument => aDocument.createElement("button"),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "textarea",
+ createElement: aDocument => aDocument.createElement("textarea"),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: "textarea[readonly]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("textarea");
+ element.setAttribute("readonly", "");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "select (dropdown list)",
+ createElement: aDocument => {
+ const select = aDocument.createElement("select");
+ const option1 = aDocument.createElement("option");
+ option1.textContent = "abc";
+ const option2 = aDocument.createElement("option");
+ option2.textContent = "def";
+ const option3 = aDocument.createElement("option");
+ option3.textContent = "ghi";
+ select.appendChild(option1);
+ select.appendChild(option2);
+ select.appendChild(option3);
+ return select;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected:
+ IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "select (list box)",
+ createElement: aDocument => {
+ const select = aDocument.createElement("select");
+ select.setAttribute("multiple", "multiple");
+ const option1 = aDocument.createElement("option");
+ option1.textContent = "abc";
+ const option2 = aDocument.createElement("option");
+ option2.textContent = "def";
+ const option3 = aDocument.createElement("option");
+ option3.textContent = "ghi";
+ select.appendChild(option1);
+ select.appendChild(option2);
+ select.appendChild(option3);
+ return select;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected:
+ IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+
+ // a element
+ {
+ id: "a_href",
+ description: "a[href]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("a");
+ element.setAttribute("href", "about:blank");
+ return element;
+ },
+ isFocusable: IMEStateOnFocusMoveTester.#elementIsConnectedAndNotEditable,
+ focusEventIsExpected:
+ IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+
+ // audio element
+ {
+ description: "audio[controls]",
+ createElement: aDocument => {
+ const element = aDocument.createElement("audio");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "playButton in audio",
+ createElement: aDocument => {
+ const element = aDocument.createElement("audio");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("playButton")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "scrubber in audio",
+ createElement: aDocument => {
+ const element = aDocument.createElement("audio");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("scrubber")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "muteButton in audio",
+ createElement: aDocument => {
+ const element = aDocument.createElement("audio");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("muteButton")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "volumeControl in audio",
+ createElement: aDocument => {
+ const element = aDocument.createElement("audio");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("volumeControl")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+
+ // video element
+ {
+ description: "video",
+ createElement: aDocument => {
+ const element = aDocument.createElement("video");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable,
+ },
+ {
+ description: "playButton in video",
+ createElement: aDocument => {
+ const element = aDocument.createElement("video");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("playButton")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "scrubber in video",
+ createElement: aDocument => {
+ const element = aDocument.createElement("video");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("scrubber")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "muteButton in video",
+ createElement: aDocument => {
+ const element = aDocument.createElement("video");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("muteButton")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+ {
+ description: "volumeControl in video",
+ createElement: aDocument => {
+ const element = aDocument.createElement("video");
+ element.setAttribute("controls", "");
+ return element;
+ },
+ setFocusIntoUAWidget: aElement =>
+ SpecialPowers.wrap(aElement)
+ .openOrClosedShadowRoot.getElementById("volumeControl")
+ .focus(),
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode,
+ },
+
+ // ime-mode
+ {
+ description: 'input[type=text][style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=text][style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=text][style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: true,
+ },
+ {
+ description: 'input[type=text][style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: false,
+ },
+ {
+ description: 'input[type=text][style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+
+ {
+ description: 'input[type=url][style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "url");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=url][style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "url");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=url][style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "url");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedOpenState: true,
+ },
+ {
+ description: 'input[type=url][style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "url");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedOpenState: false,
+ },
+ {
+ description: 'input[type=url][style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "url");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+
+ {
+ description: 'input[type=email][style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "email");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=email][style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "email");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=email][style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "email");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedOpenState: true,
+ },
+ {
+ description: 'input[type=email][style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "email");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedOpenState: false,
+ },
+ {
+ description: 'input[type=email][style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "email");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+
+ {
+ description: 'input[type=search][style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "search");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=search][style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "search");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=search][style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "search");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedOpenState: true,
+ },
+ {
+ description: 'input[type=search][style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "search");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedOpenState: false,
+ },
+ {
+ description: 'input[type=search][style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "search");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+
+ {
+ description: 'input[type=tel][style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "tel");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=tel][style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "tel");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=tel][style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "tel");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: true,
+ },
+ {
+ description: 'input[type=tel][style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "tel");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: false,
+ },
+ {
+ description: 'input[type=tel][style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "tel");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+
+ {
+ description: 'input[type=number][style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "number");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=number][style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "number");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=number][style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "number");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: true,
+ },
+ {
+ description: 'input[type=number][style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "number");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: false,
+ },
+ {
+ description: 'input[type=number][style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "number");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+
+ {
+ description: 'input[type=password][style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "password");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+ {
+ description: 'input[type=password][style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "password");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'input[type=password][style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "password");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: true,
+ },
+ {
+ description: 'input[type=password][style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "password");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: false,
+ },
+ {
+ description: 'input[type=password][style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("input");
+ element.setAttribute("type", "password");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+ {
+ description: 'textarea[style="ime-mode: auto;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("textarea");
+ element.setAttribute("style", "ime-mode: auto;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'textarea[style="ime-mode: normal;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("textarea");
+ element.setAttribute("style", "ime-mode: normal;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: 'textarea[style="ime-mode: active;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("textarea");
+ element.setAttribute("style", "ime-mode: active;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: true,
+ },
+ {
+ description: 'textarea[style="ime-mode: inactive;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("textarea");
+ element.setAttribute("style", "ime-mode: inactive;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ expectedOpenState: false,
+ },
+ {
+ description: 'textarea[style="ime-mode: disabled;"]',
+ createElement: aDocument => {
+ const element = aDocument.createElement("textarea");
+ element.setAttribute("style", "ime-mode: disabled;");
+ return element;
+ },
+ isFocusable:
+ IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue:
+ IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode,
+ },
+
+ // HTML editors
+ {
+ description: 'div[contenteditable="true"]',
+ createElement: aDocument => {
+ const div = aDocument.createElement("div");
+ div.setAttribute("contenteditable", "");
+ return div;
+ },
+ isNewElementEditingHost: true,
+ isFocusable: IMEStateOnFocusMoveTester.#elementIsFocusableIfEditingHost,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ {
+ description: "designMode editor",
+ createElement: aDocument => {
+ const iframe = aDocument.createElement("iframe");
+ iframe.srcdoc = "<!doctype html><html><body></body></html>";
+ iframe.addEventListener(
+ "load",
+ () => (iframe.contentDocument.designMode = "on"),
+ { capture: true, once: true }
+ );
+ return iframe;
+ },
+ isFocusable: () => true,
+ focusEventIsExpected: () => true,
+ expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways,
+ },
+ ];
+
+ static get numberOfTests() {
+ return IMEStateOnFocusMoveTester.#sTestList.length;
+ }
+}
diff --git a/widget/tests/file_test_ime_state_on_input_type_change.js b/widget/tests/file_test_ime_state_on_input_type_change.js
new file mode 100644
index 0000000000..38c139d6a2
--- /dev/null
+++ b/widget/tests/file_test_ime_state_on_input_type_change.js
@@ -0,0 +1,315 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+
+class IMEStateOnInputTypeChangeTester {
+ static #sInputElementList = [
+ {
+ type: "",
+ textControl: true,
+ },
+ {
+ type: "text",
+ textControl: true,
+ },
+ {
+ type: "text",
+ readonly: true,
+ textControl: true,
+ },
+ {
+ type: "password",
+ textControl: true,
+ },
+ {
+ type: "password",
+ readonly: true,
+ textControl: true,
+ },
+ {
+ type: "checkbox",
+ buttonControl: true,
+ },
+ {
+ type: "radio",
+ buttonControl: true,
+ },
+ {
+ type: "submit",
+ buttonControl: true,
+ },
+ {
+ type: "reset",
+ buttonControl: true,
+ },
+ {
+ type: "file",
+ buttonControl: true,
+ },
+ {
+ type: "button",
+ buttonControl: true,
+ },
+ {
+ type: "image",
+ alt: "image",
+ buttonControl: true,
+ },
+ {
+ type: "url",
+ textControl: true,
+ },
+ {
+ type: "email",
+ textControl: true,
+ },
+ {
+ type: "search",
+ textControl: true,
+ },
+ {
+ type: "tel",
+ textControl: true,
+ },
+ {
+ type: "number",
+ textControl: true,
+ },
+ {
+ type: "date",
+ dateTimeControl: true,
+ },
+ {
+ type: "datetime-local",
+ dateTimeControl: true,
+ },
+ {
+ type: "time",
+ dateTimeControl: true,
+ },
+ {
+ type: "range",
+ buttonControl: true,
+ },
+ {
+ type: "color",
+ buttonControl: true,
+ },
+ // TODO(bug 1283382, bug 1283382): month and week
+ ];
+
+ static get numberOfTests() {
+ return IMEStateOnInputTypeChangeTester.#sInputElementList.length;
+ }
+
+ /**
+ * @param {HTMLInputElement} aInputElement The input element.
+ * @returns {number} Expected IME state when aInputElement has focus.
+ */
+ static #getExpectedIMEEnabledState(aInputElement) {
+ if (aInputElement.readonly) {
+ return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED;
+ }
+ switch (aInputElement.type) {
+ case "text":
+ case "url":
+ case "email":
+ case "search":
+ case "tel":
+ case "number":
+ return aInputElement.style.imeMode == "disabled"
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+
+ case "password":
+ return aInputElement.style.imeMode == "" ||
+ aInputElement.style.imeMode == "auto" ||
+ aInputElement.style.imeMode == "disabled"
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD
+ : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
+
+ default:
+ return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED;
+ }
+ }
+
+ static #getDescription(aInputElement) {
+ let desc = "<input";
+ if (aInputElement.getAttribute("type")) {
+ desc += ` type="${aInputElement.getAttribute("type")}"`;
+ }
+ if (aInputElement.readonly) {
+ desc += " readonly";
+ }
+ if (aInputElement.getAttribute("style")) {
+ desc += ` style="ime-mode: ${aInputElement.style.imeMode}"`;
+ }
+ return desc + ">";
+ }
+
+ // Only runner fields
+ #mSrcTypeIndex;
+ #mType;
+ #mInputElement;
+ #mNewType;
+ #mWindow;
+
+ // Only checker fields
+ #mWindowUtils;
+ #mTIPWrapper;
+
+ /**
+ * @param {number} aSrcTypeIndex `type` attribute value index in #sInputElementList
+ */
+ constructor(aSrcTypeIndex) {
+ this.#mSrcTypeIndex = aSrcTypeIndex;
+ this.#mType =
+ IMEStateOnInputTypeChangeTester.#sInputElementList[this.#mSrcTypeIndex];
+ }
+
+ clear() {
+ this.#mInputElement?.remove();
+ this.#mTIPWrapper?.clearFocusBlurNotifications();
+ this.#mTIPWrapper = null;
+ }
+
+ #flushPendingIMENotifications() {
+ return new Promise(resolve =>
+ this.#mWindow.requestAnimationFrame(() =>
+ this.#mWindow.requestAnimationFrame(resolve)
+ )
+ );
+ }
+
+ /**
+ * @param {number} aDestTypeIndex New type index.
+ * @param {Window} aWindow The window running tests.
+ * @param {Element} aContainer The element which should have new <input>.
+ * @param {undefined|string} aIMEModeValue [optional] ime-mode value if you want to specify.
+ * @returns {object|bool} Expected data before running tests. If the test should not run, returns false.
+ */
+ async prepareToRun(aDestTypeIndex, aWindow, aContainer, aIMEModeValue) {
+ this.#mWindow = aWindow;
+
+ if (aContainer.ownerDocument.activeElement) {
+ aContainer.ownerDocument.activeElement.blur();
+ await this.#flushPendingIMENotifications();
+ }
+ if (this.#mInputElement?.isConnected) {
+ this.#mInputElement.remove();
+ }
+ this.#mInputElement = null;
+
+ this.#mNewType =
+ IMEStateOnInputTypeChangeTester.#sInputElementList[aDestTypeIndex];
+ if (
+ aDestTypeIndex == this.#mSrcTypeIndex ||
+ this.#mNewType.readonly ||
+ (!this.#mType.textControl && !this.#mNewType.textControl)
+ ) {
+ return false;
+ }
+ this.#mInputElement = aContainer.ownerDocument.createElement("input");
+ if (this.#mType.type != "") {
+ this.#mInputElement.setAttribute("type", this.#mType.type);
+ }
+ if (this.#mType.readonly) {
+ this.#mInputElement.setAttribute("readonly", "");
+ }
+ if (aIMEModeValue && aIMEModeValue !== "") {
+ this.#mInputElement.setAttribute("style", `ime-mode: ${aIMEModeValue}`);
+ }
+ if (this.#mType.alt) {
+ this.#mInputElement.setAttribute("alt", this.#mType.alt);
+ }
+ const waitForFocus = new Promise(resolve =>
+ this.#mInputElement.addEventListener("focus", resolve, { once: true })
+ );
+ aContainer.appendChild(this.#mInputElement);
+ this.#mInputElement.focus();
+ await waitForFocus;
+
+ await this.#flushPendingIMENotifications();
+
+ const expectedIMEState =
+ IMEStateOnInputTypeChangeTester.#getExpectedIMEEnabledState(
+ this.#mInputElement
+ );
+ return {
+ description: IMEStateOnInputTypeChangeTester.#getDescription(
+ this.#mInputElement
+ ),
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedData The expected data which was returned by prepareToRun().
+ * @param {TIPWrapper} aTIPWrapper TIP wrapper in aWindow.
+ * @param {Window} aWindow [optional] The window object of which you want to check IME state.
+ */
+ checkBeforeRun(aExpectedData, aTIPWrapper, aWindow = window) {
+ this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
+ this.#mTIPWrapper = aTIPWrapper;
+ const description = `IMEStateOnInputTypeChangeTester(${aExpectedData.description})`;
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.expectedIMEState,
+ `${description}: IME state should be set to expected value before running test`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedData.expectedIMEFocus,
+ `${description}: IME state should ${
+ aExpectedData.expectedIMEFocus ? "" : "not"
+ }have focus before running test`
+ );
+ }
+
+ /**
+ * @returns {object} The expected results.
+ */
+ async run() {
+ if (this.#mNewType.type == "") {
+ this.#mInputElement.removeAttribute("type");
+ } else {
+ this.#mInputElement.setAttribute("type", this.#mNewType.type);
+ }
+
+ await this.#flushPendingIMENotifications();
+
+ const expectedIMEState =
+ IMEStateOnInputTypeChangeTester.#getExpectedIMEEnabledState(
+ this.#mInputElement
+ );
+ return {
+ newType: this.#mNewType.type,
+ expectedIMEState,
+ expectedIMEFocus:
+ expectedIMEState !=
+ SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ };
+ }
+
+ checkResult(aExpectedDataBeforeRun, aExpectedData) {
+ const description = `IMEStateOnInputTypeChangeTester(${
+ aExpectedDataBeforeRun.description
+ } -> type="${aExpectedData.newType || ""}")`;
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.expectedIMEState,
+ `${description}: IME state should be set to expected value`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedData.expectedIMEFocus,
+ `${description}: IME state should have focus`
+ );
+ }
+}
diff --git a/widget/tests/file_test_ime_state_on_readonly_change.js b/widget/tests/file_test_ime_state_on_readonly_change.js
new file mode 100644
index 0000000000..af21f538ba
--- /dev/null
+++ b/widget/tests/file_test_ime_state_on_readonly_change.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+
+class IMEStateOnReadonlyChangeTester {
+ static #sTextControlTypes = [
+ {
+ description: "<input>",
+ createElement: aDoc => aDoc.createElement("input"),
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ {
+ description: `<input type="text">`,
+ createElement: aDoc => {
+ const input = aDoc.createElement("input");
+ input.setAttribute("type", "text");
+ return input;
+ },
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ {
+ description: `<input type="password">`,
+ createElement: aDoc => {
+ const input = aDoc.createElement("input");
+ input.setAttribute("type", "password");
+ return input;
+ },
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD,
+ },
+ {
+ description: `<input type="url">`,
+ createElement: aDoc => {
+ const input = aDoc.createElement("input");
+ input.setAttribute("type", "url");
+ return input;
+ },
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ {
+ description: `<input type="email">`,
+ createElement: aDoc => {
+ const input = aDoc.createElement("input");
+ input.setAttribute("type", "email");
+ return input;
+ },
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ {
+ description: `<input type="search">`,
+ createElement: aDoc => {
+ const input = aDoc.createElement("input");
+ input.setAttribute("type", "search");
+ return input;
+ },
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ {
+ description: `<input type="tel">`,
+ createElement: aDoc => {
+ const input = aDoc.createElement("input");
+ input.setAttribute("type", "tel");
+ return input;
+ },
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ {
+ description: `<input type="number">`,
+ createElement: aDoc => {
+ const input = aDoc.createElement("input");
+ input.setAttribute("type", "number");
+ return input;
+ },
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ {
+ description: `<textarea></textarea>`,
+ createElement: aDoc => aDoc.createElement("textarea"),
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ },
+ ];
+
+ static get numberOfTextControlTypes() {
+ return IMEStateOnReadonlyChangeTester.#sTextControlTypes.length;
+ }
+
+ // Only runner fields
+ #mTextControl;
+ #mTextControlElement;
+ #mWindow;
+
+ // Only checker fields
+ #mWindowUtils;
+ #mTIPWrapper;
+
+ clear() {
+ this.#mTextControlElement?.remove();
+ this.#mTIPWrapper?.clearFocusBlurNotifications();
+ this.#mTIPWrapper = null;
+ }
+
+ #flushPendingIMENotifications() {
+ return new Promise(resolve =>
+ this.#mWindow.requestAnimationFrame(() =>
+ this.#mWindow.requestAnimationFrame(resolve)
+ )
+ );
+ }
+
+ /**
+ * @param {number} aIndex The index of text control types.
+ * @param {Window} aWindow The window running tests.
+ * @param {Element} aContainer The element which should have new <input> or <textarea>.
+ * @returns {object} Expected data before running tests.
+ */
+ async prepareToRun(aIndex, aWindow, aContainer) {
+ this.#mWindow = aWindow;
+ this.#mTextControl =
+ IMEStateOnReadonlyChangeTester.#sTextControlTypes[aIndex];
+
+ if (aContainer.ownerDocument.activeElement) {
+ aContainer.ownerDocument.activeElement.blur();
+ await this.#flushPendingIMENotifications();
+ }
+ if (this.#mTextControlElement?.isConnected) {
+ this.#mTextControlElement.remove();
+ }
+ this.#mTextControlElement = this.#mTextControl.createElement(
+ aContainer.ownerDocument
+ );
+
+ const waitForFocus = new Promise(resolve =>
+ this.#mTextControlElement.addEventListener("focus", resolve, {
+ once: true,
+ })
+ );
+ aContainer.appendChild(this.#mTextControlElement);
+ this.#mTextControlElement.focus();
+ await waitForFocus;
+
+ await this.#flushPendingIMENotifications();
+
+ return {
+ description: this.#mTextControl.description,
+ expectedIMEState: this.#mTextControl.expectedIMEState,
+ expectedIMEFocus: true,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedData The expected data which was returned by prepareToRun().
+ * @param {TIPWrapper} aTIPWrapper TIP wrapper in aWindow.
+ * @param {Window} aWindow [optional] The window object of which you want to check IME state.
+ */
+ checkBeforeRun(aExpectedData, aTIPWrapper, aWindow = window) {
+ this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
+ this.#mTIPWrapper = aTIPWrapper;
+ const description = `IMEStateOnReadonlyChangeTester(${aExpectedData.description})`;
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.expectedIMEState,
+ `${description}: IME state should be set to expected value before setting readonly attribute`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedData.expectedIMEFocus,
+ `${description}: IME state should ${
+ aExpectedData.expectedIMEFocus ? "" : "not"
+ } have focus before setting readonly attribute`
+ );
+ }
+
+ /**
+ * @returns {object} Expected result.
+ */
+ async runToMakeTextControlReadonly() {
+ this.#mTextControlElement.setAttribute("readonly", "");
+
+ await this.#flushPendingIMENotifications();
+
+ return {
+ description: this.#mTextControl.description,
+ expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ expectedIMEFocus: false,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedData Expected result which is returned by runToMakeTextControlReadonly().
+ */
+ checkResultOfMakingTextControlReadonly(aExpectedData) {
+ const description = `IMEStateOnReadonlyChangeTester(${aExpectedData.description})`;
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.expectedIMEState,
+ `${description}: IME state should be set to expected value after setting readonly attribute`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedData.expectedIMEFocus,
+ `${description}: IME state should ${
+ aExpectedData.expectedIMEFocus ? "" : "not"
+ } have focus after setting readonly attribute`
+ );
+ }
+
+ /**
+ * @returns {object} Expected result.
+ */
+ async runToMakeTextControlEditable() {
+ this.#mTextControlElement.removeAttribute("readonly");
+
+ await this.#flushPendingIMENotifications();
+
+ return {
+ description: this.#mTextControl.description,
+ expectedIMEState: this.#mTextControl.expectedIMEState,
+ expectedIMEFocus: true,
+ };
+ }
+
+ /**
+ * @param {object} aExpectedData Expected result which is returned by runToMakeTextControlEditable().
+ */
+ checkResultOfMakingTextControlEditable(aExpectedData) {
+ const description = `IMEStateOnReadonlyChangeTester(${aExpectedData.description})`;
+ is(
+ this.#mWindowUtils.IMEStatus,
+ aExpectedData.expectedIMEState,
+ `${description}: IME state should be set to expected value after removing readonly attribute`
+ );
+ is(
+ this.#mTIPWrapper.IMEHasFocus,
+ aExpectedData.expectedIMEFocus,
+ `${description}: IME state should ${
+ aExpectedData.expectedIMEFocus ? "" : "not"
+ } have focus after removing readonly attribute`
+ );
+ }
+}
diff --git a/widget/tests/gtest/MockWinWidget.cpp b/widget/tests/gtest/MockWinWidget.cpp
new file mode 100644
index 0000000000..ed7850076f
--- /dev/null
+++ b/widget/tests/gtest/MockWinWidget.cpp
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "MockWinWidget.h"
+
+#include "mozilla/gfx/Logging.h"
+
+NS_IMPL_ISUPPORTS_INHERITED0(MockWinWidget, nsBaseWidget)
+
+// static
+RefPtr<MockWinWidget> MockWinWidget::Create(DWORD aStyle, DWORD aExStyle,
+ const LayoutDeviceIntRect& aRect) {
+ RefPtr<MockWinWidget> window = new MockWinWidget;
+ if (!window->Initialize(aStyle, aExStyle, aRect)) {
+ return nullptr;
+ }
+
+ return window;
+}
+
+MockWinWidget::MockWinWidget() {}
+
+MockWinWidget::~MockWinWidget() {
+ if (mWnd) {
+ ::DestroyWindow(mWnd);
+ mWnd = 0;
+ }
+}
+
+bool MockWinWidget::Initialize(DWORD aStyle, DWORD aExStyle,
+ const LayoutDeviceIntRect& aRect) {
+ WNDCLASSW wc;
+ const wchar_t className[] = L"MozillaMockWinWidget";
+ HMODULE hSelf = ::GetModuleHandle(nullptr);
+
+ if (!GetClassInfoW(hSelf, className, &wc)) {
+ ZeroMemory(&wc, sizeof(WNDCLASSW));
+ wc.hInstance = hSelf;
+ wc.lpfnWndProc = ::DefWindowProc;
+ wc.lpszClassName = className;
+ RegisterClassW(&wc);
+ }
+
+ mWnd = ::CreateWindowExW(aExStyle, className, className, aStyle, aRect.X(),
+ aRect.Y(), aRect.Width(), aRect.Height(), nullptr,
+ nullptr, hSelf, nullptr);
+ if (!mWnd) {
+ gfxCriticalNoteOnce << "GetClientRect failed " << ::GetLastError();
+ return false;
+ }
+
+ // First nccalcszie (during CreateWindow) for captioned windows is
+ // deliberately ignored so force a second one here to get the right
+ // non-client set up.
+ if (mWnd && (aStyle & WS_CAPTION)) {
+ ::SetWindowPos(mWnd, NULL, 0, 0, 0, 0,
+ SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
+ SWP_NOACTIVATE | SWP_NOREDRAW);
+ }
+
+ mBounds = aRect;
+ return true;
+}
+
+void MockWinWidget::NotifyOcclusionState(
+ mozilla::widget::OcclusionState aState) {
+ mCurrentState = aState;
+}
+
+nsSizeMode MockWinWidget::SizeMode() {
+ if (::IsIconic(mWnd)) {
+ return nsSizeMode_Minimized;
+ }
+ return nsSizeMode_Normal;
+}
diff --git a/widget/tests/gtest/MockWinWidget.h b/widget/tests/gtest/MockWinWidget.h
new file mode 100644
index 0000000000..891e1c942d
--- /dev/null
+++ b/widget/tests/gtest/MockWinWidget.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef GTEST_MockWinWidget_H
+#define GTEST_MockWinWidget_H
+
+#include <windows.h>
+
+#include "nsBaseWidget.h"
+#include "Units.h"
+
+class MockWinWidget : public nsBaseWidget {
+ public:
+ static RefPtr<MockWinWidget> Create(DWORD aStyle, DWORD aExStyle,
+ const LayoutDeviceIntRect& aRect);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ void NotifyOcclusionState(mozilla::widget::OcclusionState aState) override;
+
+ void SetExpectation(mozilla::widget::OcclusionState aExpectation) {
+ mExpectation = aExpectation;
+ }
+
+ bool IsExpectingCall() const { return mExpectation != mCurrentState; }
+
+ HWND GetWnd() { return mWnd; }
+
+ nsSizeMode SizeMode() override;
+ void SetSizeMode(nsSizeMode aMode) override {}
+
+ void* GetNativeData(uint32_t aDataType) override { return nullptr; }
+
+ virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* aInitData = nullptr) override {
+ return NS_OK;
+ }
+ virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const DesktopIntRect& aRect,
+ InitData* aInitData = nullptr) override {
+ return NS_OK;
+ }
+ virtual void Show(bool aState) override {}
+ virtual bool IsVisible() const override { return true; }
+ virtual void Move(double aX, double aY) override {}
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override {}
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override {}
+
+ virtual void Enable(bool aState) override {}
+ virtual bool IsEnabled() const override { return true; }
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override {}
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override {}
+ virtual nsresult SetTitle(const nsAString& title) override { return NS_OK; }
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override {
+ return LayoutDeviceIntPoint(0, 0);
+ }
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override {
+ return NS_OK;
+ }
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override {}
+ virtual InputContext GetInputContext() override { abort(); }
+
+ private:
+ MockWinWidget();
+ ~MockWinWidget();
+
+ bool Initialize(DWORD aStyle, DWORD aExStyle,
+ const LayoutDeviceIntRect& aRect);
+
+ HWND mWnd = 0;
+
+ mozilla::widget::OcclusionState mExpectation =
+ mozilla::widget::OcclusionState::UNKNOWN;
+ mozilla::widget::OcclusionState mCurrentState =
+ mozilla::widget::OcclusionState::UNKNOWN;
+};
+
+#endif
diff --git a/widget/tests/gtest/TestTimeConverter.cpp b/widget/tests/gtest/TestTimeConverter.cpp
new file mode 100644
index 0000000000..22cbc3f9e6
--- /dev/null
+++ b/widget/tests/gtest/TestTimeConverter.cpp
@@ -0,0 +1,265 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+#include "mozilla/TimeStamp.h"
+#include "SystemTimeConverter.h"
+
+using mozilla::SystemTimeConverter;
+using mozilla::TimeDuration;
+using mozilla::TimeStamp;
+
+namespace {
+
+// This class provides a mock implementation of the CurrentTimeGetter template
+// type used in SystemTimeConverter. It can be constructed with a particular
+// Time and always returns that Time.
+template <typename Time>
+class MockCurrentTimeGetter {
+ public:
+ MockCurrentTimeGetter() : mTime(0) {}
+ explicit MockCurrentTimeGetter(Time aTime) : mTime(aTime) {}
+
+ // Methods needed for CurrentTimeGetter compatibility
+ Time GetCurrentTime() const { return mTime; }
+ void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) {}
+
+ private:
+ Time mTime;
+};
+
+// This is another mock implementation of the CurrentTimeGetter template
+// type used in SystemTimeConverter, except this asserts that it will not be
+// used. i.e. it should only be used in calls to SystemTimeConverter that we
+// know will not invoke it.
+template <typename Time>
+class UnusedCurrentTimeGetter {
+ public:
+ Time GetCurrentTime() const {
+ EXPECT_TRUE(false);
+ return 0;
+ }
+
+ void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) {
+ EXPECT_TRUE(false);
+ }
+};
+
+// This class provides a mock implementation of the TimeStampNowProvider
+// template type used in SystemTimeConverter. It also has other things in it
+// that allow the test to better control time for testing purposes.
+class MockTimeStamp {
+ public:
+ // This should generally be called at the start of every test function, as
+ // it will initialize this class's static fields to sane values. In particular
+ // it will initialize the baseline TimeStamp against which all other
+ // TimeStamps are compared.
+ static void Init() {
+ sBaseline = TimeStamp::Now();
+ sTimeStamp = sBaseline;
+ }
+
+ // Advance the timestamp returned by `MockTimeStamp::Now()`
+ static void Advance(double ms) {
+ sTimeStamp += TimeDuration::FromMilliseconds(ms);
+ }
+
+ // Returns the baseline TimeStamp, that is used as a fixed reference point
+ // in time against which other TimeStamps can be compared. This is needed
+ // because mozilla::TimeStamp itself doesn't provide any conversion to
+ // human-readable strings, and we need to convert it to a TimeDuration in
+ // order to get that. This baseline TimeStamp can be used to turn an
+ // arbitrary TimeStamp into a TimeDuration.
+ static TimeStamp Baseline() { return sBaseline; }
+
+ // This is the method needed for TimeStampNowProvider compatibility, and
+ // simulates `TimeStamp::Now()`
+ static TimeStamp Now() { return sTimeStamp; }
+
+ private:
+ static TimeStamp sTimeStamp;
+ static TimeStamp sBaseline;
+};
+
+TimeStamp MockTimeStamp::sTimeStamp;
+TimeStamp MockTimeStamp::sBaseline;
+
+// Could have platform-specific implementations of this using DWORD, guint32,
+// etc behind ifdefs. But this is sufficient for now.
+using GTestTime = uint32_t;
+using TimeConverter = SystemTimeConverter<GTestTime, MockTimeStamp>;
+
+} // namespace
+
+// Checks the expectation that the TimeStamp `ts` is exactly `ms` milliseconds
+// after the baseline timestamp. This is a macro so gtest still gives us useful
+// line numbers for failures.
+#define EXPECT_TS(ts, ms) \
+ EXPECT_EQ((ts)-MockTimeStamp::Baseline(), TimeDuration::FromMilliseconds(ms))
+
+#define EXPECT_TS_FUZZY(ts, ms) \
+ EXPECT_DOUBLE_EQ(((ts)-MockTimeStamp::Baseline()).ToMilliseconds(), ms)
+
+TEST(TimeConverter, SanityCheck)
+{
+ MockTimeStamp::Init();
+
+ MockCurrentTimeGetter timeGetter(10);
+ UnusedCurrentTimeGetter<GTestTime> unused;
+ TimeConverter converter;
+
+ // This call sets the reference time and timestamp
+ TimeStamp ts = converter.GetTimeStampFromSystemTime(10, timeGetter);
+ EXPECT_TS(ts, 0);
+
+ // Advance "TimeStamp::Now" by 10ms, use the same event time and OS time.
+ // Since the event time is the same as before, we expect to get back the
+ // same TimeStamp as before too, despite Now() changing.
+ MockTimeStamp::Advance(10);
+ ts = converter.GetTimeStampFromSystemTime(10, unused);
+ EXPECT_TS(ts, 0);
+
+ // Now let's use an event time 20ms after the old event. This will trigger
+ // forward skew detection and resync the TimeStamp for the new event to Now().
+ ts = converter.GetTimeStampFromSystemTime(30, unused);
+ EXPECT_TS(ts, 10);
+}
+
+TEST(TimeConverter, Overflow)
+{
+ // This tests wrapping time around the max value supported in the GTestTime
+ // type and ensuring it is handled properly.
+
+ MockTimeStamp::Init();
+
+ const GTestTime max = std::numeric_limits<GTestTime>::max();
+ const GTestTime min = std::numeric_limits<GTestTime>::min();
+ double fullRange = (double)max - (double)min;
+ double wrapPeriod = fullRange + 1.0;
+
+ GTestTime almostOverflowed = max - 100;
+ GTestTime overflowed = max + 100;
+ MockCurrentTimeGetter timeGetter(almostOverflowed);
+ UnusedCurrentTimeGetter<GTestTime> unused;
+ TimeConverter converter;
+
+ // Set reference time to 100ms before the overflow point
+ TimeStamp ts =
+ converter.GetTimeStampFromSystemTime(almostOverflowed, timeGetter);
+ EXPECT_TS(ts, 0);
+
+ // Advance everything by 200ms and verify we get back a TimeStamp 200ms from
+ // the baseline despite wrapping an overflow.
+ MockTimeStamp::Advance(200);
+ ts = converter.GetTimeStampFromSystemTime(overflowed, unused);
+ EXPECT_TS(ts, 200);
+
+ // Advance by another full wraparound of the time. This loses some precision
+ // so we have to do the FUZZY match
+ MockTimeStamp::Advance(wrapPeriod);
+ ts = converter.GetTimeStampFromSystemTime(overflowed, unused);
+ EXPECT_TS_FUZZY(ts, 200.0 + wrapPeriod);
+}
+
+TEST(TimeConverter, InvertedOverflow)
+{
+ // This tests time going from near the min value of GTestTime to the max
+ // value of GTestTime
+
+ MockTimeStamp::Init();
+
+ const GTestTime max = std::numeric_limits<GTestTime>::max();
+ const GTestTime min = std::numeric_limits<GTestTime>::min();
+ double fullRange = (double)max - (double)min;
+ double wrapPeriod = fullRange + 1.0;
+
+ GTestTime nearRangeMin = min + 100;
+ GTestTime nearRangeMax = max - 100;
+ double gap = (double)nearRangeMax - (double)nearRangeMin;
+
+ MockCurrentTimeGetter timeGetter(nearRangeMin);
+ UnusedCurrentTimeGetter<GTestTime> unused;
+ TimeConverter converter;
+
+ // Set reference time to value near min numeric limit
+ TimeStamp ts = converter.GetTimeStampFromSystemTime(nearRangeMin, timeGetter);
+ EXPECT_TS(ts, 0);
+
+ // Advance to value near max numeric limit
+ MockTimeStamp::Advance(gap);
+ ts = converter.GetTimeStampFromSystemTime(nearRangeMax, unused);
+ EXPECT_TS(ts, gap);
+
+ // Advance by another full wraparound of the time. This loses some precision
+ // so we have to do the FUZZY match
+ MockTimeStamp::Advance(wrapPeriod);
+ ts = converter.GetTimeStampFromSystemTime(nearRangeMax, unused);
+ EXPECT_TS_FUZZY(ts, gap + wrapPeriod);
+}
+
+TEST(TimeConverter, HalfRangeBoundary)
+{
+ MockTimeStamp::Init();
+
+ GTestTime max = std::numeric_limits<GTestTime>::max();
+ GTestTime min = std::numeric_limits<GTestTime>::min();
+ double fullRange = (double)max - (double)min;
+ double wrapPeriod = fullRange + 1.0;
+ GTestTime halfRange = (GTestTime)(fullRange / 2.0);
+ GTestTime halfWrapPeriod = (GTestTime)(wrapPeriod / 2.0);
+
+ TimeConverter converter;
+
+ GTestTime firstEvent = 10;
+ MockCurrentTimeGetter timeGetter(firstEvent);
+ UnusedCurrentTimeGetter<GTestTime> unused;
+
+ // Set reference time
+ TimeStamp ts = converter.GetTimeStampFromSystemTime(firstEvent, timeGetter);
+ EXPECT_TS(ts, 0);
+
+ // Advance event time by just under the half-period, to trigger about as big
+ // a forwards skew as we possibly can.
+ GTestTime secondEvent = firstEvent + (halfWrapPeriod - 1);
+ ts = converter.GetTimeStampFromSystemTime(secondEvent, unused);
+ EXPECT_TS(ts, 0);
+
+ // The above forwards skew will have reset the reference timestamp. Now
+ // advance Now time by just under the half-range, to trigger about as big
+ // a backwards skew as we possibly can.
+ MockTimeStamp::Advance(halfRange - 1);
+ ts = converter.GetTimeStampFromSystemTime(secondEvent, unused);
+ EXPECT_TS(ts, 0);
+}
+
+TEST(TimeConverter, FractionalMillisBug1626734)
+{
+ MockTimeStamp::Init();
+
+ TimeConverter converter;
+
+ GTestTime eventTime = 10;
+ MockCurrentTimeGetter timeGetter(eventTime);
+ UnusedCurrentTimeGetter<GTestTime> unused;
+
+ TimeStamp ts = converter.GetTimeStampFromSystemTime(eventTime, timeGetter);
+ EXPECT_TS(ts, 0);
+
+ MockTimeStamp::Advance(0.2);
+ ts = converter.GetTimeStampFromSystemTime(eventTime, unused);
+ EXPECT_TS(ts, 0);
+
+ MockTimeStamp::Advance(0.9);
+ TimeStamp ts2 = converter.GetTimeStampFromSystemTime(eventTime, unused);
+ EXPECT_TS(ts2, 0);
+
+ // Since ts2 came from a "future" call relative to ts, we expect ts2 to not
+ // be "before" ts. (i.e. time shouldn't go backwards, even by fractional
+ // milliseconds). This assertion is technically already implied by the
+ // EXPECT_TS checks above, but fixing this assertion is the end result that
+ // we wanted in bug 1626734 so it feels appropriate to recheck it explicitly.
+ EXPECT_TRUE(ts <= ts2);
+}
diff --git a/widget/tests/gtest/TestTouchResampler.cpp b/widget/tests/gtest/TestTouchResampler.cpp
new file mode 100644
index 0000000000..1a5b8e2430
--- /dev/null
+++ b/widget/tests/gtest/TestTouchResampler.cpp
@@ -0,0 +1,941 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 <initializer_list>
+#include "InputData.h"
+#include "Units.h"
+#include "gtest/gtest.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h"
+#include "TouchResampler.h"
+
+using namespace mozilla;
+using widget::TouchResampler;
+
+class TouchResamplerTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() { baseTimeStamp = TimeStamp::Now(); }
+
+ TimeStamp Time(double aMilliseconds) {
+ return baseTimeStamp + TimeDuration::FromMilliseconds(aMilliseconds);
+ }
+
+ uint64_t ProcessEvent(
+ MultiTouchInput::MultiTouchType aType,
+ std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>>
+ aHistoricalData,
+ const TimeStamp& aTimeStamp, const ScreenIntPoint& aPosition) {
+ MultiTouchInput input(aType, 0, aTimeStamp, 0);
+ input.mTouches.AppendElement(SingleTouchData(1, aPosition, {}, 0.0f, 0.0f));
+ for (const auto& histData : aHistoricalData) {
+ input.mTouches[0].mHistoricalData.AppendElement(
+ SingleTouchData::HistoricalTouchData{
+ histData.first, histData.second, {}, {}, 0.0f, 0.0f});
+ }
+ return resampler.ProcessEvent(std::move(input));
+ }
+
+ void CheckTime(const TimeStamp& aTimeStamp,
+ const TimeStamp& aExpectedTimeStamp) {
+ EXPECT_EQ((aTimeStamp - baseTimeStamp).ToMilliseconds(),
+ (aExpectedTimeStamp - baseTimeStamp).ToMilliseconds());
+ }
+
+ void CheckEvent(const MultiTouchInput& aEvent,
+ MultiTouchInput::MultiTouchType aExpectedType,
+ std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>>
+ aExpectedHistoricalData,
+ const TimeStamp& aExpectedTimeStamp,
+ const ScreenIntPoint& aExpectedPosition) {
+ EXPECT_EQ(aEvent.mType, aExpectedType);
+ EXPECT_EQ(aEvent.mTouches.Length(), size_t(1));
+ EXPECT_EQ(aEvent.mTouches[0].mHistoricalData.Length(),
+ aExpectedHistoricalData.size());
+ for (size_t i = 0; i < aExpectedHistoricalData.size(); i++) {
+ CheckTime(aEvent.mTouches[0].mHistoricalData[i].mTimeStamp,
+ aExpectedHistoricalData.begin()[i].first);
+ EXPECT_EQ(aEvent.mTouches[0].mHistoricalData[i].mScreenPoint,
+ aExpectedHistoricalData.begin()[i].second);
+ }
+ CheckTime(aEvent.mTimeStamp, aExpectedTimeStamp);
+ EXPECT_EQ(aEvent.mTouches[0].mScreenPoint, aExpectedPosition);
+ }
+
+ struct ExpectedOutgoingEvent {
+ Maybe<uint64_t> mEventId;
+ MultiTouchInput::MultiTouchType mType = MultiTouchInput::MULTITOUCH_START;
+ std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>> mHistoricalData;
+ TimeStamp mTimeStamp;
+ ScreenIntPoint mPosition;
+ };
+
+ void CheckOutgoingEvents(
+ std::initializer_list<ExpectedOutgoingEvent> aExpectedEvents) {
+ auto outgoing = resampler.ConsumeOutgoingEvents();
+ EXPECT_EQ(outgoing.size(), aExpectedEvents.size());
+ for (const auto& expectedEvent : aExpectedEvents) {
+ auto outgoingEvent = std::move(outgoing.front());
+ outgoing.pop();
+
+ EXPECT_EQ(outgoingEvent.mEventId, expectedEvent.mEventId);
+ CheckEvent(outgoingEvent.mEvent, expectedEvent.mType,
+ expectedEvent.mHistoricalData, expectedEvent.mTimeStamp,
+ expectedEvent.mPosition);
+ }
+ }
+
+ TimeStamp baseTimeStamp;
+ TouchResampler resampler;
+};
+
+TEST_F(TouchResamplerTest, BasicExtrapolation) {
+ // Execute the following sequence:
+ //
+ // 0----------10-------16-----20---------------32------------
+ // * touchstart at (10, 10)
+ // * touchmove at (20, 20)
+ // * frame
+ // * touchend at (20, 20)
+ // * frame
+ //
+ // And expect the following output:
+ //
+ // 0----------10-------16-----20---------------32------------
+ // * touchstart at (10, 10)
+ // * touchmove at (26, 26)
+ // * touchmove at (20, 20)
+ // * touchend at (20, 20)
+ //
+ // The first frame should emit an extrapolated touchmove from the position
+ // data in the touchstart and touchmove events.
+ // The touchend should force a synthesized touchmove that returns back to a
+ // non-resampled position.
+
+ EXPECT_FALSE(resampler.InTouchingState());
+
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(10, 10));
+ EXPECT_TRUE(resampler.InTouchingState());
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0),
+ ScreenIntPoint(20, 20));
+
+ resampler.NotifyFrame(Time(16.0));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(10, 10)},
+
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(20, 20)}},
+ Time(16.0),
+ ScreenIntPoint(26, 26)},
+ });
+
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(20.0),
+ ScreenIntPoint(20, 20));
+
+ EXPECT_FALSE(resampler.InTouchingState());
+
+ CheckOutgoingEvents({
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(16.0),
+ ScreenIntPoint(20, 20)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(20.0),
+ ScreenIntPoint(20, 20)},
+ });
+
+ // No more events should be produced from here on out.
+ resampler.NotifyFrame(Time(32.0));
+ auto outgoing = resampler.ConsumeOutgoingEvents();
+ EXPECT_TRUE(outgoing.empty());
+}
+
+TEST_F(TouchResamplerTest, BasicInterpolation) {
+ // Same test as BasicExtrapolation, but with a frame time that's 10ms earlier.
+ //
+ // Execute the following sequence:
+ //
+ // 0------6---10-----------20--22------------30-------------
+ // * touchstart at (10, 10)
+ // * touchmove at (20, 20)
+ // * frame
+ // * touchend at (20, 20)
+ // * frame
+ //
+ // And expect the following output:
+ //
+ // 0------6---10-----------20--22------------30-------------
+ // * touchstart at (10, 10)
+ // * touchmove (16, 16)
+ // * touchmove (20, 20)
+ // * touchend at (20, 20)
+ //
+ // The first frame should emit an interpolated touchmove from the position
+ // data in the touchstart and touchmove events.
+ // The touchend should create a touchmove that returns back to a non-resampled
+ // position.
+
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(10, 10));
+ EXPECT_TRUE(resampler.InTouchingState());
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0),
+ ScreenIntPoint(20, 20));
+
+ resampler.NotifyFrame(Time(6.0));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(10, 10)},
+
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(6.0),
+ ScreenIntPoint(16, 16)},
+ });
+
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(20.0),
+ ScreenIntPoint(20, 20));
+ EXPECT_FALSE(resampler.InTouchingState());
+
+ CheckOutgoingEvents({
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(10.0),
+ ScreenIntPoint(20, 20)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(20.0),
+ ScreenIntPoint(20, 20)},
+ });
+
+ // No more events should be produced from here on out.
+ resampler.NotifyFrame(Time(22.0));
+ auto outgoing = resampler.ConsumeOutgoingEvents();
+ EXPECT_TRUE(outgoing.empty());
+}
+
+TEST_F(TouchResamplerTest, InterpolationFromHistoricalData) {
+ // Interpolate from the historical data in a touch move event.
+ //
+ // Execute the following sequence:
+ //
+ // 0----------10-------16-----20-----------30--32------------
+ // * touchstart at (10, 10)
+ // * [hist] at (20, 25) for
+ // `---------------* touchmove at (30, 30)
+ // * frame
+ // * touchend at (30, 30)
+ // * frame
+ //
+ // And expect the following output:
+ //
+ // 0----------10-------16-----20-----------30--32------------
+ // * touchstart at (10, 10)
+ // * [hist] at (20, 25) for
+ // `--------* touchmove at (26, 28)
+ // * touchmove at (30, 30)
+ // * touchend at (30, 30)
+ //
+ // The first frame should emit an interpolated touchmove from the position
+ // data in the touchmove event, and integrate the historical data point into
+ // the resampled event.
+ // The touchend should force a synthesized touchmove that returns back to a
+ // non-resampled position.
+
+ // This also tests that interpolation works for both x and y, by giving the
+ // historical datapoint different values for x and y.
+ // (26, 28) is 60% of the way from (20, 25) to (30, 30).
+
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(10, 10));
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(20, 25)}},
+ Time(20.0), ScreenIntPoint(30, 30));
+ resampler.NotifyFrame(Time(16.0));
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(30.0),
+ ScreenIntPoint(30, 30));
+ resampler.NotifyFrame(Time(32.0));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(10, 10)},
+
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(20, 25)}},
+ Time(16.0),
+ ScreenIntPoint(26, 28)},
+
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(20.0),
+ ScreenIntPoint(30, 30)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(30.0),
+ ScreenIntPoint(30, 30)},
+ });
+}
+
+TEST_F(TouchResamplerTest, MultipleTouches) {
+ EXPECT_FALSE(resampler.InTouchingState());
+
+ // Touch start
+ MultiTouchInput inputStart0(MultiTouchInput::MULTITOUCH_START, 0, Time(0.0),
+ 0);
+ inputStart0.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(10, 10), {}, 0.0f, 0.0f));
+ auto idStart0 = resampler.ProcessEvent(std::move(inputStart0));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Touch move
+ MultiTouchInput inputMove1(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(20.0),
+ 0);
+ inputMove1.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 30), {}, 0.0f, 0.0f));
+ inputMove1.mTouches[0].mHistoricalData.AppendElement(
+ SingleTouchData::HistoricalTouchData{
+ Time(10.0), ScreenIntPoint(20, 25), {}, {}, 0.0f, 0.0f});
+ auto idMove1 = resampler.ProcessEvent(std::move(inputMove1));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Frame
+ resampler.NotifyFrame(Time(16.0));
+
+ // Touch move
+ MultiTouchInput inputMove2(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(30.0),
+ 0);
+ inputMove2.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 40), {}, 0.0f, 0.0f));
+ auto idMove2 = resampler.ProcessEvent(std::move(inputMove2));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Touch start
+ MultiTouchInput inputStart3(MultiTouchInput::MULTITOUCH_START, 0, Time(30.0),
+ 0);
+ inputStart3.mTouches.AppendElement(
+ SingleTouchData(2, ScreenIntPoint(100, 10), {}, 0.0f, 0.0f));
+ inputStart3.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 40), {}, 0.0f, 0.0f));
+ auto idStart3 = resampler.ProcessEvent(std::move(inputStart3));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Touch move
+ MultiTouchInput inputMove4(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(40.0),
+ 0);
+ inputMove4.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 50), {}, 0.0f, 0.0f));
+ inputMove4.mTouches.AppendElement(
+ SingleTouchData(2, ScreenIntPoint(100, 30), {}, 0.0f, 0.0f));
+ auto idMove4 = resampler.ProcessEvent(std::move(inputMove4));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Frame
+ resampler.NotifyFrame(Time(32.0));
+
+ // Touch move
+ MultiTouchInput inputMove5(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(50.0),
+ 0);
+ inputMove5.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 60), {}, 0.0f, 0.0f));
+ inputMove5.mTouches.AppendElement(
+ SingleTouchData(2, ScreenIntPoint(100, 40), {}, 0.0f, 0.0f));
+ auto idMove5 = resampler.ProcessEvent(std::move(inputMove5));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Touch end
+ MultiTouchInput inputEnd6(MultiTouchInput::MULTITOUCH_END, 0, Time(50.0), 0);
+ // Touch point with identifier 1 is lifted
+ inputEnd6.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 60), {}, 0.0f, 0.0f));
+ auto idEnd6 = resampler.ProcessEvent(std::move(inputEnd6));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Frame
+ resampler.NotifyFrame(Time(48.0));
+
+ // Touch move
+ MultiTouchInput inputMove7(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(60.0),
+ 0);
+ inputMove7.mTouches.AppendElement(
+ SingleTouchData(2, ScreenIntPoint(100, 60), {}, 0.0f, 0.0f));
+ auto idMove7 = resampler.ProcessEvent(std::move(inputMove7));
+ EXPECT_TRUE(resampler.InTouchingState());
+
+ // Frame
+ resampler.NotifyFrame(Time(64.0));
+
+ // Touch end
+ MultiTouchInput inputEnd8(MultiTouchInput::MULTITOUCH_END, 0, Time(70.0), 0);
+ // Touch point with identifier 2 is lifted
+ inputEnd8.mTouches.AppendElement(
+ SingleTouchData(2, ScreenIntPoint(100, 60), {}, 0.0f, 0.0f));
+ auto idEnd8 = resampler.ProcessEvent(std::move(inputEnd8));
+ EXPECT_FALSE(resampler.InTouchingState());
+
+ // Check outgoing events
+ auto outgoing = resampler.ConsumeOutgoingEvents();
+ EXPECT_EQ(outgoing.size(), size_t(9));
+
+ auto outgoingStart0 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingStart0.mEventId, Some(idStart0));
+ CheckEvent(outgoingStart0.mEvent, MultiTouchInput::MULTITOUCH_START, {},
+ Time(0.0), ScreenIntPoint(10, 10));
+
+ auto outgoingMove1 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingMove1.mEventId, Some(idMove1));
+ // (26, 28) is 60% of the way from (20, 25) to (30, 30).
+ CheckEvent(outgoingMove1.mEvent, MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(20, 25)}}, Time(16.0),
+ ScreenIntPoint(26, 28));
+
+ auto outgoingMove2 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingMove2.mEventId, Some(idMove2));
+ CheckEvent(outgoingMove2.mEvent, MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(30, 30)}}, Time(30.0),
+ ScreenIntPoint(30, 40));
+
+ auto outgoingStart3 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingStart3.mEventId, Some(idStart3));
+ EXPECT_EQ(outgoingStart3.mEvent.mType, MultiTouchInput::MULTITOUCH_START);
+ CheckTime(outgoingStart3.mEvent.mTimeStamp, Time(30.0));
+ EXPECT_EQ(outgoingStart3.mEvent.mTouches.Length(), size_t(2));
+ // touch order should be taken from the original touch start event
+ EXPECT_EQ(outgoingStart3.mEvent.mTouches[0].mIdentifier, 2);
+ EXPECT_EQ(outgoingStart3.mEvent.mTouches[0].mScreenPoint,
+ ScreenIntPoint(100, 10));
+ EXPECT_EQ(outgoingStart3.mEvent.mTouches[1].mIdentifier, 1);
+ EXPECT_EQ(outgoingStart3.mEvent.mTouches[1].mScreenPoint,
+ ScreenIntPoint(30, 40));
+
+ auto outgoingMove4 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingMove4.mEventId, Some(idMove4));
+ EXPECT_EQ(outgoingMove4.mEvent.mType, MultiTouchInput::MULTITOUCH_MOVE);
+ CheckTime(outgoingMove4.mEvent.mTimeStamp, Time(32.0));
+ EXPECT_EQ(outgoingMove4.mEvent.mTouches.Length(), size_t(2));
+ // Touch order should be taken from the original touch move event.
+ // Both touches should be resampled.
+ EXPECT_EQ(outgoingMove4.mEvent.mTouches[0].mIdentifier, 1);
+ EXPECT_EQ(outgoingMove4.mEvent.mTouches[0].mScreenPoint,
+ ScreenIntPoint(30, 42));
+ EXPECT_EQ(outgoingMove4.mEvent.mTouches[1].mIdentifier, 2);
+ EXPECT_EQ(outgoingMove4.mEvent.mTouches[1].mScreenPoint,
+ ScreenIntPoint(100, 14));
+
+ auto outgoingMove5 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingMove5.mEventId, Some(idMove5));
+ EXPECT_EQ(outgoingMove5.mEvent.mType, MultiTouchInput::MULTITOUCH_MOVE);
+ CheckTime(outgoingMove5.mEvent.mTimeStamp, Time(50.0));
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches.Length(), size_t(2));
+ // touch order should be taken from the original touch move event
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mIdentifier, 1);
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mScreenPoint,
+ ScreenIntPoint(30, 60));
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mHistoricalData.Length(),
+ size_t(1));
+ CheckTime(outgoingMove5.mEvent.mTouches[0].mHistoricalData[0].mTimeStamp,
+ Time(40.0));
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mHistoricalData[0].mScreenPoint,
+ ScreenIntPoint(30, 50));
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mIdentifier, 2);
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mScreenPoint,
+ ScreenIntPoint(100, 40));
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mHistoricalData.Length(),
+ size_t(1));
+ CheckTime(outgoingMove5.mEvent.mTouches[1].mHistoricalData[0].mTimeStamp,
+ Time(40.0));
+ EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mHistoricalData[0].mScreenPoint,
+ ScreenIntPoint(100, 30));
+
+ auto outgoingEnd6 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingEnd6.mEventId, Some(idEnd6));
+ CheckEvent(outgoingEnd6.mEvent, MultiTouchInput::MULTITOUCH_END, {},
+ Time(50.0), ScreenIntPoint(30, 60));
+
+ auto outgoingMove7 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingMove7.mEventId, Some(idMove7));
+ // No extrapolation because the frame at 64.0 cleared the data points because
+ // there was no pending touch move event at that point
+ CheckEvent(outgoingMove7.mEvent, MultiTouchInput::MULTITOUCH_MOVE, {},
+ Time(60.0), ScreenIntPoint(100, 60));
+ EXPECT_EQ(outgoingMove7.mEvent.mTouches[0].mIdentifier, 2);
+
+ auto outgoingEnd8 = std::move(outgoing.front());
+ outgoing.pop();
+ EXPECT_EQ(outgoingEnd8.mEventId, Some(idEnd8));
+ CheckEvent(outgoingEnd8.mEvent, MultiTouchInput::MULTITOUCH_END, {},
+ Time(70.0), ScreenIntPoint(100, 60));
+}
+
+TEST_F(TouchResamplerTest, MovingPauses) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(10, 10));
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0),
+ ScreenIntPoint(20, 20));
+ resampler.NotifyFrame(Time(16.0));
+ auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(30.0),
+ ScreenIntPoint(40, 40));
+ resampler.NotifyFrame(Time(32.0));
+ auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0),
+ ScreenIntPoint(50, 40));
+ resampler.NotifyFrame(Time(48.0));
+ resampler.NotifyFrame(Time(64.0));
+ auto idEnd4 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(50, 40));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(10, 10)},
+
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(20, 20)}},
+ Time(16.0),
+ ScreenIntPoint(26, 26)},
+
+ {Some(idMove2),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(30.0), ScreenIntPoint(40, 40)}},
+ Time(32.0),
+ ScreenIntPoint(42, 42)},
+
+ {Some(idMove3),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(40.0), ScreenIntPoint(50, 40)}},
+ Time(48.0),
+ ScreenIntPoint(58, 40)},
+
+ // There was no event between two frames here, so we expect a reset event,
+ // so that we pause at a non-resampled position.
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(48.0),
+ ScreenIntPoint(50, 40)},
+
+ {Some(idEnd4),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(50, 40)},
+ });
+}
+
+TEST_F(TouchResamplerTest, MixedInterAndExtrapolation) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0),
+ ScreenIntPoint(0, 10));
+ resampler.NotifyFrame(Time(11.0)); // 16 - 5
+ auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)}}, Time(30.0),
+ ScreenIntPoint(0, 30));
+ resampler.NotifyFrame(Time(27.0)); // 32 - 5
+ auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0),
+ ScreenIntPoint(0, 40));
+ resampler.NotifyFrame(Time(43.0)); // 48 - 5
+ auto idMove4 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(50.0), ScreenIntPoint(0, 50)}}, Time(60.0),
+ ScreenIntPoint(0, 60));
+ resampler.NotifyFrame(Time(59.0)); // 64 - 5
+ auto idEnd5 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(0, 60));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)}},
+ Time(11.0),
+ ScreenIntPoint(0, 11)},
+
+ {Some(idMove2),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)}},
+ Time(27.0),
+ ScreenIntPoint(0, 27)},
+
+ {Some(idMove3),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(30.0), ScreenIntPoint(0, 30)},
+ {Time(40.0), ScreenIntPoint(0, 40)}},
+ Time(43.0),
+ ScreenIntPoint(0, 43)},
+
+ {Some(idMove4),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(50.0), ScreenIntPoint(0, 50)}},
+ Time(59.0),
+ ScreenIntPoint(0, 59)},
+
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(60.0),
+ ScreenIntPoint(0, 60)},
+
+ {Some(idEnd5),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(0, 60)},
+ });
+}
+
+TEST_F(TouchResamplerTest, MultipleMoveEvents) {
+ // Test what happens if multiple touch move events appear between two frames.
+ // This scenario shouldn't occur on Android but we should be able to deal with
+ // it anyway. Check that we don't discard any event IDs.
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0),
+ ScreenIntPoint(0, 10));
+ resampler.NotifyFrame(Time(11.0)); // 16 - 5
+ auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)}}, Time(30.0),
+ ScreenIntPoint(0, 30));
+ auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0),
+ ScreenIntPoint(0, 40));
+ auto idMove4 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(45.0), ScreenIntPoint(0, 45)}}, Time(50.0),
+ ScreenIntPoint(0, 50));
+ auto idMove5 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(55.0),
+ ScreenIntPoint(0, 55));
+ resampler.NotifyFrame(Time(43.0)); // 48 - 5
+ resampler.NotifyFrame(Time(59.0)); // 64 - 5
+ auto idEnd5 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(0, 60));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)}},
+ Time(11.0),
+ ScreenIntPoint(0, 11)},
+
+ {Some(idMove2),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)}},
+ Time(30.0),
+ ScreenIntPoint(0, 30)},
+
+ {Some(idMove3),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(40.0),
+ ScreenIntPoint(0, 40)},
+
+ {Some(idMove4),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(43.0),
+ ScreenIntPoint(0, 43)},
+
+ {Some(idMove5),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(45.0), ScreenIntPoint(0, 45)},
+ {Time(50.0), ScreenIntPoint(0, 50)},
+ {Time(55.0), ScreenIntPoint(0, 55)}},
+ Time(59.0),
+ ScreenIntPoint(0, 59)},
+
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(59.0),
+ ScreenIntPoint(0, 55)},
+
+ {Some(idEnd5),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(0, 60)},
+ });
+}
+
+TEST_F(TouchResamplerTest, LimitFuturePrediction) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ // Fingers move until time 44, then pause. UI thread is occupied until 64.
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)},
+ {Time(32.0), ScreenIntPoint(0, 32)}},
+ Time(44.0), ScreenIntPoint(0, 44));
+ resampler.NotifyFrame(Time(59.0)); // 64 - 5
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(0, 44));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ // kTouchResampleMaxPredictMs == 8
+ // Refuse to predict more than 8ms into the future, the fingers might have
+ // paused. Make an event for time 52 (= 44 + 8) instead of 59.
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)},
+ {Time(32.0), ScreenIntPoint(0, 32)},
+ {Time(44.0), ScreenIntPoint(0, 44)}},
+ Time(52.0),
+ ScreenIntPoint(0, 52)},
+
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(52.0),
+ ScreenIntPoint(0, 44)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(0, 44)},
+ });
+}
+
+TEST_F(TouchResamplerTest, LimitBacksampling) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ // Fingers move until time 44, then pause. UI thread is occupied until 64.
+ // Then we get a frame callback with a very outdated frametime.
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)},
+ {Time(32.0), ScreenIntPoint(0, 32)}},
+ Time(44.0), ScreenIntPoint(0, 44));
+ resampler.NotifyFrame(Time(11.0)); // 16 - 5
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(0, 44));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ // kTouchResampleMaxBacksampleMs == 20
+ // Refuse to sample further back than 20ms before the last data point.
+ // Make an event for time 24 (= 44 - 20) instead of time 11.
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)}},
+ Time(24.0),
+ ScreenIntPoint(0, 24)},
+
+ {Nothing(),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(32.0), ScreenIntPoint(0, 32)}},
+ Time(44.0),
+ ScreenIntPoint(0, 44)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(0, 44)},
+ });
+}
+
+TEST_F(TouchResamplerTest, DontExtrapolateFromOldTouch) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ // Fingers move until time 40, then pause. UI thread is occupied until 64.
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)},
+ {Time(30.0), ScreenIntPoint(0, 30)}},
+ Time(40.0), ScreenIntPoint(0, 40));
+ resampler.NotifyFrame(Time(59.0)); // 64 - 5
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(0, 44));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ // kTouchResampleOldTouchThresholdMs == 17
+ // Refuse to extrapolate from a data point that's more than 17ms older
+ // than the frame time.
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(20.0), ScreenIntPoint(0, 20)},
+ {Time(30.0), ScreenIntPoint(0, 30)}},
+ Time(40.0),
+ ScreenIntPoint(0, 40)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(0, 44)},
+ });
+}
+
+TEST_F(TouchResamplerTest, DontExtrapolateIfTooOld) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ // Fingers move until time 10, pause, and move again at 55.
+ // UI thread is occupied until 64.
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(55.0),
+ ScreenIntPoint(0, 55));
+ resampler.NotifyFrame(Time(59.0)); // 64 - 5
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(0, 60));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ // kTouchResampleWindowSize == 40
+ // Refuse to resample between two data points that are more than 40ms
+ // apart.
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)}},
+ Time(55.0),
+ ScreenIntPoint(0, 55)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(0, 60)},
+ });
+}
+
+TEST_F(TouchResamplerTest, DontInterpolateIfTooOld) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ // Fingers move until time 10, pause, and move again at 60.
+ // UI thread is occupied until 64.
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(60.0),
+ ScreenIntPoint(0, 60));
+ resampler.NotifyFrame(Time(59.0)); // 64 - 5
+ auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0),
+ ScreenIntPoint(0, 60));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ // kTouchResampleWindowSize == 40
+ // Refuse to resample between two data points that are more than 40ms
+ // apart.
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)}},
+ Time(60.0),
+ ScreenIntPoint(0, 60)},
+
+ {Some(idEnd2),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(70.0),
+ ScreenIntPoint(0, 60)},
+ });
+}
+
+TEST_F(TouchResamplerTest, DiscardOutdatedHistoricalData) {
+ auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0),
+ ScreenIntPoint(0, 0));
+ auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(16.0),
+ ScreenIntPoint(0, 16));
+ resampler.NotifyFrame(Time(20.0));
+ auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(18.0), ScreenIntPoint(0, 18)}}, Time(25.0),
+ ScreenIntPoint(0, 25));
+ auto idEnd3 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(35.0),
+ ScreenIntPoint(0, 25));
+
+ CheckOutgoingEvents({
+ {Some(idStart0),
+ MultiTouchInput::MULTITOUCH_START,
+ {},
+ Time(0.0),
+ ScreenIntPoint(0, 0)},
+
+ {Some(idMove1),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {{Time(10.0), ScreenIntPoint(0, 10)},
+ {Time(16.0), ScreenIntPoint(0, 16)}},
+ Time(20.0),
+ ScreenIntPoint(0, 20)},
+
+ // Discard the historical data point from time 18, because we've already
+ // sent out an event with time 20 and don't want to go back before that.
+ {Some(idMove2),
+ MultiTouchInput::MULTITOUCH_MOVE,
+ {},
+ Time(25.0),
+ ScreenIntPoint(0, 25)},
+
+ {Some(idEnd3),
+ MultiTouchInput::MULTITOUCH_END,
+ {},
+ Time(35.0),
+ ScreenIntPoint(0, 25)},
+ });
+}
diff --git a/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp b/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp
new file mode 100644
index 0000000000..5ac361fea0
--- /dev/null
+++ b/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+#include <shlwapi.h>
+
+TEST(WinHeaderOnlyUtils, MozPathGetDriveNumber)
+{
+ constexpr auto TestPathGetDriveNumber = [](const wchar_t* aPath) {
+ return mozilla::MozPathGetDriveNumber(aPath) ==
+ ::PathGetDriveNumberW(aPath);
+ };
+ EXPECT_TRUE(TestPathGetDriveNumber(nullptr));
+ EXPECT_TRUE(TestPathGetDriveNumber(L""));
+ EXPECT_TRUE(TestPathGetDriveNumber(L" :"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"a:"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"C:\\file"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"x:/file"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"@:\\\\"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"B"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"abc:\\file"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\:A"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\\\"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\\\"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\c:\\"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\A"));
+ EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\ \\"));
+}
diff --git a/widget/tests/gtest/TestWinMessageLoggingUtils.cpp b/widget/tests/gtest/TestWinMessageLoggingUtils.cpp
new file mode 100644
index 0000000000..b0ab09f866
--- /dev/null
+++ b/widget/tests/gtest/TestWinMessageLoggingUtils.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+#include "windows/nsWindowDbg.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_CombinationFlagsHandledFirst)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x3, "COMBO"}, {0x1, "ONE"}, {0x2, "TWO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x3, flags, nullptr));
+ EXPECT_STREQ("COMBO", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_SingleFlag)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x4, flags, nullptr));
+ EXPECT_STREQ("FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_SingleFlagWithName)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x4, flags, "paramName"));
+ EXPECT_STREQ("paramName=FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_MultipleFlags)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x5, flags, nullptr));
+ EXPECT_STREQ("ONE|FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NoFlags)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, nullptr));
+ EXPECT_STREQ("Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_OnlySomeFlagsMatch)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x9, flags, nullptr));
+ EXPECT_STREQ("ONE|Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_FlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x2, flags, nullptr));
+ EXPECT_STREQ("TWO", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NoFlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, nullptr));
+ EXPECT_STREQ("Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NameAndNoFlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, "paramName"));
+ EXPECT_STREQ("paramName=Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_ValueIsZero_ZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x0, flags, nullptr));
+ EXPECT_STREQ("ZERO", result.get());
+}
diff --git a/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp b/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp
new file mode 100644
index 0000000000..5ef7e3f81c
--- /dev/null
+++ b/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+
+#include <dwmapi.h>
+#include <windows.h>
+
+#include "MockWinWidget.h"
+#include "mozilla/widget/WinWindowOcclusionTracker.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+class WinWindowOcclusionTrackerTest : public ::testing::Test {
+ protected:
+ HWND CreateNativeWindow(DWORD aStyle, DWORD aExStyle) {
+ mMockWinWidget =
+ MockWinWidget::Create(WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | aStyle,
+ aExStyle, LayoutDeviceIntRect(0, 0, 100, 100));
+ EXPECT_NE(nullptr, mMockWinWidget.get());
+ HWND hwnd = mMockWinWidget->GetWnd();
+ HRGN region = ::CreateRectRgn(0, 0, 0, 0);
+ EXPECT_NE(nullptr, region);
+ if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) {
+ // On Windows 7, the newly created window has a complex region, which
+ // means it will be ignored during the occlusion calculation. So, force
+ // it to have a simple region so that we get test coverage on win 7.
+ RECT boundingRect;
+ EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect));
+ HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect);
+ EXPECT_NE(nullptr, rectangularRegion);
+ ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE);
+ ::DeleteObject(rectangularRegion);
+ }
+ ::DeleteObject(region);
+
+ ::ShowWindow(hwnd, SW_SHOWNORMAL);
+ EXPECT_TRUE(UpdateWindow(hwnd));
+ return hwnd;
+ }
+
+ // Wrapper around IsWindowVisibleAndFullyOpaque so only the test class
+ // needs to be a friend of NativeWindowOcclusionTrackerWin.
+ bool CheckWindowVisibleAndFullyOpaque(HWND aHWnd,
+ LayoutDeviceIntRect* aWinRect) {
+ bool ret = WinWindowOcclusionTracker::IsWindowVisibleAndFullyOpaque(
+ aHWnd, aWinRect);
+ // In general, if IsWindowVisibleAndFullyOpaque returns false, the
+ // returned rect should not be altered.
+ if (!ret) {
+ EXPECT_EQ(*aWinRect, LayoutDeviceIntRect(0, 0, 0, 0));
+ }
+ return ret;
+ }
+
+ RefPtr<MockWinWidget> mMockWinWidget;
+};
+
+TEST_F(WinWindowOcclusionTrackerTest, VisibleOpaqueWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0);
+ LayoutDeviceIntRect returnedRect;
+ // Normal windows should be visible.
+ EXPECT_TRUE(CheckWindowVisibleAndFullyOpaque(hwnd, &returnedRect));
+
+ // Check that the returned rect == the actual window rect of the hwnd.
+ RECT winRect;
+ ASSERT_TRUE(::GetWindowRect(hwnd, &winRect));
+ EXPECT_EQ(returnedRect, LayoutDeviceIntRect(winRect.left, winRect.top,
+ winRect.right - winRect.left,
+ winRect.bottom - winRect.top));
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, MinimizedWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0);
+ LayoutDeviceIntRect winRect;
+ ::ShowWindow(hwnd, SW_MINIMIZE);
+ // Minimized windows are not considered visible.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, TransparentWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_TRANSPARENT);
+ LayoutDeviceIntRect winRect;
+ // Transparent windows are not considered visible and opaque.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, ToolWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_TOOLWINDOW);
+ LayoutDeviceIntRect winRect;
+ // Tool windows are not considered visible and opaque.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, LayeredAlphaWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED);
+ LayoutDeviceIntRect winRect;
+ BYTE alpha = 1;
+ DWORD flags = LWA_ALPHA;
+ COLORREF colorRef = RGB(1, 1, 1);
+ SetLayeredWindowAttributes(hwnd, colorRef, alpha, flags);
+ // Layered windows with alpha < 255 are not considered visible and opaque.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, UpdatedLayeredAlphaWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED);
+ LayoutDeviceIntRect winRect;
+ HDC hdc = ::CreateCompatibleDC(nullptr);
+ EXPECT_NE(nullptr, hdc);
+ BLENDFUNCTION blend = {AC_SRC_OVER, 0x00, 0xFF, AC_SRC_ALPHA};
+
+ ::UpdateLayeredWindow(hwnd, hdc, nullptr, nullptr, nullptr, nullptr,
+ RGB(0xFF, 0xFF, 0xFF), &blend, ULW_OPAQUE);
+ // Layered windows set up with UpdateLayeredWindow instead of
+ // SetLayeredWindowAttributes should not be considered visible and opaque.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+ ::DeleteDC(hdc);
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, LayeredNonAlphaWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED);
+ LayoutDeviceIntRect winRect;
+ BYTE alpha = 1;
+ DWORD flags = 0;
+ COLORREF colorRef = RGB(1, 1, 1);
+ ::SetLayeredWindowAttributes(hwnd, colorRef, alpha, flags);
+ // Layered non alpha windows are considered visible and opaque.
+ EXPECT_TRUE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, ComplexRegionWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0);
+ LayoutDeviceIntRect winRect;
+ // Create a region with rounded corners, which should be a complex region.
+ HRGN region = CreateRoundRectRgn(1, 1, 100, 100, 5, 5);
+ EXPECT_NE(nullptr, region);
+ ::SetWindowRgn(hwnd, region, /* bRedraw = */ TRUE);
+ // Windows with complex regions are not considered visible and fully opaque.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+ DeleteObject(region);
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, PopupWindow) {
+ HWND hwnd = CreateNativeWindow(WS_POPUP, /* aExStyle = */ 0);
+ LayoutDeviceIntRect winRect;
+ // Normal Popup Windows are not considered visible.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+}
+
+TEST_F(WinWindowOcclusionTrackerTest, CloakedWindow) {
+ HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0);
+ LayoutDeviceIntRect winRect;
+ BOOL cloak = TRUE;
+ ::DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak));
+ // Cloaked Windows are not considered visible.
+ EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect));
+}
diff --git a/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp b/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp
new file mode 100644
index 0000000000..13cd95651c
--- /dev/null
+++ b/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp
@@ -0,0 +1,402 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+
+#include "MockWinWidget.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/widget/WinEventObserver.h"
+#include "mozilla/widget/WinWindowOcclusionTracker.h"
+#include "nsThreadUtils.h"
+#include "Units.h"
+#include "WinUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#define PREF_DISPLAY_STATE \
+ "widget.windows.window_occlusion_tracking_display_state.enabled"
+#define PREF_SESSION_LOCK \
+ "widget.windows.window_occlusion_tracking_session_lock.enabled"
+
+class WinWindowOcclusionTrackerInteractiveTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // Shut down WinWindowOcclusionTracker if it exists.
+ // This could happen when WinWindowOcclusionTracker was initialized by other
+ // gtest
+ if (WinWindowOcclusionTracker::Get()) {
+ WinWindowOcclusionTracker::ShutDown();
+ }
+ EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());
+
+ WinWindowOcclusionTracker::Ensure();
+ EXPECT_NE(nullptr, WinWindowOcclusionTracker::Get());
+
+ WinWindowOcclusionTracker::Get()->EnsureDisplayStatusObserver();
+ WinWindowOcclusionTracker::Get()->EnsureSessionChangeObserver();
+ }
+
+ void TearDown() override {
+ WinWindowOcclusionTracker::ShutDown();
+ EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());
+ }
+
+ void SetNativeWindowBounds(HWND aHWnd, const LayoutDeviceIntRect aBounds) {
+ RECT wr;
+ wr.left = aBounds.X();
+ wr.top = aBounds.Y();
+ wr.right = aBounds.XMost();
+ wr.bottom = aBounds.YMost();
+
+ ::AdjustWindowRectEx(&wr, ::GetWindowLong(aHWnd, GWL_STYLE), FALSE,
+ ::GetWindowLong(aHWnd, GWL_EXSTYLE));
+
+ // Make sure to keep the window onscreen, as AdjustWindowRectEx() may have
+ // moved part of it offscreen. But, if the original requested bounds are
+ // offscreen, don't adjust the position.
+ LayoutDeviceIntRect windowBounds(wr.left, wr.top, wr.right - wr.left,
+ wr.bottom - wr.top);
+
+ if (aBounds.X() >= 0) {
+ windowBounds.x = std::max(0, windowBounds.X());
+ }
+ if (aBounds.Y() >= 0) {
+ windowBounds.y = std::max(0, windowBounds.Y());
+ }
+ ::SetWindowPos(aHWnd, HWND_TOP, windowBounds.X(), windowBounds.Y(),
+ windowBounds.Width(), windowBounds.Height(),
+ SWP_NOREPOSITION);
+ EXPECT_TRUE(::UpdateWindow(aHWnd));
+ }
+
+ void CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds) {
+ mMockWinWidget = MockWinWidget::Create(
+ WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle = */ 0, aBounds);
+ EXPECT_NE(nullptr, mMockWinWidget.get());
+ HWND hwnd = mMockWinWidget->GetWnd();
+ SetNativeWindowBounds(hwnd, aBounds);
+ HRGN region = ::CreateRectRgn(0, 0, 0, 0);
+ EXPECT_NE(nullptr, region);
+ if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) {
+ // On Windows 7, the newly created window has a complex region, which
+ // means it will be ignored during the occlusion calculation. So, force
+ // it to have a simple region so that we get test coverage on win 7.
+ RECT boundingRect;
+ EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect));
+ HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect);
+ EXPECT_NE(nullptr, rectangularRegion);
+ ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE);
+ ::DeleteObject(rectangularRegion);
+ }
+ ::DeleteObject(region);
+
+ ::ShowWindow(hwnd, SW_SHOWNORMAL);
+ EXPECT_TRUE(UpdateWindow(hwnd));
+ }
+
+ RefPtr<MockWinWidget> CreateTrackedWindowWithBounds(
+ LayoutDeviceIntRect aBounds) {
+ RefPtr<MockWinWidget> window = MockWinWidget::Create(
+ WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle */ 0, aBounds);
+ EXPECT_NE(nullptr, window.get());
+ HWND hwnd = window->GetWnd();
+ ::ShowWindow(hwnd, SW_SHOWNORMAL);
+ WinWindowOcclusionTracker::Get()->Enable(window, window->GetWnd());
+ return window;
+ }
+
+ int GetNumVisibleRootWindows() {
+ return WinWindowOcclusionTracker::Get()->mNumVisibleRootWindows;
+ }
+
+ void OnDisplayStateChanged(bool aDisplayOn) {
+ WinWindowOcclusionTracker::Get()->OnDisplayStateChanged(aDisplayOn);
+ }
+
+ RefPtr<MockWinWidget> mMockWinWidget;
+};
+
+// Simple test completely covering a tracked window with a native window.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ window->SetExpectation(widget::OcclusionState::OCCLUDED);
+ CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Simple test partially covering a tracked window with a native window.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, PartialOcclusion) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Simple test that a partly off screen tracked window, with the on screen part
+// occluded by a native window, is considered occluded.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, OffscreenOcclusion) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ // Move the tracked window 50 pixels offscreen to the left.
+ int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN);
+ ::SetWindowPos(window->GetWnd(), HWND_TOP, screenLeft - 50, 0, 100, 100,
+ SWP_NOZORDER | SWP_NOSIZE);
+
+ // Create a native window that covers the onscreen part of the tracked window.
+ CreateNativeWindowWithBounds(LayoutDeviceIntRect(screenLeft, 0, 50, 100));
+ window->SetExpectation(widget::OcclusionState::OCCLUDED);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Simple test with a tracked window and native window that do not overlap.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleVisible) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Simple test with a minimized tracked window and native window.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleHidden) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
+ // Iconify the tracked window and check that its occlusion state is HIDDEN.
+ ::CloseWindow(window->GetWnd());
+ window->SetExpectation(widget::OcclusionState::HIDDEN);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Test that minimizing and restoring a tracked window results in the occlusion
+// tracker re-registering for win events and detecting that a native window
+// occludes the tracked window.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest,
+ OcclusionAfterVisibilityToggle) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ window->SetExpectation(widget::OcclusionState::HIDDEN);
+ WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
+ window, /* aVisible = */ false);
+
+ // This makes the window iconic.
+ ::CloseWindow(window->GetWnd());
+
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ // HIDDEN state is set synchronously by OnWindowVsiblityChanged notification,
+ // before occlusion is calculated, so the above expectation will be met w/o an
+ // occlusion calculation.
+ // Loop until an occlusion calculation has run with no non-hidden windows.
+ while (GetNumVisibleRootWindows() != 0) {
+ // Need to pump events in order for UpdateOcclusionState to get called, and
+ // update the number of non hidden windows. When that number is 0,
+ // occlusion has been calculated with no visible tracked windows.
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
+ window, /* aVisible = */ true);
+ // This opens the window made iconic above.
+ ::OpenIcon(window->GetWnd());
+
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ // Open a native window that occludes the visible tracked window.
+ window->SetExpectation(widget::OcclusionState::OCCLUDED);
+ CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Test that locking the screen causes visible windows to become occluded.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenVisibleOcclusion) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ window->SetExpectation(widget::OcclusionState::OCCLUDED);
+ // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
+ // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but
+ // actually locking the screen isn't feasible.
+ DWORD currentSessionId = 0;
+ ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
+ ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
+ WTS_SESSION_LOCK, currentSessionId);
+
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+
+ MSG msg;
+ bool gotMessage =
+ ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
+ if (gotMessage) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessageW(&msg);
+ }
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
+ PR_Sleep(PR_INTERVAL_NO_WAIT);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Test that locking the screen leaves hidden windows as hidden.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenHiddenOcclusion) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ // Iconify the tracked window and check that its occlusion state is HIDDEN.
+ ::CloseWindow(window->GetWnd());
+ window->SetExpectation(widget::OcclusionState::HIDDEN);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ // Force the state to VISIBLE.
+ window->NotifyOcclusionState(widget::OcclusionState::VISIBLE);
+
+ window->SetExpectation(widget::OcclusionState::HIDDEN);
+
+ // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
+ // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but
+ // actually locking the screen isn't feasible.
+ DWORD currentSessionId = 0;
+ ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
+ PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
+ WTS_SESSION_LOCK, currentSessionId);
+
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+
+ MSG msg;
+ bool gotMessage =
+ ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
+ if (gotMessage) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessageW(&msg);
+ }
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
+ PR_Sleep(PR_INTERVAL_NO_WAIT);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Test that locking the screen from a different session doesn't mark window
+// as occluded.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenDifferentSession) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ // Force the state to OCCLUDED.
+ window->NotifyOcclusionState(widget::OcclusionState::OCCLUDED);
+
+ // Generate a session change lock screen with a session id that's not
+ // currentSessionId.
+ DWORD currentSessionId = 0;
+ ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
+ ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
+ WTS_SESSION_LOCK, currentSessionId + 1);
+
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ // Create a native window to trigger occlusion calculation.
+ CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+
+ MSG msg;
+ bool gotMessage =
+ ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
+ if (gotMessage) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessageW(&msg);
+ }
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
+ PR_Sleep(PR_INTERVAL_NO_WAIT);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
+
+// Test that display off & on power state notification causes visible windows to
+// become occluded, then visible.
+TEST_F(WinWindowOcclusionTrackerInteractiveTest, DisplayOnOffHandling) {
+ RefPtr<MockWinWidget> window =
+ CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ window->SetExpectation(widget::OcclusionState::OCCLUDED);
+
+ // Turning display off and on isn't feasible, so send a notification.
+ OnDisplayStateChanged(/* aDisplayOn */ false);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+
+ window->SetExpectation(widget::OcclusionState::VISIBLE);
+ OnDisplayStateChanged(/* aDisplayOn */ true);
+ while (window->IsExpectingCall()) {
+ WinWindowOcclusionTracker::Get()->TriggerCalculation();
+
+ NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
+ }
+ EXPECT_FALSE(window->IsExpectingCall());
+}
diff --git a/widget/tests/gtest/moz.build b/widget/tests/gtest/moz.build
new file mode 100644
index 0000000000..613844fa78
--- /dev/null
+++ b/widget/tests/gtest/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES = [
+ "TestTimeConverter.cpp",
+ "TestTouchResampler.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ UNIFIED_SOURCES += [
+ "MockWinWidget.cpp",
+ "TestWinHeaderOnlyUtils.cpp",
+ "TestWinMessageLoggingUtils.cpp",
+ "TestWinWindowOcclusionTracker.cpp",
+ "TestWinWindowOcclusionTrackerInteractive.cpp",
+ ]
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/widget",
+]
+
+DisableStlWrapping()
diff --git a/widget/tests/mochitest.toml b/widget/tests/mochitest.toml
new file mode 100644
index 0000000000..32dcf5c795
--- /dev/null
+++ b/widget/tests/mochitest.toml
@@ -0,0 +1,53 @@
+[DEFAULT]
+support-files = ["clipboard_helper.js"]
+
+["test_AltGr_key_events_in_web_content_on_windows.html"]
+run-if = ["os == 'win'"]
+skip-if = ["headless"] # bug 1410525
+
+["test_actionhint.html"]
+
+["test_assign_event_data.html"]
+skip-if = [
+ "os == 'mac'", # bug 933303
+ "os == 'android' && debug", # bug 1285414
+ "android_version == '24'",
+ "headless && os == 'win'",
+]
+
+["test_autocapitalize.html"]
+
+["test_clipboard.html"]
+skip-if = [
+ "headless", # bug 1852983
+ "display == 'wayland' && os_version == '22.04'", # Bug 1857075
+]
+support-files = ["file_test_clipboard.js"]
+
+["test_clipboard_asyncGetData.html"]
+skip-if = ["display == 'wayland'"] # Bug 1864211
+support-files = ["file_test_clipboard_asyncGetData.js"]
+
+["test_clipboard_asyncSetData.html"]
+support-files = ["file_test_clipboard_asyncSetData.js"]
+
+["test_contextmenu_by_mouse_on_unix.html"]
+run-if = ["os == 'linux'"]
+skip-if = ["headless"] # headless widget doesn't dispatch contextmenu event by mouse event.
+
+["test_keypress_event_with_alt_on_mac.html"]
+run-if = ["os == 'mac'"]
+
+["test_mouse_event_with_control_on_mac.html"]
+run-if = ["os == 'mac'"]
+support-files = ["!/gfx/layers/apz/test/mochitest/apz_test_utils.js"]
+
+["test_picker_no_crash.html"]
+skip-if = [
+ "asan",
+ "debug", # bug 1267491
+]
+support-files = ["window_picker_no_crash_child.html"]
+
+["test_textScaleFactor_system_font.html"]
+skip-if = ["display == 'wayland' && os_version == '22.04'"] # Bug 1857075
diff --git a/widget/tests/moz.build b/widget/tests/moz.build
new file mode 100644
index 0000000000..fe0d068efd
--- /dev/null
+++ b/widget/tests/moz.build
@@ -0,0 +1,125 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget")
+
+with Files("browser/browser_test_ContentCache.html"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("unit/*macwebapputils*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("unit/*taskbar_jumplistitems*"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+
+with Files("TestChromeMargin.cpp"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+
+with Files("*413277*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*428405*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*429954*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*444800*"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+
+with Files("*466599*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*478536*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*485118*"):
+ BUG_COMPONENT = ("Toolkit", "UI Widgets")
+
+with Files("*517396*"):
+ BUG_COMPONENT = ("Toolkit", "UI Widgets")
+
+with Files("*522217*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*538242*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*565392*"):
+ BUG_COMPONENT = ("Core", "DOM: Serializers")
+
+with Files("*586713*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*593307*"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+
+with Files("*596600*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*673301*"):
+ BUG_COMPONENT = ("Firefox", "Bookmarks & History")
+
+with Files("test_assign_event_data.html"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("test_input_events_on_deactive_window.xhtml"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*chrome_context_menus_win*"):
+ BUG_COMPONENT = ("Core", "General")
+
+with Files("*composition_text_querycontent*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*key_event_counts*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*imestate*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*mouse_scroll*"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+
+with Files("*native*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*panel_mouse_coords*"):
+ BUG_COMPONENT = ("Core", "Widget: Gtk")
+
+with Files("*picker_no_crash*"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+
+with Files("*platform_colors*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*position_on_resize*"):
+ BUG_COMPONENT = ("Core", "Widget: Gtk")
+
+with Files("test_sizemode_events.xhtml"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*system_status_bar*"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+with Files("*taskbar_progress*"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+
+with Files("*wheeltransaction*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"]
+MOCHITEST_MANIFESTS += ["mochitest.toml"]
+MOCHITEST_CHROME_MANIFESTS += ["chrome.toml"]
+BROWSER_CHROME_MANIFESTS += ["browser/browser.toml"]
+
+# if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+#
+# Test disabled because it requires the internal API. Re-enabling this test
+# is bug 652123.
+# CPP_UNIT_TESTS += ['TestChromeMargin']
diff --git a/widget/tests/native_menus_window.xhtml b/widget/tests/native_menus_window.xhtml
new file mode 100644
index 0000000000..2b3d3f2aa3
--- /dev/null
+++ b/widget/tests/native_menus_window.xhtml
@@ -0,0 +1,282 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="NativeMenuWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="300"
+ height="300"
+ onload="onLoad();"
+ title="Native Menu Test">
+
+ <command id="cmd_FooItem0" oncommand="executedCommandID = 'cmd_FooItem0';"/>
+ <command id="cmd_FooItem1" oncommand="executedCommandID = 'cmd_FooItem1';"/>
+ <command id="cmd_BarItem0" oncommand="executedCommandID = 'cmd_BarItem0';"/>
+ <command id="cmd_BarItem1" oncommand="executedCommandID = 'cmd_BarItem1';"/>
+ <command id="cmd_NewItem0" oncommand="executedCommandID = 'cmd_NewItem0';"/>
+ <command id="cmd_NewItem1" oncommand="executedCommandID = 'cmd_NewItem1';"/>
+ <command id="cmd_NewItem2" oncommand="executedCommandID = 'cmd_NewItem2';"/>
+ <command id="cmd_NewItem3" oncommand="executedCommandID = 'cmd_NewItem3';"/>
+ <command id="cmd_NewItem4" oncommand="executedCommandID = 'cmd_NewItem4';"/>
+ <command id="cmd_NewItem5" oncommand="executedCommandID = 'cmd_NewItem5';"/>
+
+ <!-- We do not modify any menus or menu items defined here in testing. These
+ serve as a baseline structure for us to test after other modifications.
+ We add children to the menubar defined here and test by modifying those
+ children. -->
+ <menubar id="nativemenubar">
+ <menu id="foo" label="Foo">
+ <menupopup>
+ <menuitem label="FooItem0" command="cmd_FooItem0"/>
+ <menuitem label="FooItem1" command="cmd_FooItem1"/>
+ <menuseparator/>
+ <menu label="Bar">
+ <menupopup>
+ <menuitem label="BarItem0" command="cmd_BarItem0"/>
+ <menuitem label="BarItem1" command="cmd_BarItem1"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <script type="application/javascript"><![CDATA[
+
+ function ok(condition, message) {
+ window.arguments[0].SimpleTest.ok(condition, message);
+ }
+
+ function onTestsFinished() {
+ window.close();
+ window.arguments[0].SimpleTest.finish();
+ }
+
+ // Force a menu to update itself. All of the menus parents will be updated
+ // as well. An empty string will force a complete menu system reload.
+ function forceUpdateNativeMenuAt(location) {
+ var utils = window.windowUtils;
+ try {
+ utils.forceUpdateNativeMenuAt(location);
+ }
+ catch (e) {
+ dump(e + "\n");
+ }
+ }
+
+ var executedCommandID = "";
+
+ function testItem(location, targetID) {
+ var utils = window.windowUtils;
+ var correctCommandHandler = false;
+ try {
+ utils.activateNativeMenuItemAt(location);
+ correctCommandHandler = executedCommandID == targetID;
+ }
+ catch (e) {
+ dump(e + "\n");
+ }
+ finally {
+ executedCommandID = "";
+ }
+ return correctCommandHandler;
+ }
+
+ function createXULMenuPopup() {
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var item = document.createElementNS(XUL_NS, "menupopup");
+ return item;
+ }
+
+ function createXULMenu(aLabel) {
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var item = document.createElementNS(XUL_NS, "menu");
+ item.setAttribute("label", aLabel);
+ return item;
+ }
+
+ function createXULMenuItem(aLabel, aCommandId) {
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var item = document.createElementNS(XUL_NS, "menuitem");
+ item.setAttribute("label", aLabel);
+ item.setAttribute("command", aCommandId);
+ return item;
+ }
+
+ function runBaseMenuTests() {
+ forceUpdateNativeMenuAt("0|3");
+ return testItem("0|0", "cmd_FooItem0") &&
+ testItem("0|1", "cmd_FooItem1") &&
+ testItem("0|3|0", "cmd_BarItem0") &&
+ testItem("0|3|1", "cmd_BarItem1");
+ }
+
+ function onLoad() {
+ var _delayedOnLoad = function() {
+ // First let's run the base menu tests.
+ ok(runBaseMenuTests());
+
+ // Set up some nodes that we'll use.
+ var menubarNode = document.getElementById("nativemenubar");
+ var newMenu0 = createXULMenu("NewMenu0");
+ var newMenu1 = createXULMenu("NewMenu1");
+ var newMenuPopup0 = createXULMenuPopup();
+ var newMenuPopup1 = createXULMenuPopup();
+ var newMenuItem0 = createXULMenuItem("NewMenuItem0", "cmd_NewItem0");
+ var newMenuItem1 = createXULMenuItem("NewMenuItem1", "cmd_NewItem1");
+ var newMenuItem2 = createXULMenuItem("NewMenuItem2", "cmd_NewItem2");
+ var newMenuItem3 = createXULMenuItem("NewMenuItem3", "cmd_NewItem3");
+ var newMenuItem4 = createXULMenuItem("NewMenuItem4", "cmd_NewItem4");
+ var newMenuItem5 = createXULMenuItem("NewMenuItem5", "cmd_NewItem5");
+
+ // Create another submenu with hierarchy via DOM manipulation.
+ // ******************
+ // * Foo * NewMenu0 * <- Menu bar
+ // ******************
+ // ****************
+ // * NewMenuItem0 * <- NewMenu0 submenu
+ // ****************
+ // * NewMenuItem1 *
+ // ****************
+ // * NewMenuItem2 *
+ // *******************************
+ // * NewMenu1 > * NewMenuItem3 * <- NewMenu1 submenu
+ // *******************************
+ // * NewMenuItem4 *
+ // ****************
+ // * NewMenuItem5 *
+ // ****************
+ newMenu0.appendChild(newMenuPopup0);
+ newMenuPopup0.appendChild(newMenuItem0);
+ newMenuPopup0.appendChild(newMenuItem1);
+ newMenuPopup0.appendChild(newMenuItem2);
+ newMenuPopup0.appendChild(newMenu1);
+ newMenu1.appendChild(newMenuPopup1);
+ newMenuPopup1.appendChild(newMenuItem3);
+ newMenuPopup1.appendChild(newMenuItem4);
+ newMenuPopup1.appendChild(newMenuItem5);
+ //XXX - we have to append the menu to the top-level of the menu bar
+ // only after constructing it. If we append before construction, it is
+ // invalid because it has no children and we don't validate it if we add
+ // children later.
+ menubarNode.appendChild(newMenu0);
+ forceUpdateNativeMenuAt("1|3");
+ // Run basic tests again.
+ ok(runBaseMenuTests());
+
+ // Error strings.
+ var sa = "Command handler(s) should have activated";
+ var sna = "Command handler(s) should not have activated";
+
+ // Test middle items.
+ ok(testItem("1|1", "cmd_NewItem1"), sa);
+ ok(testItem("1|3|1", "cmd_NewItem4"), sa);
+
+ // Hide newMenu0.
+ newMenu0.setAttribute("hidden", "true");
+ ok(runBaseMenuTests(), sa); // the base menu should still be unhidden
+ ok(!testItem("1|0", ""), sna);
+ ok(!testItem("1|1", ""), sna);
+ ok(!testItem("1|2", ""), sna);
+ ok(!testItem("1|3|0", ""), sna);
+ ok(!testItem("1|3|1", ""), sna);
+ ok(!testItem("1|3|2", ""), sna);
+
+ // Show newMenu0.
+ newMenu0.setAttribute("hidden", "false");
+ forceUpdateNativeMenuAt("1|3");
+ ok(runBaseMenuTests(), sa);
+ ok(testItem("1|0", "cmd_NewItem0"), sa);
+ ok(testItem("1|1", "cmd_NewItem1"), sa);
+ ok(testItem("1|2", "cmd_NewItem2"), sa);
+ ok(testItem("1|3|0", "cmd_NewItem3"), sa);
+ ok(testItem("1|3|1", "cmd_NewItem4"), sa);
+ ok(testItem("1|3|2", "cmd_NewItem5"), sa);
+
+ // Hide items.
+ newMenuItem1.setAttribute("hidden", "true");
+ newMenuItem4.setAttribute("hidden", "true");
+ forceUpdateNativeMenuAt("1|2");
+ ok(runBaseMenuTests(), sa);
+ ok(testItem("1|0", "cmd_NewItem0"), sa);
+ ok(testItem("1|1", "cmd_NewItem2"), sa);
+ ok(!testItem("1|2", ""), sna);
+ ok(testItem("1|2|0", "cmd_NewItem3"), sa);
+ ok(testItem("1|2|1", "cmd_NewItem5"), sa);
+ ok(!testItem("1|2|2", ""), sna);
+
+ // Show items.
+ newMenuItem1.setAttribute("hidden", "false");
+ newMenuItem4.setAttribute("hidden", "false");
+ forceUpdateNativeMenuAt("1|3");
+ ok(runBaseMenuTests(), sa);
+ ok(testItem("1|0", "cmd_NewItem0"), sa);
+ ok(testItem("1|1", "cmd_NewItem1"), sa);
+ ok(testItem("1|2", "cmd_NewItem2"), sa);
+ ok(testItem("1|3|0", "cmd_NewItem3"), sa);
+ ok(testItem("1|3|1", "cmd_NewItem4"), sa);
+ ok(testItem("1|3|2", "cmd_NewItem5"), sa);
+
+ // At this point in the tests the state of the menus has been returned
+ // to the originally diagramed state.
+
+ // Test command disabling
+ var cmd_NewItem0 = document.getElementById("cmd_NewItem0");
+ ok(testItem("1|0", "cmd_NewItem0"), sa);
+ cmd_NewItem0.setAttribute("disabled", "true");
+ ok(!testItem("1|0", "cmd_NewItem0"), sna);
+ cmd_NewItem0.removeAttribute("disabled");
+ ok(testItem("1|0", "cmd_NewItem0"), sa);
+
+ // Remove menu.
+ menubarNode.removeChild(newMenu0);
+ ok(runBaseMenuTests(), sa);
+ ok(!testItem("1|0", ""), sna);
+ ok(!testItem("1|1", ""), sna);
+ ok(!testItem("1|2", ""), sna);
+ ok(!testItem("1|3|0", ""), sna);
+ ok(!testItem("1|3|1", ""), sna);
+ ok(!testItem("1|3|2", ""), sna);
+ // return state to original diagramed state
+ menubarNode.appendChild(newMenu0);
+
+ // Test for bug 447042, make sure that adding a menu node with no children
+ // to the menu bar and then adding another menu node with children works.
+ // Menus with no children don't get their native menu items shown and that
+ // caused internal arrays to get out of sync and an append crashed.
+ var tmpMenu0 = createXULMenu("tmpMenu0");
+ menubarNode.removeChild(newMenu0);
+ menubarNode.appendChild(tmpMenu0);
+ menubarNode.appendChild(newMenu0);
+ forceUpdateNativeMenuAt("1|3");
+ ok(runBaseMenuTests());
+ ok(testItem("1|0", "cmd_NewItem0"), sa);
+ ok(testItem("1|1", "cmd_NewItem1"), sa);
+ ok(testItem("1|2", "cmd_NewItem2"), sa);
+ ok(testItem("1|3|0", "cmd_NewItem3"), sa);
+ ok(testItem("1|3|1", "cmd_NewItem4"), sa);
+ ok(testItem("1|3|2", "cmd_NewItem5"), sa);
+ // return state to original diagramed state
+ menubarNode.removeChild(tmpMenu0);
+
+ // This test is basically a crash test for bug 433858.
+ newMenuItem1.setAttribute("hidden", "true");
+ newMenuItem2.setAttribute("hidden", "true");
+ newMenu1.setAttribute("hidden", "true");
+ forceUpdateNativeMenuAt("1");
+ newMenuItem1.setAttribute("hidden", "false");
+ newMenuItem2.setAttribute("hidden", "false");
+ newMenu1.setAttribute("hidden", "false");
+ forceUpdateNativeMenuAt("1");
+
+ onTestsFinished();
+ }
+
+ setTimeout(_delayedOnLoad, 1000);
+ }
+
+ ]]></script>
+</window>
diff --git a/widget/tests/standalone_native_menu_window.xhtml b/widget/tests/standalone_native_menu_window.xhtml
new file mode 100644
index 0000000000..4155126ad5
--- /dev/null
+++ b/widget/tests/standalone_native_menu_window.xhtml
@@ -0,0 +1,374 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="StandaloneNativeMenuWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="300"
+ height="300"
+ onload="onLoad();"
+ title="nsIStandaloneNativeMenu Test">
+
+ <command id="cmd_FooItem0" oncommand="gExecutedCommandID = 'cmd_FooItem0';"/>
+ <command id="cmd_FooItem1" oncommand="gExecutedCommandID = 'cmd_FooItem1';"/>
+ <command id="cmd_BarItem0" oncommand="gExecutedCommandID = 'cmd_BarItem0';"/>
+ <command id="cmd_BarItem1" oncommand="gExecutedCommandID = 'cmd_BarItem1';"/>
+ <command id="cmd_NewItem0" oncommand="gExecutedCommandID = 'cmd_NewItem0';"/>
+ <command id="cmd_NewItem1" oncommand="gExecutedCommandID = 'cmd_NewItem1';"/>
+ <command id="cmd_NewItem2" oncommand="gExecutedCommandID = 'cmd_NewItem2';"/>
+ <command id="cmd_NewItem3" oncommand="gExecutedCommandID = 'cmd_NewItem3';"/>
+ <command id="cmd_NewItem4" oncommand="gExecutedCommandID = 'cmd_NewItem4';"/>
+ <command id="cmd_NewItem5" oncommand="gExecutedCommandID = 'cmd_NewItem5';"/>
+
+ <!-- We do not modify any menus or menu items defined here in testing. These
+ serve as a baseline structure for us to test after other modifications.
+ We add children to the menubar defined here and test by modifying those
+ children. -->
+ <popupset>
+ <menupopup id="standalonenativemenu">
+ <menu id="foo" label="Foo">
+ <menupopup>
+ <menuitem label="FooItem0" command="cmd_FooItem0"/>
+ <menuitem label="FooItem1" command="cmd_FooItem1"/>
+ <menuseparator/>
+ <menu label="Bar">
+ <menupopup>
+ <menuitem label="BarItem0" command="cmd_BarItem0"/>
+ <menuitem label="BarItem1" command="cmd_BarItem1"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </popupset>
+
+ <script type="application/javascript"><![CDATA[
+
+ function ok(condition, message) {
+ window.arguments[0].SimpleTest.ok(condition, message);
+ }
+
+ function is(a, b, message) {
+ window.arguments[0].SimpleTest.is(a, b, message);
+ }
+
+ function isnot(a, b, message) {
+ window.arguments[0].SimpleTest.isnot(a, b, message);
+ }
+
+ function todo(condition, message) {
+ window.arguments[0].SimpleTest.todo(condition, message);
+ }
+
+ function todo_is(a, b, message) {
+ window.arguments[0].SimpleTest.todo_is(a, b, message);
+ }
+
+ function todo_isnot(a, b, message) {
+ window.arguments[0].SimpleTest.todo_isnot(a, b, message);
+ }
+
+ function onTestsFinished() {
+ window.close();
+ window.arguments[0].SimpleTest.finish();
+ }
+
+ var gExecutedCommandID;
+
+ // returns the executed command ID, or the empty string if nothing was executed
+ function activateItem(menu, location) {
+ try {
+ gExecutedCommandID = "";
+ menu.menuWillOpen();
+ menu.activateNativeMenuItemAt(location);
+ return gExecutedCommandID;
+ }
+ catch (e) {
+ // activateNativeMenuItemAt throws an exception if the item was not found
+ dump(e + "\n");
+ return "";
+ }
+ }
+
+ function testItem(menu, location, targetID) {
+ var activatedCommandID = activateItem(menu, location);
+ return activatedCommandID != "" && activatedCommandID == targetID;
+ }
+
+ var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ function createXULMenuPopup() {
+ return document.createElementNS(XUL_NS, "menupopup");
+ }
+
+ function createXULMenu(aLabel) {
+ var item = document.createElementNS(XUL_NS, "menu");
+ item.setAttribute("label", aLabel);
+ return item;
+ }
+
+ function createXULMenuItem(aLabel, aCommandId) {
+ var item = document.createElementNS(XUL_NS, "menuitem");
+ item.setAttribute("label", aLabel);
+ item.setAttribute("command", aCommandId);
+ return item;
+ }
+
+ function runBaseMenuTests(menu) {
+ menu.forceUpdateNativeMenuAt("0|3");
+ return testItem(menu, "0|0", "cmd_FooItem0") &&
+ testItem(menu, "0|1", "cmd_FooItem1") &&
+ testItem(menu, "0|3|0", "cmd_BarItem0") &&
+ testItem(menu, "0|3|1", "cmd_BarItem1");
+ }
+
+ function createStandaloneNativeMenu(menuNode) {
+ try {
+ let menu = Cc["@mozilla.org/widget/standalonenativemenu;1"].createInstance(Ci.nsIStandaloneNativeMenu);
+ menu.init(menuNode);
+ return menu;
+ } catch (e) {
+ ok(false, "Failed creating nsIStandaloneNativeMenu instance");
+ throw e;
+ }
+ }
+
+ function runDetachedMenuTests(addMenupopupBeforeCreatingSNM) {
+ let menu = createXULMenu("Detached menu");
+ menu.setAttribute("image", 'data:image/svg+xml,<svg%20xmlns="http://www.w3.org/2000/svg"%20width="32"%20height="32"><circle%20cx="16"%20cy="16"%20r="16"/></svg>');
+ let menupopup = createXULMenuPopup();
+
+ let popupShowingFired = false;
+ let itemActivated = false;
+
+ menupopup.addEventListener("popupshowing", function (e) {
+ popupShowingFired = true;
+
+ let menuitem = document.createElementNS(XUL_NS, "menuitem");
+ menuitem.setAttribute("label", "detached menu item");
+ /* eslint-disable-next-line no-shadow */
+ menuitem.addEventListener("command", function (e) {
+ itemActivated = true;
+ })
+ menupopup.appendChild(menuitem);
+ })
+
+ // It shouldn't make a difference whether the menupopup is added to the
+ // menu element before or after we create the nsIStandaloneNativeMenu
+ // instance with it. We test both orders by calling this function twice
+ // with different values for addMenupopupBeforeCreatingSNM.
+
+ var menuSNM = null; // the nsIStandaloneNativeMenu object for "menu"
+ if (addMenupopupBeforeCreatingSNM) {
+ menu.appendChild(menupopup);
+ menuSNM = createStandaloneNativeMenu(menu);
+ } else {
+ menuSNM = createStandaloneNativeMenu(menu);
+ menu.appendChild(menupopup);
+ }
+
+ try {
+ ok(!popupShowingFired, "popupshowing shouldn't have fired before our call to menuWillOpen()");
+ menuSNM.menuWillOpen();
+ ok(popupShowingFired, "calling menuWillOpen() should have notified our popupshowing listener");
+
+ ok(!itemActivated, "our dynamically-added menuitem shouldn't have been activated yet");
+ menuSNM.activateNativeMenuItemAt("0");
+ ok(itemActivated, "the new menu item should have been activated now");
+ } catch (ex) {
+ ok(false, "dynamic menu test failed: " + ex);
+ }
+ }
+
+ function onLoad() {
+ var _delayedOnLoad = function() {
+ try {
+
+ var menuNode = document.getElementById("standalonenativemenu");
+ var menu = createStandaloneNativeMenu(menuNode);
+
+ // First let's run the base menu tests.
+ ok(runBaseMenuTests(menu), "base tests #1");
+
+ // Set up some nodes that we'll use.
+ var newMenu0 = createXULMenu("NewMenu0");
+ var newMenu1 = createXULMenu("NewMenu1");
+ var newMenuPopup0 = createXULMenuPopup();
+ var newMenuPopup1 = createXULMenuPopup();
+ var newMenuItem0 = createXULMenuItem("NewMenuItem0", "cmd_NewItem0");
+ var newMenuItem1 = createXULMenuItem("NewMenuItem1", "cmd_NewItem1");
+ var newMenuItem2 = createXULMenuItem("NewMenuItem2", "cmd_NewItem2");
+ var newMenuItem3 = createXULMenuItem("NewMenuItem3", "cmd_NewItem3");
+ var newMenuItem4 = createXULMenuItem("NewMenuItem4", "cmd_NewItem4");
+ var newMenuItem5 = createXULMenuItem("NewMenuItem5", "cmd_NewItem5");
+
+ // Create another submenu with hierarchy via DOM manipulation.
+ // ******************
+ // * Foo * NewMenu0 * <- Menu bar
+ // ******************
+ // ****************
+ // * NewMenuItem0 * <- NewMenu0 submenu
+ // ****************
+ // * NewMenuItem1 *
+ // ****************
+ // * NewMenuItem2 *
+ // *******************************
+ // * NewMenu1 > * NewMenuItem3 * <- NewMenu1 submenu
+ // *******************************
+ // * NewMenuItem4 *
+ // ****************
+ // * NewMenuItem5 *
+ // ****************
+ newMenu0.appendChild(newMenuPopup0);
+ newMenuPopup0.appendChild(newMenuItem0);
+ newMenuPopup0.appendChild(newMenuItem1);
+ newMenuPopup0.appendChild(newMenuItem2);
+ newMenuPopup0.appendChild(newMenu1);
+ newMenu1.appendChild(newMenuPopup1);
+ newMenuPopup1.appendChild(newMenuItem3);
+ newMenuPopup1.appendChild(newMenuItem4);
+ newMenuPopup1.appendChild(newMenuItem5);
+ //XXX - we have to append the menu to the top-level of the menu bar
+ // only after constructing it. If we append before construction, it is
+ // invalid because it has no children and we don't validate it if we add
+ // children later.
+ menuNode.appendChild(newMenu0);
+ menu.forceUpdateNativeMenuAt("1|3");
+ // Run basic tests again.
+ ok(runBaseMenuTests(menu), "base tests #2");
+
+ // Error strings.
+ var sa = "Command handler(s) should have activated";
+ var sna = "Command handler(s) should not have activated";
+
+ // Test middle items.
+ is(activateItem(menu, "1|1"), "cmd_NewItem1", "#1:" + sa);
+ is(activateItem(menu, "1|3|1"), "cmd_NewItem4", "#2:" + sa);
+
+ // Hide newMenu0.
+ newMenu0.setAttribute("hidden", "true");
+ ok(runBaseMenuTests(menu), "base tests #3: " + sa); // the base menu should still be unhidden
+ is(activateItem(menu, "1|0"), "", "#3:" + sna);
+ is(activateItem(menu, "1|1"), "", "#4:" + sna);
+ is(activateItem(menu, "1|2"), "", "#5:" + sna);
+ is(activateItem(menu, "1|3|0"), "", "#6:" + sna);
+ is(activateItem(menu, "1|3|1"), "", "#7:" + sna);
+ is(activateItem(menu, "1|3|2"), "", "#8:" + sna);
+
+ // Show newMenu0.
+ newMenu0.setAttribute("hidden", "false");
+ menu.forceUpdateNativeMenuAt("1|3");
+ ok(runBaseMenuTests(menu), "base tests #4:" + sa);
+ is(activateItem(menu, "1|0"), "cmd_NewItem0", "#9:" + sa);
+ is(activateItem(menu, "1|1"), "cmd_NewItem1", "#10:" + sa);
+ is(activateItem(menu, "1|2"), "cmd_NewItem2", "#11:" + sa);
+ is(activateItem(menu, "1|3|0"), "cmd_NewItem3", "#12:" + sa);
+ is(activateItem(menu, "1|3|1"), "cmd_NewItem4", "#13:" + sa);
+ is(activateItem(menu, "1|3|2"), "cmd_NewItem5", "#14:" + sa);
+
+ // Hide items.
+ newMenuItem1.setAttribute("hidden", "true");
+ newMenuItem4.setAttribute("hidden", "true");
+ menu.forceUpdateNativeMenuAt("1|2");
+ ok(runBaseMenuTests(menu), "base tests #5:" + sa);
+ is(activateItem(menu, "1|0"), "cmd_NewItem0", "#15:" + sa);
+ is(activateItem(menu, "1|1"), "cmd_NewItem2", "#16:" + sa);
+ is(activateItem(menu, "1|2"), "", "#17:" + sna);
+ is(activateItem(menu, "1|2|0"), "cmd_NewItem3", "#18:" + sa);
+ is(activateItem(menu, "1|2|1"), "cmd_NewItem5", "#19:" + sa);
+ is(activateItem(menu, "1|2|2"), "", "#20:" + sna);
+
+ // Show items.
+ newMenuItem1.setAttribute("hidden", "false");
+ newMenuItem4.setAttribute("hidden", "false");
+ //forceUpdateNativeMenuAt("1|3");
+ ok(runBaseMenuTests(menu), "base tests #6:" + sa);
+ is(activateItem(menu, "1|0"), "cmd_NewItem0", "#21:" + sa);
+ is(activateItem(menu, "1|1"), "cmd_NewItem1", "#22:" + sa);
+ is(activateItem(menu, "1|2"), "cmd_NewItem2", "#23:" + sa);
+ is(activateItem(menu, "1|3|0"), "cmd_NewItem3", "#24:" + sa);
+ is(activateItem(menu, "1|3|1"), "cmd_NewItem4", "#25:" + sa);
+ is(activateItem(menu, "1|3|2"), "cmd_NewItem5", "#26:" + sa);
+
+ // At this point in the tests the state of the menus has been returned
+ // to the originally diagramed state.
+
+ // Remove menu.
+ menuNode.removeChild(newMenu0);
+ ok(runBaseMenuTests(menu), "base tests #7:" + sa);
+ is(activateItem(menu, "1|0"), "", "#27:" + sna);
+ is(activateItem(menu, "1|1"), "", "#28:" + sna);
+ is(activateItem(menu, "1|2"), "", "#29:" + sna);
+ is(activateItem(menu, "1|3|0"), "", "#30:" + sna);
+ is(activateItem(menu, "1|3|1"), "", "#31:" + sna);
+ is(activateItem(menu, "1|3|2"), "", "#32:" + sna);
+ // return state to original diagramed state
+ menuNode.appendChild(newMenu0);
+
+ // The following is based on a similar test bug 447042 from the native
+ // menu bar test: Make sure that adding a menu node with no children
+ // to the menu bar and then adding another menu node with children works.
+ // In the menubar, root menus with no children are skipped - they're not
+ // visible in the menubar.
+ // Regular menus currently treat submenus without children differently:
+ // submenus without children *are* visible.
+ // We may want to change this in the future.
+ // After the mutation below we have the following root menu content:
+ // - [0] Foo (with submenu)
+ // - [1] tmpMenu0 (with empty submenu)
+ // - [2] NewMenu0 (with submenu)
+ // Since the empty tmpMenu0 item is not skipped, NewMenu0 has index 2,
+ // so we use "2|..." below, rather than the "1|..." that's used in the
+ // menubar test.
+ var tmpMenu0 = createXULMenu("tmpMenu0");
+ menuNode.removeChild(newMenu0);
+ menuNode.appendChild(tmpMenu0);
+ menuNode.appendChild(newMenu0);
+ menu.forceUpdateNativeMenuAt("2|3");
+ ok(runBaseMenuTests(menu), "base tests #8");
+ is(activateItem(menu, "2|0"), "cmd_NewItem0", "#33:" + sa);
+ is(activateItem(menu, "2|1"), "cmd_NewItem1", "#34:" + sa);
+ is(activateItem(menu, "2|2"), "cmd_NewItem2", "#35:" + sa);
+ is(activateItem(menu, "2|3|0"), "cmd_NewItem3", "#36:" + sa);
+ is(activateItem(menu, "2|3|1"), "cmd_NewItem4", "#37:" + sa);
+ is(activateItem(menu, "2|3|2"), "cmd_NewItem5", "#38:" + sa);
+ // return state to original diagramed state
+ menuNode.removeChild(tmpMenu0);
+
+ // This test is basically a crash test for bug 433858.
+ newMenuItem1.setAttribute("hidden", "true");
+ newMenuItem2.setAttribute("hidden", "true");
+ newMenu1.setAttribute("hidden", "true");
+ menu.forceUpdateNativeMenuAt("1");
+ newMenuItem1.setAttribute("hidden", "false");
+ newMenuItem2.setAttribute("hidden", "false");
+ newMenu1.setAttribute("hidden", "false");
+ menu.forceUpdateNativeMenuAt("1");
+
+ // Check that "path components" which are out-of-range are not ignored.
+ // There are only two menu items in the root menu (with index 0 and 1),
+ // so index 2 is out of range.
+ is(activateItem(menu, "2|1|0"), "", "#39:" + sna);
+
+ // Check that hiding and then un-hiding the root menu doesn't result in
+ // a cyclic native menu structure.
+ menuNode.setAttribute("collapsed", "true");
+ menuNode.removeAttribute("collapsed");
+ ok(runBaseMenuTests(menu), "base tests #9");
+
+ // Run tests where the menu nodes are not in the document's node tree.
+ runDetachedMenuTests(false);
+ runDetachedMenuTests(true);
+
+ } catch (e) {
+ ok(false, "Caught an exception: " + e);
+ } finally {
+ onTestsFinished();
+ }
+ }
+
+ setTimeout(_delayedOnLoad, 1000);
+ }
+
+ ]]></script>
+</window>
diff --git a/widget/tests/system_font_changes.xhtml b/widget/tests/system_font_changes.xhtml
new file mode 100644
index 0000000000..1cef650015
--- /dev/null
+++ b/widget/tests/system_font_changes.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window id="system_font_changes_window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="300"
+ height="300"
+ onload="start();">
+
+<span id="target" style="font:menu">Hello world</span>
+
+<script type="application/javascript"><![CDATA[
+ function is(condition, message) {
+ window.arguments[0].SimpleTest.is(condition, message);
+ }
+ function registerCleanupFunction(func) {
+ window.arguments[0].SimpleTest.registerCleanupFunction(func);
+ }
+
+ async function waitForFrame() {
+ return new Promise(resolve => {
+ requestAnimationFrame(resolve);
+ });
+ }
+
+ let windowUtils = window.windowUtils;
+ async function start() {
+ await waitForFrame();
+
+ const originalSystemFont = windowUtils.systemFont;
+ registerCleanupFunction(() => {
+ windowUtils.systemFont = originalSystemFont;
+ });
+
+ windowUtils.systemFont = 'Sans 11';
+ is(windowUtils.systemFont, 'Sans 11');
+
+ // Wait for two frames for the safety since the notification for system
+ // font changes is asynchronously processed.
+ await waitForFrame();
+ await waitForFrame();
+
+ const target = document.getElementById('target');
+ is(getComputedStyle(target).fontFamily, 'Sans');
+
+ windowUtils.systemFont = 'Serif 11';
+ is(windowUtils.systemFont, 'Serif 11');
+
+ await waitForFrame();
+ await waitForFrame();
+
+ is(getComputedStyle(target).fontFamily, 'Serif');
+
+ window.close();
+ window.arguments[0].SimpleTest.finish();
+ }
+]]></script>
+</window>
diff --git a/widget/tests/taskbar_previews.xhtml b/widget/tests/taskbar_previews.xhtml
new file mode 100644
index 0000000000..b220d5662b
--- /dev/null
+++ b/widget/tests/taskbar_previews.xhtml
@@ -0,0 +1,116 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Taskbar Previews Test"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="loaded();">
+
+ <title>Previews - yeah!</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+ let taskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(Ci.nsIWinTaskbar);
+
+ function IsWin7OrHigher() {
+ try {
+ var ver = parseFloat(Services.sysinfo.getProperty("version"));
+ if (ver >= 6.1)
+ return true;
+ } catch (ex) { }
+ return false;
+ }
+ isnot(taskbar, null, "Taskbar service is defined");
+ is(taskbar.available, IsWin7OrHigher(), "Expected availability of taskbar");
+
+ SimpleTest.waitForExplicitFinish();
+
+ function stdPreviewSuite(p) {
+ p.visible = !p.visible;
+ p.visible = !p.visible;
+ p.visible = true;
+ p.invalidate();
+ p.visible = false;
+ }
+
+ function loaded()
+ {
+ if (!taskbar.available)
+ SimpleTest.finish();
+ let controller = {
+ width: 400,
+ height: 400,
+ thumbnailAspectRatio: 1.0,
+ get wrappedJSObject() { return this; }
+ }
+ // HACK from mconnor:
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let docShell = win.gBrowser.docShell;
+
+ let winPreview = taskbar.getTaskbarWindowPreview(docShell);
+ isnot(winPreview, null, "Window preview is not null");
+ winPreview.controller = controller;
+ let button = winPreview.getButton(0);
+ isnot(button, null, "Could get button at valid index");
+ try {
+ winPreview.getButton(-1);
+ ok(false, "Got button at negative index");
+ } catch (ex) {}
+ try {
+ winPreview.getButton(Ci.nsITaskbarWindowPreview.NUM_TOOLBAR_BUTTONS);
+ ok(false, "Got button at index that is too large");
+ } catch (ex) {}
+ button.image = null;
+ stdPreviewSuite(winPreview);
+ // Let's not perma-hide this window from the taskbar
+ winPreview.visible = true;
+
+ let tabP = taskbar.createTaskbarTabPreview(docShell, controller);
+ isnot(tabP, null, "Tab preview is not null");
+ is(tabP.controller.wrappedJSObject, controller, "Controllers match");
+ is(tabP.icon, null, "Default icon is null (windows default)");
+ tabP.icon = null;
+ tabP.move(null);
+ try {
+ tabP.move(tabP);
+ ok(false, "Moved a preview next to itself!");
+ } catch (ex) {}
+ stdPreviewSuite(tabP);
+
+ let tabP2 = taskbar.createTaskbarTabPreview(docShell, controller);
+ tabP.visible = true;
+ tabP2.visible = true;
+
+ isnot(tabP2, null, "2nd Tab preview is not null");
+ isnot(tabP,tabP2, "Tab previews are different");
+ tabP.active = true;
+ ok(tabP.active && !tabP2.active, "Only one tab is active (part 1)");
+ tabP2.active = true;
+ ok(!tabP.active && tabP2.active, "Only one tab is active (part 2)");
+ tabP.active = true;
+ ok(tabP.active && !tabP2.active, "Only one tab is active (part 3)");
+ tabP.active = false;
+ ok(!tabP.active && !tabP2.active, "Neither tab is active");
+ is(winPreview.active, false, "Window preview is not active");
+ tabP.active = true;
+ winPreview.active = true;
+ ok(winPreview.active && !tabP.active, "Tab preview takes activation from window");
+ tabP.active = true;
+ ok(tabP.active && !winPreview.active, "Tab preview takes activation from window");
+
+ tabP.visible = false;
+ tabP2.visible = false;
+
+ SimpleTest.finish();
+ }
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+</window>
diff --git a/widget/tests/test_AltGr_key_events_in_web_content_on_windows.html b/widget/tests/test_AltGr_key_events_in_web_content_on_windows.html
new file mode 100644
index 0000000000..ff89f8fdad
--- /dev/null
+++ b/widget/tests/test_AltGr_key_events_in_web_content_on_windows.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Testing if AltGr keydown and keyup events are fired in web content on Windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+ <input id="input">
+</div>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function checkEvent(aEvent, aExpectedEvents, aDescription) {
+ if (!aExpectedEvents.length) {
+ ok(false, `${aDescription}: no more expected events ` +
+ `(type: ${aEvent.type}, code: ${aEvent.code}, key: ${aEvent.key}, keyCode: ${aEvent.keyCode}`);
+ }
+ let expectedEvent = aExpectedEvents.shift();
+ for (let property in expectedEvent) {
+ is(aEvent[property], expectedEvent[property], `${aDescription}: ${property}`);
+ }
+}
+
+async function runAltGrKeyTest() {
+ return new Promise(resolve => {
+ let target = document.getElementById("input");
+ target.focus();
+
+ let events = [
+ { type: "keydown", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL },
+ { type: "keydown", code: "AltRight", key: "AltGraph", keyCode: KeyboardEvent.DOM_VK_ALT },
+ { type: "keyup", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL },
+ { type: "keyup", code: "AltRight", key: "AltGraph", keyCode: KeyboardEvent.DOM_VK_ALT },
+ ];
+ function handleEvent(aEvent) {
+ checkEvent(aEvent, events, "runAltGrKeyTest");
+ if (aEvent.type === "keyup" && aEvent.code === "AltRight") {
+ is(events.length, 0, "runAltGrKeyTest: all expected events are fired");
+ SimpleTest.executeSoon(() => {
+ target.removeEventListener("keydown", handleEvent);
+ target.removeEventListener("keypress", handleEvent);
+ target.removeEventListener("keyup", handleEvent);
+ resolve();
+ });
+ }
+ }
+ target.addEventListener("keydown", handleEvent);
+ target.addEventListener("keypress", handleEvent);
+ target.addEventListener("keyup", handleEvent);
+
+ synthesizeNativeKey(KEYBOARD_LAYOUT_SPANISH, WIN_VK_RMENU, {},
+ "", "");
+ });
+}
+
+async function runEmulatingAltGrKeyTest() {
+ return new Promise(resolve => {
+ let target = document.getElementById("input");
+ target.focus();
+
+ let events = [
+ { type: "keydown", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL },
+ { type: "keydown", code: "AltLeft", key: "Alt", keyCode: KeyboardEvent.DOM_VK_ALT },
+ { type: "keyup", code: "AltLeft", key: "Alt", keyCode: KeyboardEvent.DOM_VK_ALT },
+ { type: "keyup", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL },
+ ];
+ function handleEvent(aEvent) {
+ checkEvent(aEvent, events, "runEmulatingAltGrKeyTest");
+ if (aEvent.type === "keyup" && aEvent.code === "ControlLeft") {
+ is(events.length, 0, "runAltGrKeyTest: all expected events are fired");
+ SimpleTest.executeSoon(() => {
+ target.removeEventListener("keydown", handleEvent);
+ target.removeEventListener("keypress", handleEvent);
+ target.removeEventListener("keyup", handleEvent);
+ resolve();
+ });
+ }
+ }
+ target.addEventListener("keydown", handleEvent);
+ target.addEventListener("keypress", handleEvent);
+ target.addEventListener("keyup", handleEvent);
+
+ synthesizeNativeKey(KEYBOARD_LAYOUT_SPANISH, WIN_VK_LMENU, { ctrlKey: true },
+ "", "");
+ });
+}
+
+async function runTests() {
+ await runAltGrKeyTest();
+ await runEmulatingAltGrKeyTest();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/widget/tests/test_actionhint.html b/widget/tests/test_actionhint.html
new file mode 100644
index 0000000000..6ab2cf40de
--- /dev/null
+++ b/widget/tests/test_actionhint.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Tests for action hint that is used by software keyboard</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/SpecialPowers.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<div>
+<form><input type="text" id="a1"><input type="text" id="a2"><input type="submit"></form>
+<form><input type="search" id="b1"><input type="submit"></form>
+<form><input type="text" id="c1"></form>
+<form><input type="text" id="d1"><textarea></textarea><input type="submit"></form>
+<form><input type="text" id="e1"><input type="number"><input type="submit"></form>
+<form><input type="text" id="f1"><input type="date"><input type="submit"></form>
+<form><input type="text" id="g1"><input type="radio"><input type="submit"></form>
+<form><input type="text" id="h1"><input type="text" readonly><input type="submit"></form>
+<form><input type="text" id="i1"><input type="text" disabled><input type="submit"></form>
+<input type="text" id="j1"><input type="text"><input type="button">
+<form><input type="text" id="k1"><a href="#foo">foo</a><input type="text"><input type="submit"></form>
+<form>
+ <input id="l1" enterkeyhint="enter">
+ <input id="l2" enterkeyhint="DONE">
+ <input id="l3" enterkeyhint="go">
+ <input id="l4" enterkeyhint="Next">
+ <input id="l5" enterkeyhint="Previous">
+ <input id="l6" enterkeyhint="search">
+ <textarea id="l7" enterkeyhint="send"></textarea>
+ <input id="l8" type="number" enterkeyhint="previous">
+ <input id="l9" type="date" enterkeyhint="done">
+ <input id="l10" type="time" enterkeyhint="done">
+ <input id="l11" enterkeyhint="NONE">
+</form>
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+add_task(async function setup() {
+ await new Promise(r => SimpleTest.waitForFocus(r));
+});
+
+add_task(async function basic() {
+ const tests = [
+ { id: "a1", hint: "maybenext", desc: "next element is type=text" },
+ { id: "a2", hint: "go", desc: "next element is type=submit" },
+ { id: "b1", hint: "search", desc: "current is type=search" },
+ { id: "c1", hint: "go", desc: "only this element" },
+ { id: "d1", hint: "maybenext", desc: "next element is textarea" },
+ { id: "e1", hint: "maybenext", desc: "next element is type=number" },
+ { id: "h1", hint: "go", desc: "next element is readonly" },
+ // XXX Feel free to change this result if you get some bugs reports
+ { id: "i1", hint: "go", desc: "next element is disabled" },
+ { id: "j1", hint: "", desc: "no form element" },
+ { id: "l1", hint: "enter", desc: "enterkeyhint=enter" },
+ { id: "l2", hint: "done", desc: "enterkeyhint=DONE" },
+ { id: "l3", hint: "go", desc: "enterkeyhint=go" },
+ { id: "l4", hint: "next", desc: "enterkeyhint=Next" },
+ { id: "l5", hint: "previous", desc: "enterkeyhint=Previous" },
+ { id: "l6", hint: "search", desc: "enterkeyhint=search" },
+ { id: "l7", hint: "send", desc: "enterkeyhint=send" },
+ { id: "l8", hint: "previous", desc: "type=number enterkeyhint=previous" },
+ // type=date is readonly content
+ { id: "l9", hint: "", desc: "type=date enterkeyhint=done" },
+ // type=time is readonly content
+ { id: "l10", hint: "", desc: "type=time enterkeyhint=done" },
+ // Since enterkeyhint is invalid, we infer action hint. So feel free to change this.
+ { id: "l11", hint: "go", desc: "enterkeyhint is invalid" },
+ ];
+
+ const todo_tests = [
+ { id: "f1", hint: "maybenext", desc: "next element is type=date" },
+ { id: "k1", hint: "", desc: "next is anchor link" },
+ ];
+
+
+ for (let test of tests) {
+ document.getElementById(test.id).focus();
+ is(SpecialPowers.DOMWindowUtils.focusedActionHint, test.hint, test.desc);
+ }
+
+ for (let test of todo_tests) {
+ document.getElementById(test.id).focus();
+ todo_is(SpecialPowers.DOMWindowUtils.focusedActionHint, test.hint, test.desc);
+ }
+});
+
+add_task(async function dynamicChange() {
+ let element = document.getElementById("l1");
+ element.focus();
+ is(SpecialPowers.DOMWindowUtils.focusedActionHint, "enter",
+ "Initial enterKeyHint");
+
+ element.setAttribute("enterkeyhint", "next");
+ is(SpecialPowers.DOMWindowUtils.focusedActionHint, "next",
+ "enterKeyHint in InputContext has to sync with enterkeyhint attribute");
+
+ element.enterKeyHint = "search";
+ is(SpecialPowers.DOMWindowUtils.focusedActionHint, "search",
+ "enterKeyHint in InputContext has to sync with enterKeyHint setter");
+
+ document.getElementById("l2").setAttribute("enterkeyhint", "send");
+ is(SpecialPowers.DOMWindowUtils.focusedActionHint, "search",
+ "enterKeyHint in InputContext keeps focused enterKeyHint value");
+
+ // Storing the original value may be safer.
+ element.enterkeyhint = "enter";
+ document.getElementById("l2").enterKeyHint = "done";
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/widget/tests/test_alwaysontop_focus.xhtml b/widget/tests/test_alwaysontop_focus.xhtml
new file mode 100644
index 0000000000..b9cc3ee33c
--- /dev/null
+++ b/widget/tests/test_alwaysontop_focus.xhtml
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta charset="utf-8" />
+ <title>Test that alwaysontop windows do not pull focus when opened.</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script><![CDATA[
+ add_task(async function testAlwaysOnTop() {
+ let topWin = window.docShell.rootTreeItem.domWindow;
+ await SimpleTest.promiseFocus(topWin);
+ is(Services.focus.activeWindow, topWin, "Top level window is focused");
+
+ let newWin = Services.ww.openWindow(
+ null,
+ "about:blank",
+ null,
+ "chrome,alwaysontop,width=300,height=300",
+ null
+ );
+ await new Promise(resolve => {
+ newWin.addEventListener("load", resolve, { once: true });
+ });
+
+ // Wait one tick of the event loop to give the window a chance to focus.
+ await new Promise(resolve => { SimpleTest.executeSoon(resolve); });
+
+ is(Services.focus.activeWindow, topWin, "Top level window is still focused");
+ newWin.close();
+ });
+ ]]></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/widget/tests/test_assign_event_data.html b/widget/tests/test_assign_event_data.html
new file mode 100644
index 0000000000..1da9bb535f
--- /dev/null
+++ b/widget/tests/test_assign_event_data.html
@@ -0,0 +1,708 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Testing ns*Event::Assign*EventData()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+ <style>
+ #a {
+ background-color: transparent;
+ transition: background-color 0.1s linear;
+ }
+ #a:focus {
+ background-color: red;
+ }
+ .slidin {
+ border: green 1px solid;
+ width: 10px;
+ height: 10px;
+ animation-name: slidein;
+ animation-duration: 1s;
+ }
+ @keyframes slidein {
+ from {
+ margin-left: 100%;
+ }
+ to {
+ margin-left: 0;
+ }
+ }
+ #pointer-target {
+ border: 1px dashed red;
+ background: yellow;
+ margin: 0px 10px;
+ padding: 0px 10px;
+ }
+ #scrollable-div {
+ background: green;
+ overflow: auto;
+ width: 30px;
+ height: 30px;
+ }
+ #scrolled-div {
+ background: magenta;
+ width: 10px;
+ height: 10px;
+ }
+ #form {
+ background: silver;
+ padding: 0px 10px;
+ }
+ #animated-div {
+ background: cyan;
+ padding: 0px 10px;
+ }
+ </style>
+</head>
+<body>
+<div id="display">
+ <input id="input-text">
+ <button id="button">button</button>
+ <a id="a" href="about:blank">hyper link</a>
+ <span id="pointer-target">span</span>
+ <div id="scrollable-div"><div id="scrolled-div"></div></div>
+ <form id="form">form</form>
+ <div id="animated-div">&nbsp;</div>
+</div>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 34);
+
+const kIsMac = (navigator.platform.indexOf("Mac") == 0);
+const kIsWin = (navigator.platform.indexOf("Win") == 0);
+
+var gDescription = "";
+var gEvent = null;
+var gCopiedEvent = [];
+var gCallback = null;
+var gCallPreventDefault = false;
+
+function onEvent(aEvent) {
+ if (gCallPreventDefault) {
+ aEvent.preventDefault();
+ }
+ ok(gEvent === null, gDescription + `: We should receive only one event to check per test: already got: ${gEvent ? gEvent.type : "null"}, got: ${aEvent.type}`);
+ gEvent = aEvent;
+ for (var attr in aEvent) {
+ if (!attr.match(/^[A-Z0-9_]+$/) && // ignore const attributes
+ attr != "multipleActionsPrevented" && // multipleActionsPrevented isn't defined in any DOM event specs.
+ typeof(aEvent[attr]) != "function") {
+ gCopiedEvent.push({ name: attr, value: aEvent[attr]});
+ }
+ }
+ setTimeout(gCallback, 0);
+}
+
+function observeKeyUpOnContent(aKeyCode, aCallback) {
+ document.addEventListener("keyup", function keyUp(ev) {
+ if (ev.keyCode == aKeyCode) {
+ document.removeEventListener("keyup", keyUp);
+ SimpleTest.executeSoon(aCallback);
+ }
+ });
+}
+
+const kTests = [
+ { description: "InternalScrollPortEvent (overflow, vertical)",
+ targetID: "scrollable-div", eventType: "overflow",
+ dispatchEvent() {
+ document.getElementById("scrolled-div").style.height = "500px";
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalScrollPortEvent (overflow, horizontal)",
+ targetID: "scrollable-div", eventType: "overflow",
+ dispatchEvent() {
+ document.getElementById("scrolled-div").style.width = "500px";
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalScrollAreaEvent (MozScrolledAreaChanged, spreading)",
+ target() { return document; }, eventType: "MozScrolledAreaChanged",
+ dispatchEvent() {
+ document.getElementById("scrollable-div").style.width = "50000px";
+ document.getElementById("scrollable-div").style.height = "50000px";
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalScrollAreaEvent (MozScrolledAreaChanged, shrinking)",
+ target() { return document; }, eventType: "MozScrolledAreaChanged",
+ dispatchEvent() {
+ document.getElementById("scrollable-div").style.width = "30px";
+ document.getElementById("scrollable-div").style.height = "30px";
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetKeyboardEvent (keydown of 'a' key without modifiers)",
+ targetID: "input-text", eventType: "keydown",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_A : MAC_VK_ANSI_A,
+ {}, "a", "a");
+ observeKeyUpOnContent(KeyboardEvent.DOM_VK_A, runNextTest);
+ return true;
+ },
+ canRun() {
+ return (kIsMac || kIsWin);
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetKeyboardEvent (keyup of 'a' key without modifiers)",
+ targetID: "input-text", eventType: "keydown",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_A : MAC_VK_ANSI_A,
+ {}, "a", "a");
+ observeKeyUpOnContent(KeyboardEvent.DOM_VK_A, runNextTest);
+ return true;
+ },
+ canRun() {
+ return (kIsMac || kIsWin);
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetKeyboardEvent (keypress of 'b' key with Shift)",
+ targetID: "input-text", eventType: "keypress",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_B : MAC_VK_ANSI_B,
+ { shiftKey: true }, "B", "B");
+
+ // On Windows, synthesizeNativeKey will also fire keyup for shiftKey.
+ // We have to wait for it to prevent the key event break the next test case.
+ let waitKeyCode = _EU_isWin(window) ? KeyboardEvent.DOM_VK_SHIFT :
+ KeyboardEvent.DOM_VK_B;
+ observeKeyUpOnContent(waitKeyCode, runNextTest);
+ return true;
+ },
+ canRun() {
+ return (kIsMac || kIsWin);
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetKeyboardEvent (keyup during composition)",
+ targetID: "input-text", eventType: "keyup",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeCompositionChange({ "composition":
+ { "string": "\u306D",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ synthesizeComposition({ type: "compositioncommitasis", key: {} });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetKeyboardEvent (keydown during composition)",
+ targetID: "input-text", eventType: "keydown",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeCompositionChange({ "composition":
+ { "string": "\u306D",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": {},
+ });
+ synthesizeComposition({ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" } });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetMouseEvent (mousedown of left button without modifier)",
+ targetID: "button", eventType: "mousedown",
+ dispatchEvent() {
+ synthesizeMouseAtCenter(document.getElementById(this.targetID),
+ { button: 0 });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetMouseEvent (click of middle button with Shift)",
+ targetID: "button", eventType: "auxclick",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeMouseAtCenter(document.getElementById(this.targetID),
+ { button: 1, shiftKey: true, pressure: 0.5 });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetMouseEvent (mouseup of right button with Alt)",
+ targetID: "button", eventType: "mouseup",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeMouseAtCenter(document.getElementById(this.targetID),
+ { button: 2, altKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetDragEvent",
+ targetID: "input-text", eventType: "dragstart",
+ dispatchEvent() {
+
+ },
+ canRun() {
+ todo(false, "WidgetDragEvent isn't tested");
+ return false;
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetCompositionEvent (compositionupdate)",
+ targetID: "input-text", eventType: "compositionupdate",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeComposition({ type: "compositioncommit", data: "\u30E9\u30FC\u30E1\u30F3", key: { key: "KEY_Enter" } });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "InternalEditorInputEvent (input at key input)",
+ targetID: "input-text", eventType: "input",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_B : MAC_VK_ANSI_B,
+ { shiftKey: true }, "B", "B");
+ observeKeyUpOnContent(KeyboardEvent.DOM_VK_B, runNextTest);
+ return true;
+ },
+ canRun() {
+ return (kIsMac || kIsWin);
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalEditorInputEvent (input at composing)",
+ targetID: "input-text", eventType: "input",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ document.getElementById(this.targetID).focus();
+ synthesizeCompositionChange({ "composition":
+ { "string": "\u30E9\u30FC\u30E1\u30F3",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 4, "length": 0 },
+ "key": { key: "y" },
+ });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "InternalEditorInputEvent (input at committing)",
+ targetID: "input-text", eventType: "input",
+ dispatchEvent() {
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetMouseScrollEvent (DOMMouseScroll, vertical)",
+ targetID: "input-text", eventType: "DOMMouseScroll",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeWheel(document.getElementById(this.targetID), 3, 4,
+ { deltaY: 30, lineOrPageDeltaY: 2 });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetMouseScrollEvent (DOMMouseScroll, horizontal)",
+ targetID: "input-text", eventType: "DOMMouseScroll",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeWheel(document.getElementById(this.targetID), 4, 5,
+ { deltaX: 30, lineOrPageDeltaX: 2, shiftKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetMouseScrollEvent (MozMousePixelScroll, vertical)",
+ targetID: "input-text", eventType: "MozMousePixelScroll",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeWheel(document.getElementById(this.targetID), 3, 4,
+ { deltaY: 20, lineOrPageDeltaY: 1, altKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetMouseScrollEvent (MozMousePixelScroll, horizontal)",
+ targetID: "input-text", eventType: "MozMousePixelScroll",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeWheel(document.getElementById(this.targetID), 4, 5,
+ { deltaX: 20, lineOrPageDeltaX: 1, ctrlKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetWheelEvent (wheel, vertical)",
+ targetID: "input-text", eventType: "wheel",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeWheel(document.getElementById(this.targetID), 3, 4,
+ { deltaY: 20, lineOrPageDeltaY: 1, altKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetWheelEvent (wheel, horizontal)",
+ targetID: "input-text", eventType: "wheel",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeWheel(document.getElementById(this.targetID), 4, 5,
+ { deltaX: 20, lineOrPageDeltaX: 1, ctrlKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetWheelEvent (wheel, both)",
+ targetID: "input-text", eventType: "wheel",
+ dispatchEvent() {
+ document.getElementById(this.targetID).value = "";
+ synthesizeWheel(document.getElementById(this.targetID), 4, 5,
+ { deltaX: 20, deltaY: 10,
+ lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetTouchEvent (touchstart)",
+ target() { return document; }, eventType: "touchstart",
+ dispatchEvent() {
+ synthesizeTouchAtPoint(1, 2, { id: 10, rx: 4, ry: 3, angle: 0, force: 1, shiftKey: true});
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetTouchEvent (touchend)",
+ target() { return document; }, eventType: "touchend",
+ dispatchEvent() {
+ synthesizeTouchAtPoint(4, 6, { id: 5, rx: 1, ry: 2, angle: 0.5, force: 0.8, ctrlKey: true});
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "InternalFormEvent (reset)",
+ targetID: "form", eventType: "reset",
+ dispatchEvent() {
+ document.getElementById("form").reset();
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "WidgetCommandEvent",
+ targetID: "input-text", eventType: "",
+ dispatchEvent() {
+
+ },
+ canRun() {
+ todo(false, "WidgetCommandEvent isn't tested");
+ return false;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalClipboardEvent (copy)",
+ targetID: "input-text", eventType: "copy",
+ dispatchEvent() {
+ document.getElementById("input-text").value = "go to clipboard!";
+ document.getElementById("input-text").focus();
+ document.getElementById("input-text").select();
+ synthesizeKey("c", { accelKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [ ],
+ },
+ { description: "InternalUIEvent (DOMActivate)",
+ targetID: "button", eventType: "DOMActivate",
+ dispatchEvent() {
+ synthesizeMouseAtCenter(document.getElementById(this.targetID),
+ { button: 0, shiftKey: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalFocusEvent (focus)",
+ targetID: "input-text", eventType: "focus",
+ dispatchEvent() {
+ document.getElementById(this.targetID).focus();
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalFocusEvent (blur)",
+ targetID: "input-text", eventType: "blur",
+ dispatchEvent() {
+ document.getElementById(this.targetID).blur();
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "WidgetSimpleGestureEvent",
+ targetID: "", eventType: "",
+ dispatchEvent() {
+
+ },
+ canRun() {
+ // Simple gesture event may be handled before it comes content.
+ // So, we cannot test it in this test.
+ todo(false, "WidgetSimpleGestureEvent isn't tested");
+ return false;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalTransitionEvent (transitionend)",
+ targetID: "a", eventType: "transitionend",
+ dispatchEvent() {
+ document.getElementById(this.targetID).focus();
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalAnimationEvent (animationend)",
+ targetID: "animated-div", eventType: "animationend",
+ dispatchEvent() {
+ document.getElementById(this.targetID).className = "slidin";
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalMutationEvent (DOMAttrModified)",
+ targetID: "animated-div", eventType: "DOMAttrModified",
+ dispatchEvent() {
+ document.getElementById(this.targetID).setAttribute("x-data", "foo");
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalMutationEvent (DOMNodeInserted)",
+ targetID: "animated-div", eventType: "DOMNodeInserted",
+ dispatchEvent() {
+ var div = document.createElement("div");
+ div.id = "inserted-div";
+ document.getElementById("animated-div").appendChild(div);
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "InternalMutationEvent (DOMNodeRemoved)",
+ targetID: "animated-div", eventType: "DOMNodeRemoved",
+ dispatchEvent() {
+ document.getElementById("animated-div").removeChild(document.getElementById("inserted-div"));
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "PointerEvent (pointerdown)",
+ targetID: "pointer-target", eventType: "mousedown",
+ dispatchEvent() {
+ var elem = document.getElementById(this.targetID);
+ var rect = elem.getBoundingClientRect();
+ synthesizeMouse(elem, rect.width / 2, rect.height / 2,
+ { type: this.eventType, button: 1, clickCount: 1, inputSource: 2, pressure: 0.25, isPrimary: true });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+ { description: "PointerEvent (pointerup)",
+ targetID: "pointer-target", eventType: "mouseup",
+ dispatchEvent() {
+ var elem = document.getElementById(this.targetID);
+ var rect = elem.getBoundingClientRect();
+ synthesizeMouse(elem, rect.width / 2, rect.height / 2,
+ { type: this.eventType, button: -1, ctrlKey: true, shiftKey: true, altKey: true, isSynthesized: false });
+ },
+ canRun() {
+ return true;
+ },
+ todoMismatch: [],
+ },
+];
+
+function doTest(aTest) {
+ if (!aTest.canRun()) {
+ SimpleTest.executeSoon(runNextTest);
+ return;
+ }
+ gEvent = null;
+ gCopiedEvent = [];
+ gDescription = aTest.description + " (gCallPreventDefault=" + gCallPreventDefault + ")";
+ var target = aTest.target ? aTest.target() : document.getElementById(aTest.targetID);
+ target.addEventListener(aTest.eventType, onEvent, true);
+ gCallback = function() {
+ target.removeEventListener(aTest.eventType, onEvent, true);
+ ok(gEvent !== null, gDescription + ": failed to get duplicated event");
+ ok(!!gCopiedEvent.length, gDescription + ": count of attribute of the event must be larger than 0");
+ for (var i = 0; i < gCopiedEvent.length; ++i) {
+ var name = gCopiedEvent[i].name;
+ if (name == "rangeOffset") {
+ todo(false, gDescription + ": " + name + " attribute value is never reset (" + gEvent[name] + ")");
+ } else if (name == "eventPhase") {
+ is(gEvent[name], 0, gDescription + ": mismatch with fixed value (" + name + ")");
+ } else if (name == "rangeParent" || name == "currentTarget") {
+ is(gEvent[name], null, gDescription + ": mismatch with fixed value (" + name + ")");
+ } else if (aTest.todoMismatch.includes(name)) {
+ todo_is(gEvent[name], gCopiedEvent[i].value, gDescription + ": mismatch (" + name + ")");
+ } else if (name == "offsetX" || name == "offsetY") {
+ // do nothing; these are defined to return different values during event dispatch
+ // vs not during event dispatch
+ } else {
+ is(gEvent[name], gCopiedEvent[i].value, gDescription + ": mismatch (" + name + ")");
+ }
+ }
+ if (!testWillCallRunNextTest) {
+ runNextTest();
+ }
+ };
+ var testWillCallRunNextTest = aTest.dispatchEvent();
+}
+
+var gIndex = -1;
+function runNextTest() {
+ if (++gIndex == kTests.length) {
+ if (gCallPreventDefault) {
+ finish();
+ return;
+ }
+ // Test with a call of preventDefault() of the events.
+ gCallPreventDefault = true;
+ gIndex = -1;
+ // Restoring the initial state of all elements.
+ document.getElementById("scrollable-div").style.height = "30px";
+ document.getElementById("scrollable-div").style.width = "30px";
+ document.getElementById("scrolled-div").style.height = "10px";
+ document.getElementById("scrolled-div").style.width = "10px";
+ document.getElementById("input-text").value = "";
+ document.getElementById("animated-div").className = "";
+ document.getElementById("animated-div").removeAttribute("x-data");
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+ window.requestAnimationFrame(function() {
+ setTimeout(runNextTest, 0);
+ });
+ return;
+ }
+ doTest(kTests[gIndex]);
+}
+
+function init() {
+ SpecialPowers.pushPrefEnv({"set": [["middlemouse.contentLoadURL", false],
+ ["middlemouse.paste", false],
+ ["general.autoScroll", false],
+ ["mousewheel.default.action", 0],
+ ["mousewheel.default.action.override_x", -1],
+ ["mousewheel.with_shift.action", 0],
+ ["mousewheel.with_shift.action.override_x", -1],
+ ["mousewheel.with_control.action", 0],
+ ["mousewheel.with_control.action.override_x", -1],
+ ["mousewheel.with_alt.action", 0],
+ ["mousewheel.with_alt.action.override_x", -1],
+ ["mousewheel.with_meta.action", 0],
+ ["mousewheel.with_meta.action.override_x", -1]]}, runNextTest);
+}
+
+function finish() {
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(init);
+
+</script>
+</body>
diff --git a/widget/tests/test_autocapitalize.html b/widget/tests/test_autocapitalize.html
new file mode 100644
index 0000000000..de628d1190
--- /dev/null
+++ b/widget/tests/test_autocapitalize.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Tests for autocapitalize that is used by software keyboard</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/SpecialPowers.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+
+<div>
+<input type="text" id="a1"><br>
+<input type="text" id="a2" autocapitalize="characters"><br>
+<input type="text" id="a3" autocapitalize="sentences"><br>
+<input type="text" id="a4" autocapitalize="words"><br>
+<input type="text" id="a5" autocapitalize="off"><br>
+<input type="text" id="a6" autocapitalize="on"><br>
+<input type="url" id="a7" autocapitalize="on"><br>
+<input type="email" id="a8" autocapitalize="on"><br>
+<input type="password" id="a9" autocapitalize="on"><br>
+<textarea id="b1" autocapitalize="characters"></textarea><br>
+<div contenteditable id="c1" autocapitalize="sentences"></div><br>
+<form><input type="text" id="d1" autocapitalize="words"></form><br>
+<form autocapitalize="on"><input type="text" id="d2"></form><br>
+<form autocapitalize="off"><input type="text" id="d3" autocapitalize="on"></form><br>
+</div>
+
+<pre id="test">
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(async () => {
+ const tests = [
+ { id: "a1", autocapitalize: "", desc: "input without autocapitalize" },
+ { id: "a2", autocapitalize: "characters", desc: "input with autocapitalize=characters" },
+ { id: "a3", autocapitalize: "sentences", desc: "input with autocapitalize=sentences" },
+ { id: "a4", autocapitalize: "words", desc: "input with autocapitalize=words" },
+ { id: "a5", autocapitalize: "none", desc: "input with autocapitalize=off" },
+ { id: "a6", autocapitalize: "sentences", desc: "input with autocapitalize=on" },
+ { id: "a7", autocapitalize: "", desc: "input with type=url and autocapitalize=on" },
+ { id: "a8", autocapitalize: "", desc: "input with type=email and autocapitalize=on" },
+ { id: "a9", autocapitalize: "", desc: "input with type=password and autocapitalize=on" },
+ { id: "b1", autocapitalize: "characters", desc: "textarea with autocapitalize=characters" },
+ { id: "c1", autocapitalize: "sentences", desc: "contenteditable with autocapitalize=sentences" },
+ { id: "d1", autocapitalize: "words", desc: "input with autocapitalize=words in form" },
+ { id: "d2", autocapitalize: "sentences", desc: "input in form with autocapitalize=on" },
+ { id: "d3", autocapitalize: "sentences", desc: "input with autocapitalize=on in form" },
+ ];
+
+ for (let test of tests) {
+ document.getElementById(test.id).focus();
+ is(SpecialPowers.DOMWindowUtils.focusedAutocapitalize, test.autocapitalize, test.desc);
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/widget/tests/test_bug1123480.xhtml b/widget/tests/test_bug1123480.xhtml
new file mode 100644
index 0000000000..18a40202fd
--- /dev/null
+++ b/widget/tests/test_bug1123480.xhtml
@@ -0,0 +1,155 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1123480
+-->
+<window title="Mozilla Bug 1123480"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="RunTest();">
+ <title>nsTransferable PBM Overflow Selection Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ // Create over 1 MB of sample garbage text. JavaScript strings are represented by
+ // UTF16 strings, so the size is twice as much as the actual string length.
+ // This value is chosen such that the size of the memory for the string exceeds
+ // the kLargeDatasetSize threshold in nsTransferable.h.
+ // It is also not a round number to reduce the odds of having an accidental
+ // collisions with another file (since the test below looks at the file size
+ // to identify the file).
+ var Ipsum = "0123456789".repeat(1234321);
+ var IpsumByteLength = Ipsum.length * 2;
+ var SHORT_STRING_NO_CACHE = "short string that will never be cached to the disk";
+
+ function isWindows() {
+ const {AppConstants} = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ return AppConstants.platform === 'win';
+ }
+
+ // Get a list of open file descriptors that refer to a file with the same size as
+ // the expected data (and assume that any mutations in file descriptor counts
+ // are caused by our test).
+ function getClipboardCacheFDCount() {
+ var dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ if (isWindows()) {
+ // On Windows, nsAnonymousTemporaryFile does not immediately delete the file.
+ // Instead, the Windows-specific FILE_FLAG_DELETE_ON_CLOSE flag is used,
+ // which means that the file is deleted when the last handle is closed.
+ // Apparently, this flag is unreliable (e.g. when the application crashes),
+ // so nsAnonymousTemporaryFile stores the temporary files in a subdirectory,
+ // which is cleaned up some time after start-up.
+
+ // This is just a test, and during the test we deterministically close the
+ // handles, so if FILE_FLAG_DELETE_ON_CLOSE does the thing it promises, the
+ // file is actually removed when the handle is closed.
+
+ // Path from nsAnonymousTemporaryFile.cpp, GetTempDir.
+ dir.initWithPath(PathUtils.join(PathUtils.tempDir, "mozilla-temp-files"));
+ } else {
+ dir.initWithPath("/dev/fd");
+ }
+ var count = 0;
+ for (var de = dir.directoryEntries; de.hasMoreElements(); ) {
+ var fdFile = de.nextFile;
+ var fileSize;
+ try {
+ fileSize = fdFile.fileSize;
+ } catch (e) {
+ // This can happen on macOS.
+ continue;
+ }
+ if (fileSize === IpsumByteLength) {
+ // Assume that the file was created by us if the size matches.
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ async function RunTest() {
+ SimpleTest.waitForExplicitFinish();
+ const gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+
+ // Sanitize environment
+ gClipboardHelper.copyString(SHORT_STRING_NO_CACHE);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ var initialFdCount = getClipboardCacheFDCount();
+
+ // Overflow a nsTransferable region by using the clipboard helper
+ gClipboardHelper.copyString(Ipsum);
+
+ // gClipboardHelper.copyString also puts the data on the selection
+ // clipboard if the platform supports it.
+ var expectedFdDelta = Services.clipboard.isClipboardTypeSupported(Services.clipboard.kSelectionClipboard) ? 2 : 1;
+ // Undefined private browsing mode should cache to disk
+ is(getClipboardCacheFDCount(), initialFdCount + expectedFdDelta, "should cache to disk when PBM is undefined");
+
+ // Sanitize environment again.
+ gClipboardHelper.copyString(SHORT_STRING_NO_CACHE);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ is(getClipboardCacheFDCount(), initialFdCount, "should have cleared the clipboard data");
+
+ // Repeat procedure of plain text selection with private browsing
+ // disabled and enabled
+ const {PrivateBrowsingUtils} = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+ );
+ for (let private of [false, true]) {
+ var win = window.browsingContext.topChromeWindow.open("about:blank", "_blank", "chrome, width=500, height=200" + (private ? ", private" : ""));
+ ok(win, private ? "should open private window" : "should open non-private window");
+ is(PrivateBrowsingUtils.isContentWindowPrivate(win), private, "used correct window context");
+
+ // Select plaintext in private/non-private channel
+ const nsTransferable = Components.Constructor("@mozilla.org/widget/transferable;1", "nsITransferable");
+ const nsSupportsString = Components.Constructor("@mozilla.org/supports-string;1", "nsISupportsString");
+ var Loadctx = PrivateBrowsingUtils.privacyContextFromWindow(win);
+ var Transfer = nsTransferable();
+ var Suppstr = nsSupportsString();
+ Suppstr.data = Ipsum;
+ Transfer.init(Loadctx);
+ Transfer.addDataFlavor("text/plain");
+ Transfer.setTransferData("text/plain", Suppstr);
+
+ // Enabled private browsing mode should not cache any selection to disk; disabled should
+ if (private) {
+ is(getClipboardCacheFDCount(), initialFdCount, "did not violate private browsing mode");
+ } else {
+ is(getClipboardCacheFDCount(), initialFdCount + 1, "should save memory by caching non-private clipboard data to disk");
+ }
+
+ // Share the transferable with the system.
+ Services.clipboard.setData(Transfer, null, Services.clipboard.kGlobalClipboard);
+ if (private) {
+ is(getClipboardCacheFDCount(), initialFdCount, "did not violate private browsing mode");
+ } else {
+ is(getClipboardCacheFDCount(), initialFdCount + 1, "should save memory by caching non-private clipboard data to disk");
+ }
+
+ // Sanitize the environment.
+ Suppstr = nsSupportsString();
+ Suppstr.data = SHORT_STRING_NO_CACHE;
+ Transfer.setTransferData("text/plain", Suppstr);
+ await new Promise(resolve => setTimeout(resolve, 0));
+ is(getClipboardCacheFDCount(), initialFdCount, "should drop the cache file, if any.");
+
+ Services.clipboard.setData(Transfer, null, Services.clipboard.kGlobalClipboard);
+ is(getClipboardCacheFDCount(), initialFdCount, "should postsanitize the environment");
+ }
+
+ SimpleTest.finish();
+ }
+ ]]>
+ </script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1123480"
+ target="_blank">Mozilla Bug 1123480</a>
+ </body>
+</window>
diff --git a/widget/tests/test_bug343416.xhtml b/widget/tests/test_bug343416.xhtml
new file mode 100644
index 0000000000..bf4dbbb9b4
--- /dev/null
+++ b/widget/tests/test_bug343416.xhtml
@@ -0,0 +1,191 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=343416
+-->
+<window title="Mozilla Bug 343416"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=343416">Mozilla Bug 343416</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 343416 */
+SimpleTest.waitForExplicitFinish();
+
+// Observer:
+var idleObserver =
+{
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe: function _observe(subject, topic, data)
+ {
+ if (topic != "idle")
+ return;
+
+// var diff = Math.abs(data - newIdleSeconds * 1000);
+// ok (diff < 5000, "The idle time should have increased by roughly 6 seconds, " +
+// "as that's when we told this listener to fire.");
+// if (diff >= 5000)
+// alert(data + " " + newIdleSeconds);
+
+ // Attempt to get to the nsIUserIdleService
+ var subjectOK = false;
+ try {
+ var idleService = subject.QueryInterface(nsIUserIdleService);
+ subjectOK = true;
+ }
+ catch (ex)
+ {}
+ ok(subjectOK, "The subject of the notification should be the " +
+ "nsIUserIdleService.");
+
+ // Attempt to remove ourselves.
+ var removedObserver = false;
+ try {
+ idleService.removeIdleObserver(this, newIdleSeconds);
+ removedObserver = true;
+ }
+ catch (ex)
+ {}
+ ok(removedObserver, "We should be able to remove our observer here.");
+ finishedListenerOK = true;
+ if (finishedTimeoutOK)
+ {
+ clearTimeout(testBailout);
+ finishThisTest();
+ }
+ }
+};
+
+
+const nsIUserIdleService = Ci.nsIUserIdleService;
+const nsIISCID = "@mozilla.org/widget/useridleservice;1";
+var idleService = null;
+try
+{
+ idleService = Cc[nsIISCID].getService(nsIUserIdleService);
+}
+catch (ex)
+{}
+
+ok(idleService, "nsIUserIdleService should exist and be implemented on all tier 1 platforms.");
+
+var idleTime = null;
+var gotIdleTime = false;
+try
+{
+ idleTime = idleService.idleTime;
+ gotIdleTime = true;
+}
+catch (ex)
+{}
+
+ok (gotIdleTime, "Getting the idle time should not fail " +
+ "in normal circumstances on any tier 1 platform.");
+
+// Now we set up a timeout to sanity-test the idleTime after 5 seconds
+setTimeout(testIdleTime, 5000);
+var startTimeStamp = Date.now();
+
+// Now we add the listener:
+var newIdleSeconds = Math.floor(idleTime / 1000) + 6;
+var addedObserver = false;
+try
+{
+ idleService.addIdleObserver(idleObserver, newIdleSeconds);
+ addedObserver = true;
+}
+catch (ex)
+{}
+
+ok(addedObserver, "The nsIUserIdleService should allow us to add an observer.");
+
+addedObserver = false;
+try
+{
+ idleService.addIdleObserver(idleObserver, newIdleSeconds);
+ addedObserver = true;
+}
+catch (ex)
+{}
+
+ok(addedObserver, "The nsIUserIdleService should allow us to add the same observer again.");
+
+var removedObserver = false;
+try
+{
+ idleService.removeIdleObserver(idleObserver, newIdleSeconds);
+ removedObserver = true;
+}
+catch (ex)
+{}
+
+ok(removedObserver, "The nsIUserIdleService should allow us to remove the observer just once.");
+
+function testIdleTime()
+{
+ /* eslint-disable-next-line no-shadow */
+ var gotIdleTime = false
+ try
+ {
+ var newIdleTime = idleService.idleTime;
+ gotIdleTime = true
+ }
+ catch (ex)
+ {}
+ ok(gotIdleTime, "Getting the idle time should not fail " +
+ "in normal circumstances on any tier 1 platform.");
+ // Get the time difference, remove the approx. 5 seconds that we've waited,
+ // should be very close to 0 left.
+ var timeDiff = Math.abs((newIdleTime - idleTime) -
+ (Date.now() - startTimeStamp));
+
+ // 1.5 second leniency.
+ ok(timeDiff < 1500, "The idle time should have increased by roughly the " +
+ "amount of time it took for the timeout to fire. " +
+ "You didn't touch the mouse or keyboard during the " +
+ "test did you?");
+ finishedTimeoutOK = true;
+}
+
+// make sure we still exit when the listener and/or setTimeout don't fire:
+var testBailout = setTimeout(finishThisTest, 12000);
+var finishedTimeoutOK = false, finishedListenerOK = false;
+function finishThisTest()
+{
+ ok(finishedTimeoutOK, "We set a timeout and it should have fired by now.");
+ ok(finishedListenerOK, "We added a listener and it should have been called by now.");
+ if (!finishedListenerOK)
+ {
+ var removedListener = false;
+ try
+ {
+ idleService.removeIdleObserver(idleObserver, newIdleSeconds);
+ removedListener = true;
+ }
+ catch (ex)
+ {}
+
+ ok(removedListener, "We added a listener and we should be able to remove it.");
+ }
+ // Done:
+ SimpleTest.finish();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug428405.xhtml b/widget/tests/test_bug428405.xhtml
new file mode 100644
index 0000000000..25d796ab7b
--- /dev/null
+++ b/widget/tests/test_bug428405.xhtml
@@ -0,0 +1,168 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window id="window1" title="Test Bug 428405"
+ onload="setGlobals(); loadFirstTab();"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"/>
+
+ <tabbox id="tabbox" style="-moz-box-flex: 100">
+ <tabs>
+ <tab label="Tab 1"/>
+ <tab label="Tab 2"/>
+ </tabs>
+ <tabpanels style="-moz-box-flex: 100">
+ <browser onload="configureFirstTab();" id="tab1browser" style="-moz-box-flex: 100"/>
+ <browser onload="configureSecondTab();" id="tab2browser" style="-moz-box-flex: 100"/>
+ </tabpanels>
+ </tabbox>
+
+ <script type="application/javascript"><![CDATA[
+ const {BrowserTestUtils} = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+ );
+
+ SimpleTest.waitForExplicitFinish();
+
+ var gCmdOptYReceived = false;
+
+ // Look for a cmd-opt-y event.
+ function onKeyPress(aEvent) {
+ gCmdOptYReceived = false;
+ if (String.fromCharCode(aEvent.charCode) != 'y')
+ return;
+ if (aEvent.ctrlKey || aEvent.shiftKey || !aEvent.metaKey || !aEvent.altKey)
+ return;
+ gCmdOptYReceived = true;
+ }
+
+ function setGlobals() {
+ let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ // For some reason, a global <key> element's oncommand handler only gets
+ // invoked if the focus is outside both of the <browser> elements
+ // (tab1browser and tab2browser). So, to make sure we can see a
+ // cmd-opt-y event in window1 (if one is available), regardless of where
+ // the focus is in this window, we need to add a "keypress" event
+ // listener to gChromeWindow, and then check (in onKeyPress()) to see if
+ // it's a cmd-opt-y event.
+ chromeWindow.addEventListener("keypress", onKeyPress);
+ }
+
+ // 1) Start loading first tab.
+ // 6) Start reloading first tab.
+ function loadFirstTab() {
+ var browser = document.getElementById("tab1browser");
+ BrowserTestUtils.startLoadingURIString(browser, "data:text/html;charset=utf-8,<body><h2>First Tab</h2><p><input type='submit' value='Button' id='button1'/></body>");
+ }
+
+ function configureFirstTab() {
+ try {
+ var button = document.getElementById("tab1browser").contentDocument.getElementById("button1");
+ button.addEventListener("click", onFirstTabButtonClicked);
+ button.focus();
+ if (document.getElementById("tabbox").selectedIndex == 0) {
+ // 2) When first tab has finished loading (while first tab is
+ // focused), hit Return to trigger the action of first tab's
+ // button.
+ synthesizeNativeReturnKey();
+ } else {
+ // 7) When first tab has finished reloading (while second tab is
+ // focused), start loading second tab.
+ loadSecondTab();
+ }
+ } catch(e) {
+ }
+ }
+
+ // 8) Start loading second tab.
+ function loadSecondTab() {
+ var browser = document.getElementById("tab2browser");
+ BrowserTestUtils.startLoadingURIString(browser, "data:text/html;charset=utf-8,<body><h2>Second Tab</h2><p><input type='submit' value='Button' id='button1'/></body>");
+ }
+
+ function configureSecondTab() {
+ try {
+ var button = document.getElementById("tab2browser").contentDocument.getElementById("button1");
+ button.addEventListener("click", onSecondTabButtonClicked);
+ button.focus();
+ if (document.getElementById("tabbox").selectedIndex == 1) {
+ // 9) When second tab has finished loading (while second tab is
+ // focused), hit Return to trigger action of second tab's
+ // button.
+ synthesizeNativeReturnKey();
+ }
+ } catch(e) {
+ }
+ }
+
+ // 3) First tab's button clicked.
+ function onFirstTabButtonClicked() {
+ switchToSecondTabAndReloadFirst();
+ }
+
+ // 10) Second tab's button clicked.
+ function onSecondTabButtonClicked() {
+ switchToFirstTab();
+ }
+
+ function switchToSecondTabAndReloadFirst() {
+ // 4) Switch to second tab.
+ document.getElementById("tabbox").selectedIndex = 1;
+ // 5) Start reloading first tab (while second tab is focused).
+ loadFirstTab();
+ }
+
+ function switchToFirstTab() {
+ // 11) Switch back to first tab.
+ document.getElementById("tabbox").selectedIndex = 0;
+ doCmdY();
+ }
+
+ function doCmdY() {
+ // 12) Back in first tab, try cmd-y.
+ gCmdOptYReceived = false;
+ if (!synthesizeNativeCmdOptY(finishTest)) {
+ ok(false, "Failed to synthesize native key");
+ finishTest();
+ }
+ }
+
+ function finishTest() {
+ // 13) Check result.
+ is(gCmdOptYReceived, true);
+
+ SimpleTest.finish();
+ }
+
+ // synthesizeNativeReturnKey() and synthesizeNativeCmdOptY() are needed
+ // because their synthesizeKey() counterparts don't work properly -- the
+ // latter make this test succeed when it should fail.
+
+ // The 'aNativeKeyCode', 'aCharacters' and 'aUnmodifiedCharacters'
+ // parameters used below (in synthesizeNativeReturnKey() and
+ // synthesizeNativeCmdOptY()) were confirmed accurate using the
+ // DebugEventsPlugin v1.01 from bmo bug 441880.
+
+ function synthesizeNativeReturnKey() {
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_Return, {}, "\u000a", "\u000a");
+ }
+
+ function synthesizeNativeCmdOptY(aCallback) {
+ return synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_Y, {metaKey:1, altKey:1}, "y", "y", aCallback);
+ }
+
+ ]]></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+</window>
diff --git a/widget/tests/test_bug429954.xhtml b/widget/tests/test_bug429954.xhtml
new file mode 100644
index 0000000000..40de88cd32
--- /dev/null
+++ b/widget/tests/test_bug429954.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=429954
+-->
+<window title="Mozilla Bug 429954"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function () {
+ var win = Services.wm.getMostRecentWindow("navigator:browser");
+ win.maximize();
+ var maxX = win.screenX, maxY = win.screenY;
+ var maxWidth = win.outerWidth, maxHeight = win.outerHeight;
+ win.restore();
+
+ window.openDialog("window_bug429954.xhtml", "_blank",
+ "chrome,noopener,resizable,width=" + maxWidth + ",height=" + maxHeight +
+ ",screenX=" + maxX + "screenY=" + maxY,
+ window);
+});
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug444800.xhtml b/widget/tests/test_bug444800.xhtml
new file mode 100644
index 0000000000..f6d3535d72
--- /dev/null
+++ b/widget/tests/test_bug444800.xhtml
@@ -0,0 +1,97 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=444800
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Mozilla Bug 444800" onload="initAndRunTests()">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=444800"
+ target="_blank">Mozilla Bug 444800</a>
+ <p/>
+ <img id="bitmapImage" src="data:image/bmp;base64,Qk2KAwAAAAAAAIoAAAB8AAAADwAAABAAAAABABgAAAAAAAADAAASCwAAEgsAAAAAAAAAAAAAAAD%2FAAD%2FAAD%2FAAAAAAAA%2FwEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F0vf%2FAABc8tKY%2F%2F%2F%2FyNfq3Mi9%2F%2F%2F70vf%2FAABP8s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABB8s2R5f%2F%2FAAB5LgAA%2F%2B7Czff%2FAABB7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABB99KRpdz%2FAAAAAAAA4Ktm0vv%2FAABB7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABB7teYQZHNkS4AebfImAAA1%2FfyAABP7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABByMiYAAB5159P0v%2F%2FAABBwtKrAABc7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABPcIJwAAAA%2B%2BW3%2F%2F%2F%2FAHC3gnBBAABP7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABcAAAAmE8A%2F%2F%2Fy%2F%2F%2F%2Fn9LyAAAAAAAA7s2Y%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FzfL%2FAABcAAAA4LFw%2F%2F%2F%2F%2F%2F%2F%2F4P%2F%2FAAB5AAAA7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F0vf%2FAABmXAAA%2F%2B7I%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FXJ%2FSAAAA8s2Y%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA"/>
+ <p/>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+const knsIClipboard = Ci.nsIClipboard;
+
+function copyImageToClipboard()
+{
+ SpecialPowers.setCommandNode(window, document.getElementById("bitmapImage"));
+
+ const kCmd = "cmd_copyImageContents";
+ var controller = top.document.commandDispatcher
+ .getControllerForCommand(kCmd);
+ ok((controller && controller.isCommandEnabled(kCmd)), "have copy command");
+ controller.doCommand(kCmd);
+
+ SpecialPowers.setCommandNode(window, null);
+}
+
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function runImageClipboardTests(aCBSvc, aImageType)
+{
+ // Verify that hasDataMatchingFlavors() is working correctly.
+ var typeArray = [ aImageType ];
+ var hasImage = aCBSvc.hasDataMatchingFlavors(typeArray,
+ knsIClipboard.kGlobalClipboard);
+ ok(hasImage, aImageType + " - hasDataMatchingFlavors()");
+
+ // Verify that getData() is working correctly.
+ var xfer = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ xfer.init(getLoadContext());
+ xfer.addDataFlavor(aImageType);
+ aCBSvc.getData(xfer, knsIClipboard.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext);
+
+ var typeObj = {}, dataObj = {};
+ xfer.getAnyTransferData(typeObj, dataObj);
+ var gotValue = (null != dataObj.value);
+ ok(gotValue, aImageType + " - getData() returned a value");
+ if (gotValue)
+ {
+ const knsIInputStream = Ci.nsIInputStream;
+ var imgStream = dataObj.value.QueryInterface(knsIInputStream);
+ ok((null != imgStream), aImageType + " - got an nsIInputStream");
+ var bytesAvailable = imgStream.available();
+ ok((bytesAvailable > 10), aImageType + " - got some data");
+ }
+}
+
+function initAndRunTests()
+{
+ SimpleTest.waitForExplicitFinish();
+
+ copyImageToClipboard();
+
+ var cbSvc = Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(knsIClipboard);
+
+ // Work around a problem on Windows where clipboard is not ready after copy.
+ setTimeout(function() { runTests(cbSvc); }, 0);
+}
+
+function runTests(aCBSvc)
+{
+ runImageClipboardTests(aCBSvc, "image/png");
+ runImageClipboardTests(aCBSvc, "image/jpg");
+ runImageClipboardTests(aCBSvc, "image/jpeg");
+
+ SimpleTest.finish();
+}
+
+]]>
+</script>
+</window>
diff --git a/widget/tests/test_bug466599.xhtml b/widget/tests/test_bug466599.xhtml
new file mode 100644
index 0000000000..95b3593437
--- /dev/null
+++ b/widget/tests/test_bug466599.xhtml
@@ -0,0 +1,102 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=466599
+-->
+<window title="Mozilla Bug 466599"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="initAndRunTests()">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <!-- test code goes here -->
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+
+ /** Test for Bug 466599 */
+
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function copyToClipboard(txt)
+{
+ var clipid = Ci.nsIClipboard;
+ var clip =
+ Cc['@mozilla.org/widget/clipboard;1'].createInstance(clipid);
+ if (!clip)
+ return false;
+ var trans =
+ Cc['@mozilla.org/widget/transferable;1'].createInstance(Ci.nsITransferable);
+ if (!trans)
+ return false;
+ trans.init(getLoadContext());
+ trans.addDataFlavor('text/html');
+ var str =
+ Cc['@mozilla.org/supports-string;1'].createInstance(Ci.nsISupportsString);
+ var copytext = txt;
+ str.data = copytext;
+ trans.setTransferData("text/html",str);
+ if (!clip)
+ return false;
+ clip.setData(trans,null,clipid.kGlobalClipboard);
+ return true;
+}
+
+function readFromClipboard()
+{
+ var clipid = Ci.nsIClipboard;
+ var clip =
+ Cc['@mozilla.org/widget/clipboard;1'].createInstance(clipid);
+ if (!clip)
+ return "";
+ var trans =
+ Cc['@mozilla.org/widget/transferable;1'].createInstance(Ci.nsITransferable);
+ if (!trans)
+ return "";
+ trans.init(getLoadContext());
+ trans.addDataFlavor('text/html');
+ clip.getData(trans, clipid.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext);
+ var str = {};
+ trans.getTransferData("text/html",str);
+ if (str)
+ str = str.value.QueryInterface(Ci.nsISupportsString);
+ return str?.data;
+}
+
+function encodeHtmlEntities(s)
+{
+ var result = '';
+ for (var i = 0; i < s.length; i++) {
+ var c = s.charAt(i);
+ result += {'<':'&lt;', '>':'&gt;', '&':'&amp;', '"':'&quot;'}[c] || c;
+ }
+ return result;
+}
+
+function initAndRunTests()
+{
+ var source = '<p>Lorem ipsum</p>';
+ var expect = new RegExp('<html>.*charset=utf-8.*' + source + '.*</html>', 'im');
+
+ var result = copyToClipboard(source);
+ ok(result, "copied HTML data to system pasteboard");
+
+ result = readFromClipboard();
+ ok(expect.test(result), "data on system pasteboard is wrapped with charset metadata");
+
+ $("display").innerHTML =
+ '<em>source:</em> <pre>' + encodeHtmlEntities(source) + '</pre><br/>' +
+ '<em>result:</em> <pre>' + encodeHtmlEntities(result) + '</pre>';
+}
+
+ ]]>
+ </script>
+</window>
diff --git a/widget/tests/test_bug478536.xhtml b/widget/tests/test_bug478536.xhtml
new file mode 100644
index 0000000000..383c0bb42f
--- /dev/null
+++ b/widget/tests/test_bug478536.xhtml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=478536
+-->
+<window title="Mozilla Bug 478536"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <title>Test for Bug 478536</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("window_bug478536.xhtml", "_blank",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug485118.xhtml b/widget/tests/test_bug485118.xhtml
new file mode 100644
index 0000000000..5c635f2982
--- /dev/null
+++ b/widget/tests/test_bug485118.xhtml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=485118
+-->
+<window title="Mozilla Bug 485118"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<hbox height="300">
+ <vbox width="300">
+ <scrollbar orient="horizontal"
+ maxpos="10000"
+ pageincrement="1"
+ id="horizontal"/>
+ <scrollbar orient="horizontal"
+ maxpos="10000"
+ pageincrement="1"
+ style="appearance: auto; -moz-default-appearance: scrollbar-small;"
+ id="horizontalSmall"/>
+ <hbox flex="1">
+ <scrollbar orient="vertical"
+ maxpos="10000"
+ pageincrement="1"
+ id="vertical"/>
+ <scrollbar orient="vertical"
+ maxpos="10000"
+ pageincrement="1"
+ style="appearance: auto; -moz-default-appearance: scrollbar-small;"
+ id="verticalSmall"/>
+ <spacer flex="1"/>
+ </hbox>
+ </vbox>
+</hbox>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ ["horizontal", "vertical"].forEach(function (orient) {
+ ["", "Small"].forEach(function (size) {
+ var elem = document.getElementById(orient + size);
+ var thumbRect = SpecialPowers.unwrap(
+ SpecialPowers.InspectorUtils.getChildrenForNode(elem, true, false)[0])
+ .childNodes[0].getBoundingClientRect();
+ var sizeToCheck = orient == "horizontal" ? "width" : "height";
+ // var expectedSize = size == "Small" ? 19 : 26;
+ var expectedSize = 26;
+ is(thumbRect[sizeToCheck], expectedSize, size + " scrollbar has wrong minimum " + sizeToCheck);
+ });
+ });
+ SimpleTest.finish();
+}
+window.addEventListener("load", runTest);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug517396.xhtml b/widget/tests/test_bug517396.xhtml
new file mode 100644
index 0000000000..c88baf49ab
--- /dev/null
+++ b/widget/tests/test_bug517396.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=517396
+-->
+<window title="Mozilla Bug 517396"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function () {
+ // this test fails on Linux, bug 526236
+ if (navigator.platform.includes("Lin")) {
+ ok(true, "disabled on Linux");
+ SimpleTest.finish();
+ return;
+ }
+
+ var win = Services.wm.getMostRecentWindow("navigator:browser");
+ var oldWidth = win.outerWidth, oldHeight = win.outerHeight;
+ win.maximize();
+ var newWidth = win.outerWidth, newHeight = win.outerHeight;
+ win.moveBy(10, 0);
+ var sizeShouldHaveChanged = !navigator.platform.match(/Mac/);
+ var compFunc = sizeShouldHaveChanged ? isnot : is;
+ var not = sizeShouldHaveChanged ? "" : "not ";
+ compFunc(win.outerWidth, newWidth, "moving a maximized window should " + not + "have changed its width");
+ compFunc(win.outerHeight, newHeight, "moving a maximized window should " + not + "have changed its height");
+ win.restore();
+ is(win.outerWidth, oldWidth, "restored window has wrong width");
+ is(win.outerHeight, oldHeight, "restored window has wrong height");
+ SimpleTest.finish();
+});
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug522217.xhtml b/widget/tests/test_bug522217.xhtml
new file mode 100644
index 0000000000..0fa55a65e8
--- /dev/null
+++ b/widget/tests/test_bug522217.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=522217
+-->
+<window title="Mozilla Bug 522217"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function () {
+ window.openDialog("window_bug522217.xhtml", "_blank",
+ "chrome,resizable,width=400,height=300,noopener", window);
+});
+
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug538242.xhtml b/widget/tests/test_bug538242.xhtml
new file mode 100644
index 0000000000..4608a74e35
--- /dev/null
+++ b/widget/tests/test_bug538242.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=538242
+-->
+<window title="Mozilla Bug 538242"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+if (navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 1);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function () {
+ if (navigator.platform.includes("Lin")) {
+ ok(true, "This test is disabled on Linux because it expects moving windows to be synchronous which is not guaranteed on Linux.");
+ SimpleTest.finish();
+ return;
+ }
+
+ var win = window.browsingContext.topChromeWindow.open(
+ "window_bug538242.xhtml", "_blank",
+ "chrome,width=400,height=300,left=100,top=100");
+ SimpleTest.waitForFocus(function () {
+ is(win.screenX, 100, "window should open at 100, 100");
+ is(win.screenY, 100, "window should open at 100, 100");
+ var [oldX, oldY] = [win.screenX, win.screenY];
+ win.moveTo(0, 0);
+ isnot(win.screenX, oldX, "window should have moved to a point near 0, 0");
+ isnot(win.screenY, oldY, "window should have moved to a point near 0, 0");
+ win.close();
+ SimpleTest.finish();
+ }, win);
+});
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug565392.html b/widget/tests/test_bug565392.html
new file mode 100644
index 0000000000..b50053f23a
--- /dev/null
+++ b/widget/tests/test_bug565392.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=565392
+-->
+<head>
+ <title>Test for Bug 565392</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565392">Mozilla Bug 565392</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 565392 */
+
+var dir1 = Services.dirsvc.get("ProfD", Ci.nsIFile);
+var clipboard = Services.clipboard;
+
+ function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+ }
+
+ function getTransferableFile(file) {
+ var transferable = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ transferable.init(getLoadContext());
+ transferable.setTransferData("application/x-moz-file", file);
+ return transferable;
+ }
+
+ function setClipboardData(transferable) {
+ clipboard.setData(transferable, null, 1);
+ }
+
+ function getClipboardData(mime) {
+ var transferable = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ transferable.init(getLoadContext());
+ transferable.addDataFlavor(mime);
+ clipboard.getData(transferable, 1, SpecialPowers.wrap(window).browsingContext.currentWindowContext);
+ var data = {};
+ transferable.getTransferData(mime, data);
+ return data;
+ }
+
+setClipboardData(getTransferableFile(dir1));
+is(clipboard.hasDataMatchingFlavors(["application/x-moz-file"], 1), true);
+var data = getClipboardData("application/x-moz-file");
+var file = data.value.QueryInterface(Ci.nsIFile);
+ok(file.isDirectory(), true);
+is(file.target, dir1.target, true);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/widget/tests/test_bug586713.xhtml b/widget/tests/test_bug586713.xhtml
new file mode 100644
index 0000000000..4733202264
--- /dev/null
+++ b/widget/tests/test_bug586713.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Native menu system tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("bug586713_window.xhtml", "bug586713_window",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug593307.xhtml b/widget/tests/test_bug593307.xhtml
new file mode 100644
index 0000000000..770dd390cb
--- /dev/null
+++ b/widget/tests/test_bug593307.xhtml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=593307
+-->
+<window title="Mozilla Bug 593307"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function finish() {
+ offscreenWindow.close();
+ SimpleTest.finish();
+}
+
+var mainWindow = window.browsingContext.topChromeWindow;
+
+var offscreenWindow = mainWindow.openDialog("window_bug593307_offscreen.xhtml", "",
+ "dialog=no,chrome,width=200,height=200,screenX=-3000,screenY=-3000",
+ SimpleTest, finish);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug596600.xhtml b/widget/tests/test_bug596600.xhtml
new file mode 100644
index 0000000000..4acdab79bc
--- /dev/null
+++ b/widget/tests/test_bug596600.xhtml
@@ -0,0 +1,190 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Native mouse event tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+var gLeftWindow, gRightWindow, gBrowserElement;
+
+function openWindows() {
+ gLeftWindow = window.browsingContext.topChromeWindow
+ .open('empty_window.xhtml', '_blank', 'chrome,screenX=50,screenY=50,width=200,height=200');
+ SimpleTest.waitForFocus(function () {
+ gRightWindow = window.browsingContext.topChromeWindow
+ .open('empty_window.xhtml', '', 'chrome,screenX=300,screenY=50,width=200,height=200');
+ SimpleTest.waitForFocus(attachBrowserToLeftWindow, gRightWindow);
+ }, gLeftWindow);
+}
+
+function attachBrowserToLeftWindow() {
+ gBrowserElement = gLeftWindow.document.createXULElement("browser");
+ gBrowserElement.setAttribute("type", "content");
+ gBrowserElement.setAttribute("src", "file_bug596600.html");
+ gBrowserElement.style.width = "100px";
+ gBrowserElement.style.height = "100px";
+ gBrowserElement.style.margin = "50px";
+ gLeftWindow.document.documentElement.appendChild(gBrowserElement);
+ gBrowserElement.addEventListener("load", async () => {
+ await test1();
+ await test2();
+ gRightWindow.close();
+ gLeftWindow.close();
+ SimpleTest.finish();
+ }, { capture: true, once: true });
+}
+
+async function test1() {
+ // gRightWindow is active, gLeftWindow is inactive.
+ info(`Synthesizing native "mousemove" event at top-left of the screen...`);
+ await promiseNativeMouseEvent({
+ type: "mousemove",
+ screenX: 0,
+ screenY: 0,
+ scale: "inScreenPixels",
+ });
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+
+ // Move into the left window
+ info(`Synthesizing native "mousemove" event in the left window (but outside the content)...`);
+ await promiseNativeMouseEventAndWaitForEvent({
+ type: "mousemove",
+ target: gBrowserElement,
+ offsetX: -20,
+ offsetY: -20,
+ win: gLeftWindow,
+ eventTypeToWait: "mouseover",
+ eventTargetToListen: gLeftWindow,
+ });
+ ok(true, `"mouseover" event is fired on the left window when cursor is moved into it`);
+
+ // Move over the browser
+ info(`Synthesizing native "mousemove" event on the content in the left window...`);
+ await promiseNativeMouseEventAndWaitForEvent({
+ type: "mousemove",
+ target: gBrowserElement,
+ atCenter: true,
+ win: gLeftWindow,
+ eventTypeToWait: "mouseout",
+ eventTargetToListen: gLeftWindow,
+ });
+ ok(true, `"mouseout" event is fired on the left window when cursor is moved into its child browser`);
+}
+
+async function test2() {
+ // Make the browser cover the whole window.
+ gBrowserElement.style.margin = "0";
+ gBrowserElement.style.width = gBrowserElement.style.height = "200px";
+
+ // Add a box to the browser at the left edge.
+ var doc = gBrowserElement.contentDocument;
+ var box = doc.createElement("div");
+ box.setAttribute("id", "box");
+ box.style.position = "absolute";
+ box.style.left = "0";
+ box.style.top = "50px";
+ box.style.width = "100px";
+ box.style.height = "100px";
+ box.style.backgroundColor = "green";
+ doc.body.appendChild(box);
+
+ ok(!box.matches(":hover"), "Box shouldn't be hovered (since the mouse isn't over it and since it's in a non-clickthrough browser in a background window)");
+
+ // A function to waitForFocus and then wait for synthetic mouse
+ // events to happen. Note that those happen off the refresh driver,
+ // and happen after animation frame requests.
+ function changeFocusAndAwaitSyntheticMouse(winToFocus,
+ elementToWatchForMouseEventOn) {
+ return Promise.all([
+ new Promise(resolve => {
+ function mouseWatcher() {
+ elementToWatchForMouseEventOn.removeEventListener("mouseover",
+ mouseWatcher);
+ elementToWatchForMouseEventOn.removeEventListener("mouseout",
+ mouseWatcher);
+ SimpleTest.executeSoon(resolve);
+ }
+ elementToWatchForMouseEventOn.addEventListener("mouseover",
+ mouseWatcher);
+ elementToWatchForMouseEventOn.addEventListener("mouseout",
+ mouseWatcher);
+ }),
+ new Promise(resolve => SimpleTest.waitForFocus(resolve, winToFocus)),
+ ]);
+ }
+
+ // Move the mouse over the box.
+ info(`Synthesizing native "mousemove" event into the box...`);
+ await promiseNativeMouseEvent({
+ type: "mousemove",
+ target: box,
+ atCenter: true,
+ win: gLeftWindow,
+ });
+ await new Promise(resolve =>
+ requestAnimationFrame(() => SimpleTest.executeSoon(resolve))
+ );
+ // XXX We cannot guarantee that the native mousemouse have already handled here.
+ ok(!box.matches(":hover"), "Box shouldn't be hovered (since it's in a non-clickthrough browser in a background window)");
+
+ // Activate the left window.
+ info("Waiting the left window activated...");
+ await changeFocusAndAwaitSyntheticMouse(gLeftWindow, box);
+ ok(gBrowserElement.matches(":hover"), "browser should be hovered");
+ ok(box.matches(":hover"), "Box should be hovered");
+
+ // De-activate the window (by activating the right window).
+ info("Waiting the right window activated...");
+ await changeFocusAndAwaitSyntheticMouse(gRightWindow, box);
+ ok(!gBrowserElement.matches(":hover"), "browser shouldn't be hovered");
+ ok(!box.matches(":hover"), "Box shouldn't be hovered");
+
+ // Re-activate it.
+ info("Waiting the left window activated again...");
+ await changeFocusAndAwaitSyntheticMouse(gLeftWindow, box);
+ ok(gBrowserElement.matches(":hover"), "browser should be hovered");
+ ok(box.matches(":hover"), "Box should be hovered");
+
+ // Unhover the box and the left window.
+ info(`Synthesizing native "mousemove" event outside the box and the left window...`);
+ await promiseNativeMouseEventAndWaitForEvent({
+ type: "mousemove",
+ screenX: 0,
+ screenY: 0,
+ scale: "inScreenPixels",
+ win: gLeftWindow,
+ eventTargetToListen: box,
+ eventTypeToWait: "mouseout",
+ });
+ await new Promise(resolve =>
+ requestAnimationFrame(() => SimpleTest.executeSoon(resolve))
+ );
+
+ ok(!gBrowserElement.matches(":hover"), "browser shouldn't be hovered");
+ ok(!box.matches(":hover"), "box shouldn't be hovered");
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(openWindows);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_bug673301.xhtml b/widget/tests/test_bug673301.xhtml
new file mode 100644
index 0000000000..663f18397e
--- /dev/null
+++ b/widget/tests/test_bug673301.xhtml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none"/>
+</body>
+
+<script type="application/javascript">
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+var transferable = Cc['@mozilla.org/widget/transferable;1']
+ .createInstance(Ci.nsITransferable);
+transferable.init(getLoadContext());
+
+transferable.addDataFlavor("text/plain");
+transferable.setTransferData("text/plain", document);
+
+Services.clipboard.setData(transferable, null, Ci.nsIClipboard.kGlobalClipboard);
+
+transferable.setTransferData("text/plain", null);
+
+SimpleTest.ok(true, "Didn't crash setting non-text data for text/plain type");
+</script>
+</window>
diff --git a/widget/tests/test_bug760802.xhtml b/widget/tests/test_bug760802.xhtml
new file mode 100644
index 0000000000..831b4dea93
--- /dev/null
+++ b/widget/tests/test_bug760802.xhtml
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=760802
+-->
+<window title="Mozilla Bug 760802"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=760802"
+ target="_blank">Mozilla Bug 760802</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"/>
+ <iframe id="iframe_not_editable" width="300" height="150"
+ src="data:text/html,&lt;html&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;"/><br/>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+function getBaseWindowInterface(win) {
+ return win.docShell
+ .treeOwner
+ .nsIBaseWindow;
+}
+
+function getBaseWindowInterfaceFromDocShell(win) {
+ return win.docShell.QueryInterface(Ci.nsIBaseWindow);
+}
+
+function shouldThrowException(fun, exception) {
+ try {
+ fun.call();
+ return false;
+ } catch (e) {
+ $("display").innerHTML += "<br/>OK thrown: "+e.message;
+ return (e instanceof Components.Exception &&
+ e.result === exception)
+ }
+}
+function doesntThrowException(fun) {
+ return !shouldThrowException(fun);
+}
+
+var baseWindow = getBaseWindowInterface(this);
+var nativeHandle = baseWindow.nativeHandle;
+$("display").innerHTML = "found nativeHandle for this window: "+nativeHandle;
+
+var win = Services.wm.getMostRecentWindow("navigator:browser");
+let docShell = getBaseWindowInterfaceFromDocShell(win);
+
+ok(
+ shouldThrowException(function(){docShell.nativeHandle;},
+ Cr.NS_ERROR_NOT_IMPLEMENTED),
+ "nativeHandle should not be implemented for nsDocShell"
+);
+
+ok(typeof(nativeHandle) === "string", "nativeHandle should be a string");
+ok(nativeHandle.match(/^0x[0-9a-f]+$/), "nativeHandle should have a memory address format");
+
+var iWin = document.getElementById("iframe_not_editable").contentWindow;
+is(getBaseWindowInterface(iWin).nativeHandle, nativeHandle,
+ "the nativeHandle of an iframe should be its parent's nativeHandle");
+
+var dialog = win.openDialog("data:text/plain,this is an active window.", "_blank",
+ "chrome,dialog=yes,width=100,height=100");
+
+isnot(getBaseWindowInterface(dialog).nativeHandle, "",
+ "the nativeHandle of a dialog should not be empty");
+
+dialog.close();
+
+todo(false, "the nativeHandle of a window without a mainWidget should be empty"); // how to build a window without a mainWidget ?
+
+SimpleTest.finish();
+ ]]></script>
+</window>
diff --git a/widget/tests/test_clipboard.html b/widget/tests/test_clipboard.html
new file mode 100644
index 0000000000..35ad84ad73
--- /dev/null
+++ b/widget/tests/test_clipboard.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=948065
+-->
+<head>
+<title>Test for Bug 948065</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<!-- Tests are in file_test_clipboard.js -->
+<script src="file_test_clipboard.js"></script>
+</body>
+</html>
diff --git a/widget/tests/test_clipboard_asyncGetData.html b/widget/tests/test_clipboard_asyncGetData.html
new file mode 100644
index 0000000000..0cb3dc2aa1
--- /dev/null
+++ b/widget/tests/test_clipboard_asyncGetData.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1852947
+-->
+<head>
+<title>Test for Bug 1852947</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<!-- Tests are in file_clipboard_asyncGetData.js -->
+<script src="file_test_clipboard_asyncGetData.js"></script>
+</body>
+</html>
diff --git a/widget/tests/test_clipboard_asyncGetData_chrome.html b/widget/tests/test_clipboard_asyncGetData_chrome.html
new file mode 100644
index 0000000000..d4e44185a5
--- /dev/null
+++ b/widget/tests/test_clipboard_asyncGetData_chrome.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1852947
+-->
+<head>
+<title>Test for Bug 1852947</title>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<!-- Tests are in file_clipboard_asyncGetData.js -->
+<script src="file_test_clipboard_asyncGetData.js"></script>
+</body>
+</html>
diff --git a/widget/tests/test_clipboard_asyncSetData.html b/widget/tests/test_clipboard_asyncSetData.html
new file mode 100644
index 0000000000..05e2ca714b
--- /dev/null
+++ b/widget/tests/test_clipboard_asyncSetData.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1712122
+-->
+<head>
+<title>Test for Bug 1712122</title>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<!-- Tests are in file_test_clipboard_asyncSetData.js -->
+<script src="file_test_clipboard_asyncSetData.js"></script>
+</body>
+</html>
diff --git a/widget/tests/test_clipboard_asyncSetData_chrome.html b/widget/tests/test_clipboard_asyncSetData_chrome.html
new file mode 100644
index 0000000000..61afb9067b
--- /dev/null
+++ b/widget/tests/test_clipboard_asyncSetData_chrome.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1712122
+-->
+<head>
+<title>Test for Bug 1712122</title>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<!-- Tests are in file_test_clipboard_asyncSetData.js -->
+<script src="file_test_clipboard_asyncSetData.js"></script>
+</body>
+</html>
diff --git a/widget/tests/test_clipboard_cache_chrome.html b/widget/tests/test_clipboard_cache_chrome.html
new file mode 100644
index 0000000000..55b6d41589
--- /dev/null
+++ b/widget/tests/test_clipboard_cache_chrome.html
@@ -0,0 +1,231 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1812543
+-->
+<head>
+<title>Test for Bug 1812543</title>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="application/javascript">
+
+function testClipboardCache(aClipboardType, aAsync, aIsSupportGetFromCachedTransferable) {
+ add_task(function test_clipboard_get() {
+ info(`test_clipboard_get ${aAsync ? "async " : ""}` +
+ `with pref ${aIsSupportGetFromCachedTransferable ? "enabled" : "disabled"}`);
+
+ const string = generateRandomString();
+ const trans = generateNewTransferable("text/plain", string);
+
+ info(`Write text/plain data to clipboard ${aClipboardType}`);
+ if (aAsync) {
+ let request = clipboard.asyncSetData(aClipboardType);
+ request.setData(trans, null);
+ } else {
+ clipboard.setData(trans, null, aClipboardType);
+ }
+ is(getClipboardData("text/plain", aClipboardType), string,
+ `Check text/plain data on clipboard ${aClipboardType}`);
+
+ info(`Add text/foo data to transferable`);
+ addStringToTransferable("text/foo", string, trans);
+ // XXX macOS caches the transferable to implement kSelectionCache type, too,
+ // so it behaves differently than other types.
+ if (aClipboardType == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) {
+ todo_is(getClipboardData("text/foo", aClipboardType),
+ aIsSupportGetFromCachedTransferable ? string : null,
+ `Check text/foo data on clipboard ${aClipboardType}`);
+ } else {
+ is(getClipboardData("text/foo", aClipboardType),
+ aIsSupportGetFromCachedTransferable ? string : null,
+ `Check text/foo data on clipboard ${aClipboardType}`);
+ }
+
+ info(`Should not get the data from other clipboard type`);
+ clipboardTypes.forEach(function(otherType) {
+ if (otherType != aClipboardType &&
+ clipboard.isClipboardTypeSupported(otherType)) {
+ is(getClipboardData("text/plain", otherType), null,
+ `Check text/plain data on clipboard ${otherType}`);
+ is(getClipboardData("text/foo", otherType), null,
+ `Check text/foo data on clipboard ${otherType}`);
+
+ info(`Write text/plain data to clipboard ${otherType}`);
+ writeRandomStringToClipboard("text/plain", otherType);
+ }
+ });
+
+ info(`Check data on clipboard ${aClipboardType} again`);
+ is(getClipboardData("text/plain", aClipboardType), string,
+ `Check text/plain data on clipboard ${aClipboardType} again`);
+ // XXX macOS caches the transferable to implement kSelectionCache type, too,
+ // so it behaves differently than other types.
+ if (aClipboardType == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) {
+ todo_is(getClipboardData("text/foo", aClipboardType),
+ aIsSupportGetFromCachedTransferable ? string : null,
+ `Check text/foo data on clipboard ${aClipboardType} again`);
+ } else {
+ is(getClipboardData("text/foo", aClipboardType),
+ aIsSupportGetFromCachedTransferable ? string : null,
+ `Check text/foo data on clipboard ${aClipboardType} again`);
+ }
+
+ info(`Clean all clipboard data`);
+ cleanupAllClipboard();
+ });
+}
+
+function runClipboardCacheTests(aIsSupportGetFromCachedTransferable) {
+ add_task(async function setup() {
+ cleanupAllClipboard();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "widget.clipboard.use-cached-data.enabled",
+ aIsSupportGetFromCachedTransferable,
+ ],
+ ],
+ });
+ });
+
+ clipboardTypes.forEach(function (type) {
+ if (!clipboard.isClipboardTypeSupported(type)) {
+ return;
+ }
+
+ add_task(function test_clipboard_hasDataMatchingFlavors() {
+ info(`test_clipboard_hasDataMatchingFlavors with pref ` +
+ `${aIsSupportGetFromCachedTransferable ? "enabled" : "disabled"}`);
+
+ const trans = generateNewTransferable("text/plain", generateRandomString());
+
+ info(`Write text/plain data to clipboard ${type}`);
+ clipboard.setData(trans, null, type);
+ ok(clipboard.hasDataMatchingFlavors(["text/plain"], type),
+ `Check if there is text/plain flavor on clipboard ${type}`);
+ ok(!clipboard.hasDataMatchingFlavors(["text/foo"], type),
+ `Check if there is text/foo flavor on clipboard ${type}`);
+
+ info(`Add text/foo data to transferable`);
+ addStringToTransferable("text/foo", generateRandomString(), trans);
+ ok(clipboard.hasDataMatchingFlavors(["text/plain"], type),
+ `Check if there is text/plain flavor on clipboard ${type}`);
+ // XXX macOS caches the transferable to implement kSelectionCache type, too,
+ // so it behaves differently than other types.
+ if (type == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) {
+ todo_is(clipboard.hasDataMatchingFlavors(["text/foo"], type),
+ aIsSupportGetFromCachedTransferable,
+ `Check if there is text/foo flavor on clipboard ${type}`);
+ } else {
+ is(clipboard.hasDataMatchingFlavors(["text/foo"], type),
+ aIsSupportGetFromCachedTransferable,
+ `Check if there is text/foo flavor on clipboard ${type}`);
+ }
+
+ // Check other clipboard types.
+ clipboardTypes.forEach(function(otherType) {
+ if (otherType != type &&
+ clipboard.isClipboardTypeSupported(otherType)) {
+ ok(!clipboard.hasDataMatchingFlavors(["text/plain"], otherType),
+ `Check if there is text/plain flavor on clipboard ${otherType}`);
+ ok(!clipboard.hasDataMatchingFlavors(["text/foo"], otherType),
+ `Check if there is text/foo flavor on clipboard ${otherType}`);
+
+ info(`Write text/plain data to clipboard ${otherType}`);
+ writeRandomStringToClipboard("text/plain", otherType);
+ }
+ });
+
+ // Check again.
+ ok(clipboard.hasDataMatchingFlavors(["text/plain"], type),
+ `Check if there is text/plain flavor on clipboard ${type}`);
+ // XXX macOS caches the transferable to implement kSelectionCache type, too,
+ // so it behaves differently than other types.
+ if (type == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) {
+ todo_is(clipboard.hasDataMatchingFlavors(["text/foo"], type),
+ aIsSupportGetFromCachedTransferable,
+ `Check if there is text/foo flavor on clipboard ${type}`);
+ } else {
+ is(clipboard.hasDataMatchingFlavors(["text/foo"], type),
+ aIsSupportGetFromCachedTransferable,
+ `Check if there is text/foo flavor on clipboard ${type}`);
+ }
+
+ info(`Write text/plain data to clipboard ${type} again`);
+ writeRandomStringToClipboard("text/plain", type);
+ ok(clipboard.hasDataMatchingFlavors(["text/plain"], type),
+ `Check if there is text/plain flavor on clipboard ${type}`);
+ ok(!clipboard.hasDataMatchingFlavors(["text/foo"], type),
+ `Check if there is text/foo flavor on clipboard ${type}`);
+
+ // Clean clipboard data.
+ cleanupAllClipboard();
+ });
+
+ add_task(async function test_clipboard_asyncGetData() {
+ const testClipboardData = async function(aRequest, aExpectedData) {
+ is(aRequest.flavorList.length, Object.keys(aExpectedData).length, "Check flavorList length");
+ for (const [key, value] of Object.entries(aExpectedData)) {
+ ok(aRequest.flavorList.includes(key), `${key} should be available`);
+ is(await asyncClipboardRequestGetData(aRequest, key), value,
+ `Check ${key} data`);
+ }
+ };
+
+ info(`test_clipboard_hasDataMatchingFlavors with pref ` +
+ `${aIsSupportGetFromCachedTransferable ? "enabled" : "disabled"}`);
+
+ const clipboardData = { "text/plain": generateRandomString() };
+ const trans = generateNewTransferable("text/plain", clipboardData["text/plain"]);
+
+ info(`Write text/plain data to clipboard ${type}`);
+ clipboard.setData(trans, null, type);
+ await testClipboardData(await asyncGetClipboardData(type), clipboardData);
+
+ info(`Add text/html data to transferable`);
+ const htmlString = `<div>${generateRandomString()}</div>`;
+ addStringToTransferable("text/html", htmlString, trans);
+ // XXX macOS uses cached transferable to implement kSelectionCache type, too,
+ // so it behaves differently than other types.
+ if (aIsSupportGetFromCachedTransferable ||
+ (type == clipboard.kSelectionCache && !SpecialPowers.isHeadless)) {
+ clipboardData["text/html"] = htmlString;
+ }
+ await testClipboardData(await asyncGetClipboardData(type), clipboardData);
+
+ info(`Should not get the data from other clipboard type`);
+ clipboardTypes.forEach(async function(otherType) {
+ if (otherType != type &&
+ clipboard.isClipboardTypeSupported(otherType)) {
+ info(`Check clipboard type ${otherType}`);
+ await testClipboardData(await asyncGetClipboardData(otherType), {});
+ }
+ });
+
+ info(`Check data on clipboard ${type} again`);
+ await testClipboardData(await asyncGetClipboardData(type), clipboardData);
+ });
+
+ // Test sync set clipboard data.
+ testClipboardCache(type, false, aIsSupportGetFromCachedTransferable);
+
+ // Test async set clipboard data.
+ testClipboardCache(type, true, aIsSupportGetFromCachedTransferable);
+});
+}
+
+// Test not get data from clipboard cache.
+runClipboardCacheTests(false);
+
+// Test get data from clipboard cache.
+runClipboardCacheTests(true);
+
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_clipboard_chrome.html b/widget/tests/test_clipboard_chrome.html
new file mode 100644
index 0000000000..387e3975b3
--- /dev/null
+++ b/widget/tests/test_clipboard_chrome.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=948065
+-->
+<head>
+<title>Test for Bug 948065</title>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<!-- Tests are in file_test_clipboard.js -->
+<script src="file_test_clipboard.js"></script>
+</body>
+</html>
diff --git a/widget/tests/test_clipboard_owner_chrome.html b/widget/tests/test_clipboard_owner_chrome.html
new file mode 100644
index 0000000000..4759b2b14c
--- /dev/null
+++ b/widget/tests/test_clipboard_owner_chrome.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1812078
+-->
+<head>
+<title>Test for Bug 1812078</title>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="clipboard_helper.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="application/javascript">
+
+function testClipboardOwner(aClipboardType, aAsync) {
+ let losingOwnership = false;
+ const clipboardOwner = {
+ QueryInterface: ChromeUtils.generateQI(["nsIClipboardOwner"]),
+ // nsIClipboardOwner
+ LosingOwnership(aTransferable) {
+ losingOwnership = true;
+ },
+ };
+
+ add_task(function test_clipboard_owner() {
+ info(`Test clipboard owner for type ${aClipboardType} ${aAsync ? "async" : ""}`);
+
+ // Setup clipboard owner.
+ writeRandomStringToClipboard("text/plain", aClipboardType, clipboardOwner, aAsync);
+
+ // Test should not lose ownership.
+ clipboardTypes.forEach(function(otherType) {
+ losingOwnership = false;
+ if (aClipboardType != otherType && clipboard.isClipboardTypeSupported(otherType)) {
+ // Test setting clipboard data.
+ writeRandomStringToClipboard("text/plain", otherType);
+ ok(!losingOwnership, `Should not lose ownership while setting data to type ${otherType}`);
+
+ // Test async setting clipboard data.
+ writeRandomStringToClipboard("text/plain", otherType, null, true);
+ ok(!losingOwnership, `Should not lose ownership while async setting data to type ${otherType}`);
+ }
+ });
+
+ // Test whether should lose ownership.
+ losingOwnership = false;
+ writeRandomStringToClipboard("text/plain", aClipboardType, clipboardOwner);
+ ok(losingOwnership, `Should lose ownership while setting data to type ${aClipboardType}`);
+
+ losingOwnership = false;
+ writeRandomStringToClipboard("text/plain", aClipboardType, null, true);
+ ok(losingOwnership, `Should lose ownership while async setting data to type ${aClipboardType}`);
+
+ // Clean clipboard data.
+ cleanupAllClipboard();
+ });
+}
+
+/** Test for Bug 1812078 */
+clipboardTypes.forEach(function(testType) {
+ if (clipboard.isClipboardTypeSupported(testType)) {
+ // Test sync set clipboard data.
+ testClipboardOwner(testType, false);
+
+ // Test async set clipboard data.
+ testClipboardOwner(testType, true);
+ }
+});
+
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_composition_text_querycontent.xhtml b/widget/tests/test_composition_text_querycontent.xhtml
new file mode 100644
index 0000000000..48b7af8100
--- /dev/null
+++ b/widget/tests/test_composition_text_querycontent.xhtml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Testing composition, text and query content events"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+// 3 assertions are: If setting selection with eSetSelection event whose range
+// is larger than the actual range, hits "Can only call this on frames that have
+// been reflowed:
+// '!(GetStateBits() & NS_FRAME_FIRST_REFLOW) || (GetParent()->GetStateBits() &
+// NS_FRAME_TOO_DEEP_IN_FRAME_TREE)'" in nsTextFrame.cpp.
+// Strangely, this doesn't occur with RDP on Windows.
+SimpleTest.expectAssertions(0, 3);
+SimpleTest.waitForExplicitFinish();
+window.openDialog("window_composition_text_querycontent.xhtml", "_blank",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+</window>
diff --git a/widget/tests/test_contextmenu_by_mouse_on_unix.html b/widget/tests/test_contextmenu_by_mouse_on_unix.html
new file mode 100644
index 0000000000..2b1f643dfc
--- /dev/null
+++ b/widget/tests/test_contextmenu_by_mouse_on_unix.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test to fire contextmenu event by widget level</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/SpecialPowers.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<style>
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+}
+</style>
+<script>
+"use strict";
+add_task(async function test_fire_contextmenu_by_mousedown() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.context_menus.after_mouseup", false]],
+ });
+ await SimpleTest.promiseFocus();
+
+ // contextmenu event is fired by mouse down.
+ await process_contextmenu_event({ isMousedown: true, preventEvent: false });
+ // contextmenu event is fired by mouse down even if mouse handler calls preventDefault.
+ await process_contextmenu_event({ isMousedown: true, preventEvent: true });
+});
+
+add_task(async function test_fire_contextmenu_by_mouseup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.context_menus.after_mouseup", true]],
+ });
+ await SimpleTest.promiseFocus();
+
+ // contextmenu event is fired by mouse up.
+ await process_contextmenu_event({ isMousedown: false, preventEvent: false });
+ // contextmenu event is fired by mouse up even if mouse handler calls preventDefault.
+ await process_contextmenu_event({ isMousedown: false, preventEvent: true });
+});
+
+async function process_contextmenu_event({ isMousedown, preventEvent }) {
+ await SpecialPowers.contentTransformsReceived(window);
+
+ const target = document.getElementById("target");
+
+ let count = 0;
+
+ const promise = new Promise(resolve => {
+ target.addEventListener("mousedown", e => {
+ is(e.buttons, 2, "The right button down should be fired");
+ is(count++, 0, "The first event is mousedown");
+ if (isMousedown && preventEvent) {
+ e.preventDefault();
+ }
+ }, { once: true });
+
+ if (isMousedown) {
+ target.addEventListener("contextmenu", e => {
+ is(count++, 1, "The second event is contextmenu");
+ e.preventDefault();
+ }, { once: true });
+ target.addEventListener("mouseup", e => {
+ is(count++, 2, "The third event is mouseup");
+ resolve();
+ }, { once: true} );
+ } else {
+ target.addEventListener("mouseup", e => {
+ is(count++, 1, "The second event is mouseup");
+ if (preventEvent) {
+ e.preventDefault();
+ }
+ }, { once: true });
+ target.addEventListener("contextmenu", e => {
+ is(count++, 2, "The third event is contextmenu");
+ e.preventDefault();
+ resolve();
+ }, { once: true });
+ }
+ });
+
+ synthesizeNativeMouseEvent({
+ type: "mousedown",
+ target,
+ offsetX: 10,
+ offsetY: 10,
+ button: 2,
+ });
+
+ synthesizeNativeMouseEvent({
+ type: "mouseup",
+ target,
+ offsetX: 10,
+ offsetY: 10,
+ button: 2,
+ });
+
+ await promise;
+}
+</script>
+</head>
+<body>
+<div id="target"></div>
+</body>
+</html>
diff --git a/widget/tests/test_ime_state_in_contenteditable_on_readonly_change_in_parent.html b/widget/tests/test_ime_state_in_contenteditable_on_readonly_change_in_parent.html
new file mode 100644
index 0000000000..8d8662a8d8
--- /dev/null
+++ b/widget/tests/test_ime_state_in_contenteditable_on_readonly_change_in_parent.html
@@ -0,0 +1,72 @@
+<html>
+<head>
+ <title>Test for IME state of contenteditable on readonly state change</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <script src="file_test_ime_state_in_contenteditable_on_readonly_change.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<script>
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+/* import-globals-from file_test_ime_state_in_contenteditable_on_readonly_change.js */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const tipWrapper = new TIPWrapper(window);
+ const editingHost = document.querySelector("div[contenteditable]");
+ await (async function test_ime_state_in_contenteditable_on_readonly_change() {
+ const tester = new IMEStateInContentEditableOnReadonlyChangeTester();
+ tester.checkResultOfPreparation(await tester.prepareToRun(editingHost, editingHost), window, tipWrapper);
+ tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly());
+ tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable());
+ tester.checkResultOfRemovingContentEditableAttribute(await tester.runToRemoveContentEditableAttribute());
+ tester.clear();
+ })();
+
+ await (async function test_ime_state_in_button_in_contenteditable_on_readonly_change() {
+ const tester = new IMEStateInContentEditableOnReadonlyChangeTester();
+ const button = editingHost.querySelector("button");
+ tester.checkResultOfPreparation(await tester.prepareToRun(editingHost, button), window, tipWrapper);
+ tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly());
+ tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable());
+ tester.checkResultOfRemovingContentEditableAttribute(await tester.runToRemoveContentEditableAttribute());
+ tester.clear();
+ })();
+
+ await (async function test_ime_state_of_text_controls_in_contenteditable_on_readonly_change() {
+ const tester = new IMEStateOfTextControlInContentEditableOnReadonlyChangeTester();
+ for (let index = 0;
+ index < IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.numberOfTextControlTypes;
+ index++) {
+ tester.checkResultOfPreparation(await tester.prepareToRun(index, editingHost), window, tipWrapper);
+ tester.checkResultOfMakingParentEditingHost(await tester.runToMakeParentEditingHost());
+ tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly());
+ tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable());
+ tester.checkResultOfMakingParentNonEditable(await tester.runToMakeParentNonEditingHost());
+ tester.clear();
+ }
+ editingHost.setAttribute("contenteditable", "");
+ })();
+
+ await (async function test_ime_state_outside_contenteditable_on_readonly_change() {
+ const tester = new IMEStateOutsideContentEditableOnReadonlyChangeTester();
+ for (let index = 0;
+ index < IMEStateOutsideContentEditableOnReadonlyChangeTester.numberOfFocusTargets;
+ index++) {
+ tester.checkResultOfPreparation(await tester.prepareToRun(index, editingHost), window, tipWrapper);
+ tester.checkResultOfMakingParentEditingHost(await tester.runToMakeParentEditingHost());
+ tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly());
+ tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable());
+ tester.checkResultOfMakingParentNonEditable(await tester.runToMakeParentNonEditingHost());
+ tester.clear();
+ }
+ editingHost.setAttribute("contenteditable", "");
+ })();
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body><div contenteditable><br><button>button</button></div></body>
+</html>
diff --git a/widget/tests/test_ime_state_in_plugin_in_parent.html b/widget/tests/test_ime_state_in_plugin_in_parent.html
new file mode 100644
index 0000000000..9f3892ab88
--- /dev/null
+++ b/widget/tests/test_ime_state_in_plugin_in_parent.html
@@ -0,0 +1,92 @@
+<html>
+<head>
+ <title>Test for IME state on plugin</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<input>
+<object type="application/x-test"></object>
+<script>
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const tipWrapper = new TIPWrapper(window);
+ const plugin = document.querySelector("object");
+
+ // Plugins are not supported and their elements should not accept focus;
+ // therefore, IME should not enable when we play with it.
+
+ document.activeElement?.blur();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when no element has focus"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when no element has focus"
+ );
+
+ plugin.focus();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when an <object> for plugin has focus"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when an <object> for plugin has focus"
+ );
+
+ plugin.blur();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when an <object> for plugin gets blurred"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when an <object> for plugin gets blurred"
+ );
+
+ plugin.focus();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when an <object> for plugin gets focused again"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when an <object> for plugin gets focused again"
+ );
+
+ plugin.remove();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "IME enabled state should be disabled when focused <object> for plugin is removed from the document"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "IME should not have focus when focused <object> for plugin is removed from the document"
+ );
+
+ document.querySelector("input").focus();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "IME enabled state should be enabled after <input> gets focus"
+ );
+ ok(
+ tipWrapper.IMEHasFocus,
+ "IME should have focus after <input> gets focus"
+ );
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_ime_state_in_text_control_on_reframe_in_parent.html b/widget/tests/test_ime_state_in_text_control_on_reframe_in_parent.html
new file mode 100644
index 0000000000..ab38806261
--- /dev/null
+++ b/widget/tests/test_ime_state_in_text_control_on_reframe_in_parent.html
@@ -0,0 +1,42 @@
+<html>
+<head>
+ <title>Test for IME state of contenteditable on readonly state change</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <script src="file_test_ime_state_in_text_control_on_reframe.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<script>
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+/* import-globals-from file_test_ime_state_in_text_control_on_reframe.js */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const tipWrapper = new TIPWrapper(window);
+ const tester = new IMEStateInTextControlOnReframeTester();
+ for (let index = 0;
+ index < IMEStateInTextControlOnReframeTester.numberOfTextControlTypes;
+ index++) {
+ tipWrapper.clearFocusBlurNotifications();
+ const expectedData1 = await tester.prepareToRun(index, document);
+ tipWrapper.typeA();
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)
+ )); // Flush IME content observer notifications.
+ tester.checkResultAfterTypingA(expectedData1, window, tipWrapper);
+
+ const expectedData2 = await tester.prepareToRun2(index, document);
+ tipWrapper.typeA();
+ await new Promise(resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)
+ )); // Flush IME content observer notifications.
+ tester.checkResultAfterTypingA2(expectedData2);
+ }
+
+ SimpleTest.finish();
+});
+</script>
+<body></body>
+</html>
diff --git a/widget/tests/test_ime_state_on_editable_state_change_in_parent.html b/widget/tests/test_ime_state_on_editable_state_change_in_parent.html
new file mode 100644
index 0000000000..a1b307a51f
--- /dev/null
+++ b/widget/tests/test_ime_state_on_editable_state_change_in_parent.html
@@ -0,0 +1,263 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for IME state management at changing editable state</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<div></div>
+<script>
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const tipWrapper = new TIPWrapper(window);
+
+ function waitForIMEContentObserverSendingNotifications() {
+ return new Promise(
+ resolve => requestAnimationFrame(
+ () => requestAnimationFrame(resolve)
+ )
+ );
+ }
+
+ function resetIMEStateWithFocusMove() {
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+ input.focus();
+ input.remove();
+ return waitForIMEContentObserverSendingNotifications();
+ }
+
+ await (async function test_setting_contenteditable_of_focused_div() {
+ const div = document.querySelector("div");
+ div.setAttribute("tabindex", "0");
+ div.focus();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "test_setting_contenteditable_of_focused_div: IME should be disabled when non-editable <div> has focus"
+ );
+ div.setAttribute("contenteditable", "");
+ await waitForIMEContentObserverSendingNotifications();
+ // Sometimes, it's not enough waiting only 2 animation frames here to wait
+ // for IME focus, perhaps, it may be related to HTMLEditor initialization.
+ // Let's wait one more animation frame here.
+ if (!tipWrapper.IMEHasFocus) {
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ }
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ "test_setting_contenteditable_of_focused_div: IME should be enabled when contenteditable of focused <div> is set"
+ );
+ ok(
+ tipWrapper.IMEHasFocus,
+ "test_setting_contenteditable_of_focused_div: IME should have focus when contenteditable of focused <div> is set"
+ )
+ div.removeAttribute("contenteditable");
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "test_setting_contenteditable_of_focused_div: IME should be disabled when contenteditable of focused <div> is removed"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "test_setting_contenteditable_of_focused_div: IME should not have focus when contenteditable of focused <div> is removed"
+ );
+ div.removeAttribute("tabindex");
+ })();
+
+ await resetIMEStateWithFocusMove();
+
+ await (async function test_removing_contenteditable_of_non_last_editable_div() {
+ const div = document.querySelector("div");
+ div.setAttribute("tabindex", "0");
+ div.setAttribute("contenteditable", "");
+ const anotherEditableDiv = document.createElement("div");
+ anotherEditableDiv.setAttribute("contenteditable", "");
+ div.parentElement.appendChild(anotherEditableDiv);
+ div.focus();
+ await waitForIMEContentObserverSendingNotifications();
+ div.removeAttribute("contenteditable");
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ "test_removing_contenteditable_of_non_last_editable_div: IME should be disabled when contenteditable of focused <div> is removed"
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ "test_removing_contenteditable_of_non_last_editable_div: IME should not have focus when contenteditable of focused <div> is removed"
+ );
+ anotherEditableDiv.remove();
+ div.removeAttribute("tabindex");
+ })();
+
+ await resetIMEStateWithFocusMove();
+
+ await (async function test_setting_designMode() {
+ window.focus();
+ document.designMode = "on";
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ 'test_setting_designMode: IME should be enabled when designMode is set to "on"'
+ );
+ ok(
+ tipWrapper.IMEHasFocus,
+ 'test_setting_designMode: IME should have focus when designMode is set to "on"'
+ );
+ document.designMode = "off";
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ 'test_setting_designMode: IME should be disabled when designMode is set to "off"'
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ 'test_setting_designMode: IME should not have focus when designMode is set to "off"'
+ );
+ })();
+
+ await resetIMEStateWithFocusMove();
+
+ async function test_setting_content_editable_of_body_when_shadow_DOM_has_focus(aMode) {
+ const div = document.querySelector("div");
+ const shadow = div.attachShadow({mode: aMode});
+ const divInShadow = document.createElement("div");
+ divInShadow.setAttribute("tabindex", "0");
+ shadow.appendChild(divInShadow);
+ divInShadow.focus();
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should be disabled when non-editable <div> in a shadow DOM has focus`
+ );
+ document.body.setAttribute("contenteditable", "");
+ await waitForIMEContentObserverSendingNotifications();
+ // todo_is because of bug 1807597. Gecko does not update focus when focused
+ // element becomes an editable child. Therefore, cannot initialize
+ // HTMLEditor with the new editing host.
+ todo_is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should be enabled when the <body> becomes editable`
+ );
+ todo(
+ tipWrapper.IMEHasFocus,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should have focus when the <body> becomes editable`
+ );
+ document.body.removeAttribute("contenteditable");
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${
+ aMode
+ }): IME should be disabled when the <body> becomes not editable`
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${
+ aMode
+ }): IME should not have focus when the <body> becomes not editable`
+ );
+ div.remove();
+ document.body.appendChild(document.createElement("div"));
+ };
+
+ async function test_setting_designMode_when_shadow_DOM_has_focus(aMode) {
+ const div = document.querySelector("div");
+ const shadow = div.attachShadow({mode: aMode});
+ const divInShadow = document.createElement("div");
+ divInShadow.setAttribute("tabindex", "0");
+ shadow.appendChild(divInShadow);
+ divInShadow.focus();
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should be disabled when non-editable <div> in a shadow DOM has focus`
+ );
+ document.designMode = "on";
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should stay disabled when designMode is set`
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should not have focus when designMode is set`
+ );
+ divInShadow.setAttribute("contenteditable", "");
+ await waitForIMEContentObserverSendingNotifications();
+ // todo_is because of bug 1807597. Gecko does not update focus when focused
+ // document is into the design mode. Therefore, cannot initialize
+ // HTMLEditor with the document node properly.
+ todo_is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should be enabled when focused <div> in a shadow DOM becomes editable`
+ );
+ todo(
+ tipWrapper.IMEHasFocus,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should have focus when focused <div> in a shadow DOM becomes editable`
+ );
+ document.designMode = "off";
+ await waitForIMEContentObserverSendingNotifications();
+ is(
+ window.windowUtils.IMEStatus,
+ Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should be disabled when designMode is unset`
+ );
+ ok(
+ !tipWrapper.IMEHasFocus,
+ `test_setting_designMode_when_shadow_DOM_has_focus(${
+ aMode
+ }): IME should not have focus when designMode is unset`
+ );
+ div.remove();
+ document.body.appendChild(document.createElement("div"));
+ }
+
+ for (const mode of ["open", "closed"]) {
+ await test_setting_content_editable_of_body_when_shadow_DOM_has_focus(mode);
+ await resetIMEStateWithFocusMove();
+ await test_setting_designMode_when_shadow_DOM_has_focus(mode);
+ await resetIMEStateWithFocusMove();
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_ime_state_on_focus_move_in_parent.html b/widget/tests/test_ime_state_on_focus_move_in_parent.html
new file mode 100644
index 0000000000..fd74d61c7e
--- /dev/null
+++ b/widget/tests/test_ime_state_on_focus_move_in_parent.html
@@ -0,0 +1,88 @@
+<!doctype html>
+<html style="ime-mode: disabled;">
+<head>
+ <meta charset="utf-8">
+ <title>Test for IME state management on focus move in parent process</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <script src="file_test_ime_state_on_focus_move.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body style="ime-mode: disabled;">
+<div style="ime-mode: disabled;"></div>
+<script>
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+/* import-globals-from file_test_ime_state_on_focus_move.js */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const tipWrapper = new TIPWrapper(window);
+ ok(tipWrapper.isAvailable(), "TextInputProcessor should've been initialized");
+
+ const container = document.querySelector("div");
+ async function runIMEStateOnFocusMoveTests(aDescription) {
+ {
+ const runnerAndChecker = new IMEStateWhenNoActiveElementTester(aDescription);
+ const expectedData = await runnerAndChecker.run(document);
+ runnerAndChecker.check(expectedData);
+ }
+ for (let index = 0; index < IMEStateOnFocusMoveTester.numberOfTests; ++index) {
+ const runnerAndChecker = new IMEStateOnFocusMoveTester(aDescription, index);
+ const expectedData = await runnerAndChecker.prepareToRun(container);
+ runnerAndChecker.prepareToCheck(expectedData, tipWrapper);
+ await runnerAndChecker.run();
+ runnerAndChecker.check(expectedData);
+ if (runnerAndChecker.canTestOpenCloseState(expectedData)) {
+ for (const defaultOpenState of [false, true]) {
+ const expectedOpenStateData =
+ await runnerAndChecker.prepareToRunOpenCloseTest(container);
+ runnerAndChecker.prepareToCheckOpenCloseTest(
+ defaultOpenState,
+ expectedOpenStateData
+ );
+ await runnerAndChecker.runOpenCloseTest();
+ runnerAndChecker.checkOpenCloseTest(expectedOpenStateData);
+ }
+ }
+ runnerAndChecker.destroy();
+ }
+ }
+
+ // test for normal contents.
+ await runIMEStateOnFocusMoveTests("in non-editable container");
+
+ // test for contentEditable="true"
+ container.setAttribute("contenteditable", "true");
+ await runIMEStateOnFocusMoveTests("in div[contenteditable]");
+
+ // test for contentEditable="false"
+ container.setAttribute("contenteditable", "false");
+ await runIMEStateOnFocusMoveTests('in div[contenteditable="false"]');
+
+ // test for removing contentEditable
+ container.setAttribute("contenteditable", "true");
+ container.focus();
+ await new Promise(resolve =>
+ requestAnimationFrame(
+ () => requestAnimationFrame(resolve)
+ )
+ );
+ container.removeAttribute("contenteditable");
+ await runIMEStateOnFocusMoveTests("after removing contenteditable from the container");
+
+ // test designMode
+ document.designMode = "on";
+ await runIMEStateOnFocusMoveTests('in designMode="on"');
+ document.designMode = "off";
+ await runIMEStateOnFocusMoveTests('in designMode="off"');
+
+ tipWrapper.destroy();
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_ime_state_on_input_type_change_in_parent.html b/widget/tests/test_ime_state_on_input_type_change_in_parent.html
new file mode 100644
index 0000000000..2644c31e3f
--- /dev/null
+++ b/widget/tests/test_ime_state_on_input_type_change_in_parent.html
@@ -0,0 +1,39 @@
+<html>
+<head>
+ <title>Test for IME state on input type change</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <script src="file_test_ime_state_on_input_type_change.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<script>
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+/* import-globals-from file_test_ime_state_on_input_type_change.js */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(6); // Hit in IMEStateManager::UpdateIMEState
+SimpleTest.waitForFocus(async () => {
+ const tipWrapper = new TIPWrapper(window);
+ for (let srcIndex = 0; srcIndex < IMEStateOnInputTypeChangeTester.numberOfTests; srcIndex++) {
+ const tester = new IMEStateOnInputTypeChangeTester(srcIndex);
+ for (let destIndex = 0; destIndex < IMEStateOnInputTypeChangeTester.numberOfTests; destIndex++) {
+ const expectedResultBefore = await tester.prepareToRun(destIndex, window, document.body);
+ if (expectedResultBefore === false) {
+ continue;
+ }
+ tester.checkBeforeRun(expectedResultBefore, tipWrapper);
+ const expectedResult = await tester.run();
+ tester.checkResult(expectedResultBefore, expectedResult);
+ tipWrapper.clearFocusBlurNotifications();
+ tester.clear();
+ }
+ }
+
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/widget/tests/test_ime_state_on_readonly_change_in_parent.html b/widget/tests/test_ime_state_on_readonly_change_in_parent.html
new file mode 100644
index 0000000000..0557856542
--- /dev/null
+++ b/widget/tests/test_ime_state_on_readonly_change_in_parent.html
@@ -0,0 +1,31 @@
+<html>
+<head>
+ <title>Test for IME state on readonly state change</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <script src="file_test_ime_state_on_readonly_change.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<script>
+"use strict";
+
+/* import-globals-from file_ime_state_test_helper.js */
+/* import-globals-from file_test_ime_state_on_readonly_change.js */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const tipWrapper = new TIPWrapper(window);
+ const tester = new IMEStateOnReadonlyChangeTester();
+ for (let i = 0; i < IMEStateOnReadonlyChangeTester.numberOfTextControlTypes; i++) {
+ tester.checkBeforeRun(await tester.prepareToRun(i, window, document.body), tipWrapper);
+ tester.checkResultOfMakingTextControlReadonly(await tester.runToMakeTextControlReadonly());
+ tester.checkResultOfMakingTextControlEditable(await tester.runToMakeTextControlEditable());
+ tipWrapper.clearFocusBlurNotifications();
+ tester.clear();
+ }
+ SimpleTest.finish();
+});
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/widget/tests/test_ime_state_others_in_parent.html b/widget/tests/test_ime_state_others_in_parent.html
new file mode 100644
index 0000000000..e6ae0ab272
--- /dev/null
+++ b/widget/tests/test_ime_state_others_in_parent.html
@@ -0,0 +1,153 @@
+<html>
+<head>
+ <title>Test for IME state controlling in some special cases</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="file_ime_state_test_helper.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="display"></div>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+var gUtils = window.windowUtils;
+var gFM = Services.focus;
+
+function runEditorFlagChangeTests() {
+ var description = "runEditorFlagChangeTests: ";
+
+ var container = document.getElementById("display");
+
+ // Reset selection from previous tests.
+ window.getSelection().collapse(container, 0);
+
+ // the editor has focus directly.
+ container.setAttribute("contenteditable", "true");
+ container.focus();
+
+ is(gFM.focusedElement, container,
+ description + "The editor doesn't get focus");
+ is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED,
+ description + "IME isn't enabled on HTML editor");
+ const kIMEStateChangeFlags = Ci.nsIEditor.eEditorReadonlyMask;
+ const kFlagsNotAllowedWithHTMLEditor =
+ Ci.nsIEditor.eEditorPasswordMask |
+ Ci.nsIEditor.eEditorSingleLineMask;
+ var editor = window.docShell.editor;
+ var flags = editor.flags;
+
+ // input characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3078\u3093\u3057\u3093",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ ],
+ },
+ "caret": { "start": 4, "length": 0 },
+ });
+
+ editor.flags &= ~kIMEStateChangeFlags;
+ ok(editor.composing,
+ description + "#1 IME composition was committed unexpectedly");
+ is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED,
+ description + "#1 IME isn't enabled on HTML editor");
+
+ editor.flags |=
+ ~(kIMEStateChangeFlags | kFlagsNotAllowedWithHTMLEditor);
+ ok(editor.composing,
+ description + "#2 IME composition was committed unexpectedly");
+ is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED,
+ description + "#2 IME isn't enabled on HTML editor");
+
+ editor.flags = flags;
+ ok(editor.composing,
+ description + "#3 IME composition was committed unexpectedly");
+ is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED,
+ description + "#3 IME isn't enabled on HTML editor");
+
+ // cancel the composition
+ synthesizeComposition({ type: "compositioncommit", data: "" });
+
+ container.removeAttribute("contenteditable");
+}
+
+function runEditableSubframeTests() {
+ window.open("window_imestate_iframes.html", "_blank",
+ "width=600,height=600");
+}
+
+function runTestPasswordFieldOnDialog() {
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+
+ var dialog;
+
+ function WindowObserver() {
+ Services.obs.addObserver(this, "domwindowopened");
+ }
+
+ WindowObserver.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(subject, topic, data) {
+ if (topic === "domwindowopened") {
+ ok(true, "dialog window is created");
+ dialog = subject;
+ dialog.addEventListener("load", onPasswordDialogLoad);
+ }
+ },
+ };
+
+ var observer = new WindowObserver();
+ var arg1 = {}, arg2 = {};
+ Services.prompt.promptPassword(window, "title", "text", arg1, "msg", arg2);
+
+ ok(true, "password dialog was closed");
+
+ Services.obs.removeObserver(observer, "domwindowopened");
+
+ var passwordField;
+
+ function onPasswordDialogLoad() {
+ ok(true, "onPasswordDialogLoad is called");
+ dialog.removeEventListener("load", onPasswordDialogLoad);
+ passwordField = dialog.document.getElementById("password1Textbox");
+ passwordField.addEventListener("focus", onPasswordFieldFocus);
+ }
+
+ function onPasswordFieldFocus() {
+ ok(true, "onPasswordFieldFocus is called");
+ passwordField.removeEventListener("focus", onPasswordFieldFocus);
+ var utils = dialog.windowUtils;
+ is(utils.IMEStatus, utils.IME_STATUS_PASSWORD,
+ "IME isn't disabled on a password field of password dialog");
+ synthesizeKey("VK_ESCAPE", { }, dialog);
+ }
+}
+
+SimpleTest.waitForFocus(async () => {
+ // test whether the IME state and composition are not changed unexpectedly
+ runEditorFlagChangeTests();
+
+ // test password field on dialog
+ // XXX temporary disable against failure
+ // runTestPasswordFieldOnDialog();
+
+ // This will call onFinish(), so, this test must be the last.
+ // TODO: Make this test run with remote content too.
+ runEditableSubframeTests();
+});
+
+function onFinish() {
+ SimpleTest.finish();
+}
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_input_events_on_deactive_window.xhtml b/widget/tests/test_input_events_on_deactive_window.xhtml
new file mode 100644
index 0000000000..d54699f76c
--- /dev/null
+++ b/widget/tests/test_input_events_on_deactive_window.xhtml
@@ -0,0 +1,233 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Testing composition, text and query content events"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+</div>
+<p id="display">
+ <textarea id="textarea"></textarea>
+</p>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests, window);
+
+var textarea = document.getElementById("textarea");
+var otherWindow;
+var timer;
+
+function runTests()
+{
+ textarea.focus();
+ is(Services.focus.focusedElement, textarea, "we're deactive");
+ if (Services.focus.focusedElement != textarea) {
+ SimpleTest.finish();
+ return;
+ }
+
+ otherWindow =
+ window.browsingContext.topChromeWindow.open(
+ "./file_input_events_on_deactive_window.html", "_blank",
+ "chrome,width=100,height=100");
+ ok(otherWindow, "failed to open other window");
+ if (!otherWindow) {
+ SimpleTest.finish();
+ return;
+ }
+
+ SimpleTest.waitForFocus(startTests, otherWindow);
+ otherWindow.focus();
+}
+
+function startTests()
+{
+ clearTimeout(timer);
+ isnot(Services.focus.focusedWindow, window, "we're not deactive");
+ if (Services.focus.focusedWindow == window) {
+ otherWindow.close();
+ SimpleTest.finish();
+ return;
+ }
+
+ var keydownHandled, keypressHandled, keyupHandled, compositionstartHandled,
+ compositionendHandled, compositionupdateHandled, inputHandled;
+
+ function clear()
+ {
+ keydownHandled = false;
+ keypressHandled = false;
+ keyupHandled = false;
+ compositionstartHandled = false;
+ compositionendHandled = false;
+ compositionupdateHandled = false;
+ inputHandled = false;
+ }
+
+ function onEvent(aEvent)
+ {
+ if (aEvent.type == "keydown") {
+ keydownHandled = true;
+ } else if (aEvent.type == "keypress") {
+ keypressHandled = true;
+ } else if (aEvent.type == "keyup") {
+ keyupHandled = true;
+ } else if (aEvent.type == "compositionstart") {
+ compositionstartHandled = true;
+ } else if (aEvent.type == "compositionend") {
+ compositionendHandled = true;
+ } else if (aEvent.type == "compositionupdate") {
+ compositionupdateHandled = true;
+ } else if (aEvent.type == "input") {
+ inputHandled = true;
+ } else {
+ ok(false, "handled unknown event: " + aEvent.type);
+ }
+ }
+
+ textarea.addEventListener("keydown", onEvent);
+ textarea.addEventListener("keypress", onEvent);
+ textarea.addEventListener("keyup", onEvent);
+ textarea.addEventListener("compositionstart", onEvent);
+ textarea.addEventListener("compositionend", onEvent);
+ textarea.addEventListener("compositionupdate", onEvent);
+ textarea.addEventListener("input", onEvent);
+
+ startTestsInternal();
+
+ function startTestsInternal()
+ {
+ // key events
+ function checkKeyEvents(aKeydown, aKeypress, aKeyup, aInput, aDescription)
+ {
+ is(keydownHandled, aKeydown,
+ "keydown event is (not) handled: " + aDescription);
+ is(keypressHandled, aKeypress,
+ "keypress event is (not) handled: " + aDescription);
+ is(keyupHandled, aKeyup,
+ "keyup event is (not) handled: " + aDescription);
+ is(inputHandled, aInput,
+ "input event is (not) handled: " + aDescription);
+ }
+
+ function checkCompositionEvents(aStart, aEnd, aUpdate, aInput, aDescription)
+ {
+ is(compositionstartHandled, aStart,
+ "compositionstart event is (not) handled: " + aDescription);
+ is(compositionendHandled, aEnd,
+ "compositionend event is (not) handled: " + aDescription);
+ is(compositionupdateHandled, aUpdate,
+ "compositionupdate event is (not) handled: " + aDescription);
+ is(inputHandled, aInput,
+ "input event is (not) handled: " + aDescription);
+ }
+
+ clear();
+ synthesizeKey("a", {type: "keydown"});
+ checkKeyEvents(true, true, false, true, "a keydown and a keypress");
+ is(textarea.value, "a", "textarea value isn't 'a'");
+ clear();
+ synthesizeKey("a", {type: "keyup"});
+ checkKeyEvents(false, false, true, false, "a keyup");
+ clear();
+ synthesizeKey("KEY_Backspace");
+ checkKeyEvents(true, true, true, true, "KEY_Backspace key events");
+ is(textarea.value, "", "textarea value isn't empty");
+
+ // IME events
+ clear();
+ // input first character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ checkCompositionEvents(true, false, true, true, "starting to compose");
+ var queryText = synthesizeQueryTextContent(0, 100);
+ ok(queryText, "query text event result is null");
+ if (!queryText) {
+ return;
+ }
+ ok(queryText.succeeded, "query text event failed");
+ if (!queryText.succeeded) {
+ return;
+ }
+ is(queryText.text, "\u3089", "composing text is incorrect");
+ var querySelectedText = synthesizeQuerySelectedText();
+ ok(querySelectedText, "query selected text event result is null");
+ if (!querySelectedText) {
+ return;
+ }
+ ok(querySelectedText.succeeded, "query selected text event failed");
+ if (!querySelectedText.succeeded) {
+ return;
+ }
+ is(querySelectedText.offset, 1,
+ "query selected text event returns wrong offset");
+ is(querySelectedText.text, "",
+ "query selected text event returns wrong selected text");
+ clear();
+ // commit composition
+ synthesizeComposition({ type: "compositioncommitasis" });
+ checkCompositionEvents(false, true, false, true, "commit composition as is");
+ queryText = synthesizeQueryTextContent(0, 100);
+ ok(queryText, "query text event result is null after commit");
+ if (!queryText) {
+ return;
+ }
+ ok(queryText.succeeded, "query text event failed after commit");
+ if (!queryText.succeeded) {
+ return;
+ }
+ is(queryText.text, "\u3089", "composing text is incorrect after commit");
+ querySelectedText = synthesizeQuerySelectedText();
+ ok(querySelectedText,
+ "query selected text event result is null after commit");
+ if (!querySelectedText) {
+ return;
+ }
+ ok(querySelectedText.succeeded,
+ "query selected text event failed after commit");
+ if (!querySelectedText.succeeded) {
+ return;
+ }
+ is(querySelectedText.offset, 1,
+ "query selected text event returns wrong offset after commit");
+ is(querySelectedText.text, "",
+ "query selected text event returns wrong selected text after commit");
+ clear();
+ }
+
+ textarea.removeEventListener("keydown", onEvent);
+ textarea.removeEventListener("keypress", onEvent);
+ textarea.removeEventListener("keyup", onEvent);
+ textarea.removeEventListener("compositionstart", onEvent);
+ textarea.removeEventListener("compositionupdate", onEvent);
+ textarea.removeEventListener("compositionend", onEvent);
+ textarea.removeEventListener("input", onEvent);
+
+ otherWindow.close();
+
+ SimpleTest.finish();
+}
+
+
+]]>
+</script>
+</window>
diff --git a/widget/tests/test_key_event_counts.xhtml b/widget/tests/test_key_event_counts.xhtml
new file mode 100644
index 0000000000..6eda6a52fb
--- /dev/null
+++ b/widget/tests/test_key_event_counts.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<!-- We've had issues on Mac OS X where native key events either don't get processed
+ or they get processed twice. This test tests some of those scenarios. -->
+
+<window id="window1" title="Test Key Event Counts" onload="runTest()"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <script type="application/javascript"><![CDATA[
+ var gKeyPressEventCount = 0;
+ var gKeyDownEventCount = 0;
+
+ function onKeyDown(e)
+ {
+ gKeyDownEventCount++;
+ }
+
+ function onKeyPress(e)
+ {
+ gKeyPressEventCount++;
+ e.preventDefault();
+ }
+
+ function* testBody()
+ {
+ window.addEventListener("keydown", onKeyDown);
+ window.addEventListener("keypress", onKeyPress);
+
+ // Test ctrl-tab
+ gKeyDownEventCount = 0;
+ gKeyPressEventCount = 0;
+ yield synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_Tab, {ctrlKey:1}, "\t", "\t", continueTest);
+ is(gKeyDownEventCount, 1);
+ is(gKeyPressEventCount, 0, "ctrl-tab should be consumed by tabbox of tabbrowser at keydown");
+
+ // Test cmd+shift+a
+ gKeyDownEventCount = 0;
+ gKeyPressEventCount = 0;
+ yield synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {metaKey:1, shiftKey:1}, "a", "A", continueTest);
+ is(gKeyDownEventCount, 1);
+ is(gKeyPressEventCount, 1);
+
+ // Test cmd-;
+ gKeyDownEventCount = 0;
+ gKeyPressEventCount = 0;
+ yield synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_Semicolon, {metaKey:1}, ";", ";", continueTest);
+ is(gKeyDownEventCount, 1);
+ is(gKeyPressEventCount, 1);
+
+ window.removeEventListener("keydown", onKeyDown);
+ window.removeEventListener("keypress", onKeyPress);
+ }
+
+ var gTestContinuation = null;
+
+ function continueTest()
+ {
+ if (!gTestContinuation) {
+ gTestContinuation = testBody();
+ }
+ var ret = gTestContinuation.next();
+ if (ret.done) {
+ SimpleTest.finish();
+ } else {
+ is(ret.value, true, "Key synthesized successfully");
+ }
+ }
+
+ function runTest()
+ {
+ SimpleTest.waitForExplicitFinish();
+ continueTest();
+ }
+ ]]></script>
+
+</window>
diff --git a/widget/tests/test_keycodes.xhtml b/widget/tests/test_keycodes.xhtml
new file mode 100644
index 0000000000..aea6aaae98
--- /dev/null
+++ b/widget/tests/test_keycodes.xhtml
@@ -0,0 +1,5647 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Key event tests"
+ onload="runTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<commandset>
+ <command id="expectedCommand" oncommand="this.activeCount++" disabled="true"/>
+ <command id="unexpectedCommand" oncommand="this.activeCount++" disabled="true"/>
+ <command id="expectedReservedCommand" oncommand="this.activeCount++" reserved="true" disabled="true"/>
+</commandset>
+<keyset>
+ <key id="unshiftedKey" key=";" modifiers="accel" command="unexpectedCommand"/>
+ <key id="shiftedKey" key=":" modifiers="accel" command="unexpectedCommand"/>
+ <key id="commandOptionF" key='f' modifiers="accel,alt" command="unexpectedCommand"/>
+ <key id="question" key='?' modifiers="accel" command="unexpectedCommand"/>
+ <key id="unshiftedX" key="x" modifiers="accel" command="unexpectedCommand"/>
+ <key id="shiftedX" key="X" modifiers="accel,shift" command="unexpectedCommand"/>
+ <key id="ctrlAltA" key="a" modifiers="accel,alt" command="unexpectedCommand"/>
+ <key id="ctrlAltShiftA" key="A" modifiers="accel,alt,shift" command="unexpectedCommand"/>
+ <key id="unshiftedPlus" key="+" modifiers="accel" command="unexpectedCommand"/>
+ <key id="reservedUnshiftedKey" key="'" modifiers="accel" command="unexpectedCommand"/>
+ <key id="reservedShiftedKey" key='"' modifiers="accel" command="unexpectedCommand"/>
+</keyset>
+
+<browser id="browser" type="content" src="data:text/html;charset=utf-8,&lt;button id='content_button'&gt;button&lt;/button&gt;" width="200" height="32"/>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display">
+ <!-- for some reason, if we don't have 'accesskey' here, adding it dynamically later
+ doesn't work! -->
+ <button id="button" accesskey="z">Hello</button>
+ <input type="text" id="textbox" value=""/>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+const IS_MAC = navigator.platform.indexOf("Mac") == 0;
+const IS_WIN = navigator.platform.indexOf("Win") == 0;
+const OS_VERSION =
+ IS_WIN ? parseFloat(Services.sysinfo.getProperty("version")) : 0;
+const WIN8 = 6.2; // Can remove once bug 1594270 is closed
+
+function isModifierKeyEvent(aEvent)
+{
+ switch (aEvent.key) {
+ case "Alt":
+ case "AltGraph":
+ case "CapsLock":
+ case "Control":
+ case "Fn":
+ case "FnLock":
+ case "Hyper":
+ case "Meta":
+ case "NumLock":
+ case "ScrollLock":
+ case "Shift":
+ case "Super":
+ case "Symbol":
+ case "SymbolLock":
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * Firefox infobar UI can have access keys which conflict with this test. Really
+ * stupid workaround until we can move this test into its own chrome window.
+ */
+function clearInfobars()
+{
+ var browser = window.top.docShell.chromeEventHandler;
+ var chromeWin = browser.ownerGlobal;
+ var nb = chromeWin.gBrowser.getNotificationBox(browser);
+ for (let n of nb.allNotifications) {
+ nb.removeNotification(n, true);
+ }
+}
+
+function eventToString(aEvent)
+{
+ var name = aEvent.layout.name + " keyCode=" +
+ aEvent.keyCode + " (0x" + aEvent.keyCode.toString(16).toUpperCase() +
+ ") chars='" + aEvent.chars + "'";
+ if (typeof aEvent.unmodifiedChars === "string") {
+ name += " unmodifiedChars='" + aEvent.unmodifiedChars + "'";
+ }
+ if (aEvent.modifiers.capsLockKey) {
+ name += " [CapsLock]";
+ }
+ if (aEvent.modifiers.shiftKey) {
+ name += " [Shift]";
+ }
+ if (aEvent.modifiers.shiftRightKey) {
+ name += " [Right Shift]";
+ }
+ if (aEvent.modifiers.ctrlKey) {
+ name += " [Ctrl]";
+ }
+ if (aEvent.modifiers.ctrlRightKey) {
+ name += " [Right Ctrl]";
+ }
+ if (aEvent.modifiers.altKey) {
+ name += " [Alt]";
+ }
+ if (aEvent.modifiers.altGrKey) {
+ name += " [AltGr]";
+ }
+ if (aEvent.modifiers.altRightKey) {
+ name += " [Right Alt]";
+ }
+ if (aEvent.modifiers.metaKey) {
+ name += ` [${IS_MAC ? "Command" : "Win"}]`;
+ }
+ if (aEvent.modifiers.metaRightKey) {
+ name += ` [Right ${IS_MAC ? "Command" : "Win"}]`;
+ }
+
+ return name;
+}
+
+function getPhase(aDOMEvent)
+{
+ switch (aDOMEvent.eventPhase) {
+ case aDOMEvent.None:
+ return "none";
+ case aDOMEvent.CAPTURING_PHASE:
+ return "capture";
+ case aDOMEvent.AT_TARGET:
+ return "target";
+ case aDOMEvent.BUBBLING_PHASE:
+ return "bubble";
+ default:
+ return "";
+ }
+}
+
+function eventTargetToString(aEventTarget)
+{
+ if (aEventTarget.navigator) {
+ return "window";
+ }
+ switch (aEventTarget.nodeType) {
+ case Node.ELEMENT_NODE:
+ return "element (" + aEventTarget.tagName + ")";
+ case Node.DOCUMENT_NODE:
+ return "document";
+ default:
+ return "";
+ }
+}
+
+function synthesizeKey(aEvent, aFocusElementId, aCallback)
+{
+ if (aFocusElementId.startsWith("content_")) {
+ var browser = document.getElementById("browser");
+ browser.contentDocument.getElementById(aFocusElementId).focus();
+ } else {
+ document.getElementById(aFocusElementId).focus();
+ }
+
+ return synthesizeNativeKey(aEvent.layout, aEvent.keyCode,
+ aEvent.modifiers,
+ aEvent.chars, aEvent.unmodifiedChars,
+ aCallback);
+}
+
+// Test the charcodes and modifiers being delivered to keypress handlers and
+// also keydown/keyup events too.
+function* runKeyEventTests()
+{
+ var currentTestName;
+ var eventList, keyDownFlags, keyUpFlags, testingEvent, expectedDOMKeyCode;
+ const kShiftFlag = 0x1;
+ const kCtrlFlag = 0x2;
+ const kAltFlag = 0x4;
+ const kMetaFlag = 0x8;
+ const kNumLockFlag = 0x10;
+ const kCapsLockFlag = 0x20;
+ const kAltGraphFlag = 0x40;
+
+ function onKeyEvent(e)
+ {
+ /* eslint-disable-next-line no-shadow */
+ function removeFlag(e, aFlag)
+ {
+ if (e.type == "keydown") {
+ let oldValue = keyDownFlags;
+ keyDownFlags &= ~aFlag;
+ return oldValue != keyDownFlags;
+ } else if (e.type == "keyup") {
+ let oldValue = keyUpFlags;
+ keyUpFlags &= ~aFlag;
+ return oldValue != keyUpFlags;
+ }
+ return false;
+ }
+
+ /* eslint-disable-next-line no-shadow, complexity */
+ function isStateChangingModifierKeyEvent(e)
+ {
+ var flags = 0;
+ if (e.type == "keydown") {
+ flags = keyDownFlags ^ keyUpFlags;
+ } else if (e.type == "keyup") {
+ flags = keyUpFlags;
+ }
+ switch (e.key) {
+ case "Shift":
+ is(e.ctrlKey, (flags & kCtrlFlag) != 0,
+ currentTestName + ", Ctrl of Shift " + e.type + " event mismatch");
+ is(e.metaKey, (flags & kMetaFlag) != 0,
+ currentTestName + ", Command of Shift " + e.type + " event mismatch");
+ is(e.altKey, (flags & kAltFlag) != 0,
+ currentTestName + ", Alt of Shift " + e.type + " event mismatch");
+ is(e.shiftKey, e.type == "keydown",
+ currentTestName + ", Shift of Shift " + e.type + " event mismatch");
+ // AltGr on Windows is always pressed after and released before Shift key operation.
+ is(e.getModifierState("AltGraph"), (IS_MAC && e.altKey),
+ currentTestName + ", AltGraph of Shift " + e.type + " event mismatch");
+ return (testingEvent.modifiers.shiftKey || testingEvent.modifiers.shiftRightKey) &&
+ removeFlag(e, kShiftFlag) && expectedDOMKeyCode != e.keyCode;
+ case "Control":
+ is(e.ctrlKey, e.type == "keydown",
+ currentTestName + ", Ctrl of Ctrl " + e.type + " event mismatch");
+ is(e.metaKey, (flags & kMetaFlag) != 0,
+ currentTestName + ", Command of Ctrl " + e.type + " event mismatch");
+ // When AltGr key is released on Windows, ControlLeft keyup event
+ // is followed by AltRight keyup event. However, altKey should be
+ // false in such case.
+ is(e.altKey, (flags & kAltFlag) != 0 && !(IS_WIN && !!testingEvent.modifiers.altGrKey),
+ currentTestName + ", Alt of Ctrl " + e.type + " event mismatch");
+ is(e.shiftKey, (flags & kShiftFlag) != 0,
+ currentTestName + ", Shift of Ctrl " + e.type + " event mismatch");
+ is(e.getModifierState("AltGraph"),
+ (IS_WIN && !!testingEvent.modifiers.altGrKey && e.type == "keyup") || (IS_MAC && e.altKey),
+ currentTestName + ", AltGraph of Ctrl " + e.type + " event mismatch");
+ return (testingEvent.modifiers.ctrlKey || testingEvent.modifiers.ctrlRightKey ||
+ (IS_WIN && !!testingEvent.modifiers.altGrKey)) &&
+ removeFlag(e, kCtrlFlag) && expectedDOMKeyCode != e.keyCode;
+ case "Alt":
+ is(e.ctrlKey, (flags & kCtrlFlag) != 0 && !(IS_WIN && !!testingEvent.modifiers.altGrKey),
+ currentTestName + ", Ctrl of Alt " + e.type + " event mismatch");
+ is(e.metaKey, (flags & kMetaFlag) != 0,
+ currentTestName + ", Command of Alt " + e.type + " event mismatch");
+ is(e.altKey, e.type == "keydown" && !(IS_WIN && !!testingEvent.modifiers.altGrKey),
+ currentTestName + ", Alt of Alt " + e.type + " event mismatch");
+ is(e.shiftKey, (flags & kShiftFlag) != 0,
+ currentTestName + ", Shift of Alt " + e.type + " event mismatch");
+ is(e.getModifierState("AltGraph"),
+ e.type == "keydown" && ((IS_WIN && !!testingEvent.modifiers.altGrKey) || (IS_MAC && e.altKey)),
+ currentTestName + ", AltGraph of Alt " + e.type + " event mismatch");
+ return (testingEvent.modifiers.altKey || testingEvent.modifiers.altRightKey ||
+ (IS_WIN && !!testingEvent.modifiers.altGrKey)) &&
+ removeFlag(e, kAltFlag) && expectedDOMKeyCode != e.keyCode;
+ case "AltGraph":
+ // On Windows, AltGraph events are fired only when AltRight key is
+ // pressed when active keyboard layout maps AltGraph to AltRight.
+ // Note that AltGraph is represented with pressing both Control key
+ // and Alt key. Therefore, when AltGraph keyboard event is fired,
+ // both ctrlKey and altKey are always false on Windows.
+ is(e.ctrlKey, (flags & kCtrlFlag) != 0 && !IS_WIN,
+ currentTestName + ", Ctrl of AltGraph " + e.type + " event mismatch");
+ is(e.metaKey, (flags & kMetaFlag) != 0,
+ currentTestName + ", Command of AltGraph " + e.type + " event mismatch");
+ is(e.altKey, (flags & kAltFlag) != 0 && !IS_WIN,
+ currentTestName + ", Alt of AltGraph " + e.type + " event mismatch");
+ is(e.shiftKey, (flags & kShiftFlag) != 0,
+ currentTestName + ", Shift of Ctrl " + e.type + " event mismatch");
+ is(e.getModifierState("AltGraph"), e.type === "keydown",
+ currentTestName + ", AltGraph of AltGraph " + e.type + " event mismatch");
+ return IS_WIN && testingEvent.modifiers.altGrKey &&
+ removeFlag(e, kAltGraphFlag) && expectedDOMKeyCode != e.keyCode;
+ case "Meta":
+ is(e.ctrlKey, (flags & kCtrlFlag) != 0,
+ currentTestName + ", Ctrl of Command " + e.type + " event mismatch");
+ is(e.metaKey, e.type == "keydown",
+ currentTestName + ", Command of Command " + e.type + " event mismatch");
+ is(e.altKey, (flags & kAltFlag) != 0,
+ currentTestName + ", Alt of Command " + e.type + " event mismatch");
+ is(e.shiftKey, (flags & kShiftFlag) != 0,
+ currentTestName + ", Shift of Command " + e.type + " event mismatch");
+ is(e.getModifierState("AltGraph"),
+ (IS_WIN && (flags & kAltGraphFlag) != 0) || (IS_MAC && e.altKey),
+ currentTestName + ", AltGraph of Meta " + e.type + " event mismatch");
+ return (testingEvent.modifiers.metaKey || testingEvent.modifiers.metaRightKey) &&
+ removeFlag(e, kMetaFlag) && expectedDOMKeyCode != e.keyCode;
+ case "NumLock":
+ is(e.ctrlKey, (flags & kCtrlFlag) != 0,
+ currentTestName + ", Ctrl of NumLock " + e.type + " event mismatch");
+ is(e.metaKey, (flags & kMetaFlag) != 0,
+ currentTestName + ", Command of NumLock " + e.type + " event mismatch");
+ is(e.altKey, (flags & kAltFlag) != 0,
+ currentTestName + ", Alt of NumLock " + e.type + " event mismatch");
+ is(e.shiftKey, (flags & kShiftFlag) != 0,
+ currentTestName + ", Shift of NumLock " + e.type + " event mismatch");
+ is(e.getModifierState("AltGraph"), false,
+ currentTestName + ", AltGraph of NumLock " + e.type + " event mismatch");
+ // AltGr on Windows is always pressed after and released before NumLock key operation.
+ return (testingEvent.modifiers.numLockKey || testingEvent.modifiers.numericKeyPadKey) &&
+ removeFlag(e, kNumLockFlag) && expectedDOMKeyCode != e.keyCode;
+ case "CapsLock":
+ is(e.ctrlKey, (flags & kCtrlFlag) != 0,
+ currentTestName + ", Ctrl of CapsLock " + e.type + " event mismatch");
+ is(e.metaKey, (flags & kMetaFlag) != 0,
+ currentTestName + ", Command of CapsLock " + e.type + " event mismatch");
+ is(e.altKey, (flags & kAltFlag) != 0,
+ currentTestName + ", Alt of CapsLock " + e.type + " event mismatch");
+ is(e.shiftKey, (flags & kShiftFlag) != 0,
+ currentTestName + ", Shift of CapsLock " + e.type + " event mismatch");
+ // AltGr on Windows is always pressed after and released before CapsLock key operation.
+ is(e.getModifierState("AltGraph"), false,
+ currentTestName + ", AltGraph of CapsLock " + e.type + " event mismatch");
+ return testingEvent.modifiers.capsLockKey &&
+ removeFlag(e, kCapsLockFlag) && expectedDOMKeyCode != e.keyCode;
+ }
+ return false;
+ }
+
+ // Ignore the state changing key events which is fired by the testing event.
+ if (!isStateChangingModifierKeyEvent(e))
+ eventList.push(e);
+ }
+
+ function consumer(aEvent)
+ {
+ aEvent.preventDefault();
+ }
+
+ const SHOULD_DELIVER_KEYDOWN = 0x1;
+ const SHOULD_DELIVER_KEYPRESS = 0x2;
+ const SHOULD_DELIVER_KEYUP = 0x4;
+ const SHOULD_DELIVER_ALL = SHOULD_DELIVER_KEYDOWN |
+ SHOULD_DELIVER_KEYPRESS |
+ SHOULD_DELIVER_KEYUP;
+ const SHOULD_DELIVER_KEYDOWN_KEYUP = SHOULD_DELIVER_KEYDOWN |
+ SHOULD_DELIVER_KEYUP;
+ const SHOULD_DELIVER_KEYDOWN_KEYPRESS = SHOULD_DELIVER_KEYDOWN |
+ SHOULD_DELIVER_KEYPRESS;
+
+ // The first parameter is the complete input event. The second parameter is
+ // what to test against. The third parameter is which key events should be
+ // delived for the event.
+ // @param aExpectedKeyValues Can be string or array of string.
+ // If all keyboard events have same key value,
+ // specify it as string. Otherwise, specify
+ // each key value in array.
+ function testKey(aEvent, aExpectedKeyValues, aExpectedCodeValue,
+ aExpectedGeckoKeyCode, aExpectGeckoChar,
+ aShouldDelivedEvent, aExpectLocation)
+ {
+ ok(aExpectedGeckoKeyCode != undefined, "keycode is undefined");
+ eventList = [];
+
+ // The modifier key events which are fired for state changing are har to
+ // test. We should ignore them for now.
+ keyDownFlags = keyUpFlags = 0;
+ if (!IS_MAC) {
+ // On Mac, nsChildView doesn't generate modifier keydown/keyup events for
+ // state changing for synthesizeNativeKeyEvent.
+ if (aEvent.modifiers.shiftKey || aEvent.modifiers.shiftRightKey) {
+ keyDownFlags |= kShiftFlag;
+ }
+ if (aEvent.modifiers.ctrlKey || aEvent.modifiers.ctrlRightKey ||
+ (IS_WIN && aEvent.modifiers.altGrKey)) {
+ keyDownFlags |= kCtrlFlag;
+ }
+ if (aEvent.modifiers.altKey || aEvent.modifiers.altRightKey) {
+ keyDownFlags |= kAltFlag;
+ }
+ if (aEvent.modifiers.altGrKey) {
+ keyDownFlags |= kAltGraphFlag;
+ }
+ if (aEvent.modifiers.metaKey || aEvent.modifiers.metaRightKey) {
+ keyDownFlags |= kMetaFlag;
+ }
+ if (aEvent.modifiers.numLockKey || aEvent.modifiers.numericKeyPadKey) {
+ keyDownFlags |= kNumLockFlag;
+ }
+ if (aEvent.modifiers.capsLockKey) {
+ keyDownFlags |= kCapsLockFlag;
+ }
+ keyUpFlags = keyDownFlags;
+ }
+
+ testingEvent = aEvent;
+ expectedDOMKeyCode = aExpectedGeckoKeyCode;
+
+ currentTestName = eventToString(aEvent);
+ ok(true, "Starting: " + currentTestName);
+
+ // eslint-disable-next-line complexity
+ return synthesizeKey(aEvent, "button", function() {
+
+ var expectEventTypeList = [];
+ if (aShouldDelivedEvent & SHOULD_DELIVER_KEYDOWN)
+ expectEventTypeList.push("keydown");
+ if (aShouldDelivedEvent & SHOULD_DELIVER_KEYPRESS) {
+ expectEventTypeList.push("keypress");
+ for (let i = 1; i < aExpectGeckoChar.length; i++) {
+ expectEventTypeList.push("keypress");
+ }
+ }
+ if (aShouldDelivedEvent & SHOULD_DELIVER_KEYUP)
+ expectEventTypeList.push("keyup");
+ is(eventList.length, expectEventTypeList.length,
+ currentTestName + ", wrong number of key events");
+
+ var longerLength = Math.max(eventList.length, expectEventTypeList.length);
+ var keypressCount = 0;
+ for (let i = 0; i < longerLength; i++) {
+ var firedEventType = i < eventList.length ? eventList[i].type : "";
+ var expectEventType = i < expectEventTypeList.length ? expectEventTypeList[i] : "";
+ if (firedEventType != "") {
+ is(firedEventType, expectEventType,
+ currentTestName + ", " + expectEventType + " should be fired");
+ } else {
+ is(firedEventType, expectEventType,
+ currentTestName + ", a needed event is not fired");
+ }
+
+ if (firedEventType != "") {
+ var expectedKeyValue =
+ // eslint-disable-next-line no-nested-ternary
+ typeof aExpectedKeyValues === "string" ? aExpectedKeyValues :
+ i < aExpectedKeyValues.length ? aExpectedKeyValues[i] :
+ undefined;
+
+ var e = eventList[i];
+ switch (e.key) {
+ case "Shift":
+ case "Control":
+ case "Alt":
+ case "AltGraph":
+ case "Meta":
+ case "CapsLock":
+ case "NumLock":
+ // XXX To check modifier state of modifiers, we need to check
+ // e.type since modifier key may change modifier state.
+ // However, doing it makes the following check more
+ // complicated. So, we ignore the modifier state of
+ // modifier keydown/keyup events for now.
+ break;
+ default:
+ is(e.shiftKey, !!(aEvent.modifiers.shiftKey || aEvent.modifiers.shiftRightKey),
+ currentTestName + ", Shift of " + e.type + " of " + e.code + " mismatch");
+ is(e.metaKey, !!(aEvent.modifiers.metaKey || aEvent.modifiers.metaRightKey),
+ currentTestName + ", Meta of " + e.type + " of " + e.code + " mismatch");
+ var isControlPressed = !!(aEvent.modifiers.ctrlKey || aEvent.modifiers.ctrlRightKey);
+ var isAltPressed = !!(aEvent.modifiers.altKey || aEvent.modifiers.altRightKey);
+ var isAltGraphExpected =
+ !!aEvent.modifiers.altGrKey ||
+ (IS_WIN && aEvent.layout.hasAltGrOnWin &&
+ isControlPressed && isAltPressed &&
+ (aEvent.isInputtingCharacters || expectedKeyValue == "Dead")) ||
+ (IS_MAC && isAltPressed);
+ var isControlExpected = !(IS_WIN && isAltGraphExpected) && isControlPressed;
+ var isAltExpected = !(IS_WIN && isAltGraphExpected) && isAltPressed;
+ if (e.type == "keypress" && aEvent.isInputtingCharacters) {
+ isControlExpected = false;
+ isAltExpected = false;
+ }
+ is(e.ctrlKey, isControlExpected,
+ currentTestName + ", Ctrl of " + e.type + " of " + e.code + " mismatch");
+ is(e.altKey, isAltExpected,
+ currentTestName + ", Alt of " + e.type + " of " + e.code + " mismatch");
+ is(e.getModifierState("AltGraph"), isAltGraphExpected,
+ currentTestName + ", AltGraph of " + e.type + " of " + e.code + " mismatch");
+ break;
+ }
+
+ is(e.key, expectedKeyValue, currentTestName + ", wrong key value");
+ is(e.code, aExpectedCodeValue, currentTestName + ", wrong code value");
+
+ if (aExpectGeckoChar.length && e.type == "keypress") {
+ is(e.charCode, aExpectGeckoChar.charCodeAt(keypressCount++),
+ currentTestName + ", charcode");
+ if (aExpectedGeckoKeyCode >= 0) {
+ if (aExpectGeckoChar) {
+ is(e.keyCode, 0,
+ currentTestName + ", wrong keycode");
+ } else {
+ is(e.keyCode, aExpectedGeckoKeyCode,
+ currentTestName + ", wrong keycode");
+ }
+ }
+ } else {
+ is(e.charCode, 0,
+ currentTestName + ", no charcode");
+ if (aExpectedGeckoKeyCode >= 0) {
+ is(e.keyCode, aExpectedGeckoKeyCode,
+ currentTestName + ", wrong keycode");
+ }
+ }
+ is(e.location, aExpectLocation,
+ currentTestName + ", wrong location");
+ }
+ }
+
+ continueTest();
+ });
+ }
+
+ // These tests have to be per-plaform.
+ document.addEventListener("keydown", onKeyEvent);
+ document.addEventListener("keypress", onKeyEvent);
+ document.addEventListener("keyup", onKeyEvent);
+ // Prevent almost all shortcut key handlers.
+ SpecialPowers.addSystemEventListener(document, "keypress", consumer, true);
+
+ function cleanup()
+ {
+ document.removeEventListener("keydown", onKeyEvent);
+ document.removeEventListener("keypress", onKeyEvent);
+ document.removeEventListener("keyup", onKeyEvent);
+ SpecialPowers.removeSystemEventListener(document, "keypress", consumer, true);
+ }
+
+ function* testKeysOnMac()
+ {
+ // On Mac, you can produce event records for any desired keyboard input
+ // by running with NSPR_LOG_MODULES=TextInputHandlerWidgets:5 and typing
+ // into the browser. We will dump the key event fields to the console
+ // (Find TextInputHandler::HandleKeyDownEvent or
+ // TextInputHandler::HandleKeyUpEvent in the log). Use the International system
+ // preferences widget to enable other keyboard layouts and select them from the
+ // input menu to see what keyboard events they generate.
+ // Note that it's possible to send bogus key events here, e.g.
+ // {keyCode:0, chars:"z", unmodifiedChars:"P"} --- sendNativeKeyEvent
+ // makes no attempt to verify that the keyCode matches the characters. So only
+ // test key event records that you saw Cocoa send.
+
+ // Command keys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1}, chars:"a", unmodifiedChars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Shift-cmd gives us the shifted character
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"a", unmodifiedChars:"A"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Ctrl-cmd gives us the unshifted character
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Alt-cmd gives us the shifted character
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, altKey:1}, chars:"\u00e5", unmodifiedChars:"a"},
+ "\u00e5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00e5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, altKey:1, shiftKey:1}, chars:"\u00c5", unmodifiedChars:"a"},
+ "\u00c5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00c5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Greek ctrl keys produce Latin charcodes
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"},
+ "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"\u0391"},
+ "\u0391", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Greek command keys
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1}, chars:"a", unmodifiedChars:"\u03b1"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Shift-cmd gives us the shifted character
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"a", unmodifiedChars:"\u0391"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Ctrl-cmd gives us the unshifted character
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, ctrlKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"},
+ "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Alt-cmd gives us the shifted character
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, altKey:1}, chars:"\u00a8", unmodifiedChars:"\u03b1"},
+ "\u00a8", "KeyA", KeyboardEvent.DOM_VK_A, "\u00a8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1, altKey:1, shiftKey:1}, chars:"\u00b9", unmodifiedChars:"\u0391"},
+ "\u00b9", "KeyA", KeyboardEvent.DOM_VK_A, "\u00b9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // German
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_A,
+ modifiers: {}, chars:"a", unmodifiedChars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers: {}, chars:"\u00fc", unmodifiedChars:"\u00fc"},
+ "\u00fc", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u00fc", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus,
+ modifiers: {}, chars:"\u00df", unmodifiedChars:"\u00df"},
+ "\u00df", "Minus", KeyboardEvent.DOM_VK_QUESTION_MARK, "\u00df", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{shiftKey:1}, chars:"?", unmodifiedChars:"?"},
+ "?", "Minus", KeyboardEvent.DOM_VK_QUESTION_MARK, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Note that Shift+SS is '?' but Cmd+Shift+SS is '/' on German layout.
+ // Therefore, when Cmd key is pressed, the SS key's keycode is changed.
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{metaKey:1}, chars:"\u00df", unmodifiedChars:"\u00df"},
+ "\u00df", "Minus", KeyboardEvent.DOM_VK_SLASH, "\u00df", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"?"},
+ "/", "Minus", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Caps Lock key event
+ // XXX keyup event of Caps Lock key is not fired.
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_CapsLock,
+ modifiers:{capsLockKey:1}, chars:"", unmodifiedChars:""},
+ "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_CapsLock,
+ modifiers:{capsLockKey:0}, chars:"", unmodifiedChars:""},
+ "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Shift/RightShift key event
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Shift,
+ modifiers:{shiftKey:1}, chars:"", unmodifiedChars:""},
+ "Shift", "ShiftLeft", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Shift,
+ modifiers:{shiftKey:0}, chars:"", unmodifiedChars:""},
+ "Shift", "ShiftLeft", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightShift,
+ modifiers:{shiftRightKey:1}, chars:"", unmodifiedChars:""},
+ "Shift", "ShiftRight", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightShift,
+ modifiers:{shiftRightKey:0}, chars:"", unmodifiedChars:""},
+ "Shift", "ShiftRight", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // Control/RightControl key event
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Control,
+ modifiers:{ctrlKey:1}, chars:"", unmodifiedChars:""},
+ "Control", "ControlLeft", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Control,
+ modifiers:{ctrlKey:0}, chars:"", unmodifiedChars:""},
+ "Control", "ControlLeft", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightControl,
+ modifiers:{ctrlRightKey:1}, chars:"", unmodifiedChars:""},
+ "Control", "ControlRight", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightControl,
+ modifiers:{ctrlRightKey:0}, chars:"", unmodifiedChars:""},
+ "Control", "ControlRight", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // Option/RightOption key event
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Option,
+ modifiers:{altKey:1}, chars:"", unmodifiedChars:""},
+ "Alt", "AltLeft", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Option,
+ modifiers:{altKey:0}, chars:"", unmodifiedChars:""},
+ "Alt", "AltLeft", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightOption,
+ modifiers:{altRightKey:1}, chars:"", unmodifiedChars:""},
+ "Alt", "AltRight", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightOption,
+ modifiers:{altRightKey:0}, chars:"", unmodifiedChars:""},
+ "Alt", "AltRight", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // Command/RightCommand key event
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Command,
+ modifiers:{metaKey:1}, chars:"", unmodifiedChars:""},
+ "Meta", "MetaLeft", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Command,
+ modifiers:{metaKey:0}, chars:"", unmodifiedChars:""},
+ "Meta", "MetaLeft", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightCommand,
+ modifiers:{metaRightKey:1}, chars:"", unmodifiedChars:""},
+ "Meta", "MetaRight", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightCommand,
+ modifiers:{metaRightKey:0}, chars:"", unmodifiedChars:""},
+ "Meta", "MetaRight", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // all keys on keyboard (keyCode test)
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Tab,
+ modifiers: {}, chars:"\t", unmodifiedChars:"\t"},
+ "Tab", "Tab", KeyboardEvent.DOM_VK_TAB, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadClear,
+ modifiers: {}, chars:"\uF739", unmodifiedChars:"\uF739"},
+ "Clear", "NumLock", KeyboardEvent.DOM_VK_CLEAR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Return,
+ modifiers: {}, chars:"\u000D", unmodifiedChars:"\u000D"},
+ "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_Pause,
+ modifiers: {}, chars:"\uF712", unmodifiedChars:"\uF712"},
+ "F15", "F15", KeyboardEvent.DOM_VK_PAUSE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Escape,
+ modifiers: {}, chars:"\u001B", unmodifiedChars:"\u001B"},
+ "Escape", "Escape", KeyboardEvent.DOM_VK_ESCAPE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Space,
+ modifiers: {}, chars:" ", unmodifiedChars:" "},
+ " ", "Space", KeyboardEvent.DOM_VK_SPACE, " ", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PageUp,
+ modifiers: {}, chars:"\uF72C", unmodifiedChars:"\uF72C"},
+ "PageUp", "PageUp", KeyboardEvent.DOM_VK_PAGE_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PageDown,
+ modifiers: {}, chars:"\uF72D", unmodifiedChars:"\uF72D"},
+ "PageDown", "PageDown", KeyboardEvent.DOM_VK_PAGE_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_End,
+ modifiers: {}, chars:"\uF72B", unmodifiedChars:"\uF72B"},
+ "End", "End", KeyboardEvent.DOM_VK_END, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Home,
+ modifiers: {}, chars:"\uF729", unmodifiedChars:"\uF729"},
+ "Home", "Home", KeyboardEvent.DOM_VK_HOME, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_LeftArrow,
+ modifiers: {}, chars:"\uF702", unmodifiedChars:"\uF702"},
+ "ArrowLeft", "ArrowLeft", KeyboardEvent.DOM_VK_LEFT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_UpArrow,
+ modifiers: {}, chars:"\uF700", unmodifiedChars:"\uF700"},
+ "ArrowUp", "ArrowUp", KeyboardEvent.DOM_VK_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightArrow,
+ modifiers: {}, chars:"\uF703", unmodifiedChars:"\uF703"},
+ "ArrowRight", "ArrowRight", KeyboardEvent.DOM_VK_RIGHT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_DownArrow,
+ modifiers: {}, chars:"\uF701", unmodifiedChars:"\uF701"},
+ "ArrowDown", "ArrowDown", KeyboardEvent.DOM_VK_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_PrintScreen,
+ modifiers: {}, chars:"\uF710", unmodifiedChars:"\uF710"},
+ "F13", "F13", KeyboardEvent.DOM_VK_PRINTSCREEN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_Delete,
+ modifiers: {}, chars:"\uF728", unmodifiedChars:"\uF728"},
+ "Delete", "Delete", KeyboardEvent.DOM_VK_DELETE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_ScrollLock,
+ modifiers: {}, chars:"\uF711", unmodifiedChars:"\uF711"},
+ "F14", "F14", KeyboardEvent.DOM_VK_SCROLL_LOCK, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_ContextMenu,
+ modifiers: {}, chars:"\u0010", unmodifiedChars:"\u0010"},
+ "ContextMenu", "ContextMenu", KeyboardEvent.DOM_VK_CONTEXT_MENU, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F1,
+ modifiers:{fnKey:1}, chars:"\uF704", unmodifiedChars:"\uF704"},
+ "F1", "F1", KeyboardEvent.DOM_VK_F1, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F2,
+ modifiers:{fnKey:1}, chars:"\uF705", unmodifiedChars:"\uF705"},
+ "F2", "F2", KeyboardEvent.DOM_VK_F2, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F3,
+ modifiers:{fnKey:1}, chars:"\uF706", unmodifiedChars:"\uF706"},
+ "F3", "F3", KeyboardEvent.DOM_VK_F3, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F4,
+ modifiers:{fnKey:1}, chars:"\uF707", unmodifiedChars:"\uF707"},
+ "F4", "F4", KeyboardEvent.DOM_VK_F4, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F5,
+ modifiers:{fnKey:1}, chars:"\uF708", unmodifiedChars:"\uF708"},
+ "F5", "F5", KeyboardEvent.DOM_VK_F5, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F6,
+ modifiers:{fnKey:1}, chars:"\uF709", unmodifiedChars:"\uF709"},
+ "F6", "F6", KeyboardEvent.DOM_VK_F6, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F7,
+ modifiers:{fnKey:1}, chars:"\uF70A", unmodifiedChars:"\uF70A"},
+ "F7", "F7", KeyboardEvent.DOM_VK_F7, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F8,
+ modifiers:{fnKey:1}, chars:"\uF70B", unmodifiedChars:"\uF70B"},
+ "F8", "F8", KeyboardEvent.DOM_VK_F8, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F9,
+ modifiers:{fnKey:1}, chars:"\uF70C", unmodifiedChars:"\uF70C"},
+ "F9", "F9", KeyboardEvent.DOM_VK_F9, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F10,
+ modifiers:{fnKey:1}, chars:"\uF70D", unmodifiedChars:"\uF70D"},
+ "F10", "F10", KeyboardEvent.DOM_VK_F10, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F11,
+ modifiers:{fnKey:1}, chars:"\uF70E", unmodifiedChars:"\uF70E"},
+ "F11", "F11", KeyboardEvent.DOM_VK_F11, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F12,
+ modifiers:{fnKey:1}, chars:"\uF70F", unmodifiedChars:"\uF70F"},
+ "F12", "F12", KeyboardEvent.DOM_VK_F12, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F16,
+ modifiers:{fnKey:1}, chars:"\uF713", unmodifiedChars:"\uF713"},
+ "F16", "F16", KeyboardEvent.DOM_VK_F16, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F17,
+ modifiers:{fnKey:1}, chars:"\uF714", unmodifiedChars:"\uF714"},
+ "F17", "F17", KeyboardEvent.DOM_VK_F17, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F18,
+ modifiers:{fnKey:1}, chars:"\uF715", unmodifiedChars:"\uF715"},
+ "F18", "F18", KeyboardEvent.DOM_VK_F18, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F19,
+ modifiers:{fnKey:1}, chars:"\uF716", unmodifiedChars:"\uF716"},
+ "F19", "F19", KeyboardEvent.DOM_VK_F19, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // US
+ // Alphabet
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers: {}, chars:"a", unmodifiedChars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{shiftKey:1}, chars:"A", unmodifiedChars:"A"},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, capsLockKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, shiftKey:1, capsLockKey:1}, chars:"\u0001", unmodifiedChars:"A"},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{altKey:1}, chars:"\u00E5", unmodifiedChars:"a"},
+ "\u00E5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00E5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C5", unmodifiedChars:"A"},
+ "\u00C5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00C5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "\u00E5", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"},
+ "\u00C5", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{metaKey:1}, chars:"a", unmodifiedChars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{}, chars:"b", unmodifiedChars:"b"},
+ "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{shiftKey:1}, chars:"B", unmodifiedChars:"B"},
+ "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{ctrlKey:1}, chars:"\u0002", unmodifiedChars:"b"},
+ "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0002", unmodifiedChars:"B"},
+ "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{altKey:1}, chars:"\u222B", unmodifiedChars:"b"},
+ "\u222B", "KeyB", KeyboardEvent.DOM_VK_B, "\u222B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u0131", unmodifiedChars:"B"},
+ "\u0131", "KeyB", KeyboardEvent.DOM_VK_B, "\u0131", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0002", unmodifiedChars:"b"},
+ "\u222B", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0002", unmodifiedChars:"B"},
+ "\u0131", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B,
+ modifiers:{metaKey:1}, chars:"b", unmodifiedChars:"b"},
+ "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{}, chars:"c", unmodifiedChars:"c"},
+ "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{shiftKey:1}, chars:"C", unmodifiedChars:"C"},
+ "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{ctrlKey:1}, chars:"\u0003", unmodifiedChars:"c"},
+ "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"C"},
+ "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{altKey:1}, chars:"\u00E7", unmodifiedChars:"c"},
+ "\u00E7", "KeyC", KeyboardEvent.DOM_VK_C, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C7", unmodifiedChars:"C"},
+ "\u00C7", "KeyC", KeyboardEvent.DOM_VK_C, "\u00C7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0003", unmodifiedChars:"c"},
+ "\u00E7", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"C"},
+ "\u00C7", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C,
+ modifiers:{metaKey:1}, chars:"c", unmodifiedChars:"c"},
+ "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{}, chars:"d", unmodifiedChars:"d"},
+ "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{shiftKey:1}, chars:"D", unmodifiedChars:"D"},
+ "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{ctrlKey:1}, chars:"\u0004", unmodifiedChars:"d"},
+ "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0004", unmodifiedChars:"D"},
+ "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{altKey:1}, chars:"\u2202", unmodifiedChars:"d"},
+ "\u2202", "KeyD", KeyboardEvent.DOM_VK_D, "\u2202", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00CE", unmodifiedChars:"D"},
+ "\u00CE", "KeyD", KeyboardEvent.DOM_VK_D, "\u00CE", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0004", unmodifiedChars:"d"},
+ "\u2202", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0004", unmodifiedChars:"D"},
+ "\u00CE", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D,
+ modifiers:{metaKey:1}, chars:"d", unmodifiedChars:"d"},
+ "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{}, chars:"e", unmodifiedChars:"e"},
+ "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{shiftKey:1}, chars:"E", unmodifiedChars:"E"},
+ "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{ctrlKey:1}, chars:"\u0005", unmodifiedChars:"e"},
+ "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0005", unmodifiedChars:"E"},
+ "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{altKey:1}, chars:"", unmodifiedChars:"e"},
+ "Dead", "KeyE", KeyboardEvent.DOM_VK_E, "\u00B4", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00B4", unmodifiedChars:"E"},
+ "\u00B4", "KeyE", KeyboardEvent.DOM_VK_E, "\u00B4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0005", unmodifiedChars:"e"},
+ "Dead", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0005", unmodifiedChars:"E"},
+ "\u00B4", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E,
+ modifiers:{metaKey:1}, chars:"e", unmodifiedChars:"e"},
+ "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{}, chars:"f", unmodifiedChars:"f"},
+ "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{shiftKey:1}, chars:"F", unmodifiedChars:"F"},
+ "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{ctrlKey:1}, chars:"\u0006", unmodifiedChars:"f"},
+ "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0006", unmodifiedChars:"F"},
+ "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{altKey:1}, chars:"\u0192", unmodifiedChars:"f"},
+ "\u0192", "KeyF", KeyboardEvent.DOM_VK_F, "\u0192", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00CF", unmodifiedChars:"F"},
+ "\u00CF", "KeyF", KeyboardEvent.DOM_VK_F, "\u00CF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0006", unmodifiedChars:"f"},
+ "\u0192", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0006", unmodifiedChars:"F"},
+ "\u00CF", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // XXX This test starts fullscreen mode.
+ // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ // modifiers:{metaKey:1}, chars:"f", unmodifiedChars:"f"},
+ // "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{}, chars:"g", unmodifiedChars:"g"},
+ "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{shiftKey:1}, chars:"G", unmodifiedChars:"G"},
+ "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{ctrlKey:1}, chars:"\u0007", unmodifiedChars:"g"},
+ "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0007", unmodifiedChars:"G"},
+ "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{altKey:1}, chars:"\u00A9", unmodifiedChars:"g"},
+ "\u00A9", "KeyG", KeyboardEvent.DOM_VK_G, "\u00A9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u02DD", unmodifiedChars:"G"},
+ "\u02DD", "KeyG", KeyboardEvent.DOM_VK_G, "\u02DD", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0007", unmodifiedChars:"g"},
+ "\u00A9", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0007", unmodifiedChars:"G"},
+ "\u02DD", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G,
+ modifiers:{metaKey:1}, chars:"g", unmodifiedChars:"g"},
+ "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{}, chars:"h", unmodifiedChars:"h"},
+ "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{shiftKey:1}, chars:"H", unmodifiedChars:"H"},
+ "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{ctrlKey:1}, chars:"\u0008", unmodifiedChars:"h"},
+ "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0008", unmodifiedChars:"H"},
+ "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{altKey:1}, chars:"\u02D9", unmodifiedChars:"h"},
+ "\u02D9", "KeyH", KeyboardEvent.DOM_VK_H, "\u02D9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00D3", unmodifiedChars:"H"},
+ "\u00D3", "KeyH", KeyboardEvent.DOM_VK_H, "\u00D3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0008", unmodifiedChars:"h"},
+ "\u02D9", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0008", unmodifiedChars:"H"},
+ "\u00D3", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H,
+ modifiers:{metaKey:1}, chars:"h", unmodifiedChars:"h"},
+ "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{}, chars:"i", unmodifiedChars:"i"},
+ "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{shiftKey:1}, chars:"I", unmodifiedChars:"I"},
+ "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{ctrlKey:1}, chars:"\u0009", unmodifiedChars:"i"},
+ "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0009", unmodifiedChars:"I"},
+ "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{altKey:1}, chars:"", unmodifiedChars:"i"},
+ "Dead", "KeyI", KeyboardEvent.DOM_VK_I, "\u02C6", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u02C6", unmodifiedChars:"I"},
+ "\u02C6", "KeyI", KeyboardEvent.DOM_VK_I, "\u02C6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0009", unmodifiedChars:"i"},
+ "Dead", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0009", unmodifiedChars:"I"},
+ "\u02C6", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // XXX This test causes memory leak.
+ // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I,
+ // modifiers:{metaKey:1}, chars:"i", unmodifiedChars:"i"},
+ // "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{}, chars:"j", unmodifiedChars:"j"},
+ "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{shiftKey:1}, chars:"J", unmodifiedChars:"J"},
+ "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{ctrlKey:1}, chars:"\u000A", unmodifiedChars:"j"},
+ "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000A", unmodifiedChars:"J"},
+ "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{altKey:1}, chars:"\u2206", unmodifiedChars:"j"},
+ "\u2206", "KeyJ", KeyboardEvent.DOM_VK_J, "\u2206", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00D4", unmodifiedChars:"J"},
+ "\u00D4", "KeyJ", KeyboardEvent.DOM_VK_J, "\u00D4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u000A", unmodifiedChars:"j"},
+ "\u2206", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000A", unmodifiedChars:"J"},
+ "\u00D4", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J,
+ modifiers:{metaKey:1}, chars:"j", unmodifiedChars:"j"},
+ "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{}, chars:"k", unmodifiedChars:"k"},
+ "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{shiftKey:1}, chars:"K", unmodifiedChars:"K"},
+ "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{ctrlKey:1}, chars:"\u000B", unmodifiedChars:"k"},
+ "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000B", unmodifiedChars:"K"},
+ "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{altKey:1}, chars:"\u02DA", unmodifiedChars:"k"},
+ "\u02DA", "KeyK", KeyboardEvent.DOM_VK_K, "\u02DA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\uF8FF", unmodifiedChars:"K"},
+ "\uF8FF", "KeyK", KeyboardEvent.DOM_VK_K, "\uF8FF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u000B", unmodifiedChars:"k"},
+ "\u02DA", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000B", unmodifiedChars:"K"},
+ "\uF8FF", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K,
+ modifiers:{metaKey:1}, chars:"k", unmodifiedChars:"k"},
+ "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{}, chars:"l", unmodifiedChars:"l"},
+ "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{shiftKey:1}, chars:"L", unmodifiedChars:"L"},
+ "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{ctrlKey:1}, chars:"\u000C", unmodifiedChars:"l"},
+ "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000C", unmodifiedChars:"L"},
+ "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{altKey:1}, chars:"\u00AC", unmodifiedChars:"l"},
+ "\u00AC", "KeyL", KeyboardEvent.DOM_VK_L, "\u00AC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00D2", unmodifiedChars:"L"},
+ "\u00D2", "KeyL", KeyboardEvent.DOM_VK_L, "\u00D2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u000C", unmodifiedChars:"l"},
+ "\u00AC", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000C", unmodifiedChars:"L"},
+ "\u00D2", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L,
+ modifiers:{metaKey:1}, chars:"l", unmodifiedChars:"l"},
+ "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{}, chars:"m", unmodifiedChars:"m"},
+ "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{shiftKey:1}, chars:"M", unmodifiedChars:"M"},
+ "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{ctrlKey:1}, chars:"\u000D", unmodifiedChars:"m"},
+ "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000D", unmodifiedChars:"M"},
+ "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{altKey:1}, chars:"\u00B5", unmodifiedChars:"m"},
+ "\u00B5", "KeyM", KeyboardEvent.DOM_VK_M, "\u00B5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C2", unmodifiedChars:"M"},
+ "\u00C2", "KeyM", KeyboardEvent.DOM_VK_M, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u000D", unmodifiedChars:"m"},
+ "\u00B5", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000D", unmodifiedChars:"M"},
+ "\u00C2", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M,
+ modifiers:{metaKey:1}, chars:"m", unmodifiedChars:"m"},
+ "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{}, chars:"n", unmodifiedChars:"n"},
+ "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{shiftKey:1}, chars:"N", unmodifiedChars:"N"},
+ "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{ctrlKey:1}, chars:"\u000E", unmodifiedChars:"n"},
+ "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000E", unmodifiedChars:"N"},
+ "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{altKey:1}, chars:"", unmodifiedChars:"n"},
+ "Dead", "KeyN", KeyboardEvent.DOM_VK_N, "\u02DC", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u02DC", unmodifiedChars:"N"},
+ "\u02DC", "KeyN", KeyboardEvent.DOM_VK_N, "\u02DC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u000E", unmodifiedChars:"n"},
+ "Dead", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000E", unmodifiedChars:"N"},
+ "\u02DC", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N,
+ modifiers:{metaKey:1}, chars:"n", unmodifiedChars:"n"},
+ "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{}, chars:"o", unmodifiedChars:"o"},
+ "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{shiftKey:1}, chars:"O", unmodifiedChars:"O"},
+ "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{ctrlKey:1}, chars:"\u000F", unmodifiedChars:"o"},
+ "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000F", unmodifiedChars:"O"},
+ "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{altKey:1}, chars:"\u00F8", unmodifiedChars:"o"},
+ "\u00F8", "KeyO", KeyboardEvent.DOM_VK_O, "\u00F8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00D8", unmodifiedChars:"O"},
+ "\u00D8", "KeyO", KeyboardEvent.DOM_VK_O, "\u00D8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u000F", unmodifiedChars:"o"},
+ "\u00F8", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000F", unmodifiedChars:"O"},
+ "\u00D8", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O,
+ modifiers:{metaKey:1}, chars:"o", unmodifiedChars:"o"},
+ "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{}, chars:"p", unmodifiedChars:"p"},
+ "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{shiftKey:1}, chars:"P", unmodifiedChars:"P"},
+ "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{ctrlKey:1}, chars:"\u0010", unmodifiedChars:"p"},
+ "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0010", unmodifiedChars:"P"},
+ "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{altKey:1}, chars:"\u03C0", unmodifiedChars:"p"},
+ "\u03C0", "KeyP", KeyboardEvent.DOM_VK_P, "\u03C0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u220F", unmodifiedChars:"P"},
+ "\u220F", "KeyP", KeyboardEvent.DOM_VK_P, "\u220F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0010", unmodifiedChars:"p"},
+ "\u03C0", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0010", unmodifiedChars:"P"},
+ "\u220F", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // XXX This test starts private browsing mode (stopped at the confirmation dialog)
+ // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P,
+ // modifiers:{metaKey:1}, chars:"p", unmodifiedChars:"p"},
+ // "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{}, chars:"q", unmodifiedChars:"q"},
+ "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{shiftKey:1}, chars:"Q", unmodifiedChars:"Q"},
+ "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{ctrlKey:1}, chars:"\u0011", unmodifiedChars:"q"},
+ "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0011", unmodifiedChars:"Q"},
+ "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{altKey:1}, chars:"\u0153", unmodifiedChars:"q"},
+ "\u0153", "KeyQ", KeyboardEvent.DOM_VK_Q, "\u0153", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u0152", unmodifiedChars:"Q"},
+ "\u0152", "KeyQ", KeyboardEvent.DOM_VK_Q, "\u0152", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0011", unmodifiedChars:"q"},
+ "\u0153", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0011", unmodifiedChars:"Q"},
+ "\u0152", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q,
+ modifiers:{metaKey:1}, chars:"q", unmodifiedChars:"q"},
+ "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{}, chars:"r", unmodifiedChars:"r"},
+ "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{shiftKey:1}, chars:"R", unmodifiedChars:"R"},
+ "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{ctrlKey:1}, chars:"\u0012", unmodifiedChars:"r"},
+ "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0012", unmodifiedChars:"R"},
+ "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{altKey:1}, chars:"\u00AE", unmodifiedChars:"r"},
+ "\u00AE", "KeyR", KeyboardEvent.DOM_VK_R, "\u00AE", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u2030", unmodifiedChars:"R"},
+ "\u2030", "KeyR", KeyboardEvent.DOM_VK_R, "\u2030", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0012", unmodifiedChars:"r"},
+ "\u00AE", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0012", unmodifiedChars:"R"},
+ "\u2030", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // XXX This test makes some tabs and dialogs.
+ // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R,
+ // modifiers:{metaKey:1}, chars:"r", unmodifiedChars:"r"},
+ // "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{}, chars:"s", unmodifiedChars:"s"},
+ "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{shiftKey:1}, chars:"S", unmodifiedChars:"S"},
+ "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{ctrlKey:1}, chars:"\u0013", unmodifiedChars:"s"},
+ "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0013", unmodifiedChars:"S"},
+ "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{altKey:1}, chars:"\u00DF", unmodifiedChars:"s"},
+ "\u00DF", "KeyS", KeyboardEvent.DOM_VK_S, "\u00DF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00CD", unmodifiedChars:"S"},
+ "\u00CD", "KeyS", KeyboardEvent.DOM_VK_S, "\u00CD", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0013", unmodifiedChars:"s"},
+ "\u00DF", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0013", unmodifiedChars:"S"},
+ "\u00CD", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S,
+ modifiers:{metaKey:1}, chars:"s", unmodifiedChars:"s"},
+ "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{}, chars:"t", unmodifiedChars:"t"},
+ "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{shiftKey:1}, chars:"T", unmodifiedChars:"T"},
+ "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{ctrlKey:1}, chars:"\u0014", unmodifiedChars:"t"},
+ "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0014", unmodifiedChars:"T"},
+ "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{altKey:1}, chars:"\u2020", unmodifiedChars:"t"},
+ "\u2020", "KeyT", KeyboardEvent.DOM_VK_T, "\u2020", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u02C7", unmodifiedChars:"T"},
+ "\u02C7", "KeyT", KeyboardEvent.DOM_VK_T, "\u02C7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0014", unmodifiedChars:"t"},
+ "\u2020", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0014", unmodifiedChars:"T"},
+ "\u02C7", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{metaKey:1}, chars:"t", unmodifiedChars:"t"},
+ "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{}, chars:"u", unmodifiedChars:"u"},
+ "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{shiftKey:1}, chars:"U", unmodifiedChars:"U"},
+ "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{ctrlKey:1}, chars:"\u0015", unmodifiedChars:"u"},
+ "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0015", unmodifiedChars:"U"},
+ "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{altKey:1}, chars:"", unmodifiedChars:"u"},
+ "Dead", "KeyU", KeyboardEvent.DOM_VK_U, "\u00A8", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00A8", unmodifiedChars:"U"},
+ "\u00A8", "KeyU", KeyboardEvent.DOM_VK_U, "\u00A8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0015", unmodifiedChars:"u"},
+ "Dead", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0015", unmodifiedChars:"U"},
+ "\u00A8", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U,
+ modifiers:{metaKey:1}, chars:"u", unmodifiedChars:"u"},
+ "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{}, chars:"v", unmodifiedChars:"v"},
+ "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{shiftKey:1}, chars:"V", unmodifiedChars:"V"},
+ "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{ctrlKey:1}, chars:"\u0016", unmodifiedChars:"v"},
+ "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0016", unmodifiedChars:"V"},
+ "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{altKey:1}, chars:"\u221A", unmodifiedChars:"v"},
+ "\u221A", "KeyV", KeyboardEvent.DOM_VK_V, "\u221A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u25CA", unmodifiedChars:"V"},
+ "\u25CA", "KeyV", KeyboardEvent.DOM_VK_V, "\u25CA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0016", unmodifiedChars:"v"},
+ "\u221A", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0016", unmodifiedChars:"V"},
+ "\u25CA", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V,
+ modifiers:{metaKey:1}, chars:"v", unmodifiedChars:"v"},
+ "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{}, chars:"w", unmodifiedChars:"w"},
+ "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{shiftKey:1}, chars:"W", unmodifiedChars:"W"},
+ "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{ctrlKey:1}, chars:"\u0017", unmodifiedChars:"w"},
+ "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0017", unmodifiedChars:"W"},
+ "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{altKey:1}, chars:"\u2211", unmodifiedChars:"w"},
+ "\u2211", "KeyW", KeyboardEvent.DOM_VK_W, "\u2211", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u201E", unmodifiedChars:"W"},
+ "\u201E", "KeyW", KeyboardEvent.DOM_VK_W, "\u201E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0017", unmodifiedChars:"w"},
+ "\u2211", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0017", unmodifiedChars:"W"},
+ "\u201E", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W,
+ modifiers:{metaKey:1}, chars:"w", unmodifiedChars:"w"},
+ "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{}, chars:"x", unmodifiedChars:"x"},
+ "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{shiftKey:1}, chars:"X", unmodifiedChars:"X"},
+ "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{ctrlKey:1}, chars:"\u0018", unmodifiedChars:"x"},
+ "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018", unmodifiedChars:"X"},
+ "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{altKey:1}, chars:"\u2248", unmodifiedChars:"x"},
+ "\u2248", "KeyX", KeyboardEvent.DOM_VK_X, "\u2248", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u02DB", unmodifiedChars:"X"},
+ "\u02DB", "KeyX", KeyboardEvent.DOM_VK_X, "\u02DB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0018", unmodifiedChars:"x"},
+ "\u2248", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0018", unmodifiedChars:"X"},
+ "\u02DB", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X,
+ modifiers:{metaKey:1}, chars:"x", unmodifiedChars:"x"},
+ "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{}, chars:"y", unmodifiedChars:"y"},
+ "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{shiftKey:1}, chars:"Y", unmodifiedChars:"Y"},
+ "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{ctrlKey:1}, chars:"\u0019", unmodifiedChars:"y"},
+ "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0019", unmodifiedChars:"Y"},
+ "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{altKey:1}, chars:"\u00A5", unmodifiedChars:"y"},
+ "\u00A5", "KeyY", KeyboardEvent.DOM_VK_Y, "\u00A5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C1", unmodifiedChars:"Y"},
+ "\u00C1", "KeyY", KeyboardEvent.DOM_VK_Y, "\u00C1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u0019", unmodifiedChars:"y"},
+ "\u00A5", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0019", unmodifiedChars:"Y"},
+ "\u00C1", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y,
+ modifiers:{metaKey:1}, chars:"y", unmodifiedChars:"y"},
+ "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{}, chars:"z", unmodifiedChars:"z"},
+ "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{shiftKey:1}, chars:"Z", unmodifiedChars:"Z"},
+ "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{ctrlKey:1}, chars:"\u001A", unmodifiedChars:"z"},
+ "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001A", unmodifiedChars:"Z"},
+ "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{altKey:1}, chars:"\u03A9", unmodifiedChars:"z"},
+ "\u03A9", "KeyZ", KeyboardEvent.DOM_VK_Z, "\u03A9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00B8", unmodifiedChars:"Z"},
+ "\u00B8", "KeyZ", KeyboardEvent.DOM_VK_Z, "\u00B8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u001A", unmodifiedChars:"z"},
+ "\u03A9", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001A", unmodifiedChars:"Z"},
+ "\u00B8", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z,
+ modifiers:{metaKey:1}, chars:"z", unmodifiedChars:"z"},
+ "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // numeric
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{}, chars:"1", unmodifiedChars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{shiftKey:1}, chars:"!", unmodifiedChars:"!"},
+ "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{ctrlKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"!"},
+ "!", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1}, chars:"\u00A1", unmodifiedChars:"1"},
+ "\u00A1", "Digit1", KeyboardEvent.DOM_VK_1, "\u00A1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u2044", unmodifiedChars:"!"},
+ "\u2044", "Digit1", KeyboardEvent.DOM_VK_1, "\u2044", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"1"},
+ "\u00A1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"!"},
+ "\u2044", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1,
+ modifiers:{metaKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{}, chars:"2", unmodifiedChars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{shiftKey:1}, chars:"@", unmodifiedChars:"@"},
+ "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{ctrlKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0000", unmodifiedChars:"@"},
+ "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1}, chars:"\u2122", unmodifiedChars:"2"},
+ "\u2122", "Digit2", KeyboardEvent.DOM_VK_2, "\u2122", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u20AC", unmodifiedChars:"@"},
+ "\u20AC", "Digit2", KeyboardEvent.DOM_VK_2, "\u20AC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"2"},
+ "\u2122", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0000", unmodifiedChars:"@"},
+ "\u20AC", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2,
+ modifiers:{metaKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{}, chars:"3", unmodifiedChars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{shiftKey:1}, chars:"#", unmodifiedChars:"#"},
+ "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{ctrlKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"#"},
+ "#", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1}, chars:"\u00A3", unmodifiedChars:"3"},
+ "\u00A3", "Digit3", KeyboardEvent.DOM_VK_3, "\u00A3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u2039", unmodifiedChars:"#"},
+ "\u2039", "Digit3", KeyboardEvent.DOM_VK_3, "\u2039", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"3"},
+ "\u00A3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"#"},
+ "\u2039", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3,
+ modifiers:{metaKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{}, chars:"4", unmodifiedChars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{shiftKey:1}, chars:"$", unmodifiedChars:"$"},
+ "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{ctrlKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"$"},
+ "$", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1}, chars:"\u00A2", unmodifiedChars:"4"},
+ "\u00A2", "Digit4", KeyboardEvent.DOM_VK_4, "\u00A2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u203A", unmodifiedChars:"$"},
+ "\u203A", "Digit4", KeyboardEvent.DOM_VK_4, "\u203A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"4"},
+ "\u00A2", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"$"},
+ "\u203A", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4,
+ modifiers:{metaKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{}, chars:"5", unmodifiedChars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{shiftKey:1}, chars:"%", unmodifiedChars:"%"},
+ "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{ctrlKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"%"},
+ "%", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1}, chars:"\u221E", unmodifiedChars:"5"},
+ "\u221E", "Digit5", KeyboardEvent.DOM_VK_5, "\u221E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\uFB01", unmodifiedChars:"%"},
+ "\uFB01", "Digit5", KeyboardEvent.DOM_VK_5, "\uFB01", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"5"},
+ "\u221E", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"%"},
+ "\uFB01", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5,
+ modifiers:{metaKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{}, chars:"6", unmodifiedChars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{shiftKey:1}, chars:"^", unmodifiedChars:"^"},
+ "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{ctrlKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001E", unmodifiedChars:"^"},
+ "^", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1}, chars:"\u00A7", unmodifiedChars:"6"},
+ "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\uFB02", unmodifiedChars:"^"},
+ "\uFB02", "Digit6", KeyboardEvent.DOM_VK_6, "\uFB02", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"6", unmodifiedChars:"6"},
+ "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001E", unmodifiedChars:"^"},
+ "\uFB02", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6,
+ modifiers:{metaKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{}, chars:"7", unmodifiedChars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{shiftKey:1}, chars:"&", unmodifiedChars:"&"},
+ "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{ctrlKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"&"},
+ "&", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1}, chars:"\u00B6", unmodifiedChars:"7"},
+ "\u00B6", "Digit7", KeyboardEvent.DOM_VK_7, "\u00B6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u2021", unmodifiedChars:"&"},
+ "\u2021", "Digit7", KeyboardEvent.DOM_VK_7, "\u2021", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"7"},
+ "\u00B6", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"&"},
+ "\u2021", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7,
+ modifiers:{metaKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{}, chars:"8", unmodifiedChars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{shiftKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{ctrlKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"*"},
+ "*", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1}, chars:"\u2022", unmodifiedChars:"8"},
+ "\u2022", "Digit8", KeyboardEvent.DOM_VK_8, "\u2022", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00B0", unmodifiedChars:"*"},
+ "\u00B0", "Digit8", KeyboardEvent.DOM_VK_8, "\u00B0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"8"},
+ "\u2022", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"*"},
+ "\u00B0", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8,
+ modifiers:{metaKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{}, chars:"9", unmodifiedChars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{shiftKey:1}, chars:"(", unmodifiedChars:"("},
+ "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{ctrlKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"("},
+ "(", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1}, chars:"\u00AA", unmodifiedChars:"9"},
+ "\u00AA", "Digit9", KeyboardEvent.DOM_VK_9, "\u00AA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00B7", unmodifiedChars:"("},
+ "\u00B7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00B7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"9", unmodifiedChars:"9"},
+ "\u00AA", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"("},
+ "\u00B7", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9,
+ modifiers:{metaKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{}, chars:"0", unmodifiedChars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{shiftKey:1}, chars:")", unmodifiedChars:")"},
+ ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{ctrlKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:")"},
+ ")", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1}, chars:"\u00BA", unmodifiedChars:"0"},
+ "\u00BA", "Digit0", KeyboardEvent.DOM_VK_0, "\u00BA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u201A", unmodifiedChars:")"},
+ "\u201A", "Digit0", KeyboardEvent.DOM_VK_0, "\u201A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"0", unmodifiedChars:"0"},
+ "\u00BA", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:")"},
+ "\u201A", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0,
+ modifiers:{metaKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // other chracters
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{}, chars:"`", unmodifiedChars:"`"},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{shiftKey:1}, chars:"~", unmodifiedChars:"~"},
+ "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{ctrlKey:1}, chars:"`", unmodifiedChars:"`"},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"`", unmodifiedChars:"~"},
+ "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{altKey:1}, chars:"", unmodifiedChars:"`"},
+ "Dead", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{altKey:1, shiftKey:1}, chars:"`", unmodifiedChars:"~"},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"`", unmodifiedChars:"`"},
+ "Dead", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"`", unmodifiedChars:"~"},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave,
+ modifiers:{metaKey:1}, chars:"`", unmodifiedChars:"`"},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{}, chars:"-", unmodifiedChars:"-"},
+ "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{shiftKey:1}, chars:"_", unmodifiedChars:"_"},
+ "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{ctrlKey:1}, chars:"\u001F", unmodifiedChars:"-"},
+ "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001F", unmodifiedChars:"_"},
+ "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{altKey:1}, chars:"\u2013", unmodifiedChars:"-"},
+ "\u2013", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u2013", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u2014", unmodifiedChars:"_"},
+ "\u2014", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u2014", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u001F", unmodifiedChars:"-"},
+ "\u2013", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001F", unmodifiedChars:"_"},
+ "\u2014", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{metaKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{}, chars:"=", unmodifiedChars:"="},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{shiftKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{ctrlKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"+"},
+ "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{altKey:1}, chars:"\u2260", unmodifiedChars:"="},
+ "\u2260", "Equal", KeyboardEvent.DOM_VK_EQUALS, "\u2260", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00B1", unmodifiedChars:"+"},
+ "\u00B1", "Equal", KeyboardEvent.DOM_VK_EQUALS, "\u00B1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"=", unmodifiedChars:"="},
+ "\u2260", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"+"},
+ "\u00B1", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal,
+ modifiers:{metaKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{}, chars:"[", unmodifiedChars:"["},
+ "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{shiftKey:1}, chars:"{", unmodifiedChars:"{"},
+ "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{ctrlKey:1}, chars:"\u001B", unmodifiedChars:"["},
+ "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001B", unmodifiedChars:"{"},
+ "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{altKey:1}, chars:"\u201C", unmodifiedChars:"["},
+ "\u201C", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u201C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u201D", unmodifiedChars:"{"},
+ "\u201D", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u201D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u001B", unmodifiedChars:"["},
+ "\u201C", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001B", unmodifiedChars:"{"},
+ "\u201D", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{metaKey:1}, chars:"[", unmodifiedChars:"["},
+ "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{}, chars:"]", unmodifiedChars:"]"},
+ "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{shiftKey:1}, chars:"}", unmodifiedChars:"}"},
+ "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{ctrlKey:1}, chars:"\u001D", unmodifiedChars:"]"},
+ "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"}"},
+ "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{altKey:1}, chars:"\u2018", unmodifiedChars:"]"},
+ "\u2018", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u2018", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u2019", unmodifiedChars:"}"},
+ "\u2019", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u2019", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u001D", unmodifiedChars:"]"},
+ "\u2018", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"}"},
+ "\u2019", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket,
+ modifiers:{metaKey:1}, chars:"]", unmodifiedChars:"]"},
+ "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{}, chars:"\\", unmodifiedChars:"\\"},
+ "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{shiftKey:1}, chars:"|", unmodifiedChars:"|"},
+ "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\\"},
+ "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"|"},
+ "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{altKey:1}, chars:"\u00AB", unmodifiedChars:"\\"},
+ "\u00AB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u00AB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00BB", unmodifiedChars:"|"},
+ "\u00BB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u00BB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\\"},
+ "\u00AB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"|"},
+ "\u00BB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash,
+ modifiers:{metaKey:1}, chars:"\\", unmodifiedChars:"\\"},
+ "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{}, chars:";", unmodifiedChars:";"},
+ ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{shiftKey:1}, chars:":", unmodifiedChars:":"},
+ ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{ctrlKey:1}, chars:";", unmodifiedChars:";"},
+ ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:";", unmodifiedChars:":"},
+ ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{altKey:1}, chars:"\u2026", unmodifiedChars:";"},
+ "\u2026", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, "\u2026", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00DA", unmodifiedChars:":"},
+ "\u00DA", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, "\u00DA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{altKey:1, ctrlKey:1}, chars:";", unmodifiedChars:";"},
+ "\u2026", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:";", unmodifiedChars:":"},
+ "\u00DA", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{metaKey:1}, chars:";", unmodifiedChars:";"},
+ ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{}, chars:"'", unmodifiedChars:"'"},
+ "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{shiftKey:1}, chars:"\"", unmodifiedChars:"\""},
+ "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{ctrlKey:1}, chars:"'", unmodifiedChars:"'"},
+ "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"'", unmodifiedChars:"\""},
+ "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{altKey:1}, chars:"\u00E6", unmodifiedChars:"'"},
+ "\u00E6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00E6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C6", unmodifiedChars:"\""},
+ "\u00C6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00C6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"'", unmodifiedChars:"'"},
+ "\u00E6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"'", unmodifiedChars:"\""},
+ "\u00C6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{metaKey:1}, chars:"'", unmodifiedChars:"'"},
+ "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{}, chars:",", unmodifiedChars:","},
+ ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{shiftKey:1}, chars:"<", unmodifiedChars:"<"},
+ "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{ctrlKey:1}, chars:",", unmodifiedChars:","},
+ ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:"<"},
+ "<", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{altKey:1}, chars:"\u2264", unmodifiedChars:","},
+ "\u2264", "Comma", KeyboardEvent.DOM_VK_COMMA, "\u2264", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00AF", unmodifiedChars:"<"},
+ "\u00AF", "Comma", KeyboardEvent.DOM_VK_COMMA, "\u00AF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{altKey:1, ctrlKey:1}, chars:",", unmodifiedChars:","},
+ "\u2264", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:"<"},
+ "\u00AF", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma,
+ modifiers:{metaKey:1}, chars:",", unmodifiedChars:","},
+ ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{}, chars:".", unmodifiedChars:"."},
+ ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{shiftKey:1}, chars:">", unmodifiedChars:">"},
+ ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{ctrlKey:1}, chars:".", unmodifiedChars:"."},
+ ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:".", unmodifiedChars:">"},
+ ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{altKey:1}, chars:"\u2265", unmodifiedChars:"."},
+ "\u2265", "Period", KeyboardEvent.DOM_VK_PERIOD, "\u2265", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u02D8", unmodifiedChars:">"},
+ "\u02D8", "Period", KeyboardEvent.DOM_VK_PERIOD, "\u02D8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{altKey:1, ctrlKey:1}, chars:".", unmodifiedChars:"."},
+ "\u2265", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:".", unmodifiedChars:">"},
+ "\u02D8", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{metaKey:1}, chars:".", unmodifiedChars:"."},
+ ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{}, chars:"/", unmodifiedChars:"/"},
+ "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{shiftKey:1}, chars:"?", unmodifiedChars:"?"},
+ "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{ctrlKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"?"},
+ "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{altKey:1}, chars:"\u00F7", unmodifiedChars:"/"},
+ "\u00F7", "Slash", KeyboardEvent.DOM_VK_SLASH, "\u00F7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00BF", unmodifiedChars:"?"},
+ "\u00BF", "Slash", KeyboardEvent.DOM_VK_SLASH, "\u00BF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"/", unmodifiedChars:"/"},
+ "\u00F7", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"?"},
+ "\u00BF", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash,
+ modifiers:{metaKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // numpad
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"=", unmodifiedChars:"="},
+ "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"/", unmodifiedChars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"*", unmodifiedChars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"-", unmodifiedChars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"+", unmodifiedChars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, altKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma,
+ modifiers:{numericKeyPadKey:1, metaKey:1}, chars:",", unmodifiedChars:","},
+ ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+
+ // French, numeric
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{}, chars:"&", unmodifiedChars:"&"},
+ "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{ctrlKey:1}, chars:"1", unmodifiedChars:"&"},
+ "&", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1}, chars:"\uF8FF", unmodifiedChars:"&"},
+ "\uF8FF", "Digit1", KeyboardEvent.DOM_VK_1, "\uF8FF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:"", unmodifiedChars:"1"},
+ "Dead", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"&"},
+ "\uF8FF", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "Dead", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{metaKey:1}, chars:"&", unmodifiedChars:"&"},
+ "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{}, chars:"\u00E9", unmodifiedChars:"\u00E9"},
+ "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{ctrlKey:1}, chars:"2", unmodifiedChars:"\u00E9"},
+ "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1}, chars:"\u00EB", unmodifiedChars:"\u00E9"},
+ "\u00EB", "Digit2", KeyboardEvent.DOM_VK_2, "\u00EB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u201E", unmodifiedChars:"2"},
+ "\u201E", "Digit2", KeyboardEvent.DOM_VK_2, "\u201E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"\u00E9"},
+ "\u00EB", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "\u201E", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{metaKey:1}, chars:"\u00E9", unmodifiedChars:"\u00E9"},
+ "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{}, chars:"\"", unmodifiedChars:"\""},
+ "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{shiftKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{ctrlKey:1}, chars:"3", unmodifiedChars:"\""},
+ "\"", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1}, chars:"\u201C", unmodifiedChars:"\""},
+ "\u201C", "Digit3", KeyboardEvent.DOM_VK_3, "\u201C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u201D", unmodifiedChars:"3"},
+ "\u201D", "Digit3", KeyboardEvent.DOM_VK_3, "\u201D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"\""},
+ "\u201C", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"},
+ "\u201D", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ modifiers:{metaKey:1}, chars:"\"", unmodifiedChars:"\""},
+ "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Cmd+Shift+3 is a shortcut key of taking a snapshot
+ // yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3,
+ // modifiers:{metaKey:1, shiftKey:1}, chars:"\"", unmodifiedChars:"\""},
+ // "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{}, chars:"'", unmodifiedChars:"'"},
+ "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{ctrlKey:1}, chars:"4", unmodifiedChars:"'"},
+ "'", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1}, chars:"\u2018", unmodifiedChars:"'"},
+ "\u2018", "Digit4", KeyboardEvent.DOM_VK_4, "\u2018", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u2019", unmodifiedChars:"4"},
+ "\u2019", "Digit4", KeyboardEvent.DOM_VK_4, "\u2019", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"'"},
+ "\u2018", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ "\u2019", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ modifiers:{metaKey:1}, chars:"'", unmodifiedChars:"'"},
+ "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Cmd+Shift+4 is a shortcut key of taking a snapshot in specific range
+ // yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4,
+ // modifiers:{metaKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"},
+ // "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{}, chars:"(", unmodifiedChars:"("},
+ "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{ctrlKey:1}, chars:"5", unmodifiedChars:"("},
+ "(", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1}, chars:"{", unmodifiedChars:"("},
+ "{", "Digit5", KeyboardEvent.DOM_VK_5, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1, shiftKey:1}, chars:"[", unmodifiedChars:"5"},
+ "[", "Digit5", KeyboardEvent.DOM_VK_5, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"("},
+ "{", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "[", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{metaKey:1}, chars:"(", unmodifiedChars:"("},
+ "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{}, chars:"\u00A7", unmodifiedChars:"\u00A7"},
+ "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{shiftKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{ctrlKey:1}, chars:"\u001D", unmodifiedChars:"\u00A7"},
+ "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1}, chars:"\u00B6", unmodifiedChars:"\u00A7"},
+ "\u00B6", "Digit6", KeyboardEvent.DOM_VK_6, "\u00B6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00E5", unmodifiedChars:"6"},
+ "\u00E5", "Digit6", KeyboardEvent.DOM_VK_6, "\u00E5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u001D", unmodifiedChars:"\u00A7"},
+ "\u00B6", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"6"},
+ "\u00E5", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{metaKey:1}, chars:"\u00A7", unmodifiedChars:"\u00A7"},
+ "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u00A7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{}, chars:"\u00E8", unmodifiedChars:"\u00E8"},
+ "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{ctrlKey:1}, chars:"7", unmodifiedChars:"\u00E8"},
+ "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1}, chars:"\u00AB", unmodifiedChars:"\u00E8"},
+ "\u00AB", "Digit7", KeyboardEvent.DOM_VK_7, "\u00AB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00BB", unmodifiedChars:"7"},
+ "\u00BB", "Digit7", KeyboardEvent.DOM_VK_7, "\u00BB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"\u00E8"},
+ "\u00AB", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "\u00BB", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{metaKey:1}, chars:"\u00E8", unmodifiedChars:"\u00E8"},
+ "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{}, chars:"!", unmodifiedChars:"!"},
+ "!", "Digit8", KeyboardEvent.DOM_VK_8, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{ctrlKey:1}, chars:"8", unmodifiedChars:"!"},
+ "!", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1}, chars:"\u00A1", unmodifiedChars:"!"},
+ "\u00A1", "Digit8", KeyboardEvent.DOM_VK_8, "\u00A1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00DB", unmodifiedChars:"8"},
+ "\u00DB", "Digit8", KeyboardEvent.DOM_VK_8, "\u00DB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"!"},
+ "\u00A1", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "\u00DB", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{metaKey:1}, chars:"!", unmodifiedChars:"!"},
+ "!", "Digit8", KeyboardEvent.DOM_VK_8, "!", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{}, chars:"\u00E7", unmodifiedChars:"\u00E7"},
+ "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{shiftKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\u00E7"},
+ "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1}, chars:"\u00C7", unmodifiedChars:"\u00E7"},
+ "\u00C7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00C7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C1", unmodifiedChars:"9"},
+ "\u00C1", "Digit9", KeyboardEvent.DOM_VK_9, "\u00C1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\u00E7"},
+ "\u00C7", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"9"},
+ "\u00C1", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{metaKey:1}, chars:"\u00E7", unmodifiedChars:"\u00E7"},
+ "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{}, chars:"\u00E0", unmodifiedChars:"\u00E0"},
+ "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{shiftKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{ctrlKey:1}, chars:"", unmodifiedChars:"\u00E0"},
+ "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"", unmodifiedChars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1}, chars:"\u00F8", unmodifiedChars:"\u00E0"},
+ "\u00F8", "Digit0", KeyboardEvent.DOM_VK_0, "\u00F8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00D8", unmodifiedChars:"0"},
+ "\u00D8", "Digit0", KeyboardEvent.DOM_VK_0, "\u00D8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1, ctrlKey:1}, chars:"", unmodifiedChars:"\u00E0"},
+ "\u00F8", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"", unmodifiedChars:"0"},
+ "\u00D8", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{metaKey:1}, chars:"\u00E0", unmodifiedChars:"\u00E0"},
+ "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Thai
+ // keycode should be DOM_VK_[A-Z] of the key on the latest ASCII capable keyboard layout is for alphabet
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_A,
+ modifiers:{}, chars:"\u0E1F", unmodifiedChars:"\u0E1F"},
+ "\u0E1F", "KeyA", KeyboardEvent.DOM_VK_A, "\u0E1F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // keycode should be shifted character if unshifted character isn't an ASCII character
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{}, chars:"\u0E07", unmodifiedChars:"\u0E07"},
+ "\u0E07", "Quote", KeyboardEvent.DOM_VK_PERIOD, "\u0E07", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // keycode should be same as ANSI keyboard layout's key which causes the native virtual keycode
+ // if the character of the key on the latest ASCII capable keyboard layout isn't for alphabet
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{}, chars:"\u0E43", unmodifiedChars:"\u0E43"},
+ "\u0E43", "Period", KeyboardEvent.DOM_VK_PERIOD, "\u0E43", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // keycode should be DOM_VK_[0-9] if the key on the latest ASCII capable keyboard layout is for numeric
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_1,
+ modifiers:{}, chars:"\u0E45", unmodifiedChars:"\u0E45"},
+ "\u0E45", "Digit1", KeyboardEvent.DOM_VK_1, "\u0E45", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_2,
+ modifiers:{}, chars:"/", unmodifiedChars:"/"},
+ "/", "Digit2", KeyboardEvent.DOM_VK_2, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_3,
+ modifiers:{}, chars:"_", unmodifiedChars:"_"},
+ "_", "Digit3", KeyboardEvent.DOM_VK_3, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_4,
+ modifiers:{}, chars:"\u0E20", unmodifiedChars:"\u0E20"},
+ "\u0E20", "Digit4", KeyboardEvent.DOM_VK_4, "\u0E20", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_5,
+ modifiers:{}, chars:"\u0E16", unmodifiedChars:"\u0E16"},
+ "\u0E16", "Digit5", KeyboardEvent.DOM_VK_5, "\u0E16", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_6,
+ modifiers:{}, chars:"\u0E38", unmodifiedChars:"\u0E38"},
+ "\u0E38", "Digit6", KeyboardEvent.DOM_VK_6, "\u0E38", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_7,
+ modifiers:{}, chars:"\u0E36", unmodifiedChars:"\u0E36"},
+ "\u0E36", "Digit7", KeyboardEvent.DOM_VK_7, "\u0E36", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_8,
+ modifiers:{}, chars:"\u0E04", unmodifiedChars:"\u0E04"},
+ "\u0E04", "Digit8", KeyboardEvent.DOM_VK_8, "\u0E04", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_9,
+ modifiers:{}, chars:"\u0E15", unmodifiedChars:"\u0E15"},
+ "\u0E15", "Digit9", KeyboardEvent.DOM_VK_9, "\u0E15", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_0,
+ modifiers:{}, chars:"\u0E08", unmodifiedChars:"\u0E08"},
+ "\u0E08", "Digit0", KeyboardEvent.DOM_VK_0, "\u0E08", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Dvorak-Qwerty, layout should be changed when Command key is pressed.
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S,
+ modifiers:{}, chars:"o", unmodifiedChars:"o"},
+ "o", "KeyS", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S,
+ modifiers:{shiftKey:1}, chars:"O", unmodifiedChars:"O"},
+ "O", "KeyS", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S,
+ modifiers:{ctrlKey:1}, chars:"\u000F", unmodifiedChars:"o"},
+ "o", "KeyS", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S,
+ modifiers:{altKey:1}, chars:"\u00F8", unmodifiedChars:"o"},
+ "\u00F8", "KeyS", KeyboardEvent.DOM_VK_O, "\u00F8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S,
+ modifiers:{metaKey:1}, chars:"s", unmodifiedChars:"o"},
+ "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D,
+ modifiers:{}, chars:"e", unmodifiedChars:"e"},
+ "e", "KeyD", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D,
+ modifiers:{shiftKey:1}, chars:"E", unmodifiedChars:"E"},
+ "E", "KeyD", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D,
+ modifiers:{ctrlKey:1}, chars:"\u0005", unmodifiedChars:"e"},
+ "e", "KeyD", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D,
+ modifiers:{altKey:1}, chars:"", unmodifiedChars:"e"},
+ "Dead", "KeyD", KeyboardEvent.DOM_VK_E, "\u00B4", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D,
+ modifiers:{metaKey:1}, chars:"d", unmodifiedChars:"e"},
+ "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_I,
+ modifiers:{metaKey:1, altKey:1}, chars:"^", unmodifiedChars:"c"},
+ "^", "KeyI", KeyboardEvent.DOM_VK_I, "^", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_I,
+ modifiers:{metaKey:1, altKey:1, shiftKey:1}, chars:"\u02C6", unmodifiedChars:"C"},
+ "\u02C6", "KeyI", KeyboardEvent.DOM_VK_I, "\u02C6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Arabic - PC keyboard layout inputs 2 or more characters with some key.
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_G,
+ modifiers:{shiftKey:1}, chars:"\u0644\u0623", unmodifiedChars:"\u0644\u0623"},
+ ["\u0644\u0623", "\u0644", "\u0623", "\u0644\u0623"], "KeyG", KeyboardEvent.DOM_VK_G, "\u0644\u0623", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_T,
+ modifiers:{shiftKey:1}, chars:"\u0644\u0625", unmodifiedChars:"\u0644\u0625"},
+ ["\u0644\u0625", "\u0644", "\u0625", "\u0644\u0625"], "KeyT", KeyboardEvent.DOM_VK_T, "\u0644\u0625", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B,
+ modifiers:{shiftKey:1}, chars:"\u0644\u0622", unmodifiedChars:"\u0644\u0622"},
+ ["\u0644\u0622", "\u0644", "\u0622", "\u0644\u0622"], "KeyB", KeyboardEvent.DOM_VK_B, "\u0644\u0622", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B,
+ modifiers:{}, chars:"\u0644\u0627", unmodifiedChars:"\u0644\u0627"},
+ ["\u0644\u0627", "\u0644", "\u0627", "\u0644\u0627"], "KeyB", KeyboardEvent.DOM_VK_B, "\u0644\u0627", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ cleanup();
+ }
+
+ function* testKeysOnWindows()
+ {
+ // On Windows, you can use Spy++ or Winspector (free) to watch window messages.
+ // The keyCode is given by the wParam of the last WM_KEYDOWN message. The
+ // chars string is given by the wParam of the WM_CHAR message. unmodifiedChars
+ // is not needed on Windows.
+
+ // Shift-ctrl-alt generates no WM_CHAR, but we still get a keypress
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:""},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Greek plain text
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u03b1"},
+ "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "\u03b1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"\u0391"},
+ "\u0391", "KeyA", KeyboardEvent.DOM_VK_A, "\u0391", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Greek ctrl keys produce Latin charcodes
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1}, chars:"\u0001"},
+ "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001"},
+ "\u0391", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Caps Lock key event
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CAPITAL,
+ modifiers:{capsLockKey:1}, chars:""},
+ "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CAPITAL,
+ modifiers:{capsLockKey:0}, chars:""},
+ "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Shift keys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LSHIFT,
+ modifiers:{shiftKey:1}, chars:""},
+ "Shift", "ShiftLeft", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RSHIFT,
+ modifiers:{shiftRightKey:1}, chars:""},
+ "Shift", "ShiftRight", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // Ctrl keys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LCONTROL,
+ modifiers:{ctrlKey:1}, chars:""},
+ "Control", "ControlLeft", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RCONTROL,
+ modifiers:{ctrlRightKey:1}, chars:""},
+ "Control", "ControlRight", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // Alt keys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LMENU,
+ modifiers:{altKey:1}, chars:""},
+ "Alt", "AltLeft", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RMENU,
+ modifiers:{altRightKey:1}, chars:""},
+ "Alt", "AltRight", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // Win keys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LWIN,
+ modifiers:{metaKey:1}, chars:""},
+ "Meta", "MetaLeft", KeyboardEvent.DOM_VK_WIN, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RWIN,
+ modifiers:{metaRightKey:1}, chars:""},
+ "Meta", "MetaRight", KeyboardEvent.DOM_VK_WIN, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
+
+ // all keys on keyboard (keyCode test)
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK,
+ modifiers:{}, chars:"\u0008"},
+ "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_TAB,
+ modifiers:{}, chars:"\t"},
+ "Tab", "Tab", KeyboardEvent.DOM_VK_TAB, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN,
+ modifiers:{}, chars:"\r"},
+ "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PAUSE,
+ modifiers:{}, chars:""},
+ "Pause", "Pause", KeyboardEvent.DOM_VK_PAUSE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_KANA,
+ modifiers:{}, chars:""},
+ "Unidentified", "", KeyboardEvent.DOM_VK_KANA, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_JUNJA,
+ modifiers:{}, chars:""},
+ "JunjaMode", "", KeyboardEvent.DOM_VK_JUNJA, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_FINAL,
+ modifiers:{}, chars:""},
+ "FinalMode", "", KeyboardEvent.DOM_VK_FINAL, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_KANJI,
+ modifiers:{}, chars:""},
+ "Unidentified", "", KeyboardEvent.DOM_VK_KANJI, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ESCAPE,
+ modifiers:{}, chars:""},
+ "Escape", "Escape", KeyboardEvent.DOM_VK_ESCAPE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CONVERT,
+ modifiers:{}, chars:""},
+ "Convert", "", KeyboardEvent.DOM_VK_CONVERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NONCONVERT,
+ modifiers:{}, chars:""},
+ "NonConvert", "", KeyboardEvent.DOM_VK_NONCONVERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ACCEPT,
+ modifiers:{}, chars:""},
+ "Accept", "", KeyboardEvent.DOM_VK_ACCEPT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_MODECHANGE,
+ modifiers:{}, chars:""},
+ "ModeChange", "", KeyboardEvent.DOM_VK_MODECHANGE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SPACE,
+ modifiers:{}, chars:" "},
+ " ", "Space", KeyboardEvent.DOM_VK_SPACE, " ", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Ctrl+Space causes WM_CHAR with ' '. However, its keypress event's ctrlKey state shouldn't be false.
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SPACE,
+ modifiers:{ctrlKey:1}, chars:" "},
+ " ", "Space", KeyboardEvent.DOM_VK_SPACE, " ", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SELECT,
+ modifiers:{}, chars:""},
+ "Select", "", KeyboardEvent.DOM_VK_SELECT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PRINT,
+ modifiers:{}, chars:""},
+ "Unidentified", "", KeyboardEvent.DOM_VK_PRINT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_EXECUTE,
+ modifiers:{}, chars:""},
+ "Execute", "", KeyboardEvent.DOM_VK_EXECUTE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SNAPSHOT,
+ modifiers:{}, chars:""},
+ "PrintScreen", "PrintScreen", KeyboardEvent.DOM_VK_PRINTSCREEN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_HELP,
+ modifiers:{}, chars:""},
+ "Help", "", KeyboardEvent.DOM_VK_HELP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SLEEP,
+ modifiers:{}, chars:""},
+ "Standby", "", KeyboardEvent.DOM_VK_SLEEP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PRIOR,
+ modifiers:{}, chars:""},
+ "PageUp", "PageUp", KeyboardEvent.DOM_VK_PAGE_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NEXT,
+ modifiers:{}, chars:""},
+ "PageDown", "PageDown", KeyboardEvent.DOM_VK_PAGE_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_END,
+ modifiers:{}, chars:""},
+ "End", "End", KeyboardEvent.DOM_VK_END, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_HOME,
+ modifiers:{}, chars:""},
+ "Home", "Home", KeyboardEvent.DOM_VK_HOME, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LEFT,
+ modifiers:{}, chars:""},
+ "ArrowLeft", "ArrowLeft", KeyboardEvent.DOM_VK_LEFT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_UP,
+ modifiers:{}, chars:""},
+ "ArrowUp", "ArrowUp", KeyboardEvent.DOM_VK_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RIGHT,
+ modifiers:{}, chars:""},
+ "ArrowRight", "ArrowRight", KeyboardEvent.DOM_VK_RIGHT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DOWN,
+ modifiers:{}, chars:""},
+ "ArrowDown", "ArrowDown", KeyboardEvent.DOM_VK_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_INSERT,
+ modifiers:{}, chars:""},
+ "Insert", "Insert", KeyboardEvent.DOM_VK_INSERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DELETE,
+ modifiers:{}, chars:""},
+ "Delete", "Delete", KeyboardEvent.DOM_VK_DELETE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Backspace and Enter are handled with special path in mozilla::widget::NativeKey. So, let's test them with modifiers too.
+ // Note that when both Ctrl and Alt are pressed, they don't cause WM_(SYS)CHAR message.
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK,
+ modifiers:{ctrlKey:1}, chars:"\u007F"},
+ "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK,
+ modifiers:{altKey:1}, chars:"\u0008"},
+ "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK,
+ modifiers:{ctrl:1, altKey:1}, chars:""},
+ "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN,
+ modifiers:{ctrlKey:1}, chars:"\n"},
+ "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN,
+ modifiers:{altKey:1}, chars:"\r"},
+ "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN,
+ modifiers:{ctrl:1, altKey:1}, chars:""},
+ "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Even non-printable key could be mapped as a printable key.
+ // Only "keyup" event cannot know if it *did* cause inputting text.
+ // Therefore, only "keydown" and "keypress" event's key value should be the character inputted by the key.
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F4,
+ modifiers:{}, chars:"a"},
+ ["a", "a", "F4"], "F4", KeyboardEvent.DOM_VK_F4, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Even if key message is processed by IME, when the key causes inputting text,
+ // keypress event(s) should be fired.
+ const WIN_VK_PROCESSKEY_WITH_SC_A = WIN_VK_PROCESSKEY | 0x001E0000;
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PROCESSKEY_WITH_SC_A,
+ modifiers:{}, chars:"a"},
+ ["a", "a", "Process"], "KeyA", KeyboardEvent.DOM_VK_PROCESSKEY, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PROCESSKEY_WITH_SC_A,
+ modifiers:{altKey:1}, chars:"a"},
+ ["a", "a", "Process"], "KeyA", KeyboardEvent.DOM_VK_PROCESSKEY, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // US
+ // Alphabet
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"A"},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1}, chars:"\u0001"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001"},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{altKey:1}, chars:"a"},
+ "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{altKey:1, shiftKey:1}, chars:"A"},
+ "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B,
+ modifiers:{}, chars:"b"},
+ "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B,
+ modifiers:{shiftKey:1}, chars:"B"},
+ "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B,
+ modifiers:{ctrlKey:1}, chars:"\u0002"},
+ "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0002"},
+ "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B,
+ modifiers:{altKey:1}, chars:"b"},
+ "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B,
+ modifiers:{altKey:1, shiftKey:1}, chars:"B"},
+ "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C,
+ modifiers:{}, chars:"c"},
+ "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C,
+ modifiers:{shiftKey:1}, chars:"C"},
+ "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C,
+ modifiers:{ctrlKey:1}, chars:"\u0003"},
+ "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0003"},
+ "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C,
+ modifiers:{altKey:1}, chars:"c"},
+ "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C,
+ modifiers:{altKey:1, shiftKey:1}, chars:"C"},
+ "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D,
+ modifiers:{}, chars:"d"},
+ "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D,
+ modifiers:{shiftKey:1}, chars:"D"},
+ "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D,
+ modifiers:{ctrlKey:1}, chars:"\u0004"},
+ "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0004"},
+ "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D,
+ modifiers:{altKey:1}, chars:"d"},
+ "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D,
+ modifiers:{altKey:1, shiftKey:1}, chars:"D"},
+ "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E,
+ modifiers:{}, chars:"e"},
+ "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E,
+ modifiers:{shiftKey:1}, chars:"E"},
+ "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E,
+ modifiers:{ctrlKey:1}, chars:"\u0005"},
+ "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0005"},
+ "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E,
+ modifiers:{altKey:1}, chars:"e"},
+ "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E,
+ modifiers:{altKey:1, shiftKey:1}, chars:"E"},
+ "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F,
+ modifiers:{}, chars:"f"},
+ "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F,
+ modifiers:{shiftKey:1}, chars:"F"},
+ "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F,
+ modifiers:{ctrlKey:1}, chars:"\u0006"},
+ "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0006"},
+ "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F,
+ modifiers:{altKey:1}, chars:"f"},
+ "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F,
+ modifiers:{altKey:1, shiftKey:1}, chars:"F"},
+ "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G,
+ modifiers:{}, chars:"g"},
+ "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G,
+ modifiers:{shiftKey:1}, chars:"G"},
+ "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G,
+ modifiers:{ctrlKey:1}, chars:"\u0007"},
+ "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0007"},
+ "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G,
+ modifiers:{altKey:1}, chars:"g"},
+ "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G,
+ modifiers:{altKey:1, shiftKey:1}, chars:"G"},
+ "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H,
+ modifiers:{}, chars:"h"},
+ "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H,
+ modifiers:{shiftKey:1}, chars:"H"},
+ "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H,
+ modifiers:{ctrlKey:1}, chars:"\u0008"},
+ "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0008"},
+ "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H,
+ modifiers:{altKey:1}, chars:"h"},
+ "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H,
+ modifiers:{altKey:1, shiftKey:1}, chars:"H"},
+ "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I,
+ modifiers:{}, chars:"i"},
+ "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I,
+ modifiers:{shiftKey:1}, chars:"I"},
+ "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I,
+ modifiers:{ctrlKey:1}, chars:"\u0009"},
+ "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0009"},
+ "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I,
+ modifiers:{altKey:1}, chars:"i"},
+ "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I,
+ modifiers:{altKey:1, shiftKey:1}, chars:"I"},
+ "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J,
+ modifiers:{}, chars:"j"},
+ "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J,
+ modifiers:{shiftKey:1}, chars:"J"},
+ "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J,
+ modifiers:{ctrlKey:1}, chars:"\u000A"},
+ "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000A"},
+ "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J,
+ modifiers:{altKey:1}, chars:"j"},
+ "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J,
+ modifiers:{altKey:1, shiftKey:1}, chars:"J"},
+ "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K,
+ modifiers:{}, chars:"k"},
+ "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K,
+ modifiers:{shiftKey:1}, chars:"K"},
+ "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K,
+ modifiers:{ctrlKey:1}, chars:"\u000B"},
+ "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000B"},
+ "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K,
+ modifiers:{altKey:1}, chars:"k"},
+ "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K,
+ modifiers:{altKey:1, shiftKey:1}, chars:"K"},
+ "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L,
+ modifiers:{}, chars:"l"},
+ "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L,
+ modifiers:{shiftKey:1}, chars:"L"},
+ "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L,
+ modifiers:{ctrlKey:1}, chars:"\u000C"},
+ "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000C"},
+ "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L,
+ modifiers:{altKey:1}, chars:"l"},
+ "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L,
+ modifiers:{altKey:1, shiftKey:1}, chars:"L"},
+ "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M,
+ modifiers:{}, chars:"m"},
+ "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M,
+ modifiers:{shiftKey:1}, chars:"M"},
+ "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M,
+ modifiers:{ctrlKey:1}, chars:"\u000D"},
+ "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000D"},
+ "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M,
+ modifiers:{altKey:1}, chars:"m"},
+ "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M,
+ modifiers:{altKey:1, shiftKey:1}, chars:"M"},
+ "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N,
+ modifiers:{}, chars:"n"},
+ "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N,
+ modifiers:{shiftKey:1}, chars:"N"},
+ "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N,
+ modifiers:{ctrlKey:1}, chars:"\u000E"},
+ "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000E"},
+ "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N,
+ modifiers:{altKey:1}, chars:"n"},
+ "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N,
+ modifiers:{altKey:1, shiftKey:1}, chars:"N"},
+ "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O,
+ modifiers:{}, chars:"o"},
+ "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O,
+ modifiers:{shiftKey:1}, chars:"O"},
+ "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O,
+ modifiers:{ctrlKey:1}, chars:"\u000F"},
+ "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000F"},
+ "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O,
+ modifiers:{altKey:1}, chars:"o"},
+ "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O,
+ modifiers:{altKey:1, shiftKey:1}, chars:"O"},
+ "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P,
+ modifiers:{}, chars:"p"},
+ "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P,
+ modifiers:{shiftKey:1}, chars:"P"},
+ "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P,
+ modifiers:{ctrlKey:1}, chars:"\u0010"},
+ "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0010"},
+ "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P,
+ modifiers:{altKey:1}, chars:"p"},
+ "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P,
+ modifiers:{altKey:1, shiftKey:1}, chars:"P"},
+ "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q,
+ modifiers:{}, chars:"q"},
+ "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q,
+ modifiers:{shiftKey:1}, chars:"Q"},
+ "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q,
+ modifiers:{ctrlKey:1}, chars:"\u0011"},
+ "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0011"},
+ "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q,
+ modifiers:{altKey:1}, chars:"q"},
+ "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q,
+ modifiers:{altKey:1, shiftKey:1}, chars:"Q"},
+ "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R,
+ modifiers:{}, chars:"r"},
+ "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R,
+ modifiers:{shiftKey:1}, chars:"R"},
+ "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R,
+ modifiers:{ctrlKey:1}, chars:"\u0012"},
+ "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0012"},
+ "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R,
+ modifiers:{altKey:1}, chars:"r"},
+ "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R,
+ modifiers:{altKey:1, shiftKey:1}, chars:"R"},
+ "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S,
+ modifiers:{}, chars:"s"},
+ "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S,
+ modifiers:{shiftKey:1}, chars:"S"},
+ "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S,
+ modifiers:{ctrlKey:1}, chars:"\u0013"},
+ "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0013"},
+ "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S,
+ modifiers:{altKey:1}, chars:"s"},
+ "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S,
+ modifiers:{altKey:1, shiftKey:1}, chars:"S"},
+ "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
+ modifiers:{}, chars:"t"},
+ "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
+ modifiers:{shiftKey:1}, chars:"T"},
+ "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
+ modifiers:{ctrlKey:1}, chars:"\u0014"},
+ "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0014"},
+ "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
+ modifiers:{altKey:1}, chars:"t"},
+ "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
+ modifiers:{altKey:1, shiftKey:1}, chars:"T"},
+ "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U,
+ modifiers:{}, chars:"u"},
+ "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U,
+ modifiers:{shiftKey:1}, chars:"U"},
+ "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U,
+ modifiers:{ctrlKey:1}, chars:"\u0015"},
+ "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0015"},
+ "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U,
+ modifiers:{altKey:1}, chars:"u"},
+ "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U,
+ modifiers:{altKey:1, shiftKey:1}, chars:"U"},
+ "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V,
+ modifiers:{}, chars:"v"},
+ "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V,
+ modifiers:{shiftKey:1}, chars:"V"},
+ "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V,
+ modifiers:{ctrlKey:1}, chars:"\u0016"},
+ "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0016"},
+ "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V,
+ modifiers:{altKey:1}, chars:"v"},
+ "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V,
+ modifiers:{altKey:1, shiftKey:1}, chars:"V"},
+ "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W,
+ modifiers:{}, chars:"w"},
+ "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W,
+ modifiers:{shiftKey:1}, chars:"W"},
+ "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W,
+ modifiers:{ctrlKey:1}, chars:"\u0017"},
+ "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0017"},
+ "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W,
+ modifiers:{altKey:1}, chars:"w"},
+ "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W,
+ modifiers:{altKey:1, shiftKey:1}, chars:"W"},
+ "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{}, chars:"x"},
+ "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{shiftKey:1}, chars:"X"},
+ "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1}, chars:"\u0018"},
+ "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"},
+ "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{altKey:1}, chars:"x"},
+ "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{altKey:1, shiftKey:1}, chars:"X"},
+ "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y,
+ modifiers:{}, chars:"y"},
+ "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y,
+ modifiers:{shiftKey:1}, chars:"Y"},
+ "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y,
+ modifiers:{ctrlKey:1}, chars:"\u0019"},
+ "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0019"},
+ "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y,
+ modifiers:{altKey:1}, chars:"y"},
+ "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y,
+ modifiers:{altKey:1, shiftKey:1}, chars:"Y"},
+ "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z,
+ modifiers:{}, chars:"z"},
+ "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z,
+ modifiers:{shiftKey:1}, chars:"Z"},
+ "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z,
+ modifiers:{ctrlKey:1}, chars:"\u001A"},
+ "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001A"},
+ "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z,
+ modifiers:{altKey:1}, chars:"z"},
+ "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z,
+ modifiers:{altKey:1, shiftKey:1}, chars:"Z"},
+ "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Numeric
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0,
+ modifiers:{}, chars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0,
+ modifiers:{shiftKey:1}, chars:")"},
+ ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0,
+ modifiers:{ctrlKey:1}, chars:""},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0,
+ modifiers:{altKey:1}, chars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0,
+ modifiers:{altKey:1, shiftKey:1}, chars:")"},
+ ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1,
+ modifiers:{}, chars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1,
+ modifiers:{shiftKey:1}, chars:"!"},
+ "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1,
+ modifiers:{ctrlKey:1}, chars:""},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1,
+ modifiers:{altKey:1}, chars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:"!"},
+ "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2,
+ modifiers:{}, chars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2,
+ modifiers:{shiftKey:1}, chars:"@"},
+ "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2,
+ modifiers:{ctrlKey:1}, chars:""},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2,
+ modifiers:{altKey:1}, chars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2,
+ modifiers:{altKey:1, shiftKey:1}, chars:"@"},
+ "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3,
+ modifiers:{}, chars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3,
+ modifiers:{shiftKey:1}, chars:"#"},
+ "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3,
+ modifiers:{ctrlKey:1}, chars:""},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3,
+ modifiers:{altKey:1}, chars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3,
+ modifiers:{altKey:1, shiftKey:1}, chars:"#"},
+ "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4,
+ modifiers:{}, chars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4,
+ modifiers:{shiftKey:1}, chars:"$"},
+ "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4,
+ modifiers:{ctrlKey:1}, chars:""},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4,
+ modifiers:{altKey:1}, chars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4,
+ modifiers:{altKey:1, shiftKey:1}, chars:"$"},
+ "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5,
+ modifiers:{}, chars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5,
+ modifiers:{shiftKey:1}, chars:"%"},
+ "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5,
+ modifiers:{ctrlKey:1}, chars:""},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5,
+ modifiers:{altKey:1}, chars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5,
+ modifiers:{altKey:1, shiftKey:1}, chars:"%"},
+ "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6,
+ modifiers:{}, chars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6,
+ modifiers:{shiftKey:1}, chars:"^"},
+ "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6,
+ modifiers:{ctrlKey:1}, chars:""},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001E"},
+ "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6,
+ modifiers:{altKey:1}, chars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6,
+ modifiers:{altKey:1, shiftKey:1}, chars:"^"},
+ "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7,
+ modifiers:{}, chars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7,
+ modifiers:{shiftKey:1}, chars:"&"},
+ "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7,
+ modifiers:{ctrlKey:1}, chars:""},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7,
+ modifiers:{altKey:1}, chars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7,
+ modifiers:{altKey:1, shiftKey:1}, chars:"&"},
+ "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8,
+ modifiers:{}, chars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8,
+ modifiers:{shiftKey:1}, chars:"*"},
+ "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8,
+ modifiers:{ctrlKey:1, }, chars:""},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8,
+ modifiers:{altKey:1}, chars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8,
+ modifiers:{altKey:1, shiftKey:1}, chars:"*"},
+ "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9,
+ modifiers:{}, chars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9,
+ modifiers:{shiftKey:1}, chars:"("},
+ "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9,
+ modifiers:{ctrlKey:1}, chars:""},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9,
+ modifiers:{altKey:1}, chars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9,
+ modifiers:{altKey:1, shiftKey:1}, chars:"("},
+ "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // OEM keys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS,
+ modifiers:{}, chars:"-"},
+ "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS,
+ modifiers:{shiftKey:1}, chars:"_"},
+ "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS,
+ modifiers:{ctrlKey:1}, chars:""},
+ "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001F"},
+ "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS,
+ modifiers:{altKey:1}, chars:"-"},
+ "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS,
+ modifiers:{altKey:1, shiftKey:1}, chars:"_"},
+ "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{}, chars:"="},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{shiftKey:1}, chars:"+"},
+ "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{ctrlKey:1}, chars:""},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{altKey:1}, chars:"="},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{altKey:1, shiftKey:1}, chars:"+"},
+ "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4,
+ modifiers:{}, chars:"["},
+ "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4,
+ modifiers:{shiftKey:1}, chars:"{"},
+ "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4,
+ modifiers:{ctrlKey:1}, chars:"\u001B"},
+ "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4,
+ modifiers:{altKey:1}, chars:"["},
+ "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4,
+ modifiers:{altKey:1, shiftKey:1}, chars:"{"},
+ "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6,
+ modifiers:{}, chars:"]"},
+ "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6,
+ modifiers:{shiftKey:1}, chars:"}"},
+ "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6,
+ modifiers:{ctrlKey:1}, chars:"\u001D"},
+ "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6,
+ modifiers:{altKey:1}, chars:"]"},
+ "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6,
+ modifiers:{altKey:1, shiftKey:1}, chars:"}"},
+ "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{}, chars:";"},
+ ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{shiftKey:1}, chars:":"},
+ ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{ctrlKey:1}, chars:""},
+ ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1}, chars:";"},
+ ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:":"},
+ ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{}, chars:"'"},
+ "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{shiftKey:1}, chars:"\""},
+ "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{ctrlKey:1}, chars:""},
+ "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{altKey:1}, chars:"'"},
+ "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\""},
+ "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5,
+ modifiers:{}, chars:"\\"},
+ "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5,
+ modifiers:{shiftKey:1}, chars:"|"},
+ "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5,
+ modifiers:{ctrlKey:1}, chars:"\u001C"},
+ "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5,
+ modifiers:{altKey:1}, chars:"\\"},
+ "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5,
+ modifiers:{altKey:1, shiftKey:1}, chars:"|"},
+ "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{}, chars:","},
+ ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{shiftKey:1}, chars:"<"},
+ "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{ctrlKey:1}, chars:""},
+ ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{altKey:1}, chars:","},
+ ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{altKey:1, shiftKey:1}, chars:"<"},
+ "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{}, chars:"."},
+ ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{shiftKey:1}, chars:">"},
+ ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{ctrlKey:1}, chars:""},
+ ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{altKey:1}, chars:"."},
+ ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{altKey:1, shiftKey:1}, chars:">"},
+ ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2,
+ modifiers:{}, chars:"/"},
+ "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2,
+ modifiers:{shiftKey:1}, chars:"?"},
+ "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2,
+ modifiers:{ctrlKey:1}, chars:""},
+ "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2,
+ modifiers:{altKey:1}, chars:"/"},
+ "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2,
+ modifiers:{altKey:1, shiftKey:1}, chars:"?"},
+ "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3,
+ modifiers:{}, chars:"`"},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3,
+ modifiers:{shiftKey:1}, chars:"~"},
+ "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3,
+ modifiers:{ctrlKey:1}, chars:""},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3,
+ modifiers:{altKey:1}, chars:"`"},
+ "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3,
+ modifiers:{altKey:1, shiftKey:1}, chars:"~"},
+ "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Numpad
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD0,
+ modifiers:{numLockKey:1}, chars:"0"},
+ "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD1,
+ modifiers:{numLockKey:1}, chars:"1"},
+ "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD2,
+ modifiers:{numLockKey:1}, chars:"2"},
+ "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD3,
+ modifiers:{numLockKey:1}, chars:"3"},
+ "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD4,
+ modifiers:{numLockKey:1}, chars:"4"},
+ "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD5,
+ modifiers:{numLockKey:1}, chars:"5"},
+ "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD6,
+ modifiers:{numLockKey:1}, chars:"6"},
+ "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD7,
+ modifiers:{numLockKey:1}, chars:"7"},
+ "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD8,
+ modifiers:{numLockKey:1}, chars:"8"},
+ "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD9,
+ modifiers:{numLockKey:1}, chars:"9"},
+ "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_MULTIPLY,
+ modifiers:{numLockKey:1}, chars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_MULTIPLY,
+ modifiers:{numLockKey:1, shiftKey:1}, chars:"*"},
+ "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ADD,
+ modifiers:{numLockKey:1}, chars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ADD,
+ modifiers:{numLockKey:1, shiftKey:1}, chars:"+"},
+ "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ // VK_SEPARATOR is keycode for NEC's PC-98 series whose keyboard layout was
+ // different from current PC's keyboard layout and it cannot connect to
+ // current PC. Note that even if we synthesize WM_KEYDOWN with
+ // VK_SEPARATOR, it doesn't work on Win7.
+ //yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SEPARATOR,
+ // modifiers:{numLockKey:1}, chars:""},
+ // "", "", KeyboardEvent.DOM_VK_SEPARATOR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ //yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SEPARATOR,
+ // modifiers:{numLockKey:1, shiftKey:1}, chars:""},
+ // "", "", KeyboardEvent.DOM_VK_SEPARATOR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SUBTRACT,
+ modifiers:{numLockKey:1}, chars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SUBTRACT,
+ modifiers:{numLockKey:1, shiftKey:1}, chars:"-"},
+ "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DECIMAL,
+ modifiers:{numLockKey:1}, chars:"."},
+ ".", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DECIMAL,
+ modifiers:{numLockKey:1, shiftKey:1}, chars:"."},
+ ".", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DIVIDE,
+ modifiers:{numLockKey:1}, chars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DIVIDE,
+ modifiers:{numLockKey:1, shiftKey:1}, chars:"/"},
+ "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_RETURN,
+ modifiers:{numLockKey:1}, chars:"\r"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_RETURN,
+ modifiers:{numLockKey:1, shiftKey:1}, chars:"\r"},
+ "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+
+ // Numpad without NumLock
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_PRIOR,
+ modifiers:{}, chars:""},
+ "PageUp", "Numpad9", KeyboardEvent.DOM_VK_PAGE_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_NEXT,
+ modifiers:{}, chars:""},
+ "PageDown", "Numpad3", KeyboardEvent.DOM_VK_PAGE_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_END,
+ modifiers:{}, chars:""},
+ "End", "Numpad1", KeyboardEvent.DOM_VK_END, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_HOME,
+ modifiers:{}, chars:""},
+ "Home", "Numpad7", KeyboardEvent.DOM_VK_HOME, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_LEFT,
+ modifiers:{}, chars:""},
+ "ArrowLeft", "Numpad4", KeyboardEvent.DOM_VK_LEFT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_UP,
+ modifiers:{}, chars:""},
+ "ArrowUp", "Numpad8", KeyboardEvent.DOM_VK_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_RIGHT,
+ modifiers:{}, chars:""},
+ "ArrowRight", "Numpad6", KeyboardEvent.DOM_VK_RIGHT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_DOWN,
+ modifiers:{}, chars:""},
+ "ArrowDown", "Numpad2", KeyboardEvent.DOM_VK_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_INSERT,
+ modifiers:{}, chars:""},
+ "Insert", "Numpad0", KeyboardEvent.DOM_VK_INSERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_DELETE,
+ modifiers:{}, chars:""},
+ "Delete", "NumpadDecimal", KeyboardEvent.DOM_VK_DELETE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CLEAR,
+ modifiers:{}, chars:""},
+ "Clear", "Numpad5", KeyboardEvent.DOM_VK_CLEAR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+
+ // Even if widget receives unknown keycode, it should dispatch key events
+ // whose keycode is 0 rather than native keycode.
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:0x3A,
+ modifiers:{numLockKey:1}, chars:""},
+ "Unidentified", "", 0, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // French
+ // Numeric
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0,
+ modifiers:{}, chars:"\u00E0"},
+ "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0,
+ modifiers:{shiftKey:1}, chars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1,
+ modifiers:{}, chars:"&"},
+ "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1,
+ modifiers:{shiftKey:1}, chars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2,
+ modifiers:{}, chars:"\u00E9"},
+ "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2,
+ modifiers:{shiftKey:1}, chars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3,
+ modifiers:{}, chars:"\""},
+ "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3,
+ modifiers:{shiftKey:1}, chars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4,
+ modifiers:{}, chars:"'"},
+ "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4,
+ modifiers:{shiftKey:1}, chars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5,
+ modifiers:{}, chars:"("},
+ "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5,
+ modifiers:{shiftKey:1}, chars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6,
+ modifiers:{}, chars:"-"},
+ "-", "Digit6", KeyboardEvent.DOM_VK_6, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6,
+ modifiers:{shiftKey:1}, chars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7,
+ modifiers:{}, chars:"\u00E8"},
+ "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7,
+ modifiers:{shiftKey:1}, chars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8,
+ modifiers:{}, chars:"_"},
+ "_", "Digit8", KeyboardEvent.DOM_VK_8, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8,
+ modifiers:{shiftKey:1}, chars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9,
+ modifiers:{}, chars:"\u00E7"},
+ "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9,
+ modifiers:{shiftKey:1}, chars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Numeric with ShiftLock
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0,
+ modifiers:{capsLockKey:1}, chars:"0"},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E0"},
+ "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1,
+ modifiers:{capsLockKey:1}, chars:"1"},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"&"},
+ "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2,
+ modifiers:{capsLockKey:1}, chars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E9"},
+ "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3,
+ modifiers:{capsLockKey:1}, chars:"3"},
+ "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"\""},
+ "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4,
+ modifiers:{capsLockKey:1}, chars:"4"},
+ "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"'"},
+ "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5,
+ modifiers:{capsLockKey:1}, chars:"5"},
+ "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"("},
+ "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6,
+ modifiers:{capsLockKey:1}, chars:"6"},
+ "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"-"},
+ "-", "Digit6", KeyboardEvent.DOM_VK_6, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7,
+ modifiers:{capsLockKey:1}, chars:"7"},
+ "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E8"},
+ "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8,
+ modifiers:{capsLockKey:1}, chars:"8"},
+ "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"_"},
+ "_", "Digit8", KeyboardEvent.DOM_VK_8, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9,
+ modifiers:{capsLockKey:1}, chars:"9"},
+ "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E7"},
+ "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // OEM keys
+ // If the key doesn't cause ASCII character even with or without Shift key, keyCode value should be same as
+ // the key which causes the virtual keycode on ANSI keyboard layout.
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7,
+ modifiers:{}, chars:"\u00B2"},
+ "\u00B2", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "\u00B2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7,
+ modifiers:{shiftKey:1}, chars:""},
+ "", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4,
+ modifiers:{}, chars:")"},
+ ")", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4,
+ modifiers:{shiftKey:1}, chars:"\u00B0"},
+ "\u00B0", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, "\u00B0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{}, chars:"="},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{shiftKey:1}, chars:"+"},
+ "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ // modifiers:{}, chars:""},
+ // "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key
+ //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ // modifiers:{shiftKey:1}, chars:""},
+ // ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1,
+ modifiers:{}, chars:"$"},
+ "$", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1,
+ modifiers:{shiftKey:1}, chars:"\u00A3"},
+ "\u00A3", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "\u00A3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3,
+ modifiers:{}, chars:"\u00F9"},
+ "\u00F9", "Quote", KeyboardEvent.DOM_VK_PERCENT, "\u00F9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3,
+ modifiers:{shiftKey:1}, chars:"%"},
+ "%", "Quote", KeyboardEvent.DOM_VK_PERCENT, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5,
+ modifiers:{}, chars:"*"},
+ "*", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5,
+ modifiers:{shiftKey:1}, chars:"\u00B5"},
+ "\u00B5", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "\u00B5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102,
+ modifiers:{}, chars:"<"},
+ "<", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102,
+ modifiers:{shiftKey:1}, chars:">"},
+ ">", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{}, chars:","},
+ ",", "KeyM", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{shiftKey:1}, chars:"?"},
+ "?", "KeyM", KeyboardEvent.DOM_VK_COMMA, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{}, chars:";"},
+ ";", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{shiftKey:1}, chars:"."},
+ ".", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2,
+ modifiers:{}, chars:":"},
+ ":", "Period", KeyboardEvent.DOM_VK_COLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2,
+ modifiers:{shiftKey:1}, chars:"/"},
+ "/", "Period", KeyboardEvent.DOM_VK_COLON, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8,
+ modifiers:{}, chars:"!"},
+ "!", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8,
+ modifiers:{shiftKey:1}, chars:"\u00A7"},
+ "\u00A7", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // OEM keys with ShiftLock
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7,
+ modifiers:{capsLockKey:1}, chars:"\u00B2"},
+ "\u00B2", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "\u00B2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:""},
+ "", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4,
+ modifiers:{capsLockKey:1}, chars:"\u00B0"},
+ "\u00B0", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, "\u00B0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:")"},
+ ")", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{capsLockKey:1}, chars:"+"},
+ "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"="},
+ "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ // modifiers:{capsLockKey:1}, chars:""},
+ // "Dead", "BracketLeft", KeyboardLayout.DOM_VK_CLOSE_BRACKET, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key
+ //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ // modifiers:{capsLockKey:1, shiftKey:1}, chars:""},
+ // ["\u00A8\u00A8", "\u00A8", "\u00A8", "\u00A8"], "BracketLeft", KeyboardLayout.DOM_VK_CLOSE_BRACKET, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1,
+ modifiers:{capsLockKey:1}, chars:"\u00A3"},
+ "\u00A3", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "\u00A3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"$"},
+ "$", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3,
+ modifiers:{capsLockKey:1}, chars:"%"},
+ "%", "Quote", KeyboardEvent.DOM_VK_PERCENT, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00F9"},
+ "\u00F9", "Quote", KeyboardEvent.DOM_VK_PERCENT, "\u00F9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5,
+ modifiers:{capsLockKey:1}, chars:"\u00B5"},
+ "\u00B5", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "\u00B5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"*"},
+ "*", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102,
+ modifiers:{capsLockKey:1}, chars:"<"},
+ "<", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:">"},
+ ">", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{capsLockKey:1}, chars:"?"},
+ "?", "KeyM", KeyboardEvent.DOM_VK_COMMA, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:","},
+ ",", "KeyM", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{capsLockKey:1}, chars:"."},
+ ".", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:";"},
+ ";", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2,
+ modifiers:{capsLockKey:1}, chars:"/"},
+ "/", "Period", KeyboardEvent.DOM_VK_COLON, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:":"},
+ ":", "Period", KeyboardEvent.DOM_VK_COLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8,
+ modifiers:{capsLockKey:1}, chars:"\u00A7"},
+ "\u00A7", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8,
+ modifiers:{capsLockKey:1, shiftKey:1}, chars:"!"},
+ "!", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // AltGr
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0,
+ modifiers:{altGrKey:1}, chars:"@"},
+ "@", "Digit0", KeyboardEvent.DOM_VK_0, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // AltGr + Digit1 does not cause text input in French layout. In this case,
+ // AltGr shouldn't be used for a modifier of shortcut. Therefore, not
+ // receiving `keypress` event even in the system group is fine.
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1,
+ modifiers:{altGrKey:1}, chars:""},
+ "&", "Digit1", KeyboardEvent.DOM_VK_1, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2,
+ // modifiers:{altGrKey:1}, chars:""},
+ // "Dead", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3,
+ modifiers:{altGrKey:1}, chars:"#"},
+ "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4,
+ modifiers:{altGrKey:1}, chars:"{"},
+ "{", "Digit4", KeyboardEvent.DOM_VK_4, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5,
+ modifiers:{altGrKey:1}, chars:"["},
+ "[", "Digit5", KeyboardEvent.DOM_VK_5, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6,
+ modifiers:{altGrKey:1}, chars:"|"},
+ "|", "Digit6", KeyboardEvent.DOM_VK_6, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7,
+ // modifiers:{altGrKey:1}, chars:""},
+ // "Dead", "Digit7", KeyboardEvent.DOM_VK_7, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8,
+ modifiers:{altGrKey:1}, chars:"\\"},
+ "\\", "Digit8", KeyboardEvent.DOM_VK_8, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9,
+ modifiers:{altGrKey:1}, chars:"^"},
+ "^", "Digit9", KeyboardEvent.DOM_VK_9, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4,
+ modifiers:{altGrKey:1}, chars:"]"},
+ "]", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{altGrKey:1}, chars:"}"},
+ "}", "Equal", KeyboardEvent.DOM_VK_EQUALS, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // AltGr emulated with Ctrl and Alt
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"@", isInputtingCharacters:true},
+ "@", "Digit0", KeyboardEvent.DOM_VK_0, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0,
+ modifiers:{ctrlKey:1, altKey:1, shiftKey:1}, chars:"", isInputtingCharacters:false},
+ "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ // Different from AltGr + Digit1 case, Ctrl + Alt + Digit1 should be
+ // available as a shortcut key. Therefore, `keypress` event needs to be
+ // fired.
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"", isInputtingCharacters:false},
+ "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1,
+ modifiers:{ctrlKey:1, altKey:1, shiftKey:1}, chars:"", isInputtingCharacters:false},
+ "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // German
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_OEM_2,
+ modifiers:{}, chars:"#"},
+ "#", "Backslash", KeyboardEvent.DOM_VK_HASH, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_OEM_2,
+ modifiers:{shiftKey:1}, chars:"'"},
+ "'", "Backslash", KeyboardEvent.DOM_VK_HASH, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Khmer
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2,
+ modifiers:{}, chars:"\u17E2"},
+ "\u17E2", "Digit2", KeyboardEvent.DOM_VK_2, "\u17E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2,
+ modifiers:{shiftKey:1}, chars:"\u17D7"},
+ "\u17D7", "Digit2", KeyboardEvent.DOM_VK_2, "\u17D7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, // Ctrl+2 should cause inputting Euro sign.
+ modifiers:{ctrlKey:1}, chars:"\u20AC", isInputtingCharacters:true},
+ "\u20AC", "Digit2", KeyboardEvent.DOM_VK_2, "\u20AC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, // Ctrl+Shift+2 shouldn't cause any input.
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "\u17D7", "Digit2", KeyboardEvent.DOM_VK_2, "\u17D7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2,
+ modifiers:{altKey:1}, chars:"\u17E2"},
+ "\u17E2", "Digit2", KeyboardEvent.DOM_VK_2, "\u17E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u17D7"},
+ "\u17D7", "Digit2", KeyboardEvent.DOM_VK_2, "\u17D7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2,
+ modifiers:{altGrKey:1}, chars:"2"},
+ "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2,
+ modifiers:{altGrKey:1, shiftKey:1}, chars:"\u19E2"},
+ "\u19E2", "Digit2", KeyboardEvent.DOM_VK_2, "\u19E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Norwegian
+ yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_5,
+ modifiers:{}, chars:"|"},
+ "|", "Backquote", KeyboardEvent.DOM_VK_PIPE, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_5,
+ modifiers:{shiftKey:1}, chars:"\u00A7"},
+ "\u00A7", "Backquote", KeyboardEvent.DOM_VK_PIPE, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // Brazilian ABNT
+ yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_ABNT_C1,
+ modifiers:{}, chars:"/"},
+ "/", "IntlBackslash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_ABNT_C1,
+ modifiers:{shiftKey:1}, chars:"?"},
+ "?", "IntlBackslash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_ABNT_C2,
+ modifiers:{numLockKey:1}, chars:"."},
+ ".", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_DECIMAL,
+ modifiers:{numLockKey:1}, chars:","},
+ ",", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+
+ // Mac JIS keyboard
+ // The separator key on JIS keyboard for Mac doesn't cause any character even with Japanese keyboard layout.
+ yield testKey({layout:KEYBOARD_LAYOUT_JAPANESE, keyCode:WIN_VK_ABNT_C2,
+ modifiers:{numLockKey:1}, chars:""},
+ "", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+ yield testKey({layout:KEYBOARD_LAYOUT_JAPANESE, keyCode:WIN_VK_DECIMAL,
+ modifiers:{numLockKey:1}, chars:"."},
+ ".", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
+
+ // Dead keys on any layouts
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{}, chars:"^^"},
+ ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "^^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E2"},
+ ["\u00E2", "\u00E2", "a"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"\u00C2"},
+ ["\u00C2", "\u00C2", "A"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_Q,
+ modifiers:{}, chars:"^q"},
+ ["^q", "^", "q", "q"], "KeyA", KeyboardEvent.DOM_VK_Q, "^q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{shiftKey:1}, chars:"\u00A8\u00A8"},
+ ["\u00A8\u00A8", "\u00A8", "\u00A8", "\u00A8"], "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "\u00A8\u00A8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"\u00C4"},
+ ["\u00C4", "\u00C4", "A"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00C4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E4"},
+ ["\u00E4", "\u00E4", "a"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00E4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_Q,
+ modifiers:{shiftKey:1}, chars:"\u00A8Q"},
+ ["\u00A8Q", "\u00A8", "Q", "Q"], "KeyA", KeyboardEvent.DOM_VK_Q, "\u00A8Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{}, chars:"``"},
+ ["``", "`", "`", "`"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "``", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E0"},
+ ["\u00E0", "\u00E0", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"\u00C0"},
+ ["\u00C0", "\u00C0", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q,
+ modifiers:{}, chars:"`q"},
+ ["`q", "`", "q", "q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "`q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{shiftKey:1}, chars:"^^"},
+ ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "^^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"\u00C2"},
+ ["\u00C2", "\u00C2", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E2"},
+ ["\u00E2", "\u00E2", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q,
+ modifiers:{shiftKey:1}, chars:"^Q"},
+ ["^Q", "^", "Q", "Q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "^Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{}, chars:"\u00B4\u00B4"},
+ ["\u00B4\u00B4", "\u00B4", "\u00B4", "\u00B4"], "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00B4\u00B4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E1"},
+ ["\u00E1", "\u00E1", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"\u00C1"},
+ ["\u00C1", "\u00C1", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q,
+ modifiers:{}, chars:"\u00B4q"},
+ ["\u00B4q", "\u00B4", "q", "q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "\u00B4q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{shiftKey:1}, chars:"\u00A8\u00A8"},
+ ["\u00A8\u00A8", "\u00A8", "\u00A8", "\u00A8"], "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00A8\u00A8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"\u00C4"},
+ ["\u00C4", "\u00C4", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E4"},
+ ["\u00E4", "\u00E4", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7,
+ modifiers:{shiftKey:1}, chars:""},
+ "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q,
+ modifiers:{shiftKey:1}, chars:"\u00A8Q"},
+ ["\u00A8Q", "\u00A8", "Q", "Q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "\u00A8Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_1,
+ modifiers:{altGrKey:1}, chars:""},
+ "Dead", "BracketRight", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E3"},
+ ["\u00E3", "\u00E3", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_1,
+ modifiers:{ctrlKey:1, altKey:1}, chars:""},
+ "Dead", "BracketRight", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"\u00E3"},
+ ["\u00E3", "\u00E3", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ if (OS_VERSION >= WIN8) {
+ // On Russian Mnemonic layout, both 'KeyS' and 'KeyC' are dead key. However, the sequence 'KeyS' -> 'KeyC' causes a composite character.
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_S,
+ modifiers:{}, chars:""},
+ "Dead", "KeyS", KeyboardEvent.DOM_VK_S, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_S,
+ modifiers:{}, chars:"\u0441\u0441"},
+ ["\u0441\u0441", "\u0441", "\u0441", "\u0441"], "KeyS", KeyboardEvent.DOM_VK_S, "\u0441\u0441", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_C,
+ modifiers:{}, chars:""},
+ "Dead", "KeyC", KeyboardEvent.DOM_VK_C, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_C,
+ modifiers:{}, chars:"\u0446\u0446"},
+ ["\u0446\u0446", "\u0446", "\u0446", "\u0446"], "KeyC", KeyboardEvent.DOM_VK_C, "\u0446\u0446", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_S,
+ modifiers:{}, chars:""},
+ "Dead", "KeyS", KeyboardEvent.DOM_VK_S, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_C,
+ modifiers:{}, chars:"\u0449"},
+ ["\u0449", "\u0449", "\u0446"], "KeyC", KeyboardEvent.DOM_VK_C, "\u0449", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ }
+
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A,
+ modifiers:{altKey:1,ctrlKey:1}, chars:""},
+ "\u0444", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A,
+ modifiers:{altKey:1,ctrlKey:1,shiftKey:1}, chars:""},
+ "\u0424", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_T,
+ modifiers:{altKey:1,ctrlKey:1}, chars:""},
+ "\u0435", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_T,
+ modifiers:{altKey:1,ctrlKey:1,shiftKey:1}, chars:""},
+ "\u0415", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ // When Alt key is pressed, dead key sequence is generated with WM_SYSKEYDOWN, WM_SYSDEADCHAR, WM_SYSCHAR and WM_SYSKEYUP.
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1}, chars:"``"},
+ ["``", "`", "`", "`"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "``", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{altKey:1}, chars:"\u00E0"},
+ ["\u00E0", "\u00E0", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C0"},
+ ["\u00C0", "\u00C0", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q,
+ modifiers:{altKey:1}, chars:"`q"},
+ ["`q", "`", "q", "q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "`q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:"^^"},
+ ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "^^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{altKey:1, shiftKey:1}, chars:"\u00C2"},
+ ["\u00C2", "\u00C2", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A,
+ modifiers:{altKey:1}, chars:"\u00E2"},
+ ["\u00E2", "\u00E2", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1,
+ modifiers:{altKey:1, shiftKey:1}, chars:""},
+ "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+ yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q,
+ modifiers:{altKey:1, shiftKey:1}, chars:"^Q"},
+ ["^Q", "^", "Q", "Q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "^Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
+
+ cleanup();
+ }
+
+
+ if (IS_WIN) {
+ yield* testKeysOnWindows();
+ } else if (IS_MAC) {
+ yield* testKeysOnMac();
+ } else {
+ cleanup();
+ }
+}
+
+// Test the activation (or not) of an HTML accesskey
+function* runAccessKeyTests()
+{
+ var button = document.getElementById("button");
+ var activationCount;
+
+ function onClick(e)
+ {
+ ++activationCount;
+ }
+
+ // The first parameter is the complete input event. The second and third parameters are
+ // what to test against.
+ function testKey(aEvent, aAccessKey, aShouldActivate)
+ {
+ activationCount = 0;
+ button.setAttribute("accesskey", aAccessKey);
+
+ return synthesizeKey(aEvent, "button", function() {
+
+ var currentTestName = eventToString(aEvent);
+
+ is(activationCount, aShouldActivate ? 1 : 0,
+ currentTestName + ", activating '" + aAccessKey + "'");
+
+ continueTest();
+ });
+ }
+
+ button.addEventListener("click", onClick);
+
+ // These tests have to be per-plaform.
+ if (IS_MAC) {
+ // Basic sanity checks
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{}, chars:"a", unmodifiedChars:"a"},
+ "a", false);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "a", false);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "A", false);
+
+ // Shift-ctrl does not activate accesskeys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"},
+ "a", false);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"},
+ "A", false);
+ // Alt-ctrl activate accesskeys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "a", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"a"},
+ "A", true);
+
+ // Greek layout can activate a Latin accesskey
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"},
+ "a", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"},
+ "A", true);
+ // ... and a Greek accesskey!
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"},
+ "\u03b1", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"},
+ "\u0391", true);
+
+ // bug 359638
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period,
+ modifiers:{ctrlKey:1, altKey:1}, chars:".", unmodifiedChars:"."},
+ ".", true);
+
+ // German (KCHR/KeyTranslate case)
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"a", unmodifiedChars:"a"},
+ "a", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"a", unmodifiedChars:"a"},
+ "A", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u00fc", unmodifiedChars:"\u00fc"},
+ "\u00fc", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_LeftBracket,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u00fc", unmodifiedChars:"\u00fc"},
+ "\u00dc", true);
+ }
+ else if (IS_WIN) {
+ // Basic sanity checks
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"a"},
+ "a", false);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1, altKey:1}, chars:"A"},
+ "a", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1, altKey:1}, chars:"A"},
+ "A", true);
+
+ // shift-alt-ctrl does not activate accesskeys
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, shiftKey:1, altKey:1}, chars:""},
+ "a", false);
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, shiftKey:1, altKey:1}, chars:""},
+ "A", false);
+
+ // Greek layout can activate a Latin accesskey
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"},
+ "a", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"},
+ "A", true);
+ // ... and a Greek accesskey!
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"},
+ "\u03b1", true);
+ yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"},
+ "\u0391", true);
+
+ // bug 359638
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD,
+ modifiers:{shiftKey:1, altKey:1}, chars:".", unmodifiedChars:"."},
+ ".", true);
+ }
+
+ button.removeEventListener("click", onClick);
+}
+
+function* runXULKeyTests()
+{
+ var commandElements = {
+ expectedCommand: document.getElementById("expectedCommand"),
+ unexpectedCommand: document.getElementById("unexpectedCommand"),
+ expectedReservedCommand: document.getElementById("expectedReservedCommand")
+ };
+ // Enable all command elements.
+ for (var id in commandElements) {
+ commandElements[id].removeAttribute("disabled");
+ }
+
+ var keyEvents = [];
+
+ function onKeyInDefaultEventGroup(aDOMEvent)
+ {
+ if (isModifierKeyEvent(aDOMEvent)) {
+ return;
+ }
+ keyEvents.push({ type: aDOMEvent.type, group: "default", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) });
+ }
+
+ function onKeyInSystemEventGroup(aDOMEvent)
+ {
+ if (isModifierKeyEvent(aDOMEvent)) {
+ return;
+ }
+ keyEvents.push({ type: aDOMEvent.type, group: "system", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) });
+ }
+
+ var buttonParent = document.getElementById("button").parentNode;
+ buttonParent.addEventListener("keydown", onKeyInDefaultEventGroup, true);
+ buttonParent.addEventListener("keypress", onKeyInDefaultEventGroup, true);
+ buttonParent.addEventListener("keydown", onKeyInDefaultEventGroup);
+ buttonParent.addEventListener("keypress", onKeyInDefaultEventGroup);
+ SpecialPowers.addSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, true);
+ SpecialPowers.addSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, true);
+ SpecialPowers.addSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, false);
+ SpecialPowers.addSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, false);
+
+ function finializeKeyElementTest()
+ {
+ buttonParent.removeEventListener("keydown", onKeyInDefaultEventGroup, true);
+ buttonParent.removeEventListener("keypress", onKeyInDefaultEventGroup, true);
+ buttonParent.removeEventListener("keydown", onKeyInDefaultEventGroup);
+ buttonParent.removeEventListener("keypress", onKeyInDefaultEventGroup);
+ SpecialPowers.removeSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, true);
+ SpecialPowers.removeSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, true);
+ SpecialPowers.removeSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, false);
+ SpecialPowers.removeSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, false);
+ }
+
+ // If aKeyElement is empty string, this function tests if the event kicks
+ // no key elements.
+ function testKeyElement(aEvent, aKeyElementId)
+ {
+ var testName = "testKeyElement (with non-reserved command element): " + eventToString(aEvent) + " ";
+ var keyElement = aKeyElementId == "" ? null : document.getElementById(aKeyElementId);
+ if (keyElement) {
+ keyElement.setAttribute("command", "expectedCommand");
+ }
+
+ /* eslint-disable-next-line no-shadow */
+ for (var id in commandElements) {
+ commandElements[id].activeCount = 0;
+ }
+
+ keyEvents = [];
+ return synthesizeKey(aEvent, "button", function() {
+ if (keyElement) {
+ is(commandElements.expectedCommand.activeCount, 1, testName + "the command element (id='expectedCommand') should be preformed");
+ } else {
+ is(commandElements.expectedCommand.activeCount, 0, testName + "the command element (id='expectedCommand') shouldn't be preformed");
+ }
+ is(commandElements.unexpectedCommand.activeCount, 0, testName + "the command element (id='unexpectedCommand') shouldn't be preformed");
+ is(commandElements.expectedReservedCommand.activeCount, 0, testName + "the command element (id='expectedReservedCommand') shouldn't be preformed");
+
+ function checkFiredEvents()
+ {
+ let expectKeyPressEvent = aKeyElementId != "" ||
+ ((aEvent.modifiers.ctrlKey || aEvent.modifiers.altKey || aEvent.modifiers.metaKey) &&
+ (!IS_WIN || !aEvent.modifiers.altGrKey));
+ is(keyEvents.length, expectKeyPressEvent ? 8 : 4, testName + "wrong number events fired");
+ is(JSON.stringify(keyEvents[0]), JSON.stringify({ type: "keydown", group: "default", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the default event group #0");
+ is(JSON.stringify(keyEvents[1]), JSON.stringify({ type: "keydown", group: "default", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the default event group #1");
+
+ is(JSON.stringify(keyEvents[2]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the system event group #2");
+ is(JSON.stringify(keyEvents[3]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the system event group #3");
+
+ if (expectKeyPressEvent) {
+ is(JSON.stringify(keyEvents[4]), JSON.stringify({ type: "keypress", group: "default", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the default event group #4");
+ is(JSON.stringify(keyEvents[5]), JSON.stringify({ type: "keypress", group: "default", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the default event group #5");
+
+ is(JSON.stringify(keyEvents[6]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the system event group #6");
+ is(JSON.stringify(keyEvents[7]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the system event group #7");
+ }
+ }
+
+ checkFiredEvents();
+
+ if (keyElement) {
+ testName = "testKeyElement (with reserved command element): " + eventToString(aEvent) + " ";
+ keyElement.setAttribute("command", "expectedReservedCommand");
+
+ for (id in commandElements) {
+ commandElements[id].activeCount = 0;
+ }
+ keyEvents = [];
+ synthesizeKey(aEvent, "button", function() {
+ is(commandElements.expectedCommand.activeCount, 0, testName + "the command element (id='expectedCommand') shouldn't be preformed");
+ is(commandElements.unexpectedCommand.activeCount, 0, testName + "the command element (id='unexpectedCommand') shouldn't be preformed");
+ is(commandElements.expectedReservedCommand.activeCount, 1, testName + "the command element (id='expectedReservedCommand') should be preformed");
+
+ checkFiredEvents();
+
+ if (keyElement) {
+ keyElement.setAttribute("command", "unexpectedCommand");
+ }
+ continueTest();
+ });
+ } else {
+ if (keyElement) {
+ keyElement.setAttribute("command", "unexpectedCommand");
+ }
+ continueTest();
+ }
+ });
+ }
+
+ if (IS_MAC) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{metaKey:1}, chars:";", unmodifiedChars:";"},
+ "unshiftedKey");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon,
+ modifiers:{metaKey:1, shiftKey:1}, chars:";", unmodifiedChars:":"},
+ "shiftedKey");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{metaKey:1}, chars:"'", unmodifiedChars:"'"},
+ "reservedUnshiftedKey");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"\"", unmodifiedChars:"'"},
+ "reservedShiftedKey");
+ }
+ else if (IS_WIN) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{ctrlKey:1}, chars:""},
+ "unshiftedKey");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "shiftedKey");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{ctrlKey:1}, chars:""},
+ "reservedUnshiftedKey");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "reservedShiftedKey");
+ }
+
+ // 429160
+ if (IS_MAC) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
+ modifiers:{metaKey:1, altKey:1}, chars:"\u0192", unmodifiedChars:"f"},
+ "commandOptionF");
+ }
+ else if (IS_WIN) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"\u0006"},
+ "commandOptionF");
+ }
+
+ // 432112
+ if (IS_MAC) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_SWEDISH, keyCode:MAC_VK_ANSI_Minus,
+ modifiers:{metaKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"?"},
+ "question");
+ }
+ else if (IS_WIN) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_SWEDISH, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "question");
+ // For testing if Ctrl+? is kicked without Shift state, temporarily disable
+ // Ctrl-+ key element.
+ var unshiftedPlusKeyElement = document.getElementById("unshiftedPlus");
+ unshiftedPlusKeyElement.setAttribute("disabled", "true");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_SWEDISH, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{ctrlKey:1}, chars:""},
+ "");
+ unshiftedPlusKeyElement.removeAttribute("disabled");
+ }
+
+ // bug 433192
+ if (IS_WIN) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1}, chars:"\u0018"},
+ "unshiftedX");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"},
+ "shiftedX");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_ARABIC, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1}, chars:"\u0018"},
+ "unshiftedX");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_ARABIC, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"},
+ "shiftedX");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_HEBREW, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1}, chars:"\u0018"},
+ "unshiftedX");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_HEBREW, keyCode:WIN_VK_X,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"},
+ "shiftedX");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_JAPANESE, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "unshiftedPlus");
+ }
+
+ // bug 759346
+ if (IS_WIN) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_1,
+ modifiers:{ctrlKey:1}, chars:""},
+ "");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_1,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "unshiftedPlus");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{ctrlKey:1}, chars:""},
+ "unshiftedPlus");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_OEM_PLUS,
+ modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
+ "");
+ }
+
+ // bug 1596916
+ if (IS_WIN) {
+ // Ctrl + Alt + foo should be performed only when AltGr key is not
+ // pressed, i.e., only when Ctrl key and left Alt key are pressed if
+ // active keyboard layout has AltGr key.
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A,
+ modifiers:{altGrKey:1}, chars:""},
+ "");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, altGrKey:1}, chars:""},
+ "");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A,
+ modifiers:{altKey:1, altGrKey:1}, chars:""},
+ "");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:""},
+ "ctrlAltA");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, altKey:1, altGrKey:1}, chars:""},
+ "");
+ }
+
+ // bug 1874727
+ if (IS_WIN) {
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A,
+ modifiers:{altKey:1, ctrlKey:1}, chars:""},
+ "ctrlAltA");
+ yield testKeyElement({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A,
+ modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:""},
+ "ctrlAltShiftA");
+ }
+
+ for (id in commandElements) {
+ commandElements[id].setAttribute("disabled", "true");
+ }
+ finializeKeyElementTest();
+}
+
+function* runReservedKeyTests()
+{
+ var browser = document.getElementById("browser");
+ var contents = [
+ browser.contentWindow,
+ browser.contentDocument,
+ browser.contentDocument.documentElement,
+ browser.contentDocument.body,
+ browser.contentDocument.getElementById("content_button")
+ ];
+
+ for (var i = 0; i < contents.length; i++) {
+ contents[i].addEventListener("keydown", onKeyInDefaultEventGroup, true);
+ contents[i].addEventListener("keypress", onKeyInDefaultEventGroup, true);
+ contents[i].addEventListener("keydown", onKeyInDefaultEventGroup);
+ contents[i].addEventListener("keypress", onKeyInDefaultEventGroup);
+ SpecialPowers.addSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, true);
+ SpecialPowers.addSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, true);
+ SpecialPowers.addSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, false);
+ SpecialPowers.addSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, false);
+ }
+
+ var keyEvents = [];
+
+ function onKeyInDefaultEventGroup(aDOMEvent)
+ {
+ if (isModifierKeyEvent(aDOMEvent)) {
+ return;
+ }
+ keyEvents.push({ type: aDOMEvent.type, group: "default", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) });
+ }
+
+ function onKeyInSystemEventGroup(aDOMEvent)
+ {
+ if (isModifierKeyEvent(aDOMEvent)) {
+ return;
+ }
+ keyEvents.push({ type: aDOMEvent.type, group: "system", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) });
+ // prevents reserved default action
+ if (aDOMEvent.type == "keypress" &&
+ aDOMEvent.eventPhase == aDOMEvent.BUBBLING_PHASE &&
+ aDOMEvent.currentTarget == browser.contentWindow) {
+ aDOMEvent.preventDefault();
+ }
+ }
+
+ function finializeKeyElementTest()
+ {
+ /* eslint-disable-next-line no-shadow */
+ for (var i = 0; i < contents.length; i++) {
+ contents[i].removeEventListener("keydown", onKeyInDefaultEventGroup, true);
+ contents[i].removeEventListener("keypress", onKeyInDefaultEventGroup, true);
+ contents[i].removeEventListener("keydown", onKeyInDefaultEventGroup);
+ contents[i].removeEventListener("keypress", onKeyInDefaultEventGroup);
+ SpecialPowers.removeSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, true);
+ SpecialPowers.removeSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, true);
+ SpecialPowers.removeSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, false);
+ SpecialPowers.removeSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, false);
+ }
+ }
+
+ function testReservedKey(aEvent)
+ {
+ keyEvents = [];
+ return synthesizeKey(aEvent, "content_button", function() {
+ let testName = "testReservedKey: " + eventToString(aEvent) + " ";
+ is(keyEvents.length, 20, testName + "wrong number events fired");
+ is(JSON.stringify(keyEvents[0]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[0]) }), testName + "keydown event should be fired on content only in the system event group #0");
+ is(JSON.stringify(keyEvents[1]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[1]) }), testName + "keydown event should be fired on content only in the system event group #1");
+ is(JSON.stringify(keyEvents[2]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[2]) }), testName + "keydown event should be fired on content only in the system event group #2");
+ is(JSON.stringify(keyEvents[3]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[3]) }), testName + "keydown event should be fired on content only in the system event group #3");
+
+ is(JSON.stringify(keyEvents[4]), JSON.stringify({ type: "keydown", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keydown event should be fired on content only in the system event group #4");
+ is(JSON.stringify(keyEvents[5]), JSON.stringify({ type: "keydown", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keydown event should be fired on content only in the system event group #5");
+
+ is(JSON.stringify(keyEvents[6]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[3]) }), testName + "keydown event should be fired on content only in the system event group #6");
+ is(JSON.stringify(keyEvents[7]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[2]) }), testName + "keydown event should be fired on content only in the system event group #7");
+ is(JSON.stringify(keyEvents[8]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[1]) }), testName + "keydown event should be fired on content only in the system event group #8");
+ is(JSON.stringify(keyEvents[9]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[0]) }), testName + "keydown event should be fired on content only in the system event group #9");
+
+ is(JSON.stringify(keyEvents[10]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[0]) }), testName + "keypress event should be fired on content only in the system event group #10");
+ is(JSON.stringify(keyEvents[11]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[1]) }), testName + "keypress event should be fired on content only in the system event group #11");
+ is(JSON.stringify(keyEvents[12]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[2]) }), testName + "keypress event should be fired on content only in the system event group #12");
+ is(JSON.stringify(keyEvents[13]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[3]) }), testName + "keypress event should be fired on content only in the system event group #13");
+
+ is(JSON.stringify(keyEvents[14]), JSON.stringify({ type: "keypress", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keypress event should be fired on content only in the system event group #14");
+ is(JSON.stringify(keyEvents[15]), JSON.stringify({ type: "keypress", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keypress event should be fired on content only in the system event group #15");
+
+ is(JSON.stringify(keyEvents[16]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[3]) }), testName + "keypress event should be fired on content only in the system event group #16");
+ is(JSON.stringify(keyEvents[17]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[2]) }), testName + "keypress event should be fired on content only in the system event group #17");
+ is(JSON.stringify(keyEvents[18]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[1]) }), testName + "keypress event should be fired on content only in the system event group #18");
+ is(JSON.stringify(keyEvents[19]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[0]) }), testName + "keypress event should be fired on content only in the system event group #19");
+
+ continueTest();
+ });
+ }
+
+ if (IS_MAC) {
+ // Cmd+T is reserved for opening new tab.
+ yield testReservedKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
+ modifiers:{metaKey:1}, chars:"t", unmodifiedChars:"t"});
+ } else if (IS_WIN) {
+ // Ctrl+T is reserved for opening new tab.
+ yield testReservedKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
+ modifiers:{ctrlKey:1}, chars:"\u0014"});
+ yield testReservedKey({layout:KEYBOARD_LAYOUT_ARABIC, keyCode:WIN_VK_T,
+ modifiers:{ctrlKey:1}, chars:"\u0014"});
+ }
+
+ finializeKeyElementTest();
+}
+
+function* runTextInputTests()
+{
+ var textbox = document.getElementById("textbox");
+
+ function testKey(aEvent, aExpectText) {
+ textbox.value = "";
+ textbox.focus();
+
+ /* eslint-disable-next-line no-shadow */
+ var currentTestName = eventToString(aEvent);
+
+ // Check if the text comes with keypress events rather than composition events.
+ var keypress = 0;
+ /* eslint-disable-next-line no-shadow */
+ function onKeypress(aEvent) {
+ keypress++;
+ if (keypress == 1 && aExpectText == "") {
+ if (!aEvent.ctrlKey && !aEvent.altKey) {
+ is(aEvent.charCode, 0,
+ currentTestName + ", the charCode value should be 0 when it shouldn't cause inputting text");
+ }
+ return;
+ }
+ if (keypress > aExpectText.length) {
+ ok(false, currentTestName + " causes too many keypress events");
+ return;
+ }
+ is(aEvent.key, aExpectText[keypress - 1],
+ currentTestName + ", " + keypress + "th keypress event's key value should be '" + aExpectText[keypress - 1] + "'");
+ is(aEvent.charCode, aExpectText.charCodeAt(keypress - 1),
+ currentTestName + ", " + keypress + "th keypress event's charCode value should be 0x" + parseInt(aExpectText.charCodeAt(keypress - 1), 16));
+ }
+ textbox.addEventListener("keypress", onKeypress, true);
+
+ return synthesizeKey(aEvent, "textbox", () => {
+ textbox.removeEventListener("keypress", onKeypress, true);
+ if (aExpectText == "") {
+ if (aEvent.modifiers.ctrlKey || aEvent.modifiers.altKey) {
+ is(keypress, 1,
+ currentTestName + " should cause one keypress event because it should be available for shortcut key");
+ } else {
+ is(keypress, 0,
+ currentTestName + " should cause no keypress event because simple key press only with Shift/AltGraph " +
+ "or without any modifiers shouldn't match with a shortcut key");
+ }
+ } else {
+ is(keypress, aExpectText.length,
+ currentTestName + " should cause " + aExpectText.length + " keypress events");
+ is(textbox.value, aExpectText,
+ currentTestName + " does not input correct text.");
+ }
+
+ continueTest();
+ });
+ }
+
+ if (IS_MAC) {
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_G,
+ modifiers:{shiftKey:1}, chars:"\u0644\u0623", unmodifiedChars:"\u0644\u0623"},
+ "\u0644\u0623");
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_T,
+ modifiers:{shiftKey:1}, chars:"\u0644\u0625", unmodifiedChars:"\u0644\u0625"},
+ "\u0644\u0625");
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B,
+ modifiers:{shiftKey:1}, chars:"\u0644\u0622", unmodifiedChars:"\u0644\u0622"},
+ "\u0644\u0622");
+ yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B,
+ modifiers:{}, chars:"\u0644\u0627", unmodifiedChars:"\u0644\u0627"},
+ "\u0644\u0627");
+ } else if (IS_WIN) {
+ // Basic sanity checks
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{}, chars:"a"},
+ "a");
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{shiftKey:1}, chars:"A"},
+ "A");
+ // When Ctrl+Alt are pressed, any text should not be inputted.
+ yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:""},
+ "");
+ // AltGr should input only when it's mapped to a character
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_0,
+ modifiers:{altGrKey:1}, chars:"}"},
+ "}");
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A,
+ modifiers:{altGrKey:1}, chars:""},
+ "");
+ yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A,
+ modifiers:{ctrlKey:1, altKey:1}, chars:""},
+ "");
+
+ // Lithuanian AltGr should be consumed at 9/0 keys pressed
+ yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_8,
+ modifiers:{}, chars:"\u016B"},
+ "\u016B");
+ yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_9,
+ modifiers:{}, chars:"9"},
+ "9");
+ yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_0,
+ modifiers:{}, chars:"0"},
+ "0");
+ yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_8,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"8"},
+ "8");
+ yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_9,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"9"},
+ "9");
+ yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_0,
+ modifiers:{ctrlKey:1, altKey:1}, chars:"0"},
+ "0");
+ }
+
+ // XXX We need to move focus for canceling to search the autocomplete
+ // result. If we don't do here, Fx will crash at end of this tests.
+ document.getElementById("button").focus();
+}
+
+function* runAltRightKeyOnWindows()
+{
+ if (!IS_WIN) {
+ return;
+ }
+
+ var button = document.getElementById("button");
+ button.focus();
+
+ const kKeyboardLayouts = [
+ { layout: KEYBOARD_LAYOUT_ARABIC },
+ { layout: KEYBOARD_LAYOUT_BRAZILIAN_ABNT },
+ { layout: KEYBOARD_LAYOUT_EN_US },
+ { layout: KEYBOARD_LAYOUT_FRENCH },
+ { layout: KEYBOARD_LAYOUT_GREEK },
+ { layout: KEYBOARD_LAYOUT_GERMAN },
+ { layout: KEYBOARD_LAYOUT_HEBREW },
+ { layout: KEYBOARD_LAYOUT_JAPANESE },
+ { layout: KEYBOARD_LAYOUT_KHMER },
+ { layout: KEYBOARD_LAYOUT_LITHUANIAN },
+ { layout: KEYBOARD_LAYOUT_NORWEGIAN },
+ { layout: KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC,
+ canTestIt () { return OS_VERSION >= WIN8; } },
+ { layout: KEYBOARD_LAYOUT_SPANISH },
+ { layout: KEYBOARD_LAYOUT_SWEDISH },
+ { layout: KEYBOARD_LAYOUT_THAI },
+ ];
+ var events = [];
+ function pushEvent(aEvent) {
+ events.push(aEvent);
+ if (aEvent.key === "Alt") {
+ // Prevent working the menubar.
+ aEvent.preventDefault();
+ }
+ }
+ button.addEventListener("keydown", pushEvent);
+ button.addEventListener("keyup", pushEvent);
+
+ function testKey(aKeyboardLayout) {
+ return synthesizeKey({layout: aKeyboardLayout.layout, keyCode: WIN_VK_RMENU,
+ modifiers: {}, chars: ""}, "button", function() {
+ const kDescription =
+ "runAltRightKeyOnWindows(" + aKeyboardLayout.layout.currentTestName + "): ";
+ if (aKeyboardLayout.layout.hasAltGrOnWin) {
+ is(events.length, 4,
+ kDescription + "AltRight should fire 2 pairs of keydown and keyup events");
+ is(events[0].type, "keydown",
+ kDescription + "First event should be keydown of ControlLeft");
+ is(events[0].key, "Control",
+ kDescription + "First event should be keydown of ControlLeft whose key should be Control");
+ is(events[0].code, "ControlLeft",
+ kDescription + "First event should be keydown of ControlLeft");
+ is(events[0].location, KeyboardEvent.DOM_KEY_LOCATION_LEFT,
+ kDescription + "First event should be keydown of ControlLeft whose location should be DOM_KEY_LOCATION_LEFT");
+ is(events[0].keyCode, KeyboardEvent.DOM_VK_CONTROL,
+ kDescription + "First event should be keydown of ControlLeft whose keyCode should be DOM_VK_CONTROL");
+ is(events[0].ctrlKey, true,
+ kDescription + "First event should be keydown of ControlLeft whose ctrlKey should be true");
+ is(events[0].altKey, false,
+ kDescription + "First event should be keydown of ControlLeft whose altKey should be false");
+ is(events[0].getModifierState("AltGraph"), false,
+ kDescription + "First event should be keydown of ControlLeft whose getModifierState(\"AltGraph\") should be false");
+ is(events[1].type, "keydown",
+ kDescription + "Second event should be keydown of AltRight");
+ is(events[1].key, "AltGraph",
+ kDescription + "Second event should be keydown of AltRight whose key should be AltGraph");
+ is(events[1].code, "AltRight",
+ kDescription + "Second event should be keydown of AltRight");
+ is(events[1].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT,
+ kDescription + "Second event should be keydown of AltRight whose location should be DOM_KEY_LOCATION_RIGHT");
+ is(events[1].keyCode, KeyboardEvent.DOM_VK_ALT,
+ kDescription + "Second event should be keydown of AltRight whose keyCode should be DOM_VK_ALT");
+ is(events[1].ctrlKey, false,
+ kDescription + "Second event should be keydown of AltRight whose ctrlKey should be false");
+ is(events[1].altKey, false,
+ kDescription + "Second event should be keydown of AltRight whose altKey should be false");
+ is(events[1].getModifierState("AltGraph"), true,
+ kDescription + "Second event should be keydown of AltRight whose getModifierState(\"AltGraph\") should be true");
+ is(events[2].type, "keyup",
+ kDescription + "Third event should be keyup of ControlLeft");
+ is(events[2].key, "Control",
+ kDescription + "Third event should be keyup of ControlLeft whose key should be Control");
+ is(events[2].code, "ControlLeft",
+ kDescription + "Third event should be keyup of ControlLeft");
+ is(events[2].location, KeyboardEvent.DOM_KEY_LOCATION_LEFT,
+ kDescription + "Third event should be keyup of ControlLeft whose location should be DOM_KEY_LOCATION_LEFT");
+ is(events[2].keyCode, KeyboardEvent.DOM_VK_CONTROL,
+ kDescription + "Third event should be keyup of ControlLeft whose keyCode should be DOM_VK_CONTROL");
+ is(events[2].ctrlKey, false,
+ kDescription + "Third event should be keyup of ControlLeft whose ctrlKey should be false");
+ is(events[2].altKey, false,
+ kDescription + "Third event should be keyup of ControlLeft whose altKey should be false");
+ is(events[2].getModifierState("AltGraph"), true,
+ kDescription + "Third event should be keyup of ControlLeft whose getModifierState(\"AltGraph\") should be true");
+ is(events[3].type, "keyup",
+ kDescription + "Forth event should be keyup of AltRight");
+ is(events[3].key, "AltGraph",
+ kDescription + "Forth event should be keyup of AltRight whose key should be AltGraph");
+ is(events[3].code, "AltRight",
+ kDescription + "Forth event should be keyup of AltRight");
+ is(events[3].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT,
+ kDescription + "Forth event should be keyup of AltRight whose location should be DOM_KEY_LOCATION_RIGHT");
+ is(events[3].keyCode, KeyboardEvent.DOM_VK_ALT,
+ kDescription + "Forth event should be keyup of AltRight whose keyCode should be DOM_VK_ALT");
+ is(events[3].ctrlKey, false,
+ kDescription + "Third event should be keyup of AltRight whose ctrlKey should be false");
+ is(events[3].altKey, false,
+ kDescription + "Third event should be keyup of AltRight whose altKey should be false");
+ is(events[3].getModifierState("AltGraph"), false,
+ kDescription + "Third event should be keyup of AltRight whose getModifierState(\"AltGraph\") should be false");
+ } else {
+ is(events.length, 2,
+ kDescription + "AltRight should fire a pair of keydown and keyup events");
+ is(events[0].type, "keydown",
+ kDescription + "First event should be keydown of AltRight");
+ is(events[0].key, "Alt",
+ kDescription + "First event should be keydown of AltRight whose key should be Alt");
+ is(events[0].code, "AltRight",
+ kDescription + "First event should be keydown of AltRight");
+ is(events[0].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT,
+ kDescription + "First event should be keydown of AltRight whose location should be DOM_KEY_LOCATION_RIGHT");
+ is(events[0].keyCode, KeyboardEvent.DOM_VK_ALT,
+ kDescription + "First event should be keydown of AltRight whose keyCode should be DOM_VK_ALT");
+ is(events[0].ctrlKey, false,
+ kDescription + "First event should be keydown of AltRight whose ctrlKey should be false");
+ is(events[0].altKey, true,
+ kDescription + "First event should be keydown of AltRight whose altKey should be true");
+ is(events[0].getModifierState("AltGraph"), false,
+ kDescription + "First event should be keydown of AltRight whose getModifierState(\"AltGraph\") should be false");
+ is(events[1].type, "keyup",
+ kDescription + "Second event should be keyup of AltRight");
+ is(events[1].key, "Alt",
+ kDescription + "Second event should be keyup of AltRight whose key should be Alt");
+ is(events[1].code, "AltRight",
+ kDescription + "Second event should be keyup of AltRight");
+ is(events[1].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT,
+ kDescription + "Second event should be keyup of AltRight whose location should be DOM_KEY_LOCATION_RIGHT");
+ is(events[1].keyCode, KeyboardEvent.DOM_VK_ALT,
+ kDescription + "Second event should be keyup of AltRight whose keyCode should be DOM_VK_ALT");
+ is(events[1].ctrlKey, false,
+ kDescription + "Second event should be keyup of AltRight whose ctrlKey should be false");
+ is(events[1].altKey, false,
+ kDescription + "Second event should be keyup of AltRight whose altKey should be false");
+ is(events[1].getModifierState("AltGraph"), false,
+ kDescription + "Second event should be keyup of AltRight whose getModifierState(\"AltGraph\") should be false");
+ }
+
+ continueTest();
+ });
+ }
+
+ for (const kKeyboardLayout of kKeyboardLayouts) {
+ if (typeof kKeyboardLayout.canTestIt === "function" &&
+ !kKeyboardLayout.canTestIt()) {
+ continue;
+ }
+ events = [];
+ yield testKey(kKeyboardLayout);
+ }
+
+ button.addEventListener("keydown", pushEvent);
+ button.addEventListener("keyup", pushEvent);
+}
+
+function* runAllTests() {
+ yield* runKeyEventTests();
+ yield* runAccessKeyTests();
+ yield* runXULKeyTests();
+ yield* runReservedKeyTests();
+ yield* runTextInputTests();
+ yield* runAltRightKeyOnWindows();
+}
+
+var gTestContinuation = null;
+
+function continueTest()
+{
+ if (!gTestContinuation) {
+ gTestContinuation = runAllTests();
+ }
+ var ret = gTestContinuation.next();
+ if (ret.done) {
+ SimpleTest.finish();
+ } else {
+ is(ret.value, true, "Key synthesized successfully");
+ }
+}
+
+function runTest()
+{
+ if (!IS_MAC && !IS_WIN) {
+ todo(false, "This test is supported on MacOSX and Windows only. (Bug 431503)");
+ return;
+ }
+
+ if (IS_WIN && OS_VERSION >= WIN8) {
+ // Switching keyboard layout to Russian - Mnemonic causes 2 assertions in
+ // KeyboardLayout::LoadLayout().
+ const kAssertionCountDueToRussainMnemonic = 2 * 2;
+ SimpleTest.expectAssertions(kAssertionCountDueToRussainMnemonic,
+ kAssertionCountDueToRussainMnemonic);
+ }
+ SimpleTest.waitForExplicitFinish();
+
+ clearInfobars();
+
+ continueTest();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_keypress_event_with_alt_on_mac.html b/widget/tests/test_keypress_event_with_alt_on_mac.html
new file mode 100644
index 0000000000..01d4100f97
--- /dev/null
+++ b/widget/tests/test_keypress_event_with_alt_on_mac.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Testing if keypress event is fired when alt key is pressed</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+ <input id="input">
+ <input id="password" type="password">
+ <input id="readonly-input" readonly>
+ <textarea id="textarea"></textarea>
+ <textarea id="readonly-textarea" readonly></textarea>
+ <button id="button">button</button>
+</div>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+async function testNativeKey(aKeyboardLayout, aNativeKeyCode, aModifiers,
+ aChars, aUnmodifiedChars) {
+ // XXX Need to listen keyup event here because synthesizeNativeKey() does not
+ // guarantee that its callback will be called after "keypress" and "keyup".
+ let waitForKeyUp = new Promise(resolve => {
+ document.addEventListener("keyup", resolve, {once: true});
+ });
+ let keypress;
+ document.addEventListener("keypress", (aKeyPressEvent) => {
+ keypress = aKeyPressEvent;
+ }, {once: true});
+ synthesizeNativeKey(aKeyboardLayout, aNativeKeyCode, aModifiers, aChars, aUnmodifiedChars);
+ await waitForKeyUp;
+ return keypress;
+}
+
+async function runTests() {
+ const kTests =
+ [ { target: "input", isEditable: true },
+ { target: "password", isEditable: true },
+ { target: "readonly-input", isEditable: false },
+ { target: "textarea", isEditable: true },
+ { target: "readonly-textarea", isEditable: false },
+ { target: "button", isEditable: false } ];
+ for (const kTest of kTests) {
+ let element = document.getElementById(kTest.target);
+ element.focus();
+
+ const kDescription = kTest.target + ": ";
+
+ let keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {}, "a", "a");
+ ok(keypress,
+ kDescription + "'a' key press should cause firing keypress event");
+
+ keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {shiftKey: true}, "A", "A");
+ ok(keypress,
+ kDescription + "'a' key press with shift key should cause firing keypress event");
+ ok(keypress.shiftKey,
+ kDescription + "shiftKey of 'a' key press with shift key should be true");
+
+ // When a key inputs a character with option key, we need to unset altKey for our editor.
+ // Otherwise, altKey should be true for compatibility with the other browsers.
+ keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {altKey: true}, "\u00E5", "a");
+ ok(keypress,
+ kDescription + "'a' key press with option key should cause firing keypress event");
+ is(keypress.altKey, !kTest.isEditable,
+ kDescription + "altKey of 'a' key press with option key should be " + !kTest.isEditable);
+
+ keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {altKey: true, shiftKey: true}, "\u00C5", "A");
+ ok(keypress,
+ kDescription + "'a' key press with option key and shift key should cause firing keypress event");
+ is(keypress.altKey, !kTest.isEditable,
+ kDescription + "altKey of 'a' key press with option key and shift key should be " + !kTest.isEditable);
+ ok(keypress.shiftKey,
+ kDescription + "shiftKey of 'a' key press with option key and shift key should be true");
+
+ keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {ctrlKey: true}, "\u0001", "a");
+ ok(!keypress,
+ kDescription + "'a' key press with control key should not cause firing keypress event");
+
+ keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {altKey: true, ctrlKey: true}, "\u0001", "a");
+ ok(!keypress,
+ kDescription + "'a' key press with option key and control key should not cause firing keypress event");
+
+ // XXX Cannot test with command key for now since keyup event won't be fired due to macOS's limitation.
+
+ // Some keys of Arabic - PC keyboard layout do not input any character with option key.
+ // In such case, we shouldn't dispatch keypress event.
+ keypress = await testNativeKey(KEYBOARD_LAYOUT_ARABIC_PC, MAC_VK_ANSI_7, {altKey: true}, "", "\u0667");
+ ok(!keypress,
+ kDescription + "'7' key press with option key should not cause firing keypress event");
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/widget/tests/test_mouse_event_with_control_on_mac.html b/widget/tests/test_mouse_event_with_control_on_mac.html
new file mode 100644
index 0000000000..52ce206d35
--- /dev/null
+++ b/widget/tests/test_mouse_event_with_control_on_mac.html
@@ -0,0 +1,116 @@
+<html>
+<head>
+ <title>Test control+click on Mac</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpecialPowers.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+ <style>
+ #target {
+ width: 100px;
+ height: 100px;
+ background-color: lightgreen;
+ };
+ </style>
+</head>
+<body>
+<div id="target"></div>
+<script type="application/javascript">
+
+function waitAndCheckMouseEvents(aTarget, aExpectedEvents) {
+ return new Promise((aResolve, aReject) => {
+ let timer;
+ let cleanup = function() {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ aTarget.removeEventListener("mousedown", listener);
+ aTarget.removeEventListener("mouseup", listener);
+ aTarget.removeEventListener("contextmenu", listener);
+ aTarget.removeEventListener("click", listener);
+ aTarget.removeEventListener("auxclick", listener);
+ };
+
+ let listener = function(aEvent) {
+ aEvent.preventDefault();
+ let expectedEvent = aExpectedEvents.shift();
+ if (!expectedEvent) {
+ cleanup();
+ ok(false, `receive unexpected ${aEvent.type} event`);
+ aReject(new Error(`receive unexpected ${aEvent.type} event`));
+ return;
+ }
+
+ isDeeply([aEvent.type, aEvent.button, aEvent.ctrlKey], expectedEvent,
+ `check received ${aEvent.type} event`);
+ if (!aExpectedEvents.length) {
+ // Wait a bit to see if there is any unexpected event.
+ timer = setTimeout(function() {
+ cleanup();
+ aResolve();
+ }, 0);
+ }
+ };
+
+ aTarget.addEventListener("mousedown", listener);
+ aTarget.addEventListener("mouseup", listener);
+ aTarget.addEventListener("contextmenu", listener);
+ aTarget.addEventListener("click", listener);
+ aTarget.addEventListener("auxclick", listener);
+ });
+}
+
+add_task(async function Init() {
+ await SimpleTest.promiseFocus();
+ await waitUntilApzStable();
+
+ let target = document.getElementById("target");
+ target.addEventListener("click", function() {
+ ok(false, `should not receive click event`);
+ });
+});
+
+add_task(async function TestMouseClickWithControl() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.event.treat_ctrl_click_as_right_click.disabled", true]],
+ });
+
+ let target = document.getElementById("target");
+ let promise = waitAndCheckMouseEvents(target, [["mousedown", 0, true],
+ ["contextmenu", 0, true],
+ ["mouseup", 0, true]]);
+ synthesizeNativeMouseEvent({
+ type: "click",
+ target,
+ offsetX: 10,
+ offsetY: 10,
+ modifiers: { ctrlKey: true },
+ });
+ await promise;
+});
+
+add_task(async function TestOldBehavior() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.event.treat_ctrl_click_as_right_click.disabled", false]],
+ });
+
+ let target = document.getElementById("target");
+ let promise = waitAndCheckMouseEvents(target, [["mousedown", 2, true],
+ ["contextmenu", 2, true],
+ ["mouseup", 2, true],
+ ["auxclick", 2, true]]);
+ synthesizeNativeMouseEvent({
+ type: "click",
+ target,
+ offsetX: 10,
+ offsetY: 10,
+ modifiers: { ctrlKey: true },
+ });
+ await promise;
+});
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_mouse_scroll.xhtml b/widget/tests/test_mouse_scroll.xhtml
new file mode 100644
index 0000000000..82cb6a3ea3
--- /dev/null
+++ b/widget/tests/test_mouse_scroll.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Testing composition, text and query content events"
+ onload="setTimeout(onLoad, 0);"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+function onLoad()
+{
+ runTest();
+}
+
+function runTest()
+{
+ window.openDialog("window_mouse_scroll_win.html", "_blank",
+ "chrome,width=600,height=600,noopener", window);
+}
+]]>
+</script>
+</window>
diff --git a/widget/tests/test_native_key_bindings_mac.html b/widget/tests/test_native_key_bindings_mac.html
new file mode 100644
index 0000000000..8767a5a77d
--- /dev/null
+++ b/widget/tests/test_native_key_bindings_mac.html
@@ -0,0 +1,336 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Native Key Bindings for Cocoa Test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"></script>
+ </head>
+ <body>
+ <div id="editable" contenteditable>
+ <p>Stretching attack nullam stuck in a tree zzz, suspendisse cras nec
+ suspendisse lick suscipit. Nunc egestas amet litter box, nullam climb the
+ curtains biting I don't like that food tristique biting sleep on your
+ keyboard non. Lay down in your way cras nec tempus chase the red dot cras
+ nec, pharetra pharetra eat the grass leap run orci turpis attack.
+ Consectetur sleep in the sink eat I don't like that food, knock over the
+ lamp catnip in viverra tail flick zzz meow etiam enim. Ac ac hiss shed
+ everywhere kittens rhoncus, attack your ankles zzz iaculis kittens. Nullam
+ pellentesque rip the couch iaculis rhoncus nibh, give me fish orci turpis
+ purr sleep on your face quis nunc bibendum.</p>
+
+ <p>Neque jump on the table bat iaculis, adipiscing sleep on your keyboard
+ jump vel justo shed everywhere suspendisse lick. Zzz enim faucibus
+ hairball faucibus, pharetra sunbathe biting bat leap rip the couch attack.
+ Tortor nibh in viverra quis hairball nam, vulputate adipiscing sleep on
+ your keyboard purr knock over the lamp orci turpis. Vestibulum I don't
+ like that food et chase the red dot, adipiscing neque bibendum rutrum
+ accumsan quis rhoncus claw. Leap accumsan vehicula enim biting sleep on
+ your face, pharetra nam accumsan egestas kittens sunbathe. Pharetra chase
+ the red dot sniff non eat the grass, vulputate fluffy fur aliquam puking
+ judging you.</p>
+
+ <p>Claw purr sollicitudin sollicitudin lay down in your way consectetur,
+ pellentesque vehicula zzz orci turpis consectetur. I don't like that food
+ rhoncus pellentesque sniff attack, rhoncus tortor attack your ankles
+ iaculis scratched hiss vel. Tortor zzz tortor nullam rip the couch rutrum,
+ bat enim ut leap hairball iaculis. Bibendum sunbathe elit suspendisse
+ nibh, puking adipiscing sleep on your face sleep on your face zzz catnip.
+ Judging you rutrum bat sunbathe sleep on your face, jump on the table leap
+ tincidunt a faucibus sleep in the sink. Stuck in a tree tristique zzz hiss
+ in viverra nullam, quis tortor pharetra attack.</p>
+ </div>
+
+ <textarea id="textarea" cols="80">
+ Stretching attack nullam stuck in a tree zzz, suspendisse cras nec
+ suspendisse lick suscipit. Nunc egestas amet litter box, nullam climb the
+ curtains biting I don't like that food tristique biting sleep on your
+ keyboard non. Lay down in your way cras nec tempus chase the red dot cras
+ nec, pharetra pharetra eat the grass leap run orci turpis attack.
+ Consectetur sleep in the sink eat I don't like that food, knock over the
+ lamp catnip in viverra tail flick zzz meow etiam enim. Ac ac hiss shed
+ everywhere kittens rhoncus, attack your ankles zzz iaculis kittens. Nullam
+ pellentesque rip the couch iaculis rhoncus nibh, give me fish orci turpis
+ purr sleep on your face quis nunc bibendum.
+
+ Neque jump on the table bat iaculis, adipiscing sleep on your keyboard
+ jump vel justo shed everywhere suspendisse lick. Zzz enim faucibus
+ hairball faucibus, pharetra sunbathe biting bat leap rip the couch attack.
+ Tortor nibh in viverra quis hairball nam, vulputate adipiscing sleep on
+ your keyboard purr knock over the lamp orci turpis. Vestibulum I don't
+ like that food et chase the red dot, adipiscing neque bibendum rutrum
+ accumsan quis rhoncus claw. Leap accumsan vehicula enim biting sleep on
+ your face, pharetra nam accumsan egestas kittens sunbathe. Pharetra chase
+ the red dot sniff non eat the grass, vulputate fluffy fur aliquam puking
+ judging you.
+
+ Claw purr sollicitudin sollicitudin lay down in your way consectetur,
+ pellentesque vehicula zzz orci turpis consectetur. I don't like that food
+ rhoncus pellentesque sniff attack, rhoncus tortor attack your ankles
+ iaculis scratched hiss vel. Tortor zzz tortor nullam rip the couch rutrum,
+ bat enim ut leap hairball iaculis. Bibendum sunbathe elit suspendisse
+ nibh, puking adipiscing sleep on your face sleep on your face zzz catnip.
+ Judging you rutrum bat sunbathe sleep on your face, jump on the table leap
+ tincidunt a faucibus sleep in the sink. Stuck in a tree tristique zzz hiss
+ in viverra nullam, quis tortor pharetra attack.
+ </textarea>
+
+ <input id="input" type="text"
+ value="Stretching attack nullam stuck in a tree zzz, suspendisse cras nec
+ suspendisse lick suscipit. Nunc egestas amet litter box, nullam climb the
+ curtains biting I don't like that food tristique biting sleep on your
+ keyboard non. Lay down in your way cras nec tempus chase the red dot cras
+ nec, pharetra pharetra eat the grass leap run orci turpis attack.
+ Consectetur sleep in the sink eat I don't like that food, knock over the
+ lamp catnip in viverra tail flick zzz meow etiam enim. Ac ac hiss shed
+ everywhere kittens rhoncus, attack your ankles zzz iaculis kittens.
+ Nullam pellentesque rip the couch iaculis rhoncus nibh, give me fish orci
+ turpis purr sleep on your face quis nunc bibendum.">
+
+ <script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ let synthesizedKeys = [];
+ let expectations = [];
+
+ // Move to beginning of line
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_LeftArrow,
+ {ctrlKey: true}, "\uf702", "\uf702"]);
+ expectations.push({
+ editable: [0, 0],
+ textarea: [0, 0],
+ input: [0, 0],
+ });
+
+ // Move to end of line
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow,
+ {ctrlKey: true}, "\uf703", "\uf703"]);
+ expectations.push({
+ editable: [73, 73],
+ textarea: [72, 72],
+ input: [732, 732],
+ });
+
+ // Move down
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_N,
+ {ctrlKey: true}, "\u000e", "n"]);
+ expectations.push({
+ editable: [140, 140],
+ textarea: [145, 145],
+ input: [732, 732],
+ });
+
+ // Move to beginning of line
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_LeftArrow,
+ {ctrlKey: true}, "\uf702", "\uf702"]);
+ expectations.push({
+ editable: [73, 73],
+ textarea: [73, 73],
+ input: [0, 0],
+ });
+
+ // Move word right and modify selection
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow,
+ {altKey: true, shiftKey: true}, "\uf703", "\uf703"]);
+ expectations.push({
+ editable: [73, 84],
+ textarea: [73, 90],
+ input: [0, 10],
+ });
+
+ // Move word right
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow,
+ {altKey: true}, "\uf703", "\uf703"]);
+ expectations.push({
+ editable: [84, 84],
+ textarea: [90, 90],
+ input: [10, 10],
+ });
+
+ // Move word right
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow,
+ {altKey: true}, "\uf703", "\uf703"]);
+ expectations.push({
+ editable: [89, 89],
+ textarea: [95, 95],
+ input: [17, 17],
+ });
+
+ // Move down and modify selection
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_DownArrow,
+ {shiftKey: true}, "\uf701", "\uf701"]);
+ expectations.push({
+ editable: [89, 171],
+ textarea: [95, 175],
+ input: [17, 732],
+ });
+
+ // Move backward and modify selection
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_B,
+ {ctrlKey: true, shiftKey: true}, "\u0002", "B"]);
+ expectations.push({
+ editable: [89, 170],
+ textarea: [95, 174],
+ input: [17, 731],
+ });
+
+ // Delete forward
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_D,
+ {ctrlKey: true}, "\u0004", "d"]);
+ expectations.push({
+ editable: [89, 89],
+ textarea: [95, 95],
+ input: [17, 17],
+ });
+
+ // Delete backward
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_H,
+ {ctrlKey: true}, "\u0008", "h"]);
+ expectations.push({
+ editable: [88, 88],
+ textarea: [94, 94],
+ input: [16, 16],
+ });
+
+ // Move backward
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_B,
+ {ctrlKey: true}, "\u0002", "b"]);
+ expectations.push({
+ editable: [87, 87],
+ textarea: [93, 93],
+ input: [15, 15],
+ });
+
+ // Move to beginning of paragraph (line for now)
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A,
+ {ctrlKey: true}, "\u0001", "a"]);
+ expectations.push({
+ editable: [73, 73],
+ textarea: [73, 73],
+ input: [0, 0],
+ });
+
+ // Move forward
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_F,
+ {ctrlKey: true}, "\u0006", "f"]);
+ expectations.push({
+ editable: [74, 74],
+ textarea: [74, 74],
+ input: [1, 1],
+ });
+
+ // Move word right
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow,
+ {altKey: true}, "\uf703", "\uf703"]);
+ expectations.push({
+ editable: [84, 84],
+ textarea: [90, 90],
+ input: [10, 10],
+ });
+
+ // Move word right
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow,
+ {altKey: true}, "\uf703", "\uf703"]);
+ expectations.push({
+ editable: [88, 88],
+ textarea: [94, 94],
+ input: [17, 17],
+ });
+
+ // Delete to end of paragraph (line for now)
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_K,
+ {ctrlKey: true}, "\u000b", "k"]);
+ expectations.push({
+ editable: [88, 88],
+ textarea: [94, 94],
+ input: [17, 17],
+ });
+
+ // Move backward and modify selection
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_B,
+ {ctrlKey: true, shiftKey: true}, "\u0002", "B"]);
+ expectations.push({
+ editable: [88, 87],
+ textarea: [93, 94],
+ input: [16, 17],
+ });
+
+ // Move to end of paragraph (line for now)
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_E,
+ {ctrlKey: true}, "\u0005", "e"]);
+ expectations.push({
+ editable: [139, 139],
+ textarea: [94, 94],
+ input: [17, 17],
+ });
+
+ // Move up
+ synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_P,
+ {ctrlKey: true}, "\u0010", "p"]);
+ expectations.push({
+ editable: [73, 73],
+ textarea: [21, 21],
+ input: [0, 0],
+ });
+
+ function checkWindowSelection(aElement, aSelection) {
+ let selection = window.getSelection();
+
+ is(selection.anchorOffset, aSelection[aElement.id][0],
+ aElement.id + ": Incorrect anchor offset");
+ is(selection.focusOffset, aSelection[aElement.id][1],
+ aElement.id + ": Incorrect focus offset");
+ }
+
+ function checkElementSelection(aElement, aSelection) {
+ is(aElement.selectionStart, aSelection[aElement.id][0],
+ aElement.id + ": Incorrect selection start");
+ is(aElement.selectionEnd, aSelection[aElement.id][1],
+ aElement.id + ": Incorrect selection end");
+ }
+
+ function* testRun(aElement, aSelectionCheck, aCallback) {
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+
+ aElement.focus();
+
+ for (let i = 0; i < synthesizedKeys.length; i++) {
+ synthesizedKeys[i].push(function() {
+ aSelectionCheck(aElement, expectations[i]);
+ continueTest();
+ });
+ var synthOk = synthesizeNativeKey.apply(null, synthesizedKeys[i]);
+ synthesizedKeys[i].pop();
+ yield synthOk;
+ }
+ }
+
+ function* doTest() {
+ yield* testRun(document.getElementById("editable"), checkWindowSelection);
+ yield* testRun(document.getElementById("textarea"), checkElementSelection);
+ yield* testRun(document.getElementById("input"), checkElementSelection);
+ }
+
+ let gTestContinuation = null;
+
+ function continueTest() {
+ if (!gTestContinuation) {
+ gTestContinuation = doTest();
+ }
+ var ret = gTestContinuation.next();
+ if (ret.done) {
+ SimpleTest.finish();
+ } else {
+ is(ret.value, true, "Successfully synthesized key");
+ }
+ }
+
+ SimpleTest.waitForFocus(continueTest);
+ </script>
+ </body>
+</html>
diff --git a/widget/tests/test_native_menus.xhtml b/widget/tests/test_native_menus.xhtml
new file mode 100644
index 0000000000..d62c57f21c
--- /dev/null
+++ b/widget/tests/test_native_menus.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Native menu system tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("native_menus_window.xhtml", "NativeMenuWindow",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_panel_mouse_coords.xhtml b/widget/tests/test_panel_mouse_coords.xhtml
new file mode 100644
index 0000000000..43c4e10249
--- /dev/null
+++ b/widget/tests/test_panel_mouse_coords.xhtml
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=835044
+-->
+<window title="Mozilla Bug 835044"
+ onload="startTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<panel id="thepanel" level="parent"
+ onpopupshown="sendMouseEvent();"
+ onmousemove="checkCoords(event);"
+ style="width: 80px; height: 80px">
+</panel>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=835044"
+ id="anchor"
+ target="_blank">Mozilla Bug 835044</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+let panel = document.getElementById('thepanel');
+let rect = null;
+
+
+function startTest() {
+ // This first event is to ensure that the next event will have different
+ // coordinates to the previous mouse position, and so actually generates
+ // mouse events. The mouse is not moved off the window, as that might
+ // move focus to another application.
+ synthesizeNativeMouseEvent({
+ type: "mousemove",
+ screenX: window.mozInnerScreenX,
+ screenY: window.mozInnerScreenY,
+ elementOnWidget: window.documentElement,
+ });
+
+ panel.openPopup(document.getElementById("anchor"), "after_start");
+}
+
+function sendMouseEvent() {
+ rect = panel.getBoundingClientRect();
+ synthesizeNativeMouseEvent({
+ type: "mousemove",
+ target: panel,
+ offsetX: 10,
+ offsetY: 20,
+ });
+}
+
+function checkCoords(event) {
+ if (!rect) {
+ return;
+ }
+ isfuzzy(event.clientX, rect.left + 10, window.devicePixelRatio, "Motion x coordinate");
+ isfuzzy(event.clientY, rect.top + 20, window.devicePixelRatio, "Motion y coordinate");
+ info(`Event client: ${event.clientX}, ${event.clientY}, panel client: ${rect.left}, ${rect.top}`);
+ info(`Event screen: ${event.screenX}, ${event.screenY}, panel screen: ${panel.screenX}, ${panel.screenY}`);
+ info(`offset client: ${event.clientX - rect.left}, ${event.clientY - rect.top}`);
+ info(`offset screen: ${event.screenX - panel.screenX}, ${event.screenY - panel.screenY}`);
+ done();
+}
+
+function done() {
+ SimpleTest.finish();
+}
+ ]]>
+ </script>
+</window>
diff --git a/widget/tests/test_picker_no_crash.html b/widget/tests/test_picker_no_crash.html
new file mode 100644
index 0000000000..dbb75627b5
--- /dev/null
+++ b/widget/tests/test_picker_no_crash.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<title>Test for crashes when the parent window of a file picker is closed via script</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+SimpleTest.requestFlakyTimeout("testing we don't crash");
+
+async function testPicker(id) {
+ let childWindow = window.open("window_picker_no_crash_child.html", "childWindow", "width=500,height=500");
+ await SimpleTest.promiseFocus(childWindow);
+ ok(!childWindow.clicked, "Shouldn't have clicked");
+ synthesizeMouseAtCenter(childWindow.document.getElementById(id), {}, childWindow);
+ ok(childWindow.clicked, "Should have clicked");
+ childWindow.close();
+}
+
+add_task(async function test_simple() {
+ await testPicker("uploadbox");
+});
+
+add_task(async function test_multiple() {
+ await testPicker("multiple");
+});
+
+add_task(async function wait() {
+ await new Promise(r => setTimeout(r, 1000));
+ ok(true, "browser didn't crash");
+});
+</script>
diff --git a/widget/tests/test_platform_colors.xhtml b/widget/tests/test_platform_colors.xhtml
new file mode 100644
index 0000000000..8f9860d85b
--- /dev/null
+++ b/widget/tests/test_platform_colors.xhtml
@@ -0,0 +1,95 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Mac platform colors"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=518395">Mozilla Bug 518395</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<box id="colorbox"></box>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+var colors = {
+ "activeborder": ["rgb(0, 0, 0)"],
+ "activecaption": ["rgb(204, 204, 204)"],
+ "appworkspace": ["rgb(255, 255, 255)"],
+ "background": ["rgb(99, 99, 206)"],
+ "buttonface": ["rgb(240, 240, 240)"],
+ "buttonhighlight": ["rgb(255, 255, 255)"],
+ "buttonshadow": ["rgb(220, 220, 220)"],
+ "buttontext": ["rgb(0, 0, 0)"],
+ "captiontext": ["rgb(0, 0, 0)"],
+ "graytext": ["rgb(127, 127, 127)"],
+ "highlight": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"],
+ "highlighttext": ["rgb(255, 255, 255)", "rgb(255, 254, 254)", "rgb(254, 255, 254)"],
+ "inactiveborder": ["rgb(255, 255, 255)"],
+ "inactivecaption": ["rgb(255, 255, 255)"],
+ "inactivecaptiontext": ["rgb(69, 69, 69)"],
+ "infobackground": ["rgb(255, 255, 199)"],
+ "infotext": ["rgb(0, 0, 0)"],
+ "menu": ["rgb(255, 255, 255)", "rgb(254, 255, 254)", "rgb(255, 254, 254)"],
+ "menutext": ["rgb(0, 0, 0)"],
+ "scrollbar": ["rgb(170, 170, 170)"],
+ "threeddarkshadow": ["rgb(220, 220, 220)"],
+ "threedface": ["rgb(240, 240, 240)"],
+ "threedhighlight": ["rgb(255, 255, 255)"],
+ "threedlightshadow": ["rgb(218, 218, 218)"],
+ "threedshadow": ["rgb(224, 224, 224)"],
+ "window": ["rgb(255, 255, 255)"],
+ "windowframe": ["rgb(204, 204, 204)"],
+ "windowtext": ["rgb(0, 0, 0)"],
+ "-moz-activehyperlinktext": ["rgb(238, 0, 0)"],
+ "-moz-buttonhoverface": ["rgb(240, 240, 240)"],
+ "-moz-buttonhovertext": ["rgb(0, 0, 0)"],
+ "-moz-cellhighlight": ["rgb(212, 212, 212)", "rgb(220, 220, 220)"],
+ "-moz-cellhighlighttext": ["rgb(0, 0, 0)"],
+ "-moz-eventreerow": ["rgb(255, 255, 255)"],
+ "-moz-field": ["rgb(255, 255, 255)"],
+ "-moz-fieldtext": ["rgb(0, 0, 0)"],
+ "-moz-dialog": ["rgb(232, 232, 232)"],
+ "-moz-dialogtext": ["rgb(0, 0, 0)"],
+ "-moz-hyperlinktext": ["rgb(0, 0, 238)"],
+ "-moz-html-cellhighlight": ["rgb(212, 212, 212)"],
+ "-moz-html-cellhighlighttext": ["rgb(0, 0, 0)"],
+ "-moz-buttonactivetext": ["rgb(0, 0, 0)", "rgb(255, 255, 255)"],
+ "-moz-mac-defaultbuttontext": ["rgb(0, 0, 0)", "rgb(255, 255, 255)"],
+ //"-moz-mac-focusring": ["rgb(83, 144, 210)", "rgb(95, 112, 130)", "rgb(63, 152, 221)", "rgb(108, 126, 141)"],
+ "-moz-mac-menutextdisable": ["rgb(152, 152, 152)"],
+ "-moz-mac-menutextselect": ["rgb(255, 255, 255)"],
+ "-moz-mac-disabledtoolbartext": ["rgb(127, 127, 127)"],
+ "-moz-menuhover": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"],
+ "-moz-menuhovertext": ["rgb(255, 255, 255)", "rgb(255, 254, 254)", "rgb(254, 255, 254)"],
+ //"-moz-menubarhovertext": ["rgb(255, 255, 255)"],
+ "-moz-oddtreerow": ["rgb(236, 242, 254)", "rgb(240, 240, 240)", "rgb(243, 245, 250)", "rgb(243, 246, 250)", "rgb(245, 245, 245)"],
+ "-moz-visitedhyperlinktext": ["rgb(85, 26, 139)"],
+ "currentcolor": ["rgb(0, 0, 0)"],
+ "-moz-comboboxtext": ["rgb(0, 0, 0)"],
+ "-moz-combobox": ["rgb(255, 255, 255)"]
+};
+
+var colorbox = document.getElementById('colorbox');
+
+for (var c in colors) {
+ dump("testing color " + c + "\n");
+ colorbox.style.backgroundColor = c;
+ var rgb = document.defaultView.getComputedStyle(colorbox).getPropertyValue('background-color');
+ ok(colors[c].includes(rgb) || colors[c].length == 8, 'platform color ' + c + ' is wrong: ' + rgb);
+}
+
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_position_on_resize.xhtml b/widget/tests/test_position_on_resize.xhtml
new file mode 100644
index 0000000000..a7c5551018
--- /dev/null
+++ b/widget/tests/test_position_on_resize.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window title="Window Position On Resize Test"
+ onload="startTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<script class="testbody" type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+ let win, x, y;
+
+ function startTest() {
+ win = window.browsingContext.topChromeWindow.openDialog(
+ "about:blank",
+ null,
+ "chrome,dialog=no,outerHeight=170,outerWidth=200");
+ waitForSuccess(function() { return SpecialPowers.DOMWindowUtils.paintCount },
+ "No paint received", checkInitialSize);
+ }
+
+ function checkInitialSize() {
+ is(win.outerHeight,170, "initial outerHeight");
+ is(win.outerWidth, 200, "initial outerWidth");
+ x = win.screenX;
+ y = win.screenY;
+ shrink();
+ }
+ function shrink() {
+ win.resizeTo(180, 160);
+ waitForSuccess(function() { return win.outerHeight == 160 },
+ "outerHeight did not change to 160", checkShrink);
+ }
+ function checkShrink() {
+ is(win.outerWidth, 180, "resized outerWidth");
+ is(win.screenY, y, "resized window top should not change");
+ y = win.screenY;
+ restore();
+ }
+ function restore() {
+ win.resizeBy(20, 10);
+ waitForSuccess(function() { return win.outerHeight == 170 },
+ "outerHeight did not change to 170", checkRestore);
+ }
+ function checkRestore() {
+ is(win.outerWidth, 200, "restored outerWidth");
+ is(win.screenX, x, "restored window left should not change");
+ is(win.screenY, y, "restored window top should not change");
+ done();
+ }
+ function done() {
+ win.close();
+ SimpleTest.finish();
+ }
+
+ function waitForSuccess(testForSuccess, failureMsg, nextFunc) {
+ var waitCount = 0;
+
+ function repeatWait() {
+ ++waitCount;
+
+ if (testForSuccess()) {
+ nextFunc();
+ }
+ else if (waitCount > 50) {
+ ok(false, failureMsg);
+ nextFunc();
+ } else {
+ setTimeout(repeatWait, 100);
+ }
+ }
+
+ repeatWait();
+ }
+]]></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display">
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+</window>
diff --git a/widget/tests/test_secure_input.html b/widget/tests/test_secure_input.html
new file mode 100644
index 0000000000..846465b4c2
--- /dev/null
+++ b/widget/tests/test_secure_input.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for secure input mode</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<p>
+ <input id="input_text" type="text"><br>
+ <input id="input_password" type="password"><br>
+ <input id="input_text_readonly" type="text" readonly><br>
+ <input id="input_text_ime_mode_disabled" type="text" style="ime-mode: disabled;"><br>
+ <input id="input_change" type="text"><br>
+ <textarea id="textarea"></textarea><br>
+</p>
+<div id="contenteditable" contenteditable style="min-height: 3em;"></div>
+
+<script class="testbody" type="application/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ function sendAKeyEvent() {
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {}, "a", "a");
+ }
+
+ function isFocused(aElement) {
+ return (SpecialPowers.focusManager.focusedElement == aElement);
+ }
+
+ function runTest() {
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on the document");
+ $("input_text").focus();
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <input type=\"text\">");
+ $("input_password").focus();
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <input type=\"password\">");
+ $("input_password").blur();
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on the document after blur() of <input type=\"password\">");
+ $("input_password").focus();
+ $("input_text_readonly").focus();
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <input type=\"text\" readonly>");
+ $("input_password").focus();
+ $("input_text_ime_mode_disabled").focus();
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <input type=\"text\" style=\"ime-mode: disabled;\">");
+ $("input_password").focus();
+ $("textarea").focus();
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <textarea>");
+ $("input_password").focus();
+ $("contenteditable").focus();
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <div contenteditable>");
+
+ $("input_change").focus();
+ $("input_change").type = "password";
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <input type=\"password\"> changed from type=\"text\"");
+ $("input_change").type = "text";
+ sendAKeyEvent();
+ ok(true, "Not crashed: input on <input type=\"text\"> changed from type=\"password\"");
+
+ var otherWindow =
+ window.browsingContext.topChromeWindow.open("file_secure_input.html",
+ "_blank", "chrome,width=100,height=100");
+ ok(otherWindow, "failed to open other window");
+ if (!otherWindow) {
+ SimpleTest.finish();
+ return;
+ }
+
+ $("input_text").focus();
+ otherWindow.focus();
+
+ SimpleTest.waitForFocus(function() {
+ window.focus();
+ sendAKeyEvent();
+ ok(isFocused($("input_text")), "focused element isn't <input type=\"text\">");
+ ok(true, "Not crashed: input on <input type=\"text\"> after the other document has focus");
+
+ $("input_password").focus();
+ otherWindow.focus();
+ window.focus();
+ sendAKeyEvent();
+ ok(isFocused($("input_password")), "focused element isn't <input type=\"password\">");
+ ok(true, "Not crashed: input on <input type=\"password\"> after the other document has focus");
+
+ $("input_text").focus();
+ otherWindow.focus();
+ otherWindow.document.getElementById("text").focus();
+ window.focus();
+ sendAKeyEvent();
+ ok(isFocused($("input_text")), "focused element isn't <input type=\"text\">");
+ ok(true, "Not crashed: input on <input type=\"text\"> after the other document's <input type=\"text\"> has focus");
+
+ $("input_password").focus();
+ otherWindow.focus();
+ otherWindow.document.getElementById("text").focus();
+ window.focus();
+ sendAKeyEvent();
+ ok(isFocused($("input_password")), "focused element isn't <input type=\"password\">");
+ ok(true, "Not crashed: input on <input type=\"password\"> after the other document's <input type=\"text\"> has focus");
+
+ $("input_text").focus();
+ otherWindow.focus();
+ otherWindow.document.getElementById("password").focus();
+ window.focus();
+ sendAKeyEvent();
+ ok(isFocused($("input_text")), "focused element isn't <input type=\"text\">");
+ ok(true, "Not crashed: input on <input type=\"text\"> after the other document's <input type=\"password\"> has focus");
+
+ $("input_password").focus();
+ otherWindow.focus();
+ otherWindow.document.getElementById("password").focus();
+ window.focus();
+ sendAKeyEvent();
+ ok(isFocused($("input_password")), "focused element isn't <input type=\"password\">");
+ ok(true, "Not crashed: input on <input type=\"password\"> after the other document's <input type=\"password\"> has focus");
+
+ SimpleTest.finish();
+ }, otherWindow);
+ }
+
+ SimpleTest.waitForFocus(runTest);
+</script>
+</body>
+</html>
diff --git a/widget/tests/test_sizemode_events.xhtml b/widget/tests/test_sizemode_events.xhtml
new file mode 100644
index 0000000000..bd1e3a38d1
--- /dev/null
+++ b/widget/tests/test_sizemode_events.xhtml
@@ -0,0 +1,148 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Test for bug 715867"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody">
+<![CDATA[
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+let gWindow = null;
+
+const kIsLinux = navigator.platform.includes("Lin");
+const kIsMacOS = navigator.platform.includes("Mac");
+// On Linux and macOS sizemode changes might be async.
+const kAsyncChanges = kIsLinux || kIsMacOS;
+
+let gSizeModeDidChange = false;
+let gSizeModeDidChangeTo = 0;
+
+function sizemodeChanged(e) {
+ gSizeModeDidChange = true;
+ gSizeModeDidChangeTo = gWindow.windowState;
+}
+
+async function expectSizeModeChange(newMode, duringActionCallback) {
+ gSizeModeDidChange = false;
+
+ let promise = null;
+ if (kAsyncChanges) {
+ if (newMode) {
+ promise = new Promise(resolve => {
+ gWindow.addEventListener("sizemodechange", function() {
+ SimpleTest.executeSoon(resolve);
+ }, { once: true })
+ });
+ } else {
+ promise = new Promise(SimpleTest.executeSoon);
+ }
+ }
+
+ duringActionCallback();
+
+ if (promise) {
+ await promise;
+ }
+
+ if (newMode == 0) {
+ // No change should have taken place, no event should have fired.
+ ok(!gSizeModeDidChange, "No sizemodechange event should have fired.");
+ } else {
+ // Size mode change event was expected to fire.
+ ok(gSizeModeDidChange, "A sizemodechanged event should have fired.");
+ is(gSizeModeDidChangeTo, newMode, "The new sizemode should have the expected value.");
+ const expectedHidden = newMode == gWindow.STATE_MINIMIZED || gWindow.isFullyOccluded;
+ if (gWindow.document.hidden != expectedHidden) {
+ await new Promise(resolve => {
+ gWindow.addEventListener("visibilitychange", resolve, { once: true });
+ });
+ }
+ is(gWindow.document.hidden, expectedHidden, "Should be inactive if minimized or occluded.");
+ }
+}
+
+function startTest() {
+ openWindow();
+}
+
+function openWindow() {
+ gWindow = window.browsingContext.topChromeWindow
+ .open('empty_window.xhtml', '_blank', 'chrome,screenX=50,screenY=50,width=200,height=200,resizable');
+ SimpleTest.waitForFocus(runTest, gWindow);
+}
+
+async function runTest() {
+ // Install event handler.
+ gWindow.addEventListener("sizemodechange", sizemodeChanged);
+
+ // Run tests.
+ info("Testing minimize()");
+ await expectSizeModeChange(gWindow.STATE_MINIMIZED, function () {
+ gWindow.minimize();
+ });
+
+ info("Testing restore() after minimize()");
+ await expectSizeModeChange(gWindow.STATE_NORMAL, function () {
+ gWindow.restore();
+ });
+
+ info("Testing maximize()");
+ await expectSizeModeChange(gWindow.STATE_MAXIMIZED, function () {
+ gWindow.maximize();
+ });
+
+ info("Testing restore() after maximize()");
+ await expectSizeModeChange(gWindow.STATE_NORMAL, function () {
+ gWindow.restore();
+ });
+
+ // Normal window resizing shouldn't fire a sizemodechanged event, bug 715867.
+ info("Testing resizeTo() horizontal");
+ await expectSizeModeChange(0, function () {
+ gWindow.resizeTo(gWindow.outerWidth + 10, gWindow.outerHeight);
+ });
+
+ info("Testing resizeTo() vertical");
+ await expectSizeModeChange(0, function () {
+ gWindow.resizeTo(gWindow.outerWidth, gWindow.outerHeight + 10);
+ });
+
+ // Resizing a maximized window should change to normal sizemode.
+ info("maximize() in preparation for resize");
+ await expectSizeModeChange(gWindow.STATE_MAXIMIZED, function () {
+ gWindow.maximize();
+ });
+
+ info("Testing resizeTo() from maximized");
+ await expectSizeModeChange(gWindow.STATE_NORMAL, function () {
+ // MacOS treats windows close to the available screen size as maximized.
+ // Shrinking the window by only 10px isn't enough to change the sizemode.
+ gWindow.resizeTo(gWindow.outerWidth / 2, gWindow.outerHeight / 2);
+ });
+
+ gWindow.removeEventListener("sizemodechange", sizemodeChanged);
+ gWindow.close();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(startTest);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_standalone_native_menu.xhtml b/widget/tests/test_standalone_native_menu.xhtml
new file mode 100644
index 0000000000..96e41036c3
--- /dev/null
+++ b/widget/tests/test_standalone_native_menu.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Standalone Native Menu tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("standalone_native_menu_window.xhtml", "StandaloneNativeMenuWindow",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_surrogate_pair_native_key_handling.xhtml b/widget/tests/test_surrogate_pair_native_key_handling.xhtml
new file mode 100644
index 0000000000..98834e1206
--- /dev/null
+++ b/widget/tests/test_surrogate_pair_native_key_handling.xhtml
@@ -0,0 +1,178 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window id="window1" title="Test handling of native key input for surrogate pairs"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(async () => {
+ function promiseSynthesizeNativeKey(
+ aNativeKeyCode,
+ aChars,
+ ) {
+ return new Promise(resolve => {
+ synthesizeNativeKey(
+ KEYBOARD_LAYOUT_EN_US,
+ aNativeKeyCode,
+ {},
+ aChars,
+ aChars,
+ resolve
+ );
+ });
+ }
+ function getEventData(aEvent) {
+ return `{ type: "${aEvent.type}", key: "${aEvent.key}", code: "${
+ aEvent.code
+ }", keyCode: 0x${
+ aEvent.keyCode.toString(16).toUpperCase()
+ }, charCode: 0x${aEvent.charCode.toString(16).toUpperCase()} }`;
+ }
+ function getEventArrayData(aEvents) {
+ if (!aEvents.length) {
+ return "[]";
+ }
+ let result = "[\n";
+ for (const e of aEvents) {
+ result += ` ${getEventData(e)}\n`;
+ }
+ return result + "]";
+ }
+ let events = [];
+ function onKey(aEvent) {
+ events.push(aEvent);
+ }
+ window.addEventListener("keydown", onKey);
+ window.addEventListener("keypress", onKey);
+ window.addEventListener("keyup", onKey);
+
+ async function runTests(
+ aTestPerSurrogateKeyPress,
+ aTestIllFormedUTF16KeyValue = false
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
+ ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
+ ],
+ });
+ const settingDescription =
+ `aTestPerSurrogateKeyPress=${
+ aTestPerSurrogateKeyPress
+ }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
+ const allowIllFormedUTF16 =
+ aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
+
+ // If the keyboard layout has a key to introduce a surrogate pair,
+ // one set of WM_KEYDOWN and WM_KEYUP are generated and it's translated
+ // to two WM_CHARs.
+ await (async function test_one_key_press() {
+ events = [];
+ await promiseSynthesizeNativeKey(WIN_VK_A, "\uD83E\uDD14");
+ const keyCodeA = "A".charCodeAt(0);
+ is(
+ getEventArrayData(events),
+ getEventArrayData(
+ // eslint-disable-next-line no-nested-ternary
+ aTestPerSurrogateKeyPress
+ ? (
+ allowIllFormedUTF16
+ ? [
+ { type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
+ { type: "keypress", key: "\uD83E", code: "KeyA", keyCode: 0, charCode: 0xD83E },
+ { type: "keypress", key: "\uDD14", code: "KeyA", keyCode: 0, charCode: 0xDD14 },
+ { type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
+ ]
+ : [
+ { type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
+ { type: "keypress", key: "\uD83E\uDD14", code: "KeyA", keyCode: 0, charCode: 0xD83E },
+ { type: "keypress", key: "", code: "KeyA", keyCode: 0, charCode: 0xDD14 },
+ { type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
+ ]
+ )
+ : [
+ { type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
+ { type: "keypress", key: "\uD83E\uDD14", code: "KeyA", keyCode: 0, charCode: 0x1F914 },
+ { type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
+ ]
+ ),
+ `test_one_key_press(${
+ settingDescription
+ }): Typing surrogate pair should cause one set of keydown and keyup events with ${
+ aTestPerSurrogateKeyPress ? "2 keypress events" : "a keypress event"
+ }`
+ );
+ })();
+
+ // If a surrogate pair is sent with SendInput API, 2 sets of keyboard
+ // events are generated. E.g., Emojis in the touch keyboard on Win10 or
+ // later.
+ await (async function test_send_input() {
+ events = [];
+ // Virtual keycode for the WM_KEYDOWN/WM_KEYUP is VK_PACKET.
+ await promiseSynthesizeNativeKey(WIN_VK_PACKET, "\uD83E");
+ await promiseSynthesizeNativeKey(WIN_VK_PACKET, "\uDD14");
+ // WM_KEYDOWN, WM_CHAR and WM_KEYUP for the high surrogate input
+ // shouldn't cause DOM events.
+ is(
+ getEventArrayData(events),
+ getEventArrayData(
+ // eslint-disable-next-line no-nested-ternary
+ aTestPerSurrogateKeyPress
+ ? (
+ allowIllFormedUTF16
+ ? [
+ { type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
+ { type: "keypress", key: "\uD83E", code: "", keyCode: 0, charCode: 0xD83E },
+ { type: "keypress", key: "\uDD14", code: "", keyCode: 0, charCode: 0xDD14 },
+ { type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
+ ]
+ : [
+ { type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
+ { type: "keypress", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0xD83E },
+ { type: "keypress", key: "", code: "", keyCode: 0, charCode: 0xDD14 },
+ { type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
+ ]
+ )
+ : [
+ { type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
+ { type: "keypress", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0x1F914 },
+ { type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
+ ]
+ ),
+ `test_send_input(${
+ settingDescription
+ }): Inputting surrogate pair should cause one set of keydown and keyup events ${
+ aTestPerSurrogateKeyPress ? "2 keypress events" : "a keypress event"
+ }`
+ );
+ })();
+ }
+
+ await runTests(true, true);
+ await runTests(true, false);
+ await runTests(false);
+
+ window.removeEventListener("keydown", onKey);
+ window.removeEventListener("keypress", onKey);
+ window.removeEventListener("keyup", onKey);
+
+ SimpleTest.finish();
+ });
+ ]]></script>
+
+</window>
diff --git a/widget/tests/test_system_font_changes.xhtml b/widget/tests/test_system_font_changes.xhtml
new file mode 100644
index 0000000000..036c775463
--- /dev/null
+++ b/widget/tests/test_system_font_changes.xhtml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Native menu system tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("system_font_changes.xhtml", "system_font_changes_window",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/test_system_status_bar.xhtml b/widget/tests/test_system_status_bar.xhtml
new file mode 100644
index 0000000000..f2348fa6f5
--- /dev/null
+++ b/widget/tests/test_system_status_bar.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Testing composition, text and query content events"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<menupopup id="menuContainer">
+ <menu id="menu1" image="data:image/svg+xml,&lt;svg%20xmlns=&quot;http://www.w3.org/2000/svg&quot;%20width=&quot;32&quot;%20height=&quot;32&quot;>&lt;circle%20cx=&quot;16&quot;%20cy=&quot;16&quot;%20r=&quot;16&quot;/>&lt;/svg>">
+ <menupopup>
+ <menuitem label="Item 1 in menu 1"/>
+ <menuitem label="Item 2 in menu 1"/>
+ </menupopup>
+ </menu>
+ <menu id="menu2" image="data:image/svg+xml,&lt;svg%20xmlns=&quot;http://www.w3.org/2000/svg&quot;%20width=&quot;32&quot;%20height=&quot;32&quot;>&lt;path%20d=&quot;M0 16 L 16 0 L 32 16 L 16 32 Z&quot;/>&lt;/svg>">
+ <menupopup>
+ <menuitem label="Item 1 in menu 2"/>
+ <menuitem label="Item 2 in menu 2"/>
+ </menupopup>
+ </menu>
+</menupopup>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+ let systemStatusBar = Cc["@mozilla.org/widget/systemstatusbar;1"].getService(Ci.nsISystemStatusBar);
+ ok(systemStatusBar, "should have got an nsISystemStatusBar instance");
+
+ let menu1 = document.getElementById("menu1");
+ let menu2 = document.getElementById("menu2");
+
+ // Add and remove the item, just to get basic leak testing coverage.
+ systemStatusBar.addItem(menu1);
+ systemStatusBar.removeItem(menu1);
+
+ // Make sure that calling addItem twice with the same element doesn't leak.
+ systemStatusBar.addItem(menu2);
+ systemStatusBar.addItem(menu2);
+ systemStatusBar.removeItem(menu2);
+
+]]>
+</script>
+</window>
diff --git a/widget/tests/test_taskbar_progress.xhtml b/widget/tests/test_taskbar_progress.xhtml
new file mode 100644
index 0000000000..f2494a27bb
--- /dev/null
+++ b/widget/tests/test_taskbar_progress.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Taskbar Previews Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="loaded();">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script class="testbody" type="application/javascript">
+ <![CDATA[
+ let TP = Ci.nsITaskbarProgress;
+
+ function IsWin7OrHigher() {
+ try {
+ var ver = parseFloat(Services.sysinfo.getProperty("version"));
+ if (ver >= 6.1)
+ return true;
+ } catch (ex) { }
+ return false;
+ }
+
+ function winProgress() {
+ let taskbar = Cc["@mozilla.org/windows-taskbar;1"];
+ if (!taskbar) {
+ ok(false, "Taskbar service is always available");
+ return null;
+ }
+ taskbar = taskbar.getService(Ci.nsIWinTaskbar);
+
+ is(taskbar.available, IsWin7OrHigher(), "Expected availability of taskbar");
+ if (!taskbar.available)
+ return null;
+
+ // HACK from mconnor:
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let docShell = win.docShell;
+
+ let progress = taskbar.getTaskbarProgress(docShell);
+ isnot(progress, null, "Progress is not null");
+
+ try {
+ taskbar.getTaskbarProgress(null);
+ ok(false, "Got progress for null docshell");
+ } catch (e) {
+ ok(true, "Cannot get progress for null docshell");
+ }
+
+ return progress;
+ }
+
+ function macProgress() {
+ let progress = Cc["@mozilla.org/widget/macdocksupport;1"];
+ if (!progress) {
+ ok(false, "Should have gotten Mac progress service.");
+ return null;
+ }
+ return progress.getService(TP);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ function loaded()
+ {
+ let isWin = /Win/.test(navigator.platform);
+ let progress = isWin ? winProgress() : macProgress();
+ if (!TP || !progress) {
+ SimpleTest.finish();
+ return;
+ }
+
+ function shouldThrow(s,c,m) {
+ try {
+ progress.setProgressState(s,c,m);
+ return false;
+ } catch (e) {
+ return true;
+ }
+ }
+
+ function doesntThrow(s,c,m) {
+ return !shouldThrow(s,c,m);
+ }
+
+ ok(doesntThrow(TP.STATE_NO_PROGRESS, 0, 0), "No progress state can be set");
+ ok(doesntThrow(TP.STATE_INDETERMINATE, 0, 0), "Indeterminate state can be set");
+ ok(doesntThrow(TP.STATE_NORMAL, 0, 0), "Normal state can be set");
+ ok(doesntThrow(TP.STATE_ERROR, 0, 0), "Error state can be set");
+ ok(doesntThrow(TP.STATE_PAUSED, 0, 0), "Paused state can be set");
+
+ ok(shouldThrow(TP.STATE_NO_PROGRESS, 1, 1), "Cannot set no progress with values");
+ ok(shouldThrow(TP.STATE_INDETERMINATE, 1, 1), "Cannot set indeterminate with values");
+
+ // Technically passes since unsigned(-1) > 10
+ ok(shouldThrow(TP.STATE_NORMAL, -1, 10), "Cannot set negative progress");
+ todo(shouldThrow(TP.STATE_NORMAL, 1, -1), "Cannot set negative progress");
+ todo(shouldThrow(TP.STATE_NORMAL, -1, -1), "Cannot set negative progress");
+ todo(shouldThrow(TP.STATE_NORMAL, -2, -1), "Cannot set negative progress");
+
+ ok(shouldThrow(TP.STATE_NORMAL, 5, 3), "Cannot set progress greater than max");
+
+ ok(doesntThrow(TP.STATE_NORMAL, 1, 5), "Normal state can be set with values");
+ ok(doesntThrow(TP.STATE_ERROR, 3, 6), "Error state can be set with values");
+ ok(doesntThrow(TP.STATE_PAUSED, 2, 9), "Paused state can be set with values");
+
+ SimpleTest.finish();
+ }
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+</window>
diff --git a/widget/tests/test_textScaleFactor_system_font.html b/widget/tests/test_textScaleFactor_system_font.html
new file mode 100644
index 0000000000..2d0333e5fa
--- /dev/null
+++ b/widget/tests/test_textScaleFactor_system_font.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that system font sizing is independent from ui.textScaleFactor</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <style>
+ p { width: max-content }
+ #menu { font: menu }
+ </style>
+</head>
+<body>
+ <p id="menu">"menu" text.</p>
+ <p id="default">Default text.</p>
+</body>
+<script>
+"use strict";
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+// Returns a Number for the font size in CSS pixels.
+function elementFontSize(element) {
+ return parseFloat(getComputedStyle(element).getPropertyValue("font-size"));
+}
+
+// "look-and-feel-changed" may be dispatched twice: once for the pref
+// change and once after receiving the new values from
+// ContentChild::RecvThemeChanged().
+// pushPrefEnv() resolves after the former. This resolves after the latter.
+function promiseNewFontSizeOnThemeChange(element) {
+ return new Promise(resolve => {
+ const lastSize = elementFontSize(element);
+
+ function ThemeChanged() {
+ const size = elementFontSize(element);
+ if (size != lastSize) {
+ resolve(size);
+ }
+ }
+ // "look-and-feel-changed" is dispatched before the style system is flushed,
+ // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/layout/base/nsPresContext.cpp#1684,1703-1705
+ // so use an async observer to get a notification after style changes.
+ SpecialPowers.addAsyncObserver(ThemeChanged, "look-and-feel-changed");
+ SimpleTest.registerCleanupFunction(function() {
+ SpecialPowers.removeAsyncObserver(ThemeChanged, "look-and-feel-changed");
+ });
+ });
+}
+
+function fuzzyCompareLength(actual, expected, message, tolerance, expectFn) {
+ expectFn(Math.abs(actual-expected) <= tolerance,
+ `${message} - got ${actual}, expected ${expected} +/- ${tolerance}`);
+}
+
+add_task(async () => {
+ // MOZ_HEADLESS is set in content processes with GTK regardless of the
+ // headless state of the parent process. Check the parent state.
+ const headless = await SpecialPowers.spawnChrome([], function get_headless() {
+ return Services.env.get("MOZ_HEADLESS");
+ });
+ // LookAndFeel::TextScaleFactor::FloatID is implemented only for WINNT and
+ // GTK. ui.textScaleFactor happens to scale CSS pixels on other platforms
+ // but system font integration has not been implemented.
+ const expectSuccess = AppConstants.MOZ_WIDGET_TOOLKIT == "windows" ||
+ (AppConstants.MOZ_WIDGET_TOOLKIT == "gtk" &&
+ // Headless GTK doesn't get system font sizes from the system, but
+ // uses sizes fixed in CSS pixels.
+ !headless);
+
+ async function setScaleAndPromiseFontSize(scale, element) {
+ const prefPromise = SpecialPowers.pushPrefEnv({
+ set: [["ui.textScaleFactor", scale]],
+ });
+ if (!expectSuccess) {
+ // The size is not expected to change but get it afresh to check our
+ // assumption.
+ await prefPromise;
+ return elementFontSize(element);
+ }
+ const [size] = await Promise.all([
+ promiseNewFontSizeOnThemeChange(element),
+ prefPromise,
+ ]);
+ return size;
+ }
+
+ const menu = document.getElementById("menu");
+ const def = document.getElementById("default");
+ // Choose a scaleFactor value different enough from possible default values
+ // that app unit rounding does not prevent a change in devicePixelRatio.
+ // A scaleFactor of 120 also has no rounding of app units per dev pixel.
+ const referenceScale = 120;
+ const menuSize1 = await setScaleAndPromiseFontSize(referenceScale, menu);
+ const menuRect1 = menu.getBoundingClientRect();
+ const defSize1 = elementFontSize(def);
+ const defRect1 = def.getBoundingClientRect();
+
+ const expectFn = expectSuccess ? ok : todo;
+ const menuSize2 = await setScaleAndPromiseFontSize(2*referenceScale, menu);
+ {
+ const singlePrecisionULP = Math.pow(2, -23);
+ // Precision seems to be lost in the conversion to decimal string for the
+ // property value.
+ const reltolerance = 30 * singlePrecisionULP;
+ fuzzyCompareLength(menuSize2, menuSize1/2, "size of menu font",
+ reltolerance*menuSize1/2, expectFn);
+ }
+ {
+ const menuRect2 = menu.getBoundingClientRect();
+ // The menu font text renders exactly the same and app-unit rects are
+ // equal, but the DOMRect conversion is rounded to 1/65536 CSS pixels.
+ // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/dom/base/DOMRect.cpp#151-159
+ // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1640441#c28
+ const absTolerance = 1/65536
+ fuzzyCompareLength(menuRect2.width, menuRect1.width/2,
+ "width of menu font <p> in px", absTolerance, expectFn);
+ fuzzyCompareLength(menuRect2.height, menuRect1.height/2,
+ "height of menu font <p> in px", absTolerance, expectFn);
+ }
+
+ const defSize2 = elementFontSize(def);
+ is(defSize2, defSize1, "size of default font");
+ {
+ const defRect2 = def.getBoundingClientRect();
+ // Wider tolerance for hinting and snapping
+ const relTolerance = 1/12;
+ fuzzyCompareLength(defRect2.width, defRect1.width,
+ "width of default font <p> in px",
+ relTolerance*defRect1.width, ok);
+ fuzzyCompareLength(defRect2.height, defRect1.height,
+ "height of default font <p> in px",
+ relTolerance*defRect1.height, ok);
+ }
+});
+</script>
+</html>
diff --git a/widget/tests/test_transferable_overflow.xhtml b/widget/tests/test_transferable_overflow.xhtml
new file mode 100644
index 0000000000..dca9edcc61
--- /dev/null
+++ b/widget/tests/test_transferable_overflow.xhtml
@@ -0,0 +1,155 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="nsTransferable with large string"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="RunTest();">
+ <title>nsTransferable with large string</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ // This value is chosen such that the size of the memory for the string exceeds
+ // the kLargeDatasetSize threshold in nsTransferable.h (one million).
+ // Each character of a JS string is internally represented by two bytes,
+ // so the following string of length 500 001 uses 1 000 002 bytes.
+ const BIG_STRING = "x" + "BIGGY".repeat(100000);
+
+ // Some value with a length that is exactly kLargeDatasetSize (1 000 000).
+ const SMALL_STRING = "small".repeat(100000);
+
+ const nsTransferable = Components.Constructor("@mozilla.org/widget/transferable;1", "nsITransferable");
+ const nsSupportsString = Components.Constructor("@mozilla.org/supports-string;1", "nsISupportsString");
+
+ function assignTextToTransferable(transferable, string) {
+ var Suppstr = nsSupportsString();
+ Suppstr.data = string;
+ transferable.setTransferData("text/plain", Suppstr);
+ }
+
+ function checkTransferableText(transferable, expectedString, description) {
+ var data = {};
+ transferable.getTransferData("text/plain", data);
+ var actualValue = data.value.QueryInterface(Ci.nsISupportsString).data;
+ // Use ok + shortenString instead of is(...) to avoid dumping millions of characters in the output.
+ ok(actualValue === expectedString, description + ": text should match. " +
+ "Expected " + shortenString(expectedString) + ", got " + shortenString(actualValue));
+
+ function shortenString(str) {
+ return str && str.length > 30 ? str.slice(0, 10) + "..." + str.slice(-10) : String(str);
+ }
+ }
+
+ function isFDCountingSupported() {
+ // On on-Windows we can count the number of file handles for the current process,
+ // while on Windows we need to count the number of files in ${TempD}\mozilla-temp-files\,
+ // which can be unreliable, especially because nsAnonymousTemporaryFile has documented
+ // that the deletion might not be immediate.
+ //
+ // To avoid intermittents, we only check the file descriptor counts on non-Windows.
+ // test_bug1123480.xhtml will do some basic testing for Windows.
+ const {AppConstants} = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ return AppConstants.platform !== 'win';
+ }
+
+ function getClipboardCacheFDCount() {
+ var dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dir.initWithPath("/dev/fd");
+ var count = 0;
+ for (var de = dir.directoryEntries; de.hasMoreElements(); ) {
+ var fdFile = de.nextFile;
+ var fileSize;
+ try {
+ fileSize = fdFile.fileSize;
+ } catch (e) {
+ // This can happen on macOS.
+ continue;
+ }
+ if (fileSize === BIG_STRING.length * 2 ||
+ // We are not expecting files of this small size,
+ // but include them in the count anyway
+ // in case the files are unexpectedly created.
+ fileSize === SMALL_STRING.length * 2) {
+ // Assume that the file was created by us if the size matches.
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ function RunTest() {
+ const {PrivateBrowsingUtils} = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+ );
+
+ var win = window.browsingContext.topChromeWindow.open("about:blank", "_blank", "chrome, width=500, height=200");
+ ok(win, "should open window");
+ is(PrivateBrowsingUtils.isContentWindowPrivate(win), false, "used correct window context");
+
+ // ### Part 1 - Writing to the clipboard.
+
+ var Loadctx = PrivateBrowsingUtils.privacyContextFromWindow(win);
+ var Transfer = nsTransferable();
+ Transfer.init(Loadctx);
+ Transfer.addDataFlavor("text/plain");
+ var initialFdCount = isFDCountingSupported() ? getClipboardCacheFDCount() : -1;
+
+ assignTextToTransferable(Transfer, BIG_STRING);
+ checkTransferableText(Transfer, BIG_STRING, "transferable after assigning BIG_STRING");
+ if (isFDCountingSupported()) {
+ is(getClipboardCacheFDCount(), initialFdCount + 1, "should use a file for BIG_STRING");
+ }
+
+ // Share the transferable with the system.
+ Services.clipboard.setData(Transfer, null, Services.clipboard.kGlobalClipboard);
+
+ // Sanity check: Copying to the clipboard should not have altered the transferable.
+ checkTransferableText(Transfer, BIG_STRING, "transferable after copying to clipboard");
+ if (isFDCountingSupported()) {
+ // We are only counting file descriptors for the current process,
+ // so even if the test were to be multi-process and the parent process creates another
+ // nsTransferable, then the count should still be the same.
+ is(getClipboardCacheFDCount(), initialFdCount + 1, "should still be using files for previously stored BIG_STRING");
+
+ // Re-establish baseline for the second part of the test below.
+ initialFdCount = getClipboardCacheFDCount();
+ }
+
+ // ### Part 2 - Reading from the clipboard.
+
+ var Transfer2 = nsTransferable();
+ Transfer2.init(Loadctx);
+ Transfer2.addDataFlavor("text/plain");
+
+ // Iniitalize with a small string, so we can see that mData -> mCacheFD works.
+ assignTextToTransferable(Transfer2, SMALL_STRING);
+ checkTransferableText(Transfer2, SMALL_STRING, "transferable after assigning SMALL_STRING");
+ if (isFDCountingSupported()) {
+ is(getClipboardCacheFDCount(), initialFdCount, "should not use file to store SMALL_STRING.");
+ }
+
+ // Check whether the clipboard data can be read, and simulatenously trigger mData -> mCacheFD.
+ Services.clipboard.getData(Transfer2, Services.clipboard.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext);
+ checkTransferableText(Transfer2, BIG_STRING, "transferable after retrieving from clipboard");
+ if (isFDCountingSupported()) {
+ is(getClipboardCacheFDCount(), initialFdCount + 1, "should use a file for BIG_STRING (read from clipboard).");
+ }
+
+ // Store a small string, to exercise the code path from mCacheFD -> mData.
+ assignTextToTransferable(Transfer2, SMALL_STRING);
+ checkTransferableText(Transfer2, SMALL_STRING, "transferable after assigning SMALL_STRING");
+ if (isFDCountingSupported()) {
+ is(getClipboardCacheFDCount(), initialFdCount, "should release the file after clearing the transferable.");
+ }
+ }
+ ]]>
+ </script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ This test checks whether a big string can be copied to the clipboard, and then retrieved in the same form.
+ On non-Windows, the test also checks whether the data of the transferable is really stored in a file.
+ </body>
+</window>
diff --git a/widget/tests/test_wheeltransaction.xhtml b/widget/tests/test_wheeltransaction.xhtml
new file mode 100644
index 0000000000..23e855c39b
--- /dev/null
+++ b/widget/tests/test_wheeltransaction.xhtml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Wheel scroll transaction tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+window.openDialog("window_wheeltransaction.xhtml", "_blank",
+ "chrome,width=600,height=600,noopener", window);
+
+]]>
+</script>
+</window>
diff --git a/widget/tests/unit/test_macsharingservice.js b/widget/tests/unit/test_macsharingservice.js
new file mode 100644
index 0000000000..f6b0a8e3fc
--- /dev/null
+++ b/widget/tests/unit/test_macsharingservice.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// Basic tests to verify that MacSharingService returns expected data
+
+function test_getSharingProviders() {
+ let sharingService = Cc["@mozilla.org/widget/macsharingservice;1"].getService(
+ Ci.nsIMacSharingService
+ );
+
+ // Ensure these URL's are accepted without error by the getSharingProviders()
+ // method. This does not test if the URL's are interpreted correctly by
+ // the platform implementation and does not test that the URL will be
+ // successfully shared to the target application if the shareURL method is
+ // used. It does indicate the Mac API's used to get the sharing providers
+ // successfully created a URL object for the URL provided and returned at
+ // least one provider.
+ let urls = [
+ "http://example.org",
+ "http://example.org/#",
+ "http://example.org/dkl??",
+ "http://example.org/dkl?a=b;c=d#thisisaref",
+ "http://example.org/dkl?a=b;c=d#thisisaref#double",
+ "http://example.org/#/",
+ "http://example.org/#/#",
+ "http://example.org/#/#/",
+ // This test fails due to the '|' in the path which needs additional
+ // encoding before conversion to NSURL. See bug 1740565.
+ // "http://example.org/foo/bar/x|page.html#this_is_a_fragment",
+ "http://example.org/page.html#this_is_a_fragment",
+ "http://example.org/page.html#this_is_a_fragment#and_another",
+ "http://example.org/foo/bar;#foo",
+ "http://example.org/a file with spaces.html",
+ "https://chat.mozilla.org/#/room/#macdev:mozilla.org",
+ "https://chat.mozilla.org/#/room/%23macdev:mozilla.org",
+ ];
+
+ urls.forEach(url => testGetSharingProvidersForUrl(sharingService, url));
+}
+
+function testGetSharingProvidersForUrl(sharingService, url) {
+ let providers = sharingService.getSharingProviders(url);
+ Assert.greater(providers.length, 1, "There are providers returned");
+ providers.forEach(provider => {
+ Assert.ok("name" in provider, "Provider has name");
+ Assert.ok("menuItemTitle" in provider, "Provider has menuItemTitle");
+ Assert.ok("image" in provider, "Provider has image");
+
+ Assert.notEqual(
+ provider.title,
+ "Mail",
+ "Known filtered provider not returned"
+ );
+ });
+}
+
+function run_test() {
+ test_getSharingProviders();
+}
diff --git a/widget/tests/unit/test_macwebapputils.js b/widget/tests/unit/test_macwebapputils.js
new file mode 100644
index 0000000000..8967f8a593
--- /dev/null
+++ b/widget/tests/unit/test_macwebapputils.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// Basic tests to verify that MacWebAppUtils works
+
+function test_find_app() {
+ var mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"].createInstance(
+ Ci.nsIMacWebAppUtils
+ );
+ let sig = "com.apple.TextEdit";
+
+ let path;
+ path = mwaUtils.pathForAppWithIdentifier(sig);
+ info("TextEdit path: " + path + "\n");
+ Assert.notEqual(path, "");
+}
+
+function test_dont_find_fake_app() {
+ var mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"].createInstance(
+ Ci.nsIMacWebAppUtils
+ );
+ let sig = "calliope.penitentiary.dramamine";
+
+ let path;
+ path = mwaUtils.pathForAppWithIdentifier(sig);
+ Assert.equal(path, "");
+}
+
+function run_test() {
+ test_find_app();
+ test_dont_find_fake_app();
+}
diff --git a/widget/tests/unit/test_taskbar_legacyjumplistitems.js b/widget/tests/unit/test_taskbar_legacyjumplistitems.js
new file mode 100644
index 0000000000..e0173fd29e
--- /dev/null
+++ b/widget/tests/unit/test_taskbar_legacyjumplistitems.js
@@ -0,0 +1,229 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// This tests taskbar jump list functionality available on win7 and up.
+
+function test_basics() {
+ var item = Cc["@mozilla.org/windows-legacyjumplistitem;1"].createInstance(
+ Ci.nsILegacyJumpListItem
+ );
+
+ var sep = Cc["@mozilla.org/windows-legacyjumplistseparator;1"].createInstance(
+ Ci.nsILegacyJumpListSeparator
+ );
+
+ var shortcut = Cc[
+ "@mozilla.org/windows-legacyjumplistshortcut;1"
+ ].createInstance(Ci.nsILegacyJumpListShortcut);
+
+ var link = Cc["@mozilla.org/windows-legacyjumplistlink;1"].createInstance(
+ Ci.nsILegacyJumpListLink
+ );
+
+ Assert.ok(!item.equals(sep));
+ Assert.ok(!item.equals(shortcut));
+ Assert.ok(!item.equals(link));
+
+ Assert.ok(!sep.equals(item));
+ Assert.ok(!sep.equals(shortcut));
+ Assert.ok(!sep.equals(link));
+
+ Assert.ok(!shortcut.equals(item));
+ Assert.ok(!shortcut.equals(sep));
+ Assert.ok(!shortcut.equals(link));
+
+ Assert.ok(!link.equals(item));
+ Assert.ok(!link.equals(sep));
+ Assert.ok(!link.equals(shortcut));
+
+ Assert.ok(item.equals(item));
+ Assert.ok(sep.equals(sep));
+ Assert.ok(link.equals(link));
+ Assert.ok(shortcut.equals(shortcut));
+}
+
+function test_separator() {
+ // separators:
+
+ var item = Cc[
+ "@mozilla.org/windows-legacyjumplistseparator;1"
+ ].createInstance(Ci.nsILegacyJumpListSeparator);
+
+ Assert.ok(item.type == Ci.nsILegacyJumpListItem.JUMPLIST_ITEM_SEPARATOR);
+}
+
+function test_links() {
+ // links:
+ var link1 = Cc["@mozilla.org/windows-legacyjumplistlink;1"].createInstance(
+ Ci.nsILegacyJumpListLink
+ );
+ var link2 = Cc["@mozilla.org/windows-legacyjumplistlink;1"].createInstance(
+ Ci.nsILegacyJumpListLink
+ );
+
+ var uri1 = Cc["@mozilla.org/network/simple-uri-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec("http://www.test.com/")
+ .finalize();
+ var uri2 = Cc["@mozilla.org/network/simple-uri-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec("http://www.test.com/")
+ .finalize();
+
+ link1.uri = uri1;
+ link1.uriTitle = "Test";
+ link2.uri = uri2;
+ link2.uriTitle = "Test";
+
+ Assert.ok(link1.equals(link2));
+
+ link2.uriTitle = "Testing";
+
+ Assert.ok(!link1.equals(link2));
+
+ link2.uriTitle = "Test";
+ uri2 = uri2.mutate().setSpec("http://www.testing.com/").finalize();
+ link2.uri = uri2;
+
+ Assert.ok(!link1.equals(link2));
+}
+
+function test_shortcuts() {
+ // shortcuts:
+ var sc = Cc["@mozilla.org/windows-legacyjumplistshortcut;1"].createInstance(
+ Ci.nsILegacyJumpListShortcut
+ );
+
+ var handlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+
+ handlerApp.name = "TestApp";
+ handlerApp.detailedDescription = "TestApp detailed description.";
+ handlerApp.appendParameter("-test");
+
+ sc.iconIndex = 1;
+ Assert.equal(sc.iconIndex, 1);
+
+ var faviconPageUri = Cc["@mozilla.org/network/simple-uri-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec("http://www.123.com/")
+ .finalize();
+ sc.faviconPageUri = faviconPageUri;
+ Assert.equal(sc.faviconPageUri, faviconPageUri);
+
+ var notepad = Services.dirsvc.get("WinD", Ci.nsIFile);
+ notepad.append("notepad.exe");
+ if (notepad.exists()) {
+ handlerApp.executable = notepad;
+ sc.app = handlerApp;
+ Assert.equal(sc.app.detailedDescription, "TestApp detailed description.");
+ Assert.equal(sc.app.name, "TestApp");
+ Assert.ok(sc.app.parameterExists("-test"));
+ Assert.ok(!sc.app.parameterExists("-notset"));
+ }
+}
+
+async function test_legacyjumplist() {
+ // Jump lists can't register links unless the application is the default
+ // protocol handler for the protocol of the link, so we skip off testing
+ // those in these tests. We'll init the jump list for the xpc shell harness,
+ // add a task item, and commit it.
+
+ // not compiled in
+ if (Ci.nsIWinTaskbar == null) {
+ return;
+ }
+
+ var taskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(
+ Ci.nsIWinTaskbar
+ );
+
+ // Since we're only testing the general functionality of the JumpListBuilder
+ // et. al, we can just test the non-private browsing version.
+ // (The only difference between the two at this level is the App User Model ID.)
+ var builder = taskbar.createLegacyJumpListBuilder(false);
+
+ Assert.notEqual(builder, null);
+
+ // Win7 and up only
+ try {
+ var ver = parseFloat(Services.sysinfo.getProperty("version"));
+ if (ver < 6.1) {
+ Assert.ok(!builder.available);
+ return;
+ }
+ } catch (ex) {}
+
+ Assert.ok(taskbar.available);
+
+ builder.deleteActiveList();
+
+ var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+
+ var sc = Cc["@mozilla.org/windows-legacyjumplistshortcut;1"].createInstance(
+ Ci.nsILegacyJumpListShortcut
+ );
+
+ var handlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+
+ handlerApp.name = "Notepad";
+ handlerApp.detailedDescription = "Testing detailed description.";
+
+ var notepad = Services.dirsvc.get("WinD", Ci.nsIFile);
+ notepad.append("notepad.exe");
+ if (notepad.exists()) {
+ // To ensure "profile-before-change" will fire before
+ // "xpcom-shutdown-threads"
+ do_get_profile();
+
+ handlerApp.executable = notepad;
+ sc.app = handlerApp;
+ items.appendElement(sc);
+
+ var removed = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ Assert.ok(builder.initListBuild(removed));
+ Assert.ok(builder.addListToBuild(builder.JUMPLIST_CATEGORY_TASKS, items));
+ Assert.ok(builder.addListToBuild(builder.JUMPLIST_CATEGORY_RECENT));
+ Assert.ok(builder.addListToBuild(builder.JUMPLIST_CATEGORY_FREQUENT));
+ let rv = new Promise(resolve => {
+ builder.commitListBuild(resolve);
+ });
+ Assert.ok(await rv);
+
+ builder.deleteActiveList();
+
+ Assert.ok(builder.initListBuild(removed));
+ Assert.ok(
+ builder.addListToBuild(
+ builder.JUMPLIST_CATEGORY_CUSTOMLIST,
+ items,
+ "Custom List"
+ )
+ );
+ rv = new Promise(resolve => {
+ builder.commitListBuild(resolve);
+ });
+ Assert.ok(await rv);
+
+ builder.deleteActiveList();
+ }
+}
+
+function run_test() {
+ if (mozinfo.os != "win") {
+ return;
+ }
+ test_basics();
+ test_separator();
+ test_links();
+ test_shortcuts();
+
+ run_next_test();
+}
+
+add_task(test_legacyjumplist);
diff --git a/widget/tests/unit/xpcshell.toml b/widget/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..4e702ca356
--- /dev/null
+++ b/widget/tests/unit/xpcshell.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+head = ""
+
+["test_macsharingservice.js"]
+run-if = ["os == 'mac'"]
+
+["test_macwebapputils.js"]
+run-if = ["os == 'mac'"]
+
+["test_taskbar_legacyjumplistitems.js"]
+skip-if = ["os == 'win'"]
diff --git a/widget/tests/window_bug429954.xhtml b/widget/tests/window_bug429954.xhtml
new file mode 100644
index 0000000000..ca26d52621
--- /dev/null
+++ b/widget/tests/window_bug429954.xhtml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 478536"
+ onload="start();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml" id="body">
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+function ok(aCondition, aMessage)
+{
+ window.browsingContext.topChromeWindow.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function isnot(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage);
+}
+
+function start() {
+ var oldWidth = window.outerWidth, oldHeight = window.outerHeight;
+ window.maximize();
+ window.restore();
+ is(window.outerWidth, oldWidth, "wrong window width after maximize+restore");
+ is(window.outerHeight, oldHeight, "wrong window height after maximize+restore");
+ window.arguments[0].SimpleTest.finish();
+ window.close();
+}
+
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/window_bug478536.xhtml b/widget/tests/window_bug478536.xhtml
new file mode 100644
index 0000000000..7318eb0bff
--- /dev/null
+++ b/widget/tests/window_bug478536.xhtml
@@ -0,0 +1,211 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 478536"
+ width="600" height="600"
+ onload="onload();"
+ onunload="onunload();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml" id="body">
+<style type="text/css">
+ #view {
+ overflow: auto;
+ width: 100px;
+ height: 100px;
+ border: 1px solid;
+ margin: 0;
+ }
+</style>
+<pre id="view" onscroll="onScrollView(event);">
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+function ok(aCondition, aMessage)
+{
+ window.arguments[0].SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function isnot(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage);
+}
+
+var gBody = document.getElementById("body");
+var gView = document.getElementById("view");
+
+/**
+ * Description:
+ *
+ * First, lock the wheel scrolling target to "view" at first step.
+ * Next, scroll back to top most of the "view" at second step.
+ * Finally, scroll back again at third step. This fails to scroll the "view",
+ * then, |onMouseScrollFailed| event should be fired. And at that time, we
+ * can remove the "view". So, in post processing of the event firere, the
+ * "view" should not be referred.
+ *
+ * For suppressing random test failure, all tests will be retried if we handle
+ * unexpected timeout event.
+ */
+
+var gTests = [
+ { scrollToForward: true, shouldScroll: true },
+ { scrollToForward: false, shouldScroll: true },
+ { scrollToForward: false, shouldScroll: false }
+];
+var gCurrentTestIndex = -1;
+var gIgnoreScrollEvent = true;
+
+var gPrefSvc = SpecialPowers.Services.prefs;
+const kPrefSmoothScroll = "general.smoothScroll";
+const kPrefNameTimeout = "mousewheel.transaction.timeout";
+const kDefaultTimeout = gPrefSvc.getIntPref(kPrefNameTimeout);
+
+gPrefSvc.setBoolPref(kPrefSmoothScroll, false);
+
+var gTimeout = kDefaultTimeout;
+
+gBody.addEventListener("MozMouseScrollFailed", onMouseScrollFailed);
+gBody.addEventListener("MozMouseScrollTransactionTimeout",
+ onTransactionTimeout);
+
+function setTimeoutPrefs(aTimeout)
+{
+ gPrefSvc.setIntPref(kPrefNameTimeout, aTimeout);
+ gTimeout = aTimeout;
+}
+
+function resetTimeoutPrefs()
+{
+ if (gTimeout == kDefaultTimeout)
+ return;
+ setTimeoutPrefs(kDefaultTimeout);
+}
+
+function growUpTimeoutPrefs()
+{
+ if (gTimeout != kDefaultTimeout)
+ return;
+ setTimeoutPrefs(5000);
+}
+
+function onload()
+{
+ disableNonTestMouseEvents(true);
+ setTimeout(runNextTest, 0);
+}
+
+function onunload()
+{
+ resetTimeoutPrefs();
+ disableNonTestMouseEvents(false);
+ gPrefSvc.clearUserPref(kPrefSmoothScroll);
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+ window.arguments[0].SimpleTest.finish();
+}
+
+function finish()
+{
+ window.close();
+}
+
+// testing code
+
+var gTimer;
+function clearTimer()
+{
+ clearTimeout(gTimer);
+ gTimer = 0;
+}
+
+function runNextTest()
+{
+ clearTimer();
+ if (++gCurrentTestIndex >= gTests.length) {
+ ok(true, "didn't crash, succeeded");
+ finish();
+ return;
+ }
+ fireWheelScrollEvent(gTests[gCurrentTestIndex].scrollToForward);
+}
+
+var gRetryCount = 5;
+function retryAllTests()
+{
+ clearTimer();
+ if (--gRetryCount >= 0) {
+ gView.scrollTop = 0;
+ gView.scrollLeft = 0;
+ gCurrentTestIndex = -1;
+ growUpTimeoutPrefs();
+ ok(true, "WARNING: retry current test-list...");
+ gTimer = setTimeout(runNextTest, 0);
+ } else {
+ ok(false, "Failed by unexpected timeout");
+ finish();
+ }
+}
+
+function fireWheelScrollEvent(aForward)
+{
+ gIgnoreScrollEvent = false;
+ var event = { deltaY: aForward ? 4.0 : -4.0,
+ deltaMode: WheelEvent.DOM_DELTA_LINE };
+ sendWheelAndPaint(gView, 5, 5, event, function() {
+ // No callback - we're just forcing the refresh driver to tick.
+ }, window);
+}
+
+function onScrollView(aEvent)
+{
+ if (gIgnoreScrollEvent)
+ return;
+ gIgnoreScrollEvent = true;
+ clearTimer();
+ ok(gTests[gCurrentTestIndex].shouldScroll, "The view is scrolled");
+ gTimer = setTimeout(runNextTest, 0);
+}
+
+function onMouseScrollFailed(aEvent)
+{
+ clearTimer();
+ gIgnoreScrollEvent = true;
+ ok(!gTests[gCurrentTestIndex].shouldScroll, "The view is not scrolled");
+ if (!gTests[gCurrentTestIndex].shouldScroll)
+ gBody.removeChild(gView);
+ runNextTest();
+}
+
+function onTransactionTimeout(aEvent)
+{
+ if (!gTimer)
+ return;
+ gIgnoreScrollEvent = true;
+ retryAllTests();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/window_bug522217.xhtml b/widget/tests/window_bug522217.xhtml
new file mode 100644
index 0000000000..80eb4b6e5a
--- /dev/null
+++ b/widget/tests/window_bug522217.xhtml
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 522217"
+ onload="start();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml" id="body">
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+function ok(aCondition, aMessage)
+{
+ window.arguments[0].SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function isnot(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage);
+}
+
+function executeSoon() {
+ return new Promise(resolve => {
+ window.arguments[0].SimpleTest.executeSoon(resolve);
+ });
+}
+
+function waitForEvent(obj, name) {
+ return new Promise(resolve => {
+ obj.addEventListener(name, resolve, { once: true });
+ });
+}
+
+async function start() {
+ await waitForEvent(window, "focus");
+ var oldOuterWidth = window.outerWidth, oldOuterHeight = window.outerHeight;
+ var oldInnerWidth = window.innerWidth, oldInnerHeight = window.innerHeight;
+ document.documentElement.setAttribute("chromemargin", "0,0,0,0");
+
+ await executeSoon();
+ is(window.outerWidth, oldOuterWidth, "chromemargin shouldn't change the window's outerWidth");
+ is(window.outerHeight, oldOuterHeight, "chromemargin shouldn't change the window's outerHeight");
+ is(window.innerWidth, oldOuterWidth, "if chromemargin is set, innerWidth and outerWidth should be the same");
+ is(window.innerHeight, oldOuterHeight, "if chromemargin is set, innerHeight and outerHeight should be the same");
+
+ // Wait for going full screen and back.
+ let sizemodeChange = waitForEvent(window, "sizemodechange");
+ window.fullScreen = true;
+ await sizemodeChange;
+ sizemodeChange = waitForEvent(window, "sizemodechange");
+ window.fullScreen = false;
+ await sizemodeChange;
+ is(window.outerWidth, oldOuterWidth, "wrong outerWidth after fullscreen mode");
+ is(window.outerHeight, oldOuterHeight, "wrong outerHeight after fullscreen mode");
+ is(window.innerWidth, oldOuterWidth, "wrong innerWidth after fullscreen mode");
+ is(window.innerHeight, oldOuterHeight, "wrong innerHeight after fullscreen mode");
+ document.documentElement.removeAttribute("chromemargin");
+
+ await executeSoon();
+ is(window.outerWidth, oldOuterWidth, "wrong outerWidth after removing chromemargin");
+ is(window.outerHeight, oldOuterHeight, "wrong outerHeight after removing chromemargin");
+ is(window.innerWidth, oldInnerWidth, "wrong innerWidth after removing chromemargin");
+ is(window.innerHeight, oldInnerHeight, "wrong innerHeight after removing chromemargin");
+ window.arguments[0].SimpleTest.finish();
+ window.close();
+}
+
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/window_bug538242.xhtml b/widget/tests/window_bug538242.xhtml
new file mode 100644
index 0000000000..fb878b1383
--- /dev/null
+++ b/widget/tests/window_bug538242.xhtml
@@ -0,0 +1,3 @@
+<?xml version="1.0"?>
+<window title="Window for Test for Mozilla Bug 538242"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
diff --git a/widget/tests/window_bug593307_centerscreen.xhtml b/widget/tests/window_bug593307_centerscreen.xhtml
new file mode 100644
index 0000000000..dd73e42f84
--- /dev/null
+++ b/widget/tests/window_bug593307_centerscreen.xhtml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 593307"
+ width="100" height="100"
+ onload="onload();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml" id="body">
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+function onload()
+{
+ var SimpleTest = window.opener.SimpleTest;
+ SimpleTest.ok(window.screenX >= 0, "centerscreen window should not start offscreen (X coordinate): " + window.screenX);
+ SimpleTest.ok(window.screenY >= 0, "centerscreen window should not start offscreen (Y coordinate): " + window.screenY);
+ window.opener.finished();
+}
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/window_bug593307_offscreen.xhtml b/widget/tests/window_bug593307_offscreen.xhtml
new file mode 100644
index 0000000000..10ab701a55
--- /dev/null
+++ b/widget/tests/window_bug593307_offscreen.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Mozilla Bug 593307"
+ width="100" height="100"
+ onload="onLoad();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml" id="body">
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+var centerscreen = null;
+var SimpleTest = window.arguments[0];
+var finish = window.arguments[1];
+
+function onLoad()
+{
+ setTimeout(() => {
+ centerscreen = window.openDialog('window_bug593307_centerscreen.xhtml','', 'chrome,centerscreen,dependent,dialog=no');
+ }, 0);
+}
+
+function finished() {
+ centerscreen.close();
+ finish();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/window_composition_text_querycontent.xhtml b/widget/tests/window_composition_text_querycontent.xhtml
new file mode 100644
index 0000000000..db3b10ea30
--- /dev/null
+++ b/widget/tests/window_composition_text_querycontent.xhtml
@@ -0,0 +1,10923 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<window title="Testing composition, text and query content events"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js" />
+
+ <panel id="panel" hidden="true" orient="vertical">
+ <vbox id="vbox">
+ <html:textarea id="textbox" cols="20" rows="4" style="font-size: 36px;"/>
+ </vbox>
+ </panel>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<div id="display">
+<div id="div" style="margin: 0; padding: 0; font-size: 36px;">Here is a text frame.</div>
+<textarea style="margin: 0; font-family: -moz-fixed;" id="textarea" cols="20" rows="4"></textarea><br/>
+<iframe id="iframe" width="300" height="150"
+ src="data:text/html,&lt;textarea id='textarea' cols='20' rows='4'&gt;&lt;/textarea&gt;"></iframe><br/>
+<iframe id="iframe2" width="300" height="150"
+ src="data:text/html,&lt;body onload='document.designMode=%22on%22'&gt;body content&lt;/body&gt;"></iframe><br/>
+<iframe id="iframe3" width="300" height="150"
+ src="data:text/html,&lt;body onload='document.designMode=%22on%22'&gt;body content&lt;/body&gt;"></iframe><br/>
+<iframe id="iframe4" width="300" height="150"
+ src="data:text/html,&lt;div contenteditable id='contenteditable'&gt;&lt;/div&gt;"></iframe><br/>
+<!--
+ NOTE: the width for the next two iframes is chosen to be small enough to make
+ the Show Password button (for type=password) be outside the viewport so that
+ it doesn't affect the rendering compared to the type=text control.
+ But still large enough to comfortably fit the input values we test.
+-->
+<iframe id="iframe5" style="width:10ch" height="50" src="data:text/html,&lt;input id='input'&gt;"></iframe>
+<iframe id="iframe6" style="width:10ch" height="50" src="data:text/html,&lt;input id='password' type='password'&gt;"></iframe><br/>
+<iframe id="iframe7" width="300" height="150"
+ src="data:text/html,&lt;span contenteditable id='contenteditable'&gt;&lt;/span&gt;"></iframe><br/>
+<input id="input" type="text"/><br/>
+<input id="password" type="password"/><br/>
+</div>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+function ok(aCondition, aMessage)
+{
+ window.arguments[0].SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function isnot(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage);
+}
+
+function isfuzzy(aLeft, aRight, aEpsilon, aMessage) {
+ window.arguments[0].SimpleTest.isfuzzy(aLeft, aRight, aEpsilon, aMessage);
+}
+
+function todo(aCondition, aMessage)
+{
+ window.arguments[0].SimpleTest.todo(aCondition, aMessage);
+}
+
+function todo_is(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.todo_is(aLeft, aRight, aMessage);
+}
+
+function todo_isnot(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.todo_isnot(aLeft, aRight, aMessage);
+}
+
+function isSimilarTo(aLeft, aRight, aAllowedDifference, aMessage)
+{
+ if (Math.abs(aLeft - aRight) <= aAllowedDifference) {
+ ok(true, aMessage);
+ } else {
+ ok(false, aMessage + ", got=" + aLeft + ", expected=" + (aRight - aAllowedDifference) + "~" + (aRight + aAllowedDifference));
+ }
+}
+
+function isGreaterThan(aLeft, aRight, aMessage)
+{
+ ok(aLeft > aRight, aMessage + ", got=" + aLeft + ", expected minimum value=" + aRight);
+}
+
+/**
+ * synthesizeSimpleCompositionChange synthesizes a composition which has only
+ * one clause and put caret end of it.
+ *
+ * @param aComposition string or object. If string, it's treated as
+ * composition string whose attribute is
+ * COMPOSITION_ATTR_RAW_CLAUSE.
+ * If object, it must have .string whose type is "string".
+ * Additionally, .attr can be specified if you'd like to
+ * use the other attribute instead of
+ * COMPOSITION_ATTR_RAW_CLAUSE.
+ */
+function synthesizeSimpleCompositionChange(aComposition, aWindow, aCallback) {
+ const comp = (() => {
+ if (typeof aComposition == "string") {
+ return { string: aComposition, attr: COMPOSITION_ATTR_RAW_CLAUSE };
+ }
+ return {
+ string: aComposition.string,
+ attr: aComposition.attr === undefined
+ ? COMPOSITION_ATTR_RAW_CLAUSE
+ : aComposition.attr
+ };
+ })();
+ synthesizeCompositionChange(
+ {
+ composition: {
+ string: comp.string,
+ clauses: [
+ { length: comp.string.length, attr: comp.attr },
+ ],
+ },
+ caret: { start: comp.string.length, length: 0 },
+ },
+ aWindow,
+ aCallback
+ );
+}
+
+
+var div = document.getElementById("div");
+var textarea = document.getElementById("textarea");
+var panel = document.getElementById("panel");
+var textbox = document.getElementById("textbox");
+var iframe = document.getElementById("iframe");
+var iframe2 = document.getElementById("iframe2");
+var iframe3 = document.getElementById("iframe3");
+var contenteditable;
+var windowOfContenteditable;
+var contenteditableBySpan;
+var windowOfContenteditableBySpan;
+var input = document.getElementById("input");
+var password = document.getElementById("password");
+var textareaInFrame;
+
+const nsITextInputProcessorCallback = Ci.nsITextInputProcessorCallback;
+const nsIInterfaceRequestor = Ci.nsIInterfaceRequestor;
+const nsIWebNavigation = Ci.nsIWebNavigation;
+const nsIDocShell = Ci.nsIDocShell;
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function waitForTick() {
+ return new Promise(resolve => { SimpleTest.executeSoon(resolve); });
+}
+
+async function waitForEventLoops(aTimes)
+{
+ for (let i = 1; i < aTimes; i++) {
+ await waitForTick();
+ }
+ await new Promise(resolve => { setTimeout(resolve, 20); });
+}
+
+function getEditor(aNode)
+{
+ return aNode.editor;
+}
+
+function getHTMLEditorIMESupport(aWindow)
+{
+ return aWindow.docShell.editor;
+}
+
+const kIsWin = (navigator.platform.indexOf("Win") == 0);
+const kIsMac = (navigator.platform.indexOf("Mac") == 0);
+
+const kLFLen = (kIsWin && false) ? 2 : 1;
+const kLF = (kIsWin && false) ? "\r\n" : "\n";
+
+function checkQueryContentResult(aResult, aMessage)
+{
+ ok(aResult, aMessage + ": the result is null");
+ if (!aResult) {
+ return false;
+ }
+ ok(aResult.succeeded, aMessage + ": the query content failed");
+ return aResult.succeeded;
+}
+
+function checkContent(aExpectedText, aMessage, aID)
+{
+ if (!aID) {
+ aID = "";
+ }
+ let textContent = synthesizeQueryTextContent(0, 100);
+ if (!checkQueryContentResult(textContent, aMessage +
+ ": synthesizeQueryTextContent " + aID)) {
+ return false;
+ }
+ is(textContent.text, aExpectedText,
+ aMessage + ": composition string is wrong " + aID);
+ return textContent.text == aExpectedText;
+}
+
+function checkContentRelativeToSelection(aRelativeOffset, aLength, aExpectedOffset, aExpectedText, aMessage, aID)
+{
+ if (!aID) {
+ aID = "";
+ }
+ aMessage += " (aRelativeOffset=" + aRelativeOffset + "): "
+ let textContent = synthesizeQueryTextContent(aRelativeOffset, aLength, true);
+ if (!checkQueryContentResult(textContent, aMessage +
+ "synthesizeQueryTextContent " + aID)) {
+ return false;
+ }
+ is(textContent.offset, aExpectedOffset,
+ aMessage + "offset is wrong " + aID);
+ is(textContent.text, aExpectedText,
+ aMessage + "text is wrong " + aID);
+ return textContent.offset == aExpectedOffset &&
+ textContent.text == aExpectedText;
+}
+
+function checkSelection(aExpectedOffset, aExpectedText, aMessage, aID)
+{
+ if (!aID) {
+ aID = "";
+ }
+ let selectedText = synthesizeQuerySelectedText();
+ if (!checkQueryContentResult(selectedText, aMessage +
+ ": synthesizeQuerySelectedText " + aID)) {
+ return false;
+ }
+ if (aExpectedOffset === null) {
+ is(
+ selectedText.notFound,
+ true,
+ `${aMessage}: selection should not be found ${aID}`
+ );
+ return selectedText.notFound;
+ }
+
+ is(
+ selectedText.notFound,
+ false,
+ `${aMessage}: selection should be found ${aID}`
+ );
+ if (selectedText.notFound) {
+ return false;
+ }
+ is(
+ selectedText.offset,
+ aExpectedOffset,
+ `${aMessage}: selection offset should be ${aExpectedOffset} ${aID}`
+ );
+ is(
+ selectedText.text,
+ aExpectedText,
+ `${aMessage}: selected text should be "${aExpectedText}" ${aID}`
+ );
+ return selectedText.offset == aExpectedOffset &&
+ selectedText.text == aExpectedText;
+}
+
+function checkIMESelection(
+ aSelectionType,
+ aExpectedFound,
+ aExpectedOffset,
+ aExpectedText,
+ aMessage,
+ aID,
+ aToDo = {}
+) {
+ if (!aID) {
+ aID = "";
+ }
+ aMessage += " (" + aSelectionType + ")";
+ let {
+ notFound = is,
+ offset = is,
+ text = is,
+ } = aToDo;
+ let selectionType = 0;
+ switch (aSelectionType) {
+ case "RawClause":
+ selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT;
+ break;
+ case "SelectedRawClause":
+ selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT;
+ break;
+ case "ConvertedClause":
+ selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT;
+ break;
+ case "SelectedClause":
+ selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT;
+ break;
+ default:
+ ok(false, aMessage + ": invalid selection type, " + aSelectionType);
+ }
+ isnot(selectionType, 0, aMessage + ": wrong value");
+ let selectedText = synthesizeQuerySelectedText(selectionType);
+ if (!checkQueryContentResult(selectedText, aMessage +
+ ": synthesizeQuerySelectedText " + aID)) {
+ return false;
+ }
+ notFound(
+ selectedText.notFound,
+ !aExpectedFound,
+ `${aMessage}: selection should ${
+ aExpectedFound ? "" : "not"
+ } be found ${aID}`);
+ if (selectedText.notFound) {
+ return selectedText.notFound == !aExpectedFound;
+ }
+
+ offset(
+ selectedText.offset,
+ aExpectedOffset,
+ `${aMessage}: selection offset is wrong ${aID}`
+ );
+ text(
+ selectedText.text,
+ aExpectedText,
+ `${aMessage}: selected text is wrong ${aID}`
+ );
+ return selectedText.offset == aExpectedOffset &&
+ selectedText.text == aExpectedText;
+}
+
+function checkRect(aRect, aExpectedRect, aMessage)
+{
+ is(aRect.left, aExpectedRect.left, aMessage + ": left is wrong");
+ is(aRect.top, aExpectedRect.top, aMessage + " top is wrong");
+ is(aRect.width, aExpectedRect.width, aMessage + ": width is wrong");
+ is(aRect.height, aExpectedRect.height, aMessage + ": height is wrong");
+ return aRect.left == aExpectedRect.left &&
+ aRect.top == aExpectedRect.top &&
+ aRect.width == aExpectedRect.width &&
+ aRect.height == aExpectedRect.height;
+}
+
+function checkRectFuzzy(aRect, aExpectedRect, aEpsilon, aMessage) {
+ isfuzzy(aRect.left, aExpectedRect.left, aEpsilon.left, aMessage + ": left is wrong");
+ isfuzzy(aRect.top, aExpectedRect.top, aEpsilon.top, aMessage + " top is wrong");
+ isfuzzy(aRect.width, aExpectedRect.width, aEpsilon.width, aMessage + ": width is wrong");
+ isfuzzy(aRect.height, aExpectedRect.height, aEpsilon.height, aMessage + ": height is wrong");
+ return (aRect.left >= aExpectedRect.left - aEpsilon.left &&
+ aRect.left <= aExpectedRect.left + aEpsilon.left) &&
+ (aRect.top >= aExpectedRect.top - aEpsilon.top &&
+ aRect.top <= aExpectedRect.top + aEpsilon.top) &&
+ (aRect.width >= aExpectedRect.width - aEpsilon.width &&
+ aRect.width <= aExpectedRect.width + aEpsilon.width) &&
+ (aRect.height >= aExpectedRect.height - aEpsilon.height &&
+ aRect.height <= aExpectedRect.height + aEpsilon.height);
+}
+
+function getRectArray(aQueryTextRectArrayResult) {
+ let rects = [];
+ for (let i = 0; ; i++) {
+ let rect = { left: {}, top: {}, width: {}, height: {} };
+ try {
+ aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height);
+ } catch (e) {
+ break;
+ }
+ rects.push({
+ left: rect.left.value,
+ top: rect.top.value,
+ width: rect.width.value,
+ height: rect.height.value,
+ });
+ }
+ return rects;
+}
+
+function checkRectArray(aQueryTextRectArrayResult, aExpectedTextRectArray, aMessage)
+{
+ for (let i = 1; i < aExpectedTextRectArray.length; ++i) {
+ let rect = { left: {}, top: {}, width: {}, height: {} };
+ try {
+ aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height);
+ } catch (e) {
+ ok(false, aMessage + ": failed to retrieve " + i + "th rect (" + e + ")");
+ return false;
+ }
+ function toRect(aRect)
+ {
+ return { left: aRect.left.value, top: aRect.top.value, width: aRect.width.value, height: aRect.height.value };
+ }
+ if (!checkRect(toRect(rect), aExpectedTextRectArray[i], aMessage + " " + i + "th rect")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function checkRectContainsRect(aRect, aContainer, aMessage)
+{
+ let container = { left: Math.ceil(aContainer.left),
+ top: Math.ceil(aContainer.top),
+ width: Math.floor(aContainer.width),
+ height: Math.floor(aContainer.height) };
+
+ let ret = container.left <= aRect.left &&
+ container.top <= aRect.top &&
+ container.left + container.width >= aRect.left + aRect.width &&
+ container.top + container.height >= aRect.top + aRect.height;
+ ret = ret && aMessage;
+ ok(ret, aMessage + " container={ left=" + container.left + ", top=" +
+ container.top + ", width=" + container.width + ", height=" +
+ container.height + " } rect={ left=" + aRect.left + ", top=" + aRect.top +
+ ", width=" + aRect.width + ", height=" + aRect.height + " }");
+ return ret;
+}
+
+// eslint-disable-next-line complexity
+function runUndoRedoTest()
+{
+ textarea.value = "";
+ textarea.focus();
+
+ // input raw characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306D",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "," },
+ });
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306D\u3053",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: "b" },
+ });
+
+ // convert
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u732B",
+ "clauses":
+ [
+ { "length": 1,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: " " },
+ });
+
+ // commit
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ // input raw characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u307E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "j" },
+ });
+
+ // cancel the composition
+ synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Escape" } });
+
+ // input raw characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3080",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "]" },
+ });
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3080\u3059",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: "r" },
+ });
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3080\u3059\u3081",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ "key": { key: "/" },
+ });
+
+ // convert
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u5A18",
+ "clauses":
+ [
+ { "length": 1,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: " " },
+ });
+
+ // commit
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ sendString(" meant");
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("s \"cat-girl\". She is a ");
+
+ // input raw characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3088",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "9" },
+ });
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3088\u3046",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: "4" },
+ });
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3088\u3046\u304b",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ "key": { key: "t" },
+ });
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3088\u3046\u304b\u3044",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 },
+ "key": { key: "e" },
+ });
+
+ // convert
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u5996\u602a",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: " " },
+ });
+
+ // commit
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } });
+
+ synthesizeKey("KEY_Backspace", {repeat: 12});
+
+ let i = 0;
+ if (!checkContent("\u732B\u5A18 means \"cat-girl\".",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(20, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(32, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(30, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("\u732B\u5A18 mean",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(7, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("\u732B\u5A18 meant",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(8, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("\u732B\u5A18",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(2, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("\u732B",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ // XXX this is unexpected behavior, see bug 258291
+ if (!checkContent("\u732B",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(0, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true});
+
+ if (!checkContent("",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(0, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ // XXX this is unexpected behavior, see bug 258291
+ if (!checkContent("\u732B",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B\u5A18",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(2, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B\u5A18 meant",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(8, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B\u5A18 mean",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(7, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(30, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(32, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B\u5A18 means \"cat-girl\".",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(20, "", "runUndoRedoTest", "#" + i)) {
+ return;
+ }
+
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+
+ if (!checkContent("\u732B\u5A18 means \"cat-girl\".",
+ "runUndoRedoTest", "#" + ++i) ||
+ !checkSelection(20, "", "runUndoRedoTest", "#" + i)) {
+ // eslint-disable-next-line no-useless-return
+ return;
+ }
+}
+
+function checkInputEvent(aEvent, aIsComposing, aInputType, aData, aTargetRanges, aDescription) {
+ if (aEvent.type !== "input" && aEvent.type !== "beforeinput") {
+ throw new Error(`${aDescription}: "${aEvent.type}" is not InputEvent`);
+ }
+ ok(InputEvent.isInstance(aEvent), `"${aEvent.type}" event should be dispatched with InputEvent interface: ${aDescription}`);
+ let cancelable = aEvent.type === "beforeinput" &&
+ aInputType !== "insertCompositionText" &&
+ aInputType !== "deleteCompositionText";
+ is(aEvent.cancelable, cancelable, `"${aEvent.type}" event should ${cancelable ? "be" : "be never"} cancelable: ${aDescription}`);
+ is(aEvent.bubbles, true, `"${aEvent.type}" event should always bubble: ${aDescription}`);
+ is(aEvent.isComposing, aIsComposing, `isComposing of "${aEvent.type}" event should be ${aIsComposing}: ${aDescription}`);
+ is(aEvent.inputType, aInputType, `inputType of "${aEvent.type}" event should be "${aInputType}": ${aDescription}`);
+ is(aEvent.data, aData, `data of "${aEvent.type}" event should be ${aData}: ${aDescription}`);
+ is(aEvent.dataTransfer, null, `dataTransfer of "${aEvent.type}" event should be null: ${aDescription}`);
+ let targetRanges = aEvent.getTargetRanges();
+ if (aTargetRanges.length === 0) {
+ is(targetRanges.length, 0,
+ `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`);
+ } else {
+ is(targetRanges.length, aTargetRanges.length,
+ `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`);
+ if (targetRanges.length == aTargetRanges.length) {
+ for (let i = 0; i < targetRanges.length; i++) {
+ is(targetRanges[i].startContainer, aTargetRanges[i].startContainer,
+ `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].startOffset, aTargetRanges[i].startOffset,
+ `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].endContainer, aTargetRanges[i].endContainer,
+ `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ is(targetRanges[i].endOffset, aTargetRanges[i].endOffset,
+ `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
+ }
+ }
+ }
+}
+
+function runCompositionCommitAsIsTest()
+{
+ textarea.focus();
+
+ let result = [];
+ function clearResult()
+ {
+ result = [];
+ }
+
+ function handler(aEvent)
+ {
+ result.push(aEvent);
+ }
+
+ textarea.addEventListener("compositionupdate", handler, true);
+ textarea.addEventListener("compositionend", handler, true);
+ textarea.addEventListener("beforeinput", handler, true);
+ textarea.addEventListener("input", handler, true);
+ textarea.addEventListener("text", handler, true);
+
+ // compositioncommitasis with composing string.
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #1");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } });
+
+ is(result.length, 4,
+ "runCompositionCommitAsIsTest: 4 events should be fired after dispatching compositioncommitasis #1");
+ is(result[0].type, "text",
+ "runCompositionCommitAsIsTest: text should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
+ is(result[1].type, "beforeinput",
+ "runCompositionCommitAsIsTest: beforeinput should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
+ checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
+ is(result[2].type, "compositionend",
+ "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #1");
+ is(result[3].type, "input",
+ "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1");
+ checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
+ is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #1");
+
+ // compositioncommitasis with committed string.
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #2");
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "KEY_Enter", type: "keydown" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter", type: "keyup" } });
+
+ is(result.length, 2,
+ "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #2");
+ // XXX Do we need a "beforeinput" event here? Not sure.
+ is(result[0].type, "compositionend",
+ "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #2");
+ is(result[1].type, "input",
+ "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #2");
+ checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #2");
+ is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2");
+
+ // compositioncommitasis with committed string.
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #3");
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "KEY_Escape", type: "keydown" },
+ });
+ is(textarea.value, "", "runCompositionCommitAsIsTest: textarea has non-empty composition string #3");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape", type: "keyup" } });
+
+ is(result.length, 2,
+ "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #3");
+ // XXX Do we need a "beforeinput" event here? Not sure.
+ is(result[0].type, "compositionend",
+ "runCompositionCommitAsIsTest: compositionend shouldn't be fired after dispatching compositioncommitasis #3");
+ is(result[1].type, "input",
+ "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #3");
+ checkInputEvent(result[1], false, "insertCompositionText", "", [],
+ "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #3");
+ is(textarea.value, "", "runCompositionCommitAsIsTest: textarea doesn't have committed string #3");
+
+ textarea.removeEventListener("compositionupdate", handler, true);
+ textarea.removeEventListener("compositionend", handler, true);
+ textarea.removeEventListener("beforeinput", handler, true);
+ textarea.removeEventListener("input", handler, true);
+ textarea.removeEventListener("text", handler, true);
+}
+
+function runCompositionCommitTest()
+{
+ textarea.focus();
+
+ let result = [];
+ function clearResult()
+ {
+ result = [];
+ }
+
+ function handler(aEvent)
+ {
+ result.push(aEvent);
+ }
+
+ textarea.addEventListener("compositionupdate", handler, true);
+ textarea.addEventListener("compositionend", handler, true);
+ textarea.addEventListener("beforeinput", handler, true);
+ textarea.addEventListener("input", handler, true);
+ textarea.addEventListener("text", handler, true);
+
+ // compositioncommit with different composing string.
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a", type: "keydown" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #1");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "a", type: "keyup" } });
+
+ is(result.length, 5,
+ "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #1");
+ is(result[0].type, "compositionupdate",
+ "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1");
+ is(result[1].type, "text",
+ "runCompositionCommitTest: text should be fired after dispatching compositioncommit #1");
+ is(result[2].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1");
+ checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #1");
+ is(result[3].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #1");
+ is(result[4].type, "input",
+ "runCompositionCommitTest: input should be fired after dispatching compositioncommit #1");
+ checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #1");
+ is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #1");
+
+ // compositioncommit with different committed string when there is already committed string
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #2");
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "KEY_Enter", type: "keydown" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have committed string #2");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });
+
+ is(result.length, 5,
+ "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #2");
+ is(result[0].type, "compositionupdate",
+ "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #2");
+ is(result[1].type, "text",
+ "runCompositionCommitTest: text should be fired after dispatching compositioncommit #2");
+ is(result[2].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #2");
+ checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #2");
+ is(result[3].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #2");
+ is(result[4].type, "input",
+ "runCompositionCommitTest: input should be fired after dispatching compositioncommit #2");
+ checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #2");
+ is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #2");
+
+ // compositioncommit with empty composition string.
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #3");
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "KEY_Enter", type: "keydown" },
+ });
+ is(textarea.value, "", "runCompositionCommitTest: textarea has non-empty composition string #3");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });
+
+ is(result.length, 5,
+ "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #3");
+ is(result[0].type, "compositionupdate",
+ "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #3");
+ is(result[1].type, "text",
+ "runCompositionCommitTest: text should be fired after dispatching compositioncommit #3");
+ is(result[2].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #3");
+ checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #3");
+ is(result[3].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #3");
+ is(result[4].type, "input",
+ "runCompositionCommitTest: input should be fired after dispatching compositioncommit #3");
+ checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #3");
+ is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #3");
+
+ // inserting empty string with simple composition.
+ textarea.value = "abc";
+ textarea.setSelectionRange(3, 3);
+ synthesizeComposition({ type: "compositionstart" });
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "" });
+
+ is(result.length, 4,
+ "runCompositionCommitTest: 4 events should be fired when inserting empty string with composition");
+ is(result[0].type, "text",
+ "runCompositionCommitTest: text should be fired when inserting empty string with composition");
+ is(result[1].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired when inserting empty string with composition");
+ checkInputEvent(result[1], true, "insertCompositionText", "", [],
+ "runCompositionCommitTest: when inserting empty string with composition");
+ is(result[2].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired when inserting empty string with composition");
+ is(result[3].type, "input",
+ "runCompositionCommitTest: input should be fired when inserting empty string with composition");
+ checkInputEvent(result[3], false, "insertCompositionText", "", [],
+ "runCompositionCommitTest: when inserting empty string with composition");
+ is(textarea.value, "abc",
+ "runCompositionCommitTest: textarea should keep original value when inserting empty string with composition");
+
+ // replacing selection with empty string with simple composition.
+ textarea.value = "abc";
+ textarea.setSelectionRange(0, 3);
+ synthesizeComposition({ type: "compositionstart" });
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "" });
+
+ is(result.length, 4,
+ "runCompositionCommitTest: 4 events should be fired when replacing with empty string with composition");
+ is(result[0].type, "text",
+ "runCompositionCommitTest: text should be fired when replacing with empty string with composition");
+ is(result[1].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired when replacing with empty string with composition");
+ checkInputEvent(result[1], true, "insertCompositionText", "", [],
+ "runCompositionCommitTest: when replacing with empty string with composition");
+ is(result[2].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired when replacing with empty string with composition");
+ is(result[3].type, "input",
+ "runCompositionCommitTest: input should be fired when replacing with empty string with composition");
+ checkInputEvent(result[3], false, "insertCompositionText", "", [],
+ "runCompositionCommitTest: when replacing with empty string with composition");
+ is(textarea.value, "",
+ "runCompositionCommitTest: textarea should become empty when replacing selection with empty string with composition");
+
+ // replacing selection with same string with simple composition.
+ textarea.value = "abc";
+ textarea.setSelectionRange(0, 3);
+ synthesizeComposition({ type: "compositionstart" });
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "abc" });
+
+ is(result.length, 5,
+ "runCompositionCommitTest: 5 events should be fired when replacing selection with same string with composition");
+ is(result[0].type, "compositionupdate",
+ "runCompositionCommitTest: compositionupdate should be fired when replacing selection with same string with composition");
+ is(result[1].type, "text",
+ "runCompositionCommitTest: text should be fired when replacing selection with same string with composition");
+ is(result[2].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired when replacing selection with same string with composition");
+ checkInputEvent(result[2], true, "insertCompositionText", "abc", [],
+ "runCompositionCommitTest: when replacing selection with same string with composition");
+ is(result[3].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired when replacing selection with same string with composition");
+ is(result[4].type, "input",
+ "runCompositionCommitTest: input should be fired when replacing selection with same string with composition");
+ checkInputEvent(result[4], false, "insertCompositionText", "abc", [],
+ "runCompositionCommitTest: when replacing selection with same string with composition");
+ is(textarea.value, "abc",
+ "runCompositionCommitTest: textarea should keep same value when replacing selection with same string with composition");
+
+ // compositioncommit with non-empty composition string.
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #4");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Enter" } });
+
+ is(result.length, 5,
+ "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #4");
+ is(result[0].type, "compositionupdate",
+ "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #4");
+ is(result[1].type, "text",
+ "runCompositionCommitTest: text should be fired after dispatching compositioncommit #4");
+ is(result[2].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #4");
+ checkInputEvent(result[2], true, "insertCompositionText", "", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #4");
+ is(result[3].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #4");
+ is(result[4].type, "input",
+ "runCompositionCommitTest: input should be fired after dispatching compositioncommit #4");
+ checkInputEvent(result[4], false, "insertCompositionText", "", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #4");
+ is(textarea.value, "", "runCompositionCommitTest: textarea should be empty #4");
+
+ // compositioncommit immediately without compositionstart
+ textarea.value = "";
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "a" } });
+
+ is(result.length, 5,
+ "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #5");
+ is(result[0].type, "compositionupdate",
+ "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #5");
+ is(result[1].type, "text",
+ "runCompositionCommitTest: text should be fired after dispatching compositioncommit #5");
+ is(result[2].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #5");
+ checkInputEvent(result[2], true, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #5");
+ is(result[3].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5");
+ is(result[4].type, "input",
+ "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5");
+ checkInputEvent(result[4], false, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #5");
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should be empty #5");
+
+ // compositioncommit with same composition string.
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #5");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter" } });
+
+ is(result.length, 4,
+ "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #6");
+ is(result[0].type, "text",
+ "runCompositionCommitTest: text should be fired after dispatching compositioncommit #6");
+ is(result[1].type, "beforeinput",
+ "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #6");
+ checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #6");
+ is(result[2].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #6");
+ is(result[3].type, "input",
+ "runCompositionCommitTest: input should be fired after dispatching compositioncommit #6");
+ checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #6");
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6");
+
+ // compositioncommit with same composition string when there is committed string
+ textarea.value = "";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6");
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "KEY_Enter", type: "keydown" },
+ });
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter", type: "keyup" } });
+
+ is(result.length, 2,
+ "runCompositionCommitTest: 2 events should be fired after dispatching compositioncommit #7");
+ // XXX Do we need a "beforeinput" event here? Not sure.
+ is(result[0].type, "compositionend",
+ "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #7");
+ is(result[1].type, "input",
+ "runCompositionCommitTest: input should be fired after dispatching compositioncommit #7");
+ checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [],
+ "runCompositionCommitTest: after dispatching compositioncommit #7");
+ is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6");
+
+ textarea.removeEventListener("compositionupdate", handler, true);
+ textarea.removeEventListener("compositionend", handler, true);
+ textarea.removeEventListener("beforeinput", handler, true);
+ textarea.removeEventListener("input", handler, true);
+ textarea.removeEventListener("text", handler, true);
+}
+
+// eslint-disable-next-line complexity
+async function runCompositionTest()
+{
+ textarea.value = "";
+ textarea.focus();
+ let caretRects = [];
+
+ let caretRect = synthesizeQueryCaretRect(0);
+ if (!checkQueryContentResult(caretRect,
+ "runCompositionTest: synthesizeQueryCaretRect #0")) {
+ return;
+ }
+ caretRects[0] = caretRect;
+
+ // input first character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "o" },
+ });
+
+ if (!checkContent("\u3089", "runCompositionTest", "#1-1") ||
+ !checkSelection(1, "", "runCompositionTest", "#1-1")) {
+ return;
+ }
+
+ caretRect = synthesizeQueryCaretRect(1);
+ if (!checkQueryContentResult(caretRect,
+ "runCompositionTest: synthesizeQueryCaretRect #1-1")) {
+ return;
+ }
+ caretRects[1] = caretRect;
+
+ // input second character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: "\\", code: "IntlYen", keyCode: KeyboardEvent.DOM_VK_BACKSLASH },
+ });
+
+ if (!checkContent("\u3089\u30FC", "runCompositionTest", "#1-2") ||
+ !checkSelection(2, "", "runCompositionTest", "#1-2")) {
+ return;
+ }
+
+ caretRect = synthesizeQueryCaretRect(2);
+ if (!checkQueryContentResult(caretRect,
+ "runCompositionTest: synthesizeQueryCaretRect #1-2")) {
+ return;
+ }
+ caretRects[2] = caretRect;
+
+ isnot(caretRects[2].left, caretRects[1].left,
+ "runCompositionTest: caret isn't moved (#1-2)");
+ is(caretRects[2].top, caretRects[1].top,
+ "runCompositionTest: caret is moved to another line (#1-2)");
+ is(caretRects[2].width, caretRects[1].width,
+ "runCompositionTest: caret width is wrong (#1-2)");
+ is(caretRects[2].height, caretRects[1].height,
+ "runCompositionTest: caret width is wrong (#1-2)");
+
+ // input third character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ "key": { key: "/" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3") ||
+ !checkSelection(3, "", "runCompositionTest", "#1-3")) {
+ return;
+ }
+
+ caretRect = synthesizeQueryCaretRect(3);
+ if (!checkQueryContentResult(caretRect,
+ "runCompositionTest: synthesizeQueryCaretRect #1-3")) {
+ return;
+ }
+ caretRects[3] = caretRect;
+
+ isnot(caretRects[3].left, caretRects[2].left,
+ "runCompositionTest: caret isn't moved (#1-3)");
+ is(caretRects[3].top, caretRects[2].top,
+ "runCompositionTest: caret is moved to another line (#1-3)");
+ is(caretRects[3].width, caretRects[2].width,
+ "runCompositionTest: caret width is wrong (#1-3)");
+ is(caretRects[3].height, caretRects[2].height,
+ "runCompositionTest: caret height is wrong (#1-3)");
+
+ // moves the caret left
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: "KEY_ArrowLeft" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-1") ||
+ !checkSelection(2, "", "runCompositionTest", "#1-3-1")) {
+ return;
+ }
+
+
+ caretRect = synthesizeQueryCaretRect(2);
+ if (!checkQueryContentResult(caretRect,
+ "runCompositionTest: synthesizeQueryCaretRect #1-3-1")) {
+ return;
+ }
+
+ is(caretRect.left, caretRects[2].left,
+ "runCompositionTest: caret rects are different (#1-3-1, left)");
+ is(caretRect.top, caretRects[2].top,
+ "runCompositionTest: caret rects are different (#1-3-1, top)");
+ // by bug 335359, the caret width depends on the right side's character.
+ is(caretRect.width, caretRects[2].width + Math.round(window.devicePixelRatio),
+ "runCompositionTest: caret rects are different (#1-3-1, width)");
+ is(caretRect.height, caretRects[2].height,
+ "runCompositionTest: caret rects are different (#1-3-1, height)");
+
+ // moves the caret left
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "KEY_ArrowLeft" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-2") ||
+ !checkSelection(1, "", "runCompositionTest", "#1-3-2")) {
+ return;
+ }
+
+
+ caretRect = synthesizeQueryCaretRect(1);
+ if (!checkQueryContentResult(caretRect,
+ "runCompositionTest: synthesizeQueryCaretRect #1-3-2")) {
+ return;
+ }
+
+ is(caretRect.left, caretRects[1].left,
+ "runCompositionTest: caret rects are different (#1-3-2, left)");
+ is(caretRect.top, caretRects[1].top,
+ "runCompositionTest: caret rects are different (#1-3-2, top)");
+ // by bug 335359, the caret width depends on the right side's character.
+ is(caretRect.width, caretRects[1].width + Math.round(window.devicePixelRatio),
+ "runCompositionTest: caret rects are different (#1-3-2, width)");
+ is(caretRect.height, caretRects[1].height,
+ "runCompositionTest: caret rects are different (#1-3-2, height)");
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 },
+ "key": { key: "y" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093", "runCompositionTest", "#1-4") ||
+ !checkSelection(4, "", "runCompositionTest", "#1-4")) {
+ return;
+ }
+
+
+ // backspace
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ "key": { key: "KEY_Backspace" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-5") ||
+ !checkSelection(3, "", "runCompositionTest", "#1-5")) {
+ return;
+ }
+
+ // re-input
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 },
+ "key": { key: "y" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093", "runCompositionTest", "#1-6") ||
+ !checkSelection(4, "", "runCompositionTest", "#1-6")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055",
+ "clauses":
+ [
+ { "length": 5, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 5, "length": 0 },
+ "key": { key: "x" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055", "runCompositionTest", "#1-7") ||
+ !checkSelection(5, "", "runCompositionTest", "#1-7")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044",
+ "clauses":
+ [
+ { "length": 6, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 6, "length": 0 },
+ "key": { key: "e" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044", "runCompositionTest", "#1-8") ||
+ !checkSelection(6, "", "runCompositionTest", "#1-8")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053",
+ "clauses":
+ [
+ { "length": 7, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 7, "length": 0 },
+ "key": { key: "b" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", "runCompositionTest", "#1-8") ||
+ !checkSelection(7, "", "runCompositionTest", "#1-8")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046",
+ "clauses":
+ [
+ { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 8, "length": 0 },
+ "key": { key: "4" },
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046",
+ "runCompositionTest", "#1-9") ||
+ !checkSelection(8, "", "runCompositionTest", "#1-9")) {
+ return;
+ }
+
+ // convert
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8",
+ "clauses":
+ [
+ { "length": 4,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 2,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 },
+ "key": { key: " " },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8",
+ "runCompositionTest", "#1-10") ||
+ !checkSelection(4, "", "runCompositionTest", "#1-10")) {
+ return;
+ }
+
+ // change the selected clause
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8",
+ "clauses":
+ [
+ { "length": 4,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": 2,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 6, "length": 0 },
+ "key": { key: "KEY_ArrowLeft", shiftKey: true },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8",
+ "runCompositionTest", "#1-11") ||
+ !checkSelection(6, "", "runCompositionTest", "#1-11")) {
+ return;
+ }
+
+ // reset clauses
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046",
+ "clauses":
+ [
+ { "length": 5,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 3,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 5, "length": 0 },
+ "key": { key: "KEY_ArrowRight" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046",
+ "runCompositionTest", "#1-12") ||
+ !checkSelection(5, "", "runCompositionTest", "#1-12")) {
+ return;
+ }
+
+
+ let textRect1 = synthesizeQueryTextRect(0, 1);
+ let textRect2 = synthesizeQueryTextRect(1, 1);
+ if (!checkQueryContentResult(textRect1,
+ "runCompositionTest: synthesizeQueryTextRect #1-12-1") ||
+ !checkQueryContentResult(textRect2,
+ "runCompositionTest: synthesizeQueryTextRect #1-12-2")) {
+ return;
+ }
+
+ // commit the composition string
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046",
+ "runCompositionTest", "#1-13") ||
+ !checkSelection(8, "", "runCompositionTest", "#1-13")) {
+ return;
+ }
+
+ let textRect3 = synthesizeQueryTextRect(0, 1);
+ let textRect4 = synthesizeQueryTextRect(1, 1);
+
+ if (!checkQueryContentResult(textRect3,
+ "runCompositionTest: synthesizeQueryTextRect #1-13-1") ||
+ !checkQueryContentResult(textRect4,
+ "runCompositionTest: synthesizeQueryTextRect #1-13-2")) {
+ return;
+ }
+
+ checkRect(textRect3, textRect1, "runCompositionTest: textRect #1-13-1");
+ checkRect(textRect4, textRect2, "runCompositionTest: textRect #1-13-2");
+
+ // restart composition and input characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3057",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "d" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3057",
+ "runCompositionTest", "#2-1") ||
+ !checkSelection(8 + 1, "", "runCompositionTest", "#2-1")) {
+ return;
+ }
+
+ let textRect3QueriedWithRelativeOffset = synthesizeQueryTextRect(-8, 1, true);
+ let textRect4QueriedWithRelativeOffset = synthesizeQueryTextRect(-8 + 1, 1, true);
+ checkRect(textRect3QueriedWithRelativeOffset, textRect3, "runCompositionTest: textRect #2-1-2");
+ checkRect(textRect4QueriedWithRelativeOffset, textRect4, "runCompositionTest: textRect #2-1-3");
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3058",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "r" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058",
+ "runCompositionTest", "#2-2") ||
+ !checkSelection(8 + 1, "", "runCompositionTest", "#2-2")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3058\u3087",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: ")", code: "Digit9", keyCode: KeyboardEvent.DOM_VK_9, shiftKey: true },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087",
+ "runCompositionTest", "#2-3") ||
+ !checkSelection(8 + 2, "", "runCompositionTest", "#2-3")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3058\u3087\u3046",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ "key": { key: "4" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087\u3046",
+ "runCompositionTest", "#2-4") ||
+ !checkSelection(8 + 3, "", "runCompositionTest", "#2-4")) {
+ return;
+ }
+
+ // commit the composition string
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087\u3046",
+ "runCompositionTest", "#2-4") ||
+ !checkSelection(8 + 3, "", "runCompositionTest", "#2-4")) {
+ return;
+ }
+
+ // set selection
+ const selectionSetTest = await synthesizeSelectionSet(4, 7, false);
+ ok(selectionSetTest, "runCompositionTest: selectionSetTest failed");
+
+ if (!checkSelection(4, "\u3055\u884C\u3053\u3046\u3058\u3087\u3046", "runCompositionTest", "#3-1")) {
+ return;
+ }
+
+ // start composition with selection
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u304A",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "6" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u304A",
+ "runCompositionTest", "#3-2") ||
+ !checkSelection(4 + 1, "", "runCompositionTest", "#3-2")) {
+ return;
+ }
+
+ // remove the composition string
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "KEY_Backspace" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#3-3") ||
+ !checkSelection(4, "", "runCompositionTest", "#3-3")) {
+ return;
+ }
+
+ // re-input the composition string
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3046",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "4" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3046",
+ "runCompositionTest", "#3-4") ||
+ !checkSelection(4 + 1, "", "runCompositionTest", "#3-4")) {
+ return;
+ }
+
+ // cancel the composition
+ synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Escape" } });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#3-5") ||
+ !checkSelection(4, "", "runCompositionTest", "#3-5")) {
+ return;
+ }
+
+ // bug 271815, some Chinese IMEs for Linux make empty composition string
+ // and compty clause information when it lists up Chinese characters on
+ // its candidate window.
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "a" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#4-1") ||
+ !checkSelection(4, "", "runCompositionTest", "#4-1")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "b" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#4-2") ||
+ !checkSelection(4, "", "runCompositionTest", "#4-2")) {
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommit", data: "\u6700", key: { key: "KEY_Enter" } });
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700",
+ "runCompositionTest", "#4-3") ||
+ !checkSelection(5, "", "runCompositionTest", "#4-3")) {
+ return;
+ }
+
+ // testing the canceling case
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "a" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700",
+ "runCompositionTest", "#4-5") ||
+ !checkSelection(5, "", "runCompositionTest", "#4-5")) {
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape" } });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700",
+ "runCompositionTest", "#4-6") ||
+ !checkSelection(5, "", "runCompositionTest", "#4-6")) {
+ return;
+ }
+
+ // testing whether the empty composition string deletes selected string.
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "a" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#4-8") ||
+ !checkSelection(4, "", "runCompositionTest", "#4-8")) {
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommit", data: "\u9AD8", key: { key: "KEY_Enter" } });
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u9AD8",
+ "runCompositionTest", "#4-9") ||
+ !checkSelection(5, "", "runCompositionTest", "#4-9")) {
+ return;
+ }
+
+ synthesizeKey("KEY_Backspace");
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#4-11") ||
+ !checkSelection(4, "", "runCompositionTest", "#4-11")) {
+ return;
+ }
+
+ // bug 23558, ancient Japanese IMEs on Window may send empty text event
+ // twice at canceling composition.
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u6700",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700",
+ "runCompositionTest", "#5-1") ||
+ !checkSelection(4 + 1, "", "runCompositionTest", "#5-1")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ "caret": { "start": 0, "length": 0 },
+ "key": { key: "KEY_Backspace", type: "keydown" },
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#5-2") ||
+ !checkSelection(4, "", "runCompositionTest", "#5-2")) {
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Backspace", type: "keyup" } });
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#5-3") ||
+ !checkSelection(4, "", "runCompositionTest", "#5-3")) {
+ return;
+ }
+
+ // Undo tests for the testcases for bug 23558 and bug 271815
+ synthesizeKey("z", { accelKey: true });
+
+ // XXX this is unexpected behavior, see bug 258291
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#6-1") ||
+ !checkSelection(4, "", "runCompositionTest", "#6-1")) {
+ return;
+ }
+
+ synthesizeKey("z", { accelKey: true });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u9AD8",
+ "runCompositionTest", "#6-2") ||
+ !checkSelection(5, "", "runCompositionTest", "#6-2")) {
+ return;
+ }
+
+ synthesizeKey("z", { accelKey: true });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700",
+ "runCompositionTest", "#6-3") ||
+ !checkSelection(4, "", "runCompositionTest", "#6-3")) {
+ return;
+ }
+
+ synthesizeKey("z", { accelKey: true });
+
+ // XXX this is unexpected behavior, see bug 258291
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700",
+ "runCompositionTest", "#6-4") ||
+ !checkSelection(5, "", "runCompositionTest", "#6-4")) {
+ return;
+ }
+
+ synthesizeKey("z", { accelKey: true });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3",
+ "runCompositionTest", "#6-5") ||
+ !checkSelection(4, "", "runCompositionTest", "#6-5")) {
+ // eslint-disable-next-line no-useless-return
+ return;
+ }
+}
+
+function runCompositionEventTest()
+{
+ const kDescription = "runCompositionEventTest: ";
+ const kEvents = ["compositionstart", "compositionupdate", "compositionend",
+ "input"];
+
+ input.value = "";
+ input.focus();
+
+ let windowEventCounts = [], windowEventData = [], windowEventLocale = [];
+ let inputEventCounts = [], inputEventData = [], inputEventLocale = [];
+ let preventDefault = false;
+ let stopPropagation = false;
+
+ function initResults()
+ {
+ for (let i = 0; i < kEvents.length; i++) {
+ windowEventCounts[kEvents[i]] = 0;
+ windowEventData[kEvents[i]] = "";
+ windowEventLocale[kEvents[i]] = "";
+ inputEventCounts[kEvents[i]] = 0;
+ inputEventData[kEvents[i]] = "";
+ inputEventLocale[kEvents[i]] = "";
+ }
+ }
+
+ function compositionEventHandlerForWindow(aEvent)
+ {
+ windowEventCounts[aEvent.type]++;
+ windowEventData[aEvent.type] = aEvent.data;
+ windowEventLocale[aEvent.type] = aEvent.locale;
+ if (preventDefault) {
+ aEvent.preventDefault();
+ }
+ if (stopPropagation) {
+ aEvent.stopPropagation();
+ }
+ }
+
+ function formEventHandlerForWindow(aEvent)
+ {
+ ok(aEvent.isTrusted, "input events must be trusted events");
+ windowEventCounts[aEvent.type]++;
+ windowEventData[aEvent.type] = input.value;
+ }
+
+ function compositionEventHandlerForInput(aEvent)
+ {
+ inputEventCounts[aEvent.type]++;
+ inputEventData[aEvent.type] = aEvent.data;
+ inputEventLocale[aEvent.type] = aEvent.locale;
+ if (preventDefault) {
+ aEvent.preventDefault();
+ }
+ if (stopPropagation) {
+ aEvent.stopPropagation();
+ }
+ }
+
+ function formEventHandlerForInput(aEvent)
+ {
+ inputEventCounts[aEvent.type]++;
+ inputEventData[aEvent.type] = input.value;
+ }
+
+ window.addEventListener("compositionstart", compositionEventHandlerForWindow,
+ true, true);
+ window.addEventListener("compositionend", compositionEventHandlerForWindow,
+ true, true);
+ window.addEventListener("compositionupdate", compositionEventHandlerForWindow,
+ true, true);
+ window.addEventListener("input", formEventHandlerForWindow,
+ true, true);
+
+ input.addEventListener("compositionstart", compositionEventHandlerForInput,
+ true, true);
+ input.addEventListener("compositionend", compositionEventHandlerForInput,
+ true, true);
+ input.addEventListener("compositionupdate", compositionEventHandlerForInput,
+ true, true);
+ input.addEventListener("input", formEventHandlerForInput,
+ true, true);
+
+ // test for normal case
+ initResults();
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "o" },
+ });
+
+ is(windowEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by window #1");
+ is(windowEventData.compositionstart, "",
+ kDescription + "data of compositionstart isn't empty (window) #1");
+ is(windowEventLocale.compositionstart, "",
+ kDescription + "locale of compositionstart isn't empty (window) #1");
+ is(inputEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by input #1");
+ is(inputEventData.compositionstart, "",
+ kDescription + "data of compositionstart isn't empty (input) #1");
+ is(inputEventLocale.compositionstart, "",
+ kDescription + "locale of compositionstart isn't empty (input) #1");
+
+ is(windowEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by window #1");
+ is(windowEventData.compositionupdate, "\u3089",
+ kDescription + "data of compositionupdate doesn't match (window) #1");
+ is(windowEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (window) #1");
+ is(inputEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by input #1");
+ is(inputEventData.compositionupdate, "\u3089",
+ kDescription + "data of compositionupdate doesn't match (input) #1");
+ is(inputEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (input) #1");
+
+ is(windowEventCounts.compositionend, 0,
+ kDescription + "compositionend has been handled by window #1");
+ is(inputEventCounts.compositionend, 0,
+ kDescription + "compositionend has been handled by input #1");
+
+ is(windowEventCounts.input, 1,
+ kDescription + "input hasn't been handled by window #1");
+ is(windowEventData.input, "\u3089",
+ kDescription + "value of input element wasn't modified (window) #1");
+ is(inputEventCounts.input, 1,
+ kDescription + "input hasn't been handled by input #1");
+ is(inputEventData.input, "\u3089",
+ kDescription + "value of input element wasn't modified (input) #1");
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ "key": { key: "\\", code: "IntlYen", keyCode: KeyboardEvent.DOM_VK_BACKSLASH },
+ });
+
+ is(windowEventCounts.compositionstart, 1,
+ kDescription + "compositionstart has been handled more than once by window #2");
+ is(inputEventCounts.compositionstart, 1,
+ kDescription + "compositionstart has been handled more than once by input #2");
+
+ is(windowEventCounts.compositionupdate, 2,
+ kDescription + "compositionupdate hasn't been handled by window #2");
+ is(windowEventData.compositionupdate, "\u3089\u30FC",
+ kDescription + "data of compositionupdate doesn't match (window) #2");
+ is(windowEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (window) #2");
+ is(inputEventCounts.compositionupdate, 2,
+ kDescription + "compositionupdate hasn't been handled by input #2");
+ is(inputEventData.compositionupdate, "\u3089\u30FC",
+ kDescription + "data of compositionupdate doesn't match (input) #2");
+ is(inputEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (input) #2");
+
+ is(windowEventCounts.compositionend, 0,
+ kDescription + "compositionend has been handled during composition by window #2");
+ is(inputEventCounts.compositionend, 0,
+ kDescription + "compositionend has been handled during composition by input #2");
+
+ is(windowEventCounts.input, 2,
+ kDescription + "input hasn't been handled by window #2");
+ is(windowEventData.input, "\u3089\u30FC",
+ kDescription + "value of input element wasn't modified (window) #2");
+ is(inputEventCounts.input, 2,
+ kDescription + "input hasn't been handled by input #2");
+ is(inputEventData.input, "\u3089\u30FC",
+ kDescription + "value of input element wasn't modified (input) #2");
+
+ // text event shouldn't cause composition update, e.g., at committing.
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ is(windowEventCounts.compositionstart, 1,
+ kDescription + "compositionstart has been handled more than once by window #3");
+ is(inputEventCounts.compositionstart, 1,
+ kDescription + "compositionstart has been handled more than once by input #3");
+
+ is(windowEventCounts.compositionupdate, 2,
+ kDescription + "compositionupdate has been fired unexpectedly on window #3");
+ is(inputEventCounts.compositionupdate, 2,
+ kDescription + "compositionupdate has been fired unexpectedly on input #3");
+
+ is(windowEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by window #3");
+ is(windowEventData.compositionend, "\u3089\u30FC",
+ kDescription + "data of compositionend doesn't match (window) #3");
+ is(windowEventLocale.compositionend, "",
+ kDescription + "locale of compositionend isn't empty (window) #3");
+ is(inputEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by input #3");
+ is(inputEventData.compositionend, "\u3089\u30FC",
+ kDescription + "data of compositionend doesn't match (input) #3");
+ is(inputEventLocale.compositionend, "",
+ kDescription + "locale of compositionend isn't empty (input) #3");
+
+ is(windowEventCounts.input, 3,
+ kDescription + "input hasn't been handled by window #3");
+ is(windowEventData.input, "\u3089\u30FC",
+ kDescription + "value of input element wasn't modified (window) #3");
+ is(inputEventCounts.input, 3,
+ kDescription + "input hasn't been handled by input #3");
+ is(inputEventData.input, "\u3089\u30FC",
+ kDescription + "value of input element wasn't modified (input) #3");
+
+ // select the second character, then, data of composition start should be
+ // the selected character.
+ initResults();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "o" },
+ });
+
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ is(windowEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by window #4");
+ is(windowEventData.compositionstart, "\u30FC",
+ kDescription + "data of compositionstart is empty (window) #4");
+ is(windowEventLocale.compositionstart, "",
+ kDescription + "locale of compositionstart isn't empty (window) #4");
+ is(inputEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by input #4");
+ is(inputEventData.compositionstart, "\u30FC",
+ kDescription + "data of compositionstart is empty (input) #4");
+ is(inputEventLocale.compositionstart, "",
+ kDescription + "locale of compositionstart isn't empty (input) #4");
+
+ is(windowEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by window #4");
+ is(windowEventData.compositionupdate, "\u3089",
+ kDescription + "data of compositionupdate doesn't match (window) #4");
+ is(windowEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (window) #4");
+ is(inputEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by input #4");
+ is(inputEventData.compositionupdate, "\u3089",
+ kDescription + "data of compositionupdate doesn't match (input) #4");
+ is(inputEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (input) #4");
+
+ is(windowEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by window #4");
+ is(windowEventData.compositionend, "\u3089",
+ kDescription + "data of compositionend doesn't match (window) #4");
+ is(windowEventLocale.compositionend, "",
+ kDescription + "locale of compositionend isn't empty (window) #4");
+ is(inputEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by input #4");
+ is(inputEventData.compositionend, "\u3089",
+ kDescription + "data of compositionend doesn't match (input) #4");
+ is(inputEventLocale.compositionend, "",
+ kDescription + "locale of compositionend isn't empty (input) #4");
+
+ is(windowEventCounts.input, 2,
+ kDescription + "input hasn't been handled by window #4");
+ is(windowEventData.input, "\u3089\u3089",
+ kDescription + "value of input element wasn't modified (window) #4");
+ is(inputEventCounts.input, 2,
+ kDescription + "input hasn't been handled by input #4");
+ is(inputEventData.input, "\u3089\u3089",
+ kDescription + "value of input element wasn't modified (input) #4");
+
+ // preventDefault() should effect nothing.
+ preventDefault = true;
+
+ initResults();
+ synthesizeKey("a", { accelKey: true }); // Select All
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306D",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "," },
+ });
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ is(windowEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by window #5");
+ is(windowEventData.compositionstart, "\u3089\u3089",
+ kDescription + "data of compositionstart is empty (window) #5");
+ is(windowEventLocale.compositionstart, "",
+ kDescription + "locale of compositionstart isn't empty (window) #5");
+ is(inputEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by input #5");
+ is(inputEventData.compositionstart, "\u3089\u3089",
+ kDescription + "data of compositionstart is empty (input) #5");
+ is(inputEventLocale.compositionstart, "",
+ kDescription + "locale of compositionstart isn't empty (input) #5");
+
+ is(windowEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by window #5");
+ is(windowEventData.compositionupdate, "\u306D",
+ kDescription + "data of compositionupdate doesn't match (window) #5");
+ is(windowEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (window) #5");
+ is(inputEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by input #5");
+ is(inputEventData.compositionupdate, "\u306D",
+ kDescription + "data of compositionupdate doesn't match (input) #5");
+ is(inputEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty (input) #5");
+
+ is(windowEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by window #5");
+ is(windowEventData.compositionend, "\u306D",
+ kDescription + "data of compositionend doesn't match (window) #5");
+ is(windowEventLocale.compositionend, "",
+ kDescription + "locale of compositionend isn't empty (window) #5");
+ is(inputEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by input #5");
+ is(inputEventData.compositionend, "\u306D",
+ kDescription + "data of compositionend doesn't match (input) #5");
+ is(inputEventLocale.compositionend, "",
+ kDescription + "locale of compositionend isn't empty (input) #5");
+
+ is(windowEventCounts.input, 2,
+ kDescription + "input hasn't been handled by window #5");
+ is(windowEventData.input, "\u306D",
+ kDescription + "value of input element wasn't modified (window) #5");
+ is(inputEventCounts.input, 2,
+ kDescription + "input hasn't been handled by input #5");
+ is(inputEventData.input, "\u306D",
+ kDescription + "value of input element wasn't modified (input) #5");
+
+ preventDefault = false;
+
+ // stopPropagation() should effect nothing (except event count)
+ stopPropagation = true;
+
+ initResults();
+ synthesizeKey("a", { accelKey: true }); // Select All
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "\\", code: "IntlRo", keyCode: KeyboardEvent.DOM_VK_BACKSLASH },
+ });
+
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ is(windowEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by window #6");
+ is(windowEventData.compositionstart, "\u306D",
+ kDescription + "data of compositionstart is empty #6");
+ is(windowEventLocale.compositionstart, "",
+ kDescription + "locale of compositionstart isn't empty #6");
+ is(inputEventCounts.compositionstart, 0,
+ kDescription + "compositionstart has been handled by input #6");
+
+ is(windowEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by window #6");
+ is(windowEventData.compositionupdate, "\u306E",
+ kDescription + "data of compositionupdate doesn't match #6");
+ is(windowEventLocale.compositionupdate, "",
+ kDescription + "locale of compositionupdate isn't empty #6");
+ is(inputEventCounts.compositionupdate, 0,
+ kDescription + "compositionupdate has been handled by input #6");
+
+ is(windowEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by window #6");
+ is(windowEventData.compositionend, "\u306E",
+ kDescription + "data of compositionend doesn't match #6");
+ is(windowEventLocale.compositionend, "",
+ kDescription + "locale of compositionend isn't empty #6");
+ is(inputEventCounts.compositionend, 0,
+ kDescription + "compositionend has been handled by input #6");
+
+ is(windowEventCounts.input, 2,
+ kDescription + "input hasn't been handled by window #6");
+ is(windowEventData.input, "\u306E",
+ kDescription + "value of input element wasn't modified (window) #6");
+ is(inputEventCounts.input, 2,
+ kDescription + "input hasn't been handled by input #6");
+ is(inputEventData.input, "\u306E",
+ kDescription + "value of input element wasn't modified (input) #6");
+
+ stopPropagation = false;
+
+ // create event and dispatch it.
+ initResults();
+
+ input.value = "value of input";
+ synthesizeKey("a", { accelKey: true }); // Select All
+
+ let compositionstart = document.createEvent("CompositionEvent");
+ compositionstart.initCompositionEvent("compositionstart",
+ true, true, document.defaultView,
+ "start data", "start locale");
+ is(compositionstart.type, "compositionstart",
+ kDescription + "type doesn't match #7");
+ is(compositionstart.data, "start data",
+ kDescription + "data doesn't match #7");
+ is(compositionstart.locale, "start locale",
+ kDescription + "locale doesn't match #7");
+ is(compositionstart.detail, 0,
+ kDescription + "detail isn't 0 #7");
+
+ input.dispatchEvent(compositionstart);
+
+ is(windowEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by window #7");
+ is(windowEventData.compositionstart, "start data",
+ kDescription + "data of compositionstart was changed (window) #7");
+ is(windowEventLocale.compositionstart, "start locale",
+ kDescription + "locale of compositionstart was changed (window) #7");
+ is(inputEventCounts.compositionstart, 1,
+ kDescription + "compositionstart hasn't been handled by input #7");
+ is(inputEventData.compositionstart, "start data",
+ kDescription + "data of compositionstart was changed (input) #7");
+ is(inputEventLocale.compositionstart, "start locale",
+ kDescription + "locale of compositionstart was changed (input) #7");
+
+ is(input.value, "value of input",
+ kDescription + "input value was changed #7");
+
+ let compositionupdate1 = document.createEvent("compositionevent");
+ compositionupdate1.initCompositionEvent("compositionupdate",
+ true, false, document.defaultView,
+ "composing string", "composing locale");
+ is(compositionupdate1.type, "compositionupdate",
+ kDescription + "type doesn't match #8");
+ is(compositionupdate1.data, "composing string",
+ kDescription + "data doesn't match #8");
+ is(compositionupdate1.locale, "composing locale",
+ kDescription + "locale doesn't match #8");
+ is(compositionupdate1.detail, 0,
+ kDescription + "detail isn't 0 #8");
+
+ input.dispatchEvent(compositionupdate1);
+
+ is(windowEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by window #8");
+ is(windowEventData.compositionupdate, "composing string",
+ kDescription + "data of compositionupdate was changed (window) #8");
+ is(windowEventLocale.compositionupdate, "composing locale",
+ kDescription + "locale of compositionupdate was changed (window) #8");
+ is(inputEventCounts.compositionupdate, 1,
+ kDescription + "compositionupdate hasn't been handled by input #8");
+ is(inputEventData.compositionupdate, "composing string",
+ kDescription + "data of compositionupdate was changed (input) #8");
+ is(inputEventLocale.compositionupdate, "composing locale",
+ kDescription + "locale of compositionupdate was changed (input) #8");
+
+ is(input.value, "value of input",
+ kDescription + "input value was changed #8");
+
+ let compositionupdate2 = document.createEvent("compositionEvent");
+ compositionupdate2.initCompositionEvent("compositionupdate",
+ true, false, document.defaultView,
+ "commit string", "commit locale");
+ is(compositionupdate2.type, "compositionupdate",
+ kDescription + "type doesn't match #9");
+ is(compositionupdate2.data, "commit string",
+ kDescription + "data doesn't match #9");
+ is(compositionupdate2.locale, "commit locale",
+ kDescription + "locale doesn't match #9");
+ is(compositionupdate2.detail, 0,
+ kDescription + "detail isn't 0 #9");
+
+ input.dispatchEvent(compositionupdate2);
+
+ is(windowEventCounts.compositionupdate, 2,
+ kDescription + "compositionupdate hasn't been handled by window #9");
+ is(windowEventData.compositionupdate, "commit string",
+ kDescription + "data of compositionupdate was changed (window) #9");
+ is(windowEventLocale.compositionupdate, "commit locale",
+ kDescription + "locale of compositionupdate was changed (window) #9");
+ is(inputEventCounts.compositionupdate, 2,
+ kDescription + "compositionupdate hasn't been handled by input #9");
+ is(inputEventData.compositionupdate, "commit string",
+ kDescription + "data of compositionupdate was changed (input) #9");
+ is(inputEventLocale.compositionupdate, "commit locale",
+ kDescription + "locale of compositionupdate was changed (input) #9");
+
+ is(input.value, "value of input",
+ kDescription + "input value was changed #9");
+
+ let compositionend = document.createEvent("Compositionevent");
+ compositionend.initCompositionEvent("compositionend",
+ true, false, document.defaultView,
+ "end data", "end locale");
+ is(compositionend.type, "compositionend",
+ kDescription + "type doesn't match #10");
+ is(compositionend.data, "end data",
+ kDescription + "data doesn't match #10");
+ is(compositionend.locale, "end locale",
+ kDescription + "locale doesn't match #10");
+ is(compositionend.detail, 0,
+ kDescription + "detail isn't 0 #10");
+
+ input.dispatchEvent(compositionend);
+
+ is(windowEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by window #10");
+ is(windowEventData.compositionend, "end data",
+ kDescription + "data of compositionend was changed (window) #10");
+ is(windowEventLocale.compositionend, "end locale",
+ kDescription + "locale of compositionend was changed (window) #10");
+ is(inputEventCounts.compositionend, 1,
+ kDescription + "compositionend hasn't been handled by input #10");
+ is(inputEventData.compositionend, "end data",
+ kDescription + "data of compositionend was changed (input) #10");
+ is(inputEventLocale.compositionend, "end locale",
+ kDescription + "locale of compositionend was changed (input) #10");
+
+ is(input.value, "value of input",
+ kDescription + "input value was changed #10");
+
+ window.removeEventListener("compositionstart",
+ compositionEventHandlerForWindow, true);
+ window.removeEventListener("compositionend",
+ compositionEventHandlerForWindow, true);
+ window.removeEventListener("compositionupdate",
+ compositionEventHandlerForWindow, true);
+ window.removeEventListener("input",
+ formEventHandlerForWindow, true);
+
+ input.removeEventListener("compositionstart",
+ compositionEventHandlerForInput, true);
+ input.removeEventListener("compositionend",
+ compositionEventHandlerForInput, true);
+ input.removeEventListener("compositionupdate",
+ compositionEventHandlerForInput, true);
+ input.removeEventListener("input",
+ formEventHandlerForInput, true);
+}
+
+function runCompositionTestWhoseTextNodeModified() {
+ const selection = windowOfContenteditable.getSelection();
+
+ (function testInsertTextBeforeComposition() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testInsertTextBeforeComposition:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>def</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "def".length);
+ // Insert composition to the end of a text node
+ synthesizeSimpleCompositionChange("g");
+ is(
+ textNode.data,
+ "defg",
+ `${description} Composition should be inserted to end of the text node`
+ );
+
+ // Insert a character before the composition string
+ textNode.insertData(0, "c");
+ is(
+ textNode.data,
+ "cdefg",
+ `${
+ description
+ } Composition should be shifted when a character is inserted before it`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}cdef`.length,
+ "g",
+ `${
+ description
+ } IME selection should be shifted when a character is inserted before it`
+ );
+
+ // Update composition string (appending a character)
+ synthesizeSimpleCompositionChange("gh");
+ is(
+ textNode.data,
+ "cdefgh",
+ `${
+ description
+ } Composition should be updated correctly after inserted a character before it`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}cdef`.length,
+ "gh",
+ `${
+ description
+ } IME selection should be extended correctly at updating composition after inserted a character before it`
+ );
+
+ // Insert another character before the composition
+ textNode.insertData(0, "b");
+ is(
+ textNode.data,
+ "bcdefgh",
+ `${
+ description
+ } Composition should be shifted when a character is inserted again before it`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}bcdef`.length,
+ "gh",
+ `${
+ description
+ } IME selection should be shifted when a character is inserted again before it`
+ );
+
+ // Update the composition string again (appending another character)
+ synthesizeSimpleCompositionChange("ghi");
+ is(
+ textNode.data,
+ "bcdefghi",
+ `${
+ description
+ } Composition should be updated correctly after inserted 2 characters before it`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}bcdef`.length,
+ "ghi",
+ `${
+ description
+ } IME selection should be extended correctly at updating composition after inserted 2 characters before it`
+ );
+
+ // Insert a new character before the composition string
+ textNode.insertData(0, "a");
+ is(
+ textNode.data,
+ "abcdefghi",
+ `${
+ description
+ } Composition should be shifted when a character is inserted again and again before it`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}abcdef`.length,
+ "ghi",
+ `${
+ description
+ } IME selection should be shifted when a character is inserted again and again before it`
+ );
+
+ // Commit the composition string
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abcdefghi",
+ `${
+ description
+ } Composition should be committed as is`
+ );
+ is(
+ selection.focusOffset,
+ "abcdefghi".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+
+ // Undo the commit
+ synthesizeKey("z", { accelKey: true });
+ is(
+ textNode.data,
+ "abcdef",
+ `${
+ description
+ } Composition should be undone correctly`
+ );
+ is(
+ selection.focusOffset,
+ "abcdef".length,
+ `${
+ description
+ } Selection should be collapsed at where the composition was after undoing`
+ );
+
+ // Redo the commit
+ synthesizeKey("z", { accelKey: true, shiftKey: true });
+ is(
+ textNode.data,
+ "abcdefghi",
+ `${
+ description
+ } Composition should be redone correctly`
+ );
+ is(
+ selection.focusOffset,
+ "abcdefghi".length,
+ `${
+ description
+ } focus offset of Selection should be at end of the commit string after redoing`
+ );
+ })();
+
+ (function testInsertTextImmediatelyBeforeComposition() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyBeforeComposition:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>d</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, 0);
+ // Insert composition at start of the text node
+ synthesizeSimpleCompositionChange("b");
+ is(
+ textNode.data,
+ "bd",
+ `${description} Composition should be inserted to start of the text node`
+ );
+
+ // Insert a character before the composition string
+ textNode.insertData(0, "a");
+ is(
+ textNode.data,
+ "abd",
+ `${
+ description
+ } Composition should be shifted when a character is inserted immediately before it`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "b",
+ `${
+ description
+ } IME selection should be shifted when a character is inserted immediately before it`,
+ "",
+ { offset: todo_is, text: todo_is }
+ );
+
+ // Update the composition string after inserting character immediately before it
+ synthesizeSimpleCompositionChange("bc");
+ is(
+ textNode.data,
+ "abcd",
+ `${description} Composition should be updated after the inserted character`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "bc",
+ `${
+ description
+ } IME selection should be set at the composition string after the inserted character`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abcd",
+ `${
+ description
+ } Composition should be committed after the inserted character`
+ );
+ is(
+ selection.focusOffset,
+ "abc".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ (function testInsertTextImmediatelyAfterComposition() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyAfterComposition:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>a</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "a".length);
+ // Insert composition at end of the text node
+ synthesizeSimpleCompositionChange("b");
+ is(
+ textNode.data,
+ "ab",
+ `${description} Composition should be inserted to start of the text node`
+ );
+
+ // Insert a character after the composition string
+ textNode.insertData("ab".length, "d");
+ is(
+ textNode.data,
+ "abd",
+ `${
+ description
+ } Composition should stay when a character is inserted immediately after it`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "b",
+ `${
+ description
+ } IME selection should stay when a character is inserted immediately after it`
+ );
+
+ // Update the composition string after inserting character immediately after it
+ synthesizeSimpleCompositionChange("bc");
+ is(
+ textNode.data,
+ "abcd",
+ `${description} Composition should be updated before the inserted character`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "bc",
+ `${
+ description
+ } IME selection should be set at the composition string before the inserted character`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abcd",
+ `${
+ description
+ } Composition should be committed before the inserted character`
+ );
+ is(
+ selection.focusOffset,
+ "abc".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ // Inserting/replacing text before the last character of composition string
+ // should be contained by the composition, i.e., updated by next composition
+ // update. This is Chrome compatible.
+ (function testInsertTextMiddleOfComposition() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testInsertTextMiddleOfComposition:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>a</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "a".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("bd");
+ is(
+ textNode.data,
+ "abd",
+ `${description} Composition should be inserted to end of the text node`
+ );
+
+ // Insert a character before the composition string
+ textNode.insertData("ab".length, "c");
+ is(
+ textNode.data,
+ "abcd",
+ `${
+ description
+ } Inserted string should inserted into the middle of composition string`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "bcd",
+ `${
+ description
+ } IME selection should be extended when a character is inserted into middle of it`
+ );
+
+ // Update the composition string after inserting character into it
+ synthesizeSimpleCompositionChange("BD");
+ is(
+ textNode.data,
+ "aBD",
+ `${
+ description
+ } Composition should be replace the range containing the inserted character`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "BD",
+ `${
+ description
+ } IME selection should be set at the updated composition string`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "aBD",
+ `${
+ description
+ } Composition should be committed without the inserted character`
+ );
+ is(
+ selection.focusOffset,
+ "aBD".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ (function testReplaceFirstCharOfCompositionString() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testReplaceFirstCharOfCompositionString:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>abfg</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "ab".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("cde");
+ is(
+ textNode.data,
+ "abcdefg",
+ `${description} Composition should be inserted`
+ );
+
+ // Replace the composition string
+ textNode.replaceData("ab".length, "c".length, "XYZ");
+ is(
+ textNode.data,
+ "abXYZdefg",
+ `${description} First character of the composition should be replaced`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "XYZde",
+ `${description} IME selection should contain the replace string`
+ );
+
+ // Update the composition string after replaced
+ synthesizeSimpleCompositionChange("CDE");
+ is(
+ textNode.data,
+ "abCDEfg",
+ `${description} Composition should update the replace string too`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "CDE",
+ `${description} IME selection should update the replace string too`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abCDEfg",
+ `${description} Composition should be committed`
+ );
+ is(
+ selection.focusOffset,
+ "abCDE".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ // Although Chrome commits composition if all composition string is removed,
+ // let's keep composition for making TSF stable...
+ (function testReplaceAllCompositionString() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testReplaceAllCompositionString:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>abfg</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "ab".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("cde");
+ is(
+ textNode.data,
+ "abcdefg",
+ `${description} Composition should be inserted to the text node`
+ );
+
+ // Replace the composition string
+ textNode.replaceData("ab".length, "cde".length, "XYZ");
+ is(
+ textNode.data,
+ "abXYZfg",
+ `${description} Composition should be replaced`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "",
+ `${
+ description
+ } IME selection should be collapsed before the replace string`
+ );
+
+ // Update the composition string after replaced
+ synthesizeSimpleCompositionChange("CDE");
+ is(
+ textNode.data,
+ "abCDEXYZfg",
+ `${description} Composition should be inserted again`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "CDE",
+ `${description} IME selection should not contain the replace string`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abCDEXYZfg",
+ `${description} Composition should be committed`
+ );
+ is(
+ selection.focusOffset,
+ "abCDE".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ (function testReplaceCompositionStringAndSurroundedCharacters() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testReplaceCompositionStringAndSurroundedCharacters:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>abfg</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "ab".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("cde");
+ is(
+ textNode.data,
+ "abcdefg",
+ `${description} Composition should be inserted to the text node`
+ );
+
+ // Replace the composition string
+ textNode.replaceData("a".length, "bcdef".length, "XYZ");
+ is(
+ textNode.data,
+ "aXYZg",
+ `${description} Composition should be replaced`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "",
+ `${
+ description
+ } IME selection should be collapsed before the replace string`
+ );
+
+ // Update the composition string after replaced
+ synthesizeSimpleCompositionChange("CDE");
+ is(
+ textNode.data,
+ "aCDEXYZg",
+ `${description} Composition should be inserted again`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "CDE",
+ `${description} IME selection should not contain the replace string`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "aCDEXYZg",
+ `${description} Composition should be committed`
+ );
+ is(
+ selection.focusOffset,
+ "aCDE".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ // If start boundary characters are replaced, the replace string should be
+ // contained into the composition range. This is Chrome compatible.
+ (function testReplaceStartBoundaryOfCompositionString() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testReplaceStartBoundaryOfCompositionString:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>abfg</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "ab".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("cde");
+ is(
+ textNode.data,
+ "abcdefg",
+ `${description} Composition should be inserted to the text node`
+ );
+
+ // Replace some text
+ textNode.replaceData("a".length, "bc".length, "XYZ");
+ is(
+ textNode.data,
+ "aXYZdefg",
+ `${
+ description
+ } Start of the composition should be replaced`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "XYZde",
+ `${description} IME selection should contain the replace string`
+ );
+
+ // Update the replace string and remaining composition.
+ synthesizeSimpleCompositionChange("CDE");
+ is(
+ textNode.data,
+ "aCDEfg",
+ `${description} Composition should update the replace string too`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}a`.length,
+ "CDE",
+ `${description} IME selection should contain the replace string`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "aCDEfg",
+ `${
+ description
+ } Composition should be committed`
+ );
+ is(
+ selection.focusOffset,
+ "aCDE".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ // If start boundary characters are replaced, the replace string should NOT
+ // be contained in the composition range. This is Chrome compatible.
+ (function testReplaceEndBoundaryOfCompositionString() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testReplaceEndBoundaryOfCompositionString:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>abfg</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "ab".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("cde");
+ is(
+ textNode.data,
+ "abcdefg",
+ `${description} Composition should be inserted to the text node`
+ );
+
+ // Replace the composition string
+ textNode.replaceData("abcd".length, "ef".length, "XYZ");
+ is(
+ textNode.data,
+ "abcdXYZg",
+ `${
+ description
+ } End half of the composition should be replaced`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "cd",
+ `${
+ description
+ } IME selection should be shrunken to the non-replaced part`
+ );
+
+ // Update the composition string after replaced
+ synthesizeSimpleCompositionChange("CDE");
+ is(
+ textNode.data,
+ "abCDEXYZg",
+ `${description} Only the remaining composition string should be updated`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "CDE",
+ `${description} IME selection should NOT include the replace string`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abCDEXYZg",
+ `${description} Composition should be committed`
+ );
+ is(
+ selection.focusOffset,
+ "abCDE".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ // If the last character of composition is replaced, i.e., it should NOT be
+ // treated as a part of composition string. This is Chrome compatible.
+ (function testReplaceLastCharOfCompositionString() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testReplaceLastCharOfCompositionString:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>abfg</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "ab".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("cde");
+ is(
+ textNode.data,
+ "abcdefg",
+ `${description} Composition should be inserted`
+ );
+
+ // Replace the composition string
+ textNode.replaceData("abcd".length, "e".length, "XYZ");
+ is(
+ textNode.data,
+ "abcdXYZfg",
+ `${description} Last character of the composition should be replaced`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "cd",
+ `${description} IME selection should be shrunken`
+ );
+
+ // Update the composition string after replaced
+ synthesizeSimpleCompositionChange("CDE");
+ is(
+ textNode.data,
+ "abCDEXYZfg",
+ `${description} Composition should NOT update the replace string`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "CDE",
+ `${description} IME selection should not contain the replace string`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abCDEXYZfg",
+ `${description} Composition should be committed`
+ );
+ is(
+ selection.focusOffset,
+ "abCDE".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+
+ (function testReplaceMiddleCharOfCompositionString() {
+ const description =
+ "runCompositionTestWhoseTextNodeModified: testReplaceMiddleCharOfCompositionString:";
+ contenteditable.focus();
+ contenteditable.innerHTML = "<p>abfg</p>";
+ const textNode = contenteditable.firstChild.firstChild;
+ selection.collapse(textNode, "ab".length);
+ // Insert composition at middle of the text node
+ synthesizeSimpleCompositionChange("cde");
+ is(
+ textNode.data,
+ "abcdefg",
+ `${description} Composition should be inserted`
+ );
+
+ // Replace the composition string
+ textNode.replaceData("abc".length, "d".length, "XYZ");
+ is(
+ textNode.data,
+ "abcXYZefg",
+ `${
+ description
+ } Middle character of the composition should be replaced`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "cXYZe",
+ `${description} IME selection should be extended by the replace string`
+ );
+
+ // Update the composition string after replaced
+ synthesizeSimpleCompositionChange("CDE");
+ is(
+ textNode.data,
+ "abCDEfg",
+ `${description} Composition should update the replace string`
+ );
+ checkIMESelection(
+ "RawClause",
+ true,
+ `${kLF}ab`.length,
+ "CDE",
+ `${description} IME selection should be shrunken after update`
+ );
+
+ // Commit it
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(
+ textNode.data,
+ "abCDEfg",
+ `${description} Composition should be committed`
+ );
+ is(
+ selection.focusOffset,
+ "abCDE".length,
+ `${description} Selection should be collapsed at end of the commit string`
+ );
+ })();
+}
+
+// eslint-disable-next-line complexity
+function runQueryTextRectInContentEditableTest()
+{
+ contenteditable.focus();
+
+ contenteditable.innerHTML = "<p>abc</p><p>def</p>";
+ // \n 0 123 4 567
+ // \r\n 01 234 56 789
+
+ let description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", ";
+
+ // "a"
+ let a = synthesizeQueryTextRect(kLFLen, 1);
+ if (!checkQueryContentResult(a, description + "rect for 'a'")) {
+ return;
+ }
+
+ // "b"
+ let b = synthesizeQueryTextRect(kLFLen + 1, 1);
+ if (!checkQueryContentResult(b, description + "rect for 'b'")) {
+ return;
+ }
+
+ is(b.top, a.top, description + "'a' and 'b' should be at same top");
+ isSimilarTo(b.left, a.left + a.width, 2, description + "left of 'b' should be at similar to right of 'a'");
+ is(b.height, a.height, description + "'a' and 'b' should be same height");
+
+ // "c"
+ let c = synthesizeQueryTextRect(kLFLen + 2, 1);
+ if (!checkQueryContentResult(c, description + "rect for 'c'")) {
+ return;
+ }
+
+ is(c.top, b.top, description + "'b' and 'c' should be at same top");
+ isSimilarTo(c.left, b.left + b.width, 2, description + "left of 'c' should be at similar to right of 'b'");
+ is(c.height, b.height, description + "'b' and 'c' should be same height");
+
+ // "abc" as array
+ let abcAsArray = synthesizeQueryTextRectArray(kLFLen, 3);
+ if (!checkQueryContentResult(abcAsArray, description + "rect array for 'abc'") ||
+ !checkRectArray(abcAsArray, [a, b, c], description + "query text rect array result of 'abc' should match with each query text rect result")) {
+ return;
+ }
+
+ // 2nd <p> (can be computed with the rect of 'c')
+ let p2 = synthesizeQueryTextRect(kLFLen + 3, 1);
+ if (!checkQueryContentResult(p2, description + "rect for 2nd <p>")) {
+ return;
+ }
+
+ is(p2.top, c.top, description + "'c' and a line breaker caused by 2nd <p> should be at same top");
+ isSimilarTo(p2.left, c.left + c.width, 2, description + "left of a line breaker caused by 2nd <p> should be at similar to right of 'c'");
+ is(p2.height, c.height, description + "'c' and a line breaker caused by 2nd <p> should be same height");
+
+ // 2nd <p> as array
+ let p2AsArray = synthesizeQueryTextRectArray(kLFLen + 3, 1);
+ if (!checkQueryContentResult(p2AsArray, description + "2nd <p>'s line breaker as array") ||
+ !checkRectArray(p2AsArray, [p2], description + "query text rect array result of 2nd <p> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let p2_2 = synthesizeQueryTextRect(kLFLen + 4, 1);
+ if (!checkQueryContentResult(p2_2, description + "rect for \\n of \\r\\n caused by 2nd <p>")) {
+ return;
+ }
+
+ is(p2_2.top, p2.top, description + "'\\r' and '\\n' should be at same top");
+ is(p2_2.left, p2.left, description + "'\\r' and '\\n' should be at same top");
+ is(p2_2.height, p2.height, description + "'\\r' and '\\n' should be same height");
+ is(p2_2.width, p2.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let p2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 4, 1);
+ if (!checkQueryContentResult(p2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <p>") ||
+ !checkRectArray(p2_2AsArray, [p2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <p> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // "d"
+ let d = synthesizeQueryTextRect(kLFLen * 2 + 3, 1);
+ if (!checkQueryContentResult(d, description + "rect for 'd'")) {
+ return;
+ }
+
+ isGreaterThan(d.top, a.top + a.height, description + "top of 'd' should be greater than bottom of 'a'");
+ is(d.left, a.left, description + "'a' and 'd' should be same at same left");
+ is(d.height, a.height, description + "'a' and 'd' should be same height");
+
+ // "e"
+ let e = synthesizeQueryTextRect(kLFLen * 2 + 4, 1);
+ if (!checkQueryContentResult(e, description + "rect for 'e'")) {
+ return;
+ }
+
+ is(e.top, d.top, description + "'d' and 'd' should be at same top");
+ isSimilarTo(e.left, d.left + d.width, 2, description + "left of 'e' should be at similar to right of 'd'");
+ is(e.height, d.height, description + "'d' and 'e' should be same height");
+
+ // "f"
+ let f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1);
+ if (!checkQueryContentResult(f, description + "rect for 'f'")) {
+ return;
+ }
+
+ is(f.top, e.top, description + "'e' and 'f' should be at same top");
+ isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'");
+ is(f.height, e.height, description + "'e' and 'f' should be same height");
+
+ // "def" as array
+ let defAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 3, 3);
+ if (!checkQueryContentResult(defAsArray, description + "rect array for 'def'") ||
+ !checkRectArray(defAsArray, [d, e, f], description + "query text rect array result of 'def' should match with each query text rect result")) {
+ return;
+ }
+
+ // next of "f" (can be computed with rect of 'f')
+ let next_f = synthesizeQueryTextRect(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(next_f, description + "rect for next of 'f'")) {
+ return;
+ }
+
+ is(next_f.top, d.top, 2, description + "'f' and next of 'f' should be at same top");
+ isSimilarTo(next_f.left, f.left + f.width, 2, description + "left of next of 'f' should be at similar to right of 'f'");
+ is(next_f.height, d.height, description + "'f' and next of 'f' should be same height");
+
+ // next of "f" as array
+ let next_fAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(next_fAsArray, description + "rect array for next of 'f'") ||
+ !checkRectArray(next_fAsArray, [next_f], description + "query text rect array result of next of 'f' should match with each query text rect result")) {
+ return;
+ }
+
+ // too big offset for the editor
+ let tooBigOffset = synthesizeQueryTextRect(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) {
+ return;
+ }
+
+ is(tooBigOffset.top, next_f.top, description + "too big offset and next of 'f' should be at same top");
+ is(tooBigOffset.left, next_f.left, description + "too big offset and next of 'f' should be at same left");
+ is(tooBigOffset.height, next_f.height, description + "too big offset and next of 'f' should be same height");
+ is(tooBigOffset.width, next_f.width, description + "too big offset and next of 'f' should be same width");
+
+ // too big offset for the editors as array
+ let tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") ||
+ !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) {
+ return;
+ }
+
+ contenteditable.innerHTML = "<p>abc</p><p>def</p><p><br></p>";
+ // \n 0 123 4 567 8 9
+ // \r\n 01 234 56 789 01 23
+
+ description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", ";
+
+ // "f"
+ f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1);
+ if (!checkQueryContentResult(f, description + "rect for 'f'")) {
+ return;
+ }
+
+ is(f.top, e.top, description + "'e' and 'f' should be at same top");
+ is(f.height, e.height, description + "'e' and 'f' should be same height");
+ isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'");
+
+ // 3rd <p> (can be computed with rect of 'f')
+ let p3 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(p3, description + "rect for 3rd <p>")) {
+ return;
+ }
+
+ is(p3.top, f.top, description + "'f' and a line breaker caused by 3rd <p> should be at same top");
+ is(p3.height, f.height, description + "'f' and a line breaker caused by 3rd <p> should be same height");
+ isSimilarTo(p3.left, f.left + f.width, 2, description + "left of a line breaker caused by 3rd <p> should be similar to right of 'f'");
+
+ // 3rd <p> as array
+ let p3AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(p3AsArray, description + "3rd <p>'s line breaker as array") ||
+ !checkRectArray(p3AsArray, [p3], description + "query text rect array result of 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let p3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(p3_2, description + "rect for \\n of \\r\\n caused by 3rd <p>")) {
+ return;
+ }
+
+ is(p3_2.top, p3.top, description + "'\\r' and '\\n' should be at same top");
+ is(p3_2.left, p3.left, description + "'\\r' and '\\n' should be at same top");
+ is(p3_2.height, p3.height, description + "'\\r' and '\\n' should be same height");
+ is(p3_2.width, p3.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let p3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(p3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <p>") ||
+ !checkRectArray(p3_2AsArray, [p3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // <br> in 3rd <p>
+ let br = synthesizeQueryTextRect(kLFLen * 3 + 6, 1);
+ if (!checkQueryContentResult(br, description + "rect for <br> in 3rd <p>")) {
+ return;
+ }
+
+ isGreaterThan(br.top, d.top + d.height, description + "a line breaker caused by <br> in 3rd <p> should be greater than bottom of 'd'");
+ isSimilarTo(br.height, d.height, 2, description + "'d' and a line breaker caused by <br> in 3rd <p> should be similar height");
+ is(br.left, d.left, description + "left of a line breaker caused by <br> in 3rd <p> should be same left of 'd'");
+
+ // <br> in 3rd <p> as array
+ let brAsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1);
+ if (!checkQueryContentResult(brAsArray, description + "<br> in 3rd <p> as array") ||
+ !checkRectArray(brAsArray, [br], description + "query text rect array result of <br> in 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let br_2 = synthesizeQueryTextRect(kLFLen * 3 + 7, 1);
+ if (!checkQueryContentResult(br_2, description + "rect for \\n of \\r\\n caused by <br> in 3rd <p>")) {
+ return;
+ }
+
+ is(br_2.top, br.top, description + "'\\r' and '\\n' should be at same top");
+ is(br_2.left, br.left, description + "'\\r' and '\\n' should be at same top");
+ is(br_2.height, br.height, description + "'\\r' and '\\n' should be same height");
+ is(br_2.width, br.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let br_2AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 7, 1);
+ if (!checkQueryContentResult(br_2AsArray, description + "rect array for \\n of \\r\\n caused by <br> in 3rd <p>") ||
+ !checkRectArray(br_2AsArray, [br_2], description + "query text rect array result of \\n of \\r\\n caused by <br> in 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // next of <br> in 3rd <p>
+ let next_br = synthesizeQueryTextRect(kLFLen * 4 + 6, 1);
+ if (!checkQueryContentResult(next_br, description + "rect for next of <br> in 3rd <p>")) {
+ return;
+ }
+
+ is(next_br.top, br.top, description + "next of <br> and <br> should be at same top");
+ is(next_br.left, br.left, description + "next of <br> and <br> should be at same left");
+ is(next_br.height, br.height, description + "next of <br> and <br> should be same height");
+ is(next_br.width, br.width, description + "next of <br> and <br> should be same width");
+
+ // next of <br> in 3rd <p> as array
+ let next_brAsArray = synthesizeQueryTextRectArray(kLFLen * 4 + 6, 1);
+ if (!checkQueryContentResult(next_brAsArray, description + "rect array for next of <br> in 3rd <p>") ||
+ !checkRectArray(next_brAsArray, [next_br], description + "query text rect array result of next of <br> in 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+
+ // too big offset for the editor
+ tooBigOffset = synthesizeQueryTextRect(kLFLen * 4 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) {
+ return;
+ }
+
+ is(tooBigOffset.top, next_br.top, description + "too big offset and next of 3rd <p> should be at same top");
+ is(tooBigOffset.left, next_br.left, description + "too big offset and next of 3rd <p> should be at same left");
+ is(tooBigOffset.height, next_br.height, description + "too big offset and next of 3rd <p> should be same height");
+ is(tooBigOffset.width, next_br.width, description + "too big offset and next of 3rd <p> should be same width");
+
+ // too big offset for the editors as array
+ tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 4 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") ||
+ !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) {
+ return;
+ }
+
+ contenteditable.innerHTML = "<p>abc</p><p>def</p><p></p>";
+ // \n 0 123 4 567 8
+ // \r\n 01 234 56 789 0
+
+ description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", ";
+
+ // "f"
+ f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1);
+ if (!checkQueryContentResult(f, description + "rect for 'f'")) {
+ return;
+ }
+
+ is(f.top, e.top, description + "'e' and 'f' should be at same top");
+ isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'");
+ is(f.height, e.height, description + "'e' and 'f' should be same height");
+
+ // 3rd <p> (can be computed with rect of 'f')
+ p3 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(p3, description + "rect for 3rd <p>")) {
+ return;
+ }
+
+ is(p3.top, f.top, description + "'f' and a line breaker caused by 3rd <p> should be at same top");
+ is(p3.height, f.height, description + "'f' and a line breaker caused by 3rd <p> should be same height");
+ isSimilarTo(p3.left, f.left + f.width, 2, description + "left of a line breaker caused by 3rd <p> should be similar to right of 'f'");
+
+ // 3rd <p> as array
+ p3AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(p3AsArray, description + "3rd <p>'s line breaker as array") ||
+ !checkRectArray(p3AsArray, [p3], description + "query text rect array result of 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let p3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(p3_2, description + "rect for \\n of \\r\\n caused by 3rd <p>")) {
+ return;
+ }
+
+ is(p3_2.top, p3.top, description + "'\\r' and '\\n' should be at same top");
+ is(p3_2.left, p3.left, description + "'\\r' and '\\n' should be at same top");
+ is(p3_2.height, p3.height, description + "'\\r' and '\\n' should be same height");
+ is(p3_2.width, p3.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let p3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(p3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <p>") ||
+ !checkRectArray(p3_2AsArray, [p3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // next of 3rd <p>
+ let next_p3 = synthesizeQueryTextRect(kLFLen * 3 + 6, 1);
+ if (!checkQueryContentResult(next_p3, description + "rect for next of 3rd <p>")) {
+ return;
+ }
+
+ isGreaterThan(next_p3.top, d.top + d.height, description + "top of next of 3rd <p> should equal to or be bigger than bottom of 'd'");
+ isSimilarTo(next_p3.left, d.left, 2, description + "left of next of 3rd <p> should be at similar to left of 'd'");
+ isSimilarTo(next_p3.height, d.height, 2, description + "next of 3rd <p> and 'd' should be similar height");
+
+ // next of 3rd <p> as array
+ let next_p3AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1);
+ if (!checkQueryContentResult(next_p3AsArray, description + "next of 3rd <p> as array") ||
+ !checkRectArray(next_p3AsArray, [next_p3], description + "query text rect array result of next of 3rd <p> should match with each query text rect result")) {
+ return;
+ }
+
+ // too big offset for the editor
+ tooBigOffset = synthesizeQueryTextRect(kLFLen * 3 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) {
+ return;
+ }
+
+ is(tooBigOffset.top, next_p3.top, description + "too big offset and next of 3rd <p> should be at same top");
+ is(tooBigOffset.left, next_p3.left, description + "too big offset and next of 3rd <p> should be at same left");
+ is(tooBigOffset.height, next_p3.height, description + "too big offset and next of 3rd <p> should be same height");
+ is(tooBigOffset.width, next_p3.width, description + "too big offset and next of 3rd <p> should be same width");
+
+ // too big offset for the editors as array
+ tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") ||
+ !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) {
+ return;
+ }
+
+ contenteditable.innerHTML = "abc<br>def";
+ // \n 0123 456
+ // \r\n 01234 567
+
+ description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", ";
+
+ // "a"
+ a = synthesizeQueryTextRect(0, 1);
+ if (!checkQueryContentResult(a, description + "rect for 'a'")) {
+ return;
+ }
+
+ // "b"
+ b = synthesizeQueryTextRect(1, 1);
+ if (!checkQueryContentResult(b, description + "rect for 'b'")) {
+ return;
+ }
+
+ is(b.top, a.top, description + "'a' and 'b' should be at same top");
+ isSimilarTo(b.left, a.left + a.width, 2, description + "left of 'b' should be at similar to right of 'a'");
+ is(b.height, a.height, description + "'a' and 'b' should be same height");
+
+ // "c"
+ c = synthesizeQueryTextRect(2, 1);
+ if (!checkQueryContentResult(c, description + "rect for 'c'")) {
+ return;
+ }
+
+ is(c.top, b.top, description + "'b' and 'c' should be at same top");
+ isSimilarTo(c.left, b.left + b.width, 2, description + "left of 'c' should be at similar to right of 'b'");
+ is(c.height, b.height, description + "'b' and 'c' should be same height");
+
+ // "abc" as array
+ abcAsArray = synthesizeQueryTextRectArray(0, 3);
+ if (!checkQueryContentResult(abcAsArray, description + "rect array for 'abc'") ||
+ !checkRectArray(abcAsArray, [a, b, c], description + "query text rect array result of 'abc' should match with each query text rect result")) {
+ return;
+ }
+
+ // <br> (can be computed with the rect of 'c')
+ br = synthesizeQueryTextRect(3, 1);
+ if (!checkQueryContentResult(br, description + "rect for <br>")) {
+ return;
+ }
+
+ is(br.top, c.top, description + "'c' and a line breaker caused by <br> should be at same top");
+ isSimilarTo(br.left, c.left + c.width, 2, description + "left of a line breaker caused by <br> should be at similar to right of 'c'");
+ is(br.height, c.height, description + "'c' and a line breaker caused by <br> should be same height");
+
+ // <br> as array
+ brAsArray = synthesizeQueryTextRectArray(3, 1);
+ if (!checkQueryContentResult(brAsArray, description + "<br>'s line breaker as array") ||
+ !checkRectArray(brAsArray, [br], description + "query text rect array result of <br> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let br_2 = synthesizeQueryTextRect(4, 1);
+ if (!checkQueryContentResult(br_2, description + "rect for \n of \r\n caused by <br>")) {
+ return;
+ }
+
+ is(br_2.top, br.top, description + "'\\r' and '\\n' should be at same top");
+ is(br_2.left, br.left, description + "'\\r' and '\\n' should be at same top");
+ is(br_2.height, br.height, description + "'\\r' and '\\n' should be same height");
+ is(br_2.width, br.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let br_2AsArray = synthesizeQueryTextRectArray(4, 1);
+ if (!checkQueryContentResult(br_2AsArray, description + "rect array for \\n of \\r\\n caused by <br>") ||
+ !checkRectArray(br_2AsArray, [br_2], description + "query text rect array result of \\n of \\r\\n caused by <br> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // "d"
+ d = synthesizeQueryTextRect(kLFLen + 3, 1);
+ if (!checkQueryContentResult(d, description + "rect for 'd'")) {
+ return;
+ }
+
+ isSimilarTo(d.top, a.top + a.height, 2, description + "top of 'd' should be at similar to bottom of 'a'");
+ is(d.left, a.left, description + "'a' and 'd' should be same at same left");
+ is(d.height, a.height, description + "'a' and 'd' should be same height");
+
+ // "e"
+ e = synthesizeQueryTextRect(kLFLen + 4, 1);
+ if (!checkQueryContentResult(e, description + "rect for 'e'")) {
+ return;
+ }
+
+ is(e.top, d.top, description + "'d' and 'd' should be at same top");
+ isSimilarTo(e.left, d.left + d.width, 2, description + "left of 'e' should be at similar to right of 'd'");
+ is(e.height, d.height, description + "'d' and 'e' should be same height");
+
+ // "f"
+ f = synthesizeQueryTextRect(kLFLen + 5, 1);
+ if (!checkQueryContentResult(f, description + "rect for 'f'")) {
+ return;
+ }
+
+ is(f.top, e.top, description + "'e' and 'f' should be at same top");
+ isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'");
+ is(f.height, e.height, description + "'e' and 'f' should be same height");
+
+ // "def" as array
+ defAsArray = synthesizeQueryTextRectArray(kLFLen + 3, 3);
+ if (!checkQueryContentResult(defAsArray, description + "rect array for 'def'") ||
+ !checkRectArray(defAsArray, [d, e, f], description + "query text rect array result of 'def' should match with each query text rect result")) {
+ return;
+ }
+
+ // next of "f" (can be computed with rect of 'f')
+ next_f = synthesizeQueryTextRect(kLFLen + 6, 1);
+ if (!checkQueryContentResult(next_f, description + "rect for next of 'f'")) {
+ return;
+ }
+
+ is(next_f.top, d.top, 2, description + "'f' and next of 'f' should be at same top");
+ isSimilarTo(next_f.left, f.left + f.width, 2, description + "left of next of 'f' should be at similar to right of 'f'");
+ is(next_f.height, d.height, description + "'f' and next of 'f' should be same height");
+
+ // next of "f" as array
+ next_fAsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1);
+ if (!checkQueryContentResult(next_fAsArray, description + "rect array for next of 'f'") ||
+ !checkRectArray(next_fAsArray, [next_f], description + "query text rect array result of next of 'f' should match with each query text rect result")) {
+ return;
+ }
+
+ // too big offset for the editor
+ tooBigOffset = synthesizeQueryTextRect(kLFLen + 7, 1);
+ if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) {
+ return;
+ }
+
+ is(tooBigOffset.top, next_f.top, description + "too big offset and next of 'f' should be at same top");
+ is(tooBigOffset.left, next_f.left, description + "too big offset and next of 'f' should be at same left");
+ is(tooBigOffset.height, next_f.height, description + "too big offset and next of 'f' should be same height");
+ is(tooBigOffset.width, next_f.width, description + "too big offset and next of 'f' should be same width");
+
+ // too big offset for the editors as array
+ tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1);
+ if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") ||
+ !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) {
+ return;
+ }
+
+ // Note that this case does not have an empty line at the end.
+ contenteditable.innerHTML = "abc<br>def<br>";
+ // \n 0123 4567
+ // \r\n 01234 56789
+
+ description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", ";
+
+ // "f"
+ f = synthesizeQueryTextRect(kLFLen + 5, 1);
+ if (!checkQueryContentResult(f, description + "rect for 'f'")) {
+ return;
+ }
+
+ is(f.top, e.top, description + "'e' and 'f' should be at same top");
+ is(f.height, e.height, description + "'e' and 'f' should be same height");
+ isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'");
+
+ // 2nd <br> (can be computed with rect of 'f')
+ let br2 = synthesizeQueryTextRect(kLFLen + 6, 1);
+ if (!checkQueryContentResult(br2, description + "rect for 2nd <br>")) {
+ return;
+ }
+
+ is(br2.top, f.top, description + "'f' and a line breaker caused by 2nd <br> should be at same top");
+ is(br2.height, f.height, description + "'f' and a line breaker caused by 2nd <br> should be same height");
+ isSimilarTo(br2.left, f.left + f.width, 2, description + "left of a line breaker caused by 2nd <br> should be similar to right of 'f'");
+
+ // 2nd <br> as array
+ let br2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(br2AsArray, description + "2nd <br>'s line breaker as array") ||
+ !checkRectArray(br2AsArray, [br2], description + "query text rect array result of 2nd <br> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let br2_2 = synthesizeQueryTextRect(kLFLen + 7, 1);
+ if (!checkQueryContentResult(br2_2, description + "rect for \\n of \\r\\n caused by 2nd <br>")) {
+ return;
+ }
+
+ is(br2_2.top, br2.top, description + "'\\r' and '\\n' should be at same top");
+ is(br2_2.left, br2.left, description + "'\\r' and '\\n' should be at same top");
+ is(br2_2.height, br2.height, description + "'\\r' and '\\n' should be same height");
+ is(br2_2.width, br2.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let br2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1);
+ if (!checkQueryContentResult(br2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <br>") ||
+ !checkRectArray(br2_2AsArray, [br2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <br> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // next of 2nd <br>
+ let next_br2 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(next_br2, description + "rect for next of 2nd <br>")) {
+ return;
+ }
+
+ is(next_br2.top, br2.top, description + "2nd <br> and next of 2nd <br> should be at same top");
+ is(next_br2.left, br2.left, description + "2nd <br> and next of 2nd <br> should be at same top");
+ is(next_br2.height, br2.height, description + "2nd <br> and next of 2nd <br> should be same height");
+ is(next_br2.width, br2.width, description + "2nd <br> and next of 2nd <br> should be same width");
+
+ // next of 2nd <br> as array
+ let next_br2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1);
+ if (!checkQueryContentResult(next_br2AsArray, description + "rect array for next of 2nd <br>") ||
+ !checkRectArray(next_br2AsArray, [next_br2], description + "query text rect array result of next of 2nd <br> should match with each query text rect result")) {
+ return;
+ }
+
+ // too big offset for the editor
+ tooBigOffset = synthesizeQueryTextRect(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) {
+ return;
+ }
+
+ is(tooBigOffset.top, next_br2.top, description + "too big offset and next of 2nd <br> should be at same top");
+ is(tooBigOffset.left, next_br2.left, description + "too big offset and next of 2nd <br> should be at same left");
+ is(tooBigOffset.height, next_br2.height, description + "too big offset and next of 2nd <br> should be same height");
+ is(tooBigOffset.width, next_br2.width, description + "too big offset and next of 2nd <br> should be same width");
+
+ // too big offset for the editors as array
+ tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") ||
+ !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) {
+ return;
+ }
+
+ contenteditable.innerHTML = "abc<br>def<br><br>";
+ // \n 0123 4567 8
+ // \r\n 01234 56789 01
+
+ description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", ";
+
+ // "f"
+ f = synthesizeQueryTextRect(kLFLen + 5, 1);
+ if (!checkQueryContentResult(f, description + "rect for 'f'")) {
+ return;
+ }
+
+ is(f.top, e.top, description + "'e' and 'f' should be at same top");
+ isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'");
+ is(f.height, e.height, description + "'e' and 'f' should be same height");
+
+ // 2nd <br>
+ br2 = synthesizeQueryTextRect(kLFLen + 6, 1);
+ if (!checkQueryContentResult(br2, description + "rect for 2nd <br>")) {
+ return;
+ }
+
+ is(br2.top, f.top, description + "'f' and a line breaker caused by 2nd <br> should be at same top");
+ is(br2.height, f.height, description + "'f' and a line breaker caused by 2nd <br> should be same height");
+ ok(f.left < br2.left, description + "left of a line breaker caused by 2nd <br> should be bigger than left of 'f', f.left=" + f.left + ", br2.left=" + br2.left);
+
+ // 2nd <br> as array
+ br2AsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1);
+ if (!checkQueryContentResult(br2AsArray, description + "2nd <br>'s line breaker as array") ||
+ !checkRectArray(br2AsArray, [br2], description + "query text rect array result of 2nd <br> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let br2_2 = synthesizeQueryTextRect(kLFLen + 7, 1);
+ if (!checkQueryContentResult(br2_2, description + "rect for \\n of \\r\\n caused by 2nd <br>")) {
+ return;
+ }
+
+ is(br2_2.top, br2.top, description + "'\\r' and '\\n' should be at same top");
+ is(br2_2.left, br2.left, description + "'\\r' and '\\n' should be at same top");
+ is(br2_2.height, br2.height, description + "'\\r' and '\\n' should be same height");
+ is(br2_2.width, br2.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let br2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1);
+ if (!checkQueryContentResult(br2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <br>") ||
+ !checkRectArray(br2_2AsArray, [br2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <br> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // 3rd <br>
+ let br3 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(br3, description + "rect for next of 3rd <br>")) {
+ return;
+ }
+
+ isSimilarTo(br3.top, d.top + d.height, 3, description + "top of next of 3rd <br> should at similar to bottom of 'd'");
+ isSimilarTo(br3.left, d.left, 2, description + "left of next of 3rd <br> should be at similar to left of 'd'");
+ isSimilarTo(br3.height, d.height, 2, description + "next of 3rd <br> and 'd' should be similar height");
+
+ // 3rd <br> as array
+ let br3AsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1);
+ if (!checkQueryContentResult(br3AsArray, description + "3rd <br>'s line breaker as array") ||
+ !checkRectArray(br3AsArray, [br3], description + "query text rect array result of 3rd <br> should match with each query text rect result")) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ // \n of \r\n
+ let br3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(br3_2, description + "rect for \\n of \\r\\n caused by 3rd <br>")) {
+ return;
+ }
+
+ is(br3_2.top, br3.top, description + "'\\r' and '\\n' should be at same top");
+ is(br3_2.left, br3.left, description + "'\\r' and '\\n' should be at same left");
+ is(br3_2.height, br3.height, description + "'\\r' and '\\n' should be same height");
+ is(br3_2.width, br3.width, description + "'\\r' and '\\n' should be same width");
+
+ // \n of \r\n as array
+ let br3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1);
+ if (!checkQueryContentResult(br3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <br>") ||
+ !checkRectArray(br3_2AsArray, [br3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <br> should match with each query text rect result")) {
+ return;
+ }
+ }
+
+ // next of 3rd <br>
+ let next_br3 = synthesizeQueryTextRect(kLFLen * 3 + 6, 1);
+ if (!checkQueryContentResult(next_br3, description + "rect for next of 3rd <br>")) {
+ return;
+ }
+
+ is(next_br3.top, br3.top, description + "3rd <br> and next of 3rd <br> should be at same top");
+ is(next_br3.left, br3.left, description + "3rd <br> and next of 3rd <br> should be at same left");
+ is(next_br3.height, br3.height, description + "3rd <br> and next of 3rd <br> should be same height");
+ is(next_br3.width, br3.width, description + "3rd <br> and next of 3rd <br> should be same width");
+
+ // next of 3rd <br> as array
+ let next_br3AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1);
+ if (!checkQueryContentResult(next_br3AsArray, description + "rect array for next of 3rd <br>") ||
+ !checkRectArray(next_br3AsArray, [next_br3], description + "query text rect array result of next of 3rd <br> should match with each query text rect result")) {
+ return;
+ }
+
+ // too big offset for the editor
+ tooBigOffset = synthesizeQueryTextRect(kLFLen * 3 + 7, 1);
+ if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) {
+ return;
+ }
+
+ is(tooBigOffset.top, next_br3.top, description + "too big offset and next of 3rd <br> should be at same top");
+ is(tooBigOffset.left, next_br3.left, description + "too big offset and next of 3rd <br> should be at same left");
+ is(tooBigOffset.height, next_br3.height, description + "too big offset and next of 3rd <br> should be same height");
+ is(tooBigOffset.width, next_br3.width, description + "too big offset and next of 3rd <br> should be same width");
+
+ // too big offset for the editors as array
+ tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1);
+ if (checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset")) {
+ checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result");
+ }
+
+ if (!(function test_query_text_rects_across_invisible_text() {
+ contenteditable.innerHTML = "<div>\n<div>abc</div></div>";
+ // \n 0 1 2 345
+ // \r\n 01 2345 678
+ description = `runQueryTextRectInContentEditableTest: test_query_text_rects_across_invisible_text: "${
+ contenteditable.innerHTML.replace(/\n/g, "\\n")
+ }",`;
+ // rect of "a"
+ const rectA = synthesizeQueryTextRect(kLFLen * 3, 1);
+ if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) {
+ return false;
+ }
+ const rectArrayFromStartToA = synthesizeQueryTextRectArray(0, kLFLen * 3 + 1);
+ if (!checkQueryContentResult(rectArrayFromStartToA, `${description} rect array from invisible text to "a"`)) {
+ return false;
+ }
+ const fromStartToARects = getRectArray(rectArrayFromStartToA);
+ if (!checkRect(
+ fromStartToARects[kLFLen * 3],
+ rectA,
+ `${description} rect for "a" in array should be same as the result of query only it`
+ )) {
+ return false;
+ }
+ return checkRect(
+ fromStartToARects[kLFLen * 2],
+ fromStartToARects[0],
+ `${description} rect for the linebreak in invisible text node should be same as first linebreak`
+ );
+ })()) {
+ return;
+ }
+
+ function test_query_text_rects_starting_from_invisible_text() {
+ contenteditable.innerHTML = "<div>\n<div>abc</div></div>";
+ // \n 0 1 2 345
+ // \r\n 01 2345 678
+ description = `runQueryTextRectInContentEditableTest: test_query_text_rects_starting_from_invisible_text: "${
+ contenteditable.innerHTML.replace(/\n/g, "\\n")
+ }",`;
+ // rect of "a"
+ const rectA = synthesizeQueryTextRect(kLFLen * 3, 1);
+ if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) {
+ return false;
+ }
+ const rectArrayFromInvisibleToA = synthesizeQueryTextRectArray(kLFLen, kLFLen * 2 + 1);
+ if (!checkQueryContentResult(rectArrayFromInvisibleToA, `${description} rect array from invisible text to "a"`)) {
+ return false;
+ }
+ const fromInvisibleToARects = getRectArray(rectArrayFromInvisibleToA);
+ if (!checkRect(
+ fromInvisibleToARects[kLFLen * 2],
+ rectA,
+ `${description} rect for "a" in array should be same as the result of query only it`
+ )) {
+ return false;
+ }
+ // For now the rect of characters in invisible text node should be caret rect
+ // before the following line break. This is inconsistent from the result of
+ // the query text rect array event starting from the previous visible line
+ // break or character, but users anyway cannot insert text into the invisible
+ // text node only with user's operation. Therefore, this won't be problem
+ // in usual web apps.
+ const caretRectBeforeLineBreakBeforeA = fromInvisibleToARects[kLFLen];
+ return checkRect(
+ fromInvisibleToARects[0],
+ caretRectBeforeLineBreakBeforeA,
+ `${description} rect for the linebreak in invisible text node should be same as caret rect before the following visible linebreak before "a"`
+ );
+ }
+ if (!test_query_text_rects_starting_from_invisible_text()) {
+ return;
+ }
+
+ if (kLFLen > 1) {
+ function test_query_text_rects_starting_from_middle_of_invisible_linebreak() {
+ contenteditable.innerHTML = "<div>\n<div>abc</div></div>";
+ // \n 0 1 2 345
+ // \r\n 01 2345 678
+ description = `runQueryTextRectInContentEditableTest: test_query_text_rects_starting_from_middle_of_invisible_linebreak: "${
+ contenteditable.innerHTML.replace(/\n/g, "\\n")
+ }",`;
+ // rect of "a"
+ const rectA = synthesizeQueryTextRect(kLFLen * 3, 1);
+ if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) {
+ return false;
+ }
+ const rectArrayFromInvisibleToA = synthesizeQueryTextRectArray(kLFLen + 1, 1 + kLFLen + 1);
+ if (!checkQueryContentResult(rectArrayFromInvisibleToA, `${description} rect array from invisible text to "a"`)) {
+ return false;
+ }
+ const fromInvisibleToARects = getRectArray(rectArrayFromInvisibleToA);
+ if (!checkRect(
+ fromInvisibleToARects[1 + kLFLen],
+ rectA,
+ `${description} rect for "a" in array should be same as the result of query only it`
+ )) {
+ return false;
+ }
+ // For now the rect of characters in invisible text node should be caret rect
+ // before the following line break. This is inconsistent from the result of
+ // the query text rect array event starting from the previous visible line
+ // break or character, but users anyway cannot insert text into the invisible
+ // text node only with user's operation. Therefore, this won't be problem
+ // in usual web apps.
+ const caretRectBeforeLineBreakBeforeA = fromInvisibleToARects[1];
+ return checkRect(
+ fromInvisibleToARects[0],
+ caretRectBeforeLineBreakBeforeA,
+ `${description} rect for the linebreak in invisible text node should be same as caret rect before the following visible linebreak before "a"`
+ );
+ }
+ if (!test_query_text_rects_starting_from_middle_of_invisible_linebreak()) {
+ return;
+ }
+ }
+
+ function test_query_text_rects_ending_with_invisible_text() {
+ contenteditable.innerHTML = "<div><div>abc</div>\n</div>";
+ // \n 0 1 234 5
+ // \r\n 01 23 456 78
+ description = `runQueryTextRectInContentEditableTest: test_query_text_rects_ending_with_invisible_text: "${
+ contenteditable.innerHTML.replace(/\n/g, "\\n")
+ }",`;
+ // rect of "c"
+ const rectC = synthesizeQueryTextRect(kLFLen * 2 + 2, 1);
+ if (!checkQueryContentResult(rectC, `${description} rect of "c"`)) {
+ return false;
+ }
+ const rectArrayFromCToInvisible = synthesizeQueryTextRectArray(kLFLen * 2 + 2, 1 + kLFLen);
+ if (!checkQueryContentResult(rectArrayFromCToInvisible, `${description} rect array from "c" to invisible linebreak`)) {
+ return false;
+ }
+ const fromCToInvisibleRects = getRectArray(rectArrayFromCToInvisible);
+ if (!checkRect(
+ fromCToInvisibleRects[0],
+ rectC,
+ `${description} rect for "c" in array should be same as the result of query only it`
+ )) {
+ return false;
+ }
+ const caretRectAfterC = {
+ left: rectC.left + rectC.width,
+ top: rectC.top,
+ width: 1,
+ height: rectC.height,
+ };
+ return checkRectFuzzy(
+ fromCToInvisibleRects[1],
+ caretRectAfterC,
+ {
+ left: 1,
+ top: 0,
+ width: 0,
+ height: 0,
+ },
+ `${description} rect for the linebreak in invisible text node should be same as caret rect after "c"`
+ );
+ }
+ if (!test_query_text_rects_ending_with_invisible_text()) {
+ // eslint-disable-next-line no-useless-return
+ return;
+ }
+}
+
+function runCharAtPointTest(aFocusedEditor, aTargetName)
+{
+ aFocusedEditor.value = "This is a test of the\nContent Events";
+ // 012345678901234567890 12345678901234
+ // 0 1 2 3
+
+ aFocusedEditor.focus();
+
+ const kNone = -1;
+ const kTestingOffset = [ 0, 10, 20, 21 + kLFLen, 34 + kLFLen];
+ const kLeftSideOffset = [ kNone, 9, 19, kNone, 33 + kLFLen];
+ const kRightSideOffset = [ 1, 11, kNone, 22 + kLFLen, kNone];
+ const kLeftTentativeCaretOffset = [ 0, 10, 20, 21 + kLFLen, 34 + kLFLen];
+ const kRightTentativeCaretOffset = [ 1, 11, 21, 22 + kLFLen, 35 + kLFLen];
+
+ let editorRect = synthesizeQueryEditorRect();
+ if (!checkQueryContentResult(editorRect,
+ "runCharAtPointTest (" + aTargetName + "): editorRect")) {
+ return;
+ }
+
+ for (let i = 0; i < kTestingOffset.length; i++) {
+ let textRect = synthesizeQueryTextRect(kTestingOffset[i], 1);
+ if (!checkQueryContentResult(textRect,
+ "runCharAtPointTest (" + aTargetName + "): textRect: i=" + i)) {
+ continue;
+ }
+
+ checkRectContainsRect(textRect, editorRect,
+ "runCharAtPointTest (" + aTargetName +
+ "): the text rect isn't in the editor");
+
+ // Test #1, getting same character rect by the point near the top-left.
+ let charAtPt1 = synthesizeCharAtPoint(textRect.left + 1,
+ textRect.top + 1);
+ if (checkQueryContentResult(charAtPt1,
+ "runCharAtPointTest (" + aTargetName + "): charAtPt1: i=" + i)) {
+ ok(!charAtPt1.notFound,
+ "runCharAtPointTest (" + aTargetName + "): charAtPt1 isn't found: i=" + i);
+ if (!charAtPt1.notFound) {
+ is(charAtPt1.offset, kTestingOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): charAtPt1 offset is wrong: i=" + i);
+ checkRect(charAtPt1, textRect, "runCharAtPointTest (" + aTargetName +
+ "): charAtPt1 left is wrong: i=" + i);
+ }
+ ok(!charAtPt1.tentativeCaretOffsetNotFound,
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt1 isn't found: i=" + i);
+ if (!charAtPt1.tentativeCaretOffsetNotFound) {
+ is(charAtPt1.tentativeCaretOffset, kLeftTentativeCaretOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt1 is wrong: i=" + i);
+ }
+ }
+
+ // Test #2, getting same character rect by the point near the bottom-right.
+ let charAtPt2 = synthesizeCharAtPoint(textRect.left + textRect.width - 2,
+ textRect.top + textRect.height - 2);
+ if (checkQueryContentResult(charAtPt2,
+ "runCharAtPointTest (" + aTargetName + "): charAtPt2: i=" + i)) {
+ ok(!charAtPt2.notFound,
+ "runCharAtPointTest (" + aTargetName + "): charAtPt2 isn't found: i=" + i);
+ if (!charAtPt2.notFound) {
+ is(charAtPt2.offset, kTestingOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): charAtPt2 offset is wrong: i=" + i);
+ checkRect(charAtPt2, textRect, "runCharAtPointTest (" + aTargetName +
+ "): charAtPt1 left is wrong: i=" + i);
+ }
+ ok(!charAtPt2.tentativeCaretOffsetNotFound,
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt2 isn't found: i=" + i);
+ if (!charAtPt2.tentativeCaretOffsetNotFound) {
+ is(charAtPt2.tentativeCaretOffset, kRightTentativeCaretOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt2 is wrong: i=" + i);
+ }
+ }
+
+ // Test #3, getting left character offset.
+ let charAtPt3 = synthesizeCharAtPoint(textRect.left - 2,
+ textRect.top + 1);
+ if (checkQueryContentResult(charAtPt3,
+ "runCharAtPointTest (" + aTargetName + "): charAtPt3: i=" + i)) {
+ is(charAtPt3.notFound, kLeftSideOffset[i] == kNone,
+ kLeftSideOffset[i] == kNone ?
+ "runCharAtPointTest (" + aTargetName + "): charAtPt3 is found: i=" + i :
+ "runCharAtPointTest (" + aTargetName + "): charAtPt3 isn't found: i=" + i);
+ if (!charAtPt3.notFound) {
+ is(charAtPt3.offset, kLeftSideOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): charAtPt3 offset is wrong: i=" + i);
+ }
+ if (kLeftSideOffset[i] == kNone) {
+ // There may be no enough padding-left (depends on platform)
+ todo(false,
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 isn't tested: i=" + i);
+ } else {
+ ok(!charAtPt3.tentativeCaretOffsetNotFound,
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 isn't found: i=" + i);
+ if (!charAtPt3.tentativeCaretOffsetNotFound) {
+ is(charAtPt3.tentativeCaretOffset, kLeftTentativeCaretOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 is wrong: i=" + i);
+ }
+ }
+ }
+
+ // Test #4, getting right character offset.
+ let charAtPt4 = synthesizeCharAtPoint(textRect.left + textRect.width + 1,
+ textRect.top + textRect.height - 2);
+ if (checkQueryContentResult(charAtPt4,
+ "runCharAtPointTest (" + aTargetName + "): charAtPt4: i=" + i)) {
+ is(charAtPt4.notFound, kRightSideOffset[i] == kNone,
+ kRightSideOffset[i] == kNone ?
+ "runCharAtPointTest (" + aTargetName + "): charAtPt4 is found: i=" + i :
+ "runCharAtPointTest (" + aTargetName + "): charAtPt4 isn't found: i=" + i);
+ if (!charAtPt4.notFound) {
+ is(charAtPt4.offset, kRightSideOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): charAtPt4 offset is wrong: i=" + i);
+ }
+ ok(!charAtPt4.tentativeCaretOffsetNotFound,
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt4 isn't found: i=" + i);
+ if (!charAtPt4.tentativeCaretOffsetNotFound) {
+ is(charAtPt4.tentativeCaretOffset, kRightTentativeCaretOffset[i],
+ "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt4 is wrong: i=" + i);
+ }
+ }
+ }
+}
+
+function runCharAtPointAtOutsideTest()
+{
+ textarea.focus();
+ textarea.value = "some text";
+ let editorRect = synthesizeQueryEditorRect();
+ if (!checkQueryContentResult(editorRect,
+ "runCharAtPointAtOutsideTest: editorRect")) {
+ return;
+ }
+ // Check on a text node which is at the outside of editor.
+ let charAtPt = synthesizeCharAtPoint(editorRect.left + 20,
+ editorRect.top - 10);
+ if (checkQueryContentResult(charAtPt,
+ "runCharAtPointAtOutsideTest: charAtPt")) {
+ ok(charAtPt.notFound,
+ "runCharAtPointAtOutsideTest: charAtPt is found on outside of editor");
+ ok(charAtPt.tentativeCaretOffsetNotFound,
+ "runCharAtPointAtOutsideTest: tentative caret offset for charAtPt is found on outside of editor");
+ }
+}
+
+async function runSetSelectionEventTest()
+{
+ contenteditable.focus();
+
+ const selection = windowOfContenteditable.getSelection();
+
+ // #1
+ contenteditable.innerHTML = "abc<br>def";
+
+ await synthesizeSelectionSet(0, 100);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node of the editor");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of children");
+ checkSelection(0, "abc" + kLF + "def", "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(2, 2 + kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, 2,
+ "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 2");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(2, "c" + kLF + "d", "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(1, 2);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node");
+ is(selection.focusOffset, contenteditable.firstChild.wholeText.length,
+ "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node");
+ checkSelection(1, "bc", "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, contenteditable.firstChild.wholeText.length,
+ "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the first text node");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 2,
+ "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the index of the last text node");
+ checkSelection(3, kLF, "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(6+kLFLen, 0);
+ is(selection.anchorNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, contenteditable.lastChild.wholeText.length,
+ "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.anchorOffset, contenteditable.lastChild.wholeText.length,
+ "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(6 + kLFLen, "", "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(100, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node of the editor");
+ is(selection.anchorOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of children");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node of the editor");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of children");
+ checkSelection(6 + kLFLen, "", "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #2
+ contenteditable.innerHTML = "<p>a<b>b</b>c</p><p>def</p>";
+
+ await synthesizeSelectionSet(kLFLen, 4+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <p> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node");
+ is(selection.focusNode, contenteditable.lastChild.firstChild,
+ "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the second <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(kLFLen, "abc" + kLF + "d", "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 2);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <p> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node");
+ is(selection.focusNode, contenteditable.firstChild.childNodes.item(1).firstChild,
+ "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <b> node");
+ is(selection.focusOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length,
+ "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <b> node");
+ checkSelection(kLFLen, "ab", "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(1+kLFLen, 2);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node in the first <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node in the first <p> node");
+ checkSelection(1+kLFLen, "bc", "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(2+kLFLen, 2+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.childNodes.item(1).firstChild,
+ "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <b> node");
+ is(selection.anchorOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length,
+ "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node in the <b> node");
+ is(selection.focusNode, contenteditable.lastChild.firstChild,
+ "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the last <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(2+kLFLen, "c" + kLF + "d", "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3+kLFLen*2, 1);
+ is(selection.anchorNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the second <p> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the second <p> node");
+ is(selection.focusNode, contenteditable.lastChild.firstChild,
+ "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the second <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(3+kLFLen*2, "d", "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the first <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, kLF, "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(2+kLFLen, 1+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.childNodes.item(1).firstChild,
+ "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the <b> node");
+ is(selection.anchorOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length,
+ "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <b> node");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(2+kLFLen, "c" + kLF, "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3+kLFLen, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the first <p> node");
+ is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <p> node");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(3+kLFLen, kLF, "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3+kLFLen, 1+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the first <p> node");
+ is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <p> node");
+ is(selection.focusNode, contenteditable.lastChild.firstChild,
+ "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node of the second <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(3+kLFLen, kLF + "d", "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ // #3
+ contenteditable.innerHTML = "<div>abc<p>def</p></div>";
+
+ await synthesizeSelectionSet(1+kLFLen, 2);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node");
+ is(selection.focusOffset, contenteditable.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the first text node");
+ checkSelection(1+kLFLen, "bc", "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(1+kLFLen, 3+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(1+kLFLen, "bc" + kLF + "d", "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3+kLFLen, 0);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the first text node");
+ is(selection.focusNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node");
+ is(selection.focusOffset, contenteditable.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the first text node");
+ checkSelection(3+kLFLen, "", "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 6+kLFLen*2);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(0, kLF + "abc" + kLF + "def", "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 100);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(0, kLF + "abc" + kLF + "def", "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(4+kLFLen*2, 2);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(4+kLFLen*2, 100);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(6+kLFLen*2, 0);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node");
+ is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(6+kLFLen*2, 1);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first text node");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, kLF, "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 1+kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node");
+ is(selection.focusNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node of the <div> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(0, kLF + "a", "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(2+kLFLen, 1+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node");
+ is(selection.anchorOffset, 2,
+ "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 2");
+ is(selection.focusNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(2+kLFLen, "c" + kLF, "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3+kLFLen, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node");
+ is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node of the <div> node");
+ is(selection.focusNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(3+kLFLen, kLF, "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3+kLFLen, 1+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node");
+ is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node of the <div> node");
+ is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild,
+ "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node of the <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(3+kLFLen, kLF + "d", "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ // #4
+ contenteditable.innerHTML = "<div><p>abc</p>def</div>";
+
+ await synthesizeSelectionSet(1+kLFLen*2, 2);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <p> node");
+ checkSelection(1+kLFLen*2, "bc", "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(1+kLFLen*2, 3);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(1+kLFLen*2, "bcd", "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(3+kLFLen*2, 0);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node");
+ is(selection.anchorOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node in the <p> node");
+ is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length,
+ "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <p> node");
+ checkSelection(3+kLFLen*2, "", "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 6+kLFLen*2);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(0, kLF + kLF + "abcdef", "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 100);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(0, kLF + kLF + "abcdef", "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(4+kLFLen*2, 2);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(4+kLFLen*2, 100);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(6+kLFLen*2, 0);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node");
+ is(selection.focusNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(6+kLFLen*2, 1);
+ is(selection.anchorNode, contenteditable.firstChild.lastChild,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node");
+ is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, kLF, "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen*2);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node");
+ is(selection.focusNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, kLF + kLF, "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 1+kLFLen*2);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node");
+ is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(0, kLF + kLF + "a", "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 0);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(kLFLen, "", "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node");
+ is(selection.focusNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(kLFLen, kLF, "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 1+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node");
+ is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild,
+ "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(kLFLen, kLF +"a", "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ // #5
+ contenteditable.innerHTML = "<br>";
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(0, kLF, "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(kLFLen, "", "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 1);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(kLFLen, "", "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\"");
+
+ // #6
+ contenteditable.innerHTML = "<p><br></p>";
+
+ await synthesizeSelectionSet(kLFLen, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children");
+ checkSelection(kLFLen, kLF, "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen*2, 0);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the <p>'s children");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children");
+ checkSelection(kLFLen*2, "", "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen*2, 1);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(kLFLen*2, "", "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, kLF, "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen*2);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children");
+ checkSelection(0, kLF + kLF, "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 0);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(kLFLen, "", "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #7
+ contenteditable.innerHTML = "<br><br>";
+
+ await synthesizeSelectionSet(0, kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(0, kLF, "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, kLFLen * 2);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(0, kLF + kLF, "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(kLFLen, "", "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, kLFLen);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #7 (kLFLen, kLFLen) selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(kLFLen, kLF, "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen * 2, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(kLFLen * 2, "", "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #8
+ contenteditable.innerHTML = "<p><br><br></p>";
+
+ await synthesizeSelectionSet(kLFLen, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(kLFLen, kLF, "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, kLFLen * 2);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children");
+ checkSelection(kLFLen, kLF + kLF, "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen*2, 0);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1");
+ checkSelection(kLFLen*2, "", "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen*2, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen*2, kLFLen) selection focus node should be the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children");
+ checkSelection(kLFLen*2, kLF, "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen*3, 0);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node");
+ is(selection.anchorOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the <p>'s children");
+ is(selection.focusNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, contenteditable.firstChild.childNodes.length,
+ "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children");
+ checkSelection(kLFLen*3, "", "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #9 (ContentEventHandler cannot distinguish if <p> can have children, so, the result is same as case #5, "<br>")
+ contenteditable.innerHTML = "<p></p>";
+
+ await synthesizeSelectionSet(kLFLen, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node + 1");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node");
+ is(selection.focusOffset, 1,
+ "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the index of the <p> node + 1");
+ checkSelection(kLFLen, "", "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(kLFLen, 1);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node + 1");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #9 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #9 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(kLFLen, "", "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #10
+ contenteditable.innerHTML = "";
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 1);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\"");
+
+ // #11
+ contenteditable.innerHTML = "<span></span><i><u></u></i>";
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(0, 1);
+ is(selection.anchorNode, contenteditable,
+ "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable,
+ "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node");
+ is(selection.focusOffset, contenteditable.childNodes.length,
+ "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children");
+ checkSelection(0, "", "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\"");
+
+ // #12
+ contenteditable.innerHTML = "<span>abc</span><i><u></u></i>";
+ selection.selectAllChildren(contenteditable);
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.firstChild.firstChild,
+ "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #13
+ contenteditable.innerHTML = "<span></span><i>abc<u></u></i>";
+ selection.selectAllChildren(contenteditable);
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable.childNodes.item(1).firstChild,
+ "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.childNodes.item(1).firstChild,
+ "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #14
+ contenteditable.innerHTML = "<span></span><i><u>abc</u></i>";
+ selection.selectAllChildren(contenteditable);
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable.childNodes.item(1).firstChild.firstChild,
+ "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.childNodes.item(1).firstChild.firstChild,
+ "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #15
+ contenteditable.innerHTML = "<span></span><i><u></u>abc</i>";
+ selection.selectAllChildren(contenteditable);
+
+ await synthesizeSelectionSet(0, 0);
+ is(selection.anchorNode, contenteditable.childNodes.item(1).lastChild,
+ "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.childNodes.item(1).lastChild,
+ "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(0, "", "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #16
+ contenteditable.innerHTML = "a<blink>b</blink>c";
+ await synthesizeSelectionSet(0, 3);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node");
+ is(selection.focusOffset, contenteditable.lastChild.wholeText.length,
+ "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node");
+ checkSelection(0, "abc", "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\"");
+
+ // #17 (bug 1319660 - incorrect adjustment of content iterator last node)
+ contenteditable.innerHTML = "<div>a</div><div><br></div>";
+
+ await synthesizeSelectionSet(kLFLen, 1+kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <div> element");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(kLFLen, "a" + kLF, "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ await synthesizeSelectionSet(1+2*kLFLen, 0);
+ is(selection.anchorNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the second <div> element");
+ is(selection.anchorOffset, 0,
+ "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(1+2*kLFLen, "", "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #18 (bug 1319660 - content iterator start node regression)
+ contenteditable.innerHTML = "<div><br></div><div><br></div>";
+
+ await synthesizeSelectionSet(2*kLFLen, kLFLen);
+ is(selection.anchorNode, contenteditable.firstChild,
+ "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <div> element");
+ is(selection.anchorOffset, 1,
+ "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1");
+ is(selection.focusNode, contenteditable.lastChild,
+ "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element");
+ is(selection.focusOffset, 0,
+ "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0");
+ checkSelection(2*kLFLen, kLF, "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+}
+
+function runQueryTextContentEventTest()
+{
+ contenteditable.focus();
+
+ let result;
+
+ // #1
+ contenteditable.innerHTML = "abc<br>def";
+
+ result = synthesizeQueryTextContent(0, 6 + kLFLen);
+ is(result.text, "abc" + kLF + "def", "runQueryTextContentEventTest #1 (0, 6+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, 100);
+ is(result.text, "abc" + kLF + "def", "runQueryTextContentEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(2, 2 + kLFLen);
+ is(result.text, "c" + kLF + "d", "runQueryTextContentEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(1, 2);
+ is(result.text, "bc", "runQueryTextContentEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(6 + kLFLen, 1);
+ is(result.text, "", "runQueryTextContentEventTest #1 (6 + kLFLen, 0), \"" + contenteditable.innerHTML + "\"");
+
+ // #2
+ contenteditable.innerHTML = "<p>a<b>b</b>c</p><p>def</p>";
+
+ result = synthesizeQueryTextContent(kLFLen, 4+kLFLen);
+ is(result.text, "abc" + kLF + "d", "runQueryTextContentEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen, 2);
+ is(result.text, "ab", "runQueryTextContentEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(1+kLFLen, 2);
+ is(result.text, "bc", "runQueryTextContentEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(2+kLFLen, 2+kLFLen);
+ is(result.text, "c" + kLF + "d", "runQueryTextContentEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3+kLFLen*2, 1);
+ is(result.text, "d", "runQueryTextContentEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(2+kLFLen, 1+kLFLen);
+ is(result.text, "c" + kLF, "runQueryTextContentEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3+kLFLen, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3+kLFLen, 1+kLFLen);
+ is(result.text, kLF + "d", "runQueryTextContentEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ // #3
+ contenteditable.innerHTML = "<div>abc<p>def</p></div>";
+
+ result = synthesizeQueryTextContent(1+kLFLen, 2);
+ is(result.text, "bc", "runQueryTextContentEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(1+kLFLen, 3+kLFLen);
+ is(result.text, "bc" + kLF + "d", "runQueryTextContentEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3+kLFLen*2, 1);
+ is(result.text, "d", "runQueryTextContentEventTest #3 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, 6+kLFLen*2);
+ is(result.text, kLF + "abc" + kLF + "def", "runQueryTextContentEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, 100);
+ is(result.text, kLF + "abc" + kLF + "def", "runQueryTextContentEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(4+kLFLen*2, 2);
+ is(result.text, "ef", "runQueryTextContentEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(4+kLFLen*2, 100);
+ is(result.text, "ef", "runQueryTextContentEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(6+kLFLen*2, 1);
+ is(result.text, "", "runQueryTextContentEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, 1+kLFLen);
+ is(result.text, kLF + "a", "runQueryTextContentEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(2+kLFLen, 1+kLFLen);
+ is(result.text, "c" + kLF, "runQueryTextContentEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3+kLFLen, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3+kLFLen, 1+kLFLen);
+ is(result.text, kLF + "d", "runQueryTextContentEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ // #4
+ contenteditable.innerHTML = "<div><p>abc</p>def</div>";
+
+ result = synthesizeQueryTextContent(1+kLFLen*2, 2);
+ is(result.text, "bc", "runQueryTextContentEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(1+kLFLen*2, 3);
+ is(result.text, "bcd", "runQueryTextContentEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(3+kLFLen*2, 1);
+ is(result.text, "d", "runQueryTextContentEventTest #4 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, 6+kLFLen*2);
+ is(result.text, kLF + kLF + "abcdef", "runQueryTextContentEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, 100);
+ is(result.text, kLF + kLF + "abcdef", "runQueryTextContentEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(4+kLFLen*2, 2);
+ is(result.text, "ef", "runQueryTextContentEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(4+kLFLen*2, 100);
+ is(result.text, "ef", "runQueryTextContentEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(6+kLFLen*2, 1);
+ is(result.text, "", "runQueryTextContentEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, kLFLen*2);
+ is(result.text, kLF + kLF, "runQueryTextContentEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, 1+kLFLen*2);
+ is(result.text, kLF + kLF + "a", "runQueryTextContentEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen, 1+kLFLen);
+ is(result.text, kLF + "a", "runQueryTextContentEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ // #5
+ contenteditable.innerHTML = "<br>";
+
+ result = synthesizeQueryTextContent(0, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen, 1);
+ is(result.text, "", "runQueryTextContentEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\"");
+
+ // #6
+ contenteditable.innerHTML = "<p><br></p>";
+
+ result = synthesizeQueryTextContent(kLFLen, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen*2, 1);
+ is(result.text, "", "runQueryTextContentEventTest #5 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, kLFLen*2);
+ is(result.text, kLF + kLF, "runQueryTextContentEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ // #7
+ contenteditable.innerHTML = "<br><br>";
+
+ result = synthesizeQueryTextContent(0, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(0, kLFLen * 2);
+ is(result.text, kLF + kLF, "runQueryTextContentEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen * 2, 1);
+ is(result.text, "", "runQueryTextContentEventTest #7 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\"");
+
+ // #8
+ contenteditable.innerHTML = "<p><br><br></p>";
+
+ result = synthesizeQueryTextContent(kLFLen, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen, kLFLen * 2);
+ is(result.text, kLF + kLF, "runQueryTextContentEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen*2, kLFLen);
+ is(result.text, kLF, "runQueryTextContentEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\"");
+
+ result = synthesizeQueryTextContent(kLFLen*3, 1);
+ is(result.text, "", "runQueryTextContentEventTest #8 (kLFLen*3, 1), \"" + contenteditable.innerHTML + "\"");
+
+ // #16
+ contenteditable.innerHTML = "a<blink>b</blink>c";
+
+ result = synthesizeQueryTextContent(0, 3);
+ is(result.text, "abc", "runQueryTextContentEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\"");
+}
+
+function runQuerySelectionEventTest()
+{
+ contenteditable.focus();
+
+ let selection = windowOfContenteditable.getSelection();
+
+ // #1
+ contenteditable.innerHTML = "<br/>a";
+ selection.setBaseAndExtent(
+ contenteditable.firstChild,
+ 0,
+ contenteditable.lastChild,
+ 1
+ );
+ checkSelection(
+ 0,
+ `${kLF}a`,
+ `runQuerySelectionEventTest #1, "${contenteditable.innerHTML}"`
+ );
+
+ // #2
+ contenteditable.innerHTML = "<p></p><p>abc</p>";
+ selection.setBaseAndExtent(
+ contenteditable.firstChild,
+ 0,
+ contenteditable.lastChild.firstChild,
+ 1
+ );
+ checkSelection(
+ kLFLen,
+ `${kLF}a`,
+ `runQuerySelectionEventTest #2, "${contenteditable.innerHTML}"`
+ );
+
+ // #3
+ contenteditable.innerHTML = "<p>abc</p><p>def</p>";
+ selection.setBaseAndExtent(
+ contenteditable.firstChild,
+ 0,
+ contenteditable.lastChild.firstChild,
+ 1
+ );
+ checkSelection(
+ kLFLen,
+ `abc${kLF}d`,
+ `runQuerySelectionEventTest #3, "${contenteditable.innerHTML}"`
+ );
+
+ // #4
+ contenteditable.innerHTML = "<p>abc</p>";
+ selection.removeAllRanges();
+ checkSelection(
+ null,
+ null,
+ `runQuerySelectionEventTest #4, "${contenteditable.innerHTML}"`
+ );
+}
+
+function runQueryIMESelectionTest()
+{
+ textarea.focus();
+ textarea.value = "before after";
+ let startoffset = textarea.selectionStart = textarea.selectionEnd = "before ".length;
+
+ if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") ||
+ !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") ||
+ !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") ||
+ !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: before starting composition")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "a",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ if (!checkIMESelection("RawClause", true, startoffset, "a", "runQueryIMESelectionTest: inputting raw text") ||
+ !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text") ||
+ !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text") ||
+ !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abcdefgh",
+ "clauses":
+ [
+ { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 8, "length": 0 }
+ });
+
+ if (!checkIMESelection("RawClause", true, startoffset, "abcdefgh", "runQueryIMESelectionTest: updating raw text") ||
+ !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: updating raw text") ||
+ !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: updating raw text") ||
+ !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: updating raw text")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABCDEFGH",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: starting to convert") ||
+ !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: starting to convert") ||
+ !checkIMESelection("ConvertedClause", true, startoffset + 2, "CDE", "runQueryIMESelectionTest: starting to convert") ||
+ !checkIMESelection("SelectedClause", true, startoffset, "AB", "runQueryIMESelectionTest: starting to convert")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABCDEFGH",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": 3, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ ]
+ },
+ "caret": { "start": 5, "length": 0 }
+ });
+
+ if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: changing selected clause") ||
+ !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: changing selected clause") ||
+ !checkIMESelection("ConvertedClause", true, startoffset, "AB", "runQueryIMESelectionTest: changing selected clause") ||
+ !checkIMESelection("SelectedClause", true, startoffset + 2, "CDE", "runQueryIMESelectionTest: changing selected clause")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") ||
+ !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") ||
+ !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") ||
+ !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: after committing composition")) {
+ return;
+ }
+
+ startoffset = textarea.selectionStart;
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abcdefgh",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ ]
+ },
+ "caret": { "start": 8, "length": 0 }
+ });
+
+ if (!checkIMESelection("RawClause", true, startoffset, "a", "runQueryIMESelectionTest: unrealistic testcase") ||
+ !checkIMESelection("SelectedRawClause", true, startoffset + 1, "b", "runQueryIMESelectionTest: unrealistic testcase") ||
+ !checkIMESelection("ConvertedClause", true, startoffset + 2, "c", "runQueryIMESelectionTest: unrealistic testcase") ||
+ !checkIMESelection("SelectedClause", true, startoffset + 3, "d", "runQueryIMESelectionTest: unrealistic testcase")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+}
+
+function runQueryPasswordTest() {
+ function checkRange(aOffset, aLength, aExpectedResult, aDescription) {
+ password.focus();
+ let result = synthesizeQueryTextContent(aOffset, aLength);
+ is(result.text, aExpectedResult,
+ `${aDescription}: synthesizeQueryTextContent(${aOffset}, ${aLength})`);
+ password.setSelectionRange(aOffset, aOffset + aLength);
+ result = synthesizeQuerySelectedText();
+ is(result.text, aExpectedResult,
+ `${aDescription}: synthesizeQuerySelectedText(${aOffset}, ${aLength})`);
+ }
+
+ let editor = password.editor;
+ const kMask = editor.passwordMask;
+ password.value = "abcdef";
+
+ editor.mask();
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range is not specified #1");
+ checkRange(0, 3, `${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range is not specified #2");
+ checkRange(3, 3, `${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range is not specified #3");
+ checkRange(2, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range is not specified #4");
+
+ editor.unmask(0, 6);
+ checkRange(0, 6, "abcdef",
+ "runQueryPasswordTest: unmasked range 0-6 #1");
+ checkRange(0, 3, "abc",
+ "runQueryPasswordTest: unmasked range 0-6 #2");
+ checkRange(3, 3, "def",
+ "runQueryPasswordTest: unmasked range 0-6 #3");
+ checkRange(2, 2, "cd",
+ "runQueryPasswordTest: unmasked range 0-6 #4");
+
+ editor.unmask(0, 3);
+ checkRange(0, 6, `abc${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range 0-3 #1");
+ checkRange(0, 3, "abc",
+ "runQueryPasswordTest: unmasked range 0-3 #2");
+ checkRange(3, 3, `${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range 0-3 #3");
+ checkRange(2, 2, `c${kMask}`,
+ "runQueryPasswordTest: unmasked range 0-3 #4");
+
+ editor.unmask(3, 6);
+ checkRange(0, 6, `${kMask}${kMask}${kMask}def`,
+ "runQueryPasswordTest: unmasked range 3-6 #1");
+ checkRange(0, 3, `${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range 3-6 #2");
+ checkRange(3, 3, `def`,
+ "runQueryPasswordTest: unmasked range 3-6 #3");
+ checkRange(2, 2, `${kMask}d`,
+ "runQueryPasswordTest: unmasked range 3-6 #4");
+
+ editor.unmask(2, 4);
+ checkRange(0, 6, `${kMask}${kMask}cd${kMask}${kMask}`,
+ "runQueryPasswordTest: unmasked range 3-4 #1");
+ checkRange(1, 2, `${kMask}c`,
+ "runQueryPasswordTest: unmasked range 3-4 #2");
+ checkRange(1, 3, `${kMask}cd`,
+ "runQueryPasswordTest: unmasked range 3-4 #3");
+ checkRange(1, 4, `${kMask}cd${kMask}`,
+ "runQueryPasswordTest: unmasked range 3-4 #4");
+ checkRange(2, 2, "cd",
+ "runQueryPasswordTest: unmasked range 3-4 #5");
+ checkRange(2, 3, `cd${kMask}`,
+ "runQueryPasswordTest: unmasked range 3-4 #6");
+
+
+ const kEmoji = String.fromCodePoint(0x1f914);
+ password.value = `${kEmoji}${kEmoji}${kEmoji}`
+
+ editor.mask();
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range is not specified");
+
+ editor.unmask(0, 2);
+ checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #1");
+ checkRange(0, 2, `${kEmoji}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #2");
+ checkRange(2, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #3");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #4");
+
+ editor.unmask(2, 4);
+ checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #1");
+ checkRange(0, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #2");
+ checkRange(2, 2, `${kEmoji}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #3");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #4");
+
+ editor.unmask(4, 6);
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #1");
+ checkRange(0, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #2");
+ checkRange(2, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #3");
+ checkRange(4, 2, `${kEmoji}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #4");
+
+ editor.unmask(0, 1);
+ checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 0-1");
+
+ editor.unmask(1, 2);
+ checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 1-2");
+
+ editor.unmask(2, 3);
+ checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 2-3");
+
+ editor.unmask(3, 4);
+ checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 3-4");
+
+ editor.unmask(4, 5);
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 4-5");
+
+ editor.unmask(5, 6);
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`,
+ "runQueryPasswordTest: Emojis in password, unmasked range 5-6");
+
+
+ const kEmojiSuperhero = String.fromCodePoint(0x1f9b8);
+ const kEmojiMediumSkinTone = String.fromCodePoint(0x1f3fd);
+ const kZeroWidthJoiner = "\u200d";
+ const kFemaleSign = "\u2640";
+ const kVariationSelector16 = "\ufe0f";
+ const kComplicatedEmoji = `${kEmojiSuperhero}${kEmojiMediumSkinTone}${kZeroWidthJoiner}${kFemaleSign}${kVariationSelector16}`;
+ password.value = `${kComplicatedEmoji}${kComplicatedEmoji}${kComplicatedEmoji}`
+ editor.mask();
+ checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range is not specified");
+
+ editor.unmask(0, 7);
+ checkRange(0, 21, `${kComplicatedEmoji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #1");
+ checkRange(0, 7, `${kComplicatedEmoji}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #2");
+ checkRange(7, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #3");
+ checkRange(14, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #4");
+
+ editor.unmask(7, 14);
+ checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kComplicatedEmoji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #1");
+ checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #2");
+ checkRange(7, 7, `${kComplicatedEmoji}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #3");
+ checkRange(14, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #4");
+
+ editor.unmask(14, 21);
+ checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kComplicatedEmoji}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #1");
+ checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #2");
+ checkRange(7, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #3");
+ checkRange(14, 7, `${kComplicatedEmoji}`,
+ "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #4");
+
+ password.value = `${kComplicatedEmoji}`
+ editor.unmask(0, 1);
+ checkRange(0, 7, `${kEmojiSuperhero}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emoji in password, unmasked range 0-1");
+
+ editor.unmask(1, 2);
+ checkRange(0, 7, `${kEmojiSuperhero}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emoji in password, unmasked range 1-2");
+
+ editor.unmask(2, 3);
+ checkRange(0, 7, `${kMask}${kMask}${kEmojiMediumSkinTone}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emoji in password, unmasked range 2-3");
+
+ editor.unmask(3, 4);
+ checkRange(0, 7, `${kMask}${kMask}${kEmojiMediumSkinTone}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emoji in password, unmasked range 3-4");
+
+ editor.unmask(4, 5);
+ checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kZeroWidthJoiner}${kMask}${kMask}`,
+ "runQueryPasswordTest: Complicated emoji in password, unmasked range 4-5");
+
+ editor.unmask(5, 6);
+ checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kFemaleSign}${kMask}`,
+ "runQueryPasswordTest: Complicated emoji in password, unmasked range 5-6");
+
+ editor.unmask(6, 7);
+ checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kVariationSelector16}`,
+ "runQueryPasswordTest: Complicated emoji in password, unmasked range 6-7");
+
+
+ const kKanji = "\u8fba";
+ const kIVS = String.fromCodePoint(0xe0101);
+ const kKanjiWithIVS = `${kKanji}${kIVS}`;
+ password.value = `${kKanjiWithIVS}${kKanjiWithIVS}${kKanjiWithIVS}`
+
+ editor.mask();
+ checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range is not specified");
+
+ editor.unmask(0, 3);
+ checkRange(0, 9, `${kKanjiWithIVS}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #1");
+ checkRange(0, 3, `${kKanjiWithIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #2");
+ checkRange(1, 3, `${kIVS}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #3");
+ checkRange(0, 1, `${kKanji}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #4");
+ checkRange(1, 2, `${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #5");
+ checkRange(3, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #6");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #7");
+ checkRange(6, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #8");
+ checkRange(7, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #9");
+
+ editor.unmask(0, 1);
+ checkRange(0, 9, `${kKanji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #1");
+ checkRange(0, 1, `${kKanji}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #2");
+ checkRange(1, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #3");
+ checkRange(3, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #4");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #5");
+ checkRange(6, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #6");
+ checkRange(7, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #7");
+
+ editor.unmask(1, 3);
+ checkRange(0, 9, `${kMask}${kIVS}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #1");
+ checkRange(0, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #2");
+ checkRange(1, 2, `${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #3");
+ checkRange(3, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #4");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #5");
+ checkRange(6, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #6");
+ checkRange(7, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #7");
+
+ editor.unmask(3, 6);
+ checkRange(0, 9, `${kMask}${kMask}${kMask}${kKanjiWithIVS}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #1");
+ checkRange(3, 3, `${kKanjiWithIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #2");
+ checkRange(4, 3, `${kIVS}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #3");
+ checkRange(0, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #4");
+ checkRange(1, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #5");
+ checkRange(3, 1, `${kKanji}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #6");
+ checkRange(4, 2, `${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #7");
+ checkRange(6, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #8");
+ checkRange(7, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #9");
+
+ editor.unmask(3, 4);
+ checkRange(0, 9, `${kMask}${kMask}${kMask}${kKanji}${kMask}${kMask}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #1");
+ checkRange(0, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #2");
+ checkRange(1, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #3");
+ checkRange(3, 1, `${kKanji}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #4");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #5");
+ checkRange(6, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #6");
+ checkRange(7, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #7");
+
+ editor.unmask(4, 6);
+ checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kIVS}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #1");
+ checkRange(0, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #2");
+ checkRange(1, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #3");
+ checkRange(3, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #4");
+ checkRange(4, 2, `${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #5");
+ checkRange(6, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #6");
+ checkRange(7, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #7");
+
+ editor.unmask(6, 9);
+ checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kKanjiWithIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #1");
+ checkRange(6, 3, `${kKanjiWithIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #2");
+ checkRange(4, 3, `${kMask}${kMask}${kKanji}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #3");
+ checkRange(0, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #4");
+ checkRange(1, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #5");
+ checkRange(3, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #6");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #7");
+ checkRange(6, 1, `${kKanji}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #8");
+ checkRange(7, 2, `${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #9");
+
+ editor.unmask(6, 7);
+ checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kKanji}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #1");
+ checkRange(0, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #2");
+ checkRange(1, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #3");
+ checkRange(3, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #4");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #5");
+ checkRange(6, 1, `${kKanji}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #6");
+ checkRange(7, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #7");
+
+ editor.unmask(7, 9);
+ checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #1");
+ checkRange(0, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #2");
+ checkRange(1, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #3");
+ checkRange(3, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #4");
+ checkRange(4, 2, `${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #5");
+ checkRange(6, 1, `${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #6");
+ checkRange(7, 2, `${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #7");
+
+ password.value = `${kKanjiWithIVS}${kKanjiWithIVS}`;
+ editor.unmask(0, 2);
+ checkRange(0, 6, `${kKanjiWithIVS}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-2");
+
+ editor.unmask(1, 2);
+ checkRange(0, 6, `${kMask}${kIVS}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-2");
+
+ editor.unmask(2, 3);
+ checkRange(0, 6, `${kMask}${kIVS}${kMask}${kMask}${kMask}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 2-3");
+
+ editor.unmask(3, 5);
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kKanjiWithIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-5");
+
+ editor.unmask(4, 5);
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-5");
+
+ editor.unmask(5, 6);
+ checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kIVS}`,
+ "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 5-6");
+
+ editor.mask();
+}
+
+function runQueryContentEventRelativeToInsertionPoint()
+{
+ textarea.focus();
+ textarea.value = "0123456789";
+
+ // "[]0123456789"
+ let startOffset = textarea.selectionStart = textarea.selectionEnd = 0;
+ if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#1") ||
+ !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#2") ||
+ !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#3") ||
+ !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#4") ||
+ !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#5")) {
+ return;
+ }
+
+ // "[01234]56789"
+ textarea.selectionEnd = 5;
+ if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#1") ||
+ !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#2") ||
+ !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#3") ||
+ !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#4") ||
+ !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#5")) {
+ return;
+ }
+
+ // "0123[]456789"
+ startOffset = textarea.selectionStart = textarea.selectionEnd = 4;
+ if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "4", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#1") ||
+ !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#2") ||
+ !checkContentRelativeToSelection(1, 1, startOffset + 1, "5", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#3") ||
+ !checkContentRelativeToSelection(5, 10, startOffset + 5, "9", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#4") ||
+ !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#5")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "a",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ // "0123[a]456789"
+ if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#1") ||
+ !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#2") ||
+ !checkContentRelativeToSelection(1, 1, startOffset + 1, "4", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#3") ||
+ !checkContentRelativeToSelection(5, 10, startOffset + 5, "89", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#4") ||
+ !checkContentRelativeToSelection(11, 1, 11, "", "runQueryContentEventRelativeToInsertionPoint[composition at 4]")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ // Move start of composition at first compositionupdate event.
+ function onCompositionUpdate(aEvent)
+ {
+ startOffset = textarea.selectionStart = textarea.selectionEnd = textarea.selectionStart - 1;
+ textarea.removeEventListener("compositionupdate", onCompositionUpdate);
+ }
+ textarea.addEventListener("compositionupdate", onCompositionUpdate);
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "b",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ // "0123[b]a456789"
+ if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "b", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#1") ||
+ !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#2") ||
+ !checkContentRelativeToSelection(1, 1, startOffset + 1, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#3") ||
+ !checkContentRelativeToSelection(5, 10, startOffset + 5, "789", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#4") ||
+ !checkContentRelativeToSelection(12, 1, 12, "", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#5")) {
+ synthesizeComposition({ type: "compositioncommitasis" });
+ return;
+ }
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+}
+
+function runBug1375825Test()
+{
+ contenteditable.focus();
+
+ // #1
+ contenteditable.innerHTML = "abc<span contenteditable=\"false\">defgh</span>";
+
+ let ret = synthesizeQueryTextRect(2, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #1 (2, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "c", "runBug1375825Test #1 (2, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'c'");
+
+ ret = synthesizeQueryTextRect(3, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #1 (3, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "d", "runBug1375825Test #1 (3, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'd'");
+
+ ret = synthesizeQueryTextRect(4, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #1 (4, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "e", "runBug1375825Test #1 (4, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'e'");
+
+ ret = synthesizeQueryTextRect(5, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #1 (5, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "f", "runBug1375825Test #1 (5, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'f'");
+
+ ret = synthesizeQueryTextRect(6, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #1 (6, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "g", "runBug1375825Test #1 (6, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'g'");
+
+ ret = synthesizeQueryTextRect(7, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #1 (7, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "h", "runBug1375825Test #1 (7, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'h'");
+
+ // #2
+ contenteditable.innerHTML = "abc<span style=\"user-select: all;\">defgh</span>";
+
+ ret = synthesizeQueryTextRect(2, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #2 (2, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "c", "runBug1375825Test #2 (2, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'c'");
+
+ ret = synthesizeQueryTextRect(3, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #2 (3, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "d", "runBug1375825Test #2 (3, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'd'");
+
+ ret = synthesizeQueryTextRect(4, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #2 (4, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "e", "runBug1375825Test #2 (4, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'e'");
+
+ ret = synthesizeQueryTextRect(5, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #2 (5, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "f", "runBug1375825Test #2 (5, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'f'");
+
+ ret = synthesizeQueryTextRect(6, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #2 (6, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "g", "runBug1375825Test #2 (6, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'g'");
+
+ ret = synthesizeQueryTextRect(7, 1);
+ if (!checkQueryContentResult(ret, "runBug1375825Test #2 (7, 1), \"" + contenteditable.innerHTML + "\"")) {
+ return;
+ }
+ is(ret.text, "h", "runBug1375825Test #2 (7, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'h'");
+}
+
+function runBug1530649Test()
+{
+ // Vietnamese IME on macOS commits composition with typing space key.
+ // Then, typing new word shouldn't trim the trailing whitespace.
+ contenteditable.focus();
+ contenteditable.innerHTML = "";
+ synthesizeCompositionChange(
+ {composition: {string: "abc", clauses: [{length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ caret: {start: 3, length: 0}});
+ synthesizeComposition({type: "compositioncommit", data: "abc ", key: " "});
+
+ is(contenteditable.innerHTML, "abc <br>",
+ "runBug1530649Test: The trailing space shouldn't be removed");
+
+ synthesizeCompositionChange(
+ {composition: {string: "d", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ caret: {start: 1, length: 0}});
+
+ is(contenteditable.innerHTML, "abc d<br>",
+ "runBug1530649Test: The new composition string shouldn't remove the last space");
+
+ synthesizeComposition({type: "compositioncommitasis", key: "KEY_Enter"});
+
+ is(contenteditable.innerHTML, "abc d<br>",
+ "runBug1530649Test: Committing the new composition string shouldn't remove the last space");
+}
+
+function runBug1571375Test()
+{
+ let selection = windowOfContenteditableBySpan.getSelection();
+ let doc = document.getElementById("iframe7").contentDocument;
+
+ contenteditableBySpan.focus();
+
+ contenteditableBySpan.innerHTML = "hello world";
+ let range = doc.createRange();
+ range.setStart(contenteditableBySpan.firstChild, 6);
+ range.setEnd(contenteditableBySpan.firstChild, 11);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ synthesizeCompositionChange({
+ composition: {string: "world", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ caret: { start: 5, length: 0 },
+ });
+ synthesizeComposition({type: "compositioncommit", data: "world", key: " "});
+ is(contenteditableBySpan.innerHTML, "hello world",
+ "runBug1571375Test: space must not be removed by commit");
+
+ contenteditableBySpan.innerHTML = "hello world";
+ range = doc.createRange();
+ range.setStart(contenteditableBySpan.firstChild, 0);
+ range.setEnd(contenteditableBySpan.firstChild, 5);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ synthesizeCompositionChange({
+ composition: {string: "hello", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ caret: { start: 5, length: 0 },
+ });
+ synthesizeComposition({type: "compositioncommit", data: "hello", key: " "});
+ is(contenteditableBySpan.innerHTML, "hello world",
+ "runBug1571375Test: space must not be removed by commit");
+
+ contenteditableBySpan.innerHTML = "hello world<div>.</div>";
+ range = doc.createRange();
+ range.setStart(contenteditableBySpan.firstChild, 6);
+ range.setEnd(contenteditableBySpan.firstChild, 11);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ synthesizeCompositionChange({
+ composition: {string: "world", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ caret: {start: 0, length: 0}}
+ );
+ synthesizeComposition({type: "compositioncommit", data: "world", key: " "});
+ is(contenteditableBySpan.innerHTML, "hello world<div>.</div>",
+ "runBug1571375Test: space must not be removed by commit");
+}
+
+async function runBug1584901Test()
+{
+ contenteditableBySpan.focus();
+ contenteditableBySpan.innerHTML = "";
+
+ // XXX synthesizeCompositionChange won't work without wait.
+ await waitForTick();
+
+ synthesizeCompositionChange({
+ composition: {string: "a ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ });
+ synthesizeComposition({type: "compositioncommitasis", key: " "});
+
+ is(contenteditableBySpan.innerHTML, "a&nbsp;",
+ "runBug1584901Test: space must not be removed by composition change");
+
+ synthesizeCompositionChange({
+ composition: {string: "b ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ });
+ synthesizeComposition({type: "compositioncommitasis", key: " "});
+
+ is(contenteditableBySpan.innerHTML, "a b&nbsp;",
+ "runBug1584901Test: space must not be removed by composition change");
+}
+
+function runBug1675313Test()
+{
+ input.value = "";
+ input.focus();
+ let count = 0;
+
+ function handler() {
+ input.focus();
+ count++;
+ }
+
+ input.addEventListener("keydown", handler);
+ input.addEventListener("keyup", handler);
+
+ synthesizeCompositionChange({
+ composition: {
+ string: "a",
+ clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ key: { key: "a", type: "keyup" },
+ },
+ });
+ synthesizeCompositionChange({
+ composition: {
+ string: "b",
+ clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ key: { key: "b", type: "keyup" },
+ },
+ });
+ synthesizeComposition({type: "compositioncommitasis"});
+
+ is(count, 6, "runBug1675313Test: keydown event and keyup event are fired correctly");
+ is(input.value, "b",
+ "runBug1675313Test: re-focus element doesn't commit composition if re-focus isn't click by user");
+
+ input.removeEventListener("keyup", handler);
+}
+
+function runCommitCompositionWithSpaceKey()
+{
+ contenteditable.focus();
+ contenteditable.innerHTML = "";
+
+ // Last white space might be &nbsp; if last child is no <br>
+ // Actually, our implementation will insert <br> element at last child, so
+ // white space will be ASCII space.
+
+ synthesizeCompositionChange({
+ composition: {string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ });
+ synthesizeComposition({type: "compositioncommit", data: "a"});
+ synthesizeKey(" ");
+
+ is(contenteditable.innerHTML, "a <br>",
+ "runCommitCompositionWithSpaceKey: last single space should be kept");
+
+ synthesizeCompositionChange({
+ composition: {string: "b", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ });
+ synthesizeComposition({type: "compositioncommit", data: "b"});
+ synthesizeKey(" ");
+
+ is(contenteditable.innerHTML, "a b <br>",
+ "runCommitCompositionWithSpaceKey: inserting composition shouldn't remove last single space.");
+
+ synthesizeCompositionChange({
+ composition: {string: "c", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ });
+ synthesizeComposition({type: "compositioncommit", data: "c"});
+ synthesizeKey(" ");
+
+ is(contenteditable.innerHTML, "a b c <br>",
+ "runCommitCompositionWithSpaceKey: inserting composition shouldn't remove last single space.");
+
+ contenteditable.innerHTML = "a";
+ windowOfContenteditable.getSelection().collapse(contenteditable.firstChild, contenteditable.firstChild.length);
+ is(contenteditable.innerHTML, "a",
+ "runCommitCompositionWithSpaceKey: contenteditable should be initialized with text ending with a space and without following <br> element");
+
+ synthesizeCompositionChange({
+ composition: {string: "b", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ });
+ synthesizeComposition({type: "compositioncommit", data: "b ", key: { key: " ", code: "Space" }});
+
+ is(contenteditable.innerHTML, "ab <br>",
+ "runCommitCompositionWithSpaceKey: contenteditable should end with a padding <br> element after inserting commit string ending with a space");
+}
+
+function runCSSTransformTest()
+{
+ textarea.focus();
+ textarea.value = "some text";
+ textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
+ let editorRect = synthesizeQueryEditorRect();
+ if (!checkQueryContentResult(editorRect,
+ "runCSSTransformTest: editorRect")) {
+ return;
+ }
+ let firstCharRect = synthesizeQueryTextRect(0, 1);
+ if (!checkQueryContentResult(firstCharRect,
+ "runCSSTransformTest: firstCharRect")) {
+ return;
+ }
+ let lastCharRect = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length);
+ if (!checkQueryContentResult(lastCharRect,
+ "runCSSTransformTest: lastCharRect")) {
+ return;
+ }
+ let caretRect = synthesizeQueryCaretRect(textarea.selectionStart);
+ if (!checkQueryContentResult(caretRect,
+ "runCSSTransformTest: caretRect")) {
+ return;
+ }
+ let caretRectBeforeFirstChar = synthesizeQueryCaretRect(0);
+ if (!checkQueryContentResult(caretRectBeforeFirstChar,
+ "runCSSTransformTest: caretRectBeforeFirstChar")) {
+ return;
+ }
+
+ try {
+ textarea.style.transform = "translate(10px, 15px)";
+ function movedRect(aRect, aCSS_CX, aCSS_CY)
+ {
+ return {
+ left: aRect.left + Math.round(aCSS_CX * window.devicePixelRatio),
+ top: aRect.top + Math.round(aCSS_CY * window.devicePixelRatio),
+ width: aRect.width,
+ height: aRect.height
+ };
+ }
+
+ let editorRectTranslated = synthesizeQueryEditorRect();
+ if (!checkQueryContentResult(editorRectTranslated,
+ "runCSSTransformTest: editorRectTranslated, " + textarea.style.transform) ||
+ !checkRectFuzzy(editorRectTranslated, movedRect(editorRect, 10, 15), {left: 1, top: 1, width: 1, height: 1},
+ "runCSSTransformTest: editorRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ let firstCharRectTranslated = synthesizeQueryTextRect(0, 1);
+ if (!checkQueryContentResult(firstCharRectTranslated,
+ "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform) ||
+ !checkRectFuzzy(firstCharRectTranslated, movedRect(firstCharRect, 10, 15), {left: 1, top: 1, width: 1, height: 1},
+ "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ let lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length);
+ if (!checkQueryContentResult(lastCharRectTranslated,
+ "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform) ||
+ !checkRectFuzzy(lastCharRectTranslated, movedRect(lastCharRect, 10, 15), {left: 1, top: 1, width: 1, height: 1},
+ "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ let caretRectTranslated = synthesizeQueryCaretRect(textarea.selectionStart);
+ if (!checkQueryContentResult(caretRectTranslated,
+ "runCSSTransformTest: caretRectTranslated, " + textarea.style.transform) ||
+ !checkRectFuzzy(caretRectTranslated, movedRect(caretRect, 10, 15), {left: 1, top: 1, width: 1, height: 1},
+ "runCSSTransformTest: caretRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ let caretRectBeforeFirstCharTranslated = synthesizeQueryCaretRect(0);
+ if (!checkQueryContentResult(caretRectBeforeFirstCharTranslated,
+ "runCSSTransformTest: caretRectBeforeFirstCharTranslated, " + textarea.style.transform) ||
+ !checkRectFuzzy(caretRectBeforeFirstCharTranslated, movedRect(caretRectBeforeFirstChar, 10, 15), {left: 1, top: 1, width: 1, height: 1},
+ "runCSSTransformTest: caretRectBeforeFirstCharTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ let firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1);
+ if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) ||
+ !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) {
+ return;
+ }
+ let lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length);
+ if (!checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform) ||
+ !checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform)) {
+ return;
+ }
+
+ // XXX It's too difficult to check the result with scale and rotate...
+ // For now, let's check if query text rect and query text rect array returns same rect.
+ textarea.style.transform = "scale(1.5)";
+ firstCharRectTranslated = synthesizeQueryTextRect(0, 1);
+ if (!checkQueryContentResult(firstCharRectTranslated,
+ "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length);
+ if (!checkQueryContentResult(lastCharRectTranslated,
+ "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1);
+ if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) ||
+ !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) {
+ return;
+ }
+ lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length);
+ if (!checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform) ||
+ !checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform)) {
+ return;
+ }
+
+ textarea.style.transform = "rotate(30deg)";
+ firstCharRectTranslated = synthesizeQueryTextRect(0, 1);
+ if (!checkQueryContentResult(firstCharRectTranslated,
+ "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length);
+ if (!checkQueryContentResult(lastCharRectTranslated,
+ "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) {
+ return;
+ }
+ firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1);
+ if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) ||
+ !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) {
+ return;
+ }
+ lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length);
+ checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform);
+ checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform);
+ } finally {
+ textarea.style.transform = "";
+ }
+}
+
+function runBug722639Test()
+{
+ textarea.focus();
+ textarea.value = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
+ textarea.value += textarea.value;
+ textarea.value += textarea.value; // 80 characters
+
+ let firstLine = synthesizeQueryTextRect(0, 1);
+ if (!checkQueryContentResult(firstLine,
+ "runBug722639Test: firstLine")) {
+ return;
+ }
+ ok(true, "runBug722639Test: 1st line, top=" + firstLine.top + ", left=" + firstLine.left);
+ let firstLineAsArray = synthesizeQueryTextRectArray(0, 1);
+ if (!checkQueryContentResult(firstLineAsArray, "runBug722639Test: 1st line as array") ||
+ !checkRectArray(firstLineAsArray, [firstLine], "runBug722639Test: 1st line as array should match with text rect result")) {
+ return;
+ }
+ if (kLFLen > 1) {
+ let firstLineLF = synthesizeQueryTextRect(1, 1);
+ if (!checkQueryContentResult(firstLineLF,
+ "runBug722639Test: firstLineLF")) {
+ return;
+ }
+ is(firstLineLF.top, firstLine.top, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect");
+ is(firstLineLF.left, firstLine.left, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect");
+ isfuzzy(firstLineLF.height, firstLine.height, 1,
+ "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect");
+ is(firstLineLF.width, firstLine.width, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect");
+ let firstLineLFAsArray = synthesizeQueryTextRectArray(1, 1);
+ if (!checkQueryContentResult(firstLineLFAsArray, "runBug722639Test: 1st line's \\n rect as array") ||
+ !checkRectArray(firstLineLFAsArray, [firstLineLF], "runBug722639Test: 1st line's rect as array should match with text rect result")) {
+ return;
+ }
+ }
+ let secondLine = synthesizeQueryTextRect(kLFLen, 1);
+ if (!checkQueryContentResult(secondLine,
+ "runBug722639Test: secondLine")) {
+ return;
+ }
+ ok(true, "runBug722639Test: 2nd line, top=" + secondLine.top + ", left=" + secondLine.left);
+ let secondLineAsArray = synthesizeQueryTextRectArray(kLFLen, 1);
+ if (!checkQueryContentResult(secondLineAsArray, "runBug722639Test: 2nd line as array") ||
+ !checkRectArray(secondLineAsArray, [secondLine], "runBug722639Test: 2nd line as array should match with text rect result")) {
+ return;
+ }
+ if (kLFLen > 1) {
+ let secondLineLF = synthesizeQueryTextRect(kLFLen + 1, 1);
+ if (!checkQueryContentResult(secondLineLF,
+ "runBug722639Test: secondLineLF")) {
+ return;
+ }
+ is(secondLineLF.top, secondLine.top, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect");
+ is(secondLineLF.left, secondLine.left, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect");
+ isfuzzy(secondLineLF.height, secondLine.height, 1,
+ "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect");
+ is(secondLineLF.width, secondLine.width, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect");
+ let secondLineLFAsArray = synthesizeQueryTextRectArray(kLFLen + 1, 1);
+ if (!checkQueryContentResult(secondLineLFAsArray, "runBug722639Test: 2nd line's \\n rect as array") ||
+ !checkRectArray(secondLineLFAsArray, [secondLineLF], "runBug722639Test: 2nd line's rect as array should match with text rect result")) {
+ return;
+ }
+ }
+ let lineHeight = secondLine.top - firstLine.top;
+ ok(lineHeight > 0,
+ "runBug722639Test: lineHeight must be positive");
+ is(secondLine.left, firstLine.left,
+ "runBug722639Test: the left value must be always same value");
+ isfuzzy(secondLine.height, firstLine.height, 1,
+ "runBug722639Test: the height must be always same value");
+ let previousTop = secondLine.top;
+ for (let i = 3; i <= textarea.value.length + 1; i++) {
+ let currentLine = synthesizeQueryTextRect(kLFLen * (i - 1), 1);
+ if (!checkQueryContentResult(currentLine,
+ "runBug722639Test: " + i + "th currentLine")) {
+ return;
+ }
+ ok(true, "runBug722639Test: " + i + "th line, top=" + currentLine.top + ", left=" + currentLine.left);
+ let currentLineAsArray = synthesizeQueryTextRectArray(kLFLen * (i - 1), 1);
+ if (!checkQueryContentResult(currentLineAsArray, "runBug722639Test: " + i + "th line as array") ||
+ !checkRectArray(currentLineAsArray, [currentLine], "runBug722639Test: " + i + "th line as array should match with text rect result")) {
+ return;
+ }
+ // NOTE: the top position may be 1px larger or smaller than other lines
+ // due to sub pixel positioning.
+ if (Math.abs(currentLine.top - (previousTop + lineHeight)) <= 1) {
+ ok(true, "runBug722639Test: " + i + "th line's top is expected");
+ } else {
+ is(currentLine.top, previousTop + lineHeight,
+ "runBug722639Test: " + i + "th line's top is unexpected");
+ }
+ is(currentLine.left, firstLine.left,
+ "runBug722639Test: " + i + "th line's left is unexpected");
+ isfuzzy(currentLine.height, firstLine.height, 1,
+ `runBug722639Test: ${i}th line's height is unexpected`);
+ if (kLFLen > 1) {
+ let currentLineLF = synthesizeQueryTextRect(kLFLen * (i - 1) + 1, 1);
+ if (!checkQueryContentResult(currentLineLF,
+ "runBug722639Test: " + i + "th currentLineLF")) {
+ return;
+ }
+ is(currentLineLF.top, currentLine.top, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect");
+ is(currentLineLF.left, currentLine.left, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect");
+ isfuzzy(currentLineLF.height, currentLine.height, 1,
+ `runBug722639Test: ${i}th line's \\n rect should be same as same line's \\r rect`);
+ is(currentLineLF.width, currentLine.width, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect");
+ let currentLineLFAsArray = synthesizeQueryTextRectArray(kLFLen * (i - 1) + 1, 1);
+ if (!checkQueryContentResult(currentLineLFAsArray, "runBug722639Test: " + i + "th line's \\n rect as array") ||
+ !checkRectArray(currentLineLFAsArray, [currentLineLF], "runBug722639Test: " + i + "th line's rect as array should match with text rect result")) {
+ return;
+ }
+ }
+ previousTop = currentLine.top;
+ }
+}
+
+function runCompositionWithSelectionChange() {
+ function doTest(aEditor, aDescription) {
+ aEditor.focus();
+ const isHTMLEditor =
+ aEditor.nodeName.toLowerCase() != "input" && aEditor.nodeName.toLowerCase() != "textarea";
+ const win = isHTMLEditor ? windowOfContenteditable : window;
+ function getValue() {
+ return isHTMLEditor ? aEditor.innerHTML : aEditor.value;
+ }
+ function setSelection(aStart, aLength) {
+ if (isHTMLEditor) {
+ win.getSelection().setBaseAndExtent(aEditor.firstChild, aStart, aEditor.firstChild, aStart + aLength);
+ } else {
+ aEditor.setSelectionRange(aStart, aStart + aLength);
+ }
+ }
+
+ if (isHTMLEditor) {
+ aEditor.innerHTML = "abcxyz";
+ } else {
+ aEditor.value = "abcxyz";
+ }
+ setSelection("abc".length, 0);
+
+ synthesizeCompositionChange({
+ composition: {
+ string: "1",
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ caret: { start: 1, length: 0 },
+ }
+ });
+
+ is(getValue(), "abc1xyz",
+ `${aDescription}: First composing character should be inserted middle of the text`);
+
+ aEditor.addEventListener("compositionupdate", () => {
+ setSelection("abc".length, "1".length);
+ }, {once: true});
+
+ synthesizeCompositionChange({
+ composition: {
+ string: "12",
+ clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ caret: { start: 2, length: 0 },
+ }
+ });
+
+ is(getValue(), "abc12xyz",
+ `${aDescription}: Only composition string should be updated even if selection range is updated by "compositionupdate" event listener`);
+
+ aEditor.addEventListener("compositionupdate", () => {
+ setSelection("abc1".length, "2d".length);
+ }, {once: true});
+
+ synthesizeCompositionChange({
+ composition: {
+ string: "123",
+ clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ caret: { start: 3, length: 0 },
+ }
+ });
+
+ is(getValue(), "abc123xyz",
+ `${aDescription}: Only composition string should be updated even if selection range wider than composition string is updated by "compositionupdate" event listener`);
+
+ aEditor.addEventListener("compositionupdate", () => {
+ setSelection("ab".length, "c123d".length);
+ }, {once: true});
+
+ synthesizeCompositionChange({
+ composition: {
+ string: "456",
+ clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ caret: { start: 3, length: 0 },
+ }
+ });
+
+ is(getValue(), "abc456xyz",
+ `${aDescription}: Only composition string should be updated even if selection range which covers all over the composition string is updated by "compositionupdate" event listener`);
+
+ aEditor.addEventListener("beforeinput", () => {
+ setSelection("abc456d".length, 0);
+ }, {once: true});
+
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ is(getValue(), "abc456xyz",
+ `${aDescription}: Only composition string should be updated when committing composition but selection is updated by "beforeinput" event listener`);
+ if (isHTMLEditor) {
+ is(win.getSelection().focusNode, aEditor.firstChild,
+ `${aDescription}: The focus node after composition should be the text node`);
+ is(win.getSelection().focusOffset, "abc456".length,
+ `${aDescription}: The focus offset after composition should be end of the composition string`);
+ is(win.getSelection().anchorNode, aEditor.firstChild,
+ `${aDescription}: The anchor node after composition should be the text node`);
+ is(win.getSelection().anchorOffset, "abc456".length,
+ `${aDescription}: The anchor offset after composition should be end of the composition string`);
+ } else {
+ is(aEditor.selectionStart, "abc456".length,
+ `${aDescription}: The selectionStart after composition should be end of the composition string`);
+ is(aEditor.selectionEnd, "abc456".length,
+ `${aDescription}: The selectionEnd after composition should be end of the composition string`);
+ }
+ }
+ doTest(textarea, "runCompositionWithSelectionChange(textarea)");
+ doTest(input, "runCompositionWithSelectionChange(input)");
+ doTest(contenteditable, "runCompositionWithSelectionChange(contenteditable)");
+}
+
+function runForceCommitTest()
+{
+ let events;
+ function eventHandler(aEvent)
+ {
+ events.push(aEvent);
+ }
+ window.addEventListener("compositionstart", eventHandler, true);
+ window.addEventListener("compositionupdate", eventHandler, true);
+ window.addEventListener("compositionend", eventHandler, true);
+ window.addEventListener("beforeinput", eventHandler, true);
+ window.addEventListener("input", eventHandler, true);
+ window.addEventListener("text", eventHandler, true);
+
+ // Make the composition in textarea commit by click in the textarea
+ textarea.focus();
+ textarea.value = "";
+
+ events = [];
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ is(events.length, 5,
+ "runForceCommitTest: wrong event count #1");
+ is(events[0].type, "compositionstart",
+ "runForceCommitTest: the 1st event must be compositionstart #1");
+ is(events[1].type, "compositionupdate",
+ "runForceCommitTest: the 2nd event must be compositionupdate #1");
+ is(events[2].type, "text",
+ "runForceCommitTest: the 3rd event must be text #1");
+ is(events[3].type, "beforeinput",
+ "runForceCommitTest: the 4th event must be beforeinput #1");
+ checkInputEvent(events[3], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #1");
+ is(events[4].type, "input",
+ "runForceCommitTest: the 5th event must be input #1");
+ checkInputEvent(events[4], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #1");
+
+ events = [];
+ synthesizeMouseAtCenter(textarea, {});
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #2");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #2");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #2`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #2");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #2`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #2");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #2");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #2`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #2");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #2");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #2`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #2");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #2");
+ is(textarea.value, "\u306E",
+ "runForceCommitTest: the textarea doesn't have the committed text #2");
+
+ // Make the composition in textarea commit by click in another editor (input)
+ textarea.focus();
+ textarea.value = "";
+ input.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ synthesizeMouseAtCenter(input, {});
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #3");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #3");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #3`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #3");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #3`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #3");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #3");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #3`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #3");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #3");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #3`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #3");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #3");
+ ok(!getEditor(input).isComposing,
+ "runForceCommitTest: the input has composition #3");
+ is(textarea.value, "\u306E",
+ "runForceCommitTest: the textarea doesn't have the committed text #3");
+ is(input.value, "",
+ "runForceCommitTest: the input has the committed text? #3");
+
+ // Make the composition in textarea commit by blur()
+ textarea.focus();
+ textarea.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ textarea.blur();
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #4");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #4");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #4`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #4");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #4`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #4");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #4");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #4`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #4");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #4");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #4`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #4");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #4");
+ is(textarea.value, "\u306E",
+ "runForceCommitTest: the textarea doesn't have the committed text #4");
+
+ // Make the composition in textarea commit by input.focus()
+ textarea.focus();
+ textarea.value = "";
+ input.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ input.focus();
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #5");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #5");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #5`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #5");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #5`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #5");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #5");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #5`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #5");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #5");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #5`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #5");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #5");
+ ok(!getEditor(input).isComposing,
+ "runForceCommitTest: the input has composition #5");
+ is(textarea.value, "\u306E",
+ "runForceCommitTest: the textarea doesn't have the committed text #5");
+ is(input.value, "",
+ "runForceCommitTest: the input has the committed text? #5");
+
+ // Make the composition in textarea commit by click in another document's editor
+ textarea.focus();
+ textarea.value = "";
+ textareaInFrame.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ synthesizeMouseAtCenter(textareaInFrame, {}, iframe.contentWindow);
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #6");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #6");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #6`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #6");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #6`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #6");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #6");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #6`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #6");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #6");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #6`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #6");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #6");
+ ok(!getEditor(textareaInFrame).isComposing,
+ "runForceCommitTest: the textarea in frame has composition #6");
+ is(textarea.value, "\u306E",
+ "runForceCommitTest: the textarea doesn't have the committed text #6");
+ is(textareaInFrame.value, "",
+ "runForceCommitTest: the textarea in frame has the committed text? #6");
+
+ // Make the composition in textarea commit by another document's editor's focus()
+ textarea.focus();
+ textarea.value = "";
+ textareaInFrame.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ textareaInFrame.focus();
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #7");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #7");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #7`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #7");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #7`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #7");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #7");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #7`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #7");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #7");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #7`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #7");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #7");
+ ok(!getEditor(textareaInFrame).isComposing,
+ "runForceCommitTest: the textarea in frame has composition #7");
+ is(textarea.value, "\u306E",
+ "runForceCommitTest: the textarea doesn't have the committed text #7");
+ is(textareaInFrame.value, "",
+ "runForceCommitTest: the textarea in frame has the committed text? #7");
+
+ // Make the composition in a textarea commit by click in another editable document
+ textarea.focus();
+ textarea.value = "";
+ iframe2.contentDocument.body.innerHTML = "Text in the Body";
+ let iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML;
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ synthesizeMouseAtCenter(iframe2.contentDocument.body, {}, iframe2.contentWindow);
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #8");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #8");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The ${events[0].type} event was fired on wrong event target #8`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #8");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The ${events[1].type} event was fired on wrong event target #8`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #8");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #8");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The ${events[2].type} event was fired on wrong event target #8`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #8");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #8");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The ${events[3].type} event was fired on wrong event target #8`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #8");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #8");
+ ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing,
+ "runForceCommitTest: the editable document has composition #8");
+ is(textarea.value, "\u306E",
+ "runForceCommitTest: the textarea doesn't have the committed text #8");
+ is(iframe2.contentDocument.body.innerHTML, iframe2BodyInnerHTML,
+ "runForceCommitTest: the editable document has the committed text? #8");
+
+ // Make the composition in an editable document commit by click in it
+ iframe2.contentWindow.focus();
+ iframe2.contentDocument.body.innerHTML = "Text in the Body";
+ iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML;
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ }, iframe2.contentWindow);
+
+ events = [];
+ synthesizeMouseAtCenter(iframe2.contentDocument.body, {}, iframe2.contentWindow);
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #9");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #9");
+ is(events[0].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #9`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #9");
+ is(events[1].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #9`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E",
+ [{startContainer: iframe2.contentDocument.body.firstChild,
+ startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"),
+ endContainer: iframe2.contentDocument.body.firstChild,
+ endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}],
+ "runForceCommitTest #9");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #9");
+ is(events[2].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #9`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #9");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #9");
+ is(events[3].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #9`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #9");
+ ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing,
+ "runForceCommitTest: the editable document still has composition #9");
+ ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML &&
+ iframe2.contentDocument.body.innerHTML.includes("\u306E"),
+ "runForceCommitTest: the editable document doesn't have the committed text #9");
+
+ // Make the composition in an editable document commit by click in another document's editor
+ textarea.value = "";
+ iframe2.contentWindow.focus();
+ iframe2.contentDocument.body.innerHTML = "Text in the Body";
+ iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML;
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ }, iframe2.contentWindow);
+
+ events = [];
+ synthesizeMouseAtCenter(textarea, {});
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #10");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #10");
+ is(events[0].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The ${events[0].type} event was fired on wrong event target #10`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #10");
+ is(events[1].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The ${events[1].type} event was fired on wrong event target #10`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E",
+ [{startContainer: iframe2.contentDocument.body.firstChild,
+ startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"),
+ endContainer: iframe2.contentDocument.body.firstChild,
+ endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}],
+ "runForceCommitTest #10");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #10");
+ is(events[2].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The ${events[2].type} event was fired on wrong event target #10`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #10");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #10");
+ is(events[3].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The ${events[3].type} event was fired on wrong event target #10`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #10");
+ ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing,
+ "runForceCommitTest: the editable document still has composition #10");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea has composition #10");
+ ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML &&
+ iframe2.contentDocument.body.innerHTML.includes("\u306E"),
+ "runForceCommitTest: the editable document doesn't have the committed text #10");
+ is(textarea.value, "",
+ "runForceCommitTest: the textarea has the committed text? #10");
+
+ // Make the composition in an editable document commit by click in the another editable document
+ iframe2.contentWindow.focus();
+ iframe2.contentDocument.body.innerHTML = "Text in the Body";
+ iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML;
+ iframe3.contentDocument.body.innerHTML = "Text in the Body";
+ let iframe3BodyInnerHTML = iframe2.contentDocument.body.innerHTML;
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ }, iframe2.contentWindow);
+
+ events = [];
+ synthesizeMouseAtCenter(iframe3.contentDocument.body, {}, iframe3.contentWindow);
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #11");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #11");
+ is(events[0].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #11`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #11");
+ is(events[1].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #11`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E",
+ [{startContainer: iframe2.contentDocument.body.firstChild,
+ startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"),
+ endContainer: iframe2.contentDocument.body.firstChild,
+ endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}],
+ "runForceCommitTest #11");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #11");
+ is(events[2].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #11`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #11");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #11");
+ is(events[3].target, iframe2.contentDocument.body,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #11`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #11");
+ ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing,
+ "runForceCommitTest: the editable document still has composition #11");
+ ok(!getHTMLEditorIMESupport(iframe3.contentWindow).isComposing,
+ "runForceCommitTest: the other editable document has composition #11");
+ ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML &&
+ iframe2.contentDocument.body.innerHTML.includes("\u306E"),
+ "runForceCommitTest: the editable document doesn't have the committed text #11");
+ is(iframe3.contentDocument.body.innerHTML, iframe3BodyInnerHTML,
+ "runForceCommitTest: the other editable document has the committed text? #11");
+
+ input.focus();
+ input.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ input.value = "set value";
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #12");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #12");
+ is(events[0].target, input,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #12`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #12");
+ is(events[1].target, input,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #12`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #12");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #12");
+ is(events[2].target, input,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #12`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #12");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #12");
+ is(events[3].target, input,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #12`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #12");
+ ok(!getEditor(input).isComposing,
+ "runForceCommitTest: the input still has composition #12");
+ is(input.value, "set value",
+ "runForceCommitTest: the input doesn't have the set text #12");
+
+ textarea.focus();
+ textarea.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ textarea.value = "set value";
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #13");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #13");
+ is(events[0].target, textarea,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #13`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #13");
+ is(events[1].target, textarea,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #13`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #13");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #13");
+ is(events[2].target, textarea,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #13`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #13");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #13");
+ is(events[3].target, textarea,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #13`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #13");
+ ok(!getEditor(textarea).isComposing,
+ "runForceCommitTest: the textarea still has composition #13");
+ is(textarea.value, "set value",
+ "runForceCommitTest: the textarea doesn't have the set text #13");
+
+ input.focus();
+ input.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ input.value += " appended value";
+
+ is(events.length, 4,
+ "runForceCommitTest: wrong event count #14");
+ is(events[0].type, "text",
+ "runForceCommitTest: the 1st event must be text #14");
+ is(events[0].target, input,
+ `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #14`);
+ is(events[1].type, "beforeinput",
+ "runForceCommitTest: the 2nd event must be beforeinput #14");
+ is(events[1].target, input,
+ `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #14`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #14");
+ is(events[2].type, "compositionend",
+ "runForceCommitTest: the 3rd event must be compositionend #14");
+ is(events[2].target, input,
+ `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #14`);
+ is(events[2].data, "\u306E",
+ "runForceCommitTest: compositionend has wrong data #14");
+ is(events[3].type, "input",
+ "runForceCommitTest: the 4th event must be input #14");
+ is(events[3].target, input,
+ `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #14`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runForceCommitTest #14");
+ ok(!getEditor(input).isComposing,
+ "runForceCommitTest: the input still has composition #14");
+ is(input.value, "\u306E appended value",
+ "runForceCommitTest: the input should have both composed text and appended text #14");
+
+ input.focus();
+ input.value = "abcd";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ input.value = "abcd\u306E";
+
+ is(events.length, 0,
+ "runForceCommitTest: setting same value to input with composition shouldn't cause any events #15");
+ is(input.value, "abcd\u306E",
+ "runForceCommitTest: the input has unexpected value #15");
+
+ input.blur(); // commit composition
+
+ textarea.focus();
+ textarea.value = "abcd";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ textarea.value = "abcd\u306E";
+
+ is(events.length, 0,
+ "runForceCommitTest: setting same value to textarea with composition shouldn't cause any events #16");
+ is(textarea.value, "abcd\u306E",
+ "runForceCommitTest: the input has unexpected value #16");
+
+ textarea.blur(); // commit composition
+
+ window.removeEventListener("compositionstart", eventHandler, true);
+ window.removeEventListener("compositionupdate", eventHandler, true);
+ window.removeEventListener("compositionend", eventHandler, true);
+ window.removeEventListener("beforeinput", eventHandler, true);
+ window.removeEventListener("input", eventHandler, true);
+ window.removeEventListener("text", eventHandler, true);
+}
+
+function runNestedSettingValue()
+{
+ let isTesting = false;
+ let events = [];
+ function eventHandler(aEvent)
+ {
+ events.push(aEvent);
+ if (isTesting) {
+ aEvent.target.value += aEvent.type + ", ";
+ }
+ }
+ window.addEventListener("compositionstart", eventHandler, true);
+ window.addEventListener("compositionupdate", eventHandler, true);
+ window.addEventListener("compositionend", eventHandler, true);
+ window.addEventListener("beforeinput", eventHandler, true);
+ window.addEventListener("input", eventHandler, true);
+ window.addEventListener("text", eventHandler, true);
+
+ textarea.focus();
+ textarea.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ isTesting = true;
+ textarea.value = "first setting value, ";
+ isTesting = false;
+
+ is(events.length, 4,
+ "runNestedSettingValue: wrong event count #1");
+ is(events[0].type, "text",
+ "runNestedSettingValue: the 1st event must be text #1");
+ is(events[0].target, textarea,
+ `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #1`);
+ is(events[1].type, "beforeinput",
+ "runNestedSettingValue: the 2nd event must be beforeinput #1");
+ is(events[1].target, textarea,
+ `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #1`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #1");
+ is(events[2].type, "compositionend",
+ "runNestedSettingValue: the 3rd event must be compositionend #1");
+ is(events[2].target, textarea,
+ `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #1`);
+ is(events[2].data, "\u306E",
+ "runNestedSettingValue: compositionend has wrong data #1");
+ is(events[3].type, "input",
+ "runNestedSettingValue: the 4th event must be input #1");
+ is(events[3].target, textarea,
+ `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #1`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #1");
+ ok(!getEditor(textarea).isComposing,
+ "runNestedSettingValue: the textarea still has composition #1");
+ is(textarea.value, "first setting value, text, beforeinput, compositionend, input, ",
+ "runNestedSettingValue: the textarea should have all string set to value attribute");
+
+ input.focus();
+ input.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ isTesting = true;
+ input.value = "first setting value, ";
+ isTesting = false;
+
+ is(events.length, 4,
+ "runNestedSettingValue: wrong event count #2");
+ is(events[0].type, "text",
+ "runNestedSettingValue: the 1st event must be text #2");
+ is(events[0].target, input,
+ `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #2`);
+ is(events[1].type, "beforeinput",
+ "runNestedSettingValue: the 2nd event must be beforeinput #2");
+ is(events[1].target, input,
+ `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #2`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #2");
+ is(events[2].type, "compositionend",
+ "runNestedSettingValue: the 3rd event must be compositionend #2");
+ is(events[2].target, input,
+ `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #2`);
+ is(events[2].data, "\u306E",
+ "runNestedSettingValue: compositionend has wrong data #2");
+ is(events[3].type, "input",
+ "runNestedSettingValue: the 4th event must be input #2");
+ is(events[3].target, input,
+ `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #2`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #2");
+ ok(!getEditor(input).isComposing,
+ "runNestedSettingValue: the input still has composition #2");
+ is(textarea.value, "first setting value, text, beforeinput, compositionend, input, ",
+ "runNestedSettingValue: the input should have all string set to value attribute #2");
+
+ textarea.focus();
+ textarea.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ isTesting = true;
+ textarea.setRangeText("first setting value, ");
+ isTesting = false;
+
+ is(events.length, 4,
+ "runNestedSettingValue: wrong event count #3");
+ is(events[0].type, "text",
+ "runNestedSettingValue: the 1st event must be text #3");
+ is(events[0].target, textarea,
+ `runNestedSettingValue: The ${events[0].type} event was fired on wrong event target #3`);
+ is(events[1].type, "beforeinput",
+ "runNestedSettingValue: the 2nd event must be beforeinput #3");
+ is(events[1].target, textarea,
+ `runNestedSettingValue: The ${events[1].type} event was fired on wrong event target #3`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #3");
+ is(events[2].type, "compositionend",
+ "runNestedSettingValue: the 3rd event must be compositionend #3");
+ is(events[2].target, textarea,
+ `runNestedSettingValue: The ${events[2].type} event was fired on wrong event target #3`);
+ is(events[2].data, "\u306E",
+ "runNestedSettingValue: compositionend has wrong data #3");
+ is(events[3].type, "input",
+ "runNestedSettingValue: the 4th event must be input #3");
+ is(events[3].target, textarea,
+ `runNestedSettingValue: The ${events[3].type} event was fired on wrong event target #3`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #3");
+ ok(!getEditor(textarea).isComposing,
+ "runNestedSettingValue: the textarea still has composition #3");
+ is(textarea.value, "\u306Efirst setting value, text, beforeinput, compositionend, input, ",
+ "runNestedSettingValue: the textarea should have appended by setRangeText() and all string set to value attribute #3");
+
+ input.focus();
+ input.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ events = [];
+ isTesting = true;
+ input.setRangeText("first setting value, ");
+ isTesting = false;
+
+ is(events.length, 4,
+ "runNestedSettingValue: wrong event count #4");
+ is(events[0].type, "text",
+ "runNestedSettingValue: the 1st event must be text #4");
+ is(events[0].target, input,
+ `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #4`);
+ is(events[1].type, "beforeinput",
+ "runNestedSettingValue: the 2nd event must be beforeinput #4");
+ is(events[1].target, input,
+ `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #4`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #4");
+ is(events[2].type, "compositionend",
+ "runNestedSettingValue: the 3rd event must be compositionend #4");
+ is(events[2].target, input,
+ `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #4`);
+ is(events[2].data, "\u306E",
+ "runNestedSettingValue: compositionend has wrong data #4");
+ is(events[3].type, "input",
+ "runNestedSettingValue: the 4th event must be input #4");
+ is(events[3].target, input,
+ `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #4`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runNestedSettingValue #4");
+ ok(!getEditor(input).isComposing,
+ "runNestedSettingValue: the input still has composition #4");
+ is(textarea.value, "\u306Efirst setting value, text, beforeinput, compositionend, input, ",
+ "runNestedSettingValue: the input should have all string appended by setRangeText() and set to value attribute #4");
+
+ window.removeEventListener("compositionstart", eventHandler, true);
+ window.removeEventListener("compositionupdate", eventHandler, true);
+ window.removeEventListener("compositionend", eventHandler, true);
+ window.removeEventListener("beforeinput", eventHandler, true);
+ window.removeEventListener("input", eventHandler, true);
+ window.removeEventListener("text", eventHandler, true);
+
+}
+
+async function runAsyncForceCommitTest()
+{
+ let events;
+ function eventHandler(aEvent)
+ {
+ events.push(aEvent);
+ };
+
+ // If IME commits composition for a request, TextComposition commits
+ // composition automatically because most web apps must expect that active
+ // composition should be committed synchronously. Therefore, in this case,
+ // a click during composition should cause committing composition
+ // synchronously and delayed commit shouldn't cause composition events.
+ let commitRequested = false;
+ let onFinishTest = null;
+ function callback(aTIP, aNotification)
+ {
+ ok(true, aNotification.type);
+ if (aNotification.type != "request-to-commit") {
+ return true;
+ }
+ commitRequested = true;
+ if (onFinishTest) {
+ let resolve = onFinishTest;
+ onFinishTest = null;
+
+ SimpleTest.executeSoon(() => {
+ events = [];
+ aTIP.commitComposition();
+
+ is(events.length, 0,
+ "runAsyncForceCommitTest: composition events shouldn't been fired by asynchronous call of nsITextInputProcessor.commitComposition()");
+
+ SimpleTest.executeSoon(resolve);
+ });
+ }
+ return true;
+ };
+
+ function promiseCleanUp() {
+ return new Promise(resolve => { onFinishTest = resolve; });
+ }
+
+ window.addEventListener("compositionstart", eventHandler, true);
+ window.addEventListener("compositionupdate", eventHandler, true);
+ window.addEventListener("compositionend", eventHandler, true);
+ window.addEventListener("beforeinput", eventHandler, true);
+ window.addEventListener("input", eventHandler, true);
+ window.addEventListener("text", eventHandler, true);
+
+ // Make the composition in textarea commit by click in the textarea
+ textarea.focus();
+ textarea.value = "";
+
+ events = [];
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ }, window, callback);
+
+ is(events.length, 5,
+ "runAsyncForceCommitTest: wrong event count #1");
+ is(events[0].type, "compositionstart",
+ "runAsyncForceCommitTest: the 1st event must be compositionstart #1");
+ is(events[1].type, "compositionupdate",
+ "runAsyncForceCommitTest: the 2nd event must be compositionupdate #1");
+ is(events[2].type, "text",
+ "runAsyncForceCommitTest: the 3rd event must be text #1");
+ is(events[3].type, "beforeinput",
+ "runAsyncForceCommitTest: the 4th event must be beforeinput #1");
+ checkInputEvent(events[3], true, "insertCompositionText", "\u306E", [],
+ "runAsyncForceCommitTest #1");
+ is(events[4].type, "input",
+ "runAsyncForceCommitTest: the 5th event must be input #1");
+ checkInputEvent(events[4], true, "insertCompositionText", "\u306E", [],
+ "runAsyncForceCommitTest #1");
+
+ events = [];
+ let waitCleanState = promiseCleanUp();
+
+ synthesizeMouseAtCenter(textarea, {});
+
+ ok(commitRequested,
+ "runAsyncForceCommitTest: \"request-to-commit\" should've been notified");
+ is(events.length, 4,
+ "runAsyncForceCommitTest: wrong event count #2");
+ is(events[0].type, "text",
+ "runAsyncForceCommitTest: the 1st event must be text #2");
+ is(events[0].target, textarea,
+ `runAsyncForceCommitTest: The "${events[0].type}" event was fired on wrong event target #2`);
+ is(events[1].type, "beforeinput",
+ "runAsyncForceCommitTest: the 2nd event must be beforeinput #2");
+ is(events[1].target, textarea,
+ `runAsyncForceCommitTest: The "${events[1].type}" event was fired on wrong event target #2`);
+ checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [],
+ "runAsyncForceCommitTest #2");
+ is(events[2].type, "compositionend",
+ "runAsyncForceCommitTest: the 3rd event must be compositionend #2");
+ is(events[2].target, textarea,
+ `runAsyncForceCommitTest: The "${events[2].type}" event was fired on wrong event target #2`);
+ is(events[2].data, "\u306E",
+ "runAsyncForceCommitTest: compositionend has wrong data #2");
+ is(events[3].type, "input",
+ "runAsyncForceCommitTest: the 4th event must be input #2");
+ is(events[3].target, textarea,
+ `runAsyncForceCommitTest: The "${events[3].type}" event was fired on wrong event target #2`);
+ checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [],
+ "runAsyncForceCommitTest #2");
+ ok(!getEditor(textarea).isComposing,
+ "runAsyncForceCommitTest: the textarea still has composition #2");
+ is(textarea.value, "\u306E",
+ "runAsyncForceCommitTest: the textarea doesn't have the committed text #2");
+
+ await waitCleanState;
+
+ window.removeEventListener("compositionstart", eventHandler, true);
+ window.removeEventListener("compositionupdate", eventHandler, true);
+ window.removeEventListener("compositionend", eventHandler, true);
+ window.removeEventListener("beforeinput", eventHandler, true);
+ window.removeEventListener("input", eventHandler, true);
+ window.removeEventListener("text", eventHandler, true);
+}
+
+function runBug811755Test()
+{
+ iframe2.contentDocument.body.innerHTML = "<div>content<br/></div>";
+ iframe2.contentWindow.focus();
+ // Query everything
+ let textContent = synthesizeQueryTextContent(0, 10);
+ if (!checkQueryContentResult(textContent, "runBug811755Test: synthesizeQueryTextContent #1")) {
+ return;
+ }
+ // Query everything but specify exact end offset, which should be immediately after the <br> node
+ // If PreContentIterator is used, the next node after <br> is the node after </div>.
+ // If ContentIterator is used, the next node is the <div> node itself. In this case, the end
+ // node ends up being before the start node, and an empty string is returned.
+ let queryContent = synthesizeQueryTextContent(0, textContent.text.length);
+ if (!checkQueryContentResult(queryContent, "runBug811755Test: synthesizeQueryTextContent #2")) {
+ return;
+ }
+ is(queryContent.text, textContent.text, "runBug811755Test: two queried texts don't match");
+}
+
+function runIsComposingTest()
+{
+ let expectedIsComposing = false;
+ let description = "";
+
+ function eventHandler(aEvent)
+ {
+ if (aEvent.type == "keydown" || aEvent.type == "keyup") {
+ is(aEvent.isComposing, expectedIsComposing,
+ "runIsComposingTest: " + description + " (type=" + aEvent.type + ", key=" + aEvent.key + ")");
+ } else if (aEvent.type == "keypress") {
+ // keypress event shouldn't be fired during composition so that isComposing should be always false.
+ is(aEvent.isComposing, false,
+ "runIsComposingTest: " + description + " (type=" + aEvent.type + ")");
+ } else {
+ checkInputEvent(aEvent, expectedIsComposing, "insertCompositionText", "\u3042", [],
+ `runIsComposingTest: ${description}`);
+ }
+ }
+
+ function onComposition(aEvent)
+ {
+ if (aEvent.type == "compositionstart") {
+ expectedIsComposing = true;
+ } else if (aEvent.type == "compositionend") {
+ expectedIsComposing = false;
+ }
+ }
+
+ textarea.addEventListener("keydown", eventHandler, true);
+ textarea.addEventListener("keypress", eventHandler, true);
+ textarea.addEventListener("keyup", eventHandler, true);
+ textarea.addEventListener("beforeinput", eventHandler, true);
+ textarea.addEventListener("input", eventHandler, true);
+ textarea.addEventListener("compositionstart", onComposition, true);
+ textarea.addEventListener("compositionend", onComposition, true);
+
+ textarea.focus();
+ textarea.value = "";
+
+ // XXX These cases shouldn't occur in actual native key events because we
+ // don't dispatch key events while composition (bug 354358).
+ description = "events before dispatching compositionstart";
+ synthesizeKey("KEY_ArrowLeft");
+
+ description = "events after dispatching compositionchange";
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 },
+ "key": { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A },
+ });
+
+ // Although, firing keypress event during composition is a bug.
+ synthesizeKey("KEY_Insert");
+
+ description = "events for committing composition string";
+
+ synthesizeComposition({ type: "compositioncommitasis",
+ key: { key: "KEY_Enter", code: "Enter", type: "keydown" } });
+
+ // input event will be fired by synthesizing compositionend event.
+ // Then, its isComposing should be false.
+ description = "events after dispatching compositioncommitasis";
+ synthesizeKey("KEY_Enter", {type: "keyup"});
+
+ textarea.removeEventListener("keydown", eventHandler, true);
+ textarea.removeEventListener("keypress", eventHandler, true);
+ textarea.removeEventListener("keyup", eventHandler, true);
+ textarea.removeEventListener("beforeinput", eventHandler, true);
+ textarea.removeEventListener("input", eventHandler, true);
+ textarea.removeEventListener("compositionstart", onComposition, true);
+ textarea.removeEventListener("compositionend", onComposition, true);
+
+ textarea.value = "";
+}
+
+function runRedundantChangeTest()
+{
+ textarea.focus();
+
+ let result = [];
+ function clearResult()
+ {
+ result = [];
+ }
+
+ function handler(aEvent)
+ {
+ result.push(aEvent);
+ }
+
+ textarea.addEventListener("compositionupdate", handler, true);
+ textarea.addEventListener("compositionend", handler, true);
+ textarea.addEventListener("beforeinput", handler, true);
+ textarea.addEventListener("input", handler, true);
+ textarea.addEventListener("text", handler, true);
+
+ textarea.value = "";
+
+ // synthesize change event
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ is(result.length, 4,
+ "runRedundantChangeTest: 4 events should be fired after synthesizing composition change #1");
+ is(result[0].type, "compositionupdate",
+ "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #1");
+ is(result[1].type, "text",
+ "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #1");
+ is(result[2].type, "beforeinput",
+ "runRedundantChangeTest: beforeinput should be fired after synthesizing composition change #1");
+ checkInputEvent(result[2], true, "insertCompositionText", "\u3042", [],
+ "runRedundantChangeTest: after synthesizing composition change #1");
+ is(result[3].type, "input",
+ "runRedundantChangeTest: input should be fired after synthesizing composition change #1");
+ checkInputEvent(result[3], true, "insertCompositionText", "\u3042", [],
+ "runRedundantChangeTest: after synthesizing composition change #1");
+ is(textarea.value, "\u3042", "runRedundantChangeTest: textarea has uncommitted string #1");
+
+ // synthesize another change event
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042\u3044",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ is(result.length, 4,
+ "runRedundantChangeTest: 4 events should be fired after synthesizing composition change #2");
+ is(result[0].type, "compositionupdate",
+ "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #2");
+ is(result[1].type, "text",
+ "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #2");
+ is(result[2].type, "beforeinput",
+ "runRedundantChangeTest: beforeinput should be fired after synthesizing composition change #2");
+ checkInputEvent(result[2], true, "insertCompositionText", "\u3042\u3044", [],
+ "runRedundantChangeTest: after synthesizing composition change #2");
+ is(result[3].type, "input",
+ "runRedundantChangeTest: input should be fired after synthesizing composition change #2");
+ checkInputEvent(result[3], true, "insertCompositionText", "\u3042\u3044", [],
+ "runRedundantChangeTest: after synthesizing composition change #2");
+ is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #2");
+
+ // synthesize same change event again
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3042\u3044",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ is(result.length, 0, "runRedundantChangeTest: no events should be fired after synthesizing composition change again");
+ is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #3");
+
+ // synthesize commit-as-is
+ clearResult();
+ synthesizeComposition({ type: "compositioncommitasis" });
+ is(result.length, 4,
+ "runRedundantChangeTest: 4 events should be fired after synthesizing composition commit-as-is");
+ is(result[0].type, "text",
+ "runRedundantChangeTest: text should be fired after synthesizing composition commit-as-is for removing the ranges");
+ is(result[1].type, "beforeinput",
+ "runRedundantChangeTest: beforeinput should be fired after synthesizing composition commit-as-is for removing the ranges");
+ checkInputEvent(result[1], true, "insertCompositionText", "\u3042\u3044", [],
+ "runRedundantChangeTest: at synthesizing commit-as-is");
+ is(result[2].type, "compositionend",
+ "runRedundantChangeTest: compositionend should be fired after synthesizing composition commit-as-is");
+ is(result[3].type, "input",
+ "runRedundantChangeTest: input should be fired before compositionend at synthesizing commit-as-is");
+ checkInputEvent(result[3], false, "insertCompositionText", "\u3042\u3044", [],
+ "runRedundantChangeTest: at synthesizing commit-as-is");
+ is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has the commit string");
+
+ textarea.removeEventListener("compositionupdate", handler, true);
+ textarea.removeEventListener("compositionend", handler, true);
+ textarea.removeEventListener("beforeinput", handler, true);
+ textarea.removeEventListener("input", handler, true);
+ textarea.removeEventListener("text", handler, true);
+}
+
+function runNotRedundantChangeTest()
+{
+ textarea.focus();
+
+ let result = [];
+ function clearResult()
+ {
+ result = [];
+ }
+
+ function handler(aEvent)
+ {
+ result.push(aEvent);
+ }
+
+ textarea.addEventListener("compositionupdate", handler, true);
+ textarea.addEventListener("compositionend", handler, true);
+ textarea.addEventListener("beforeinput", handler, true);
+ textarea.addEventListener("input", handler, true);
+ textarea.addEventListener("text", handler, true);
+
+ textarea.value = "abcde";
+
+ // synthesize change event with non-null ranges
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABCDE",
+ "clauses":
+ [
+ { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 5, "length": 0 }
+ });
+
+ is(result.length, 4,
+ "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with non-null ranges");
+ is(result[0].type, "compositionupdate",
+ "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges");
+ is(result[1].type, "text",
+ "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges");
+ is(result[2].type, "beforeinput",
+ "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with non-null ranges");
+ checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges");
+ is(result[3].type, "input",
+ "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges");
+ checkInputEvent(result[3], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges");
+ is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #1");
+
+ // synthesize change event with null ranges
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABCDE",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ });
+ is(result.length, 3,
+ "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with null ranges after non-null ranges");
+ is(result[0].type, "text",
+ "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges");
+ is(result[1].type, "beforeinput",
+ "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges");
+ checkInputEvent(result[1], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with null ranges after non-null ranges");
+ is(result[2].type, "input",
+ "runNotRedundantChangeTest: input should be fired after synthesizing composition change with null ranges after non-null ranges");
+ checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with null ranges after non-null ranges");
+ is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #2");
+
+ // synthesize change event with non-null ranges
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABCDE",
+ "clauses":
+ [
+ { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 5, "length": 0 }
+ });
+
+ is(result.length, 3,
+ "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with null ranges after non-null ranges");
+ is(result[0].type, "text",
+ "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges");
+ is(result[1].type, "beforeinput",
+ "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges");
+ checkInputEvent(result[1], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after null ranges");
+ is(result[2].type, "input",
+ "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after null ranges");
+ checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after null ranges");
+ is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #3");
+
+ // synthesize change event with empty data and null ranges
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "",
+ "clauses":
+ [
+ { "length": 0, "attr": 0 }
+ ]
+ },
+ });
+ is(result.length, 4,
+ "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+ is(result[0].type, "compositionupdate",
+ "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+ is(result[1].type, "text",
+ "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data and null ranges after non-null ranges");
+ is(result[2].type, "beforeinput",
+ "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+ checkInputEvent(result[2], true, "insertCompositionText", "", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with empty data and null ranges after non-null ranges");
+ is(result[3].type, "input",
+ "runNotRedundantChangeTest: input should be fired after synthesizing composition change with empty data and null ranges after non-null ranges");
+ checkInputEvent(result[3], true, "insertCompositionText", "", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with empty data and null ranges after non-null ranges");
+ is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #1");
+
+ // synthesize change event with non-null ranges
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABCDE",
+ "clauses":
+ [
+ { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 5, "length": 0 }
+ });
+
+ is(result.length, 4,
+ "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+ is(result[0].type, "compositionupdate",
+ "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+ is(result[1].type, "text",
+ "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after empty data and null ranges");
+ is(result[2].type, "beforeinput",
+ "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+ checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after empty data and null ranges");
+ is(result[3].type, "input",
+ "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after empty data and null ranges");
+ checkInputEvent(result[3], true, "insertCompositionText", "ABCDE", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after empty data and null ranges");
+ is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #4");
+
+ clearResult();
+ synthesizeComposition({ type: "compositioncommit", data: "" });
+
+ is(result.length, 5,
+ "runNotRedundantChangeTest: 5 events should be fired after synthesizing composition commit with empty data after non-empty data");
+ is(result[0].type, "compositionupdate",
+ "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition commit with empty data after non-empty data");
+ is(result[1].type, "text",
+ "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data after non-empty data");
+ is(result[2].type, "beforeinput",
+ "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition commit with empty data after non-empty data");
+ checkInputEvent(result[2], true, "insertCompositionText", "", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with empty data after non-empty data");
+ is(result[3].type, "compositionend",
+ "runNotRedundantChangeTest: compositionend should be fired after synthesizing composition commit with empty data after non-empty data");
+ is(result[4].type, "input",
+ "runNotRedundantChangeTest: input should be fired after compositionend after synthesizing composition change with empty data after non-empty data");
+ checkInputEvent(result[4], false, "insertCompositionText", "", [],
+ "runNotRedundantChangeTest: after synthesizing composition change with empty data after non-empty data");
+ is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #2");
+
+ textarea.removeEventListener("compositionupdate", handler, true);
+ textarea.removeEventListener("compositionend", handler, true);
+ textarea.removeEventListener("beforeinput", handler, true);
+ textarea.removeEventListener("input", handler, true);
+ textarea.removeEventListener("text", handler, true);
+}
+
+function runNativeLineBreakerTest()
+{
+ textarea.focus();
+
+ let result = {};
+ function clearResult()
+ {
+ result = { compositionupdate: null, compositionend: null };
+ }
+
+ function handler(aEvent)
+ {
+ result[aEvent.type] = aEvent.data;
+ }
+
+ Services.prefs.setBoolPref("dom.compositionevent.allow_control_characters", false);
+
+ textarea.addEventListener("compositionupdate", handler, true);
+ textarea.addEventListener("compositionend", handler, true);
+
+ // '\n' in composition string shouldn't be changed.
+ clearResult();
+ textarea.value = "";
+ let clauses = [ "abc\n", "def\n\ng", "hi\n", "\njkl" ];
+ let caret = clauses[0] + clauses[1] + clauses[2];
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": clauses.join(''),
+ "clauses":
+ [
+ { "length": clauses[0].length,
+ "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ { "length": clauses[1].length,
+ "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE },
+ { "length": clauses[2].length,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": clauses[3].length,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ ]
+ },
+ "caret": { "start": caret.length, "length": 0 }
+ });
+
+ checkSelection(caret.replace(/\n/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#1");
+ checkIMESelection("RawClause", true, 0, clauses[0].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1");
+ checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\n/g, kLF).length, clauses[1].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1");
+ checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\n/g, kLF).length, clauses[2].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1");
+ checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\n/g, kLF).length, clauses[3].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1");
+ is(result.compositionupdate, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in compositionupdate.data shouldn't be removed nor replaced with other characters #1");
+ is(textarea.value, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in textarea.value shouldn't be removed nor replaced with other characters #1");
+
+ synthesizeComposition({ type: "compositioncommit", data: clauses.join('') });
+ checkSelection(clauses.join('').replace(/\n/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#2");
+ is(result.compositionend, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in compositionend.data shouldn't be removed nor replaced with other characters #2");
+ is(textarea.value, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in textarea.value shouldn't be removed nor replaced with other characters #2");
+
+ // \r\n in composition string should be replaced with \n.
+ clearResult();
+ textarea.value = "";
+ clauses = [ "abc\r\n", "def\r\n\r\ng", "hi\r\n", "\r\njkl" ];
+ caret = clauses[0] + clauses[1] + clauses[2];
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": clauses.join(''),
+ "clauses":
+ [
+ { "length": clauses[0].length,
+ "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ { "length": clauses[1].length,
+ "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE },
+ { "length": clauses[2].length,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": clauses[3].length,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ ]
+ },
+ "caret": { "start": caret.length, "length": 0 }
+ });
+
+ checkSelection(caret.replace(/\r\n/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#3");
+ checkIMESelection("RawClause", true, 0, clauses[0].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3");
+ checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\r\n/g, kLF).length, clauses[1].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3");
+ checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\r\n/g, kLF).length, clauses[2].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3");
+ checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\r\n/g, kLF).length, clauses[3].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3");
+ is(result.compositionupdate, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in compositionudpate.data should be replaced with \\n #3");
+ is(textarea.value, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in textarea.value should be replaced with \\n #3");
+
+ synthesizeComposition({ type: "compositioncommit", data: clauses.join('') });
+ checkSelection(clauses.join('').replace(/\r\n/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#4");
+ is(result.compositionend, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in compositionend.data should be replaced with \\n #4");
+ is(textarea.value, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in textarea.value should be replaced with \\n #4");
+
+ // \r (not followed by \n) in composition string should be replaced with \n.
+ clearResult();
+ textarea.value = "";
+ clauses = [ "abc\r", "def\r\rg", "hi\r", "\rjkl" ];
+ caret = clauses[0] + clauses[1] + clauses[2];
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": clauses.join(''),
+ "clauses":
+ [
+ { "length": clauses[0].length,
+ "attr": COMPOSITION_ATTR_RAW_CLAUSE },
+ { "length": clauses[1].length,
+ "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE },
+ { "length": clauses[2].length,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": clauses[3].length,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ ]
+ },
+ "caret": { "start": caret.length, "length": 0 }
+ });
+
+ checkSelection(caret.replace(/\r/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#5");
+ checkIMESelection("RawClause", true, 0, clauses[0].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5");
+ checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\r/g, kLF).length, clauses[1].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5");
+ checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\r/g, kLF).length, clauses[2].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5");
+ checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\r/g, kLF).length, clauses[3].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5");
+ is(result.compositionupdate, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in compositionupdate.data should be replaced with \\n #5");
+ is(textarea.value, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in textarea.value should be replaced with \\n #5");
+
+ synthesizeComposition({ type: "compositioncommit", data: clauses.join('') });
+ checkSelection(clauses.join('').replace(/\r/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#6");
+ is(result.compositionend, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in compositionend.data should be replaced with \\n #6");
+ is(textarea.value, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in textarea.value should be replaced with \\n #6");
+
+ textarea.removeEventListener("compositionupdate", handler, true);
+ textarea.removeEventListener("compositionend", handler, true);
+
+ Services.prefs.clearUserPref("dom.compositionevent.allow_control_characters");
+}
+
+function runControlCharTest()
+{
+ textarea.focus();
+
+ let result = {};
+ function clearResult()
+ {
+ result = { compositionupdate: null, compositionend: null };
+ }
+
+ function handler(aEvent)
+ {
+ result[aEvent.type] = aEvent.data;
+ }
+
+ textarea.addEventListener("compositionupdate", handler, true);
+ textarea.addEventListener("compositionend", handler, true);
+
+ textarea.value = "";
+
+ let controlChars = String.fromCharCode.apply(null, Object.keys(Array.from({length:0x20}))) + "\x7F";
+ let allowedChars = "\t\n\n";
+
+ let data = "AB" + controlChars + "CD" + controlChars + "EF";
+ let removedData = "AB" + allowedChars + "CD" + allowedChars + "EF";
+
+ let DIndex = data.indexOf("D");
+ let removedDIndex = removedData.indexOf("D");
+
+ // input string contains control characters
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": data,
+ "clauses":
+ [
+ { "length": DIndex,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": data.length - DIndex,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": DIndex, "length": 0 }
+ });
+
+ checkSelection(removedDIndex + (kLFLen - 1) * 2, "", "runControlCharTest", "#1")
+
+ is(result.compositionupdate, removedData, "runControlCharTest: control characters in event.data should be removed in compositionupdate event #1");
+ is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #1");
+
+ synthesizeComposition({ type: "compositioncommit", data });
+
+ is(result.compositionend, removedData, "runControlCharTest: control characters in event.data should be removed in compositionend event #2");
+ is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #2");
+
+ textarea.value = "";
+
+ clearResult();
+
+ Services.prefs.setBoolPref("dom.compositionevent.allow_control_characters", true);
+
+ // input string contains control characters, allowing control characters
+ clearResult();
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": data,
+ "clauses":
+ [
+ { "length": DIndex,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": data.length - DIndex,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": DIndex, "length": 0 }
+ });
+
+ checkSelection(DIndex + (kLFLen - 1) * 2, "", "runControlCharTest", "#3")
+
+ is(result.compositionupdate, data.replace(/\r/g, "\n"), "runControlCharTest: control characters in event.data should not be removed in compositionupdate event #3");
+ is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #3");
+
+ synthesizeComposition({ type: "compositioncommit", data });
+
+ is(result.compositionend, data.replace(/\r/g, "\n"), "runControlCharTest: control characters in event.data should not be removed in compositionend event #4");
+ is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #4");
+
+ Services.prefs.clearUserPref("dom.compositionevent.allow_control_characters");
+
+ textarea.removeEventListener("compositionupdate", handler, true);
+ textarea.removeEventListener("compositionend", handler, true);
+}
+
+async function runRemoveContentTest()
+{
+ let events = [];
+ function eventHandler(aEvent)
+ {
+ events.push(aEvent);
+ }
+ textarea.addEventListener("compositionstart", eventHandler, true);
+ textarea.addEventListener("compositionupdate", eventHandler, true);
+ textarea.addEventListener("compositionend", eventHandler, true);
+ textarea.addEventListener("beforeinput", eventHandler, true);
+ textarea.addEventListener("input", eventHandler, true);
+ textarea.addEventListener("text", eventHandler, true);
+
+ textarea.focus();
+ textarea.value = "";
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u306E",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ let nextSibling = textarea.nextSibling;
+ let parent = textarea.parentElement;
+
+ events = [];
+ parent.removeChild(textarea);
+
+ await waitForEventLoops(50);
+
+ // XXX Currently, "input" event and "beforeinput" event are not fired on removed content.
+ is(events.length, 3,
+ "runRemoveContentTest: wrong event count #1");
+ is(events[0].type, "compositionupdate",
+ "runRemoveContentTest: the 1st event must be compositionupdate #1");
+ is(events[0].target, textarea,
+ `runRemoveContentTest: The "${events[0].type}" event was fired on wrong event target #1`);
+ is(events[0].data, "",
+ "runRemoveContentTest: compositionupdate has wrong data #1");
+ is(events[1].type, "text",
+ "runRemoveContentTest: the 2nd event must be text #1");
+ is(events[1].target, textarea,
+ `runRemoveContentTest: The "${events[1].type}" event was fired on wrong event target #1`);
+ todo_is(events[2].type, "beforeinput",
+ "runRemoveContentTest: the 3rd event must be beforeinput #1");
+ // is(events[2].target, textarea,
+ // `runRemoveContentTest: The "${events[2].type}" event was fired on wrong event target #1`);
+ // checkInputEvent(events[2], true, "insertCompositionText", "", [],
+ // "runRemoveContentTest: #1");
+ is(events[2].type, "compositionend",
+ "runRemoveContentTest: the 4th event must be compositionend #1");
+ is(events[2].target, textarea,
+ `runRemoveContentTest: The "${events[2].type}" event was fired on wrong event target #1`);
+ is(events[2].data, "",
+ "runRemoveContentTest: compositionend has wrong data #1");
+ ok(!getEditor(textarea).isComposing,
+ "runRemoveContentTest: the textarea still has composition #1");
+ todo_is(textarea.value, "",
+ "runRemoveContentTest: the textarea has the committed text? #1");
+
+ parent.insertBefore(textarea, nextSibling);
+
+ textarea.focus();
+ textarea.value = "";
+
+ synthesizeComposition({ type: "compositionstart" });
+
+ events = [];
+ parent.removeChild(textarea);
+
+ await waitForEventLoops(50);
+
+ // XXX Currently, "input" event and "beforeinput" event are not fired on removed content.
+ is(events.length, 2,
+ "runRemoveContentTest: wrong event count #2");
+ is(events[0].type, "text",
+ "runRemoveContentTest: the 1st event must be text #2");
+ is(events[0].target, textarea,
+ `runRemoveContentTest: The ${events[0].type} event was fired on wrong event target #2`);
+ todo_is(events[1].type, "beforeinput",
+ "runRemoveContentTest: the 2nd event must be beforeinput #2");
+ // is(events[1].target, textarea,
+ // `runRemoveContentTest: The ${events[1].type} event was fired on wrong event target #2`);
+ // checkInputEvent(events[1], true, "insertCompositionText", "", [],
+ // "runRemoveContentTest: #2");
+ is(events[1].type, "compositionend",
+ "runRemoveContentTest: the 3rd event must be compositionend #2");
+ is(events[1].target, textarea,
+ `runRemoveContentTest: The ${events[1].type} event was fired on wrong event target #2`);
+ is(events[1].data, "",
+ "runRemoveContentTest: compositionupdate has wrong data #2");
+ ok(!getEditor(textarea).isComposing,
+ "runRemoveContentTest: the textarea still has composition #2");
+ is(textarea.value, "",
+ "runRemoveContentTest: the textarea has the committed text? #2");
+
+ parent.insertBefore(textarea, nextSibling);
+
+ textarea.removeEventListener("compositionstart", eventHandler, true);
+ textarea.removeEventListener("compositionupdate", eventHandler, true);
+ textarea.removeEventListener("compositionend", eventHandler, true);
+ textarea.removeEventListener("beforeinput", eventHandler, true);
+ textarea.removeEventListener("input", eventHandler, true);
+ textarea.removeEventListener("text", eventHandler, true);
+
+ await waitForTick();
+}
+
+function runTestOnAnotherContext(aPanelOrFrame, aFocusedEditor, aTestName)
+{
+ aFocusedEditor.value = "";
+
+ // The frames and panel are cross-origin, and we no longer
+ // propagate flushes to parent cross-origin iframes explicitly,
+ // so flush our own layout here so the positions are correct.
+ document.documentElement.getBoundingClientRect();
+
+ let editorRect = synthesizeQueryEditorRect();
+ if (!checkQueryContentResult(editorRect, aTestName + ": editorRect")) {
+ return;
+ }
+
+ let r = aPanelOrFrame.getBoundingClientRect();
+ let parentRect = {
+ left: r.left * window.devicePixelRatio,
+ top: r.top * window.devicePixelRatio,
+ width: (r.right - r.left) * window.devicePixelRatio,
+ height: (r.bottom - r.top) * window.devicePixelRatio,
+ };
+ checkRectContainsRect(editorRect, parentRect, aTestName +
+ ": the editor rect coordinates are wrong");
+
+ // input characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3078\u3093\u3057\u3093",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 }
+ });
+
+ if (!checkContent("\u3078\u3093\u3057\u3093", aTestName, "#1-1") ||
+ !checkSelection(4, "", aTestName, "#1-1")) {
+ return;
+ }
+
+ // convert them #1
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u8FD4\u4FE1",
+ "clauses":
+ [
+ { "length": 2,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ if (!checkContent("\u8FD4\u4FE1", aTestName, "#1-2") ||
+ !checkSelection(2, "", aTestName, "#1-2")) {
+ return;
+ }
+
+ // convert them #2
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u5909\u8EAB",
+ "clauses":
+ [
+ { "length": 2,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ if (!checkContent("\u5909\u8EAB", aTestName, "#1-3") ||
+ !checkSelection(2, "", aTestName, "#1-3")) {
+ return;
+ }
+
+ // commit them
+ synthesizeComposition({ type: "compositioncommitasis" });
+ if (!checkContent("\u5909\u8EAB", aTestName, "#1-4") ||
+ !checkSelection(2, "", aTestName, "#1-4")) {
+ return;
+ }
+
+ is(aFocusedEditor.value, "\u5909\u8EAB",
+ aTestName + ": composition isn't in the focused editor");
+ if (aFocusedEditor.value != "\u5909\u8EAB") {
+ return;
+ }
+
+ let textRect = synthesizeQueryTextRect(0, 1);
+ let caretRect = synthesizeQueryCaretRect(2);
+ if (!checkQueryContentResult(textRect,
+ aTestName + ": synthesizeQueryTextRect") ||
+ !checkQueryContentResult(caretRect,
+ aTestName + ": synthesizeQueryCaretRect")) {
+ return;
+ }
+ checkRectContainsRect(textRect, editorRect, aTestName + ":testRect");
+ checkRectContainsRect(caretRect, editorRect, aTestName + ":caretRect");
+}
+
+function runFrameTest()
+{
+ textareaInFrame.focus();
+ runTestOnAnotherContext(iframe, textareaInFrame, "runFrameTest");
+ runCharAtPointTest(textareaInFrame, "textarea in the iframe");
+}
+
+async function runPanelTest()
+{
+ panel.hidden = false;
+ let waitOpenPopup = new Promise(resolve => {
+ panel.addEventListener("popupshown", resolve, {once: true});
+ });
+ let waitFocusTextBox = new Promise(resolve => {
+ textbox.addEventListener("focus", resolve, {once: true});
+ });
+ panel.openPopupAtScreen(window.screenX + window.outerWidth, 0, false);
+ await waitOpenPopup;
+ textbox.focus();
+ await waitFocusTextBox;
+ is(panel.state, "open", "The panel should be open (after textbox.focus())");
+ await waitForTick();
+ is(panel.state, "open", "The panel should be open (after waitForTick())");
+ runTestOnAnotherContext(panel, textbox, "runPanelTest");
+ is(panel.state, "open", "The panel should be open (after runTestOnAnotherContext())");
+ runCharAtPointTest(textbox, "textbox in the panel");
+ is(panel.state, "open", "The panel should be open (after runCharAtPointTest())");
+ let waitClosePopup = new Promise(resolve => {
+ panel.addEventListener("popuphidden", resolve, {once: true});
+ });
+ panel.hidePopup();
+ await waitClosePopup;
+ await waitForTick();
+}
+
+// eslint-disable-next-line complexity
+function runMaxLengthTest()
+{
+ input.maxLength = 1;
+ input.value = "";
+ input.focus();
+
+ let kDesc ="runMaxLengthTest";
+
+ // input first character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ if (!checkContent("\u3089", kDesc, "#1-1") ||
+ !checkSelection(1, "", kDesc, "#1-1")) {
+ return;
+ }
+
+ // input second character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC", kDesc, "#1-2") ||
+ !checkSelection(2, "", kDesc, "#1-2")) {
+ return;
+ }
+
+ // input third character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081", kDesc, "#1-3") ||
+ !checkSelection(3, "", kDesc, "#1-3")) {
+ return;
+ }
+
+ // input fourth character
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093", kDesc, "#1-4") ||
+ !checkSelection(4, "", kDesc, "#1-4")) {
+ return;
+ }
+
+
+ // backspace
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081", kDesc, "#1-5") ||
+ !checkSelection(3, "", kDesc, "#1-5")) {
+ return;
+ }
+
+ // re-input
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093",
+ "clauses":
+ [
+ { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093", kDesc, "#1-6") ||
+ !checkSelection(4, "", kDesc, "#1-6")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055",
+ "clauses":
+ [
+ { "length": 5, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 5, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055", kDesc, "#1-7") ||
+ !checkSelection(5, "", kDesc, "#1-7")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044",
+ "clauses":
+ [
+ { "length": 6, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 6, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044", kDesc, "#1-8") ||
+ !checkSelection(6, "", kDesc, "#1-8")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053",
+ "clauses":
+ [
+ { "length": 7, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 7, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053",
+ kDesc, "#1-8") ||
+ !checkSelection(7, "", kDesc, "#1-8")) {
+ return;
+ }
+
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046",
+ "clauses":
+ [
+ { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 8, "length": 0 }
+ });
+
+ if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046",
+ kDesc, "#1-9") ||
+ !checkSelection(8, "", kDesc, "#1-9")) {
+ return;
+ }
+
+ // convert
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8",
+ "clauses":
+ [
+ { "length": 4,
+ "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 2,
+ "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 4, "length": 0 }
+ });
+
+ if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", kDesc, "#1-10") ||
+ !checkSelection(4, "", kDesc, "#1-10")) {
+ return;
+ }
+
+ // commit the composition string
+ synthesizeComposition({ type: "compositioncommitasis" });
+ if (!checkContent("\u30E9", kDesc, "#1-11") ||
+ !checkSelection(1, "", kDesc, "#1-11")) {
+ return;
+ }
+
+ // input characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u3057",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ if (!checkContent("\u30E9\u3057", kDesc, "#2-1") ||
+ !checkSelection(1 + 1, "", kDesc, "#2-1")) {
+ return;
+ }
+
+ // commit the composition string
+ synthesizeComposition({ type: "compositioncommit", data: "\u3058" });
+ if (!checkContent("\u30E9", kDesc, "#2-2") ||
+ !checkSelection(1 + 0, "", kDesc, "#2-2")) {
+ return;
+ }
+
+ // Undo
+ synthesizeKey("Z", {accelKey: true});
+
+ // XXX this is unexpected behavior, see bug 258291
+ if (!checkContent("\u30E9", kDesc, "#3-1") ||
+ !checkSelection(1 + 0, "", kDesc, "#3-1")) {
+ return;
+ }
+
+ // Undo
+ synthesizeKey("Z", {accelKey: true});
+ if (!checkContent("", kDesc, "#3-2") ||
+ !checkSelection(0, "", kDesc, "#3-2")) {
+ return;
+ }
+
+ // Redo
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ if (!checkContent("\u30E9", kDesc, "#3-3") ||
+ !checkSelection(1, "", kDesc, "#3-3")) {
+ return;
+ }
+
+ // Redo
+ synthesizeKey("Z", {accelKey: true, shiftKey: true});
+ if (!checkContent("\u30E9", kDesc, "#3-4") ||
+ !checkSelection(1 + 0, "", kDesc, "#3-4")) {
+ return;
+ }
+
+ // The input element whose content length is already maxlength and
+ // the carest is at start of the content.
+ input.value = "X";
+ input.selectionStart = input.selectionEnd = 0;
+
+ // input characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u9B54",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+
+ if (!checkContent("\u9B54X", kDesc, "#4-1") ||
+ !checkSelection(1, "", kDesc, "#4-1")) {
+ return;
+ }
+
+ // commit the composition string
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ // The input text must be discarded. Then, the caret position shouldn't be
+ // updated from its position at compositionstart.
+ if (!checkContent("X", kDesc, "#4-2") ||
+ !checkSelection(0, "", kDesc, "#4-2")) {
+ return;
+ }
+
+ // input characters
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "\u9B54\u6CD5",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+
+ if (!checkContent("\u9B54\u6CD5X", kDesc, "#5-1") ||
+ !checkSelection(2, "", kDesc, "#5-1")) {
+ return;
+ }
+
+ // commit the composition string
+ synthesizeComposition({ type: "compositioncommitasis" });
+
+ if (checkContent("X", kDesc, "#5-2")) {
+ checkSelection(0, "", kDesc, "#5-2");
+ }
+}
+
+async function runEditorReframeTests()
+{
+ async function runEditorReframeTest(aEditor, aWindow, aEventType)
+ {
+ function getValue()
+ {
+ return aEditor == contenteditable ?
+ aEditor.innerHTML.replace("<br>", "") : aEditor.value;
+ }
+
+ let description = "runEditorReframeTest(" + aEditor.id + ", \"" + aEventType + "\"): ";
+
+ let tests = [
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "a",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "a", description + "Typing 'a'");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ab", description + "Typing 'b' next to 'a'");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "abc", description + "Typing 'c' next to 'ab'");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "abc", description + "Starting to convert 'ab][c'");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABc",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABc", description + "Starting to convert 'AB][c'");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ABC",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE },
+ { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABC", description + "Starting to convert 'AB][C'");
+ },
+ },
+ { test () {
+ // Commit composition
+ synthesizeComposition({ type: "compositioncommitasis" });
+ },
+ check () {
+ is(getValue(aEditor), "ABC", description + "Committed as 'ABC'");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "d",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABCd", description + "Typing 'd' next to ABC");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "de",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABCde", description + "Typing 'e' next to ABCd");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "def",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABCdef", description + "Typing 'f' next to ABCde");
+ },
+ },
+ { test () {
+ // Commit composition
+ synthesizeComposition({ type: "compositioncommitasis" });
+ },
+ check () {
+ is(getValue(aEditor), "ABCdef", description + "Commit 'def' without convert");
+ },
+ },
+ { test () {
+ // Select "Cd"
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_Shift", {type: "keydown", shiftKey: true});
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ synthesizeKey("KEY_Shift", {type: "keyup"});
+ },
+ check () {
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "g",
+ "clauses":
+ [
+ { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 1, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABgef", description + "Typing 'g' next to AB");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "gh",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABghef", description + "Typing 'h' next to ABg");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ghi",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABghief", description + "Typing 'i' next to ABgh");
+ },
+ },
+ { test () {
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "GHI",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 }
+ });
+ },
+ check () {
+ is(getValue(aEditor), "ABGHIef", description + "Convert 'ghi' to 'GHI'");
+ },
+ },
+ { test () {
+ // Commit composition
+ synthesizeComposition({ type: "compositioncommitasis" });
+ },
+ check () {
+ is(getValue(aEditor), "ABGHIef", description + "Commit 'GHI'");
+ },
+ },
+ ];
+
+ function doReframe(aEvent)
+ {
+ aEvent.target.style.overflow =
+ aEvent.target.style.overflow != "hidden" ? "hidden" : "auto";
+ }
+ aEditor.focus();
+ aEditor.addEventListener(aEventType, doReframe);
+
+ for (const currentTest of tests) {
+ currentTest.test();
+ await waitForEventLoops(20);
+ currentTest.check();
+ await waitForTick();
+ }
+
+ await new Promise(resolve => {
+ aEditor.style.overflow = "auto";
+ aEditor.removeEventListener(aEventType, doReframe);
+ requestAnimationFrame(() => { SimpleTest.executeSoon(resolve); });
+ });
+ }
+
+ // TODO: Add "beforeinput" case.
+ input.value = "";
+ await runEditorReframeTest(input, window, "input");
+ input.value = "";
+ await runEditorReframeTest(input, window, "compositionupdate");
+ textarea.value = "";
+ await runEditorReframeTest(textarea, window, "input");
+ textarea.value = "";
+ await runEditorReframeTest(textarea, window, "compositionupdate");
+ contenteditable.innerHTML = "";
+ await runEditorReframeTest(contenteditable, windowOfContenteditable, "input");
+ contenteditable.innerHTML = "";
+ await runEditorReframeTest(contenteditable, windowOfContenteditable, "compositionupdate");
+}
+
+async function runIMEContentObserverTest()
+{
+ let notifications = [];
+ let onReceiveNotifications = null;
+ function callback(aTIP, aNotification)
+ {
+ if (aNotification.type != "notify-end-input-transaction") {
+ notifications.push(aNotification);
+ }
+ switch (aNotification.type) {
+ case "request-to-commit":
+ aTIP.commitComposition();
+ break;
+ case "request-to-cancel":
+ aTIP.cancelComposition();
+ break;
+ }
+ if (onReceiveNotifications) {
+ let resolve = onReceiveNotifications;
+ onReceiveNotifications = null;
+ SimpleTest.executeSoon(() => {
+ resolve();
+ });
+ }
+ return true;
+ }
+
+ function dumpUnexpectedNotifications(aDescription, aExpectedCount)
+ {
+ if (notifications.length <= aExpectedCount) {
+ return;
+ }
+ for (let i = aExpectedCount; i < notifications.length; i++) {
+ ok(false,
+ aDescription + " caused unexpected notification: " + notifications[i].type);
+ }
+ }
+
+ function promiseReceiveNotifications()
+ {
+ notifications = [];
+ return new Promise(resolve => {
+ onReceiveNotifications = resolve;
+ });
+ }
+
+ function flushNotifications()
+ {
+ return new Promise(resolve => {
+ // FYI: Dispatching non-op keyboard events causes forcibly flushing pending
+ // notifications.
+ synthesizeKey("KEY_Unidentified", { code: "" });
+ SimpleTest.executeSoon(()=>{
+ notifications = [];
+ resolve();
+ });
+ });
+ }
+
+ function ensureToRemovePrecedingPositionChangeNotification(aDescription)
+ {
+ if (!notifications.length) {
+ return;
+ }
+ if (notifications[0].type != "notify-position-change") {
+ return;
+ }
+ // Sometimes, notify-position-change is notified first separately if
+ // the operation causes scroll or something. Tests can ignore this.
+ ok(true, "notify-position-change", aDescription + "Unnecessary notify-position-change occurred, ignoring it");
+ notifications.shift();
+ }
+
+ // Bug 1374057 - On ubuntu 16.04 there are notify-position-change events that are
+ // recorded after all the other events so we remove them through this function.
+ function ensureToRemovePostPositionChangeNotification(aDescription, expectedCount)
+ {
+ if (!notifications.length) {
+ return;
+ }
+ if (notifications.length <= expectedCount) {
+ return;
+ }
+ if (notifications[notifications.length-1].type != "notify-position-change") {
+ return;
+ }
+ ok(true, "notify-position-change", aDescription + "Unnecessary notify-position-change occurred, ignoring it");
+ notifications.pop();
+ }
+
+ function getNativeText(aXPText)
+ {
+ if (kLF == "\n") {
+ return aXPText;
+ }
+ return aXPText.replace(/\n/g, kLF);
+ }
+
+ function checkPositionChangeNotification(aNotification, aDescription)
+ {
+ is(!aNotification || aNotification.type, "notify-position-change",
+ aDescription + " should cause position change notification");
+ }
+
+ function checkSelectionChangeNotification(aNotification, aDescription, aExpected)
+ {
+ is(!aNotification || aNotification.type, "notify-selection-change",
+ aDescription + " should cause selection change notification");
+ if (!aNotification || (aNotification.type != "notify-selection-change")) {
+ return;
+ }
+ is(aNotification.offset, aExpected.offset,
+ aDescription + " should cause selection change notification whose offset is " + aExpected.offset);
+ is(aNotification.text, aExpected.text,
+ aDescription + " should cause selection change notification whose text is '" + aExpected.text + "'");
+ is(aNotification.collapsed, !aExpected.text.length,
+ aDescription + " should cause selection change notification whose collapsed is " + (!aExpected.text.length));
+ is(aNotification.length, aExpected.text.length,
+ aDescription + " should cause selection change notification whose length is " + aExpected.text.length);
+ is(aNotification.reversed, aExpected.reversed || false,
+ aDescription + " should cause selection change notification whose reversed is " + (aExpected.reversed || false));
+ is(aNotification.writingMode, aExpected.writingMode || "horizontal-tb",
+ aDescription + " should cause selection change notification whose writingMode is '" + (aExpected.writingMode || "horizontal-tb"));
+ }
+
+ function checkTextChangeNotification(aNotification, aDescription, aExpected)
+ {
+ is(!aNotification || aNotification.type, "notify-text-change",
+ aDescription + " should cause text change notification");
+ if (!aNotification || aNotification.type != "notify-text-change") {
+ return;
+ }
+ is(aNotification.offset, aExpected.offset,
+ aDescription + " should cause text change notification whose offset is " + aExpected.offset);
+ is(aNotification.removedLength, aExpected.removedLength,
+ aDescription + " should cause text change notification whose removedLength is " + aExpected.removedLength);
+ is(aNotification.addedLength, aExpected.addedLength,
+ aDescription + " should cause text change notification whose addedLength is " + aExpected.addedLength);
+ }
+
+ async function testWithPlaintextEditor(aDescription, aElement, aTestLineBreaker)
+ {
+ aElement.value = "";
+ aElement.blur();
+ let doc = aElement.ownerDocument;
+ let win = doc.defaultView;
+ aElement.focus();
+ await flushNotifications();
+
+ // "a[]"
+ let description = aDescription + "typing 'a'";
+ let waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("a", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 0, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "ab[]"
+ description = aDescription + "typing 'b'";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("b", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 0, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "abc[]"
+ description = aDescription + "typing 'c'";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("c", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 3, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "ab[c]"
+ description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 2, text: "c", reversed: true });
+ ensureToRemovePostPositionChangeNotification(description, 1);
+ dumpUnexpectedNotifications(description, 1);
+
+ // "a[bc]"
+ description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: "bc", reversed: true });
+ ensureToRemovePostPositionChangeNotification(description, 1);
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[abc]"
+ description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "abc", reversed: true });
+ ensureToRemovePostPositionChangeNotification(description, 1);
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[]abc"
+ description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "" });
+ ensureToRemovePostPositionChangeNotification(description, 1);
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[a]bc"
+ description = aDescription + "selecting 'a' with pressing Shift+ArrowRight";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "a" });
+ ensureToRemovePostPositionChangeNotification(description, 1);
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[ab]c"
+ description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "ab" });
+ ensureToRemovePostPositionChangeNotification(description, 1);
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[]c"
+ description = aDescription + "deleting 'ab' with pressing Delete";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "[]"
+ description = aDescription + "deleting following 'c' with pressing Delete";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 1, addedLength: 0 });
+ checkPositionChangeNotification(notifications[1], description);
+ ensureToRemovePostPositionChangeNotification(description, 2);
+ dumpUnexpectedNotifications(description, 2);
+
+ // "abc[]"
+ synthesizeKey("a", {}, win, callback);
+ synthesizeKey("b", {}, win, callback);
+ synthesizeKey("c", {}, win, callback);
+ await flushNotifications();
+
+ // "ab[]"
+ description = aDescription + "deleting 'c' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 1, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "[ab]"
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "[]"
+ description = aDescription + "deleting 'ab' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "abcd[]"
+ synthesizeKey("a", {}, win, callback);
+ synthesizeKey("b", {}, win, callback);
+ synthesizeKey("c", {}, win, callback);
+ synthesizeKey("d", {}, win, callback);
+ await flushNotifications();
+
+ // "a[bc]d"
+ synthesizeKey("KEY_ArrowLeft", {}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "a[]d"
+ description = aDescription + "deleting 'bc' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "a[bc]d"
+ synthesizeKey("b", {}, win, callback);
+ synthesizeKey("c", {}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "aB[]d"
+ description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("B", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ if (!aTestLineBreaker) {
+ return;
+ }
+
+ // "aB\n[]d"
+ description = aDescription + "inserting a line break after 'B' with pressing Enter";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Enter", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: kLFLen });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "aB[]d"
+ description = aDescription + "removing a line break after 'B' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: kLFLen, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "a[B]d"
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "a\n[]d"
+ description = aDescription + "replacing 'B' with a line break with pressing Enter";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Enter", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 1, addedLength: kLFLen });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "a[\n]d"
+ description = aDescription + "selecting '\n' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: kLF, reversed: true });
+ ensureToRemovePostPositionChangeNotification(description, 1);
+ dumpUnexpectedNotifications(description, 1);
+
+ // "a[]d"
+ description = aDescription + "removing selected '\n' with pressing Delete";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: kLFLen, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // ab\ncd\nef\ngh\n[]
+ description = aDescription + "setting the value property to 'ab\ncd\nef\ngh\n'";
+ waitNotifications = promiseReceiveNotifications();
+ aElement.value = "ab\ncd\nef\ngh\n";
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: getNativeText("ab\ncd\nef\ngh\n").length });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("ab\ncd\nef\ngh\n").length, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+
+ // []
+ description = aDescription + "setting the value property to ''";
+ waitNotifications = promiseReceiveNotifications();
+ aElement.value = "";
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: getNativeText("ab\ncd\nef\ngh\n").length, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ ensureToRemovePostPositionChangeNotification(description, 3);
+ dumpUnexpectedNotifications(description, 3);
+ }
+
+ async function testWithHTMLEditor(aDescription, aElement, aDefaultParagraphSeparator)
+ {
+ let doc = aElement.ownerDocument;
+ let win = doc.defaultView;
+ let sel = doc.getSelection();
+ let inDesignMode = doc.designMode == "on";
+ let offsetAtStart = 0;
+ let offsetAtContainer = 0;
+ let isDefaultParagraphSeparatorBlock = aDefaultParagraphSeparator != "br";
+ doc.execCommand("defaultParagraphSeparator", false, aDefaultParagraphSeparator);
+
+ // "[]", "<p>[]</p>" or "<div>[]</div>"
+ switch (aDefaultParagraphSeparator) {
+ case "br":
+ aElement.innerHTML = "";
+ break;
+ case "p":
+ case "div":
+ aElement.innerHTML = "<" + aDefaultParagraphSeparator + "></" + aDefaultParagraphSeparator + ">";
+ sel.collapse(aElement.firstChild, 0);
+ offsetAtContainer = offsetAtStart + kLFLen;
+ break;
+ default:
+ ok(false, aDescription + "aDefaultParagraphSeparator is illegal value");
+ await flushNotifications();
+ return;
+ }
+
+ if (inDesignMode) {
+ win.focus();
+ } else {
+ aElement.focus();
+ }
+ await flushNotifications();
+
+ // "a[]"
+ let description = aDescription + "typing 'a'";
+ let waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("a", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 0, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "ab[]"
+ description = aDescription + "typing 'b'";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("b", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 0, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "abc[]"
+ description = aDescription + "typing 'c'";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("c", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 0, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 3 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "ab[c]"
+ description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, text: "c", reversed: true });
+ dumpUnexpectedNotifications(description, 1);
+
+ // "a[bc]"
+ description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: "bc", reversed: true });
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[abc]"
+ description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "abc", reversed: true });
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[]abc"
+ description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "" });
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[a]bc"
+ description = aDescription + "selecting 'a' with pressing Shift+ArrowRight";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "a" });
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[ab]c"
+ description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "ab" });
+ dumpUnexpectedNotifications(description, 1);
+
+ // "[]c"
+ description = aDescription + "deleting 'ab' with pressing Delete";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "[]"
+ description = aDescription + "deleting following 'c' with pressing Delete";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 1, addedLength: kLFLen });
+ checkPositionChangeNotification(notifications[1], description);
+ dumpUnexpectedNotifications(description, 2);
+
+ // "abc[]"
+ synthesizeKey("a", {}, win, callback);
+ synthesizeKey("b", {}, win, callback);
+ synthesizeKey("c", {}, win, callback);
+ await flushNotifications();
+
+ // "ab[]"
+ description = aDescription + "deleting 'c' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 1, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "[ab]"
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "[]"
+ description = aDescription + "deleting 'ab' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "abcd[]"
+ synthesizeKey("a", {}, win, callback);
+ synthesizeKey("b", {}, win, callback);
+ synthesizeKey("c", {}, win, callback);
+ synthesizeKey("d", {}, win, callback);
+ await flushNotifications();
+
+ // "a[bc]d"
+ synthesizeKey("KEY_ArrowLeft", {}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "a[]d"
+ description = aDescription + "deleting 'bc' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "a[bc]d"
+ synthesizeKey("b", {}, win, callback);
+ synthesizeKey("c", {}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "aB[]d"
+ description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("B", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "aB<br>[]d" or "<block>aB</block><block>[]d</block>"
+ description = aDescription + "inserting a line break after 'B' with pressing Enter";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Enter", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ if (isDefaultParagraphSeparatorBlock) {
+ // Splitting current block causes removing "d</block>" and inserting "</block><block>d</block>".
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer + "aB".length,
+ removedLength: getNativeText("d\n").length,
+ addedLength: getNativeText("\nd\n").length,
+ });
+ } else {
+ // Inserting <br> causes removing "d" and inserting "<br>d"
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer + "aB".length,
+ removedLength: "d".length,
+ addedLength: getNativeText("\nd").length,
+ });
+ }
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "aB[]d"
+ description = aDescription + "removing a line break after 'B' with pressing Backspace";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Backspace", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ if (isDefaultParagraphSeparatorBlock) {
+ // Joining two blocks causes removing "aB</block><block>d</block>" and inserting "aBd</block>"
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer,
+ removedLength: getNativeText("aB\nd\n").length,
+ addedLength: getNativeText("aBd\n").length,
+ });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+ } else {
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer + "aB".length,
+ removedLength: kLFLen,
+ addedLength: 0,
+ });
+ is(notifications.length, 3, description + " should cause 3 notifications");
+ is(notifications[1] && notifications[1].type, "notify-selection-change", description + " should cause selection change notification");
+ is(notifications[2] && notifications[2].type, "notify-position-change", description + " should cause position change notification");
+ }
+
+ // "a[B]d"
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await flushNotifications();
+
+ // "a<br>[]d" or "<block>a</block><block>[]d</block>"
+ description = aDescription + "replacing 'B' with a line break with pressing Enter";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Enter", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ if (isDefaultParagraphSeparatorBlock) {
+ // Splitting current block causes removing "Bd</block>" and inserting "</block><block>d</block>".
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer + "a".length,
+ removedLength: getNativeText("Bd\n").length,
+ addedLength: getNativeText("\nd\n").length,
+ });
+ } else {
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer + "a".length,
+ removedLength: "B".length,
+ addedLength: kLFLen,
+ });
+ }
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "a[<br>]d" or "<block>a[</block><block>]d</block>"
+ description = aDescription + "selecting '\\n' with pressing Shift+ArrowLeft";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: kLF, reversed: true });
+ dumpUnexpectedNotifications(description, 1);
+
+ // "a[]d"
+ description = aDescription + "removing selected '\\n' with pressing Delete";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ if (isDefaultParagraphSeparatorBlock) {
+ // Joining the blocks causes removing "a</block><block>d</block>" and inserting "<block>ad</block>".
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer,
+ removedLength: getNativeText("a\nd\n").length,
+ addedLength: getNativeText("ad\n").length,
+ });
+ } else {
+ checkTextChangeNotification(notifications[0], description, {
+ offset: offsetAtContainer + "a".length,
+ removedLength: kLFLen,
+ addedLength: 0,
+ });
+ }
+ checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"
+ description = aDescription + "inserting HTML which has nested block elements";
+ waitNotifications = promiseReceiveNotifications();
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ // There is <br> after the end of the line. Therefore, removed length includes a line breaker length.
+ if (isDefaultParagraphSeparatorBlock) {
+ checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\nad\n").length, addedLength: getNativeText("\n1\n2\n345").length });
+ } else {
+ checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2 + kLFLen, addedLength: getNativeText("\n1\n2\n345").length });
+ }
+ checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1[<div>2<div>3</div>4</div>]5</div>" and removing selection
+ sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(2), 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>15</div>", description + " should remove '<div>2<div>3</div>4</div>'");
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1[<div>2<div>3</div>]4</div>5</div>" and removing selection
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+ sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(2), 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (partially #1) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>145</div>", description + " should remove '<div>2<div>3</div></div>'");
+ // It causes removing '<div>2<div>3</div>4</div>' and inserting '4'.
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1[<div>2<div>]3</div>4</div>5</div>" and removing selection
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+ sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (partially #2) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>13<div>4</div>5</div>", description + " should remove '<div>2</div>'");
+ // It causes removing '1<div>2<div>3</div></div>' and inserting '13<div>'.
+ checkTextChangeNotification(notifications[0], description, { offset: kLFLen + offsetAtStart, removedLength: getNativeText("1\n2\n3").length, addedLength: getNativeText("13\n").length });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1<div>2<div>3[</div>4</div>]5</div>" and removing selection
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>";
+ sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(2), 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (partially #3) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>1<div>2<div>35</div></div></div>", description + " should remove '4'");
+ // It causes removing '45' and inserting '5'.
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: 2, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"
+ description = aDescription + "inserting HTML which has a pair of nested block elements";
+ waitNotifications = promiseReceiveNotifications();
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtStart, removedLength: getNativeText("\n1\n2\n35").length, addedLength: getNativeText("\n1\n2\n345\n6\n789").length });
+ checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection
+ sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (between same level descendants) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>1<div>2<div>37</div></div><div>8</div>9</div>", description + " should remove '456<div>7'");
+ // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>37</div><div>'.
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n7").length, addedLength: getNativeText("\n37\n").length });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+ sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (between different level descendants #1) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>1<div>27</div><div>8</div>9</div>", description + " should remove '<div>2<div>3</div>4</div>5<div>6<div>7</div>'");
+ // It causes removing '<div>2<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>27</div>'.
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n345\n6\n7").length, addedLength: getNativeText("\n27\n").length });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+ sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (between different level descendants #2) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>1<div>29</div></div>", description + " should remove '<div>3</div>4</div>5<div>6<div>7</div>8</div>'");
+ // It causes removing '<div>3</div>4</div>5</div>6<div>7</div>8</div>9' and inserting '9</div>'.
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n789").length, addedLength: 1 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1<div>2<div>3[</div>4</div>5<div>]6<div>7</div>8</div>9</div>" and removing selection
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+ sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).firstChild, 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (between different level descendants #3) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>1<div>2<div>36<div>7</div>8</div></div>9</div>", description + " should remove '<div>36<div>7</div>8</div>'");
+ // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'.
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n78").length, addedLength: getNativeText("\n36\n78").length });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection
+ aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>";
+ sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0);
+ await flushNotifications();
+ description = aDescription + "deleting child nodes (between different level descendants #4) with pressing Delete key";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Delete", {}, win, callback);
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<div>1<div>2<div>39</div></div></div>", description + " should remove '</div>4</div>5<div>6<div>7</div>8</div>'");
+ // It causes removing '</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'.
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: getNativeText("45\n6\n789").length, addedLength: getNativeText("9").length });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<p>abc</p><p><br></p><p>{<br>}</p>" and removing second paragraph with DOM API
+ aElement.innerHTML = "<p>abc</p><p><br></p><p><br></p>";
+ sel.collapse(aElement.firstChild.nextSibling.nextSibling, 0);
+ await flushNotifications();
+ description = aDescription + "deleting previous paragraph with DOM API";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Unidentified", { code: "" }, win, callback); // For setting the callback to recode notifications
+ aElement.firstChild.nextSibling.remove();
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<p>abc</p><p><br></p>", description + " the second paragraph should've been removed");
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\nabc").length + offsetAtStart, removedLength: getNativeText("\n\n").length, addedLength: 0 });
+ checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\nabc\n").length + offsetAtStart, text: "" });
+ checkPositionChangeNotification(notifications[2], description);
+ dumpUnexpectedNotifications(description, 3);
+
+ // "<p>abc</p><p>{<br>}</p><p><br></p>" and removing last paragraph with DOM API
+ aElement.innerHTML = "<p>abc</p><p><br></p><p><br></p>";
+ sel.collapse(aElement.firstChild.nextSibling, 0);
+ await flushNotifications();
+ description = aDescription + "deleting next paragraph with DOM API";
+ waitNotifications = promiseReceiveNotifications();
+ synthesizeKey("KEY_Unidentified", { code: "" }, win, callback); // For setting the callback to recode notifications
+ aElement.firstChild.nextSibling.nextSibling.remove();
+ await waitNotifications;
+ ensureToRemovePrecedingPositionChangeNotification();
+ is(aElement.innerHTML, "<p>abc</p><p><br></p>", description + " the last paragraph should've been removed");
+ checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\nabc\n\n").length + offsetAtStart, removedLength: getNativeText("\n\n").length, addedLength: 0 });
+ checkPositionChangeNotification(notifications[1], description);
+ dumpUnexpectedNotifications(description, 2);
+ }
+
+ await testWithPlaintextEditor("runIMEContentObserverTest with input element: ", input, false);
+ await testWithPlaintextEditor("runIMEContentObserverTest with textarea element: ", textarea, true);
+ await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is br): ", contenteditable, "br");
+ await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is p): ", contenteditable, "p");
+ await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is div): ", contenteditable, "div");
+ // XXX Due to the difference of HTML editor behavior between designMode and contenteditable,
+ // testWithHTMLEditor() gets some unexpected behavior. However, IMEContentObserver is
+ // not depend on editor's detail. So, we should investigate this issue later. It's not
+ // so important for now.
+ // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is br): ", iframe2.contentDocument.body, "br");
+ // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is p): ", iframe2.contentDocument.body, "p");
+ // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is div): ", iframe2.contentDocument.body, "div");
+}
+
+async function runPasswordMaskDelayTest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["editor.password.mask_delay", 600],
+ ["editor.password.testing.mask_delay", true],
+ ],
+ });
+
+ let iframe5 = document.getElementById("iframe5");
+ let iframe6 = document.getElementById("iframe6");
+ let inputWindow = iframe5.contentWindow;
+ let passwordWindow = iframe6.contentWindow;
+
+ let inputElement = iframe5.contentDocument.getElementById("input");
+ let passwordElement = iframe6.contentDocument.getElementById("password");
+
+ const kMask = passwordElement.editor.passwordMask;
+
+ function promiseAllPasswordMasked() {
+ return new Promise(resolve => {
+ passwordElement.addEventListener("MozLastInputMasked", resolve, {once: true});
+ });
+ }
+
+ function checkSnapshots(aResult, aReference, aMatch, aDescription) {
+ let [correct, data1, data2] = compareSnapshots(aResult, aReference, true);
+ is(correct, aMatch, `${aDescription}\nREFTEST IMAGE 1 (TEST): ${data1}\nREFTEST IMAGE 2 (REFERENCE): ${data2}`);
+ }
+
+ // First character input
+ passwordElement.value = "";
+ passwordElement.focus();
+ let waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeKey("a");
+ let unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ let maskedResult = await snapshotWindow(passwordWindow, true);
+
+ inputElement.value = "a";
+ inputElement.focus();
+ inputElement.setSelectionRange(1, 1);
+ let unmaskedReference = await snapshotWindow(inputWindow, true);
+ inputElement.value = kMask;
+ inputElement.setSelectionRange(1, 1);
+ let maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): first inputted character should be unmasked for a while");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): first inputted character should be masked after a while");
+
+ // Second character input
+ passwordElement.value = "a";
+ passwordElement.focus();
+ passwordElement.setSelectionRange(1, 1);
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeKey("b");
+ unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ maskedResult = await snapshotWindow(passwordWindow, true);
+
+ inputElement.value = `${kMask}b`;
+ inputElement.focus();
+ inputElement.setSelectionRange(2, 2);
+ unmaskedReference = await snapshotWindow(inputWindow, true);
+ inputElement.value = `${kMask}${kMask}`;
+ inputElement.setSelectionRange(2, 2);
+ maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): second inputted character should be unmasked for a while");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): second inputted character should be masked after a while");
+
+ // Typing new character should mask the previous unmasked characters
+ passwordElement.value = "ab";
+ passwordElement.focus();
+ passwordElement.setSelectionRange(2, 2);
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeKey("c");
+ synthesizeKey("d");
+ unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ maskedResult = await snapshotWindow(passwordWindow, true);
+
+ inputElement.value = `${kMask}${kMask}${kMask}d`;
+ inputElement.focus();
+ inputElement.setSelectionRange(4, 4);
+ unmaskedReference = await snapshotWindow(inputWindow, true);
+ inputElement.value = `${kMask}${kMask}${kMask}${kMask}`;
+ inputElement.setSelectionRange(4, 4);
+ maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): forth character input should mask the third character");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): forth inputted character should be masked after a while");
+
+ // Typing middle of password should unmask the last input character
+ passwordElement.value = "abcd";
+ passwordElement.focus();
+ passwordElement.setSelectionRange(2, 2);
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeKey("e");
+ unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ maskedResult = await snapshotWindow(passwordWindow, true);
+
+ inputElement.value = `${kMask}${kMask}e${kMask}${kMask}`;
+ inputElement.focus();
+ inputElement.setSelectionRange(3, 3);
+ unmaskedReference = await snapshotWindow(inputWindow, true);
+ inputElement.value = `${kMask}${kMask}${kMask}${kMask}${kMask}`;
+ inputElement.setSelectionRange(3, 3);
+ maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): inserted character should be unmasked for a while");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): inserted character should be masked after a while");
+
+ // Composition string should be unmasked for a while, and shouldn't be committed at masking
+ passwordElement.value = "ab";
+ passwordElement.focus();
+ passwordElement.setSelectionRange(1, 1);
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeCompositionChange(
+ { composition:
+ { string: "c",
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ maskedResult = await snapshotWindow(passwordWindow, true);
+ is(getEditor(passwordElement).composing, true,
+ "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #1");
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ inputElement.value = `${kMask}${kMask}`;
+ inputElement.focus();
+ inputElement.setSelectionRange(1, 1);
+ synthesizeCompositionChange(
+ { composition:
+ { string: "c",
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ unmaskedReference = await snapshotWindow(inputWindow, true);
+ synthesizeCompositionChange(
+ { composition:
+ { string: kMask,
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): composing character should be unmasked for a while");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): composing character should be masked after a while");
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ // Updating composition string should unmask the composition string for a while
+ passwordElement.value = "ab";
+ passwordElement.focus();
+ passwordElement.setSelectionRange(1, 1);
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeCompositionChange(
+ { composition:
+ { string: "c",
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ await waitForMaskingLastInput;
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeCompositionChange(
+ { composition:
+ { string: "d",
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ maskedResult = await snapshotWindow(passwordWindow, true);
+ is(getEditor(passwordElement).composing, true,
+ "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #2");
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ inputElement.value = `${kMask}${kMask}`;
+ inputElement.focus();
+ inputElement.setSelectionRange(1, 1);
+ synthesizeCompositionChange(
+ { composition:
+ { string: "d",
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ unmaskedReference = await snapshotWindow(inputWindow, true);
+ synthesizeCompositionChange(
+ { composition:
+ { string: kMask,
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): updated composing character should be unmasked for a while");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): updated composing character should be masked after a while");
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ // Composing multi-characters should be unmasked for a while.
+ passwordElement.value = "ab";
+ passwordElement.focus();
+ passwordElement.setSelectionRange(1, 1);
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeCompositionChange(
+ { composition:
+ { string: "c",
+ clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 1, length: 0 },
+ });
+ await waitForMaskingLastInput;
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeCompositionChange(
+ { composition:
+ { string: "cd",
+ clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 2, length: 0 },
+ });
+ unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ maskedResult = await snapshotWindow(passwordWindow, true);
+ is(getEditor(passwordElement).composing, true,
+ "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #3");
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ inputElement.value = `${kMask}${kMask}`;
+ inputElement.focus();
+ inputElement.setSelectionRange(1, 1);
+ synthesizeCompositionChange(
+ { composition:
+ { string: "cd",
+ clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 2, length: 0 },
+ });
+ unmaskedReference = await snapshotWindow(inputWindow, true);
+ synthesizeCompositionChange(
+ { composition:
+ { string: `${kMask}${kMask}`,
+ clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 2, length: 0 },
+ });
+ maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): all of composing string should be unmasked for a while");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): all of composing string should be masked after a while");
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+
+ // Committing composition should make the commit string unmasked.
+ passwordElement.value = "ab";
+ passwordElement.focus();
+ passwordElement.setSelectionRange(1, 1);
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeCompositionChange(
+ { composition:
+ { string: "cd",
+ clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }],
+ },
+ caret: { start: 2, length: 0 },
+ });
+ await waitForMaskingLastInput;
+ waitForMaskingLastInput = promiseAllPasswordMasked();
+ synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });
+ unmaskedResult = await snapshotWindow(passwordWindow, true);
+ await waitForMaskingLastInput;
+ maskedResult = await snapshotWindow(passwordWindow, true);
+
+ inputElement.value = `${kMask}cd${kMask}`;
+ inputElement.focus();
+ inputElement.setSelectionRange(3, 3);
+ unmaskedReference = await snapshotWindow(inputWindow, true);
+ inputElement.value = `${kMask}${kMask}${kMask}${kMask}`;
+ inputElement.setSelectionRange(3, 3);
+ maskedReference = await snapshotWindow(inputWindow, true);
+ checkSnapshots(unmaskedResult, unmaskedReference, true,
+ "runPasswordMaskDelayTest(): committed string should be unmasked for a while");
+ checkSnapshots(maskedResult, maskedReference, true,
+ "runPasswordMaskDelayTest(): committed string should be masked after a while");
+}
+
+async function runInputModeTest()
+{
+ let result = [];
+
+ function handler(aEvent)
+ {
+ result.push(aEvent);
+ }
+
+ textarea.inputMode = "text";
+ textarea.value = "";
+ textarea.focus();
+
+ textarea.addEventListener("compositionupdate", handler, true);
+ textarea.addEventListener("compositionend", handler, true);
+
+ synthesizeCompositionChange({
+ composition: {string: "a ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
+ });
+
+ is(result[0].type, "compositionupdate", "Set initial composition for inputmode test");
+ result = [];
+
+ textarea.inputMode = "tel";
+ is(result.length, 0, "No compositonend event even if inputmode is updated");
+
+ // Clean up
+ synthesizeComposition({ type: "compositioncommitasis" });
+ textarea.inputMode = "";
+ textarea.value = "";
+ textarea.removeEventListener("compositionupdate", handler, true);
+ textarea.removeEventListener("compositionend", handler, true);
+}
+
+
+async function runTest()
+{
+ window.addEventListener("unload", window.arguments[0].SimpleTest.finish, {once: true, capture: true});
+
+ contenteditable = document.getElementById("iframe4").contentDocument.getElementById("contenteditable");
+ windowOfContenteditable = document.getElementById("iframe4").contentWindow;
+ textareaInFrame = iframe.contentDocument.getElementById("textarea");
+
+ contenteditableBySpan = document.getElementById("iframe7").contentDocument.getElementById("contenteditable");
+ windowOfContenteditableBySpan = document.getElementById("iframe7").contentWindow;
+
+ await runIMEContentObserverTest();
+ await runEditorReframeTests();
+ await runAsyncForceCommitTest();
+ await runRemoveContentTest();
+ await runPanelTest();
+ await runPasswordMaskDelayTest();
+ await runBug1584901Test();
+ await runInputModeTest();
+ await runCompositionTest();
+ await runCompositionCommitTest();
+ await runSetSelectionEventTest();
+
+ runUndoRedoTest();
+ runCompositionCommitAsIsTest();
+ runCompositionEventTest();
+ runCompositionTestWhoseTextNodeModified();
+ runQueryTextRectInContentEditableTest();
+ runCharAtPointTest(textarea, "textarea in the document");
+ runCharAtPointAtOutsideTest();
+ runQueryTextContentEventTest();
+ runQuerySelectionEventTest();
+ runQueryIMESelectionTest();
+ runQueryContentEventRelativeToInsertionPoint();
+ runQueryPasswordTest();
+ runCSSTransformTest();
+ runBug722639Test();
+ runBug1375825Test();
+ runBug1530649Test();
+ runBug1571375Test();
+ runBug1675313Test();
+ runCommitCompositionWithSpaceKey();
+ runCompositionWithSelectionChange();
+ runForceCommitTest();
+ runNestedSettingValue();
+ runBug811755Test();
+ runIsComposingTest();
+ runRedundantChangeTest();
+ runNotRedundantChangeTest();
+ runNativeLineBreakerTest();
+ runControlCharTest();
+ runFrameTest();
+ runMaxLengthTest();
+
+ window.close();
+}
+
+window.arguments[0].SimpleTest.waitForFocus(runTest, window);
+
+]]>
+</script>
+
+</window>
diff --git a/widget/tests/window_imestate_iframes.html b/widget/tests/window_imestate_iframes.html
new file mode 100644
index 0000000000..c8b182977f
--- /dev/null
+++ b/widget/tests/window_imestate_iframes.html
@@ -0,0 +1,358 @@
+<html>
+<head>
+ <title>Test for IME state controling and focus moving for iframes</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <style type="text/css">
+ iframe {
+ border: none;
+ height: 100px;
+ }
+ </style>
+</head>
+<body onunload="onUnload();">
+<p id="display">
+ <!-- Use input[readonly] because it isn't affected by the partial focus
+ movement on Mac -->
+ <input id="prev" readonly><br>
+ <iframe id="iframe_not_editable"
+ src="data:text/html,&lt;html&gt;&lt;body&gt;&lt;input id='editor'&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><br>
+
+ <!-- Testing IME state and focus movement, the anchor elements cannot get focus -->
+ <iframe id="iframe_html"
+ src="data:text/html,&lt;html id='editor' contenteditable='true'&gt;&lt;body&gt;&lt;a href='about:blank'&gt;about:blank;&lt;/a&gt;&lt;/body&gt;&lt;/html&gt;"></iframe><br>
+ <iframe id="iframe_designMode"
+ src="data:text/html,&lt;body id='editor' onload='document.designMode=&quot;on&quot;;'&gt;&lt;a href='about:blank'&gt;about:blank;&lt;/a&gt;&lt;/body&gt;"></iframe><br>
+ <iframe id="iframe_body"
+ src="data:text/html,&lt;body id='editor' contenteditable='true'&gt;&lt;a href='about:blank'&gt;about:blank;&lt;/a&gt;&lt;/body&gt;"></iframe><br>
+ <iframe id="iframe_p"
+ src="data:text/html,&lt;body&gt;&lt;p id='editor' contenteditable='true'&gt;&lt;a href='about:blank'&gt;about:blank;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;"></iframe><br>
+
+ <input id="next" readonly><br>
+</p>
+<script class="testbody" type="application/javascript">
+
+window.opener.SimpleTest.waitForFocus(runTests, window);
+
+function ok(aCondition, aMessage) {
+ window.opener.SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage) {
+ window.opener.SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function onUnload() {
+ window.opener.onFinish();
+}
+
+var gFocusObservingElement = null;
+var gBlurObservingElement = null;
+
+function onFocus(aEvent) {
+ if (aEvent.target != gFocusObservingElement) {
+ return;
+ }
+ ok(gFocusObservingElement.willFocus,
+ "focus event is fired on unexpected element");
+ gFocusObservingElement.willFocus = false;
+}
+
+function onBlur(aEvent) {
+ if (aEvent.target != gBlurObservingElement) {
+ return;
+ }
+ ok(gBlurObservingElement.willBlur,
+ "blur event is fired on unexpected element");
+ gBlurObservingElement.willBlur = false;
+}
+
+function observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent,
+ aNextBlurredNode, aWillFireBlurEvent) {
+ if (gFocusObservingElement) {
+ if (gFocusObservingElement.willFocus) {
+ ok(false, "focus event was never fired on " + gFocusObservingElement);
+ }
+ gFocusObservingElement.removeEventListener("focus", onFocus, true);
+ gFocusObservingElement.willFocus = NaN;
+ gFocusObservingElement = null;
+ }
+ if (gBlurObservingElement) {
+ if (gBlurObservingElement.willBlur) {
+ ok(false, "blur event was never fired on " + gBlurObservingElement);
+ }
+ gBlurObservingElement.removeEventListener("blur", onBlur, true);
+ gBlurObservingElement.willBlur = NaN;
+ gBlurObservingElement = null;
+ }
+ if (aNextFocusedNode) {
+ gFocusObservingElement = aNextFocusedNode;
+ gFocusObservingElement.willFocus = aWillFireFocusEvent;
+ gFocusObservingElement.addEventListener("focus", onFocus, true);
+ }
+ if (aNextBlurredNode) {
+ gBlurObservingElement = aNextBlurredNode;
+ gBlurObservingElement.willBlur = aWillFireBlurEvent;
+ gBlurObservingElement.addEventListener("blur", onBlur, true);
+ }
+}
+
+function runTests() {
+ var utils = window.windowUtils;
+ var fm = Services.focus;
+
+ var iframe, editor, root;
+ var prev = document.getElementById("prev");
+ var next = document.getElementById("next");
+ var html = document.documentElement;
+
+ function resetFocusToInput(aDescription) {
+ observeFocusBlur(null, false, null, false);
+ prev.focus();
+ is(fm.focusedElement, prev,
+ "input#prev[readonly] element didn't get focus: " + aDescription);
+ is(utils.IMEStatus, utils.IME_STATUS_DISABLED,
+ "IME enabled on input#prev[readonly]: " + aDescription);
+ }
+
+ function resetFocusToParentHTML(aDescription) {
+ observeFocusBlur(null, false, null, false);
+ html.focus();
+ is(fm.focusedElement, html,
+ "Parent html element didn't get focus: " + aDescription);
+ is(utils.IMEStatus, utils.IME_STATUS_DISABLED,
+ "IME enabled on parent html element: " + aDescription);
+ }
+
+ function testTabKey(aForward,
+ aNextFocusedNode, aWillFireFocusEvent,
+ aNextBlurredNode, aWillFireBlurEvent,
+ aIMEShouldBeEnabled, aTestingCaseDescription) {
+ observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent,
+ aNextBlurredNode, aWillFireBlurEvent);
+ synthesizeKey("VK_TAB", { shiftKey: !aForward });
+ var description = "Tab key test: ";
+ if (!aForward) {
+ description = "Shift-" + description;
+ }
+ description += aTestingCaseDescription + ": ";
+ is(fm.focusedElement, aNextFocusedNode,
+ description + "didn't move focus as expected");
+ is(utils.IMEStatus,
+ aIMEShouldBeEnabled ?
+ utils.IME_STATUS_ENABLED : utils.IME_STATUS_DISABLED,
+ description + "didn't set IME state as expected");
+ }
+
+ function testMouseClick(aNextFocusedNode, aWillFireFocusEvent,
+ aWillAllNodeLostFocus,
+ aNextBlurredNode, aWillFireBlurEvent,
+ aIMEShouldBeEnabled, aTestingCaseDescription) {
+ observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent,
+ aNextBlurredNode, aWillFireBlurEvent);
+ // We're relying on layout inside the iframe being up to date, so make it so
+ iframe.contentDocument.documentElement.getBoundingClientRect();
+ synthesizeMouse(iframe, 10, 80, { });
+ var description = "Click test: " + aTestingCaseDescription + ": ";
+ is(fm.focusedElement, !aWillAllNodeLostFocus ? aNextFocusedNode : null,
+ description + "didn't move focus as expected");
+ is(utils.IMEStatus,
+ aIMEShouldBeEnabled ?
+ utils.IME_STATUS_ENABLED : utils.IME_STATUS_DISABLED,
+ description + "didn't set IME state as expected");
+ }
+
+ function testOnEditorFlagChange(aDescription, aIsInDesignMode) {
+ const kReadonly = Ci.nsIEditor.eEditorReadonlyMask;
+ var description = "testOnEditorFlagChange: " + aDescription;
+ resetFocusToParentHTML(description);
+ var htmlEditor = iframe.contentWindow.docShell.editor;
+ var e = aIsInDesignMode ? root : editor;
+ e.focus();
+ is(fm.focusedElement, e,
+ description + ": focus() of editor didn't move focus as expected");
+ is(utils.IMEStatus, utils.IME_STATUS_ENABLED,
+ description + ": IME isn't enabled when the editor gets focus");
+ var flags = htmlEditor.flags;
+ htmlEditor.flags |= kReadonly;
+ is(fm.focusedElement, e,
+ description + ": when editor becomes readonly, focus moved unexpectedly");
+ is(utils.IMEStatus, utils.IME_STATUS_DISABLED,
+ description + ": when editor becomes readonly, IME is still enabled");
+ htmlEditor.flags = flags;
+ is(fm.focusedElement, e,
+ description + ": when editor becomes read-write, focus moved unexpectedly");
+ is(utils.IMEStatus, utils.IME_STATUS_ENABLED,
+ description + ": when editor becomes read-write, IME is still disabled");
+ }
+
+ // hide all iframes
+ document.getElementById("iframe_not_editable").style.display = "none";
+ document.getElementById("iframe_html").style.display = "none";
+ document.getElementById("iframe_designMode").style.display = "none";
+ document.getElementById("iframe_body").style.display = "none";
+ document.getElementById("iframe_p").style.display = "none";
+
+ // non editable HTML element and input element can get focus.
+ iframe = document.getElementById("iframe_not_editable");
+ iframe.style.display = "inline";
+ editor = iframe.contentDocument.getElementById("editor");
+ root = iframe.contentDocument.documentElement;
+ resetFocusToInput("initializing for iframe_not_editable");
+
+ testTabKey(true, root, false, prev, true,
+ false, "input#prev[readonly] -> html");
+ testTabKey(true, editor, true, root, false,
+ true, "html -> input in the subdoc");
+ testTabKey(true, next, true, editor, true,
+ false, "input in the subdoc -> input#next[readonly]");
+ testTabKey(false, editor, true, next, true,
+ true, "input#next[readonly] -> input in the subdoc");
+ testTabKey(false, root, false, editor, true,
+ false, "input in the subdoc -> html");
+ testTabKey(false, prev, true, root, false,
+ false, "html -> input#next[readonly]");
+
+ iframe.style.display = "none";
+
+ // HTML element (of course, it's root) must enables IME.
+ iframe = document.getElementById("iframe_html");
+ iframe.style.display = "inline";
+ editor = iframe.contentDocument.getElementById("editor");
+ root = iframe.contentDocument.documentElement;
+ resetFocusToInput("initializing for iframe_html");
+
+ testTabKey(true, editor, true, prev, true,
+ true, "input#prev[readonly] -> html[contentediable=true]");
+ testTabKey(true, next, true, editor, true,
+ false, "html[contentediable=true] -> input#next[readonly]");
+ testTabKey(false, editor, true, next, true,
+ true, "input#next[readonly] -> html[contentediable=true]");
+ testTabKey(false, prev, true, editor, true,
+ false, "html[contenteditable=true] -> input[readonly]");
+
+ prev.style.display = "none";
+ resetFocusToParentHTML("testing iframe_html");
+ testTabKey(true, editor, true, html, false,
+ true, "html of parent -> html[contentediable=true]");
+ testTabKey(false, html, false, editor, true,
+ false, "html[contenteditable=true] -> html of parent");
+ prev.style.display = "inline";
+ resetFocusToInput("after parent html <-> html[contenteditable=true]");
+
+ testMouseClick(editor, true, false, prev, true, true, "iframe_html");
+
+ testOnEditorFlagChange("html[contentediable=true]", false);
+
+ iframe.style.display = "none";
+
+ // designMode should behave like <html contenteditable="true"></html>
+ // but focus/blur events shouldn't be fired on its root element because
+ // any elements shouldn't be focused state in designMode.
+ iframe = document.getElementById("iframe_designMode");
+ iframe.style.display = "inline";
+ iframe.contentDocument.designMode = "on";
+ editor = iframe.contentDocument.getElementById("editor");
+ root = iframe.contentDocument.documentElement;
+ resetFocusToInput("initializing for iframe_designMode");
+
+ testTabKey(true, root, false, prev, true,
+ true, "input#prev[readonly] -> html in designMode");
+ testTabKey(true, next, true, root, false,
+ false, "html in designMode -> input#next[readonly]");
+ testTabKey(false, root, false, next, true,
+ true, "input#next[readonly] -> html in designMode");
+ testTabKey(false, prev, true, root, false,
+ false, "html in designMode -> input#prev[readonly]");
+
+ prev.style.display = "none";
+ resetFocusToParentHTML("testing iframe_designMode");
+ testTabKey(true, root, false, html, false,
+ true, "html of parent -> html in designMode");
+ testTabKey(false, html, false, root, false,
+ false, "html in designMode -> html of parent");
+ prev.style.display = "inline";
+ resetFocusToInput("after parent html <-> html in designMode");
+
+ testMouseClick(editor, false, true, prev, true, true, "iframe_designMode");
+
+ testOnEditorFlagChange("html in designMode", true);
+
+ iframe.style.display = "none";
+
+ // When there is no HTML element but the BODY element is editable,
+ // the body element should get focus and enables IME.
+ iframe = document.getElementById("iframe_body");
+ iframe.style.display = "inline";
+ editor = iframe.contentDocument.getElementById("editor");
+ root = iframe.contentDocument.documentElement;
+ resetFocusToInput("initializing for iframe_body");
+
+ testTabKey(true, editor, true, prev, true,
+ true, "input#prev[readonly] -> body[contentediable=true]");
+ testTabKey(true, next, true, editor, true,
+ false, "body[contentediable=true] -> input#next[readonly]");
+ testTabKey(false, editor, true, next, true,
+ true, "input#next[readonly] -> body[contentediable=true]");
+ testTabKey(false, prev, true, editor, true,
+ false, "body[contenteditable=true] -> input#prev[readonly]");
+
+ prev.style.display = "none";
+ resetFocusToParentHTML("testing iframe_body");
+ testTabKey(true, editor, true, html, false,
+ true, "html of parent -> body[contentediable=true]");
+ testTabKey(false, html, false, editor, true,
+ false, "body[contenteditable=true] -> html of parent");
+ prev.style.display = "inline";
+ resetFocusToInput("after parent html <-> body[contenteditable=true]");
+
+ testMouseClick(editor, true, false, prev, true, true, "iframe_body");
+
+ testOnEditorFlagChange("body[contentediable=true]", false);
+
+ iframe.style.display = "none";
+
+ // When HTML/BODY elements are not editable, focus shouldn't be moved to
+ // the editable content directly.
+ iframe = document.getElementById("iframe_p");
+ iframe.style.display = "inline";
+ editor = iframe.contentDocument.getElementById("editor");
+ root = iframe.contentDocument.documentElement;
+ resetFocusToInput("initializing for iframe_p");
+
+ testTabKey(true, root, false, prev, true,
+ false, "input#prev[readonly] -> html (has p[contenteditable=true])");
+ testTabKey(true, editor, true, root, false,
+ true, "html (has p[contenteditable=true]) -> p[contentediable=true]");
+ testTabKey(true, next, true, editor, true,
+ false, "p[contentediable=true] -> input#next[readonly]");
+ testTabKey(false, editor, true, next, true,
+ true, "input#next[readonly] -> p[contentediable=true]");
+ testTabKey(false, root, false, editor, true,
+ false, "p[contenteditable=true] -> html (has p[contenteditable=true])");
+ testTabKey(false, prev, true, root, false,
+ false, "html (has p[contenteditable=true]) -> input#prev[readonly]");
+ prev.style.display = "none";
+
+ resetFocusToParentHTML("testing iframe_p");
+ testTabKey(true, root, false, html, false,
+ false, "html of parent -> html (has p[contentediable=true])");
+ testTabKey(false, html, false, root, false,
+ false, "html (has p[contentediable=true]) -> html of parent");
+ prev.style.display = "inline";
+ resetFocusToInput("after parent html <-> html (has p[contentediable=true])");
+
+ testMouseClick(root, false, true, prev, true, false, "iframe_p");
+
+ testOnEditorFlagChange("p[contenteditable=true]", false);
+
+ iframe.style.display = "none";
+
+ window.close();
+}
+
+</script>
+</body>
+
+</html>
diff --git a/widget/tests/window_mouse_scroll_win.html b/widget/tests/window_mouse_scroll_win.html
new file mode 100644
index 0000000000..bf90abb1b5
--- /dev/null
+++ b/widget/tests/window_mouse_scroll_win.html
@@ -0,0 +1,1516 @@
+<html lang="en-US"
+ style="font-family: Arial; font-size: 10px; line-height: 16px;">
+<head>
+ <title>Test for mouse scroll handling on Windows</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body onunload="onUnload();">
+<div id="display" style="width: 5000px; height: 5000px;">
+<p id="p1" style="font-size: 16px; width: 100px; height: 100px;">1st &lt;p&gt;.</p>
+<p id="p2" style="font-size: 32px; width: 100px; height: 100px;">2nd &lt;p&gt;.</p>
+</div>
+<script class="testbody" type="application/javascript">
+
+window.arguments[0].SimpleTest.waitForFocus(prepareTests, window);
+
+const nsIDOMWindowUtils = Ci.nsIDOMWindowUtils;
+
+const WHEEL_PAGESCROLL = 4294967295;
+
+const WM_VSCROLL = 0x0115;
+const WM_HSCROLL = 0x0114;
+const WM_MOUSEWHEEL = 0x020A;
+const WM_MOUSEHWHEEL = 0x020E;
+
+const SB_LINEUP = 0;
+const SB_LINELEFT = 0;
+const SB_LINEDOWN = 1;
+const SB_LINERIGHT = 1;
+const SB_PAGEUP = 2;
+const SB_PAGELEFT = 2;
+const SB_PAGEDOWN = 3;
+const SB_PAGERIGHT = 3;
+
+const SHIFT_L = 0x0100;
+const SHIFT_R = 0x0200;
+const CTRL_L = 0x0400;
+const CTRL_R = 0x0800;
+const ALT_L = 0x1000;
+const ALT_R = 0x2000;
+
+const DOM_PAGE_SCROLL_DELTA = 32768;
+
+const kSystemScrollSpeedOverridePref = "mousewheel.system_scroll_override.enabled";
+
+const kAltKeyActionPref = "mousewheel.with_alt.action";
+const kCtrlKeyActionPref = "mousewheel.with_control.action";
+const kShiftKeyActionPref = "mousewheel.with_shift.action";
+const kWinKeyActionPref = "mousewheel.with_meta.action";
+
+const kAltKeyDeltaMultiplierXPref = "mousewheel.with_alt.delta_multiplier_x";
+const kAltKeyDeltaMultiplierYPref = "mousewheel.with_alt.delta_multiplier_y";
+const kCtrlKeyDeltaMultiplierXPref = "mousewheel.with_control.delta_multiplier_x";
+const kCtrlKeyDeltaMultiplierYPref = "mousewheel.with_control.delta_multiplier_y";
+const kShiftKeyDeltaMultiplierXPref = "mousewheel.with_shift.delta_multiplier_x";
+const kShiftKeyDeltaMultiplierYPref = "mousewheel.with_shift.delta_multiplier_y";
+const kWinKeyDeltaMultiplierXPref = "mousewheel.with_meta.delta_multiplier_x";
+const kWinKeyDeltaMultiplierYPref = "mousewheel.with_meta.delta_multiplier_y";
+
+const kEmulateWheelByWMSCROLLPref = "mousewheel.emulate_at_wm_scroll";
+const kVAmountPref = "mousewheel.windows.vertical_amount_override";
+const kHAmountPref = "mousewheel.windows.horizontal_amount_override";
+const kTimeoutPref = "mousewheel.windows.transaction.timeout";
+
+const kMouseLineScrollEvent = "DOMMouseScroll";
+const kMousePixelScrollEvent = "MozMousePixelScroll";
+
+const kVAxis = MouseScrollEvent.VERTICAL_AXIS;
+const kHAxis = MouseScrollEvent.HORIZONTAL_AXIS;
+
+var gLineHeight = 0;
+var gCharWidth = 0;
+var gPageHeight = 0;
+var gPageWidth = 0;
+
+var gP1 = document.getElementById("p1");
+var gP2 = document.getElementById("p2");
+
+var gOtherWindow;
+
+function ok(aCondition, aMessage) {
+ window.arguments[0].SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage) {
+ window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function isnot(aLeft, aRight, aMessage) {
+ window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage);
+}
+
+function todo_is(aLeft, aRight, aMessage) {
+ window.arguments[0].SimpleTest.todo_is(aLeft, aRight, aMessage);
+}
+
+function onUnload() {
+ SpecialPowers.clearUserPref(kAltKeyActionPref);
+ SpecialPowers.clearUserPref(kCtrlKeyActionPref);
+ SpecialPowers.clearUserPref(kShiftKeyActionPref);
+ SpecialPowers.clearUserPref(kWinKeyActionPref);
+
+ SpecialPowers.clearUserPref(kAltKeyDeltaMultiplierXPref);
+ SpecialPowers.clearUserPref(kAltKeyDeltaMultiplierYPref);
+ SpecialPowers.clearUserPref(kCtrlKeyDeltaMultiplierXPref);
+ SpecialPowers.clearUserPref(kCtrlKeyDeltaMultiplierYPref);
+ SpecialPowers.clearUserPref(kShiftKeyDeltaMultiplierXPref);
+ SpecialPowers.clearUserPref(kShiftKeyDeltaMultiplierYPref);
+ SpecialPowers.clearUserPref(kWinKeyDeltaMultiplierXPref);
+ SpecialPowers.clearUserPref(kWinKeyDeltaMultiplierYPref);
+
+ SpecialPowers.clearUserPref(kSystemScrollSpeedOverridePref);
+ SpecialPowers.clearUserPref(kEmulateWheelByWMSCROLLPref);
+ SpecialPowers.clearUserPref(kVAmountPref);
+ SpecialPowers.clearUserPref(kHAmountPref);
+ SpecialPowers.clearUserPref(kTimeoutPref);
+ window.arguments[0].SimpleTest.finish();
+}
+
+function getWindowUtils(aWindow) {
+ if (!aWindow) {
+ aWindow = window;
+ }
+ return aWindow.windowUtils;
+}
+
+function getPointInScreen(aElement, aWindow) {
+ if (!aWindow) {
+ aWindow = window;
+ }
+ var bounds = aElement.getBoundingClientRect();
+ return { x: bounds.left + aWindow.mozInnerScreenX,
+ y: bounds.top + aWindow.mozInnerScreenY };
+}
+
+function cut(aNum) {
+ return (aNum >= 0) ? Math.floor(aNum) : Math.ceil(aNum);
+}
+
+/**
+ * Make each steps for the tests in following arrays in global scope. Each item
+ * of the arrays will be executed after previous test is finished.
+ *
+ * description:
+ * Set the description of the test. This will be used for the message of is()
+ * or the others.
+ *
+ * message:
+ * aNativeMessage of nsIDOMWindowUtils.sendNativeMouseScrollEvent().
+ * Must be WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_VSCROLL or WM_HSCROLL.
+ *
+ * delta:
+ * The native delta value for WM_MOUSEWHEEL or WM_MOUSEHWHEEL.
+ * Or one of the SB_* const value for WM_VSCROLL or WM_HSCROLL.
+ *
+ * target:
+ * The target element, under the mouse cursor.
+ *
+ * window:
+ * The window which is used for getting nsIDOMWindowUtils.
+ *
+ * modifiers:
+ * Pressed modifier keys, 0 means no modifier key is pressed.
+ * Otherwise, one or more values of SHIFT_L, SHIFT_R, CTRL_L, CTRL_R,
+ * ALT_L or ALT_R.
+ *
+ * additionalFlags:
+ * aAdditionalFlags of nsIDOMWindowUtils.sendNativeMouseScrollEvent().
+ * See the document of nsIDOMWindowUtils for the detail of the values.
+ *
+ * onLineScrollEvent:
+ * Must be a function or null.
+ * If the value is a function, it will be called when DOMMouseScroll event
+ * is received by the synthesized event.
+ * If return true, the common checks are canceled.
+ *
+ * onPixelScrollEvent:
+ * Must be a function or null.
+ * If the value is a function, it will be called when MozMousePixelScroll
+ * event is received by the synthesized event.
+ * If return true, the common checks are canceled.
+ *
+ * expected:
+ * Must not be null and this must have:
+ * axis:
+ * kVAxis if the synthesized event causes vertical scroll. Otherwise,
+ * it causes horizontal scroll, kHAxis.
+ * lines:
+ * Integer value which is expected detail attribute value of
+ * DOMMouseScroll. If the event shouldn't be fired, must be 0.
+ * pixels:
+ * Integer value or a function which returns double value. The value is
+ * expected detail attribute value of MozMousePixelScroll.
+ * If the event shouldn't be fired, must be 0.
+ *
+ * Note that if both lines and pixels are 0, the test framework waits
+ * a few seconds. After that, go to next test.
+ *
+ * init:
+ * Must be a function or null. If this value is a function, it's called
+ * before synthesizing the native event.
+ *
+ * finish:
+ * Must be a function or null. If this value is a function, it's called
+ * after received all expected events or timeout if no events are expected.
+ */
+
+// First, get the computed line height, char width, page height and page width.
+var gPreparingSteps = [
+ { description: "Preparing gLineHeight",
+ message: WM_MOUSEWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ onLineScrollEvent(aEvent) {
+ return true;
+ },
+ onPixelScrollEvent(aEvent) {
+ gLineHeight = aEvent.detail;
+ return true;
+ },
+ expected: {
+ axis: kVAxis, lines: 1, pixels: 1,
+ },
+ init() {
+ SpecialPowers.setIntPref(kVAmountPref, 1);
+ SpecialPowers.setIntPref(kHAmountPref, 1);
+ },
+ },
+ { description: "Preparing gCharWidth",
+ message: WM_MOUSEHWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ onLineScrollEvent(aEvent) {
+ return true;
+ },
+ onPixelScrollEvent(aEvent) {
+ gCharWidth = aEvent.detail;
+ return true;
+ },
+ expected: {
+ axis: kVAxis, lines: 1, pixels: 1,
+ },
+ },
+ { description: "Preparing gPageHeight",
+ message: WM_MOUSEWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ onLineScrollEvent(aEvent) {
+ return true;
+ },
+ onPixelScrollEvent(aEvent) {
+ gPageHeight = aEvent.detail;
+ return true;
+ },
+ expected: {
+ axis: kHAxis, lines: 1, pixels: 1,
+ },
+ init() {
+ SpecialPowers.setIntPref(kVAmountPref, 0xFFFF);
+ SpecialPowers.setIntPref(kHAmountPref, 0xFFFF);
+ },
+ },
+ { description: "Preparing gPageWidth",
+ message: WM_MOUSEHWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ onLineScrollEvent(aEvent) {
+ return true;
+ },
+ onPixelScrollEvent(aEvent) {
+ gPageWidth = aEvent.detail;
+ return true;
+ },
+ expected: {
+ axis: kHAxis, lines: 1, pixels: 1,
+ },
+ finish() {
+ ok(gLineHeight > 0, "gLineHeight isn't positive got " + gLineHeight);
+ ok(gCharWidth > 0, "gCharWidth isn't positive got " + gCharWidth);
+ ok(gPageHeight > 0, "gPageHeight isn't positive got " + gPageHeight);
+ ok(gPageWidth > 0, "gPageWidth isn't positive got " + gPageWidth);
+
+ ok(gPageHeight > gLineHeight,
+ "gPageHeight must be larger than gLineHeight");
+ ok(gPageWidth > gCharWidth,
+ "gPageWidth must be larger than gCharWidth");
+ runNextTest(gBasicTests, 0);
+ },
+ },
+];
+
+var gBasicTests = [
+ // Widget shouldn't dispatch a pixel event if the delta can be devided by
+ // lines to be scrolled. However, pixel events should be fired by ESM.
+ { description: "WM_MOUSEWHEEL, -120, 3 lines",
+ message: WM_MOUSEWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; },
+ },
+ init() {
+ SpecialPowers.setIntPref(kVAmountPref, 3);
+ SpecialPowers.setIntPref(kHAmountPref, 3);
+ },
+ },
+
+ { description: "WM_MOUSEWHEEL, 120, -3 lines",
+ message: WM_MOUSEWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, 120, 3 chars",
+ message: WM_MOUSEHWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, -120, -3 chars",
+ message: WM_MOUSEHWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; },
+ },
+ },
+
+ // Pixel scroll event should be fired always but line scroll event should be
+ // fired only when accumulated delta value is over a line.
+ { description: "WM_MOUSEWHEEL, -20, 0.5 lines",
+ message: WM_MOUSEWHEEL, delta: -20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gLineHeight / 2; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -20, 0.5 lines (pending: 0.5 lines)",
+ message: WM_MOUSEWHEEL, delta: -20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight / 2; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -20, 0.5 lines",
+ message: WM_MOUSEWHEEL, delta: -20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gLineHeight / 2; },
+ },
+ },
+
+ { description: "WM_MOUSEWHEEL, 20, -0.5 lines (pending: 0.5 lines)",
+ message: WM_MOUSEWHEEL, delta: 20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gLineHeight / -2; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, 20, -0.5 lines (pending: -0.5 lines)",
+ message: WM_MOUSEWHEEL, delta: 20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -1, pixels() { return -gLineHeight / 2; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, 20, -0.5 lines",
+ message: WM_MOUSEWHEEL, delta: 20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gLineHeight / -2; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, 20, 0.5 chars",
+ message: WM_MOUSEHWHEEL, delta: 20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gCharWidth / 2; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 20, 0.5 chars (pending: 0.5 chars)",
+ message: WM_MOUSEHWHEEL, delta: 20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth / 2; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 20, 0.5 chars",
+ message: WM_MOUSEHWHEEL, delta: 20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gCharWidth / 2; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, -20, -0.5 chars (pending: 0.5 chars)",
+ message: WM_MOUSEHWHEEL, delta: -20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, -20, -0.5 chars (pending: -0.5 chars)",
+ message: WM_MOUSEHWHEEL, delta: -20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -1, pixels() { return -gCharWidth / 2; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, -20, -0.5 chars",
+ message: WM_MOUSEHWHEEL, delta: -20,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; },
+ },
+ },
+
+ // Even if the mouse cursor is an element whose font-size is different than
+ // the scrollable element, the pixel scroll amount shouldn't be changed.
+ // Widget shouldn't dispatch a pixel event if the delta can be devided by
+ // lines to be scrolled. However, pixel events should be fired by ESM.
+ { description: "WM_MOUSEWHEEL, -120, 3 lines, on the other div whose font-size is larger",
+ message: WM_MOUSEWHEEL, delta: -120,
+ target: gP2, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; },
+ },
+ },
+
+ { description: "WM_MOUSEWHEEL, 120, -3 lines, on the other div whose font-size is larger",
+ message: WM_MOUSEWHEEL, delta: 120,
+ target: gP2, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, 120, 3 chars, on the other div whose font-size is larger",
+ message: WM_MOUSEHWHEEL, delta: 120,
+ target: gP2, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, -120, -3 chars, on the other div whose font-size is larger",
+ message: WM_MOUSEHWHEEL, delta: -120,
+ target: gP2, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; },
+ },
+ },
+
+ // Modifier key tests
+ { description: "WM_MOUSEWHEEL, -40, 1 line with left Shift",
+ message: WM_MOUSEWHEEL, delta: -40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_L,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -40, 1 line with right Shift",
+ message: WM_MOUSEWHEEL, delta: -40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_R,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -40, 1 line with left Ctrl",
+ message: WM_MOUSEWHEEL, delta: -40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_L,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -40, 1 line with right Ctrl",
+ message: WM_MOUSEWHEEL, delta: -40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_R,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -40, 1 line with left Alt",
+ message: WM_MOUSEWHEEL, delta: -40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_L,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -40, 1 line with right Alt",
+ message: WM_MOUSEWHEEL, delta: -40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_R,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, 40, 1 character with left Shift",
+ message: WM_MOUSEHWHEEL, delta: 40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_L,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 40, 1 character with right Shift",
+ message: WM_MOUSEHWHEEL, delta: 40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_R,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 40, 1 character with left Ctrl",
+ message: WM_MOUSEHWHEEL, delta: 40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_L,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 40, 1 character with right Ctrl",
+ message: WM_MOUSEHWHEEL, delta: 40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_R,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 40, 1 character with left Alt",
+ message: WM_MOUSEHWHEEL, delta: 40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_L,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 40, 1 character with right Alt",
+ message: WM_MOUSEHWHEEL, delta: 40,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_R,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+
+ finish() {
+ runNextTest(gScrollMessageTests, 0);
+ },
+ },
+];
+
+var gPageScrllTests = [
+ // Pixel scroll event should be fired always but line scroll event should be
+ // fired only when accumulated delta value is over a line.
+ { description: "WM_MOUSEWHEEL, -60, 0.5 pages",
+ message: WM_MOUSEWHEEL, delta: -60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gPageHeight / 2; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -60, 0.5 pages (pending: 0.5 pages)",
+ message: WM_MOUSEWHEEL, delta: -60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: DOM_PAGE_SCROLL_DELTA,
+ pixels() { return ((gPageHeight / 2) + (gPageHeight % 2)); },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, -60, 0.5 pages",
+ message: WM_MOUSEWHEEL, delta: -60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gPageHeight / 2; },
+ },
+ },
+
+ { description: "WM_MOUSEWHEEL, 60, -0.5 pages (pending: 0.5 pages)",
+ message: WM_MOUSEWHEEL, delta: 60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gPageHeight / -2; },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, 60, -0.5 pages (pending: -0.5 pages)",
+ message: WM_MOUSEWHEEL, delta: 60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -DOM_PAGE_SCROLL_DELTA,
+ pixels() { return -((gPageHeight / 2) + (gPageHeight % 2)); },
+ },
+ },
+ { description: "WM_MOUSEWHEEL, 60, -0.5 pages",
+ message: WM_MOUSEWHEEL, delta: 60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels() { return gPageHeight / -2; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, 60, 0.5 pages",
+ message: WM_MOUSEHWHEEL, delta: 60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gPageWidth / 2; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 60, 0.5 pages (pending: 0.5 pages)",
+ message: WM_MOUSEHWHEEL, delta: 60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: DOM_PAGE_SCROLL_DELTA,
+ pixels() { return ((gPageWidth / 2) + (gPageWidth % 2)); },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, 60, 0.5 pages",
+ message: WM_MOUSEHWHEEL, delta: 60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gPageWidth / 2; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, -60, -0.5 pages (pending: 0.5 pages)",
+ message: WM_MOUSEHWHEEL, delta: -60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, -60, -0.5 pages (pending: -0.5 pages)",
+ message: WM_MOUSEHWHEEL, delta: -60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -DOM_PAGE_SCROLL_DELTA,
+ pixels() { return -((gCharWidth / 2) + (gCharWidth % 2)); },
+ },
+ },
+ { description: "WM_MOUSEHWHEEL, -60, -0.5 pages",
+ message: WM_MOUSEHWHEEL, delta: -60,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; },
+ },
+ },
+];
+
+var gScrollMessageTests = [
+ // Widget should dispatch neither line scroll event nor pixel scroll event if
+ // the WM_*SCROLL's lParam is NULL and mouse wheel emulation is disabled.
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation disabled",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels: 0,
+ },
+ init() {
+ SpecialPowers.setIntPref(kVAmountPref, 3);
+ SpecialPowers.setIntPref(kHAmountPref, 3);
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false);
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation disabled",
+ message: WM_VSCROLL, delta: SB_LINEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 0, pixels: 0,
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation disabled",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels: 0,
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation disabled",
+ message: WM_HSCROLL, delta: SB_LINELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 0, pixels: 0,
+ },
+ },
+
+ // Widget should emulate mouse wheel behavior for WM_*SCROLL even if the
+ // kEmulateWheelByWMSCROLLPref is disabled but the message's lParam is not
+ // NULL. Then, widget doesn't dispatch a pixel event for WM_*SCROLL messages,
+ // but ESM dispatches it instead.
+ { description: "WM_VSCROLL, SB_LINEUP, lParam is not NULL, emulation disabled",
+ message: WM_VSCROLL, delta: SB_LINEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: -1, pixels() { return -gLineHeight; },
+ },
+ init() {
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false);
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINELEFT, lParam is not NULL, emulation disabled",
+ message: WM_HSCROLL, delta: SB_LINELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: -1, pixels() { return -gCharWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_PAGEUP, lParam is not NULL, emulation disabled",
+ message: WM_VSCROLL, delta: SB_PAGEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: -DOM_PAGE_SCROLL_DELTA,
+ pixels() { return -gPageHeight; },
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_PAGEDOWN, lParam is not NULL, emulation disabled",
+ message: WM_VSCROLL, delta: SB_PAGEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: DOM_PAGE_SCROLL_DELTA,
+ pixels() { return gPageHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_PAGELEFT, lParam is not NULL, emulation disabled",
+ message: WM_HSCROLL, delta: SB_PAGELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: -DOM_PAGE_SCROLL_DELTA,
+ pixels() { return -gPageWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_PAGERIGHT, lParam is not NULL, emulation disabled",
+ message: WM_HSCROLL, delta: SB_PAGERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: DOM_PAGE_SCROLL_DELTA,
+ pixels() { return gPageWidth; },
+ },
+ },
+
+ // Widget should emulate mouse wheel behavior for WM_*SCROLL when the
+ // kEmulateWheelByWMSCROLLPref is enabled even if the message's lParam is
+ // NULL. Then, widget doesn't dispatch a pixel event for WM_*SCROLL messages,
+ // but ESM dispatches it instead.
+ { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation enabled",
+ message: WM_VSCROLL, delta: SB_LINEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -1, pixels() { return -gLineHeight; },
+ },
+ init() {
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, true);
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation enabled",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation enabled",
+ message: WM_HSCROLL, delta: SB_LINELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -1, pixels() { return -gCharWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation enabled",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_PAGEUP, lParam is NULL, emulation enabled",
+ message: WM_VSCROLL, delta: SB_PAGEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -DOM_PAGE_SCROLL_DELTA,
+ pixels() { return -gPageHeight; },
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_PAGEDOWN, lParam is NULL, emulation enabled",
+ message: WM_VSCROLL, delta: SB_PAGEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: DOM_PAGE_SCROLL_DELTA,
+ pixels() { return gPageHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_PAGELEFT, lParam is NULL, emulation enabled",
+ message: WM_HSCROLL, delta: SB_PAGELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -DOM_PAGE_SCROLL_DELTA,
+ pixels() { return -gPageWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_PAGERIGHT, lParam is NULL, emulation enabled",
+ message: WM_HSCROLL, delta: SB_PAGERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: DOM_PAGE_SCROLL_DELTA,
+ pixels() { return gPageWidth; },
+ },
+ },
+
+ // Modifier key tests for WM_*SCROLL
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with left Shift",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ init() {
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false);
+ },
+ },
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with right Shift",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_R,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with left Ctrl",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with right Ctrl",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with left Alt",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with right Alt",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_R,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with left Shift",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with right Shift",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: SHIFT_R,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with left Ctrl",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with right Ctrl",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: CTRL_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with left Alt",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_L,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with right Alt",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: ALT_R,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+
+ finish() {
+ runDeactiveWindowTests();
+ },
+ },
+];
+
+var gDeactiveWindowTests = [
+ // Typically, mouse drivers send wheel messages to focused window.
+ // However, we prefer to scroll a scrollable element under the mouse cursor.
+ { description: "WM_MOUSEWHEEL, -120, 3 lines, window is deactive",
+ message: WM_MOUSEWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; },
+ },
+ init() {
+ SpecialPowers.setIntPref(kVAmountPref, 3);
+ SpecialPowers.setIntPref(kHAmountPref, 3);
+ },
+ onLineScrollEvent(aEvent) {
+ var fm = Services.focus;
+ is(fm.activeWindow, gOtherWindow, "The other window isn't activated");
+ },
+ },
+
+ { description: "WM_MOUSEWHEEL, 120, -3 lines, window is deactive",
+ message: WM_MOUSEWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, 120, 3 chars, window is deactive",
+ message: WM_MOUSEHWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, -120, -3 chars, window is deactive",
+ message: WM_MOUSEHWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; },
+ },
+ },
+
+ // Of course, even if some drivers prefer the cursor position, we don't need
+ // to change anything.
+ { description: "WM_MOUSEWHEEL, -120, 3 lines, window is deactive (receive the message directly)",
+ message: WM_MOUSEWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; },
+ },
+ },
+
+ { description: "WM_MOUSEWHEEL, 120, -3 lines, window is deactive (receive the message directly)",
+ message: WM_MOUSEWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, 120, 3 chars, window is deactive (receive the message directly)",
+ message: WM_MOUSEHWHEEL, delta: 120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; },
+ },
+ },
+
+ { description: "WM_MOUSEHWHEEL, -120, -3 chars, window is deactive (receive the message directly)",
+ message: WM_MOUSEHWHEEL, delta: -120,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; },
+ },
+ },
+
+ // Same for WM_*SCROLL if lParam is not NULL
+ { description: "WM_VSCROLL, SB_LINEUP, lParam is not NULL, emulation disabled, window is deactive",
+ message: WM_VSCROLL, delta: SB_LINEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: -1, pixels() { return -gLineHeight; },
+ },
+ init() {
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false);
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, window is deactive",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINELEFT, lParam is not NULL, emulation disabled, window is deactive",
+ message: WM_HSCROLL, delta: SB_LINELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: -1, pixels() { return -gCharWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, window is deactive",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+
+ // Same for WM_*SCROLL if lParam is NULL but emulation is enabled
+ { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation enabled, window is deactive",
+ message: WM_VSCROLL, delta: SB_LINEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: -1, pixels() { return -gLineHeight; },
+ },
+ init() {
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, true);
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation enabled, window is deactive",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation enabled, window is deactive",
+ message: WM_HSCROLL, delta: SB_LINELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: -1, pixels() { return -gCharWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation enabled, window is deactive",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: 0,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+
+ // Same for WM_*SCROLL if lParam is not NULL and message sent to the deactive window directly
+ { description: "WM_VSCROLL, SB_LINEUP, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)",
+ message: WM_VSCROLL, delta: SB_LINEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL |
+ nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kVAxis, lines: -1, pixels() { return -gLineHeight; },
+ },
+ init() {
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false);
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL |
+ nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINELEFT, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)",
+ message: WM_HSCROLL, delta: SB_LINELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL |
+ nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kHAxis, lines: -1, pixels() { return -gCharWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL |
+ nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+ },
+
+ // Same for WM_*SCROLL if lParam is NULL but emulation is enabled, and message sent to the deactive window directly
+ { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation enabled, window is deactive (receive the message directly)",
+ message: WM_VSCROLL, delta: SB_LINEUP,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kVAxis, lines: -1, pixels() { return -gLineHeight; },
+ },
+ init() {
+ SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, true);
+ },
+ },
+
+ { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation enabled, window is deactive (receive the message directly)",
+ message: WM_VSCROLL, delta: SB_LINEDOWN,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kVAxis, lines: 1, pixels() { return gLineHeight; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation enabled, window is deactive (receive the message directly)",
+ message: WM_HSCROLL, delta: SB_LINELEFT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kHAxis, lines: -1, pixels() { return -gCharWidth; },
+ },
+ },
+
+ { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation enabled, window is deactive (receive the message directly)",
+ message: WM_HSCROLL, delta: SB_LINERIGHT,
+ target: gP1, x: 10, y: 10, window,
+ modifiers: 0,
+ additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT,
+ expected: {
+ axis: kHAxis, lines: 1, pixels() { return gCharWidth; },
+ },
+
+ finish() {
+ gOtherWindow.close();
+ gOtherWindow = null;
+ window.close();
+ },
+ },
+];
+
+function runDeactiveWindowTests() {
+ gOtherWindow = window.open("window_mouse_scroll_win_2.html", "_blank",
+ "chrome,width=100,height=100,top=700,left=700");
+
+ window.arguments[0].SimpleTest.waitForFocus(function() {
+ runNextTest(gDeactiveWindowTests, 0);
+ }, gOtherWindow);
+}
+
+function runNextTest(aTests, aIndex) {
+ if (aIndex > 0 && aTests[aIndex - 1] && aTests[aIndex - 1].finish) {
+ aTests[aIndex - 1].finish();
+ }
+
+ if (aTests.length == aIndex) {
+ return;
+ }
+
+ var test = aTests[aIndex++];
+ if (test.init) {
+ test.init();
+ }
+ test.handled = { lines: false, pixels: false };
+
+ switch (test.message) {
+ case WM_MOUSEWHEEL:
+ case WM_MOUSEHWHEEL:
+ case WM_VSCROLL:
+ case WM_HSCROLL:
+ var expectedLines = test.expected.lines;
+ var expectedPixels =
+ cut((typeof test.expected.pixels == "function") ?
+ test.expected.pixels() : test.expected.pixels);
+ var handler = function(aEvent) {
+ var doCommonTests = true;
+
+ if (!aEvent) {
+ ok(!test.handled.lines,
+ test.description + ", line scroll event has been handled");
+ ok(!test.handled.pixels,
+ test.description + ", pixel scroll event has been handled");
+ doCommonTests = false;
+ } else if (aEvent.type == kMouseLineScrollEvent) {
+ ok(!test.handled.lines,
+ test.description + ":(" + aEvent.type + "), same event has already been handled");
+ test.handled.lines = true;
+ isnot(expectedLines, 0,
+ test.description + ":(" + aEvent.type + "), event shouldn't be fired");
+ if (test.onLineScrollEvent && test.onLineScrollEvent(aEvent)) {
+ doCommonTests = false;
+ }
+ } else if (aEvent.type == kMousePixelScrollEvent) {
+ ok(!test.handled.pixels,
+ test.description + ":(" + aEvent.type + "), same event has already been handled");
+ test.handled.pixels = true;
+ isnot(expectedPixels, 0,
+ test.description + ":(" + aEvent.type + "), event shouldn't be fired");
+ if (test.onPixelScrollEvent && test.onPixelScrollEvent(aEvent)) {
+ doCommonTests = false;
+ }
+ }
+
+ if (doCommonTests) {
+ var expectedDelta =
+ (aEvent.type == kMouseLineScrollEvent) ?
+ expectedLines : expectedPixels;
+ is(aEvent.target.id, test.target.id,
+ test.description + ":(" + aEvent.type + "), ID mismatch");
+ is(aEvent.axis, test.expected.axis,
+ test.description + ":(" + aEvent.type + "), axis mismatch");
+ ok(aEvent.detail != 0,
+ test.description + ":(" + aEvent.type + "), delta must not be 0");
+ is(aEvent.detail, expectedDelta,
+ test.description + ":(" + aEvent.type + "), delta mismatch");
+ is(aEvent.shiftKey, (test.modifiers & (SHIFT_L | SHIFT_R)) != 0,
+ test.description + ":(" + aEvent.type + "), shiftKey mismatch");
+ is(aEvent.ctrlKey, (test.modifiers & (CTRL_L | CTRL_R)) != 0,
+ test.description + ":(" + aEvent.type + "), ctrlKey mismatch");
+ is(aEvent.altKey, (test.modifiers & (ALT_L | ALT_R)) != 0,
+ test.description + ":(" + aEvent.type + "), altKey mismatch");
+ }
+
+ if (!aEvent || (test.handled.lines || expectedLines == 0) &&
+ (test.handled.pixels || expectedPixels == 0)) {
+ // Don't scroll actually.
+ if (aEvent) {
+ aEvent.preventDefault();
+ }
+ test.target.removeEventListener(kMouseLineScrollEvent, handler, true);
+ test.target.removeEventListener(kMousePixelScrollEvent, handler, true);
+ setTimeout(runNextTest, 0, aTests, aIndex);
+ }
+ };
+
+ test.target.addEventListener(kMouseLineScrollEvent, handler, true);
+ test.target.addEventListener(kMousePixelScrollEvent, handler, true);
+
+ if (expectedLines == 0 && expectedPixels == 0) {
+ // The timeout might not be enough if system is slow by other process,
+ // so, the test might be passed unexpectedly. However, it must be able
+ // to be detected by random orange.
+ setTimeout(handler, 500);
+ }
+
+ var utils = getWindowUtils(test.window);
+ var ptInScreen = getPointInScreen(test.target, test.window);
+ var isVertical =
+ ((test.message == WM_MOUSEWHEEL) || (test.message == WM_VSCROLL));
+ var deltaX = !isVertical ? test.delta : 0;
+ var deltaY = isVertical ? test.delta : 0;
+ utils.sendNativeMouseScrollEvent(ptInScreen.x + test.x,
+ ptInScreen.y + test.y,
+ test.message, deltaX, deltaY, 0,
+ test.modifiers,
+ test.additionalFlags,
+ test.target);
+ break;
+ default:
+ ok(false, test.description + ": invalid message");
+ // Let's timeout.
+ }
+}
+
+function prepareTests() {
+ // Disable special action with modifier key
+ SpecialPowers.setIntPref(kAltKeyActionPref, 1);
+ SpecialPowers.setIntPref(kCtrlKeyActionPref, 1);
+ SpecialPowers.setIntPref(kShiftKeyActionPref, 1);
+ SpecialPowers.setIntPref(kWinKeyActionPref, 1);
+
+ SpecialPowers.setIntPref(kAltKeyDeltaMultiplierXPref, 100);
+ SpecialPowers.setIntPref(kAltKeyDeltaMultiplierYPref, 100);
+ SpecialPowers.setIntPref(kCtrlKeyDeltaMultiplierXPref, 100);
+ SpecialPowers.setIntPref(kCtrlKeyDeltaMultiplierYPref, 100);
+ SpecialPowers.setIntPref(kShiftKeyDeltaMultiplierXPref, 100);
+ SpecialPowers.setIntPref(kShiftKeyDeltaMultiplierYPref, 100);
+ SpecialPowers.setIntPref(kWinKeyDeltaMultiplierXPref, 100);
+ SpecialPowers.setIntPref(kWinKeyDeltaMultiplierYPref, 100);
+
+ SpecialPowers.setBoolPref(kSystemScrollSpeedOverridePref, false);
+ SpecialPowers.setIntPref(kTimeoutPref, -1);
+
+ runNextTest(gPreparingSteps, 0);
+}
+
+</script>
+</body>
+
+</html>
diff --git a/widget/tests/window_mouse_scroll_win_2.html b/widget/tests/window_mouse_scroll_win_2.html
new file mode 100644
index 0000000000..c8d3762405
--- /dev/null
+++ b/widget/tests/window_mouse_scroll_win_2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ Helper file for window_mouse_scroll_win.html
+</body>
+</html>
diff --git a/widget/tests/window_picker_no_crash_child.html b/widget/tests/window_picker_no_crash_child.html
new file mode 100644
index 0000000000..c980f979be
--- /dev/null
+++ b/widget/tests/window_picker_no_crash_child.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<title>Picker window</title>
+<form name="form1">
+ <input type="file" name="uploadbox" id="uploadbox" onclick="window.clicked = true;">
+ <input type="file" name="multiple" id="multiple" multiple onclick="window.clicked = true">
+</form>
diff --git a/widget/tests/window_state_windows.xhtml b/widget/tests/window_state_windows.xhtml
new file mode 100644
index 0000000000..60989db144
--- /dev/null
+++ b/widget/tests/window_state_windows.xhtml
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window id="NativeWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ width="300"
+ height="300"
+ onload="onLoad();"
+ title="Window State Tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function onLoad() {
+ var win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ /*
+ switch(win.windowState) {
+ case win.STATE_FULLSCREEN:
+ dump("STATE_FULLSCREEN \n");
+ break;
+ case win.STATE_MAXIMIZED:
+ dump("STATE_MAXIMIZED \n");
+ break;
+ case win.STATE_MINIMIZED:
+ dump("STATE_MINIMIZED \n");
+ break;
+ case win.STATE_NORMAL:
+ dump("STATE_NORMAL \n");
+ break;
+ }
+ */
+
+ // Make sure size mode changes are reflected in the widget.
+ win.restore();
+ ok(win.windowState == win.STATE_NORMAL, "window state is restored.");
+ win.minimize();
+ ok(win.windowState == win.STATE_MINIMIZED, "window state is minimized.");
+
+ // Windows resizes children to 0x0. Code in nsWindow filters these changes out. Without
+ // this all sorts of screwy things can happen in child widgets.
+ ok(document.documentElement.clientHeight > 0, "document height should not be zero for a minimized window!");
+ ok(document.documentElement.clientWidth > 0, "document width should not be zero for a minimized window!");
+
+ // Make sure size mode changes are reflected in the widget.
+ win.restore();
+ ok(win.windowState == win.STATE_NORMAL, "window state is restored.");
+ win.maximize();
+ ok(win.windowState == win.STATE_MAXIMIZED, "window state is maximized.");
+ win.restore();
+ ok(win.windowState == win.STATE_NORMAL, "window state is restored.");
+
+ /*
+ dump(win.screenX + "\n");
+ win.minimize();
+ dump(win.screenX + "\n");
+ win.restore();
+ dump(win.screenX + "\n");
+ */
+
+ SimpleTest.finish();
+ }
+
+ ]]>
+ </script>
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/widget/tests/window_wheeltransaction.xhtml b/widget/tests/window_wheeltransaction.xhtml
new file mode 100644
index 0000000000..f3c081b105
--- /dev/null
+++ b/widget/tests/window_wheeltransaction.xhtml
@@ -0,0 +1,1569 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<window title="Wheel scroll tests"
+ width="600" height="600"
+ onload="onload();"
+ onunload="onunload();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js" />
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<style type="text/css">
+ #rootview {
+ overflow: auto;
+ width: 400px;
+ height: 400px;
+ border: 1px solid;
+ }
+ #container {
+ overflow: auto;
+ width: 600px;
+ height: 600px;
+ }
+ #rootview pre {
+ margin: 20px 0 20px 20px;
+ padding: 0;
+ overflow: auto;
+ display: block;
+ width: 100px;
+ height: 100.5px;
+ font-size: 16px;
+ }
+</style>
+<div id="rootview" onscroll="onScrollView(event);">
+ <div id="container">
+ <pre id="subview1" onscroll="onScrollView(event);">
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+ </pre>
+ <pre id="subview2" onscroll="onScrollView(event);">
+Text.
+Text.
+Text.
+Text.
+Text.
+Text.
+Text.
+Text.
+Text.
+Text.
+ </pre>
+ <pre id="subview3" onscroll="onScrollView(event);">
+Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text.
+ </pre>
+ </div>
+</div>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+function ok(aCondition, aMessage)
+{
+ window.arguments[0].SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function isnot(aLeft, aRight, aMessage)
+{
+ window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage);
+}
+
+var gCurrentTestListStatus = { nextListIndex: 0 };
+var gCurrentTest;
+
+const kListenEvent_None = 0;
+const kListenEvent_OnScroll = 1;
+const kListenEvent_OnScrollFailed = 2;
+const kListenEvent_OnTransactionTimeout = 4;
+const kListenEvent_All = kListenEvent_OnScroll |
+ kListenEvent_OnScrollFailed |
+ kListenEvent_OnTransactionTimeout;
+var gLitesnEvents = kListenEvent_None;
+
+/**
+ * At unexpected transaction timeout, we need to stop *all* timers. But it is
+ * difficult and it can be create more complex testing code. So, we should use
+ * only one timer at one time. For that, we must store the timer id to this
+ * variable. And the functions which may be called via a timer must clear the
+ * current timer by |_clearTimer| function.
+ */
+var gTimer;
+
+var gPrefSvc = SpecialPowers.Services.prefs;
+const kPrefSmoothScroll = "general.smoothScroll";
+const kPrefNameTimeout = "mousewheel.transaction.timeout";
+const kPrefNameIgnoreMoveDelay = "mousewheel.transaction.ignoremovedelay";
+const kPrefTestEventsAsyncEnabled = "test.events.async.enabled";
+
+const kDefaultTimeout = gPrefSvc.getIntPref(kPrefNameTimeout);
+const kDefaultIgnoreMoveDelay = gPrefSvc.getIntPref(kPrefNameIgnoreMoveDelay);
+
+gPrefSvc.setBoolPref(kPrefSmoothScroll, false);
+gPrefSvc.setBoolPref(kPrefTestEventsAsyncEnabled, true);
+
+var gTimeout, gIgnoreMoveDelay;
+var gEnoughForTimeout, gEnoughForIgnoreMoveDelay;
+
+function setTimeoutPrefs(aTimeout, aIgnoreMoveDelay)
+{
+ gPrefSvc.setIntPref(kPrefNameTimeout, aTimeout);
+ gPrefSvc.setIntPref(kPrefNameIgnoreMoveDelay, aIgnoreMoveDelay);
+ gTimeout = aTimeout;
+ gIgnoreMoveDelay = aIgnoreMoveDelay;
+ gEnoughForTimeout = gTimeout * 2;
+ gEnoughForIgnoreMoveDelay = gIgnoreMoveDelay * 1.2;
+}
+
+function resetTimeoutPrefs()
+{
+ if (gTimeout == kDefaultTimeout)
+ return;
+ setTimeoutPrefs(kDefaultTimeout, kDefaultIgnoreMoveDelay);
+ initTestList();
+}
+
+function growUpTimeoutPrefs()
+{
+ if (gTimeout != kDefaultTimeout)
+ return;
+ setTimeoutPrefs(5000, 1000);
+ initTestList();
+}
+
+// setting enough time for testing.
+gPrefSvc.setIntPref(kPrefNameTimeout, gTimeout);
+gPrefSvc.setIntPref(kPrefNameIgnoreMoveDelay, gIgnoreMoveDelay);
+
+var gRootView = document.getElementById("rootview");
+var gSubView1 = document.getElementById("subview1");
+var gSubView2 = document.getElementById("subview2");
+var gSubView3 = document.getElementById("subview3");
+
+gRootView.addEventListener("MozMouseScrollFailed", onMouseScrollFailed);
+gRootView.addEventListener("MozMouseScrollTransactionTimeout",
+ onTransactionTimeout);
+
+function finish()
+{
+ window.close();
+}
+
+async function onload()
+{
+ // Before actually running tests, we disable auto-dir scrolling, becasue the
+ // tests in this file are meant to test scrolling transactions, not meant to
+ // test default actions for wheel events, so we simply disabled auto-dir
+ // scrolling, which are well tested in
+ // dom/events/test/window_wheel_default_action.html.
+ await SpecialPowers.pushPrefEnv({"set": [["mousewheel.autodir.enabled",
+ false]]});
+
+ runNextTestList();
+}
+
+function onunload()
+{
+ resetTimeoutPrefs();
+ gPrefSvc.clearUserPref(kPrefSmoothScroll);
+ gPrefSvc.clearUserPref(kPrefTestEventsAsyncEnabled);
+ disableNonTestMouseEvents(false);
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+ window.arguments[0].SimpleTest.finish();
+}
+
+function offsetForRootView()
+{
+ let rootViewRect = gRootView.getBoundingClientRect();
+ let subView1Rect = gSubView1.getBoundingClientRect();
+ return {
+ x: (subView1Rect.left - rootViewRect.left) / 2,
+ y: (subView1Rect.top - rootViewRect.top) / 2,
+ }
+}
+
+function _offsetFor(aSubView)
+{
+ let rootViewRect = gRootView.getBoundingClientRect();
+ let subViewRect = aSubView.getBoundingClientRect();
+ return {
+ x: subViewRect.left - rootViewRect.left + subViewRect.width / 2,
+ y: subViewRect.top - rootViewRect.top + subViewRect.height / 2,
+ }
+}
+
+function offsetForSubView1()
+{
+ return _offsetFor(gSubView1);
+}
+
+function offsetForSubView2()
+{
+ return _offsetFor(gSubView2);
+}
+
+function offsetForSubView3()
+{
+ return _offsetFor(gSubView3);
+}
+
+/**
+ * Define the tests here:
+ * Scrolls are processed async always. Therefore, we need to call all tests
+ * by timer. gTestLists is array of testing lists. In other words, an item
+ * of gTestList is a group of one or more testing. Each items has following
+ * properties:
+ *
+ * - retryWhenTransactionTimeout
+ * The testing of wheel transaction might be fialed randomly by
+ * timeout. Then, automatically the failed test list will be retested
+ * automatically only this number of times.
+ *
+ * - steps
+ * This property is array of testing. Each steps must have following
+ * properties at least.
+ *
+ * - func
+ * This property means function which will be called via
+ * |setTimeout|. The function cannot have params. If you need
+ * some additional parameters, you can specify some original
+ * properties for the test function. If you do so, you should
+ * document it in the testing function.
+ * - delay
+ * This property means delay time until the function to be called.
+ * I.e., the value used for the second param of |setTimeout|.
+ *
+ * And also you need one more property when you call a testing function.
+ *
+ * - description
+ * This property is description of the test. This is used for
+ * logging.
+ *
+ * At testing, you can access to current step via |gCurrentTest|.
+ */
+
+var gTestLists;
+function initTestList()
+{
+ gTestLists = [
+ /**************************************************************************
+ * Continuous scrolling test for |gRootView|
+ * |gRootView| has both scrollbars and it has three children which are
+ * |gSubView1|, |gSubView2| and |gSubView3|. They have scrollbars. If
+ * the current transaction targets |gRootView|, other children should not
+ * be scrolled even if the wheel events are fired on them.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Vertical wheel events should scroll |gRootView| even if the position
+ // of wheel events in a child view which has scrollbar.
+ { func: testContinuousScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Continuous scrolling test for root view (vertical/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ description: "Continuous scrolling test for root view (vertical/backward)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Horizontal wheel events should scroll |gRootView| even if the
+ // position of wheel events in a child view which has scrollbar.
+ { func: testContinuousScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ description: "Continuous scrolling test for root view (horizontal/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ description: "Continuous scrolling test for root view (horizontal/backward)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Continuous scrolling test for |gSubView1|
+ * |gSubView1| has both scrollbars.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Vertical wheel events should scroll |gSubView1|.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ description: "Continuous scrolling test for sub view 1 (vertical/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: true, expectedView: gSubView1,
+ description: "Continuous scrolling test for sub view 1 (vertical/backward)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Horitontal wheel events should scroll |gSubView1|.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ description: "Continuous scrolling test for sub view 1 (horizontal/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: false, expectedView: gSubView1,
+ description: "Continuous scrolling test for sub view 1 (horizontal/backward)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Continuous scrolling test for |gSubView2|
+ * |gSubView2| has only vertical scrollbar.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Vertical wheel events should scroll |gSubView2|.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView2,
+ isForward: true, isVertical: true, expectedView: gSubView2,
+ description: "Continuous scrolling test for sub view 2 (vertical/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView2,
+ isForward: false, isVertical: true, expectedView: gSubView2,
+ description: "Continuous scrolling test for sub view 2 (vertical/backward)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Horizontal wheel events should scroll its nearest scrollable ancestor
+ // view, i.e., it is |gRootView|.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView2,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ description: "Continuous scrolling test for sub view 2 (horizontal/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView2,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ description: "Continuous scrolling test for sub view 2 (horizontal/backward)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Continuous scrolling test for |gSubView3|
+ * |gSubView3| has only horizontal scrollbar.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Vertical wheel events should scroll its nearest scrollable ancestor
+ // view, i.e., it is |gRootView|.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView3,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Continuous scrolling test for sub view 3 (vertical/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView3,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ description: "Continuous scrolling test for sub view 3 (vertical/backward)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Horitontal wheel events should scroll |gSubView3|.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView3,
+ isForward: true, isVertical: false, expectedView: gSubView3,
+ description: "Continuous scrolling test for sub view 3 (horizontal/forward)" },
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView3,
+ isForward: false, isVertical: false, expectedView: gSubView3,
+ description: "Continuous scrolling test for sub view 3 (horizontal/backward)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Don't reset transaction by a different direction wheel event
+ * Even if a wheel event doesn't same direction as last wheel event, the
+ * current transaction should not be reset.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical -> Horizontal
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView| by a vertical wheel
+ // event.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Don't reset transaction by a different direction wheel event (1-1)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ description: "Don't reset transaction by a different direction wheel event (1-2)" },
+ // Send a horizontal wheel event over |gSubView1| but |gRootView| should
+ // be scrolled.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Don't reset transaction by a different direction wheel event (1-3)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal -> Vertical
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView| by a horizontal wheel
+ // event.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ description: "Don't reset transaction by a different direction wheel event (2-1)" },
+ // Scroll back to left-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ description: "Don't reset transaction by a different direction wheel event (2-2)" },
+ // Send a vertical wheel event over |gSubView1| but |gRootView| should
+ // be scrolled.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Don't reset transaction by a different direction wheel event (2-3)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Don't reset transaction even if a wheel event cannot scroll
+ * Even if a wheel event cannot scroll to specified direction in the
+ * current target view, the transaction should not be reset. E.g., there
+ * are some devices which can scroll obliquely. If so, probably, users
+ * cannot input only intended direction.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // A view only has vertical scrollbar case.
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gSubView2|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView2,
+ isForward: true, isVertical: true, expectedView: gSubView2,
+ description: "Don't reset transaction even if a wheel event cannot scroll (1-1)" },
+ // |gSubView2| doesn't have horizontal scrollbar but should not scroll
+ // any views.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView2,
+ isForward: true, isVertical: false, expectedView: null,
+ description: "Don't reset transaction even if a wheel event cannot scroll (1-2)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // A view only has horizontal scrollbar case.
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gSubView3|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView3,
+ isForward: true, isVertical: false, expectedView: gSubView3,
+ description: "Don't reset transaction even if a wheel event cannot scroll (2-1)" },
+ // |gSubView3| doesn't have vertical scrollbar but should not scroll any
+ // views.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView3,
+ isForward: true, isVertical: true, expectedView: null,
+ description: "Don't reset transaction even if a wheel event cannot scroll (2-2)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Reset transaction by mouse down/mouse up events
+ * Mouse down and mouse up events should cause resetting the current
+ * transaction.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by mouse down/mouse up events (v-1)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by mouse down/mouse up events (v-2)" },
+ // Send mouse button events which should reset the current transaction.
+ // So, the next wheel event should scroll |gSubView1|.
+ { func: sendMouseButtonEvents, delay: 0,
+ description: "sendMouseButtonEvents" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ description: "Reset transaction by mouse down/mouse up events (v-3)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ description: "Reset transaction by mouse down/mouse up events (h-1)" },
+ // Scroll back to left-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ description: "Reset transaction by mouse down/mouse up events (h-2)" },
+ // Send mouse button events which should reset the current transaction.
+ // So, the next wheel event should scroll |gSubView1|.
+ { func: sendMouseButtonEvents, delay: 0,
+ description: "sendMouseButtonEvents" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ description: "Reset transaction by mouse down/mouse up events (h-3)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Reset transaction by a key event
+ * A key event should cause resetting the current transaction.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by a key event (v-1)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by a key event (v-2)" },
+ // Send a key event which should reset the current transaction. So, the
+ // next wheel event should scroll |gSubView1|.
+ { func: sendKeyEvents, delay: 0, key: "a",
+ description: "sendKeyEvents" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ description: "Reset transaction by a key event (v-3)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ description: "Reset transaction by a key event (h-1)" },
+ // Scroll back to left-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ description: "Reset transaction by a key event (h-2)" },
+ // Send a key event which should reset the current transaction. So, the
+ // next wheel event should scroll |gSubView1|.
+ { func: sendKeyEvents, delay: 0, key: "a",
+ description: "sendKeyEvents" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ description: "Reset transaction by a key event (h-3)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Reset transaction by a mouse move event
+ * A mouse move event can cause reseting the current transaction even if
+ * mouse cursor is inside the target view of current transaction. Only
+ * when a wheel event is fired after |gIgnoreMoveDelay| milliseconds since
+ * the first mouse move event from last wheel event, the transaction
+ * should be reset.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by a mouse move event (v-1)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by a mouse move event (v-2)" },
+ // Send a mouse move event immediately after last wheel event, then,
+ // current transaction should be kept.
+ { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (v-3)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (v-4)" },
+ // Send a mouse move event after |gIgnoreMoveDelay| milliseconds since
+ // last wheel event, then, current transaction should be kept.
+ { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (v-5)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (v-6)" },
+ // Send a wheel event after |gIgnoreMoveDelay| milliseconds since last
+ // mouse move event but it is fired immediately after the last wheel
+ // event, then, current transaction should be kept.
+ { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (v-7)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (v-8)" },
+ // Send a wheel event after |gIgnoreMoveDelay| milliseconds have passed
+ // since last mouse move event which is fired after |gIgnoreMoveDelay|
+ // milliseconds since last wheel event, then, current transaction should
+ // be reset.
+ { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ canFailRandomly: { possibleView: gRootView },
+ description: "Reset transaction by a mouse move event (v-9)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-1)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-2)" },
+ // Send a mouse move event immediately after last wheel event, then,
+ // current transaction should be kept.
+ { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-3)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-4)" },
+ // Send a mouse move event after |gIgnoreMoveDelay| milliseconds since
+ // last wheel event, then, current transaction should be kept.
+ { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-5)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-6)" },
+ // Send a wheel event after |gIgnoreMoveDelay| milliseconds since last
+ // mouse move event but it is fired immediately after the last wheel
+ // event, then, current transaction should be kept.
+ { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-7)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Reset transaction by a mouse move event (h-8)" },
+ // Send a wheel event after |gIgnoreMoveDelay| milliseconds have passed
+ // since last mouse move event which is fired after |gIgnoreMoveDelay|
+ // milliseconds since last wheel event, then, current transaction should
+ // be reset.
+ { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ description: "sendMouseMoveEvent" },
+ { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay,
+ offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ canFailRandomly: { possibleView: gRootView },
+ description: "Reset transaction by a mouse move event (h-9)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Reset transaction by a mouse move event on outside of view
+ * When mouse cursor is moved to outside of the current target view, the
+ * transaction should be reset immediately.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gSubView1|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ description: "Reset transaction by a mouse move event on outside of view (v-1)" },
+ // Send mouse move event over |gRootView|.
+ { func: sendMouseMoveEvent, delay: 0, offset: offsetForRootView,
+ description: "sendMouseMoveEvent" },
+ // Send Wheel event over |gRootView| which should be scrolled.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by a mouse move event on outside of view (v-2)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Create a transaction which targets |gSubView1|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ description: "Reset transaction by a mouse move event on outside of view (h-1)" },
+ // Send mouse move event over |gRootView|.
+ { func: sendMouseMoveEvent, delay: 0, offset: offsetForRootView,
+ description: "sendMouseMoveEvent" },
+ // Send Wheel event over |gRootView| which should be scrolled.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Reset transaction by a mouse move event on outside of view (h-2)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Timeout test
+ * A view should not be scrolled during another to be transaction for
+ * another view scrolling. However, a wheel event which is sent after
+ * timeout, a view which is under the mouse cursor should be scrolled.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // First, create a transaction which should target the |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Timeout test (v-1)" },
+ // Scroll back to top-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ description: "Timeout test (v-2)" },
+ // A wheel event over |gSubView1| should not scroll it during current
+ // transaction.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Timeout test (v-3)" },
+ // Scroll back to top-most again.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: true, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Timeout test (v-4)" },
+ // A wheel event over |gSubView1| after timeout should scroll
+ // |gSubView1|.
+ { func: testOneTimeScroll, delay: gEnoughForTimeout,
+ offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ isTimeoutTesting: true,
+ description: "Timeout test (v-5)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // First, create a transaction which should target the |gRootView|.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ description: "Timeout test (h-1)" },
+ // Scroll back to left-most for easy cursor position specifying.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForRootView,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ description: "Timeout test (h-2)" },
+ // A wheel event over |gSubView1| should not scroll it during current
+ // transaction.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Timeout test (h-3)" },
+ // Scroll back to left-most again.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: false, isVertical: false, expectedView: gRootView,
+ canFailRandomly: { possibleView: gSubView1 },
+ description: "Timeout test (h-4)" },
+ // A wheel event over |gSubView1| after timeout should scroll
+ // |gSubView1|.
+ { func: testOneTimeScroll, delay: gEnoughForTimeout,
+ offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ isTimeoutTesting: true,
+ description: "Timeout test (h-5)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Timeout test even with many wheel events
+ * This tests whether timeout is occurred event if wheel events are sent.
+ * The transaction should not be updated by non-scrollable wheel events.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Vertical case
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Scroll |gSubView1| to bottom-most.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ description: "Timeout test even with many wheel events (v-1)" },
+ // Don't scroll any views before timeout.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: null,
+ canFailRandomly: { possibleView: gRootView },
+ description: "Timeout test even with many wheel events (v-2)" },
+ // Recreate a transaction which is scrolling |gRootView| after time out.
+ { func: testRestartScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gRootView,
+ description: "Timeout test even with many wheel events (v-3)" }
+ ]
+ },
+
+
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ // Horizontal case
+ { func: initElements, delay: 0, forVertical: false,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ // Scroll |gSubView1| to right-most.
+ { func: testContinuousScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ description: "Timeout test even with many wheel events (h-1)" },
+ // Don't scroll any views before timeout.
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: null,
+ canFailRandomly: { possibleView: gRootView },
+ description: "Timeout test even with many wheel events (h-2)" },
+ // Recreate a transaction which is scrolling |gRootView| after time out.
+ { func: testRestartScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gRootView,
+ description: "Timeout test even with many wheel events (h-3)" }
+ ]
+ },
+
+
+ /**************************************************************************
+ * Very large scrolling wheel event
+ * If the delta value is larger than the scrolling page size, it should be
+ * scrolled only one page instead of the delta value.
+ **************************************************************************/
+ { retryWhenTransactionTimeout: 5,
+ steps: [
+ { func: initElements, delay: 0, forVertical: true,
+ description: "initElements" },
+ { func: clearWheelTransaction, delay: 0,
+ description: "clearWheelTransaction" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ delta: 5000,
+ description: "Very large delta scrolling (v-1)" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: true, expectedView: gSubView1,
+ delta: 5000,
+ description: "Very large delta scrolling (v-2)" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ delta: 5000,
+ description: "Very large delta scrolling (h-1)" },
+ { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1,
+ isForward: true, isVertical: false, expectedView: gSubView1,
+ delta: 5000,
+ description: "Very large delta scrolling (h-2)" }
+ ]
+ }
+ ];
+}
+
+/******************************************************************************
+ * Actions for preparing tests
+ ******************************************************************************/
+
+function initElements()
+{
+ _clearTimer();
+
+ function resetScrollPosition(aElement)
+ {
+ aElement.scrollTop = 0;
+ aElement.scrollLeft = 0;
+ }
+
+ const kDisplay = gCurrentTest.forVertical ? "block" : "inline-block";
+ gSubView1.style.display = kDisplay;
+ gSubView2.style.display = kDisplay;
+ gSubView3.style.display = kDisplay;
+
+ resetScrollPosition(gRootView);
+ resetScrollPosition(gSubView1);
+ resetScrollPosition(gSubView2);
+ resetScrollPosition(gSubView3);
+ _getDOMWindowUtils(window).advanceTimeAndRefresh(0);
+
+ runNextTestStep();
+}
+
+function clearWheelTransaction()
+{
+ _clearTimer();
+ _clearTransaction();
+ runNextTestStep();
+}
+
+function sendKeyEvents()
+{
+ _clearTimer();
+ synthesizeKey(gCurrentTest.key, {}, window);
+ runNextTestStep();
+}
+
+function sendMouseButtonEvents()
+{
+ _clearTimer();
+ synthesizeMouse(gRootView, -1, -1, { type:"mousedown" }, window);
+ synthesizeMouse(gRootView, -1, -1, { type:"mouseup" }, window);
+ runNextTestStep();
+}
+
+function sendMouseMoveEvent()
+{
+ _clearTimer();
+ _fireMouseMoveEvent(gCurrentTest.offset());
+ runNextTestStep();
+}
+
+/******************************************************************************
+ * Utilities for testing functions
+ ******************************************************************************/
+
+function _clearTransaction()
+{
+ synthesizeMouse(gRootView, -1, -1, { type:"mousedown" }, window);
+ synthesizeMouse(gRootView, -1, -1, { type:"mouseup" }, window);
+}
+
+function _saveScrollPositions()
+{
+ function save(aElement)
+ {
+ aElement.prevTop = aElement.scrollTop;
+ aElement.prevLeft = aElement.scrollLeft;
+ }
+ save(gRootView);
+ save(gSubView1);
+ save(gSubView2);
+ save(gSubView3);
+}
+
+function _fireMouseMoveEvent(aOffset)
+{
+ synthesizeMouse(gRootView, aOffset.x, aOffset.y, { type:"mousemove" }, window);
+}
+
+function _fireWheelScrollEvent(aOffset, aIsVertical, aForward, aDelta)
+{
+ var event = { deltaMode: WheelEvent.DOM_DELTA_LINE };
+ if (aIsVertical) {
+ event.deltaY = aForward ? aDelta : -aDelta;
+ } else {
+ event.deltaX = aForward ? aDelta : -aDelta;
+ }
+ sendWheelAndPaint(gRootView, aOffset.x, aOffset.y, event, null, window);
+}
+
+function _canScroll(aElement, aIsVertical, aForward)
+{
+ if (aIsVertical) {
+ if (!aForward)
+ return aElement.scrollTop > 0;
+ return aElement.scrollHeight > aElement.scrollTop + aElement.clientHeight;
+ }
+ if (!aForward)
+ return aElement.scrollLeft > 0;
+ return aElement.scrollWidth > aElement.scrollLeft + aElement.clientWidth;
+}
+
+const kNotScrolled = 0;
+const kScrolledToTop = 1;
+const kScrolledToBottom = 2;
+const kScrolledToLeft = 4;
+const kScrolledToRight = 8;
+
+const kScrolledVertical = kScrolledToTop | kScrolledToBottom;
+const kScrolledHorizontal = kScrolledToLeft | kScrolledToRight;
+
+function _getScrolledState(aElement)
+{
+ var ret = kNotScrolled;
+ if (aElement.scrollTop != aElement.prevTop) {
+ ret |= aElement.scrollTop < aElement.prevTop ? kScrolledToTop :
+ kScrolledToBottom;
+ }
+ if (aElement.scrollLeft != aElement.prevLeft) {
+ ret |= aElement.scrollLeft < aElement.prevLeft ? kScrolledToLeft :
+ kScrolledToRight;
+ }
+ return ret;
+}
+
+function _getExpectedScrolledState()
+{
+ // eslint-disable-next-line no-nested-ternary
+ return gCurrentTest.isVertical ?
+ gCurrentTest.isForward ? kScrolledToBottom : kScrolledToTop :
+ gCurrentTest.isForward ? kScrolledToRight : kScrolledToLeft;
+}
+
+function _getScrolledStateText(aScrolledState)
+{
+ if (aScrolledState == kNotScrolled)
+ return "Not scrolled";
+
+ var s = "scrolled to ";
+ if (aScrolledState & kScrolledVertical) {
+ s += aScrolledState & kScrolledToTop ? "backward" : "forward";
+ s += " (vertical)"
+ if (aScrolledState & kScrolledHorizontal)
+ s += " and to ";
+ }
+ if (aScrolledState & kScrolledHorizontal) {
+ s += aScrolledState & kScrolledToLeft ? "backward" : "forward";
+ s += " (horizontal)"
+ }
+ return s;
+}
+
+function _getCurrentTestList()
+{
+ return gTestLists[gCurrentTestListStatus.nextListIndex - 1];
+}
+
+function _clearTimer()
+{
+ clearTimeout(gTimer);
+ gTimer = 0;
+}
+
+/******************************************************************************
+ * Testing functions
+ ******************************************************************************/
+
+/**
+ * Note that testing functions must set following variables:
+ *
+ * gCurrentTest.repeatTest: See comment in |continueTest|.
+ * gCurrentTest.autoRepeatDelay: See comment in |continueTest|.
+ * gListenScrollEvent: When this is not true, the event handlers ignores the
+ * events.
+ */
+
+function testContinuousScroll()
+{
+ /**
+ * Testing continuous scrolling. This function synthesizes a wheel event. If
+ * the test was success, this function will be recalled automatically.
+ * And when a generating wheel event cannot scroll the expected view, this
+ * function fires the wheel event only one time.
+ *
+ * @param gCurrentTest.offset
+ * A function to compute the cursor position of firing wheel event.
+ * The values are offset from |gRootView|.
+ * @param gCurrentTest.isVertical
+ * Whether the wheel event is for virtical scrolling or horizontal.
+ * @param gCurrentTest.isForward
+ * Whether the wheel event is to forward or to backward.
+ * @param gCurrentTest.expectedView
+ * The expected view which will be scrolled by wheel event. This
+ * value must not be null.
+ */
+
+ _clearTimer();
+ _saveScrollPositions();
+ if (!gCurrentTest.expectedView) {
+ runNextTestStep();
+ return;
+ }
+
+ gLitesnEvents = kListenEvent_All;
+ gCurrentTest.repeatTest = true;
+ gCurrentTest.autoRepeatDelay = 0;
+
+ if (!_canScroll(gCurrentTest.expectedView,
+ gCurrentTest.isVertical, gCurrentTest.isForward)) {
+ gCurrentTest.expectedView = null;
+ }
+ var delta = gCurrentTest.delta ? gCurrentTest.delta : 4;
+ _fireWheelScrollEvent(gCurrentTest.offset(),
+ gCurrentTest.isVertical, gCurrentTest.isForward, delta);
+}
+
+function testOneTimeScroll()
+{
+ /**
+ * Testing one wheel event. |runNextTestStep| will be called immediately
+ * after this function by |onScrollView| or |onTimeout|.
+ *
+ * @param gCurrentTest.offset
+ * A function to compute the cursor position of firing wheel event.
+ * The values are offset from |gRootView|.
+ * @param gCurrentTest.isVertical
+ * Whether the wheel event is for virtical scrolling or horizontal.
+ * @param gCurrentTest.isForward
+ * Whether the wheel event is to forward or to backward.
+ * @param gCurrentTest.expectedView
+ * The expected view which will be scrolled by wheel event. This
+ * value can be null. It means any views should not be scrolled.
+ */
+
+ _clearTimer();
+ _saveScrollPositions();
+
+ gLitesnEvents = kListenEvent_All;
+ gCurrentTest.repeatTest = false;
+ gCurrentTest.autoRepeatDelay = 0;
+
+ var delta = gCurrentTest.delta ? gCurrentTest.delta : 4;
+ _fireWheelScrollEvent(gCurrentTest.offset(),
+ gCurrentTest.isVertical, gCurrentTest.isForward, delta);
+}
+
+function testRestartScroll()
+{
+ /**
+ * Testing restart to scroll in expected view after timeout from the current
+ * transaction. This function recall this itself until to success this test
+ * or timeout from this test.
+ *
+ * @param gCurrentTest.offset
+ * A function to compute the cursor position of firing wheel event.
+ * The values are offset from |gRootView|.
+ * @param gCurrentTest.isVertical
+ * Whether the wheel event is for virtical scrolling or horizontal.
+ * @param gCurrentTest.isForward
+ * Whether the wheel event is to forward or to backward.
+ * @param gCurrentTest.expectedView
+ * The expected view which will be scrolled by wheel event. This
+ * value must not be null.
+ */
+
+ _clearTimer();
+ _saveScrollPositions();
+
+ if (!gCurrentTest.wasTransactionTimeout) {
+ gCurrentTest.repeatTest = true;
+ gCurrentTest.autoRepeatDelay = gTimeout / 3;
+ gLitesnEvents = kListenEvent_All;
+ gCurrentTest.isTimeoutTesting = true;
+ if (gCurrentTest.expectedView) {
+ gCurrentTest.expectedViewAfterTimeout = gCurrentTest.expectedView;
+ gCurrentTest.expectedView = null;
+ }
+ } else {
+ gCurrentTest.repeatTest = false;
+ gCurrentTest.autoRepeatDelay = 0;
+ gLitesnEvents = kListenEvent_All;
+ gCurrentTest.isTimeoutTesting = false;
+ gCurrentTest.expectedView = gCurrentTest.expectedViewAfterTimeout;
+ }
+
+ var delta = gCurrentTest.delta ? gCurrentTest.delta : 4;
+ _fireWheelScrollEvent(gCurrentTest.offset(),
+ gCurrentTest.isVertical, gCurrentTest.isForward, delta);
+}
+
+/******************************************************************************
+ * Event handlers
+ ******************************************************************************/
+
+function onScrollView(aEvent)
+{
+ /**
+ * Scroll event handler of |gRootView|, |gSubView1|, |gSubView2| and
+ * |gSubView3|. If testing is failed, this function cancels all left tests.
+ * For checking the event is expected, the event firer must call
+ * |_saveScrollPositions|.
+ *
+ * @param gCurrentTest.expectedView
+ * The expected view which should be scrolled by the wheel event.
+ * This value can be null. It means any views should not be
+ * scrolled.
+ * @param gCurrentTest.isVertical
+ * The expected view should be scrolled vertical or horizontal.
+ * @param gCurrentTest.isForward
+ * The expected view should be scrolled to forward or backward.
+ * @param gCurrentTest.canFailRandomly
+ * If this is not undefined, this test can fail by unexpected view
+ * scrolling which is caused by unexpected timeout. If this is
+ * defined, |gCurrentTest.possibleView| must be set. If the view is
+ * same as the event target, the failure can be random. At this
+ * time, we should retry the current test list.
+ */
+
+ if (!(gLitesnEvents & kListenEvent_OnScroll))
+ return;
+
+ // Now testing a timeout, but a view is scrolled before timeout.
+ if (gCurrentTest.isTimeoutTesting && !gCurrentTest.wasTransactionTimeout) {
+ is(aEvent.target.id, "",
+ "The view scrolled before timeout (the expected view after timeout is " +
+ gCurrentTest.expectedView ? gCurrentTest.expectedView.id : "null" +
+ "): " + gCurrentTest.description);
+ runNextTestList();
+ return;
+ }
+
+ // Check whether the scrolled event should be fired or not.
+ if (!gCurrentTest.expectedView) {
+ is(aEvent.target.id, "",
+ "no views should be scrolled (" +
+ _getScrolledStateText(_getScrolledState(aEvent.target)) + "): " +
+ gCurrentTest.description);
+ runNextTestList();
+ return;
+ }
+
+ // Check whether the scrolled view is expected or not.
+ if (aEvent.target != gCurrentTest.expectedView) {
+ // If current test can fail randomly and the possible view is same as the
+ // event target, this failure may be caused by unexpected timeout.
+ // At this time, we should retry the current tests with slower settings.
+ if (gCurrentTest.canFailRandomly &&
+ gCurrentTest.canFailRandomly.possibleView == aEvent.target &&
+ gCurrentTestListStatus.retryWhenTransactionTimeout > 0) {
+ gCurrentTestListStatus.retryWhenTransactionTimeout--;
+ retryCurrentTestList();
+ return;
+ }
+ is(aEvent.target.id, gCurrentTest.expectedView.id,
+ "wrong view was scrolled: " + gCurrentTest.description);
+ runNextTestList();
+ return;
+ }
+
+ // Check whether the scrolling direction is expected or not.
+ var expectedState = _getExpectedScrolledState();
+ var currentState = _getScrolledState(aEvent.target);
+ if (expectedState != currentState) {
+ is(_getScrolledStateText(currentState),
+ _getScrolledStateText(expectedState),
+ "scrolled to wrong direction: " + gCurrentTest.description);
+ runNextTestList();
+ return;
+ }
+
+ ok(true, "passed: " + gCurrentTest.description);
+ continueTest();
+}
+
+function onMouseScrollFailed()
+{
+ /**
+ * Scroll failed event handler. If testing is failed, this function cancels
+ * all remains of current test-list, and go to next test-list.
+ *
+ * NOTE: This event is fired immediately after |_fireWheelScrollEvent|.
+ *
+ * @param gCurrentTest.expectedView
+ * The expected view which should be scrolled by the wheel event.
+ * This value can be null. It means any views should not be
+ * scrolled. When this is not null, this event means the test may
+ * be failed.
+ */
+
+ if (!(gLitesnEvents & kListenEvent_OnScrollFailed))
+ return;
+
+ ok(!gCurrentTest.expectedView,
+ "failed to scroll on current target: " + gCurrentTest.description);
+ if (gCurrentTest.expectedView) {
+ runNextTestList();
+ return;
+ }
+
+ continueTest();
+}
+
+function onTransactionTimeout()
+{
+ /**
+ * Scroll transaction timeout event handler. If the timeout is unexpected,
+ * i.e., |gCurrentTest.isTimeoutTesting| is not true, this function retry
+ * the current test-list. However, if the current test-list failed by timeout
+ * |gCurrentTestListStatus.retryWhenTransactionTimeout| times already, marking
+ * to failed the current test-list, and go to next test-list.
+ *
+ * @param gCurrentTest.expectedView
+ * The expected view which should be scrolled by the wheel event.
+ * This value can be null. It means any views should not be
+ * scrolled. When this is not null, this event means the testing may
+ * be failed.
+ * @param gCurrentTest.isTimeoutTesting
+ * If this value is true, the current testing have waited this
+ * event. Otherwise, the testing may be failed.
+ * @param gCurrentTestListStatus.retryWhenTransactionTimeout
+ * If |gCurrentTest.isTimeoutTesting| is not true but this event is
+ * fired, the failure may be randomly. Then, this event handler
+ * retry to test the current test-list until this cound will be zero.
+ */
+
+ if (!gCurrentTest.isTimeoutTesting &&
+ gCurrentTestListStatus.retryWhenTransactionTimeout > 0) {
+ gCurrentTestListStatus.retryWhenTransactionTimeout--;
+ // retry current test list
+ retryCurrentTestList();
+ return;
+ }
+
+ gCurrentTest.wasTransactionTimeout = true;
+
+ if (!(gLitesnEvents & kListenEvent_OnTransactionTimeout))
+ return;
+
+ ok(gCurrentTest.isTimeoutTesting,
+ "transaction timeout: " + gCurrentTest.description);
+ if (!gCurrentTest.isTimeoutTesting) {
+ runNextTestList();
+ return;
+ }
+
+ continueTest();
+}
+
+/******************************************************************************
+ * Main function for this tests
+ ******************************************************************************/
+
+function runNextTestStep()
+{
+ // When this is first time or the current test list is finised, load next
+ // test-list.
+ _clearTimer();
+ if (!gCurrentTest)
+ runNextTestList();
+ else
+ runTestStepAt(gCurrentTestListStatus.nextStepIndex);
+}
+
+function runNextTestList()
+{
+ _clearTimer();
+
+ gLitesnEvents = kListenEvent_None;
+ _clearTransaction();
+ resetTimeoutPrefs();
+ if (gCurrentTestListStatus.nextListIndex >= gTestLists.length) {
+ finish();
+ return;
+ }
+
+ gCurrentTestListStatus.nextListIndex++;
+ gCurrentTestListStatus.retryWhenTransactionTimeout =
+ _getCurrentTestList().retryWhenTransactionTimeout;
+ runTestStepAt(0);
+}
+
+function runTestStepAt(aStepIndex)
+{
+ _clearTimer();
+
+ disableNonTestMouseEvents(true);
+
+ // load a step of testing.
+ gCurrentTestListStatus.nextStepIndex = aStepIndex;
+ gCurrentTest =
+ _getCurrentTestList().steps[gCurrentTestListStatus.nextStepIndex++];
+ if (gCurrentTest) {
+ gCurrentTest.wasTransactionTimeout = false;
+ gTimer = setTimeout(gCurrentTest.func, gCurrentTest.delay);
+ } else {
+ // If current test-list doesn't have more testing, go to next test-list
+ // after cleaning up the current transaction.
+ _clearTransaction();
+ runNextTestList();
+ }
+}
+
+function retryCurrentTestList()
+{
+ _clearTimer();
+
+ gLitesnEvents = kListenEvent_None;
+ _clearTransaction();
+ ok(true, "WARNING: retry current test-list...");
+ growUpTimeoutPrefs(); // retry the test with longer timeout settings.
+ runTestStepAt(0);
+}
+
+function continueTest()
+{
+ /**
+ * This function is called from an event handler when a test succeeded.
+ *
+ * @param gCurrentTest.repeatTest
+ * When this is true, onScrollView calls |gCurrentTest.func|. So,
+ * same test can repeat. Otherwise, this calls |runNextTestStep|.
+ * @param gCurrentTest.autoRepeatDelay
+ * The delay value in milliseconds, this is used to call
+ * |gCurrentTest.func| via |setTimeout|.
+ */
+
+ _clearTimer();
+ gLitesnEvents = kListenEvent_OnTransactionTimeout;
+
+ // We should call each functions via setTimeout. Because sometimes this test
+ // is broken by stack overflow.
+ if (gCurrentTest.repeatTest) {
+ gTimer = setTimeout(gCurrentTest.func, gCurrentTest.autoRepeatDelay);
+ } else {
+ gTimer = setTimeout(runNextTestStep, 0);
+ }
+}
+
+]]>
+</script>
+
+</window>
diff --git a/widget/uikit/GfxInfo.cpp b/widget/uikit/GfxInfo.cpp
new file mode 100644
index 0000000000..d797e4f4a0
--- /dev/null
+++ b/widget/uikit/GfxInfo.cpp
@@ -0,0 +1,174 @@
+/* -*- 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 "GfxInfo.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla {
+namespace widget {
+
+#ifdef DEBUG
+NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug)
+#endif
+
+GfxInfo::GfxInfo() {}
+
+GfxInfo::~GfxInfo() {}
+
+nsresult GfxInfo::GetD2DEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+nsresult GfxInfo::GetDWriteEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) { return NS_ERROR_FAILURE; }
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) {
+ return NS_ERROR_FAILURE;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) { return NS_ERROR_FAILURE; }
+
+const nsTArray<GfxDriverInfo>& GfxInfo::GetGfxDriverInfo() {
+ if (sDriverInfo->IsEmpty()) {
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Ios, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_STATUS_OK,
+ DRIVER_COMPARISON_IGNORED, GfxDriverInfo::allDriverVersions);
+ }
+
+ return *sDriverInfo;
+}
+
+nsresult GfxInfo::GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo,
+ OperatingSystem* aOS /* = nullptr */) {
+ NS_ENSURE_ARG_POINTER(aStatus);
+ aSuggestedDriverVersion.SetIsVoid(true);
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ if (aOS) *aOS = OperatingSystem::Ios;
+
+ if (sShutdownOccurred) {
+ return NS_OK;
+ }
+
+ // OpenGL layers are never blocklisted on iOS.
+ // This early return is so we avoid potentially slow
+ // GLStrings initialization on startup when we initialize GL layers.
+ if (aFeature == nsIGfxInfo::FEATURE_OPENGL_LAYERS ||
+ aFeature == nsIGfxInfo::FEATURE_WEBGL_OPENGL) {
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_OK;
+ return NS_OK;
+ }
+
+ return GfxInfoBase::GetFeatureStatusImpl(
+ aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aOS);
+}
+
+#ifdef DEBUG
+
+// Implement nsIGfxInfoDebug
+
+NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) {
+ return NS_ERROR_FAILURE;
+}
+
+#endif
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/uikit/GfxInfo.h b/widget/uikit/GfxInfo.h
new file mode 100644
index 0000000000..fe75e28d23
--- /dev/null
+++ b/widget/uikit/GfxInfo.h
@@ -0,0 +1,75 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_GfxInfo_h__
+#define __mozilla_widget_GfxInfo_h__
+
+#include "GfxInfoBase.h"
+#include "GfxDriverInfo.h"
+
+#include "nsString.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+namespace gl {
+class GLContext;
+}
+
+namespace widget {
+
+class GfxInfo : public GfxInfoBase {
+ private:
+ ~GfxInfo();
+
+ public:
+ GfxInfo();
+
+ // We only declare the subset of nsIGfxInfo that we actually implement. The
+ // rest is brought forward from GfxInfoBase.
+ NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override;
+ NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override;
+ NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override;
+ NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override;
+ NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVersion2(
+ nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override;
+ using GfxInfoBase::GetFeatureStatus;
+ using GfxInfoBase::GetFeatureSuggestedDriverVersion;
+
+#ifdef DEBUG
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIGFXINFODEBUG
+#endif
+
+ protected:
+ virtual nsresult GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo,
+ OperatingSystem* aOS = nullptr);
+ virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_GfxInfo_h__ */
diff --git a/widget/uikit/moz.build b/widget/uikit/moz.build
new file mode 100644
index 0000000000..6a3c2db994
--- /dev/null
+++ b/widget/uikit/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget")
+
+SOURCES += [
+ "GfxInfo.cpp",
+ "nsAppShell.mm",
+ "nsLookAndFeel.mm",
+ "nsScreenManager.mm",
+ "nsWidgetFactory.mm",
+ "nsWindow.mm",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/widget",
+]
diff --git a/widget/uikit/nsAppShell.h b/widget/uikit/nsAppShell.h
new file mode 100644
index 0000000000..fd46a51025
--- /dev/null
+++ b/widget/uikit/nsAppShell.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+/*
+ * Runs the main native UIKit run loop, interrupting it as needed to process
+ * Gecko events.
+ */
+
+#ifndef nsAppShell_h_
+#define nsAppShell_h_
+
+#include "nsBaseAppShell.h"
+#include "nsTArray.h"
+
+#include <Foundation/NSAutoreleasePool.h>
+#include <CoreFoundation/CFRunLoop.h>
+#include <UIKit/UIWindow.h>
+
+@class AppShellDelegate;
+
+class nsAppShell : public nsBaseAppShell {
+ public:
+ NS_IMETHOD ResumeNative(void) override;
+
+ nsAppShell();
+
+ nsresult Init();
+
+ NS_IMETHOD Run(void) override;
+ NS_IMETHOD Exit(void) override;
+ // Called by the application delegate
+ void WillTerminate(void);
+
+ static nsAppShell* gAppShell;
+ static UIWindow* gWindow;
+ static NSMutableArray* gTopLevelViews;
+
+ protected:
+ virtual ~nsAppShell();
+
+ static void ProcessGeckoEvents(void* aInfo);
+ virtual void ScheduleNativeEventCallback();
+ virtual bool ProcessNextNativeEvent(bool aMayWait);
+
+ NSAutoreleasePool* mAutoreleasePool;
+ AppShellDelegate* mDelegate;
+ CFRunLoopRef mCFRunLoop;
+ CFRunLoopSourceRef mCFRunLoopSource;
+
+ bool mTerminated;
+ bool mNotifiedWillTerminate;
+};
+
+#endif // nsAppShell_h_
diff --git a/widget/uikit/nsAppShell.mm b/widget/uikit/nsAppShell.mm
new file mode 100644
index 0000000000..6324a04b4d
--- /dev/null
+++ b/widget/uikit/nsAppShell.mm
@@ -0,0 +1,243 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#import <UIKit/UIApplication.h>
+#import <UIKit/UIScreen.h>
+#import <UIKit/UIWindow.h>
+#import <UIKit/UIViewController.h>
+
+#include "nsAppShell.h"
+#include "nsCOMPtr.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsString.h"
+#include "nsIRollupListener.h"
+#include "nsIWidget.h"
+#include "nsThreadUtils.h"
+#include "nsMemoryPressure.h"
+#include "nsServiceManagerUtils.h"
+
+nsAppShell* nsAppShell::gAppShell = NULL;
+UIWindow* nsAppShell::gWindow = nil;
+NSMutableArray* nsAppShell::gTopLevelViews = [[NSMutableArray alloc] init];
+
+#define ALOG(args...) \
+ fprintf(stderr, args); \
+ fprintf(stderr, "\n")
+
+// ViewController
+@interface ViewController : UIViewController
+@end
+
+@implementation ViewController
+
+- (void)loadView {
+ ALOG("[ViewController loadView]");
+ CGRect r = {{0, 0}, {100, 100}};
+ self.view = [[UIView alloc] initWithFrame:r];
+ [self.view setBackgroundColor:[UIColor lightGrayColor]];
+ // add all of the top level views as children
+ for (UIView* v in nsAppShell::gTopLevelViews) {
+ ALOG("[ViewController.view addSubView:%p]", v);
+ [self.view addSubview:v];
+ }
+ [nsAppShell::gTopLevelViews release];
+ nsAppShell::gTopLevelViews = nil;
+}
+@end
+
+// AppShellDelegate
+//
+// Acts as a delegate for the UIApplication
+
+@interface AppShellDelegate : NSObject <UIApplicationDelegate> {
+}
+@property(strong, nonatomic) UIWindow* window;
+@end
+
+@implementation AppShellDelegate
+
+- (BOOL)application:(UIApplication*)application
+ didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+ ALOG("[AppShellDelegate application:didFinishLaunchingWithOptions:]");
+ // We only create one window, since we can only display one window at
+ // a time anyway. Also, iOS 4 fails to display UIWindows if you
+ // create them before calling UIApplicationMain, so this makes more sense.
+ nsAppShell::gWindow = [[[UIWindow alloc]
+ initWithFrame:[[UIScreen mainScreen] applicationFrame]] retain];
+ self.window = nsAppShell::gWindow;
+
+ self.window.rootViewController = [[ViewController alloc] init];
+
+ // just to make things more visible for now
+ nsAppShell::gWindow.backgroundColor = [UIColor blueColor];
+ [nsAppShell::gWindow makeKeyAndVisible];
+
+ return YES;
+}
+
+- (void)applicationWillTerminate:(UIApplication*)application {
+ ALOG("[AppShellDelegate applicationWillTerminate:]");
+ nsAppShell::gAppShell->WillTerminate();
+}
+
+- (void)applicationDidBecomeActive:(UIApplication*)application {
+ ALOG("[AppShellDelegate applicationDidBecomeActive:]");
+}
+
+- (void)applicationWillResignActive:(UIApplication*)application {
+ ALOG("[AppShellDelegate applicationWillResignActive:]");
+}
+
+- (void)applicationDidReceiveMemoryWarning:(UIApplication*)application {
+ ALOG("[AppShellDelegate applicationDidReceiveMemoryWarning:]");
+ NS_NotifyOfMemoryPressure(MemoryPressureState::LowMemory);
+}
+@end
+
+// nsAppShell implementation
+
+NS_IMETHODIMP
+nsAppShell::ResumeNative(void) { return nsBaseAppShell::ResumeNative(); }
+
+nsAppShell::nsAppShell()
+ : mAutoreleasePool(NULL),
+ mDelegate(NULL),
+ mCFRunLoop(NULL),
+ mCFRunLoopSource(NULL),
+ mTerminated(false),
+ mNotifiedWillTerminate(false) {
+ gAppShell = this;
+}
+
+nsAppShell::~nsAppShell() {
+ if (mAutoreleasePool) {
+ [mAutoreleasePool release];
+ mAutoreleasePool = NULL;
+ }
+
+ if (mCFRunLoop) {
+ if (mCFRunLoopSource) {
+ ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource,
+ kCFRunLoopCommonModes);
+ ::CFRelease(mCFRunLoopSource);
+ }
+ ::CFRelease(mCFRunLoop);
+ }
+
+ gAppShell = NULL;
+}
+
+// Init
+//
+// public
+nsresult nsAppShell::Init() {
+ mAutoreleasePool = [[NSAutoreleasePool alloc] init];
+
+ // Add a CFRunLoopSource to the main native run loop. The source is
+ // responsible for interrupting the run loop when Gecko events are ready.
+
+ mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
+ NS_ENSURE_STATE(mCFRunLoop);
+ ::CFRetain(mCFRunLoop);
+
+ CFRunLoopSourceContext context;
+ bzero(&context, sizeof(context));
+ // context.version = 0;
+ context.info = this;
+ context.perform = ProcessGeckoEvents;
+
+ mCFRunLoopSource = ::CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
+ NS_ENSURE_STATE(mCFRunLoopSource);
+
+ ::CFRunLoopAddSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes);
+
+ return nsBaseAppShell::Init();
+}
+
+// ProcessGeckoEvents
+//
+// The "perform" target of mCFRunLoop, called when mCFRunLoopSource is
+// signalled from ScheduleNativeEventCallback.
+//
+// protected static
+void nsAppShell::ProcessGeckoEvents(void* aInfo) {
+ nsAppShell* self = static_cast<nsAppShell*>(aInfo);
+ self->NativeEventCallback();
+ self->Release();
+}
+
+// WillTerminate
+//
+// public
+void nsAppShell::WillTerminate() {
+ mNotifiedWillTerminate = true;
+ if (mTerminated) return;
+ mTerminated = true;
+ // We won't get another chance to process events
+ NS_ProcessPendingEvents(NS_GetCurrentThread());
+
+ // Unless we call nsBaseAppShell::Exit() here, it might not get called
+ // at all.
+ nsBaseAppShell::Exit();
+}
+
+// ScheduleNativeEventCallback
+//
+// protected virtual
+void nsAppShell::ScheduleNativeEventCallback() {
+ if (mTerminated) return;
+
+ NS_ADDREF_THIS();
+
+ // This will invoke ProcessGeckoEvents on the main thread.
+ ::CFRunLoopSourceSignal(mCFRunLoopSource);
+ ::CFRunLoopWakeUp(mCFRunLoop);
+}
+
+// ProcessNextNativeEvent
+//
+// protected virtual
+bool nsAppShell::ProcessNextNativeEvent(bool aMayWait) {
+ if (mTerminated) return false;
+
+ NSString* currentMode = nil;
+ NSDate* waitUntil = nil;
+ if (aMayWait) waitUntil = [NSDate distantFuture];
+ NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
+
+ BOOL eventProcessed = NO;
+ do {
+ currentMode = [currentRunLoop currentMode];
+ if (!currentMode) currentMode = NSDefaultRunLoopMode;
+
+ if (aMayWait)
+ eventProcessed = [currentRunLoop runMode:currentMode
+ beforeDate:waitUntil];
+ else
+ [currentRunLoop acceptInputForMode:currentMode beforeDate:waitUntil];
+ } while (eventProcessed && aMayWait);
+
+ return false;
+}
+
+// Run
+//
+// public
+NS_IMETHODIMP
+nsAppShell::Run(void) {
+ ALOG("nsAppShell::Run");
+ char argv[1][4] = {"app"};
+ UIApplicationMain(1, (char**)argv, nil, @"AppShellDelegate");
+ // UIApplicationMain doesn't exit. :-(
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppShell::Exit(void) {
+ if (mTerminated) return NS_OK;
+
+ mTerminated = true;
+ return nsBaseAppShell::Exit();
+}
diff --git a/widget/uikit/nsLookAndFeel.h b/widget/uikit/nsLookAndFeel.h
new file mode 100644
index 0000000000..7de7e0712b
--- /dev/null
+++ b/widget/uikit/nsLookAndFeel.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef __nsLookAndFeel
+#define __nsLookAndFeel
+
+#include "nsXPLookAndFeel.h"
+
+class nsLookAndFeel final : public nsXPLookAndFeel {
+ public:
+ nsLookAndFeel();
+ virtual ~nsLookAndFeel();
+
+ void NativeInit() final;
+ virtual void RefreshImpl();
+ nsresult NativeGetImpl(IntID aID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID aID, float& aResult) override;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override;
+ bool NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) override;
+ virtual char16_t GetPasswordCharacterImpl() {
+ // unicode value for the bullet character, used for password textfields.
+ return 0x2022;
+ }
+
+ static bool UseOverlayScrollbars() { return true; }
+
+ private:
+ nscolor mColorTextSelectForeground;
+ nscolor mColorDarkText;
+
+ bool mInitialized;
+
+ void EnsureInit();
+};
+
+#endif
diff --git a/widget/uikit/nsLookAndFeel.mm b/widget/uikit/nsLookAndFeel.mm
new file mode 100644
index 0000000000..29eb52a234
--- /dev/null
+++ b/widget/uikit/nsLookAndFeel.mm
@@ -0,0 +1,361 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#import <UIKit/UIColor.h>
+#import <UIKit/UIInterface.h>
+
+#include "nsLookAndFeel.h"
+
+#include "mozilla/FontPropertyTypes.h"
+#include "nsStyleConsts.h"
+#include "gfxFont.h"
+#include "gfxFontConstants.h"
+
+nsLookAndFeel::nsLookAndFeel() : mInitialized(false) {}
+
+nsLookAndFeel::~nsLookAndFeel() {}
+
+static nscolor GetColorFromUIColor(UIColor* aColor) {
+ CGColorRef cgColor = [aColor CGColor];
+ CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(cgColor));
+ const CGFloat* components = CGColorGetComponents(cgColor);
+ if (model == kCGColorSpaceModelRGB) {
+ return NS_RGB((unsigned int)(components[0] * 255.0),
+ (unsigned int)(components[1] * 255.0),
+ (unsigned int)(components[2] * 255.0));
+ } else if (model == kCGColorSpaceModelMonochrome) {
+ unsigned int val = (unsigned int)(components[0] * 255.0);
+ return NS_RGBA(val, val, val, (unsigned int)(components[1] * 255.0));
+ }
+ MOZ_ASSERT_UNREACHABLE("Unhandled color space!");
+ return 0;
+}
+
+void nsLookAndFeel::NativeInit() { EnsureInit(); }
+
+void nsLookAndFeel::RefreshImpl() {
+ nsXPLookAndFeel::RefreshImpl();
+
+ mInitialized = false;
+}
+
+nsresult nsLookAndFeel::NativeGetColor(ColorID, ColorScheme, nscolor& aResult) {
+ EnsureInit();
+
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case ColorID::Highlight:
+ aResult = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::MozMenuhover:
+ aResult = NS_RGB(0xee, 0xee, 0xee);
+ break;
+ case ColorID::Highlighttext:
+ case ColorID::MozMenuhovertext:
+ aResult = mColorTextSelectForeground;
+ break;
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ aResult = NS_TRANSPARENT;
+ break;
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ aResult = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ aResult = NS_40PERCENT_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ aResult = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::SpellCheckerUnderline:
+ aResult = NS_RGB(0xff, 0, 0);
+ break;
+
+ //
+ // css2 system colors http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
+ //
+ case ColorID::Buttontext:
+ case ColorID::MozButtonhovertext:
+ case ColorID::Captiontext:
+ case ColorID::Menutext:
+ case ColorID::Infotext:
+ case ColorID::Windowtext:
+ aResult = mColorDarkText;
+ break;
+ case ColorID::Activecaption:
+ aResult = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::Activeborder:
+ aResult = NS_RGB(0x00, 0x00, 0x00);
+ break;
+ case ColorID::Appworkspace:
+ aResult = NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::Background:
+ aResult = NS_RGB(0x63, 0x63, 0xCE);
+ break;
+ case ColorID::Buttonface:
+ case ColorID::MozButtonhoverface:
+ aResult = NS_RGB(0xF0, 0xF0, 0xF0);
+ break;
+ case ColorID::Buttonhighlight:
+ aResult = NS_RGB(0xFF, 0xFF, 0xFF);
+ break;
+ case ColorID::Buttonshadow:
+ aResult = NS_RGB(0xDC, 0xDC, 0xDC);
+ break;
+ case ColorID::Graytext:
+ aResult = NS_RGB(0x44, 0x44, 0x44);
+ break;
+ case ColorID::Inactiveborder:
+ aResult = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::Inactivecaption:
+ aResult = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::Inactivecaptiontext:
+ aResult = NS_RGB(0x45, 0x45, 0x45);
+ break;
+ case ColorID::Scrollbar:
+ aResult = NS_RGB(0, 0, 0); // XXX
+ break;
+ case ColorID::Threeddarkshadow:
+ aResult = NS_RGB(0xDC, 0xDC, 0xDC);
+ break;
+ case ColorID::Threedshadow:
+ aResult = NS_RGB(0xE0, 0xE0, 0xE0);
+ break;
+ case ColorID::Threedface:
+ aResult = NS_RGB(0xF0, 0xF0, 0xF0);
+ break;
+ case ColorID::Threedhighlight:
+ aResult = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::Threedlightshadow:
+ aResult = NS_RGB(0xDA, 0xDA, 0xDA);
+ break;
+ case ColorID::Menu:
+ aResult = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::Infobackground:
+ aResult = NS_RGB(0xFF, 0xFF, 0xC7);
+ break;
+ case ColorID::Windowframe:
+ aResult = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::Window:
+ case ColorID::MozField:
+ case ColorID::MozCombobox:
+ aResult = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::MozFieldtext:
+ case ColorID::MozComboboxtext:
+ aResult = mColorDarkText;
+ break;
+ case ColorID::MozDialog:
+ aResult = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::MozDialogtext:
+ case ColorID::MozCellhighlighttext:
+ case ColorID::Selecteditemtext:
+ case ColorID::MozColheadertext:
+ case ColorID::MozColheaderhovertext:
+ aResult = mColorDarkText;
+ break;
+ case ColorID::MozMacFocusring:
+ aResult = NS_RGB(0x3F, 0x98, 0xDD);
+ break;
+ case ColorID::MozMacMenutextdisable:
+ aResult = NS_RGB(0x88, 0x88, 0x88);
+ break;
+ case ColorID::MozMacMenutextselect:
+ aResult = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::MozMacDisabledtoolbartext:
+ aResult = NS_RGB(0x3F, 0x3F, 0x3F);
+ break;
+ case ColorID::MozCellhighlight:
+ case ColorID::Selecteditem:
+ // For inactive list selection
+ aResult = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::MozEventreerow:
+ // Background color of even list rows.
+ aResult = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::MozOddtreerow:
+ // Background color of odd list rows.
+ aResult = NS_TRANSPARENT;
+ break;
+ case ColorID::MozNativehyperlinktext:
+ // There appears to be no available system defined color. HARDCODING to
+ // the appropriate color.
+ aResult = NS_RGB(0x14, 0x4F, 0xAE);
+ break;
+ case ColorID::MozNativevisitedhyperlinktext:
+ // Safari defaults to the MacOS color implementation for visited links,
+ // which in turn uses systemPurpleColor, so we do the same here.
+ aResult = GetColorFromUIColor([UIColor systemPurpleColor]);
+ break;
+ default:
+ NS_WARNING("Someone asked nsILookAndFeel for a color I don't know about");
+ aResult = NS_RGB(0xff, 0xff, 0xff);
+ res = NS_ERROR_FAILURE;
+ break;
+ }
+
+ return res;
+}
+
+NS_IMETHODIMP
+nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ break;
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 3;
+ break;
+ case IntID::CaretBlinkTime:
+ aResult = 567;
+ break;
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+ case IntID::SelectTextfieldsOnKeyFocus:
+ // Select textfield content when focused by kbd
+ // used by nsEventStateManager::sTextfieldSelectModel
+ aResult = 1;
+ break;
+ case IntID::SubmenuDelay:
+ aResult = 200;
+ break;
+ case IntID::MenusCanOverlapOSBar:
+ // xul popups are not allowed to overlap the menubar.
+ aResult = 0;
+ break;
+ case IntID::SkipNavigatingDisabledMenuItem:
+ aResult = 1;
+ break;
+ case IntID::DragThresholdX:
+ case IntID::DragThresholdY:
+ aResult = 4;
+ break;
+ case IntID::ScrollArrowStyle:
+ aResult = eScrollArrow_None;
+ break;
+ case IntID::TreeOpenDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeCloseDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeLazyScrollDelay:
+ aResult = 150;
+ break;
+ case IntID::TreeScrollDelay:
+ aResult = 100;
+ break;
+ case IntID::TreeScrollLinesMax:
+ aResult = 3;
+ break;
+ case IntID::TabFocusModel:
+ aResult = 1; // default to just textboxes
+ break;
+ case IntID::ScrollToClick:
+ aResult = 0;
+ break;
+ case IntID::ChosenMenuItemsShouldBlink:
+ aResult = 1;
+ break;
+ case IntID::IMERawInputUnderlineStyle:
+ case IntID::IMEConvertedTextUnderlineStyle:
+ case IntID::IMESelectedRawTextUnderlineStyle:
+ case IntID::IMESelectedConvertedTextUnderline:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
+ break;
+ case IntID::ContextMenuOffsetVertical:
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 2;
+ break;
+ default:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ }
+ return res;
+}
+
+NS_IMETHODIMP
+nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 2.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 2.0f;
+ break;
+ default:
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ }
+
+ return res;
+}
+
+bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ // hack for now
+ if (aID == FontID::Window || aID == FontID::Document) {
+ aFontStyle.style = FontSlantStyle::Normal();
+ aFontStyle.weight = FontWeight::Normal();
+ aFontStyle.stretch = FontStretch::Normal();
+ aFontStyle.size = 14;
+ aFontStyle.systemFont = true;
+
+ aFontName.AssignLiteral("sans-serif");
+ return true;
+ }
+
+ // TODO: implement more here?
+ return false;
+}
+
+void nsLookAndFeel::EnsureInit() {
+ if (mInitialized) {
+ return;
+ }
+ mInitialized = true;
+
+ nscolor color;
+ GetColor(ColorID::TextSelectBackground, color);
+ if (color == 0x000000) {
+ mColorTextSelectForeground = NS_RGB(0xff, 0xff, 0xff);
+ } else {
+ mColorTextSelectForeground = NS_SAME_AS_FOREGROUND_COLOR;
+ }
+
+ mColorDarkText = GetColorFromUIColor([UIColor darkTextColor]);
+
+ RecordTelemetry();
+}
diff --git a/widget/uikit/nsScreenManager.h b/widget/uikit/nsScreenManager.h
new file mode 100644
index 0000000000..0d19728264
--- /dev/null
+++ b/widget/uikit/nsScreenManager.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsScreenManager_h_
+#define nsScreenManager_h_
+
+#include "nsBaseScreen.h"
+#include "nsIScreenManager.h"
+#include "nsCOMPtr.h"
+#include "nsRect.h"
+
+@class UIScreen;
+
+class UIKitScreen : public nsBaseScreen {
+ public:
+ explicit UIKitScreen(UIScreen* screen);
+ ~UIKitScreen() {}
+
+ NS_IMETHOD GetId(uint32_t* outId) override {
+ *outId = 0;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetRect(int32_t* aLeft, int32_t* aTop, int32_t* aWidth,
+ int32_t* aHeight) override;
+ NS_IMETHOD GetAvailRect(int32_t* aLeft, int32_t* aTop, int32_t* aWidth,
+ int32_t* aHeight) override;
+ NS_IMETHOD GetRectDisplayPix(int32_t* aLeft, int32_t* aTop, int32_t* aWidth,
+ int32_t* aHeight) override;
+ NS_IMETHOD GetAvailRectDisplayPix(int32_t* aLeft, int32_t* aTop,
+ int32_t* aWidth, int32_t* aHeight) override;
+ NS_IMETHOD GetPixelDepth(int32_t* aPixelDepth) override;
+ NS_IMETHOD GetColorDepth(int32_t* aColorDepth) override;
+ NS_IMETHOD GetContentsScaleFactor(double* aContentsScaleFactor) override;
+ NS_IMETHOD GetDefaultCSSScaleFactor(double* aScaleFactor) override {
+ return GetContentsScaleFactor(aScaleFactor);
+ }
+
+ private:
+ UIScreen* mScreen;
+};
+
+class UIKitScreenManager : public nsIScreenManager {
+ public:
+ UIKitScreenManager();
+
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSISCREENMANAGER
+
+ static LayoutDeviceIntRect GetBounds();
+
+ private:
+ virtual ~UIKitScreenManager() {}
+ // TODO: support >1 screen, iPad supports external displays
+ nsCOMPtr<nsIScreen> mScreen;
+};
+
+#endif // nsScreenManager_h_
diff --git a/widget/uikit/nsScreenManager.mm b/widget/uikit/nsScreenManager.mm
new file mode 100644
index 0000000000..da37a4199d
--- /dev/null
+++ b/widget/uikit/nsScreenManager.mm
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#import <UIKit/UIScreen.h>
+
+#include "gfxPoint.h"
+#include "nsScreenManager.h"
+#include "nsAppShell.h"
+
+static LayoutDeviceIntRect gScreenBounds;
+static bool gScreenBoundsSet = false;
+
+UIKitScreen::UIKitScreen(UIScreen* aScreen) { mScreen = [aScreen retain]; }
+
+NS_IMETHODIMP
+UIKitScreen::GetRect(int32_t* outX, int32_t* outY, int32_t* outWidth,
+ int32_t* outHeight) {
+ return GetRectDisplayPix(outX, outY, outWidth, outHeight);
+}
+
+NS_IMETHODIMP
+UIKitScreen::GetAvailRect(int32_t* outX, int32_t* outY, int32_t* outWidth,
+ int32_t* outHeight) {
+ return GetAvailRectDisplayPix(outX, outY, outWidth, outHeight);
+}
+
+NS_IMETHODIMP
+UIKitScreen::GetRectDisplayPix(int32_t* outX, int32_t* outY, int32_t* outWidth,
+ int32_t* outHeight) {
+ nsIntRect rect = UIKitScreenManager::GetBounds();
+ *outX = rect.x;
+ *outY = rect.y;
+ *outWidth = rect.width;
+ *outHeight = rect.height;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UIKitScreen::GetAvailRectDisplayPix(int32_t* outX, int32_t* outY,
+ int32_t* outWidth, int32_t* outHeight) {
+ CGRect rect = [mScreen applicationFrame];
+ CGFloat scale = [mScreen scale];
+
+ *outX = rect.origin.x * scale;
+ *outY = rect.origin.y * scale;
+ *outWidth = rect.size.width * scale;
+ *outHeight = rect.size.height * scale;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UIKitScreen::GetPixelDepth(int32_t* aPixelDepth) {
+ // Close enough.
+ *aPixelDepth = 24;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UIKitScreen::GetColorDepth(int32_t* aColorDepth) {
+ return GetPixelDepth(aColorDepth);
+}
+
+NS_IMETHODIMP
+UIKitScreen::GetContentsScaleFactor(double* aContentsScaleFactor) {
+ *aContentsScaleFactor = [mScreen scale];
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(UIKitScreenManager, nsIScreenManager)
+
+UIKitScreenManager::UIKitScreenManager()
+ : mScreen(new UIKitScreen([UIScreen mainScreen])) {}
+
+LayoutDeviceIntRect UIKitScreenManager::GetBounds() {
+ if (!gScreenBoundsSet) {
+ CGRect rect = [[UIScreen mainScreen] bounds];
+ CGFloat scale = [[UIScreen mainScreen] scale];
+ gScreenBounds.x = rect.origin.x * scale;
+ gScreenBounds.y = rect.origin.y * scale;
+ gScreenBounds.width = rect.size.width * scale;
+ gScreenBounds.height = rect.size.height * scale;
+ gScreenBoundsSet = true;
+ }
+ printf("UIKitScreenManager::GetBounds: %d %d %d %d\n", gScreenBounds.x,
+ gScreenBounds.y, gScreenBounds.width, gScreenBounds.height);
+ return gScreenBounds;
+}
+
+NS_IMETHODIMP
+UIKitScreenManager::GetPrimaryScreen(nsIScreen** outScreen) {
+ NS_IF_ADDREF(*outScreen = mScreen.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UIKitScreenManager::ScreenForRect(int32_t inLeft, int32_t inTop,
+ int32_t inWidth, int32_t inHeight,
+ nsIScreen** outScreen) {
+ return GetPrimaryScreen(outScreen);
+}
diff --git a/widget/uikit/nsWidgetFactory.mm b/widget/uikit/nsWidgetFactory.mm
new file mode 100644
index 0000000000..356e5b8cdf
--- /dev/null
+++ b/widget/uikit/nsWidgetFactory.mm
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsISupports.h"
+#include "mozilla/ModuleUtils.h"
+
+#include "nsWidgetsCID.h"
+
+#include "nsAppShell.h"
+#include "nsAppShellSingleton.h"
+#include "nsLookAndFeel.h"
+#include "nsScreenManager.h"
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(UIKitScreenManager)
+
+#include "GfxInfo.h"
+namespace mozilla {
+namespace widget {
+// This constructor should really be shared with all platforms.
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(GfxInfo, Init)
+} // namespace widget
+} // namespace mozilla
+
+NS_DEFINE_NAMED_CID(NS_APPSHELL_CID);
+NS_DEFINE_NAMED_CID(NS_SCREENMANAGER_CID);
+NS_DEFINE_NAMED_CID(NS_GFXINFO_CID);
+
+static const mozilla::Module::CIDEntry kWidgetCIDs[] = {
+ {&kNS_APPSHELL_CID, false, nullptr, nsAppShellConstructor},
+ {&kNS_SCREENMANAGER_CID, false, nullptr, UIKitScreenManagerConstructor},
+ {&kNS_GFXINFO_CID, false, nullptr, mozilla::widget::GfxInfoConstructor},
+ {nullptr}};
+
+static const mozilla::Module::ContractIDEntry kWidgetContracts[] = {
+ {"@mozilla.org/widget/appshell/uikit;1", &kNS_APPSHELL_CID},
+ {"@mozilla.org/gfx/screenmanager;1", &kNS_SCREENMANAGER_CID},
+ {"@mozilla.org/gfx/info;1", &kNS_GFXINFO_CID},
+ {nullptr}};
+
+static void nsWidgetUIKitModuleDtor() {
+ nsLookAndFeel::Shutdown();
+ nsAppShellShutdown();
+}
+
+extern const mozilla::Module kWidgetModule = {mozilla::Module::kVersion,
+ kWidgetCIDs,
+ kWidgetContracts,
+ nullptr,
+ nullptr,
+ nsAppShellInit,
+ nsWidgetUIKitModuleDtor};
diff --git a/widget/uikit/nsWindow.h b/widget/uikit/nsWindow.h
new file mode 100644
index 0000000000..5dad452930
--- /dev/null
+++ b/widget/uikit/nsWindow.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef NSWINDOW_H_
+#define NSWINDOW_H_
+
+#include "nsBaseWidget.h"
+#include "gfxPoint.h"
+
+#include "nsTArray.h"
+
+@class UIWindow;
+@class UIView;
+@class ChildView;
+
+class nsWindow final : public nsBaseWidget {
+ typedef nsBaseWidget Inherited;
+
+ public:
+ nsWindow();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsWindow, Inherited)
+
+ //
+ // nsIWidget
+ //
+
+ [[nodiscard]] virtual nsresult Create(
+ nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData = nullptr) override;
+ virtual void Destroy() override;
+ virtual void Show(bool aState) override;
+ virtual void Enable(bool aState) override {}
+ virtual bool IsEnabled() const override { return true; }
+ virtual bool IsVisible() const override { return mVisible; }
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+
+ virtual void SetBackgroundColor(const nscolor& aColor) override;
+ virtual void* GetNativeData(uint32_t aDataType) override;
+
+ virtual void Move(double aX, double aY) override;
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override;
+ void EnteredFullScreen(bool aFullScreen);
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+ virtual LayoutDeviceIntRect GetScreenBounds() override;
+ void ReportMoveEvent();
+ void ReportSizeEvent();
+ void ReportSizeModeEvent(nsSizeMode aMode);
+
+ CGFloat BackingScaleFactor();
+ void BackingScaleFactorChanged();
+ virtual float GetDPI() override {
+ // XXX: terrible
+ return 326.0f;
+ }
+ virtual double GetDefaultScaleInternal() override {
+ return BackingScaleFactor();
+ }
+ virtual int32_t RoundsWidgetCoordinatesTo() override;
+
+ virtual nsresult SetTitle(const nsAString& aTitle) override { return NS_OK; }
+
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+
+ void WillPaintWindow();
+ bool PaintWindow(LayoutDeviceIntRegion aRegion);
+
+ bool HasModalDescendents() { return false; }
+
+ // virtual nsresult
+ // NotifyIME(const IMENotification& aIMENotification) override;
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction);
+ virtual InputContext GetInputContext();
+ /*
+ virtual bool ExecuteNativeKeyBinding(
+ NativeKeyBindingsType aType,
+ const mozilla::WidgetKeyboardEvent& aEvent,
+ DoCommandCallback aCallback,
+ void* aCallbackData) override;
+ */
+
+ protected:
+ virtual ~nsWindow();
+ void BringToFront();
+ nsWindow* FindTopLevel();
+ bool IsTopLevel();
+ nsresult GetCurrentOffset(uint32_t& aOffset, uint32_t& aLength);
+ nsresult DeleteRange(int aOffset, int aLen);
+
+ void TearDownView();
+
+ ChildView* mNativeView;
+ bool mVisible;
+ nsSizeMode mSizeMode;
+ nsTArray<nsWindow*> mChildren;
+ nsWindow* mParent;
+ InputContext mInputContext;
+
+ void OnSizeChanged(const mozilla::gfx::IntSize& aSize);
+
+ static void DumpWindows();
+ static void DumpWindows(const nsTArray<nsWindow*>& wins, int indent = 0);
+ static void LogWindow(nsWindow* win, int index, int indent);
+};
+
+#endif /* NSWINDOW_H_ */
diff --git a/widget/uikit/nsWindow.mm b/widget/uikit/nsWindow.mm
new file mode 100644
index 0000000000..0c1a38c27c
--- /dev/null
+++ b/widget/uikit/nsWindow.mm
@@ -0,0 +1,767 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#import <UIKit/UIEvent.h>
+#import <UIKit/UIGraphics.h>
+#import <UIKit/UIInterface.h>
+#import <UIKit/UIScreen.h>
+#import <UIKit/UITapGestureRecognizer.h>
+#import <UIKit/UITouch.h>
+#import <UIKit/UIView.h>
+#import <UIKit/UIViewController.h>
+#import <UIKit/UIWindow.h>
+#import <QuartzCore/QuartzCore.h>
+
+#include <algorithm>
+
+#include "nsWindow.h"
+#include "nsScreenManager.h"
+#include "nsAppShell.h"
+
+#include "nsWidgetsCID.h"
+#include "nsGfxCIID.h"
+
+#include "gfxQuartzSurface.h"
+#include "gfxUtils.h"
+#include "gfxImageSurface.h"
+#include "gfxContext.h"
+#include "nsRegion.h"
+#include "Layers.h"
+#include "nsTArray.h"
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/MouseEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::layers;
+
+#define ALOG(args...) \
+ fprintf(stderr, args); \
+ fprintf(stderr, "\n")
+
+static LayoutDeviceIntPoint UIKitPointsToDevPixels(CGPoint aPoint,
+ CGFloat aBackingScale) {
+ return LayoutDeviceIntPoint(NSToIntRound(aPoint.x * aBackingScale),
+ NSToIntRound(aPoint.y * aBackingScale));
+}
+
+static CGRect DevPixelsToUIKitPoints(const LayoutDeviceIntRect& aRect,
+ CGFloat aBackingScale) {
+ return CGRectMake((CGFloat)aRect.x / aBackingScale,
+ (CGFloat)aRect.y / aBackingScale,
+ (CGFloat)aRect.width / aBackingScale,
+ (CGFloat)aRect.height / aBackingScale);
+}
+
+// Used to retain a Cocoa object for the remainder of a method's execution.
+class nsAutoRetainUIKitObject {
+ public:
+ nsAutoRetainUIKitObject(id anObject) { mObject = [anObject retain]; }
+ ~nsAutoRetainUIKitObject() { [mObject release]; }
+
+ private:
+ id mObject; // [STRONG]
+};
+
+@interface ChildView : UIView {
+ @public
+ nsWindow* mGeckoChild; // weak ref
+ BOOL mWaitingForPaint;
+ CFMutableDictionaryRef mTouches;
+ int mNextTouchID;
+}
+// sets up our view, attaching it to its owning gecko view
+- (id)initWithFrame:(CGRect)inFrame geckoChild:(nsWindow*)inChild;
+// Our Gecko child was Destroy()ed
+- (void)widgetDestroyed;
+// Tear down this ChildView
+- (void)delayedTearDown;
+- (void)sendMouseEvent:(EventMessage)aType
+ point:(LayoutDeviceIntPoint)aPoint
+ widget:(nsWindow*)aWindow;
+- (void)handleTap:(UITapGestureRecognizer*)sender;
+- (BOOL)isUsingMainThreadOpenGL;
+- (void)drawUsingOpenGL;
+- (void)drawUsingOpenGLCallback;
+- (void)sendTouchEvent:(EventMessage)aType
+ touches:(NSSet*)aTouches
+ widget:(nsWindow*)aWindow;
+// Event handling (UIResponder)
+- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event;
+- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event;
+- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event;
+- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event;
+@end
+
+@implementation ChildView
++ (Class)layerClass {
+ return [CAEAGLLayer class];
+}
+
+- (id)initWithFrame:(CGRect)inFrame geckoChild:(nsWindow*)inChild {
+ self.multipleTouchEnabled = YES;
+ if ((self = [super initWithFrame:inFrame])) {
+ mGeckoChild = inChild;
+ }
+ ALOG("[ChildView[%p] initWithFrame:] (mGeckoChild = %p)", (void*)self,
+ (void*)mGeckoChild);
+ self.opaque = YES;
+ self.alpha = 1.0;
+
+ UITapGestureRecognizer* tapRecognizer =
+ [[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(handleTap:)];
+ tapRecognizer.numberOfTapsRequired = 1;
+ [self addGestureRecognizer:tapRecognizer];
+
+ mTouches =
+ CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr);
+ mNextTouchID = 0;
+ return self;
+}
+
+- (void)widgetDestroyed {
+ mGeckoChild = nullptr;
+ CFRelease(mTouches);
+}
+
+- (void)delayedTearDown {
+ [self removeFromSuperview];
+ [self release];
+}
+
+- (void)sendMouseEvent:(EventMessage)aType
+ point:(LayoutDeviceIntPoint)aPoint
+ widget:(nsWindow*)aWindow {
+ WidgetMouseEvent event(true, aType, aWindow, WidgetMouseEvent::eReal,
+ WidgetMouseEvent::eNormal);
+
+ event.mRefPoint = aPoint;
+ event.mClickCount = 1;
+ event.button = MouseButton::ePrimary;
+ event.mTime = PR_IntervalNow();
+ event.inputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;
+
+ nsEventStatus status;
+ aWindow->DispatchEvent(&event, status);
+}
+
+- (void)handleTap:(UITapGestureRecognizer*)sender {
+ if (sender.state == UIGestureRecognizerStateEnded) {
+ ALOG("[ChildView[%p] handleTap]", self);
+ LayoutDeviceIntPoint lp = UIKitPointsToDevPixels(
+ [sender locationInView:self], [self contentScaleFactor]);
+ [self sendMouseEvent:eMouseMove point:lp widget:mGeckoChild];
+ [self sendMouseEvent:eMouseDown point:lp widget:mGeckoChild];
+ [self sendMouseEvent:eMouseUp point:lp widget:mGeckoChild];
+ }
+}
+
+- (void)sendTouchEvent:(EventMessage)aType
+ touches:(NSSet*)aTouches
+ widget:(nsWindow*)aWindow {
+ WidgetTouchEvent event(true, aType, aWindow);
+ // XXX: I think nativeEvent.timestamp * 1000 is probably usable here but
+ // I don't care that much right now.
+ event.mTime = PR_IntervalNow();
+ event.mTouches.SetCapacity(aTouches.count);
+ for (UITouch* touch in aTouches) {
+ LayoutDeviceIntPoint loc = UIKitPointsToDevPixels(
+ [touch locationInView:self], [self contentScaleFactor]);
+ LayoutDeviceIntPoint radius =
+ UIKitPointsToDevPixels([touch majorRadius], [touch majorRadius]);
+ void* value;
+ if (!CFDictionaryGetValueIfPresent(mTouches, touch, (const void**)&value)) {
+ // This shouldn't happen.
+ NS_ASSERTION(false, "Got a touch that we didn't know about");
+ continue;
+ }
+ int id = reinterpret_cast<int>(value);
+ RefPtr<Touch> t = new Touch(id, loc, radius, 0.0f, 1.0f);
+ event.mRefPoint = loc;
+ event.mTouches.AppendElement(t);
+ }
+ aWindow->DispatchInputEvent(&event);
+}
+
+- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
+ ALOG("[ChildView[%p] touchesBegan", self);
+ if (!mGeckoChild) return;
+
+ for (UITouch* touch : touches) {
+ CFDictionaryAddValue(mTouches, touch, (void*)mNextTouchID);
+ mNextTouchID++;
+ }
+ [self sendTouchEvent:eTouchStart
+ touches:[event allTouches]
+ widget:mGeckoChild];
+}
+
+- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
+ ALOG("[ChildView[%p] touchesCancelled", self);
+ [self sendTouchEvent:eTouchCancel touches:touches widget:mGeckoChild];
+ for (UITouch* touch : touches) {
+ CFDictionaryRemoveValue(mTouches, touch);
+ }
+ if (CFDictionaryGetCount(mTouches) == 0) {
+ mNextTouchID = 0;
+ }
+}
+
+- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
+ ALOG("[ChildView[%p] touchesEnded", self);
+ if (!mGeckoChild) return;
+
+ [self sendTouchEvent:eTouchEnd touches:touches widget:mGeckoChild];
+ for (UITouch* touch : touches) {
+ CFDictionaryRemoveValue(mTouches, touch);
+ }
+ if (CFDictionaryGetCount(mTouches) == 0) {
+ mNextTouchID = 0;
+ }
+}
+
+- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
+ ALOG("[ChildView[%p] touchesMoved", self);
+ if (!mGeckoChild) return;
+
+ [self sendTouchEvent:eTouchMove
+ touches:[event allTouches]
+ widget:mGeckoChild];
+}
+
+- (void)setNeedsDisplayInRect:(CGRect)aRect {
+ if ([self isUsingMainThreadOpenGL]) {
+ // Draw without calling drawRect. This prevent us from
+ // needing to access the normal window buffer surface unnecessarily, so we
+ // waste less time synchronizing the two surfaces.
+ if (!mWaitingForPaint) {
+ mWaitingForPaint = YES;
+ // Use NSRunLoopCommonModes instead of the default NSDefaultRunLoopMode
+ // so that the timer also fires while a native menu is open.
+ [self performSelector:@selector(drawUsingOpenGLCallback)
+ withObject:nil
+ afterDelay:0
+ inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
+ }
+ }
+}
+
+- (BOOL)isUsingMainThreadOpenGL {
+ if (!mGeckoChild || ![self window]) return NO;
+
+ return NO;
+}
+
+- (void)drawUsingOpenGL {
+ ALOG("drawUsingOpenGL");
+ AUTO_PROFILER_LABEL("ChildView::drawUsingOpenGL", OTHER);
+
+ if (!mGeckoChild->IsVisible()) return;
+
+ mWaitingForPaint = NO;
+
+ LayoutDeviceIntRect geckoBounds = mGeckoChild->GetBounds();
+ LayoutDeviceIntRegion region(geckoBounds);
+
+ mGeckoChild->PaintWindow(region);
+}
+
+// Called asynchronously after setNeedsDisplay in order to avoid entering the
+// normal drawing machinery.
+- (void)drawUsingOpenGLCallback {
+ if (mWaitingForPaint) {
+ [self drawUsingOpenGL];
+ }
+}
+
+// The display system has told us that a portion of our view is dirty. Tell
+// gecko to paint it
+- (void)drawRect:(CGRect)aRect {
+ CGContextRef cgContext = UIGraphicsGetCurrentContext();
+ [self drawRect:aRect inContext:cgContext];
+}
+
+- (void)drawRect:(CGRect)aRect inContext:(CGContextRef)aContext {
+#ifdef DEBUG_UPDATE
+ LayoutDeviceIntRect geckoBounds = mGeckoChild->GetBounds();
+
+ fprintf(stderr,
+ "---- Update[%p][%p] [%f %f %f %f] cgc: %p\n gecko bounds: [%d %d "
+ "%d %d]\n",
+ self, mGeckoChild, aRect.origin.x, aRect.origin.y, aRect.size.width,
+ aRect.size.height, aContext, geckoBounds.x, geckoBounds.y,
+ geckoBounds.width, geckoBounds.height);
+
+ CGAffineTransform xform = CGContextGetCTM(aContext);
+ fprintf(stderr, " xform in: [%f %f %f %f %f %f]\n", xform.a, xform.b,
+ xform.c, xform.d, xform.tx, xform.ty);
+#endif
+
+ if (true) {
+ // For Gecko-initiated repaints in OpenGL mode, drawUsingOpenGL is
+ // directly called from a delayed perform callback - without going through
+ // drawRect.
+ // Paints that come through here are triggered by something that Cocoa
+ // controls, for example by window resizing or window focus changes.
+
+ // Do GL composition and return.
+ [self drawUsingOpenGL];
+ return;
+ }
+ AUTO_PROFILER_LABEL("ChildView::drawRect", OTHER);
+
+ // The CGContext that drawRect supplies us with comes with a transform that
+ // scales one user space unit to one Cocoa point, which can consist of
+ // multiple dev pixels. But Gecko expects its supplied context to be scaled
+ // to device pixels, so we need to reverse the scaling.
+ double scale = mGeckoChild->BackingScaleFactor();
+ CGContextSaveGState(aContext);
+ CGContextScaleCTM(aContext, 1.0 / scale, 1.0 / scale);
+
+ CGSize viewSize = [self bounds].size;
+ gfx::IntSize backingSize(viewSize.width * scale, viewSize.height * scale);
+
+ CGContextSaveGState(aContext);
+
+ LayoutDeviceIntRegion region =
+ LayoutDeviceIntRect(NSToIntRound(aRect.origin.x * scale),
+ NSToIntRound(aRect.origin.y * scale),
+ NSToIntRound(aRect.size.width * scale),
+ NSToIntRound(aRect.size.height * scale));
+
+ // Create Cairo objects.
+ RefPtr<gfxQuartzSurface> targetSurface;
+
+ UniquePtrPtr<gfxContext> targetContext;
+ if (gfxPlatform::GetPlatform()->SupportsAzureContentForType(
+ gfx::BackendType::CAIRO)) {
+ // This is dead code unless you mess with prefs, but keep it around for
+ // debugging.
+ targetSurface = new gfxQuartzSurface(aContext, backingSize);
+ targetSurface->SetAllowUseAsSource(false);
+ RefPtr<gfx::DrawTarget> dt =
+ gfxPlatform::CreateDrawTargetForSurface(targetSurface, backingSize);
+ if (!dt || !dt->IsValid()) {
+ gfxDevCrash(mozilla::gfx::LogReason::InvalidContext)
+ << "Window context problem 2 " << backingSize;
+ return;
+ }
+ targetContext = gfxContext::CreateOrNull(dt);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("COREGRAPHICS is the only supported backend");
+ }
+ MOZ_ASSERT(targetContext); // already checked for valid draw targets above
+
+ // Set up the clip region.
+ targetContext->NewPath();
+ for (auto iter = region.RectIter(); !iter.Done(); iter.Next()) {
+ const LayoutDeviceIntRect& r = iter.Get();
+ targetContext->Rectangle(gfxRect(r.x, r.y, r.width, r.height));
+ }
+ targetContext->Clip();
+
+ // nsAutoRetainCocoaObject kungFuDeathGrip(self);
+ bool painted = false;
+ targetContext = nullptr;
+ targetSurface = nullptr;
+
+ CGContextRestoreGState(aContext);
+
+ // Undo the scale transform so that from now on the context is in
+ // CocoaPoints again.
+ CGContextRestoreGState(aContext);
+ if (!painted && [self isOpaque]) {
+ // Gecko refused to draw, but we've claimed to be opaque, so we have to
+ // draw something--fill with white.
+ CGContextSetRGBFillColor(aContext, 1, 1, 1, 1);
+ CGContextFillRect(aContext, aRect);
+ }
+
+#ifdef DEBUG_UPDATE
+ fprintf(stderr, "---- update done ----\n");
+
+# if 0
+ CGContextSetRGBStrokeColor (aContext,
+ ((((unsigned long)self) & 0xff)) / 255.0,
+ ((((unsigned long)self) & 0xff00) >> 8) / 255.0,
+ ((((unsigned long)self) & 0xff0000) >> 16) / 255.0,
+ 0.5);
+# endif
+ CGContextSetRGBStrokeColor(aContext, 1, 0, 0, 0.8);
+ CGContextSetLineWidth(aContext, 4.0);
+ CGContextStrokeRect(aContext, aRect);
+#endif
+}
+@end
+
+nsWindow::nsWindow()
+ : mNativeView(nullptr),
+ mVisible(false),
+ mSizeMode(nsSizeMode_Normal),
+ mParent(nullptr) {}
+
+nsWindow::~nsWindow() {
+ [mNativeView widgetDestroyed]; // Safe if mNativeView is nil.
+ TearDownView(); // Safe if called twice.
+}
+
+void nsWindow::TearDownView() {
+ if (!mNativeView) return;
+
+ [mNativeView performSelectorOnMainThread:@selector(delayedTearDown)
+ withObject:nil
+ waitUntilDone:false];
+ mNativeView = nil;
+}
+
+bool nsWindow::IsTopLevel() {
+ return mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog ||
+ mWindowType == WindowType::Invisible;
+}
+
+//
+// nsIWidget
+//
+
+nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ ALOG("nsWindow[%p]::Create %p/%p [%d %d %d %d]", (void*)this, (void*)aParent,
+ (void*)aNativeParent, aRect.x, aRect.y, aRect.width, aRect.height);
+ nsWindow* parent = (nsWindow*)aParent;
+ ChildView* nativeParent = (ChildView*)aNativeParent;
+
+ if (parent == nullptr && nativeParent) parent = nativeParent->mGeckoChild;
+ if (parent && nativeParent == nullptr) nativeParent = parent->mNativeView;
+
+ // for toplevel windows, bounds are fixed to full screen size
+ if (parent == nullptr) {
+ if (nsAppShell::gWindow == nil) {
+ mBounds = UIKitScreenManager::GetBounds();
+ } else {
+ CGRect cgRect = [nsAppShell::gWindow bounds];
+ mBounds.x = cgRect.origin.x;
+ mBounds.y = cgRect.origin.y;
+ mBounds.width = cgRect.size.width;
+ mBounds.height = cgRect.size.height;
+ }
+ } else {
+ mBounds = aRect;
+ }
+
+ ALOG("nsWindow[%p]::Create bounds: %d %d %d %d", (void*)this, mBounds.x,
+ mBounds.y, mBounds.width, mBounds.height);
+
+ // Set defaults which can be overriden from aInitData in BaseCreate
+ mWindowType = WindowType::TopLevel;
+ mBorderStyle = BorderStyle::Default;
+
+ Inherited::BaseCreate(aParent, aInitData);
+
+ NS_ASSERTION(IsTopLevel() || parent,
+ "non top level window doesn't have a parent!");
+
+ mNativeView = [[ChildView alloc]
+ initWithFrame:DevPixelsToUIKitPoints(mBounds, BackingScaleFactor())
+ geckoChild:this];
+ mNativeView.hidden = YES;
+
+ if (parent) {
+ parent->mChildren.AppendElement(this);
+ mParent = parent;
+ }
+
+ if (nativeParent) {
+ [nativeParent addSubview:mNativeView];
+ } else if (nsAppShell::gWindow) {
+ [nsAppShell::gWindow.rootViewController.view addSubview:mNativeView];
+ } else {
+ [nsAppShell::gTopLevelViews addObject:mNativeView];
+ }
+
+ return NS_OK;
+}
+
+void nsWindow::Destroy() {
+ for (uint32_t i = 0; i < mChildren.Length(); ++i) {
+ // why do we still have children?
+ mChildren[i]->SetParent(nullptr);
+ }
+
+ if (mParent) mParent->mChildren.RemoveElement(this);
+
+ [mNativeView widgetDestroyed];
+
+ nsBaseWidget::Destroy();
+
+ // ReportDestroyEvent();
+
+ TearDownView();
+
+ nsBaseWidget::OnDestroy();
+
+ return NS_OK;
+}
+
+void nsWindow::Show(bool aState) {
+ if (aState != mVisible) {
+ mNativeView.hidden = aState ? NO : YES;
+ if (aState) {
+ UIView* parentView = mParent
+ ? mParent->mNativeView
+ : nsAppShell::gWindow.rootViewController.view;
+ [parentView bringSubviewToFront:mNativeView];
+ [mNativeView setNeedsDisplay];
+ }
+ mVisible = aState;
+ }
+}
+
+void nsWindow::Move(double aX, double aY) {
+ if (!mNativeView || (mBounds.x == aX && mBounds.y == aY)) return;
+
+ // XXX: handle this
+ // The point we have is in Gecko coordinates (origin top-left). Convert
+ // it to Cocoa ones (origin bottom-left).
+ mBounds.x = aX;
+ mBounds.y = aY;
+
+ mNativeView.frame = DevPixelsToUIKitPoints(mBounds, BackingScaleFactor());
+
+ if (mVisible) [mNativeView setNeedsDisplay];
+
+ ReportMoveEvent();
+}
+
+void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ BOOL isMoving = (mBounds.x != aX || mBounds.y != aY);
+ BOOL isResizing = (mBounds.width != aWidth || mBounds.height != aHeight);
+ if (!mNativeView || (!isMoving && !isResizing)) return;
+
+ if (isMoving) {
+ mBounds.x = aX;
+ mBounds.y = aY;
+ }
+ if (isResizing) {
+ mBounds.width = aWidth;
+ mBounds.height = aHeight;
+ }
+
+ [mNativeView setFrame:DevPixelsToUIKitPoints(mBounds, BackingScaleFactor())];
+
+ if (mVisible && aRepaint) [mNativeView setNeedsDisplay];
+
+ if (isMoving) ReportMoveEvent();
+
+ if (isResizing) ReportSizeEvent();
+}
+
+void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
+ if (!mNativeView || (mBounds.width == aWidth && mBounds.height == aHeight))
+ return;
+
+ mBounds.width = aWidth;
+ mBounds.height = aHeight;
+
+ [mNativeView setFrame:DevPixelsToUIKitPoints(mBounds, BackingScaleFactor())];
+
+ if (mVisible && aRepaint) [mNativeView setNeedsDisplay];
+
+ ReportSizeEvent();
+}
+
+void nsWindow::SetSizeMode(nsSizeMode aMode) {
+ if (aMode == static_cast<int32_t>(mSizeMode)) {
+ return;
+ }
+
+ mSizeMode = static_cast<nsSizeMode>(aMode);
+ if (aMode == nsSizeMode_Maximized || aMode == nsSizeMode_Fullscreen) {
+ // Resize to fill screen
+ nsBaseWidget::InfallibleMakeFullScreen(true);
+ }
+ ReportSizeModeEvent(aMode);
+}
+
+void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) {
+ if (!mNativeView || !mVisible) return;
+
+ MOZ_RELEASE_ASSERT(
+ GetLayerManager()->GetBackendType() != LayersBackend::LAYERS_WR,
+ "Shouldn't need to invalidate with accelerated OMTC layers!");
+
+ [mNativeView setNeedsLayout];
+ [mNativeView setNeedsDisplayInRect:DevPixelsToUIKitPoints(
+ mBounds, BackingScaleFactor())];
+}
+
+void nsWindow::SetFocus(Raise) {
+ [[mNativeView window] makeKeyWindow];
+ [mNativeView becomeFirstResponder];
+}
+
+void nsWindow::WillPaintWindow() {
+ if (mWidgetListener) {
+ mWidgetListener->WillPaintWindow(this);
+ }
+}
+
+bool nsWindow::PaintWindow(LayoutDeviceIntRegion aRegion) {
+ if (!mWidgetListener) return false;
+
+ bool returnValue = false;
+ returnValue = mWidgetListener->PaintWindow(this, aRegion);
+
+ if (mWidgetListener) {
+ mWidgetListener->DidPaintWindow();
+ }
+
+ return returnValue;
+}
+
+void nsWindow::ReportMoveEvent() { NotifyWindowMoved(mBounds.x, mBounds.y); }
+
+void nsWindow::ReportSizeModeEvent(nsSizeMode aMode) {
+ if (mWidgetListener) {
+ // This is terrible.
+ nsSizeMode theMode;
+ switch (aMode) {
+ case nsSizeMode_Maximized:
+ theMode = nsSizeMode_Maximized;
+ break;
+ case nsSizeMode_Fullscreen:
+ theMode = nsSizeMode_Fullscreen;
+ break;
+ default:
+ return;
+ }
+ mWidgetListener->SizeModeChanged(theMode);
+ }
+}
+
+void nsWindow::ReportSizeEvent() {
+ if (mWidgetListener) {
+ LayoutDeviceIntRect innerBounds = GetClientBounds();
+ mWidgetListener->WindowResized(this, innerBounds.width, innerBounds.height);
+ }
+}
+
+LayoutDeviceIntRect nsWindow::GetScreenBounds() {
+ return LayoutDeviceIntRect(WidgetToScreenOffset(), mBounds.Size());
+}
+
+LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() {
+ LayoutDeviceIntPoint offset(0, 0);
+ if (mParent) {
+ offset = mParent->WidgetToScreenOffset();
+ }
+
+ CGPoint temp = [mNativeView convertPoint:temp toView:nil];
+
+ if (!mParent && nsAppShell::gWindow) {
+ // convert to screen coords
+ temp = [nsAppShell::gWindow convertPoint:temp toWindow:nil];
+ }
+
+ offset.x += temp.x;
+ offset.y += temp.y;
+
+ return offset;
+}
+
+nsresult nsWindow::DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) {
+ aStatus = nsEventStatus_eIgnore;
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(aEvent->mWidget);
+
+ if (mWidgetListener)
+ aStatus = mWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+
+ return NS_OK;
+}
+
+void nsWindow::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ // TODO: actually show VKB
+ mInputContext = aContext;
+}
+
+mozilla::widget::InputContext nsWindow::GetInputContext() {
+ return mInputContext;
+}
+
+void nsWindow::SetBackgroundColor(const nscolor& aColor) {
+ mNativeView.backgroundColor = [UIColor colorWithRed:NS_GET_R(aColor)
+ green:NS_GET_G(aColor)
+ blue:NS_GET_B(aColor)
+ alpha:NS_GET_A(aColor)];
+}
+
+void* nsWindow::GetNativeData(uint32_t aDataType) {
+ void* retVal = nullptr;
+
+ switch (aDataType) {
+ case NS_NATIVE_WIDGET:
+ retVal = (void*)mNativeView;
+ break;
+
+ case NS_NATIVE_WINDOW:
+ retVal = [mNativeView window];
+ break;
+
+ case NS_NATIVE_GRAPHIC:
+ NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a UIKit child view!");
+ break;
+
+ case NS_NATIVE_OFFSETX:
+ retVal = 0;
+ break;
+
+ case NS_NATIVE_OFFSETY:
+ retVal = 0;
+ break;
+
+ case NS_RAW_NATIVE_IME_CONTEXT:
+ retVal = GetPseudoIMEContext();
+ if (retVal) {
+ break;
+ }
+ retVal = NS_ONLY_ONE_NATIVE_IME_CONTEXT;
+ break;
+ }
+
+ return retVal;
+}
+
+CGFloat nsWindow::BackingScaleFactor() {
+ if (mNativeView) {
+ return [mNativeView contentScaleFactor];
+ }
+ return [UIScreen mainScreen].scale;
+}
+
+int32_t nsWindow::RoundsWidgetCoordinatesTo() {
+ if (BackingScaleFactor() == 2.0) {
+ return 2;
+ }
+ return 1;
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow();
+ return window.forget();
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow();
+ return window.forget();
+}
diff --git a/widget/windows/AudioSession.cpp b/widget/windows/AudioSession.cpp
new file mode 100644
index 0000000000..c14278f56c
--- /dev/null
+++ b/widget/windows/AudioSession.cpp
@@ -0,0 +1,346 @@
+/* -*- 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 <atomic>
+#include <audiopolicy.h>
+#include <windows.h>
+#include <mmdeviceapi.h>
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIStringBundle.h"
+
+#include "nsCOMPtr.h"
+#include "nsID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/mscom/AgileReference.h"
+#include "mozilla/mscom/Utils.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/WindowsVersion.h"
+
+namespace mozilla {
+namespace widget {
+
+/*
+ * To take advantage of what Vista+ have to offer with respect to audio,
+ * we need to maintain an audio session. This class wraps IAudioSessionControl
+ * and implements IAudioSessionEvents (for callbacks from Windows)
+ */
+class AudioSession final : public IAudioSessionEvents {
+ public:
+ AudioSession();
+
+ static AudioSession* GetSingleton();
+
+ // COM IUnknown
+ STDMETHODIMP_(ULONG) AddRef();
+ STDMETHODIMP QueryInterface(REFIID, void**);
+ STDMETHODIMP_(ULONG) Release();
+
+ // IAudioSessionEvents
+ STDMETHODIMP OnChannelVolumeChanged(DWORD aChannelCount,
+ float aChannelVolumeArray[],
+ DWORD aChangedChannel, LPCGUID aContext);
+ STDMETHODIMP OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext);
+ STDMETHODIMP OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext);
+ STDMETHODIMP OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext);
+ STDMETHODIMP OnSessionDisconnected(AudioSessionDisconnectReason aReason);
+ STDMETHODIMP OnSimpleVolumeChanged(float aVolume, BOOL aMute,
+ LPCGUID aContext);
+ STDMETHODIMP OnStateChanged(AudioSessionState aState);
+
+ void Start();
+ void Stop(bool shouldRestart = false);
+
+ nsresult GetSessionData(nsID& aID, nsString& aSessionName,
+ nsString& aIconPath);
+ nsresult SetSessionData(const nsID& aID, const nsString& aSessionName,
+ const nsString& aIconPath);
+
+ private:
+ ~AudioSession() = default;
+
+ void StopInternal(const MutexAutoLock& aProofOfLock,
+ bool shouldRestart = false);
+
+ protected:
+ RefPtr<IAudioSessionControl> mAudioSessionControl;
+ nsString mDisplayName;
+ nsString mIconPath;
+ nsID mSessionGroupingParameter;
+ // Guards the IAudioSessionControl
+ mozilla::Mutex mMutex MOZ_UNANNOTATED;
+
+ ThreadSafeAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+};
+
+StaticRefPtr<AudioSession> sService;
+
+void StartAudioSession() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!sService);
+ sService = new AudioSession();
+
+ // Destroy AudioSession only after any background task threads have been
+ // stopped or abandoned.
+ ClearOnShutdown(&sService, ShutdownPhase::XPCOMShutdownFinal);
+
+ NS_DispatchBackgroundTask(
+ NS_NewCancelableRunnableFunction("StartAudioSession", []() -> void {
+ MOZ_ASSERT(AudioSession::GetSingleton(),
+ "AudioSession should outlive background threads");
+ AudioSession::GetSingleton()->Start();
+ }));
+}
+
+void StopAudioSession() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(sService);
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction("StopAudioSession", []() -> void {
+ MOZ_ASSERT(AudioSession::GetSingleton(),
+ "AudioSession should outlive background threads");
+ AudioSession::GetSingleton()->Stop();
+ }));
+}
+
+AudioSession* AudioSession::GetSingleton() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ return sService;
+}
+
+// It appears Windows will use us on a background thread ...
+NS_IMPL_ADDREF(AudioSession)
+NS_IMPL_RELEASE(AudioSession)
+
+STDMETHODIMP
+AudioSession::QueryInterface(REFIID iid, void** ppv) {
+ const IID IID_IAudioSessionEvents = __uuidof(IAudioSessionEvents);
+ if ((IID_IUnknown == iid) || (IID_IAudioSessionEvents == iid)) {
+ *ppv = static_cast<IAudioSessionEvents*>(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+AudioSession::AudioSession() : mMutex("AudioSessionControl") {
+ // This func must be run on the main thread as
+ // nsStringBundle is not thread safe otherwise
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Should only get here in a chrome process!");
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ MOZ_ASSERT(bundleService);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ bundleService->CreateBundle("chrome://branding/locale/brand.properties",
+ getter_AddRefs(bundle));
+ MOZ_ASSERT(bundle);
+ bundle->GetStringFromName("brandFullName", mDisplayName);
+
+ wchar_t* buffer;
+ mIconPath.GetMutableData(&buffer, MAX_PATH);
+ ::GetModuleFileNameW(nullptr, buffer, MAX_PATH);
+
+ [[maybe_unused]] nsresult rv =
+ nsID::GenerateUUIDInPlace(mSessionGroupingParameter);
+ MOZ_ASSERT(rv == NS_OK);
+}
+
+// Once we are started Windows will hold a reference to us through our
+// IAudioSessionEvents interface that will keep us alive until the appshell
+// calls Stop.
+void AudioSession::Start() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+
+ const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
+ const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
+ const IID IID_IAudioSessionManager = __uuidof(IAudioSessionManager);
+
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(!mAudioSessionControl);
+ MOZ_ASSERT(!mDisplayName.IsEmpty() || !mIconPath.IsEmpty(),
+ "Should never happen ...");
+
+ auto scopeExit = MakeScopeExit([&] { StopInternal(lock); });
+
+ RefPtr<IMMDeviceEnumerator> enumerator;
+ HRESULT hr =
+ ::CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL,
+ IID_IMMDeviceEnumerator, getter_AddRefs(enumerator));
+ if (FAILED(hr)) {
+ return;
+ }
+
+ RefPtr<IMMDevice> device;
+ hr = enumerator->GetDefaultAudioEndpoint(
+ EDataFlow::eRender, ERole::eMultimedia, getter_AddRefs(device));
+ if (FAILED(hr)) {
+ return;
+ }
+
+ RefPtr<IAudioSessionManager> manager;
+ hr = device->Activate(IID_IAudioSessionManager, CLSCTX_ALL, nullptr,
+ getter_AddRefs(manager));
+ if (FAILED(hr)) {
+ return;
+ }
+
+ hr = manager->GetAudioSessionControl(&GUID_NULL, 0,
+ getter_AddRefs(mAudioSessionControl));
+
+ if (FAILED(hr) || !mAudioSessionControl) {
+ return;
+ }
+
+ // Increments refcount of 'this'.
+ hr = mAudioSessionControl->RegisterAudioSessionNotification(this);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ hr = mAudioSessionControl->SetGroupingParam(
+ (LPGUID) & (mSessionGroupingParameter), nullptr);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ hr = mAudioSessionControl->SetDisplayName(mDisplayName.get(), nullptr);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ hr = mAudioSessionControl->SetIconPath(mIconPath.get(), nullptr);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ scopeExit.release();
+}
+
+void AudioSession::Stop(bool shouldRestart) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+
+ MutexAutoLock lock(mMutex);
+ StopInternal(lock, shouldRestart);
+}
+
+void AudioSession::StopInternal(const MutexAutoLock& aProofOfLock,
+ bool shouldRestart) {
+ if (!mAudioSessionControl) {
+ return;
+ }
+
+ // Decrement refcount of 'this'
+ mAudioSessionControl->UnregisterAudioSessionNotification(this);
+
+ // Deleting the IAudioSessionControl COM object requires the STA/main thread.
+ // Audio code may concurrently be running on the main thread and it may
+ // block waiting for this to complete, creating deadlock. So we destroy the
+ // IAudioSessionControl on the main thread instead. In order to do that, we
+ // need to marshall the object to the main thread's apartment with an
+ // AgileReference.
+ mscom::AgileReference agileAsc(mAudioSessionControl);
+ mAudioSessionControl = nullptr;
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "FreeAudioSession",
+ [agileAsc = std::move(agileAsc), shouldRestart]() mutable {
+ // Now release the AgileReference which holds our only reference to the
+ // IAudioSessionControl, then maybe restart.
+ agileAsc = nullptr;
+ if (shouldRestart) {
+ NS_DispatchBackgroundTask(
+ NS_NewCancelableRunnableFunction("RestartAudioSession", [] {
+ AudioSession* as = AudioSession::GetSingleton();
+ MOZ_ASSERT(as);
+ as->Start();
+ }));
+ }
+ }));
+}
+
+void CopynsID(nsID& lhs, const nsID& rhs) {
+ lhs.m0 = rhs.m0;
+ lhs.m1 = rhs.m1;
+ lhs.m2 = rhs.m2;
+ for (int i = 0; i < 8; i++) {
+ lhs.m3[i] = rhs.m3[i];
+ }
+}
+
+nsresult AudioSession::GetSessionData(nsID& aID, nsString& aSessionName,
+ nsString& aIconPath) {
+ CopynsID(aID, mSessionGroupingParameter);
+ aSessionName = mDisplayName;
+ aIconPath = mIconPath;
+
+ return NS_OK;
+}
+
+nsresult AudioSession::SetSessionData(const nsID& aID,
+ const nsString& aSessionName,
+ const nsString& aIconPath) {
+ MOZ_ASSERT(!XRE_IsParentProcess(),
+ "Should never get here in a chrome process!");
+ CopynsID(mSessionGroupingParameter, aID);
+ mDisplayName = aSessionName;
+ mIconPath = aIconPath;
+ return NS_OK;
+}
+
+STDMETHODIMP
+AudioSession::OnChannelVolumeChanged(DWORD aChannelCount,
+ float aChannelVolumeArray[],
+ DWORD aChangedChannel, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnSessionDisconnected(AudioSessionDisconnectReason aReason) {
+ Stop(true /* shouldRestart */);
+ return S_OK;
+}
+
+STDMETHODIMP
+AudioSession::OnSimpleVolumeChanged(float aVolume, BOOL aMute,
+ LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnStateChanged(AudioSessionState aState) {
+ return S_OK; // NOOP
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/AudioSession.h b/widget/windows/AudioSession.h
new file mode 100644
index 0000000000..612a5e209b
--- /dev/null
+++ b/widget/windows/AudioSession.h
@@ -0,0 +1,19 @@
+/* -*- 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 "nsString.h"
+
+namespace mozilla {
+namespace widget {
+
+// Start the audio session in the current process
+void StartAudioSession();
+
+// Stop the audio session in the current process
+void StopAudioSession();
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/CheckInvariantWrapper.h b/widget/windows/CheckInvariantWrapper.h
new file mode 100644
index 0000000000..da6024ec21
--- /dev/null
+++ b/widget/windows/CheckInvariantWrapper.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+// A wrapper that uses RAII to ensure that a class invariant is checked
+// before and after any public function is called
+
+#ifndef CHECKINVARIANTWRAPPER_H_
+#define CHECKINVARIANTWRAPPER_H_
+
+#include "mozilla/Attributes.h"
+#include <utility>
+
+namespace mozilla {
+
+//
+// Wraps an object of type T and allows access to its public API by
+// deferencing it using the pointer syntax "->".
+//
+// Using that operator will return a temporary RAII object that
+// calls a method named "CheckInvariant" in its constructor, calls the
+// requested method, and then calls "CheckInvariant" again in its
+// destructor.
+//
+// The only thing your class requires is a method with the following signature:
+//
+// void CheckInvariant() const;
+//
+template <typename T>
+class CheckInvariantWrapper {
+ public:
+ class Wrapper {
+ public:
+ explicit Wrapper(T& aObject) : mObject(aObject) {
+ mObject.CheckInvariant();
+ }
+ ~Wrapper() { mObject.CheckInvariant(); }
+
+ T* operator->() { return &mObject; }
+
+ private:
+ T& mObject;
+ };
+
+ class ConstWrapper {
+ public:
+ explicit ConstWrapper(const T& aObject) : mObject(aObject) {
+ mObject.CheckInvariant();
+ }
+ ~ConstWrapper() { mObject.CheckInvariant(); }
+
+ const T* operator->() const { return &mObject; }
+
+ private:
+ const T& mObject;
+ };
+
+ CheckInvariantWrapper() = default;
+
+ MOZ_IMPLICIT CheckInvariantWrapper(T aObject) : mObject(std::move(aObject)) {}
+
+ template <typename... Args>
+ explicit CheckInvariantWrapper(std::in_place_t, Args&&... args)
+ : mObject(std::forward<Args>(args)...) {}
+
+ const ConstWrapper operator->() const { return ConstWrapper(mObject); }
+
+ Wrapper operator->() { return Wrapper(mObject); }
+
+ private:
+ T mObject;
+};
+
+} // namespace mozilla
+
+#endif // CHECKINVARIANTWRAPPER_H_
diff --git a/widget/windows/CompositorWidgetChild.cpp b/widget/windows/CompositorWidgetChild.cpp
new file mode 100644
index 0000000000..b294efef51
--- /dev/null
+++ b/widget/windows/CompositorWidgetChild.cpp
@@ -0,0 +1,114 @@
+/* -*- 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 "CompositorWidgetChild.h"
+#include "mozilla/Unused.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/widget/CompositorWidgetVsyncObserver.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "nsBaseWidget.h"
+#include "VsyncDispatcher.h"
+#include "gfxPlatform.h"
+#include "RemoteBackbuffer.h"
+
+namespace mozilla {
+namespace widget {
+
+CompositorWidgetChild::CompositorWidgetChild(
+ RefPtr<CompositorVsyncDispatcher> aVsyncDispatcher,
+ RefPtr<CompositorWidgetVsyncObserver> aVsyncObserver,
+ const CompositorWidgetInitData& aInitData)
+ : mVsyncDispatcher(aVsyncDispatcher),
+ mVsyncObserver(aVsyncObserver),
+ mCompositorWnd(nullptr),
+ mWnd(reinterpret_cast<HWND>(
+ aInitData.get_WinCompositorWidgetInitData().hWnd())),
+ mTransparencyMode(
+ aInitData.get_WinCompositorWidgetInitData().transparencyMode()),
+ mRemoteBackbufferProvider() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(!gfxPlatform::IsHeadless());
+ MOZ_ASSERT(mWnd && ::IsWindow(mWnd));
+}
+
+CompositorWidgetChild::~CompositorWidgetChild() {}
+
+bool CompositorWidgetChild::Initialize() {
+ mRemoteBackbufferProvider = std::make_unique<remote_backbuffer::Provider>();
+ if (!mRemoteBackbufferProvider->Initialize(mWnd, OtherPid(),
+ mTransparencyMode)) {
+ return false;
+ }
+
+ auto maybeRemoteHandles = mRemoteBackbufferProvider->CreateRemoteHandles();
+ if (!maybeRemoteHandles) {
+ return false;
+ }
+
+ Unused << SendInitialize(*maybeRemoteHandles);
+
+ return true;
+}
+
+void CompositorWidgetChild::EnterPresentLock() {
+ Unused << SendEnterPresentLock();
+}
+
+void CompositorWidgetChild::LeavePresentLock() {
+ Unused << SendLeavePresentLock();
+}
+
+void CompositorWidgetChild::OnDestroyWindow() {}
+
+bool CompositorWidgetChild::OnWindowResize(const LayoutDeviceIntSize& aSize) {
+ return true;
+}
+
+void CompositorWidgetChild::OnWindowModeChange(nsSizeMode aSizeMode) {}
+
+void CompositorWidgetChild::UpdateTransparency(TransparencyMode aMode) {
+ mTransparencyMode = aMode;
+ mRemoteBackbufferProvider->UpdateTransparencyMode(aMode);
+ Unused << SendUpdateTransparency(aMode);
+}
+
+void CompositorWidgetChild::NotifyVisibilityUpdated(nsSizeMode aSizeMode,
+ bool aIsFullyOccluded) {
+ Unused << SendNotifyVisibilityUpdated(aSizeMode, aIsFullyOccluded);
+};
+
+void CompositorWidgetChild::ClearTransparentWindow() {
+ Unused << SendClearTransparentWindow();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetChild::RecvObserveVsync() {
+ mVsyncDispatcher->SetCompositorVsyncObserver(mVsyncObserver);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetChild::RecvUnobserveVsync() {
+ mVsyncDispatcher->SetCompositorVsyncObserver(nullptr);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetChild::RecvUpdateCompositorWnd(
+ const WindowsHandle& aCompositorWnd, const WindowsHandle& aParentWnd,
+ UpdateCompositorWndResolver&& aResolve) {
+ HWND parentWnd = reinterpret_cast<HWND>(aParentWnd);
+ if (mWnd == parentWnd) {
+ mCompositorWnd = reinterpret_cast<HWND>(aCompositorWnd);
+ ::SetParent(mCompositorWnd, mWnd);
+ aResolve(true);
+ } else {
+ aResolve(false);
+ gfxCriticalNote << "Parent winow does not match";
+ MOZ_ASSERT_UNREACHABLE("unexpected to happen");
+ }
+
+ return IPC_OK();
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/CompositorWidgetChild.h b/widget/windows/CompositorWidgetChild.h
new file mode 100644
index 0000000000..bec8ead98e
--- /dev/null
+++ b/widget/windows/CompositorWidgetChild.h
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+
+#ifndef widget_windows_CompositorWidgetChild_h
+#define widget_windows_CompositorWidgetChild_h
+
+#include "WinCompositorWidget.h"
+#include "mozilla/widget/PCompositorWidgetChild.h"
+#include "mozilla/widget/CompositorWidgetVsyncObserver.h"
+
+namespace mozilla {
+class CompositorVsyncDispatcher;
+
+namespace widget {
+
+namespace remote_backbuffer {
+class Provider;
+}
+
+class CompositorWidgetChild final : public PCompositorWidgetChild,
+ public PlatformCompositorWidgetDelegate {
+ public:
+ CompositorWidgetChild(RefPtr<CompositorVsyncDispatcher> aVsyncDispatcher,
+ RefPtr<CompositorWidgetVsyncObserver> aVsyncObserver,
+ const CompositorWidgetInitData& aInitData);
+ ~CompositorWidgetChild() override;
+
+ bool Initialize();
+
+ void EnterPresentLock() override;
+ void LeavePresentLock() override;
+ void OnDestroyWindow() override;
+ bool OnWindowResize(const LayoutDeviceIntSize& aSize) override;
+ void OnWindowModeChange(nsSizeMode aSizeMode) override;
+ void UpdateTransparency(TransparencyMode aMode) override;
+ void NotifyVisibilityUpdated(nsSizeMode aSizeMode,
+ bool aIsFullyOccluded) override;
+ void ClearTransparentWindow() override;
+
+ mozilla::ipc::IPCResult RecvObserveVsync() override;
+ mozilla::ipc::IPCResult RecvUnobserveVsync() override;
+ mozilla::ipc::IPCResult RecvUpdateCompositorWnd(
+ const WindowsHandle& aCompositorWnd, const WindowsHandle& aParentWnd,
+ UpdateCompositorWndResolver&& aResolve) override;
+
+ private:
+ RefPtr<CompositorVsyncDispatcher> mVsyncDispatcher;
+ RefPtr<CompositorWidgetVsyncObserver> mVsyncObserver;
+ HWND mCompositorWnd;
+
+ HWND mWnd;
+ TransparencyMode mTransparencyMode;
+
+ std::unique_ptr<remote_backbuffer::Provider> mRemoteBackbufferProvider;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_windows_CompositorWidgetChild_h
diff --git a/widget/windows/CompositorWidgetParent.cpp b/widget/windows/CompositorWidgetParent.cpp
new file mode 100644
index 0000000000..b25d30d9d5
--- /dev/null
+++ b/widget/windows/CompositorWidgetParent.cpp
@@ -0,0 +1,232 @@
+/* -*- 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 "CompositorWidgetParent.h"
+
+#include "mozilla/Unused.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "nsWindow.h"
+#include "VsyncDispatcher.h"
+#include "WinCompositorWindowThread.h"
+#include "VRShMem.h"
+#include "RemoteBackbuffer.h"
+
+#include <ddraw.h>
+
+namespace mozilla {
+namespace widget {
+
+using namespace mozilla::gfx;
+using namespace mozilla;
+
+CompositorWidgetParent::CompositorWidgetParent(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions)
+ : WinCompositorWidget(aInitData.get_WinCompositorWidgetInitData(),
+ aOptions),
+ mWnd(reinterpret_cast<HWND>(
+ aInitData.get_WinCompositorWidgetInitData().hWnd())),
+ mTransparencyMode(uint32_t(
+ aInitData.get_WinCompositorWidgetInitData().transparencyMode())),
+ mSizeMode(nsSizeMode_Normal),
+ mIsFullyOccluded(false),
+ mRemoteBackbufferClient() {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+ MOZ_ASSERT(mWnd && ::IsWindow(mWnd));
+}
+
+CompositorWidgetParent::~CompositorWidgetParent() {}
+
+bool CompositorWidgetParent::Initialize(
+ const RemoteBackbufferHandles& aRemoteHandles) {
+ mRemoteBackbufferClient = std::make_unique<remote_backbuffer::Client>();
+ if (!mRemoteBackbufferClient->Initialize(aRemoteHandles)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool CompositorWidgetParent::PreRender(WidgetRenderingContext* aContext) {
+ // This can block waiting for WM_SETTEXT to finish
+ // Using PreRender is unnecessarily pessimistic because
+ // we technically only need to block during the present call
+ // not all of compositor rendering
+ mPresentLock.Enter();
+ return true;
+}
+
+void CompositorWidgetParent::PostRender(WidgetRenderingContext* aContext) {
+ mPresentLock.Leave();
+}
+
+LayoutDeviceIntSize CompositorWidgetParent::GetClientSize() {
+ RECT r;
+ if (!::GetClientRect(mWnd, &r)) {
+ return LayoutDeviceIntSize();
+ }
+ return LayoutDeviceIntSize(r.right - r.left, r.bottom - r.top);
+}
+
+already_AddRefed<gfx::DrawTarget>
+CompositorWidgetParent::StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) {
+ MOZ_ASSERT(mRemoteBackbufferClient);
+ MOZ_ASSERT(aBufferMode);
+
+ // Because we use remote backbuffering, there is no need to use a local
+ // backbuffer too.
+ (*aBufferMode) = layers::BufferMode::BUFFER_NONE;
+
+ return mRemoteBackbufferClient->BorrowDrawTarget();
+}
+
+void CompositorWidgetParent::EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
+ Unused << mRemoteBackbufferClient->PresentDrawTarget(
+ aInvalidRegion.ToUnknownRegion());
+}
+
+bool CompositorWidgetParent::NeedsToDeferEndRemoteDrawing() { return false; }
+
+already_AddRefed<gfx::DrawTarget>
+CompositorWidgetParent::GetBackBufferDrawTarget(gfx::DrawTarget* aScreenTarget,
+ const gfx::IntRect& aRect,
+ bool* aOutIsCleared) {
+ MOZ_CRASH(
+ "Unexpected call to GetBackBufferDrawTarget() with remote "
+ "backbuffering in use");
+}
+
+already_AddRefed<gfx::SourceSurface>
+CompositorWidgetParent::EndBackBufferDrawing() {
+ MOZ_CRASH(
+ "Unexpected call to EndBackBufferDrawing() with remote "
+ "backbuffering in use");
+}
+
+bool CompositorWidgetParent::InitCompositor(layers::Compositor* aCompositor) {
+ return true;
+}
+
+bool CompositorWidgetParent::IsHidden() const { return ::IsIconic(mWnd); }
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvInitialize(
+ const RemoteBackbufferHandles& aRemoteHandles) {
+ Unused << Initialize(aRemoteHandles);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvEnterPresentLock() {
+ mPresentLock.Enter();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvLeavePresentLock() {
+ mPresentLock.Leave();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvUpdateTransparency(
+ const TransparencyMode& aMode) {
+ mTransparencyMode = uint32_t(aMode);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvNotifyVisibilityUpdated(
+ const nsSizeMode& aSizeMode, const bool& aIsFullyOccluded) {
+ mSizeMode = aSizeMode;
+ mIsFullyOccluded = aIsFullyOccluded;
+ return IPC_OK();
+}
+
+nsSizeMode CompositorWidgetParent::CompositorWidgetParent::GetWindowSizeMode()
+ const {
+ nsSizeMode sizeMode = mSizeMode;
+ return sizeMode;
+}
+
+bool CompositorWidgetParent::CompositorWidgetParent::GetWindowIsFullyOccluded()
+ const {
+ bool isFullyOccluded = mIsFullyOccluded;
+ return isFullyOccluded;
+}
+
+mozilla::ipc::IPCResult CompositorWidgetParent::RecvClearTransparentWindow() {
+ gfx::CriticalSectionAutoEnter lock(&mPresentLock);
+
+ RefPtr<DrawTarget> drawTarget = mRemoteBackbufferClient->BorrowDrawTarget();
+ if (!drawTarget) {
+ return IPC_OK();
+ }
+
+ IntSize size = drawTarget->GetSize();
+ if (size.IsEmpty()) {
+ return IPC_OK();
+ }
+
+ drawTarget->ClearRect(Rect(0, 0, size.width, size.height));
+
+ Unused << mRemoteBackbufferClient->PresentDrawTarget(
+ IntRect(0, 0, size.width, size.height));
+
+ return IPC_OK();
+}
+
+nsIWidget* CompositorWidgetParent::RealWidget() { return nullptr; }
+
+void CompositorWidgetParent::ObserveVsync(VsyncObserver* aObserver) {
+ if (aObserver) {
+ Unused << SendObserveVsync();
+ } else {
+ Unused << SendUnobserveVsync();
+ }
+ mVsyncObserver = aObserver;
+}
+
+RefPtr<VsyncObserver> CompositorWidgetParent::GetVsyncObserver() const {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+ return mVsyncObserver;
+}
+
+void CompositorWidgetParent::UpdateCompositorWnd(const HWND aCompositorWnd,
+ const HWND aParentWnd) {
+ MOZ_ASSERT(layers::CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(mRootLayerTreeID.isSome());
+
+ RefPtr<CompositorWidgetParent> self = this;
+ SendUpdateCompositorWnd(reinterpret_cast<WindowsHandle>(aCompositorWnd),
+ reinterpret_cast<WindowsHandle>(aParentWnd))
+ ->Then(
+ layers::CompositorThread(), __func__,
+ [self](const bool& aSuccess) {
+ if (aSuccess && self->mRootLayerTreeID.isSome() &&
+ layers::CompositorThreadHolder::IsActive()) {
+ self->mSetParentCompleted = true;
+ // Schedule composition after ::SetParent() call in parent
+ // process.
+ layers::CompositorBridgeParent::ScheduleForcedComposition(
+ self->mRootLayerTreeID.ref(), wr::RenderReasons::WIDGET);
+ }
+ },
+ [self](const mozilla::ipc::ResponseRejectReason&) {});
+}
+
+void CompositorWidgetParent::SetRootLayerTreeID(
+ const layers::LayersId& aRootLayerTreeId) {
+ mRootLayerTreeID = Some(aRootLayerTreeId);
+}
+
+void CompositorWidgetParent::ActorDestroy(ActorDestroyReason aWhy) {}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/CompositorWidgetParent.h b/widget/windows/CompositorWidgetParent.h
new file mode 100644
index 0000000000..0f91fa7ccb
--- /dev/null
+++ b/widget/windows/CompositorWidgetParent.h
@@ -0,0 +1,92 @@
+/* -*- 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/. */
+
+#ifndef widget_windows_CompositorWidgetParent_h
+#define widget_windows_CompositorWidgetParent_h
+
+#include "WinCompositorWidget.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/widget/PCompositorWidgetParent.h"
+
+namespace mozilla {
+namespace widget {
+
+enum class TransparencyMode : uint8_t;
+
+namespace remote_backbuffer {
+class Client;
+}
+
+class CompositorWidgetParent final : public PCompositorWidgetParent,
+ public WinCompositorWidget {
+ public:
+ explicit CompositorWidgetParent(const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions);
+ ~CompositorWidgetParent() override;
+
+ bool Initialize(const RemoteBackbufferHandles& aRemoteHandles);
+
+ bool PreRender(WidgetRenderingContext*) override;
+ void PostRender(WidgetRenderingContext*) override;
+ already_AddRefed<gfx::DrawTarget> StartRemoteDrawingInRegion(
+ const LayoutDeviceIntRegion& aInvalidRegion,
+ layers::BufferMode* aBufferMode) override;
+ void EndRemoteDrawingInRegion(
+ gfx::DrawTarget* aDrawTarget,
+ const LayoutDeviceIntRegion& aInvalidRegion) override;
+ bool NeedsToDeferEndRemoteDrawing() override;
+ LayoutDeviceIntSize GetClientSize() override;
+ already_AddRefed<gfx::DrawTarget> GetBackBufferDrawTarget(
+ gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect,
+ bool* aOutIsCleared) override;
+ already_AddRefed<gfx::SourceSurface> EndBackBufferDrawing() override;
+ bool InitCompositor(layers::Compositor* aCompositor) override;
+ bool IsHidden() const override;
+
+ nsSizeMode GetWindowSizeMode() const override;
+ bool GetWindowIsFullyOccluded() const override;
+
+ mozilla::ipc::IPCResult RecvInitialize(
+ const RemoteBackbufferHandles& aRemoteHandles) override;
+ mozilla::ipc::IPCResult RecvEnterPresentLock() override;
+ mozilla::ipc::IPCResult RecvLeavePresentLock() override;
+ mozilla::ipc::IPCResult RecvUpdateTransparency(
+ const TransparencyMode& aMode) override;
+ mozilla::ipc::IPCResult RecvNotifyVisibilityUpdated(
+ const nsSizeMode& aSizeMode, const bool& aIsFullyOccluded) override;
+ mozilla::ipc::IPCResult RecvClearTransparentWindow() override;
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ nsIWidget* RealWidget() override;
+ void ObserveVsync(VsyncObserver* aObserver) override;
+ RefPtr<VsyncObserver> GetVsyncObserver() const override;
+
+ // PlatformCompositorWidgetDelegate Overrides
+ void UpdateCompositorWnd(const HWND aCompositorWnd,
+ const HWND aParentWnd) override;
+ void SetRootLayerTreeID(const layers::LayersId& aRootLayerTreeId) override;
+
+ private:
+ RefPtr<VsyncObserver> mVsyncObserver;
+ Maybe<layers::LayersId> mRootLayerTreeID;
+
+ HWND mWnd;
+
+ gfx::CriticalSection mPresentLock;
+
+ // Transparency handling.
+ mozilla::Atomic<uint32_t, MemoryOrdering::Relaxed> mTransparencyMode;
+
+ // Visibility handling.
+ mozilla::Atomic<nsSizeMode, MemoryOrdering::Relaxed> mSizeMode;
+ mozilla::Atomic<bool, MemoryOrdering::Relaxed> mIsFullyOccluded;
+
+ std::unique_ptr<remote_backbuffer::Client> mRemoteBackbufferClient;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_windows_CompositorWidgetParent_h
diff --git a/widget/windows/DirectManipulationOwner.cpp b/widget/windows/DirectManipulationOwner.cpp
new file mode 100644
index 0000000000..569dd4e189
--- /dev/null
+++ b/widget/windows/DirectManipulationOwner.cpp
@@ -0,0 +1,722 @@
+/* -*- 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 "DirectManipulationOwner.h"
+#include "nsWindow.h"
+#include "WinModifierKeyState.h"
+#include "InputData.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/SwipeTracker.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/VsyncDispatcher.h"
+
+#include "directmanipulation.h"
+
+namespace mozilla {
+namespace widget {
+
+class DManipEventHandler : public IDirectManipulationViewportEventHandler,
+ public IDirectManipulationInteractionEventHandler {
+ public:
+ typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect;
+
+ friend class DirectManipulationOwner;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DManipEventHandler)
+
+ STDMETHODIMP QueryInterface(REFIID, void**) override;
+
+ friend class DirectManipulationOwner;
+
+ explicit DManipEventHandler(nsWindow* aWindow,
+ DirectManipulationOwner* aOwner,
+ const LayoutDeviceIntRect& aBounds);
+
+ HRESULT STDMETHODCALLTYPE OnViewportStatusChanged(
+ IDirectManipulationViewport* viewport, DIRECTMANIPULATION_STATUS current,
+ DIRECTMANIPULATION_STATUS previous) override;
+
+ HRESULT STDMETHODCALLTYPE
+ OnViewportUpdated(IDirectManipulationViewport* viewport) override;
+
+ HRESULT STDMETHODCALLTYPE
+ OnContentUpdated(IDirectManipulationViewport* viewport,
+ IDirectManipulationContent* content) override;
+
+ HRESULT STDMETHODCALLTYPE
+ OnInteraction(IDirectManipulationViewport2* viewport,
+ DIRECTMANIPULATION_INTERACTION_TYPE interaction) override;
+
+ void Update();
+
+ class VObserver final : public mozilla::VsyncObserver {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DManipEventHandler::VObserver,
+ override)
+
+ public:
+ void NotifyVsync(const mozilla::VsyncEvent& aVsync) override {
+ if (mOwner) {
+ mOwner->Update();
+ }
+ }
+ explicit VObserver(DManipEventHandler* aOwner) : mOwner(aOwner) {}
+
+ void ClearOwner() { mOwner = nullptr; }
+
+ private:
+ virtual ~VObserver() {}
+ DManipEventHandler* mOwner;
+ };
+
+ enum class State { eNone, ePanning, eInertia, ePinching };
+ void TransitionToState(State aNewState);
+
+ enum class Phase { eStart, eMiddle, eEnd };
+ // Return value indicates if we sent an event or not and hence if we should
+ // update mLastScale. (We only want to send pinch events if the computed
+ // deltaY for the corresponding WidgetWheelEvent would be non-zero.)
+ bool SendPinch(Phase aPhase, float aScale);
+ void SendPan(Phase aPhase, float x, float y, bool aIsInertia);
+ static void SendPanCommon(nsWindow* aWindow, Phase aPhase,
+ ScreenPoint aPosition, double aDeltaX,
+ double aDeltaY, Modifiers aMods, bool aIsInertia);
+
+ static void SynthesizeNativeTouchpadPan(
+ nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags);
+
+ private:
+ virtual ~DManipEventHandler() = default;
+
+ nsWindow* mWindow;
+ DirectManipulationOwner* mOwner;
+ RefPtr<VObserver> mObserver;
+ float mLastScale;
+ float mLastXOffset;
+ float mLastYOffset;
+ LayoutDeviceIntRect mBounds;
+ bool mShouldSendPanStart;
+ bool mShouldSendPinchStart;
+ State mState = State::eNone;
+};
+
+DManipEventHandler::DManipEventHandler(nsWindow* aWindow,
+ DirectManipulationOwner* aOwner,
+ const LayoutDeviceIntRect& aBounds)
+ : mWindow(aWindow),
+ mOwner(aOwner),
+ mLastScale(1.f),
+ mLastXOffset(0.f),
+ mLastYOffset(0.f),
+ mBounds(aBounds),
+ mShouldSendPanStart(false),
+ mShouldSendPinchStart(false) {}
+
+STDMETHODIMP
+DManipEventHandler::QueryInterface(REFIID iid, void** ppv) {
+ const IID IID_IDirectManipulationViewportEventHandler =
+ __uuidof(IDirectManipulationViewportEventHandler);
+ const IID IID_IDirectManipulationInteractionEventHandler =
+ __uuidof(IDirectManipulationInteractionEventHandler);
+
+ if ((IID_IUnknown == iid) ||
+ (IID_IDirectManipulationViewportEventHandler == iid)) {
+ *ppv = static_cast<IDirectManipulationViewportEventHandler*>(this);
+ AddRef();
+ return S_OK;
+ }
+ if (IID_IDirectManipulationInteractionEventHandler == iid) {
+ *ppv = static_cast<IDirectManipulationInteractionEventHandler*>(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+HRESULT
+DManipEventHandler::OnViewportStatusChanged(
+ IDirectManipulationViewport* viewport, DIRECTMANIPULATION_STATUS current,
+ DIRECTMANIPULATION_STATUS previous) {
+ if (current == previous) {
+ return S_OK;
+ }
+
+ if (current == DIRECTMANIPULATION_INERTIA) {
+ if (previous != DIRECTMANIPULATION_RUNNING || mState != State::ePanning) {
+ // xxx transition to none?
+ return S_OK;
+ }
+
+ TransitionToState(State::eInertia);
+ }
+
+ if (current == DIRECTMANIPULATION_RUNNING) {
+ // INERTIA -> RUNNING, should start a new sequence.
+ if (previous == DIRECTMANIPULATION_INERTIA) {
+ TransitionToState(State::eNone);
+ }
+ }
+
+ if (current != DIRECTMANIPULATION_ENABLED &&
+ current != DIRECTMANIPULATION_READY) {
+ return S_OK;
+ }
+
+ // A session has ended, reset the transform.
+ if (mLastScale != 1.f || mLastXOffset != 0.f || mLastYOffset != 0.f) {
+ HRESULT hr =
+ viewport->ZoomToRect(0, 0, mBounds.width, mBounds.height, false);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("ZoomToRect failed");
+ }
+ }
+ mLastScale = 1.f;
+ mLastXOffset = 0.f;
+ mLastYOffset = 0.f;
+
+ TransitionToState(State::eNone);
+
+ return S_OK;
+}
+
+HRESULT
+DManipEventHandler::OnViewportUpdated(IDirectManipulationViewport* viewport) {
+ return S_OK;
+}
+
+void DManipEventHandler::TransitionToState(State aNewState) {
+ if (mState == aNewState) {
+ return;
+ }
+
+ State prevState = mState;
+ mState = aNewState;
+
+ // End the previous sequence.
+ switch (prevState) {
+ case State::ePanning: {
+ // ePanning -> *: PanEnd
+ SendPan(Phase::eEnd, 0.f, 0.f, false);
+ break;
+ }
+ case State::eInertia: {
+ // eInertia -> *: MomentumEnd
+ SendPan(Phase::eEnd, 0.f, 0.f, true);
+ break;
+ }
+ case State::ePinching: {
+ MOZ_ASSERT(aNewState == State::eNone);
+ // ePinching -> eNone: PinchEnd. ePinching should only transition to
+ // eNone.
+ // Only send a pinch end if we sent a pinch start.
+ if (!mShouldSendPinchStart) {
+ SendPinch(Phase::eEnd, 0.f);
+ }
+ mShouldSendPinchStart = false;
+ break;
+ }
+ case State::eNone: {
+ // eNone -> *: no cleanup is needed.
+ break;
+ }
+ default:
+ MOZ_ASSERT(false);
+ }
+
+ // Start the new sequence.
+ switch (aNewState) {
+ case State::ePanning: {
+ // eInertia, eNone -> ePanning: PanStart.
+ // We're being called from OnContentUpdated, it has the coords we need to
+ // pass to SendPan(Phase::eStart), so set mShouldSendPanStart and when we
+ // return OnContentUpdated will check it and call SendPan(Phase::eStart).
+ mShouldSendPanStart = true;
+ break;
+ }
+ case State::eInertia: {
+ // Only ePanning can transition to eInertia.
+ MOZ_ASSERT(prevState == State::ePanning);
+ SendPan(Phase::eStart, 0.f, 0.f, true);
+ break;
+ }
+ case State::ePinching: {
+ // * -> ePinching: PinchStart.
+ // Pinch gesture may begin with some scroll events.
+ // We're being called from OnContentUpdated, it has the scale we need to
+ // pass to SendPinch(Phase::eStart), so set mShouldSendPinchStart and when
+ // we return OnContentUpdated will check it and call
+ // SendPinch(Phase::eStart).
+ mShouldSendPinchStart = true;
+ break;
+ }
+ case State::eNone: {
+ // * -> eNone: only cleanup is needed.
+ break;
+ }
+ default:
+ MOZ_ASSERT(false);
+ }
+}
+
+HRESULT
+DManipEventHandler::OnContentUpdated(IDirectManipulationViewport* viewport,
+ IDirectManipulationContent* content) {
+ float transform[6];
+ HRESULT hr = content->GetContentTransform(transform, ARRAYSIZE(transform));
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("GetContentTransform failed");
+ return S_OK;
+ }
+
+ float scale = transform[0];
+ float xoffset = transform[4];
+ float yoffset = transform[5];
+
+ // Not different from last time.
+ if (FuzzyEqualsMultiplicative(scale, mLastScale) && xoffset == mLastXOffset &&
+ yoffset == mLastYOffset) {
+ return S_OK;
+ }
+
+ // Consider this is a Scroll when scale factor equals 1.0.
+ if (FuzzyEqualsMultiplicative(scale, 1.f)) {
+ if (mState == State::eNone) {
+ TransitionToState(State::ePanning);
+ }
+ } else {
+ // Pinch gesture may begin with some scroll events.
+ TransitionToState(State::ePinching);
+ }
+
+ if (mState == State::ePanning || mState == State::eInertia) {
+ // Accumulate the offset (by not updating mLastX/YOffset) until we have at
+ // least one pixel.
+ float dx = std::abs(mLastXOffset - xoffset);
+ float dy = std::abs(mLastYOffset - yoffset);
+ if (dx < 1.f && dy < 1.f) {
+ return S_OK;
+ }
+ }
+
+ bool updateLastScale = true;
+ if (mState == State::ePanning) {
+ if (mShouldSendPanStart) {
+ SendPan(Phase::eStart, mLastXOffset - xoffset, mLastYOffset - yoffset,
+ false);
+ mShouldSendPanStart = false;
+ } else {
+ SendPan(Phase::eMiddle, mLastXOffset - xoffset, mLastYOffset - yoffset,
+ false);
+ }
+ } else if (mState == State::eInertia) {
+ SendPan(Phase::eMiddle, mLastXOffset - xoffset, mLastYOffset - yoffset,
+ true);
+ } else if (mState == State::ePinching) {
+ if (mShouldSendPinchStart) {
+ updateLastScale = SendPinch(Phase::eStart, scale);
+ // Only clear mShouldSendPinchStart if we actually sent the event
+ // (updateLastScale tells us if we sent an event).
+ if (updateLastScale) {
+ mShouldSendPinchStart = false;
+ }
+ } else {
+ updateLastScale = SendPinch(Phase::eMiddle, scale);
+ }
+ }
+
+ if (updateLastScale) {
+ mLastScale = scale;
+ }
+ mLastXOffset = xoffset;
+ mLastYOffset = yoffset;
+
+ return S_OK;
+}
+
+HRESULT
+DManipEventHandler::OnInteraction(
+ IDirectManipulationViewport2* viewport,
+ DIRECTMANIPULATION_INTERACTION_TYPE interaction) {
+ if (interaction == DIRECTMANIPULATION_INTERACTION_BEGIN) {
+ if (!mObserver) {
+ mObserver = new VObserver(this);
+ }
+
+ gfxWindowsPlatform::GetPlatform()
+ ->GetGlobalVsyncDispatcher()
+ ->AddMainThreadObserver(mObserver);
+ }
+
+ if (mObserver && interaction == DIRECTMANIPULATION_INTERACTION_END) {
+ gfxWindowsPlatform::GetPlatform()
+ ->GetGlobalVsyncDispatcher()
+ ->RemoveMainThreadObserver(mObserver);
+ }
+
+ return S_OK;
+}
+
+void DManipEventHandler::Update() {
+ if (mOwner) {
+ mOwner->Update();
+ }
+}
+
+void DirectManipulationOwner::Update() {
+ if (mDmUpdateManager) {
+ mDmUpdateManager->Update(nullptr);
+ }
+}
+
+DirectManipulationOwner::DirectManipulationOwner(nsWindow* aWindow)
+ : mWindow(aWindow) {}
+
+DirectManipulationOwner::~DirectManipulationOwner() { Destroy(); }
+
+bool DManipEventHandler::SendPinch(Phase aPhase, float aScale) {
+ if (!mWindow) {
+ return false;
+ }
+
+ if (aScale == mLastScale && aPhase != Phase::eEnd) {
+ return false;
+ }
+
+ PinchGestureInput::PinchGestureType pinchGestureType =
+ PinchGestureInput::PINCHGESTURE_SCALE;
+ switch (aPhase) {
+ case Phase::eStart:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_START;
+ break;
+ case Phase::eMiddle:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
+ break;
+ case Phase::eEnd:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_END;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("handle all enum values");
+ }
+
+ TimeStamp eventTimeStamp = TimeStamp::Now();
+
+ ModifierKeyState modifierKeyState;
+ Modifiers mods = modifierKeyState.GetModifiers();
+
+ ExternalPoint screenOffset = ViewAs<ExternalPixel>(
+ mWindow->WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ POINT cursor_pos;
+ ::GetCursorPos(&cursor_pos);
+ HWND wnd = static_cast<HWND>(mWindow->GetNativeData(NS_NATIVE_WINDOW));
+ ::ScreenToClient(wnd, &cursor_pos);
+ ScreenPoint position = {(float)cursor_pos.x, (float)cursor_pos.y};
+
+ PinchGestureInput event{pinchGestureType,
+ PinchGestureInput::TRACKPAD,
+ eventTimeStamp,
+ screenOffset,
+ position,
+ 100.0 * ((aPhase == Phase::eEnd) ? 1.f : aScale),
+ 100.0 * ((aPhase == Phase::eEnd) ? 1.f : mLastScale),
+ mods};
+
+ if (!event.SetLineOrPageDeltaY(mWindow)) {
+ return false;
+ }
+
+ mWindow->SendAnAPZEvent(event);
+
+ return true;
+}
+
+void DManipEventHandler::SendPan(Phase aPhase, float x, float y,
+ bool aIsInertia) {
+ if (!mWindow) {
+ return;
+ }
+
+ ModifierKeyState modifierKeyState;
+ Modifiers mods = modifierKeyState.GetModifiers();
+
+ POINT cursor_pos;
+ ::GetCursorPos(&cursor_pos);
+ HWND wnd = static_cast<HWND>(mWindow->GetNativeData(NS_NATIVE_WINDOW));
+ ::ScreenToClient(wnd, &cursor_pos);
+ ScreenPoint position = {(float)cursor_pos.x, (float)cursor_pos.y};
+
+ SendPanCommon(mWindow, aPhase, position, x, y, mods, aIsInertia);
+}
+
+/* static */
+void DManipEventHandler::SendPanCommon(nsWindow* aWindow, Phase aPhase,
+ ScreenPoint aPosition, double aDeltaX,
+ double aDeltaY, Modifiers aMods,
+ bool aIsInertia) {
+ if (!aWindow) {
+ return;
+ }
+
+ PanGestureInput::PanGestureType panGestureType =
+ PanGestureInput::PANGESTURE_PAN;
+ if (aIsInertia) {
+ switch (aPhase) {
+ case Phase::eStart:
+ panGestureType = PanGestureInput::PANGESTURE_MOMENTUMSTART;
+ break;
+ case Phase::eMiddle:
+ panGestureType = PanGestureInput::PANGESTURE_MOMENTUMPAN;
+ break;
+ case Phase::eEnd:
+ panGestureType = PanGestureInput::PANGESTURE_MOMENTUMEND;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("handle all enum values");
+ }
+ } else {
+ switch (aPhase) {
+ case Phase::eStart:
+ panGestureType = PanGestureInput::PANGESTURE_START;
+ break;
+ case Phase::eMiddle:
+ panGestureType = PanGestureInput::PANGESTURE_PAN;
+ break;
+ case Phase::eEnd:
+ panGestureType = PanGestureInput::PANGESTURE_END;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("handle all enum values");
+ }
+ }
+
+ TimeStamp eventTimeStamp = TimeStamp::Now();
+
+ PanGestureInput event{panGestureType, eventTimeStamp, aPosition,
+ ScreenPoint(aDeltaX, aDeltaY), aMods};
+
+ aWindow->SendAnAPZEvent(event);
+}
+
+void DirectManipulationOwner::Init(const LayoutDeviceIntRect& aBounds) {
+ HRESULT hr = CoCreateInstance(
+ CLSID_DirectManipulationManager, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IDirectManipulationManager, getter_AddRefs(mDmManager));
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("CoCreateInstance(CLSID_DirectManipulationManager failed");
+ mDmManager = nullptr;
+ return;
+ }
+
+ hr = mDmManager->GetUpdateManager(IID_IDirectManipulationUpdateManager,
+ getter_AddRefs(mDmUpdateManager));
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("GetUpdateManager failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ return;
+ }
+
+ HWND wnd = static_cast<HWND>(mWindow->GetNativeData(NS_NATIVE_WINDOW));
+
+ hr = mDmManager->CreateViewport(nullptr, wnd, IID_IDirectManipulationViewport,
+ getter_AddRefs(mDmViewport));
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("CreateViewport failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ return;
+ }
+
+ DIRECTMANIPULATION_CONFIGURATION configuration =
+ DIRECTMANIPULATION_CONFIGURATION_INTERACTION |
+ DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X |
+ DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y |
+ DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA |
+ DIRECTMANIPULATION_CONFIGURATION_RAILS_X |
+ DIRECTMANIPULATION_CONFIGURATION_RAILS_Y;
+ if (StaticPrefs::apz_allow_zooming()) {
+ configuration |= DIRECTMANIPULATION_CONFIGURATION_SCALING;
+ }
+
+ hr = mDmViewport->ActivateConfiguration(configuration);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("ActivateConfiguration failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ return;
+ }
+
+ hr = mDmViewport->SetViewportOptions(
+ DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("SetViewportOptions failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ return;
+ }
+
+ mDmHandler = new DManipEventHandler(mWindow, this, aBounds);
+
+ hr = mDmViewport->AddEventHandler(wnd, mDmHandler.get(),
+ &mDmViewportHandlerCookie);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("AddEventHandler failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ mDmHandler = nullptr;
+ return;
+ }
+
+ RECT rect = {0, 0, aBounds.Width(), aBounds.Height()};
+ hr = mDmViewport->SetViewportRect(&rect);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("SetViewportRect failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ mDmHandler = nullptr;
+ return;
+ }
+
+ hr = mDmManager->Activate(wnd);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("manager Activate failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ mDmHandler = nullptr;
+ return;
+ }
+
+ hr = mDmViewport->Enable();
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("mDmViewport->Enable failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ mDmHandler = nullptr;
+ return;
+ }
+
+ hr = mDmUpdateManager->Update(nullptr);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("mDmUpdateManager->Update failed");
+ mDmManager = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmViewport = nullptr;
+ mDmHandler = nullptr;
+ return;
+ }
+}
+
+void DirectManipulationOwner::ResizeViewport(
+ const LayoutDeviceIntRect& aBounds) {
+ if (mDmHandler) {
+ mDmHandler->mBounds = aBounds;
+ }
+
+ if (mDmViewport) {
+ RECT rect = {0, 0, aBounds.Width(), aBounds.Height()};
+ HRESULT hr = mDmViewport->SetViewportRect(&rect);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("SetViewportRect failed");
+ }
+ }
+}
+
+void DirectManipulationOwner::Destroy() {
+ if (mDmHandler) {
+ mDmHandler->mWindow = nullptr;
+ mDmHandler->mOwner = nullptr;
+ if (mDmHandler->mObserver) {
+ gfxWindowsPlatform::GetPlatform()
+ ->GetGlobalVsyncDispatcher()
+ ->RemoveMainThreadObserver(mDmHandler->mObserver);
+ mDmHandler->mObserver->ClearOwner();
+ mDmHandler->mObserver = nullptr;
+ }
+ }
+
+ HRESULT hr;
+ if (mDmViewport) {
+ hr = mDmViewport->Stop();
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("mDmViewport->Stop() failed");
+ }
+
+ hr = mDmViewport->Disable();
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("mDmViewport->Disable() failed");
+ }
+
+ hr = mDmViewport->RemoveEventHandler(mDmViewportHandlerCookie);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("mDmViewport->RemoveEventHandler() failed");
+ }
+
+ hr = mDmViewport->Abandon();
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("mDmViewport->Abandon() failed");
+ }
+ }
+
+ if (mWindow) {
+ HWND wnd = static_cast<HWND>(mWindow->GetNativeData(NS_NATIVE_WINDOW));
+
+ if (mDmManager) {
+ hr = mDmManager->Deactivate(wnd);
+ if (!SUCCEEDED(hr)) {
+ NS_WARNING("mDmManager->Deactivate() failed");
+ }
+ }
+ }
+
+ mDmHandler = nullptr;
+ mDmViewport = nullptr;
+ mDmUpdateManager = nullptr;
+ mDmManager = nullptr;
+ mWindow = nullptr;
+}
+
+void DirectManipulationOwner::SetContact(UINT aContactId) {
+ if (mDmViewport) {
+ mDmViewport->SetContact(aContactId);
+ }
+}
+
+/*static */ void DirectManipulationOwner::SynthesizeNativeTouchpadPan(
+ nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags) {
+ DManipEventHandler::SynthesizeNativeTouchpadPan(
+ aWindow, aEventPhase, aPoint, aDeltaX, aDeltaY, aModifierFlags);
+}
+
+/*static */ void DManipEventHandler::SynthesizeNativeTouchpadPan(
+ nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags) {
+ ScreenPoint position = {(float)aPoint.x, (float)aPoint.y};
+ Phase phase = Phase::eStart;
+ if (aEventPhase == nsIWidget::PHASE_UPDATE) {
+ phase = Phase::eMiddle;
+ }
+
+ if (aEventPhase == nsIWidget::PHASE_END) {
+ phase = Phase::eEnd;
+ }
+ SendPanCommon(aWindow, phase, position, aDeltaX, aDeltaY, aModifierFlags,
+ /* aIsInertia = */ false);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/DirectManipulationOwner.h b/widget/windows/DirectManipulationOwner.h
new file mode 100644
index 0000000000..b4d5877d70
--- /dev/null
+++ b/widget/windows/DirectManipulationOwner.h
@@ -0,0 +1,54 @@
+/* -*- 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/. */
+
+#ifndef DirectManipulationOwner_h__
+#define DirectManipulationOwner_h__
+
+#include <windows.h>
+#include "Units.h"
+#include "nsIWidget.h" // for TouchpadGesturePhase
+
+class nsWindow;
+class IDirectManipulationManager;
+class IDirectManipulationUpdateManager;
+class IDirectManipulationViewport;
+
+namespace mozilla {
+namespace widget {
+
+class DManipEventHandler;
+
+class DirectManipulationOwner {
+ public:
+ typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect;
+
+ explicit DirectManipulationOwner(nsWindow* aWindow);
+ ~DirectManipulationOwner();
+ void Init(const LayoutDeviceIntRect& aBounds);
+ void ResizeViewport(const LayoutDeviceIntRect& aBounds);
+ void Destroy();
+
+ void SetContact(UINT aContactId);
+
+ void Update();
+
+ static void SynthesizeNativeTouchpadPan(
+ nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags);
+
+ private:
+ nsWindow* mWindow;
+ DWORD mDmViewportHandlerCookie;
+ RefPtr<IDirectManipulationManager> mDmManager;
+ RefPtr<IDirectManipulationUpdateManager> mDmUpdateManager;
+ RefPtr<IDirectManipulationViewport> mDmViewport;
+ RefPtr<DManipEventHandler> mDmHandler;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef DirectManipulationOwner_h__
diff --git a/widget/windows/GfxInfo.cpp b/widget/windows/GfxInfo.cpp
new file mode 100644
index 0000000000..6f033785f5
--- /dev/null
+++ b/widget/windows/GfxInfo.cpp
@@ -0,0 +1,2082 @@
+/* -*- 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 "GfxInfo.h"
+
+#include "gfxConfig.h"
+#include "GfxDriverInfo.h"
+#include "gfxWindowsPlatform.h"
+#include "jsapi.h"
+#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty
+#include "nsExceptionHandler.h"
+#include "nsPrintfCString.h"
+#include "nsUnicharUtils.h"
+#include "prenv.h"
+#include "prprf.h"
+#include "xpcpublic.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/SSE.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/WindowsProcessMitigations.h"
+
+#include <intrin.h>
+#include <windows.h>
+#include <devguid.h> // for GUID_DEVCLASS_BATTERY
+#include <setupapi.h> // for SetupDi*
+#include <winioctl.h> // for IOCTL_*
+#include <batclass.h> // for BATTERY_*
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+
+#ifdef DEBUG
+NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug)
+#endif
+
+static void AssertNotWin32kLockdown() {
+ // Check that we are not in Win32k lockdown
+ MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown(),
+ "Invalid Windows GfxInfo API with Win32k lockdown");
+}
+
+/* GetD2DEnabled and GetDwriteEnabled shouldn't be called until after
+ * gfxPlatform initialization has occurred because they depend on it for
+ * information. (See bug 591561) */
+nsresult GfxInfo::GetD2DEnabled(bool* aEnabled) {
+ // Telemetry queries this during XPCOM initialization, and there's no
+ // gfxPlatform by then. Just bail out if gfxPlatform isn't initialized.
+ if (!gfxPlatform::Initialized()) {
+ *aEnabled = false;
+ return NS_OK;
+ }
+
+ // We check gfxConfig rather than the actual render mode, since the UI
+ // process does not use Direct2D if the GPU process is enabled. However,
+ // content processes can still use Direct2D.
+ *aEnabled = gfx::gfxConfig::IsEnabled(gfx::Feature::DIRECT2D);
+ return NS_OK;
+}
+
+nsresult GfxInfo::GetDWriteEnabled(bool* aEnabled) {
+ *aEnabled = gfxWindowsPlatform::GetPlatform()->DWriteEnabled();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) {
+ gfxWindowsPlatform::GetDLLVersion(L"dwrite.dll", aDwriteVersion);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetHasBattery(bool* aHasBattery) {
+ AssertNotWin32kLockdown();
+
+ *aHasBattery = mHasBattery;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) {
+ *aEmbeddedInFirefoxReality = gfxVars::FxREmbedded();
+ return NS_OK;
+}
+
+#define PIXEL_STRUCT_RGB 1
+#define PIXEL_STRUCT_BGR 2
+
+NS_IMETHODIMP
+GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) {
+ nsTArray<ClearTypeParameterInfo> clearTypeParams;
+
+ gfxWindowsPlatform::GetPlatform()->GetCleartypeParams(clearTypeParams);
+ uint32_t d, numDisplays = clearTypeParams.Length();
+ bool displayNames = (numDisplays > 1);
+ bool foundData = false;
+ nsString outStr;
+
+ for (d = 0; d < numDisplays; d++) {
+ ClearTypeParameterInfo& params = clearTypeParams[d];
+
+ if (displayNames) {
+ outStr.AppendPrintf(
+ "%S [ ", static_cast<const wchar_t*>(params.displayName.get()));
+ }
+
+ if (params.gamma >= 0) {
+ foundData = true;
+ outStr.AppendPrintf("Gamma: %.4g ", params.gamma / 1000.0);
+ }
+
+ if (params.pixelStructure >= 0) {
+ foundData = true;
+ if (params.pixelStructure == PIXEL_STRUCT_RGB ||
+ params.pixelStructure == PIXEL_STRUCT_BGR) {
+ outStr.AppendPrintf(
+ "Pixel Structure: %s ",
+ (params.pixelStructure == PIXEL_STRUCT_RGB ? "RGB" : "BGR"));
+ } else {
+ outStr.AppendPrintf("Pixel Structure: %d ", params.pixelStructure);
+ }
+ }
+
+ if (params.clearTypeLevel >= 0) {
+ foundData = true;
+ outStr.AppendPrintf("ClearType Level: %d ", params.clearTypeLevel);
+ }
+
+ if (params.enhancedContrast >= 0) {
+ foundData = true;
+ outStr.AppendPrintf("Enhanced Contrast: %d ", params.enhancedContrast);
+ }
+
+ if (displayNames) {
+ outStr.Append(u"] ");
+ }
+ }
+
+ if (foundData) {
+ aCleartypeParams.Assign(outStr);
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetTestType(nsAString& aTestType) { return NS_ERROR_NOT_IMPLEMENTED; }
+
+static nsresult GetKeyValue(const WCHAR* keyLocation, const WCHAR* keyName,
+ uint32_t& destValue, int type) {
+ MOZ_ASSERT(type == REG_DWORD || type == REG_QWORD);
+ HKEY key;
+ DWORD dwcbData;
+ DWORD dValue;
+ DWORD resultType;
+ LONG result;
+ nsresult retval = NS_OK;
+
+ result =
+ RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyLocation, 0, KEY_QUERY_VALUE, &key);
+ if (result != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ switch (type) {
+ case REG_DWORD: {
+ // We only use this for vram size
+ dwcbData = sizeof(dValue);
+ result = RegQueryValueExW(key, keyName, nullptr, &resultType,
+ (LPBYTE)&dValue, &dwcbData);
+ if (result == ERROR_SUCCESS && resultType == REG_DWORD) {
+ destValue = (uint32_t)(dValue / 1024 / 1024);
+ } else {
+ retval = NS_ERROR_FAILURE;
+ }
+ break;
+ }
+ case REG_QWORD: {
+ // We only use this for vram size
+ LONGLONG qValue;
+ dwcbData = sizeof(qValue);
+ result = RegQueryValueExW(key, keyName, nullptr, &resultType,
+ (LPBYTE)&qValue, &dwcbData);
+ if (result == ERROR_SUCCESS && resultType == REG_QWORD) {
+ destValue = (uint32_t)(qValue / 1024 / 1024);
+ } else {
+ retval = NS_ERROR_FAILURE;
+ }
+ break;
+ }
+ }
+ RegCloseKey(key);
+
+ return retval;
+}
+
+static nsresult GetKeyValue(const WCHAR* keyLocation, const WCHAR* keyName,
+ nsAString& destString, int type) {
+ MOZ_ASSERT(type == REG_MULTI_SZ);
+
+ HKEY key;
+ DWORD dwcbData;
+ DWORD resultType;
+ LONG result;
+ nsresult retval = NS_OK;
+
+ result =
+ RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyLocation, 0, KEY_QUERY_VALUE, &key);
+ if (result != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // A chain of null-separated strings; we convert the nulls to spaces
+ WCHAR wCharValue[1024];
+ dwcbData = sizeof(wCharValue);
+
+ result = RegQueryValueExW(key, keyName, nullptr, &resultType,
+ (LPBYTE)wCharValue, &dwcbData);
+ if (result == ERROR_SUCCESS && resultType == REG_MULTI_SZ) {
+ // This bit here could probably be cleaner.
+ bool isValid = false;
+
+ DWORD strLen = dwcbData / sizeof(wCharValue[0]);
+ for (DWORD i = 0; i < strLen; i++) {
+ if (wCharValue[i] == '\0') {
+ if (i < strLen - 1 && wCharValue[i + 1] == '\0') {
+ isValid = true;
+ break;
+ } else {
+ wCharValue[i] = ' ';
+ }
+ }
+ }
+
+ // ensure wCharValue is null terminated
+ wCharValue[strLen - 1] = '\0';
+
+ if (isValid) destString = wCharValue;
+
+ } else {
+ retval = NS_ERROR_FAILURE;
+ }
+
+ RegCloseKey(key);
+
+ return retval;
+}
+
+static nsresult GetKeyValues(const WCHAR* keyLocation, const WCHAR* keyName,
+ nsTArray<nsString>& destStrings) {
+ // First ask for the size of the value
+ DWORD size;
+ LONG rv = RegGetValueW(HKEY_LOCAL_MACHINE, keyLocation, keyName,
+ RRF_RT_REG_MULTI_SZ, nullptr, nullptr, &size);
+ if (rv != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create a buffer with the proper size and retrieve the value
+ WCHAR* wCharValue = new WCHAR[size / sizeof(WCHAR)];
+ rv = RegGetValueW(HKEY_LOCAL_MACHINE, keyLocation, keyName,
+ RRF_RT_REG_MULTI_SZ, nullptr, (LPBYTE)wCharValue, &size);
+ if (rv != ERROR_SUCCESS) {
+ delete[] wCharValue;
+ return NS_ERROR_FAILURE;
+ }
+
+ // The value is a sequence of null-terminated strings, usually terminated by
+ // an empty string (\0). RegGetValue ensures that the value is properly
+ // terminated with a null character.
+ DWORD i = 0;
+ DWORD strLen = size / sizeof(WCHAR);
+ while (i < strLen) {
+ nsString value(wCharValue + i);
+ if (!value.IsEmpty()) {
+ destStrings.AppendElement(value);
+ }
+ i += value.Length() + 1;
+ }
+ delete[] wCharValue;
+
+ return NS_OK;
+}
+
+// The device ID is a string like PCI\VEN_15AD&DEV_0405&SUBSYS_040515AD
+// this function is used to extract the id's out of it
+uint32_t ParseIDFromDeviceID(const nsAString& key, const nsAString& prefix,
+ int length) {
+ nsAutoString id(key);
+ ToUpperCase(id);
+ int32_t start = id.Find(prefix);
+ if (start != -1) {
+ id.Cut(0, start + prefix.Length());
+ id.Truncate(length);
+ }
+ if (id.Equals(L"QCOM", nsCaseInsensitiveStringComparator)) {
+ // String format assumptions are broken, so use a Qualcomm PCI Vendor ID
+ // for now. See also GfxDriverInfo::GetDeviceVendor.
+ return 0x5143;
+ }
+ nsresult err;
+ return id.ToInteger(&err, 16);
+}
+
+// OS version in 16.16 major/minor form
+// based on http://msdn.microsoft.com/en-us/library/ms724834(VS.85).aspx
+enum {
+ kWindowsUnknown = 0,
+ kWindows7 = 0x60001,
+ kWindows8 = 0x60002,
+ kWindows8_1 = 0x60003,
+ kWindows10 = 0xA0000
+};
+
+static bool HasBattery() {
+ // Helper classes to manage lifetimes of Windows structs.
+ class MOZ_STACK_CLASS HDevInfoHolder final {
+ public:
+ explicit HDevInfoHolder(HDEVINFO aHandle) : mHandle(aHandle) {}
+
+ ~HDevInfoHolder() { ::SetupDiDestroyDeviceInfoList(mHandle); }
+
+ private:
+ HDEVINFO mHandle;
+ };
+
+ class MOZ_STACK_CLASS HandleHolder final {
+ public:
+ explicit HandleHolder(HANDLE aHandle) : mHandle(aHandle) {}
+
+ ~HandleHolder() { ::CloseHandle(mHandle); }
+
+ private:
+ HANDLE mHandle;
+ };
+
+ HDEVINFO hdev =
+ ::SetupDiGetClassDevs(&GUID_DEVCLASS_BATTERY, nullptr, nullptr,
+ DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
+ if (hdev == INVALID_HANDLE_VALUE) {
+ return true;
+ }
+
+ HDevInfoHolder hdevHolder(hdev);
+
+ DWORD i = 0;
+ SP_DEVICE_INTERFACE_DATA did = {0};
+ did.cbSize = sizeof(did);
+
+ while (::SetupDiEnumDeviceInterfaces(hdev, nullptr, &GUID_DEVCLASS_BATTERY, i,
+ &did)) {
+ DWORD bufferSize = 0;
+ ::SetupDiGetDeviceInterfaceDetail(hdev, &did, nullptr, 0, &bufferSize,
+ nullptr);
+ if (::GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+ return true;
+ }
+
+ UniquePtr<uint8_t[]> buffer(new (std::nothrow) uint8_t[bufferSize]);
+ if (!buffer) {
+ return true;
+ }
+
+ PSP_DEVICE_INTERFACE_DETAIL_DATA pdidd =
+ reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA>(buffer.get());
+ pdidd->cbSize = sizeof(*pdidd);
+ if (!::SetupDiGetDeviceInterfaceDetail(hdev, &did, pdidd, bufferSize,
+ &bufferSize, nullptr)) {
+ return true;
+ }
+
+ HANDLE hbat = ::CreateFile(pdidd->DevicePath, GENERIC_READ | GENERIC_WRITE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (hbat == INVALID_HANDLE_VALUE) {
+ return true;
+ }
+
+ HandleHolder hbatHolder(hbat);
+
+ BATTERY_QUERY_INFORMATION bqi = {0};
+ DWORD dwWait = 0;
+ DWORD dwOut;
+
+ // We need the tag to query the information below.
+ if (!::DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_TAG, &dwWait,
+ sizeof(dwWait), &bqi.BatteryTag,
+ sizeof(bqi.BatteryTag), &dwOut, nullptr) ||
+ !bqi.BatteryTag) {
+ return true;
+ }
+
+ BATTERY_INFORMATION bi = {0};
+ bqi.InformationLevel = BatteryInformation;
+
+ if (!::DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, &bqi,
+ sizeof(bqi), &bi, sizeof(bi), &dwOut, nullptr)) {
+ return true;
+ }
+
+ // If a battery intended for general use (i.e. system use) is not a UPS
+ // (i.e. short term), then we know for certain we have a battery.
+ if ((bi.Capabilities & BATTERY_SYSTEM_BATTERY) &&
+ !(bi.Capabilities & BATTERY_IS_SHORT_TERM)) {
+ return true;
+ }
+
+ // Otherwise we check the next battery.
+ ++i;
+ }
+
+ // If we fail to enumerate because there are no more batteries to check, then
+ // we can safely say there are indeed no system batteries.
+ return ::GetLastError() != ERROR_NO_MORE_ITEMS;
+}
+
+/* Other interesting places for info:
+ * IDXGIAdapter::GetDesc()
+ * IDirectDraw7::GetAvailableVidMem()
+ * e->GetAvailableTextureMem()
+ * */
+
+#define DEVICE_KEY_PREFIX L"\\Registry\\Machine\\"
+nsresult GfxInfo::Init() {
+ nsresult rv = GfxInfoBase::Init();
+
+ // If we are locked down in a content process, we can't call any of the
+ // Win32k APIs below. Any method that accesses members of this class should
+ // assert that it's not used in content
+ if (IsWin32kLockedDown()) {
+ return rv;
+ }
+
+ mHasBattery = HasBattery();
+
+ DISPLAY_DEVICEW displayDevice;
+ displayDevice.cb = sizeof(displayDevice);
+ int deviceIndex = 0;
+
+ const char* spoofedWindowsVersion =
+ PR_GetEnv("MOZ_GFX_SPOOF_WINDOWS_VERSION");
+ if (spoofedWindowsVersion) {
+ PR_sscanf(spoofedWindowsVersion, "%x,%u", &mWindowsVersion,
+ &mWindowsBuildNumber);
+ } else {
+ OSVERSIONINFO vinfo;
+ vinfo.dwOSVersionInfoSize = sizeof(vinfo);
+#ifdef _MSC_VER
+# pragma warning(push)
+# pragma warning(disable : 4996)
+#endif
+ if (!GetVersionEx(&vinfo)) {
+#ifdef _MSC_VER
+# pragma warning(pop)
+#endif
+ mWindowsVersion = kWindowsUnknown;
+ } else {
+ mWindowsVersion =
+ int32_t(vinfo.dwMajorVersion << 16) + vinfo.dwMinorVersion;
+ mWindowsBuildNumber = vinfo.dwBuildNumber;
+ }
+ }
+
+ mDeviceKeyDebug = u"PrimarySearch"_ns;
+
+ while (EnumDisplayDevicesW(nullptr, deviceIndex, &displayDevice, 0)) {
+ if (displayDevice.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) {
+ mDeviceKeyDebug = u"NullSearch"_ns;
+ break;
+ }
+ deviceIndex++;
+ }
+
+ // make sure the string is nullptr terminated
+ if (wcsnlen(displayDevice.DeviceKey, ArrayLength(displayDevice.DeviceKey)) ==
+ ArrayLength(displayDevice.DeviceKey)) {
+ // we did not find a nullptr
+ return rv;
+ }
+
+ mDeviceKeyDebug = displayDevice.DeviceKey;
+
+ /* DeviceKey is "reserved" according to MSDN so we'll be careful with it */
+ /* check that DeviceKey begins with DEVICE_KEY_PREFIX */
+ /* some systems have a DeviceKey starting with \REGISTRY\Machine\ so we need
+ * to compare case insenstively */
+ /* If the device key is empty, we are most likely in a remote desktop
+ * environment. In this case we set the devicekey to an empty string so
+ * it can be handled later.
+ */
+ if (displayDevice.DeviceKey[0] != '\0') {
+ if (_wcsnicmp(displayDevice.DeviceKey, DEVICE_KEY_PREFIX,
+ ArrayLength(DEVICE_KEY_PREFIX) - 1) != 0) {
+ return rv;
+ }
+
+ // chop off DEVICE_KEY_PREFIX
+ mDeviceKey[0] =
+ displayDevice.DeviceKey + ArrayLength(DEVICE_KEY_PREFIX) - 1;
+ } else {
+ mDeviceKey[0].Truncate();
+ }
+
+ mDeviceID[0] = displayDevice.DeviceID;
+ mDeviceString[0] = displayDevice.DeviceString;
+
+ // On Windows 8 and Server 2012 hosts, we want to not block RDP
+ // sessions from attempting hardware acceleration. RemoteFX
+ // provides features and functionaltiy that can give a good D3D10 +
+ // D2D + DirectWrite experience emulated via a software GPU.
+ //
+ // Unfortunately, the Device ID is nullptr, and we can't enumerate
+ // it using the setup infrastructure (SetupDiGetClassDevsW below
+ // will return INVALID_HANDLE_VALUE).
+ UINT flags = DIGCF_PRESENT | DIGCF_PROFILE | DIGCF_ALLCLASSES;
+ if (mWindowsVersion >= kWindows8 && mDeviceID[0].Length() == 0 &&
+ mDeviceString[0].EqualsLiteral("RDPUDD Chained DD")) {
+ WCHAR sysdir[255];
+ UINT len = GetSystemDirectory(sysdir, sizeof(sysdir));
+ if (len < sizeof(sysdir)) {
+ nsString rdpudd(sysdir);
+ rdpudd.AppendLiteral("\\rdpudd.dll");
+ gfxWindowsPlatform::GetDLLVersion(rdpudd.BeginReading(),
+ mDriverVersion[0]);
+ mDriverDate[0].AssignLiteral("01-01-1970");
+
+ // 0x1414 is Microsoft; 0xfefe is an invented (and unused) code
+ mDeviceID[0].AssignLiteral("PCI\\VEN_1414&DEV_FEFE&SUBSYS_00000000");
+ flags |= DIGCF_DEVICEINTERFACE;
+ }
+ }
+
+ /* create a device information set composed of the current display device */
+ HDEVINFO devinfo =
+ SetupDiGetClassDevsW(nullptr, mDeviceID[0].get(), nullptr, flags);
+
+ if (devinfo != INVALID_HANDLE_VALUE) {
+ HKEY key;
+ LONG result;
+ WCHAR value[255];
+ DWORD dwcbData;
+ SP_DEVINFO_DATA devinfoData;
+ DWORD memberIndex = 0;
+
+ devinfoData.cbSize = sizeof(devinfoData);
+ constexpr auto driverKeyPre =
+ u"System\\CurrentControlSet\\Control\\Class\\"_ns;
+ /* enumerate device information elements in the device information set */
+ while (SetupDiEnumDeviceInfo(devinfo, memberIndex++, &devinfoData)) {
+ /* get a string that identifies the device's driver key */
+ if (SetupDiGetDeviceRegistryPropertyW(devinfo, &devinfoData, SPDRP_DRIVER,
+ nullptr, (PBYTE)value,
+ sizeof(value), nullptr)) {
+ nsAutoString driverKey(driverKeyPre);
+ driverKey += value;
+ result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, driverKey.get(), 0,
+ KEY_QUERY_VALUE, &key);
+ if (result == ERROR_SUCCESS) {
+ /* we've found the driver we're looking for */
+ dwcbData = sizeof(value);
+ result = RegQueryValueExW(key, L"DriverVersion", nullptr, nullptr,
+ (LPBYTE)value, &dwcbData);
+ if (result == ERROR_SUCCESS) {
+ mDriverVersion[0] = value;
+ } else {
+ // If the entry wasn't found, assume the worst (0.0.0.0).
+ mDriverVersion[0].AssignLiteral("0.0.0.0");
+ }
+ dwcbData = sizeof(value);
+ result = RegQueryValueExW(key, L"DriverDate", nullptr, nullptr,
+ (LPBYTE)value, &dwcbData);
+ if (result == ERROR_SUCCESS) {
+ mDriverDate[0] = value;
+ } else {
+ // Again, assume the worst
+ mDriverDate[0].AssignLiteral("01-01-1970");
+ }
+ RegCloseKey(key);
+ break;
+ }
+ }
+ }
+
+ SetupDiDestroyDeviceInfoList(devinfo);
+ }
+
+ // It is convenient to have these as integers
+ uint32_t adapterVendorID[2] = {0, 0};
+ uint32_t adapterDeviceID[2] = {0, 0};
+ uint32_t adapterSubsysID[2] = {0, 0};
+
+ adapterVendorID[0] = ParseIDFromDeviceID(mDeviceID[0], u"VEN_"_ns, 4);
+ adapterDeviceID[0] = ParseIDFromDeviceID(mDeviceID[0], u"&DEV_"_ns, 4);
+ adapterSubsysID[0] = ParseIDFromDeviceID(mDeviceID[0], u"&SUBSYS_"_ns, 8);
+
+ // Sometimes we don't get the valid device using this method. For now,
+ // allow zero vendor or device as valid, as long as the other value is
+ // non-zero.
+ bool foundValidDevice = (adapterVendorID[0] != 0 || adapterDeviceID[0] != 0);
+
+ // We now check for second display adapter. If we didn't find the valid
+ // device using the original approach, we will try the alternative.
+
+ // Device interface class for display adapters.
+ CLSID GUID_DISPLAY_DEVICE_ARRIVAL;
+ HRESULT hresult = CLSIDFromString(L"{1CA05180-A699-450A-9A0C-DE4FBE3DDD89}",
+ &GUID_DISPLAY_DEVICE_ARRIVAL);
+ if (hresult == NOERROR) {
+ devinfo =
+ SetupDiGetClassDevsW(&GUID_DISPLAY_DEVICE_ARRIVAL, nullptr, nullptr,
+ DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
+
+ if (devinfo != INVALID_HANDLE_VALUE) {
+ HKEY key;
+ LONG result;
+ WCHAR value[255];
+ DWORD dwcbData;
+ SP_DEVINFO_DATA devinfoData;
+ DWORD memberIndex = 0;
+ devinfoData.cbSize = sizeof(devinfoData);
+
+ nsAutoString adapterDriver2;
+ nsAutoString deviceID2;
+ nsAutoString driverVersion2;
+ nsAutoString driverDate2;
+
+ constexpr auto driverKeyPre =
+ u"System\\CurrentControlSet\\Control\\Class\\"_ns;
+ /* enumerate device information elements in the device information set */
+ while (SetupDiEnumDeviceInfo(devinfo, memberIndex++, &devinfoData)) {
+ /* get a string that identifies the device's driver key */
+ if (SetupDiGetDeviceRegistryPropertyW(
+ devinfo, &devinfoData, SPDRP_DRIVER, nullptr, (PBYTE)value,
+ sizeof(value), nullptr)) {
+ nsAutoString driverKey2(driverKeyPre);
+ driverKey2 += value;
+ result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, driverKey2.get(), 0,
+ KEY_QUERY_VALUE, &key);
+ if (result == ERROR_SUCCESS) {
+ dwcbData = sizeof(value);
+ result = RegQueryValueExW(key, L"MatchingDeviceId", nullptr,
+ nullptr, (LPBYTE)value, &dwcbData);
+ if (result != ERROR_SUCCESS) {
+ continue;
+ }
+ deviceID2 = value;
+ adapterVendorID[1] = ParseIDFromDeviceID(deviceID2, u"VEN_"_ns, 4);
+ adapterDeviceID[1] = ParseIDFromDeviceID(deviceID2, u"&DEV_"_ns, 4);
+ // Skip the devices we already considered, as well as any
+ // "zero" ones.
+ if ((adapterVendorID[0] == adapterVendorID[1] &&
+ adapterDeviceID[0] == adapterDeviceID[1]) ||
+ (adapterVendorID[1] == 0 && adapterDeviceID[1] == 0)) {
+ RegCloseKey(key);
+ continue;
+ }
+
+ // If this device is missing driver information, it is unlikely to
+ // be a real display adapter.
+ if (NS_FAILED(GetKeyValue(driverKey2.get(),
+ L"InstalledDisplayDrivers",
+ adapterDriver2, REG_MULTI_SZ))) {
+ RegCloseKey(key);
+ continue;
+ }
+ dwcbData = sizeof(value);
+ result = RegQueryValueExW(key, L"DriverVersion", nullptr, nullptr,
+ (LPBYTE)value, &dwcbData);
+ if (result != ERROR_SUCCESS) {
+ RegCloseKey(key);
+ continue;
+ }
+ driverVersion2 = value;
+ dwcbData = sizeof(value);
+ result = RegQueryValueExW(key, L"DriverDate", nullptr, nullptr,
+ (LPBYTE)value, &dwcbData);
+ if (result != ERROR_SUCCESS) {
+ RegCloseKey(key);
+ continue;
+ }
+ driverDate2 = value;
+ dwcbData = sizeof(value);
+ result = RegQueryValueExW(key, L"Device Description", nullptr,
+ nullptr, (LPBYTE)value, &dwcbData);
+ if (result != ERROR_SUCCESS) {
+ dwcbData = sizeof(value);
+ result = RegQueryValueExW(key, L"DriverDesc", nullptr, nullptr,
+ (LPBYTE)value, &dwcbData);
+ }
+ RegCloseKey(key);
+ if (result == ERROR_SUCCESS) {
+ // If we didn't find a valid device with the original method
+ // take this one, and continue looking for the second GPU.
+ if (!foundValidDevice) {
+ foundValidDevice = true;
+ adapterVendorID[0] = adapterVendorID[1];
+ adapterDeviceID[0] = adapterDeviceID[1];
+ mDeviceString[0] = value;
+ mDeviceID[0] = deviceID2;
+ mDeviceKey[0] = driverKey2;
+ mDriverVersion[0] = driverVersion2;
+ mDriverDate[0] = driverDate2;
+ adapterSubsysID[0] =
+ ParseIDFromDeviceID(mDeviceID[0], u"&SUBSYS_"_ns, 8);
+ continue;
+ }
+
+ mHasDualGPU = true;
+ mDeviceString[1] = value;
+ mDeviceID[1] = deviceID2;
+ mDeviceKey[1] = driverKey2;
+ mDriverVersion[1] = driverVersion2;
+ mDriverDate[1] = driverDate2;
+ adapterSubsysID[1] =
+ ParseIDFromDeviceID(mDeviceID[1], u"&SUBSYS_"_ns, 8);
+ mAdapterVendorID[1].AppendPrintf("0x%04x", adapterVendorID[1]);
+ mAdapterDeviceID[1].AppendPrintf("0x%04x", adapterDeviceID[1]);
+ mAdapterSubsysID[1].AppendPrintf("%08x", adapterSubsysID[1]);
+ break;
+ }
+ }
+ }
+ }
+
+ SetupDiDestroyDeviceInfoList(devinfo);
+ }
+ }
+
+ mAdapterVendorID[0].AppendPrintf("0x%04x", adapterVendorID[0]);
+ mAdapterDeviceID[0].AppendPrintf("0x%04x", adapterDeviceID[0]);
+ mAdapterSubsysID[0].AppendPrintf("%08x", adapterSubsysID[0]);
+
+ // Sometimes, the enumeration is not quite right and the two adapters
+ // end up being swapped. Actually enumerate the adapters that come
+ // back from the DXGI factory to check, and tag the second as active
+ // if found.
+ if (mHasDualGPU) {
+ nsModuleHandle dxgiModule(LoadLibrarySystem32(L"dxgi.dll"));
+ decltype(CreateDXGIFactory)* createDXGIFactory =
+ (decltype(CreateDXGIFactory)*)GetProcAddress(dxgiModule,
+ "CreateDXGIFactory");
+
+ if (createDXGIFactory) {
+ RefPtr<IDXGIFactory> factory = nullptr;
+ createDXGIFactory(__uuidof(IDXGIFactory), (void**)(&factory));
+ if (factory) {
+ RefPtr<IDXGIAdapter> adapter;
+ if (SUCCEEDED(factory->EnumAdapters(0, getter_AddRefs(adapter)))) {
+ DXGI_ADAPTER_DESC desc;
+ PodZero(&desc);
+ if (SUCCEEDED(adapter->GetDesc(&desc))) {
+ if (desc.VendorId != adapterVendorID[0] &&
+ desc.DeviceId != adapterDeviceID[0] &&
+ desc.VendorId == adapterVendorID[1] &&
+ desc.DeviceId == adapterDeviceID[1]) {
+ mActiveGPUIndex = 1;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ mHasDriverVersionMismatch = false;
+ if (mAdapterVendorID[mActiveGPUIndex] ==
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::Intel)) {
+ // we've had big crashers (bugs 590373 and 595364) apparently correlated
+ // with bad Intel driver installations where the DriverVersion reported
+ // by the registry was not the version of the DLL.
+
+ // Note that these start without the .dll extension but eventually gain it.
+ bool is64bitApp = sizeof(void*) == 8;
+ nsAutoString dllFileName(is64bitApp ? u"igd10umd64" : u"igd10umd32");
+ nsAutoString dllFileName2(is64bitApp ? u"igd10iumd64" : u"igd10iumd32");
+
+ nsString dllVersion, dllVersion2;
+ uint64_t dllNumericVersion = 0, dllNumericVersion2 = 0,
+ driverNumericVersion = 0, knownSafeMismatchVersion = 0;
+
+ // Only parse the DLL version for those found in the driver list
+ nsAutoString eligibleDLLs;
+ if (NS_SUCCEEDED(GetAdapterDriver(eligibleDLLs))) {
+ if (FindInReadable(dllFileName, eligibleDLLs)) {
+ dllFileName += u".dll"_ns;
+ gfxWindowsPlatform::GetDLLVersion(dllFileName.get(), dllVersion);
+ ParseDriverVersion(dllVersion, &dllNumericVersion);
+ }
+ if (FindInReadable(dllFileName2, eligibleDLLs)) {
+ dllFileName2 += u".dll"_ns;
+ gfxWindowsPlatform::GetDLLVersion(dllFileName2.get(), dllVersion2);
+ ParseDriverVersion(dllVersion2, &dllNumericVersion2);
+ }
+ }
+
+ // Sometimes the DLL is not in the System32 nor SysWOW64 directories. But
+ // UserModeDriverName (or UserModeDriverNameWow, if available) might provide
+ // the full path to the DLL in some DriverStore FileRepository.
+ if (dllNumericVersion == 0 && dllNumericVersion2 == 0) {
+ nsTArray<nsString> eligibleDLLpaths;
+ const WCHAR* keyLocation = mDeviceKey[mActiveGPUIndex].get();
+ GetKeyValues(keyLocation, L"UserModeDriverName", eligibleDLLpaths);
+ GetKeyValues(keyLocation, L"UserModeDriverNameWow", eligibleDLLpaths);
+ size_t length = eligibleDLLpaths.Length();
+ for (size_t i = 0;
+ i < length && dllNumericVersion == 0 && dllNumericVersion2 == 0;
+ ++i) {
+ if (FindInReadable(dllFileName, eligibleDLLpaths[i])) {
+ gfxWindowsPlatform::GetDLLVersion(eligibleDLLpaths[i].get(),
+ dllVersion);
+ ParseDriverVersion(dllVersion, &dllNumericVersion);
+ } else if (FindInReadable(dllFileName2, eligibleDLLpaths[i])) {
+ gfxWindowsPlatform::GetDLLVersion(eligibleDLLpaths[i].get(),
+ dllVersion2);
+ ParseDriverVersion(dllVersion2, &dllNumericVersion2);
+ }
+ }
+ }
+
+ ParseDriverVersion(mDriverVersion[mActiveGPUIndex], &driverNumericVersion);
+ ParseDriverVersion(u"9.17.10.0"_ns, &knownSafeMismatchVersion);
+
+ // If there's a driver version mismatch, consider this harmful only when
+ // the driver version is less than knownSafeMismatchVersion. See the
+ // above comment about crashes with old mismatches. If the GetDllVersion
+ // call fails, we are not calling it a mismatch.
+ if ((dllNumericVersion != 0 && dllNumericVersion != driverNumericVersion) ||
+ (dllNumericVersion2 != 0 &&
+ dllNumericVersion2 != driverNumericVersion)) {
+ if (driverNumericVersion < knownSafeMismatchVersion ||
+ std::max(dllNumericVersion, dllNumericVersion2) <
+ knownSafeMismatchVersion) {
+ mHasDriverVersionMismatch = true;
+ gfxCriticalNoteOnce
+ << "Mismatched driver versions between the registry "
+ << NS_ConvertUTF16toUTF8(mDriverVersion[mActiveGPUIndex]).get()
+ << " and DLL(s) " << NS_ConvertUTF16toUTF8(dllVersion).get() << ", "
+ << NS_ConvertUTF16toUTF8(dllVersion2).get() << " reported.";
+ }
+ } else if (dllNumericVersion == 0 && dllNumericVersion2 == 0) {
+ // Leave it as an asserting error for now, to see if we can find
+ // a system that exhibits this kind of a problem internally.
+ gfxCriticalErrorOnce()
+ << "Potential driver version mismatch ignored due to missing DLLs "
+ << NS_ConvertUTF16toUTF8(dllFileName).get()
+ << " v=" << NS_ConvertUTF16toUTF8(dllVersion).get() << " and "
+ << NS_ConvertUTF16toUTF8(dllFileName2).get()
+ << " v=" << NS_ConvertUTF16toUTF8(dllVersion2).get();
+ }
+ }
+
+ const char* spoofedDriverVersionString =
+ PR_GetEnv("MOZ_GFX_SPOOF_DRIVER_VERSION");
+ if (spoofedDriverVersionString) {
+ mDriverVersion[mActiveGPUIndex].AssignASCII(spoofedDriverVersionString);
+ }
+
+ const char* spoofedVendor = PR_GetEnv("MOZ_GFX_SPOOF_VENDOR_ID");
+ if (spoofedVendor) {
+ mAdapterVendorID[mActiveGPUIndex].AssignASCII(spoofedVendor);
+ }
+
+ const char* spoofedDevice = PR_GetEnv("MOZ_GFX_SPOOF_DEVICE_ID");
+ if (spoofedDevice) {
+ mAdapterDeviceID[mActiveGPUIndex].AssignASCII(spoofedDevice);
+ }
+
+ AddCrashReportAnnotations();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDescription = mDeviceString[mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDescription = mDeviceString[1 - mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) {
+ AssertNotWin32kLockdown();
+
+ uint32_t result = 0;
+ if (NS_FAILED(GetKeyValue(mDeviceKey[mActiveGPUIndex].get(),
+ L"HardwareInformation.qwMemorySize", result,
+ REG_QWORD)) ||
+ result == 0) {
+ if (NS_FAILED(GetKeyValue(mDeviceKey[mActiveGPUIndex].get(),
+ L"HardwareInformation.MemorySize", result,
+ REG_DWORD))) {
+ result = 0;
+ }
+ }
+ *aAdapterRAM = result;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) {
+ AssertNotWin32kLockdown();
+
+ uint32_t result = 0;
+ if (mHasDualGPU) {
+ if (NS_FAILED(GetKeyValue(mDeviceKey[1 - mActiveGPUIndex].get(),
+ L"HardwareInformation.qwMemorySize", result,
+ REG_QWORD)) ||
+ result == 0) {
+ if (NS_FAILED(GetKeyValue(mDeviceKey[1 - mActiveGPUIndex].get(),
+ L"HardwareInformation.MemorySize", result,
+ REG_DWORD))) {
+ result = 0;
+ }
+ }
+ }
+ *aAdapterRAM = result;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) {
+ AssertNotWin32kLockdown();
+
+ if (NS_FAILED(GetKeyValue(mDeviceKey[mActiveGPUIndex].get(),
+ L"InstalledDisplayDrivers", aAdapterDriver,
+ REG_MULTI_SZ)))
+ aAdapterDriver = L"Unknown";
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) {
+ AssertNotWin32kLockdown();
+
+ if (!mHasDualGPU) {
+ aAdapterDriver.Truncate();
+ } else if (NS_FAILED(GetKeyValue(mDeviceKey[1 - mActiveGPUIndex].get(),
+ L"InstalledDisplayDrivers", aAdapterDriver,
+ REG_MULTI_SZ))) {
+ aAdapterDriver = L"Unknown";
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) {
+ aAdapterDriverVendor.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDriverVersion = mDriverVersion[mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDriverDate = mDriverDate[mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) {
+ aAdapterDriverVendor.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDriverVersion = mDriverVersion[1 - mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDriverDate = mDriverDate[1 - mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) {
+ AssertNotWin32kLockdown();
+
+ aAdapterVendorID = mAdapterVendorID[mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) {
+ AssertNotWin32kLockdown();
+
+ aAdapterVendorID = mAdapterVendorID[1 - mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDeviceID = mAdapterDeviceID[mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) {
+ AssertNotWin32kLockdown();
+
+ aAdapterDeviceID = mAdapterDeviceID[1 - mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) {
+ AssertNotWin32kLockdown();
+
+ aAdapterSubsysID = mAdapterSubsysID[mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) {
+ AssertNotWin32kLockdown();
+
+ aAdapterSubsysID = mAdapterSubsysID[1 - mActiveGPUIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) {
+ // This is never the case, as the active GPU ends up being
+ // the first one. It should probably be removed.
+ *aIsGPU2Active = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* Cisco's VPN software can cause corruption of the floating point state.
+ * Make a note of this in our crash reports so that some weird crashes
+ * make more sense */
+static void CheckForCiscoVPN() {
+ LONG result;
+ HKEY key;
+ /* This will give false positives, but hopefully no false negatives */
+ result =
+ RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Cisco Systems\\VPN Client",
+ 0, KEY_QUERY_VALUE, &key);
+ if (result == ERROR_SUCCESS) {
+ RegCloseKey(key);
+ CrashReporter::AppendAppNotesToCrashReport("Cisco VPN\n"_ns);
+ }
+}
+
+void GfxInfo::AddCrashReportAnnotations() {
+ AssertNotWin32kLockdown();
+
+ CheckForCiscoVPN();
+
+ if (mHasDriverVersionMismatch) {
+ CrashReporter::AppendAppNotesToCrashReport("DriverVersionMismatch\n"_ns);
+ }
+
+ nsString deviceID, vendorID, driverVersion, subsysID;
+ nsCString narrowDeviceID, narrowVendorID, narrowDriverVersion, narrowSubsysID;
+
+ GetAdapterDeviceID(deviceID);
+ CopyUTF16toUTF8(deviceID, narrowDeviceID);
+ GetAdapterVendorID(vendorID);
+ CopyUTF16toUTF8(vendorID, narrowVendorID);
+ GetAdapterDriverVersion(driverVersion);
+ CopyUTF16toUTF8(driverVersion, narrowDriverVersion);
+ GetAdapterSubsysID(subsysID);
+ CopyUTF16toUTF8(subsysID, narrowSubsysID);
+
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID,
+ narrowVendorID);
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID,
+ narrowDeviceID);
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AdapterDriverVersion, narrowDriverVersion);
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterSubsysID,
+ narrowSubsysID);
+
+ /* Add an App Note, this contains extra information. */
+ nsAutoCString note;
+
+ // TODO: We should probably convert this into a proper annotation
+ if (vendorID == GfxDriverInfo::GetDeviceVendor(DeviceVendor::All)) {
+ /* if we didn't find a valid vendorID lets append the mDeviceID string to
+ * try to find out why */
+ LossyAppendUTF16toASCII(mDeviceID[mActiveGPUIndex], note);
+ note.AppendLiteral(", ");
+ LossyAppendUTF16toASCII(mDeviceKeyDebug, note);
+ }
+ note.AppendLiteral("\n");
+
+ if (mHasDualGPU) {
+ nsString deviceID2, vendorID2, subsysID2;
+ nsAutoString adapterDriverVersionString2;
+ nsCString narrowDeviceID2, narrowVendorID2, narrowSubsysID2;
+
+ // Make a slight difference between the two cases so that we
+ // can see it in the crash reports. It may come in handy.
+ if (mActiveGPUIndex == 1) {
+ note.AppendLiteral("Has dual GPUs. GPU-#2: ");
+ } else {
+ note.AppendLiteral("Has dual GPUs. GPU #2: ");
+ }
+ GetAdapterDeviceID2(deviceID2);
+ CopyUTF16toUTF8(deviceID2, narrowDeviceID2);
+ GetAdapterVendorID2(vendorID2);
+ CopyUTF16toUTF8(vendorID2, narrowVendorID2);
+ GetAdapterDriverVersion2(adapterDriverVersionString2);
+ GetAdapterSubsysID(subsysID2);
+ CopyUTF16toUTF8(subsysID2, narrowSubsysID2);
+ note.AppendLiteral("AdapterVendorID2: ");
+ note.Append(narrowVendorID2);
+ note.AppendLiteral(", AdapterDeviceID2: ");
+ note.Append(narrowDeviceID2);
+ note.AppendLiteral(", AdapterSubsysID2: ");
+ note.Append(narrowSubsysID2);
+ note.AppendLiteral(", AdapterDriverVersion2: ");
+ note.Append(NS_LossyConvertUTF16toASCII(adapterDriverVersionString2));
+ }
+ CrashReporter::AppendAppNotesToCrashReport(note);
+}
+
+static OperatingSystem WindowsVersionToOperatingSystem(
+ int32_t aWindowsVersion) {
+ switch (aWindowsVersion) {
+ case kWindows7:
+ return OperatingSystem::Windows7;
+ case kWindows8:
+ return OperatingSystem::Windows8;
+ case kWindows8_1:
+ return OperatingSystem::Windows8_1;
+ case kWindows10:
+ return OperatingSystem::Windows10;
+ case kWindowsUnknown:
+ default:
+ return OperatingSystem::Unknown;
+ }
+}
+
+// Return true if the CPU supports AVX, but the operating system does not.
+#if defined(_M_X64)
+static inline bool DetectBrokenAVX() {
+ int regs[4];
+ __cpuid(regs, 0);
+ if (regs[0] == 0) {
+ // Level not supported.
+ return false;
+ }
+
+ __cpuid(regs, 1);
+
+ const unsigned AVX = 1u << 28;
+ const unsigned XSAVE = 1u << 26;
+ if ((regs[2] & (AVX | XSAVE)) != (AVX | XSAVE)) {
+ // AVX is not supported on this CPU.
+ return false;
+ }
+
+ const unsigned OSXSAVE = 1u << 27;
+ if ((regs[2] & OSXSAVE) != OSXSAVE) {
+ // AVX is supported, but the OS didn't enable it.
+ // This can be forced via bcdedit /set xsavedisable 1.
+ return true;
+ }
+
+ const unsigned AVX_CTRL_BITS = (1 << 1) | (1 << 2);
+ return (xgetbv(0) & AVX_CTRL_BITS) != AVX_CTRL_BITS;
+}
+#endif
+
+const nsTArray<GfxDriverInfo>& GfxInfo::GetGfxDriverInfo() {
+ if (!sDriverInfo->Length()) {
+ /*
+ * It should be noted here that more specialized rules on certain features
+ * should be inserted -before- more generalized restriction. As the first
+ * match for feature/OS/device found in the list will be used for the final
+ * blocklisting call.
+ */
+
+ /*
+ * NVIDIA entries
+ */
+ /*
+ * The last 5 digit of the NVIDIA driver version maps to the version that
+ * NVIDIA uses. The minor version (15, 16, 17) corresponds roughtly to the
+ * OS (Vista, Win7, Win7) but they show up in smaller numbers across all
+ * OS versions (perhaps due to OS upgrades). So we want to support
+ * October 2009+ drivers across all these minor versions.
+ *
+ * 187.45 (late October 2009) and earlier contain a bug which can cause us
+ * to crash on shutdown.
+ */
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::NvidiaAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN_OR_EQUAL,
+ V(8, 15, 11, 8745), "FEATURE_FAILURE_NV_W7_15",
+ "nVidia driver > 187.45");
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows7, DeviceFamily::NvidiaAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BETWEEN_INCLUSIVE_START, V(8, 16, 10, 0000), V(8, 16, 11, 8745),
+ "FEATURE_FAILURE_NV_W7_16", "nVidia driver > 187.45");
+ // Telemetry doesn't show any driver in this range so it might not even be
+ // required.
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows7, DeviceFamily::NvidiaAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BETWEEN_INCLUSIVE_START, V(8, 17, 10, 0000), V(8, 17, 11, 8745),
+ "FEATURE_FAILURE_NV_W7_17", "nVidia driver > 187.45");
+
+ /*
+ * AMD/ATI entries. 8.56.1.15 is the driver that shipped with Windows 7 RTM
+ */
+ APPEND_TO_DRIVER_BLOCKLIST(OperatingSystem::Windows, DeviceFamily::AtiAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_LESS_THAN, V(8, 56, 1, 15),
+ "FEATURE_FAILURE_AMD1", "8.56.1.15");
+
+ // Bug 1099252
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows7, DeviceFamily::AtiAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_EQUAL, V(8, 832, 0, 0),
+ "FEATURE_FAILURE_BUG_1099252");
+
+ // Bug 1118695
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows7, DeviceFamily::AtiAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_EQUAL, V(8, 783, 2, 2000),
+ "FEATURE_FAILURE_BUG_1118695");
+
+ // Bug 1587155
+ //
+ // There are a several reports of strange rendering corruptions with this
+ // driver version, with and without webrender. We weren't able to
+ // reproduce these problems, but the users were able to update their
+ // drivers and it went away. So just to be safe, let's blocklist all
+ // gpu use with this particular (very old) driver, restricted
+ // to Win10 since we only have reports from that platform.
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows10, DeviceFamily::AtiAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL,
+ V(22, 19, 162, 4), "FEATURE_FAILURE_BUG_1587155");
+
+ // Bug 1829487 - Work around a gen6 driver bug that miscompiles shaders
+ // resulting
+ // in black squares. Disabling shader optimization pass
+ // appears to work around this for now.
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelSandyBridge,
+ nsIGfxInfo::FEATURE_WEBRENDER_OPTIMIZED_SHADERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1829487");
+
+ // Bug 1198815
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(15, 200, 0, 0), V(15, 200, 1062, 1004), "FEATURE_FAILURE_BUG_1198815",
+ "15.200.0.0-15.200.1062.1004");
+
+ // Bug 1267970
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows10, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(15, 200, 0, 0), V(15, 301, 2301, 1002), "FEATURE_FAILURE_BUG_1267970",
+ "15.200.0.0-15.301.2301.1002");
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows10, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(16, 100, 0, 0), V(16, 300, 2311, 0), "FEATURE_FAILURE_BUG_1267970",
+ "16.100.0.0-16.300.2311.0");
+
+ /*
+ * Bug 783517 - crashes in AMD driver on Windows 8
+ */
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows8, DeviceFamily::AtiAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BETWEEN_INCLUSIVE_START, V(8, 982, 0, 0), V(8, 983, 0, 0),
+ "FEATURE_FAILURE_BUG_783517_AMD", "!= 8.982.*.*");
+
+ /*
+ * Bug 1599981 - crashes in AMD driver on Windows 10
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows10, DeviceFamily::RadeonCaicos,
+ nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(15, 301, 1901, 0), "FEATURE_FAILURE_BUG_1599981");
+
+ /* OpenGL on any ATI/AMD hardware is discouraged
+ * See:
+ * bug 619773 - WebGL: Crash with blue screen : "NMI: Parity Check / Memory
+ * Parity Error" bugs 584403, 584404, 620924 - crashes in atioglxx
+ * + many complaints about incorrect rendering
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_DISCOURAGED,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_OGL_ATI_DIS");
+
+/*
+ * Intel entries
+ */
+
+/* The driver versions used here come from bug 594877. They might not
+ * be particularly relevant anymore.
+ */
+#define IMPLEMENT_INTEL_DRIVER_BLOCKLIST(winVer, devFamily, driverVer, ruleId) \
+ APPEND_TO_DRIVER_BLOCKLIST2(winVer, devFamily, \
+ GfxDriverInfo::optionalFeatures, \
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, \
+ DRIVER_LESS_THAN, driverVer, ruleId)
+
+#define IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(winVer, devFamily, driverVer, \
+ ruleId) \
+ APPEND_TO_DRIVER_BLOCKLIST2(winVer, devFamily, nsIGfxInfo::FEATURE_DIRECT2D, \
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, \
+ DRIVER_BUILD_ID_LESS_THAN, driverVer, ruleId)
+
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7,
+ DeviceFamily::IntelGMA500, 2026,
+ "FEATURE_FAILURE_594877_7");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMA900,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_594877_8");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7,
+ DeviceFamily::IntelGMA950, 1930,
+ "FEATURE_FAILURE_594877_9");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7,
+ DeviceFamily::IntelGMA3150, 2117,
+ "FEATURE_FAILURE_594877_10");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7,
+ DeviceFamily::IntelGMAX3000, 1930,
+ "FEATURE_FAILURE_594877_11");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(
+ OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge,
+ 2202, "FEATURE_FAILURE_594877_12");
+
+ /* Disable Direct2D on Intel GMAX4500 devices because of rendering
+ * corruption discovered in bug 1180379. These seems to affect even the most
+ * recent drivers. We're black listing all of the devices to be safe even
+ * though we've only confirmed the issue on the G45
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelGMAX4500HD,
+ nsIGfxInfo::FEATURE_DIRECT2D, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_1180379");
+
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMA500, V(5, 0, 0, 2026),
+ "FEATURE_FAILURE_INTEL_16");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMA900,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_INTEL_17");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMA950,
+ V(8, 15, 10, 1930), "FEATURE_FAILURE_INTEL_18");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMA3150,
+ V(8, 14, 10, 1972), "FEATURE_FAILURE_INTEL_19");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMAX3000,
+ V(7, 15, 10, 1666), "FEATURE_FAILURE_INTEL_20");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMAX4500HD,
+ V(7, 15, 10, 1666), "FEATURE_FAILURE_INTEL_21");
+ IMPLEMENT_INTEL_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge,
+ V(7, 15, 10, 1666), "FEATURE_FAILURE_INTEL_22");
+
+ // Bug 1074378
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelGMAX4500HD,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL,
+ V(8, 15, 10, 1749), "FEATURE_FAILURE_BUG_1074378_1", "8.15.10.2342");
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL,
+ V(8, 15, 10, 1749), "FEATURE_FAILURE_BUG_1074378_2", "8.15.10.2342");
+
+ /* OpenGL on any Intel hardware is discouraged */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_DISCOURAGED,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_INTEL_OGL_DIS");
+
+ /**
+ * Disable acceleration on Intel HD 3000 for graphics drivers
+ * <= 8.15.10.2321. See bug 1018278 and bug 1060736.
+ */
+ APPEND_TO_DRIVER_BLOCKLIST(OperatingSystem::Windows,
+ DeviceFamily::IntelSandyBridge,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 2321,
+ "FEATURE_FAILURE_BUG_1018278", "X.X.X.2342");
+
+ /**
+ * Disable D2D on Win7 on Intel Haswell for graphics drivers build id <=
+ * 4578. See bug 1432610
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows7,
+ DeviceFamily::IntelHaswell,
+ nsIGfxInfo::FEATURE_DIRECT2D,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 4578,
+ "FEATURE_FAILURE_BUG_1432610");
+ /**
+ * Disable VP8 HW decoding on Windows 8.1 on Intel Haswel and a certain
+ * driver version. See bug 1760464 comment 6 and bug 1761332.
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows8_1, DeviceFamily::IntelHaswell,
+ nsIGfxInfo::FEATURE_VP8_HW_DECODE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_BUG_1760464");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows8_1, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_VP8_HW_DECODE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_EQUAL, V(10, 18, 14, 4264), "FEATURE_FAILURE_BUG_1761332");
+
+ /* Disable D2D on Win7 on Intel HD Graphics on driver <= 8.15.10.2302
+ * See bug 806786
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows7, DeviceFamily::IntelMobileHDGraphics,
+ nsIGfxInfo::FEATURE_DIRECT2D,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN_OR_EQUAL,
+ V(8, 15, 10, 2302), "FEATURE_FAILURE_BUG_806786");
+
+ /* Disable D2D on Win8 on Intel HD Graphics on driver <= 8.15.10.2302
+ * See bug 804144 and 863683
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows8, DeviceFamily::IntelMobileHDGraphics,
+ nsIGfxInfo::FEATURE_DIRECT2D,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN_OR_EQUAL,
+ V(8, 15, 10, 2302), "FEATURE_FAILURE_BUG_804144");
+
+ /* Disable D2D on Win7 on Intel HD Graphics on driver == 8.15.10.2418
+ * See bug 1433790
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge,
+ nsIGfxInfo::FEATURE_DIRECT2D,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL,
+ V(8, 15, 10, 2418), "FEATURE_FAILURE_BUG_1433790");
+
+ /* Disable D3D11 layers on Intel G41 express graphics and Intel GM965, Intel
+ * X3100, for causing device resets. See bug 1116812.
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::Bug1116812,
+ nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1116812");
+
+ /* Disable D3D11 layers on Intel GMA 3150 for failing to allocate a shared
+ * handle for textures. See bug 1207665. Additionally block D2D so we don't
+ * accidentally use WARP.
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::Bug1207665,
+ nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1207665_1");
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::Bug1207665,
+ nsIGfxInfo::FEATURE_DIRECT2D, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_BUG_1207665_2");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows10, DeviceFamily::QualcommAll,
+ nsIGfxInfo::FEATURE_DIRECT2D,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_QUALCOMM");
+
+ // Bug 1548410. Disable hardware accelerated video decoding on
+ // Qualcomm drivers used on Windows on ARM64 which are known to
+ // cause BSOD's and output suprious green frames while decoding video.
+ // Bug 1592826 expands the blocklist.
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows10, DeviceFamily::QualcommAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN_OR_EQUAL,
+ V(25, 18, 10440, 0), "FEATURE_FAILURE_BUG_1592826");
+
+ /* Disable D2D on AMD Catalyst 14.4 until 14.6
+ * See bug 984488
+ */
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_DIRECT2D,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BETWEEN_INCLUSIVE_START, V(14, 1, 0, 0), V(14, 2, 0, 0),
+ "FEATURE_FAILURE_BUG_984488_1", "ATI Catalyst 14.6+");
+
+ /* Disable D3D9 layers on NVIDIA 6100/6150/6200 series due to glitches
+ * whilst scrolling. See bugs: 612007, 644787 & 645872.
+ */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::NvidiaBlockD3D9Layers,
+ nsIGfxInfo::FEATURE_DIRECT3D_9_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_612007");
+
+ /* Microsoft RemoteFX; blocked less than 6.2.0.0 */
+ APPEND_TO_DRIVER_BLOCKLIST(
+ OperatingSystem::Windows, DeviceFamily::MicrosoftAll,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(6, 2, 0, 0), "< 6.2.0.0", "FEATURE_FAILURE_REMOTE_FX");
+
+ /* Bug 1008759: Optimus (NVidia) crash. Disable D2D on NV 310M. */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::Nvidia310M,
+ nsIGfxInfo::FEATURE_DIRECT2D, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_BUG_1008759");
+
+ /* Bug 1139503: DXVA crashes with ATI cards on windows 10. */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows10, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL,
+ V(15, 200, 1006, 0), "FEATURE_FAILURE_BUG_1139503");
+
+ /* Bug 1213107: D3D9 crashes with ATI cards on Windows 7. */
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows7, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(8, 861, 0, 0), V(8, 862, 6, 5000), "FEATURE_FAILURE_BUG_1213107_1",
+ "Radeon driver > 8.862.6.5000");
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows7, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_WEBGL_ANGLE,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(8, 861, 0, 0), V(8, 862, 6, 5000), "FEATURE_FAILURE_BUG_1213107_2",
+ "Radeon driver > 8.862.6.5000");
+
+ /* This may not be needed at all */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows7, DeviceFamily::Bug1155608,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(8, 15, 10, 2869), "FEATURE_FAILURE_INTEL_W7_HW_DECODING");
+
+ /* Bug 1203199/1092166: DXVA startup crashes on some intel drivers. */
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(9, 17, 10, 0), V(9, 17, 10, 2849), "FEATURE_FAILURE_BUG_1203199_1",
+ "Intel driver > 9.17.10.2849");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::Nvidia8800GTS,
+ nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL,
+ V(9, 18, 13, 4052), "FEATURE_FAILURE_BUG_1203199_2");
+
+ /* Bug 1137716: XXX this should really check for the matching Intel piece as
+ * well. Unfortunately, we don't have the infrastructure to do that */
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE_GPU2(
+ OperatingSystem::Windows7, DeviceFamily::Bug1137716,
+ GfxDriverInfo::optionalFeatures,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(8, 17, 12, 5730), V(8, 17, 12, 6901), "FEATURE_FAILURE_BUG_1137716",
+ "Nvidia driver > 8.17.12.6901");
+
+ /* Bug 1336710: Crash in rx::Blit9::initialize. */
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::WindowsXP, DeviceFamily::IntelGMAX4500HD,
+ nsIGfxInfo::FEATURE_WEBGL_ANGLE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_BUG_1336710");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::WindowsXP, DeviceFamily::IntelHDGraphicsToSandyBridge,
+ nsIGfxInfo::FEATURE_WEBGL_ANGLE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_FAILURE_BUG_1336710");
+
+ /* Bug 1304360: Graphical artifacts with D3D9 on Windows 7. */
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows7,
+ DeviceFamily::IntelGMAX3000,
+ nsIGfxInfo::FEATURE_DIRECT3D_9_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 1749,
+ "FEATURE_FAILURE_INTEL_W7_D3D9_LAYERS");
+
+ /* Bug 1717519/1717911: Crashes while drawing with swgl.
+ * Reproducible but not investigated yet.*/
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(8, 15, 10, 2125), V(8, 15, 10, 2141), "FEATURE_FAILURE_BUG_1717911",
+ "Intel driver > 8.15.10.2141");
+
+#if defined(_M_X64)
+ if (DetectBrokenAVX()) {
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows7, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1403353");
+ }
+#endif
+
+ ////////////////////////////////////
+ // WebGL
+
+ // Older than 5-15-2016
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_DISCOURAGED,
+ DRIVER_LESS_THAN, V(16, 200, 1010, 1002), "WEBGL_NATIVE_GL_OLD_AMD");
+
+ // Older than 11-18-2015
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_DISCOURAGED,
+ DRIVER_BUILD_ID_LESS_THAN, 4331, "WEBGL_NATIVE_GL_OLD_INTEL");
+
+ // Older than 2-23-2016
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_DISCOURAGED,
+ DRIVER_LESS_THAN, V(10, 18, 13, 6200), "WEBGL_NATIVE_GL_OLD_NVIDIA");
+
+ ////////////////////////////////////
+ // FEATURE_DX_INTEROP2
+
+ // All AMD.
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_DX_INTEROP2,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "DX_INTEROP2_AMD_CRASH");
+
+ ////////////////////////////////////
+ // FEATURE_D3D11_KEYED_MUTEX
+
+ // bug 1359416
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelHDGraphicsToSandyBridge,
+ nsIGfxInfo::FEATURE_D3D11_KEYED_MUTEX,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1359416");
+
+ // Bug 1447141, for causing device creation crashes.
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows7, DeviceFamily::Bug1447141,
+ GfxDriverInfo::optionalFeatures, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_EQUAL, V(15, 201, 2201, 0), "FEATURE_FAILURE_BUG_1447141_1");
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows7, DeviceFamily::Bug1447141,
+ GfxDriverInfo::optionalFeatures, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_EQUAL, V(15, 201, 1701, 0), "FEATURE_FAILURE_BUG_1447141_1");
+
+ // bug 1457758
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::NvidiaAll,
+ GfxDriverInfo::optionalFeatures, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_EQUAL, V(24, 21, 13, 9731), "FEATURE_FAILURE_BUG_1457758");
+
+ ////////////////////////////////////
+ // FEATURE_DX_NV12
+
+ // Bug 1437334
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelHDGraphicsToSandyBridge,
+ nsIGfxInfo::FEATURE_DX_NV12, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 4459,
+ "FEATURE_BLOCKED_DRIVER_VERSION");
+
+ ////////////////////////////////////
+ // FEATURE_DX_P010
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_DX_P010, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "FEATURE_UNQUALIFIED_P010_NVIDIA");
+
+ ////////////////////////////////////
+ // FEATURE_VIDEO_OVERLAY - ALLOWLIST
+#ifdef EARLY_BETA_OR_EARLIER
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::All,
+ nsIGfxInfo::FEATURE_VIDEO_OVERLAY, nsIGfxInfo::FEATURE_ALLOW_ALWAYS,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_ROLLOUT_ALL");
+#else
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_VIDEO_OVERLAY, nsIGfxInfo::FEATURE_ALLOW_ALWAYS,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_ROLLOUT_INTEL");
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_VIDEO_OVERLAY, nsIGfxInfo::FEATURE_ALLOW_ALWAYS,
+ DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_ROLLOUT_NVIDIA");
+#endif
+
+ ////////////////////////////////////
+ // FEATURE_HW_DECODED_VIDEO_ZERO_COPY
+
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows10, DeviceFamily::IntelSkylake,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(20, 19, 15, 4285), V(20, 19, 15, 4390), "FEATURE_FAILURE_BUG_1763280",
+ "Intel driver 20.19.15.*");
+
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows10, DeviceFamily::IntelSkylake,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(10, 18, 15, 4256), V(10, 18, 15, 4293), "FEATURE_FAILURE_BUG_1763280",
+ "Intel driver 10.18.15.*");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows10, DeviceFamily::IntelKabyLake,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1802357");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(21, 21, 13, 7576), "FEATURE_FAILURE_BUG_1767212");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_LESS_THAN, V(23, 20, 826, 5120),
+ "FEATURE_FAILURE_BUG_1767212");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::RadeonBlockZeroVideoCopy,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN,
+ V(26, 20, 15000, 37), "FEATURE_FAILURE_BUG_1767212");
+
+ ////////////////////////////////////
+ // FEATURE_HW_DECODED_VIDEO_ZERO_COPY - ALLOWLIST
+
+ // XXX ZeroCopyNV12Texture is disabled with non-intel GPUs for now.
+ // See Bug 1798242
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY,
+ nsIGfxInfo::FEATURE_ALLOW_ALWAYS, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_ROLLOUT_ALL");
+
+ ////////////////////////////////////
+ // FEATURE_REUSE_DECODER_DEVICE
+
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows10, DeviceFamily::IntelSkylake,
+ nsIGfxInfo::FEATURE_REUSE_DECODER_DEVICE,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(20, 19, 15, 4285), V(20, 19, 15, 4390), "FEATURE_FAILURE_BUG_1833809",
+ "Intel driver 20.19.15.*");
+
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows10, DeviceFamily::IntelSkylake,
+ nsIGfxInfo::FEATURE_REUSE_DECODER_DEVICE,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(10, 18, 15, 4256), V(10, 18, 15, 4293), "FEATURE_FAILURE_BUG_1833809",
+ "Intel driver 10.18.15.*");
+
+ ////////////////////////////////////
+ // FEATURE_WEBRENDER
+ // Block 8.56.1.15/16
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION,
+ DRIVER_LESS_THAN_OR_EQUAL, V(8, 56, 1, 16),
+ "CRASHY_DRIVERS_BUG_1678808");
+
+ // Shader compilation startup crashes with WebRender on Windows 7.
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows7, DeviceFamily::NvidiaAll,
+ nsIGfxInfo::FEATURE_WEBRENDER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE,
+ V(8, 17, 12, 8019), V(8, 17, 12, 8026), "FEATURE_FAILURE_BUG_1709629",
+ "nVidia driver > 280.26");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelWebRenderBlocked,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions,
+ "INTEL_DEVICE_GEN5_OR_OLDER");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::NvidiaWebRenderBlocked,
+ nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, "EARLY_NVIDIA");
+
+ ////////////////////////////////////
+ // FEATURE_WEBRENDER_COMPOSITOR
+
+#ifndef EARLY_BETA_OR_EARLIER
+ // See also bug 1616874
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_EQUAL, V(24, 20, 100, 6293),
+ "FEATURE_FAILURE_BUG_1602511");
+
+ APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows, DeviceFamily::AtiAll,
+ nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE,
+ DRIVER_LESS_THAN_OR_EQUAL, V(8, 17, 10, 1129),
+ "FEATURE_FAILURE_CHROME_BUG_800950");
+#endif
+
+ // WebRender is unable to use scissored clears in some cases
+ APPEND_TO_DRIVER_BLOCKLIST2(
+ OperatingSystem::Windows, DeviceFamily::IntelAll,
+ nsIGfxInfo::FEATURE_WEBRENDER_SCISSORED_CACHE_CLEARS,
+ nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED,
+ V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1603515");
+
+ ////////////////////////////////////
+ // FEATURE_BACKDROP_FILTER
+
+ // Backdrop filter crashes the driver. See bug 1785366 and bug 1784093.
+ APPEND_TO_DRIVER_BLOCKLIST_RANGE(
+ OperatingSystem::Windows, DeviceFamily::IntelHDGraphicsToIvyBridge,
+ nsIGfxInfo::FEATURE_BACKDROP_FILTER,
+ nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_EXCLUSIVE,
+ V(8, 15, 10, 2879), V(10, 18, 10, 4425), "FEATURE_FAILURE_BUG_1785366",
+ "Intel driver >= 10.18.10.4425");
+ }
+ return *sDriverInfo;
+}
+
+OperatingSystem GfxInfo::GetOperatingSystem() {
+ return WindowsVersionToOperatingSystem(mWindowsVersion);
+}
+
+nsresult GfxInfo::GetFeatureStatusImpl(
+ int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo, nsACString& aFailureId,
+ OperatingSystem* aOS /* = nullptr */) {
+ AssertNotWin32kLockdown();
+
+ NS_ENSURE_ARG_POINTER(aStatus);
+ aSuggestedDriverVersion.SetIsVoid(true);
+ OperatingSystem os = WindowsVersionToOperatingSystem(mWindowsVersion);
+ *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
+ if (aOS) *aOS = os;
+
+ if (sShutdownOccurred) {
+ return NS_OK;
+ }
+
+ // Don't evaluate special cases if we're checking the downloaded blocklist.
+ if (!aDriverInfo.Length()) {
+ nsAutoString adapterVendorID;
+ nsAutoString adapterDeviceID;
+ nsAutoString adapterDriverVersionString;
+ if (NS_FAILED(GetAdapterVendorID(adapterVendorID)) ||
+ NS_FAILED(GetAdapterDeviceID(adapterDeviceID)) ||
+ NS_FAILED(GetAdapterDriverVersion(adapterDriverVersionString))) {
+ if (OnlyAllowFeatureOnKnownConfig(aFeature)) {
+ aFailureId = "FEATURE_FAILURE_GET_ADAPTER";
+ *aStatus = FEATURE_BLOCKED_DEVICE;
+ } else {
+ *aStatus = FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ if (OnlyAllowFeatureOnKnownConfig(aFeature) &&
+ !adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::Intel),
+ nsCaseInsensitiveStringComparator) &&
+ !adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA),
+ nsCaseInsensitiveStringComparator) &&
+ !adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI),
+ nsCaseInsensitiveStringComparator) &&
+ !adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::Microsoft),
+ nsCaseInsensitiveStringComparator) &&
+ !adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::Parallels),
+ nsCaseInsensitiveStringComparator) &&
+ !adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::Qualcomm),
+ nsCaseInsensitiveStringComparator) &&
+ // FIXME - these special hex values are currently used in xpcshell tests
+ // introduced by bug 625160 patch 8/8. Maybe these tests need to be
+ // adjusted now that we're only whitelisting intel/ati/nvidia.
+ !adapterVendorID.LowerCaseEqualsLiteral("0xabcd") &&
+ !adapterVendorID.LowerCaseEqualsLiteral("0xdcba") &&
+ !adapterVendorID.LowerCaseEqualsLiteral("0xabab") &&
+ !adapterVendorID.LowerCaseEqualsLiteral("0xdcdc")) {
+ if (adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::MicrosoftHyperV),
+ nsCaseInsensitiveStringComparator) ||
+ adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::VMWare),
+ nsCaseInsensitiveStringComparator) ||
+ adapterVendorID.Equals(
+ GfxDriverInfo::GetDeviceVendor(DeviceVendor::VirtualBox),
+ nsCaseInsensitiveStringComparator)) {
+ aFailureId = "FEATURE_FAILURE_VM_VENDOR";
+ } else if (adapterVendorID.Equals(GfxDriverInfo::GetDeviceVendor(
+ DeviceVendor::MicrosoftBasic),
+ nsCaseInsensitiveStringComparator)) {
+ aFailureId = "FEATURE_FAILURE_MICROSOFT_BASIC_VENDOR";
+ } else if (adapterVendorID.IsEmpty()) {
+ aFailureId = "FEATURE_FAILURE_EMPTY_DEVICE_VENDOR";
+ } else {
+ aFailureId = "FEATURE_FAILURE_UNKNOWN_DEVICE_VENDOR";
+ }
+ *aStatus = FEATURE_BLOCKED_DEVICE;
+ return NS_OK;
+ }
+
+ if (adapterDriverVersionString.Length() == 0) {
+ if (OnlyAllowFeatureOnKnownConfig(aFeature)) {
+ aFailureId = "FEATURE_FAILURE_EMPTY_DRIVER_VERSION";
+ *aStatus = FEATURE_BLOCKED_DRIVER_VERSION;
+ } else {
+ *aStatus = FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+
+ uint64_t driverVersion;
+ if (!ParseDriverVersion(adapterDriverVersionString, &driverVersion)) {
+ if (OnlyAllowFeatureOnKnownConfig(aFeature)) {
+ aFailureId = "FEATURE_FAILURE_PARSE_DRIVER";
+ *aStatus = FEATURE_BLOCKED_DRIVER_VERSION;
+ } else {
+ *aStatus = FEATURE_STATUS_OK;
+ }
+ return NS_OK;
+ }
+ }
+
+ return GfxInfoBase::GetFeatureStatusImpl(
+ aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os);
+}
+
+void GfxInfo::DescribeFeatures(JSContext* aCx, JS::Handle<JSObject*> aObj) {
+ // Add the platform neutral features
+ GfxInfoBase::DescribeFeatures(aCx, aObj);
+
+ JS::Rooted<JSObject*> obj(aCx);
+
+ gfx::FeatureState& d3d11 = gfxConfig::GetFeature(Feature::D3D11_COMPOSITING);
+ if (!InitFeatureObject(aCx, aObj, "d3d11", d3d11, &obj)) {
+ return;
+ }
+ if (d3d11.GetValue() == gfx::FeatureStatus::Available) {
+ DeviceManagerDx* dm = DeviceManagerDx::Get();
+ JS::Rooted<JS::Value> val(aCx,
+ JS::Int32Value(dm->GetCompositorFeatureLevel()));
+ JS_SetProperty(aCx, obj, "version", val);
+
+ val = JS::BooleanValue(dm->IsWARP());
+ JS_SetProperty(aCx, obj, "warp", val);
+
+ val = JS::BooleanValue(dm->TextureSharingWorks());
+ JS_SetProperty(aCx, obj, "textureSharing", val);
+
+ bool blocklisted = false;
+ if (nsCOMPtr<nsIGfxInfo> gfxInfo = components::GfxInfo::Service()) {
+ int32_t status;
+ nsCString discardFailureId;
+ if (SUCCEEDED(
+ gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS,
+ discardFailureId, &status))) {
+ blocklisted = (status != nsIGfxInfo::FEATURE_STATUS_OK);
+ }
+ }
+
+ val = JS::BooleanValue(blocklisted);
+ JS_SetProperty(aCx, obj, "blocklisted", val);
+ }
+
+ gfx::FeatureState& d2d = gfxConfig::GetFeature(Feature::DIRECT2D);
+ if (!InitFeatureObject(aCx, aObj, "d2d", d2d, &obj)) {
+ return;
+ }
+ {
+ const char* version = "1.1";
+ JS::Rooted<JSString*> str(aCx, JS_NewStringCopyZ(aCx, version));
+ JS::Rooted<JS::Value> val(aCx, JS::StringValue(str));
+ JS_SetProperty(aCx, obj, "version", val);
+ }
+}
+
+#ifdef DEBUG
+
+// Implement nsIGfxInfoDebug
+
+NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) {
+ mAdapterVendorID[mActiveGPUIndex] = aVendorID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) {
+ mAdapterDeviceID[mActiveGPUIndex] = aDeviceID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) {
+ mDriverVersion[mActiveGPUIndex] = aDriverVersion;
+ return NS_OK;
+}
+
+NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) {
+ mWindowsVersion = aVersion;
+ return NS_OK;
+}
+
+#endif
diff --git a/widget/windows/GfxInfo.h b/widget/windows/GfxInfo.h
new file mode 100644
index 0000000000..f2549afa4e
--- /dev/null
+++ b/widget/windows/GfxInfo.h
@@ -0,0 +1,105 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef WIDGET_WINDOWS_GFXINFO_H_
+#define WIDGET_WINDOWS_GFXINFO_H_
+
+#include "GfxInfoBase.h"
+
+namespace mozilla::widget {
+
+class GfxInfo : public GfxInfoBase {
+ public:
+ using GfxInfoBase::GetFeatureStatus;
+ using GfxInfoBase::GetFeatureSuggestedDriverVersion;
+
+ GfxInfo() = default;
+ nsresult Init() override;
+
+ // We only declare the subset of nsIGfxInfo that we actually implement. The
+ // rest is brought forward from GfxInfoBase.
+ NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override;
+ NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override;
+ NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override;
+ NS_IMETHOD GetEmbeddedInFirefoxReality(
+ bool* aEmbeddedInFirefoxReality) override;
+ NS_IMETHOD GetHasBattery(bool* aHasBattery) override;
+ NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override;
+ NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override;
+ NS_IMETHOD GetTestType(nsAString& aTestType) override;
+ NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override;
+ NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override;
+ NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override;
+ NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override;
+ NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override;
+ NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override;
+ NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override;
+ NS_IMETHOD GetAdapterDriverVersion2(
+ nsAString& aAdapterDriverVersion) override;
+ NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override;
+ NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override;
+ NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override;
+
+ uint32_t OperatingSystemVersion() override { return mWindowsVersion; }
+ uint32_t OperatingSystemBuild() override { return mWindowsBuildNumber; }
+
+#ifdef DEBUG
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIGFXINFODEBUG
+#endif
+
+ private:
+ ~GfxInfo() = default;
+
+ // Disallow copy/move
+ GfxInfo(const GfxInfo&) = delete;
+ GfxInfo& operator=(const GfxInfo&) = delete;
+ GfxInfo(GfxInfo&&) = delete;
+ GfxInfo& operator=(GfxInfo&&) = delete;
+
+ OperatingSystem GetOperatingSystem() override;
+
+ nsresult GetFeatureStatusImpl(int32_t aFeature, int32_t* aStatus,
+ nsAString& aSuggestedDriverVersion,
+ const nsTArray<GfxDriverInfo>& aDriverInfo,
+ nsACString& aFailureId,
+ OperatingSystem* aOS = nullptr) override;
+ const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override;
+
+ void DescribeFeatures(JSContext* cx, JS::Handle<JSObject*> aOut) override;
+
+ void AddCrashReportAnnotations();
+
+ nsString mDeviceString[2];
+ nsString mDeviceID[2];
+ nsString mDriverVersion[2];
+ nsString mDriverDate[2];
+ nsString mDeviceKey[2];
+ nsString mDeviceKeyDebug;
+ nsString mAdapterVendorID[2];
+ nsString mAdapterDeviceID[2];
+ nsString mAdapterSubsysID[2];
+ uint32_t mWindowsVersion = 0;
+ uint32_t mWindowsBuildNumber = 0;
+ uint32_t mActiveGPUIndex = 0; // This must be 0 or 1
+ bool mHasDualGPU = false;
+ bool mHasDriverVersionMismatch = false;
+ bool mHasBattery = false;
+};
+
+} // namespace mozilla::widget
+
+#endif // WIDGET_WINDOWS_GFXINFO_H_
diff --git a/widget/windows/IEnumFE.cpp b/widget/windows/IEnumFE.cpp
new file mode 100644
index 0000000000..8fe7138609
--- /dev/null
+++ b/widget/windows/IEnumFE.cpp
@@ -0,0 +1,139 @@
+/* -*- 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 "IEnumFE.h"
+#include "nsAlgorithm.h"
+#include <algorithm>
+
+CEnumFormatEtc::CEnumFormatEtc() : mRefCnt(0), mCurrentIdx(0) {}
+
+// Constructor used by Clone()
+CEnumFormatEtc::CEnumFormatEtc(nsTArray<FormatEtc>& aArray)
+ : mRefCnt(0), mCurrentIdx(0) {
+ // a deep copy, calls FormatEtc's copy constructor on each
+ mFormatList.AppendElements(aArray);
+}
+
+CEnumFormatEtc::~CEnumFormatEtc() {}
+
+/* IUnknown impl. */
+
+STDMETHODIMP
+CEnumFormatEtc::QueryInterface(REFIID riid, LPVOID* ppv) {
+ *ppv = nullptr;
+
+ if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IEnumFORMATETC))
+ *ppv = (LPVOID)this;
+
+ if (*ppv == nullptr) return E_NOINTERFACE;
+
+ // AddRef any interface we'll return.
+ ((LPUNKNOWN)*ppv)->AddRef();
+ return S_OK;
+}
+
+STDMETHODIMP_(ULONG)
+CEnumFormatEtc::AddRef() {
+ ++mRefCnt;
+ NS_LOG_ADDREF(this, mRefCnt, "CEnumFormatEtc", sizeof(*this));
+ return mRefCnt;
+}
+
+STDMETHODIMP_(ULONG)
+CEnumFormatEtc::Release() {
+ uint32_t refReturn;
+
+ refReturn = --mRefCnt;
+ NS_LOG_RELEASE(this, mRefCnt, "CEnumFormatEtc");
+
+ if (mRefCnt == 0) delete this;
+
+ return refReturn;
+}
+
+/* IEnumFORMATETC impl. */
+
+STDMETHODIMP
+CEnumFormatEtc::Next(ULONG aMaxToFetch, FORMATETC* aResult,
+ ULONG* aNumFetched) {
+ // If the method retrieves the number of items requested, the return
+ // value is S_OK. Otherwise, it is S_FALSE.
+
+ if (aNumFetched) *aNumFetched = 0;
+
+ // aNumFetched can be null if aMaxToFetch is 1
+ if (!aNumFetched && aMaxToFetch > 1) return S_FALSE;
+
+ if (!aResult) return S_FALSE;
+
+ // We're done walking the list
+ if (mCurrentIdx >= mFormatList.Length()) return S_FALSE;
+
+ uint32_t left = mFormatList.Length() - mCurrentIdx;
+
+ if (!aMaxToFetch) return S_FALSE;
+
+ uint32_t count = std::min(static_cast<uint32_t>(aMaxToFetch), left);
+
+ uint32_t idx = 0;
+ while (count > 0) {
+ // Copy out to aResult
+ mFormatList[mCurrentIdx++].CopyOut(&aResult[idx++]);
+ count--;
+ }
+
+ if (aNumFetched) *aNumFetched = idx;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+CEnumFormatEtc::Skip(ULONG aSkipNum) {
+ // If the method skips the number of items requested, the return value is
+ // S_OK. Otherwise, it is S_FALSE.
+
+ if ((mCurrentIdx + aSkipNum) >= mFormatList.Length()) return S_FALSE;
+
+ mCurrentIdx += aSkipNum;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+CEnumFormatEtc::Reset(void) {
+ mCurrentIdx = 0;
+ return S_OK;
+}
+
+STDMETHODIMP
+CEnumFormatEtc::Clone(LPENUMFORMATETC* aResult) {
+ // Must return a new IEnumFORMATETC interface with the same iterative state.
+
+ if (!aResult) return E_INVALIDARG;
+
+ CEnumFormatEtc* pEnumObj = new CEnumFormatEtc(mFormatList);
+
+ if (!pEnumObj) return E_OUTOFMEMORY;
+
+ pEnumObj->AddRef();
+ pEnumObj->SetIndex(mCurrentIdx);
+
+ *aResult = pEnumObj;
+
+ return S_OK;
+}
+
+/* utils */
+
+void CEnumFormatEtc::AddFormatEtc(LPFORMATETC aFormat) {
+ if (!aFormat) return;
+ FormatEtc* etc = mFormatList.AppendElement();
+ // Make a copy of aFormat
+ if (etc) etc->CopyIn(aFormat);
+}
+
+/* private */
+
+void CEnumFormatEtc::SetIndex(uint32_t aIdx) { mCurrentIdx = aIdx; }
diff --git a/widget/windows/IEnumFE.h b/widget/windows/IEnumFE.h
new file mode 100644
index 0000000000..b8cb6ad9d0
--- /dev/null
+++ b/widget/windows/IEnumFE.h
@@ -0,0 +1,88 @@
+/* -*- 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/. */
+
+#ifndef IEnumeFE_h__
+#define IEnumeFE_h__
+
+/*
+ * CEnumFormatEtc - implements IEnumFORMATETC
+ */
+
+#include <ole2.h>
+
+#include "nsTArray.h"
+#include "mozilla/Attributes.h"
+
+// FORMATETC container
+class FormatEtc {
+ public:
+ FormatEtc() { memset(&mFormat, 0, sizeof(FORMATETC)); }
+ FormatEtc(const FormatEtc& copy) { CopyIn(&copy.mFormat); }
+ ~FormatEtc() {
+ if (mFormat.ptd) CoTaskMemFree(mFormat.ptd);
+ }
+
+ void CopyIn(const FORMATETC* aSrc) {
+ if (!aSrc) {
+ memset(&mFormat, 0, sizeof(FORMATETC));
+ return;
+ }
+ mFormat = *aSrc;
+ if (aSrc->ptd) {
+ mFormat.ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE));
+ *(mFormat.ptd) = *(aSrc->ptd);
+ }
+ }
+
+ void CopyOut(LPFORMATETC aDest) {
+ if (!aDest) return;
+ *aDest = mFormat;
+ if (mFormat.ptd) {
+ aDest->ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE));
+ *(aDest->ptd) = *(mFormat.ptd);
+ }
+ }
+
+ private:
+ FORMATETC mFormat;
+};
+
+/*
+ * CEnumFormatEtc is created within IDataObject::EnumFormatEtc. This object
+ * lives on its own, that is, QueryInterface only knows IUnknown and
+ * IEnumFormatEtc, nothing more. We still use an outer unknown for reference
+ * counting, because as long as this enumerator lives, the data object should
+ * live, thereby keeping the application up.
+ */
+
+class CEnumFormatEtc final : public IEnumFORMATETC {
+ public:
+ explicit CEnumFormatEtc(nsTArray<FormatEtc>& aArray);
+ CEnumFormatEtc();
+ ~CEnumFormatEtc();
+
+ // IUnknown impl.
+ STDMETHODIMP QueryInterface(REFIID riid, LPVOID* ppv);
+ STDMETHODIMP_(ULONG) AddRef();
+ STDMETHODIMP_(ULONG) Release();
+
+ // IEnumFORMATETC impl.
+ STDMETHODIMP Next(ULONG aMaxToFetch, FORMATETC* aResult, ULONG* aNumFetched);
+ STDMETHODIMP Skip(ULONG aSkipNum);
+ STDMETHODIMP Reset();
+ STDMETHODIMP Clone(LPENUMFORMATETC* aResult); // Addrefs
+
+ // Utils
+ void AddFormatEtc(LPFORMATETC aFormat);
+
+ private:
+ nsTArray<FormatEtc> mFormatList; // Formats
+ ULONG mRefCnt; // Object reference count
+ ULONG mCurrentIdx; // Current element
+
+ void SetIndex(uint32_t aIdx);
+};
+
+#endif //_IENUMFE_H_
diff --git a/widget/windows/IMMHandler.cpp b/widget/windows/IMMHandler.cpp
new file mode 100644
index 0000000000..2f64f93922
--- /dev/null
+++ b/widget/windows/IMMHandler.cpp
@@ -0,0 +1,2408 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 "mozilla/Logging.h"
+
+#include "IMMHandler.h"
+#include "nsWindow.h"
+#include "nsWindowDefs.h"
+#include "WinIMEHandler.h"
+#include "WinUtils.h"
+#include "KeyboardLayout.h"
+#include <algorithm>
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
+
+#ifndef IME_PROP_ACCEPT_WIDE_VKEY
+# define IME_PROP_ACCEPT_WIDE_VKEY 0x20
+#endif
+
+//-------------------------------------------------------------------------
+//
+// from
+// http://download.microsoft.com/download/6/0/9/60908e9e-d2c1-47db-98f6-216af76a235f/msime.h
+// The document for this has been removed from MSDN...
+//
+//-------------------------------------------------------------------------
+
+#define RWM_MOUSE TEXT("MSIMEMouseOperation")
+
+#define IMEMOUSE_NONE 0x00 // no mouse button was pushed
+#define IMEMOUSE_LDOWN 0x01
+#define IMEMOUSE_RDOWN 0x02
+#define IMEMOUSE_MDOWN 0x04
+#define IMEMOUSE_WUP 0x10 // wheel up
+#define IMEMOUSE_WDOWN 0x20 // wheel down
+
+// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync`
+// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+extern mozilla::LazyLogModule gIMELog;
+
+static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; }
+
+static void HandleSeparator(nsACString& aDesc) {
+ if (!aDesc.IsEmpty()) {
+ aDesc.AppendLiteral(" | ");
+ }
+}
+
+class GetIMEGeneralPropertyName : public nsAutoCString {
+ public:
+ explicit GetIMEGeneralPropertyName(DWORD aFlags) {
+ if (!aFlags) {
+ AppendLiteral("no flags");
+ return;
+ }
+ if (aFlags & IME_PROP_AT_CARET) {
+ AppendLiteral("IME_PROP_AT_CARET");
+ }
+ if (aFlags & IME_PROP_SPECIAL_UI) {
+ HandleSeparator(*this);
+ AppendLiteral("IME_PROP_SPECIAL_UI");
+ }
+ if (aFlags & IME_PROP_CANDLIST_START_FROM_1) {
+ HandleSeparator(*this);
+ AppendLiteral("IME_PROP_CANDLIST_START_FROM_1");
+ }
+ if (aFlags & IME_PROP_UNICODE) {
+ HandleSeparator(*this);
+ AppendLiteral("IME_PROP_UNICODE");
+ }
+ if (aFlags & IME_PROP_COMPLETE_ON_UNSELECT) {
+ HandleSeparator(*this);
+ AppendLiteral("IME_PROP_COMPLETE_ON_UNSELECT");
+ }
+ if (aFlags & IME_PROP_ACCEPT_WIDE_VKEY) {
+ HandleSeparator(*this);
+ AppendLiteral("IME_PROP_ACCEPT_WIDE_VKEY");
+ }
+ }
+ virtual ~GetIMEGeneralPropertyName() {}
+};
+
+class GetIMEUIPropertyName : public nsAutoCString {
+ public:
+ explicit GetIMEUIPropertyName(DWORD aFlags) {
+ if (!aFlags) {
+ AppendLiteral("no flags");
+ return;
+ }
+ if (aFlags & UI_CAP_2700) {
+ AppendLiteral("UI_CAP_2700");
+ }
+ if (aFlags & UI_CAP_ROT90) {
+ HandleSeparator(*this);
+ AppendLiteral("UI_CAP_ROT90");
+ }
+ if (aFlags & UI_CAP_ROTANY) {
+ HandleSeparator(*this);
+ AppendLiteral("UI_CAP_ROTANY");
+ }
+ }
+ virtual ~GetIMEUIPropertyName() {}
+};
+
+class GetReconvertStringLog : public nsAutoCString {
+ public:
+ explicit GetReconvertStringLog(RECONVERTSTRING* aReconv) {
+ AssignLiteral("{ dwSize=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwSize));
+ AppendLiteral(", dwVersion=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwVersion));
+ AppendLiteral(", dwStrLen=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwStrLen));
+ AppendLiteral(", dwStrOffset=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwStrOffset));
+ AppendLiteral(", dwCompStrLen=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwCompStrLen));
+ AppendLiteral(", dwCompStrOffset=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwCompStrOffset));
+ AppendLiteral(", dwTargetStrLen=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwTargetStrLen));
+ AppendLiteral(", dwTargetStrOffset=");
+ AppendInt(static_cast<uint32_t>(aReconv->dwTargetStrOffset));
+ AppendLiteral(", result str=\"");
+ if (aReconv->dwStrLen) {
+ char16_t* strStart = reinterpret_cast<char16_t*>(
+ reinterpret_cast<char*>(aReconv) + aReconv->dwStrOffset);
+ nsDependentString str(strStart, aReconv->dwStrLen);
+ Append(NS_ConvertUTF16toUTF8(str));
+ }
+ AppendLiteral("\" }");
+ }
+ virtual ~GetReconvertStringLog() {}
+};
+
+namespace mozilla {
+namespace widget {
+
+static IMMHandler* gIMMHandler = nullptr;
+
+/******************************************************************************
+ * IMEContext
+ ******************************************************************************/
+
+IMEContext::IMEContext(HWND aWnd) : mWnd(aWnd), mIMC(::ImmGetContext(aWnd)) {}
+
+IMEContext::IMEContext(nsWindow* aWindowBase)
+ : mWnd(aWindowBase->GetWindowHandle()),
+ mIMC(::ImmGetContext(aWindowBase->GetWindowHandle())) {}
+
+void IMEContext::Init(HWND aWnd) {
+ Clear();
+ mWnd = aWnd;
+ mIMC = ::ImmGetContext(mWnd);
+}
+
+void IMEContext::Init(nsWindow* aWindowBase) {
+ Init(aWindowBase->GetWindowHandle());
+}
+
+void IMEContext::Clear() {
+ if (mWnd && mIMC) {
+ ::ImmReleaseContext(mWnd, mIMC);
+ }
+ mWnd = nullptr;
+ mIMC = nullptr;
+}
+
+/******************************************************************************
+ * IMMHandler
+ ******************************************************************************/
+
+static UINT sWM_MSIME_MOUSE = 0; // mouse message for MSIME 98/2000
+
+WritingMode IMMHandler::sWritingModeOfCompositionFont;
+nsString IMMHandler::sIMEName;
+UINT IMMHandler::sCodePage = 0;
+DWORD IMMHandler::sIMEProperty = 0;
+DWORD IMMHandler::sIMEUIProperty = 0;
+bool IMMHandler::sAssumeVerticalWritingModeNotSupported = false;
+bool IMMHandler::sHasFocus = false;
+
+#define IMPL_IS_IME_ACTIVE(aReadableName, aActualName) \
+ bool IMMHandler::Is##aReadableName##Active() { \
+ return sIMEName.Equals(aActualName); \
+ }
+
+IMPL_IS_IME_ACTIVE(ATOK2006, u"ATOK 2006")
+IMPL_IS_IME_ACTIVE(ATOK2007, u"ATOK 2007")
+IMPL_IS_IME_ACTIVE(ATOK2008, u"ATOK 2008")
+IMPL_IS_IME_ACTIVE(ATOK2009, u"ATOK 2009")
+IMPL_IS_IME_ACTIVE(ATOK2010, u"ATOK 2010")
+// NOTE: Even on Windows for en-US, the name of Google Japanese Input is
+// written in Japanese.
+IMPL_IS_IME_ACTIVE(GoogleJapaneseInput,
+ u"Google \x65E5\x672C\x8A9E\x5165\x529B "
+ u"IMM32 \x30E2\x30B8\x30E5\x30FC\x30EB")
+IMPL_IS_IME_ACTIVE(Japanist2003, u"Japanist 2003")
+
+#undef IMPL_IS_IME_ACTIVE
+
+// static
+bool IMMHandler::IsActiveIMEInBlockList() {
+ if (sIMEName.IsEmpty()) {
+ return false;
+ }
+#ifdef _WIN64
+ // ATOK started to be TIP of TSF since 2011. Older than it, i.e., ATOK 2010
+ // and earlier have a lot of problems even for daily use. Perhaps, the
+ // reason is Win 8 has a lot of changes around IMM-IME support and TSF,
+ // and ATOK 2010 is released earlier than Win 8.
+ // ATOK 2006 crashes while converting a word with candidate window.
+ // ATOK 2007 doesn't paint and resize suggest window and candidate window
+ // correctly (showing white window or too big window).
+ // ATOK 2008 and ATOK 2009 crash when user just opens their open state.
+ // ATOK 2010 isn't installable newly on Win 7 or later, but we have a lot of
+ // crash reports.
+ if ((IsATOK2006Active() || IsATOK2007Active() || IsATOK2008Active() ||
+ IsATOK2009Active() || IsATOK2010Active())) {
+ return true;
+ }
+#endif // #ifdef _WIN64
+ return false;
+}
+
+// static
+void IMMHandler::EnsureHandlerInstance() {
+ if (!gIMMHandler) {
+ gIMMHandler = new IMMHandler();
+ }
+}
+
+// static
+void IMMHandler::Initialize() {
+ if (!sWM_MSIME_MOUSE) {
+ sWM_MSIME_MOUSE = ::RegisterWindowMessage(RWM_MOUSE);
+ }
+ sAssumeVerticalWritingModeNotSupported = Preferences::GetBool(
+ "intl.imm.vertical_writing.always_assume_not_supported", false);
+ InitKeyboardLayout(nullptr, ::GetKeyboardLayout(0));
+}
+
+// static
+void IMMHandler::Terminate() {
+ if (!gIMMHandler) return;
+ delete gIMMHandler;
+ gIMMHandler = nullptr;
+}
+
+// static
+bool IMMHandler::IsComposingOnOurEditor() {
+ return gIMMHandler && gIMMHandler->mIsComposing;
+}
+
+// static
+bool IMMHandler::IsComposingWindow(nsWindow* aWindow) {
+ return gIMMHandler && gIMMHandler->mComposingWindow == aWindow;
+}
+
+// static
+bool IMMHandler::IsTopLevelWindowOfComposition(nsWindow* aWindow) {
+ if (!gIMMHandler || !gIMMHandler->mComposingWindow) {
+ return false;
+ }
+ HWND wnd = gIMMHandler->mComposingWindow->GetWindowHandle();
+ return WinUtils::GetTopLevelHWND(wnd, true) == aWindow->GetWindowHandle();
+}
+
+// static
+bool IMMHandler::ShouldDrawCompositionStringOurselves() {
+ // If current IME has special UI or its composition window should not
+ // positioned to caret position, we should now draw composition string
+ // ourselves.
+ return !(sIMEProperty & IME_PROP_SPECIAL_UI) &&
+ (sIMEProperty & IME_PROP_AT_CARET);
+}
+
+// static
+bool IMMHandler::IsVerticalWritingSupported() {
+ // Even if IME claims that they support vertical writing mode but it may not
+ // support vertical writing mode for its candidate window.
+ if (sAssumeVerticalWritingModeNotSupported) {
+ return false;
+ }
+ // Google Japanese Input doesn't support vertical writing mode. We should
+ // return false if it's active IME.
+ if (IsGoogleJapaneseInputActive()) {
+ return false;
+ }
+ return !!(sIMEUIProperty & (UI_CAP_2700 | UI_CAP_ROT90 | UI_CAP_ROTANY));
+}
+
+// static
+void IMMHandler::InitKeyboardLayout(nsWindow* aWindow, HKL aKeyboardLayout) {
+ UINT IMENameLength = ::ImmGetDescriptionW(aKeyboardLayout, nullptr, 0);
+ if (IMENameLength) {
+ // Add room for the terminating null character
+ sIMEName.SetLength(++IMENameLength);
+ IMENameLength =
+ ::ImmGetDescriptionW(aKeyboardLayout, sIMEName.get(), IMENameLength);
+ // Adjust the length to ignore the terminating null character
+ sIMEName.SetLength(IMENameLength);
+ } else {
+ sIMEName.Truncate();
+ }
+
+ WORD langID = LOWORD(aKeyboardLayout);
+ ::GetLocaleInfoW(MAKELCID(langID, SORT_DEFAULT),
+ LOCALE_IDEFAULTANSICODEPAGE | LOCALE_RETURN_NUMBER,
+ (PWSTR)&sCodePage, sizeof(sCodePage) / sizeof(WCHAR));
+ sIMEProperty = ::ImmGetProperty(aKeyboardLayout, IGP_PROPERTY);
+ sIMEUIProperty = ::ImmGetProperty(aKeyboardLayout, IGP_UI);
+
+ // If active IME is a TIP of TSF, we cannot retrieve the name with IMM32 API.
+ // For hacking some bugs of some TIP, we should set an IME name from the
+ // pref.
+ if (sCodePage == 932 && sIMEName.IsEmpty()) {
+ Preferences::GetString("intl.imm.japanese.assume_active_tip_name_as",
+ sIMEName);
+ }
+
+ // Whether the IME supports vertical writing mode might be changed or
+ // some IMEs may need specific font for their UI. Therefore, we should
+ // update composition font forcibly here.
+ if (aWindow) {
+ MaybeAdjustCompositionFont(aWindow, sWritingModeOfCompositionFont, true);
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::InitKeyboardLayout, aKeyboardLayout=%p (\"%s\"), "
+ "sCodePage=%u, sIMEProperty=%s, sIMEUIProperty=%s",
+ aKeyboardLayout, NS_ConvertUTF16toUTF8(sIMEName).get(), sCodePage,
+ GetIMEGeneralPropertyName(sIMEProperty).get(),
+ GetIMEUIPropertyName(sIMEUIProperty).get()));
+}
+
+// static
+UINT IMMHandler::GetKeyboardCodePage() { return sCodePage; }
+
+// static
+IMENotificationRequests IMMHandler::GetIMENotificationRequests() {
+ return IMENotificationRequests(
+ IMENotificationRequests::NOTIFY_POSITION_CHANGE |
+ IMENotificationRequests::NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR);
+}
+
+// used for checking the lParam of WM_IME_COMPOSITION
+#define IS_COMPOSING_LPARAM(lParam) \
+ ((lParam) & (GCS_COMPSTR | GCS_COMPATTR | GCS_COMPCLAUSE | GCS_CURSORPOS))
+#define IS_COMMITTING_LPARAM(lParam) ((lParam) & GCS_RESULTSTR)
+// Some IMEs (e.g., the standard IME for Korean) don't have caret position,
+// then, we should not set caret position to compositionchange event.
+#define NO_IME_CARET -1
+
+IMMHandler::IMMHandler()
+ : mComposingWindow(nullptr),
+ mCursorPosition(NO_IME_CARET),
+ mCompositionStart(0),
+ mIsComposing(false) {
+ MOZ_LOG(gIMELog, LogLevel::Debug, ("IMMHandler::IMMHandler is created"));
+}
+
+IMMHandler::~IMMHandler() {
+ if (mIsComposing) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ (" IMMHandler::~IMMHandler, ERROR, the instance is still composing"));
+ }
+ MOZ_LOG(gIMELog, LogLevel::Debug, ("IMMHandler::IMMHandler is destroyed"));
+}
+
+nsresult IMMHandler::EnsureClauseArray(int32_t aCount) {
+ NS_ENSURE_ARG_MIN(aCount, 0);
+ mClauseArray.SetCapacity(aCount + 32);
+ return NS_OK;
+}
+
+nsresult IMMHandler::EnsureAttributeArray(int32_t aCount) {
+ NS_ENSURE_ARG_MIN(aCount, 0);
+ mAttributeArray.SetCapacity(aCount + 64);
+ return NS_OK;
+}
+
+// static
+void IMMHandler::CommitComposition(nsWindow* aWindow, bool aForce) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::CommitComposition, aForce=%s, aWindow=%p, hWnd=%p, "
+ "mComposingWindow=%p%s",
+ GetBoolName(aForce), aWindow, aWindow->GetWindowHandle(),
+ gIMMHandler ? gIMMHandler->mComposingWindow : nullptr,
+ gIMMHandler && gIMMHandler->mComposingWindow
+ ? IsComposingOnOurEditor() ? " (composing on editor)"
+ : " (composing on plug-in)"
+ : ""));
+ if (!aForce && !IsComposingWindow(aWindow)) {
+ return;
+ }
+
+ IMEContext context(aWindow);
+ bool associated = context.AssociateDefaultContext();
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::CommitComposition, associated=%s",
+ GetBoolName(associated)));
+
+ if (context.IsValid()) {
+ ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
+ ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0);
+ }
+
+ if (associated) {
+ context.Disassociate();
+ }
+}
+
+// static
+void IMMHandler::CancelComposition(nsWindow* aWindow, bool aForce) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::CancelComposition, aForce=%s, aWindow=%p, hWnd=%p, "
+ "mComposingWindow=%p%s",
+ GetBoolName(aForce), aWindow, aWindow->GetWindowHandle(),
+ gIMMHandler ? gIMMHandler->mComposingWindow : nullptr,
+ gIMMHandler && gIMMHandler->mComposingWindow
+ ? IsComposingOnOurEditor() ? " (composing on editor)"
+ : " (composing on plug-in)"
+ : ""));
+ if (!aForce && !IsComposingWindow(aWindow)) {
+ return;
+ }
+
+ IMEContext context(aWindow);
+ bool associated = context.AssociateDefaultContext();
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::CancelComposition, associated=%s",
+ GetBoolName(associated)));
+
+ if (context.IsValid()) {
+ ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0);
+ }
+
+ if (associated) {
+ context.Disassociate();
+ }
+}
+
+// static
+void IMMHandler::OnFocusChange(bool aFocus, nsWindow* aWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnFocusChange(aFocus=%s, aWindow=%p), sHasFocus=%s, "
+ "IsComposingWindow(aWindow)=%s, aWindow->Destroyed()=%s",
+ GetBoolName(aFocus), aWindow, GetBoolName(sHasFocus),
+ GetBoolName(IsComposingWindow(aWindow)),
+ GetBoolName(aWindow->Destroyed())));
+
+ if (!aFocus) {
+ IMEHandler::MaybeDestroyNativeCaret();
+ if (IsComposingWindow(aWindow) && aWindow->Destroyed()) {
+ CancelComposition(aWindow);
+ }
+ }
+ if (gIMMHandler) {
+ gIMMHandler->mContentSelection.reset();
+ }
+ sHasFocus = aFocus;
+}
+
+// static
+void IMMHandler::OnUpdateComposition(nsWindow* aWindow) {
+ if (!gIMMHandler) {
+ return;
+ }
+
+ IMEContext context(aWindow);
+ gIMMHandler->SetIMERelatedWindowsPos(aWindow, context);
+}
+
+// static
+void IMMHandler::OnSelectionChange(nsWindow* aWindow,
+ const IMENotification& aIMENotification,
+ bool aIsIMMActive) {
+ if (!aIMENotification.mSelectionChangeData.mCausedByComposition &&
+ aIsIMMActive) {
+ MaybeAdjustCompositionFont(
+ aWindow, aIMENotification.mSelectionChangeData.GetWritingMode());
+ }
+ // MaybeAdjustCompositionFont() may create gIMMHandler. So, check it
+ // after a call of MaybeAdjustCompositionFont().
+ if (gIMMHandler) {
+ gIMMHandler->mContentSelection =
+ Some(ContentSelection(aIMENotification.mSelectionChangeData));
+ }
+}
+
+// static
+void IMMHandler::MaybeAdjustCompositionFont(nsWindow* aWindow,
+ const WritingMode& aWritingMode,
+ bool aForceUpdate) {
+ switch (sCodePage) {
+ case 932: // Japanese Shift-JIS
+ case 936: // Simlified Chinese GBK
+ case 949: // Korean
+ case 950: // Traditional Chinese Big5
+ EnsureHandlerInstance();
+ break;
+ default:
+ // If there is no instance of nsIMM32Hander, we shouldn't waste footprint.
+ if (!gIMMHandler) {
+ return;
+ }
+ }
+
+ // Like Navi-Bar of ATOK, some IMEs may require proper composition font even
+ // before sending WM_IME_STARTCOMPOSITION.
+ IMEContext context(aWindow);
+ gIMMHandler->AdjustCompositionFont(aWindow, context, aWritingMode,
+ aForceUpdate);
+}
+
+// static
+bool IMMHandler::ProcessInputLangChangeMessage(nsWindow* aWindow, WPARAM wParam,
+ LPARAM lParam,
+ MSGResult& aResult) {
+ aResult.mResult = 0;
+ aResult.mConsumed = false;
+ // We don't need to create the instance of the handler here.
+ if (gIMMHandler) {
+ gIMMHandler->OnInputLangChange(aWindow, wParam, lParam, aResult);
+ }
+ InitKeyboardLayout(aWindow, reinterpret_cast<HKL>(lParam));
+ // We can release the instance here, because the instance may be never
+ // used. E.g., the new keyboard layout may not use IME, or it may use TSF.
+ Terminate();
+ // Don't return as "processed", the messages should be processed on nsWindow
+ // too.
+ return false;
+}
+
+// static
+bool IMMHandler::ProcessMessage(nsWindow* aWindow, UINT msg, WPARAM& wParam,
+ LPARAM& lParam, MSGResult& aResult) {
+ // XXX We store the composing window in mComposingWindow. If IME messages are
+ // sent to different window, we should commit the old transaction. And also
+ // if the new window handle is not focused, probably, we should not start
+ // the composition, however, such case should not be, it's just bad scenario.
+
+ aResult.mResult = 0;
+ switch (msg) {
+ case WM_INPUTLANGCHANGE:
+ return ProcessInputLangChangeMessage(aWindow, wParam, lParam, aResult);
+ case WM_IME_STARTCOMPOSITION:
+ EnsureHandlerInstance();
+ return gIMMHandler->OnIMEStartComposition(aWindow, aResult);
+ case WM_IME_COMPOSITION:
+ EnsureHandlerInstance();
+ return gIMMHandler->OnIMEComposition(aWindow, wParam, lParam, aResult);
+ case WM_IME_ENDCOMPOSITION:
+ EnsureHandlerInstance();
+ return gIMMHandler->OnIMEEndComposition(aWindow, aResult);
+ case WM_IME_CHAR:
+ return OnIMEChar(aWindow, wParam, lParam, aResult);
+ case WM_IME_NOTIFY:
+ return OnIMENotify(aWindow, wParam, lParam, aResult);
+ case WM_IME_REQUEST:
+ EnsureHandlerInstance();
+ return gIMMHandler->OnIMERequest(aWindow, wParam, lParam, aResult);
+ case WM_IME_SELECT:
+ return OnIMESelect(aWindow, wParam, lParam, aResult);
+ case WM_IME_SETCONTEXT:
+ return OnIMESetContext(aWindow, wParam, lParam, aResult);
+ case WM_KEYDOWN:
+ return OnKeyDownEvent(aWindow, wParam, lParam, aResult);
+ case WM_CHAR:
+ if (!gIMMHandler) {
+ return false;
+ }
+ return gIMMHandler->OnChar(aWindow, wParam, lParam, aResult);
+ default:
+ return false;
+ };
+}
+
+/****************************************************************************
+ * message handlers
+ ****************************************************************************/
+
+void IMMHandler::OnInputLangChange(nsWindow* aWindow, WPARAM wParam,
+ LPARAM lParam, MSGResult& aResult) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnInputLangChange, hWnd=%p, wParam=%08zx, "
+ "lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), wParam, lParam));
+
+ aWindow->NotifyIME(REQUEST_TO_COMMIT_COMPOSITION);
+ NS_ASSERTION(!mIsComposing, "ResetInputState failed");
+
+ if (mIsComposing) {
+ HandleEndComposition(aWindow);
+ }
+
+ aResult.mConsumed = false;
+}
+
+bool IMMHandler::OnIMEStartComposition(nsWindow* aWindow, MSGResult& aResult) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMEStartComposition, hWnd=%p, mIsComposing=%s",
+ aWindow->GetWindowHandle(), GetBoolName(mIsComposing)));
+ aResult.mConsumed = ShouldDrawCompositionStringOurselves();
+ if (mIsComposing) {
+ NS_WARNING("Composition has been already started");
+ return true;
+ }
+
+ IMEContext context(aWindow);
+ HandleStartComposition(aWindow, context);
+ return true;
+}
+
+bool IMMHandler::OnIMEComposition(nsWindow* aWindow, WPARAM wParam,
+ LPARAM lParam, MSGResult& aResult) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMEComposition, hWnd=%p, lParam=%08" PRIxLPTR
+ ", mIsComposing=%s, "
+ "GCS_RESULTSTR=%s, GCS_COMPSTR=%s, GCS_COMPATTR=%s, GCS_COMPCLAUSE=%s, "
+ "GCS_CURSORPOS=%s,",
+ aWindow->GetWindowHandle(), lParam, GetBoolName(mIsComposing),
+ GetBoolName(lParam & GCS_RESULTSTR), GetBoolName(lParam & GCS_COMPSTR),
+ GetBoolName(lParam & GCS_COMPATTR), GetBoolName(lParam & GCS_COMPCLAUSE),
+ GetBoolName(lParam & GCS_CURSORPOS)));
+
+ IMEContext context(aWindow);
+ aResult.mConsumed = HandleComposition(aWindow, context, lParam);
+ return true;
+}
+
+bool IMMHandler::OnIMEEndComposition(nsWindow* aWindow, MSGResult& aResult) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMEEndComposition, hWnd=%p, mIsComposing=%s",
+ aWindow->GetWindowHandle(), GetBoolName(mIsComposing)));
+
+ aResult.mConsumed = ShouldDrawCompositionStringOurselves();
+ if (!mIsComposing) {
+ return true;
+ }
+
+ // Korean IME posts WM_IME_ENDCOMPOSITION first when we hit space during
+ // composition. Then, we should ignore the message and commit the composition
+ // string at following WM_IME_COMPOSITION.
+ MSG compositionMsg;
+ if (WinUtils::PeekMessage(&compositionMsg, aWindow->GetWindowHandle(),
+ WM_IME_STARTCOMPOSITION, WM_IME_COMPOSITION,
+ PM_NOREMOVE) &&
+ compositionMsg.message == WM_IME_COMPOSITION &&
+ IS_COMMITTING_LPARAM(compositionMsg.lParam)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::OnIMEEndComposition, WM_IME_ENDCOMPOSITION is "
+ "followed by WM_IME_COMPOSITION, ignoring the message..."));
+ return true;
+ }
+
+ // Otherwise, e.g., ChangJie doesn't post WM_IME_COMPOSITION before
+ // WM_IME_ENDCOMPOSITION when composition string becomes empty.
+ // Then, we should dispatch a compositionupdate event, a compositionchange
+ // event and a compositionend event.
+ // XXX Shouldn't we dispatch the compositionchange event with actual or
+ // latest composition string?
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::OnIMEEndComposition, mCompositionString=\"%s\"%s",
+ NS_ConvertUTF16toUTF8(mCompositionString).get(),
+ mCompositionString.IsEmpty() ? "" : ", but canceling it..."));
+
+ HandleEndComposition(aWindow, &EmptyString());
+
+ return true;
+}
+
+// static
+bool IMMHandler::OnIMEChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMEChar, hWnd=%p, char=%08zx",
+ aWindow->GetWindowHandle(), wParam));
+
+ // We don't need to fire any compositionchange events from here. This method
+ // will be called when the composition string of the current IME is not drawn
+ // by us and some characters are committed. In that case, the committed
+ // string was processed in nsWindow::OnIMEComposition already.
+
+ // We need to consume the message so that Windows don't send two WM_CHAR msgs
+ aResult.mConsumed = true;
+ return true;
+}
+
+// static
+bool IMMHandler::OnIMECompositionFull(nsWindow* aWindow, MSGResult& aResult) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMECompositionFull, hWnd=%p",
+ aWindow->GetWindowHandle()));
+
+ // not implement yet
+ aResult.mConsumed = false;
+ return true;
+}
+
+// static
+bool IMMHandler::OnIMENotify(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ switch (wParam) {
+ case IMN_CHANGECANDIDATE:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CHANGECANDIDATE, "
+ "lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), lParam));
+ break;
+ case IMN_CLOSECANDIDATE:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CLOSECANDIDATE, "
+ "lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), lParam));
+ break;
+ case IMN_CLOSESTATUSWINDOW:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CLOSESTATUSWINDOW",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_GUIDELINE:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_GUIDELINE",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_OPENCANDIDATE:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_OPENCANDIDATE, "
+ "lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), lParam));
+ break;
+ case IMN_OPENSTATUSWINDOW:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_OPENSTATUSWINDOW",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_SETCANDIDATEPOS:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCANDIDATEPOS, "
+ "lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), lParam));
+ break;
+ case IMN_SETCOMPOSITIONFONT:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCOMPOSITIONFONT",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_SETCOMPOSITIONWINDOW:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCOMPOSITIONWINDOW",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_SETCONVERSIONMODE:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCONVERSIONMODE",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_SETOPENSTATUS:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETOPENSTATUS",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_SETSENTENCEMODE:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETSENTENCEMODE",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_SETSTATUSWINDOWPOS:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETSTATUSWINDOWPOS",
+ aWindow->GetWindowHandle()));
+ break;
+ case IMN_PRIVATE:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMENotify, hWnd=%p, IMN_PRIVATE",
+ aWindow->GetWindowHandle()));
+ break;
+ }
+
+ // not implement yet
+ aResult.mConsumed = false;
+ return true;
+}
+
+bool IMMHandler::OnIMERequest(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ switch (wParam) {
+ case IMR_RECONVERTSTRING:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMERequest, hWnd=%p, IMR_RECONVERTSTRING",
+ aWindow->GetWindowHandle()));
+ aResult.mConsumed = HandleReconvert(aWindow, lParam, &aResult.mResult);
+ return true;
+ case IMR_QUERYCHARPOSITION:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMERequest, hWnd=%p, IMR_QUERYCHARPOSITION",
+ aWindow->GetWindowHandle()));
+ aResult.mConsumed =
+ HandleQueryCharPosition(aWindow, lParam, &aResult.mResult);
+ return true;
+ case IMR_DOCUMENTFEED:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMERequest, hWnd=%p, IMR_DOCUMENTFEED",
+ aWindow->GetWindowHandle()));
+ aResult.mConsumed = HandleDocumentFeed(aWindow, lParam, &aResult.mResult);
+ return true;
+ default:
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMERequest, hWnd=%p, wParam=%08zx",
+ aWindow->GetWindowHandle(), wParam));
+ aResult.mConsumed = false;
+ return true;
+ }
+}
+
+// static
+bool IMMHandler::OnIMESelect(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMESelect, hWnd=%p, wParam=%08zx, lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), wParam, lParam));
+
+ // not implement yet
+ aResult.mConsumed = false;
+ return true;
+}
+
+// static
+bool IMMHandler::OnIMESetContext(nsWindow* aWindow, WPARAM wParam,
+ LPARAM lParam, MSGResult& aResult) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnIMESetContext, hWnd=%p, %s, lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), wParam ? "Active" : "Deactive", lParam));
+
+ aResult.mConsumed = false;
+
+ // NOTE: If the aWindow is top level window of the composing window because
+ // when a window on deactive window gets focus, WM_IME_SETCONTEXT (wParam is
+ // TRUE) is sent to the top level window first. After that,
+ // WM_IME_SETCONTEXT (wParam is FALSE) is sent to the top level window.
+ // Finally, WM_IME_SETCONTEXT (wParam is TRUE) is sent to the focused window.
+ // The top level window never becomes composing window, so, we can ignore
+ // the WM_IME_SETCONTEXT on the top level window.
+ if (IsTopLevelWindowOfComposition(aWindow)) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::OnIMESetContext, hWnd=%p is top level window",
+ aWindow->GetWindowHandle()));
+ return true;
+ }
+
+ // When IME context is activating on another window,
+ // we should commit the old composition on the old window.
+ bool cancelComposition = false;
+ if (wParam && gIMMHandler) {
+ cancelComposition = gIMMHandler->CommitCompositionOnPreviousWindow(aWindow);
+ }
+
+ if (wParam && (lParam & ISC_SHOWUICOMPOSITIONWINDOW) &&
+ ShouldDrawCompositionStringOurselves()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::OnIMESetContext, ISC_SHOWUICOMPOSITIONWINDOW is "
+ "removed"));
+ lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
+ }
+
+ // We should sent WM_IME_SETCONTEXT to the DefWndProc here because the
+ // ancestor windows shouldn't receive this message. If they receive the
+ // message, we cannot know whether which window is the target of the message.
+ aResult.mResult = ::DefWindowProc(aWindow->GetWindowHandle(),
+ WM_IME_SETCONTEXT, wParam, lParam);
+
+ // Cancel composition on the new window if we committed our composition on
+ // another window.
+ if (cancelComposition) {
+ CancelComposition(aWindow, true);
+ }
+
+ aResult.mConsumed = true;
+ return true;
+}
+
+bool IMMHandler::OnChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ // The return value must be same as aResult.mConsumed because only when we
+ // consume the message, the caller shouldn't do anything anymore but
+ // otherwise, the caller should handle the message.
+ aResult.mConsumed = false;
+ if (IsIMECharRecordsEmpty()) {
+ return aResult.mConsumed;
+ }
+ WPARAM recWParam;
+ LPARAM recLParam;
+ DequeueIMECharRecords(recWParam, recLParam);
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::OnChar, aWindow=%p, wParam=%08zx, lParam=%08" PRIxLPTR ", "
+ "recorded: wParam=%08zx, lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), wParam, lParam, recWParam, recLParam));
+ // If an unexpected char message comes, we should reset the records,
+ // of course, this shouldn't happen.
+ if (recWParam != wParam || recLParam != lParam) {
+ ResetIMECharRecords();
+ return aResult.mConsumed;
+ }
+ // Eat the char message which is caused by WM_IME_CHAR because we should
+ // have processed the IME messages, so, this message could be come from
+ // a windowless plug-in.
+ aResult.mConsumed = true;
+ return aResult.mConsumed;
+}
+
+/****************************************************************************
+ * others
+ ****************************************************************************/
+
+TextEventDispatcher* IMMHandler::GetTextEventDispatcherFor(nsWindow* aWindow) {
+ return aWindow == mComposingWindow && mDispatcher
+ ? mDispatcher.get()
+ : aWindow->GetTextEventDispatcher();
+}
+
+void IMMHandler::HandleStartComposition(nsWindow* aWindow,
+ const IMEContext& aContext) {
+ MOZ_ASSERT(!mIsComposing,
+ "HandleStartComposition is called but mIsComposing is TRUE");
+
+ const Maybe<ContentSelection>& contentSelection =
+ GetContentSelectionWithQueryIfNothing(aWindow);
+ if (contentSelection.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleStartComposition, FAILED, due to "
+ "Selection::GetContentSelectionWithQueryIfNothing() failure"));
+ return;
+ }
+ if (!contentSelection->HasRange()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleStartComposition, FAILED, due to "
+ "there is no selection"));
+ return;
+ }
+
+ AdjustCompositionFont(aWindow, aContext, contentSelection->WritingModeRef());
+
+ mCompositionStart = contentSelection->OffsetAndDataRef().StartOffset();
+ mCursorPosition = NO_IME_CARET;
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcherFor(aWindow);
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleStartComposition, FAILED due to "
+ "TextEventDispatcher::BeginNativeInputTransaction() failure"));
+ return;
+ }
+ WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime();
+ nsEventStatus status;
+ rv = dispatcher->StartComposition(status, &eventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleStartComposition, FAILED, due to "
+ "TextEventDispatcher::StartComposition() failure"));
+ return;
+ }
+
+ mIsComposing = true;
+ mComposingWindow = aWindow;
+ mDispatcher = dispatcher;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleStartComposition, START composition, "
+ "mCompositionStart=%u",
+ mCompositionStart));
+}
+
+bool IMMHandler::HandleComposition(nsWindow* aWindow,
+ const IMEContext& aContext, LPARAM lParam) {
+ // for bug #60050
+ // MS-IME 95/97/98/2000 may send WM_IME_COMPOSITION with non-conversion
+ // mode before it send WM_IME_STARTCOMPOSITION.
+ // However, ATOK sends a WM_IME_COMPOSITION before WM_IME_STARTCOMPOSITION,
+ // and if we access ATOK via some APIs, ATOK will sometimes fail to
+ // initialize its state. If WM_IME_STARTCOMPOSITION is already in the
+ // message queue, we should ignore the strange WM_IME_COMPOSITION message and
+ // skip to the next. So, we should look for next composition message
+ // (WM_IME_STARTCOMPOSITION or WM_IME_ENDCOMPOSITION or WM_IME_COMPOSITION),
+ // and if it's WM_IME_STARTCOMPOSITION, and one more next composition message
+ // is WM_IME_COMPOSITION, current IME is ATOK, probably. Otherwise, we
+ // should start composition forcibly.
+ if (!mIsComposing) {
+ MSG msg1, msg2;
+ HWND wnd = aWindow->GetWindowHandle();
+ if (WinUtils::PeekMessage(&msg1, wnd, WM_IME_STARTCOMPOSITION,
+ WM_IME_COMPOSITION, PM_NOREMOVE) &&
+ msg1.message == WM_IME_STARTCOMPOSITION &&
+ WinUtils::PeekMessage(&msg2, wnd, WM_IME_ENDCOMPOSITION,
+ WM_IME_COMPOSITION, PM_NOREMOVE) &&
+ msg2.message == WM_IME_COMPOSITION) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleComposition, Ignores due to find a "
+ "WM_IME_STARTCOMPOSITION"));
+ return ShouldDrawCompositionStringOurselves();
+ }
+ }
+
+ bool startCompositionMessageHasBeenSent = mIsComposing;
+
+ //
+ // This catches a fixed result
+ //
+ if (IS_COMMITTING_LPARAM(lParam)) {
+ if (!mIsComposing) {
+ HandleStartComposition(aWindow, aContext);
+ }
+
+ GetCompositionString(aContext, GCS_RESULTSTR, mCompositionString);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleComposition, GCS_RESULTSTR"));
+
+ HandleEndComposition(aWindow, &mCompositionString);
+
+ if (!IS_COMPOSING_LPARAM(lParam)) {
+ return ShouldDrawCompositionStringOurselves();
+ }
+ }
+
+ //
+ // This provides us with a composition string
+ //
+ if (!mIsComposing) {
+ HandleStartComposition(aWindow, aContext);
+ }
+
+ //--------------------------------------------------------
+ // 1. Get GCS_COMPSTR
+ //--------------------------------------------------------
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleComposition, GCS_COMPSTR"));
+
+ nsAutoString previousCompositionString(mCompositionString);
+ GetCompositionString(aContext, GCS_COMPSTR, mCompositionString);
+
+ if (!IS_COMPOSING_LPARAM(lParam)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ (" IMMHandler::HandleComposition, lParam doesn't indicate composing, "
+ "mCompositionString=\"%s\", previousCompositionString=\"%s\"",
+ NS_ConvertUTF16toUTF8(mCompositionString).get(),
+ NS_ConvertUTF16toUTF8(previousCompositionString).get()));
+
+ // If composition string isn't changed, we can trust the lParam.
+ // So, we need to do nothing.
+ if (previousCompositionString == mCompositionString) {
+ return ShouldDrawCompositionStringOurselves();
+ }
+
+ // IME may send WM_IME_COMPOSITION without composing lParam values
+ // when composition string becomes empty (e.g., using Backspace key).
+ // If composition string is empty, we should dispatch a compositionchange
+ // event with empty string and clear the clause information.
+ if (mCompositionString.IsEmpty()) {
+ mClauseArray.Clear();
+ mAttributeArray.Clear();
+ mCursorPosition = 0;
+ DispatchCompositionChangeEvent(aWindow, aContext);
+ return ShouldDrawCompositionStringOurselves();
+ }
+
+ // Otherwise, we cannot trust the lParam value. We might need to
+ // dispatch compositionchange event with the latest composition string
+ // information.
+ }
+
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=296339
+ if (mCompositionString.IsEmpty() && !startCompositionMessageHasBeenSent) {
+ // In this case, maybe, the sender is MSPinYin. That sends *only*
+ // WM_IME_COMPOSITION with GCS_COMP* and GCS_RESULT* when
+ // user inputted the Chinese full stop. So, that doesn't send
+ // WM_IME_STARTCOMPOSITION and WM_IME_ENDCOMPOSITION.
+ // If WM_IME_STARTCOMPOSITION was not sent and the composition
+ // string is null (it indicates the composition transaction ended),
+ // WM_IME_ENDCOMPOSITION may not be sent. If so, we cannot run
+ // HandleEndComposition() in other place.
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::HandleComposition, Aborting GCS_COMPSTR"));
+ HandleEndComposition(aWindow);
+ return IS_COMMITTING_LPARAM(lParam);
+ }
+
+ //--------------------------------------------------------
+ // 2. Get GCS_COMPCLAUSE
+ //--------------------------------------------------------
+ long clauseArrayLength =
+ ::ImmGetCompositionStringW(aContext.get(), GCS_COMPCLAUSE, nullptr, 0);
+ clauseArrayLength /= sizeof(uint32_t);
+
+ if (clauseArrayLength > 0) {
+ nsresult rv = EnsureClauseArray(clauseArrayLength);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Intelligent ABC IME (Simplified Chinese IME, the code page is 936)
+ // will crash in ImmGetCompositionStringW for GCS_COMPCLAUSE (bug 424663).
+ // See comment 35 of the bug for the detail. Therefore, we should use A
+ // API for it, however, we should not kill Unicode support on all IMEs.
+ bool useA_API = !(sIMEProperty & IME_PROP_UNICODE);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::HandleComposition, GCS_COMPCLAUSE, useA_API=%s",
+ useA_API ? "TRUE" : "FALSE"));
+
+ long clauseArrayLength2 =
+ useA_API ? ::ImmGetCompositionStringA(
+ aContext.get(), GCS_COMPCLAUSE, mClauseArray.Elements(),
+ mClauseArray.Capacity() * sizeof(uint32_t))
+ : ::ImmGetCompositionStringW(
+ aContext.get(), GCS_COMPCLAUSE, mClauseArray.Elements(),
+ mClauseArray.Capacity() * sizeof(uint32_t));
+ clauseArrayLength2 /= sizeof(uint32_t);
+
+ if (clauseArrayLength != clauseArrayLength2) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::HandleComposition, GCS_COMPCLAUSE, "
+ "clauseArrayLength=%ld but clauseArrayLength2=%ld",
+ clauseArrayLength, clauseArrayLength2));
+ if (clauseArrayLength > clauseArrayLength2)
+ clauseArrayLength = clauseArrayLength2;
+ }
+
+ if (useA_API && clauseArrayLength > 0) {
+ // Convert each values of sIMECompClauseArray. The values mean offset of
+ // the clauses in ANSI string. But we need the values in Unicode string.
+ nsAutoCString compANSIStr;
+ if (ConvertToANSIString(mCompositionString, GetKeyboardCodePage(),
+ compANSIStr)) {
+ uint32_t maxlen = compANSIStr.Length();
+ mClauseArray.SetLength(clauseArrayLength);
+ mClauseArray[0] = 0; // first value must be 0
+ for (int32_t i = 1; i < clauseArrayLength; i++) {
+ uint32_t len = std::min(mClauseArray[i], maxlen);
+ mClauseArray[i] =
+ ::MultiByteToWideChar(GetKeyboardCodePage(), MB_PRECOMPOSED,
+ (LPCSTR)compANSIStr.get(), len, nullptr, 0);
+ }
+ }
+ }
+ }
+ // compClauseArrayLength may be negative. I.e., ImmGetCompositionStringW
+ // may return an error code.
+ mClauseArray.SetLength(std::max<long>(0, clauseArrayLength));
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::HandleComposition, GCS_COMPCLAUSE, mClauseLength=%zu",
+ mClauseArray.Length()));
+
+ //--------------------------------------------------------
+ // 3. Get GCS_COMPATTR
+ //--------------------------------------------------------
+ // This provides us with the attribute string necessary
+ // for doing hiliting
+ long attrArrayLength =
+ ::ImmGetCompositionStringW(aContext.get(), GCS_COMPATTR, nullptr, 0);
+ attrArrayLength /= sizeof(uint8_t);
+
+ if (attrArrayLength > 0) {
+ nsresult rv = EnsureAttributeArray(attrArrayLength);
+ NS_ENSURE_SUCCESS(rv, false);
+ attrArrayLength = ::ImmGetCompositionStringW(
+ aContext.get(), GCS_COMPATTR, mAttributeArray.Elements(),
+ mAttributeArray.Capacity() * sizeof(uint8_t));
+ }
+
+ // attrStrLen may be negative. I.e., ImmGetCompositionStringW may return an
+ // error code.
+ mAttributeArray.SetLength(std::max<long>(0, attrArrayLength));
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ (" IMMHandler::HandleComposition, GCS_COMPATTR, mAttributeLength=%zu",
+ mAttributeArray.Length()));
+
+ //--------------------------------------------------------
+ // 4. Get GCS_CURSOPOS
+ //--------------------------------------------------------
+ // Some IMEs (e.g., the standard IME for Korean) don't have caret position.
+ if (lParam & GCS_CURSORPOS) {
+ mCursorPosition =
+ ::ImmGetCompositionStringW(aContext.get(), GCS_CURSORPOS, nullptr, 0);
+ if (mCursorPosition < 0) {
+ mCursorPosition = NO_IME_CARET; // The result is error
+ }
+ } else {
+ mCursorPosition = NO_IME_CARET;
+ }
+
+ NS_ASSERTION(mCursorPosition <= (long)mCompositionString.Length(),
+ "illegal pos");
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::HandleComposition, GCS_CURSORPOS, mCursorPosition=%d",
+ mCursorPosition));
+
+ //--------------------------------------------------------
+ // 5. Send the compositionchange event
+ //--------------------------------------------------------
+ DispatchCompositionChangeEvent(aWindow, aContext);
+
+ return ShouldDrawCompositionStringOurselves();
+}
+
+void IMMHandler::HandleEndComposition(nsWindow* aWindow,
+ const nsAString* aCommitString) {
+ MOZ_ASSERT(mIsComposing,
+ "HandleEndComposition is called but mIsComposing is FALSE");
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleEndComposition(aWindow=0x%p, aCommitString=0x%p "
+ "(\"%s\"))",
+ aWindow, aCommitString,
+ aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));
+
+ IMEHandler::MaybeDestroyNativeCaret();
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcherFor(aWindow);
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleEndComposition, FAILED due to "
+ "TextEventDispatcher::BeginNativeInputTransaction() failure"));
+ return;
+ }
+ WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime();
+ nsEventStatus status;
+ rv = dispatcher->CommitComposition(status, aCommitString, &eventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleStartComposition, FAILED, due to "
+ "TextEventDispatcher::CommitComposition() failure"));
+ return;
+ }
+ mIsComposing = false;
+ // XXX aWindow and mComposingWindow are always same??
+ mComposingWindow = nullptr;
+ mDispatcher = nullptr;
+}
+
+bool IMMHandler::HandleReconvert(nsWindow* aWindow, LPARAM lParam,
+ LRESULT* oResult) {
+ *oResult = 0;
+ RECONVERTSTRING* pReconv = reinterpret_cast<RECONVERTSTRING*>(lParam);
+
+ const Maybe<ContentSelection>& contentSelection =
+ GetContentSelectionWithQueryIfNothing(aWindow);
+ if (contentSelection.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleReconvert, FAILED, due to "
+ "Selection::GetContentSelectionWithQueryIfNothing() failure"));
+ return false;
+ }
+
+ const uint32_t len = contentSelection->HasRange()
+ ? contentSelection->OffsetAndDataRef().Length()
+ : 0u;
+ uint32_t needSize = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR);
+
+ if (!pReconv) {
+ // Return need size to reconvert.
+ if (len == 0) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleReconvert, There are not selected text"));
+ return false;
+ }
+ *oResult = needSize;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleReconvert, succeeded, result=%" PRIdLPTR,
+ *oResult));
+ return true;
+ }
+
+ if (pReconv->dwSize < needSize) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleReconvert, FAILED, pReconv->dwSize=%ld, "
+ "needSize=%u",
+ pReconv->dwSize, needSize));
+ return false;
+ }
+
+ *oResult = needSize;
+
+ // Fill reconvert struct
+ pReconv->dwVersion = 0;
+ pReconv->dwStrLen = len;
+ pReconv->dwStrOffset = sizeof(RECONVERTSTRING);
+ pReconv->dwCompStrLen = len;
+ pReconv->dwCompStrOffset = 0;
+ pReconv->dwTargetStrLen = len;
+ pReconv->dwTargetStrOffset = 0;
+
+ if (len) {
+ ::CopyMemory(reinterpret_cast<LPVOID>(lParam + sizeof(RECONVERTSTRING)),
+ contentSelection->OffsetAndDataRef().DataRef().get(),
+ len * sizeof(WCHAR));
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleReconvert, SUCCEEDED, pReconv=%s, result=%" PRIdLPTR,
+ GetReconvertStringLog(pReconv).get(), *oResult));
+
+ return true;
+}
+
+bool IMMHandler::HandleQueryCharPosition(nsWindow* aWindow, LPARAM lParam,
+ LRESULT* oResult) {
+ uint32_t len = mIsComposing ? mCompositionString.Length() : 0;
+ *oResult = false;
+ IMECHARPOSITION* pCharPosition = reinterpret_cast<IMECHARPOSITION*>(lParam);
+ if (!pCharPosition) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleQueryCharPosition, FAILED, due to "
+ "pCharPosition is null"));
+ return false;
+ }
+ if (pCharPosition->dwSize < sizeof(IMECHARPOSITION)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleReconvert, FAILED, pCharPosition->dwSize=%lu, "
+ "sizeof(IMECHARPOSITION)=%zu",
+ pCharPosition->dwSize, sizeof(IMECHARPOSITION)));
+ return false;
+ }
+ if (::GetFocus() != aWindow->GetWindowHandle()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleReconvert, FAILED, ::GetFocus()=%p, "
+ "OurWindowHandle=%p",
+ ::GetFocus(), aWindow->GetWindowHandle()));
+ return false;
+ }
+ if (pCharPosition->dwCharPos > len) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleQueryCharPosition, FAILED, "
+ "pCharPosition->dwCharPos=%ld, len=%u",
+ pCharPosition->dwCharPos, len));
+ return false;
+ }
+
+ LayoutDeviceIntRect r;
+ bool ret =
+ GetCharacterRectOfSelectedTextAt(aWindow, pCharPosition->dwCharPos, r);
+ NS_ENSURE_TRUE(ret, false);
+
+ LayoutDeviceIntRect screenRect;
+ // We always need top level window that is owner window of the popup window
+ // even if the content of the popup window has focus.
+ ResolveIMECaretPos(aWindow->GetTopLevelWindow(false), r, nullptr, screenRect);
+
+ // XXX This might need to check writing mode. However, MSDN doesn't explain
+ // how to set the values in vertical writing mode. Additionally, IME
+ // doesn't work well with top-left of the character (this is explicitly
+ // documented) and its horizontal width. So, it might be better to set
+ // top-right corner of the character and horizontal width, but we're not
+ // sure if it doesn't cause any problems with a lot of IMEs...
+ pCharPosition->pt.x = screenRect.X();
+ pCharPosition->pt.y = screenRect.Y();
+
+ pCharPosition->cLineHeight = r.Height();
+
+ WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, aWindow);
+ aWindow->InitEvent(queryEditorRectEvent);
+ DispatchEvent(aWindow, queryEditorRectEvent);
+ if (NS_WARN_IF(queryEditorRectEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleQueryCharPosition, eQueryEditorRect failed"));
+ ::GetWindowRect(aWindow->GetWindowHandle(), &pCharPosition->rcDocument);
+ } else {
+ LayoutDeviceIntRect editorRectInWindow = queryEditorRectEvent.mReply->mRect;
+ nsWindow* window = !!queryEditorRectEvent.mReply->mFocusedWidget
+ ? static_cast<nsWindow*>(
+ queryEditorRectEvent.mReply->mFocusedWidget)
+ : aWindow;
+ LayoutDeviceIntRect editorRectInScreen;
+ ResolveIMECaretPos(window, editorRectInWindow, nullptr, editorRectInScreen);
+ ::SetRect(&pCharPosition->rcDocument, editorRectInScreen.X(),
+ editorRectInScreen.Y(), editorRectInScreen.XMost(),
+ editorRectInScreen.YMost());
+ }
+
+ *oResult = TRUE;
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleQueryCharPosition, SUCCEEDED, pCharPosition={ "
+ "pt={ x=%ld, y=%ld }, cLineHeight=%d, rcDocument={ left=%ld, top=%ld, "
+ "right=%ld, bottom=%ld } }",
+ pCharPosition->pt.x, pCharPosition->pt.y, pCharPosition->cLineHeight,
+ pCharPosition->rcDocument.left, pCharPosition->rcDocument.top,
+ pCharPosition->rcDocument.right, pCharPosition->rcDocument.bottom));
+ return true;
+}
+
+bool IMMHandler::HandleDocumentFeed(nsWindow* aWindow, LPARAM lParam,
+ LRESULT* oResult) {
+ *oResult = 0;
+ RECONVERTSTRING* pReconv = reinterpret_cast<RECONVERTSTRING*>(lParam);
+
+ LayoutDeviceIntPoint point(0, 0);
+
+ bool hasCompositionString =
+ mIsComposing && ShouldDrawCompositionStringOurselves();
+
+ int32_t targetOffset, targetLength;
+ if (!hasCompositionString) {
+ const Maybe<ContentSelection>& contentSelection =
+ GetContentSelectionWithQueryIfNothing(aWindow);
+ if (contentSelection.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleDocumentFeed, FAILED, due to "
+ "Selection::GetContentSelectionWithQueryIfNothing() failure"));
+ return false;
+ }
+ if (contentSelection->HasRange()) {
+ targetOffset = static_cast<int32_t>(
+ contentSelection->OffsetAndDataRef().StartOffset());
+ targetLength =
+ static_cast<int32_t>(contentSelection->OffsetAndDataRef().Length());
+ } else {
+ // If there is no selection range, let's return all text in the editor.
+ targetOffset = 0;
+ targetLength = INT32_MAX;
+ }
+ } else {
+ targetOffset = int32_t(mCompositionStart);
+ targetLength = int32_t(mCompositionString.Length());
+ }
+
+ // XXX nsString::Find and nsString::RFind take int32_t for offset, so,
+ // we cannot support this message when the current offset is larger than
+ // INT32_MAX.
+ if (targetOffset < 0 || targetLength < 0 || targetOffset + targetLength < 0) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleDocumentFeed, FAILED, "
+ "due to the selection is out of range"));
+ return false;
+ }
+
+ // Get all contents of the focused editor.
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ aWindow);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ aWindow->InitEvent(queryTextContentEvent, &point);
+ DispatchEvent(aWindow, queryTextContentEvent);
+ if (NS_WARN_IF(queryTextContentEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleDocumentFeed, FAILED, "
+ "due to eQueryTextContent failure"));
+ return false;
+ }
+
+ nsAutoString str(queryTextContentEvent.mReply->DataRef());
+ if (targetOffset > static_cast<int32_t>(str.Length())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::HandleDocumentFeed, FAILED, "
+ "due to the caret offset is invalid"));
+ return false;
+ }
+
+ // Get the focused paragraph, we decide that it starts from the previous CRLF
+ // (or start of the editor) to the next one (or the end of the editor).
+ int32_t paragraphStart = 0;
+ if (targetOffset > 0) {
+ paragraphStart = Substring(str, 0, targetOffset).RFind(u"\n") + 1;
+ }
+ int32_t paragraphEnd = str.Find(u"\r", targetOffset + targetLength);
+ if (paragraphEnd < 0) {
+ paragraphEnd = str.Length();
+ }
+ nsDependentSubstring paragraph(str, paragraphStart,
+ paragraphEnd - paragraphStart);
+
+ uint32_t len = paragraph.Length();
+ uint32_t needSize = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR);
+
+ if (!pReconv) {
+ *oResult = needSize;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleDocumentFeed, succeeded, result=%" PRIdLPTR,
+ *oResult));
+ return true;
+ }
+
+ if (pReconv->dwSize < needSize) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleDocumentFeed, FAILED, "
+ "pReconv->dwSize=%ld, needSize=%u",
+ pReconv->dwSize, needSize));
+ return false;
+ }
+
+ // Fill reconvert struct
+ pReconv->dwVersion = 0;
+ pReconv->dwStrLen = len;
+ pReconv->dwStrOffset = sizeof(RECONVERTSTRING);
+ if (hasCompositionString) {
+ pReconv->dwCompStrLen = targetLength;
+ pReconv->dwCompStrOffset = (targetOffset - paragraphStart) * sizeof(WCHAR);
+ // Set composition target clause information
+ uint32_t offset, length;
+ if (!GetTargetClauseRange(&offset, &length)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::HandleDocumentFeed, FAILED, "
+ "due to IMMHandler::GetTargetClauseRange() failure"));
+ return false;
+ }
+ pReconv->dwTargetStrLen = length;
+ pReconv->dwTargetStrOffset = (offset - paragraphStart) * sizeof(WCHAR);
+ } else {
+ pReconv->dwTargetStrLen = targetLength;
+ pReconv->dwTargetStrOffset =
+ (targetOffset - paragraphStart) * sizeof(WCHAR);
+ // There is no composition string, so, the length is zero but we should
+ // set the cursor offset to the composition str offset.
+ pReconv->dwCompStrLen = 0;
+ pReconv->dwCompStrOffset = pReconv->dwTargetStrOffset;
+ }
+
+ *oResult = needSize;
+ ::CopyMemory(reinterpret_cast<LPVOID>(lParam + sizeof(RECONVERTSTRING)),
+ paragraph.BeginReading(), len * sizeof(WCHAR));
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::HandleDocumentFeed, SUCCEEDED, pReconv=%s, "
+ "result=%" PRIdLPTR,
+ GetReconvertStringLog(pReconv).get(), *oResult));
+
+ return true;
+}
+
+bool IMMHandler::CommitCompositionOnPreviousWindow(nsWindow* aWindow) {
+ if (!mComposingWindow || mComposingWindow == aWindow) {
+ return false;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::CommitCompositionOnPreviousWindow, mIsComposing=%s",
+ GetBoolName(mIsComposing)));
+
+ // If we have composition, we should dispatch composition events internally.
+ if (mIsComposing) {
+ IMEContext context(mComposingWindow);
+ NS_ASSERTION(context.IsValid(), "IME context must be valid");
+
+ HandleEndComposition(mComposingWindow);
+ return true;
+ }
+
+ return false;
+}
+
+static TextRangeType PlatformToNSAttr(uint8_t aAttr) {
+ switch (aAttr) {
+ case ATTR_INPUT_ERROR:
+ // case ATTR_FIXEDCONVERTED:
+ case ATTR_INPUT:
+ return TextRangeType::eRawClause;
+ case ATTR_CONVERTED:
+ return TextRangeType::eConvertedClause;
+ case ATTR_TARGET_NOTCONVERTED:
+ return TextRangeType::eSelectedRawClause;
+ case ATTR_TARGET_CONVERTED:
+ return TextRangeType::eSelectedClause;
+ default:
+ NS_ASSERTION(false, "unknown attribute");
+ return TextRangeType::eCaret;
+ }
+}
+
+// static
+void IMMHandler::DispatchEvent(nsWindow* aWindow, WidgetGUIEvent& aEvent) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::DispatchEvent(aWindow=0x%p, aEvent={ mMessage=%s }, "
+ "aWindow->Destroyed()=%s",
+ aWindow, ToChar(aEvent.mMessage), GetBoolName(aWindow->Destroyed())));
+
+ if (aWindow->Destroyed()) {
+ return;
+ }
+
+ aWindow->DispatchWindowEvent(aEvent);
+}
+
+void IMMHandler::DispatchCompositionChangeEvent(nsWindow* aWindow,
+ const IMEContext& aContext) {
+ NS_ASSERTION(mIsComposing, "conflict state");
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::DispatchCompositionChangeEvent"));
+
+ // If we don't need to draw composition string ourselves, we don't need to
+ // fire compositionchange event during composing.
+ if (!ShouldDrawCompositionStringOurselves()) {
+ // But we need to adjust composition window pos and native caret pos, here.
+ SetIMERelatedWindowsPos(aWindow, aContext);
+ return;
+ }
+
+ RefPtr<nsWindow> kungFuDeathGrip(aWindow);
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcherFor(aWindow);
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to "
+ "TextEventDispatcher::BeginNativeInputTransaction() failure"));
+ return;
+ }
+
+ // NOTE: Calling SetIMERelatedWindowsPos() from this method will be failure
+ // in e10s mode. compositionchange event will notify this of
+ // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED, then
+ // SetIMERelatedWindowsPos() will be called.
+
+ // XXX Sogou (Simplified Chinese IME) returns contradictory values:
+ // The cursor position is actual cursor position. However, other values
+ // (composition string and attributes) are empty.
+
+ if (mCompositionString.IsEmpty()) {
+ // Don't append clause information if composition string is empty.
+ } else if (mClauseArray.IsEmpty()) {
+ // Some IMEs don't return clause array information, then, we assume that
+ // all characters in the composition string are in one clause.
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::DispatchCompositionChangeEvent, "
+ "mClauseArray.Length()=0"));
+ rv = dispatcher->SetPendingComposition(mCompositionString, nullptr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to"
+ "TextEventDispatcher::SetPendingComposition() failure"));
+ return;
+ }
+ } else {
+ // iterate over the attributes
+ rv = dispatcher->SetPendingCompositionString(mCompositionString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to"
+ "TextEventDispatcher::SetPendingCompositionString() failure"));
+ return;
+ }
+ uint32_t lastOffset = 0;
+ for (uint32_t i = 0; i < mClauseArray.Length() - 1; i++) {
+ uint32_t current = mClauseArray[i + 1];
+ if (current > mCompositionString.Length()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::DispatchCompositionChangeEvent, "
+ "mClauseArray[%u]=%u. "
+ "This is larger than mCompositionString.Length()=%zu",
+ i + 1, current, mCompositionString.Length()));
+ current = int32_t(mCompositionString.Length());
+ }
+
+ uint32_t length = current - lastOffset;
+ if (NS_WARN_IF(lastOffset >= mAttributeArray.Length())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to "
+ "invalid data of mClauseArray or mAttributeArray"));
+ return;
+ }
+ TextRangeType textRangeType =
+ PlatformToNSAttr(mAttributeArray[lastOffset]);
+ rv = dispatcher->AppendClauseToPendingComposition(length, textRangeType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to"
+ "TextEventDispatcher::AppendClauseToPendingComposition() "
+ "failure"));
+ return;
+ }
+
+ lastOffset = current;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::DispatchCompositionChangeEvent, index=%u, "
+ "rangeType=%s, range length=%u",
+ i, ToChar(textRangeType), length));
+ }
+ }
+
+ if (mCursorPosition == NO_IME_CARET) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::DispatchCompositionChangeEvent, no caret"));
+ } else {
+ uint32_t cursor = static_cast<uint32_t>(mCursorPosition);
+ if (cursor > mCompositionString.Length()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::CreateTextRangeArray, mCursorPosition=%d. "
+ "This is larger than mCompositionString.Length()=%zu",
+ mCursorPosition, mCompositionString.Length()));
+ cursor = mCompositionString.Length();
+ }
+
+ // If caret is in the target clause, the target clause will be painted as
+ // normal selection range. Since caret shouldn't be in selection range on
+ // Windows, we shouldn't append caret range in such case.
+ const TextRangeArray* clauses = dispatcher->GetPendingCompositionClauses();
+ const TextRange* targetClause =
+ clauses ? clauses->GetTargetClause() : nullptr;
+ if (targetClause && cursor >= targetClause->mStartOffset &&
+ cursor <= targetClause->mEndOffset) {
+ // Forget the caret position specified by IME since Gecko's caret position
+ // will be at the end of composition string.
+ mCursorPosition = NO_IME_CARET;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::CreateTextRangeArray, no caret due to it's in "
+ "the target clause, now, mCursorPosition is NO_IME_CARET"));
+ }
+
+ if (mCursorPosition != NO_IME_CARET) {
+ rv = dispatcher->SetCaretInPendingComposition(cursor, 0);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to"
+ "TextEventDispatcher::SetCaretInPendingComposition() failure"));
+ return;
+ }
+ }
+ }
+
+ WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime();
+ nsEventStatus status;
+ rv = dispatcher->FlushPendingComposition(status, &eventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to"
+ "TextEventDispatcher::FlushPendingComposition() failure"));
+ return;
+ }
+}
+
+void IMMHandler::GetCompositionString(const IMEContext& aContext, DWORD aIndex,
+ nsAString& aCompositionString) const {
+ aCompositionString.Truncate();
+
+ // Retrieve the size of the required output buffer.
+ long lRtn = ::ImmGetCompositionStringW(aContext.get(), aIndex, nullptr, 0);
+ if (lRtn < 0 || !aCompositionString.SetLength((lRtn / sizeof(WCHAR)) + 1,
+ mozilla::fallible)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::GetCompositionString, FAILED, due to OOM"));
+ return; // Error or out of memory.
+ }
+
+ // Actually retrieve the composition string information.
+ lRtn = ::ImmGetCompositionStringW(aContext.get(), aIndex,
+ (LPVOID)aCompositionString.BeginWriting(),
+ lRtn + sizeof(WCHAR));
+ aCompositionString.SetLength(lRtn / sizeof(WCHAR));
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::GetCompositionString, succeeded, aCompositionString=\"%s\"",
+ NS_ConvertUTF16toUTF8(aCompositionString).get()));
+}
+
+bool IMMHandler::GetTargetClauseRange(uint32_t* aOffset, uint32_t* aLength) {
+ NS_ENSURE_TRUE(aOffset, false);
+ NS_ENSURE_TRUE(mIsComposing, false);
+ NS_ENSURE_TRUE(ShouldDrawCompositionStringOurselves(), false);
+
+ bool found = false;
+ *aOffset = mCompositionStart;
+ for (uint32_t i = 0; i < mAttributeArray.Length(); i++) {
+ if (mAttributeArray[i] == ATTR_TARGET_NOTCONVERTED ||
+ mAttributeArray[i] == ATTR_TARGET_CONVERTED) {
+ *aOffset = mCompositionStart + i;
+ found = true;
+ break;
+ }
+ }
+
+ if (!aLength) {
+ return true;
+ }
+
+ if (!found) {
+ // The all composition string is targetted when there is no ATTR_TARGET_*
+ // clause. E.g., there is only ATTR_INPUT
+ *aLength = mCompositionString.Length();
+ return true;
+ }
+
+ uint32_t offsetInComposition = *aOffset - mCompositionStart;
+ *aLength = mCompositionString.Length() - offsetInComposition;
+ for (uint32_t i = offsetInComposition; i < mAttributeArray.Length(); i++) {
+ if (mAttributeArray[i] != ATTR_TARGET_NOTCONVERTED &&
+ mAttributeArray[i] != ATTR_TARGET_CONVERTED) {
+ *aLength = i - offsetInComposition;
+ break;
+ }
+ }
+ return true;
+}
+
+bool IMMHandler::ConvertToANSIString(const nsString& aStr, UINT aCodePage,
+ nsACString& aANSIStr) {
+ int len = ::WideCharToMultiByte(aCodePage, 0, (LPCWSTR)aStr.get(),
+ aStr.Length(), nullptr, 0, nullptr, nullptr);
+ NS_ENSURE_TRUE(len >= 0, false);
+
+ if (!aANSIStr.SetLength(len, mozilla::fallible)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::ConvertToANSIString, FAILED, due to OOM"));
+ return false;
+ }
+ ::WideCharToMultiByte(aCodePage, 0, (LPCWSTR)aStr.get(), aStr.Length(),
+ (LPSTR)aANSIStr.BeginWriting(), len, nullptr, nullptr);
+ return true;
+}
+
+bool IMMHandler::GetCharacterRectOfSelectedTextAt(
+ nsWindow* aWindow, uint32_t aOffset, LayoutDeviceIntRect& aCharRect,
+ WritingMode* aWritingMode) {
+ LayoutDeviceIntPoint point(0, 0);
+
+ const Maybe<ContentSelection>& contentSelection =
+ GetContentSelectionWithQueryIfNothing(aWindow);
+ if (contentSelection.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to "
+ "Selection::GetContentSelectionWithQueryIfNothing() failure"));
+ return false;
+ }
+
+ // If there is neither a selection range nor composition string, cannot return
+ // character rect, of course.
+ if (!contentSelection->HasRange() && !mIsComposing) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to "
+ "there is neither a selection range nor composition string"));
+ return false;
+ }
+
+ // If the offset is larger than the end of composition string or selected
+ // string, we should return false since such case must be a bug of the caller
+ // or the active IME. If it's an IME's bug, we need to set targetLength to
+ // aOffset.
+ const uint32_t targetLength =
+ mIsComposing ? mCompositionString.Length()
+ : contentSelection->OffsetAndDataRef().Length();
+ if (NS_WARN_IF(aOffset > targetLength)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to "
+ "aOffset is too large (aOffset=%u, targetLength=%u, mIsComposing=%s)",
+ aOffset, targetLength, GetBoolName(mIsComposing)));
+ return false;
+ }
+
+ // If there is caret, we might be able to use caret rect.
+ uint32_t caretOffset = UINT32_MAX;
+ // There is a caret only when the normal selection is collapsed.
+ if (contentSelection.isNothing() ||
+ contentSelection->OffsetAndDataRef().IsDataEmpty()) {
+ if (mIsComposing) {
+ // If it's composing, mCursorPosition is the offset to caret in
+ // the composition string.
+ if (mCursorPosition != NO_IME_CARET) {
+ MOZ_ASSERT(mCursorPosition >= 0);
+ caretOffset = mCursorPosition;
+ } else if (!ShouldDrawCompositionStringOurselves() ||
+ mCompositionString.IsEmpty()) {
+ // Otherwise, if there is no composition string, we should assume that
+ // there is a caret at the start of composition string.
+ caretOffset = 0;
+ }
+ } else {
+ // If there is no composition, the selection offset is the caret offset.
+ caretOffset = 0;
+ }
+ }
+
+ // If there is a caret and retrieving offset is same as the caret offset,
+ // we should use the caret rect.
+ if (aOffset != caretOffset) {
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, aWindow);
+ WidgetQueryContentEvent::Options options;
+ options.mRelativeToInsertionPoint = true;
+ queryTextRectEvent.InitForQueryTextRect(aOffset, 1, options);
+ aWindow->InitEvent(queryTextRectEvent, &point);
+ DispatchEvent(aWindow, queryTextRectEvent);
+ if (queryTextRectEvent.Succeeded()) {
+ aCharRect = queryTextRectEvent.mReply->mRect;
+ if (aWritingMode) {
+ *aWritingMode = queryTextRectEvent.mReply->WritingModeRef();
+ }
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("IMMHandler::GetCharacterRectOfSelectedTextAt, Succeeded, "
+ "aOffset=%u, aCharRect={ x: %d, y: %d, width: %d, height: %d }, "
+ "queryTextRectEvent={ mReply=%s }",
+ aOffset, aCharRect.X(), aCharRect.Y(), aCharRect.Width(),
+ aCharRect.Height(), ToString(queryTextRectEvent.mReply).c_str()));
+ return true;
+ }
+ }
+
+ return GetCaretRect(aWindow, aCharRect, aWritingMode);
+}
+
+bool IMMHandler::GetCaretRect(nsWindow* aWindow,
+ LayoutDeviceIntRect& aCaretRect,
+ WritingMode* aWritingMode) {
+ LayoutDeviceIntPoint point(0, 0);
+
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWindow);
+ WidgetQueryContentEvent::Options options;
+ options.mRelativeToInsertionPoint = true;
+ queryCaretRectEvent.InitForQueryCaretRect(0, options);
+ aWindow->InitEvent(queryCaretRectEvent, &point);
+ DispatchEvent(aWindow, queryCaretRectEvent);
+ if (queryCaretRectEvent.Failed()) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::GetCaretRect, FAILED, due to eQueryCaretRect failure"));
+ return false;
+ }
+ aCaretRect = queryCaretRectEvent.mReply->mRect;
+ if (aWritingMode) {
+ *aWritingMode = queryCaretRectEvent.mReply->WritingModeRef();
+ }
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::GetCaretRect, SUCCEEDED, "
+ "aCaretRect={ x: %d, y: %d, width: %d, height: %d }, "
+ "queryCaretRectEvent={ mReply=%s }",
+ aCaretRect.X(), aCaretRect.Y(), aCaretRect.Width(),
+ aCaretRect.Height(), ToString(queryCaretRectEvent.mReply).c_str()));
+ return true;
+}
+
+bool IMMHandler::SetIMERelatedWindowsPos(nsWindow* aWindow,
+ const IMEContext& aContext) {
+ // Get first character rect of current a normal selected text or a composing
+ // string.
+ WritingMode writingMode;
+ LayoutDeviceIntRect firstSelectedCharRectRelativeToWindow;
+ bool ret = GetCharacterRectOfSelectedTextAt(
+ aWindow, 0, firstSelectedCharRectRelativeToWindow, &writingMode);
+ NS_ENSURE_TRUE(ret, false);
+ nsWindow* toplevelWindow = aWindow->GetTopLevelWindow(false);
+ LayoutDeviceIntRect firstSelectedCharRect;
+ ResolveIMECaretPos(toplevelWindow, firstSelectedCharRectRelativeToWindow,
+ aWindow, firstSelectedCharRect);
+
+ // Set native caret size/position to our caret. Some IMEs honor it. E.g.,
+ // "Intelligent ABC" (Simplified Chinese) and "MS PinYin 3.0" (Simplified
+ // Chinese) on XP. But if a11y module is handling native caret, we shouldn't
+ // touch it.
+ if (!IMEHandler::IsA11yHandlingNativeCaret()) {
+ LayoutDeviceIntRect caretRect(firstSelectedCharRect),
+ caretRectRelativeToWindow;
+ if (GetCaretRect(aWindow, caretRectRelativeToWindow)) {
+ ResolveIMECaretPos(toplevelWindow, caretRectRelativeToWindow, aWindow,
+ caretRect);
+ } else {
+ NS_WARNING("failed to get caret rect");
+ caretRect.SetWidth(1);
+ }
+ IMEHandler::CreateNativeCaret(aWindow, caretRect);
+ }
+
+ if (ShouldDrawCompositionStringOurselves()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::SetIMERelatedWindowsPos, Set candidate window"));
+
+ // Get a rect of first character in current target in composition string.
+ LayoutDeviceIntRect firstTargetCharRect, lastTargetCharRect;
+ if (mIsComposing && !mCompositionString.IsEmpty()) {
+ // If there are no targetted selection, we should use it's first character
+ // rect instead.
+ uint32_t offset, length;
+ if (!GetTargetClauseRange(&offset, &length)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::SetIMERelatedWindowsPos, FAILED, due to "
+ "GetTargetClauseRange() failure"));
+ return false;
+ }
+ ret =
+ GetCharacterRectOfSelectedTextAt(aWindow, offset - mCompositionStart,
+ firstTargetCharRect, &writingMode);
+ NS_ENSURE_TRUE(ret, false);
+ if (length) {
+ ret = GetCharacterRectOfSelectedTextAt(
+ aWindow, offset + length - 1 - mCompositionStart,
+ lastTargetCharRect);
+ NS_ENSURE_TRUE(ret, false);
+ } else {
+ lastTargetCharRect = firstTargetCharRect;
+ }
+ } else {
+ // If there are no composition string, we should use a first character
+ // rect.
+ ret = GetCharacterRectOfSelectedTextAt(aWindow, 0, firstTargetCharRect,
+ &writingMode);
+ NS_ENSURE_TRUE(ret, false);
+ lastTargetCharRect = firstTargetCharRect;
+ }
+ ResolveIMECaretPos(toplevelWindow, firstTargetCharRect, aWindow,
+ firstTargetCharRect);
+ ResolveIMECaretPos(toplevelWindow, lastTargetCharRect, aWindow,
+ lastTargetCharRect);
+ LayoutDeviceIntRect targetClauseRect;
+ targetClauseRect.UnionRect(firstTargetCharRect, lastTargetCharRect);
+
+ // Move the candidate window to proper position from the target clause as
+ // far as possible.
+ CANDIDATEFORM candForm;
+ candForm.dwIndex = 0;
+ if (!writingMode.IsVertical() || IsVerticalWritingSupported()) {
+ candForm.dwStyle = CFS_EXCLUDE;
+ // Candidate window shouldn't overlap the target clause in any writing
+ // mode.
+ candForm.rcArea.left = targetClauseRect.X();
+ candForm.rcArea.right = targetClauseRect.XMost();
+ candForm.rcArea.top = targetClauseRect.Y();
+ candForm.rcArea.bottom = targetClauseRect.YMost();
+ if (!writingMode.IsVertical()) {
+ // In horizontal layout, current point of interest should be top-left
+ // of the first character.
+ candForm.ptCurrentPos.x = firstTargetCharRect.X();
+ candForm.ptCurrentPos.y = firstTargetCharRect.Y();
+ } else if (writingMode.IsVerticalRL()) {
+ // In vertical layout (RL), candidate window should be positioned right
+ // side of target clause. However, we don't set vertical writing font
+ // to the IME. Therefore, the candidate window may be positioned
+ // bottom-left of target clause rect with these information.
+ candForm.ptCurrentPos.x = targetClauseRect.X();
+ candForm.ptCurrentPos.y = targetClauseRect.Y();
+ } else {
+ MOZ_ASSERT(writingMode.IsVerticalLR(), "Did we miss some causes?");
+ // In vertical layout (LR), candidate window should be poisitioned left
+ // side of target clause. Although, we don't set vertical writing font
+ // to the IME, the candidate window may be positioned bottom-right of
+ // the target clause rect with these information.
+ candForm.ptCurrentPos.x = targetClauseRect.XMost();
+ candForm.ptCurrentPos.y = targetClauseRect.Y();
+ }
+ } else {
+ // If vertical writing is not supported by IME, let's set candidate
+ // window position to the bottom-left of the target clause because
+ // the position must be the safest position to prevent the candidate
+ // window to overlap with the target clause.
+ candForm.dwStyle = CFS_CANDIDATEPOS;
+ candForm.ptCurrentPos.x = targetClauseRect.X();
+ candForm.ptCurrentPos.y = targetClauseRect.YMost();
+ }
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::SetIMERelatedWindowsPos, Calling "
+ "ImmSetCandidateWindow()... ptCurrentPos={ x=%ld, y=%ld }, "
+ "rcArea={ left=%ld, top=%ld, right=%ld, bottom=%ld }, "
+ "writingMode=%s",
+ candForm.ptCurrentPos.x, candForm.ptCurrentPos.y,
+ candForm.rcArea.left, candForm.rcArea.top, candForm.rcArea.right,
+ candForm.rcArea.bottom, ToString(writingMode).c_str()));
+ ::ImmSetCandidateWindow(aContext.get(), &candForm);
+ } else {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::SetIMERelatedWindowsPos, Set composition window"));
+
+ // Move the composition window to caret position (if selected some
+ // characters, we should use first character rect of them).
+ // And in this mode, IME adjusts the candidate window position
+ // automatically. So, we don't need to set it.
+ COMPOSITIONFORM compForm;
+ compForm.dwStyle = CFS_POINT;
+ compForm.ptCurrentPos.x = !writingMode.IsVerticalLR()
+ ? firstSelectedCharRect.X()
+ : firstSelectedCharRect.XMost();
+ compForm.ptCurrentPos.y = firstSelectedCharRect.Y();
+ ::ImmSetCompositionWindow(aContext.get(), &compForm);
+ }
+
+ return true;
+}
+
+void IMMHandler::ResolveIMECaretPos(nsIWidget* aReferenceWidget,
+ LayoutDeviceIntRect& aCursorRect,
+ nsIWidget* aNewOriginWidget,
+ LayoutDeviceIntRect& aOutRect) {
+ aOutRect = aCursorRect;
+
+ if (aReferenceWidget == aNewOriginWidget) return;
+
+ if (aReferenceWidget)
+ aOutRect.MoveBy(aReferenceWidget->WidgetToScreenOffset());
+
+ if (aNewOriginWidget)
+ aOutRect.MoveBy(-aNewOriginWidget->WidgetToScreenOffset());
+}
+
+static void SetHorizontalFontToLogFont(const nsAString& aFontFace,
+ LOGFONTW& aLogFont) {
+ aLogFont.lfEscapement = aLogFont.lfOrientation = 0;
+ if (NS_WARN_IF(aFontFace.Length() > LF_FACESIZE - 1)) {
+ memcpy(aLogFont.lfFaceName, L"System", sizeof(L"System"));
+ return;
+ }
+ memcpy(aLogFont.lfFaceName, aFontFace.BeginReading(),
+ aFontFace.Length() * sizeof(wchar_t));
+ aLogFont.lfFaceName[aFontFace.Length()] = 0;
+}
+
+static void SetVerticalFontToLogFont(const nsAString& aFontFace,
+ LOGFONTW& aLogFont) {
+ aLogFont.lfEscapement = aLogFont.lfOrientation = 2700;
+ if (NS_WARN_IF(aFontFace.Length() > LF_FACESIZE - 2)) {
+ memcpy(aLogFont.lfFaceName, L"@System", sizeof(L"@System"));
+ return;
+ }
+ aLogFont.lfFaceName[0] = '@';
+ memcpy(&aLogFont.lfFaceName[1], aFontFace.BeginReading(),
+ aFontFace.Length() * sizeof(wchar_t));
+ aLogFont.lfFaceName[aFontFace.Length() + 1] = 0;
+}
+
+void IMMHandler::AdjustCompositionFont(nsWindow* aWindow,
+ const IMEContext& aContext,
+ const WritingMode& aWritingMode,
+ bool aForceUpdate) {
+ // An instance of IMMHandler is destroyed when active IME is changed.
+ // Therefore, we need to store the information which are set to the IM
+ // context to static variables since IM context is never recreated.
+ static bool sCompositionFontsInitialized = false;
+ static nsString sCompositionFont;
+ static bool sCompositionFontPrefDone = false;
+ if (!sCompositionFontPrefDone) {
+ sCompositionFontPrefDone = true;
+ Preferences::GetString("intl.imm.composition_font", sCompositionFont);
+ }
+
+ // If composition font is customized by pref, we need to modify the
+ // composition font of the IME context at first time even if the writing mode
+ // is horizontal.
+ bool setCompositionFontForcibly =
+ aForceUpdate ||
+ (!sCompositionFontsInitialized && !sCompositionFont.IsEmpty());
+
+ static WritingMode sCurrentWritingMode;
+ static nsString sCurrentIMEName;
+ if (!setCompositionFontForcibly &&
+ sWritingModeOfCompositionFont == aWritingMode &&
+ sCurrentIMEName == sIMEName) {
+ // Nothing to do if writing mode isn't being changed.
+ return;
+ }
+
+ // Decide composition fonts for both horizontal writing mode and vertical
+ // writing mode. If the font isn't specified by the pref, use default
+ // font which is already set to the IM context. And also in vertical writing
+ // mode, insert '@' to the start of the font.
+ if (!sCompositionFontsInitialized) {
+ sCompositionFontsInitialized = true;
+ // sCompositionFontH must not start with '@' and its length is less than
+ // LF_FACESIZE since it needs to end with null terminating character.
+ if (sCompositionFont.IsEmpty() ||
+ sCompositionFont.Length() > LF_FACESIZE - 1 ||
+ sCompositionFont[0] == '@') {
+ LOGFONTW defaultLogFont;
+ if (NS_WARN_IF(
+ !::ImmGetCompositionFont(aContext.get(), &defaultLogFont))) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ (" IMMHandler::AdjustCompositionFont, ::ImmGetCompositionFont() "
+ "failed"));
+ sCompositionFont.AssignLiteral("System");
+ } else {
+ // The font face is typically, "System".
+ sCompositionFont.Assign(defaultLogFont.lfFaceName);
+ }
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" IMMHandler::AdjustCompositionFont, sCompositionFont=\"%s\" is "
+ "initialized",
+ NS_ConvertUTF16toUTF8(sCompositionFont).get()));
+ }
+
+ static nsString sCompositionFontForJapanist2003;
+ if (IsJapanist2003Active() && sCompositionFontForJapanist2003.IsEmpty()) {
+ const char* kCompositionFontForJapanist2003 =
+ "intl.imm.composition_font.japanist_2003";
+ Preferences::GetString(kCompositionFontForJapanist2003,
+ sCompositionFontForJapanist2003);
+ // If the font name is not specified properly, let's use
+ // "MS PGothic" instead.
+ if (sCompositionFontForJapanist2003.IsEmpty() ||
+ sCompositionFontForJapanist2003.Length() > LF_FACESIZE - 2 ||
+ sCompositionFontForJapanist2003[0] == '@') {
+ sCompositionFontForJapanist2003.AssignLiteral("MS PGothic");
+ }
+ }
+
+ sWritingModeOfCompositionFont = aWritingMode;
+ sCurrentIMEName = sIMEName;
+
+ LOGFONTW logFont;
+ memset(&logFont, 0, sizeof(logFont));
+ if (!::ImmGetCompositionFont(aContext.get(), &logFont)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::AdjustCompositionFont, ::ImmGetCompositionFont() "
+ "failed"));
+ logFont.lfFaceName[0] = 0;
+ }
+ // Need to reset some information which should be recomputed with new font.
+ logFont.lfWidth = 0;
+ logFont.lfWeight = FW_DONTCARE;
+ logFont.lfOutPrecision = OUT_DEFAULT_PRECIS;
+ logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS;
+ logFont.lfPitchAndFamily = DEFAULT_PITCH;
+
+ if (aWritingMode.IsVertical() && IsVerticalWritingSupported()) {
+ SetVerticalFontToLogFont(IsJapanist2003Active()
+ ? sCompositionFontForJapanist2003
+ : sCompositionFont,
+ logFont);
+ } else {
+ SetHorizontalFontToLogFont(IsJapanist2003Active()
+ ? sCompositionFontForJapanist2003
+ : sCompositionFont,
+ logFont);
+ }
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ (" IMMHandler::AdjustCompositionFont, calling "
+ "::ImmSetCompositionFont(\"%s\")",
+ NS_ConvertUTF16toUTF8(nsDependentString(logFont.lfFaceName)).get()));
+ ::ImmSetCompositionFontW(aContext.get(), &logFont);
+}
+
+// static
+nsresult IMMHandler::OnMouseButtonEvent(
+ nsWindow* aWindow, const IMENotification& aIMENotification) {
+ // We don't need to create the instance of the handler here.
+ if (!gIMMHandler) {
+ return NS_OK;
+ }
+
+ if (!sWM_MSIME_MOUSE || !IsComposingOnOurEditor() ||
+ !ShouldDrawCompositionStringOurselves()) {
+ return NS_OK;
+ }
+
+ // We need to handle only mousedown event.
+ if (aIMENotification.mMouseButtonEventData.mEventMessage != eMouseDown) {
+ return NS_OK;
+ }
+
+ // If the character under the cursor is not in the composition string,
+ // we don't need to notify IME of it.
+ uint32_t compositionStart = gIMMHandler->mCompositionStart;
+ uint32_t compositionEnd =
+ compositionStart + gIMMHandler->mCompositionString.Length();
+ if (aIMENotification.mMouseButtonEventData.mOffset < compositionStart ||
+ aIMENotification.mMouseButtonEventData.mOffset >= compositionEnd) {
+ return NS_OK;
+ }
+
+ BYTE button;
+ switch (aIMENotification.mMouseButtonEventData.mButton) {
+ case MouseButton::ePrimary:
+ button = IMEMOUSE_LDOWN;
+ break;
+ case MouseButton::eMiddle:
+ button = IMEMOUSE_MDOWN;
+ break;
+ case MouseButton::eSecondary:
+ button = IMEMOUSE_RDOWN;
+ break;
+ default:
+ return NS_OK;
+ }
+
+ // calcurate positioning and offset
+ // char : JCH1|JCH2|JCH3
+ // offset: 0011 1122 2233
+ // positioning: 2301 2301 2301
+ LayoutDeviceIntPoint cursorPos =
+ aIMENotification.mMouseButtonEventData.mCursorPos;
+ LayoutDeviceIntRect charRect =
+ aIMENotification.mMouseButtonEventData.mCharRect;
+ int32_t cursorXInChar = cursorPos.x - charRect.X();
+ // The event might hit to zero-width character, see bug 694913.
+ // The reason might be:
+ // * There are some zero-width characters are actually.
+ // * font-size is specified zero.
+ // But nobody reproduced this bug actually...
+ // We should assume that user clicked on right most of the zero-width
+ // character in such case.
+ int positioning = 1;
+ if (charRect.Width() > 0) {
+ positioning = cursorXInChar * 4 / charRect.Width();
+ positioning = (positioning + 2) % 4;
+ }
+
+ int offset =
+ aIMENotification.mMouseButtonEventData.mOffset - compositionStart;
+ if (positioning < 2) {
+ offset++;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::OnMouseButtonEvent, x,y=%d,%d, offset=%d, "
+ "positioning=%d",
+ cursorPos.x.value, cursorPos.y.value, offset, positioning));
+
+ // send MS_MSIME_MOUSE message to default IME window.
+ HWND imeWnd = ::ImmGetDefaultIMEWnd(aWindow->GetWindowHandle());
+ IMEContext context(aWindow);
+ if (::SendMessageW(imeWnd, sWM_MSIME_MOUSE,
+ MAKELONG(MAKEWORD(button, positioning), offset),
+ (LPARAM)context.get()) == 1) {
+ return NS_SUCCESS_EVENT_CONSUMED;
+ }
+ return NS_OK;
+}
+
+// static
+bool IMMHandler::OnKeyDownEvent(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("IMMHandler::OnKeyDownEvent, hWnd=%p, wParam=%08zx, lParam=%08" PRIxLPTR,
+ aWindow->GetWindowHandle(), wParam, lParam));
+ aResult.mConsumed = false;
+ switch (wParam) {
+ case VK_TAB:
+ case VK_PRIOR:
+ case VK_NEXT:
+ case VK_END:
+ case VK_HOME:
+ case VK_LEFT:
+ case VK_UP:
+ case VK_RIGHT:
+ case VK_DOWN:
+ case VK_RETURN:
+ // If IME didn't process the key message (the virtual key code wasn't
+ // converted to VK_PROCESSKEY), and the virtual key code event causes
+ // moving caret or editing text with keeping composing state, we should
+ // cancel the composition here because we cannot support moving
+ // composition string with DOM events (IE also cancels the composition
+ // in same cases). Then, this event will be dispatched.
+ if (IsComposingOnOurEditor()) {
+ // NOTE: We don't need to cancel the composition on another window.
+ CancelComposition(aWindow, false);
+ }
+ return false;
+ default:
+ return false;
+ }
+}
+
+Maybe<ContentSelection> IMMHandler::QueryContentSelection(nsWindow* aWindow) {
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ aWindow);
+ LayoutDeviceIntPoint point(0, 0);
+ aWindow->InitEvent(querySelectedTextEvent, &point);
+ DispatchEvent(aWindow, querySelectedTextEvent);
+ if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::Selection::Init, FAILED, due to eQuerySelectedText "
+ "failure"));
+ return Nothing();
+ }
+ // If the window is destroyed during querying selected text, we shouldn't
+ // do anymore.
+ if (aWindow->Destroyed()) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ (" IMMHandler::Selection::Init, FAILED, due to the widget destroyed"));
+ return Nothing();
+ }
+
+ ContentSelection contentSelection(querySelectedTextEvent);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("IMMHandler::Selection::Init, querySelectedTextEvent={ mReply=%s }",
+ ToString(querySelectedTextEvent.mReply).c_str()));
+
+ if (contentSelection.HasRange() &&
+ !contentSelection.OffsetAndDataRef().IsValid()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" IMMHandler::Selection::Init, FAILED, due to invalid range"));
+ return Nothing();
+ }
+ return Some(contentSelection);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/IMMHandler.h b/widget/windows/IMMHandler.h
new file mode 100644
index 0000000000..e012541fae
--- /dev/null
+++ b/widget/windows/IMMHandler.h
@@ -0,0 +1,427 @@
+/* -*- 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/. */
+
+#ifndef IMMHandler_h_
+#define IMMHandler_h_
+
+#include "mozilla/ContentData.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/WritingModes.h"
+
+#include "windef.h"
+#include "winnetwk.h"
+#include "npapi.h"
+
+#include "nsCOMPtr.h"
+#include "nsIWidget.h"
+#include "nsRect.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+#include <windows.h>
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+struct MSGResult;
+
+class IMEContext final {
+ public:
+ IMEContext() : mWnd(nullptr), mIMC(nullptr) {}
+
+ explicit IMEContext(HWND aWnd);
+ explicit IMEContext(nsWindow* aWindowBase);
+
+ ~IMEContext() { Clear(); }
+
+ HIMC get() const { return mIMC; }
+
+ void Init(HWND aWnd);
+ void Init(nsWindow* aWindowBase);
+ void Clear();
+
+ bool IsValid() const { return !!mIMC; }
+
+ void SetOpenState(bool aOpen) const {
+ if (!mIMC) {
+ return;
+ }
+ ::ImmSetOpenStatus(mIMC, aOpen);
+ }
+
+ bool GetOpenState() const {
+ if (!mIMC) {
+ return false;
+ }
+ return (::ImmGetOpenStatus(mIMC) != FALSE);
+ }
+
+ bool AssociateDefaultContext() {
+ // We assume that there is only default IMC, no new IMC has been created.
+ if (mIMC) {
+ return false;
+ }
+ if (!::ImmAssociateContextEx(mWnd, nullptr, IACE_DEFAULT)) {
+ return false;
+ }
+ mIMC = ::ImmGetContext(mWnd);
+ return (mIMC != nullptr);
+ }
+
+ bool Disassociate() {
+ if (!mIMC) {
+ return false;
+ }
+ if (!::ImmAssociateContextEx(mWnd, nullptr, 0)) {
+ return false;
+ }
+ ::ImmReleaseContext(mWnd, mIMC);
+ mIMC = nullptr;
+ return true;
+ }
+
+ protected:
+ IMEContext(const IMEContext& aOther) { MOZ_CRASH("Don't copy IMEContext"); }
+
+ HWND mWnd;
+ HIMC mIMC;
+};
+
+class IMMHandler final {
+ public:
+ static void Initialize();
+ static void Terminate();
+
+ // If Process*() returns true, the caller shouldn't do anything anymore.
+ static bool ProcessMessage(nsWindow* aWindow, UINT msg, WPARAM& wParam,
+ LPARAM& lParam, MSGResult& aResult);
+ static bool IsComposing() { return IsComposingOnOurEditor(); }
+ static bool IsComposingOn(nsWindow* aWindow) {
+ return IsComposing() && IsComposingWindow(aWindow);
+ }
+
+#ifdef DEBUG
+ /**
+ * IsIMEAvailable() returns TRUE when current keyboard layout has IME.
+ * Otherwise, FALSE.
+ */
+ static bool IsIMEAvailable() { return !!::ImmIsIME(::GetKeyboardLayout(0)); }
+#endif
+
+ // If aForce is TRUE, these methods doesn't check whether we have composition
+ // or not. If you don't set it to TRUE, these method doesn't commit/cancel
+ // the composition on uexpected window.
+ static void CommitComposition(nsWindow* aWindow, bool aForce = false);
+ static void CancelComposition(nsWindow* aWindow, bool aForce = false);
+ static void OnFocusChange(bool aFocus, nsWindow* aWindow);
+ static void OnUpdateComposition(nsWindow* aWindow);
+ static void OnSelectionChange(nsWindow* aWindow,
+ const IMENotification& aIMENotification,
+ bool aIsIMMActive);
+
+ static IMENotificationRequests GetIMENotificationRequests();
+
+ // Returns NS_SUCCESS_EVENT_CONSUMED if the mouse button event is consumed by
+ // IME. Otherwise, NS_OK.
+ static nsresult OnMouseButtonEvent(nsWindow* aWindow,
+ const IMENotification& aIMENotification);
+
+#define DECL_IS_IME_ACTIVE(aReadableName) \
+ static bool Is##aReadableName##Active();
+
+ // Japanese IMEs
+ DECL_IS_IME_ACTIVE(ATOK2006)
+ DECL_IS_IME_ACTIVE(ATOK2007)
+ DECL_IS_IME_ACTIVE(ATOK2008)
+ DECL_IS_IME_ACTIVE(ATOK2009)
+ DECL_IS_IME_ACTIVE(ATOK2010)
+ DECL_IS_IME_ACTIVE(GoogleJapaneseInput)
+ DECL_IS_IME_ACTIVE(Japanist2003)
+
+#undef DECL_IS_IME_ACTIVE
+
+ /**
+ * IsActiveIMEInBlockList() returns true if we know active keyboard layout's
+ * IME has some crash bugs or something which make some damage to us. When
+ * this returns true, IMC shouldn't be associated with any windows.
+ */
+ static bool IsActiveIMEInBlockList();
+
+ protected:
+ static void EnsureHandlerInstance();
+
+ static bool IsComposingOnOurEditor();
+ static bool IsComposingWindow(nsWindow* aWindow);
+
+ static bool ShouldDrawCompositionStringOurselves();
+ static bool IsVerticalWritingSupported();
+ // aWindow can be nullptr if it's called without receiving WM_INPUTLANGCHANGE.
+ static void InitKeyboardLayout(nsWindow* aWindow, HKL aKeyboardLayout);
+ static UINT GetKeyboardCodePage();
+
+ /**
+ * Checks whether the window is top level window of the composing window.
+ * In this method, the top level window means in all windows, not only in all
+ * OUR windows. I.e., if the aWindow is embedded, this always returns FALSE.
+ */
+ static bool IsTopLevelWindowOfComposition(nsWindow* aWindow);
+
+ static bool ProcessInputLangChangeMessage(nsWindow* aWindow, WPARAM wParam,
+ LPARAM lParam, MSGResult& aResult);
+
+ IMMHandler();
+ ~IMMHandler();
+
+ // On*() methods return true if the caller of message handler shouldn't do
+ // anything anymore. Otherwise, false.
+ static bool OnKeyDownEvent(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+
+ bool OnIMEStartComposition(nsWindow* aWindow, MSGResult& aResult);
+ bool OnIMEComposition(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+ bool OnIMEEndComposition(nsWindow* aWindow, MSGResult& aResult);
+ bool OnIMERequest(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+ bool OnChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+ void OnInputLangChange(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+
+ // These message handlers don't use instance members, we should not create
+ // the instance by the messages. So, they should be static.
+ static bool OnIMEChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+ static bool OnIMESetContext(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+ static bool OnIMECompositionFull(nsWindow* aWindow, MSGResult& aResult);
+ static bool OnIMENotify(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+ static bool OnIMESelect(nsWindow* aWindow, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+
+ // The result of Handle* method mean "Processed" when it's TRUE.
+ void HandleStartComposition(nsWindow* aWindow, const IMEContext& aContext);
+ bool HandleComposition(nsWindow* aWindow, const IMEContext& aContext,
+ LPARAM lParam);
+ // If aCommitString is null, this commits composition with the latest
+ // dispatched data. Otherwise, commits composition with the value.
+ void HandleEndComposition(nsWindow* aWindow,
+ const nsAString* aCommitString = nullptr);
+ bool HandleReconvert(nsWindow* aWindow, LPARAM lParam, LRESULT* oResult);
+ bool HandleQueryCharPosition(nsWindow* aWindow, LPARAM lParam,
+ LRESULT* oResult);
+ bool HandleDocumentFeed(nsWindow* aWindow, LPARAM lParam, LRESULT* oResult);
+
+ /**
+ * When a window's IME context is activating but we have composition on
+ * another window, we should commit our composition because IME context is
+ * shared by all our windows (including plug-ins).
+ * @param aWindow is a new activated window.
+ * If aWindow is our composing window, this method does nothing.
+ * Otherwise, this commits the composition on the previous window.
+ * If this method did commit a composition, this returns TRUE.
+ */
+ bool CommitCompositionOnPreviousWindow(nsWindow* aWindow);
+
+ /**
+ * ResolveIMECaretPos
+ * Convert the caret rect of a composition event to another widget's
+ * coordinate system.
+ *
+ * @param aReferenceWidget The origin widget of aCursorRect.
+ * Typically, this is mReferenceWidget of the
+ * composing events. If the aCursorRect is in screen
+ * coordinates, set nullptr.
+ * @param aCursorRect The cursor rect.
+ * @param aNewOriginWidget aOutRect will be in this widget's coordinates. If
+ * this is nullptr, aOutRect will be in screen
+ * coordinates.
+ * @param aOutRect The converted cursor rect.
+ */
+ void ResolveIMECaretPos(nsIWidget* aReferenceWidget,
+ mozilla::LayoutDeviceIntRect& aCursorRect,
+ nsIWidget* aNewOriginWidget,
+ mozilla::LayoutDeviceIntRect& aOutRect);
+
+ bool ConvertToANSIString(const nsString& aStr, UINT aCodePage,
+ nsACString& aANSIStr);
+
+ bool SetIMERelatedWindowsPos(nsWindow* aWindow, const IMEContext& aContext);
+ /**
+ * GetCharacterRectOfSelectedTextAt() returns character rect of the offset
+ * from the selection start or the start of composition string if there is
+ * a composition.
+ *
+ * @param aWindow The window which has focus.
+ * @param aOffset Offset from the selection start or the start of
+ * composition string when there is a composition.
+ * This must be in the selection range or
+ * the composition string.
+ * @param aCharRect The result.
+ * @param aWritingMode The writing mode of current selection. When this
+ * is nullptr, this assumes that the selection is in
+ * horizontal writing mode.
+ * @return true if this succeeded to retrieve the rect.
+ * Otherwise, false.
+ */
+ bool GetCharacterRectOfSelectedTextAt(
+ nsWindow* aWindow, uint32_t aOffset,
+ mozilla::LayoutDeviceIntRect& aCharRect,
+ mozilla::WritingMode* aWritingMode = nullptr);
+ /**
+ * GetCaretRect() returns caret rect at current selection start.
+ *
+ * @param aWindow The window which has focus.
+ * @param aCaretRect The result.
+ * @param aWritingMode The writing mode of current selection. When this
+ * is nullptr, this assumes that the selection is in
+ * horizontal writing mode.
+ * @return true if this succeeded to retrieve the rect.
+ * Otherwise, false.
+ */
+ bool GetCaretRect(nsWindow* aWindow, mozilla::LayoutDeviceIntRect& aCaretRect,
+ mozilla::WritingMode* aWritingMode = nullptr);
+ void GetCompositionString(const IMEContext& aContext, DWORD aIndex,
+ nsAString& aCompositionString) const;
+
+ /**
+ * AdjustCompositionFont() makes IME vertical writing mode if it's supported.
+ * If aForceUpdate is true, it will update composition font even if writing
+ * mode isn't being changed.
+ */
+ void AdjustCompositionFont(nsWindow* aWindow, const IMEContext& aContext,
+ const mozilla::WritingMode& aWritingMode,
+ bool aForceUpdate = false);
+
+ /**
+ * MaybeAdjustCompositionFont() calls AdjustCompositionFont() when the
+ * locale of active IME is CJK. Note that this creates an instance even
+ * when there is no composition but the locale is CJK.
+ */
+ static void MaybeAdjustCompositionFont(
+ nsWindow* aWindow, const mozilla::WritingMode& aWritingMode,
+ bool aForceUpdate = false);
+
+ /**
+ * Get the current target clause of composition string.
+ * If there are one or more characters whose attribute is ATTR_TARGET_*,
+ * this returns the first character's offset and its length.
+ * Otherwise, e.g., the all characters are ATTR_INPUT, this returns
+ * the composition string range because the all is the current target.
+ *
+ * aLength can be null (default), but aOffset must not be null.
+ *
+ * The aOffset value is offset in the contents. So, when you need offset
+ * in the composition string, you need to subtract mCompositionStart from it.
+ */
+ bool GetTargetClauseRange(uint32_t* aOffset, uint32_t* aLength = nullptr);
+
+ /**
+ * DispatchEvent() dispatches aEvent if aWidget hasn't been destroyed yet.
+ */
+ static void DispatchEvent(nsWindow* aWindow, WidgetGUIEvent& aEvent);
+
+ /**
+ * DispatchCompositionChangeEvent() dispatches eCompositionChange event
+ * with clause information (it'll be retrieved by CreateTextRangeArray()).
+ * I.e., this should be called only during composing. If a composition is
+ * being committed, only HandleCompositionEnd() should be called.
+ *
+ * @param aWindow The window which has the composition.
+ * @param aContext Native IME context which has the composition.
+ */
+ void DispatchCompositionChangeEvent(nsWindow* aWindow,
+ const IMEContext& aContext);
+
+ nsresult EnsureClauseArray(int32_t aCount);
+ nsresult EnsureAttributeArray(int32_t aCount);
+
+ /**
+ * When WM_IME_CHAR is received and passed to DefWindowProc, we need to
+ * record the messages. In other words, we should record the messages
+ * when we receive WM_IME_CHAR on windowless plug-in (if we have focus,
+ * we always eat them). When focus is moved from a windowless plug-in to
+ * our window during composition, WM_IME_CHAR messages were received when
+ * the plug-in has focus. However, WM_CHAR messages are received after the
+ * plug-in lost focus. So, we need to ignore the WM_CHAR messages because
+ * they make unexpected text input events on us.
+ */
+ nsTArray<MSG> mPassedIMEChar;
+
+ bool IsIMECharRecordsEmpty() { return mPassedIMEChar.IsEmpty(); }
+ void ResetIMECharRecords() { mPassedIMEChar.Clear(); }
+ void DequeueIMECharRecords(WPARAM& wParam, LPARAM& lParam) {
+ MSG msg = mPassedIMEChar.ElementAt(0);
+ wParam = msg.wParam;
+ lParam = msg.lParam;
+ mPassedIMEChar.RemoveElementAt(0);
+ }
+ void EnqueueIMECharRecords(WPARAM wParam, LPARAM lParam) {
+ MSG msg;
+ msg.wParam = wParam;
+ msg.lParam = lParam;
+ mPassedIMEChar.AppendElement(msg);
+ }
+
+ TextEventDispatcher* GetTextEventDispatcherFor(nsWindow* aWindow);
+
+ nsWindow* mComposingWindow;
+ RefPtr<TextEventDispatcher> mDispatcher;
+ nsString mCompositionString;
+ nsTArray<uint32_t> mClauseArray;
+ nsTArray<uint8_t> mAttributeArray;
+
+ int32_t mCursorPosition;
+ uint32_t mCompositionStart;
+
+ // mContentSelection stores the latest selection data only when sHasFocus is
+ // true. Don't access mContentSelection directly. You should use
+ // GetContentSelectionWithQueryIfNothing() for getting proper state.
+ Maybe<ContentSelection> mContentSelection;
+
+ const Maybe<ContentSelection>& GetContentSelectionWithQueryIfNothing(
+ nsWindow* aWindow) {
+ // When IME has focus, mContentSelection is automatically updated by
+ // NOTIFY_IME_OF_SELECTION_CHANGE.
+ if (sHasFocus) {
+ if (mContentSelection.isNothing()) {
+ // But if this is the first access of mContentSelection, we need to
+ // query selection now.
+ mContentSelection = QueryContentSelection(aWindow);
+ }
+ return mContentSelection;
+ }
+ // Otherwise, i.e., While IME doesn't have focus, we cannot observe
+ // selection changes. So, in such case, we need to query selection
+ // when it's necessary.
+ static Maybe<ContentSelection> sTempContentSelection;
+ sTempContentSelection = QueryContentSelection(aWindow);
+ return sTempContentSelection;
+ }
+
+ /**
+ * Query content selection on aWindow with WidgetQueryContent event.
+ */
+ static Maybe<ContentSelection> QueryContentSelection(nsWindow* aWindow);
+
+ bool mIsComposing;
+
+ static mozilla::WritingMode sWritingModeOfCompositionFont;
+ static nsString sIMEName;
+ static UINT sCodePage;
+ static DWORD sIMEProperty;
+ static DWORD sIMEUIProperty;
+ static bool sAssumeVerticalWritingModeNotSupported;
+ static bool sHasFocus;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // IMMHandler_h_
diff --git a/widget/windows/InProcessWinCompositorWidget.cpp b/widget/windows/InProcessWinCompositorWidget.cpp
new file mode 100644
index 0000000000..de491b734b
--- /dev/null
+++ b/widget/windows/InProcessWinCompositorWidget.cpp
@@ -0,0 +1,368 @@
+/* -*- 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 "InProcessWinCompositorWidget.h"
+
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "gfxPlatform.h"
+#include "HeadlessCompositorWidget.h"
+#include "HeadlessWidget.h"
+#include "nsIWidget.h"
+#include "nsWindow.h"
+#include "VsyncDispatcher.h"
+#include "WinCompositorWindowThread.h"
+#include "VRShMem.h"
+
+#include <ddraw.h>
+
+namespace mozilla::widget {
+
+using namespace mozilla::gfx;
+using namespace mozilla;
+
+/* static */
+RefPtr<CompositorWidget> CompositorWidget::CreateLocal(
+ const CompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsIWidget* aWidget) {
+ if (aInitData.type() ==
+ CompositorWidgetInitData::THeadlessCompositorWidgetInitData) {
+ return new HeadlessCompositorWidget(
+ aInitData.get_HeadlessCompositorWidgetInitData(), aOptions,
+ static_cast<HeadlessWidget*>(aWidget));
+ } else {
+ return new InProcessWinCompositorWidget(
+ aInitData.get_WinCompositorWidgetInitData(), aOptions,
+ static_cast<nsWindow*>(aWidget));
+ }
+}
+
+InProcessWinCompositorWidget::InProcessWinCompositorWidget(
+ const WinCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, nsWindow* aWindow)
+ : WinCompositorWidget(aInitData, aOptions),
+ mWindow(aWindow),
+ mWnd(reinterpret_cast<HWND>(aInitData.hWnd())),
+ mTransparentSurfaceLock("mTransparentSurfaceLock"),
+ mTransparencyMode(uint32_t(aInitData.transparencyMode())),
+ mMemoryDC(nullptr),
+ mCompositeDC(nullptr),
+ mLockedBackBufferData(nullptr) {
+ MOZ_ASSERT(mWindow);
+ MOZ_ASSERT(mWnd && ::IsWindow(mWnd));
+
+ // mNotDeferEndRemoteDrawing is set on the main thread during init,
+ // but is only accessed after on the compositor thread.
+ mNotDeferEndRemoteDrawing =
+ StaticPrefs::layers_offmainthreadcomposition_frame_rate() == 0 ||
+ gfxPlatform::IsInLayoutAsapMode() || gfxPlatform::ForceSoftwareVsync();
+}
+
+void InProcessWinCompositorWidget::OnDestroyWindow() {
+ gfx::CriticalSectionAutoEnter presentLock(&mPresentLock);
+ MutexAutoLock lock(mTransparentSurfaceLock);
+ mTransparentSurface = nullptr;
+ mMemoryDC = nullptr;
+}
+
+bool InProcessWinCompositorWidget::OnWindowResize(
+ const LayoutDeviceIntSize& aSize) {
+ return true;
+}
+
+void InProcessWinCompositorWidget::OnWindowModeChange(nsSizeMode aSizeMode) {}
+
+bool InProcessWinCompositorWidget::PreRender(WidgetRenderingContext* aContext) {
+ // This can block waiting for WM_SETTEXT to finish
+ // Using PreRender is unnecessarily pessimistic because
+ // we technically only need to block during the present call
+ // not all of compositor rendering
+ mPresentLock.Enter();
+ return true;
+}
+
+void InProcessWinCompositorWidget::PostRender(
+ WidgetRenderingContext* aContext) {
+ mPresentLock.Leave();
+}
+
+LayoutDeviceIntSize InProcessWinCompositorWidget::GetClientSize() {
+ RECT r;
+ if (!::GetClientRect(mWnd, &r)) {
+ return LayoutDeviceIntSize();
+ }
+ return LayoutDeviceIntSize(r.right - r.left, r.bottom - r.top);
+}
+
+already_AddRefed<gfx::DrawTarget>
+InProcessWinCompositorWidget::StartRemoteDrawing() {
+ MutexAutoLock lock(mTransparentSurfaceLock);
+
+ MOZ_ASSERT(!mCompositeDC);
+
+ RefPtr<gfxASurface> surf;
+ if (TransparencyModeIs(TransparencyMode::Transparent)) {
+ surf = EnsureTransparentSurface();
+ }
+
+ // Must call this after EnsureTransparentSurface(), since it could update
+ // the DC.
+ HDC dc = GetWindowSurface();
+ if (!surf) {
+ if (!dc) {
+ return nullptr;
+ }
+ uint32_t flags = TransparencyModeIs(TransparencyMode::Opaque)
+ ? 0
+ : gfxWindowsSurface::FLAG_IS_TRANSPARENT;
+ surf = new gfxWindowsSurface(dc, flags);
+ }
+
+ IntSize size = surf->GetSize();
+ if (size.width <= 0 || size.height <= 0) {
+ if (dc) {
+ FreeWindowSurface(dc);
+ }
+ return nullptr;
+ }
+
+ RefPtr<DrawTarget> dt =
+ mozilla::gfx::Factory::CreateDrawTargetForCairoSurface(
+ surf->CairoSurface(), size);
+ if (dt) {
+ mCompositeDC = dc;
+ } else {
+ FreeWindowSurface(dc);
+ }
+
+ return dt.forget();
+}
+
+void InProcessWinCompositorWidget::EndRemoteDrawing() {
+ MOZ_ASSERT(!mLockedBackBufferData);
+
+ if (TransparencyModeIs(TransparencyMode::Transparent)) {
+ MOZ_ASSERT(mTransparentSurface);
+ RedrawTransparentWindow();
+ }
+ if (mCompositeDC) {
+ FreeWindowSurface(mCompositeDC);
+ }
+ mCompositeDC = nullptr;
+}
+
+bool InProcessWinCompositorWidget::NeedsToDeferEndRemoteDrawing() {
+ if (mNotDeferEndRemoteDrawing) {
+ return false;
+ }
+
+ IDirectDraw7* ddraw = DeviceManagerDx::Get()->GetDirectDraw();
+ if (!ddraw) {
+ return false;
+ }
+
+ DWORD scanLine = 0;
+ int height = ::GetSystemMetrics(SM_CYSCREEN);
+ HRESULT ret = ddraw->GetScanLine(&scanLine);
+ if (ret == DDERR_VERTICALBLANKINPROGRESS) {
+ scanLine = 0;
+ } else if (ret != DD_OK) {
+ return false;
+ }
+
+ // Check if there is a risk of tearing with GDI.
+ if (static_cast<int>(scanLine) > height / 2) {
+ // No need to defer.
+ return false;
+ }
+
+ return true;
+}
+
+already_AddRefed<gfx::DrawTarget>
+InProcessWinCompositorWidget::GetBackBufferDrawTarget(
+ gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect,
+ bool* aOutIsCleared) {
+ MOZ_ASSERT(!mLockedBackBufferData);
+
+ RefPtr<gfx::DrawTarget> target = CompositorWidget::GetBackBufferDrawTarget(
+ aScreenTarget, aRect, aOutIsCleared);
+ if (!target) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(target->GetBackendType() == BackendType::CAIRO);
+
+ uint8_t* destData;
+ IntSize destSize;
+ int32_t destStride;
+ SurfaceFormat destFormat;
+ if (!target->LockBits(&destData, &destSize, &destStride, &destFormat)) {
+ // LockBits is not supported. Use original DrawTarget.
+ return target.forget();
+ }
+
+ RefPtr<gfx::DrawTarget> dataTarget = Factory::CreateDrawTargetForData(
+ BackendType::CAIRO, destData, destSize, destStride, destFormat);
+ mLockedBackBufferData = destData;
+
+ return dataTarget.forget();
+}
+
+already_AddRefed<gfx::SourceSurface>
+InProcessWinCompositorWidget::EndBackBufferDrawing() {
+ if (mLockedBackBufferData) {
+ MOZ_ASSERT(mLastBackBuffer);
+ mLastBackBuffer->ReleaseBits(mLockedBackBufferData);
+ mLockedBackBufferData = nullptr;
+ }
+ return CompositorWidget::EndBackBufferDrawing();
+}
+
+bool InProcessWinCompositorWidget::InitCompositor(
+ layers::Compositor* aCompositor) {
+ return true;
+}
+
+void InProcessWinCompositorWidget::EnterPresentLock() { mPresentLock.Enter(); }
+
+void InProcessWinCompositorWidget::LeavePresentLock() { mPresentLock.Leave(); }
+
+RefPtr<gfxASurface> InProcessWinCompositorWidget::EnsureTransparentSurface() {
+ mTransparentSurfaceLock.AssertCurrentThreadOwns();
+ MOZ_ASSERT(TransparencyModeIs(TransparencyMode::Transparent));
+
+ IntSize size = GetClientSize().ToUnknownSize();
+ if (!mTransparentSurface || mTransparentSurface->GetSize() != size) {
+ mTransparentSurface = nullptr;
+ mMemoryDC = nullptr;
+ CreateTransparentSurface(size);
+ }
+
+ RefPtr<gfxASurface> surface = mTransparentSurface;
+ return surface.forget();
+}
+
+void InProcessWinCompositorWidget::CreateTransparentSurface(
+ const gfx::IntSize& aSize) {
+ mTransparentSurfaceLock.AssertCurrentThreadOwns();
+ MOZ_ASSERT(!mTransparentSurface && !mMemoryDC);
+ RefPtr<gfxWindowsSurface> surface =
+ new gfxWindowsSurface(aSize, SurfaceFormat::A8R8G8B8_UINT32);
+ mTransparentSurface = surface;
+ mMemoryDC = surface->GetDC();
+}
+
+void InProcessWinCompositorWidget::UpdateTransparency(TransparencyMode aMode) {
+ gfx::CriticalSectionAutoEnter presentLock(&mPresentLock);
+ MutexAutoLock lock(mTransparentSurfaceLock);
+ if (TransparencyModeIs(aMode)) {
+ return;
+ }
+
+ mTransparencyMode = uint32_t(aMode);
+ mTransparentSurface = nullptr;
+ mMemoryDC = nullptr;
+
+ if (aMode == TransparencyMode::Transparent) {
+ EnsureTransparentSurface();
+ }
+}
+
+void InProcessWinCompositorWidget::NotifyVisibilityUpdated(
+ nsSizeMode aSizeMode, bool aIsFullyOccluded) {
+ mSizeMode = aSizeMode;
+ mIsFullyOccluded = aIsFullyOccluded;
+}
+
+nsSizeMode InProcessWinCompositorWidget::GetWindowSizeMode() const {
+ nsSizeMode sizeMode = mSizeMode;
+ return sizeMode;
+}
+
+bool InProcessWinCompositorWidget::GetWindowIsFullyOccluded() const {
+ bool isFullyOccluded = mIsFullyOccluded;
+ return isFullyOccluded;
+}
+
+void InProcessWinCompositorWidget::ClearTransparentWindow() {
+ gfx::CriticalSectionAutoEnter presentLock(&mPresentLock);
+ MutexAutoLock lock(mTransparentSurfaceLock);
+ if (!mTransparentSurface) {
+ return;
+ }
+
+ EnsureTransparentSurface();
+
+ IntSize size = mTransparentSurface->GetSize();
+ if (!size.IsEmpty()) {
+ RefPtr<DrawTarget> drawTarget =
+ gfxPlatform::CreateDrawTargetForSurface(mTransparentSurface, size);
+ if (!drawTarget) {
+ return;
+ }
+ drawTarget->ClearRect(Rect(0, 0, size.width, size.height));
+ RedrawTransparentWindow();
+ }
+}
+
+bool InProcessWinCompositorWidget::RedrawTransparentWindow() {
+ MOZ_ASSERT(TransparencyModeIs(TransparencyMode::Transparent));
+
+ LayoutDeviceIntSize size = GetClientSize();
+
+ ::GdiFlush();
+
+ BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
+ SIZE winSize = {size.width, size.height};
+ POINT srcPos = {0, 0};
+ HWND hWnd = WinUtils::GetTopLevelHWND(mWnd, true);
+ RECT winRect;
+ ::GetWindowRect(hWnd, &winRect);
+
+ // perform the alpha blend
+ return !!::UpdateLayeredWindow(hWnd, nullptr, (POINT*)&winRect, &winSize,
+ mMemoryDC, &srcPos, 0, &bf, ULW_ALPHA);
+}
+
+HDC InProcessWinCompositorWidget::GetWindowSurface() {
+ return TransparencyModeIs(TransparencyMode::Transparent) ? mMemoryDC
+ : ::GetDC(mWnd);
+}
+
+void InProcessWinCompositorWidget::FreeWindowSurface(HDC dc) {
+ if (!TransparencyModeIs(TransparencyMode::Transparent)) {
+ ::ReleaseDC(mWnd, dc);
+ }
+}
+
+bool InProcessWinCompositorWidget::IsHidden() const { return ::IsIconic(mWnd); }
+
+nsIWidget* InProcessWinCompositorWidget::RealWidget() { return mWindow; }
+
+void InProcessWinCompositorWidget::ObserveVsync(VsyncObserver* aObserver) {
+ if (RefPtr<CompositorVsyncDispatcher> cvd =
+ mWindow->GetCompositorVsyncDispatcher()) {
+ cvd->SetCompositorVsyncObserver(aObserver);
+ }
+}
+
+void InProcessWinCompositorWidget::UpdateCompositorWnd(
+ const HWND aCompositorWnd, const HWND aParentWnd) {
+ MOZ_ASSERT(layers::CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(aCompositorWnd && aParentWnd);
+ MOZ_ASSERT(aParentWnd == mWnd);
+
+ // Since we're in the parent process anyway, we can just call SetParent
+ // directly.
+ ::SetParent(aCompositorWnd, aParentWnd);
+ mSetParentCompleted = true;
+}
+} // namespace mozilla::widget
diff --git a/widget/windows/InProcessWinCompositorWidget.h b/widget/windows/InProcessWinCompositorWidget.h
new file mode 100644
index 0000000000..e287204461
--- /dev/null
+++ b/widget/windows/InProcessWinCompositorWidget.h
@@ -0,0 +1,111 @@
+/* -*- 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/. */
+
+#ifndef widget_windows_InProcessWinCompositorWidget_h
+#define widget_windows_InProcessWinCompositorWidget_h
+
+#include "WinCompositorWidget.h"
+
+class nsWindow;
+class gfxASurface;
+
+namespace mozilla::widget {
+
+// This is the Windows-specific implementation of CompositorWidget. For
+// the most part it only requires an HWND, however it maintains extra state
+// for transparent windows, as well as for synchronizing WM_SETTEXT messages
+// with the compositor.
+class InProcessWinCompositorWidget final
+ : public WinCompositorWidget,
+ public PlatformCompositorWidgetDelegate {
+ public:
+ InProcessWinCompositorWidget(const WinCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions,
+ nsWindow* aWindow);
+
+ bool PreRender(WidgetRenderingContext*) override;
+ void PostRender(WidgetRenderingContext*) override;
+ already_AddRefed<gfx::DrawTarget> StartRemoteDrawing() override;
+ void EndRemoteDrawing() override;
+ bool NeedsToDeferEndRemoteDrawing() override;
+ LayoutDeviceIntSize GetClientSize() override;
+ already_AddRefed<gfx::DrawTarget> GetBackBufferDrawTarget(
+ gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect,
+ bool* aOutIsCleared) override;
+ already_AddRefed<gfx::SourceSurface> EndBackBufferDrawing() override;
+ bool InitCompositor(layers::Compositor* aCompositor) override;
+ CompositorWidgetDelegate* AsDelegate() override { return this; }
+ bool IsHidden() const override;
+
+ // PlatformCompositorWidgetDelegate Overrides
+
+ void EnterPresentLock() override;
+ void LeavePresentLock() override;
+ void OnDestroyWindow() override;
+ bool OnWindowResize(const LayoutDeviceIntSize& aSize) override;
+ void OnWindowModeChange(nsSizeMode aSizeMode) override;
+ void UpdateTransparency(TransparencyMode aMode) override;
+ void NotifyVisibilityUpdated(nsSizeMode aSizeMode,
+ bool aIsFullyOccluded) override;
+ void ClearTransparentWindow() override;
+
+ bool RedrawTransparentWindow();
+
+ // Ensure that a transparent surface exists, then return it.
+ RefPtr<gfxASurface> EnsureTransparentSurface();
+
+ HDC GetTransparentDC() const { return mMemoryDC; }
+
+ mozilla::Mutex& GetTransparentSurfaceLock() {
+ return mTransparentSurfaceLock;
+ }
+
+ nsSizeMode GetWindowSizeMode() const override;
+ bool GetWindowIsFullyOccluded() const override;
+
+ void ObserveVsync(VsyncObserver* aObserver) override;
+ nsIWidget* RealWidget() override;
+
+ void UpdateCompositorWnd(const HWND aCompositorWnd,
+ const HWND aParentWnd) override;
+ void SetRootLayerTreeID(const layers::LayersId& aRootLayerTreeId) override {}
+
+ private:
+ HDC GetWindowSurface();
+ void FreeWindowSurface(HDC dc);
+
+ void CreateTransparentSurface(const gfx::IntSize& aSize);
+
+ nsWindow* mWindow;
+
+ HWND mWnd;
+
+ gfx::CriticalSection mPresentLock;
+
+ // Transparency handling.
+ mozilla::Mutex mTransparentSurfaceLock MOZ_UNANNOTATED;
+ mozilla::Atomic<uint32_t, MemoryOrdering::Relaxed> mTransparencyMode;
+
+ bool TransparencyModeIs(TransparencyMode aMode) const {
+ return TransparencyMode(uint32_t(mTransparencyMode)) == aMode;
+ }
+
+ // Visibility handling.
+ mozilla::Atomic<nsSizeMode, MemoryOrdering::Relaxed> mSizeMode;
+ mozilla::Atomic<bool, MemoryOrdering::Relaxed> mIsFullyOccluded;
+
+ RefPtr<gfxASurface> mTransparentSurface;
+ HDC mMemoryDC;
+ HDC mCompositeDC;
+
+ // Locked back buffer of BasicCompositor
+ uint8_t* mLockedBackBufferData;
+
+ bool mNotDeferEndRemoteDrawing;
+};
+
+} // namespace mozilla::widget
+
+#endif // widget_windows_InProcessWinCompositorWidget_h
diff --git a/widget/windows/InputDeviceUtils.cpp b/widget/windows/InputDeviceUtils.cpp
new file mode 100644
index 0000000000..3636b93d6a
--- /dev/null
+++ b/widget/windows/InputDeviceUtils.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 "InputDeviceUtils.h"
+
+#define INITGUID
+#include <dbt.h>
+#include <hidclass.h>
+#include <ntddmou.h>
+#include <setupapi.h>
+
+namespace mozilla {
+namespace widget {
+
+HDEVNOTIFY
+InputDeviceUtils::RegisterNotification(HWND aHwnd) {
+ DEV_BROADCAST_DEVICEINTERFACE filter = {};
+
+ filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
+ filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
+ // Some touchsreen devices are not GUID_DEVINTERFACE_MOUSE, so here we use
+ // GUID_DEVINTERFACE_HID instead.
+ filter.dbcc_classguid = GUID_DEVINTERFACE_HID;
+ return RegisterDeviceNotification(aHwnd, &filter,
+ DEVICE_NOTIFY_WINDOW_HANDLE);
+}
+
+void InputDeviceUtils::UnregisterNotification(HDEVNOTIFY aHandle) {
+ if (!aHandle) {
+ return;
+ }
+ UnregisterDeviceNotification(aHandle);
+}
+
+DWORD
+InputDeviceUtils::CountMouseDevices() {
+ HDEVINFO hdev =
+ SetupDiGetClassDevs(&GUID_DEVINTERFACE_MOUSE, nullptr, nullptr,
+ DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
+ if (hdev == INVALID_HANDLE_VALUE) {
+ return 0;
+ }
+
+ DWORD count = 0;
+ SP_INTERFACE_DEVICE_DATA info = {};
+ info.cbSize = sizeof(SP_INTERFACE_DEVICE_DATA);
+ while (SetupDiEnumDeviceInterfaces(hdev, nullptr, &GUID_DEVINTERFACE_MOUSE,
+ count, &info)) {
+ if (info.Flags & SPINT_ACTIVE) {
+ count++;
+ }
+ }
+ SetupDiDestroyDeviceInfoList(hdev);
+ return count;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/InputDeviceUtils.h b/widget/windows/InputDeviceUtils.h
new file mode 100644
index 0000000000..d16698a03a
--- /dev/null
+++ b/widget/windows/InputDeviceUtils.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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/. */
+
+#ifndef mozilla_widget_InputDeviceUtils_h__
+#define mozilla_widget_InputDeviceUtils_h__
+
+#include <windows.h>
+
+namespace mozilla {
+namespace widget {
+
+class InputDeviceUtils {
+ public:
+ static HDEVNOTIFY RegisterNotification(HWND aHwnd);
+ static void UnregisterNotification(HDEVNOTIFY aHandle);
+
+ // Returns the number of mouse type devices connected to this system.
+ static DWORD CountMouseDevices();
+};
+
+} // namespace widget
+} // namespace mozilla
+#endif // mozilla_widget_InputDeviceUtils_h__
diff --git a/widget/windows/JumpListBuilder.cpp b/widget/windows/JumpListBuilder.cpp
new file mode 100644
index 0000000000..80b1c29aa7
--- /dev/null
+++ b/widget/windows/JumpListBuilder.cpp
@@ -0,0 +1,818 @@
+/* -*- 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 <windows.h>
+#include <shobjidl.h>
+#include <propkey.h>
+#include <propvarutil.h>
+#include <shellapi.h>
+#include "JumpListBuilder.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h"
+#include "mozilla/mscom/EnsureMTA.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsServiceManagerUtils.h"
+#include "WinUtils.h"
+
+using mozilla::dom::Promise;
+using mozilla::dom::WindowsJumpListShortcutDescription;
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(JumpListBuilder, nsIJumpListBuilder, nsIObserver)
+
+#define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change"
+#define TOPIC_CLEAR_PRIVATE_DATA "clear-private-data"
+
+// The amount of time, in milliseconds, that our IO thread will stay alive after
+// the last event it processes.
+#define DEFAULT_THREAD_TIMEOUT_MS 30000
+
+const char kPrefTaskbarEnabled[] = "browser.taskbar.lists.enabled";
+
+/**
+ * A wrapper around a ICustomDestinationList that implements the JumpListBackend
+ * interface. This is an implementation of JumpListBackend that actually causes
+ * items to appear in a Windows jump list.
+ */
+class NativeJumpListBackend : public JumpListBackend {
+ // We use NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET because this
+ // class might be destroyed on a different thread than the one it
+ // was created on, since it's maintained by a LazyIdleThread.
+ //
+ // This is a workaround for bug 1648031.
+ NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(JumpListBackend, override)
+
+ NativeJumpListBackend() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ mscom::EnsureMTA([&]() {
+ RefPtr<ICustomDestinationList> destList;
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_DestinationList, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ICustomDestinationList, getter_AddRefs(destList));
+ if (FAILED(hr)) {
+ return;
+ }
+
+ mWindowsDestList = destList;
+ });
+ }
+
+ virtual bool IsAvailable() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ return mWindowsDestList != nullptr;
+ }
+
+ virtual HRESULT SetAppID(LPCWSTR pszAppID) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->SetAppID(pszAppID);
+ }
+
+ virtual HRESULT BeginList(UINT* pcMinSlots, REFIID riid,
+ void** ppv) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->BeginList(pcMinSlots, riid, ppv);
+ }
+
+ virtual HRESULT AddUserTasks(IObjectArray* poa) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->AddUserTasks(poa);
+ }
+
+ virtual HRESULT AppendCategory(LPCWSTR pszCategory,
+ IObjectArray* poa) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->AppendCategory(pszCategory, poa);
+ }
+
+ virtual HRESULT CommitList() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->CommitList();
+ }
+
+ virtual HRESULT AbortList() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->AbortList();
+ }
+
+ virtual HRESULT DeleteList(LPCWSTR pszAppID) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->DeleteList(pszAppID);
+ }
+
+ virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mWindowsDestList);
+
+ return mWindowsDestList->AppendKnownCategory(category);
+ }
+
+ protected:
+ virtual ~NativeJumpListBackend() override{};
+
+ private:
+ RefPtr<ICustomDestinationList> mWindowsDestList;
+};
+
+JumpListBuilder::JumpListBuilder(const nsAString& aAppUserModelId,
+ RefPtr<JumpListBackend> aTestingBackend) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mAppUserModelId.Assign(aAppUserModelId);
+
+ Preferences::AddStrongObserver(this, kPrefTaskbarEnabled);
+
+ // Make a lazy thread for any IO.
+ mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "Jump List",
+ LazyIdleThread::ManualShutdown);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService("@mozilla.org/observer-service;1");
+ if (observerService) {
+ observerService->AddObserver(this, TOPIC_PROFILE_BEFORE_CHANGE, false);
+ observerService->AddObserver(this, TOPIC_CLEAR_PRIVATE_DATA, false);
+ }
+
+ nsCOMPtr<nsIRunnable> runnable;
+
+ if (aTestingBackend) {
+ // Dispatch a task that hands a reference to the testing backend
+ // to the background thread. The testing backend was probably
+ // constructed on the main thread, and is responsible for doing
+ // any locking as well as cleanup.
+ runnable = NewRunnableMethod<RefPtr<JumpListBackend>>(
+ "SetupTestingBackend", this, &JumpListBuilder::DoSetupTestingBackend,
+ aTestingBackend);
+
+ } else {
+ // Dispatch a task that constructs the native jump list backend.
+ runnable = NewRunnableMethod("SetupBackend", this,
+ &JumpListBuilder::DoSetupBackend);
+ }
+
+ mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+
+ // MSIX packages explicitly do not support setting the appid from within
+ // the app, as it is set in the package manifest instead.
+ if (!mozilla::widget::WinUtils::HasPackageIdentity()) {
+ mIOThread->Dispatch(
+ NewRunnableMethod<nsString>(
+ "SetAppID", this, &JumpListBuilder::DoSetAppID, aAppUserModelId),
+ NS_DISPATCH_NORMAL);
+ }
+}
+
+JumpListBuilder::~JumpListBuilder() {
+ Preferences::RemoveObserver(this, kPrefTaskbarEnabled);
+}
+
+void JumpListBuilder::DoSetupTestingBackend(
+ RefPtr<JumpListBackend> aTestingBackend) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ mJumpListBackend = aTestingBackend;
+}
+
+void JumpListBuilder::DoSetupBackend() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(!mJumpListBackend);
+ mJumpListBackend = new NativeJumpListBackend();
+}
+
+void JumpListBuilder::DoShutdownBackend() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ mJumpListBackend = nullptr;
+}
+
+void JumpListBuilder::DoSetAppID(nsString aAppUserModelID) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mJumpListBackend);
+ mJumpListBackend->SetAppID(aAppUserModelID.get());
+}
+
+NS_IMETHODIMP
+JumpListBuilder::ObtainAndCacheFavicon(nsIURI* aFaviconURI,
+ nsAString& aCachedIconPath) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsString iconFilePath;
+ nsresult rv = mozilla::widget::FaviconHelper::ObtainCachedIconFile(
+ aFaviconURI, iconFilePath, mIOThread, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aCachedIconPath = iconFilePath;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+JumpListBuilder::IsAvailable(JSContext* aCx, Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(mIOThread);
+
+ ErrorResult result;
+ RefPtr<Promise> promise =
+ Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
+
+ if (MOZ_UNLIKELY(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsMainThreadPtrHandle<Promise> promiseHolder(
+ new nsMainThreadPtrHolder<Promise>("JumpListBuilder::IsAvailable promise",
+ promise));
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod<nsMainThreadPtrHandle<Promise>>(
+ "IsAvailable", this, &JumpListBuilder::DoIsAvailable,
+ std::move(promiseHolder));
+ nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+JumpListBuilder::CheckForRemovals(JSContext* aCx, Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(mIOThread);
+
+ ErrorResult result;
+ RefPtr<Promise> promise =
+ Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
+
+ if (MOZ_UNLIKELY(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsMainThreadPtrHandle<Promise> promiseHolder(
+ new nsMainThreadPtrHolder<Promise>(
+ "JumpListBuilder::CheckForRemovals promise", promise));
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod<nsMainThreadPtrHandle<Promise>>(
+ "CheckForRemovals", this, &JumpListBuilder::DoCheckForRemovals,
+ std::move(promiseHolder));
+
+ nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(aPromise);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+JumpListBuilder::PopulateJumpList(
+ const nsTArray<JS::Value>& aTaskDescriptions, const nsAString& aCustomTitle,
+ const nsTArray<JS::Value>& aCustomDescriptions, JSContext* aCx,
+ Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(mIOThread);
+
+ if (aCustomDescriptions.Length() && aCustomTitle.IsEmpty()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Get rid of the old icons
+ nsCOMPtr<nsIRunnable> event =
+ new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true);
+ mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+
+ nsTArray<WindowsJumpListShortcutDescription> taskDescs;
+ for (auto& jsval : aTaskDescriptions) {
+ JS::Rooted<JS::Value> rootedVal(aCx);
+ if (NS_WARN_IF(!dom::ToJSValue(aCx, jsval, &rootedVal))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ WindowsJumpListShortcutDescription desc;
+ if (!desc.Init(aCx, rootedVal)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ taskDescs.AppendElement(std::move(desc));
+ }
+
+ nsTArray<WindowsJumpListShortcutDescription> customDescs;
+ for (auto& jsval : aCustomDescriptions) {
+ JS::Rooted<JS::Value> rootedVal(aCx);
+ if (NS_WARN_IF(!dom::ToJSValue(aCx, jsval, &rootedVal))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ WindowsJumpListShortcutDescription desc;
+ if (!desc.Init(aCx, rootedVal)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ customDescs.AppendElement(std::move(desc));
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise =
+ Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
+
+ if (MOZ_UNLIKELY(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsMainThreadPtrHandle<Promise> promiseHolder(
+ new nsMainThreadPtrHolder<Promise>(
+ "JumpListBuilder::PopulateJumpList promise", promise));
+
+ nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<
+ StoreCopyPassByRRef<nsTArray<WindowsJumpListShortcutDescription>>,
+ nsString,
+ StoreCopyPassByRRef<nsTArray<WindowsJumpListShortcutDescription>>,
+ nsMainThreadPtrHandle<Promise>>(
+ "PopulateJumpList", this, &JumpListBuilder::DoPopulateJumpList,
+ std::move(taskDescs), aCustomTitle, std::move(customDescs),
+ std::move(promiseHolder));
+ nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(aPromise);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+JumpListBuilder::ClearJumpList(JSContext* aCx, Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(mIOThread);
+
+ ErrorResult result;
+ RefPtr<Promise> promise =
+ Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
+
+ if (MOZ_UNLIKELY(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsMainThreadPtrHandle<Promise> promiseHolder(
+ new nsMainThreadPtrHolder<Promise>(
+ "JumpListBuilder::ClearJumpList promise", promise));
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod<nsMainThreadPtrHandle<Promise>>(
+ "ClearJumpList", this, &JumpListBuilder::DoClearJumpList,
+ std::move(promiseHolder));
+ nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+void JumpListBuilder::DoIsAvailable(
+ const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aPromiseHolder);
+
+ if (!mJumpListBackend) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoIsAvailable", [promiseHolder = std::move(aPromiseHolder)]() {
+ promiseHolder.get()->MaybeResolve(false);
+ }));
+ return;
+ }
+
+ bool isAvailable = mJumpListBackend->IsAvailable();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoIsAvailable",
+ [promiseHolder = std::move(aPromiseHolder), isAvailable]() {
+ promiseHolder.get()->MaybeResolve(isAvailable);
+ }));
+}
+
+void JumpListBuilder::DoCheckForRemovals(
+ const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aPromiseHolder);
+
+ nsresult rv = NS_ERROR_UNEXPECTED;
+
+ auto errorHandler = MakeScopeExit([&aPromiseHolder, &rv]() {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoCheckForRemovals",
+ [promiseHolder = std::move(aPromiseHolder), rv]() {
+ promiseHolder.get()->MaybeReject(rv);
+ }));
+ });
+
+ MOZ_ASSERT(mJumpListBackend);
+ if (!mJumpListBackend) {
+ return;
+ }
+
+ // Abort any ongoing list building that might not have been committed,
+ // otherwise BeginList will give us problems.
+ Unused << mJumpListBackend->AbortList();
+
+ nsTArray<nsString> urisToRemove;
+ RefPtr<IObjectArray> objArray;
+ uint32_t maxItems = 0;
+
+ HRESULT hr = mJumpListBackend->BeginList(
+ &maxItems,
+ IID_PPV_ARGS(static_cast<IObjectArray**>(getter_AddRefs(objArray))));
+
+ if (FAILED(hr)) {
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ RemoveIconCacheAndGetJumplistShortcutURIs(objArray, urisToRemove);
+
+ // We began a list in order to get the removals, which we can now abort.
+ Unused << mJumpListBackend->AbortList();
+
+ errorHandler.release();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoCheckForRemovals", [urisToRemove = std::move(urisToRemove),
+ promiseHolder = std::move(aPromiseHolder)]() {
+ promiseHolder.get()->MaybeResolve(urisToRemove);
+ }));
+}
+
+void JumpListBuilder::DoPopulateJumpList(
+ const nsTArray<WindowsJumpListShortcutDescription>&& aTaskDescriptions,
+ const nsAString& aCustomTitle,
+ const nsTArray<WindowsJumpListShortcutDescription>&& aCustomDescriptions,
+ const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aPromiseHolder);
+
+ nsresult rv = NS_ERROR_UNEXPECTED;
+
+ auto errorHandler = MakeScopeExit([&aPromiseHolder, &rv]() {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoPopulateJumpList",
+ [promiseHolder = std::move(aPromiseHolder), rv]() {
+ promiseHolder.get()->MaybeReject(rv);
+ }));
+ });
+
+ MOZ_ASSERT(mJumpListBackend);
+ if (!mJumpListBackend) {
+ return;
+ }
+
+ // Abort any ongoing list building that might not have been committed,
+ // otherwise BeginList will give us problems.
+ Unused << mJumpListBackend->AbortList();
+
+ nsTArray<nsString> urisToRemove;
+ RefPtr<IObjectArray> objArray;
+ uint32_t maxItems = 0;
+
+ HRESULT hr = mJumpListBackend->BeginList(
+ &maxItems,
+ IID_PPV_ARGS(static_cast<IObjectArray**>(getter_AddRefs(objArray))));
+
+ if (FAILED(hr)) {
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ if (urisToRemove.Length()) {
+ // It'd be nice if we could return a more descriptive error here so that
+ // the caller knows that they should have called checkForRemovals first.
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ if (aTaskDescriptions.Length()) {
+ RefPtr<IObjectCollection> taskCollection;
+ hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
+ CLSCTX_INPROC_SERVER, IID_IObjectCollection,
+ getter_AddRefs(taskCollection));
+ if (FAILED(hr)) {
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ // Start by building the task list
+ for (auto& desc : aTaskDescriptions) {
+ // These should all be ShellLinks
+ RefPtr<IShellLinkW> link;
+ rv = GetShellLinkFromDescription(desc, link);
+ if (NS_FAILED(rv)) {
+ // Let the errorHandler deal with this.
+ return;
+ }
+ taskCollection->AddObject(link);
+ }
+
+ RefPtr<IObjectArray> pTaskArray;
+ hr = taskCollection->QueryInterface(IID_IObjectArray,
+ getter_AddRefs(pTaskArray));
+
+ if (FAILED(hr)) {
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ hr = mJumpListBackend->AddUserTasks(pTaskArray);
+
+ if (FAILED(hr)) {
+ rv = NS_ERROR_FAILURE;
+ return;
+ }
+ }
+
+ if (aCustomDescriptions.Length()) {
+ // Then build the custom list
+ RefPtr<IObjectCollection> customCollection;
+ hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
+ CLSCTX_INPROC_SERVER, IID_IObjectCollection,
+ getter_AddRefs(customCollection));
+ if (FAILED(hr)) {
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ for (auto& desc : aCustomDescriptions) {
+ // These should all be ShellLinks
+ RefPtr<IShellLinkW> link;
+ rv = GetShellLinkFromDescription(desc, link);
+ if (NS_FAILED(rv)) {
+ // Let the errorHandler deal with this.
+ return;
+ }
+ customCollection->AddObject(link);
+ }
+
+ RefPtr<IObjectArray> pCustomArray;
+ hr = customCollection->QueryInterface(IID_IObjectArray,
+ getter_AddRefs(pCustomArray));
+ if (FAILED(hr)) {
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ hr = mJumpListBackend->AppendCategory(
+ reinterpret_cast<const wchar_t*>(aCustomTitle.BeginReading()),
+ pCustomArray);
+
+ if (FAILED(hr)) {
+ rv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+ }
+
+ hr = mJumpListBackend->CommitList();
+ if (FAILED(hr)) {
+ rv = NS_ERROR_FAILURE;
+ return;
+ }
+
+ errorHandler.release();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoPopulateJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
+ promiseHolder.get()->MaybeResolveWithUndefined();
+ }));
+}
+
+void JumpListBuilder::DoClearJumpList(
+ const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aPromiseHolder);
+
+ if (!mJumpListBackend) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
+ promiseHolder.get()->MaybeReject(NS_ERROR_UNEXPECTED);
+ }));
+ return;
+ }
+
+ if (SUCCEEDED(mJumpListBackend->DeleteList(mAppUserModelId.get()))) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
+ promiseHolder.get()->MaybeResolveWithUndefined();
+ }));
+ } else {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
+ promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
+ }));
+ }
+}
+
+// RemoveIconCacheAndGetJumplistShortcutURIs - does multiple things to
+// avoid unnecessary extra XPCOM incantations. For each object in the input
+// array, if it's an IShellLinkW, this deletes the cached icon and adds the
+// url param to a list of URLs to be removed from the places database.
+void JumpListBuilder::RemoveIconCacheAndGetJumplistShortcutURIs(
+ IObjectArray* aObjArray, nsTArray<nsString>& aURISpecs) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Early return here just in case some versions of Windows don't populate this
+ if (!aObjArray) {
+ return;
+ }
+
+ uint32_t count = 0;
+ aObjArray->GetCount(&count);
+
+ for (uint32_t idx = 0; idx < count; idx++) {
+ RefPtr<IShellLinkW> pLink;
+
+ if (FAILED(aObjArray->GetAt(idx, IID_IShellLinkW,
+ static_cast<void**>(getter_AddRefs(pLink))))) {
+ continue;
+ }
+
+ wchar_t buf[MAX_PATH];
+ HRESULT hres = pLink->GetArguments(buf, MAX_PATH);
+ if (SUCCEEDED(hres)) {
+ LPWSTR* arglist;
+ int32_t numArgs;
+
+ arglist = ::CommandLineToArgvW(buf, &numArgs);
+ if (arglist && numArgs > 0) {
+ nsString spec(arglist[0]);
+ aURISpecs.AppendElement(std::move(spec));
+ ::LocalFree(arglist);
+ }
+ }
+
+ int iconIdx = 0;
+ hres = pLink->GetIconLocation(buf, MAX_PATH, &iconIdx);
+ if (SUCCEEDED(hres)) {
+ nsDependentString spec(buf);
+ DeleteIconFromDisk(spec);
+ }
+ }
+}
+
+void JumpListBuilder::DeleteIconFromDisk(const nsAString& aPath) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Check that we aren't deleting some arbitrary file that is not an icon
+ if (StringTail(aPath, 4).LowerCaseEqualsASCII(".ico")) {
+ // Construct the parent path of the passed in path
+ nsCOMPtr<nsIFile> icoFile;
+ nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(icoFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ icoFile->Remove(false);
+ }
+}
+
+// Converts a WindowsJumpListShortcutDescription into a IShellLinkW
+nsresult JumpListBuilder::GetShellLinkFromDescription(
+ const WindowsJumpListShortcutDescription& aDesc,
+ RefPtr<IShellLinkW>& aShellLink) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ HRESULT hr;
+ IShellLinkW* psl;
+
+ // Shell links:
+ // http://msdn.microsoft.com/en-us/library/bb776891(VS.85).aspx
+ // http://msdn.microsoft.com/en-us/library/bb774950(VS.85).aspx
+
+ // Create a IShellLink
+ hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, (LPVOID*)&psl);
+ if (FAILED(hr)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Store the title of the app
+ if (!aDesc.mTitle.IsEmpty()) {
+ IPropertyStore* pPropStore = nullptr;
+ hr = psl->QueryInterface(IID_IPropertyStore, (LPVOID*)&pPropStore);
+ if (FAILED(hr)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ PROPVARIANT pv;
+ ::InitPropVariantFromString(aDesc.mTitle.get(), &pv);
+
+ pPropStore->SetValue(PKEY_Title, pv);
+ pPropStore->Commit();
+ pPropStore->Release();
+
+ PropVariantClear(&pv);
+ }
+
+ // Store the rest of the params
+ hr = psl->SetPath(aDesc.mPath.get());
+
+ // According to the documentation at [1], the maximum description length is
+ // INFOTIPSIZE, so we copy the const string from the description into a buffer
+ // of that maximum size. However, testing reveals that INFOTIPSIZE is still
+ // sometimes too large. MAX_PATH seems to work instead.
+ //
+ // We truncate to MAX_PATH - 1, since nsAutoString's don't include the null
+ // character in their capacity calculations, but the string for IShellLinkW
+ // description does. So by truncating to MAX_PATH - 1, the full contents of
+ // the truncated nsAutoString will be copied into the IShellLink description
+ // buffer.
+ //
+ // [1]:
+ // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinka-setdescription
+ nsAutoString descriptionCopy(aDesc.mDescription.get());
+ if (descriptionCopy.Length() >= MAX_PATH) {
+ descriptionCopy.Truncate(MAX_PATH - 1);
+ }
+
+ hr = psl->SetDescription(descriptionCopy.get());
+
+ if (aDesc.mArguments.WasPassed() && !aDesc.mArguments.Value().IsEmpty()) {
+ hr = psl->SetArguments(aDesc.mArguments.Value().get());
+ } else {
+ hr = psl->SetArguments(L"");
+ }
+
+ // Set up the fallback icon in the event that a valid icon URI has
+ // not been supplied.
+ hr = psl->SetIconLocation(aDesc.mPath.get(), aDesc.mFallbackIconIndex);
+
+ if (aDesc.mIconPath.WasPassed() && !aDesc.mIconPath.Value().IsEmpty()) {
+ // Always use the first icon in the ICO file, as our encoded icon only has 1
+ // resource
+ hr = psl->SetIconLocation(aDesc.mIconPath.Value().get(), 0);
+ }
+
+ aShellLink = dont_AddRef(psl);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+JumpListBuilder::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG_POINTER(aTopic);
+ if (strcmp(aTopic, TOPIC_PROFILE_BEFORE_CHANGE) == 0) {
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService("@mozilla.org/observer-service;1");
+ if (observerService) {
+ observerService->RemoveObserver(this, TOPIC_PROFILE_BEFORE_CHANGE);
+ observerService->RemoveObserver(this, TOPIC_CLEAR_PRIVATE_DATA);
+ }
+
+ mIOThread->Dispatch(NewRunnableMethod("ShutdownBackend", this,
+ &JumpListBuilder::DoShutdownBackend),
+ NS_DISPATCH_NORMAL);
+
+ mIOThread->Shutdown();
+ } else if (strcmp(aTopic, "nsPref:changed") == 0 &&
+ nsDependentString(aData).EqualsASCII(kPrefTaskbarEnabled)) {
+ bool enabled = Preferences::GetBool(kPrefTaskbarEnabled, true);
+ if (!enabled) {
+ nsCOMPtr<nsIRunnable> event =
+ new mozilla::widget::AsyncDeleteAllFaviconsFromDisk();
+ mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ }
+ } else if (strcmp(aTopic, TOPIC_CLEAR_PRIVATE_DATA) == 0) {
+ // Delete JumpListCache icons from Disk, if any.
+ nsCOMPtr<nsIRunnable> event =
+ new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(false);
+ mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ }
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/JumpListBuilder.h b/widget/windows/JumpListBuilder.h
new file mode 100644
index 0000000000..e228669f11
--- /dev/null
+++ b/widget/windows/JumpListBuilder.h
@@ -0,0 +1,102 @@
+/* -*- 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/. */
+
+#ifndef __JumpListBuilder_h__
+#define __JumpListBuilder_h__
+
+#include "nsIJumpListBuilder.h"
+
+#include "nsIObserver.h"
+#include "nsProxyRelease.h"
+#include "mozilla/LazyIdleThread.h"
+
+namespace mozilla {
+
+namespace dom {
+struct WindowsJumpListShortcutDescription;
+} // namespace dom
+
+namespace widget {
+
+/**
+ * This is an abstract class for a backend to write to the Windows Jump List.
+ *
+ * It has a 1-to-1 method mapping with ICustomDestinationList. The abtract
+ * class allows us to implement a "fake" backend for automated testing.
+ */
+class JumpListBackend {
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual bool IsAvailable() = 0;
+
+ virtual HRESULT SetAppID(LPCWSTR pszAppID) = 0;
+ virtual HRESULT BeginList(UINT* pcMinSlots, REFIID riid, void** ppv) = 0;
+ virtual HRESULT AddUserTasks(IObjectArray* poa) = 0;
+ virtual HRESULT AppendCategory(LPCWSTR pszCategory, IObjectArray* poa) = 0;
+ virtual HRESULT CommitList() = 0;
+ virtual HRESULT AbortList() = 0;
+ virtual HRESULT DeleteList(LPCWSTR pszAppID) = 0;
+ virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) = 0;
+
+ protected:
+ virtual ~JumpListBackend() {}
+};
+
+/**
+ * JumpListBuilder is a component that can be used to manage the Windows
+ * Jump List off of the main thread.
+ */
+class JumpListBuilder : public nsIJumpListBuilder, public nsIObserver {
+ virtual ~JumpListBuilder();
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIJUMPLISTBUILDER
+ NS_DECL_NSIOBSERVER
+
+ explicit JumpListBuilder(const nsAString& aAppUserModelId,
+ RefPtr<JumpListBackend> aTestingBackend = nullptr);
+
+ private:
+ // These all run on the lazy helper thread.
+ void DoSetupBackend();
+ void DoSetupTestingBackend(RefPtr<JumpListBackend> aTestingBackend);
+ void DoShutdownBackend();
+ void DoSetAppID(nsString aAppUserModelID);
+ void DoIsAvailable(const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
+ void DoCheckForRemovals(
+ const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
+ void DoPopulateJumpList(
+ const nsTArray<dom::WindowsJumpListShortcutDescription>&&
+ aTaskDescriptions,
+ const nsAString& aCustomTitle,
+ const nsTArray<dom::WindowsJumpListShortcutDescription>&&
+ aCustomDescriptions,
+ const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
+ void DoClearJumpList(
+ const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
+ void RemoveIconCacheAndGetJumplistShortcutURIs(IObjectArray* aObjArray,
+ nsTArray<nsString>& aURISpecs);
+ void DeleteIconFromDisk(const nsAString& aPath);
+ nsresult GetShellLinkFromDescription(
+ const dom::WindowsJumpListShortcutDescription& aDesc,
+ RefPtr<IShellLinkW>& aShellLink);
+
+ // This is written to once during construction on the main thread before the
+ // lazy helper thread is created. After that, the lazy helper thread might
+ // read from it.
+ nsString mAppUserModelId;
+
+ // This is only accessed by the lazy helper thread.
+ RefPtr<JumpListBackend> mJumpListBackend;
+
+ // This is only accessed by the main thread.
+ RefPtr<LazyIdleThread> mIOThread;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __JumpListBuilder_h__ */
diff --git a/widget/windows/KeyboardLayout.cpp b/widget/windows/KeyboardLayout.cpp
new file mode 100644
index 0000000000..3b59aa9319
--- /dev/null
+++ b/widget/windows/KeyboardLayout.cpp
@@ -0,0 +1,5442 @@
+/* -*- 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 "mozilla/Logging.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/widget/WinRegistry.h"
+
+#include "nsExceptionHandler.h"
+#include "nsGkAtoms.h"
+#include "nsIUserIdleServiceInternal.h"
+#include "nsIWindowsRegKey.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTArray.h"
+#include "nsUnicharUtils.h"
+#include "nsWindowDbg.h"
+
+#include "KeyboardLayout.h"
+#include "WidgetUtils.h"
+#include "WinUtils.h"
+
+#include "npapi.h"
+
+#include <windows.h>
+#include <winnls.h>
+#include <winuser.h>
+#include <algorithm>
+
+#ifndef WINABLEAPI
+# include <winable.h>
+#endif
+
+// For collecting other people's log, tell them `MOZ_LOG=KeyboardHandler:4,sync`
+// rather than `MOZ_LOG=KeyboardHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gKeyLog("KeyboardHandler");
+
+namespace mozilla {
+namespace widget {
+
+static const char* const kVirtualKeyName[] = {
+ "NULL",
+ "VK_LBUTTON",
+ "VK_RBUTTON",
+ "VK_CANCEL",
+ "VK_MBUTTON",
+ "VK_XBUTTON1",
+ "VK_XBUTTON2",
+ "0x07",
+ "VK_BACK",
+ "VK_TAB",
+ "0x0A",
+ "0x0B",
+ "VK_CLEAR",
+ "VK_RETURN",
+ "0x0E",
+ "0x0F",
+
+ "VK_SHIFT",
+ "VK_CONTROL",
+ "VK_MENU",
+ "VK_PAUSE",
+ "VK_CAPITAL",
+ "VK_KANA, VK_HANGUL",
+ "0x16",
+ "VK_JUNJA",
+ "VK_FINAL",
+ "VK_HANJA, VK_KANJI",
+ "0x1A",
+ "VK_ESCAPE",
+ "VK_CONVERT",
+ "VK_NONCONVERT",
+ "VK_ACCEPT",
+ "VK_MODECHANGE",
+
+ "VK_SPACE",
+ "VK_PRIOR",
+ "VK_NEXT",
+ "VK_END",
+ "VK_HOME",
+ "VK_LEFT",
+ "VK_UP",
+ "VK_RIGHT",
+ "VK_DOWN",
+ "VK_SELECT",
+ "VK_PRINT",
+ "VK_EXECUTE",
+ "VK_SNAPSHOT",
+ "VK_INSERT",
+ "VK_DELETE",
+ "VK_HELP",
+
+ "VK_0",
+ "VK_1",
+ "VK_2",
+ "VK_3",
+ "VK_4",
+ "VK_5",
+ "VK_6",
+ "VK_7",
+ "VK_8",
+ "VK_9",
+ "0x3A",
+ "0x3B",
+ "0x3C",
+ "0x3D",
+ "0x3E",
+ "0x3F",
+
+ "0x40",
+ "VK_A",
+ "VK_B",
+ "VK_C",
+ "VK_D",
+ "VK_E",
+ "VK_F",
+ "VK_G",
+ "VK_H",
+ "VK_I",
+ "VK_J",
+ "VK_K",
+ "VK_L",
+ "VK_M",
+ "VK_N",
+ "VK_O",
+
+ "VK_P",
+ "VK_Q",
+ "VK_R",
+ "VK_S",
+ "VK_T",
+ "VK_U",
+ "VK_V",
+ "VK_W",
+ "VK_X",
+ "VK_Y",
+ "VK_Z",
+ "VK_LWIN",
+ "VK_RWIN",
+ "VK_APPS",
+ "0x5E",
+ "VK_SLEEP",
+
+ "VK_NUMPAD0",
+ "VK_NUMPAD1",
+ "VK_NUMPAD2",
+ "VK_NUMPAD3",
+ "VK_NUMPAD4",
+ "VK_NUMPAD5",
+ "VK_NUMPAD6",
+ "VK_NUMPAD7",
+ "VK_NUMPAD8",
+ "VK_NUMPAD9",
+ "VK_MULTIPLY",
+ "VK_ADD",
+ "VK_SEPARATOR",
+ "VK_SUBTRACT",
+ "VK_DECIMAL",
+ "VK_DIVIDE",
+
+ "VK_F1",
+ "VK_F2",
+ "VK_F3",
+ "VK_F4",
+ "VK_F5",
+ "VK_F6",
+ "VK_F7",
+ "VK_F8",
+ "VK_F9",
+ "VK_F10",
+ "VK_F11",
+ "VK_F12",
+ "VK_F13",
+ "VK_F14",
+ "VK_F15",
+ "VK_F16",
+
+ "VK_F17",
+ "VK_F18",
+ "VK_F19",
+ "VK_F20",
+ "VK_F21",
+ "VK_F22",
+ "VK_F23",
+ "VK_F24",
+ "0x88",
+ "0x89",
+ "0x8A",
+ "0x8B",
+ "0x8C",
+ "0x8D",
+ "0x8E",
+ "0x8F",
+
+ "VK_NUMLOCK",
+ "VK_SCROLL",
+ "VK_OEM_NEC_EQUAL, VK_OEM_FJ_JISHO",
+ "VK_OEM_FJ_MASSHOU",
+ "VK_OEM_FJ_TOUROKU",
+ "VK_OEM_FJ_LOYA",
+ "VK_OEM_FJ_ROYA",
+ "0x97",
+ "0x98",
+ "0x99",
+ "0x9A",
+ "0x9B",
+ "0x9C",
+ "0x9D",
+ "0x9E",
+ "0x9F",
+
+ "VK_LSHIFT",
+ "VK_RSHIFT",
+ "VK_LCONTROL",
+ "VK_RCONTROL",
+ "VK_LMENU",
+ "VK_RMENU",
+ "VK_BROWSER_BACK",
+ "VK_BROWSER_FORWARD",
+ "VK_BROWSER_REFRESH",
+ "VK_BROWSER_STOP",
+ "VK_BROWSER_SEARCH",
+ "VK_BROWSER_FAVORITES",
+ "VK_BROWSER_HOME",
+ "VK_VOLUME_MUTE",
+ "VK_VOLUME_DOWN",
+ "VK_VOLUME_UP",
+
+ "VK_MEDIA_NEXT_TRACK",
+ "VK_MEDIA_PREV_TRACK",
+ "VK_MEDIA_STOP",
+ "VK_MEDIA_PLAY_PAUSE",
+ "VK_LAUNCH_MAIL",
+ "VK_LAUNCH_MEDIA_SELECT",
+ "VK_LAUNCH_APP1",
+ "VK_LAUNCH_APP2",
+ "0xB8",
+ "0xB9",
+ "VK_OEM_1",
+ "VK_OEM_PLUS",
+ "VK_OEM_COMMA",
+ "VK_OEM_MINUS",
+ "VK_OEM_PERIOD",
+ "VK_OEM_2",
+
+ "VK_OEM_3",
+ "VK_ABNT_C1",
+ "VK_ABNT_C2",
+ "0xC3",
+ "0xC4",
+ "0xC5",
+ "0xC6",
+ "0xC7",
+ "0xC8",
+ "0xC9",
+ "0xCA",
+ "0xCB",
+ "0xCC",
+ "0xCD",
+ "0xCE",
+ "0xCF",
+
+ "0xD0",
+ "0xD1",
+ "0xD2",
+ "0xD3",
+ "0xD4",
+ "0xD5",
+ "0xD6",
+ "0xD7",
+ "0xD8",
+ "0xD9",
+ "0xDA",
+ "VK_OEM_4",
+ "VK_OEM_5",
+ "VK_OEM_6",
+ "VK_OEM_7",
+ "VK_OEM_8",
+
+ "0xE0",
+ "VK_OEM_AX",
+ "VK_OEM_102",
+ "VK_ICO_HELP",
+ "VK_ICO_00",
+ "VK_PROCESSKEY",
+ "VK_ICO_CLEAR",
+ "VK_PACKET",
+ "0xE8",
+ "VK_OEM_RESET",
+ "VK_OEM_JUMP",
+ "VK_OEM_PA1",
+ "VK_OEM_PA2",
+ "VK_OEM_PA3",
+ "VK_OEM_WSCTRL",
+ "VK_OEM_CUSEL",
+
+ "VK_OEM_ATTN",
+ "VK_OEM_FINISH",
+ "VK_OEM_COPY",
+ "VK_OEM_AUTO",
+ "VK_OEM_ENLW",
+ "VK_OEM_BACKTAB",
+ "VK_ATTN",
+ "VK_CRSEL",
+ "VK_EXSEL",
+ "VK_EREOF",
+ "VK_PLAY",
+ "VK_ZOOM",
+ "VK_NONAME",
+ "VK_PA1",
+ "VK_OEM_CLEAR",
+ "0xFF"};
+
+static_assert(sizeof(kVirtualKeyName) / sizeof(const char*) == 0x100,
+ "The virtual key name must be defined just 256 keys");
+
+static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; }
+
+static const nsCString GetCharacterCodeName(WPARAM aCharCode) {
+ switch (aCharCode) {
+ case 0x0000:
+ return "NULL (0x0000)"_ns;
+ case 0x0008:
+ return "BACKSPACE (0x0008)"_ns;
+ case 0x0009:
+ return "CHARACTER TABULATION (0x0009)"_ns;
+ case 0x000A:
+ return "LINE FEED (0x000A)"_ns;
+ case 0x000B:
+ return "LINE TABULATION (0x000B)"_ns;
+ case 0x000C:
+ return "FORM FEED (0x000C)"_ns;
+ case 0x000D:
+ return "CARRIAGE RETURN (0x000D)"_ns;
+ case 0x0018:
+ return "CANCEL (0x0018)"_ns;
+ case 0x001B:
+ return "ESCAPE (0x001B)"_ns;
+ case 0x0020:
+ return "SPACE (0x0020)"_ns;
+ case 0x007F:
+ return "DELETE (0x007F)"_ns;
+ case 0x00A0:
+ return "NO-BREAK SPACE (0x00A0)"_ns;
+ case 0x00AD:
+ return "SOFT HYPHEN (0x00AD)"_ns;
+ case 0x2000:
+ return "EN QUAD (0x2000)"_ns;
+ case 0x2001:
+ return "EM QUAD (0x2001)"_ns;
+ case 0x2002:
+ return "EN SPACE (0x2002)"_ns;
+ case 0x2003:
+ return "EM SPACE (0x2003)"_ns;
+ case 0x2004:
+ return "THREE-PER-EM SPACE (0x2004)"_ns;
+ case 0x2005:
+ return "FOUR-PER-EM SPACE (0x2005)"_ns;
+ case 0x2006:
+ return "SIX-PER-EM SPACE (0x2006)"_ns;
+ case 0x2007:
+ return "FIGURE SPACE (0x2007)"_ns;
+ case 0x2008:
+ return "PUNCTUATION SPACE (0x2008)"_ns;
+ case 0x2009:
+ return "THIN SPACE (0x2009)"_ns;
+ case 0x200A:
+ return "HAIR SPACE (0x200A)"_ns;
+ case 0x200B:
+ return "ZERO WIDTH SPACE (0x200B)"_ns;
+ case 0x200C:
+ return "ZERO WIDTH NON-JOINER (0x200C)"_ns;
+ case 0x200D:
+ return "ZERO WIDTH JOINER (0x200D)"_ns;
+ case 0x200E:
+ return "LEFT-TO-RIGHT MARK (0x200E)"_ns;
+ case 0x200F:
+ return "RIGHT-TO-LEFT MARK (0x200F)"_ns;
+ case 0x2029:
+ return "PARAGRAPH SEPARATOR (0x2029)"_ns;
+ case 0x202A:
+ return "LEFT-TO-RIGHT EMBEDDING (0x202A)"_ns;
+ case 0x202B:
+ return "RIGHT-TO-LEFT EMBEDDING (0x202B)"_ns;
+ case 0x202D:
+ return "LEFT-TO-RIGHT OVERRIDE (0x202D)"_ns;
+ case 0x202E:
+ return "RIGHT-TO-LEFT OVERRIDE (0x202E)"_ns;
+ case 0x202F:
+ return "NARROW NO-BREAK SPACE (0x202F)"_ns;
+ case 0x205F:
+ return "MEDIUM MATHEMATICAL SPACE (0x205F)"_ns;
+ case 0x2060:
+ return "WORD JOINER (0x2060)"_ns;
+ case 0x2066:
+ return "LEFT-TO-RIGHT ISOLATE (0x2066)"_ns;
+ case 0x2067:
+ return "RIGHT-TO-LEFT ISOLATE (0x2067)"_ns;
+ case 0x3000:
+ return "IDEOGRAPHIC SPACE (0x3000)"_ns;
+ case 0xFEFF:
+ return "ZERO WIDTH NO-BREAK SPACE (0xFEFF)"_ns;
+ default: {
+ if (aCharCode < ' ' || (aCharCode >= 0x80 && aCharCode < 0xA0)) {
+ return nsPrintfCString("control (0x%04zX)", aCharCode);
+ }
+ if (NS_IS_HIGH_SURROGATE(aCharCode)) {
+ return nsPrintfCString("high surrogate (0x%04zX)", aCharCode);
+ }
+ if (NS_IS_LOW_SURROGATE(aCharCode)) {
+ return nsPrintfCString("low surrogate (0x%04zX)", aCharCode);
+ }
+ return IS_IN_BMP(aCharCode)
+ ? nsPrintfCString(
+ "'%s' (0x%04zX)",
+ NS_ConvertUTF16toUTF8(nsAutoString(aCharCode)).get(),
+ aCharCode)
+ : nsPrintfCString(
+ "'%s' (0x%08zX)",
+ NS_ConvertUTF16toUTF8(nsAutoString(aCharCode)).get(),
+ aCharCode);
+ }
+ }
+}
+
+static const nsCString GetKeyLocationName(uint32_t aLocation) {
+ switch (aLocation) {
+ case eKeyLocationLeft:
+ return "KEY_LOCATION_LEFT"_ns;
+ case eKeyLocationRight:
+ return "KEY_LOCATION_RIGHT"_ns;
+ case eKeyLocationStandard:
+ return "KEY_LOCATION_STANDARD"_ns;
+ case eKeyLocationNumpad:
+ return "KEY_LOCATION_NUMPAD"_ns;
+ default:
+ return nsPrintfCString("Unknown (0x%04X)", aLocation);
+ }
+}
+
+static const nsCString GetCharacterCodeNames(const char16_t* aChars,
+ uint32_t aLength) {
+ if (!aLength) {
+ return ""_ns;
+ }
+ nsCString result;
+ result.AssignLiteral("\"");
+ StringJoinAppend(result, ", "_ns, Span{aChars, aLength},
+ [](nsACString& dest, const char16_t charValue) {
+ dest.Append(GetCharacterCodeName(charValue));
+ });
+ result.AppendLiteral("\"");
+ return result;
+}
+
+static const nsCString GetCharacterCodeNames(
+ const UniCharsAndModifiers& aUniCharsAndModifiers) {
+ if (aUniCharsAndModifiers.IsEmpty()) {
+ return ""_ns;
+ }
+ nsCString result;
+ result.AssignLiteral("\"");
+ StringJoinAppend(result, ", "_ns, Span{aUniCharsAndModifiers.ToString()},
+ [](nsACString& dest, const char16_t charValue) {
+ dest.Append(GetCharacterCodeName(charValue));
+ });
+ result.AppendLiteral("\"");
+ return result;
+}
+
+class MOZ_STACK_CLASS GetShiftStateName final : public nsAutoCString {
+ public:
+ explicit GetShiftStateName(VirtualKey::ShiftState aShiftState) {
+ if (!aShiftState) {
+ AssignLiteral("none");
+ return;
+ }
+ if (aShiftState & VirtualKey::STATE_SHIFT) {
+ AssignLiteral("Shift");
+ aShiftState &= ~VirtualKey::STATE_SHIFT;
+ }
+ if (aShiftState & VirtualKey::STATE_CONTROL) {
+ MaybeAppendSeparator();
+ AssignLiteral("Ctrl");
+ aShiftState &= ~VirtualKey::STATE_CONTROL;
+ }
+ if (aShiftState & VirtualKey::STATE_ALT) {
+ MaybeAppendSeparator();
+ AssignLiteral("Alt");
+ aShiftState &= ~VirtualKey::STATE_ALT;
+ }
+ if (aShiftState & VirtualKey::STATE_CAPSLOCK) {
+ MaybeAppendSeparator();
+ AssignLiteral("CapsLock");
+ aShiftState &= ~VirtualKey::STATE_CAPSLOCK;
+ }
+ MOZ_ASSERT(!aShiftState);
+ }
+
+ private:
+ void MaybeAppendSeparator() {
+ if (!IsEmpty()) {
+ AppendLiteral(" | ");
+ }
+ }
+};
+
+static const nsCString GetMessageName(UINT aMessage) {
+ switch (aMessage) {
+ case WM_NULL:
+ return "WM_NULL"_ns;
+ case WM_KEYDOWN:
+ return "WM_KEYDOWN"_ns;
+ case WM_KEYUP:
+ return "WM_KEYUP"_ns;
+ case WM_SYSKEYDOWN:
+ return "WM_SYSKEYDOWN"_ns;
+ case WM_SYSKEYUP:
+ return "WM_SYSKEYUP"_ns;
+ case WM_CHAR:
+ return "WM_CHAR"_ns;
+ case WM_UNICHAR:
+ return "WM_UNICHAR"_ns;
+ case WM_SYSCHAR:
+ return "WM_SYSCHAR"_ns;
+ case WM_DEADCHAR:
+ return "WM_DEADCHAR"_ns;
+ case WM_SYSDEADCHAR:
+ return "WM_SYSDEADCHAR"_ns;
+ case WM_APPCOMMAND:
+ return "WM_APPCOMMAND"_ns;
+ case WM_QUIT:
+ return "WM_QUIT"_ns;
+ default:
+ return nsPrintfCString("Unknown Message (0x%04X)", aMessage);
+ }
+}
+
+static const nsCString GetVirtualKeyCodeName(WPARAM aVK) {
+ if (aVK >= ArrayLength(kVirtualKeyName)) {
+ return nsPrintfCString("Invalid (0x%08zX)", aVK);
+ }
+ return nsCString(kVirtualKeyName[aVK]);
+}
+
+static const nsCString GetAppCommandName(WPARAM aCommand) {
+ switch (aCommand) {
+ case APPCOMMAND_BASS_BOOST:
+ return "APPCOMMAND_BASS_BOOST"_ns;
+ case APPCOMMAND_BASS_DOWN:
+ return "APPCOMMAND_BASS_DOWN"_ns;
+ case APPCOMMAND_BASS_UP:
+ return "APPCOMMAND_BASS_UP"_ns;
+ case APPCOMMAND_BROWSER_BACKWARD:
+ return "APPCOMMAND_BROWSER_BACKWARD"_ns;
+ case APPCOMMAND_BROWSER_FAVORITES:
+ return "APPCOMMAND_BROWSER_FAVORITES"_ns;
+ case APPCOMMAND_BROWSER_FORWARD:
+ return "APPCOMMAND_BROWSER_FORWARD"_ns;
+ case APPCOMMAND_BROWSER_HOME:
+ return "APPCOMMAND_BROWSER_HOME"_ns;
+ case APPCOMMAND_BROWSER_REFRESH:
+ return "APPCOMMAND_BROWSER_REFRESH"_ns;
+ case APPCOMMAND_BROWSER_SEARCH:
+ return "APPCOMMAND_BROWSER_SEARCH"_ns;
+ case APPCOMMAND_BROWSER_STOP:
+ return "APPCOMMAND_BROWSER_STOP"_ns;
+ case APPCOMMAND_CLOSE:
+ return "APPCOMMAND_CLOSE"_ns;
+ case APPCOMMAND_COPY:
+ return "APPCOMMAND_COPY"_ns;
+ case APPCOMMAND_CORRECTION_LIST:
+ return "APPCOMMAND_CORRECTION_LIST"_ns;
+ case APPCOMMAND_CUT:
+ return "APPCOMMAND_CUT"_ns;
+ case APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE:
+ return "APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE"_ns;
+ case APPCOMMAND_FIND:
+ return "APPCOMMAND_FIND"_ns;
+ case APPCOMMAND_FORWARD_MAIL:
+ return "APPCOMMAND_FORWARD_MAIL"_ns;
+ case APPCOMMAND_HELP:
+ return "APPCOMMAND_HELP"_ns;
+ case APPCOMMAND_LAUNCH_APP1:
+ return "APPCOMMAND_LAUNCH_APP1"_ns;
+ case APPCOMMAND_LAUNCH_APP2:
+ return "APPCOMMAND_LAUNCH_APP2"_ns;
+ case APPCOMMAND_LAUNCH_MAIL:
+ return "APPCOMMAND_LAUNCH_MAIL"_ns;
+ case APPCOMMAND_LAUNCH_MEDIA_SELECT:
+ return "APPCOMMAND_LAUNCH_MEDIA_SELECT"_ns;
+ case APPCOMMAND_MEDIA_CHANNEL_DOWN:
+ return "APPCOMMAND_MEDIA_CHANNEL_DOWN"_ns;
+ case APPCOMMAND_MEDIA_CHANNEL_UP:
+ return "APPCOMMAND_MEDIA_CHANNEL_UP"_ns;
+ case APPCOMMAND_MEDIA_FAST_FORWARD:
+ return "APPCOMMAND_MEDIA_FAST_FORWARD"_ns;
+ case APPCOMMAND_MEDIA_NEXTTRACK:
+ return "APPCOMMAND_MEDIA_NEXTTRACK"_ns;
+ case APPCOMMAND_MEDIA_PAUSE:
+ return "APPCOMMAND_MEDIA_PAUSE"_ns;
+ case APPCOMMAND_MEDIA_PLAY:
+ return "APPCOMMAND_MEDIA_PLAY"_ns;
+ case APPCOMMAND_MEDIA_PLAY_PAUSE:
+ return "APPCOMMAND_MEDIA_PLAY_PAUSE"_ns;
+ case APPCOMMAND_MEDIA_PREVIOUSTRACK:
+ return "APPCOMMAND_MEDIA_PREVIOUSTRACK"_ns;
+ case APPCOMMAND_MEDIA_RECORD:
+ return "APPCOMMAND_MEDIA_RECORD"_ns;
+ case APPCOMMAND_MEDIA_REWIND:
+ return "APPCOMMAND_MEDIA_REWIND"_ns;
+ case APPCOMMAND_MEDIA_STOP:
+ return "APPCOMMAND_MEDIA_STOP"_ns;
+ case APPCOMMAND_MIC_ON_OFF_TOGGLE:
+ return "APPCOMMAND_MIC_ON_OFF_TOGGLE"_ns;
+ case APPCOMMAND_MICROPHONE_VOLUME_DOWN:
+ return "APPCOMMAND_MICROPHONE_VOLUME_DOWN"_ns;
+ case APPCOMMAND_MICROPHONE_VOLUME_MUTE:
+ return "APPCOMMAND_MICROPHONE_VOLUME_MUTE"_ns;
+ case APPCOMMAND_MICROPHONE_VOLUME_UP:
+ return "APPCOMMAND_MICROPHONE_VOLUME_UP"_ns;
+ case APPCOMMAND_NEW:
+ return "APPCOMMAND_NEW"_ns;
+ case APPCOMMAND_OPEN:
+ return "APPCOMMAND_OPEN"_ns;
+ case APPCOMMAND_PASTE:
+ return "APPCOMMAND_PASTE"_ns;
+ case APPCOMMAND_PRINT:
+ return "APPCOMMAND_PRINT"_ns;
+ case APPCOMMAND_REDO:
+ return "APPCOMMAND_REDO"_ns;
+ case APPCOMMAND_REPLY_TO_MAIL:
+ return "APPCOMMAND_REPLY_TO_MAIL"_ns;
+ case APPCOMMAND_SAVE:
+ return "APPCOMMAND_SAVE"_ns;
+ case APPCOMMAND_SEND_MAIL:
+ return "APPCOMMAND_SEND_MAIL"_ns;
+ case APPCOMMAND_SPELL_CHECK:
+ return "APPCOMMAND_SPELL_CHECK"_ns;
+ case APPCOMMAND_TREBLE_DOWN:
+ return "APPCOMMAND_TREBLE_DOWN"_ns;
+ case APPCOMMAND_TREBLE_UP:
+ return "APPCOMMAND_TREBLE_UP"_ns;
+ case APPCOMMAND_UNDO:
+ return "APPCOMMAND_UNDO"_ns;
+ case APPCOMMAND_VOLUME_DOWN:
+ return "APPCOMMAND_VOLUME_DOWN"_ns;
+ case APPCOMMAND_VOLUME_MUTE:
+ return "APPCOMMAND_VOLUME_MUTE"_ns;
+ case APPCOMMAND_VOLUME_UP:
+ return "APPCOMMAND_VOLUME_UP"_ns;
+ default:
+ return nsPrintfCString("Unknown app command (0x%08zX)", aCommand);
+ }
+}
+
+static const nsCString GetAppCommandDeviceName(LPARAM aDevice) {
+ switch (aDevice) {
+ case FAPPCOMMAND_KEY:
+ return "FAPPCOMMAND_KEY"_ns;
+ case FAPPCOMMAND_MOUSE:
+ return "FAPPCOMMAND_MOUSE"_ns;
+ case FAPPCOMMAND_OEM:
+ return "FAPPCOMMAND_OEM"_ns;
+ default:
+ return nsPrintfCString("Unknown app command device (0x%04" PRIXLPTR ")",
+ aDevice);
+ }
+};
+
+class MOZ_STACK_CLASS GetAppCommandKeysName final : public nsAutoCString {
+ public:
+ explicit GetAppCommandKeysName(WPARAM aKeys) {
+ if (aKeys & MK_CONTROL) {
+ AppendLiteral("MK_CONTROL");
+ aKeys &= ~MK_CONTROL;
+ }
+ if (aKeys & MK_LBUTTON) {
+ MaybeAppendSeparator();
+ AppendLiteral("MK_LBUTTON");
+ aKeys &= ~MK_LBUTTON;
+ }
+ if (aKeys & MK_MBUTTON) {
+ MaybeAppendSeparator();
+ AppendLiteral("MK_MBUTTON");
+ aKeys &= ~MK_MBUTTON;
+ }
+ if (aKeys & MK_RBUTTON) {
+ MaybeAppendSeparator();
+ AppendLiteral("MK_RBUTTON");
+ aKeys &= ~MK_RBUTTON;
+ }
+ if (aKeys & MK_SHIFT) {
+ MaybeAppendSeparator();
+ AppendLiteral("MK_SHIFT");
+ aKeys &= ~MK_SHIFT;
+ }
+ if (aKeys & MK_XBUTTON1) {
+ MaybeAppendSeparator();
+ AppendLiteral("MK_XBUTTON1");
+ aKeys &= ~MK_XBUTTON1;
+ }
+ if (aKeys & MK_XBUTTON2) {
+ MaybeAppendSeparator();
+ AppendLiteral("MK_XBUTTON2");
+ aKeys &= ~MK_XBUTTON2;
+ }
+ if (aKeys) {
+ MaybeAppendSeparator();
+ AppendPrintf("Unknown Flags (0x%04zX)", aKeys);
+ }
+ if (IsEmpty()) {
+ AssignLiteral("none (0x0000)");
+ }
+ }
+
+ private:
+ void MaybeAppendSeparator() {
+ if (!IsEmpty()) {
+ AppendLiteral(" | ");
+ }
+ }
+};
+
+static const nsCString ToString(const MSG& aMSG) {
+ nsCString result;
+ result.AssignLiteral("{ message=");
+ result.Append(GetMessageName(aMSG.message).get());
+ result.AppendLiteral(", ");
+ switch (aMSG.message) {
+ case WM_KEYDOWN:
+ case WM_KEYUP:
+ case WM_SYSKEYDOWN:
+ case WM_SYSKEYUP:
+ result.AppendPrintf(
+ "virtual keycode=%s, repeat count=%" PRIdLPTR
+ ", "
+ "scancode=0x%02X, extended key=%s, "
+ "context code=%s, previous key state=%s, "
+ "transition state=%s",
+ GetVirtualKeyCodeName(aMSG.wParam).get(), aMSG.lParam & 0xFFFF,
+ WinUtils::GetScanCode(aMSG.lParam),
+ GetBoolName(WinUtils::IsExtendedScanCode(aMSG.lParam)),
+ GetBoolName((aMSG.lParam & (1 << 29)) != 0),
+ GetBoolName((aMSG.lParam & (1 << 30)) != 0),
+ GetBoolName((aMSG.lParam & (1 << 31)) != 0));
+ break;
+ case WM_CHAR:
+ case WM_DEADCHAR:
+ case WM_SYSCHAR:
+ case WM_SYSDEADCHAR:
+ result.AppendPrintf(
+ "character code=%s, repeat count=%" PRIdLPTR
+ ", "
+ "scancode=0x%02X, extended key=%s, "
+ "context code=%s, previous key state=%s, "
+ "transition state=%s",
+ GetCharacterCodeName(aMSG.wParam).get(), aMSG.lParam & 0xFFFF,
+ WinUtils::GetScanCode(aMSG.lParam),
+ GetBoolName(WinUtils::IsExtendedScanCode(aMSG.lParam)),
+ GetBoolName((aMSG.lParam & (1 << 29)) != 0),
+ GetBoolName((aMSG.lParam & (1 << 30)) != 0),
+ GetBoolName((aMSG.lParam & (1 << 31)) != 0));
+ break;
+ case WM_APPCOMMAND:
+ result.AppendPrintf(
+ "window handle=0x%zx, app command=%s, device=%s, dwKeys=%s",
+ aMSG.wParam,
+ GetAppCommandName(GET_APPCOMMAND_LPARAM(aMSG.lParam)).get(),
+ GetAppCommandDeviceName(GET_DEVICE_LPARAM(aMSG.lParam)).get(),
+ GetAppCommandKeysName(GET_KEYSTATE_LPARAM(aMSG.lParam)).get());
+ break;
+ default:
+ result.AppendPrintf("wParam=%zu, lParam=%" PRIdLPTR, aMSG.wParam,
+ aMSG.lParam);
+ break;
+ }
+ result.AppendPrintf(", hwnd=0x%p", aMSG.hwnd);
+ return result;
+}
+
+static const nsCString ToString(
+ const UniCharsAndModifiers& aUniCharsAndModifiers) {
+ if (aUniCharsAndModifiers.IsEmpty()) {
+ return "{}"_ns;
+ }
+ nsCString result;
+ result.AssignLiteral("{ ");
+ result.Append(GetCharacterCodeName(aUniCharsAndModifiers.CharAt(0)));
+ for (size_t i = 1; i < aUniCharsAndModifiers.Length(); ++i) {
+ if (aUniCharsAndModifiers.ModifiersAt(i - 1) !=
+ aUniCharsAndModifiers.ModifiersAt(i)) {
+ result.AppendLiteral(" [");
+ result.Append(GetModifiersName(aUniCharsAndModifiers.ModifiersAt(0)));
+ result.AppendLiteral("]");
+ }
+ result.AppendLiteral(", ");
+ result.Append(GetCharacterCodeName(aUniCharsAndModifiers.CharAt(i)));
+ }
+ result.AppendLiteral(" [");
+ uint32_t lastIndex = aUniCharsAndModifiers.Length() - 1;
+ result.Append(GetModifiersName(aUniCharsAndModifiers.ModifiersAt(lastIndex)));
+ result.AppendLiteral("] }");
+ return result;
+}
+
+const nsCString ToString(const ModifierKeyState& aModifierKeyState) {
+ nsCString result;
+ result.AssignLiteral("{ ");
+ result.Append(GetModifiersName(aModifierKeyState.GetModifiers()).get());
+ result.AppendLiteral(" }");
+ return result;
+}
+
+// Unique id counter associated with a keydown / keypress events. Used in
+// identifing keypress events for removal from async event dispatch queue
+// in metrofx after preventDefault is called on keydown events.
+static uint32_t sUniqueKeyEventId = 0;
+
+/*****************************************************************************
+ * mozilla::widget::ModifierKeyState
+ *****************************************************************************/
+
+ModifierKeyState::ModifierKeyState() { Update(); }
+
+ModifierKeyState::ModifierKeyState(Modifiers aModifiers)
+ : mModifiers(aModifiers) {
+ MOZ_ASSERT(!(mModifiers & MODIFIER_ALTGRAPH) || (!IsControl() && !IsAlt()),
+ "Neither MODIFIER_CONTROL nor MODIFIER_ALT should be set "
+ "if MODIFIER_ALTGRAPH is set");
+}
+
+void ModifierKeyState::Update() {
+ mModifiers = 0;
+ if (IS_VK_DOWN(VK_SHIFT)) {
+ mModifiers |= MODIFIER_SHIFT;
+ }
+ // If AltGr key (i.e., VK_RMENU on some keyboard layout) is pressed, only
+ // MODIFIER_ALTGRAPH should be set. Otherwise, i.e., if both Ctrl and Alt
+ // keys are pressed to emulate AltGr key, MODIFIER_CONTROL and MODIFIER_ALT
+ // keys should be set separately.
+ if (IS_VK_DOWN(VK_RMENU) && KeyboardLayout::GetInstance()->HasAltGr()) {
+ mModifiers |= MODIFIER_ALTGRAPH;
+ } else {
+ if (IS_VK_DOWN(VK_CONTROL)) {
+ mModifiers |= MODIFIER_CONTROL;
+ }
+ if (IS_VK_DOWN(VK_MENU)) {
+ mModifiers |= MODIFIER_ALT;
+ }
+ }
+ if (IS_VK_DOWN(VK_LWIN) || IS_VK_DOWN(VK_RWIN)) {
+ mModifiers |= MODIFIER_META;
+ }
+ if (::GetKeyState(VK_CAPITAL) & 1) {
+ mModifiers |= MODIFIER_CAPSLOCK;
+ }
+ if (::GetKeyState(VK_NUMLOCK) & 1) {
+ mModifiers |= MODIFIER_NUMLOCK;
+ }
+ if (::GetKeyState(VK_SCROLL) & 1) {
+ mModifiers |= MODIFIER_SCROLLLOCK;
+ }
+}
+
+void ModifierKeyState::Unset(Modifiers aRemovingModifiers) {
+ mModifiers &= ~aRemovingModifiers;
+}
+
+void ModifierKeyState::Set(Modifiers aAddingModifiers) {
+ mModifiers |= aAddingModifiers;
+ MOZ_ASSERT(!(mModifiers & MODIFIER_ALTGRAPH) || (!IsControl() && !IsAlt()),
+ "Neither MODIFIER_CONTROL nor MODIFIER_ALT should be set "
+ "if MODIFIER_ALTGRAPH is set");
+}
+
+void ModifierKeyState::InitInputEvent(WidgetInputEvent& aInputEvent) const {
+ aInputEvent.mModifiers = mModifiers;
+
+ switch (aInputEvent.mClass) {
+ case eMouseEventClass:
+ case eMouseScrollEventClass:
+ case eWheelEventClass:
+ case eDragEventClass:
+ case eSimpleGestureEventClass:
+ InitMouseEvent(aInputEvent);
+ break;
+ default:
+ break;
+ }
+}
+
+void ModifierKeyState::InitMouseEvent(WidgetInputEvent& aMouseEvent) const {
+ NS_ASSERTION(aMouseEvent.mClass == eMouseEventClass ||
+ aMouseEvent.mClass == eWheelEventClass ||
+ aMouseEvent.mClass == eDragEventClass ||
+ aMouseEvent.mClass == eSimpleGestureEventClass,
+ "called with non-mouse event");
+
+ WidgetMouseEventBase& mouseEvent = *aMouseEvent.AsMouseEventBase();
+ mouseEvent.mButtons = 0;
+ if (::GetKeyState(VK_LBUTTON) < 0) {
+ mouseEvent.mButtons |= MouseButtonsFlag::ePrimaryFlag;
+ }
+ if (::GetKeyState(VK_RBUTTON) < 0) {
+ mouseEvent.mButtons |= MouseButtonsFlag::eSecondaryFlag;
+ }
+ if (::GetKeyState(VK_MBUTTON) < 0) {
+ mouseEvent.mButtons |= MouseButtonsFlag::eMiddleFlag;
+ }
+ if (::GetKeyState(VK_XBUTTON1) < 0) {
+ mouseEvent.mButtons |= MouseButtonsFlag::e4thFlag;
+ }
+ if (::GetKeyState(VK_XBUTTON2) < 0) {
+ mouseEvent.mButtons |= MouseButtonsFlag::e5thFlag;
+ }
+}
+
+bool ModifierKeyState::IsShift() const {
+ return (mModifiers & MODIFIER_SHIFT) != 0;
+}
+
+bool ModifierKeyState::IsControl() const {
+ return (mModifiers & MODIFIER_CONTROL) != 0;
+}
+
+bool ModifierKeyState::IsAlt() const {
+ return (mModifiers & MODIFIER_ALT) != 0;
+}
+
+bool ModifierKeyState::IsWin() const {
+ return (mModifiers & MODIFIER_META) != 0;
+}
+
+bool ModifierKeyState::MaybeMatchShortcutKey() const {
+ // If Windows key is pressed, even if both Ctrl key and Alt key are pressed,
+ // it's possible to match a shortcut key.
+ if (IsWin()) {
+ return true;
+ }
+ // Otherwise, when both Ctrl key and Alt key are pressed, it shouldn't be
+ // a shortcut key for Windows since it means pressing AltGr key on
+ // some keyboard layouts.
+ if (IsControl() ^ IsAlt()) {
+ return true;
+ }
+ // If no modifier key is active except a lockable modifier nor Shift key,
+ // the key shouldn't match any shortcut keys (there are Space and
+ // Shift+Space, though, let's ignore these special case...).
+ return false;
+}
+
+bool ModifierKeyState::IsCapsLocked() const {
+ return (mModifiers & MODIFIER_CAPSLOCK) != 0;
+}
+
+bool ModifierKeyState::IsNumLocked() const {
+ return (mModifiers & MODIFIER_NUMLOCK) != 0;
+}
+
+bool ModifierKeyState::IsScrollLocked() const {
+ return (mModifiers & MODIFIER_SCROLLLOCK) != 0;
+}
+
+/*****************************************************************************
+ * mozilla::widget::UniCharsAndModifiers
+ *****************************************************************************/
+
+void UniCharsAndModifiers::Append(char16_t aUniChar, Modifiers aModifiers) {
+ mChars.Append(aUniChar);
+ mModifiers.AppendElement(aModifiers);
+}
+
+void UniCharsAndModifiers::FillModifiers(Modifiers aModifiers) {
+ for (size_t i = 0; i < Length(); i++) {
+ mModifiers[i] = aModifiers;
+ }
+}
+
+void UniCharsAndModifiers::OverwriteModifiersIfBeginsWith(
+ const UniCharsAndModifiers& aOther) {
+ if (!BeginsWith(aOther)) {
+ return;
+ }
+ for (size_t i = 0; i < aOther.Length(); ++i) {
+ mModifiers[i] = aOther.mModifiers[i];
+ }
+}
+
+bool UniCharsAndModifiers::UniCharsEqual(
+ const UniCharsAndModifiers& aOther) const {
+ return mChars.Equals(aOther.mChars);
+}
+
+bool UniCharsAndModifiers::UniCharsCaseInsensitiveEqual(
+ const UniCharsAndModifiers& aOther) const {
+ return mChars.Equals(aOther.mChars, nsCaseInsensitiveStringComparator);
+}
+
+bool UniCharsAndModifiers::BeginsWith(
+ const UniCharsAndModifiers& aOther) const {
+ return StringBeginsWith(mChars, aOther.mChars);
+}
+
+UniCharsAndModifiers& UniCharsAndModifiers::operator+=(
+ const UniCharsAndModifiers& aOther) {
+ mChars.Append(aOther.mChars);
+ mModifiers.AppendElements(aOther.mModifiers);
+ return *this;
+}
+
+UniCharsAndModifiers UniCharsAndModifiers::operator+(
+ const UniCharsAndModifiers& aOther) const {
+ UniCharsAndModifiers result(*this);
+ result += aOther;
+ return result;
+}
+
+/*****************************************************************************
+ * mozilla::widget::VirtualKey
+ *****************************************************************************/
+
+// static
+VirtualKey::ShiftState VirtualKey::ModifiersToShiftState(Modifiers aModifiers) {
+ ShiftState state = 0;
+ if (aModifiers & MODIFIER_SHIFT) {
+ state |= STATE_SHIFT;
+ }
+ if (aModifiers & MODIFIER_ALTGRAPH) {
+ state |= STATE_ALTGRAPH;
+ } else {
+ if (aModifiers & MODIFIER_CONTROL) {
+ state |= STATE_CONTROL;
+ }
+ if (aModifiers & MODIFIER_ALT) {
+ state |= STATE_ALT;
+ }
+ }
+ if (aModifiers & MODIFIER_CAPSLOCK) {
+ state |= STATE_CAPSLOCK;
+ }
+ return state;
+}
+
+// static
+Modifiers VirtualKey::ShiftStateToModifiers(ShiftState aShiftState) {
+ Modifiers modifiers = 0;
+ if (aShiftState & STATE_SHIFT) {
+ modifiers |= MODIFIER_SHIFT;
+ }
+ if (aShiftState & STATE_ALTGRAPH) {
+ modifiers |= MODIFIER_ALTGRAPH;
+ } else {
+ if (aShiftState & STATE_CONTROL) {
+ modifiers |= MODIFIER_CONTROL;
+ }
+ if (aShiftState & STATE_ALT) {
+ modifiers |= MODIFIER_ALT;
+ }
+ }
+ if (aShiftState & STATE_CAPSLOCK) {
+ modifiers |= MODIFIER_CAPSLOCK;
+ }
+ return modifiers;
+}
+
+const DeadKeyTable* VirtualKey::MatchingDeadKeyTable(
+ const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) const {
+ if (!mIsDeadKey) {
+ return nullptr;
+ }
+
+ for (ShiftState shiftState = 0; shiftState < 16; shiftState++) {
+ if (!IsDeadKey(shiftState)) {
+ continue;
+ }
+ const DeadKeyTable* dkt = mShiftStates[shiftState].DeadKey.Table;
+ if (dkt && dkt->IsEqual(aDeadKeyArray, aEntries)) {
+ return dkt;
+ }
+ }
+
+ return nullptr;
+}
+
+void VirtualKey::SetNormalChars(ShiftState aShiftState, const char16_t* aChars,
+ uint32_t aNumOfChars) {
+ MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState));
+
+ SetDeadKey(aShiftState, false);
+
+ for (uint32_t index = 0; index < aNumOfChars; index++) {
+ // Ignore legacy non-printable control characters
+ mShiftStates[aShiftState].Normal.Chars[index] =
+ (aChars[index] >= 0x20) ? aChars[index] : 0;
+ }
+
+ uint32_t len = ArrayLength(mShiftStates[aShiftState].Normal.Chars);
+ for (uint32_t index = aNumOfChars; index < len; index++) {
+ mShiftStates[aShiftState].Normal.Chars[index] = 0;
+ }
+}
+
+void VirtualKey::SetDeadChar(ShiftState aShiftState, char16_t aDeadChar) {
+ MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState));
+
+ SetDeadKey(aShiftState, true);
+
+ mShiftStates[aShiftState].DeadKey.DeadChar = aDeadChar;
+ mShiftStates[aShiftState].DeadKey.Table = nullptr;
+}
+
+UniCharsAndModifiers VirtualKey::GetUniChars(ShiftState aShiftState) const {
+ UniCharsAndModifiers result = GetNativeUniChars(aShiftState);
+
+ const uint8_t kShiftStateIndex = ToShiftStateIndex(aShiftState);
+ if (!(kShiftStateIndex & STATE_CONTROL_ALT)) {
+ // If neither Alt nor Ctrl key is pressed, just return stored data
+ // for the key.
+ return result;
+ }
+
+ if (result.IsEmpty()) {
+ // If Alt and/or Control are pressed and the key produces no
+ // character, return characters which is produced by the key without
+ // Alt and Control, and return given modifiers as is.
+ result = GetNativeUniChars(kShiftStateIndex & ~STATE_CONTROL_ALT);
+ result.FillModifiers(ShiftStateToModifiers(aShiftState));
+ return result;
+ }
+
+ if (IsAltGrIndex(kShiftStateIndex)) {
+ // If AltGr or both Ctrl and Alt are pressed and the key produces
+ // character(s), we need to clear MODIFIER_ALT and MODIFIER_CONTROL
+ // since TextEditor won't handle eKeyPress event whose mModifiers
+ // has MODIFIER_ALT or MODIFIER_CONTROL. Additionally, we need to
+ // use MODIFIER_ALTGRAPH when a key produces character(s) with
+ // AltGr or both Ctrl and Alt on Windows. See following spec issue:
+ // <https://github.com/w3c/uievents/issues/147>
+ Modifiers finalModifiers = ShiftStateToModifiers(aShiftState);
+ finalModifiers &= ~(MODIFIER_ALT | MODIFIER_CONTROL);
+ finalModifiers |= MODIFIER_ALTGRAPH;
+ result.FillModifiers(finalModifiers);
+ return result;
+ }
+
+ // Otherwise, i.e., Alt or Ctrl is pressed and it produces character(s),
+ // check if different character(s) is produced by the key without Alt/Ctrl.
+ // If it produces different character, we need to consume the Alt and
+ // Ctrl modifier for TextEditor. Otherwise, the key does not produces the
+ // character actually. So, keep setting Alt and Ctrl modifiers.
+ UniCharsAndModifiers unmodifiedReslt =
+ GetNativeUniChars(kShiftStateIndex & ~STATE_CONTROL_ALT);
+ if (!result.UniCharsEqual(unmodifiedReslt)) {
+ Modifiers finalModifiers = ShiftStateToModifiers(aShiftState);
+ finalModifiers &= ~(MODIFIER_ALT | MODIFIER_CONTROL);
+ result.FillModifiers(finalModifiers);
+ }
+ return result;
+}
+
+UniCharsAndModifiers VirtualKey::GetNativeUniChars(
+ ShiftState aShiftState) const {
+ const uint8_t kShiftStateIndex = ToShiftStateIndex(aShiftState);
+ UniCharsAndModifiers result;
+ Modifiers modifiers = ShiftStateToModifiers(aShiftState);
+ if (IsDeadKey(aShiftState)) {
+ result.Append(mShiftStates[kShiftStateIndex].DeadKey.DeadChar, modifiers);
+ return result;
+ }
+
+ uint32_t len = ArrayLength(mShiftStates[kShiftStateIndex].Normal.Chars);
+ for (uint32_t i = 0;
+ i < len && mShiftStates[kShiftStateIndex].Normal.Chars[i]; i++) {
+ result.Append(mShiftStates[kShiftStateIndex].Normal.Chars[i], modifiers);
+ }
+ return result;
+}
+
+// static
+void VirtualKey::FillKbdState(PBYTE aKbdState, const ShiftState aShiftState) {
+ if (aShiftState & STATE_SHIFT) {
+ aKbdState[VK_SHIFT] |= 0x80;
+ } else {
+ aKbdState[VK_SHIFT] &= ~0x80;
+ aKbdState[VK_LSHIFT] &= ~0x80;
+ aKbdState[VK_RSHIFT] &= ~0x80;
+ }
+
+ if (aShiftState & STATE_ALTGRAPH) {
+ aKbdState[VK_CONTROL] |= 0x80;
+ aKbdState[VK_LCONTROL] |= 0x80;
+ aKbdState[VK_RCONTROL] &= ~0x80;
+ aKbdState[VK_MENU] |= 0x80;
+ aKbdState[VK_LMENU] &= ~0x80;
+ aKbdState[VK_RMENU] |= 0x80;
+ } else {
+ if (aShiftState & STATE_CONTROL) {
+ aKbdState[VK_CONTROL] |= 0x80;
+ } else {
+ aKbdState[VK_CONTROL] &= ~0x80;
+ aKbdState[VK_LCONTROL] &= ~0x80;
+ aKbdState[VK_RCONTROL] &= ~0x80;
+ }
+
+ if (aShiftState & STATE_ALT) {
+ aKbdState[VK_MENU] |= 0x80;
+ } else {
+ aKbdState[VK_MENU] &= ~0x80;
+ aKbdState[VK_LMENU] &= ~0x80;
+ aKbdState[VK_RMENU] &= ~0x80;
+ }
+ }
+
+ if (aShiftState & STATE_CAPSLOCK) {
+ aKbdState[VK_CAPITAL] |= 0x01;
+ } else {
+ aKbdState[VK_CAPITAL] &= ~0x01;
+ }
+}
+
+/*****************************************************************************
+ * mozilla::widget::NativeKey
+ *****************************************************************************/
+
+uint8_t NativeKey::sDispatchedKeyOfAppCommand = 0;
+NativeKey* NativeKey::sLatestInstance = nullptr;
+const MSG NativeKey::sEmptyMSG = {};
+MSG NativeKey::sLastKeyOrCharMSG = {};
+MSG NativeKey::sLastKeyMSG = {};
+char16_t NativeKey::sPendingHighSurrogate = 0;
+
+NativeKey::NativeKey(nsWindow* aWidget, const MSG& aMessage,
+ const ModifierKeyState& aModKeyState,
+ HKL aOverrideKeyboardLayout,
+ nsTArray<FakeCharMsg>* aFakeCharMsgs)
+ : mLastInstance(sLatestInstance),
+ mRemovingMsg(sEmptyMSG),
+ mReceivedMsg(sEmptyMSG),
+ mWidget(aWidget),
+ mDispatcher(aWidget->GetTextEventDispatcher()),
+ mMsg(aMessage),
+ mFocusedWndBeforeDispatch(::GetFocus()),
+ mDOMKeyCode(0),
+ mKeyNameIndex(KEY_NAME_INDEX_Unidentified),
+ mCodeNameIndex(CODE_NAME_INDEX_UNKNOWN),
+ mModKeyState(aModKeyState),
+ mVirtualKeyCode(0),
+ mOriginalVirtualKeyCode(0),
+ mShiftedLatinChar(0),
+ mUnshiftedLatinChar(0),
+ mScanCode(0),
+ mIsExtended(false),
+ mIsRepeat(false),
+ mIsDeadKey(false),
+ mIsPrintableKey(false),
+ mIsSkippableInRemoteProcess(false),
+ mCharMessageHasGone(false),
+ mCanIgnoreModifierStateAtKeyPress(true),
+ mFakeCharMsgs(aFakeCharMsgs && aFakeCharMsgs->Length() ? aFakeCharMsgs
+ : nullptr) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::NativeKey(aWidget=0x%p { GetWindowHandle()=0x%p }, "
+ "aMessage=%s, aModKeyState=%s), sLatestInstance=0x%p",
+ this, aWidget, aWidget->GetWindowHandle(), ToString(aMessage).get(),
+ ToString(aModKeyState).get(), sLatestInstance));
+
+ MOZ_ASSERT(aWidget);
+ MOZ_ASSERT(mDispatcher);
+ sLatestInstance = this;
+ KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance();
+ mKeyboardLayout = KeyboardLayout::GetLayout();
+ if (aOverrideKeyboardLayout && mKeyboardLayout != aOverrideKeyboardLayout) {
+ keyboardLayout->OverrideLayout(aOverrideKeyboardLayout);
+ mKeyboardLayout = keyboardLayout->GetLoadedLayout();
+ MOZ_ASSERT(mKeyboardLayout == aOverrideKeyboardLayout);
+ mIsOverridingKeyboardLayout = true;
+ } else {
+ mIsOverridingKeyboardLayout = false;
+ sLastKeyOrCharMSG = aMessage;
+ }
+
+ if (mMsg.message == WM_APPCOMMAND) {
+ InitWithAppCommand();
+ } else {
+ InitWithKeyOrChar();
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::NativeKey(), mKeyboardLayout=0x%p, "
+ "mFocusedWndBeforeDispatch=0x%p, mDOMKeyCode=%s, "
+ "mKeyNameIndex=%s, mCodeNameIndex=%s, mModKeyState=%s, "
+ "mVirtualKeyCode=%s, mOriginalVirtualKeyCode=%s, "
+ "mCommittedCharsAndModifiers=%s, mInputtingStringAndModifiers=%s, "
+ "mShiftedString=%s, mUnshiftedString=%s, mShiftedLatinChar=%s, "
+ "mUnshiftedLatinChar=%s, mScanCode=0x%04X, mIsExtended=%s, "
+ "mIsRepeat=%s, mIsDeadKey=%s, mIsPrintableKey=%s, "
+ "mIsSkippableInRemoteProcess=%s, mCharMessageHasGone=%s, "
+ "mIsOverridingKeyboardLayout=%s",
+ this, mKeyboardLayout, mFocusedWndBeforeDispatch,
+ GetDOMKeyCodeName(mDOMKeyCode).get(), ToString(mKeyNameIndex).get(),
+ ToString(mCodeNameIndex).get(), ToString(mModKeyState).get(),
+ GetVirtualKeyCodeName(mVirtualKeyCode).get(),
+ GetVirtualKeyCodeName(mOriginalVirtualKeyCode).get(),
+ ToString(mCommittedCharsAndModifiers).get(),
+ ToString(mInputtingStringAndModifiers).get(),
+ ToString(mShiftedString).get(), ToString(mUnshiftedString).get(),
+ GetCharacterCodeName(mShiftedLatinChar).get(),
+ GetCharacterCodeName(mUnshiftedLatinChar).get(), mScanCode,
+ GetBoolName(mIsExtended), GetBoolName(mIsRepeat),
+ GetBoolName(mIsDeadKey), GetBoolName(mIsPrintableKey),
+ GetBoolName(mIsSkippableInRemoteProcess),
+ GetBoolName(mCharMessageHasGone),
+ GetBoolName(mIsOverridingKeyboardLayout)));
+}
+
+void NativeKey::InitIsSkippableForKeyOrChar(const MSG& aLastKeyMSG) {
+ mIsSkippableInRemoteProcess = false;
+
+ if (!mIsRepeat) {
+ // If the message is not repeated key message, the event should be always
+ // handled in remote process even if it's too old.
+ return;
+ }
+
+ // Keyboard utilities may send us some generated messages and such messages
+ // may be marked as "repeated", e.g., SendInput() calls with
+ // KEYEVENTF_UNICODE but without KEYEVENTF_KEYUP. However, key sequence
+ // comes from such utilities may be really important. For example, utilities
+ // may send WM_KEYDOWN for VK_BACK to remove previous character and send
+ // WM_KEYDOWN for VK_PACKET to insert a composite character. Therefore, we
+ // should check if current message and previous key message are caused by
+ // same physical key. If not, the message may be generated by such
+ // utility.
+ // XXX With this approach, if VK_BACK messages are generated with known
+ // scancode, we cannot distinguish whether coming VK_BACK message is
+ // actually repeated by the auto-repeat feature. Currently, we need
+ // this hack only for "SinhalaTamil IME" and fortunately, it generates
+ // VK_BACK messages with odd scancode. So, we don't need to handle
+ // VK_BACK specially at least for now.
+
+ if (mCodeNameIndex == CODE_NAME_INDEX_UNKNOWN) {
+ // If current event is not caused by physical key operation, it may be
+ // caused by a keyboard utility. If so, the event shouldn't be ignored by
+ // BrowserChild since it want to insert the character, delete a character or
+ // move caret.
+ return;
+ }
+
+ if (mOriginalVirtualKeyCode == VK_PACKET) {
+ // If the message is VK_PACKET, that means that a keyboard utility
+ // tries to insert a character.
+ return;
+ }
+
+ switch (mMsg.message) {
+ case WM_KEYDOWN:
+ case WM_SYSKEYDOWN:
+ case WM_CHAR:
+ case WM_SYSCHAR:
+ case WM_DEADCHAR:
+ case WM_SYSDEADCHAR:
+ // However, some keyboard layouts may send some keyboard messages with
+ // activating the bit. If we dispatch repeated keyboard events, they
+ // may be ignored by BrowserChild due to performance reason. So, we need
+ // to check if actually a physical key is repeated by the auto-repeat
+ // feature.
+ switch (aLastKeyMSG.message) {
+ case WM_KEYDOWN:
+ case WM_SYSKEYDOWN:
+ if (aLastKeyMSG.wParam == VK_PACKET) {
+ // If the last message was VK_PACKET, that means that a keyboard
+ // utility tried to insert a character. So, current message is
+ // not repeated key event of the previous event.
+ return;
+ }
+ // Let's check whether current message and previous message are
+ // caused by same physical key.
+ mIsSkippableInRemoteProcess =
+ mScanCode == WinUtils::GetScanCode(aLastKeyMSG.lParam) &&
+ mIsExtended == WinUtils::IsExtendedScanCode(aLastKeyMSG.lParam);
+ return;
+ default:
+ // If previous message is not a keydown, this must not be generated
+ // by the auto-repeat feature.
+ return;
+ }
+ case WM_APPCOMMAND:
+ MOZ_ASSERT_UNREACHABLE(
+ "WM_APPCOMMAND should be handled in "
+ "InitWithAppCommand()");
+ return;
+ default:
+ // keyup message shouldn't be repeated by the auto-repeat feature.
+ return;
+ }
+}
+
+void NativeKey::InitWithKeyOrChar() {
+ MSG lastKeyMSG = sLastKeyMSG;
+ char16_t pendingHighSurrogate = sPendingHighSurrogate;
+ mScanCode = WinUtils::GetScanCode(mMsg.lParam);
+ mIsExtended = WinUtils::IsExtendedScanCode(mMsg.lParam);
+ switch (mMsg.message) {
+ case WM_KEYDOWN:
+ case WM_SYSKEYDOWN:
+ sPendingHighSurrogate = 0;
+ [[fallthrough]];
+ case WM_KEYUP:
+ case WM_SYSKEYUP: {
+ // Modify sLastKeyMSG now since retrieving following char messages may
+ // cause sending another key message if odd tool hooks GetMessage(),
+ // PeekMessage().
+ sLastKeyMSG = mMsg;
+
+ // Note that we don't need to compute raw virtual keycode here even when
+ // it's VK_PROCESS (i.e., already handled by IME) because we need to
+ // export it as DOM_VK_PROCESS and KEY_NAME_INDEX_Process.
+ mOriginalVirtualKeyCode = static_cast<uint8_t>(mMsg.wParam);
+
+ // If the key message is sent from other application like a11y tools, the
+ // scancode value might not be set proper value. Then, probably the value
+ // is 0.
+ // NOTE: If the virtual keycode can be caused by both non-extended key
+ // and extended key, the API returns the non-extended key's
+ // scancode. E.g., VK_LEFT causes "4" key on numpad.
+ if (!mScanCode && mOriginalVirtualKeyCode != VK_PACKET) {
+ uint16_t scanCodeEx = ComputeScanCodeExFromVirtualKeyCode(mMsg.wParam);
+ if (scanCodeEx) {
+ mScanCode = static_cast<uint8_t>(scanCodeEx & 0xFF);
+ uint8_t extended = static_cast<uint8_t>((scanCodeEx & 0xFF00) >> 8);
+ mIsExtended = (extended == 0xE0) || (extended == 0xE1);
+ }
+ }
+
+ // Most keys are not distinguished as left or right keys.
+ bool isLeftRightDistinguishedKey = false;
+
+ // mOriginalVirtualKeyCode must not distinguish left or right of
+ // Shift, Control or Alt.
+ switch (mOriginalVirtualKeyCode) {
+ case VK_SHIFT:
+ case VK_CONTROL:
+ case VK_MENU:
+ isLeftRightDistinguishedKey = true;
+ break;
+ case VK_LSHIFT:
+ case VK_RSHIFT:
+ mVirtualKeyCode = mOriginalVirtualKeyCode;
+ mOriginalVirtualKeyCode = VK_SHIFT;
+ isLeftRightDistinguishedKey = true;
+ break;
+ case VK_LCONTROL:
+ case VK_RCONTROL:
+ mVirtualKeyCode = mOriginalVirtualKeyCode;
+ mOriginalVirtualKeyCode = VK_CONTROL;
+ isLeftRightDistinguishedKey = true;
+ break;
+ case VK_LMENU:
+ case VK_RMENU:
+ mVirtualKeyCode = mOriginalVirtualKeyCode;
+ mOriginalVirtualKeyCode = VK_MENU;
+ isLeftRightDistinguishedKey = true;
+ break;
+ }
+
+ // If virtual keycode (left-right distinguished keycode) is already
+ // computed, we don't need to do anymore.
+ if (mVirtualKeyCode) {
+ break;
+ }
+
+ // If the keycode doesn't have LR distinguished keycode, we just set
+ // mOriginalVirtualKeyCode to mVirtualKeyCode. Note that don't compute
+ // it from MapVirtualKeyEx() because the scan code might be wrong if
+ // the message is sent/posted by other application. Then, we will compute
+ // unexpected keycode from the scan code.
+ if (!isLeftRightDistinguishedKey) {
+ break;
+ }
+
+ NS_ASSERTION(!mVirtualKeyCode,
+ "mVirtualKeyCode has been computed already");
+
+ // Otherwise, compute the virtual keycode with MapVirtualKeyEx().
+ mVirtualKeyCode = ComputeVirtualKeyCodeFromScanCodeEx();
+
+ // Following code shouldn't be used now because we compute scancode value
+ // if we detect that the sender doesn't set proper scancode.
+ // However, the detection might fail. Therefore, let's keep using this.
+ switch (mOriginalVirtualKeyCode) {
+ case VK_CONTROL:
+ if (mVirtualKeyCode != VK_LCONTROL &&
+ mVirtualKeyCode != VK_RCONTROL) {
+ mVirtualKeyCode = mIsExtended ? VK_RCONTROL : VK_LCONTROL;
+ }
+ break;
+ case VK_MENU:
+ if (mVirtualKeyCode != VK_LMENU && mVirtualKeyCode != VK_RMENU) {
+ mVirtualKeyCode = mIsExtended ? VK_RMENU : VK_LMENU;
+ }
+ break;
+ case VK_SHIFT:
+ if (mVirtualKeyCode != VK_LSHIFT && mVirtualKeyCode != VK_RSHIFT) {
+ // Neither left shift nor right shift is an extended key,
+ // let's use VK_LSHIFT for unknown mapping.
+ mVirtualKeyCode = VK_LSHIFT;
+ }
+ break;
+ default:
+ MOZ_CRASH("Unsupported mOriginalVirtualKeyCode");
+ }
+ break;
+ }
+ case WM_CHAR:
+ case WM_UNICHAR:
+ case WM_SYSCHAR:
+ sPendingHighSurrogate = 0;
+ // If there is another instance and it is trying to remove a char message
+ // from the queue, this message should be handled in the old instance.
+ if (IsAnotherInstanceRemovingCharMessage()) {
+ // XXX Do we need to make mReceivedMsg an array?
+ MOZ_ASSERT(IsEmptyMSG(mLastInstance->mReceivedMsg));
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::InitWithKeyOrChar(), WARNING, detecting another "
+ "instance is trying to remove a char message, so, this instance "
+ "should do nothing, mLastInstance=0x%p, mRemovingMsg=%s, "
+ "mReceivedMsg=%s",
+ this, mLastInstance, ToString(mLastInstance->mRemovingMsg).get(),
+ ToString(mLastInstance->mReceivedMsg).get()));
+ mLastInstance->mReceivedMsg = mMsg;
+ return;
+ }
+
+ // NOTE: If other applications like a11y tools sends WM_*CHAR without
+ // scancode, we cannot compute virtual keycode. I.e., with such
+ // applications, we cannot generate proper KeyboardEvent.code value.
+
+ mVirtualKeyCode = mOriginalVirtualKeyCode =
+ ComputeVirtualKeyCodeFromScanCodeEx();
+ NS_ASSERTION(mVirtualKeyCode, "Failed to compute virtual keycode");
+ break;
+ default: {
+ MOZ_CRASH_UNSAFE_PRINTF("Unsupported message: 0x%04X", mMsg.message);
+ break;
+ }
+ }
+
+ if (!mVirtualKeyCode) {
+ mVirtualKeyCode = mOriginalVirtualKeyCode;
+ }
+
+ KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance();
+ mDOMKeyCode =
+ keyboardLayout->ConvertNativeKeyCodeToDOMKeyCode(mVirtualKeyCode);
+ // Be aware, keyboard utilities can change non-printable keys to printable
+ // keys. In such case, we should make the key value as a printable key.
+ // FYI: IsFollowedByPrintableCharMessage() returns true only when it's
+ // handling a keydown message.
+ mKeyNameIndex =
+ IsFollowedByPrintableCharMessage()
+ ? KEY_NAME_INDEX_USE_STRING
+ : keyboardLayout->ConvertNativeKeyCodeToKeyNameIndex(mVirtualKeyCode);
+ mCodeNameIndex = KeyboardLayout::ConvertScanCodeToCodeNameIndex(
+ GetScanCodeWithExtendedFlag());
+
+ // If next message of WM_(SYS)KEYDOWN is WM_*CHAR message and the key
+ // combination is not reserved by the system, let's consume it now.
+ // TODO: We cannot initialize mCommittedCharsAndModifiers for VK_PACKET
+ // if the message is WM_KEYUP because we don't have preceding
+ // WM_CHAR message.
+ // TODO: Like Edge, we shouldn't dispatch two sets of keyboard events
+ // for a Unicode character in non-BMP because its key value looks
+ // broken and not good thing for our editor if only one keydown or
+ // keypress event's default is prevented. I guess, we should store
+ // key message information globally and we should wait following
+ // WM_KEYDOWN if following WM_CHAR is a part of a Unicode character.
+ if ((mMsg.message == WM_KEYDOWN || mMsg.message == WM_SYSKEYDOWN) &&
+ !IsReservedBySystem()) {
+ MSG charMsg;
+ while (GetFollowingCharMessage(charMsg)) {
+ // Although, got message shouldn't be WM_NULL in desktop apps,
+ // we should keep checking this. FYI: This was added for Metrofox.
+ if (charMsg.message == WM_NULL) {
+ continue;
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::InitWithKeyOrChar(), removed char message, %s",
+ this, ToString(charMsg).get()));
+ Unused << NS_WARN_IF(charMsg.hwnd != mMsg.hwnd);
+ mFollowingCharMsgs.AppendElement(charMsg);
+ }
+ if (mFollowingCharMsgs.Length() == 1) {
+ // If we receive a keydown message for a high-surrogate, a low-surrogate
+ // keydown message **will** and should follow it. We cannot translate the
+ // following WM_KEYDOWN message for the low-surrogate right now since
+ // it's not yet queued into the message queue yet. Therefore, we need to
+ // wait next one to dispatch keypress event with setting its `.key` value
+ // to a surrogate pair rather than setting it to a lone surrogate.
+ // FYI: This may happen with typing a non-BMP character on the touch
+ // keyboard on Windows 10 or later except when an IME is installed. (If
+ // IME is installed, composition is used instead.)
+ if (IS_HIGH_SURROGATE(mFollowingCharMsgs[0].wParam)) {
+ if (pendingHighSurrogate) {
+ MOZ_LOG(gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::InitWithKeyOrChar(), there is pending "
+ "high surrogate input, but received another high surrogate "
+ "input. The previous one is discarded",
+ this));
+ }
+ sPendingHighSurrogate = mFollowingCharMsgs[0].wParam;
+ mFollowingCharMsgs.Clear();
+ } else if (IS_LOW_SURROGATE(mFollowingCharMsgs[0].wParam)) {
+ // If we stopped dispathing a keypress event for a preceding
+ // high-surrogate, treat this keydown (for a low-surrogate) as
+ // introducing both the high surrogate and the low surrogate.
+ if (pendingHighSurrogate) {
+ MSG charMsg = mFollowingCharMsgs[0];
+ mFollowingCharMsgs[0].wParam = pendingHighSurrogate;
+ mFollowingCharMsgs.AppendElement(std::move(charMsg));
+ } else {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::InitWithKeyOrChar(), there is no pending high "
+ "surrogate input, but received lone low surrogate input",
+ this));
+ }
+ } else {
+ MOZ_LOG(gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::InitWithKeyOrChar(), there is pending "
+ "high surrogate input, but received non-surrogate input. "
+ "The high surrogate input is discarded",
+ this));
+ }
+ } else if (!mFollowingCharMsgs.IsEmpty()) {
+ MOZ_LOG(gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::InitWithKeyOrChar(), there is pending "
+ "high surrogate input, but received 2 or more character input. "
+ "The high surrogate input is discarded",
+ this));
+ }
+ }
+
+ keyboardLayout->InitNativeKey(*this);
+
+ // Now, we can know if the key produces character(s) or a dead key with
+ // AltGraph modifier. When user emulates AltGr key press with pressing
+ // both Ctrl and Alt and the key produces character(s) or a dead key, we
+ // need to replace Control and Alt state with AltGraph if the keyboard
+ // layout has AltGr key.
+ // Note that if Ctrl and/or Alt are pressed (not to emulate to press AltGr),
+ // we need to set actual modifiers to eKeyDown and eKeyUp.
+ if (MaybeEmulatingAltGraph() &&
+ (mCommittedCharsAndModifiers.IsProducingCharsWithAltGr() ||
+ mKeyNameIndex == KEY_NAME_INDEX_Dead)) {
+ mModKeyState.Unset(MODIFIER_CONTROL | MODIFIER_ALT);
+ mModKeyState.Set(MODIFIER_ALTGRAPH);
+ }
+
+ mIsDeadKey =
+ (IsFollowedByDeadCharMessage() ||
+ keyboardLayout->IsDeadKey(mOriginalVirtualKeyCode, mModKeyState));
+ mIsPrintableKey = mKeyNameIndex == KEY_NAME_INDEX_USE_STRING ||
+ KeyboardLayout::IsPrintableCharKey(mOriginalVirtualKeyCode);
+ // The repeat count in mMsg.lParam isn't useful to check whether the event
+ // is caused by the auto-repeat feature because it's not incremented even
+ // if it's repeated twice or more (i.e., always 1). Therefore, we need to
+ // check previous key state (31th bit) instead. If it's 1, the key was down
+ // before the message was sent.
+ mIsRepeat = (mMsg.lParam & (1 << 30)) != 0;
+ InitIsSkippableForKeyOrChar(lastKeyMSG);
+
+ if (IsKeyDownMessage()) {
+ // Compute some strings which may be inputted by the key with various
+ // modifier state if this key event won't cause text input actually.
+ // They will be used for setting mAlternativeCharCodes in the callback
+ // method which will be called by TextEventDispatcher.
+ if (!IsFollowedByPrintableCharMessage()) {
+ ComputeInputtingStringWithKeyboardLayout();
+ }
+ // Remove odd char messages if there are.
+ RemoveFollowingOddCharMessages();
+ }
+}
+
+void NativeKey::InitCommittedCharsAndModifiersWithFollowingCharMessages() {
+ mCommittedCharsAndModifiers.Clear();
+ // This should cause inputting text in focused editor. However, it
+ // ignores keypress events whose altKey or ctrlKey is true.
+ // Therefore, we need to remove these modifier state here.
+ Modifiers modifiers = mModKeyState.GetModifiers();
+ if (IsFollowedByPrintableCharMessage()) {
+ modifiers &= ~(MODIFIER_ALT | MODIFIER_CONTROL);
+ if (MaybeEmulatingAltGraph()) {
+ modifiers |= MODIFIER_ALTGRAPH;
+ }
+ }
+ // NOTE: This method assumes that WM_CHAR and WM_SYSCHAR are never retrieved
+ // at same time.
+ for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) {
+ // Ignore non-printable char messages.
+ if (!IsPrintableCharOrSysCharMessage(mFollowingCharMsgs[i])) {
+ continue;
+ }
+ char16_t ch = static_cast<char16_t>(mFollowingCharMsgs[i].wParam);
+ mCommittedCharsAndModifiers.Append(ch, modifiers);
+ }
+}
+
+NativeKey::~NativeKey() {
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::~NativeKey(), destroyed", this));
+ if (mIsOverridingKeyboardLayout) {
+ KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance();
+ keyboardLayout->RestoreLayout();
+ }
+ sLatestInstance = mLastInstance;
+}
+
+void NativeKey::InitWithAppCommand() {
+ if (GET_DEVICE_LPARAM(mMsg.lParam) != FAPPCOMMAND_KEY) {
+ return;
+ }
+
+ uint32_t appCommand = GET_APPCOMMAND_LPARAM(mMsg.lParam);
+ switch (GET_APPCOMMAND_LPARAM(mMsg.lParam)) {
+#undef NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX
+#define NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX(aAppCommand, aKeyNameIndex) \
+ case aAppCommand: \
+ mKeyNameIndex = aKeyNameIndex; \
+ break;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ mKeyNameIndex = KEY_NAME_INDEX_Unidentified;
+ }
+
+ // Guess the virtual keycode which caused this message.
+ switch (appCommand) {
+ case APPCOMMAND_BROWSER_BACKWARD:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_BACK;
+ break;
+ case APPCOMMAND_BROWSER_FORWARD:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_FORWARD;
+ break;
+ case APPCOMMAND_BROWSER_REFRESH:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_REFRESH;
+ break;
+ case APPCOMMAND_BROWSER_STOP:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_STOP;
+ break;
+ case APPCOMMAND_BROWSER_SEARCH:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_SEARCH;
+ break;
+ case APPCOMMAND_BROWSER_FAVORITES:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_FAVORITES;
+ break;
+ case APPCOMMAND_BROWSER_HOME:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_HOME;
+ break;
+ case APPCOMMAND_VOLUME_MUTE:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_VOLUME_MUTE;
+ break;
+ case APPCOMMAND_VOLUME_DOWN:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_VOLUME_DOWN;
+ break;
+ case APPCOMMAND_VOLUME_UP:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_VOLUME_UP;
+ break;
+ case APPCOMMAND_MEDIA_NEXTTRACK:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_NEXT_TRACK;
+ break;
+ case APPCOMMAND_MEDIA_PREVIOUSTRACK:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_PREV_TRACK;
+ break;
+ case APPCOMMAND_MEDIA_STOP:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_STOP;
+ break;
+ case APPCOMMAND_MEDIA_PLAY_PAUSE:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_PLAY_PAUSE;
+ break;
+ case APPCOMMAND_LAUNCH_MAIL:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_MAIL;
+ break;
+ case APPCOMMAND_LAUNCH_MEDIA_SELECT:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_MEDIA_SELECT;
+ break;
+ case APPCOMMAND_LAUNCH_APP1:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_APP1;
+ break;
+ case APPCOMMAND_LAUNCH_APP2:
+ mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_APP2;
+ break;
+ default:
+ return;
+ }
+
+ uint16_t scanCodeEx = ComputeScanCodeExFromVirtualKeyCode(mVirtualKeyCode);
+ mScanCode = static_cast<uint8_t>(scanCodeEx & 0xFF);
+ uint8_t extended = static_cast<uint8_t>((scanCodeEx & 0xFF00) >> 8);
+ mIsExtended = (extended == 0xE0) || (extended == 0xE1);
+ mDOMKeyCode = KeyboardLayout::GetInstance()->ConvertNativeKeyCodeToDOMKeyCode(
+ mOriginalVirtualKeyCode);
+ mCodeNameIndex = KeyboardLayout::ConvertScanCodeToCodeNameIndex(
+ GetScanCodeWithExtendedFlag());
+ // If we can map the WM_APPCOMMAND to a virtual keycode, we can trust
+ // the result of GetKeyboardState(). Otherwise, we dispatch both
+ // keydown and keyup events from WM_APPCOMMAND handler. Therefore,
+ // even if WM_APPCOMMAND is caused by auto key repeat, web apps receive
+ // a pair of DOM keydown and keyup events. I.e., KeyboardEvent.repeat
+ // should be never true of such keys.
+ // XXX Isn't the key state always true? If the key press caused this
+ // WM_APPCOMMAND, that means it's pressed at that time.
+ if (mVirtualKeyCode) {
+ BYTE kbdState[256];
+ memset(kbdState, 0, sizeof(kbdState));
+ ::GetKeyboardState(kbdState);
+ mIsSkippableInRemoteProcess = mIsRepeat = !!kbdState[mVirtualKeyCode];
+ }
+}
+
+bool NativeKey::MaybeEmulatingAltGraph() const {
+ return IsControl() && IsAlt() && KeyboardLayout::GetInstance()->HasAltGr();
+}
+
+// static
+bool NativeKey::IsControlChar(char16_t aChar) {
+ static const char16_t U_SPACE = 0x20;
+ static const char16_t U_DELETE = 0x7F;
+ return aChar < U_SPACE || aChar == U_DELETE;
+}
+
+bool NativeKey::IsFollowedByDeadCharMessage() const {
+ if (mFollowingCharMsgs.IsEmpty()) {
+ return false;
+ }
+ return IsDeadCharMessage(mFollowingCharMsgs[0]);
+}
+
+bool NativeKey::IsFollowedByPrintableCharMessage() const {
+ for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) {
+ if (IsPrintableCharMessage(mFollowingCharMsgs[i])) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool NativeKey::IsFollowedByPrintableCharOrSysCharMessage() const {
+ for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) {
+ if (IsPrintableCharOrSysCharMessage(mFollowingCharMsgs[i])) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool NativeKey::IsReservedBySystem() const {
+ // Alt+Space key is handled by OS, we shouldn't touch it.
+ if (mModKeyState.IsAlt() && !mModKeyState.IsControl() &&
+ mVirtualKeyCode == VK_SPACE) {
+ return true;
+ }
+
+ // XXX How about Alt+F4? We receive WM_SYSKEYDOWN for F4 before closing the
+ // window. Although, we don't prevent to close the window but the key
+ // event shouldn't be exposed to the web.
+
+ return false;
+}
+
+bool NativeKey::IsIMEDoingKakuteiUndo() const {
+ // Following message pattern is caused by "Kakutei-Undo" of ATOK or WXG:
+ // ---------------------------------------------------------------------------
+ // WM_KEYDOWN * n (wParam = VK_BACK, lParam = 0x1)
+ // WM_KEYUP * 1 (wParam = VK_BACK, lParam = 0xC0000001) # ATOK
+ // WM_IME_STARTCOMPOSITION * 1 (wParam = 0x0, lParam = 0x0)
+ // WM_IME_COMPOSITION * 1 (wParam = 0x0, lParam = 0x1BF)
+ // WM_CHAR * n (wParam = VK_BACK, lParam = 0x1)
+ // WM_KEYUP * 1 (wParam = VK_BACK, lParam = 0xC00E0001)
+ // ---------------------------------------------------------------------------
+ // This doesn't match usual key message pattern such as:
+ // WM_KEYDOWN -> WM_CHAR -> WM_KEYDOWN -> WM_CHAR -> ... -> WM_KEYUP
+ // See following bugs for the detail.
+ // https://bugzilla.mozilla.gr.jp/show_bug.cgi?id=2885 (written in Japanese)
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=194559 (written in English)
+ MSG startCompositionMsg, compositionMsg, charMsg;
+ return WinUtils::PeekMessage(&startCompositionMsg, mMsg.hwnd,
+ WM_IME_STARTCOMPOSITION, WM_IME_STARTCOMPOSITION,
+ PM_NOREMOVE | PM_NOYIELD) &&
+ WinUtils::PeekMessage(&compositionMsg, mMsg.hwnd, WM_IME_COMPOSITION,
+ WM_IME_COMPOSITION, PM_NOREMOVE | PM_NOYIELD) &&
+ WinUtils::PeekMessage(&charMsg, mMsg.hwnd, WM_CHAR, WM_CHAR,
+ PM_NOREMOVE | PM_NOYIELD) &&
+ startCompositionMsg.wParam == 0x0 &&
+ startCompositionMsg.lParam == 0x0 && compositionMsg.wParam == 0x0 &&
+ compositionMsg.lParam == 0x1BF && charMsg.wParam == VK_BACK &&
+ charMsg.lParam == 0x1 &&
+ startCompositionMsg.time <= compositionMsg.time &&
+ compositionMsg.time <= charMsg.time;
+}
+
+void NativeKey::RemoveFollowingOddCharMessages() {
+ MOZ_ASSERT(IsKeyDownMessage());
+
+ // If the keydown message is synthesized for automated tests, there is
+ // nothing to do here.
+ if (mFakeCharMsgs) {
+ return;
+ }
+
+ // If there are some following char messages before another key message,
+ // there is nothing to do here.
+ if (!mFollowingCharMsgs.IsEmpty()) {
+ return;
+ }
+
+ // If the handling key isn't Backspace, there is nothing to do here.
+ if (mOriginalVirtualKeyCode != VK_BACK) {
+ return;
+ }
+
+ // If we don't see the odd message pattern, there is nothing to do here.
+ if (!IsIMEDoingKakuteiUndo()) {
+ return;
+ }
+
+ // Otherwise, we need to remove odd WM_CHAR messages for ATOK or WXG (both
+ // of them are Japanese IME).
+ MSG msg;
+ while (WinUtils::PeekMessage(&msg, mMsg.hwnd, WM_CHAR, WM_CHAR,
+ PM_REMOVE | PM_NOYIELD)) {
+ if (msg.message != WM_CHAR) {
+ MOZ_RELEASE_ASSERT(msg.message == WM_NULL,
+ "Unexpected message was removed");
+ continue;
+ }
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::RemoveFollowingOddCharMessages(), removed odd char "
+ "message, %s",
+ this, ToString(msg).get()));
+ mRemovedOddCharMsgs.AppendElement(msg);
+ }
+}
+
+UINT NativeKey::GetScanCodeWithExtendedFlag() const {
+ if (!mIsExtended) {
+ return mScanCode;
+ }
+ return (0xE000 | mScanCode);
+}
+
+uint32_t NativeKey::GetKeyLocation() const {
+ switch (mVirtualKeyCode) {
+ case VK_LSHIFT:
+ case VK_LCONTROL:
+ case VK_LMENU:
+ case VK_LWIN:
+ return eKeyLocationLeft;
+
+ case VK_RSHIFT:
+ case VK_RCONTROL:
+ case VK_RMENU:
+ case VK_RWIN:
+ return eKeyLocationRight;
+
+ case VK_RETURN:
+ // XXX This code assumes that all keyboard drivers use same mapping.
+ return !mIsExtended ? eKeyLocationStandard : eKeyLocationNumpad;
+
+ case VK_INSERT:
+ case VK_DELETE:
+ case VK_END:
+ case VK_DOWN:
+ case VK_NEXT:
+ case VK_LEFT:
+ case VK_CLEAR:
+ case VK_RIGHT:
+ case VK_HOME:
+ case VK_UP:
+ case VK_PRIOR:
+ // XXX This code assumes that all keyboard drivers use same mapping.
+ return mIsExtended ? eKeyLocationStandard : eKeyLocationNumpad;
+
+ // NumLock key isn't included due to IE9's behavior.
+ case VK_NUMPAD0:
+ case VK_NUMPAD1:
+ case VK_NUMPAD2:
+ case VK_NUMPAD3:
+ case VK_NUMPAD4:
+ case VK_NUMPAD5:
+ case VK_NUMPAD6:
+ case VK_NUMPAD7:
+ case VK_NUMPAD8:
+ case VK_NUMPAD9:
+ case VK_DECIMAL:
+ case VK_DIVIDE:
+ case VK_MULTIPLY:
+ case VK_SUBTRACT:
+ case VK_ADD:
+ // Separator key of Brazilian keyboard or JIS keyboard for Mac
+ case VK_ABNT_C2:
+ return eKeyLocationNumpad;
+
+ case VK_SHIFT:
+ case VK_CONTROL:
+ case VK_MENU:
+ NS_WARNING("Failed to decide the key location?");
+ [[fallthrough]];
+
+ default:
+ return eKeyLocationStandard;
+ }
+}
+
+uint8_t NativeKey::ComputeVirtualKeyCodeFromScanCode() const {
+ return static_cast<uint8_t>(
+ ::MapVirtualKeyEx(mScanCode, MAPVK_VSC_TO_VK, mKeyboardLayout));
+}
+
+uint8_t NativeKey::ComputeVirtualKeyCodeFromScanCodeEx() const {
+ // MapVirtualKeyEx() has been improved for supporting extended keys since
+ // Vista. When we call it for mapping a scancode of an extended key and
+ // a virtual keycode, we need to add 0xE000 to the scancode.
+ return static_cast<uint8_t>(::MapVirtualKeyEx(
+ GetScanCodeWithExtendedFlag(), MAPVK_VSC_TO_VK_EX, mKeyboardLayout));
+}
+
+uint16_t NativeKey::ComputeScanCodeExFromVirtualKeyCode(
+ UINT aVirtualKeyCode) const {
+ return static_cast<uint16_t>(
+ ::MapVirtualKeyEx(aVirtualKeyCode, MAPVK_VK_TO_VSC_EX, mKeyboardLayout));
+}
+
+char16_t NativeKey::ComputeUnicharFromScanCode() const {
+ return static_cast<char16_t>(::MapVirtualKeyEx(
+ ComputeVirtualKeyCodeFromScanCode(), MAPVK_VK_TO_CHAR, mKeyboardLayout));
+}
+
+nsEventStatus NativeKey::InitKeyEvent(WidgetKeyboardEvent& aKeyEvent) const {
+ return InitKeyEvent(aKeyEvent, mModKeyState);
+}
+
+nsEventStatus NativeKey::InitKeyEvent(
+ WidgetKeyboardEvent& aKeyEvent,
+ const ModifierKeyState& aModKeyState) const {
+ if (mWidget->Destroyed()) {
+ MOZ_CRASH("NativeKey tries to dispatch a key event on destroyed widget");
+ }
+
+ LayoutDeviceIntPoint point(0, 0);
+ mWidget->InitEvent(aKeyEvent, &point);
+
+ switch (aKeyEvent.mMessage) {
+ case eKeyDown:
+ // If it was followed by a char message but it was consumed by somebody,
+ // we should mark it as consumed because somebody must have handled it
+ // and we should prevent to do "double action" for the key operation.
+ // However, for compatibility with older version and other browsers,
+ // we should dispatch the events even in the web content.
+ if (mCharMessageHasGone) {
+ aKeyEvent.PreventDefaultBeforeDispatch(CrossProcessForwarding::eAllow);
+ }
+ aKeyEvent.mKeyCode = mDOMKeyCode;
+ // Unique id for this keydown event and its associated keypress.
+ sUniqueKeyEventId++;
+ aKeyEvent.mUniqueId = sUniqueKeyEventId;
+ break;
+ case eKeyUp:
+ aKeyEvent.mKeyCode = mDOMKeyCode;
+ // Set defaultPrevented of the key event if the VK_MENU is not a system
+ // key release, so that the menu bar does not trigger. This helps avoid
+ // triggering the menu bar for ALT key accelerators used in assistive
+ // technologies such as Window-Eyes and ZoomText or for switching open
+ // state of IME. On the other hand, we should dispatch the events even
+ // in the web content for compatibility with older version and other
+ // browsers.
+ if (mOriginalVirtualKeyCode == VK_MENU && mMsg.message != WM_SYSKEYUP) {
+ aKeyEvent.PreventDefaultBeforeDispatch(CrossProcessForwarding::eAllow);
+ }
+ break;
+ case eKeyPress:
+ MOZ_ASSERT(!mCharMessageHasGone,
+ "If following char message was consumed by somebody, "
+ "keydown event should be consumed above");
+ aKeyEvent.mUniqueId = sUniqueKeyEventId;
+ break;
+ default:
+ MOZ_CRASH("Invalid event message");
+ }
+
+ aKeyEvent.mIsRepeat = mIsRepeat;
+ aKeyEvent.mMaybeSkippableInRemoteProcess = mIsSkippableInRemoteProcess;
+ aKeyEvent.mKeyNameIndex = mKeyNameIndex;
+ if (mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
+ aKeyEvent.mKeyValue = mCommittedCharsAndModifiers.ToString();
+ }
+ aKeyEvent.mCodeNameIndex = mCodeNameIndex;
+ MOZ_ASSERT(mCodeNameIndex != CODE_NAME_INDEX_USE_STRING);
+ aKeyEvent.mLocation = GetKeyLocation();
+ aModKeyState.InitInputEvent(aKeyEvent);
+
+ KeyboardLayout::NotifyIdleServiceOfUserActivity();
+
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::InitKeyEvent(), initialized, aKeyEvent={ "
+ "mMessage=%s, mKeyNameIndex=%s, mKeyValue=\"%s\", mCodeNameIndex=%s, "
+ "mKeyCode=%s, mLocation=%s, mModifiers=%s, DefaultPrevented()=%s }",
+ this, ToChar(aKeyEvent.mMessage),
+ ToString(aKeyEvent.mKeyNameIndex).get(),
+ NS_ConvertUTF16toUTF8(aKeyEvent.mKeyValue).get(),
+ ToString(aKeyEvent.mCodeNameIndex).get(),
+ GetDOMKeyCodeName(aKeyEvent.mKeyCode).get(),
+ GetKeyLocationName(aKeyEvent.mLocation).get(),
+ GetModifiersName(aKeyEvent.mModifiers).get(),
+ GetBoolName(aKeyEvent.DefaultPrevented())));
+
+ return aKeyEvent.DefaultPrevented() ? nsEventStatus_eConsumeNoDefault
+ : nsEventStatus_eIgnore;
+}
+
+bool NativeKey::DispatchCommandEvent(uint32_t aEventCommand) const {
+ RefPtr<nsAtom> command;
+ switch (aEventCommand) {
+ case APPCOMMAND_BROWSER_BACKWARD:
+ command = nsGkAtoms::Back;
+ break;
+ case APPCOMMAND_BROWSER_FORWARD:
+ command = nsGkAtoms::Forward;
+ break;
+ case APPCOMMAND_BROWSER_REFRESH:
+ command = nsGkAtoms::Reload;
+ break;
+ case APPCOMMAND_BROWSER_STOP:
+ command = nsGkAtoms::Stop;
+ break;
+ case APPCOMMAND_BROWSER_SEARCH:
+ command = nsGkAtoms::Search;
+ break;
+ case APPCOMMAND_BROWSER_FAVORITES:
+ command = nsGkAtoms::Bookmarks;
+ break;
+ case APPCOMMAND_BROWSER_HOME:
+ command = nsGkAtoms::Home;
+ break;
+ case APPCOMMAND_CLOSE:
+ command = nsGkAtoms::Close;
+ break;
+ case APPCOMMAND_FIND:
+ command = nsGkAtoms::Find;
+ break;
+ case APPCOMMAND_HELP:
+ command = nsGkAtoms::Help;
+ break;
+ case APPCOMMAND_NEW:
+ command = nsGkAtoms::New;
+ break;
+ case APPCOMMAND_OPEN:
+ command = nsGkAtoms::Open;
+ break;
+ case APPCOMMAND_PRINT:
+ command = nsGkAtoms::Print;
+ break;
+ case APPCOMMAND_SAVE:
+ command = nsGkAtoms::Save;
+ break;
+ case APPCOMMAND_FORWARD_MAIL:
+ command = nsGkAtoms::ForwardMail;
+ break;
+ case APPCOMMAND_REPLY_TO_MAIL:
+ command = nsGkAtoms::ReplyToMail;
+ break;
+ case APPCOMMAND_SEND_MAIL:
+ command = nsGkAtoms::SendMail;
+ break;
+ case APPCOMMAND_MEDIA_NEXTTRACK:
+ command = nsGkAtoms::NextTrack;
+ break;
+ case APPCOMMAND_MEDIA_PREVIOUSTRACK:
+ command = nsGkAtoms::PreviousTrack;
+ break;
+ case APPCOMMAND_MEDIA_STOP:
+ command = nsGkAtoms::MediaStop;
+ break;
+ case APPCOMMAND_MEDIA_PLAY_PAUSE:
+ command = nsGkAtoms::PlayPause;
+ break;
+ default:
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchCommandEvent(), doesn't dispatch command "
+ "event",
+ this));
+ return false;
+ }
+ WidgetCommandEvent appCommandEvent(true, command, mWidget);
+
+ mWidget->InitEvent(appCommandEvent);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchCommandEvent(), dispatching "
+ "%s app command event...",
+ this, nsAtomCString(command).get()));
+ bool ok =
+ mWidget->DispatchWindowEvent(appCommandEvent) || mWidget->Destroyed();
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchCommandEvent(), dispatched app command event, "
+ "result=%s, mWidget->Destroyed()=%s",
+ this, GetBoolName(ok), GetBoolName(mWidget->Destroyed())));
+ return ok;
+}
+
+bool NativeKey::HandleAppCommandMessage() const {
+ // If the widget has gone, we should do nothing.
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::HandleAppCommandMessage(), WARNING, not handled "
+ "due to "
+ "destroyed the widget",
+ this));
+ return false;
+ }
+
+ // NOTE: Typical behavior of WM_APPCOMMAND caused by key is, WM_APPCOMMAND
+ // message is _sent_ first. Then, the DefaultWndProc will _post_
+ // WM_KEYDOWN message and WM_KEYUP message if the keycode for the
+ // command is available (i.e., mVirtualKeyCode is not 0).
+
+ // NOTE: IntelliType (Microsoft's keyboard utility software) always consumes
+ // WM_KEYDOWN and WM_KEYUP.
+
+ // Let's dispatch keydown message before our chrome handles the command
+ // when the message is caused by a keypress. This behavior makes handling
+ // WM_APPCOMMAND be a default action of the keydown event. This means that
+ // web applications can handle multimedia keys and prevent our default action.
+ // This allow web applications to provide better UX for multimedia keyboard
+ // users.
+ bool dispatchKeyEvent = (GET_DEVICE_LPARAM(mMsg.lParam) == FAPPCOMMAND_KEY);
+ if (dispatchKeyEvent) {
+ // If a plug-in window has focus but it didn't consume the message, our
+ // window receive WM_APPCOMMAND message. In this case, we shouldn't
+ // dispatch KeyboardEvents because an event handler may access the
+ // plug-in process synchronously.
+ dispatchKeyEvent =
+ WinUtils::IsOurProcessWindow(reinterpret_cast<HWND>(mMsg.wParam));
+ }
+
+ bool consumed = false;
+
+ if (dispatchKeyEvent) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ ("%p NativeKey::HandleAppCommandMessage(), FAILED due to "
+ "BeginNativeInputTransaction() failure",
+ this));
+ return true;
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), initializing keydown "
+ "event...",
+ this));
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, mWidget);
+ nsEventStatus status = InitKeyEvent(keydownEvent, mModKeyState);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), tries to dispatch "
+ "keydown event...",
+ this));
+ // NOTE: If the keydown event is consumed by web contents, we shouldn't
+ // continue to handle the command.
+ if (!mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status,
+ const_cast<NativeKey*>(this))) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), keydown event isn't "
+ "dispatched",
+ this));
+ // If keyboard event wasn't fired, there must be composition.
+ // So, we don't need to dispatch a command event.
+ return true;
+ }
+ consumed = status == nsEventStatus_eConsumeNoDefault;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), keydown event was "
+ "dispatched, consumed=%s",
+ this, GetBoolName(consumed)));
+ sDispatchedKeyOfAppCommand = mVirtualKeyCode;
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), keydown event caused "
+ "destroying the widget",
+ this));
+ return true;
+ }
+ }
+
+ // Dispatch a command event or a content command event if the command is
+ // supported.
+ if (!consumed) {
+ uint32_t appCommand = GET_APPCOMMAND_LPARAM(mMsg.lParam);
+ EventMessage contentCommandMessage = eVoidEvent;
+ switch (appCommand) {
+ case APPCOMMAND_BROWSER_BACKWARD:
+ case APPCOMMAND_BROWSER_FORWARD:
+ case APPCOMMAND_BROWSER_REFRESH:
+ case APPCOMMAND_BROWSER_STOP:
+ case APPCOMMAND_BROWSER_SEARCH:
+ case APPCOMMAND_BROWSER_FAVORITES:
+ case APPCOMMAND_BROWSER_HOME:
+ case APPCOMMAND_CLOSE:
+ case APPCOMMAND_FIND:
+ case APPCOMMAND_HELP:
+ case APPCOMMAND_NEW:
+ case APPCOMMAND_OPEN:
+ case APPCOMMAND_PRINT:
+ case APPCOMMAND_SAVE:
+ case APPCOMMAND_FORWARD_MAIL:
+ case APPCOMMAND_REPLY_TO_MAIL:
+ case APPCOMMAND_SEND_MAIL:
+ case APPCOMMAND_MEDIA_NEXTTRACK:
+ case APPCOMMAND_MEDIA_PREVIOUSTRACK:
+ case APPCOMMAND_MEDIA_STOP:
+ case APPCOMMAND_MEDIA_PLAY_PAUSE:
+ // We shouldn't consume the message always because if we don't handle
+ // the message, the sender (typically, utility of keyboard or mouse)
+ // may send other key messages which indicate well known shortcut key.
+ consumed = DispatchCommandEvent(appCommand);
+ break;
+
+ // Use content command for following commands:
+ case APPCOMMAND_COPY:
+ contentCommandMessage = eContentCommandCopy;
+ break;
+ case APPCOMMAND_CUT:
+ contentCommandMessage = eContentCommandCut;
+ break;
+ case APPCOMMAND_PASTE:
+ contentCommandMessage = eContentCommandPaste;
+ break;
+ case APPCOMMAND_REDO:
+ contentCommandMessage = eContentCommandRedo;
+ break;
+ case APPCOMMAND_UNDO:
+ contentCommandMessage = eContentCommandUndo;
+ break;
+ }
+
+ if (contentCommandMessage) {
+ MOZ_ASSERT(!mWidget->Destroyed());
+ WidgetContentCommandEvent contentCommandEvent(true, contentCommandMessage,
+ mWidget);
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), dispatching %s event...",
+ this, ToChar(contentCommandMessage)));
+ mWidget->DispatchWindowEvent(contentCommandEvent);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), dispatched %s event",
+ this, ToChar(contentCommandMessage)));
+ consumed = true;
+
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), %s event caused "
+ "destroying the widget",
+ this, ToChar(contentCommandMessage)));
+ return true;
+ }
+ } else {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), doesn't dispatch "
+ "content "
+ "command event",
+ this));
+ }
+ }
+
+ // Dispatch a keyup event if the command is caused by pressing a key and
+ // the key isn't mapped to a virtual keycode.
+ if (dispatchKeyEvent && !mVirtualKeyCode) {
+ MOZ_ASSERT(!mWidget->Destroyed());
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ ("%p NativeKey::HandleAppCommandMessage(), FAILED due to "
+ "BeginNativeInputTransaction() failure",
+ this));
+ return true;
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), initializing keyup "
+ "event...",
+ this));
+ WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget);
+ nsEventStatus status = InitKeyEvent(keyupEvent, mModKeyState);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), dispatching keyup "
+ "event...",
+ this));
+ // NOTE: Ignore if the keyup event is consumed because keyup event
+ // represents just a physical key event state change.
+ mDispatcher->DispatchKeyboardEvent(eKeyUp, keyupEvent, status,
+ const_cast<NativeKey*>(this));
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), dispatched keyup event",
+ this));
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleAppCommandMessage(), keyup event caused "
+ "destroying the widget",
+ this));
+ return true;
+ }
+ }
+
+ return consumed;
+}
+
+bool NativeKey::HandleKeyDownMessage(bool* aEventDispatched) const {
+ MOZ_ASSERT(IsKeyDownMessage());
+
+ if (aEventDispatched) {
+ *aEventDispatched = false;
+ }
+
+ if (sDispatchedKeyOfAppCommand &&
+ sDispatchedKeyOfAppCommand == mOriginalVirtualKeyCode) {
+ // The multimedia key event has already been dispatch from
+ // HandleAppCommandMessage().
+ sDispatchedKeyOfAppCommand = 0;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keydown "
+ "event due to already dispatched from HandleAppCommandMessage(), ",
+ this));
+ if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) {
+ RedirectedKeyDownMessageManager::Forget();
+ }
+ return true;
+ }
+
+ if (IsReservedBySystem()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keydown "
+ "event because the key combination is reserved by the system",
+ this));
+ if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) {
+ RedirectedKeyDownMessageManager::Forget();
+ }
+ return false;
+ }
+
+ if (sPendingHighSurrogate) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keydown "
+ "event because the key introduced only a high surrotate, so we "
+ "should wait the following low surrogate input",
+ this));
+ if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) {
+ RedirectedKeyDownMessageManager::Forget();
+ }
+ return false;
+ }
+
+ // If the widget has gone, we should do nothing.
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::HandleKeyDownMessage(), WARNING, not handled due to "
+ "destroyed the widget",
+ this));
+ if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) {
+ RedirectedKeyDownMessageManager::Forget();
+ }
+ return false;
+ }
+
+ bool defaultPrevented = false;
+ if (mFakeCharMsgs ||
+ !RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) {
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ ("%p NativeKey::HandleKeyDownMessage(), FAILED due to "
+ "BeginNativeInputTransaction() failure",
+ this));
+ return true;
+ }
+
+ bool isIMEEnabled = WinUtils::IsIMEEnabled(mWidget->GetInputContext());
+
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::HandleKeyDownMessage(), initializing keydown "
+ "event...",
+ this));
+
+ WidgetKeyboardEvent keydownEvent(true, eKeyDown, mWidget);
+ nsEventStatus status = InitKeyEvent(keydownEvent, mModKeyState);
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), dispatching keydown event...",
+ this));
+ bool dispatched = mDispatcher->DispatchKeyboardEvent(
+ eKeyDown, keydownEvent, status, const_cast<NativeKey*>(this));
+ if (aEventDispatched) {
+ *aEventDispatched = dispatched;
+ }
+ if (!dispatched) {
+ // If the keydown event wasn't fired, there must be composition.
+ // we don't need to do anything anymore.
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keypress "
+ "event(s) because keydown event isn't dispatched actually",
+ this));
+ return false;
+ }
+ defaultPrevented = status == nsEventStatus_eConsumeNoDefault;
+
+ if (mWidget->Destroyed() || IsFocusedWindowChanged()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), keydown event caused "
+ "destroying the widget",
+ this));
+ return true;
+ }
+
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), dispatched keydown event, "
+ "dispatched=%s, defaultPrevented=%s",
+ this, GetBoolName(dispatched), GetBoolName(defaultPrevented)));
+
+ // If IMC wasn't associated to the window but is associated it now (i.e.,
+ // focus is moved from a non-editable editor to an editor by keydown
+ // event handler), WM_CHAR and WM_SYSCHAR shouldn't cause first character
+ // inputting if IME is opened. But then, we should redirect the native
+ // keydown message to IME.
+ // However, note that if focus has been already moved to another
+ // application, we shouldn't redirect the message to it because the keydown
+ // message is processed by us, so, nobody shouldn't process it.
+ HWND focusedWnd = ::GetFocus();
+ if (!defaultPrevented && !mFakeCharMsgs && focusedWnd && !isIMEEnabled &&
+ WinUtils::IsIMEEnabled(mWidget->GetInputContext())) {
+ RedirectedKeyDownMessageManager::RemoveNextCharMessage(focusedWnd);
+
+ INPUT keyinput;
+ keyinput.type = INPUT_KEYBOARD;
+ keyinput.ki.wVk = mOriginalVirtualKeyCode;
+ keyinput.ki.wScan = mScanCode;
+ keyinput.ki.dwFlags = KEYEVENTF_SCANCODE;
+ if (mIsExtended) {
+ keyinput.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
+ }
+ keyinput.ki.time = 0;
+ keyinput.ki.dwExtraInfo = 0;
+
+ RedirectedKeyDownMessageManager::WillRedirect(mMsg, defaultPrevented);
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), redirecting %s...",
+ this, ToString(mMsg).get()));
+
+ ::SendInput(1, &keyinput, sizeof(keyinput));
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), redirected %s", this,
+ ToString(mMsg).get()));
+
+ // Return here. We shouldn't dispatch keypress event for this WM_KEYDOWN.
+ // If it's needed, it will be dispatched after next (redirected)
+ // WM_KEYDOWN.
+ return true;
+ }
+ } else {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), received a redirected %s",
+ this, ToString(mMsg).get()));
+
+ defaultPrevented = RedirectedKeyDownMessageManager::DefaultPrevented();
+ // If this is redirected keydown message, we have dispatched the keydown
+ // event already.
+ if (aEventDispatched) {
+ *aEventDispatched = true;
+ }
+ }
+
+ RedirectedKeyDownMessageManager::Forget();
+
+ MOZ_ASSERT(!mWidget->Destroyed());
+
+ // If the key was processed by IME and didn't cause WM_(SYS)CHAR messages, we
+ // shouldn't dispatch keypress event.
+ if (mOriginalVirtualKeyCode == VK_PROCESSKEY &&
+ !IsFollowedByPrintableCharOrSysCharMessage()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress "
+ "event because the key was already handled by IME, "
+ "defaultPrevented=%s",
+ this, GetBoolName(defaultPrevented)));
+ return defaultPrevented;
+ }
+
+ if (defaultPrevented) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress "
+ "event because preceding keydown event was consumed",
+ this));
+ return true;
+ }
+
+ MOZ_ASSERT(!mCharMessageHasGone,
+ "If following char message was consumed by somebody, "
+ "keydown event should have been consumed before dispatch");
+
+ // If mCommittedCharsAndModifiers was initialized with following char
+ // messages, we should dispatch keypress events with its information.
+ if (IsFollowedByPrintableCharOrSysCharMessage()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), tries to be dispatching "
+ "keypress events with retrieved char messages...",
+ this));
+ return DispatchKeyPressEventsWithRetrievedCharMessages();
+ }
+
+ // If we won't be getting a WM_CHAR, WM_SYSCHAR or WM_DEADCHAR, synthesize a
+ // keypress for almost all keys
+ if (NeedsToHandleWithoutFollowingCharMessages()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), tries to be dispatching "
+ "keypress events...",
+ this));
+ return DispatchKeyPressEventsWithoutCharMessage();
+ }
+
+ // If WM_KEYDOWN of VK_PACKET isn't followed by WM_CHAR, we don't need to
+ // dispatch keypress events.
+ if (mVirtualKeyCode == VK_PACKET) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress "
+ "event "
+ "because the key is VK_PACKET and there are no char messages",
+ this));
+ return false;
+ }
+
+ if (!mModKeyState.IsControl() && !mModKeyState.IsAlt() &&
+ !mModKeyState.IsWin() && mIsPrintableKey) {
+ // If this is simple KeyDown event but next message is not WM_CHAR,
+ // this event may not input text, so we should ignore this event.
+ // See bug 314130.
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress "
+ "event "
+ "because the key event is simple printable key's event but not "
+ "followed "
+ "by char messages",
+ this));
+ return false;
+ }
+
+ if (mIsDeadKey) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress "
+ "event "
+ "because the key is a dead key and not followed by char messages",
+ this));
+ return false;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyDownMessage(), tries to be dispatching "
+ "keypress events due to no following char messages...",
+ this));
+ return DispatchKeyPressEventsWithoutCharMessage();
+}
+
+bool NativeKey::HandleCharMessage(bool* aEventDispatched) const {
+ MOZ_ASSERT(IsCharOrSysCharMessage(mMsg));
+ return HandleCharMessage(mMsg, aEventDispatched);
+}
+
+bool NativeKey::HandleCharMessage(const MSG& aCharMsg,
+ bool* aEventDispatched) const {
+ MOZ_ASSERT(IsKeyDownMessage() || IsCharOrSysCharMessage(mMsg));
+ MOZ_ASSERT(IsCharOrSysCharMessage(aCharMsg.message));
+
+ if (aEventDispatched) {
+ *aEventDispatched = false;
+ }
+
+ if ((IsCharOrSysCharMessage(mMsg) || IsEnterKeyPressCharMessage(mMsg)) &&
+ IsAnotherInstanceRemovingCharMessage()) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::HandleCharMessage(), WARNING, does nothing because "
+ "the message should be handled in another instance removing this "
+ "message",
+ this));
+ // Consume this for now because it will be handled by another instance.
+ return true;
+ }
+
+ // If the key combinations is reserved by the system, we shouldn't dispatch
+ // eKeyPress event for it and passes the message to next wndproc.
+ if (IsReservedBySystem()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleCharMessage(), doesn't dispatch keypress "
+ "event because the key combination is reserved by the system",
+ this));
+ return false;
+ }
+
+ // If the widget has gone, we should do nothing.
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::HandleCharMessage(), WARNING, not handled due to "
+ "destroyed the widget",
+ this));
+ return false;
+ }
+
+ // When a control key is inputted by a key, it should be handled without
+ // WM_*CHAR messages at receiving WM_*KEYDOWN message. So, when we receive
+ // WM_*CHAR message directly, we see a control character here.
+ // Note that when the char is '\r', it means that the char message should
+ // cause "Enter" keypress event for inserting a line break.
+ if (IsControlCharMessage(aCharMsg) && !IsEnterKeyPressCharMessage(aCharMsg)) {
+ // In this case, we don't need to dispatch eKeyPress event because:
+ // 1. We're the only browser which dispatches "keypress" event for
+ // non-printable characters (Although, both Chrome and Edge dispatch
+ // "keypress" event for some keys accidentally. For example, "IntlRo"
+ // key with Ctrl of Japanese keyboard layout).
+ // 2. Currently, we may handle shortcut keys with "keydown" event if
+ // it's reserved or something. So, we shouldn't dispatch "keypress"
+ // event without it.
+ // Note that this does NOT mean we stop dispatching eKeyPress event for
+ // key presses causes a control character when Ctrl is pressed. In such
+ // case, DispatchKeyPressEventsWithoutCharMessage() dispatches eKeyPress
+ // instead of this method.
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleCharMessage(), doesn't dispatch keypress "
+ "event because received a control character input without WM_KEYDOWN",
+ this));
+ return false;
+ }
+
+ // XXXmnakano I think that if mMsg is WM_CHAR, i.e., it comes without
+ // preceding WM_KEYDOWN, we should should dispatch composition
+ // events instead of eKeyPress because they are not caused by
+ // actual keyboard operation.
+
+ // First, handle normal text input or non-printable key case here.
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, mWidget);
+ if (IsEnterKeyPressCharMessage(aCharMsg)) {
+ keypressEvent.mKeyCode = NS_VK_RETURN;
+ } else {
+ keypressEvent.mCharCode = static_cast<uint32_t>(aCharMsg.wParam);
+ }
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ ("%p NativeKey::HandleCharMessage(), FAILED due to "
+ "BeginNativeInputTransaction() failure",
+ this));
+ return true;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::HandleCharMessage(), initializing keypress "
+ "event...",
+ this));
+
+ ModifierKeyState modKeyState(mModKeyState);
+ // When AltGr is pressed, both Alt and Ctrl are active. However, when they
+ // are active, TextEditor won't treat the keypress event as inputting a
+ // character. Therefore, when AltGr is pressed and the key tries to input
+ // a character, let's set them to false.
+ if (modKeyState.IsControl() && modKeyState.IsAlt() &&
+ IsPrintableCharMessage(aCharMsg)) {
+ modKeyState.Unset(MODIFIER_ALT | MODIFIER_CONTROL);
+ }
+ nsEventStatus status = InitKeyEvent(keypressEvent, modKeyState);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleCharMessage(), dispatching keypress event...",
+ this));
+ bool dispatched = mDispatcher->MaybeDispatchKeypressEvents(
+ keypressEvent, status, const_cast<NativeKey*>(this));
+ if (aEventDispatched) {
+ *aEventDispatched = dispatched;
+ }
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleCharMessage(), keypress event caused "
+ "destroying the widget",
+ this));
+ return true;
+ }
+ bool consumed = status == nsEventStatus_eConsumeNoDefault;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleCharMessage(), dispatched keypress event, "
+ "dispatched=%s, consumed=%s",
+ this, GetBoolName(dispatched), GetBoolName(consumed)));
+ return consumed;
+}
+
+bool NativeKey::HandleKeyUpMessage(bool* aEventDispatched) const {
+ MOZ_ASSERT(IsKeyUpMessage());
+
+ if (aEventDispatched) {
+ *aEventDispatched = false;
+ }
+
+ // If the key combinations is reserved by the system, we shouldn't dispatch
+ // eKeyUp event for it and passes the message to next wndproc.
+ if (IsReservedBySystem()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyUpMessage(), doesn't dispatch keyup "
+ "event because the key combination is reserved by the system",
+ this));
+ return false;
+ }
+
+ if (sPendingHighSurrogate) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyUpMessage(), doesn't dispatch keyup "
+ "event because the key introduced only a high surrotate, so we "
+ "should wait the following low surrogate input",
+ this));
+ return false;
+ }
+
+ // If the widget has gone, we should do nothing.
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::HandleKeyUpMessage(), WARNING, not handled due to "
+ "destroyed the widget",
+ this));
+ return false;
+ }
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ ("%p NativeKey::HandleKeyUpMessage(), FAILED due to "
+ "BeginNativeInputTransaction() failure",
+ this));
+ return true;
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::HandleKeyUpMessage(), initializing keyup event...",
+ this));
+ WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget);
+ nsEventStatus status = InitKeyEvent(keyupEvent, mModKeyState);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyUpMessage(), dispatching keyup event...",
+ this));
+ bool dispatched = mDispatcher->DispatchKeyboardEvent(
+ eKeyUp, keyupEvent, status, const_cast<NativeKey*>(this));
+ if (aEventDispatched) {
+ *aEventDispatched = dispatched;
+ }
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyUpMessage(), keyup event caused "
+ "destroying the widget",
+ this));
+ return true;
+ }
+ bool consumed = status == nsEventStatus_eConsumeNoDefault;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::HandleKeyUpMessage(), dispatched keyup event, "
+ "dispatched=%s, consumed=%s",
+ this, GetBoolName(dispatched), GetBoolName(consumed)));
+ return consumed;
+}
+
+bool NativeKey::NeedsToHandleWithoutFollowingCharMessages() const {
+ MOZ_ASSERT(IsKeyDownMessage());
+
+ // If the key combination is reserved by the system, the caller shouldn't
+ // do anything with following WM_*CHAR messages. So, let's return true here.
+ if (IsReservedBySystem()) {
+ return true;
+ }
+
+ // If the keydown message is generated for inputting some Unicode characters
+ // via SendInput() API, we need to handle it only with WM_*CHAR messages.
+ if (mVirtualKeyCode == VK_PACKET) {
+ return false;
+ }
+
+ // If following char message is for a control character, it should be handled
+ // without WM_CHAR message. This is typically Ctrl + [a-z].
+ if (mFollowingCharMsgs.Length() == 1 &&
+ IsControlCharMessage(mFollowingCharMsgs[0])) {
+ return true;
+ }
+
+ // If keydown message is followed by WM_CHAR or WM_SYSCHAR whose wParam isn't
+ // a control character, we should dispatch keypress event with the char
+ // message even with any modifier state.
+ if (IsFollowedByPrintableCharOrSysCharMessage()) {
+ return false;
+ }
+
+ // If any modifier keys which may cause printable keys becoming non-printable
+ // are not pressed, we don't need special handling for the key.
+ // Note that if the key does not produce a character with AltGr and when
+ // AltGr key is pressed, we don't need to dispatch eKeyPress event for it
+ // because AltGr shouldn't be used for a modifier for a shortcut without
+ // Ctrl, Alt or Win. That means that we should treat it in same path for
+ // Shift key.
+ if (!mModKeyState.IsControl() && !mModKeyState.IsAlt() &&
+ !mModKeyState.IsWin()) {
+ return false;
+ }
+
+ // If the key event causes dead key event, we don't need to dispatch keypress
+ // event.
+ if (mIsDeadKey && mCommittedCharsAndModifiers.IsEmpty()) {
+ return false;
+ }
+
+ // Even if the key is a printable key, it might cause non-printable character
+ // input with modifier key(s).
+ return mIsPrintableKey;
+}
+
+static nsCString GetResultOfInSendMessageEx() {
+ DWORD ret = ::InSendMessageEx(nullptr);
+ if (!ret) {
+ return "ISMEX_NOSEND"_ns;
+ }
+ nsCString result;
+ if (ret & ISMEX_CALLBACK) {
+ result = "ISMEX_CALLBACK";
+ }
+ if (ret & ISMEX_NOTIFY) {
+ if (!result.IsEmpty()) {
+ result += " | ";
+ }
+ result += "ISMEX_NOTIFY";
+ }
+ if (ret & ISMEX_REPLIED) {
+ if (!result.IsEmpty()) {
+ result += " | ";
+ }
+ result += "ISMEX_REPLIED";
+ }
+ if (ret & ISMEX_SEND) {
+ if (!result.IsEmpty()) {
+ result += " | ";
+ }
+ result += "ISMEX_SEND";
+ }
+ return result;
+}
+
+bool NativeKey::MayBeSameCharMessage(const MSG& aCharMsg1,
+ const MSG& aCharMsg2) const {
+ // NOTE: Although, we don't know when this case occurs, the scan code value
+ // in lParam may be changed from 0 to something. The changed value
+ // is different from the scan code of handling keydown message.
+ static const LPARAM kScanCodeMask = 0x00FF0000;
+ return aCharMsg1.message == aCharMsg2.message &&
+ aCharMsg1.wParam == aCharMsg2.wParam &&
+ (aCharMsg1.lParam & ~kScanCodeMask) ==
+ (aCharMsg2.lParam & ~kScanCodeMask);
+}
+
+bool NativeKey::IsSamePhysicalKeyMessage(const MSG& aKeyOrCharMsg1,
+ const MSG& aKeyOrCharMsg2) const {
+ if (NS_WARN_IF(aKeyOrCharMsg1.message < WM_KEYFIRST) ||
+ NS_WARN_IF(aKeyOrCharMsg1.message > WM_KEYLAST) ||
+ NS_WARN_IF(aKeyOrCharMsg2.message < WM_KEYFIRST) ||
+ NS_WARN_IF(aKeyOrCharMsg2.message > WM_KEYLAST)) {
+ return false;
+ }
+ return WinUtils::GetScanCode(aKeyOrCharMsg1.lParam) ==
+ WinUtils::GetScanCode(aKeyOrCharMsg2.lParam) &&
+ WinUtils::IsExtendedScanCode(aKeyOrCharMsg1.lParam) ==
+ WinUtils::IsExtendedScanCode(aKeyOrCharMsg2.lParam);
+}
+
+bool NativeKey::GetFollowingCharMessage(MSG& aCharMsg) {
+ MOZ_ASSERT(IsKeyDownMessage());
+
+ aCharMsg.message = WM_NULL;
+
+ if (mFakeCharMsgs) {
+ for (size_t i = 0; i < mFakeCharMsgs->Length(); i++) {
+ FakeCharMsg& fakeCharMsg = mFakeCharMsgs->ElementAt(i);
+ if (fakeCharMsg.mConsumed) {
+ continue;
+ }
+ MSG charMsg = fakeCharMsg.GetCharMsg(mMsg.hwnd);
+ fakeCharMsg.mConsumed = true;
+ if (!IsCharMessage(charMsg)) {
+ return false;
+ }
+ aCharMsg = charMsg;
+ return true;
+ }
+ return false;
+ }
+
+ // If next key message is not char message, we should give up to find a
+ // related char message for the handling keydown event for now.
+ // Note that it's possible other applications may send other key message
+ // after we call TranslateMessage(). That may cause PeekMessage() failing
+ // to get char message for the handling keydown message.
+ MSG nextKeyMsg;
+ if (!WinUtils::PeekMessage(&nextKeyMsg, mMsg.hwnd, WM_KEYFIRST, WM_KEYLAST,
+ PM_NOREMOVE | PM_NOYIELD) ||
+ !IsCharMessage(nextKeyMsg)) {
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::GetFollowingCharMessage(), there are no char "
+ "messages",
+ this));
+ return false;
+ }
+ const MSG kFoundCharMsg = nextKeyMsg;
+
+ AutoRestore<MSG> saveLastRemovingMsg(mRemovingMsg);
+ mRemovingMsg = nextKeyMsg;
+
+ mReceivedMsg = sEmptyMSG;
+ AutoRestore<MSG> ensureToClearRecivedMsg(mReceivedMsg);
+
+ // On Metrofox, PeekMessage() sometimes returns WM_NULL even if we specify
+ // the message range. So, if it returns WM_NULL, we should retry to get
+ // the following char message it was found above.
+ for (uint32_t i = 0; i < 50; i++) {
+ MSG removedMsg, nextKeyMsgInAllWindows;
+ bool doCrash = false;
+ if (!WinUtils::PeekMessage(&removedMsg, mMsg.hwnd, nextKeyMsg.message,
+ nextKeyMsg.message, PM_REMOVE | PM_NOYIELD)) {
+ // We meets unexpected case. We should collect the message queue state
+ // and crash for reporting the bug.
+ doCrash = true;
+
+ // If another instance was created for the removing message during trying
+ // to remove a char message, the instance didn't handle it for preventing
+ // recursive handling. So, let's handle it in this instance.
+ if (!IsEmptyMSG(mReceivedMsg)) {
+ // If focus is moved to different window, we shouldn't handle it on
+ // the widget. Let's discard it for now.
+ if (mReceivedMsg.hwnd != nextKeyMsg.hwnd) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, received a "
+ "char message during removing it from the queue, but it's for "
+ "different window, mReceivedMsg=%s, nextKeyMsg=%s, "
+ "kFoundCharMsg=%s",
+ this, ToString(mReceivedMsg).get(), ToString(nextKeyMsg).get(),
+ ToString(kFoundCharMsg).get()));
+ // There might still exist char messages, the loop of calling
+ // this method should be continued.
+ aCharMsg.message = WM_NULL;
+ return true;
+ }
+ // Even if the received message is different from what we tried to
+ // remove from the queue, let's take the received message as a part of
+ // the result of this key sequence.
+ if (mReceivedMsg.message != nextKeyMsg.message ||
+ mReceivedMsg.wParam != nextKeyMsg.wParam ||
+ mReceivedMsg.lParam != nextKeyMsg.lParam) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, received a "
+ "char message during removing it from the queue, but it's "
+ "differnt from what trying to remove from the queue, "
+ "aCharMsg=%s, nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(mReceivedMsg).get(), ToString(nextKeyMsg).get(),
+ ToString(kFoundCharMsg).get()));
+ } else {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::GetFollowingCharMessage(), succeeded to "
+ "retrieve next char message via another instance, aCharMsg=%s, "
+ "kFoundCharMsg=%s",
+ this, ToString(mReceivedMsg).get(),
+ ToString(kFoundCharMsg).get()));
+ }
+ aCharMsg = mReceivedMsg;
+ return true;
+ }
+
+ // The char message is redirected to different thread's window by focus
+ // move or something or just cancelled by external application.
+ if (!WinUtils::PeekMessage(&nextKeyMsgInAllWindows, 0, WM_KEYFIRST,
+ WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message, but it's already gone from all message "
+ "queues, nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get()));
+ return true; // XXX should return false in this case
+ }
+ // The next key message is redirected to different window created by our
+ // thread, we should do nothing because we must not have focus.
+ if (nextKeyMsgInAllWindows.hwnd != mMsg.hwnd) {
+ aCharMsg = nextKeyMsgInAllWindows;
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message, but found in another message queue, "
+ "nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(nextKeyMsgInAllWindows).get(),
+ ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get()));
+ return true;
+ }
+ // If next key message becomes non-char message, this key operation
+ // may have already been consumed or canceled.
+ if (!IsCharMessage(nextKeyMsgInAllWindows)) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message and next key message becomes non-char "
+ "message, nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, "
+ "kFoundCharMsg=%s",
+ this, ToString(nextKeyMsgInAllWindows).get(),
+ ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get()));
+ MOZ_ASSERT(!mCharMessageHasGone);
+ mFollowingCharMsgs.Clear();
+ mCharMessageHasGone = true;
+ return false;
+ }
+ // If next key message is still a char message but different key message,
+ // we should treat current key operation is consumed or canceled and
+ // next char message should be handled as an orphan char message later.
+ if (!IsSamePhysicalKeyMessage(nextKeyMsgInAllWindows, kFoundCharMsg)) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message and next key message becomes differnt "
+ "key's "
+ "char message, nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, "
+ "kFoundCharMsg=%s",
+ this, ToString(nextKeyMsgInAllWindows).get(),
+ ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get()));
+ MOZ_ASSERT(!mCharMessageHasGone);
+ mFollowingCharMsgs.Clear();
+ mCharMessageHasGone = true;
+ return false;
+ }
+ // If next key message is still a char message but the message is changed,
+ // we should retry to remove the new message with PeekMessage() again.
+ if (nextKeyMsgInAllWindows.message != nextKeyMsg.message) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message due to message change, let's retry to "
+ "remove the message with newly found char message, "
+ "nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(nextKeyMsgInAllWindows).get(),
+ ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get()));
+ nextKeyMsg = nextKeyMsgInAllWindows;
+ continue;
+ }
+ // If there is still existing a char message caused by same physical key
+ // in the queue but PeekMessage(PM_REMOVE) failed to remove it from the
+ // queue, it might be possible that the odd keyboard layout or utility
+ // hooks only PeekMessage(PM_NOREMOVE) and GetMessage(). So, let's try
+ // remove the char message with GetMessage() again.
+ // FYI: The wParam might be different from the found message, but it's
+ // okay because we assume that odd keyboard layouts return actual
+ // inputting character at removing the char message.
+ if (WinUtils::GetMessage(&removedMsg, mMsg.hwnd, nextKeyMsg.message,
+ nextKeyMsg.message)) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message, but succeeded with GetMessage(), "
+ "removedMsg=%s, kFoundCharMsg=%s",
+ this, ToString(removedMsg).get(), ToString(kFoundCharMsg).get()));
+ // Cancel to crash, but we need to check the removed message value.
+ doCrash = false;
+ }
+ // If we've already removed some WM_NULL messages from the queue and
+ // the found message has already gone from the queue, let's treat the key
+ // as inputting no characters and already consumed.
+ else if (i > 0) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message, but removed %d WM_NULL messages",
+ this, i));
+ // If the key is a printable key or a control key but tried to input
+ // a character, mark mCharMessageHasGone true for handling the keydown
+ // event as inputting empty string.
+ MOZ_ASSERT(!mCharMessageHasGone);
+ mFollowingCharMsgs.Clear();
+ mCharMessageHasGone = true;
+ return false;
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ ("%p NativeKey::GetFollowingCharMessage(), FAILED, lost target "
+ "message to remove, nextKeyMsg=%s",
+ this, ToString(nextKeyMsg).get()));
+ }
+
+ if (doCrash) {
+ nsPrintfCString info(
+ "\nPeekMessage() failed to remove char message! "
+ "\nActive keyboard layout=0x%p (%s), "
+ "\nHandling message: %s, InSendMessageEx()=%s, "
+ "\nFound message: %s, "
+ "\nWM_NULL has been removed: %d, "
+ "\nNext key message in all windows: %s, "
+ "time=%ld, ",
+ KeyboardLayout::GetInstance()->GetLoadedLayout(),
+ KeyboardLayout::GetInstance()->GetLoadedLayoutName().get(),
+ ToString(mMsg).get(), GetResultOfInSendMessageEx().get(),
+ ToString(kFoundCharMsg).get(), i,
+ ToString(nextKeyMsgInAllWindows).get(), nextKeyMsgInAllWindows.time);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MSG nextMsg;
+ if (WinUtils::PeekMessage(&nextMsg, 0, 0, 0, PM_NOREMOVE | PM_NOYIELD)) {
+ nsPrintfCString info("\nNext message in all windows: %s, time=%ld",
+ ToString(nextMsg).get(), nextMsg.time);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ } else {
+ CrashReporter::AppendAppNotesToCrashReport(
+ "\nThere is no message in any window"_ns);
+ }
+
+ MOZ_CRASH("We lost the following char message");
+ }
+
+ // We're still not sure why ::PeekMessage() may return WM_NULL even with
+ // its first message and its last message are same message. However,
+ // at developing Metrofox, we met this case even with usual keyboard
+ // layouts. So, it might be possible in desktop application or it really
+ // occurs with some odd keyboard layouts which perhaps hook API.
+ if (removedMsg.message == WM_NULL) {
+ MOZ_LOG(gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message, instead, removed WM_NULL message, "
+ "removedMsg=%s",
+ this, ToString(removedMsg).get()));
+ // Check if there is the message which we're trying to remove.
+ MSG newNextKeyMsg;
+ if (!WinUtils::PeekMessage(&newNextKeyMsg, mMsg.hwnd, WM_KEYFIRST,
+ WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) {
+ // If there is no key message, we should mark this keydown as consumed
+ // because the key operation may have already been handled or canceled.
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message because it's gone during removing it from "
+ "the queue, nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get()));
+ MOZ_ASSERT(!mCharMessageHasGone);
+ mFollowingCharMsgs.Clear();
+ mCharMessageHasGone = true;
+ return false;
+ }
+ if (!IsCharMessage(newNextKeyMsg)) {
+ // If next key message becomes a non-char message, we should mark this
+ // keydown as consumed because the key operation may have already been
+ // handled or canceled.
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to "
+ "remove a char message because it's gone during removing it from "
+ "the queue, nextKeyMsg=%s, newNextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(nextKeyMsg).get(), ToString(newNextKeyMsg).get(),
+ ToString(kFoundCharMsg).get()));
+ MOZ_ASSERT(!mCharMessageHasGone);
+ mFollowingCharMsgs.Clear();
+ mCharMessageHasGone = true;
+ return false;
+ }
+ MOZ_LOG(
+ gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::GetFollowingCharMessage(), there is the message "
+ "which is being tried to be removed from the queue, trying again...",
+ this));
+ continue;
+ }
+
+ // Typically, this case occurs with WM_DEADCHAR. If the removed message's
+ // wParam becomes 0, that means that the key event shouldn't cause text
+ // input. So, let's ignore the strange char message.
+ if (removedMsg.message == nextKeyMsg.message && !removedMsg.wParam) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, succeeded to "
+ "remove a char message, but the removed message's wParam is 0, "
+ "removedMsg=%s",
+ this, ToString(removedMsg).get()));
+ return false;
+ }
+
+ // This is normal case.
+ if (MayBeSameCharMessage(removedMsg, nextKeyMsg)) {
+ aCharMsg = removedMsg;
+ MOZ_LOG(
+ gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::GetFollowingCharMessage(), succeeded to retrieve "
+ "next char message, aCharMsg=%s",
+ this, ToString(aCharMsg).get()));
+ return true;
+ }
+
+ // Even if removed message is different char message from the found char
+ // message, when the scan code is same, we can assume that the message
+ // is overwritten by somebody who hooks API. See bug 1336028 comment 0 for
+ // the possible scenarios.
+ if (IsCharMessage(removedMsg) &&
+ IsSamePhysicalKeyMessage(removedMsg, nextKeyMsg)) {
+ aCharMsg = removedMsg;
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, succeeded to "
+ "remove a char message, but the removed message was changed from "
+ "the found message except their scancode, aCharMsg=%s, "
+ "nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(aCharMsg).get(), ToString(nextKeyMsg).get(),
+ ToString(kFoundCharMsg).get()));
+ return true;
+ }
+
+ // When found message's wParam is 0 and its scancode is 0xFF, we may remove
+ // usual char message actually. In such case, we should use the removed
+ // char message.
+ if (IsCharMessage(removedMsg) && !nextKeyMsg.wParam &&
+ WinUtils::GetScanCode(nextKeyMsg.lParam) == 0xFF) {
+ aCharMsg = removedMsg;
+ MOZ_LOG(
+ gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::GetFollowingCharMessage(), WARNING, succeeded to "
+ "remove a char message, but the removed message was changed from "
+ "the found message but the found message was odd, so, ignoring the "
+ "odd found message and respecting the removed message, aCharMsg=%s, "
+ "nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(aCharMsg).get(), ToString(nextKeyMsg).get(),
+ ToString(kFoundCharMsg).get()));
+ return true;
+ }
+
+ // NOTE: Although, we don't know when this case occurs, the scan code value
+ // in lParam may be changed from 0 to something. The changed value
+ // is different from the scan code of handling keydown message.
+ MOZ_LOG(
+ gKeyLog, LogLevel::Error,
+ ("%p NativeKey::GetFollowingCharMessage(), FAILED, removed message "
+ "is really different from what we have already found, removedMsg=%s, "
+ "nextKeyMsg=%s, kFoundCharMsg=%s",
+ this, ToString(removedMsg).get(), ToString(nextKeyMsg).get(),
+ ToString(kFoundCharMsg).get()));
+ nsPrintfCString info(
+ "\nPeekMessage() removed unexpcted char message! "
+ "\nActive keyboard layout=0x%p (%s), "
+ "\nHandling message: %s, InSendMessageEx()=%s, "
+ "\nFound message: %s, "
+ "\nRemoved message: %s, ",
+ KeyboardLayout::GetInstance()->GetLoadedLayout(),
+ KeyboardLayout::GetInstance()->GetLoadedLayoutName().get(),
+ ToString(mMsg).get(), GetResultOfInSendMessageEx().get(),
+ ToString(kFoundCharMsg).get(), ToString(removedMsg).get());
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ // What's the next key message?
+ MSG nextKeyMsgAfter;
+ if (WinUtils::PeekMessage(&nextKeyMsgAfter, mMsg.hwnd, WM_KEYFIRST,
+ WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) {
+ nsPrintfCString info(
+ "\nNext key message after unexpected char message "
+ "removed: %s, ",
+ ToString(nextKeyMsgAfter).get());
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ } else {
+ CrashReporter::AppendAppNotesToCrashReport(
+ nsLiteralCString("\nThere is no key message after unexpected char "
+ "message removed, "));
+ }
+ // Another window has a key message?
+ if (WinUtils::PeekMessage(&nextKeyMsgInAllWindows, 0, WM_KEYFIRST,
+ WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) {
+ nsPrintfCString info("\nNext key message in all windows: %s.",
+ ToString(nextKeyMsgInAllWindows).get());
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ } else {
+ CrashReporter::AppendAppNotesToCrashReport(
+ "\nThere is no key message in any windows."_ns);
+ }
+
+ MOZ_CRASH("PeekMessage() removed unexpected message");
+ }
+ MOZ_LOG(
+ gKeyLog, LogLevel::Error,
+ ("%p NativeKey::GetFollowingCharMessage(), FAILED, removed messages "
+ "are all WM_NULL, nextKeyMsg=%s",
+ this, ToString(nextKeyMsg).get()));
+ nsPrintfCString info(
+ "\nWe lost following char message! "
+ "\nActive keyboard layout=0x%p (%s), "
+ "\nHandling message: %s, InSendMessageEx()=%s, \n"
+ "Found message: %s, removed a lot of WM_NULL",
+ KeyboardLayout::GetInstance()->GetLoadedLayout(),
+ KeyboardLayout::GetInstance()->GetLoadedLayoutName().get(),
+ ToString(mMsg).get(), GetResultOfInSendMessageEx().get(),
+ ToString(kFoundCharMsg).get());
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_CRASH("We lost the following char message");
+ return false;
+}
+
+void NativeKey::ComputeInputtingStringWithKeyboardLayout() {
+ KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance();
+
+ if (KeyboardLayout::IsPrintableCharKey(mVirtualKeyCode) ||
+ mCharMessageHasGone) {
+ mInputtingStringAndModifiers = mCommittedCharsAndModifiers;
+ } else {
+ mInputtingStringAndModifiers.Clear();
+ }
+ mShiftedString.Clear();
+ mUnshiftedString.Clear();
+ mShiftedLatinChar = mUnshiftedLatinChar = 0;
+
+ // XXX How about when Win key is pressed?
+ if (!mModKeyState.IsControl() && !mModKeyState.IsAlt()) {
+ return;
+ }
+
+ // If user is inputting a Unicode character with typing Alt + Numpad
+ // keys, we shouldn't set alternative key codes because the key event
+ // shouldn't match with a mnemonic. However, we should set only
+ // mUnshiftedString for keeping traditional behavior at WM_SYSKEYDOWN.
+ // FYI: I guess that it's okay that mUnshiftedString stays empty. So,
+ // if its value breaks something, you must be able to just return here.
+ if (MaybeTypingUnicodeScalarValue()) {
+ if (!mCommittedCharsAndModifiers.IsEmpty()) {
+ MOZ_ASSERT(mMsg.message == WM_SYSKEYDOWN);
+ char16_t num = mCommittedCharsAndModifiers.CharAt(0);
+ MOZ_ASSERT(num >= '0' && num <= '9');
+ mUnshiftedString.Append(num, MODIFIER_NONE);
+ return;
+ }
+ // If user presses a function key without NumLock or 3rd party utility
+ // synthesizes a function key on numpad, we should handle it as-is because
+ // the user's intention may be performing `Alt` + foo.
+ MOZ_ASSERT(!KeyboardLayout::IsPrintableCharKey(mVirtualKeyCode));
+ return;
+ }
+
+ ModifierKeyState capsLockState(mModKeyState.GetModifiers() &
+ MODIFIER_CAPSLOCK);
+
+ mUnshiftedString =
+ keyboardLayout->GetUniCharsAndModifiers(mVirtualKeyCode, capsLockState);
+ capsLockState.Set(MODIFIER_SHIFT);
+ mShiftedString =
+ keyboardLayout->GetUniCharsAndModifiers(mVirtualKeyCode, capsLockState);
+
+ // The current keyboard cannot input alphabets or numerics,
+ // we should append them for Shortcut/Access keys.
+ // E.g., for Cyrillic keyboard layout.
+ capsLockState.Unset(MODIFIER_SHIFT);
+ WidgetUtils::GetLatinCharCodeForKeyCode(
+ mDOMKeyCode, capsLockState.GetModifiers(), &mUnshiftedLatinChar,
+ &mShiftedLatinChar);
+
+ // If the mShiftedLatinChar isn't 0, the key code is NS_VK_[A-Z].
+ if (mShiftedLatinChar) {
+ // If the produced characters of the key on current keyboard layout
+ // are same as computed Latin characters, we shouldn't append the
+ // Latin characters to alternativeCharCode.
+ if (mUnshiftedLatinChar == mUnshiftedString.CharAt(0) &&
+ mShiftedLatinChar == mShiftedString.CharAt(0)) {
+ mShiftedLatinChar = mUnshiftedLatinChar = 0;
+ }
+ } else if (mUnshiftedLatinChar) {
+ // If the mShiftedLatinChar is 0, the mKeyCode doesn't produce
+ // alphabet character. At that time, the character may be produced
+ // with Shift key. E.g., on French keyboard layout, NS_VK_PERCENT
+ // key produces LATIN SMALL LETTER U WITH GRAVE (U+00F9) without
+ // Shift key but with Shift key, it produces '%'.
+ // If the mUnshiftedLatinChar is produced by the key on current
+ // keyboard layout, we shouldn't append it to alternativeCharCode.
+ if (mUnshiftedLatinChar == mUnshiftedString.CharAt(0) ||
+ mUnshiftedLatinChar == mShiftedString.CharAt(0)) {
+ mUnshiftedLatinChar = 0;
+ }
+ }
+
+ if (!mModKeyState.IsControl()) {
+ return;
+ }
+
+ // If the mCharCode is not ASCII character, we should replace the
+ // mCharCode with ASCII character only when Ctrl is pressed.
+ // But don't replace the mCharCode when the mCharCode is not same as
+ // unmodified characters. In such case, Ctrl is sometimes used for a
+ // part of character inputting key combination like Shift.
+ uint32_t ch =
+ mModKeyState.IsShift() ? mShiftedLatinChar : mUnshiftedLatinChar;
+ if (!ch) {
+ return;
+ }
+ if (mInputtingStringAndModifiers.IsEmpty() ||
+ mInputtingStringAndModifiers.UniCharsCaseInsensitiveEqual(
+ mModKeyState.IsShift() ? mShiftedString : mUnshiftedString)) {
+ mInputtingStringAndModifiers.Clear();
+ mInputtingStringAndModifiers.Append(ch, mModKeyState.GetModifiers());
+ }
+}
+
+bool NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages() const {
+ MOZ_ASSERT(IsKeyDownMessage());
+ MOZ_ASSERT(IsFollowedByPrintableCharOrSysCharMessage());
+ MOZ_ASSERT(!mWidget->Destroyed());
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Error,
+ ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), "
+ "FAILED due to BeginNativeInputTransaction() failure",
+ this));
+ return true;
+ }
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, mWidget);
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), "
+ "initializing keypress event...",
+ this));
+ ModifierKeyState modKeyState(mModKeyState);
+ if (mCanIgnoreModifierStateAtKeyPress && IsFollowedByPrintableCharMessage()) {
+ // If eKeyPress event should cause inputting text in focused editor,
+ // we need to remove Alt and Ctrl state.
+ modKeyState.Unset(MODIFIER_ALT | MODIFIER_CONTROL);
+ }
+ // We don't need to send char message here if there are two or more retrieved
+ // messages because we need to set each message to each eKeyPress event.
+ bool needsCallback = mFollowingCharMsgs.Length() > 1;
+ nsEventStatus status = InitKeyEvent(keypressEvent, modKeyState);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), "
+ "dispatching keypress event(s)...",
+ this));
+ bool dispatched = mDispatcher->MaybeDispatchKeypressEvents(
+ keypressEvent, status, const_cast<NativeKey*>(this), needsCallback);
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), "
+ "keypress event(s) caused destroying the widget",
+ this));
+ return true;
+ }
+ bool consumed = status == nsEventStatus_eConsumeNoDefault;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), "
+ "dispatched keypress event(s), dispatched=%s, consumed=%s",
+ this, GetBoolName(dispatched), GetBoolName(consumed)));
+ return consumed;
+}
+
+bool NativeKey::DispatchKeyPressEventsWithoutCharMessage() const {
+ MOZ_ASSERT(IsKeyDownMessage());
+ MOZ_ASSERT(!mIsDeadKey || !mCommittedCharsAndModifiers.IsEmpty());
+ MOZ_ASSERT(!mWidget->Destroyed());
+
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), "
+ "FAILED due "
+ "to BeginNativeInputTransaction() failure",
+ this));
+ return true;
+ }
+
+ WidgetKeyboardEvent keypressEvent(true, eKeyPress, mWidget);
+ if (mInputtingStringAndModifiers.IsEmpty() && mShiftedString.IsEmpty() &&
+ mUnshiftedString.IsEmpty()) {
+ keypressEvent.mKeyCode = mDOMKeyCode;
+ }
+ MOZ_LOG(gKeyLog, LogLevel::Debug,
+ ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), "
+ "initializing "
+ "keypress event...",
+ this));
+ nsEventStatus status = InitKeyEvent(keypressEvent, mModKeyState);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), "
+ "dispatching "
+ "keypress event(s)...",
+ this));
+ bool dispatched = mDispatcher->MaybeDispatchKeypressEvents(
+ keypressEvent, status, const_cast<NativeKey*>(this));
+ if (mWidget->Destroyed()) {
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), "
+ "keypress event(s) caused destroying the widget",
+ this));
+ return true;
+ }
+ bool consumed = status == nsEventStatus_eConsumeNoDefault;
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), dispatched "
+ "keypress event(s), dispatched=%s, consumed=%s",
+ this, GetBoolName(dispatched), GetBoolName(consumed)));
+ return consumed;
+}
+
+void NativeKey::WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndex) {
+ // If it's an eKeyPress event and it's generated from retrieved char message,
+ // we need to set raw message information for plugins.
+ if (aKeyboardEvent.mMessage == eKeyPress &&
+ IsFollowedByPrintableCharOrSysCharMessage()) {
+ MOZ_RELEASE_ASSERT(aIndex < mCommittedCharsAndModifiers.Length());
+ uint32_t foundPrintableCharMessages = 0;
+ for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) {
+ if (!IsPrintableCharOrSysCharMessage(mFollowingCharMsgs[i])) {
+ // XXX Should we dispatch a plugin event for WM_*DEADCHAR messages and
+ // WM_CHAR with a control character here? But we're not sure
+ // how can we create such message queue (i.e., WM_CHAR or
+ // WM_SYSCHAR with a printable character and such message are
+ // generated by a keydown). So, let's ignore such case until
+ // we'd get some bug reports.
+ MOZ_LOG(gKeyLog, LogLevel::Warning,
+ ("%p NativeKey::WillDispatchKeyboardEvent(), WARNING, "
+ "ignoring %zuth message due to non-printable char message, %s",
+ this, i + 1, ToString(mFollowingCharMsgs[i]).get()));
+ continue;
+ }
+ if (foundPrintableCharMessages++ == aIndex) {
+ // Found message which caused the eKeyPress event.
+ break;
+ }
+ }
+ // Set modifier state from mCommittedCharsAndModifiers because some of them
+ // might be different. For example, Shift key was pressed at inputting
+ // dead char but Shift key was released before inputting next character.
+ if (mCanIgnoreModifierStateAtKeyPress) {
+ ModifierKeyState modKeyState(mModKeyState);
+ modKeyState.Unset(MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT |
+ MODIFIER_ALTGRAPH | MODIFIER_CAPSLOCK);
+ modKeyState.Set(mCommittedCharsAndModifiers.ModifiersAt(aIndex));
+ modKeyState.InitInputEvent(aKeyboardEvent);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::WillDispatchKeyboardEvent(), "
+ "setting %uth modifier state to %s",
+ this, aIndex + 1, ToString(modKeyState).get()));
+ }
+ }
+ size_t longestLength =
+ std::max(mInputtingStringAndModifiers.Length(),
+ std::max(mShiftedString.Length(), mUnshiftedString.Length()));
+ size_t skipUniChars = longestLength - mInputtingStringAndModifiers.Length();
+ size_t skipShiftedChars = longestLength - mShiftedString.Length();
+ size_t skipUnshiftedChars = longestLength - mUnshiftedString.Length();
+ if (aIndex >= longestLength) {
+ MOZ_LOG(
+ gKeyLog, LogLevel::Info,
+ ("%p NativeKey::WillDispatchKeyboardEvent(), does nothing for %uth "
+ "%s event",
+ this, aIndex + 1, ToChar(aKeyboardEvent.mMessage)));
+ return;
+ }
+
+ // Check if aKeyboardEvent is the last event for a key press.
+ // So, if it's not an eKeyPress event, it's always the last event.
+ // Otherwise, check if the index is the last character of
+ // mCommittedCharsAndModifiers.
+ bool isLastIndex = aKeyboardEvent.mMessage != eKeyPress ||
+ mCommittedCharsAndModifiers.IsEmpty() ||
+ mCommittedCharsAndModifiers.Length() - 1 == aIndex;
+
+ nsTArray<AlternativeCharCode>& altArray =
+ aKeyboardEvent.mAlternativeCharCodes;
+
+ // Set charCode and adjust modifier state for every eKeyPress event.
+ // This is not necessary for the other keyboard events because the other
+ // keyboard events shouldn't have non-zero charCode value and should have
+ // current modifier state.
+ if (aKeyboardEvent.mMessage == eKeyPress && skipUniChars <= aIndex) {
+ // XXX Modifying modifier state of aKeyboardEvent is illegal, but no way
+ // to set different modifier state per keypress event except this
+ // hack. Note that ideally, dead key should cause composition events
+ // instead of keypress events, though.
+ if (aIndex - skipUniChars < mInputtingStringAndModifiers.Length()) {
+ ModifierKeyState modKeyState(mModKeyState);
+ // If key in combination with Alt and/or Ctrl produces a different
+ // character than without them then do not report these flags
+ // because it is separate keyboard layout shift state. If dead-key
+ // and base character does not produce a valid composite character
+ // then both produced dead-key character and following base
+ // character may have different modifier flags, too.
+ modKeyState.Unset(MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT |
+ MODIFIER_ALTGRAPH | MODIFIER_CAPSLOCK);
+ modKeyState.Set(
+ mInputtingStringAndModifiers.ModifiersAt(aIndex - skipUniChars));
+ modKeyState.InitInputEvent(aKeyboardEvent);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::WillDispatchKeyboardEvent(), "
+ "setting %uth modifier state to %s",
+ this, aIndex + 1, ToString(modKeyState).get()));
+ }
+ uint16_t uniChar =
+ mInputtingStringAndModifiers.CharAt(aIndex - skipUniChars);
+
+ // The mCharCode was set from mKeyValue. However, for example, when Ctrl key
+ // is pressed, its value should indicate an ASCII character for backward
+ // compatibility rather than inputting character without the modifiers.
+ // Therefore, we need to modify mCharCode value here.
+ aKeyboardEvent.SetCharCode(uniChar);
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("%p NativeKey::WillDispatchKeyboardEvent(), "
+ "setting %uth charCode to %s",
+ this, aIndex + 1, GetCharacterCodeName(uniChar).get()));
+ }
+
+ // We need to append alterntaive charCode values:
+ // - if the event is eKeyPress, we need to append for the index because
+ // eKeyPress event is dispatched for every character inputted by a
+ // key press.
+ // - if the event is not eKeyPress, we need to append for all characters
+ // inputted by the key press because the other keyboard events (e.g.,
+ // eKeyDown are eKeyUp) are fired only once for a key press.
+ size_t count;
+ if (aKeyboardEvent.mMessage == eKeyPress) {
+ // Basically, append alternative charCode values only for the index.
+ count = 1;
+ // However, if it's the last eKeyPress event but different shift state
+ // can input longer string, the last eKeyPress event should have all
+ // remaining alternative charCode values.
+ if (isLastIndex) {
+ count = longestLength - aIndex;
+ }
+ } else {
+ count = longestLength;
+ }
+ for (size_t i = 0; i < count; ++i) {
+ uint16_t shiftedChar = 0, unshiftedChar = 0;
+ if (skipShiftedChars <= aIndex + i) {
+ shiftedChar = mShiftedString.CharAt(aIndex + i - skipShiftedChars);
+ }
+ if (skipUnshiftedChars <= aIndex + i) {
+ unshiftedChar = mUnshiftedString.CharAt(aIndex + i - skipUnshiftedChars);
+ }
+
+ if (shiftedChar || unshiftedChar) {
+ AlternativeCharCode chars(unshiftedChar, shiftedChar);
+ altArray.AppendElement(chars);
+ }
+
+ if (!isLastIndex) {
+ continue;
+ }
+
+ if (mUnshiftedLatinChar || mShiftedLatinChar) {
+ AlternativeCharCode chars(mUnshiftedLatinChar, mShiftedLatinChar);
+ altArray.AppendElement(chars);
+ }
+
+ // Typically, following virtual keycodes are used for a key which can
+ // input the character. However, these keycodes are also used for
+ // other keys on some keyboard layout. E.g., in spite of Shift+'1'
+ // inputs '+' on Thai keyboard layout, a key which is at '=/+'
+ // key on ANSI keyboard layout is VK_OEM_PLUS. Native applications
+ // handle it as '+' key if Ctrl key is pressed.
+ char16_t charForOEMKeyCode = 0;
+ switch (mVirtualKeyCode) {
+ case VK_OEM_PLUS:
+ charForOEMKeyCode = '+';
+ break;
+ case VK_OEM_COMMA:
+ charForOEMKeyCode = ',';
+ break;
+ case VK_OEM_MINUS:
+ charForOEMKeyCode = '-';
+ break;
+ case VK_OEM_PERIOD:
+ charForOEMKeyCode = '.';
+ break;
+ }
+ if (charForOEMKeyCode && charForOEMKeyCode != mUnshiftedString.CharAt(0) &&
+ charForOEMKeyCode != mShiftedString.CharAt(0) &&
+ charForOEMKeyCode != mUnshiftedLatinChar &&
+ charForOEMKeyCode != mShiftedLatinChar) {
+ AlternativeCharCode OEMChars(charForOEMKeyCode, charForOEMKeyCode);
+ altArray.AppendElement(OEMChars);
+ }
+ }
+}
+
+/*****************************************************************************
+ * mozilla::widget::KeyboardLayout
+ *****************************************************************************/
+
+KeyboardLayout* KeyboardLayout::sInstance = nullptr;
+StaticRefPtr<nsIUserIdleServiceInternal> KeyboardLayout::sIdleService;
+
+// static
+KeyboardLayout* KeyboardLayout::GetInstance() {
+ if (!sInstance) {
+ sInstance = new KeyboardLayout();
+ }
+ return sInstance;
+}
+
+// static
+void KeyboardLayout::Shutdown() {
+ delete sInstance;
+ sInstance = nullptr;
+ sIdleService = nullptr;
+}
+
+// static
+void KeyboardLayout::NotifyIdleServiceOfUserActivity() {
+ if (!sIdleService) {
+ sIdleService = nsCOMPtr<nsIUserIdleServiceInternal>(
+ do_GetService("@mozilla.org/widget/useridleservice;1"))
+ .forget();
+ if (NS_WARN_IF(!sIdleService)) {
+ return;
+ }
+ }
+ sIdleService->ResetIdleTimeOut(0);
+}
+
+KeyboardLayout::KeyboardLayout() {
+ mDeadKeyTableListHead = nullptr;
+ // A dead key sequence should be made from up to 5 keys. Therefore, 4 is
+ // enough and makes sense because the item is uint8_t.
+ // (Although, even if it's possible to be 6 keys or more in a sequence,
+ // this array will be re-allocated).
+ mActiveDeadKeys.SetCapacity(4);
+ mDeadKeyShiftStates.SetCapacity(4);
+
+ // If we put it off to load active keyboard layout when first needed, we need
+ // to load it at instanciation. That makes us save the cost of a call of
+ // GetKeyboardLayout() API.
+ if (StaticPrefs::ui_key_layout_load_when_first_needed()) {
+ OnLayoutChange(::GetKeyboardLayout(0));
+ }
+}
+
+KeyboardLayout::~KeyboardLayout() { ReleaseDeadKeyTables(); }
+
+bool KeyboardLayout::IsPrintableCharKey(uint8_t aVirtualKey) {
+ return GetKeyIndex(aVirtualKey) >= 0;
+}
+
+WORD KeyboardLayout::ComputeScanCodeForVirtualKeyCode(
+ uint8_t aVirtualKeyCode) const {
+ return static_cast<WORD>(::MapVirtualKeyEx(aVirtualKeyCode, MAPVK_VK_TO_VSC,
+ KeyboardLayout::GetLayout()));
+}
+
+bool KeyboardLayout::IsDeadKey(uint8_t aVirtualKey,
+ const ModifierKeyState& aModKeyState) const {
+ int32_t virtualKeyIndex = GetKeyIndex(aVirtualKey);
+
+ // XXX KeyboardLayout class doesn't support unusual keyboard layout which
+ // maps some function keys as dead keys.
+ if (virtualKeyIndex < 0) {
+ return false;
+ }
+
+ return mVirtualKeys[virtualKeyIndex].IsDeadKey(
+ VirtualKey::ModifiersToShiftState(aModKeyState.GetModifiers()));
+}
+
+bool KeyboardLayout::IsSysKey(uint8_t aVirtualKey,
+ const ModifierKeyState& aModKeyState) const {
+ // If Alt key is not pressed, it's never a system key combination.
+ // Additionally, if Ctrl key is pressed, it's never a system key combination
+ // too.
+ // FYI: Windows logo key state won't affect if it's a system key.
+ if (!aModKeyState.IsAlt() || aModKeyState.IsControl()) {
+ return false;
+ }
+
+ int32_t virtualKeyIndex = GetKeyIndex(aVirtualKey);
+ if (virtualKeyIndex < 0) {
+ return true;
+ }
+
+ UniCharsAndModifiers inputCharsAndModifiers =
+ GetUniCharsAndModifiers(aVirtualKey, aModKeyState);
+ if (inputCharsAndModifiers.IsEmpty()) {
+ return true;
+ }
+
+ // If the Alt key state isn't consumed, that means that the key with Alt
+ // doesn't cause text input. So, the combination is a system key.
+ return !!(inputCharsAndModifiers.ModifiersAt(0) & MODIFIER_ALT);
+}
+
+void KeyboardLayout::InitNativeKey(NativeKey& aNativeKey) {
+ if (mIsPendingToRestoreKeyboardLayout) {
+ LoadLayout(::GetKeyboardLayout(0));
+ }
+
+ // If the aNativeKey is initialized with WM_CHAR, the key information
+ // should be discarded because mKeyValue should have the string to be
+ // inputted.
+ if (aNativeKey.mMsg.message == WM_CHAR) {
+ char16_t ch = static_cast<char16_t>(aNativeKey.mMsg.wParam);
+ // But don't set key value as printable key if the character is a control
+ // character such as 0x0D at pressing Enter key.
+ if (!NativeKey::IsControlChar(ch)) {
+ aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ Modifiers modifiers =
+ aNativeKey.GetModifiers() & ~(MODIFIER_ALT | MODIFIER_CONTROL);
+ aNativeKey.mCommittedCharsAndModifiers.Append(ch, modifiers);
+ return;
+ }
+ }
+
+ // If the aNativeKey is in a sequence to input a Unicode character with
+ // Alt + numpad keys, we should just set the number as the inputting charcter.
+ // Note that we should compute the key value from the virtual key code
+ // because they may be mapped to alphabets, but they should be treated as
+ // Alt + [0-9] even by web apps.
+ // However, we shouldn't touch the key value if given virtual key code is
+ // not a printable key because it may be synthesized by 3rd party utility
+ // or just NumLock is unlocked and user tries to use shortcut key. In the
+ // latter case, we cannot solve the conflict issue with Alt+foo shortcut key
+ // and inputting a Unicode scalar value like reported to bug 1606655, though,
+ // I have no better idea. Perhaps, `Alt` shouldn't be used for shortcut key
+ // combination on Windows.
+ if (aNativeKey.MaybeTypingUnicodeScalarValue() &&
+ KeyboardLayout::IsPrintableCharKey(aNativeKey.mVirtualKeyCode)) {
+ // If the key code value is mapped to a Numpad key, let's compute the key
+ // value with it. This is same behavior as Chrome. In strictly speaking,
+ // I think that the else block's computation is better because it seems
+ // that Windows does not refer virtual key code value, but we should avoid
+ // web-compat issues.
+ char16_t num = '0';
+ if (aNativeKey.mVirtualKeyCode >= VK_NUMPAD0 &&
+ aNativeKey.mVirtualKeyCode <= VK_NUMPAD9) {
+ num = '0' + aNativeKey.mVirtualKeyCode - VK_NUMPAD0;
+ }
+ // Otherwise, let's use fake key value for making never match with
+ // mnemonic.
+ else {
+ switch (aNativeKey.mScanCode) {
+ case 0x0052: // Numpad0
+ num = '0';
+ break;
+ case 0x004F: // Numpad1
+ num = '1';
+ break;
+ case 0x0050: // Numpad2
+ num = '2';
+ break;
+ case 0x0051: // Numpad3
+ num = '3';
+ break;
+ case 0x004B: // Numpad4
+ num = '4';
+ break;
+ case 0x004C: // Numpad5
+ num = '5';
+ break;
+ case 0x004D: // Numpad6
+ num = '6';
+ break;
+ case 0x0047: // Numpad7
+ num = '7';
+ break;
+ case 0x0048: // Numpad8
+ num = '8';
+ break;
+ case 0x0049: // Numpad9
+ num = '9';
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "IsTypingUnicodeScalarValue() must have returned true for wrong "
+ "scancode");
+ break;
+ }
+ }
+ aNativeKey.mCommittedCharsAndModifiers.Append(num,
+ aNativeKey.GetModifiers());
+ aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+ return;
+ }
+
+ // When it's followed by non-dead char message(s) for printable character(s),
+ // aNativeKey should dispatch eKeyPress events for them rather than
+ // information from keyboard layout because respecting WM_(SYS)CHAR messages
+ // guarantees that we can always input characters which is expected by
+ // the user even if the user uses odd keyboard layout.
+ // Or, when it was followed by non-dead char message for a printable character
+ // but it's gone at removing the message from the queue, let's treat it
+ // as a key inputting empty string.
+ if (aNativeKey.IsFollowedByPrintableCharOrSysCharMessage() ||
+ aNativeKey.mCharMessageHasGone) {
+ MOZ_ASSERT(!aNativeKey.IsCharMessage(aNativeKey.mMsg));
+ if (aNativeKey.IsFollowedByPrintableCharOrSysCharMessage()) {
+ // Initialize mCommittedCharsAndModifiers with following char messages.
+ aNativeKey.InitCommittedCharsAndModifiersWithFollowingCharMessages();
+ MOZ_ASSERT(!aNativeKey.mCommittedCharsAndModifiers.IsEmpty());
+
+ // Currently, we are doing a ugly hack to keypress events to cause
+ // inputting character even if Ctrl or Alt key is pressed, that is, we
+ // remove Ctrl and Alt modifier state from keypress event. However, for
+ // example, Ctrl+Space which causes ' ' of WM_CHAR message never causes
+ // keypress event whose ctrlKey is true. For preventing this problem,
+ // we should mark as not removable if Ctrl or Alt key does not cause
+ // changing inputting character.
+ if (IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode) &&
+ (aNativeKey.IsControl() ^ aNativeKey.IsAlt())) {
+ ModifierKeyState state = aNativeKey.ModifierKeyStateRef();
+ state.Unset(MODIFIER_ALT | MODIFIER_CONTROL);
+ UniCharsAndModifiers charsWithoutModifier =
+ GetUniCharsAndModifiers(aNativeKey.GenericVirtualKeyCode(), state);
+ aNativeKey.mCanIgnoreModifierStateAtKeyPress =
+ !charsWithoutModifier.UniCharsEqual(
+ aNativeKey.mCommittedCharsAndModifiers);
+ }
+ } else {
+ aNativeKey.mCommittedCharsAndModifiers.Clear();
+ }
+ aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
+
+ // If it's not in dead key sequence, we don't need to do anymore here.
+ if (!IsInDeadKeySequence()) {
+ return;
+ }
+
+ // If it's in dead key sequence and dead char is inputted as is, we need to
+ // set the previous modifier state which is stored when preceding dead key
+ // is pressed.
+ UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
+ aNativeKey.mCommittedCharsAndModifiers.OverwriteModifiersIfBeginsWith(
+ deadChars);
+ // Finish the dead key sequence.
+ DeactivateDeadKeyState();
+ return;
+ }
+
+ // If it's a dead key, aNativeKey will be initialized by
+ // MaybeInitNativeKeyAsDeadKey().
+ if (MaybeInitNativeKeyAsDeadKey(aNativeKey)) {
+ return;
+ }
+
+ // If the key is not a usual printable key, KeyboardLayout class assume that
+ // it's not cause dead char nor printable char. Therefore, there are nothing
+ // to do here fore such keys (e.g., function keys).
+ // However, this should keep dead key state even if non-printable key is
+ // pressed during a dead key sequence.
+ if (!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode)) {
+ return;
+ }
+
+ MOZ_ASSERT(aNativeKey.mOriginalVirtualKeyCode != VK_PACKET,
+ "At handling VK_PACKET, we shouldn't refer keyboard layout");
+ MOZ_ASSERT(
+ aNativeKey.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING,
+ "Printable key's key name index must be KEY_NAME_INDEX_USE_STRING");
+
+ // If it's in dead key handling and the pressed key causes a composite
+ // character, aNativeKey will be initialized by
+ // MaybeInitNativeKeyWithCompositeChar().
+ if (MaybeInitNativeKeyWithCompositeChar(aNativeKey)) {
+ return;
+ }
+
+ UniCharsAndModifiers baseChars = GetUniCharsAndModifiers(aNativeKey);
+
+ // If the key press isn't related to any dead keys, initialize aNativeKey
+ // with the characters which should be caused by the key.
+ if (!IsInDeadKeySequence()) {
+ aNativeKey.mCommittedCharsAndModifiers = baseChars;
+ return;
+ }
+
+ // If the key doesn't cause a composite character with preceding dead key,
+ // initialize aNativeKey with the dead-key character followed by current
+ // key's character.
+ UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
+ aNativeKey.mCommittedCharsAndModifiers = deadChars + baseChars;
+ if (aNativeKey.IsKeyDownMessage()) {
+ DeactivateDeadKeyState();
+ }
+}
+
+bool KeyboardLayout::MaybeInitNativeKeyAsDeadKey(NativeKey& aNativeKey) {
+ // Only when it's not in dead key sequence, we can trust IsDeadKey() result.
+ if (!IsInDeadKeySequence() && !IsDeadKey(aNativeKey)) {
+ return false;
+ }
+
+ // When keydown message is followed by a dead char message, it should be
+ // initialized as dead key.
+ bool isDeadKeyDownEvent =
+ aNativeKey.IsKeyDownMessage() && aNativeKey.IsFollowedByDeadCharMessage();
+
+ // When keyup message is received, let's check if it's one of preceding
+ // dead keys because keydown message order and keyup message order may be
+ // different.
+ bool isDeadKeyUpEvent =
+ !aNativeKey.IsKeyDownMessage() &&
+ mActiveDeadKeys.Contains(aNativeKey.GenericVirtualKeyCode());
+
+ if (isDeadKeyDownEvent || isDeadKeyUpEvent) {
+ ActivateDeadKeyState(aNativeKey);
+ // Any dead key events don't generate characters. So, a dead key should
+ // cause only keydown event and keyup event whose KeyboardEvent.key
+ // values are "Dead".
+ aNativeKey.mCommittedCharsAndModifiers.Clear();
+ aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_Dead;
+ return true;
+ }
+
+ // At keydown message handling, we need to forget the first dead key
+ // because there is no guarantee coming WM_KEYUP for the second dead
+ // key before next WM_KEYDOWN. E.g., due to auto key repeat or pressing
+ // another dead key before releasing current key. Therefore, we can
+ // set only a character for current key for keyup event.
+ if (!IsInDeadKeySequence()) {
+ aNativeKey.mCommittedCharsAndModifiers =
+ GetUniCharsAndModifiers(aNativeKey);
+ return true;
+ }
+
+ // When non-printable key event comes during a dead key sequence, that must
+ // be a modifier key event. So, such events shouldn't be handled as a part
+ // of the dead key sequence.
+ if (!IsDeadKey(aNativeKey)) {
+ return false;
+ }
+
+ // FYI: Following code may run when the user doesn't input text actually
+ // but the key sequence is a dead key sequence. For example,
+ // ` -> Ctrl+` with Spanish keyboard layout. Let's keep using this
+ // complicated code for now because this runs really rarely.
+
+ // Dead key followed by another dead key may cause a composed character
+ // (e.g., "Russian - Mnemonic" keyboard layout's 's' -> 'c').
+ if (MaybeInitNativeKeyWithCompositeChar(aNativeKey)) {
+ return true;
+ }
+
+ // Otherwise, dead key followed by another dead key causes inputting both
+ // character.
+ UniCharsAndModifiers prevDeadChars = GetDeadUniCharsAndModifiers();
+ UniCharsAndModifiers newChars = GetUniCharsAndModifiers(aNativeKey);
+ // But keypress events should be fired for each committed character.
+ aNativeKey.mCommittedCharsAndModifiers = prevDeadChars + newChars;
+ if (aNativeKey.IsKeyDownMessage()) {
+ DeactivateDeadKeyState();
+ }
+ return true;
+}
+
+bool KeyboardLayout::MaybeInitNativeKeyWithCompositeChar(
+ NativeKey& aNativeKey) {
+ if (!IsInDeadKeySequence()) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) {
+ return false;
+ }
+
+ UniCharsAndModifiers baseChars = GetUniCharsAndModifiers(aNativeKey);
+ if (baseChars.IsEmpty() || !baseChars.CharAt(0)) {
+ return false;
+ }
+
+ char16_t compositeChar = GetCompositeChar(baseChars.CharAt(0));
+ if (!compositeChar) {
+ return false;
+ }
+
+ // Active dead-key and base character does produce exactly one composite
+ // character.
+ aNativeKey.mCommittedCharsAndModifiers.Append(compositeChar,
+ baseChars.ModifiersAt(0));
+ if (aNativeKey.IsKeyDownMessage()) {
+ DeactivateDeadKeyState();
+ }
+ return true;
+}
+
+UniCharsAndModifiers KeyboardLayout::GetUniCharsAndModifiers(
+ uint8_t aVirtualKey, VirtualKey::ShiftState aShiftState) const {
+ UniCharsAndModifiers result;
+ int32_t key = GetKeyIndex(aVirtualKey);
+ if (key < 0) {
+ return result;
+ }
+ return mVirtualKeys[key].GetUniChars(aShiftState);
+}
+
+UniCharsAndModifiers KeyboardLayout::GetDeadUniCharsAndModifiers() const {
+ MOZ_RELEASE_ASSERT(mActiveDeadKeys.Length() == mDeadKeyShiftStates.Length());
+
+ if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
+ return UniCharsAndModifiers();
+ }
+
+ UniCharsAndModifiers result;
+ for (size_t i = 0; i < mActiveDeadKeys.Length(); ++i) {
+ result +=
+ GetUniCharsAndModifiers(mActiveDeadKeys[i], mDeadKeyShiftStates[i]);
+ }
+ return result;
+}
+
+char16_t KeyboardLayout::GetCompositeChar(char16_t aBaseChar) const {
+ if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
+ return 0;
+ }
+ // XXX Currently, we don't support computing a composite character with
+ // two or more dead keys since it needs big table for supporting
+ // long chained dead keys. However, this should be a minor bug
+ // because this runs only when the latest keydown event does not cause
+ // WM_(SYS)CHAR messages. So, when user wants to input a character,
+ // this path never runs.
+ if (mActiveDeadKeys.Length() > 1) {
+ return 0;
+ }
+ int32_t key = GetKeyIndex(mActiveDeadKeys[0]);
+ if (key < 0) {
+ return 0;
+ }
+ return mVirtualKeys[key].GetCompositeChar(mDeadKeyShiftStates[0], aBaseChar);
+}
+
+static bool IsValidKeyboardLayoutsChild(const nsAString& aChildName) {
+ if (aChildName.Length() != 8) {
+ return false;
+ }
+ for (size_t i = 0; i < aChildName.Length(); i++) {
+ if ((aChildName[i] >= '0' && aChildName[i] <= '9') ||
+ (aChildName[i] >= 'a' && aChildName[i] <= 'f') ||
+ (aChildName[i] >= 'A' && aChildName[i] <= 'F')) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+}
+
+// static
+nsCString KeyboardLayout::GetLayoutName(HKL aLayout) {
+ constexpr auto kKeyboardLayouts =
+ u"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\"_ns;
+ uint16_t language = reinterpret_cast<uintptr_t>(aLayout) & 0xFFFF;
+ uint16_t layout = (reinterpret_cast<uintptr_t>(aLayout) >> 16) & 0xFFFF;
+ // If the layout is less than 0xA000XXXX (normal keyboard layout for the
+ // language) or 0xEYYYXXXX (IMM-IME), we can retrieve its name simply.
+ if (layout < 0xA000 || (layout & 0xF000) == 0xE000) {
+ nsAutoString key(kKeyboardLayouts);
+ key.AppendPrintf("%08" PRIXPTR, layout < 0xA000
+ ? layout
+ : reinterpret_cast<uintptr_t>(aLayout));
+ wchar_t buf[256];
+ if (NS_WARN_IF(!WinRegistry::GetString(
+ HKEY_LOCAL_MACHINE, key, u"Layout Text"_ns, buf,
+ WinRegistry::kLegacyWinUtilsStringFlags))) {
+ return "No name or too long name"_ns;
+ }
+ return NS_ConvertUTF16toUTF8(buf);
+ }
+
+ if (NS_WARN_IF((layout & 0xF000) != 0xF000)) {
+ nsCString result;
+ result.AppendPrintf("Odd HKL: 0x%08" PRIXPTR,
+ reinterpret_cast<uintptr_t>(aLayout));
+ return result;
+ }
+
+ // Otherwise, we need to walk the registry under "Keyboard Layouts".
+ WinRegistry::Key regKey(HKEY_LOCAL_MACHINE, kKeyboardLayouts,
+ WinRegistry::KeyMode::Read);
+ if (NS_WARN_IF(!regKey)) {
+ return ""_ns;
+ }
+ uint32_t childCount = regKey.GetChildCount();
+ if (NS_WARN_IF(!childCount)) {
+ return ""_ns;
+ }
+ for (uint32_t i = 0; i < childCount; i++) {
+ nsAutoString childName;
+ if (NS_WARN_IF(!regKey.GetChildName(i, childName)) ||
+ !IsValidKeyboardLayoutsChild(childName)) {
+ continue;
+ }
+ nsresult rv = NS_OK;
+ uint32_t childNum = static_cast<uint32_t>(childName.ToInteger64(&rv, 16));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ // Ignore normal keyboard layouts for each language.
+ if (childNum <= 0xFFFF) {
+ continue;
+ }
+ // If it doesn't start with 'A' nor 'a', language should be matched.
+ if ((childNum & 0xFFFF) != language &&
+ (childNum & 0xF0000000) != 0xA0000000) {
+ continue;
+ }
+ // Then, the child should have "Layout Id" which is "YYY" of 0xFYYYXXXX.
+ nsAutoString key(kKeyboardLayouts);
+ key += childName;
+ WinRegistry::Key regKey(HKEY_LOCAL_MACHINE, key,
+ WinRegistry::KeyMode::QueryValue);
+ if (NS_WARN_IF(!regKey)) {
+ continue;
+ }
+ wchar_t buf[256];
+ if (NS_WARN_IF(!regKey.GetValueAsString(
+ u"Layout Id"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags))) {
+ continue;
+ }
+ uint16_t layoutId = wcstol(buf, nullptr, 16);
+ if (layoutId != (layout & 0x0FFF)) {
+ continue;
+ }
+ if (NS_WARN_IF(!regKey.GetValueAsString(
+ u"Layout Text"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags))) {
+ continue;
+ }
+ return NS_ConvertUTF16toUTF8(buf);
+ }
+ return ""_ns;
+}
+
+void KeyboardLayout::LoadLayout(HKL aLayout) {
+ mIsPendingToRestoreKeyboardLayout = false;
+
+ if (mKeyboardLayout == aLayout) {
+ return;
+ }
+
+ mKeyboardLayout = aLayout;
+ mHasAltGr = false;
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ ("KeyboardLayout::LoadLayout(aLayout=0x%p (%s))", aLayout,
+ GetLayoutName(aLayout).get()));
+
+ BYTE kbdState[256];
+ memset(kbdState, 0, sizeof(kbdState));
+
+ BYTE originalKbdState[256];
+ // Bitfield with all shift states that have at least one dead-key.
+ uint16_t shiftStatesWithDeadKeys = 0;
+ // Bitfield with all shift states that produce any possible dead-key base
+ // characters.
+ uint16_t shiftStatesWithBaseChars = 0;
+
+ mActiveDeadKeys.Clear();
+ mDeadKeyShiftStates.Clear();
+
+ ReleaseDeadKeyTables();
+
+ ::GetKeyboardState(originalKbdState);
+
+ // For each shift state gather all printable characters that are produced
+ // for normal case when no any dead-key is active.
+
+ for (VirtualKey::ShiftState shiftState = 0; shiftState < 16; shiftState++) {
+ VirtualKey::FillKbdState(kbdState, shiftState);
+ bool isAltGr = VirtualKey::IsAltGrIndex(shiftState);
+ for (uint32_t virtualKey = 0; virtualKey < 256; virtualKey++) {
+ int32_t vki = GetKeyIndex(virtualKey);
+ if (vki < 0) {
+ continue;
+ }
+ NS_ASSERTION(uint32_t(vki) < ArrayLength(mVirtualKeys), "invalid index");
+ char16_t uniChars[5];
+ int32_t ret = ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)uniChars,
+ ArrayLength(uniChars), 0, mKeyboardLayout);
+ // dead-key
+ if (ret < 0) {
+ shiftStatesWithDeadKeys |= (1 << shiftState);
+ // Repeat dead-key to deactivate it and get its character
+ // representation.
+ char16_t deadChar[2];
+ ret = ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)deadChar,
+ ArrayLength(deadChar), 0, mKeyboardLayout);
+ NS_ASSERTION(ret == 2, "Expecting twice repeated dead-key character");
+ mVirtualKeys[vki].SetDeadChar(shiftState, deadChar[0]);
+
+ MOZ_LOG(gKeyLog, LogLevel::Verbose,
+ (" %s (%d): DeadChar(%s, %s) (ret=%d)",
+ kVirtualKeyName[virtualKey], vki,
+ GetShiftStateName(shiftState).get(),
+ GetCharacterCodeNames(deadChar, 1).get(), ret));
+ } else {
+ if (ret == 1) {
+ // dead-key can pair only with exactly one base character.
+ shiftStatesWithBaseChars |= (1 << shiftState);
+ }
+ mVirtualKeys[vki].SetNormalChars(shiftState, uniChars, ret);
+ MOZ_LOG(gKeyLog, LogLevel::Verbose,
+ (" %s (%d): NormalChar(%s, %s) (ret=%d)",
+ kVirtualKeyName[virtualKey], vki,
+ GetShiftStateName(shiftState).get(),
+ GetCharacterCodeNames(uniChars, ret).get(), ret));
+ }
+
+ // If the key inputs at least one character with AltGr modifier,
+ // check if AltGr changes inputting character. If it does, mark
+ // this keyboard layout has AltGr modifier actually.
+ if (!mHasAltGr && ret > 0 && isAltGr &&
+ mVirtualKeys[vki].IsChangedByAltGr(shiftState)) {
+ mHasAltGr = true;
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" Found a key (%s) changed by AltGr: %s -> %s (%s) (ret=%d)",
+ kVirtualKeyName[virtualKey],
+ GetCharacterCodeNames(
+ mVirtualKeys[vki].GetNativeUniChars(
+ shiftState - VirtualKey::ShiftStateIndex::eAltGr))
+ .get(),
+ GetCharacterCodeNames(
+ mVirtualKeys[vki].GetNativeUniChars(shiftState))
+ .get(),
+ GetShiftStateName(shiftState).get(), ret));
+ }
+ }
+ }
+
+ // Now process each dead-key to find all its base characters and resulting
+ // composite characters.
+ for (VirtualKey::ShiftState shiftState = 0; shiftState < 16; shiftState++) {
+ if (!(shiftStatesWithDeadKeys & (1 << shiftState))) {
+ continue;
+ }
+
+ VirtualKey::FillKbdState(kbdState, shiftState);
+
+ for (uint32_t virtualKey = 0; virtualKey < 256; virtualKey++) {
+ int32_t vki = GetKeyIndex(virtualKey);
+ if (vki >= 0 && mVirtualKeys[vki].IsDeadKey(shiftState)) {
+ AutoTArray<DeadKeyEntry, 256> deadKeyArray;
+ uint32_t n = GetDeadKeyCombinations(
+ virtualKey, kbdState, shiftStatesWithBaseChars, deadKeyArray);
+ const DeadKeyTable* dkt =
+ mVirtualKeys[vki].MatchingDeadKeyTable(deadKeyArray.Elements(), n);
+ if (!dkt) {
+ dkt = AddDeadKeyTable(deadKeyArray.Elements(), n);
+ }
+ mVirtualKeys[vki].AttachDeadKeyTable(shiftState, dkt);
+ }
+ }
+ }
+
+ ::SetKeyboardState(originalKbdState);
+
+ if (MOZ_LOG_TEST(gKeyLog, LogLevel::Verbose)) {
+ static const UINT kExtendedScanCode[] = {0x0000, 0xE000};
+ static const UINT kMapType = MAPVK_VSC_TO_VK_EX;
+ MOZ_LOG(gKeyLog, LogLevel::Verbose,
+ ("Logging virtual keycode values for scancode (0x%p)...",
+ mKeyboardLayout));
+ for (uint32_t i = 0; i < ArrayLength(kExtendedScanCode); i++) {
+ for (uint32_t j = 1; j <= 0xFF; j++) {
+ UINT scanCode = kExtendedScanCode[i] + j;
+ UINT virtualKeyCode =
+ ::MapVirtualKeyEx(scanCode, kMapType, mKeyboardLayout);
+ MOZ_LOG(gKeyLog, LogLevel::Verbose,
+ ("0x%04X, %s", scanCode, kVirtualKeyName[virtualKeyCode]));
+ }
+ }
+ }
+
+ MOZ_LOG(gKeyLog, LogLevel::Info,
+ (" AltGr key is %s in %s", mHasAltGr ? "found" : "not found",
+ GetLayoutName(aLayout).get()));
+}
+
+inline int32_t KeyboardLayout::GetKeyIndex(uint8_t aVirtualKey) {
+ // Currently these 68 (NS_NUM_OF_KEYS) virtual keys are assumed
+ // to produce visible representation:
+ // 0x20 - VK_SPACE ' '
+ // 0x30..0x39 '0'..'9'
+ // 0x41..0x5A 'A'..'Z'
+ // 0x60..0x69 '0'..'9' on numpad
+ // 0x6A - VK_MULTIPLY '*' on numpad
+ // 0x6B - VK_ADD '+' on numpad
+ // 0x6D - VK_SUBTRACT '-' on numpad
+ // 0x6E - VK_DECIMAL '.' on numpad
+ // 0x6F - VK_DIVIDE '/' on numpad
+ // 0x6E - VK_DECIMAL '.'
+ // 0xBA - VK_OEM_1 ';:' for US
+ // 0xBB - VK_OEM_PLUS '+' any country
+ // 0xBC - VK_OEM_COMMA ',' any country
+ // 0xBD - VK_OEM_MINUS '-' any country
+ // 0xBE - VK_OEM_PERIOD '.' any country
+ // 0xBF - VK_OEM_2 '/?' for US
+ // 0xC0 - VK_OEM_3 '`~' for US
+ // 0xC1 - VK_ABNT_C1 '/?' for Brazilian
+ // 0xC2 - VK_ABNT_C2 separator key on numpad (Brazilian or JIS for Mac)
+ // 0xDB - VK_OEM_4 '[{' for US
+ // 0xDC - VK_OEM_5 '\|' for US
+ // 0xDD - VK_OEM_6 ']}' for US
+ // 0xDE - VK_OEM_7 ''"' for US
+ // 0xDF - VK_OEM_8
+ // 0xE1 - no name
+ // 0xE2 - VK_OEM_102 '\_' for JIS
+ // 0xE3 - no name
+ // 0xE4 - no name
+
+ static const int8_t xlat[256] = {
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ //-----------------------------------------------------------------------
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10
+ 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1, -1, -1, -1, -1, -1, // 30
+ -1, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // 40
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, -1, -1, -1, -1, -1, // 50
+ 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, -1, 49, 50, 51, // 60
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 70
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 90
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // A0
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 52, 53, 54, 55, 56, 57, // B0
+ 58, 59, 60, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // C0
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 61, 62, 63, 64, 65, // D0
+ -1, 66, 67, 68, 69, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // E0
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 // F0
+ };
+
+ return xlat[aVirtualKey];
+}
+
+const DeadKeyTable* KeyboardLayout::AddDeadKeyTable(
+ const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) {
+ DeadKeyTableListEntry* next = mDeadKeyTableListHead;
+
+ const size_t bytes = offsetof(DeadKeyTableListEntry, data) +
+ DeadKeyTable::SizeInBytes(aEntries);
+ uint8_t* p = new uint8_t[bytes];
+
+ mDeadKeyTableListHead = reinterpret_cast<DeadKeyTableListEntry*>(p);
+ mDeadKeyTableListHead->next = next;
+
+ DeadKeyTable* dkt =
+ reinterpret_cast<DeadKeyTable*>(mDeadKeyTableListHead->data);
+
+ dkt->Init(aDeadKeyArray, aEntries);
+
+ return dkt;
+}
+
+void KeyboardLayout::ReleaseDeadKeyTables() {
+ while (mDeadKeyTableListHead) {
+ uint8_t* p = reinterpret_cast<uint8_t*>(mDeadKeyTableListHead);
+ mDeadKeyTableListHead = mDeadKeyTableListHead->next;
+
+ delete[] p;
+ }
+}
+
+bool KeyboardLayout::EnsureDeadKeyActive(bool aIsActive, uint8_t aDeadKey,
+ const PBYTE aDeadKeyKbdState) {
+ int32_t ret;
+ do {
+ char16_t dummyChars[5];
+ ret =
+ ::ToUnicodeEx(aDeadKey, 0, (PBYTE)aDeadKeyKbdState, (LPWSTR)dummyChars,
+ ArrayLength(dummyChars), 0, mKeyboardLayout);
+ // returned values:
+ // <0 - Dead key state is active. The keyboard driver will wait for next
+ // character.
+ // 1 - Previous pressed key was a valid base character that produced
+ // exactly one composite character.
+ // >1 - Previous pressed key does not produce any composite characters.
+ // Return dead-key character followed by base character(s).
+ } while ((ret < 0) != aIsActive);
+
+ return (ret < 0);
+}
+
+void KeyboardLayout::ActivateDeadKeyState(const NativeKey& aNativeKey) {
+ // Dead-key state should be activated at keydown.
+ if (!aNativeKey.IsKeyDownMessage()) {
+ return;
+ }
+
+ mActiveDeadKeys.AppendElement(aNativeKey.mOriginalVirtualKeyCode);
+ mDeadKeyShiftStates.AppendElement(aNativeKey.GetShiftState());
+}
+
+void KeyboardLayout::DeactivateDeadKeyState() {
+ if (mActiveDeadKeys.IsEmpty()) {
+ return;
+ }
+
+ BYTE kbdState[256];
+ memset(kbdState, 0, sizeof(kbdState));
+
+ // Assume that the last dead key can finish dead key sequence.
+ VirtualKey::FillKbdState(kbdState, mDeadKeyShiftStates.LastElement());
+ EnsureDeadKeyActive(false, mActiveDeadKeys.LastElement(), kbdState);
+ mActiveDeadKeys.Clear();
+ mDeadKeyShiftStates.Clear();
+}
+
+bool KeyboardLayout::AddDeadKeyEntry(char16_t aBaseChar,
+ char16_t aCompositeChar,
+ nsTArray<DeadKeyEntry>& aDeadKeyArray) {
+ auto dke = DeadKeyEntry(aBaseChar, aCompositeChar);
+ for (uint32_t index = 0; index < aDeadKeyArray.Length(); index++) {
+ if (aDeadKeyArray[index] == dke) {
+ return false;
+ }
+ }
+
+ aDeadKeyArray.AppendElement(dke);
+
+ return true;
+}
+
+uint32_t KeyboardLayout::GetDeadKeyCombinations(
+ uint8_t aDeadKey, const PBYTE aDeadKeyKbdState,
+ uint16_t aShiftStatesWithBaseChars, nsTArray<DeadKeyEntry>& aDeadKeyArray) {
+ bool deadKeyActive = false;
+ uint32_t entries = 0;
+ BYTE kbdState[256];
+ memset(kbdState, 0, sizeof(kbdState));
+
+ for (uint32_t shiftState = 0; shiftState < 16; shiftState++) {
+ if (!(aShiftStatesWithBaseChars & (1 << shiftState))) {
+ continue;
+ }
+
+ VirtualKey::FillKbdState(kbdState, shiftState);
+
+ for (uint32_t virtualKey = 0; virtualKey < 256; virtualKey++) {
+ int32_t vki = GetKeyIndex(virtualKey);
+ // Dead-key can pair only with such key that produces exactly one base
+ // character.
+ if (vki >= 0 &&
+ mVirtualKeys[vki].GetNativeUniChars(shiftState).Length() == 1) {
+ // Ensure dead-key is in active state, when it swallows entered
+ // character and waits for the next pressed key.
+ if (!deadKeyActive) {
+ deadKeyActive = EnsureDeadKeyActive(true, aDeadKey, aDeadKeyKbdState);
+ }
+
+ // Depending on the character the followed the dead-key, the keyboard
+ // driver can produce one composite character, or a dead-key character
+ // followed by a second character.
+ char16_t compositeChars[5];
+ int32_t ret =
+ ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)compositeChars,
+ ArrayLength(compositeChars), 0, mKeyboardLayout);
+ switch (ret) {
+ case 0:
+ // This key combination does not produce any characters. The
+ // dead-key is still in active state.
+ break;
+ case 1: {
+ char16_t baseChars[5];
+ ret = ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)baseChars,
+ ArrayLength(baseChars), 0, mKeyboardLayout);
+ if (entries < aDeadKeyArray.Capacity()) {
+ switch (ret) {
+ case 1:
+ // Exactly one composite character produced. Now, when
+ // dead-key is not active, repeat the last character one more
+ // time to determine the base character.
+ if (AddDeadKeyEntry(baseChars[0], compositeChars[0],
+ aDeadKeyArray)) {
+ entries++;
+ }
+ deadKeyActive = false;
+ break;
+ case -1: {
+ // If pressing another dead-key produces different character,
+ // we should register the dead-key entry with first character
+ // produced by current key.
+
+ // First inactivate the dead-key state completely.
+ deadKeyActive =
+ EnsureDeadKeyActive(false, aDeadKey, aDeadKeyKbdState);
+ if (NS_WARN_IF(deadKeyActive)) {
+ MOZ_LOG(gKeyLog, LogLevel::Error,
+ (" failed to deactivating the dead-key state..."));
+ break;
+ }
+ for (int32_t i = 0; i < 5; ++i) {
+ ret = ::ToUnicodeEx(
+ virtualKey, 0, kbdState, (LPWSTR)baseChars,
+ ArrayLength(baseChars), 0, mKeyboardLayout);
+ if (ret >= 0) {
+ break;
+ }
+ }
+ if (ret > 0 &&
+ AddDeadKeyEntry(baseChars[0], compositeChars[0],
+ aDeadKeyArray)) {
+ entries++;
+ }
+ // Inactivate dead-key state for current virtual keycode.
+ EnsureDeadKeyActive(false, virtualKey, kbdState);
+ break;
+ }
+ default:
+ NS_WARNING("File a bug for this dead-key handling!");
+ deadKeyActive = false;
+ break;
+ }
+ }
+ MOZ_LOG(
+ gKeyLog, LogLevel::Verbose,
+ (" %s -> %s (%d): DeadKeyEntry(%s, %s) (ret=%d)",
+ kVirtualKeyName[aDeadKey], kVirtualKeyName[virtualKey], vki,
+ GetCharacterCodeNames(compositeChars, 1).get(),
+ ret <= 0
+ ? "''"
+ : GetCharacterCodeNames(baseChars, std::min(ret, 5)).get(),
+ ret));
+ break;
+ }
+ default:
+ // 1. Unexpected dead-key. Dead-key chaining is not supported.
+ // 2. More than one character generated. This is not a valid
+ // dead-key and base character combination.
+ deadKeyActive = false;
+ MOZ_LOG(
+ gKeyLog, LogLevel::Verbose,
+ (" %s -> %s (%d): Unsupport dead key type(%s) (ret=%d)",
+ kVirtualKeyName[aDeadKey], kVirtualKeyName[virtualKey], vki,
+ ret <= 0
+ ? "''"
+ : GetCharacterCodeNames(compositeChars, std::min(ret, 5))
+ .get(),
+ ret));
+ break;
+ }
+ }
+ }
+ }
+
+ if (deadKeyActive) {
+ deadKeyActive = EnsureDeadKeyActive(false, aDeadKey, aDeadKeyKbdState);
+ }
+
+ aDeadKeyArray.Sort();
+
+ return entries;
+}
+
+uint32_t KeyboardLayout::ConvertNativeKeyCodeToDOMKeyCode(
+ UINT aNativeKeyCode) const {
+ // Alphabet or Numeric or Numpad or Function keys
+ if ((aNativeKeyCode >= 0x30 && aNativeKeyCode <= 0x39) ||
+ (aNativeKeyCode >= 0x41 && aNativeKeyCode <= 0x5A) ||
+ (aNativeKeyCode >= 0x60 && aNativeKeyCode <= 0x87)) {
+ return static_cast<uint32_t>(aNativeKeyCode);
+ }
+ switch (aNativeKeyCode) {
+ // Following keycodes are same as our DOM keycodes
+ case VK_CANCEL:
+ case VK_BACK:
+ case VK_TAB:
+ case VK_CLEAR:
+ case VK_RETURN:
+ case VK_SHIFT:
+ case VK_CONTROL:
+ case VK_MENU: // Alt
+ case VK_PAUSE:
+ case VK_CAPITAL: // CAPS LOCK
+ case VK_KANA: // same as VK_HANGUL
+ case VK_JUNJA:
+ case VK_FINAL:
+ case VK_HANJA: // same as VK_KANJI
+ case VK_ESCAPE:
+ case VK_CONVERT:
+ case VK_NONCONVERT:
+ case VK_ACCEPT:
+ case VK_MODECHANGE:
+ case VK_SPACE:
+ case VK_PRIOR: // PAGE UP
+ case VK_NEXT: // PAGE DOWN
+ case VK_END:
+ case VK_HOME:
+ case VK_LEFT:
+ case VK_UP:
+ case VK_RIGHT:
+ case VK_DOWN:
+ case VK_SELECT:
+ case VK_PRINT:
+ case VK_EXECUTE:
+ case VK_SNAPSHOT:
+ case VK_INSERT:
+ case VK_DELETE:
+ case VK_APPS: // Context Menu
+ case VK_SLEEP:
+ case VK_NUMLOCK:
+ case VK_SCROLL: // SCROLL LOCK
+ case VK_ATTN: // Attension key of IBM midrange computers, e.g., AS/400
+ case VK_CRSEL: // Cursor Selection
+ case VK_EXSEL: // Extend Selection
+ case VK_EREOF: // Erase EOF key of IBM 3270 keyboard layout
+ case VK_PLAY:
+ case VK_ZOOM:
+ case VK_PA1: // PA1 key of IBM 3270 keyboard layout
+ return uint32_t(aNativeKeyCode);
+
+ case VK_HELP:
+ return NS_VK_HELP;
+
+ // Windows key should be mapped to a Win keycode
+ // They should be able to be distinguished by DOM3 KeyboardEvent.location
+ case VK_LWIN:
+ case VK_RWIN:
+ return NS_VK_WIN;
+
+ case VK_VOLUME_MUTE:
+ return NS_VK_VOLUME_MUTE;
+ case VK_VOLUME_DOWN:
+ return NS_VK_VOLUME_DOWN;
+ case VK_VOLUME_UP:
+ return NS_VK_VOLUME_UP;
+
+ case VK_LSHIFT:
+ case VK_RSHIFT:
+ return NS_VK_SHIFT;
+
+ case VK_LCONTROL:
+ case VK_RCONTROL:
+ return NS_VK_CONTROL;
+
+ // Note that even if the key is AltGr, we should return NS_VK_ALT for
+ // compatibility with both older Gecko and the other browsers.
+ case VK_LMENU:
+ case VK_RMENU:
+ return NS_VK_ALT;
+
+ // Following keycodes are not defined in our DOM keycodes.
+ case VK_BROWSER_BACK:
+ case VK_BROWSER_FORWARD:
+ case VK_BROWSER_REFRESH:
+ case VK_BROWSER_STOP:
+ case VK_BROWSER_SEARCH:
+ case VK_BROWSER_FAVORITES:
+ case VK_BROWSER_HOME:
+ case VK_MEDIA_NEXT_TRACK:
+ case VK_MEDIA_PREV_TRACK:
+ case VK_MEDIA_STOP:
+ case VK_MEDIA_PLAY_PAUSE:
+ case VK_LAUNCH_MAIL:
+ case VK_LAUNCH_MEDIA_SELECT:
+ case VK_LAUNCH_APP1:
+ case VK_LAUNCH_APP2:
+ return 0;
+
+ // Following OEM specific virtual keycodes should pass through DOM keyCode
+ // for compatibility with the other browsers on Windows.
+
+ // Following OEM specific virtual keycodes are defined for Fujitsu/OASYS.
+ case VK_OEM_FJ_JISHO:
+ case VK_OEM_FJ_MASSHOU:
+ case VK_OEM_FJ_TOUROKU:
+ case VK_OEM_FJ_LOYA:
+ case VK_OEM_FJ_ROYA:
+ // Not sure what means "ICO".
+ case VK_ICO_HELP:
+ case VK_ICO_00:
+ case VK_ICO_CLEAR:
+ // Following OEM specific virtual keycodes are defined for Nokia/Ericsson.
+ case VK_OEM_RESET:
+ case VK_OEM_JUMP:
+ case VK_OEM_PA1:
+ case VK_OEM_PA2:
+ case VK_OEM_PA3:
+ case VK_OEM_WSCTRL:
+ case VK_OEM_CUSEL:
+ case VK_OEM_ATTN:
+ case VK_OEM_FINISH:
+ case VK_OEM_COPY:
+ case VK_OEM_AUTO:
+ case VK_OEM_ENLW:
+ case VK_OEM_BACKTAB:
+ // VK_OEM_CLEAR is defined as not OEM specific, but let's pass though
+ // DOM keyCode like other OEM specific virtual keycodes.
+ case VK_OEM_CLEAR:
+ return uint32_t(aNativeKeyCode);
+
+ // 0xE1 is an OEM specific virtual keycode. However, the value is already
+ // used in our DOM keyCode for AltGr on Linux. So, this virtual keycode
+ // cannot pass through DOM keyCode.
+ case 0xE1:
+ return 0;
+
+ // Following keycodes are OEM keys which are keycodes for non-alphabet and
+ // non-numeric keys, we should compute each keycode of them from unshifted
+ // character which is inputted by each key. But if the unshifted character
+ // is not an ASCII character but shifted character is an ASCII character,
+ // we should refer it.
+ case VK_OEM_1:
+ case VK_OEM_PLUS:
+ case VK_OEM_COMMA:
+ case VK_OEM_MINUS:
+ case VK_OEM_PERIOD:
+ case VK_OEM_2:
+ case VK_OEM_3:
+ case VK_OEM_4:
+ case VK_OEM_5:
+ case VK_OEM_6:
+ case VK_OEM_7:
+ case VK_OEM_8:
+ case VK_OEM_102:
+ case VK_ABNT_C1: {
+ NS_ASSERTION(IsPrintableCharKey(aNativeKeyCode),
+ "The key must be printable");
+ ModifierKeyState modKeyState(0);
+ UniCharsAndModifiers uniChars =
+ GetUniCharsAndModifiers(aNativeKeyCode, modKeyState);
+ if (uniChars.Length() != 1 || uniChars.CharAt(0) < ' ' ||
+ uniChars.CharAt(0) > 0x7F) {
+ modKeyState.Set(MODIFIER_SHIFT);
+ uniChars = GetUniCharsAndModifiers(aNativeKeyCode, modKeyState);
+ if (uniChars.Length() != 1 || uniChars.CharAt(0) < ' ' ||
+ uniChars.CharAt(0) > 0x7F) {
+ // In this case, we've returned 0 in this case for long time because
+ // we decided that we should avoid setting same keyCode value to 2 or
+ // more keys since active keyboard layout may have a key to input the
+ // punctuation with different key. However, setting keyCode to 0
+ // makes some web applications which are aware of neither
+ // KeyboardEvent.key nor KeyboardEvent.code not work with Firefox
+ // when user selects non-ASCII capable keyboard layout such as
+ // Russian and Thai layout. So, let's decide keyCode value with
+ // major keyboard layout's key which causes the OEM keycode.
+ // Actually, this maps same keyCode value to 2 keys on Russian
+ // keyboard layout. "Period" key causes VK_OEM_PERIOD but inputs
+ // Yu of Cyrillic and "Slash" key causes VK_OEM_2 (same as US
+ // keyboard layout) but inputs "." (period of ASCII). Therefore,
+ // we return DOM_VK_PERIOD which is same as VK_OEM_PERIOD for
+ // "Period" key. On the other hand, we use same keyCode value for
+ // "Slash" key too because it inputs ".".
+ CodeNameIndex code;
+ switch (aNativeKeyCode) {
+ case VK_OEM_1:
+ code = CODE_NAME_INDEX_Semicolon;
+ break;
+ case VK_OEM_PLUS:
+ code = CODE_NAME_INDEX_Equal;
+ break;
+ case VK_OEM_COMMA:
+ code = CODE_NAME_INDEX_Comma;
+ break;
+ case VK_OEM_MINUS:
+ code = CODE_NAME_INDEX_Minus;
+ break;
+ case VK_OEM_PERIOD:
+ code = CODE_NAME_INDEX_Period;
+ break;
+ case VK_OEM_2:
+ code = CODE_NAME_INDEX_Slash;
+ break;
+ case VK_OEM_3:
+ code = CODE_NAME_INDEX_Backquote;
+ break;
+ case VK_OEM_4:
+ code = CODE_NAME_INDEX_BracketLeft;
+ break;
+ case VK_OEM_5:
+ code = CODE_NAME_INDEX_Backslash;
+ break;
+ case VK_OEM_6:
+ code = CODE_NAME_INDEX_BracketRight;
+ break;
+ case VK_OEM_7:
+ code = CODE_NAME_INDEX_Quote;
+ break;
+ case VK_OEM_8:
+ // Use keyCode value for "Backquote" key on UK keyboard layout.
+ code = CODE_NAME_INDEX_Backquote;
+ break;
+ case VK_OEM_102:
+ // Use keyCode value for "IntlBackslash" key.
+ code = CODE_NAME_INDEX_IntlBackslash;
+ break;
+ case VK_ABNT_C1: // "/" of ABNT.
+ // Use keyCode value for "IntlBackslash" key on ABNT keyboard
+ // layout.
+ code = CODE_NAME_INDEX_IntlBackslash;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Handle all OEM keycode values");
+ return 0;
+ }
+ return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code);
+ }
+ }
+ return WidgetUtils::ComputeKeyCodeFromChar(uniChars.CharAt(0));
+ }
+
+ // IE sets 0xC2 to the DOM keyCode for VK_ABNT_C2. However, we're already
+ // using NS_VK_SEPARATOR for the separator key on Mac and Linux. Therefore,
+ // We should keep consistency between Gecko on all platforms rather than
+ // with other browsers since a lot of keyCode values are already different
+ // between browsers.
+ case VK_ABNT_C2:
+ return NS_VK_SEPARATOR;
+
+ // VK_PROCESSKEY means IME already consumed the key event.
+ case VK_PROCESSKEY:
+ return NS_VK_PROCESSKEY;
+ // VK_PACKET is generated by SendInput() API, we don't need to
+ // care this message as key event.
+ case VK_PACKET:
+ return 0;
+ // If a key is not mapped to a virtual keycode, 0xFF is used.
+ case 0xFF:
+ NS_WARNING("The key is failed to be converted to a virtual keycode");
+ return 0;
+ }
+#ifdef DEBUG
+ nsPrintfCString warning(
+ "Unknown virtual keycode (0x%08X), please check the "
+ "latest MSDN document, there may be some new "
+ "keycodes we've never known.",
+ aNativeKeyCode);
+ NS_WARNING(warning.get());
+#endif
+ return 0;
+}
+
+KeyNameIndex KeyboardLayout::ConvertNativeKeyCodeToKeyNameIndex(
+ uint8_t aVirtualKey) const {
+ if (IsPrintableCharKey(aVirtualKey) || aVirtualKey == VK_PACKET) {
+ return KEY_NAME_INDEX_USE_STRING;
+ }
+
+ // If the keyboard layout has AltGr and AltRight key is pressed,
+ // return AltGraph.
+ if (aVirtualKey == VK_RMENU && HasAltGr()) {
+ return KEY_NAME_INDEX_AltGraph;
+ }
+
+ switch (aVirtualKey) {
+#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ break;
+ }
+
+ HKL layout = KeyboardLayout::GetLayout();
+ WORD langID = LOWORD(static_cast<HKL>(layout));
+ WORD primaryLangID = PRIMARYLANGID(langID);
+
+ if (primaryLangID == LANG_JAPANESE) {
+ switch (aVirtualKey) {
+#undef NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+#define NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \
+ aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ break;
+ }
+ } else if (primaryLangID == LANG_KOREAN) {
+ switch (aVirtualKey) {
+#undef NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+#define NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ return KEY_NAME_INDEX_Unidentified;
+ }
+ }
+
+ switch (aVirtualKey) {
+#undef NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+#define NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
+ case aNativeKey: \
+ return aKeyNameIndex;
+
+#include "NativeKeyToDOMKeyName.h"
+
+#undef NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
+
+ default:
+ return KEY_NAME_INDEX_Unidentified;
+ }
+}
+
+// static
+CodeNameIndex KeyboardLayout::ConvertScanCodeToCodeNameIndex(UINT aScanCode) {
+ switch (aScanCode) {
+#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \
+ case aNativeKey: \
+ return aCodeNameIndex;
+
+#include "NativeKeyToDOMCodeName.h"
+
+#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX
+
+ default:
+ return CODE_NAME_INDEX_UNKNOWN;
+ }
+}
+
+nsresult KeyboardLayout::SynthesizeNativeKeyEvent(
+ nsWindow* aWidget, int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters) {
+ UINT keyboardLayoutListCount = ::GetKeyboardLayoutList(0, nullptr);
+ NS_ASSERTION(keyboardLayoutListCount > 0,
+ "One keyboard layout must be installed at least");
+ HKL keyboardLayoutListBuff[50];
+ HKL* keyboardLayoutList = keyboardLayoutListCount < 50
+ ? keyboardLayoutListBuff
+ : new HKL[keyboardLayoutListCount];
+ keyboardLayoutListCount =
+ ::GetKeyboardLayoutList(keyboardLayoutListCount, keyboardLayoutList);
+ NS_ASSERTION(keyboardLayoutListCount > 0,
+ "Failed to get all keyboard layouts installed on the system");
+
+ nsPrintfCString layoutName("%08x", aNativeKeyboardLayout);
+ HKL loadedLayout = LoadKeyboardLayoutA(layoutName.get(), KLF_NOTELLSHELL);
+ if (loadedLayout == nullptr) {
+ if (keyboardLayoutListBuff != keyboardLayoutList) {
+ delete[] keyboardLayoutList;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Setup clean key state and load desired layout
+ BYTE originalKbdState[256];
+ ::GetKeyboardState(originalKbdState);
+ BYTE kbdState[256];
+ memset(kbdState, 0, sizeof(kbdState));
+ // This changes the state of the keyboard for the current thread only,
+ // and we'll restore it soon, so this should be OK.
+ ::SetKeyboardState(kbdState);
+
+ OverrideLayout(loadedLayout);
+
+ bool isAltGrKeyPress = false;
+ if (aModifierFlags & nsIWidget::ALTGRAPH) {
+ if (!HasAltGr()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // AltGr emulates ControlLeft key press and AltRight key press.
+ // So, we should remove those flags from aModifierFlags before
+ // calling WinUtils::SetupKeyModifiersSequence() to create correct
+ // key sequence.
+ // FYI: We don't support both ControlLeft and AltRight (AltGr) are
+ // pressed at the same time unless synthesizing key is
+ // VK_LCONTROL.
+ aModifierFlags &= ~(nsIWidget::CTRL_L | nsIWidget::ALT_R);
+ }
+
+ uint8_t argumentKeySpecific = 0;
+ switch (aNativeKeyCode & 0xFF) {
+ case VK_SHIFT:
+ aModifierFlags &= ~(nsIWidget::SHIFT_L | nsIWidget::SHIFT_R);
+ argumentKeySpecific = VK_LSHIFT;
+ break;
+ case VK_LSHIFT:
+ aModifierFlags &= ~nsIWidget::SHIFT_L;
+ argumentKeySpecific = aNativeKeyCode & 0xFF;
+ aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_SHIFT;
+ break;
+ case VK_RSHIFT:
+ aModifierFlags &= ~nsIWidget::SHIFT_R;
+ argumentKeySpecific = aNativeKeyCode & 0xFF;
+ aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_SHIFT;
+ break;
+ case VK_CONTROL:
+ aModifierFlags &= ~(nsIWidget::CTRL_L | nsIWidget::CTRL_R);
+ argumentKeySpecific = VK_LCONTROL;
+ break;
+ case VK_LCONTROL:
+ aModifierFlags &= ~nsIWidget::CTRL_L;
+ argumentKeySpecific = aNativeKeyCode & 0xFF;
+ aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_CONTROL;
+ break;
+ case VK_RCONTROL:
+ aModifierFlags &= ~nsIWidget::CTRL_R;
+ argumentKeySpecific = aNativeKeyCode & 0xFF;
+ aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_CONTROL;
+ break;
+ case VK_MENU:
+ aModifierFlags &= ~(nsIWidget::ALT_L | nsIWidget::ALT_R);
+ argumentKeySpecific = VK_LMENU;
+ break;
+ case VK_LMENU:
+ aModifierFlags &= ~nsIWidget::ALT_L;
+ argumentKeySpecific = aNativeKeyCode & 0xFF;
+ aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_MENU;
+ break;
+ case VK_RMENU:
+ aModifierFlags &= ~(nsIWidget::ALT_R | nsIWidget::ALTGRAPH);
+ argumentKeySpecific = aNativeKeyCode & 0xFF;
+ aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_MENU;
+ // If AltRight key is AltGr in the keyboard layout, let's use
+ // SetupKeyModifiersSequence() to emulate the native behavior
+ // since the same event order between keydown and keyup makes
+ // the following code complicated.
+ if (HasAltGr()) {
+ isAltGrKeyPress = true;
+ aModifierFlags &= ~nsIWidget::CTRL_L;
+ aModifierFlags |= nsIWidget::ALTGRAPH;
+ }
+ break;
+ case VK_CAPITAL:
+ aModifierFlags &= ~nsIWidget::CAPS_LOCK;
+ argumentKeySpecific = VK_CAPITAL;
+ break;
+ case VK_NUMLOCK:
+ aModifierFlags &= ~nsIWidget::NUM_LOCK;
+ argumentKeySpecific = VK_NUMLOCK;
+ break;
+ }
+
+ AutoTArray<KeyPair, 10> keySequence;
+ WinUtils::SetupKeyModifiersSequence(&keySequence, aModifierFlags, WM_KEYDOWN);
+ if (!isAltGrKeyPress) {
+ keySequence.AppendElement(KeyPair(aNativeKeyCode, argumentKeySpecific));
+ }
+
+ // Simulate the pressing of each modifier key and then the real key
+ // FYI: Each NativeKey instance here doesn't need to override keyboard layout
+ // since this method overrides and restores the keyboard layout.
+ for (uint32_t i = 0; i < keySequence.Length(); ++i) {
+ uint8_t key = keySequence[i].mGeneral;
+ uint8_t keySpecific = keySequence[i].mSpecific;
+ uint16_t scanCode = keySequence[i].mScanCode;
+ kbdState[key] = 0x81; // key is down and toggled on if appropriate
+ if (keySpecific) {
+ kbdState[keySpecific] = 0x81;
+ }
+ ::SetKeyboardState(kbdState);
+ ModifierKeyState modKeyState;
+ // If scan code isn't specified explicitly, let's compute it with current
+ // keyboard layout.
+ if (!scanCode) {
+ scanCode =
+ ComputeScanCodeForVirtualKeyCode(keySpecific ? keySpecific : key);
+ }
+ LPARAM lParam = static_cast<LPARAM>(scanCode << 16);
+ // If the scan code is for an extended key, set extended key flag.
+ if ((scanCode & 0xFF00) == 0xE000) {
+ lParam |= 0x1000000;
+ }
+ // When AltGr key is pressed, both ControlLeft and AltRight cause
+ // WM_KEYDOWN messages.
+ bool makeSysKeyMsg =
+ !(aModifierFlags & nsIWidget::ALTGRAPH) && IsSysKey(key, modKeyState);
+ MSG keyDownMsg =
+ WinUtils::InitMSG(makeSysKeyMsg ? WM_SYSKEYDOWN : WM_KEYDOWN, key,
+ lParam, aWidget->GetWindowHandle());
+ if (i == keySequence.Length() - 1) {
+ bool makeDeadCharMsg =
+ (IsDeadKey(key, modKeyState) && aCharacters.IsEmpty());
+ nsAutoString chars(aCharacters);
+ if (makeDeadCharMsg) {
+ UniCharsAndModifiers deadChars =
+ GetUniCharsAndModifiers(key, modKeyState);
+ chars = deadChars.ToString();
+ NS_ASSERTION(chars.Length() == 1,
+ "Dead char must be only one character");
+ }
+ if (chars.IsEmpty()) {
+ NativeKey nativeKey(aWidget, keyDownMsg, modKeyState);
+ nativeKey.HandleKeyDownMessage();
+ } else {
+ AutoTArray<NativeKey::FakeCharMsg, 10> fakeCharMsgs;
+ for (uint32_t j = 0; j < chars.Length(); j++) {
+ NativeKey::FakeCharMsg* fakeCharMsg = fakeCharMsgs.AppendElement();
+ fakeCharMsg->mCharCode = chars.CharAt(j);
+ fakeCharMsg->mScanCode = scanCode;
+ fakeCharMsg->mIsSysKey = makeSysKeyMsg;
+ fakeCharMsg->mIsDeadKey = makeDeadCharMsg;
+ }
+ NativeKey nativeKey(aWidget, keyDownMsg, modKeyState, 0, &fakeCharMsgs);
+ bool dispatched;
+ nativeKey.HandleKeyDownMessage(&dispatched);
+ // If some char messages are not consumed, let's emulate the widget
+ // receiving the message directly.
+ for (uint32_t j = 1; j < fakeCharMsgs.Length(); j++) {
+ if (fakeCharMsgs[j].mConsumed) {
+ continue;
+ }
+ MSG charMsg = fakeCharMsgs[j].GetCharMsg(aWidget->GetWindowHandle());
+ NativeKey nativeKey(aWidget, charMsg, modKeyState);
+ nativeKey.HandleCharMessage(charMsg);
+ }
+ }
+ } else {
+ NativeKey nativeKey(aWidget, keyDownMsg, modKeyState);
+ nativeKey.HandleKeyDownMessage();
+ }
+ }
+
+ keySequence.Clear();
+ if (!isAltGrKeyPress) {
+ keySequence.AppendElement(KeyPair(aNativeKeyCode, argumentKeySpecific));
+ }
+ WinUtils::SetupKeyModifiersSequence(&keySequence, aModifierFlags, WM_KEYUP);
+ for (uint32_t i = 0; i < keySequence.Length(); ++i) {
+ uint8_t key = keySequence[i].mGeneral;
+ uint8_t keySpecific = keySequence[i].mSpecific;
+ uint16_t scanCode = keySequence[i].mScanCode;
+ kbdState[key] = 0; // key is up and toggled off if appropriate
+ if (keySpecific) {
+ kbdState[keySpecific] = 0;
+ }
+ ::SetKeyboardState(kbdState);
+ ModifierKeyState modKeyState;
+ // If scan code isn't specified explicitly, let's compute it with current
+ // keyboard layout.
+ if (!scanCode) {
+ scanCode =
+ ComputeScanCodeForVirtualKeyCode(keySpecific ? keySpecific : key);
+ }
+ LPARAM lParam = static_cast<LPARAM>(scanCode << 16);
+ // If the scan code is for an extended key, set extended key flag.
+ if ((scanCode & 0xFF00) == 0xE000) {
+ lParam |= 0x1000000;
+ }
+ // Don't use WM_SYSKEYUP for Alt keyup.
+ // NOTE: When AltGr was pressed, ControlLeft causes WM_SYSKEYUP normally.
+ bool makeSysKeyMsg = IsSysKey(key, modKeyState) && key != VK_MENU;
+ MSG keyUpMsg = WinUtils::InitMSG(makeSysKeyMsg ? WM_SYSKEYUP : WM_KEYUP,
+ key, lParam, aWidget->GetWindowHandle());
+ NativeKey nativeKey(aWidget, keyUpMsg, modKeyState);
+ nativeKey.HandleKeyUpMessage();
+ }
+
+ // Restore old key state and layout
+ ::SetKeyboardState(originalKbdState);
+ RestoreLayout();
+
+ // Don't unload the layout if it's installed actually.
+ for (uint32_t i = 0; i < keyboardLayoutListCount; i++) {
+ if (keyboardLayoutList[i] == loadedLayout) {
+ loadedLayout = 0;
+ break;
+ }
+ }
+ if (keyboardLayoutListBuff != keyboardLayoutList) {
+ delete[] keyboardLayoutList;
+ }
+ if (loadedLayout) {
+ ::UnloadKeyboardLayout(loadedLayout);
+ }
+ return NS_OK;
+}
+
+/*****************************************************************************
+ * mozilla::widget::DeadKeyTable
+ *****************************************************************************/
+
+char16_t DeadKeyTable::GetCompositeChar(char16_t aBaseChar) const {
+ // Dead-key table is sorted by BaseChar in ascending order.
+ // Usually they are too small to use binary search.
+
+ for (uint32_t index = 0; index < mEntries; index++) {
+ if (mTable[index].BaseChar == aBaseChar) {
+ return mTable[index].CompositeChar;
+ }
+ if (mTable[index].BaseChar > aBaseChar) {
+ break;
+ }
+ }
+
+ return 0;
+}
+
+/*****************************************************************************
+ * mozilla::widget::RedirectedKeyDownMessage
+ *****************************************************************************/
+
+MSG RedirectedKeyDownMessageManager::sRedirectedKeyDownMsg;
+bool RedirectedKeyDownMessageManager::sDefaultPreventedOfRedirectedMsg = false;
+
+// static
+bool RedirectedKeyDownMessageManager::IsRedirectedMessage(const MSG& aMsg) {
+ return (aMsg.message == WM_KEYDOWN || aMsg.message == WM_SYSKEYDOWN) &&
+ (sRedirectedKeyDownMsg.message == aMsg.message &&
+ WinUtils::GetScanCode(sRedirectedKeyDownMsg.lParam) ==
+ WinUtils::GetScanCode(aMsg.lParam));
+}
+
+// static
+void RedirectedKeyDownMessageManager::RemoveNextCharMessage(HWND aWnd) {
+ MSG msg;
+ if (WinUtils::PeekMessage(&msg, aWnd, WM_KEYFIRST, WM_KEYLAST,
+ PM_NOREMOVE | PM_NOYIELD) &&
+ (msg.message == WM_CHAR || msg.message == WM_SYSCHAR)) {
+ WinUtils::PeekMessage(&msg, aWnd, msg.message, msg.message,
+ PM_REMOVE | PM_NOYIELD);
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/KeyboardLayout.h b/widget/windows/KeyboardLayout.h
new file mode 100644
index 0000000000..5d430dbea0
--- /dev/null
+++ b/widget/windows/KeyboardLayout.h
@@ -0,0 +1,1156 @@
+/* -*- 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/. */
+
+#ifndef KeyboardLayout_h__
+#define KeyboardLayout_h__
+
+#include "mozilla/RefPtr.h"
+#include "nscore.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWindow.h"
+#include "nsWindowDefs.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/widget/WinMessages.h"
+#include "mozilla/widget/WinModifierKeyState.h"
+#include <windows.h>
+
+#define NS_NUM_OF_KEYS 70
+
+#define VK_OEM_1 0xBA // ';:' for US
+#define VK_OEM_PLUS 0xBB // '+' any country
+#define VK_OEM_COMMA 0xBC
+#define VK_OEM_MINUS 0xBD // '-' any country
+#define VK_OEM_PERIOD 0xBE
+#define VK_OEM_2 0xBF
+#define VK_OEM_3 0xC0
+// '/?' for Brazilian (ABNT)
+#define VK_ABNT_C1 0xC1
+// Separator in Numpad for Brazilian (ABNT) or JIS keyboard for Mac.
+#define VK_ABNT_C2 0xC2
+#define VK_OEM_4 0xDB
+#define VK_OEM_5 0xDC
+#define VK_OEM_6 0xDD
+#define VK_OEM_7 0xDE
+#define VK_OEM_8 0xDF
+#define VK_OEM_102 0xE2
+#define VK_OEM_CLEAR 0xFE
+
+class nsIUserIdleServiceInternal;
+
+namespace mozilla {
+namespace widget {
+
+enum ScanCode : uint16_t {
+ eCapsLock = 0x003A,
+ eNumLock = 0xE045,
+ eShiftLeft = 0x002A,
+ eShiftRight = 0x0036,
+ eControlLeft = 0x001D,
+ eControlRight = 0xE01D,
+ eAltLeft = 0x0038,
+ eAltRight = 0xE038,
+};
+
+// 0: nsIWidget's native modifier flag
+// 1: Virtual keycode which does not distinguish whether left or right location.
+// 2: Virtual keycode which distinguishes whether left or right location.
+// 3: Scan code.
+static const uint32_t sModifierKeyMap[][4] = {
+ {nsIWidget::CAPS_LOCK, VK_CAPITAL, 0, ScanCode::eCapsLock},
+ {nsIWidget::NUM_LOCK, VK_NUMLOCK, 0, ScanCode::eNumLock},
+ {nsIWidget::SHIFT_L, VK_SHIFT, VK_LSHIFT, ScanCode::eShiftLeft},
+ {nsIWidget::SHIFT_R, VK_SHIFT, VK_RSHIFT, ScanCode::eShiftRight},
+ {nsIWidget::CTRL_L, VK_CONTROL, VK_LCONTROL, ScanCode::eControlLeft},
+ {nsIWidget::CTRL_R, VK_CONTROL, VK_RCONTROL, ScanCode::eControlRight},
+ {nsIWidget::ALT_L, VK_MENU, VK_LMENU, ScanCode::eAltLeft},
+ {nsIWidget::ALT_R, VK_MENU, VK_RMENU, ScanCode::eAltRight}};
+
+class KeyboardLayout;
+
+class MOZ_STACK_CLASS UniCharsAndModifiers final {
+ public:
+ UniCharsAndModifiers() {}
+ UniCharsAndModifiers operator+(const UniCharsAndModifiers& aOther) const;
+ UniCharsAndModifiers& operator+=(const UniCharsAndModifiers& aOther);
+
+ /**
+ * Append a pair of unicode character and the final modifier.
+ */
+ void Append(char16_t aUniChar, Modifiers aModifiers);
+ void Clear() {
+ mChars.Truncate();
+ mModifiers.Clear();
+ }
+ bool IsEmpty() const {
+ MOZ_ASSERT(mChars.Length() == mModifiers.Length());
+ return mChars.IsEmpty();
+ }
+
+ char16_t CharAt(size_t aIndex) const {
+ MOZ_ASSERT(aIndex < Length());
+ return mChars[aIndex];
+ }
+ Modifiers ModifiersAt(size_t aIndex) const {
+ MOZ_ASSERT(aIndex < Length());
+ return mModifiers[aIndex];
+ }
+ size_t Length() const {
+ MOZ_ASSERT(mChars.Length() == mModifiers.Length());
+ return mChars.Length();
+ }
+
+ bool IsProducingCharsWithAltGr() const {
+ return !IsEmpty() && (ModifiersAt(0) & MODIFIER_ALTGRAPH) != 0;
+ }
+
+ void FillModifiers(Modifiers aModifiers);
+ /**
+ * OverwriteModifiersIfBeginsWith() assigns mModifiers with aOther between
+ * [0] and [aOther.mLength - 1] only when mChars begins with aOther.mChars.
+ */
+ void OverwriteModifiersIfBeginsWith(const UniCharsAndModifiers& aOther);
+
+ bool UniCharsEqual(const UniCharsAndModifiers& aOther) const;
+ bool UniCharsCaseInsensitiveEqual(const UniCharsAndModifiers& aOther) const;
+ bool BeginsWith(const UniCharsAndModifiers& aOther) const;
+
+ const nsString& ToString() const { return mChars; }
+
+ private:
+ nsAutoString mChars;
+ // 5 is enough number for normal keyboard layout handling. On Windows,
+ // a dead key sequence may cause inputting up to 5 characters per key press.
+ CopyableAutoTArray<Modifiers, 5> mModifiers;
+};
+
+struct DeadKeyEntry {
+ char16_t BaseChar;
+ char16_t CompositeChar;
+
+ DeadKeyEntry(char16_t aBaseChar, char16_t aCompositeChar)
+ : BaseChar(aBaseChar), CompositeChar(aCompositeChar) {}
+
+ bool operator<(const DeadKeyEntry& aOther) const {
+ return this->BaseChar < aOther.BaseChar;
+ }
+
+ bool operator==(const DeadKeyEntry& aOther) const {
+ return this->BaseChar == aOther.BaseChar;
+ }
+};
+
+class DeadKeyTable {
+ friend class KeyboardLayout;
+
+ uint16_t mEntries;
+ // KeyboardLayout::AddDeadKeyTable() will allocate as many entries as
+ // required. It is the only way to create new DeadKeyTable instances.
+ DeadKeyEntry mTable[1];
+
+ void Init(const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) {
+ mEntries = aEntries;
+ memcpy(mTable, aDeadKeyArray, aEntries * sizeof(DeadKeyEntry));
+ }
+
+ static uint32_t SizeInBytes(uint32_t aEntries) {
+ return offsetof(DeadKeyTable, mTable) + aEntries * sizeof(DeadKeyEntry);
+ }
+
+ public:
+ uint32_t Entries() const { return mEntries; }
+
+ bool IsEqual(const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) const {
+ return (mEntries == aEntries &&
+ !memcmp(mTable, aDeadKeyArray, aEntries * sizeof(DeadKeyEntry)));
+ }
+
+ char16_t GetCompositeChar(char16_t aBaseChar) const;
+};
+
+class VirtualKey {
+ public:
+ enum ShiftStateIndex : uint8_t {
+ // 0 - Normal
+ eNormal = 0,
+ // 1 - Shift
+ eShift,
+ // 2 - Control
+ eControl,
+ // 3 - Control + Shift
+ eControlShift,
+ // 4 - Alt
+ eAlt,
+ // 5 - Alt + Shift
+ eAltShift,
+ // 6 - Alt + Control (AltGr)
+ eAltGr,
+ // 7 - Alt + Control + Shift (AltGr + Shift)
+ eAltGrShift,
+ // 8 - CapsLock
+ eWithCapsLock,
+ // 9 - CapsLock + Shift
+ eShiftWithCapsLock,
+ // 10 - CapsLock + Control
+ eControlWithCapsLock,
+ // 11 - CapsLock + Control + Shift
+ eControlShiftWithCapsLock,
+ // 12 - CapsLock + Alt
+ eAltWithCapsLock,
+ // 13 - CapsLock + Alt + Shift
+ eAltShiftWithCapsLock,
+ // 14 - CapsLock + Alt + Control (CapsLock + AltGr)
+ eAltGrWithCapsLock,
+ // 15 - CapsLock + Alt + Control + Shift (CapsLock + AltGr + Shift)
+ eAltGrShiftWithCapsLock,
+ };
+
+ enum ShiftStateFlag {
+ STATE_SHIFT = 0x01,
+ STATE_CONTROL = 0x02,
+ STATE_ALT = 0x04,
+ STATE_CAPSLOCK = 0x08,
+ // ShiftState needs to have AltGr state separately since this is necessary
+ // for lossless conversion with Modifiers.
+ STATE_ALTGRAPH = 0x80,
+ // Useful to remove or check Ctrl and Alt flags.
+ STATE_CONTROL_ALT = STATE_CONTROL | STATE_ALT,
+ };
+
+ typedef uint8_t ShiftState;
+
+ static ShiftState ModifiersToShiftState(Modifiers aModifiers);
+ static ShiftState ModifierKeyStateToShiftState(
+ const ModifierKeyState& aModKeyState) {
+ return ModifiersToShiftState(aModKeyState.GetModifiers());
+ }
+ static Modifiers ShiftStateToModifiers(ShiftState aShiftState);
+ static bool IsAltGrIndex(uint8_t aIndex) {
+ return (aIndex & STATE_CONTROL_ALT) == STATE_CONTROL_ALT;
+ }
+
+ private:
+ union KeyShiftState {
+ struct {
+ char16_t Chars[4];
+ } Normal;
+ struct {
+ const DeadKeyTable* Table;
+ char16_t DeadChar;
+ } DeadKey;
+ };
+
+ KeyShiftState mShiftStates[16];
+ uint16_t mIsDeadKey;
+
+ static uint8_t ToShiftStateIndex(ShiftState aShiftState) {
+ if (!(aShiftState & STATE_ALTGRAPH)) {
+ MOZ_ASSERT(aShiftState <= eAltGrShiftWithCapsLock);
+ return static_cast<uint8_t>(aShiftState);
+ }
+ uint8_t index = aShiftState & ~STATE_ALTGRAPH;
+ index |= (STATE_ALT | STATE_CONTROL);
+ MOZ_ASSERT(index <= eAltGrShiftWithCapsLock);
+ return index;
+ }
+
+ void SetDeadKey(ShiftState aShiftState, bool aIsDeadKey) {
+ if (aIsDeadKey) {
+ mIsDeadKey |= 1 << ToShiftStateIndex(aShiftState);
+ } else {
+ mIsDeadKey &= ~(1 << ToShiftStateIndex(aShiftState));
+ }
+ }
+
+ public:
+ static void FillKbdState(PBYTE aKbdState, const ShiftState aShiftState);
+
+ bool IsDeadKey(ShiftState aShiftState) const {
+ return (mIsDeadKey & (1 << ToShiftStateIndex(aShiftState))) != 0;
+ }
+
+ /**
+ * IsChangedByAltGr() is useful to check if a key with AltGr produces
+ * different character(s) from the key without AltGr.
+ * Note that this is designed for checking if a keyboard layout has AltGr
+ * key. So, this result may not exactly correct for the key since it's
+ * okay to fails in some edge cases when we check all keys which produce
+ * character(s) in a layout.
+ */
+ bool IsChangedByAltGr(ShiftState aShiftState) const {
+ MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState));
+ MOZ_ASSERT(IsAltGrIndex(aShiftState));
+ MOZ_ASSERT(IsDeadKey(aShiftState) ||
+ mShiftStates[aShiftState].Normal.Chars[0]);
+ const ShiftState kShiftStateWithoutAltGr =
+ aShiftState - ShiftStateIndex::eAltGr;
+ if (IsDeadKey(aShiftState) != IsDeadKey(kShiftStateWithoutAltGr)) {
+ return false;
+ }
+ if (IsDeadKey(aShiftState)) {
+ return mShiftStates[aShiftState].DeadKey.DeadChar !=
+ mShiftStates[kShiftStateWithoutAltGr].DeadKey.DeadChar;
+ }
+ for (size_t i = 0; i < 4; i++) {
+ if (mShiftStates[aShiftState].Normal.Chars[i] !=
+ mShiftStates[kShiftStateWithoutAltGr].Normal.Chars[i]) {
+ return true;
+ }
+ if (!mShiftStates[aShiftState].Normal.Chars[i] &&
+ !mShiftStates[kShiftStateWithoutAltGr].Normal.Chars[i]) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ void AttachDeadKeyTable(ShiftState aShiftState,
+ const DeadKeyTable* aDeadKeyTable) {
+ MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState));
+ mShiftStates[aShiftState].DeadKey.Table = aDeadKeyTable;
+ }
+
+ void SetNormalChars(ShiftState aShiftState, const char16_t* aChars,
+ uint32_t aNumOfChars);
+ void SetDeadChar(ShiftState aShiftState, char16_t aDeadChar);
+ const DeadKeyTable* MatchingDeadKeyTable(const DeadKeyEntry* aDeadKeyArray,
+ uint32_t aEntries) const;
+ inline char16_t GetCompositeChar(ShiftState aShiftState,
+ char16_t aBaseChar) const {
+ return mShiftStates[ToShiftStateIndex(aShiftState)]
+ .DeadKey.Table->GetCompositeChar(aBaseChar);
+ }
+
+ char16_t GetCompositeChar(const ModifierKeyState& aModKeyState,
+ char16_t aBaseChar) const {
+ return GetCompositeChar(ModifierKeyStateToShiftState(aModKeyState),
+ aBaseChar);
+ }
+
+ /**
+ * GetNativeUniChars() returns character(s) which is produced by the
+ * key with given modifiers. This does NOT return proper MODIFIER_ALTGRAPH
+ * state because this is raw accessor of the database of this key.
+ */
+ UniCharsAndModifiers GetNativeUniChars(ShiftState aShiftState) const;
+ UniCharsAndModifiers GetNativeUniChars(
+ const ModifierKeyState& aModKeyState) const {
+ return GetNativeUniChars(ModifierKeyStateToShiftState(aModKeyState));
+ }
+
+ /**
+ * GetUniChars() returns characters and modifiers which are not consumed
+ * to input the character.
+ * For example, if you specify Ctrl key but the key produces no character
+ * with Ctrl, this returns character(s) which is produced by the key
+ * without Ctrl. So, the result is useful to decide KeyboardEvent.key
+ * value.
+ * Another example is, if you specify Ctrl key and the key produces
+ * different character(s) from the case without Ctrl key, this returns
+ * the character(s) *without* MODIFIER_CONTROL. This modifier information
+ * is useful for eKeyPress since TextEditor does not treat eKeyPress events
+ * whose modifier includes MODIFIER_ALT and/or MODIFIER_CONTROL.
+ *
+ * @param aShiftState Modifiers which you want to retrieve
+ * KeyboardEvent.key value for the key with.
+ * If AltGr key is pressed, this should include
+ * STATE_ALTGRAPH and should NOT include
+ * STATE_ALT nor STATE_CONTROL.
+ * If both Alt and Ctrl are pressed to emulate
+ * AltGr, this should include both STATE_ALT and
+ * STATE_CONTROL but should NOT include
+ * MODIFIER_ALTGRAPH.
+ * Then, this returns proper modifiers when
+ * this key produces no character with AltGr.
+ */
+ UniCharsAndModifiers GetUniChars(ShiftState aShiftState) const;
+ UniCharsAndModifiers GetUniChars(const ModifierKeyState& aModKeyState) const {
+ return GetUniChars(ModifierKeyStateToShiftState(aModKeyState));
+ }
+};
+
+class MOZ_STACK_CLASS NativeKey final {
+ friend class KeyboardLayout;
+
+ public:
+ struct FakeCharMsg {
+ UINT mCharCode;
+ UINT mScanCode;
+ bool mIsSysKey;
+ bool mIsDeadKey;
+ bool mConsumed;
+
+ FakeCharMsg()
+ : mCharCode(0),
+ mScanCode(0),
+ mIsSysKey(false),
+ mIsDeadKey(false),
+ mConsumed(false) {}
+
+ MSG GetCharMsg(HWND aWnd) const {
+ MSG msg;
+ msg.hwnd = aWnd;
+ msg.message = mIsDeadKey && mIsSysKey ? WM_SYSDEADCHAR
+ : mIsDeadKey ? WM_DEADCHAR
+ : mIsSysKey ? WM_SYSCHAR
+ : WM_CHAR;
+ msg.wParam = static_cast<WPARAM>(mCharCode);
+ msg.lParam = static_cast<LPARAM>(mScanCode << 16);
+ msg.time = 0;
+ msg.pt.x = msg.pt.y = 0;
+ return msg;
+ }
+ };
+
+ NativeKey(nsWindow* aWidget, const MSG& aMessage,
+ const ModifierKeyState& aModKeyState,
+ HKL aOverrideKeyboardLayout = 0,
+ nsTArray<FakeCharMsg>* aFakeCharMsgs = nullptr);
+
+ ~NativeKey();
+
+ /**
+ * Handle WM_KEYDOWN message or WM_SYSKEYDOWN message. The instance must be
+ * initialized with WM_KEYDOWN or WM_SYSKEYDOWN.
+ * Returns true if dispatched keydown event or keypress event is consumed.
+ * Otherwise, false.
+ */
+ bool HandleKeyDownMessage(bool* aEventDispatched = nullptr) const;
+
+ /**
+ * Handles WM_CHAR message or WM_SYSCHAR message. The instance must be
+ * initialized with them.
+ * Returns true if dispatched keypress event is consumed. Otherwise, false.
+ */
+ bool HandleCharMessage(bool* aEventDispatched = nullptr) const;
+
+ /**
+ * Handles keyup message. Returns true if the event is consumed.
+ * Otherwise, false.
+ */
+ bool HandleKeyUpMessage(bool* aEventDispatched = nullptr) const;
+
+ /**
+ * Handles WM_APPCOMMAND message. Returns true if the event is consumed.
+ * Otherwise, false.
+ */
+ bool HandleAppCommandMessage() const;
+
+ /**
+ * Callback of TextEventDispatcherListener::WillDispatchKeyboardEvent().
+ * This method sets alternative char codes of aKeyboardEvent.
+ */
+ void WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndex);
+
+ /**
+ * Returns true if aChar is a control character which shouldn't be inputted
+ * into focused text editor.
+ */
+ static bool IsControlChar(char16_t aChar);
+
+ bool IsShift() const { return mModKeyState.IsShift(); }
+ bool IsControl() const { return mModKeyState.IsControl(); }
+ bool IsAlt() const { return mModKeyState.IsAlt(); }
+ bool MaybeEmulatingAltGraph() const;
+ Modifiers GetModifiers() const { return mModKeyState.GetModifiers(); }
+ const ModifierKeyState& ModifierKeyStateRef() const { return mModKeyState; }
+ VirtualKey::ShiftState GetShiftState() const {
+ return VirtualKey::ModifierKeyStateToShiftState(mModKeyState);
+ }
+
+ /**
+ * GenericVirtualKeyCode() returns virtual keycode which cannot distinguish
+ * position of modifier keys. E.g., VK_CONTROL for both ControlLeft and
+ * ControlRight.
+ */
+ uint8_t GenericVirtualKeyCode() const { return mOriginalVirtualKeyCode; }
+
+ /**
+ * SpecificVirtualKeyCode() returns virtual keycode which can distinguish
+ * position of modifier keys. E.g., returns VK_LCONTROL or VK_RCONTROL
+ * instead of VK_CONTROL. If the key message is synthesized with not
+ * enough information, this prefers left position's keycode.
+ */
+ uint8_t SpecificVirtualKeyCode() const { return mVirtualKeyCode; }
+
+ private:
+ NativeKey* mLastInstance;
+ // mRemovingMsg is set at removing a char message from
+ // GetFollowingCharMessage().
+ MSG mRemovingMsg;
+ // mReceivedMsg is set when another instance starts to handle the message
+ // unexpectedly.
+ MSG mReceivedMsg;
+ RefPtr<nsWindow> mWidget;
+ RefPtr<TextEventDispatcher> mDispatcher;
+ HKL mKeyboardLayout;
+ MSG mMsg;
+ // mFollowingCharMsgs stores WM_CHAR, WM_SYSCHAR, WM_DEADCHAR or
+ // WM_SYSDEADCHAR message which follows WM_KEYDOWN.
+ // Note that the stored messaged are already removed from the queue.
+ // FYI: 5 is enough number for usual keyboard layout handling. On Windows,
+ // a dead key sequence may cause inputting up to 5 characters per key press.
+ AutoTArray<MSG, 5> mFollowingCharMsgs;
+ // mRemovedOddCharMsgs stores WM_CHAR messages which are caused by ATOK or
+ // WXG (they are Japanese IME) when the user tries to do "Kakutei-undo"
+ // (it means "undo the last commit").
+ nsTArray<MSG> mRemovedOddCharMsgs;
+ // If dispatching eKeyDown or eKeyPress event causes focus change,
+ // the instance shouldn't handle remaning char messages. For checking it,
+ // this should store first focused window.
+ HWND mFocusedWndBeforeDispatch;
+
+ uint32_t mDOMKeyCode;
+ KeyNameIndex mKeyNameIndex;
+ CodeNameIndex mCodeNameIndex;
+
+ ModifierKeyState mModKeyState;
+
+ // mVirtualKeyCode distinguishes left key or right key of modifier key.
+ uint8_t mVirtualKeyCode;
+ // mOriginalVirtualKeyCode doesn't distinguish left key or right key of
+ // modifier key. However, if the given keycode is VK_PROCESS, it's resolved
+ // to a keycode before it's handled by IME.
+ uint8_t mOriginalVirtualKeyCode;
+
+ // mCommittedChars indicates the inputted characters which is committed by
+ // the key. If dead key fail to composite a character, mCommittedChars
+ // indicates both the dead characters and the base characters.
+ UniCharsAndModifiers mCommittedCharsAndModifiers;
+
+ // Following strings are computed by
+ // ComputeInputtingStringWithKeyboardLayout() which is typically called
+ // before dispatching keydown event.
+ // mInputtingStringAndModifiers's string is the string to be
+ // inputted into the focused editor and its modifier state is proper
+ // modifier state for inputting the string into the editor.
+ UniCharsAndModifiers mInputtingStringAndModifiers;
+ // mShiftedString is the string to be inputted into the editor with
+ // current modifier state with active shift state.
+ UniCharsAndModifiers mShiftedString;
+ // mUnshiftedString is the string to be inputted into the editor with
+ // current modifier state without shift state.
+ UniCharsAndModifiers mUnshiftedString;
+ // Following integers are computed by
+ // ComputeInputtingStringWithKeyboardLayout() which is typically called
+ // before dispatching keydown event. The meaning of these values is same
+ // as charCode.
+ uint32_t mShiftedLatinChar;
+ uint32_t mUnshiftedLatinChar;
+
+ WORD mScanCode;
+ bool mIsExtended;
+ // mIsRepeat is true if the key message is caused by the auto-repeat
+ // feature.
+ bool mIsRepeat;
+ bool mIsDeadKey;
+ // mIsPrintableKey is true if the key may be a printable key without
+ // any modifier keys. Otherwise, false.
+ // Please note that the event may not cause any text input even if this
+ // is true. E.g., it might be dead key state or Ctrl key may be pressed.
+ bool mIsPrintableKey;
+ // mIsSkippableInRemoteProcess is false if the key event shouldn't be
+ // skipped in the remote process even if it's too old event.
+ bool mIsSkippableInRemoteProcess;
+ // mCharMessageHasGone is true if the message is a keydown message and
+ // it's followed by at least one char message but it's gone at removing
+ // from the queue. This could occur if PeekMessage() or something is
+ // hooked by odd tool.
+ bool mCharMessageHasGone;
+ // mIsOverridingKeyboardLayout is true if the instance temporarily overriding
+ // keyboard layout with specified by the constructor.
+ bool mIsOverridingKeyboardLayout;
+ // mCanIgnoreModifierStateAtKeyPress is true if it's allowed to remove
+ // Ctrl or Alt modifier state at dispatching eKeyPress.
+ bool mCanIgnoreModifierStateAtKeyPress;
+
+ nsTArray<FakeCharMsg>* mFakeCharMsgs;
+
+ // When a keydown event is dispatched at handling WM_APPCOMMAND, the computed
+ // virtual keycode is set to this. Even if we consume WM_APPCOMMAND message,
+ // Windows may send WM_KEYDOWN and WM_KEYUP message for them.
+ // At that time, we should not dispatch key events for them.
+ static uint8_t sDispatchedKeyOfAppCommand;
+
+ NativeKey() {
+ MOZ_CRASH("The default constructor of NativeKey isn't available");
+ }
+
+ void InitWithAppCommand();
+ void InitWithKeyOrChar();
+
+ /**
+ * InitIsSkippableForKeyOrChar() initializes mIsSkippableInRemoteProcess with
+ * mIsRepeat and previous key message information. So, this must be called
+ * after mIsRepeat is initialized.
+ */
+ void InitIsSkippableForKeyOrChar(const MSG& aLastKeyMSG);
+
+ /**
+ * InitCommittedCharsAndModifiersWithFollowingCharMessages() initializes
+ * mCommittedCharsAndModifiers with mFollowingCharMsgs and mModKeyState.
+ * If mFollowingCharMsgs includes non-printable char messages, they are
+ * ignored (skipped).
+ */
+ void InitCommittedCharsAndModifiersWithFollowingCharMessages();
+
+ UINT GetScanCodeWithExtendedFlag() const;
+
+ // The result is one of eKeyLocation*.
+ uint32_t GetKeyLocation() const;
+
+ /**
+ * RemoveFollowingOddCharMessages() removes odd WM_CHAR messages from the
+ * queue when IsIMEDoingKakuteiUndo() returns true.
+ */
+ void RemoveFollowingOddCharMessages();
+
+ /**
+ * "Kakutei-Undo" of ATOK or WXG (both of them are Japanese IME) causes
+ * strange WM_KEYDOWN/WM_KEYUP/WM_CHAR message pattern. So, when this
+ * returns true, the caller needs to be careful for processing the messages.
+ */
+ bool IsIMEDoingKakuteiUndo() const;
+
+ /**
+ * This returns true if user types a number key in numpad with Alt key
+ * to input a Unicode character from its scalar value.
+ * Note that inputting Unicode scalar value is available without NumLock.
+ * Therefore, this returns true even if user presses a function key on
+ * numpad without NumLock, but that may be intended to perform a shortcut
+ * key like Alt + Home.
+ */
+ bool MaybeTypingUnicodeScalarValue() const {
+ return !mIsExtended && IsSysKeyDownOrKeyUpMessage() && IsAlt() &&
+ !IsControl() && !IsShift() &&
+ ((mScanCode >= 0x004F && mScanCode <= 0x0052) || // Numpad0-3
+ (mScanCode >= 0x004B && mScanCode <= 0x004D) || // Numpad4-6
+ (mScanCode >= 0x0047 && mScanCode <= 0x0049)); // Numpad7-9
+ }
+
+ bool IsKeyDownMessage() const {
+ return mMsg.message == WM_KEYDOWN || mMsg.message == WM_SYSKEYDOWN;
+ }
+ bool IsKeyUpMessage() const {
+ return mMsg.message == WM_KEYUP || mMsg.message == WM_SYSKEYUP;
+ }
+ bool IsSysKeyDownOrKeyUpMessage() const {
+ return mMsg.message == WM_SYSKEYDOWN || mMsg.message == WM_SYSKEYUP;
+ }
+ bool IsCharOrSysCharMessage(const MSG& aMSG) const {
+ return IsCharOrSysCharMessage(aMSG.message);
+ }
+ bool IsCharOrSysCharMessage(UINT aMessage) const {
+ return (aMessage == WM_CHAR || aMessage == WM_SYSCHAR);
+ }
+ bool IsCharMessage(const MSG& aMSG) const {
+ return IsCharMessage(aMSG.message);
+ }
+ bool IsCharMessage(UINT aMessage) const {
+ return (IsCharOrSysCharMessage(aMessage) || IsDeadCharMessage(aMessage));
+ }
+ bool IsDeadCharMessage(const MSG& aMSG) const {
+ return IsDeadCharMessage(aMSG.message);
+ }
+ bool IsDeadCharMessage(UINT aMessage) const {
+ return (aMessage == WM_DEADCHAR || aMessage == WM_SYSDEADCHAR);
+ }
+ bool IsSysCharMessage(const MSG& aMSG) const {
+ return IsSysCharMessage(aMSG.message);
+ }
+ bool IsSysCharMessage(UINT aMessage) const {
+ return (aMessage == WM_SYSCHAR || aMessage == WM_SYSDEADCHAR);
+ }
+ bool MayBeSameCharMessage(const MSG& aCharMsg1, const MSG& aCharMsg2) const;
+ bool IsSamePhysicalKeyMessage(const MSG& aKeyOrCharMsg1,
+ const MSG& aKeyOrCharMsg2) const;
+ bool IsFollowedByPrintableCharMessage() const;
+ bool IsFollowedByPrintableCharOrSysCharMessage() const;
+ bool IsFollowedByDeadCharMessage() const;
+ bool IsPrintableCharMessage(const MSG& aMSG) const {
+ return aMSG.message == WM_CHAR &&
+ !IsControlChar(static_cast<char16_t>(aMSG.wParam));
+ }
+ bool IsEnterKeyPressCharMessage(const MSG& aMSG) const {
+ return aMSG.message == WM_CHAR && aMSG.wParam == '\r';
+ }
+ bool IsPrintableCharOrSysCharMessage(const MSG& aMSG) const {
+ return IsCharOrSysCharMessage(aMSG) &&
+ !IsControlChar(static_cast<char16_t>(aMSG.wParam));
+ }
+ bool IsControlCharMessage(const MSG& aMSG) const {
+ return IsCharMessage(aMSG.message) &&
+ IsControlChar(static_cast<char16_t>(aMSG.wParam));
+ }
+
+ /**
+ * IsReservedBySystem() returns true if the key combination is reserved by
+ * the system. Even if it's consumed by web apps, the message should be
+ * sent to next wndproc.
+ */
+ bool IsReservedBySystem() const;
+
+ /**
+ * GetFollowingCharMessage() returns following char message of handling
+ * keydown event. If the message is found, this method returns true.
+ * Otherwise, returns false.
+ *
+ * WARNING: Even if this returns true, aCharMsg may be WM_NULL or its
+ * hwnd may be different window.
+ */
+ bool GetFollowingCharMessage(MSG& aCharMsg);
+
+ /**
+ * Wraps MapVirtualKeyEx() with MAPVK_VSC_TO_VK.
+ */
+ uint8_t ComputeVirtualKeyCodeFromScanCode() const;
+
+ /**
+ * Wraps MapVirtualKeyEx() with MAPVK_VSC_TO_VK_EX.
+ */
+ uint8_t ComputeVirtualKeyCodeFromScanCodeEx() const;
+
+ /**
+ * Wraps MapVirtualKeyEx() with MAPVK_VK_TO_VSC_EX or MAPVK_VK_TO_VSC.
+ */
+ uint16_t ComputeScanCodeExFromVirtualKeyCode(UINT aVirtualKeyCode) const;
+
+ /**
+ * Wraps MapVirtualKeyEx() with MAPVK_VSC_TO_VK and MAPVK_VK_TO_CHAR.
+ */
+ char16_t ComputeUnicharFromScanCode() const;
+
+ /**
+ * Initializes the aKeyEvent with the information stored in the instance.
+ */
+ nsEventStatus InitKeyEvent(WidgetKeyboardEvent& aKeyEvent,
+ const ModifierKeyState& aModKeyState) const;
+ nsEventStatus InitKeyEvent(WidgetKeyboardEvent& aKeyEvent) const;
+
+ /**
+ * Dispatches a command event for aEventCommand.
+ * Returns true if the event is consumed. Otherwise, false.
+ */
+ bool DispatchCommandEvent(uint32_t aEventCommand) const;
+
+ /**
+ * DispatchKeyPressEventsWithRetrievedCharMessages() dispatches keypress
+ * event(s) with retrieved char messages.
+ */
+ bool DispatchKeyPressEventsWithRetrievedCharMessages() const;
+
+ /**
+ * DispatchKeyPressEventsWithoutCharMessage() dispatches keypress event(s)
+ * without char messages. So, this should be used only when there are no
+ * following char messages.
+ */
+ bool DispatchKeyPressEventsWithoutCharMessage() const;
+
+ /**
+ * Checkes whether the key event down message is handled without following
+ * WM_CHAR messages. For example, if following WM_CHAR message indicates
+ * control character input, the WM_CHAR message is unclear whether it's
+ * caused by a printable key with Ctrl or just a function key such as Enter
+ * or Backspace.
+ */
+ bool NeedsToHandleWithoutFollowingCharMessages() const;
+
+ /**
+ * ComputeInputtingStringWithKeyboardLayout() computes string to be inputted
+ * with the key and the modifier state, without shift state and with shift
+ * state.
+ */
+ void ComputeInputtingStringWithKeyboardLayout();
+
+ /**
+ * IsFocusedWindowChanged() returns true if focused window is changed
+ * after the instance is created.
+ */
+ bool IsFocusedWindowChanged() const {
+ return mFocusedWndBeforeDispatch != ::GetFocus();
+ }
+
+ /**
+ * Handles WM_CHAR message or WM_SYSCHAR message. The instance must be
+ * initialized with WM_KEYDOWN, WM_SYSKEYDOWN or them.
+ * Returns true if dispatched keypress event is consumed. Otherwise, false.
+ */
+ bool HandleCharMessage(const MSG& aCharMsg,
+ bool* aEventDispatched = nullptr) const;
+
+ // Calls of PeekMessage() from NativeKey might cause nested message handling
+ // due to (perhaps) odd API hook. NativeKey should do nothing if given
+ // message is tried to be retrieved by another instance.
+
+ /**
+ * sLatestInstacne is a pointer to the newest instance of NativeKey which is
+ * handling a key or char message(s).
+ */
+ static NativeKey* sLatestInstance;
+
+ static const MSG sEmptyMSG;
+
+ static MSG sLastKeyOrCharMSG;
+
+ static MSG sLastKeyMSG;
+
+ // Set to non-zero if we receive a WM_KEYDOWN message which introduces only
+ // a high surrogate. Then, it'll be cleared when next keydown or char message
+ // is received.
+ static char16_t sPendingHighSurrogate;
+
+ static bool IsEmptyMSG(const MSG& aMSG) {
+ return !memcmp(&aMSG, &sEmptyMSG, sizeof(MSG));
+ }
+
+ bool IsAnotherInstanceRemovingCharMessage() const {
+ return mLastInstance && !IsEmptyMSG(mLastInstance->mRemovingMsg);
+ }
+
+ public:
+ /**
+ * Returns last key or char MSG. If no MSG has been received yet, the result
+ * is empty MSG (i.e., .message is WM_NULL).
+ */
+ static const MSG& LastKeyOrCharMSG() { return sLastKeyOrCharMSG; }
+};
+
+class KeyboardLayout {
+ public:
+ static KeyboardLayout* GetInstance();
+ static void Shutdown();
+
+ /**
+ * GetLayout() returns a keyboard layout which has already been loaded in the
+ * singleton instance or active keyboard layout.
+ */
+ static HKL GetLayout() {
+ if (!sInstance || sInstance->mIsPendingToRestoreKeyboardLayout) {
+ return ::GetKeyboardLayout(0);
+ }
+ return sInstance->mKeyboardLayout;
+ }
+
+ /**
+ * GetLoadedLayout() returns a keyboard layout which was loaded in the
+ * singleton instance. This may be different from the active keyboard layout
+ * on the system if we override the keyboard layout for synthesizing native
+ * key events for tests.
+ */
+ HKL GetLoadedLayout() { return mKeyboardLayout; }
+
+ /**
+ * GetLoadedLayoutName() returns the name of the loaded keyboard layout in the
+ * singleton instance.
+ */
+ nsCString GetLoadedLayoutName() {
+ return KeyboardLayout::GetLayoutName(mKeyboardLayout);
+ }
+
+ static void NotifyIdleServiceOfUserActivity();
+
+ static bool IsPrintableCharKey(uint8_t aVirtualKey);
+
+ /**
+ * HasAltGr() returns true if the keyboard layout's AltRight key is AltGr
+ * key.
+ */
+ bool HasAltGr() const { return mHasAltGr; }
+
+ /**
+ * IsDeadKey() returns true if aVirtualKey is a dead key with aModKeyState.
+ * This method isn't stateful.
+ */
+ bool IsDeadKey(uint8_t aVirtualKey,
+ const ModifierKeyState& aModKeyState) const;
+ bool IsDeadKey(const NativeKey& aNativeKey) const {
+ return IsDeadKey(aNativeKey.GenericVirtualKeyCode(),
+ aNativeKey.ModifierKeyStateRef());
+ }
+
+ /**
+ * IsInDeadKeySequence() returns true when it's in a dead key sequence.
+ * It starts when a dead key is down and ends when another key down causes
+ * inactivating the dead key state.
+ */
+ bool IsInDeadKeySequence() const { return !mActiveDeadKeys.IsEmpty(); }
+
+ /**
+ * IsSysKey() returns true if aVirtualKey with aModKeyState causes WM_SYSKEY*
+ * or WM_SYS*CHAR messages.
+ */
+ bool IsSysKey(uint8_t aVirtualKey,
+ const ModifierKeyState& aModKeyState) const;
+ bool IsSysKey(const NativeKey& aNativeKey) const {
+ return IsSysKey(aNativeKey.GenericVirtualKeyCode(),
+ aNativeKey.ModifierKeyStateRef());
+ }
+
+ /**
+ * GetUniCharsAndModifiers() returns characters which are inputted by
+ * aVirtualKey with aModKeyState. This method isn't stateful.
+ * Note that if the combination causes text input, the result's Ctrl and
+ * Alt key state are never active.
+ */
+ UniCharsAndModifiers GetUniCharsAndModifiers(
+ uint8_t aVirtualKey, const ModifierKeyState& aModKeyState) const {
+ VirtualKey::ShiftState shiftState =
+ VirtualKey::ModifierKeyStateToShiftState(aModKeyState);
+ return GetUniCharsAndModifiers(aVirtualKey, shiftState);
+ }
+ UniCharsAndModifiers GetUniCharsAndModifiers(
+ const NativeKey& aNativeKey) const {
+ return GetUniCharsAndModifiers(aNativeKey.GenericVirtualKeyCode(),
+ aNativeKey.GetShiftState());
+ }
+
+ /**
+ * OnLayoutChange() must be called before the first keydown message is
+ * received. LoadLayout() changes the keyboard state, that causes breaking
+ * dead key state. Therefore, we need to load the layout before the first
+ * keydown message.
+ */
+ void OnLayoutChange(HKL aKeyboardLayout) {
+ MOZ_ASSERT(!mIsOverridden);
+ LoadLayout(aKeyboardLayout);
+ }
+
+ /**
+ * OverrideLayout() loads the specified keyboard layout.
+ */
+ void OverrideLayout(HKL aLayout) {
+ mIsOverridden = true;
+ LoadLayout(aLayout);
+ }
+
+ /**
+ * RestoreLayout() loads the current keyboard layout of the thread.
+ */
+ void RestoreLayout() {
+ mIsOverridden = false;
+ mIsPendingToRestoreKeyboardLayout = true;
+ }
+
+ uint32_t ConvertNativeKeyCodeToDOMKeyCode(UINT aNativeKeyCode) const;
+
+ /**
+ * ConvertNativeKeyCodeToKeyNameIndex() returns KeyNameIndex value for
+ * non-printable keys (except some special keys like space key).
+ */
+ KeyNameIndex ConvertNativeKeyCodeToKeyNameIndex(uint8_t aVirtualKey) const;
+
+ /**
+ * ConvertScanCodeToCodeNameIndex() returns CodeNameIndex value for
+ * the given scan code. aScanCode can be over 0xE000 since this method
+ * doesn't use Windows API.
+ */
+ static CodeNameIndex ConvertScanCodeToCodeNameIndex(UINT aScanCode);
+
+ /**
+ * This wraps MapVirtualKeyEx() API with MAPVK_VK_TO_VSC.
+ */
+ WORD ComputeScanCodeForVirtualKeyCode(uint8_t aVirtualKeyCode) const;
+
+ /**
+ * Implementation of nsIWidget::SynthesizeNativeKeyEvent().
+ */
+ nsresult SynthesizeNativeKeyEvent(nsWindow* aWidget,
+ int32_t aNativeKeyboardLayout,
+ int32_t aNativeKeyCode,
+ uint32_t aModifierFlags,
+ const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters);
+
+ private:
+ KeyboardLayout();
+ ~KeyboardLayout();
+
+ static KeyboardLayout* sInstance;
+ static StaticRefPtr<nsIUserIdleServiceInternal> sIdleService;
+
+ struct DeadKeyTableListEntry {
+ DeadKeyTableListEntry* next;
+ uint8_t data[1];
+ };
+
+ HKL mKeyboardLayout = nullptr;
+
+ VirtualKey mVirtualKeys[NS_NUM_OF_KEYS] = {};
+ DeadKeyTableListEntry* mDeadKeyTableListHead = nullptr;
+ // When mActiveDeadKeys is empty, it's not in dead key sequence.
+ // Otherwise, it contains virtual keycodes which are pressed in current
+ // dead key sequence.
+ nsTArray<uint8_t> mActiveDeadKeys;
+ // mDeadKeyShiftStates is always same length as mActiveDeadKeys.
+ // This stores shift states at pressing each dead key stored in
+ // mActiveDeadKeys.
+ nsTArray<VirtualKey::ShiftState> mDeadKeyShiftStates;
+
+ bool mIsOverridden = false;
+ bool mIsPendingToRestoreKeyboardLayout = false;
+ bool mHasAltGr = false;
+
+ static inline int32_t GetKeyIndex(uint8_t aVirtualKey);
+ static bool AddDeadKeyEntry(char16_t aBaseChar, char16_t aCompositeChar,
+ nsTArray<DeadKeyEntry>& aDeadKeyArray);
+ bool EnsureDeadKeyActive(bool aIsActive, uint8_t aDeadKey,
+ const PBYTE aDeadKeyKbdState);
+ uint32_t GetDeadKeyCombinations(uint8_t aDeadKey,
+ const PBYTE aDeadKeyKbdState,
+ uint16_t aShiftStatesWithBaseChars,
+ nsTArray<DeadKeyEntry>& aDeadKeyArray);
+ /**
+ * Activates or deactivates dead key state.
+ */
+ void ActivateDeadKeyState(const NativeKey& aNativeKey);
+ void DeactivateDeadKeyState();
+
+ const DeadKeyTable* AddDeadKeyTable(const DeadKeyEntry* aDeadKeyArray,
+ uint32_t aEntries);
+ void ReleaseDeadKeyTables();
+
+ /**
+ * Loads the specified keyboard layout. This method always clear the dead key
+ * state.
+ */
+ void LoadLayout(HKL aLayout);
+
+ /**
+ * Gets the keyboard layout name of aLayout. Be careful, this may be too
+ * slow to call at handling user input.
+ */
+ static nsCString GetLayoutName(HKL aLayout);
+
+ /**
+ * InitNativeKey() must be called when actually widget receives WM_KEYDOWN or
+ * WM_KEYUP. This method is stateful. This saves current dead key state at
+ * WM_KEYDOWN. Additionally, computes current inputted character(s) and set
+ * them to the aNativeKey.
+ */
+ void InitNativeKey(NativeKey& aNativeKey);
+
+ /**
+ * MaybeInitNativeKeyAsDeadKey() initializes aNativeKey only when aNativeKey
+ * is a dead key's event.
+ * When it's not in a dead key sequence, this activates the dead key state.
+ * When it's in a dead key sequence, this initializes aNativeKey with a
+ * composite character or a preceding dead char and a dead char which should
+ * be caused by aNativeKey.
+ * Returns true when this initializes aNativeKey. Otherwise, false.
+ */
+ bool MaybeInitNativeKeyAsDeadKey(NativeKey& aNativeKey);
+
+ /**
+ * MaybeInitNativeKeyWithCompositeChar() may initialize aNativeKey with
+ * proper composite character when dead key produces a composite character.
+ * Otherwise, just returns false.
+ */
+ bool MaybeInitNativeKeyWithCompositeChar(NativeKey& aNativeKey);
+
+ /**
+ * See the comment of GetUniCharsAndModifiers() below.
+ */
+ UniCharsAndModifiers GetUniCharsAndModifiers(
+ uint8_t aVirtualKey, VirtualKey::ShiftState aShiftState) const;
+
+ /**
+ * GetDeadUniCharsAndModifiers() returns dead chars which are stored in
+ * current dead key sequence. So, this is stateful.
+ */
+ UniCharsAndModifiers GetDeadUniCharsAndModifiers() const;
+
+ /**
+ * GetCompositeChar() returns a composite character with dead character
+ * caused by mActiveDeadKeys, mDeadKeyShiftStates and a base character
+ * (aBaseChar).
+ * If the combination of the dead character and the base character doesn't
+ * cause a composite character, this returns 0.
+ */
+ char16_t GetCompositeChar(char16_t aBaseChar) const;
+
+ // NativeKey class should access InitNativeKey() directly, but it shouldn't
+ // be available outside of NativeKey. So, let's make NativeKey a friend
+ // class of this.
+ friend class NativeKey;
+};
+
+class RedirectedKeyDownMessageManager {
+ public:
+ /*
+ * If a window receives WM_KEYDOWN message or WM_SYSKEYDOWM message which is
+ * a redirected message, NativeKey::DispatchKeyDownAndKeyPressEvent()
+ * prevents to dispatch eKeyDown event because it has been dispatched
+ * before the message was redirected. However, in some cases, WM_*KEYDOWN
+ * message handler may not handle actually. Then, the message handler needs
+ * to forget the redirected message and remove WM_CHAR message or WM_SYSCHAR
+ * message for the redirected keydown message. AutoFlusher class is a helper
+ * class for doing it. This must be created in the stack.
+ */
+ class MOZ_STACK_CLASS AutoFlusher final {
+ public:
+ AutoFlusher(nsWindow* aWidget, const MSG& aMsg)
+ : mCancel(!RedirectedKeyDownMessageManager::IsRedirectedMessage(aMsg)),
+ mWidget(aWidget),
+ mMsg(aMsg) {}
+
+ ~AutoFlusher() {
+ if (mCancel) {
+ return;
+ }
+ // Prevent unnecessary keypress event
+ if (!mWidget->Destroyed()) {
+ RedirectedKeyDownMessageManager::RemoveNextCharMessage(mMsg.hwnd);
+ }
+ // Foreget the redirected message
+ RedirectedKeyDownMessageManager::Forget();
+ }
+
+ void Cancel() { mCancel = true; }
+
+ private:
+ bool mCancel;
+ RefPtr<nsWindow> mWidget;
+ const MSG& mMsg;
+ };
+
+ static void WillRedirect(const MSG& aMsg, bool aDefualtPrevented) {
+ sRedirectedKeyDownMsg = aMsg;
+ sDefaultPreventedOfRedirectedMsg = aDefualtPrevented;
+ }
+
+ static void Forget() { sRedirectedKeyDownMsg.message = WM_NULL; }
+
+ static void PreventDefault() { sDefaultPreventedOfRedirectedMsg = true; }
+ static bool DefaultPrevented() { return sDefaultPreventedOfRedirectedMsg; }
+
+ static bool IsRedirectedMessage(const MSG& aMsg);
+
+ /**
+ * RemoveNextCharMessage() should be called by WM_KEYDOWN or WM_SYSKEYDOWM
+ * message handler. If there is no WM_(SYS)CHAR message for it, this
+ * method does nothing.
+ * NOTE: WM_(SYS)CHAR message is posted by TranslateMessage() API which is
+ * called in message loop. So, WM_(SYS)KEYDOWN message should have
+ * WM_(SYS)CHAR message in the queue if the keydown event causes character
+ * input.
+ */
+ static void RemoveNextCharMessage(HWND aWnd);
+
+ private:
+ // sRedirectedKeyDownMsg is WM_KEYDOWN message or WM_SYSKEYDOWN message which
+ // is reirected with SendInput() API by
+ // widget::NativeKey::DispatchKeyDownAndKeyPressEvent()
+ static MSG sRedirectedKeyDownMsg;
+ static bool sDefaultPreventedOfRedirectedMsg;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/windows/LSPAnnotator.cpp b/widget/windows/LSPAnnotator.cpp
new file mode 100644
index 0000000000..ab3a768d66
--- /dev/null
+++ b/widget/windows/LSPAnnotator.cpp
@@ -0,0 +1,135 @@
+/* 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/. */
+
+/**
+ * LSPs are evil little bits of code that are allowed to inject into our
+ * networking stack by Windows. Once they have wormed into our process
+ * they gnaw at our innards until we crash. Here we force the buggers
+ * into the light by recording them in our crash information.
+ * We do the enumeration on a thread because I'm concerned about startup perf
+ * on machines with several LSPs.
+ */
+
+#include "nsICrashReporter.h"
+#include "nsISupportsImpl.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsQueryObject.h"
+#include "nsWindowsHelpers.h"
+#include <windows.h>
+#include <rpc.h>
+#include <ws2spi.h>
+
+namespace mozilla {
+namespace crashreporter {
+
+class LSPAnnotationGatherer : public Runnable {
+ ~LSPAnnotationGatherer() {}
+
+ public:
+ LSPAnnotationGatherer() : Runnable("crashreporter::LSPAnnotationGatherer") {}
+ NS_DECL_NSIRUNNABLE
+
+ void Annotate();
+ nsCString mString;
+};
+
+void LSPAnnotationGatherer::Annotate() {
+ nsCOMPtr<nsICrashReporter> cr =
+ do_GetService("@mozilla.org/toolkit/crash-reporter;1");
+ bool enabled;
+ if (cr && NS_SUCCEEDED(cr->GetCrashReporterEnabled(&enabled)) && enabled) {
+ cr->AnnotateCrashReport("Winsock_LSP"_ns, mString);
+ }
+}
+
+NS_IMETHODIMP
+LSPAnnotationGatherer::Run() {
+ DWORD size = 0;
+ int err;
+ // Get the size of the buffer we need
+ if (SOCKET_ERROR != WSCEnumProtocols(nullptr, nullptr, &size, &err) ||
+ err != WSAENOBUFS) {
+ // Er, what?
+ MOZ_ASSERT_UNREACHABLE(
+ "WSCEnumProtocols succeeded when it should have "
+ "failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ auto byteArray = MakeUnique<char[]>(size);
+ WSAPROTOCOL_INFOW* providers =
+ reinterpret_cast<WSAPROTOCOL_INFOW*>(byteArray.get());
+
+ int n = WSCEnumProtocols(nullptr, providers, &size, &err);
+ if (n == SOCKET_ERROR) {
+ // Lame. We provided the right size buffer; we'll just give up now.
+ NS_WARNING("Could not get LSP list");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCString str;
+ for (int i = 0; i < n; i++) {
+ AppendUTF16toUTF8(nsDependentString(providers[i].szProtocol), str);
+ str.AppendLiteral(" : ");
+ str.AppendInt(providers[i].iVersion);
+ str.AppendLiteral(" : ");
+ str.AppendInt(providers[i].iAddressFamily);
+ str.AppendLiteral(" : ");
+ str.AppendInt(providers[i].iSocketType);
+ str.AppendLiteral(" : ");
+ str.AppendInt(providers[i].iProtocol);
+ str.AppendLiteral(" : ");
+ str.AppendPrintf("0x%lx", providers[i].dwServiceFlags1);
+ str.AppendLiteral(" : ");
+ str.AppendPrintf("0x%lx", providers[i].dwProviderFlags);
+ str.AppendLiteral(" : ");
+
+ wchar_t path[MAX_PATH];
+ int pathLen = MAX_PATH;
+ if (!WSCGetProviderPath(&providers[i].ProviderId, path, &pathLen, &err)) {
+ AppendUTF16toUTF8(nsDependentString(path), str);
+ }
+
+ str.AppendLiteral(" : ");
+ // Call WSCGetProviderInfo to obtain the category flags for this provider.
+ // When present, these flags inform Windows as to which order to chain the
+ // providers.
+ DWORD categoryInfo;
+ size_t categoryInfoSize = sizeof(categoryInfo);
+ if (!WSCGetProviderInfo(&providers[i].ProviderId, ProviderInfoLspCategories,
+ (PBYTE)&categoryInfo, &categoryInfoSize, 0, &err)) {
+ str.AppendPrintf("0x%lx", categoryInfo);
+ }
+
+ str.AppendLiteral(" : ");
+ if (providers[i].ProtocolChain.ChainLen <= BASE_PROTOCOL) {
+ // If we're dealing with a catalog entry that identifies an individual
+ // base or layer provider, log its provider GUID.
+ RPC_CSTR provIdStr = nullptr;
+ if (UuidToStringA(&providers[i].ProviderId, &provIdStr) == RPC_S_OK) {
+ str.Append(reinterpret_cast<char*>(provIdStr));
+ RpcStringFreeA(&provIdStr);
+ }
+ }
+
+ if (i + 1 != n) {
+ str.AppendLiteral(" \n ");
+ }
+ }
+
+ mString = str;
+ NS_DispatchToMainThread(
+ NewRunnableMethod("crashreporter::LSPAnnotationGatherer::Annotate", this,
+ &LSPAnnotationGatherer::Annotate));
+ return NS_OK;
+}
+
+void LSPAnnotate() {
+ nsCOMPtr<nsIRunnable> runnable(new LSPAnnotationGatherer());
+ NS_DispatchBackgroundTask(runnable.forget());
+}
+
+} // namespace crashreporter
+} // namespace mozilla
diff --git a/widget/windows/LegacyJumpListBuilder.cpp b/widget/windows/LegacyJumpListBuilder.cpp
new file mode 100644
index 0000000000..fbfe10f64b
--- /dev/null
+++ b/widget/windows/LegacyJumpListBuilder.cpp
@@ -0,0 +1,647 @@
+/* -*- 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 "LegacyJumpListBuilder.h"
+
+#include "nsError.h"
+#include "nsCOMPtr.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsArrayUtils.h"
+#include "nsWidgetsCID.h"
+#include "WinTaskbar.h"
+#include "nsDirectoryServiceUtils.h"
+#include "mozilla/Preferences.h"
+#include "nsStringStream.h"
+#include "nsThreadUtils.h"
+#include "mozilla/LazyIdleThread.h"
+#include "nsIObserverService.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/mscom/ApartmentRegion.h"
+#include "mozilla/mscom/EnsureMTA.h"
+
+#include <shellapi.h>
+#include "WinUtils.h"
+
+using mozilla::dom::Promise;
+
+// The amount of time, in milliseconds, that our IO thread will stay alive after
+// the last event it processes.
+#define DEFAULT_THREAD_TIMEOUT_MS 30000
+
+namespace mozilla {
+namespace widget {
+
+// defined in WinTaskbar.cpp
+extern const wchar_t* gMozillaJumpListIDGeneric;
+
+Atomic<bool> LegacyJumpListBuilder::sBuildingList(false);
+const char kPrefTaskbarEnabled[] = "browser.taskbar.lists.enabled";
+
+NS_IMPL_ISUPPORTS(LegacyJumpListBuilder, nsILegacyJumpListBuilder, nsIObserver)
+#define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change"
+#define TOPIC_CLEAR_PRIVATE_DATA "clear-private-data"
+
+namespace detail {
+
+class DoneCommitListBuildCallback final : public nsIRunnable {
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ public:
+ DoneCommitListBuildCallback(nsILegacyJumpListCommittedCallback* aCallback,
+ LegacyJumpListBuilder* aBuilder)
+ : mCallback(aCallback), mBuilder(aBuilder), mResult(false) {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mCallback) {
+ Unused << mCallback->Done(mResult);
+ }
+ // Ensure we are releasing on the main thread.
+ Destroy();
+ return NS_OK;
+ }
+
+ void SetResult(bool aResult) { mResult = aResult; }
+
+ private:
+ ~DoneCommitListBuildCallback() {
+ // Destructor does not always call on the main thread.
+ MOZ_ASSERT(!mCallback);
+ MOZ_ASSERT(!mBuilder);
+ }
+
+ void Destroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mCallback = nullptr;
+ mBuilder = nullptr;
+ }
+
+ // These two references MUST be released on the main thread.
+ RefPtr<nsILegacyJumpListCommittedCallback> mCallback;
+ RefPtr<LegacyJumpListBuilder> mBuilder;
+ bool mResult;
+};
+
+NS_IMPL_ISUPPORTS(DoneCommitListBuildCallback, nsIRunnable);
+
+} // namespace detail
+
+LegacyJumpListBuilder::LegacyJumpListBuilder()
+ : mMaxItems(0),
+ mHasCommit(false),
+ mMonitor("LegacyJumpListBuilderMonitor") {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Instantiate mJumpListMgr in the multithreaded apartment so that proxied
+ // calls on that object do not need to interact with the main thread's message
+ // pump.
+ mscom::EnsureMTA([&]() {
+ RefPtr<ICustomDestinationList> jumpListMgr;
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_DestinationList, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ICustomDestinationList, getter_AddRefs(jumpListMgr));
+ if (FAILED(hr)) {
+ return;
+ }
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ // Since we are accessing mJumpListMgr across different threads
+ // (ie, different apartments), mJumpListMgr must be an agile reference.
+ mJumpListMgr = mscom::AgileReference(jumpListMgr);
+ });
+
+ if (!mJumpListMgr) {
+ return;
+ }
+
+ // Make a lazy thread for any IO
+ mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "Jump List",
+ LazyIdleThread::ManualShutdown);
+ Preferences::AddStrongObserver(this, kPrefTaskbarEnabled);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService("@mozilla.org/observer-service;1");
+ if (observerService) {
+ observerService->AddObserver(this, TOPIC_PROFILE_BEFORE_CHANGE, false);
+ observerService->AddObserver(this, TOPIC_CLEAR_PRIVATE_DATA, false);
+ }
+}
+
+LegacyJumpListBuilder::~LegacyJumpListBuilder() {
+ Preferences::RemoveObserver(this, kPrefTaskbarEnabled);
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::SetAppUserModelID(
+ const nsAString& aAppUserModelId) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE;
+
+ RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve();
+ if (!jumpListMgr) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mAppUserModelId.Assign(aAppUserModelId);
+ // MSIX packages explicitly do not support setting the appid from within
+ // the app, as it is set in the package manifest instead.
+ if (!mozilla::widget::WinUtils::HasPackageIdentity()) {
+ jumpListMgr->SetAppID(mAppUserModelId.get());
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::GetAvailable(int16_t* aAvailable) {
+ *aAvailable = false;
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (mJumpListMgr) *aAvailable = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::GetIsListCommitted(bool* aCommit) {
+ *aCommit = mHasCommit;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::GetMaxListItems(int16_t* aMaxItems) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE;
+
+ *aMaxItems = 0;
+
+ if (sBuildingList) {
+ *aMaxItems = mMaxItems;
+ return NS_OK;
+ }
+
+ RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve();
+ if (!jumpListMgr) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ IObjectArray* objArray;
+ if (SUCCEEDED(jumpListMgr->BeginList(&mMaxItems, IID_PPV_ARGS(&objArray)))) {
+ *aMaxItems = mMaxItems;
+
+ if (objArray) objArray->Release();
+
+ jumpListMgr->AbortList();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::InitListBuild(JSContext* aCx,
+ Promise** aPromise) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (!mJumpListMgr) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod<StoreCopyPassByRRef<RefPtr<Promise>>>(
+ "InitListBuild", this, &LegacyJumpListBuilder::DoInitListBuild,
+ promise);
+ nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+void LegacyJumpListBuilder::DoInitListBuild(RefPtr<Promise>&& aPromise) {
+ // Since we're invoking COM interfaces to talk to the shell on a background
+ // thread, we need to be running inside a multithreaded apartment.
+ mscom::MTARegion mta;
+ MOZ_ASSERT(mta.IsValid());
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ MOZ_ASSERT(mJumpListMgr);
+
+ if (sBuildingList) {
+ AbortListBuild();
+ }
+
+ HRESULT hr = E_UNEXPECTED;
+ auto errorHandler = MakeScopeExit([&aPromise, &hr]() {
+ if (SUCCEEDED(hr)) {
+ return;
+ }
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "InitListBuildReject", [promise = std::move(aPromise)]() {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ }));
+ });
+
+ RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve();
+ if (!jumpListMgr) {
+ return;
+ }
+
+ nsTArray<nsString> urisToRemove;
+ RefPtr<IObjectArray> objArray;
+ hr = jumpListMgr->BeginList(
+ &mMaxItems,
+ IID_PPV_ARGS(static_cast<IObjectArray**>(getter_AddRefs(objArray))));
+ if (FAILED(hr)) {
+ return;
+ }
+
+ // The returned objArray of removed items are for manually removed items.
+ // This does not return items which are removed because they were previously
+ // part of the jump list but are no longer part of the jump list.
+ sBuildingList = true;
+ RemoveIconCacheAndGetJumplistShortcutURIs(objArray, urisToRemove);
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "InitListBuildResolve", [urisToRemove = std::move(urisToRemove),
+ promise = std::move(aPromise)]() {
+ promise->MaybeResolve(urisToRemove);
+ }));
+}
+
+// Ensures that we have no old ICO files left in the jump list cache
+nsresult LegacyJumpListBuilder::RemoveIconCacheForAllItems() {
+ // Construct the path of our jump list cache
+ nsCOMPtr<nsIFile> jumpListCacheDir;
+ nsresult rv =
+ NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(jumpListCacheDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = jumpListCacheDir->AppendNative(
+ nsDependentCString(mozilla::widget::FaviconHelper::kJumpListCacheDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = jumpListCacheDir->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Loop through each directory entry and remove all ICO files found
+ do {
+ nsCOMPtr<nsIFile> currFile;
+ if (NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile))) || !currFile)
+ break;
+
+ nsAutoString path;
+ if (NS_FAILED(currFile->GetPath(path))) continue;
+
+ if (StringTail(path, 4).LowerCaseEqualsASCII(".ico")) {
+ // Check if the cached ICO file exists
+ bool exists;
+ if (NS_FAILED(currFile->Exists(&exists)) || !exists) continue;
+
+ // We found an ICO file that exists, so we should remove it
+ currFile->Remove(false);
+ }
+ } while (true);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::AddListToBuild(int16_t aCatType,
+ nsIArray* items,
+ const nsAString& catName,
+ bool* _retval) {
+ nsresult rv;
+
+ *_retval = false;
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE;
+
+ RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve();
+ if (!jumpListMgr) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ switch (aCatType) {
+ case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_TASKS: {
+ NS_ENSURE_ARG_POINTER(items);
+
+ HRESULT hr;
+ RefPtr<IObjectCollection> collection;
+ hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
+ CLSCTX_INPROC_SERVER, IID_IObjectCollection,
+ getter_AddRefs(collection));
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ // Build the list
+ uint32_t length;
+ items->GetLength(&length);
+ for (uint32_t i = 0; i < length; ++i) {
+ nsCOMPtr<nsILegacyJumpListItem> item = do_QueryElementAt(items, i);
+ if (!item) continue;
+ // Check for separators
+ if (IsSeparator(item)) {
+ RefPtr<IShellLinkW> link;
+ rv = LegacyJumpListSeparator::GetSeparator(link);
+ if (NS_FAILED(rv)) return rv;
+ collection->AddObject(link);
+ continue;
+ }
+ // These should all be ShellLinks
+ RefPtr<IShellLinkW> link;
+ rv = LegacyJumpListShortcut::GetShellLink(item, link, mIOThread);
+ if (NS_FAILED(rv)) return rv;
+ collection->AddObject(link);
+ }
+
+ // We need IObjectArray to submit
+ RefPtr<IObjectArray> pArray;
+ hr = collection->QueryInterface(IID_IObjectArray, getter_AddRefs(pArray));
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ // Add the tasks
+ hr = jumpListMgr->AddUserTasks(pArray);
+ if (SUCCEEDED(hr)) *_retval = true;
+ return NS_OK;
+ } break;
+ case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_RECENT: {
+ if (SUCCEEDED(jumpListMgr->AppendKnownCategory(KDC_RECENT)))
+ *_retval = true;
+ return NS_OK;
+ } break;
+ case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_FREQUENT: {
+ if (SUCCEEDED(jumpListMgr->AppendKnownCategory(KDC_FREQUENT)))
+ *_retval = true;
+ return NS_OK;
+ } break;
+ case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_CUSTOMLIST: {
+ NS_ENSURE_ARG_POINTER(items);
+
+ if (catName.IsEmpty()) return NS_ERROR_INVALID_ARG;
+
+ HRESULT hr;
+ RefPtr<IObjectCollection> collection;
+ hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
+ CLSCTX_INPROC_SERVER, IID_IObjectCollection,
+ getter_AddRefs(collection));
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ uint32_t length;
+ items->GetLength(&length);
+ for (uint32_t i = 0; i < length; ++i) {
+ nsCOMPtr<nsILegacyJumpListItem> item = do_QueryElementAt(items, i);
+ if (!item) continue;
+ int16_t type;
+ if (NS_FAILED(item->GetType(&type))) continue;
+ switch (type) {
+ case nsILegacyJumpListItem::JUMPLIST_ITEM_SEPARATOR: {
+ RefPtr<IShellLinkW> shellItem;
+ rv = LegacyJumpListSeparator::GetSeparator(shellItem);
+ if (NS_FAILED(rv)) return rv;
+ collection->AddObject(shellItem);
+ } break;
+ case nsILegacyJumpListItem::JUMPLIST_ITEM_LINK: {
+ RefPtr<IShellItem2> shellItem;
+ rv = LegacyJumpListLink::GetShellItem(item, shellItem);
+ if (NS_FAILED(rv)) return rv;
+ collection->AddObject(shellItem);
+ } break;
+ case nsILegacyJumpListItem::JUMPLIST_ITEM_SHORTCUT: {
+ RefPtr<IShellLinkW> shellItem;
+ rv = LegacyJumpListShortcut::GetShellLink(item, shellItem,
+ mIOThread);
+ if (NS_FAILED(rv)) return rv;
+ collection->AddObject(shellItem);
+ } break;
+ }
+ }
+
+ // We need IObjectArray to submit
+ RefPtr<IObjectArray> pArray;
+ hr = collection->QueryInterface(IID_IObjectArray, (LPVOID*)&pArray);
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ // Add the tasks
+ hr = jumpListMgr->AppendCategory(
+ reinterpret_cast<const wchar_t*>(catName.BeginReading()), pArray);
+ if (SUCCEEDED(hr)) *_retval = true;
+
+ // Get rid of the old icons
+ nsCOMPtr<nsIRunnable> event =
+ new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true);
+ mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+
+ return NS_OK;
+ } break;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::AbortListBuild() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE;
+
+ RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve();
+ if (!jumpListMgr) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ jumpListMgr->AbortList();
+ sBuildingList = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::CommitListBuild(
+ nsILegacyJumpListCommittedCallback* aCallback) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE;
+
+ // Also holds a strong reference to this to prevent use-after-free.
+ RefPtr<detail::DoneCommitListBuildCallback> callback =
+ new detail::DoneCommitListBuildCallback(aCallback, this);
+
+ // The builder has a strong reference in the callback already, so we do not
+ // need to do it for this runnable again.
+ RefPtr<nsIRunnable> event =
+ NewNonOwningRunnableMethod<RefPtr<detail::DoneCommitListBuildCallback>>(
+ "LegacyJumpListBuilder::DoCommitListBuild", this,
+ &LegacyJumpListBuilder::DoCommitListBuild, std::move(callback));
+ Unused << mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+
+ return NS_OK;
+}
+
+void LegacyJumpListBuilder::DoCommitListBuild(
+ RefPtr<detail::DoneCommitListBuildCallback> aCallback) {
+ // Since we're invoking COM interfaces to talk to the shell on a background
+ // thread, we need to be running inside a multithreaded apartment.
+ mscom::MTARegion mta;
+ MOZ_ASSERT(mta.IsValid());
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ MOZ_ASSERT(mJumpListMgr);
+ MOZ_ASSERT(aCallback);
+
+ HRESULT hr = E_UNEXPECTED;
+ auto onExit = MakeScopeExit([&hr, &aCallback]() {
+ // XXX We might want some specific error data here.
+ aCallback->SetResult(SUCCEEDED(hr));
+ Unused << NS_DispatchToMainThread(aCallback);
+ });
+
+ RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve();
+ if (!jumpListMgr) {
+ return;
+ }
+
+ hr = jumpListMgr->CommitList();
+ sBuildingList = false;
+
+ if (SUCCEEDED(hr)) {
+ mHasCommit = true;
+ }
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::DeleteActiveList(bool* _retval) {
+ *_retval = false;
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE;
+
+ if (sBuildingList) {
+ AbortListBuild();
+ }
+
+ RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve();
+ if (!jumpListMgr) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (SUCCEEDED(jumpListMgr->DeleteList(mAppUserModelId.get()))) {
+ *_retval = true;
+ }
+
+ return NS_OK;
+}
+
+/* internal */
+
+bool LegacyJumpListBuilder::IsSeparator(nsCOMPtr<nsILegacyJumpListItem>& item) {
+ int16_t type;
+ item->GetType(&type);
+ if (NS_FAILED(item->GetType(&type))) return false;
+
+ if (type == nsILegacyJumpListItem::JUMPLIST_ITEM_SEPARATOR) return true;
+ return false;
+}
+
+// RemoveIconCacheAndGetJumplistShortcutURIs - does multiple things to
+// avoid unnecessary extra XPCOM incantations. For each object in the input
+// array, if it's an IShellLinkW, this deletes the cached icon and adds the
+// url param to a list of URLs to be removed from the places database.
+void LegacyJumpListBuilder::RemoveIconCacheAndGetJumplistShortcutURIs(
+ IObjectArray* aObjArray, nsTArray<nsString>& aURISpecs) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Early return here just in case some versions of Windows don't populate this
+ if (!aObjArray) {
+ return;
+ }
+
+ uint32_t count = 0;
+ aObjArray->GetCount(&count);
+
+ for (uint32_t idx = 0; idx < count; idx++) {
+ RefPtr<IShellLinkW> pLink;
+
+ if (FAILED(aObjArray->GetAt(idx, IID_IShellLinkW,
+ static_cast<void**>(getter_AddRefs(pLink))))) {
+ continue;
+ }
+
+ wchar_t buf[MAX_PATH];
+ HRESULT hres = pLink->GetArguments(buf, MAX_PATH);
+ if (SUCCEEDED(hres)) {
+ LPWSTR* arglist;
+ int32_t numArgs;
+
+ arglist = ::CommandLineToArgvW(buf, &numArgs);
+ if (arglist && numArgs > 0) {
+ nsString spec(arglist[0]);
+ aURISpecs.AppendElement(std::move(spec));
+ ::LocalFree(arglist);
+ }
+ }
+
+ int iconIdx = 0;
+ hres = pLink->GetIconLocation(buf, MAX_PATH, &iconIdx);
+ if (SUCCEEDED(hres)) {
+ nsDependentString spec(buf);
+ DeleteIconFromDisk(spec);
+ }
+ }
+}
+
+void LegacyJumpListBuilder::DeleteIconFromDisk(const nsAString& aPath) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Check that we aren't deleting some arbitrary file that is not an icon
+ if (StringTail(aPath, 4).LowerCaseEqualsASCII(".ico")) {
+ // Construct the parent path of the passed in path
+ nsCOMPtr<nsIFile> icoFile;
+ nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(icoFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ icoFile->Remove(false);
+ }
+}
+
+NS_IMETHODIMP LegacyJumpListBuilder::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ NS_ENSURE_ARG_POINTER(aTopic);
+ if (strcmp(aTopic, TOPIC_PROFILE_BEFORE_CHANGE) == 0) {
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService("@mozilla.org/observer-service;1");
+ if (observerService) {
+ observerService->RemoveObserver(this, TOPIC_PROFILE_BEFORE_CHANGE);
+ }
+ mIOThread->Shutdown();
+ // Clear out mJumpListMgr, as MSCOM services won't be available soon.
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ mJumpListMgr = nullptr;
+ } else if (strcmp(aTopic, "nsPref:changed") == 0 &&
+ nsDependentString(aData).EqualsASCII(kPrefTaskbarEnabled)) {
+ bool enabled = Preferences::GetBool(kPrefTaskbarEnabled, true);
+ if (!enabled) {
+ nsCOMPtr<nsIRunnable> event =
+ new mozilla::widget::AsyncDeleteAllFaviconsFromDisk();
+ mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ }
+ } else if (strcmp(aTopic, TOPIC_CLEAR_PRIVATE_DATA) == 0) {
+ // Delete JumpListCache icons from Disk, if any.
+ nsCOMPtr<nsIRunnable> event =
+ new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(false);
+ mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ }
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/LegacyJumpListBuilder.h b/widget/windows/LegacyJumpListBuilder.h
new file mode 100644
index 0000000000..1d96773c47
--- /dev/null
+++ b/widget/windows/LegacyJumpListBuilder.h
@@ -0,0 +1,71 @@
+/* -*- 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/. */
+
+#ifndef __LegacyJumpListBuilder_h__
+#define __LegacyJumpListBuilder_h__
+
+#include <windows.h>
+
+// Needed for various com interfaces
+#include <shobjidl.h>
+#undef LogSeverity // SetupAPI.h #defines this as DWORD
+
+#include "nsString.h"
+
+#include "nsILegacyJumpListBuilder.h"
+#include "nsILegacyJumpListItem.h"
+#include "LegacyJumpListItem.h"
+#include "nsIObserver.h"
+#include "nsTArray.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/LazyIdleThread.h"
+#include "mozilla/mscom/AgileReference.h"
+#include "mozilla/ReentrantMonitor.h"
+
+namespace mozilla {
+namespace widget {
+
+namespace detail {
+class DoneCommitListBuildCallback;
+} // namespace detail
+
+class LegacyJumpListBuilder : public nsILegacyJumpListBuilder,
+ public nsIObserver {
+ virtual ~LegacyJumpListBuilder();
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSILEGACYJUMPLISTBUILDER
+ NS_DECL_NSIOBSERVER
+
+ LegacyJumpListBuilder();
+
+ protected:
+ static Atomic<bool> sBuildingList;
+
+ private:
+ mscom::AgileReference<ICustomDestinationList> mJumpListMgr
+ MOZ_GUARDED_BY(mMonitor);
+ uint32_t mMaxItems MOZ_GUARDED_BY(mMonitor);
+ bool mHasCommit;
+ RefPtr<LazyIdleThread> mIOThread;
+ ReentrantMonitor mMonitor;
+ nsString mAppUserModelId;
+
+ bool IsSeparator(nsCOMPtr<nsILegacyJumpListItem>& item);
+ void RemoveIconCacheAndGetJumplistShortcutURIs(IObjectArray* aObjArray,
+ nsTArray<nsString>& aURISpecs);
+ void DeleteIconFromDisk(const nsAString& aPath);
+ nsresult RemoveIconCacheForAllItems();
+ void DoCommitListBuild(RefPtr<detail::DoneCommitListBuildCallback> aCallback);
+ void DoInitListBuild(RefPtr<dom::Promise>&& aPromise);
+
+ friend class WinTaskbar;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __LegacyJumpListBuilder_h__ */
diff --git a/widget/windows/LegacyJumpListItem.cpp b/widget/windows/LegacyJumpListItem.cpp
new file mode 100644
index 0000000000..3ffddf11f9
--- /dev/null
+++ b/widget/windows/LegacyJumpListItem.cpp
@@ -0,0 +1,559 @@
+/* -*- 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 "LegacyJumpListItem.h"
+
+#include <shellapi.h>
+#include <propvarutil.h>
+#include <propkey.h>
+
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsCRT.h"
+#include "nsNetCID.h"
+#include "nsCExternalHandlerService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Preferences.h"
+#include "LegacyJumpListBuilder.h"
+#include "WinUtils.h"
+
+namespace mozilla {
+namespace widget {
+
+// ISUPPORTS Impl's
+NS_IMPL_ISUPPORTS(LegacyJumpListItem, nsILegacyJumpListItem)
+
+NS_INTERFACE_MAP_BEGIN(LegacyJumpListSeparator)
+ NS_INTERFACE_MAP_ENTRY(nsILegacyJumpListSeparator)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsILegacyJumpListItem,
+ LegacyJumpListItemBase)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LegacyJumpListItemBase)
+NS_INTERFACE_MAP_END
+NS_IMPL_ADDREF(LegacyJumpListSeparator)
+NS_IMPL_RELEASE(LegacyJumpListSeparator)
+
+NS_INTERFACE_MAP_BEGIN(LegacyJumpListLink)
+ NS_INTERFACE_MAP_ENTRY(nsILegacyJumpListLink)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsILegacyJumpListItem,
+ LegacyJumpListItemBase)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LegacyJumpListItemBase)
+NS_INTERFACE_MAP_END
+NS_IMPL_ADDREF(LegacyJumpListLink)
+NS_IMPL_RELEASE(LegacyJumpListLink)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LegacyJumpListShortcut)
+ NS_INTERFACE_MAP_ENTRY(nsILegacyJumpListShortcut)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsILegacyJumpListItem,
+ LegacyJumpListItemBase)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsILegacyJumpListShortcut)
+NS_INTERFACE_MAP_END
+NS_IMPL_CYCLE_COLLECTING_ADDREF(LegacyJumpListShortcut)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(LegacyJumpListShortcut)
+NS_IMPL_CYCLE_COLLECTION(LegacyJumpListShortcut, mHandlerApp)
+
+NS_IMETHODIMP LegacyJumpListItemBase::GetType(int16_t* aType) {
+ NS_ENSURE_ARG_POINTER(aType);
+
+ *aType = mItemType;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListItemBase::Equals(nsILegacyJumpListItem* aItem,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aItem);
+
+ *aResult = false;
+
+ int16_t theType = nsILegacyJumpListItem::JUMPLIST_ITEM_EMPTY;
+ if (NS_FAILED(aItem->GetType(&theType))) return NS_OK;
+
+ // Make sure the types match.
+ if (Type() != theType) return NS_OK;
+
+ *aResult = true;
+
+ return NS_OK;
+}
+
+/* link impl. */
+
+NS_IMETHODIMP LegacyJumpListLink::GetUri(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = mURI);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListLink::SetUri(nsIURI* aURI) {
+ mURI = aURI;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListLink::SetUriTitle(const nsAString& aUriTitle) {
+ mUriTitle.Assign(aUriTitle);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListLink::GetUriTitle(nsAString& aUriTitle) {
+ aUriTitle.Assign(mUriTitle);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListLink::Equals(nsILegacyJumpListItem* aItem,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aItem);
+
+ nsresult rv;
+
+ *aResult = false;
+
+ int16_t theType = nsILegacyJumpListItem::JUMPLIST_ITEM_EMPTY;
+ if (NS_FAILED(aItem->GetType(&theType))) return NS_OK;
+
+ // Make sure the types match.
+ if (Type() != theType) return NS_OK;
+
+ nsCOMPtr<nsILegacyJumpListLink> link = do_QueryInterface(aItem, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // Check the titles
+ nsAutoString title;
+ link->GetUriTitle(title);
+ if (!mUriTitle.Equals(title)) return NS_OK;
+
+ // Call the internal object's equals() method to check.
+ nsCOMPtr<nsIURI> theUri;
+ bool equals = false;
+ if (NS_SUCCEEDED(link->GetUri(getter_AddRefs(theUri)))) {
+ if (!theUri) {
+ if (!mURI) *aResult = true;
+ return NS_OK;
+ }
+ if (NS_SUCCEEDED(theUri->Equals(mURI, &equals)) && equals) {
+ *aResult = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+/* shortcut impl. */
+
+NS_IMETHODIMP LegacyJumpListShortcut::GetApp(nsILocalHandlerApp** aApp) {
+ NS_IF_ADDREF(*aApp = mHandlerApp);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListShortcut::SetApp(nsILocalHandlerApp* aApp) {
+ mHandlerApp = aApp;
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListShortcut::GetIconIndex(int32_t* aIconIndex) {
+ NS_ENSURE_ARG_POINTER(aIconIndex);
+
+ *aIconIndex = mIconIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListShortcut::SetIconIndex(int32_t aIconIndex) {
+ mIconIndex = aIconIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListShortcut::GetFaviconPageUri(
+ nsIURI** aFaviconPageURI) {
+ NS_IF_ADDREF(*aFaviconPageURI = mFaviconPageURI);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListShortcut::SetFaviconPageUri(
+ nsIURI* aFaviconPageURI) {
+ mFaviconPageURI = aFaviconPageURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP LegacyJumpListShortcut::Equals(nsILegacyJumpListItem* aItem,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aItem);
+
+ nsresult rv;
+
+ *aResult = false;
+
+ int16_t theType = nsILegacyJumpListItem::JUMPLIST_ITEM_EMPTY;
+ if (NS_FAILED(aItem->GetType(&theType))) return NS_OK;
+
+ // Make sure the types match.
+ if (Type() != theType) return NS_OK;
+
+ nsCOMPtr<nsILegacyJumpListShortcut> shortcut = do_QueryInterface(aItem, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // Check the icon index
+ // int32_t idx;
+ // shortcut->GetIconIndex(&idx);
+ // if (mIconIndex != idx)
+ // return NS_OK;
+ // No need to check the icon page URI either
+
+ // Call the internal object's equals() method to check.
+ nsCOMPtr<nsILocalHandlerApp> theApp;
+ bool equals = false;
+ if (NS_SUCCEEDED(shortcut->GetApp(getter_AddRefs(theApp)))) {
+ if (!theApp) {
+ if (!mHandlerApp) *aResult = true;
+ return NS_OK;
+ }
+ if (NS_SUCCEEDED(theApp->Equals(mHandlerApp, &equals)) && equals) {
+ *aResult = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+/* internal helpers */
+
+// (static) Creates a ShellLink that encapsulate a separator.
+nsresult LegacyJumpListSeparator::GetSeparator(
+ RefPtr<IShellLinkW>& aShellLink) {
+ HRESULT hr;
+ IShellLinkW* psl;
+
+ // Create a IShellLink.
+ hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, (LPVOID*)&psl);
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ IPropertyStore* pPropStore = nullptr;
+ hr = psl->QueryInterface(IID_IPropertyStore, (LPVOID*)&pPropStore);
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ PROPVARIANT pv;
+ InitPropVariantFromBoolean(TRUE, &pv);
+
+ pPropStore->SetValue(PKEY_AppUserModel_IsDestListSeparator, pv);
+ pPropStore->Commit();
+ pPropStore->Release();
+
+ PropVariantClear(&pv);
+
+ aShellLink = dont_AddRef(psl);
+
+ return NS_OK;
+}
+
+// (static) Creates a ShellLink that encapsulate a shortcut to local apps.
+nsresult LegacyJumpListShortcut::GetShellLink(
+ nsCOMPtr<nsILegacyJumpListItem>& item, RefPtr<IShellLinkW>& aShellLink,
+ RefPtr<LazyIdleThread>& aIOThread) {
+ HRESULT hr;
+ IShellLinkW* psl;
+ nsresult rv;
+
+ // Shell links:
+ // http://msdn.microsoft.com/en-us/library/bb776891(VS.85).aspx
+ // http://msdn.microsoft.com/en-us/library/bb774950(VS.85).aspx
+
+ int16_t type;
+ if (NS_FAILED(item->GetType(&type))) return NS_ERROR_INVALID_ARG;
+
+ if (type != nsILegacyJumpListItem::JUMPLIST_ITEM_SHORTCUT)
+ return NS_ERROR_INVALID_ARG;
+
+ nsCOMPtr<nsILegacyJumpListShortcut> shortcut = do_QueryInterface(item, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILocalHandlerApp> handlerApp;
+ rv = shortcut->GetApp(getter_AddRefs(handlerApp));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create a IShellLink
+ hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, (LPVOID*)&psl);
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ // Retrieve the app path, title, description and optional command line args.
+ nsAutoString appPath, appTitle, appDescription, appArgs;
+ int32_t appIconIndex = 0;
+
+ // Path
+ nsCOMPtr<nsIFile> executable;
+ handlerApp->GetExecutable(getter_AddRefs(executable));
+
+ rv = executable->GetPath(appPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Command line parameters
+ uint32_t count = 0;
+ handlerApp->GetParameterCount(&count);
+ for (uint32_t idx = 0; idx < count; idx++) {
+ if (idx > 0) appArgs.Append(' ');
+ nsAutoString param;
+ rv = handlerApp->GetParameter(idx, param);
+ if (NS_FAILED(rv)) return rv;
+ appArgs.Append(param);
+ }
+
+ handlerApp->GetName(appTitle);
+ handlerApp->GetDetailedDescription(appDescription);
+
+ bool useUriIcon = false; // if we want to use the URI icon
+ bool usedUriIcon = false; // if we did use the URI icon
+ shortcut->GetIconIndex(&appIconIndex);
+
+ nsCOMPtr<nsIURI> iconUri;
+ rv = shortcut->GetFaviconPageUri(getter_AddRefs(iconUri));
+ if (NS_SUCCEEDED(rv) && iconUri) {
+ useUriIcon = true;
+ }
+
+ // Store the title of the app
+ if (appTitle.Length() > 0) {
+ IPropertyStore* pPropStore = nullptr;
+ hr = psl->QueryInterface(IID_IPropertyStore, (LPVOID*)&pPropStore);
+ if (FAILED(hr)) return NS_ERROR_UNEXPECTED;
+
+ PROPVARIANT pv;
+ InitPropVariantFromString(appTitle.get(), &pv);
+
+ pPropStore->SetValue(PKEY_Title, pv);
+ pPropStore->Commit();
+ pPropStore->Release();
+
+ PropVariantClear(&pv);
+ }
+
+ // Store the rest of the params
+ psl->SetPath(appPath.get());
+ psl->SetDescription(appDescription.get());
+ psl->SetArguments(appArgs.get());
+
+ if (useUriIcon) {
+ nsString icoFilePath;
+ rv = mozilla::widget::FaviconHelper::ObtainCachedIconFile(
+ iconUri, icoFilePath, aIOThread, false);
+ if (NS_SUCCEEDED(rv)) {
+ // Always use the first icon in the ICO file
+ // our encoded icon only has 1 resource
+ psl->SetIconLocation(icoFilePath.get(), 0);
+ usedUriIcon = true;
+ }
+ }
+
+ // We didn't use an ICO via URI so fall back to the app icon
+ if (!usedUriIcon) {
+ psl->SetIconLocation(appPath.get(), appIconIndex);
+ }
+
+ aShellLink = dont_AddRef(psl);
+
+ return NS_OK;
+}
+
+// If successful fills in the aSame parameter
+// aSame will be true if the path is in our icon cache
+static nsresult IsPathInOurIconCache(
+ nsCOMPtr<nsILegacyJumpListShortcut>& aShortcut, wchar_t* aPath,
+ bool* aSame) {
+ NS_ENSURE_ARG_POINTER(aPath);
+ NS_ENSURE_ARG_POINTER(aSame);
+
+ *aSame = false;
+
+ // Construct the path of our jump list cache
+ nsCOMPtr<nsIFile> jumpListCache;
+ nsresult rv =
+ NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(jumpListCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = jumpListCache->AppendNative(
+ nsDependentCString(FaviconHelper::kJumpListCacheDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString jumpListCachePath;
+ rv = jumpListCache->GetPath(jumpListCachePath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Construct the parent path of the passed in path
+ nsCOMPtr<nsIFile> passedInFile =
+ do_CreateInstance("@mozilla.org/file/local;1");
+ NS_ENSURE_TRUE(passedInFile, NS_ERROR_FAILURE);
+ nsAutoString passedInPath(aPath);
+ rv = passedInFile->InitWithPath(passedInPath);
+ nsCOMPtr<nsIFile> passedInParentFile;
+ passedInFile->GetParent(getter_AddRefs(passedInParentFile));
+ nsAutoString passedInParentPath;
+ rv = jumpListCache->GetPath(passedInParentPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aSame = jumpListCachePath.Equals(passedInParentPath);
+ return NS_OK;
+}
+
+// (static) For a given IShellLink, create and return a populated
+// nsILegacyJumpListShortcut.
+nsresult LegacyJumpListShortcut::GetJumpListShortcut(
+ IShellLinkW* pLink, nsCOMPtr<nsILegacyJumpListShortcut>& aShortcut) {
+ NS_ENSURE_ARG_POINTER(pLink);
+
+ nsresult rv;
+ HRESULT hres;
+
+ nsCOMPtr<nsILocalHandlerApp> handlerApp =
+ do_CreateInstance(NS_LOCALHANDLERAPP_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ wchar_t buf[MAX_PATH];
+
+ // Path
+ hres = pLink->GetPath(buf, MAX_PATH, nullptr, SLGP_UNCPRIORITY);
+ if (FAILED(hres)) return NS_ERROR_INVALID_ARG;
+
+ nsCOMPtr<nsIFile> file;
+ nsDependentString filepath(buf);
+ rv = NS_NewLocalFile(filepath, false, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = handlerApp->SetExecutable(file);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Parameters
+ hres = pLink->GetArguments(buf, MAX_PATH);
+ if (SUCCEEDED(hres)) {
+ LPWSTR* arglist;
+ int32_t numArgs;
+ int32_t idx;
+
+ arglist = ::CommandLineToArgvW(buf, &numArgs);
+ if (arglist) {
+ for (idx = 0; idx < numArgs; idx++) {
+ // szArglist[i] is null terminated
+ nsDependentString arg(arglist[idx]);
+ handlerApp->AppendParameter(arg);
+ }
+ ::LocalFree(arglist);
+ }
+ }
+
+ rv = aShortcut->SetApp(handlerApp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Icon index or file location
+ int iconIdx = 0;
+ hres = pLink->GetIconLocation(buf, MAX_PATH, &iconIdx);
+ if (SUCCEEDED(hres)) {
+ // XXX How do we handle converting local files to images here? Do we need
+ // to?
+ aShortcut->SetIconIndex(iconIdx);
+
+ // Obtain the local profile directory and construct the output icon file
+ // path We only set the Icon Uri if we're sure it was from our icon cache.
+ bool isInOurCache;
+ if (NS_SUCCEEDED(IsPathInOurIconCache(aShortcut, buf, &isInOurCache)) &&
+ isInOurCache) {
+ nsCOMPtr<nsIURI> iconUri;
+ nsAutoString path(buf);
+ rv = NS_NewURI(getter_AddRefs(iconUri), path);
+ if (NS_SUCCEEDED(rv)) {
+ aShortcut->SetFaviconPageUri(iconUri);
+ }
+ }
+ }
+
+ // Do we need the title and description? Probably not since handler app
+ // doesn't compare these in equals.
+
+ return NS_OK;
+}
+
+// (static) ShellItems are used to encapsulate links to things. We currently
+// only support URI links, but more support could be added, such as local file
+// and directory links.
+nsresult LegacyJumpListLink::GetShellItem(nsCOMPtr<nsILegacyJumpListItem>& item,
+ RefPtr<IShellItem2>& aShellItem) {
+ IShellItem2* psi = nullptr;
+ nsresult rv;
+
+ int16_t type;
+ if (NS_FAILED(item->GetType(&type))) return NS_ERROR_INVALID_ARG;
+
+ if (type != nsILegacyJumpListItem::JUMPLIST_ITEM_LINK)
+ return NS_ERROR_INVALID_ARG;
+
+ nsCOMPtr<nsILegacyJumpListLink> link = do_QueryInterface(item, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = link->GetUri(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec;
+ rv = uri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create the IShellItem
+ if (FAILED(SHCreateItemFromParsingName(NS_ConvertASCIItoUTF16(spec).get(),
+ nullptr, IID_PPV_ARGS(&psi)))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Set the title
+ nsAutoString linkTitle;
+ link->GetUriTitle(linkTitle);
+
+ IPropertyStore* pPropStore = nullptr;
+ HRESULT hres = psi->GetPropertyStore(GPS_DEFAULT, IID_IPropertyStore,
+ (void**)&pPropStore);
+ if (FAILED(hres)) return NS_ERROR_UNEXPECTED;
+
+ PROPVARIANT pv;
+ InitPropVariantFromString(linkTitle.get(), &pv);
+
+ // May fail due to shell item access permissions.
+ pPropStore->SetValue(PKEY_ItemName, pv);
+ pPropStore->Commit();
+ pPropStore->Release();
+
+ PropVariantClear(&pv);
+
+ aShellItem = dont_AddRef(psi);
+
+ return NS_OK;
+}
+
+// (static) For a given IShellItem, create and return a populated
+// nsILegacyJumpListLink.
+nsresult LegacyJumpListLink::GetJumpListLink(
+ IShellItem* pItem, nsCOMPtr<nsILegacyJumpListLink>& aLink) {
+ NS_ENSURE_ARG_POINTER(pItem);
+
+ // We assume for now these are URI links, but through properties we could
+ // query and create other types.
+ nsresult rv;
+ LPWSTR lpstrName = nullptr;
+
+ if (SUCCEEDED(pItem->GetDisplayName(SIGDN_URL, &lpstrName))) {
+ nsCOMPtr<nsIURI> uri;
+ nsAutoString spec(lpstrName);
+
+ rv = NS_NewURI(getter_AddRefs(uri), NS_ConvertUTF16toUTF8(spec));
+ if (NS_FAILED(rv)) return NS_ERROR_INVALID_ARG;
+
+ aLink->SetUri(uri);
+
+ ::CoTaskMemFree(lpstrName);
+ }
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/LegacyJumpListItem.h b/widget/windows/LegacyJumpListItem.h
new file mode 100644
index 0000000000..bcef82d349
--- /dev/null
+++ b/widget/windows/LegacyJumpListItem.h
@@ -0,0 +1,133 @@
+/* -*- 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/. */
+
+#ifndef __LegacyJumpListItem_h__
+#define __LegacyJumpListItem_h__
+
+#include <windows.h>
+#include <shobjidl.h>
+#undef LogSeverity // SetupAPI.h #defines this as DWORD
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/LazyIdleThread.h"
+#include "nsILegacyJumpListItem.h" // defines nsILegacyJumpListItem
+#include "nsIMIMEInfo.h" // defines nsILocalHandlerApp
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+#include "nsICryptoHash.h"
+#include "nsString.h"
+#include "nsCycleCollectionParticipant.h"
+
+class nsIThread;
+
+namespace mozilla {
+namespace widget {
+
+class LegacyJumpListItemBase : public nsILegacyJumpListItem {
+ public:
+ LegacyJumpListItemBase()
+ : mItemType(nsILegacyJumpListItem::JUMPLIST_ITEM_EMPTY) {}
+
+ explicit LegacyJumpListItemBase(int32_t type) : mItemType(type) {}
+
+ NS_DECL_NSILEGACYJUMPLISTITEM
+
+ static const char kJumpListCacheDir[];
+
+ protected:
+ virtual ~LegacyJumpListItemBase() {}
+
+ short Type() { return mItemType; }
+ short mItemType;
+};
+
+class LegacyJumpListItem : public LegacyJumpListItemBase {
+ ~LegacyJumpListItem() {}
+
+ public:
+ using LegacyJumpListItemBase::LegacyJumpListItemBase;
+
+ NS_DECL_ISUPPORTS
+};
+
+class LegacyJumpListSeparator : public LegacyJumpListItemBase,
+ public nsILegacyJumpListSeparator {
+ ~LegacyJumpListSeparator() {}
+
+ public:
+ LegacyJumpListSeparator()
+ : LegacyJumpListItemBase(nsILegacyJumpListItem::JUMPLIST_ITEM_SEPARATOR) {
+ }
+
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_NSILEGACYJUMPLISTITEM(LegacyJumpListItemBase::)
+
+ static nsresult GetSeparator(RefPtr<IShellLinkW>& aShellLink);
+};
+
+class LegacyJumpListLink : public LegacyJumpListItemBase,
+ public nsILegacyJumpListLink {
+ ~LegacyJumpListLink() {}
+
+ public:
+ LegacyJumpListLink()
+ : LegacyJumpListItemBase(nsILegacyJumpListItem::JUMPLIST_ITEM_LINK) {}
+
+ NS_DECL_ISUPPORTS
+ NS_IMETHOD GetType(int16_t* aType) override {
+ return LegacyJumpListItemBase::GetType(aType);
+ }
+ NS_IMETHOD Equals(nsILegacyJumpListItem* item, bool* _retval) override;
+ NS_DECL_NSILEGACYJUMPLISTLINK
+
+ static nsresult GetShellItem(nsCOMPtr<nsILegacyJumpListItem>& item,
+ RefPtr<IShellItem2>& aShellItem);
+ static nsresult GetJumpListLink(IShellItem* pItem,
+ nsCOMPtr<nsILegacyJumpListLink>& aLink);
+
+ protected:
+ nsString mUriTitle;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsICryptoHash> mCryptoHash;
+};
+
+class LegacyJumpListShortcut : public LegacyJumpListItemBase,
+ public nsILegacyJumpListShortcut {
+ ~LegacyJumpListShortcut() {}
+
+ public:
+ LegacyJumpListShortcut()
+ : LegacyJumpListItemBase(nsILegacyJumpListItem::JUMPLIST_ITEM_SHORTCUT) {}
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(LegacyJumpListShortcut,
+ LegacyJumpListItemBase)
+ NS_IMETHOD GetType(int16_t* aType) override {
+ return LegacyJumpListItemBase::GetType(aType);
+ }
+ NS_IMETHOD Equals(nsILegacyJumpListItem* item, bool* _retval) override;
+ NS_DECL_NSILEGACYJUMPLISTSHORTCUT
+
+ static nsresult GetShellLink(nsCOMPtr<nsILegacyJumpListItem>& item,
+ RefPtr<IShellLinkW>& aShellLink,
+ RefPtr<LazyIdleThread>& aIOThread);
+ static nsresult GetJumpListShortcut(
+ IShellLinkW* pLink, nsCOMPtr<nsILegacyJumpListShortcut>& aShortcut);
+ static nsresult GetOutputIconPath(nsCOMPtr<nsIURI> aFaviconPageURI,
+ nsCOMPtr<nsIFile>& aICOFile);
+
+ protected:
+ int32_t mIconIndex;
+ nsCOMPtr<nsIURI> mFaviconPageURI;
+ nsCOMPtr<nsILocalHandlerApp> mHandlerApp;
+
+ bool ExecutableExists(nsCOMPtr<nsILocalHandlerApp>& handlerApp);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __LegacyJumpListItem_h__ */
diff --git a/widget/windows/MediaKeysEventSourceFactory.cpp b/widget/windows/MediaKeysEventSourceFactory.cpp
new file mode 100644
index 0000000000..525aab19c6
--- /dev/null
+++ b/widget/windows/MediaKeysEventSourceFactory.cpp
@@ -0,0 +1,20 @@
+/* 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 "MediaKeysEventSourceFactory.h"
+#include "WindowsSMTCProvider.h"
+
+namespace mozilla {
+namespace widget {
+
+mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() {
+#ifndef __MINGW32__
+ return new WindowsSMTCProvider();
+#else
+ return nullptr; // MinGW doesn't support the required Windows 8.1+ APIs
+#endif
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/OSKInputPaneManager.cpp b/widget/windows/OSKInputPaneManager.cpp
new file mode 100644
index 0000000000..293a84cd28
--- /dev/null
+++ b/widget/windows/OSKInputPaneManager.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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/. */
+
+#define NTDDI_VERSION NTDDI_WIN10_RS1
+
+#include "OSKInputPaneManager.h"
+#include "nsDebug.h"
+
+#ifndef __MINGW32__
+# include <inputpaneinterop.h>
+# include <windows.ui.viewmanagement.h>
+# include <wrl.h>
+
+using namespace ABI::Windows::UI::ViewManagement;
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+#endif
+
+namespace mozilla {
+namespace widget {
+
+#ifndef __MINGW32__
+static ComPtr<IInputPane2> GetInputPane(HWND aHwnd) {
+ ComPtr<IInputPaneInterop> inputPaneInterop;
+ HRESULT hr = RoGetActivationFactory(
+ HStringReference(RuntimeClass_Windows_UI_ViewManagement_InputPane).Get(),
+ IID_PPV_ARGS(&inputPaneInterop));
+ if (NS_WARN_IF(FAILED(hr))) {
+ return nullptr;
+ }
+
+ ComPtr<IInputPane> inputPane;
+ hr = inputPaneInterop->GetForWindow(aHwnd, IID_PPV_ARGS(&inputPane));
+ if (NS_WARN_IF(FAILED(hr))) {
+ return nullptr;
+ }
+
+ ComPtr<IInputPane2> inputPane2;
+ inputPane.As(&inputPane2);
+ return inputPane2;
+}
+
+# ifdef DEBUG
+static bool IsInputPaneVisible(ComPtr<IInputPane2>& aInputPane2) {
+ ComPtr<IInputPaneControl> inputPaneControl;
+ aInputPane2.As(&inputPaneControl);
+ if (NS_WARN_IF(!inputPaneControl)) {
+ return false;
+ }
+ boolean visible = false;
+ inputPaneControl->get_Visible(&visible);
+ return visible;
+}
+
+static bool IsForegroundApp() {
+ HWND foregroundWnd = ::GetForegroundWindow();
+ if (!foregroundWnd) {
+ return false;
+ }
+ DWORD pid;
+ ::GetWindowThreadProcessId(foregroundWnd, &pid);
+ return pid == ::GetCurrentProcessId();
+}
+# endif
+#endif
+
+// static
+void OSKInputPaneManager::ShowOnScreenKeyboard(HWND aWnd) {
+#ifndef __MINGW32__
+ ComPtr<IInputPane2> inputPane2 = GetInputPane(aWnd);
+ if (!inputPane2) {
+ return;
+ }
+ boolean result;
+ inputPane2->TryShow(&result);
+ NS_WARNING_ASSERTION(
+ result || !IsForegroundApp() || IsInputPaneVisible(inputPane2),
+ "IInputPane2::TryShow is failure");
+#endif
+}
+
+// static
+void OSKInputPaneManager::DismissOnScreenKeyboard(HWND aWnd) {
+#ifndef __MINGW32__
+ ComPtr<IInputPane2> inputPane2 = GetInputPane(aWnd);
+ if (!inputPane2) {
+ return;
+ }
+ boolean result;
+ inputPane2->TryHide(&result);
+ NS_WARNING_ASSERTION(
+ result || !IsForegroundApp() || !IsInputPaneVisible(inputPane2),
+ "IInputPane2::TryHide is failure");
+#endif
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/OSKInputPaneManager.h b/widget/windows/OSKInputPaneManager.h
new file mode 100644
index 0000000000..421a5f1e02
--- /dev/null
+++ b/widget/windows/OSKInputPaneManager.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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/. */
+
+#ifndef mozilla_widget_OSKInputPaneManager_h
+#define mozilla_widget_OSKInputPaneManager_h
+
+#include <windows.h>
+
+namespace mozilla {
+namespace widget {
+
+class OSKInputPaneManager final {
+ public:
+ static void ShowOnScreenKeyboard(HWND aHwnd);
+ static void DismissOnScreenKeyboard(HWND aHwnd);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_OSKInputPaneManager_h
diff --git a/widget/windows/OSKTabTipManager.cpp b/widget/windows/OSKTabTipManager.cpp
new file mode 100644
index 0000000000..b7b06c08d1
--- /dev/null
+++ b/widget/windows/OSKTabTipManager.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 4; 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 "OSKTabTipManager.h"
+
+#include "mozilla/Preferences.h"
+#include "nsDebug.h"
+#include "mozilla/widget/WinRegistry.h"
+
+#include <shellapi.h>
+#include <shlobj.h>
+#include <windows.h>
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * Get the HWND for the on-screen keyboard, if it's up. Only
+ * allowed for Windows 8 and higher.
+ */
+static HWND GetOnScreenKeyboardWindow() {
+ const wchar_t kOSKClassName[] = L"IPTip_Main_Window";
+ HWND osk = ::FindWindowW(kOSKClassName, nullptr);
+ if (::IsWindow(osk) && ::IsWindowEnabled(osk) && ::IsWindowVisible(osk)) {
+ return osk;
+ }
+ return nullptr;
+}
+
+// static
+void OSKTabTipManager::ShowOnScreenKeyboard() {
+ const char* kOskPathPrefName = "ui.osk.on_screen_keyboard_path";
+
+ if (GetOnScreenKeyboardWindow()) {
+ return;
+ }
+
+ nsAutoString cachedPath;
+ nsresult result = Preferences::GetString(kOskPathPrefName, cachedPath);
+ if (NS_FAILED(result) || cachedPath.IsEmpty()) {
+ wchar_t path[MAX_PATH];
+ // The path to TabTip.exe is defined at the following registry key.
+ // This is pulled out of the 64-bit registry hive directly.
+ constexpr auto kRegKeyName =
+ u"Software\\Classes\\CLSID\\{054AAE20-4BEA-4347-8A35-64A533254A9D}\\LocalServer32"_ns;
+ if (!WinRegistry::GetString(HKEY_LOCAL_MACHINE, kRegKeyName, u""_ns, path,
+ WinRegistry::kLegacyWinUtilsStringFlags)) {
+ return;
+ }
+
+ std::wstring wstrpath(path);
+ // The path provided by the registry will often contain
+ // %CommonProgramFiles%, which will need to be replaced if it is present.
+ size_t commonProgramFilesOffset = wstrpath.find(L"%CommonProgramFiles%");
+ if (commonProgramFilesOffset != std::wstring::npos) {
+ // The path read from the registry contains the %CommonProgramFiles%
+ // environment variable prefix. On 64 bit Windows the
+ // SHGetKnownFolderPath function returns the common program files path
+ // with the X86 suffix for the FOLDERID_ProgramFilesCommon value.
+ // To get the correct path to TabTip.exe we first read the environment
+ // variable CommonProgramW6432 which points to the desired common
+ // files path. Failing that we fallback to the SHGetKnownFolderPath API.
+ // We then replace the %CommonProgramFiles% value with the actual common
+ // files path found in the process.
+ std::wstring commonProgramFilesPath;
+ std::vector<wchar_t> commonProgramFilesPathW6432;
+ DWORD bufferSize =
+ ::GetEnvironmentVariableW(L"CommonProgramW6432", nullptr, 0);
+ if (bufferSize) {
+ commonProgramFilesPathW6432.resize(bufferSize);
+ ::GetEnvironmentVariableW(L"CommonProgramW6432",
+ commonProgramFilesPathW6432.data(),
+ bufferSize);
+ commonProgramFilesPath =
+ std::wstring(commonProgramFilesPathW6432.data());
+ } else {
+ PWSTR path = nullptr;
+ HRESULT hres = SHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0,
+ nullptr, &path);
+ if (FAILED(hres) || !path) {
+ return;
+ }
+ commonProgramFilesPath =
+ static_cast<const wchar_t*>(nsDependentString(path).get());
+ ::CoTaskMemFree(path);
+ }
+ wstrpath.replace(commonProgramFilesOffset,
+ wcslen(L"%CommonProgramFiles%"), commonProgramFilesPath);
+ }
+
+ cachedPath.Assign(wstrpath.data());
+ Preferences::SetString(kOskPathPrefName, cachedPath);
+ }
+
+ const char16_t* cachedPathPtr;
+ cachedPath.GetData(&cachedPathPtr);
+ ShellExecuteW(nullptr, L"", char16ptr_t(cachedPathPtr), nullptr, nullptr,
+ SW_SHOW);
+}
+
+// static
+void OSKTabTipManager::DismissOnScreenKeyboard() {
+ HWND osk = GetOnScreenKeyboardWindow();
+ if (osk) {
+ ::PostMessage(osk, WM_SYSCOMMAND, SC_CLOSE, 0);
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/OSKTabTipManager.h b/widget/windows/OSKTabTipManager.h
new file mode 100644
index 0000000000..231403375b
--- /dev/null
+++ b/widget/windows/OSKTabTipManager.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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/. */
+
+#ifndef OSKTabTipManager_h
+#define OSKTabTipManager_h
+
+namespace mozilla {
+namespace widget {
+
+class OSKTabTipManager final {
+ public:
+ static void ShowOnScreenKeyboard();
+ static void DismissOnScreenKeyboard();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // OSKTabTipManager_h
diff --git a/widget/windows/OSKVRManager.cpp b/widget/windows/OSKVRManager.cpp
new file mode 100644
index 0000000000..158a0f3561
--- /dev/null
+++ b/widget/windows/OSKVRManager.cpp
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 "OSKVRManager.h"
+
+#include "FxRWindowManager.h"
+#include "VRShMem.h"
+#include "moz_external_vr.h"
+
+namespace mozilla {
+namespace widget {
+
+// static
+void OSKVRManager::ShowOnScreenKeyboard() {
+#ifdef NIGHTLY_BUILD
+ mozilla::gfx::VRShMem shmem(nullptr, true /*aRequiresMutex*/);
+ shmem.SendIMEState(FxRWindowManager::GetInstance()->GetWindowID(),
+ mozilla::gfx::VRFxEventState::FOCUS);
+#endif // NIGHTLY_BUILD
+}
+
+// static
+void OSKVRManager::DismissOnScreenKeyboard() {
+#ifdef NIGHTLY_BUILD
+ mozilla::gfx::VRShMem shmem(nullptr, true /*aRequiresMutex*/);
+ shmem.SendIMEState(FxRWindowManager::GetInstance()->GetWindowID(),
+ mozilla::gfx::VRFxEventState::BLUR);
+#endif // NIGHTLY_BUILD
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/OSKVRManager.h b/widget/windows/OSKVRManager.h
new file mode 100644
index 0000000000..7c5a1afa07
--- /dev/null
+++ b/widget/windows/OSKVRManager.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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/. */
+
+#ifndef OSKVRManager_h
+#define OSKVRManager_h
+
+namespace mozilla {
+namespace widget {
+
+class OSKVRManager final {
+ public:
+ static void ShowOnScreenKeyboard();
+ static void DismissOnScreenKeyboard();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // OSKVRManager_h
diff --git a/widget/windows/PCompositorWidget.ipdl b/widget/windows/PCompositorWidget.ipdl
new file mode 100644
index 0000000000..89ea575a47
--- /dev/null
+++ b/widget/windows/PCompositorWidget.ipdl
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 protocol PCompositorBridge;
+
+include "mozilla/dom/TabMessageUtils.h";
+include "mozilla/widget/WidgetMessageUtils.h";
+
+using mozilla::gfx::IntSize from "mozilla/gfx/Point.h";
+using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h";
+using mozilla::widget::TransparencyMode from "nsIWidget.h";
+using nsSizeMode from "nsIWidget.h";
+
+namespace mozilla {
+namespace widget {
+
+struct RemoteBackbufferHandles {
+ FileDescriptor fileMapping;
+ FileDescriptor requestReadyEvent;
+ FileDescriptor responseReadyEvent;
+};
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PCompositorWidget
+{
+ manager PCompositorBridge;
+
+parent:
+ sync Initialize(RemoteBackbufferHandles aRemoteHandles);
+
+ sync EnterPresentLock();
+ sync LeavePresentLock();
+ async UpdateTransparency(TransparencyMode aMode);
+ async NotifyVisibilityUpdated(nsSizeMode aSizeMode, bool aIsFullyOccluded);
+ sync ClearTransparentWindow();
+ async __delete__();
+
+child:
+ async ObserveVsync();
+ async UnobserveVsync();
+ async UpdateCompositorWnd(WindowsHandle aCompositorWnd, WindowsHandle aParentWnd)
+ returns (bool success);
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/PlatformWidgetTypes.ipdlh b/widget/windows/PlatformWidgetTypes.ipdlh
new file mode 100644
index 0000000000..67dd92b802
--- /dev/null
+++ b/widget/windows/PlatformWidgetTypes.ipdlh
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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 "mozilla/dom/TabMessageUtils.h";
+include "mozilla/widget/WidgetMessageUtils.h";
+
+include HeadlessWidgetTypes;
+
+using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h";
+using nsSizeMode from "nsIWidgetListener.h";
+using mozilla::widget::TransparencyMode from "nsIWidget.h";
+
+namespace mozilla {
+namespace widget {
+
+struct WinCompositorWidgetInitData
+{
+ WindowsHandle hWnd;
+ uintptr_t widgetKey;
+ TransparencyMode transparencyMode;
+ nsSizeMode sizeMode;
+};
+
+union CompositorWidgetInitData
+{
+ WinCompositorWidgetInitData;
+ HeadlessCompositorWidgetInitData;
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/RemoteBackbuffer.cpp b/widget/windows/RemoteBackbuffer.cpp
new file mode 100644
index 0000000000..56a8bae0fe
--- /dev/null
+++ b/widget/windows/RemoteBackbuffer.cpp
@@ -0,0 +1,713 @@
+/* -*- 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 "RemoteBackbuffer.h"
+#include "GeckoProfiler.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Span.h"
+#include "mozilla/gfx/Point.h"
+#include "WinUtils.h"
+#include <algorithm>
+#include <type_traits>
+
+namespace mozilla {
+namespace widget {
+namespace remote_backbuffer {
+
+// This number can be adjusted as a time-memory tradeoff
+constexpr uint8_t kMaxDirtyRects = 8;
+
+struct IpcSafeRect {
+ explicit IpcSafeRect(const gfx::IntRect& aRect)
+ : x(aRect.x), y(aRect.y), width(aRect.width), height(aRect.height) {}
+ int32_t x;
+ int32_t y;
+ int32_t width;
+ int32_t height;
+};
+
+enum class ResponseResult {
+ Unknown,
+ Error,
+ BorrowSuccess,
+ BorrowSameBuffer,
+ PresentSuccess
+};
+
+enum class SharedDataType {
+ BorrowRequest,
+ BorrowRequestAllowSameBuffer,
+ BorrowResponse,
+ PresentRequest,
+ PresentResponse
+};
+
+struct BorrowResponseData {
+ ResponseResult result;
+ int32_t width;
+ int32_t height;
+ HANDLE fileMapping;
+};
+
+struct PresentRequestData {
+ uint8_t lenDirtyRects;
+ IpcSafeRect dirtyRects[kMaxDirtyRects];
+};
+
+struct PresentResponseData {
+ ResponseResult result;
+};
+
+struct SharedData {
+ SharedDataType dataType;
+ union {
+ BorrowResponseData borrowResponse;
+ PresentRequestData presentRequest;
+ PresentResponseData presentResponse;
+ } data;
+};
+
+static_assert(std::is_trivially_copyable<SharedData>::value &&
+ std::is_standard_layout<SharedData>::value,
+ "SharedData must be safe to pass over IPC boundaries");
+
+class SharedImage {
+ public:
+ SharedImage()
+ : mWidth(0), mHeight(0), mFileMapping(nullptr), mPixelData(nullptr) {}
+
+ ~SharedImage() {
+ if (mPixelData) {
+ MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mPixelData));
+ }
+
+ if (mFileMapping) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping));
+ }
+ }
+
+ bool Initialize(int32_t aWidth, int32_t aHeight) {
+ MOZ_ASSERT(aWidth > 0);
+ MOZ_ASSERT(aHeight > 0);
+
+ mWidth = aWidth;
+ mHeight = aHeight;
+
+ DWORD bufferSize = static_cast<DWORD>(mHeight * GetStride());
+
+ mFileMapping = ::CreateFileMappingW(
+ INVALID_HANDLE_VALUE, nullptr /*secattr*/, PAGE_READWRITE,
+ 0 /*sizeHigh*/, bufferSize, nullptr /*name*/);
+ if (!mFileMapping) {
+ return false;
+ }
+
+ void* mappedFilePtr =
+ ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/,
+ 0 /*offsetLow*/, 0 /*bytesToMap*/);
+ if (!mappedFilePtr) {
+ return false;
+ }
+
+ mPixelData = reinterpret_cast<unsigned char*>(mappedFilePtr);
+
+ return true;
+ }
+
+ bool InitializeRemote(int32_t aWidth, int32_t aHeight, HANDLE aFileMapping) {
+ MOZ_ASSERT(aWidth > 0);
+ MOZ_ASSERT(aHeight > 0);
+ MOZ_ASSERT(aFileMapping);
+
+ mWidth = aWidth;
+ mHeight = aHeight;
+ mFileMapping = aFileMapping;
+
+ void* mappedFilePtr =
+ ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/,
+ 0 /*offsetLow*/, 0 /*bytesToMap*/);
+ if (!mappedFilePtr) {
+ return false;
+ }
+
+ mPixelData = reinterpret_cast<unsigned char*>(mappedFilePtr);
+
+ return true;
+ }
+
+ HBITMAP CreateDIBSection() {
+ BITMAPINFO bitmapInfo = {};
+ bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader);
+ bitmapInfo.bmiHeader.biWidth = mWidth;
+ bitmapInfo.bmiHeader.biHeight = -mHeight;
+ bitmapInfo.bmiHeader.biPlanes = 1;
+ bitmapInfo.bmiHeader.biBitCount = 32;
+ bitmapInfo.bmiHeader.biCompression = BI_RGB;
+ void* dummy = nullptr;
+ return ::CreateDIBSection(nullptr /*paletteDC*/, &bitmapInfo,
+ DIB_RGB_COLORS, &dummy, mFileMapping,
+ 0 /*offset*/);
+ }
+
+ HANDLE CreateRemoteFileMapping(HANDLE aTargetProcess) {
+ MOZ_ASSERT(aTargetProcess);
+
+ HANDLE fileMapping = nullptr;
+ if (!::DuplicateHandle(GetCurrentProcess(), mFileMapping, aTargetProcess,
+ &fileMapping, 0 /*desiredAccess*/,
+ FALSE /*inheritHandle*/, DUPLICATE_SAME_ACCESS)) {
+ return nullptr;
+ }
+ return fileMapping;
+ }
+
+ already_AddRefed<gfx::DrawTarget> CreateDrawTarget() {
+ return gfx::Factory::CreateDrawTargetForData(
+ gfx::BackendType::CAIRO, mPixelData, gfx::IntSize(mWidth, mHeight),
+ GetStride(), gfx::SurfaceFormat::B8G8R8A8);
+ }
+
+ void CopyPixelsFrom(const SharedImage& other) {
+ const unsigned char* src = other.mPixelData;
+ unsigned char* dst = mPixelData;
+
+ int32_t width = std::min(mWidth, other.mWidth);
+ int32_t height = std::min(mHeight, other.mHeight);
+
+ for (int32_t row = 0; row < height; ++row) {
+ memcpy(dst, src, static_cast<uint32_t>(width * kBytesPerPixel));
+ src += other.GetStride();
+ dst += GetStride();
+ }
+ }
+
+ int32_t GetWidth() { return mWidth; }
+
+ int32_t GetHeight() { return mHeight; }
+
+ SharedImage(const SharedImage&) = delete;
+ SharedImage(SharedImage&&) = delete;
+ SharedImage& operator=(const SharedImage&) = delete;
+ SharedImage& operator=(SharedImage&&) = delete;
+
+ private:
+ static constexpr int32_t kBytesPerPixel = 4;
+
+ int32_t GetStride() const {
+ // DIB requires 32-bit row alignment
+ return (((mWidth * kBytesPerPixel) + 3) / 4) * 4;
+ }
+
+ int32_t mWidth;
+ int32_t mHeight;
+ HANDLE mFileMapping;
+ unsigned char* mPixelData;
+};
+
+class PresentableSharedImage {
+ public:
+ PresentableSharedImage()
+ : mSharedImage(),
+ mDeviceContext(nullptr),
+ mDIBSection(nullptr),
+ mSavedObject(nullptr) {}
+
+ ~PresentableSharedImage() {
+ if (mSavedObject) {
+ MOZ_ALWAYS_TRUE(::SelectObject(mDeviceContext, mSavedObject));
+ }
+
+ if (mDIBSection) {
+ MOZ_ALWAYS_TRUE(::DeleteObject(mDIBSection));
+ }
+
+ if (mDeviceContext) {
+ MOZ_ALWAYS_TRUE(::DeleteDC(mDeviceContext));
+ }
+ }
+
+ bool Initialize(int32_t aWidth, int32_t aHeight) {
+ if (!mSharedImage.Initialize(aWidth, aHeight)) {
+ return false;
+ }
+
+ mDeviceContext = ::CreateCompatibleDC(nullptr);
+ if (!mDeviceContext) {
+ return false;
+ }
+
+ mDIBSection = mSharedImage.CreateDIBSection();
+ if (!mDIBSection) {
+ return false;
+ }
+
+ mSavedObject = ::SelectObject(mDeviceContext, mDIBSection);
+ if (!mSavedObject) {
+ return false;
+ }
+
+ return true;
+ }
+
+ bool PresentToWindow(HWND aWindowHandle, TransparencyMode aTransparencyMode,
+ Span<const IpcSafeRect> aDirtyRects) {
+ if (aTransparencyMode == TransparencyMode::Transparent) {
+ // If our window is a child window or a child-of-a-child, the window
+ // that needs to be updated is the top level ancestor of the tree
+ HWND topLevelWindow = WinUtils::GetTopLevelHWND(aWindowHandle, true);
+ MOZ_ASSERT(::GetWindowLongPtr(topLevelWindow, GWL_EXSTYLE) &
+ WS_EX_LAYERED);
+
+ BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
+ POINT srcPos = {0, 0};
+ RECT clientRect = {};
+ if (!::GetClientRect(aWindowHandle, &clientRect)) {
+ return false;
+ }
+ MOZ_ASSERT(clientRect.left == 0);
+ MOZ_ASSERT(clientRect.top == 0);
+ int32_t width = clientRect.right;
+ int32_t height = clientRect.bottom;
+ SIZE winSize = {width, height};
+ // Window resize could cause the client area to be different than
+ // mSharedImage's size. If the client area doesn't match,
+ // PresentToWindow() returns false without calling UpdateLayeredWindow().
+ // Another call to UpdateLayeredWindow() will follow shortly, since the
+ // resize will eventually force the backbuffer to repaint itself again.
+ // When client area is larger than mSharedImage's size,
+ // UpdateLayeredWindow() draws the window completely invisible. But it
+ // does not return false.
+ if (width != mSharedImage.GetWidth() ||
+ height != mSharedImage.GetHeight()) {
+ return false;
+ }
+
+ return !!::UpdateLayeredWindow(
+ topLevelWindow, nullptr /*paletteDC*/, nullptr /*newPos*/, &winSize,
+ mDeviceContext, &srcPos, 0 /*colorKey*/, &bf, ULW_ALPHA);
+ }
+
+ gfx::IntRect sharedImageRect{0, 0, mSharedImage.GetWidth(),
+ mSharedImage.GetHeight()};
+
+ bool result = true;
+
+ HDC windowDC = ::GetDC(aWindowHandle);
+ if (!windowDC) {
+ return false;
+ }
+
+ for (auto& ipcDirtyRect : aDirtyRects) {
+ gfx::IntRect dirtyRect{ipcDirtyRect.x, ipcDirtyRect.y, ipcDirtyRect.width,
+ ipcDirtyRect.height};
+ gfx::IntRect bltRect = dirtyRect.Intersect(sharedImageRect);
+
+ if (!::BitBlt(windowDC, bltRect.x /*dstX*/, bltRect.y /*dstY*/,
+ bltRect.width, bltRect.height, mDeviceContext,
+ bltRect.x /*srcX*/, bltRect.y /*srcY*/, SRCCOPY)) {
+ result = false;
+ break;
+ }
+ }
+
+ MOZ_ALWAYS_TRUE(::ReleaseDC(aWindowHandle, windowDC));
+
+ return result;
+ }
+
+ HANDLE CreateRemoteFileMapping(HANDLE aTargetProcess) {
+ return mSharedImage.CreateRemoteFileMapping(aTargetProcess);
+ }
+
+ already_AddRefed<gfx::DrawTarget> CreateDrawTarget() {
+ return mSharedImage.CreateDrawTarget();
+ }
+
+ void CopyPixelsFrom(const PresentableSharedImage& other) {
+ mSharedImage.CopyPixelsFrom(other.mSharedImage);
+ }
+
+ int32_t GetWidth() { return mSharedImage.GetWidth(); }
+
+ int32_t GetHeight() { return mSharedImage.GetHeight(); }
+
+ PresentableSharedImage(const PresentableSharedImage&) = delete;
+ PresentableSharedImage(PresentableSharedImage&&) = delete;
+ PresentableSharedImage& operator=(const PresentableSharedImage&) = delete;
+ PresentableSharedImage& operator=(PresentableSharedImage&&) = delete;
+
+ private:
+ SharedImage mSharedImage;
+ HDC mDeviceContext;
+ HBITMAP mDIBSection;
+ HGDIOBJ mSavedObject;
+};
+
+Provider::Provider()
+ : mWindowHandle(nullptr),
+ mTargetProcess(nullptr),
+ mFileMapping(nullptr),
+ mRequestReadyEvent(nullptr),
+ mResponseReadyEvent(nullptr),
+ mSharedDataPtr(nullptr),
+ mStopServiceThread(false),
+ mServiceThread(nullptr),
+ mBackbuffer() {}
+
+Provider::~Provider() {
+ mBackbuffer.reset();
+
+ if (mServiceThread) {
+ mStopServiceThread = true;
+ MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent));
+ MOZ_ALWAYS_TRUE(PR_JoinThread(mServiceThread) == PR_SUCCESS);
+ }
+
+ if (mSharedDataPtr) {
+ MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mSharedDataPtr));
+ }
+
+ if (mResponseReadyEvent) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mResponseReadyEvent));
+ }
+
+ if (mRequestReadyEvent) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mRequestReadyEvent));
+ }
+
+ if (mFileMapping) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping));
+ }
+
+ if (mTargetProcess) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mTargetProcess));
+ }
+}
+
+bool Provider::Initialize(HWND aWindowHandle, DWORD aTargetProcessId,
+ TransparencyMode aTransparencyMode) {
+ MOZ_ASSERT(aWindowHandle);
+ MOZ_ASSERT(aTargetProcessId);
+
+ mWindowHandle = aWindowHandle;
+
+ mTargetProcess = ::OpenProcess(PROCESS_DUP_HANDLE, FALSE /*inheritHandle*/,
+ aTargetProcessId);
+ if (!mTargetProcess) {
+ return false;
+ }
+
+ mFileMapping = ::CreateFileMappingW(
+ INVALID_HANDLE_VALUE, nullptr /*secattr*/, PAGE_READWRITE, 0 /*sizeHigh*/,
+ static_cast<DWORD>(sizeof(SharedData)), nullptr /*name*/);
+ if (!mFileMapping) {
+ return false;
+ }
+
+ mRequestReadyEvent =
+ ::CreateEventW(nullptr /*secattr*/, FALSE /*manualReset*/,
+ FALSE /*initialState*/, nullptr /*name*/);
+ if (!mRequestReadyEvent) {
+ return false;
+ }
+
+ mResponseReadyEvent =
+ ::CreateEventW(nullptr /*secattr*/, FALSE /*manualReset*/,
+ FALSE /*initialState*/, nullptr /*name*/);
+ if (!mResponseReadyEvent) {
+ return false;
+ }
+
+ void* mappedFilePtr =
+ ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/,
+ 0 /*offsetLow*/, 0 /*bytesToMap*/);
+ if (!mappedFilePtr) {
+ return false;
+ }
+
+ mSharedDataPtr = reinterpret_cast<SharedData*>(mappedFilePtr);
+
+ mStopServiceThread = false;
+
+ // Use a raw NSPR OS-level thread here instead of nsThread because we are
+ // performing low-level synchronization across processes using Win32 Events,
+ // and nsThread is designed around an incompatible "in-process task queue"
+ // model
+ mServiceThread = PR_CreateThread(
+ PR_USER_THREAD, [](void* p) { static_cast<Provider*>(p)->ThreadMain(); },
+ this, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD,
+ 0 /*default stack size*/);
+ if (!mServiceThread) {
+ return false;
+ }
+
+ mTransparencyMode = uint32_t(aTransparencyMode);
+
+ return true;
+}
+
+Maybe<RemoteBackbufferHandles> Provider::CreateRemoteHandles() {
+ return Some(
+ RemoteBackbufferHandles(ipc::FileDescriptor(mFileMapping),
+ ipc::FileDescriptor(mRequestReadyEvent),
+ ipc::FileDescriptor(mResponseReadyEvent)));
+}
+
+void Provider::UpdateTransparencyMode(TransparencyMode aTransparencyMode) {
+ mTransparencyMode = uint32_t(aTransparencyMode);
+}
+
+void Provider::ThreadMain() {
+ AUTO_PROFILER_REGISTER_THREAD("RemoteBackbuffer");
+ NS_SetCurrentThreadName("RemoteBackbuffer");
+
+ while (true) {
+ {
+ AUTO_PROFILER_THREAD_SLEEP;
+ MOZ_ALWAYS_TRUE(::WaitForSingleObject(mRequestReadyEvent, INFINITE) ==
+ WAIT_OBJECT_0);
+ }
+
+ if (mStopServiceThread) {
+ break;
+ }
+
+ switch (mSharedDataPtr->dataType) {
+ case SharedDataType::BorrowRequest:
+ case SharedDataType::BorrowRequestAllowSameBuffer: {
+ BorrowResponseData responseData = {};
+
+ HandleBorrowRequest(&responseData,
+ mSharedDataPtr->dataType ==
+ SharedDataType::BorrowRequestAllowSameBuffer);
+
+ mSharedDataPtr->dataType = SharedDataType::BorrowResponse;
+ mSharedDataPtr->data.borrowResponse = responseData;
+
+ MOZ_ALWAYS_TRUE(::SetEvent(mResponseReadyEvent));
+
+ break;
+ }
+ case SharedDataType::PresentRequest: {
+ PresentRequestData requestData = mSharedDataPtr->data.presentRequest;
+ PresentResponseData responseData = {};
+
+ HandlePresentRequest(requestData, &responseData);
+
+ mSharedDataPtr->dataType = SharedDataType::PresentResponse;
+ mSharedDataPtr->data.presentResponse = responseData;
+
+ MOZ_ALWAYS_TRUE(::SetEvent(mResponseReadyEvent));
+
+ break;
+ }
+ default:
+ break;
+ };
+ }
+}
+
+void Provider::HandleBorrowRequest(BorrowResponseData* aResponseData,
+ bool aAllowSameBuffer) {
+ MOZ_ASSERT(aResponseData);
+
+ aResponseData->result = ResponseResult::Error;
+
+ RECT clientRect = {};
+ if (!::GetClientRect(mWindowHandle, &clientRect)) {
+ return;
+ }
+
+ MOZ_ASSERT(clientRect.left == 0);
+ MOZ_ASSERT(clientRect.top == 0);
+
+ int32_t width = clientRect.right ? clientRect.right : 1;
+ int32_t height = clientRect.bottom ? clientRect.bottom : 1;
+
+ bool needNewBackbuffer = !aAllowSameBuffer || !mBackbuffer ||
+ (mBackbuffer->GetWidth() != width) ||
+ (mBackbuffer->GetHeight() != height);
+
+ if (!needNewBackbuffer) {
+ aResponseData->result = ResponseResult::BorrowSameBuffer;
+ return;
+ }
+
+ auto newBackbuffer = std::make_unique<PresentableSharedImage>();
+ if (!newBackbuffer->Initialize(width, height)) {
+ return;
+ }
+
+ // Preserve the contents of the old backbuffer (if it exists)
+ if (mBackbuffer) {
+ newBackbuffer->CopyPixelsFrom(*mBackbuffer);
+ mBackbuffer.reset();
+ }
+
+ HANDLE remoteFileMapping =
+ newBackbuffer->CreateRemoteFileMapping(mTargetProcess);
+ if (!remoteFileMapping) {
+ return;
+ }
+
+ aResponseData->result = ResponseResult::BorrowSuccess;
+ aResponseData->width = width;
+ aResponseData->height = height;
+ aResponseData->fileMapping = remoteFileMapping;
+
+ mBackbuffer = std::move(newBackbuffer);
+}
+
+void Provider::HandlePresentRequest(const PresentRequestData& aRequestData,
+ PresentResponseData* aResponseData) {
+ MOZ_ASSERT(aResponseData);
+
+ Span rectSpan(aRequestData.dirtyRects, kMaxDirtyRects);
+
+ aResponseData->result = ResponseResult::Error;
+
+ if (!mBackbuffer) {
+ return;
+ }
+
+ if (!mBackbuffer->PresentToWindow(
+ mWindowHandle, GetTransparencyMode(),
+ rectSpan.First(aRequestData.lenDirtyRects))) {
+ return;
+ }
+
+ aResponseData->result = ResponseResult::PresentSuccess;
+}
+
+Client::Client()
+ : mFileMapping(nullptr),
+ mRequestReadyEvent(nullptr),
+ mResponseReadyEvent(nullptr),
+ mSharedDataPtr(nullptr),
+ mBackbuffer() {}
+
+Client::~Client() {
+ mBackbuffer.reset();
+
+ if (mSharedDataPtr) {
+ MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mSharedDataPtr));
+ }
+
+ if (mResponseReadyEvent) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mResponseReadyEvent));
+ }
+
+ if (mRequestReadyEvent) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mRequestReadyEvent));
+ }
+
+ if (mFileMapping) {
+ MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping));
+ }
+}
+
+bool Client::Initialize(const RemoteBackbufferHandles& aRemoteHandles) {
+ MOZ_ASSERT(aRemoteHandles.fileMapping().IsValid());
+ MOZ_ASSERT(aRemoteHandles.requestReadyEvent().IsValid());
+ MOZ_ASSERT(aRemoteHandles.responseReadyEvent().IsValid());
+
+ // FIXME: Due to PCompositorWidget using virtual Recv methods,
+ // RemoteBackbufferHandles is passed by const reference, and cannot have its
+ // signature customized, meaning that we need to clone the handles here.
+ //
+ // Once PCompositorWidget is migrated to use direct call semantics, the
+ // signature can be changed to accept RemoteBackbufferHandles by rvalue
+ // reference or value, and the DuplicateHandle calls here can be avoided.
+ mFileMapping = aRemoteHandles.fileMapping().ClonePlatformHandle().release();
+ mRequestReadyEvent =
+ aRemoteHandles.requestReadyEvent().ClonePlatformHandle().release();
+ mResponseReadyEvent =
+ aRemoteHandles.responseReadyEvent().ClonePlatformHandle().release();
+
+ void* mappedFilePtr =
+ ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/,
+ 0 /*offsetLow*/, 0 /*bytesToMap*/);
+ if (!mappedFilePtr) {
+ return false;
+ }
+
+ mSharedDataPtr = reinterpret_cast<SharedData*>(mappedFilePtr);
+
+ return true;
+}
+
+already_AddRefed<gfx::DrawTarget> Client::BorrowDrawTarget() {
+ mSharedDataPtr->dataType = mBackbuffer
+ ? SharedDataType::BorrowRequestAllowSameBuffer
+ : SharedDataType::BorrowRequest;
+
+ MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent));
+ MOZ_ALWAYS_TRUE(::WaitForSingleObject(mResponseReadyEvent, INFINITE) ==
+ WAIT_OBJECT_0);
+
+ if (mSharedDataPtr->dataType != SharedDataType::BorrowResponse) {
+ return nullptr;
+ }
+
+ BorrowResponseData responseData = mSharedDataPtr->data.borrowResponse;
+
+ if ((responseData.result != ResponseResult::BorrowSameBuffer) &&
+ (responseData.result != ResponseResult::BorrowSuccess)) {
+ return nullptr;
+ }
+
+ if (responseData.result == ResponseResult::BorrowSuccess) {
+ mBackbuffer.reset();
+
+ auto newBackbuffer = std::make_unique<SharedImage>();
+ if (!newBackbuffer->InitializeRemote(responseData.width,
+ responseData.height,
+ responseData.fileMapping)) {
+ return nullptr;
+ }
+
+ mBackbuffer = std::move(newBackbuffer);
+ }
+
+ MOZ_ASSERT(mBackbuffer);
+
+ return mBackbuffer->CreateDrawTarget();
+}
+
+bool Client::PresentDrawTarget(gfx::IntRegion aDirtyRegion) {
+ mSharedDataPtr->dataType = SharedDataType::PresentRequest;
+
+ // Simplify the region until it has <= kMaxDirtyRects
+ aDirtyRegion.SimplifyOutward(kMaxDirtyRects);
+
+ Span rectSpan(mSharedDataPtr->data.presentRequest.dirtyRects, kMaxDirtyRects);
+
+ uint8_t rectIndex = 0;
+ for (auto iter = aDirtyRegion.RectIter(); !iter.Done(); iter.Next()) {
+ rectSpan[rectIndex] = IpcSafeRect(iter.Get());
+ ++rectIndex;
+ }
+
+ mSharedDataPtr->data.presentRequest.lenDirtyRects = rectIndex;
+
+ MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent));
+ MOZ_ALWAYS_TRUE(::WaitForSingleObject(mResponseReadyEvent, INFINITE) ==
+ WAIT_OBJECT_0);
+
+ if (mSharedDataPtr->dataType != SharedDataType::PresentResponse) {
+ return false;
+ }
+
+ if (mSharedDataPtr->data.presentResponse.result !=
+ ResponseResult::PresentSuccess) {
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace remote_backbuffer
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/RemoteBackbuffer.h b/widget/windows/RemoteBackbuffer.h
new file mode 100644
index 0000000000..5899e80984
--- /dev/null
+++ b/widget/windows/RemoteBackbuffer.h
@@ -0,0 +1,95 @@
+/* -*- 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/. */
+
+#ifndef widget_windows_RemoteBackbuffer_h
+#define widget_windows_RemoteBackbuffer_h
+
+#include "nsIWidget.h"
+#include "mozilla/widget/PCompositorWidgetParent.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/gfx/2D.h"
+#include "prthread.h"
+#include <windows.h>
+
+namespace mozilla {
+namespace widget {
+namespace remote_backbuffer {
+
+struct IpcRect;
+struct SharedData;
+struct BorrowResponseData;
+struct PresentRequestData;
+struct PresentResponseData;
+class SharedImage;
+class PresentableSharedImage;
+
+class Provider {
+ public:
+ Provider();
+ ~Provider();
+
+ bool Initialize(HWND aWindowHandle, DWORD aTargetProcessId,
+ TransparencyMode aTransparencyMode);
+
+ Maybe<RemoteBackbufferHandles> CreateRemoteHandles();
+
+ void UpdateTransparencyMode(TransparencyMode aTransparencyMode);
+
+ Provider(const Provider&) = delete;
+ Provider(Provider&&) = delete;
+ Provider& operator=(const Provider&) = delete;
+ Provider& operator=(Provider&&) = delete;
+
+ private:
+ void ThreadMain();
+
+ void HandleBorrowRequest(BorrowResponseData* aResponseData,
+ bool aAllowSameBuffer);
+ void HandlePresentRequest(const PresentRequestData& aRequestData,
+ PresentResponseData* aResponseData);
+
+ HWND mWindowHandle;
+ HANDLE mTargetProcess;
+ HANDLE mFileMapping;
+ HANDLE mRequestReadyEvent;
+ HANDLE mResponseReadyEvent;
+ SharedData* mSharedDataPtr;
+ bool mStopServiceThread;
+ PRThread* mServiceThread;
+ std::unique_ptr<PresentableSharedImage> mBackbuffer;
+ mozilla::Atomic<uint32_t, MemoryOrdering::Relaxed> mTransparencyMode;
+ TransparencyMode GetTransparencyMode() const {
+ return TransparencyMode(uint32_t(mTransparencyMode));
+ }
+};
+
+class Client {
+ public:
+ Client();
+ ~Client();
+
+ bool Initialize(const RemoteBackbufferHandles& aRemoteHandles);
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget();
+ bool PresentDrawTarget(gfx::IntRegion aDirtyRegion);
+
+ Client(const Client&) = delete;
+ Client(Client&&) = delete;
+ Client& operator=(const Client&) = delete;
+ Client& operator=(Client&&) = delete;
+
+ private:
+ HANDLE mFileMapping;
+ HANDLE mRequestReadyEvent;
+ HANDLE mResponseReadyEvent;
+ SharedData* mSharedDataPtr;
+ std::unique_ptr<SharedImage> mBackbuffer;
+};
+
+} // namespace remote_backbuffer
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_windows_RemoteBackbuffer_h
diff --git a/widget/windows/ScreenHelperWin.cpp b/widget/windows/ScreenHelperWin.cpp
new file mode 100644
index 0000000000..8a0ec3b608
--- /dev/null
+++ b/widget/windows/ScreenHelperWin.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "ScreenHelperWin.h"
+
+#include "mozilla/Logging.h"
+#include "nsTArray.h"
+#include "WinUtils.h"
+
+static mozilla::LazyLogModule sScreenLog("WidgetScreen");
+
+namespace mozilla {
+namespace widget {
+
+static void GetDisplayInfo(const char16ptr_t aName,
+ hal::ScreenOrientation& aOrientation,
+ uint16_t& aAngle, bool& aIsPseudoDisplay,
+ uint32_t& aRefreshRate) {
+ DISPLAY_DEVICEW displayDevice = {.cb = sizeof(DISPLAY_DEVICEW)};
+
+ // XXX Is the pseudodisplay status really useful?
+ aIsPseudoDisplay =
+ EnumDisplayDevicesW(aName, 0, &displayDevice, 0) &&
+ (displayDevice.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER);
+
+ DEVMODEW mode = {.dmSize = sizeof(DEVMODEW)};
+ if (!EnumDisplaySettingsW(aName, ENUM_CURRENT_SETTINGS, &mode)) {
+ return;
+ }
+ MOZ_ASSERT(mode.dmFields & DM_DISPLAYORIENTATION);
+
+ aRefreshRate = mode.dmDisplayFrequency;
+
+ // conver to default/natural size
+ if (mode.dmDisplayOrientation == DMDO_90 ||
+ mode.dmDisplayOrientation == DMDO_270) {
+ DWORD temp = mode.dmPelsHeight;
+ mode.dmPelsHeight = mode.dmPelsWidth;
+ mode.dmPelsWidth = temp;
+ }
+
+ bool defaultIsLandscape = mode.dmPelsWidth >= mode.dmPelsHeight;
+ switch (mode.dmDisplayOrientation) {
+ case DMDO_DEFAULT:
+ aOrientation = defaultIsLandscape
+ ? hal::ScreenOrientation::LandscapePrimary
+ : hal::ScreenOrientation::PortraitPrimary;
+ aAngle = 0;
+ break;
+ case DMDO_90:
+ aOrientation = defaultIsLandscape
+ ? hal::ScreenOrientation::PortraitPrimary
+ : hal::ScreenOrientation::LandscapeSecondary;
+ aAngle = 270;
+ break;
+ case DMDO_180:
+ aOrientation = defaultIsLandscape
+ ? hal::ScreenOrientation::LandscapeSecondary
+ : hal::ScreenOrientation::PortraitSecondary;
+ aAngle = 180;
+ break;
+ case DMDO_270:
+ aOrientation = defaultIsLandscape
+ ? hal::ScreenOrientation::PortraitSecondary
+ : hal::ScreenOrientation::LandscapePrimary;
+ aAngle = 90;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected angle");
+ break;
+ }
+}
+
+BOOL CALLBACK CollectMonitors(HMONITOR aMon, HDC, LPRECT, LPARAM ioParam) {
+ auto screens = reinterpret_cast<nsTArray<RefPtr<Screen>>*>(ioParam);
+ BOOL success = FALSE;
+ MONITORINFOEX info;
+ info.cbSize = sizeof(MONITORINFOEX);
+ success = ::GetMonitorInfoW(aMon, &info);
+ if (!success) {
+ MOZ_LOG(sScreenLog, LogLevel::Error, ("GetMonitorInfoW failed"));
+ return TRUE; // continue the enumeration
+ }
+
+ double scale = WinUtils::LogToPhysFactor(aMon);
+ DesktopToLayoutDeviceScale contentsScaleFactor;
+ if (WinUtils::IsPerMonitorDPIAware()) {
+ contentsScaleFactor.scale = 1.0;
+ } else {
+ contentsScaleFactor.scale = scale;
+ }
+ CSSToLayoutDeviceScale defaultCssScaleFactor(scale);
+ LayoutDeviceIntRect rect(info.rcMonitor.left, info.rcMonitor.top,
+ info.rcMonitor.right - info.rcMonitor.left,
+ info.rcMonitor.bottom - info.rcMonitor.top);
+ LayoutDeviceIntRect availRect(info.rcWork.left, info.rcWork.top,
+ info.rcWork.right - info.rcWork.left,
+ info.rcWork.bottom - info.rcWork.top);
+
+ HDC hDC = CreateDC(nullptr, info.szDevice, nullptr, nullptr);
+ if (!hDC) {
+ MOZ_LOG(sScreenLog, LogLevel::Error, ("CollectMonitors CreateDC failed"));
+ return TRUE;
+ }
+ uint32_t pixelDepth = ::GetDeviceCaps(hDC, BITSPIXEL);
+ DeleteDC(hDC);
+ if (pixelDepth == 32) {
+ // If a device uses 32 bits per pixel, it's still only using 8 bits
+ // per color component, which is what our callers want to know.
+ // (Some devices report 32 and some devices report 24.)
+ pixelDepth = 24;
+ }
+
+ float dpi = WinUtils::MonitorDPI(aMon);
+
+ auto orientation = hal::ScreenOrientation::None;
+ uint16_t angle = 0;
+ bool isPseudoDisplay = false;
+ uint32_t refreshRate = 0;
+ GetDisplayInfo(info.szDevice, orientation, angle, isPseudoDisplay,
+ refreshRate);
+
+ MOZ_LOG(sScreenLog, LogLevel::Debug,
+ ("New screen [%s (%s) %d %u %f %f %f %d %d %d]",
+ ToString(rect).c_str(), ToString(availRect).c_str(), pixelDepth,
+ refreshRate, contentsScaleFactor.scale, defaultCssScaleFactor.scale,
+ dpi, isPseudoDisplay, uint32_t(orientation), angle));
+ auto screen = MakeRefPtr<Screen>(
+ rect, availRect, pixelDepth, pixelDepth, refreshRate, contentsScaleFactor,
+ defaultCssScaleFactor, dpi, Screen::IsPseudoDisplay(isPseudoDisplay),
+ orientation, angle);
+ if (info.dwFlags & MONITORINFOF_PRIMARY) {
+ // The primary monitor must be the first element of the screen list.
+ screens->InsertElementAt(0, std::move(screen));
+ } else {
+ screens->AppendElement(std::move(screen));
+ }
+ return TRUE;
+}
+
+void ScreenHelperWin::RefreshScreens() {
+ MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refreshing screens"));
+
+ AutoTArray<RefPtr<Screen>, 4> screens;
+ BOOL result = ::EnumDisplayMonitors(
+ nullptr, nullptr, (MONITORENUMPROC)CollectMonitors, (LPARAM)&screens);
+ if (!result) {
+ NS_WARNING("Unable to EnumDisplayMonitors");
+ }
+ ScreenManager::Refresh(std::move(screens));
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/ScreenHelperWin.h b/widget/windows/ScreenHelperWin.h
new file mode 100644
index 0000000000..275014232a
--- /dev/null
+++ b/widget/windows/ScreenHelperWin.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_windows_ScreenHelperWin_h
+#define mozilla_widget_windows_ScreenHelperWin_h
+
+#include "mozilla/widget/ScreenManager.h"
+
+namespace mozilla {
+namespace widget {
+
+class ScreenHelperWin final : public ScreenManager::Helper {
+ public:
+ ScreenHelperWin(){};
+ ~ScreenHelperWin() override {}
+
+ static void RefreshScreens();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_windows_ScreenHelperWin_h
diff --git a/widget/windows/ShellHeaderOnlyUtils.h b/widget/windows/ShellHeaderOnlyUtils.h
new file mode 100644
index 0000000000..ea95077fb3
--- /dev/null
+++ b/widget/windows/ShellHeaderOnlyUtils.h
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_ShellHeaderOnlyUtils_h
+#define mozilla_ShellHeaderOnlyUtils_h
+
+#if defined(LIBXUL) && !defined(UNICODE)
+# error \
+ "UNICODE not set - must be set to prevent compile failure in `comdef.h` due to us deleting `FormatMessage` when absent."
+#endif
+
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+#include <objbase.h>
+
+#include <exdisp.h>
+#include <shldisp.h>
+#include <shlobj.h>
+#include <shlwapi.h>
+#include <shobjidl.h>
+#include <shtypes.h>
+// NB: include this after shldisp.h so its macros do not conflict with COM
+// interfaces defined by shldisp.h
+#include <shellapi.h>
+#include <type_traits>
+
+#include <comdef.h>
+#include <comutil.h>
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+/**
+ * Ask the current user's Desktop to ShellExecute on our behalf, thus causing
+ * the resulting launched process to inherit its security priviliges from
+ * Explorer instead of our process.
+ *
+ * This is useful in two scenarios, in particular:
+ * * We are running as an elevated user and we want to start something as the
+ * "normal" user;
+ * * We are starting a process that is incompatible with our process's
+ * process mitigation policies. By delegating to Explorer, the child process
+ * will not be affected by our process mitigations.
+ *
+ * Since this communication happens over DCOM, Explorer's COM DACL governs
+ * whether or not we can execute against it, thus avoiding privilege escalation.
+ */
+inline LauncherVoidResult ShellExecuteByExplorer(const _bstr_t& aPath,
+ const _variant_t& aArgs,
+ const _variant_t& aVerb,
+ const _variant_t& aWorkingDir,
+ const _variant_t& aShowCmd) {
+ // NB: Explorer may be a local server, not an inproc server
+ RefPtr<IShellWindows> shellWindows;
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_ShellWindows, nullptr, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER,
+ IID_IShellWindows, getter_AddRefs(shellWindows));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // 1. Find the shell view for the desktop.
+ _variant_t loc(int(CSIDL_DESKTOP));
+ _variant_t empty;
+ long hwnd;
+ RefPtr<IDispatch> dispDesktop;
+ hr = shellWindows->FindWindowSW(&loc, &empty, SWC_DESKTOP, &hwnd,
+ SWFO_NEEDDISPATCH,
+ getter_AddRefs(dispDesktop));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ if (hr == S_FALSE) {
+ // The call succeeded but the window was not found.
+ return LAUNCHER_ERROR_FROM_WIN32(ERROR_NOT_FOUND);
+ }
+
+ RefPtr<IServiceProvider> servProv;
+ hr = dispDesktop->QueryInterface(IID_IServiceProvider,
+ getter_AddRefs(servProv));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ RefPtr<IShellBrowser> browser;
+ hr = servProv->QueryService(SID_STopLevelBrowser, IID_IShellBrowser,
+ getter_AddRefs(browser));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ RefPtr<IShellView> activeShellView;
+ hr = browser->QueryActiveShellView(getter_AddRefs(activeShellView));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // 2. Get the automation object for the desktop.
+ RefPtr<IDispatch> dispView;
+ hr = activeShellView->GetItemObject(SVGIO_BACKGROUND, IID_IDispatch,
+ getter_AddRefs(dispView));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ RefPtr<IShellFolderViewDual> folderView;
+ hr = dispView->QueryInterface(IID_IShellFolderViewDual,
+ getter_AddRefs(folderView));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // 3. Get the interface to IShellDispatch2
+ RefPtr<IDispatch> dispShell;
+ hr = folderView->get_Application(getter_AddRefs(dispShell));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ RefPtr<IShellDispatch2> shellDisp;
+ hr =
+ dispShell->QueryInterface(IID_IShellDispatch2, getter_AddRefs(shellDisp));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // Passing the foreground privilege so that the shell can launch an
+ // application in the foreground. This fails with E_ACCESSDENIED if the
+ // current window is shown in the background. We keep a soft assert for
+ // the other failures to investigate how it happened.
+ hr = ::CoAllowSetForegroundWindow(shellDisp, nullptr);
+ MOZ_ASSERT(SUCCEEDED(hr) || hr == E_ACCESSDENIED);
+
+ // shellapi.h macros interfere with the correct naming of the method being
+ // called on IShellDispatch2. Temporarily remove that definition.
+#if defined(ShellExecute)
+# define MOZ_REDEFINE_SHELLEXECUTE
+# undef ShellExecute
+#endif // defined(ShellExecute)
+
+ // 4. Now call IShellDispatch2::ShellExecute to ask Explorer to execute.
+ hr = shellDisp->ShellExecute(aPath, aArgs, aWorkingDir, aVerb, aShowCmd);
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // Restore the macro that was removed prior to IShellDispatch2::ShellExecute
+#if defined(MOZ_REDEFINE_SHELLEXECUTE)
+# if defined(UNICODE)
+# define ShellExecute ShellExecuteW
+# else
+# define ShellExecute ShellExecuteA
+# endif
+# undef MOZ_REDEFINE_SHELLEXECUTE
+#endif // defined(MOZ_REDEFINE_SHELLEXECUTE)
+
+ return Ok();
+}
+
+using UniqueAbsolutePidl =
+ UniquePtr<std::remove_pointer_t<PIDLIST_ABSOLUTE>, CoTaskMemFreeDeleter>;
+
+inline LauncherResult<UniqueAbsolutePidl> ShellParseDisplayName(
+ const wchar_t* aPath) {
+ PIDLIST_ABSOLUTE rawAbsPidl = nullptr;
+ SFGAOF sfgao;
+ HRESULT hr = ::SHParseDisplayName(aPath, nullptr, &rawAbsPidl, 0, &sfgao);
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ return UniqueAbsolutePidl(rawAbsPidl);
+}
+
+} // namespace mozilla
+
+#endif // mozilla_ShellHeaderOnlyUtils_h
diff --git a/widget/windows/SystemStatusBar.cpp b/widget/windows/SystemStatusBar.cpp
new file mode 100644
index 0000000000..6cc5668bbe
--- /dev/null
+++ b/widget/windows/SystemStatusBar.cpp
@@ -0,0 +1,339 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 <strsafe.h>
+#include "SystemStatusBar.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/widget/IconLoader.h"
+#include "mozilla/dom/XULButtonElement.h"
+#include "nsComputedDOMStyle.h"
+#include "nsIContentPolicy.h"
+#include "nsISupports.h"
+#include "nsMenuPopupFrame.h"
+#include "nsXULPopupManager.h"
+#include "nsIDocShell.h"
+#include "nsDocShell.h"
+#include "nsWindowGfx.h"
+
+#include "shellapi.h"
+
+namespace mozilla::widget {
+
+using mozilla::LinkedListElement;
+using mozilla::dom::Element;
+
+class StatusBarEntry final : public LinkedListElement<RefPtr<StatusBarEntry>>,
+ public IconLoader::Listener,
+ public nsISupports {
+ public:
+ explicit StatusBarEntry(Element* aMenu);
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(StatusBarEntry)
+ nsresult Init();
+ void Destroy();
+
+ MOZ_CAN_RUN_SCRIPT LRESULT OnMessage(HWND hWnd, UINT msg, WPARAM wp,
+ LPARAM lp);
+ const Element* GetMenu() { return mMenu; };
+
+ nsresult OnComplete(imgIContainer* aImage) override;
+
+ private:
+ ~StatusBarEntry();
+ RefPtr<mozilla::widget::IconLoader> mIconLoader;
+ // Effectively const but is cycle collected
+ MOZ_KNOWN_LIVE RefPtr<Element> mMenu;
+ NOTIFYICONDATAW mIconData;
+ boolean mInitted;
+};
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(StatusBarEntry)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(StatusBarEntry)
+ tmp->Destroy();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(StatusBarEntry)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIconLoader)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMenu)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StatusBarEntry)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(StatusBarEntry)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(StatusBarEntry)
+
+StatusBarEntry::StatusBarEntry(Element* aMenu) : mMenu(aMenu), mInitted(false) {
+ mIconData = {/* cbSize */ sizeof(NOTIFYICONDATA),
+ /* hWnd */ 0,
+ /* uID */ 2,
+ /* uFlags */ NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP,
+ /* uCallbackMessage */ WM_USER,
+ /* hIcon */ 0,
+ /* szTip */ L"", // This is updated in Init()
+ /* dwState */ 0,
+ /* dwStateMask */ 0,
+ /* szInfo */ L"",
+ /* uVersion */ {NOTIFYICON_VERSION_4},
+ /* szInfoTitle */ L"",
+ /* dwInfoFlags */ 0};
+ MOZ_ASSERT(mMenu);
+}
+
+StatusBarEntry::~StatusBarEntry() {
+ if (!mInitted) {
+ return;
+ }
+ Destroy();
+ ::Shell_NotifyIconW(NIM_DELETE, &mIconData);
+ VERIFY(::DestroyWindow(mIconData.hWnd));
+}
+
+void StatusBarEntry::Destroy() {
+ if (mIconLoader) {
+ mIconLoader->Destroy();
+ mIconLoader = nullptr;
+ }
+}
+
+nsresult StatusBarEntry::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // First, look at the content node's "image" attribute.
+ nsAutoString imageURIString;
+ bool hasImageAttr = mMenu->GetAttr(nsGkAtoms::image, imageURIString);
+
+ nsresult rv;
+ nsCOMPtr<nsIURI> iconURI;
+ if (!hasImageAttr) {
+ // If the content node has no "image" attribute, get the
+ // "list-style-image" property from CSS.
+ RefPtr<mozilla::dom::Document> document = mMenu->GetComposedDoc();
+ if (!document) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<const ComputedStyle> sc =
+ nsComputedDOMStyle::GetComputedStyle(mMenu);
+ if (!sc) {
+ return NS_ERROR_FAILURE;
+ }
+
+ iconURI = sc->StyleList()->GetListStyleImageURI();
+ } else {
+ uint64_t dummy = 0;
+ nsContentPolicyType policyType;
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal = mMenu->NodePrincipal();
+ nsContentUtils::GetContentPolicyTypeForUIImageLoading(
+ mMenu, getter_AddRefs(triggeringPrincipal), policyType, &dummy);
+ if (policyType != nsIContentPolicy::TYPE_INTERNAL_IMAGE) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // If this menu item shouldn't have an icon, the string will be empty,
+ // and NS_NewURI will fail.
+ rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ mIconLoader = new IconLoader(this);
+
+ if (iconURI) {
+ rv = mIconLoader->LoadIcon(iconURI, mMenu);
+ }
+
+ HWND iconWindow;
+ NS_ENSURE_TRUE(iconWindow = ::CreateWindowExW(
+ /* extended style */ 0,
+ /* className */ L"IconWindowClass",
+ /* title */ 0,
+ /* style */ WS_CAPTION,
+ /* x, y, cx, cy */ 0, 0, 0, 0,
+ /* parent */ 0,
+ /* menu */ 0,
+ /* instance */ 0,
+ /* create struct */ 0),
+ NS_ERROR_FAILURE);
+ ::SetWindowLongPtr(iconWindow, GWLP_USERDATA, (LONG_PTR)this);
+
+ mIconData.hWnd = iconWindow;
+ mIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), IDI_APPLICATION);
+
+ nsAutoString labelAttr;
+ mMenu->GetAttr(nsGkAtoms::label, labelAttr);
+ const nsString& label = PromiseFlatString(labelAttr);
+
+ size_t destLength = sizeof mIconData.szTip / (sizeof mIconData.szTip[0]);
+ wchar_t* tooltip = &(mIconData.szTip[0]);
+ ::StringCchCopyNW(tooltip, destLength, label.get(), label.Length());
+
+ ::Shell_NotifyIconW(NIM_ADD, &mIconData);
+ ::Shell_NotifyIconW(NIM_SETVERSION, &mIconData);
+
+ mInitted = true;
+ return NS_OK;
+}
+
+nsresult StatusBarEntry::OnComplete(imgIContainer* aImage) {
+ NS_ENSURE_ARG_POINTER(aImage);
+
+ RefPtr<StatusBarEntry> kungFuDeathGrip = this;
+
+ nsresult rv = nsWindowGfx::CreateIcon(
+ aImage, false, LayoutDeviceIntPoint(),
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon), &mIconData.hIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ::Shell_NotifyIconW(NIM_MODIFY, &mIconData);
+
+ if (mIconData.hIcon) {
+ ::DestroyIcon(mIconData.hIcon);
+ mIconData.hIcon = nullptr;
+ }
+
+ // To simplify things, we won't react to CSS changes to update the icon
+ // with this implementation. We can get rid of the IconLoader at this point.
+ mIconLoader->Destroy();
+ mIconLoader = nullptr;
+ return NS_OK;
+}
+
+LRESULT StatusBarEntry::OnMessage(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
+ if (msg == WM_USER &&
+ (LOWORD(lp) == NIN_SELECT || LOWORD(lp) == NIN_KEYSELECT ||
+ LOWORD(lp) == WM_CONTEXTMENU)) {
+ auto* menu = dom::XULButtonElement::FromNode(mMenu);
+ if (!menu) {
+ return TRUE;
+ }
+
+ nsMenuPopupFrame* popupFrame = menu->GetMenuPopup(FlushType::None);
+ if (NS_WARN_IF(!popupFrame)) {
+ return TRUE;
+ }
+
+ nsIWidget* widget = popupFrame->GetNearestWidget();
+ MOZ_DIAGNOSTIC_ASSERT(widget);
+ if (!widget) {
+ return TRUE;
+ }
+
+ HWND win = static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW));
+ MOZ_DIAGNOSTIC_ASSERT(win);
+ if (!win) {
+ return TRUE;
+ }
+
+ if (LOWORD(lp) == NIN_KEYSELECT && ::GetForegroundWindow() == win) {
+ // When enter is pressed on the icon, the shell sends two NIN_KEYSELECT
+ // notifications. This might cause us to open two windows. To work around
+ // this, if we're already the foreground window (which happens below),
+ // ignore this notification.
+ return TRUE;
+ }
+
+ if (LOWORD(lp) != WM_CONTEXTMENU &&
+ mMenu->HasAttr(nsGkAtoms::contextmenu)) {
+ ::SetForegroundWindow(win);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULSystemStatusBarClick, nullptr,
+ WidgetMouseEvent::eReal);
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ EventDispatcher::Dispatch(mMenu, presContext, &event, nullptr, &status);
+ return DefWindowProc(hWnd, msg, wp, lp);
+ }
+
+ nsPresContext* pc = popupFrame->PresContext();
+ const CSSIntPoint point = gfx::RoundedToInt(
+ LayoutDeviceIntPoint(GET_X_LPARAM(wp), GET_Y_LPARAM(wp)) /
+ pc->CSSToDevPixelScale());
+
+ // The menu that is being opened is a Gecko <xul:menu>, and the popup code
+ // that manages it expects that the window that the <xul:menu> belongs to
+ // will be in the foreground when it opens. If we don't do this, then if the
+ // icon is clicked when the window is _not_ in the foreground, then the
+ // opened menu will not be keyboard focusable, nor will it close on its own
+ // if the user clicks away from the menu (at least, not until the user
+ // focuses any window in the parent process).
+ ::SetForegroundWindow(win);
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ pm->ShowPopupAtScreen(popupFrame->GetContent()->AsElement(), point.x,
+ point.y, false, nullptr);
+ }
+
+ return DefWindowProc(hWnd, msg, wp, lp);
+}
+
+NS_IMPL_ISUPPORTS(SystemStatusBar, nsISystemStatusBar)
+
+MOZ_CAN_RUN_SCRIPT static LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg,
+ WPARAM wp, LPARAM lp) {
+ if (RefPtr<StatusBarEntry> entry =
+ (StatusBarEntry*)GetWindowLongPtr(hWnd, GWLP_USERDATA)) {
+ return entry->OnMessage(hWnd, msg, wp, lp);
+ }
+ return TRUE;
+}
+
+static StaticRefPtr<SystemStatusBar> sSingleton;
+
+SystemStatusBar& SystemStatusBar::GetSingleton() {
+ if (!sSingleton) {
+ sSingleton = new SystemStatusBar();
+ ClearOnShutdown(&sSingleton);
+ }
+ return *sSingleton;
+}
+
+already_AddRefed<SystemStatusBar> SystemStatusBar::GetAddRefedSingleton() {
+ RefPtr<SystemStatusBar> sm = &GetSingleton();
+ return sm.forget();
+}
+
+nsresult SystemStatusBar::Init() {
+ WNDCLASS classStruct = {/* style */ 0,
+ /* lpfnWndProc */ &WindowProc,
+ /* cbClsExtra */ 0,
+ /* cbWndExtra */ 0,
+ /* hInstance */ 0,
+ /* hIcon */ 0,
+ /* hCursor */ 0,
+ /* hbrBackground */ 0,
+ /* lpszMenuName */ 0,
+ /* lpszClassName */ L"IconWindowClass"};
+ NS_ENSURE_TRUE(::RegisterClass(&classStruct), NS_ERROR_FAILURE);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SystemStatusBar::AddItem(Element* aElement) {
+ RefPtr<StatusBarEntry> entry = new StatusBarEntry(aElement);
+ nsresult rv = entry->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mStatusBarEntries.insertBack(entry);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SystemStatusBar::RemoveItem(Element* aElement) {
+ for (StatusBarEntry* entry : mStatusBarEntries) {
+ if (entry->GetMenu() == aElement) {
+ entry->removeFrom(mStatusBarEntries);
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/windows/SystemStatusBar.h b/widget/windows/SystemStatusBar.h
new file mode 100644
index 0000000000..6afafd9d80
--- /dev/null
+++ b/widget/windows/SystemStatusBar.h
@@ -0,0 +1,33 @@
+/* -*- 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/. */
+
+#ifndef widget_windows_SystemStatusBar_h
+#define widget_windows_SystemStatusBar_h
+
+#include "nsISystemStatusBar.h"
+#include "mozilla/LinkedList.h"
+
+namespace mozilla::widget {
+class StatusBarEntry;
+
+class SystemStatusBar final : public nsISystemStatusBar {
+ public:
+ explicit SystemStatusBar() = default;
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISYSTEMSTATUSBAR
+
+ static SystemStatusBar& GetSingleton();
+ static already_AddRefed<SystemStatusBar> GetAddRefedSingleton();
+
+ nsresult Init();
+
+ private:
+ ~SystemStatusBar() = default;
+ mozilla::LinkedList<RefPtr<StatusBarEntry>> mStatusBarEntries;
+};
+
+} // namespace mozilla::widget
+
+#endif // widget_windows_SystemStatusBar_h
diff --git a/widget/windows/TSFTextStore.cpp b/widget/windows/TSFTextStore.cpp
new file mode 100644
index 0000000000..b3e3a164ae
--- /dev/null
+++ b/widget/windows/TSFTextStore.cpp
@@ -0,0 +1,7513 @@
+/* -*- 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/. */
+
+#define INPUTSCOPE_INIT_GUID
+#define TEXTATTRS_INIT_GUID
+#include "TSFTextStore.h"
+
+#include <algorithm>
+#include <comutil.h> // for _bstr_t
+#include <oleauto.h> // for SysAllocString
+#include <olectl.h>
+#include "nscore.h"
+
+#include "IMMHandler.h"
+#include "KeyboardLayout.h"
+#include "WinIMEHandler.h"
+#include "WinUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/ToString.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/widget/WinRegistry.h"
+#include "nsWindow.h"
+#include "nsPrintfCString.h"
+
+// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync`
+// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
+// big file.
+// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
+mozilla::LazyLogModule gIMELog("IMEHandler");
+
+// TODO: GUID_PROP_URL has not been declared in the SDK yet. We should drop the
+// `s` prefix after it's released by a new SDK and define it with #if.
+static const GUID sGUID_PROP_URL = {
+ 0xd5138268,
+ 0xa1bf,
+ 0x4308,
+ {0xbc, 0xbf, 0x2e, 0x73, 0x93, 0x98, 0xe2, 0x34}};
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * TSF related code should log its behavior even on release build especially
+ * in the interface methods.
+ *
+ * In interface methods, use LogLevel::Info.
+ * In internal methods, use LogLevel::Debug for logging normal behavior.
+ * For logging error, use LogLevel::Error.
+ *
+ * When an instance method is called, start with following text:
+ * "0x%p TSFFoo::Bar(", the 0x%p should be the "this" of the nsFoo.
+ * after that, start with:
+ * "0x%p TSFFoo::Bar("
+ * In an internal method, start with following text:
+ * "0x%p TSFFoo::Bar("
+ * When a static method is called, start with following text:
+ * "TSFFoo::Bar("
+ */
+
+enum class TextInputProcessorID {
+ // Internal use only. This won't be returned by TSFStaticSink::ActiveTIP().
+ eNotComputed,
+
+ // Not a TIP. E.g., simple keyboard layout or IMM-IME.
+ eNone,
+
+ // Used for other TIPs, i.e., any TIPs which we don't support specifically.
+ eUnknown,
+
+ // TIP for Japanese.
+ eMicrosoftIMEForJapanese,
+ eMicrosoftOfficeIME2010ForJapanese,
+ eGoogleJapaneseInput,
+ eATOK2011,
+ eATOK2012,
+ eATOK2013,
+ eATOK2014,
+ eATOK2015,
+ eATOK2016,
+ eATOKUnknown,
+ eJapanist10,
+
+ // TIP for Traditional Chinese.
+ eMicrosoftBopomofo,
+ eMicrosoftChangJie,
+ eMicrosoftPhonetic,
+ eMicrosoftQuick,
+ eMicrosoftNewChangJie,
+ eMicrosoftNewPhonetic,
+ eMicrosoftNewQuick,
+ eFreeChangJie,
+
+ // TIP for Simplified Chinese.
+ eMicrosoftPinyin,
+ eMicrosoftPinyinNewExperienceInputStyle,
+ eMicrosoftWubi,
+
+ // TIP for Korean.
+ eMicrosoftIMEForKorean,
+ eMicrosoftOldHangul,
+
+ // Keyman Desktop, which can install various language keyboards.
+ eKeymanDesktop,
+};
+
+static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; }
+
+static void HandleSeparator(nsCString& aDesc) {
+ if (!aDesc.IsEmpty()) {
+ aDesc.AppendLiteral(" | ");
+ }
+}
+
+static const nsCString GetFindFlagName(DWORD aFindFlag) {
+ nsCString description;
+ if (!aFindFlag) {
+ description.AppendLiteral("no flags (0)");
+ return description;
+ }
+ if (aFindFlag & TS_ATTR_FIND_BACKWARDS) {
+ description.AppendLiteral("TS_ATTR_FIND_BACKWARDS");
+ }
+ if (aFindFlag & TS_ATTR_FIND_WANT_OFFSET) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_ATTR_FIND_WANT_OFFSET");
+ }
+ if (aFindFlag & TS_ATTR_FIND_UPDATESTART) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_ATTR_FIND_UPDATESTART");
+ }
+ if (aFindFlag & TS_ATTR_FIND_WANT_VALUE) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_ATTR_FIND_WANT_VALUE");
+ }
+ if (aFindFlag & TS_ATTR_FIND_WANT_END) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_ATTR_FIND_WANT_END");
+ }
+ if (aFindFlag & TS_ATTR_FIND_HIDDEN) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_ATTR_FIND_HIDDEN");
+ }
+ if (description.IsEmpty()) {
+ description.AppendLiteral("Unknown (");
+ description.AppendInt(static_cast<uint32_t>(aFindFlag));
+ description.Append(')');
+ }
+ return description;
+}
+
+class GetACPFromPointFlagName : public nsAutoCString {
+ public:
+ explicit GetACPFromPointFlagName(DWORD aFlags) {
+ if (!aFlags) {
+ AppendLiteral("no flags (0)");
+ return;
+ }
+ if (aFlags & GXFPF_ROUND_NEAREST) {
+ AppendLiteral("GXFPF_ROUND_NEAREST");
+ aFlags &= ~GXFPF_ROUND_NEAREST;
+ }
+ if (aFlags & GXFPF_NEAREST) {
+ HandleSeparator(*this);
+ AppendLiteral("GXFPF_NEAREST");
+ aFlags &= ~GXFPF_NEAREST;
+ }
+ if (aFlags) {
+ HandleSeparator(*this);
+ AppendLiteral("Unknown(");
+ AppendInt(static_cast<uint32_t>(aFlags));
+ Append(')');
+ }
+ }
+ virtual ~GetACPFromPointFlagName() {}
+};
+
+static const char* GetFocusChangeName(
+ InputContextAction::FocusChange aFocusChange) {
+ switch (aFocusChange) {
+ case InputContextAction::FOCUS_NOT_CHANGED:
+ return "FOCUS_NOT_CHANGED";
+ case InputContextAction::GOT_FOCUS:
+ return "GOT_FOCUS";
+ case InputContextAction::LOST_FOCUS:
+ return "LOST_FOCUS";
+ case InputContextAction::MENU_GOT_PSEUDO_FOCUS:
+ return "MENU_GOT_PSEUDO_FOCUS";
+ case InputContextAction::MENU_LOST_PSEUDO_FOCUS:
+ return "MENU_LOST_PSEUDO_FOCUS";
+ case InputContextAction::WIDGET_CREATED:
+ return "WIDGET_CREATED";
+ default:
+ return "Unknown";
+ }
+}
+
+static nsCString GetCLSIDNameStr(REFCLSID aCLSID) {
+ LPOLESTR str = nullptr;
+ HRESULT hr = ::StringFromCLSID(aCLSID, &str);
+ if (FAILED(hr) || !str || !str[0]) {
+ return ""_ns;
+ }
+
+ nsCString result;
+ result = NS_ConvertUTF16toUTF8(str);
+ ::CoTaskMemFree(str);
+ return result;
+}
+
+static nsCString GetGUIDNameStr(REFGUID aGUID) {
+ OLECHAR str[40];
+ int len = ::StringFromGUID2(aGUID, str, ArrayLength(str));
+ if (!len || !str[0]) {
+ return ""_ns;
+ }
+
+ return NS_ConvertUTF16toUTF8(str);
+}
+
+static nsCString GetGUIDNameStrWithTable(REFGUID aGUID) {
+#define RETURN_GUID_NAME(aNamedGUID) \
+ if (IsEqualGUID(aGUID, aNamedGUID)) { \
+ return nsLiteralCString(#aNamedGUID); \
+ }
+
+ RETURN_GUID_NAME(GUID_PROP_INPUTSCOPE)
+ RETURN_GUID_NAME(sGUID_PROP_URL)
+ RETURN_GUID_NAME(TSATTRID_OTHERS)
+ RETURN_GUID_NAME(TSATTRID_Font)
+ RETURN_GUID_NAME(TSATTRID_Font_FaceName)
+ RETURN_GUID_NAME(TSATTRID_Font_SizePts)
+ RETURN_GUID_NAME(TSATTRID_Font_Style)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Bold)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Italic)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_SmallCaps)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Capitalize)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Uppercase)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Lowercase)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_LasVegasLights)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_BlinkingBackground)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_SparkleText)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_MarchingBlackAnts)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_MarchingRedAnts)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_Shimmer)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_WipeDown)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_WipeRight)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Emboss)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Engrave)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Hidden)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Kerning)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Outlined)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Position)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Protected)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Shadow)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Spacing)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Weight)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Height)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Underline)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Underline_Single)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Underline_Double)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough_Single)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough_Double)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Overline)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Overline_Single)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Overline_Double)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Blink)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Subscript)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Superscript)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_Color)
+ RETURN_GUID_NAME(TSATTRID_Font_Style_BackgroundColor)
+ RETURN_GUID_NAME(TSATTRID_Text)
+ RETURN_GUID_NAME(TSATTRID_Text_VerticalWriting)
+ RETURN_GUID_NAME(TSATTRID_Text_RightToLeft)
+ RETURN_GUID_NAME(TSATTRID_Text_Orientation)
+ RETURN_GUID_NAME(TSATTRID_Text_Language)
+ RETURN_GUID_NAME(TSATTRID_Text_ReadOnly)
+ RETURN_GUID_NAME(TSATTRID_Text_EmbeddedObject)
+ RETURN_GUID_NAME(TSATTRID_Text_Alignment)
+ RETURN_GUID_NAME(TSATTRID_Text_Alignment_Left)
+ RETURN_GUID_NAME(TSATTRID_Text_Alignment_Right)
+ RETURN_GUID_NAME(TSATTRID_Text_Alignment_Center)
+ RETURN_GUID_NAME(TSATTRID_Text_Alignment_Justify)
+ RETURN_GUID_NAME(TSATTRID_Text_Link)
+ RETURN_GUID_NAME(TSATTRID_Text_Hyphenation)
+ RETURN_GUID_NAME(TSATTRID_Text_Para)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_FirstLineIndent)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LeftIndent)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_RightIndent)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_SpaceAfter)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_SpaceBefore)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Single)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_OnePtFive)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Double)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_AtLeast)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Exactly)
+ RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Multiple)
+ RETURN_GUID_NAME(TSATTRID_List)
+ RETURN_GUID_NAME(TSATTRID_List_LevelIndel)
+ RETURN_GUID_NAME(TSATTRID_List_Type)
+ RETURN_GUID_NAME(TSATTRID_List_Type_Bullet)
+ RETURN_GUID_NAME(TSATTRID_List_Type_Arabic)
+ RETURN_GUID_NAME(TSATTRID_List_Type_LowerLetter)
+ RETURN_GUID_NAME(TSATTRID_List_Type_UpperLetter)
+ RETURN_GUID_NAME(TSATTRID_List_Type_LowerRoman)
+ RETURN_GUID_NAME(TSATTRID_List_Type_UpperRoman)
+ RETURN_GUID_NAME(TSATTRID_App)
+ RETURN_GUID_NAME(TSATTRID_App_IncorrectSpelling)
+ RETURN_GUID_NAME(TSATTRID_App_IncorrectGrammar)
+
+#undef RETURN_GUID_NAME
+
+ return GetGUIDNameStr(aGUID);
+}
+
+static nsCString GetRIIDNameStr(REFIID aRIID) {
+ LPOLESTR str = nullptr;
+ HRESULT hr = ::StringFromIID(aRIID, &str);
+ if (FAILED(hr) || !str || !str[0]) {
+ return ""_ns;
+ }
+
+ nsAutoString key(L"Interface\\");
+ key += str;
+
+ nsCString result;
+ wchar_t buf[256];
+ if (WinRegistry::GetString(HKEY_CLASSES_ROOT, key, u""_ns, buf,
+ WinRegistry::kLegacyWinUtilsStringFlags)) {
+ result = NS_ConvertUTF16toUTF8(buf);
+ } else {
+ result = NS_ConvertUTF16toUTF8(str);
+ }
+
+ ::CoTaskMemFree(str);
+ return result;
+}
+
+static const char* GetCommonReturnValueName(HRESULT aResult) {
+ switch (aResult) {
+ case S_OK:
+ return "S_OK";
+ case E_ABORT:
+ return "E_ABORT";
+ case E_ACCESSDENIED:
+ return "E_ACCESSDENIED";
+ case E_FAIL:
+ return "E_FAIL";
+ case E_HANDLE:
+ return "E_HANDLE";
+ case E_INVALIDARG:
+ return "E_INVALIDARG";
+ case E_NOINTERFACE:
+ return "E_NOINTERFACE";
+ case E_NOTIMPL:
+ return "E_NOTIMPL";
+ case E_OUTOFMEMORY:
+ return "E_OUTOFMEMORY";
+ case E_POINTER:
+ return "E_POINTER";
+ case E_UNEXPECTED:
+ return "E_UNEXPECTED";
+ default:
+ return SUCCEEDED(aResult) ? "Succeeded" : "Failed";
+ }
+}
+
+static const char* GetTextStoreReturnValueName(HRESULT aResult) {
+ switch (aResult) {
+ case TS_E_FORMAT:
+ return "TS_E_FORMAT";
+ case TS_E_INVALIDPOINT:
+ return "TS_E_INVALIDPOINT";
+ case TS_E_INVALIDPOS:
+ return "TS_E_INVALIDPOS";
+ case TS_E_NOINTERFACE:
+ return "TS_E_NOINTERFACE";
+ case TS_E_NOLAYOUT:
+ return "TS_E_NOLAYOUT";
+ case TS_E_NOLOCK:
+ return "TS_E_NOLOCK";
+ case TS_E_NOOBJECT:
+ return "TS_E_NOOBJECT";
+ case TS_E_NOSELECTION:
+ return "TS_E_NOSELECTION";
+ case TS_E_NOSERVICE:
+ return "TS_E_NOSERVICE";
+ case TS_E_READONLY:
+ return "TS_E_READONLY";
+ case TS_E_SYNCHRONOUS:
+ return "TS_E_SYNCHRONOUS";
+ case TS_S_ASYNC:
+ return "TS_S_ASYNC";
+ default:
+ return GetCommonReturnValueName(aResult);
+ }
+}
+
+static const nsCString GetSinkMaskNameStr(DWORD aSinkMask) {
+ nsCString description;
+ if (aSinkMask & TS_AS_TEXT_CHANGE) {
+ description.AppendLiteral("TS_AS_TEXT_CHANGE");
+ }
+ if (aSinkMask & TS_AS_SEL_CHANGE) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_AS_SEL_CHANGE");
+ }
+ if (aSinkMask & TS_AS_LAYOUT_CHANGE) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_AS_LAYOUT_CHANGE");
+ }
+ if (aSinkMask & TS_AS_ATTR_CHANGE) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_AS_ATTR_CHANGE");
+ }
+ if (aSinkMask & TS_AS_STATUS_CHANGE) {
+ HandleSeparator(description);
+ description.AppendLiteral("TS_AS_STATUS_CHANGE");
+ }
+ if (description.IsEmpty()) {
+ description.AppendLiteral("not-specified");
+ }
+ return description;
+}
+
+static const nsCString GetLockFlagNameStr(DWORD aLockFlags) {
+ nsCString description;
+ if ((aLockFlags & TS_LF_READWRITE) == TS_LF_READWRITE) {
+ description.AppendLiteral("TS_LF_READWRITE");
+ } else if (aLockFlags & TS_LF_READ) {
+ description.AppendLiteral("TS_LF_READ");
+ }
+ if (aLockFlags & TS_LF_SYNC) {
+ if (!description.IsEmpty()) {
+ description.AppendLiteral(" | ");
+ }
+ description.AppendLiteral("TS_LF_SYNC");
+ }
+ if (description.IsEmpty()) {
+ description.AppendLiteral("not-specified");
+ }
+ return description;
+}
+
+static const char* GetTextRunTypeName(TsRunType aRunType) {
+ switch (aRunType) {
+ case TS_RT_PLAIN:
+ return "TS_RT_PLAIN";
+ case TS_RT_HIDDEN:
+ return "TS_RT_HIDDEN";
+ case TS_RT_OPAQUE:
+ return "TS_RT_OPAQUE";
+ default:
+ return "Unknown";
+ }
+}
+
+static nsCString GetColorName(const TF_DA_COLOR& aColor) {
+ switch (aColor.type) {
+ case TF_CT_NONE:
+ return "TF_CT_NONE"_ns;
+ case TF_CT_SYSCOLOR:
+ return nsPrintfCString("TF_CT_SYSCOLOR, nIndex:0x%08X",
+ static_cast<int32_t>(aColor.nIndex));
+ case TF_CT_COLORREF:
+ return nsPrintfCString("TF_CT_COLORREF, cr:0x%08X",
+ static_cast<int32_t>(aColor.cr));
+ break;
+ default:
+ return nsPrintfCString("Unknown(%08X)",
+ static_cast<int32_t>(aColor.type));
+ }
+}
+
+static nsCString GetLineStyleName(TF_DA_LINESTYLE aLineStyle) {
+ switch (aLineStyle) {
+ case TF_LS_NONE:
+ return "TF_LS_NONE"_ns;
+ case TF_LS_SOLID:
+ return "TF_LS_SOLID"_ns;
+ case TF_LS_DOT:
+ return "TF_LS_DOT"_ns;
+ case TF_LS_DASH:
+ return "TF_LS_DASH"_ns;
+ case TF_LS_SQUIGGLE:
+ return "TF_LS_SQUIGGLE"_ns;
+ default: {
+ return nsPrintfCString("Unknown(%08X)", static_cast<int32_t>(aLineStyle));
+ }
+ }
+}
+
+static nsCString GetClauseAttrName(TF_DA_ATTR_INFO aAttr) {
+ switch (aAttr) {
+ case TF_ATTR_INPUT:
+ return "TF_ATTR_INPUT"_ns;
+ case TF_ATTR_TARGET_CONVERTED:
+ return "TF_ATTR_TARGET_CONVERTED"_ns;
+ case TF_ATTR_CONVERTED:
+ return "TF_ATTR_CONVERTED"_ns;
+ case TF_ATTR_TARGET_NOTCONVERTED:
+ return "TF_ATTR_TARGET_NOTCONVERTED"_ns;
+ case TF_ATTR_INPUT_ERROR:
+ return "TF_ATTR_INPUT_ERROR"_ns;
+ case TF_ATTR_FIXEDCONVERTED:
+ return "TF_ATTR_FIXEDCONVERTED"_ns;
+ case TF_ATTR_OTHER:
+ return "TF_ATTR_OTHER"_ns;
+ default: {
+ return nsPrintfCString("Unknown(%08X)", static_cast<int32_t>(aAttr));
+ }
+ }
+}
+
+static nsCString GetDisplayAttrStr(const TF_DISPLAYATTRIBUTE& aDispAttr) {
+ nsCString str;
+ str = "crText:{ ";
+ str += GetColorName(aDispAttr.crText);
+ str += " }, crBk:{ ";
+ str += GetColorName(aDispAttr.crBk);
+ str += " }, lsStyle: ";
+ str += GetLineStyleName(aDispAttr.lsStyle);
+ str += ", fBoldLine: ";
+ str += GetBoolName(aDispAttr.fBoldLine);
+ str += ", crLine:{ ";
+ str += GetColorName(aDispAttr.crLine);
+ str += " }, bAttr: ";
+ str += GetClauseAttrName(aDispAttr.bAttr);
+ return str;
+}
+
+static const char* GetMouseButtonName(int16_t aButton) {
+ switch (aButton) {
+ case MouseButton::ePrimary:
+ return "LeftButton";
+ case MouseButton::eMiddle:
+ return "MiddleButton";
+ case MouseButton::eSecondary:
+ return "RightButton";
+ default:
+ return "UnknownButton";
+ }
+}
+
+#define ADD_SEPARATOR_IF_NECESSARY(aStr) \
+ if (!aStr.IsEmpty()) { \
+ aStr.AppendLiteral(", "); \
+ }
+
+static nsCString GetMouseButtonsName(int16_t aButtons) {
+ if (!aButtons) {
+ return "no buttons"_ns;
+ }
+ nsCString names;
+ if (aButtons & MouseButtonsFlag::ePrimaryFlag) {
+ names = "LeftButton";
+ }
+ if (aButtons & MouseButtonsFlag::eSecondaryFlag) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += "RightButton";
+ }
+ if (aButtons & MouseButtonsFlag::eMiddleFlag) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += "MiddleButton";
+ }
+ if (aButtons & MouseButtonsFlag::e4thFlag) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += "4thButton";
+ }
+ if (aButtons & MouseButtonsFlag::e5thFlag) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += "5thButton";
+ }
+ return names;
+}
+
+static nsCString GetModifiersName(Modifiers aModifiers) {
+ if (aModifiers == MODIFIER_NONE) {
+ return "no modifiers"_ns;
+ }
+ nsCString names;
+ if (aModifiers & MODIFIER_ALT) {
+ names = NS_DOM_KEYNAME_ALT;
+ }
+ if (aModifiers & MODIFIER_ALTGRAPH) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_ALTGRAPH;
+ }
+ if (aModifiers & MODIFIER_CAPSLOCK) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_CAPSLOCK;
+ }
+ if (aModifiers & MODIFIER_CONTROL) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_CONTROL;
+ }
+ if (aModifiers & MODIFIER_FN) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_FN;
+ }
+ if (aModifiers & MODIFIER_FNLOCK) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_FNLOCK;
+ }
+ if (aModifiers & MODIFIER_META) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_META;
+ }
+ if (aModifiers & MODIFIER_NUMLOCK) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_NUMLOCK;
+ }
+ if (aModifiers & MODIFIER_SCROLLLOCK) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_SCROLLLOCK;
+ }
+ if (aModifiers & MODIFIER_SHIFT) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_SHIFT;
+ }
+ if (aModifiers & MODIFIER_SYMBOL) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_SYMBOL;
+ }
+ if (aModifiers & MODIFIER_SYMBOLLOCK) {
+ ADD_SEPARATOR_IF_NECESSARY(names);
+ names += NS_DOM_KEYNAME_SYMBOLLOCK;
+ }
+ return names;
+}
+
+class GetWritingModeName : public nsAutoCString {
+ public:
+ explicit GetWritingModeName(const WritingMode& aWritingMode) {
+ if (!aWritingMode.IsVertical()) {
+ AssignLiteral("Horizontal");
+ return;
+ }
+ if (aWritingMode.IsVerticalLR()) {
+ AssignLiteral("Vertical (LR)");
+ return;
+ }
+ AssignLiteral("Vertical (RL)");
+ }
+ virtual ~GetWritingModeName() {}
+};
+
+class GetEscapedUTF8String final : public NS_ConvertUTF16toUTF8 {
+ public:
+ explicit GetEscapedUTF8String(const nsAString& aString)
+ : NS_ConvertUTF16toUTF8(aString) {
+ Escape();
+ }
+ explicit GetEscapedUTF8String(const char16ptr_t aString)
+ : NS_ConvertUTF16toUTF8(aString) {
+ Escape();
+ }
+ GetEscapedUTF8String(const char16ptr_t aString, uint32_t aLength)
+ : NS_ConvertUTF16toUTF8(aString, aLength) {
+ Escape();
+ }
+
+ private:
+ void Escape() {
+ ReplaceSubstring("\r", "\\r");
+ ReplaceSubstring("\n", "\\n");
+ ReplaceSubstring("\t", "\\t");
+ }
+};
+
+class GetInputScopeString : public nsAutoCString {
+ public:
+ explicit GetInputScopeString(const nsTArray<InputScope>& aList) {
+ for (InputScope inputScope : aList) {
+ if (!IsEmpty()) {
+ AppendLiteral(", ");
+ }
+ switch (inputScope) {
+ case IS_DEFAULT:
+ AppendLiteral("IS_DEFAULT");
+ break;
+ case IS_URL:
+ AppendLiteral("IS_URL");
+ break;
+ case IS_FILE_FULLFILEPATH:
+ AppendLiteral("IS_FILE_FULLFILEPATH");
+ break;
+ case IS_FILE_FILENAME:
+ AppendLiteral("IS_FILE_FILENAME");
+ break;
+ case IS_EMAIL_USERNAME:
+ AppendLiteral("IS_EMAIL_USERNAME");
+ break;
+ case IS_EMAIL_SMTPEMAILADDRESS:
+ AppendLiteral("IS_EMAIL_SMTPEMAILADDRESS");
+ break;
+ case IS_LOGINNAME:
+ AppendLiteral("IS_LOGINNAME");
+ break;
+ case IS_PERSONALNAME_FULLNAME:
+ AppendLiteral("IS_PERSONALNAME_FULLNAME");
+ break;
+ case IS_PERSONALNAME_PREFIX:
+ AppendLiteral("IS_PERSONALNAME_PREFIX");
+ break;
+ case IS_PERSONALNAME_GIVENNAME:
+ AppendLiteral("IS_PERSONALNAME_GIVENNAME");
+ break;
+ case IS_PERSONALNAME_MIDDLENAME:
+ AppendLiteral("IS_PERSONALNAME_MIDDLENAME");
+ break;
+ case IS_PERSONALNAME_SURNAME:
+ AppendLiteral("IS_PERSONALNAME_SURNAME");
+ break;
+ case IS_PERSONALNAME_SUFFIX:
+ AppendLiteral("IS_PERSONALNAME_SUFFIX");
+ break;
+ case IS_ADDRESS_FULLPOSTALADDRESS:
+ AppendLiteral("IS_ADDRESS_FULLPOSTALADDRESS");
+ break;
+ case IS_ADDRESS_POSTALCODE:
+ AppendLiteral("IS_ADDRESS_POSTALCODE");
+ break;
+ case IS_ADDRESS_STREET:
+ AppendLiteral("IS_ADDRESS_STREET");
+ break;
+ case IS_ADDRESS_STATEORPROVINCE:
+ AppendLiteral("IS_ADDRESS_STATEORPROVINCE");
+ break;
+ case IS_ADDRESS_CITY:
+ AppendLiteral("IS_ADDRESS_CITY");
+ break;
+ case IS_ADDRESS_COUNTRYNAME:
+ AppendLiteral("IS_ADDRESS_COUNTRYNAME");
+ break;
+ case IS_ADDRESS_COUNTRYSHORTNAME:
+ AppendLiteral("IS_ADDRESS_COUNTRYSHORTNAME");
+ break;
+ case IS_CURRENCY_AMOUNTANDSYMBOL:
+ AppendLiteral("IS_CURRENCY_AMOUNTANDSYMBOL");
+ break;
+ case IS_CURRENCY_AMOUNT:
+ AppendLiteral("IS_CURRENCY_AMOUNT");
+ break;
+ case IS_DATE_FULLDATE:
+ AppendLiteral("IS_DATE_FULLDATE");
+ break;
+ case IS_DATE_MONTH:
+ AppendLiteral("IS_DATE_MONTH");
+ break;
+ case IS_DATE_DAY:
+ AppendLiteral("IS_DATE_DAY");
+ break;
+ case IS_DATE_YEAR:
+ AppendLiteral("IS_DATE_YEAR");
+ break;
+ case IS_DATE_MONTHNAME:
+ AppendLiteral("IS_DATE_MONTHNAME");
+ break;
+ case IS_DATE_DAYNAME:
+ AppendLiteral("IS_DATE_DAYNAME");
+ break;
+ case IS_DIGITS:
+ AppendLiteral("IS_DIGITS");
+ break;
+ case IS_NUMBER:
+ AppendLiteral("IS_NUMBER");
+ break;
+ case IS_ONECHAR:
+ AppendLiteral("IS_ONECHAR");
+ break;
+ case IS_PASSWORD:
+ AppendLiteral("IS_PASSWORD");
+ break;
+ case IS_TELEPHONE_FULLTELEPHONENUMBER:
+ AppendLiteral("IS_TELEPHONE_FULLTELEPHONENUMBER");
+ break;
+ case IS_TELEPHONE_COUNTRYCODE:
+ AppendLiteral("IS_TELEPHONE_COUNTRYCODE");
+ break;
+ case IS_TELEPHONE_AREACODE:
+ AppendLiteral("IS_TELEPHONE_AREACODE");
+ break;
+ case IS_TELEPHONE_LOCALNUMBER:
+ AppendLiteral("IS_TELEPHONE_LOCALNUMBER");
+ break;
+ case IS_TIME_FULLTIME:
+ AppendLiteral("IS_TIME_FULLTIME");
+ break;
+ case IS_TIME_HOUR:
+ AppendLiteral("IS_TIME_HOUR");
+ break;
+ case IS_TIME_MINORSEC:
+ AppendLiteral("IS_TIME_MINORSEC");
+ break;
+ case IS_NUMBER_FULLWIDTH:
+ AppendLiteral("IS_NUMBER_FULLWIDTH");
+ break;
+ case IS_ALPHANUMERIC_HALFWIDTH:
+ AppendLiteral("IS_ALPHANUMERIC_HALFWIDTH");
+ break;
+ case IS_ALPHANUMERIC_FULLWIDTH:
+ AppendLiteral("IS_ALPHANUMERIC_FULLWIDTH");
+ break;
+ case IS_CURRENCY_CHINESE:
+ AppendLiteral("IS_CURRENCY_CHINESE");
+ break;
+ case IS_BOPOMOFO:
+ AppendLiteral("IS_BOPOMOFO");
+ break;
+ case IS_HIRAGANA:
+ AppendLiteral("IS_HIRAGANA");
+ break;
+ case IS_KATAKANA_HALFWIDTH:
+ AppendLiteral("IS_KATAKANA_HALFWIDTH");
+ break;
+ case IS_KATAKANA_FULLWIDTH:
+ AppendLiteral("IS_KATAKANA_FULLWIDTH");
+ break;
+ case IS_HANJA:
+ AppendLiteral("IS_HANJA");
+ break;
+ case IS_PHRASELIST:
+ AppendLiteral("IS_PHRASELIST");
+ break;
+ case IS_REGULAREXPRESSION:
+ AppendLiteral("IS_REGULAREXPRESSION");
+ break;
+ case IS_SRGS:
+ AppendLiteral("IS_SRGS");
+ break;
+ case IS_XML:
+ AppendLiteral("IS_XML");
+ break;
+ case IS_PRIVATE:
+ AppendLiteral("IS_PRIVATE");
+ break;
+ default:
+ AppendPrintf("Unknown Value(%d)", inputScope);
+ break;
+ }
+ }
+ }
+};
+
+/******************************************************************/
+/* InputScopeImpl */
+/******************************************************************/
+
+class InputScopeImpl final : public ITfInputScope {
+ ~InputScopeImpl() {}
+
+ public:
+ explicit InputScopeImpl(const nsTArray<InputScope>& aList)
+ : mInputScopes(aList.Clone()) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p InputScopeImpl(%s)", this, GetInputScopeString(aList).get()));
+ }
+
+ NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(InputScopeImpl)
+
+ STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
+ *ppv = nullptr;
+ if ((IID_IUnknown == riid) || (IID_ITfInputScope == riid)) {
+ *ppv = static_cast<ITfInputScope*>(this);
+ }
+ if (*ppv) {
+ AddRef();
+ return S_OK;
+ }
+ return E_NOINTERFACE;
+ }
+
+ STDMETHODIMP GetInputScopes(InputScope** pprgInputScopes, UINT* pcCount) {
+ uint32_t count = (mInputScopes.IsEmpty() ? 1 : mInputScopes.Length());
+
+ InputScope* pScope =
+ (InputScope*)CoTaskMemAlloc(sizeof(InputScope) * count);
+ NS_ENSURE_TRUE(pScope, E_OUTOFMEMORY);
+
+ if (mInputScopes.IsEmpty()) {
+ *pScope = IS_DEFAULT;
+ *pcCount = 1;
+ *pprgInputScopes = pScope;
+ return S_OK;
+ }
+
+ *pcCount = 0;
+
+ for (uint32_t idx = 0; idx < count; idx++) {
+ *(pScope + idx) = mInputScopes[idx];
+ (*pcCount)++;
+ }
+
+ *pprgInputScopes = pScope;
+ return S_OK;
+ }
+
+ STDMETHODIMP GetPhrase(BSTR** ppbstrPhrases, UINT* pcCount) {
+ return E_NOTIMPL;
+ }
+ STDMETHODIMP GetRegularExpression(BSTR* pbstrRegExp) { return E_NOTIMPL; }
+ STDMETHODIMP GetSRGS(BSTR* pbstrSRGS) { return E_NOTIMPL; }
+ STDMETHODIMP GetXML(BSTR* pbstrXML) { return E_NOTIMPL; }
+
+ private:
+ nsTArray<InputScope> mInputScopes;
+};
+
+/******************************************************************/
+/* TSFStaticSink */
+/******************************************************************/
+
+class TSFStaticSink final : public ITfInputProcessorProfileActivationSink {
+ public:
+ static TSFStaticSink* GetInstance() {
+ if (!sInstance) {
+ RefPtr<ITfThreadMgr> threadMgr = TSFTextStore::GetThreadMgr();
+ if (NS_WARN_IF(!threadMgr)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink "
+ "instance due to no ThreadMgr instance"));
+ return nullptr;
+ }
+ RefPtr<ITfInputProcessorProfiles> inputProcessorProfiles =
+ TSFTextStore::GetInputProcessorProfiles();
+ if (NS_WARN_IF(!inputProcessorProfiles)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink "
+ "instance due to no InputProcessorProfiles instance"));
+ return nullptr;
+ }
+ RefPtr<TSFStaticSink> staticSink = new TSFStaticSink();
+ if (NS_WARN_IF(!staticSink->Init(threadMgr, inputProcessorProfiles))) {
+ staticSink->Destroy();
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink "
+ "instance"));
+ return nullptr;
+ }
+ sInstance = staticSink.forget();
+ }
+ return sInstance;
+ }
+
+ static void Shutdown() {
+ if (sInstance) {
+ sInstance->Destroy();
+ sInstance = nullptr;
+ }
+ }
+
+ bool Init(ITfThreadMgr* aThreadMgr,
+ ITfInputProcessorProfiles* aInputProcessorProfiles);
+ STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
+ *ppv = nullptr;
+ if (IID_IUnknown == riid ||
+ IID_ITfInputProcessorProfileActivationSink == riid) {
+ *ppv = static_cast<ITfInputProcessorProfileActivationSink*>(this);
+ }
+ if (*ppv) {
+ AddRef();
+ return S_OK;
+ }
+ return E_NOINTERFACE;
+ }
+
+ NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(TSFStaticSink)
+
+ const nsString& GetActiveTIPKeyboardDescription() const {
+ return mActiveTIPKeyboardDescription;
+ }
+
+ static bool IsIMM_IMEActive() {
+ // Use IMM API until TSFStaticSink starts to work.
+ if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) {
+ return IsIMM_IME(::GetKeyboardLayout(0));
+ }
+ return sInstance->mIsIMM_IME;
+ }
+
+ static bool IsIMM_IME(HKL aHKL) {
+ return (::ImmGetIMEFileNameW(aHKL, nullptr, 0) > 0);
+ }
+
+ static bool IsTraditionalChinese() {
+ EnsureInstance();
+ return sInstance && sInstance->IsTraditionalChineseInternal();
+ }
+ static bool IsSimplifiedChinese() {
+ EnsureInstance();
+ return sInstance && sInstance->IsSimplifiedChineseInternal();
+ }
+ static bool IsJapanese() {
+ EnsureInstance();
+ return sInstance && sInstance->IsJapaneseInternal();
+ }
+ static bool IsKorean() {
+ EnsureInstance();
+ return sInstance && sInstance->IsKoreanInternal();
+ }
+
+ /**
+ * ActiveTIP() returns an ID for currently active TIP.
+ * Please note that this method is expensive due to needs a lot of GUID
+ * comparations if active language ID is one of CJKT. If you need to
+ * check TIPs for a specific language, you should check current language
+ * first.
+ */
+ static TextInputProcessorID ActiveTIP() {
+ EnsureInstance();
+ if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) {
+ return TextInputProcessorID::eUnknown;
+ }
+ sInstance->ComputeActiveTextInputProcessor();
+ if (NS_WARN_IF(sInstance->mActiveTIP ==
+ TextInputProcessorID::eNotComputed)) {
+ return TextInputProcessorID::eUnknown;
+ }
+ return sInstance->mActiveTIP;
+ }
+
+ static bool GetActiveTIPNameForTelemetry(nsAString& aName) {
+ if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) {
+ return false;
+ }
+ if (sInstance->mActiveTIPGUID == GUID_NULL) {
+ aName.Truncate();
+ aName.AppendPrintf("0x%04X", sInstance->mLangID);
+ return true;
+ }
+ // key should be "LocaleID|Description". Although GUID of the
+ // profile is unique key since description may be localized for system
+ // language, unfortunately, it's too long to record as key with its
+ // description. Therefore, we should record only the description with
+ // LocaleID because Microsoft IME may not include language information.
+ // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
+ aName.Truncate();
+ aName.AppendPrintf("0x%04X|", sInstance->mLangID);
+ nsAutoString description;
+ description.Assign(sInstance->mActiveTIPKeyboardDescription);
+ static const uint32_t kMaxDescriptionLength = 72 - aName.Length();
+ if (description.Length() > kMaxDescriptionLength) {
+ if (NS_IS_LOW_SURROGATE(description[kMaxDescriptionLength - 1]) &&
+ NS_IS_HIGH_SURROGATE(description[kMaxDescriptionLength - 2])) {
+ description.Truncate(kMaxDescriptionLength - 2);
+ } else {
+ description.Truncate(kMaxDescriptionLength - 1);
+ }
+ // U+2026 is "..."
+ description.Append(char16_t(0x2026));
+ }
+ aName.Append(description);
+ return true;
+ }
+
+ static bool IsMSChangJieOrMSQuickActive() {
+ // ActiveTIP() is expensive if it hasn't computed active TIP yet.
+ // For avoiding unnecessary computation, we should check if the language
+ // for current TIP is Traditional Chinese.
+ if (!IsTraditionalChinese()) {
+ return false;
+ }
+ switch (ActiveTIP()) {
+ case TextInputProcessorID::eMicrosoftChangJie:
+ case TextInputProcessorID::eMicrosoftQuick:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool IsMSPinyinOrMSWubiActive() {
+ // ActiveTIP() is expensive if it hasn't computed active TIP yet.
+ // For avoiding unnecessary computation, we should check if the language
+ // for current TIP is Simplified Chinese.
+ if (!IsSimplifiedChinese()) {
+ return false;
+ }
+ switch (ActiveTIP()) {
+ case TextInputProcessorID::eMicrosoftPinyin:
+ case TextInputProcessorID::eMicrosoftWubi:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool IsMSJapaneseIMEActive() {
+ // ActiveTIP() is expensive if it hasn't computed active TIP yet.
+ // For avoiding unnecessary computation, we should check if the language
+ // for current TIP is Japanese.
+ if (!IsJapanese()) {
+ return false;
+ }
+ return ActiveTIP() == TextInputProcessorID::eMicrosoftIMEForJapanese;
+ }
+
+ static bool IsGoogleJapaneseInputActive() {
+ // ActiveTIP() is expensive if it hasn't computed active TIP yet.
+ // For avoiding unnecessary computation, we should check if the language
+ // for current TIP is Japanese.
+ if (!IsJapanese()) {
+ return false;
+ }
+ return ActiveTIP() == TextInputProcessorID::eGoogleJapaneseInput;
+ }
+
+ static bool IsATOKActive() {
+ // ActiveTIP() is expensive if it hasn't computed active TIP yet.
+ // For avoiding unnecessary computation, we should check if active TIP is
+ // ATOK first since it's cheaper.
+ return IsJapanese() && sInstance->IsATOKActiveInternal();
+ }
+
+ // Note that ATOK 2011 - 2016 refers native caret position for deciding its
+ // popup window position.
+ static bool IsATOKReferringNativeCaretActive() {
+ // ActiveTIP() is expensive if it hasn't computed active TIP yet.
+ // For avoiding unnecessary computation, we should check if active TIP is
+ // ATOK first since it's cheaper.
+ if (!IsJapanese() || !sInstance->IsATOKActiveInternal()) {
+ return false;
+ }
+ switch (ActiveTIP()) {
+ case TextInputProcessorID::eATOK2011:
+ case TextInputProcessorID::eATOK2012:
+ case TextInputProcessorID::eATOK2013:
+ case TextInputProcessorID::eATOK2014:
+ case TextInputProcessorID::eATOK2015:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private:
+ static void EnsureInstance() {
+ if (!sInstance) {
+ RefPtr<TSFStaticSink> staticSink = GetInstance();
+ Unused << staticSink;
+ }
+ }
+
+ bool IsTraditionalChineseInternal() const { return mLangID == 0x0404; }
+ bool IsSimplifiedChineseInternal() const { return mLangID == 0x0804; }
+ bool IsJapaneseInternal() const { return mLangID == 0x0411; }
+ bool IsKoreanInternal() const { return mLangID == 0x0412; }
+
+ bool IsATOKActiveInternal() {
+ EnsureInitActiveTIPKeyboard();
+ // FYI: Name of packaged ATOK includes the release year like "ATOK 2015".
+ // Name of ATOK Passport (subscription) equals "ATOK".
+ return StringBeginsWith(mActiveTIPKeyboardDescription, u"ATOK "_ns) ||
+ mActiveTIPKeyboardDescription.EqualsLiteral("ATOK");
+ }
+
+ void ComputeActiveTextInputProcessor() {
+ if (mActiveTIP != TextInputProcessorID::eNotComputed) {
+ return;
+ }
+
+ if (mActiveTIPGUID == GUID_NULL) {
+ mActiveTIP = TextInputProcessorID::eNone;
+ return;
+ }
+
+ // Comparing GUID is slow. So, we should use language information to
+ // reduce the comparing cost for TIP which is not we do not support
+ // specifically since they are always compared with all supported TIPs.
+ switch (mLangID) {
+ case 0x0404:
+ mActiveTIP = ComputeActiveTIPAsTraditionalChinese();
+ break;
+ case 0x0411:
+ mActiveTIP = ComputeActiveTIPAsJapanese();
+ break;
+ case 0x0412:
+ mActiveTIP = ComputeActiveTIPAsKorean();
+ break;
+ case 0x0804:
+ mActiveTIP = ComputeActiveTIPAsSimplifiedChinese();
+ break;
+ default:
+ mActiveTIP = TextInputProcessorID::eUnknown;
+ break;
+ }
+ // Special case for Keyman Desktop, it is available for any languages.
+ // Therefore, we need to check it only if we don't know the active TIP.
+ if (mActiveTIP != TextInputProcessorID::eUnknown) {
+ return;
+ }
+
+ // Note that keyboard layouts for Keyman assign its GUID on install
+ // randomly, but CLSID is constant in any environments.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1670834#c7
+ // https://github.com/keymanapp/keyman/blob/318c73a9e1d571d942837ff9964590626e5bd5aa/windows/src/engine/kmtip/globals.cpp#L37
+ // {FE0420F1-38D1-4B4C-96BF-E7E20A74CFB7}
+ static constexpr CLSID kKeymanDesktop_CLSID = {
+ 0xFE0420F1,
+ 0x38D1,
+ 0x4B4C,
+ {0x96, 0xBF, 0xE7, 0xE2, 0x0A, 0x74, 0xCF, 0xB7}};
+ if (mActiveTIPCLSID == kKeymanDesktop_CLSID) {
+ mActiveTIP = TextInputProcessorID::eKeymanDesktop;
+ }
+ }
+
+ TextInputProcessorID ComputeActiveTIPAsJapanese() {
+ // {A76C93D9-5523-4E90-AAFA-4DB112F9AC76} (Win7, Win8.1, Win10)
+ static constexpr GUID kMicrosoftIMEForJapaneseGUID = {
+ 0xA76C93D9,
+ 0x5523,
+ 0x4E90,
+ {0xAA, 0xFA, 0x4D, 0xB1, 0x12, 0xF9, 0xAC, 0x76}};
+ if (mActiveTIPGUID == kMicrosoftIMEForJapaneseGUID) {
+ return TextInputProcessorID::eMicrosoftIMEForJapanese;
+ }
+ // {54EDCC94-1524-4BB1-9FB7-7BABE4F4CA64}
+ static constexpr GUID kMicrosoftOfficeIME2010ForJapaneseGUID = {
+ 0x54EDCC94,
+ 0x1524,
+ 0x4BB1,
+ {0x9F, 0xB7, 0x7B, 0xAB, 0xE4, 0xF4, 0xCA, 0x64}};
+ if (mActiveTIPGUID == kMicrosoftOfficeIME2010ForJapaneseGUID) {
+ return TextInputProcessorID::eMicrosoftOfficeIME2010ForJapanese;
+ }
+ // {773EB24E-CA1D-4B1B-B420-FA985BB0B80D}
+ static constexpr GUID kGoogleJapaneseInputGUID = {
+ 0x773EB24E,
+ 0xCA1D,
+ 0x4B1B,
+ {0xB4, 0x20, 0xFA, 0x98, 0x5B, 0xB0, 0xB8, 0x0D}};
+ if (mActiveTIPGUID == kGoogleJapaneseInputGUID) {
+ return TextInputProcessorID::eGoogleJapaneseInput;
+ }
+ // {F9C24A5C-8A53-499D-9572-93B2FF582115}
+ static const GUID kATOK2011GUID = {
+ 0xF9C24A5C,
+ 0x8A53,
+ 0x499D,
+ {0x95, 0x72, 0x93, 0xB2, 0xFF, 0x58, 0x21, 0x15}};
+ if (mActiveTIPGUID == kATOK2011GUID) {
+ return TextInputProcessorID::eATOK2011;
+ }
+ // {1DE01562-F445-401B-B6C3-E5B18DB79461}
+ static constexpr GUID kATOK2012GUID = {
+ 0x1DE01562,
+ 0xF445,
+ 0x401B,
+ {0xB6, 0xC3, 0xE5, 0xB1, 0x8D, 0xB7, 0x94, 0x61}};
+ if (mActiveTIPGUID == kATOK2012GUID) {
+ return TextInputProcessorID::eATOK2012;
+ }
+ // {3C4DB511-189A-4168-B6EA-BFD0B4C85615}
+ static constexpr GUID kATOK2013GUID = {
+ 0x3C4DB511,
+ 0x189A,
+ 0x4168,
+ {0xB6, 0xEA, 0xBF, 0xD0, 0xB4, 0xC8, 0x56, 0x15}};
+ if (mActiveTIPGUID == kATOK2013GUID) {
+ return TextInputProcessorID::eATOK2013;
+ }
+ // {4EF33B79-6AA9-4271-B4BF-9321C279381B}
+ static constexpr GUID kATOK2014GUID = {
+ 0x4EF33B79,
+ 0x6AA9,
+ 0x4271,
+ {0xB4, 0xBF, 0x93, 0x21, 0xC2, 0x79, 0x38, 0x1B}};
+ if (mActiveTIPGUID == kATOK2014GUID) {
+ return TextInputProcessorID::eATOK2014;
+ }
+ // {EAB4DC00-CE2E-483D-A86A-E6B99DA9599A}
+ static constexpr GUID kATOK2015GUID = {
+ 0xEAB4DC00,
+ 0xCE2E,
+ 0x483D,
+ {0xA8, 0x6A, 0xE6, 0xB9, 0x9D, 0xA9, 0x59, 0x9A}};
+ if (mActiveTIPGUID == kATOK2015GUID) {
+ return TextInputProcessorID::eATOK2015;
+ }
+ // {0B557B4C-5740-4110-A60A-1493FA10BF2B}
+ static constexpr GUID kATOK2016GUID = {
+ 0x0B557B4C,
+ 0x5740,
+ 0x4110,
+ {0xA6, 0x0A, 0x14, 0x93, 0xFA, 0x10, 0xBF, 0x2B}};
+ if (mActiveTIPGUID == kATOK2016GUID) {
+ return TextInputProcessorID::eATOK2016;
+ }
+
+ // * ATOK 2017
+ // - {6DBFD8F5-701D-11E6-920F-782BCBA6348F}
+ // * ATOK Passport (confirmed with version 31.1.2)
+ // - {A38F2FD9-7199-45E1-841C-BE0313D8052F}
+
+ if (IsATOKActiveInternal()) {
+ return TextInputProcessorID::eATOKUnknown;
+ }
+
+ // {E6D66705-1EDA-4373-8D01-1D0CB2D054C7}
+ static constexpr GUID kJapanist10GUID = {
+ 0xE6D66705,
+ 0x1EDA,
+ 0x4373,
+ {0x8D, 0x01, 0x1D, 0x0C, 0xB2, 0xD0, 0x54, 0xC7}};
+ if (mActiveTIPGUID == kJapanist10GUID) {
+ return TextInputProcessorID::eJapanist10;
+ }
+
+ return TextInputProcessorID::eUnknown;
+ }
+
+ TextInputProcessorID ComputeActiveTIPAsTraditionalChinese() {
+ // {B2F9C502-1742-11D4-9790-0080C882687E} (Win8.1, Win10)
+ static constexpr GUID kMicrosoftBopomofoGUID = {
+ 0xB2F9C502,
+ 0x1742,
+ 0x11D4,
+ {0x97, 0x90, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftBopomofoGUID) {
+ return TextInputProcessorID::eMicrosoftBopomofo;
+ }
+ // {4BDF9F03-C7D3-11D4-B2AB-0080C882687E} (Win7, Win8.1, Win10)
+ static const GUID kMicrosoftChangJieGUID = {
+ 0x4BDF9F03,
+ 0xC7D3,
+ 0x11D4,
+ {0xB2, 0xAB, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftChangJieGUID) {
+ return TextInputProcessorID::eMicrosoftChangJie;
+ }
+ // {761309DE-317A-11D4-9B5D-0080C882687E} (Win7)
+ static constexpr GUID kMicrosoftPhoneticGUID = {
+ 0x761309DE,
+ 0x317A,
+ 0x11D4,
+ {0x9B, 0x5D, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftPhoneticGUID) {
+ return TextInputProcessorID::eMicrosoftPhonetic;
+ }
+ // {6024B45F-5C54-11D4-B921-0080C882687E} (Win7, Win8.1, Win10)
+ static constexpr GUID kMicrosoftQuickGUID = {
+ 0x6024B45F,
+ 0x5C54,
+ 0x11D4,
+ {0xB9, 0x21, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftQuickGUID) {
+ return TextInputProcessorID::eMicrosoftQuick;
+ }
+ // {F3BA907A-6C7E-11D4-97FA-0080C882687E} (Win7)
+ static constexpr GUID kMicrosoftNewChangJieGUID = {
+ 0xF3BA907A,
+ 0x6C7E,
+ 0x11D4,
+ {0x97, 0xFA, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftNewChangJieGUID) {
+ return TextInputProcessorID::eMicrosoftNewChangJie;
+ }
+ // {B2F9C502-1742-11D4-9790-0080C882687E} (Win7)
+ static constexpr GUID kMicrosoftNewPhoneticGUID = {
+ 0xB2F9C502,
+ 0x1742,
+ 0x11D4,
+ {0x97, 0x90, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftNewPhoneticGUID) {
+ return TextInputProcessorID::eMicrosoftNewPhonetic;
+ }
+ // {0B883BA0-C1C7-11D4-87F9-0080C882687E} (Win7)
+ static constexpr GUID kMicrosoftNewQuickGUID = {
+ 0x0B883BA0,
+ 0xC1C7,
+ 0x11D4,
+ {0x87, 0xF9, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftNewQuickGUID) {
+ return TextInputProcessorID::eMicrosoftNewQuick;
+ }
+
+ // NOTE: There are some other Traditional Chinese TIPs installed in Windows:
+ // * Chinese Traditional Array (version 6.0)
+ // - {D38EFF65-AA46-4FD5-91A7-67845FB02F5B} (Win7, Win8.1)
+ // * Chinese Traditional DaYi (version 6.0)
+ // - {037B2C25-480C-4D7F-B027-D6CA6B69788A} (Win7, Win8.1)
+
+ // {B58630B5-0ED3-4335-BBC9-E77BBCB43CAD}
+ static const GUID kFreeChangJieGUID = {
+ 0xB58630B5,
+ 0x0ED3,
+ 0x4335,
+ {0xBB, 0xC9, 0xE7, 0x7B, 0xBC, 0xB4, 0x3C, 0xAD}};
+ if (mActiveTIPGUID == kFreeChangJieGUID) {
+ return TextInputProcessorID::eFreeChangJie;
+ }
+
+ return TextInputProcessorID::eUnknown;
+ }
+
+ TextInputProcessorID ComputeActiveTIPAsSimplifiedChinese() {
+ // FYI: This matches with neither "Microsoft Pinyin ABC Input Style" nor
+ // "Microsoft Pinyin New Experience Input Style" on Win7.
+ // {FA550B04-5AD7-411F-A5AC-CA038EC515D7} (Win8.1, Win10)
+ static constexpr GUID kMicrosoftPinyinGUID = {
+ 0xFA550B04,
+ 0x5AD7,
+ 0x411F,
+ {0xA5, 0xAC, 0xCA, 0x03, 0x8E, 0xC5, 0x15, 0xD7}};
+ if (mActiveTIPGUID == kMicrosoftPinyinGUID) {
+ return TextInputProcessorID::eMicrosoftPinyin;
+ }
+
+ // {F3BA9077-6C7E-11D4-97FA-0080C882687E} (Win7)
+ static constexpr GUID kMicrosoftPinyinNewExperienceInputStyleGUID = {
+ 0xF3BA9077,
+ 0x6C7E,
+ 0x11D4,
+ {0x97, 0xFA, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}};
+ if (mActiveTIPGUID == kMicrosoftPinyinNewExperienceInputStyleGUID) {
+ return TextInputProcessorID::eMicrosoftPinyinNewExperienceInputStyle;
+ }
+ // {82590C13-F4DD-44F4-BA1D-8667246FDF8E} (Win8.1, Win10)
+ static constexpr GUID kMicrosoftWubiGUID = {
+ 0x82590C13,
+ 0xF4DD,
+ 0x44F4,
+ {0xBA, 0x1D, 0x86, 0x67, 0x24, 0x6F, 0xDF, 0x8E}};
+ if (mActiveTIPGUID == kMicrosoftWubiGUID) {
+ return TextInputProcessorID::eMicrosoftWubi;
+ }
+ // NOTE: There are some other Simplified Chinese TIPs installed in Windows:
+ // * Chinese Simplified QuanPin (version 6.0)
+ // - {54FC610E-6ABD-4685-9DDD-A130BDF1B170} (Win8.1)
+ // * Chinese Simplified ZhengMa (version 6.0)
+ // - {733B4D81-3BC3-4132-B91A-E9CDD5E2BFC9} (Win8.1)
+ // * Chinese Simplified ShuangPin (version 6.0)
+ // - {EF63706D-31C4-490E-9DBB-BD150ADC454B} (Win8.1)
+ // * Microsoft Pinyin ABC Input Style
+ // - {FCA121D2-8C6D-41FB-B2DE-A2AD110D4820} (Win7)
+ return TextInputProcessorID::eUnknown;
+ }
+
+ TextInputProcessorID ComputeActiveTIPAsKorean() {
+ // {B5FE1F02-D5F2-4445-9C03-C568F23C99A1} (Win7, Win8.1, Win10)
+ static constexpr GUID kMicrosoftIMEForKoreanGUID = {
+ 0xB5FE1F02,
+ 0xD5F2,
+ 0x4445,
+ {0x9C, 0x03, 0xC5, 0x68, 0xF2, 0x3C, 0x99, 0xA1}};
+ if (mActiveTIPGUID == kMicrosoftIMEForKoreanGUID) {
+ return TextInputProcessorID::eMicrosoftIMEForKorean;
+ }
+ // {B60AF051-257A-46BC-B9D3-84DAD819BAFB} (Win8.1, Win10)
+ static constexpr GUID kMicrosoftOldHangulGUID = {
+ 0xB60AF051,
+ 0x257A,
+ 0x46BC,
+ {0xB9, 0xD3, 0x84, 0xDA, 0xD8, 0x19, 0xBA, 0xFB}};
+ if (mActiveTIPGUID == kMicrosoftOldHangulGUID) {
+ return TextInputProcessorID::eMicrosoftOldHangul;
+ }
+
+ // NOTE: There is the other Korean TIP installed in Windows:
+ // * Microsoft IME 2010
+ // - {48878C45-93F9-4aaf-A6A1-272CD863C4F5} (Win7)
+
+ return TextInputProcessorID::eUnknown;
+ }
+
+ public: // ITfInputProcessorProfileActivationSink
+ STDMETHODIMP OnActivated(DWORD, LANGID, REFCLSID, REFGUID, REFGUID, HKL,
+ DWORD);
+
+ private:
+ TSFStaticSink();
+ virtual ~TSFStaticSink() {}
+
+ bool EnsureInitActiveTIPKeyboard();
+
+ void Destroy();
+
+ void GetTIPDescription(REFCLSID aTextService, LANGID aLangID,
+ REFGUID aProfile, nsAString& aDescription);
+ bool IsTIPCategoryKeyboard(REFCLSID aTextService, LANGID aLangID,
+ REFGUID aProfile);
+
+ TextInputProcessorID mActiveTIP;
+
+ // Cookie of installing ITfInputProcessorProfileActivationSink
+ DWORD mIPProfileCookie;
+
+ LANGID mLangID;
+
+ // True if current IME is implemented with IMM.
+ bool mIsIMM_IME;
+ // True if OnActivated() is already called
+ bool mOnActivatedCalled;
+
+ RefPtr<ITfThreadMgr> mThreadMgr;
+ RefPtr<ITfInputProcessorProfiles> mInputProcessorProfiles;
+
+ // Active TIP keyboard's description. If active language profile isn't TIP,
+ // i.e., IMM-IME or just a keyboard layout, this is empty.
+ nsString mActiveTIPKeyboardDescription;
+
+ // Active TIP's GUID and CLSID
+ GUID mActiveTIPGUID;
+ CLSID mActiveTIPCLSID;
+
+ static StaticRefPtr<TSFStaticSink> sInstance;
+};
+
+StaticRefPtr<TSFStaticSink> TSFStaticSink::sInstance;
+
+TSFStaticSink::TSFStaticSink()
+ : mActiveTIP(TextInputProcessorID::eNotComputed),
+ mIPProfileCookie(TF_INVALID_COOKIE),
+ mLangID(0),
+ mIsIMM_IME(false),
+ mOnActivatedCalled(false),
+ mActiveTIPGUID(GUID_NULL) {}
+
+bool TSFStaticSink::Init(ITfThreadMgr* aThreadMgr,
+ ITfInputProcessorProfiles* aInputProcessorProfiles) {
+ MOZ_ASSERT(!mThreadMgr && !mInputProcessorProfiles,
+ "TSFStaticSink::Init() must be called only once");
+
+ mThreadMgr = aThreadMgr;
+ mInputProcessorProfiles = aInputProcessorProfiles;
+
+ RefPtr<ITfSource> source;
+ HRESULT hr =
+ mThreadMgr->QueryInterface(IID_ITfSource, getter_AddRefs(source));
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFStaticSink::Init() FAILED to get ITfSource "
+ "instance (0x%08lX)",
+ this, hr));
+ return false;
+ }
+
+ // NOTE: On Vista or later, Windows let us know activate IME changed only
+ // with ITfInputProcessorProfileActivationSink.
+ hr = source->AdviseSink(
+ IID_ITfInputProcessorProfileActivationSink,
+ static_cast<ITfInputProcessorProfileActivationSink*>(this),
+ &mIPProfileCookie);
+ if (FAILED(hr) || mIPProfileCookie == TF_INVALID_COOKIE) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFStaticSink::Init() FAILED to install "
+ "ITfInputProcessorProfileActivationSink (0x%08lX)",
+ this, hr));
+ return false;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFStaticSink::Init(), "
+ "mIPProfileCookie=0x%08lX",
+ this, mIPProfileCookie));
+ return true;
+}
+
+void TSFStaticSink::Destroy() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFStaticSink::Shutdown() "
+ "mIPProfileCookie=0x%08lX",
+ this, mIPProfileCookie));
+
+ if (mIPProfileCookie != TF_INVALID_COOKIE) {
+ RefPtr<ITfSource> source;
+ HRESULT hr =
+ mThreadMgr->QueryInterface(IID_ITfSource, getter_AddRefs(source));
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFStaticSink::Shutdown() FAILED to get "
+ "ITfSource instance (0x%08lX)",
+ this, hr));
+ } else {
+ hr = source->UnadviseSink(mIPProfileCookie);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Shutdown() FAILED to uninstall "
+ "ITfInputProcessorProfileActivationSink (0x%08lX)",
+ this, hr));
+ }
+ }
+ }
+
+ mThreadMgr = nullptr;
+ mInputProcessorProfiles = nullptr;
+}
+
+STDMETHODIMP
+TSFStaticSink::OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID rclsid,
+ REFGUID catid, REFGUID guidProfile, HKL hkl,
+ DWORD dwFlags) {
+ if ((dwFlags & TF_IPSINK_FLAG_ACTIVE) &&
+ (dwProfileType == TF_PROFILETYPE_KEYBOARDLAYOUT ||
+ catid == GUID_TFCAT_TIP_KEYBOARD)) {
+ mOnActivatedCalled = true;
+ mActiveTIP = TextInputProcessorID::eNotComputed;
+ mActiveTIPGUID = guidProfile;
+ mActiveTIPCLSID = rclsid;
+ mLangID = langid & 0xFFFF;
+ mIsIMM_IME = IsIMM_IME(hkl);
+ GetTIPDescription(rclsid, langid, guidProfile,
+ mActiveTIPKeyboardDescription);
+ if (mActiveTIPGUID != GUID_NULL) {
+ // key should be "LocaleID|Description". Although GUID of the
+ // profile is unique key since description may be localized for system
+ // language, unfortunately, it's too long to record as key with its
+ // description. Therefore, we should record only the description with
+ // LocaleID because Microsoft IME may not include language information.
+ // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
+ nsAutoString key;
+ TSFStaticSink::GetActiveTIPNameForTelemetry(key);
+ Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_WINDOWS, key,
+ true);
+ }
+ // Notify IMEHandler of changing active keyboard layout.
+ IMEHandler::OnKeyboardLayoutChanged();
+ }
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFStaticSink::OnActivated(dwProfileType=%s (0x%08lX), "
+ "langid=0x%08X, rclsid=%s, catid=%s, guidProfile=%s, hkl=0x%p, "
+ "dwFlags=0x%08lX (TF_IPSINK_FLAG_ACTIVE: %s)), mIsIMM_IME=%s, "
+ "mActiveTIPDescription=\"%s\"",
+ this,
+ dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR
+ ? "TF_PROFILETYPE_INPUTPROCESSOR"
+ : dwProfileType == TF_PROFILETYPE_KEYBOARDLAYOUT
+ ? "TF_PROFILETYPE_KEYBOARDLAYOUT"
+ : "Unknown",
+ dwProfileType, langid, GetCLSIDNameStr(rclsid).get(),
+ GetGUIDNameStr(catid).get(), GetGUIDNameStr(guidProfile).get(), hkl,
+ dwFlags, GetBoolName(dwFlags & TF_IPSINK_FLAG_ACTIVE),
+ GetBoolName(mIsIMM_IME),
+ NS_ConvertUTF16toUTF8(mActiveTIPKeyboardDescription).get()));
+ return S_OK;
+}
+
+bool TSFStaticSink::EnsureInitActiveTIPKeyboard() {
+ if (mOnActivatedCalled) {
+ return true;
+ }
+
+ RefPtr<ITfInputProcessorProfileMgr> profileMgr;
+ HRESULT hr = mInputProcessorProfiles->QueryInterface(
+ IID_ITfInputProcessorProfileMgr, getter_AddRefs(profileMgr));
+ if (FAILED(hr) || !profileMgr) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED "
+ "to get input processor profile manager, hr=0x%08lX",
+ this, hr));
+ return false;
+ }
+
+ TF_INPUTPROCESSORPROFILE profile;
+ hr = profileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile);
+ if (hr == S_FALSE) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED "
+ "to get active keyboard layout profile due to no active profile, "
+ "hr=0x%08lX",
+ this, hr));
+ // XXX Should we call OnActivated() with arguments like non-TIP in this
+ // case?
+ return false;
+ }
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED "
+ "to get active TIP keyboard, hr=0x%08lX",
+ this, hr));
+ return false;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), "
+ "calling OnActivated() manually...",
+ this));
+ OnActivated(profile.dwProfileType, profile.langid, profile.clsid,
+ profile.catid, profile.guidProfile, ::GetKeyboardLayout(0),
+ TF_IPSINK_FLAG_ACTIVE);
+ return true;
+}
+
+void TSFStaticSink::GetTIPDescription(REFCLSID aTextService, LANGID aLangID,
+ REFGUID aProfile,
+ nsAString& aDescription) {
+ aDescription.Truncate();
+
+ if (aTextService == CLSID_NULL || aProfile == GUID_NULL) {
+ return;
+ }
+
+ BSTR description = nullptr;
+ HRESULT hr = mInputProcessorProfiles->GetLanguageProfileDescription(
+ aTextService, aLangID, aProfile, &description);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFStaticSink::InitActiveTIPDescription() FAILED "
+ "due to GetLanguageProfileDescription() failure, hr=0x%08lX",
+ this, hr));
+ return;
+ }
+
+ if (description && description[0]) {
+ aDescription.Assign(description);
+ }
+ ::SysFreeString(description);
+}
+
+bool TSFStaticSink::IsTIPCategoryKeyboard(REFCLSID aTextService, LANGID aLangID,
+ REFGUID aProfile) {
+ if (aTextService == CLSID_NULL || aProfile == GUID_NULL) {
+ return false;
+ }
+
+ RefPtr<IEnumTfLanguageProfiles> enumLangProfiles;
+ HRESULT hr = mInputProcessorProfiles->EnumLanguageProfiles(
+ aLangID, getter_AddRefs(enumLangProfiles));
+ if (FAILED(hr) || !enumLangProfiles) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFStaticSink::IsTIPCategoryKeyboard(), FAILED "
+ "to get language profiles enumerator, hr=0x%08lX",
+ this, hr));
+ return false;
+ }
+
+ TF_LANGUAGEPROFILE profile;
+ ULONG fetch = 0;
+ while (SUCCEEDED(enumLangProfiles->Next(1, &profile, &fetch)) && fetch) {
+ // XXX We're not sure a profile is registered with two or more categories.
+ if (profile.clsid == aTextService && profile.guidProfile == aProfile &&
+ profile.catid == GUID_TFCAT_TIP_KEYBOARD) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/******************************************************************/
+/* TSFTextStore */
+/******************************************************************/
+
+StaticRefPtr<ITfThreadMgr> TSFTextStore::sThreadMgr;
+StaticRefPtr<ITfMessagePump> TSFTextStore::sMessagePump;
+StaticRefPtr<ITfKeystrokeMgr> TSFTextStore::sKeystrokeMgr;
+StaticRefPtr<ITfDisplayAttributeMgr> TSFTextStore::sDisplayAttrMgr;
+StaticRefPtr<ITfCategoryMgr> TSFTextStore::sCategoryMgr;
+StaticRefPtr<ITfCompartment> TSFTextStore::sCompartmentForOpenClose;
+StaticRefPtr<ITfDocumentMgr> TSFTextStore::sDisabledDocumentMgr;
+StaticRefPtr<ITfContext> TSFTextStore::sDisabledContext;
+StaticRefPtr<ITfInputProcessorProfiles> TSFTextStore::sInputProcessorProfiles;
+StaticRefPtr<TSFTextStore> TSFTextStore::sEnabledTextStore;
+const MSG* TSFTextStore::sHandlingKeyMsg = nullptr;
+DWORD TSFTextStore::sClientId = 0;
+bool TSFTextStore::sIsKeyboardEventDispatched = false;
+
+#define TEXTSTORE_DEFAULT_VIEW (1)
+
+TSFTextStore::TSFTextStore()
+ : mEditCookie(0),
+ mSinkMask(0),
+ mLock(0),
+ mLockQueued(0),
+ mHandlingKeyMessage(0) {
+ // We hope that 5 or more actions don't occur at once.
+ mPendingActions.SetCapacity(5);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::TSFTextStore() SUCCEEDED", this));
+}
+
+TSFTextStore::~TSFTextStore() {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore instance is destroyed", this));
+}
+
+bool TSFTextStore::Init(nsWindow* aWidget, const InputContext& aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::Init(aWidget=0x%p)", this, aWidget));
+
+ if (NS_WARN_IF(!aWidget) || NS_WARN_IF(aWidget->Destroyed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED due to being initialized with "
+ "destroyed widget",
+ this));
+ return false;
+ }
+
+ if (mDocumentMgr) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED due to already initialized",
+ this));
+ return false;
+ }
+
+ mWidget = aWidget;
+ if (NS_WARN_IF(!mWidget)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED "
+ "due to aWidget is nullptr ",
+ this));
+ return false;
+ }
+ mDispatcher = mWidget->GetTextEventDispatcher();
+ if (NS_WARN_IF(!mDispatcher)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED "
+ "due to aWidget->GetTextEventDispatcher() failure",
+ this));
+ return false;
+ }
+
+ mInPrivateBrowsing = aContext.mInPrivateBrowsing;
+ SetInputScope(aContext.mHTMLInputType, aContext.mHTMLInputMode);
+
+ if (aContext.mURI) {
+ // We don't need the document URL if it fails, let's ignore the error.
+ nsAutoCString spec;
+ if (NS_SUCCEEDED(aContext.mURI->GetSpec(spec))) {
+ CopyUTF8toUTF16(spec, mDocumentURL);
+ }
+ }
+
+ // Create document manager
+ RefPtr<ITfThreadMgr> threadMgr = sThreadMgr;
+ RefPtr<ITfDocumentMgr> documentMgr;
+ HRESULT hr = threadMgr->CreateDocumentMgr(getter_AddRefs(documentMgr));
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED to create ITfDocumentMgr "
+ "(0x%08lX)",
+ this, hr));
+ return false;
+ }
+ if (NS_WARN_IF(mDestroyed)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED to create ITfDocumentMgr due to "
+ "TextStore being destroyed during calling "
+ "ITfThreadMgr::CreateDocumentMgr()",
+ this));
+ return false;
+ }
+ // Create context and add it to document manager
+ RefPtr<ITfContext> context;
+ hr = documentMgr->CreateContext(sClientId, 0,
+ static_cast<ITextStoreACP*>(this),
+ getter_AddRefs(context), &mEditCookie);
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED to create the context "
+ "(0x%08lX)",
+ this, hr));
+ return false;
+ }
+ if (NS_WARN_IF(mDestroyed)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED to create ITfContext due to "
+ "TextStore being destroyed during calling "
+ "ITfDocumentMgr::CreateContext()",
+ this));
+ return false;
+ }
+
+ hr = documentMgr->Push(context);
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED to push the context (0x%08lX)",
+ this, hr));
+ return false;
+ }
+ if (NS_WARN_IF(mDestroyed)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::Init() FAILED to create ITfContext due to "
+ "TextStore being destroyed during calling ITfDocumentMgr::Push()",
+ this));
+ documentMgr->Pop(TF_POPF_ALL);
+ return false;
+ }
+
+ mDocumentMgr = documentMgr;
+ mContext = context;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::Init() succeeded: "
+ "mDocumentMgr=0x%p, mContext=0x%p, mEditCookie=0x%08lX",
+ this, mDocumentMgr.get(), mContext.get(), mEditCookie));
+
+ return true;
+}
+
+void TSFTextStore::Destroy() {
+ if (mBeingDestroyed) {
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::Destroy(), mLock=%s, "
+ "mComposition=%s, mHandlingKeyMessage=%u",
+ this, GetLockFlagNameStr(mLock).get(),
+ ToString(mComposition).c_str(), mHandlingKeyMessage));
+
+ mDestroyed = true;
+
+ // Destroy native caret first because it's not directly related to TSF and
+ // there may be another textstore which gets focus. So, we should avoid
+ // to destroy caret after the new one recreates caret.
+ IMEHandler::MaybeDestroyNativeCaret();
+
+ if (mLock) {
+ mPendingDestroy = true;
+ return;
+ }
+
+ AutoRestore<bool> savedBeingDestroyed(mBeingDestroyed);
+ mBeingDestroyed = true;
+
+ // If there is composition, TSF keeps the composition even after the text
+ // store destroyed. So, we should clear the composition here.
+ if (mComposition.isSome()) {
+ CommitCompositionInternal(false);
+ }
+
+ if (mSink) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::Destroy(), calling "
+ "ITextStoreACPSink::OnLayoutChange(TS_LC_DESTROY)...",
+ this));
+ RefPtr<ITextStoreACPSink> sink = mSink;
+ sink->OnLayoutChange(TS_LC_DESTROY, TEXTSTORE_DEFAULT_VIEW);
+ }
+
+ // If this is called during handling a keydown or keyup message, we should
+ // put off to release TSF objects until it completely finishes since
+ // MS-IME for Japanese refers some objects without grabbing them.
+ if (!mHandlingKeyMessage) {
+ ReleaseTSFObjects();
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::Destroy() succeeded", this));
+}
+
+void TSFTextStore::ReleaseTSFObjects() {
+ MOZ_ASSERT(!mHandlingKeyMessage);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::ReleaseTSFObjects()", this));
+
+ mDocumentURL.Truncate();
+ mContext = nullptr;
+ if (mDocumentMgr) {
+ RefPtr<ITfDocumentMgr> documentMgr = mDocumentMgr.forget();
+ documentMgr->Pop(TF_POPF_ALL);
+ }
+ mSink = nullptr;
+ mWidget = nullptr;
+ mDispatcher = nullptr;
+
+ if (!mMouseTrackers.IsEmpty()) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::ReleaseTSFObjects(), "
+ "removing a mouse tracker...",
+ this));
+ mMouseTrackers.Clear();
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::ReleaseTSFObjects() completed", this));
+}
+
+STDMETHODIMP
+TSFTextStore::QueryInterface(REFIID riid, void** ppv) {
+ *ppv = nullptr;
+ if ((IID_IUnknown == riid) || (IID_ITextStoreACP == riid)) {
+ *ppv = static_cast<ITextStoreACP*>(this);
+ } else if (IID_ITfContextOwnerCompositionSink == riid) {
+ *ppv = static_cast<ITfContextOwnerCompositionSink*>(this);
+ } else if (IID_ITfMouseTrackerACP == riid) {
+ *ppv = static_cast<ITfMouseTrackerACP*>(this);
+ }
+ if (*ppv) {
+ AddRef();
+ return S_OK;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::QueryInterface() FAILED, riid=%s", this,
+ GetRIIDNameStr(riid).get()));
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP
+TSFTextStore::AdviseSink(REFIID riid, IUnknown* punk, DWORD dwMask) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::AdviseSink(riid=%s, punk=0x%p, dwMask=%s), "
+ "mSink=0x%p, mSinkMask=%s",
+ this, GetRIIDNameStr(riid).get(), punk, GetSinkMaskNameStr(dwMask).get(),
+ mSink.get(), GetSinkMaskNameStr(mSinkMask).get()));
+
+ if (!punk) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseSink() FAILED due to the null punk",
+ this));
+ return E_UNEXPECTED;
+ }
+
+ if (IID_ITextStoreACPSink != riid) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseSink() FAILED due to "
+ "unsupported interface",
+ this));
+ return E_INVALIDARG; // means unsupported interface.
+ }
+
+ if (!mSink) {
+ // Install sink
+ punk->QueryInterface(IID_ITextStoreACPSink, getter_AddRefs(mSink));
+ if (!mSink) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseSink() FAILED due to "
+ "punk not having the interface",
+ this));
+ return E_UNEXPECTED;
+ }
+ } else {
+ // If sink is already installed we check to see if they are the same
+ // Get IUnknown from both sides for comparison
+ RefPtr<IUnknown> comparison1, comparison2;
+ punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1));
+ mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2));
+ if (comparison1 != comparison2) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseSink() FAILED due to "
+ "the sink being different from the stored sink",
+ this));
+ return CONNECT_E_ADVISELIMIT;
+ }
+ }
+ // Update mask either for a new sink or an existing sink
+ mSinkMask = dwMask;
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::UnadviseSink(IUnknown* punk) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::UnadviseSink(punk=0x%p), mSink=0x%p", this, punk,
+ mSink.get()));
+
+ if (!punk) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::UnadviseSink() FAILED due to the null punk",
+ this));
+ return E_INVALIDARG;
+ }
+ if (!mSink) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::UnadviseSink() FAILED due to "
+ "any sink not stored",
+ this));
+ return CONNECT_E_NOCONNECTION;
+ }
+ // Get IUnknown from both sides for comparison
+ RefPtr<IUnknown> comparison1, comparison2;
+ punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1));
+ mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2));
+ // Unadvise only if sinks are the same
+ if (comparison1 != comparison2) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::UnadviseSink() FAILED due to "
+ "the sink being different from the stored sink",
+ this));
+ return CONNECT_E_NOCONNECTION;
+ }
+ mSink = nullptr;
+ mSinkMask = 0;
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::RequestLock(DWORD dwLockFlags, HRESULT* phrSession) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RequestLock(dwLockFlags=%s, phrSession=0x%p), "
+ "mLock=%s, mDestroyed=%s",
+ this, GetLockFlagNameStr(dwLockFlags).get(), phrSession,
+ GetLockFlagNameStr(mLock).get(), GetBoolName(mDestroyed)));
+
+ if (!mSink) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RequestLock() FAILED due to "
+ "any sink not stored",
+ this));
+ return E_FAIL;
+ }
+ if (mDestroyed &&
+ (mContentForTSF.isNothing() || mSelectionForTSF.isNothing())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RequestLock() FAILED due to "
+ "being destroyed and no information of the contents",
+ this));
+ return E_FAIL;
+ }
+ if (!phrSession) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RequestLock() FAILED due to "
+ "null phrSession",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (!mLock) {
+ // put on lock
+ mLock = dwLockFlags & (~TS_LF_SYNC);
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p Locking (%s) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>",
+ this, GetLockFlagNameStr(mLock).get()));
+ // Don't release this instance during this lock because this is called by
+ // TSF but they don't grab us during this call.
+ RefPtr<TSFTextStore> kungFuDeathGrip(this);
+ RefPtr<ITextStoreACPSink> sink = mSink;
+ *phrSession = sink->OnLockGranted(mLock);
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
+ "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<",
+ this, GetLockFlagNameStr(mLock).get()));
+ DidLockGranted();
+ while (mLockQueued) {
+ mLock = mLockQueued;
+ mLockQueued = 0;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p Locking for the request in the queue (%s) >>>>>>>>>>>>>>"
+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+ ">>>>>",
+ this, GetLockFlagNameStr(mLock).get()));
+ sink->OnLockGranted(mLock);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
+ "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
+ "<<<<<",
+ this, GetLockFlagNameStr(mLock).get()));
+ DidLockGranted();
+ }
+
+ // The document is now completely unlocked.
+ mLock = 0;
+
+ MaybeFlushPendingNotifications();
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RequestLock() succeeded: *phrSession=%s",
+ this, GetTextStoreReturnValueName(*phrSession)));
+ return S_OK;
+ }
+
+ // only time when reentrant lock is allowed is when caller holds a
+ // read-only lock and is requesting an async write lock
+ if (IsReadLocked() && !IsReadWriteLocked() && IsReadWriteLock(dwLockFlags) &&
+ !(dwLockFlags & TS_LF_SYNC)) {
+ *phrSession = TS_S_ASYNC;
+ mLockQueued = dwLockFlags & (~TS_LF_SYNC);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RequestLock() stores the request in the "
+ "queue, *phrSession=TS_S_ASYNC",
+ this));
+ return S_OK;
+ }
+
+ // no more locks allowed
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RequestLock() didn't allow to lock, "
+ "*phrSession=TS_E_SYNCHRONOUS",
+ this));
+ *phrSession = TS_E_SYNCHRONOUS;
+ return E_FAIL;
+}
+
+void TSFTextStore::DidLockGranted() {
+ if (IsReadWriteLocked()) {
+ // FreeCJ (TIP for Traditional Chinese) calls SetSelection() to set caret
+ // to the start of composition string and insert a full width space for
+ // a placeholder with a call of SetText(). After that, it calls
+ // OnUpdateComposition() without new range. Therefore, let's record the
+ // composition update information here.
+ CompleteLastActionIfStillIncomplete();
+
+ FlushPendingActions();
+ }
+
+ // If the widget has gone, we don't need to notify anything.
+ if (mDestroyed || !mWidget || mWidget->Destroyed()) {
+ mPendingSelectionChangeData.reset();
+ mHasReturnedNoLayoutError = false;
+ }
+}
+
+void TSFTextStore::DispatchEvent(WidgetGUIEvent& aEvent) {
+ if (NS_WARN_IF(!mWidget) || NS_WARN_IF(mWidget->Destroyed())) {
+ return;
+ }
+ // If the event isn't a query content event, the event may be handled
+ // asynchronously. So, we should put off to answer from GetTextExt() etc.
+ if (!aEvent.AsQueryContentEvent()) {
+ mDeferNotifyingTSFUntilNextUpdate = true;
+ }
+ mWidget->DispatchWindowEvent(aEvent);
+}
+
+void TSFTextStore::FlushPendingActions() {
+ if (!mWidget || mWidget->Destroyed()) {
+ // Note that don't clear mContentForTSF because TIP may try to commit
+ // composition with a document lock. In such case, TSFTextStore needs to
+ // behave as expected by TIP.
+ mPendingActions.Clear();
+ mPendingSelectionChangeData.reset();
+ mHasReturnedNoLayoutError = false;
+ return;
+ }
+
+ // Some TIP may request lock but does nothing during the lock. In such case,
+ // this should do nothing. For example, when MS-IME for Japanese is active
+ // and we're inactivating, this case occurs and causes different behavior
+ // from the other TIPs.
+ if (mPendingActions.IsEmpty()) {
+ return;
+ }
+
+ RefPtr<nsWindow> widget(mWidget);
+ nsresult rv = mDispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "FAILED due to BeginNativeInputTransaction() failure",
+ this));
+ return;
+ }
+ for (uint32_t i = 0; i < mPendingActions.Length(); i++) {
+ PendingAction& action = mPendingActions[i];
+ switch (action.mType) {
+ case PendingAction::Type::eKeyboardEvent:
+ if (mDestroyed) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Warning,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "IGNORED pending KeyboardEvent(%s) due to already destroyed",
+ this,
+ action.mKeyMsg.message == WM_KEYDOWN ? "eKeyDown" : "eKeyUp"));
+ }
+ MOZ_DIAGNOSTIC_ASSERT(action.mKeyMsg.message == WM_KEYDOWN ||
+ action.mKeyMsg.message == WM_KEYUP);
+ DispatchKeyboardEventAsProcessedByIME(action.mKeyMsg);
+ if (!widget || widget->Destroyed()) {
+ break;
+ }
+ break;
+ case PendingAction::Type::eCompositionStart: {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "flushing Type::eCompositionStart={ mSelectionStart=%ld, "
+ "mSelectionLength=%ld }, mDestroyed=%s",
+ this, action.mSelectionStart, action.mSelectionLength,
+ GetBoolName(mDestroyed)));
+
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "IGNORED pending compositionstart due to already destroyed",
+ this));
+ break;
+ }
+
+ if (action.mAdjustSelection) {
+ // Select composition range so the new composition replaces the range
+ WidgetSelectionEvent selectionSet(true, eSetSelection, widget);
+ widget->InitEvent(selectionSet);
+ selectionSet.mOffset = static_cast<uint32_t>(action.mSelectionStart);
+ selectionSet.mLength = static_cast<uint32_t>(action.mSelectionLength);
+ selectionSet.mReversed = false;
+ selectionSet.mExpandToClusterBoundary =
+ TSFStaticSink::ActiveTIP() !=
+ TextInputProcessorID::eKeymanDesktop &&
+ StaticPrefs::
+ intl_tsf_hack_extend_setting_selection_range_to_cluster_boundaries();
+ DispatchEvent(selectionSet);
+ if (!selectionSet.mSucceeded) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "FAILED due to eSetSelection failure",
+ this));
+ break;
+ }
+ }
+
+ // eCompositionStart always causes
+ // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. Therefore, we should
+ // wait to clear mContentForTSF until it's notified.
+ mDeferClearingContentForTSF = true;
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "dispatching compositionstart event...",
+ this));
+ WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime();
+ nsEventStatus status;
+ rv = mDispatcher->StartComposition(status, &eventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "FAILED to dispatch compositionstart event, "
+ "IsHandlingCompositionInContent()=%s",
+ this, GetBoolName(IsHandlingCompositionInContent())));
+ // XXX Is this right? If there is a composition in content,
+ // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED?
+ mDeferClearingContentForTSF = !IsHandlingCompositionInContent();
+ }
+ if (!widget || widget->Destroyed()) {
+ break;
+ }
+ break;
+ }
+ case PendingAction::Type::eCompositionUpdate: {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "flushing Type::eCompositionUpdate={ mData=\"%s\", "
+ "mRanges=0x%p, mRanges->Length()=%zu }",
+ this, GetEscapedUTF8String(action.mData).get(),
+ action.mRanges.get(),
+ action.mRanges ? action.mRanges->Length() : 0));
+
+ // eCompositionChange causes a DOM text event, the IME will be notified
+ // of NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. In this case, we
+ // should not clear mContentForTSF until we notify the IME of the
+ // composition update.
+ mDeferClearingContentForTSF = true;
+
+ rv = mDispatcher->SetPendingComposition(action.mData, action.mRanges);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "FAILED to setting pending composition... "
+ "IsHandlingCompositionInContent()=%s",
+ this, GetBoolName(IsHandlingCompositionInContent())));
+ // XXX Is this right? If there is a composition in content,
+ // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED?
+ mDeferClearingContentForTSF = !IsHandlingCompositionInContent();
+ } else {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "dispatching compositionchange event...",
+ this));
+ WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime();
+ nsEventStatus status;
+ rv = mDispatcher->FlushPendingComposition(status, &eventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "FAILED to dispatch compositionchange event, "
+ "IsHandlingCompositionInContent()=%s",
+ this, GetBoolName(IsHandlingCompositionInContent())));
+ // XXX Is this right? If there is a composition in content,
+ // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED?
+ mDeferClearingContentForTSF = !IsHandlingCompositionInContent();
+ }
+ // Be aware, the mWidget might already have been destroyed.
+ }
+ break;
+ }
+ case PendingAction::Type::eCompositionEnd: {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "flushing Type::eCompositionEnd={ mData=\"%s\" }",
+ this, GetEscapedUTF8String(action.mData).get()));
+
+ // Dispatching eCompositionCommit causes a DOM text event, then,
+ // the IME will be notified of NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED
+ // when focused content actually handles the event. For example,
+ // when focused content is in a remote process, it's sent when
+ // all dispatched composition events have been handled in the remote
+ // process. So, until then, we don't have newer content information.
+ // Therefore, we need to put off to clear mContentForTSF.
+ mDeferClearingContentForTSF = true;
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::FlushPendingActions(), "
+ "dispatching compositioncommit event...",
+ this));
+ WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime();
+ nsEventStatus status;
+ rv = mDispatcher->CommitComposition(status, &action.mData, &eventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "FAILED to dispatch compositioncommit event, "
+ "IsHandlingCompositionInContent()=%s",
+ this, GetBoolName(IsHandlingCompositionInContent())));
+ // XXX Is this right? If there is a composition in content,
+ // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED?
+ mDeferClearingContentForTSF = !IsHandlingCompositionInContent();
+ }
+ break;
+ }
+ case PendingAction::Type::eSetSelection: {
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "flushing Type::eSetSelection={ mSelectionStart=%ld, "
+ "mSelectionLength=%ld, mSelectionReversed=%s }, "
+ "mDestroyed=%s",
+ this, action.mSelectionStart, action.mSelectionLength,
+ GetBoolName(action.mSelectionReversed), GetBoolName(mDestroyed)));
+
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "IGNORED pending selectionset due to already destroyed",
+ this));
+ break;
+ }
+
+ WidgetSelectionEvent selectionSet(true, eSetSelection, widget);
+ selectionSet.mOffset = static_cast<uint32_t>(action.mSelectionStart);
+ selectionSet.mLength = static_cast<uint32_t>(action.mSelectionLength);
+ selectionSet.mReversed = action.mSelectionReversed;
+ selectionSet.mExpandToClusterBoundary =
+ TSFStaticSink::ActiveTIP() !=
+ TextInputProcessorID::eKeymanDesktop &&
+ StaticPrefs::
+ intl_tsf_hack_extend_setting_selection_range_to_cluster_boundaries();
+ DispatchEvent(selectionSet);
+ if (!selectionSet.mSucceeded) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::FlushPendingActions() "
+ "FAILED due to eSetSelection failure",
+ this));
+ break;
+ }
+ break;
+ }
+ default:
+ MOZ_CRASH("unexpected action type");
+ }
+
+ if (widget && !widget->Destroyed()) {
+ continue;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::FlushPendingActions(), "
+ "qutting since the mWidget has gone",
+ this));
+ break;
+ }
+ mPendingActions.Clear();
+}
+
+void TSFTextStore::MaybeFlushPendingNotifications() {
+ if (mDeferNotifyingTSF) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "putting off flushing pending notifications due to initializing "
+ "something...",
+ this));
+ return;
+ }
+
+ if (IsReadLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "putting off flushing pending notifications due to being the "
+ "document locked...",
+ this));
+ return;
+ }
+
+ if (mDeferCommittingComposition) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "calling TSFTextStore::CommitCompositionInternal(false)...",
+ this));
+ mDeferCommittingComposition = mDeferCancellingComposition = false;
+ CommitCompositionInternal(false);
+ } else if (mDeferCancellingComposition) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "calling TSFTextStore::CommitCompositionInternal(true)...",
+ this));
+ mDeferCommittingComposition = mDeferCancellingComposition = false;
+ CommitCompositionInternal(true);
+ }
+
+ if (mDeferNotifyingTSFUntilNextUpdate) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "putting off flushing pending notifications due to being "
+ "dispatching events...",
+ this));
+ return;
+ }
+
+ if (mPendingDestroy) {
+ Destroy();
+ return;
+ }
+
+ if (mDestroyed) {
+ // If it's already been destroyed completely, this shouldn't notify TSF of
+ // anything anymore.
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "does nothing because this has already destroyed completely...",
+ this));
+ return;
+ }
+
+ if (!mDeferClearingContentForTSF && mContentForTSF.isSome()) {
+ mContentForTSF.reset();
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "mContentForTSF is set to `Nothing`",
+ this));
+ }
+
+ // When there is no cached content, we can sync actual contents and TSF/TIP
+ // expecting contents.
+ RefPtr<TSFTextStore> kungFuDeathGrip = this;
+ Unused << kungFuDeathGrip;
+ if (mContentForTSF.isNothing()) {
+ if (mPendingTextChangeData.IsValid()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "calling TSFTextStore::NotifyTSFOfTextChange()...",
+ this));
+ NotifyTSFOfTextChange();
+ }
+ if (mPendingSelectionChangeData.isSome()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "calling TSFTextStore::NotifyTSFOfSelectionChange()...",
+ this));
+ NotifyTSFOfSelectionChange();
+ }
+ }
+
+ if (mHasReturnedNoLayoutError) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), "
+ "calling TSFTextStore::NotifyTSFOfLayoutChange()...",
+ this));
+ NotifyTSFOfLayoutChange();
+ }
+}
+
+void TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME() {
+ // If we've already been destroyed, we cannot do anything.
+ if (mDestroyed) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), "
+ "does nothing because it's already been destroyed",
+ this));
+ return;
+ }
+
+ // If we're not handling key message or we've already dispatched a keyboard
+ // event for the handling key message, we should do nothing anymore.
+ if (!sHandlingKeyMsg || sIsKeyboardEventDispatched) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), "
+ "does nothing because not necessary to dispatch keyboard event",
+ this));
+ return;
+ }
+
+ sIsKeyboardEventDispatched = true;
+ // If the document is locked, just adding the task to dispatching an event
+ // to the queue.
+ if (IsReadLocked()) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), "
+ "adding to dispatch a keyboard event into the queue...",
+ this));
+ PendingAction* action = mPendingActions.AppendElement();
+ action->mType = PendingAction::Type::eKeyboardEvent;
+ memcpy(&action->mKeyMsg, sHandlingKeyMsg, sizeof(MSG));
+ return;
+ }
+
+ // Otherwise, dispatch a keyboard event.
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), "
+ "trying to dispatch a keyboard event...",
+ this));
+ DispatchKeyboardEventAsProcessedByIME(*sHandlingKeyMsg);
+}
+
+void TSFTextStore::DispatchKeyboardEventAsProcessedByIME(const MSG& aMsg) {
+ MOZ_ASSERT(mWidget);
+ MOZ_ASSERT(!mWidget->Destroyed());
+ MOZ_ASSERT(!mDestroyed);
+
+ ModifierKeyState modKeyState;
+ MSG msg(aMsg);
+ msg.wParam = VK_PROCESSKEY;
+ NativeKey nativeKey(mWidget, msg, modKeyState);
+ switch (aMsg.message) {
+ case WM_KEYDOWN:
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), "
+ "dispatching an eKeyDown event...",
+ this));
+ nativeKey.HandleKeyDownMessage();
+ break;
+ case WM_KEYUP:
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), "
+ "dispatching an eKeyUp event...",
+ this));
+ nativeKey.HandleKeyUpMessage();
+ break;
+ default:
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), "
+ "ERROR, it doesn't handle the message",
+ this));
+ break;
+ }
+}
+
+STDMETHODIMP
+TSFTextStore::GetStatus(TS_STATUS* pdcs) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetStatus(pdcs=0x%p)", this, pdcs));
+
+ if (!pdcs) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetStatus() FAILED due to null pdcs", this));
+ return E_INVALIDARG;
+ }
+ // We manage on-screen keyboard by own.
+ pdcs->dwDynamicFlags = TS_SD_INPUTPANEMANUALDISPLAYENABLE;
+ // we use a "flat" text model for TSF support so no hidden text
+ pdcs->dwStaticFlags = TS_SS_NOHIDDENTEXT;
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::QueryInsert(LONG acpTestStart, LONG acpTestEnd, ULONG cch,
+ LONG* pacpResultStart, LONG* pacpResultEnd) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::QueryInsert(acpTestStart=%ld, "
+ "acpTestEnd=%ld, cch=%lu, pacpResultStart=0x%p, pacpResultEnd=0x%p)",
+ this, acpTestStart, acpTestEnd, cch, pacpResultStart, pacpResultEnd));
+
+ if (!pacpResultStart || !pacpResultEnd) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::QueryInsert() FAILED due to "
+ "the null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (acpTestStart < 0 || acpTestStart > acpTestEnd) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::QueryInsert() FAILED due to "
+ "wrong argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ // XXX need to adjust to cluster boundary
+ // Assume we are given good offsets for now
+ if (mComposition.isNothing() &&
+ ((StaticPrefs::
+ intl_tsf_hack_ms_traditional_chinese_query_insert_result() &&
+ TSFStaticSink::IsMSChangJieOrMSQuickActive()) ||
+ (StaticPrefs::
+ intl_tsf_hack_ms_simplified_chinese_query_insert_result() &&
+ TSFStaticSink::IsMSPinyinOrMSWubiActive()))) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p TSFTextStore::QueryInsert() WARNING using different "
+ "result for the TIP",
+ this));
+ // Chinese TIPs of Microsoft assume that QueryInsert() returns selected
+ // range which should be removed.
+ *pacpResultStart = acpTestStart;
+ *pacpResultEnd = acpTestEnd;
+ } else {
+ *pacpResultStart = acpTestStart;
+ *pacpResultEnd = acpTestStart + cch;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::QueryInsert() succeeded: "
+ "*pacpResultStart=%ld, *pacpResultEnd=%ld)",
+ this, *pacpResultStart, *pacpResultEnd));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::GetSelection(ULONG ulIndex, ULONG ulCount,
+ TS_SELECTION_ACP* pSelection, ULONG* pcFetched) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetSelection(ulIndex=%lu, ulCount=%lu, "
+ "pSelection=0x%p, pcFetched=0x%p)",
+ this, ulIndex, ulCount, pSelection, pcFetched));
+
+ if (!IsReadLocked()) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetSelection() FAILED due to not locked", this));
+ return TS_E_NOLOCK;
+ }
+ if (!ulCount || !pSelection || !pcFetched) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetSelection() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ *pcFetched = 0;
+
+ if (ulIndex != static_cast<ULONG>(TS_DEFAULT_SELECTION) && ulIndex != 0) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetSelection() FAILED due to "
+ "unsupported selection",
+ this));
+ return TS_E_NOSELECTION;
+ }
+
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (selectionForTSF.isNothing()) {
+ if (DoNotReturnErrorFromGetSelection()) {
+ *pSelection = Selection::EmptyACP();
+ *pcFetched = 1;
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetSelection() returns fake selection range "
+ "for avoiding a crash in TSF, *pSelection=%s",
+ this, mozilla::ToString(*pSelection).c_str()));
+ return S_OK;
+ }
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetSelection() FAILED due to "
+ "SelectionForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+ if (!selectionForTSF->HasRange()) {
+ *pSelection = Selection::EmptyACP();
+ *pcFetched = 0;
+ return TS_E_NOSELECTION;
+ }
+ *pSelection = selectionForTSF->ACPRef();
+ *pcFetched = 1;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetSelection() succeeded, *pSelection=%s",
+ this, mozilla::ToString(*pSelection).c_str()));
+ return S_OK;
+}
+
+// static
+bool TSFTextStore::DoNotReturnErrorFromGetSelection() {
+ // There is a crash bug of TSF if we return error from GetSelection().
+ // That was introduced in Anniversary Update (build 14393, see bug 1312302)
+ // TODO: We should avoid to run this hack on fixed builds. When we get
+ // exact build number, we should get back here.
+ static bool sTSFMayCrashIfGetSelectionReturnsError =
+ IsWin10AnniversaryUpdateOrLater();
+ return sTSFMayCrashIfGetSelectionReturnsError;
+}
+
+Maybe<TSFTextStore::Content>& TSFTextStore::ContentForTSF() {
+ // This should be called when the document is locked or the content hasn't
+ // been abandoned yet.
+ if (NS_WARN_IF(!IsReadLocked() && mContentForTSF.isNothing())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to "
+ "called wrong timing, IsReadLocked()=%s, mContentForTSF=Nothing",
+ this, GetBoolName(IsReadLocked())));
+ return mContentForTSF;
+ }
+
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (selectionForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to "
+ "SelectionForTSF() failure",
+ this));
+ mContentForTSF.reset();
+ return mContentForTSF;
+ }
+
+ if (mContentForTSF.isNothing()) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !mIsInitializingContentForTSF,
+ "TSFTextStore::ContentForTSF() shouldn't be called recursively");
+
+ AutoNotifyingTSFBatch deferNotifyingTSF(*this);
+ AutoRestore<bool> saveInitializingContetTSF(mIsInitializingContentForTSF);
+ mIsInitializingContentForTSF = true;
+
+ nsString text; // Don't use auto string for avoiding to copy long string.
+ if (NS_WARN_IF(!GetCurrentText(text))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to "
+ "GetCurrentText() failure",
+ this));
+ return mContentForTSF;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mContentForTSF.isNothing(),
+ "How was it initialized recursively?");
+ mContentForTSF.reset(); // For avoiding crash in release channel
+ mContentForTSF.emplace(*this, text);
+ // Basically, the cached content which is expected by TSF/TIP should be
+ // cleared after active composition is committed or the document lock is
+ // unlocked. However, in e10s mode, content will be modified
+ // asynchronously. In such case, mDeferClearingContentForTSF may be
+ // true until whole dispatched events are handled by the focused editor.
+ mDeferClearingContentForTSF = false;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::ContentForTSF(): mContentForTSF=%s", this,
+ mozilla::ToString(mContentForTSF).c_str()));
+
+ return mContentForTSF;
+}
+
+bool TSFTextStore::CanAccessActualContentDirectly() const {
+ if (mContentForTSF.isNothing() || mSelectionForTSF.isNothing()) {
+ return true;
+ }
+
+ // If the cached content has been changed by something except composition,
+ // the content cache may be different from actual content.
+ if (mPendingTextChangeData.IsValid() &&
+ !mPendingTextChangeData.mCausedOnlyByComposition) {
+ return false;
+ }
+
+ // If the cached selection isn't changed, cached content and actual content
+ // should be same.
+ if (mPendingSelectionChangeData.isNothing()) {
+ return true;
+ }
+
+ return mSelectionForTSF->EqualsExceptDirection(*mPendingSelectionChangeData);
+}
+
+bool TSFTextStore::GetCurrentText(nsAString& aTextContent) {
+ if (mContentForTSF.isSome()) {
+ aTextContent = mContentForTSF->TextRef();
+ return true;
+ }
+
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(mWidget && !mWidget->Destroyed());
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::GetCurrentText(): "
+ "retrieving text from the content...",
+ this));
+
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ mWidget);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ mWidget->InitEvent(queryTextContentEvent);
+ DispatchEvent(queryTextContentEvent);
+ if (NS_WARN_IF(queryTextContentEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetCurrentText(), FAILED, due to "
+ "eQueryTextContent failure",
+ this));
+ aTextContent.Truncate();
+ return false;
+ }
+
+ aTextContent = queryTextContentEvent.mReply->DataRef();
+ return true;
+}
+
+Maybe<TSFTextStore::Selection>& TSFTextStore::SelectionForTSF() {
+ if (mSelectionForTSF.isNothing()) {
+ MOZ_ASSERT(!mDestroyed);
+ // If the window has never been available, we should crash since working
+ // with broken values may make TIP confused.
+ if (!mWidget || mWidget->Destroyed()) {
+ MOZ_ASSERT_UNREACHABLE("There should be non-destroyed widget");
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ !mIsInitializingSelectionForTSF,
+ "TSFTextStore::SelectionForTSF() shouldn't be called recursively");
+
+ AutoNotifyingTSFBatch deferNotifyingTSF(*this);
+ AutoRestore<bool> saveInitializingSelectionForTSF(
+ mIsInitializingSelectionForTSF);
+ mIsInitializingSelectionForTSF = true;
+
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ mWidget);
+ mWidget->InitEvent(querySelectedTextEvent);
+ DispatchEvent(querySelectedTextEvent);
+ if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
+ return mSelectionForTSF;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mSelectionForTSF.isNothing(),
+ "How was it initialized recursively?");
+ mSelectionForTSF = Some(Selection(querySelectedTextEvent));
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::SelectionForTSF() succeeded, "
+ "mSelectionForTSF=%s",
+ this, ToString(mSelectionForTSF).c_str()));
+
+ return mSelectionForTSF;
+}
+
+static HRESULT GetRangeExtent(ITfRange* aRange, LONG* aStart, LONG* aLength) {
+ RefPtr<ITfRangeACP> rangeACP;
+ aRange->QueryInterface(IID_ITfRangeACP, getter_AddRefs(rangeACP));
+ NS_ENSURE_TRUE(rangeACP, E_FAIL);
+ return rangeACP->GetExtent(aStart, aLength);
+}
+
+static TextRangeType GetGeckoSelectionValue(TF_DISPLAYATTRIBUTE& aDisplayAttr) {
+ switch (aDisplayAttr.bAttr) {
+ case TF_ATTR_TARGET_CONVERTED:
+ return TextRangeType::eSelectedClause;
+ case TF_ATTR_CONVERTED:
+ return TextRangeType::eConvertedClause;
+ case TF_ATTR_TARGET_NOTCONVERTED:
+ return TextRangeType::eSelectedRawClause;
+ default:
+ return TextRangeType::eRawClause;
+ }
+}
+
+HRESULT
+TSFTextStore::GetDisplayAttribute(ITfProperty* aAttrProperty, ITfRange* aRange,
+ TF_DISPLAYATTRIBUTE* aResult) {
+ NS_ENSURE_TRUE(aAttrProperty, E_FAIL);
+ NS_ENSURE_TRUE(aRange, E_FAIL);
+ NS_ENSURE_TRUE(aResult, E_FAIL);
+
+ HRESULT hr;
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Debug)) {
+ LONG start = 0, length = 0;
+ hr = GetRangeExtent(aRange, &start, &length);
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::GetDisplayAttribute(): "
+ "GetDisplayAttribute range=%ld-%ld (hr=%s)",
+ this, start - mComposition->StartOffset(),
+ start - mComposition->StartOffset() + length,
+ GetCommonReturnValueName(hr)));
+ }
+
+ VARIANT propValue;
+ ::VariantInit(&propValue);
+ hr = aAttrProperty->GetValue(TfEditCookie(mEditCookie), aRange, &propValue);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to "
+ "ITfProperty::GetValue() failed",
+ this));
+ return hr;
+ }
+ if (VT_I4 != propValue.vt) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to "
+ "ITfProperty::GetValue() returns non-VT_I4 value",
+ this));
+ ::VariantClear(&propValue);
+ return E_FAIL;
+ }
+
+ RefPtr<ITfCategoryMgr> categoryMgr = GetCategoryMgr();
+ if (NS_WARN_IF(!categoryMgr)) {
+ return E_FAIL;
+ }
+ GUID guid;
+ hr = categoryMgr->GetGUID(DWORD(propValue.lVal), &guid);
+ ::VariantClear(&propValue);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to "
+ "ITfCategoryMgr::GetGUID() failed",
+ this));
+ return hr;
+ }
+
+ RefPtr<ITfDisplayAttributeMgr> displayAttrMgr = GetDisplayAttributeMgr();
+ if (NS_WARN_IF(!displayAttrMgr)) {
+ return E_FAIL;
+ }
+ RefPtr<ITfDisplayAttributeInfo> info;
+ hr = displayAttrMgr->GetDisplayAttributeInfo(guid, getter_AddRefs(info),
+ nullptr);
+ if (FAILED(hr) || !info) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to "
+ "ITfDisplayAttributeMgr::GetDisplayAttributeInfo() failed",
+ this));
+ return hr;
+ }
+
+ hr = info->GetAttributeInfo(aResult);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to "
+ "ITfDisplayAttributeInfo::GetAttributeInfo() failed",
+ this));
+ return hr;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::GetDisplayAttribute() succeeded: "
+ "Result={ %s }",
+ this, GetDisplayAttrStr(*aResult).get()));
+ return S_OK;
+}
+
+HRESULT
+TSFTextStore::RestartCompositionIfNecessary(ITfRange* aRangeNew) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RestartCompositionIfNecessary("
+ "aRangeNew=0x%p), mComposition=%s",
+ this, aRangeNew, ToString(mComposition).c_str()));
+
+ if (mComposition.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED "
+ "due to no composition view",
+ this));
+ return E_FAIL;
+ }
+
+ HRESULT hr;
+ RefPtr<ITfCompositionView> pComposition(mComposition->GetView());
+ RefPtr<ITfRange> composingRange(aRangeNew);
+ if (!composingRange) {
+ hr = pComposition->GetRange(getter_AddRefs(composingRange));
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RestartCompositionIfNecessary() "
+ "FAILED due to pComposition->GetRange() failure",
+ this));
+ return hr;
+ }
+ }
+
+ // Get starting offset of the composition
+ LONG compStart = 0, compLength = 0;
+ hr = GetRangeExtent(composingRange, &compStart, &compLength);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED "
+ "due to GetRangeExtent() failure",
+ this));
+ return hr;
+ }
+
+ if (mComposition->StartOffset() == compStart &&
+ mComposition->Length() == compLength) {
+ return S_OK;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RestartCompositionIfNecessary(), "
+ "restaring composition because of compostion range is changed "
+ "(range=%ld-%ld, mComposition=%s)",
+ this, compStart, compStart + compLength,
+ ToString(mComposition).c_str()));
+
+ // If the queried composition length is different from the length
+ // of our composition string, OnUpdateComposition is being called
+ // because a part of the original composition was committed.
+ hr = RestartComposition(*mComposition, pComposition, composingRange);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RestartCompositionIfNecessary() "
+ "FAILED due to RestartComposition() failure",
+ this));
+ return hr;
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RestartCompositionIfNecessary() succeeded", this));
+ return S_OK;
+}
+
+HRESULT TSFTextStore::RestartComposition(Composition& aCurrentComposition,
+ ITfCompositionView* aCompositionView,
+ ITfRange* aNewRange) {
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+
+ LONG newStart, newLength;
+ HRESULT hr = GetRangeExtent(aNewRange, &newStart, &newLength);
+ LONG newEnd = newStart + newLength;
+
+ if (selectionForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RestartComposition() FAILED "
+ "due to SelectionForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RestartComposition() FAILED "
+ "due to GetRangeExtent() failure",
+ this));
+ return hr;
+ }
+
+ // If the new range has no overlap with the crrent range, we just commit
+ // the composition and restart new composition with the new range but
+ // current selection range should be preserved.
+ if (newStart >= aCurrentComposition.EndOffset() ||
+ newEnd <= aCurrentComposition.StartOffset()) {
+ RecordCompositionEndAction();
+ RecordCompositionStartAction(aCompositionView, newStart, newLength, true);
+ return S_OK;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RestartComposition(aCompositionView=0x%p, "
+ "aNewRange=0x%p { newStart=%ld, newLength=%ld }), "
+ "aCurrentComposition=%s, "
+ "selectionForTSF=%s",
+ this, aCompositionView, aNewRange, newStart, newLength,
+ ToString(aCurrentComposition).c_str(),
+ ToString(selectionForTSF).c_str()));
+
+ // If the new range has an overlap with the current one, we should not commit
+ // the whole current range to avoid creating an odd undo transaction.
+ // I.e., the overlapped range which is being composed should not appear in
+ // undo transaction.
+
+ // Backup current composition data and selection data.
+ Composition oldComposition = aCurrentComposition;
+ Selection oldSelection = *selectionForTSF;
+
+ // Commit only the part of composition.
+ LONG keepComposingStartOffset =
+ std::max(oldComposition.StartOffset(), newStart);
+ LONG keepComposingEndOffset = std::min(oldComposition.EndOffset(), newEnd);
+ MOZ_ASSERT(
+ keepComposingStartOffset <= keepComposingEndOffset,
+ "Why keepComposingEndOffset is smaller than keepComposingStartOffset?");
+ LONG keepComposingLength = keepComposingEndOffset - keepComposingStartOffset;
+ // Remove the overlapped part from the commit string.
+ nsAutoString commitString(oldComposition.DataRef());
+ commitString.Cut(keepComposingStartOffset - oldComposition.StartOffset(),
+ keepComposingLength);
+ // Update the composition string.
+ Maybe<Content>& contentForTSF = ContentForTSF();
+ if (contentForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RestartComposition() FAILED "
+ "due to ContentForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+ contentForTSF->ReplaceTextWith(oldComposition.StartOffset(),
+ oldComposition.Length(), commitString);
+ MOZ_ASSERT(mComposition.isSome());
+ // Record a compositionupdate action for commit the part of composing string.
+ PendingAction* action = LastOrNewPendingCompositionUpdate();
+ if (mComposition.isSome()) {
+ action->mData = mComposition->DataRef();
+ }
+ action->mRanges->Clear();
+ // Note that we shouldn't append ranges when composition string
+ // is empty because it may cause TextComposition confused.
+ if (!action->mData.IsEmpty()) {
+ TextRange caretRange;
+ caretRange.mStartOffset = caretRange.mEndOffset = static_cast<uint32_t>(
+ oldComposition.StartOffset() + commitString.Length());
+ caretRange.mRangeType = TextRangeType::eCaret;
+ action->mRanges->AppendElement(caretRange);
+ }
+ action->mIncomplete = false;
+
+ // Record compositionend action.
+ RecordCompositionEndAction();
+
+ // Record compositionstart action only with the new start since this method
+ // hasn't restored composing string yet.
+ RecordCompositionStartAction(aCompositionView, newStart, 0, false);
+
+ // Restore the latest text content and selection.
+ contentForTSF->ReplaceSelectedTextWith(nsDependentSubstring(
+ oldComposition.DataRef(),
+ keepComposingStartOffset - oldComposition.StartOffset(),
+ keepComposingLength));
+ selectionForTSF = Some(oldSelection);
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RestartComposition() succeeded, "
+ "mComposition=%s, selectionForTSF=%s",
+ this, ToString(mComposition).c_str(),
+ ToString(selectionForTSF).c_str()));
+
+ return S_OK;
+}
+
+static bool GetColor(const TF_DA_COLOR& aTSFColor, nscolor& aResult) {
+ switch (aTSFColor.type) {
+ case TF_CT_SYSCOLOR: {
+ DWORD sysColor = ::GetSysColor(aTSFColor.nIndex);
+ aResult =
+ NS_RGB(GetRValue(sysColor), GetGValue(sysColor), GetBValue(sysColor));
+ return true;
+ }
+ case TF_CT_COLORREF:
+ aResult = NS_RGB(GetRValue(aTSFColor.cr), GetGValue(aTSFColor.cr),
+ GetBValue(aTSFColor.cr));
+ return true;
+ case TF_CT_NONE:
+ default:
+ return false;
+ }
+}
+
+static bool GetLineStyle(TF_DA_LINESTYLE aTSFLineStyle,
+ TextRangeStyle::LineStyle& aTextRangeLineStyle) {
+ switch (aTSFLineStyle) {
+ case TF_LS_NONE:
+ aTextRangeLineStyle = TextRangeStyle::LineStyle::None;
+ return true;
+ case TF_LS_SOLID:
+ aTextRangeLineStyle = TextRangeStyle::LineStyle::Solid;
+ return true;
+ case TF_LS_DOT:
+ aTextRangeLineStyle = TextRangeStyle::LineStyle::Dotted;
+ return true;
+ case TF_LS_DASH:
+ aTextRangeLineStyle = TextRangeStyle::LineStyle::Dashed;
+ return true;
+ case TF_LS_SQUIGGLE:
+ aTextRangeLineStyle = TextRangeStyle::LineStyle::Wavy;
+ return true;
+ default:
+ return false;
+ }
+}
+
+HRESULT
+TSFTextStore::RecordCompositionUpdateAction() {
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction(), mComposition=%s",
+ this, ToString(mComposition).c_str()));
+
+ if (mComposition.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED "
+ "due to no composition view",
+ this));
+ return E_FAIL;
+ }
+
+ // Getting display attributes is *really* complicated!
+ // We first get the context and the property objects to query for
+ // attributes, but since a big range can have a variety of values for
+ // the attribute, we have to find out all the ranges that have distinct
+ // attribute values. Then we query for what the value represents through
+ // the display attribute manager and translate that to TextRange to be
+ // sent in eCompositionChange
+
+ RefPtr<ITfProperty> attrPropetry;
+ HRESULT hr =
+ mContext->GetProperty(GUID_PROP_ATTRIBUTE, getter_AddRefs(attrPropetry));
+ if (FAILED(hr) || !attrPropetry) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED "
+ "due to mContext->GetProperty() failure",
+ this));
+ return FAILED(hr) ? hr : E_FAIL;
+ }
+
+ RefPtr<ITfRange> composingRange;
+ hr = mComposition->GetView()->GetRange(getter_AddRefs(composingRange));
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() "
+ "FAILED due to mComposition->GetView()->GetRange() failure",
+ this));
+ return hr;
+ }
+
+ RefPtr<IEnumTfRanges> enumRanges;
+ hr = attrPropetry->EnumRanges(TfEditCookie(mEditCookie),
+ getter_AddRefs(enumRanges), composingRange);
+ if (FAILED(hr) || !enumRanges) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED "
+ "due to attrPropetry->EnumRanges() failure",
+ this));
+ return FAILED(hr) ? hr : E_FAIL;
+ }
+
+ // First, put the log of content and selection here.
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (selectionForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED "
+ "due to SelectionForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+
+ PendingAction* action = LastOrNewPendingCompositionUpdate();
+ action->mData = mComposition->DataRef();
+ // The ranges might already have been initialized, however, if this is
+ // called again, that means we need to overwrite the ranges with current
+ // information.
+ action->mRanges->Clear();
+
+ // Note that we shouldn't append ranges when composition string
+ // is empty because it may cause TextComposition confused.
+ if (!action->mData.IsEmpty()) {
+ TextRange newRange;
+ // No matter if we have display attribute info or not,
+ // we always pass in at least one range to eCompositionChange
+ newRange.mStartOffset = 0;
+ newRange.mEndOffset = action->mData.Length();
+ newRange.mRangeType = TextRangeType::eRawClause;
+ action->mRanges->AppendElement(newRange);
+
+ RefPtr<ITfRange> range;
+ while (enumRanges->Next(1, getter_AddRefs(range), nullptr) == S_OK) {
+ if (NS_WARN_IF(!range)) {
+ break;
+ }
+
+ LONG rangeStart = 0, rangeLength = 0;
+ if (FAILED(GetRangeExtent(range, &rangeStart, &rangeLength))) {
+ continue;
+ }
+ // The range may include out of composition string. We should ignore
+ // outside of the composition string.
+ LONG start = std::min(std::max(rangeStart, mComposition->StartOffset()),
+ mComposition->EndOffset());
+ LONG end = std::max(
+ std::min(rangeStart + rangeLength, mComposition->EndOffset()),
+ mComposition->StartOffset());
+ LONG length = end - start;
+ if (length < 0) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() "
+ "ignores invalid range (%ld-%ld)",
+ this, rangeStart - mComposition->StartOffset(),
+ rangeStart - mComposition->StartOffset() + rangeLength));
+ continue;
+ }
+ if (!length) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() "
+ "ignores a range due to outside of the composition or empty "
+ "(%ld-%ld)",
+ this, rangeStart - mComposition->StartOffset(),
+ rangeStart - mComposition->StartOffset() + rangeLength));
+ continue;
+ }
+
+ TextRange newRange;
+ newRange.mStartOffset =
+ static_cast<uint32_t>(start - mComposition->StartOffset());
+ // The end of the last range in the array is
+ // always kept at the end of composition
+ newRange.mEndOffset = mComposition->Length();
+
+ TF_DISPLAYATTRIBUTE attr;
+ hr = GetDisplayAttribute(attrPropetry, range, &attr);
+ if (FAILED(hr)) {
+ newRange.mRangeType = TextRangeType::eRawClause;
+ } else {
+ newRange.mRangeType = GetGeckoSelectionValue(attr);
+ if (GetColor(attr.crText, newRange.mRangeStyle.mForegroundColor)) {
+ newRange.mRangeStyle.mDefinedStyles |=
+ TextRangeStyle::DEFINED_FOREGROUND_COLOR;
+ }
+ if (GetColor(attr.crBk, newRange.mRangeStyle.mBackgroundColor)) {
+ newRange.mRangeStyle.mDefinedStyles |=
+ TextRangeStyle::DEFINED_BACKGROUND_COLOR;
+ }
+ if (GetColor(attr.crLine, newRange.mRangeStyle.mUnderlineColor)) {
+ newRange.mRangeStyle.mDefinedStyles |=
+ TextRangeStyle::DEFINED_UNDERLINE_COLOR;
+ }
+ if (GetLineStyle(attr.lsStyle, newRange.mRangeStyle.mLineStyle)) {
+ newRange.mRangeStyle.mDefinedStyles |=
+ TextRangeStyle::DEFINED_LINESTYLE;
+ newRange.mRangeStyle.mIsBoldLine = attr.fBoldLine != 0;
+ }
+ }
+
+ TextRange& lastRange = action->mRanges->LastElement();
+ if (lastRange.mStartOffset == newRange.mStartOffset) {
+ // Replace range if last range is the same as this one
+ // So that ranges don't overlap and confuse the editor
+ lastRange = newRange;
+ } else {
+ lastRange.mEndOffset = newRange.mStartOffset;
+ action->mRanges->AppendElement(newRange);
+ }
+ }
+
+ // We need to hack for Korean Input System which is Korean standard TIP.
+ // It sets no change style to IME selection (the selection is always only
+ // one). So, the composition string looks like normal (or committed)
+ // string. At this time, current selection range is same as the
+ // composition string range. Other applications set a wide caret which
+ // covers the composition string, however, Gecko doesn't support the wide
+ // caret drawing now (Gecko doesn't support XOR drawing), unfortunately.
+ // For now, we should change the range style to undefined.
+ if (!selectionForTSF->Collapsed() && action->mRanges->Length() == 1) {
+ TextRange& range = action->mRanges->ElementAt(0);
+ LONG start = selectionForTSF->MinOffset();
+ LONG end = selectionForTSF->MaxOffset();
+ if (static_cast<LONG>(range.mStartOffset) ==
+ start - mComposition->StartOffset() &&
+ static_cast<LONG>(range.mEndOffset) ==
+ end - mComposition->StartOffset() &&
+ range.mRangeStyle.IsNoChangeStyle()) {
+ range.mRangeStyle.Clear();
+ // The looks of selected type is better than others.
+ range.mRangeType = TextRangeType::eSelectedRawClause;
+ }
+ }
+
+ // The caret position has to be collapsed.
+ uint32_t caretPosition = static_cast<uint32_t>(
+ selectionForTSF->HasRange()
+ ? selectionForTSF->MaxOffset() - mComposition->StartOffset()
+ : mComposition->StartOffset());
+
+ // If caret is in the target clause and it doesn't have specific style,
+ // the target clause will be painted as normal selection range. Since
+ // caret shouldn't be in selection range on Windows, we shouldn't append
+ // caret range in such case.
+ const TextRange* targetClause = action->mRanges->GetTargetClause();
+ if (!targetClause || targetClause->mRangeStyle.IsDefined() ||
+ caretPosition < targetClause->mStartOffset ||
+ caretPosition > targetClause->mEndOffset) {
+ TextRange caretRange;
+ caretRange.mStartOffset = caretRange.mEndOffset = caretPosition;
+ caretRange.mRangeType = TextRangeType::eCaret;
+ action->mRanges->AppendElement(caretRange);
+ }
+ }
+
+ action->mIncomplete = false;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RecordCompositionUpdateAction() "
+ "succeeded",
+ this));
+
+ return S_OK;
+}
+
+HRESULT
+TSFTextStore::SetSelectionInternal(const TS_SELECTION_ACP* pSelection,
+ bool aDispatchCompositionChangeEvent) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::SetSelectionInternal(pSelection=%s, "
+ "aDispatchCompositionChangeEvent=%s), mComposition=%s",
+ this, pSelection ? mozilla::ToString(*pSelection).c_str() : "nullptr",
+ GetBoolName(aDispatchCompositionChangeEvent),
+ ToString(mComposition).c_str()));
+
+ MOZ_ASSERT(IsReadWriteLocked());
+
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (selectionForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to "
+ "SelectionForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to "
+ "destroyed during dispatching a keyboard event",
+ this));
+ return E_FAIL;
+ }
+
+ // If actually the range is not changing, we should do nothing.
+ // Perhaps, we can ignore the difference change because it must not be
+ // important for following edit.
+ if (selectionForTSF->EqualsExceptDirection(*pSelection)) {
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p TSFTextStore::SetSelectionInternal() Succeeded but "
+ "did nothing because the selection range isn't changing",
+ this));
+ selectionForTSF->SetSelection(*pSelection);
+ return S_OK;
+ }
+
+ if (mComposition.isSome()) {
+ if (aDispatchCompositionChangeEvent) {
+ HRESULT hr = RestartCompositionIfNecessary();
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to "
+ "RestartCompositionIfNecessary() failure",
+ this));
+ return hr;
+ }
+ }
+ if (pSelection->acpStart < mComposition->StartOffset() ||
+ pSelection->acpEnd > mComposition->EndOffset()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to "
+ "the selection being out of the composition string",
+ this));
+ return TS_E_INVALIDPOS;
+ }
+ // Emulate selection during compositions
+ selectionForTSF->SetSelection(*pSelection);
+ if (aDispatchCompositionChangeEvent) {
+ HRESULT hr = RecordCompositionUpdateAction();
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to "
+ "RecordCompositionUpdateAction() failure",
+ this));
+ return hr;
+ }
+ }
+ return S_OK;
+ }
+
+ TS_SELECTION_ACP selectionInContent(*pSelection);
+
+ // If mContentForTSF caches old contents which is now different from
+ // actual contents, we need some complicated hack here...
+ // Note that this hack assumes that this is used for reconversion.
+ if (mContentForTSF.isSome() && mPendingTextChangeData.IsValid() &&
+ !mPendingTextChangeData.mCausedOnlyByComposition) {
+ uint32_t startOffset = static_cast<uint32_t>(selectionInContent.acpStart);
+ uint32_t endOffset = static_cast<uint32_t>(selectionInContent.acpEnd);
+ if (mPendingTextChangeData.mStartOffset >= endOffset) {
+ // Setting selection before any changed ranges is fine.
+ } else if (mPendingTextChangeData.mRemovedEndOffset <= startOffset) {
+ // Setting selection after removed range is fine with following
+ // adjustment.
+ selectionInContent.acpStart += mPendingTextChangeData.Difference();
+ selectionInContent.acpEnd += mPendingTextChangeData.Difference();
+ } else if (startOffset == endOffset) {
+ // Moving caret position may be fine in most cases even if the insertion
+ // point has already gone but in this case, composition will be inserted
+ // to unexpected position, though.
+ // It seems that moving caret into middle of the new text is odd.
+ // Perhaps, end of it is expected by users in most cases.
+ selectionInContent.acpStart = mPendingTextChangeData.mAddedEndOffset;
+ selectionInContent.acpEnd = selectionInContent.acpStart;
+ } else {
+ // Otherwise, i.e., setting range has already gone, we cannot set
+ // selection properly.
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to "
+ "there is unknown content change",
+ this));
+ return E_FAIL;
+ }
+ }
+
+ CompleteLastActionIfStillIncomplete();
+ PendingAction* action = mPendingActions.AppendElement();
+ action->mType = PendingAction::Type::eSetSelection;
+ action->mSelectionStart = selectionInContent.acpStart;
+ action->mSelectionLength =
+ selectionInContent.acpEnd - selectionInContent.acpStart;
+ action->mSelectionReversed = (selectionInContent.style.ase == TS_AE_START);
+
+ // Use TSF specified selection for updating mSelectionForTSF.
+ selectionForTSF->SetSelection(*pSelection);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::SetSelection(ULONG ulCount, const TS_SELECTION_ACP* pSelection) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::SetSelection(ulCount=%lu, pSelection=%s }), "
+ "mComposition=%s",
+ this, ulCount,
+ pSelection ? mozilla::ToString(pSelection).c_str() : "nullptr",
+ ToString(mComposition).c_str()));
+
+ if (!IsReadWriteLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelection() FAILED due to "
+ "not locked (read-write)",
+ this));
+ return TS_E_NOLOCK;
+ }
+ if (ulCount != 1) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelection() FAILED due to "
+ "trying setting multiple selection",
+ this));
+ return E_INVALIDARG;
+ }
+ if (!pSelection) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelection() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ HRESULT hr = SetSelectionInternal(pSelection, true);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetSelection() FAILED due to "
+ "SetSelectionInternal() failure",
+ this));
+ } else {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::SetSelection() succeeded", this));
+ }
+ return hr;
+}
+
+STDMETHODIMP
+TSFTextStore::GetText(LONG acpStart, LONG acpEnd, WCHAR* pchPlain,
+ ULONG cchPlainReq, ULONG* pcchPlainOut,
+ TS_RUNINFO* prgRunInfo, ULONG ulRunInfoReq,
+ ULONG* pulRunInfoOut, LONG* pacpNext) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetText(acpStart=%ld, acpEnd=%ld, pchPlain=0x%p, "
+ "cchPlainReq=%lu, pcchPlainOut=0x%p, prgRunInfo=0x%p, ulRunInfoReq=%lu, "
+ "pulRunInfoOut=0x%p, pacpNext=0x%p), mComposition=%s",
+ this, acpStart, acpEnd, pchPlain, cchPlainReq, pcchPlainOut, prgRunInfo,
+ ulRunInfoReq, pulRunInfoOut, pacpNext, ToString(mComposition).c_str()));
+
+ if (!IsReadLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetText() FAILED due to "
+ "not locked (read)",
+ this));
+ return TS_E_NOLOCK;
+ }
+
+ if (!pcchPlainOut || (!pchPlain && !prgRunInfo) ||
+ !cchPlainReq != !pchPlain || !ulRunInfoReq != !prgRunInfo) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetText() FAILED due to "
+ "invalid argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (acpStart < 0 || acpEnd < -1 || (acpEnd != -1 && acpStart > acpEnd)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetText() FAILED due to "
+ "invalid position",
+ this));
+ return TS_E_INVALIDPOS;
+ }
+
+ // Making sure to null-terminate string just to be on the safe side
+ *pcchPlainOut = 0;
+ if (pchPlain && cchPlainReq) *pchPlain = 0;
+ if (pulRunInfoOut) *pulRunInfoOut = 0;
+ if (pacpNext) *pacpNext = acpStart;
+ if (prgRunInfo && ulRunInfoReq) {
+ prgRunInfo->uCount = 0;
+ prgRunInfo->type = TS_RT_PLAIN;
+ }
+
+ Maybe<Content>& contentForTSF = ContentForTSF();
+ if (contentForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetText() FAILED due to "
+ "ContentForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+ if (contentForTSF->TextRef().Length() < static_cast<uint32_t>(acpStart)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetText() FAILED due to "
+ "acpStart is larger offset than the actual text length",
+ this));
+ return TS_E_INVALIDPOS;
+ }
+ if (acpEnd != -1 &&
+ contentForTSF->TextRef().Length() < static_cast<uint32_t>(acpEnd)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetText() FAILED due to "
+ "acpEnd is larger offset than the actual text length",
+ this));
+ return TS_E_INVALIDPOS;
+ }
+ uint32_t length = (acpEnd == -1) ? contentForTSF->TextRef().Length() -
+ static_cast<uint32_t>(acpStart)
+ : static_cast<uint32_t>(acpEnd - acpStart);
+ if (cchPlainReq && cchPlainReq - 1 < length) {
+ length = cchPlainReq - 1;
+ }
+ if (length) {
+ if (pchPlain && cchPlainReq) {
+ const char16_t* startChar =
+ contentForTSF->TextRef().BeginReading() + acpStart;
+ memcpy(pchPlain, startChar, length * sizeof(*pchPlain));
+ pchPlain[length] = 0;
+ *pcchPlainOut = length;
+ }
+ if (prgRunInfo && ulRunInfoReq) {
+ prgRunInfo->uCount = length;
+ prgRunInfo->type = TS_RT_PLAIN;
+ if (pulRunInfoOut) *pulRunInfoOut = 1;
+ }
+ if (pacpNext) *pacpNext = acpStart + length;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetText() succeeded: pcchPlainOut=0x%p, "
+ "*prgRunInfo={ uCount=%lu, type=%s }, *pulRunInfoOut=%lu, "
+ "*pacpNext=%ld)",
+ this, pcchPlainOut, prgRunInfo ? prgRunInfo->uCount : 0,
+ prgRunInfo ? GetTextRunTypeName(prgRunInfo->type) : "N/A",
+ pulRunInfoOut ? *pulRunInfoOut : 0, pacpNext ? *pacpNext : 0));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::SetText(DWORD dwFlags, LONG acpStart, LONG acpEnd,
+ const WCHAR* pchText, ULONG cch, TS_TEXTCHANGE* pChange) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::SetText(dwFlags=%s, acpStart=%ld, acpEnd=%ld, "
+ "pchText=0x%p \"%s\", cch=%lu, pChange=0x%p), mComposition=%s",
+ this, dwFlags == TS_ST_CORRECTION ? "TS_ST_CORRECTION" : "not-specified",
+ acpStart, acpEnd, pchText,
+ pchText && cch ? GetEscapedUTF8String(pchText, cch).get() : "", cch,
+ pChange, ToString(mComposition).c_str()));
+
+ // Per SDK documentation, and since we don't have better
+ // ways to do this, this method acts as a helper to
+ // call SetSelection followed by InsertTextAtSelection
+ if (!IsReadWriteLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetText() FAILED due to "
+ "not locked (read)",
+ this));
+ return TS_E_NOLOCK;
+ }
+
+ TS_SELECTION_ACP selection;
+ selection.acpStart = acpStart;
+ selection.acpEnd = acpEnd;
+ selection.style.ase = TS_AE_END;
+ selection.style.fInterimChar = 0;
+ // Set selection to desired range
+ HRESULT hr = SetSelectionInternal(&selection);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetText() FAILED due to "
+ "SetSelectionInternal() failure",
+ this));
+ return hr;
+ }
+ // Replace just selected text
+ if (!InsertTextAtSelectionInternal(nsDependentSubstring(pchText, cch),
+ pChange)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::SetText() FAILED due to "
+ "InsertTextAtSelectionInternal() failure",
+ this));
+ return E_FAIL;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::SetText() succeeded: pChange={ "
+ "acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld }",
+ this, pChange ? pChange->acpStart : 0,
+ pChange ? pChange->acpOldEnd : 0, pChange ? pChange->acpNewEnd : 0));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::GetFormattedText(LONG acpStart, LONG acpEnd,
+ IDataObject** ppDataObject) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetFormattedText() called "
+ "but not supported (E_NOTIMPL)",
+ this));
+
+ // no support for formatted text
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+TSFTextStore::GetEmbedded(LONG acpPos, REFGUID rguidService, REFIID riid,
+ IUnknown** ppunk) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetEmbedded() called "
+ "but not supported (E_NOTIMPL)",
+ this));
+
+ // embedded objects are not supported
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+TSFTextStore::QueryInsertEmbedded(const GUID* pguidService,
+ const FORMATETC* pFormatEtc,
+ BOOL* pfInsertable) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::QueryInsertEmbedded() called "
+ "but not supported, *pfInsertable=FALSE (S_OK)",
+ this));
+
+ // embedded objects are not supported
+ *pfInsertable = FALSE;
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::InsertEmbedded(DWORD dwFlags, LONG acpStart, LONG acpEnd,
+ IDataObject* pDataObject, TS_TEXTCHANGE* pChange) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::InsertEmbedded() called "
+ "but not supported (E_NOTIMPL)",
+ this));
+
+ // embedded objects are not supported
+ return E_NOTIMPL;
+}
+
+// static
+bool TSFTextStore::ShouldSetInputScopeOfURLBarToDefault() {
+ // FYI: Google Japanese Input may be an IMM-IME. If it's installed on
+ // Win7, it's always IMM-IME. Otherwise, basically, it's a TIP.
+ // However, if it's installed on Win7 and has not been updated yet
+ // after the OS is upgraded to Win8 or later, it's still an IMM-IME.
+ // Therefore, we also need to check with IMMHandler here.
+ if (!StaticPrefs::intl_ime_hack_set_input_scope_of_url_bar_to_default()) {
+ return false;
+ }
+
+ if (IMMHandler::IsGoogleJapaneseInputActive()) {
+ return true;
+ }
+
+ switch (TSFStaticSink::ActiveTIP()) {
+ case TextInputProcessorID::eMicrosoftIMEForJapanese:
+ case TextInputProcessorID::eGoogleJapaneseInput:
+ case TextInputProcessorID::eMicrosoftBopomofo:
+ case TextInputProcessorID::eMicrosoftChangJie:
+ case TextInputProcessorID::eMicrosoftPhonetic:
+ case TextInputProcessorID::eMicrosoftQuick:
+ case TextInputProcessorID::eMicrosoftNewChangJie:
+ case TextInputProcessorID::eMicrosoftNewPhonetic:
+ case TextInputProcessorID::eMicrosoftNewQuick:
+ case TextInputProcessorID::eMicrosoftPinyin:
+ case TextInputProcessorID::eMicrosoftPinyinNewExperienceInputStyle:
+ case TextInputProcessorID::eMicrosoftOldHangul:
+ case TextInputProcessorID::eMicrosoftWubi:
+ case TextInputProcessorID::eMicrosoftIMEForKorean:
+ return true;
+ default:
+ return false;
+ }
+}
+
+void TSFTextStore::SetInputScope(const nsString& aHTMLInputType,
+ const nsString& aHTMLInputMode) {
+ mInputScopes.Clear();
+
+ // IME may refer only first input scope, but we will append inputmode's
+ // input scopes too like Chrome since IME may refer it.
+ IMEHandler::AppendInputScopeFromType(aHTMLInputType, mInputScopes);
+ IMEHandler::AppendInputScopeFromInputMode(aHTMLInputMode, mInputScopes);
+
+ if (mInPrivateBrowsing) {
+ mInputScopes.AppendElement(IS_PRIVATE);
+ }
+}
+
+int32_t TSFTextStore::GetRequestedAttrIndex(const TS_ATTRID& aAttrID) {
+ if (IsEqualGUID(aAttrID, GUID_PROP_INPUTSCOPE)) {
+ return eInputScope;
+ }
+ if (IsEqualGUID(aAttrID, sGUID_PROP_URL)) {
+ return eDocumentURL;
+ }
+ if (IsEqualGUID(aAttrID, TSATTRID_Text_VerticalWriting)) {
+ return eTextVerticalWriting;
+ }
+ if (IsEqualGUID(aAttrID, TSATTRID_Text_Orientation)) {
+ return eTextOrientation;
+ }
+ return eNotSupported;
+}
+
+TS_ATTRID
+TSFTextStore::GetAttrID(int32_t aIndex) {
+ switch (aIndex) {
+ case eInputScope:
+ return GUID_PROP_INPUTSCOPE;
+ case eDocumentURL:
+ return sGUID_PROP_URL;
+ case eTextVerticalWriting:
+ return TSATTRID_Text_VerticalWriting;
+ case eTextOrientation:
+ return TSATTRID_Text_Orientation;
+ default:
+ MOZ_CRASH("Invalid index? Or not implemented yet?");
+ return GUID_NULL;
+ }
+}
+
+HRESULT
+TSFTextStore::HandleRequestAttrs(DWORD aFlags, ULONG aFilterCount,
+ const TS_ATTRID* aFilterAttrs) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::HandleRequestAttrs(aFlags=%s, "
+ "aFilterCount=%lu)",
+ this, GetFindFlagName(aFlags).get(), aFilterCount));
+
+ // This is a little weird! RequestSupportedAttrs gives us advanced notice
+ // of a support query via RetrieveRequestedAttrs for a specific attribute.
+ // RetrieveRequestedAttrs needs to return valid data for all attributes we
+ // support, but the text service will only want the input scope object
+ // returned in RetrieveRequestedAttrs if the dwFlags passed in here contains
+ // TS_ATTR_FIND_WANT_VALUE.
+ for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) {
+ mRequestedAttrs[i] = false;
+ }
+ mRequestedAttrValues = !!(aFlags & TS_ATTR_FIND_WANT_VALUE);
+
+ for (uint32_t i = 0; i < aFilterCount; i++) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::HandleRequestAttrs(), "
+ "requested attr=%s",
+ this, GetGUIDNameStrWithTable(aFilterAttrs[i]).get()));
+ int32_t index = GetRequestedAttrIndex(aFilterAttrs[i]);
+ if (index != eNotSupported) {
+ mRequestedAttrs[index] = true;
+ }
+ }
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::RequestSupportedAttrs(DWORD dwFlags, ULONG cFilterAttrs,
+ const TS_ATTRID* paFilterAttrs) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RequestSupportedAttrs(dwFlags=%s, "
+ "cFilterAttrs=%lu)",
+ this, GetFindFlagName(dwFlags).get(), cFilterAttrs));
+
+ return HandleRequestAttrs(dwFlags, cFilterAttrs, paFilterAttrs);
+}
+
+STDMETHODIMP
+TSFTextStore::RequestAttrsAtPosition(LONG acpPos, ULONG cFilterAttrs,
+ const TS_ATTRID* paFilterAttrs,
+ DWORD dwFlags) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RequestAttrsAtPosition(acpPos=%ld, "
+ "cFilterAttrs=%lu, dwFlags=%s)",
+ this, acpPos, cFilterAttrs, GetFindFlagName(dwFlags).get()));
+
+ return HandleRequestAttrs(dwFlags | TS_ATTR_FIND_WANT_VALUE, cFilterAttrs,
+ paFilterAttrs);
+}
+
+STDMETHODIMP
+TSFTextStore::RequestAttrsTransitioningAtPosition(LONG acpPos,
+ ULONG cFilterAttrs,
+ const TS_ATTRID* paFilterAttr,
+ DWORD dwFlags) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RequestAttrsTransitioningAtPosition("
+ "acpPos=%ld, cFilterAttrs=%lu, dwFlags=%s) called but not supported "
+ "(S_OK)",
+ this, acpPos, cFilterAttrs, GetFindFlagName(dwFlags).get()));
+
+ // no per character attributes defined
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::FindNextAttrTransition(LONG acpStart, LONG acpHalt,
+ ULONG cFilterAttrs,
+ const TS_ATTRID* paFilterAttrs,
+ DWORD dwFlags, LONG* pacpNext,
+ BOOL* pfFound, LONG* plFoundOffset) {
+ if (!pacpNext || !pfFound || !plFoundOffset) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" 0x%p TSFTextStore::FindNextAttrTransition() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::FindNextAttrTransition() called "
+ "but not supported (S_OK)",
+ this));
+
+ // no per character attributes defined
+ *pacpNext = *plFoundOffset = acpHalt;
+ *pfFound = FALSE;
+ return S_OK;
+}
+
+// To test the document URL result, define this to out put it to the stdout
+// #define DEBUG_PRINT_DOCUMENT_URL
+
+STDMETHODIMP
+TSFTextStore::RetrieveRequestedAttrs(ULONG ulCount, TS_ATTRVAL* paAttrVals,
+ ULONG* pcFetched) {
+ if (!pcFetched || !paAttrVals) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RetrieveRequestedAttrs() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ ULONG expectedCount = 0;
+ for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) {
+ if (mRequestedAttrs[i]) {
+ expectedCount++;
+ }
+ }
+ if (ulCount < expectedCount) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RetrieveRequestedAttrs() FAILED due to "
+ "not enough count ulCount=%lu, expectedCount=%lu",
+ this, ulCount, expectedCount));
+ return E_INVALIDARG;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RetrieveRequestedAttrs() called "
+ "ulCount=%lu, mRequestedAttrValues=%s",
+ this, ulCount, GetBoolName(mRequestedAttrValues)));
+
+ auto GetExposingURL = [&]() -> BSTR {
+ const bool allowed =
+ StaticPrefs::intl_tsf_expose_url_allowed() &&
+ (!mInPrivateBrowsing ||
+ StaticPrefs::intl_tsf_expose_url_in_private_browsing_allowed());
+ if (!allowed || mDocumentURL.IsEmpty()) {
+ BSTR emptyString = ::SysAllocString(L"");
+ MOZ_ASSERT(
+ emptyString,
+ "We need to return valid BSTR pointer to notify TSF of supporting it "
+ "with a pointer to empty string");
+ return emptyString;
+ }
+ return ::SysAllocString(mDocumentURL.get());
+ };
+
+#ifdef DEBUG_PRINT_DOCUMENT_URL
+ {
+ BSTR exposingURL = GetExposingURL();
+ printf("TSFTextStore::RetrieveRequestedAttrs: DocumentURL=\"%s\"\n",
+ NS_ConvertUTF16toUTF8(static_cast<char16ptr_t>(_bstr_t(exposingURL)))
+ .get());
+ ::SysFreeString(exposingURL);
+ }
+#endif // #ifdef DEBUG_PRINT_DOCUMENT_URL
+
+ int32_t count = 0;
+ for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) {
+ if (!mRequestedAttrs[i]) {
+ continue;
+ }
+ mRequestedAttrs[i] = false;
+
+ TS_ATTRID attrID = GetAttrID(i);
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RetrieveRequestedAttrs() for %s", this,
+ GetGUIDNameStrWithTable(attrID).get()));
+
+ paAttrVals[count].idAttr = attrID;
+ paAttrVals[count].dwOverlapId = 0;
+
+ if (!mRequestedAttrValues) {
+ paAttrVals[count].varValue.vt = VT_EMPTY;
+ } else {
+ switch (i) {
+ case eInputScope: {
+ paAttrVals[count].varValue.vt = VT_UNKNOWN;
+ RefPtr<IUnknown> inputScope = new InputScopeImpl(mInputScopes);
+ paAttrVals[count].varValue.punkVal = inputScope.forget().take();
+ break;
+ }
+ case eDocumentURL: {
+ paAttrVals[count].varValue.vt = VT_BSTR;
+ paAttrVals[count].varValue.bstrVal = GetExposingURL();
+ break;
+ }
+ case eTextVerticalWriting: {
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ paAttrVals[count].varValue.vt = VT_BOOL;
+ paAttrVals[count].varValue.boolVal =
+ selectionForTSF.isSome() &&
+ selectionForTSF->WritingModeRef().IsVertical()
+ ? VARIANT_TRUE
+ : VARIANT_FALSE;
+ break;
+ }
+ case eTextOrientation: {
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ paAttrVals[count].varValue.vt = VT_I4;
+ paAttrVals[count].varValue.lVal =
+ selectionForTSF.isSome() &&
+ selectionForTSF->WritingModeRef().IsVertical()
+ ? 2700
+ : 0;
+ break;
+ }
+ default:
+ MOZ_CRASH("Invalid index? Or not implemented yet?");
+ break;
+ }
+ }
+ count++;
+ }
+
+ mRequestedAttrValues = false;
+
+ if (count) {
+ *pcFetched = count;
+ return S_OK;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RetrieveRequestedAttrs() called "
+ "for unknown TS_ATTRVAL, *pcFetched=0 (S_OK)",
+ this));
+
+ paAttrVals->dwOverlapId = 0;
+ paAttrVals->varValue.vt = VT_EMPTY;
+ *pcFetched = 0;
+ return S_OK;
+}
+
+#undef DEBUG_PRINT_DOCUMENT_URL
+
+STDMETHODIMP
+TSFTextStore::GetEndACP(LONG* pacp) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetEndACP(pacp=0x%p)", this, pacp));
+
+ if (!IsReadLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetEndACP() FAILED due to "
+ "not locked (read)",
+ this));
+ return TS_E_NOLOCK;
+ }
+
+ if (!pacp) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetEndACP() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ Maybe<Content>& contentForTSF = ContentForTSF();
+ if (contentForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetEndACP() FAILED due to "
+ "ContentForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+ *pacp = static_cast<LONG>(contentForTSF->TextRef().Length());
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::GetActiveView(TsViewCookie* pvcView) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetActiveView(pvcView=0x%p)", this, pvcView));
+
+ if (!pvcView) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetActiveView() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ *pvcView = TEXTSTORE_DEFAULT_VIEW;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetActiveView() succeeded: *pvcView=%ld", this,
+ *pvcView));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::GetACPFromPoint(TsViewCookie vcView, const POINT* pt,
+ DWORD dwFlags, LONG* pacp) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetACPFromPoint(pvcView=%ld, pt=%p (x=%ld, "
+ "y=%ld), dwFlags=%s, pacp=%p, mDeferNotifyingTSFUntilNextUpdate=%s, "
+ "mWaitingQueryLayout=%s",
+ this, vcView, pt, pt ? pt->x : 0, pt ? pt->y : 0,
+ GetACPFromPointFlagName(dwFlags).get(), pacp,
+ GetBoolName(mDeferNotifyingTSFUntilNextUpdate),
+ GetBoolName(mWaitingQueryLayout)));
+
+ if (!IsReadLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to "
+ "not locked (read)",
+ this));
+ return TS_E_NOLOCK;
+ }
+
+ if (vcView != TEXTSTORE_DEFAULT_VIEW) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to "
+ "called with invalid view",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (!pt) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to "
+ "null pt",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (!pacp) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to "
+ "null pacp",
+ this));
+ return E_INVALIDARG;
+ }
+
+ mWaitingQueryLayout = false;
+
+ if (mDestroyed ||
+ (mContentForTSF.isSome() && mContentForTSF->IsLayoutChanged())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() returned "
+ "TS_E_NOLAYOUT",
+ this));
+ mHasReturnedNoLayoutError = true;
+ return TS_E_NOLAYOUT;
+ }
+
+ LayoutDeviceIntPoint ourPt(pt->x, pt->y);
+ // Convert to widget relative coordinates from screen's.
+ ourPt -= mWidget->WidgetToScreenOffset();
+
+ // NOTE: Don't check if the point is in the widget since the point can be
+ // outside of the widget if focused editor is in a XUL <panel>.
+
+ WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint,
+ mWidget);
+ mWidget->InitEvent(queryCharAtPointEvent, &ourPt);
+
+ // FYI: WidgetQueryContentEvent may cause flushing pending layout and it
+ // may cause focus change or something.
+ RefPtr<TSFTextStore> kungFuDeathGrip(this);
+ DispatchEvent(queryCharAtPointEvent);
+ if (!mWidget || mWidget->Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to "
+ "mWidget was destroyed during eQueryCharacterAtPoint",
+ this));
+ return E_FAIL;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::GetACPFromPoint(), queryCharAtPointEvent={ "
+ "mReply=%s }",
+ this, ToString(queryCharAtPointEvent.mReply).c_str()));
+
+ if (NS_WARN_IF(queryCharAtPointEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to "
+ "eQueryCharacterAtPoint failure",
+ this));
+ return E_FAIL;
+ }
+
+ // If dwFlags isn't set and the point isn't in any character's bounding box,
+ // we should return TS_E_INVALIDPOINT.
+ if (!(dwFlags & GXFPF_NEAREST) && queryCharAtPointEvent.DidNotFindChar()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to the "
+ "point contained by no bounding box",
+ this));
+ return TS_E_INVALIDPOINT;
+ }
+
+ // Although, we're not sure if mTentativeCaretOffset becomes NOT_FOUND,
+ // let's assume that there is no content in such case.
+ NS_WARNING_ASSERTION(queryCharAtPointEvent.DidNotFindTentativeCaretOffset(),
+ "Tentative caret offset was not found");
+
+ uint32_t offset;
+
+ // If dwFlags includes GXFPF_ROUND_NEAREST, we should return tentative
+ // caret offset (MSDN calls it "range position").
+ if (dwFlags & GXFPF_ROUND_NEAREST) {
+ offset = queryCharAtPointEvent.mReply->mTentativeCaretOffset.valueOr(0);
+ } else if (queryCharAtPointEvent.FoundChar()) {
+ // Otherwise, we should return character offset whose bounding box contains
+ // the point.
+ offset = queryCharAtPointEvent.mReply->StartOffset();
+ } else {
+ // If the point isn't in any character's bounding box but we need to return
+ // the nearest character from the point, we should *guess* the character
+ // offset since there is no inexpensive API to check it strictly.
+ // XXX If we retrieve 2 bounding boxes, one is before the offset and
+ // the other is after the offset, we could resolve the offset.
+ // However, dispatching 2 eQueryTextRect may be expensive.
+
+ // So, use tentative offset for now.
+ offset = queryCharAtPointEvent.mReply->mTentativeCaretOffset.valueOr(0);
+
+ // However, if it's after the last character, we need to decrement the
+ // offset.
+ Maybe<Content>& contentForTSF = ContentForTSF();
+ if (contentForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to "
+ "ContentForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+ if (contentForTSF->TextRef().Length() <= offset) {
+ // If the tentative caret is after the last character, let's return
+ // the last character's offset.
+ offset = contentForTSF->TextRef().Length() - 1;
+ }
+ }
+
+ if (NS_WARN_IF(offset > LONG_MAX)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to out of "
+ "range of the result",
+ this));
+ return TS_E_INVALIDPOINT;
+ }
+
+ *pacp = static_cast<LONG>(offset);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetACPFromPoint() succeeded: *pacp=%ld", this,
+ *pacp));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::GetTextExt(TsViewCookie vcView, LONG acpStart, LONG acpEnd,
+ RECT* prc, BOOL* pfClipped) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetTextExt(vcView=%ld, "
+ "acpStart=%ld, acpEnd=%ld, prc=0x%p, pfClipped=0x%p), "
+ "IsHandlingCompositionInParent()=%s, "
+ "IsHandlingCompositionInContent()=%s, mContentForTSF=%s, "
+ "mSelectionForTSF=%s, mComposition=%s, "
+ "mDeferNotifyingTSFUntilNextUpdate=%s, mWaitingQueryLayout=%s, "
+ "IMEHandler::IsA11yHandlingNativeCaret()=%s",
+ this, vcView, acpStart, acpEnd, prc, pfClipped,
+ GetBoolName(IsHandlingCompositionInParent()),
+ GetBoolName(IsHandlingCompositionInContent()),
+ mozilla::ToString(mContentForTSF).c_str(),
+ ToString(mSelectionForTSF).c_str(), ToString(mComposition).c_str(),
+ GetBoolName(mDeferNotifyingTSFUntilNextUpdate),
+ GetBoolName(mWaitingQueryLayout),
+ GetBoolName(IMEHandler::IsA11yHandlingNativeCaret())));
+
+ if (!IsReadLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() FAILED due to "
+ "not locked (read)",
+ this));
+ return TS_E_NOLOCK;
+ }
+
+ if (vcView != TEXTSTORE_DEFAULT_VIEW) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() FAILED due to "
+ "called with invalid view",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (!prc || !pfClipped) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ // According to MSDN, ITextStoreACP::GetTextExt() should return
+ // TS_E_INVALIDARG when acpStart and acpEnd are same (i.e., collapsed range).
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ms538435(v=vs.85).aspx
+ // > TS_E_INVALIDARG: The specified start and end character positions are
+ // > equal.
+ // However, some TIPs (including Microsoft's Chinese TIPs!) call this with
+ // collapsed range and if we return TS_E_INVALIDARG, they stops showing their
+ // owning window or shows it but odd position. So, we should just return
+ // error only when acpStart and/or acpEnd are really odd.
+
+ if (acpStart < 0 || acpEnd < acpStart) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() FAILED due to "
+ "invalid position",
+ this));
+ return TS_E_INVALIDPOS;
+ }
+
+ mWaitingQueryLayout = false;
+
+ if (IsHandlingCompositionInContent() && mContentForTSF.isSome() &&
+ mContentForTSF->HasOrHadComposition() &&
+ mContentForTSF->IsLayoutChanged() &&
+ mContentForTSF->MinModifiedOffset().value() >
+ static_cast<uint32_t>(LONG_MAX)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt(), FAILED due to the text "
+ "is too big for TSF (cannot treat modified offset as LONG), "
+ "mContentForTSF=%s",
+ this, ToString(mContentForTSF).c_str()));
+ return E_FAIL;
+ }
+
+ // At Windows 10 build 17643 (an insider preview for RS5), Microsoft fixed
+ // the bug of TS_E_NOLAYOUT (even when we returned TS_E_NOLAYOUT, TSF
+ // returned E_FAIL to TIP). However, until we drop to support older Windows
+ // and all TIPs are aware of TS_E_NOLAYOUT result, we need to keep returning
+ // S_OK and available rectangle only for them.
+ if (!MaybeHackNoErrorLayoutBugs(acpStart, acpEnd) &&
+ mContentForTSF.isSome() && mContentForTSF->IsLayoutChangedAt(acpEnd)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT "
+ "(acpEnd=%ld)",
+ this, acpEnd));
+ mHasReturnedNoLayoutError = true;
+ return TS_E_NOLAYOUT;
+ }
+
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT "
+ "(acpEnd=%ld) because this has already been destroyed",
+ this, acpEnd));
+ mHasReturnedNoLayoutError = true;
+ return TS_E_NOLAYOUT;
+ }
+
+ // use eQueryTextRect to get rect in system, screen coordinates
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, mWidget);
+ mWidget->InitEvent(queryTextRectEvent);
+
+ WidgetQueryContentEvent::Options options;
+ int64_t startOffset = acpStart;
+ if (mComposition.isSome()) {
+ // If there is a composition, TSF must want character rects related to
+ // the composition. Therefore, we should use insertion point relative
+ // query because the composition might be at different position from
+ // the position where TSFTextStore believes it at.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mComposition->StartOffset();
+ } else if (IsHandlingCompositionInParent() && mContentForTSF.isSome() &&
+ mContentForTSF->HasOrHadComposition()) {
+ // If there was a composition and its commit event hasn't been dispatched
+ // yet, ContentCacheInParent is still open for relative offset query from
+ // the latest composition.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mContentForTSF->LatestCompositionRange()->StartOffset();
+ } else if (!CanAccessActualContentDirectly() &&
+ mSelectionForTSF->HasRange()) {
+ // If TSF/TIP cannot access actual content directly, there may be pending
+ // text and/or selection changes which have not been notified TSF yet.
+ // Therefore, we should use relative to insertion point query since
+ // TSF/TIP computes the offset from the cached selection.
+ options.mRelativeToInsertionPoint = true;
+ startOffset -= mSelectionForTSF->StartOffset();
+ }
+ // ContentEventHandler and ContentCache return actual caret rect when
+ // the queried range is collapsed and selection is collapsed at the
+ // queried range. Then, its height (in horizontal layout, width in vertical
+ // layout) may be different from actual font height of the line. In such
+ // case, users see "dancing" of candidate or suggest window of TIP.
+ // For preventing it, we should query text rect with at least 1 length.
+ uint32_t length = std::max(static_cast<int32_t>(acpEnd - acpStart), 1);
+ queryTextRectEvent.InitForQueryTextRect(startOffset, length, options);
+
+ DispatchEvent(queryTextRectEvent);
+ if (NS_WARN_IF(queryTextRectEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() FAILED due to "
+ "eQueryTextRect failure",
+ this));
+ return TS_E_INVALIDPOS; // but unexpected failure, maybe.
+ }
+
+ // IMEs don't like empty rects, fix here
+ if (queryTextRectEvent.mReply->mRect.Width() <= 0) {
+ queryTextRectEvent.mReply->mRect.SetWidth(1);
+ }
+ if (queryTextRectEvent.mReply->mRect.Height() <= 0) {
+ queryTextRectEvent.mReply->mRect.SetHeight(1);
+ }
+
+ // convert to unclipped screen rect
+ nsWindow* refWindow =
+ static_cast<nsWindow*>(!!queryTextRectEvent.mReply->mFocusedWidget
+ ? queryTextRectEvent.mReply->mFocusedWidget
+ : static_cast<nsIWidget*>(mWidget.get()));
+ // Result rect is in top level widget coordinates
+ refWindow = refWindow->GetTopLevelWindow(false);
+ if (!refWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() FAILED due to "
+ "no top level window",
+ this));
+ return E_FAIL;
+ }
+
+ queryTextRectEvent.mReply->mRect.MoveBy(refWindow->WidgetToScreenOffset());
+
+ // get bounding screen rect to test for clipping
+ if (!GetScreenExtInternal(*prc)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetTextExt() FAILED due to "
+ "GetScreenExtInternal() failure",
+ this));
+ return E_FAIL;
+ }
+
+ // clip text rect to bounding rect
+ RECT textRect;
+ ::SetRect(&textRect, queryTextRectEvent.mReply->mRect.X(),
+ queryTextRectEvent.mReply->mRect.Y(),
+ queryTextRectEvent.mReply->mRect.XMost(),
+ queryTextRectEvent.mReply->mRect.YMost());
+ if (!::IntersectRect(prc, prc, &textRect))
+ // Text is not visible
+ ::SetRectEmpty(prc);
+
+ // not equal if text rect was clipped
+ *pfClipped = !::EqualRect(prc, &textRect);
+
+ // ATOK 2011 - 2016 refers native caret position and size on windows whose
+ // class name is one of Mozilla's windows for deciding candidate window
+ // position. Additionally, ATOK 2015 and earlier behaves really odd when
+ // we don't create native caret. Therefore, we need to create native caret
+ // only when ATOK 2011 - 2015 is active (i.e., not necessary for ATOK 2016).
+ // However, if a11y module is handling native caret, we shouldn't touch it.
+ // Note that ATOK must require the latest information of the caret. So,
+ // even if we'll create native caret later, we need to creat it here with
+ // current information.
+ if (!IMEHandler::IsA11yHandlingNativeCaret() &&
+ StaticPrefs::intl_tsf_hack_atok_create_native_caret() &&
+ TSFStaticSink::IsATOKReferringNativeCaretActive() &&
+ mComposition.isSome() &&
+ mComposition->IsOffsetInRangeOrEndOffset(acpStart) &&
+ mComposition->IsOffsetInRangeOrEndOffset(acpEnd)) {
+ CreateNativeCaret();
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetTextExt() succeeded: "
+ "*prc={ left=%ld, top=%ld, right=%ld, bottom=%ld }, *pfClipped=%s",
+ this, prc->left, prc->top, prc->right, prc->bottom,
+ GetBoolName(*pfClipped)));
+
+ return S_OK;
+}
+
+bool TSFTextStore::MaybeHackNoErrorLayoutBugs(LONG& aACPStart, LONG& aACPEnd) {
+ // When ITextStoreACP::GetTextExt() returns TS_E_NOLAYOUT, TSF returns E_FAIL
+ // to its caller (typically, active TIP). Then, most TIPs abort current job
+ // or treat such application as non-GUI apps. E.g., some of them give up
+ // showing candidate window, some others show candidate window at top-left of
+ // the screen. For avoiding this issue, when there is composition (until
+ // composition is actually committed in remote content), we should not
+ // return TS_E_NOLAYOUT error for TIPs whose some features are broken by
+ // this issue.
+ // Note that ideally, this issue should be avoided by each TIP since this
+ // won't be fixed at least on non-latest Windows. Actually, Google Japanese
+ // Input (based on Mozc) does it. When GetTextExt() returns E_FAIL, TIPs
+ // should try to check result of GetRangeFromPoint() because TSF returns
+ // TS_E_NOLAYOUT correctly in this case. See:
+ // https://github.com/google/mozc/blob/6b878e31fb6ac4347dc9dfd8ccc1080fe718479f/src/win32/tip/tip_range_util.cc#L237-L257
+
+ if (!IsHandlingCompositionInContent() || mContentForTSF.isNothing() ||
+ !mContentForTSF->HasOrHadComposition() ||
+ !mContentForTSF->IsLayoutChangedAt(aACPEnd)) {
+ return false;
+ }
+
+ MOZ_ASSERT(mComposition.isNothing() ||
+ mComposition->StartOffset() ==
+ mContentForTSF->LatestCompositionRange()->StartOffset());
+ MOZ_ASSERT(mComposition.isNothing() ||
+ mComposition->EndOffset() ==
+ mContentForTSF->LatestCompositionRange()->EndOffset());
+
+ // If TSF does not have the bug, we need to hack only with a few TIPs.
+ static const bool sAlllowToStopHackingIfFine =
+ IsWindows10BuildOrLater(17643) &&
+ StaticPrefs::
+ intl_tsf_hack_allow_to_stop_hacking_on_build_17643_or_later();
+
+ // We need to compute active TIP now. This may take a couple of milliseconds,
+ // however, it'll be cached, so, must be faster than check active TIP every
+ // GetTextExt() calls.
+ const Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ switch (TSFStaticSink::ActiveTIP()) {
+ // MS IME for Japanese doesn't support asynchronous handling at deciding
+ // its suggest list window position. The feature was implemented
+ // starting from Windows 8. And also we may meet same trouble in e10s
+ // mode on Win7. So, we should never return TS_E_NOLAYOUT to MS IME for
+ // Japanese.
+ case TextInputProcessorID::eMicrosoftIMEForJapanese:
+ // Basically, MS-IME tries to retrieve whole composition string rect
+ // at deciding suggest window immediately after unlocking the document.
+ // However, in e10s mode, the content hasn't updated yet in most cases.
+ // Therefore, if the first character at the retrieving range rect is
+ // available, we should use it as the result.
+ // Note that according to bug 1609675, MS-IME for Japanese itself does
+ // not handle TS_E_NOLAYOUT correctly at least on Build 18363.657 (1909).
+ if (StaticPrefs::
+ intl_tsf_hack_ms_japanese_ime_do_not_return_no_layout_error_at_first_char() &&
+ aACPStart < aACPEnd) {
+ aACPEnd = aACPStart;
+ break;
+ }
+ if (sAlllowToStopHackingIfFine) {
+ return false;
+ }
+ // Although, the condition is not clear, MS-IME sometimes retrieves the
+ // caret rect immediately after modifying the composition string but
+ // before unlocking the document. In such case, we should return the
+ // nearest character rect.
+ // (Let's return true if there is no selection which must be not expected
+ // by MS-IME nor TSF.)
+ if (StaticPrefs::
+ intl_tsf_hack_ms_japanese_ime_do_not_return_no_layout_error_at_caret() &&
+ aACPStart == aACPEnd && selectionForTSF.isSome() &&
+ (!selectionForTSF->HasRange() ||
+ (selectionForTSF->Collapsed() &&
+ selectionForTSF->EndOffset() == aACPEnd))) {
+ int32_t minOffsetOfLayoutChanged =
+ static_cast<int32_t>(mContentForTSF->MinModifiedOffset().value());
+ aACPEnd = aACPStart = std::max(minOffsetOfLayoutChanged - 1, 0);
+ } else {
+ return false;
+ }
+ break;
+ // The bug of Microsoft Office IME 2010 for Japanese is similar to
+ // MS-IME for Win 8.1 and Win 10. Newer version of MS Office IME is not
+ // released yet. So, we can hack it without prefs because there must be
+ // no developers who want to disable this hack for tests.
+ // XXX We have not tested with Microsoft Office IME 2010 since it's
+ // installable only with Win7 and Win8 (i.e., cannot install Win8.1
+ // and Win10), and requires upgrade to Win10.
+ case TextInputProcessorID::eMicrosoftOfficeIME2010ForJapanese:
+ // Basically, MS-IME tries to retrieve whole composition string rect
+ // at deciding suggest window immediately after unlocking the document.
+ // However, in e10s mode, the content hasn't updated yet in most cases.
+ // Therefore, if the first character at the retrieving range rect is
+ // available, we should use it as the result.
+ if (aACPStart < aACPEnd) {
+ aACPEnd = aACPStart;
+ }
+ // Although, the condition is not clear, MS-IME sometimes retrieves the
+ // caret rect immediately after modifying the composition string but
+ // before unlocking the document. In such case, we should return the
+ // nearest character rect.
+ // (Let's return true if there is no selection which must be not expected
+ // by MS-IME nor TSF.)
+ else if (aACPStart == aACPEnd && selectionForTSF.isSome() &&
+ (!selectionForTSF->HasRange() ||
+ (selectionForTSF->Collapsed() &&
+ selectionForTSF->EndOffset() == aACPEnd))) {
+ int32_t minOffsetOfLayoutChanged =
+ static_cast<int32_t>(mContentForTSF->MinModifiedOffset().value());
+ aACPEnd = aACPStart = std::max(minOffsetOfLayoutChanged - 1, 0);
+ } else {
+ return false;
+ }
+ break;
+ // ATOK fails to handle TS_E_NOLAYOUT only when it decides the position of
+ // suggest window. In such case, ATOK tries to query rect of whole or a
+ // part of composition string.
+ // FYI: ATOK changes their implementation around candidate window and
+ // suggest widget at ATOK 2016. Therefore, there are some differences
+ // ATOK 2015 (or older) and ATOK 2016 (or newer).
+ // FYI: ATOK 2017 stops referring our window class name. I.e., ATOK 2016
+ // and older may behave differently only on Gecko but this must be
+ // finished from ATOK 2017.
+ // FYI: For testing with legacy ATOK, we should hack it even if current ATOK
+ // refers native caret rect on windows whose window class is one of
+ // Mozilla window classes and we stop creating native caret for ATOK
+ // because creating native caret causes ATOK refers caret position
+ // when GetTextExt() returns TS_E_NOLAYOUT.
+ case TextInputProcessorID::eATOK2011:
+ case TextInputProcessorID::eATOK2012:
+ case TextInputProcessorID::eATOK2013:
+ case TextInputProcessorID::eATOK2014:
+ case TextInputProcessorID::eATOK2015:
+ // ATOK 2016 and later may temporarily show candidate window at odd
+ // position when you convert a word quickly (e.g., keep pressing
+ // space bar). So, on ATOK 2016 or later, we need to keep hacking the
+ // result of GetTextExt().
+ if (sAlllowToStopHackingIfFine) {
+ return false;
+ }
+ // If we'll create native caret where we paint our caret. Then, ATOK
+ // will refer native caret. So, we don't need to hack anything in
+ // this case.
+ if (StaticPrefs::intl_tsf_hack_atok_create_native_caret()) {
+ MOZ_ASSERT(TSFStaticSink::IsATOKReferringNativeCaretActive());
+ return false;
+ }
+ [[fallthrough]];
+ case TextInputProcessorID::eATOK2016:
+ case TextInputProcessorID::eATOKUnknown:
+ if (!StaticPrefs::
+ intl_tsf_hack_atok_do_not_return_no_layout_error_of_composition_string()) {
+ return false;
+ }
+ // If the range is in the composition string, we should return rectangle
+ // in it as far as possible.
+ if (!mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset(
+ aACPStart) ||
+ !mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset(
+ aACPEnd)) {
+ return false;
+ }
+ break;
+ // Japanist 10 fails to handle TS_E_NOLAYOUT when it decides the position
+ // of candidate window. In such case, Japanist shows candidate window at
+ // top-left of the screen. So, we should return the nearest caret rect
+ // where we know. This is Japanist's bug. So, even after build 17643,
+ // we need this hack.
+ case TextInputProcessorID::eJapanist10:
+ if (!StaticPrefs::
+ intl_tsf_hack_japanist10_do_not_return_no_layout_error_of_composition_string()) {
+ return false;
+ }
+ if (!mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset(
+ aACPStart) ||
+ !mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset(
+ aACPEnd)) {
+ return false;
+ }
+ break;
+ // Free ChangJie 2010 doesn't handle ITfContextView::GetTextExt() properly.
+ // This must be caused by the bug of TSF since Free ChangJie works fine on
+ // build 17643 and later.
+ case TextInputProcessorID::eFreeChangJie:
+ if (sAlllowToStopHackingIfFine) {
+ return false;
+ }
+ if (!StaticPrefs::
+ intl_tsf_hack_free_chang_jie_do_not_return_no_layout_error()) {
+ return false;
+ }
+ aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset();
+ aACPStart = std::min(aACPStart, aACPEnd);
+ break;
+ // Some Traditional Chinese TIPs of Microsoft don't show candidate window
+ // in e10s mode on Win8 or later.
+ case TextInputProcessorID::eMicrosoftQuick:
+ if (sAlllowToStopHackingIfFine) {
+ return false; // MS Quick works fine with Win10 build 17643.
+ }
+ [[fallthrough]];
+ case TextInputProcessorID::eMicrosoftChangJie:
+ if (!StaticPrefs::
+ intl_tsf_hack_ms_traditional_chinese_do_not_return_no_layout_error()) {
+ return false;
+ }
+ aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset();
+ aACPStart = std::min(aACPStart, aACPEnd);
+ break;
+ // Some Simplified Chinese TIPs of Microsoft don't show candidate window
+ // in e10s mode on Win8 or later.
+ // FYI: Only Simplified Chinese TIPs of Microsoft still require this hack
+ // because they sometimes do not show candidate window when we return
+ // TS_E_NOLAYOUT for first query. Note that even when they show
+ // candidate window properly, we return TS_E_NOLAYOUT and following
+ // log looks same as when they don't show candidate window. Perhaps,
+ // there is stateful cause or race in them.
+ case TextInputProcessorID::eMicrosoftPinyin:
+ case TextInputProcessorID::eMicrosoftWubi:
+ if (!StaticPrefs::
+ intl_tsf_hack_ms_simplified_chinese_do_not_return_no_layout_error()) {
+ return false;
+ }
+ aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset();
+ aACPStart = std::min(aACPStart, aACPEnd);
+ break;
+ default:
+ return false;
+ }
+
+ // If we hack the queried range for active TIP, that means we should not
+ // return TS_E_NOLAYOUT even if hacked offset is still modified. So, as
+ // far as possible, we should adjust the offset.
+ MOZ_ASSERT(mContentForTSF->IsLayoutChanged());
+ bool collapsed = aACPStart == aACPEnd;
+ // Note that even if all characters in the editor or the composition
+ // string was modified, 0 or start offset of the composition string is
+ // useful because it may return caret rect or old character's rect which
+ // the user still see. That must be useful information for TIP.
+ int32_t firstModifiedOffset =
+ static_cast<int32_t>(mContentForTSF->MinModifiedOffset().value());
+ LONG lastUnmodifiedOffset = std::max(firstModifiedOffset - 1, 0);
+ if (mContentForTSF->IsLayoutChangedAt(aACPStart)) {
+ if (aACPStart >= mContentForTSF->LatestCompositionRange()->StartOffset()) {
+ // If mContentForTSF has last composition string and current
+ // composition string, we can assume that ContentCacheInParent has
+ // cached rects of composition string at least length of current
+ // composition string. Otherwise, we can assume that rect for
+ // first character of composition string is stored since it was
+ // selection start or caret position.
+ LONG maxCachedOffset =
+ mContentForTSF->LatestCompositionRange()->EndOffset();
+ if (mContentForTSF->LastComposition().isSome()) {
+ maxCachedOffset = std::min(
+ maxCachedOffset, mContentForTSF->LastComposition()->EndOffset());
+ }
+ aACPStart = std::min(aACPStart, maxCachedOffset);
+ }
+ // Otherwise, we don't know which character rects are cached. So, we
+ // need to use first unmodified character's rect in this case. Even
+ // if there is no character, the query event will return caret rect
+ // instead.
+ else {
+ aACPStart = lastUnmodifiedOffset;
+ }
+ MOZ_ASSERT(aACPStart <= aACPEnd);
+ }
+
+ // If TIP requests caret rect with collapsed range, we should keep
+ // collapsing the range.
+ if (collapsed) {
+ aACPEnd = aACPStart;
+ }
+ // Let's set aACPEnd to larger offset of last unmodified offset or
+ // aACPStart which may be the first character offset of the composition
+ // string. However, some TIPs may want to know the right edge of the
+ // range. Therefore, if aACPEnd is in composition string and active TIP
+ // doesn't retrieve caret rect (i.e., the range isn't collapsed), we
+ // should keep using the original aACPEnd. Otherwise, we should set
+ // aACPEnd to larger value of aACPStart and lastUnmodifiedOffset.
+ else if (mContentForTSF->IsLayoutChangedAt(aACPEnd) &&
+ !mContentForTSF->LatestCompositionRange()
+ ->IsOffsetInRangeOrEndOffset(aACPEnd)) {
+ aACPEnd = std::max(aACPStart, lastUnmodifiedOffset);
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::HackNoErrorLayoutBugs() hacked the queried range "
+ "for not returning TS_E_NOLAYOUT, new values are: "
+ "aACPStart=%ld, aACPEnd=%ld",
+ this, aACPStart, aACPEnd));
+
+ return true;
+}
+
+STDMETHODIMP
+TSFTextStore::GetScreenExt(TsViewCookie vcView, RECT* prc) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetScreenExt(vcView=%ld, prc=0x%p)", this,
+ vcView, prc));
+
+ if (vcView != TEXTSTORE_DEFAULT_VIEW) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetScreenExt() FAILED due to "
+ "called with invalid view",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (!prc) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetScreenExt() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetScreenExt() returns empty rect "
+ "due to already destroyed",
+ this));
+ prc->left = prc->top = prc->right = prc->bottom = 0;
+ return S_OK;
+ }
+
+ if (!GetScreenExtInternal(*prc)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetScreenExt() FAILED due to "
+ "GetScreenExtInternal() failure",
+ this));
+ return E_FAIL;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetScreenExt() succeeded: "
+ "*prc={ left=%ld, top=%ld, right=%ld, bottom=%ld }",
+ this, prc->left, prc->top, prc->right, prc->bottom));
+ return S_OK;
+}
+
+bool TSFTextStore::GetScreenExtInternal(RECT& aScreenExt) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::GetScreenExtInternal()", this));
+
+ MOZ_ASSERT(!mDestroyed);
+
+ // use NS_QUERY_EDITOR_RECT to get rect in system, screen coordinates
+ WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, mWidget);
+ mWidget->InitEvent(queryEditorRectEvent);
+ DispatchEvent(queryEditorRectEvent);
+ if (queryEditorRectEvent.Failed()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetScreenExtInternal() FAILED due to "
+ "eQueryEditorRect failure",
+ this));
+ return false;
+ }
+
+ nsWindow* refWindow =
+ static_cast<nsWindow*>(!!queryEditorRectEvent.mReply->mFocusedWidget
+ ? queryEditorRectEvent.mReply->mFocusedWidget
+ : static_cast<nsIWidget*>(mWidget.get()));
+ // Result rect is in top level widget coordinates
+ refWindow = refWindow->GetTopLevelWindow(false);
+ if (!refWindow) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetScreenExtInternal() FAILED due to "
+ "no top level window",
+ this));
+ return false;
+ }
+
+ LayoutDeviceIntRect boundRect = refWindow->GetClientBounds();
+ boundRect.MoveTo(0, 0);
+
+ // Clip frame rect to window rect
+ boundRect.IntersectRect(queryEditorRectEvent.mReply->mRect, boundRect);
+ if (!boundRect.IsEmpty()) {
+ boundRect.MoveBy(refWindow->WidgetToScreenOffset());
+ ::SetRect(&aScreenExt, boundRect.X(), boundRect.Y(), boundRect.XMost(),
+ boundRect.YMost());
+ } else {
+ ::SetRectEmpty(&aScreenExt);
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::GetScreenExtInternal() succeeded: "
+ "aScreenExt={ left=%ld, top=%ld, right=%ld, bottom=%ld }",
+ this, aScreenExt.left, aScreenExt.top, aScreenExt.right,
+ aScreenExt.bottom));
+ return true;
+}
+
+STDMETHODIMP
+TSFTextStore::GetWnd(TsViewCookie vcView, HWND* phwnd) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetWnd(vcView=%ld, phwnd=0x%p), "
+ "mWidget=0x%p",
+ this, vcView, phwnd, mWidget.get()));
+
+ if (vcView != TEXTSTORE_DEFAULT_VIEW) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetWnd() FAILED due to "
+ "called with invalid view",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (!phwnd) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::GetScreenExt() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ *phwnd = mWidget ? mWidget->GetWindowHandle() : nullptr;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::GetWnd() succeeded: *phwnd=0x%p", this,
+ static_cast<void*>(*phwnd)));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::InsertTextAtSelection(DWORD dwFlags, const WCHAR* pchText,
+ ULONG cch, LONG* pacpStart, LONG* pacpEnd,
+ TS_TEXTCHANGE* pChange) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::InsertTextAtSelection(dwFlags=%s, "
+ "pchText=0x%p \"%s\", cch=%lu, pacpStart=0x%p, pacpEnd=0x%p, "
+ "pChange=0x%p), mComposition=%s",
+ this,
+ dwFlags == 0 ? "0"
+ : dwFlags == TF_IAS_NOQUERY ? "TF_IAS_NOQUERY"
+ : dwFlags == TF_IAS_QUERYONLY ? "TF_IAS_QUERYONLY"
+ : "Unknown",
+ pchText, pchText && cch ? GetEscapedUTF8String(pchText, cch).get() : "",
+ cch, pacpStart, pacpEnd, pChange, ToString(mComposition).c_str()));
+
+ if (cch && !pchText) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "null pchText",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (TS_IAS_QUERYONLY == dwFlags) {
+ if (!IsReadLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "not locked (read)",
+ this));
+ return TS_E_NOLOCK;
+ }
+
+ if (!pacpStart || !pacpEnd) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ // Get selection first
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (selectionForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "SelectionForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+
+ // Simulate text insertion
+ if (selectionForTSF->HasRange()) {
+ *pacpStart = selectionForTSF->StartOffset();
+ *pacpEnd = selectionForTSF->EndOffset();
+ if (pChange) {
+ *pChange = TS_TEXTCHANGE{.acpStart = selectionForTSF->StartOffset(),
+ .acpOldEnd = selectionForTSF->EndOffset(),
+ .acpNewEnd = selectionForTSF->StartOffset() +
+ static_cast<LONG>(cch)};
+ }
+ } else {
+ // There is no error code to return "no selection" state from this method.
+ // This means that TSF/TIP should check `GetSelection` result first and
+ // stop using this. However, this could be called by TIP/TSF if they do
+ // not do so. Therefore, we should use start of editor instead, but
+ // notify the caller of nothing will be inserted with pChange->acpNewEnd.
+ *pacpStart = *pacpEnd = 0;
+ if (pChange) {
+ *pChange = TS_TEXTCHANGE{.acpStart = 0, .acpOldEnd = 0, .acpNewEnd = 0};
+ }
+ }
+ } else {
+ if (!IsReadWriteLocked()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "not locked (read-write)",
+ this));
+ return TS_E_NOLOCK;
+ }
+
+ if (!pChange) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "null pChange",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (TS_IAS_NOQUERY != dwFlags && (!pacpStart || !pacpEnd)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "null argument",
+ this));
+ return E_INVALIDARG;
+ }
+
+ if (!InsertTextAtSelectionInternal(nsDependentSubstring(pchText, cch),
+ pChange)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to "
+ "InsertTextAtSelectionInternal() failure",
+ this));
+ return E_FAIL;
+ }
+
+ if (TS_IAS_NOQUERY != dwFlags) {
+ *pacpStart = pChange->acpStart;
+ *pacpEnd = pChange->acpNewEnd;
+ }
+ }
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::InsertTextAtSelection() succeeded: "
+ "*pacpStart=%ld, *pacpEnd=%ld, "
+ "*pChange={ acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld })",
+ this, pacpStart ? *pacpStart : 0, pacpEnd ? *pacpEnd : 0,
+ pChange ? pChange->acpStart : 0, pChange ? pChange->acpOldEnd : 0,
+ pChange ? pChange->acpNewEnd : 0));
+ return S_OK;
+}
+
+bool TSFTextStore::InsertTextAtSelectionInternal(const nsAString& aInsertStr,
+ TS_TEXTCHANGE* aTextChange) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::InsertTextAtSelectionInternal("
+ "aInsertStr=\"%s\", aTextChange=0x%p), mComposition=%s",
+ this, GetEscapedUTF8String(aInsertStr).get(), aTextChange,
+ ToString(mComposition).c_str()));
+
+ Maybe<Content>& contentForTSF = ContentForTSF();
+ if (contentForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelectionInternal() failed "
+ "due to ContentForTSF() failure()",
+ this));
+ return false;
+ }
+
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ if (mDestroyed) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::InsertTextAtSelectionInternal() FAILED due to "
+ "destroyed during dispatching a keyboard event",
+ this));
+ return false;
+ }
+
+ const auto numberOfCRLFs = [&]() -> uint32_t {
+ const auto* str = aInsertStr.BeginReading();
+ uint32_t num = 0;
+ for (uint32_t i = 0; i + 1 < aInsertStr.Length(); i++) {
+ if (str[i] == '\r' && str[i + 1] == '\n') {
+ num++;
+ i++;
+ }
+ }
+ return num;
+ }();
+ if (numberOfCRLFs) {
+ nsAutoString key;
+ if (TSFStaticSink::GetActiveTIPNameForTelemetry(key)) {
+ Telemetry::ScalarSet(
+ Telemetry::ScalarID::WIDGET_IME_NAME_ON_WINDOWS_INSERTED_CRLF, key,
+ true);
+ }
+ }
+
+ TS_SELECTION_ACP oldSelection = contentForTSF->Selection()->ACPRef();
+ if (mComposition.isNothing()) {
+ // Use a temporary composition to contain the text
+ PendingAction* compositionStart = mPendingActions.AppendElements(2);
+ PendingAction* compositionEnd = compositionStart + 1;
+
+ compositionStart->mType = PendingAction::Type::eCompositionStart;
+ compositionStart->mSelectionStart = oldSelection.acpStart;
+ compositionStart->mSelectionLength =
+ oldSelection.acpEnd - oldSelection.acpStart;
+ compositionStart->mAdjustSelection = false;
+
+ compositionEnd->mType = PendingAction::Type::eCompositionEnd;
+ compositionEnd->mData = aInsertStr;
+ compositionEnd->mSelectionStart = compositionStart->mSelectionStart;
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::InsertTextAtSelectionInternal() "
+ "appending pending compositionstart and compositionend... "
+ "PendingCompositionStart={ mSelectionStart=%ld, "
+ "mSelectionLength=%ld }, PendingCompositionEnd={ mData=\"%s\" "
+ "(Length()=%zu), mSelectionStart=%ld }",
+ this, compositionStart->mSelectionStart,
+ compositionStart->mSelectionLength,
+ GetEscapedUTF8String(compositionEnd->mData).get(),
+ compositionEnd->mData.Length(), compositionEnd->mSelectionStart));
+ }
+
+ contentForTSF->ReplaceSelectedTextWith(aInsertStr);
+
+ if (aTextChange) {
+ aTextChange->acpStart = oldSelection.acpStart;
+ aTextChange->acpOldEnd = oldSelection.acpEnd;
+ aTextChange->acpNewEnd = contentForTSF->Selection()->EndOffset();
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::InsertTextAtSelectionInternal() "
+ "succeeded: mWidget=0x%p, mWidget->Destroyed()=%s, aTextChange={ "
+ "acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld }",
+ this, mWidget.get(), GetBoolName(mWidget ? mWidget->Destroyed() : true),
+ aTextChange ? aTextChange->acpStart : 0,
+ aTextChange ? aTextChange->acpOldEnd : 0,
+ aTextChange ? aTextChange->acpNewEnd : 0));
+ return true;
+}
+
+STDMETHODIMP
+TSFTextStore::InsertEmbeddedAtSelection(DWORD dwFlags, IDataObject* pDataObject,
+ LONG* pacpStart, LONG* pacpEnd,
+ TS_TEXTCHANGE* pChange) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::InsertEmbeddedAtSelection() called "
+ "but not supported (E_NOTIMPL)",
+ this));
+
+ // embedded objects are not supported
+ return E_NOTIMPL;
+}
+
+HRESULT TSFTextStore::RecordCompositionStartAction(
+ ITfCompositionView* aCompositionView, ITfRange* aRange,
+ bool aPreserveSelection) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RecordCompositionStartAction("
+ "aCompositionView=0x%p, aRange=0x%p, aPreserveSelection=%s), "
+ "mComposition=%s",
+ this, aCompositionView, aRange, GetBoolName(aPreserveSelection),
+ ToString(mComposition).c_str()));
+
+ LONG start = 0, length = 0;
+ HRESULT hr = GetRangeExtent(aRange, &start, &length);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED "
+ "due to GetRangeExtent() failure",
+ this));
+ return hr;
+ }
+
+ return RecordCompositionStartAction(aCompositionView, start, length,
+ aPreserveSelection);
+}
+
+HRESULT TSFTextStore::RecordCompositionStartAction(
+ ITfCompositionView* aCompositionView, LONG aStart, LONG aLength,
+ bool aPreserveSelection) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RecordCompositionStartAction("
+ "aCompositionView=0x%p, aStart=%ld, aLength=%ld, "
+ "aPreserveSelection=%s), "
+ "mComposition=%s",
+ this, aCompositionView, aStart, aLength,
+ GetBoolName(aPreserveSelection), ToString(mComposition).c_str()));
+
+ Maybe<Content>& contentForTSF = ContentForTSF();
+ if (contentForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED "
+ "due to ContentForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ if (mDestroyed) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED due to "
+ "destroyed during dispatching a keyboard event",
+ this));
+ return false;
+ }
+
+ CompleteLastActionIfStillIncomplete();
+
+ // TIP may have inserted text at selection before calling
+ // OnStartComposition(). In this case, we've already created a pending
+ // compositionend. If new composition replaces all commit string of the
+ // pending compositionend, we should cancel the pending compositionend and
+ // keep the previous composition normally.
+ // On Windows 7, MS-IME for Korean, MS-IME 2010 for Korean and MS Old Hangul
+ // may start composition with calling InsertTextAtSelection() and
+ // OnStartComposition() with this order (bug 1208043).
+ // On Windows 10, MS Pinyin, MS Wubi, MS ChangJie and MS Quick commits
+ // last character and replace it with empty string with new composition
+ // when user removes last character of composition string with Backspace
+ // key (bug 1462257).
+ if (!aPreserveSelection &&
+ IsLastPendingActionCompositionEndAt(aStart, aLength)) {
+ const PendingAction& pendingCompositionEnd = mPendingActions.LastElement();
+ contentForTSF->RestoreCommittedComposition(aCompositionView,
+ pendingCompositionEnd);
+ mPendingActions.RemoveLastElement();
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RecordCompositionStartAction() "
+ "succeeded: restoring the committed string as composing string, "
+ "mComposition=%s, mSelectionForTSF=%s",
+ this, ToString(mComposition).c_str(),
+ ToString(mSelectionForTSF).c_str()));
+ return S_OK;
+ }
+
+ PendingAction* action = mPendingActions.AppendElement();
+ action->mType = PendingAction::Type::eCompositionStart;
+ action->mSelectionStart = aStart;
+ action->mSelectionLength = aLength;
+
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (selectionForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED "
+ "due to SelectionForTSF() failure",
+ this));
+ action->mAdjustSelection = true;
+ } else if (!selectionForTSF->HasRange()) {
+ // If there is no selection, let's collapse seletion to the insertion point.
+ action->mAdjustSelection = true;
+ } else if (selectionForTSF->MinOffset() != aStart ||
+ selectionForTSF->MaxOffset() != aStart + aLength) {
+ // If new composition range is different from current selection range,
+ // we need to set selection before dispatching compositionstart event.
+ action->mAdjustSelection = true;
+ } else {
+ // We shouldn't dispatch selection set event before dispatching
+ // compositionstart event because it may cause put caret different
+ // position in HTML editor since generated flat text content and offset in
+ // it are lossy data of HTML contents.
+ action->mAdjustSelection = false;
+ }
+
+ contentForTSF->StartComposition(aCompositionView, *action,
+ aPreserveSelection);
+ MOZ_ASSERT(mComposition.isSome());
+ action->mData = mComposition->DataRef();
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RecordCompositionStartAction() succeeded: "
+ "mComposition=%s, mSelectionForTSF=%s }",
+ this, ToString(mComposition).c_str(),
+ ToString(mSelectionForTSF).c_str()));
+ return S_OK;
+}
+
+HRESULT
+TSFTextStore::RecordCompositionEndAction() {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::RecordCompositionEndAction(), "
+ "mComposition=%s",
+ this, ToString(mComposition).c_str()));
+
+ MOZ_ASSERT(mComposition.isSome());
+
+ if (mComposition.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due to "
+ "no composition",
+ this));
+ return false;
+ }
+
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due to "
+ "destroyed during dispatching a keyboard event",
+ this));
+ return false;
+ }
+
+ // If we're handling incomplete composition update or already handled
+ // composition update, we can forget them since composition end will send
+ // the latest composition string and it overwrites the composition string
+ // even if we dispatch eCompositionChange event before that. So, let's
+ // forget all composition updates now.
+ RemoveLastCompositionUpdateActions();
+ PendingAction* action = mPendingActions.AppendElement();
+ action->mType = PendingAction::Type::eCompositionEnd;
+ action->mData = mComposition->DataRef();
+ action->mSelectionStart = mComposition->StartOffset();
+
+ Maybe<Content>& contentForTSF = ContentForTSF();
+ if (contentForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due "
+ "to ContentForTSF() failure",
+ this));
+ return E_FAIL;
+ }
+ contentForTSF->EndComposition(*action);
+
+ // If this composition was restart but the composition doesn't modify
+ // anything, we should remove the pending composition for preventing to
+ // dispatch redundant composition events.
+ for (size_t i = mPendingActions.Length(), j = 1; i > 0; --i, ++j) {
+ PendingAction& pendingAction = mPendingActions[i - 1];
+ if (pendingAction.mType == PendingAction::Type::eCompositionStart) {
+ if (pendingAction.mData != action->mData) {
+ break;
+ }
+ // When only setting selection is necessary, we should append it.
+ if (pendingAction.mAdjustSelection) {
+ LONG selectionStart = pendingAction.mSelectionStart;
+ LONG selectionLength = pendingAction.mSelectionLength;
+
+ PendingAction* setSelection = mPendingActions.AppendElement();
+ setSelection->mType = PendingAction::Type::eSetSelection;
+ setSelection->mSelectionStart = selectionStart;
+ setSelection->mSelectionLength = selectionLength;
+ setSelection->mSelectionReversed = false;
+ }
+ // Remove the redundant pending composition.
+ mPendingActions.RemoveElementsAt(i - 1, j);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RecordCompositionEndAction(), "
+ "succeeded, but the composition was canceled due to redundant",
+ this));
+ return S_OK;
+ }
+ }
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::RecordCompositionEndAction(), succeeded", this));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::OnStartComposition(ITfCompositionView* pComposition, BOOL* pfOk) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnStartComposition(pComposition=0x%p, "
+ "pfOk=0x%p), mComposition=%s",
+ this, pComposition, pfOk, ToString(mComposition).c_str()));
+
+ AutoPendingActionAndContentFlusher flusher(this);
+
+ *pfOk = FALSE;
+
+ // Only one composition at a time
+ if (mComposition.isSome()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnStartComposition() FAILED due to "
+ "there is another composition already (but returns S_OK)",
+ this));
+ return S_OK;
+ }
+
+ RefPtr<ITfRange> range;
+ HRESULT hr = pComposition->GetRange(getter_AddRefs(range));
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnStartComposition() FAILED due to "
+ "pComposition->GetRange() failure",
+ this));
+ return hr;
+ }
+ hr = RecordCompositionStartAction(pComposition, range, false);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnStartComposition() FAILED due to "
+ "RecordCompositionStartAction() failure",
+ this));
+ return hr;
+ }
+
+ *pfOk = TRUE;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnStartComposition() succeeded", this));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::OnUpdateComposition(ITfCompositionView* pComposition,
+ ITfRange* pRangeNew) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnUpdateComposition(pComposition=0x%p, "
+ "pRangeNew=0x%p), mComposition=%s",
+ this, pComposition, pRangeNew, ToString(mComposition).c_str()));
+
+ AutoPendingActionAndContentFlusher flusher(this);
+
+ if (!mDocumentMgr || !mContext) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to "
+ "not ready for the composition",
+ this));
+ return E_UNEXPECTED;
+ }
+ if (mComposition.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to "
+ "no active composition",
+ this));
+ return E_UNEXPECTED;
+ }
+ if (mComposition->GetView() != pComposition) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to "
+ "different composition view specified",
+ this));
+ return E_UNEXPECTED;
+ }
+
+ // pRangeNew is null when the update is not complete
+ if (!pRangeNew) {
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to "
+ "destroyed during dispatching a keyboard event",
+ this));
+ return E_FAIL;
+ }
+ PendingAction* action = LastOrNewPendingCompositionUpdate();
+ action->mIncomplete = true;
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnUpdateComposition() succeeded but "
+ "not complete",
+ this));
+ return S_OK;
+ }
+
+ HRESULT hr = RestartCompositionIfNecessary(pRangeNew);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to "
+ "RestartCompositionIfNecessary() failure",
+ this));
+ return hr;
+ }
+
+ hr = RecordCompositionUpdateAction();
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to "
+ "RecordCompositionUpdateAction() failure",
+ this));
+ return hr;
+ }
+
+ if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) {
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (selectionForTSF.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to "
+ "SelectionForTSF() failure",
+ this));
+ return S_OK; // Don't return error only when we're logging.
+ }
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnUpdateComposition() succeeded: "
+ "mComposition=%s, SelectionForTSF()=%s",
+ this, ToString(mComposition).c_str(),
+ ToString(selectionForTSF).c_str()));
+ }
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::OnEndComposition(ITfCompositionView* pComposition) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnEndComposition(pComposition=0x%p), "
+ "mComposition=%s",
+ this, pComposition, ToString(mComposition).c_str()));
+
+ AutoPendingActionAndContentFlusher flusher(this);
+
+ if (mComposition.isNothing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnEndComposition() FAILED due to "
+ "no active composition",
+ this));
+ return E_UNEXPECTED;
+ }
+
+ if (mComposition->GetView() != pComposition) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnEndComposition() FAILED due to "
+ "different composition view specified",
+ this));
+ return E_UNEXPECTED;
+ }
+
+ HRESULT hr = RecordCompositionEndAction();
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::OnEndComposition() FAILED due to "
+ "RecordCompositionEndAction() failure",
+ this));
+ return hr;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnEndComposition(), succeeded", this));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::AdviseMouseSink(ITfRangeACP* range, ITfMouseSink* pSink,
+ DWORD* pdwCookie) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::AdviseMouseSink(range=0x%p, pSink=0x%p, "
+ "pdwCookie=0x%p)",
+ this, range, pSink, pdwCookie));
+
+ if (!pdwCookie) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the "
+ "pdwCookie is null",
+ this));
+ return E_INVALIDARG;
+ }
+ // Initialize the result with invalid cookie for safety.
+ *pdwCookie = MouseTracker::kInvalidCookie;
+
+ if (!range) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the "
+ "range is null",
+ this));
+ return E_INVALIDARG;
+ }
+ if (!pSink) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the "
+ "pSink is null",
+ this));
+ return E_INVALIDARG;
+ }
+
+ // Looking for an unusing tracker.
+ MouseTracker* tracker = nullptr;
+ for (size_t i = 0; i < mMouseTrackers.Length(); i++) {
+ if (mMouseTrackers[i].IsUsing()) {
+ continue;
+ }
+ tracker = &mMouseTrackers[i];
+ }
+ // If there is no unusing tracker, create new one.
+ // XXX Should we make limitation of the number of installs?
+ if (!tracker) {
+ tracker = mMouseTrackers.AppendElement();
+ HRESULT hr = tracker->Init(this);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to "
+ "failure of MouseTracker::Init()",
+ this));
+ return hr;
+ }
+ }
+ HRESULT hr = tracker->AdviseSink(this, range, pSink);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to failure "
+ "of MouseTracker::Init()",
+ this));
+ return hr;
+ }
+ *pdwCookie = tracker->Cookie();
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::AdviseMouseSink(), succeeded, "
+ "*pdwCookie=%ld",
+ this, *pdwCookie));
+ return S_OK;
+}
+
+STDMETHODIMP
+TSFTextStore::UnadviseMouseSink(DWORD dwCookie) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::UnadviseMouseSink(dwCookie=%ld)", this, dwCookie));
+ if (dwCookie == MouseTracker::kInvalidCookie) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to "
+ "the cookie is invalid value",
+ this));
+ return E_INVALIDARG;
+ }
+ // The cookie value must be an index of mMouseTrackers.
+ // We can use this shortcut for now.
+ if (static_cast<size_t>(dwCookie) >= mMouseTrackers.Length()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to "
+ "the cookie is too large value",
+ this));
+ return E_INVALIDARG;
+ }
+ MouseTracker& tracker = mMouseTrackers[dwCookie];
+ if (!tracker.IsUsing()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to "
+ "the found tracker uninstalled already",
+ this));
+ return E_INVALIDARG;
+ }
+ tracker.UnadviseSink();
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::UnadviseMouseSink(), succeeded", this));
+ return S_OK;
+}
+
+// static
+nsresult TSFTextStore::OnFocusChange(bool aGotFocus, nsWindow* aFocusedWidget,
+ const InputContext& aContext) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ (" TSFTextStore::OnFocusChange(aGotFocus=%s, "
+ "aFocusedWidget=0x%p, aContext=%s), "
+ "sThreadMgr=0x%p, sEnabledTextStore=0x%p",
+ GetBoolName(aGotFocus), aFocusedWidget,
+ mozilla::ToString(aContext).c_str(), sThreadMgr.get(),
+ sEnabledTextStore.get()));
+
+ if (NS_WARN_IF(!IsInTSFMode())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<ITfDocumentMgr> prevFocusedDocumentMgr;
+ bool hasFocus = ThinksHavingFocus();
+ RefPtr<TSFTextStore> oldTextStore = sEnabledTextStore.forget();
+
+ // If currently oldTextStore still has focus, notifies TSF of losing focus.
+ if (hasFocus) {
+ RefPtr<ITfThreadMgr> threadMgr = sThreadMgr;
+ DebugOnly<HRESULT> hr = threadMgr->AssociateFocus(
+ oldTextStore->mWidget->GetWindowHandle(), nullptr,
+ getter_AddRefs(prevFocusedDocumentMgr));
+ NS_ASSERTION(SUCCEEDED(hr), "Disassociating focus failed");
+ NS_ASSERTION(prevFocusedDocumentMgr == oldTextStore->mDocumentMgr,
+ "different documentMgr has been associated with the window");
+ }
+
+ // Even if there was a focused TextStore, we won't use it with new focused
+ // editor. So, release it now.
+ if (oldTextStore) {
+ oldTextStore->Destroy();
+ }
+
+ if (NS_WARN_IF(!sThreadMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::OnFocusChange() FAILED, due to "
+ "sThreadMgr being destroyed during calling "
+ "ITfThreadMgr::AssociateFocus()"));
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_WARN_IF(sEnabledTextStore)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ (" TSFTextStore::OnFocusChange() FAILED, due to "
+ "nested event handling has created another focused TextStore during "
+ "calling ITfThreadMgr::AssociateFocus()"));
+ return NS_ERROR_FAILURE;
+ }
+
+ // If this is a notification of blur, move focus to the dummy document
+ // manager.
+ if (!aGotFocus || !aContext.mIMEState.IsEditable()) {
+ RefPtr<ITfThreadMgr> threadMgr = sThreadMgr;
+ RefPtr<ITfDocumentMgr> disabledDocumentMgr = sDisabledDocumentMgr;
+ HRESULT hr = threadMgr->SetFocus(disabledDocumentMgr);
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::OnFocusChange() FAILED due to "
+ "ITfThreadMgr::SetFocus() failure"));
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+ }
+
+ // If an editor is getting focus, create new TextStore and set focus.
+ if (NS_WARN_IF(!CreateAndSetFocus(aFocusedWidget, aContext))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::OnFocusChange() FAILED due to "
+ "ITfThreadMgr::CreateAndSetFocus() failure"));
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+// static
+void TSFTextStore::EnsureToDestroyAndReleaseEnabledTextStoreIf(
+ RefPtr<TSFTextStore>& aTextStore) {
+ aTextStore->Destroy();
+ if (sEnabledTextStore == aTextStore) {
+ sEnabledTextStore = nullptr;
+ }
+ aTextStore = nullptr;
+}
+
+// static
+bool TSFTextStore::CreateAndSetFocus(nsWindow* aFocusedWidget,
+ const InputContext& aContext) {
+ // TSF might do something which causes that we need to access static methods
+ // of TSFTextStore. At that time, sEnabledTextStore may be necessary.
+ // So, we should set sEnabledTextStore directly.
+ RefPtr<TSFTextStore> textStore = new TSFTextStore();
+ sEnabledTextStore = textStore;
+ if (NS_WARN_IF(!textStore->Init(aFocusedWidget, aContext))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "TSFTextStore::Init() failure"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+ RefPtr<ITfDocumentMgr> newDocMgr = textStore->mDocumentMgr;
+ if (NS_WARN_IF(!newDocMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "invalid TSFTextStore::mDocumentMgr"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+ if (aContext.mIMEState.mEnabled == IMEEnabled::Password) {
+ MarkContextAsKeyboardDisabled(textStore->mContext);
+ RefPtr<ITfContext> topContext;
+ newDocMgr->GetTop(getter_AddRefs(topContext));
+ if (topContext && topContext != textStore->mContext) {
+ MarkContextAsKeyboardDisabled(topContext);
+ }
+ }
+
+ HRESULT hr;
+ RefPtr<ITfThreadMgr> threadMgr = sThreadMgr;
+ hr = threadMgr->SetFocus(newDocMgr);
+
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "ITfTheadMgr::SetFocus() failure"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+ if (NS_WARN_IF(!sThreadMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "sThreadMgr being destroyed during calling "
+ "ITfTheadMgr::SetFocus()"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+ if (NS_WARN_IF(sEnabledTextStore != textStore)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "creating TextStore has lost focus during calling "
+ "ITfThreadMgr::SetFocus()"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+
+ // Use AssociateFocus() for ensuring that any native focus event
+ // never steal focus from our documentMgr.
+ RefPtr<ITfDocumentMgr> prevFocusedDocumentMgr;
+ hr = threadMgr->AssociateFocus(aFocusedWidget->GetWindowHandle(), newDocMgr,
+ getter_AddRefs(prevFocusedDocumentMgr));
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "ITfTheadMgr::AssociateFocus() failure"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+ if (NS_WARN_IF(!sThreadMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "sThreadMgr being destroyed during calling "
+ "ITfTheadMgr::AssociateFocus()"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+ if (NS_WARN_IF(sEnabledTextStore != textStore)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "creating TextStore has lost focus during calling "
+ "ITfTheadMgr::AssociateFocus()"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+
+ if (textStore->mSink) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" TSFTextStore::CreateAndSetFocus(), calling "
+ "ITextStoreACPSink::OnLayoutChange(TS_LC_CREATE) for 0x%p...",
+ textStore.get()));
+ RefPtr<ITextStoreACPSink> sink = textStore->mSink;
+ sink->OnLayoutChange(TS_LC_CREATE, TEXTSTORE_DEFAULT_VIEW);
+ if (NS_WARN_IF(sEnabledTextStore != textStore)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CreateAndSetFocus() FAILED due to "
+ "creating TextStore has lost focus during calling "
+ "ITextStoreACPSink::OnLayoutChange(TS_LC_CREATE)"));
+ EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore);
+ return false;
+ }
+ }
+ return true;
+}
+
+// static
+IMENotificationRequests TSFTextStore::GetIMENotificationRequests() {
+ if (!sEnabledTextStore || NS_WARN_IF(!sEnabledTextStore->mDocumentMgr)) {
+ // If there is no active text store, we don't need any notifications
+ // since there is no sink which needs notifications.
+ return IMENotificationRequests();
+ }
+
+ // Otherwise, requests all notifications since even if some of them may not
+ // be required by the sink of active TIP, active TIP may be changed and
+ // other TIPs may need all notifications.
+ // Note that Windows temporarily steal focus from active window if the main
+ // process which created the window becomes busy. In this case, we shouldn't
+ // commit composition since user may want to continue to compose the
+ // composition after becoming not busy. Therefore, we need notifications
+ // even during deactive.
+ // Be aware, we don't need to check actual focused text store. For example,
+ // MS-IME for Japanese handles focus messages by themselves and sets focused
+ // text store to nullptr when the process is being inactivated. However,
+ // we still need to reuse sEnabledTextStore if the process is activated and
+ // focused element isn't changed. Therefore, if sEnabledTextStore isn't
+ // nullptr, we need to keep notifying the sink even when it is not focused
+ // text store for the thread manager.
+ return IMENotificationRequests(
+ IMENotificationRequests::NOTIFY_TEXT_CHANGE |
+ IMENotificationRequests::NOTIFY_POSITION_CHANGE |
+ IMENotificationRequests::NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR |
+ IMENotificationRequests::NOTIFY_DURING_DEACTIVE);
+}
+
+nsresult TSFTextStore::OnTextChangeInternal(
+ const IMENotification& aIMENotification) {
+ const TextChangeDataBase& textChangeData = aIMENotification.mTextChangeData;
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::OnTextChangeInternal(aIMENotification={ "
+ "mMessage=0x%08X, mTextChangeData=%s }), "
+ "mDestroyed=%s, mSink=0x%p, mSinkMask=%s, "
+ "mComposition=%s",
+ this, aIMENotification.mMessage,
+ mozilla::ToString(textChangeData).c_str(), GetBoolName(mDestroyed),
+ mSink.get(), GetSinkMaskNameStr(mSinkMask).get(),
+ ToString(mComposition).c_str()));
+
+ if (mDestroyed) {
+ // If this instance is already destroyed, we shouldn't notify TSF of any
+ // changes.
+ return NS_OK;
+ }
+
+ mDeferNotifyingTSFUntilNextUpdate = false;
+
+ // Different from selection change, we don't modify anything with text
+ // change data. Therefore, if neither TSF not TIP wants text change
+ // notifications, we don't need to store the changes.
+ if (!mSink || !(mSinkMask & TS_AS_TEXT_CHANGE)) {
+ return NS_OK;
+ }
+
+ // Merge any text change data even if it's caused by composition.
+ mPendingTextChangeData.MergeWith(textChangeData);
+
+ MaybeFlushPendingNotifications();
+
+ return NS_OK;
+}
+
+void TSFTextStore::NotifyTSFOfTextChange() {
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(!IsReadLocked());
+ MOZ_ASSERT(mComposition.isNothing());
+ MOZ_ASSERT(mPendingTextChangeData.IsValid());
+
+ // If the text changes are caused only by composition, we don't need to
+ // notify TSF of the text changes.
+ if (mPendingTextChangeData.mCausedOnlyByComposition) {
+ mPendingTextChangeData.Clear();
+ return;
+ }
+
+ // First, forget cached selection.
+ mSelectionForTSF.reset();
+
+ // For making it safer, we should check if there is a valid sink to receive
+ // text change notification.
+ if (NS_WARN_IF(!mSink) || NS_WARN_IF(!(mSinkMask & TS_AS_TEXT_CHANGE))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::NotifyTSFOfTextChange() FAILED due to "
+ "mSink is not ready to call ITextStoreACPSink::OnTextChange()...",
+ this));
+ mPendingTextChangeData.Clear();
+ return;
+ }
+
+ if (NS_WARN_IF(!mPendingTextChangeData.IsInInt32Range())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::NotifyTSFOfTextChange() FAILED due to "
+ "offset is too big for calling "
+ "ITextStoreACPSink::OnTextChange()...",
+ this));
+ mPendingTextChangeData.Clear();
+ return;
+ }
+
+ TS_TEXTCHANGE textChange;
+ textChange.acpStart = static_cast<LONG>(mPendingTextChangeData.mStartOffset);
+ textChange.acpOldEnd =
+ static_cast<LONG>(mPendingTextChangeData.mRemovedEndOffset);
+ textChange.acpNewEnd =
+ static_cast<LONG>(mPendingTextChangeData.mAddedEndOffset);
+ mPendingTextChangeData.Clear();
+
+ MOZ_LOG(
+ gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfTextChange(), calling "
+ "ITextStoreACPSink::OnTextChange(0, { acpStart=%ld, acpOldEnd=%ld, "
+ "acpNewEnd=%ld })...",
+ this, textChange.acpStart, textChange.acpOldEnd, textChange.acpNewEnd));
+ RefPtr<ITextStoreACPSink> sink = mSink;
+ sink->OnTextChange(0, &textChange);
+}
+
+nsresult TSFTextStore::OnSelectionChangeInternal(
+ const IMENotification& aIMENotification) {
+ const SelectionChangeDataBase& selectionChangeData =
+ aIMENotification.mSelectionChangeData;
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::OnSelectionChangeInternal("
+ "aIMENotification={ mSelectionChangeData=%s }), mDestroyed=%s, "
+ "mSink=0x%p, mSinkMask=%s, mIsRecordingActionsWithoutLock=%s, "
+ "mComposition=%s",
+ this, mozilla::ToString(selectionChangeData).c_str(),
+ GetBoolName(mDestroyed), mSink.get(),
+ GetSinkMaskNameStr(mSinkMask).get(),
+ GetBoolName(mIsRecordingActionsWithoutLock),
+ ToString(mComposition).c_str()));
+
+ if (mDestroyed) {
+ // If this instance is already destroyed, we shouldn't notify TSF of any
+ // changes.
+ return NS_OK;
+ }
+
+ mDeferNotifyingTSFUntilNextUpdate = false;
+
+ // Assign the new selection change data to the pending selection change data
+ // because only the latest selection data is necessary.
+ // Note that this is necessary to update mSelectionForTSF. Therefore, even if
+ // neither TSF nor TIP wants selection change notifications, we need to
+ // store the selection information.
+ mPendingSelectionChangeData = Some(selectionChangeData);
+
+ // Flush remaining pending notifications here if it's possible.
+ MaybeFlushPendingNotifications();
+
+ // If we're available, we should create native caret instead of IMEHandler
+ // because we may have some cache to do it.
+ // Note that if we have composition, we'll notified composition-updated
+ // later so that we don't need to create native caret in such case.
+ if (!IsHandlingCompositionInContent() &&
+ IMEHandler::NeedsToCreateNativeCaret()) {
+ CreateNativeCaret();
+ }
+
+ return NS_OK;
+}
+
+void TSFTextStore::NotifyTSFOfSelectionChange() {
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(!IsReadLocked());
+ MOZ_ASSERT(mComposition.isNothing());
+ MOZ_ASSERT(mPendingSelectionChangeData.isSome());
+
+ // If selection range isn't actually changed, we don't need to notify TSF
+ // of this selection change.
+ if (mSelectionForTSF.isNothing()) {
+ MOZ_DIAGNOSTIC_ASSERT(!mIsInitializingSelectionForTSF,
+ "While mSelectionForTSF is being initialized, this "
+ "should not be called");
+ mSelectionForTSF.emplace(*mPendingSelectionChangeData);
+ } else if (!mSelectionForTSF->SetSelection(*mPendingSelectionChangeData)) {
+ mPendingSelectionChangeData.reset();
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::NotifyTSFOfSelectionChange(), "
+ "selection isn't actually changed.",
+ this));
+ return;
+ }
+
+ mPendingSelectionChangeData.reset();
+
+ if (!mSink || !(mSinkMask & TS_AS_SEL_CHANGE)) {
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfSelectionChange(), calling "
+ "ITextStoreACPSink::OnSelectionChange()...",
+ this));
+ RefPtr<ITextStoreACPSink> sink = mSink;
+ sink->OnSelectionChange();
+}
+
+nsresult TSFTextStore::OnLayoutChangeInternal() {
+ if (mDestroyed) {
+ // If this instance is already destroyed, we shouldn't notify TSF of any
+ // changes.
+ return NS_OK;
+ }
+
+ NS_ENSURE_TRUE(mContext, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(mSink, NS_ERROR_FAILURE);
+
+ mDeferNotifyingTSFUntilNextUpdate = false;
+
+ nsresult rv = NS_OK;
+
+ // We need to notify TSF of layout change even if the document is locked.
+ // So, don't use MaybeFlushPendingNotifications() for flushing pending
+ // layout change.
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::OnLayoutChangeInternal(), calling "
+ "NotifyTSFOfLayoutChange()...",
+ this));
+ if (NS_WARN_IF(!NotifyTSFOfLayoutChange())) {
+ rv = NS_ERROR_FAILURE;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::OnLayoutChangeInternal(), calling "
+ "MaybeFlushPendingNotifications()...",
+ this));
+ MaybeFlushPendingNotifications();
+
+ return rv;
+}
+
+bool TSFTextStore::NotifyTSFOfLayoutChange() {
+ MOZ_ASSERT(!mDestroyed);
+
+ // If we're waiting a query of layout information from TIP, it means that
+ // we've returned TS_E_NOLAYOUT error.
+ bool returnedNoLayoutError = mHasReturnedNoLayoutError || mWaitingQueryLayout;
+
+ // If we returned TS_E_NOLAYOUT, TIP should query the computed layout again.
+ mWaitingQueryLayout = returnedNoLayoutError;
+
+ // For avoiding to call this method again at unlocking the document during
+ // calls of OnLayoutChange(), reset mHasReturnedNoLayoutError.
+ mHasReturnedNoLayoutError = false;
+
+ // Now, layout has been computed. We should notify mContentForTSF for
+ // making GetTextExt() and GetACPFromPoint() not return TS_E_NOLAYOUT.
+ if (mContentForTSF.isSome()) {
+ mContentForTSF->OnLayoutChanged();
+ }
+
+ if (IMEHandler::NeedsToCreateNativeCaret()) {
+ // If we're available, we should create native caret instead of IMEHandler
+ // because we may have some cache to do it.
+ CreateNativeCaret();
+ } else {
+ // Now, the caret position is different from ours. Destroy the native caret
+ // if we've create it only for GetTextExt().
+ IMEHandler::MaybeDestroyNativeCaret();
+ }
+
+ // This method should return true if either way succeeds.
+ bool ret = true;
+
+ if (mSink) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "calling ITextStoreACPSink::OnLayoutChange()...",
+ this));
+ RefPtr<ITextStoreACPSink> sink = mSink;
+ HRESULT hr = sink->OnLayoutChange(TS_LC_CHANGE, TEXTSTORE_DEFAULT_VIEW);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "called ITextStoreACPSink::OnLayoutChange()",
+ this));
+ ret = SUCCEEDED(hr);
+ }
+
+ // The layout change caused by composition string change should cause
+ // calling ITfContextOwnerServices::OnLayoutChange() too.
+ if (returnedNoLayoutError && mContext) {
+ RefPtr<ITfContextOwnerServices> service;
+ mContext->QueryInterface(IID_ITfContextOwnerServices,
+ getter_AddRefs(service));
+ if (service) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "calling ITfContextOwnerServices::OnLayoutChange()...",
+ this));
+ HRESULT hr = service->OnLayoutChange();
+ ret = ret && SUCCEEDED(hr);
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "called ITfContextOwnerServices::OnLayoutChange()",
+ this));
+ }
+ }
+
+ if (!mWidget || mWidget->Destroyed()) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "the widget is destroyed during calling OnLayoutChange()",
+ this));
+ return ret;
+ }
+
+ if (mDestroyed) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "the TSFTextStore instance is destroyed during calling "
+ "OnLayoutChange()",
+ this));
+ return ret;
+ }
+
+ // If we returned TS_E_NOLAYOUT again, we need another call of
+ // OnLayoutChange() later. So, let's wait a query from TIP.
+ if (mHasReturnedNoLayoutError) {
+ mWaitingQueryLayout = true;
+ }
+
+ if (!mWaitingQueryLayout) {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "succeeded notifying TIP of our layout change",
+ this));
+ return ret;
+ }
+
+ // If we believe that TIP needs to retry to retrieve our layout information
+ // later, we should call it with ::PostMessage() hack.
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), "
+ "posing MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE for calling "
+ "OnLayoutChange() again...",
+ this));
+ ::PostMessage(mWidget->GetWindowHandle(), MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE,
+ reinterpret_cast<WPARAM>(this), 0);
+
+ return true;
+}
+
+void TSFTextStore::NotifyTSFOfLayoutChangeAgain() {
+ // Don't notify TSF of layout change after destroyed.
+ if (mDestroyed) {
+ mWaitingQueryLayout = false;
+ return;
+ }
+
+ // Before preforming this method, TIP has accessed our layout information by
+ // itself. In such case, we don't need to call OnLayoutChange() anymore.
+ if (!mWaitingQueryLayout) {
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), "
+ "calling NotifyTSFOfLayoutChange()...",
+ this));
+ NotifyTSFOfLayoutChange();
+
+ // If TIP didn't retrieved our layout information during a call of
+ // NotifyTSFOfLayoutChange(), it means that the TIP already gave up to
+ // retry to retrieve layout information or doesn't necessary it anymore.
+ // But don't forget that the call may have caused returning TS_E_NOLAYOUT
+ // error again. In such case we still need to call OnLayoutChange() later.
+ if (!mHasReturnedNoLayoutError && mWaitingQueryLayout) {
+ mWaitingQueryLayout = false;
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), "
+ "called NotifyTSFOfLayoutChange() but TIP didn't retry to "
+ "retrieve the layout information",
+ this));
+ } else {
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), "
+ "called NotifyTSFOfLayoutChange()",
+ this));
+ }
+}
+
+nsresult TSFTextStore::OnUpdateCompositionInternal() {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::OnUpdateCompositionInternal(), "
+ "mDestroyed=%s, mDeferNotifyingTSFUntilNextUpdate=%s",
+ this, GetBoolName(mDestroyed),
+ GetBoolName(mDeferNotifyingTSFUntilNextUpdate)));
+
+ // There are nothing to do after destroyed.
+ if (mDestroyed) {
+ return NS_OK;
+ }
+
+ // Update cached data now because all pending events have been handled now.
+ if (mContentForTSF.isSome()) {
+ mContentForTSF->OnCompositionEventsHandled();
+ }
+
+ // If composition is completely finished both in TSF/TIP and the focused
+ // editor which may be in a remote process, we can clear the cache and don't
+ // have it until starting next composition.
+ if (mComposition.isNothing() && !IsHandlingCompositionInContent()) {
+ mDeferClearingContentForTSF = false;
+ }
+ mDeferNotifyingTSFUntilNextUpdate = false;
+ MaybeFlushPendingNotifications();
+
+ // If we're available, we should create native caret instead of IMEHandler
+ // because we may have some cache to do it.
+ if (IMEHandler::NeedsToCreateNativeCaret()) {
+ CreateNativeCaret();
+ }
+
+ return NS_OK;
+}
+
+nsresult TSFTextStore::OnMouseButtonEventInternal(
+ const IMENotification& aIMENotification) {
+ if (mDestroyed) {
+ // If this instance is already destroyed, we shouldn't notify TSF of any
+ // events.
+ return NS_OK;
+ }
+
+ if (mMouseTrackers.IsEmpty()) {
+ return NS_OK;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::OnMouseButtonEventInternal("
+ "aIMENotification={ mEventMessage=%s, mOffset=%u, mCursorPos=%s, "
+ "mCharRect=%s, mButton=%s, mButtons=%s, mModifiers=%s })",
+ this, ToChar(aIMENotification.mMouseButtonEventData.mEventMessage),
+ aIMENotification.mMouseButtonEventData.mOffset,
+ ToString(aIMENotification.mMouseButtonEventData.mCursorPos).c_str(),
+ ToString(aIMENotification.mMouseButtonEventData.mCharRect).c_str(),
+ GetMouseButtonName(aIMENotification.mMouseButtonEventData.mButton),
+ GetMouseButtonsName(aIMENotification.mMouseButtonEventData.mButtons)
+ .get(),
+ GetModifiersName(aIMENotification.mMouseButtonEventData.mModifiers)
+ .get()));
+
+ uint32_t offset = aIMENotification.mMouseButtonEventData.mOffset;
+ if (offset > static_cast<uint32_t>(LONG_MAX)) {
+ return NS_OK;
+ }
+ LayoutDeviceIntRect charRect =
+ aIMENotification.mMouseButtonEventData.mCharRect;
+ LayoutDeviceIntPoint cursorPos =
+ aIMENotification.mMouseButtonEventData.mCursorPos;
+ ULONG quadrant = 1;
+ if (charRect.Width() > 0) {
+ int32_t cursorXInChar = cursorPos.x - charRect.X();
+ quadrant = cursorXInChar * 4 / charRect.Width();
+ quadrant = (quadrant + 2) % 4;
+ }
+ ULONG edge = quadrant < 2 ? offset + 1 : offset;
+ DWORD buttonStatus = 0;
+ bool isMouseUp =
+ aIMENotification.mMouseButtonEventData.mEventMessage == eMouseUp;
+ if (!isMouseUp) {
+ switch (aIMENotification.mMouseButtonEventData.mButton) {
+ case MouseButton::ePrimary:
+ buttonStatus = MK_LBUTTON;
+ break;
+ case MouseButton::eMiddle:
+ buttonStatus = MK_MBUTTON;
+ break;
+ case MouseButton::eSecondary:
+ buttonStatus = MK_RBUTTON;
+ break;
+ }
+ }
+ if (aIMENotification.mMouseButtonEventData.mModifiers & MODIFIER_CONTROL) {
+ buttonStatus |= MK_CONTROL;
+ }
+ if (aIMENotification.mMouseButtonEventData.mModifiers & MODIFIER_SHIFT) {
+ buttonStatus |= MK_SHIFT;
+ }
+ for (size_t i = 0; i < mMouseTrackers.Length(); i++) {
+ MouseTracker& tracker = mMouseTrackers[i];
+ if (!tracker.IsUsing() || tracker.Range().isNothing() ||
+ !tracker.Range()->IsOffsetInRange(offset)) {
+ continue;
+ }
+ if (tracker.OnMouseButtonEvent(edge - tracker.Range()->StartOffset(),
+ quadrant, buttonStatus)) {
+ return NS_SUCCESS_EVENT_CONSUMED;
+ }
+ }
+ return NS_OK;
+}
+
+void TSFTextStore::CreateNativeCaret() {
+ MOZ_ASSERT(!IMEHandler::IsA11yHandlingNativeCaret());
+
+ IMEHandler::MaybeDestroyNativeCaret();
+
+ // Don't create native caret after destroyed.
+ if (mDestroyed) {
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::CreateNativeCaret(), mComposition=%s", this,
+ ToString(mComposition).c_str()));
+
+ Maybe<Selection>& selectionForTSF = SelectionForTSF();
+ if (MOZ_UNLIKELY(selectionForTSF.isNothing())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to "
+ "SelectionForTSF() failure",
+ this));
+ return;
+ }
+ if (!selectionForTSF->HasRange() && mComposition.isNothing()) {
+ // If there is no selection range nor composition, then, we don't have a
+ // good position to show windows of TIP...
+ // XXX It seems that storing last caret rect and using it in this case might
+ // be better?
+ MOZ_LOG(gIMELog, LogLevel::Warning,
+ ("0x%p TSFTextStore::CreateNativeCaret() couludn't create native "
+ "caret due to no selection range",
+ this));
+ return;
+ }
+
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, mWidget);
+ mWidget->InitEvent(queryCaretRectEvent);
+
+ WidgetQueryContentEvent::Options options;
+ // XXX If this is called without composition and the selection isn't
+ // collapsed, is it OK?
+ int64_t caretOffset = selectionForTSF->HasRange()
+ ? selectionForTSF->MaxOffset()
+ : mComposition->StartOffset();
+ if (mComposition.isSome()) {
+ // If there is a composition, use the relative query for deciding caret
+ // position because composition might be different place from that
+ // TSFTextStore assumes.
+ options.mRelativeToInsertionPoint = true;
+ caretOffset -= mComposition->StartOffset();
+ } else if (!CanAccessActualContentDirectly()) {
+ // If TSF/TIP cannot access actual content directly, there may be pending
+ // text and/or selection changes which have not been notified TSF yet.
+ // Therefore, we should use the relative query from start of selection where
+ // TSFTextStore assumes since TSF/TIP computes the offset from our cached
+ // selection.
+ options.mRelativeToInsertionPoint = true;
+ caretOffset -= selectionForTSF->StartOffset();
+ }
+ queryCaretRectEvent.InitForQueryCaretRect(caretOffset, options);
+
+ DispatchEvent(queryCaretRectEvent);
+ if (NS_WARN_IF(queryCaretRectEvent.Failed())) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to "
+ "eQueryCaretRect failure (offset=%lld)",
+ this, caretOffset));
+ return;
+ }
+
+ if (!IMEHandler::CreateNativeCaret(static_cast<nsWindow*>(mWidget.get()),
+ queryCaretRectEvent.mReply->mRect)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to "
+ "IMEHandler::CreateNativeCaret() failure",
+ this));
+ return;
+ }
+}
+
+void TSFTextStore::CommitCompositionInternal(bool aDiscard) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::CommitCompositionInternal(aDiscard=%s), "
+ "mSink=0x%p, mContext=0x%p, mComposition=%s",
+ this, GetBoolName(aDiscard), mSink.get(), mContext.get(),
+ ToString(mComposition).c_str()));
+
+ // If the document is locked, TSF will fail to commit composition since
+ // TSF needs another document lock. So, let's put off the request.
+ // Note that TextComposition will commit composition in the focused editor
+ // with the latest composition string for web apps and waits asynchronous
+ // committing messages. Therefore, we can and need to perform this
+ // asynchronously.
+ if (IsReadLocked()) {
+ if (mDeferCommittingComposition || mDeferCancellingComposition) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::CommitCompositionInternal(), "
+ "does nothing because already called and waiting unlock...",
+ this));
+ return;
+ }
+ if (aDiscard) {
+ mDeferCancellingComposition = true;
+ } else {
+ mDeferCommittingComposition = true;
+ }
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::CommitCompositionInternal(), "
+ "putting off to request to %s composition after unlocking the "
+ "document",
+ this, aDiscard ? "cancel" : "commit"));
+ return;
+ }
+
+ if (mComposition.isSome() && aDiscard) {
+ LONG endOffset = mComposition->EndOffset();
+ mComposition->SetData(EmptyString());
+ // Note that don't notify TSF of text change after this is destroyed.
+ if (mSink && !mDestroyed) {
+ TS_TEXTCHANGE textChange;
+ textChange.acpStart = mComposition->StartOffset();
+ textChange.acpOldEnd = endOffset;
+ textChange.acpNewEnd = mComposition->StartOffset();
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ ("0x%p TSFTextStore::CommitCompositionInternal(), calling"
+ "mSink->OnTextChange(0, { acpStart=%ld, acpOldEnd=%ld, "
+ "acpNewEnd=%ld })...",
+ this, textChange.acpStart, textChange.acpOldEnd,
+ textChange.acpNewEnd));
+ RefPtr<ITextStoreACPSink> sink = mSink;
+ sink->OnTextChange(0, &textChange);
+ }
+ }
+ // Terminate two contexts, the base context (mContext) and the top
+ // if the top context is not the same as the base context
+ RefPtr<ITfContext> context = mContext;
+ do {
+ if (context) {
+ RefPtr<ITfContextOwnerCompositionServices> services;
+ context->QueryInterface(IID_ITfContextOwnerCompositionServices,
+ getter_AddRefs(services));
+ if (services) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::CommitCompositionInternal(), "
+ "requesting TerminateComposition() for the context 0x%p...",
+ this, context.get()));
+ services->TerminateComposition(nullptr);
+ }
+ }
+ if (context != mContext) break;
+ if (mDocumentMgr) mDocumentMgr->GetTop(getter_AddRefs(context));
+ } while (context != mContext);
+}
+
+static bool GetCompartment(IUnknown* pUnk, const GUID& aID,
+ ITfCompartment** aCompartment) {
+ if (!pUnk) return false;
+
+ RefPtr<ITfCompartmentMgr> compMgr;
+ pUnk->QueryInterface(IID_ITfCompartmentMgr, getter_AddRefs(compMgr));
+ if (!compMgr) return false;
+
+ return SUCCEEDED(compMgr->GetCompartment(aID, aCompartment)) &&
+ (*aCompartment) != nullptr;
+}
+
+// static
+void TSFTextStore::SetIMEOpenState(bool aState) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("TSFTextStore::SetIMEOpenState(aState=%s)", GetBoolName(aState)));
+
+ if (!sThreadMgr) {
+ return;
+ }
+
+ RefPtr<ITfCompartment> comp = GetCompartmentForOpenClose();
+ if (NS_WARN_IF(!comp)) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ (" TSFTextStore::SetIMEOpenState() FAILED due to"
+ "no compartment available"));
+ return;
+ }
+
+ VARIANT variant;
+ variant.vt = VT_I4;
+ variant.lVal = aState;
+ HRESULT hr = comp->SetValue(sClientId, &variant);
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::SetIMEOpenState() FAILED due to "
+ "ITfCompartment::SetValue() failure, hr=0x%08lX",
+ hr));
+ return;
+ }
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ (" TSFTextStore::SetIMEOpenState(), setting "
+ "0x%04lX to GUID_COMPARTMENT_KEYBOARD_OPENCLOSE...",
+ variant.lVal));
+}
+
+// static
+bool TSFTextStore::GetIMEOpenState() {
+ if (!sThreadMgr) {
+ return false;
+ }
+
+ RefPtr<ITfCompartment> comp = GetCompartmentForOpenClose();
+ if (NS_WARN_IF(!comp)) {
+ return false;
+ }
+
+ VARIANT variant;
+ ::VariantInit(&variant);
+ HRESULT hr = comp->GetValue(&variant);
+ if (NS_WARN_IF(FAILED(hr))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetIMEOpenState() FAILED due to "
+ "ITfCompartment::GetValue() failure, hr=0x%08lX",
+ hr));
+ return false;
+ }
+ // Until IME is open in this process, the result may be empty.
+ if (variant.vt == VT_EMPTY) {
+ return false;
+ }
+ if (NS_WARN_IF(variant.vt != VT_I4)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetIMEOpenState() FAILED due to "
+ "invalid result of ITfCompartment::GetValue()"));
+ ::VariantClear(&variant);
+ return false;
+ }
+
+ return variant.lVal != 0;
+}
+
+// static
+void TSFTextStore::SetInputContext(nsWindow* aWidget,
+ const InputContext& aContext,
+ const InputContextAction& aAction) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("TSFTextStore::SetInputContext(aWidget=%p, "
+ "aContext=%s, aAction.mFocusChange=%s), "
+ "sEnabledTextStore(0x%p)={ mWidget=0x%p }, ThinksHavingFocus()=%s",
+ aWidget, mozilla::ToString(aContext).c_str(),
+ GetFocusChangeName(aAction.mFocusChange), sEnabledTextStore.get(),
+ sEnabledTextStore ? sEnabledTextStore->mWidget.get() : nullptr,
+ GetBoolName(ThinksHavingFocus())));
+
+ switch (aAction.mFocusChange) {
+ case InputContextAction::WIDGET_CREATED:
+ // If this is called when the widget is created, there is nothing to do.
+ return;
+ case InputContextAction::FOCUS_NOT_CHANGED:
+ case InputContextAction::MENU_LOST_PSEUDO_FOCUS:
+ if (NS_WARN_IF(!IsInTSFMode())) {
+ return;
+ }
+ // In these cases, `NOTIFY_IME_OF_FOCUS` won't be sent. Therefore,
+ // we need to reset text store for new state right now.
+ break;
+ default:
+ NS_WARNING_ASSERTION(IsInTSFMode(),
+ "Why is this called when TSF is disabled?");
+ if (sEnabledTextStore) {
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ textStore->mInPrivateBrowsing = aContext.mInPrivateBrowsing;
+ textStore->SetInputScope(aContext.mHTMLInputType,
+ aContext.mHTMLInputMode);
+ if (aContext.mURI) {
+ nsAutoCString spec;
+ if (NS_SUCCEEDED(aContext.mURI->GetSpec(spec))) {
+ CopyUTF8toUTF16(spec, textStore->mDocumentURL);
+ } else {
+ textStore->mDocumentURL.Truncate();
+ }
+ } else {
+ textStore->mDocumentURL.Truncate();
+ }
+ }
+ return;
+ }
+
+ // If focus isn't actually changed but the enabled state is changed,
+ // emulate the focus move.
+ if (!ThinksHavingFocus() && aContext.mIMEState.IsEditable()) {
+ if (!IMEHandler::GetFocusedWindow()) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::SetInputContent() gets called to enable IME, "
+ "but IMEHandler has not received focus notification"));
+ } else {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ (" TSFTextStore::SetInputContent() emulates focus for IME "
+ "state change"));
+ OnFocusChange(true, aWidget, aContext);
+ }
+ } else if (ThinksHavingFocus() && !aContext.mIMEState.IsEditable()) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ (" TSFTextStore::SetInputContent() emulates blur for IME "
+ "state change"));
+ OnFocusChange(false, aWidget, aContext);
+ }
+}
+
+// static
+void TSFTextStore::MarkContextAsKeyboardDisabled(ITfContext* aContext) {
+ VARIANT variant_int4_value1;
+ variant_int4_value1.vt = VT_I4;
+ variant_int4_value1.lVal = 1;
+
+ RefPtr<ITfCompartment> comp;
+ if (!GetCompartment(aContext, GUID_COMPARTMENT_KEYBOARD_DISABLED,
+ getter_AddRefs(comp))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::MarkContextAsKeyboardDisabled() failed"
+ "aContext=0x%p...",
+ aContext));
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("TSFTextStore::MarkContextAsKeyboardDisabled(), setting "
+ "to disable context 0x%p...",
+ aContext));
+ comp->SetValue(sClientId, &variant_int4_value1);
+}
+
+// static
+void TSFTextStore::MarkContextAsEmpty(ITfContext* aContext) {
+ VARIANT variant_int4_value1;
+ variant_int4_value1.vt = VT_I4;
+ variant_int4_value1.lVal = 1;
+
+ RefPtr<ITfCompartment> comp;
+ if (!GetCompartment(aContext, GUID_COMPARTMENT_EMPTYCONTEXT,
+ getter_AddRefs(comp))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::MarkContextAsEmpty() failed"
+ "aContext=0x%p...",
+ aContext));
+ return;
+ }
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("TSFTextStore::MarkContextAsEmpty(), setting "
+ "to mark empty context 0x%p...",
+ aContext));
+ comp->SetValue(sClientId, &variant_int4_value1);
+}
+
+// static
+void TSFTextStore::Initialize() {
+ MOZ_LOG(gIMELog, LogLevel::Info, ("TSFTextStore::Initialize() is called..."));
+
+ if (sThreadMgr) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::Initialize() FAILED due to already initialized"));
+ return;
+ }
+
+ const bool enableTsf = StaticPrefs::intl_tsf_enabled_AtStartup();
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" TSFTextStore::Initialize(), TSF is %s",
+ enableTsf ? "enabled" : "disabled"));
+ if (!enableTsf) {
+ return;
+ }
+
+ RefPtr<ITfThreadMgr> threadMgr;
+ HRESULT hr =
+ ::CoCreateInstance(CLSID_TF_ThreadMgr, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITfThreadMgr, getter_AddRefs(threadMgr));
+ if (FAILED(hr) || !threadMgr) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::Initialize() FAILED to "
+ "create the thread manager, hr=0x%08lX",
+ hr));
+ return;
+ }
+
+ hr = threadMgr->Activate(&sClientId);
+ if (FAILED(hr)) {
+ MOZ_LOG(
+ gIMELog, LogLevel::Error,
+ (" TSFTextStore::Initialize() FAILED to activate, hr=0x%08lX", hr));
+ return;
+ }
+
+ RefPtr<ITfDocumentMgr> disabledDocumentMgr;
+ hr = threadMgr->CreateDocumentMgr(getter_AddRefs(disabledDocumentMgr));
+ if (FAILED(hr) || !disabledDocumentMgr) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::Initialize() FAILED to create "
+ "a document manager for disabled mode, hr=0x%08lX",
+ hr));
+ return;
+ }
+
+ RefPtr<ITfContext> disabledContext;
+ DWORD editCookie = 0;
+ hr = disabledDocumentMgr->CreateContext(
+ sClientId, 0, nullptr, getter_AddRefs(disabledContext), &editCookie);
+ if (FAILED(hr) || !disabledContext) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::Initialize() FAILED to create "
+ "a context for disabled mode, hr=0x%08lX",
+ hr));
+ return;
+ }
+
+ MarkContextAsKeyboardDisabled(disabledContext);
+ MarkContextAsEmpty(disabledContext);
+
+ sThreadMgr = threadMgr;
+ sDisabledDocumentMgr = disabledDocumentMgr;
+ sDisabledContext = disabledContext;
+
+ MOZ_LOG(gIMELog, LogLevel::Info,
+ (" TSFTextStore::Initialize(), sThreadMgr=0x%p, "
+ "sClientId=0x%08lX, sDisabledDocumentMgr=0x%p, sDisabledContext=%p",
+ sThreadMgr.get(), sClientId, sDisabledDocumentMgr.get(),
+ sDisabledContext.get()));
+}
+
+// static
+already_AddRefed<ITfThreadMgr> TSFTextStore::GetThreadMgr() {
+ RefPtr<ITfThreadMgr> threadMgr = sThreadMgr;
+ return threadMgr.forget();
+}
+
+// static
+already_AddRefed<ITfMessagePump> TSFTextStore::GetMessagePump() {
+ static bool sInitialized = false;
+ if (!sThreadMgr) {
+ return nullptr;
+ }
+ if (sMessagePump) {
+ RefPtr<ITfMessagePump> messagePump = sMessagePump;
+ return messagePump.forget();
+ }
+ // If it tried to retrieve ITfMessagePump from sThreadMgr but it failed,
+ // we shouldn't retry it at every message due to performance reason.
+ // Although this shouldn't occur actually.
+ if (sInitialized) {
+ return nullptr;
+ }
+ sInitialized = true;
+
+ RefPtr<ITfMessagePump> messagePump;
+ HRESULT hr = sThreadMgr->QueryInterface(IID_ITfMessagePump,
+ getter_AddRefs(messagePump));
+ if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!messagePump)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetMessagePump() FAILED to "
+ "QI message pump from the thread manager, hr=0x%08lX",
+ hr));
+ return nullptr;
+ }
+ sMessagePump = messagePump;
+ return messagePump.forget();
+}
+
+// static
+already_AddRefed<ITfDisplayAttributeMgr>
+TSFTextStore::GetDisplayAttributeMgr() {
+ RefPtr<ITfDisplayAttributeMgr> displayAttributeMgr;
+ if (sDisplayAttrMgr) {
+ displayAttributeMgr = sDisplayAttrMgr;
+ return displayAttributeMgr.forget();
+ }
+
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_TF_DisplayAttributeMgr, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITfDisplayAttributeMgr, getter_AddRefs(displayAttributeMgr));
+ if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!displayAttributeMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetDisplayAttributeMgr() FAILED to create "
+ "a display attribute manager instance, hr=0x%08lX",
+ hr));
+ return nullptr;
+ }
+ sDisplayAttrMgr = displayAttributeMgr;
+ return displayAttributeMgr.forget();
+}
+
+// static
+already_AddRefed<ITfCategoryMgr> TSFTextStore::GetCategoryMgr() {
+ RefPtr<ITfCategoryMgr> categoryMgr;
+ if (sCategoryMgr) {
+ categoryMgr = sCategoryMgr;
+ return categoryMgr.forget();
+ }
+ HRESULT hr =
+ ::CoCreateInstance(CLSID_TF_CategoryMgr, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITfCategoryMgr, getter_AddRefs(categoryMgr));
+ if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!categoryMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetCategoryMgr() FAILED to create "
+ "a category manager instance, hr=0x%08lX",
+ hr));
+ return nullptr;
+ }
+ sCategoryMgr = categoryMgr;
+ return categoryMgr.forget();
+}
+
+// static
+already_AddRefed<ITfCompartment> TSFTextStore::GetCompartmentForOpenClose() {
+ if (sCompartmentForOpenClose) {
+ RefPtr<ITfCompartment> compartment = sCompartmentForOpenClose;
+ return compartment.forget();
+ }
+
+ if (!sThreadMgr) {
+ return nullptr;
+ }
+
+ RefPtr<ITfCompartmentMgr> compartmentMgr;
+ HRESULT hr = sThreadMgr->QueryInterface(IID_ITfCompartmentMgr,
+ getter_AddRefs(compartmentMgr));
+ if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!compartmentMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetCompartmentForOpenClose() FAILED due to"
+ "sThreadMgr not having ITfCompartmentMgr, hr=0x%08lX",
+ hr));
+ return nullptr;
+ }
+
+ RefPtr<ITfCompartment> compartment;
+ hr = compartmentMgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_OPENCLOSE,
+ getter_AddRefs(compartment));
+ if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!compartment)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetCompartmentForOpenClose() FAILED due to"
+ "ITfCompartmentMgr::GetCompartment() failuere, hr=0x%08lX",
+ hr));
+ return nullptr;
+ }
+
+ sCompartmentForOpenClose = compartment;
+ return compartment.forget();
+}
+
+// static
+already_AddRefed<ITfInputProcessorProfiles>
+TSFTextStore::GetInputProcessorProfiles() {
+ RefPtr<ITfInputProcessorProfiles> inputProcessorProfiles;
+ if (sInputProcessorProfiles) {
+ inputProcessorProfiles = sInputProcessorProfiles;
+ return inputProcessorProfiles.forget();
+ }
+ // XXX MSDN documents that ITfInputProcessorProfiles is available only on
+ // desktop apps. However, there is no known way to obtain
+ // ITfInputProcessorProfileMgr instance without ITfInputProcessorProfiles
+ // instance.
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_TF_InputProcessorProfiles, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITfInputProcessorProfiles, getter_AddRefs(inputProcessorProfiles));
+ if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!inputProcessorProfiles)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::GetInputProcessorProfiles() FAILED to create input "
+ "processor profiles, hr=0x%08lX",
+ hr));
+ return nullptr;
+ }
+ sInputProcessorProfiles = inputProcessorProfiles;
+ return inputProcessorProfiles.forget();
+}
+
+// static
+void TSFTextStore::Terminate() {
+ MOZ_LOG(gIMELog, LogLevel::Info, ("TSFTextStore::Terminate()"));
+
+ TSFStaticSink::Shutdown();
+
+ sDisplayAttrMgr = nullptr;
+ sCategoryMgr = nullptr;
+ sEnabledTextStore = nullptr;
+ sDisabledDocumentMgr = nullptr;
+ sDisabledContext = nullptr;
+ sCompartmentForOpenClose = nullptr;
+ sInputProcessorProfiles = nullptr;
+ sClientId = 0;
+ if (sThreadMgr) {
+ sThreadMgr->Deactivate();
+ sThreadMgr = nullptr;
+ sMessagePump = nullptr;
+ sKeystrokeMgr = nullptr;
+ }
+}
+
+// static
+bool TSFTextStore::ProcessRawKeyMessage(const MSG& aMsg) {
+ if (!sThreadMgr) {
+ return false; // not in TSF mode
+ }
+ static bool sInitialized = false;
+ if (!sKeystrokeMgr) {
+ // If it tried to retrieve ITfKeystrokeMgr from sThreadMgr but it failed,
+ // we shouldn't retry it at every keydown nor keyup due to performance
+ // reason. Although this shouldn't occur actually.
+ if (sInitialized) {
+ return false;
+ }
+ sInitialized = true;
+ RefPtr<ITfKeystrokeMgr> keystrokeMgr;
+ HRESULT hr = sThreadMgr->QueryInterface(IID_ITfKeystrokeMgr,
+ getter_AddRefs(keystrokeMgr));
+ if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!keystrokeMgr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::ProcessRawKeyMessage() FAILED to "
+ "QI keystroke manager from the thread manager, hr=0x%08lX",
+ hr));
+ return false;
+ }
+ sKeystrokeMgr = keystrokeMgr.forget();
+ }
+
+ if (aMsg.message == WM_KEYDOWN) {
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ if (textStore) {
+ textStore->OnStartToHandleKeyMessage();
+ if (NS_WARN_IF(textStore != sEnabledTextStore)) {
+ // Let's handle the key message with new focused TSFTextStore.
+ textStore = sEnabledTextStore;
+ }
+ }
+ AutoRestore<const MSG*> savePreviousKeyMsg(sHandlingKeyMsg);
+ AutoRestore<bool> saveKeyEventDispatched(sIsKeyboardEventDispatched);
+ sHandlingKeyMsg = &aMsg;
+ sIsKeyboardEventDispatched = false;
+ BOOL eaten;
+ RefPtr<ITfKeystrokeMgr> keystrokeMgr = sKeystrokeMgr;
+ HRESULT hr = keystrokeMgr->TestKeyDown(aMsg.wParam, aMsg.lParam, &eaten);
+ if (FAILED(hr) || !sKeystrokeMgr || !eaten) {
+ return false;
+ }
+ hr = keystrokeMgr->KeyDown(aMsg.wParam, aMsg.lParam, &eaten);
+ if (textStore) {
+ textStore->OnEndHandlingKeyMessage(!!eaten);
+ }
+ return SUCCEEDED(hr) &&
+ (eaten || !sKeystrokeMgr || sIsKeyboardEventDispatched);
+ }
+ if (aMsg.message == WM_KEYUP) {
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ if (textStore) {
+ textStore->OnStartToHandleKeyMessage();
+ if (NS_WARN_IF(textStore != sEnabledTextStore)) {
+ // Let's handle the key message with new focused TSFTextStore.
+ textStore = sEnabledTextStore;
+ }
+ }
+ AutoRestore<const MSG*> savePreviousKeyMsg(sHandlingKeyMsg);
+ AutoRestore<bool> saveKeyEventDispatched(sIsKeyboardEventDispatched);
+ sHandlingKeyMsg = &aMsg;
+ sIsKeyboardEventDispatched = false;
+ BOOL eaten;
+ RefPtr<ITfKeystrokeMgr> keystrokeMgr = sKeystrokeMgr;
+ HRESULT hr = keystrokeMgr->TestKeyUp(aMsg.wParam, aMsg.lParam, &eaten);
+ if (FAILED(hr) || !sKeystrokeMgr || !eaten) {
+ return false;
+ }
+ hr = keystrokeMgr->KeyUp(aMsg.wParam, aMsg.lParam, &eaten);
+ if (textStore) {
+ textStore->OnEndHandlingKeyMessage(!!eaten);
+ }
+ return SUCCEEDED(hr) &&
+ (eaten || !sKeystrokeMgr || sIsKeyboardEventDispatched);
+ }
+ return false;
+}
+
+// static
+void TSFTextStore::ProcessMessage(nsWindow* aWindow, UINT aMessage,
+ WPARAM& aWParam, LPARAM& aLParam,
+ MSGResult& aResult) {
+ switch (aMessage) {
+ case WM_IME_SETCONTEXT:
+ // If a windowless plugin had focus and IME was handled on it, composition
+ // window was set the position. After that, even in TSF mode, WinXP keeps
+ // to use composition window at the position if the active IME is not
+ // aware TSF. For avoiding this issue, we need to hide the composition
+ // window here.
+ if (aWParam) {
+ aLParam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
+ }
+ break;
+ case WM_ENTERIDLE:
+ // When an modal dialog such as a file picker is open, composition
+ // should be committed because IME might be used on it.
+ if (!IsComposingOn(aWindow)) {
+ break;
+ }
+ CommitComposition(false);
+ break;
+ case MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE: {
+ TSFTextStore* maybeTextStore = reinterpret_cast<TSFTextStore*>(aWParam);
+ if (maybeTextStore == sEnabledTextStore) {
+ RefPtr<TSFTextStore> textStore(maybeTextStore);
+ textStore->NotifyTSFOfLayoutChangeAgain();
+ }
+ break;
+ }
+ }
+}
+
+// static
+bool TSFTextStore::IsIMM_IMEActive() {
+ return TSFStaticSink::IsIMM_IMEActive();
+}
+
+// static
+bool TSFTextStore::IsMSJapaneseIMEActive() {
+ return TSFStaticSink::IsMSJapaneseIMEActive();
+}
+
+// static
+bool TSFTextStore::IsGoogleJapaneseInputActive() {
+ return TSFStaticSink::IsGoogleJapaneseInputActive();
+}
+
+// static
+bool TSFTextStore::IsATOKActive() { return TSFStaticSink::IsATOKActive(); }
+
+/******************************************************************************
+ * TSFTextStore::Content
+ *****************************************************************************/
+
+const nsDependentSubstring TSFTextStore::Content::GetSelectedText() const {
+ if (NS_WARN_IF(mSelection.isNothing())) {
+ return nsDependentSubstring();
+ }
+ return GetSubstring(static_cast<uint32_t>(mSelection->StartOffset()),
+ static_cast<uint32_t>(mSelection->Length()));
+}
+
+const nsDependentSubstring TSFTextStore::Content::GetSubstring(
+ uint32_t aStart, uint32_t aLength) const {
+ return nsDependentSubstring(mText, aStart, aLength);
+}
+
+void TSFTextStore::Content::ReplaceSelectedTextWith(const nsAString& aString) {
+ if (NS_WARN_IF(mSelection.isNothing())) {
+ return;
+ }
+ ReplaceTextWith(mSelection->StartOffset(), mSelection->Length(), aString);
+}
+
+inline uint32_t FirstDifferentCharOffset(const nsAString& aStr1,
+ const nsAString& aStr2) {
+ MOZ_ASSERT(aStr1 != aStr2);
+ uint32_t i = 0;
+ uint32_t minLength = std::min(aStr1.Length(), aStr2.Length());
+ for (; i < minLength && aStr1[i] == aStr2[i]; i++) {
+ /* nothing to do */
+ }
+ return i;
+}
+
+void TSFTextStore::Content::ReplaceTextWith(LONG aStart, LONG aLength,
+ const nsAString& aReplaceString) {
+ MOZ_ASSERT(aStart >= 0);
+ MOZ_ASSERT(aLength >= 0);
+ const nsDependentSubstring replacedString = GetSubstring(
+ static_cast<uint32_t>(aStart), static_cast<uint32_t>(aLength));
+ if (aReplaceString != replacedString) {
+ uint32_t firstDifferentOffset = mMinModifiedOffset.valueOr(UINT32_MAX);
+ if (mComposition.isSome()) {
+ // Emulate text insertion during compositions, because during a
+ // composition, editor expects the whole composition string to
+ // be sent in eCompositionChange, not just the inserted part.
+ // The actual eCompositionChange will be sent in SetSelection
+ // or OnUpdateComposition.
+ MOZ_ASSERT(aStart >= mComposition->StartOffset());
+ MOZ_ASSERT(aStart + aLength <= mComposition->EndOffset());
+ mComposition->ReplaceData(
+ static_cast<uint32_t>(aStart - mComposition->StartOffset()),
+ static_cast<uint32_t>(aLength), aReplaceString);
+ // TIP may set composition string twice or more times during a document
+ // lock. Therefore, we should compute the first difference offset with
+ // mLastComposition.
+ if (mLastComposition.isNothing()) {
+ firstDifferentOffset = mComposition->StartOffset();
+ } else if (mComposition->DataRef() != mLastComposition->DataRef()) {
+ firstDifferentOffset =
+ mComposition->StartOffset() +
+ FirstDifferentCharOffset(mComposition->DataRef(),
+ mLastComposition->DataRef());
+ // The previous change to the composition string is canceled.
+ if (mMinModifiedOffset.isSome() &&
+ mMinModifiedOffset.value() >=
+ static_cast<uint32_t>(mComposition->StartOffset()) &&
+ mMinModifiedOffset.value() < firstDifferentOffset) {
+ mMinModifiedOffset = Some(firstDifferentOffset);
+ }
+ } else if (mMinModifiedOffset.isSome() &&
+ mMinModifiedOffset.value() < static_cast<uint32_t>(LONG_MAX) &&
+ mComposition->IsOffsetInRange(
+ static_cast<long>(mMinModifiedOffset.value()))) {
+ // The previous change to the composition string is canceled.
+ firstDifferentOffset = mComposition->EndOffset();
+ mMinModifiedOffset = Some(firstDifferentOffset);
+ }
+ mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets());
+ MOZ_LOG(
+ gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::Content::ReplaceTextWith(aStart=%ld, "
+ "aLength=%ld, aReplaceString=\"%s\"), mComposition=%s, "
+ "mLastComposition=%s, mMinModifiedOffset=%s, "
+ "firstDifferentOffset=%u",
+ this, aStart, aLength, GetEscapedUTF8String(aReplaceString).get(),
+ ToString(mComposition).c_str(), ToString(mLastComposition).c_str(),
+ ToString(mMinModifiedOffset).c_str(), firstDifferentOffset));
+ } else {
+ firstDifferentOffset =
+ static_cast<uint32_t>(aStart) +
+ FirstDifferentCharOffset(aReplaceString, replacedString);
+ }
+ mMinModifiedOffset =
+ mMinModifiedOffset.isNothing()
+ ? Some(firstDifferentOffset)
+ : Some(std::min(mMinModifiedOffset.value(), firstDifferentOffset));
+ mText.Replace(static_cast<uint32_t>(aStart), static_cast<uint32_t>(aLength),
+ aReplaceString);
+ }
+ // Selection should be collapsed at the end of the inserted string.
+ mSelection = Some(TSFTextStore::Selection(static_cast<uint32_t>(aStart) +
+ aReplaceString.Length()));
+}
+
+void TSFTextStore::Content::StartComposition(
+ ITfCompositionView* aCompositionView, const PendingAction& aCompStart,
+ bool aPreserveSelection) {
+ MOZ_ASSERT(aCompositionView);
+ MOZ_ASSERT(mComposition.isNothing());
+ MOZ_ASSERT(aCompStart.mType == PendingAction::Type::eCompositionStart);
+
+ mComposition.reset(); // Avoid new crash in the beta and nightly channels.
+ mComposition.emplace(
+ aCompositionView, aCompStart.mSelectionStart,
+ GetSubstring(static_cast<uint32_t>(aCompStart.mSelectionStart),
+ static_cast<uint32_t>(aCompStart.mSelectionLength)));
+ mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets());
+ if (!aPreserveSelection) {
+ // XXX Do we need to set a new writing-mode here when setting a new
+ // selection? Currently, we just preserve the existing value.
+ WritingMode writingMode =
+ mSelection.isNothing() ? WritingMode() : mSelection->WritingModeRef();
+ mSelection = Some(TSFTextStore::Selection(mComposition->StartOffset(),
+ mComposition->Length(), false,
+ writingMode));
+ }
+}
+
+void TSFTextStore::Content::RestoreCommittedComposition(
+ ITfCompositionView* aCompositionView,
+ const PendingAction& aCanceledCompositionEnd) {
+ MOZ_ASSERT(aCompositionView);
+ MOZ_ASSERT(mComposition.isNothing());
+ MOZ_ASSERT(aCanceledCompositionEnd.mType ==
+ PendingAction::Type::eCompositionEnd);
+ MOZ_ASSERT(
+ GetSubstring(
+ static_cast<uint32_t>(aCanceledCompositionEnd.mSelectionStart),
+ static_cast<uint32_t>(aCanceledCompositionEnd.mData.Length())) ==
+ aCanceledCompositionEnd.mData);
+
+ // Restore the committed string as composing string.
+ mComposition.reset(); // Avoid new crash in the beta and nightly channels.
+ mComposition.emplace(aCompositionView,
+ aCanceledCompositionEnd.mSelectionStart,
+ aCanceledCompositionEnd.mData);
+ mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets());
+}
+
+void TSFTextStore::Content::EndComposition(const PendingAction& aCompEnd) {
+ MOZ_ASSERT(mComposition.isSome());
+ MOZ_ASSERT(aCompEnd.mType == PendingAction::Type::eCompositionEnd);
+
+ if (mComposition.isNothing()) {
+ return; // Avoid new crash in the beta and nightly channels.
+ }
+
+ mSelection = Some(TSFTextStore::Selection(mComposition->StartOffset() +
+ aCompEnd.mData.Length()));
+ mComposition.reset();
+}
+
+/******************************************************************************
+ * TSFTextStore::MouseTracker
+ *****************************************************************************/
+
+TSFTextStore::MouseTracker::MouseTracker() : mCookie(kInvalidCookie) {}
+
+HRESULT
+TSFTextStore::MouseTracker::Init(TSFTextStore* aTextStore) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MouseTracker::Init(aTextStore=0x%p), "
+ "aTextStore->mMouseTrackers.Length()=%zu",
+ this, aTextStore, aTextStore->mMouseTrackers.Length()));
+
+ if (&aTextStore->mMouseTrackers.LastElement() != this) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::MouseTracker::Init() FAILED due to "
+ "this is not the last element of mMouseTrackers",
+ this));
+ return E_FAIL;
+ }
+ if (aTextStore->mMouseTrackers.Length() > kInvalidCookie) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::MouseTracker::Init() FAILED due to "
+ "no new cookie available",
+ this));
+ return E_FAIL;
+ }
+ MOZ_ASSERT(!aTextStore->mMouseTrackers.IsEmpty(),
+ "This instance must be in TSFTextStore::mMouseTrackers");
+ mCookie = static_cast<DWORD>(aTextStore->mMouseTrackers.Length() - 1);
+ return S_OK;
+}
+
+HRESULT
+TSFTextStore::MouseTracker::AdviseSink(TSFTextStore* aTextStore,
+ ITfRangeACP* aTextRange,
+ ITfMouseSink* aMouseSink) {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MouseTracker::AdviseSink(aTextStore=0x%p, "
+ "aTextRange=0x%p, aMouseSink=0x%p), mCookie=%ld, mSink=0x%p",
+ this, aTextStore, aTextRange, aMouseSink, mCookie, mSink.get()));
+ MOZ_ASSERT(mCookie != kInvalidCookie, "This hasn't been initalized?");
+
+ if (mSink) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED "
+ "due to already being used",
+ this));
+ return E_FAIL;
+ }
+
+ MOZ_ASSERT(mRange.isNothing());
+
+ LONG start = 0, length = 0;
+ HRESULT hr = aTextRange->GetExtent(&start, &length);
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED "
+ "due to failure of ITfRangeACP::GetExtent()",
+ this));
+ return hr;
+ }
+
+ if (start < 0 || length <= 0 || start + length > LONG_MAX) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED "
+ "due to odd result of ITfRangeACP::GetExtent(), "
+ "start=%ld, length=%ld",
+ this, start, length));
+ return E_INVALIDARG;
+ }
+
+ nsAutoString textContent;
+ if (NS_WARN_IF(!aTextStore->GetCurrentText(textContent))) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED "
+ "due to failure of TSFTextStore::GetCurrentText()",
+ this));
+ return E_FAIL;
+ }
+
+ if (textContent.Length() <= static_cast<uint32_t>(start) ||
+ textContent.Length() < static_cast<uint32_t>(start + length)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED "
+ "due to out of range, start=%ld, length=%ld, "
+ "textContent.Length()=%zu",
+ this, start, length, textContent.Length()));
+ return E_INVALIDARG;
+ }
+
+ mRange.emplace(start, start + length);
+
+ mSink = aMouseSink;
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink(), "
+ "succeeded, mRange=%s, textContent.Length()=%zu",
+ this, ToString(mRange).c_str(), textContent.Length()));
+ return S_OK;
+}
+
+void TSFTextStore::MouseTracker::UnadviseSink() {
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MouseTracker::UnadviseSink(), "
+ "mCookie=%ld, mSink=0x%p, mRange=%s",
+ this, mCookie, mSink.get(), ToString(mRange).c_str()));
+ mSink = nullptr;
+ mRange.reset();
+}
+
+bool TSFTextStore::MouseTracker::OnMouseButtonEvent(ULONG aEdge,
+ ULONG aQuadrant,
+ DWORD aButtonStatus) {
+ MOZ_ASSERT(IsUsing(), "The caller must check before calling OnMouseEvent()");
+
+ BOOL eaten = FALSE;
+ RefPtr<ITfMouseSink> sink = mSink;
+ HRESULT hr = sink->OnMouseEvent(aEdge, aQuadrant, aButtonStatus, &eaten);
+
+ MOZ_LOG(gIMELog, LogLevel::Debug,
+ ("0x%p TSFTextStore::MouseTracker::OnMouseEvent(aEdge=%ld, "
+ "aQuadrant=%ld, aButtonStatus=0x%08lX), hr=0x%08lX, eaten=%s",
+ this, aEdge, aQuadrant, aButtonStatus, hr, GetBoolName(!!eaten)));
+
+ return SUCCEEDED(hr) && eaten;
+}
+
+#ifdef DEBUG
+// static
+bool TSFTextStore::CurrentKeyboardLayoutHasIME() {
+ RefPtr<ITfInputProcessorProfiles> inputProcessorProfiles =
+ TSFTextStore::GetInputProcessorProfiles();
+ if (!inputProcessorProfiles) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ ("TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED due to "
+ "there is no input processor profiles instance"));
+ return false;
+ }
+ RefPtr<ITfInputProcessorProfileMgr> profileMgr;
+ HRESULT hr = inputProcessorProfiles->QueryInterface(
+ IID_ITfInputProcessorProfileMgr, getter_AddRefs(profileMgr));
+ if (FAILED(hr) || !profileMgr) {
+ // On Windows Vista or later, ImmIsIME() API always returns true.
+ // If we failed to obtain the profile manager, we cannot know if current
+ // keyboard layout has IME.
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED to query "
+ "ITfInputProcessorProfileMgr"));
+ return false;
+ }
+
+ TF_INPUTPROCESSORPROFILE profile;
+ hr = profileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile);
+ if (hr == S_FALSE) {
+ return false; // not found or not active
+ }
+ if (FAILED(hr)) {
+ MOZ_LOG(gIMELog, LogLevel::Error,
+ (" TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED to retreive "
+ "active profile"));
+ return false;
+ }
+ return (profile.dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR);
+}
+#endif // #ifdef DEBUG
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/TSFTextStore.h b/widget/windows/TSFTextStore.h
new file mode 100644
index 0000000000..17358a488d
--- /dev/null
+++ b/widget/windows/TSFTextStore.h
@@ -0,0 +1,1161 @@
+/* -*- 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/. */
+
+#ifndef TSFTextStore_h_
+#define TSFTextStore_h_
+
+#include "nsCOMPtr.h"
+#include "nsIWidget.h"
+#include "nsString.h"
+#include "nsWindow.h"
+
+#include "WinUtils.h"
+#include "WritingModes.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TextRange.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/widget/IMEData.h"
+
+#include <msctf.h>
+#include <textstor.h>
+
+// GUID_PROP_INPUTSCOPE is declared in inputscope.h using INIT_GUID.
+// With initguid.h, we get its instance instead of extern declaration.
+#ifdef INPUTSCOPE_INIT_GUID
+# include <initguid.h>
+#endif
+#ifdef TEXTATTRS_INIT_GUID
+# include <tsattrs.h>
+#endif
+#include <inputscope.h>
+
+// TSF InputScope, for earlier SDK 8
+#define IS_SEARCH static_cast<InputScope>(50)
+
+struct ITfThreadMgr;
+struct ITfDocumentMgr;
+struct ITfDisplayAttributeMgr;
+struct ITfCategoryMgr;
+class nsWindow;
+
+inline std::ostream& operator<<(std::ostream& aStream,
+ const TS_SELECTIONSTYLE& aSelectionStyle) {
+ const char* ase = "Unknown";
+ switch (aSelectionStyle.ase) {
+ case TS_AE_START:
+ ase = "TS_AE_START";
+ break;
+ case TS_AE_END:
+ ase = "TS_AE_END";
+ break;
+ case TS_AE_NONE:
+ ase = "TS_AE_NONE";
+ break;
+ }
+ aStream << "{ ase=" << ase << ", fInterimChar="
+ << (aSelectionStyle.fInterimChar ? "TRUE" : "FALSE") << " }";
+ return aStream;
+}
+
+inline std::ostream& operator<<(std::ostream& aStream,
+ const TS_SELECTION_ACP& aACP) {
+ aStream << "{ acpStart=" << aACP.acpStart << ", acpEnd=" << aACP.acpEnd
+ << ", style=" << mozilla::ToString(aACP.style).c_str() << " }";
+ return aStream;
+}
+
+namespace mozilla {
+namespace widget {
+
+class TSFStaticSink;
+struct MSGResult;
+
+/*
+ * Text Services Framework text store
+ */
+
+class TSFTextStore final : public ITextStoreACP,
+ public ITfContextOwnerCompositionSink,
+ public ITfMouseTrackerACP {
+ friend class TSFStaticSink;
+
+ private:
+ typedef IMENotification::SelectionChangeDataBase SelectionChangeDataBase;
+ typedef IMENotification::SelectionChangeData SelectionChangeData;
+ typedef IMENotification::TextChangeDataBase TextChangeDataBase;
+ typedef IMENotification::TextChangeData TextChangeData;
+
+ public: /*IUnknown*/
+ STDMETHODIMP QueryInterface(REFIID, void**);
+
+ NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(TSFTextStore)
+
+ public: /*ITextStoreACP*/
+ STDMETHODIMP AdviseSink(REFIID, IUnknown*, DWORD);
+ STDMETHODIMP UnadviseSink(IUnknown*);
+ STDMETHODIMP RequestLock(DWORD, HRESULT*);
+ STDMETHODIMP GetStatus(TS_STATUS*);
+ STDMETHODIMP QueryInsert(LONG, LONG, ULONG, LONG*, LONG*);
+ STDMETHODIMP GetSelection(ULONG, ULONG, TS_SELECTION_ACP*, ULONG*);
+ STDMETHODIMP SetSelection(ULONG, const TS_SELECTION_ACP*);
+ STDMETHODIMP GetText(LONG, LONG, WCHAR*, ULONG, ULONG*, TS_RUNINFO*, ULONG,
+ ULONG*, LONG*);
+ STDMETHODIMP SetText(DWORD, LONG, LONG, const WCHAR*, ULONG, TS_TEXTCHANGE*);
+ STDMETHODIMP GetFormattedText(LONG, LONG, IDataObject**);
+ STDMETHODIMP GetEmbedded(LONG, REFGUID, REFIID, IUnknown**);
+ STDMETHODIMP QueryInsertEmbedded(const GUID*, const FORMATETC*, BOOL*);
+ STDMETHODIMP InsertEmbedded(DWORD, LONG, LONG, IDataObject*, TS_TEXTCHANGE*);
+ STDMETHODIMP RequestSupportedAttrs(DWORD, ULONG, const TS_ATTRID*);
+ STDMETHODIMP RequestAttrsAtPosition(LONG, ULONG, const TS_ATTRID*, DWORD);
+ STDMETHODIMP RequestAttrsTransitioningAtPosition(LONG, ULONG,
+ const TS_ATTRID*, DWORD);
+ STDMETHODIMP FindNextAttrTransition(LONG, LONG, ULONG, const TS_ATTRID*,
+ DWORD, LONG*, BOOL*, LONG*);
+ STDMETHODIMP RetrieveRequestedAttrs(ULONG, TS_ATTRVAL*, ULONG*);
+ STDMETHODIMP GetEndACP(LONG*);
+ STDMETHODIMP GetActiveView(TsViewCookie*);
+ STDMETHODIMP GetACPFromPoint(TsViewCookie, const POINT*, DWORD, LONG*);
+ STDMETHODIMP GetTextExt(TsViewCookie, LONG, LONG, RECT*, BOOL*);
+ STDMETHODIMP GetScreenExt(TsViewCookie, RECT*);
+ STDMETHODIMP GetWnd(TsViewCookie, HWND*);
+ STDMETHODIMP InsertTextAtSelection(DWORD, const WCHAR*, ULONG, LONG*, LONG*,
+ TS_TEXTCHANGE*);
+ STDMETHODIMP InsertEmbeddedAtSelection(DWORD, IDataObject*, LONG*, LONG*,
+ TS_TEXTCHANGE*);
+
+ public: /*ITfContextOwnerCompositionSink*/
+ STDMETHODIMP OnStartComposition(ITfCompositionView*, BOOL*);
+ STDMETHODIMP OnUpdateComposition(ITfCompositionView*, ITfRange*);
+ STDMETHODIMP OnEndComposition(ITfCompositionView*);
+
+ public: /*ITfMouseTrackerACP*/
+ STDMETHODIMP AdviseMouseSink(ITfRangeACP*, ITfMouseSink*, DWORD*);
+ STDMETHODIMP UnadviseMouseSink(DWORD);
+
+ public:
+ static void Initialize(void);
+ static void Terminate(void);
+
+ static bool ProcessRawKeyMessage(const MSG& aMsg);
+ static void ProcessMessage(nsWindow* aWindow, UINT aMessage, WPARAM& aWParam,
+ LPARAM& aLParam, MSGResult& aResult);
+
+ static void SetIMEOpenState(bool);
+ static bool GetIMEOpenState(void);
+
+ static void CommitComposition(bool aDiscard) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ textStore->CommitCompositionInternal(aDiscard);
+ }
+
+ static void SetInputContext(nsWindow* aWidget, const InputContext& aContext,
+ const InputContextAction& aAction);
+
+ static nsresult OnFocusChange(bool aGotFocus, nsWindow* aFocusedWidget,
+ const InputContext& aContext);
+ static nsresult OnTextChange(const IMENotification& aIMENotification) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnTextChangeInternal(aIMENotification);
+ }
+
+ static nsresult OnSelectionChange(const IMENotification& aIMENotification) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnSelectionChangeInternal(aIMENotification);
+ }
+
+ static nsresult OnLayoutChange() {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnLayoutChangeInternal();
+ }
+
+ static nsresult OnUpdateComposition() {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnUpdateCompositionInternal();
+ }
+
+ static nsresult OnMouseButtonEvent(const IMENotification& aIMENotification) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnMouseButtonEventInternal(aIMENotification);
+ }
+
+ static IMENotificationRequests GetIMENotificationRequests();
+
+ // Returns the address of the pointer so that the TSF automatic test can
+ // replace the system object with a custom implementation for testing.
+ // XXX TSF doesn't work now. Should we remove it?
+ static void* GetNativeData(uint32_t aDataType) {
+ switch (aDataType) {
+ case NS_NATIVE_TSF_THREAD_MGR:
+ Initialize(); // Apply any previous changes
+ return static_cast<void*>(&sThreadMgr);
+ case NS_NATIVE_TSF_CATEGORY_MGR:
+ return static_cast<void*>(&sCategoryMgr);
+ case NS_NATIVE_TSF_DISPLAY_ATTR_MGR:
+ return static_cast<void*>(&sDisplayAttrMgr);
+ default:
+ return nullptr;
+ }
+ }
+
+ static void* GetThreadManager() { return static_cast<void*>(sThreadMgr); }
+
+ static bool ThinksHavingFocus() {
+ return (sEnabledTextStore && sEnabledTextStore->mContext);
+ }
+
+ static bool IsInTSFMode() { return sThreadMgr != nullptr; }
+
+ static bool IsComposing() {
+ return (sEnabledTextStore && sEnabledTextStore->mComposition.isSome());
+ }
+
+ static bool IsComposingOn(nsWindow* aWidget) {
+ return (IsComposing() && sEnabledTextStore->mWidget == aWidget);
+ }
+
+ static nsWindow* GetEnabledWindowBase() {
+ return sEnabledTextStore ? sEnabledTextStore->mWidget.get() : nullptr;
+ }
+
+ /**
+ * Returns true if active keyboard layout is a legacy IMM-IME.
+ */
+ static bool IsIMM_IMEActive();
+
+ /**
+ * Returns true if active TIP is MS-IME for Japanese.
+ */
+ static bool IsMSJapaneseIMEActive();
+
+ /**
+ * Returns true if active TIP is Google Japanese Input.
+ * Note that if Google Japanese Input is installed as an IMM-IME,
+ * this return false even if Google Japanese Input is active.
+ * So, you may need to check IMMHandler::IsGoogleJapaneseInputActive() too.
+ */
+ static bool IsGoogleJapaneseInputActive();
+
+ /**
+ * Returns true if active TIP is ATOK.
+ */
+ static bool IsATOKActive();
+
+ /**
+ * Returns true if active TIP or IME is a black listed one and we should
+ * set input scope of URL bar to IS_DEFAULT rather than IS_URL.
+ */
+ static bool ShouldSetInputScopeOfURLBarToDefault();
+
+ /**
+ * Returns true if TSF may crash if GetSelection() returns E_FAIL.
+ */
+ static bool DoNotReturnErrorFromGetSelection();
+
+#ifdef DEBUG
+ // Returns true when keyboard layout has IME (TIP).
+ static bool CurrentKeyboardLayoutHasIME();
+#endif // #ifdef DEBUG
+
+ protected:
+ TSFTextStore();
+ ~TSFTextStore();
+
+ static bool CreateAndSetFocus(nsWindow* aFocusedWidget,
+ const InputContext& aContext);
+ static void EnsureToDestroyAndReleaseEnabledTextStoreIf(
+ RefPtr<TSFTextStore>& aTextStore);
+ static void MarkContextAsKeyboardDisabled(ITfContext* aContext);
+ static void MarkContextAsEmpty(ITfContext* aContext);
+
+ bool Init(nsWindow* aWidget, const InputContext& aContext);
+ void Destroy();
+ void ReleaseTSFObjects();
+
+ bool IsReadLock(DWORD aLock) const {
+ return (TS_LF_READ == (aLock & TS_LF_READ));
+ }
+ bool IsReadWriteLock(DWORD aLock) const {
+ return (TS_LF_READWRITE == (aLock & TS_LF_READWRITE));
+ }
+ bool IsReadLocked() const { return IsReadLock(mLock); }
+ bool IsReadWriteLocked() const { return IsReadWriteLock(mLock); }
+
+ // This is called immediately after a call of OnLockGranted() of mSink.
+ // Note that mLock isn't cleared yet when this is called.
+ void DidLockGranted();
+
+ bool GetScreenExtInternal(RECT& aScreenExt);
+ // If aDispatchCompositionChangeEvent is true, this method will dispatch
+ // compositionchange event if this is called during IME composing.
+ // aDispatchCompositionChangeEvent should be true only when this is called
+ // from SetSelection. Because otherwise, the compositionchange event should
+ // not be sent from here.
+ HRESULT SetSelectionInternal(const TS_SELECTION_ACP*,
+ bool aDispatchCompositionChangeEvent = false);
+ bool InsertTextAtSelectionInternal(const nsAString& aInsertStr,
+ TS_TEXTCHANGE* aTextChange);
+ void CommitCompositionInternal(bool);
+ HRESULT GetDisplayAttribute(ITfProperty* aProperty, ITfRange* aRange,
+ TF_DISPLAYATTRIBUTE* aResult);
+ HRESULT RestartCompositionIfNecessary(ITfRange* pRangeNew = nullptr);
+ class Composition;
+ HRESULT RestartComposition(Composition& aCurrentComposition,
+ ITfCompositionView* aCompositionView,
+ ITfRange* aNewRange);
+
+ // Following methods record composing action(s) to mPendingActions.
+ // They will be flushed FlushPendingActions().
+ HRESULT RecordCompositionStartAction(ITfCompositionView* aCompositionView,
+ ITfRange* aRange,
+ bool aPreserveSelection);
+ HRESULT RecordCompositionStartAction(ITfCompositionView* aCompositionView,
+ LONG aStart, LONG aLength,
+ bool aPreserveSelection);
+ HRESULT RecordCompositionUpdateAction();
+ HRESULT RecordCompositionEndAction();
+
+ // DispatchEvent() dispatches the event and if it may not be handled
+ // synchronously, this makes the instance not notify TSF of pending
+ // notifications until next notification from content.
+ void DispatchEvent(WidgetGUIEvent& aEvent);
+ void OnLayoutInformationAvaliable();
+
+ // FlushPendingActions() performs pending actions recorded in mPendingActions
+ // and clear it.
+ void FlushPendingActions();
+ // MaybeFlushPendingNotifications() performs pending notifications to TSF.
+ void MaybeFlushPendingNotifications();
+
+ nsresult OnTextChangeInternal(const IMENotification& aIMENotification);
+ nsresult OnSelectionChangeInternal(const IMENotification& aIMENotification);
+ nsresult OnMouseButtonEventInternal(const IMENotification& aIMENotification);
+ nsresult OnLayoutChangeInternal();
+ nsresult OnUpdateCompositionInternal();
+
+ // mPendingSelectionChangeData stores selection change data until notifying
+ // TSF of selection change. If two or more selection changes occur, this
+ // stores the latest selection change data because only it is necessary.
+ Maybe<SelectionChangeData> mPendingSelectionChangeData;
+
+ // mPendingTextChangeData stores one or more text change data until notifying
+ // TSF of text change. If two or more text changes occur, this merges
+ // every text change data.
+ TextChangeData mPendingTextChangeData;
+
+ void NotifyTSFOfTextChange();
+ void NotifyTSFOfSelectionChange();
+ bool NotifyTSFOfLayoutChange();
+ void NotifyTSFOfLayoutChangeAgain();
+
+ HRESULT HandleRequestAttrs(DWORD aFlags, ULONG aFilterCount,
+ const TS_ATTRID* aFilterAttrs);
+ void SetInputScope(const nsString& aHTMLInputType,
+ const nsString& aHTMLInputMode);
+
+ // Creates native caret over our caret. This method only works on desktop
+ // application. Otherwise, this does nothing.
+ void CreateNativeCaret();
+ // Destroys native caret if there is.
+ void MaybeDestroyNativeCaret();
+
+ /**
+ * MaybeHackNoErrorLayoutBugs() is a helper method of GetTextExt(). In
+ * strictly speaking, TSF is aware of asynchronous layout computation like us.
+ * However, Windows 10 version 1803 and older (including Windows 8.1 and
+ * older) Windows has a bug which is that the caller of GetTextExt() of TSF
+ * does not return TS_E_NOLAYOUT to TIP as is. Additionally, even after
+ * fixing this bug, some TIPs are not work well when we return TS_E_NOLAYOUT.
+ * For avoiding this issue, this method checks current Windows version and
+ * active TIP, and if in case we cannot return TS_E_NOLAYOUT, this modifies
+ * aACPStart and aACPEnd to making sure that they are in range of unmodified
+ * characters.
+ *
+ * @param aACPStart Initial value should be acpStart of GetTextExt().
+ * If this method returns true, this may be modified
+ * to be in range of unmodified characters.
+ * @param aACPEnd Initial value should be acpEnd of GetTextExt().
+ * If this method returns true, this may be modified
+ * to be in range of unmodified characters.
+ * And also this may become same as aACPStart.
+ * @return true if the caller shouldn't return TS_E_NOLAYOUT.
+ * In this case, this method modifies aACPStart and/or
+ * aASCPEnd to compute rectangle of unmodified characters.
+ * false if the caller can return TS_E_NOLAYOUT or
+ * we cannot have proper unmodified characters.
+ */
+ bool MaybeHackNoErrorLayoutBugs(LONG& aACPStart, LONG& aACPEnd);
+
+ // Holds the pointer to our current win32 widget
+ RefPtr<nsWindow> mWidget;
+ // mDispatcher is a helper class to dispatch composition events.
+ RefPtr<TextEventDispatcher> mDispatcher;
+ // Document manager for the currently focused editor
+ RefPtr<ITfDocumentMgr> mDocumentMgr;
+ // Edit cookie associated with the current editing context
+ DWORD mEditCookie;
+ // Editing context at the bottom of mDocumentMgr's context stack
+ RefPtr<ITfContext> mContext;
+ // Currently installed notification sink
+ RefPtr<ITextStoreACPSink> mSink;
+ // TS_AS_* mask of what events to notify
+ DWORD mSinkMask;
+ // 0 if not locked, otherwise TS_LF_* indicating the current lock
+ DWORD mLock;
+ // 0 if no lock is queued, otherwise TS_LF_* indicating the queue lock
+ DWORD mLockQueued;
+
+ uint32_t mHandlingKeyMessage;
+ void OnStartToHandleKeyMessage() {
+ // If we're starting to handle another key message during handling a
+ // key message, let's assume that the handling key message is handled by
+ // TIP and it sends another key message for hacking something.
+ // Let's try to dispatch a keyboard event now.
+ // FYI: All callers of this method grab this instance with local variable.
+ // So, even after calling MaybeDispatchKeyboardEventAsProcessedByIME(),
+ // we're safe to access any members.
+ if (!mDestroyed && sHandlingKeyMsg && !sIsKeyboardEventDispatched) {
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ }
+ ++mHandlingKeyMessage;
+ }
+ void OnEndHandlingKeyMessage(bool aIsProcessedByTSF) {
+ // If sHandlingKeyMsg has been handled by TSF or TIP and we're still
+ // alive, but we haven't dispatch keyboard event for it, let's fire it now.
+ // FYI: All callers of this method grab this instance with local variable.
+ // So, even after calling MaybeDispatchKeyboardEventAsProcessedByIME(),
+ // we're safe to access any members.
+ if (!mDestroyed && sHandlingKeyMsg && aIsProcessedByTSF &&
+ !sIsKeyboardEventDispatched) {
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ }
+ MOZ_ASSERT(mHandlingKeyMessage);
+ if (--mHandlingKeyMessage) {
+ return;
+ }
+ // If TSFTextStore instance is destroyed during handling key message(s),
+ // release all TSF objects when all nested key messages have been handled.
+ if (mDestroyed) {
+ ReleaseTSFObjects();
+ }
+ }
+
+ /**
+ * MaybeDispatchKeyboardEventAsProcessedByIME() tries to dispatch eKeyDown
+ * event or eKeyUp event for sHandlingKeyMsg and marking the dispatching
+ * event as "processed by IME". Note that if the document is locked, this
+ * just adds a pending action into the queue and sets
+ * sIsKeyboardEventDispatched to true.
+ */
+ void MaybeDispatchKeyboardEventAsProcessedByIME();
+
+ /**
+ * DispatchKeyboardEventAsProcessedByIME() dispatches an eKeyDown or
+ * eKeyUp event with NativeKey class and aMsg.
+ */
+ void DispatchKeyboardEventAsProcessedByIME(const MSG& aMsg);
+
+ // Composition class stores a copy of the active composition string. Only
+ // the data is updated during an InsertTextAtSelection call if we have a
+ // composition. The data acts as a buffer until OnUpdateComposition is
+ // called and the data is flushed to editor through eCompositionChange.
+ // This allows all changes to be updated in batches to avoid inconsistencies
+ // and artifacts.
+ class Composition final : public OffsetAndData<LONG> {
+ public:
+ explicit Composition(ITfCompositionView* aCompositionView,
+ LONG aCompositionStartOffset,
+ const nsAString& aCompositionString)
+ : OffsetAndData<LONG>(aCompositionStartOffset, aCompositionString),
+ mView(aCompositionView) {}
+
+ ITfCompositionView* GetView() const { return mView; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Composition& aComposition) {
+ aStream << "{ mView=0x" << aComposition.mView.get()
+ << ", OffsetAndData<LONG>="
+ << static_cast<const OffsetAndData<LONG>&>(aComposition) << " }";
+ return aStream;
+ }
+
+ private:
+ RefPtr<ITfCompositionView> const mView;
+ };
+ // While the document is locked, we cannot dispatch any events which cause
+ // DOM events since the DOM events' handlers may modify the locked document.
+ // However, even while the document is locked, TSF may queries us.
+ // For that, TSFTextStore modifies mComposition even while the document is
+ // locked. With mComposition, query methods can returns the text content
+ // information.
+ Maybe<Composition> mComposition;
+
+ /**
+ * IsHandlingCompositionInParent() returns true if eCompositionStart is
+ * dispatched, but eCompositionCommit(AsIs) is not dispatched. This means
+ * that if composition is handled in a content process, this status indicates
+ * whether ContentCacheInParent has composition or not. On the other hand,
+ * if it's handled in the chrome process, this is exactly same as
+ * IsHandlingCompositionInContent().
+ */
+ bool IsHandlingCompositionInParent() const {
+ return mDispatcher && mDispatcher->IsComposing();
+ }
+
+ /**
+ * IsHandlingCompositionInContent() returns true if there is a composition in
+ * the focused editor which may be in a content process.
+ */
+ bool IsHandlingCompositionInContent() const {
+ return mDispatcher && mDispatcher->IsHandlingComposition();
+ }
+
+ class Selection {
+ public:
+ static TS_SELECTION_ACP EmptyACP() {
+ return TS_SELECTION_ACP{
+ .acpStart = 0,
+ .acpEnd = 0,
+ .style = {.ase = TS_AE_NONE, .fInterimChar = FALSE}};
+ }
+
+ bool HasRange() const { return mACP.isSome(); }
+ const TS_SELECTION_ACP& ACPRef() const { return mACP.ref(); }
+
+ explicit Selection(const TS_SELECTION_ACP& aSelection) {
+ SetSelection(aSelection);
+ }
+
+ explicit Selection(uint32_t aOffsetToCollapse) {
+ Collapse(aOffsetToCollapse);
+ }
+
+ explicit Selection(const SelectionChangeDataBase& aSelectionChangeData) {
+ SetSelection(aSelectionChangeData);
+ }
+
+ explicit Selection(const WidgetQueryContentEvent& aQuerySelectionEvent) {
+ SetSelection(aQuerySelectionEvent);
+ }
+
+ Selection(uint32_t aStart, uint32_t aLength, bool aReversed,
+ const WritingMode& aWritingMode) {
+ SetSelection(aStart, aLength, aReversed, aWritingMode);
+ }
+
+ void SetSelection(const TS_SELECTION_ACP& aSelection) {
+ mACP = Some(aSelection);
+ // Selection end must be active in our editor.
+ if (mACP->style.ase != TS_AE_START) {
+ mACP->style.ase = TS_AE_END;
+ }
+ // We're not support interim char selection for now.
+ // XXX Probably, this is necessary for supporting South Asian languages.
+ mACP->style.fInterimChar = FALSE;
+ }
+
+ bool SetSelection(const SelectionChangeDataBase& aSelectionChangeData) {
+ MOZ_ASSERT(aSelectionChangeData.IsInitialized());
+ if (!aSelectionChangeData.HasRange()) {
+ if (mACP.isNothing()) {
+ return false;
+ }
+ mACP.reset();
+ // Let's keep the WritingMode because users don't want to change the UI
+ // of TIP temporarily since no selection case is created only by web
+ // apps, but they or TIP would restore selection at last point later.
+ return true;
+ }
+ return SetSelection(aSelectionChangeData.mOffset,
+ aSelectionChangeData.Length(),
+ aSelectionChangeData.mReversed,
+ aSelectionChangeData.GetWritingMode());
+ }
+
+ bool SetSelection(const WidgetQueryContentEvent& aQuerySelectionEvent) {
+ MOZ_ASSERT(aQuerySelectionEvent.mMessage == eQuerySelectedText);
+ MOZ_ASSERT(aQuerySelectionEvent.Succeeded());
+ if (aQuerySelectionEvent.DidNotFindSelection()) {
+ if (mACP.isNothing()) {
+ return false;
+ }
+ mACP.reset();
+ // Let's keep the WritingMode because users don't want to change the UI
+ // of TIP temporarily since no selection case is created only by web
+ // apps, but they or TIP would restore selection at last point later.
+ return true;
+ }
+ return SetSelection(aQuerySelectionEvent.mReply->StartOffset(),
+ aQuerySelectionEvent.mReply->DataLength(),
+ aQuerySelectionEvent.mReply->mReversed,
+ aQuerySelectionEvent.mReply->WritingModeRef());
+ }
+
+ bool SetSelection(uint32_t aStart, uint32_t aLength, bool aReversed,
+ const WritingMode& aWritingMode) {
+ const bool changed = mACP.isNothing() ||
+ mACP->acpStart != static_cast<LONG>(aStart) ||
+ mACP->acpEnd != static_cast<LONG>(aStart + aLength);
+ mACP = Some(
+ TS_SELECTION_ACP{.acpStart = static_cast<LONG>(aStart),
+ .acpEnd = static_cast<LONG>(aStart + aLength),
+ .style = {.ase = aReversed ? TS_AE_START : TS_AE_END,
+ .fInterimChar = FALSE}});
+ mWritingMode = aWritingMode;
+
+ return changed;
+ }
+
+ bool Collapsed() const {
+ return mACP.isNothing() || mACP->acpStart == mACP->acpEnd;
+ }
+
+ void Collapse(uint32_t aOffset) {
+ // XXX This does not update the selection's mWritingMode.
+ // If it is ever used to "collapse" to an entirely new location,
+ // we may need to fix that.
+ mACP = Some(
+ TS_SELECTION_ACP{.acpStart = static_cast<LONG>(aOffset),
+ .acpEnd = static_cast<LONG>(aOffset),
+ .style = {.ase = TS_AE_END, .fInterimChar = FALSE}});
+ }
+
+ LONG MinOffset() const {
+ MOZ_ASSERT(mACP.isSome());
+ LONG min = std::min(mACP->acpStart, mACP->acpEnd);
+ MOZ_ASSERT(min >= 0);
+ return min;
+ }
+
+ LONG MaxOffset() const {
+ MOZ_ASSERT(mACP.isSome());
+ LONG max = std::max(mACP->acpStart, mACP->acpEnd);
+ MOZ_ASSERT(max >= 0);
+ return max;
+ }
+
+ LONG StartOffset() const {
+ MOZ_ASSERT(mACP.isSome());
+ MOZ_ASSERT(mACP->acpStart >= 0);
+ return mACP->acpStart;
+ }
+
+ LONG EndOffset() const {
+ MOZ_ASSERT(mACP.isSome());
+ MOZ_ASSERT(mACP->acpEnd >= 0);
+ return mACP->acpEnd;
+ }
+
+ LONG Length() const {
+ MOZ_ASSERT_IF(mACP.isSome(), mACP->acpEnd >= mACP->acpStart);
+ return mACP.isSome() ? std::abs(mACP->acpEnd - mACP->acpStart) : 0;
+ }
+
+ bool IsReversed() const {
+ return mACP.isSome() && mACP->style.ase == TS_AE_START;
+ }
+
+ TsActiveSelEnd ActiveSelEnd() const {
+ return mACP.isSome() ? mACP->style.ase : TS_AE_NONE;
+ }
+
+ bool IsInterimChar() const {
+ return mACP.isSome() && mACP->style.fInterimChar != FALSE;
+ }
+
+ const WritingMode& WritingModeRef() const { return mWritingMode; }
+
+ bool EqualsExceptDirection(const TS_SELECTION_ACP& aACP) const {
+ if (mACP.isNothing()) {
+ return false;
+ }
+ if (mACP->style.ase == aACP.style.ase) {
+ return mACP->acpStart == aACP.acpStart && mACP->acpEnd == aACP.acpEnd;
+ }
+ return mACP->acpStart == aACP.acpEnd && mACP->acpEnd == aACP.acpStart;
+ }
+
+ bool EqualsExceptDirection(
+ const SelectionChangeDataBase& aChangedSelection) const {
+ MOZ_ASSERT(aChangedSelection.IsInitialized());
+ if (mACP.isNothing()) {
+ return aChangedSelection.HasRange();
+ }
+ return aChangedSelection.Length() == static_cast<uint32_t>(Length()) &&
+ aChangedSelection.mOffset == static_cast<uint32_t>(StartOffset());
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Selection& aSelection) {
+ aStream << "{ mACP=" << ToString(aSelection.mACP).c_str()
+ << ", mWritingMode=" << ToString(aSelection.mWritingMode).c_str()
+ << ", Collapsed()="
+ << (aSelection.Collapsed() ? "true" : "false")
+ << ", Length=" << aSelection.Length() << " }";
+ return aStream;
+ }
+
+ private:
+ Maybe<TS_SELECTION_ACP> mACP; // If Nothing, there is no selection
+ WritingMode mWritingMode;
+ };
+ // Don't access mSelection directly. Instead, Use SelectionForTSFRef().
+ // This is modified immediately when TSF requests to set selection and not
+ // updated by selection change in content until mContentForTSF is cleared.
+ Maybe<Selection> mSelectionForTSF;
+
+ /**
+ * Get the selection expected by TSF. If mSelectionForTSF is already valid,
+ * this just return the reference to it. Otherwise, this initializes it
+ * with eQuerySelectedText. Please check if the result is valid before
+ * actually using it.
+ * Note that this is also called by ContentForTSF().
+ */
+ Maybe<Selection>& SelectionForTSF();
+
+ struct PendingAction final {
+ enum class Type : uint8_t {
+ eCompositionStart,
+ eCompositionUpdate,
+ eCompositionEnd,
+ eSetSelection,
+ eKeyboardEvent,
+ };
+ Type mType;
+ // For eCompositionStart, eCompositionEnd and eSetSelection
+ LONG mSelectionStart;
+ // For eCompositionStart and eSetSelection
+ LONG mSelectionLength;
+ // For eCompositionStart, eCompositionUpdate and eCompositionEnd
+ nsString mData;
+ // For eCompositionUpdate
+ RefPtr<TextRangeArray> mRanges;
+ // For eKeyboardEvent
+ MSG mKeyMsg;
+ // For eSetSelection
+ bool mSelectionReversed;
+ // For eCompositionUpdate
+ bool mIncomplete;
+ // For eCompositionStart
+ bool mAdjustSelection;
+ };
+ // Items of mPendingActions are appended when TSF tells us to need to dispatch
+ // DOM composition events. However, we cannot dispatch while the document is
+ // locked because it can cause modifying the locked document. So, the pending
+ // actions should be performed when document lock is unlocked.
+ nsTArray<PendingAction> mPendingActions;
+
+ PendingAction* LastOrNewPendingCompositionUpdate() {
+ if (!mPendingActions.IsEmpty()) {
+ PendingAction& lastAction = mPendingActions.LastElement();
+ if (lastAction.mType == PendingAction::Type::eCompositionUpdate) {
+ return &lastAction;
+ }
+ }
+ PendingAction* newAction = mPendingActions.AppendElement();
+ newAction->mType = PendingAction::Type::eCompositionUpdate;
+ newAction->mRanges = new TextRangeArray();
+ newAction->mIncomplete = true;
+ return newAction;
+ }
+
+ /**
+ * IsLastPendingActionCompositionEndAt() checks whether the previous pending
+ * action is committing composition whose range starts from aStart and its
+ * length is aLength. In other words, this checks whether new composition
+ * which will replace same range as previous pending commit can be merged
+ * with the previous composition.
+ *
+ * @param aStart The inserted offset you expected.
+ * @param aLength The inserted text length you expected.
+ * @return true if the last pending action is
+ * eCompositionEnd and it inserted the text
+ * between aStart and aStart + aLength.
+ */
+ bool IsLastPendingActionCompositionEndAt(LONG aStart, LONG aLength) const {
+ if (mPendingActions.IsEmpty()) {
+ return false;
+ }
+ const PendingAction& pendingLastAction = mPendingActions.LastElement();
+ return pendingLastAction.mType == PendingAction::Type::eCompositionEnd &&
+ pendingLastAction.mSelectionStart == aStart &&
+ pendingLastAction.mData.Length() == static_cast<ULONG>(aLength);
+ }
+
+ bool IsPendingCompositionUpdateIncomplete() const {
+ if (mPendingActions.IsEmpty()) {
+ return false;
+ }
+ const PendingAction& lastAction = mPendingActions.LastElement();
+ return lastAction.mType == PendingAction::Type::eCompositionUpdate &&
+ lastAction.mIncomplete;
+ }
+
+ void CompleteLastActionIfStillIncomplete() {
+ if (!IsPendingCompositionUpdateIncomplete()) {
+ return;
+ }
+ RecordCompositionUpdateAction();
+ }
+
+ void RemoveLastCompositionUpdateActions() {
+ while (!mPendingActions.IsEmpty()) {
+ const PendingAction& lastAction = mPendingActions.LastElement();
+ if (lastAction.mType != PendingAction::Type::eCompositionUpdate) {
+ break;
+ }
+ mPendingActions.RemoveLastElement();
+ }
+ }
+
+ // When On*Composition() is called without document lock, we need to flush
+ // the recorded actions at quitting the method.
+ // AutoPendingActionAndContentFlusher class is usedful for it.
+ class MOZ_STACK_CLASS AutoPendingActionAndContentFlusher final {
+ public:
+ explicit AutoPendingActionAndContentFlusher(TSFTextStore* aTextStore)
+ : mTextStore(aTextStore) {
+ MOZ_ASSERT(!mTextStore->mIsRecordingActionsWithoutLock);
+ if (!mTextStore->IsReadWriteLocked()) {
+ mTextStore->mIsRecordingActionsWithoutLock = true;
+ }
+ }
+
+ ~AutoPendingActionAndContentFlusher() {
+ if (!mTextStore->mIsRecordingActionsWithoutLock) {
+ return;
+ }
+ mTextStore->FlushPendingActions();
+ mTextStore->mIsRecordingActionsWithoutLock = false;
+ }
+
+ private:
+ AutoPendingActionAndContentFlusher() {}
+
+ RefPtr<TSFTextStore> mTextStore;
+ };
+
+ class Content final {
+ public:
+ Content(TSFTextStore& aTSFTextStore, const nsAString& aText)
+ : mText(aText),
+ mLastComposition(aTSFTextStore.mComposition),
+ mComposition(aTSFTextStore.mComposition),
+ mSelection(aTSFTextStore.mSelectionForTSF) {}
+
+ void OnLayoutChanged() { mMinModifiedOffset.reset(); }
+
+ // OnCompositionEventsHandled() is called when all pending composition
+ // events are handled in the focused content which may be in a remote
+ // process.
+ void OnCompositionEventsHandled() { mLastComposition = mComposition; }
+
+ const nsDependentSubstring GetSelectedText() const;
+ const nsDependentSubstring GetSubstring(uint32_t aStart,
+ uint32_t aLength) const;
+ void ReplaceSelectedTextWith(const nsAString& aString);
+ void ReplaceTextWith(LONG aStart, LONG aLength, const nsAString& aString);
+
+ void StartComposition(ITfCompositionView* aCompositionView,
+ const PendingAction& aCompStart,
+ bool aPreserveSelection);
+ /**
+ * RestoreCommittedComposition() restores the committed string as
+ * composing string. If InsertTextAtSelection() or something is called
+ * before a call of OnStartComposition() or previous composition is
+ * committed and new composition is restarted to clean up the commited
+ * string, there is a pending compositionend. In this case, we need to
+ * cancel the pending compositionend and continue the composition.
+ *
+ * @param aCompositionView The composition view.
+ * @param aCanceledCompositionEnd The pending compositionend which is
+ * canceled for restarting the composition.
+ */
+ void RestoreCommittedComposition(
+ ITfCompositionView* aCompositionView,
+ const PendingAction& aCanceledCompositionEnd);
+ void EndComposition(const PendingAction& aCompEnd);
+
+ const nsString& TextRef() const { return mText; }
+ const Maybe<OffsetAndData<LONG>>& LastComposition() const {
+ return mLastComposition;
+ }
+ const Maybe<uint32_t>& MinModifiedOffset() const {
+ return mMinModifiedOffset;
+ }
+ const Maybe<StartAndEndOffsets<LONG>>& LatestCompositionRange() const {
+ return mLatestCompositionRange;
+ }
+
+ // Returns true if layout of the character at the aOffset has not been
+ // calculated.
+ bool IsLayoutChangedAt(uint32_t aOffset) const {
+ return IsLayoutChanged() && (mMinModifiedOffset.value() <= aOffset);
+ }
+ // Returns true if layout of the content has been changed, i.e., the new
+ // layout has not been calculated.
+ bool IsLayoutChanged() const { return mMinModifiedOffset.isSome(); }
+ bool HasOrHadComposition() const {
+ return mLatestCompositionRange.isSome();
+ }
+
+ Maybe<TSFTextStore::Composition>& Composition() { return mComposition; }
+ Maybe<TSFTextStore::Selection>& Selection() { return mSelection; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Content& aContent) {
+ aStream << "{ mText="
+ << PrintStringDetail(aContent.mText,
+ PrintStringDetail::kMaxLengthForEditor)
+ .get()
+ << ", mLastComposition=" << aContent.mLastComposition
+ << ", mLatestCompositionRange="
+ << aContent.mLatestCompositionRange
+ << ", mMinModifiedOffset=" << aContent.mMinModifiedOffset << " }";
+ return aStream;
+ }
+
+ private:
+ nsString mText;
+
+ // mLastComposition may store the composition string and its start offset
+ // when the document is locked. This is necessary to compute
+ // mMinTextModifiedOffset.
+ Maybe<OffsetAndData<LONG>> mLastComposition;
+
+ Maybe<TSFTextStore::Composition>& mComposition;
+ Maybe<TSFTextStore::Selection>& mSelection;
+
+ // The latest composition's start and end offset.
+ Maybe<StartAndEndOffsets<LONG>> mLatestCompositionRange;
+
+ // The minimum offset of modified part of the text.
+ Maybe<uint32_t> mMinModifiedOffset;
+ };
+ // mContentForTSF is cache of content. The information is expected by TSF
+ // and TIP. Therefore, this is useful for answering the query from TSF or
+ // TIP.
+ // This is initialized by ContentForTSF() automatically (therefore, don't
+ // access this member directly except at calling Clear(), IsInitialized(),
+ // IsLayoutChangeAfter() or IsLayoutChanged()).
+ // This is cleared when:
+ // - When there is no composition, the document is unlocked.
+ // - When there is a composition, all dispatched events are handled by
+ // the focused editor which may be in a remote process.
+ // So, if two compositions are created very quickly, this cache may not be
+ // cleared between eCompositionCommit(AsIs) and eCompositionStart.
+ Maybe<Content> mContentForTSF;
+
+ Maybe<Content>& ContentForTSF();
+
+ class MOZ_STACK_CLASS AutoNotifyingTSFBatch final {
+ public:
+ explicit AutoNotifyingTSFBatch(TSFTextStore& aTextStore)
+ : mTextStore(aTextStore), mOldValue(aTextStore.mDeferNotifyingTSF) {
+ mTextStore.mDeferNotifyingTSF = true;
+ }
+ ~AutoNotifyingTSFBatch() {
+ mTextStore.mDeferNotifyingTSF = mOldValue;
+ mTextStore.MaybeFlushPendingNotifications();
+ }
+
+ private:
+ TSFTextStore& mTextStore;
+ bool mOldValue;
+ };
+
+ // CanAccessActualContentDirectly() returns true when TSF/TIP can access
+ // actual content directly. In other words, mContentForTSF and/or
+ // mSelectionForTSF doesn't cache content or they matches with actual
+ // contents due to no pending text/selection change notifications.
+ bool CanAccessActualContentDirectly() const;
+
+ // While mContentForTSF is valid, this returns the text stored by it.
+ // Otherwise, return the current text content retrieved by eQueryTextContent.
+ bool GetCurrentText(nsAString& aTextContent);
+
+ class MouseTracker final {
+ public:
+ static const DWORD kInvalidCookie = static_cast<DWORD>(-1);
+
+ MouseTracker();
+
+ HRESULT Init(TSFTextStore* aTextStore);
+ HRESULT AdviseSink(TSFTextStore* aTextStore, ITfRangeACP* aTextRange,
+ ITfMouseSink* aMouseSink);
+ void UnadviseSink();
+
+ bool IsUsing() const { return mSink != nullptr; }
+ DWORD Cookie() const { return mCookie; }
+ bool OnMouseButtonEvent(ULONG aEdge, ULONG aQuadrant, DWORD aButtonStatus);
+ const Maybe<StartAndEndOffsets<LONG>> Range() const { return mRange; }
+
+ private:
+ RefPtr<ITfMouseSink> mSink;
+ Maybe<StartAndEndOffsets<LONG>> mRange;
+ DWORD mCookie;
+ };
+ // mMouseTrackers is an array to store each information of installed
+ // ITfMouseSink instance.
+ nsTArray<MouseTracker> mMouseTrackers;
+
+ // The input scopes for this context, defaults to IS_DEFAULT.
+ nsTArray<InputScope> mInputScopes;
+
+ // The URL cache of the focused document.
+ nsString mDocumentURL;
+
+ // Support retrieving attributes.
+ // TODO: We should support RightToLeft, perhaps.
+ enum {
+ // Used for result of GetRequestedAttrIndex()
+ eNotSupported = -1,
+
+ // Supported attributes
+ eInputScope = 0,
+ eDocumentURL,
+ eTextVerticalWriting,
+ eTextOrientation,
+
+ // Count of the supported attributes
+ NUM_OF_SUPPORTED_ATTRS
+ };
+ bool mRequestedAttrs[NUM_OF_SUPPORTED_ATTRS] = {false};
+
+ int32_t GetRequestedAttrIndex(const TS_ATTRID& aAttrID);
+ TS_ATTRID GetAttrID(int32_t aIndex);
+
+ bool mRequestedAttrValues = false;
+
+ // If edit actions are being recorded without document lock, this is true.
+ // Otherwise, false.
+ bool mIsRecordingActionsWithoutLock = false;
+ // If GetTextExt() or GetACPFromPoint() is called and the layout hasn't been
+ // calculated yet, these methods return TS_E_NOLAYOUT. At that time,
+ // mHasReturnedNoLayoutError is set to true.
+ bool mHasReturnedNoLayoutError = false;
+ // Before calling ITextStoreACPSink::OnLayoutChange() and
+ // ITfContextOwnerServices::OnLayoutChange(), mWaitingQueryLayout is set to
+ // true. This is set to false when GetTextExt() or GetACPFromPoint() is
+ // called.
+ bool mWaitingQueryLayout = false;
+ // During the document is locked, we shouldn't destroy the instance.
+ // If this is true, the instance will be destroyed after unlocked.
+ bool mPendingDestroy = false;
+ // If this is false, MaybeFlushPendingNotifications() will clear the
+ // mContentForTSF.
+ bool mDeferClearingContentForTSF = false;
+ // While the instance is initializing content/selection cache, another
+ // initialization shouldn't run recursively. Therefore, while the
+ // initialization is running, this is set to true. Use AutoNotifyingTSFBatch
+ // to set this.
+ bool mDeferNotifyingTSF = false;
+ // While the instance is dispatching events, the event may not be handled
+ // synchronously when remote content has focus. In the case, we cannot
+ // return the latest layout/content information to TSF/TIP until we get next
+ // update notification from ContentCacheInParent. For preventing TSF/TIP
+ // retrieves the latest content/layout information while it becomes available,
+ // we should put off notifying TSF of any updates.
+ bool mDeferNotifyingTSFUntilNextUpdate = false;
+ // While the document is locked, committing composition always fails since
+ // TSF needs another document lock for modifying the composition, selection
+ // and etc. So, committing composition should be performed after the
+ // document is unlocked.
+ bool mDeferCommittingComposition = false;
+ bool mDeferCancellingComposition = false;
+ // Immediately after a call of Destroy(), mDestroyed becomes true. If this
+ // is true, the instance shouldn't grant any requests from the TIP anymore.
+ bool mDestroyed = false;
+ // While the instance is being destroyed, this is set to true for avoiding
+ // recursive Destroy() calls.
+ bool mBeingDestroyed = false;
+ // Whether we're in the private browsing mode.
+ bool mInPrivateBrowsing = true;
+ // Debug flag to check whether we're initializing mContentForTSF and
+ // mSelectionForTSF.
+ bool mIsInitializingContentForTSF = false;
+ bool mIsInitializingSelectionForTSF = false;
+
+ // TSF thread manager object for the current application
+ static StaticRefPtr<ITfThreadMgr> sThreadMgr;
+ static already_AddRefed<ITfThreadMgr> GetThreadMgr();
+ // sMessagePump is QI'ed from sThreadMgr
+ static StaticRefPtr<ITfMessagePump> sMessagePump;
+
+ public:
+ // Expose GetMessagePump() for WinUtils.
+ static already_AddRefed<ITfMessagePump> GetMessagePump();
+
+ private:
+ // sKeystrokeMgr is QI'ed from sThreadMgr
+ static StaticRefPtr<ITfKeystrokeMgr> sKeystrokeMgr;
+ // TSF display attribute manager
+ static StaticRefPtr<ITfDisplayAttributeMgr> sDisplayAttrMgr;
+ static already_AddRefed<ITfDisplayAttributeMgr> GetDisplayAttributeMgr();
+ // TSF category manager
+ static StaticRefPtr<ITfCategoryMgr> sCategoryMgr;
+ static already_AddRefed<ITfCategoryMgr> GetCategoryMgr();
+ // Compartment for (Get|Set)IMEOpenState()
+ static StaticRefPtr<ITfCompartment> sCompartmentForOpenClose;
+ static already_AddRefed<ITfCompartment> GetCompartmentForOpenClose();
+
+ // Current text store which is managing a keyboard enabled editor (i.e.,
+ // editable editor). Currently only ONE TSFTextStore instance is ever used,
+ // although Create is called when an editor is focused and Destroy called
+ // when the focused editor is blurred.
+ static StaticRefPtr<TSFTextStore> sEnabledTextStore;
+
+ // For IME (keyboard) disabled state:
+ static StaticRefPtr<ITfDocumentMgr> sDisabledDocumentMgr;
+ static StaticRefPtr<ITfContext> sDisabledContext;
+
+ static StaticRefPtr<ITfInputProcessorProfiles> sInputProcessorProfiles;
+ static already_AddRefed<ITfInputProcessorProfiles>
+ GetInputProcessorProfiles();
+
+ // Handling key message.
+ static const MSG* sHandlingKeyMsg;
+
+ // TSF client ID for the current application
+ static DWORD sClientId;
+
+ // true if an eKeyDown or eKeyUp event for sHandlingKeyMsg has already
+ // been dispatched.
+ static bool sIsKeyboardEventDispatched;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef TSFTextStore_h_
diff --git a/widget/windows/TaskbarPreview.cpp b/widget/windows/TaskbarPreview.cpp
new file mode 100644
index 0000000000..2b958f84c1
--- /dev/null
+++ b/widget/windows/TaskbarPreview.cpp
@@ -0,0 +1,413 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "TaskbarPreview.h"
+#include <nsITaskbarPreviewController.h>
+#include <windows.h>
+
+#include <nsError.h>
+#include <nsCOMPtr.h>
+#include <nsIWidget.h>
+#include <nsServiceManagerUtils.h>
+
+#include "nsUXThemeData.h"
+#include "nsWindow.h"
+#include "nsAppShell.h"
+#include "TaskbarPreviewButton.h"
+#include "WinUtils.h"
+
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/Telemetry.h"
+
+// Defined in dwmapi in a header that needs a higher numbered _WINNT #define
+#ifndef DWM_SIT_DISPLAYFRAME
+# define DWM_SIT_DISPLAYFRAME 0x1
+#endif
+
+namespace mozilla {
+namespace widget {
+
+///////////////////////////////////////////////////////////////////////////////
+// TaskbarPreview
+
+TaskbarPreview::TaskbarPreview(ITaskbarList4* aTaskbar,
+ nsITaskbarPreviewController* aController,
+ HWND aHWND, nsIDocShell* aShell)
+ : mTaskbar(aTaskbar),
+ mController(aController),
+ mWnd(aHWND),
+ mVisible(false),
+ mDocShell(do_GetWeakReference(aShell)) {}
+
+TaskbarPreview::~TaskbarPreview() {
+ // Avoid dangling pointer
+ if (sActivePreview == this) sActivePreview = nullptr;
+
+ // Our subclass should have invoked DetachFromNSWindow already.
+ NS_ASSERTION(
+ !mWnd,
+ "TaskbarPreview::DetachFromNSWindow was not called before destruction");
+
+ // Make sure to release before potentially uninitializing COM
+ mTaskbar = nullptr;
+
+ ::CoUninitialize();
+}
+
+nsresult TaskbarPreview::Init() {
+ // TaskbarPreview may outlive the WinTaskbar that created it
+ if (FAILED(::CoInitialize(nullptr))) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ WindowHook* hook = GetWindowHook();
+ if (!hook) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return hook->AddMonitor(WM_DESTROY, MainWindowHook, this);
+}
+
+NS_IMETHODIMP
+TaskbarPreview::SetController(nsITaskbarPreviewController* aController) {
+ NS_ENSURE_ARG(aController);
+
+ mController = aController;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreview::GetController(nsITaskbarPreviewController** aController) {
+ NS_ADDREF(*aController = mController);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreview::GetTooltip(nsAString& aTooltip) {
+ aTooltip = mTooltip;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreview::SetTooltip(const nsAString& aTooltip) {
+ mTooltip = aTooltip;
+ return CanMakeTaskbarCalls() ? UpdateTooltip() : NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreview::SetVisible(bool visible) {
+ if (mVisible == visible) return NS_OK;
+ mVisible = visible;
+
+ // If the nsWindow has already been destroyed but the caller is still trying
+ // to use it then just pretend that everything succeeded. The caller doesn't
+ // actually have a way to detect this since it's the same case as when we
+ // CanMakeTaskbarCalls returns false.
+ if (!IsWindowAvailable()) return NS_OK;
+
+ return visible ? Enable() : Disable();
+}
+
+NS_IMETHODIMP
+TaskbarPreview::GetVisible(bool* visible) {
+ *visible = mVisible;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreview::SetActive(bool active) {
+ if (active)
+ sActivePreview = this;
+ else if (sActivePreview == this)
+ sActivePreview = nullptr;
+
+ return CanMakeTaskbarCalls() ? ShowActive(active) : NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreview::GetActive(bool* active) {
+ *active = sActivePreview == this;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreview::Invalidate() {
+ if (!mVisible) return NS_OK;
+
+ HWND previewWindow = PreviewWindow();
+ return FAILED(DwmInvalidateIconicBitmaps(previewWindow)) ? NS_ERROR_FAILURE
+ : NS_OK;
+}
+
+nsresult TaskbarPreview::UpdateTaskbarProperties() {
+ nsresult rv = UpdateTooltip();
+
+ // If we are the active preview and our window is the active window, restore
+ // our active state - otherwise some other non-preview window is now active
+ // and should be displayed as so.
+ if (sActivePreview == this) {
+ if (mWnd == ::GetActiveWindow()) {
+ nsresult rvActive = ShowActive(true);
+ if (NS_FAILED(rvActive)) rv = rvActive;
+ } else {
+ sActivePreview = nullptr;
+ }
+ }
+ return rv;
+}
+
+nsresult TaskbarPreview::Enable() {
+ nsresult rv = NS_OK;
+ if (CanMakeTaskbarCalls()) {
+ rv = UpdateTaskbarProperties();
+ } else if (IsWindowAvailable()) {
+ WindowHook* hook = GetWindowHook();
+ MOZ_ASSERT(hook,
+ "IsWindowAvailable() should have eliminated the null case.");
+ hook->AddMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(),
+ MainWindowHook, this);
+ }
+ return rv;
+}
+
+nsresult TaskbarPreview::Disable() {
+ if (!IsWindowAvailable()) {
+ // Window is already destroyed
+ return NS_OK;
+ }
+
+ WindowHook* hook = GetWindowHook();
+ MOZ_ASSERT(hook, "IsWindowAvailable() should have eliminated the null case.");
+ (void)hook->RemoveMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(),
+ MainWindowHook, this);
+
+ return NS_OK;
+}
+
+bool TaskbarPreview::IsWindowAvailable() const {
+ if (mWnd) {
+ nsWindow* win = WinUtils::GetNSWindowPtr(mWnd);
+ if (win && !win->Destroyed()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void TaskbarPreview::DetachFromNSWindow() {
+ if (WindowHook* hook = GetWindowHook()) {
+ hook->RemoveMonitor(WM_DESTROY, MainWindowHook, this);
+ }
+ mWnd = nullptr;
+}
+
+LRESULT
+TaskbarPreview::WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) {
+ switch (nMsg) {
+ case WM_DWMSENDICONICTHUMBNAIL: {
+ uint32_t width = HIWORD(lParam);
+ uint32_t height = LOWORD(lParam);
+ float aspectRatio = width / float(height);
+
+ nsresult rv;
+ float preferredAspectRatio;
+ rv = mController->GetThumbnailAspectRatio(&preferredAspectRatio);
+ if (NS_FAILED(rv)) break;
+
+ uint32_t thumbnailWidth = width;
+ uint32_t thumbnailHeight = height;
+
+ if (aspectRatio > preferredAspectRatio) {
+ thumbnailWidth = uint32_t(thumbnailHeight * preferredAspectRatio);
+ } else {
+ thumbnailHeight = uint32_t(thumbnailWidth / preferredAspectRatio);
+ }
+
+ DrawBitmap(thumbnailWidth, thumbnailHeight, false);
+ } break;
+ case WM_DWMSENDICONICLIVEPREVIEWBITMAP: {
+ uint32_t width, height;
+ nsresult rv;
+ rv = mController->GetWidth(&width);
+ if (NS_FAILED(rv)) break;
+ rv = mController->GetHeight(&height);
+ if (NS_FAILED(rv)) break;
+
+ double scale = StaticPrefs::layout_css_devPixelsPerPx();
+ if (scale <= 0.0) {
+ scale = WinUtils::LogToPhysFactor(PreviewWindow());
+ }
+ DrawBitmap(NSToIntRound(scale * width), NSToIntRound(scale * height),
+ true);
+ } break;
+ }
+ return ::DefWindowProcW(PreviewWindow(), nMsg, wParam, lParam);
+}
+
+bool TaskbarPreview::CanMakeTaskbarCalls() {
+ // If the nsWindow has already been destroyed and we know it but our caller
+ // clearly doesn't so we can't make any calls.
+ if (!mWnd) return false;
+ // Certain functions like SetTabOrder seem to require a visible window. During
+ // window close, the window seems to be hidden before being destroyed.
+ if (!::IsWindowVisible(mWnd)) return false;
+ if (mVisible) {
+ nsWindow* window = WinUtils::GetNSWindowPtr(mWnd);
+ NS_ASSERTION(window, "Could not get nsWindow from HWND");
+ return window ? window->HasTaskbarIconBeenCreated() : false;
+ }
+ return false;
+}
+
+WindowHook* TaskbarPreview::GetWindowHook() {
+ nsWindow* window = WinUtils::GetNSWindowPtr(mWnd);
+ NS_ASSERTION(window, "Cannot use taskbar previews in an embedded context!");
+
+ return window ? &window->GetWindowHook() : nullptr;
+}
+
+void TaskbarPreview::EnableCustomDrawing(HWND aHWND, bool aEnable) {
+ BOOL enabled = aEnable;
+ DwmSetWindowAttribute(aHWND, DWMWA_FORCE_ICONIC_REPRESENTATION, &enabled,
+ sizeof(enabled));
+
+ DwmSetWindowAttribute(aHWND, DWMWA_HAS_ICONIC_BITMAP, &enabled,
+ sizeof(enabled));
+}
+
+nsresult TaskbarPreview::UpdateTooltip() {
+ NS_ASSERTION(CanMakeTaskbarCalls() && mVisible,
+ "UpdateTooltip called on invisible tab preview");
+
+ if (FAILED(mTaskbar->SetThumbnailTooltip(PreviewWindow(), mTooltip.get())))
+ return NS_ERROR_FAILURE;
+ return NS_OK;
+}
+
+void TaskbarPreview::DrawBitmap(uint32_t width, uint32_t height,
+ bool isPreview) {
+ nsresult rv;
+ nsCOMPtr<nsITaskbarPreviewCallback> callback =
+ do_CreateInstance("@mozilla.org/widget/taskbar-preview-callback;1", &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ ((TaskbarPreviewCallback*)callback.get())->SetPreview(this);
+
+ if (isPreview) {
+ ((TaskbarPreviewCallback*)callback.get())->SetIsPreview();
+ mController->RequestPreview(callback);
+ } else {
+ mController->RequestThumbnail(callback, width, height);
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TaskbarPreviewCallback
+
+NS_IMPL_ISUPPORTS(TaskbarPreviewCallback, nsITaskbarPreviewCallback)
+
+/* void done (in nsISupports aCanvas, in boolean aDrawBorder); */
+NS_IMETHODIMP
+TaskbarPreviewCallback::Done(nsISupports* aCanvas, bool aDrawBorder) {
+ // We create and destroy TaskbarTabPreviews from front end code in response
+ // to TabOpen and TabClose events. Each TaskbarTabPreview creates and owns a
+ // proxy HWND which it hands to Windows as a tab identifier. When a tab
+ // closes, TaskbarTabPreview Disable() method is called by front end, which
+ // destroys the proxy window and clears mProxyWindow which is the HWND
+ // returned from PreviewWindow(). So, since this is async, we should check to
+ // be sure the tab is still alive before doing all this gfx work and making
+ // dwm calls. To accomplish this we check the result of PreviewWindow().
+ if (!aCanvas || !mPreview || !mPreview->PreviewWindow() ||
+ !mPreview->IsWindowAvailable()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aCanvas));
+ auto canvas = dom::HTMLCanvasElement::FromNodeOrNull(content);
+ if (!canvas) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<gfx::SourceSurface> source = canvas->GetSurfaceSnapshot();
+ if (!source) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<gfxWindowsSurface> target = new gfxWindowsSurface(
+ source->GetSize(), gfx::SurfaceFormat::A8R8G8B8_UINT32);
+ if (target->CairoStatus() != CAIRO_STATUS_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ using DataSrcSurf = gfx::DataSourceSurface;
+ RefPtr<DataSrcSurf> srcSurface = source->GetDataSurface();
+ RefPtr<gfxImageSurface> imageSurface = target->GetAsImageSurface();
+ if (!srcSurface || !imageSurface) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (DataSrcSurf::ScopedMap const sourceMap(srcSurface, DataSrcSurf::READ);
+ sourceMap.IsMapped()) {
+ mozilla::gfx::CopySurfaceDataToPackedArray(
+ sourceMap.GetData(), imageSurface->Data(), srcSurface->GetSize(),
+ sourceMap.GetStride(), BytesPerPixel(srcSurface->GetFormat()));
+ } else if (source->GetSize().IsEmpty()) {
+ // A zero-size source-surface probably shouldn't happen, but is harmless
+ // here. Fall through.
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+
+ HDC hDC = target->GetDC();
+ HBITMAP hBitmap = (HBITMAP)GetCurrentObject(hDC, OBJ_BITMAP);
+
+ DWORD flags = aDrawBorder ? DWM_SIT_DISPLAYFRAME : 0;
+ HRESULT hr;
+ if (!mIsThumbnail) {
+ POINT pptClient = {0, 0};
+ hr = DwmSetIconicLivePreviewBitmap(mPreview->PreviewWindow(), hBitmap,
+ &pptClient, flags);
+ } else {
+ hr = DwmSetIconicThumbnail(mPreview->PreviewWindow(), hBitmap, flags);
+ }
+ MOZ_ASSERT(SUCCEEDED(hr));
+ mozilla::Unused << hr;
+ return NS_OK;
+}
+
+/* static */
+bool TaskbarPreview::MainWindowHook(void* aContext, HWND hWnd, UINT nMsg,
+ WPARAM wParam, LPARAM lParam,
+ LRESULT* aResult) {
+ NS_ASSERTION(nMsg == nsAppShell::GetTaskbarButtonCreatedMessage() ||
+ nMsg == WM_DESTROY,
+ "Window hook proc called with wrong message");
+ NS_ASSERTION(aContext, "Null context in MainWindowHook");
+ if (!aContext) return false;
+ TaskbarPreview* preview = reinterpret_cast<TaskbarPreview*>(aContext);
+ if (nMsg == WM_DESTROY) {
+ // nsWindow is being destroyed
+ // We can't really do anything at this point including removing hooks
+ return false;
+ } else {
+ nsWindow* window = WinUtils::GetNSWindowPtr(preview->mWnd);
+ if (window) {
+ window->SetHasTaskbarIconBeenCreated();
+
+ if (preview->mVisible) preview->UpdateTaskbarProperties();
+ }
+ }
+ return false;
+}
+
+TaskbarPreview* TaskbarPreview::sActivePreview = nullptr;
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/TaskbarPreview.h b/widget/windows/TaskbarPreview.h
new file mode 100644
index 0000000000..ca21e3eeb8
--- /dev/null
+++ b/widget/windows/TaskbarPreview.h
@@ -0,0 +1,132 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_TaskbarPreview_h__
+#define __mozilla_widget_TaskbarPreview_h__
+
+#include <windows.h>
+#include <shobjidl.h>
+#undef LogSeverity // SetupAPI.h #defines this as DWORD
+
+#include "mozilla/RefPtr.h"
+#include <nsITaskbarPreview.h>
+#include <nsITaskbarPreviewController.h>
+#include <nsString.h>
+#include <nsIWeakReferenceUtils.h>
+#include <nsIDocShell.h>
+#include "WindowHook.h"
+
+namespace mozilla {
+namespace widget {
+
+class TaskbarPreviewCallback;
+
+class TaskbarPreview : public nsITaskbarPreview {
+ public:
+ TaskbarPreview(ITaskbarList4* aTaskbar,
+ nsITaskbarPreviewController* aController, HWND aHWND,
+ nsIDocShell* aShell);
+ virtual nsresult Init();
+
+ friend class TaskbarPreviewCallback;
+
+ NS_DECL_NSITASKBARPREVIEW
+
+ protected:
+ virtual ~TaskbarPreview();
+
+ // Called to update ITaskbarList4 dependent properties
+ virtual nsresult UpdateTaskbarProperties();
+
+ // Invoked when the preview is made visible
+ virtual nsresult Enable();
+ // Invoked when the preview is made invisible
+ virtual nsresult Disable();
+
+ // Detaches this preview from the nsWindow instance it's tied to
+ virtual void DetachFromNSWindow();
+
+ // Determines if the window is available and a destroy has not yet started
+ bool IsWindowAvailable() const;
+
+ // Marks this preview as being active
+ virtual nsresult ShowActive(bool active) = 0;
+ // Gets a reference to the window used to handle the preview messages
+ virtual HWND& PreviewWindow() = 0;
+
+ // Window procedure for the PreviewWindow (hooked for window previews)
+ virtual LRESULT WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam);
+
+ // Returns whether or not the taskbar icon has been created for mWnd The
+ // ITaskbarList4 API requires that we wait until the icon has been created
+ // before we can call its methods.
+ bool CanMakeTaskbarCalls();
+
+ // Gets the WindowHook for the nsWindow
+ WindowHook* GetWindowHook();
+
+ // Enables/disables custom drawing for the given window
+ static void EnableCustomDrawing(HWND aHWND, bool aEnable);
+
+ // MSCOM Taskbar interface
+ RefPtr<ITaskbarList4> mTaskbar;
+ // Controller for this preview
+ nsCOMPtr<nsITaskbarPreviewController> mController;
+ // The HWND to the nsWindow that this object previews
+ HWND mWnd;
+ // Whether or not this preview is visible
+ bool mVisible;
+
+ private:
+ // Called when the tooltip should be updated
+ nsresult UpdateTooltip();
+
+ // Requests the controller to draw into a canvas of the given width and
+ // height. The resulting bitmap is sent to the DWM to display.
+ void DrawBitmap(uint32_t width, uint32_t height, bool isPreview);
+
+ // WindowHook procedure for hooking mWnd
+ static bool MainWindowHook(void* aContext, HWND hWnd, UINT nMsg,
+ WPARAM wParam, LPARAM lParam, LRESULT* aResult);
+
+ // Docshell corresponding to the <window> the nsWindow contains
+ nsWeakPtr mDocShell;
+ nsString mTooltip;
+
+ // The preview currently marked as active in the taskbar. nullptr if no
+ // preview is active (some other window is).
+ static TaskbarPreview* sActivePreview;
+};
+
+/*
+ * Callback object TaskbarPreview hands to preview controllers when we
+ * request async thumbnail or live preview images. Controllers invoke
+ * this interface once they have aquired the requested image.
+ */
+class TaskbarPreviewCallback : public nsITaskbarPreviewCallback {
+ public:
+ TaskbarPreviewCallback() : mIsThumbnail(true) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITASKBARPREVIEWCALLBACK
+
+ void SetPreview(TaskbarPreview* aPreview) { mPreview = aPreview; }
+
+ void SetIsPreview() { mIsThumbnail = false; }
+
+ protected:
+ virtual ~TaskbarPreviewCallback() {}
+
+ private:
+ RefPtr<TaskbarPreview> mPreview;
+ bool mIsThumbnail;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_TaskbarPreview_h__ */
diff --git a/widget/windows/TaskbarPreviewButton.cpp b/widget/windows/TaskbarPreviewButton.cpp
new file mode 100644
index 0000000000..abbb1069a9
--- /dev/null
+++ b/widget/windows/TaskbarPreviewButton.cpp
@@ -0,0 +1,137 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 <windows.h>
+#include <strsafe.h>
+
+#include "TaskbarWindowPreview.h"
+#include "TaskbarPreviewButton.h"
+#include "nsWindowGfx.h"
+#include <imgIContainer.h>
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(TaskbarPreviewButton, nsITaskbarPreviewButton,
+ nsISupportsWeakReference)
+
+TaskbarPreviewButton::TaskbarPreviewButton(TaskbarWindowPreview* preview,
+ uint32_t index)
+ : mPreview(preview), mIndex(index) {}
+
+TaskbarPreviewButton::~TaskbarPreviewButton() { SetVisible(false); }
+
+NS_IMETHODIMP
+TaskbarPreviewButton::GetTooltip(nsAString& aTooltip) {
+ aTooltip = mTooltip;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::SetTooltip(const nsAString& aTooltip) {
+ mTooltip = aTooltip;
+ size_t destLength = sizeof Button().szTip / (sizeof Button().szTip[0]);
+ wchar_t* tooltip = &(Button().szTip[0]);
+ StringCchCopyNW(tooltip, destLength, mTooltip.get(), mTooltip.Length());
+ return Update();
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::GetDismissOnClick(bool* dismiss) {
+ *dismiss = (Button().dwFlags & THBF_DISMISSONCLICK) == THBF_DISMISSONCLICK;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::SetDismissOnClick(bool dismiss) {
+ if (dismiss)
+ Button().dwFlags |= THBF_DISMISSONCLICK;
+ else
+ Button().dwFlags &= ~THBF_DISMISSONCLICK;
+ return Update();
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::GetHasBorder(bool* hasBorder) {
+ *hasBorder = (Button().dwFlags & THBF_NOBACKGROUND) != THBF_NOBACKGROUND;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::SetHasBorder(bool hasBorder) {
+ if (hasBorder)
+ Button().dwFlags &= ~THBF_NOBACKGROUND;
+ else
+ Button().dwFlags |= THBF_NOBACKGROUND;
+ return Update();
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::GetDisabled(bool* disabled) {
+ *disabled = (Button().dwFlags & THBF_DISABLED) == THBF_DISABLED;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::SetDisabled(bool disabled) {
+ if (disabled)
+ Button().dwFlags |= THBF_DISABLED;
+ else
+ Button().dwFlags &= ~THBF_DISABLED;
+ return Update();
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::GetImage(imgIContainer** img) {
+ if (mImage)
+ NS_ADDREF(*img = mImage);
+ else
+ *img = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::SetImage(imgIContainer* img) {
+ if (Button().hIcon) ::DestroyIcon(Button().hIcon);
+ if (img) {
+ nsresult rv;
+ rv = nsWindowGfx::CreateIcon(
+ img, false, LayoutDeviceIntPoint(),
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon),
+ &Button().hIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ Button().hIcon = nullptr;
+ }
+ return Update();
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::GetVisible(bool* visible) {
+ *visible = (Button().dwFlags & THBF_HIDDEN) != THBF_HIDDEN;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarPreviewButton::SetVisible(bool visible) {
+ if (visible)
+ Button().dwFlags &= ~THBF_HIDDEN;
+ else
+ Button().dwFlags |= THBF_HIDDEN;
+ return Update();
+}
+
+THUMBBUTTON& TaskbarPreviewButton::Button() {
+ return mPreview->mThumbButtons[mIndex];
+}
+
+nsresult TaskbarPreviewButton::Update() {
+ return mPreview->UpdateButton(mIndex);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/TaskbarPreviewButton.h b/widget/windows/TaskbarPreviewButton.h
new file mode 100644
index 0000000000..e13b2d6771
--- /dev/null
+++ b/widget/windows/TaskbarPreviewButton.h
@@ -0,0 +1,47 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_TaskbarPreviewButton_h__
+#define __mozilla_widget_TaskbarPreviewButton_h__
+
+#include <windows.h>
+#include <shobjidl.h>
+#undef LogSeverity // SetupAPI.h #defines this as DWORD
+
+#include "mozilla/RefPtr.h"
+#include <nsITaskbarPreviewButton.h>
+#include <nsString.h>
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace widget {
+
+class TaskbarWindowPreview;
+class TaskbarPreviewButton : public nsITaskbarPreviewButton,
+ public nsSupportsWeakReference {
+ virtual ~TaskbarPreviewButton();
+
+ public:
+ TaskbarPreviewButton(TaskbarWindowPreview* preview, uint32_t index);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITASKBARPREVIEWBUTTON
+
+ private:
+ THUMBBUTTON& Button();
+ nsresult Update();
+
+ RefPtr<TaskbarWindowPreview> mPreview;
+ uint32_t mIndex;
+ nsString mTooltip;
+ nsCOMPtr<imgIContainer> mImage;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_TaskbarPreviewButton_h__ */
diff --git a/widget/windows/TaskbarTabPreview.cpp b/widget/windows/TaskbarTabPreview.cpp
new file mode 100644
index 0000000000..3421e68ffb
--- /dev/null
+++ b/widget/windows/TaskbarTabPreview.cpp
@@ -0,0 +1,344 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "TaskbarTabPreview.h"
+#include "nsWindowGfx.h"
+#include "nsUXThemeData.h"
+#include "WinUtils.h"
+#include <nsITaskbarPreviewController.h>
+
+#define TASKBARPREVIEW_HWNDID L"TaskbarTabPreviewHwnd"
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(TaskbarTabPreview, nsITaskbarTabPreview)
+
+const wchar_t* const kWindowClass = L"MozillaTaskbarPreviewClass";
+
+TaskbarTabPreview::TaskbarTabPreview(ITaskbarList4* aTaskbar,
+ nsITaskbarPreviewController* aController,
+ HWND aHWND, nsIDocShell* aShell)
+ : TaskbarPreview(aTaskbar, aController, aHWND, aShell),
+ mProxyWindow(nullptr),
+ mIcon(nullptr),
+ mRegistered(false) {}
+
+TaskbarTabPreview::~TaskbarTabPreview() {
+ if (mIcon) {
+ ::DestroyIcon(mIcon);
+ mIcon = nullptr;
+ }
+
+ // We need to ensure that proxy window disappears or else Bad Things happen.
+ if (mProxyWindow) Disable();
+
+ NS_ASSERTION(!mProxyWindow, "Taskbar proxy window was not destroyed!");
+
+ if (IsWindowAvailable()) {
+ DetachFromNSWindow();
+ } else {
+ mWnd = nullptr;
+ }
+}
+
+nsresult TaskbarTabPreview::Init() {
+ nsresult rv = TaskbarPreview::Init();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ WindowHook* hook = GetWindowHook();
+ if (!hook) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return hook->AddMonitor(WM_WINDOWPOSCHANGED, MainWindowHook, this);
+}
+
+nsresult TaskbarTabPreview::ShowActive(bool active) {
+ NS_ASSERTION(mVisible && CanMakeTaskbarCalls(),
+ "ShowActive called on invisible window or before taskbar calls "
+ "can be made for this window");
+ return FAILED(
+ mTaskbar->SetTabActive(active ? mProxyWindow : nullptr, mWnd, 0))
+ ? NS_ERROR_FAILURE
+ : NS_OK;
+}
+
+HWND& TaskbarTabPreview::PreviewWindow() { return mProxyWindow; }
+
+nativeWindow TaskbarTabPreview::GetHWND() { return mProxyWindow; }
+
+void TaskbarTabPreview::EnsureRegistration() {
+ NS_ASSERTION(mVisible && CanMakeTaskbarCalls(),
+ "EnsureRegistration called when it is not safe to do so");
+
+ (void)UpdateTaskbarProperties();
+}
+
+NS_IMETHODIMP
+TaskbarTabPreview::GetTitle(nsAString& aTitle) {
+ aTitle = mTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarTabPreview::SetTitle(const nsAString& aTitle) {
+ mTitle = aTitle;
+ return mVisible ? UpdateTitle() : NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarTabPreview::SetIcon(imgIContainer* icon) {
+ HICON hIcon = nullptr;
+ if (icon) {
+ nsresult rv;
+ rv = nsWindowGfx::CreateIcon(
+ icon, false, LayoutDeviceIntPoint(),
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon), &hIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mIcon) ::DestroyIcon(mIcon);
+ mIcon = hIcon;
+ mIconImage = icon;
+ return mVisible ? UpdateIcon() : NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarTabPreview::GetIcon(imgIContainer** icon) {
+ NS_IF_ADDREF(*icon = mIconImage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarTabPreview::Move(nsITaskbarTabPreview* aNext) {
+ if (aNext == this) return NS_ERROR_INVALID_ARG;
+ mNext = aNext;
+ return CanMakeTaskbarCalls() ? UpdateNext() : NS_OK;
+}
+
+nsresult TaskbarTabPreview::UpdateTaskbarProperties() {
+ if (mRegistered) return NS_OK;
+
+ if (FAILED(mTaskbar->RegisterTab(mProxyWindow, mWnd)))
+ return NS_ERROR_FAILURE;
+
+ nsresult rv = UpdateNext();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = TaskbarPreview::UpdateTaskbarProperties();
+ mRegistered = true;
+ return rv;
+}
+
+LRESULT
+TaskbarTabPreview::WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) {
+ RefPtr<TaskbarTabPreview> kungFuDeathGrip(this);
+ switch (nMsg) {
+ case WM_CREATE:
+ TaskbarPreview::EnableCustomDrawing(mProxyWindow, true);
+ return 0;
+ case WM_CLOSE:
+ mController->OnClose();
+ return 0;
+ case WM_ACTIVATE:
+ if (LOWORD(wParam) == WA_ACTIVE) {
+ // Activate the tab the user selected then restore the main window,
+ // keeping normal/max window state intact.
+ bool activateWindow;
+ nsresult rv = mController->OnActivate(&activateWindow);
+ if (NS_SUCCEEDED(rv) && activateWindow) {
+ nsWindow* win = WinUtils::GetNSWindowPtr(mWnd);
+ if (win) {
+ nsWindow* parent = win->GetTopLevelWindow(true);
+ if (parent) {
+ parent->Show(true);
+ }
+ }
+ }
+ }
+ return 0;
+ case WM_GETICON:
+ return (LRESULT)mIcon;
+ case WM_SYSCOMMAND:
+ // Send activation events to the top level window and select the proper
+ // tab through the controller.
+ if (wParam == SC_RESTORE || wParam == SC_MAXIMIZE) {
+ bool activateWindow;
+ nsresult rv = mController->OnActivate(&activateWindow);
+ if (NS_SUCCEEDED(rv) && activateWindow) {
+ // Note, restoring an iconic, maximized window here will only
+ // activate the maximized window. This is not a bug, it's default
+ // windows behavior.
+ ::SendMessageW(mWnd, WM_SYSCOMMAND, wParam, lParam);
+ }
+ return 0;
+ }
+ // Forward everything else to the top level window. Do not forward
+ // close since that's intended for the tab. When the preview proxy
+ // closes, we'll close the tab above.
+ return wParam == SC_CLOSE
+ ? ::DefWindowProcW(mProxyWindow, WM_SYSCOMMAND, wParam, lParam)
+ : ::SendMessageW(mWnd, WM_SYSCOMMAND, wParam, lParam);
+ }
+ return TaskbarPreview::WndProc(nMsg, wParam, lParam);
+}
+
+/* static */
+LRESULT CALLBACK TaskbarTabPreview::GlobalWndProc(HWND hWnd, UINT nMsg,
+ WPARAM wParam,
+ LPARAM lParam) {
+ TaskbarTabPreview* preview(nullptr);
+ if (nMsg == WM_CREATE) {
+ CREATESTRUCT* cs = reinterpret_cast<CREATESTRUCT*>(lParam);
+ preview = reinterpret_cast<TaskbarTabPreview*>(cs->lpCreateParams);
+ if (!::SetPropW(hWnd, TASKBARPREVIEW_HWNDID, preview))
+ NS_ERROR("Could not associate native window with tab preview");
+ preview->mProxyWindow = hWnd;
+ } else {
+ preview = reinterpret_cast<TaskbarTabPreview*>(
+ ::GetPropW(hWnd, TASKBARPREVIEW_HWNDID));
+ if (nMsg == WM_DESTROY) ::RemovePropW(hWnd, TASKBARPREVIEW_HWNDID);
+ }
+
+ if (preview) return preview->WndProc(nMsg, wParam, lParam);
+ return ::DefWindowProcW(hWnd, nMsg, wParam, lParam);
+}
+
+nsresult TaskbarTabPreview::Enable() {
+ WNDCLASSW wc;
+ HINSTANCE module = GetModuleHandle(nullptr);
+
+ if (!GetClassInfoW(module, kWindowClass, &wc)) {
+ wc.style = 0;
+ wc.lpfnWndProc = GlobalWndProc;
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hInstance = module;
+ wc.hIcon = nullptr;
+ wc.hCursor = nullptr;
+ wc.hbrBackground = (HBRUSH) nullptr;
+ wc.lpszMenuName = (LPCWSTR) nullptr;
+ wc.lpszClassName = kWindowClass;
+ RegisterClassW(&wc);
+ }
+ ::CreateWindowW(kWindowClass, L"TaskbarPreviewWindow",
+ WS_CAPTION | WS_SYSMENU, 0, 0, 200, 60, nullptr, nullptr,
+ module, this);
+ // GlobalWndProc will set mProxyWindow so that WM_CREATE can have a valid HWND
+ if (!mProxyWindow) return NS_ERROR_INVALID_ARG;
+
+ UpdateProxyWindowStyle();
+
+ nsresult rv = TaskbarPreview::Enable();
+ nsresult rvUpdate;
+ rvUpdate = UpdateTitle();
+ if (NS_FAILED(rvUpdate)) rv = rvUpdate;
+
+ rvUpdate = UpdateIcon();
+ if (NS_FAILED(rvUpdate)) rv = rvUpdate;
+
+ return rv;
+}
+
+nsresult TaskbarTabPreview::Disable() {
+ // TaskbarPreview::Disable assumes that mWnd is valid but this method can be
+ // called when it is null iff the nsWindow has already been destroyed and we
+ // are still visible for some reason during object destruction.
+ if (mWnd) TaskbarPreview::Disable();
+
+ if (FAILED(mTaskbar->UnregisterTab(mProxyWindow))) return NS_ERROR_FAILURE;
+ mRegistered = false;
+
+ // TaskbarPreview::WndProc will set mProxyWindow to null
+ if (!DestroyWindow(mProxyWindow)) return NS_ERROR_FAILURE;
+ mProxyWindow = nullptr;
+ return NS_OK;
+}
+
+void TaskbarTabPreview::DetachFromNSWindow() {
+ (void)SetVisible(false);
+ if (WindowHook* hook = GetWindowHook()) {
+ hook->RemoveMonitor(WM_WINDOWPOSCHANGED, MainWindowHook, this);
+ }
+ TaskbarPreview::DetachFromNSWindow();
+}
+
+/* static */
+bool TaskbarTabPreview::MainWindowHook(void* aContext, HWND hWnd, UINT nMsg,
+ WPARAM wParam, LPARAM lParam,
+ LRESULT* aResult) {
+ if (nMsg == WM_WINDOWPOSCHANGED) {
+ TaskbarTabPreview* preview = reinterpret_cast<TaskbarTabPreview*>(aContext);
+ WINDOWPOS* pos = reinterpret_cast<WINDOWPOS*>(lParam);
+ if (SWP_FRAMECHANGED == (pos->flags & SWP_FRAMECHANGED))
+ preview->UpdateProxyWindowStyle();
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "Style changed hook fired on non-style changed "
+ "message");
+ }
+ return false;
+}
+
+void TaskbarTabPreview::UpdateProxyWindowStyle() {
+ if (!mProxyWindow) return;
+
+ DWORD minMaxMask = WS_MINIMIZE | WS_MAXIMIZE;
+ DWORD windowStyle = GetWindowLongW(mWnd, GWL_STYLE);
+
+ DWORD proxyStyle = GetWindowLongW(mProxyWindow, GWL_STYLE);
+ proxyStyle &= ~minMaxMask;
+ proxyStyle |= windowStyle & minMaxMask;
+ SetWindowLongW(mProxyWindow, GWL_STYLE, proxyStyle);
+
+ DWORD exStyle =
+ (WS_MAXIMIZE == (windowStyle & WS_MAXIMIZE)) ? WS_EX_TOOLWINDOW : 0;
+ SetWindowLongW(mProxyWindow, GWL_EXSTYLE, exStyle);
+}
+
+nsresult TaskbarTabPreview::UpdateTitle() {
+ NS_ASSERTION(mVisible, "UpdateTitle called on invisible preview");
+
+ if (!::SetWindowTextW(mProxyWindow, mTitle.get())) return NS_ERROR_FAILURE;
+ return NS_OK;
+}
+
+nsresult TaskbarTabPreview::UpdateIcon() {
+ NS_ASSERTION(mVisible, "UpdateIcon called on invisible preview");
+
+ ::SendMessageW(mProxyWindow, WM_SETICON, ICON_SMALL, (LPARAM)mIcon);
+
+ return NS_OK;
+}
+
+nsresult TaskbarTabPreview::UpdateNext() {
+ NS_ASSERTION(CanMakeTaskbarCalls() && mVisible,
+ "UpdateNext called on invisible tab preview");
+ HWND hNext = nullptr;
+ if (mNext) {
+ bool visible;
+ nsresult rv = mNext->GetVisible(&visible);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Can only move next to enabled previews
+ if (!visible) return NS_ERROR_FAILURE;
+
+ hNext = (HWND)mNext->GetHWND();
+
+ // hNext must be registered with the taskbar if the call is to succeed
+ mNext->EnsureRegistration();
+ }
+ if (FAILED(mTaskbar->SetTabOrder(mProxyWindow, hNext)))
+ return NS_ERROR_FAILURE;
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/TaskbarTabPreview.h b/widget/windows/TaskbarTabPreview.h
new file mode 100644
index 0000000000..9a15760557
--- /dev/null
+++ b/widget/windows/TaskbarTabPreview.h
@@ -0,0 +1,70 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_TaskbarTabPreview_h__
+#define __mozilla_widget_TaskbarTabPreview_h__
+
+#include "nsITaskbarTabPreview.h"
+#include "TaskbarPreview.h"
+
+namespace mozilla {
+namespace widget {
+
+class TaskbarTabPreview : public nsITaskbarTabPreview, public TaskbarPreview {
+ virtual ~TaskbarTabPreview();
+
+ public:
+ TaskbarTabPreview(ITaskbarList4* aTaskbar,
+ nsITaskbarPreviewController* aController, HWND aHWND,
+ nsIDocShell* aShell);
+ virtual nsresult Init() override;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITASKBARTABPREVIEW
+ NS_FORWARD_NSITASKBARPREVIEW(TaskbarPreview::)
+
+ private:
+ virtual nsresult ShowActive(bool active);
+ virtual HWND& PreviewWindow();
+ virtual LRESULT WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam);
+ static LRESULT CALLBACK GlobalWndProc(HWND hWnd, UINT nMsg, WPARAM wParam,
+ LPARAM lParam);
+
+ virtual nsresult UpdateTaskbarProperties();
+ virtual nsresult Enable();
+ virtual nsresult Disable();
+ virtual void DetachFromNSWindow();
+
+ // WindowHook procedure for hooking mWnd
+ static bool MainWindowHook(void* aContext, HWND hWnd, UINT nMsg,
+ WPARAM wParam, LPARAM lParam, LRESULT* aResult);
+
+ // Bug 520807 - we need to update the proxy window style based on the main
+ // window's style to workaround a bug with the way the DWM displays the
+ // previews.
+ void UpdateProxyWindowStyle();
+
+ nsresult UpdateTitle();
+ nsresult UpdateIcon();
+ nsresult UpdateNext();
+
+ // Handle to the toplevel proxy window
+ HWND mProxyWindow;
+ nsString mTitle;
+ nsCOMPtr<imgIContainer> mIconImage;
+ // Cached Windows icon of mIconImage
+ HICON mIcon;
+ // Preview that follows this preview in the taskbar (left-to-right order)
+ nsCOMPtr<nsITaskbarTabPreview> mNext;
+ // True if this preview has been registered with the taskbar
+ bool mRegistered;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_TaskbarTabPreview_h__ */
diff --git a/widget/windows/TaskbarWindowPreview.cpp b/widget/windows/TaskbarWindowPreview.cpp
new file mode 100644
index 0000000000..212278be4a
--- /dev/null
+++ b/widget/windows/TaskbarWindowPreview.cpp
@@ -0,0 +1,326 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "mozilla/ArrayUtils.h"
+
+#include <nsITaskbarPreviewController.h>
+#include "TaskbarWindowPreview.h"
+#include "WindowHook.h"
+#include "nsUXThemeData.h"
+#include "TaskbarPreviewButton.h"
+#include "nsWindow.h"
+#include "nsWindowGfx.h"
+
+namespace mozilla {
+namespace widget {
+
+namespace {
+bool WindowHookProc(void* aContext, HWND hWnd, UINT nMsg, WPARAM wParam,
+ LPARAM lParam, LRESULT* aResult) {
+ TaskbarWindowPreview* preview =
+ reinterpret_cast<TaskbarWindowPreview*>(aContext);
+ *aResult = preview->WndProc(nMsg, wParam, lParam);
+ return true;
+}
+} // namespace
+
+NS_IMPL_ISUPPORTS(TaskbarWindowPreview, nsITaskbarWindowPreview,
+ nsITaskbarProgress, nsITaskbarOverlayIconController,
+ nsISupportsWeakReference)
+
+/**
+ * These correspond directly to the states defined in nsITaskbarProgress.idl, so
+ * they should be kept in sync.
+ */
+static TBPFLAG sNativeStates[] = {TBPF_NOPROGRESS, TBPF_INDETERMINATE,
+ TBPF_NORMAL, TBPF_ERROR, TBPF_PAUSED};
+
+TaskbarWindowPreview::TaskbarWindowPreview(
+ ITaskbarList4* aTaskbar, nsITaskbarPreviewController* aController,
+ HWND aHWND, nsIDocShell* aShell)
+ : TaskbarPreview(aTaskbar, aController, aHWND, aShell),
+ mCustomDrawing(false),
+ mHaveButtons(false),
+ mState(TBPF_NOPROGRESS),
+ mCurrentValue(0),
+ mMaxValue(0),
+ mOverlayIcon(nullptr) {
+ // Window previews are visible by default
+ (void)SetVisible(true);
+
+ memset(mThumbButtons, 0, sizeof mThumbButtons);
+ for (int32_t i = 0; i < nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS; i++) {
+ mThumbButtons[i].dwMask = THB_FLAGS | THB_ICON | THB_TOOLTIP;
+ mThumbButtons[i].iId = i;
+ mThumbButtons[i].dwFlags = THBF_HIDDEN;
+ }
+}
+
+TaskbarWindowPreview::~TaskbarWindowPreview() {
+ if (mOverlayIcon) {
+ ::DestroyIcon(mOverlayIcon);
+ mOverlayIcon = nullptr;
+ }
+
+ // We need to clean up a hook associated with the "this" pointer.
+ SetVisible(false);
+
+ if (IsWindowAvailable()) {
+ DetachFromNSWindow();
+ } else {
+ mWnd = nullptr;
+ }
+}
+
+nsresult TaskbarWindowPreview::Init() {
+ nsresult rv = TaskbarPreview::Init();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (CanMakeTaskbarCalls()) {
+ return NS_OK;
+ }
+
+ WindowHook* hook = GetWindowHook();
+ if (!hook) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return hook->AddMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(),
+ TaskbarWindowHook, this);
+}
+
+nsresult TaskbarWindowPreview::ShowActive(bool active) {
+ return FAILED(mTaskbar->ActivateTab(active ? mWnd : nullptr))
+ ? NS_ERROR_FAILURE
+ : NS_OK;
+}
+
+HWND& TaskbarWindowPreview::PreviewWindow() { return mWnd; }
+
+nsresult TaskbarWindowPreview::GetButton(uint32_t index,
+ nsITaskbarPreviewButton** _retVal) {
+ if (index >= nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS)
+ return NS_ERROR_INVALID_ARG;
+
+ nsCOMPtr<nsITaskbarPreviewButton> button(
+ do_QueryReferent(mWeakButtons[index]));
+
+ if (!button) {
+ // Lost reference
+ button = new TaskbarPreviewButton(this, index);
+ if (!button) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ mWeakButtons[index] = do_GetWeakReference(button);
+ }
+
+ if (!mHaveButtons) {
+ mHaveButtons = true;
+
+ WindowHook* hook = GetWindowHook();
+ if (!hook) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ (void)hook->AddHook(WM_COMMAND, WindowHookProc, this);
+
+ if (mVisible && FAILED(mTaskbar->ThumbBarAddButtons(
+ mWnd, nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS,
+ mThumbButtons))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ button.forget(_retVal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarWindowPreview::SetEnableCustomDrawing(bool aEnable) {
+ if (aEnable == mCustomDrawing) return NS_OK;
+
+ WindowHook* hook = GetWindowHook();
+ if (!hook) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mCustomDrawing = aEnable;
+ TaskbarPreview::EnableCustomDrawing(mWnd, aEnable);
+
+ if (aEnable) {
+ (void)hook->AddHook(WM_DWMSENDICONICTHUMBNAIL, WindowHookProc, this);
+ (void)hook->AddHook(WM_DWMSENDICONICLIVEPREVIEWBITMAP, WindowHookProc,
+ this);
+ } else {
+ (void)hook->RemoveHook(WM_DWMSENDICONICLIVEPREVIEWBITMAP, WindowHookProc,
+ this);
+ (void)hook->RemoveHook(WM_DWMSENDICONICTHUMBNAIL, WindowHookProc, this);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarWindowPreview::GetEnableCustomDrawing(bool* aEnable) {
+ *aEnable = mCustomDrawing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarWindowPreview::SetProgressState(nsTaskbarProgressState aState,
+ uint64_t aCurrentValue,
+ uint64_t aMaxValue) {
+ NS_ENSURE_ARG_RANGE(aState, nsTaskbarProgressState(0),
+ nsTaskbarProgressState(ArrayLength(sNativeStates) - 1));
+
+ TBPFLAG nativeState = sNativeStates[aState];
+ if (nativeState == TBPF_NOPROGRESS || nativeState == TBPF_INDETERMINATE) {
+ NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG);
+ }
+
+ if (aCurrentValue > aMaxValue) return NS_ERROR_ILLEGAL_VALUE;
+
+ mState = nativeState;
+ mCurrentValue = aCurrentValue;
+ mMaxValue = aMaxValue;
+
+ // Only update if we can
+ return CanMakeTaskbarCalls() ? UpdateTaskbarProgress() : NS_OK;
+}
+
+NS_IMETHODIMP
+TaskbarWindowPreview::SetOverlayIcon(imgIContainer* aStatusIcon,
+ const nsAString& aStatusDescription) {
+ nsresult rv;
+ if (aStatusIcon) {
+ // The image shouldn't be animated
+ bool isAnimated;
+ rv = aStatusIcon->GetAnimated(&isAnimated);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_FALSE(isAnimated, NS_ERROR_INVALID_ARG);
+ }
+
+ HICON hIcon = nullptr;
+ if (aStatusIcon) {
+ rv = nsWindowGfx::CreateIcon(
+ aStatusIcon, false, LayoutDeviceIntPoint(),
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon), &hIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mOverlayIcon) ::DestroyIcon(mOverlayIcon);
+ mOverlayIcon = hIcon;
+ mIconDescription = aStatusDescription;
+
+ // Only update if we can
+ return CanMakeTaskbarCalls() ? UpdateOverlayIcon() : NS_OK;
+}
+
+nsresult TaskbarWindowPreview::UpdateTaskbarProperties() {
+ if (mHaveButtons) {
+ if (FAILED(mTaskbar->ThumbBarAddButtons(
+ mWnd, nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS, mThumbButtons)))
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = UpdateTaskbarProgress();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = UpdateOverlayIcon();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return TaskbarPreview::UpdateTaskbarProperties();
+}
+
+nsresult TaskbarWindowPreview::UpdateTaskbarProgress() {
+ HRESULT hr = mTaskbar->SetProgressState(mWnd, mState);
+ if (SUCCEEDED(hr) && mState != TBPF_NOPROGRESS &&
+ mState != TBPF_INDETERMINATE)
+ hr = mTaskbar->SetProgressValue(mWnd, mCurrentValue, mMaxValue);
+
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult TaskbarWindowPreview::UpdateOverlayIcon() {
+ HRESULT hr =
+ mTaskbar->SetOverlayIcon(mWnd, mOverlayIcon, mIconDescription.get());
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+LRESULT
+TaskbarWindowPreview::WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) {
+ RefPtr<TaskbarWindowPreview> kungFuDeathGrip(this);
+ switch (nMsg) {
+ case WM_COMMAND: {
+ uint32_t id = LOWORD(wParam);
+ uint32_t index = id;
+ nsCOMPtr<nsITaskbarPreviewButton> button;
+ nsresult rv = GetButton(index, getter_AddRefs(button));
+ if (NS_SUCCEEDED(rv)) mController->OnClick(button);
+ }
+ return 0;
+ }
+ return TaskbarPreview::WndProc(nMsg, wParam, lParam);
+}
+
+/* static */
+bool TaskbarWindowPreview::TaskbarWindowHook(void* aContext, HWND hWnd,
+ UINT nMsg, WPARAM wParam,
+ LPARAM lParam, LRESULT* aResult) {
+ NS_ASSERTION(nMsg == nsAppShell::GetTaskbarButtonCreatedMessage(),
+ "Window hook proc called with wrong message");
+ TaskbarWindowPreview* preview =
+ reinterpret_cast<TaskbarWindowPreview*>(aContext);
+ // Now we can make all the calls to mTaskbar
+ preview->UpdateTaskbarProperties();
+ return false;
+}
+
+nsresult TaskbarWindowPreview::Enable() {
+ nsresult rv = TaskbarPreview::Enable();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return FAILED(mTaskbar->AddTab(mWnd)) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+nsresult TaskbarWindowPreview::Disable() {
+ nsresult rv = TaskbarPreview::Disable();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return FAILED(mTaskbar->DeleteTab(mWnd)) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+void TaskbarWindowPreview::DetachFromNSWindow() {
+ // Remove the hooks we have for drawing
+ SetEnableCustomDrawing(false);
+
+ if (WindowHook* hook = GetWindowHook()) {
+ (void)hook->RemoveHook(WM_COMMAND, WindowHookProc, this);
+ (void)hook->RemoveMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(),
+ TaskbarWindowHook, this);
+ }
+ TaskbarPreview::DetachFromNSWindow();
+}
+
+nsresult TaskbarWindowPreview::UpdateButtons() {
+ NS_ASSERTION(mVisible, "UpdateButtons called on invisible preview");
+
+ if (FAILED(mTaskbar->ThumbBarUpdateButtons(
+ mWnd, nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS, mThumbButtons)))
+ return NS_ERROR_FAILURE;
+ return NS_OK;
+}
+
+nsresult TaskbarWindowPreview::UpdateButton(uint32_t index) {
+ if (index >= nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS)
+ return NS_ERROR_INVALID_ARG;
+ if (mVisible) {
+ if (FAILED(mTaskbar->ThumbBarUpdateButtons(mWnd, 1, &mThumbButtons[index])))
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/TaskbarWindowPreview.h b/widget/windows/TaskbarWindowPreview.h
new file mode 100644
index 0000000000..97c074197c
--- /dev/null
+++ b/widget/windows/TaskbarWindowPreview.h
@@ -0,0 +1,85 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __mozilla_widget_TaskbarWindowPreview_h__
+#define __mozilla_widget_TaskbarWindowPreview_h__
+
+#include "nsITaskbarWindowPreview.h"
+#include "nsITaskbarProgress.h"
+#include "nsITaskbarOverlayIconController.h"
+#include "TaskbarPreview.h"
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace widget {
+
+class TaskbarPreviewButton;
+class TaskbarWindowPreview : public TaskbarPreview,
+ public nsITaskbarWindowPreview,
+ public nsITaskbarProgress,
+ public nsITaskbarOverlayIconController,
+ public nsSupportsWeakReference {
+ virtual ~TaskbarWindowPreview();
+
+ public:
+ TaskbarWindowPreview(ITaskbarList4* aTaskbar,
+ nsITaskbarPreviewController* aController, HWND aHWND,
+ nsIDocShell* aShell);
+ virtual nsresult Init() override;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITASKBARWINDOWPREVIEW
+ NS_DECL_NSITASKBARPROGRESS
+ NS_DECL_NSITASKBAROVERLAYICONCONTROLLER
+ NS_FORWARD_NSITASKBARPREVIEW(TaskbarPreview::)
+
+ virtual LRESULT WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) override;
+
+ private:
+ virtual nsresult ShowActive(bool active) override;
+ virtual HWND& PreviewWindow() override;
+
+ virtual nsresult UpdateTaskbarProperties() override;
+ virtual nsresult Enable() override;
+ virtual nsresult Disable() override;
+ virtual void DetachFromNSWindow() override;
+ nsresult UpdateButton(uint32_t index);
+ nsresult UpdateButtons();
+
+ // Is custom drawing enabled?
+ bool mCustomDrawing;
+ // Have we made any buttons?
+ bool mHaveButtons;
+ // Windows button format
+ THUMBBUTTON mThumbButtons[nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS];
+ // Pointers to our button class (cached instances)
+ nsWeakPtr mWeakButtons[nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS];
+
+ // Called to update ITaskbarList4 dependent properties
+ nsresult UpdateTaskbarProgress();
+ nsresult UpdateOverlayIcon();
+
+ // The taskbar progress
+ TBPFLAG mState;
+ ULONGLONG mCurrentValue;
+ ULONGLONG mMaxValue;
+
+ // Taskbar overlay icon
+ HICON mOverlayIcon;
+ nsString mIconDescription;
+
+ // WindowHook procedure for hooking mWnd for taskbar progress and icon stuff
+ static bool TaskbarWindowHook(void* aContext, HWND hWnd, UINT nMsg,
+ WPARAM wParam, LPARAM lParam, LRESULT* aResult);
+
+ friend class TaskbarPreviewButton;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __mozilla_widget_TaskbarWindowPreview_h__ */
diff --git a/widget/windows/ToastNotification.cpp b/widget/windows/ToastNotification.cpp
new file mode 100644
index 0000000000..afdffc19ac
--- /dev/null
+++ b/widget/windows/ToastNotification.cpp
@@ -0,0 +1,915 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 "ToastNotification.h"
+
+#include <windows.h>
+#include <appmodel.h>
+#include <ktmw32.h>
+#include <windows.foundation.h>
+#include <wrl/client.h>
+
+#include "ErrorList.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Buffer.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/DynamicallyLinkedFunctionPtr.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/mscom/COMWrappers.h"
+#include "mozilla/mscom/Utils.h"
+#include "mozilla/widget/WinRegistry.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+#include "mozilla/WidgetUtils.h"
+#include "nsAppRunner.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIObserverService.h"
+#include "nsIWindowMediator.h"
+#include "nsPIDOMWindow.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsWindowsHelpers.h"
+#include "nsXREDirProvider.h"
+#include "prenv.h"
+#include "ToastNotificationHandler.h"
+#include "ToastNotificationHeaderOnlyUtils.h"
+
+namespace mozilla {
+namespace widget {
+
+using namespace toastnotification;
+
+using namespace ABI::Windows::Foundation;
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+// Needed to disambiguate internal and Windows `ToastNotification` classes.
+using namespace ABI::Windows::UI::Notifications;
+using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification;
+using IVectorView_ToastNotification =
+ ABI::Windows::Foundation::Collections::IVectorView<WinToastNotification*>;
+using IVectorView_ScheduledToastNotification =
+ ABI::Windows::Foundation::Collections::IVectorView<
+ ScheduledToastNotification*>;
+
+LazyLogModule sWASLog("WindowsAlertsService");
+
+NS_IMPL_ISUPPORTS(ToastNotification, nsIAlertsService, nsIWindowsAlertsService,
+ nsIAlertsDoNotDisturb, nsIObserver)
+
+ToastNotification::ToastNotification() = default;
+
+ToastNotification::~ToastNotification() = default;
+
+nsresult ToastNotification::Init() {
+ if (!PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) {
+ // Windows Toast Notification requires AppId. But allow `xpcshell` to
+ // create the service to test other functionality.
+ if (!EnsureAumidRegistered()) {
+ MOZ_LOG(sWASLog, LogLevel::Warning, ("Failed to register AUMID!"));
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Info, ("Using dummy AUMID in xpcshell test"));
+ mAumid.emplace(u"XpcshellTestToastAumid"_ns);
+ }
+
+ MOZ_LOG(sWASLog, LogLevel::Info,
+ ("Using AUMID: '%s'", NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ if (obsServ) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(obsServ->AddObserver(this, "last-pb-context-exited", false)));
+ Unused << NS_WARN_IF(
+ NS_FAILED(obsServ->AddObserver(this, "quit-application", false)));
+ }
+
+ return NS_OK;
+}
+
+bool ToastNotification::EnsureAumidRegistered() {
+ // Check if this is an MSIX install, app identity is provided by the package
+ // so no registration is necessary.
+ if (AssignIfMsixAumid(mAumid)) {
+ MOZ_LOG(
+ sWASLog, LogLevel::Info,
+ ("Found MSIX AUMID: '%s'", NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+ return true;
+ }
+
+ nsAutoString installHash;
+ nsresult rv = gDirServiceProvider->GetInstallHash(installHash);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Check if toasts were registered during NSIS/MSI installation.
+ if (AssignIfNsisAumid(installHash, mAumid)) {
+ MOZ_LOG(sWASLog, LogLevel::Info,
+ ("Found AUMID from installer (with install hash '%s'): '%s'",
+ NS_ConvertUTF16toUTF8(installHash).get(),
+ NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+ return true;
+ }
+
+ // No AUMID registered, fall through to runtime registration for development
+ // and portable builds.
+ if (RegisterRuntimeAumid(installHash, mAumid)) {
+ MOZ_LOG(
+ sWASLog, LogLevel::Info,
+ ("Updated AUMID registration at runtime (for install hash '%s'): '%s'",
+ NS_ConvertUTF16toUTF8(installHash).get(),
+ NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+ return true;
+ }
+
+ MOZ_LOG(sWASLog, LogLevel::Warning,
+ ("Failed to register AUMID at runtime! (for install hash '%s')",
+ NS_ConvertUTF16toUTF8(installHash).get()));
+ return false;
+}
+
+bool ToastNotification::AssignIfMsixAumid(Maybe<nsAutoString>& aAumid) {
+ UINT32 len = 0;
+ // ERROR_INSUFFICIENT_BUFFER signals that we're in an MSIX package, and
+ // therefore should use the package's AUMID.
+ if (GetCurrentApplicationUserModelId(&len, nullptr) !=
+ ERROR_INSUFFICIENT_BUFFER) {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Not an MSIX package"));
+ return false;
+ }
+ mozilla::Buffer<wchar_t> buffer(len);
+ LONG success = GetCurrentApplicationUserModelId(&len, buffer.Elements());
+ NS_ENSURE_TRUE(success == ERROR_SUCCESS, false);
+
+ aAumid.emplace(buffer.Elements());
+ return true;
+}
+
+bool ToastNotification::AssignIfNsisAumid(nsAutoString& aInstallHash,
+ Maybe<nsAutoString>& aAumid) {
+ nsAutoString nsisAumidName =
+ u""_ns MOZ_TOAST_APP_NAME u"Toast-"_ns + aInstallHash;
+ nsAutoString nsisAumidPath = u"AppUserModelId\\"_ns + nsisAumidName;
+ if (!WinRegistry::HasKey(HKEY_CLASSES_ROOT, nsisAumidPath)) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("No CustomActivator value from installer in key 'HKCR\\%s'",
+ NS_ConvertUTF16toUTF8(nsisAumidPath).get()));
+ return false;
+ }
+
+ aAumid.emplace(nsisAumidName);
+ return true;
+}
+
+bool ToastNotification::RegisterRuntimeAumid(nsAutoString& aInstallHash,
+ Maybe<nsAutoString>& aAumid) {
+ // Portable AUMID slightly differs from installed AUMID so we can
+ // differentiate installed to HKCU vs portable installs if necessary.
+ nsAutoString portableAumid =
+ u""_ns MOZ_TOAST_APP_NAME u"PortableToast-"_ns + aInstallHash;
+
+ nsCOMPtr<nsIFile> appdir;
+ nsresult rv = gDirServiceProvider->GetGREDir()->Clone(getter_AddRefs(appdir));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsIFile> icon;
+ rv = appdir->Clone(getter_AddRefs(icon));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = icon->Append(u"browser"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = icon->Append(u"VisualElements"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = icon->Append(u"VisualElements_70.png"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoString iconPath;
+ rv = icon->GetPath(iconPath);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsIFile> comDll;
+ rv = appdir->Clone(getter_AddRefs(comDll));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = comDll->Append(u"notificationserver.dll"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoString dllPath;
+ rv = comDll->GetPath(dllPath);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoHandle txn;
+ // Manipulate the registry using a transaction so that any failures are
+ // rolled back.
+ wchar_t transactionName[] = L"" MOZ_TOAST_APP_NAME L" toast registration";
+ txn.own(::CreateTransaction(nullptr, nullptr, TRANSACTION_DO_NOT_PROMOTE, 0,
+ 0, 0, transactionName));
+ NS_ENSURE_TRUE(txn.get() != INVALID_HANDLE_VALUE, false);
+
+ LSTATUS status;
+
+ auto RegisterKey = [&](const nsAString& path, nsAutoRegKey& key) {
+ HKEY rawKey;
+ status = ::RegCreateKeyTransactedW(
+ HKEY_CURRENT_USER, PromiseFlatString(path).get(), 0, nullptr,
+ REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &rawKey, nullptr, txn,
+ nullptr);
+ NS_ENSURE_TRUE(status == ERROR_SUCCESS, false);
+
+ key.own(rawKey);
+ return true;
+ };
+ auto RegisterValue = [&](nsAutoRegKey& key, const nsAString& name,
+ unsigned long type, const nsAString& data) {
+ status = ::RegSetValueExW(
+ key, PromiseFlatString(name).get(), 0, type,
+ static_cast<const BYTE*>(PromiseFlatString(data).get()),
+ (data.Length() + 1) * sizeof(wchar_t));
+
+ return status == ERROR_SUCCESS;
+ };
+
+ // clang-format off
+ /* Writes the following keys and values to the registry.
+ * HKEY_CURRENT_USER\Software\Classes\AppID\{GUID} DllSurrogate : REG_SZ = ""
+ * \AppUserModelId\{MOZ_TOAST_APP_NAME}PortableToast-{install hash} CustomActivator : REG_SZ = {GUID}
+ * DisplayName : REG_EXPAND_SZ = {display name}
+ * IconUri : REG_EXPAND_SZ = {icon path}
+ * \CLSID\{GUID} AppID : REG_SZ = {GUID}
+ * \InprocServer32 (Default) : REG_SZ = {notificationserver.dll path}
+ */
+ // clang-format on
+
+ constexpr nsLiteralString classes = u"Software\\Classes\\"_ns;
+
+ nsAutoString aumid = classes + u"AppUserModelId\\"_ns + portableAumid;
+ nsAutoRegKey aumidKey;
+ NS_ENSURE_TRUE(RegisterKey(aumid, aumidKey), false);
+
+ nsAutoString guidStr;
+ {
+ DWORD bufferSizeBytes = NSID_LENGTH * sizeof(wchar_t);
+ Buffer<wchar_t> guidBuffer(bufferSizeBytes);
+ status = ::RegGetValueW(HKEY_CURRENT_USER, aumid.get(), L"CustomActivator",
+ RRF_RT_REG_SZ, 0, guidBuffer.Elements(),
+ &bufferSizeBytes);
+
+ CLSID unused;
+ if (status == ERROR_SUCCESS &&
+ SUCCEEDED(CLSIDFromString(guidBuffer.Elements(), &unused))) {
+ guidStr = guidBuffer.Elements();
+ } else {
+ nsIDToCString uuidString(nsID::GenerateUUID());
+ size_t len = strlen(uuidString.get());
+ MOZ_ASSERT(len == NSID_LENGTH - 1);
+ CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), guidStr);
+ }
+
+ if (status == ERROR_SUCCESS) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Existing CustomActivator guid found: '%s'",
+ NS_ConvertUTF16toUTF8(guidStr).get()));
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("New CustomActivator guid generated: '%s'",
+ NS_ConvertUTF16toUTF8(guidStr).get()));
+ }
+ }
+ NS_ENSURE_TRUE(
+ RegisterValue(aumidKey, u"CustomActivator"_ns, REG_SZ, guidStr), false);
+ nsAutoString brandName;
+ WidgetUtils::GetBrandShortName(brandName);
+ NS_ENSURE_TRUE(
+ RegisterValue(aumidKey, u"DisplayName"_ns, REG_EXPAND_SZ, brandName),
+ false);
+ NS_ENSURE_TRUE(
+ RegisterValue(aumidKey, u"IconUri"_ns, REG_EXPAND_SZ, iconPath), false);
+
+ nsAutoString appid = classes + u"AppID\\"_ns + guidStr;
+ nsAutoRegKey appidKey;
+ NS_ENSURE_TRUE(RegisterKey(appid, appidKey), false);
+ NS_ENSURE_TRUE(RegisterValue(appidKey, u"DllSurrogate"_ns, REG_SZ, u""_ns),
+ false);
+
+ nsAutoString clsid = classes + u"CLSID\\"_ns + guidStr;
+ nsAutoRegKey clsidKey;
+ NS_ENSURE_TRUE(RegisterKey(clsid, clsidKey), false);
+ NS_ENSURE_TRUE(RegisterValue(clsidKey, u"AppID"_ns, REG_SZ, guidStr), false);
+
+ nsAutoString inproc = clsid + u"\\InprocServer32"_ns;
+ nsAutoRegKey inprocKey;
+ NS_ENSURE_TRUE(RegisterKey(inproc, inprocKey), false);
+ // Set the component's path to this DLL
+ NS_ENSURE_TRUE(RegisterValue(inprocKey, u""_ns, REG_SZ, dllPath), false);
+
+ NS_ENSURE_TRUE(::CommitTransaction(txn), false);
+
+ MOZ_LOG(
+ sWASLog, LogLevel::Debug,
+ ("Updated registration for CustomActivator value in key 'HKCU\\%s': '%s'",
+ NS_ConvertUTF16toUTF8(aumid).get(),
+ NS_ConvertUTF16toUTF8(guidStr).get()));
+ aAumid.emplace(portableAumid);
+ return true;
+}
+
+nsresult ToastNotification::BackgroundDispatch(nsIRunnable* runnable) {
+ return NS_DispatchBackgroundTask(runnable);
+}
+
+NS_IMETHODIMP
+ToastNotification::GetSuppressForScreenSharing(bool* aRetVal) {
+ *aRetVal = mSuppressForScreenSharing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::SetSuppressForScreenSharing(bool aSuppress) {
+ mSuppressForScreenSharing = aSuppress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ nsDependentCString topic(aTopic);
+
+ for (auto iter = mActiveHandlers.Iter(); !iter.Done(); iter.Next()) {
+ RefPtr<ToastNotificationHandler> handler = iter.UserData();
+
+ auto removeNotification = [&]() {
+ // The handlers' destructors will do the right thing (de-register with
+ // Windows).
+ iter.Remove();
+
+ // Break the cycle between the handler and the MSCOM notification so the
+ // handler's destructor will be called.
+ handler->UnregisterHandler();
+ };
+
+ if (topic == "last-pb-context-exited"_ns) {
+ if (handler->IsPrivate()) {
+ handler->HideAlert();
+ removeNotification();
+ }
+ } else if (topic == "quit-application"_ns) {
+ removeNotification();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::ShowAlertNotification(
+ const nsAString& aImageUrl, const nsAString& aAlertTitle,
+ const nsAString& aAlertText, bool aAlertTextClickable,
+ const nsAString& aAlertCookie, nsIObserver* aAlertListener,
+ const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
+ const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
+ bool aRequireInteraction) {
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ if (NS_WARN_IF(!alert)) {
+ return NS_ERROR_FAILURE;
+ }
+ // vibrate is unused for now
+ nsTArray<uint32_t> vibrate;
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText,
+ aAlertTextClickable, aAlertCookie, aBidi, aLang,
+ aData, aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction, false, vibrate);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return ShowAlert(alert, aAlertListener);
+}
+
+NS_IMETHODIMP
+ToastNotification::ShowPersistentNotification(const nsAString& aPersistentData,
+ nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ return ShowAlert(aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
+ToastNotification::SetManualDoNotDisturb(bool aDoNotDisturb) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ToastNotification::GetManualDoNotDisturb(bool* aRet) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ToastNotification::ShowAlert(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ NS_ENSURE_ARG(aAlert);
+
+ if (mSuppressForScreenSharing) {
+ return NS_OK;
+ }
+
+ nsAutoString cookie;
+ MOZ_TRY(aAlert->GetCookie(cookie));
+
+ nsAutoString name;
+ MOZ_TRY(aAlert->GetName(name));
+
+ nsAutoString title;
+ MOZ_TRY(aAlert->GetTitle(title));
+ if (!EnsureUTF16Validity(title)) {
+ MOZ_LOG(sWASLog, LogLevel::Warning,
+ ("Notification title was invalid UTF16, unpaired surrogates have "
+ "been replaced."));
+ }
+
+ nsAutoString text;
+ MOZ_TRY(aAlert->GetText(text));
+ if (!EnsureUTF16Validity(text)) {
+ MOZ_LOG(sWASLog, LogLevel::Warning,
+ ("Notification text was invalid UTF16, unpaired surrogates have "
+ "been replaced."));
+ }
+
+ bool textClickable;
+ MOZ_TRY(aAlert->GetTextClickable(&textClickable));
+
+ bool isSilent;
+ MOZ_TRY(aAlert->GetSilent(&isSilent));
+
+ nsAutoString hostPort;
+ MOZ_TRY(aAlert->GetSource(hostPort));
+
+ nsAutoString opaqueRelaunchData;
+ MOZ_TRY(aAlert->GetOpaqueRelaunchData(opaqueRelaunchData));
+
+ bool requireInteraction;
+ MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction));
+
+ bool inPrivateBrowsing;
+ MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing));
+
+ nsTArray<RefPtr<nsIAlertAction>> actions;
+ MOZ_TRY(aAlert->GetActions(actions));
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal)));
+ bool isSystemPrincipal = principal && principal->IsSystemPrincipal();
+
+ bool handleActions = false;
+ auto imagePlacement = ImagePlacement::eInline;
+ if (isSystemPrincipal) {
+ nsCOMPtr<nsIWindowsAlertNotification> winAlert(do_QueryInterface(aAlert));
+ if (winAlert) {
+ MOZ_TRY(winAlert->GetHandleActions(&handleActions));
+
+ nsIWindowsAlertNotification::ImagePlacement placement;
+ MOZ_TRY(winAlert->GetImagePlacement(&placement));
+ switch (placement) {
+ case nsIWindowsAlertNotification::eHero:
+ imagePlacement = ImagePlacement::eHero;
+ break;
+ case nsIWindowsAlertNotification::eIcon:
+ imagePlacement = ImagePlacement::eIcon;
+ break;
+ case nsIWindowsAlertNotification::eInline:
+ imagePlacement = ImagePlacement::eInline;
+ break;
+ default:
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Invalid image placement enum value: %hhu", placement));
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+ }
+
+ RefPtr<ToastNotificationHandler> oldHandler = mActiveHandlers.Get(name);
+
+ NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED);
+ RefPtr<ToastNotificationHandler> handler = new ToastNotificationHandler(
+ this, mAumid.ref(), aAlertListener, name, cookie, title, text, hostPort,
+ textClickable, requireInteraction, actions, isSystemPrincipal,
+ opaqueRelaunchData, inPrivateBrowsing, isSilent, handleActions,
+ imagePlacement);
+ mActiveHandlers.InsertOrUpdate(name, RefPtr{handler});
+
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Adding handler '%s': [%p] (now %d handlers)",
+ NS_ConvertUTF16toUTF8(name).get(), handler.get(),
+ mActiveHandlers.Count()));
+
+ nsresult rv = handler->InitAlertAsync(aAlert);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Failed to init alert, removing '%s'",
+ NS_ConvertUTF16toUTF8(name).get()));
+ mActiveHandlers.Remove(name);
+ handler->UnregisterHandler();
+ return rv;
+ }
+
+ // If there was a previous handler with the same name then unregister it.
+ if (oldHandler) {
+ oldHandler->UnregisterHandler();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::GetXmlStringForWindowsAlert(nsIAlertNotification* aAlert,
+ const nsAString& aWindowsTag,
+ nsAString& aString) {
+ NS_ENSURE_ARG(aAlert);
+
+ nsAutoString cookie;
+ MOZ_TRY(aAlert->GetCookie(cookie));
+
+ nsAutoString name;
+ MOZ_TRY(aAlert->GetName(name));
+
+ nsAutoString title;
+ MOZ_TRY(aAlert->GetTitle(title));
+
+ nsAutoString text;
+ MOZ_TRY(aAlert->GetText(text));
+
+ bool textClickable;
+ MOZ_TRY(aAlert->GetTextClickable(&textClickable));
+
+ bool isSilent;
+ MOZ_TRY(aAlert->GetSilent(&isSilent));
+
+ nsAutoString hostPort;
+ MOZ_TRY(aAlert->GetSource(hostPort));
+
+ nsAutoString opaqueRelaunchData;
+ MOZ_TRY(aAlert->GetOpaqueRelaunchData(opaqueRelaunchData));
+
+ bool requireInteraction;
+ MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction));
+
+ bool inPrivateBrowsing;
+ MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing));
+
+ nsTArray<RefPtr<nsIAlertAction>> actions;
+ MOZ_TRY(aAlert->GetActions(actions));
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal)));
+ bool isSystemPrincipal = principal && principal->IsSystemPrincipal();
+
+ NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED);
+ RefPtr<ToastNotificationHandler> handler = new ToastNotificationHandler(
+ this, mAumid.ref(), nullptr /* aAlertListener */, name, cookie, title,
+ text, hostPort, textClickable, requireInteraction, actions,
+ isSystemPrincipal, opaqueRelaunchData, inPrivateBrowsing, isSilent);
+
+ // Usually, this will be empty during testing, making test output
+ // deterministic.
+ MOZ_TRY(handler->SetWindowsTag(aWindowsTag));
+
+ nsAutoString imageURL;
+ MOZ_TRY(aAlert->GetImageURL(imageURL));
+
+ return handler->CreateToastXmlString(imageURL, aString);
+}
+
+// Verifies that the tag recieved associates to a notification created during
+// this application's session, or handles fallback behavior.
+RefPtr<ToastHandledPromise> ToastNotification::VerifyTagPresentOrFallback(
+ const nsAString& aWindowsTag) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Iterating %d handlers", mActiveHandlers.Count()));
+
+ for (auto iter = mActiveHandlers.Iter(); !iter.Done(); iter.Next()) {
+ RefPtr<ToastNotificationHandler> handler = iter.UserData();
+ nsAutoString tag;
+ nsresult rv = handler->GetWindowsTag(tag);
+
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Comparing external windowsTag '%s' to handled windowsTag '%s'",
+ NS_ConvertUTF16toUTF8(aWindowsTag).get(),
+ NS_ConvertUTF16toUTF8(tag).get()));
+ if (aWindowsTag.Equals(tag)) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("External windowsTag '%s' is handled by handler [%p]",
+ NS_ConvertUTF16toUTF8(aWindowsTag).get(), handler.get()));
+ return ToastHandledPromise::CreateAndResolve(true, __func__);
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Failed to get windowsTag for handler [%p]", handler.get()));
+ }
+ }
+
+ // Fallback handling is required.
+
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("External windowsTag '%s' is not handled",
+ NS_ConvertUTF16toUTF8(aWindowsTag).get()));
+
+ RefPtr<ToastHandledPromise::Private> fallbackPromise =
+ new ToastHandledPromise::Private(__func__);
+
+ // TODO: Bug 1806005 - At time of writing this function is called in a call
+ // stack containing `WndProc` callback on an STA thread. As a result attempts
+ // to create a `ToastNotificationManager` instance results an an
+ // `RPC_E_CANTCALLOUT_ININPUTSYNCCALL` error. We can simplify the the XPCOM
+ // interface and synchronize the COM interactions if notification fallback
+ // handling were no longer handled in a `WndProc` context.
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "VerifyTagPresentOrFallback fallback background task",
+ [fallbackPromise]() { fallbackPromise->Resolve(false, __func__); }));
+
+ return fallbackPromise;
+}
+
+// Send our window's PID to the notification server so that it can grant us
+// `SetForegroundWindow` permissions. PID 0 is sent to signal no window PID.
+// Absense of PID which may occur when we are yet unable to retrieve the
+// window during startup, which is not a problem in practice as new windows
+// receive focus by default.
+void ToastNotification::SignalComNotificationHandled(
+ const nsAString& aWindowsTag) {
+ DWORD pid = 0;
+
+ nsCOMPtr<nsIWindowMediator> winMediator(
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
+ if (winMediator) {
+ nsCOMPtr<mozIDOMWindowProxy> navWin;
+ winMediator->GetMostRecentWindow(u"navigator:browser",
+ getter_AddRefs(navWin));
+ if (navWin) {
+ nsCOMPtr<nsIWidget> widget =
+ WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin));
+ if (widget) {
+ HWND hwnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW);
+ GetWindowThreadProcessId(hwnd, &pid);
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get widget"));
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get navWin"));
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get WinMediator"));
+ }
+
+ // Run pipe communication off the main thread to prevent UI jank from
+ // blocking. Nothing relies on the COM server's response or that it has
+ // responded at time of commit.
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "SignalComNotificationHandled background task",
+ [pid, aWindowsTag = nsString{aWindowsTag}]() mutable {
+ std::wstring pipeName = GetNotificationPipeName(aWindowsTag.get());
+
+ nsAutoHandle pipe;
+ pipe.own(CreateFileW(pipeName.c_str(), GENERIC_READ | GENERIC_WRITE,
+ 0, nullptr, OPEN_EXISTING,
+ FILE_FLAG_OVERLAPPED, nullptr));
+ if (pipe.get() == INVALID_HANDLE_VALUE) {
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Unable to open notification server pipe."));
+ return;
+ }
+
+ DWORD pipeFlags = PIPE_READMODE_MESSAGE;
+ if (!SetNamedPipeHandleState(pipe.get(), &pipeFlags, nullptr,
+ nullptr)) {
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Error setting pipe handle state, error %lu",
+ GetLastError()));
+ return;
+ }
+
+ // Pass our window's PID to the COM server receive
+ // `SetForegroundWindow` permissions, and wait for a message
+ // acknowledging the permission has been granted.
+ ToastNotificationPidMessage in{};
+ in.pid = pid;
+ ToastNotificationPermissionMessage out{};
+ auto transact = [&](OVERLAPPED& overlapped) {
+ return TransactNamedPipe(pipe.get(), &in, sizeof(in), &out,
+ sizeof(out), nullptr, &overlapped);
+ };
+ bool result =
+ SyncDoOverlappedIOWithTimeout(pipe, sizeof(out), transact);
+
+ if (result && out.setForegroundPermissionGranted && pid != 0) {
+ MOZ_LOG(
+ sWASLog, LogLevel::Info,
+ ("SetForegroundWindow permission granted to our window."));
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("SetForegroundWindow permission not granted to our "
+ "window."));
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+}
+
+NS_IMETHODIMP
+ToastNotification::HandleWindowsTag(const nsAString& aWindowsTag,
+ JSContext* aCx, dom::Promise** aPromise) {
+ NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED);
+ NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD);
+
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ ENSURE_SUCCESS(rv, rv.StealNSResult());
+
+ this->VerifyTagPresentOrFallback(aWindowsTag)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aWindowsTag = nsString(aWindowsTag),
+ promise](const bool aTagWasHandled) {
+ // We no longer need to query toast information from OS and can
+ // allow the COM server to proceed (toast information is lost once
+ // the COM server's `Activate` callback returns).
+ SignalComNotificationHandled(aWindowsTag);
+
+ dom::AutoJSAPI js;
+ if (NS_WARN_IF(!js.Init(promise->GetGlobalObject()))) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Resolve the DOM Promise with a JS object. Set properties if
+ // fallback handling is necessary.
+
+ JSContext* cx = js.cx();
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+
+ JS::Rooted<JS::Value> attVal(cx, JS::BooleanValue(aTagWasHandled));
+ Unused << NS_WARN_IF(
+ !JS_SetProperty(cx, obj, "tagWasHandled", attVal));
+
+ promise->MaybeResolve(obj);
+ },
+ [aWindowsTag = nsString(aWindowsTag), promise]() {
+ // We no longer need to query toast information from OS and can
+ // allow the COM server to proceed (toast information is lost once
+ // the COM server's `Activate` callback returns).
+ SignalComNotificationHandled(aWindowsTag);
+
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::CloseAlert(const nsAString& aAlertName,
+ bool aContextClosed) {
+ RefPtr<ToastNotificationHandler> handler;
+ if (NS_WARN_IF(!mActiveHandlers.Get(aAlertName, getter_AddRefs(handler)))) {
+ return NS_OK;
+ }
+
+ if (!aContextClosed || handler->IsPrivate()) {
+ // Hide the alert when not implicitly closed by tab/window closing or when
+ // notification originated from a private tab.
+ handler->HideAlert();
+ }
+
+ mActiveHandlers.Remove(aAlertName);
+ handler->UnregisterHandler();
+
+ return NS_OK;
+}
+
+bool ToastNotification::IsActiveHandler(const nsAString& aAlertName,
+ ToastNotificationHandler* aHandler) {
+ RefPtr<ToastNotificationHandler> handler;
+ if (NS_WARN_IF(!mActiveHandlers.Get(aAlertName, getter_AddRefs(handler)))) {
+ return false;
+ }
+ return handler == aHandler;
+}
+
+void ToastNotification::RemoveHandler(const nsAString& aAlertName,
+ ToastNotificationHandler* aHandler) {
+ // The alert may have been replaced; only remove it from the active
+ // handler's map if it's the same.
+ if (IsActiveHandler(aAlertName, aHandler)) {
+ // Terrible things happen if the destructor of a handler is called inside
+ // the hashtable .Remove() method. Wait until we have returned from there.
+ RefPtr<ToastNotificationHandler> kungFuDeathGrip(aHandler);
+ mActiveHandlers.Remove(aAlertName);
+ aHandler->UnregisterHandler();
+ }
+}
+
+NS_IMETHODIMP
+ToastNotification::RemoveAllNotificationsForInstall() {
+ HRESULT hr = S_OK;
+
+ ComPtr<IToastNotificationManagerStatics> manager;
+ hr = GetActivationFactory(
+ HStringReference(
+ RuntimeClass_Windows_UI_Notifications_ToastNotificationManager)
+ .Get(),
+ &manager);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
+
+ HString aumid;
+ MOZ_ASSERT(mAumid.isSome());
+ hr = aumid.Set(mAumid.ref().get());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
+
+ // Hide toasts in action center.
+ [&]() {
+ ComPtr<IToastNotificationManagerStatics2> manager2;
+ hr = manager.As(&manager2);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ ComPtr<IToastNotificationHistory> history;
+ hr = manager2->get_History(&history);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ hr = history->ClearWithId(aumid.Get());
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+ }();
+
+ // Hide scheduled toasts.
+ [&]() {
+ ComPtr<IToastNotifier> notifier;
+ hr = manager->CreateToastNotifierWithId(aumid.Get(), &notifier);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ ComPtr<IVectorView_ScheduledToastNotification> scheduledToasts;
+ hr = notifier->GetScheduledToastNotifications(&scheduledToasts);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ unsigned int schedSize;
+ hr = scheduledToasts->get_Size(&schedSize);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ for (unsigned int i = 0; i < schedSize; i++) {
+ ComPtr<IScheduledToastNotification> schedToast;
+ hr = scheduledToasts->GetAt(i, &schedToast);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ hr = notifier->RemoveFromSchedule(schedToast.Get());
+ Unused << NS_WARN_IF(FAILED(hr));
+ }
+ }();
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(WindowsAlertNotification, AlertNotification,
+ nsIWindowsAlertNotification)
+
+NS_IMETHODIMP
+WindowsAlertNotification::GetHandleActions(bool* aHandleActions) {
+ *aHandleActions = mHandleActions;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsAlertNotification::SetHandleActions(bool aHandleActions) {
+ mHandleActions = aHandleActions;
+ return NS_OK;
+}
+
+NS_IMETHODIMP WindowsAlertNotification::GetImagePlacement(
+ nsIWindowsAlertNotification::ImagePlacement* aImagePlacement) {
+ *aImagePlacement = mImagePlacement;
+ return NS_OK;
+}
+
+NS_IMETHODIMP WindowsAlertNotification::SetImagePlacement(
+ nsIWindowsAlertNotification::ImagePlacement aImagePlacement) {
+ switch (aImagePlacement) {
+ case eHero:
+ case eIcon:
+ case eInline:
+ mImagePlacement = aImagePlacement;
+ break;
+ default:
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Invalid image placement enum value: %hhu", aImagePlacement));
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/ToastNotification.h b/widget/windows/ToastNotification.h
new file mode 100644
index 0000000000..9beb08cc27
--- /dev/null
+++ b/widget/windows/ToastNotification.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef ToastNotification_h__
+#define ToastNotification_h__
+
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+#include "nsIAlertsService.h"
+#include "nsIObserver.h"
+#include "nsIThread.h"
+#include "nsIWindowsAlertsService.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/AlertNotification.h"
+
+namespace mozilla {
+namespace widget {
+
+using ToastHandledPromise = MozPromise<bool, bool, true>;
+
+class ToastNotificationHandler;
+
+class WindowsAlertNotification final : public AlertNotification,
+ public nsIWindowsAlertNotification {
+ public:
+ NS_DECL_NSIWINDOWSALERTNOTIFICATION
+ NS_FORWARD_NSIALERTNOTIFICATION(AlertNotification::)
+ NS_DECL_ISUPPORTS_INHERITED
+
+ WindowsAlertNotification() = default;
+
+ protected:
+ virtual ~WindowsAlertNotification() = default;
+ bool mHandleActions = false;
+ nsIWindowsAlertNotification::ImagePlacement mImagePlacement = eInline;
+};
+
+class ToastNotification final : public nsIWindowsAlertsService,
+ public nsIAlertsDoNotDisturb,
+ public nsIObserver {
+ public:
+ NS_DECL_NSIALERTSSERVICE
+ NS_DECL_NSIWINDOWSALERTSSERVICE
+ NS_DECL_NSIALERTSDONOTDISTURB
+ NS_DECL_NSIOBSERVER
+ NS_DECL_ISUPPORTS
+
+ ToastNotification();
+
+ nsresult Init();
+
+ bool IsActiveHandler(const nsAString& aAlertName,
+ ToastNotificationHandler* aHandler);
+ void RemoveHandler(const nsAString& aAlertName,
+ ToastNotificationHandler* aHandler);
+
+ nsresult BackgroundDispatch(nsIRunnable* runnable);
+
+ protected:
+ virtual ~ToastNotification();
+ bool EnsureAumidRegistered();
+
+ static bool AssignIfMsixAumid(Maybe<nsAutoString>& aAumid);
+ static bool AssignIfNsisAumid(nsAutoString& aInstallHash,
+ Maybe<nsAutoString>& aAumid);
+ static bool RegisterRuntimeAumid(nsAutoString& aInstallHash,
+ Maybe<nsAutoString>& aAumid);
+
+ RefPtr<ToastHandledPromise> VerifyTagPresentOrFallback(
+ const nsAString& aWindowsTag);
+ static void SignalComNotificationHandled(const nsAString& aWindowsTag);
+
+ nsRefPtrHashtable<nsStringHashKey, ToastNotificationHandler> mActiveHandlers;
+ Maybe<nsAutoString> mAumid;
+ bool mSuppressForScreenSharing = false;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/windows/ToastNotificationHandler.cpp b/widget/windows/ToastNotificationHandler.cpp
new file mode 100644
index 0000000000..a493342719
--- /dev/null
+++ b/widget/windows/ToastNotificationHandler.cpp
@@ -0,0 +1,1167 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 "ToastNotificationHandler.h"
+
+#include <windows.foundation.h>
+
+#include "gfxUtils.h"
+#include "gfxPlatform.h"
+#include "imgIContainer.h"
+#include "imgIRequest.h"
+#include "json/json.h"
+#include "mozilla/gfx/2D.h"
+#ifdef MOZ_BACKGROUNDTASKS
+# include "mozilla/BackgroundTasks.h"
+#endif
+#include "mozilla/HashFunctions.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+#include "mozilla/Result.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Tokenizer.h"
+#include "mozilla/Unused.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/intl/Localization.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsAppRunner.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIDUtils.h"
+#include "nsIStringBundle.h"
+#include "nsIToolkitProfile.h"
+#include "nsIToolkitProfileService.h"
+#include "nsIURI.h"
+#include "nsIWidget.h"
+#include "nsIWindowMediator.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsProxyRelease.h"
+#include "nsXREDirProvider.h"
+#include "ToastNotificationHeaderOnlyUtils.h"
+#include "WidgetUtils.h"
+#include "WinUtils.h"
+
+#include "ToastNotification.h"
+
+namespace mozilla {
+namespace widget {
+
+extern LazyLogModule sWASLog;
+
+using namespace ABI::Windows::Data::Xml::Dom;
+using namespace ABI::Windows::Foundation;
+using namespace ABI::Windows::UI::Notifications;
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+using namespace toastnotification;
+
+// Needed to disambiguate internal and Windows `ToastNotification` classes.
+using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification;
+using ToastActivationHandler =
+ ITypedEventHandler<WinToastNotification*, IInspectable*>;
+using ToastDismissedHandler =
+ ITypedEventHandler<WinToastNotification*, ToastDismissedEventArgs*>;
+using ToastFailedHandler =
+ ITypedEventHandler<WinToastNotification*, ToastFailedEventArgs*>;
+using IVectorView_ToastNotification =
+ Collections::IVectorView<WinToastNotification*>;
+
+NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener)
+
+static bool SetNodeValueString(const nsString& aString, IXmlNode* node,
+ IXmlDocument* xml) {
+ ComPtr<IXmlText> inputText;
+ HRESULT hr;
+ hr = xml->CreateTextNode(HStringReference(aString.get()).Get(), &inputText);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ ComPtr<IXmlNode> inputTextNode;
+ hr = inputText.As(&inputTextNode);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ ComPtr<IXmlNode> appendedChild;
+ hr = node->AppendChild(inputTextNode.Get(), &appendedChild);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ return true;
+}
+
+static bool SetAttribute(ComPtr<IXmlElement>& element,
+ const HStringReference& name, const nsAString& value) {
+ HString valueStr;
+ valueStr.Set(PromiseFlatString(value).get());
+
+ HRESULT hr = element->SetAttribute(name.Get(), valueStr.Get());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ return true;
+}
+
+static bool AddActionNode(ComPtr<IXmlDocument>& toastXml,
+ ComPtr<IXmlNode>& actionsNode,
+ const nsAString& actionTitle,
+ const nsAString& launchArg,
+ const nsAString& actionArgs,
+ const nsAString& actionPlacement = u""_ns,
+ const nsAString& activationType = u""_ns) {
+ ComPtr<IXmlElement> action;
+ HRESULT hr =
+ toastXml->CreateElement(HStringReference(L"action").Get(), &action);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ bool success =
+ SetAttribute(action, HStringReference(L"content"), actionTitle);
+ NS_ENSURE_TRUE(success, false);
+
+ // Action arguments overwrite the toast's launch arguments, so we need to
+ // prepend the launch arguments necessary for the Notification Server to
+ // reconstruct the toast's origin.
+ //
+ // Web Notification actions are arbitrary strings; to prevent breaking launch
+ // argument parsing the action argument must be last. All delimiters after
+ // `action` are part of the action arugment.
+ nsAutoString args = launchArg + u"\n"_ns +
+ nsDependentString(kLaunchArgAction) + u"\n"_ns +
+ actionArgs;
+ success = SetAttribute(action, HStringReference(L"arguments"), args);
+ NS_ENSURE_TRUE(success, false);
+
+ if (!actionPlacement.IsEmpty()) {
+ success =
+ SetAttribute(action, HStringReference(L"placement"), actionPlacement);
+ NS_ENSURE_TRUE(success, false);
+ }
+
+ if (!activationType.IsEmpty()) {
+ success = SetAttribute(action, HStringReference(L"activationType"),
+ activationType);
+ NS_ENSURE_TRUE(success, false);
+
+ // No special argument handling: when `activationType="system"`, `arguments`
+ // should be a Windows-specific keyword, namely "dismiss" or "snooze", which
+ // are supposed to make a system handled dismiss/snooze buttons.
+ // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml#snoozedismiss
+ //
+ // Note that while using it prevents calling our notification COM server,
+ // it somehow still calls OnActivate instead of OnDismiss. Thus, we still
+ // need to handle such callbacks manually by checking `arguments`.
+ success = SetAttribute(action, HStringReference(L"arguments"), actionArgs);
+ NS_ENSURE_TRUE(success, false);
+ }
+
+ // Add <action> to <actions>
+ ComPtr<IXmlNode> actionNode;
+ hr = action.As(&actionNode);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ ComPtr<IXmlNode> appendedChild;
+ hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ return true;
+}
+
+nsresult ToastNotificationHandler::GetWindowsTag(nsAString& aWindowsTag) {
+ aWindowsTag.Assign(mWindowsTag);
+ return NS_OK;
+}
+
+nsresult ToastNotificationHandler::SetWindowsTag(const nsAString& aWindowsTag) {
+ mWindowsTag.Assign(aWindowsTag);
+ return NS_OK;
+}
+
+// clang - format off
+/* Populate the launch argument so the COM server can reconstruct the toast
+ * origin.
+ *
+ * program
+ * {MOZ_APP_NAME}
+ * profile
+ * {path to profile}
+ */
+// clang-format on
+Result<nsString, nsresult> ToastNotificationHandler::GetLaunchArgument() {
+ nsString launchArg;
+
+ // When the preference is false, the COM notification server will be invoked,
+ // discover that there is no `program`, and exit (successfully), after which
+ // Windows will invoke the in-product Windows 8-style callbacks. When true,
+ // the COM notification server will launch Firefox with sufficient arguments
+ // for Firefox to handle the notification.
+ if (!Preferences::GetBool(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ false)) {
+ // Include dummy key/value so that newline appended arguments aren't off by
+ // one line.
+ launchArg += u"invalid key\ninvalid value"_ns;
+ return launchArg;
+ }
+
+ // `program` argument.
+ launchArg += nsDependentString(kLaunchArgProgram) + u"\n"_ns MOZ_APP_NAME;
+
+ // `profile` argument.
+ nsCOMPtr<nsIFile> profDir;
+ bool wantCurrentProfile = true;
+#ifdef MOZ_BACKGROUNDTASKS
+ if (BackgroundTasks::IsBackgroundTaskMode()) {
+ // Notifications popped from a background task want to invoke Firefox with a
+ // different profile -- the default browsing profile. We'd prefer to not
+ // specify a profile, so that the Firefox invoked by the notification server
+ // chooses its default profile, but this might pop the profile chooser in
+ // some configurations.
+ wantCurrentProfile = false;
+
+ nsCOMPtr<nsIToolkitProfileService> profileSvc =
+ do_GetService(NS_PROFILESERVICE_CONTRACTID);
+ if (profileSvc) {
+ nsCOMPtr<nsIToolkitProfile> defaultProfile;
+ nsresult rv =
+ profileSvc->GetDefaultProfile(getter_AddRefs(defaultProfile));
+ if (NS_SUCCEEDED(rv) && defaultProfile) {
+ // Not all installations have a default profile. But if one is set,
+ // then it should have a profile directory.
+ MOZ_TRY(defaultProfile->GetRootDir(getter_AddRefs(profDir)));
+ }
+ }
+ }
+#endif
+ if (wantCurrentProfile) {
+ MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profDir)));
+ }
+
+ if (profDir) {
+ nsAutoString profilePath;
+ MOZ_TRY(profDir->GetPath(profilePath));
+ launchArg += u"\n"_ns + nsDependentString(kLaunchArgProfile) + u"\n"_ns +
+ profilePath;
+ }
+
+ // `windowsTag` argument.
+ launchArg +=
+ u"\n"_ns + nsDependentString(kLaunchArgTag) + u"\n"_ns + mWindowsTag;
+
+ // `logging` argument.
+ if (Preferences::GetBool(
+ "alerts.useSystemBackend.windows.notificationserver.verbose",
+ false)) {
+ // Signal notification to log verbose messages.
+ launchArg +=
+ u"\n"_ns + nsDependentString(kLaunchArgLogging) + u"\nverbose"_ns;
+ }
+
+ return launchArg;
+}
+
+static ComPtr<IToastNotificationManagerStatics>
+GetToastNotificationManagerStatics() {
+ ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics;
+ HRESULT hr = GetActivationFactory(
+ HStringReference(
+ RuntimeClass_Windows_UI_Notifications_ToastNotificationManager)
+ .Get(),
+ &toastNotificationManagerStatics);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ return toastNotificationManagerStatics;
+}
+
+ToastNotificationHandler::~ToastNotificationHandler() {
+ if (mImageRequest) {
+ mImageRequest->Cancel(NS_BINDING_ABORTED);
+ mImageRequest = nullptr;
+ }
+
+ if (mHasImage && mImageFile) {
+ DebugOnly<nsresult> rv = mImageFile->Remove(false);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Cannot remove temporary image file");
+ }
+
+ UnregisterHandler();
+}
+
+void ToastNotificationHandler::UnregisterHandler() {
+ if (mNotification) {
+ mNotification->remove_Dismissed(mDismissedToken);
+ mNotification->remove_Activated(mActivatedToken);
+ mNotification->remove_Failed(mFailedToken);
+ }
+
+ mNotification = nullptr;
+ mNotifier = nullptr;
+
+ SendFinished();
+}
+
+nsresult ToastNotificationHandler::InitAlertAsync(
+ nsIAlertNotification* aAlert) {
+ MOZ_TRY(InitWindowsTag());
+
+#ifdef MOZ_BACKGROUNDTASKS
+ nsAutoString imageUrl;
+ if (BackgroundTasks::IsBackgroundTaskMode() &&
+ NS_SUCCEEDED(aAlert->GetImageURL(imageUrl)) && !imageUrl.IsEmpty()) {
+ // Bug 1870750: Image decoding relies on gfx and runs on a thread pool,
+ // which expects to have been initialized early and on the main thread.
+ // Since background tasks run headless this never occurs. In this case we
+ // force gfx initialization.
+ Unused << NS_WARN_IF(!gfxPlatform::GetPlatform());
+ }
+#endif
+
+ return aAlert->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr,
+ getter_AddRefs(mImageRequest));
+}
+
+// Uniquely identify this toast to Windows. Existing names and cookies are not
+// suitable: we want something generated and unique. This is needed to check if
+// toast is still present in the Windows Action Center when we receive a dismiss
+// timeout.
+//
+// Local testing reveals that the space of tags is not global but instead is per
+// AUMID. Since an installation uses a unique AUMID incorporating the install
+// directory hash, it should not witness another installation's tag.
+nsresult ToastNotificationHandler::InitWindowsTag() {
+ mWindowsTag.Truncate();
+
+ nsAutoString tag;
+
+ // Multiple profiles might overwrite each other's toast messages when a
+ // common name is used for a given host port. We prevent this by including
+ // the profile directory as part of the toast hash.
+ nsCOMPtr<nsIFile> profDir;
+ MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profDir)));
+ MOZ_TRY(profDir->GetPath(tag));
+
+ if (!mHostPort.IsEmpty()) {
+ // Notification originated from a web notification.
+ // `mName` will be in the form `{mHostPort}#tag:{tag}` if the notification
+ // was created with a tag and `{mHostPort}#notag:{uuid}` otherwise.
+ tag += mName;
+ } else {
+ // Notification originated from the browser chrome.
+ if (!mName.IsEmpty()) {
+ tag += u"chrome#tag:"_ns;
+ // Browser chrome notifications don't follow any convention for naming.
+ tag += mName;
+ } else {
+ // No associated name, append a UUID to prevent reuse of the same tag.
+ nsIDToCString uuidString(nsID::GenerateUUID());
+ size_t len = strlen(uuidString.get());
+ MOZ_ASSERT(len == NSID_LENGTH - 1);
+ nsAutoString uuid;
+ CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), uuid);
+
+ tag += u"chrome#notag:"_ns;
+ tag += uuid;
+ }
+ }
+
+ // Windows notification tags are limited to 16 characters, or 64 characters
+ // after the Creators Update; therefore we hash the tag to fit the minimum
+ // range.
+ HashNumber hash = HashString(tag);
+ mWindowsTag.AppendPrintf("%010u", hash);
+
+ return NS_OK;
+}
+
+nsString ToastNotificationHandler::ActionArgsJSONString(
+ const nsString& aAction, const nsString& aOpaqueRelaunchData = u""_ns) {
+ nsAutoCString actionArgsData;
+
+ JSONStringRefWriteFunc js(actionArgsData);
+ JSONWriter w(js, JSONWriter::SingleLineStyle);
+ w.Start();
+
+ w.StringProperty("action", NS_ConvertUTF16toUTF8(aAction));
+
+ if (mIsSystemPrincipal) {
+ // Privileged/chrome alerts (not activated by Windows) can have custom
+ // relaunch data.
+ if (!aOpaqueRelaunchData.IsEmpty()) {
+ w.StringProperty("opaqueRelaunchData",
+ NS_ConvertUTF16toUTF8(aOpaqueRelaunchData));
+ }
+
+ // Privileged alerts include any provided name for metrics.
+ if (!mName.IsEmpty()) {
+ w.StringProperty("privilegedName", NS_ConvertUTF16toUTF8(mName));
+ }
+ } else {
+ if (!mHostPort.IsEmpty()) {
+ w.StringProperty("launchUrl", NS_ConvertUTF16toUTF8(mHostPort));
+ }
+ }
+
+ w.End();
+
+ return NS_ConvertUTF8toUTF16(actionArgsData);
+}
+
+ComPtr<IXmlDocument> ToastNotificationHandler::CreateToastXmlDocument() {
+ ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics =
+ GetToastNotificationManagerStatics();
+ NS_ENSURE_TRUE(toastNotificationManagerStatics, nullptr);
+
+ ToastTemplateType toastTemplate;
+ if (mHostPort.IsEmpty()) {
+ toastTemplate =
+ mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03
+ : ToastTemplateType::ToastTemplateType_ToastText03;
+ } else {
+ toastTemplate =
+ mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04
+ : ToastTemplateType::ToastTemplateType_ToastText04;
+ }
+
+ ComPtr<IXmlDocument> toastXml;
+ toastNotificationManagerStatics->GetTemplateContent(toastTemplate, &toastXml);
+
+ if (!toastXml) {
+ return nullptr;
+ }
+
+ nsresult ns;
+ HRESULT hr;
+ bool success;
+
+ if (mHasImage) {
+ ComPtr<IXmlNodeList> toastImageElements;
+ hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(),
+ &toastImageElements);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlNode> imageNode;
+ hr = toastImageElements->Item(0, &imageNode);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlElement> image;
+ hr = imageNode.As(&image);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ success = SetAttribute(image, HStringReference(L"src"), mImageUri);
+ NS_ENSURE_TRUE(success, nullptr);
+
+ switch (mImagePlacement) {
+ case ImagePlacement::eHero:
+ success =
+ SetAttribute(image, HStringReference(L"placement"), u"hero"_ns);
+ NS_ENSURE_TRUE(success, nullptr);
+ break;
+ case ImagePlacement::eIcon:
+ success = SetAttribute(image, HStringReference(L"placement"),
+ u"appLogoOverride"_ns);
+ NS_ENSURE_TRUE(success, nullptr);
+ break;
+ case ImagePlacement::eInline:
+ // No attribute placement attribute for inline images.
+ break;
+ }
+ }
+
+ ComPtr<IXmlNodeList> toastTextElements;
+ hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(),
+ &toastTextElements);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlNode> titleTextNodeRoot;
+ hr = toastTextElements->Item(0, &titleTextNodeRoot);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlNode> msgTextNodeRoot;
+ hr = toastTextElements->Item(1, &msgTextNodeRoot);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ success = SetNodeValueString(mTitle, titleTextNodeRoot.Get(), toastXml.Get());
+ NS_ENSURE_TRUE(success, nullptr);
+
+ success = SetNodeValueString(mMsg, msgTextNodeRoot.Get(), toastXml.Get());
+ NS_ENSURE_TRUE(success, nullptr);
+
+ ComPtr<IXmlNodeList> toastElements;
+ hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(),
+ &toastElements);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlNode> toastNodeRoot;
+ hr = toastElements->Item(0, &toastNodeRoot);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlElement> toastElement;
+ hr = toastNodeRoot.As(&toastElement);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ if (mRequireInteraction) {
+ success = SetAttribute(toastElement, HStringReference(L"scenario"),
+ u"reminder"_ns);
+ NS_ENSURE_TRUE(success, nullptr);
+ }
+
+ auto maybeLaunchArg = GetLaunchArgument();
+ NS_ENSURE_TRUE(maybeLaunchArg.isOk(), nullptr);
+ nsString launchArg = maybeLaunchArg.unwrap();
+
+ nsString launchArgWithoutAction = launchArg;
+
+ if (!mIsSystemPrincipal) {
+ // Unprivileged/content alerts can't have custom relaunch data.
+ NS_WARNING_ASSERTION(mOpaqueRelaunchData.IsEmpty(),
+ "unprivileged/content alert "
+ "should have trivial `mOpaqueRelaunchData`");
+ }
+
+ launchArg += u"\n"_ns + nsDependentString(kLaunchArgAction) + u"\n"_ns +
+ ActionArgsJSONString(u""_ns, mOpaqueRelaunchData);
+
+ success = SetAttribute(toastElement, HStringReference(L"launch"), launchArg);
+ NS_ENSURE_TRUE(success, nullptr);
+
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg).get()));
+
+ // Use newer toast layout for system (chrome-privileged) toasts. This gains us
+ // UI elements such as new image placement options (default image placement is
+ // larger and inline) and buttons.
+ if (mIsSystemPrincipal) {
+ ComPtr<IXmlNodeList> bindingElements;
+ hr = toastXml->GetElementsByTagName(HStringReference(L"binding").Get(),
+ &bindingElements);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlNode> bindingNodeRoot;
+ hr = bindingElements->Item(0, &bindingNodeRoot);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlElement> bindingElement;
+ hr = bindingNodeRoot.As(&bindingElement);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ success = SetAttribute(bindingElement, HStringReference(L"template"),
+ u"ToastGeneric"_ns);
+ NS_ENSURE_TRUE(success, nullptr);
+ }
+
+ ComPtr<IXmlElement> actions;
+ hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IXmlNode> actionsNode;
+ hr = actions.As(&actionsNode);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ nsCOMPtr<nsIStringBundleService> sbs =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ NS_ENSURE_TRUE(sbs, nullptr);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ sbs->CreateBundle("chrome://alerts/locale/alert.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_TRUE(bundle, nullptr);
+
+ if (!mHostPort.IsEmpty()) {
+ AutoTArray<nsString, 1> formatStrings = {mHostPort};
+
+ ComPtr<IXmlNode> urlTextNodeRoot;
+ hr = toastTextElements->Item(2, &urlTextNodeRoot);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ nsAutoString urlReference;
+ bundle->FormatStringFromName("source.label", formatStrings, urlReference);
+
+ success =
+ SetNodeValueString(urlReference, urlTextNodeRoot.Get(), toastXml.Get());
+ NS_ENSURE_TRUE(success, nullptr);
+
+ if (IsWin10AnniversaryUpdateOrLater()) {
+ ComPtr<IXmlElement> placementText;
+ hr = urlTextNodeRoot.As(&placementText);
+ if (SUCCEEDED(hr)) {
+ // placement is supported on Windows 10 Anniversary Update or later
+ SetAttribute(placementText, HStringReference(L"placement"),
+ u"attribution"_ns);
+ }
+ }
+
+ nsAutoString disableButtonTitle;
+ ns = bundle->FormatStringFromName("webActions.disableForOrigin.label",
+ formatStrings, disableButtonTitle);
+ NS_ENSURE_SUCCESS(ns, nullptr);
+
+ AddActionNode(toastXml, actionsNode, disableButtonTitle,
+ // TODO: launch into `about:preferences`?
+ launchArgWithoutAction, ActionArgsJSONString(u"snooze"_ns),
+ u"contextmenu"_ns);
+ }
+
+ bool wantSettings = true;
+#ifdef MOZ_BACKGROUNDTASKS
+ if (BackgroundTasks::IsBackgroundTaskMode()) {
+ // Notifications popped from a background task want to invoke Firefox with a
+ // different profile -- the default browsing profile. Don't link to Firefox
+ // settings in some different profile: the relevant Firefox settings won't
+ // take effect.
+ wantSettings = false;
+ }
+#endif
+ if (MOZ_LIKELY(wantSettings)) {
+ nsAutoString settingsButtonTitle;
+ bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
+ success = AddActionNode(
+ toastXml, actionsNode, settingsButtonTitle, launchArgWithoutAction,
+ // TODO: launch into `about:preferences`?
+ ActionArgsJSONString(u"settings"_ns), u"contextmenu"_ns);
+ NS_ENSURE_TRUE(success, nullptr);
+ }
+
+ for (const auto& action : mActions) {
+ // Bug 1778596: include per-action icon from image URL.
+ nsString title;
+ ns = action->GetTitle(title);
+ NS_ENSURE_SUCCESS(ns, nullptr);
+
+ nsString actionString;
+ ns = action->GetAction(actionString);
+ NS_ENSURE_SUCCESS(ns, nullptr);
+
+ nsString opaqueRelaunchData;
+ ns = action->GetOpaqueRelaunchData(opaqueRelaunchData);
+ NS_ENSURE_SUCCESS(ns, nullptr);
+
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("launchArgWithoutAction for '%s': '%s'",
+ NS_ConvertUTF16toUTF8(actionString).get(),
+ NS_ConvertUTF16toUTF8(launchArgWithoutAction).get()));
+
+ // Privileged/chrome alerts can have actions that are activated by Windows.
+ // Recognize these actions and enable these activations.
+ bool activationType(false);
+ ns = action->GetWindowsSystemActivationType(&activationType);
+ NS_ENSURE_SUCCESS(ns, nullptr);
+
+ nsString activationTypeString(
+ (mIsSystemPrincipal && activationType) ? u"system"_ns : u""_ns);
+
+ nsString actionArgs;
+ if (mIsSystemPrincipal && activationType) {
+ // Privileged/chrome alerts that are activated by Windows can't have
+ // custom relaunch data.
+ actionArgs = actionString;
+
+ NS_WARNING_ASSERTION(opaqueRelaunchData.IsEmpty(),
+ "action with `windowsSystemActivationType=true` "
+ "should have trivial `opaqueRelaunchData`");
+ } else {
+ actionArgs = ActionArgsJSONString(actionString, opaqueRelaunchData);
+ }
+
+ success = AddActionNode(toastXml, actionsNode, title,
+ /* launchArg */ launchArgWithoutAction,
+ /* actionArgs */ actionArgs,
+ /* actionPlacement */ u""_ns,
+ /* activationType */ activationTypeString);
+ NS_ENSURE_TRUE(success, nullptr);
+ }
+
+ // Windows ignores scenario=reminder added by mRequiredInteraction if
+ // there's no non-contextmenu action.
+ if (mRequireInteraction && !mActions.Length()) {
+ // `activationType="system" arguments="dismiss" content=""` provides
+ // localized text from Windows, but we support more locales than Windows
+ // does, so let's have our own.
+ nsTArray<nsCString> resIds = {
+ "toolkit/global/alert.ftl"_ns,
+ };
+ RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true);
+ IgnoredErrorResult rv;
+ nsAutoCString closeTitle;
+ l10n->FormatValueSync("notification-default-dismiss"_ns, {}, closeTitle,
+ rv);
+ NS_ENSURE_TRUE(!rv.Failed(), nullptr);
+
+ NS_ENSURE_TRUE(
+ AddActionNode(toastXml, actionsNode, NS_ConvertUTF8toUTF16(closeTitle),
+ u""_ns, u"dismiss"_ns, u""_ns, u"system"_ns),
+ nullptr);
+ }
+
+ ComPtr<IXmlNode> appendedChild;
+ hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ if (mIsSilent) {
+ ComPtr<IXmlNode> audioNode;
+ // Create <audio silent="true"/> for silent notifications.
+ ComPtr<IXmlElement> audio;
+ hr = toastXml->CreateElement(HStringReference(L"audio").Get(), &audio);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ SetAttribute(audio, HStringReference(L"silent"), u"true"_ns);
+
+ hr = audio.As(&audioNode);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+ hr = toastNodeRoot->AppendChild(audioNode.Get(), &appendedChild);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+ }
+
+ return toastXml;
+}
+
+nsresult ToastNotificationHandler::CreateToastXmlString(
+ const nsAString& aImageURL, nsAString& aString) {
+ HRESULT hr;
+
+ if (!aImageURL.IsEmpty()) {
+ // For testing: don't fetch and write image to disk, just include the URL.
+ mHasImage = true;
+ mImageUri.Assign(aImageURL);
+ }
+
+ ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument();
+ if (!toastXml) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ComPtr<IXmlNodeSerializer> ser;
+ hr = toastXml.As(&ser);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
+
+ HString data;
+ hr = ser->GetXml(data.GetAddressOf());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
+
+ uint32_t len = 0;
+ const wchar_t* rawData = data.GetRawBuffer(&len);
+ NS_ENSURE_TRUE(rawData, NS_ERROR_FAILURE);
+ aString.Assign(rawData, len);
+
+ return NS_OK;
+}
+
+bool ToastNotificationHandler::ShowAlert() {
+ if (!mBackend->IsActiveHandler(mName, this)) {
+ return false;
+ }
+
+ ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument();
+
+ if (!toastXml) {
+ return false;
+ }
+
+ return CreateWindowsNotificationFromXml(toastXml);
+}
+
+bool ToastNotificationHandler::IsPrivate() { return mInPrivateBrowsing; }
+
+void ToastNotificationHandler::HideAlert() {
+ if (mNotifier && mNotification) {
+ mNotifier->Hide(mNotification.Get());
+ }
+}
+
+bool ToastNotificationHandler::CreateWindowsNotificationFromXml(
+ ComPtr<IXmlDocument>& aXml) {
+ ComPtr<IToastNotificationFactory> factory;
+ HRESULT hr;
+
+ hr = GetActivationFactory(
+ HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification)
+ .Get(),
+ &factory);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = factory->CreateToastNotification(aXml.Get(), &mNotification);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ RefPtr<ToastNotificationHandler> self = this;
+
+ hr = mNotification->add_Activated(
+ Callback<ToastActivationHandler>([self](IToastNotification* aNotification,
+ IInspectable* aInspectable) {
+ return self->OnActivate(ComPtr<IToastNotification>(aNotification),
+ ComPtr<IInspectable>(aInspectable));
+ }).Get(),
+ &mActivatedToken);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = mNotification->add_Dismissed(
+ Callback<ToastDismissedHandler>([self](IToastNotification* aNotification,
+ IToastDismissedEventArgs* aArgs) {
+ return self->OnDismiss(ComPtr<IToastNotification>(aNotification),
+ ComPtr<IToastDismissedEventArgs>(aArgs));
+ }).Get(),
+ &mDismissedToken);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = mNotification->add_Failed(
+ Callback<ToastFailedHandler>([self](IToastNotification* aNotification,
+ IToastFailedEventArgs* aArgs) {
+ return self->OnFail(ComPtr<IToastNotification>(aNotification),
+ ComPtr<IToastFailedEventArgs>(aArgs));
+ }).Get(),
+ &mFailedToken);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ ComPtr<IToastNotification2> notification2;
+ hr = mNotification.As(&notification2);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ HString hTag;
+ hr = hTag.Set(mWindowsTag.get());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = notification2->put_Tag(hTag.Get());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics =
+ GetToastNotificationManagerStatics();
+ NS_ENSURE_TRUE(toastNotificationManagerStatics, false);
+
+ HString aumid;
+ hr = aumid.Set(mAumid.get());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+ hr = toastNotificationManagerStatics->CreateToastNotifierWithId(aumid.Get(),
+ &mNotifier);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = mNotifier->Show(mNotification.Get());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ if (mAlertListener) {
+ mAlertListener->Observe(nullptr, "alertshow", mCookie.get());
+ }
+
+ return true;
+}
+
+void ToastNotificationHandler::SendFinished() {
+ if (!mSentFinished && mAlertListener) {
+ mAlertListener->Observe(nullptr, "alertfinished", mCookie.get());
+ }
+
+ mSentFinished = true;
+}
+
+HRESULT
+ToastNotificationHandler::OnActivate(
+ const ComPtr<IToastNotification>& notification,
+ const ComPtr<IInspectable>& inspectable) {
+ MOZ_LOG(sWASLog, LogLevel::Info, ("OnActivate"));
+
+ if (mAlertListener) {
+ // Extract the `action` value from the argument string.
+ nsAutoString argumentsString;
+ nsAutoString actionString;
+ if (inspectable) {
+ ComPtr<IToastActivatedEventArgs> eventArgs;
+ HRESULT hr = inspectable.As(&eventArgs);
+ if (SUCCEEDED(hr)) {
+ HString arguments;
+ hr = eventArgs->get_Arguments(arguments.GetAddressOf());
+ if (SUCCEEDED(hr)) {
+ uint32_t len = 0;
+ const char16_t* buffer = (char16_t*)arguments.GetRawBuffer(&len);
+ if (buffer) {
+ MOZ_LOG(sWASLog, LogLevel::Info,
+ ("OnActivate: arguments: %s",
+ NS_ConvertUTF16toUTF8(buffer).get()));
+ argumentsString.Assign(buffer);
+
+ // Toast arguments are a newline separated key/value combination of
+ // launch arguments and an optional action argument provided as an
+ // argument to the toast's constructor. After the `action` key is
+ // found, the remainder of toast argument (including newlines) is
+ // the `action` value.
+ Tokenizer16 parse(buffer);
+ nsDependentSubstring token;
+
+ while (parse.ReadUntil(Tokenizer16::Token::NewLine(), token)) {
+ if (token == nsDependentString(kLaunchArgAction)) {
+ Unused << parse.ReadUntil(Tokenizer16::Token::EndOfFile(),
+ actionString);
+ } else {
+ // Next line is a value in a key/value pair, skip.
+ parse.SkipUntil(Tokenizer16::Token::NewLine());
+ }
+ // Skip newline.
+ Tokenizer16::Token unused;
+ Unused << parse.Next(unused);
+ }
+ }
+ }
+ }
+ }
+
+ if (argumentsString.EqualsLiteral("dismiss")) {
+ // XXX: Somehow Windows still fires OnActivate instead of OnDismiss for
+ // supposedly system managed dismiss button (with activationType=system
+ // and arguments=dismiss). We have to manually treat such callback as a
+ // dismiss action. For this case `arguments` only includes a keyword so we
+ // don't need to compare with a parsed result.
+ SendFinished();
+ } else if (actionString.EqualsLiteral("settings")) {
+ mAlertListener->Observe(nullptr, "alertsettingscallback", mCookie.get());
+ } else if (actionString.EqualsLiteral("snooze")) {
+ mAlertListener->Observe(nullptr, "alertdisablecallback", mCookie.get());
+ } else if (mClickable) {
+ // When clicking toast, focus moves to another process, but we want to set
+ // focus on Firefox process.
+ nsCOMPtr<nsIWindowMediator> winMediator(
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
+ if (winMediator) {
+ nsCOMPtr<mozIDOMWindowProxy> navWin;
+ winMediator->GetMostRecentWindow(u"navigator:browser",
+ getter_AddRefs(navWin));
+ if (navWin) {
+ nsCOMPtr<nsIWidget> widget =
+ WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin));
+ if (widget) {
+ SetForegroundWindow(
+ static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW)));
+ }
+ }
+ }
+
+ if (mHandleActions) {
+ Json::Value jsonData;
+ Json::Reader jsonReader;
+
+ if (jsonReader.parse(NS_ConvertUTF16toUTF8(actionString).get(),
+ jsonData, false)) {
+ char actionKey[] = "action";
+ if (jsonData.isMember(actionKey) && jsonData[actionKey].isString()) {
+ mAlertListener->Observe(
+ nullptr, "alertactioncallback",
+ NS_ConvertUTF8toUTF16(jsonData[actionKey].asCString()).get());
+ }
+ }
+ }
+
+ mAlertListener->Observe(nullptr, "alertclickcallback", mCookie.get());
+ }
+ }
+ mBackend->RemoveHandler(mName, this);
+ return S_OK;
+}
+
+// Returns `nullptr` if no such toast exists.
+/* static */ ComPtr<IToastNotification>
+ToastNotificationHandler::FindNotificationByTag(const nsAString& aWindowsTag,
+ const nsAString& aAumid) {
+ HRESULT hr = S_OK;
+
+ HString current_id;
+ current_id.Set(PromiseFlatString(aWindowsTag).get());
+
+ ComPtr<IToastNotificationManagerStatics> manager =
+ GetToastNotificationManagerStatics();
+ NS_ENSURE_TRUE(manager, nullptr);
+
+ ComPtr<IToastNotificationManagerStatics2> manager2;
+ hr = manager.As(&manager2);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IToastNotificationHistory> history;
+ hr = manager2->get_History(&history);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+ ComPtr<IToastNotificationHistory2> history2;
+ hr = history.As(&history2);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ ComPtr<IVectorView_ToastNotification> toasts;
+ hr = history2->GetHistoryWithId(
+ HStringReference(PromiseFlatString(aAumid).get()).Get(), &toasts);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ unsigned int hist_size;
+ hr = toasts->get_Size(&hist_size);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+ for (unsigned int i = 0; i < hist_size; i++) {
+ ComPtr<IToastNotification> hist_toast;
+ hr = toasts->GetAt(i, &hist_toast);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ ComPtr<IToastNotification2> hist_toast2;
+ hr = hist_toast.As(&hist_toast2);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ HString history_id;
+ hr = hist_toast2->get_Tag(history_id.GetAddressOf());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ // We can not directly compare IToastNotification objects; their IUnknown
+ // pointers should be equivalent but under inspection were not. Therefore we
+ // use the notification's tag instead.
+ if (current_id == history_id) {
+ return hist_toast;
+ }
+ }
+
+ return nullptr;
+}
+
+// A single toast message can receive multiple dismiss events, at most one for
+// the popup and at most one for the action center. We can't simply count
+// dismiss events as the user may have disabled either popups or action center
+// notifications, therefore we have to check if the toast remains in the history
+// (action center) to determine if the toast is fully dismissed.
+HRESULT
+ToastNotificationHandler::OnDismiss(
+ const ComPtr<IToastNotification>& notification,
+ const ComPtr<IToastDismissedEventArgs>& aArgs) {
+ ComPtr<IToastNotification2> notification2;
+ HRESULT hr = notification.As(&notification2);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
+
+ HString tagHString;
+ hr = notification2->get_Tag(tagHString.GetAddressOf());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
+
+ unsigned int len;
+ const wchar_t* tagPtr = tagHString.GetRawBuffer(&len);
+ nsAutoString tag(tagPtr, len);
+
+ if (FindNotificationByTag(tag, mAumid)) {
+ return S_OK;
+ }
+
+ SendFinished();
+ mBackend->RemoveHandler(mName, this);
+ return S_OK;
+}
+
+HRESULT
+ToastNotificationHandler::OnFail(const ComPtr<IToastNotification>& notification,
+ const ComPtr<IToastFailedEventArgs>& aArgs) {
+ HRESULT err;
+ aArgs->get_ErrorCode(&err);
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Error creating notification, error: %ld", err));
+
+ if (mHandleActions) {
+ mAlertListener->Observe(nullptr, "alerterror", mCookie.get());
+ }
+
+ SendFinished();
+ mBackend->RemoveHandler(mName, this);
+ return S_OK;
+}
+
+nsresult ToastNotificationHandler::TryShowAlert() {
+ if (NS_WARN_IF(!ShowAlert())) {
+ mBackend->RemoveHandler(mName, this);
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotificationHandler::OnImageMissing(nsISupports*) {
+ return TryShowAlert();
+}
+
+NS_IMETHODIMP
+ToastNotificationHandler::OnImageReady(nsISupports*, imgIRequest* aRequest) {
+ nsresult rv = AsyncSaveImage(aRequest);
+ if (NS_FAILED(rv)) {
+ return TryShowAlert();
+ }
+ return rv;
+}
+
+nsresult ToastNotificationHandler::AsyncSaveImage(imgIRequest* aRequest) {
+ nsresult rv =
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mImageFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mImageFile->Append(u"notificationimages"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mImageFile->Create(nsIFile::DIRECTORY_TYPE, 0500);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) {
+ return rv;
+ }
+
+ nsID uuid;
+ rv = nsID::GenerateUUIDInPlace(uuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NSID_TrimBracketsASCII uuidStr(uuid);
+ uuidStr.AppendLiteral(".png");
+ mImageFile->AppendNative(uuidStr);
+
+ nsCOMPtr<imgIContainer> imgContainer;
+ rv = aRequest->GetImage(getter_AddRefs(imgContainer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMainThreadPtrHandle<ToastNotificationHandler> self(
+ new nsMainThreadPtrHolder<ToastNotificationHandler>(
+ "ToastNotificationHandler", this));
+
+ nsCOMPtr<nsIFile> imageFile(mImageFile);
+ RefPtr<mozilla::gfx::SourceSurface> surface = imgContainer->GetFrame(
+ imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "ToastNotificationHandler::AsyncWriteImage",
+ [self, imageFile, surface]() -> void {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (surface) {
+ FILE* file = nullptr;
+ rv = imageFile->OpenANSIFileDesc("wb", &file);
+ if (NS_SUCCEEDED(rv)) {
+ rv = gfxUtils::EncodeSourceSurface(surface, ImageType::PNG, u""_ns,
+ gfxUtils::eBinaryEncode, file);
+ fclose(file);
+ }
+ }
+
+ nsCOMPtr<nsIRunnable> cbRunnable = NS_NewRunnableFunction(
+ "ToastNotificationHandler::AsyncWriteImageCb",
+ [self, rv]() -> void {
+ auto handler = const_cast<ToastNotificationHandler*>(self.get());
+ handler->OnWriteImageFinished(rv);
+ });
+
+ NS_DispatchToMainThread(cbRunnable);
+ });
+
+ return mBackend->BackgroundDispatch(r);
+}
+
+void ToastNotificationHandler::OnWriteImageFinished(nsresult rv) {
+ if (NS_SUCCEEDED(rv)) {
+ OnWriteImageSuccess();
+ }
+ TryShowAlert();
+}
+
+nsresult ToastNotificationHandler::OnWriteImageSuccess() {
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> fileURI;
+ rv = NS_NewFileURI(getter_AddRefs(fileURI), mImageFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString uriStr;
+ rv = fileURI->GetSpec(uriStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AppendUTF8toUTF16(uriStr, mImageUri);
+
+ mHasImage = true;
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/ToastNotificationHandler.h b/widget/windows/ToastNotificationHandler.h
new file mode 100644
index 0000000000..b3be34709c
--- /dev/null
+++ b/widget/windows/ToastNotificationHandler.h
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef ToastNotificationHandler_h__
+#define ToastNotificationHandler_h__
+
+#include <windows.ui.notifications.h>
+#include <windows.data.xml.dom.h>
+#include <wrl.h>
+#include "nsCOMPtr.h"
+#include "nsICancelable.h"
+#include "nsIFile.h"
+#include "nsIWindowsAlertsService.h"
+#include "nsString.h"
+#include "mozilla/Result.h"
+
+namespace mozilla {
+namespace widget {
+
+enum class ImagePlacement {
+ eInline,
+ eHero,
+ eIcon,
+};
+
+class ToastNotification;
+
+class ToastNotificationHandler final
+ : public nsIAlertNotificationImageListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIALERTNOTIFICATIONIMAGELISTENER
+
+ ToastNotificationHandler(
+ ToastNotification* backend, const nsAString& aumid,
+ nsIObserver* aAlertListener, const nsAString& aName,
+ const nsAString& aCookie, const nsAString& aTitle, const nsAString& aMsg,
+ const nsAString& aHostPort, bool aClickable, bool aRequireInteraction,
+ const nsTArray<RefPtr<nsIAlertAction>>& aActions, bool aIsSystemPrincipal,
+ const nsAString& aOpaqueRelaunchData, bool aInPrivateBrowsing,
+ bool aIsSilent, bool aHandlesActions = false,
+ ImagePlacement aImagePlacement = ImagePlacement::eInline)
+ : mBackend(backend),
+ mAumid(aumid),
+ mHasImage(false),
+ mAlertListener(aAlertListener),
+ mName(aName),
+ mCookie(aCookie),
+ mTitle(aTitle),
+ mMsg(aMsg),
+ mHostPort(aHostPort),
+ mClickable(aClickable),
+ mRequireInteraction(aRequireInteraction),
+ mInPrivateBrowsing(aInPrivateBrowsing),
+ mActions(aActions.Clone()),
+ mIsSystemPrincipal(aIsSystemPrincipal),
+ mOpaqueRelaunchData(aOpaqueRelaunchData),
+ mIsSilent(aIsSilent),
+ mSentFinished(!aAlertListener),
+ mHandleActions(aHandlesActions),
+ mImagePlacement(aImagePlacement) {}
+
+ nsresult InitAlertAsync(nsIAlertNotification* aAlert);
+
+ void OnWriteImageFinished(nsresult rv);
+
+ void HideAlert();
+ bool IsPrivate();
+
+ void UnregisterHandler();
+
+ nsString ActionArgsJSONString(
+ const nsString& aAction,
+ const nsString& aOpaqueRelaunchData /* = u""_ns */);
+ nsresult CreateToastXmlString(const nsAString& aImageURL, nsAString& aString);
+
+ nsresult GetWindowsTag(nsAString& aWindowsTag);
+ nsresult SetWindowsTag(const nsAString& aWindowsTag);
+
+ // Exposed for consumption by `ToastNotification.cpp`.
+ static nsresult FindNotificationDataForWindowsTag(
+ const nsAString& aWindowsTag, const nsAString& aAumid, bool& aFoundTag,
+ nsAString& aNotificationData);
+
+ protected:
+ virtual ~ToastNotificationHandler();
+
+ using IXmlDocument = ABI::Windows::Data::Xml::Dom::IXmlDocument;
+ using IToastNotifier = ABI::Windows::UI::Notifications::IToastNotifier;
+ using IToastNotification =
+ ABI::Windows::UI::Notifications::IToastNotification;
+ using IToastDismissedEventArgs =
+ ABI::Windows::UI::Notifications::IToastDismissedEventArgs;
+ using IToastFailedEventArgs =
+ ABI::Windows::UI::Notifications::IToastFailedEventArgs;
+ using ToastTemplateType = ABI::Windows::UI::Notifications::ToastTemplateType;
+ template <typename T>
+ using ComPtr = Microsoft::WRL::ComPtr<T>;
+
+ Result<nsString, nsresult> GetLaunchArgument();
+
+ ComPtr<IToastNotification> mNotification;
+ ComPtr<IToastNotifier> mNotifier;
+
+ RefPtr<ToastNotification> mBackend;
+
+ nsString mAumid;
+ nsString mWindowsTag;
+
+ nsCOMPtr<nsICancelable> mImageRequest;
+ nsCOMPtr<nsIFile> mImageFile;
+ nsString mImageUri;
+ bool mHasImage;
+
+ EventRegistrationToken mActivatedToken;
+ EventRegistrationToken mDismissedToken;
+ EventRegistrationToken mFailedToken;
+
+ nsCOMPtr<nsIObserver> mAlertListener;
+ nsString mName;
+ nsString mCookie;
+ nsString mTitle;
+ nsString mMsg;
+ nsString mHostPort;
+ bool mClickable;
+ bool mRequireInteraction;
+ bool mInPrivateBrowsing;
+ nsTArray<RefPtr<nsIAlertAction>> mActions;
+ bool mIsSystemPrincipal;
+ nsString mOpaqueRelaunchData;
+ bool mIsSilent;
+ bool mSentFinished;
+ bool mHandleActions;
+ ImagePlacement mImagePlacement;
+
+ nsresult TryShowAlert();
+ bool ShowAlert();
+ nsresult AsyncSaveImage(imgIRequest* aRequest);
+ nsresult OnWriteImageSuccess();
+ void SendFinished();
+
+ nsresult InitWindowsTag();
+ bool CreateWindowsNotificationFromXml(ComPtr<IXmlDocument>& aToastXml);
+ ComPtr<IXmlDocument> CreateToastXmlDocument();
+
+ HRESULT OnActivate(const ComPtr<IToastNotification>& notification,
+ const ComPtr<IInspectable>& inspectable);
+ HRESULT OnDismiss(const ComPtr<IToastNotification>& notification,
+ const ComPtr<IToastDismissedEventArgs>& aArgs);
+ HRESULT OnFail(const ComPtr<IToastNotification>& notification,
+ const ComPtr<IToastFailedEventArgs>& aArgs);
+
+ static ComPtr<IToastNotification> FindNotificationByTag(
+ const nsAString& aWindowsTag, const nsAString& aAumid);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/windows/ToastNotificationHeaderOnlyUtils.h b/widget/windows/ToastNotificationHeaderOnlyUtils.h
new file mode 100644
index 0000000000..dd777c0c32
--- /dev/null
+++ b/widget/windows/ToastNotificationHeaderOnlyUtils.h
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_ToastNotificationHeaderOnlyUtils_h
+#define mozilla_ToastNotificationHeaderOnlyUtils_h
+
+/**
+ * This header is intended for self-contained, header-only, utility code to
+ * share between Windows toast notification code in firefox.exe and
+ * notificationserver.dll.
+ */
+
+// Use XPCOM logging if we're in a XUL context, otherwise use Windows Event
+// logging.
+// NOTE: The `printf` `format` equivalent argument to `NOTIFY_LOG` is converted
+// to a wide string when outside of a XUL context. String format specifiers need
+// to specify they're a wide string with `%ls` or narrow string with `%hs`.
+#include "mozilla/Logging.h"
+#ifdef IMPL_LIBXUL
+namespace mozilla::widget {
+extern LazyLogModule sWASLog;
+} // namespace mozilla::widget
+# define NOTIFY_LOG(_level, _args) \
+ MOZ_LOG(mozilla::widget::sWASLog, _level, _args)
+#else
+# include "mozilla/WindowsEventLog.h"
+
+bool gVerbose = false;
+
+# define NOTIFY_LOG(_level, _args) \
+ if (gVerbose || _level == mozilla::LogLevel::Error) { \
+ POST_EXPAND_NOTIFY_LOG(MOZ_LOG_EXPAND_ARGS _args); \
+ }
+# define POST_EXPAND_NOTIFY_LOG(...) \
+ MOZ_WIN_EVENT_LOG_ERROR_MESSAGE( \
+ L"" MOZ_APP_DISPLAYNAME " Notification Server", L"" __VA_ARGS__)
+#endif
+
+#include <functional>
+#include <string>
+
+#include "nsWindowsHelpers.h"
+
+namespace mozilla::widget::toastnotification {
+
+const wchar_t kLaunchArgProgram[] = L"program";
+const wchar_t kLaunchArgProfile[] = L"profile";
+const wchar_t kLaunchArgTag[] = L"windowsTag";
+const wchar_t kLaunchArgLogging[] = L"logging";
+const wchar_t kLaunchArgAction[] = L"action";
+
+const DWORD kNotificationServerTimeoutMs = (10 * 1000);
+
+struct ToastNotificationPidMessage {
+ DWORD pid = 0;
+};
+
+struct ToastNotificationPermissionMessage {
+ DWORD setForegroundPermissionGranted = 0;
+};
+
+inline std::wstring GetNotificationPipeName(const wchar_t* aTag) {
+ // Prefix required by pipe API.
+ std::wstring pipeName(LR"(\\.\pipe\)");
+
+ pipeName += L"" MOZ_APP_NAME;
+ pipeName += aTag;
+
+ return pipeName;
+}
+
+inline bool WaitEventWithTimeout(const HANDLE& event) {
+ DWORD result = WaitForSingleObject(event, kNotificationServerTimeoutMs);
+
+ switch (result) {
+ case WAIT_OBJECT_0:
+ NOTIFY_LOG(LogLevel::Info, ("Pipe wait signaled"));
+ return true;
+ case WAIT_TIMEOUT:
+ NOTIFY_LOG(LogLevel::Warning, ("Pipe wait timed out"));
+ return false;
+ case WAIT_FAILED:
+ NOTIFY_LOG(LogLevel::Error,
+ ("Pipe wait failed, error %lu", GetLastError()));
+ return false;
+ case WAIT_ABANDONED:
+ NOTIFY_LOG(LogLevel::Error, ("Pipe wait abandoned"));
+ return false;
+ default:
+ NOTIFY_LOG(LogLevel::Error, ("Pipe wait unknown error"));
+ return false;
+ }
+}
+
+/* Handles running overlapped transactions for a Windows pipe. This function
+ * manages lifetimes of Event and OVERLAPPED objects to ensure they are not used
+ * while an overlapped operation is pending. */
+inline bool SyncDoOverlappedIOWithTimeout(
+ const nsAutoHandle& pipe, const size_t bytesExpected,
+ const std::function<BOOL(OVERLAPPED&)>& transactPipe) {
+ nsAutoHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
+ if (!event) {
+ NOTIFY_LOG(
+ LogLevel::Error,
+ ("Error creating pipe transaction event, error %lu", GetLastError()));
+ return false;
+ }
+
+ OVERLAPPED overlapped{};
+ overlapped.hEvent = event.get();
+ BOOL result = transactPipe(overlapped);
+
+ if (!result && GetLastError() != ERROR_IO_PENDING) {
+ NOTIFY_LOG(LogLevel::Error,
+ ("Error reading from pipe, error %lu", GetLastError()));
+ return false;
+ }
+
+ if (!WaitEventWithTimeout(overlapped.hEvent)) {
+ NOTIFY_LOG(LogLevel::Warning, ("Pipe transaction timed out, canceling "
+ "(transaction may still succeed)."));
+
+ CancelIo(pipe.get());
+
+ // Transaction may still succeed before cancellation is handled; fall
+ // through to normal handling.
+ }
+
+ DWORD bytesTransferred = 0;
+ // Pipe transfer has either been signaled or cancelled by this point, so it
+ // should be safe to wait on.
+ BOOL overlappedResult =
+ GetOverlappedResult(pipe.get(), &overlapped, &bytesTransferred, TRUE);
+
+ if (!overlappedResult) {
+ NOTIFY_LOG(
+ LogLevel::Error,
+ ("Error retrieving pipe overlapped result, error %lu", GetLastError()));
+ return false;
+ } else if (bytesTransferred != bytesExpected) {
+ NOTIFY_LOG(LogLevel::Error,
+ ("%lu bytes read from pipe, but %zu bytes expected",
+ bytesTransferred, bytesExpected));
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace mozilla::widget::toastnotification
+
+#endif // mozilla_ToastNotificationHeaderOnlyUtils_h
diff --git a/widget/windows/UrlmonHeaderOnlyUtils.h b/widget/windows/UrlmonHeaderOnlyUtils.h
new file mode 100644
index 0000000000..dd9209f78f
--- /dev/null
+++ b/widget/windows/UrlmonHeaderOnlyUtils.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_UrlmonHeaderOnlyUtils_h
+#define mozilla_UrlmonHeaderOnlyUtils_h
+
+#include "mozilla/ShellHeaderOnlyUtils.h"
+
+namespace mozilla {
+
+/**
+ * We used to validate a uri with SHParseDisplayName to mitigate the Windows
+ * bug (Bug 394974). However, Bug 1573051 revealed an issue that a fragment,
+ * a string following a hash mark (#), is dropped when we extract a string
+ * from PIDL. This is the intended behavior of Windows.
+ *
+ * To deal with the fragment issue as well as keeping our mitigation, we
+ * decided to use CreateUri to validate a uri string, but we also keep using
+ * SHParseDisplayName as a pre-check. This is because there are several
+ * cases where CreateUri succeeds while SHParseDisplayName fails such as
+ * a non-existent file: uri.
+ *
+ * To minimize the impact of introducing CreateUri into the validation logic,
+ * we try to mimic the logic of windows_storage!IUriToPidl (ieframe!IUriToPidl
+ * in Win7) which is executed behind SHParseDisplayName.
+ * What IUriToPidl does is:
+ * 1) If a given uri has a fragment, removes a fragment.
+ * 2) Takes an absolute uri if it's available in the given uri, otherwise
+ * takes a raw uri.
+ *
+ * As we need to get a full uri including a fragment, this function does 2).
+ */
+inline LauncherResult<_bstr_t> UrlmonValidateUri(const wchar_t* aUri) {
+ LauncherResult<UniqueAbsolutePidl> pidlResult = ShellParseDisplayName(aUri);
+ if (pidlResult.isErr()) {
+ return pidlResult.propagateErr();
+ }
+
+ // The value of |flags| is the same value as used in ieframe!_EnsureIUri in
+ // Win7, which is called behind SHParseDisplayName. In Win10, on the other
+ // hand, an flag 0x03000000 is also passed to CreateUri, but we don't
+ // specify it because it's undocumented and unknown.
+ constexpr DWORD flags =
+ Uri_CREATE_NO_DECODE_EXTRA_INFO | Uri_CREATE_CANONICALIZE |
+ Uri_CREATE_CRACK_UNKNOWN_SCHEMES | Uri_CREATE_PRE_PROCESS_HTML_URI |
+ Uri_CREATE_IE_SETTINGS;
+ RefPtr<IUri> uri;
+ HRESULT hr;
+ SAFECALL_URLMON_FUNC(CreateUri, aUri, flags, 0, getter_AddRefs(uri));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ _bstr_t bstrUri;
+
+ hr = uri->GetAbsoluteUri(bstrUri.GetAddress());
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ if (hr == S_FALSE) {
+ hr = uri->GetRawUri(bstrUri.GetAddress());
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+ }
+
+ return bstrUri;
+}
+
+} // namespace mozilla
+
+#endif // mozilla_UrlmonHeaderOnlyUtils_h
diff --git a/widget/windows/WidgetTraceEvent.cpp b/widget/windows/WidgetTraceEvent.cpp
new file mode 100644
index 0000000000..15bb4d720c
--- /dev/null
+++ b/widget/windows/WidgetTraceEvent.cpp
@@ -0,0 +1,121 @@
+/* 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/. */
+
+/*
+ * Windows widget support for event loop instrumentation.
+ * See toolkit/xre/EventTracer.cpp for more details.
+ */
+
+#include <stdio.h>
+#include <windows.h>
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/WidgetTraceEvent.h"
+#include "nsAppShellCID.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIAppShellService.h"
+#include "nsIBaseWindow.h"
+#include "nsIDocShell.h"
+#include "nsISupportsImpl.h"
+#include "nsIWidget.h"
+#include "nsIAppWindow.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsWindowDefs.h"
+
+namespace {
+
+// Used for signaling the background thread from the main thread.
+HANDLE sEventHandle = nullptr;
+
+// We need a runnable in order to find the hidden window on the main
+// thread.
+class HWNDGetter : public mozilla::Runnable {
+ public:
+ HWNDGetter() : Runnable("HWNDGetter"), hidden_window_hwnd(nullptr) {}
+
+ HWND hidden_window_hwnd;
+
+ NS_IMETHOD Run() override {
+ // Jump through some hoops to locate the hidden window.
+ nsCOMPtr<nsIAppShellService> appShell(
+ do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ nsCOMPtr<nsIAppWindow> hiddenWindow;
+
+ nsresult rv = appShell->GetHiddenWindow(getter_AddRefs(hiddenWindow));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell;
+ rv = hiddenWindow->GetDocShell(getter_AddRefs(docShell));
+ if (NS_FAILED(rv) || !docShell) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(docShell));
+
+ if (!baseWindow) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIWidget> widget;
+ baseWindow->GetMainWidget(getter_AddRefs(widget));
+
+ if (!widget) return NS_ERROR_FAILURE;
+
+ hidden_window_hwnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW);
+
+ return NS_OK;
+ }
+};
+
+HWND GetHiddenWindowHWND() {
+ // Need to dispatch this to the main thread because plenty of
+ // the things it wants to access are main-thread-only.
+ RefPtr<HWNDGetter> getter = new HWNDGetter();
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GetHiddenWindowHWND"_ns, mozilla::GetMainThreadSerialEventTarget(),
+ do_AddRef(getter));
+ return getter->hidden_window_hwnd;
+}
+
+} // namespace
+
+namespace mozilla {
+
+bool InitWidgetTracing() {
+ sEventHandle = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+ return sEventHandle != nullptr;
+}
+
+void CleanUpWidgetTracing() {
+ CloseHandle(sEventHandle);
+ sEventHandle = nullptr;
+}
+
+// This function is called from the main (UI) thread.
+void SignalTracerThread() {
+ if (sEventHandle != nullptr) SetEvent(sEventHandle);
+}
+
+// This function is called from the background tracer thread.
+bool FireAndWaitForTracerEvent() {
+ MOZ_ASSERT(sEventHandle, "Tracing not initialized!");
+
+ // First, try to find the hidden window.
+ static HWND hidden_window = nullptr;
+ if (hidden_window == nullptr) {
+ hidden_window = GetHiddenWindowHWND();
+ }
+
+ if (hidden_window == nullptr) return false;
+
+ // Post the tracer message into the hidden window's message queue,
+ // and then block until it's processed.
+ PostMessage(hidden_window, MOZ_WM_TRACE, 0, 0);
+ WaitForSingleObject(sEventHandle, INFINITE);
+ return true;
+}
+
+} // namespace mozilla
diff --git a/widget/windows/WinCompositorWidget.cpp b/widget/windows/WinCompositorWidget.cpp
new file mode 100644
index 0000000000..15957a1c3f
--- /dev/null
+++ b/widget/windows/WinCompositorWidget.cpp
@@ -0,0 +1,105 @@
+/* -*- 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 "WinCompositorWidget.h"
+
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "nsWindow.h"
+#include "VsyncDispatcher.h"
+#include "WinCompositorWindowThread.h"
+#include "VRShMem.h"
+
+#include <ddraw.h>
+
+namespace mozilla {
+namespace widget {
+
+using namespace mozilla::gfx;
+using namespace mozilla;
+
+WinCompositorWidget::WinCompositorWidget(
+ const WinCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions)
+ : CompositorWidget(aOptions),
+ mSetParentCompleted(false),
+ mWidgetKey(aInitData.widgetKey()),
+ mWnd(reinterpret_cast<HWND>(aInitData.hWnd())),
+ mCompositorWnds(nullptr, nullptr) {
+ MOZ_ASSERT(mWnd && ::IsWindow(mWnd));
+}
+
+WinCompositorWidget::~WinCompositorWidget() { DestroyCompositorWindow(); }
+
+uintptr_t WinCompositorWidget::GetWidgetKey() { return mWidgetKey; }
+
+void WinCompositorWidget::EnsureCompositorWindow() {
+ if (mCompositorWnds.mCompositorWnd || mCompositorWnds.mInitialParentWnd) {
+ return;
+ }
+
+ mCompositorWnds = WinCompositorWindowThread::CreateCompositorWindow();
+ UpdateCompositorWnd(mCompositorWnds.mCompositorWnd, mWnd);
+
+ MOZ_ASSERT(mCompositorWnds.mCompositorWnd);
+ MOZ_ASSERT(mCompositorWnds.mInitialParentWnd);
+}
+
+void WinCompositorWidget::DestroyCompositorWindow() {
+ if (!mCompositorWnds.mCompositorWnd && !mCompositorWnds.mInitialParentWnd) {
+ return;
+ }
+ WinCompositorWindowThread::DestroyCompositorWindow(mCompositorWnds);
+ mCompositorWnds = WinCompositorWnds(nullptr, nullptr);
+}
+
+void WinCompositorWidget::UpdateCompositorWndSizeIfNecessary() {
+ if (!mCompositorWnds.mCompositorWnd) {
+ return;
+ }
+
+ LayoutDeviceIntSize size = GetClientSize();
+ if (mLastCompositorWndSize == size) {
+ return;
+ }
+
+ // This code is racing with the compositor, which needs to reparent the
+ // compositor surface to the actual window (mWnd). To avoid racing mutations,
+ // we refuse to proceed until ::SetParent() is called in the parent process.
+ // After the ::SetParent() call, composition is scheduled in
+ // CompositorWidgetParent::UpdateCompositorWnd().
+ if (!mSetParentCompleted) {
+ // ::SetParent() is not completed yet.
+ return;
+ }
+
+ MOZ_ASSERT(mWnd == ::GetParent(mCompositorWnds.mCompositorWnd));
+
+ // Force a resize and redraw (but not a move, activate, etc.).
+ if (!::SetWindowPos(
+ mCompositorWnds.mCompositorWnd, nullptr, 0, 0, size.width,
+ size.height,
+ SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOOWNERZORDER | SWP_NOZORDER)) {
+ return;
+ }
+
+ mLastCompositorWndSize = size;
+}
+
+// Creates a new instance of FxROutputHandler so that this compositor widget
+// can send its output to Firefox Reality for Desktop.
+void WinCompositorWidget::RequestFxrOutput() {
+ MOZ_ASSERT(mFxrHandler == nullptr);
+
+ mFxrHandler.reset(new FxROutputHandler());
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WinCompositorWidget.h b/widget/windows/WinCompositorWidget.h
new file mode 100644
index 0000000000..fef967380c
--- /dev/null
+++ b/widget/windows/WinCompositorWidget.h
@@ -0,0 +1,103 @@
+/* -*- 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/. */
+
+#ifndef widget_windows_WinCompositorWidget_h
+#define widget_windows_WinCompositorWidget_h
+
+#include "CompositorWidget.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/gfx/CriticalSection.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/widget/WinCompositorWindowThread.h"
+#include "FxROutputHandler.h"
+#include "nsIWidget.h"
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+class PlatformCompositorWidgetDelegate : public CompositorWidgetDelegate {
+ public:
+ // Callbacks for nsWindow.
+ virtual void EnterPresentLock() = 0;
+ virtual void LeavePresentLock() = 0;
+ virtual void OnDestroyWindow() = 0;
+ virtual bool OnWindowResize(const LayoutDeviceIntSize& aSize) = 0;
+ virtual void OnWindowModeChange(nsSizeMode aSizeMode) = 0;
+
+ // Transparency handling.
+ virtual void UpdateTransparency(TransparencyMode aMode) = 0;
+ virtual void ClearTransparentWindow() = 0;
+
+ // Deliver visibility info
+ virtual void NotifyVisibilityUpdated(nsSizeMode aSizeMode,
+ bool aIsFullyOccluded) = 0;
+
+ // CompositorWidgetDelegate Overrides
+
+ PlatformCompositorWidgetDelegate* AsPlatformSpecificDelegate() override {
+ return this;
+ }
+};
+
+class WinCompositorWidgetInitData;
+
+// This is the Windows-specific implementation of CompositorWidget. For
+// the most part it only requires an HWND, however it maintains extra state
+// for transparent windows, as well as for synchronizing WM_SETTEXT messages
+// with the compositor.
+class WinCompositorWidget : public CompositorWidget {
+ public:
+ WinCompositorWidget(const WinCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions);
+ ~WinCompositorWidget() override;
+
+ // CompositorWidget Overrides
+
+ uintptr_t GetWidgetKey() override;
+ WinCompositorWidget* AsWindows() override { return this; }
+
+ HWND GetHwnd() const {
+ return mCompositorWnds.mCompositorWnd ? mCompositorWnds.mCompositorWnd
+ : mWnd;
+ }
+
+ HWND GetCompositorHwnd() const { return mCompositorWnds.mCompositorWnd; }
+
+ void EnsureCompositorWindow();
+ void DestroyCompositorWindow();
+ void UpdateCompositorWndSizeIfNecessary();
+
+ void RequestFxrOutput();
+ bool HasFxrOutputHandler() const { return mFxrHandler != nullptr; }
+ FxROutputHandler* GetFxrOutputHandler() const { return mFxrHandler.get(); }
+
+ virtual nsSizeMode GetWindowSizeMode() const = 0;
+ virtual bool GetWindowIsFullyOccluded() const = 0;
+
+ virtual void UpdateCompositorWnd(const HWND aCompositorWnd,
+ const HWND aParentWnd) = 0;
+ virtual void SetRootLayerTreeID(const layers::LayersId& aRootLayerTreeId) = 0;
+
+ protected:
+ bool mSetParentCompleted;
+
+ private:
+ uintptr_t mWidgetKey;
+ HWND mWnd;
+
+ WinCompositorWnds mCompositorWnds;
+ LayoutDeviceIntSize mLastCompositorWndSize;
+
+ UniquePtr<FxROutputHandler> mFxrHandler;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_windows_WinCompositorWidget_h
diff --git a/widget/windows/WinCompositorWindowThread.cpp b/widget/windows/WinCompositorWindowThread.cpp
new file mode 100644
index 0000000000..3b06c098e6
--- /dev/null
+++ b/widget/windows/WinCompositorWindowThread.cpp
@@ -0,0 +1,294 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "base/platform_thread.h"
+#include "WinCompositorWindowThread.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/layers/SynchronousTask.h"
+#include "mozilla/StaticPtr.h"
+#include "transport/runnable_utils.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+namespace mozilla {
+namespace widget {
+
+static StaticRefPtr<WinCompositorWindowThread> sWinCompositorWindowThread;
+
+/// A window procedure that logs when an input event is received to the gfx
+/// error log
+///
+/// This is done because this window is supposed to be WM_DISABLED, but
+/// malfunctioning software may still end up targetting this window. If that
+/// happens, it's almost-certainly a bug and should be brought to the attention
+/// of the developers that are debugging the issue.
+static LRESULT CALLBACK InputEventRejectingWindowProc(HWND window, UINT msg,
+ WPARAM wparam,
+ LPARAM lparam) {
+ switch (msg) {
+ case WM_LBUTTONDOWN:
+ case WM_LBUTTONUP:
+ case WM_RBUTTONDOWN:
+ case WM_RBUTTONUP:
+ case WM_MBUTTONDOWN:
+ case WM_MBUTTONUP:
+ case WM_MOUSEWHEEL:
+ case WM_MOUSEHWHEEL:
+ case WM_MOUSEMOVE:
+ case WM_KEYDOWN:
+ case WM_KEYUP:
+ case WM_SYSKEYDOWN:
+ case WM_SYSKEYUP:
+ gfxCriticalNoteOnce
+ << "The compositor window received an input event even though it's "
+ "disabled. There is likely malfunctioning "
+ "software on the user's machine.";
+
+ break;
+ default:
+ break;
+ }
+ return ::DefWindowProcW(window, msg, wparam, lparam);
+}
+
+WinCompositorWindowThread::WinCompositorWindowThread(base::Thread* aThread)
+ : mThread(aThread), mMonitor("WinCompositorWindowThread") {}
+
+/* static */
+WinCompositorWindowThread* WinCompositorWindowThread::Get() {
+ if (!sWinCompositorWindowThread ||
+ sWinCompositorWindowThread->mHasAttemptedShutdown) {
+ return nullptr;
+ }
+ return sWinCompositorWindowThread;
+}
+
+/* static */
+void WinCompositorWindowThread::Start() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ base::Thread::Options options;
+ // HWND requests ui thread.
+ options.message_loop_type = MessageLoop::TYPE_UI;
+
+ if (sWinCompositorWindowThread) {
+ // Try to reuse the thread, which involves stopping and restarting it.
+ sWinCompositorWindowThread->mThread->Stop();
+ if (sWinCompositorWindowThread->mThread->StartWithOptions(options)) {
+ // Success!
+ sWinCompositorWindowThread->mHasAttemptedShutdown = false;
+ return;
+ }
+ // Restart failed, so null out our sWinCompositorWindowThread and
+ // try again with a new thread. This will cause the old singleton
+ // instance to be deallocated, which will destroy its mThread as well.
+ sWinCompositorWindowThread = nullptr;
+ }
+
+ base::Thread* thread = new base::Thread("WinCompositor");
+ if (!thread->StartWithOptions(options)) {
+ delete thread;
+ return;
+ }
+
+ sWinCompositorWindowThread = new WinCompositorWindowThread(thread);
+}
+
+/* static */
+void WinCompositorWindowThread::ShutDown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(sWinCompositorWindowThread);
+
+ sWinCompositorWindowThread->mHasAttemptedShutdown = true;
+
+ // Our thread could hang while we're waiting for it to stop.
+ // Since we're shutting down, that's not a critical problem.
+ // We set a reasonable amount of time to wait for shutdown,
+ // and if it succeeds within that time, we correctly stop
+ // our thread by nulling out the refptr, which will cause it
+ // to be deallocated and join the thread. If it times out,
+ // we do nothing, which means that the thread will not be
+ // joined and sWinCompositorWindowThread memory will leak.
+ CVStatus status;
+ {
+ // It's important to hold the lock before posting the
+ // runnable. This ensures that the runnable can't begin
+ // until we've started our Wait, which prevents us from
+ // Waiting on a monitor that has already been notified.
+ MonitorAutoLock lock(sWinCompositorWindowThread->mMonitor);
+
+ static const TimeDuration TIMEOUT = TimeDuration::FromSeconds(2.0);
+ RefPtr<Runnable> runnable =
+ NewRunnableMethod("WinCompositorWindowThread::ShutDownTask",
+ sWinCompositorWindowThread.get(),
+ &WinCompositorWindowThread::ShutDownTask);
+ Loop()->PostTask(runnable.forget());
+
+ // Monitor uses SleepConditionVariableSRW, which can have
+ // spurious wakeups which are reported as timeouts, so we
+ // check timestamps to ensure that we've waited as long we
+ // intended to. If we wake early, we don't bother calculating
+ // a precise amount for the next wait; we just wait the same
+ // amount of time. This means timeout might happen after as
+ // much as 2x the TIMEOUT time.
+ TimeStamp timeStart = TimeStamp::NowLoRes();
+ do {
+ status = sWinCompositorWindowThread->mMonitor.Wait(TIMEOUT);
+ } while ((status == CVStatus::Timeout) &&
+ ((TimeStamp::NowLoRes() - timeStart) < TIMEOUT));
+ }
+
+ if (status == CVStatus::NoTimeout) {
+ sWinCompositorWindowThread = nullptr;
+ }
+}
+
+void WinCompositorWindowThread::ShutDownTask() {
+ MonitorAutoLock lock(mMonitor);
+
+ MOZ_ASSERT(IsInCompositorWindowThread());
+ mMonitor.NotifyAll();
+}
+
+/* static */
+MessageLoop* WinCompositorWindowThread::Loop() {
+ return sWinCompositorWindowThread
+ ? sWinCompositorWindowThread->mThread->message_loop()
+ : nullptr;
+}
+
+/* static */
+bool WinCompositorWindowThread::IsInCompositorWindowThread() {
+ return sWinCompositorWindowThread &&
+ sWinCompositorWindowThread->mThread->thread_id() ==
+ PlatformThread::CurrentId();
+}
+
+const wchar_t kClassNameCompositorInitalParent[] =
+ L"MozillaCompositorInitialParentClass";
+const wchar_t kClassNameCompositor[] = L"MozillaCompositorWindowClass";
+
+ATOM g_compositor_inital_parent_window_class;
+ATOM g_compositor_window_class;
+
+// This runs on the window owner thread.
+void InitializeInitialParentWindowClass() {
+ if (g_compositor_inital_parent_window_class) {
+ return;
+ }
+
+ WNDCLASSW wc;
+ wc.style = 0;
+ wc.lpfnWndProc = ::DefWindowProcW;
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hInstance = GetModuleHandle(nullptr);
+ wc.hIcon = nullptr;
+ wc.hCursor = nullptr;
+ wc.hbrBackground = nullptr;
+ wc.lpszMenuName = nullptr;
+ wc.lpszClassName = kClassNameCompositorInitalParent;
+ g_compositor_inital_parent_window_class = ::RegisterClassW(&wc);
+}
+
+// This runs on the window owner thread.
+void InitializeWindowClass() {
+ if (g_compositor_window_class) {
+ return;
+ }
+
+ WNDCLASSW wc;
+ wc.style = CS_OWNDC;
+ wc.lpfnWndProc = InputEventRejectingWindowProc;
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hInstance = GetModuleHandle(nullptr);
+ wc.hIcon = nullptr;
+ wc.hCursor = nullptr;
+ wc.hbrBackground = nullptr;
+ wc.lpszMenuName = nullptr;
+ wc.lpszClassName = kClassNameCompositor;
+ g_compositor_window_class = ::RegisterClassW(&wc);
+}
+
+/* static */
+WinCompositorWnds WinCompositorWindowThread::CreateCompositorWindow() {
+ MOZ_ASSERT(Loop());
+
+ if (!Loop()) {
+ return WinCompositorWnds(nullptr, nullptr);
+ }
+
+ layers::SynchronousTask task("Create compositor window");
+
+ HWND initialParentWnd = nullptr;
+ HWND compositorWnd = nullptr;
+
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "WinCompositorWindowThread::CreateCompositorWindow::Runnable", [&]() {
+ layers::AutoCompleteTask complete(&task);
+
+ InitializeInitialParentWindowClass();
+ InitializeWindowClass();
+
+ // Create initial parent window.
+ // We could not directly create a compositor window with a main window
+ // as parent window, so instead create it with a temporary placeholder
+ // parent. Its parent is set as main window in UI process.
+ initialParentWnd =
+ ::CreateWindowEx(WS_EX_TOOLWINDOW, kClassNameCompositorInitalParent,
+ nullptr, WS_POPUP | WS_DISABLED, 0, 0, 1, 1,
+ nullptr, 0, GetModuleHandle(nullptr), 0);
+ if (!initialParentWnd) {
+ gfxCriticalNoteOnce << "Inital parent window failed "
+ << ::GetLastError();
+ return;
+ }
+
+ DWORD extendedStyle = WS_EX_NOPARENTNOTIFY | WS_EX_NOREDIRECTIONBITMAP;
+
+ if (!StaticPrefs::apz_windows_force_disable_direct_manipulation()) {
+ extendedStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT;
+ }
+
+ compositorWnd = ::CreateWindowEx(
+ extendedStyle, kClassNameCompositor, nullptr,
+ WS_CHILDWINDOW | WS_DISABLED | WS_VISIBLE, 0, 0, 1, 1,
+ initialParentWnd, 0, GetModuleHandle(nullptr), 0);
+ if (!compositorWnd) {
+ gfxCriticalNoteOnce << "Compositor window failed "
+ << ::GetLastError();
+ }
+ });
+
+ Loop()->PostTask(runnable.forget());
+
+ task.Wait();
+
+ return WinCompositorWnds(compositorWnd, initialParentWnd);
+}
+
+/* static */
+void WinCompositorWindowThread::DestroyCompositorWindow(
+ WinCompositorWnds aWnds) {
+ MOZ_ASSERT(aWnds.mCompositorWnd);
+ MOZ_ASSERT(aWnds.mInitialParentWnd);
+ MOZ_ASSERT(Loop());
+
+ if (!Loop()) {
+ return;
+ }
+
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "WinCompositorWidget::CreateNativeWindow::Runnable", [aWnds]() {
+ ::DestroyWindow(aWnds.mCompositorWnd);
+ ::DestroyWindow(aWnds.mInitialParentWnd);
+ });
+
+ Loop()->PostTask(runnable.forget());
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WinCompositorWindowThread.h b/widget/windows/WinCompositorWindowThread.h
new file mode 100644
index 0000000000..372e3d91c2
--- /dev/null
+++ b/widget/windows/WinCompositorWindowThread.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef widget_windows_WinCompositorWindowThread_h
+#define widget_windows_WinCompositorWindowThread_h
+
+#include "base/thread.h"
+#include "base/message_loop.h"
+#include "mozilla/Monitor.h"
+
+namespace mozilla {
+namespace widget {
+
+struct WinCompositorWnds {
+ HWND mCompositorWnd;
+ HWND mInitialParentWnd;
+ WinCompositorWnds(HWND aCompositorWnd, HWND aInitialParentWnd)
+ : mCompositorWnd(aCompositorWnd), mInitialParentWnd(aInitialParentWnd) {}
+};
+
+class WinCompositorWindowThread final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD(
+ WinCompositorWindowThread)
+
+ public:
+ /// Can be called from any thread.
+ static WinCompositorWindowThread* Get();
+
+ /// Can only be called from the main thread.
+ static void Start();
+
+ /// Can only be called from the main thread.
+ static void ShutDown();
+
+ /// Can be called from any thread.
+ static MessageLoop* Loop();
+
+ /// Can be called from any thread.
+ static bool IsInCompositorWindowThread();
+
+ /// Can be called from any thread.
+ static WinCompositorWnds CreateCompositorWindow();
+
+ /// Can be called from any thread.
+ static void DestroyCompositorWindow(WinCompositorWnds aWnds);
+
+ private:
+ explicit WinCompositorWindowThread(base::Thread* aThread);
+ ~WinCompositorWindowThread() {}
+
+ void ShutDownTask();
+
+ UniquePtr<base::Thread> const mThread;
+ Monitor mMonitor;
+
+ // Has ShutDown been called on us? We might have survived if our thread join
+ // timed out.
+ bool mHasAttemptedShutdown = false;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_windows_WinCompositorWindowThread_h
diff --git a/widget/windows/WinEventObserver.cpp b/widget/windows/WinEventObserver.cpp
new file mode 100644
index 0000000000..7abac8a59a
--- /dev/null
+++ b/widget/windows/WinEventObserver.cpp
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 <windows.h>
+#include <winuser.h>
+#include <wtsapi32.h>
+
+#include "WinEventObserver.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPtr.h"
+#include "nsHashtablesFwd.h"
+#include "nsdefs.h"
+
+namespace mozilla::widget {
+
+LazyLogModule gWinEventObserverLog("WinEventObserver");
+#define LOG(...) MOZ_LOG(gWinEventObserverLog, LogLevel::Info, (__VA_ARGS__))
+
+// static
+StaticRefPtr<WinEventHub> WinEventHub::sInstance;
+
+// static
+bool WinEventHub::Ensure() {
+ if (sInstance) {
+ return true;
+ }
+
+ LOG("WinEventHub::Ensure()");
+
+ RefPtr<WinEventHub> instance = new WinEventHub();
+ if (!instance->Initialize()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return false;
+ }
+ sInstance = instance;
+ ClearOnShutdown(&sInstance);
+ return true;
+}
+
+WinEventHub::WinEventHub() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("WinEventHub::WinEventHub()");
+}
+
+WinEventHub::~WinEventHub() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mObservers.IsEmpty());
+ LOG("WinEventHub::~WinEventHub()");
+
+ if (mHWnd) {
+ ::DestroyWindow(mHWnd);
+ mHWnd = nullptr;
+ }
+}
+
+bool WinEventHub::Initialize() {
+ WNDCLASSW wc;
+ HMODULE hSelf = ::GetModuleHandle(nullptr);
+
+ if (!GetClassInfoW(hSelf, L"MozillaWinEventHubClass", &wc)) {
+ ZeroMemory(&wc, sizeof(WNDCLASSW));
+ wc.hInstance = hSelf;
+ wc.lpfnWndProc = WinEventProc;
+ wc.lpszClassName = L"MozillaWinEventHubClass";
+ RegisterClassW(&wc);
+ }
+
+ mHWnd = ::CreateWindowW(L"MozillaWinEventHubClass", L"WinEventHub", 0, 0, 0,
+ 0, 0, nullptr, nullptr, hSelf, nullptr);
+ if (!mHWnd) {
+ return false;
+ }
+
+ return true;
+}
+
+// static
+LRESULT CALLBACK WinEventHub::WinEventProc(HWND aHwnd, UINT aMsg,
+ WPARAM aWParam, LPARAM aLParam) {
+ if (sInstance) {
+ sInstance->ProcessWinEventProc(aHwnd, aMsg, aWParam, aLParam);
+ }
+ return ::DefWindowProc(aHwnd, aMsg, aWParam, aLParam);
+}
+
+void WinEventHub::ProcessWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam,
+ LPARAM aLParam) {
+ for (const auto& observer : mObservers) {
+ observer->OnWinEventProc(aHwnd, aMsg, aWParam, aLParam);
+ }
+}
+
+void WinEventHub::AddObserver(WinEventObserver* aObserver) {
+ LOG("WinEventHub::AddObserver() aObserver %p", aObserver);
+
+ mObservers.Insert(aObserver);
+}
+
+void WinEventHub::RemoveObserver(WinEventObserver* aObserver) {
+ LOG("WinEventHub::RemoveObserver() aObserver %p", aObserver);
+
+ mObservers.Remove(aObserver);
+}
+
+WinEventObserver::~WinEventObserver() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDestroyed);
+}
+
+void WinEventObserver::Destroy() {
+ LOG("WinEventObserver::Destroy() this %p", this);
+
+ WinEventHub::Get()->RemoveObserver(this);
+ mDestroyed = true;
+}
+
+// static
+already_AddRefed<DisplayStatusObserver> DisplayStatusObserver::Create(
+ DisplayStatusListener* aListener) {
+ if (!WinEventHub::Ensure()) {
+ return nullptr;
+ }
+ RefPtr<DisplayStatusObserver> observer = new DisplayStatusObserver(aListener);
+ WinEventHub::Get()->AddObserver(observer);
+ return observer.forget();
+}
+
+DisplayStatusObserver::DisplayStatusObserver(DisplayStatusListener* aListener)
+ : mListener(aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("DisplayStatusObserver::DisplayStatusObserver() this %p", this);
+
+ mDisplayStatusHandle = ::RegisterPowerSettingNotification(
+ WinEventHub::Get()->GetWnd(), &GUID_SESSION_DISPLAY_STATUS,
+ DEVICE_NOTIFY_WINDOW_HANDLE);
+}
+
+DisplayStatusObserver::~DisplayStatusObserver() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("DisplayStatusObserver::~DisplayStatusObserver() this %p", this);
+
+ if (mDisplayStatusHandle) {
+ ::UnregisterPowerSettingNotification(mDisplayStatusHandle);
+ mDisplayStatusHandle = nullptr;
+ }
+}
+
+void DisplayStatusObserver::OnWinEventProc(HWND aHwnd, UINT aMsg,
+ WPARAM aWParam, LPARAM aLParam) {
+ if (aMsg == WM_POWERBROADCAST && aWParam == PBT_POWERSETTINGCHANGE) {
+ POWERBROADCAST_SETTING* setting = (POWERBROADCAST_SETTING*)aLParam;
+ if (setting &&
+ ::IsEqualGUID(setting->PowerSetting, GUID_SESSION_DISPLAY_STATUS) &&
+ setting->DataLength == sizeof(DWORD)) {
+ bool displayOn = PowerMonitorOff !=
+ static_cast<MONITOR_DISPLAY_STATE>(setting->Data[0]);
+
+ LOG("DisplayStatusObserver::OnWinEventProc() displayOn %d this %p",
+ displayOn, this);
+ mListener->OnDisplayStateChanged(displayOn);
+ }
+ }
+}
+
+// static
+already_AddRefed<SessionChangeObserver> SessionChangeObserver::Create(
+ SessionChangeListener* aListener) {
+ if (!WinEventHub::Ensure()) {
+ return nullptr;
+ }
+ RefPtr<SessionChangeObserver> observer = new SessionChangeObserver(aListener);
+ WinEventHub::Get()->AddObserver(observer);
+ return observer.forget();
+}
+
+SessionChangeObserver::SessionChangeObserver(SessionChangeListener* aListener)
+ : mListener(aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("SessionChangeObserver::SessionChangeObserver() this %p", this);
+
+ auto hwnd = WinEventHub::Get()->GetWnd();
+ DebugOnly<BOOL> wtsRegistered =
+ ::WTSRegisterSessionNotification(hwnd, NOTIFY_FOR_THIS_SESSION);
+ NS_ASSERTION(wtsRegistered, "WTSRegisterSessionNotification failed!\n");
+}
+SessionChangeObserver::~SessionChangeObserver() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("SessionChangeObserver::~SessionChangeObserver() this %p", this);
+
+ auto hwnd = WinEventHub::Get()->GetWnd();
+ // Unregister notifications from terminal services
+ ::WTSUnRegisterSessionNotification(hwnd);
+}
+
+void SessionChangeObserver::OnWinEventProc(HWND aHwnd, UINT aMsg,
+ WPARAM aWParam, LPARAM aLParam) {
+ if (aMsg == WM_WTSSESSION_CHANGE &&
+ (aWParam == WTS_SESSION_LOCK || aWParam == WTS_SESSION_UNLOCK)) {
+ Maybe<bool> isCurrentSession;
+ DWORD currentSessionId = 0;
+ if (!::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId)) {
+ isCurrentSession = Nothing();
+ } else {
+ LOG("SessionChangeObserver::OnWinEventProc() aWParam %zu aLParam "
+ "%" PRIdLPTR
+ " "
+ "currentSessionId %lu this %p",
+ aWParam, aLParam, currentSessionId, this);
+
+ isCurrentSession = Some(static_cast<DWORD>(aLParam) == currentSessionId);
+ }
+ mListener->OnSessionChange(aWParam, isCurrentSession);
+ }
+}
+
+#undef LOG
+
+} // namespace mozilla::widget
diff --git a/widget/windows/WinEventObserver.h b/widget/windows/WinEventObserver.h
new file mode 100644
index 0000000000..6a267871dd
--- /dev/null
+++ b/widget/windows/WinEventObserver.h
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef widget_windows_WinEventObserver_h
+#define widget_windows_WinEventObserver_h
+
+#include <windows.h>
+
+#include "mozilla/Maybe.h"
+#include "nsISupportsImpl.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+namespace widget {
+
+class WinEventObserver {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinEventObserver)
+ public:
+ virtual void OnWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam,
+ LPARAM aLParam) {}
+ virtual void Destroy();
+
+ protected:
+ virtual ~WinEventObserver();
+
+ bool mDestroyed = false;
+};
+
+// Uses singleton window to observe events like display status and session
+// change.
+class WinEventHub final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinEventHub)
+
+ public:
+ // Returns true if the singleton exists (or was created). Will return
+ // false if the singleton couldn't be created, in which case a call
+ // to Get() will return a nullptr. It is safe to call this function
+ // repeatedly.
+ static bool Ensure();
+ static RefPtr<WinEventHub> Get() { return sInstance; }
+
+ void AddObserver(WinEventObserver* aObserver);
+ void RemoveObserver(WinEventObserver* aObserver);
+
+ HWND GetWnd() { return mHWnd; }
+
+ private:
+ WinEventHub();
+ ~WinEventHub();
+
+ static LRESULT CALLBACK WinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam,
+ LPARAM aLParam);
+
+ bool Initialize();
+ void ProcessWinEventProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
+
+ HWND mHWnd = nullptr;
+ nsTHashSet<nsRefPtrHashKey<WinEventObserver>> mObservers;
+
+ static StaticRefPtr<WinEventHub> sInstance;
+};
+
+class DisplayStatusListener {
+ public:
+ virtual void OnDisplayStateChanged(bool aDisplayOn) = 0;
+};
+
+// Observe Display on/off event
+class DisplayStatusObserver final : public WinEventObserver {
+ public:
+ static already_AddRefed<DisplayStatusObserver> Create(
+ DisplayStatusListener* aListener);
+
+ private:
+ explicit DisplayStatusObserver(DisplayStatusListener* aListener);
+ virtual ~DisplayStatusObserver();
+ void OnWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam,
+ LPARAM aLParam) override;
+
+ DisplayStatusListener* mListener;
+
+ HPOWERNOTIFY mDisplayStatusHandle = nullptr;
+};
+
+class SessionChangeListener {
+ public:
+ virtual void OnSessionChange(WPARAM aStatusCode,
+ Maybe<bool> aIsCurrentSession) = 0;
+};
+
+// Observe session lock/unlock event
+class SessionChangeObserver : public WinEventObserver {
+ public:
+ static already_AddRefed<SessionChangeObserver> Create(
+ SessionChangeListener* aListener);
+
+ private:
+ explicit SessionChangeObserver(SessionChangeListener* aListener);
+ virtual ~SessionChangeObserver();
+
+ void Initialize();
+ void OnWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam,
+ LPARAM aLParam) override;
+
+ SessionChangeListener* mListener;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_windows_WinEventObserver_h
diff --git a/widget/windows/WinHeaderOnlyUtils.h b/widget/windows/WinHeaderOnlyUtils.h
new file mode 100644
index 0000000000..15861cc73b
--- /dev/null
+++ b/widget/windows/WinHeaderOnlyUtils.h
@@ -0,0 +1,820 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_WinHeaderOnlyUtils_h
+#define mozilla_WinHeaderOnlyUtils_h
+
+#include <windows.h>
+#include <winerror.h>
+#include <winnt.h>
+#include <winternl.h>
+#include <objbase.h>
+#include <shlwapi.h>
+#undef ParseURL
+#include <stdlib.h>
+#include <tuple>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DynamicallyLinkedFunctionPtr.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/UniquePtr.h"
+#include "nsWindowsHelpers.h"
+
+#if defined(MOZILLA_INTERNAL_API)
+# include "nsIFile.h"
+# include "nsString.h"
+#endif // defined(MOZILLA_INTERNAL_API)
+
+/**
+ * This header is intended for self-contained, header-only, utility code for
+ * Win32. It may be used outside of xul.dll, in places such as firefox.exe or
+ * mozglue.dll. If your code creates dependencies on Mozilla libraries, you
+ * should put it elsewhere.
+ */
+
+#if !defined(STATUS_SUCCESS)
+# define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
+#endif // !defined(STATUS_SUCCESS)
+
+// Our data indicates a few users of Win7 x86 hit failure to load urlmon.dll
+// for unknown reasons. Since we don't always require urlmon.dll on Win7,
+// we delay-load it, which causes a crash if loading urlmon.dll fails. This
+// macro is to safely load and call urlmon's API graciously without crash.
+#if defined(_X86_)
+# define SAFECALL_URLMON_FUNC(FuncName, ...) \
+ do { \
+ static const mozilla::StaticDynamicallyLinkedFunctionPtr< \
+ decltype(&::FuncName)> \
+ func(L"urlmon.dll", #FuncName); \
+ hr = \
+ func ? func(__VA_ARGS__) : HRESULT_FROM_WIN32(ERROR_PROC_NOT_FOUND); \
+ } while (0)
+#else
+# define SAFECALL_URLMON_FUNC(FuncName, ...) hr = ::FuncName(__VA_ARGS__)
+#endif
+
+namespace mozilla {
+
+class WindowsError final {
+ private:
+ // HRESULT and NTSTATUS are both typedefs of LONG, so we cannot use
+ // overloading to properly differentiate between the two. Instead we'll use
+ // static functions to convert the various error types to HRESULTs before
+ // instantiating.
+ explicit constexpr WindowsError(HRESULT aHResult) : mHResult(aHResult) {}
+
+ public:
+ using UniqueString = UniquePtr<WCHAR[], LocalFreeDeleter>;
+
+ static constexpr WindowsError FromNtStatus(NTSTATUS aNtStatus) {
+ if (aNtStatus == STATUS_SUCCESS) {
+ // Special case: we don't want to set FACILITY_NT_BIT
+ // (HRESULT_FROM_NT does not handle this case, unlike HRESULT_FROM_WIN32)
+ return WindowsError(S_OK);
+ }
+
+ return WindowsError(HRESULT_FROM_NT(aNtStatus));
+ }
+
+ static constexpr WindowsError FromHResult(HRESULT aHResult) {
+ return WindowsError(aHResult);
+ }
+
+ static constexpr WindowsError FromWin32Error(DWORD aWin32Err) {
+ return WindowsError(HRESULT_FROM_WIN32(aWin32Err));
+ }
+
+ static WindowsError FromLastError() {
+ return FromWin32Error(::GetLastError());
+ }
+
+ static WindowsError CreateSuccess() { return WindowsError(S_OK); }
+
+ static WindowsError CreateGeneric() {
+ return FromWin32Error(ERROR_UNIDENTIFIED_ERROR);
+ }
+
+ bool IsSuccess() const { return SUCCEEDED(mHResult); }
+
+ bool IsFailure() const { return FAILED(mHResult); }
+
+ bool IsAvailableAsWin32Error() const {
+ return IsAvailableAsNtStatus() ||
+ HRESULT_FACILITY(mHResult) == FACILITY_WIN32;
+ }
+
+ bool IsAvailableAsNtStatus() const {
+ return mHResult == S_OK || (mHResult & FACILITY_NT_BIT);
+ }
+
+ bool IsAvailableAsHResult() const { return true; }
+
+ UniqueString AsString() const {
+ LPWSTR rawMsgBuf = nullptr;
+ constexpr DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS;
+ DWORD result =
+ ::FormatMessageW(flags, nullptr, mHResult, 0,
+ reinterpret_cast<LPWSTR>(&rawMsgBuf), 0, nullptr);
+ if (!result) {
+ return nullptr;
+ }
+
+ return UniqueString(rawMsgBuf);
+ }
+
+ HRESULT AsHResult() const { return mHResult; }
+
+ // Not all HRESULTs are convertible to Win32 Errors, so we use Maybe
+ Maybe<DWORD> AsWin32Error() const {
+ if (mHResult == S_OK) {
+ return Some(static_cast<DWORD>(ERROR_SUCCESS));
+ }
+
+ if (HRESULT_FACILITY(mHResult) == FACILITY_WIN32) {
+ // This is the inverse of HRESULT_FROM_WIN32
+ return Some(static_cast<DWORD>(HRESULT_CODE(mHResult)));
+ }
+
+ // The NTSTATUS facility is a special case and thus does not utilize the
+ // HRESULT_FACILITY and HRESULT_CODE macros.
+ if (mHResult & FACILITY_NT_BIT) {
+ return Some(NtStatusToWin32Error(
+ static_cast<NTSTATUS>(mHResult & ~FACILITY_NT_BIT)));
+ }
+
+ return Nothing();
+ }
+
+ // Not all HRESULTs are convertible to NTSTATUS, so we use Maybe
+ Maybe<NTSTATUS> AsNtStatus() const {
+ if (mHResult == S_OK) {
+ return Some(STATUS_SUCCESS);
+ }
+
+ // The NTSTATUS facility is a special case and thus does not utilize the
+ // HRESULT_FACILITY and HRESULT_CODE macros.
+ if (mHResult & FACILITY_NT_BIT) {
+ return Some(static_cast<NTSTATUS>(mHResult & ~FACILITY_NT_BIT));
+ }
+
+ return Nothing();
+ }
+
+ constexpr bool operator==(const WindowsError& aOther) const {
+ return mHResult == aOther.mHResult;
+ }
+
+ constexpr bool operator!=(const WindowsError& aOther) const {
+ return mHResult != aOther.mHResult;
+ }
+
+ static DWORD NtStatusToWin32Error(NTSTATUS aNtStatus) {
+ static const StaticDynamicallyLinkedFunctionPtr<
+ decltype(&RtlNtStatusToDosError)>
+ pRtlNtStatusToDosError(L"ntdll.dll", "RtlNtStatusToDosError");
+
+ MOZ_ASSERT(!!pRtlNtStatusToDosError);
+ if (!pRtlNtStatusToDosError) {
+ return ERROR_UNIDENTIFIED_ERROR;
+ }
+
+ return pRtlNtStatusToDosError(aNtStatus);
+ }
+
+ private:
+ // We store the error code as an HRESULT because they can encode both Win32
+ // error codes and NTSTATUS codes.
+ HRESULT mHResult;
+};
+
+namespace detail {
+template <>
+struct UnusedZero<WindowsError> {
+ using StorageType = WindowsError;
+
+ static constexpr bool value = true;
+ static constexpr StorageType nullValue = WindowsError::FromHResult(S_OK);
+
+ static constexpr void AssertValid(StorageType aValue) {}
+ static constexpr const WindowsError& Inspect(const StorageType& aValue) {
+ return aValue;
+ }
+ static constexpr WindowsError Unwrap(StorageType aValue) { return aValue; }
+ static constexpr StorageType Store(WindowsError aValue) { return aValue; }
+};
+} // namespace detail
+
+enum DetourResultCode : uint32_t {
+ RESULT_OK = 0,
+ INTERCEPTOR_MOD_NULL,
+ INTERCEPTOR_MOD_INACCESSIBLE,
+ INTERCEPTOR_PROC_NULL,
+ INTERCEPTOR_PROC_INACCESSIBLE,
+ DETOUR_PATCHER_RESERVE_FOR_MODULE_PE_ERROR,
+ DETOUR_PATCHER_RESERVE_FOR_MODULE_TEXT_ERROR,
+ DETOUR_PATCHER_RESERVE_FOR_MODULE_RESERVE_ERROR,
+ DETOUR_PATCHER_DO_RESERVE_ERROR,
+ DETOUR_PATCHER_NEXT_TRAMPOLINE_ERROR,
+ DETOUR_PATCHER_INVALID_TRAMPOLINE,
+ DETOUR_PATCHER_WRITE_POINTER_ERROR,
+ DETOUR_PATCHER_CREATE_TRAMPOLINE_ERROR,
+ FUNCHOOKCROSSPROCESS_COPYSTUB_ERROR,
+ MMPOLICY_RESERVE_INVALIDARG,
+ MMPOLICY_RESERVE_ZERO_RESERVATIONSIZE,
+ MMPOLICY_RESERVE_CREATEFILEMAPPING,
+ MMPOLICY_RESERVE_MAPVIEWOFFILE,
+ MMPOLICY_RESERVE_NOBOUND_RESERVE_ERROR,
+ MMPOLICY_RESERVE_FINDREGION_INVALIDLEN,
+ MMPOLICY_RESERVE_FINDREGION_INVALIDRANGE,
+ MMPOLICY_RESERVE_FINDREGION_VIRTUALQUERY_ERROR,
+ MMPOLICY_RESERVE_FINDREGION_NO_FREE_REGION,
+ MMPOLICY_RESERVE_FINAL_RESERVE_ERROR,
+};
+
+#if defined(NIGHTLY_BUILD)
+struct DetourError {
+ // We have a 16-bytes buffer, but only minimum bytes to detour per
+ // architecture are copied. See CreateTrampoline in PatcherDetour.h.
+ DetourResultCode mErrorCode;
+ uint8_t mOrigBytes[16];
+ explicit DetourError(DetourResultCode aError)
+ : mErrorCode(aError), mOrigBytes{} {}
+ DetourError(DetourResultCode aError, DWORD aWin32Error)
+ : mErrorCode(aError), mOrigBytes{} {
+ static_assert(sizeof(mOrigBytes) >= sizeof(aWin32Error),
+ "Can't fit a DWORD in mOrigBytes");
+ *reinterpret_cast<DWORD*>(mOrigBytes) = aWin32Error;
+ }
+ operator WindowsError() const {
+ return WindowsError::FromHResult(mErrorCode);
+ }
+};
+#endif // defined(NIGHTLY_BUILD)
+
+template <typename T>
+using WindowsErrorResult = Result<T, WindowsError>;
+
+struct LauncherError {
+ LauncherError(const char* aFile, int aLine, WindowsError aWin32Error)
+ : mFile(aFile), mLine(aLine), mError(aWin32Error) {}
+
+#if defined(NIGHTLY_BUILD)
+ LauncherError(const char* aFile, int aLine,
+ const Maybe<DetourError>& aDetourError)
+ : mFile(aFile),
+ mLine(aLine),
+ mError(aDetourError.isSome() ? aDetourError.value()
+ : WindowsError::CreateGeneric()),
+ mDetourError(aDetourError) {}
+#endif // defined(NIGHTLY_BUILD)
+
+ const char* mFile;
+ int mLine;
+ WindowsError mError;
+#if defined(NIGHTLY_BUILD)
+ Maybe<DetourError> mDetourError;
+#endif // defined(NIGHTLY_BUILD)
+
+ bool operator==(const LauncherError& aOther) const {
+ return mError == aOther.mError;
+ }
+
+ bool operator!=(const LauncherError& aOther) const {
+ return mError != aOther.mError;
+ }
+
+ bool operator==(const WindowsError& aOther) const { return mError == aOther; }
+
+ bool operator!=(const WindowsError& aOther) const { return mError != aOther; }
+};
+
+#if defined(MOZ_USE_LAUNCHER_ERROR)
+
+template <typename T>
+using LauncherResult = Result<T, LauncherError>;
+
+template <typename T>
+using LauncherResultWithLineInfo = LauncherResult<T>;
+
+using WindowsErrorType = LauncherError;
+
+#else
+
+template <typename T>
+using LauncherResult = WindowsErrorResult<T>;
+
+template <typename T>
+using LauncherResultWithLineInfo = Result<T, LauncherError>;
+
+using WindowsErrorType = WindowsError;
+
+#endif // defined(MOZ_USE_LAUNCHER_ERROR)
+
+using LauncherVoidResult = LauncherResult<Ok>;
+
+using LauncherVoidResultWithLineInfo = LauncherResultWithLineInfo<Ok>;
+
+#if defined(MOZ_USE_LAUNCHER_ERROR)
+
+# define LAUNCHER_ERROR_GENERIC() \
+ ::mozilla::Err(::mozilla::LauncherError( \
+ __FILE__, __LINE__, ::mozilla::WindowsError::CreateGeneric()))
+
+# if defined(NIGHTLY_BUILD)
+# define LAUNCHER_ERROR_FROM_DETOUR_ERROR(err) \
+ ::mozilla::Err(::mozilla::LauncherError(__FILE__, __LINE__, err))
+# else
+# define LAUNCHER_ERROR_FROM_DETOUR_ERROR(err) LAUNCHER_ERROR_GENERIC()
+# endif // defined(NIGHTLY_BUILD)
+
+# define LAUNCHER_ERROR_FROM_WIN32(err) \
+ ::mozilla::Err(::mozilla::LauncherError( \
+ __FILE__, __LINE__, ::mozilla::WindowsError::FromWin32Error(err)))
+
+# define LAUNCHER_ERROR_FROM_LAST() \
+ ::mozilla::Err(::mozilla::LauncherError( \
+ __FILE__, __LINE__, ::mozilla::WindowsError::FromLastError()))
+
+# define LAUNCHER_ERROR_FROM_NTSTATUS(ntstatus) \
+ ::mozilla::Err(::mozilla::LauncherError( \
+ __FILE__, __LINE__, ::mozilla::WindowsError::FromNtStatus(ntstatus)))
+
+# define LAUNCHER_ERROR_FROM_HRESULT(hresult) \
+ ::mozilla::Err(::mozilla::LauncherError( \
+ __FILE__, __LINE__, ::mozilla::WindowsError::FromHResult(hresult)))
+
+// This macro wraps the supplied WindowsError with a LauncherError
+# define LAUNCHER_ERROR_FROM_MOZ_WINDOWS_ERROR(err) \
+ ::mozilla::Err(::mozilla::LauncherError(__FILE__, __LINE__, err))
+
+#else
+
+# define LAUNCHER_ERROR_GENERIC() \
+ ::mozilla::Err(::mozilla::WindowsError::CreateGeneric())
+
+# define LAUNCHER_ERROR_FROM_DETOUR_ERROR(err) LAUNCHER_ERROR_GENERIC()
+
+# define LAUNCHER_ERROR_FROM_WIN32(err) \
+ ::mozilla::Err(::mozilla::WindowsError::FromWin32Error(err))
+
+# define LAUNCHER_ERROR_FROM_LAST() \
+ ::mozilla::Err(::mozilla::WindowsError::FromLastError())
+
+# define LAUNCHER_ERROR_FROM_NTSTATUS(ntstatus) \
+ ::mozilla::Err(::mozilla::WindowsError::FromNtStatus(ntstatus))
+
+# define LAUNCHER_ERROR_FROM_HRESULT(hresult) \
+ ::mozilla::Err(::mozilla::WindowsError::FromHResult(hresult))
+
+# define LAUNCHER_ERROR_FROM_MOZ_WINDOWS_ERROR(err) ::mozilla::Err(err)
+
+#endif // defined(MOZ_USE_LAUNCHER_ERROR)
+
+// How long to wait for a created process to become available for input,
+// to prevent that process's windows being forced to the background.
+// This is used across update, restart, and the launcher.
+const DWORD kWaitForInputIdleTimeoutMS = 10 * 1000;
+
+/**
+ * Wait for a child GUI process to become "idle." Idle means that the process
+ * has created its message queue and has begun waiting for user input.
+ *
+ * Note that this must only be used when the child process is going to display
+ * GUI! Otherwise you're going to be waiting for a very long time ;-)
+ *
+ * @return true if we successfully waited for input idle;
+ * false if we timed out or failed to wait.
+ */
+inline bool WaitForInputIdle(HANDLE aProcess,
+ DWORD aTimeoutMs = kWaitForInputIdleTimeoutMS) {
+ const DWORD kSleepTimeMs = 10;
+ const DWORD waitStart = aTimeoutMs == INFINITE ? 0 : ::GetTickCount();
+ DWORD elapsed = 0;
+
+ while (true) {
+ if (aTimeoutMs != INFINITE) {
+ elapsed = ::GetTickCount() - waitStart;
+ }
+
+ if (elapsed >= aTimeoutMs) {
+ return false;
+ }
+
+ // ::WaitForInputIdle() doesn't always set the last-error code on failure
+ ::SetLastError(ERROR_SUCCESS);
+
+ DWORD waitResult = ::WaitForInputIdle(aProcess, aTimeoutMs - elapsed);
+ if (!waitResult) {
+ return true;
+ }
+
+ if (waitResult == WAIT_FAILED &&
+ ::GetLastError() == ERROR_NOT_GUI_PROCESS) {
+ ::Sleep(kSleepTimeMs);
+ continue;
+ }
+
+ return false;
+ }
+}
+
+enum class PathType {
+ eNtPath,
+ eDosPath,
+};
+
+class FileUniqueId final {
+ public:
+ explicit FileUniqueId(const wchar_t* aPath, PathType aPathType)
+ : mId(FILE_ID_INFO()) {
+ if (!aPath) {
+ mId = LAUNCHER_ERROR_FROM_HRESULT(E_INVALIDARG);
+ return;
+ }
+
+ nsAutoHandle file;
+
+ switch (aPathType) {
+ default:
+ mId = LAUNCHER_ERROR_FROM_HRESULT(E_INVALIDARG);
+ MOZ_ASSERT_UNREACHABLE("Unhandled PathType");
+ return;
+
+ case PathType::eNtPath: {
+ UNICODE_STRING unicodeString;
+ ::RtlInitUnicodeString(&unicodeString, aPath);
+ OBJECT_ATTRIBUTES objectAttributes;
+ InitializeObjectAttributes(&objectAttributes, &unicodeString,
+ OBJ_CASE_INSENSITIVE, nullptr, nullptr);
+ IO_STATUS_BLOCK ioStatus = {};
+ HANDLE ntHandle;
+ NTSTATUS status = ::NtOpenFile(
+ &ntHandle, SYNCHRONIZE | FILE_READ_ATTRIBUTES, &objectAttributes,
+ &ioStatus, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_FOR_BACKUP_INTENT);
+ // We don't need to check |ntHandle| for INVALID_HANDLE_VALUE here,
+ // as that value is set by the Win32 layer.
+ if (!NT_SUCCESS(status)) {
+ mId = LAUNCHER_ERROR_FROM_NTSTATUS(status);
+ return;
+ }
+
+ file.own(ntHandle);
+ break;
+ }
+
+ case PathType::eDosPath: {
+ file.own(::CreateFileW(
+ aPath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr));
+ if (file == INVALID_HANDLE_VALUE) {
+ mId = LAUNCHER_ERROR_FROM_LAST();
+ return;
+ }
+
+ break;
+ }
+ }
+
+ GetId(file);
+ }
+
+ explicit FileUniqueId(const nsAutoHandle& aFile) : mId(FILE_ID_INFO()) {
+ GetId(aFile);
+ }
+
+ ~FileUniqueId() = default;
+
+ bool IsError() const { return mId.isErr(); }
+
+ const WindowsErrorType& GetError() const { return mId.inspectErr(); }
+
+ FileUniqueId(FileUniqueId&& aOther) = default;
+ FileUniqueId& operator=(FileUniqueId&& aOther) = delete;
+
+ bool operator==(const FileUniqueId& aOther) const {
+ return mId.isOk() && aOther.mId.isOk() &&
+ !memcmp(&mId.inspect(), &aOther.mId.inspect(), sizeof(FILE_ID_INFO));
+ }
+
+ bool operator!=(const FileUniqueId& aOther) const {
+ return !((*this) == aOther);
+ }
+
+ private:
+ void GetId(const nsAutoHandle& aFile) {
+ FILE_ID_INFO fileIdInfo = {};
+ if (::GetFileInformationByHandleEx(aFile.get(), FileIdInfo, &fileIdInfo,
+ sizeof(fileIdInfo))) {
+ mId = fileIdInfo;
+ return;
+ }
+ // Only NTFS and ReFS support FileIdInfo. So we have to fallback if
+ // GetFileInformationByHandleEx failed.
+
+ BY_HANDLE_FILE_INFORMATION info = {};
+ if (!::GetFileInformationByHandle(aFile.get(), &info)) {
+ mId = LAUNCHER_ERROR_FROM_LAST();
+ return;
+ }
+
+ fileIdInfo.VolumeSerialNumber = info.dwVolumeSerialNumber;
+ memcpy(&fileIdInfo.FileId.Identifier[0], &info.nFileIndexLow,
+ sizeof(DWORD));
+ memcpy(&fileIdInfo.FileId.Identifier[sizeof(DWORD)], &info.nFileIndexHigh,
+ sizeof(DWORD));
+ mId = fileIdInfo;
+ }
+
+ private:
+ LauncherResult<FILE_ID_INFO> mId;
+};
+
+class MOZ_RAII AutoVirtualProtect final {
+ public:
+ AutoVirtualProtect(void* aAddress, size_t aLength, DWORD aProtFlags,
+ HANDLE aTargetProcess = ::GetCurrentProcess())
+ : mAddress(aAddress),
+ mLength(aLength),
+ mTargetProcess(aTargetProcess),
+ mPrevProt(0),
+ mError(WindowsError::CreateSuccess()) {
+ if (!::VirtualProtectEx(aTargetProcess, aAddress, aLength, aProtFlags,
+ &mPrevProt)) {
+ mError = WindowsError::FromLastError();
+ }
+ }
+
+ ~AutoVirtualProtect() {
+ if (mError.IsFailure()) {
+ return;
+ }
+
+ ::VirtualProtectEx(mTargetProcess, mAddress, mLength, mPrevProt,
+ &mPrevProt);
+ }
+
+ explicit operator bool() const { return mError.IsSuccess(); }
+
+ WindowsError GetError() const { return mError; }
+
+ DWORD PrevProt() const { return mPrevProt; }
+
+ AutoVirtualProtect(const AutoVirtualProtect&) = delete;
+ AutoVirtualProtect(AutoVirtualProtect&&) = delete;
+ AutoVirtualProtect& operator=(const AutoVirtualProtect&) = delete;
+ AutoVirtualProtect& operator=(AutoVirtualProtect&&) = delete;
+
+ private:
+ void* mAddress;
+ size_t mLength;
+ HANDLE mTargetProcess;
+ DWORD mPrevProt;
+ WindowsError mError;
+};
+
+inline UniquePtr<wchar_t[]> GetFullModulePath(HMODULE aModule) {
+ DWORD bufLen = MAX_PATH;
+ mozilla::UniquePtr<wchar_t[]> buf;
+ DWORD retLen;
+
+ while (true) {
+ buf = mozilla::MakeUnique<wchar_t[]>(bufLen);
+ retLen = ::GetModuleFileNameW(aModule, buf.get(), bufLen);
+ if (!retLen) {
+ return nullptr;
+ }
+
+ if (retLen == bufLen && ::GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
+ bufLen *= 2;
+ continue;
+ }
+
+ break;
+ }
+
+ // Upon success, retLen *excludes* the null character
+ ++retLen;
+
+ // Since we're likely to have a bunch of unused space in buf, let's
+ // reallocate a string to the actual size of the file name.
+ auto result = mozilla::MakeUnique<wchar_t[]>(retLen);
+ if (wcscpy_s(result.get(), retLen, buf.get())) {
+ return nullptr;
+ }
+
+ return result;
+}
+
+inline UniquePtr<wchar_t[]> GetFullBinaryPath() {
+ return GetFullModulePath(nullptr);
+}
+
+// Generates the install directory without a trailing path separator.
+inline bool GetInstallDirectory(UniquePtr<wchar_t[]>& installPath) {
+ installPath = GetFullBinaryPath();
+ // It's not safe to use PathRemoveFileSpecW with strings longer than MAX_PATH
+ // (including null terminator).
+ if (wcslen(installPath.get()) >= MAX_PATH) {
+ return false;
+ }
+ ::PathRemoveFileSpecW(installPath.get());
+ return true;
+}
+
+class ModuleVersion final {
+ public:
+ constexpr ModuleVersion() : mVersion(0ULL) {}
+
+ explicit ModuleVersion(const VS_FIXEDFILEINFO& aFixedInfo)
+ : mVersion((static_cast<uint64_t>(aFixedInfo.dwFileVersionMS) << 32) |
+ static_cast<uint64_t>(aFixedInfo.dwFileVersionLS)) {}
+
+ explicit ModuleVersion(const uint64_t aVersion) : mVersion(aVersion) {}
+
+ ModuleVersion(const ModuleVersion& aOther) : mVersion(aOther.mVersion) {}
+
+ uint64_t AsInteger() const { return mVersion; }
+
+ operator uint64_t() const { return AsInteger(); }
+
+ std::tuple<uint16_t, uint16_t, uint16_t, uint16_t> AsTuple() const {
+ uint16_t major = static_cast<uint16_t>((mVersion >> 48) & 0xFFFFU);
+ uint16_t minor = static_cast<uint16_t>((mVersion >> 32) & 0xFFFFU);
+ uint16_t patch = static_cast<uint16_t>((mVersion >> 16) & 0xFFFFU);
+ uint16_t build = static_cast<uint16_t>(mVersion & 0xFFFFU);
+
+ return {major, minor, patch, build};
+ }
+
+ explicit operator bool() const { return !!mVersion; }
+
+ bool operator<(const ModuleVersion& aOther) const {
+ return mVersion < aOther.mVersion;
+ }
+
+ bool operator<(const uint64_t& aOther) const { return mVersion < aOther; }
+
+ ModuleVersion& operator=(const uint64_t aIntVersion) {
+ mVersion = aIntVersion;
+ return *this;
+ }
+
+ private:
+ uint64_t mVersion;
+};
+
+inline LauncherResult<ModuleVersion> GetModuleVersion(
+ const wchar_t* aModuleFullPath) {
+ DWORD verInfoLen = ::GetFileVersionInfoSizeW(aModuleFullPath, nullptr);
+ if (!verInfoLen) {
+ return LAUNCHER_ERROR_FROM_LAST();
+ }
+
+ auto verInfoBuf = MakeUnique<BYTE[]>(verInfoLen);
+ if (!::GetFileVersionInfoW(aModuleFullPath, 0, verInfoLen,
+ verInfoBuf.get())) {
+ return LAUNCHER_ERROR_FROM_LAST();
+ }
+
+ UINT fixedInfoLen;
+ VS_FIXEDFILEINFO* fixedInfo = nullptr;
+ if (!::VerQueryValueW(verInfoBuf.get(), L"\\",
+ reinterpret_cast<LPVOID*>(&fixedInfo), &fixedInfoLen)) {
+ // VerQueryValue may fail if the resource does not exist. This is not an
+ // error; we'll return 0 in this case.
+ return ModuleVersion(0ULL);
+ }
+
+ return ModuleVersion(*fixedInfo);
+}
+
+inline LauncherResult<ModuleVersion> GetModuleVersion(HMODULE aModule) {
+ UniquePtr<wchar_t[]> fullPath(GetFullModulePath(aModule));
+ if (!fullPath) {
+ return LAUNCHER_ERROR_GENERIC();
+ }
+
+ return GetModuleVersion(fullPath.get());
+}
+
+#if defined(MOZILLA_INTERNAL_API)
+inline LauncherResult<ModuleVersion> GetModuleVersion(nsIFile* aFile) {
+ if (!aFile) {
+ return LAUNCHER_ERROR_FROM_HRESULT(E_INVALIDARG);
+ }
+
+ nsAutoString fullPath;
+ nsresult rv = aFile->GetPath(fullPath);
+ if (NS_FAILED(rv)) {
+ return LAUNCHER_ERROR_GENERIC();
+ }
+
+ return GetModuleVersion(fullPath.get());
+}
+#endif // defined(MOZILLA_INTERNAL_API)
+
+struct CoTaskMemFreeDeleter {
+ void operator()(void* aPtr) { ::CoTaskMemFree(aPtr); }
+};
+
+inline LauncherResult<TOKEN_ELEVATION_TYPE> GetElevationType(
+ const nsAutoHandle& aToken) {
+ DWORD retLen;
+ TOKEN_ELEVATION_TYPE elevationType;
+ if (!::GetTokenInformation(aToken.get(), TokenElevationType, &elevationType,
+ sizeof(elevationType), &retLen)) {
+ return LAUNCHER_ERROR_FROM_LAST();
+ }
+
+ return elevationType;
+}
+
+inline bool HasPackageIdentity() {
+ HMODULE kernel32Dll = ::GetModuleHandleW(L"kernel32");
+ if (!kernel32Dll) {
+ return false;
+ }
+
+ typedef LONG(WINAPI * GetCurrentPackageIdProc)(UINT32*, BYTE*);
+ GetCurrentPackageIdProc pGetCurrentPackageId =
+ (GetCurrentPackageIdProc)::GetProcAddress(kernel32Dll,
+ "GetCurrentPackageId");
+
+ // If there was any package identity to retrieve, we get
+ // ERROR_INSUFFICIENT_BUFFER. If there had been no package identity it
+ // would instead return APPMODEL_ERROR_NO_PACKAGE.
+ UINT32 packageNameSize = 0;
+ return pGetCurrentPackageId &&
+ (pGetCurrentPackageId(&packageNameSize, nullptr) ==
+ ERROR_INSUFFICIENT_BUFFER);
+}
+
+inline UniquePtr<wchar_t[]> GetPackageFamilyName() {
+ HMODULE kernel32Dll = ::GetModuleHandleW(L"kernel32");
+ if (!kernel32Dll) {
+ return nullptr;
+ }
+
+ typedef LONG(WINAPI * GetCurrentPackageFamilyNameProc)(UINT32*, PWSTR);
+ GetCurrentPackageFamilyNameProc pGetCurrentPackageFamilyName =
+ (GetCurrentPackageFamilyNameProc)::GetProcAddress(
+ kernel32Dll, "GetCurrentPackageFamilyName");
+ if (!pGetCurrentPackageFamilyName) {
+ return nullptr;
+ }
+
+ UINT32 packageNameSize = 0;
+ if (pGetCurrentPackageFamilyName(&packageNameSize, nullptr) !=
+ ERROR_INSUFFICIENT_BUFFER) {
+ return nullptr;
+ }
+
+ UniquePtr<wchar_t[]> packageIdentity = MakeUnique<wchar_t[]>(packageNameSize);
+ if (pGetCurrentPackageFamilyName(&packageNameSize, packageIdentity.get()) !=
+ ERROR_SUCCESS) {
+ return nullptr;
+ }
+
+ return packageIdentity;
+}
+
+// This implementation is equivalent to PathGetDriveNumber[AW].
+// We define our own version because using PathGetDriveNumber
+// delay-loads shlwapi.dll, which may fail when the process is
+// sandboxed.
+template <typename T>
+int MozPathGetDriveNumber(const T* aPath) {
+ const auto ToDriveNumber = [](const T* aPath) -> int {
+ if (*aPath == '\0' || *(aPath + 1) != ':') {
+ return -1;
+ }
+
+ T c = *aPath;
+ return (c >= 'A' && c <= 'Z') ? c - 'A'
+ : (c >= 'a' && c <= 'z') ? c - 'a'
+ : -1;
+ };
+
+ if (!aPath) {
+ return -1;
+ }
+
+ if (*aPath == '\\' && *(aPath + 1) == '\\' && *(aPath + 2) == '?' &&
+ *(aPath + 3) == '\\') {
+ return ToDriveNumber(aPath + 4);
+ }
+
+ return ToDriveNumber(aPath);
+}
+
+} // namespace mozilla
+
+#endif // mozilla_WinHeaderOnlyUtils_h
diff --git a/widget/windows/WinIMEHandler.cpp b/widget/windows/WinIMEHandler.cpp
new file mode 100644
index 0000000000..961c61fae0
--- /dev/null
+++ b/widget/windows/WinIMEHandler.cpp
@@ -0,0 +1,1081 @@
+/* -*- 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 "WinIMEHandler.h"
+
+#include "IMMHandler.h"
+#include "KeyboardLayout.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/Unused.h"
+#include "mozilla/WindowsVersion.h"
+#include "nsWindowDefs.h"
+#include "WinTextEventDispatcherListener.h"
+
+#include "TSFTextStore.h"
+
+#include "OSKInputPaneManager.h"
+#include "OSKTabTipManager.h"
+#include "OSKVRManager.h"
+#include "nsLookAndFeel.h"
+#include "nsWindow.h"
+#include "WinUtils.h"
+#include "nsIWindowsRegKey.h"
+#include "WindowsUIUtils.h"
+
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+#endif // #ifdef ACCESSIBILITY
+
+#include "shellapi.h"
+#include "shlobj.h"
+#include "powrprof.h"
+#include "setupapi.h"
+#include "cfgmgr32.h"
+
+#include "FxRWindowManager.h"
+#include "moz_external_vr.h"
+
+const char* kOskEnabled = "ui.osk.enabled";
+const char* kOskDetectPhysicalKeyboard = "ui.osk.detect_physical_keyboard";
+const char* kOskDebugReason = "ui.osk.debug.keyboardDisplayReason";
+
+namespace mozilla {
+namespace widget {
+
+/******************************************************************************
+ * IMEHandler
+ ******************************************************************************/
+
+nsWindow* IMEHandler::sFocusedWindow = nullptr;
+InputContextAction::Cause IMEHandler::sLastContextActionCause =
+ InputContextAction::CAUSE_UNKNOWN;
+bool IMEHandler::sMaybeEditable = false;
+bool IMEHandler::sForceDisableCurrentIMM_IME = false;
+bool IMEHandler::sNativeCaretIsCreated = false;
+bool IMEHandler::sHasNativeCaretBeenRequested = false;
+
+bool IMEHandler::sIsInTSFMode = false;
+bool IMEHandler::sIsIMMEnabled = true;
+decltype(SetInputScopes)* IMEHandler::sSetInputScopes = nullptr;
+
+static POWER_PLATFORM_ROLE sPowerPlatformRole = PlatformRoleUnspecified;
+static bool sDeterminedPowerPlatformRole = false;
+
+// static
+void IMEHandler::Initialize() {
+ TSFTextStore::Initialize();
+ sIsInTSFMode = TSFTextStore::IsInTSFMode();
+ sIsIMMEnabled =
+ !sIsInTSFMode || StaticPrefs::intl_tsf_support_imm_AtStartup();
+ if (!sIsInTSFMode) {
+ // When full TSFTextStore is not available, try to use SetInputScopes API
+ // to enable at least InputScope. Use GET_MODULE_HANDLE_EX_FLAG_PIN to
+ // ensure that msctf.dll will not be unloaded.
+ HMODULE module = nullptr;
+ if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN, L"msctf.dll",
+ &module)) {
+ sSetInputScopes = reinterpret_cast<decltype(SetInputScopes)*>(
+ GetProcAddress(module, "SetInputScopes"));
+ }
+ }
+
+ IMMHandler::Initialize();
+
+ sForceDisableCurrentIMM_IME = IMMHandler::IsActiveIMEInBlockList();
+}
+
+// static
+void IMEHandler::Terminate() {
+ if (sIsInTSFMode) {
+ TSFTextStore::Terminate();
+ sIsInTSFMode = false;
+ }
+
+ IMMHandler::Terminate();
+ WinTextEventDispatcherListener::Shutdown();
+}
+
+// static
+void* IMEHandler::GetNativeData(nsWindow* aWindow, uint32_t aDataType) {
+ if (aDataType == NS_RAW_NATIVE_IME_CONTEXT) {
+ if (IsTSFAvailable()) {
+ return TSFTextStore::GetThreadManager();
+ }
+ IMEContext context(aWindow);
+ if (context.IsValid()) {
+ return context.get();
+ }
+ // If IMC isn't associated with the window, IME is disabled on the window
+ // now. In such case, we should return default IMC instead.
+ const IMEContext& defaultIMC = aWindow->DefaultIMC();
+ if (defaultIMC.IsValid()) {
+ return defaultIMC.get();
+ }
+ // If there is no default IMC, we should return the pointer to the window
+ // since if we return nullptr, IMEStateManager cannot manage composition
+ // with TextComposition instance. This is possible if no IME is installed,
+ // but composition may occur with dead key sequence.
+ return aWindow;
+ }
+
+ void* result = TSFTextStore::GetNativeData(aDataType);
+ if (!result || !(*(static_cast<void**>(result)))) {
+ return nullptr;
+ }
+ // XXX During the TSF module test, sIsInTSFMode must be true. After that,
+ // the value should be restored but currently, there is no way for that.
+ // When the TSF test is enabled again, we need to fix this. Perhaps,
+ // sending a message can fix this.
+ sIsInTSFMode = true;
+ return result;
+}
+
+// static
+bool IMEHandler::ProcessRawKeyMessage(const MSG& aMsg) {
+ if (StaticPrefs::ui_key_layout_load_when_first_needed()) {
+ // Getting instance creates the singleton instance and that will
+ // automatically load active keyboard layout data. We should do that
+ // before TSF or TranslateMessage handles a key message.
+ Unused << KeyboardLayout::GetInstance();
+ }
+ if (IsTSFAvailable()) {
+ return TSFTextStore::ProcessRawKeyMessage(aMsg);
+ }
+ return false; // noting to do in IMM mode.
+}
+
+// static
+bool IMEHandler::ProcessMessage(nsWindow* aWindow, UINT aMessage,
+ WPARAM& aWParam, LPARAM& aLParam,
+ MSGResult& aResult) {
+ // If we're putting native caret over our caret, Windows dispatches
+ // EVENT_OBJECT_LOCATIONCHANGE event on other applications which hook
+ // the event with ::SetWinEventHook() and handles WM_GETOBJECT for
+ // OBJID_CARET (this is request of caret from such applications) instead
+ // of us. If a11y module is active, it observes every our caret change
+ // and put native caret over it automatically. However, if other
+ // applications require only caret information, activating a11y module is
+ // overwork and such applications may requires carets only in editors.
+ // Therefore, if it'd be possible, IMEHandler should put native caret over
+ // our caret, but there is a problem. Some versions of ATOK (Japanese TIP)
+ // refer native caret and if there is, the behavior is worse than the
+ // behavior without native caret. Therefore, we shouldn't put native caret
+ // as far as possible.
+ if (!sHasNativeCaretBeenRequested && aMessage == WM_GETOBJECT &&
+ static_cast<LONG>(aLParam) == OBJID_CARET) {
+ // So, when we receive first WM_GETOBJECT for OBJID_CARET, let's start to
+ // create native caret for such applications.
+ sHasNativeCaretBeenRequested = true;
+ // If an editable element has focus, we can put native caret now.
+ // XXX Should we avoid doing this if there is composition?
+ MaybeCreateNativeCaret(aWindow);
+ }
+
+ if (IsTSFAvailable()) {
+ TSFTextStore::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult);
+ if (aResult.mConsumed) {
+ return true;
+ }
+ // If we don't support IMM in TSF mode, we don't use IMMHandler.
+ if (!sIsIMMEnabled) {
+ return false;
+ }
+ // IME isn't implemented with IMM, IMMHandler shouldn't handle any
+ // messages.
+ if (!IsIMMActive()) {
+ return false;
+ }
+ }
+
+ bool keepGoing =
+ IMMHandler::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult);
+
+ // If user changes active IME to an IME which is listed in our block list,
+ // we should disassociate IMC from the window for preventing the IME to work
+ // and crash.
+ if (aMessage == WM_INPUTLANGCHANGE) {
+ bool disableIME = IMMHandler::IsActiveIMEInBlockList();
+ if (disableIME != sForceDisableCurrentIMM_IME) {
+ bool enable =
+ !disableIME && WinUtils::IsIMEEnabled(aWindow->InputContextRef());
+ AssociateIMEContext(aWindow, enable);
+ sForceDisableCurrentIMM_IME = disableIME;
+ }
+ }
+
+ return keepGoing;
+}
+
+// static
+bool IMEHandler::IsA11yHandlingNativeCaret() {
+#ifndef ACCESSIBILITY
+ return false;
+#else // #ifndef ACCESSIBILITY
+ // Let's assume that when there is the service, it handles native caret.
+ return GetAccService() != nullptr;
+#endif // #ifndef ACCESSIBILITY #else
+}
+
+// static
+bool IMEHandler::IsIMMActive() { return TSFTextStore::IsIMM_IMEActive(); }
+
+// static
+bool IMEHandler::IsComposing() {
+ if (IsTSFAvailable()) {
+ return TSFTextStore::IsComposing() || IMMHandler::IsComposing();
+ }
+
+ return IMMHandler::IsComposing();
+}
+
+// static
+bool IMEHandler::IsComposingOn(nsWindow* aWindow) {
+ if (IsTSFAvailable()) {
+ return TSFTextStore::IsComposingOn(aWindow) ||
+ IMMHandler::IsComposingOn(aWindow);
+ }
+
+ return IMMHandler::IsComposingOn(aWindow);
+}
+
+// static
+nsresult IMEHandler::NotifyIME(nsWindow* aWindow,
+ const IMENotification& aIMENotification) {
+ if (IsTSFAvailable()) {
+ switch (aIMENotification.mMessage) {
+ case NOTIFY_IME_OF_SELECTION_CHANGE: {
+ nsresult rv = TSFTextStore::OnSelectionChange(aIMENotification);
+ // If IMM IME is active, we need to notify IMMHandler of updating
+ // composition change. It will adjust candidate window position or
+ // composition window position.
+ bool isIMMActive = IsIMMActive();
+ if (isIMMActive) {
+ IMMHandler::OnUpdateComposition(aWindow);
+ }
+ IMMHandler::OnSelectionChange(aWindow, aIMENotification, isIMMActive);
+ return rv;
+ }
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ // If IMM IME is active, we need to notify IMMHandler of updating
+ // composition change. It will adjust candidate window position or
+ // composition window position.
+ if (IsIMMActive()) {
+ IMMHandler::OnUpdateComposition(aWindow);
+ } else {
+ TSFTextStore::OnUpdateComposition();
+ }
+ return NS_OK;
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ return TSFTextStore::OnTextChange(aIMENotification);
+ case NOTIFY_IME_OF_FOCUS: {
+ sFocusedWindow = aWindow;
+ IMMHandler::OnFocusChange(true, aWindow);
+ nsresult rv = TSFTextStore::OnFocusChange(true, aWindow,
+ aWindow->GetInputContext());
+ MaybeCreateNativeCaret(aWindow);
+ IMEHandler::MaybeShowOnScreenKeyboard(aWindow,
+ aWindow->GetInputContext());
+ return rv;
+ }
+ case NOTIFY_IME_OF_BLUR:
+ sFocusedWindow = nullptr;
+ IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
+ IMMHandler::OnFocusChange(false, aWindow);
+ return TSFTextStore::OnFocusChange(false, aWindow,
+ aWindow->GetInputContext());
+ case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ // If IMM IME is active, we should send a mouse button event via IMM.
+ if (IsIMMActive()) {
+ return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification);
+ }
+ return TSFTextStore::OnMouseButtonEvent(aIMENotification);
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ if (TSFTextStore::IsComposingOn(aWindow)) {
+ TSFTextStore::CommitComposition(false);
+ } else if (IsIMMActive()) {
+ IMMHandler::CommitComposition(aWindow);
+ }
+ return NS_OK;
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ if (TSFTextStore::IsComposingOn(aWindow)) {
+ TSFTextStore::CommitComposition(true);
+ } else if (IsIMMActive()) {
+ IMMHandler::CancelComposition(aWindow);
+ }
+ return NS_OK;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ return TSFTextStore::OnLayoutChange();
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ }
+
+ switch (aIMENotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ IMMHandler::CommitComposition(aWindow);
+ return NS_OK;
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ IMMHandler::CancelComposition(aWindow);
+ return NS_OK;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ IMMHandler::OnUpdateComposition(aWindow);
+ return NS_OK;
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ IMMHandler::OnSelectionChange(aWindow, aIMENotification, true);
+ // IMMHandler::OnSelectionChange() cannot work without its singleton
+ // instance. Therefore, IMEHandler needs to create native caret instead
+ // if it's necessary.
+ MaybeCreateNativeCaret(aWindow);
+ return NS_OK;
+ case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+ return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification);
+ case NOTIFY_IME_OF_FOCUS:
+ sFocusedWindow = aWindow;
+ IMMHandler::OnFocusChange(true, aWindow);
+ IMEHandler::MaybeShowOnScreenKeyboard(aWindow,
+ aWindow->GetInputContext());
+ MaybeCreateNativeCaret(aWindow);
+ return NS_OK;
+ case NOTIFY_IME_OF_BLUR:
+ sFocusedWindow = nullptr;
+ IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
+ IMMHandler::OnFocusChange(false, aWindow);
+ // If a plugin gets focus while TSF has focus, we need to notify TSF of
+ // the blur.
+ if (TSFTextStore::ThinksHavingFocus()) {
+ return TSFTextStore::OnFocusChange(false, aWindow,
+ aWindow->GetInputContext());
+ }
+ return NS_OK;
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+// static
+IMENotificationRequests IMEHandler::GetIMENotificationRequests() {
+ if (IsTSFAvailable()) {
+ if (!sIsIMMEnabled) {
+ return TSFTextStore::GetIMENotificationRequests();
+ }
+ // Even if TSF is available, the active IME may be an IMM-IME.
+ // Unfortunately, changing the result of GetIMENotificationRequests() while
+ // an editor has focus isn't supported by IMEContentObserver nor
+ // ContentCacheInParent. Therefore, we need to request whole notifications
+ // which are necessary either IMMHandler or TSFTextStore.
+ return IMMHandler::GetIMENotificationRequests() |
+ TSFTextStore::GetIMENotificationRequests();
+ }
+
+ return IMMHandler::GetIMENotificationRequests();
+}
+
+// static
+TextEventDispatcherListener*
+IMEHandler::GetNativeTextEventDispatcherListener() {
+ return WinTextEventDispatcherListener::GetInstance();
+}
+
+// static
+bool IMEHandler::GetOpenState(nsWindow* aWindow) {
+ if (IsTSFAvailable() && !IsIMMActive()) {
+ return TSFTextStore::GetIMEOpenState();
+ }
+
+ IMEContext context(aWindow);
+ return context.GetOpenState();
+}
+
+// static
+void IMEHandler::OnDestroyWindow(nsWindow* aWindow) {
+ // When focus is in remote process, but the window is being destroyed, we
+ // need to clean up TSFTextStore here since NOTIFY_IME_OF_BLUR won't reach
+ // here because BrowserParent already lost the reference to the nsWindow when
+ // it receives from the remote process.
+ if (sFocusedWindow == aWindow) {
+ MOZ_ASSERT(aWindow->GetInputContext().IsOriginContentProcess(),
+ "input context of focused widget should've been set by a remote "
+ "process "
+ "if IME focus isn't cleared before destroying the widget");
+ NotifyIME(aWindow, IMENotification(NOTIFY_IME_OF_BLUR));
+ }
+
+ // We need to do nothing here for TSF. Just restore the default context
+ // if it's been disassociated.
+ if (!sIsInTSFMode) {
+ // MSDN says we need to set IS_DEFAULT to avoid memory leak when we use
+ // SetInputScopes API. Use an empty string to do this.
+ SetInputScopeForIMM32(aWindow, u""_ns, u""_ns, false);
+ }
+ AssociateIMEContext(aWindow, true);
+}
+
+// static
+bool IMEHandler::NeedsToAssociateIMC() { return !sForceDisableCurrentIMM_IME; }
+
+// static
+void IMEHandler::SetInputContext(nsWindow* aWindow, InputContext& aInputContext,
+ const InputContextAction& aAction) {
+ sLastContextActionCause = aAction.mCause;
+ // FYI: If there is no composition, this call will do nothing.
+ NotifyIME(aWindow, IMENotification(REQUEST_TO_COMMIT_COMPOSITION));
+
+ if (aInputContext.mHTMLInputMode.EqualsLiteral("none")) {
+ IMEHandler::MaybeDismissOnScreenKeyboard(aWindow, Sync::Yes);
+ } else if (aAction.UserMightRequestOpenVKB()) {
+ IMEHandler::MaybeShowOnScreenKeyboard(aWindow, aInputContext);
+ }
+
+ bool enable = WinUtils::IsIMEEnabled(aInputContext);
+ bool adjustOpenState = (enable && aInputContext.mIMEState.mOpen !=
+ IMEState::DONT_CHANGE_OPEN_STATE);
+ bool open =
+ (adjustOpenState && aInputContext.mIMEState.mOpen == IMEState::OPEN);
+
+ // Note that even while a plugin has focus, we need to notify TSF of that.
+ if (sIsInTSFMode) {
+ TSFTextStore::SetInputContext(aWindow, aInputContext, aAction);
+ if (IsTSFAvailable()) {
+ if (sIsIMMEnabled) {
+ // Associate IMC with aWindow only when it's necessary.
+ AssociateIMEContext(aWindow, enable && NeedsToAssociateIMC());
+ }
+ if (adjustOpenState) {
+ TSFTextStore::SetIMEOpenState(open);
+ }
+ return;
+ }
+ } else {
+ // Set at least InputScope even when TextStore is not available.
+ SetInputScopeForIMM32(aWindow, aInputContext.mHTMLInputType,
+ aInputContext.mHTMLInputMode,
+ aInputContext.mInPrivateBrowsing);
+ }
+
+ AssociateIMEContext(aWindow, enable);
+
+ IMEContext context(aWindow);
+ if (adjustOpenState) {
+ context.SetOpenState(open);
+ }
+}
+
+// static
+void IMEHandler::AssociateIMEContext(nsWindow* aWindowBase, bool aEnable) {
+ IMEContext context(aWindowBase);
+ if (aEnable) {
+ context.AssociateDefaultContext();
+ return;
+ }
+ // Don't disassociate the context after the window is destroyed.
+ if (aWindowBase->Destroyed()) {
+ return;
+ }
+ context.Disassociate();
+}
+
+// static
+void IMEHandler::InitInputContext(nsWindow* aWindow,
+ InputContext& aInputContext) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aWindow->GetWindowHandle(),
+ "IMEHandler::SetInputContext() requires non-nullptr HWND");
+
+ static bool sInitialized = false;
+ if (!sInitialized) {
+ sInitialized = true;
+ // Some TIPs like QQ Input (Simplified Chinese) may need normal window
+ // (i.e., windows except message window) when initializing themselves.
+ // Therefore, we need to initialize TSF/IMM modules after first normal
+ // window is created. InitInputContext() should be called immediately
+ // after creating each normal window, so, here is a good place to
+ // initialize these modules.
+ Initialize();
+ }
+
+ // For a11y, the default enabled state should be 'enabled'.
+ aInputContext.mIMEState.mEnabled = IMEEnabled::Enabled;
+
+ if (sIsInTSFMode) {
+ TSFTextStore::SetInputContext(
+ aWindow, aInputContext,
+ InputContextAction(InputContextAction::CAUSE_UNKNOWN,
+ InputContextAction::WIDGET_CREATED));
+ // IME context isn't necessary in pure TSF mode.
+ if (!sIsIMMEnabled) {
+ AssociateIMEContext(aWindow, false);
+ }
+ return;
+ }
+
+#ifdef DEBUG
+ // NOTE: IMC may be null if IMM module isn't installed.
+ IMEContext context(aWindow);
+ MOZ_ASSERT(context.IsValid() || !CurrentKeyboardLayoutHasIME());
+#endif // #ifdef DEBUG
+}
+
+#ifdef DEBUG
+// static
+bool IMEHandler::CurrentKeyboardLayoutHasIME() {
+ if (sIsInTSFMode) {
+ return TSFTextStore::CurrentKeyboardLayoutHasIME();
+ }
+
+ return IMMHandler::IsIMEAvailable();
+}
+#endif // #ifdef DEBUG
+
+// static
+void IMEHandler::OnKeyboardLayoutChanged() {
+ // Be aware, this method won't be called until TSFStaticSink starts to
+ // observe active TIP change. If you need to be notified of this, you
+ // need to create TSFStaticSink::Observe() or something and call it
+ // TSFStaticSink::EnsureInitActiveTIPKeyboard() forcibly.
+
+ if (!sIsIMMEnabled || !IsTSFAvailable()) {
+ return;
+ }
+}
+
+// static
+void IMEHandler::SetInputScopeForIMM32(nsWindow* aWindow,
+ const nsAString& aHTMLInputType,
+ const nsAString& aHTMLInputMode,
+ bool aInPrivateBrowsing) {
+ if (sIsInTSFMode || !sSetInputScopes || aWindow->Destroyed()) {
+ return;
+ }
+ AutoTArray<InputScope, 3> scopes;
+
+ // IME may refer only first input scope, but we will append inputmode's
+ // input scopes since IME may refer it like Chrome.
+ AppendInputScopeFromType(aHTMLInputType, scopes);
+ AppendInputScopeFromInputMode(aHTMLInputMode, scopes);
+
+ if (aInPrivateBrowsing) {
+ scopes.AppendElement(IS_PRIVATE);
+ }
+
+ if (scopes.IsEmpty()) {
+ // At least, 1 item is necessary.
+ scopes.AppendElement(IS_DEFAULT);
+ }
+
+ sSetInputScopes(aWindow->GetWindowHandle(), scopes.Elements(),
+ scopes.Length(), nullptr, 0, nullptr, nullptr);
+}
+
+// static
+void IMEHandler::AppendInputScopeFromInputMode(const nsAString& aHTMLInputMode,
+ nsTArray<InputScope>& aScopes) {
+ if (aHTMLInputMode.EqualsLiteral("mozAwesomebar")) {
+ // Even if Awesomebar has focus, user may not input URL directly.
+ // However, on-screen keyboard for URL should be shown because it has
+ // some useful additional keys like ".com" and they are not hindrances
+ // even when inputting non-URL text, e.g., words to search something in
+ // the web. On the other hand, a lot of Microsoft's IMEs and Google
+ // Japanese Input make their open state "closed" automatically if we
+ // notify them of URL as the input scope. However, this is very annoying
+ // for the users when they try to input some words to search the web or
+ // bookmark/history items. Therefore, if they are active, we need to
+ // notify them of the default input scope for avoiding this issue.
+ // FYI: We cannot check active TIP without TSF. Therefore, if it's
+ // not in TSF mode, this will check only if active IMM-IME is Google
+ // Japanese Input. Google Japanese Input is a TIP of TSF basically.
+ // However, if the OS is Win7 or it's installed on Win7 but has not
+ // been updated yet even after the OS is upgraded to Win8 or later,
+ // it's installed as IMM-IME.
+ if (TSFTextStore::ShouldSetInputScopeOfURLBarToDefault()) {
+ return;
+ }
+ // Don't append IS_SEARCH here for showing on-screen keyboard for URL.
+ if (!aScopes.Contains(IS_URL)) {
+ aScopes.AppendElement(IS_URL);
+ }
+ return;
+ }
+
+ // https://html.spec.whatwg.org/dev/interaction.html#attr-inputmode
+ if (aHTMLInputMode.EqualsLiteral("url")) {
+ if (!aScopes.Contains(IS_SEARCH)) {
+ aScopes.AppendElement(IS_URL);
+ }
+ return;
+ }
+ if (aHTMLInputMode.EqualsLiteral("email")) {
+ if (!aScopes.Contains(IS_EMAIL_SMTPEMAILADDRESS)) {
+ aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS);
+ }
+ return;
+ }
+ if (aHTMLInputMode.EqualsLiteral("tel")) {
+ if (!aScopes.Contains(IS_TELEPHONE_FULLTELEPHONENUMBER)) {
+ aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER);
+ }
+ if (!aScopes.Contains(IS_TELEPHONE_LOCALNUMBER)) {
+ aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER);
+ }
+ return;
+ }
+ if (aHTMLInputMode.EqualsLiteral("numeric")) {
+ if (!aScopes.Contains(IS_DIGITS)) {
+ aScopes.AppendElement(IS_DIGITS);
+ }
+ return;
+ }
+ if (aHTMLInputMode.EqualsLiteral("decimal")) {
+ if (!aScopes.Contains(IS_NUMBER)) {
+ aScopes.AppendElement(IS_NUMBER);
+ }
+ return;
+ }
+ if (aHTMLInputMode.EqualsLiteral("search")) {
+ if (NeedsSearchInputScope() && !aScopes.Contains(IS_SEARCH)) {
+ aScopes.AppendElement(IS_SEARCH);
+ }
+ return;
+ }
+}
+
+// static
+void IMEHandler::AppendInputScopeFromType(const nsAString& aHTMLInputType,
+ nsTArray<InputScope>& aScopes) {
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html
+ if (aHTMLInputType.EqualsLiteral("url")) {
+ aScopes.AppendElement(IS_URL);
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("search")) {
+ if (NeedsSearchInputScope()) {
+ aScopes.AppendElement(IS_SEARCH);
+ }
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("email")) {
+ aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS);
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("password")) {
+ aScopes.AppendElement(IS_PASSWORD);
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("datetime") ||
+ aHTMLInputType.EqualsLiteral("datetime-local")) {
+ aScopes.AppendElement(IS_DATE_FULLDATE);
+ aScopes.AppendElement(IS_TIME_FULLTIME);
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("date") ||
+ aHTMLInputType.EqualsLiteral("month") ||
+ aHTMLInputType.EqualsLiteral("week")) {
+ aScopes.AppendElement(IS_DATE_FULLDATE);
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("time")) {
+ aScopes.AppendElement(IS_TIME_FULLTIME);
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("tel")) {
+ aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER);
+ aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER);
+ return;
+ }
+ if (aHTMLInputType.EqualsLiteral("number")) {
+ aScopes.AppendElement(IS_NUMBER);
+ return;
+ }
+}
+
+// static
+bool IMEHandler::NeedsSearchInputScope() {
+ return !StaticPrefs::intl_tsf_hack_atok_search_input_scope_disabled() ||
+ !TSFTextStore::IsATOKActive();
+}
+
+// static
+bool IMEHandler::IsOnScreenKeyboardSupported() {
+#ifdef NIGHTLY_BUILD
+ if (FxRWindowManager::GetInstance()->IsFxRWindow(sFocusedWindow)) {
+ return true;
+ }
+#endif // NIGHTLY_BUILD
+ if (!Preferences::GetBool(kOskEnabled, true) ||
+ !IMEHandler::NeedOnScreenKeyboard()) {
+ return false;
+ }
+
+ // On Windows 11, we ignore tablet mode (see bug 1722208)
+ 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()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// static
+void IMEHandler::MaybeShowOnScreenKeyboard(nsWindow* aWindow,
+ const InputContext& aInputContext) {
+ if (aInputContext.mHTMLInputMode.EqualsLiteral("none")) {
+ return;
+ }
+
+ if (!IsOnScreenKeyboardSupported()) {
+ return;
+ }
+
+ IMEHandler::ShowOnScreenKeyboard(aWindow);
+}
+
+// static
+void IMEHandler::MaybeDismissOnScreenKeyboard(nsWindow* aWindow, Sync aSync) {
+#ifdef NIGHTLY_BUILD
+ if (FxRWindowManager::GetInstance()->IsFxRWindow(aWindow)) {
+ OSKVRManager::DismissOnScreenKeyboard();
+ }
+#endif // NIGHTLY_BUILD
+ if (aSync == Sync::Yes) {
+ DismissOnScreenKeyboard(aWindow);
+ return;
+ }
+
+ RefPtr<nsWindow> window(aWindow);
+ NS_DispatchToCurrentThreadQueue(
+ NS_NewRunnableFunction("IMEHandler::MaybeDismissOnScreenKeyboard",
+ [window]() {
+ if (window->Destroyed()) {
+ return;
+ }
+ if (!sFocusedWindow) {
+ DismissOnScreenKeyboard(window);
+ }
+ }),
+ EventQueuePriority::Idle);
+}
+
+// static
+bool IMEHandler::WStringStartsWithCaseInsensitive(const std::wstring& aHaystack,
+ const std::wstring& aNeedle) {
+ std::wstring lowerCaseHaystack(aHaystack);
+ std::wstring lowerCaseNeedle(aNeedle);
+ std::transform(lowerCaseHaystack.begin(), lowerCaseHaystack.end(),
+ lowerCaseHaystack.begin(), ::tolower);
+ std::transform(lowerCaseNeedle.begin(), lowerCaseNeedle.end(),
+ lowerCaseNeedle.begin(), ::tolower);
+ return wcsstr(lowerCaseHaystack.c_str(), lowerCaseNeedle.c_str()) ==
+ lowerCaseHaystack.c_str();
+}
+
+// Returns false if a physical keyboard is detected on Windows 8 and up,
+// or there is some other reason why an onscreen keyboard is not necessary.
+// Returns true if no keyboard is found and this device looks like it needs
+// an on-screen keyboard for text input.
+// static
+bool IMEHandler::NeedOnScreenKeyboard() {
+ if (!Preferences::GetBool(kOskDetectPhysicalKeyboard, true)) {
+ Preferences::SetString(kOskDebugReason, L"IKPOS: Detection disabled.");
+ return true;
+ }
+
+ // If the last focus cause was not user-initiated (ie a result of code
+ // setting focus to an element) then don't auto-show a keyboard. This
+ // avoids cases where the keyboard would pop up "just" because e.g. a
+ // web page chooses to focus a search field on the page, even when that
+ // really isn't what the user is trying to do at that moment.
+ if (!InputContextAction::IsHandlingUserInput(sLastContextActionCause)) {
+ return false;
+ }
+
+ // This function should be only invoked for machines with touch screens.
+ if ((::GetSystemMetrics(SM_DIGITIZER) & NID_INTEGRATED_TOUCH) !=
+ NID_INTEGRATED_TOUCH) {
+ Preferences::SetString(kOskDebugReason, L"IKPOS: Touch screen not found.");
+ return false;
+ }
+
+ // If the device is docked, the user is treating the device as a PC.
+ if (::GetSystemMetrics(SM_SYSTEMDOCKED) != 0) {
+ Preferences::SetString(kOskDebugReason, L"IKPOS: System docked.");
+ return false;
+ }
+
+ // To determine whether a keyboard is present on the device, we do the
+ // following:-
+ // 1. If the platform role is that of a mobile or slate device, check the
+ // system metric SM_CONVERTIBLESLATEMODE to see if it is being used
+ // in slate mode. If it is, also check that the last input was a touch.
+ // If all of this is true, then we should show the on-screen keyboard.
+
+ // 2. If step 1 didn't determine we should show the keyboard, we check if
+ // this device has keyboards attached to it.
+
+ // Check if the device is being used as a laptop or a tablet. This can be
+ // checked by first checking the role of the device and then the
+ // corresponding system metric (SM_CONVERTIBLESLATEMODE). If it is being
+ // used as a tablet then we want the OSK to show up.
+ if (!sDeterminedPowerPlatformRole) {
+ sDeterminedPowerPlatformRole = true;
+ sPowerPlatformRole = WinUtils::GetPowerPlatformRole();
+ }
+
+ // If this a mobile or slate (tablet) device, check if it is in slate mode.
+ // If the last input was touch, ignore whether or not a keyboard is present.
+ if ((sPowerPlatformRole == PlatformRoleMobile ||
+ sPowerPlatformRole == PlatformRoleSlate) &&
+ ::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) == 0 &&
+ sLastContextActionCause == InputContextAction::CAUSE_TOUCH) {
+ Preferences::SetString(
+ kOskDebugReason,
+ L"IKPOS: Mobile/Slate Platform role, in slate mode with touch event.");
+ return true;
+ }
+
+ return !IMEHandler::IsKeyboardPresentOnSlate();
+}
+
+// Uses the Setup APIs to enumerate the attached keyboards and returns true
+// if the keyboard count is 1 or more. While this will work in most cases
+// it won't work if there are devices which expose keyboard interfaces which
+// are attached to the machine.
+// Based on IsKeyboardPresentOnSlate() in Chromium's base/win/win_util.cc.
+// static
+bool IMEHandler::IsKeyboardPresentOnSlate() {
+ const GUID KEYBOARD_CLASS_GUID = {
+ 0x4D36E96B,
+ 0xE325,
+ 0x11CE,
+ {0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18}};
+
+ // Query for all the keyboard devices.
+ HDEVINFO device_info = ::SetupDiGetClassDevs(&KEYBOARD_CLASS_GUID, nullptr,
+ nullptr, DIGCF_PRESENT);
+ if (device_info == INVALID_HANDLE_VALUE) {
+ Preferences::SetString(kOskDebugReason, L"IKPOS: No keyboard info.");
+ return false;
+ }
+
+ // Enumerate all keyboards and look for ACPI\PNP and HID\VID devices. If
+ // the count is more than 1 we assume that a keyboard is present. This is
+ // under the assumption that there will always be one keyboard device.
+ for (DWORD i = 0;; ++i) {
+ SP_DEVINFO_DATA device_info_data = {0};
+ device_info_data.cbSize = sizeof(device_info_data);
+ if (!::SetupDiEnumDeviceInfo(device_info, i, &device_info_data)) {
+ break;
+ }
+
+ // Get the device ID.
+ wchar_t device_id[MAX_DEVICE_ID_LEN];
+ CONFIGRET status = ::CM_Get_Device_ID(device_info_data.DevInst, device_id,
+ MAX_DEVICE_ID_LEN, 0);
+ if (status == CR_SUCCESS) {
+ static const std::wstring BT_HID_DEVICE = L"HID\\{00001124";
+ static const std::wstring BT_HOGP_DEVICE = L"HID\\{00001812";
+ // To reduce the scope of the hack we only look for ACPI and HID\\VID
+ // prefixes in the keyboard device ids.
+ if (IMEHandler::WStringStartsWithCaseInsensitive(device_id, L"ACPI") ||
+ IMEHandler::WStringStartsWithCaseInsensitive(device_id,
+ L"HID\\VID") ||
+ IMEHandler::WStringStartsWithCaseInsensitive(device_id,
+ BT_HID_DEVICE) ||
+ IMEHandler::WStringStartsWithCaseInsensitive(device_id,
+ BT_HOGP_DEVICE)) {
+ // The heuristic we are using is to check the count of keyboards and
+ // return true if the API's report one or more keyboards. Please note
+ // that this will break for non keyboard devices which expose a
+ // keyboard PDO.
+ Preferences::SetString(kOskDebugReason,
+ L"IKPOS: Keyboard presence confirmed.");
+ return true;
+ }
+ }
+ }
+ Preferences::SetString(kOskDebugReason,
+ L"IKPOS: Lack of keyboard confirmed.");
+ return false;
+}
+
+// static
+bool IMEHandler::IsInTabletMode() {
+ bool isInTabletMode = WindowsUIUtils::GetInTabletMode();
+ if (isInTabletMode) {
+ Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=true.");
+ } else {
+ Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=false.");
+ }
+ return isInTabletMode;
+}
+
+static bool ReadEnableDesktopModeAutoInvoke(uint32_t aRoot,
+ nsIWindowsRegKey* aRegKey,
+ uint32_t& aValue) {
+ nsresult rv;
+ rv = aRegKey->Open(aRoot, u"SOFTWARE\\Microsoft\\TabletTip\\1.7"_ns,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) {
+ Preferences::SetString(kOskDebugReason,
+ L"AIOSKIDM: failed opening regkey.");
+ return false;
+ }
+ // EnableDesktopModeAutoInvoke is an opt-in option from the Windows
+ // Settings to "Automatically show the touch keyboard in windowed apps
+ // when there's no keyboard attached to your device." If the user has
+ // opted-in to this behavior, the tablet-mode requirement is skipped.
+ rv = aRegKey->ReadIntValue(u"EnableDesktopModeAutoInvoke"_ns, &aValue);
+ if (NS_FAILED(rv)) {
+ Preferences::SetString(kOskDebugReason,
+ L"AIOSKIDM: failed reading value of regkey.");
+ return false;
+ }
+ return true;
+}
+
+// static
+bool IMEHandler::AutoInvokeOnScreenKeyboardInDesktopMode() {
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> regKey(
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Preferences::SetString(kOskDebugReason,
+ L"AIOSKIDM: "
+ L"nsIWindowsRegKey not available");
+ return false;
+ }
+
+ uint32_t value;
+ if (!ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ regKey, value) &&
+ !ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE,
+ regKey, value)) {
+ return false;
+ }
+ if (!!value) {
+ Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=true.");
+ } else {
+ Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=false.");
+ }
+ return !!value;
+}
+
+// Based on DisplayVirtualKeyboard() in Chromium's base/win/win_util.cc.
+// static
+void IMEHandler::ShowOnScreenKeyboard(nsWindow* aWindow) {
+#ifdef NIGHTLY_BUILD
+ if (FxRWindowManager::GetInstance()->IsFxRWindow(sFocusedWindow)) {
+ OSKVRManager::ShowOnScreenKeyboard();
+ return;
+ }
+#endif // NIGHTLY_BUILD
+
+ if (IsWin10AnniversaryUpdateOrLater()) {
+ OSKInputPaneManager::ShowOnScreenKeyboard(aWindow->GetWindowHandle());
+ return;
+ }
+
+ OSKTabTipManager::ShowOnScreenKeyboard();
+}
+
+// Based on DismissVirtualKeyboard() in Chromium's base/win/win_util.cc.
+// static
+void IMEHandler::DismissOnScreenKeyboard(nsWindow* aWindow) {
+ // Dismiss the virtual keyboard if it's open
+ if (IsWin10AnniversaryUpdateOrLater()) {
+ OSKInputPaneManager::DismissOnScreenKeyboard(aWindow->GetWindowHandle());
+ return;
+ }
+
+ OSKTabTipManager::DismissOnScreenKeyboard();
+}
+
+bool IMEHandler::MaybeCreateNativeCaret(nsWindow* aWindow) {
+ MOZ_ASSERT(aWindow);
+
+ if (IsA11yHandlingNativeCaret()) {
+ return false;
+ }
+
+ if (!sHasNativeCaretBeenRequested) {
+ // If we have not received WM_GETOBJECT for OBJID_CARET, there may be new
+ // application which requires our caret information. For kicking its
+ // window event proc, we should fire a window event here.
+ // (If there is such application, sHasNativeCaretBeenRequested will be set
+ // to true later.)
+ // FYI: If we create native caret and move its position, native caret
+ // causes EVENT_OBJECT_LOCATIONCHANGE event with OBJID_CARET and
+ // OBJID_CLIENT.
+ ::NotifyWinEvent(EVENT_OBJECT_LOCATIONCHANGE, aWindow->GetWindowHandle(),
+ OBJID_CARET, OBJID_CLIENT);
+ return false;
+ }
+
+ MaybeDestroyNativeCaret();
+
+ // If focused content is not text editable, we don't support caret
+ // caret information without a11y module.
+ if (!aWindow->GetInputContext().mIMEState.IsEditable()) {
+ return false;
+ }
+
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWindow);
+ aWindow->InitEvent(queryCaretRectEvent);
+
+ WidgetQueryContentEvent::Options options;
+ options.mRelativeToInsertionPoint = true;
+ queryCaretRectEvent.InitForQueryCaretRect(0, options);
+
+ aWindow->DispatchWindowEvent(queryCaretRectEvent);
+ if (NS_WARN_IF(queryCaretRectEvent.Failed())) {
+ return false;
+ }
+
+ return CreateNativeCaret(aWindow, queryCaretRectEvent.mReply->mRect);
+}
+
+bool IMEHandler::CreateNativeCaret(nsWindow* aWindow,
+ const LayoutDeviceIntRect& aCaretRect) {
+ MOZ_ASSERT(aWindow);
+
+ MOZ_ASSERT(!IsA11yHandlingNativeCaret());
+
+ sNativeCaretIsCreated =
+ ::CreateCaret(aWindow->GetWindowHandle(), nullptr, aCaretRect.Width(),
+ aCaretRect.Height());
+ if (!sNativeCaretIsCreated) {
+ return false;
+ }
+ nsWindow* toplevelWindow = aWindow->GetTopLevelWindow(false);
+ if (NS_WARN_IF(!toplevelWindow)) {
+ MaybeDestroyNativeCaret();
+ return false;
+ }
+
+ LayoutDeviceIntPoint caretPosition(aCaretRect.TopLeft());
+ if (toplevelWindow != aWindow) {
+ caretPosition += toplevelWindow->WidgetToScreenOffset();
+ caretPosition -= aWindow->WidgetToScreenOffset();
+ }
+
+ ::SetCaretPos(caretPosition.x, caretPosition.y);
+ return true;
+}
+
+void IMEHandler::MaybeDestroyNativeCaret() {
+ if (!sNativeCaretIsCreated) {
+ return;
+ }
+ ::DestroyCaret();
+ sNativeCaretIsCreated = false;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WinIMEHandler.h b/widget/windows/WinIMEHandler.h
new file mode 100644
index 0000000000..410e1ebd22
--- /dev/null
+++ b/widget/windows/WinIMEHandler.h
@@ -0,0 +1,247 @@
+/* -*- 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/. */
+
+#ifndef WinIMEHandler_h_
+#define WinIMEHandler_h_
+
+#include "nscore.h"
+#include "nsWindow.h"
+#include "npapi.h"
+#include <windows.h>
+#include <inputscope.h>
+
+#define NS_WM_IMEFIRST WM_IME_SETCONTEXT
+#define NS_WM_IMELAST WM_IME_KEYUP
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+struct MSGResult;
+
+/**
+ * IMEHandler class is a mediator class. On Windows, there are two IME API
+ * sets: One is IMM which is legacy API set. The other is TSF which is modern
+ * API set. By using this class, non-IME handler classes don't need to worry
+ * that we're in which mode.
+ */
+class IMEHandler final {
+ private:
+ /**
+ * Initialize() initializes both TSF modules and IMM modules. Some TIPs
+ * may require a normal window (i.e., not message window) belonging to
+ * this process. Therefore, this is called immediately after first normal
+ * window is created.
+ */
+ static void Initialize();
+
+ public:
+ static void Terminate();
+
+ /**
+ * Returns TSF related native data or native IME context.
+ */
+ static void* GetNativeData(nsWindow* aWindow, uint32_t aDataType);
+
+ /**
+ * ProcessRawKeyMessage() message is called before calling TranslateMessage()
+ * and DispatchMessage(). If this returns true, the message is consumed.
+ * Then, caller must not perform TranslateMessage() nor DispatchMessage().
+ */
+ static bool ProcessRawKeyMessage(const MSG& aMsg);
+
+ /**
+ * When the message is not needed to handle anymore by the caller, this
+ * returns true. Otherwise, false.
+ */
+ static bool ProcessMessage(nsWindow* aWindow, UINT aMessage, WPARAM& aWParam,
+ LPARAM& aLParam, MSGResult& aResult);
+
+ /**
+ * IsA11yHandlingNativeCaret() returns true if a11y is handling
+ * native caret. In such case, IME modules shouldn't touch native caret.
+ **/
+ static bool IsA11yHandlingNativeCaret();
+
+ /**
+ * NeedsToCreateNativeCaret() returns true if IME handler needs to create
+ * native caret for other applications which requests OBJID_CARET with
+ * WM_GETOBJECT and a11y module isn't active (if a11y module is active,
+ * it always creates native caret, i.e., even if no editor has focus).
+ */
+ static bool NeedsToCreateNativeCaret() {
+ return sHasNativeCaretBeenRequested && !IsA11yHandlingNativeCaret();
+ }
+
+ /**
+ * CreateNativeCaret() create native caret if this has been created it.
+ *
+ * @param aWindow The window which owns the caret.
+ * @param aCaretRect The caret rect relative to aWindow.
+ */
+ static bool CreateNativeCaret(nsWindow* aWindow,
+ const LayoutDeviceIntRect& aCaretRect);
+
+ /**
+ * MaybeDestroyNativeCaret() destroies native caret if it has been created
+ * by IMEHandler.
+ */
+ static void MaybeDestroyNativeCaret();
+
+ /**
+ * HasNativeCaret() returns true if there is native caret and it was created
+ * by IMEHandler.
+ */
+ static bool HasNativeCaret() { return sNativeCaretIsCreated; }
+
+ /**
+ * When there is a composition, returns true. Otherwise, false.
+ */
+ static bool IsComposing();
+
+ /**
+ * When there is a composition and it's in the window, returns true.
+ * Otherwise, false.
+ */
+ static bool IsComposingOn(nsWindow* aWindow);
+
+ /**
+ * Notifies IME of the notification (a request or an event).
+ */
+ static nsresult NotifyIME(nsWindow* aWindow,
+ const IMENotification& aIMENotification);
+
+ /**
+ * Returns notification requests of IME.
+ */
+ static IMENotificationRequests GetIMENotificationRequests();
+
+ /**
+ * Returns native text event dispatcher listener.
+ */
+ static TextEventDispatcherListener* GetNativeTextEventDispatcherListener();
+
+ /**
+ * Returns IME open state on the window.
+ */
+ static bool GetOpenState(nsWindow* aWindow);
+
+ /**
+ * Called when the window is destroying.
+ */
+ static void OnDestroyWindow(nsWindow* aWindow);
+
+ /**
+ * Called when nsIWidget::SetInputContext() is called before the window's
+ * InputContext is modified actually.
+ */
+ static void SetInputContext(nsWindow* aWindow, InputContext& aInputContext,
+ const InputContextAction& aAction);
+
+ /**
+ * Associate or disassociate IME context to/from the aWindowBase.
+ */
+ static void AssociateIMEContext(nsWindow* aWindowBase, bool aEnable);
+
+ /**
+ * Called when the window is created.
+ */
+ static void InitInputContext(nsWindow* aWindow, InputContext& aInputContext);
+
+ /**
+ * This is called by TSFStaticSink when active IME is changed.
+ */
+ static void OnKeyboardLayoutChanged();
+
+#ifdef DEBUG
+ /**
+ * Returns true when current keyboard layout has IME. Otherwise, false.
+ */
+ static bool CurrentKeyboardLayoutHasIME();
+#endif // #ifdef DEBUG
+
+ /**
+ * Append InputScope values from inputmode string.
+ */
+ static void AppendInputScopeFromInputMode(const nsAString& aHTMLInputMode,
+ nsTArray<InputScope>& aScopes);
+
+ /**
+ * Append InputScope values from type attreibute string of input element
+ */
+ static void AppendInputScopeFromType(const nsAString& aInputType,
+ nsTArray<InputScope>& aScopes);
+
+ /**
+ * Return focused window if this receives focus notification and has not
+ * received blur notification yet.
+ */
+ static nsWindow* GetFocusedWindow() { return sFocusedWindow; }
+
+ private:
+ static nsWindow* sFocusedWindow;
+ static InputContextAction::Cause sLastContextActionCause;
+
+ static bool sMaybeEditable;
+ static bool sForceDisableCurrentIMM_IME;
+ static bool sNativeCaretIsCreated;
+ static bool sHasNativeCaretBeenRequested;
+
+ /**
+ * MaybeCreateNativeCaret() may create native caret over our caret if
+ * focused content is text editable and we need to create native caret
+ * for other applications.
+ *
+ * @param aWindow The window which owns the native caret.
+ */
+ static bool MaybeCreateNativeCaret(nsWindow* aWindow);
+
+ static decltype(SetInputScopes)* sSetInputScopes;
+ static void SetInputScopeForIMM32(nsWindow* aWindow,
+ const nsAString& aHTMLInputType,
+ const nsAString& aHTMLInputMode,
+ bool aInPrivateBrowsing);
+ static bool sIsInTSFMode;
+ // If sIMMEnabled is false, any IME messages are not handled in TSF mode.
+ // Additionally, IME context is always disassociated from focused window.
+ static bool sIsIMMEnabled;
+
+ static bool IsTSFAvailable() { return sIsInTSFMode; }
+ static bool IsIMMActive();
+
+ static bool IsOnScreenKeyboardSupported();
+
+ static void MaybeShowOnScreenKeyboard(nsWindow* aWindow,
+ const InputContext& aInputContext);
+ enum class Sync { Yes, No };
+ static void MaybeDismissOnScreenKeyboard(nsWindow* aWindow,
+ Sync aSync = Sync::No);
+ static bool WStringStartsWithCaseInsensitive(const std::wstring& aHaystack,
+ const std::wstring& aNeedle);
+ static bool NeedOnScreenKeyboard();
+ static bool IsKeyboardPresentOnSlate();
+ static bool IsInTabletMode();
+ static bool AutoInvokeOnScreenKeyboardInDesktopMode();
+ static bool NeedsToAssociateIMC();
+ static bool NeedsSearchInputScope();
+
+ /**
+ * Show the Windows on-screen keyboard. Only allowed for
+ * chrome documents and Windows 8 and higher.
+ */
+ static void ShowOnScreenKeyboard(nsWindow* aWindow);
+
+ /**
+ * Dismiss the Windows on-screen keyboard. Only allowed for
+ * Windows 8 and higher.
+ */
+ static void DismissOnScreenKeyboard(nsWindow* aWindow);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef WinIMEHandler_h_
diff --git a/widget/windows/WinMessages.h b/widget/windows/WinMessages.h
new file mode 100644
index 0000000000..8c90460e8e
--- /dev/null
+++ b/widget/windows/WinMessages.h
@@ -0,0 +1,93 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_WinMessages_h_
+#define mozilla_widget_WinMessages_h_
+
+/*****************************************************************************
+ * MOZ_WM_* messages
+ ****************************************************************************/
+
+// A magic APP message that can be sent to quit, sort of like a
+// QUERYENDSESSION/ENDSESSION, but without the query.
+#define MOZ_WM_APP_QUIT (WM_APP + 0x0300)
+// Used as a "tracer" event to probe event loop latency.
+#define MOZ_WM_TRACE (WM_APP + 0x0301)
+// accessibility priming
+#define MOZ_WM_STARTA11Y (WM_APP + 0x0302)
+// Our internal message for WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_VSCROLL and
+// WM_HSCROLL
+#define MOZ_WM_MOUSEVWHEEL (WM_APP + 0x0310)
+#define MOZ_WM_MOUSEHWHEEL (WM_APP + 0x0311)
+#define MOZ_WM_VSCROLL (WM_APP + 0x0312)
+#define MOZ_WM_HSCROLL (WM_APP + 0x0313)
+#define MOZ_WM_MOUSEWHEEL_FIRST MOZ_WM_MOUSEVWHEEL
+#define MOZ_WM_MOUSEWHEEL_LAST MOZ_WM_HSCROLL
+// If a popup window is being activated, we try to reactivate the previous
+// window with this message.
+#define MOZ_WM_REACTIVATE (WM_APP + 0x0314)
+// If TSFTextStore needs to notify TSF/TIP of layout change later, this
+// message is posted.
+#define MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE (WM_APP + 0x0315)
+// Internal message used in correcting backwards clock skew
+#define MOZ_WM_SKEWFIX (WM_APP + 0x0316)
+// Internal message used for rolling up popups for dmanip events
+#define MOZ_WM_DMANIP (WM_APP + 0x0317)
+
+// Internal message used to work around race condition in explorer.exe's
+// fullscreen window-state update handler in Windows 10+. (See bug 1835851.)
+#define MOZ_WM_FULLSCREEN_STATE_UPDATE (WM_APP + 0x0318)
+
+// XXX Should rename them to MOZ_WM_* and use safer values!
+// Messages for fullscreen transition window
+#define WM_FULLSCREEN_TRANSITION_BEFORE (WM_USER + 0)
+#define WM_FULLSCREEN_TRANSITION_AFTER (WM_USER + 1)
+
+#ifndef APPCOMMAND_BROWSER_BACKWARD
+# define APPCOMMAND_BROWSER_BACKWARD 1
+# define APPCOMMAND_BROWSER_FORWARD 2
+# define APPCOMMAND_BROWSER_REFRESH 3
+# define APPCOMMAND_BROWSER_STOP 4
+# define APPCOMMAND_BROWSER_SEARCH 5
+# define APPCOMMAND_BROWSER_FAVORITES 6
+# define APPCOMMAND_BROWSER_HOME 7
+
+# define APPCOMMAND_MEDIA_NEXTTRACK 11
+# define APPCOMMAND_MEDIA_PREVIOUSTRACK 12
+# define APPCOMMAND_MEDIA_STOP 13
+# define APPCOMMAND_MEDIA_PLAY_PAUSE 14
+
+/*
+ * Additional commands currently not in use.
+ *
+ *#define APPCOMMAND_VOLUME_MUTE 8
+ *#define APPCOMMAND_VOLUME_DOWN 9
+ *#define APPCOMMAND_VOLUME_UP 10
+ *#define APPCOMMAND_LAUNCH_MAIL 15
+ *#define APPCOMMAND_LAUNCH_MEDIA_SELECT 16
+ *#define APPCOMMAND_LAUNCH_APP1 17
+ *#define APPCOMMAND_LAUNCH_APP2 18
+ *#define APPCOMMAND_BASS_DOWN 19
+ *#define APPCOMMAND_BASS_BOOST 20
+ *#define APPCOMMAND_BASS_UP 21
+ *#define APPCOMMAND_TREBLE_DOWN 22
+ *#define APPCOMMAND_TREBLE_UP 23
+ *#define FAPPCOMMAND_MOUSE 0x8000
+ *#define FAPPCOMMAND_KEY 0
+ *#define FAPPCOMMAND_OEM 0x1000
+ */
+
+# define GET_APPCOMMAND_LPARAM(lParam) \
+ ((short)(HIWORD(lParam) & ~FAPPCOMMAND_MASK))
+
+/*
+ *#define GET_DEVICE_LPARAM(lParam) ((WORD)(HIWORD(lParam) &
+ *FAPPCOMMAND_MASK)) #define GET_MOUSEORKEY_LPARAM GET_DEVICE_LPARAM
+ *#define GET_FLAGS_LPARAM(lParam) (LOWORD(lParam))
+ *#define GET_KEYSTATE_LPARAM(lParam) GET_FLAGS_LPARAM(lParam)
+ */
+#endif // #ifndef APPCOMMAND_BROWSER_BACKWARD
+
+#endif // #ifndef mozilla_widget_WinMessages_h_
diff --git a/widget/windows/WinModifierKeyState.h b/widget/windows/WinModifierKeyState.h
new file mode 100644
index 0000000000..0788c25939
--- /dev/null
+++ b/widget/windows/WinModifierKeyState.h
@@ -0,0 +1,59 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_WinModifierKeyState_h_
+#define mozilla_widget_WinModifierKeyState_h_
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/EventForwards.h"
+#include "nsStringFwd.h"
+#include <windows.h>
+
+namespace mozilla {
+namespace widget {
+
+class MOZ_STACK_CLASS ModifierKeyState final {
+ public:
+ ModifierKeyState();
+ explicit ModifierKeyState(Modifiers aModifiers);
+
+ void Update();
+
+ void Unset(Modifiers aRemovingModifiers);
+ void Set(Modifiers aAddingModifiers);
+
+ void InitInputEvent(WidgetInputEvent& aInputEvent) const;
+
+ // Do not create IsAltGr() because it's unclear whether:
+ // - AltGr key is actually pressed.
+ // - Both Ctrl and Alt keys are pressed when a keyboard layout which
+ // has AltGr key.
+ // - Both Ctrl and Alt keys are pressed when a keyboard layout which
+ // does not have AltGr key.
+ bool IsShift() const;
+ bool IsControl() const;
+ bool IsAlt() const;
+ bool IsWin() const;
+
+ bool MaybeMatchShortcutKey() const;
+
+ bool IsCapsLocked() const;
+ bool IsNumLocked() const;
+ bool IsScrollLocked() const;
+
+ MOZ_ALWAYS_INLINE Modifiers GetModifiers() const { return mModifiers; }
+
+ private:
+ Modifiers mModifiers;
+
+ void InitMouseEvent(WidgetInputEvent& aMouseEvent) const;
+};
+
+const nsCString ToString(const ModifierKeyState& aModifierKeyState);
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef mozilla_widget_WinModifierKeyState_h_
diff --git a/widget/windows/WinMouseScrollHandler.cpp b/widget/windows/WinMouseScrollHandler.cpp
new file mode 100644
index 0000000000..e10f7b29dc
--- /dev/null
+++ b/widget/windows/WinMouseScrollHandler.cpp
@@ -0,0 +1,1634 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 "mozilla/DebugOnly.h"
+
+#include "mozilla/Logging.h"
+
+#include "WinMouseScrollHandler.h"
+#include "nsWindow.h"
+#include "nsWindowDefs.h"
+#include "KeyboardLayout.h"
+#include "WinUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIDOMWindowUtils.h"
+
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/WheelEventBinding.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+#include "mozilla/widget/WinRegistry.h"
+
+#include <psapi.h>
+
+namespace mozilla {
+namespace widget {
+
+LazyLogModule gMouseScrollLog("MouseScrollHandlerWidgets");
+
+static const char* GetBoolName(bool aBool) { return aBool ? "TRUE" : "FALSE"; }
+
+MouseScrollHandler* MouseScrollHandler::sInstance = nullptr;
+
+bool MouseScrollHandler::Device::sFakeScrollableWindowNeeded = false;
+
+bool MouseScrollHandler::Device::SynTP::sInitialized = false;
+int32_t MouseScrollHandler::Device::SynTP::sMajorVersion = 0;
+int32_t MouseScrollHandler::Device::SynTP::sMinorVersion = -1;
+
+bool MouseScrollHandler::Device::Elantech::sUseSwipeHack = false;
+bool MouseScrollHandler::Device::Elantech::sUsePinchHack = false;
+DWORD MouseScrollHandler::Device::Elantech::sZoomUntil = 0;
+
+bool MouseScrollHandler::Device::Apoint::sInitialized = false;
+int32_t MouseScrollHandler::Device::Apoint::sMajorVersion = 0;
+int32_t MouseScrollHandler::Device::Apoint::sMinorVersion = -1;
+
+bool MouseScrollHandler::Device::SetPoint::sMightBeUsing = false;
+
+// The duration until timeout of events transaction. The value is 1.5 sec,
+// it's just a magic number, it was suggested by Logitech's engineer, see
+// bug 605648 comment 90.
+#define DEFAULT_TIMEOUT_DURATION 1500
+
+/******************************************************************************
+ *
+ * MouseScrollHandler
+ *
+ ******************************************************************************/
+
+/* static */
+POINTS
+MouseScrollHandler::GetCurrentMessagePos() {
+ if (SynthesizingEvent::IsSynthesizing()) {
+ return sInstance->mSynthesizingEvent->GetCursorPoint();
+ }
+ DWORD pos = ::GetMessagePos();
+ return MAKEPOINTS(pos);
+}
+
+// Get rid of the GetMessagePos() API.
+#define GetMessagePos()
+
+/* static */
+void MouseScrollHandler::Initialize() { Device::Init(); }
+
+/* static */
+void MouseScrollHandler::Shutdown() {
+ delete sInstance;
+ sInstance = nullptr;
+}
+
+/* static */
+MouseScrollHandler* MouseScrollHandler::GetInstance() {
+ if (!sInstance) {
+ sInstance = new MouseScrollHandler();
+ }
+ return sInstance;
+}
+
+MouseScrollHandler::MouseScrollHandler()
+ : mIsWaitingInternalMessage(false), mSynthesizingEvent(nullptr) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll: Creating an instance, this=%p, sInstance=%p", this,
+ sInstance));
+}
+
+MouseScrollHandler::~MouseScrollHandler() {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll: Destroying an instance, this=%p, sInstance=%p", this,
+ sInstance));
+
+ delete mSynthesizingEvent;
+}
+
+/* static */
+void MouseScrollHandler::MaybeLogKeyState() {
+ if (!MOZ_LOG_TEST(gMouseScrollLog, LogLevel::Debug)) {
+ return;
+ }
+ BYTE keyboardState[256];
+ if (::GetKeyboardState(keyboardState)) {
+ for (size_t i = 0; i < ArrayLength(keyboardState); i++) {
+ if (keyboardState[i]) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Debug,
+ (" Current key state: keyboardState[0x%02zX]=0x%02X (%s)", i,
+ keyboardState[i],
+ ((keyboardState[i] & 0x81) == 0x81) ? "Pressed and Toggled"
+ : (keyboardState[i] & 0x80) ? "Pressed"
+ : (keyboardState[i] & 0x01) ? "Toggled"
+ : "Unknown"));
+ }
+ }
+ } else {
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Debug,
+ ("MouseScroll::MaybeLogKeyState(): Failed to print current keyboard "
+ "state"));
+ }
+}
+
+/* static */
+bool MouseScrollHandler::NeedsMessage(UINT aMsg) {
+ switch (aMsg) {
+ case WM_SETTINGCHANGE:
+ case WM_MOUSEWHEEL:
+ case WM_MOUSEHWHEEL:
+ case WM_HSCROLL:
+ case WM_VSCROLL:
+ case MOZ_WM_MOUSEVWHEEL:
+ case MOZ_WM_MOUSEHWHEEL:
+ case MOZ_WM_HSCROLL:
+ case MOZ_WM_VSCROLL:
+ case WM_KEYDOWN:
+ case WM_KEYUP:
+ return true;
+ }
+ return false;
+}
+
+/* static */
+bool MouseScrollHandler::ProcessMessage(nsWindow* aWidget, UINT msg,
+ WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ Device::Elantech::UpdateZoomUntil();
+
+ switch (msg) {
+ case WM_SETTINGCHANGE:
+ if (!sInstance) {
+ return false;
+ }
+ if (wParam == SPI_SETWHEELSCROLLLINES ||
+ wParam == SPI_SETWHEELSCROLLCHARS) {
+ sInstance->mSystemSettings.MarkDirty();
+ }
+ return false;
+
+ case WM_MOUSEWHEEL:
+ case WM_MOUSEHWHEEL:
+ GetInstance()->ProcessNativeMouseWheelMessage(aWidget, msg, wParam,
+ lParam);
+ sInstance->mSynthesizingEvent->NotifyNativeMessageHandlingFinished();
+ // We don't need to call next wndproc for WM_MOUSEWHEEL and
+ // WM_MOUSEHWHEEL. We should consume them always. If the messages
+ // would be handled by our window again, it caused making infinite
+ // message loop.
+ aResult.mConsumed = true;
+ aResult.mResult = (msg != WM_MOUSEHWHEEL);
+ return true;
+
+ case WM_HSCROLL:
+ case WM_VSCROLL:
+ aResult.mConsumed = GetInstance()->ProcessNativeScrollMessage(
+ aWidget, msg, wParam, lParam);
+ sInstance->mSynthesizingEvent->NotifyNativeMessageHandlingFinished();
+ aResult.mResult = 0;
+ return true;
+
+ case MOZ_WM_MOUSEVWHEEL:
+ case MOZ_WM_MOUSEHWHEEL:
+ GetInstance()->HandleMouseWheelMessage(aWidget, msg, wParam, lParam);
+ sInstance->mSynthesizingEvent->NotifyInternalMessageHandlingFinished();
+ // Doesn't need to call next wndproc for internal wheel message.
+ aResult.mConsumed = true;
+ return true;
+
+ case MOZ_WM_HSCROLL:
+ case MOZ_WM_VSCROLL:
+ GetInstance()->HandleScrollMessageAsMouseWheelMessage(aWidget, msg,
+ wParam, lParam);
+ sInstance->mSynthesizingEvent->NotifyInternalMessageHandlingFinished();
+ // Doesn't need to call next wndproc for internal scroll message.
+ aResult.mConsumed = true;
+ return true;
+
+ case WM_KEYDOWN:
+ case WM_KEYUP:
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessMessage(): aWidget=%p, "
+ "msg=%s(0x%04X), wParam=0x%02zX, ::GetMessageTime()=%ld",
+ aWidget,
+ msg == WM_KEYDOWN ? "WM_KEYDOWN"
+ : msg == WM_KEYUP ? "WM_KEYUP"
+ : "Unknown",
+ msg, wParam, ::GetMessageTime()));
+ MaybeLogKeyState();
+ if (Device::Elantech::HandleKeyMessage(aWidget, msg, wParam, lParam)) {
+ aResult.mResult = 0;
+ aResult.mConsumed = true;
+ return true;
+ }
+ return false;
+
+ default:
+ return false;
+ }
+}
+
+/* static */
+nsresult MouseScrollHandler::SynthesizeNativeMouseScrollEvent(
+ nsWindow* aWidget, const LayoutDeviceIntPoint& aPoint,
+ uint32_t aNativeMessage, int32_t aDelta, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags) {
+ bool useFocusedWindow = !(
+ aAdditionalFlags & nsIDOMWindowUtils::MOUSESCROLL_PREFER_WIDGET_AT_POINT);
+
+ POINT pt;
+ pt.x = aPoint.x;
+ pt.y = aPoint.y;
+
+ HWND target = useFocusedWindow ? ::WindowFromPoint(pt) : ::GetFocus();
+ NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);
+
+ WPARAM wParam = 0;
+ LPARAM lParam = 0;
+ switch (aNativeMessage) {
+ case WM_MOUSEWHEEL:
+ case WM_MOUSEHWHEEL: {
+ lParam = MAKELPARAM(pt.x, pt.y);
+ WORD mod = 0;
+ if (aModifierFlags & (nsIWidget::CTRL_L | nsIWidget::CTRL_R)) {
+ mod |= MK_CONTROL;
+ }
+ if (aModifierFlags & (nsIWidget::SHIFT_L | nsIWidget::SHIFT_R)) {
+ mod |= MK_SHIFT;
+ }
+ wParam = MAKEWPARAM(mod, aDelta);
+ break;
+ }
+ case WM_VSCROLL:
+ case WM_HSCROLL:
+ lParam = (aAdditionalFlags &
+ nsIDOMWindowUtils::MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL)
+ ? reinterpret_cast<LPARAM>(target)
+ : 0;
+ wParam = aDelta;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Ensure to make the instance.
+ GetInstance();
+
+ BYTE kbdState[256];
+ memset(kbdState, 0, sizeof(kbdState));
+
+ AutoTArray<KeyPair, 10> keySequence;
+ WinUtils::SetupKeyModifiersSequence(&keySequence, aModifierFlags,
+ aNativeMessage);
+
+ for (uint32_t i = 0; i < keySequence.Length(); ++i) {
+ uint8_t key = keySequence[i].mGeneral;
+ uint8_t keySpecific = keySequence[i].mSpecific;
+ kbdState[key] = 0x81; // key is down and toggled on if appropriate
+ if (keySpecific) {
+ kbdState[keySpecific] = 0x81;
+ }
+ }
+
+ if (!sInstance->mSynthesizingEvent) {
+ sInstance->mSynthesizingEvent = new SynthesizingEvent();
+ }
+
+ POINTS pts;
+ pts.x = static_cast<SHORT>(pt.x);
+ pts.y = static_cast<SHORT>(pt.y);
+ return sInstance->mSynthesizingEvent->Synthesize(pts, target, aNativeMessage,
+ wParam, lParam, kbdState);
+}
+
+/* static */
+void MouseScrollHandler::InitEvent(nsWindow* aWidget, WidgetGUIEvent& aEvent,
+ LPARAM* aPoint) {
+ NS_ENSURE_TRUE_VOID(aWidget);
+
+ // If a point is provided, use it; otherwise, get current message point or
+ // synthetic point
+ POINTS pointOnScreen;
+ if (aPoint != nullptr) {
+ pointOnScreen = MAKEPOINTS(*aPoint);
+ } else {
+ pointOnScreen = GetCurrentMessagePos();
+ }
+
+ // InitEvent expects the point to be in window coordinates, so translate the
+ // point from screen coordinates.
+ POINT pointOnWindow;
+ POINTSTOPOINT(pointOnWindow, pointOnScreen);
+ ::ScreenToClient(aWidget->GetWindowHandle(), &pointOnWindow);
+
+ LayoutDeviceIntPoint point;
+ point.x = pointOnWindow.x;
+ point.y = pointOnWindow.y;
+
+ aWidget->InitEvent(aEvent, &point);
+}
+
+/* static */
+ModifierKeyState MouseScrollHandler::GetModifierKeyState(UINT aMessage) {
+ ModifierKeyState result;
+ // Assume the Control key is down if the Elantech touchpad has sent the
+ // mis-ordered WM_KEYDOWN/WM_MOUSEWHEEL messages. (See the comment in
+ // MouseScrollHandler::Device::Elantech::HandleKeyMessage().)
+ if ((aMessage == MOZ_WM_MOUSEVWHEEL || aMessage == WM_MOUSEWHEEL) &&
+ !result.IsControl() && Device::Elantech::IsZooming()) {
+ // XXX Do we need to unset MODIFIER_SHIFT, MODIFIER_ALT, MODIFIER_META too?
+ // If one of them are true, the default action becomes not zooming.
+ result.Unset(MODIFIER_ALTGRAPH);
+ result.Set(MODIFIER_CONTROL);
+ }
+ return result;
+}
+
+POINT
+MouseScrollHandler::ComputeMessagePos(UINT aMessage, WPARAM aWParam,
+ LPARAM aLParam) {
+ POINT point;
+ if (Device::SetPoint::IsGetMessagePosResponseValid(aMessage, aWParam,
+ aLParam)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ComputeMessagePos: Using ::GetCursorPos()"));
+ ::GetCursorPos(&point);
+ } else {
+ POINTS pts = GetCurrentMessagePos();
+ point.x = pts.x;
+ point.y = pts.y;
+ }
+ return point;
+}
+
+void MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindow* aWidget,
+ UINT aMessage,
+ WPARAM aWParam,
+ LPARAM aLParam) {
+ if (SynthesizingEvent::IsSynthesizing()) {
+ mSynthesizingEvent->NativeMessageReceived(aWidget, aMessage, aWParam,
+ aLParam);
+ }
+
+ POINT point = ComputeMessagePos(aMessage, aWParam, aLParam);
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: aWidget=%p, "
+ "aMessage=%s, wParam=0x%08zX, lParam=0x%08" PRIXLPTR
+ ", point: { x=%ld, y=%ld }",
+ aWidget,
+ aMessage == WM_MOUSEWHEEL ? "WM_MOUSEWHEEL"
+ : aMessage == WM_MOUSEHWHEEL ? "WM_MOUSEHWHEEL"
+ : aMessage == WM_VSCROLL ? "WM_VSCROLL"
+ : "WM_HSCROLL",
+ aWParam, aLParam, point.x, point.y));
+ MaybeLogKeyState();
+
+ HWND underCursorWnd = ::WindowFromPoint(point);
+ if (!underCursorWnd) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: "
+ "No window is not found under the cursor"));
+ return;
+ }
+
+ if (Device::Elantech::IsPinchHackNeeded() &&
+ Device::Elantech::IsHelperWindow(underCursorWnd)) {
+ // The Elantech driver places a window right underneath the cursor
+ // when sending a WM_MOUSEWHEEL event to us as part of a pinch-to-zoom
+ // gesture. We detect that here, and search for our window that would
+ // be beneath the cursor if that window wasn't there.
+ underCursorWnd = WinUtils::FindOurWindowAtPoint(point);
+ if (!underCursorWnd) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: "
+ "Our window is not found under the Elantech helper window"));
+ return;
+ }
+ }
+
+ // Handle most cases first. If the window under mouse cursor is our window
+ // except plugin window (MozillaWindowClass), we should handle the message
+ // on the window.
+ if (WinUtils::IsOurProcessWindow(underCursorWnd)) {
+ nsWindow* destWindow = WinUtils::GetNSWindowPtr(underCursorWnd);
+ if (!destWindow) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: "
+ "Found window under the cursor isn't managed by nsWindow..."));
+ HWND wnd = ::GetParent(underCursorWnd);
+ for (; wnd; wnd = ::GetParent(wnd)) {
+ destWindow = WinUtils::GetNSWindowPtr(wnd);
+ if (destWindow) {
+ break;
+ }
+ }
+ if (!wnd) {
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: Our window which is "
+ "managed by nsWindow is not found under the cursor"));
+ return;
+ }
+ }
+
+ MOZ_ASSERT(destWindow, "destWindow must not be NULL");
+
+ // Some odd touchpad utils sets focus to window under the mouse cursor.
+ // this emulates the odd behavior for debug.
+ if (mUserPrefs.ShouldEmulateToMakeWindowUnderCursorForeground() &&
+ (aMessage == WM_MOUSEWHEEL || aMessage == WM_MOUSEHWHEEL) &&
+ ::GetForegroundWindow() != destWindow->GetWindowHandle()) {
+ ::SetForegroundWindow(destWindow->GetWindowHandle());
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: Succeeded, "
+ "Posting internal message to an nsWindow (%p)...",
+ destWindow));
+ mIsWaitingInternalMessage = true;
+ UINT internalMessage = WinUtils::GetInternalMessage(aMessage);
+ ::PostMessage(destWindow->GetWindowHandle(), internalMessage, aWParam,
+ aLParam);
+ return;
+ }
+
+ // If the window under cursor is not in our process, it means:
+ // 1. The window may be a plugin window (GeckoPluginWindow or its descendant).
+ // 2. The window may be another application's window.
+ HWND pluginWnd = WinUtils::FindOurProcessWindow(underCursorWnd);
+ if (!pluginWnd) {
+ // If there is no plugin window in ancestors of the window under cursor,
+ // the window is for another applications (case 2).
+ // We don't need to handle this message.
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: "
+ "Our window is not found under the cursor"));
+ return;
+ }
+
+ // If the window is a part of plugin, we should post the message to it.
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeMouseWheelMessage: Succeeded, "
+ "Redirecting the message to a window which is a plugin child window"));
+ ::PostMessage(underCursorWnd, aMessage, aWParam, aLParam);
+}
+
+bool MouseScrollHandler::ProcessNativeScrollMessage(nsWindow* aWidget,
+ UINT aMessage,
+ WPARAM aWParam,
+ LPARAM aLParam) {
+ if (aLParam || mUserPrefs.IsScrollMessageHandledAsWheelMessage()) {
+ // Scroll message generated by Thinkpad Trackpoint Driver or similar
+ // Treat as a mousewheel message and scroll appropriately
+ ProcessNativeMouseWheelMessage(aWidget, aMessage, aWParam, aLParam);
+ // Always consume the scroll message if we try to emulate mouse wheel
+ // action.
+ return true;
+ }
+
+ if (SynthesizingEvent::IsSynthesizing()) {
+ mSynthesizingEvent->NativeMessageReceived(aWidget, aMessage, aWParam,
+ aLParam);
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::ProcessNativeScrollMessage: aWidget=%p, "
+ "aMessage=%s, wParam=0x%08zX, lParam=0x%08" PRIXLPTR,
+ aWidget, aMessage == WM_VSCROLL ? "WM_VSCROLL" : "WM_HSCROLL",
+ aWParam, aLParam));
+
+ // Scroll message generated by external application
+ WidgetContentCommandEvent commandEvent(true, eContentCommandScroll, aWidget);
+ commandEvent.mScroll.mIsHorizontal = (aMessage == WM_HSCROLL);
+
+ switch (LOWORD(aWParam)) {
+ case SB_LINEUP: // SB_LINELEFT
+ commandEvent.mScroll.mUnit =
+ WidgetContentCommandEvent::eCmdScrollUnit_Line;
+ commandEvent.mScroll.mAmount = -1;
+ break;
+ case SB_LINEDOWN: // SB_LINERIGHT
+ commandEvent.mScroll.mUnit =
+ WidgetContentCommandEvent::eCmdScrollUnit_Line;
+ commandEvent.mScroll.mAmount = 1;
+ break;
+ case SB_PAGEUP: // SB_PAGELEFT
+ commandEvent.mScroll.mUnit =
+ WidgetContentCommandEvent::eCmdScrollUnit_Page;
+ commandEvent.mScroll.mAmount = -1;
+ break;
+ case SB_PAGEDOWN: // SB_PAGERIGHT
+ commandEvent.mScroll.mUnit =
+ WidgetContentCommandEvent::eCmdScrollUnit_Page;
+ commandEvent.mScroll.mAmount = 1;
+ break;
+ case SB_TOP: // SB_LEFT
+ commandEvent.mScroll.mUnit =
+ WidgetContentCommandEvent::eCmdScrollUnit_Whole;
+ commandEvent.mScroll.mAmount = -1;
+ break;
+ case SB_BOTTOM: // SB_RIGHT
+ commandEvent.mScroll.mUnit =
+ WidgetContentCommandEvent::eCmdScrollUnit_Whole;
+ commandEvent.mScroll.mAmount = 1;
+ break;
+ default:
+ return false;
+ }
+ // XXX If this is a plugin window, we should dispatch the event from
+ // parent window.
+ aWidget->DispatchContentCommandEvent(&commandEvent);
+ return true;
+}
+
+void MouseScrollHandler::HandleMouseWheelMessage(nsWindow* aWidget,
+ UINT aMessage, WPARAM aWParam,
+ LPARAM aLParam) {
+ MOZ_ASSERT((aMessage == MOZ_WM_MOUSEVWHEEL || aMessage == MOZ_WM_MOUSEHWHEEL),
+ "HandleMouseWheelMessage must be called with "
+ "MOZ_WM_MOUSEVWHEEL or MOZ_WM_MOUSEHWHEEL");
+
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::HandleMouseWheelMessage: aWidget=%p, "
+ "aMessage=MOZ_WM_MOUSE%sWHEEL, aWParam=0x%08zX, aLParam=0x%08" PRIXLPTR,
+ aWidget, aMessage == MOZ_WM_MOUSEVWHEEL ? "V" : "H", aWParam, aLParam));
+
+ mIsWaitingInternalMessage = false;
+
+ // If it's not allowed to cache system settings, we need to reset the cache
+ // before handling the mouse wheel message.
+ mSystemSettings.TrustedScrollSettingsDriver();
+
+ EventInfo eventInfo(aWidget, WinUtils::GetNativeMessage(aMessage), aWParam,
+ aLParam);
+ if (!eventInfo.CanDispatchWheelEvent()) {
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::HandleMouseWheelMessage: Cannot dispatch the events"));
+ mLastEventInfo.ResetTransaction();
+ return;
+ }
+
+ // Discard the remaining delta if current wheel message and last one are
+ // received by different window or to scroll different direction or
+ // different unit scroll. Furthermore, if the last event was too old.
+ if (!mLastEventInfo.CanContinueTransaction(eventInfo)) {
+ mLastEventInfo.ResetTransaction();
+ }
+
+ mLastEventInfo.RecordEvent(eventInfo);
+
+ ModifierKeyState modKeyState = GetModifierKeyState(aMessage);
+
+ // Grab the widget, it might be destroyed by a DOM event handler.
+ RefPtr<nsWindow> kungFuDethGrip(aWidget);
+
+ WidgetWheelEvent wheelEvent(true, eWheel, aWidget);
+ if (mLastEventInfo.InitWheelEvent(aWidget, wheelEvent, modKeyState,
+ aLParam)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::HandleMouseWheelMessage: dispatching "
+ "eWheel event"));
+ aWidget->DispatchWheelEvent(&wheelEvent);
+ if (aWidget->Destroyed()) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::HandleMouseWheelMessage: The window was destroyed "
+ "by eWheel event"));
+ mLastEventInfo.ResetTransaction();
+ return;
+ }
+ } else {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::HandleMouseWheelMessage: eWheel event is not "
+ "dispatched"));
+ }
+}
+
+void MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage(
+ nsWindow* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) {
+ MOZ_ASSERT((aMessage == MOZ_WM_VSCROLL || aMessage == MOZ_WM_HSCROLL),
+ "HandleScrollMessageAsMouseWheelMessage must be called with "
+ "MOZ_WM_VSCROLL or MOZ_WM_HSCROLL");
+
+ mIsWaitingInternalMessage = false;
+
+ ModifierKeyState modKeyState = GetModifierKeyState(aMessage);
+
+ WidgetWheelEvent wheelEvent(true, eWheel, aWidget);
+ double& delta =
+ (aMessage == MOZ_WM_VSCROLL) ? wheelEvent.mDeltaY : wheelEvent.mDeltaX;
+ int32_t& lineOrPageDelta = (aMessage == MOZ_WM_VSCROLL)
+ ? wheelEvent.mLineOrPageDeltaY
+ : wheelEvent.mLineOrPageDeltaX;
+
+ delta = 1.0;
+ lineOrPageDelta = 1;
+
+ switch (LOWORD(aWParam)) {
+ case SB_PAGEUP:
+ delta = -1.0;
+ lineOrPageDelta = -1;
+ [[fallthrough]];
+ case SB_PAGEDOWN:
+ wheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_PAGE;
+ break;
+
+ case SB_LINEUP:
+ delta = -1.0;
+ lineOrPageDelta = -1;
+ [[fallthrough]];
+ case SB_LINEDOWN:
+ wheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_LINE;
+ break;
+
+ default:
+ return;
+ }
+ modKeyState.InitInputEvent(wheelEvent);
+
+ // Current mouse position may not be same as when the original message
+ // is received. However, this data is not available with the original
+ // message, which is why nullptr is passed in. We need to know the actual
+ // mouse cursor position when the original message was received.
+ InitEvent(aWidget, wheelEvent, nullptr);
+
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::HandleScrollMessageAsMouseWheelMessage: aWidget=%p, "
+ "aMessage=MOZ_WM_%sSCROLL, aWParam=0x%08zX, aLParam=0x%08" PRIXLPTR ", "
+ "wheelEvent { mRefPoint: { x: %d, y: %d }, mDeltaX: %f, mDeltaY: %f, "
+ "mLineOrPageDeltaX: %d, mLineOrPageDeltaY: %d, "
+ "isShift: %s, isControl: %s, isAlt: %s, isMeta: %s }",
+ aWidget, (aMessage == MOZ_WM_VSCROLL) ? "V" : "H", aWParam, aLParam,
+ wheelEvent.mRefPoint.x.value, wheelEvent.mRefPoint.y.value,
+ wheelEvent.mDeltaX, wheelEvent.mDeltaY, wheelEvent.mLineOrPageDeltaX,
+ wheelEvent.mLineOrPageDeltaY, GetBoolName(wheelEvent.IsShift()),
+ GetBoolName(wheelEvent.IsControl()), GetBoolName(wheelEvent.IsAlt()),
+ GetBoolName(wheelEvent.IsMeta())));
+
+ aWidget->DispatchWheelEvent(&wheelEvent);
+}
+
+/******************************************************************************
+ *
+ * EventInfo
+ *
+ ******************************************************************************/
+
+MouseScrollHandler::EventInfo::EventInfo(nsWindow* aWidget, UINT aMessage,
+ WPARAM aWParam, LPARAM aLParam) {
+ MOZ_ASSERT(
+ aMessage == WM_MOUSEWHEEL || aMessage == WM_MOUSEHWHEEL,
+ "EventInfo must be initialized with WM_MOUSEWHEEL or WM_MOUSEHWHEEL");
+
+ MouseScrollHandler::GetInstance()->mSystemSettings.Init();
+
+ mIsVertical = (aMessage == WM_MOUSEWHEEL);
+ mIsPage =
+ MouseScrollHandler::sInstance->mSystemSettings.IsPageScroll(mIsVertical);
+ mDelta = (short)HIWORD(aWParam);
+ mWnd = aWidget->GetWindowHandle();
+ mTimeStamp = TimeStamp::Now();
+}
+
+bool MouseScrollHandler::EventInfo::CanDispatchWheelEvent() const {
+ if (!GetScrollAmount()) {
+ // XXX I think that we should dispatch mouse wheel events even if the
+ // operation will not scroll because the wheel operation really happened
+ // and web application may want to handle the event for non-scroll action.
+ return false;
+ }
+
+ return (mDelta != 0);
+}
+
+int32_t MouseScrollHandler::EventInfo::GetScrollAmount() const {
+ if (mIsPage) {
+ return 1;
+ }
+ return MouseScrollHandler::sInstance->mSystemSettings.GetScrollAmount(
+ mIsVertical);
+}
+
+/******************************************************************************
+ *
+ * LastEventInfo
+ *
+ ******************************************************************************/
+
+bool MouseScrollHandler::LastEventInfo::CanContinueTransaction(
+ const EventInfo& aNewEvent) {
+ int32_t timeout = MouseScrollHandler::sInstance->mUserPrefs
+ .GetMouseScrollTransactionTimeout();
+ return !mWnd ||
+ (mWnd == aNewEvent.GetWindowHandle() &&
+ IsPositive() == aNewEvent.IsPositive() &&
+ mIsVertical == aNewEvent.IsVertical() &&
+ mIsPage == aNewEvent.IsPage() &&
+ (timeout < 0 || TimeStamp::Now() - mTimeStamp <=
+ TimeDuration::FromMilliseconds(timeout)));
+}
+
+void MouseScrollHandler::LastEventInfo::ResetTransaction() {
+ if (!mWnd) {
+ return;
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::LastEventInfo::ResetTransaction()"));
+
+ mWnd = nullptr;
+ mAccumulatedDelta = 0;
+}
+
+void MouseScrollHandler::LastEventInfo::RecordEvent(const EventInfo& aEvent) {
+ mWnd = aEvent.GetWindowHandle();
+ mDelta = aEvent.GetNativeDelta();
+ mIsVertical = aEvent.IsVertical();
+ mIsPage = aEvent.IsPage();
+ mTimeStamp = TimeStamp::Now();
+}
+
+/* static */
+int32_t MouseScrollHandler::LastEventInfo::RoundDelta(double aDelta) {
+ return (aDelta >= 0) ? (int32_t)floor(aDelta) : (int32_t)ceil(aDelta);
+}
+
+bool MouseScrollHandler::LastEventInfo::InitWheelEvent(
+ nsWindow* aWidget, WidgetWheelEvent& aWheelEvent,
+ const ModifierKeyState& aModKeyState, LPARAM aLParam) {
+ MOZ_ASSERT(aWheelEvent.mMessage == eWheel);
+
+ if (StaticPrefs::mousewheel_ignore_cursor_position_in_lparam()) {
+ InitEvent(aWidget, aWheelEvent, nullptr);
+ } else {
+ InitEvent(aWidget, aWheelEvent, &aLParam);
+ }
+
+ aModKeyState.InitInputEvent(aWheelEvent);
+
+ // Our positive delta value means to bottom or right.
+ // But positive native delta value means to top or right.
+ // Use orienter for computing our delta value with native delta value.
+ int32_t orienter = mIsVertical ? -1 : 1;
+
+ aWheelEvent.mDeltaMode = mIsPage ? dom::WheelEvent_Binding::DOM_DELTA_PAGE
+ : dom::WheelEvent_Binding::DOM_DELTA_LINE;
+
+ double ticks = double(mDelta) * orienter / double(WHEEL_DELTA);
+ if (mIsVertical) {
+ aWheelEvent.mWheelTicksY = ticks;
+ } else {
+ aWheelEvent.mWheelTicksX = ticks;
+ }
+
+ double& delta = mIsVertical ? aWheelEvent.mDeltaY : aWheelEvent.mDeltaX;
+ int32_t& lineOrPageDelta = mIsVertical ? aWheelEvent.mLineOrPageDeltaY
+ : aWheelEvent.mLineOrPageDeltaX;
+
+ double nativeDeltaPerUnit =
+ mIsPage ? double(WHEEL_DELTA) : double(WHEEL_DELTA) / GetScrollAmount();
+
+ delta = double(mDelta) * orienter / nativeDeltaPerUnit;
+ mAccumulatedDelta += mDelta;
+ lineOrPageDelta =
+ mAccumulatedDelta * orienter / RoundDelta(nativeDeltaPerUnit);
+ mAccumulatedDelta -=
+ lineOrPageDelta * orienter * RoundDelta(nativeDeltaPerUnit);
+
+ if (aWheelEvent.mDeltaMode != dom::WheelEvent_Binding::DOM_DELTA_LINE) {
+ // If the scroll delta mode isn't per line scroll, we shouldn't allow to
+ // override the system scroll speed setting.
+ aWheelEvent.mAllowToOverrideSystemScrollSpeed = false;
+ }
+
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::LastEventInfo::InitWheelEvent: aWidget=%p, "
+ "aWheelEvent { mRefPoint: { x: %d, y: %d }, mDeltaX: %f, mDeltaY: %f, "
+ "mLineOrPageDeltaX: %d, mLineOrPageDeltaY: %d, "
+ "isShift: %s, isControl: %s, isAlt: %s, isMeta: %s, "
+ "mAllowToOverrideSystemScrollSpeed: %s }, "
+ "mAccumulatedDelta: %d",
+ aWidget, aWheelEvent.mRefPoint.x.value, aWheelEvent.mRefPoint.y.value,
+ aWheelEvent.mDeltaX, aWheelEvent.mDeltaY, aWheelEvent.mLineOrPageDeltaX,
+ aWheelEvent.mLineOrPageDeltaY, GetBoolName(aWheelEvent.IsShift()),
+ GetBoolName(aWheelEvent.IsControl()), GetBoolName(aWheelEvent.IsAlt()),
+ GetBoolName(aWheelEvent.IsMeta()),
+ GetBoolName(aWheelEvent.mAllowToOverrideSystemScrollSpeed),
+ mAccumulatedDelta));
+
+ return (delta != 0);
+}
+
+/******************************************************************************
+ *
+ * SystemSettings
+ *
+ ******************************************************************************/
+
+void MouseScrollHandler::SystemSettings::Init() {
+ if (mInitialized) {
+ return;
+ }
+
+ InitScrollLines();
+ InitScrollChars();
+
+ mInitialized = true;
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::SystemSettings::Init(): initialized, "
+ "mScrollLines=%d, mScrollChars=%d",
+ mScrollLines, mScrollChars));
+}
+
+bool MouseScrollHandler::SystemSettings::InitScrollLines() {
+ int32_t oldValue = mInitialized ? mScrollLines : 0;
+ mIsReliableScrollLines = false;
+ mScrollLines = MouseScrollHandler::sInstance->mUserPrefs
+ .GetOverriddenVerticalScrollAmout();
+ if (mScrollLines >= 0) {
+ // overridden by the pref.
+ mIsReliableScrollLines = true;
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::SystemSettings::InitScrollLines(): mScrollLines is "
+ "overridden by the pref: %d",
+ mScrollLines));
+ } else if (!::SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &mScrollLines,
+ 0)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::SystemSettings::InitScrollLines(): "
+ "::SystemParametersInfo("
+ "SPI_GETWHEELSCROLLLINES) failed"));
+ mScrollLines = DefaultScrollLines();
+ }
+
+ if (mScrollLines > WHEEL_DELTA) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::SystemSettings::InitScrollLines(): the result of "
+ "::SystemParametersInfo(SPI_GETWHEELSCROLLLINES) is too large: %d",
+ mScrollLines));
+ // sScrollLines usually equals 3 or 0 (for no scrolling)
+ // However, if sScrollLines > WHEEL_DELTA, we assume that
+ // the mouse driver wants a page scroll. The docs state that
+ // sScrollLines should explicitly equal WHEEL_PAGESCROLL, but
+ // since some mouse drivers use an arbitrary large number instead,
+ // we have to handle that as well.
+ mScrollLines = WHEEL_PAGESCROLL;
+ }
+
+ return oldValue != mScrollLines;
+}
+
+bool MouseScrollHandler::SystemSettings::InitScrollChars() {
+ int32_t oldValue = mInitialized ? mScrollChars : 0;
+ mIsReliableScrollChars = false;
+ mScrollChars = MouseScrollHandler::sInstance->mUserPrefs
+ .GetOverriddenHorizontalScrollAmout();
+ if (mScrollChars >= 0) {
+ // overridden by the pref.
+ mIsReliableScrollChars = true;
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::SystemSettings::InitScrollChars(): mScrollChars is "
+ "overridden by the pref: %d",
+ mScrollChars));
+ } else if (!::SystemParametersInfo(SPI_GETWHEELSCROLLCHARS, 0, &mScrollChars,
+ 0)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::SystemSettings::InitScrollChars(): "
+ "::SystemParametersInfo("
+ "SPI_GETWHEELSCROLLCHARS) failed, this is unexpected on Vista or "
+ "later"));
+ // XXX Should we use DefaultScrollChars()?
+ mScrollChars = 1;
+ }
+
+ if (mScrollChars > WHEEL_DELTA) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::SystemSettings::InitScrollChars(): the result of "
+ "::SystemParametersInfo(SPI_GETWHEELSCROLLCHARS) is too large: %d",
+ mScrollChars));
+ // See the comments for the case mScrollLines > WHEEL_DELTA.
+ mScrollChars = WHEEL_PAGESCROLL;
+ }
+
+ return oldValue != mScrollChars;
+}
+
+void MouseScrollHandler::SystemSettings::MarkDirty() {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScrollHandler::SystemSettings::MarkDirty(): "
+ "Marking SystemSettings dirty"));
+ mInitialized = false;
+ // When system settings are changed, we should reset current transaction.
+ MOZ_ASSERT(sInstance,
+ "Must not be called at initializing MouseScrollHandler");
+ MouseScrollHandler::sInstance->mLastEventInfo.ResetTransaction();
+}
+
+void MouseScrollHandler::SystemSettings::RefreshCache() {
+ bool isChanged = InitScrollLines();
+ isChanged = InitScrollChars() || isChanged;
+ if (!isChanged) {
+ return;
+ }
+ // If the scroll amount is changed, we should reset current transaction.
+ MOZ_ASSERT(sInstance,
+ "Must not be called at initializing MouseScrollHandler");
+ MouseScrollHandler::sInstance->mLastEventInfo.ResetTransaction();
+}
+
+void MouseScrollHandler::SystemSettings::TrustedScrollSettingsDriver() {
+ if (!mInitialized) {
+ return;
+ }
+
+ // if the cache is initialized with prefs, we don't need to refresh it.
+ if (mIsReliableScrollLines && mIsReliableScrollChars) {
+ return;
+ }
+
+ MouseScrollHandler::UserPrefs& userPrefs =
+ MouseScrollHandler::sInstance->mUserPrefs;
+
+ // If system settings cache is disabled, we should always refresh them.
+ if (!userPrefs.IsSystemSettingCacheEnabled()) {
+ RefreshCache();
+ return;
+ }
+
+ // If pref is set to as "always trust the cache", we shouldn't refresh them
+ // in any environments.
+ if (userPrefs.IsSystemSettingCacheForciblyEnabled()) {
+ return;
+ }
+
+ // If SynTP of Synaptics or Apoint of Alps is installed, it may hook
+ // ::SystemParametersInfo() and returns different value from system settings.
+ if (Device::SynTP::IsDriverInstalled() ||
+ Device::Apoint::IsDriverInstalled()) {
+ RefreshCache();
+ return;
+ }
+
+ // XXX We're not sure about other touchpad drivers...
+}
+
+/******************************************************************************
+ *
+ * UserPrefs
+ *
+ ******************************************************************************/
+
+MouseScrollHandler::UserPrefs::UserPrefs() : mInitialized(false) {
+ // We need to reset mouse wheel transaction when all of mousewheel related
+ // prefs are changed.
+ DebugOnly<nsresult> rv =
+ Preferences::RegisterPrefixCallback(OnChange, "mousewheel.", this);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to register callback for mousewheel.");
+}
+
+MouseScrollHandler::UserPrefs::~UserPrefs() {
+ DebugOnly<nsresult> rv =
+ Preferences::UnregisterPrefixCallback(OnChange, "mousewheel.", this);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to unregister callback for mousewheel.");
+}
+
+void MouseScrollHandler::UserPrefs::Init() {
+ if (mInitialized) {
+ return;
+ }
+
+ mInitialized = true;
+
+ mScrollMessageHandledAsWheelMessage =
+ Preferences::GetBool("mousewheel.emulate_at_wm_scroll", false);
+ mEnableSystemSettingCache =
+ Preferences::GetBool("mousewheel.system_settings_cache.enabled", true);
+ mForceEnableSystemSettingCache = Preferences::GetBool(
+ "mousewheel.system_settings_cache.force_enabled", false);
+ mEmulateToMakeWindowUnderCursorForeground = Preferences::GetBool(
+ "mousewheel.debug.make_window_under_cursor_foreground", false);
+ mOverriddenVerticalScrollAmount =
+ Preferences::GetInt("mousewheel.windows.vertical_amount_override", -1);
+ mOverriddenHorizontalScrollAmount =
+ Preferences::GetInt("mousewheel.windows.horizontal_amount_override", -1);
+ mMouseScrollTransactionTimeout = Preferences::GetInt(
+ "mousewheel.windows.transaction.timeout", DEFAULT_TIMEOUT_DURATION);
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::UserPrefs::Init(): initialized, "
+ "mScrollMessageHandledAsWheelMessage=%s, "
+ "mEnableSystemSettingCache=%s, "
+ "mForceEnableSystemSettingCache=%s, "
+ "mEmulateToMakeWindowUnderCursorForeground=%s, "
+ "mOverriddenVerticalScrollAmount=%d, "
+ "mOverriddenHorizontalScrollAmount=%d, "
+ "mMouseScrollTransactionTimeout=%d",
+ GetBoolName(mScrollMessageHandledAsWheelMessage),
+ GetBoolName(mEnableSystemSettingCache),
+ GetBoolName(mForceEnableSystemSettingCache),
+ GetBoolName(mEmulateToMakeWindowUnderCursorForeground),
+ mOverriddenVerticalScrollAmount, mOverriddenHorizontalScrollAmount,
+ mMouseScrollTransactionTimeout));
+}
+
+void MouseScrollHandler::UserPrefs::MarkDirty() {
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScrollHandler::UserPrefs::MarkDirty(): Marking UserPrefs dirty"));
+ mInitialized = false;
+ // Some prefs might override system settings, so, we should mark them dirty.
+ MouseScrollHandler::sInstance->mSystemSettings.MarkDirty();
+ // When user prefs for mousewheel are changed, we should reset current
+ // transaction.
+ MOZ_ASSERT(sInstance,
+ "Must not be called at initializing MouseScrollHandler");
+ MouseScrollHandler::sInstance->mLastEventInfo.ResetTransaction();
+}
+
+/******************************************************************************
+ *
+ * Device
+ *
+ ******************************************************************************/
+
+/* static */
+bool MouseScrollHandler::Device::GetWorkaroundPref(const char* aPrefName,
+ bool aValueIfAutomatic) {
+ if (!aPrefName) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::GetWorkaroundPref(): Failed, aPrefName is "
+ "NULL"));
+ return aValueIfAutomatic;
+ }
+
+ int32_t lHackValue = 0;
+ if (NS_FAILED(Preferences::GetInt(aPrefName, &lHackValue))) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::GetWorkaroundPref(): Preferences::GetInt() "
+ "failed,"
+ " aPrefName=\"%s\", aValueIfAutomatic=%s",
+ aPrefName, GetBoolName(aValueIfAutomatic)));
+ return aValueIfAutomatic;
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::GetWorkaroundPref(): Succeeded, "
+ "aPrefName=\"%s\", aValueIfAutomatic=%s, lHackValue=%d",
+ aPrefName, GetBoolName(aValueIfAutomatic), lHackValue));
+
+ switch (lHackValue) {
+ case 0: // disabled
+ return false;
+ case 1: // enabled
+ return true;
+ default: // -1: autodetect
+ return aValueIfAutomatic;
+ }
+}
+
+/* static */
+void MouseScrollHandler::Device::Init() {
+ // FYI: Thinkpad's TrackPoint is Apoint of Alps and UltraNav is SynTP of
+ // Synaptics. So, those drivers' information should be initialized
+ // before calling methods of TrackPoint and UltraNav.
+ SynTP::Init();
+ Elantech::Init();
+ Apoint::Init();
+
+ sFakeScrollableWindowNeeded = GetWorkaroundPref(
+ "ui.trackpoint_hack.enabled", (TrackPoint::IsDriverInstalled() ||
+ UltraNav::IsObsoleteDriverInstalled()));
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Init(): sFakeScrollableWindowNeeded=%s",
+ GetBoolName(sFakeScrollableWindowNeeded)));
+}
+
+/******************************************************************************
+ *
+ * Device::SynTP
+ *
+ ******************************************************************************/
+
+/* static */
+void MouseScrollHandler::Device::SynTP::Init() {
+ if (sInitialized) {
+ return;
+ }
+
+ sInitialized = true;
+ sMajorVersion = 0;
+ sMinorVersion = -1;
+
+ wchar_t buf[40];
+ if (!WinRegistry::GetString(
+ HKEY_LOCAL_MACHINE, u"Software\\Synaptics\\SynTP\\Install"_ns,
+ u"DriverVersion"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::SynTP::Init(): "
+ "SynTP driver is not found"));
+ return;
+ }
+
+ sMajorVersion = wcstol(buf, nullptr, 10);
+ sMinorVersion = 0;
+ wchar_t* p = wcschr(buf, L'.');
+ if (p) {
+ sMinorVersion = wcstol(p + 1, nullptr, 10);
+ }
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::SynTP::Init(): "
+ "found driver version = %d.%d",
+ sMajorVersion, sMinorVersion));
+}
+
+/******************************************************************************
+ *
+ * Device::Elantech
+ *
+ ******************************************************************************/
+
+/* static */
+void MouseScrollHandler::Device::Elantech::Init() {
+ int32_t version = GetDriverMajorVersion();
+ bool needsHack = Device::GetWorkaroundPref(
+ "ui.elantech_gesture_hacks.enabled", version != 0);
+ sUseSwipeHack = needsHack && version <= 7;
+ sUsePinchHack = needsHack && version <= 8;
+
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Elantech::Init(): version=%d, sUseSwipeHack=%s, "
+ "sUsePinchHack=%s",
+ version, GetBoolName(sUseSwipeHack), GetBoolName(sUsePinchHack)));
+}
+
+/* static */
+int32_t MouseScrollHandler::Device::Elantech::GetDriverMajorVersion() {
+ wchar_t buf[40];
+ // The driver version is found in one of these two registry keys.
+ if (!WinRegistry::GetString(
+ HKEY_CURRENT_USER, u"Software\\Elantech\\MainOption"_ns,
+ u"DriverVersion"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags) &&
+ !WinRegistry::GetString(HKEY_CURRENT_USER, u"Software\\Elantech"_ns,
+ u"DriverVersion"_ns, buf,
+ WinRegistry::kLegacyWinUtilsStringFlags)) {
+ return 0;
+ }
+
+ // Assume that the major version number can be found just after a space
+ // or at the start of the string.
+ for (wchar_t* p = buf; *p; p++) {
+ if (*p >= L'0' && *p <= L'9' && (p == buf || *(p - 1) == L' ')) {
+ return wcstol(p, nullptr, 10);
+ }
+ }
+
+ return 0;
+}
+
+/* static */
+bool MouseScrollHandler::Device::Elantech::IsHelperWindow(HWND aWnd) {
+ // The helper window cannot be distinguished based on its window class, so we
+ // need to check if it is owned by the helper process, ETDCtrl.exe.
+
+ const wchar_t* filenameSuffix = L"\\etdctrl.exe";
+ const int filenameSuffixLength = 12;
+
+ DWORD pid;
+ ::GetWindowThreadProcessId(aWnd, &pid);
+
+ HANDLE hProcess = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
+ if (!hProcess) {
+ return false;
+ }
+
+ bool result = false;
+ wchar_t path[256] = {L'\0'};
+ if (::GetProcessImageFileNameW(hProcess, path, ArrayLength(path))) {
+ int pathLength = lstrlenW(path);
+ if (pathLength >= filenameSuffixLength) {
+ if (lstrcmpiW(path + pathLength - filenameSuffixLength, filenameSuffix) ==
+ 0) {
+ result = true;
+ }
+ }
+ }
+ ::CloseHandle(hProcess);
+
+ return result;
+}
+
+/* static */
+bool MouseScrollHandler::Device::Elantech::HandleKeyMessage(nsWindow* aWidget,
+ UINT aMsg,
+ WPARAM aWParam,
+ LPARAM aLParam) {
+ // The Elantech touchpad driver understands three-finger swipe left and
+ // right gestures, and translates them into Page Up and Page Down key
+ // events for most applications. For Firefox 3.6, it instead sends
+ // Alt+Left and Alt+Right to trigger browser back/forward actions. As
+ // with the Thinkpad Driver hack in nsWindow::Create, the change in
+ // HWND structure makes Firefox not trigger the driver's heuristics
+ // any longer.
+ //
+ // The Elantech driver actually sends these messages for a three-finger
+ // swipe right:
+ //
+ // WM_KEYDOWN virtual_key = 0xCC or 0xFF ScanCode = 00
+ // WM_KEYDOWN virtual_key = VK_NEXT ScanCode = 00
+ // WM_KEYUP virtual_key = VK_NEXT ScanCode = 00
+ // WM_KEYUP virtual_key = 0xCC or 0xFF ScanCode = 00
+ //
+ // Whether 0xCC or 0xFF is sent is suspected to depend on the driver
+ // version. 7.0.4.12_14Jul09_WHQL, 7.0.5.10, and 7.0.6.0 generate 0xCC.
+ // 7.0.4.3 from Asus on EeePC generates 0xFF.
+ //
+ // On some hardware, IS_VK_DOWN(0xFF) returns true even when Elantech
+ // messages are not involved, meaning that alone is not enough to
+ // distinguish the gesture from a regular Page Up or Page Down key press.
+ // The ScanCode is therefore also tested to detect the gesture.
+ // We then pretend that we should dispatch "Go Forward" command. Similarly
+ // for VK_PRIOR and "Go Back" command.
+ if (sUseSwipeHack && (aWParam == VK_NEXT || aWParam == VK_PRIOR) &&
+ WinUtils::GetScanCode(aLParam) == 0 &&
+ (IS_VK_DOWN(0xFF) || IS_VK_DOWN(0xCC))) {
+ if (aMsg == WM_KEYDOWN) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Elantech::HandleKeyMessage(): Dispatching "
+ "%s command event",
+ aWParam == VK_NEXT ? "Forward" : "Back"));
+
+ WidgetCommandEvent appCommandEvent(
+ true, (aWParam == VK_NEXT) ? nsGkAtoms::Forward : nsGkAtoms::Back,
+ aWidget);
+
+ // In this scenario, the coordinate of the event isn't supplied, so pass
+ // nullptr as an argument to indicate using the coordinate from the last
+ // available window message.
+ InitEvent(aWidget, appCommandEvent, nullptr);
+ aWidget->DispatchWindowEvent(appCommandEvent);
+ } else {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Elantech::HandleKeyMessage(): Consumed"));
+ }
+ return true; // consume the message (doesn't need to dispatch key events)
+ }
+
+ // Version 8 of the Elantech touchpad driver sends these messages for
+ // zoom gestures:
+ //
+ // WM_KEYDOWN virtual_key = 0xCC time = 10
+ // WM_KEYDOWN virtual_key = VK_CONTROL time = 10
+ // WM_MOUSEWHEEL time = ::GetTickCount()
+ // WM_KEYUP virtual_key = VK_CONTROL time = 10
+ // WM_KEYUP virtual_key = 0xCC time = 10
+ //
+ // The result of this is that we process all of the WM_KEYDOWN/WM_KEYUP
+ // messages first because their timestamps make them appear to have
+ // been sent before the WM_MOUSEWHEEL message. To work around this,
+ // we store the current time when we process the WM_KEYUP message and
+ // assume that any WM_MOUSEWHEEL message with a timestamp before that
+ // time is one that should be processed as if the Control key was down.
+ if (sUsePinchHack && aMsg == WM_KEYUP && aWParam == VK_CONTROL &&
+ ::GetMessageTime() == 10) {
+ // We look only at the bottom 31 bits of the system tick count since
+ // GetMessageTime returns a LONG, which is signed, so we want values
+ // that are more easily comparable.
+ sZoomUntil = ::GetTickCount() & 0x7FFFFFFF;
+
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Elantech::HandleKeyMessage(): sZoomUntil=%lu",
+ sZoomUntil));
+ }
+
+ return false;
+}
+
+/* static */
+void MouseScrollHandler::Device::Elantech::UpdateZoomUntil() {
+ if (!sZoomUntil) {
+ return;
+ }
+
+ // For the Elantech Touchpad Zoom Gesture Hack, we should check that the
+ // system time (32-bit milliseconds) hasn't wrapped around. Otherwise we
+ // might get into the situation where wheel events for the next 50 days of
+ // system uptime are assumed to be Ctrl+Wheel events. (It is unlikely that
+ // we would get into that state, because the system would already need to be
+ // up for 50 days and the Control key message would need to be processed just
+ // before the system time overflow and the wheel message just after.)
+ //
+ // We also take the chance to reset sZoomUntil if we simply have passed that
+ // time.
+ LONG msgTime = ::GetMessageTime();
+ if ((sZoomUntil >= 0x3fffffffu && DWORD(msgTime) < 0x40000000u) ||
+ (sZoomUntil < DWORD(msgTime))) {
+ sZoomUntil = 0;
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Elantech::UpdateZoomUntil(): "
+ "sZoomUntil was reset"));
+ }
+}
+
+/* static */
+bool MouseScrollHandler::Device::Elantech::IsZooming() {
+ // Assume the Control key is down if the Elantech touchpad has sent the
+ // mis-ordered WM_KEYDOWN/WM_MOUSEWHEEL messages. (See the comment in
+ // OnKeyUp.)
+ return (sZoomUntil && static_cast<DWORD>(::GetMessageTime()) < sZoomUntil);
+}
+
+/******************************************************************************
+ *
+ * Device::Apoint
+ *
+ ******************************************************************************/
+
+/* static */
+void MouseScrollHandler::Device::Apoint::Init() {
+ if (sInitialized) {
+ return;
+ }
+
+ sInitialized = true;
+ sMajorVersion = 0;
+ sMinorVersion = -1;
+
+ wchar_t buf[40];
+ if (!WinRegistry::GetString(HKEY_LOCAL_MACHINE, u"Software\\Alps\\Apoint"_ns,
+ u"ProductVer"_ns, buf,
+ WinRegistry::kLegacyWinUtilsStringFlags)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Apoint::Init(): "
+ "Apoint driver is not found"));
+ return;
+ }
+
+ sMajorVersion = wcstol(buf, nullptr, 10);
+ sMinorVersion = 0;
+ wchar_t* p = wcschr(buf, L'.');
+ if (p) {
+ sMinorVersion = wcstol(p + 1, nullptr, 10);
+ }
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::Apoint::Init(): "
+ "found driver version = %d.%d",
+ sMajorVersion, sMinorVersion));
+}
+
+/******************************************************************************
+ *
+ * Device::TrackPoint
+ *
+ ******************************************************************************/
+
+/* static */
+bool MouseScrollHandler::Device::TrackPoint::IsDriverInstalled() {
+ if (WinRegistry::HasKey(HKEY_CURRENT_USER,
+ u"Software\\Lenovo\\TrackPoint"_ns)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::TrackPoint::IsDriverInstalled(): "
+ "Lenovo's TrackPoint driver is found"));
+ return true;
+ }
+
+ if (WinRegistry::HasKey(HKEY_CURRENT_USER,
+ u"Software\\Alps\\Apoint\\TrackPoint"_ns)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::TrackPoint::IsDriverInstalled(): "
+ "Alps's TrackPoint driver is found"));
+ return true;
+ }
+
+ return false;
+}
+
+/******************************************************************************
+ *
+ * Device::UltraNav
+ *
+ ******************************************************************************/
+
+/* static */
+bool MouseScrollHandler::Device::UltraNav::IsObsoleteDriverInstalled() {
+ if (WinRegistry::HasKey(HKEY_CURRENT_USER,
+ u"Software\\Lenovo\\UltraNav"_ns)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): "
+ "Lenovo's UltraNav driver is found"));
+ return true;
+ }
+
+ bool installed = false;
+ if (WinRegistry::HasKey(HKEY_CURRENT_USER,
+ u"Software\\Synaptics\\SynTPEnh\\UltraNavUSB"_ns)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): "
+ "Synaptics's UltraNav (USB) driver is found"));
+ installed = true;
+ } else if (WinRegistry::HasKey(
+ HKEY_CURRENT_USER,
+ u"Software\\Synaptics\\SynTPEnh\\UltraNavPS2"_ns)) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): "
+ "Synaptics's UltraNav (PS/2) driver is found"));
+ installed = true;
+ }
+
+ if (!installed) {
+ return false;
+ }
+
+ int32_t majorVersion = Device::SynTP::GetDriverMajorVersion();
+ if (!majorVersion) {
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): "
+ "Failed to get UltraNav driver version"));
+ return false;
+ }
+ int32_t minorVersion = Device::SynTP::GetDriverMinorVersion();
+ return majorVersion < 15 || (majorVersion == 15 && minorVersion == 0);
+}
+
+/******************************************************************************
+ *
+ * Device::SetPoint
+ *
+ ******************************************************************************/
+
+/* static */
+bool MouseScrollHandler::Device::SetPoint::IsGetMessagePosResponseValid(
+ UINT aMessage, WPARAM aWParam, LPARAM aLParam) {
+ if (aMessage != WM_MOUSEHWHEEL) {
+ return false;
+ }
+
+ POINTS pts = MouseScrollHandler::GetCurrentMessagePos();
+ LPARAM messagePos = MAKELPARAM(pts.x, pts.y);
+
+ // XXX We should check whether SetPoint is installed or not by registry.
+
+ // SetPoint, Logitech (Logicool) mouse driver, (confirmed with 4.82.11 and
+ // MX-1100) always sets 0 to the lParam of WM_MOUSEHWHEEL. The driver SENDs
+ // one message at first time, this time, ::GetMessagePos() works fine.
+ // Then, we will return 0 (0 means we process it) to the message. Then, the
+ // driver will POST the same messages continuously during the wheel tilted.
+ // But ::GetMessagePos() API always returns (0, 0) for them, even if the
+ // actual mouse cursor isn't 0,0. Therefore, we cannot trust the result of
+ // ::GetMessagePos API if the sender is SetPoint.
+ if (!sMightBeUsing && !aLParam && aLParam != messagePos &&
+ ::InSendMessage()) {
+ sMightBeUsing = true;
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::SetPoint::IsGetMessagePosResponseValid(): "
+ "Might using SetPoint"));
+ } else if (sMightBeUsing && aLParam != 0 && ::InSendMessage()) {
+ // The user has changed the mouse from Logitech's to another one (e.g.,
+ // the user has changed to the touchpad of the notebook.
+ sMightBeUsing = false;
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScroll::Device::SetPoint::IsGetMessagePosResponseValid(): "
+ "Might stop using SetPoint"));
+ }
+ return (sMightBeUsing && !aLParam && !messagePos);
+}
+
+/******************************************************************************
+ *
+ * SynthesizingEvent
+ *
+ ******************************************************************************/
+
+/* static */
+bool MouseScrollHandler::SynthesizingEvent::IsSynthesizing() {
+ return MouseScrollHandler::sInstance &&
+ MouseScrollHandler::sInstance->mSynthesizingEvent &&
+ MouseScrollHandler::sInstance->mSynthesizingEvent->mStatus !=
+ NOT_SYNTHESIZING;
+}
+
+nsresult MouseScrollHandler::SynthesizingEvent::Synthesize(
+ const POINTS& aCursorPoint, HWND aWnd, UINT aMessage, WPARAM aWParam,
+ LPARAM aLParam, const BYTE (&aKeyStates)[256]) {
+ MOZ_LOG(
+ gMouseScrollLog, LogLevel::Info,
+ ("MouseScrollHandler::SynthesizingEvent::Synthesize(): aCursorPoint: { "
+ "x: %d, y: %d }, aWnd=0x%p, aMessage=0x%04X, aWParam=0x%08zX, "
+ "aLParam=0x%08" PRIXLPTR ", IsSynthesized()=%s, mStatus=%s",
+ aCursorPoint.x, aCursorPoint.y, aWnd, aMessage, aWParam, aLParam,
+ GetBoolName(IsSynthesizing()), GetStatusName()));
+
+ if (IsSynthesizing()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ ::GetKeyboardState(mOriginalKeyState);
+
+ // Note that we cannot use ::SetCursorPos() because it works asynchronously.
+ // We should SEND the message for reducing the possibility of receiving
+ // unexpected message which were not sent from here.
+ mCursorPoint = aCursorPoint;
+
+ mWnd = aWnd;
+ mMessage = aMessage;
+ mWParam = aWParam;
+ mLParam = aLParam;
+
+ memcpy(mKeyState, aKeyStates, sizeof(mKeyState));
+ ::SetKeyboardState(mKeyState);
+
+ mStatus = SENDING_MESSAGE;
+
+ // Don't assume that aWnd is always managed by nsWindow. It might be
+ // a plugin window.
+ ::SendMessage(aWnd, aMessage, aWParam, aLParam);
+
+ return NS_OK;
+}
+
+void MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(
+ nsWindow* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) {
+ if (mStatus == SENDING_MESSAGE && mMessage == aMessage &&
+ mWParam == aWParam && mLParam == aLParam) {
+ mStatus = NATIVE_MESSAGE_RECEIVED;
+ if (aWidget && aWidget->GetWindowHandle() == mWnd) {
+ return;
+ }
+ // Otherwise, the message may not be sent by us.
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(): "
+ "aWidget=%p, aWidget->GetWindowHandle()=0x%p, mWnd=0x%p, "
+ "aMessage=0x%04X, aWParam=0x%08zX, aLParam=0x%08" PRIXLPTR
+ ", mStatus=%s",
+ aWidget, aWidget ? aWidget->GetWindowHandle() : nullptr, mWnd,
+ aMessage, aWParam, aLParam, GetStatusName()));
+
+ // We failed to receive our sent message, we failed to do the job.
+ Finish();
+
+ return;
+}
+
+void MouseScrollHandler::SynthesizingEvent::
+ NotifyNativeMessageHandlingFinished() {
+ if (!IsSynthesizing()) {
+ return;
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScrollHandler::SynthesizingEvent::"
+ "NotifyNativeMessageHandlingFinished(): IsWaitingInternalMessage=%s",
+ GetBoolName(MouseScrollHandler::IsWaitingInternalMessage())));
+
+ if (MouseScrollHandler::IsWaitingInternalMessage()) {
+ mStatus = INTERNAL_MESSAGE_POSTED;
+ return;
+ }
+
+ // If the native message handler didn't post our internal message,
+ // we our job is finished.
+ // TODO: When we post the message to plugin window, there is remaning job.
+ Finish();
+}
+
+void MouseScrollHandler::SynthesizingEvent::
+ NotifyInternalMessageHandlingFinished() {
+ if (!IsSynthesizing()) {
+ return;
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScrollHandler::SynthesizingEvent::"
+ "NotifyInternalMessageHandlingFinished()"));
+
+ Finish();
+}
+
+void MouseScrollHandler::SynthesizingEvent::Finish() {
+ if (!IsSynthesizing()) {
+ return;
+ }
+
+ MOZ_LOG(gMouseScrollLog, LogLevel::Info,
+ ("MouseScrollHandler::SynthesizingEvent::Finish()"));
+
+ // Restore the original key state.
+ ::SetKeyboardState(mOriginalKeyState);
+
+ mStatus = NOT_SYNTHESIZING;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WinMouseScrollHandler.h b/widget/windows/WinMouseScrollHandler.h
new file mode 100644
index 0000000000..ecf6c3df44
--- /dev/null
+++ b/widget/windows/WinMouseScrollHandler.h
@@ -0,0 +1,567 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_WinMouseScrollHandler_h__
+#define mozilla_widget_WinMouseScrollHandler_h__
+
+#include "nscore.h"
+#include "nsDebug.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TimeStamp.h"
+#include "Units.h"
+#include <windows.h>
+#include "nsPoint.h"
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+class ModifierKeyState;
+
+struct MSGResult;
+
+class MouseScrollHandler {
+ public:
+ static MouseScrollHandler* GetInstance();
+
+ static void Initialize();
+ static void Shutdown();
+
+ static bool NeedsMessage(UINT aMsg);
+ static bool ProcessMessage(nsWindow* aWidget, UINT msg, WPARAM wParam,
+ LPARAM lParam, MSGResult& aResult);
+
+ /**
+ * See nsIWidget::SynthesizeNativeMouseScrollEvent() for the detail about
+ * this method.
+ */
+ static nsresult SynthesizeNativeMouseScrollEvent(
+ nsWindow* aWidget, const LayoutDeviceIntPoint& aPoint,
+ uint32_t aNativeMessage, int32_t aDelta, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags);
+
+ /**
+ * IsWaitingInternalMessage() returns true if MouseScrollHandler posted
+ * an internal message for a native mouse wheel message and has not
+ * received it. Otherwise, false.
+ */
+ static bool IsWaitingInternalMessage() {
+ return sInstance && sInstance->mIsWaitingInternalMessage;
+ }
+
+ private:
+ MouseScrollHandler();
+ ~MouseScrollHandler();
+
+ bool mIsWaitingInternalMessage;
+
+ static void MaybeLogKeyState();
+
+ static MouseScrollHandler* sInstance;
+
+ /**
+ * InitEvent() initializes the aEvent. If aPoint is null, the result of
+ * GetCurrentMessagePos() will be used.
+ */
+ static void InitEvent(nsWindow* aWidget, WidgetGUIEvent& aEvent,
+ LPARAM* aPoint);
+
+ /**
+ * GetModifierKeyState() returns current modifier key state.
+ * Note that some devices need some hack for the modifier key state.
+ * This method does it automatically.
+ *
+ * @param aMessage Handling message.
+ */
+ static ModifierKeyState GetModifierKeyState(UINT aMessage);
+
+ /**
+ * MozGetMessagePos() returns the mouse cursor position when GetMessage()
+ * was called last time. However, if we're sending a native message,
+ * this returns the specified cursor position by
+ * SynthesizeNativeMouseScrollEvent().
+ */
+ static POINTS GetCurrentMessagePos();
+
+ /**
+ * ProcessNativeMouseWheelMessage() processes WM_MOUSEWHEEL and
+ * WM_MOUSEHWHEEL. Additionally, processes WM_VSCROLL and WM_HSCROLL if they
+ * should be processed as mouse wheel message.
+ * This method posts MOZ_WM_MOUSEVWHEEL, MOZ_WM_MOUSEHWHEEL,
+ * MOZ_WM_VSCROLL or MOZ_WM_HSCROLL if we need to dispatch mouse scroll
+ * events. That avoids deadlock with plugin process.
+ *
+ * @param aWidget A window which receives the message.
+ * @param aMessage WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_VSCROLL or
+ * WM_HSCROLL.
+ * @param aWParam The wParam value of the message.
+ * @param aLParam The lParam value of the message.
+ */
+ void ProcessNativeMouseWheelMessage(nsWindow* aWidget, UINT aMessage,
+ WPARAM aWParam, LPARAM aLParam);
+
+ /**
+ * ProcessNativeScrollMessage() processes WM_VSCROLL and WM_HSCROLL.
+ * This method just call ProcessMouseWheelMessage() if the message should be
+ * processed as mouse wheel message. Otherwise, dispatches a content
+ * command event.
+ *
+ * @param aWidget A window which receives the message.
+ * @param aMessage WM_VSCROLL or WM_HSCROLL.
+ * @param aWParam The wParam value of the message.
+ * @param aLParam The lParam value of the message.
+ * @return TRUE if the message is processed. Otherwise, FALSE.
+ */
+ bool ProcessNativeScrollMessage(nsWindow* aWidget, UINT aMessage,
+ WPARAM aWParam, LPARAM aLParam);
+
+ /**
+ * HandleMouseWheelMessage() processes MOZ_WM_MOUSEVWHEEL and
+ * MOZ_WM_MOUSEHWHEEL which are posted when one of our windows received
+ * WM_MOUSEWHEEL or WM_MOUSEHWHEEL for avoiding deadlock with OOPP.
+ *
+ * @param aWidget A window which receives the wheel message.
+ * @param aMessage MOZ_WM_MOUSEWHEEL or MOZ_WM_MOUSEHWHEEL.
+ * @param aWParam The wParam value of the original message.
+ * @param aLParam The lParam value of the original message.
+ */
+ void HandleMouseWheelMessage(nsWindow* aWidget, UINT aMessage, WPARAM aWParam,
+ LPARAM aLParam);
+
+ /**
+ * HandleScrollMessageAsMouseWheelMessage() processes the MOZ_WM_VSCROLL and
+ * MOZ_WM_HSCROLL which are posted when one of mouse windows received
+ * WM_VSCROLL or WM_HSCROLL and user wants them to emulate mouse wheel
+ * message's behavior.
+ *
+ * @param aWidget A window which receives the scroll message.
+ * @param aMessage MOZ_WM_VSCROLL or MOZ_WM_HSCROLL.
+ * @param aWParam The wParam value of the original message.
+ * @param aLParam The lParam value of the original message.
+ */
+ void HandleScrollMessageAsMouseWheelMessage(nsWindow* aWidget, UINT aMessage,
+ WPARAM aWParam, LPARAM aLParam);
+
+ /**
+ * ComputeMessagePos() computes the cursor position when the message was
+ * added to the queue.
+ *
+ * @param aMessage Handling message.
+ * @param aWParam Handling message's wParam.
+ * @param aLParam Handling message's lParam.
+ * @return Mouse cursor position when the message is added to
+ * the queue or current cursor position if the result of
+ * ::GetMessagePos() is broken.
+ */
+ POINT ComputeMessagePos(UINT aMessage, WPARAM aWParam, LPARAM aLParam);
+
+ class EventInfo {
+ public:
+ /**
+ * @param aWidget An nsWindow which is handling the event.
+ * @param aMessage Must be WM_MOUSEWHEEL or WM_MOUSEHWHEEL.
+ */
+ EventInfo(nsWindow* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam);
+
+ bool CanDispatchWheelEvent() const;
+
+ int32_t GetNativeDelta() const { return mDelta; }
+ HWND GetWindowHandle() const { return mWnd; }
+ const TimeStamp& GetTimeStamp() const { return mTimeStamp; }
+ bool IsVertical() const { return mIsVertical; }
+ bool IsPositive() const { return (mDelta > 0); }
+ bool IsPage() const { return mIsPage; }
+
+ /**
+ * @return Number of lines or pages scrolled per WHEEL_DELTA.
+ */
+ int32_t GetScrollAmount() const;
+
+ protected:
+ EventInfo()
+ : mIsVertical(false), mIsPage(false), mDelta(0), mWnd(nullptr) {}
+
+ // TRUE if event is for vertical scroll. Otherwise, FALSE.
+ bool mIsVertical;
+ // TRUE if event scrolls per page, otherwise, FALSE.
+ bool mIsPage;
+ // The native delta value.
+ int32_t mDelta;
+ // The window handle which is handling the event.
+ HWND mWnd;
+ // Timestamp of the event.
+ TimeStamp mTimeStamp;
+ };
+
+ class LastEventInfo : public EventInfo {
+ public:
+ LastEventInfo() : EventInfo(), mAccumulatedDelta(0) {}
+
+ /**
+ * CanContinueTransaction() checks whether the new event can continue the
+ * last transaction or not. Note that if there is no transaction, this
+ * returns true.
+ */
+ bool CanContinueTransaction(const EventInfo& aNewEvent);
+
+ /**
+ * ResetTransaction() resets the transaction, i.e., the instance forgets
+ * the last event information.
+ */
+ void ResetTransaction();
+
+ /**
+ * RecordEvent() saves the information of new event.
+ */
+ void RecordEvent(const EventInfo& aEvent);
+
+ /**
+ * InitWheelEvent() initializes NS_WHEEL_WHEEL event and
+ * recomputes the remaning detla for the event.
+ * This must be called only once during handling a message and after
+ * RecordEvent() is called.
+ *
+ * @param aWidget A window which will dispatch the event.
+ * @param aWheelEvent An NS_WHEEL_WHEEL event, this will be
+ * initialized.
+ * @param aModKeyState Current modifier key state.
+ * @return TRUE if the event is ready to dispatch.
+ * Otherwise, FALSE.
+ */
+ bool InitWheelEvent(nsWindow* aWidget, WidgetWheelEvent& aWheelEvent,
+ const ModifierKeyState& aModKeyState, LPARAM aLParam);
+
+ private:
+ static int32_t RoundDelta(double aDelta);
+
+ int32_t mAccumulatedDelta;
+ };
+
+ LastEventInfo mLastEventInfo;
+
+ class SystemSettings {
+ public:
+ SystemSettings() : mInitialized(false) {}
+
+ void Init();
+ void MarkDirty();
+ void NotifyUserPrefsMayOverrideSystemSettings();
+
+ // On some environments, SystemParametersInfo() may be hooked by touchpad
+ // utility or something. In such case, when user changes active pointing
+ // device to another one, the result of SystemParametersInfo() may be
+ // changed without WM_SETTINGCHANGE message. For avoiding this trouble,
+ // we need to modify cache of system settings at every wheel message
+ // handling if we meet known device whose utility may hook the API.
+ void TrustedScrollSettingsDriver();
+
+ int32_t GetScrollAmount(bool aForVertical) const {
+ MOZ_ASSERT(mInitialized, "SystemSettings must be initialized");
+ return aForVertical ? mScrollLines : mScrollChars;
+ }
+
+ bool IsPageScroll(bool aForVertical) const {
+ MOZ_ASSERT(mInitialized, "SystemSettings must be initialized");
+ return aForVertical ? (uint32_t(mScrollLines) == WHEEL_PAGESCROLL)
+ : (uint32_t(mScrollChars) == WHEEL_PAGESCROLL);
+ }
+
+ // The default vertical and horizontal scrolling speed is 3, this is defined
+ // on the document of SystemParametersInfo in MSDN.
+ static int32_t DefaultScrollLines() { return 3; }
+
+ private:
+ bool mInitialized;
+ // The result of SystemParametersInfo() may not be reliable since it may
+ // be hooked. So, if the values are initialized with prefs, we can trust
+ // the value. Following mIsReliableScroll* are set true when mScroll* are
+ // initialized with prefs.
+ bool mIsReliableScrollLines;
+ bool mIsReliableScrollChars;
+
+ int32_t mScrollLines;
+ int32_t mScrollChars;
+
+ // Returns true if cached value is changed.
+ bool InitScrollLines();
+ bool InitScrollChars();
+
+ void RefreshCache();
+ };
+
+ SystemSettings mSystemSettings;
+
+ class UserPrefs {
+ public:
+ UserPrefs();
+ ~UserPrefs();
+
+ void MarkDirty();
+
+ bool IsScrollMessageHandledAsWheelMessage() {
+ Init();
+ return mScrollMessageHandledAsWheelMessage;
+ }
+
+ bool IsSystemSettingCacheEnabled() {
+ Init();
+ return mEnableSystemSettingCache;
+ }
+
+ bool IsSystemSettingCacheForciblyEnabled() {
+ Init();
+ return mForceEnableSystemSettingCache;
+ }
+
+ bool ShouldEmulateToMakeWindowUnderCursorForeground() {
+ Init();
+ return mEmulateToMakeWindowUnderCursorForeground;
+ }
+
+ int32_t GetOverriddenVerticalScrollAmout() {
+ Init();
+ return mOverriddenVerticalScrollAmount;
+ }
+
+ int32_t GetOverriddenHorizontalScrollAmout() {
+ Init();
+ return mOverriddenHorizontalScrollAmount;
+ }
+
+ int32_t GetMouseScrollTransactionTimeout() {
+ Init();
+ return mMouseScrollTransactionTimeout;
+ }
+
+ private:
+ void Init();
+
+ static void OnChange(const char* aPrefName, void* aSelf) {
+ static_cast<UserPrefs*>(aSelf)->MarkDirty();
+ }
+
+ bool mInitialized;
+ bool mScrollMessageHandledAsWheelMessage;
+ bool mEnableSystemSettingCache;
+ bool mForceEnableSystemSettingCache;
+ bool mEmulateToMakeWindowUnderCursorForeground;
+ int32_t mOverriddenVerticalScrollAmount;
+ int32_t mOverriddenHorizontalScrollAmount;
+ int32_t mMouseScrollTransactionTimeout;
+ };
+
+ UserPrefs mUserPrefs;
+
+ class SynthesizingEvent {
+ public:
+ SynthesizingEvent()
+ : mWnd(nullptr),
+ mMessage(0),
+ mWParam(0),
+ mLParam(0),
+ mStatus(NOT_SYNTHESIZING) {}
+
+ ~SynthesizingEvent() {}
+
+ static bool IsSynthesizing();
+
+ nsresult Synthesize(const POINTS& aCursorPoint, HWND aWnd, UINT aMessage,
+ WPARAM aWParam, LPARAM aLParam,
+ const BYTE (&aKeyStates)[256]);
+
+ void NativeMessageReceived(nsWindow* aWidget, UINT aMessage, WPARAM aWParam,
+ LPARAM aLParam);
+
+ void NotifyNativeMessageHandlingFinished();
+ void NotifyInternalMessageHandlingFinished();
+
+ const POINTS& GetCursorPoint() const { return mCursorPoint; }
+
+ private:
+ POINTS mCursorPoint;
+ HWND mWnd;
+ UINT mMessage;
+ WPARAM mWParam;
+ LPARAM mLParam;
+ BYTE mKeyState[256];
+ BYTE mOriginalKeyState[256];
+
+ enum Status {
+ NOT_SYNTHESIZING,
+ SENDING_MESSAGE,
+ NATIVE_MESSAGE_RECEIVED,
+ INTERNAL_MESSAGE_POSTED,
+ };
+ Status mStatus;
+
+ const char* GetStatusName() {
+ switch (mStatus) {
+ case NOT_SYNTHESIZING:
+ return "NOT_SYNTHESIZING";
+ case SENDING_MESSAGE:
+ return "SENDING_MESSAGE";
+ case NATIVE_MESSAGE_RECEIVED:
+ return "NATIVE_MESSAGE_RECEIVED";
+ case INTERNAL_MESSAGE_POSTED:
+ return "INTERNAL_MESSAGE_POSTED";
+ default:
+ return "Unknown";
+ }
+ }
+
+ void Finish();
+ }; // SynthesizingEvent
+
+ SynthesizingEvent* mSynthesizingEvent;
+
+ public:
+ class Device {
+ public:
+ // SynTP is a touchpad driver of Synaptics.
+ class SynTP {
+ public:
+ static bool IsDriverInstalled() { return sMajorVersion != 0; }
+ /**
+ * GetDriverMajorVersion() returns the installed driver's major version.
+ * If SynTP driver isn't installed, this returns 0.
+ */
+ static int32_t GetDriverMajorVersion() { return sMajorVersion; }
+ /**
+ * GetDriverMinorVersion() returns the installed driver's minor version.
+ * If SynTP driver isn't installed, this returns -1.
+ */
+ static int32_t GetDriverMinorVersion() { return sMinorVersion; }
+
+ static void Init();
+
+ private:
+ static bool sInitialized;
+ static int32_t sMajorVersion;
+ static int32_t sMinorVersion;
+ };
+
+ class Elantech {
+ public:
+ /**
+ * GetDriverMajorVersion() returns the installed driver's major version.
+ * If Elantech's driver was installed, returns 0.
+ */
+ static int32_t GetDriverMajorVersion();
+
+ /**
+ * IsHelperWindow() checks whether aWnd is a helper window of Elantech's
+ * touchpad. Returns TRUE if so. Otherwise, FALSE.
+ */
+ static bool IsHelperWindow(HWND aWnd);
+
+ /**
+ * Key message handler for Elantech's hack. Returns TRUE if the message
+ * is consumed by this handler. Otherwise, FALSE.
+ */
+ static bool HandleKeyMessage(nsWindow* aWidget, UINT aMsg, WPARAM aWParam,
+ LPARAM aLParam);
+
+ static void UpdateZoomUntil();
+ static bool IsZooming();
+
+ static void Init();
+
+ static bool IsPinchHackNeeded() { return sUsePinchHack; }
+
+ private:
+ // Whether to enable the Elantech swipe gesture hack.
+ static bool sUseSwipeHack;
+ // Whether to enable the Elantech pinch-to-zoom gesture hack.
+ static bool sUsePinchHack;
+ static DWORD sZoomUntil;
+ }; // class Elantech
+
+ // Apoint is a touchpad driver of Alps.
+ class Apoint {
+ public:
+ static bool IsDriverInstalled() { return sMajorVersion != 0; }
+ /**
+ * GetDriverMajorVersion() returns the installed driver's major version.
+ * If Apoint driver isn't installed, this returns 0.
+ */
+ static int32_t GetDriverMajorVersion() { return sMajorVersion; }
+ /**
+ * GetDriverMinorVersion() returns the installed driver's minor version.
+ * If Apoint driver isn't installed, this returns -1.
+ */
+ static int32_t GetDriverMinorVersion() { return sMinorVersion; }
+
+ static void Init();
+
+ private:
+ static bool sInitialized;
+ static int32_t sMajorVersion;
+ static int32_t sMinorVersion;
+ };
+
+ class TrackPoint {
+ public:
+ /**
+ * IsDriverInstalled() returns TRUE if TrackPoint's driver is installed.
+ * Otherwise, returns FALSE.
+ */
+ static bool IsDriverInstalled();
+ }; // class TrackPoint
+
+ class UltraNav {
+ public:
+ /**
+ * IsObsoleteDriverInstalled() checks whether obsoleted UltraNav
+ * is installed on the environment.
+ * Returns TRUE if it was installed. Otherwise, FALSE.
+ */
+ static bool IsObsoleteDriverInstalled();
+ }; // class UltraNav
+
+ class SetPoint {
+ public:
+ /**
+ * SetPoint, Logitech's mouse driver, may report wrong cursor position
+ * for WM_MOUSEHWHEEL message. See comment in the implementation for
+ * the detail.
+ */
+ static bool IsGetMessagePosResponseValid(UINT aMessage, WPARAM aWParam,
+ LPARAM aLParam);
+
+ private:
+ static bool sMightBeUsing;
+ };
+
+ static void Init();
+
+ static bool IsFakeScrollableWindowNeeded() {
+ return sFakeScrollableWindowNeeded;
+ }
+
+ private:
+ /**
+ * Gets the bool value of aPrefName used to enable or disable an input
+ * workaround (like the Trackpoint hack). The pref can take values 0 (for
+ * disabled), 1 (for enabled) or -1 (to automatically detect whether to
+ * enable the workaround).
+ *
+ * @param aPrefName The name of the pref.
+ * @param aValueIfAutomatic Whether the given input workaround should be
+ * enabled by default.
+ */
+ static bool GetWorkaroundPref(const char* aPrefName,
+ bool aValueIfAutomatic);
+
+ static bool sFakeScrollableWindowNeeded;
+ }; // class Device
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_WinMouseScrollHandler_h__
diff --git a/widget/windows/WinPointerEvents.cpp b/widget/windows/WinPointerEvents.cpp
new file mode 100644
index 0000000000..57e19a0c4b
--- /dev/null
+++ b/widget/windows/WinPointerEvents.cpp
@@ -0,0 +1,181 @@
+/* -*- 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/. */
+
+/*
+ * WinPointerEvents - Helper functions to retrieve PointerEvent's attributes
+ */
+
+#include "nscore.h"
+#include "nsWindowDefs.h"
+#include "WinPointerEvents.h"
+#include "WinUtils.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/MouseEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+const wchar_t WinPointerEvents::kPointerLibraryName[] = L"user32.dll";
+HMODULE WinPointerEvents::sLibraryHandle = nullptr;
+WinPointerEvents::GetPointerTypePtr WinPointerEvents::getPointerType = nullptr;
+WinPointerEvents::GetPointerInfoPtr WinPointerEvents::getPointerInfo = nullptr;
+WinPointerEvents::GetPointerPenInfoPtr WinPointerEvents::getPointerPenInfo =
+ nullptr;
+
+WinPointerEvents::WinPointerEvents() { InitLibrary(); }
+
+/* Load and shutdown */
+void WinPointerEvents::InitLibrary() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (getPointerType) {
+ // Return if we already initialized the PointerEvent related interfaces
+ return;
+ }
+ sLibraryHandle = ::LoadLibraryW(kPointerLibraryName);
+ MOZ_ASSERT(sLibraryHandle, "cannot load pointer library");
+ if (sLibraryHandle) {
+ getPointerType =
+ (GetPointerTypePtr)GetProcAddress(sLibraryHandle, "GetPointerType");
+ getPointerInfo =
+ (GetPointerInfoPtr)GetProcAddress(sLibraryHandle, "GetPointerInfo");
+ getPointerPenInfo = (GetPointerPenInfoPtr)GetProcAddress(
+ sLibraryHandle, "GetPointerPenInfo");
+ }
+
+ if (!getPointerType || !getPointerInfo || !getPointerPenInfo) {
+ MOZ_ASSERT(false, "get PointerEvent interfaces failed");
+ getPointerType = nullptr;
+ getPointerInfo = nullptr;
+ getPointerPenInfo = nullptr;
+ return;
+ }
+}
+
+bool WinPointerEvents::ShouldHandleWinPointerMessages(UINT aMsg,
+ WPARAM aWParam) {
+ MOZ_ASSERT(aMsg == WM_POINTERDOWN || aMsg == WM_POINTERUP ||
+ aMsg == WM_POINTERUPDATE || aMsg == WM_POINTERLEAVE);
+ if (!sLibraryHandle) {
+ return false;
+ }
+
+ // We only handle WM_POINTER* when the input source is pen. This is because
+ // we need some information (e.g. tiltX, tiltY) which can't be retrieved by
+ // WM_*BUTTONDOWN.
+ uint32_t pointerId = GetPointerId(aWParam);
+ POINTER_INPUT_TYPE pointerType = PT_POINTER;
+ if (!GetPointerType(pointerId, &pointerType)) {
+ MOZ_ASSERT(false, "cannot find PointerType");
+ return false;
+ }
+ return (pointerType == PT_PEN);
+}
+
+bool WinPointerEvents::GetPointerType(uint32_t aPointerId,
+ POINTER_INPUT_TYPE* aPointerType) {
+ if (!getPointerType) {
+ return false;
+ }
+ return getPointerType(aPointerId, aPointerType);
+}
+
+POINTER_INPUT_TYPE
+WinPointerEvents::GetPointerType(uint32_t aPointerId) {
+ POINTER_INPUT_TYPE pointerType = PT_POINTER;
+ Unused << GetPointerType(aPointerId, &pointerType);
+ return pointerType;
+}
+
+bool WinPointerEvents::GetPointerInfo(uint32_t aPointerId,
+ POINTER_INFO* aPointerInfo) {
+ if (!getPointerInfo) {
+ return false;
+ }
+ return getPointerInfo(aPointerId, aPointerInfo);
+}
+
+bool WinPointerEvents::GetPointerPenInfo(uint32_t aPointerId,
+ POINTER_PEN_INFO* aPenInfo) {
+ if (!getPointerPenInfo) {
+ return false;
+ }
+ return getPointerPenInfo(aPointerId, aPenInfo);
+}
+
+bool WinPointerEvents::ShouldRollupOnPointerEvent(UINT aMsg, WPARAM aWParam) {
+ MOZ_ASSERT(aMsg == WM_POINTERDOWN);
+ // Only roll up popups when we handling WM_POINTER* to fire Gecko
+ // WidgetMouseEvent and suppress Windows WM_*BUTTONDOWN.
+ return ShouldHandleWinPointerMessages(aMsg, aWParam) &&
+ ShouldFirePointerEventByWinPointerMessages();
+}
+
+bool WinPointerEvents::ShouldFirePointerEventByWinPointerMessages() {
+ MOZ_ASSERT(sLibraryHandle);
+ return StaticPrefs::dom_w3c_pointer_events_dispatch_by_pointer_messages();
+}
+
+WinPointerInfo* WinPointerEvents::GetCachedPointerInfo(UINT aMsg,
+ WPARAM aWParam) {
+ if (!sLibraryHandle ||
+ MOUSE_INPUT_SOURCE() != dom::MouseEvent_Binding::MOZ_SOURCE_PEN ||
+ ShouldFirePointerEventByWinPointerMessages()) {
+ return nullptr;
+ }
+ switch (aMsg) {
+ case WM_LBUTTONDOWN:
+ case WM_MBUTTONDOWN:
+ case WM_RBUTTONDOWN:
+ return &mPenPointerDownInfo;
+ case WM_LBUTTONUP:
+ case WM_MBUTTONUP:
+ case WM_RBUTTONUP:
+ return &mPenPointerDownInfo;
+ case WM_MOUSEMOVE:
+ return &mPenPointerUpdateInfo;
+ default:
+ MOZ_ASSERT(false);
+ }
+ return nullptr;
+}
+
+void WinPointerEvents::ConvertAndCachePointerInfo(UINT aMsg, WPARAM aWParam) {
+ MOZ_ASSERT(
+ !StaticPrefs::dom_w3c_pointer_events_dispatch_by_pointer_messages());
+ // Windows doesn't support chorded buttons for pen, so we can simply keep the
+ // latest information from pen generated pointer messages and use them when
+ // handling mouse messages. Used different pointer info for pointerdown,
+ // pointerupdate, and pointerup because Windows doesn't always interleave
+ // pointer messages and mouse messages.
+ switch (aMsg) {
+ case WM_POINTERDOWN:
+ ConvertAndCachePointerInfo(aWParam, &mPenPointerDownInfo);
+ break;
+ case WM_POINTERUP:
+ ConvertAndCachePointerInfo(aWParam, &mPenPointerUpInfo);
+ break;
+ case WM_POINTERUPDATE:
+ ConvertAndCachePointerInfo(aWParam, &mPenPointerUpdateInfo);
+ break;
+ default:
+ break;
+ }
+}
+
+void WinPointerEvents::ConvertAndCachePointerInfo(WPARAM aWParam,
+ WinPointerInfo* aInfo) {
+ MOZ_ASSERT(
+ !StaticPrefs::dom_w3c_pointer_events_dispatch_by_pointer_messages());
+ aInfo->pointerId = GetPointerId(aWParam);
+ MOZ_ASSERT(GetPointerType(aInfo->pointerId) == PT_PEN);
+ POINTER_PEN_INFO penInfo;
+ GetPointerPenInfo(aInfo->pointerId, &penInfo);
+ aInfo->tiltX = penInfo.tiltX;
+ aInfo->tiltY = penInfo.tiltY;
+ // Windows defines the pen pressure is normalized to a range between 0 and
+ // 1024. Convert it to float.
+ aInfo->mPressure = penInfo.pressure ? (float)penInfo.pressure / 1024 : 0;
+}
diff --git a/widget/windows/WinPointerEvents.h b/widget/windows/WinPointerEvents.h
new file mode 100644
index 0000000000..94179e7b4e
--- /dev/null
+++ b/widget/windows/WinPointerEvents.h
@@ -0,0 +1,75 @@
+/* -*- 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/. */
+
+#ifndef WinPointerEvents_h__
+#define WinPointerEvents_h__
+
+#include "mozilla/MouseEvents.h"
+#include "touchinjection_sdk80.h"
+#include <windef.h>
+
+/******************************************************************************
+ * WinPointerInfo
+ *
+ * This is a helper class to handle WM_POINTER*. It only supports Win8 or later.
+ *
+ ******************************************************************************/
+class WinPointerInfo final : public mozilla::WidgetPointerHelper {
+ public:
+ WinPointerInfo() : WidgetPointerHelper(), mPressure(0), mButtons(0) {}
+
+ WinPointerInfo(uint32_t aPointerId, uint32_t aTiltX, uint32_t aTiltY,
+ float aPressure, int16_t aButtons)
+ : WidgetPointerHelper(aPointerId, aTiltX, aTiltY),
+ mPressure(aPressure),
+ mButtons(aButtons) {}
+
+ float mPressure;
+ int16_t mButtons;
+};
+
+class WinPointerEvents final {
+ public:
+ explicit WinPointerEvents();
+
+ public:
+ bool ShouldHandleWinPointerMessages(UINT aMsg, WPARAM aWParam);
+
+ uint32_t GetPointerId(WPARAM aWParam) {
+ return GET_POINTERID_WPARAM(aWParam);
+ }
+ bool GetPointerType(uint32_t aPointerId, POINTER_INPUT_TYPE* aPointerType);
+ POINTER_INPUT_TYPE GetPointerType(uint32_t aPointerId);
+ bool GetPointerInfo(uint32_t aPointerId, POINTER_INFO* aPointerInfo);
+ bool GetPointerPenInfo(uint32_t aPointerId, POINTER_PEN_INFO* aPenInfo);
+ bool ShouldRollupOnPointerEvent(UINT aMsg, WPARAM aWParam);
+ bool ShouldFirePointerEventByWinPointerMessages();
+ WinPointerInfo* GetCachedPointerInfo(UINT aMsg, WPARAM aWParam);
+ void ConvertAndCachePointerInfo(UINT aMsg, WPARAM aWParam);
+ void ConvertAndCachePointerInfo(WPARAM aWParam, WinPointerInfo* aInfo);
+
+ private:
+ // Function prototypes
+ typedef BOOL(WINAPI* GetPointerTypePtr)(uint32_t aPointerId,
+ POINTER_INPUT_TYPE* aPointerType);
+ typedef BOOL(WINAPI* GetPointerInfoPtr)(uint32_t aPointerId,
+ POINTER_INFO* aPointerInfo);
+ typedef BOOL(WINAPI* GetPointerPenInfoPtr)(uint32_t aPointerId,
+ POINTER_PEN_INFO* aPenInfo);
+
+ void InitLibrary();
+
+ static HMODULE sLibraryHandle;
+ static const wchar_t kPointerLibraryName[];
+ // Static function pointers
+ static GetPointerTypePtr getPointerType;
+ static GetPointerInfoPtr getPointerInfo;
+ static GetPointerPenInfoPtr getPointerPenInfo;
+ WinPointerInfo mPenPointerDownInfo;
+ WinPointerInfo mPenPointerUpInfo;
+ WinPointerInfo mPenPointerUpdateInfo;
+};
+
+#endif // #ifndef WinPointerEvents_h__
diff --git a/widget/windows/WinRegistry.cpp b/widget/windows/WinRegistry.cpp
new file mode 100644
index 0000000000..b04ae1df45
--- /dev/null
+++ b/widget/windows/WinRegistry.cpp
@@ -0,0 +1,325 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 "WinRegistry.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::widget::WinRegistry {
+
+Key::Key(HKEY aParent, const nsString& aPath, KeyMode aMode, CreateFlag) {
+ MOZ_ASSERT(aParent);
+ DWORD disposition;
+ ::RegCreateKeyExW(aParent, aPath.get(), 0, nullptr, REG_OPTION_NON_VOLATILE,
+ (REGSAM)aMode, nullptr, &mKey, &disposition);
+}
+
+Key::Key(HKEY aParent, const nsString& aPath, KeyMode aMode) {
+ MOZ_ASSERT(aParent);
+ ::RegOpenKeyExW(aParent, aPath.get(), 0, (REGSAM)aMode, &mKey);
+}
+
+uint32_t Key::GetChildCount() const {
+ MOZ_ASSERT(mKey);
+ DWORD result = 0;
+ ::RegQueryInfoKeyW(mKey, nullptr, nullptr, nullptr, &result, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr, nullptr);
+ return result;
+}
+
+bool Key::GetChildName(uint32_t aIndex, nsAString& aResult) const {
+ MOZ_ASSERT(mKey);
+ FILETIME lastWritten;
+
+ wchar_t nameBuf[kMaxKeyNameLen + 1];
+ DWORD nameLen = std::size(nameBuf);
+
+ LONG rv = RegEnumKeyExW(mKey, aIndex, nameBuf, &nameLen, nullptr, nullptr,
+ nullptr, &lastWritten);
+ if (rv != ERROR_SUCCESS) {
+ return false;
+ }
+ aResult.Assign(nameBuf, nameLen);
+ return true;
+}
+
+bool Key::RemoveChildKey(const nsString& aName) const {
+ MOZ_ASSERT(mKey);
+ return SUCCEEDED(RegDeleteKeyW(mKey, aName.get()));
+}
+
+uint32_t Key::GetValueCount() const {
+ MOZ_ASSERT(mKey);
+ DWORD result = 0;
+ ::RegQueryInfoKeyW(mKey, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+ &result, nullptr, nullptr, nullptr, nullptr);
+ return result;
+}
+
+bool Key::GetValueName(uint32_t aIndex, nsAString& aResult) const {
+ MOZ_ASSERT(mKey);
+ wchar_t nameBuf[kMaxValueNameLen + 1];
+ DWORD nameLen = std::size(nameBuf);
+
+ LONG rv = RegEnumValueW(mKey, aIndex, nameBuf, &nameLen, nullptr, nullptr,
+ nullptr, nullptr);
+ if (rv != ERROR_SUCCESS) {
+ return false;
+ }
+ aResult.Assign(nameBuf, nameLen);
+ return true;
+}
+
+ValueType Key::GetValueType(const nsString& aName) const {
+ MOZ_ASSERT(mKey);
+ DWORD result;
+ LONG rv =
+ RegQueryValueExW(mKey, aName.get(), nullptr, &result, nullptr, nullptr);
+ return SUCCEEDED(rv) ? ValueType(result) : ValueType::None;
+}
+
+bool Key::RemoveValue(const nsString& aName) const {
+ MOZ_ASSERT(mKey);
+ return SUCCEEDED(RegDeleteValueW(mKey, aName.get()));
+}
+
+Maybe<uint32_t> Key::GetValueAsDword(const nsString& aName) const {
+ MOZ_ASSERT(mKey);
+ DWORD type;
+ DWORD value = 0;
+ DWORD size = sizeof(DWORD);
+ HRESULT rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type,
+ (LPBYTE)&value, &size);
+ if (FAILED(rv) || type != REG_DWORD) {
+ return Nothing();
+ }
+ return Some(value);
+}
+
+bool Key::WriteValueAsDword(const nsString& aName, uint32_t aValue) {
+ MOZ_ASSERT(mKey);
+ return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_DWORD,
+ (const BYTE*)&aValue, sizeof(aValue)));
+}
+
+Maybe<uint64_t> Key::GetValueAsQword(const nsString& aName) const {
+ MOZ_ASSERT(mKey);
+ DWORD type;
+ uint64_t value = 0;
+ DWORD size = sizeof(uint64_t);
+ HRESULT rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type,
+ (LPBYTE)&value, &size);
+ if (FAILED(rv) || type != REG_QWORD) {
+ return Nothing();
+ }
+ return Some(value);
+}
+
+bool Key::WriteValueAsQword(const nsString& aName, uint64_t aValue) {
+ MOZ_ASSERT(mKey);
+ return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_QWORD,
+ (const BYTE*)&aValue, sizeof(aValue)));
+}
+
+bool Key::GetValueAsBinary(const nsString& aName,
+ nsTArray<uint8_t>& aResult) const {
+ MOZ_ASSERT(mKey);
+ DWORD type;
+ DWORD size;
+ LONG rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type, nullptr, &size);
+ if (FAILED(rv) || type != REG_BINARY) {
+ return false;
+ }
+ if (!aResult.SetLength(size, fallible)) {
+ return false;
+ }
+ rv = RegQueryValueExW(mKey, aName.get(), nullptr, nullptr, aResult.Elements(),
+ &size);
+ return SUCCEEDED(rv);
+}
+
+Maybe<nsTArray<uint8_t>> Key::GetValueAsBinary(const nsString& aName) const {
+ nsTArray<uint8_t> value;
+ Maybe<nsTArray<uint8_t>> result;
+ if (GetValueAsBinary(aName, value)) {
+ result.emplace(std::move(value));
+ }
+ return result;
+}
+
+bool Key::WriteValueAsBinary(const nsString& aName,
+ Span<const uint8_t> aValue) {
+ MOZ_ASSERT(mKey);
+ return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_BINARY,
+ (const BYTE*)aValue.data(), aValue.size()));
+}
+
+static bool IsStringType(DWORD aType, StringFlags aFlags) {
+ switch (aType) {
+ case REG_SZ:
+ return bool(aFlags & StringFlags::Sz);
+ case REG_EXPAND_SZ:
+ return bool(aFlags & StringFlags::ExpandSz);
+ case REG_MULTI_SZ:
+ return bool(aFlags & StringFlags::LegacyMultiSz);
+ default:
+ return false;
+ }
+}
+
+bool Key::GetValueAsString(const nsString& aName, nsString& aResult,
+ StringFlags aFlags) const {
+ MOZ_ASSERT(mKey);
+ DWORD type;
+ DWORD size;
+ LONG rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type, nullptr, &size);
+ if (FAILED(rv) || !IsStringType(type, aFlags)) {
+ return false;
+ }
+ if (!size) {
+ aResult.Truncate();
+ return true;
+ }
+ // The buffer size must be a multiple of 2.
+ if (NS_WARN_IF(size % 2 != 0)) {
+ return false;
+ }
+ size_t resultLen = size / 2;
+ {
+ auto handleOrError =
+ aResult.BulkWrite(resultLen, 0, /* aAllowShrinking = */ false);
+ if (NS_WARN_IF(handleOrError.isErr())) {
+ return false;
+ }
+ auto handle = handleOrError.unwrap();
+ auto len = GetValueAsString(aName, {handle.Elements(), handle.Length() + 1},
+ aFlags & ~StringFlags::ExpandEnvironment);
+ if (NS_WARN_IF(!len)) {
+ return false;
+ }
+ handle.Finish(*len, /* aAllowShrinking = */ false);
+ if (*len && !aResult.CharAt(*len - 1)) {
+ // The string passed to us had a null terminator in the final
+ // position.
+ aResult.Truncate(*len - 1);
+ }
+ }
+ if (type == REG_EXPAND_SZ && (aFlags & StringFlags::ExpandEnvironment)) {
+ resultLen = ExpandEnvironmentStringsW(aResult.get(), nullptr, 0);
+ if (resultLen > 1) {
+ nsString expandedResult;
+ // |resultLen| includes the terminating null character
+ resultLen--;
+ if (!expandedResult.SetLength(resultLen, fallible)) {
+ return false;
+ }
+ resultLen = ExpandEnvironmentStringsW(aResult.get(), expandedResult.get(),
+ resultLen + 1);
+ if (resultLen <= 0) {
+ return false;
+ }
+ aResult = std::move(expandedResult);
+ } else if (resultLen == 1) {
+ // It apparently expands to nothing (just a null terminator).
+ resultLen = 0;
+ aResult.Truncate();
+ }
+ }
+ return true;
+}
+
+Maybe<nsString> Key::GetValueAsString(const nsString& aName,
+ StringFlags aFlags) const {
+ nsString value;
+ Maybe<nsString> result;
+ if (GetValueAsString(aName, value, aFlags)) {
+ result.emplace(std::move(value));
+ }
+ return result;
+}
+
+Maybe<uint32_t> Key::GetValueAsString(const nsString& aName,
+ Span<char16_t> aBuffer,
+ StringFlags aFlags) const {
+ MOZ_ASSERT(mKey);
+ MOZ_ASSERT(aBuffer.Length(), "Empty buffer?");
+ MOZ_ASSERT(!(aFlags & StringFlags::ExpandEnvironment),
+ "Environment expansion not performed on a single buffer");
+
+ DWORD size = aBuffer.LengthBytes();
+ DWORD type;
+ HRESULT rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type,
+ (LPBYTE)aBuffer.data(), &size);
+ if (FAILED(rv)) {
+ return Nothing();
+ }
+ if (!IsStringType(type, aFlags)) {
+ return Nothing();
+ }
+ uint32_t len = size ? size / sizeof(char16_t) - 1 : 0;
+ aBuffer[len] = 0;
+ return Some(len);
+}
+
+KeyWatcher::KeyWatcher(Key&& aKey,
+ nsISerialEventTarget* aTargetSerialEventTarget,
+ Callback&& aCallback)
+ : mKey(std::move(aKey)),
+ mEventTarget(aTargetSerialEventTarget),
+ mCallback(std::move(aCallback)) {
+ MOZ_ASSERT(mKey);
+ MOZ_ASSERT(mEventTarget);
+ MOZ_ASSERT(mCallback);
+ mEvent = CreateEvent(nullptr, /* bManualReset = */ FALSE,
+ /* bInitialState = */ FALSE, nullptr);
+ if (NS_WARN_IF(!mEvent)) {
+ return;
+ }
+
+ if (NS_WARN_IF(!Register())) {
+ return;
+ }
+
+ // The callback only dispatches to the relevant event target, so we can use
+ // WT_EXECUTEINWAITTHREAD.
+ RegisterWaitForSingleObject(&mWaitObject, mEvent, WatchCallback, this,
+ INFINITE, WT_EXECUTEINWAITTHREAD);
+}
+
+void KeyWatcher::WatchCallback(void* aContext, BOOLEAN) {
+ auto* watcher = static_cast<KeyWatcher*>(aContext);
+ watcher->Register();
+ watcher->mEventTarget->Dispatch(
+ NS_NewRunnableFunction("KeyWatcher callback", watcher->mCallback));
+}
+
+// As per the documentation in:
+// https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue
+//
+// This function detects a single change. After the caller receives a
+// notification event, it should call the function again to receive the next
+// notification.
+bool KeyWatcher::Register() {
+ MOZ_ASSERT(mEvent);
+ DWORD flags = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES |
+ REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_CHANGE_SECURITY |
+ REG_NOTIFY_THREAD_AGNOSTIC;
+ HRESULT rv =
+ RegNotifyChangeKeyValue(mKey.RawKey(), /* bWatchSubtree = */ TRUE, flags,
+ mEvent, /* fAsynchronous = */ TRUE);
+ return !NS_WARN_IF(FAILED(rv));
+}
+
+KeyWatcher::~KeyWatcher() {
+ if (mWaitObject) {
+ UnregisterWait(mWaitObject);
+ CloseHandle(mWaitObject);
+ }
+ if (mEvent) {
+ CloseHandle(mEvent);
+ }
+}
+
+} // namespace mozilla::widget::WinRegistry
diff --git a/widget/windows/WinRegistry.h b/widget/windows/WinRegistry.h
new file mode 100644
index 0000000000..8ab221928e
--- /dev/null
+++ b/widget/windows/WinRegistry.h
@@ -0,0 +1,235 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_WinRegistry_h__
+#define mozilla_widget_WinRegistry_h__
+
+#include <windows.h>
+#include <functional>
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Span.h"
+
+class nsISerialEventTarget;
+
+namespace mozilla::widget::WinRegistry {
+
+// According to MSDN, the following limits apply (in characters excluding room
+// for terminating null character):
+static constexpr size_t kMaxKeyNameLen = 255;
+static constexpr size_t kMaxValueNameLen = 16383;
+
+/// https://learn.microsoft.com/en-us/windows/win32/shell/regsam
+enum class KeyMode : uint32_t {
+ AllAccess = KEY_ALL_ACCESS,
+ QueryValue = KEY_QUERY_VALUE,
+ CreateLink = KEY_CREATE_LINK,
+ CreateSubKey = KEY_CREATE_SUB_KEY,
+ EnumerateSubkeys = KEY_ENUMERATE_SUB_KEYS,
+ Execute = KEY_EXECUTE,
+ Notify = KEY_NOTIFY,
+ Read = KEY_READ,
+ SetValue = KEY_SET_VALUE,
+ Write = KEY_WRITE,
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(KeyMode);
+
+enum class StringFlags : uint32_t {
+ // Whether to allow REG_SZ strings.
+ Sz = 1 << 0,
+ // Whether to read EXPAND_SZ strings.
+ ExpandSz = 1 << 1,
+ // Whether to treat MULTI_SZ values as strings. This is a historical
+ // idiosyncrasy of the nsIWindowsRegKey, but most likely not what you want.
+ LegacyMultiSz = 1 << 2,
+ // Whether to expand environment variables in EXPAND_SZ values.
+ // Only makes sense along with the ExpandSz variable.
+ ExpandEnvironment = 1 << 3,
+ // By default, only allow regular Sz, and don't perform environment expansion.
+ Default = Sz,
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(StringFlags);
+
+// Convenience alias for legacy WinUtils callers, to preserve behavior.
+// Chances are users of these flags could just look at Sz strings, tho.
+static constexpr auto kLegacyWinUtilsStringFlags =
+ StringFlags::Sz | StringFlags::ExpandSz;
+
+// https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
+enum class ValueType : uint32_t {
+ Binary = REG_BINARY,
+ Dword = REG_DWORD,
+ ExpandSz = REG_EXPAND_SZ,
+ Link = REG_LINK,
+ MultiSz = REG_MULTI_SZ,
+ None = REG_NONE,
+ Qword = REG_QWORD,
+ Sz = REG_SZ,
+};
+
+class Key {
+ public:
+ enum CreateFlag {
+ Create,
+ };
+
+ Key() = default;
+ Key(const Key&) = delete;
+ Key(Key&& aOther) { std::swap(mKey, aOther.mKey); }
+
+ Key& operator=(const Key&) = delete;
+ Key& operator=(Key&& aOther) {
+ std::swap(mKey, aOther.mKey);
+ return *this;
+ }
+
+ Key(HKEY aParent, const nsString& aPath, KeyMode aMode, CreateFlag);
+ Key(HKEY aParent, const nsString& aPath, KeyMode aMode);
+
+ Key(const Key& aParent, const nsString& aPath, KeyMode aMode, CreateFlag)
+ : Key(aParent.mKey, aPath, aMode, Create) {}
+ Key(const Key& aParent, const nsString& aPath, KeyMode aMode)
+ : Key(aParent.mKey, aPath, aMode) {}
+
+ ~Key() {
+ if (mKey) {
+ ::RegCloseKey(mKey);
+ }
+ }
+
+ explicit operator bool() const { return !!mKey; }
+
+ uint32_t GetChildCount() const;
+ [[nodiscard]] bool GetChildName(uint32_t aIndex, nsAString& aResult) const;
+ [[nodiscard]] bool RemoveChildKey(const nsString& aName) const;
+
+ uint32_t GetValueCount() const;
+ [[nodiscard]] bool GetValueName(uint32_t aIndex, nsAString& aResult) const;
+ ValueType GetValueType(const nsString& aName) const;
+ bool RemoveValue(const nsString& aName) const;
+
+ Maybe<uint32_t> GetValueAsDword(const nsString& aName) const;
+ bool WriteValueAsDword(const nsString& aName, uint32_t aValue);
+
+ Maybe<uint64_t> GetValueAsQword(const nsString& aName) const;
+ [[nodiscard]] bool WriteValueAsQword(const nsString& aName, uint64_t aValue);
+
+ [[nodiscard]] bool GetValueAsBinary(const nsString& aName,
+ nsTArray<uint8_t>&) const;
+ Maybe<nsTArray<uint8_t>> GetValueAsBinary(const nsString& aName) const;
+ [[nodiscard]] bool WriteValueAsBinary(const nsString& aName,
+ Span<const uint8_t> aValue);
+
+ [[nodiscard]] bool GetValueAsString(const nsString& aName, nsString& aResult,
+ StringFlags = StringFlags::Default) const;
+ Maybe<nsString> GetValueAsString(const nsString& aName,
+ StringFlags = StringFlags::Default) const;
+ // Reads a string value into a buffer. Returns Some(length) if the string is
+ // read fully, in which case the passed memory region contains a
+ // null-terminated string.
+ // Doesn't perform environment expansion (and asserts if you pass the
+ // ExpandEnvironment flag).
+ [[nodiscard]] Maybe<uint32_t> GetValueAsString(
+ const nsString& aName, Span<char16_t>,
+ StringFlags = StringFlags::Default) const;
+ [[nodiscard]] Maybe<uint32_t> GetValueAsString(
+ const nsString& aName, Span<wchar_t> aBuffer,
+ StringFlags aFlags = StringFlags::Default) const {
+ return GetValueAsString(
+ aName, Span<char16_t>((char16_t*)aBuffer.data(), aBuffer.Length()),
+ aFlags);
+ }
+
+ [[nodiscard]] bool WriteValueAsString(const nsString& aName,
+ const nsString& aValue) {
+ MOZ_ASSERT(mKey);
+ return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_SZ,
+ (const BYTE*)aValue.get(),
+ (aValue.Length() + 1) * sizeof(char16_t)));
+ }
+
+ HKEY RawKey() const { return mKey; }
+
+ private:
+ HKEY mKey = nullptr;
+};
+
+inline bool HasKey(HKEY aRootKey, const nsString& aKeyName) {
+ return !!Key(aRootKey, aKeyName, KeyMode::Read);
+}
+
+// Returns a single string value from the registry into a buffer.
+[[nodiscard]] inline bool GetString(HKEY aRootKey, const nsString& aKeyName,
+ const nsString& aValueName,
+ Span<char16_t> aBuffer,
+ StringFlags aFlags = StringFlags::Default) {
+ Key k(aRootKey, aKeyName, KeyMode::QueryValue);
+ return k && k.GetValueAsString(aValueName, aBuffer, aFlags);
+}
+[[nodiscard]] inline bool GetString(HKEY aRootKey, const nsString& aKeyName,
+ const nsString& aValueName,
+ Span<wchar_t> aBuffer,
+ StringFlags aFlags = StringFlags::Default) {
+ return GetString(aRootKey, aKeyName, aValueName,
+ Span<char16_t>((char16_t*)aBuffer.data(), aBuffer.Length()),
+ aFlags);
+}
+[[nodiscard]] inline bool GetString(HKEY aRootKey, const nsString& aKeyName,
+ const nsString& aValueName,
+ nsString& aBuffer,
+ StringFlags aFlags = StringFlags::Default) {
+ Key k(aRootKey, aKeyName, KeyMode::QueryValue);
+ return k && k.GetValueAsString(aValueName, aBuffer, aFlags);
+}
+inline Maybe<nsString> GetString(HKEY aRootKey, const nsString& aKeyName,
+ const nsString& aValueName,
+ StringFlags aFlags = StringFlags::Default) {
+ Key k(aRootKey, aKeyName, KeyMode::QueryValue);
+ if (!k) {
+ return Nothing();
+ }
+ return k.GetValueAsString(aValueName, aFlags);
+}
+
+class KeyWatcher final {
+ public:
+ using Callback = std::function<void()>;
+
+ KeyWatcher(const KeyWatcher&) = delete;
+
+ const Key& GetKey() const { return mKey; }
+
+ // Start watching a key. The watching is recursive (the whole key subtree is
+ // watched), and the callback is executed every time the key or any of its
+ // descendants change until the watcher is destroyed.
+ //
+ // @param aKey the key to watch. Must have been opened with the
+ // KeyMode::Notify flag.
+ // @param aTargetSerialEventTarget the target event target to dispatch the
+ // callback to.
+ // @param aCallback the closure to run every time that registry key changes.
+ KeyWatcher(Key&& aKey, nsISerialEventTarget* aTargetSerialEventTarget,
+ Callback&& aCallback);
+
+ ~KeyWatcher();
+
+ private:
+ static void CALLBACK WatchCallback(void* aContext, BOOLEAN);
+ bool Register();
+
+ Key mKey;
+ nsCOMPtr<nsISerialEventTarget> mEventTarget;
+ Callback mCallback;
+ HANDLE mEvent = nullptr;
+ HANDLE mWaitObject = nullptr;
+};
+
+} // namespace mozilla::widget::WinRegistry
+
+#endif
diff --git a/widget/windows/WinTaskbar.cpp b/widget/windows/WinTaskbar.cpp
new file mode 100644
index 0000000000..56608503da
--- /dev/null
+++ b/widget/windows/WinTaskbar.cpp
@@ -0,0 +1,493 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "nsIWinTaskbar.h"
+#include "WinTaskbar.h"
+#include "TaskbarPreview.h"
+#include "nsITaskbarPreviewController.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/widget/JumpListBuilder.h"
+#include <nsError.h>
+#include <nsCOMPtr.h>
+#include <nsIWidget.h>
+#include <nsIBaseWindow.h>
+#include <nsServiceManagerUtils.h>
+#include "nsIXULAppInfo.h"
+#include "nsILegacyJumpListBuilder.h"
+#include "nsUXThemeData.h"
+#include "nsWindow.h"
+#include "WinUtils.h"
+#include "TaskbarTabPreview.h"
+#include "TaskbarWindowPreview.h"
+#include "LegacyJumpListBuilder.h"
+#include "nsWidgetsCID.h"
+#include "nsPIDOMWindow.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "mozilla/Preferences.h"
+#include "nsAppRunner.h"
+#include "nsXREDirProvider.h"
+#include "mozilla/widget/WinRegistry.h"
+#include <io.h>
+#include <propvarutil.h>
+#include <propkey.h>
+#include <shellapi.h>
+
+static NS_DEFINE_CID(kLegacyJumpListBuilderCID,
+ NS_WIN_LEGACYJUMPLISTBUILDER_CID);
+
+namespace {
+
+HWND GetHWNDFromDocShell(nsIDocShell* aShell) {
+ nsCOMPtr<nsIBaseWindow> baseWindow(
+ do_QueryInterface(reinterpret_cast<nsISupports*>(aShell)));
+
+ if (!baseWindow) return nullptr;
+
+ nsCOMPtr<nsIWidget> widget;
+ baseWindow->GetMainWidget(getter_AddRefs(widget));
+
+ return widget ? (HWND)widget->GetNativeData(NS_NATIVE_WINDOW) : nullptr;
+}
+
+HWND GetHWNDFromDOMWindow(mozIDOMWindow* dw) {
+ nsCOMPtr<nsIWidget> widget;
+
+ if (!dw) return nullptr;
+
+ nsCOMPtr<nsPIDOMWindowInner> window = nsPIDOMWindowInner::From(dw);
+ return GetHWNDFromDocShell(window->GetDocShell());
+}
+
+nsresult SetWindowAppUserModelProp(mozIDOMWindow* aParent,
+ const nsString& aIdentifier) {
+ NS_ENSURE_ARG_POINTER(aParent);
+
+ if (aIdentifier.IsEmpty()) return NS_ERROR_INVALID_ARG;
+
+ HWND toplevelHWND = ::GetAncestor(GetHWNDFromDOMWindow(aParent), GA_ROOT);
+
+ if (!toplevelHWND) return NS_ERROR_INVALID_ARG;
+
+ RefPtr<IPropertyStore> pPropStore;
+ if (FAILED(SHGetPropertyStoreForWindow(toplevelHWND, IID_IPropertyStore,
+ getter_AddRefs(pPropStore)))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ PROPVARIANT pv;
+ if (FAILED(InitPropVariantFromString(aIdentifier.get(), &pv))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult rv = NS_OK;
+ if (FAILED(pPropStore->SetValue(PKEY_AppUserModel_ID, pv)) ||
+ FAILED(pPropStore->Commit())) {
+ rv = NS_ERROR_FAILURE;
+ }
+
+ PropVariantClear(&pv);
+
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// default nsITaskbarPreviewController
+
+class DefaultController final : public nsITaskbarPreviewController {
+ ~DefaultController() {}
+ HWND mWnd;
+
+ public:
+ explicit DefaultController(HWND hWnd) : mWnd(hWnd) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITASKBARPREVIEWCONTROLLER
+};
+
+NS_IMETHODIMP
+DefaultController::GetWidth(uint32_t* aWidth) {
+ RECT r;
+ ::GetClientRect(mWnd, &r);
+ *aWidth = r.right;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultController::GetHeight(uint32_t* aHeight) {
+ RECT r;
+ ::GetClientRect(mWnd, &r);
+ *aHeight = r.bottom;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultController::GetThumbnailAspectRatio(float* aThumbnailAspectRatio) {
+ uint32_t width, height;
+ GetWidth(&width);
+ GetHeight(&height);
+ if (!height) height = 1;
+
+ *aThumbnailAspectRatio = width / float(height);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultController::RequestThumbnail(nsITaskbarPreviewCallback* aCallback,
+ uint32_t width, uint32_t height) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultController::RequestPreview(nsITaskbarPreviewCallback* aCallback) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultController::OnClose(void) {
+ MOZ_ASSERT_UNREACHABLE(
+ "OnClose should not be called for "
+ "TaskbarWindowPreviews");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultController::OnActivate(bool* rAcceptActivation) {
+ *rAcceptActivation = true;
+ MOZ_ASSERT_UNREACHABLE(
+ "OnActivate should not be called for "
+ "TaskbarWindowPreviews");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultController::OnClick(nsITaskbarPreviewButton* button) { return NS_OK; }
+
+NS_IMPL_ISUPPORTS(DefaultController, nsITaskbarPreviewController)
+} // namespace
+
+namespace mozilla {
+namespace widget {
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIWinTaskbar
+
+NS_IMPL_ISUPPORTS(WinTaskbar, nsIWinTaskbar)
+
+bool WinTaskbar::Initialize() {
+ if (mTaskbar) return true;
+
+ ::CoInitialize(nullptr);
+ HRESULT hr =
+ ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITaskbarList4, (void**)&mTaskbar);
+ if (FAILED(hr)) return false;
+
+ hr = mTaskbar->HrInit();
+ if (FAILED(hr)) {
+ // This may fail with shell extensions like blackbox installed.
+ NS_WARNING("Unable to initialize taskbar");
+ NS_RELEASE(mTaskbar);
+ return false;
+ }
+ return true;
+}
+
+WinTaskbar::WinTaskbar() : mTaskbar(nullptr) {}
+
+WinTaskbar::~WinTaskbar() {
+ if (mTaskbar) { // match successful Initialize() call
+ NS_RELEASE(mTaskbar);
+ ::CoUninitialize();
+ }
+}
+
+// static
+bool WinTaskbar::GenerateAppUserModelID(nsAString& aAppUserModelId,
+ bool aPrivateBrowsing) {
+ // If marked as such in prefs, use a hash of the profile path for the id
+ // instead of the install path hash setup by the installer.
+ if (Preferences::GetBool("taskbar.grouping.useprofile", false)) {
+ nsCOMPtr<nsIFile> profileDir;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ bool exists = false;
+ if (profileDir && NS_SUCCEEDED(profileDir->Exists(&exists)) && exists) {
+ nsAutoCString path;
+ if (NS_SUCCEEDED(profileDir->GetPersistentDescriptor(path))) {
+ nsAutoString id;
+ id.AppendInt(HashString(path));
+ if (!id.IsEmpty()) {
+ aAppUserModelId.Assign(id);
+ return true;
+ }
+ }
+ }
+ }
+
+ // The default value is set by the installer and is stored in the registry
+ // under (HKLM||HKCU)/Software/Mozilla/Firefox/TaskBarIDs. If for any reason
+ // hash generation operation fails, the installer will not store a value in
+ // the registry or set ids on shortcuts. A lack of an id can also occur for
+ // zipped builds.
+ nsCOMPtr<nsIXULAppInfo> appInfo =
+ do_GetService("@mozilla.org/xre/app-info;1");
+ nsCString appName;
+ if (appInfo && NS_SUCCEEDED(appInfo->GetName(appName))) {
+ nsAutoString regKey;
+ regKey.AssignLiteral("Software\\Mozilla\\");
+ AppendASCIItoUTF16(appName, regKey);
+ regKey.AppendLiteral("\\TaskBarIDs");
+
+ WCHAR path[MAX_PATH];
+ if (GetModuleFileNameW(nullptr, path, MAX_PATH)) {
+ wchar_t* slash = wcsrchr(path, '\\');
+ if (!slash) return false;
+ *slash = '\0'; // no trailing slash
+
+ nsDependentString pathStr(path);
+ for (auto* rootKey : {HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER}) {
+ if (auto aumid = WinRegistry::GetString(rootKey, regKey, pathStr)) {
+ aAppUserModelId = std::move(*aumid);
+ break;
+ }
+ }
+ }
+ }
+
+ // If we haven't found an ID yet then use the install hash. In xpcshell tests
+ // the directory provider may not have been initialized so bypass in this
+ // case.
+ if (aAppUserModelId.IsEmpty() && gDirServiceProvider) {
+ gDirServiceProvider->GetInstallHash(aAppUserModelId);
+ }
+
+ if (aPrivateBrowsing) {
+ aAppUserModelId.AppendLiteral(";PrivateBrowsingAUMID");
+ }
+
+ return !aAppUserModelId.IsEmpty();
+}
+
+// static
+bool WinTaskbar::GetAppUserModelID(nsAString& aAppUserModelId,
+ bool aPrivateBrowsing) {
+ // If an ID has already been set then use that.
+ PWSTR id;
+ if (SUCCEEDED(GetCurrentProcessExplicitAppUserModelID(&id))) {
+ aAppUserModelId.Assign(id);
+ CoTaskMemFree(id);
+ }
+
+ return GenerateAppUserModelID(aAppUserModelId, aPrivateBrowsing);
+}
+
+NS_IMETHODIMP
+WinTaskbar::GetDefaultGroupId(nsAString& aDefaultGroupId) {
+ if (!GetAppUserModelID(aDefaultGroupId)) return NS_ERROR_UNEXPECTED;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WinTaskbar::GetDefaultPrivateGroupId(nsAString& aDefaultPrivateGroupId) {
+ if (!GetAppUserModelID(aDefaultPrivateGroupId, true))
+ return NS_ERROR_UNEXPECTED;
+
+ return NS_OK;
+}
+
+// (static) Called from AppShell
+bool WinTaskbar::RegisterAppUserModelID() {
+ nsAutoString uid;
+ if (!GetAppUserModelID(uid)) return false;
+
+ return SUCCEEDED(SetCurrentProcessExplicitAppUserModelID(uid.get()));
+}
+
+NS_IMETHODIMP
+WinTaskbar::GetAvailable(bool* aAvailable) {
+ // ITaskbarList4::HrInit() may fail with shell extensions like blackbox
+ // installed. Initialize early to return available=false in those cases.
+ *aAvailable = Initialize();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WinTaskbar::CreateTaskbarTabPreview(nsIDocShell* shell,
+ nsITaskbarPreviewController* controller,
+ nsITaskbarTabPreview** _retval) {
+ if (!Initialize()) return NS_ERROR_NOT_AVAILABLE;
+
+ NS_ENSURE_ARG_POINTER(shell);
+ NS_ENSURE_ARG_POINTER(controller);
+
+ HWND toplevelHWND = ::GetAncestor(GetHWNDFromDocShell(shell), GA_ROOT);
+
+ if (!toplevelHWND) return NS_ERROR_INVALID_ARG;
+
+ RefPtr<TaskbarTabPreview> preview(
+ new TaskbarTabPreview(mTaskbar, controller, toplevelHWND, shell));
+ if (!preview) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv = preview->Init();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ preview.forget(_retval);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WinTaskbar::GetTaskbarWindowPreview(nsIDocShell* shell,
+ nsITaskbarWindowPreview** _retval) {
+ if (!Initialize()) return NS_ERROR_NOT_AVAILABLE;
+
+ NS_ENSURE_ARG_POINTER(shell);
+
+ HWND toplevelHWND = ::GetAncestor(GetHWNDFromDocShell(shell), GA_ROOT);
+
+ if (!toplevelHWND) return NS_ERROR_INVALID_ARG;
+
+ nsWindow* window = WinUtils::GetNSWindowPtr(toplevelHWND);
+
+ if (!window) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsITaskbarWindowPreview> preview = window->GetTaskbarPreview();
+ if (!preview) {
+ RefPtr<DefaultController> defaultController =
+ new DefaultController(toplevelHWND);
+
+ TaskbarWindowPreview* previewRaw = new TaskbarWindowPreview(
+ mTaskbar, defaultController, toplevelHWND, shell);
+ if (!previewRaw) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ preview = previewRaw;
+
+ nsresult rv = previewRaw->Init();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ window->SetTaskbarPreview(preview);
+ }
+
+ preview.forget(_retval);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WinTaskbar::GetTaskbarProgress(nsIDocShell* shell,
+ nsITaskbarProgress** _retval) {
+ nsCOMPtr<nsITaskbarWindowPreview> preview;
+ nsresult rv = GetTaskbarWindowPreview(shell, getter_AddRefs(preview));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(preview, _retval);
+}
+
+NS_IMETHODIMP
+WinTaskbar::GetOverlayIconController(
+ nsIDocShell* shell, nsITaskbarOverlayIconController** _retval) {
+ nsCOMPtr<nsITaskbarWindowPreview> preview;
+ nsresult rv = GetTaskbarWindowPreview(shell, getter_AddRefs(preview));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(preview, _retval);
+}
+
+NS_IMETHODIMP
+WinTaskbar::CreateLegacyJumpListBuilder(
+ bool aPrivateBrowsing, nsILegacyJumpListBuilder** aJumpListBuilder) {
+ nsresult rv;
+
+ if (LegacyJumpListBuilder::sBuildingList) return NS_ERROR_ALREADY_INITIALIZED;
+
+ nsCOMPtr<nsILegacyJumpListBuilder> builder =
+ do_CreateInstance(kLegacyJumpListBuilderCID, &rv);
+ if (NS_FAILED(rv)) return NS_ERROR_UNEXPECTED;
+
+ NS_IF_ADDREF(*aJumpListBuilder = builder);
+
+ nsAutoString aumid;
+ GenerateAppUserModelID(aumid, aPrivateBrowsing);
+ builder->SetAppUserModelID(aumid);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WinTaskbar::CreateJumpListBuilder(bool aPrivateBrowsing,
+ nsIJumpListBuilder** aJumpListBuilder) {
+ nsAutoString aumid;
+ GenerateAppUserModelID(aumid, aPrivateBrowsing);
+
+ nsCOMPtr<nsIJumpListBuilder> builder = new JumpListBuilder(aumid);
+ if (!builder) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ NS_IF_ADDREF(*aJumpListBuilder = builder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WinTaskbar::GetGroupIdForWindow(mozIDOMWindow* aParent,
+ nsAString& aIdentifier) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ HWND toplevelHWND = ::GetAncestor(GetHWNDFromDOMWindow(aParent), GA_ROOT);
+ if (!toplevelHWND) return NS_ERROR_INVALID_ARG;
+ RefPtr<IPropertyStore> pPropStore;
+ if (FAILED(SHGetPropertyStoreForWindow(toplevelHWND, IID_IPropertyStore,
+ getter_AddRefs(pPropStore)))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ PROPVARIANT pv;
+ PropVariantInit(&pv);
+ auto cleanupPropVariant = MakeScopeExit([&] { PropVariantClear(&pv); });
+ if (FAILED(pPropStore->GetValue(PKEY_AppUserModel_ID, &pv))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (pv.vt != VT_LPWSTR) {
+ // This can happen when there is no window specific group ID set
+ // It's not an error case so we have to check for empty strings
+ // returned from the function.
+ return NS_OK;
+ }
+ aIdentifier.Assign(char16ptr_t(pv.pwszVal));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WinTaskbar::SetGroupIdForWindow(mozIDOMWindow* aParent,
+ const nsAString& aIdentifier) {
+ return SetWindowAppUserModelProp(aParent, nsString(aIdentifier));
+}
+
+NS_IMETHODIMP
+WinTaskbar::PrepareFullScreen(void* aHWND, bool aFullScreen) {
+ if (!Initialize()) return NS_ERROR_NOT_AVAILABLE;
+
+ NS_ENSURE_ARG_POINTER(aHWND);
+
+ if (!::IsWindow((HWND)aHWND)) return NS_ERROR_INVALID_ARG;
+
+ HRESULT hr = mTaskbar->MarkFullscreenWindow((HWND)aHWND, aFullScreen);
+ if (FAILED(hr)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WinTaskbar.h b/widget/windows/WinTaskbar.h
new file mode 100644
index 0000000000..a6c483a561
--- /dev/null
+++ b/widget/windows/WinTaskbar.h
@@ -0,0 +1,46 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef __WinTaskbar_h__
+#define __WinTaskbar_h__
+
+#include <windows.h>
+#include <shobjidl.h>
+#undef LogSeverity // SetupAPI.h #defines this as DWORD
+#include "nsIWinTaskbar.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+namespace widget {
+
+class WinTaskbar final : public nsIWinTaskbar {
+ ~WinTaskbar();
+
+ public:
+ WinTaskbar();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIWINTASKBAR
+
+ static bool GenerateAppUserModelID(nsAString& aAppUserModelId,
+ bool aPrivateBrowsing = false);
+ // Registers the global app user model id for the instance.
+ // See comments in WinTaskbar.cpp for more information.
+ static bool RegisterAppUserModelID();
+ static bool GetAppUserModelID(nsAString& aDefaultGroupId,
+ bool aPrivateBrowsing = false);
+
+ private:
+ bool Initialize();
+
+ ITaskbarList4* mTaskbar;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* __WinTaskbar_h__ */
diff --git a/widget/windows/WinTextEventDispatcherListener.cpp b/widget/windows/WinTextEventDispatcherListener.cpp
new file mode 100644
index 0000000000..aeed8be01b
--- /dev/null
+++ b/widget/windows/WinTextEventDispatcherListener.cpp
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 4; 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 "KeyboardLayout.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/widget/IMEData.h"
+#include "nsWindow.h"
+#include "WinIMEHandler.h"
+#include "WinTextEventDispatcherListener.h"
+
+namespace mozilla {
+namespace widget {
+
+StaticRefPtr<WinTextEventDispatcherListener>
+ WinTextEventDispatcherListener::sInstance;
+
+// static
+WinTextEventDispatcherListener* WinTextEventDispatcherListener::GetInstance() {
+ if (!sInstance) {
+ sInstance = new WinTextEventDispatcherListener();
+ }
+ return sInstance.get();
+}
+
+void WinTextEventDispatcherListener::Shutdown() { sInstance = nullptr; }
+
+NS_IMPL_ISUPPORTS(WinTextEventDispatcherListener, TextEventDispatcherListener,
+ nsISupportsWeakReference)
+
+WinTextEventDispatcherListener::WinTextEventDispatcherListener() {}
+
+WinTextEventDispatcherListener::~WinTextEventDispatcherListener() {}
+
+NS_IMETHODIMP
+WinTextEventDispatcherListener::NotifyIME(
+ TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) {
+ nsWindow* window = static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
+ if (NS_WARN_IF(!window)) {
+ return NS_ERROR_FAILURE;
+ }
+ return IMEHandler::NotifyIME(window, aNotification);
+}
+
+NS_IMETHODIMP_(IMENotificationRequests)
+WinTextEventDispatcherListener::GetIMENotificationRequests() {
+ return IMEHandler::GetIMENotificationRequests();
+}
+
+NS_IMETHODIMP_(void)
+WinTextEventDispatcherListener::OnRemovedFrom(
+ TextEventDispatcher* aTextEventDispatcher) {
+ // XXX When input transaction is being stolen by add-on, what should we do?
+}
+
+NS_IMETHODIMP_(void)
+WinTextEventDispatcherListener::WillDispatchKeyboardEvent(
+ TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
+ void* aData) {
+ static_cast<NativeKey*>(aData)->WillDispatchKeyboardEvent(aKeyboardEvent,
+ aIndexOfKeypress);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WinTextEventDispatcherListener.h b/widget/windows/WinTextEventDispatcherListener.h
new file mode 100644
index 0000000000..d799d0b280
--- /dev/null
+++ b/widget/windows/WinTextEventDispatcherListener.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef WinTextEventDispatcherListener_h_
+#define WinTextEventDispatcherListener_h_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TextEventDispatcherListener.h"
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * On Windows, it's enough TextEventDispatcherListener to be a singleton
+ * because we have only one input context per process (IMM can create
+ * multiple IM context but we don't support such behavior).
+ */
+
+class WinTextEventDispatcherListener final
+ : public TextEventDispatcherListener {
+ public:
+ static WinTextEventDispatcherListener* GetInstance();
+ static void Shutdown();
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification) override;
+ NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
+ NS_IMETHOD_(void)
+ OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;
+ NS_IMETHOD_(void)
+ WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress, void* aData) override;
+
+ private:
+ WinTextEventDispatcherListener();
+ virtual ~WinTextEventDispatcherListener();
+
+ static StaticRefPtr<WinTextEventDispatcherListener> sInstance;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef WinTextEventDispatcherListener_h_
diff --git a/widget/windows/WinUtils.cpp b/widget/windows/WinUtils.cpp
new file mode 100644
index 0000000000..1d8fad16f7
--- /dev/null
+++ b/widget/windows/WinUtils.cpp
@@ -0,0 +1,2107 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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 "WinUtils.h"
+
+#include <knownfolders.h>
+#include <winioctl.h>
+
+#include "gfxPlatform.h"
+#include "gfxUtils.h"
+#include "nsWindow.h"
+#include "nsWindowDefs.h"
+#include "InputDeviceUtils.h"
+#include "KeyboardLayout.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/DisplayConfigWindows.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerThreadSleep.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "mozilla/Unused.h"
+#include "nsIContentPolicy.h"
+#include "WindowsUIUtils.h"
+#include "nsContentUtils.h"
+
+#include "mozilla/Logging.h"
+
+#include "nsString.h"
+#include "nsDirectoryServiceUtils.h"
+#include "imgIContainer.h"
+#include "imgITools.h"
+#include "nsNetUtil.h"
+#include "nsIOutputStream.h"
+#include "nsNetCID.h"
+#include "prtime.h"
+#ifdef MOZ_PLACES
+# include "nsIFaviconService.h"
+#endif
+#include "nsIDownloader.h"
+#include "nsIChannel.h"
+#include "nsIThread.h"
+#include "MainThreadUtils.h"
+#include "nsLookAndFeel.h"
+#include "nsUnicharUtils.h"
+#include "nsWindowsHelpers.h"
+#include "WinWindowOcclusionTracker.h"
+
+#include <textstor.h>
+#include "TSFTextStore.h"
+
+#include <shellscalingapi.h>
+#include <shlobj.h>
+#include <shlwapi.h>
+
+mozilla::LazyLogModule gWindowsLog("Widget");
+
+#define LOG_E(...) MOZ_LOG(gWindowsLog, LogLevel::Error, (__VA_ARGS__))
+#define LOG_D(...) MOZ_LOG(gWindowsLog, LogLevel::Debug, (__VA_ARGS__))
+
+using namespace mozilla::gfx;
+
+namespace mozilla {
+namespace widget {
+
+#ifdef MOZ_PLACES
+NS_IMPL_ISUPPORTS(myDownloadObserver, nsIDownloadObserver)
+NS_IMPL_ISUPPORTS(AsyncFaviconDataReady, nsIFaviconDataCallback)
+#endif
+NS_IMPL_ISUPPORTS(AsyncEncodeAndWriteIcon, nsIRunnable)
+NS_IMPL_ISUPPORTS(AsyncDeleteAllFaviconsFromDisk, nsIRunnable)
+
+const char FaviconHelper::kJumpListCacheDir[] = "jumpListCache";
+const char FaviconHelper::kShortcutCacheDir[] = "shortcutCache";
+
+struct CoTaskMemFreePolicy {
+ void operator()(void* aPtr) { ::CoTaskMemFree(aPtr); }
+};
+
+SetThreadDpiAwarenessContextProc WinUtils::sSetThreadDpiAwarenessContext = NULL;
+EnableNonClientDpiScalingProc WinUtils::sEnableNonClientDpiScaling = NULL;
+GetSystemMetricsForDpiProc WinUtils::sGetSystemMetricsForDpi = NULL;
+bool WinUtils::sHasPackageIdentity = false;
+
+using GetDpiForWindowProc = UINT(WINAPI*)(HWND);
+static GetDpiForWindowProc sGetDpiForWindow = NULL;
+
+/* static */
+void WinUtils::Initialize() {
+ // Dpi-Awareness is not supported with Win32k Lockdown enabled, so we don't
+ // initialize DPI-related members and assert later that nothing accidently
+ // uses these static members
+ if (!IsWin32kLockedDown()) {
+ HMODULE user32Dll = ::GetModuleHandleW(L"user32");
+ if (user32Dll) {
+ auto getThreadDpiAwarenessContext =
+ (decltype(GetThreadDpiAwarenessContext)*)::GetProcAddress(
+ user32Dll, "GetThreadDpiAwarenessContext");
+ auto areDpiAwarenessContextsEqual =
+ (decltype(AreDpiAwarenessContextsEqual)*)::GetProcAddress(
+ user32Dll, "AreDpiAwarenessContextsEqual");
+ if (getThreadDpiAwarenessContext && areDpiAwarenessContextsEqual &&
+ areDpiAwarenessContextsEqual(
+ getThreadDpiAwarenessContext(),
+ DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) {
+ // Only per-monitor v1 requires these workarounds.
+ sEnableNonClientDpiScaling =
+ (EnableNonClientDpiScalingProc)::GetProcAddress(
+ user32Dll, "EnableNonClientDpiScaling");
+ sSetThreadDpiAwarenessContext =
+ (SetThreadDpiAwarenessContextProc)::GetProcAddress(
+ user32Dll, "SetThreadDpiAwarenessContext");
+ }
+
+ sGetSystemMetricsForDpi = (GetSystemMetricsForDpiProc)::GetProcAddress(
+ user32Dll, "GetSystemMetricsForDpi");
+ sGetDpiForWindow =
+ (GetDpiForWindowProc)::GetProcAddress(user32Dll, "GetDpiForWindow");
+ }
+ }
+
+ sHasPackageIdentity = mozilla::HasPackageIdentity();
+}
+
+// static
+LRESULT WINAPI WinUtils::NonClientDpiScalingDefWindowProcW(HWND hWnd, UINT msg,
+ WPARAM wParam,
+ LPARAM lParam) {
+ MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown());
+
+ // NOTE: this function was copied out into the body of the pre-XUL skeleton
+ // UI window proc (PreXULSkeletonUI.cpp). If this function changes at any
+ // point, we should probably factor this out and use it from both locations.
+ if (msg == WM_NCCREATE && sEnableNonClientDpiScaling) {
+ sEnableNonClientDpiScaling(hWnd);
+ }
+ return ::DefWindowProcW(hWnd, msg, wParam, lParam);
+}
+
+// static
+void WinUtils::LogW(const wchar_t* fmt, ...) {
+ va_list args = nullptr;
+ if (!lstrlenW(fmt)) {
+ return;
+ }
+ va_start(args, fmt);
+ int buflen = _vscwprintf(fmt, args);
+ wchar_t* buffer = new wchar_t[buflen + 1];
+ if (!buffer) {
+ va_end(args);
+ return;
+ }
+ vswprintf(buffer, buflen, fmt, args);
+ va_end(args);
+
+ // MSVC, including remote debug sessions
+ OutputDebugStringW(buffer);
+ OutputDebugStringW(L"\n");
+
+ int len =
+ WideCharToMultiByte(CP_ACP, 0, buffer, -1, nullptr, 0, nullptr, nullptr);
+ if (len) {
+ char* utf8 = new char[len];
+ if (WideCharToMultiByte(CP_ACP, 0, buffer, -1, utf8, len, nullptr,
+ nullptr) > 0) {
+ // desktop console
+ printf("%s\n", utf8);
+ NS_ASSERTION(gWindowsLog,
+ "Called WinUtils Log() but Widget "
+ "log module doesn't exist!");
+ MOZ_LOG(gWindowsLog, LogLevel::Error, ("%s", utf8));
+ }
+ delete[] utf8;
+ }
+ delete[] buffer;
+}
+
+// static
+void WinUtils::Log(const char* fmt, ...) {
+ va_list args = nullptr;
+ if (!strlen(fmt)) {
+ return;
+ }
+ va_start(args, fmt);
+ int buflen = _vscprintf(fmt, args);
+ char* buffer = new char[buflen + 1];
+ if (!buffer) {
+ va_end(args);
+ return;
+ }
+ vsprintf(buffer, fmt, args);
+ va_end(args);
+
+ // MSVC, including remote debug sessions
+ OutputDebugStringA(buffer);
+ OutputDebugStringW(L"\n");
+
+ // desktop console
+ printf("%s\n", buffer);
+
+ NS_ASSERTION(gWindowsLog,
+ "Called WinUtils Log() but Widget "
+ "log module doesn't exist!");
+ MOZ_LOG(gWindowsLog, LogLevel::Error, ("%s", buffer));
+ delete[] buffer;
+}
+
+// static
+float WinUtils::SystemDPI() {
+ // The result of GetDeviceCaps won't change dynamically, as it predates
+ // per-monitor DPI and support for on-the-fly resolution changes.
+ // Therefore, we only need to look it up once.
+ static float dpi = 0;
+ if (dpi <= 0) {
+ HDC screenDC = GetDC(nullptr);
+ dpi = GetDeviceCaps(screenDC, LOGPIXELSY);
+ ReleaseDC(nullptr, screenDC);
+ }
+
+ // Bug 1012487 - dpi can be 0 when the Screen DC is used off the
+ // main thread on windows. For now just assume a 100% DPI for this
+ // drawing call.
+ // XXX - fixme!
+ return dpi > 0 ? dpi : 96;
+}
+
+// static
+double WinUtils::SystemScaleFactor() { return SystemDPI() / 96.0; }
+
+typedef HRESULT(WINAPI* GETDPIFORMONITORPROC)(HMONITOR, MONITOR_DPI_TYPE, UINT*,
+ UINT*);
+
+typedef HRESULT(WINAPI* GETPROCESSDPIAWARENESSPROC)(HANDLE,
+ PROCESS_DPI_AWARENESS*);
+
+GETDPIFORMONITORPROC sGetDpiForMonitor;
+GETPROCESSDPIAWARENESSPROC sGetProcessDpiAwareness;
+
+static bool SlowIsPerMonitorDPIAware() {
+ // Intentionally leak the handle.
+ HMODULE shcore = LoadLibraryEx(L"shcore", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
+ if (shcore) {
+ sGetDpiForMonitor =
+ (GETDPIFORMONITORPROC)GetProcAddress(shcore, "GetDpiForMonitor");
+ sGetProcessDpiAwareness = (GETPROCESSDPIAWARENESSPROC)GetProcAddress(
+ shcore, "GetProcessDpiAwareness");
+ }
+ PROCESS_DPI_AWARENESS dpiAwareness;
+ return sGetDpiForMonitor && sGetProcessDpiAwareness &&
+ SUCCEEDED(
+ sGetProcessDpiAwareness(GetCurrentProcess(), &dpiAwareness)) &&
+ dpiAwareness == PROCESS_PER_MONITOR_DPI_AWARE;
+}
+
+/* static */
+bool WinUtils::IsPerMonitorDPIAware() {
+ static bool perMonitorDPIAware = SlowIsPerMonitorDPIAware();
+ return perMonitorDPIAware;
+}
+
+/* static */
+float WinUtils::MonitorDPI(HMONITOR aMonitor) {
+ if (IsPerMonitorDPIAware()) {
+ UINT dpiX, dpiY = 96;
+ sGetDpiForMonitor(aMonitor ? aMonitor : GetPrimaryMonitor(),
+ MDT_EFFECTIVE_DPI, &dpiX, &dpiY);
+ return dpiY;
+ }
+
+ // We're not per-monitor aware, use system DPI instead.
+ return SystemDPI();
+}
+
+/* static */
+double WinUtils::LogToPhysFactor(HMONITOR aMonitor) {
+ return MonitorDPI(aMonitor) / 96.0;
+}
+
+/* static */
+int32_t WinUtils::LogToPhys(HMONITOR aMonitor, double aValue) {
+ return int32_t(NS_round(aValue * LogToPhysFactor(aMonitor)));
+}
+
+/* static */
+double WinUtils::LogToPhysFactor(HWND aWnd) {
+ // if there's an ancestor window, we want to share its DPI setting
+ HWND ancestor = ::GetAncestor(aWnd, GA_ROOTOWNER);
+
+ // The GetDpiForWindow api is not available everywhere where we run as
+ // per-monitor, but if it is available rely on it to tell us the scale
+ // factor of the window. See bug 1722085.
+ if (sGetDpiForWindow) {
+ UINT dpi = sGetDpiForWindow(ancestor ? ancestor : aWnd);
+ if (dpi > 0) {
+ return static_cast<double>(dpi) / 96.0;
+ }
+ }
+ return LogToPhysFactor(::MonitorFromWindow(ancestor ? ancestor : aWnd,
+ MONITOR_DEFAULTTOPRIMARY));
+}
+
+/* static */
+HMONITOR
+WinUtils::GetPrimaryMonitor() {
+ const POINT pt = {0, 0};
+ return ::MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
+}
+
+/* static */
+HMONITOR
+WinUtils::MonitorFromRect(const gfx::Rect& rect) {
+ // convert coordinates from desktop to device pixels for MonitorFromRect
+ double dpiScale =
+ IsPerMonitorDPIAware() ? 1.0 : LogToPhysFactor(GetPrimaryMonitor());
+
+ RECT globalWindowBounds = {NSToIntRound(dpiScale * rect.X()),
+ NSToIntRound(dpiScale * rect.Y()),
+ NSToIntRound(dpiScale * (rect.XMost())),
+ NSToIntRound(dpiScale * (rect.YMost()))};
+
+ return ::MonitorFromRect(&globalWindowBounds, MONITOR_DEFAULTTONEAREST);
+}
+
+/* static */
+bool WinUtils::HasSystemMetricsForDpi() {
+ MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown());
+ return (sGetSystemMetricsForDpi != NULL);
+}
+
+/* static */
+int WinUtils::GetSystemMetricsForDpi(int nIndex, UINT dpi) {
+ MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown());
+ if (HasSystemMetricsForDpi()) {
+ return sGetSystemMetricsForDpi(nIndex, dpi);
+ } else {
+ double scale = IsPerMonitorDPIAware() ? dpi / SystemDPI() : 1.0;
+ return NSToIntRound(::GetSystemMetrics(nIndex) * scale);
+ }
+}
+
+/* static */
+gfx::MarginDouble WinUtils::GetUnwriteableMarginsForDeviceInInches(HDC aHdc) {
+ if (!aHdc) {
+ return gfx::MarginDouble();
+ }
+
+ int pixelsPerInchY = ::GetDeviceCaps(aHdc, LOGPIXELSY);
+ int marginTop = ::GetDeviceCaps(aHdc, PHYSICALOFFSETY);
+ int printableAreaHeight = ::GetDeviceCaps(aHdc, VERTRES);
+ int physicalHeight = ::GetDeviceCaps(aHdc, PHYSICALHEIGHT);
+
+ double marginTopInch = double(marginTop) / pixelsPerInchY;
+
+ double printableAreaHeightInch = double(printableAreaHeight) / pixelsPerInchY;
+ double physicalHeightInch = double(physicalHeight) / pixelsPerInchY;
+ double marginBottomInch =
+ physicalHeightInch - printableAreaHeightInch - marginTopInch;
+
+ int pixelsPerInchX = ::GetDeviceCaps(aHdc, LOGPIXELSX);
+ int marginLeft = ::GetDeviceCaps(aHdc, PHYSICALOFFSETX);
+ int printableAreaWidth = ::GetDeviceCaps(aHdc, HORZRES);
+ int physicalWidth = ::GetDeviceCaps(aHdc, PHYSICALWIDTH);
+
+ double marginLeftInch = double(marginLeft) / pixelsPerInchX;
+
+ double printableAreaWidthInch = double(printableAreaWidth) / pixelsPerInchX;
+ double physicalWidthInch = double(physicalWidth) / pixelsPerInchX;
+ double marginRightInch =
+ physicalWidthInch - printableAreaWidthInch - marginLeftInch;
+
+ return gfx::MarginDouble(marginTopInch, marginRightInch, marginBottomInch,
+ marginLeftInch);
+}
+
+#ifdef ACCESSIBILITY
+/* static */
+a11y::LocalAccessible* WinUtils::GetRootAccessibleForHWND(HWND aHwnd) {
+ nsWindow* window = GetNSWindowPtr(aHwnd);
+ if (!window) {
+ return nullptr;
+ }
+
+ return window->GetAccessible();
+}
+#endif // ACCESSIBILITY
+
+/* static */
+bool WinUtils::PeekMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage,
+ UINT aLastMessage, UINT aOption) {
+ RefPtr<ITfMessagePump> msgPump = TSFTextStore::GetMessagePump();
+ if (msgPump) {
+ BOOL ret = FALSE;
+ HRESULT hr = msgPump->PeekMessageW(aMsg, aWnd, aFirstMessage, aLastMessage,
+ aOption, &ret);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+ return ret;
+ }
+ return ::PeekMessageW(aMsg, aWnd, aFirstMessage, aLastMessage, aOption);
+}
+
+/* static */
+bool WinUtils::GetMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage,
+ UINT aLastMessage) {
+ RefPtr<ITfMessagePump> msgPump = TSFTextStore::GetMessagePump();
+ if (msgPump) {
+ BOOL ret = FALSE;
+ HRESULT hr =
+ msgPump->GetMessageW(aMsg, aWnd, aFirstMessage, aLastMessage, &ret);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+ return ret;
+ }
+ return ::GetMessageW(aMsg, aWnd, aFirstMessage, aLastMessage);
+}
+
+#if defined(ACCESSIBILITY)
+static DWORD GetWaitFlags() {
+ DWORD result = MWMO_INPUTAVAILABLE;
+ if (XRE_IsContentProcess()) {
+ result |= MWMO_ALERTABLE;
+ }
+ return result;
+}
+#endif
+
+/* static */
+void WinUtils::WaitForMessage(DWORD aTimeoutMs) {
+#if defined(ACCESSIBILITY)
+ static const DWORD waitFlags = GetWaitFlags();
+#else
+ const DWORD waitFlags = MWMO_INPUTAVAILABLE;
+#endif
+
+ const DWORD waitStart = ::GetTickCount();
+ DWORD elapsed = 0;
+ while (true) {
+ if (aTimeoutMs != INFINITE) {
+ elapsed = ::GetTickCount() - waitStart;
+ }
+ if (elapsed >= aTimeoutMs) {
+ break;
+ }
+ DWORD result;
+ {
+ AUTO_PROFILER_THREAD_SLEEP;
+ result = ::MsgWaitForMultipleObjectsEx(0, NULL, aTimeoutMs - elapsed,
+ MOZ_QS_ALLEVENT, waitFlags);
+ }
+ NS_WARNING_ASSERTION(result != WAIT_FAILED, "Wait failed");
+ if (result == WAIT_TIMEOUT) {
+ break;
+ }
+#if defined(ACCESSIBILITY)
+ if (result == WAIT_IO_COMPLETION) {
+ if (NS_IsMainThread()) {
+ // We executed an APC that would have woken up the hang monitor. Since
+ // there are no more APCs pending and we are now going to sleep again,
+ // we should notify the hang monitor.
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+ continue;
+ }
+#endif // defined(ACCESSIBILITY)
+
+ // Sent messages (via SendMessage and friends) are processed differently
+ // than queued messages (via PostMessage); the destination window procedure
+ // of the sent message is called during (Get|Peek)Message. Since PeekMessage
+ // does not tell us whether it processed any sent messages, we need to query
+ // this ahead of time.
+ bool haveSentMessagesPending =
+ (HIWORD(::GetQueueStatus(QS_SENDMESSAGE)) & QS_SENDMESSAGE) != 0;
+
+ MSG msg = {0};
+ if (haveSentMessagesPending ||
+ ::PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE)) {
+ break;
+ }
+ // The message is intended for another thread that has been synchronized
+ // with our input queue; yield to give other threads an opportunity to
+ // process the message. This should prevent busy waiting if resumed due
+ // to another thread's message.
+ ::SwitchToThread();
+ }
+}
+
+/* static */
+HWND WinUtils::GetTopLevelHWND(HWND aWnd, bool aStopIfNotChild,
+ bool aStopIfNotPopup) {
+ HWND curWnd = aWnd;
+ HWND topWnd = nullptr;
+
+ while (curWnd) {
+ topWnd = curWnd;
+
+ if (aStopIfNotChild) {
+ DWORD_PTR style = ::GetWindowLongPtrW(curWnd, GWL_STYLE);
+
+ VERIFY_WINDOW_STYLE(style);
+
+ if (!(style & WS_CHILD)) // first top-level window
+ break;
+ }
+
+ HWND upWnd = ::GetParent(curWnd); // Parent or owner (if has no parent)
+
+ // GetParent will only return the owner if the passed in window
+ // has the WS_POPUP style.
+ if (!upWnd && !aStopIfNotPopup) {
+ upWnd = ::GetWindow(curWnd, GW_OWNER);
+ }
+ curWnd = upWnd;
+ }
+
+ return topWnd;
+}
+
+// Map from native window handles to nsWindow structures. Does not AddRef.
+// Inherently unsafe to access outside the main thread.
+static nsTHashMap<HWND, nsWindow*> sExtantNSWindows;
+
+/* static */
+void WinUtils::SetNSWindowPtr(HWND aWnd, nsWindow* aWindow) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!aWindow) {
+ sExtantNSWindows.Remove(aWnd);
+ } else {
+ sExtantNSWindows.InsertOrUpdate(aWnd, aWindow);
+ }
+}
+
+/* static */
+nsWindow* WinUtils::GetNSWindowPtr(HWND aWnd) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return sExtantNSWindows.Get(aWnd); // or nullptr
+}
+
+/* static */
+bool WinUtils::IsOurProcessWindow(HWND aWnd) {
+ if (!aWnd) {
+ return false;
+ }
+ DWORD processId = 0;
+ ::GetWindowThreadProcessId(aWnd, &processId);
+ return (processId == ::GetCurrentProcessId());
+}
+
+/* static */
+HWND WinUtils::FindOurProcessWindow(HWND aWnd) {
+ for (HWND wnd = ::GetParent(aWnd); wnd; wnd = ::GetParent(wnd)) {
+ if (IsOurProcessWindow(wnd)) {
+ return wnd;
+ }
+ }
+ return nullptr;
+}
+
+static bool IsPointInWindow(HWND aWnd, const POINT& aPointInScreen) {
+ RECT bounds;
+ if (!::GetWindowRect(aWnd, &bounds)) {
+ return false;
+ }
+
+ return (aPointInScreen.x >= bounds.left && aPointInScreen.x < bounds.right &&
+ aPointInScreen.y >= bounds.top && aPointInScreen.y < bounds.bottom);
+}
+
+/**
+ * FindTopmostWindowAtPoint() returns the topmost child window (topmost means
+ * forground in this context) of aWnd.
+ */
+
+static HWND FindTopmostWindowAtPoint(HWND aWnd, const POINT& aPointInScreen) {
+ if (!::IsWindowVisible(aWnd) || !IsPointInWindow(aWnd, aPointInScreen)) {
+ return nullptr;
+ }
+
+ HWND childWnd = ::GetTopWindow(aWnd);
+ while (childWnd) {
+ HWND topmostWnd = FindTopmostWindowAtPoint(childWnd, aPointInScreen);
+ if (topmostWnd) {
+ return topmostWnd;
+ }
+ childWnd = ::GetNextWindow(childWnd, GW_HWNDNEXT);
+ }
+
+ return aWnd;
+}
+
+struct FindOurWindowAtPointInfo {
+ POINT mInPointInScreen;
+ HWND mOutWnd;
+};
+
+static BOOL CALLBACK FindOurWindowAtPointCallback(HWND aWnd, LPARAM aLPARAM) {
+ if (!WinUtils::IsOurProcessWindow(aWnd)) {
+ // This isn't one of our top-level windows; continue enumerating.
+ return TRUE;
+ }
+
+ // Get the top-most child window under the point. If there's no child
+ // window, and the point is within the top-level window, then the top-level
+ // window will be returned. (This is the usual case. A child window
+ // would be returned for plugins.)
+ FindOurWindowAtPointInfo* info =
+ reinterpret_cast<FindOurWindowAtPointInfo*>(aLPARAM);
+ HWND childWnd = FindTopmostWindowAtPoint(aWnd, info->mInPointInScreen);
+ if (!childWnd) {
+ // This window doesn't contain the point; continue enumerating.
+ return TRUE;
+ }
+
+ // Return the HWND and stop enumerating.
+ info->mOutWnd = childWnd;
+ return FALSE;
+}
+
+/* static */
+HWND WinUtils::FindOurWindowAtPoint(const POINT& aPointInScreen) {
+ FindOurWindowAtPointInfo info;
+ info.mInPointInScreen = aPointInScreen;
+ info.mOutWnd = nullptr;
+
+ // This will enumerate all top-level windows in order from top to bottom.
+ EnumWindows(FindOurWindowAtPointCallback, reinterpret_cast<LPARAM>(&info));
+ return info.mOutWnd;
+}
+
+/* static */
+UINT WinUtils::GetInternalMessage(UINT aNativeMessage) {
+ switch (aNativeMessage) {
+ case WM_MOUSEWHEEL:
+ return MOZ_WM_MOUSEVWHEEL;
+ case WM_MOUSEHWHEEL:
+ return MOZ_WM_MOUSEHWHEEL;
+ case WM_VSCROLL:
+ return MOZ_WM_VSCROLL;
+ case WM_HSCROLL:
+ return MOZ_WM_HSCROLL;
+ default:
+ return aNativeMessage;
+ }
+}
+
+/* static */
+UINT WinUtils::GetNativeMessage(UINT aInternalMessage) {
+ switch (aInternalMessage) {
+ case MOZ_WM_MOUSEVWHEEL:
+ return WM_MOUSEWHEEL;
+ case MOZ_WM_MOUSEHWHEEL:
+ return WM_MOUSEHWHEEL;
+ case MOZ_WM_VSCROLL:
+ return WM_VSCROLL;
+ case MOZ_WM_HSCROLL:
+ return WM_HSCROLL;
+ default:
+ return aInternalMessage;
+ }
+}
+
+/* static */
+uint16_t WinUtils::GetMouseInputSource() {
+ int32_t inputSource = dom::MouseEvent_Binding::MOZ_SOURCE_MOUSE;
+ LPARAM lParamExtraInfo = ::GetMessageExtraInfo();
+ if ((lParamExtraInfo & TABLET_INK_SIGNATURE) == TABLET_INK_CHECK) {
+ inputSource = (lParamExtraInfo & TABLET_INK_TOUCH)
+ ? dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH
+ : dom::MouseEvent_Binding::MOZ_SOURCE_PEN;
+ }
+ return static_cast<uint16_t>(inputSource);
+}
+
+/* static */
+uint16_t WinUtils::GetMousePointerID() {
+ LPARAM lParamExtraInfo = ::GetMessageExtraInfo();
+ return lParamExtraInfo & TABLET_INK_ID_MASK;
+}
+
+/* static */
+bool WinUtils::GetIsMouseFromTouch(EventMessage aEventMessage) {
+ const uint32_t MOZ_T_I_SIGNATURE = TABLET_INK_TOUCH | TABLET_INK_SIGNATURE;
+ const uint32_t MOZ_T_I_CHECK_TCH = TABLET_INK_TOUCH | TABLET_INK_CHECK;
+ return ((aEventMessage == eMouseMove || aEventMessage == eMouseDown ||
+ aEventMessage == eMouseUp || aEventMessage == eMouseAuxClick ||
+ aEventMessage == eMouseDoubleClick) &&
+ (GetMessageExtraInfo() & MOZ_T_I_SIGNATURE) == MOZ_T_I_CHECK_TCH);
+}
+
+/* static */
+MSG WinUtils::InitMSG(UINT aMessage, WPARAM wParam, LPARAM lParam, HWND aWnd) {
+ MSG msg;
+ msg.message = aMessage;
+ msg.wParam = wParam;
+ msg.lParam = lParam;
+ msg.hwnd = aWnd;
+ return msg;
+}
+
+#ifdef MOZ_PLACES
+/************************************************************************
+ * Constructs as AsyncFaviconDataReady Object
+ * @param aIOThread : the thread which performs the action
+ * @param aURLShortcut : Differentiates between (false)Jumplistcache and
+ * (true)Shortcutcache
+ * @param aRunnable : Executed in the aIOThread when the favicon cache is
+ * avaiable
+ ************************************************************************/
+
+AsyncFaviconDataReady::AsyncFaviconDataReady(
+ nsIURI* aNewURI, RefPtr<LazyIdleThread>& aIOThread, const bool aURLShortcut,
+ already_AddRefed<nsIRunnable> aRunnable)
+ : mNewURI(aNewURI),
+ mIOThread(aIOThread),
+ mRunnable(aRunnable),
+ mURLShortcut(aURLShortcut) {}
+
+NS_IMETHODIMP
+myDownloadObserver::OnDownloadComplete(nsIDownloader* downloader,
+ nsIRequest* request, nsresult status,
+ nsIFile* result) {
+ return NS_OK;
+}
+
+nsresult AsyncFaviconDataReady::OnFaviconDataNotAvailable(void) {
+ if (!mURLShortcut) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> icoFile;
+ nsresult rv =
+ FaviconHelper::GetOutputIconPath(mNewURI, icoFile, mURLShortcut);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> mozIconURI;
+ rv = NS_NewURI(getter_AddRefs(mozIconURI), "moz-icon://.html?size=32");
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel), mozIconURI,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDownloadObserver> downloadObserver = new myDownloadObserver;
+ nsCOMPtr<nsIStreamListener> listener;
+ rv = NS_NewDownloader(getter_AddRefs(listener), downloadObserver, icoFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return channel->AsyncOpen(listener);
+}
+
+NS_IMETHODIMP
+AsyncFaviconDataReady::OnComplete(nsIURI* aFaviconURI, uint32_t aDataLen,
+ const uint8_t* aData,
+ const nsACString& aMimeType,
+ uint16_t aWidth) {
+ if (!aDataLen || !aData) {
+ if (mURLShortcut) {
+ OnFaviconDataNotAvailable();
+ }
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> icoFile;
+ nsresult rv =
+ FaviconHelper::GetOutputIconPath(mNewURI, icoFile, mURLShortcut);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString path;
+ rv = icoFile->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Decode the image from the format it was returned to us in (probably PNG)
+ nsCOMPtr<imgIContainer> container;
+ nsCOMPtr<imgITools> imgtool = do_CreateInstance("@mozilla.org/image/tools;1");
+ rv = imgtool->DecodeImageFromBuffer(reinterpret_cast<const char*>(aData),
+ aDataLen, aMimeType,
+ getter_AddRefs(container));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<SourceSurface> surface = container->GetFrame(
+ imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+
+ RefPtr<DataSourceSurface> dataSurface;
+ IntSize size;
+
+ if (mURLShortcut &&
+ (surface->GetSize().width < 48 || surface->GetSize().height < 48)) {
+ // Create a 48x48 surface and paint the icon into the central rect.
+ size.width = std::max(surface->GetSize().width, 48);
+ size.height = std::max(surface->GetSize().height, 48);
+ dataSurface =
+ Factory::CreateDataSourceSurface(size, SurfaceFormat::B8G8R8A8);
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
+ BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride,
+ dataSurface->GetFormat());
+ if (!dt) {
+ gfxWarning() << "AsyncFaviconDataReady::OnComplete failed in "
+ "CreateDrawTargetForData";
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ dt->FillRect(Rect(0, 0, size.width, size.height),
+ ColorPattern(ToDeviceColor(sRGBColor::OpaqueWhite())));
+ IntPoint point;
+ point.x = (size.width - surface->GetSize().width) / 2;
+ point.y = (size.height - surface->GetSize().height) / 2;
+ dt->DrawSurface(surface,
+ Rect(point.x, point.y, surface->GetSize().width,
+ surface->GetSize().height),
+ Rect(Point(0, 0), Size(surface->GetSize().width,
+ surface->GetSize().height)));
+
+ dataSurface->Unmap();
+ } else {
+ // By using the input image surface's size, we may end up encoding
+ // to a different size than a 16x16 (or bigger for higher DPI) ICO, but
+ // Windows will resize appropriately for us. If we want to encode ourselves
+ // one day because we like our resizing better, we'd have to manually
+ // resize the image here and use GetSystemMetrics w/ SM_CXSMICON and
+ // SM_CYSMICON. We don't support resizing images asynchronously at the
+ // moment anyway so getting the DPI aware icon size won't help.
+ size.width = surface->GetSize().width;
+ size.height = surface->GetSize().height;
+ dataSurface = surface->GetDataSurface();
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+ }
+
+ // Allocate a new buffer that we own and can use out of line in
+ // another thread.
+ UniquePtr<uint8_t[]> data = SurfaceToPackedBGRA(dataSurface);
+ if (!data) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ int32_t stride = 4 * size.width;
+
+ // AsyncEncodeAndWriteIcon takes ownership of the heap allocated buffer
+ nsCOMPtr<nsIRunnable> event =
+ new AsyncEncodeAndWriteIcon(path, std::move(data), stride, size.width,
+ size.height, mRunnable.forget());
+ mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+
+ return NS_OK;
+}
+#endif
+
+// Warning: AsyncEncodeAndWriteIcon assumes ownership of the aData buffer passed
+// in
+AsyncEncodeAndWriteIcon::AsyncEncodeAndWriteIcon(
+ const nsAString& aIconPath, UniquePtr<uint8_t[]> aBuffer, uint32_t aStride,
+ uint32_t aWidth, uint32_t aHeight, already_AddRefed<nsIRunnable> aRunnable)
+ : mIconPath(aIconPath),
+ mBuffer(std::move(aBuffer)),
+ mRunnable(aRunnable),
+ mStride(aStride),
+ mWidth(aWidth),
+ mHeight(aHeight) {}
+
+NS_IMETHODIMP AsyncEncodeAndWriteIcon::Run() {
+ MOZ_ASSERT(!NS_IsMainThread(), "Should not be called on the main thread.");
+
+ // Note that since we're off the main thread we can't use
+ // gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget()
+ RefPtr<DataSourceSurface> surface = Factory::CreateWrappingDataSourceSurface(
+ mBuffer.get(), mStride, IntSize(mWidth, mHeight),
+ SurfaceFormat::B8G8R8A8);
+
+ FILE* file = _wfopen(mIconPath.get(), L"wb");
+ if (!file) {
+ // Maybe the directory doesn't exist; try creating it, then fopen again.
+ nsresult rv = NS_ERROR_FAILURE;
+ nsCOMPtr<nsIFile> comFile = do_CreateInstance("@mozilla.org/file/local;1");
+ if (comFile) {
+ rv = comFile->InitWithPath(mIconPath);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFile> dirPath;
+ comFile->GetParent(getter_AddRefs(dirPath));
+ if (dirPath) {
+ rv = dirPath->Create(nsIFile::DIRECTORY_TYPE, 0777);
+ if (NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_ALREADY_EXISTS) {
+ file = _wfopen(mIconPath.get(), L"wb");
+ if (!file) {
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+ }
+ }
+ }
+ if (!file) {
+ return rv;
+ }
+ }
+ nsresult rv = gfxUtils::EncodeSourceSurface(surface, ImageType::ICO, u""_ns,
+ gfxUtils::eBinaryEncode, file);
+ fclose(file);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mRunnable) {
+ mRunnable->Run();
+ }
+ return rv;
+}
+
+AsyncEncodeAndWriteIcon::~AsyncEncodeAndWriteIcon() {}
+
+AsyncDeleteAllFaviconsFromDisk::AsyncDeleteAllFaviconsFromDisk(
+ bool aIgnoreRecent)
+ : mIgnoreRecent(aIgnoreRecent) {
+ // We can't call FaviconHelper::GetICOCacheSecondsTimeout() on non-main
+ // threads, as it reads a pref, so cache its value here.
+ mIcoNoDeleteSeconds = FaviconHelper::GetICOCacheSecondsTimeout() + 600;
+
+ // Prepare the profile directory cache on the main thread, to ensure we wont
+ // do this on non-main threads.
+ Unused << NS_GetSpecialDirectory("ProfLDS",
+ getter_AddRefs(mJumpListCacheDir));
+}
+
+NS_IMETHODIMP AsyncDeleteAllFaviconsFromDisk::Run() {
+ if (!mJumpListCacheDir) {
+ return NS_ERROR_FAILURE;
+ }
+ // Construct the path of our jump list cache
+ nsresult rv = mJumpListCacheDir->AppendNative(
+ nsDependentCString(FaviconHelper::kJumpListCacheDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDirectoryEnumerator> entries;
+ rv = mJumpListCacheDir->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Loop through each directory entry and remove all ICO files found
+ do {
+ nsCOMPtr<nsIFile> currFile;
+ if (NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile))) || !currFile)
+ break;
+
+ nsAutoString path;
+ if (NS_FAILED(currFile->GetPath(path))) continue;
+
+ if (StringTail(path, 4).LowerCaseEqualsASCII(".ico")) {
+ // Check if the cached ICO file exists
+ bool exists;
+ if (NS_FAILED(currFile->Exists(&exists)) || !exists) continue;
+
+ if (mIgnoreRecent) {
+ // Check to make sure the icon wasn't just recently created.
+ // If it was created recently, don't delete it yet.
+ int64_t fileModTime = 0;
+ rv = currFile->GetLastModifiedTime(&fileModTime);
+ fileModTime /= PR_MSEC_PER_SEC;
+ // If the icon is older than the regeneration time (+ 10 min to be
+ // safe), then it's old and we can get rid of it.
+ // This code is only hit directly after a regeneration.
+ int64_t nowTime = PR_Now() / int64_t(PR_USEC_PER_SEC);
+ if (NS_FAILED(rv) || (nowTime - fileModTime) < mIcoNoDeleteSeconds) {
+ continue;
+ }
+ }
+
+ // We found an ICO file that exists, so we should remove it
+ currFile->Remove(false);
+ }
+ } while (true);
+
+ return NS_OK;
+}
+
+AsyncDeleteAllFaviconsFromDisk::~AsyncDeleteAllFaviconsFromDisk() {}
+
+/*
+ * (static) If the data is available, will return the path on disk where
+ * the favicon for page aFaviconPageURI is stored. If the favicon does not
+ * exist, or its cache is expired, this method will kick off an async request
+ * for the icon so that next time the method is called it will be available.
+ * @param aFaviconPageURI The URI of the page to obtain
+ * @param aICOFilePath The path of the icon file
+ * @param aIOThread The thread to perform the Fetch on
+ * @param aURLShortcut to distinguish between jumplistcache(false) and
+ * shortcutcache(true)
+ * @param aRunnable Executed in the aIOThread when the favicon cache is
+ * avaiable
+ */
+nsresult FaviconHelper::ObtainCachedIconFile(
+ nsCOMPtr<nsIURI> aFaviconPageURI, nsString& aICOFilePath,
+ RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut,
+ already_AddRefed<nsIRunnable> aRunnable) {
+ nsCOMPtr<nsIRunnable> runnable = aRunnable;
+ // Obtain the ICO file path
+ nsCOMPtr<nsIFile> icoFile;
+ nsresult rv = GetOutputIconPath(aFaviconPageURI, icoFile, aURLShortcut);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check if the cached ICO file already exists
+ bool exists;
+ rv = icoFile->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (exists) {
+ // Obtain the file's last modification date in seconds
+ int64_t fileModTime = 0;
+ rv = icoFile->GetLastModifiedTime(&fileModTime);
+ fileModTime /= PR_MSEC_PER_SEC;
+ int32_t icoReCacheSecondsTimeout = GetICOCacheSecondsTimeout();
+ int64_t nowTime = PR_Now() / int64_t(PR_USEC_PER_SEC);
+
+ // If the last mod call failed or the icon is old then re-cache it
+ // This check is in case the favicon of a page changes
+ // the next time we try to build the jump list, the data will be available.
+ if (NS_FAILED(rv) || (nowTime - fileModTime) > icoReCacheSecondsTimeout) {
+ CacheIconFileFromFaviconURIAsync(aFaviconPageURI, icoFile, aIOThread,
+ aURLShortcut, runnable.forget());
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ } else {
+ // The file does not exist yet, obtain it async from the favicon service so
+ // that the next time we try to build the jump list it'll be available.
+ CacheIconFileFromFaviconURIAsync(aFaviconPageURI, icoFile, aIOThread,
+ aURLShortcut, runnable.forget());
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // The icoFile is filled with a path that exists, get its path
+ rv = icoFile->GetPath(aICOFilePath);
+ return rv;
+}
+
+// Hash a URI using a cryptographic hash function (currently SHA-256)
+// Output will be a base64-encoded string of the hash.
+static nsresult HashURI(nsIURI* aUri, nsACString& aUriHash) {
+ nsAutoCString spec;
+ nsresult rv = aUri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsICryptoHash> cryptoHash =
+ do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cryptoHash->Init(nsICryptoHash::SHA256);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add some context to the hash to even further reduce the chances of
+ // collision. Note that we are hashing this string with its null-terminator.
+ const char kHashUriContext[] = "firefox-uri";
+ rv = cryptoHash->Update(reinterpret_cast<const uint8_t*>(kHashUriContext),
+ sizeof(kHashUriContext));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cryptoHash->Update(reinterpret_cast<const uint8_t*>(spec.BeginReading()),
+ spec.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cryptoHash->Finish(true, aUriHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// (static) Obtains the ICO file for the favicon at page aFaviconPageURI
+// If successful, the file path on disk is in the format:
+// <ProfLDS>\jumpListCache\<hash(aFaviconPageURI)>.ico
+//
+// We generate the name with a cryptographically secure hash function in order
+// to ensure that malicious websites can't intentionally craft URLs to collide
+// with legitimate websites.
+nsresult FaviconHelper::GetOutputIconPath(nsCOMPtr<nsIURI> aFaviconPageURI,
+ nsCOMPtr<nsIFile>& aICOFile,
+ bool aURLShortcut) {
+ nsAutoCString inputURIHash;
+ nsresult rv = HashURI(aFaviconPageURI, inputURIHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ char* cur = inputURIHash.BeginWriting();
+ char* end = inputURIHash.EndWriting();
+ for (; cur < end; ++cur) {
+ if ('/' == *cur) {
+ *cur = '_';
+ }
+ }
+
+ // Obtain the local profile directory and construct the output icon file path
+ rv = NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(aICOFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!aURLShortcut)
+ rv = aICOFile->AppendNative(nsDependentCString(kJumpListCacheDir));
+ else
+ rv = aICOFile->AppendNative(nsDependentCString(kShortcutCacheDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Append the icon extension
+ inputURIHash.AppendLiteral(".ico");
+ rv = aICOFile->AppendNative(inputURIHash);
+
+ return rv;
+}
+
+// (static) Asynchronously creates a cached ICO file on disk for the favicon of
+// page aFaviconPageURI and stores it to disk at the path of aICOFile.
+nsresult FaviconHelper::CacheIconFileFromFaviconURIAsync(
+ nsCOMPtr<nsIURI> aFaviconPageURI, nsCOMPtr<nsIFile> aICOFile,
+ RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut,
+ already_AddRefed<nsIRunnable> aRunnable) {
+ nsCOMPtr<nsIRunnable> runnable = aRunnable;
+#ifdef MOZ_PLACES
+ // Obtain the favicon service and get the favicon for the specified page
+ nsCOMPtr<nsIFaviconService> favIconSvc(
+ do_GetService("@mozilla.org/browser/favicon-service;1"));
+ NS_ENSURE_TRUE(favIconSvc, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIFaviconDataCallback> callback =
+ new mozilla::widget::AsyncFaviconDataReady(
+ aFaviconPageURI, aIOThread, aURLShortcut, runnable.forget());
+
+ favIconSvc->GetFaviconDataForPage(aFaviconPageURI, callback, 0);
+#endif
+ return NS_OK;
+}
+
+// Obtains the jump list 'ICO cache timeout in seconds' pref
+int32_t FaviconHelper::GetICOCacheSecondsTimeout() {
+ // Only obtain the setting at most once from the pref service.
+ // In the rare case that 2 threads call this at the same
+ // time it is no harm and we will simply obtain the pref twice.
+ // None of the taskbar list prefs are currently updated via a
+ // pref observer so I think this should suffice.
+ const int32_t kSecondsPerDay = 86400;
+ static bool alreadyObtained = false;
+ static int32_t icoReCacheSecondsTimeout = kSecondsPerDay;
+ if (alreadyObtained) {
+ return icoReCacheSecondsTimeout;
+ }
+
+ // Obtain the pref
+ const char PREF_ICOTIMEOUT[] = "browser.taskbar.lists.icoTimeoutInSeconds";
+ icoReCacheSecondsTimeout =
+ Preferences::GetInt(PREF_ICOTIMEOUT, kSecondsPerDay);
+ alreadyObtained = true;
+ return icoReCacheSecondsTimeout;
+}
+
+/* static */
+LayoutDeviceIntRegion WinUtils::ConvertHRGNToRegion(HRGN aRgn) {
+ NS_ASSERTION(aRgn, "Don't pass NULL region here");
+
+ LayoutDeviceIntRegion rgn;
+
+ DWORD size = ::GetRegionData(aRgn, 0, nullptr);
+ AutoTArray<uint8_t, 100> buffer;
+ buffer.SetLength(size);
+
+ RGNDATA* data = reinterpret_cast<RGNDATA*>(buffer.Elements());
+ if (!::GetRegionData(aRgn, size, data)) return rgn;
+
+ if (data->rdh.nCount > MAX_RECTS_IN_REGION) {
+ rgn = ToIntRect(data->rdh.rcBound);
+ return rgn;
+ }
+
+ RECT* rects = reinterpret_cast<RECT*>(data->Buffer);
+ for (uint32_t i = 0; i < data->rdh.nCount; ++i) {
+ RECT* r = rects + i;
+ rgn.Or(rgn, ToIntRect(*r));
+ }
+
+ return rgn;
+}
+
+LayoutDeviceIntRect WinUtils::ToIntRect(const RECT& aRect) {
+ return LayoutDeviceIntRect(aRect.left, aRect.top, aRect.right - aRect.left,
+ aRect.bottom - aRect.top);
+}
+
+/* static */
+bool WinUtils::IsIMEEnabled(const InputContext& aInputContext) {
+ return IsIMEEnabled(aInputContext.mIMEState.mEnabled);
+}
+
+/* static */
+bool WinUtils::IsIMEEnabled(IMEEnabled aIMEState) {
+ return aIMEState == IMEEnabled::Enabled;
+}
+
+/* static */
+void WinUtils::SetupKeyModifiersSequence(nsTArray<KeyPair>* aArray,
+ uint32_t aModifiers, UINT aMessage) {
+ MOZ_ASSERT(!(aModifiers & nsIWidget::ALTGRAPH) ||
+ !(aModifiers & (nsIWidget::CTRL_L | nsIWidget::ALT_R)));
+ if (aMessage == WM_KEYUP) {
+ // If AltGr is released, ControlLeft key is released first, then,
+ // AltRight key is released.
+ if (aModifiers & nsIWidget::ALTGRAPH) {
+ aArray->AppendElement(
+ KeyPair(VK_CONTROL, VK_LCONTROL, ScanCode::eControlLeft));
+ aArray->AppendElement(KeyPair(VK_MENU, VK_RMENU, ScanCode::eAltRight));
+ }
+ for (uint32_t i = ArrayLength(sModifierKeyMap); i; --i) {
+ const uint32_t* map = sModifierKeyMap[i - 1];
+ if (aModifiers & map[0]) {
+ aArray->AppendElement(KeyPair(map[1], map[2], map[3]));
+ }
+ }
+ } else {
+ for (uint32_t i = 0; i < ArrayLength(sModifierKeyMap); ++i) {
+ const uint32_t* map = sModifierKeyMap[i];
+ if (aModifiers & map[0]) {
+ aArray->AppendElement(KeyPair(map[1], map[2], map[3]));
+ }
+ }
+ // If AltGr is pressed, ControlLeft key is pressed first, then,
+ // AltRight key is pressed.
+ if (aModifiers & nsIWidget::ALTGRAPH) {
+ aArray->AppendElement(
+ KeyPair(VK_CONTROL, VK_LCONTROL, ScanCode::eControlLeft));
+ aArray->AppendElement(KeyPair(VK_MENU, VK_RMENU, ScanCode::eAltRight));
+ }
+ }
+}
+
+/* static */
+nsresult WinUtils::WriteBitmap(nsIFile* aFile, imgIContainer* aImage) {
+ RefPtr<SourceSurface> surface = aImage->GetFrame(
+ imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+
+ return WriteBitmap(aFile, surface);
+}
+
+/* static */
+nsresult WinUtils::WriteBitmap(nsIFile* aFile, SourceSurface* surface) {
+ nsresult rv;
+
+ // For either of the following formats we want to set the biBitCount member
+ // of the BITMAPINFOHEADER struct to 32, below. For that value the bitmap
+ // format defines that the A8/X8 WORDs in the bitmap byte stream be ignored
+ // for the BI_RGB value we use for the biCompression member.
+ MOZ_ASSERT(surface->GetFormat() == SurfaceFormat::B8G8R8A8 ||
+ surface->GetFormat() == SurfaceFormat::B8G8R8X8);
+
+ RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+
+ int32_t width = dataSurface->GetSize().width;
+ int32_t height = dataSurface->GetSize().height;
+ int32_t bytesPerPixel = 4 * sizeof(uint8_t);
+ uint32_t bytesPerRow = bytesPerPixel * width;
+ bool hasAlpha = surface->GetFormat() == SurfaceFormat::B8G8R8A8;
+
+ // initialize these bitmap structs which we will later
+ // serialize directly to the head of the bitmap file
+ BITMAPV4HEADER bmi;
+ memset(&bmi, 0, sizeof(BITMAPV4HEADER));
+ bmi.bV4Size = sizeof(BITMAPV4HEADER);
+ bmi.bV4Width = width;
+ bmi.bV4Height = height;
+ bmi.bV4Planes = 1;
+ bmi.bV4BitCount = (WORD)bytesPerPixel * 8;
+ bmi.bV4V4Compression = hasAlpha ? BI_BITFIELDS : BI_RGB;
+ bmi.bV4SizeImage = bytesPerRow * height;
+ bmi.bV4CSType = LCS_sRGB;
+ if (hasAlpha) {
+ bmi.bV4RedMask = 0x00FF0000;
+ bmi.bV4GreenMask = 0x0000FF00;
+ bmi.bV4BlueMask = 0x000000FF;
+ bmi.bV4AlphaMask = 0xFF000000;
+ }
+
+ BITMAPFILEHEADER bf;
+ DWORD colormask[3];
+ bf.bfType = 0x4D42; // 'BM'
+ bf.bfReserved1 = 0;
+ bf.bfReserved2 = 0;
+ bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPV4HEADER) +
+ (hasAlpha ? sizeof(colormask) : 0);
+ bf.bfSize = bf.bfOffBits + bmi.bV4SizeImage;
+
+ // get a file output stream
+ nsCOMPtr<nsIOutputStream> stream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // write the bitmap headers and rgb pixel data to the file
+ rv = NS_ERROR_FAILURE;
+ if (stream) {
+ uint32_t written;
+ stream->Write((const char*)&bf, sizeof(BITMAPFILEHEADER), &written);
+ if (written == sizeof(BITMAPFILEHEADER)) {
+ stream->Write((const char*)&bmi, sizeof(BITMAPV4HEADER), &written);
+ if (written == sizeof(BITMAPV4HEADER)) {
+ if (hasAlpha) {
+ // color mask
+ colormask[0] = 0x00FF0000;
+ colormask[1] = 0x0000FF00;
+ colormask[2] = 0x000000FF;
+
+ stream->Write((const char*)colormask, sizeof(colormask), &written);
+ }
+ if (!hasAlpha || written == sizeof(colormask)) {
+ // write out the image data backwards because the desktop won't
+ // show bitmaps with negative heights for top-to-bottom
+ uint32_t i = map.mStride * height;
+ do {
+ i -= map.mStride;
+ stream->Write(((const char*)map.mData) + i, bytesPerRow, &written);
+ if (written == bytesPerRow) {
+ rv = NS_OK;
+ } else {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ } while (i != 0);
+ }
+ }
+ }
+
+ stream->Close();
+ }
+
+ dataSurface->Unmap();
+
+ return rv;
+}
+
+// This is in use here and in dom/events/TouchEvent.cpp
+/* static */
+uint32_t WinUtils::IsTouchDeviceSupportPresent() {
+ int32_t touchCapabilities = ::GetSystemMetrics(SM_DIGITIZER);
+ int32_t touchFlags = NID_EXTERNAL_TOUCH | NID_INTEGRATED_TOUCH;
+ if (StaticPrefs::dom_w3c_pointer_events_scroll_by_pen_enabled()) {
+ touchFlags |= NID_EXTERNAL_PEN | NID_INTEGRATED_PEN;
+ }
+ return (touchCapabilities & NID_READY) && (touchCapabilities & touchFlags);
+}
+
+/* static */
+uint32_t WinUtils::GetMaxTouchPoints() {
+ if (IsTouchDeviceSupportPresent()) {
+ return GetSystemMetrics(SM_MAXIMUMTOUCHES);
+ }
+ return 0;
+}
+
+/* static */
+POWER_PLATFORM_ROLE
+WinUtils::GetPowerPlatformRole() {
+ typedef POWER_PLATFORM_ROLE(WINAPI *
+ PowerDeterminePlatformRoleEx)(ULONG Version);
+ static PowerDeterminePlatformRoleEx power_determine_platform_role =
+ reinterpret_cast<PowerDeterminePlatformRoleEx>(::GetProcAddress(
+ ::LoadLibraryW(L"PowrProf.dll"), "PowerDeterminePlatformRoleEx"));
+
+ POWER_PLATFORM_ROLE powerPlatformRole = PlatformRoleUnspecified;
+ if (!power_determine_platform_role) {
+ return powerPlatformRole;
+ }
+
+ return power_determine_platform_role(POWER_PLATFORM_ROLE_V2);
+}
+
+// static
+bool WinUtils::GetAutoRotationState(AR_STATE* aRotationState) {
+ typedef BOOL(WINAPI * GetAutoRotationStateFunc)(PAR_STATE pState);
+ static GetAutoRotationStateFunc get_auto_rotation_state_func =
+ reinterpret_cast<GetAutoRotationStateFunc>(::GetProcAddress(
+ GetModuleHandleW(L"user32.dll"), "GetAutoRotationState"));
+ if (get_auto_rotation_state_func) {
+ ZeroMemory(aRotationState, sizeof(AR_STATE));
+ return get_auto_rotation_state_func(aRotationState);
+ }
+ return false;
+}
+
+// static
+void WinUtils::GetClipboardFormatAsString(UINT aFormat, nsAString& aOutput) {
+ wchar_t buf[256] = {};
+ // Get registered format name and ensure the existence of a terminating '\0'
+ // if the registered name is more than 256 characters.
+ if (::GetClipboardFormatNameW(aFormat, buf, ARRAYSIZE(buf) - 1)) {
+ aOutput.Append(buf);
+ return;
+ }
+ // Standard clipboard formats
+ // https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
+ switch (aFormat) {
+ case CF_TEXT: // 1
+ aOutput.Append(u"CF_TEXT"_ns);
+ break;
+ case CF_BITMAP: // 2
+ aOutput.Append(u"CF_BITMAP"_ns);
+ break;
+ case CF_DIB: // 8
+ aOutput.Append(u"CF_DIB"_ns);
+ break;
+ case CF_UNICODETEXT: // 13
+ aOutput.Append(u"CF_UNICODETEXT"_ns);
+ break;
+ case CF_HDROP: // 15
+ aOutput.Append(u"CF_HDROP"_ns);
+ break;
+ case CF_DIBV5: // 17
+ aOutput.Append(u"CF_DIBV5"_ns);
+ break;
+ default:
+ aOutput.AppendPrintf("%u", aFormat);
+ break;
+ }
+}
+
+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()) {
+ return true;
+ }
+
+ if (!GetSystemMetrics(SM_MAXIMUMTOUCHES)) {
+ return false;
+ }
+
+ // If the device is docked, the user is treating the device as a PC.
+ if (GetSystemMetrics(SM_SYSTEMDOCKED)) {
+ return false;
+ }
+
+ // If the device is not supporting rotation, it's unlikely to be a tablet,
+ // a convertible or a detachable. See:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dn629263(v=vs.85).aspx
+ AR_STATE rotation_state;
+ if (WinUtils::GetAutoRotationState(&rotation_state) &&
+ (rotation_state & (AR_NOT_SUPPORTED | AR_LAPTOP | AR_NOSENSOR))) {
+ return false;
+ }
+
+ // PlatformRoleSlate was added in Windows 8+.
+ POWER_PLATFORM_ROLE role = WinUtils::GetPowerPlatformRole();
+ if (role == PlatformRoleMobile || role == PlatformRoleSlate) {
+ return !GetSystemMetrics(SM_CONVERTIBLESLATEMODE);
+ }
+ return false;
+}
+
+static bool SystemHasMouse() {
+ // As per MSDN, this value is rarely false because of virtual mice, and
+ // some machines report the existance of a mouse port as a mouse.
+ //
+ // We probably could try to distinguish if we wanted, but a virtual mouse
+ // might be there for a reason, and maybe we shouldn't assume we know
+ // better.
+ return !!::GetSystemMetrics(SM_MOUSEPRESENT);
+}
+
+/* static */
+PointerCapabilities WinUtils::GetPrimaryPointerCapabilities() {
+ if (IsTabletDevice()) {
+ return PointerCapabilities::Coarse;
+ }
+
+ if (SystemHasMouse()) {
+ return PointerCapabilities::Fine | PointerCapabilities::Hover;
+ }
+
+ if (IsTouchDeviceSupportPresent()) {
+ return PointerCapabilities::Coarse;
+ }
+
+ return PointerCapabilities::None;
+}
+
+static bool SystemHasTouchscreen() {
+ int digitizerMetrics = ::GetSystemMetrics(SM_DIGITIZER);
+ return (digitizerMetrics & NID_INTEGRATED_TOUCH) ||
+ (digitizerMetrics & NID_EXTERNAL_TOUCH);
+}
+
+static bool SystemHasPenDigitizer() {
+ int digitizerMetrics = ::GetSystemMetrics(SM_DIGITIZER);
+ return (digitizerMetrics & NID_INTEGRATED_PEN) ||
+ (digitizerMetrics & NID_EXTERNAL_PEN);
+}
+
+/* static */
+PointerCapabilities WinUtils::GetAllPointerCapabilities() {
+ PointerCapabilities pointerCapabilities = PointerCapabilities::None;
+
+ if (SystemHasTouchscreen()) {
+ pointerCapabilities |= PointerCapabilities::Coarse;
+ }
+
+ if (SystemHasPenDigitizer() || SystemHasMouse()) {
+ pointerCapabilities |=
+ PointerCapabilities::Fine | PointerCapabilities::Hover;
+ }
+
+ return pointerCapabilities;
+}
+
+void WinUtils::GetPointerExplanation(nsAString* aExplanation) {
+ // To support localization, we will return a comma-separated list of
+ // Fluent IDs
+ *aExplanation = u"pointing-device-none";
+
+ bool first = true;
+ auto append = [&](const char16_t* str) {
+ if (first) {
+ aExplanation->Truncate();
+ first = false;
+ } else {
+ aExplanation->Append(u",");
+ }
+ aExplanation->Append(str);
+ };
+
+ if (SystemHasTouchscreen()) {
+ append(u"pointing-device-touchscreen");
+ }
+
+ if (SystemHasPenDigitizer()) {
+ append(u"pointing-device-pen-digitizer");
+ }
+
+ if (SystemHasMouse()) {
+ append(u"pointing-device-mouse");
+ }
+}
+
+/* static */
+bool WinUtils::ResolveJunctionPointsAndSymLinks(std::wstring& aPath) {
+ LOG_D("ResolveJunctionPointsAndSymLinks: Resolving path: %S", aPath.c_str());
+
+ wchar_t path[MAX_PATH] = {0};
+
+ nsAutoHandle handle(::CreateFileW(
+ aPath.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr));
+
+ if (handle == INVALID_HANDLE_VALUE) {
+ LOG_E("Failed to open file handle to resolve path. GetLastError=%lu",
+ GetLastError());
+ return false;
+ }
+
+ DWORD pathLen = GetFinalPathNameByHandleW(
+ handle, path, MAX_PATH, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
+ if (pathLen == 0 || pathLen >= MAX_PATH) {
+ LOG_E("GetFinalPathNameByHandleW failed. GetLastError=%lu", GetLastError());
+ return false;
+ }
+ aPath = path;
+
+ // GetFinalPathNameByHandle sticks a '\\?\' in front of the path,
+ // but that confuses some APIs so strip it off. It will also put
+ // '\\?\UNC\' in front of network paths, we convert that to '\\'.
+ if (aPath.compare(0, 7, L"\\\\?\\UNC") == 0) {
+ aPath.erase(2, 6);
+ } else if (aPath.compare(0, 4, L"\\\\?\\") == 0) {
+ aPath.erase(0, 4);
+ }
+
+ LOG_D("ResolveJunctionPointsAndSymLinks: Resolved path to: %S",
+ aPath.c_str());
+ return true;
+}
+
+/* static */
+bool WinUtils::ResolveJunctionPointsAndSymLinks(nsIFile* aPath) {
+ MOZ_ASSERT(aPath);
+
+ nsAutoString filePath;
+ nsresult rv = aPath->GetPath(filePath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ std::wstring resolvedPath(filePath.get());
+ if (!ResolveJunctionPointsAndSymLinks(resolvedPath)) {
+ return false;
+ }
+
+ rv = aPath->InitWithPath(nsDependentString(resolvedPath.c_str()));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return true;
+}
+
+/* static */
+bool WinUtils::RunningFromANetworkDrive() {
+ wchar_t exePath[MAX_PATH];
+ if (!::GetModuleFileNameW(nullptr, exePath, MAX_PATH)) {
+ return false;
+ }
+
+ std::wstring exeString(exePath);
+ if (!widget::WinUtils::ResolveJunctionPointsAndSymLinks(exeString)) {
+ return false;
+ }
+
+ wchar_t volPath[MAX_PATH];
+ if (!::GetVolumePathNameW(exeString.c_str(), volPath, MAX_PATH)) {
+ return false;
+ }
+
+ return (::GetDriveTypeW(volPath) == DRIVE_REMOTE);
+}
+
+/* static */
+bool WinUtils::CanonicalizePath(nsAString& aPath) {
+ wchar_t tempPath[MAX_PATH + 1];
+ if (!PathCanonicalizeW(tempPath,
+ (char16ptr_t)PromiseFlatString(aPath).get())) {
+ return false;
+ }
+ aPath = tempPath;
+ MOZ_ASSERT(aPath.Length() <= MAX_PATH);
+ return true;
+}
+
+/* static */
+bool WinUtils::MakeLongPath(nsAString& aPath) {
+ wchar_t tempPath[MAX_PATH + 1];
+ DWORD longResult =
+ GetLongPathNameW((char16ptr_t)PromiseFlatString(aPath).get(), tempPath,
+ ArrayLength(tempPath));
+ if (longResult > ArrayLength(tempPath)) {
+ // Our buffer is too short, and we're guaranteeing <= MAX_PATH results.
+ return false;
+ } else if (longResult) {
+ // Success.
+ aPath = tempPath;
+ MOZ_ASSERT(aPath.Length() <= MAX_PATH);
+ }
+ // GetLongPathNameW returns 0 if the path is not found or is not rooted,
+ // but we shouldn't consider that a failure condition.
+ return true;
+}
+
+/* static */
+bool WinUtils::UnexpandEnvVars(nsAString& aPath) {
+ wchar_t tempPath[MAX_PATH + 1];
+ // PathUnExpandEnvStringsW returns false if it doesn't make any
+ // substitutions. Silently continue using the unaltered path.
+ if (PathUnExpandEnvStringsW((char16ptr_t)PromiseFlatString(aPath).get(),
+ tempPath, ArrayLength(tempPath))) {
+ aPath = tempPath;
+ MOZ_ASSERT(aPath.Length() <= MAX_PATH);
+ }
+ return true;
+}
+
+/* static */
+WinUtils::WhitelistVec WinUtils::BuildWhitelist() {
+ WhitelistVec result;
+
+ Unused << result.emplaceBack(
+ std::make_pair(nsString(u"%ProgramFiles%"_ns), nsDependentString()));
+
+ // When no substitution is required, set the void flag
+ result.back().second.SetIsVoid(true);
+
+ Unused << result.emplaceBack(
+ std::make_pair(nsString(u"%SystemRoot%"_ns), nsDependentString()));
+ result.back().second.SetIsVoid(true);
+
+ wchar_t tmpPath[MAX_PATH + 1] = {};
+ if (GetTempPath(MAX_PATH, tmpPath)) {
+ // GetTempPath's result always ends with a backslash, which we don't want
+ uint32_t tmpPathLen = wcslen(tmpPath);
+ if (tmpPathLen) {
+ tmpPath[tmpPathLen - 1] = 0;
+ }
+
+ nsAutoString cleanTmpPath(tmpPath);
+ if (UnexpandEnvVars(cleanTmpPath)) {
+ constexpr auto tempVar = u"%TEMP%"_ns;
+ Unused << result.emplaceBack(std::make_pair(
+ nsString(cleanTmpPath), nsDependentString(tempVar, 0)));
+ }
+ }
+
+ // If we add more items to the whitelist, ensure we still don't invoke an
+ // unnecessary heap allocation.
+ MOZ_ASSERT(result.length() <= kMaxWhitelistedItems);
+
+ return result;
+}
+
+/**
+ * This function provides an array of (system path, substitution) pairs that are
+ * considered to be acceptable with respect to privacy, for the purposes of
+ * submitting within telemetry or crash reports.
+ *
+ * The substitution string's void flag may be set. If it is, no subsitution is
+ * necessary. Otherwise, the consumer should replace the system path with the
+ * substitution.
+ *
+ * @see PreparePathForTelemetry for an example of its usage.
+ */
+/* static */
+const WinUtils::WhitelistVec& WinUtils::GetWhitelistedPaths() {
+ static WhitelistVec sWhitelist([]() -> WhitelistVec {
+ auto setClearFn = [ptr = &sWhitelist]() -> void {
+ RunOnShutdown([ptr]() -> void { ptr->clear(); },
+ ShutdownPhase::XPCOMShutdownFinal);
+ };
+
+ if (NS_IsMainThread()) {
+ setClearFn();
+ } else {
+ SchedulerGroup::Dispatch(NS_NewRunnableFunction(
+ "WinUtils::GetWhitelistedPaths", std::move(setClearFn)));
+ }
+
+ return BuildWhitelist();
+ }());
+ return sWhitelist;
+}
+
+/**
+ * This function is located here (as opposed to nsSystemInfo or elsewhere)
+ * because we need to gather this information as early as possible during
+ * startup.
+ */
+/* static */
+bool WinUtils::GetAppInitDLLs(nsAString& aOutput) {
+ aOutput.Truncate();
+ HKEY hkey = NULL;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows",
+ 0, KEY_QUERY_VALUE, &hkey)) {
+ return false;
+ }
+ nsAutoRegKey key(hkey);
+ LONG status;
+ const wchar_t kLoadAppInitDLLs[] = L"LoadAppInit_DLLs";
+ DWORD loadAppInitDLLs = 0;
+ DWORD loadAppInitDLLsLen = sizeof(loadAppInitDLLs);
+ status = RegQueryValueExW(hkey, kLoadAppInitDLLs, nullptr, nullptr,
+ (LPBYTE)&loadAppInitDLLs, &loadAppInitDLLsLen);
+ if (status != ERROR_SUCCESS) {
+ return false;
+ }
+ if (!loadAppInitDLLs) {
+ // If loadAppInitDLLs is zero then AppInit_DLLs is disabled.
+ // In this case we'll return true along with an empty output string.
+ return true;
+ }
+ DWORD numBytes = 0;
+ const wchar_t kAppInitDLLs[] = L"AppInit_DLLs";
+ // Query for required buffer size
+ status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr, nullptr,
+ &numBytes);
+ if (status != ERROR_SUCCESS) {
+ return false;
+ }
+ // Allocate the buffer and query for the actual data
+ mozilla::UniquePtr<wchar_t[]> data =
+ mozilla::MakeUnique<wchar_t[]>(numBytes / sizeof(wchar_t));
+ status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr,
+ (LPBYTE)data.get(), &numBytes);
+ if (status != ERROR_SUCCESS) {
+ return false;
+ }
+ // For each token, split up the filename components and then check the
+ // name of the file.
+ const wchar_t kDelimiters[] = L", ";
+ wchar_t* tokenContext = nullptr;
+ wchar_t* token = wcstok_s(data.get(), kDelimiters, &tokenContext);
+ while (token) {
+ nsAutoString cleanPath(token);
+ // Since these paths are short paths originating from the registry, we need
+ // to canonicalize them, lengthen them, and sanitize them before we can
+ // check them against the whitelist
+ if (PreparePathForTelemetry(cleanPath)) {
+ if (!aOutput.IsEmpty()) {
+ aOutput += L";";
+ }
+ aOutput += cleanPath;
+ }
+ token = wcstok_s(nullptr, kDelimiters, &tokenContext);
+ }
+ return true;
+}
+
+/* static */
+bool WinUtils::PreparePathForTelemetry(nsAString& aPath,
+ PathTransformFlags aFlags) {
+ if (aFlags & PathTransformFlags::Canonicalize) {
+ if (!CanonicalizePath(aPath)) {
+ return false;
+ }
+ }
+ if (aFlags & PathTransformFlags::Lengthen) {
+ if (!MakeLongPath(aPath)) {
+ return false;
+ }
+ }
+ if (aFlags & PathTransformFlags::UnexpandEnvVars) {
+ if (!UnexpandEnvVars(aPath)) {
+ return false;
+ }
+ }
+
+ const WhitelistVec& whitelistedPaths = GetWhitelistedPaths();
+
+ for (uint32_t i = 0; i < whitelistedPaths.length(); ++i) {
+ const nsString& testPath = whitelistedPaths[i].first;
+ const nsDependentString& substitution = whitelistedPaths[i].second;
+ if (StringBeginsWith(aPath, testPath, nsCaseInsensitiveStringComparator)) {
+ if (!substitution.IsVoid()) {
+ aPath.Replace(0, testPath.Length(), substitution);
+ }
+ return true;
+ }
+ }
+
+ // For non-whitelisted paths, we strip the path component and just leave
+ // the filename. We can't use nsLocalFile to do this because these paths may
+ // begin with environment variables, and nsLocalFile doesn't like
+ // non-absolute paths.
+ const nsString& flatPath = PromiseFlatString(aPath);
+ LPCWSTR leafStart = ::PathFindFileNameW(flatPath.get());
+ ptrdiff_t cutLen = leafStart - flatPath.get();
+ if (cutLen) {
+ aPath.Cut(0, cutLen);
+ } else if (aFlags & PathTransformFlags::RequireFilePath) {
+ return false;
+ }
+
+ return true;
+}
+
+nsString WinUtils::GetPackageFamilyName() {
+ nsString rv;
+
+ UniquePtr<wchar_t[]> packageIdentity = mozilla::GetPackageFamilyName();
+ if (packageIdentity) {
+ rv = packageIdentity.get();
+ }
+
+ return rv;
+}
+
+bool WinUtils::GetClassName(HWND aHwnd, nsAString& aClassName) {
+ const int bufferLength = 256;
+ aClassName.SetLength(bufferLength);
+
+ int length = ::GetClassNameW(aHwnd, (char16ptr_t)aClassName.BeginWriting(),
+ bufferLength);
+ if (length == 0) {
+ return false;
+ }
+ MOZ_RELEASE_ASSERT(length <= (bufferLength - 1));
+ aClassName.Truncate(length);
+ return true;
+}
+
+static BOOL CALLBACK EnumUpdateWindowOcclusionProc(HWND aHwnd, LPARAM aLParam) {
+ const bool* const enable = reinterpret_cast<bool*>(aLParam);
+ nsWindow* window = WinUtils::GetNSWindowPtr(aHwnd);
+ if (window) {
+ window->MaybeEnableWindowOcclusion(*enable);
+ }
+ return TRUE;
+}
+
+void WinUtils::EnableWindowOcclusion(const bool aEnable) {
+ if (aEnable) {
+ WinWindowOcclusionTracker::Ensure();
+ }
+ ::EnumWindows(EnumUpdateWindowOcclusionProc,
+ reinterpret_cast<LPARAM>(&aEnable));
+}
+
+bool WinUtils::GetTimezoneName(wchar_t* aBuffer) {
+ DYNAMIC_TIME_ZONE_INFORMATION tzInfo;
+ DWORD tzid = GetDynamicTimeZoneInformation(&tzInfo);
+
+ if (tzid == TIME_ZONE_ID_INVALID) {
+ return false;
+ }
+
+ wcscpy_s(aBuffer, 128, tzInfo.TimeZoneKeyName);
+
+ return true;
+}
+
+// There are undocumented APIs to query/change the system DPI settings found by
+// https://github.com/lihas/ . We use those APIs only for testing purpose, i.e.
+// in mochitests or some such. To avoid exposing them in our official release
+// builds unexpectedly we restrict them only in debug builds.
+#ifdef DEBUG
+
+# define DISPLAYCONFIG_DEVICE_INFO_SET_SOURCE_DPI_SCALE (int)-4
+# define DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_DPI_SCALE (int)-3
+
+// Following two struts are copied from
+// https://github.com/lihas/windows-DPI-scaling-sample/blob/master/DPIHelper/DpiHelper.h
+
+/*
+ * struct DISPLAYCONFIG_SOURCE_DPI_SCALE_GET
+ * @brief used to fetch min, max, suggested, and currently applied DPI scaling
+ * values. All values are relative to the recommended DPI scaling value Note
+ * that DPI scaling is a property of the source, and not of target.
+ */
+struct DISPLAYCONFIG_SOURCE_DPI_SCALE_GET {
+ DISPLAYCONFIG_DEVICE_INFO_HEADER header;
+ /*
+ * @brief min value of DPI scaling is always 100, minScaleRel gives no. of
+ * steps down from recommended scaling eg. if minScaleRel is -3 => 100 is 3
+ * steps down from recommended scaling => recommended scaling is 175%
+ */
+ int32_t minScaleRel;
+
+ /*
+ * @brief currently applied DPI scaling value wrt the recommended value. eg.
+ * if recommended value is 175%,
+ * => if curScaleRel == 0 the current scaling is 175%, if curScaleRel == -1,
+ * then current scale is 150%
+ */
+ int32_t curScaleRel;
+
+ /*
+ * @brief maximum supported DPI scaling wrt recommended value
+ */
+ int32_t maxScaleRel;
+};
+
+/*
+ * struct DISPLAYCONFIG_SOURCE_DPI_SCALE_SET
+ * @brief set DPI scaling value of a source
+ * Note that DPI scaling is a property of the source, and not of target.
+ */
+struct DISPLAYCONFIG_SOURCE_DPI_SCALE_SET {
+ DISPLAYCONFIG_DEVICE_INFO_HEADER header;
+ /*
+ * @brief The value we want to set. The value should be relative to the
+ * recommended DPI scaling value of source. eg. if scaleRel == 1, and
+ * recommended value is 175% => we are trying to set 200% scaling for the
+ * source
+ */
+ int32_t scaleRel;
+};
+
+static int32_t sCurRelativeScaleStep = std::numeric_limits<int32_t>::max();
+
+static LONG SetRelativeScaleStep(LUID aAdapterId, int32_t aRelativeScaleStep) {
+ DISPLAYCONFIG_SOURCE_DPI_SCALE_SET setDPIScale = {};
+ setDPIScale.header.adapterId = aAdapterId;
+ setDPIScale.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE)
+ DISPLAYCONFIG_DEVICE_INFO_SET_SOURCE_DPI_SCALE;
+ setDPIScale.header.size = sizeof(setDPIScale);
+ setDPIScale.scaleRel = aRelativeScaleStep;
+
+ return DisplayConfigSetDeviceInfo(&setDPIScale.header);
+}
+
+nsresult WinUtils::SetHiDPIMode(bool aHiDPI) {
+ auto config = GetDisplayConfig();
+ if (!config) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (config->mPaths.empty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ DISPLAYCONFIG_SOURCE_DPI_SCALE_GET dpiScale = {};
+ dpiScale.header.adapterId = config->mPaths[0].targetInfo.adapterId;
+ dpiScale.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE)
+ DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_DPI_SCALE;
+ dpiScale.header.size = sizeof(dpiScale);
+ LONG result = ::DisplayConfigGetDeviceInfo(&dpiScale.header);
+ if (result != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (dpiScale.minScaleRel == dpiScale.maxScaleRel) {
+ // We can't change the setting at all.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aHiDPI && dpiScale.curScaleRel == dpiScale.maxScaleRel) {
+ // We've already at the maximum level.
+ if (sCurRelativeScaleStep == std::numeric_limits<int32_t>::max()) {
+ sCurRelativeScaleStep = dpiScale.curScaleRel;
+ }
+ return NS_OK;
+ }
+
+ if (!aHiDPI && dpiScale.curScaleRel == dpiScale.minScaleRel) {
+ // We've already at the minimum level.
+ if (sCurRelativeScaleStep == std::numeric_limits<int32_t>::max()) {
+ sCurRelativeScaleStep = dpiScale.curScaleRel;
+ }
+ return NS_OK;
+ }
+
+ result = SetRelativeScaleStep(
+ config->mPaths[0].targetInfo.adapterId,
+ aHiDPI ? dpiScale.maxScaleRel : dpiScale.minScaleRel);
+ if (result != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (sCurRelativeScaleStep == std::numeric_limits<int>::max()) {
+ sCurRelativeScaleStep = dpiScale.curScaleRel;
+ }
+
+ return NS_OK;
+}
+
+nsresult WinUtils::RestoreHiDPIMode() {
+ if (sCurRelativeScaleStep == std::numeric_limits<int>::max()) {
+ // The DPI setting hasn't been changed.
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ auto config = GetDisplayConfig();
+ if (!config) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (config->mPaths.empty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ LONG result = SetRelativeScaleStep(config->mPaths[0].targetInfo.adapterId,
+ sCurRelativeScaleStep);
+ sCurRelativeScaleStep = std::numeric_limits<int32_t>::max();
+ if (result != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+#endif
+
+/* static */
+const char* WinUtils::WinEventToEventName(UINT msg) {
+ const auto eventMsgInfo = mozilla::widget::gAllEvents.find(msg);
+ return eventMsgInfo != mozilla::widget::gAllEvents.end()
+ ? eventMsgInfo->second.mStr
+ : nullptr;
+}
+
+// Note to testers and/or test-authors: on Windows 10, and possibly on other
+// versions as well, supplying the `WS_EX_LAYOUTRTL` flag here has no effect
+// whatsoever on child common-dialogs **unless the system UI locale is also set
+// to an RTL language**.
+//
+// If it is, the flag is still required; otherwise, the picker dialog will be
+// presented in English (or possibly some other LTR language) as a fallback.
+ScopedRtlShimWindow::ScopedRtlShimWindow(nsIWidget* aParent) : mWnd(nullptr) {
+ NS_ENSURE_TRUE_VOID(aParent);
+
+ // Headless windows don't have HWNDs, but also probably shouldn't be launching
+ // print dialogs.
+ HWND const hwnd = (HWND)aParent->GetNativeData(NS_NATIVE_WINDOW);
+ NS_ENSURE_TRUE_VOID(hwnd);
+
+ nsWindow* const win = WinUtils::GetNSWindowPtr(hwnd);
+ NS_ENSURE_TRUE_VOID(win);
+
+ ATOM const wclass = ::GetClassWord(hwnd, GCW_ATOM);
+ mWnd = ::CreateWindowExW(
+ win->IsRTL() ? WS_EX_LAYOUTRTL : 0, (LPCWSTR)(uintptr_t)wclass, L"",
+ WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+ hwnd, nullptr, nsToolkit::mDllInstance, nullptr);
+
+ MOZ_ASSERT(mWnd);
+}
+
+ScopedRtlShimWindow::~ScopedRtlShimWindow() {
+ if (mWnd) {
+ ::DestroyWindow(mWnd);
+ }
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WinUtils.h b/widget/windows/WinUtils.h
new file mode 100644
index 0000000000..daef5ff4bc
--- /dev/null
+++ b/widget/windows/WinUtils.h
@@ -0,0 +1,684 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_WinUtils_h__
+#define mozilla_widget_WinUtils_h__
+
+#include "nscore.h"
+#include <windows.h>
+#include <shobjidl.h>
+#include <uxtheme.h>
+#include <dwmapi.h>
+#include <unordered_map>
+#include <utility>
+
+// Undo the windows.h damage
+#undef GetMessage
+#undef CreateEvent
+#undef GetClassName
+#undef GetBinaryType
+#undef RemoveDirectory
+
+#include "nsString.h"
+#include "nsRegion.h"
+#include "nsRect.h"
+
+#include "nsIRunnable.h"
+#include "nsICryptoHash.h"
+#ifdef MOZ_PLACES
+# include "nsIFaviconService.h"
+#endif
+#include "nsIDownloader.h"
+#include "nsIURI.h"
+#include "nsIWidget.h"
+#include "nsIThread.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/HalScreenConfiguration.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/LazyIdleThread.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Vector.h"
+#include "mozilla/WindowsDpiAwareness.h"
+#include "mozilla/WindowsProcessMitigations.h"
+#include "mozilla/gfx/2D.h"
+
+/**
+ * NS_INLINE_DECL_IUNKNOWN_REFCOUNTING should be used for defining and
+ * implementing AddRef() and Release() of IUnknown interface.
+ * This depends on xpcom/base/nsISupportsImpl.h.
+ */
+
+#define NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(_class) \
+ public: \
+ STDMETHODIMP_(ULONG) AddRef() { \
+ MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(_class) \
+ MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \
+ NS_ASSERT_OWNINGTHREAD(_class); \
+ ++mRefCnt; \
+ NS_LOG_ADDREF(this, mRefCnt, #_class, sizeof(*this)); \
+ return static_cast<ULONG>(mRefCnt.get()); \
+ } \
+ STDMETHODIMP_(ULONG) Release() { \
+ MOZ_ASSERT(int32_t(mRefCnt) > 0, \
+ "Release called on object that has already been released!"); \
+ NS_ASSERT_OWNINGTHREAD(_class); \
+ --mRefCnt; \
+ NS_LOG_RELEASE(this, mRefCnt, #_class); \
+ if (mRefCnt == 0) { \
+ NS_ASSERT_OWNINGTHREAD(_class); \
+ mRefCnt = 1; /* stabilize */ \
+ delete this; \
+ return 0; \
+ } \
+ return static_cast<ULONG>(mRefCnt.get()); \
+ } \
+ \
+ protected: \
+ nsAutoRefCnt mRefCnt; \
+ NS_DECL_OWNINGTHREAD \
+ public:
+
+class nsWindow;
+struct KeyPair;
+
+namespace mozilla {
+enum class PointerCapabilities : uint8_t;
+#if defined(ACCESSIBILITY)
+namespace a11y {
+class LocalAccessible;
+} // namespace a11y
+#endif // defined(ACCESSIBILITY)
+
+// Helper function: enumerate all the toplevel HWNDs attached to the current
+// thread via ::EnumThreadWindows().
+//
+// Note that this use of ::EnumThreadWindows() is, unfortunately, not an
+// abstract implementation detail.
+template <typename F>
+void EnumerateThreadWindows(F&& f)
+// requires requires(F f, HWND h) { f(h); }
+{
+ class Impl {
+ public:
+ F f;
+ explicit Impl(F&& f) : f(std::forward<F>(f)) {}
+
+ void invoke() {
+ WNDENUMPROC proc = &Impl::Callback;
+ ::EnumThreadWindows(::GetCurrentThreadId(), proc,
+ reinterpret_cast<LPARAM>(&f));
+ }
+
+ private:
+ static BOOL CALLBACK Callback(HWND hwnd, LPARAM lp) {
+ (*reinterpret_cast<F*>(lp))(hwnd);
+ return TRUE;
+ }
+ };
+
+ Impl(std::forward<F>(f)).invoke();
+}
+
+namespace widget {
+
+// More complete QS definitions for MsgWaitForMultipleObjects() and
+// GetQueueStatus() that include newer win8 specific defines.
+
+#ifndef QS_RAWINPUT
+# define QS_RAWINPUT 0x0400
+#endif
+
+#ifndef QS_TOUCH
+# define QS_TOUCH 0x0800
+# define QS_POINTER 0x1000
+#endif
+
+#define MOZ_QS_ALLEVENT \
+ (QS_KEY | QS_MOUSEMOVE | QS_MOUSEBUTTON | QS_POSTMESSAGE | QS_TIMER | \
+ QS_PAINT | QS_SENDMESSAGE | QS_HOTKEY | QS_ALLPOSTMESSAGE | QS_RAWINPUT | \
+ QS_TOUCH | QS_POINTER)
+
+// Logging macros
+#define LogFunction() mozilla::widget::WinUtils::Log(__FUNCTION__)
+#define LogThread() \
+ mozilla::widget::WinUtils::Log("%s: IsMainThread:%d ThreadId:%X", \
+ __FUNCTION__, NS_IsMainThread(), \
+ GetCurrentThreadId())
+#define LogThis() mozilla::widget::WinUtils::Log("[%X] %s", this, __FUNCTION__)
+#define LogException(e) \
+ mozilla::widget::WinUtils::Log("%s Exception:%s", __FUNCTION__, \
+ e->ToString()->Data())
+#define LogHRESULT(hr) \
+ mozilla::widget::WinUtils::Log("%s hr=%X", __FUNCTION__, hr)
+
+#ifdef MOZ_PLACES
+class myDownloadObserver final : public nsIDownloadObserver {
+ ~myDownloadObserver() {}
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOWNLOADOBSERVER
+};
+#endif
+
+class WinUtils {
+ // Function pointers for APIs that may not be available depending on
+ // the Win10 update version -- will be set up in Initialize().
+ static SetThreadDpiAwarenessContextProc sSetThreadDpiAwarenessContext;
+ static EnableNonClientDpiScalingProc sEnableNonClientDpiScaling;
+ static GetSystemMetricsForDpiProc sGetSystemMetricsForDpi;
+
+ // Set on Initialize().
+ static bool sHasPackageIdentity;
+
+ public:
+ class AutoSystemDpiAware {
+ public:
+ AutoSystemDpiAware() {
+ MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown());
+
+ if (sSetThreadDpiAwarenessContext) {
+ mPrevContext =
+ sSetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
+ }
+ }
+
+ ~AutoSystemDpiAware() {
+ if (sSetThreadDpiAwarenessContext) {
+ sSetThreadDpiAwarenessContext(mPrevContext);
+ }
+ }
+
+ private:
+ DPI_AWARENESS_CONTEXT mPrevContext;
+ };
+
+ // Wrapper for DefWindowProc that will enable non-client dpi scaling on the
+ // window during creation.
+ static LRESULT WINAPI NonClientDpiScalingDefWindowProcW(HWND hWnd, UINT msg,
+ WPARAM wParam,
+ LPARAM lParam);
+
+ /**
+ * Get the system's default logical-to-physical DPI scaling factor,
+ * which is based on the primary display. Note however that unlike
+ * LogToPhysFactor(GetPrimaryMonitor()), this will not change during
+ * a session even if the displays are reconfigured. This scale factor
+ * is used by Windows theme metrics etc, which do not fully support
+ * dynamic resolution changes but are only updated on logout.
+ */
+ static double SystemScaleFactor();
+
+ static bool IsPerMonitorDPIAware();
+ /**
+ * Get the DPI of the given monitor if it's per-monitor DPI aware, otherwise
+ * return the system DPI.
+ */
+ static float MonitorDPI(HMONITOR aMonitor);
+ static float SystemDPI();
+ /**
+ * Functions to convert between logical pixels as used by most Windows APIs
+ * and physical (device) pixels.
+ */
+ static double LogToPhysFactor(HMONITOR aMonitor);
+ static double LogToPhysFactor(HWND aWnd);
+ static double LogToPhysFactor(HDC aDC) {
+ return LogToPhysFactor(::WindowFromDC(aDC));
+ }
+ static int32_t LogToPhys(HMONITOR aMonitor, double aValue);
+ static HMONITOR GetPrimaryMonitor();
+ static HMONITOR MonitorFromRect(const gfx::Rect& rect);
+
+ static bool HasSystemMetricsForDpi();
+ static int GetSystemMetricsForDpi(int nIndex, UINT dpi);
+
+ /**
+ * @param msg Windows event message
+ * @return User-friendly event name, or nullptr if no
+ * match is found.
+ */
+ static const char* WinEventToEventName(UINT msg);
+
+ /**
+ * @param aHdc HDC for printer
+ * @return unwritable margins for currently set page on aHdc or empty margins
+ * if aHdc is null
+ */
+ static gfx::MarginDouble GetUnwriteableMarginsForDeviceInInches(HDC aHdc);
+
+ static bool HasPackageIdentity() { return sHasPackageIdentity; }
+
+ /*
+ * The "family name" of a Windows app package is the full name without any of
+ * the components that might change during the life cycle of the app (such as
+ * the version number, or the architecture). This leaves only those properties
+ * which together serve to uniquely identify the app within one Windows
+ * installation, namely the base name and the publisher name. Meaning, this
+ * string is safe to use anywhere that a string uniquely identifying an app
+ * installation is called for (because multiple copies of the same app on the
+ * same system is not a supported feature in the app framework).
+ */
+ static nsString GetPackageFamilyName();
+
+ /**
+ * Logging helpers that dump output to prlog module 'Widget', console, and
+ * OutputDebugString. Note these output in both debug and release builds.
+ */
+ static void Log(const char* fmt, ...);
+ static void LogW(const wchar_t* fmt, ...);
+
+ /**
+ * PeekMessage() and GetMessage() are wrapper methods for PeekMessageW(),
+ * GetMessageW(), ITfMessageMgr::PeekMessageW() and
+ * ITfMessageMgr::GetMessageW().
+ * Don't call the native APIs directly. You MUST use these methods instead.
+ */
+ static bool PeekMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage,
+ UINT aLastMessage, UINT aOption);
+ static bool GetMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage,
+ UINT aLastMessage);
+
+ /**
+ * Wait until a message is ready to be processed.
+ * Prefer using this method to directly calling ::WaitMessage since
+ * ::WaitMessage will wait if there is an unread message in the queue.
+ * That can cause freezes until another message enters the queue if the
+ * message is marked read by a call to PeekMessage which the caller is
+ * not aware of (e.g., from a different thread).
+ * Note that this method may cause sync dispatch of sent (as opposed to
+ * posted) messages.
+ * @param aTimeoutMs Timeout for waiting in ms, defaults to INFINITE
+ */
+ static void WaitForMessage(DWORD aTimeoutMs = INFINITE);
+
+ /**
+ * GetTopLevelHWND() returns a window handle of the top level window which
+ * aWnd belongs to. Note that the result may not be our window, i.e., it
+ * may not be managed by nsWindow.
+ *
+ * See follwing table for the detail of the result window type.
+ *
+ * +-------------------------+-----------------------------------------------+
+ * | | aStopIfNotPopup |
+ * +-------------------------+-----------------------+-----------------------+
+ * | | TRUE | FALSE |
+ + +-----------------+-------+-----------------------+-----------------------+
+ * | | | * an independent top level window |
+ * | | TRUE | * a pupup window (WS_POPUP) |
+ * | | | * an owned top level window (like dialog) |
+ * | aStopIfNotChild +-------+-----------------------+-----------------------+
+ * | | | * independent window | * only an independent |
+ * | | FALSE | * non-popup-owned- | top level window |
+ * | | | window like dialog | |
+ * +-----------------+-------+-----------------------+-----------------------+
+ */
+ static HWND GetTopLevelHWND(HWND aWnd, bool aStopIfNotChild = false,
+ bool aStopIfNotPopup = true);
+
+ /**
+ * SetNSWindowPtr() associates aWindow with aWnd. If aWidget is nullptr, it
+ * instead dissociates any nsWindow from aWnd.
+ *
+ * No AddRef is performed. May not be used off of the main thread.
+ */
+ static void SetNSWindowPtr(HWND aWnd, nsWindow* aWindow);
+ /**
+ * GetNSWindowPtr() returns a pointer to the associated nsWindow pointer, if
+ * one exists, or nullptr, if not.
+ *
+ * No AddRef is performed. May not be used off of the main thread.
+ */
+ static nsWindow* GetNSWindowPtr(HWND aWnd);
+
+ /**
+ * IsOurProcessWindow() returns TRUE if aWnd belongs our process.
+ * Otherwise, FALSE.
+ */
+ static bool IsOurProcessWindow(HWND aWnd);
+
+ /**
+ * FindOurProcessWindow() returns the nearest ancestor window which
+ * belongs to our process. If it fails to find our process's window by the
+ * top level window, returns nullptr. And note that this is using
+ * ::GetParent() for climbing the window hierarchy, therefore, it gives
+ * up at an owned top level window except popup window (e.g., dialog).
+ */
+ static HWND FindOurProcessWindow(HWND aWnd);
+
+ /**
+ * FindOurWindowAtPoint() returns the topmost child window which belongs to
+ * our process's top level window.
+ *
+ * NOTE: the topmost child window may NOT be our process's window like a
+ * plugin's window.
+ */
+ static HWND FindOurWindowAtPoint(const POINT& aPointInScreen);
+
+ /**
+ * InitMSG() returns an MSG struct which was initialized by the params.
+ * Don't trust the other members in the result.
+ */
+ static MSG InitMSG(UINT aMessage, WPARAM wParam, LPARAM lParam, HWND aWnd);
+
+ /**
+ * GetScanCode() returns a scan code for the LPARAM of WM_KEYDOWN, WM_KEYUP,
+ * WM_CHAR and WM_UNICHAR.
+ *
+ */
+ static WORD GetScanCode(LPARAM aLParam) { return (aLParam >> 16) & 0xFF; }
+
+ /**
+ * IsExtendedScanCode() returns TRUE if the LPARAM indicates the key message
+ * is an extended key event.
+ */
+ static bool IsExtendedScanCode(LPARAM aLParam) {
+ return (aLParam & 0x1000000) != 0;
+ }
+
+ /**
+ * GetInternalMessage() converts a native message to an internal message.
+ * If there is no internal message for the given native message, returns
+ * the native message itself.
+ */
+ static UINT GetInternalMessage(UINT aNativeMessage);
+
+ /**
+ * GetNativeMessage() converts an internal message to a native message.
+ * If aInternalMessage is a native message, returns the native message itself.
+ */
+ static UINT GetNativeMessage(UINT aInternalMessage);
+
+ /**
+ * GetMouseInputSource() returns a pointing device information. The value is
+ * one of MouseEvent_Binding::MOZ_SOURCE_*. This method MUST be called during
+ * mouse message handling.
+ */
+ static uint16_t GetMouseInputSource();
+
+ /**
+ * Windows also fires mouse window messages for pens and touches, so we should
+ * retrieve their pointer ID on receiving mouse events as well. Please refer
+ * to
+ * https://msdn.microsoft.com/en-us/library/windows/desktop/ms703320(v=vs.85).aspx
+ */
+ static uint16_t GetMousePointerID();
+
+ static bool GetIsMouseFromTouch(EventMessage aEventType);
+
+ /**
+ * ConvertHRGNToRegion converts a Windows HRGN to an LayoutDeviceIntRegion.
+ *
+ * aRgn the HRGN to convert.
+ * returns the LayoutDeviceIntRegion.
+ */
+ static LayoutDeviceIntRegion ConvertHRGNToRegion(HRGN aRgn);
+
+ /**
+ * ToIntRect converts a Windows RECT to a LayoutDeviceIntRect.
+ *
+ * aRect the RECT to convert.
+ * returns the LayoutDeviceIntRect.
+ */
+ static LayoutDeviceIntRect ToIntRect(const RECT& aRect);
+
+ /**
+ * Returns true if the context or IME state is enabled. Otherwise, false.
+ */
+ static bool IsIMEEnabled(const InputContext& aInputContext);
+ static bool IsIMEEnabled(IMEEnabled aIMEState);
+
+ /**
+ * Returns modifier key array for aModifiers. This is for
+ * nsIWidget::SynthethizeNative*Event().
+ */
+ static void SetupKeyModifiersSequence(nsTArray<KeyPair>* aArray,
+ uint32_t aModifiers, UINT aMessage);
+
+ /**
+ * Does device have touch support
+ */
+ static uint32_t IsTouchDeviceSupportPresent();
+
+ /**
+ * The maximum number of simultaneous touch contacts supported by the device.
+ * In the case of devices with multiple digitizers (e.g. multiple touch
+ * screens), the value will be the maximum of the set of maximum supported
+ * contacts by each individual digitizer.
+ */
+ static uint32_t GetMaxTouchPoints();
+
+ /**
+ * Returns the windows power platform role, which is useful for detecting
+ * tablets.
+ */
+ static POWER_PLATFORM_ROLE GetPowerPlatformRole();
+
+ // For pointer and hover media queries features.
+ static PointerCapabilities GetPrimaryPointerCapabilities();
+ // For any-pointer and any-hover media queries features.
+ static PointerCapabilities GetAllPointerCapabilities();
+ // Returns a string containing a comma-separated list of Fluent IDs
+ // representing the currently active pointing devices
+ static void GetPointerExplanation(nsAString* aExplanation);
+
+ /**
+ * Fully resolves a path to its final path name. So if path contains
+ * junction points or symlinks to other folders, we'll resolve the path
+ * fully to the actual path that the links target.
+ *
+ * @param aPath path to be resolved.
+ * @return true if successful, including if nothing needs to be changed.
+ * false if something failed or aPath does not exist, aPath will
+ * remain unchanged.
+ */
+ static bool ResolveJunctionPointsAndSymLinks(std::wstring& aPath);
+ static bool ResolveJunctionPointsAndSymLinks(nsIFile* aPath);
+
+ /**
+ * Returns true if executable's path is on a network drive.
+ */
+ static bool RunningFromANetworkDrive();
+
+ static void Initialize();
+
+ static nsresult WriteBitmap(nsIFile* aFile,
+ mozilla::gfx::SourceSurface* surface);
+ // This function is a helper, but it cannot be called from the main thread.
+ // Use the one above!
+ static nsresult WriteBitmap(nsIFile* aFile, imgIContainer* aImage);
+
+ /**
+ * Wrapper for PathCanonicalize().
+ * Upon success, the resulting output string length is <= MAX_PATH.
+ * @param aPath [in,out] The path to transform.
+ * @return true on success, false on failure.
+ */
+ static bool CanonicalizePath(nsAString& aPath);
+
+ /**
+ * Converts short paths (e.g. "C:\\PROGRA~1\\XYZ") to full paths.
+ * Upon success, the resulting output string length is <= MAX_PATH.
+ * @param aPath [in,out] The path to transform.
+ * @return true on success, false on failure.
+ */
+ static bool MakeLongPath(nsAString& aPath);
+
+ /**
+ * Wrapper for PathUnExpandEnvStringsW().
+ * Upon success, the resulting output string length is <= MAX_PATH.
+ * @param aPath [in,out] The path to transform.
+ * @return true on success, false on failure.
+ */
+ static bool UnexpandEnvVars(nsAString& aPath);
+
+ /**
+ * Retrieve a semicolon-delimited list of DLL files derived from AppInit_DLLs
+ */
+ static bool GetAppInitDLLs(nsAString& aOutput);
+
+ enum class PathTransformFlags : uint32_t {
+ Canonicalize = 1,
+ Lengthen = 2,
+ UnexpandEnvVars = 4,
+ RequireFilePath = 8,
+
+ Default = 7, // Default omits RequireFilePath
+ };
+
+ /**
+ * Given a path, transforms it in preparation to be reported via telemetry.
+ * That can include canonicalization, converting short to long paths,
+ * unexpanding environment strings, and removing potentially sensitive data
+ * from the path.
+ *
+ * @param aPath [in,out] The path to transform.
+ * @param aFlags [in] Specifies which transformations to perform, allowing
+ * the caller to skip operations they know have already been
+ * performed.
+ * @return true on success, false on failure.
+ */
+ static bool PreparePathForTelemetry(
+ nsAString& aPath,
+ PathTransformFlags aFlags = PathTransformFlags::Default);
+
+ static const size_t kMaxWhitelistedItems = 3;
+ using WhitelistVec =
+ Vector<std::pair<nsString, nsDependentString>, kMaxWhitelistedItems>;
+
+ static const WhitelistVec& GetWhitelistedPaths();
+
+ static bool GetClassName(HWND aHwnd, nsAString& aName);
+
+ static void EnableWindowOcclusion(const bool aEnable);
+
+ static bool GetTimezoneName(wchar_t* aBuffer);
+
+#ifdef DEBUG
+ static nsresult SetHiDPIMode(bool aHiDPI);
+ static nsresult RestoreHiDPIMode();
+#endif
+
+ static bool GetAutoRotationState(AR_STATE* aRotationState);
+
+ static void GetClipboardFormatAsString(UINT aFormat, nsAString& aOutput);
+
+ private:
+ static WhitelistVec BuildWhitelist();
+
+ public:
+#ifdef ACCESSIBILITY
+ static a11y::LocalAccessible* GetRootAccessibleForHWND(HWND aHwnd);
+#endif
+};
+
+#ifdef MOZ_PLACES
+class AsyncFaviconDataReady final : public nsIFaviconDataCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFAVICONDATACALLBACK
+
+ AsyncFaviconDataReady(nsIURI* aNewURI, RefPtr<LazyIdleThread>& aIOThread,
+ const bool aURLShortcut,
+ already_AddRefed<nsIRunnable> aRunnable);
+ nsresult OnFaviconDataNotAvailable(void);
+
+ private:
+ ~AsyncFaviconDataReady() {}
+
+ nsCOMPtr<nsIURI> mNewURI;
+ RefPtr<LazyIdleThread> mIOThread;
+ nsCOMPtr<nsIRunnable> mRunnable;
+ const bool mURLShortcut;
+};
+#endif
+
+/**
+ * Asynchronously tries add the list to the build
+ */
+class AsyncEncodeAndWriteIcon : public nsIRunnable {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIRUNNABLE
+
+ // Warning: AsyncEncodeAndWriteIcon assumes ownership of the aData buffer
+ // passed in
+ AsyncEncodeAndWriteIcon(const nsAString& aIconPath,
+ UniquePtr<uint8_t[]> aData, uint32_t aStride,
+ uint32_t aWidth, uint32_t aHeight,
+ already_AddRefed<nsIRunnable> aRunnable);
+
+ private:
+ virtual ~AsyncEncodeAndWriteIcon();
+
+ nsAutoString mIconPath;
+ UniquePtr<uint8_t[]> mBuffer;
+ nsCOMPtr<nsIRunnable> mRunnable;
+ uint32_t mStride;
+ uint32_t mWidth;
+ uint32_t mHeight;
+};
+
+class AsyncDeleteAllFaviconsFromDisk : public nsIRunnable {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIRUNNABLE
+
+ explicit AsyncDeleteAllFaviconsFromDisk(bool aIgnoreRecent = false);
+
+ private:
+ virtual ~AsyncDeleteAllFaviconsFromDisk();
+
+ int32_t mIcoNoDeleteSeconds;
+ bool mIgnoreRecent;
+ nsCOMPtr<nsIFile> mJumpListCacheDir;
+};
+
+class FaviconHelper {
+ public:
+ static const char kJumpListCacheDir[];
+ static const char kShortcutCacheDir[];
+ static nsresult ObtainCachedIconFile(
+ nsCOMPtr<nsIURI> aFaviconPageURI, nsString& aICOFilePath,
+ RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut,
+ already_AddRefed<nsIRunnable> aRunnable = nullptr);
+
+ static nsresult GetOutputIconPath(nsCOMPtr<nsIURI> aFaviconPageURI,
+ nsCOMPtr<nsIFile>& aICOFile,
+ bool aURLShortcut);
+
+ static nsresult CacheIconFileFromFaviconURIAsync(
+ nsCOMPtr<nsIURI> aFaviconPageURI, nsCOMPtr<nsIFile> aICOFile,
+ RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut,
+ already_AddRefed<nsIRunnable> aRunnable);
+
+ static int32_t GetICOCacheSecondsTimeout();
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(WinUtils::PathTransformFlags);
+
+// RTL shim windows are temporary child windows of our nsWindows created to
+// address RTL issues in picker dialogs. (See bug 588735.)
+class ScopedRtlShimWindow {
+ public:
+ explicit ScopedRtlShimWindow(nsIWidget* aParent);
+ ~ScopedRtlShimWindow();
+
+ ScopedRtlShimWindow(const ScopedRtlShimWindow&) = delete;
+ ScopedRtlShimWindow(ScopedRtlShimWindow&& that) noexcept : mWnd(that.mWnd) {
+ that.mWnd = nullptr;
+ };
+
+ HWND get() const { return mWnd; }
+
+ private:
+ HWND mWnd;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_WinUtils_h__
diff --git a/widget/windows/WinWindowOcclusionTracker.cpp b/widget/windows/WinWindowOcclusionTracker.cpp
new file mode 100644
index 0000000000..a38f950585
--- /dev/null
+++ b/widget/windows/WinWindowOcclusionTracker.cpp
@@ -0,0 +1,1471 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 <queue>
+#include <windows.h>
+#include <winuser.h>
+#include <wtsapi32.h>
+
+#include "WinWindowOcclusionTracker.h"
+
+#include "base/thread.h"
+#include "base/message_loop.h"
+#include "base/platform_thread.h"
+#include "gfxConfig.h"
+#include "nsThreadUtils.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/StaticPtr.h"
+#include "nsBaseWidget.h"
+#include "nsWindow.h"
+#include "transport/runnable_utils.h"
+#include "WinUtils.h"
+
+namespace mozilla::widget {
+
+// Can be called on Main thread
+LazyLogModule gWinOcclusionTrackerLog("WinOcclusionTracker");
+#define LOG(type, ...) MOZ_LOG(gWinOcclusionTrackerLog, type, (__VA_ARGS__))
+
+// Can be called on OcclusionCalculator thread
+LazyLogModule gWinOcclusionCalculatorLog("WinOcclusionCalculator");
+#define CALC_LOG(type, ...) \
+ MOZ_LOG(gWinOcclusionCalculatorLog, type, (__VA_ARGS__))
+
+// ~16 ms = time between frames when frame rate is 60 FPS.
+const int kOcclusionUpdateRunnableDelayMs = 16;
+
+class OcclusionUpdateRunnable : public CancelableRunnable {
+ public:
+ explicit OcclusionUpdateRunnable(
+ WinWindowOcclusionTracker::WindowOcclusionCalculator*
+ aOcclusionCalculator)
+ : CancelableRunnable("OcclusionUpdateRunnable"),
+ mOcclusionCalculator(aOcclusionCalculator) {
+ mTimeStamp = TimeStamp::Now();
+ }
+
+ NS_IMETHOD Run() override {
+ if (mIsCanceled) {
+ return NS_OK;
+ }
+ MOZ_ASSERT(WinWindowOcclusionTracker::IsInWinWindowOcclusionThread());
+
+ uint32_t latencyMs =
+ round((TimeStamp::Now() - mTimeStamp).ToMilliseconds());
+ CALC_LOG(LogLevel::Debug,
+ "ComputeNativeWindowOcclusionStatus() latencyMs %u", latencyMs);
+
+ mOcclusionCalculator->ComputeNativeWindowOcclusionStatus();
+ return NS_OK;
+ }
+
+ nsresult Cancel() override {
+ mIsCanceled = true;
+ mOcclusionCalculator = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ bool mIsCanceled = false;
+ RefPtr<WinWindowOcclusionTracker::WindowOcclusionCalculator>
+ mOcclusionCalculator;
+ TimeStamp mTimeStamp;
+};
+
+// Used to serialize tasks related to mRootWindowHwndsOcclusionState.
+class SerializedTaskDispatcher {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SerializedTaskDispatcher)
+
+ public:
+ SerializedTaskDispatcher();
+
+ void Destroy();
+ void PostTaskToMain(already_AddRefed<nsIRunnable> aTask);
+ void PostTaskToCalculator(already_AddRefed<nsIRunnable> aTask);
+ void PostDelayedTaskToCalculator(already_AddRefed<Runnable> aTask,
+ int aDelayMs);
+ bool IsOnCurrentThread();
+
+ private:
+ friend class DelayedTaskRunnable;
+
+ ~SerializedTaskDispatcher();
+
+ struct Data {
+ std::queue<std::pair<RefPtr<nsIRunnable>, RefPtr<nsISerialEventTarget>>>
+ mTasks;
+ bool mDestroyed = false;
+ RefPtr<Runnable> mCurrentRunnable;
+ };
+
+ void PostTasksIfNecessary(nsISerialEventTarget* aEventTarget,
+ const DataMutex<Data>::AutoLock& aProofOfLock);
+ void HandleDelayedTask(already_AddRefed<nsIRunnable> aTask);
+ void HandleTasks();
+
+ // Hold current EventTarget during calling nsIRunnable::Run().
+ RefPtr<nsISerialEventTarget> mCurrentEventTarget = nullptr;
+
+ DataMutex<Data> mData;
+};
+
+class DelayedTaskRunnable : public Runnable {
+ public:
+ DelayedTaskRunnable(SerializedTaskDispatcher* aSerializedTaskDispatcher,
+ already_AddRefed<Runnable> aTask)
+ : Runnable("DelayedTaskRunnable"),
+ mSerializedTaskDispatcher(aSerializedTaskDispatcher),
+ mTask(aTask) {}
+
+ NS_IMETHOD Run() override {
+ mSerializedTaskDispatcher->HandleDelayedTask(mTask.forget());
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<SerializedTaskDispatcher> mSerializedTaskDispatcher;
+ RefPtr<Runnable> mTask;
+};
+
+SerializedTaskDispatcher::SerializedTaskDispatcher()
+ : mData("SerializedTaskDispatcher::mData") {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info,
+ "SerializedTaskDispatcher::SerializedTaskDispatcher() this %p", this);
+}
+
+SerializedTaskDispatcher::~SerializedTaskDispatcher() {
+#ifdef DEBUG
+ auto data = mData.Lock();
+ MOZ_ASSERT(data->mDestroyed);
+ MOZ_ASSERT(data->mTasks.empty());
+#endif
+}
+
+void SerializedTaskDispatcher::Destroy() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info, "SerializedTaskDispatcher::Destroy() this %p", this);
+
+ auto data = mData.Lock();
+ if (data->mDestroyed) {
+ return;
+ }
+
+ data->mDestroyed = true;
+ std::queue<std::pair<RefPtr<nsIRunnable>, RefPtr<nsISerialEventTarget>>>
+ empty;
+ std::swap(data->mTasks, empty);
+}
+
+void SerializedTaskDispatcher::PostTaskToMain(
+ already_AddRefed<nsIRunnable> aTask) {
+ RefPtr<nsIRunnable> task = aTask;
+
+ auto data = mData.Lock();
+ if (data->mDestroyed) {
+ return;
+ }
+
+ nsISerialEventTarget* eventTarget = GetMainThreadSerialEventTarget();
+ data->mTasks.push({std::move(task), eventTarget});
+
+ MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1);
+ PostTasksIfNecessary(eventTarget, data);
+}
+
+void SerializedTaskDispatcher::PostTaskToCalculator(
+ already_AddRefed<nsIRunnable> aTask) {
+ RefPtr<nsIRunnable> task = aTask;
+
+ auto data = mData.Lock();
+ if (data->mDestroyed) {
+ return;
+ }
+
+ nsISerialEventTarget* eventTarget =
+ WinWindowOcclusionTracker::OcclusionCalculatorLoop()->SerialEventTarget();
+ data->mTasks.push({std::move(task), eventTarget});
+
+ MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1);
+ PostTasksIfNecessary(eventTarget, data);
+}
+
+void SerializedTaskDispatcher::PostDelayedTaskToCalculator(
+ already_AddRefed<Runnable> aTask, int aDelayMs) {
+ CALC_LOG(LogLevel::Debug,
+ "SerializedTaskDispatcher::PostDelayedTaskToCalculator()");
+
+ RefPtr<DelayedTaskRunnable> runnable =
+ new DelayedTaskRunnable(this, std::move(aTask));
+ MessageLoop* targetLoop =
+ WinWindowOcclusionTracker::OcclusionCalculatorLoop();
+ targetLoop->PostDelayedTask(runnable.forget(), aDelayMs);
+}
+
+bool SerializedTaskDispatcher::IsOnCurrentThread() {
+ return !!mCurrentEventTarget;
+}
+
+void SerializedTaskDispatcher::PostTasksIfNecessary(
+ nsISerialEventTarget* aEventTarget,
+ const DataMutex<Data>::AutoLock& aProofOfLock) {
+ MOZ_ASSERT(!aProofOfLock->mTasks.empty());
+
+ if (aProofOfLock->mCurrentRunnable) {
+ return;
+ }
+
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<SerializedTaskDispatcher>(this),
+ &SerializedTaskDispatcher::HandleTasks);
+ aProofOfLock->mCurrentRunnable = runnable;
+ aEventTarget->Dispatch(runnable.forget());
+}
+
+void SerializedTaskDispatcher::HandleDelayedTask(
+ already_AddRefed<nsIRunnable> aTask) {
+ MOZ_ASSERT(WinWindowOcclusionTracker::IsInWinWindowOcclusionThread());
+ CALC_LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleDelayedTask()");
+
+ RefPtr<nsIRunnable> task = aTask;
+
+ auto data = mData.Lock();
+ if (data->mDestroyed) {
+ return;
+ }
+
+ nsISerialEventTarget* eventTarget =
+ WinWindowOcclusionTracker::OcclusionCalculatorLoop()->SerialEventTarget();
+ data->mTasks.push({std::move(task), eventTarget});
+
+ MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1);
+ PostTasksIfNecessary(eventTarget, data);
+}
+
+void SerializedTaskDispatcher::HandleTasks() {
+ RefPtr<nsIRunnable> frontTask;
+
+ // Get front task
+ {
+ auto data = mData.Lock();
+ if (data->mDestroyed) {
+ return;
+ }
+ MOZ_RELEASE_ASSERT(data->mCurrentRunnable);
+ MOZ_RELEASE_ASSERT(!data->mTasks.empty());
+
+ frontTask = data->mTasks.front().first;
+
+ MOZ_RELEASE_ASSERT(!mCurrentEventTarget);
+ mCurrentEventTarget = data->mTasks.front().second;
+ }
+
+ while (frontTask) {
+ if (NS_IsMainThread()) {
+ LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleTasks()");
+ } else {
+ CALC_LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleTasks()");
+ }
+
+ MOZ_ASSERT_IF(NS_IsMainThread(),
+ mCurrentEventTarget == GetMainThreadSerialEventTarget());
+ MOZ_ASSERT_IF(
+ !NS_IsMainThread(),
+ mCurrentEventTarget == MessageLoop::current()->SerialEventTarget());
+
+ frontTask->Run();
+
+ // Get next task
+ {
+ auto data = mData.Lock();
+ if (data->mDestroyed) {
+ return;
+ }
+
+ frontTask = nullptr;
+ data->mTasks.pop();
+ // Check if next task could be handled on current thread
+ if (!data->mTasks.empty() &&
+ data->mTasks.front().second == mCurrentEventTarget) {
+ frontTask = data->mTasks.front().first;
+ }
+ }
+ }
+
+ MOZ_ASSERT(!frontTask);
+
+ // Post tasks to different thread if pending tasks exist.
+ {
+ auto data = mData.Lock();
+ data->mCurrentRunnable = nullptr;
+ mCurrentEventTarget = nullptr;
+
+ if (data->mDestroyed || data->mTasks.empty()) {
+ return;
+ }
+
+ PostTasksIfNecessary(data->mTasks.front().second, data);
+ }
+}
+
+// static
+StaticRefPtr<WinWindowOcclusionTracker> WinWindowOcclusionTracker::sTracker;
+
+/* static */
+WinWindowOcclusionTracker* WinWindowOcclusionTracker::Get() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sTracker || sTracker->mHasAttemptedShutdown) {
+ return nullptr;
+ }
+ return sTracker;
+}
+
+/* static */
+void WinWindowOcclusionTracker::Ensure() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info, "WinWindowOcclusionTracker::Ensure()");
+
+ base::Thread::Options options;
+ options.message_loop_type = MessageLoop::TYPE_UI;
+
+ if (sTracker) {
+ // Try to reuse the thread, which involves stopping and restarting it.
+ sTracker->mThread->Stop();
+ if (sTracker->mThread->StartWithOptions(options)) {
+ // Success!
+ sTracker->mHasAttemptedShutdown = false;
+
+ // Take this opportunity to ensure that mDisplayStatusObserver and
+ // mSessionChangeObserver exist. They might have failed to be
+ // created when sTracker was created.
+ sTracker->EnsureDisplayStatusObserver();
+ sTracker->EnsureSessionChangeObserver();
+ return;
+ }
+ // Restart failed, so null out our sTracker and try again with a new
+ // thread. This will cause the old singleton instance to be deallocated,
+ // which will destroy its mThread as well.
+ sTracker = nullptr;
+ }
+
+ UniquePtr<base::Thread> thread =
+ MakeUnique<base::Thread>("WinWindowOcclusionCalc");
+
+ if (!thread->StartWithOptions(options)) {
+ return;
+ }
+
+ sTracker = new WinWindowOcclusionTracker(std::move(thread));
+ WindowOcclusionCalculator::CreateInstance();
+
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<WindowOcclusionCalculator>(
+ WindowOcclusionCalculator::GetInstance()),
+ &WindowOcclusionCalculator::Initialize);
+ sTracker->mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget());
+}
+
+/* static */
+void WinWindowOcclusionTracker::ShutDown() {
+ if (!sTracker) {
+ return;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info, "WinWindowOcclusionTracker::ShutDown()");
+
+ sTracker->mHasAttemptedShutdown = true;
+ sTracker->Destroy();
+
+ // Our thread could hang while we're waiting for it to stop.
+ // Since we're shutting down, that's not a critical problem.
+ // We set a reasonable amount of time to wait for shutdown,
+ // and if it succeeds within that time, we correctly stop
+ // our thread by nulling out the refptr, which will cause it
+ // to be deallocated and join the thread. If it times out,
+ // we do nothing, which means that the thread will not be
+ // joined and sTracker memory will leak.
+ CVStatus status;
+ {
+ // It's important to hold the lock before posting the
+ // runnable. This ensures that the runnable can't begin
+ // until we've started our Wait, which prevents us from
+ // Waiting on a monitor that has already been notified.
+ MonitorAutoLock lock(sTracker->mMonitor);
+
+ static const TimeDuration TIMEOUT = TimeDuration::FromSeconds(2.0);
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<WindowOcclusionCalculator>(
+ WindowOcclusionCalculator::GetInstance()),
+ &WindowOcclusionCalculator::Shutdown);
+ OcclusionCalculatorLoop()->PostTask(runnable.forget());
+
+ // Monitor uses SleepConditionVariableSRW, which can have
+ // spurious wakeups which are reported as timeouts, so we
+ // check timestamps to ensure that we've waited as long we
+ // intended to. If we wake early, we don't bother calculating
+ // a precise amount for the next wait; we just wait the same
+ // amount of time. This means timeout might happen after as
+ // much as 2x the TIMEOUT time.
+ TimeStamp timeStart = TimeStamp::NowLoRes();
+ do {
+ status = sTracker->mMonitor.Wait(TIMEOUT);
+ } while ((status == CVStatus::Timeout) &&
+ ((TimeStamp::NowLoRes() - timeStart) < TIMEOUT));
+ }
+
+ if (status == CVStatus::NoTimeout) {
+ WindowOcclusionCalculator::ClearInstance();
+ sTracker = nullptr;
+ }
+}
+
+void WinWindowOcclusionTracker::Destroy() {
+ if (mDisplayStatusObserver) {
+ mDisplayStatusObserver->Destroy();
+ mDisplayStatusObserver = nullptr;
+ }
+ if (mSessionChangeObserver) {
+ mSessionChangeObserver->Destroy();
+ mSessionChangeObserver = nullptr;
+ }
+ if (mSerializedTaskDispatcher) {
+ mSerializedTaskDispatcher->Destroy();
+ }
+}
+
+/* static */
+MessageLoop* WinWindowOcclusionTracker::OcclusionCalculatorLoop() {
+ return sTracker ? sTracker->mThread->message_loop() : nullptr;
+}
+
+/* static */
+bool WinWindowOcclusionTracker::IsInWinWindowOcclusionThread() {
+ return sTracker &&
+ sTracker->mThread->thread_id() == PlatformThread::CurrentId();
+}
+
+void WinWindowOcclusionTracker::EnsureDisplayStatusObserver() {
+ if (mDisplayStatusObserver) {
+ return;
+ }
+ if (StaticPrefs::
+ widget_windows_window_occlusion_tracking_display_state_enabled()) {
+ mDisplayStatusObserver = DisplayStatusObserver::Create(this);
+ }
+}
+
+void WinWindowOcclusionTracker::EnsureSessionChangeObserver() {
+ if (mSessionChangeObserver) {
+ return;
+ }
+ if (StaticPrefs::
+ widget_windows_window_occlusion_tracking_session_lock_enabled()) {
+ mSessionChangeObserver = SessionChangeObserver::Create(this);
+ }
+}
+
+void WinWindowOcclusionTracker::Enable(nsBaseWidget* aWindow, HWND aHwnd) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info, "WinWindowOcclusionTracker::Enable() aWindow %p aHwnd %p",
+ aWindow, aHwnd);
+
+ auto it = mHwndRootWindowMap.find(aHwnd);
+ if (it != mHwndRootWindowMap.end()) {
+ return;
+ }
+
+ nsWeakPtr weak = do_GetWeakReference(aWindow);
+ mHwndRootWindowMap.emplace(aHwnd, weak);
+
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<WindowOcclusionCalculator>(
+ WindowOcclusionCalculator::GetInstance()),
+ &WindowOcclusionCalculator::EnableOcclusionTrackingForWindow, aHwnd);
+ mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget());
+}
+
+void WinWindowOcclusionTracker::Disable(nsBaseWidget* aWindow, HWND aHwnd) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info,
+ "WinWindowOcclusionTracker::Disable() aWindow %p aHwnd %p", aWindow,
+ aHwnd);
+
+ auto it = mHwndRootWindowMap.find(aHwnd);
+ if (it == mHwndRootWindowMap.end()) {
+ return;
+ }
+
+ mHwndRootWindowMap.erase(it);
+
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<WindowOcclusionCalculator>(
+ WindowOcclusionCalculator::GetInstance()),
+ &WindowOcclusionCalculator::DisableOcclusionTrackingForWindow, aHwnd);
+ mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget());
+}
+
+void WinWindowOcclusionTracker::OnWindowVisibilityChanged(nsBaseWidget* aWindow,
+ bool aVisible) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info,
+ "WinWindowOcclusionTracker::OnWindowVisibilityChanged() aWindow %p "
+ "aVisible %d",
+ aWindow, aVisible);
+
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<WindowOcclusionCalculator>(
+ WindowOcclusionCalculator::GetInstance()),
+ &WindowOcclusionCalculator::HandleVisibilityChanged, aVisible);
+ mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget());
+}
+
+WinWindowOcclusionTracker::WinWindowOcclusionTracker(
+ UniquePtr<base::Thread> aThread)
+ : mThread(std::move(aThread)), mMonitor("WinWindowOcclusionTracker") {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info, "WinWindowOcclusionTracker::WinWindowOcclusionTracker()");
+
+ EnsureDisplayStatusObserver();
+ EnsureSessionChangeObserver();
+
+ mSerializedTaskDispatcher = new SerializedTaskDispatcher();
+}
+
+WinWindowOcclusionTracker::~WinWindowOcclusionTracker() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info,
+ "WinWindowOcclusionTracker::~WinWindowOcclusionTracker()");
+}
+
+// static
+bool WinWindowOcclusionTracker::IsWindowVisibleAndFullyOpaque(
+ HWND aHwnd, LayoutDeviceIntRect* aWindowRect) {
+ // Filter out windows that are not "visible", IsWindowVisible().
+ if (!::IsWindow(aHwnd) || !::IsWindowVisible(aHwnd)) {
+ return false;
+ }
+
+ // Filter out minimized windows.
+ if (::IsIconic(aHwnd)) {
+ return false;
+ }
+
+ LONG exStyles = ::GetWindowLong(aHwnd, GWL_EXSTYLE);
+ // Filter out "transparent" windows, windows where the mouse clicks fall
+ // through them.
+ if (exStyles & WS_EX_TRANSPARENT) {
+ return false;
+ }
+
+ // Filter out "tool windows", which are floating windows that do not appear on
+ // the taskbar or ALT-TAB. Floating windows can have larger window rectangles
+ // than what is visible to the user, so by filtering them out we will avoid
+ // incorrectly marking native windows as occluded. We do not filter out the
+ // Windows Taskbar.
+ if (exStyles & WS_EX_TOOLWINDOW) {
+ nsAutoString className;
+ if (WinUtils::GetClassName(aHwnd, className)) {
+ if (!className.Equals(L"Shell_TrayWnd")) {
+ return false;
+ }
+ }
+ }
+
+ // Filter out layered windows that are not opaque or that set a transparency
+ // colorkey.
+ if (exStyles & WS_EX_LAYERED) {
+ BYTE alpha;
+ DWORD flags;
+
+ // GetLayeredWindowAttributes only works if the application has
+ // previously called SetLayeredWindowAttributes on the window.
+ // The function will fail if the layered window was setup with
+ // UpdateLayeredWindow. Treat this failure as the window being transparent.
+ // See Remarks section of
+ // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getlayeredwindowattributes
+ if (!::GetLayeredWindowAttributes(aHwnd, nullptr, &alpha, &flags)) {
+ return false;
+ }
+
+ if (flags & LWA_ALPHA && alpha < 255) {
+ return false;
+ }
+ if (flags & LWA_COLORKEY) {
+ return false;
+ }
+ }
+
+ // Filter out windows that do not have a simple rectangular region.
+ HRGN region = ::CreateRectRgn(0, 0, 0, 0);
+ int result = GetWindowRgn(aHwnd, region);
+ ::DeleteObject(region);
+ if (result == COMPLEXREGION) {
+ return false;
+ }
+
+ // Windows 10 has cloaked windows, windows with WS_VISIBLE attribute but
+ // not displayed. explorer.exe, in particular has one that's the
+ // size of the desktop. It's usually behind Chrome windows in the z-order,
+ // but using a remote desktop can move it up in the z-order. So, ignore them.
+ DWORD reason;
+ if (SUCCEEDED(::DwmGetWindowAttribute(aHwnd, DWMWA_CLOAKED, &reason,
+ sizeof(reason))) &&
+ reason != 0) {
+ return false;
+ }
+
+ RECT winRect;
+ // Filter out windows that take up zero area. The call to GetWindowRect is one
+ // of the most expensive parts of this function, so it is last.
+ if (!::GetWindowRect(aHwnd, &winRect)) {
+ return false;
+ }
+ if (::IsRectEmpty(&winRect)) {
+ return false;
+ }
+
+ // Ignore popup windows since they're transient unless it is the Windows
+ // Taskbar
+ // XXX Chrome Widget popup handling is removed for now.
+ if (::GetWindowLong(aHwnd, GWL_STYLE) & WS_POPUP) {
+ nsAutoString className;
+ if (WinUtils::GetClassName(aHwnd, className)) {
+ if (!className.Equals(L"Shell_TrayWnd")) {
+ return false;
+ }
+ }
+ }
+
+ *aWindowRect = LayoutDeviceIntRect(winRect.left, winRect.top,
+ winRect.right - winRect.left,
+ winRect.bottom - winRect.top);
+
+ WINDOWPLACEMENT windowPlacement = {0};
+ windowPlacement.length = sizeof(WINDOWPLACEMENT);
+ ::GetWindowPlacement(aHwnd, &windowPlacement);
+ if (windowPlacement.showCmd == SW_MAXIMIZE) {
+ // If the window is maximized the window border extends beyond the visible
+ // region of the screen. Adjust the maximized window rect to fit the
+ // screen dimensions to ensure that fullscreen windows, which do not extend
+ // beyond the screen boundaries since they typically have no borders, will
+ // occlude maximized windows underneath them.
+ HMONITOR hmon = ::MonitorFromWindow(aHwnd, MONITOR_DEFAULTTONEAREST);
+ if (hmon) {
+ MONITORINFO mi;
+ mi.cbSize = sizeof(mi);
+ if (GetMonitorInfo(hmon, &mi)) {
+ LayoutDeviceIntRect workArea(mi.rcWork.left, mi.rcWork.top,
+ mi.rcWork.right - mi.rcWork.left,
+ mi.rcWork.bottom - mi.rcWork.top);
+ // Adjust aWindowRect to fit to monitor.
+ aWindowRect->width = std::min(workArea.width, aWindowRect->width);
+ if (aWindowRect->x < workArea.x) {
+ aWindowRect->x = workArea.x;
+ } else {
+ aWindowRect->x = std::min(workArea.x + workArea.width,
+ aWindowRect->x + aWindowRect->width) -
+ aWindowRect->width;
+ }
+ aWindowRect->height = std::min(workArea.height, aWindowRect->height);
+ if (aWindowRect->y < workArea.y) {
+ aWindowRect->y = workArea.y;
+ } else {
+ aWindowRect->y = std::min(workArea.y + workArea.height,
+ aWindowRect->y + aWindowRect->height) -
+ aWindowRect->height;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+// static
+void WinWindowOcclusionTracker::CallUpdateOcclusionState(
+ std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto* tracker = WinWindowOcclusionTracker::Get();
+ if (!tracker) {
+ return;
+ }
+ tracker->UpdateOcclusionState(aMap, aShowAllWindows);
+}
+
+void WinWindowOcclusionTracker::UpdateOcclusionState(
+ std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread());
+ LOG(LogLevel::Debug,
+ "WinWindowOcclusionTracker::UpdateOcclusionState() aShowAllWindows %d",
+ aShowAllWindows);
+
+ mNumVisibleRootWindows = 0;
+ for (auto& [hwnd, state] : *aMap) {
+ auto it = mHwndRootWindowMap.find(hwnd);
+ // The window was destroyed while processing occlusion.
+ if (it == mHwndRootWindowMap.end()) {
+ continue;
+ }
+ auto occlState = state;
+
+ // If the screen is locked or off, ignore occlusion state results and
+ // mark the window as occluded.
+ if (mScreenLocked || !mDisplayOn) {
+ occlState = OcclusionState::OCCLUDED;
+ } else if (aShowAllWindows) {
+ occlState = OcclusionState::VISIBLE;
+ }
+ nsCOMPtr<nsIWidget> widget = do_QueryReferent(it->second);
+ if (!widget) {
+ continue;
+ }
+ auto* baseWidget = static_cast<nsBaseWidget*>(widget.get());
+ baseWidget->NotifyOcclusionState(occlState);
+ if (baseWidget->SizeMode() != nsSizeMode_Minimized) {
+ mNumVisibleRootWindows++;
+ }
+ }
+}
+
+void WinWindowOcclusionTracker::OnSessionChange(WPARAM aStatusCode,
+ Maybe<bool> aIsCurrentSession) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aIsCurrentSession.isNothing() || !*aIsCurrentSession) {
+ return;
+ }
+
+ if (aStatusCode == WTS_SESSION_UNLOCK) {
+ LOG(LogLevel::Info,
+ "WinWindowOcclusionTracker::OnSessionChange() WTS_SESSION_UNLOCK");
+
+ // UNLOCK will cause a foreground window change, which will
+ // trigger an occlusion calculation on its own.
+ mScreenLocked = false;
+ } else if (aStatusCode == WTS_SESSION_LOCK) {
+ LOG(LogLevel::Info,
+ "WinWindowOcclusionTracker::OnSessionChange() WTS_SESSION_LOCK");
+
+ mScreenLocked = true;
+ MarkNonIconicWindowsOccluded();
+ }
+}
+
+void WinWindowOcclusionTracker::OnDisplayStateChanged(bool aDisplayOn) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info,
+ "WinWindowOcclusionTracker::OnDisplayStateChanged() aDisplayOn %d",
+ aDisplayOn);
+
+ if (mDisplayOn == aDisplayOn) {
+ return;
+ }
+
+ mDisplayOn = aDisplayOn;
+ if (aDisplayOn) {
+ // Notify the window occlusion calculator of the display turning on
+ // which will schedule an occlusion calculation. This must be run
+ // on the WindowOcclusionCalculator thread.
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<WindowOcclusionCalculator>(
+ WindowOcclusionCalculator::GetInstance()),
+ &WindowOcclusionCalculator::HandleVisibilityChanged,
+ /* aVisible */ true);
+ mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget());
+ } else {
+ MarkNonIconicWindowsOccluded();
+ }
+}
+
+void WinWindowOcclusionTracker::MarkNonIconicWindowsOccluded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info,
+ "WinWindowOcclusionTracker::MarkNonIconicWindowsOccluded()");
+
+ // Set all visible root windows as occluded. If not visible,
+ // set them as hidden.
+ for (auto& [hwnd, weak] : mHwndRootWindowMap) {
+ nsCOMPtr<nsIWidget> widget = do_QueryReferent(weak);
+ if (!widget) {
+ continue;
+ }
+ auto* baseWidget = static_cast<nsBaseWidget*>(widget.get());
+ auto state = (baseWidget->SizeMode() == nsSizeMode_Minimized)
+ ? OcclusionState::HIDDEN
+ : OcclusionState::OCCLUDED;
+ baseWidget->NotifyOcclusionState(state);
+ }
+}
+
+void WinWindowOcclusionTracker::TriggerCalculation() {
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<WindowOcclusionCalculator>(
+ WindowOcclusionCalculator::GetInstance()),
+ &WindowOcclusionCalculator::HandleTriggerCalculation);
+ mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget());
+}
+
+// static
+BOOL WinWindowOcclusionTracker::DumpOccludingWindowsCallback(HWND aHWnd,
+ LPARAM aLParam) {
+ HWND hwnd = reinterpret_cast<HWND>(aLParam);
+
+ LayoutDeviceIntRect windowRect;
+ bool windowIsOccluding = IsWindowVisibleAndFullyOpaque(aHWnd, &windowRect);
+ if (windowIsOccluding) {
+ nsAutoString className;
+ if (WinUtils::GetClassName(aHWnd, className)) {
+ const auto name = NS_ConvertUTF16toUTF8(className);
+ printf_stderr(
+ "DumpOccludingWindowsCallback() aHWnd %p className %s windowRect(%d, "
+ "%d, %d, %d)\n",
+ aHWnd, name.get(), windowRect.x, windowRect.y, windowRect.width,
+ windowRect.height);
+ }
+ }
+
+ if (aHWnd == hwnd) {
+ return false;
+ }
+ return true;
+}
+
+void WinWindowOcclusionTracker::DumpOccludingWindows(HWND aHWnd) {
+ printf_stderr("DumpOccludingWindows() until aHWnd %p visible %d iconic %d\n",
+ aHWnd, ::IsWindowVisible(aHWnd), ::IsIconic(aHWnd));
+ ::EnumWindows(&DumpOccludingWindowsCallback, reinterpret_cast<LPARAM>(aHWnd));
+}
+
+// static
+StaticRefPtr<WinWindowOcclusionTracker::WindowOcclusionCalculator>
+ WinWindowOcclusionTracker::WindowOcclusionCalculator::sCalculator;
+
+WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ WindowOcclusionCalculator()
+ : mMonitor(WinWindowOcclusionTracker::Get()->mMonitor) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Info, "WindowOcclusionCalculator()");
+
+ mSerializedTaskDispatcher =
+ WinWindowOcclusionTracker::Get()->GetSerializedTaskDispatcher();
+}
+
+WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ ~WindowOcclusionCalculator() {}
+
+// static
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::CreateInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ sCalculator = new WindowOcclusionCalculator();
+}
+
+// static
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::ClearInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ sCalculator = nullptr;
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::Initialize() {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ MOZ_ASSERT(!mVirtualDesktopManager);
+ CALC_LOG(LogLevel::Info, "Initialize()");
+
+#ifndef __MINGW32__
+ RefPtr<IVirtualDesktopManager> desktopManager;
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_VirtualDesktopManager, NULL, CLSCTX_INPROC_SERVER,
+ __uuidof(IVirtualDesktopManager), getter_AddRefs(desktopManager));
+ if (FAILED(hr)) {
+ return;
+ }
+ mVirtualDesktopManager = desktopManager;
+#endif
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::Shutdown() {
+ MonitorAutoLock lock(mMonitor);
+
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ CALC_LOG(LogLevel::Info, "Shutdown()");
+
+ UnregisterEventHooks();
+ if (mOcclusionUpdateRunnable) {
+ mOcclusionUpdateRunnable->Cancel();
+ mOcclusionUpdateRunnable = nullptr;
+ }
+ mVirtualDesktopManager = nullptr;
+
+ mMonitor.NotifyAll();
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ EnableOcclusionTrackingForWindow(HWND aHwnd) {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread());
+ CALC_LOG(LogLevel::Info, "EnableOcclusionTrackingForWindow() aHwnd %p",
+ aHwnd);
+
+ MOZ_RELEASE_ASSERT(mRootWindowHwndsOcclusionState.find(aHwnd) ==
+ mRootWindowHwndsOcclusionState.end());
+ mRootWindowHwndsOcclusionState[aHwnd] = OcclusionState::UNKNOWN;
+
+ if (mGlobalEventHooks.empty()) {
+ RegisterEventHooks();
+ }
+
+ // Schedule an occlusion calculation so that the newly tracked window does
+ // not have a stale occlusion status.
+ ScheduleOcclusionCalculationIfNeeded();
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ DisableOcclusionTrackingForWindow(HWND aHwnd) {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread());
+ CALC_LOG(LogLevel::Info, "DisableOcclusionTrackingForWindow() aHwnd %p",
+ aHwnd);
+
+ MOZ_RELEASE_ASSERT(mRootWindowHwndsOcclusionState.find(aHwnd) !=
+ mRootWindowHwndsOcclusionState.end());
+ mRootWindowHwndsOcclusionState.erase(aHwnd);
+
+ if (mMovingWindow == aHwnd) {
+ mMovingWindow = 0;
+ }
+
+ if (mRootWindowHwndsOcclusionState.empty()) {
+ UnregisterEventHooks();
+ if (mOcclusionUpdateRunnable) {
+ mOcclusionUpdateRunnable->Cancel();
+ mOcclusionUpdateRunnable = nullptr;
+ }
+ }
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ HandleVisibilityChanged(bool aVisible) {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ CALC_LOG(LogLevel::Info, "HandleVisibilityChange() aVisible %d", aVisible);
+
+ // May have gone from having no visible windows to having one, in
+ // which case we need to register event hooks, and make sure that an
+ // occlusion calculation is scheduled.
+ if (aVisible) {
+ MaybeRegisterEventHooks();
+ ScheduleOcclusionCalculationIfNeeded();
+ }
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ HandleTriggerCalculation() {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ CALC_LOG(LogLevel::Info, "HandleTriggerCalculation()");
+
+ MaybeRegisterEventHooks();
+ ScheduleOcclusionCalculationIfNeeded();
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ MaybeRegisterEventHooks() {
+ if (mGlobalEventHooks.empty()) {
+ RegisterEventHooks();
+ }
+}
+
+// static
+void CALLBACK
+WinWindowOcclusionTracker::WindowOcclusionCalculator::EventHookCallback(
+ HWINEVENTHOOK aWinEventHook, DWORD aEvent, HWND aHwnd, LONG aIdObject,
+ LONG aIdChild, DWORD aEventThread, DWORD aMsEventTime) {
+ if (sCalculator) {
+ sCalculator->ProcessEventHookCallback(aWinEventHook, aEvent, aHwnd,
+ aIdObject, aIdChild);
+ }
+}
+
+// static
+BOOL CALLBACK WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ ComputeNativeWindowOcclusionStatusCallback(HWND aHwnd, LPARAM aLParam) {
+ if (sCalculator) {
+ return sCalculator->ProcessComputeNativeWindowOcclusionStatusCallback(
+ aHwnd, reinterpret_cast<std::unordered_set<DWORD>*>(aLParam));
+ }
+ return FALSE;
+}
+
+// static
+BOOL CALLBACK WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ UpdateVisibleWindowProcessIdsCallback(HWND aHwnd, LPARAM aLParam) {
+ if (sCalculator) {
+ sCalculator->ProcessUpdateVisibleWindowProcessIdsCallback(aHwnd);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ UpdateVisibleWindowProcessIds() {
+ mPidsForLocationChangeHook.clear();
+ ::EnumWindows(&UpdateVisibleWindowProcessIdsCallback, 0);
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ ComputeNativeWindowOcclusionStatus() {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread());
+
+ if (mOcclusionUpdateRunnable) {
+ mOcclusionUpdateRunnable = nullptr;
+ }
+
+ if (mRootWindowHwndsOcclusionState.empty()) {
+ return;
+ }
+
+ // Set up initial conditions for occlusion calculation.
+ bool shouldUnregisterEventHooks = true;
+
+ // Compute the LayoutDeviceIntRegion for the screen.
+ int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN);
+ int screenTop = ::GetSystemMetrics(SM_YVIRTUALSCREEN);
+ int screenWidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
+ int screenHeight = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
+ LayoutDeviceIntRegion screenRegion =
+ LayoutDeviceIntRect(screenLeft, screenTop, screenWidth, screenHeight);
+ mNumRootWindowsWithUnknownOcclusionState = 0;
+
+ CALC_LOG(LogLevel::Debug,
+ "ComputeNativeWindowOcclusionStatus() screen(%d, %d, %d, %d)",
+ screenLeft, screenTop, screenWidth, screenHeight);
+
+ for (auto& [hwnd, state] : mRootWindowHwndsOcclusionState) {
+ // IsIconic() checks for a minimized window. Immediately set the state of
+ // minimized windows to HIDDEN.
+ if (::IsIconic(hwnd)) {
+ state = OcclusionState::HIDDEN;
+ } else if (IsWindowOnCurrentVirtualDesktop(hwnd) == Some(false)) {
+ // If window is not on the current virtual desktop, immediately
+ // set the state of the window to OCCLUDED.
+ state = OcclusionState::OCCLUDED;
+ // Don't unregister event hooks when not on current desktop. There's no
+ // notification when that changes, so we can't reregister event hooks.
+ shouldUnregisterEventHooks = false;
+ } else {
+ state = OcclusionState::UNKNOWN;
+ shouldUnregisterEventHooks = false;
+ mNumRootWindowsWithUnknownOcclusionState++;
+ }
+ }
+
+ // Unregister event hooks if all native windows are minimized.
+ if (shouldUnregisterEventHooks) {
+ UnregisterEventHooks();
+ } else {
+ std::unordered_set<DWORD> currentPidsWithVisibleWindows;
+ mUnoccludedDesktopRegion = screenRegion;
+ // Calculate unoccluded region if there is a non-minimized native window.
+ // Also compute |current_pids_with_visible_windows| as we enumerate
+ // the windows.
+ EnumWindows(&ComputeNativeWindowOcclusionStatusCallback,
+ reinterpret_cast<LPARAM>(&currentPidsWithVisibleWindows));
+ // Check if mPidsForLocationChangeHook has any pids of processes
+ // currently without visible windows. If so, unhook the win event,
+ // remove the pid from mPidsForLocationChangeHook and remove
+ // the corresponding event hook from mProcessEventHooks.
+ std::unordered_set<DWORD> pidsToRemove;
+ for (auto locChangePid : mPidsForLocationChangeHook) {
+ if (currentPidsWithVisibleWindows.find(locChangePid) ==
+ currentPidsWithVisibleWindows.end()) {
+ // Remove the event hook from our map, and unregister the event hook.
+ // It's possible the eventhook will no longer be valid, but if we don't
+ // unregister the event hook, a process that toggles between having
+ // visible windows and not having visible windows could cause duplicate
+ // event hooks to get registered for the process.
+ UnhookWinEvent(mProcessEventHooks[locChangePid]);
+ mProcessEventHooks.erase(locChangePid);
+ pidsToRemove.insert(locChangePid);
+ }
+ }
+ if (!pidsToRemove.empty()) {
+ // XXX simplify
+ for (auto it = mPidsForLocationChangeHook.begin();
+ it != mPidsForLocationChangeHook.end();) {
+ if (pidsToRemove.find(*it) != pidsToRemove.end()) {
+ it = mPidsForLocationChangeHook.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ }
+ }
+
+ std::unordered_map<HWND, OcclusionState>* map =
+ &mRootWindowHwndsOcclusionState;
+ bool showAllWindows = mShowingThumbnails;
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "CallUpdateOcclusionState", [map, showAllWindows]() {
+ WinWindowOcclusionTracker::CallUpdateOcclusionState(map,
+ showAllWindows);
+ });
+ mSerializedTaskDispatcher->PostTaskToMain(runnable.forget());
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ ScheduleOcclusionCalculationIfNeeded() {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+
+ // OcclusionUpdateRunnable is already queued.
+ if (mOcclusionUpdateRunnable) {
+ return;
+ }
+
+ CALC_LOG(LogLevel::Debug, "ScheduleOcclusionCalculationIfNeeded()");
+
+ RefPtr<CancelableRunnable> task = new OcclusionUpdateRunnable(this);
+ mOcclusionUpdateRunnable = task;
+ mSerializedTaskDispatcher->PostDelayedTaskToCalculator(
+ task.forget(), kOcclusionUpdateRunnableDelayMs);
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ RegisterGlobalEventHook(DWORD aEventMin, DWORD aEventMax) {
+ HWINEVENTHOOK eventHook =
+ ::SetWinEventHook(aEventMin, aEventMax, nullptr, &EventHookCallback, 0, 0,
+ WINEVENT_OUTOFCONTEXT);
+ mGlobalEventHooks.push_back(eventHook);
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ RegisterEventHookForProcess(DWORD aPid) {
+ mPidsForLocationChangeHook.insert(aPid);
+ mProcessEventHooks[aPid] = SetWinEventHook(
+ EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, nullptr,
+ &EventHookCallback, aPid, 0, WINEVENT_OUTOFCONTEXT);
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ RegisterEventHooks() {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ MOZ_RELEASE_ASSERT(mGlobalEventHooks.empty());
+ CALC_LOG(LogLevel::Info, "RegisterEventHooks()");
+
+ // Detects native window lost mouse capture
+ RegisterGlobalEventHook(EVENT_SYSTEM_CAPTUREEND, EVENT_SYSTEM_CAPTUREEND);
+
+ // Detects native window move (drag) and resizing events.
+ RegisterGlobalEventHook(EVENT_SYSTEM_MOVESIZESTART, EVENT_SYSTEM_MOVESIZEEND);
+
+ // Detects native window minimize and restore from taskbar events.
+ RegisterGlobalEventHook(EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MINIMIZEEND);
+
+ // Detects foreground window changing.
+ RegisterGlobalEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND);
+
+ // Detects objects getting shown and hidden. Used to know when the task bar
+ // and alt tab are showing preview windows so we can unocclude windows.
+ RegisterGlobalEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE);
+
+ // Detects object state changes, e.g., enable/disable state, native window
+ // maximize and native window restore events.
+ RegisterGlobalEventHook(EVENT_OBJECT_STATECHANGE, EVENT_OBJECT_STATECHANGE);
+
+ // Cloaking and uncloaking of windows should trigger an occlusion calculation.
+ // In particular, switching virtual desktops seems to generate these events.
+ RegisterGlobalEventHook(EVENT_OBJECT_CLOAKED, EVENT_OBJECT_UNCLOAKED);
+
+ // Determine which subset of processes to set EVENT_OBJECT_LOCATIONCHANGE on
+ // because otherwise event throughput is very high, as it generates events
+ // for location changes of all objects, including the mouse moving on top of a
+ // window.
+ UpdateVisibleWindowProcessIds();
+ for (DWORD pid : mPidsForLocationChangeHook) {
+ RegisterEventHookForProcess(pid);
+ }
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ UnregisterEventHooks() {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+ CALC_LOG(LogLevel::Info, "UnregisterEventHooks()");
+
+ for (const auto eventHook : mGlobalEventHooks) {
+ ::UnhookWinEvent(eventHook);
+ }
+ mGlobalEventHooks.clear();
+
+ for (const auto& [pid, eventHook] : mProcessEventHooks) {
+ ::UnhookWinEvent(eventHook);
+ }
+ mProcessEventHooks.clear();
+
+ mPidsForLocationChangeHook.clear();
+}
+
+bool WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ ProcessComputeNativeWindowOcclusionStatusCallback(
+ HWND aHwnd, std::unordered_set<DWORD>* aCurrentPidsWithVisibleWindows) {
+ LayoutDeviceIntRegion currUnoccludedDestkop = mUnoccludedDesktopRegion;
+ LayoutDeviceIntRect windowRect;
+ bool windowIsOccluding =
+ WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(aHwnd, &windowRect);
+ if (windowIsOccluding) {
+ // Hook this window's process with EVENT_OBJECT_LOCATION_CHANGE, if we are
+ // not already doing so.
+ DWORD pid;
+ ::GetWindowThreadProcessId(aHwnd, &pid);
+ aCurrentPidsWithVisibleWindows->insert(pid);
+ auto it = mProcessEventHooks.find(pid);
+ if (it == mProcessEventHooks.end()) {
+ RegisterEventHookForProcess(pid);
+ }
+
+ // If no more root windows to consider, return true so we can continue
+ // looking for windows we haven't hooked.
+ if (mNumRootWindowsWithUnknownOcclusionState == 0) {
+ return true;
+ }
+
+ mUnoccludedDesktopRegion.SubOut(windowRect);
+ } else if (mNumRootWindowsWithUnknownOcclusionState == 0) {
+ // This window can't occlude other windows, but we've determined the
+ // occlusion state of all root windows, so we can return.
+ return true;
+ }
+
+ // Ignore moving windows when deciding if windows under it are occluded.
+ if (aHwnd == mMovingWindow) {
+ return true;
+ }
+
+ // Check if |hwnd| is a root window; if so, we're done figuring out
+ // if it's occluded because we've seen all the windows "over" it.
+ auto it = mRootWindowHwndsOcclusionState.find(aHwnd);
+ if (it == mRootWindowHwndsOcclusionState.end() ||
+ it->second != OcclusionState::UNKNOWN) {
+ return true;
+ }
+
+ CALC_LOG(LogLevel::Debug,
+ "ProcessComputeNativeWindowOcclusionStatusCallback() windowRect(%d, "
+ "%d, %d, %d) IsOccluding %d",
+ windowRect.x, windowRect.y, windowRect.width, windowRect.height,
+ windowIsOccluding);
+
+ // On Win7, default theme makes root windows have complex regions by
+ // default. But we can still check if their bounding rect is occluded.
+ if (!windowIsOccluding) {
+ RECT rect;
+ if (::GetWindowRect(aHwnd, &rect) != 0) {
+ LayoutDeviceIntRect windowRect(
+ rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
+ currUnoccludedDestkop.SubOut(windowRect);
+ }
+ }
+
+ it->second = (mUnoccludedDesktopRegion == currUnoccludedDestkop)
+ ? OcclusionState::OCCLUDED
+ : OcclusionState::VISIBLE;
+ mNumRootWindowsWithUnknownOcclusionState--;
+
+ return true;
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ ProcessEventHookCallback(HWINEVENTHOOK aWinEventHook, DWORD aEvent,
+ HWND aHwnd, LONG aIdObject, LONG aIdChild) {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+
+ // No need to calculate occlusion if a zero HWND generated the event. This
+ // happens if there is no window associated with the event, e.g., mouse move
+ // events.
+ if (!aHwnd) {
+ return;
+ }
+
+ // We only care about events for window objects. In particular, we don't care
+ // about OBJID_CARET, which is spammy.
+ if (aIdObject != OBJID_WINDOW) {
+ return;
+ }
+
+ CALC_LOG(LogLevel::Debug,
+ "WindowOcclusionCalculator::ProcessEventHookCallback() aEvent 0x%lx",
+ aEvent);
+
+ // We generally ignore events for popup windows, except for when the taskbar
+ // is hidden or Windows Taskbar, in which case we recalculate occlusion.
+ // XXX Chrome Widget popup handling is removed for now.
+ bool calculateOcclusion = true;
+ if (::GetWindowLong(aHwnd, GWL_STYLE) & WS_POPUP) {
+ nsAutoString className;
+ if (WinUtils::GetClassName(aHwnd, className)) {
+ calculateOcclusion = className.Equals(L"Shell_TrayWnd");
+ }
+ }
+
+ // Detect if either the alt tab view or the task list thumbnail is being
+ // shown. If so, mark all non-hidden windows as occluded, and remember that
+ // we're in the showing_thumbnails state. This lasts until we get told that
+ // either the alt tab view or task list thumbnail are hidden.
+ if (aEvent == EVENT_OBJECT_SHOW) {
+ // Avoid getting the aHwnd's class name, and recomputing occlusion, if not
+ // needed.
+ if (mShowingThumbnails) {
+ return;
+ }
+ nsAutoString className;
+ if (WinUtils::GetClassName(aHwnd, className)) {
+ const auto name = NS_ConvertUTF16toUTF8(className);
+ CALC_LOG(LogLevel::Debug,
+ "ProcessEventHookCallback() EVENT_OBJECT_SHOW %s", name.get());
+
+ if (name.Equals("MultitaskingViewFrame") ||
+ name.Equals("TaskListThumbnailWnd")) {
+ CALC_LOG(LogLevel::Info,
+ "ProcessEventHookCallback() mShowingThumbnails = true");
+ mShowingThumbnails = true;
+
+ std::unordered_map<HWND, OcclusionState>* map =
+ &mRootWindowHwndsOcclusionState;
+ bool showAllWindows = mShowingThumbnails;
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "CallUpdateOcclusionState", [map, showAllWindows]() {
+ WinWindowOcclusionTracker::CallUpdateOcclusionState(
+ map, showAllWindows);
+ });
+ mSerializedTaskDispatcher->PostTaskToMain(runnable.forget());
+ }
+ }
+ return;
+ } else if (aEvent == EVENT_OBJECT_HIDE) {
+ // Avoid getting the aHwnd's class name, and recomputing occlusion, if not
+ // needed.
+ if (!mShowingThumbnails) {
+ return;
+ }
+ nsAutoString className;
+ WinUtils::GetClassName(aHwnd, className);
+ const auto name = NS_ConvertUTF16toUTF8(className);
+ CALC_LOG(LogLevel::Debug, "ProcessEventHookCallback() EVENT_OBJECT_HIDE %s",
+ name.get());
+ if (name.Equals("MultitaskingViewFrame") ||
+ name.Equals("TaskListThumbnailWnd")) {
+ CALC_LOG(LogLevel::Info,
+ "ProcessEventHookCallback() mShowingThumbnails = false");
+ mShowingThumbnails = false;
+ // Let occlusion calculation fix occlusion state, even though hwnd might
+ // be a popup window.
+ calculateOcclusion = true;
+ } else {
+ return;
+ }
+ }
+ // Don't continually calculate occlusion while a window is moving (unless it's
+ // a root window), but instead once at the beginning and once at the end.
+ // Remember the window being moved so if it's a root window, we can ignore
+ // it when deciding if windows under it are occluded.
+ else if (aEvent == EVENT_SYSTEM_MOVESIZESTART) {
+ mMovingWindow = aHwnd;
+ } else if (aEvent == EVENT_SYSTEM_MOVESIZEEND) {
+ mMovingWindow = 0;
+ } else if (mMovingWindow != 0) {
+ if (aEvent == EVENT_OBJECT_LOCATIONCHANGE ||
+ aEvent == EVENT_OBJECT_STATECHANGE) {
+ // Ignore move events if it's not a root window that's being moved. If it
+ // is a root window, we want to calculate occlusion to support tab
+ // dragging to windows that were occluded when the drag was started but
+ // are no longer occluded.
+ if (mRootWindowHwndsOcclusionState.find(aHwnd) ==
+ mRootWindowHwndsOcclusionState.end()) {
+ return;
+ }
+ } else {
+ // If we get an event that isn't a location/state change, then we probably
+ // missed the movesizeend notification, or got events out of order. In
+ // that case, we want to go back to normal occlusion calculation.
+ mMovingWindow = 0;
+ }
+ }
+
+ if (!calculateOcclusion) {
+ return;
+ }
+
+ ScheduleOcclusionCalculationIfNeeded();
+}
+
+void WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ ProcessUpdateVisibleWindowProcessIdsCallback(HWND aHwnd) {
+ MOZ_ASSERT(IsInWinWindowOcclusionThread());
+
+ LayoutDeviceIntRect windowRect;
+ if (WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(aHwnd, &windowRect)) {
+ DWORD pid;
+ ::GetWindowThreadProcessId(aHwnd, &pid);
+ mPidsForLocationChangeHook.insert(pid);
+ }
+}
+
+bool WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(
+ HWND aHwnd, LayoutDeviceIntRect* aWindowRect) {
+ return IsWindowVisibleAndFullyOpaque(aHwnd, aWindowRect) &&
+ (IsWindowOnCurrentVirtualDesktop(aHwnd) == Some(true));
+}
+
+Maybe<bool> WinWindowOcclusionTracker::WindowOcclusionCalculator::
+ IsWindowOnCurrentVirtualDesktop(HWND aHwnd) {
+ if (!mVirtualDesktopManager) {
+ return Some(true);
+ }
+
+ BOOL onCurrentDesktop;
+ HRESULT hr = mVirtualDesktopManager->IsWindowOnCurrentVirtualDesktop(
+ aHwnd, &onCurrentDesktop);
+ if (FAILED(hr)) {
+ // In this case, we do not know the window is in which virtual desktop.
+ return Nothing();
+ }
+
+ if (onCurrentDesktop) {
+ return Some(true);
+ }
+
+ GUID workspaceGuid;
+ hr = mVirtualDesktopManager->GetWindowDesktopId(aHwnd, &workspaceGuid);
+ if (FAILED(hr)) {
+ // In this case, we do not know the window is in which virtual desktop.
+ return Nothing();
+ }
+
+ // IsWindowOnCurrentVirtualDesktop() is flaky for newly opened windows,
+ // which causes test flakiness. Occasionally, it incorrectly says a window
+ // is not on the current virtual desktop when it is. In this situation,
+ // it also returns GUID_NULL for the desktop id.
+ if (workspaceGuid == GUID_NULL) {
+ // In this case, we do not know if the window is in which virtual desktop.
+ // But we hanle it as on current virtual desktop.
+ // It does not cause a problem to window occlusion.
+ // Since if window is not on current virtual desktop, window size becomes
+ // (0, 0, 0, 0). It makes window occlusion handling explicit. It is
+ // necessary for gtest.
+ return Some(true);
+ }
+
+ return Some(false);
+}
+
+#undef LOG
+#undef CALC_LOG
+
+} // namespace mozilla::widget
diff --git a/widget/windows/WinWindowOcclusionTracker.h b/widget/windows/WinWindowOcclusionTracker.h
new file mode 100644
index 0000000000..b82a41b984
--- /dev/null
+++ b/widget/windows/WinWindowOcclusionTracker.h
@@ -0,0 +1,333 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef widget_windows_WinWindowOcclusionTracker_h
+#define widget_windows_WinWindowOcclusionTracker_h
+
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "nsIWeakReferenceUtils.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+#include "mozilla/widget/WindowOcclusionState.h"
+#include "mozilla/widget/WinEventObserver.h"
+#include "Units.h"
+#include "nsThreadUtils.h"
+
+class nsBaseWidget;
+struct IVirtualDesktopManager;
+class WinWindowOcclusionTrackerTest;
+class WinWindowOcclusionTrackerInteractiveTest;
+
+namespace base {
+class Thread;
+} // namespace base
+
+namespace mozilla {
+
+namespace widget {
+
+class OcclusionUpdateRunnable;
+class SerializedTaskDispatcher;
+class UpdateOcclusionStateRunnable;
+
+// This class handles window occlusion tracking by using HWND.
+// Implementation is borrowed from chromium's NativeWindowOcclusionTrackerWin.
+class WinWindowOcclusionTracker final : public DisplayStatusListener,
+ public SessionChangeListener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinWindowOcclusionTracker)
+
+ /// Can only be called from the main thread.
+ static WinWindowOcclusionTracker* Get();
+
+ /// Can only be called from the main thread.
+ static void Ensure();
+
+ /// Can only be called from the main thread.
+ static void ShutDown();
+
+ /// Can be called from any thread.
+ static MessageLoop* OcclusionCalculatorLoop();
+
+ /// Can be called from any thread.
+ static bool IsInWinWindowOcclusionThread();
+
+ /// Can only be called from the main thread.
+ void EnsureDisplayStatusObserver();
+
+ /// Can only be called from the main thread.
+ void EnsureSessionChangeObserver();
+
+ // Enables notifying to widget via NotifyOcclusionState() when the occlusion
+ // state has been computed.
+ void Enable(nsBaseWidget* aWindow, HWND aHwnd);
+
+ // Disables notifying to widget via NotifyOcclusionState() when the occlusion
+ // state has been computed.
+ void Disable(nsBaseWidget* aWindow, HWND aHwnd);
+
+ // Called when widget's visibility is changed
+ void OnWindowVisibilityChanged(nsBaseWidget* aWindow, bool aVisible);
+
+ SerializedTaskDispatcher* GetSerializedTaskDispatcher() {
+ return mSerializedTaskDispatcher;
+ }
+
+ void TriggerCalculation();
+
+ void DumpOccludingWindows(HWND aHWnd);
+
+ private:
+ friend class ::WinWindowOcclusionTrackerTest;
+ friend class ::WinWindowOcclusionTrackerInteractiveTest;
+
+ explicit WinWindowOcclusionTracker(UniquePtr<base::Thread> aThread);
+ virtual ~WinWindowOcclusionTracker();
+
+ // This class computes the occlusion state of the tracked windows.
+ // It runs on a separate thread, and notifies the main thread of
+ // the occlusion state of the tracked windows.
+ class WindowOcclusionCalculator {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowOcclusionCalculator)
+ public:
+ // Creates WindowOcclusionCalculator instance.
+ static void CreateInstance();
+
+ // Clear WindowOcclusionCalculator instance.
+ static void ClearInstance();
+
+ // Returns existing WindowOcclusionCalculator instance.
+ static WindowOcclusionCalculator* GetInstance() { return sCalculator; }
+
+ void Initialize();
+ void Shutdown();
+
+ void EnableOcclusionTrackingForWindow(HWND hwnd);
+ void DisableOcclusionTrackingForWindow(HWND hwnd);
+
+ // If a window becomes visible, makes sure event hooks are registered.
+ void HandleVisibilityChanged(bool aVisible);
+
+ void HandleTriggerCalculation();
+
+ private:
+ WindowOcclusionCalculator();
+ ~WindowOcclusionCalculator();
+
+ // Registers event hooks, if not registered.
+ void MaybeRegisterEventHooks();
+
+ // This is the callback registered to get notified of various Windows
+ // events, like window moving/resizing.
+ static void CALLBACK EventHookCallback(HWINEVENTHOOK aWinEventHook,
+ DWORD aEvent, HWND aHwnd,
+ LONG aIdObject, LONG aIdChild,
+ DWORD aEventThread,
+ DWORD aMsEventTime);
+
+ // EnumWindows callback used to iterate over all hwnds to determine
+ // occlusion status of all tracked root windows. Also builds up
+ // |current_pids_with_visible_windows_| and registers event hooks for newly
+ // discovered processes with visible hwnds.
+ static BOOL CALLBACK
+ ComputeNativeWindowOcclusionStatusCallback(HWND hwnd, LPARAM lParam);
+
+ // EnumWindows callback used to update the list of process ids with
+ // visible hwnds, |pids_for_location_change_hook_|.
+ static BOOL CALLBACK UpdateVisibleWindowProcessIdsCallback(HWND aHwnd,
+ LPARAM aLParam);
+
+ // Determines which processes owning visible application windows to set the
+ // EVENT_OBJECT_LOCATIONCHANGE event hook for and stores the pids in
+ // |pids_for_location_change_hook_|.
+ void UpdateVisibleWindowProcessIds();
+
+ // Computes the native window occlusion status for all tracked root gecko
+ // windows in |root_window_hwnds_occlusion_state_| and notifies them if
+ // their occlusion status has changed.
+ void ComputeNativeWindowOcclusionStatus();
+
+ // Schedules an occlusion calculation , if one isn't already scheduled.
+ void ScheduleOcclusionCalculationIfNeeded();
+
+ // Registers a global event hook (not per process) for the events in the
+ // range from |event_min| to |event_max|, inclusive.
+ void RegisterGlobalEventHook(DWORD aEventMin, DWORD aEventMax);
+
+ // Registers the EVENT_OBJECT_LOCATIONCHANGE event hook for the process with
+ // passed id. The process has one or more visible, opaque windows.
+ void RegisterEventHookForProcess(DWORD aPid);
+
+ // Registers/Unregisters the event hooks necessary for occlusion tracking
+ // via calls to RegisterEventHook. These event hooks are disabled when all
+ // tracked windows are minimized.
+ void RegisterEventHooks();
+ void UnregisterEventHooks();
+
+ // EnumWindows callback for occlusion calculation. Returns true to
+ // continue enumeration, false otherwise. Currently, always returns
+ // true because this function also updates currentPidsWithVisibleWindows,
+ // and needs to see all HWNDs.
+ bool ProcessComputeNativeWindowOcclusionStatusCallback(
+ HWND aHwnd, std::unordered_set<DWORD>* aCurrentPidsWithVisibleWindows);
+
+ // Processes events sent to OcclusionEventHookCallback.
+ // It generally triggers scheduling of the occlusion calculation, but
+ // ignores certain events in order to not calculate occlusion more than
+ // necessary.
+ void ProcessEventHookCallback(HWINEVENTHOOK aWinEventHook, DWORD aEvent,
+ HWND aHwnd, LONG aIdObject, LONG aIdChild);
+
+ // EnumWindows callback for determining which processes to set the
+ // EVENT_OBJECT_LOCATIONCHANGE event hook for. We set that event hook for
+ // processes hosting fully visible, opaque windows.
+ void ProcessUpdateVisibleWindowProcessIdsCallback(HWND aHwnd);
+
+ // Returns true if the window is visible, fully opaque, and on the current
+ // virtual desktop, false otherwise.
+ bool WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(
+ HWND aHwnd, LayoutDeviceIntRect* aWindowRect);
+
+ // Returns true if aHwnd is definitely on the current virtual desktop,
+ // false if it's definitely not on the current virtual desktop, and Nothing
+ // if we we can't tell for sure.
+ Maybe<bool> IsWindowOnCurrentVirtualDesktop(HWND aHwnd);
+
+ static StaticRefPtr<WindowOcclusionCalculator> sCalculator;
+
+ // Map of root app window hwnds and their occlusion state. This contains
+ // both visible and hidden windows.
+ // It is accessed from WinWindowOcclusionTracker::UpdateOcclusionState()
+ // without using mutex. The access is safe by using
+ // SerializedTaskDispatcher.
+ std::unordered_map<HWND, OcclusionState> mRootWindowHwndsOcclusionState;
+
+ // Values returned by SetWinEventHook are stored so that hooks can be
+ // unregistered when necessary.
+ std::vector<HWINEVENTHOOK> mGlobalEventHooks;
+
+ // Map from process id to EVENT_OBJECT_LOCATIONCHANGE event hook.
+ std::unordered_map<DWORD, HWINEVENTHOOK> mProcessEventHooks;
+
+ // Pids of processes for which the EVENT_OBJECT_LOCATIONCHANGE event hook is
+ // set.
+ std::unordered_set<DWORD> mPidsForLocationChangeHook;
+
+ // Used as a timer to delay occlusion update.
+ RefPtr<CancelableRunnable> mOcclusionUpdateRunnable;
+
+ // Used to determine if a window is occluded. As we iterate through the
+ // hwnds in z-order, we subtract each opaque window's rect from
+ // mUnoccludedDesktopRegion. When we get to a root window, we subtract
+ // it from mUnoccludedDesktopRegion, and if mUnoccludedDesktopRegion
+ // doesn't change, the root window was already occluded.
+ LayoutDeviceIntRegion mUnoccludedDesktopRegion;
+
+ // Keeps track of how many root windows we need to compute the occlusion
+ // state of in a call to ComputeNativeWindowOcclusionStatus. Once we've
+ // determined the state of all root windows, we can stop subtracting
+ // windows from mUnoccludedDesktopRegion;.
+ int mNumRootWindowsWithUnknownOcclusionState;
+
+ // This is true if the task bar thumbnails or the alt tab thumbnails are
+ // showing.
+ bool mShowingThumbnails = false;
+
+ // Used to keep track of the window that's currently moving. That window
+ // is ignored for calculation occlusion so that tab dragging won't
+ // ignore windows occluded by the dragged window.
+ HWND mMovingWindow = 0;
+
+ // Only used on Win10+.
+ RefPtr<IVirtualDesktopManager> mVirtualDesktopManager;
+
+ // Used to serialize tasks related to mRootWindowHwndsOcclusionState.
+ RefPtr<SerializedTaskDispatcher> mSerializedTaskDispatcher;
+
+ // This is an alias to the singleton WinWindowOcclusionTracker mMonitor,
+ // and is used in ShutDown().
+ Monitor& mMonitor;
+
+ friend class OcclusionUpdateRunnable;
+ };
+
+ static BOOL CALLBACK DumpOccludingWindowsCallback(HWND aHWnd, LPARAM aLParam);
+
+ // Returns true if we are interested in |hwnd| for purposes of occlusion
+ // calculation. We are interested in |hwnd| if it is a window that is
+ // visible, opaque, bounded, and not a popup or floating window. If we are
+ // interested in |hwnd|, stores the window rectangle in |window_rect|.
+ static bool IsWindowVisibleAndFullyOpaque(HWND aHwnd,
+ LayoutDeviceIntRect* aWindowRect);
+
+ void Destroy();
+
+ static void CallUpdateOcclusionState(
+ std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows);
+
+ // Updates root windows occclusion state. If aShowAllWindows is true,
+ // all non-hidden windows will be marked visible. This is used to force
+ // rendering of thumbnails.
+ void UpdateOcclusionState(std::unordered_map<HWND, OcclusionState>* aMap,
+ bool aShowAllWindows);
+
+ // This is called with session changed notifications. If the screen is locked
+ // by the current session, it marks app windows as occluded.
+ void OnSessionChange(WPARAM aStatusCode,
+ Maybe<bool> aIsCurrentSession) override;
+
+ // This is called when the display is put to sleep. If the display is sleeping
+ // it marks app windows as occluded.
+ void OnDisplayStateChanged(bool aDisplayOn) override;
+
+ // Marks all root windows as either occluded, or if hwnd IsIconic, hidden.
+ void MarkNonIconicWindowsOccluded();
+
+ static StaticRefPtr<WinWindowOcclusionTracker> sTracker;
+
+ // "WinWindowOcclusionCalc" thread.
+ UniquePtr<base::Thread> mThread;
+ Monitor mMonitor;
+
+ // Has ShutDown been called on us? We might have survived if our thread join
+ // timed out.
+ bool mHasAttemptedShutdown = false;
+
+ // Map of HWND to widget. Maintained on main thread, and used to send
+ // occlusion state notifications to Windows from
+ // mRootWindowHwndsOcclusionState.
+ std::unordered_map<HWND, nsWeakPtr> mHwndRootWindowMap;
+
+ // This is set by UpdateOcclusionState(). It is currently only used by tests.
+ int mNumVisibleRootWindows = 0;
+
+ // If the screen is locked, windows are considered occluded.
+ bool mScreenLocked = false;
+
+ // If the display is off, windows are considered occluded.
+ bool mDisplayOn = true;
+
+ RefPtr<DisplayStatusObserver> mDisplayStatusObserver;
+
+ RefPtr<SessionChangeObserver> mSessionChangeObserver;
+
+ // Used to serialize tasks related to mRootWindowHwndsOcclusionState.
+ RefPtr<SerializedTaskDispatcher> mSerializedTaskDispatcher;
+
+ friend class OcclusionUpdateRunnable;
+ friend class UpdateOcclusionStateRunnable;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_windows_WinWindowOcclusionTracker_h
diff --git a/widget/windows/WindowHook.cpp b/widget/windows/WindowHook.cpp
new file mode 100644
index 0000000000..06773f21b7
--- /dev/null
+++ b/widget/windows/WindowHook.cpp
@@ -0,0 +1,113 @@
+/* -*- 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 "WindowHook.h"
+#include "nsWindow.h"
+#include "nsWindowDefs.h"
+
+namespace mozilla {
+namespace widget {
+
+nsresult WindowHook::AddHook(UINT nMsg, Callback callback, void* context) {
+ MessageData* data = LookupOrCreate(nMsg);
+
+ if (!data) return NS_ERROR_OUT_OF_MEMORY;
+
+ // Ensure we don't overwrite another hook
+ NS_ENSURE_TRUE(nullptr == data->hook.cb, NS_ERROR_UNEXPECTED);
+
+ data->hook = CallbackData(callback, context);
+
+ return NS_OK;
+}
+
+nsresult WindowHook::RemoveHook(UINT nMsg, Callback callback, void* context) {
+ CallbackData cbdata(callback, context);
+ MessageData* data = Lookup(nMsg);
+ if (!data) return NS_ERROR_UNEXPECTED;
+ if (data->hook != cbdata) return NS_ERROR_UNEXPECTED;
+ data->hook = CallbackData();
+
+ DeleteIfEmpty(data);
+ return NS_OK;
+}
+
+nsresult WindowHook::AddMonitor(UINT nMsg, Callback callback, void* context) {
+ MessageData* data = LookupOrCreate(nMsg);
+ return (data && data->monitors.AppendElement(CallbackData(callback, context),
+ fallible))
+ ? NS_OK
+ : NS_ERROR_OUT_OF_MEMORY;
+}
+
+nsresult WindowHook::RemoveMonitor(UINT nMsg, Callback callback,
+ void* context) {
+ CallbackData cbdata(callback, context);
+ MessageData* data = Lookup(nMsg);
+ if (!data) return NS_ERROR_UNEXPECTED;
+ CallbackDataArray::index_type idx = data->monitors.IndexOf(cbdata);
+ if (idx == CallbackDataArray::NoIndex) return NS_ERROR_UNEXPECTED;
+ data->monitors.RemoveElementAt(idx);
+ DeleteIfEmpty(data);
+ return NS_OK;
+}
+
+WindowHook::MessageData* WindowHook::Lookup(UINT nMsg) {
+ MessageDataArray::index_type idx;
+ for (idx = 0; idx < mMessageData.Length(); idx++) {
+ MessageData& data = mMessageData[idx];
+ if (data.nMsg == nMsg) return &data;
+ }
+ return nullptr;
+}
+
+WindowHook::MessageData* WindowHook::LookupOrCreate(UINT nMsg) {
+ MessageData* data = Lookup(nMsg);
+ if (!data) {
+ data = mMessageData.AppendElement();
+
+ if (!data) return nullptr;
+
+ data->nMsg = nMsg;
+ }
+ return data;
+}
+
+void WindowHook::DeleteIfEmpty(MessageData* data) {
+ // Never remove a MessageData that has still a hook or monitor entries.
+ if (data->hook || !data->monitors.IsEmpty()) return;
+
+ MessageDataArray::index_type idx;
+ idx = data - mMessageData.Elements();
+ NS_ASSERTION(
+ idx < mMessageData.Length(),
+ "Attempted to delete MessageData that doesn't belong to this array!");
+ mMessageData.RemoveElementAt(idx);
+}
+
+bool WindowHook::Notify(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult) {
+ MessageData* data = Lookup(nMsg);
+ if (!data) return false;
+
+ uint32_t length = data->monitors.Length();
+ for (uint32_t midx = 0; midx < length; midx++) {
+ data->monitors[midx].Invoke(hWnd, nMsg, wParam, lParam, &aResult.mResult);
+ }
+
+ aResult.mConsumed =
+ data->hook.Invoke(hWnd, nMsg, wParam, lParam, &aResult.mResult);
+ return aResult.mConsumed;
+}
+
+bool WindowHook::CallbackData::Invoke(HWND hWnd, UINT msg, WPARAM wParam,
+ LPARAM lParam, LRESULT* aResult) {
+ if (!cb) return false;
+ return cb(context, hWnd, msg, wParam, lParam, aResult);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WindowHook.h b/widget/windows/WindowHook.h
new file mode 100644
index 0000000000..1d1f4b02da
--- /dev/null
+++ b/widget/windows/WindowHook.h
@@ -0,0 +1,76 @@
+/* -*- 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/. */
+
+#ifndef __mozilla_WindowHook_h__
+#define __mozilla_WindowHook_h__
+
+#include <windows.h>
+
+#include <nsHashKeys.h>
+#include <nsClassHashtable.h>
+#include <nsTArray.h>
+
+#include "nsAppShell.h"
+
+class nsWindow;
+
+namespace mozilla {
+namespace widget {
+
+struct MSGResult;
+
+class WindowHook {
+ public:
+ // It is expected that most callbacks will return false
+ typedef bool (*Callback)(void* aContext, HWND hWnd, UINT nMsg, WPARAM wParam,
+ LPARAM lParam, LRESULT* aResult);
+
+ nsresult AddHook(UINT nMsg, Callback callback, void* context);
+ nsresult RemoveHook(UINT nMsg, Callback callback, void* context);
+ nsresult AddMonitor(UINT nMsg, Callback callback, void* context);
+ nsresult RemoveMonitor(UINT nMsg, Callback callback, void* context);
+
+ private:
+ struct CallbackData {
+ Callback cb;
+ void* context;
+
+ CallbackData() : cb(nullptr), context(nullptr) {}
+ CallbackData(Callback cb, void* ctx) : cb(cb), context(ctx) {}
+ bool Invoke(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam,
+ LRESULT* aResult);
+ bool operator==(const CallbackData& rhs) const {
+ return cb == rhs.cb && context == rhs.context;
+ }
+ bool operator!=(const CallbackData& rhs) const { return !(*this == rhs); }
+ explicit operator bool() const { return !!cb; }
+ };
+
+ typedef nsTArray<CallbackData> CallbackDataArray;
+ struct MessageData {
+ UINT nMsg;
+ CallbackData hook;
+ CallbackDataArray monitors;
+ };
+
+ bool Notify(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam,
+ MSGResult& aResult);
+
+ MessageData* Lookup(UINT nMsg);
+ MessageData* LookupOrCreate(UINT nMsg);
+ void DeleteIfEmpty(MessageData* data);
+
+ typedef nsTArray<MessageData> MessageDataArray;
+ MessageDataArray mMessageData;
+
+ // For Notify
+ friend class ::nsWindow;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // __mozilla_WindowHook_h__
diff --git a/widget/windows/WindowsConsole.cpp b/widget/windows/WindowsConsole.cpp
new file mode 100644
index 0000000000..c8e7eb1a11
--- /dev/null
+++ b/widget/windows/WindowsConsole.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 4; 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 "WindowsConsole.h"
+
+#include <windows.h>
+#include <fcntl.h>
+#include <cstdio>
+#include <io.h>
+
+namespace mozilla {
+
+static void AssignStdHandle(const char* aPath, const char* aMode, FILE* aStream,
+ DWORD aStdHandle) {
+ // Visual Studio's _fileno() returns -2 for the standard
+ // streams if they aren't associated with an output stream.
+ const int fd = _fileno(aStream);
+ if (fd == -2) {
+ freopen(aPath, aMode, aStream);
+ return;
+ }
+ if (fd < 0) {
+ return;
+ }
+
+ const HANDLE handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
+ if (handle == INVALID_HANDLE_VALUE) {
+ return;
+ }
+
+ const HANDLE oldHandle = GetStdHandle(aStdHandle);
+ if (handle == oldHandle) {
+ return;
+ }
+
+ SetStdHandle(aStdHandle, handle);
+}
+
+// This code attaches the process to the appropriate console.
+void UseParentConsole() {
+ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
+ // Redirect the standard streams to the existing console, but
+ // only if they haven't been redirected to a valid file.
+ AssignStdHandle("CONOUT$", "w", stdout, STD_OUTPUT_HANDLE);
+ // There is no CONERR$, so use CONOUT$ for stderr as well.
+ AssignStdHandle("CONOUT$", "w", stderr, STD_ERROR_HANDLE);
+ AssignStdHandle("CONIN$", "r", stdin, STD_INPUT_HANDLE);
+ }
+}
+
+} // namespace mozilla
diff --git a/widget/windows/WindowsConsole.h b/widget/windows/WindowsConsole.h
new file mode 100644
index 0000000000..4b8ecf1823
--- /dev/null
+++ b/widget/windows/WindowsConsole.h
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef mozilla_WindowsConsole_h
+#define mozilla_WindowsConsole_h
+
+namespace mozilla {
+
+// This code attaches the process to the appropriate console.
+void UseParentConsole();
+
+} // namespace mozilla
+
+#endif // mozilla_WindowsConsole_h
diff --git a/widget/windows/WindowsEMF.cpp b/widget/windows/WindowsEMF.cpp
new file mode 100644
index 0000000000..71e3631bea
--- /dev/null
+++ b/widget/windows/WindowsEMF.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 20; 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 "WindowsEMF.h"
+
+namespace mozilla {
+namespace widget {
+
+WindowsEMF::WindowsEMF() : mEmf(nullptr), mDC(nullptr) {}
+
+WindowsEMF::~WindowsEMF() { ReleaseAllResource(); }
+
+bool WindowsEMF::InitForDrawing(const wchar_t* aMetafilePath /* = nullptr */) {
+ ReleaseAllResource();
+
+ mDC = ::CreateEnhMetaFile(nullptr, aMetafilePath, nullptr, nullptr);
+ return !!mDC;
+}
+
+bool WindowsEMF::InitFromFileContents(const wchar_t* aMetafilePath) {
+ MOZ_ASSERT(aMetafilePath);
+ ReleaseAllResource();
+
+ mEmf = ::GetEnhMetaFileW(aMetafilePath);
+ return !!mEmf;
+}
+
+bool WindowsEMF::InitFromFileContents(LPBYTE aBytes, UINT aSize) {
+ MOZ_ASSERT(aBytes && aSize != 0);
+ ReleaseAllResource();
+
+ mEmf = SetEnhMetaFileBits(aSize, aBytes);
+
+ return !!mEmf;
+}
+
+bool WindowsEMF::FinishDocument() {
+ if (mDC) {
+ mEmf = ::CloseEnhMetaFile(mDC);
+ mDC = nullptr;
+ }
+ return !!mEmf;
+}
+
+void WindowsEMF::ReleaseEMFHandle() {
+ if (mEmf) {
+ ::DeleteEnhMetaFile(mEmf);
+ mEmf = nullptr;
+ }
+}
+
+void WindowsEMF::ReleaseAllResource() {
+ FinishDocument();
+ ReleaseEMFHandle();
+}
+
+bool WindowsEMF::Playback(HDC aDeviceContext, const RECT& aRect) {
+ DebugOnly<bool> result = FinishDocument();
+ MOZ_ASSERT(result, "This function should be used after InitXXX.");
+
+ return ::PlayEnhMetaFile(aDeviceContext, mEmf, &aRect) != 0;
+}
+
+bool WindowsEMF::SaveToFile() {
+ DebugOnly<bool> result = FinishDocument();
+ MOZ_ASSERT(result, "This function should be used after InitXXX.");
+
+ ReleaseEMFHandle();
+ return true;
+}
+
+UINT WindowsEMF::GetEMFContentSize() {
+ DebugOnly<bool> result = FinishDocument();
+ MOZ_ASSERT(result, "This function should be used after InitXXX.");
+
+ return GetEnhMetaFileBits(mEmf, 0, NULL);
+}
+
+bool WindowsEMF::GetEMFContentBits(LPBYTE aBytes) {
+ DebugOnly<bool> result = FinishDocument();
+ MOZ_ASSERT(result, "This function should be used after InitXXX.");
+
+ UINT emfSize = GetEMFContentSize();
+ if (GetEnhMetaFileBits(mEmf, emfSize, aBytes) != emfSize) {
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/WindowsEMF.h b/widget/windows/WindowsEMF.h
new file mode 100644
index 0000000000..3a7a20173c
--- /dev/null
+++ b/widget/windows/WindowsEMF.h
@@ -0,0 +1,106 @@
+/* -*- 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/. */
+
+#ifndef MOZILLA_WIDGET_WINDOWSEMF_H
+#define MOZILLA_WIDGET_WINDOWSEMF_H
+
+/* include windows.h for the HDC definitions that we need. */
+#include <windows.h>
+
+namespace mozilla {
+namespace widget {
+
+/**
+ * Windows Enhance Metafile: https://en.wikipedia.org/wiki/Windows_Metafile
+ * A metafile, also called a vector image, is an image that is stored as a
+ * sequence of drawing commands and settings. The commands and settings
+ * recorded in a Metafile object can be stored in memory or saved to a file.
+ *
+ * The metafile device context is used for all drawing operations required to
+ * create the picture. When the system processes a GDI function associated with
+ * a metafile DC, it converts the function into the appropriate data and stores
+ * this data in a record appended to the metafile.
+ */
+class WindowsEMF {
+ public:
+ WindowsEMF();
+ ~WindowsEMF();
+
+ /**
+ * Initializes the object with the path of a file where the EMF data stream
+ * should be stored. Callers are then expected to call GetDC() to draw output
+ * before going on to call Playback() or SaveToFile() to generate the EMF
+ * output.
+ */
+ bool InitForDrawing(const wchar_t* aMetafilePath = nullptr);
+
+ /**
+ * Initializes the object with an existing EMF file. Consumers cannot use
+ * GetDC() to obtain an HDC to modify the file. They can only use Playback().
+ */
+ bool InitFromFileContents(const wchar_t* aMetafilePath);
+
+ /**
+ * Creates the EMF from the specified data
+ *
+ * @param aByte Pointer to a buffer that contains EMF data.
+ * @param aSize Specifies the size, in bytes, of aByte.
+ */
+ bool InitFromFileContents(PBYTE aBytes, UINT aSize);
+
+ /**
+ * If this object was initiaziled using InitForDrawing() then this function
+ * returns an HDC that can be drawn to generate the EMF output. Otherwise it
+ * returns null. After finishing with the HDC, consumers could call Playback()
+ * to draw EMF onto the given DC or call SaveToFile() to finish writing the
+ * EMF file.
+ */
+ HDC GetDC() const {
+ MOZ_ASSERT(mDC,
+ "GetDC can be used only after "
+ "InitForDrawing/ InitFromFileContents and before"
+ "Playback/ SaveToFile");
+ return mDC;
+ }
+
+ /**
+ * Play the EMF's drawing commands onto the given DC.
+ */
+ bool Playback(HDC aDeviceContext, const RECT& aRect);
+
+ /**
+ * Called to generate the EMF file once a consumer has finished drawing to
+ * the HDC returned by GetDC(), if initializes the object with the path of a
+ * file.
+ */
+ bool SaveToFile();
+
+ /**
+ * Return the size of the enhanced metafile, in bytes.
+ */
+ UINT GetEMFContentSize();
+
+ /**
+ * Retrieves the contents of the EMF and copies them into a buffer.
+ *
+ * @param aByte the buffer to receive the data.
+ */
+ bool GetEMFContentBits(PBYTE aBytes);
+
+ private:
+ WindowsEMF(const WindowsEMF& aEMF) = delete;
+ bool FinishDocument();
+ void ReleaseEMFHandle();
+ void ReleaseAllResource();
+
+ /* Compiled EMF data handle. */
+ HENHMETAFILE mEmf;
+ HDC mDC;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif /* MOZILLA_WIDGET_WINDOWSEMF_H */
diff --git a/widget/windows/WindowsEventLog.h b/widget/windows/WindowsEventLog.h
new file mode 100644
index 0000000000..e98d5077a0
--- /dev/null
+++ b/widget/windows/WindowsEventLog.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef mozilla_WindowsEventLog_h
+#define mozilla_WindowsEventLog_h
+
+/**
+ * Report messages to the Windows Event Log.
+ */
+
+#include <stdio.h>
+#include <windows.h>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+
+/**
+ * This header is intended for self-contained, header-only, utility code for
+ * Win32. It may be used outside of xul.dll, in places such as
+ * default-browser-agent.exe or notificationrouter.dll. If your code creates
+ * dependencies on Mozilla libraries, you should put it elsewhere.
+ */
+
+#define MOZ_WIN_EVENT_LOG_ERROR(source, hr) \
+ mozilla::WriteWindowsEventLogHresult(source, hr, __FUNCTION__, __LINE__)
+#define MOZ_WIN_EVENT_LOG_ERROR_MESSAGE(source, format, ...) \
+ mozilla::WriteWindowsEventLogErrorMessage(source, format, __FUNCTION__, \
+ __LINE__, ##__VA_ARGS__)
+
+namespace mozilla {
+
+static void WriteWindowsEventLogErrorBuffer(const wchar_t* eventSourceName,
+ const wchar_t* buffer,
+ DWORD eventId) {
+ HANDLE source = RegisterEventSourceW(nullptr, eventSourceName);
+ if (!source) {
+ // Not much we can do about this.
+ return;
+ }
+
+ const wchar_t* stringsArray[] = {buffer};
+ ReportEventW(source, EVENTLOG_ERROR_TYPE, 0, eventId, nullptr, 1, 0,
+ stringsArray, nullptr);
+
+ DeregisterEventSource(source);
+}
+
+inline void WriteWindowsEventLogHresult(const wchar_t* eventSourceName,
+ HRESULT hr, const char* sourceFile,
+ int sourceLine) {
+ const wchar_t* format = L"0x%X in %S:%d";
+ int bufferSize = _scwprintf(format, hr, sourceFile, sourceLine);
+ ++bufferSize; // Extra character for terminating null
+ mozilla::UniquePtr<wchar_t[]> errorStr =
+ mozilla::MakeUnique<wchar_t[]>(bufferSize);
+
+ _snwprintf_s(errorStr.get(), bufferSize, _TRUNCATE, format, hr, sourceFile,
+ sourceLine);
+
+ WriteWindowsEventLogErrorBuffer(eventSourceName, errorStr.get(), hr);
+}
+
+MOZ_FORMAT_WPRINTF(1, 4)
+inline void WriteWindowsEventLogErrorMessage(const wchar_t* eventSourceName,
+ const wchar_t* messageFormat,
+ const char* sourceFile,
+ int sourceLine, ...) {
+ // First assemble the passed message
+ va_list ap;
+ va_start(ap, sourceLine);
+ int bufferSize = _vscwprintf(messageFormat, ap);
+ ++bufferSize; // Extra character for terminating null
+ va_end(ap);
+ mozilla::UniquePtr<wchar_t[]> message =
+ mozilla::MakeUnique<wchar_t[]>(bufferSize);
+
+ va_start(ap, sourceLine);
+ vswprintf(message.get(), bufferSize, messageFormat, ap);
+ va_end(ap);
+
+ // Next, assemble the complete error message to print
+ const wchar_t* errorFormat = L"Error: %s (%S:%d)";
+ bufferSize = _scwprintf(errorFormat, message.get(), sourceFile, sourceLine);
+ ++bufferSize; // Extra character for terminating null
+ mozilla::UniquePtr<wchar_t[]> errorStr =
+ mozilla::MakeUnique<wchar_t[]>(bufferSize);
+
+ _snwprintf_s(errorStr.get(), bufferSize, _TRUNCATE, errorFormat,
+ message.get(), sourceFile, sourceLine);
+
+ WriteWindowsEventLogErrorBuffer(eventSourceName, errorStr.get(), 0);
+}
+
+} // namespace mozilla
+
+#endif // mozilla_WindowsEventLog_h
diff --git a/widget/windows/WindowsSMTCProvider.cpp b/widget/windows/WindowsSMTCProvider.cpp
new file mode 100644
index 0000000000..04d833a8e7
--- /dev/null
+++ b/widget/windows/WindowsSMTCProvider.cpp
@@ -0,0 +1,716 @@
+/* -*- 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/. */
+
+/* mingw currently doesn't support windows.media.h, so we disable
+ * the whole related class until this is fixed.
+ * @TODO: Maybe contact MinGW Team for inclusion?*/
+#ifndef __MINGW32__
+
+# include "WindowsSMTCProvider.h"
+
+# include <windows.h>
+# include <windows.media.h>
+# include <wrl.h>
+
+# include "nsMimeTypes.h"
+# include "mozilla/Assertions.h"
+# include "mozilla/Logging.h"
+# include "mozilla/Maybe.h"
+# include "mozilla/WidgetUtils.h"
+# include "mozilla/ScopeExit.h"
+# include "mozilla/dom/MediaControlUtils.h"
+# include "mozilla/media/MediaUtils.h"
+# include "nsThreadUtils.h"
+
+# pragma comment(lib, "runtimeobject.lib")
+
+using namespace ABI::Windows::Foundation;
+using namespace ABI::Windows::Media;
+using namespace ABI::Windows::Storage::Streams;
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+using namespace mozilla;
+
+# ifndef RuntimeClass_Windows_Media_SystemMediaTransportControls
+# define RuntimeClass_Windows_Media_SystemMediaTransportControls \
+ L"Windows.Media.SystemMediaTransportControls"
+# endif
+
+# ifndef RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference
+# define RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference \
+ L"Windows.Storage.Streams.RandomAccessStreamReference"
+# endif
+
+# ifndef ISystemMediaTransportControlsInterop
+EXTERN_C const IID IID_ISystemMediaTransportControlsInterop;
+MIDL_INTERFACE("ddb0472d-c911-4a1f-86d9-dc3d71a95f5a")
+ISystemMediaTransportControlsInterop : public IInspectable {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE GetForWindow(
+ /* [in] */ __RPC__in HWND appWindow,
+ /* [in] */ __RPC__in REFIID riid,
+ /* [iid_is][retval][out] */
+ __RPC__deref_out_opt void** mediaTransportControl) = 0;
+};
+# endif /* __ISystemMediaTransportControlsInterop_INTERFACE_DEFINED__ */
+
+extern mozilla::LazyLogModule gMediaControlLog;
+
+# undef LOG
+# define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("WindowSMTCProvider=%p, " msg, this, ##__VA_ARGS__))
+
+static inline Maybe<mozilla::dom::MediaControlKey> TranslateKeycode(
+ SystemMediaTransportControlsButton keycode) {
+ switch (keycode) {
+ case SystemMediaTransportControlsButton_Play:
+ return Some(mozilla::dom::MediaControlKey::Play);
+ case SystemMediaTransportControlsButton_Pause:
+ return Some(mozilla::dom::MediaControlKey::Pause);
+ case SystemMediaTransportControlsButton_Next:
+ return Some(mozilla::dom::MediaControlKey::Nexttrack);
+ case SystemMediaTransportControlsButton_Previous:
+ return Some(mozilla::dom::MediaControlKey::Previoustrack);
+ case SystemMediaTransportControlsButton_Stop:
+ return Some(mozilla::dom::MediaControlKey::Stop);
+ case SystemMediaTransportControlsButton_FastForward:
+ return Some(mozilla::dom::MediaControlKey::Seekforward);
+ case SystemMediaTransportControlsButton_Rewind:
+ return Some(mozilla::dom::MediaControlKey::Seekbackward);
+ default:
+ return Nothing(); // Not supported Button
+ }
+}
+
+static IAsyncInfo* GetIAsyncInfo(IAsyncOperation<unsigned int>* aAsyncOp) {
+ MOZ_ASSERT(aAsyncOp);
+ IAsyncInfo* asyncInfo;
+ HRESULT hr = aAsyncOp->QueryInterface(IID_IAsyncInfo,
+ reinterpret_cast<void**>(&asyncInfo));
+ // The assertion always works since IAsyncOperation implements IAsyncInfo
+ MOZ_ASSERT(SUCCEEDED(hr));
+ Unused << hr;
+ MOZ_ASSERT(asyncInfo);
+ return asyncInfo;
+}
+
+WindowsSMTCProvider::WindowsSMTCProvider() {
+ LOG("Creating an empty and invisible window");
+
+ // In order to create a SMTC-Provider, we need a hWnd, which shall be created
+ // dynamically from an invisible window. This leads to the following
+ // boilerplate code.
+ WNDCLASS wnd{};
+ wnd.lpszClassName = L"Firefox-MediaKeys";
+ wnd.hInstance = nullptr;
+ wnd.lpfnWndProc = DefWindowProc;
+ GetLastError(); // Clear the error
+ RegisterClass(&wnd);
+ MOZ_ASSERT(!GetLastError());
+
+ mWindow = CreateWindowExW(0, L"Firefox-MediaKeys", L"Firefox Media Keys", 0,
+ CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, nullptr,
+ nullptr, nullptr, nullptr);
+ MOZ_ASSERT(mWindow);
+ MOZ_ASSERT(!GetLastError());
+}
+
+WindowsSMTCProvider::~WindowsSMTCProvider() {
+ // Dispose the window
+ MOZ_ASSERT(mWindow);
+ if (!DestroyWindow(mWindow)) {
+ LOG("Failed to destroy the hidden window. Error Code: %lu", GetLastError());
+ }
+ if (!UnregisterClass(L"Firefox-MediaKeys", nullptr)) {
+ // Note that this is logged when the class wasn't even registered.
+ LOG("Failed to unregister the class. Error Code: %lu", GetLastError());
+ }
+}
+
+bool WindowsSMTCProvider::IsOpened() const { return mInitialized; }
+
+bool WindowsSMTCProvider::Open() {
+ LOG("Opening Source");
+ MOZ_ASSERT(!mInitialized);
+
+ if (!InitDisplayAndControls()) {
+ LOG("Failed to initialize the SMTC and its display");
+ return false;
+ }
+
+ if (!UpdateButtons()) {
+ LOG("Failed to initialize the buttons");
+ return false;
+ }
+
+ if (!RegisterEvents()) {
+ LOG("Failed to register SMTC key-event listener");
+ return false;
+ }
+
+ if (!EnableControl(true)) {
+ LOG("Failed to enable SMTC control");
+ return false;
+ }
+
+ mInitialized = true;
+ SetPlaybackState(mozilla::dom::MediaSessionPlaybackState::None);
+ return mInitialized;
+}
+
+void WindowsSMTCProvider::Close() {
+ MediaControlKeySource::Close();
+ // Prevent calling Set methods when init failed
+ if (mInitialized) {
+ SetPlaybackState(mozilla::dom::MediaSessionPlaybackState::None);
+ UnregisterEvents();
+ ClearMetadata();
+ // We have observed an Windows issue, if we modify `mControls` , (such as
+ // setting metadata, disable buttons) before disabling control, and those
+ // operations are not done sequentially within a same main thread task,
+ // then it would cause a problem where the SMTC wasn't clean up completely
+ // and show the executable name.
+ EnableControl(false);
+ mInitialized = false;
+ }
+}
+
+void WindowsSMTCProvider::SetPlaybackState(
+ mozilla::dom::MediaSessionPlaybackState aState) {
+ MOZ_ASSERT(mInitialized);
+ MediaControlKeySource::SetPlaybackState(aState);
+
+ HRESULT hr;
+
+ // Note: we can't return the status of put_PlaybackStatus, but we can at least
+ // assert it.
+ switch (aState) {
+ case mozilla::dom::MediaSessionPlaybackState::Paused:
+ hr = mControls->put_PlaybackStatus(
+ ABI::Windows::Media::MediaPlaybackStatus_Paused);
+ break;
+ case mozilla::dom::MediaSessionPlaybackState::Playing:
+ hr = mControls->put_PlaybackStatus(
+ ABI::Windows::Media::MediaPlaybackStatus_Playing);
+ break;
+ case mozilla::dom::MediaSessionPlaybackState::None:
+ hr = mControls->put_PlaybackStatus(
+ ABI::Windows::Media::MediaPlaybackStatus_Stopped);
+ break;
+ // MediaPlaybackStatus still supports Closed and Changing, which we don't
+ // use (yet)
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Enum Inconsitency between PlaybackState and WindowsSMTCProvider");
+ break;
+ }
+
+ MOZ_ASSERT(SUCCEEDED(hr));
+ Unused << hr;
+}
+
+void WindowsSMTCProvider::SetMediaMetadata(
+ const mozilla::dom::MediaMetadataBase& aMetadata) {
+ MOZ_ASSERT(mInitialized);
+ SetMusicMetadata(aMetadata.mArtist, aMetadata.mTitle);
+ LoadThumbnail(aMetadata.mArtwork);
+}
+
+void WindowsSMTCProvider::ClearMetadata() {
+ MOZ_ASSERT(mDisplay);
+ if (FAILED(mDisplay->ClearAll())) {
+ LOG("Failed to clear SMTC display");
+ }
+ mImageFetchRequest.DisconnectIfExists();
+ CancelPendingStoreAsyncOperation();
+ mThumbnailUrl.Truncate();
+ mProcessingUrl.Truncate();
+ mNextImageIndex = 0;
+ mSupportedKeys = 0;
+}
+
+void WindowsSMTCProvider::SetSupportedMediaKeys(
+ const MediaKeysArray& aSupportedKeys) {
+ MOZ_ASSERT(mInitialized);
+
+ uint32_t supportedKeys = 0;
+ for (const mozilla::dom::MediaControlKey& key : aSupportedKeys) {
+ supportedKeys |= GetMediaKeyMask(key);
+ }
+
+ if (supportedKeys == mSupportedKeys) {
+ LOG("Supported keys stay the same");
+ return;
+ }
+
+ LOG("Update supported keys");
+ mSupportedKeys = supportedKeys;
+ UpdateButtons();
+}
+
+void WindowsSMTCProvider::UnregisterEvents() {
+ if (mControls && mButtonPressedToken.value != 0) {
+ mControls->remove_ButtonPressed(mButtonPressedToken);
+ }
+}
+
+bool WindowsSMTCProvider::RegisterEvents() {
+ MOZ_ASSERT(mControls);
+ auto self = RefPtr<WindowsSMTCProvider>(this);
+ auto callbackbtnPressed = Callback<
+ ITypedEventHandler<SystemMediaTransportControls*,
+ SystemMediaTransportControlsButtonPressedEventArgs*>>(
+ [this, self](ISystemMediaTransportControls*,
+ ISystemMediaTransportControlsButtonPressedEventArgs* pArgs)
+ -> HRESULT {
+ MOZ_ASSERT(pArgs);
+ SystemMediaTransportControlsButton btn;
+
+ if (FAILED(pArgs->get_Button(&btn))) {
+ LOG("SystemMediaTransportControls: ButtonPressedEvent - Could "
+ "not get Button.");
+ return S_OK; // Propagating the error probably wouldn't help.
+ }
+
+ Maybe<mozilla::dom::MediaControlKey> keyCode = TranslateKeycode(btn);
+ if (keyCode.isSome() && IsOpened()) {
+ OnButtonPressed(keyCode.value());
+ }
+ return S_OK;
+ });
+
+ if (FAILED(mControls->add_ButtonPressed(callbackbtnPressed.Get(),
+ &mButtonPressedToken))) {
+ LOG("SystemMediaTransportControls: Failed at "
+ "registerEvents().add_ButtonPressed()");
+ return false;
+ }
+
+ return true;
+}
+
+void WindowsSMTCProvider::OnButtonPressed(
+ mozilla::dom::MediaControlKey aKey) const {
+ if (!IsKeySupported(aKey)) {
+ LOG("key: %s is not supported", ToMediaControlKeyStr(aKey));
+ return;
+ }
+
+ for (auto& listener : mListeners) {
+ listener->OnActionPerformed(mozilla::dom::MediaControlAction(aKey));
+ }
+}
+
+bool WindowsSMTCProvider::EnableControl(bool aEnabled) const {
+ MOZ_ASSERT(mControls);
+ return SUCCEEDED(mControls->put_IsEnabled(aEnabled));
+}
+
+bool WindowsSMTCProvider::UpdateButtons() const {
+ static const mozilla::dom::MediaControlKey kKeys[] = {
+ mozilla::dom::MediaControlKey::Play, mozilla::dom::MediaControlKey::Pause,
+ mozilla::dom::MediaControlKey::Previoustrack,
+ mozilla::dom::MediaControlKey::Nexttrack,
+ mozilla::dom::MediaControlKey::Stop};
+
+ bool success = true;
+ for (const mozilla::dom::MediaControlKey& key : kKeys) {
+ if (!EnableKey(key, IsKeySupported(key))) {
+ success = false;
+ LOG("Failed to set %s=%s", ToMediaControlKeyStr(key),
+ IsKeySupported(key) ? "true" : "false");
+ }
+ }
+
+ return success;
+}
+
+bool WindowsSMTCProvider::IsKeySupported(
+ mozilla::dom::MediaControlKey aKey) const {
+ return mSupportedKeys & GetMediaKeyMask(aKey);
+}
+
+bool WindowsSMTCProvider::EnableKey(mozilla::dom::MediaControlKey aKey,
+ bool aEnable) const {
+ MOZ_ASSERT(mControls);
+ switch (aKey) {
+ case mozilla::dom::MediaControlKey::Play:
+ return SUCCEEDED(mControls->put_IsPlayEnabled(aEnable));
+ case mozilla::dom::MediaControlKey::Pause:
+ return SUCCEEDED(mControls->put_IsPauseEnabled(aEnable));
+ case mozilla::dom::MediaControlKey::Previoustrack:
+ return SUCCEEDED(mControls->put_IsPreviousEnabled(aEnable));
+ case mozilla::dom::MediaControlKey::Nexttrack:
+ return SUCCEEDED(mControls->put_IsNextEnabled(aEnable));
+ case mozilla::dom::MediaControlKey::Stop:
+ return SUCCEEDED(mControls->put_IsStopEnabled(aEnable));
+ default:
+ LOG("No button for %s", ToMediaControlKeyStr(aKey));
+ return false;
+ }
+}
+
+bool WindowsSMTCProvider::InitDisplayAndControls() {
+ // As Open() might be called multiple times, "cache" the results of the COM
+ // API
+ if (mControls && mDisplay) {
+ return true;
+ }
+ ComPtr<ISystemMediaTransportControlsInterop> interop;
+ HRESULT hr = GetActivationFactory(
+ HStringReference(RuntimeClass_Windows_Media_SystemMediaTransportControls)
+ .Get(),
+ interop.GetAddressOf());
+ if (FAILED(hr)) {
+ LOG("SystemMediaTransportControls: Failed at instantiating the "
+ "Interop object");
+ return false;
+ }
+ MOZ_ASSERT(interop);
+
+ if (!mControls && FAILED(interop->GetForWindow(
+ mWindow, IID_PPV_ARGS(mControls.GetAddressOf())))) {
+ LOG("SystemMediaTransportControls: Failed at GetForWindow()");
+ return false;
+ }
+ MOZ_ASSERT(mControls);
+
+ if (!mDisplay &&
+ FAILED(mControls->get_DisplayUpdater(mDisplay.GetAddressOf()))) {
+ LOG("SystemMediaTransportControls: Failed at get_DisplayUpdater()");
+ }
+
+ MOZ_ASSERT(mDisplay);
+ return true;
+}
+
+bool WindowsSMTCProvider::SetMusicMetadata(const nsString& aArtist,
+ const nsString& aTitle) {
+ MOZ_ASSERT(mDisplay);
+ ComPtr<IMusicDisplayProperties> musicProps;
+
+ HRESULT hr = mDisplay->put_Type(MediaPlaybackType::MediaPlaybackType_Music);
+ MOZ_ASSERT(SUCCEEDED(hr));
+ Unused << hr;
+ hr = mDisplay->get_MusicProperties(musicProps.GetAddressOf());
+ if (FAILED(hr)) {
+ LOG("Failed to get music properties");
+ return false;
+ }
+
+ hr = musicProps->put_Artist(HStringReference(aArtist.get()).Get());
+ if (FAILED(hr)) {
+ LOG("Failed to set the music's artist");
+ return false;
+ }
+
+ hr = musicProps->put_Title(HStringReference(aTitle.get()).Get());
+ if (FAILED(hr)) {
+ LOG("Failed to set the music's title");
+ return false;
+ }
+
+ hr = mDisplay->Update();
+ if (FAILED(hr)) {
+ LOG("Failed to refresh the display");
+ return false;
+ }
+
+ return true;
+}
+
+void WindowsSMTCProvider::LoadThumbnail(
+ const nsTArray<mozilla::dom::MediaImage>& aArtwork) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // TODO: Sort the images by the preferred size or format.
+ mArtwork = aArtwork;
+ mNextImageIndex = 0;
+
+ // Abort the loading if
+ // 1) thumbnail is being updated, and one in processing is in the artwork
+ // 2) thumbnail is not being updated, and one in use is in the artwork
+ if (!mProcessingUrl.IsEmpty()) {
+ LOG("Load thumbnail while image: %s is being processed",
+ NS_ConvertUTF16toUTF8(mProcessingUrl).get());
+ if (mozilla::dom::IsImageIn(mArtwork, mProcessingUrl)) {
+ LOG("No need to load thumbnail. The one being processed is in the "
+ "artwork");
+ return;
+ }
+ } else if (!mThumbnailUrl.IsEmpty()) {
+ if (mozilla::dom::IsImageIn(mArtwork, mThumbnailUrl)) {
+ LOG("No need to load thumbnail. The one in use is in the artwork");
+ return;
+ }
+ }
+
+ // If there is a pending image store operation, that image must be different
+ // from the new image will be loaded below, so the pending one should be
+ // cancelled.
+ CancelPendingStoreAsyncOperation();
+ // Remove the current thumbnail on the interface
+ ClearThumbnail();
+ // Then load the new thumbnail asynchronously
+ LoadImageAtIndex(mNextImageIndex++);
+}
+
+void WindowsSMTCProvider::LoadImageAtIndex(const size_t aIndex) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aIndex >= mArtwork.Length()) {
+ LOG("Stop loading thumbnail. No more available images");
+ mImageFetchRequest.DisconnectIfExists();
+ mProcessingUrl.Truncate();
+ return;
+ }
+
+ const mozilla::dom::MediaImage& image = mArtwork[aIndex];
+
+ // TODO: No need to fetch the default image and do image processing since the
+ // the default image is local file and it's trustworthy. For the default
+ // image, we can use `CreateFromFile` to create the IRandomAccessStream. We
+ // should probably cache it since it could be used very often (Bug 1643102)
+
+ if (!mozilla::dom::IsValidImageUrl(image.mSrc)) {
+ LOG("Skip the image with invalid URL. Try next image");
+ mImageFetchRequest.DisconnectIfExists();
+ LoadImageAtIndex(mNextImageIndex++);
+ return;
+ }
+
+ mImageFetchRequest.DisconnectIfExists();
+ mProcessingUrl = image.mSrc;
+
+ mImageFetcher = mozilla::MakeUnique<mozilla::dom::FetchImageHelper>(image);
+ RefPtr<WindowsSMTCProvider> self = this;
+ mImageFetcher->FetchImage()
+ ->Then(
+ AbstractThread::MainThread(), __func__,
+ [this, self](const nsCOMPtr<imgIContainer>& aImage) {
+ LOG("The image is fetched successfully");
+ mImageFetchRequest.Complete();
+
+ // Although IMAGE_JPEG or IMAGE_BMP are valid types as well, but a
+ // png image with transparent background will be converted into a
+ // jpeg/bmp file with a colored background. IMAGE_PNG format seems
+ // to be the best choice for now.
+ uint32_t size = 0;
+ char* src = nullptr;
+ // Only used to hold the image data
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = mozilla::dom::GetEncodedImageBuffer(
+ aImage, nsLiteralCString(IMAGE_PNG),
+ getter_AddRefs(inputStream), &size, &src);
+ if (NS_FAILED(rv) || !inputStream || size == 0 || !src) {
+ LOG("Failed to get the image buffer info. Try next image");
+ LoadImageAtIndex(mNextImageIndex++);
+ return;
+ }
+
+ LoadImage(src, size);
+ },
+ [this, self](bool) {
+ LOG("Failed to fetch image. Try next image");
+ mImageFetchRequest.Complete();
+ LoadImageAtIndex(mNextImageIndex++);
+ })
+ ->Track(mImageFetchRequest);
+}
+
+void WindowsSMTCProvider::LoadImage(const char* aImageData,
+ uint32_t aDataSize) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // 1. Use mImageDataWriter to write the binary data of image into mImageStream
+ // 2. Refer the image by mImageStreamReference and then set it to the SMTC
+ // In case of the race condition between they are being destroyed and the
+ // async operation for image loading, mImageDataWriter, mImageStream, and
+ // mImageStreamReference are member variables
+
+ HRESULT hr = ActivateInstance(
+ HStringReference(
+ RuntimeClass_Windows_Storage_Streams_InMemoryRandomAccessStream)
+ .Get(),
+ mImageStream.GetAddressOf());
+ if (FAILED(hr)) {
+ LOG("Failed to make mImageStream refer to an instance of "
+ "InMemoryRandomAccessStream");
+ return;
+ }
+
+ ComPtr<IOutputStream> outputStream;
+ hr = mImageStream.As(&outputStream);
+ if (FAILED(hr)) {
+ LOG("Failed when query IOutputStream interface from mImageStream");
+ return;
+ }
+
+ ComPtr<IDataWriterFactory> dataWriterFactory;
+ hr = GetActivationFactory(
+ HStringReference(RuntimeClass_Windows_Storage_Streams_DataWriter).Get(),
+ dataWriterFactory.GetAddressOf());
+ if (FAILED(hr)) {
+ LOG("Failed to get an activation factory for IDataWriterFactory");
+ return;
+ }
+
+ hr = dataWriterFactory->CreateDataWriter(outputStream.Get(),
+ mImageDataWriter.GetAddressOf());
+ if (FAILED(hr)) {
+ LOG("Failed to create mImageDataWriter that writes data to mImageStream");
+ return;
+ }
+
+ hr = mImageDataWriter->WriteBytes(
+ aDataSize, reinterpret_cast<BYTE*>(const_cast<char*>(aImageData)));
+ if (FAILED(hr)) {
+ LOG("Failed to write data to mImageStream");
+ return;
+ }
+
+ hr = mImageDataWriter->StoreAsync(&mStoreAsyncOperation);
+ if (FAILED(hr)) {
+ LOG("Failed to create a DataWriterStoreOperation for mStoreAsyncOperation");
+ return;
+ }
+
+ // Upon the image is stored in mImageStream, set the image to the SMTC
+ // interface
+ auto onStoreCompleted = Callback<
+ IAsyncOperationCompletedHandler<unsigned int>>(
+ [this, self = RefPtr<WindowsSMTCProvider>(this),
+ aImageUrl = nsString(mProcessingUrl)](
+ IAsyncOperation<unsigned int>* aAsyncOp, AsyncStatus aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aStatus != AsyncStatus::Completed) {
+ LOG("Asynchronous operation is not completed");
+ return E_ABORT;
+ }
+
+ HRESULT hr = S_OK;
+ IAsyncInfo* asyncInfo = GetIAsyncInfo(aAsyncOp);
+ asyncInfo->get_ErrorCode(&hr);
+ if (FAILED(hr)) {
+ LOG("Failed to get termination status of the asynchronous operation");
+ return hr;
+ }
+
+ if (!UpdateThumbnail(aImageUrl)) {
+ LOG("Failed to update thumbnail");
+ }
+
+ // If an error occurs above:
+ // - If aImageUrl is not mProcessingUrl. It's fine.
+ // - If aImageUrl is mProcessingUrl, then mProcessingUrl won't be reset.
+ // Therefore the thumbnail will remain empty until a new image whose
+ // url is different from mProcessingUrl is loaded.
+
+ return S_OK;
+ });
+
+ hr = mStoreAsyncOperation->put_Completed(onStoreCompleted.Get());
+ if (FAILED(hr)) {
+ LOG("Failed to set callback on completeing the asynchronous operation");
+ }
+}
+
+bool WindowsSMTCProvider::SetThumbnail(const nsAString& aUrl) {
+ MOZ_ASSERT(mDisplay);
+ MOZ_ASSERT(mImageStream);
+ MOZ_ASSERT(!aUrl.IsEmpty());
+
+ ComPtr<IRandomAccessStreamReferenceStatics> streamRefFactory;
+
+ HRESULT hr = GetActivationFactory(
+ HStringReference(
+ RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference)
+ .Get(),
+ streamRefFactory.GetAddressOf());
+ auto cleanup =
+ MakeScopeExit([this, self = RefPtr<WindowsSMTCProvider>(this)] {
+ LOG("Clean mThumbnailUrl");
+ mThumbnailUrl.Truncate();
+ });
+
+ if (FAILED(hr)) {
+ LOG("Failed to get an activation factory for "
+ "IRandomAccessStreamReferenceStatics type");
+ return false;
+ }
+
+ hr = streamRefFactory->CreateFromStream(mImageStream.Get(),
+ mImageStreamReference.GetAddressOf());
+ if (FAILED(hr)) {
+ LOG("Failed to create mImageStreamReference from mImageStream");
+ return false;
+ }
+
+ hr = mDisplay->put_Thumbnail(mImageStreamReference.Get());
+ if (FAILED(hr)) {
+ LOG("Failed to update thumbnail");
+ return false;
+ }
+
+ hr = mDisplay->Update();
+ if (FAILED(hr)) {
+ LOG("Failed to refresh display");
+ return false;
+ }
+
+ // No need to clean mThumbnailUrl since thumbnail is set successfully
+ cleanup.release();
+ mThumbnailUrl = aUrl;
+
+ return true;
+}
+
+void WindowsSMTCProvider::ClearThumbnail() {
+ MOZ_ASSERT(mDisplay);
+ HRESULT hr = mDisplay->put_Thumbnail(nullptr);
+ MOZ_ASSERT(SUCCEEDED(hr));
+ hr = mDisplay->Update();
+ MOZ_ASSERT(SUCCEEDED(hr));
+ Unused << hr;
+ mThumbnailUrl.Truncate();
+}
+
+bool WindowsSMTCProvider::UpdateThumbnail(const nsAString& aUrl) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!IsOpened()) {
+ LOG("Abort the thumbnail update: SMTC is closed");
+ return false;
+ }
+
+ if (aUrl != mProcessingUrl) {
+ LOG("Abort the thumbnail update: The image from %s is out of date",
+ NS_ConvertUTF16toUTF8(aUrl).get());
+ return false;
+ }
+
+ mProcessingUrl.Truncate();
+
+ if (!SetThumbnail(aUrl)) {
+ LOG("Failed to update thumbnail");
+ return false;
+ }
+
+ MOZ_ASSERT(mThumbnailUrl == aUrl);
+ LOG("The thumbnail is updated to the image from: %s",
+ NS_ConvertUTF16toUTF8(mThumbnailUrl).get());
+ return true;
+}
+
+void WindowsSMTCProvider::CancelPendingStoreAsyncOperation() const {
+ if (mStoreAsyncOperation) {
+ IAsyncInfo* asyncInfo = GetIAsyncInfo(mStoreAsyncOperation.Get());
+ asyncInfo->Cancel();
+ }
+}
+
+#endif // __MINGW32__
diff --git a/widget/windows/WindowsSMTCProvider.h b/widget/windows/WindowsSMTCProvider.h
new file mode 100644
index 0000000000..3926618d1f
--- /dev/null
+++ b/widget/windows/WindowsSMTCProvider.h
@@ -0,0 +1,128 @@
+/* -*- 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/. */
+
+#ifndef WIDGET_WINDOWS_WINDOWSSTMCPROVIDER_H_
+#define WIDGET_WINDOWS_WINDOWSSTMCPROVIDER_H_
+
+#ifndef __MINGW32__
+
+# include <functional>
+# include <Windows.Media.h>
+# include <wrl.h>
+
+# include "mozilla/dom/FetchImageHelper.h"
+# include "mozilla/dom/MediaController.h"
+# include "mozilla/dom/MediaControlKeySource.h"
+# include "mozilla/UniquePtr.h"
+
+using ISMTC = ABI::Windows::Media::ISystemMediaTransportControls;
+using SMTCProperty = ABI::Windows::Media::SystemMediaTransportControlsProperty;
+using ISMTCDisplayUpdater =
+ ABI::Windows::Media::ISystemMediaTransportControlsDisplayUpdater;
+
+using ABI::Windows::Foundation::IAsyncOperation;
+using ABI::Windows::Storage::Streams::IDataWriter;
+using ABI::Windows::Storage::Streams::IRandomAccessStream;
+using ABI::Windows::Storage::Streams::IRandomAccessStreamReference;
+using Microsoft::WRL::ComPtr;
+
+class WindowsSMTCProvider final : public mozilla::dom::MediaControlKeySource {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsSMTCProvider, override)
+
+ public:
+ WindowsSMTCProvider();
+
+ bool IsOpened() const override;
+ bool Open() override;
+ void Close() override;
+
+ void SetPlaybackState(
+ mozilla::dom::MediaSessionPlaybackState aState) override;
+
+ void SetMediaMetadata(
+ const mozilla::dom::MediaMetadataBase& aMetadata) override;
+
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override;
+
+ private:
+ ~WindowsSMTCProvider();
+ void UnregisterEvents();
+ bool RegisterEvents();
+
+ void OnButtonPressed(mozilla::dom::MediaControlKey aKey) const;
+ // Enable the SMTC interface
+ bool EnableControl(bool aEnabled) const;
+ // Sets the play, pause, next, previous buttons on the SMTC interface by
+ // mSupportedKeys
+ bool UpdateButtons() const;
+ bool IsKeySupported(mozilla::dom::MediaControlKey aKey) const;
+ bool EnableKey(mozilla::dom::MediaControlKey aKey, bool aEnable) const;
+
+ bool InitDisplayAndControls();
+
+ // Sets the Metadata for the currently playing media and sets the playback
+ // type to "MUSIC"
+ bool SetMusicMetadata(const nsString& aArtist, const nsString& aTitle);
+
+ // Sets one of the artwork to the SMTC interface asynchronously
+ void LoadThumbnail(const nsTArray<mozilla::dom::MediaImage>& aArtwork);
+ // Stores the image at index aIndex of the mArtwork to the Thumbnail
+ // asynchronously
+ void LoadImageAtIndex(const size_t aIndex);
+ // Stores the raw binary data of an image to mImageStream and set it to the
+ // Thumbnail asynchronously
+ void LoadImage(const char* aImageData, uint32_t aDataSize);
+ // Sets the Thumbnail to the image stored in mImageStream
+ bool SetThumbnail(const nsAString& aUrl);
+ void ClearThumbnail();
+
+ bool UpdateThumbnail(const nsAString& aUrl);
+ void CancelPendingStoreAsyncOperation() const;
+
+ void ClearMetadata();
+
+ bool mInitialized = false;
+
+ // A bit table indicating what keys are enabled
+ uint32_t mSupportedKeys = 0;
+
+ ComPtr<ISMTC> mControls;
+ ComPtr<ISMTCDisplayUpdater> mDisplay;
+
+ // Use mImageDataWriter to write the binary data of image into mImageStream
+ // and refer the image by mImageStreamReference and then set it to the SMTC
+ // interface
+ ComPtr<IDataWriter> mImageDataWriter;
+ ComPtr<IRandomAccessStream> mImageStream;
+ ComPtr<IRandomAccessStreamReference> mImageStreamReference;
+ ComPtr<IAsyncOperation<unsigned int>> mStoreAsyncOperation;
+
+ // mThumbnailUrl is the url of the current Thumbnail
+ // mProcessingUrl is the url that is being processed. The process starts from
+ // fetching an image from the url and then storing the fetched image to the
+ // mImageStream. If mProcessingUrl is not empty, it means there is an image is
+ // in processing
+ // mThumbnailUrl and mProcessingUrl won't be set at the same time and they can
+ // only be touched on main thread
+ nsString mThumbnailUrl;
+ nsString mProcessingUrl;
+
+ // mArtwork can only be used in main thread in case of data racing
+ CopyableTArray<mozilla::dom::MediaImage> mArtwork;
+ size_t mNextImageIndex;
+
+ mozilla::UniquePtr<mozilla::dom::FetchImageHelper> mImageFetcher;
+ mozilla::MozPromiseRequestHolder<mozilla::dom::ImagePromise>
+ mImageFetchRequest;
+
+ HWND mWindow; // handle to the invisible window
+
+ // EventRegistrationTokens are used to have a handle on a callback (to remove
+ // it again)
+ EventRegistrationToken mButtonPressedToken;
+};
+
+#endif // __MINGW32__
+#endif // WIDGET_WINDOWS_WINDOWSSTMCPROVIDER_H_
diff --git a/widget/windows/WindowsUIUtils.cpp b/widget/windows/WindowsUIUtils.cpp
new file mode 100644
index 0000000000..3d5cff7e23
--- /dev/null
+++ b/widget/windows/WindowsUIUtils.cpp
@@ -0,0 +1,809 @@
+/* -*- 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 <windows.h>
+#include <wrl.h>
+
+#include "nsServiceManagerUtils.h"
+
+#include "WindowsUIUtils.h"
+
+#include "nsIObserverService.h"
+#include "nsIAppShellService.h"
+#include "nsAppShellCID.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/WidgetUtils.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/media/MediaUtils.h"
+#include "nsString.h"
+#include "nsGlobalWindowOuter.h"
+#include "nsIWidget.h"
+#include "nsIWindowMediator.h"
+#include "nsPIDOMWindow.h"
+#include "nsWindowGfx.h"
+#include "Units.h"
+
+/* mingw currently doesn't support windows.ui.viewmanagement.h, so we disable it
+ * until it's fixed. */
+
+// See
+// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/winrt/windows.ui.viewmanagement.h
+// for the source of some of these definitions for older SDKs.
+#ifndef __MINGW32__
+
+# include <inspectable.h>
+# include <roapi.h>
+# include <windows.ui.viewmanagement.h>
+
+# pragma comment(lib, "runtimeobject.lib")
+
+using namespace ABI::Windows::UI;
+using namespace ABI::Windows::UI::ViewManagement;
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+using namespace ABI::Windows::Foundation;
+using namespace ABI::Windows::ApplicationModel::DataTransfer;
+
+# ifndef RuntimeClass_Windows_UI_ViewManagement_UIViewSettings
+# define RuntimeClass_Windows_UI_ViewManagement_UIViewSettings \
+ L"Windows.UI.ViewManagement.UIViewSettings"
+# endif
+
+# ifndef IUIViewSettingsInterop
+
+using IUIViewSettingsInterop = interface IUIViewSettingsInterop;
+
+MIDL_INTERFACE("3694dbf9-8f68-44be-8ff5-195c98ede8a6")
+IUIViewSettingsInterop : public IInspectable {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE GetForWindow(HWND hwnd, REFIID riid,
+ void** ppv) = 0;
+};
+# endif
+
+# ifndef __IDataTransferManagerInterop_INTERFACE_DEFINED__
+# define __IDataTransferManagerInterop_INTERFACE_DEFINED__
+
+using IDataTransferManagerInterop = interface IDataTransferManagerInterop;
+
+MIDL_INTERFACE("3A3DCD6C-3EAB-43DC-BCDE-45671CE800C8")
+IDataTransferManagerInterop : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE GetForWindow(
+ HWND appWindow, REFIID riid, void** dataTransferManager) = 0;
+ virtual HRESULT STDMETHODCALLTYPE ShowShareUIForWindow(HWND appWindow) = 0;
+};
+
+# endif
+
+# if !defined( \
+ ____x_ABI_CWindows_CApplicationModel_CDataTransfer_CIDataPackage4_INTERFACE_DEFINED__)
+# define ____x_ABI_CWindows_CApplicationModel_CDataTransfer_CIDataPackage4_INTERFACE_DEFINED__
+
+MIDL_INTERFACE("13a24ec8-9382-536f-852a-3045e1b29a3b")
+IDataPackage4 : public IInspectable {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE add_ShareCanceled(
+ __FITypedEventHandler_2_Windows__CApplicationModel__CDataTransfer__CDataPackage_IInspectable *
+ handler,
+ EventRegistrationToken * token) = 0;
+ virtual HRESULT STDMETHODCALLTYPE remove_ShareCanceled(
+ EventRegistrationToken token) = 0;
+};
+
+# endif
+
+# ifndef RuntimeClass_Windows_UI_ViewManagement_UISettings
+# define RuntimeClass_Windows_UI_ViewManagement_UISettings \
+ L"Windows.UI.ViewManagement.UISettings"
+# endif
+# if WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION < 0x80000
+namespace ABI {
+namespace Windows {
+namespace UI {
+namespace ViewManagement {
+
+class UISettings;
+class UISettingsAutoHideScrollBarsChangedEventArgs;
+interface IUISettingsAutoHideScrollBarsChangedEventArgs;
+MIDL_INTERFACE("87afd4b2-9146-5f02-8f6b-06d454174c0f")
+IUISettingsAutoHideScrollBarsChangedEventArgs : public IInspectable{};
+
+} // namespace ViewManagement
+} // namespace UI
+} // namespace Windows
+} // namespace ABI
+
+namespace ABI {
+namespace Windows {
+namespace Foundation {
+
+template <>
+struct __declspec(uuid("808aef30-2660-51b0-9c11-f75dd42006b4"))
+ ITypedEventHandler<ABI::Windows::UI::ViewManagement::UISettings*,
+ ABI::Windows::UI::ViewManagement::
+ UISettingsAutoHideScrollBarsChangedEventArgs*>
+ : ITypedEventHandler_impl<
+ ABI::Windows::Foundation::Internal::AggregateType<
+ ABI::Windows::UI::ViewManagement::UISettings*,
+ ABI::Windows::UI::ViewManagement::IUISettings*>,
+ ABI::Windows::Foundation::Internal::AggregateType<
+ ABI::Windows::UI::ViewManagement::
+ UISettingsAutoHideScrollBarsChangedEventArgs*,
+ ABI::Windows::UI::ViewManagement::
+ IUISettingsAutoHideScrollBarsChangedEventArgs*>> {
+ static const wchar_t* z_get_rc_name_impl() {
+ return L"Windows.Foundation.TypedEventHandler`2<Windows.UI.ViewManagement."
+ L"UISettings, "
+ L"Windows.UI.ViewManagement."
+ L"UISettingsAutoHideScrollBarsChangedEventArgs>";
+ }
+};
+// Define a typedef for the parameterized interface specialization's mangled
+// name. This allows code which uses the mangled name for the parameterized
+// interface to access the correct parameterized interface specialization.
+typedef ITypedEventHandler<ABI::Windows::UI::ViewManagement::UISettings*,
+ ABI::Windows::UI::ViewManagement::
+ UISettingsAutoHideScrollBarsChangedEventArgs*>
+ __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs_t;
+# define __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs \
+ ABI::Windows::Foundation:: \
+ __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs_t
+
+} // namespace Foundation
+} // namespace Windows
+} // namespace ABI
+
+namespace ABI {
+namespace Windows {
+namespace UI {
+namespace ViewManagement {
+class UISettings;
+class UISettingsAutoHideScrollBarsChangedEventArgs;
+interface IUISettings5;
+MIDL_INTERFACE("5349d588-0cb5-5f05-bd34-706b3231f0bd")
+IUISettings5 : public IInspectable {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE get_AutoHideScrollBars(boolean * value) = 0;
+ virtual HRESULT STDMETHODCALLTYPE add_AutoHideScrollBarsChanged(
+ __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs *
+ handler,
+ EventRegistrationToken * token) = 0;
+ virtual HRESULT STDMETHODCALLTYPE remove_AutoHideScrollBarsChanged(
+ EventRegistrationToken token) = 0;
+};
+} // namespace ViewManagement
+} // namespace UI
+} // namespace Windows
+} // namespace ABI
+# endif
+#endif
+
+using namespace mozilla;
+
+enum class TabletModeState : uint8_t { Unknown, Off, On };
+static TabletModeState sInTabletModeState;
+
+WindowsUIUtils::WindowsUIUtils() = default;
+WindowsUIUtils::~WindowsUIUtils() = default;
+
+NS_IMPL_ISUPPORTS(WindowsUIUtils, nsIWindowsUIUtils)
+
+NS_IMETHODIMP
+WindowsUIUtils::GetSystemSmallIconSize(int32_t* aSize) {
+ NS_ENSURE_ARG(aSize);
+
+ mozilla::LayoutDeviceIntSize size =
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon);
+ *aSize = std::max(size.width, size.height);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsUIUtils::GetSystemLargeIconSize(int32_t* aSize) {
+ NS_ENSURE_ARG(aSize);
+
+ mozilla::LayoutDeviceIntSize size =
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon);
+ *aSize = std::max(size.width, size.height);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsUIUtils::SetWindowIcon(mozIDOMWindowProxy* aWindow,
+ imgIContainer* aSmallIcon,
+ imgIContainer* aBigIcon) {
+ NS_ENSURE_ARG(aWindow);
+
+ nsCOMPtr<nsIWidget> widget =
+ nsGlobalWindowOuter::Cast(aWindow)->GetMainWidget();
+ nsWindow* window = static_cast<nsWindow*>(widget.get());
+
+ nsresult rv;
+
+ if (aSmallIcon) {
+ HICON hIcon = nullptr;
+ rv = nsWindowGfx::CreateIcon(
+ aSmallIcon, false, mozilla::LayoutDeviceIntPoint(),
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon), &hIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ window->SetSmallIcon(hIcon);
+ }
+
+ if (aBigIcon) {
+ HICON hIcon = nullptr;
+ rv = nsWindowGfx::CreateIcon(
+ aBigIcon, false, mozilla::LayoutDeviceIntPoint(),
+ nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon), &hIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ window->SetBigIcon(hIcon);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsUIUtils::SetWindowIconFromExe(mozIDOMWindowProxy* aWindow,
+ const nsAString& aExe, uint16_t aIndex) {
+ NS_ENSURE_ARG(aWindow);
+
+ nsCOMPtr<nsIWidget> widget =
+ nsGlobalWindowOuter::Cast(aWindow)->GetMainWidget();
+ nsWindow* window = static_cast<nsWindow*>(widget.get());
+
+ HICON icon = ::LoadIconW(::GetModuleHandleW(PromiseFlatString(aExe).get()),
+ MAKEINTRESOURCEW(aIndex));
+ window->SetBigIcon(icon);
+ window->SetSmallIcon(icon);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsUIUtils::SetWindowIconNoData(mozIDOMWindowProxy* aWindow) {
+ NS_ENSURE_ARG(aWindow);
+
+ nsCOMPtr<nsIWidget> widget =
+ nsGlobalWindowOuter::Cast(aWindow)->GetMainWidget();
+ nsWindow* window = static_cast<nsWindow*>(widget.get());
+
+ window->SetSmallIconNoData();
+ window->SetBigIconNoData();
+
+ return NS_OK;
+}
+
+bool WindowsUIUtils::GetInTabletMode() {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ if (sInTabletModeState == TabletModeState::Unknown) {
+ UpdateInTabletMode();
+ }
+ return sInTabletModeState == TabletModeState::On;
+}
+
+NS_IMETHODIMP
+WindowsUIUtils::GetInTabletMode(bool* aResult) {
+ *aResult = GetInTabletMode();
+ return NS_OK;
+}
+
+static IInspectable* GetUISettings() {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+#ifndef __MINGW32__
+ // We need to keep this alive for ~ever so that change callbacks work as
+ // expected, sigh.
+ static StaticRefPtr<IInspectable> sUiSettingsAsInspectable;
+
+ if (!sUiSettingsAsInspectable) {
+ ComPtr<IInspectable> uiSettingsAsInspectable;
+ ::RoActivateInstance(
+ HStringReference(RuntimeClass_Windows_UI_ViewManagement_UISettings)
+ .Get(),
+ &uiSettingsAsInspectable);
+ if (NS_WARN_IF(!uiSettingsAsInspectable)) {
+ return nullptr;
+ }
+
+ ComPtr<IUISettings5> uiSettings5;
+ if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings5))) {
+ EventRegistrationToken unusedToken;
+ auto callback = Callback<ITypedEventHandler<
+ UISettings*, UISettingsAutoHideScrollBarsChangedEventArgs*>>(
+ [](auto...) {
+ // Scrollbar sizes change layout.
+ LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::StyleAndLayout);
+ return S_OK;
+ });
+ (void)NS_WARN_IF(FAILED(uiSettings5->add_AutoHideScrollBarsChanged(
+ callback.Get(), &unusedToken)));
+ }
+
+ ComPtr<IUISettings2> uiSettings2;
+ if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings2))) {
+ EventRegistrationToken unusedToken;
+ auto callback =
+ Callback<ITypedEventHandler<UISettings*, IInspectable*>>([](auto...) {
+ // Text scale factor changes style and layout.
+ LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::StyleAndLayout);
+ return S_OK;
+ });
+ (void)NS_WARN_IF(FAILED(uiSettings2->add_TextScaleFactorChanged(
+ callback.Get(), &unusedToken)));
+ }
+
+ ComPtr<IUISettings3> uiSettings3;
+ if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings3))) {
+ EventRegistrationToken unusedToken;
+ auto callback =
+ Callback<ITypedEventHandler<UISettings*, IInspectable*>>([](auto...) {
+ // System color changes change style only.
+ LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::Style);
+ return S_OK;
+ });
+ (void)NS_WARN_IF(FAILED(
+ uiSettings3->add_ColorValuesChanged(callback.Get(), &unusedToken)));
+ }
+
+ ComPtr<IUISettings4> uiSettings4;
+ if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings4))) {
+ EventRegistrationToken unusedToken;
+ auto callback =
+ Callback<ITypedEventHandler<UISettings*, IInspectable*>>([](auto...) {
+ // Transparent effects changes change media queries only.
+ LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::MediaQueriesOnly);
+ return S_OK;
+ });
+ (void)NS_WARN_IF(FAILED(uiSettings4->add_AdvancedEffectsEnabledChanged(
+ callback.Get(), &unusedToken)));
+ }
+
+ sUiSettingsAsInspectable = dont_AddRef(uiSettingsAsInspectable.Detach());
+ ClearOnShutdown(&sUiSettingsAsInspectable);
+ }
+
+ return sUiSettingsAsInspectable.get();
+#else
+ return nullptr;
+#endif
+}
+
+Maybe<nscolor> WindowsUIUtils::GetAccentColor(int aTone) {
+ MOZ_ASSERT(aTone >= -3);
+ MOZ_ASSERT(aTone <= 3);
+#ifndef __MINGW32__
+ ComPtr<IInspectable> settings = GetUISettings();
+ if (NS_WARN_IF(!settings)) {
+ return Nothing();
+ }
+ ComPtr<IUISettings3> uiSettings3;
+ if (NS_WARN_IF(FAILED(settings.As(&uiSettings3)))) {
+ return Nothing();
+ }
+ Color color;
+ auto colorType = UIColorType(int(UIColorType_Accent) + aTone);
+ if (NS_WARN_IF(FAILED(uiSettings3->GetColorValue(colorType, &color)))) {
+ return Nothing();
+ }
+ return Some(NS_RGBA(color.R, color.G, color.B, color.A));
+#else
+ return Nothing();
+#endif
+}
+
+Maybe<nscolor> WindowsUIUtils::GetSystemColor(ColorScheme aScheme,
+ int aSysColor) {
+#ifndef __MINGW32__
+ if (!StaticPrefs::widget_windows_uwp_system_colors_enabled()) {
+ return Nothing();
+ }
+
+ // https://docs.microsoft.com/en-us/windows/apps/design/style/color
+ // Is a useful resource to see which values have decent contrast.
+ if (StaticPrefs::widget_windows_uwp_system_colors_highlight_accent()) {
+ if (aSysColor == COLOR_HIGHLIGHT) {
+ int tone = aScheme == ColorScheme::Light ? 0 : -1;
+ if (auto c = GetAccentColor(tone)) {
+ return c;
+ }
+ }
+ if (aSysColor == COLOR_HIGHLIGHTTEXT && GetAccentColor()) {
+ return Some(NS_RGBA(255, 255, 255, 255));
+ }
+ }
+
+ if (aScheme == ColorScheme::Dark) {
+ // There are no explicitly dark colors in UWP, other than the highlight
+ // colors above.
+ return Nothing();
+ }
+
+ auto knownType = [&]() -> Maybe<UIElementType> {
+# define MAP(_win32, _uwp) \
+ case COLOR_##_win32: \
+ return Some(UIElementType_##_uwp)
+ switch (aSysColor) {
+ MAP(HIGHLIGHT, Highlight);
+ MAP(HIGHLIGHTTEXT, HighlightText);
+ MAP(ACTIVECAPTION, ActiveCaption);
+ MAP(BTNFACE, ButtonFace);
+ MAP(BTNTEXT, ButtonText);
+ MAP(CAPTIONTEXT, CaptionText);
+ MAP(GRAYTEXT, GrayText);
+ MAP(HOTLIGHT, Hotlight);
+ MAP(INACTIVECAPTION, InactiveCaption);
+ MAP(INACTIVECAPTIONTEXT, InactiveCaptionText);
+ MAP(WINDOW, Window);
+ MAP(WINDOWTEXT, WindowText);
+ default:
+ return Nothing();
+ }
+# undef MAP
+ }();
+ if (!knownType) {
+ return Nothing();
+ }
+ ComPtr<IInspectable> settings = GetUISettings();
+ if (NS_WARN_IF(!settings)) {
+ return Nothing();
+ }
+ ComPtr<IUISettings> uiSettings;
+ if (NS_WARN_IF(FAILED(settings.As(&uiSettings)))) {
+ return Nothing();
+ }
+ Color color;
+ if (NS_WARN_IF(FAILED(uiSettings->UIElementColor(*knownType, &color)))) {
+ return Nothing();
+ }
+ return Some(NS_RGBA(color.R, color.G, color.B, color.A));
+#else
+ return Nothing();
+#endif
+}
+bool WindowsUIUtils::ComputeOverlayScrollbars() {
+#ifndef __MINGW32__
+ if (!IsWin11OrLater()) {
+ // While in theory Windows 10 supports overlay scrollbar settings, it's off
+ // by default and it's untested whether our Win10 scrollbar drawing code
+ // deals with it properly.
+ return false;
+ }
+ if (!StaticPrefs::widget_windows_overlay_scrollbars_enabled()) {
+ return false;
+ }
+ ComPtr<IInspectable> settings = GetUISettings();
+ if (NS_WARN_IF(!settings)) {
+ return false;
+ }
+ ComPtr<IUISettings5> uiSettings5;
+ if (NS_WARN_IF(FAILED(settings.As(&uiSettings5)))) {
+ return false;
+ }
+ boolean autoHide = false;
+ if (NS_WARN_IF(FAILED(uiSettings5->get_AutoHideScrollBars(&autoHide)))) {
+ return false;
+ }
+ return autoHide;
+#else
+ return false;
+#endif
+}
+
+double WindowsUIUtils::ComputeTextScaleFactor() {
+#ifndef __MINGW32__
+ ComPtr<IInspectable> settings = GetUISettings();
+ if (NS_WARN_IF(!settings)) {
+ return 1.0;
+ }
+ ComPtr<IUISettings2> uiSettings2;
+ if (NS_WARN_IF(FAILED(settings.As(&uiSettings2)))) {
+ return false;
+ }
+ double scaleFactor = 1.0;
+ if (NS_WARN_IF(FAILED(uiSettings2->get_TextScaleFactor(&scaleFactor)))) {
+ return 1.0;
+ }
+ return scaleFactor;
+#else
+ return 1.0;
+#endif
+}
+
+bool WindowsUIUtils::ComputeTransparencyEffects() {
+ constexpr bool kDefault = true;
+#ifndef __MINGW32__
+ ComPtr<IInspectable> settings = GetUISettings();
+ if (NS_WARN_IF(!settings)) {
+ return kDefault;
+ }
+ ComPtr<IUISettings4> uiSettings4;
+ if (NS_WARN_IF(FAILED(settings.As(&uiSettings4)))) {
+ return kDefault;
+ }
+ boolean transparencyEffects = kDefault;
+ if (NS_WARN_IF(FAILED(
+ uiSettings4->get_AdvancedEffectsEnabled(&transparencyEffects)))) {
+ return kDefault;
+ }
+ return transparencyEffects;
+#else
+ return kDefault;
+#endif
+}
+
+void WindowsUIUtils::UpdateInTabletMode() {
+#ifndef __MINGW32__
+ nsresult rv;
+ nsCOMPtr<nsIWindowMediator> winMediator(
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> widget;
+ nsCOMPtr<mozIDOMWindowProxy> navWin;
+
+ rv = winMediator->GetMostRecentWindow(u"navigator:browser",
+ getter_AddRefs(navWin));
+ if (NS_FAILED(rv) || !navWin) {
+ // Fall back to the hidden window
+ nsCOMPtr<nsIAppShellService> appShell(
+ do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+
+ rv = appShell->GetHiddenDOMWindow(getter_AddRefs(navWin));
+ if (NS_FAILED(rv) || !navWin) {
+ return;
+ }
+ }
+
+ nsPIDOMWindowOuter* win = nsPIDOMWindowOuter::From(navWin);
+ widget = widget::WidgetUtils::DOMWindowToWidget(win);
+
+ if (!widget) {
+ return;
+ }
+
+ HWND winPtr = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW);
+ ComPtr<IUIViewSettingsInterop> uiViewSettingsInterop;
+
+ HRESULT hr = GetActivationFactory(
+ HStringReference(RuntimeClass_Windows_UI_ViewManagement_UIViewSettings)
+ .Get(),
+ &uiViewSettingsInterop);
+ if (FAILED(hr)) {
+ return;
+ }
+ ComPtr<IUIViewSettings> uiViewSettings;
+ hr = uiViewSettingsInterop->GetForWindow(winPtr,
+ IID_PPV_ARGS(&uiViewSettings));
+ if (FAILED(hr)) {
+ return;
+ }
+ UserInteractionMode mode;
+ hr = uiViewSettings->get_UserInteractionMode(&mode);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ TabletModeState oldTabletModeState = sInTabletModeState;
+ sInTabletModeState = mode == UserInteractionMode_Touch ? TabletModeState::On
+ : TabletModeState::Off;
+ if (sInTabletModeState != oldTabletModeState) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->NotifyObservers(nullptr, "tablet-mode-change",
+ sInTabletModeState == TabletModeState::On
+ ? u"tablet-mode"
+ : u"normal-mode");
+ }
+#endif
+}
+
+#ifndef __MINGW32__
+struct HStringDeleter {
+ using pointer = HSTRING;
+ void operator()(pointer aString) { WindowsDeleteString(aString); }
+};
+
+using HStringUniquePtr = UniquePtr<HSTRING, HStringDeleter>;
+
+Result<HStringUniquePtr, HRESULT> ConvertToWindowsString(
+ const nsAString& aStr) {
+ HSTRING rawStr;
+ HRESULT hr = WindowsCreateString(PromiseFlatString(aStr).get(), aStr.Length(),
+ &rawStr);
+ if (FAILED(hr)) {
+ return Err(hr);
+ }
+ return HStringUniquePtr(rawStr);
+}
+
+static Result<Ok, nsresult> RequestShare(
+ const std::function<HRESULT(IDataRequestedEventArgs* pArgs)>& aCallback) {
+ HWND hwnd = GetForegroundWindow();
+ if (!hwnd) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ ComPtr<IDataTransferManagerInterop> dtmInterop;
+ ComPtr<IDataTransferManager> dtm;
+
+ HRESULT hr = RoGetActivationFactory(
+ HStringReference(
+ RuntimeClass_Windows_ApplicationModel_DataTransfer_DataTransferManager)
+ .Get(),
+ IID_PPV_ARGS(&dtmInterop));
+ if (FAILED(hr) ||
+ FAILED(dtmInterop->GetForWindow(hwnd, IID_PPV_ARGS(&dtm)))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto callback = Callback<
+ ITypedEventHandler<DataTransferManager*, DataRequestedEventArgs*>>(
+ [aCallback](IDataTransferManager*,
+ IDataRequestedEventArgs* pArgs) -> HRESULT {
+ return aCallback(pArgs);
+ });
+
+ EventRegistrationToken dataRequestedToken;
+ if (FAILED(dtm->add_DataRequested(callback.Get(), &dataRequestedToken)) ||
+ FAILED(dtmInterop->ShowShareUIForWindow(hwnd))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return Ok();
+}
+
+static Result<Ok, nsresult> AddShareEventListeners(
+ const RefPtr<mozilla::media::Refcountable<MozPromiseHolder<SharePromise>>>&
+ aPromiseHolder,
+ const ComPtr<IDataPackage>& aDataPackage) {
+ ComPtr<IDataPackage3> spDataPackage3;
+
+ if (FAILED(aDataPackage.As(&spDataPackage3))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto completedCallback =
+ Callback<ITypedEventHandler<DataPackage*, ShareCompletedEventArgs*>>(
+ [aPromiseHolder](IDataPackage*,
+ IShareCompletedEventArgs*) -> HRESULT {
+ aPromiseHolder->Resolve(true, __func__);
+ return S_OK;
+ });
+
+ EventRegistrationToken dataRequestedToken;
+ if (FAILED(spDataPackage3->add_ShareCompleted(completedCallback.Get(),
+ &dataRequestedToken))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ ComPtr<IDataPackage4> spDataPackage4;
+ if (SUCCEEDED(aDataPackage.As(&spDataPackage4))) {
+ // Use SharedCanceled API only on supported versions of Windows
+ // So that the older ones can still use ShareUrl()
+
+ auto canceledCallback =
+ Callback<ITypedEventHandler<DataPackage*, IInspectable*>>(
+ [aPromiseHolder](IDataPackage*, IInspectable*) -> HRESULT {
+ aPromiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+ return S_OK;
+ });
+
+ if (FAILED(spDataPackage4->add_ShareCanceled(canceledCallback.Get(),
+ &dataRequestedToken))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ return Ok();
+}
+#endif
+
+RefPtr<SharePromise> WindowsUIUtils::Share(nsAutoString aTitle,
+ nsAutoString aText,
+ nsAutoString aUrl) {
+ auto promiseHolder = MakeRefPtr<
+ mozilla::media::Refcountable<MozPromiseHolder<SharePromise>>>();
+ RefPtr<SharePromise> promise = promiseHolder->Ensure(__func__);
+
+#ifndef __MINGW32__
+ auto result = RequestShare([promiseHolder, title = std::move(aTitle),
+ text = std::move(aText), url = std::move(aUrl)](
+ IDataRequestedEventArgs* pArgs) {
+ ComPtr<IDataRequest> spDataRequest;
+ ComPtr<IDataPackage> spDataPackage;
+ ComPtr<IDataPackage2> spDataPackage2;
+ ComPtr<IDataPackagePropertySet> spDataPackageProperties;
+
+ if (FAILED(pArgs->get_Request(&spDataRequest)) ||
+ FAILED(spDataRequest->get_Data(&spDataPackage)) ||
+ FAILED(spDataPackage.As(&spDataPackage2)) ||
+ FAILED(spDataPackage->get_Properties(&spDataPackageProperties))) {
+ promiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+ return E_FAIL;
+ }
+
+ /*
+ * Windows always requires a title, and an empty string does not work.
+ * Thus we trick the API by passing a whitespace when we have no title.
+ * https://docs.microsoft.com/en-us/windows/uwp/app-to-app/share-data
+ */
+ auto wTitle = ConvertToWindowsString((title.IsVoid() || title.Length() == 0)
+ ? nsAutoString(u" "_ns)
+ : title);
+ if (wTitle.isErr() ||
+ FAILED(spDataPackageProperties->put_Title(wTitle.unwrap().get()))) {
+ promiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+ return E_FAIL;
+ }
+
+ // Assign even if empty, as Windows requires some data to share
+ auto wText = ConvertToWindowsString(text);
+ if (wText.isErr() || FAILED(spDataPackage->SetText(wText.unwrap().get()))) {
+ promiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+ return E_FAIL;
+ }
+
+ if (!url.IsVoid()) {
+ auto wUrl = ConvertToWindowsString(url);
+ if (wUrl.isErr()) {
+ promiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+ return wUrl.unwrapErr();
+ }
+
+ ComPtr<IUriRuntimeClassFactory> uriFactory;
+ ComPtr<IUriRuntimeClass> uri;
+
+ auto hr = GetActivationFactory(
+ HStringReference(RuntimeClass_Windows_Foundation_Uri).Get(),
+ &uriFactory);
+
+ if (FAILED(hr) ||
+ FAILED(uriFactory->CreateUri(wUrl.unwrap().get(), &uri)) ||
+ FAILED(spDataPackage2->SetWebLink(uri.Get()))) {
+ promiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+ return E_FAIL;
+ }
+ }
+
+ if (!StaticPrefs::widget_windows_share_wait_action_enabled()) {
+ promiseHolder->Resolve(true, __func__);
+ } else if (AddShareEventListeners(promiseHolder, spDataPackage).isErr()) {
+ promiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+ return E_FAIL;
+ }
+
+ return S_OK;
+ });
+ if (result.isErr()) {
+ promiseHolder->Reject(result.unwrapErr(), __func__);
+ }
+#else
+ promiseHolder->Reject(NS_ERROR_FAILURE, __func__);
+#endif
+
+ return promise;
+}
+
+NS_IMETHODIMP
+WindowsUIUtils::ShareUrl(const nsAString& aUrlToShare,
+ const nsAString& aShareTitle) {
+ nsAutoString text;
+ text.SetIsVoid(true);
+ WindowsUIUtils::Share(nsAutoString(aShareTitle), text,
+ nsAutoString(aUrlToShare));
+ return NS_OK;
+}
diff --git a/widget/windows/WindowsUIUtils.h b/widget/windows/WindowsUIUtils.h
new file mode 100644
index 0000000000..a55f92c8da
--- /dev/null
+++ b/widget/windows/WindowsUIUtils.h
@@ -0,0 +1,50 @@
+/* -*- 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/. */
+
+#ifndef mozilla_widget_WindowsUIUtils_h__
+#define mozilla_widget_WindowsUIUtils_h__
+
+#include "nsIWindowsUIUtils.h"
+#include "nsString.h"
+#include "nsColor.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+
+using SharePromise =
+ mozilla::MozPromise<bool, nsresult, /* IsExclusive */ true>;
+
+namespace mozilla {
+enum class ColorScheme : uint8_t;
+}
+
+class WindowsUIUtils final : public nsIWindowsUIUtils {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWINDOWSUIUTILS
+
+ WindowsUIUtils();
+
+ static RefPtr<SharePromise> Share(nsAutoString aTitle, nsAutoString aText,
+ nsAutoString aUrl);
+
+ static void UpdateInTabletMode();
+ static bool GetInTabletMode();
+
+ // 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
+ // disallowed).
+ static mozilla::Maybe<nscolor> GetAccentColor(int aTone = 0);
+ static mozilla::Maybe<nscolor> GetSystemColor(mozilla::ColorScheme, int);
+
+ // Use LookAndFeel for a cached getter.
+ static bool ComputeOverlayScrollbars();
+ static double ComputeTextScaleFactor();
+ static bool ComputeTransparencyEffects();
+
+ protected:
+ ~WindowsUIUtils();
+};
+
+#endif // mozilla_widget_WindowsUIUtils_h__
diff --git a/widget/windows/components.conf b/widget/windows/components.conf
new file mode 100644
index 0000000000..e5089ed9e1
--- /dev/null
+++ b/widget/windows/components.conf
@@ -0,0 +1,220 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Headers = [
+ '/widget/windows/nsWidgetFactory.h',
+]
+
+InitFunc = 'nsWidgetWindowsModuleCtor'
+UnloadFunc = 'nsWidgetWindowsModuleDtor'
+
+Classes = [
+ {
+ 'cid': '{4c9dee4a-b083-4261-8bbe-c6883d2a6bc9}',
+ 'contract_ids': ['@mozilla.org/gfx/parent/screenmanager;1'],
+ 'singleton': True,
+ 'type': 'mozilla::widget::ScreenManager',
+ 'constructor': 'mozilla::widget::ScreenManager::GetAddRefedSingleton',
+ 'headers': ['/widget/ScreenManager.h'],
+ 'processes': ProcessSelector.ALLOW_IN_GPU_AND_MAIN_PROCESS,
+ },
+ {
+ 'cid': '{2d96b3df-c051-11d1-a827-0040959a28c9}',
+ 'contract_ids': ['@mozilla.org/widget/appshell/win;1'],
+ 'headers': ['/widget/windows/nsWidgetFactory.h'],
+ 'legacy_constructor': 'nsAppShellConstructor',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_UTILITY_AND_GMPLUGIN_PROCESS,
+ },
+ {
+ 'cid': '{6987230e-0098-4e78-bc5f-1493ee7519fa}',
+ 'contract_ids': ['@mozilla.org/widget/useridleservice;1'],
+ 'singleton': True,
+ 'type': 'nsUserIdleServiceWin',
+ 'constructor': 'nsUserIdleServiceWin::GetInstance',
+ 'headers': ['/widget/windows/nsUserIdleServiceWin.h', 'nsUserIdleService.h'],
+ },
+ {
+ 'cid': '{b148eed2-236d-11d3-b35c-00a0cc3c1cde}',
+ 'contract_ids': ['@mozilla.org/sound;1'],
+ 'singleton': True,
+ 'type': 'nsISound',
+ 'constructor': 'nsSound::GetInstance',
+ 'headers': ['/widget/windows/nsSound.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}',
+ 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'],
+ 'type': 'nsClipboardHelper',
+ 'headers': ['/widget/nsClipboardHelper.h'],
+ },
+ {
+ 'cid': '{b8e5bc54-a22f-4eb2-b061-24cb6d19c15f}',
+ 'contract_ids': ['@mozilla.org/windows-taskbar;1'],
+ 'type': 'mozilla::widget::WinTaskbar',
+ 'headers': ['/widget/windows/WinTaskbar.h'],
+ },
+ {
+ 'cid': '{73a5946f-608d-454f-9d33-0b8f8c7294b6}',
+ 'contract_ids': ['@mozilla.org/windows-legacyjumplistbuilder;1'],
+ 'type': 'mozilla::widget::LegacyJumpListBuilder',
+ 'headers': ['/widget/windows/LegacyJumpListBuilder.h'],
+ },
+ {
+ 'cid': '{2b9a1f2c-27ce-45b6-8d4e-755d0e34f8db}',
+ 'contract_ids': ['@mozilla.org/windows-legacyjumplistitem;1'],
+ 'type': 'mozilla::widget::LegacyJumpListItem',
+ 'headers': ['/widget/windows/LegacyJumpListItem.h'],
+ },
+ {
+ 'cid': '{21f1f13b-f75a-42ad-867a-d91ad694447e}',
+ 'contract_ids': ['@mozilla.org/windows-legacyjumplistseparator;1'],
+ 'type': 'mozilla::widget::LegacyJumpListSeparator',
+ 'headers': ['/widget/windows/LegacyJumpListItem.h'],
+ },
+ {
+ 'cid': '{f72c5dc4-5a12-47be-be28-ab105f33b08f}',
+ 'contract_ids': ['@mozilla.org/windows-legacyjumplistlink;1'],
+ 'type': 'mozilla::widget::LegacyJumpListLink',
+ 'headers': ['/widget/windows/LegacyJumpListItem.h'],
+ },
+ {
+ 'cid': '{b16656b2-5187-498f-abf4-56346126bfdb}',
+ 'contract_ids': ['@mozilla.org/windows-legacyjumplistshortcut;1'],
+ 'type': 'mozilla::widget::LegacyJumpListShortcut',
+ 'headers': ['/widget/windows/LegacyJumpListItem.h'],
+ },
+ {
+ 'cid': '{e04a55e8-fee3-4ea2-a98b-41d2621adc3c}',
+ 'contract_ids': ['@mozilla.org/windows-ui-utils;1'],
+ 'type': 'WindowsUIUtils',
+ 'headers': ['/widget/windows/WindowsUIUtils.h'],
+ },
+ {
+ 'cid': '{8b5314bc-db01-11d2-96ce-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/transferable;1'],
+ 'type': 'nsTransferable',
+ 'headers': ['/widget/nsTransferable.h'],
+ },
+ {
+ 'cid': '{948a0023-e3a7-11d2-96cf-0060b0fb9956}',
+ 'contract_ids': ['@mozilla.org/widget/htmlformatconverter;1'],
+ 'type': 'nsHTMLFormatConverter',
+ 'headers': ['/widget/nsHTMLFormatConverter.h'],
+ },
+ {
+ 'cid': '{f92e733e-33a3-4752-90e5-25801ddeaf7b}',
+ 'contract_ids': ['@mozilla.org/widget/parent/dragservice;1'],
+ 'type': 'nsDragService',
+ 'headers': ['/widget/windows/nsDragService.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{9a0cb62b-d638-4faf-9588-ae96f5e29093}',
+ 'contract_ids': ['@mozilla.org/widget/taskbar-preview-callback;1'],
+ 'type': 'mozilla::widget::TaskbarPreviewCallback',
+ 'headers': ['/widget/windows/TaskbarPreview.h'],
+ },
+ {
+ 'name': 'GfxInfo',
+ 'cid': '{d755a760-9f27-11df-0800-200c9a664242}',
+ 'contract_ids': ['@mozilla.org/gfx/info;1'],
+ 'type': 'mozilla::widget::GfxInfo',
+ 'headers': ['/widget/windows/GfxInfo.h'],
+ 'init_method': 'Init',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_SOCKET_AND_UTILITY_PROCESS,
+ },
+ {
+ 'cid': '{e2fc3e45-c893-4b34-8f6d-b87faf65a897}',
+ 'contract_ids': ['@mozilla.org/parent/filepicker;1'],
+ 'type': 'nsFilePicker',
+ 'headers': ['/widget/windows/nsFilePicker.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{035d92f3-3802-4cf5-87cb-1758bfc5d4da}',
+ 'contract_ids': ['@mozilla.org/parent/colorpicker;1'],
+ 'type': 'nsColorPicker',
+ 'headers': ['/widget/windows/nsColorPicker.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{1201d357-8417-4926-a694-e6408fbedcf8}',
+ 'contract_ids': ['@mozilla.org/sharepicker;1'],
+ 'type': 'nsSharePicker',
+ 'headers': ['/widget/windows/nsSharePicker.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{25b4efa0-7054-4787-9cd6-630efb3fe6fa}',
+ 'contract_ids': ['@mozilla.org/widget/parent/clipboard;1'],
+ 'interfaces': ['nsIClipboard'],
+ 'type': 'nsIClipboard',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{b6e1a890-b2b8-4883-a65f-9476f6185313}',
+ 'contract_ids': ['@mozilla.org/widget/systemstatusbar;1'],
+ 'singleton': True,
+ 'init_method': 'Init',
+ 'type': 'mozilla::widget::SystemStatusBar',
+ 'constructor': 'mozilla::widget::SystemStatusBar::GetAddRefedSingleton',
+ 'headers': ['/widget/windows/SystemStatusBar.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
+
+if buildconfig.substs['CC_TYPE'] == 'clang-cl':
+ Classes += [
+ {
+ 'cid': '{84e11f80-ca55-11dd-ad8b-0800200c9a66}',
+ 'contract_ids': ['@mozilla.org/system-alerts-service;1'],
+ 'type': 'mozilla::widget::ToastNotification',
+ 'headers': ['/widget/windows/ToastNotification.h'],
+ 'init_method': 'Init',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{a46c385b-a45c-4b48-ab7c-aaed1252bb83}',
+ 'contract_ids': ['@mozilla.org/windows-alert-notification;1'],
+ 'type': 'mozilla::widget::WindowsAlertNotification',
+ 'headers': ['/widget/windows/ToastNotification.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ }
+ ]
+
+if defined('NS_PRINTING'):
+ Classes += [
+ {
+ 'cid': '{d3f69889-e13a-4321-980c-a39332e21f34}',
+ 'contract_ids': ['@mozilla.org/gfx/devicecontextspec;1'],
+ 'type': 'nsDeviceContextSpecWin',
+ 'headers': ['/widget/windows/nsDeviceContextSpecWin.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{06beec76-a183-4d9f-85dd-085f26da565a}',
+ 'contract_ids': ['@mozilla.org/widget/printdialog-service;1'],
+ 'type': 'nsPrintDialogServiceWin',
+ 'headers': ['/widget/windows/nsPrintDialogWin.h'],
+ 'init_method': 'Init',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{841387c8-72e6-484b-9296-bf6eea80d58a}',
+ 'contract_ids': ['@mozilla.org/gfx/printsettings-service;1'],
+ 'type': 'nsPrintSettingsServiceWin',
+ 'headers': ['/widget/windows/nsPrintSettingsServiceWin.h'],
+ 'init_method': 'Init',
+ },
+ {
+ 'cid': '{a6cf9129-15b3-11d2-932e-00805f8add32}',
+ 'contract_ids': ['@mozilla.org/gfx/printerlist;1'],
+ 'type': 'nsPrinterListWin',
+ 'headers': ['/widget/windows/nsDeviceContextSpecWin.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ ]
diff --git a/widget/windows/docs/blocklist.rst b/widget/windows/docs/blocklist.rst
new file mode 100644
index 0000000000..5450faaa1b
--- /dev/null
+++ b/widget/windows/docs/blocklist.rst
@@ -0,0 +1,347 @@
+========================
+Windows DLL Blocklisting
+========================
+
+--------
+Overview
+--------
+
+There are many applications which interact with another application, which means
+they run their code as a DLL in a different process. This technique is used, for
+example, when an antivirus software tries to monitor/block navigation to a
+malicious website, or a screen reader tries to access UI parts. If such an
+application injects their code into Firefox, and if there is a bug in their code
+running in our firefox.exe, it will emerge as Firefox’s bug even though it’s
+not.
+
+Firefox for Windows has a feature to prevent DLLs from being loaded into our
+processes. If we are aware that a particular DLL causes a problem in our
+processes such as a crash or performance degradation, we can stop the problem by
+blocking the DLL from being loaded.
+
+This blocklist is about a third-party application which runs outside Firefox but
+interacts with Firefox. For add-ons, there is `a different process
+<https://extensionworkshop.com/documentation/publish/add-ons-blocking-process/>`_.
+
+This page explains how to request to block a DLL which you think we should block
+it as well as technical details about the feature.
+
+-----------------------
+Two types of blocklists
+-----------------------
+
+There are two types of blocklists in Firefox:
+
+1. A static blocklist that is compiled in to Firefox. This consists of DLLs
+ known to cause problems with Firefox, and this blocklist cannot be disabled
+ by the user. For more information and instructions on how to add a new DLL
+ to this list, see :ref:`Process for blocking a DLL in the static blocklist
+ <how-to-block-dll-in-static-blocklist>` below.
+2. A dynamic blocklist that users can use to block DLLs that are giving them
+ problems. This was added in
+ `bug 1744362 <https://bugzilla.mozilla.org/show_bug.cgi?id=1744362>`_.
+
+The static blocklist has ways to specify if only certain versions of a DLL
+should be blocked, or only for certain Firefox processes, etc. The dynamic
+blocklist does not have this capability; if a DLL is on the list it will always
+be blocked.
+
+Regardless of which blocklist the DLL is on, if it meets the criteria for being
+blocked Firefox uses the same mechanism to block it. There are more details
+below in :ref:`How the blocklist blocks a DLL <how-the-blocklist-blocks-a-dll>`.
+
+.. _how-to-block-dll-in-static-blocklist:
+
+--------------------------------------------------
+Process for blocking a DLL in the static blocklist
+--------------------------------------------------
+
+But wait, should we really block it?
+------------------------------------
+
+Blocking a DLL with the static blocklist should be our last resort to fix a
+problem because doing it normally breaks functionality of an application which
+installed the DLL. If there is another option, we should always go for it.
+Sometimes we can safely bypass a third-party’s problem by changing our code even
+though its root cause is not on our side.
+
+When we decide to block it, we must be certain that the issue at hand is so
+great that it outweighs the user's choice to install the software, the utility
+it provides, and the vendor's freedom to distribute and control their software.
+
+How to request to block a DLL
+-----------------------------
+
+Our codebase has the file named
+`WindowsDllBlocklistDefs.in <https://searchfox.org/mozilla-central/source/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in>`_ from which our build process generates DLL blocklists as C++ header files and compiles them. To block a new DLL, you create a patch to update WindowsDllBlocklistDefs.in and land it on our codebase, following our standard development process. Moreover, you need to fill out a form specific to the DLL blockling request so that reviewers can review the impact and risk as well as the patch itself.
+
+Here are the steps:
+
+1. File `a bug
+ <https://bugzilla.mozilla.org/enter_bug.cgi?format=__default__&bug_type=defect&product=Toolkit&component=Blocklist%20Policy%20Requests&op_sys=Windows&short_desc=DLL%20block%20request%3A%20%3CDLL%20name%3E&comment=Please%20go%20through%20https%3A%2F%2Fwiki.mozilla.org%2FBlocklisting%2FDLL%20before%20filing%20a%20new%20bug.>`_
+ if it does not exist.
+2. Answer all the questions in `this questionnaire
+ <https://msmania.github.io/assets/mozilla/third-party-modules/questionnaire.txt>`_,
+ and attach it to the bug as a plaintext.
+3. Make a patch and start a code review via Phabricator as usual.
+
+How to edit WindowsDllBlocklistDefs.in
+--------------------------------------
+
+WindowsDllBlocklistDefs.in defines several variables as a Python Array. When you
+add a new entry in the blocklists, you pick one of the variables and add an
+entry in the following syntax:
+
+Syntax
+******
+
+::
+
+ Variable += [
+ ...
+ # One-liner comment including a bug number
+ EntryType(Name, Version, Flags),
+ ...
+ ]
+
+Parameters
+**********
+
++-----------+--------------------------------------------------------------------------------+
+| Parameter | Value |
++===========+================================================================================+
+| Variable | ALL_PROCESSES \| BROWSER_PROCESS \| CHILD_PROCESSES \| GMPLUGIN_PROCESSES \| |
+| | GPU_PROCESSES \| SOCKET_PROCESSES \| UTILITY_PROCESSES |
++-----------+--------------------------------------------------------------------------------+
+| EntryType | DllBlocklistEntry \| A11yBlocklistEntry \| RedirectToNoOpEntryPoint |
++-----------+--------------------------------------------------------------------------------+
+| Name | A case-insensitive string representing a DLL's filename to block |
++-----------+--------------------------------------------------------------------------------+
+| Version | One of the following formats: |
+| | |
+| | - ALL_VERSIONS \| UNVERSIONED |
+| | - A tuple consisting of four digits |
+| | - A 32-bit integer representing a Unix timestamp with PETimeStamp |
++-----------+--------------------------------------------------------------------------------+
+
+Variable
+********
+
+Choose one of the following predefined variables.
+
+- **ALL_PROCESSES**: DLLs defined here are blocked in BROWSER_PROCESS +
+ CHILD_PROCESSES
+- **BROWSER_PROCESS**: DLLs defined here are blocked in the browser process
+- **CHILD_PROCESSES**: DLLs defined here are blocked in non-browser processes
+- **GMPLUGIN_PROCESSES**: DLLs defined here are blocked in GMPlugin processes
+- **GPU_PROCESSES**: DLLs defined here are blocked in GPU processes
+- **SOCKET_PROCESSES**: DLLs defined here are blocked in socket processes
+- **UTILITY_PROCESSES**: DLLs defined here are blocked in utility processes
+
+EntryType
+*********
+Choose one of the following predefined EntryTypes.
+
+- **DllBlocklistEntry**: Use this EntryType unless your case matches the other
+ EntryTypes.
+- **A11yBlocklistEntry**: If you want to block a module only when it’s loaded by
+ an accessibility application such as a screen reader, you can use this
+ EntryType.
+- **RedirectToNoOpEntryPoint**: If a modules is injected via Import Directory
+ Table, adding the module as DllBlocklistEntry breaks process launch, meaning
+ DllBlocklistEntry is not an option. You can use RedirectToNoOpEntryPoint
+ instead.
+
+Name
+****
+A case-insensitive string representing a DLL's filename to block. Don’t include a directory name.
+
+Version
+*******
+
+A maximum version to be blocked. If you specify a value, a module with the
+specified version, older versions, and a module with no version are blocked.
+
+| If you want to block a module regardless of its version, use ALL_VERSIONS.
+| If you want to block a module with no version, use UNVERSIONED.
+
+
+To specify a version, you can use either of the following formats:
+
+- | A tuple consisting of four digits. This is compared to the version that is embedded in a DLL as a version resource.
+ | Example: (1, 2, 3, 4)
+- | A 32-bit integer representing a Unix timestamp with PETimeStamp. This is compared to an integer of IMAGE_FILE_HEADER::TimeDateStamp.
+ | Example: PETimeStamp(0x12345678)
+
+
+-----------------
+Technical details
+-----------------
+
+.. _how-the-blocklist-blocks-a-dll:
+
+How the blocklist blocks a DLL
+------------------------------
+
+Briefly speaking, we make ntdll!NtMapViewOfSection return
+``STATUS_ACCESS_DENIED`` if a given module is on the blocklist, thereby a
+third-party’s code, or even Firefox’s legitimate code, which tries to load a DLL
+in our processes in any way such as LoadLibrary API fails and receives an
+access-denied error.
+
+Cases where we should not block a module
+----------------------------------------
+
+As our blocklist works as explained above, there are the cases where we should not block a module.
+
+- | A module is loaded via `Import Directory Table <https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table>`_
+ | Blocking this type of module blocks even a process from launching. You may be able to block this type of module with RedirectToNoOpEntryPoint.
+- | A module is loaded as a `Layered Service Provider <https://docs.microsoft.com/en-us/windows/win32/winsock/categorizing-layered-service-providers-and-applications>`_
+ | Blocking this type of module on Windows 8 or newer breaks networking. Blocking a LSP on Windows 7 is ok.
+
+(we used to have to avoid blocking modules loaded via a
+`Window hook <https://docs.microsoft.com/en-us/windows/win32/winmsg/hooks>`_ because blocking this type of
+module would cause repetitive attempts to load a module, resulting in slow performance
+like `Bug 1633718 <https://bugzilla.mozilla.org/show_bug.cgi?id=1633718>`_, but this should be fixed
+as of `Bug 1823412 <https://bugzilla.mozilla.org/show_bug.cgi?id=1823412>`_.)
+
+Third-party-module ping
+-----------------------
+
+We’re collecting the :ref:`third-party-module ping <third-party-modules-ping>`
+which captures a moment when a third-party module is loaded into the
+Browser/Tab/RDD process. As it’s asked in the request form, it’s important to
+check the third-party-module ping and see whether a module we want to block
+appears in the ping or not. If it appears, you may be able to know how a module
+is loaded by looking at a callstack in the ping.
+
+How to view callstacks in the ping
+**********************************
+
+1. You can run a query on BigQuery console or STMO. (BigQuery console is much
+ faster and can handle larger data.)
+
+ - BigQuery console (visit
+ `here <https://docs.telemetry.mozilla.org/cookbooks/bigquery.html#gcp-bigquery-console>`_
+ to request access): https://console.cloud.google.com/bigquery
+ - STMO: https://sql.telemetry.mozilla.org/
+
+2. Make your own query based on `this template
+ <https://msmania.github.io/assets/mozilla/third-party-modules/query-template.txt>`_.
+3. Run the query.
+4. Save the result as a JSON file.
+
+ - In BigQuery console, click [SAVE RESULTS] and choose [JSON (local file)].
+ - In STMO, click [...] at the right-top corner and select [Show API Key],
+ then you can download a JSON from a URL shown in the [Results in JSON format].
+
+5. | Go to https://msmania.github.io/assets/mozilla/third-party-modules/
+ | (A temporal link. Need to find a permanent place.)
+6. Click [Upload JSON] and select the file you saved at the step 4.
+7. Click a row in the table to view a callstack
+
+
+How to see the versions of a specific module in the ping
+********************************************************
+
+You can use `this template query
+<https://msmania.github.io/assets/mozilla/third-party-modules/query-groupby-template.txt>`_
+to query which versions of a specific module are captured in the ping. This
+tells the product versions which are actively used including the crashing
+versions and the working versions.
+
+You can also get the crashing versions by querying the crash reports or the
+Socorro table. Having two version lists, you can decide whether you can specify
+the Version parameter in a blocklist entry.
+
+Initialization
+--------------
+
+In order to have the most effective blocking of DLLs, the blocklist is
+initialized very early during browser startup. If the :ref:`launcher process
+<launcher-process>` is available, the steps are:
+
+- Launcher process loads dynamic blocklist from disk (see
+ `DynamicBlocklist::LoadFile()
+ <https://searchfox.org/mozilla-central/search?q=DynamicBlocklist%3A%3ALoadFile&path=&case=false&regexp=false>`_)
+- Launcher process puts dynamic blocklist data in shared section (see
+ `SharedSection::AddBlocklist()
+ <https://searchfox.org/mozilla-central/search?q=SharedSection%3A%3AAddBlocklist&path=&case=false&regexp=false>`_)
+- Launcher process creates the browser process in a suspended mode, sets up its
+ dynamic blocklist, then starts it. (see `LauncherMain()
+ <https://searchfox.org/mozilla-central/search?q=LauncherMain&path=&case=false&regexp=false>`_)
+
+ - This is so (ideally) no DLLs can be injected before the blocklist is set up.
+
+If the launcher process is not available, a different blocklist is used, defined
+in `mozglue/WindowsDllBlocklist.cpp
+<https://searchfox.org/mozilla-central/source/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp>`_.
+This code does not currently support the dynamic blocklist. This is intended to
+only be used in testing and other non-deployed scenarios, so this shouldn't be
+a problem for users.
+
+Note that the mozglue blocklist also has a feature to block threads that start
+in ``LoadLibrary`` and variants. This code is currently only turned on in
+Nightly builds because it breaks some third-party DLP products.
+
+Dynamic blocklist file location
+-------------------------------
+
+Because the blocklist is loaded so early during startup, we don't have access to
+what profile is going to be loaded, so the blocklist file can't be stored there.
+Instead, by default the blocklist file is stored in the Windows user's roaming
+app data directory, specifically
+
+``<Roaming AppData directory>\Mozilla\Firefox\blocklist-<install hash>``
+
+Note that the install hash here is what is returned by `GetInstallHash()
+<https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/common/commonupdatedir.cpp#404>`_,
+and is suitable for uniquely identifying the particular Firefox installation
+that is running.
+
+On first launch, this location will be written to the registry, and can be
+overriden by setting that key to a different file location. The registry key is
+``HKEY_CURRENT_USER\Software\Mozilla\Firefox\Launcher``, and the name is the
+full path to firefox.exe with "\|Blocklist" appended. This code is in
+`LauncherRegistryInfo
+<https://searchfox.org/mozilla-central/source/toolkit/xre/LauncherRegistryInfo.cpp>`_.
+
+Adding to and removing from the dynamic blocklist
+-------------------------------------------------
+
+Users can add or remove DLLs from the dynamic blocklist by navigating to
+``about:third-party``, finding the entry for the DLL they are interested in, and
+clicking on the dash icon. They will then be prompted to restart the browser, as
+the change will only take effect after the browser restarts.
+
+Disabling the dynamic blocklist
+-------------------------------
+
+It is possible that users can get Firefox into a bad state by putting a DLL on
+the dynamic blocklist. One possibility is that the user blocks only one of a set
+of DLLs that interact, which could make Firefox behave in unpredictable ways or
+crash.
+
+By launching Firefox with ``--disableDynamicBlocklist``\, the dynamic blocklist
+will be loaded but not used to block DLLs. This lets the user go to
+``about:third-party`` and attempt to fix the problem by unblocking or blocking
+DLLs.
+
+Similarly, in safe mode the dynamic blocklist is also disabled.
+
+Enterprise policy
+-----------------
+
+The dynamic blocklist can be disabled by setting a registry key at
+``HKEY_CURRENT_USER\Software\Policies\Mozilla\Firefox`` with a name of
+DisableThirdPartyModuleBlocking and a DWORD value of 1. This will have the
+effect of not loading the dynamic blocklist, and no icons will show up in
+``about:third-party`` to allow blocking DLLs.
+
+-------
+Contact
+-------
+
+Any questions or feedback are welcome!
+
+**Matrix**: `#hardening <https://app.element.io/#/room/#hardening:mozilla.org>`_
diff --git a/widget/windows/docs/index.rst b/widget/windows/docs/index.rst
new file mode 100644
index 0000000000..9a24cb9cdb
--- /dev/null
+++ b/widget/windows/docs/index.rst
@@ -0,0 +1,9 @@
+==================
+Firefox on Windows
+==================
+
+.. toctree::
+ :maxdepth: 2
+
+ blocklist
+ windows-pointing-device/index
diff --git a/widget/windows/docs/windows-pointing-device/apple_vision.jpg b/widget/windows/docs/windows-pointing-device/apple_vision.jpg
new file mode 100644
index 0000000000..9515f4d4fb
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/apple_vision.jpg
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/apple_vision_user.webp b/widget/windows/docs/windows-pointing-device/apple_vision_user.webp
new file mode 100644
index 0000000000..64f8afc0e5
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/apple_vision_user.webp
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/index.rst b/widget/windows/docs/windows-pointing-device/index.rst
new file mode 100644
index 0000000000..eda552b3dd
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/index.rst
@@ -0,0 +1,1384 @@
+################################################################################
+Windows Pointing Device Support in Firefox
+################################################################################
+
+.. contents:: Table of Contents
+ :depth: 4
+
+================================================================================
+Introduction
+================================================================================
+
+This document is intended to provide the reader with a quick primer and/or
+refresher on pointing devices and the various operating system APIs, user
+experience guidelines, and Web standards that contribute to the way Firefox
+handles input devices on Microsoft Windows.
+
+The documentation for these things is scattered across the web and has varying
+levels of detail and completeness; some of it is missing or ambiguous and was
+only determined experimentally or by reading about other people's experiences
+through forum posts. An explicit goal of this document is to gather this
+information into a cohesive picture.
+
+We will then discuss the ways in which Firefox currently (as of early 2023)
+produces incorrect or suboptimal behavior when implementing those standards
+and guidelines.
+
+Finally, we will raise some thoughts and questions to spark discussion on how
+we might improve the situation and handle corner cases. Some of
+these issues are intrinsically "opinion based" or "policy based", so clear
+direction on these is desirable before engineering effort is invested into
+reimplementation.
+
+
+================================================================================
+Motivation
+================================================================================
+
+A quick look at the `pile of defects <https://bugzilla.mozilla.orgbuglist.cgi?query_format=advanced&status_whiteboard=%5Bwin%3Atouch%5D&list_id=16586149&status_whiteboard_type=allwordssubstr>`__
+on *bugzilla.mozilla.org* marked with *[win:touch]* will show anyone that
+Firefox's input stack for pointer devices has issues, but the bugs recorded
+there don't begin to capture the full range of unreported glitches and
+difficult-to-reproduce hiccups that users run into while using touchscreen
+hardware and pen digitizers on Firefox, nor does it capture the ways that
+Firefox misbehaves according to various W3C standards that are (luckily) either
+rarely used or worked around in web apps (and thus go undetected or
+unreported).
+
+These bugs primarily manifest in a few ways that will each be discussed in
+their own section:
+
+1. Firefox failing to return the proper values for the ``pointer``,
+ ``any-pointer``, ``hover``, and ``any-hover`` CSS Media Queries
+
+2. Firefox failing to fire the correct pointer-related DOM events at the
+ correct time (or at all)
+
+3. Firefox's inconsistent handling of touch-related gestures like scrolling,
+ where certain machines (like the Surface Pro) fail to meet the expected
+ behavior of scrolling inertia and overscroll. This leads to a weird touch
+ experience where the page comes to a choppy, dead-stop when using
+ single-finger scrolling
+
+
+It's worth noting that Firefox is not alone in having these types of issues,
+and that handling input devices is a notoriously difficult task for many
+applications; even a substantial amount of Microsoft's own software has trouble
+navigating this minefield on their own Microsoft Surface devices. Defects are
+instigated by a combination of the *intrinsic complexity* of the problem domain
+and the *accidential complexity* introduced by device vendors and Windows
+itself.
+
+The *intrinsic complexity* comes from the simple fact that human-machine
+interaction is difficult. A person must attempt to convey complex
+and abstract goals through a series of simple movements involving a few pieces
+of physical hardware. The devices can send signals that are unclear
+or even contradictory, and the software must decide how to handle
+this.
+
+As a trivial example, every software engineer that's ever written
+page scrolling logic has to answer the question, "What should my
+program do if the user hits 'Page Up' and 'Page Down' at the same time?".
+While it may seem obvious that the answer is "Do nothing.", naively-written
+keyboard input logic might assume the two are mutually-exclusive and only
+process whichever key is handled first in program order.
+
+Occasionally, a new device will be invented that doesn't obviously map to
+existing abstractions and input pipelines. There will be a period of time where
+applications will want to support the new device, but it won't be well
+understood by either the application developers nor the device vendor
+themselves what ideal integration would look like. The new Apple Vision VR
+headset is such a device; traditional VR headsets have used controllers to
+point at things, but Apple insists that the entire thing should be done using
+only hand tracking and eye tracking. Developers of VR video games and other
+apps (like Firefox) will inevitably make many mistakes on the road to
+supporting this new headset.
+
+A major source of defect-causing *accidental complexity* is the lack of clear
+expectations and documentation from Microsoft for apps (like Firefox) that are
+not using their Universal Windows Platform (UWP). The Microsoft Developer
+Network (MSDN) mentions concepts like inertia, overscroll, elastic bounce,
+single-finger panning, etc., but the solution is presented in the context
+of UWP, and the solution for non-UWP apps is either unclear or undocumented.
+
+Adding to this complexity is the fact that Windows itself has gone through
+several iterations of input APIs for different classes of devices, and
+these APIs interact with each other in ways that are surprising or
+unintuitive. Again, the advice given on MSDN pertains to UWP apps, and the
+documentation about the newer "pointer" based window messages is
+a mix of incomplete and inaccurate.
+
+Finally, individual input devices have bugs in their driver software that
+would disrupt even applications that are using the Windows input APIs perfectly.
+Handling all of these deviations is impossible and would result in fragile,
+unmaintainable code, but Firefox inevitably has to work around common ones to
+avoid alienating large portions of the userbase.
+
+
+================================================================================
+Technical Background
+================================================================================
+
+
+A Quick Primer on Pointing Devices
+======================================
+
+
+Traditionally, web browsers were designed to accommodate computer mice and
+devices that behave in a similar way, like trackballs and touchpads on
+laptops. Generally, it was assumed that there would be one such device attached
+to the computer, and it would be used to control a hovering "cursor" whose
+movements would be changed by relative movement of the physical input device.
+
+However, modern computers can be controlled using a variety of different
+pointing devices, all with different characteristics. Many allow
+multiple concurrent targets to be pointed at and have multiple sensors,
+buttons, and other actuators.
+
+For example, the screen of the Microsoft Surface Pro has dual capabilities
+of being a touch sensor and a digitizer for a tablet pen. When being used as a
+workstation, it's not uncommon for a user to also connect the "keyboard +
+touchpad" cover and a mouse (via USB or Bluetooth) to provide the more
+productivity-oriented "keyboard and mouse" setup. In that configuration, there
+are 4 pointer devices connected to the machine simultaneously: a touch screen,
+a pen digitizer, a touchpad, and a mouse.
+
+The next section will give a quick overview of common pointing devices.
+Many will be familiar to the reader, but they are still mentioned to establish
+common terminology and to avoid making assumptions about familiarity with every
+input device.
+
+
+Common Pointing Devices
+---------------------------
+
+Here are some descriptions of a few pointing device types that demonstrate
+the diversity of hardware:
+
+**Touchscreen**
+
+ A touchscreen is a computer display that is able to sense the
+ location of (possibly-multiple) fingers (or stylus) making contact with its
+ surface. Software can then respond to the touches by changing the displayed
+ objects quickly, giving the user a sense of actually physically manipulating
+ them on screen with their hands.
+
+ .. image:: touchscreen.jpg
+ :width: 25%
+
+
+**Digitizing Tablet + Pen Stylus**
+
+ These advanced pointing devices tend to
+ exist in two forms: as an external sensing "pad" that can be plugged into a
+ computer and sits on a desk or in someone's lap, or as a sensor built right
+ into a computer display. Both use a "stylus", which is a pen-shaped
+ electronic device that is detectable by the surface. Common features
+ include the ability to distinguish proximity to the surface ("hovering")
+ versus actual contact, pressure sensitivity, angle/tilt detection, multiple
+ "ends" such as a tip and an eraser, and one-or-more buttons/switch
+ actuators.
+
+ .. image:: wacom_tablet.png
+ :width: 25%
+
+
+**Joystick/Pointer Stick**
+
+ Pointer sticks are most often seen in laptop
+ computers made by IBM/Lenovo, where they exist as a little red nub located
+ between the G, H, and B keys on a standard QWERTY keyboard. They function
+ similarly to the analog sticks on a game controller -- The user displaces
+ the stick from its center position, and that is interpreted as a relative
+ direction to move the on-screen cursor. A greater displacement from center
+ is interpreted as increased velocity of movement.
+
+ .. image:: trackpoint.jpg
+ :width: 25%
+
+
+**Touchpad**
+
+ A touchpad is a rectangular surface (often found on laptop
+ computers) that detects touch and motion of a finger and moves an on-screen
+ cursor relative to the motion. Modern touchpads often support multiple
+ touches simultaneously, and therefore offer functionality that is quite
+ similar to a touchscreen, albeit with different movement semantics because
+ of their physical separation from the screen (discussed below).
+
+ .. image:: touchpad.jpg
+ :width: 25%
+
+
+**VR Controllers**
+
+ VR controllers (and other similar devices like the
+ Wiimote from the Nintendo Wii) allow users to point at objects in a
+ three-dimensional virtual world by moving a real-world controller and
+ "projecting" the controller's position into the virtual space. They often
+ also include sensors to detect the yaw, pitch, and roll of the sensors.
+ There are often other inputs in the controller device, like analog sticks
+ and buttons.
+
+ .. image:: vrcontroller.jpg
+ :width: 25%
+
+
+**Hand Tracking**
+
+ Devices like the Apple Vision (introduced during the
+ time this document was being written) and (to a lesser extent) the Meta
+ Quest have the ability to track the wearer's hand and directly interpret
+ gestures and movements as input. As the human hand can assume a staggering
+ number of orientations and configurations, a finite list of specific shapes
+ and movements must be identified and labelled to allow for clear
+ software-user interaction.
+
+ .. image:: apple_vision_user.webp
+ :width: 25%
+
+ .. image:: apple_vision.jpg
+ :width: 25%
+
+
+**Mouse**
+
+ A pointing device that needs no introduction. Moving a physical
+ clam-shaped device across a surface translates to relative movement of a
+ cursor on screen.
+
+ .. image:: mouse.jpg
+ :width: 25%
+
+
+The Buxton Three-State Model
+-------------------------------
+
+
+Bill Buxton, an early pioneer in the field of human-computer interaction,
+came up with a three-state model for pointing devices; a device can be
+"Out of Range", "Tracking", or "Dragging". Not all devices support all three
+states, and some devices have multiple actuators that can have the three-state
+model individually applied.
+
+.. mermaid::
+
+ stateDiagram-v2
+ direction LR
+ state "State 0" as s0
+ state "State 1" as s1
+ state "State 2" as s2
+ s0 --> s0 : Out Of Range
+ s1 --> s1 : Tracking
+ s2 --> s2 : Dragging
+ s0 --> s1 : Stylus On
+ s1 --> s0 : Stylus Lift
+ s1 --> s2 : Tip Switch Close
+ s2 --> s1 : Tip Switch Open
+
+
+For demonstration, here is the model applied to a few devices:
+
+**Computer Mouse**
+
+ A mouse is never in the "Out of Range" state. Even though it can technically
+ be lifted off its surface, the mouse does not report this as a separate
+ condition; instead, it behaves as-if it is stationary until it can once
+ again sense the surface moving underneath.
+
+ The remaining two states apply to each button individually; when a button is
+ not being pressed, the mouse is considered in the "tracking" state with
+ respect to that button. When a button is held down, the mouse is "dragging"
+ with respect to that button. A "click" is simply considered a zero-length
+ drag under this model.
+
+ In the case of a two-button mouse, this means that the mouse can be in a
+ total of 4 different states: tracking, left button dragging, right button
+ dragging, and two-button dragging. In practice, very little software
+ actually does anything meaningful with two-button dragging.
+
+**Touch Screen**
+
+ Applying the model to a touch screen, one can observe that current hardware
+ has no way to sense that a finger that is "hovering, but not quite making
+ contact with the screen". This means that the "Tracking" state can be ruled
+ out, leaving only the "Out of Range" and "Dragging" states. Since many touch
+ screens can support multiple fingers touching the screen concurrently, and
+ each finger can be in one of two states, there are potentially 2^N different
+ "states" that a touchscreen can be in. Windows assigns meaning to many two,
+ three, and four-finger gestures.
+
+**Tablet Digitizer**
+
+ A tablet digitizer supports all three states: when the stylus is far away
+ from the surface, it is considered "out of range"; when it is located
+ slightly above the surface, it is "tracking"; and when it is making contact
+ with the surface, it is "dragging".
+
+The W3C standards for pointing devices are based on this three-state model, but
+applied to each individual web element instead of the entire system. This
+makes things like "Out-of-Range" possible for the mouse, since it can be
+out of range of a web element.
+
+The W3C uses the terms "over" and "out" to convey the transition between
+"out-of-range" and "tracking" (which the W3C calls "hover"), and the terms
+"down" and "up" convey the transition between "tracking" and "dragging".
+
+The standard also address some of the known shortcomings of the model to
+improve portability and consistency; these improvements will be discussed more
+below.
+
+The Windows Pointer API is *supposedly* based around this model,
+but unfortunately real-world testing shows that the model is not followed
+very consistently with respect to the actual signals sent to the application.
+
+
+Gestures
+=====================================
+
+
+In contrast to the sort-of "anything goes" UI designs of the past,
+modern operating systems like Windows, Mac OS X, iOS, Android, and even
+modern Linux DEs have an "opinionated" idea of how user interaction
+should behave across all apps on the platform (the so-called "look and feel"
+of the operating system).
+
+Users expect gestures like swipes, pinches, and taps to act the same way
+across all apps for a given operating system, and they expect things like
+on-screen keyboards or handwriting recognition to pop up in certain contexts.
+Failing to meet those expectations makes an app look less polished, and
+(especially as far as accessibility is concerned) it frustrates the user
+and makes it more difficult for them to interact with the app.
+
+Microsoft defines guidelines for various behaviours that Windows applications
+should ideally adhere to in the `Input and Interactions <https://learn.microsoft.com/en-us/windows/apps/design/input/>`__
+section on MSDN. Some of these are summarized quickly below:
+
+**Drag and Drop**
+
+ Drag and drop allows a user to transfer data from one application to
+ another. The gesture begins when a pointer device moves into the "Dragging"
+ state over top of a UI element, usually as a result of holding down a mouse
+ button or pressing a finger on a touchscreen. The user moves the pointer
+ over top of the receiver of the data, and then ends the gesture by releasing
+ the mouse button or lifting their finger off the touchscreen. Window
+ interprets this transition out of the "Dragging" state as permission to
+ initiate the data transfer.
+
+ Firefox has supported Drag and Drop for a very long time, so it will not be
+ discussed further.
+
+
+**Pan and Zoom**
+
+ When using touchscreens (and multi-touch touchpads), users expect to be able
+ to cause the viewport to "pan" left/right/up/down by pressing two fingers on
+ the screen (creating two pointers in "Dragging" state) and moving their
+ fingers in the direction of movement. When they are done, they can release
+ both fingers (changing both pointers to "Out of Bounds").
+
+ A zoom can be signalled by moving the two fingers apart or together
+ in a "pinch" or "reverse pinch" gesture.
+
+
+**Single Pointer Panning**
+
+ Applications that are based on a UI model of the user interacting with a
+ "page" often allow a single pointer "Dragging" over the viewport to cause
+ the viewport to pan, similarly to the two-finger panning discussed in the
+ previous section.
+
+ Note that this gesture is not as universal as two-finger panning is -- as a
+ counterexample, graphics programs tend to treat one-finger dragging as
+ object manipulation and two-finger dragging as viewport panning.
+
+
+**Inertia**
+
+ When a user is done panning, they may lift their finger/pen off the screen
+ while the viewport is still in motion. Users expect that the page will
+ continue to move for a little while, as-if the user had "tossed" the page
+ when they let go. Effectively, the page behaves as though it has "momentum"
+ that needs to be gradually lost before the page comes to a full stop.
+
+ Modern operating systems provide this behavior via their various native
+ widget toolkits, and the curve that objects follow as they slow to a stop
+ are different across OSes. In that way, they can be considered part of the
+ unique "look and feel" of the OS. Users expect the scrolling of pages in
+ their web browser to behave this way, and so when Firefox fails to provide
+ this behavior it can be jarring.
+
+
+**Overscroll and Elastic Bounce**
+
+ When a user is panning the page and reaches the outer edges, Microsoft
+ recommends that the app should begin an "elastic bounce" animation, where
+ the page will allow the user to scroll past the end ("overscroll"),
+ show empty space underneath the page, and then sort of "snap back" like a
+ rubber band that's been stretched and then released. You can see a
+ demonstration in `this article <https://www.windowslatest.com/2020/05/21/microsoft-is-adding-elastic-scrolling-to-chrome-on-windows-10/>`__,
+ which discusses Microsoft adding it to Chromium.
+
+
+History of Web Standards and Windows APIs
+===========================================
+
+The World-Wide Web Consortium (W3C) and the Web Hypertext Application
+Technology Working Group (WHATWG) manage the standards that detail the
+interface between a user agent (like Firefox) and applications designed to run
+on the Web Platform. The user agent, in turn, must rely on the operating system
+(Windows, in this case) to provide the necessary APIs to implement the
+standards required by the Web Platform.
+
+As a result of that relationship, a Web Standard is unlikely to be created
+until all widely-used operating systems provide the required APIs. That allows
+us to build a linear timeline with a predictable pattern: a new type of device
+becomes popular, the APIs to support it are introduced into operating systems,
+and eventually a cross-platform standard is introduced into the Web Platform.
+
+The following sections detail the history of input devices supported by
+Windows and the Web Platform:
+
+
+**1985 - Computer Mouse Support (Windows 1.0)**
+
+ The first version of Windows (1985) supported a computer mouse. Support
+ for other input devices is not well-documented, but probably non-existant.
+
+
+**1991 - Third-Party De-facto Pen Support (Wintab)**
+
+ In the late 80s and early 90s, any tablet pen hardware vendor that wanted
+ to support Windows would need to write a device driver and design a
+ proprietary user-mode API to expose the device to user applications. In
+ turn, application developers would have to write and maintain code to
+ support the APIs of every relevant device vendor.
+
+ In 1991, a company named LCS/Telegraphics released an API for Windows
+ called "Wintab", which was designed in collaboration with hardware and
+ software vendors to define a general API that could be targetted by
+ device drivers and applications.
+
+ It would take Microsoft more than a decade to include first-party support
+ for tablet pens in Windows, which allowed Wintab to become the de-facto
+ standard for pen support on Windows. The Wintab API continues to be
+ supported by virtually all artist tablets to this day. Notable companies
+ include Wacom, Huion, XP-Pen, etc.
+
+
+**1992 - Early Windows Pen Support (Windows for Pen Computing)**
+
+ The earliest Windows operating system to support non-mouse pointing devices
+ was Windows 3.1 with the "Windows for Pen Computing" add-on (1992).
+ (`For the curious <https://socket3.wordpress.com/2019/07/31/windows-for-pen-computing-1-0/>`__,
+ and I'm certain `this book <https://www.amazon.com/Microsoft-Windows-Pen-Computing-Programmers/dp/1556154690>`__
+ is a must-read!). Pen support was mostly implemented by translating actions
+ into the existing ``WM_MOUSExxx`` messages, but also "upgraded" any
+ application's ``EDIT`` controls into ``HEDIT`` controls, which looked the
+ same but were capable of being handwritten into using a pen. This was not
+ very user-friendly, as the controls stayed the same size and the UI was not
+ adapted to the input method. This add-on never achieved much popularity.
+
+ It is not documented whether Netscape Navigator (the ancestor of Mozilla
+ Firefox) supported this add-on or not, but there is no trace of it in modern
+ Firefox code.
+
+
+**1995 - Introduction of JavaScript and Mouse Events (De-facto Web Standard)**
+
+ The introduction of JavaScript in 1995 by Netscape Communications added a
+ programmable, event-driven scripting environment to the Web Platform.
+ Browser vendors quickly added the ability for scripts to listen for and
+ react to mouse events. These are the well-known events like ``mouseover``,
+ ``mouseenter``, ``mousedown``, etc. that are ubiquitous on the web, and are
+ known by basically anyone who has ever written front-end JavaScript.
+
+ This ubiquity created a de-facto standard for mouse input, which would
+ eventually be formally standardized by the W3C in the HTML Living Standard
+ in 2001.
+
+ The Mouse Event APIs assume that the computer has one single pointing device
+ which is always present, has a single cursor capable of "hovering" over an
+ element, and has between one and three buttons.
+
+ When support for other pointing devices like touchscreen and pen first
+ became available in operating systems, it was exposed to the web by
+ interpreting user actions into equivalent mouse events. Unfortunately, this
+ is unable to handle multiple concurrent pointers (like one would get from
+ multitouch screens) or report the kind of rich information a pen digitizer
+ can provide, like tilt angle, pressure, etc. This eventually lead the W3C
+ to develop the new "Touch Events" standard to expose touch functionality,
+ and eventually the "Pointer Events" to expose more of the rich information
+ provided by pens.
+
+
+**2005 - Mainstream Pen Support (Windows XP Tablet PC Edition)**
+
+ It was the release of Windows XP Tablet PC Edition (2005) that allowed
+ Windows applications to directly support tablet pens by using the new COM
+ "`Windows Tablet PC <https://learn.microsoft.com/en-us/windows/win32/tablet/tablet-pc-development-guide>`__"
+ APIs, most of which are provided through the main `InkCollector <https://learn.microsoft.com/en-us/windows/win32/tablet/inkcollector-class>`__
+ class. The ``InkCollector`` functionality would eventually be "mainlined"
+ into Windows XP Professional Service Pack 2, and continues to exist in
+ modern Windows releases.
+
+ The Tablet PC APIs consist of a large group of COM objects that work
+ together to facilitate enumerating attached pens, detecting pen movement and
+ pen strokes, and analyzing them to provide:
+
+ 1. **Cursor Movement**: translates the movements of the pen into the
+ standard mouse events that applications expect from mouse cursor
+ movement, namely ``WM_NCHITTEST``, ``WM_SETCURSOR`` and
+ ``WM_MOUSEMOVE``.
+
+ 2. **Gesture Recognition**: detects common user actions, like "tap",
+ "double-tap", "press-and-hold", and "drag". The `InkCollector` delivers
+ these events via COM `SystemGesture <https://learn.microsoft.com/en-us/windows/win32/tablet/inkcollector-systemgesture>`__
+ events using the `InkSystemGesture <https://learn.microsoft.com/en-us/windows/win32/api/msinkaut/ne-msinkaut-inksystemgesture>`__
+ enumeration. It will also translate them into common Win32 messages; for
+ example, a "drag" gesture would be translated into a ``WM_LBUTTONDOWN``
+ message, several ``WM_MOUSEMOVE`` messages, and finally a
+ ``WM_LBUTTONUP`` message.
+
+ An application that is using ``InkCollector`` will receive both types of
+ messages: traditional mouse input through the Win32 message queue, and
+ "Tablet PC API" events through COM callbacks. It is up to the
+ application to determine which events matter to it in a given context,
+ as the two types of events are not guaranteed by Microsoft to correspond
+ in any predictable way.
+
+ 3. **Shape and Text Recognition**: allows the app to
+ recognize letters, numbers, punctuation, and other `common shapes <https://learn.microsoft.com/en-us/windows/win32/api/msinkaut/ne-msinkaut-inkapplicationgesture>`__
+ the user might make using their pen. Supported shapes include circles,
+ squares, arrows, and motions like "scratch out" to correct a misspelled
+ word. Custom recognizers exist that allow recognition of other symbols,
+ like music notes or mathematical notation.
+
+ 4. **Flick Recognition**: allows the user to invoke actions via quick,
+ linear motions that are recognized by Windows and sent to the app as
+ ``WM_TABLET_FLICK`` messages. The app can choose to handle the window
+ message or pass it on to the default window procedure, which will
+ translate it to scrolling messages or mouse messages.
+
+ For example, a quick upward 'flick' corresponds to "Page up", and
+ a quick sideways flick in a web browser would be "back". Flicks were
+ never widely used by Windows apps, and they may have been removed in
+ more recent versions of Windows, as the existing Control Panel menus
+ for configuring them seem to no longer exist as of Windows 10 22H2.
+
+
+ Firefox does not appear to have ever used these APIs to allow tablet pen
+ input, with the exception of `one piece of code <https://searchfox.org/mozilla-central/rev/e6cb503ac22402421186e7488d4250cc1c5fecab/widget/windows/InkCollector.cpp>`__
+ to detect when the pen leaves the Firefox window to solve
+ `Bug 1016232 <https://bugzilla.mozilla.org/show_bug.cgi?id=1016232>`__.
+
+
+**2009 - Touch Support: WM_GESTURE (Windows 7)**
+
+ While attempts were made with the release of Windows Vista (2007) to support
+ touchscreens through the existing tablet APIs, it was ultimately the release
+ of Windows 7 (2009) that brought first-class support for Touchscreen devices
+ to Windows with new Win32 APIs and two main window messages: ``WM_TOUCH``
+ and ``WM_GESTURE``.
+
+ These two messages are mutually-exclusive, and all applications are
+ initially set to receive only ``WM_GESTURE`` messages. Under this
+ configuration, Windows will attempt to recognize specific movements on a
+ touch digitizer and post "gesture" messages to the application's message
+ queue. These gestures are similar to (but, somewhat-confusingly, not
+ identical to) the gestures provided by the "Windows Tablet PC" APIs
+ mentioned above. The main gesture messages are: zoom, pan, rotate,
+ two-finger-tap, and press-and-tap (one finger presses, another finger
+ quickly taps the screen).
+
+ In contrast to the behavior of the ``InkCollector`` APIs, which will send
+ both gesture events and translated mouse messages, the ``WM_GESTURE``
+ message is truly "upstream" of the translated mouse messages; the translated
+ mouse messages will only be generated if the application forwards the
+ ``WM_GESTURE`` message to the default window procedure. This makes
+ programming against this API simpler than the ``InkCollector`` API, as
+ there is no need to state-fully "remember" that an action has already been
+ serviced by one codepath and needs to be ignored by the other.
+
+ Firefox current supports the ``WM_GESTURE`` message when Asynchronous Pan
+ and Zoom (APZ) is not enabled (although we do not handle inertia in this
+ case, so the page comes to a dead-stop immediately when the user stops
+ scrolling).
+
+
+**2009 - Touch Support: WM_TOUCH (Windows 7)**
+
+ Also introduced in Windows 7, an application that needs full control over
+ touchscreen events can use `RegisterTouchWindow <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registertouchwindow>`__
+ to change any of its windows to receive ``WM_TOUCH`` messages instead of the
+ more high-level ``WM_GESTURE`` messages. These messages explicitly notify
+ the application about every finger that contacts or breaks contact with the
+ digitizer (as well as each finger's movement over time). This provides
+ absolute control over touch interpretation, but also means that the burden
+ of handling touch behavior falls completely on the application.
+
+ To help ease this burden, Microsoft provides two COM APIs to interpret
+ touch messages, ``IManipulationProcessor`` and ``IInertiaProcessor``.
+
+ ``IManipulationProcessor`` can be considered a superset of the functionality
+ available through normal gestures. The application feeds ``WM_TOUCH`` data
+ into it (along with other state, such as pivot points and timestamps), and
+ it allows for manipulations like: two-finger rotation around a pivot,
+ single-finger rotation around a pivot, simultaneous rotation and translation
+ (for example, 'dragging' a single corner of a square).
+ `These MSDN diagrams <https://learn.microsoft.com/en-us/windows/win32/wintouch/advanced-manipulations-overview>`__
+ give a good overview of the kinds of advanced manipulations an app might
+ support.
+
+ ``IInertiaProcessor`` works with ``IManipulationProcessor`` to add inertia
+ to objects in a standard way across the operating system. It is likely that
+ later APIs that provide this (like DirectManipulation) are using these COM
+ objects under the hood to accomplish their inertia handling.
+
+ Firefox currently handles the ``WM_TOUCH`` event when Asynchronous Pan and
+ Zoom (APZ) is enabled, but we do not use either the ``IInertiaProcessor``
+ nor the ``IManipulationProcessor``.
+
+
+**2012 - Unified Pointer API (Windows 8)**
+
+ Windows 8 (2012) was Microsoft's initial attempt to make a touch-first,
+ mobile-first operating system that (ideally) would make it easy for app
+ developers to treat touch, pen, and mouse as first-class input devices.
+
+ By this point, the Windows Tablet APIs would allow tablet pens to draw
+ text and shapes like squares, triangles, and music notes, and those shapes
+ would be recognizable by the Windows Ink subsystem.
+
+ At the same time, Windows Touch allowed touchscreens to have advanced
+ manipulation, like rotate + translate, or simultaneous pan and zoom, and it
+ allowed objects manipulated by touch to have momentum and angular velocity.
+
+ The shortcomings of having separate input stacks for these various devices
+ starts to be become apparent after a while: Why shouldn't a touchscreen be
+ able to recognize a circle or a triangle? Why shouldn't a pen be able to
+ have complex rotation and zoom functionality? How do we handle these newer
+ laptop touchpads that are starting to handle multi-touch gestures like a
+ touchscreen, but still cause relative cursor movement like a mouse? Why does
+ my program have to have 3 separate codepaths for different pointing devices
+ that are all very similar?
+
+ The Windows Pointer Device Input Stack introduces new APIs and window
+ messages that generalize the various types of pointing devices under a
+ single API while still falling back to the legacy touch and tablet input
+ stacks in the event that the API is unused. (Note that the touch and tablet
+ stacks themselves fall back to the traditional mouse input stack when they
+ are unused.)
+
+ Microsoft based their pointer APIs off the Buxton Three-State Model
+ (discussed earlier), where changes between "Out-of-Range" and "Tracking" are
+ signalled by ``WM_POINTERENTER`` AND ``WM_POINTERLEAVE`` messages, and
+ changes between "Tracking" and "Dragging" are signalled by
+ ``WM_POINTERDOWN`` and ``WM_POINTERUP``. Movement is indicated via
+ ``WM_POINTERUPDATE`` messages.
+
+ If these messages are unhandled (the message is forwarded to
+ ``DefWindowProc``), the Win32 subsystem will translate them
+ into touch or gesture messages. If unhandled, those will be further
+ translated into mouse and system messages.
+
+ While the Pointer API is not without some unfortunate pitfalls (which will
+ be discussed later), it still provides several advantages over the
+ previously available APIs: it can allow a mostly-unified codepath for
+ handling pointing devices, it circumvents many of the often-complex
+ interactions between the previous APIs, and it provides the ability to
+ simulate pointing devices to help facilitate end-to-end automated testing.
+
+ Firefox currently uses the Pointer APIs to handle tablet stylus input only,
+ while other input methods still use the historical mouse and touch input
+ APIs above.
+
+
+**2013 - DirectManipulation (Windows 8.1)**
+
+ DirectManipulation is a DirectX based API that was added during the release
+ of Windows 8.1 (2013). This API allows an app to create a series of
+ "viewports" inside a window and have scrollable content within each of these
+ viewports. The manipulation engine will then take care of automatically
+ reading Pointer API messages from the window's event queue and generating
+ pan and zoom events to be consumed by the app.
+
+ In the case that the app is also using DirectComposition to draw its window,
+ DirectManipulation can pipe the events directly into it, causing the app
+ to essentially get asynchronous pan and zoom with proper handling of inertia
+ and overscroll with very little coding.
+
+ DirectManipulation is only used in Firefox to handle data coming from
+ Precision Touchpads, as Microsoft provides no other convenient API for
+ obtaining data from such devices. Firefox creates fake content inside of
+ a fake viewport to capture the incoming events from the touchpad and
+ translates them into the standard Asynchronous Pan and Zoom (APZ) events
+ that the rest of the input pipeline uses.
+
+
+**2013 - Touch Events (Web Standard)**
+
+ "`Touch Events <https://www.w3.org/TR/touch-events/>`__" became a W3C
+ recommendation in October, 2013.
+
+ At this point, Microsoft's first operating system to include touch support
+ (Windows 7) was the most popular desktop operating system, and the ubiquity
+ of smart phones brought a huge uptick in users with touchscreen inputs. All
+ major browsers included some API that allowed reading touch input,
+ prompting the W3C to formalize a new standard to ensure interoperability.
+
+ With the Touch Events API, multiple touch interactions may be reported
+ simultaneously, each with their own separate identifier for tracking and
+ their own coordinates within the screen, viewport, and client area. A
+ touch is reported by: a ``touchstart`` event with a unique ID for each
+ contact, zero-or-more ``touchmove`` events with that ID, and finally a
+ ``touchend`` event to signal the end of that specific contact.
+
+ The API also has some amount of support for pen styluses, but it lacks
+ important features necessary to truly support them: hovering, pressure,
+ tilt, or multiple cursors like an erasure. Ultimately, its functionality
+ has been superceded by the newer "Pointer Events" API, discussed below.
+
+
+**2016 - Precision Touchpads (Windows 10)**
+
+ Early touchpads emulated a computer mouse by directly using the same IBM
+ PS/2 interface that most computer mice used and translating relative
+ movement of the user's finger into equivalent movements of a mouse on a
+ surface.
+
+ As touchpad technology advanced and more powerful interface standards like
+ USB begun to take over the consumer market, touchpad vendors started adding
+ extra features to their hardware, like tap-to-click, tap-and-drag, and
+ tap-and-hold (to simulate a right click). These behaviors were implemented
+ by touchpad vendors either in hardware drivers and/or user mode "hooks" that
+ injected equivalent Win32 messages into the appropriate target.
+
+ As expected, each touchpad vendor's driver had its own subtly-different
+ behavior from others, its own bugs, and its own negative interactions with
+ other software.
+
+ During the later years of Windows 8, Microsoft and touchpad company
+ Synaptics co-developed the "Precision Touchpad" standard, which defines an
+ interface for touchpad hardware to report its physical measurements,
+ precision, and sensor configuration to Windows and allows it to deliver raw
+ touch data. Windows then interprets the data and generates gestures and
+ window messages in a standard way, removing the burden of implementing these
+ behaviors from the touchpad vendor and providing the OS with rich
+ information about the user's movements.
+
+ It wasn't until the 2016 release of Windows 10 14946 that Microsoft would
+ support all the standard gestures through the new standard. Although
+ adoption by vendors has been a bit slow, the fact that
+ `it is a requirement for Windows 11 <https://pocketnow.com/all-windows-11-pcs-will-be-required-to-have-a-precision-touchpad-and-webcam/>`__
+ means that vendor support for this standard is imminent.
+
+ Unfortunately, there's a piece of bad news: Microsoft did not
+ implement the above "Unified Pointer API" for use with touchpads, as the
+ developers of Blender discovered when `they moved to the Pointer API <https://archive.blender.org/developer/D7660>`__.
+ Instead, Microsoft expects developers to either use DirectManipulation to
+ automatically get pan/zoom enabled for their app, or the RawInput API to
+ directly read touchpad data.
+
+
+**2019 - Pointer Events (Web Standard)**
+
+ "`Pointer Events <https://www.w3.org/TR/pointerevents/>`__" became a level 2
+ W3C recommendation in April, 2019. They considered `the work done by Microsoft <https://www.w3.org/Submission/2012/SUBM-pointer-events-20120907/>`__
+ as part of the design of their own Pointer API, and in many ways the W3C
+ standard resembles an improved, better specified, more consistent, and
+ easier-to-use version of the APIs provided by the Win32 subsystem.
+
+ The Pointer Events API generalizes devices like touchscreens, mice, tablet
+ pens, VR controllers, etc. into a "thing that points". A pointer has
+ (optional) properties: a width and height (big for a finger, 1px for a
+ mouse), an amount of pressure, a tilt angle relative to the surface, some
+ buttons, etc. This helps applications maximize code reuse for handling
+ pointer input by having a common codebase written against these generalized
+ traits. If needed, the application may also have smaller, specialized
+ sections of code for each concrete pointer type.
+
+ Certain types of pointers (like pens and touchscreens) have a behavior where
+ they are always "captured" by the first object that they interact with. For
+ example, if a user puts their finger on an empty part of a web page and
+ starts to scroll, their finger is now "captured" by the web page itself.
+ "Captured" means that even if their finger moves over an element in
+ the web page, that element will not receive events from the finger -- the
+ page itself will until the entire interaction stops.
+
+ The events themselves very closely follow the Buxton Three-State Model
+ (discussed earlier), where ``pointerover/pointerout`` messages indicate
+ transitions from "Out of Range" to "Tracking" and visa-versa, and
+ ``pointerdown/pointerup`` messages transition between "Tracking" and
+ "Dragging". ``pointermove`` updates the position of the pointer, and a
+ special ``pointercancel`` message is sent to inform the page that the
+ browser is "cancelling" a ``pointerdown`` event because it has decided to
+ consume it for a gesture or because the operating system cancelled the
+ pointer for its own reasons.
+
+
+CSS "interaction" Media Queries
+==========================================
+
+(Note that this section is **not** about the `pointer-events <https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events>`__
+CSS property, which defines the circumstances where an element can be the target
+of pointer events.)
+
+The W3C defines the interaction-related media queries in the
+`Media Queries Level 4 - Interaction Media Features <https://www.w3.org/TR/mediaqueries-4/#mf-interaction>`__
+document.
+
+To summarize, the main interaction-related CSS Media Queries that Firefox must
+support are ``pointer``, ``any-pointer``, ``hover`` and ``any-hover``.
+
+
+``pointer``
+
+ Allows the webpage to query the existence of a pointing device on
+ the machine, and (if available) the assumed "pointing accuracy" of the
+ "primary" pointing device. The device considered "primary" on a machine with
+ multiple input devices is a policy decision that must be made by the web
+ browser; Windows simply provides the APIs to query information about
+ attached devices.
+
+ The browser is expected to return one of three strings to this media query:
+
+ ``none``
+
+ There is no pointing device attached to the computer.
+
+ ``coarse``
+
+ The primary pointing device is capable of approximately
+ pointing at a relatively large target (like a finger on a
+ touchscreen).
+
+ ``fine``
+
+ The primary pointing device is capable of near-pixel-level
+ accuracy (like a computer mouse or a tablet pen).
+
+
+``any-pointer``
+
+ Similar to ``pointer``, but represents the union of
+ capabilities of all pointers attached to the system, such that the meanings
+ become:
+
+ ``none``
+
+ There is no pointing device attached to the computer.
+
+ ``coarse``
+
+ There is at-least one "coarse" pointer attached.
+
+ ``fine``
+
+ There is at-least one "fine" pointer attached.
+
+
+``hover``
+
+ Allows the webpage to query whether the primary pointer is
+ capable of "hovering" over top of elements on the page. Computer mice,
+ touchpad cursors, and higher-end pen tablets all support this, whereas
+ current touchscreens are "touch" or "no touch", and they cannot detect a
+ finger hovering over the screen.
+
+ ``hover``
+
+ The primary pointer is capable of reporting hovering.
+
+ ``none``
+
+ The primary pointer is not capable of reporting hovering.
+
+``any-hover``
+
+ Indicates whether any pointer attached to the system has the
+ ``hover`` capability.
+
+
+Selection of the Primary Pointing Device
+--------------------------------------------
+
+To illustrate the complexity of this topic, consider the Microsoft Surface Pro.
+
+The Surface Pro has an advanced screen that is capable of receiving touch
+input, but it can also behave like a pen digitizer and receive input from a
+stylus with advanced pen capabilities, like hover sensing, pressure
+sensitivity, multiple buttons, and even multiple "tips" (a pen and eraser end).
+
+In this case, what should Firefox consider the primary pointing device?
+
+Perhaps the user intends to use their Surface Pro like a touchscreen tablet,
+at which point Firefox should report ``pointer: coarse`` and ``hover: none``
+capabilities.
+
+But what if, instead, the user wants to sketch art or take notes using a pen on
+their Surface Pro? In this case, Firefox should be reporting ``pointer: fine``
+and ``hover: hover``.
+
+Imagine that the user then attaches the "keyboard + touchpad" cover attachment
+to their Surface Pro; naturally, we will consider that the user's intent is for
+the touchpad to become the primary pointing device, and so it is fairly clear
+that we should return ``pointer: fine`` and ``hover: hover`` in this state.
+
+However, what if the user tucks the keyboard/touchpad attachment behind the
+tablet and begins exclusively operating the device with their finger?
+
+This example shows that complex, multi-input machines can resist classification
+and blur the lines between labels like "touch device", "laptop", "drawing
+tablet", etc. It also illustrates that identifying the "primary" pointing
+device using only machine configuration may yield unintuitive and suboptimal
+results.
+
+While we can almost-certainly improve our hardware detection heuristics to
+better answer this question (and we should, at the very least), perhaps it
+makes more sense for Firefox to incorporate user intentions into the decision.
+Intentions could be communicated directly by the user through some sort of
+setting or indirectly through the user's actions.
+
+For example, if the user intends to draw on the screen with a pen, perhaps
+Firefox provides something like a "drawing mode" that the user can toggle to
+change the primary pointing device to the pen. Or perhaps it's better for
+Firefox to interpret the mere fact of receiving pen input as evidence of the
+user's intent and switch the reported primary pointing device automatically.
+
+If we wanted to switch automatically, there are predictable traps and pitfalls
+we need to think about: we need to ensure that we don't create frustrating user
+experiences where web pages may "pop" beneath the user suddenly, and
+we should likely incorporate some kind of "settling time" so we don't
+oscillate between devices.
+
+It's worth noting that Chromium doesn't seem to incorporate anything like
+what's being suggested here, so if this is well-designed it may be an
+opportunity for Firefox to try something novel.
+
+
+
+
+================================================================================
+State of the Browser
+================================================================================
+
+Pan and Zoom, Inertia, Overscroll, and Elastic Bounce
+=========================================================
+
+As can be seen in the videos below, Firefox's support for inertia, overscroll,
+and elastic bounce works well on all platforms when a stylus pen is used
+as the input device, and it also works just fine with the touchscreen on the
+Dell XPS 15. However, it completely fails when the touchscreen is used on
+the Microsoft Surface Pro. While more investigation is needed to completely
+understand these issues, the fact that the correctly-behaving digitizing pens
+use the Pointer API and the misbehaving input devices do not may be related.
+
+- `Video 1 <https://drive.google.com/file/d/1Z1QRSf2RluNhJwkKCzPb6-14vRtkqK8s/view?usp=sharing>`__
+ showcasing overscroll and bounce not working on Surface Pro with touch, but
+ other devices/inputs are working
+
+- `Video 2 <https://drive.google.com/file/d/1bOgpVGBeZtwelvPJzYdA6uFRpubGtu4W/view?usp=sharing>`__
+ showing that everything works just fine with an external Wacom digitizer
+
+
+Pointer Media Queries
+=========================================================
+
+**"any-pointer" Queries**
+
+Unlike the ``pointer`` media queries, which rely on the browser to make a policy
+decision about what should be considered the "primary" pointer in a given
+system configuration, the ``any-pointer`` queries are much more objective and
+binary: the computer either has a type of device attached to it, or it
+doesn't.
+
+**any-pointer: coarse**
+
+Firefox reports that there are "coarse" pointing devices present if either of
+these two points is true:
+
+1. ``GetSystemMetrics(SM_DIGITIZER)`` reports that a device that supports
+ touch or pen is present.
+
+2. Based on heuristics, Firefox concludes that it is running on a computer it
+ considers a "tablet".
+
+Point #1 is incorrect, as a pen is not a "coarse" pointing device. Note that
+this is a recent regression in `Bug 1811303 <https://bugzilla.mozilla.org/show_bug.cgi?id=1811303>`__
+that was uplifted to Firefox 112, so this actually regressed as this document
+was being written! This is responsible for the incorrect "Windows 10 Desktop +
+Wacom USB Tablet" issue in the table.
+
+Point #2 is a clear case of the `XY Problem <https://en.wikipedia.org/wiki/XY_problem>`__,
+where Firefox is trying to determine if a coarse pointing device is present
+by determining whether it is running on a tablet, when instead it should be
+directly testing for coarse pointing devices (since, of course, those can exist
+on machines that wouldn't normally be considered a "tablet"). This is
+responsible for the incorrect "Windows 10 Dell XPS 15 (Touch Disabled) + Wacom
+USB Tablet" issue in the table below.
+
+**any-pointer: fine**
+
+Firefox reports that there are "fine" pointing devices present if and only if
+it detects a mouse. This is clearly already wrong. Firefox determines that the
+computer has a mouse using the following algorithm:
+
+1. If ``GetSystemMetrics(SM_MOUSEPRESENT)`` returns false, report no mouse.
+
+2. If Firefox does not consider the current computer to be a tablet, report a
+ mouse if there is at-least one "mouse" device driver running on the
+ computer.
+
+3. If Firefox considers the current computer to be a tablet or a touch system,
+ only report a mouse if there are at-least two "mouse" device drivers
+ running. This exists because some tablet pens and touch digitizers report
+ themselves as computer mice.
+
+This algorithm also suffers from the XY problem -- Firefox is trying to
+determine whether a fine pointing device exists by determining if there is
+a computer mouse present, when instead it should be directly testing for
+fine pointing devices, since mice are not the only fine pointing
+devices.
+
+Because of this proxy question, this algorithm is completely dependent on any
+attached fine pointing device (like a pen tablet) to report itself as a mouse.
+Point #3 makes the problem even worse, because if a computer that resembles a
+tablet fails to report its digitizers as mice, the algorithm will completely
+ignore an actual computer mouse attached to the system because it expects two
+of them to be reported!
+
+Unfortunately, the Surface Pro has both a pen digitizer and a touch digitizer,
+and it reports neither as a mouse. As a result, this algorithm completely falls
+apart on the Surface Pro, failing to report any "fine" pointing device even
+when a computer mouse is plugged in, a pen is plugged in, or even when
+the tablet is docked because its touchpad is only one mouse and it expects
+at least two.
+
+This is also responsible for failing to report the trackpad on the Dell XPS 15
+as "fine", because the Dell XPS 15 has a touchscreen and therefore looks like
+a "tablet", but doesn't report 2 mouse drivers.
+
+**any-pointer: hover**
+
+
+Firefox reports that any device that is a "fine" pointer also supports "hover",
+which does generally hold true, but isn't necessarily true for lower-end pens
+that only support tapping. It would be better for Firefox to directly
+query the operating system instead of just assuming.
+
+**"pointer" media query**
+
+As discussed previously at length, this media query relies on a "primary"
+designation made by the browser. Below is the current algorithm used to
+determine this:
+
+1. If the computer is considered a "tablet" (see below), report primary
+ pointer as "coarse" (this is clearly already the wrong behavior).
+
+2. Otherwise, if the computer has a mouse plugged in, report "fine".
+
+3. Otherwise, if the computer has a touchscreen or pen digitizer, report
+ "coarse" (this is wrong in the case of the digitizer).
+
+4. Otherwise, report "fine" (this is wrong; should report "None").
+
+Firefox uses the following algorithm to determine if the computer is a
+"tablet" for point #1 above:
+
+1. It is not a tablet if it's not at-least running Windows 8.
+
+2. If Windows "Tablet Mode" is enabled, it is a tablet no matter what.
+
+3. If no touch-capable digitizers are attached, it is not a tablet.
+
+4. If the system doesn't support auto-rotation, perhaps because it has
+ no rotation sensor, or perhaps because it's docked and operating in
+ "laptop mode" where rotation won't happen, it's not a tablet.
+
+5. If the vendor that made the computer reports to Windows that it supports
+ "convertible slate mode" and it is currently operating in "slate mode",
+ it's a tablet.
+
+6. Otherwise, it's not a tablet.
+
+
+**Table with comparison to Chromium**
+
+The following table shows how Firefox and Chromium respond to various pointer
+queries. The "any-pointer" and "any-hover" columns are not subjective and
+therefore are always either green or red to indicate "pass" or "fail", but the
+"pointer" and "hover" may also be yellow to indicate that it's "open to
+interpretation" because of the aforementioned difficulty in determining the
+"primary pointer".
+
+.. image:: touch_media_queries.png
+ :width: 100%
+
+
+**Related Bugs**
+
+- Bug 1813979 - For Surface Pro media query "any-pointer: fine" is true only
+ when both the Type Cover and mouse are connected
+
+- Bug 1747942 - Incorrect CSS media query matches for pointer, any-pointer,
+ hover and any-hover on Surface Laptop
+
+- Bug 1528441 - @media (hover) and (any-hover) does not work on Firefox 64/65
+ where certain dual inputs are present
+
+- Bug 1697294 - Content processes unable to detect Windows 10 Tablet Mode
+
+- Bug 1806259 - CSS media queries wrongly detect a Win10 desktop computer
+ with a mouse and a touchscreen, as a device with no mouse (hover: none)
+ and a touchscreen (pointer: coarse)
+
+
+Web Events
+=====================
+
+The pen stylus worked well on all tested systems -- The correct pointer events
+were fired in the correct order, and mouse events were properly simulated in
+case the default behavior was allowed.
+
+The touchscreen input was less reliable. On the Dell XPS 15, the
+"Pointer Events" were flawless, but the "Touch Events" were missing
+an important step: the ``touchstart`` and ``touchmove`` messages were sent just
+fine, but Firefox never sends the ``touchend`` message! (Hopefully that isn't
+too difficult to fix!)
+
+Unfortunately, everything really falls apart on the Surface Pro using the
+touchscreen -- neither the "Pointer Events" nor the "Touch Events" fire at all!
+Instead, the touch is completely absorbed by pan and zoom gestures, and nothing
+is sent to the web page. The website's request for ``touch-action: none`` is
+ignored, and the web page is never given any opportunity to call
+``Event.preventDefault()`` to cancel the pan/zoom behavior.
+
+
+Operating System Interfaces
+================================
+
+As was discussed above, Windows has multiple input APIs that were each
+introduced in newer version of Windows to handle devices that were not
+well-served by existing APIs.
+
+Backward compatibility with applications designed against older APIs is
+realized when applications call the default event handler (``DefWindowProc``)
+upon receiving an event type that they don't recognize (which is what apps have
+always been instructed to do if they receive events they don't recognize).
+The unrecognized newer events will be translated by the default event handler
+into older events and sent back to the application. A very old application may
+have this process repeat through several generations of APIs until it finally
+sees events that it recognizes.
+
+Firefox currently uses a mix of the older and newer APIs, which complicates
+the input handling logic and may be responsible for some of the
+difficult-to-explain bugs that we see reported by users.
+
+Here is an explanation of the codepaths Firefox uses to handle pointer input:
+
+1. Firefox handles the ``WM_POINTER[LEAVE|DOWN|UP|UPDATE]`` messages if the
+ input device is a tablet pen and an Asynchronous Pan and Zoom (APZ)
+ compositor is available. Note that this already may not be ideal, as
+ Microsoft warns (`here <https://learn.microsoft.com/en-us/windows/win32/inputmsg/wm-pointercapturechanged>`__)
+ that handling some pointer messages and passing other pointer messages to
+ ``DefWindowProc`` has unspecified behavior (meaning that Win32 may do
+ something unexpected or nonsensical).
+
+ If the above criteria aren't met, Firefox will call ``DefWindowProc``, which
+ will re-post the pointer messages as either touch messages or mouse
+ messages.
+
+2. If DirectManipulation is being used for APZ, it will output the
+ ``WM_POINTERCAPTURECHANGED`` if it detects a pan or zoom gesture it can
+ handle. It will then handle the rest of the gesture itself.
+
+ DirectManipulation is used for all top-level and popup windows as long as
+ it isn't disabled via the ``apz.allow_zooming``,
+ ``apz.windows.use_direct_manipulation``, or
+ ``apz.windows.force_disable_direct_manipulation`` prefs.
+
+3. If the pointing device is touch, the next action depends on
+ whether an Asynchronous Pan and Zoom (APZ) compositor is available. If it
+ is, the window will have been registered using ``RegisterTouchWindow``, and
+ Firefox will receive ``WM_TOUCH`` messages, which will be sent to the
+ "Touch Event" API and handled directly by the APZ compositor.
+
+ If there is no APZ compositor, it will instead be received as a
+ ``WM_GESTURE`` message or a mouse message, depending on the movement. Note
+ that these will be more basic gestures, like tap-and-hold.
+
+4. If none of the above apply, the message will be converted into standard
+ ``WM_MOUSExxx`` messages via a call to ``DefWindowProc``.
+
+
+================================================================================
+Discussion
+================================================================================
+
+Here is where some of the outstanding thoughts or questions can be listed.
+This can be updated as more questions come about and (hopefully) as answers to
+questions become apparent.
+
+CSS "pointer" Media Queries
+===============================
+
+- The logic for the ``any-pointer`` and ``any-hover`` queries are objectively
+ incorrect and should be rewritten altogether. That is not as
+ big of a job as it sounds, as the code is fairly straightforward and
+ self-contained. (Note: Improvements have already been made in
+ `Bug 1813979 <https://bugzilla.mozilla.org/show_bug.cgi?id=1813979>`__)
+
+- There are a few behaviors for ``pointer`` and ``hover`` that are
+ objectively wrong (such as reporting a ``coarse`` pointer when the
+ Surface Pro is docked with a touchpad). Those should be fixable with a
+ code change similar to the previous bullet.
+
+- Do we want to continue to use only machine configuration to decide what
+ the "primary" pointer is, or do we also want to incorporate user intent
+ into the algorithm? Or, alternatively:
+
+ 1. Do we create a way for the user to override? For example, a "Drawing
+ Mode" button if a tablet digitizer is sensed.
+
+ 2. Do we attempt to change automatically in response to user action?
+
+ - An example was used above of a docked Surface Pro computer, where
+ the user may use the keyboard and touchpad for a while, then perhaps
+ tuck that behind and use the device as a touchscreen, and then
+ perhaps draw on it with a tablet stylus.
+
+ - We would need to be careful to avoid careless "popping" or
+ "oscillating" if we react too quickly to changing input types.
+
+- On a separate-but-related note, the `W3C suggested <https://www.w3.org/TR/mediaqueries-5/#descdef-media-pointer>`__
+ that it might be beneficial to allow users to at-least disable all
+ reporting of ``fine`` pointing devices for users who may have a disability
+ that prevents them from being able to click small objects, even with a fine
+ pointing device.
+
+
+Pan-and-Zoom, Inertia, Overscroll, and Elastic Bounce
+=========================================================
+
+- Inertia, overscroll, and elastic bounce are just plain broken on the
+ Surface Pro. That should definitely be investigated.
+
+- We can see from the video below that Microsoft Edge has quite a bit more
+ overscroll and a more elastic bounce than Firefox does, and it also
+ allows elastic bounce in directions that the page itself doesn't scroll.
+
+ Edge's way seems more similar to the user experience I'd expect from using
+ Firefox on an iPhone or Android device. Perhaps we should consider
+ following suit?
+
+ (`Link to video <https://drive.google.com/file/d/14XVLT6CNn2RaXcHHCRIrQmRwoMYjj6fu/view?usp=sharing>`__)
+
+
+Web Events
+==============
+
+- It's worth investigating why the ``touchend`` message never seems
+ to be sent by Firefox on any tested devices.
+
+- It's very disappointing that neither the Pointer Events API nor the
+ Touch Events API works at all on Firefox on the Surface Pro. That should
+ be investigated very soon!
+
+
+Operating System Interfaces
+================================
+
+- With the upcoming sun-setting of Windows 7 support, Firefox has an
+ opportunity to revisit the implementation of our input handling and try to
+ simplify our codepaths and eliminate some of the workarounds that exist to
+ handle some of these complex interactions, as well as fix entire classes of
+ bugs - both reported and unreported - that currently exist as a result.
+
+- Does it make sense to combine the touchscreen and pen handling together
+ and use the ``WM_POINTERXXX`` messages for both?
+
+ - This would eliminate the need to handle the ``WM_TOUCH`` and
+ ``WM_GESTURE`` messages at all.
+
+ - Note that there is precedent for this, as `GTK <https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/1563>`__
+ has already done so. It appears that `Blender <https://archive.blender.org/developer/D7660>`__
+ has plans to move toward this as well.
+
+ - Tablet pens seemed to do very well in most of the testing,
+ and they are also the part of the code that mainly exercises the
+ ``WM_POINTERXXX`` codepaths. That may imply increased reliability in
+ that codepath?
+
+ - The Pointer APIs also have good device simulation for integration
+ testing.
+
+ - Would we also want to roll mouse handling into it using the
+ `EnableMouseInPointer <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enablemouseinpointer>` __
+ call? That would allow us to also get rid of handling
+ ``WM_MOUSE[MOVE/WHEEL/HWHEEL]`` and ``WM_[LRM]BUTTON[UP|DOWN]``
+ messages. Truly one codepath (with a few minor branches) to rule them
+ all!
+
+ - Nick Rishel sent `this link <http://the-witness.net/news/2012/10/wm_touch-is-totally-bananas/>`__
+ that details the troubles that the developers of The Witness (a video
+ game) ran into when using the ``WM_TOUCH`` API. It argues that the API
+ is poorly-designed, and advises that if Windows 7 support is not
+ needed, the API should be avoided.
+
+- Should we exclusively use DirectManipulation for Pan/Zoom?
+
+ - Multitouch touchpads bypass all of the ``WM_POINTER`` machinery
+ for anything gesture-related and directly send their messages to
+ DirectManipulation. We then "capture" all the DirectManipulation events
+ and pump them into our events pipeline, as explained above.
+
+ - DirectManipulation also handles "overscroll + elastic bounce" in a way
+ that aligns with Windows look-and-feel.
+
+ - Perhaps it makes sense to just use DirectManipulation for all APZ
+ handling and eliminate any attempt at handling this through other
+ codepaths.
+
+High-Frequency Input
+================================
+
+"High-Frequency Input" refers to the ability for an app to be able to still
+perceive input events despite them happening at a rate faster than the app
+itself actually handles them.
+
+Consider a mouse that moves through several points: "A->B->C->D->E". If the
+application processes input when the mouse is at "A" and doesn't poll again
+until the mouse is at point "E", the default behavior of all modern operating
+systems is to "coalesce" these events and simply report "A->E". This is fine
+for the majority of use cases, but certain workloads (such as digital
+handwriting and video games) can benefit from knowing the complete path that
+was taken to get from the start point to the end point.
+
+Generally, solutions to this involve the operating system keeping a history of
+pointer movements that can be retrieved through an API. For example,
+Android provides the `MotionEvent <https://developer.android.com/reference/android/view/MotionEvent.html>`__
+API that batches historal movements.
+
+Unfortunately, the APIs to do this in Windows are terribly broken. As
+`this blog <https://blog.getpaint.net/2019/11/14/paint-net-4-2-6-alpha-build-7258/>`__
+makes clear, `GetMouseMovePointsEx <https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmousemovepointsex>`__
+has so many issues that they had to remove its usage from their program because
+of the burden. That same blog entry also details that the newer Pointer API has
+the `GetPointerInfoHistory <https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getpointerinfohistory>`__
+that is *supposed* to support tracking pointer history, but it only ever tracks
+a single entry!
+
+Perhaps luckily, there is currently no web standard for high-frequency input,
+although it `has been asked about in the past <https://lists.w3.org/Archives/Public/public-pointer-events/2014AprJun/0057.html>`__.
+
+If such a standard was ever created, it would likely be very difficult for
+Firefox on Windows to support it.
+
+
+DirectManipulation and Pens
+=============================
+
+- This is a todo item, but it needs to be investigated whether or not
+ DirectManipulation can directly scoop up pen input, or whether it has
+ to be handled by the application (and forwarded to DM if desired).
diff --git a/widget/windows/docs/windows-pointing-device/mouse.jpg b/widget/windows/docs/windows-pointing-device/mouse.jpg
new file mode 100644
index 0000000000..c4fca9ba31
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/mouse.jpg
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/touch_media_queries.png b/widget/windows/docs/windows-pointing-device/touch_media_queries.png
new file mode 100644
index 0000000000..f0de661d7f
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/touch_media_queries.png
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/touchpad.jpg b/widget/windows/docs/windows-pointing-device/touchpad.jpg
new file mode 100644
index 0000000000..1327ec5b1c
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/touchpad.jpg
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/touchscreen.jpg b/widget/windows/docs/windows-pointing-device/touchscreen.jpg
new file mode 100644
index 0000000000..90246ca02e
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/touchscreen.jpg
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/trackpoint.jpg b/widget/windows/docs/windows-pointing-device/trackpoint.jpg
new file mode 100644
index 0000000000..9eae5b5c21
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/trackpoint.jpg
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/vrcontroller.jpg b/widget/windows/docs/windows-pointing-device/vrcontroller.jpg
new file mode 100644
index 0000000000..20f2e90bfe
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/vrcontroller.jpg
Binary files differ
diff --git a/widget/windows/docs/windows-pointing-device/wacom_tablet.png b/widget/windows/docs/windows-pointing-device/wacom_tablet.png
new file mode 100644
index 0000000000..2bc30a3b4f
--- /dev/null
+++ b/widget/windows/docs/windows-pointing-device/wacom_tablet.png
Binary files differ
diff --git a/widget/windows/filedialog/PWinFileDialog.ipdl b/widget/windows/filedialog/PWinFileDialog.ipdl
new file mode 100644
index 0000000000..812db7e103
--- /dev/null
+++ b/widget/windows/filedialog/PWinFileDialog.ipdl
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=ipdl : */
+/* 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 WinFileDialogCommandsDefn;
+using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h";
+using mozilla::widget::filedialog::FileDialogType from "mozilla/widget/filedialog/WinFileDialogCommands.h";
+
+namespace mozilla {
+namespace widget {
+namespace filedialog {
+
+[ChildProc=Utility]
+protocol PWinFileDialog {
+
+child:
+ // Exactly one Show function should be called per instance. Further calls will
+ // result in IPC failure.
+ //
+ // Each will return `Nothing` iff the operation was canceled by the user.
+
+ async ShowFileDialog(WindowsHandle parentHwnd, FileDialogType type, Command[] commands)
+ returns (Results? results);
+ async ShowFolderDialog(WindowsHandle parentHwnd, Command[] commands)
+ returns (nsString? path);
+};
+
+} // namespace filedialog
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/filedialog/WinFileDialogChild.cpp b/widget/windows/filedialog/WinFileDialogChild.cpp
new file mode 100644
index 0000000000..1a2903f8ec
--- /dev/null
+++ b/widget/windows/filedialog/WinFileDialogChild.cpp
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/widget/filedialog/WinFileDialogChild.h"
+
+#include <combaseapi.h>
+#include <objbase.h>
+#include <shobjidl.h>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/widget/filedialog/WinFileDialogCommands.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla::widget::filedialog {
+
+/* extern */ mozilla::LazyLogModule sLogFileDialog("FileDialog");
+
+WinFileDialogChild::WinFileDialogChild() {
+ MOZ_LOG(sLogFileDialog, LogLevel::Info, ("%s %p", __PRETTY_FUNCTION__, this));
+};
+
+WinFileDialogChild::~WinFileDialogChild() {
+ MOZ_LOG(sLogFileDialog, LogLevel::Info, ("%s %p", __PRETTY_FUNCTION__, this));
+};
+
+#define MOZ_ABORT_IF_ALREADY_USED() \
+ do { \
+ MOZ_RELEASE_ASSERT( \
+ !mUsed, "called Show* twice on a single WinFileDialog instance"); \
+ MOZ_LOG( \
+ sLogFileDialog, LogLevel::Info, \
+ ("%s %p: first call to a Show* function", __PRETTY_FUNCTION__, this)); \
+ mUsed = true; \
+ } while (0)
+
+template <size_t N>
+WinFileDialogChild::IPCResult WinFileDialogChild::MakeIpcFailure(
+ HRESULT hr, const char (&what)[N]) {
+ // The crash-report annotator stringifies integer values anyway. We do so
+ // eagerly here to avoid questions about C int/long conversion semantics.
+ nsPrintfCString data("%lu", hr);
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::WindowsFileDialogErrorCode, data);
+
+ return IPC_FAIL(this, what);
+}
+
+#define MOZ_IPC_ENSURE_HRESULT_OK(hr, what) \
+ do { \
+ MOZ_LOG(sLogFileDialog, LogLevel::Verbose, \
+ ("checking HRESULT for %s", what)); \
+ HRESULT const _hr_ = (hr); \
+ if (FAILED(_hr_)) { \
+ MOZ_LOG(sLogFileDialog, LogLevel::Error, \
+ ("HRESULT %8lX while %s", (hr), (what))); \
+ return MakeIpcFailure(_hr_, (what)); \
+ } \
+ } while (0)
+
+WinFileDialogChild::IPCResult WinFileDialogChild::RecvShowFileDialog(
+ uintptr_t parentHwnd, FileDialogType type, nsTArray<Command> commands,
+ FileResolver&& resolver) {
+ MOZ_ABORT_IF_ALREADY_USED();
+
+ SpawnFilePicker(HWND(parentHwnd), type, std::move(commands))
+ ->Then(
+ GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
+ [resolver = std::move(resolver)](Maybe<Results> const& res) {
+ resolver(res);
+ },
+ [self = RefPtr(this)](HRESULT hr) {
+ // this doesn't need to be returned anywhere; it'll crash the
+ // process as a side effect of construction
+ self->MakeIpcFailure(hr, "SpawnFilePicker");
+ });
+
+ return IPC_OK();
+}
+
+WinFileDialogChild::IPCResult WinFileDialogChild::RecvShowFolderDialog(
+ uintptr_t parentHwnd, nsTArray<Command> commands,
+ FolderResolver&& resolver) {
+ MOZ_ABORT_IF_ALREADY_USED();
+
+ SpawnFolderPicker(HWND(parentHwnd), std::move(commands))
+ ->Then(
+ GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
+ [resolver = std::move(resolver)](Maybe<nsString> const& res) {
+ resolver(res);
+ },
+ [self = RefPtr(this), resolver](HRESULT hr) {
+ // this doesn't need to be returned anywhere; it'll crash the
+ // process as a side effect of construction
+ self->MakeIpcFailure(hr, "SpawnFolderPicker");
+ });
+
+ return IPC_OK();
+}
+
+#undef MOZ_IPC_ENSURE_HRESULT_OK
+
+void WinFileDialogChild::ProcessingError(Result aCode, const char* aReason) {
+ detail::LogProcessingError(sLogFileDialog, this, aCode, aReason);
+}
+
+} // namespace mozilla::widget::filedialog
diff --git a/widget/windows/filedialog/WinFileDialogChild.h b/widget/windows/filedialog/WinFileDialogChild.h
new file mode 100644
index 0000000000..b0939ce2ed
--- /dev/null
+++ b/widget/windows/filedialog/WinFileDialogChild.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef widget_windows_filedialog_WinFileDialogChild_h__
+#define widget_windows_filedialog_WinFileDialogChild_h__
+
+#include "mozilla/widget/filedialog/PWinFileDialogChild.h"
+
+// forward declaration of native Windows interface-struct
+struct IFileDialog;
+
+namespace mozilla::widget::filedialog {
+
+class WinFileDialogChild : public PWinFileDialogChild {
+ public:
+ using Command = mozilla::widget::filedialog::Command;
+ using IPCResult = mozilla::ipc::IPCResult;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinFileDialogChild, override);
+
+ WinFileDialogChild();
+
+ public:
+ using FileResolver = PWinFileDialogChild::ShowFileDialogResolver;
+ IPCResult RecvShowFileDialog(uintptr_t parentHwnd, FileDialogType,
+ nsTArray<Command>, FileResolver&&);
+
+ using FolderResolver = PWinFileDialogChild::ShowFolderDialogResolver;
+ IPCResult RecvShowFolderDialog(uintptr_t parentHwnd, nsTArray<Command>,
+ FolderResolver&&);
+
+ private:
+ ~WinFileDialogChild();
+
+ void ProcessingError(Result aCode, const char* aReason) override;
+
+ // Defined and used only in WinFileDialogChild.cpp.
+ template <size_t N>
+ IPCResult MakeIpcFailure(HRESULT hr, const char (&what)[N]);
+
+ // This flag properly _should_ be static (_i.e._, per-process) rather than
+ // per-instance; but we can't presently instantiate two separate utility
+ // processes with the same sandbox type, so we have to reuse the existing
+ // utility process if there is one.
+ bool mUsed = false;
+};
+
+} // namespace mozilla::widget::filedialog
+
+#endif // widget_windows_filedialog_WinFileDialogChild_h__
diff --git a/widget/windows/filedialog/WinFileDialogCommands.cpp b/widget/windows/filedialog/WinFileDialogCommands.cpp
new file mode 100644
index 0000000000..f0503ab8f0
--- /dev/null
+++ b/widget/windows/filedialog/WinFileDialogCommands.cpp
@@ -0,0 +1,460 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/widget/filedialog/WinFileDialogCommands.h"
+
+#include <type_traits>
+#include <shobjidl.h>
+#include <shtypes.h>
+#include <winerror.h>
+#include "WinUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/ipc/UtilityProcessManager.h"
+#include "mozilla/mscom/ApartmentRegion.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::widget::filedialog {
+
+// Visitor to apply commands to the dialog.
+struct Applicator {
+ IFileDialog* dialog = nullptr;
+
+ HRESULT Visit(Command const& c) {
+ switch (c.type()) {
+ default:
+ case Command::T__None:
+ return E_INVALIDARG;
+
+ case Command::TSetOptions:
+ return Apply(c.get_SetOptions());
+ case Command::TSetTitle:
+ return Apply(c.get_SetTitle());
+ case Command::TSetOkButtonLabel:
+ return Apply(c.get_SetOkButtonLabel());
+ case Command::TSetFolder:
+ return Apply(c.get_SetFolder());
+ case Command::TSetFileName:
+ return Apply(c.get_SetFileName());
+ case Command::TSetDefaultExtension:
+ return Apply(c.get_SetDefaultExtension());
+ case Command::TSetFileTypes:
+ return Apply(c.get_SetFileTypes());
+ case Command::TSetFileTypeIndex:
+ return Apply(c.get_SetFileTypeIndex());
+ }
+ }
+
+ HRESULT Apply(SetOptions const& c) { return dialog->SetOptions(c.options()); }
+ HRESULT Apply(SetTitle const& c) { return dialog->SetTitle(c.title().get()); }
+ HRESULT Apply(SetOkButtonLabel const& c) {
+ return dialog->SetOkButtonLabel(c.label().get());
+ }
+ HRESULT Apply(SetFolder const& c) {
+ RefPtr<IShellItem> folder;
+ if (SUCCEEDED(SHCreateItemFromParsingName(
+ c.path().get(), nullptr, IID_IShellItem, getter_AddRefs(folder)))) {
+ return dialog->SetFolder(folder);
+ }
+ // graciously accept that the provided path may have been nonsense
+ return S_OK;
+ }
+ HRESULT Apply(SetFileName const& c) {
+ return dialog->SetFileName(c.filename().get());
+ }
+ HRESULT Apply(SetDefaultExtension const& c) {
+ return dialog->SetDefaultExtension(c.extension().get());
+ }
+ HRESULT Apply(SetFileTypes const& c) {
+ std::vector<COMDLG_FILTERSPEC> vec;
+ for (auto const& filter : c.filterList()) {
+ vec.push_back(
+ {.pszName = filter.name().get(), .pszSpec = filter.spec().get()});
+ }
+ return dialog->SetFileTypes(vec.size(), vec.data());
+ }
+ HRESULT Apply(SetFileTypeIndex const& c) {
+ return dialog->SetFileTypeIndex(c.index());
+ }
+};
+
+namespace {
+static HRESULT GetShellItemPath(IShellItem* aItem, nsString& aResultString) {
+ NS_ENSURE_TRUE(aItem, E_INVALIDARG);
+
+ mozilla::UniquePtr<wchar_t, CoTaskMemFreeDeleter> str;
+ HRESULT const hr =
+ aItem->GetDisplayName(SIGDN_FILESYSPATH, getter_Transfers(str));
+ if (SUCCEEDED(hr)) {
+ aResultString.Assign(str.get());
+ }
+ return hr;
+}
+} // namespace
+
+#define MOZ_ENSURE_HRESULT_OK(call_) \
+ do { \
+ HRESULT const _tmp_hr_ = (call_); \
+ if (FAILED(_tmp_hr_)) return Err(_tmp_hr_); \
+ } while (0)
+
+mozilla::Result<RefPtr<IFileDialog>, HRESULT> MakeFileDialog(
+ FileDialogType type) {
+ RefPtr<IFileDialog> dialog;
+
+ CLSID const clsid = type == FileDialogType::Open ? CLSID_FileOpenDialog
+ : CLSID_FileSaveDialog;
+ HRESULT const hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IFileDialog, getter_AddRefs(dialog));
+ MOZ_ENSURE_HRESULT_OK(hr);
+
+ return std::move(dialog);
+}
+
+HRESULT ApplyCommands(::IFileDialog* dialog,
+ nsTArray<Command> const& commands) {
+ Applicator applicator{.dialog = dialog};
+ for (auto const& cmd : commands) {
+ HRESULT const hr = applicator.Visit(cmd);
+ if (FAILED(hr)) {
+ return hr;
+ }
+ }
+ return S_OK;
+}
+
+mozilla::Result<Results, HRESULT> GetFileResults(::IFileDialog* dialog) {
+ FILEOPENDIALOGOPTIONS fos;
+ MOZ_ENSURE_HRESULT_OK(dialog->GetOptions(&fos));
+
+ using widget::WinUtils;
+
+ // Extract which filter type the user selected
+ UINT index;
+ MOZ_ENSURE_HRESULT_OK(dialog->GetFileTypeIndex(&index));
+
+ // single selection
+ if ((fos & FOS_ALLOWMULTISELECT) == 0) {
+ RefPtr<IShellItem> item;
+ MOZ_ENSURE_HRESULT_OK(dialog->GetResult(getter_AddRefs(item)));
+ if (!item) {
+ return Err(E_FAIL);
+ }
+
+ nsAutoString path;
+ MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, path));
+
+ return Results({path}, index);
+ }
+
+ // multiple selection
+ RefPtr<IFileOpenDialog> openDlg;
+ dialog->QueryInterface(IID_IFileOpenDialog, getter_AddRefs(openDlg));
+ if (!openDlg) {
+ MOZ_ASSERT(false, "a file-save dialog was given FOS_ALLOWMULTISELECT?");
+ return Err(E_UNEXPECTED);
+ }
+
+ RefPtr<IShellItemArray> items;
+ MOZ_ENSURE_HRESULT_OK(openDlg->GetResults(getter_AddRefs(items)));
+ if (!items) {
+ return Err(E_FAIL);
+ }
+
+ nsTArray<nsString> paths;
+
+ DWORD count = 0;
+ MOZ_ENSURE_HRESULT_OK(items->GetCount(&count));
+ for (DWORD idx = 0; idx < count; idx++) {
+ RefPtr<IShellItem> item;
+ MOZ_ENSURE_HRESULT_OK(items->GetItemAt(idx, getter_AddRefs(item)));
+
+ nsAutoString str;
+ MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, str));
+
+ paths.EmplaceBack(str);
+ }
+
+ return Results(std::move(paths), std::move(index));
+}
+
+mozilla::Result<nsString, HRESULT> GetFolderResults(::IFileDialog* dialog) {
+ RefPtr<IShellItem> item;
+ MOZ_ENSURE_HRESULT_OK(dialog->GetResult(getter_AddRefs(item)));
+ if (!item) {
+ // shouldn't happen -- probably a precondition failure on our part, but
+ // might be due to misbehaving shell extensions?
+ MOZ_ASSERT(false,
+ "unexpected lack of item: was `Show`'s return value checked?");
+ return Err(E_FAIL);
+ }
+
+ // If the user chose a Win7 Library, resolve to the library's
+ // default save folder.
+ RefPtr<IShellLibrary> shellLib;
+ RefPtr<IShellItem> folderPath;
+ MOZ_ENSURE_HRESULT_OK(
+ CoCreateInstance(CLSID_ShellLibrary, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLibrary, getter_AddRefs(shellLib)));
+
+ if (shellLib && SUCCEEDED(shellLib->LoadLibraryFromItem(item, STGM_READ)) &&
+ SUCCEEDED(shellLib->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem,
+ getter_AddRefs(folderPath)))) {
+ item.swap(folderPath);
+ }
+
+ // get the folder's file system path
+ nsAutoString str;
+ MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, str));
+ return str;
+}
+
+#undef MOZ_ENSURE_HRESULT_OK
+
+namespace detail {
+void LogProcessingError(LogModule* aModule, ipc::IProtocol* aCaller,
+ ipc::HasResultCodes::Result aCode,
+ const char* aReason) {
+ LogLevel const level = [&]() {
+ switch (aCode) {
+ case ipc::HasResultCodes::MsgProcessed:
+ // Normal operation. (We probably never actually get this code.)
+ return LogLevel::Verbose;
+
+ case ipc::HasResultCodes::MsgDropped:
+ return LogLevel::Verbose;
+
+ default:
+ return LogLevel::Error;
+ }
+ }();
+
+ // Processing errors are sometimes unhelpfully formatted. We can't fix that
+ // directly because the unhelpful formatting has made its way to telemetry
+ // (table `telemetry.socorro_crash`, column `ipc_channel_error`) and is being
+ // aggregated on. :(
+ nsCString reason(aReason);
+ if (reason.Last() == '\n') {
+ reason.Truncate(reason.Length() - 1);
+ }
+
+ if (MOZ_LOG_TEST(aModule, level)) {
+ const char* const side = [&]() {
+ switch (aCaller->GetSide()) {
+ case ipc::ParentSide:
+ return "parent";
+ case ipc::ChildSide:
+ return "child";
+ case ipc::UnknownSide:
+ return "unknown side";
+ default:
+ return "<illegal value>";
+ }
+ }();
+
+ const char* const errorStr = [&]() {
+ switch (aCode) {
+ case ipc::HasResultCodes::MsgProcessed:
+ return "Processed";
+ case ipc::HasResultCodes::MsgDropped:
+ return "Dropped";
+ case ipc::HasResultCodes::MsgNotKnown:
+ return "NotKnown";
+ case ipc::HasResultCodes::MsgNotAllowed:
+ return "NotAllowed";
+ case ipc::HasResultCodes::MsgPayloadError:
+ return "PayloadError";
+ case ipc::HasResultCodes::MsgProcessingError:
+ return "ProcessingError";
+ case ipc::HasResultCodes::MsgRouteError:
+ return "RouteError";
+ case ipc::HasResultCodes::MsgValueError:
+ return "ValueError";
+ default:
+ return "<illegal error type>";
+ }
+ }();
+
+ MOZ_LOG(aModule, level,
+ ("%s [%s]: IPC error (%s): %s", aCaller->GetProtocolName(), side,
+ errorStr, reason.get()));
+ }
+
+ if (level == LogLevel::Error) {
+ // kill the child process...
+ if (aCaller->GetSide() == ipc::ParentSide) {
+ // ... which isn't us
+ ipc::UtilityProcessManager::GetSingleton()->CleanShutdown(
+ ipc::SandboxingKind::WINDOWS_FILE_DIALOG);
+ } else {
+ // ... which (presumably) is us
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::ipc_channel_error, reason);
+
+ MOZ_CRASH("IPC error");
+ }
+ }
+}
+
+// Given a (synchronous) Action returning a Result<T, HRESULT>, perform that
+// action on a new single-purpose "File Dialog" thread, with COM initialized as
+// STA. (The thread will be destroyed afterwards.)
+//
+// Returns a Promise which will resolve to T (if the action returns Ok) or
+// reject with an HRESULT (if the action either returns Err or couldn't be
+// performed).
+template <typename Res, typename Action, size_t N>
+RefPtr<Promise<Res>> SpawnFileDialogThread(const char (&where)[N],
+ Action action) {
+ RefPtr<nsIThread> thread;
+ {
+ nsresult rv = NS_NewNamedThread("File Dialog", getter_AddRefs(thread),
+ nullptr, {.isUiThread = true});
+ if (NS_FAILED(rv)) {
+ return Promise<Res>::CreateAndReject((HRESULT)rv, where);
+ }
+ }
+ // `thread` is single-purpose, and should not perform any additional work
+ // after `action`. Shut it down after we've dispatched that.
+ auto close_thread_ = MakeScopeExit([&]() {
+ auto const res = thread->AsyncShutdown();
+ static_assert(
+ std::is_same_v<uint32_t, std::underlying_type_t<decltype(res)>>);
+ if (NS_FAILED(res)) {
+ MOZ_LOG(sLogFileDialog, LogLevel::Warning,
+ ("thread->AsyncShutdown() failed: res=0x%08" PRIX32,
+ static_cast<uint32_t>(res)));
+ }
+ });
+
+ // our eventual return value
+ RefPtr promise = MakeRefPtr<typename Promise<Res>::Private>(where);
+
+ // alias to reduce indentation depth
+ auto const dispatch = [&](auto closure) {
+ return thread->DispatchToQueue(
+ NS_NewRunnableFunction(where, std::move(closure)),
+ mozilla::EventQueuePriority::Normal);
+ };
+
+ dispatch([thread, promise, where, action = std::move(action)]() {
+ // Like essentially all COM UI components, the file dialog is STA: it must
+ // be associated with a specific thread to create its HWNDs and receive
+ // messages for them. If it's launched from a thread in the multithreaded
+ // apartment (including via implicit MTA), COM will proxy out to the
+ // process's main STA thread, and the file-dialog's modal loop will run
+ // there.
+ //
+ // This of course would completely negate any point in using a separate
+ // thread, since behind the scenes the dialog would still be running on the
+ // process's main thread. In particular, under that arrangement, file
+ // dialogs (and other nested modal loops, like those performed by
+ // `SpinEventLoopUntil`) will resolve in strictly LIFO order, effectively
+ // remaining suspended until all later modal loops resolve.
+ //
+ // To avoid this, we initialize COM as STA, so that it (rather than the main
+ // STA thread) is the file dialog's "home" thread and the IFileDialog's home
+ // apartment.
+
+ mozilla::mscom::STARegion staRegion;
+ if (!staRegion) {
+ MOZ_LOG(sLogFileDialog, LogLevel::Error,
+ ("COM init failed on file dialog thread: hr = %08lx",
+ staRegion.GetHResult()));
+
+ APTTYPE at;
+ APTTYPEQUALIFIER atq;
+ HRESULT const hr = ::CoGetApartmentType(&at, &atq);
+ MOZ_LOG(sLogFileDialog, LogLevel::Error,
+ (" current COM apartment state: hr = %08lX, APTTYPE = "
+ "%08X, APTTYPEQUALIFIER = %08X",
+ hr, at, atq));
+
+ // If this happens in the utility process, crash so we learn about it.
+ // (TODO: replace this with a telemetry ping.)
+ if (!XRE_IsParentProcess()) {
+ // Preserve relevant data on the stack for later analysis.
+ std::tuple volatile info{staRegion.GetHResult(), hr, at, atq};
+ MOZ_CRASH("Could not initialize COM STA in utility process");
+ }
+
+ // If this happens in the parent process, don't crash; just fall back to a
+ // nested modal loop. This isn't ideal, but it will probably still work
+ // well enough for the common case, wherein no other modal loops are
+ // active.
+ //
+ // (TODO: replace this with a telemetry ping, too.)
+ }
+
+ // Actually invoke the action and report the result.
+ Result<Res, HRESULT> val = action();
+ if (val.isErr()) {
+ promise->Reject(val.unwrapErr(), where);
+ } else {
+ promise->Resolve(val.unwrap(), where);
+ }
+ });
+
+ return promise;
+}
+
+// For F returning `Result<T, E>`, yields the type `T`.
+template <typename F, typename... Args>
+using inner_result_of =
+ typename std::remove_reference_t<decltype(std::declval<F>()(
+ std::declval<Args>()...))>::ok_type;
+
+template <typename ExtractorF,
+ typename RetT = inner_result_of<ExtractorF, IFileDialog*>>
+auto SpawnPickerT(HWND parent, FileDialogType type, ExtractorF&& extractor,
+ nsTArray<Command> commands) -> RefPtr<Promise<Maybe<RetT>>> {
+ return detail::SpawnFileDialogThread<Maybe<RetT>>(
+ __PRETTY_FUNCTION__,
+ [=, commands = std::move(commands)]() -> Result<Maybe<RetT>, HRESULT> {
+ // On Win10, the picker doesn't support per-monitor DPI, so we create it
+ // with our context set temporarily to system-dpi-aware.
+ WinUtils::AutoSystemDpiAware dpiAwareness;
+
+ RefPtr<IFileDialog> dialog;
+ MOZ_TRY_VAR(dialog, MakeFileDialog(type));
+
+ if (HRESULT const rv = ApplyCommands(dialog, commands); FAILED(rv)) {
+ return mozilla::Err(rv);
+ }
+
+ if (HRESULT const rv = dialog->Show(parent); FAILED(rv)) {
+ if (rv == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
+ return Result<Maybe<RetT>, HRESULT>(Nothing());
+ }
+ return mozilla::Err(rv);
+ }
+
+ RetT res;
+ MOZ_TRY_VAR(res, extractor(dialog.get()));
+
+ return Some(res);
+ });
+}
+
+} // namespace detail
+
+RefPtr<Promise<Maybe<Results>>> SpawnFilePicker(HWND parent,
+ FileDialogType type,
+ nsTArray<Command> commands) {
+ return detail::SpawnPickerT(parent, type, GetFileResults,
+ std::move(commands));
+}
+
+RefPtr<Promise<Maybe<nsString>>> SpawnFolderPicker(HWND parent,
+ nsTArray<Command> commands) {
+ return detail::SpawnPickerT(parent, FileDialogType::Open, GetFolderResults,
+ std::move(commands));
+}
+
+} // namespace mozilla::widget::filedialog
diff --git a/widget/windows/filedialog/WinFileDialogCommands.h b/widget/windows/filedialog/WinFileDialogCommands.h
new file mode 100644
index 0000000000..ca4561a8f2
--- /dev/null
+++ b/widget/windows/filedialog/WinFileDialogCommands.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef widget_windows_filedialog_WinFileDialogCommands_h__
+#define widget_windows_filedialog_WinFileDialogCommands_h__
+
+#include "ipc/EnumSerializer.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/ipc/MessageLink.h"
+#include "mozilla/widget/filedialog/WinFileDialogCommandsDefn.h"
+
+// Windows interface types, defined in <shobjidl.h>
+struct IFileDialog;
+struct IFileOpenDialog;
+
+namespace mozilla::widget::filedialog {
+
+extern LazyLogModule sLogFileDialog;
+
+enum class FileDialogType : uint8_t { Open, Save };
+
+// Create a file-dialog of the relevant type. Requires MSCOM to be initialized.
+mozilla::Result<RefPtr<IFileDialog>, HRESULT> MakeFileDialog(FileDialogType);
+
+// Apply the selected commands to the IFileDialog, in preparation for showing
+// it. (The actual showing step is left to the caller.)
+[[nodiscard]] HRESULT ApplyCommands(::IFileDialog*,
+ nsTArray<Command> const& commands);
+
+// Extract one or more results from the file-picker dialog.
+//
+// Requires that Show() has been called and has returned S_OK.
+mozilla::Result<Results, HRESULT> GetFileResults(::IFileDialog*);
+
+// Extract the chosen folder from the folder-picker dialog.
+//
+// Requires that Show() has been called and has returned S_OK.
+mozilla::Result<nsString, HRESULT> GetFolderResults(::IFileDialog*);
+
+namespace detail {
+// Log the error. If it's a notable error, kill the child process.
+void LogProcessingError(LogModule* aModule, ipc::IProtocol* aCaller,
+ ipc::HasResultCodes::Result aCode, const char* aReason);
+
+} // namespace detail
+
+template <typename R>
+using Promise = MozPromise<R, HRESULT, true>;
+
+// Show a file-picker on another thread in the current process.
+RefPtr<Promise<Maybe<Results>>> SpawnFilePicker(HWND parent,
+ FileDialogType type,
+ nsTArray<Command> commands);
+
+// Show a folder-picker on another thread in the current process.
+RefPtr<Promise<Maybe<nsString>>> SpawnFolderPicker(HWND parent,
+ nsTArray<Command> commands);
+
+} // namespace mozilla::widget::filedialog
+
+namespace IPC {
+template <>
+struct ParamTraits<mozilla::widget::filedialog::FileDialogType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::widget::filedialog::FileDialogType,
+ mozilla::widget::filedialog::FileDialogType::Open,
+ mozilla::widget::filedialog::FileDialogType::Save> {};
+} // namespace IPC
+
+#endif // widget_windows_filedialog_WinFileDialogCommands_h__
diff --git a/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh b/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh
new file mode 100644
index 0000000000..dd85942f24
--- /dev/null
+++ b/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=ipdl : */
+/* 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/. */
+
+namespace mozilla {
+namespace widget {
+namespace filedialog {
+
+// Commands corresponding to the various functions in IFileDialog (or at least
+// the ones we actually make use of).
+//
+// All commands' semantics are direct parallels of their equivalently-named
+// functions on IFileDialog, with the only changes being those necessary to use
+// IPDLable representation-datatypes. (Thus, e.g., `SetOptions` effectively
+// takes a `FILEOPENDIALOGOPTIONS`, and `SetFileTypeIndex` is 1-based.)
+struct SetOptions { uint32_t options; };
+struct SetTitle { nsString title; };
+struct SetOkButtonLabel { nsString label; };
+struct SetFolder { nsString path; };
+struct SetFileName { nsString filename; };
+struct SetDefaultExtension { nsString extension; };
+struct ComDlgFilterSpec { nsString name; nsString spec; };
+struct SetFileTypes { ComDlgFilterSpec[] filterList; };
+struct SetFileTypeIndex { uint32_t index; };
+
+// Union of the above.
+union Command {
+ SetOptions;
+ SetTitle;
+ SetOkButtonLabel;
+ SetFolder;
+ SetFileName;
+ SetDefaultExtension;
+ SetFileTypes;
+ SetFileTypeIndex;
+};
+
+// The results from opening a file dialog. (Note that folder selection only
+// returns an nsString.)
+struct Results {
+ nsString[] paths;
+ uint32_t selectedFileTypeIndex;
+};
+
+} // namespace filedialog
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/windows/filedialog/WinFileDialogParent.cpp b/widget/windows/filedialog/WinFileDialogParent.cpp
new file mode 100644
index 0000000000..2c256a1506
--- /dev/null
+++ b/widget/windows/filedialog/WinFileDialogParent.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/widget/filedialog/WinFileDialogParent.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/Result.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/ipc/UtilityProcessManager.h"
+#include "nsISupports.h"
+
+namespace mozilla::widget::filedialog {
+
+// Count of currently-open file dialogs (not just open-file dialogs).
+static size_t sOpenDialogActors = 0;
+
+WinFileDialogParent::WinFileDialogParent() {
+ MOZ_LOG(sLogFileDialog, LogLevel::Debug,
+ ("%s %p", __PRETTY_FUNCTION__, this));
+}
+
+WinFileDialogParent::~WinFileDialogParent() {
+ MOZ_LOG(sLogFileDialog, LogLevel::Debug,
+ ("%s %p", __PRETTY_FUNCTION__, this));
+}
+
+PWinFileDialogParent::nsresult WinFileDialogParent::BindToUtilityProcess(
+ mozilla::ipc::UtilityProcessParent* aUtilityParent) {
+ Endpoint<PWinFileDialogParent> parentEnd;
+ Endpoint<PWinFileDialogChild> childEnd;
+ nsresult rv = PWinFileDialog::CreateEndpoints(base::GetCurrentProcId(),
+ aUtilityParent->OtherPid(),
+ &parentEnd, &childEnd);
+
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Protocol endpoints failure");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!aUtilityParent->SendStartWinFileDialogService(std::move(childEnd))) {
+ MOZ_ASSERT(false, "SendStartWinFileDialogService failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!parentEnd.Bind(this)) {
+ MOZ_ASSERT(false, "parentEnd.Bind failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ sOpenDialogActors++;
+ return NS_OK;
+}
+
+void WinFileDialogParent::ProcessingError(Result aCode, const char* aReason) {
+ detail::LogProcessingError(sLogFileDialog, this, aCode, aReason);
+}
+
+ProcessProxy::ProcessProxy(RefPtr<WFDP>&& obj)
+ : data(MakeRefPtr<Contents>(std::move(obj))) {}
+
+ProcessProxy::Contents::Contents(RefPtr<WFDP>&& obj) : ptr(std::move(obj)) {}
+
+ProcessProxy::Contents::~Contents() {
+ AssertIsOnMainThread();
+
+ // destroy the actor...
+ ptr->Close();
+
+ // ... and possibly the process
+ if (!--sOpenDialogActors) {
+ StopProcess();
+ }
+}
+
+void ProcessProxy::Contents::StopProcess() {
+ auto const upm = ipc::UtilityProcessManager::GetSingleton();
+ if (!upm) {
+ // This is only possible when the UtilityProcessManager has shut down -- in
+ // which case the file-dialog process has also already been directed to shut
+ // down, and there's nothing we need to do here.
+ return;
+ }
+
+ MOZ_LOG(sLogFileDialog, LogLevel::Debug,
+ ("%s: killing the WINDOWS_FILE_DIALOG process (no more live "
+ "actors)",
+ __PRETTY_FUNCTION__));
+ upm->CleanShutdown(ipc::SandboxingKind::WINDOWS_FILE_DIALOG);
+}
+
+} // namespace mozilla::widget::filedialog
diff --git a/widget/windows/filedialog/WinFileDialogParent.h b/widget/windows/filedialog/WinFileDialogParent.h
new file mode 100644
index 0000000000..a2c1197c55
--- /dev/null
+++ b/widget/windows/filedialog/WinFileDialogParent.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef widget_windows_filedialog_WinFileDialogParent_h__
+#define widget_windows_filedialog_WinFileDialogParent_h__
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/ProcInfo.h"
+#include "mozilla/Result.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/ChromeUtilsBinding.h"
+#include "mozilla/ipc/UtilityProcessParent.h"
+#include "mozilla/widget/filedialog/PWinFileDialogParent.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::widget::filedialog {
+
+class WinFileDialogParent : public PWinFileDialogParent {
+ public:
+ using UtilityActorName = ::mozilla::UtilityActorName;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinFileDialogParent, override);
+
+ public:
+ WinFileDialogParent();
+ nsresult BindToUtilityProcess(
+ mozilla::ipc::UtilityProcessParent* aUtilityParent);
+
+ UtilityActorName GetActorName() {
+ return UtilityActorName::WindowsFileDialog;
+ }
+
+ private:
+ ~WinFileDialogParent();
+
+ void ProcessingError(Result aCode, const char* aReason) override;
+};
+
+// Proxy for the WinFileDialog process and actor.
+//
+// The IPC subsystem holds a strong reference to all IPC actors, so releasing
+// the last RefPtr for such an actor does not actually cause the actor to be
+// destroyed. Similarly, the UtilityProcessManager owns the host process for an
+// actor, and merely destroying all actors within that host process will not
+// cause it to be reaped.
+//
+// This object, then, acts as a proxy for those objects' lifetimes: when the
+// last reference to `Contents` is released, the necessary explicit cleanup of
+// the actor (and, if possible, the host process) will be performed.
+class ProcessProxy {
+ public:
+ using WFDP = WinFileDialogParent;
+
+ explicit ProcessProxy(RefPtr<WFDP>&& obj);
+ ~ProcessProxy() = default;
+
+ explicit operator bool() const { return data->ptr && data->ptr->CanSend(); }
+ bool operator!() const { return !bool(*this); }
+
+ WFDP& operator*() const { return *data->ptr; }
+ WFDP* operator->() const { return data->ptr; }
+ WFDP* get() const { return data->ptr; }
+
+ ProcessProxy(ProcessProxy const& that) = default;
+ ProcessProxy(ProcessProxy&&) = default;
+
+ private:
+ struct Contents {
+ NS_INLINE_DECL_REFCOUNTING(Contents);
+
+ public:
+ explicit Contents(RefPtr<WFDP>&& obj);
+ RefPtr<WFDP> const ptr;
+
+ private:
+ ~Contents();
+ void StopProcess();
+ };
+ // guaranteed nonnull
+ RefPtr<Contents> data;
+};
+
+} // namespace mozilla::widget::filedialog
+
+#endif // widget_windows_filedialog_WinFileDialogParent_h__
diff --git a/widget/windows/filedialog/moz.build b/widget/windows/filedialog/moz.build
new file mode 100644
index 0000000000..d2732faf78
--- /dev/null
+++ b/widget/windows/filedialog/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+IPDL_SOURCES += [
+ "PWinFileDialog.ipdl",
+ "WinFileDialogCommandsDefn.ipdlh",
+]
+
+UNIFIED_SOURCES += [
+ "WinFileDialogChild.cpp",
+ "WinFileDialogCommands.cpp",
+ "WinFileDialogParent.cpp",
+]
+
+EXPORTS.mozilla.widget.filedialog += [
+ "WinFileDialogChild.h",
+ "WinFileDialogCommands.h",
+ "WinFileDialogParent.h",
+]
+
+# needed for IPC header files
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/widget/windows/metrics.yaml b/widget/windows/metrics.yaml
new file mode 100644
index 0000000000..e9d3b4f5cf
--- /dev/null
+++ b/widget/windows/metrics.yaml
@@ -0,0 +1,58 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: Widget: Win32'
+
+file_dialog:
+ fallback:
+ type: event
+ description: >
+ Records the result of an attempt to open and use the out-of-process file
+ dialog when the in-process file-dialog is available as a fallback.
+ metadata:
+ # mostly technical, but includes timing data that may derive from user
+ # interactions
+ data-sensitivity: [technical, interaction]
+ notification_emails:
+ - rkraesig@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1872397
+ # this event may alternatively be manually expired once bug 1677170 is
+ # closed
+ expires: 135
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1872397#c7
+ extra_keys:
+ succeeded:
+ type: boolean
+ description: >
+ Whether the out-of-process dialog succeeded or failed. (Note that
+ user-induced cancellation is considered a form of success.)
+ time_remote:
+ type: quantity
+ description: >
+ The time between the out-of-process file dialog's instantiation
+ attempt and its failure, in milliseconds.
+ hresult_remote:
+ type: string
+ description: >
+ The failure code produced by the out-of-process file dialog, formatted
+ as eight hexdigits. Only present when `!succeeded`.
+ time_local:
+ type: quantity
+ description: >
+ The time between the in-process file dialog's instantiation attempt
+ and its conclusion (successfully or otherwise), in milliseconds. Only
+ present when `!succeeded`.
+ hresult_local:
+ type: string
+ description: >
+ The return code produced by the in-process file dialog, formatted as
+ eight hexdigits. Only present when `!succeeded`.
diff --git a/widget/windows/moz.build b/widget/windows/moz.build
new file mode 100644
index 0000000000..f19a46caf1
--- /dev/null
+++ b/widget/windows/moz.build
@@ -0,0 +1,213 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Win32")
+ SCHEDULES.exclusive = ["windows"]
+
+with Files("*CompositorWidget*"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+with Files("*IMEHandler*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*IMMHandler*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*KeyboardLayout*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("OSK*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+with Files("*TSFTextStore*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+DIRS += [
+ "filedialog",
+]
+
+TEST_DIRS += ["tests"]
+
+EXPORTS += [
+ "nsdefs.h",
+ "WindowHook.h",
+ "WinUtils.h",
+]
+
+EXPORTS.mozilla += [
+ "ShellHeaderOnlyUtils.h",
+ "ToastNotificationHeaderOnlyUtils.h",
+ "UrlmonHeaderOnlyUtils.h",
+ "WindowsConsole.h",
+ "WindowsEventLog.h",
+ "WinHeaderOnlyUtils.h",
+]
+
+EXPORTS.mozilla.widget += [
+ "AudioSession.h",
+ "CompositorWidgetChild.h",
+ "CompositorWidgetParent.h",
+ "InProcessWinCompositorWidget.h",
+ "JumpListBuilder.h",
+ "nsWindowLoggedMessages.h",
+ "WinCompositorWidget.h",
+ "WinCompositorWindowThread.h",
+ "WindowsEMF.h",
+ "WindowsSMTCProvider.h",
+ "WinEventObserver.h",
+ "WinMessages.h",
+ "WinModifierKeyState.h",
+ "WinRegistry.h",
+ "WinTaskbar.h",
+ "WinWindowOcclusionTracker.h",
+]
+
+UNIFIED_SOURCES += [
+ "AudioSession.cpp",
+ "CompositorWidgetChild.cpp",
+ "DirectManipulationOwner.cpp",
+ "GfxInfo.cpp",
+ "IEnumFE.cpp",
+ "IMMHandler.cpp",
+ "JumpListBuilder.cpp",
+ "KeyboardLayout.cpp",
+ "LegacyJumpListItem.cpp",
+ "LSPAnnotator.cpp",
+ "nsAppShell.cpp",
+ "nsClipboard.cpp",
+ "nsColorPicker.cpp",
+ "nsDataObj.cpp",
+ "nsDataObjCollection.cpp",
+ "nsDragService.cpp",
+ "nsLookAndFeel.cpp",
+ "nsNativeDragSource.cpp",
+ "nsNativeDragTarget.cpp",
+ "nsNativeThemeWin.cpp",
+ "nsSound.cpp",
+ "nsToolkit.cpp",
+ "nsUserIdleServiceWin.cpp",
+ "nsUXThemeData.cpp",
+ "nsWindow.cpp",
+ "nsWindowDbg.cpp",
+ "nsWindowGfx.cpp",
+ "nsWindowLoggedMessages.cpp",
+ "nsWindowTaskbarConcealer.cpp",
+ "nsWinGesture.cpp",
+ "OSKTabTipManager.cpp",
+ "OSKVRManager.cpp",
+ "RemoteBackbuffer.cpp",
+ "ScreenHelperWin.cpp",
+ "SystemStatusBar.cpp",
+ "TaskbarPreview.cpp",
+ "TaskbarPreviewButton.cpp",
+ "TaskbarTabPreview.cpp",
+ "TaskbarWindowPreview.cpp",
+ "WidgetTraceEvent.cpp",
+ "WinCompositorWindowThread.cpp",
+ "WindowHook.cpp",
+ "WindowsConsole.cpp",
+ "WinEventObserver.cpp",
+ "WinIMEHandler.cpp",
+ "WinPointerEvents.cpp",
+ "WinRegistry.cpp",
+ "WinTaskbar.cpp",
+ "WinTextEventDispatcherListener.cpp",
+ "WinUtils.cpp",
+ "WinWindowOcclusionTracker.cpp",
+]
+
+# The following files cannot be built in unified mode because of name clashes.
+SOURCES += [
+ "CompositorWidgetParent.cpp",
+ "InProcessWinCompositorWidget.cpp",
+ "LegacyJumpListBuilder.cpp",
+ "MediaKeysEventSourceFactory.cpp",
+ "nsBidiKeyboard.cpp",
+ "nsFilePicker.cpp",
+ "nsSharePicker.cpp",
+ "nsWidgetFactory.cpp",
+ "OSKInputPaneManager.cpp",
+ "WinCompositorWidget.cpp",
+ "WindowsSMTCProvider.cpp",
+ "WindowsUIUtils.cpp",
+ "WinMouseScrollHandler.cpp",
+]
+
+# Needs INITGUID and we don't allow INITGUID in unified sources since bug 970429.
+SOURCES += [
+ "InputDeviceUtils.cpp",
+ "TSFTextStore.cpp",
+]
+
+if CONFIG["NS_PRINTING"]:
+ UNIFIED_SOURCES += [
+ "nsDeviceContextSpecWin.cpp",
+ "nsPrintDialogWin.cpp",
+ "nsPrinterWin.cpp",
+ "nsPrintSettingsServiceWin.cpp",
+ "nsPrintSettingsWin.cpp",
+ ]
+ SOURCES += [
+ "nsPrintDialogUtil.cpp",
+ ]
+
+if CONFIG["MOZ_ENABLE_SKIA_PDF"]:
+ UNIFIED_SOURCES += [
+ "WindowsEMF.cpp",
+ ]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["MOZ_ENABLE_SKIA_PDF"]:
+ LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"]
+
+LOCAL_INCLUDES += [
+ "/gfx/cairo/cairo/src",
+ "/layout/forms",
+ "/layout/generic",
+ "/layout/style",
+ "/layout/xul",
+ "/toolkit/components/jsoncpp/include",
+ "/toolkit/xre",
+ "/widget",
+ "/widget/headless",
+ "/xpcom/base",
+]
+
+DEFINES["MOZ_UNICODE"] = True
+DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"]
+# Turn `firefox` into `Firefox`.
+DEFINES["MOZ_TOAST_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"].title()
+
+for var in ("MOZ_ENABLE_D3D10_LAYER",):
+ if CONFIG[var]:
+ DEFINES[var] = True
+
+USE_LIBS += [
+ "jsoncpp",
+]
+
+OS_LIBS += [
+ "ktmw32",
+ "rpcrt4",
+ "urlmon",
+]
+
+# mingw is missing Windows toast notification definitions.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ SOURCES += [
+ "ToastNotification.cpp",
+ "ToastNotificationHandler.cpp",
+ ]
+
+SPHINX_TREES["/widget/windows"] = "docs"
diff --git a/widget/windows/nsAppShell.cpp b/widget/windows/nsAppShell.cpp
new file mode 100644
index 0000000000..314176766d
--- /dev/null
+++ b/widget/windows/nsAppShell.cpp
@@ -0,0 +1,1000 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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 "mozilla/Attributes.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/MessageChannel.h"
+#include "mozilla/ipc/WindowsMessageLoop.h"
+#include "nsAppShell.h"
+#include "nsToolkit.h"
+#include "nsThreadUtils.h"
+#include "WinUtils.h"
+#include "WinTaskbar.h"
+#include "WinMouseScrollHandler.h"
+#include "nsWindowDefs.h"
+#include "nsWindow.h"
+#include "nsString.h"
+#include "WinIMEHandler.h"
+#include "mozilla/widget/AudioSession.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/Hal.h"
+#include "nsIDOMWakeLockListener.h"
+#include "nsIPowerManagerService.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/StaticPtr.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsComponentManagerUtils.h"
+#include "ScreenHelperWin.h"
+#include "HeadlessScreenHelper.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/NativeNt.h"
+#include "mozilla/WindowsProcessMitigations.h"
+
+#include <winternl.h>
+
+#ifdef MOZ_BACKGROUNDTASKS
+# include "mozilla/BackgroundTasks.h"
+#endif
+
+#if defined(ACCESSIBILITY)
+# include "mozilla/a11y/Compatibility.h"
+# include "mozilla/a11y/Platform.h"
+#endif // defined(ACCESSIBILITY)
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#define WAKE_LOCK_LOG(...) \
+ MOZ_LOG(gWinWakeLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+static mozilla::LazyLogModule gWinWakeLockLog("WinWakeLock");
+
+// This wakelock listener is used for Window7 and above.
+class WinWakeLockListener final : public nsIDOMMozWakeLockListener {
+ public:
+ NS_DECL_ISUPPORTS
+ WinWakeLockListener() { MOZ_ASSERT(XRE_IsParentProcess()); }
+
+ private:
+ ~WinWakeLockListener() {
+ ReleaseWakelockIfNeeded(PowerRequestDisplayRequired);
+ ReleaseWakelockIfNeeded(PowerRequestExecutionRequired);
+ }
+
+ void SetHandle(HANDLE aHandle, POWER_REQUEST_TYPE aType) {
+ switch (aType) {
+ case PowerRequestDisplayRequired: {
+ if (!aHandle && mDisplayHandle) {
+ CloseHandle(mDisplayHandle);
+ }
+ mDisplayHandle = aHandle;
+ return;
+ }
+ case PowerRequestExecutionRequired: {
+ if (!aHandle && mNonDisplayHandle) {
+ CloseHandle(mNonDisplayHandle);
+ }
+ mNonDisplayHandle = aHandle;
+ return;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid request type");
+ return;
+ }
+ }
+
+ HANDLE GetHandle(POWER_REQUEST_TYPE aType) const {
+ switch (aType) {
+ case PowerRequestDisplayRequired:
+ return mDisplayHandle;
+ case PowerRequestExecutionRequired:
+ return mNonDisplayHandle;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid request type");
+ return nullptr;
+ }
+ }
+
+ HANDLE CreateHandle(POWER_REQUEST_TYPE aType) {
+ MOZ_ASSERT(!GetHandle(aType));
+ REASON_CONTEXT context = {0};
+ context.Version = POWER_REQUEST_CONTEXT_VERSION;
+ context.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING;
+ context.Reason.SimpleReasonString = RequestTypeLPWSTR(aType);
+ HANDLE handle = PowerCreateRequest(&context);
+ if (!handle) {
+ WAKE_LOCK_LOG("Failed to create handle for %s, error=%lu",
+ RequestTypeStr(aType), GetLastError());
+ return nullptr;
+ }
+ SetHandle(handle, aType);
+ return handle;
+ }
+
+ LPWSTR RequestTypeLPWSTR(POWER_REQUEST_TYPE aType) const {
+ switch (aType) {
+ case PowerRequestDisplayRequired:
+ return const_cast<LPWSTR>(L"display request"); // -Wwritable-strings
+ case PowerRequestExecutionRequired:
+ return const_cast<LPWSTR>(
+ L"non-display request"); // -Wwritable-strings
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid request type");
+ return const_cast<LPWSTR>(L"unknown"); // -Wwritable-strings
+ }
+ }
+
+ const char* RequestTypeStr(POWER_REQUEST_TYPE aType) const {
+ switch (aType) {
+ case PowerRequestDisplayRequired:
+ return "display request";
+ case PowerRequestExecutionRequired:
+ return "non-display request";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid request type");
+ return "unknown";
+ }
+ }
+
+ void RequestWakelockIfNeeded(POWER_REQUEST_TYPE aType) {
+ if (GetHandle(aType)) {
+ WAKE_LOCK_LOG("Already requested lock for %s", RequestTypeStr(aType));
+ return;
+ }
+
+ WAKE_LOCK_LOG("Prepare a wakelock for %s", RequestTypeStr(aType));
+ HANDLE handle = CreateHandle(aType);
+ if (!handle) {
+ WAKE_LOCK_LOG("Failed due to no handle for %s", RequestTypeStr(aType));
+ return;
+ }
+
+ if (PowerSetRequest(handle, aType)) {
+ WAKE_LOCK_LOG("Requested %s lock", RequestTypeStr(aType));
+ } else {
+ WAKE_LOCK_LOG("Failed to request %s lock, error=%lu",
+ RequestTypeStr(aType), GetLastError());
+ SetHandle(nullptr, aType);
+ }
+ }
+
+ void ReleaseWakelockIfNeeded(POWER_REQUEST_TYPE aType) {
+ if (!GetHandle(aType)) {
+ WAKE_LOCK_LOG("Already released lock for %s", RequestTypeStr(aType));
+ return;
+ }
+
+ WAKE_LOCK_LOG("Prepare to release wakelock for %s", RequestTypeStr(aType));
+ if (!PowerClearRequest(GetHandle(aType), aType)) {
+ WAKE_LOCK_LOG("Failed to release %s lock, error=%lu",
+ RequestTypeStr(aType), GetLastError());
+ return;
+ }
+ SetHandle(nullptr, aType);
+ WAKE_LOCK_LOG("Released wakelock for %s", RequestTypeStr(aType));
+ }
+
+ NS_IMETHOD Callback(const nsAString& aTopic,
+ const nsAString& aState) override {
+ WAKE_LOCK_LOG("topic=%s, state=%s", NS_ConvertUTF16toUTF8(aTopic).get(),
+ NS_ConvertUTF16toUTF8(aState).get());
+ if (!aTopic.EqualsASCII("screen") && !aTopic.EqualsASCII("audio-playing") &&
+ !aTopic.EqualsASCII("video-playing")) {
+ return NS_OK;
+ }
+
+ const bool isNonDisplayLock = aTopic.EqualsASCII("audio-playing");
+ bool requestLock = false;
+ if (isNonDisplayLock) {
+ requestLock = aState.EqualsASCII("locked-foreground") ||
+ aState.EqualsASCII("locked-background");
+ } else {
+ requestLock = aState.EqualsASCII("locked-foreground");
+ }
+
+ if (isNonDisplayLock) {
+ if (requestLock) {
+ RequestWakelockIfNeeded(PowerRequestExecutionRequired);
+ } else {
+ ReleaseWakelockIfNeeded(PowerRequestExecutionRequired);
+ }
+ } else {
+ if (requestLock) {
+ RequestWakelockIfNeeded(PowerRequestDisplayRequired);
+ } else {
+ ReleaseWakelockIfNeeded(PowerRequestDisplayRequired);
+ }
+ }
+ return NS_OK;
+ }
+
+ // Handle would only exist when we request wakelock successfully.
+ HANDLE mDisplayHandle = nullptr;
+ HANDLE mNonDisplayHandle = nullptr;
+};
+NS_IMPL_ISUPPORTS(WinWakeLockListener, nsIDOMMozWakeLockListener)
+StaticRefPtr<nsIDOMMozWakeLockListener> sWakeLockListener;
+
+static void AddScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sWakeLockListener = new WinWakeLockListener();
+ sPowerManagerService->AddWakeLockListener(sWakeLockListener);
+ } else {
+ NS_WARNING(
+ "Failed to retrieve PowerManagerService, wakelocks will be broken!");
+ }
+}
+
+static void RemoveScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sPowerManagerService->RemoveWakeLockListener(sWakeLockListener);
+ sPowerManagerService = nullptr;
+ sWakeLockListener = nullptr;
+ }
+}
+
+class SingleNativeEventPump final : public nsIThreadObserver {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITHREADOBSERVER
+
+ SingleNativeEventPump() {
+ MOZ_ASSERT(!XRE_UseNativeEventProcessing(),
+ "Should only be used when not properly processing events.");
+ }
+
+ private:
+ ~SingleNativeEventPump() {}
+};
+
+NS_IMPL_ISUPPORTS(SingleNativeEventPump, nsIThreadObserver)
+
+NS_IMETHODIMP
+SingleNativeEventPump::OnDispatchedEvent() { return NS_OK; }
+
+NS_IMETHODIMP
+SingleNativeEventPump::OnProcessNextEvent(nsIThreadInternal* aThread,
+ bool aMayWait) {
+ MSG msg;
+ bool gotMessage = WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE);
+ if (gotMessage) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessageW(&msg);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SingleNativeEventPump::AfterProcessNextEvent(nsIThreadInternal* aThread,
+ bool aMayWait) {
+ return NS_OK;
+}
+
+// RegisterWindowMessage values
+// Native event callback message
+const wchar_t* kAppShellGeckoEventId = L"nsAppShell:EventID";
+UINT sAppShellGeckoMsgId = 0x10001; // initialize to invalid message ID
+// Taskbar button creation message
+const wchar_t* kTaskbarButtonEventId = L"TaskbarButtonCreated";
+UINT sTaskbarButtonCreatedMsg = 0x10002; // initialize to invalid message ID
+
+/* static */
+UINT nsAppShell::GetTaskbarButtonCreatedMessage() {
+ return sTaskbarButtonCreatedMsg;
+}
+
+namespace mozilla {
+namespace crashreporter {
+void LSPAnnotate();
+} // namespace crashreporter
+} // namespace mozilla
+
+using mozilla::crashreporter::LSPAnnotate;
+
+//-------------------------------------------------------------------------
+
+// Note that since we're on x86-ish processors here, ReleaseAcquire is the
+// semantics that normal loads and stores would use anyway.
+static Atomic<size_t, ReleaseAcquire> sOutstandingNativeEventCallbacks;
+
+/*static*/ LRESULT CALLBACK nsAppShell::EventWindowProc(HWND hwnd, UINT uMsg,
+ WPARAM wParam,
+ LPARAM lParam) {
+ NativeEventLogger eventLogger("AppShell", hwnd, uMsg, wParam, lParam);
+
+ if (uMsg == sAppShellGeckoMsgId) {
+ // The app shell might have been destroyed between this message being
+ // posted and being executed, so be extra careful.
+ if (!sOutstandingNativeEventCallbacks) {
+ return TRUE;
+ }
+
+ nsAppShell* as = reinterpret_cast<nsAppShell*>(lParam);
+ as->NativeEventCallback();
+ --sOutstandingNativeEventCallbacks;
+ return TRUE;
+ }
+
+ LRESULT ret = DefWindowProc(hwnd, uMsg, wParam, lParam);
+ eventLogger.SetResult(ret, false);
+ return ret;
+}
+
+nsAppShell::~nsAppShell() {
+ hal::Shutdown();
+
+ if (mEventWnd) {
+ // DestroyWindow doesn't do anything when called from a non UI thread.
+ // Since mEventWnd was created on the UI thread, it must be destroyed on
+ // the UI thread.
+ SendMessage(mEventWnd, WM_CLOSE, 0, 0);
+ }
+
+ // Cancel any outstanding native event callbacks.
+ sOutstandingNativeEventCallbacks = 0;
+}
+
+NS_IMETHODIMP
+nsAppShell::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsIObserverService> obsServ(
+ mozilla::services::GetObserverService());
+
+ if (!strcmp(aTopic, "sessionstore-restoring-on-startup")) {
+ nsWindow::SetIsRestoringSession(true);
+ // Now that we've handled the observer notification, we can remove it
+ obsServ->RemoveObserver(this, "sessionstore-restoring-on-startup");
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, "sessionstore-windows-restored")) {
+ nsWindow::SetIsRestoringSession(false);
+ // Now that we've handled the observer notification, we can remove it
+ obsServ->RemoveObserver(this, "sessionstore-windows-restored");
+ return NS_OK;
+ }
+ }
+
+ return nsBaseAppShell::Observe(aSubject, aTopic, aData);
+}
+
+namespace {
+
+// Struct storing the visible, loggable error-state of a Windows thread.
+// Approximately `std:pair(::GetLastError(), ::RtlGetLastNtStatus())`.
+//
+// Uses sentinel values rather than a proper `Maybe` type to simplify
+// minidump-analysis.
+struct WinErrorState {
+ // Last error, as provided by ::GetLastError().
+ DWORD error = ~0;
+ // Last NTSTATUS, as provided by the TIB.
+ NTSTATUS ntStatus = ~0;
+
+ private:
+ // per WINE et al.; stable since NT 3.51
+ constexpr static size_t kLastNtStatusOffset =
+ sizeof(size_t) == 8 ? 0x1250 : 0xbf4;
+
+ static void SetLastNtStatus(NTSTATUS status) {
+ auto* teb = ::NtCurrentTeb();
+ *reinterpret_cast<NTSTATUS*>(reinterpret_cast<char*>(teb) +
+ kLastNtStatusOffset) = status;
+ }
+
+ static NTSTATUS GetLastNtStatus() {
+ auto const* teb = ::NtCurrentTeb();
+ return *reinterpret_cast<NTSTATUS const*>(
+ reinterpret_cast<char const*>(teb) + kLastNtStatusOffset);
+ }
+
+ public:
+ // Restore (or just set) the error state of the current thread.
+ static void Apply(WinErrorState const& state) {
+ SetLastNtStatus(state.ntStatus);
+ ::SetLastError(state.error);
+ }
+
+ // Clear the error-state of the current thread.
+ static void Clear() { Apply({.error = 0, .ntStatus = 0}); }
+
+ // Get the error-state of the current thread.
+ static WinErrorState Get() {
+ return WinErrorState{
+ .error = ::GetLastError(),
+ .ntStatus = GetLastNtStatus(),
+ };
+ }
+
+ bool operator==(WinErrorState const& that) const {
+ return this->error == that.error && this->ntStatus == that.ntStatus;
+ }
+
+ bool operator!=(WinErrorState const& that) const { return !operator==(that); }
+};
+
+// Struct containing information about the user atom table. (See
+// DiagnoseUserAtomTable(), below.)
+struct AtomTableInformation {
+ // Number of atoms in use. (Exactly 0x4000 == 16384, if all are.)
+ UINT in_use = 0;
+ // Number of atoms confirmed not in use.
+ UINT free = 0;
+ // Number of atoms which gave errors when checked.
+ UINT errors = 0;
+
+ // Last atom which gave an unexpected error...
+ UINT lastErrorAtom = ~0u;
+ // ... and the error it gave.
+ WinErrorState lastErrorState;
+};
+
+// Return a summary of the state of the atom table.
+MOZ_NEVER_INLINE static AtomTableInformation DiagnoseUserAtomTable() {
+ // Restore error state on exit, for the sake of automated minidump analyses.
+ auto const _restoreErrState =
+ mozilla::MakeScopeExit([oldErrState = WinErrorState::Get()]() {
+ WinErrorState::Apply(oldErrState);
+ });
+
+ AtomTableInformation retval;
+
+ // Expected error-state on failure-return when the atom is assigned, but not
+ // enough space was provided for the full string.
+ constexpr WinErrorState kBufferTooSmall = {
+ .error = ERROR_INSUFFICIENT_BUFFER,
+ .ntStatus = ((NTSTATUS)0xC0000023), // == STATUS_BUFFER_TOO_SMALL
+ };
+ // Expected error-state on failure-return when the atom is not assigned.
+ constexpr WinErrorState kInvalidAtom = {
+ .error = ERROR_INVALID_HANDLE,
+ .ntStatus = ((NTSTATUS)STATUS_INVALID_HANDLE),
+ };
+
+ // Iterate over only the dynamic portion of the atom table.
+ for (UINT atom = 0xC000; atom <= 0xFFFF; ++atom) {
+ // The actual atom values are PII. Don't acquire them in their entirety, and
+ // don't keep more information about them than is needed.
+ WCHAR buf[2] = {};
+ // USE OF UNDOCUMENTED BEHAVIOR: The user atom table is shared by message
+ // names, window-class names, and clipboard-format names. Only the last has
+ // a documented getter-mechanism.
+ BOOL const ok = ::GetClipboardFormatNameW(atom, buf, 1);
+ WinErrorState const errState = WinErrorState::Get();
+ if (ok || errState == kBufferTooSmall) {
+ ++retval.in_use;
+ } else if (errState == kInvalidAtom) {
+ ++retval.free;
+ } else {
+ // Unexpected error-state.
+ ++retval.errors;
+ retval.lastErrorAtom = atom;
+ retval.lastErrorState = errState;
+ }
+ }
+
+ return retval;
+}
+
+} // namespace
+
+#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && defined(_M_X64)
+MOZ_NEVER_INLINE MOZ_NAKED void EnableTrapFlag() {
+ asm volatile(
+ "pushfq;"
+ "orw $0x100,(%rsp);"
+ "popfq;"
+ "retq;");
+}
+
+MOZ_NEVER_INLINE MOZ_NAKED void DisableTrapFlag() { asm volatile("retq;"); }
+
+# define SSD_MAX_USER32_STEPS 0x1800
+# define SSD_MAX_ERROR_STATES 0x200
+struct SingleStepData {
+ uint32_t mUser32StepsLog[SSD_MAX_USER32_STEPS]{};
+ WinErrorState mErrorStatesLog[SSD_MAX_ERROR_STATES];
+ uint16_t mUser32StepsAtErrorState[SSD_MAX_ERROR_STATES]{};
+};
+
+struct SingleStepStaticState {
+ SingleStepData* mData{};
+ uintptr_t mUser32Start{};
+ uintptr_t mUser32End{};
+ uint32_t mUser32Steps{};
+ uint32_t mErrorStates{};
+ WinErrorState mLastRecordedErrorState;
+
+ constexpr void Reset() { *this = SingleStepStaticState{}; }
+};
+
+static SingleStepStaticState sSingleStepStaticState{};
+
+LONG SingleStepExceptionHandler(_EXCEPTION_POINTERS* aExceptionInfo) {
+ auto& state = sSingleStepStaticState;
+ if (state.mData &&
+ aExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
+ auto instructionPointer = aExceptionInfo->ContextRecord->Rip;
+ if (instructionPointer == reinterpret_cast<uintptr_t>(&DisableTrapFlag)) {
+ // Stop handling any exception in this handler
+ state.mData = nullptr;
+ } else {
+ // Record data for the current step, if in user32
+ if (state.mUser32Start <= instructionPointer &&
+ instructionPointer < state.mUser32End) {
+ // We record the instruction pointer
+ if (state.mUser32Steps < SSD_MAX_USER32_STEPS) {
+ state.mData->mUser32StepsLog[state.mUser32Steps] =
+ static_cast<uint32_t>(instructionPointer - state.mUser32Start);
+ }
+
+ // We record changes in the error state
+ auto currentErrorState{WinErrorState::Get()};
+ if (currentErrorState != state.mLastRecordedErrorState) {
+ state.mLastRecordedErrorState = currentErrorState;
+
+ if (state.mErrorStates < SSD_MAX_ERROR_STATES) {
+ state.mData->mErrorStatesLog[state.mErrorStates] =
+ currentErrorState;
+ state.mData->mUser32StepsAtErrorState[state.mErrorStates] =
+ state.mUser32Steps;
+ }
+
+ ++state.mErrorStates;
+ }
+
+ ++state.mUser32Steps;
+ }
+
+ // Continue single-stepping
+ aExceptionInfo->ContextRecord->EFlags |= 0x100;
+ }
+ return EXCEPTION_CONTINUE_EXECUTION;
+ }
+ return EXCEPTION_CONTINUE_SEARCH;
+}
+
+enum CSSD_RESULT {
+ CSSD_SUCCESS = 0,
+ CSSD_ERROR_DEBUGGER_PRESENT = 1,
+ CSSD_ERROR_GET_MODULE_HANDLE = 2,
+ CSSD_ERROR_PARSING_USER32 = 3,
+ CSSD_ERROR_ADD_VECTORED_EXCEPTION_HANDLER = 4,
+};
+
+template <typename CallbackToRun, typename PostCollectionCallback>
+[[clang::optnone]] MOZ_NEVER_INLINE CSSD_RESULT
+CollectSingleStepData(CallbackToRun aCallbackToRun,
+ PostCollectionCallback aPostCollectionCallback) {
+ if (::IsDebuggerPresent()) {
+ return CSSD_ERROR_DEBUGGER_PRESENT;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(!sSingleStepStaticState.mData,
+ "Single-stepping is already active");
+ HANDLE user32 = ::GetModuleHandleW(L"user32.dll");
+ if (!user32) {
+ return CSSD_ERROR_GET_MODULE_HANDLE;
+ }
+
+ nt::PEHeaders user32Headers{user32};
+ auto bounds = user32Headers.GetBounds();
+ if (bounds.isNothing()) {
+ return CSSD_ERROR_PARSING_USER32;
+ }
+
+ SingleStepData singleStepData{};
+
+ sSingleStepStaticState.Reset();
+ sSingleStepStaticState.mUser32Start =
+ reinterpret_cast<uintptr_t>(bounds.ref().begin().get());
+ sSingleStepStaticState.mUser32End =
+ reinterpret_cast<uintptr_t>(bounds.ref().end().get());
+ sSingleStepStaticState.mData = &singleStepData;
+ auto veh = ::AddVectoredExceptionHandler(TRUE, SingleStepExceptionHandler);
+ if (!veh) {
+ sSingleStepStaticState.mData = nullptr;
+ return CSSD_ERROR_ADD_VECTORED_EXCEPTION_HANDLER;
+ }
+
+ EnableTrapFlag();
+ aCallbackToRun();
+ DisableTrapFlag();
+ ::RemoveVectoredExceptionHandler(veh);
+ sSingleStepStaticState.mData = nullptr;
+
+ aPostCollectionCallback();
+
+ return CSSD_SUCCESS;
+}
+#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED && _M_X64
+
+// Collect data for bug 1571516. We don't automatically send up `GetLastError`
+// or `GetLastNtStatus` data for beta/release builds, so extract the relevant
+// error values and store them on the stack, where they can be viewed in
+// minidumps -- in fact, do so after each individual API call. This takes the
+// form of various local variables whose initial character is an underscore,
+// most of which are also marked [[maybe_unused]].
+//
+// We tag this function `[[clang::optnone]]` to prevent the compiler from
+// eliding those values as _actually_ unused, as well as to generally simplify
+// the haruspex's task once the minidumps are in. (As this function should be
+// called at most once per process, the minor performance hit is not a concern.)
+//
+[[clang::optnone]] MOZ_NEVER_INLINE nsresult nsAppShell::InitHiddenWindow() {
+ // note the incoming error-state; this may be relevant to errors we get later
+ auto _initialErr [[maybe_unused]] = WinErrorState::Get();
+ // reset the error-state, to avoid ambiguity below
+ WinErrorState::Clear();
+
+ // Diagnostic variable. Only collected in the event of a failure in one of the
+ // functions that attempts to register an atom.
+ AtomTableInformation _atomTableInfo [[maybe_unused]];
+
+ // Attempt to register the window message. On failure, retain the initial
+ // value of `sAppShellGeckoMsgId`.
+ auto const _msgId = ::RegisterWindowMessageW(kAppShellGeckoEventId);
+ if (_msgId) {
+ sAppShellGeckoMsgId = _msgId;
+ }
+ auto const _sAppShellGeckoMsgId [[maybe_unused]] = sAppShellGeckoMsgId;
+ auto const _rwmErr [[maybe_unused]] = WinErrorState::Get();
+ if (!_msgId) _atomTableInfo = DiagnoseUserAtomTable();
+ NS_ASSERTION(sAppShellGeckoMsgId,
+ "Could not register hidden window event message!");
+
+ mLastNativeEventScheduled = TimeStamp::NowLoRes();
+
+ WNDCLASSW wc;
+ HINSTANCE const module = GetModuleHandle(nullptr);
+
+ constexpr const wchar_t* kWindowClass = L"nsAppShell:EventWindowClass";
+ // (Undocumented behavior note: on success, this will specifically be the
+ // window-class atom. We don't rely on this.)
+ BOOL const _gciwRet = ::GetClassInfoW(module, kWindowClass, &wc);
+ auto const _gciwErr [[maybe_unused]] = WinErrorState::Get();
+ WinErrorState::Clear();
+
+ WinErrorState _rcErr [[maybe_unused]];
+ if (!_gciwRet) {
+ wc.style = 0;
+ wc.lpfnWndProc = EventWindowProc;
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hInstance = module;
+ wc.hIcon = nullptr;
+ wc.hCursor = nullptr;
+ wc.hbrBackground = (HBRUSH) nullptr;
+ wc.lpszMenuName = (LPCWSTR) nullptr;
+ wc.lpszClassName = kWindowClass;
+
+ ATOM _windowClassAtom = ::RegisterClassW(&wc);
+ _rcErr = WinErrorState::Get();
+
+ if (!_windowClassAtom) _atomTableInfo = DiagnoseUserAtomTable();
+
+#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && defined(_M_X64)
+ if (!_windowClassAtom) {
+ // Retry with single-step data collection
+ auto cssdResult = CollectSingleStepData(
+ [&wc, &_windowClassAtom]() {
+ _windowClassAtom = ::RegisterClassW(&wc);
+ },
+ [&_windowClassAtom]() {
+ // Crashing here gives access to the single step data on stack
+ MOZ_DIAGNOSTIC_ASSERT(
+ _windowClassAtom,
+ "RegisterClassW for EventWindowClass failed twice");
+ });
+ auto const _cssdErr [[maybe_unused]] = WinErrorState::Get();
+ MOZ_DIAGNOSTIC_ASSERT(
+ cssdResult == CSSD_SUCCESS,
+ "Failed to collect single step data for RegisterClassW");
+ // If we reach this point then somehow the single-stepped call succeeded
+ // and we can proceed
+ }
+#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED && _M_X64
+
+ MOZ_DIAGNOSTIC_ASSERT(_windowClassAtom,
+ "RegisterClassW for EventWindowClass failed");
+ WinErrorState::Clear();
+ }
+
+ mEventWnd = CreateWindowW(kWindowClass, L"nsAppShell:EventWindow", 0, 0, 0,
+ 10, 10, HWND_MESSAGE, nullptr, module, nullptr);
+ auto const _cwErr [[maybe_unused]] = WinErrorState::Get();
+
+#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && defined(_M_X64)
+ if (!mEventWnd) {
+ // Retry with single-step data collection
+ HWND eventWnd{};
+ auto cssdResult = CollectSingleStepData(
+ [module, &eventWnd]() {
+ eventWnd =
+ CreateWindowW(kWindowClass, L"nsAppShell:EventWindow", 0, 0, 0,
+ 10, 10, HWND_MESSAGE, nullptr, module, nullptr);
+ },
+ [&eventWnd]() {
+ // Crashing here gives access to the single step data on stack
+ MOZ_DIAGNOSTIC_ASSERT(eventWnd,
+ "CreateWindowW for EventWindow failed twice");
+ });
+ auto const _cssdErr [[maybe_unused]] = WinErrorState::Get();
+ MOZ_DIAGNOSTIC_ASSERT(
+ cssdResult == CSSD_SUCCESS,
+ "Failed to collect single step data for CreateWindowW");
+ // If we reach this point then somehow the single-stepped call succeeded and
+ // we can proceed
+ mEventWnd = eventWnd;
+ }
+#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED && _M_X64
+
+ MOZ_DIAGNOSTIC_ASSERT(mEventWnd, "CreateWindowW for EventWindow failed");
+ NS_ENSURE_STATE(mEventWnd);
+
+ return NS_OK;
+}
+
+nsresult nsAppShell::Init() {
+ LSPAnnotate();
+
+ hal::Init();
+
+ if (XRE_IsParentProcess()) {
+ sTaskbarButtonCreatedMsg = ::RegisterWindowMessageW(kTaskbarButtonEventId);
+ NS_ASSERTION(sTaskbarButtonCreatedMsg,
+ "Could not register taskbar button creation message");
+ }
+
+ // The hidden message window is used for interrupting the processing of native
+ // events, so that we can process gecko events. Therefore, we only need it if
+ // we are processing native events. Disabling this is required for win32k
+ // syscall lockdown.
+ if (XRE_UseNativeEventProcessing()) {
+ if (nsresult rv = this->InitHiddenWindow(); NS_FAILED(rv)) {
+ return rv;
+ }
+ } else if (XRE_IsContentProcess() && !IsWin32kLockedDown()) {
+ // We're not generally processing native events, but still using GDI and we
+ // still have some internal windows, e.g. from calling CoInitializeEx.
+ // So we use a class that will do a single event pump where previously we
+ // might have processed multiple events to make sure any occasional messages
+ // to these windows are processed. This also allows any internal Windows
+ // messages to be processed to ensure the GDI data remains fresh.
+ nsCOMPtr<nsIThreadInternal> threadInt =
+ do_QueryInterface(NS_GetCurrentThread());
+ if (threadInt) {
+ threadInt->SetObserver(new SingleNativeEventPump());
+ }
+ }
+
+ if (XRE_IsParentProcess()) {
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+ if (gfxPlatform::IsHeadless()) {
+ screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>());
+ } else {
+ screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperWin>());
+ ScreenHelperWin::RefreshScreens();
+ }
+
+ nsCOMPtr<nsIObserverService> obsServ(
+ mozilla::services::GetObserverService());
+
+ obsServ->AddObserver(this, "sessionstore-restoring-on-startup", false);
+ obsServ->AddObserver(this, "sessionstore-windows-restored", false);
+ }
+
+ if (!WinUtils::GetTimezoneName(mTimezoneName)) {
+ NS_WARNING("Unable to get system timezone name, timezone may be invalid\n");
+ }
+
+ return nsBaseAppShell::Init();
+}
+
+NS_IMETHODIMP
+nsAppShell::Run(void) {
+ bool wantAudio = true;
+ if (XRE_IsParentProcess()) {
+#ifdef MOZ_BACKGROUNDTASKS
+ if (BackgroundTasks::IsBackgroundTaskMode()) {
+ wantAudio = false;
+ }
+#endif
+ if (MOZ_LIKELY(wantAudio)) {
+ mozilla::widget::StartAudioSession();
+ }
+
+ // Add an observer that disables the screen saver when requested by Gecko.
+ // For example when we're playing video in the foreground tab. Whole firefox
+ // only needs one wakelock instance, so we would only create one listener in
+ // chrome process to prevent requesting unnecessary wakelock.
+ AddScreenWakeLockListener();
+ }
+
+ nsresult rv = nsBaseAppShell::Run();
+
+ if (XRE_IsParentProcess()) {
+ RemoveScreenWakeLockListener();
+
+ if (MOZ_LIKELY(wantAudio)) {
+ mozilla::widget::StopAudioSession();
+ }
+ }
+
+ return rv;
+}
+
+void nsAppShell::DoProcessMoreGeckoEvents() {
+ // Called by nsBaseAppShell's NativeEventCallback() after it has finished
+ // processing pending gecko events and there are still gecko events pending
+ // for the thread. (This can happen if NS_ProcessPendingEvents reached it's
+ // starvation timeout limit.) The default behavior in nsBaseAppShell is to
+ // call ScheduleNativeEventCallback to post a follow up native event callback
+ // message. This triggers an additional call to NativeEventCallback for more
+ // gecko event processing.
+
+ // There's a deadlock risk here with certain internal Windows modal loops. In
+ // our dispatch code, we prioritize messages so that input is handled first.
+ // However Windows modal dispatch loops often prioritize posted messages. If
+ // we find ourselves in a tight gecko timer loop where NS_ProcessPendingEvents
+ // takes longer than the timer duration, NS_HasPendingEvents(thread) will
+ // always be true. ScheduleNativeEventCallback will be called on every
+ // NativeEventCallback callback, and in a Windows modal dispatch loop, the
+ // callback message will be processed first -> input gets starved, dead lock.
+
+ // To avoid, don't post native callback messages from NativeEventCallback
+ // when we're in a modal loop. This gets us back into the Windows modal
+ // dispatch loop dispatching input messages. Once we drop out of the modal
+ // loop, we use mNativeCallbackPending to fire off a final NativeEventCallback
+ // if we need it, which insures NS_ProcessPendingEvents gets called and all
+ // gecko events get processed.
+ if (mEventloopNestingLevel < 2) {
+ OnDispatchedEvent();
+ mNativeCallbackPending = false;
+ } else {
+ mNativeCallbackPending = true;
+ }
+}
+
+void nsAppShell::ScheduleNativeEventCallback() {
+ MOZ_ASSERT(mEventWnd,
+ "We should have created mEventWnd in Init, if this is called.");
+
+ // Post a message to the hidden message window
+ ++sOutstandingNativeEventCallbacks;
+ {
+ MutexAutoLock lock(mLastNativeEventScheduledMutex);
+ // Time stamp this event so we can detect cases where the event gets
+ // dropping in sub classes / modal loops we do not control.
+ mLastNativeEventScheduled = TimeStamp::NowLoRes();
+ }
+ ::PostMessage(mEventWnd, sAppShellGeckoMsgId, 0,
+ reinterpret_cast<LPARAM>(this));
+}
+
+bool nsAppShell::ProcessNextNativeEvent(bool mayWait) {
+ // Notify ipc we are spinning a (possibly nested) gecko event loop.
+ mozilla::ipc::MessageChannel::NotifyGeckoEventDispatch();
+
+ bool gotMessage = false;
+
+ do {
+ MSG msg;
+
+ // For avoiding deadlock between our process and plugin process by
+ // mouse wheel messages, we're handling actually when we receive one of
+ // following internal messages which is posted by native mouse wheel
+ // message handler. Any other events, especially native modifier key
+ // events, should not be handled between native message and posted
+ // internal message because it may make different modifier key state or
+ // mouse cursor position between them.
+ if (mozilla::widget::MouseScrollHandler::IsWaitingInternalMessage()) {
+ gotMessage = WinUtils::PeekMessage(&msg, nullptr, MOZ_WM_MOUSEWHEEL_FIRST,
+ MOZ_WM_MOUSEWHEEL_LAST, PM_REMOVE);
+ NS_ASSERTION(gotMessage,
+ "waiting internal wheel message, but it has not come");
+ }
+
+ if (!gotMessage) {
+ gotMessage = WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE);
+ }
+
+ if (gotMessage) {
+ if (msg.message == WM_QUIT) {
+ ::PostQuitMessage(msg.wParam);
+ Exit();
+ } else {
+ // If we had UI activity we would be processing it now so we know we
+ // have either kUIActivity or kActivityNoUIAVail.
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+
+ if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST &&
+ IMEHandler::ProcessRawKeyMessage(msg)) {
+ continue; // the message is consumed.
+ }
+
+#if defined(_X86_)
+ // Store Printer dialog messages for reposting on x86, because on x86
+ // Windows 7 they are not processed by a window procedure, but are
+ // explicitly waited for in the winspool.drv code that will be further
+ // up the stack (winspool!WaitForCompletionMessage). These are
+ // undocumented Windows Message identifiers found in winspool.drv.
+ if (msg.message == 0x5b7a || msg.message == 0x5b7f ||
+ msg.message == 0x5b80 || msg.message == 0x5b81) {
+ mMsgsToRepost.push_back(msg);
+ continue;
+ }
+#endif
+
+ // Windows documentation suggets that WM_SETTINGSCHANGE is the message
+ // to watch for timezone changes, but experimentation showed that it
+ // doesn't fire on changing the timezone, but that WM_TIMECHANGE does,
+ // even if there's no immediate effect on the clock (e.g., changing
+ // from Pacific Daylight at UTC-7 to Arizona at UTC-7).
+ if (msg.message == WM_TIMECHANGE) {
+ // The message may not give us sufficient information to determine
+ // if the timezone changed, so keep track of it ourselves.
+ wchar_t systemTimezone[128];
+ bool getSystemTimeSucceeded =
+ WinUtils::GetTimezoneName(systemTimezone);
+ if (getSystemTimeSucceeded && wcscmp(systemTimezone, mTimezoneName)) {
+ nsBaseAppShell::OnSystemTimezoneChange();
+
+ wcscpy_s(mTimezoneName, 128, systemTimezone);
+ }
+ }
+
+ ::TranslateMessage(&msg);
+ ::DispatchMessageW(&msg);
+ }
+ } else if (mayWait) {
+ // Block and wait for any posted application message
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ {
+ AUTO_PROFILER_LABEL("nsAppShell::ProcessNextNativeEvent::Wait", IDLE);
+ WinUtils::WaitForMessage();
+ }
+ }
+ } while (!gotMessage && mayWait);
+
+ // See DoProcessNextNativeEvent, mEventloopNestingLevel will be
+ // one when a modal loop unwinds.
+ if (mNativeCallbackPending && mEventloopNestingLevel == 1)
+ DoProcessMoreGeckoEvents();
+
+ // Check for starved native callbacks. If we haven't processed one
+ // of these events in NATIVE_EVENT_STARVATION_LIMIT, fire one off.
+ static const mozilla::TimeDuration nativeEventStarvationLimit =
+ mozilla::TimeDuration::FromSeconds(NATIVE_EVENT_STARVATION_LIMIT);
+
+ TimeDuration timeSinceLastNativeEventScheduled;
+ {
+ MutexAutoLock lock(mLastNativeEventScheduledMutex);
+ timeSinceLastNativeEventScheduled =
+ TimeStamp::NowLoRes() - mLastNativeEventScheduled;
+ }
+ if (timeSinceLastNativeEventScheduled > nativeEventStarvationLimit) {
+ ScheduleNativeEventCallback();
+ }
+
+ return gotMessage;
+}
+
+nsresult nsAppShell::AfterProcessNextEvent(nsIThreadInternal* /* unused */,
+ bool /* unused */) {
+ if (!mMsgsToRepost.empty()) {
+ for (MSG msg : mMsgsToRepost) {
+ ::PostMessageW(msg.hwnd, msg.message, msg.wParam, msg.lParam);
+ }
+ mMsgsToRepost.clear();
+ }
+ return NS_OK;
+}
diff --git a/widget/windows/nsAppShell.h b/widget/windows/nsAppShell.h
new file mode 100644
index 0000000000..20ea65e834
--- /dev/null
+++ b/widget/windows/nsAppShell.h
@@ -0,0 +1,64 @@
+/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+#ifndef nsAppShell_h__
+#define nsAppShell_h__
+
+#include "nsBaseAppShell.h"
+#include <windows.h>
+#include <vector>
+#include "mozilla/TimeStamp.h"
+#include "mozilla/Mutex.h"
+
+// The maximum time we allow before forcing a native event callback.
+// In seconds.
+#define NATIVE_EVENT_STARVATION_LIMIT 1
+
+/**
+ * Native Win32 Application shell wrapper
+ */
+class nsAppShell final : public nsBaseAppShell {
+ public:
+ nsAppShell()
+ : mEventWnd(nullptr),
+ mNativeCallbackPending(false),
+ mLastNativeEventScheduledMutex(
+ "nsAppShell::mLastNativeEventScheduledMutex") {}
+ typedef mozilla::TimeStamp TimeStamp;
+ typedef mozilla::Mutex Mutex;
+
+ nsresult Init();
+ void DoProcessMoreGeckoEvents();
+
+ static UINT GetTaskbarButtonCreatedMessage();
+
+ NS_IMETHOD AfterProcessNextEvent(nsIThreadInternal* thread,
+ bool eventWasProcessed) final;
+
+ protected:
+ NS_IMETHOD Run() override;
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override;
+
+ virtual void ScheduleNativeEventCallback();
+ virtual bool ProcessNextNativeEvent(bool mayWait);
+ virtual ~nsAppShell();
+
+ static LRESULT CALLBACK EventWindowProc(HWND, UINT, WPARAM, LPARAM);
+
+ protected:
+ nsresult InitHiddenWindow();
+ HWND mEventWnd;
+ bool mNativeCallbackPending;
+
+ Mutex mLastNativeEventScheduledMutex MOZ_UNANNOTATED;
+ TimeStamp mLastNativeEventScheduled;
+ std::vector<MSG> mMsgsToRepost;
+
+ private:
+ wchar_t mTimezoneName[128];
+};
+
+#endif // nsAppShell_h__
diff --git a/widget/windows/nsBidiKeyboard.cpp b/widget/windows/nsBidiKeyboard.cpp
new file mode 100644
index 0000000000..87d81d458e
--- /dev/null
+++ b/widget/windows/nsBidiKeyboard.cpp
@@ -0,0 +1,169 @@
+/* -*- 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 <stdio.h>
+#include "nsBidiKeyboard.h"
+#include "WidgetUtils.h"
+#include "nsIWidget.h"
+#include <tchar.h>
+
+NS_IMPL_ISUPPORTS(nsBidiKeyboard, nsIBidiKeyboard)
+
+nsBidiKeyboard::nsBidiKeyboard() : nsIBidiKeyboard() { Reset(); }
+
+nsBidiKeyboard::~nsBidiKeyboard() {}
+
+NS_IMETHODIMP nsBidiKeyboard::Reset() {
+ mInitialized = false;
+ mHaveBidiKeyboards = false;
+ mLTRKeyboard[0] = '\0';
+ mRTLKeyboard[0] = '\0';
+ mCurrentLocaleName[0] = '\0';
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBidiKeyboard::IsLangRTL(bool* aIsRTL) {
+ *aIsRTL = false;
+
+ nsresult result = SetupBidiKeyboards();
+ if (NS_FAILED(result)) return result;
+
+ HKL currentLocale;
+
+ currentLocale = ::GetKeyboardLayout(0);
+ *aIsRTL = IsRTLLanguage(currentLocale);
+
+ if (!::GetKeyboardLayoutNameW(mCurrentLocaleName)) return NS_ERROR_FAILURE;
+
+ NS_ASSERTION(*mCurrentLocaleName,
+ "GetKeyboardLayoutName return string length == 0");
+ NS_ASSERTION((wcslen(mCurrentLocaleName) < KL_NAMELENGTH),
+ "GetKeyboardLayoutName return string length >= KL_NAMELENGTH");
+
+ // The language set by the user overrides the default language for that
+ // direction
+ if (*aIsRTL) {
+ wcsncpy(mRTLKeyboard, mCurrentLocaleName, KL_NAMELENGTH);
+ mRTLKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate
+ } else {
+ wcsncpy(mLTRKeyboard, mCurrentLocaleName, KL_NAMELENGTH);
+ mLTRKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate
+ }
+
+ NS_ASSERTION((wcslen(mRTLKeyboard) < KL_NAMELENGTH),
+ "mLTRKeyboard has string length >= KL_NAMELENGTH");
+ NS_ASSERTION((wcslen(mLTRKeyboard) < KL_NAMELENGTH),
+ "mRTLKeyboard has string length >= KL_NAMELENGTH");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBidiKeyboard::GetHaveBidiKeyboards(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsresult result = SetupBidiKeyboards();
+ if (NS_FAILED(result)) return result;
+
+ *aResult = mHaveBidiKeyboards;
+ return NS_OK;
+}
+
+// Get the list of keyboard layouts available in the system
+// Set mLTRKeyboard to the first LTR keyboard in the list and mRTLKeyboard to
+// the first RTL keyboard in the list These defaults will be used unless the
+// user explicitly sets something else.
+nsresult nsBidiKeyboard::SetupBidiKeyboards() {
+ if (mInitialized) return mHaveBidiKeyboards ? NS_OK : NS_ERROR_FAILURE;
+
+ int keyboards;
+ HKL far* buf;
+ HKL locale;
+ wchar_t localeName[KL_NAMELENGTH];
+ bool isLTRKeyboardSet = false;
+ bool isRTLKeyboardSet = false;
+
+ // GetKeyboardLayoutList with 0 as first parameter returns the number of
+ // keyboard layouts available
+ keyboards = ::GetKeyboardLayoutList(0, nullptr);
+ if (!keyboards) return NS_ERROR_FAILURE;
+
+ // allocate a buffer to hold the list
+ buf = (HKL far*)malloc(keyboards * sizeof(HKL));
+ if (!buf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // Call again to fill the buffer
+ if (::GetKeyboardLayoutList(keyboards, buf) != keyboards) {
+ free(buf);
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Go through the list and pick a default LTR and RTL keyboard layout
+ while (keyboards--) {
+ locale = buf[keyboards];
+ if (IsRTLLanguage(locale)) {
+ _snwprintf(mRTLKeyboard, KL_NAMELENGTH, L"%.*x", KL_NAMELENGTH - 1,
+ LANGIDFROMLCID((DWORD_PTR)locale));
+ isRTLKeyboardSet = true;
+ } else {
+ _snwprintf(mLTRKeyboard, KL_NAMELENGTH, L"%.*x", KL_NAMELENGTH - 1,
+ LANGIDFROMLCID((DWORD_PTR)locale));
+ isLTRKeyboardSet = true;
+ }
+ }
+ free(buf);
+ mInitialized = true;
+
+ // If there is not at least one keyboard of each directionality, Bidi
+ // keyboard functionality will be disabled.
+ mHaveBidiKeyboards = (isRTLKeyboardSet && isLTRKeyboardSet);
+ if (!mHaveBidiKeyboards) return NS_ERROR_FAILURE;
+
+ // Get the current keyboard layout and use it for either mRTLKeyboard or
+ // mLTRKeyboard as appropriate. If the user has many keyboard layouts
+ // installed this prevents us from arbitrarily resetting the current
+ // layout (bug 80274)
+ locale = ::GetKeyboardLayout(0);
+ if (!::GetKeyboardLayoutNameW(localeName)) return NS_ERROR_FAILURE;
+
+ NS_ASSERTION(*localeName, "GetKeyboardLayoutName return string length == 0");
+ NS_ASSERTION((wcslen(localeName) < KL_NAMELENGTH),
+ "GetKeyboardLayout return string length >= KL_NAMELENGTH");
+
+ if (IsRTLLanguage(locale)) {
+ wcsncpy(mRTLKeyboard, localeName, KL_NAMELENGTH);
+ mRTLKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate
+ } else {
+ wcsncpy(mLTRKeyboard, localeName, KL_NAMELENGTH);
+ mLTRKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate
+ }
+
+ NS_ASSERTION(*mRTLKeyboard, "mLTRKeyboard has string length == 0");
+ NS_ASSERTION(*mLTRKeyboard, "mLTRKeyboard has string length == 0");
+
+ return NS_OK;
+}
+
+// Test whether the language represented by this locale identifier is a
+// right-to-left language, using bit 123 of the Unicode subset bitfield in
+// the LOCALESIGNATURE
+// See
+// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/intl/unicode_63ub.asp
+bool nsBidiKeyboard::IsRTLLanguage(HKL aLocale) {
+ LOCALESIGNATURE localesig;
+ return (::GetLocaleInfoW(PRIMARYLANGID((DWORD_PTR)aLocale),
+ LOCALE_FONTSIGNATURE, (LPWSTR)&localesig,
+ (sizeof(localesig) / sizeof(WCHAR))) &&
+ (localesig.lsUsb[3] & 0x08000000));
+}
+
+// static
+void nsBidiKeyboard::OnLayoutChange() {
+ mozilla::widget::WidgetUtils::SendBidiKeyboardInfoToContent();
+}
+
+// static
+already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() {
+ return do_AddRef(new nsBidiKeyboard());
+}
diff --git a/widget/windows/nsBidiKeyboard.h b/widget/windows/nsBidiKeyboard.h
new file mode 100644
index 0000000000..584d4d2dee
--- /dev/null
+++ b/widget/windows/nsBidiKeyboard.h
@@ -0,0 +1,34 @@
+/* -*- 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/. */
+
+#ifndef __nsBidiKeyboard
+#define __nsBidiKeyboard
+#include "nsIBidiKeyboard.h"
+#include <windows.h>
+
+class nsBidiKeyboard : public nsIBidiKeyboard {
+ virtual ~nsBidiKeyboard();
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIBIDIKEYBOARD
+
+ nsBidiKeyboard();
+
+ static void OnLayoutChange();
+
+ protected:
+ nsresult SetupBidiKeyboards();
+ bool IsRTLLanguage(HKL aLocale);
+
+ bool mInitialized;
+ bool mHaveBidiKeyboards;
+ wchar_t mLTRKeyboard[KL_NAMELENGTH];
+ wchar_t mRTLKeyboard[KL_NAMELENGTH];
+ wchar_t mCurrentLocaleName[KL_NAMELENGTH];
+};
+
+#endif // __nsBidiKeyboard
diff --git a/widget/windows/nsClipboard.cpp b/widget/windows/nsClipboard.cpp
new file mode 100644
index 0000000000..8783affd1e
--- /dev/null
+++ b/widget/windows/nsClipboard.cpp
@@ -0,0 +1,1472 @@
+/* -*- 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 "nsClipboard.h"
+
+#include <shlobj.h>
+#include <intshcut.h>
+
+// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN
+#include <shellapi.h>
+
+#include <functional>
+#include <thread>
+#include <chrono>
+
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/Compatibility.h"
+#endif
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_clipboard.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/WindowsVersion.h"
+#include "SpecialSystemDirectory.h"
+
+#include "nsArrayUtils.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDataObj.h"
+#include "nsString.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIInputStream.h"
+#include "nsITransferable.h"
+#include "nsXPCOM.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsIWidget.h"
+#include "nsWidgetsCID.h"
+#include "nsCRT.h"
+#include "nsNetUtil.h"
+#include "nsIFileProtocolHandler.h"
+#include "nsEscape.h"
+#include "nsIObserverService.h"
+#include "nsMimeTypes.h"
+#include "imgITools.h"
+#include "imgIContainer.h"
+#include "WinUtils.h"
+
+/* static */
+UINT nsClipboard::GetClipboardFileDescriptorFormatA() {
+ static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORA);
+ MOZ_ASSERT(format);
+ return format;
+}
+
+/* static */
+UINT nsClipboard::GetClipboardFileDescriptorFormatW() {
+ static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW);
+ MOZ_ASSERT(format);
+ return format;
+}
+
+/* static */
+UINT nsClipboard::GetHtmlClipboardFormat() {
+ static UINT format = ::RegisterClipboardFormatW(L"HTML Format");
+ return format;
+}
+
+/* static */
+UINT nsClipboard::GetCustomClipboardFormat() {
+ static UINT format =
+ ::RegisterClipboardFormatW(L"application/x-moz-custom-clipdata");
+ return format;
+}
+
+//-------------------------------------------------------------------------
+//
+// nsClipboard constructor
+//
+//-------------------------------------------------------------------------
+nsClipboard::nsClipboard()
+ : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(
+ false /* supportsSelectionClipboard */,
+ false /* supportsFindClipboard */,
+ false /* supportsSelectionCache */)) {
+ mWindow = nullptr;
+
+ // Register for a shutdown notification so that we can flush data
+ // to the OS clipboard.
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService("@mozilla.org/observer-service;1");
+ if (observerService) {
+ observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
+ false);
+ }
+}
+
+//-------------------------------------------------------------------------
+// nsClipboard destructor
+//-------------------------------------------------------------------------
+nsClipboard::~nsClipboard() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsClipboard, nsBaseClipboard, nsIObserver)
+
+NS_IMETHODIMP
+nsClipboard::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ // This will be called on shutdown.
+ ::OleFlushClipboard();
+ ::CloseClipboard();
+
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+UINT nsClipboard::GetFormat(const char* aMimeStr, bool aMapHTMLMime) {
+ UINT format;
+
+ if (strcmp(aMimeStr, kTextMime) == 0) {
+ format = CF_UNICODETEXT;
+ } else if (strcmp(aMimeStr, kRTFMime) == 0) {
+ format = ::RegisterClipboardFormat(L"Rich Text Format");
+ } else if (strcmp(aMimeStr, kJPEGImageMime) == 0 ||
+ strcmp(aMimeStr, kJPGImageMime) == 0 ||
+ strcmp(aMimeStr, kPNGImageMime) == 0) {
+ format = CF_DIBV5;
+ } else if (strcmp(aMimeStr, kFileMime) == 0 ||
+ strcmp(aMimeStr, kFilePromiseMime) == 0) {
+ format = CF_HDROP;
+ } else if ((strcmp(aMimeStr, kNativeHTMLMime) == 0) ||
+ (aMapHTMLMime && strcmp(aMimeStr, kHTMLMime) == 0)) {
+ format = GetHtmlClipboardFormat();
+ } else if (strcmp(aMimeStr, kCustomTypesMime) == 0) {
+ format = GetCustomClipboardFormat();
+ } else {
+ format = ::RegisterClipboardFormatW(NS_ConvertASCIItoUTF16(aMimeStr).get());
+ }
+
+ return format;
+}
+
+//-------------------------------------------------------------------------
+// static
+nsresult nsClipboard::CreateNativeDataObject(
+ nsITransferable* aTransferable, IDataObject** aDataObj, nsIURI* aUri,
+ MightNeedToFlush* aMightNeedToFlush) {
+ MOZ_ASSERT(aTransferable);
+ if (!aTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create our native DataObject that implements the OLE IDataObject interface
+ RefPtr<nsDataObj> dataObj = new nsDataObj(aUri);
+
+ // Now set it up with all the right data flavors & enums
+ nsresult res =
+ SetupNativeDataObject(aTransferable, dataObj, aMightNeedToFlush);
+ if (NS_SUCCEEDED(res)) {
+ dataObj.forget(aDataObj);
+ }
+ return res;
+}
+
+static nsresult StoreValueInDataObject(nsDataObj* aObj,
+ LPCWSTR aClipboardFormat, DWORD value) {
+ HGLOBAL hGlobalMemory = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
+ if (!hGlobalMemory) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ DWORD* pdw = (DWORD*)::GlobalLock(hGlobalMemory);
+ *pdw = value;
+ ::GlobalUnlock(hGlobalMemory);
+
+ STGMEDIUM stg;
+ stg.tymed = TYMED_HGLOBAL;
+ stg.pUnkForRelease = nullptr;
+ stg.hGlobal = hGlobalMemory;
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, ::RegisterClipboardFormat(aClipboardFormat), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ aObj->SetData(&fe, &stg, TRUE);
+
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+nsresult nsClipboard::SetupNativeDataObject(
+ nsITransferable* aTransferable, IDataObject* aDataObj,
+ MightNeedToFlush* aMightNeedToFlush) {
+ MOZ_ASSERT(aTransferable);
+ MOZ_ASSERT(aDataObj);
+ if (!aTransferable || !aDataObj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto* dObj = static_cast<nsDataObj*>(aDataObj);
+ if (aMightNeedToFlush) {
+ *aMightNeedToFlush = MightNeedToFlush::No;
+ }
+
+ // Now give the Transferable to the DataObject
+ // for getting the data out of it
+ dObj->SetTransferable(aTransferable);
+
+ // Get the transferable list of data flavors
+ nsTArray<nsCString> flavors;
+ aTransferable->FlavorsTransferableCanExport(flavors);
+
+ // Walk through flavors that contain data and register them
+ // into the DataObj as supported flavors
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ // When putting data onto the clipboard, we want to maintain kHTMLMime
+ // ("text/html") and not map it to CF_HTML here since this will be done
+ // below.
+ UINT format = GetFormat(flavorStr.get(), false);
+
+ // Now tell the native IDataObject about both our mime type and
+ // the native data format
+ FORMATETC fe;
+ SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ dObj->AddDataFlavor(flavorStr.get(), &fe);
+
+ // Do various things internal to the implementation, like map one
+ // flavor to another or add additional flavors based on what's required
+ // for the win32 impl.
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ // if we find text/plain, also add CF_TEXT, but we can add it for
+ // text/plain as well.
+ FORMATETC textFE;
+ SET_FORMATETC(textFE, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ dObj->AddDataFlavor(kTextMime, &textFE);
+ if (aMightNeedToFlush) {
+ *aMightNeedToFlush = MightNeedToFlush::Yes;
+ }
+ } else if (flavorStr.EqualsLiteral(kHTMLMime)) {
+ // if we find text/html, also advertise win32's html flavor (which we will
+ // convert on our own in nsDataObj::GetText().
+ FORMATETC htmlFE;
+ SET_FORMATETC(htmlFE, GetHtmlClipboardFormat(), 0, DVASPECT_CONTENT, -1,
+ TYMED_HGLOBAL);
+ dObj->AddDataFlavor(kHTMLMime, &htmlFE);
+ } else if (flavorStr.EqualsLiteral(kURLMime)) {
+ // if we're a url, in addition to also being text, we need to register
+ // the "file" flavors so that the win32 shell knows to create an internet
+ // shortcut when it sees one of these beasts.
+ FORMATETC shortcutFE;
+ SET_FORMATETC(shortcutFE,
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(kURLMime, &shortcutFE);
+ SET_FORMATETC(shortcutFE,
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(kURLMime, &shortcutFE);
+ SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_FILECONTENTS),
+ 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(kURLMime, &shortcutFE);
+ SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLA), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(kURLMime, &shortcutFE);
+ SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLW), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(kURLMime, &shortcutFE);
+ } else if (flavorStr.EqualsLiteral(kPNGImageMime) ||
+ flavorStr.EqualsLiteral(kJPEGImageMime) ||
+ flavorStr.EqualsLiteral(kJPGImageMime) ||
+ flavorStr.EqualsLiteral(kGIFImageMime) ||
+ flavorStr.EqualsLiteral(kNativeImageMime)) {
+ // if we're an image, register the native bitmap flavor
+ FORMATETC imageFE;
+ // Add DIBv5
+ SET_FORMATETC(imageFE, CF_DIBV5, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(flavorStr.get(), &imageFE);
+ // Add DIBv3
+ SET_FORMATETC(imageFE, CF_DIB, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(flavorStr.get(), &imageFE);
+ } else if (flavorStr.EqualsLiteral(kFilePromiseMime)) {
+ // if we're a file promise flavor, also register the
+ // CFSTR_PREFERREDDROPEFFECT format. The data object
+ // returns a value of DROPEFFECTS_MOVE to the drop target
+ // when it asks for the value of this format. This causes
+ // the file to be moved from the temporary location instead
+ // of being copied. The right thing to do here is to call
+ // SetData() on the data object and set the value of this format
+ // to DROPEFFECTS_MOVE on this particular data object. But,
+ // since all the other clipboard formats follow the model of setting
+ // data on the data object only when the drop object calls GetData(),
+ // I am leaving this format's value hard coded in the data object.
+ // We can change this if other consumers of this format get added to this
+ // codebase and they need different values.
+ FORMATETC shortcutFE;
+ SET_FORMATETC(shortcutFE,
+ ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL)
+ dObj->AddDataFlavor(kFilePromiseMime, &shortcutFE);
+ }
+ }
+
+ if (!mozilla::StaticPrefs::
+ clipboard_copyPrivateDataToClipboardCloudOrHistory()) {
+ // Let Clipboard know that data is sensitive and must not be copied to
+ // the Cloud Clipboard, Clipboard History and similar.
+ // https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats
+ if (aTransferable->GetIsPrivateData()) {
+ nsresult rv =
+ StoreValueInDataObject(dObj, TEXT("CanUploadToCloudClipboard"), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv =
+ StoreValueInDataObject(dObj, TEXT("CanIncludeInClipboardHistory"), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = StoreValueInDataObject(
+ dObj, TEXT("ExcludeClipboardContentFromMonitorProcessing"), 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+// See methods listed at
+// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-idataobject#methods>.
+static void IDataObjectMethodResultToString(const HRESULT aHres,
+ nsACString& aResult) {
+ switch (aHres) {
+ case E_INVALIDARG:
+ aResult = "E_INVALIDARG";
+ break;
+ case E_UNEXPECTED:
+ aResult = "E_UNEXPECTED";
+ break;
+ case E_OUTOFMEMORY:
+ aResult = "E_OUTOFMEMORY";
+ break;
+ case DV_E_LINDEX:
+ aResult = "DV_E_LINDEX";
+ break;
+ case DV_E_FORMATETC:
+ aResult = "DV_E_FORMATETC";
+ break;
+ case DV_E_TYMED:
+ aResult = "DV_E_TYMED";
+ break;
+ case DV_E_DVASPECT:
+ aResult = "DV_E_DVASPECT";
+ break;
+ case OLE_E_NOTRUNNING:
+ aResult = "OLE_E_NOTRUNNING";
+ break;
+ case STG_E_MEDIUMFULL:
+ aResult = "STG_E_MEDIUMFULL";
+ break;
+ case DV_E_CLIPFORMAT:
+ aResult = "DV_E_CLIPFORMAT";
+ break;
+ case S_OK:
+ aResult = "S_OK";
+ break;
+ default:
+ // Explicit template instantiaton, because otherwise the call is
+ // ambiguous.
+ constexpr int kRadix = 16;
+ aResult = IntToCString<int32_t>(aHres, kRadix);
+ break;
+ }
+}
+
+// See
+// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olegetclipboard>.
+static void OleGetClipboardResultToString(const HRESULT aHres,
+ nsACString& aResult) {
+ switch (aHres) {
+ case S_OK:
+ aResult = "S_OK";
+ break;
+ case CLIPBRD_E_CANT_OPEN:
+ aResult = "CLIPBRD_E_CANT_OPEN";
+ break;
+ case CLIPBRD_E_CANT_CLOSE:
+ aResult = "CLIPBRD_E_CANT_CLOSE";
+ break;
+ default:
+ // Explicit template instantiaton, because otherwise the call is
+ // ambiguous.
+ constexpr int kRadix = 16;
+ aResult = IntToCString<int32_t>(aHres, kRadix);
+ break;
+ }
+}
+
+// See
+// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olegetclipboard>.
+static void LogOleGetClipboardResult(const HRESULT aHres) {
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ nsAutoCString hresString;
+ OleGetClipboardResultToString(aHres, hresString);
+ MOZ_CLIPBOARD_LOG("OleGetClipboard result: %s", hresString.get());
+ }
+}
+
+// See
+// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olesetclipboard>.
+static void OleSetClipboardResultToString(HRESULT aHres, nsACString& aResult) {
+ switch (aHres) {
+ case S_OK:
+ aResult = "S_OK";
+ break;
+ case CLIPBRD_E_CANT_OPEN:
+ aResult = "CLIPBRD_E_CANT_OPEN";
+ break;
+ case CLIPBRD_E_CANT_EMPTY:
+ aResult = "CLIPBRD_E_CANT_EMPTY";
+ break;
+ case CLIPBRD_E_CANT_CLOSE:
+ aResult = "CLIPBRD_E_CANT_CLOSE";
+ break;
+ case CLIPBRD_E_CANT_SET:
+ aResult = "CLIPBRD_E_CANT_SET";
+ break;
+ default:
+ // Explicit template instantiaton, because otherwise the call is
+ // ambiguous.
+ constexpr int kRadix = 16;
+ aResult = IntToCString<int32_t>(aHres, kRadix);
+ break;
+ }
+}
+
+// See
+// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olesetclipboard>.
+static void LogOleSetClipboardResult(const HRESULT aHres) {
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ nsAutoCString hresString;
+ OleSetClipboardResultToString(aHres, hresString);
+ MOZ_CLIPBOARD_LOG("OleSetClipboard result: %s", hresString.get());
+ }
+}
+
+template <typename Function, typename LogFunction, typename... Args>
+static HRESULT RepeatedlyTry(Function aFunction, LogFunction aLogFunction,
+ Args... aArgs) {
+ // These are magic values based on local testing. They are chosen not higher
+ // to avoid jank (<https://developer.mozilla.org/en-US/docs/Glossary/Jank>).
+ // When changing them, be careful.
+ static constexpr int kNumberOfTries = 3;
+ static constexpr int kDelayInMs = 3;
+
+ HRESULT hres;
+ for (int i = 0; i < kNumberOfTries; ++i) {
+ hres = aFunction(aArgs...);
+ aLogFunction(hres);
+
+ if (hres == S_OK) {
+ break;
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(kDelayInMs));
+ }
+
+ return hres;
+}
+
+// Other apps can block access to the clipboard. This repeatedly
+// calls `::OleSetClipboard` for a fixed number of times and should be called
+// instead of `::OleSetClipboard`.
+static void RepeatedlyTryOleSetClipboard(IDataObject* aDataObj) {
+ RepeatedlyTry(::OleSetClipboard, LogOleSetClipboardResult, aDataObj);
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP nsClipboard::SetNativeClipboardData(
+ nsITransferable* aTransferable, int32_t aWhichClipboard) {
+ MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);
+
+ if (aWhichClipboard != kGlobalClipboard) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // make sure we have a good transferable
+ if (!aTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+#ifdef ACCESSIBILITY
+ mozilla::a11y::Compatibility::SuppressA11yForClipboardCopy();
+#endif
+
+ RefPtr<IDataObject> dataObj;
+ auto mightNeedToFlush = MightNeedToFlush::No;
+ if (NS_SUCCEEDED(CreateNativeDataObject(aTransferable,
+ getter_AddRefs(dataObj), nullptr,
+ &mightNeedToFlush))) {
+ RepeatedlyTryOleSetClipboard(dataObj);
+
+ const bool doFlush = [&] {
+ switch (mozilla::StaticPrefs::widget_windows_sync_clipboard_flush()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ // Bug 1774285: Windows Suggested Actions (introduced in Windows 11
+ // 22H2) walks the entire a11y tree using UIA if something is placed
+ // on the clipboard using delayed rendering. (The OLE clipboard always
+ // uses delayed rendering.) This a11y tree walk causes an unacceptable
+ // hang, particularly when the a11y cache is disabled. We choose the
+ // lesser of the two performance/memory evils here and force immediate
+ // rendering as part of our workaround.
+ return mightNeedToFlush == MightNeedToFlush::Yes &&
+ mozilla::IsWin1122H2OrLater();
+ }
+ }();
+ if (doFlush) {
+ RepeatedlyTry(::OleFlushClipboard, [](HRESULT) {});
+ }
+ } else {
+ // Clear the native clipboard
+ RepeatedlyTryOleSetClipboard(nullptr);
+ }
+
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+nsresult nsClipboard::GetGlobalData(HGLOBAL aHGBL, void** aData,
+ uint32_t* aLen) {
+ MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);
+
+ // Allocate a new memory buffer and copy the data from global memory.
+ // Recall that win98 allocates to nearest DWORD boundary. As a safety
+ // precaution, allocate an extra 3 bytes (but don't report them in |aLen|!)
+ // and null them out to ensure that all of our NS_strlen calls will succeed.
+ // NS_strlen operates on char16_t, so we need 3 NUL bytes to ensure it finds
+ // a full NUL char16_t when |*aLen| is odd.
+ nsresult result = NS_ERROR_FAILURE;
+ if (aHGBL != nullptr) {
+ LPSTR lpStr = (LPSTR)GlobalLock(aHGBL);
+ mozilla::CheckedInt<uint32_t> allocSize =
+ mozilla::CheckedInt<uint32_t>(GlobalSize(aHGBL)) + 3;
+ if (!allocSize.isValid()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ char* data = static_cast<char*>(malloc(allocSize.value()));
+ if (data) {
+ uint32_t size = allocSize.value() - 3;
+ memcpy(data, lpStr, size);
+ // null terminate for safety
+ data[size] = data[size + 1] = data[size + 2] = '\0';
+
+ GlobalUnlock(aHGBL);
+ *aData = data;
+ *aLen = size;
+
+ result = NS_OK;
+ }
+ } else {
+ // We really shouldn't ever get here
+ // but just in case
+ *aData = nullptr;
+ *aLen = 0;
+ LPVOID lpMsgBuf;
+
+ FormatMessageW(
+ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, nullptr,
+ GetLastError(),
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
+ (LPWSTR)&lpMsgBuf, 0, nullptr);
+
+ // Display the string.
+ MessageBoxW(nullptr, (LPCWSTR)lpMsgBuf, L"GetLastError",
+ MB_OK | MB_ICONINFORMATION);
+
+ // Free the buffer.
+ LocalFree(lpMsgBuf);
+ }
+
+ return result;
+}
+
+//-------------------------------------------------------------------------
+nsresult nsClipboard::GetNativeDataOffClipboard(nsIWidget* aWidget,
+ UINT /*aIndex*/, UINT aFormat,
+ void** aData, uint32_t* aLen) {
+ MOZ_CLIPBOARD_LOG("%s: overload taking nsIWidget*.", __FUNCTION__);
+
+ HGLOBAL hglb;
+ nsresult result = NS_ERROR_FAILURE;
+
+ HWND nativeWin = nullptr;
+ if (::OpenClipboard(nativeWin)) {
+ hglb = ::GetClipboardData(aFormat);
+ result = GetGlobalData(hglb, aData, aLen);
+ ::CloseClipboard();
+ }
+ return result;
+}
+
+// See methods listed at
+// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-idataobject#methods>.
+static void LogIDataObjectMethodResult(const HRESULT aHres,
+ const nsCString& aMethodName) {
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ nsAutoCString hresString;
+ IDataObjectMethodResultToString(aHres, hresString);
+ MOZ_CLIPBOARD_LOG("IDataObject::%s result : %s", aMethodName.get(),
+ hresString.get());
+ }
+}
+
+// Other apps can block access to the clipboard. This repeatedly calls
+// `GetData` for a fixed number of times and should be called instead of
+// `GetData`. See
+// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-idataobject-getdata>.
+// While Microsoft's documentation doesn't include `CLIPBRD_E_CANT_OPEN`
+// explicitly, it allows it implicitly and in local experiments it was indeed
+// returned.
+static HRESULT RepeatedlyTryGetData(IDataObject& aDataObject, LPFORMATETC pFE,
+ LPSTGMEDIUM pSTM) {
+ return RepeatedlyTry(
+ [&aDataObject, &pFE, &pSTM]() { return aDataObject.GetData(pFE, pSTM); },
+ std::bind(LogIDataObjectMethodResult, std::placeholders::_1,
+ "GetData"_ns));
+}
+
+//-------------------------------------------------------------------------
+// static
+HRESULT nsClipboard::FillSTGMedium(IDataObject* aDataObject, UINT aFormat,
+ LPFORMATETC pFE, LPSTGMEDIUM pSTM,
+ DWORD aTymed) {
+ SET_FORMATETC(*pFE, aFormat, 0, DVASPECT_CONTENT, -1, aTymed);
+
+ // Starting by querying for the data to see if we can get it as from global
+ // memory
+ HRESULT hres = S_FALSE;
+ hres = aDataObject->QueryGetData(pFE);
+ LogIDataObjectMethodResult(hres, "QueryGetData"_ns);
+ if (S_OK == hres) {
+ hres = RepeatedlyTryGetData(*aDataObject, pFE, pSTM);
+ }
+ return hres;
+}
+
+//-------------------------------------------------------------------------
+// If aFormat is CF_DIBV5, aMIMEImageFormat must be a type for which we have
+// an image encoder (e.g. image/png).
+// For other values of aFormat, it is OK to pass null for aMIMEImageFormat.
+nsresult nsClipboard::GetNativeDataOffClipboard(IDataObject* aDataObject,
+ UINT aIndex, UINT aFormat,
+ const char* aMIMEImageFormat,
+ void** aData, uint32_t* aLen) {
+ MOZ_CLIPBOARD_LOG("%s: overload taking IDataObject*.", __FUNCTION__);
+
+ nsresult result = NS_ERROR_FAILURE;
+ *aData = nullptr;
+ *aLen = 0;
+
+ if (!aDataObject) {
+ return result;
+ }
+
+ UINT format = aFormat;
+ HRESULT hres = S_FALSE;
+
+ // XXX at the moment we only support global memory transfers
+ // It is here where we will add support for native images
+ // and IStream
+ FORMATETC fe;
+ STGMEDIUM stm;
+ hres = FillSTGMedium(aDataObject, format, &fe, &stm, TYMED_HGLOBAL);
+
+ // If the format is CF_HDROP and we haven't found any files we can try looking
+ // for virtual files with FILEDESCRIPTOR.
+ if (FAILED(hres) && format == CF_HDROP) {
+ hres = FillSTGMedium(aDataObject,
+ nsClipboard::GetClipboardFileDescriptorFormatW(), &fe,
+ &stm, TYMED_HGLOBAL);
+ if (FAILED(hres)) {
+ hres = FillSTGMedium(aDataObject,
+ nsClipboard::GetClipboardFileDescriptorFormatA(),
+ &fe, &stm, TYMED_HGLOBAL);
+ }
+ }
+
+ // Currently this is only handling TYMED_HGLOBAL data
+ // For Text, Dibs, Files, and generic data (like HTML)
+ if (S_OK == hres) {
+ static CLIPFORMAT fileDescriptorFlavorA =
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA);
+ static CLIPFORMAT fileDescriptorFlavorW =
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW);
+ static CLIPFORMAT fileFlavor =
+ ::RegisterClipboardFormat(CFSTR_FILECONTENTS);
+ static CLIPFORMAT preferredDropEffect =
+ ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT);
+
+ switch (stm.tymed) {
+ case TYMED_HGLOBAL: {
+ switch (fe.cfFormat) {
+ case CF_TEXT: {
+ // Get the data out of the global data handle. The size we
+ // return should not include the null because the other
+ // platforms don't use nulls, so just return the length we get
+ // back from strlen(), since we know CF_TEXT is null
+ // terminated. Recall that GetGlobalData() returns the size of
+ // the allocated buffer, not the size of the data (on 98, these
+ // are not the same) so we can't use that.
+ uint32_t allocLen = 0;
+ if (NS_SUCCEEDED(GetGlobalData(stm.hGlobal, aData, &allocLen))) {
+ *aLen = strlen(reinterpret_cast<char*>(*aData));
+ result = NS_OK;
+ }
+ } break;
+
+ case CF_UNICODETEXT: {
+ // Get the data out of the global data handle. The size we
+ // return should not include the null because the other
+ // platforms don't use nulls, so just return the length we get
+ // back from strlen(), since we know CF_UNICODETEXT is null
+ // terminated. Recall that GetGlobalData() returns the size of
+ // the allocated buffer, not the size of the data (on 98, these
+ // are not the same) so we can't use that.
+ uint32_t allocLen = 0;
+ if (NS_SUCCEEDED(GetGlobalData(stm.hGlobal, aData, &allocLen))) {
+ *aLen = NS_strlen(reinterpret_cast<char16_t*>(*aData)) * 2;
+ result = NS_OK;
+ }
+ } break;
+
+ case CF_DIBV5:
+ if (aMIMEImageFormat) {
+ uint32_t allocLen = 0;
+ const char* clipboardData;
+ if (NS_SUCCEEDED(GetGlobalData(
+ stm.hGlobal, (void**)&clipboardData, &allocLen))) {
+ nsCOMPtr<imgIContainer> container;
+ nsCOMPtr<imgITools> imgTools =
+ do_CreateInstance("@mozilla.org/image/tools;1");
+ result = imgTools->DecodeImageFromBuffer(
+ clipboardData, allocLen,
+ nsLiteralCString(IMAGE_BMP_MS_CLIPBOARD),
+ getter_AddRefs(container));
+ if (NS_FAILED(result)) {
+ break;
+ }
+
+ nsAutoCString mimeType;
+ if (strcmp(aMIMEImageFormat, kJPGImageMime) == 0) {
+ mimeType.Assign(IMAGE_JPEG);
+ } else {
+ mimeType.Assign(aMIMEImageFormat);
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ result = imgTools->EncodeImage(container, mimeType, u""_ns,
+ getter_AddRefs(inputStream));
+ if (NS_FAILED(result)) {
+ break;
+ }
+
+ if (!inputStream) {
+ result = NS_ERROR_FAILURE;
+ break;
+ }
+
+ *aData = inputStream.forget().take();
+ *aLen = sizeof(nsIInputStream*);
+ }
+ }
+ break;
+
+ case CF_HDROP: {
+ // in the case of a file drop, multiple files are stashed within a
+ // single data object. In order to match mozilla's D&D apis, we
+ // just pull out the file at the requested index, pretending as
+ // if there really are multiple drag items.
+ HDROP dropFiles = (HDROP)GlobalLock(stm.hGlobal);
+
+ UINT numFiles = ::DragQueryFileW(dropFiles, 0xFFFFFFFF, nullptr, 0);
+ NS_ASSERTION(numFiles > 0,
+ "File drop flavor, but no files...hmmmm");
+ NS_ASSERTION(aIndex < numFiles,
+ "Asked for a file index out of range of list");
+ if (numFiles > 0) {
+ UINT fileNameLen =
+ ::DragQueryFileW(dropFiles, aIndex, nullptr, 0);
+ wchar_t* buffer = reinterpret_cast<wchar_t*>(
+ moz_xmalloc((fileNameLen + 1) * sizeof(wchar_t)));
+ ::DragQueryFileW(dropFiles, aIndex, buffer, fileNameLen + 1);
+ *aData = buffer;
+ *aLen = fileNameLen * sizeof(char16_t);
+ result = NS_OK;
+ }
+ GlobalUnlock(stm.hGlobal);
+
+ } break;
+
+ default: {
+ if (fe.cfFormat == fileDescriptorFlavorA ||
+ fe.cfFormat == fileDescriptorFlavorW) {
+ nsAutoString tempPath;
+
+ LPFILEGROUPDESCRIPTOR fgdesc =
+ static_cast<LPFILEGROUPDESCRIPTOR>(GlobalLock(stm.hGlobal));
+ if (fgdesc) {
+ result = GetTempFilePath(
+ nsDependentString((fgdesc->fgd)[aIndex].cFileName),
+ tempPath);
+ GlobalUnlock(stm.hGlobal);
+ }
+ if (NS_FAILED(result)) {
+ break;
+ }
+ result = SaveStorageOrStream(aDataObject, aIndex, tempPath);
+ if (NS_FAILED(result)) {
+ break;
+ }
+ wchar_t* buffer = reinterpret_cast<wchar_t*>(
+ moz_xmalloc((tempPath.Length() + 1) * sizeof(wchar_t)));
+ wcscpy(buffer, tempPath.get());
+ *aData = buffer;
+ *aLen = tempPath.Length() * sizeof(wchar_t);
+ result = NS_OK;
+ } else if (fe.cfFormat == fileFlavor) {
+ NS_WARNING(
+ "Mozilla doesn't yet understand how to read this type of "
+ "file flavor");
+ } else {
+ // Get the data out of the global data handle. The size we
+ // return should not include the null because the other
+ // platforms don't use nulls, so just return the length we get
+ // back from strlen(), since we know CF_UNICODETEXT is null
+ // terminated. Recall that GetGlobalData() returns the size of
+ // the allocated buffer, not the size of the data (on 98, these
+ // are not the same) so we can't use that.
+ //
+ // NOTE: we are assuming that anything that falls into this
+ // default case is unicode. As we start to get more
+ // kinds of binary data, this may become an incorrect
+ // assumption. Stay tuned.
+ uint32_t allocLen = 0;
+ if (NS_SUCCEEDED(GetGlobalData(stm.hGlobal, aData, &allocLen))) {
+ if (fe.cfFormat == GetHtmlClipboardFormat()) {
+ // CF_HTML is actually UTF8, not unicode, so disregard the
+ // assumption above. We have to check the header for the
+ // actual length, and we'll do that in FindPlatformHTML().
+ // For now, return the allocLen. This case is mostly to
+ // ensure we don't try to call strlen on the buffer.
+ *aLen = allocLen;
+ } else if (fe.cfFormat == GetCustomClipboardFormat()) {
+ // Binary data
+ *aLen = allocLen;
+ } else if (fe.cfFormat == preferredDropEffect) {
+ // As per the MSDN doc entitled: "Shell Clipboard Formats"
+ // CFSTR_PREFERREDDROPEFFECT should return a DWORD
+ // Reference:
+ // http://msdn.microsoft.com/en-us/library/bb776902(v=vs.85).aspx
+ NS_ASSERTION(
+ allocLen == sizeof(DWORD),
+ "CFSTR_PREFERREDDROPEFFECT should return a DWORD");
+ *aLen = allocLen;
+ } else {
+ *aLen = NS_strlen(reinterpret_cast<char16_t*>(*aData)) *
+ sizeof(char16_t);
+ }
+ result = NS_OK;
+ }
+ }
+ } break;
+ } // switch
+ } break;
+
+ case TYMED_GDI: {
+#ifdef DEBUG
+ MOZ_CLIPBOARD_LOG("*********************** TYMED_GDI");
+#endif
+ } break;
+
+ default:
+ break;
+ } // switch
+
+ ReleaseStgMedium(&stm);
+ }
+
+ return result;
+}
+
+//-------------------------------------------------------------------------
+nsresult nsClipboard::GetDataFromDataObject(IDataObject* aDataObject,
+ UINT anIndex, nsIWidget* aWindow,
+ nsITransferable* aTransferable) {
+ MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);
+
+ // make sure we have a good transferable
+ if (!aTransferable) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult res = NS_ERROR_FAILURE;
+
+ // get flavor list that includes all flavors that can be written (including
+ // ones obtained through conversion)
+ nsTArray<nsCString> flavors;
+ res = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(res)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Walk through flavors and see which flavor is on the clipboard them on the
+ // native clipboard,
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+ UINT format = GetFormat(flavorStr.get());
+
+ // Try to get the data using the desired flavor. This might fail, but all is
+ // not lost.
+ void* data = nullptr;
+ uint32_t dataLen = 0;
+ bool dataFound = false;
+ if (nullptr != aDataObject) {
+ if (NS_SUCCEEDED(GetNativeDataOffClipboard(aDataObject, anIndex, format,
+ flavorStr.get(), &data,
+ &dataLen))) {
+ dataFound = true;
+ }
+ } else if (nullptr != aWindow) {
+ if (NS_SUCCEEDED(GetNativeDataOffClipboard(aWindow, anIndex, format,
+ &data, &dataLen))) {
+ dataFound = true;
+ }
+ }
+
+ // This is our second chance to try to find some data, having not found it
+ // when directly asking for the flavor. Let's try digging around in other
+ // flavors to help satisfy our craving for data.
+ if (!dataFound) {
+ if (flavorStr.EqualsLiteral(kTextMime)) {
+ dataFound =
+ FindUnicodeFromPlainText(aDataObject, anIndex, &data, &dataLen);
+ } else if (flavorStr.EqualsLiteral(kURLMime)) {
+ // drags from other windows apps expose the native
+ // CFSTR_INETURL{A,W} flavor
+ dataFound = FindURLFromNativeURL(aDataObject, anIndex, &data, &dataLen);
+ if (!dataFound) {
+ dataFound =
+ FindURLFromLocalFile(aDataObject, anIndex, &data, &dataLen);
+ }
+ }
+ } // if we try one last ditch effort to find our data
+
+ // Hopefully by this point we've found it and can go about our business
+ if (dataFound) {
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ if (flavorStr.EqualsLiteral(kFileMime)) {
+ // we have a file path in |data|. Create an nsLocalFile object.
+ nsDependentString filepath(reinterpret_cast<char16_t*>(data));
+ nsCOMPtr<nsIFile> file;
+ if (NS_SUCCEEDED(
+ NS_NewLocalFile(filepath, false, getter_AddRefs(file)))) {
+ genericDataWrapper = do_QueryInterface(file);
+ }
+ free(data);
+ } else if (flavorStr.EqualsLiteral(kNativeHTMLMime)) {
+ uint32_t dummy;
+ // the editor folks want CF_HTML exactly as it's on the clipboard, no
+ // conversions, no fancy stuff. Pull it off the clipboard, stuff it into
+ // a wrapper and hand it back to them.
+ if (FindPlatformHTML(aDataObject, anIndex, &data, &dummy, &dataLen)) {
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ flavorStr, data, dataLen, getter_AddRefs(genericDataWrapper));
+ } else {
+ free(data);
+ continue; // something wrong with this flavor, keep looking for other
+ // data
+ }
+ free(data);
+ } else if (flavorStr.EqualsLiteral(kHTMLMime)) {
+ uint32_t startOfData = 0;
+ // The JS folks want CF_HTML exactly as it is on the clipboard, but
+ // minus the CF_HTML header index information.
+ // It also needs to be converted to UTF16 and have linebreaks changed.
+ if (FindPlatformHTML(aDataObject, anIndex, &data, &startOfData,
+ &dataLen)) {
+ dataLen -= startOfData;
+ nsPrimitiveHelpers::CreatePrimitiveForCFHTML(
+ static_cast<char*>(data) + startOfData, &dataLen,
+ getter_AddRefs(genericDataWrapper));
+ } else {
+ free(data);
+ continue; // something wrong with this flavor, keep looking for other
+ // data
+ }
+ free(data);
+ } else if (flavorStr.EqualsLiteral(kJPEGImageMime) ||
+ flavorStr.EqualsLiteral(kJPGImageMime) ||
+ flavorStr.EqualsLiteral(kPNGImageMime)) {
+ nsIInputStream* imageStream = reinterpret_cast<nsIInputStream*>(data);
+ genericDataWrapper = do_QueryInterface(imageStream);
+ NS_IF_RELEASE(imageStream);
+ } else {
+ // Treat custom types as a string of bytes.
+ if (!flavorStr.EqualsLiteral(kCustomTypesMime)) {
+ bool isRTF = flavorStr.EqualsLiteral(kRTFMime);
+ // we probably have some form of text. The DOM only wants LF, so
+ // convert from Win32 line endings to DOM line endings.
+ int32_t signedLen = static_cast<int32_t>(dataLen);
+ nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(isRTF, &data,
+ &signedLen);
+ dataLen = signedLen;
+
+ if (isRTF) {
+ // RTF on Windows is known to sometimes deliver an extra null byte.
+ if (dataLen > 0 && static_cast<char*>(data)[dataLen - 1] == '\0') {
+ dataLen--;
+ }
+ }
+ }
+
+ nsPrimitiveHelpers::CreatePrimitiveForData(
+ flavorStr, data, dataLen, getter_AddRefs(genericDataWrapper));
+ free(data);
+ }
+
+ NS_ASSERTION(genericDataWrapper,
+ "About to put null data into the transferable");
+ aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
+ res = NS_OK;
+
+ // we found one, get out of the loop
+ break;
+ }
+ } // foreach flavor
+
+ return res;
+}
+
+//
+// FindPlatformHTML
+//
+// Someone asked for the OS CF_HTML flavor. We give it back to them exactly
+// as-is.
+//
+bool nsClipboard ::FindPlatformHTML(IDataObject* inDataObject, UINT inIndex,
+ void** outData, uint32_t* outStartOfData,
+ uint32_t* outDataLen) {
+ // Reference: MSDN doc entitled "HTML Clipboard Format"
+ // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854
+ // CF_HTML is UTF8, not unicode. We also can't rely on it being
+ // null-terminated so we have to check the CF_HTML header for the correct
+ // length. The length we return is the bytecount from the beginning of the
+ // selected data to the end of the selected data, without the null
+ // termination. Because it's UTF8, we're guaranteed the header is ASCII.
+
+ if (!outData || !*outData) {
+ return false;
+ }
+
+ char version[8] = {0};
+ int32_t startOfData = 0;
+ int32_t endOfData = 0;
+ int numFound =
+ sscanf((char*)*outData, "Version:%7s\nStartHTML:%d\nEndHTML:%d", version,
+ &startOfData, &endOfData);
+
+ if (numFound != 3 || startOfData < -1 || endOfData < -1) {
+ return false;
+ }
+
+ // Fixup the start and end markers if they have no context (set to -1)
+ if (startOfData == -1) {
+ startOfData = 0;
+ }
+ if (endOfData == -1) {
+ endOfData = *outDataLen;
+ }
+
+ // Make sure we were passed sane values within our buffer size.
+ // (Note that we've handled all cases of negative endOfData above, so we can
+ // safely cast it to be unsigned here.)
+ if (!endOfData || startOfData >= endOfData ||
+ static_cast<uint32_t>(endOfData) > *outDataLen) {
+ return false;
+ }
+
+ // We want to return the buffer not offset by startOfData because it will be
+ // parsed out later (probably by HTMLEditor::ParseCFHTML) when it is still
+ // in CF_HTML format.
+
+ // We return the byte offset from the start of the data buffer to where the
+ // HTML data starts. The caller might want to extract the HTML only.
+ *outStartOfData = startOfData;
+ *outDataLen = endOfData;
+ return true;
+}
+
+//
+// FindUnicodeFromPlainText
+//
+// Looks for CF_TEXT on the clipboard and converts it into an UTF-16 string
+// if present. Returns this string in outData, and its length in outDataLen.
+// XXXndeakin Windows converts between CF_UNICODE and CF_TEXT automatically
+// so it doesn't seem like this is actually needed.
+//
+bool nsClipboard ::FindUnicodeFromPlainText(IDataObject* inDataObject,
+ UINT inIndex, void** outData,
+ uint32_t* outDataLen) {
+ MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);
+
+ // We are looking for text/plain and we failed to find it on the clipboard
+ // first, so try again with CF_TEXT. If that is present, convert it to
+ // unicode.
+ nsresult rv = GetNativeDataOffClipboard(inDataObject, inIndex, CF_TEXT,
+ nullptr, outData, outDataLen);
+ if (NS_FAILED(rv) || !*outData) {
+ return false;
+ }
+
+ const char* castedText = static_cast<char*>(*outData);
+ nsAutoString tmp;
+ rv = NS_CopyNativeToUnicode(nsDependentCSubstring(castedText, *outDataLen),
+ tmp);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // out with the old, in with the new
+ free(*outData);
+ *outData = ToNewUnicode(tmp);
+ *outDataLen = tmp.Length() * sizeof(char16_t);
+
+ return true;
+
+} // FindUnicodeFromPlainText
+
+//
+// FindURLFromLocalFile
+//
+// we are looking for a URL and couldn't find it, try again with looking for
+// a local file. If we have one, it may either be a normal file or an internet
+// shortcut. In both cases, however, we can get a URL (it will be a file:// url
+// in the local file case).
+//
+bool nsClipboard ::FindURLFromLocalFile(IDataObject* inDataObject, UINT inIndex,
+ void** outData, uint32_t* outDataLen) {
+ MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);
+
+ bool dataFound = false;
+
+ nsresult loadResult =
+ GetNativeDataOffClipboard(inDataObject, inIndex, GetFormat(kFileMime),
+ nullptr, outData, outDataLen);
+ if (NS_SUCCEEDED(loadResult) && *outData) {
+ // we have a file path in |data|. Is it an internet shortcut or a normal
+ // file?
+ const nsDependentString filepath(static_cast<char16_t*>(*outData));
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(filepath, true, getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ free(*outData);
+ return dataFound;
+ }
+
+ if (IsInternetShortcut(filepath)) {
+ free(*outData);
+ nsAutoCString url;
+ ResolveShortcut(file, url);
+ if (!url.IsEmpty()) {
+ // convert it to unicode and pass it out
+ NS_ConvertUTF8toUTF16 urlString(url);
+ // the internal mozilla URL format, text/x-moz-url, contains
+ // URL\ntitle. We can guess the title from the file's name.
+ nsAutoString title;
+ file->GetLeafName(title);
+ // We rely on IsInternetShortcut check that file has a .url extension.
+ title.SetLength(title.Length() - 4);
+ if (title.IsEmpty()) {
+ title = urlString;
+ }
+ *outData = ToNewUnicode(urlString + u"\n"_ns + title);
+ *outDataLen =
+ NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);
+
+ dataFound = true;
+ }
+ } else {
+ // we have a normal file, use some Necko objects to get our file path
+ nsAutoCString urlSpec;
+ NS_GetURLSpecFromFile(file, urlSpec);
+
+ // convert it to unicode and pass it out
+ free(*outData);
+ *outData = UTF8ToNewUnicode(urlSpec);
+ *outDataLen =
+ NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);
+ dataFound = true;
+ } // else regular file
+ }
+
+ return dataFound;
+} // FindURLFromLocalFile
+
+//
+// FindURLFromNativeURL
+//
+// we are looking for a URL and couldn't find it using our internal
+// URL flavor, so look for it using the native URL flavor,
+// CF_INETURLSTRW (We don't handle CF_INETURLSTRA currently)
+//
+bool nsClipboard ::FindURLFromNativeURL(IDataObject* inDataObject, UINT inIndex,
+ void** outData, uint32_t* outDataLen) {
+ MOZ_CLIPBOARD_LOG("%s", __FUNCTION__);
+
+ bool dataFound = false;
+
+ void* tempOutData = nullptr;
+ uint32_t tempDataLen = 0;
+
+ nsresult loadResult = GetNativeDataOffClipboard(
+ inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLW), nullptr,
+ &tempOutData, &tempDataLen);
+ if (NS_SUCCEEDED(loadResult) && tempOutData) {
+ nsDependentString urlString(static_cast<char16_t*>(tempOutData));
+ // the internal mozilla URL format, text/x-moz-url, contains
+ // URL\ntitle. Since we don't actually have a title here,
+ // just repeat the URL to fake it.
+ *outData = ToNewUnicode(urlString + u"\n"_ns + urlString);
+ *outDataLen =
+ NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);
+ free(tempOutData);
+ dataFound = true;
+ } else {
+ loadResult = GetNativeDataOffClipboard(
+ inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLA),
+ nullptr, &tempOutData, &tempDataLen);
+ if (NS_SUCCEEDED(loadResult) && tempOutData) {
+ // CFSTR_INETURLA is (currently) equal to CFSTR_SHELLURL which is equal to
+ // CF_TEXT which is by definition ANSI encoded.
+ nsCString urlUnescapedA;
+ bool unescaped =
+ NS_UnescapeURL(static_cast<char*>(tempOutData), tempDataLen,
+ esc_OnlyNonASCII | esc_SkipControl, urlUnescapedA);
+
+ nsString urlString;
+ if (unescaped) {
+ NS_CopyNativeToUnicode(urlUnescapedA, urlString);
+ } else {
+ NS_CopyNativeToUnicode(
+ nsDependentCString(static_cast<char*>(tempOutData), tempDataLen),
+ urlString);
+ }
+
+ // the internal mozilla URL format, text/x-moz-url, contains
+ // URL\ntitle. Since we don't actually have a title here,
+ // just repeat the URL to fake it.
+ *outData = ToNewUnicode(urlString + u"\n"_ns + urlString);
+ *outDataLen =
+ NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t);
+ free(tempOutData);
+ dataFound = true;
+ }
+ }
+
+ return dataFound;
+} // FindURLFromNativeURL
+
+// Other apps can block access to the clipboard. This repeatedly
+// calls `::OleGetClipboard` for a fixed number of times and should be called
+// instead of `::OleGetClipboard`.
+static HRESULT RepeatedlyTryOleGetClipboard(IDataObject** aDataObj) {
+ return RepeatedlyTry(::OleGetClipboard, LogOleGetClipboardResult, aDataObj);
+}
+
+//
+// ResolveShortcut
+//
+void nsClipboard ::ResolveShortcut(nsIFile* aFile, nsACString& outURL) {
+ nsCOMPtr<nsIFileProtocolHandler> fph;
+ nsresult rv = NS_GetFileProtocolHandler(getter_AddRefs(fph));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = fph->ReadURLFile(aFile, getter_AddRefs(uri));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ uri->GetSpec(outURL);
+} // ResolveShortcut
+
+//
+// IsInternetShortcut
+//
+// A file is an Internet Shortcut if it ends with .URL
+//
+bool nsClipboard ::IsInternetShortcut(const nsAString& inFileName) {
+ return StringEndsWith(inFileName, u".url"_ns,
+ nsCaseInsensitiveStringComparator);
+} // IsInternetShortcut
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(aTransferable);
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+
+ MOZ_CLIPBOARD_LOG("%s aWhichClipboard=%i", __FUNCTION__, aWhichClipboard);
+
+ nsresult res;
+ // This makes sure we can use the OLE functionality for the clipboard
+ IDataObject* dataObj;
+ if (S_OK == RepeatedlyTryOleGetClipboard(&dataObj)) {
+ // Use OLE IDataObject for clipboard operations
+ MOZ_CLIPBOARD_LOG(" use OLE IDataObject:");
+ if (MOZ_CLIPBOARD_LOG_ENABLED()) {
+ IEnumFORMATETC* pEnum = nullptr;
+ if (S_OK == dataObj->EnumFormatEtc(DATADIR_GET, &pEnum)) {
+ FORMATETC fEtc;
+ while (S_OK == pEnum->Next(1, &fEtc, nullptr)) {
+ nsAutoString format;
+ mozilla::widget::WinUtils::GetClipboardFormatAsString(fEtc.cfFormat,
+ format);
+ MOZ_CLIPBOARD_LOG(" FORMAT %s",
+ NS_ConvertUTF16toUTF8(format).get());
+ }
+ }
+ pEnum->Release();
+ }
+
+ res = GetDataFromDataObject(dataObj, 0, nullptr, aTransferable);
+ dataObj->Release();
+ } else {
+ // do it the old manual way
+ res = GetDataFromDataObject(nullptr, 0, mWindow, aTransferable);
+ }
+ return res;
+}
+
+nsresult nsClipboard::EmptyNativeClipboardData(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+ // Some programs such as ZoneAlarm monitor clipboard usage and then open the
+ // clipboard to scan it. If we i) empty and then ii) set data, then the
+ // 'set data' can sometimes fail with access denied becacuse another program
+ // has the clipboard open. So to avoid this race condition for OpenClipboard
+ // we do not empty the clipboard when we're setting it.
+ RepeatedlyTryOleSetClipboard(nullptr);
+ return NS_OK;
+}
+
+mozilla::Result<int32_t, nsresult>
+nsClipboard::GetNativeClipboardSequenceNumber(int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(kGlobalClipboard == aWhichClipboard);
+ return (int32_t)::GetClipboardSequenceNumber();
+}
+
+//-------------------------------------------------------------------------
+mozilla::Result<bool, nsresult>
+nsClipboard::HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));
+ for (const auto& flavor : aFlavorList) {
+ UINT format = GetFormat(flavor.get());
+ if (IsClipboardFormatAvailable(format)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+//-------------------------------------------------------------------------
+nsresult nsClipboard::GetTempFilePath(const nsAString& aFileName,
+ nsAString& aFilePath) {
+ nsresult result = NS_OK;
+
+ nsCOMPtr<nsIFile> tmpFile;
+ result =
+ GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(tmpFile));
+ NS_ENSURE_SUCCESS(result, result);
+
+ result = tmpFile->Append(aFileName);
+ NS_ENSURE_SUCCESS(result, result);
+
+ result = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660);
+ NS_ENSURE_SUCCESS(result, result);
+ result = tmpFile->GetPath(aFilePath);
+
+ return result;
+}
+
+//-------------------------------------------------------------------------
+nsresult nsClipboard::SaveStorageOrStream(IDataObject* aDataObject, UINT aIndex,
+ const nsAString& aFileName) {
+ NS_ENSURE_ARG_POINTER(aDataObject);
+
+ FORMATETC fe = {0};
+ SET_FORMATETC(fe, RegisterClipboardFormat(CFSTR_FILECONTENTS), 0,
+ DVASPECT_CONTENT, aIndex, TYMED_ISTORAGE | TYMED_ISTREAM);
+
+ STGMEDIUM stm = {0};
+ HRESULT hres = aDataObject->GetData(&fe, &stm);
+ if (FAILED(hres)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto releaseMediumGuard =
+ mozilla::MakeScopeExit([&] { ReleaseStgMedium(&stm); });
+
+ // We do this check because, even though we *asked* for IStorage or IStream,
+ // it seems that IDataObject providers can just hand us back whatever they
+ // feel like. See Bug 1824644 for a fun example of that!
+ if (stm.tymed != TYMED_ISTORAGE && stm.tymed != TYMED_ISTREAM) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (stm.tymed == TYMED_ISTORAGE) {
+ RefPtr<IStorage> file;
+ hres = StgCreateStorageEx(
+ aFileName.Data(), STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE,
+ STGFMT_STORAGE, 0, NULL, NULL, IID_IStorage, getter_AddRefs(file));
+ if (FAILED(hres)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ hres = stm.pstg->CopyTo(0, NULL, NULL, file);
+ if (FAILED(hres)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ file->Commit(STGC_DEFAULT);
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(stm.tymed == TYMED_ISTREAM);
+
+ HANDLE handle = CreateFile(aFileName.Data(), GENERIC_WRITE, FILE_SHARE_READ,
+ NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (handle == INVALID_HANDLE_VALUE) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto fileCloseGuard = mozilla::MakeScopeExit([&] { CloseHandle(handle); });
+
+ const ULONG bufferSize = 4096;
+ char buffer[bufferSize] = {0};
+ ULONG bytesRead = 0;
+ DWORD bytesWritten = 0;
+ while (true) {
+ HRESULT result = stm.pstm->Read(buffer, bufferSize, &bytesRead);
+ if (FAILED(result)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (bytesRead == 0) {
+ break;
+ }
+ if (!WriteFile(handle, buffer, static_cast<DWORD>(bytesRead), &bytesWritten,
+ NULL)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
diff --git a/widget/windows/nsClipboard.h b/widget/windows/nsClipboard.h
new file mode 100644
index 0000000000..b0afb9ee40
--- /dev/null
+++ b/widget/windows/nsClipboard.h
@@ -0,0 +1,113 @@
+/* -*- 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/. */
+
+#ifndef nsClipboard_h__
+#define nsClipboard_h__
+
+#include "nsBaseClipboard.h"
+#include "nsIObserver.h"
+#include "nsIURI.h"
+
+#include <ole2.h>
+#include <windows.h>
+
+class nsITransferable;
+class nsIWidget;
+class nsIFile;
+struct IDataObject;
+
+/**
+ * Native Win32 Clipboard wrapper
+ */
+
+class nsClipboard : public nsBaseClipboard, public nsIObserver {
+ virtual ~nsClipboard();
+
+ public:
+ nsClipboard();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIObserver
+ NS_DECL_NSIOBSERVER
+
+ // Internal Native Routines
+ enum class MightNeedToFlush : bool { No, Yes };
+ static nsresult CreateNativeDataObject(nsITransferable* aTransferable,
+ IDataObject** aDataObj, nsIURI* aUri,
+ MightNeedToFlush* = nullptr);
+ static nsresult SetupNativeDataObject(nsITransferable* aTransferable,
+ IDataObject* aDataObj,
+ MightNeedToFlush* = nullptr);
+ static nsresult GetDataFromDataObject(IDataObject* aDataObject, UINT anIndex,
+ nsIWidget* aWindow,
+ nsITransferable* aTransferable);
+ static nsresult GetNativeDataOffClipboard(nsIWidget* aWindow, UINT aIndex,
+ UINT aFormat, void** aData,
+ uint32_t* aLen);
+ static nsresult GetNativeDataOffClipboard(IDataObject* aDataObject,
+ UINT aIndex, UINT aFormat,
+ const char* aMIMEImageFormat,
+ void** aData, uint32_t* aLen);
+ static nsresult GetGlobalData(HGLOBAL aHGBL, void** aData, uint32_t* aLen);
+
+ // This function returns the internal Windows clipboard format identifier
+ // for a given Mime string. The default is to map kHTMLMime ("text/html")
+ // to the clipboard format CF_HTML ("HTLM Format"), but it can also be
+ // registered as clipboard format "text/html" to support previous versions
+ // of Gecko.
+ static UINT GetFormat(const char* aMimeStr, bool aMapHTMLMime = true);
+
+ static UINT GetClipboardFileDescriptorFormatA();
+ static UINT GetClipboardFileDescriptorFormatW();
+ static UINT GetHtmlClipboardFormat();
+ static UINT GetCustomClipboardFormat();
+
+ protected:
+ // @param aDataObject must be non-nullptr.
+ static HRESULT FillSTGMedium(IDataObject* aDataObject, UINT aFormat,
+ LPFORMATETC pFE, LPSTGMEDIUM pSTM, DWORD aTymed);
+
+ // Implement the native clipboard behavior.
+ NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) override;
+ nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) override;
+ mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber(
+ int32_t aWhichClipboard) override;
+ mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override;
+
+ static bool IsInternetShortcut(const nsAString& inFileName);
+ static bool FindURLFromLocalFile(IDataObject* inDataObject, UINT inIndex,
+ void** outData, uint32_t* outDataLen);
+ static bool FindURLFromNativeURL(IDataObject* inDataObject, UINT inIndex,
+ void** outData, uint32_t* outDataLen);
+ static bool FindUnicodeFromPlainText(IDataObject* inDataObject, UINT inIndex,
+ void** outData, uint32_t* outDataLen);
+ static bool FindPlatformHTML(IDataObject* inDataObject, UINT inIndex,
+ void** outData, uint32_t* outStartOfData,
+ uint32_t* outDataLen);
+
+ static void ResolveShortcut(nsIFile* inFileName, nsACString& outURL);
+ static nsresult GetTempFilePath(const nsAString& aFileName,
+ nsAString& aFilePath);
+ static nsresult SaveStorageOrStream(IDataObject* aDataObject, UINT aIndex,
+ const nsAString& aFileName);
+
+ nsIWidget* mWindow;
+};
+
+#define SET_FORMATETC(fe, cf, td, asp, li, med) \
+ { \
+ (fe).cfFormat = cf; \
+ (fe).ptd = td; \
+ (fe).dwAspect = asp; \
+ (fe).lindex = li; \
+ (fe).tymed = med; \
+ }
+
+#endif // nsClipboard_h__
diff --git a/widget/windows/nsColorPicker.cpp b/widget/windows/nsColorPicker.cpp
new file mode 100644
index 0000000000..5074b620f5
--- /dev/null
+++ b/widget/windows/nsColorPicker.cpp
@@ -0,0 +1,202 @@
+/* -*- 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 "nsColorPicker.h"
+
+#include <shlwapi.h>
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AutoRestore.h"
+#include "nsIWidget.h"
+#include "nsString.h"
+#include "WidgetUtils.h"
+#include "WinUtils.h"
+#include "nsPIDOMWindow.h"
+
+using namespace mozilla::widget;
+
+namespace {
+static DWORD ColorStringToRGB(const nsAString& aColor) {
+ DWORD result = 0;
+
+ for (uint32_t i = 1; i < aColor.Length(); ++i) {
+ result *= 16;
+
+ char16_t c = aColor[i];
+ if (c >= '0' && c <= '9') {
+ result += c - '0';
+ } else if (c >= 'a' && c <= 'f') {
+ result += 10 + (c - 'a');
+ } else {
+ result += 10 + (c - 'A');
+ }
+ }
+
+ DWORD r = result & 0x00FF0000;
+ DWORD g = result & 0x0000FF00;
+ DWORD b = result & 0x000000FF;
+
+ r = r >> 16;
+ b = b << 16;
+
+ result = r | g | b;
+
+ return result;
+}
+
+static nsString ToHexString(BYTE n) {
+ nsString result;
+ if (n <= 0x0F) {
+ result.Append('0');
+ }
+ result.AppendInt(n, 16);
+ return result;
+}
+
+static void BGRIntToRGBString(DWORD color, nsAString& aResult) {
+ BYTE r = GetRValue(color);
+ BYTE g = GetGValue(color);
+ BYTE b = GetBValue(color);
+
+ aResult.Assign('#');
+ aResult.Append(ToHexString(r));
+ aResult.Append(ToHexString(g));
+ aResult.Append(ToHexString(b));
+}
+} // namespace
+
+static AsyncColorChooser* gColorChooser;
+
+AsyncColorChooser::AsyncColorChooser(COLORREF aInitialColor,
+ const nsTArray<nsString>& aDefaultColors,
+ nsIWidget* aParentWidget,
+ nsIColorPickerShownCallback* aCallback)
+ : mozilla::Runnable("AsyncColorChooser"),
+ mInitialColor(aInitialColor),
+ mDefaultColors(aDefaultColors.Clone()),
+ mColor(aInitialColor),
+ mParentWidget(aParentWidget),
+ mCallback(aCallback) {}
+
+NS_IMETHODIMP
+AsyncColorChooser::Run() {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Color pickers can only be opened from main thread currently");
+
+ // Allow only one color picker to be opened at a time, to workaround bug
+ // 944737
+ if (!gColorChooser) {
+ mozilla::AutoRestore<AsyncColorChooser*> restoreColorChooser(gColorChooser);
+ gColorChooser = this;
+
+ ScopedRtlShimWindow shim(mParentWidget.get());
+
+ COLORREF customColors[16];
+ for (size_t i = 0; i < mozilla::ArrayLength(customColors); i++) {
+ if (i < mDefaultColors.Length()) {
+ customColors[i] = ColorStringToRGB(mDefaultColors[i]);
+ } else {
+ customColors[i] = 0x00FFFFFF;
+ }
+ }
+
+ CHOOSECOLOR options;
+ options.lStructSize = sizeof(options);
+ options.hwndOwner = shim.get();
+ options.Flags = CC_RGBINIT | CC_FULLOPEN | CC_ENABLEHOOK;
+ options.rgbResult = mInitialColor;
+ options.lpCustColors = customColors;
+ options.lpfnHook = HookProc;
+
+ mColor = ChooseColor(&options) ? options.rgbResult : mInitialColor;
+ } else {
+ NS_WARNING(
+ "Currently, it's not possible to open more than one color "
+ "picker at a time");
+ mColor = mInitialColor;
+ }
+
+ if (mCallback) {
+ nsAutoString colorStr;
+ BGRIntToRGBString(mColor, colorStr);
+ mCallback->Done(colorStr);
+ }
+
+ return NS_OK;
+}
+
+void AsyncColorChooser::Update(COLORREF aColor) {
+ if (mColor != aColor) {
+ mColor = aColor;
+
+ nsAutoString colorStr;
+ BGRIntToRGBString(mColor, colorStr);
+ mCallback->Update(colorStr);
+ }
+}
+
+/* static */ UINT_PTR CALLBACK AsyncColorChooser::HookProc(HWND aDialog,
+ UINT aMsg,
+ WPARAM aWParam,
+ LPARAM aLParam) {
+ if (!gColorChooser) {
+ return 0;
+ }
+
+ if (aMsg == WM_INITDIALOG) {
+ // "The default dialog box procedure processes the WM_INITDIALOG message
+ // before passing it to the hook procedure.
+ // For all other messages, the hook procedure receives the message first."
+ // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nc-commdlg-lpcchookproc
+ // "The dialog box procedure should return TRUE to direct the system to
+ // set the keyboard focus to the control specified by wParam."
+ // https://docs.microsoft.com/en-us/windows/win32/dlgbox/wm-initdialog
+ return 1;
+ }
+
+ if (aMsg == WM_CTLCOLORSTATIC) {
+ // The color picker does not expose a proper way to retrieve the current
+ // color, so we need to obtain it from the static control displaying the
+ // current color instead.
+ const int kCurrentColorBoxID = 709;
+ if ((HWND)aLParam == GetDlgItem(aDialog, kCurrentColorBoxID)) {
+ gColorChooser->Update(GetPixel((HDC)aWParam, 0, 0));
+ }
+ }
+
+ // Let the default dialog box procedure processes the message.
+ return 0;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIColorPicker
+
+nsColorPicker::nsColorPicker() {}
+
+nsColorPicker::~nsColorPicker() {}
+
+NS_IMPL_ISUPPORTS(nsColorPicker, nsIColorPicker)
+
+NS_IMETHODIMP
+nsColorPicker::Init(mozIDOMWindowProxy* parent, const nsAString& title,
+ const nsAString& aInitialColor,
+ const nsTArray<nsString>& aDefaultColors) {
+ MOZ_ASSERT(parent,
+ "Null parent passed to colorpicker, no color picker for you!");
+ mParentWidget =
+ WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(parent));
+ mInitialColor = ColorStringToRGB(aInitialColor);
+ mDefaultColors.Assign(aDefaultColors);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsColorPicker::Open(nsIColorPickerShownCallback* aCallback) {
+ NS_ENSURE_ARG(aCallback);
+ nsCOMPtr<nsIRunnable> event = new AsyncColorChooser(
+ mInitialColor, mDefaultColors, mParentWidget, aCallback);
+ return NS_DispatchToMainThread(event);
+}
diff --git a/widget/windows/nsColorPicker.h b/widget/windows/nsColorPicker.h
new file mode 100644
index 0000000000..ff34b9bd48
--- /dev/null
+++ b/widget/windows/nsColorPicker.h
@@ -0,0 +1,55 @@
+/* -*- 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/. */
+
+#ifndef nsColorPicker_h__
+#define nsColorPicker_h__
+
+#include <windows.h>
+#include <commdlg.h>
+
+#include "nsCOMPtr.h"
+#include "nsIColorPicker.h"
+#include "nsThreadUtils.h"
+
+class nsIWidget;
+
+class AsyncColorChooser : public mozilla::Runnable {
+ public:
+ AsyncColorChooser(COLORREF aInitialColor,
+ const nsTArray<nsString>& aDefaultColors,
+ nsIWidget* aParentWidget,
+ nsIColorPickerShownCallback* aCallback);
+ NS_IMETHOD Run() override;
+
+ private:
+ void Update(COLORREF aColor);
+
+ static UINT_PTR CALLBACK HookProc(HWND aDialog, UINT aMsg, WPARAM aWParam,
+ LPARAM aLParam);
+
+ COLORREF mInitialColor;
+ nsTArray<nsString> mDefaultColors;
+ COLORREF mColor;
+ nsCOMPtr<nsIWidget> mParentWidget;
+ nsCOMPtr<nsIColorPickerShownCallback> mCallback;
+};
+
+class nsColorPicker : public nsIColorPicker {
+ virtual ~nsColorPicker();
+
+ public:
+ nsColorPicker();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOLORPICKER
+
+ private:
+ COLORREF mInitialColor;
+ nsTArray<nsString> mDefaultColors;
+ nsCOMPtr<nsIWidget> mParentWidget;
+};
+
+#endif // nsColorPicker_h__
diff --git a/widget/windows/nsDataObj.cpp b/widget/windows/nsDataObj.cpp
new file mode 100644
index 0000000000..88a2a2ad09
--- /dev/null
+++ b/widget/windows/nsDataObj.cpp
@@ -0,0 +1,2276 @@
+/* -*- 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 "mozilla/ArrayUtils.h"
+#include "mozilla/TextUtils.h"
+
+#include <ole2.h>
+#include <shlobj.h>
+
+#include "nsComponentManagerUtils.h"
+#include "nsDataObj.h"
+#include "nsArrayUtils.h"
+#include "nsClipboard.h"
+#include "nsReadableUtils.h"
+#include "nsICookieJarSettings.h"
+#include "nsIHttpChannel.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+#include "IEnumFE.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsString.h"
+#include "nsCRT.h"
+#include "nsPrintfCString.h"
+#include "nsIStringBundle.h"
+#include "nsEscape.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "mozilla/Components.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/Unused.h"
+#include "nsProxyRelease.h"
+#include "nsIObserverService.h"
+#include "nsIOutputStream.h"
+#include "nscore.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsITimer.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Preferences.h"
+#include "nsContentUtils.h"
+#include "nsIPrincipal.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsMimeTypes.h"
+#include "nsIMIMEService.h"
+#include "imgIEncoder.h"
+#include "imgITools.h"
+#include "WinUtils.h"
+#include "nsLocalFile.h"
+
+#include "mozilla/LazyIdleThread.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::glue;
+using namespace mozilla::widget;
+
+#define BFH_LENGTH 14
+#define DEFAULT_THREAD_TIMEOUT_MS 30000
+
+//-----------------------------------------------------------------------------
+// CStreamBase implementation
+nsDataObj::CStreamBase::CStreamBase() : mStreamRead(0) {}
+
+//-----------------------------------------------------------------------------
+nsDataObj::CStreamBase::~CStreamBase() {}
+
+NS_IMPL_ISUPPORTS(nsDataObj::CStream, nsIStreamListener)
+
+//-----------------------------------------------------------------------------
+// CStream implementation
+nsDataObj::CStream::CStream() : mChannelRead(false) {}
+
+//-----------------------------------------------------------------------------
+nsDataObj::CStream::~CStream() {}
+
+//-----------------------------------------------------------------------------
+// helper - initializes the stream
+nsresult nsDataObj::CStream::Init(nsIURI* pSourceURI,
+ nsContentPolicyType aContentPolicyType,
+ nsIPrincipal* aRequestingPrincipal,
+ nsICookieJarSettings* aCookieJarSettings,
+ nsIReferrerInfo* aReferrerInfo) {
+ // we can not create a channel without a requestingPrincipal
+ if (!aRequestingPrincipal) {
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv;
+ rv = NS_NewChannel(getter_AddRefs(mChannel), pSourceURI, aRequestingPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ aContentPolicyType, aCookieJarSettings,
+ nullptr, // PerformanceStorage
+ nullptr, // loadGroup
+ nullptr, // aCallbacks
+ nsIRequest::LOAD_FROM_CACHE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel)) {
+ rv = httpChannel->SetReferrerInfo(aReferrerInfo);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+
+ rv = mChannel->AsyncOpen(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// IUnknown's QueryInterface, nsISupport's AddRef and Release are shared by
+// IUnknown and nsIStreamListener.
+STDMETHODIMP nsDataObj::CStream::QueryInterface(REFIID refiid,
+ void** ppvResult) {
+ *ppvResult = nullptr;
+ if (IID_IUnknown == refiid || refiid == IID_IStream)
+
+ {
+ *ppvResult = this;
+ }
+
+ if (nullptr != *ppvResult) {
+ ((LPUNKNOWN)*ppvResult)->AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+// nsIStreamListener implementation
+NS_IMETHODIMP
+nsDataObj::CStream::OnDataAvailable(
+ nsIRequest* aRequest, nsIInputStream* aInputStream,
+ uint64_t aOffset, // offset within the stream
+ uint32_t aCount) // bytes available on this call
+{
+ // If we've been asked to read zero bytes, call `Read` once, just to ensure
+ // any side-effects take place, and return immediately.
+ if (aCount == 0) {
+ char buffer[1] = {0};
+ uint32_t bytesReadByCall = 0;
+ nsresult rv = aInputStream->Read(buffer, 0, &bytesReadByCall);
+ MOZ_ASSERT(bytesReadByCall == 0);
+ return rv;
+ }
+
+ // Extend the write buffer for the incoming data.
+ size_t oldLength = mChannelData.Length();
+ char* buffer =
+ reinterpret_cast<char*>(mChannelData.AppendElements(aCount, fallible));
+ if (!buffer) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ MOZ_ASSERT(mChannelData.Length() == (aOffset + aCount),
+ "stream length mismatch w/write buffer");
+
+ // Read() may not return aCount on a single call, so loop until we've
+ // accumulated all the data OnDataAvailable has promised.
+ uint32_t bytesRead = 0;
+ while (bytesRead < aCount) {
+ uint32_t bytesReadByCall = 0;
+ nsresult rv = aInputStream->Read(buffer + bytesRead, aCount - bytesRead,
+ &bytesReadByCall);
+ bytesRead += bytesReadByCall;
+
+ if (bytesReadByCall == 0) {
+ // A `bytesReadByCall` of zero indicates EOF without failure... but we
+ // were promised `aCount` elements and haven't gotten them. Return a
+ // generic failure.
+ rv = NS_ERROR_FAILURE;
+ }
+
+ if (NS_FAILED(rv)) {
+ // Drop any trailing uninitialized elements before erroring out.
+ mChannelData.RemoveElementsAt(oldLength + bytesRead, aCount - bytesRead);
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDataObj::CStream::OnStartRequest(nsIRequest* aRequest) {
+ mChannelResult = NS_OK;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDataObj::CStream::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ mChannelRead = true;
+ mChannelResult = aStatusCode;
+ return NS_OK;
+}
+
+// Pumps thread messages while waiting for the async listener operation to
+// complete. Failing this call will fail the stream incall from Windows
+// and cancel the operation.
+nsresult nsDataObj::CStream::WaitForCompletion() {
+ // We are guaranteed OnStopRequest will get called, so this should be ok.
+ SpinEventLoopUntil("widget:nsDataObj::CStream::WaitForCompletion"_ns,
+ [&]() { return mChannelRead; });
+
+ if (!mChannelData.Length()) mChannelResult = NS_ERROR_FAILURE;
+
+ return mChannelResult;
+}
+
+//-----------------------------------------------------------------------------
+// IStream
+STDMETHODIMP nsDataObj::CStreamBase::Clone(IStream** ppStream) {
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::Commit(DWORD dwFrags) { return E_NOTIMPL; }
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::CopyTo(IStream* pDestStream,
+ ULARGE_INTEGER nBytesToCopy,
+ ULARGE_INTEGER* nBytesRead,
+ ULARGE_INTEGER* nBytesWritten) {
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::LockRegion(ULARGE_INTEGER nStart,
+ ULARGE_INTEGER nBytes,
+ DWORD dwFlags) {
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStream::Read(void* pvBuffer, ULONG nBytesToRead,
+ ULONG* nBytesRead) {
+ // Wait for the write into our buffer to complete via the stream listener.
+ // We can't respond to this by saying "call us back later".
+ if (NS_FAILED(WaitForCompletion())) return E_FAIL;
+
+ // Bytes left for Windows to read out of our buffer
+ ULONG bytesLeft = mChannelData.Length() - mStreamRead;
+ // Let Windows know what we will hand back, usually this is the entire buffer
+ *nBytesRead = std::min(bytesLeft, nBytesToRead);
+ // Copy the buffer data over
+ memcpy(pvBuffer, ((char*)mChannelData.Elements() + mStreamRead), *nBytesRead);
+ // Update our bytes read tracking
+ mStreamRead += *nBytesRead;
+ return S_OK;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::Revert(void) { return E_NOTIMPL; }
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::Seek(LARGE_INTEGER nMove, DWORD dwOrigin,
+ ULARGE_INTEGER* nNewPos) {
+ if (nNewPos == nullptr) return STG_E_INVALIDPOINTER;
+
+ if (nMove.LowPart == 0 && nMove.HighPart == 0 &&
+ (dwOrigin == STREAM_SEEK_SET || dwOrigin == STREAM_SEEK_CUR)) {
+ nNewPos->LowPart = 0;
+ nNewPos->HighPart = 0;
+ return S_OK;
+ }
+
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::SetSize(ULARGE_INTEGER nNewSize) {
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStream::Stat(STATSTG* statstg, DWORD dwFlags) {
+ if (statstg == nullptr) return STG_E_INVALIDPOINTER;
+
+ if (!mChannel || NS_FAILED(WaitForCompletion())) return E_FAIL;
+
+ memset((void*)statstg, 0, sizeof(STATSTG));
+
+ if (dwFlags != STATFLAG_NONAME) {
+ nsCOMPtr<nsIURI> sourceURI;
+ if (NS_FAILED(mChannel->GetURI(getter_AddRefs(sourceURI)))) {
+ return E_FAIL;
+ }
+
+ nsAutoCString strFileName;
+ nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI);
+ sourceURL->GetFileName(strFileName);
+
+ if (strFileName.IsEmpty()) return E_FAIL;
+
+ NS_UnescapeURL(strFileName);
+ NS_ConvertUTF8toUTF16 wideFileName(strFileName);
+
+ uint32_t nMaxNameLength = (wideFileName.Length() * 2) + 2;
+ void* retBuf = CoTaskMemAlloc(nMaxNameLength); // freed by caller
+ if (!retBuf) return STG_E_INSUFFICIENTMEMORY;
+
+ ZeroMemory(retBuf, nMaxNameLength);
+ memcpy(retBuf, wideFileName.get(), wideFileName.Length() * 2);
+ statstg->pwcsName = (LPOLESTR)retBuf;
+ }
+
+ SYSTEMTIME st;
+
+ statstg->type = STGTY_STREAM;
+
+ GetSystemTime(&st);
+ SystemTimeToFileTime((const SYSTEMTIME*)&st, (LPFILETIME)&statstg->mtime);
+ statstg->ctime = statstg->atime = statstg->mtime;
+
+ statstg->cbSize.QuadPart = mChannelData.Length();
+ statstg->grfMode = STGM_READ;
+ statstg->grfLocksSupported = LOCK_ONLYONCE;
+ statstg->clsid = CLSID_NULL;
+
+ return S_OK;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::UnlockRegion(ULARGE_INTEGER nStart,
+ ULARGE_INTEGER nBytes,
+ DWORD dwFlags) {
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CStreamBase::Write(const void* pvBuffer,
+ ULONG nBytesToRead,
+ ULONG* nBytesRead) {
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------------------------------
+HRESULT nsDataObj::CreateStream(IStream** outStream) {
+ NS_ENSURE_TRUE(outStream, E_INVALIDARG);
+
+ nsresult rv = NS_ERROR_FAILURE;
+ nsAutoString wideFileName;
+ nsCOMPtr<nsIURI> sourceURI;
+ HRESULT res;
+
+ res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
+ if (FAILED(res)) return res;
+
+ nsDataObj::CStream* pStream = new nsDataObj::CStream();
+ NS_ENSURE_TRUE(pStream, E_OUTOFMEMORY);
+
+ pStream->AddRef();
+
+ // query the requestingPrincipal from the transferable and add it to the new
+ // channel
+ nsCOMPtr<nsIPrincipal> requestingPrincipal =
+ mTransferable->GetRequestingPrincipal();
+ MOZ_ASSERT(requestingPrincipal, "can not create channel without a principal");
+
+ // Note that the cookieJarSettings could be null if the data object is for the
+ // image copy. We will fix this in Bug 1690532.
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ mTransferable->GetCookieJarSettings();
+
+ // The referrer is optional.
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = mTransferable->GetReferrerInfo();
+
+ nsContentPolicyType contentPolicyType = mTransferable->GetContentPolicyType();
+ rv = pStream->Init(sourceURI, contentPolicyType, requestingPrincipal,
+ cookieJarSettings, referrerInfo);
+ if (NS_FAILED(rv)) {
+ pStream->Release();
+ return E_FAIL;
+ }
+ *outStream = pStream;
+
+ return S_OK;
+}
+
+//-----------------------------------------------------------------------------
+// AutoCloseEvent implementation
+nsDataObj::AutoCloseEvent::AutoCloseEvent()
+ : mEvent(::CreateEventW(nullptr, TRUE, FALSE, nullptr)) {}
+
+bool nsDataObj::AutoCloseEvent::IsInited() const { return !!mEvent; }
+
+void nsDataObj::AutoCloseEvent::Signal() const { ::SetEvent(mEvent); }
+
+DWORD nsDataObj::AutoCloseEvent::Wait(DWORD aMillisec) const {
+ return ::WaitForSingleObject(mEvent, aMillisec);
+}
+
+//-----------------------------------------------------------------------------
+// AutoSetEvent implementation
+nsDataObj::AutoSetEvent::AutoSetEvent(NotNull<AutoCloseEvent*> aEvent)
+ : mEvent(aEvent) {}
+
+nsDataObj::AutoSetEvent::~AutoSetEvent() { Signal(); }
+
+void nsDataObj::AutoSetEvent::Signal() const { mEvent->Signal(); }
+
+bool nsDataObj::AutoSetEvent::IsWaiting() const {
+ return mEvent->Wait(0) == WAIT_TIMEOUT;
+}
+
+//-----------------------------------------------------------------------------
+// CMemStream implementation
+Win32SRWLock nsDataObj::CMemStream::mLock;
+
+//-----------------------------------------------------------------------------
+nsDataObj::CMemStream::CMemStream(nsHGLOBAL aGlobalMem, uint32_t aTotalLength,
+ already_AddRefed<AutoCloseEvent> aEvent)
+ : mGlobalMem(aGlobalMem), mEvent(aEvent), mTotalLength(aTotalLength) {
+ ::CoCreateFreeThreadedMarshaler(this, getter_AddRefs(mMarshaler));
+}
+
+//-----------------------------------------------------------------------------
+nsDataObj::CMemStream::~CMemStream() {}
+
+//-----------------------------------------------------------------------------
+// IUnknown
+STDMETHODIMP nsDataObj::CMemStream::QueryInterface(REFIID refiid,
+ void** ppvResult) {
+ *ppvResult = nullptr;
+ if (refiid == IID_IUnknown || refiid == IID_IStream ||
+ refiid == IID_IAgileObject) {
+ *ppvResult = this;
+ } else if (refiid == IID_IMarshal && mMarshaler) {
+ return mMarshaler->QueryInterface(refiid, ppvResult);
+ }
+
+ if (nullptr != *ppvResult) {
+ ((LPUNKNOWN)*ppvResult)->AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+void nsDataObj::CMemStream::WaitForCompletion() {
+ if (!mEvent) {
+ // We are not waiting for obtaining the icon cache.
+ return;
+ }
+ if (!NS_IsMainThread()) {
+ mEvent->Wait(INFINITE);
+ } else {
+ // We should not block the main thread.
+ mEvent->Signal();
+ }
+ // mEvent will always be in the signaled state here.
+}
+
+//-----------------------------------------------------------------------------
+// IStream
+STDMETHODIMP nsDataObj::CMemStream::Read(void* pvBuffer, ULONG nBytesToRead,
+ ULONG* nBytesRead) {
+ // Wait until the event is signaled.
+ WaitForCompletion();
+
+ AutoExclusiveLock lock(mLock);
+ char* contents = reinterpret_cast<char*>(GlobalLock(mGlobalMem.get()));
+ if (!contents) {
+ return E_OUTOFMEMORY;
+ }
+
+ // Bytes left for Windows to read out of our buffer
+ ULONG bytesLeft = mTotalLength - mStreamRead;
+ // Let Windows know what we will hand back, usually this is the entire buffer
+ *nBytesRead = std::min(bytesLeft, nBytesToRead);
+ // Copy the buffer data over
+ memcpy(pvBuffer, contents + mStreamRead, *nBytesRead);
+ // Update our bytes read tracking
+ mStreamRead += *nBytesRead;
+
+ GlobalUnlock(mGlobalMem.get());
+ return S_OK;
+}
+
+//-----------------------------------------------------------------------------
+STDMETHODIMP nsDataObj::CMemStream::Stat(STATSTG* statstg, DWORD dwFlags) {
+ if (statstg == nullptr) return STG_E_INVALIDPOINTER;
+
+ memset((void*)statstg, 0, sizeof(STATSTG));
+
+ if (dwFlags != STATFLAG_NONAME) {
+ constexpr size_t kMaxNameLength = sizeof(wchar_t);
+ void* retBuf = CoTaskMemAlloc(kMaxNameLength); // freed by caller
+ if (!retBuf) return STG_E_INSUFFICIENTMEMORY;
+
+ ZeroMemory(retBuf, kMaxNameLength);
+ statstg->pwcsName = (LPOLESTR)retBuf;
+ }
+
+ SYSTEMTIME st;
+
+ statstg->type = STGTY_STREAM;
+
+ GetSystemTime(&st);
+ SystemTimeToFileTime((const SYSTEMTIME*)&st, (LPFILETIME)&statstg->mtime);
+ statstg->ctime = statstg->atime = statstg->mtime;
+
+ statstg->cbSize.QuadPart = mTotalLength;
+ statstg->grfMode = STGM_READ;
+ statstg->grfLocksSupported = LOCK_ONLYONCE;
+ statstg->clsid = CLSID_NULL;
+
+ return S_OK;
+}
+
+/*
+ * Class nsDataObj
+ */
+
+//-----------------------------------------------------
+// construction
+//-----------------------------------------------------
+nsDataObj::nsDataObj(nsIURI* uri)
+ : m_cRef(0),
+ mTransferable(nullptr),
+ mIsAsyncMode(FALSE),
+ mIsInOperation(FALSE) {
+ mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "nsDataObj",
+ LazyIdleThread::ManualShutdown);
+ m_enumFE = new CEnumFormatEtc();
+ m_enumFE->AddRef();
+
+ if (uri) {
+ // A URI was obtained, so pass this through to the DataObject
+ // so it can create a SourceURL for CF_HTML flavour
+ uri->GetSpec(mSourceURL);
+ }
+}
+//-----------------------------------------------------
+// destruction
+//-----------------------------------------------------
+nsDataObj::~nsDataObj() {
+ NS_IF_RELEASE(mTransferable);
+
+ mDataFlavors.Clear();
+
+ m_enumFE->Release();
+
+ // Free arbitrary system formats
+ for (uint32_t idx = 0; idx < mDataEntryList.Length(); idx++) {
+ CoTaskMemFree(mDataEntryList[idx]->fe.ptd);
+ ReleaseStgMedium(&mDataEntryList[idx]->stgm);
+ CoTaskMemFree(mDataEntryList[idx]);
+ }
+}
+
+//-----------------------------------------------------
+// IUnknown interface methods - see inknown.h for documentation
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::QueryInterface(REFIID riid, void** ppv) {
+ *ppv = nullptr;
+
+ if ((IID_IUnknown == riid) || (IID_IDataObject == riid)) {
+ *ppv = this;
+ AddRef();
+ return S_OK;
+ } else if (IID_IDataObjectAsyncCapability == riid) {
+ *ppv = static_cast<IDataObjectAsyncCapability*>(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP_(ULONG) nsDataObj::AddRef() {
+ ++m_cRef;
+ NS_LOG_ADDREF(this, m_cRef, "nsDataObj", sizeof(*this));
+
+ // When the first reference is taken, hold our own internal reference.
+ if (m_cRef == 1) {
+ mKeepAlive = this;
+ }
+
+ return m_cRef;
+}
+
+namespace {
+class RemoveTempFileHelper final : public nsIObserver, public nsINamed {
+ public:
+ explicit RemoveTempFileHelper(nsIFile* aTempFile) : mTempFile(aTempFile) {
+ MOZ_ASSERT(mTempFile);
+ }
+
+ // The attach method is seperate from the constructor as we may be addref-ing
+ // ourself, and we want to be sure someone has a strong reference to us.
+ void Attach() {
+ // We need to listen to both the xpcom shutdown message and our timer, and
+ // fire when the first of either of these two messages is received.
+ nsresult rv;
+ rv = NS_NewTimerWithObserver(getter_AddRefs(mTimer), this, 500,
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService("@mozilla.org/observer-service;1");
+ if (NS_WARN_IF(!observerService)) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ return;
+ }
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ }
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSINAMED
+
+ private:
+ ~RemoveTempFileHelper() {
+ if (mTempFile) {
+ mTempFile->Remove(false);
+ }
+ }
+
+ nsCOMPtr<nsIFile> mTempFile;
+ nsCOMPtr<nsITimer> mTimer;
+};
+
+NS_IMPL_ISUPPORTS(RemoveTempFileHelper, nsIObserver, nsINamed);
+
+NS_IMETHODIMP
+RemoveTempFileHelper::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ // Let's be careful and make sure that we don't die immediately
+ RefPtr<RemoveTempFileHelper> grip = this;
+
+ // Make sure that we aren't called again by destroying references to ourself.
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService("@mozilla.org/observer-service;1");
+ if (observerService) {
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ // Remove the tempfile
+ if (mTempFile) {
+ mTempFile->Remove(false);
+ mTempFile = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RemoveTempFileHelper::GetName(nsACString& aName) {
+ aName.AssignLiteral("RemoveTempFileHelper");
+ return NS_OK;
+}
+} // namespace
+
+//-----------------------------------------------------
+STDMETHODIMP_(ULONG) nsDataObj::Release() {
+ --m_cRef;
+
+ NS_LOG_RELEASE(this, m_cRef, "nsDataObj");
+
+ // If we hold the last reference, submit release of it to the main thread.
+ if (m_cRef == 1 && mKeepAlive) {
+ NS_ReleaseOnMainThread("nsDataObj release", mKeepAlive.forget(), true);
+ }
+
+ if (0 != m_cRef) return m_cRef;
+
+ // We have released our last ref on this object and need to delete the
+ // temp file. External app acting as drop target may still need to open the
+ // temp file. Addref a timer so it can delay deleting file and destroying
+ // this object.
+ if (mCachedTempFile) {
+ RefPtr<RemoveTempFileHelper> helper =
+ new RemoveTempFileHelper(mCachedTempFile);
+ mCachedTempFile = nullptr;
+ helper->Attach();
+ }
+
+ // In case the destructor ever AddRef/Releases, ensure we don't delete twice
+ // or take mKeepAlive as another reference.
+ m_cRef = 1;
+
+ delete this;
+
+ return 0;
+}
+
+//-----------------------------------------------------
+BOOL nsDataObj::FormatsMatch(const FORMATETC& source,
+ const FORMATETC& target) const {
+ if ((source.cfFormat == target.cfFormat) &&
+ (source.dwAspect & target.dwAspect) && (source.tymed & target.tymed)) {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
+
+//-----------------------------------------------------
+// IDataObject methods
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::GetData(LPFORMATETC aFormat, LPSTGMEDIUM pSTM) {
+ if (!mTransferable) return DV_E_FORMATETC;
+
+ // Hold an extra reference in case we end up spinning the event loop.
+ RefPtr<nsDataObj> keepAliveDuringGetData(this);
+
+ uint32_t dfInx = 0;
+
+ static CLIPFORMAT fileDescriptorFlavorA =
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA);
+ static CLIPFORMAT fileDescriptorFlavorW =
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW);
+ static CLIPFORMAT uniformResourceLocatorA =
+ ::RegisterClipboardFormat(CFSTR_INETURLA);
+ static CLIPFORMAT uniformResourceLocatorW =
+ ::RegisterClipboardFormat(CFSTR_INETURLW);
+ static CLIPFORMAT fileFlavor = ::RegisterClipboardFormat(CFSTR_FILECONTENTS);
+ static CLIPFORMAT PreferredDropEffect =
+ ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT);
+
+ // Arbitrary system formats are used for image feedback during drag
+ // and drop. We are responsible for storing these internally during
+ // drag operations.
+ LPDATAENTRY pde;
+ if (LookupArbitraryFormat(aFormat, &pde, FALSE)) {
+ return CopyMediumData(pSTM, &pde->stgm, aFormat, FALSE) ? S_OK
+ : E_UNEXPECTED;
+ }
+
+ // Firefox internal formats
+ ULONG count;
+ FORMATETC fe;
+ m_enumFE->Reset();
+ while (NOERROR == m_enumFE->Next(1, &fe, &count) &&
+ dfInx < mDataFlavors.Length()) {
+ nsCString& df = mDataFlavors.ElementAt(dfInx);
+ if (FormatsMatch(fe, *aFormat)) {
+ pSTM->pUnkForRelease =
+ nullptr; // caller is responsible for deleting this data
+ CLIPFORMAT format = aFormat->cfFormat;
+ switch (format) {
+ // Someone is asking for plain or unicode text
+ case CF_TEXT:
+ case CF_UNICODETEXT:
+ return GetText(df, *aFormat, *pSTM);
+
+ // Some 3rd party apps that receive drag and drop files from the browser
+ // window require support for this.
+ case CF_HDROP:
+ return GetFile(*aFormat, *pSTM);
+
+ // Someone is asking for an image
+ case CF_DIBV5:
+ case CF_DIB:
+ return GetDib(df, *aFormat, *pSTM);
+
+ default:
+ if (format == fileDescriptorFlavorA)
+ return GetFileDescriptor(*aFormat, *pSTM, false);
+ if (format == fileDescriptorFlavorW)
+ return GetFileDescriptor(*aFormat, *pSTM, true);
+ if (format == uniformResourceLocatorA)
+ return GetUniformResourceLocator(*aFormat, *pSTM, false);
+ if (format == uniformResourceLocatorW)
+ return GetUniformResourceLocator(*aFormat, *pSTM, true);
+ if (format == fileFlavor) return GetFileContents(*aFormat, *pSTM);
+ if (format == PreferredDropEffect)
+ return GetPreferredDropEffect(*aFormat, *pSTM);
+ // MOZ_LOG(gWindowsLog, LogLevel::Info,
+ // ("***** nsDataObj::GetData - Unknown format %u\n", format));
+ return GetText(df, *aFormat, *pSTM);
+ } // switch
+ } // if
+ dfInx++;
+ } // while
+
+ return DATA_E_FORMATETC;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::GetDataHere(LPFORMATETC pFE, LPSTGMEDIUM pSTM) {
+ return E_FAIL;
+}
+
+//-----------------------------------------------------
+// Other objects querying to see if we support a
+// particular format
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::QueryGetData(LPFORMATETC pFE) {
+ // Arbitrary system formats are used for image feedback during drag
+ // and drop. We are responsible for storing these internally during
+ // drag operations.
+ LPDATAENTRY pde;
+ if (LookupArbitraryFormat(pFE, &pde, FALSE)) return S_OK;
+
+ // Firefox internal formats
+ ULONG count;
+ FORMATETC fe;
+ m_enumFE->Reset();
+ while (NOERROR == m_enumFE->Next(1, &fe, &count)) {
+ if (fe.cfFormat == pFE->cfFormat) {
+ return S_OK;
+ }
+ }
+ return E_FAIL;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::GetCanonicalFormatEtc(LPFORMATETC pFEIn,
+ LPFORMATETC pFEOut) {
+ return E_NOTIMPL;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::SetData(LPFORMATETC aFormat, LPSTGMEDIUM aMedium,
+ BOOL shouldRel) {
+ // Arbitrary system formats are used for image feedback during drag
+ // and drop. We are responsible for storing these internally during
+ // drag operations.
+ LPDATAENTRY pde;
+ if (LookupArbitraryFormat(aFormat, &pde, TRUE)) {
+ // Release the old data the lookup handed us for this format. This
+ // may have been set in CopyMediumData when we originally stored the
+ // data.
+ if (pde->stgm.tymed) {
+ ReleaseStgMedium(&pde->stgm);
+ memset(&pde->stgm, 0, sizeof(STGMEDIUM));
+ }
+
+ bool result = true;
+ if (shouldRel) {
+ // If shouldRel is TRUE, the data object called owns the storage medium
+ // after the call returns. Store the incoming data in our data array for
+ // release when we are destroyed. This is the common case with arbitrary
+ // data from explorer.
+ pde->stgm = *aMedium;
+ } else {
+ // Copy the incoming data into our data array. (AFAICT, this never gets
+ // called with arbitrary formats for drag images.)
+ result = CopyMediumData(&pde->stgm, aMedium, aFormat, TRUE);
+ }
+ pde->fe.tymed = pde->stgm.tymed;
+
+ return result ? S_OK : DV_E_TYMED;
+ }
+
+ if (shouldRel) ReleaseStgMedium(aMedium);
+
+ return S_OK;
+}
+
+bool nsDataObj::LookupArbitraryFormat(FORMATETC* aFormat,
+ LPDATAENTRY* aDataEntry,
+ BOOL aAddorUpdate) {
+ *aDataEntry = nullptr;
+
+ if (aFormat->ptd != nullptr) return false;
+
+ // See if it's already in our list. If so return the data entry.
+ for (uint32_t idx = 0; idx < mDataEntryList.Length(); idx++) {
+ if (mDataEntryList[idx]->fe.cfFormat == aFormat->cfFormat &&
+ mDataEntryList[idx]->fe.dwAspect == aFormat->dwAspect &&
+ mDataEntryList[idx]->fe.lindex == aFormat->lindex) {
+ if (aAddorUpdate || (mDataEntryList[idx]->fe.tymed & aFormat->tymed)) {
+ // If the caller requests we update, or if the
+ // medium type matches, return the entry.
+ *aDataEntry = mDataEntryList[idx];
+ return true;
+ } else {
+ // Medium does not match, not found.
+ return false;
+ }
+ }
+ }
+
+ if (!aAddorUpdate) return false;
+
+ // Add another entry to mDataEntryList
+ LPDATAENTRY dataEntry = (LPDATAENTRY)CoTaskMemAlloc(sizeof(DATAENTRY));
+ if (!dataEntry) return false;
+
+ dataEntry->fe = *aFormat;
+ *aDataEntry = dataEntry;
+ memset(&dataEntry->stgm, 0, sizeof(STGMEDIUM));
+
+ // Add this to our IEnumFORMATETC impl. so we can return it when
+ // it's requested.
+ m_enumFE->AddFormatEtc(aFormat);
+
+ // Store a copy internally in the arbitrary formats array.
+ mDataEntryList.AppendElement(dataEntry);
+
+ return true;
+}
+
+bool nsDataObj::CopyMediumData(STGMEDIUM* aMediumDst, STGMEDIUM* aMediumSrc,
+ LPFORMATETC aFormat, BOOL aSetData) {
+ STGMEDIUM stgmOut = *aMediumSrc;
+
+ switch (stgmOut.tymed) {
+ case TYMED_ISTREAM:
+ stgmOut.pstm->AddRef();
+ break;
+ case TYMED_ISTORAGE:
+ stgmOut.pstg->AddRef();
+ break;
+ case TYMED_HGLOBAL:
+ if (!aMediumSrc->pUnkForRelease) {
+ if (aSetData) {
+ if (aMediumSrc->tymed != TYMED_HGLOBAL) return false;
+ stgmOut.hGlobal =
+ OleDuplicateData(aMediumSrc->hGlobal, aFormat->cfFormat, 0);
+ if (!stgmOut.hGlobal) return false;
+ } else {
+ // We are returning this data from LookupArbitraryFormat, indicate to
+ // the shell we hold it and will free it.
+ stgmOut.pUnkForRelease = static_cast<IDataObject*>(this);
+ }
+ }
+ break;
+ default:
+ return false;
+ }
+
+ if (stgmOut.pUnkForRelease) stgmOut.pUnkForRelease->AddRef();
+
+ *aMediumDst = stgmOut;
+
+ return true;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::EnumFormatEtc(DWORD dwDir, LPENUMFORMATETC* ppEnum) {
+ switch (dwDir) {
+ case DATADIR_GET:
+ m_enumFE->Clone(ppEnum);
+ break;
+ case DATADIR_SET:
+ // fall through
+ default:
+ *ppEnum = nullptr;
+ } // switch
+
+ if (nullptr == *ppEnum) return E_FAIL;
+
+ (*ppEnum)->Reset();
+ // Clone already AddRefed the result so don't addref it again.
+ return NOERROR;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::DAdvise(LPFORMATETC pFE, DWORD dwFlags,
+ LPADVISESINK pIAdviseSink, DWORD* pdwConn) {
+ return OLE_E_ADVISENOTSUPPORTED;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::DUnadvise(DWORD dwConn) {
+ return OLE_E_ADVISENOTSUPPORTED;
+}
+
+//-----------------------------------------------------
+STDMETHODIMP nsDataObj::EnumDAdvise(LPENUMSTATDATA* ppEnum) {
+ return OLE_E_ADVISENOTSUPPORTED;
+}
+
+// IDataObjectAsyncCapability methods
+STDMETHODIMP nsDataObj::EndOperation(HRESULT hResult, IBindCtx* pbcReserved,
+ DWORD dwEffects) {
+ mIsInOperation = FALSE;
+ return S_OK;
+}
+
+STDMETHODIMP nsDataObj::GetAsyncMode(BOOL* pfIsOpAsync) {
+ *pfIsOpAsync = mIsAsyncMode;
+
+ return S_OK;
+}
+
+STDMETHODIMP nsDataObj::InOperation(BOOL* pfInAsyncOp) {
+ *pfInAsyncOp = mIsInOperation;
+
+ return S_OK;
+}
+
+STDMETHODIMP nsDataObj::SetAsyncMode(BOOL fDoOpAsync) {
+ mIsAsyncMode = fDoOpAsync;
+ return S_OK;
+}
+
+STDMETHODIMP nsDataObj::StartOperation(IBindCtx* pbcReserved) {
+ mIsInOperation = TRUE;
+ return S_OK;
+}
+
+//
+// GetDIB
+//
+// Someone is asking for a bitmap. The data in the transferable will be a
+// straight imgIContainer, so just QI it.
+//
+HRESULT
+nsDataObj::GetDib(const nsACString& inFlavor, FORMATETC& aFormat,
+ STGMEDIUM& aSTG) {
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ if (NS_FAILED(
+ mTransferable->GetTransferData(PromiseFlatCString(inFlavor).get(),
+ getter_AddRefs(genericDataWrapper)))) {
+ return E_FAIL;
+ }
+
+ nsCOMPtr<imgIContainer> image = do_QueryInterface(genericDataWrapper);
+ if (!image) {
+ return E_FAIL;
+ }
+
+ nsCOMPtr<imgITools> imgTools =
+ do_CreateInstance("@mozilla.org/image/tools;1");
+
+ nsAutoString options(u"bpp=32;"_ns);
+ if (aFormat.cfFormat == CF_DIBV5) {
+ options.AppendLiteral("version=5");
+ } else {
+ options.AppendLiteral("version=3");
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = imgTools->EncodeImage(image, nsLiteralCString(IMAGE_BMP),
+ options, getter_AddRefs(inputStream));
+ if (NS_FAILED(rv) || !inputStream) {
+ return E_FAIL;
+ }
+
+ nsCOMPtr<imgIEncoder> encoder = do_QueryInterface(inputStream);
+ if (!encoder) {
+ return E_FAIL;
+ }
+
+ uint32_t size = 0;
+ rv = encoder->GetImageBufferUsed(&size);
+ if (NS_FAILED(rv) || size <= BFH_LENGTH) {
+ return E_FAIL;
+ }
+
+ char* src = nullptr;
+ rv = encoder->GetImageBuffer(&src);
+ if (NS_FAILED(rv) || !src) {
+ return E_FAIL;
+ }
+
+ // We don't want the file header.
+ src += BFH_LENGTH;
+ size -= BFH_LENGTH;
+
+ HGLOBAL glob = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, size);
+ if (!glob) {
+ return E_FAIL;
+ }
+
+ char* dst = (char*)::GlobalLock(glob);
+ ::CopyMemory(dst, src, size);
+ ::GlobalUnlock(glob);
+
+ aSTG.hGlobal = glob;
+ aSTG.tymed = TYMED_HGLOBAL;
+ return S_OK;
+}
+
+//
+// GetFileDescriptor
+//
+
+HRESULT
+nsDataObj ::GetFileDescriptor(FORMATETC& aFE, STGMEDIUM& aSTG,
+ bool aIsUnicode) {
+ HRESULT res = S_OK;
+
+ // How we handle this depends on if we're dealing with an internet
+ // shortcut, since those are done under the covers.
+ if (IsFlavourPresent(kFilePromiseMime) || IsFlavourPresent(kFileMime)) {
+ if (aIsUnicode)
+ return GetFileDescriptor_IStreamW(aFE, aSTG);
+ else
+ return GetFileDescriptor_IStreamA(aFE, aSTG);
+ } else if (IsFlavourPresent(kURLMime)) {
+ if (aIsUnicode)
+ res = GetFileDescriptorInternetShortcutW(aFE, aSTG);
+ else
+ res = GetFileDescriptorInternetShortcutA(aFE, aSTG);
+ } else
+ NS_WARNING("Not yet implemented\n");
+
+ return res;
+} // GetFileDescriptor
+
+//
+HRESULT
+nsDataObj ::GetFileContents(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ HRESULT res = S_OK;
+
+ // How we handle this depends on if we're dealing with an internet
+ // shortcut, since those are done under the covers.
+ if (IsFlavourPresent(kFilePromiseMime) || IsFlavourPresent(kFileMime))
+ return GetFileContents_IStream(aFE, aSTG);
+ else if (IsFlavourPresent(kURLMime))
+ return GetFileContentsInternetShortcut(aFE, aSTG);
+ else
+ NS_WARNING("Not yet implemented\n");
+
+ return res;
+
+} // GetFileContents
+
+// Ensure that the supplied name doesn't have invalid characters.
+static void ValidateFilename(nsString& aFilename, bool isShortcut) {
+ nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
+ if (NS_WARN_IF(!mimeService)) {
+ aFilename.Truncate();
+ return;
+ }
+
+ uint32_t flags = nsIMIMEService::VALIDATE_SANITIZE_ONLY;
+ if (isShortcut) {
+ flags |= nsIMIMEService::VALIDATE_ALLOW_INVALID_FILENAMES;
+ }
+
+ nsAutoString outFilename;
+ mimeService->ValidateFileNameForSaving(aFilename, EmptyCString(), flags,
+ outFilename);
+ aFilename = outFilename;
+}
+
+//
+// Given a unicode string, convert it to a valid local charset filename
+// and append the .url extension to be used for a shortcut file.
+// This ensures that we do not cut MBCS characters in the middle.
+//
+// It would seem that this is more functionality suited to being in nsIFile.
+//
+static bool CreateURLFilenameFromTextA(nsAutoString& aText, char* aFilename) {
+ if (aText.IsEmpty()) {
+ return false;
+ }
+ aText.AppendLiteral(".url");
+ ValidateFilename(aText, true);
+ if (aText.IsEmpty()) {
+ return false;
+ }
+
+ // ValidateFilename should already be checking the filename length, but do
+ // an extra check to verify for the local code page that the converted text
+ // doesn't go over MAX_PATH and just return false if it does.
+ char defaultChar = '_';
+ int currLen = WideCharToMultiByte(CP_ACP, WC_COMPOSITECHECK | WC_DEFAULTCHAR,
+ aText.get(), -1, aFilename, MAX_PATH,
+ &defaultChar, nullptr);
+ return currLen != 0;
+}
+
+// Wide character version of CreateURLFilenameFromTextA
+static bool CreateURLFilenameFromTextW(nsAutoString& aText,
+ wchar_t* aFilename) {
+ if (aText.IsEmpty()) {
+ return false;
+ }
+ aText.AppendLiteral(".url");
+ ValidateFilename(aText, true);
+ if (aText.IsEmpty() || aText.Length() >= MAX_PATH) {
+ return false;
+ }
+
+ wcscpy(&aFilename[0], aText.get());
+ return true;
+}
+
+#define PAGEINFO_PROPERTIES "chrome://navigator/locale/pageInfo.properties"
+
+static bool GetLocalizedString(const char* aName, nsAString& aString) {
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (!stringService) return false;
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ nsresult rv = stringService->CreateBundle(PAGEINFO_PROPERTIES,
+ getter_AddRefs(stringBundle));
+ if (NS_FAILED(rv)) return false;
+
+ rv = stringBundle->GetStringFromName(aName, aString);
+ return NS_SUCCEEDED(rv);
+}
+
+//
+// GetFileDescriptorInternetShortcut
+//
+// Create the special format for an internet shortcut and build up the data
+// structures the shell is expecting.
+//
+HRESULT
+nsDataObj ::GetFileDescriptorInternetShortcutA(FORMATETC& aFE,
+ STGMEDIUM& aSTG) {
+ // get the title of the shortcut
+ nsAutoString title;
+ if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY;
+
+ HGLOBAL fileGroupDescHandle =
+ ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORA));
+ if (!fileGroupDescHandle) return E_OUTOFMEMORY;
+
+ LPFILEGROUPDESCRIPTORA fileGroupDescA =
+ reinterpret_cast<LPFILEGROUPDESCRIPTORA>(
+ ::GlobalLock(fileGroupDescHandle));
+ if (!fileGroupDescA) {
+ ::GlobalFree(fileGroupDescHandle);
+ return E_OUTOFMEMORY;
+ }
+
+ // get a valid filename in the following order: 1) from the page title,
+ // 2) localized string for an untitled page, 3) just use "Untitled.url"
+ if (!CreateURLFilenameFromTextA(title, fileGroupDescA->fgd[0].cFileName)) {
+ nsAutoString untitled;
+ if (!GetLocalizedString("noPageTitle", untitled) ||
+ !CreateURLFilenameFromTextA(untitled,
+ fileGroupDescA->fgd[0].cFileName)) {
+ strcpy(fileGroupDescA->fgd[0].cFileName, "Untitled.url");
+ }
+ }
+
+ // one file in the file block
+ fileGroupDescA->cItems = 1;
+ fileGroupDescA->fgd[0].dwFlags = FD_LINKUI;
+
+ ::GlobalUnlock(fileGroupDescHandle);
+ aSTG.hGlobal = fileGroupDescHandle;
+ aSTG.tymed = TYMED_HGLOBAL;
+
+ return S_OK;
+} // GetFileDescriptorInternetShortcutA
+
+HRESULT
+nsDataObj ::GetFileDescriptorInternetShortcutW(FORMATETC& aFE,
+ STGMEDIUM& aSTG) {
+ // get the title of the shortcut
+ nsAutoString title;
+ if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY;
+
+ HGLOBAL fileGroupDescHandle =
+ ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW));
+ if (!fileGroupDescHandle) return E_OUTOFMEMORY;
+
+ LPFILEGROUPDESCRIPTORW fileGroupDescW =
+ reinterpret_cast<LPFILEGROUPDESCRIPTORW>(
+ ::GlobalLock(fileGroupDescHandle));
+ if (!fileGroupDescW) {
+ ::GlobalFree(fileGroupDescHandle);
+ return E_OUTOFMEMORY;
+ }
+
+ // get a valid filename in the following order: 1) from the page title,
+ // 2) localized string for an untitled page, 3) just use "Untitled.url"
+ if (!CreateURLFilenameFromTextW(title, fileGroupDescW->fgd[0].cFileName)) {
+ nsAutoString untitled;
+ if (!GetLocalizedString("noPageTitle", untitled) ||
+ !CreateURLFilenameFromTextW(untitled,
+ fileGroupDescW->fgd[0].cFileName)) {
+ wcscpy(fileGroupDescW->fgd[0].cFileName, L"Untitled.url");
+ }
+ }
+
+ // one file in the file block
+ fileGroupDescW->cItems = 1;
+ fileGroupDescW->fgd[0].dwFlags = FD_LINKUI;
+
+ ::GlobalUnlock(fileGroupDescHandle);
+ aSTG.hGlobal = fileGroupDescHandle;
+ aSTG.tymed = TYMED_HGLOBAL;
+
+ return S_OK;
+} // GetFileDescriptorInternetShortcutW
+
+//
+// GetFileContentsInternetShortcut
+//
+// Create the special format for an internet shortcut and build up the data
+// structures the shell is expecting.
+//
+HRESULT
+nsDataObj ::GetFileContentsInternetShortcut(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ static const char* kShellIconPref = "browser.shell.shortcutFavicons";
+ nsAutoString url;
+ if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY;
+
+ nsCOMPtr<nsIURI> aUri;
+ nsresult rv = NS_NewURI(getter_AddRefs(aUri), url);
+ if (NS_FAILED(rv)) {
+ return E_FAIL;
+ }
+
+ nsAutoCString asciiUrl;
+ rv = aUri->GetAsciiSpec(asciiUrl);
+ if (NS_FAILED(rv)) {
+ return E_FAIL;
+ }
+
+ RefPtr<AutoCloseEvent> event;
+
+ const char* shortcutFormatStr;
+ int totalLen;
+ nsCString asciiPath;
+ if (!Preferences::GetBool(kShellIconPref, true)) {
+ shortcutFormatStr = "[InternetShortcut]\r\nURL=%s\r\n";
+ const int formatLen = strlen(shortcutFormatStr) - 2; // don't include %s
+ totalLen = formatLen + asciiUrl.Length(); // don't include null character
+ } else {
+ nsCOMPtr<nsIFile> icoFile;
+
+ nsAutoString aUriHash;
+
+ event = new AutoCloseEvent();
+ if (!event->IsInited()) {
+ return E_FAIL;
+ }
+
+ RefPtr<AutoSetEvent> e = new AutoSetEvent(WrapNotNull(event));
+ mozilla::widget::FaviconHelper::ObtainCachedIconFile(
+ aUri, aUriHash, mIOThread, true,
+ NS_NewRunnableFunction(
+ "FaviconHelper::RefreshDesktop", [e = std::move(e)] {
+ if (e->IsWaiting()) {
+ // Unblock IStream:::Read.
+ e->Signal();
+ } else {
+ // We could not wait until the favicon was available. We have
+ // to refresh to refect the favicon.
+ SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE,
+ SPI_SETNONCLIENTMETRICS, 0);
+ }
+ }));
+
+ rv = mozilla::widget::FaviconHelper::GetOutputIconPath(aUri, icoFile, true);
+ NS_ENSURE_SUCCESS(rv, E_FAIL);
+ nsString path;
+ rv = icoFile->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, E_FAIL);
+
+ if (IsAsciiNullTerminated(static_cast<const char16_t*>(path.get()))) {
+ LossyCopyUTF16toASCII(path, asciiPath);
+ shortcutFormatStr =
+ "[InternetShortcut]\r\nURL=%s\r\n"
+ "IDList=\r\nHotKey=0\r\nIconFile=%s\r\n"
+ "IconIndex=0\r\n";
+ } else {
+ int len =
+ WideCharToMultiByte(CP_UTF7, 0, char16ptr_t(path.BeginReading()),
+ path.Length(), nullptr, 0, nullptr, nullptr);
+ NS_ENSURE_TRUE(len > 0, E_FAIL);
+ asciiPath.SetLength(len);
+ WideCharToMultiByte(CP_UTF7, 0, char16ptr_t(path.BeginReading()),
+ path.Length(), asciiPath.BeginWriting(), len, nullptr,
+ nullptr);
+ shortcutFormatStr =
+ "[InternetShortcut]\r\nURL=%s\r\n"
+ "IDList=\r\nHotKey=0\r\nIconIndex=0\r\n"
+ "[InternetShortcut.W]\r\nIconFile=%s\r\n";
+ }
+ const int formatLen = strlen(shortcutFormatStr) - 2 * 2; // no %s twice
+ totalLen = formatLen + asciiUrl.Length() +
+ asciiPath.Length(); // we don't want a null character on the end
+ }
+
+ // create a global memory area and build up the file contents w/in it
+ nsAutoGlobalMem globalMem(nsHGLOBAL(::GlobalAlloc(GMEM_SHARE, totalLen)));
+ if (!globalMem) return E_OUTOFMEMORY;
+
+ char* contents = reinterpret_cast<char*>(::GlobalLock(globalMem.get()));
+ if (!contents) {
+ return E_OUTOFMEMORY;
+ }
+
+ // NOTE: we intentionally use the Microsoft version of snprintf here because
+ // it does NOT null
+ // terminate strings which reach the maximum size of the buffer. Since we know
+ // that the formatted length here is totalLen, this call to _snprintf will
+ // format the string into the buffer without appending the null character.
+
+ if (!Preferences::GetBool(kShellIconPref, true)) {
+ _snprintf(contents, totalLen, shortcutFormatStr, asciiUrl.get());
+ } else {
+ _snprintf(contents, totalLen, shortcutFormatStr, asciiUrl.get(),
+ asciiPath.get());
+ }
+
+ ::GlobalUnlock(globalMem.get());
+
+ if (aFE.tymed & TYMED_ISTREAM) {
+ if (!mIsInOperation) {
+ // The drop target didn't initiate an async operation.
+ // We can't block CMemStream::Read.
+ event = nullptr;
+ }
+ RefPtr<IStream> stream =
+ new CMemStream(globalMem.disown(), totalLen, event.forget());
+ stream.forget(&aSTG.pstm);
+ aSTG.tymed = TYMED_ISTREAM;
+ } else {
+ if (event && event->IsInited()) {
+ event->Signal(); // We can't block reading the global memory
+ }
+ aSTG.hGlobal = globalMem.disown();
+ aSTG.tymed = TYMED_HGLOBAL;
+ }
+
+ return S_OK;
+} // GetFileContentsInternetShortcut
+
+// check if specified flavour is present in the transferable
+bool nsDataObj ::IsFlavourPresent(const char* inFlavour) {
+ bool retval = false;
+ NS_ENSURE_TRUE(mTransferable, false);
+
+ // get the list of flavors available in the transferable
+ nsTArray<nsCString> flavors;
+ nsresult rv = mTransferable->FlavorsTransferableCanExport(flavors);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // try to find requested flavour
+ for (uint32_t i = 0; i < flavors.Length(); ++i) {
+ if (flavors[i].Equals(inFlavour)) {
+ retval = true; // found it!
+ break;
+ }
+ } // for each flavor
+
+ return retval;
+}
+
+HRESULT nsDataObj::GetPreferredDropEffect(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ HRESULT res = S_OK;
+ aSTG.tymed = TYMED_HGLOBAL;
+ aSTG.pUnkForRelease = nullptr;
+ HGLOBAL hGlobalMemory = nullptr;
+ hGlobalMemory = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
+ if (hGlobalMemory) {
+ DWORD* pdw = (DWORD*)GlobalLock(hGlobalMemory);
+ // The PreferredDropEffect clipboard format is only registered if a
+ // drag/drop of an image happens from Mozilla to the desktop. We want its
+ // value to be DROPEFFECT_MOVE in that case so that the file is moved from
+ // the temporary location, not copied. This value should, ideally, be set on
+ // the data object via SetData() but our IDataObject implementation doesn't
+ // implement SetData. It adds data to the data object lazily only when the
+ // drop target asks for it.
+ *pdw = (DWORD)DROPEFFECT_MOVE;
+ GlobalUnlock(hGlobalMemory);
+ } else {
+ res = E_OUTOFMEMORY;
+ }
+ aSTG.hGlobal = hGlobalMemory;
+ return res;
+}
+
+//-----------------------------------------------------
+HRESULT nsDataObj::GetText(const nsACString& aDataFlavor, FORMATETC& aFE,
+ STGMEDIUM& aSTG) {
+ void* data = nullptr;
+
+ const nsPromiseFlatCString& flavorStr = PromiseFlatCString(aDataFlavor);
+
+ // NOTE: CreateDataFromPrimitive creates new memory, that needs to be deleted
+ nsCOMPtr<nsISupports> genericDataWrapper;
+ nsresult rv = mTransferable->GetTransferData(
+ flavorStr.get(), getter_AddRefs(genericDataWrapper));
+ if (NS_FAILED(rv) || !genericDataWrapper) {
+ return E_FAIL;
+ }
+
+ uint32_t len;
+ nsPrimitiveHelpers::CreateDataFromPrimitive(
+ nsDependentCString(flavorStr.get()), genericDataWrapper, &data, &len);
+ if (!data) return E_FAIL;
+
+ HGLOBAL hGlobalMemory = nullptr;
+
+ aSTG.tymed = TYMED_HGLOBAL;
+ aSTG.pUnkForRelease = nullptr;
+
+ // We play games under the hood and advertise flavors that we know we
+ // can support, only they require a bit of conversion or munging of the data.
+ // Do that here.
+ //
+ // The transferable gives us data that is null-terminated, but this isn't
+ // reflected in the |len| parameter. Windoze apps expect this null to be there
+ // so bump our data buffer by the appropriate size to account for the null
+ // (one char for CF_TEXT, one char16_t for CF_UNICODETEXT).
+ DWORD allocLen = (DWORD)len;
+ if (aFE.cfFormat == CF_TEXT) {
+ // Someone is asking for text/plain; convert the unicode (assuming it's
+ // present) to text with the correct platform encoding.
+ size_t bufferSize = sizeof(char) * (len + 2);
+ char* plainTextData = static_cast<char*>(moz_xmalloc(bufferSize));
+ char16_t* castedUnicode = reinterpret_cast<char16_t*>(data);
+ int32_t plainTextLen =
+ WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)castedUnicode, len / 2 + 1,
+ plainTextData, bufferSize, NULL, NULL);
+ // replace the unicode data with our plaintext data. Recall that
+ // |plainTextLen| doesn't include the null in the length.
+ free(data);
+ if (plainTextLen) {
+ data = plainTextData;
+ allocLen = plainTextLen;
+ } else {
+ free(plainTextData);
+ NS_WARNING("Oh no, couldn't convert unicode to plain text");
+ return S_OK;
+ }
+ } else if (aFE.cfFormat == nsClipboard::GetHtmlClipboardFormat()) {
+ // Someone is asking for win32's HTML flavor. Convert our html fragment
+ // from unicode to UTF-8 then put it into a format specified by msft.
+ NS_ConvertUTF16toUTF8 converter(reinterpret_cast<char16_t*>(data));
+ char* utf8HTML = nullptr;
+ nsresult rv =
+ BuildPlatformHTML(converter.get(), &utf8HTML); // null terminates
+
+ free(data);
+ if (NS_SUCCEEDED(rv) && utf8HTML) {
+ // replace the unicode data with our HTML data. Don't forget the null.
+ data = utf8HTML;
+ allocLen = strlen(utf8HTML) + sizeof(char);
+ } else {
+ NS_WARNING("Oh no, couldn't convert to HTML");
+ return S_OK;
+ }
+ } else if (aFE.cfFormat != nsClipboard::GetCustomClipboardFormat()) {
+ // we assume that any data that isn't caught above is unicode. This may
+ // be an erroneous assumption, but is true so far.
+ allocLen += sizeof(char16_t);
+ }
+
+ hGlobalMemory = (HGLOBAL)GlobalAlloc(GMEM_MOVEABLE, allocLen);
+
+ // Copy text to Global Memory Area
+ if (hGlobalMemory) {
+ char* dest = reinterpret_cast<char*>(GlobalLock(hGlobalMemory));
+ char* source = reinterpret_cast<char*>(data);
+ memcpy(dest, source, allocLen); // copies the null as well
+ GlobalUnlock(hGlobalMemory);
+ }
+ aSTG.hGlobal = hGlobalMemory;
+
+ // Now, delete the memory that was created by CreateDataFromPrimitive (or our
+ // text/plain data)
+ free(data);
+
+ return S_OK;
+}
+
+//-----------------------------------------------------
+HRESULT nsDataObj::GetFile(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ uint32_t dfInx = 0;
+ ULONG count;
+ FORMATETC fe;
+ m_enumFE->Reset();
+ while (NOERROR == m_enumFE->Next(1, &fe, &count) &&
+ dfInx < mDataFlavors.Length()) {
+ if (mDataFlavors[dfInx].EqualsLiteral(kNativeImageMime))
+ return DropImage(aFE, aSTG);
+ if (mDataFlavors[dfInx].EqualsLiteral(kFileMime))
+ return DropFile(aFE, aSTG);
+ if (mDataFlavors[dfInx].EqualsLiteral(kFilePromiseMime))
+ return DropTempFile(aFE, aSTG);
+ dfInx++;
+ }
+ return E_FAIL;
+}
+
+HRESULT nsDataObj::DropFile(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ nsresult rv;
+ nsCOMPtr<nsISupports> genericDataWrapper;
+
+ if (NS_FAILED(mTransferable->GetTransferData(
+ kFileMime, getter_AddRefs(genericDataWrapper)))) {
+ return E_FAIL;
+ }
+ nsCOMPtr<nsIFile> file(do_QueryInterface(genericDataWrapper));
+ if (!file) return E_FAIL;
+
+ aSTG.tymed = TYMED_HGLOBAL;
+ aSTG.pUnkForRelease = nullptr;
+
+ nsAutoString path;
+ rv = file->GetPath(path);
+ if (NS_FAILED(rv)) return E_FAIL;
+
+ uint32_t allocLen = path.Length() + 2;
+ HGLOBAL hGlobalMemory = nullptr;
+ char16_t* dest;
+
+ hGlobalMemory = GlobalAlloc(GMEM_MOVEABLE,
+ sizeof(DROPFILES) + allocLen * sizeof(char16_t));
+ if (!hGlobalMemory) return E_FAIL;
+
+ DROPFILES* pDropFile = (DROPFILES*)GlobalLock(hGlobalMemory);
+
+ // First, populate the drop file structure
+ pDropFile->pFiles = sizeof(DROPFILES); // Offset to start of file name string
+ pDropFile->fNC = 0;
+ pDropFile->pt.x = 0;
+ pDropFile->pt.y = 0;
+ pDropFile->fWide = TRUE;
+
+ // Copy the filename right after the DROPFILES structure
+ dest = (char16_t*)(((char*)pDropFile) + pDropFile->pFiles);
+ memcpy(dest, path.get(), (allocLen - 1) * sizeof(char16_t));
+
+ // Two null characters are needed at the end of the file name.
+ // Lookup the CF_HDROP shell clipboard format for more info.
+ // Add the second null character right after the first one.
+ dest[allocLen - 1] = L'\0';
+
+ GlobalUnlock(hGlobalMemory);
+
+ aSTG.hGlobal = hGlobalMemory;
+
+ return S_OK;
+}
+
+HRESULT nsDataObj::DropImage(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ nsresult rv;
+ if (!mCachedTempFile) {
+ nsCOMPtr<nsISupports> genericDataWrapper;
+
+ if (NS_FAILED(mTransferable->GetTransferData(
+ kNativeImageMime, getter_AddRefs(genericDataWrapper)))) {
+ return E_FAIL;
+ }
+ nsCOMPtr<imgIContainer> image(do_QueryInterface(genericDataWrapper));
+ if (!image) return E_FAIL;
+
+ nsCOMPtr<imgITools> imgTools =
+ do_CreateInstance("@mozilla.org/image/tools;1");
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = imgTools->EncodeImage(image, nsLiteralCString(IMAGE_BMP),
+ u"bpp=32;version=3"_ns,
+ getter_AddRefs(inputStream));
+ if (NS_FAILED(rv) || !inputStream) {
+ return E_FAIL;
+ }
+
+ nsCOMPtr<imgIEncoder> encoder = do_QueryInterface(inputStream);
+ if (!encoder) {
+ return E_FAIL;
+ }
+
+ uint32_t size = 0;
+ rv = encoder->GetImageBufferUsed(&size);
+ if (NS_FAILED(rv)) {
+ return E_FAIL;
+ }
+
+ char* src = nullptr;
+ rv = encoder->GetImageBuffer(&src);
+ if (NS_FAILED(rv) || !src) {
+ return E_FAIL;
+ }
+
+ // Save the bitmap to a temporary location.
+ nsCOMPtr<nsIFile> dropFile;
+ rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dropFile));
+ if (!dropFile) {
+ return E_FAIL;
+ }
+
+ // Filename must be random so as not to confuse apps like
+ // Photoshop which handle multiple drags into a single window.
+ char buf[13];
+ nsCString filename;
+ NS_MakeRandomString(buf, 8);
+ memcpy(buf + 8, ".bmp", 5);
+ filename.Append(nsDependentCString(buf, 12));
+ dropFile->AppendNative(filename);
+ rv = dropFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660);
+ if (NS_FAILED(rv)) {
+ return E_FAIL;
+ }
+
+ // Cache the temp file so we can delete it later and so
+ // it doesn't get recreated over and over on multiple calls
+ // which does occur from windows shell.
+ dropFile->Clone(getter_AddRefs(mCachedTempFile));
+
+ // Write the data to disk.
+ nsCOMPtr<nsIOutputStream> outStream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(outStream), dropFile);
+ if (NS_FAILED(rv)) {
+ return E_FAIL;
+ }
+
+ uint32_t written = 0;
+ rv = outStream->Write(src, size, &written);
+ if (NS_FAILED(rv) || written != size) {
+ return E_FAIL;
+ }
+
+ outStream->Close();
+ }
+
+ // Pass the file name back to the drop target so that it can access the file.
+ nsAutoString path;
+ rv = mCachedTempFile->GetPath(path);
+ if (NS_FAILED(rv)) return E_FAIL;
+
+ // Two null characters are needed to terminate the file name list.
+ HGLOBAL hGlobalMemory = nullptr;
+
+ uint32_t allocLen = path.Length() + 2;
+
+ aSTG.tymed = TYMED_HGLOBAL;
+ aSTG.pUnkForRelease = nullptr;
+
+ hGlobalMemory = GlobalAlloc(GMEM_MOVEABLE,
+ sizeof(DROPFILES) + allocLen * sizeof(char16_t));
+ if (!hGlobalMemory) return E_FAIL;
+
+ DROPFILES* pDropFile = (DROPFILES*)GlobalLock(hGlobalMemory);
+
+ // First, populate the drop file structure.
+ pDropFile->pFiles =
+ sizeof(DROPFILES); // Offset to start of file name char array.
+ pDropFile->fNC = 0;
+ pDropFile->pt.x = 0;
+ pDropFile->pt.y = 0;
+ pDropFile->fWide = TRUE;
+
+ // Copy the filename right after the DROPFILES structure.
+ char16_t* dest = (char16_t*)(((char*)pDropFile) + pDropFile->pFiles);
+ memcpy(dest, path.get(),
+ (allocLen - 1) *
+ sizeof(char16_t)); // Copies the null character in path as well.
+
+ // Two null characters are needed at the end of the file name.
+ // Lookup the CF_HDROP shell clipboard format for more info.
+ // Add the second null character right after the first one.
+ dest[allocLen - 1] = L'\0';
+
+ GlobalUnlock(hGlobalMemory);
+
+ aSTG.hGlobal = hGlobalMemory;
+
+ return S_OK;
+}
+
+HRESULT nsDataObj::DropTempFile(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ nsresult rv;
+ if (!mCachedTempFile) {
+ // Tempfile will need a temporary location.
+ nsCOMPtr<nsIFile> dropFile;
+ rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dropFile));
+ if (!dropFile) return E_FAIL;
+
+ // Filename must be random
+ nsCString filename;
+ nsAutoString wideFileName;
+ nsCOMPtr<nsIURI> sourceURI;
+ HRESULT res;
+ res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
+ if (FAILED(res)) return res;
+ NS_CopyUnicodeToNative(wideFileName, filename);
+
+ dropFile->AppendNative(filename);
+ rv = dropFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660);
+ if (NS_FAILED(rv)) return E_FAIL;
+
+ // Cache the temp file so we can delete it later and so
+ // it doesn't get recreated over and over on multiple calls
+ // which does occur from windows shell.
+ dropFile->Clone(getter_AddRefs(mCachedTempFile));
+
+ // Write the data to disk.
+ nsCOMPtr<nsIOutputStream> outStream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(outStream), dropFile);
+ if (NS_FAILED(rv)) return E_FAIL;
+
+ IStream* pStream = nullptr;
+ nsDataObj::CreateStream(&pStream);
+ NS_ENSURE_TRUE(pStream, E_FAIL);
+
+ char buffer[512];
+ ULONG readCount = 0;
+ uint32_t writeCount = 0;
+ while (1) {
+ HRESULT hres = pStream->Read(buffer, sizeof(buffer), &readCount);
+ if (FAILED(hres)) return E_FAIL;
+ if (readCount == 0) break;
+ rv = outStream->Write(buffer, readCount, &writeCount);
+ if (NS_FAILED(rv)) return E_FAIL;
+ }
+ outStream->Close();
+ pStream->Release();
+ }
+
+ // Pass the file name back to the drop target so that it can access the file.
+ nsAutoString path;
+ rv = mCachedTempFile->GetPath(path);
+ if (NS_FAILED(rv)) return E_FAIL;
+
+ uint32_t allocLen = path.Length() + 2;
+
+ // Two null characters are needed to terminate the file name list.
+ HGLOBAL hGlobalMemory = nullptr;
+
+ aSTG.tymed = TYMED_HGLOBAL;
+ aSTG.pUnkForRelease = nullptr;
+
+ hGlobalMemory = GlobalAlloc(GMEM_MOVEABLE,
+ sizeof(DROPFILES) + allocLen * sizeof(char16_t));
+ if (!hGlobalMemory) return E_FAIL;
+
+ DROPFILES* pDropFile = (DROPFILES*)GlobalLock(hGlobalMemory);
+
+ // First, populate the drop file structure.
+ pDropFile->pFiles =
+ sizeof(DROPFILES); // Offset to start of file name char array.
+ pDropFile->fNC = 0;
+ pDropFile->pt.x = 0;
+ pDropFile->pt.y = 0;
+ pDropFile->fWide = TRUE;
+
+ // Copy the filename right after the DROPFILES structure.
+ char16_t* dest = (char16_t*)(((char*)pDropFile) + pDropFile->pFiles);
+ memcpy(dest, path.get(),
+ (allocLen - 1) *
+ sizeof(char16_t)); // Copies the null character in path as well.
+
+ // Two null characters are needed at the end of the file name.
+ // Lookup the CF_HDROP shell clipboard format for more info.
+ // Add the second null character right after the first one.
+ dest[allocLen - 1] = L'\0';
+
+ GlobalUnlock(hGlobalMemory);
+
+ aSTG.hGlobal = hGlobalMemory;
+
+ return S_OK;
+}
+
+//-----------------------------------------------------
+// Registers the DataFlavor/FE pair.
+//-----------------------------------------------------
+void nsDataObj::AddDataFlavor(const char* aDataFlavor, LPFORMATETC aFE) {
+ // These two lists are the mapping to and from data flavors and FEs.
+ // Later, OLE will tell us it needs a certain type of FORMATETC (text,
+ // unicode, etc) unicode, etc), so we will look up the data flavor that
+ // corresponds to the FE and then ask the transferable for that type of data.
+ mDataFlavors.AppendElement(aDataFlavor);
+ m_enumFE->AddFormatEtc(aFE);
+}
+
+//-----------------------------------------------------
+// Sets the transferable object
+//-----------------------------------------------------
+void nsDataObj::SetTransferable(nsITransferable* aTransferable) {
+ NS_IF_RELEASE(mTransferable);
+
+ mTransferable = aTransferable;
+ if (nullptr == mTransferable) {
+ return;
+ }
+
+ NS_ADDREF(mTransferable);
+
+ return;
+}
+
+//
+// ExtractURL
+//
+// Roots around in the transferable for the appropriate flavor that indicates
+// a url and pulls out the url portion of the data. Used mostly for creating
+// internet shortcuts on the desktop. The url flavor is of the format:
+//
+// <url> <linefeed> <page title>
+//
+nsresult nsDataObj ::ExtractShortcutURL(nsString& outURL) {
+ NS_ASSERTION(mTransferable, "We don't have a good transferable");
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsISupports> genericURL;
+ if (NS_SUCCEEDED(mTransferable->GetTransferData(
+ kURLMime, getter_AddRefs(genericURL)))) {
+ nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
+ if (urlObject) {
+ nsAutoString url;
+ urlObject->GetData(url);
+ outURL = url;
+
+ // find the first linefeed in the data, that's where the url ends. trunc
+ // the result string at that point.
+ int32_t lineIndex = outURL.FindChar('\n');
+ NS_ASSERTION(lineIndex > 0,
+ "Format for url flavor is <url> <linefeed> <page title>");
+ if (lineIndex > 0) {
+ outURL.Truncate(lineIndex);
+ rv = NS_OK;
+ }
+ }
+ } else if (NS_SUCCEEDED(mTransferable->GetTransferData(
+ kURLDataMime, getter_AddRefs(genericURL))) ||
+ NS_SUCCEEDED(mTransferable->GetTransferData(
+ kURLPrivateMime, getter_AddRefs(genericURL)))) {
+ nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
+ if (urlObject) {
+ nsAutoString url;
+ urlObject->GetData(url);
+ outURL = url;
+
+ rv = NS_OK;
+ }
+
+ } // if found flavor
+
+ return rv;
+
+} // ExtractShortcutURL
+
+//
+// ExtractShortcutTitle
+//
+// Roots around in the transferable for the appropriate flavor that indicates
+// a url and pulls out the title portion of the data. Used mostly for creating
+// internet shortcuts on the desktop. The url flavor is of the format:
+//
+// <url> <linefeed> <page title>
+//
+nsresult nsDataObj ::ExtractShortcutTitle(nsString& outTitle) {
+ NS_ASSERTION(mTransferable, "We'd don't have a good transferable");
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsISupports> genericURL;
+ if (NS_SUCCEEDED(mTransferable->GetTransferData(
+ kURLMime, getter_AddRefs(genericURL)))) {
+ nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
+ if (urlObject) {
+ nsAutoString url;
+ urlObject->GetData(url);
+
+ // find the first linefeed in the data, that's where the url ends. we want
+ // everything after that linefeed. FindChar() returns -1 if we can't find
+ int32_t lineIndex = url.FindChar('\n');
+ NS_ASSERTION(lineIndex != -1,
+ "Format for url flavor is <url> <linefeed> <page title>");
+ if (lineIndex != -1) {
+ url.Mid(outTitle, lineIndex + 1, url.Length() - (lineIndex + 1));
+ rv = NS_OK;
+ }
+ }
+ } // if found flavor
+
+ return rv;
+
+} // ExtractShortcutTitle
+
+//
+// BuildPlatformHTML
+//
+// Munge our HTML data to win32's CF_HTML spec. Basically, put the requisite
+// header information on it. This will null-terminate |outPlatformHTML|. See
+// https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
+// for details.
+//
+// We assume that |inOurHTML| is already a fragment (ie, doesn't have <HTML>
+// or <BODY> tags). We'll wrap the fragment with them to make other apps
+// happy.
+//
+nsresult nsDataObj ::BuildPlatformHTML(const char* inOurHTML,
+ char** outPlatformHTML) {
+ *outPlatformHTML = nullptr;
+ nsDependentCString inHTMLString(inOurHTML);
+
+ // Do we already have mSourceURL from a drag?
+ if (mSourceURL.IsEmpty()) {
+ nsAutoString url;
+ ExtractShortcutURL(url);
+
+ AppendUTF16toUTF8(url, mSourceURL);
+ }
+
+ constexpr auto kStartHTMLPrefix = "Version:0.9\r\nStartHTML:"_ns;
+ constexpr auto kEndHTMLPrefix = "\r\nEndHTML:"_ns;
+ constexpr auto kStartFragPrefix = "\r\nStartFragment:"_ns;
+ constexpr auto kEndFragPrefix = "\r\nEndFragment:"_ns;
+ constexpr auto kStartSourceURLPrefix = "\r\nSourceURL:"_ns;
+ constexpr auto kEndFragTrailer = "\r\n"_ns;
+
+ // The CF_HTML's size is embedded in the fragment, in such a way that the
+ // number of digits in the size is part of the size itself. While it _is_
+ // technically possible to compute the necessary size of the size-field
+ // precisely -- by trial and error, if nothing else -- it's simpler just to
+ // pick a rough but generous estimate and zero-pad it. (Zero-padding is
+ // explicitly permitted by the format definition.)
+ //
+ // Originally, in 2001, the "rough but generous estimate" was 8 digits. While
+ // a maximum size of (10**9 - 1) bytes probably would have covered all
+ // possible use-cases at the time, it's somewhat more likely to overflow
+ // nowadays. Nonetheless, for the sake of backwards compatibility with any
+ // misbehaving consumers of our existing CF_HTML output, we retain exactly
+ // that padding for (most) fragments where it suffices. (No such misbehaving
+ // consumers are actually known, so this is arguably paranoia.)
+ //
+ // It is now 2022. A padding size of 16 will cover up to about 8.8 petabytes,
+ // which should be enough for at least the next few years or so.
+ const size_t numberLength = inHTMLString.Length() < 9999'0000 ? 8 : 16;
+
+ const size_t sourceURLLength = mSourceURL.Length();
+
+ const size_t fixedHeaderLen =
+ kStartHTMLPrefix.Length() + kEndHTMLPrefix.Length() +
+ kStartFragPrefix.Length() + kEndFragPrefix.Length() +
+ kEndFragTrailer.Length() + (4 * numberLength);
+
+ const size_t totalHeaderLen =
+ fixedHeaderLen + (sourceURLLength > 0
+ ? kStartSourceURLPrefix.Length() + sourceURLLength
+ : 0);
+
+ constexpr auto kHeaderString = "<html><body>\r\n<!--StartFragment-->"_ns;
+ constexpr auto kTrailingString =
+ "<!--EndFragment-->\r\n"
+ "</body>\r\n"
+ "</html>"_ns;
+
+ // calculate the offsets
+ size_t startHTMLOffset = totalHeaderLen;
+ size_t startFragOffset = startHTMLOffset + kHeaderString.Length();
+
+ size_t endFragOffset = startFragOffset + inHTMLString.Length();
+ size_t endHTMLOffset = endFragOffset + kTrailingString.Length();
+
+ // now build the final version
+ nsCString clipboardString;
+ clipboardString.SetCapacity(endHTMLOffset);
+
+ const int numberLengthInt = static_cast<int>(numberLength);
+ clipboardString.Append(kStartHTMLPrefix);
+ clipboardString.AppendPrintf("%0*zu", numberLengthInt, startHTMLOffset);
+
+ clipboardString.Append(kEndHTMLPrefix);
+ clipboardString.AppendPrintf("%0*zu", numberLengthInt, endHTMLOffset);
+
+ clipboardString.Append(kStartFragPrefix);
+ clipboardString.AppendPrintf("%0*zu", numberLengthInt, startFragOffset);
+
+ clipboardString.Append(kEndFragPrefix);
+ clipboardString.AppendPrintf("%0*zu", numberLengthInt, endFragOffset);
+
+ if (sourceURLLength > 0) {
+ clipboardString.Append(kStartSourceURLPrefix);
+ clipboardString.Append(mSourceURL);
+ }
+
+ clipboardString.Append(kEndFragTrailer);
+
+ // Assert that the positional values were correct as we pass by their
+ // corresponding positions.
+ MOZ_ASSERT(clipboardString.Length() == startHTMLOffset);
+ clipboardString.Append(kHeaderString);
+ MOZ_ASSERT(clipboardString.Length() == startFragOffset);
+ clipboardString.Append(inHTMLString);
+ MOZ_ASSERT(clipboardString.Length() == endFragOffset);
+ clipboardString.Append(kTrailingString);
+ MOZ_ASSERT(clipboardString.Length() == endHTMLOffset);
+
+ *outPlatformHTML = ToNewCString(clipboardString, mozilla::fallible);
+ if (!*outPlatformHTML) return NS_ERROR_OUT_OF_MEMORY;
+
+ return NS_OK;
+}
+
+HRESULT
+nsDataObj ::GetUniformResourceLocator(FORMATETC& aFE, STGMEDIUM& aSTG,
+ bool aIsUnicode) {
+ HRESULT res = S_OK;
+ if (IsFlavourPresent(kURLMime)) {
+ if (aIsUnicode)
+ res = ExtractUniformResourceLocatorW(aFE, aSTG);
+ else
+ res = ExtractUniformResourceLocatorA(aFE, aSTG);
+ } else
+ NS_WARNING("Not yet implemented\n");
+ return res;
+}
+
+HRESULT
+nsDataObj::ExtractUniformResourceLocatorA(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ HRESULT result = S_OK;
+
+ nsAutoString url;
+ if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY;
+
+ NS_LossyConvertUTF16toASCII asciiUrl(url);
+ const int totalLen = asciiUrl.Length() + 1;
+ HGLOBAL hGlobalMemory = GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, totalLen);
+ if (!hGlobalMemory) return E_OUTOFMEMORY;
+
+ char* contents = reinterpret_cast<char*>(GlobalLock(hGlobalMemory));
+ if (!contents) {
+ GlobalFree(hGlobalMemory);
+ return E_OUTOFMEMORY;
+ }
+
+ strcpy(contents, asciiUrl.get());
+ GlobalUnlock(hGlobalMemory);
+ aSTG.hGlobal = hGlobalMemory;
+ aSTG.tymed = TYMED_HGLOBAL;
+
+ return result;
+}
+
+HRESULT
+nsDataObj::ExtractUniformResourceLocatorW(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ HRESULT result = S_OK;
+
+ nsAutoString url;
+ if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY;
+
+ const int totalLen = (url.Length() + 1) * sizeof(char16_t);
+ HGLOBAL hGlobalMemory = GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, totalLen);
+ if (!hGlobalMemory) return E_OUTOFMEMORY;
+
+ wchar_t* contents = reinterpret_cast<wchar_t*>(GlobalLock(hGlobalMemory));
+ if (!contents) {
+ GlobalFree(hGlobalMemory);
+ return E_OUTOFMEMORY;
+ }
+
+ wcscpy(contents, url.get());
+ GlobalUnlock(hGlobalMemory);
+ aSTG.hGlobal = hGlobalMemory;
+ aSTG.tymed = TYMED_HGLOBAL;
+
+ return result;
+}
+
+// Gets the filename from the kFilePromiseURLMime flavour
+HRESULT nsDataObj::GetDownloadDetails(nsIURI** aSourceURI,
+ nsAString& aFilename) {
+ *aSourceURI = nullptr;
+
+ NS_ENSURE_TRUE(mTransferable, E_FAIL);
+
+ // get the URI from the kFilePromiseURLMime flavor
+ nsCOMPtr<nsISupports> urlPrimitive;
+ nsresult rv = mTransferable->GetTransferData(kFilePromiseURLMime,
+ getter_AddRefs(urlPrimitive));
+ NS_ENSURE_SUCCESS(rv, E_FAIL);
+ nsCOMPtr<nsISupportsString> srcUrlPrimitive = do_QueryInterface(urlPrimitive);
+ NS_ENSURE_TRUE(srcUrlPrimitive, E_FAIL);
+
+ nsAutoString srcUri;
+ srcUrlPrimitive->GetData(srcUri);
+ if (srcUri.IsEmpty()) return E_FAIL;
+ nsCOMPtr<nsIURI> sourceURI;
+ NS_NewURI(getter_AddRefs(sourceURI), srcUri);
+
+ nsAutoString srcFileName;
+ nsCOMPtr<nsISupports> fileNamePrimitive;
+ Unused << mTransferable->GetTransferData(kFilePromiseDestFilename,
+ getter_AddRefs(fileNamePrimitive));
+ nsCOMPtr<nsISupportsString> srcFileNamePrimitive =
+ do_QueryInterface(fileNamePrimitive);
+ if (srcFileNamePrimitive) {
+ srcFileNamePrimitive->GetData(srcFileName);
+ } else {
+ nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI);
+ if (!sourceURL) return E_FAIL;
+
+ nsAutoCString urlFileName;
+ sourceURL->GetFileName(urlFileName);
+ NS_UnescapeURL(urlFileName);
+ CopyUTF8toUTF16(urlFileName, srcFileName);
+ }
+
+ // make the name safe for the filesystem
+ ValidateFilename(srcFileName, false);
+ if (srcFileName.IsEmpty()) return E_FAIL;
+
+ sourceURI.swap(*aSourceURI);
+ aFilename = srcFileName;
+ return S_OK;
+}
+
+HRESULT nsDataObj::GetFileDescriptor_IStreamA(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ HGLOBAL fileGroupDescHandle =
+ ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW));
+ NS_ENSURE_TRUE(fileGroupDescHandle, E_OUTOFMEMORY);
+
+ LPFILEGROUPDESCRIPTORA fileGroupDescA =
+ reinterpret_cast<LPFILEGROUPDESCRIPTORA>(GlobalLock(fileGroupDescHandle));
+ if (!fileGroupDescA) {
+ ::GlobalFree(fileGroupDescHandle);
+ return E_OUTOFMEMORY;
+ }
+
+ nsAutoString wideFileName;
+ HRESULT res;
+ nsCOMPtr<nsIURI> sourceURI;
+ res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
+ if (FAILED(res)) {
+ ::GlobalFree(fileGroupDescHandle);
+ return res;
+ }
+
+ nsAutoCString nativeFileName;
+ NS_CopyUnicodeToNative(wideFileName, nativeFileName);
+
+ strncpy(fileGroupDescA->fgd[0].cFileName, nativeFileName.get(), MAX_PATH - 1);
+ fileGroupDescA->fgd[0].cFileName[MAX_PATH - 1] = '\0';
+
+ // one file in the file block
+ fileGroupDescA->cItems = 1;
+ fileGroupDescA->fgd[0].dwFlags = FD_PROGRESSUI;
+
+ GlobalUnlock(fileGroupDescHandle);
+ aSTG.hGlobal = fileGroupDescHandle;
+ aSTG.tymed = TYMED_HGLOBAL;
+
+ return S_OK;
+}
+
+HRESULT nsDataObj::GetFileDescriptor_IStreamW(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ HGLOBAL fileGroupDescHandle =
+ ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW));
+ NS_ENSURE_TRUE(fileGroupDescHandle, E_OUTOFMEMORY);
+
+ LPFILEGROUPDESCRIPTORW fileGroupDescW =
+ reinterpret_cast<LPFILEGROUPDESCRIPTORW>(GlobalLock(fileGroupDescHandle));
+ if (!fileGroupDescW) {
+ ::GlobalFree(fileGroupDescHandle);
+ return E_OUTOFMEMORY;
+ }
+
+ nsAutoString wideFileName;
+ HRESULT res;
+ nsCOMPtr<nsIURI> sourceURI;
+ res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
+ if (FAILED(res)) {
+ ::GlobalFree(fileGroupDescHandle);
+ return res;
+ }
+
+ wcsncpy(fileGroupDescW->fgd[0].cFileName, wideFileName.get(), MAX_PATH - 1);
+ fileGroupDescW->fgd[0].cFileName[MAX_PATH - 1] = '\0';
+ // one file in the file block
+ fileGroupDescW->cItems = 1;
+ fileGroupDescW->fgd[0].dwFlags = FD_PROGRESSUI;
+
+ GlobalUnlock(fileGroupDescHandle);
+ aSTG.hGlobal = fileGroupDescHandle;
+ aSTG.tymed = TYMED_HGLOBAL;
+
+ return S_OK;
+}
+
+HRESULT nsDataObj::GetFileContents_IStream(FORMATETC& aFE, STGMEDIUM& aSTG) {
+ IStream* pStream = nullptr;
+
+ nsDataObj::CreateStream(&pStream);
+ NS_ENSURE_TRUE(pStream, E_FAIL);
+
+ aSTG.tymed = TYMED_ISTREAM;
+ aSTG.pstm = pStream;
+ aSTG.pUnkForRelease = nullptr;
+
+ return S_OK;
+}
diff --git a/widget/windows/nsDataObj.h b/widget/windows/nsDataObj.h
new file mode 100644
index 0000000000..17683e371a
--- /dev/null
+++ b/widget/windows/nsDataObj.h
@@ -0,0 +1,315 @@
+/* -*- 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/. */
+
+#ifndef _NSDATAOBJ_H_
+#define _NSDATAOBJ_H_
+
+#include <oleidl.h>
+#include <shldisp.h>
+
+#include "mozilla/glue/WinUtils.h"
+#include "mozilla/LazyIdleThread.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsIURI.h"
+#include "nsIStreamListener.h"
+#include "nsIChannel.h"
+#include "nsCOMArray.h"
+#include "nsITimer.h"
+#include "nsIURI.h"
+#include "nsString.h"
+#include "nsWindowsHelpers.h"
+
+class nsICookieJarSettings;
+class nsIPrincipal;
+class nsIReferrerInfo;
+class nsIThread;
+class nsITransferable;
+class CEnumFormatEtc;
+
+/*
+ * This ole registered class is used to facilitate drag-drop of objects which
+ * can be adapted by an object derived from CfDragDrop. The CfDragDrop is
+ * associated with instances via SetDragDrop().
+ */
+class nsDataObj : public IDataObject, public IDataObjectAsyncCapability {
+ RefPtr<mozilla::LazyIdleThread> mIOThread;
+
+ public: // construction, destruction
+ explicit nsDataObj(nsIURI* uri = nullptr);
+
+ protected:
+ virtual ~nsDataObj();
+
+ public: // IUnknown methods - see iunknown.h for documentation
+ STDMETHODIMP_(ULONG) AddRef() override;
+ STDMETHODIMP QueryInterface(REFIID, void**) override;
+ STDMETHODIMP_(ULONG) Release() override;
+
+ // support for clipboard
+ virtual void AddDataFlavor(const char* aDataFlavor, LPFORMATETC aFE);
+ void SetTransferable(nsITransferable* aTransferable);
+
+ public: // IDataObject methods - these are general comments. see CfDragDrop
+ // for overriding behavior
+ // Store data in pSTM according to the format specified by pFE, if the
+ // format is supported (supported formats are specified in CfDragDrop::
+ // GetFormats) and return NOERROR; otherwise return DATA_E_FORMATETC. It
+ // is the callers responsibility to free pSTM if NOERROR is returned.
+ STDMETHODIMP GetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM) override;
+
+ // Similar to GetData except that the caller allocates the structure
+ // referenced by pSTM.
+ STDMETHODIMP GetDataHere(LPFORMATETC pFE, LPSTGMEDIUM pSTM) override;
+
+ // Returns S_TRUE if this object supports the format specified by pSTM,
+ // S_FALSE otherwise.
+ STDMETHODIMP QueryGetData(LPFORMATETC pFE) override;
+
+ // Set pCanonFE to the canonical format of pFE if one exists and return
+ // NOERROR, otherwise return DATA_S_SAMEFORMATETC. A canonical format
+ // implies an identical rendering.
+ STDMETHODIMP GetCanonicalFormatEtc(LPFORMATETC pFE,
+ LPFORMATETC pCanonFE) final;
+
+ // Set this objects data according to the format specified by pFE and
+ // the storage medium specified by pSTM and return NOERROR, if the format
+ // is supported. If release is TRUE this object must release the storage
+ // associated with pSTM.
+ STDMETHODIMP SetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM,
+ BOOL release) override;
+
+ // Set ppEnum to an IEnumFORMATETC object which will iterate all of the
+ // data formats that this object supports. direction is either DATADIR_GET
+ // or DATADIR_SET.
+ STDMETHODIMP EnumFormatEtc(DWORD direction, LPENUMFORMATETC* ppEnum) final;
+
+ // Set up an advisory connection to this object based on the format specified
+ // by pFE, flags, and the pAdvise. Set pConn to the established advise
+ // connection.
+ STDMETHODIMP DAdvise(LPFORMATETC pFE, DWORD flags, LPADVISESINK pAdvise,
+ DWORD* pConn) final;
+
+ // Turn off advising of a previous call to DAdvise which set pConn.
+ STDMETHODIMP DUnadvise(DWORD pConn) final;
+
+ // Set ppEnum to an IEnumSTATDATA object which will iterate over the
+ // existing objects which have established advisory connections to this
+ // object.
+ STDMETHODIMP EnumDAdvise(LPENUMSTATDATA* ppEnum) final;
+
+ // IDataObjectAsyncCapability methods
+ STDMETHODIMP EndOperation(HRESULT hResult, IBindCtx* pbcReserved,
+ DWORD dwEffects) final;
+ STDMETHODIMP GetAsyncMode(BOOL* pfIsOpAsync) final;
+ STDMETHODIMP InOperation(BOOL* pfInAsyncOp) final;
+ STDMETHODIMP SetAsyncMode(BOOL fDoOpAsync) final;
+ STDMETHODIMP StartOperation(IBindCtx* pbcReserved) final;
+
+ private: // other methods
+ // Gets the filename from the kFilePromiseURLMime flavour
+ HRESULT GetDownloadDetails(nsIURI** aSourceURI, nsAString& aFilename);
+
+ // help determine the kind of drag
+ bool IsFlavourPresent(const char* inFlavour);
+
+ protected:
+ HRESULT GetFile(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT GetText(const nsACString& aDF, FORMATETC& aFE, STGMEDIUM& aSTG);
+
+ private:
+ HRESULT GetDib(const nsACString& inFlavor, FORMATETC&, STGMEDIUM& aSTG);
+
+ HRESULT DropImage(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT DropFile(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT DropTempFile(FORMATETC& aFE, STGMEDIUM& aSTG);
+
+ HRESULT GetUniformResourceLocator(FORMATETC& aFE, STGMEDIUM& aSTG,
+ bool aIsUnicode);
+ HRESULT ExtractUniformResourceLocatorA(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT ExtractUniformResourceLocatorW(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT GetFileDescriptor(FORMATETC& aFE, STGMEDIUM& aSTG, bool aIsUnicode);
+
+ protected:
+ HRESULT GetFileContents(FORMATETC& aFE, STGMEDIUM& aSTG);
+
+ private:
+ HRESULT GetPreferredDropEffect(FORMATETC& aFE, STGMEDIUM& aSTG);
+
+ // Provide the structures needed for an internet shortcut by the shell
+ HRESULT GetFileDescriptorInternetShortcutA(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT GetFileDescriptorInternetShortcutW(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT GetFileContentsInternetShortcut(FORMATETC& aFE, STGMEDIUM& aSTG);
+
+ // IStream implementation
+ HRESULT GetFileDescriptor_IStreamA(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT GetFileDescriptor_IStreamW(FORMATETC& aFE, STGMEDIUM& aSTG);
+ HRESULT GetFileContents_IStream(FORMATETC& aFE, STGMEDIUM& aSTG);
+
+ nsresult ExtractShortcutURL(nsString& outURL);
+ nsresult ExtractShortcutTitle(nsString& outTitle);
+
+ // munge our HTML data to win32's CF_HTML spec. Will null terminate
+ nsresult BuildPlatformHTML(const char* inOurHTML, char** outPlatformHTML);
+
+ // Used for the SourceURL part of CF_HTML
+ nsCString mSourceURL;
+
+ protected:
+ BOOL FormatsMatch(const FORMATETC& source, const FORMATETC& target) const;
+
+ ULONG m_cRef; // the reference count
+
+ private:
+ nsTArray<nsCString> mDataFlavors;
+
+ nsITransferable* mTransferable; // nsDataObj owns and ref counts
+ // nsITransferable, the nsITransferable does
+ // know anything about the nsDataObj
+
+ protected:
+ CEnumFormatEtc* m_enumFE; // Ownership Rules:
+ // nsDataObj owns and ref counts CEnumFormatEtc,
+
+ private:
+ nsCOMPtr<nsIFile> mCachedTempFile;
+ RefPtr<nsDataObj> mKeepAlive;
+
+ BOOL mIsAsyncMode;
+ BOOL mIsInOperation;
+ ///////////////////////////////////////////////////////////////////////////////
+ // CStream class implementation
+ // this class is used in Drag and drop with download sample
+ // called from IDataObject::GetData
+ class CStreamBase : public IStream {
+ // IStream
+ STDMETHODIMP Clone(IStream** ppStream) final;
+ STDMETHODIMP Commit(DWORD dwFrags) final;
+ STDMETHODIMP CopyTo(IStream* pDestStream, ULARGE_INTEGER nBytesToCopy,
+ ULARGE_INTEGER* nBytesRead,
+ ULARGE_INTEGER* nBytesWritten) final;
+ STDMETHODIMP LockRegion(ULARGE_INTEGER nStart, ULARGE_INTEGER nBytes,
+ DWORD dwFlags) final;
+ STDMETHODIMP Revert(void) final;
+ STDMETHODIMP Seek(LARGE_INTEGER nMove, DWORD dwOrigin,
+ ULARGE_INTEGER* nNewPos) final;
+ STDMETHODIMP SetSize(ULARGE_INTEGER nNewSize) final;
+ STDMETHODIMP UnlockRegion(ULARGE_INTEGER nStart, ULARGE_INTEGER nBytes,
+ DWORD dwFlags) final;
+ STDMETHODIMP Write(const void* pvBuffer, ULONG nBytesToRead,
+ ULONG* nBytesRead) final;
+
+ protected:
+ uint32_t mStreamRead;
+
+ CStreamBase();
+ virtual ~CStreamBase();
+ };
+
+ class CStream final : public CStreamBase, public nsIStreamListener {
+ nsCOMPtr<nsIChannel> mChannel;
+ FallibleTArray<uint8_t> mChannelData;
+ nsresult mChannelResult;
+ bool mChannelRead;
+
+ virtual ~CStream();
+ nsresult WaitForCompletion();
+
+ // IUnknown
+ STDMETHOD(QueryInterface)(REFIID refiid, void** ppvResult) final;
+
+ // IStream
+ STDMETHODIMP Read(void* pvBuffer, ULONG nBytesToRead,
+ ULONG* nBytesRead) final;
+ STDMETHODIMP Stat(STATSTG* statstg, DWORD dwFlags) final;
+
+ public:
+ CStream();
+ nsresult Init(nsIURI* pSourceURI, nsContentPolicyType aContentPolicyType,
+ nsIPrincipal* aRequestingPrincipal,
+ nsICookieJarSettings* aCookieJarSettings,
+ nsIReferrerInfo* aReferrerInfo);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ };
+
+ HRESULT CreateStream(IStream** outStream);
+
+ // This class must be thread-safe.
+ class AutoCloseEvent final {
+ const nsAutoHandle mEvent;
+
+ AutoCloseEvent(const AutoCloseEvent&) = delete;
+ void operator=(const AutoCloseEvent&) = delete;
+ ~AutoCloseEvent() = default;
+
+ public:
+ AutoCloseEvent();
+ bool IsInited() const;
+ void Signal() const;
+ DWORD Wait(DWORD aMillisec) const;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AutoCloseEvent)
+ };
+
+ // This class must be thread-safe.
+ class AutoSetEvent final {
+ const RefPtr<AutoCloseEvent> mEvent;
+
+ AutoSetEvent(const AutoSetEvent&) = delete;
+ void operator=(const AutoSetEvent&) = delete;
+ ~AutoSetEvent();
+
+ public:
+ explicit AutoSetEvent(mozilla::NotNull<AutoCloseEvent*> aEvent);
+ void Signal() const;
+ bool IsWaiting() const;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AutoSetEvent)
+ };
+
+ // This class must be thread-safe.
+ class CMemStream final : public CStreamBase {
+ static mozilla::glue::Win32SRWLock mLock;
+ const nsAutoGlobalMem mGlobalMem;
+ const RefPtr<AutoCloseEvent> mEvent;
+ const uint32_t mTotalLength;
+ RefPtr<IUnknown> mMarshaler;
+
+ virtual ~CMemStream();
+ void WaitForCompletion();
+
+ // IStream
+ STDMETHODIMP Read(void* pvBuffer, ULONG nBytesToRead,
+ ULONG* nBytesRead) final;
+ STDMETHODIMP Stat(STATSTG* statstg, DWORD dwFlags) final;
+
+ public:
+ CMemStream(nsHGLOBAL aGlobalMem, uint32_t mTotalLength,
+ already_AddRefed<AutoCloseEvent> aEvent);
+
+ // IUnknown
+ STDMETHOD(QueryInterface)(REFIID refiid, void** ppvResult) final;
+ NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(CMemStream, final)
+ };
+
+ private:
+ // Drag and drop helper data for implementing drag and drop image support
+ typedef struct {
+ FORMATETC fe;
+ STGMEDIUM stgm;
+ } DATAENTRY, *LPDATAENTRY;
+
+ nsTArray<LPDATAENTRY> mDataEntryList;
+ nsCOMPtr<nsITimer> mTimer;
+
+ bool LookupArbitraryFormat(FORMATETC* aFormat, LPDATAENTRY* aDataEntry,
+ BOOL aAddorUpdate);
+ bool CopyMediumData(STGMEDIUM* aMediumDst, STGMEDIUM* aMediumSrc,
+ LPFORMATETC aFormat, BOOL aSetData);
+};
+
+#endif // _NSDATAOBJ_H_
diff --git a/widget/windows/nsDataObjCollection.cpp b/widget/windows/nsDataObjCollection.cpp
new file mode 100644
index 0000000000..8750563602
--- /dev/null
+++ b/widget/windows/nsDataObjCollection.cpp
@@ -0,0 +1,370 @@
+/* -*- 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 <shlobj.h>
+
+#include "nsDataObjCollection.h"
+#include "nsClipboard.h"
+#include "IEnumFE.h"
+
+#include <ole2.h>
+
+// {25589C3E-1FAC-47b9-BF43-CAEA89B79533}
+const IID IID_IDataObjCollection = {
+ 0x25589c3e,
+ 0x1fac,
+ 0x47b9,
+ {0xbf, 0x43, 0xca, 0xea, 0x89, 0xb7, 0x95, 0x33}};
+
+/*
+ * Class nsDataObjCollection
+ */
+
+nsDataObjCollection::nsDataObjCollection() {}
+
+nsDataObjCollection::~nsDataObjCollection() { mDataObjects.Clear(); }
+
+// IUnknown interface methods - see iunknown.h for documentation
+STDMETHODIMP nsDataObjCollection::QueryInterface(REFIID riid, void** ppv) {
+ *ppv = nullptr;
+
+ if ((IID_IUnknown == riid) || (IID_IDataObject == riid)) {
+ *ppv = static_cast<IDataObject*>(this);
+ AddRef();
+ return NOERROR;
+ }
+
+ if (IID_IDataObjCollection == riid) {
+ *ppv = static_cast<nsIDataObjCollection*>(this);
+ AddRef();
+ return NOERROR;
+ }
+ // offer to operate asynchronously (required by nsDragService)
+ if (IID_IDataObjectAsyncCapability == riid) {
+ *ppv = static_cast<IDataObjectAsyncCapability*>(this);
+ AddRef();
+ return NOERROR;
+ }
+
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP_(ULONG) nsDataObjCollection::AddRef() { return ++m_cRef; }
+
+STDMETHODIMP_(ULONG) nsDataObjCollection::Release() {
+ if (0 != --m_cRef) return m_cRef;
+
+ delete this;
+
+ return 0;
+}
+
+// IDataObject methods
+STDMETHODIMP nsDataObjCollection::GetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM) {
+ static CLIPFORMAT fileDescriptorFlavorA =
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA);
+ static CLIPFORMAT fileDescriptorFlavorW =
+ ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW);
+ static CLIPFORMAT fileFlavor = ::RegisterClipboardFormat(CFSTR_FILECONTENTS);
+
+ switch (pFE->cfFormat) {
+ case CF_TEXT:
+ case CF_UNICODETEXT:
+ return GetText(pFE, pSTM);
+ case CF_HDROP:
+ return GetFile(pFE, pSTM);
+ default:
+ if (pFE->cfFormat == fileDescriptorFlavorA ||
+ pFE->cfFormat == fileDescriptorFlavorW) {
+ return GetFileDescriptors(pFE, pSTM);
+ }
+ if (pFE->cfFormat == fileFlavor) {
+ return GetFileContents(pFE, pSTM);
+ }
+ }
+ return GetFirstSupporting(pFE, pSTM);
+}
+
+STDMETHODIMP nsDataObjCollection::GetDataHere(LPFORMATETC pFE,
+ LPSTGMEDIUM pSTM) {
+ return E_FAIL;
+}
+
+// Other objects querying to see if we support a particular format
+STDMETHODIMP nsDataObjCollection::QueryGetData(LPFORMATETC pFE) {
+ UINT format = nsClipboard::GetFormat(MULTI_MIME);
+
+ if (format == pFE->cfFormat) {
+ return S_OK;
+ }
+
+ for (uint32_t i = 0; i < mDataObjects.Length(); ++i) {
+ IDataObject* dataObj = mDataObjects.ElementAt(i);
+ if (S_OK == dataObj->QueryGetData(pFE)) {
+ return S_OK;
+ }
+ }
+
+ return DV_E_FORMATETC;
+}
+
+STDMETHODIMP nsDataObjCollection::SetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM,
+ BOOL fRelease) {
+ // Set arbitrary data formats on the first object in the collection and let
+ // it handle the heavy lifting
+ if (mDataObjects.Length() == 0) return E_FAIL;
+ return mDataObjects.ElementAt(0)->SetData(pFE, pSTM, fRelease);
+}
+
+// Registers a DataFlavor/FE pair
+void nsDataObjCollection::AddDataFlavor(const char* aDataFlavor,
+ LPFORMATETC aFE) {
+ // Add the FormatEtc to our list if it's not already there. We don't care
+ // about the internal aDataFlavor because nsDataObj handles that.
+ IEnumFORMATETC* ifEtc;
+ FORMATETC fEtc;
+ ULONG num;
+ if (S_OK != this->EnumFormatEtc(DATADIR_GET, &ifEtc)) return;
+ while (S_OK == ifEtc->Next(1, &fEtc, &num)) {
+ NS_ASSERTION(
+ 1 == num,
+ "Bit off more than we can chew in nsDataObjCollection::AddDataFlavor");
+ if (FormatsMatch(fEtc, *aFE)) {
+ ifEtc->Release();
+ return;
+ }
+ } // If we didn't find a matching format, add this one
+ ifEtc->Release();
+ m_enumFE->AddFormatEtc(aFE);
+}
+
+// We accept ownership of the nsDataObj which we free on destruction
+void nsDataObjCollection::AddDataObject(IDataObject* aDataObj) {
+ nsDataObj* dataObj = reinterpret_cast<nsDataObj*>(aDataObj);
+ mDataObjects.AppendElement(dataObj);
+}
+
+// Methods for getting data
+HRESULT nsDataObjCollection::GetFile(LPFORMATETC pFE, LPSTGMEDIUM pSTM) {
+ STGMEDIUM workingmedium;
+ FORMATETC fe = *pFE;
+ HGLOBAL hGlobalMemory;
+ HRESULT hr;
+ // Make enough space for the header and the trailing null
+ uint32_t buffersize = sizeof(DROPFILES) + sizeof(char16_t);
+ uint32_t alloclen = 0;
+ char16_t* realbuffer;
+ nsAutoString filename;
+
+ hGlobalMemory = GlobalAlloc(GHND, buffersize);
+
+ for (uint32_t i = 0; i < mDataObjects.Length(); ++i) {
+ nsDataObj* dataObj = mDataObjects.ElementAt(i);
+ hr = dataObj->GetData(&fe, &workingmedium);
+ if (hr != S_OK) {
+ switch (hr) {
+ case DV_E_FORMATETC:
+ continue;
+ default:
+ return hr;
+ }
+ }
+ // Now we need to pull out the filename
+ char16_t* buffer = (char16_t*)GlobalLock(workingmedium.hGlobal);
+ if (buffer == nullptr) return E_FAIL;
+ buffer += sizeof(DROPFILES) / sizeof(char16_t);
+ filename = buffer;
+ GlobalUnlock(workingmedium.hGlobal);
+ ReleaseStgMedium(&workingmedium);
+ // Now put the filename into our buffer
+ alloclen = (filename.Length() + 1) * sizeof(char16_t);
+ hGlobalMemory = ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND);
+ if (hGlobalMemory == nullptr) return E_FAIL;
+ realbuffer = (char16_t*)((char*)GlobalLock(hGlobalMemory) + buffersize);
+ if (!realbuffer) return E_FAIL;
+ realbuffer--; // Overwrite the preceding null
+ memcpy(realbuffer, filename.get(), alloclen);
+ GlobalUnlock(hGlobalMemory);
+ buffersize += alloclen;
+ }
+ // We get the last null (on the double null terminator) for free since we used
+ // the zero memory flag when we allocated. All we need to do is fill the
+ // DROPFILES structure
+ DROPFILES* df = (DROPFILES*)GlobalLock(hGlobalMemory);
+ if (!df) return E_FAIL;
+ df->pFiles = sizeof(DROPFILES); // Offset to start of file name string
+ df->fNC = 0;
+ df->pt.x = 0;
+ df->pt.y = 0;
+ df->fWide = TRUE; // utf-16 chars
+ GlobalUnlock(hGlobalMemory);
+ // Finally fill out the STGMEDIUM struct
+ pSTM->tymed = TYMED_HGLOBAL;
+ pSTM->pUnkForRelease = nullptr; // Caller gets to free the data
+ pSTM->hGlobal = hGlobalMemory;
+ return S_OK;
+}
+
+HRESULT nsDataObjCollection::GetText(LPFORMATETC pFE, LPSTGMEDIUM pSTM) {
+ STGMEDIUM workingmedium;
+ FORMATETC fe = *pFE;
+ HGLOBAL hGlobalMemory;
+ HRESULT hr;
+ uint32_t buffersize = 1;
+ uint32_t alloclen = 0;
+
+ hGlobalMemory = GlobalAlloc(GHND, buffersize);
+
+ if (pFE->cfFormat == CF_TEXT) {
+ nsAutoCString text;
+ for (uint32_t i = 0; i < mDataObjects.Length(); ++i) {
+ nsDataObj* dataObj = mDataObjects.ElementAt(i);
+ hr = dataObj->GetData(&fe, &workingmedium);
+ if (hr != S_OK) {
+ switch (hr) {
+ case DV_E_FORMATETC:
+ continue;
+ default:
+ return hr;
+ }
+ }
+ // Now we need to pull out the text
+ char* buffer = (char*)GlobalLock(workingmedium.hGlobal);
+ if (buffer == nullptr) return E_FAIL;
+ text = buffer;
+ GlobalUnlock(workingmedium.hGlobal);
+ ReleaseStgMedium(&workingmedium);
+ // Now put the text into our buffer
+ alloclen = text.Length();
+ hGlobalMemory =
+ ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND);
+ if (hGlobalMemory == nullptr) return E_FAIL;
+ buffer = ((char*)GlobalLock(hGlobalMemory) + buffersize);
+ if (!buffer) return E_FAIL;
+ buffer--; // Overwrite the preceding null
+ memcpy(buffer, text.get(), alloclen);
+ GlobalUnlock(hGlobalMemory);
+ buffersize += alloclen;
+ }
+ pSTM->tymed = TYMED_HGLOBAL;
+ pSTM->pUnkForRelease = nullptr; // Caller gets to free the data
+ pSTM->hGlobal = hGlobalMemory;
+ return S_OK;
+ }
+ if (pFE->cfFormat == CF_UNICODETEXT) {
+ buffersize = sizeof(char16_t);
+ nsAutoString text;
+ for (uint32_t i = 0; i < mDataObjects.Length(); ++i) {
+ nsDataObj* dataObj = mDataObjects.ElementAt(i);
+ hr = dataObj->GetData(&fe, &workingmedium);
+ if (hr != S_OK) {
+ switch (hr) {
+ case DV_E_FORMATETC:
+ continue;
+ default:
+ return hr;
+ }
+ }
+ // Now we need to pull out the text
+ char16_t* buffer = (char16_t*)GlobalLock(workingmedium.hGlobal);
+ if (buffer == nullptr) return E_FAIL;
+ text = buffer;
+ GlobalUnlock(workingmedium.hGlobal);
+ ReleaseStgMedium(&workingmedium);
+ // Now put the text into our buffer
+ alloclen = text.Length() * sizeof(char16_t);
+ hGlobalMemory =
+ ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND);
+ if (hGlobalMemory == nullptr) return E_FAIL;
+ buffer = (char16_t*)((char*)GlobalLock(hGlobalMemory) + buffersize);
+ if (!buffer) return E_FAIL;
+ buffer--; // Overwrite the preceding null
+ memcpy(buffer, text.get(), alloclen);
+ GlobalUnlock(hGlobalMemory);
+ buffersize += alloclen;
+ }
+ pSTM->tymed = TYMED_HGLOBAL;
+ pSTM->pUnkForRelease = nullptr; // Caller gets to free the data
+ pSTM->hGlobal = hGlobalMemory;
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+
+HRESULT nsDataObjCollection::GetFileDescriptors(LPFORMATETC pFE,
+ LPSTGMEDIUM pSTM) {
+ STGMEDIUM workingmedium;
+ FORMATETC fe = *pFE;
+ HGLOBAL hGlobalMemory;
+ HRESULT hr;
+ uint32_t buffersize = sizeof(UINT);
+ uint32_t alloclen = sizeof(FILEDESCRIPTOR);
+
+ hGlobalMemory = GlobalAlloc(GHND, buffersize);
+
+ for (uint32_t i = 0; i < mDataObjects.Length(); ++i) {
+ nsDataObj* dataObj = mDataObjects.ElementAt(i);
+ hr = dataObj->GetData(&fe, &workingmedium);
+ if (hr != S_OK) {
+ switch (hr) {
+ case DV_E_FORMATETC:
+ continue;
+ default:
+ return hr;
+ }
+ }
+ // Now we need to pull out the filedescriptor
+ FILEDESCRIPTOR* buffer =
+ (FILEDESCRIPTOR*)((char*)GlobalLock(workingmedium.hGlobal) +
+ sizeof(UINT));
+ if (buffer == nullptr) return E_FAIL;
+ hGlobalMemory = ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND);
+ if (hGlobalMemory == nullptr) return E_FAIL;
+ FILEGROUPDESCRIPTOR* realbuffer =
+ (FILEGROUPDESCRIPTOR*)GlobalLock(hGlobalMemory);
+ if (!realbuffer) return E_FAIL;
+ FILEDESCRIPTOR* copyloc = (FILEDESCRIPTOR*)((char*)realbuffer + buffersize);
+ memcpy(copyloc, buffer, alloclen);
+ realbuffer->cItems++;
+ GlobalUnlock(hGlobalMemory);
+ GlobalUnlock(workingmedium.hGlobal);
+ ReleaseStgMedium(&workingmedium);
+ buffersize += alloclen;
+ }
+ pSTM->tymed = TYMED_HGLOBAL;
+ pSTM->pUnkForRelease = nullptr; // Caller gets to free the data
+ pSTM->hGlobal = hGlobalMemory;
+ return S_OK;
+}
+
+HRESULT nsDataObjCollection::GetFileContents(LPFORMATETC pFE,
+ LPSTGMEDIUM pSTM) {
+ ULONG num = 0;
+ ULONG numwanted = (pFE->lindex == -1) ? 0 : pFE->lindex;
+ FORMATETC fEtc = *pFE;
+ fEtc.lindex = -1; // We're lying to the data object so it thinks it's alone
+
+ // The key for this data type is to figure out which data object the index
+ // corresponds to and then just pass it along
+ for (uint32_t i = 0; i < mDataObjects.Length(); ++i) {
+ nsDataObj* dataObj = mDataObjects.ElementAt(i);
+ if (dataObj->QueryGetData(&fEtc) != S_OK) continue;
+ if (num == numwanted) return dataObj->GetData(pFE, pSTM);
+ num++;
+ }
+ return DV_E_LINDEX;
+}
+
+HRESULT nsDataObjCollection::GetFirstSupporting(LPFORMATETC pFE,
+ LPSTGMEDIUM pSTM) {
+ // There is no way to pass more than one of this, so just find the first data
+ // object that supports it and pass it along
+ for (uint32_t i = 0; i < mDataObjects.Length(); ++i) {
+ if (mDataObjects.ElementAt(i)->QueryGetData(pFE) == S_OK)
+ return mDataObjects.ElementAt(i)->GetData(pFE, pSTM);
+ }
+ return DV_E_FORMATETC;
+}
diff --git a/widget/windows/nsDataObjCollection.h b/widget/windows/nsDataObjCollection.h
new file mode 100644
index 0000000000..02ec7e8916
--- /dev/null
+++ b/widget/windows/nsDataObjCollection.h
@@ -0,0 +1,92 @@
+/* -*- 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/. */
+
+#ifndef _NSDATAOBJCOLLECTION_H_
+#define _NSDATAOBJCOLLECTION_H_
+
+#include <oleidl.h>
+
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsDataObj.h"
+#include "mozilla/Attributes.h"
+
+#define MULTI_MIME "Mozilla/IDataObjectCollectionFormat"
+
+EXTERN_C const IID IID_IDataObjCollection;
+
+// An interface to make sure we have the right kind of object for D&D
+// this way we can filter out collection objects that aren't ours
+class nsIDataObjCollection : public IUnknown {
+ public:
+};
+
+/*
+ * This ole registered class is used to facilitate drag-drop of objects which
+ * can be adapted by an object derived from CfDragDrop. The CfDragDrop is
+ * associated with instances via SetDragDrop().
+ */
+
+class nsDataObjCollection final : public nsIDataObjCollection,
+ public nsDataObj {
+ public:
+ nsDataObjCollection();
+
+ private:
+ ~nsDataObjCollection() final;
+
+ public: // IUnknown methods - see iunknown.h for documentation
+ STDMETHODIMP_(ULONG) AddRef() final;
+ STDMETHODIMP QueryInterface(REFIID, void**) final;
+ STDMETHODIMP_(ULONG) Release() final;
+
+ private: // DataGet and DataSet helper methods
+ HRESULT GetFile(LPFORMATETC pFE, LPSTGMEDIUM pSTM);
+ HRESULT GetText(LPFORMATETC pFE, LPSTGMEDIUM pSTM);
+ HRESULT GetFileDescriptors(LPFORMATETC pFE, LPSTGMEDIUM pSTM);
+ HRESULT GetFileContents(LPFORMATETC pFE, LPSTGMEDIUM pSTM);
+ HRESULT GetFirstSupporting(LPFORMATETC pFE, LPSTGMEDIUM pSTM);
+
+ using nsDataObj::GetFile;
+ using nsDataObj::GetFileContents;
+ using nsDataObj::GetText;
+
+ // support for clipboard
+ void AddDataFlavor(const char* aDataFlavor, LPFORMATETC aFE) final;
+
+ public: // from nsPIDataObjCollection
+ void AddDataObject(IDataObject* aDataObj);
+ int32_t GetNumDataObjects() { return mDataObjects.Length(); }
+ nsDataObj* GetDataObjectAt(uint32_t aItem) {
+ return mDataObjects.SafeElementAt(aItem, RefPtr<nsDataObj>());
+ }
+
+ public:
+ // Store data in pSTM according to the format specified by pFE, if the
+ // format is supported (supported formats are specified in CfDragDrop::
+ // GetFormats) and return NOERROR; otherwise return DATA_E_FORMATETC. It
+ // is the callers responsibility to free pSTM if NOERROR is returned.
+ STDMETHODIMP GetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM) final;
+
+ // Similar to GetData except that the caller allocates the structure
+ // referenced by pSTM.
+ STDMETHODIMP GetDataHere(LPFORMATETC pFE, LPSTGMEDIUM pSTM) final;
+
+ // Returns S_TRUE if this object supports the format specified by pSTM,
+ // S_FALSE otherwise.
+ STDMETHODIMP QueryGetData(LPFORMATETC pFE) final;
+
+ // Set this objects data according to the format specified by pFE and
+ // the storage medium specified by pSTM and return NOERROR, if the format
+ // is supported. If release is TRUE this object must release the storage
+ // associated with pSTM.
+ STDMETHODIMP SetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM, BOOL release) final;
+
+ private:
+ nsTArray<RefPtr<nsDataObj> > mDataObjects;
+};
+
+#endif //
diff --git a/widget/windows/nsDeviceContextSpecWin.cpp b/widget/windows/nsDeviceContextSpecWin.cpp
new file mode 100644
index 0000000000..ac3bf6f6ed
--- /dev/null
+++ b/widget/windows/nsDeviceContextSpecWin.cpp
@@ -0,0 +1,680 @@
+/* -*- 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 "nsDeviceContextSpecWin.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/gfx/PrintPromise.h"
+#include "mozilla/gfx/PrintTargetPDF.h"
+#include "mozilla/gfx/PrintTargetWindows.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Telemetry.h"
+#include "nsAnonymousTemporaryFile.h"
+
+#include <wchar.h>
+#include <windef.h>
+#include <winspool.h>
+
+#include "nsIWidget.h"
+
+#include "nsTArray.h"
+#include "nsIPrintSettingsWin.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsPrinterWin.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+
+#include "gfxWindowsSurface.h"
+
+#include "nsIFileStreams.h"
+#include "nsWindowsHelpers.h"
+
+#include "mozilla/gfx/Logging.h"
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+# include "mozilla/gfx/PrintTargetSkPDF.h"
+# include "mozilla/gfx/PrintTargetEMF.h"
+# include "nsIUUIDGenerator.h"
+# include "nsDirectoryServiceDefs.h"
+# include "nsPrintfCString.h"
+# include "nsThreadUtils.h"
+#endif
+
+extern mozilla::LazyLogModule gPrintingLog;
+#define PR_PL(_p1) MOZ_LOG(gPrintingLog, mozilla::LogLevel::Debug, _p1)
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+using namespace mozilla::widget;
+#endif
+
+static const wchar_t kDriverName[] = L"WINSPOOL";
+
+//----------------------------------------------------------------------------------
+//---------------
+// static members
+//----------------------------------------------------------------------------------
+nsDeviceContextSpecWin::nsDeviceContextSpecWin() = default;
+
+//----------------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsDeviceContextSpecWin, nsIDeviceContextSpec)
+
+nsDeviceContextSpecWin::~nsDeviceContextSpecWin() {
+ SetDevMode(nullptr);
+
+ if (mTempFile) {
+ mTempFile->Remove(/* recursive = */ false);
+ }
+
+ if (nsCOMPtr<nsIPrintSettingsWin> ps = do_QueryInterface(mPrintSettings)) {
+ ps->SetDeviceName(u""_ns);
+ ps->SetDriverName(u""_ns);
+ ps->SetDevMode(nullptr);
+ }
+}
+
+static bool GetDefaultPrinterName(nsAString& aDefaultPrinterName) {
+ DWORD length = 0;
+ GetDefaultPrinterW(nullptr, &length);
+
+ if (length) {
+ aDefaultPrinterName.SetLength(length);
+ if (GetDefaultPrinterW((LPWSTR)aDefaultPrinterName.BeginWriting(),
+ &length)) {
+ // `length` includes the terminating null, so we subtract that from our
+ // string length.
+ aDefaultPrinterName.SetLength(length - 1);
+ PR_PL(("DEFAULT PRINTER [%s]\n",
+ NS_ConvertUTF16toUTF8(aDefaultPrinterName).get()));
+ return true;
+ }
+ }
+
+ aDefaultPrinterName.Truncate();
+ PR_PL(("NO DEFAULT PRINTER\n"));
+ return false;
+}
+
+//----------------------------------------------------------------------------------
+NS_IMETHODIMP nsDeviceContextSpecWin::Init(nsIPrintSettings* aPrintSettings,
+ bool aIsPrintPreview) {
+ mPrintSettings = aPrintSettings;
+
+ // Get the Printer Name to be used and output format.
+ nsAutoString printerName;
+ if (mPrintSettings) {
+ mOutputFormat = mPrintSettings->GetOutputFormat();
+ mPrintSettings->GetPrinterName(printerName);
+ }
+
+ // If there is no name then use the default printer
+ if (printerName.IsEmpty()) {
+ GetDefaultPrinterName(printerName);
+ }
+
+ // Gather telemetry on the print target type.
+ //
+ // Unfortunately, if we're not using our own internal save-to-pdf codepaths,
+ // there isn't a good way to determine whether a print is going to be to a
+ // physical printer or to a file or some other non-physical output. We do our
+ // best by checking for what seems to be the most common save-to-PDF virtual
+ // printers.
+ //
+ // We use StringBeginsWith below, since printer names are often followed by a
+ // version number or other product differentiating string. (True for doPDF,
+ // novaPDF, PDF-XChange and Soda PDF, for example.)
+ if (mOutputFormat == nsIPrintSettings::kOutputFormatPDF) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"pdf_file"_ns, 1);
+ } else if (StringBeginsWith(printerName, u"Microsoft Print to PDF"_ns) ||
+ StringBeginsWith(printerName, u"Adobe PDF"_ns) ||
+ StringBeginsWith(printerName, u"Bullzip PDF Printer"_ns) ||
+ StringBeginsWith(printerName, u"CutePDF Writer"_ns) ||
+ StringBeginsWith(printerName, u"doPDF"_ns) ||
+ StringBeginsWith(printerName, u"Foxit Reader PDF Printer"_ns) ||
+ StringBeginsWith(printerName, u"Nitro PDF Creator"_ns) ||
+ StringBeginsWith(printerName, u"novaPDF"_ns) ||
+ StringBeginsWith(printerName, u"PDF-XChange"_ns) ||
+ StringBeginsWith(printerName, u"PDF24 PDF"_ns) ||
+ StringBeginsWith(printerName, u"PDFCreator"_ns) ||
+ StringBeginsWith(printerName, u"PrimoPDF"_ns) ||
+ StringBeginsWith(printerName, u"Soda PDF"_ns) ||
+ StringBeginsWith(printerName, u"Solid PDF Creator"_ns) ||
+ StringBeginsWith(printerName,
+ u"Universal Document Converter"_ns)) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"pdf_file"_ns, 1);
+ } else if (printerName.EqualsLiteral("Microsoft XPS Document Writer")) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"xps_file"_ns, 1);
+ } else {
+ nsAString::const_iterator start, end;
+ printerName.BeginReading(start);
+ printerName.EndReading(end);
+ if (CaseInsensitiveFindInReadable(u"pdf"_ns, start, end)) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"pdf_unknown"_ns, 1);
+ } else {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE,
+ u"unknown"_ns, 1);
+ }
+ }
+
+ nsresult rv = NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE;
+ if (aPrintSettings) {
+#ifdef MOZ_ENABLE_SKIA_PDF
+ nsAutoString printViaPdf;
+ Preferences::GetString("print.print_via_pdf_encoder", printViaPdf);
+ if (printViaPdf.EqualsLiteral("skia-pdf")) {
+ mPrintViaSkPDF = true;
+ }
+#endif
+
+ // If we're in the child or we're printing to PDF we only need information
+ // from the print settings.
+ if (XRE_IsContentProcess() ||
+ mOutputFormat == nsIPrintSettings::kOutputFormatPDF) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrintSettingsWin> psWin(do_QueryInterface(aPrintSettings));
+ if (psWin) {
+ nsAutoString deviceName;
+ nsAutoString driverName;
+ psWin->GetDeviceName(deviceName);
+ psWin->GetDriverName(driverName);
+
+ LPDEVMODEW devMode;
+ psWin->GetDevMode(&devMode); // creates new memory (makes a copy)
+
+ if (!deviceName.IsEmpty() && !driverName.IsEmpty() && devMode) {
+ // Scaling is special, it is one of the few
+ // devMode items that we control in layout
+ if (devMode->dmFields & DM_SCALE) {
+ double scale = double(devMode->dmScale) / 100.0f;
+ if (scale != 1.0) {
+ aPrintSettings->SetScaling(scale);
+ devMode->dmScale = 100;
+ }
+ }
+
+ SetDeviceName(deviceName);
+ SetDriverName(driverName);
+ SetDevMode(devMode);
+
+ return NS_OK;
+ } else {
+ PR_PL(
+ ("***** nsDeviceContextSpecWin::Init - "
+ "deviceName/driverName/devMode was NULL!\n"));
+ if (devMode) ::HeapFree(::GetProcessHeap(), 0, devMode);
+ }
+ }
+ } else {
+ PR_PL(("***** nsDeviceContextSpecWin::Init - aPrintSettingswas NULL!\n"));
+ }
+
+ if (printerName.IsEmpty()) {
+ return rv;
+ }
+
+ return GetDataFromPrinter(printerName, mPrintSettings);
+}
+
+//----------------------------------------------------------
+
+already_AddRefed<PrintTarget> nsDeviceContextSpecWin::MakePrintTarget() {
+ NS_ASSERTION(mDevMode || mOutputFormat == nsIPrintSettings::kOutputFormatPDF,
+ "DevMode can't be NULL here unless we're printing to PDF.");
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ if (mPrintViaSkPDF) {
+ double width, height;
+ mPrintSettings->GetEffectivePageSize(&width, &height);
+ if (width <= 0 || height <= 0) {
+ return nullptr;
+ }
+
+ // convert twips to points
+ width /= TWIPS_PER_POINT_FLOAT;
+ height /= TWIPS_PER_POINT_FLOAT;
+ IntSize size = IntSize::Ceil(width, height);
+
+ if (mOutputFormat == nsIPrintSettings::kOutputFormatPDF) {
+ nsString filename;
+ // TODO(dshin):
+ // - Does this handle bug 1659470?
+ // - Should this code path be enabled, we should use temporary files and
+ // then move the file in `EndDocument()`.
+ mPrintSettings->GetToFileName(filename);
+
+ nsAutoCString printFile(NS_ConvertUTF16toUTF8(filename).get());
+ auto skStream = MakeUnique<SkFILEWStream>(printFile.get());
+ return PrintTargetSkPDF::CreateOrNull(std::move(skStream), size);
+ }
+
+ if (mDevMode) {
+ NS_WARNING_ASSERTION(!mDriverName.IsEmpty(), "No driver!");
+ HDC dc =
+ ::CreateDCW(mDriverName.get(), mDeviceName.get(), nullptr, mDevMode);
+ if (!dc) {
+ gfxCriticalError(gfxCriticalError::DefaultOptions(false))
+ << "Failed to create device context in GetSurfaceForPrinter";
+ return nullptr;
+ }
+ return PrintTargetEMF::CreateOrNull(dc, size);
+ }
+ }
+#endif
+
+ if (mOutputFormat == nsIPrintSettings::kOutputFormatPDF) {
+ double width, height;
+ mPrintSettings->GetEffectiveSheetSize(&width, &height);
+ if (width <= 0 || height <= 0) {
+ return nullptr;
+ }
+
+ // convert twips to points
+ width /= TWIPS_PER_POINT_FLOAT;
+ height /= TWIPS_PER_POINT_FLOAT;
+
+ auto stream = [&]() -> nsCOMPtr<nsIOutputStream> {
+ if (mPrintSettings->GetOutputDestination() ==
+ nsIPrintSettings::kOutputDestinationStream) {
+ nsCOMPtr<nsIOutputStream> out;
+ mPrintSettings->GetOutputStream(getter_AddRefs(out));
+ return out;
+ }
+
+ // Even if the destination may be a named path, write to a temp file -
+ // this is consistent with behaviour of `PrintTarget` on other platforms.
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(mTempFile));
+ file = mTempFile;
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIFileOutputStream> stream =
+ do_CreateInstance("@mozilla.org/network/file-output-stream;1");
+ if (NS_FAILED(stream->Init(file, -1, -1, 0))) {
+ return nullptr;
+ }
+ return stream;
+ }();
+
+ return PrintTargetPDF::CreateOrNull(stream, IntSize::Ceil(width, height));
+ }
+
+ if (mDevMode) {
+ NS_WARNING_ASSERTION(!mDriverName.IsEmpty(), "No driver!");
+ HDC dc =
+ ::CreateDCW(mDriverName.get(), mDeviceName.get(), nullptr, mDevMode);
+ if (!dc) {
+ gfxCriticalError(gfxCriticalError::DefaultOptions(false))
+ << "Failed to create device context in GetSurfaceForPrinter";
+ return nullptr;
+ }
+
+ // The PrintTargetWindows takes over ownership of this DC
+ return PrintTargetWindows::CreateOrNull(dc);
+ }
+
+ return nullptr;
+}
+
+RefPtr<PrintEndDocumentPromise> nsDeviceContextSpecWin::EndDocument() {
+ if (mPrintSettings->GetOutputDestination() !=
+ nsIPrintSettings::kOutputDestinationFile ||
+ mOutputFormat != nsIPrintSettings::kOutputFormatPDF) {
+ return PrintEndDocumentPromise::CreateAndResolve(true, __func__);
+ }
+
+#ifdef MOZ_ENABLE_SKIA_PDF
+ if (mPrintViaSkPDF) {
+ return PrintEndDocumentPromise::CreateAndResolve(true, __func__);
+ }
+#endif
+
+ MOZ_ASSERT(mTempFile, "No handle to temporary PDF file.");
+
+ nsAutoString targetPath;
+ mPrintSettings->GetToFileName(targetPath);
+
+ if (targetPath.IsEmpty()) {
+ return PrintEndDocumentPromise::CreateAndResolve(true, __func__);
+ }
+
+ // We still need to move the file to its actual destination.
+ nsCOMPtr<nsIFile> destFile;
+ auto rv = NS_NewLocalFile(targetPath, false, getter_AddRefs(destFile));
+ if (NS_FAILED(rv)) {
+ return PrintEndDocumentPromise::CreateAndReject(rv, __func__);
+ }
+
+ return nsIDeviceContextSpec::EndDocumentAsync(
+ __func__,
+ [destFile = std::move(destFile),
+ tempFile = std::move(mTempFile)]() -> nsresult {
+ nsAutoString destLeafName;
+ MOZ_TRY(destFile->GetLeafName(destLeafName));
+
+ nsCOMPtr<nsIFile> destDir;
+ MOZ_TRY(destFile->GetParent(getter_AddRefs(destDir)));
+
+ // This should be fine - Windows API calls usually prevent moving
+ // between different volumes (See Win32 API's `MOVEFILE_COPY_ALLOWED`
+ // flag), but we handle that down this call.
+ MOZ_TRY(tempFile->MoveTo(destDir, destLeafName));
+ return NS_OK;
+ });
+}
+
+//----------------------------------------------------------------------------------
+void nsDeviceContextSpecWin::SetDeviceName(const nsAString& aDeviceName) {
+ mDeviceName = aDeviceName;
+}
+
+//----------------------------------------------------------------------------------
+void nsDeviceContextSpecWin::SetDriverName(const nsAString& aDriverName) {
+ mDriverName = aDriverName;
+}
+
+//----------------------------------------------------------------------------------
+void nsDeviceContextSpecWin::SetDevMode(LPDEVMODEW aDevMode) {
+ if (mDevMode) {
+ ::HeapFree(::GetProcessHeap(), 0, mDevMode);
+ }
+
+ mDevMode = aDevMode;
+}
+
+//------------------------------------------------------------------
+void nsDeviceContextSpecWin::GetDevMode(LPDEVMODEW& aDevMode) {
+ aDevMode = mDevMode;
+}
+
+#define DISPLAY_LAST_ERROR
+
+//----------------------------------------------------------------------------------
+// Setup the object's data member with the selected printer's data
+nsresult nsDeviceContextSpecWin::GetDataFromPrinter(const nsAString& aName,
+ nsIPrintSettings* aPS) {
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsHPRINTER hPrinter = nullptr;
+ const nsString& flat = PromiseFlatString(aName);
+ wchar_t* name =
+ (wchar_t*)flat.get(); // Windows APIs use non-const name argument
+
+ BOOL status = ::OpenPrinterW(name, &hPrinter, nullptr);
+ if (status) {
+ nsAutoPrinter autoPrinter(hPrinter);
+
+ LPDEVMODEW pDevMode;
+
+ // Allocate a buffer of the correct size.
+ LONG needed =
+ ::DocumentPropertiesW(nullptr, hPrinter, name, nullptr, nullptr, 0);
+ if (needed < 0) {
+ PR_PL(
+ ("**** nsDeviceContextSpecWin::GetDataFromPrinter - Couldn't get "
+ "size of DEVMODE using DocumentPropertiesW(pDeviceName = \"%s\"). "
+ "GetLastEror() = %08lx\n",
+ NS_ConvertUTF16toUTF8(aName).get(), GetLastError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Some drivers do not return the correct size for their DEVMODE, so we
+ // over-allocate to try and compensate.
+ // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5)
+ needed *= 2;
+ pDevMode =
+ (LPDEVMODEW)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, needed);
+ if (!pDevMode) return NS_ERROR_FAILURE;
+
+ // Get the default DevMode for the printer and modify it for our needs.
+ LONG ret = ::DocumentPropertiesW(nullptr, hPrinter, name, pDevMode, nullptr,
+ DM_OUT_BUFFER);
+
+ if (ret == IDOK && aPS) {
+ nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aPS);
+ MOZ_ASSERT(psWin);
+ psWin->CopyToNative(pDevMode);
+ // Sets back the changes we made to the DevMode into the Printer Driver
+ ret = ::DocumentPropertiesW(nullptr, hPrinter, name, pDevMode, pDevMode,
+ DM_IN_BUFFER | DM_OUT_BUFFER);
+
+ // We need to copy the final DEVMODE settings back to our print settings,
+ // because they may have been set from invalid prefs.
+ if (ret == IDOK) {
+ // We need to get information from the device as well.
+ nsAutoHDC printerDC(::CreateICW(kDriverName, name, nullptr, pDevMode));
+ if (NS_WARN_IF(!printerDC)) {
+ ::HeapFree(::GetProcessHeap(), 0, pDevMode);
+ return NS_ERROR_FAILURE;
+ }
+
+ psWin->CopyFromNative(printerDC, pDevMode);
+ }
+ }
+
+ if (ret != IDOK) {
+ ::HeapFree(::GetProcessHeap(), 0, pDevMode);
+ PR_PL(
+ ("***** nsDeviceContextSpecWin::GetDataFromPrinter - "
+ "DocumentProperties call failed code: %ld/0x%lx\n",
+ ret, ret));
+ DISPLAY_LAST_ERROR
+ return NS_ERROR_FAILURE;
+ }
+
+ SetDevMode(
+ pDevMode); // cache the pointer and takes responsibility for the memory
+
+ SetDeviceName(aName);
+
+ SetDriverName(nsDependentString(kDriverName));
+
+ rv = NS_OK;
+ } else {
+ rv = NS_ERROR_GFX_PRINTER_NAME_NOT_FOUND;
+ PR_PL(
+ ("***** nsDeviceContextSpecWin::GetDataFromPrinter - Couldn't open "
+ "printer: [%s]\n",
+ NS_ConvertUTF16toUTF8(aName).get()));
+ DISPLAY_LAST_ERROR
+ }
+ return rv;
+}
+
+//***********************************************************
+// Printer List
+//***********************************************************
+
+nsPrinterListWin::~nsPrinterListWin() = default;
+
+// Helper to get the array of PRINTER_INFO_4 records from the OS into a
+// caller-supplied byte array; returns the number of records present.
+static unsigned GetPrinterInfo4(nsTArray<BYTE>& aBuffer) {
+ const DWORD kLevel = 4;
+ DWORD needed = 0;
+ DWORD count = 0;
+ const DWORD kFlags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
+ BOOL ok = ::EnumPrintersW(kFlags,
+ nullptr, // Name
+ kLevel, // Level
+ nullptr, // pPrinterEnum
+ 0, // cbBuf (buffer size)
+ &needed, // Bytes needed in buffer
+ &count);
+ if (needed > 0) {
+ if (!aBuffer.SetLength(needed, fallible)) {
+ return 0;
+ }
+ ok = ::EnumPrintersW(kFlags, nullptr, kLevel, aBuffer.Elements(),
+ aBuffer.Length(), &needed, &count);
+ }
+ if (!ok) {
+ return 0;
+ }
+ return count;
+}
+
+nsTArray<nsPrinterListBase::PrinterInfo> nsPrinterListWin::Printers() const {
+ PR_PL(("nsPrinterListWin::Printers\n"));
+
+ AutoTArray<BYTE, 1024> buffer;
+ unsigned count = GetPrinterInfo4(buffer);
+
+ if (!count) {
+ PR_PL(("[No printers found]\n"));
+ return {};
+ }
+
+ const auto* printers =
+ reinterpret_cast<const _PRINTER_INFO_4W*>(buffer.Elements());
+ nsTArray<PrinterInfo> list;
+ for (unsigned i = 0; i < count; i++) {
+ // For LOCAL printers, we check whether OpenPrinter succeeds, and omit
+ // them from the list if not. This avoids presenting printers that the
+ // user cannot actually use (e.g. due to Windows permissions).
+ // For NETWORK printers, this check may block for a long time (waiting for
+ // network timeout), so we skip it; if the user tries to access a printer
+ // that isn't available, we'll have to show an error later.
+ // (We always need to be able to handle an error, anyhow, as the printer
+ // could get disconnected after we've created the list, for example.)
+ bool isAvailable = false;
+ if (printers[i].Attributes & PRINTER_ATTRIBUTE_NETWORK) {
+ isAvailable = true;
+ } else if (printers[i].Attributes & PRINTER_ATTRIBUTE_LOCAL) {
+ HANDLE handle;
+ if (::OpenPrinterW(printers[i].pPrinterName, &handle, nullptr)) {
+ ::ClosePrinter(handle);
+ isAvailable = true;
+ }
+ }
+ if (isAvailable) {
+ list.AppendElement(PrinterInfo{nsString(printers[i].pPrinterName)});
+ PR_PL(("Printer Name: %s\n",
+ NS_ConvertUTF16toUTF8(printers[i].pPrinterName).get()));
+ }
+ }
+
+ if (list.IsEmpty()) {
+ PR_PL(("[No usable printers found]\n"));
+ return {};
+ }
+
+ list.Sort([](const PrinterInfo& a, const PrinterInfo& b) {
+ size_t len = std::min(a.mName.Length(), b.mName.Length());
+ int result = CaseInsensitiveCompare(a.mName.BeginReading(),
+ b.mName.BeginReading(), len);
+ return result ? result : int(a.mName.Length()) - int(b.mName.Length());
+ });
+
+ return list;
+}
+
+Maybe<nsPrinterListBase::PrinterInfo> nsPrinterListWin::PrinterByName(
+ nsString aName) const {
+ Maybe<PrinterInfo> rv;
+
+ AutoTArray<BYTE, 1024> buffer;
+ unsigned count = GetPrinterInfo4(buffer);
+
+ const auto* printers =
+ reinterpret_cast<const _PRINTER_INFO_4W*>(buffer.Elements());
+ for (unsigned i = 0; i < count; ++i) {
+ if (aName.Equals(nsString(printers[i].pPrinterName))) {
+ rv.emplace(PrinterInfo{aName});
+ break;
+ }
+ }
+
+ return rv;
+}
+
+Maybe<nsPrinterListBase::PrinterInfo> nsPrinterListWin::PrinterBySystemName(
+ nsString aName) const {
+ return PrinterByName(std::move(aName));
+}
+
+RefPtr<nsIPrinter> nsPrinterListWin::CreatePrinter(PrinterInfo aInfo) const {
+ return nsPrinterWin::Create(mCommonPaperInfo, std::move(aInfo.mName));
+}
+
+nsresult nsPrinterListWin::SystemDefaultPrinterName(nsAString& aName) const {
+ if (!GetDefaultPrinterName(aName)) {
+ NS_WARNING("Uh oh, GetDefaultPrinterName failed");
+ // Indicate failure by leaving aName untouched, i.e. the empty string.
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrinterListWin::InitPrintSettingsFromPrinter(
+ const nsAString& aPrinterName, nsIPrintSettings* aPrintSettings) {
+ NS_ENSURE_ARG_POINTER(aPrintSettings);
+
+ if (aPrinterName.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // When printing to PDF on Windows there is no associated printer driver.
+ int16_t outputFormat = aPrintSettings->GetOutputFormat();
+ if (outputFormat == nsIPrintSettings::kOutputFormatPDF) {
+ return NS_OK;
+ }
+
+ RefPtr<nsDeviceContextSpecWin> devSpecWin = new nsDeviceContextSpecWin();
+ if (!devSpecWin) return NS_ERROR_OUT_OF_MEMORY;
+
+ // If the settings have already been initialized from prefs then pass these to
+ // GetDataFromPrinter, so that they are saved to the printer.
+ bool initializedFromPrefs;
+ nsresult rv =
+ aPrintSettings->GetIsInitializedFromPrefs(&initializedFromPrefs);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (initializedFromPrefs) {
+ // If we pass in print settings to GetDataFromPrinter it already copies
+ // things back to the settings, so we can return here.
+ return devSpecWin->GetDataFromPrinter(aPrinterName, aPrintSettings);
+ }
+
+ devSpecWin->GetDataFromPrinter(aPrinterName);
+
+ LPDEVMODEW devmode;
+ devSpecWin->GetDevMode(devmode);
+ if (NS_WARN_IF(!devmode)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aPrintSettings->SetPrinterName(aPrinterName);
+
+ // We need to get information from the device as well.
+ const nsString& flat = PromiseFlatString(aPrinterName);
+ char16ptr_t printerName = flat.get();
+ HDC dc = ::CreateICW(kDriverName, printerName, nullptr, devmode);
+ if (NS_WARN_IF(!dc)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aPrintSettings);
+ MOZ_ASSERT(psWin);
+ psWin->CopyFromNative(dc, devmode);
+ ::DeleteDC(dc);
+
+ return NS_OK;
+}
diff --git a/widget/windows/nsDeviceContextSpecWin.h b/widget/windows/nsDeviceContextSpecWin.h
new file mode 100644
index 0000000000..fff6472d62
--- /dev/null
+++ b/widget/windows/nsDeviceContextSpecWin.h
@@ -0,0 +1,98 @@
+/* -*- 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/. */
+
+#ifndef nsDeviceContextSpecWin_h___
+#define nsDeviceContextSpecWin_h___
+
+#include "nsCOMPtr.h"
+#include "nsIDeviceContextSpec.h"
+#include "nsPrinterListBase.h"
+#include "nsIPrintSettings.h"
+#include <windows.h>
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/PrintPromise.h"
+
+class nsIFile;
+class nsIWidget;
+
+class nsDeviceContextSpecWin : public nsIDeviceContextSpec {
+ public:
+ nsDeviceContextSpecWin();
+
+ NS_DECL_ISUPPORTS
+
+ already_AddRefed<PrintTarget> MakePrintTarget() final;
+ NS_IMETHOD BeginDocument(const nsAString& aTitle,
+ const nsAString& aPrintToFileName,
+ int32_t aStartPage, int32_t aEndPage) override {
+ return NS_OK;
+ }
+ RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() override;
+ NS_IMETHOD BeginPage(const IntSize& aSizeInPoints) override { return NS_OK; }
+ NS_IMETHOD EndPage() override { return NS_OK; }
+
+ NS_IMETHOD Init(nsIPrintSettings* aPS, bool aIsPrintPreview) override;
+
+ void GetDriverName(nsAString& aDriverName) const {
+ aDriverName = mDriverName;
+ }
+ void GetDeviceName(nsAString& aDeviceName) const {
+ aDeviceName = mDeviceName;
+ }
+
+ // The GetDevMode will return a pointer to a DevMode
+ // whether it is from the Global memory handle or just the DevMode
+ // To get the DevMode from the Global memory Handle it must lock it
+ // So this call must be paired with a call to UnlockGlobalHandle
+ void GetDevMode(LPDEVMODEW& aDevMode);
+
+ // helper functions
+ nsresult GetDataFromPrinter(const nsAString& aName,
+ nsIPrintSettings* aPS = nullptr);
+
+ protected:
+ void SetDeviceName(const nsAString& aDeviceName);
+ void SetDriverName(const nsAString& aDriverName);
+ void SetDevMode(LPDEVMODEW aDevMode);
+
+ virtual ~nsDeviceContextSpecWin();
+
+ nsString mDriverName;
+ nsString mDeviceName;
+ LPDEVMODEW mDevMode = nullptr;
+
+ int16_t mOutputFormat = nsIPrintSettings::kOutputFormatNative;
+
+ // A temporary file to create an "anonymous" print target. See bug 1664253,
+ // this should ideally not be needed.
+ nsCOMPtr<nsIFile> mTempFile;
+};
+
+//-------------------------------------------------------------------------
+// Printer List
+//-------------------------------------------------------------------------
+class nsPrinterListWin final : public nsPrinterListBase {
+ public:
+ NS_IMETHOD InitPrintSettingsFromPrinter(const nsAString&,
+ nsIPrintSettings*) final;
+
+ nsTArray<PrinterInfo> Printers() const final;
+ RefPtr<nsIPrinter> CreatePrinter(PrinterInfo) const final;
+
+ nsPrinterListWin() = default;
+
+ protected:
+ nsresult SystemDefaultPrinterName(nsAString&) const final;
+
+ mozilla::Maybe<PrinterInfo> PrinterByName(nsString) const final;
+ mozilla::Maybe<PrinterInfo> PrinterBySystemName(
+ nsString aPrinterName) const final;
+
+ private:
+ ~nsPrinterListWin();
+};
+
+#endif
diff --git a/widget/windows/nsDragService.cpp b/widget/windows/nsDragService.cpp
new file mode 100644
index 0000000000..80f545d9a7
--- /dev/null
+++ b/widget/windows/nsDragService.cpp
@@ -0,0 +1,664 @@
+/* -*- 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 <ole2.h>
+#include <oleidl.h>
+#include <shlobj.h>
+#include <shlwapi.h>
+
+// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN
+#include <shellapi.h>
+
+#include "mozilla/RefPtr.h"
+#include "nsDragService.h"
+#include "nsITransferable.h"
+#include "nsDataObj.h"
+
+#include "nsWidgetsCID.h"
+#include "nsNativeDragTarget.h"
+#include "nsNativeDragSource.h"
+#include "nsClipboard.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsDataObjCollection.h"
+
+#include "nsArrayUtils.h"
+#include "nsString.h"
+#include "nsEscape.h"
+#include "nsIScreenManager.h"
+#include "nsToolkit.h"
+#include "nsCRT.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsUnicharUtils.h"
+#include "nsRect.h"
+#include "nsMathUtils.h"
+#include "WinUtils.h"
+#include "KeyboardLayout.h"
+#include "gfxContext.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/Tools.h"
+#include "mozilla/ScopeExit.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+
+//-------------------------------------------------------------------------
+//
+// DragService constructor
+//
+//-------------------------------------------------------------------------
+nsDragService::nsDragService()
+ : mDataObject(nullptr), mSentLocalDropEvent(false) {}
+
+//-------------------------------------------------------------------------
+//
+// DragService destructor
+//
+//-------------------------------------------------------------------------
+nsDragService::~nsDragService() { NS_IF_RELEASE(mDataObject); }
+
+bool nsDragService::CreateDragImage(nsINode* aDOMNode,
+ const Maybe<CSSIntRegion>& aRegion,
+ SHDRAGIMAGE* psdi) {
+ if (!psdi) return false;
+
+ memset(psdi, 0, sizeof(SHDRAGIMAGE));
+ if (!aDOMNode) return false;
+
+ // Prepare the drag image
+ LayoutDeviceIntRect dragRect;
+ RefPtr<SourceSurface> surface;
+ nsPresContext* pc;
+ DrawDrag(aDOMNode, aRegion, mScreenPosition, &dragRect, &surface, &pc);
+ if (!surface) return false;
+
+ uint32_t bmWidth = dragRect.Width(), bmHeight = dragRect.Height();
+
+ if (bmWidth == 0 || bmHeight == 0) return false;
+
+ psdi->crColorKey = CLR_NONE;
+
+ RefPtr<DataSourceSurface> dataSurface = Factory::CreateDataSourceSurface(
+ IntSize(bmWidth, bmHeight), SurfaceFormat::B8G8R8A8);
+ NS_ENSURE_TRUE(dataSurface, false);
+
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ_WRITE, &map)) {
+ return false;
+ }
+
+ RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
+ BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride,
+ dataSurface->GetFormat());
+ if (!dt) {
+ dataSurface->Unmap();
+ return false;
+ }
+
+ dt->DrawSurface(
+ surface,
+ Rect(0, 0, dataSurface->GetSize().width, dataSurface->GetSize().height),
+ Rect(0, 0, surface->GetSize().width, surface->GetSize().height),
+ DrawSurfaceOptions(), DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+ dt->Flush();
+
+ BITMAPV5HEADER bmih;
+ memset((void*)&bmih, 0, sizeof(BITMAPV5HEADER));
+ bmih.bV5Size = sizeof(BITMAPV5HEADER);
+ bmih.bV5Width = bmWidth;
+ bmih.bV5Height = -(int32_t)bmHeight; // flip vertical
+ bmih.bV5Planes = 1;
+ bmih.bV5BitCount = 32;
+ bmih.bV5Compression = BI_BITFIELDS;
+ bmih.bV5RedMask = 0x00FF0000;
+ bmih.bV5GreenMask = 0x0000FF00;
+ bmih.bV5BlueMask = 0x000000FF;
+ bmih.bV5AlphaMask = 0xFF000000;
+
+ HDC hdcSrc = CreateCompatibleDC(nullptr);
+ void* lpBits = nullptr;
+ if (hdcSrc) {
+ psdi->hbmpDragImage =
+ ::CreateDIBSection(hdcSrc, (BITMAPINFO*)&bmih, DIB_RGB_COLORS,
+ (void**)&lpBits, nullptr, 0);
+ if (psdi->hbmpDragImage && lpBits) {
+ CopySurfaceDataToPackedArray(map.mData, static_cast<uint8_t*>(lpBits),
+ dataSurface->GetSize(), map.mStride,
+ BytesPerPixel(dataSurface->GetFormat()));
+ }
+
+ psdi->sizeDragImage.cx = bmWidth;
+ psdi->sizeDragImage.cy = bmHeight;
+
+ const auto screenPoint =
+ LayoutDeviceIntPoint::Round(mScreenPosition * pc->CSSToDevPixelScale());
+ psdi->ptOffset.x = screenPoint.x - dragRect.X();
+ psdi->ptOffset.y = screenPoint.y - dragRect.Y();
+
+ DeleteDC(hdcSrc);
+ }
+
+ dataSurface->Unmap();
+
+ return psdi->hbmpDragImage != nullptr;
+}
+
+//-------------------------------------------------------------------------
+nsresult nsDragService::InvokeDragSessionImpl(
+ nsIArray* anArrayTransferables, const Maybe<CSSIntRegion>& aRegion,
+ uint32_t aActionType) {
+ // Try and get source URI of the items that are being dragged
+ nsIURI* uri = nullptr;
+
+ RefPtr<dom::Document> doc(mSourceDocument);
+ if (doc) {
+ uri = doc->GetDocumentURI();
+ }
+
+ uint32_t numItemsToDrag = 0;
+ nsresult rv = anArrayTransferables->GetLength(&numItemsToDrag);
+ if (!numItemsToDrag) return NS_ERROR_FAILURE;
+
+ // The clipboard class contains some static utility methods that we
+ // can use to create an IDataObject from the transferable
+
+ // if we're dragging more than one item, we need to create a
+ // "collection" object to fake out the OS. This collection contains
+ // one |IDataObject| for each transferable. If there is just the one
+ // (most cases), only pass around the native |IDataObject|.
+ RefPtr<IDataObject> itemToDrag;
+ if (numItemsToDrag > 1) {
+ nsDataObjCollection* dataObjCollection = new nsDataObjCollection();
+ if (!dataObjCollection) return NS_ERROR_OUT_OF_MEMORY;
+ itemToDrag = dataObjCollection;
+ for (uint32_t i = 0; i < numItemsToDrag; ++i) {
+ nsCOMPtr<nsITransferable> trans =
+ do_QueryElementAt(anArrayTransferables, i);
+ if (trans) {
+ RefPtr<IDataObject> dataObj;
+ rv = nsClipboard::CreateNativeDataObject(trans, getter_AddRefs(dataObj),
+ uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Add the flavors to the collection object too
+ rv = nsClipboard::SetupNativeDataObject(trans, dataObjCollection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dataObjCollection->AddDataObject(dataObj);
+ }
+ }
+ } // if dragging multiple items
+ else {
+ nsCOMPtr<nsITransferable> trans =
+ do_QueryElementAt(anArrayTransferables, 0);
+ if (trans) {
+ rv = nsClipboard::CreateNativeDataObject(trans,
+ getter_AddRefs(itemToDrag), uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } // else dragging a single object
+
+ // Create a drag image if support is available
+ IDragSourceHelper* pdsh;
+ if (SUCCEEDED(CoCreateInstance(CLSID_DragDropHelper, nullptr,
+ CLSCTX_INPROC_SERVER, IID_IDragSourceHelper,
+ (void**)&pdsh))) {
+ SHDRAGIMAGE sdi;
+ if (CreateDragImage(mSourceNode, aRegion, &sdi)) {
+ if (FAILED(pdsh->InitializeFromBitmap(&sdi, itemToDrag)))
+ DeleteObject(sdi.hbmpDragImage);
+ }
+ pdsh->Release();
+ }
+
+ // Kick off the native drag session
+ return StartInvokingDragSession(itemToDrag, aActionType);
+}
+
+static HWND GetSourceWindow(dom::Document* aSourceDocument) {
+ if (!aSourceDocument) {
+ return nullptr;
+ }
+
+ auto* pc = aSourceDocument->GetPresContext();
+ if (!pc) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIWidget> widget = pc->GetRootWidget();
+ if (!widget) {
+ return nullptr;
+ }
+
+ return (HWND)widget->GetNativeData(NS_NATIVE_WINDOW);
+}
+
+//-------------------------------------------------------------------------
+nsresult nsDragService::StartInvokingDragSession(IDataObject* aDataObj,
+ uint32_t aActionType) {
+ // To do the drag we need to create an object that
+ // implements the IDataObject interface (for OLE)
+ RefPtr<nsNativeDragSource> nativeDragSrc =
+ new nsNativeDragSource(mDataTransfer);
+
+ // Now figure out what the native drag effect should be
+ DWORD winDropRes;
+ DWORD effects = DROPEFFECT_SCROLL;
+ if (aActionType & DRAGDROP_ACTION_COPY) {
+ effects |= DROPEFFECT_COPY;
+ }
+ if (aActionType & DRAGDROP_ACTION_MOVE) {
+ effects |= DROPEFFECT_MOVE;
+ }
+ if (aActionType & DRAGDROP_ACTION_LINK) {
+ effects |= DROPEFFECT_LINK;
+ }
+
+ // XXX not sure why we bother to cache this, it can change during
+ // the drag
+ mDragAction = aActionType;
+ mSentLocalDropEvent = false;
+
+ // Start dragging
+ StartDragSession();
+ OpenDragPopup();
+
+ RefPtr<IDataObjectAsyncCapability> pAsyncOp;
+ // Offer to do an async drag
+ if (SUCCEEDED(aDataObj->QueryInterface(IID_IDataObjectAsyncCapability,
+ getter_AddRefs(pAsyncOp)))) {
+ pAsyncOp->SetAsyncMode(VARIANT_TRUE);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("When did our data object stop being async");
+ }
+
+ // Call the native D&D method
+ HRESULT res = ::DoDragDrop(aDataObj, nativeDragSrc, effects, &winDropRes);
+
+ // In cases where the drop operation completed outside the application,
+ // update the source node's DataTransfer dropEffect value so it is up to date.
+ if (!mSentLocalDropEvent) {
+ uint32_t dropResult;
+ // Order is important, since multiple flags can be returned.
+ if (winDropRes & DROPEFFECT_COPY)
+ dropResult = DRAGDROP_ACTION_COPY;
+ else if (winDropRes & DROPEFFECT_LINK)
+ dropResult = DRAGDROP_ACTION_LINK;
+ else if (winDropRes & DROPEFFECT_MOVE)
+ dropResult = DRAGDROP_ACTION_MOVE;
+ else
+ dropResult = DRAGDROP_ACTION_NONE;
+
+ if (mDataTransfer) {
+ if (res == DRAGDROP_S_DROP) // Success
+ mDataTransfer->SetDropEffectInt(dropResult);
+ else
+ mDataTransfer->SetDropEffectInt(DRAGDROP_ACTION_NONE);
+ }
+ }
+
+ mUserCancelled = nativeDragSrc->UserCancelled();
+
+ // We're done dragging, get the cursor position and end the drag
+ // Use GetMessagePos to get the position of the mouse at the last message
+ // seen by the event loop. (Bug 489729)
+ // Note that we must convert this from device pixels back to Windows logical
+ // pixels (bug 818927).
+ DWORD pos = ::GetMessagePos();
+ POINT cpos;
+ cpos.x = GET_X_LPARAM(pos);
+ cpos.y = GET_Y_LPARAM(pos);
+ if (auto wnd = GetSourceWindow(mSourceDocument)) {
+ // Convert from screen to client coordinates like nsWindow::InitEvent does.
+ ::ScreenToClient(wnd, &cpos);
+ }
+ SetDragEndPoint(LayoutDeviceIntPoint(cpos.x, cpos.y));
+
+ ModifierKeyState modifierKeyState;
+ EndDragSession(true, modifierKeyState.GetModifiers());
+
+ mDoingDrag = false;
+
+ return DRAGDROP_S_DROP == res ? NS_OK : NS_ERROR_FAILURE;
+}
+
+//-------------------------------------------------------------------------
+// Make Sure we have the right kind of object
+nsDataObjCollection* nsDragService::GetDataObjCollection(
+ IDataObject* aDataObj) {
+ nsDataObjCollection* dataObjCol = nullptr;
+ if (aDataObj) {
+ nsIDataObjCollection* dataObj;
+ if (aDataObj->QueryInterface(IID_IDataObjCollection, (void**)&dataObj) ==
+ S_OK) {
+ dataObjCol = static_cast<nsDataObjCollection*>(aDataObj);
+ dataObj->Release();
+ }
+ }
+
+ return dataObjCol;
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsDragService::GetNumDropItems(uint32_t* aNumItems) {
+ if (!mDataObject) {
+ *aNumItems = 0;
+ return NS_OK;
+ }
+
+ if (IsCollectionObject(mDataObject)) {
+ nsDataObjCollection* dataObjCol = GetDataObjCollection(mDataObject);
+ // If the count cannot be determined just return 0.
+ // This can happen if we have collection data of type
+ // MULTI_MIME ("Mozilla/IDataObjectCollectionFormat") on the clipboard
+ // from another process but we can't obtain an IID_IDataObjCollection
+ // from this process.
+ *aNumItems = dataObjCol ? dataObjCol->GetNumDataObjects() : 0;
+ return NS_OK;
+ }
+ // Next check if we have a file drop. Return the number of files in
+ // the file drop as the number of items we have, pretending like we
+ // actually have > 1 drag item.
+ FORMATETC fe2;
+ SET_FORMATETC(fe2, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (SUCCEEDED(mDataObject->QueryGetData(&fe2))) {
+ STGMEDIUM stm;
+ if (FAILED(mDataObject->GetData(&fe2, &stm))) {
+ *aNumItems = 1;
+ return NS_OK;
+ }
+ HDROP hdrop = static_cast<HDROP>(GlobalLock(stm.hGlobal));
+ MOZ_ASSERT(hdrop != NULL);
+ *aNumItems = ::DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0);
+ ::GlobalUnlock(stm.hGlobal);
+ ::ReleaseStgMedium(&stm);
+ // Data may be provided later, so assume we have 1 item
+ if (*aNumItems == 0) {
+ *aNumItems = 1;
+ }
+ return NS_OK;
+ }
+ // Next check if we have a virtual file drop.
+ SET_FORMATETC(fe2, nsClipboard::GetClipboardFileDescriptorFormatW(), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ STGMEDIUM stm;
+
+ if (SUCCEEDED(mDataObject->GetData(&fe2, &stm))) {
+ LPFILEGROUPDESCRIPTOR pDesc =
+ static_cast<LPFILEGROUPDESCRIPTOR>(GlobalLock(stm.hGlobal));
+ if (pDesc) {
+ *aNumItems = pDesc->cItems;
+ }
+ GlobalUnlock(stm.hGlobal);
+ ReleaseStgMedium(&stm);
+ return NS_OK;
+ }
+ *aNumItems = 1;
+ return NS_OK;
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsDragService::GetData(nsITransferable* aTransferable, uint32_t anItem) {
+ // This typcially happens on a drop, the target would be asking
+ // for it's transferable to be filled in
+ // Use a static clipboard utility method for this
+ if (!mDataObject) return NS_ERROR_FAILURE;
+
+ nsresult dataFound = NS_ERROR_FAILURE;
+
+ if (IsCollectionObject(mDataObject)) {
+ // multiple items, use |anItem| as an index into our collection
+ nsDataObjCollection* dataObjCol = GetDataObjCollection(mDataObject);
+ uint32_t cnt = dataObjCol->GetNumDataObjects();
+ if (anItem < cnt) {
+ IDataObject* dataObj = dataObjCol->GetDataObjectAt(anItem);
+ dataFound = nsClipboard::GetDataFromDataObject(dataObj, 0, nullptr,
+ aTransferable);
+ } else
+ NS_WARNING("Index out of range!");
+ } else {
+ // If they are asking for item "0", we can just get it...
+ if (anItem == 0) {
+ dataFound = nsClipboard::GetDataFromDataObject(mDataObject, anItem,
+ nullptr, aTransferable);
+ } else {
+ // It better be a file drop, or else non-zero indexes are invalid!
+ FORMATETC fe2;
+ FORMATETC fe3;
+ SET_FORMATETC(fe2, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ SET_FORMATETC(fe3, nsClipboard::GetClipboardFileDescriptorFormatW(), 0,
+ DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (SUCCEEDED(mDataObject->QueryGetData(&fe2)) ||
+ SUCCEEDED(mDataObject->QueryGetData(&fe3)))
+ dataFound = nsClipboard::GetDataFromDataObject(mDataObject, anItem,
+ nullptr, aTransferable);
+ else
+ NS_WARNING(
+ "Reqesting non-zero index, but clipboard data is not a "
+ "collection!");
+ }
+ }
+ return dataFound;
+}
+
+//---------------------------------------------------------
+NS_IMETHODIMP
+nsDragService::SetIDataObject(IDataObject* aDataObj) {
+ // When the native drag starts the DragService gets
+ // the IDataObject that is being dragged
+ NS_IF_RELEASE(mDataObject);
+ mDataObject = aDataObj;
+ NS_IF_ADDREF(mDataObject);
+
+ if (MOZ_DRAGSERVICE_LOG_ENABLED()) {
+ MOZ_DRAGSERVICE_LOG("nsDragService::SetIDataObject (%p)", mDataObject);
+ IEnumFORMATETC* pEnum = nullptr;
+ if (mDataObject &&
+ S_OK == mDataObject->EnumFormatEtc(DATADIR_GET, &pEnum)) {
+ MOZ_DRAGSERVICE_LOG(" formats in DataObject:");
+
+ FORMATETC fEtc;
+ while (S_OK == pEnum->Next(1, &fEtc, nullptr)) {
+ nsAutoString format;
+ WinUtils::GetClipboardFormatAsString(fEtc.cfFormat, format);
+ MOZ_DRAGSERVICE_LOG(" FORMAT %s",
+ NS_ConvertUTF16toUTF8(format).get());
+ }
+ pEnum->Release();
+ }
+ }
+
+ return NS_OK;
+}
+
+//---------------------------------------------------------
+void nsDragService::SetDroppedLocal() {
+ // Sent from the native drag handler, letting us know
+ // a drop occurred within the application vs. outside of it.
+ mSentLocalDropEvent = true;
+ return;
+}
+
+//-------------------------------------------------------------------------
+NS_IMETHODIMP
+nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) {
+ if (!aDataFlavor || !mDataObject || !_retval) {
+ MOZ_DRAGSERVICE_LOG("%s: error", __PRETTY_FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+
+ *_retval = false;
+ auto logging = MakeScopeExit([&] {
+ MOZ_DRAGSERVICE_LOG("IsDataFlavorSupported: %s is%s found", aDataFlavor,
+ *_retval ? "" : " not");
+ });
+
+ FORMATETC fe;
+ UINT format = 0;
+
+ if (IsCollectionObject(mDataObject)) {
+ // We know we have one of our special collection objects.
+ format = nsClipboard::GetFormat(aDataFlavor);
+ SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1,
+ TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI);
+
+ // See if any one of the IDataObjects in the collection supports
+ // this data type
+ nsDataObjCollection* dataObjCol = GetDataObjCollection(mDataObject);
+ if (dataObjCol) {
+ uint32_t cnt = dataObjCol->GetNumDataObjects();
+ for (uint32_t i = 0; i < cnt; ++i) {
+ IDataObject* dataObj = dataObjCol->GetDataObjectAt(i);
+ if (S_OK == dataObj->QueryGetData(&fe)) {
+ *_retval = true; // found it!
+ }
+ }
+ }
+ return NS_OK;
+ }
+
+ // Ok, so we have a single object. Check to see if has the correct
+ // data type. Since this can come from an outside app, we also
+ // need to see if we need to perform text->unicode conversion if
+ // the client asked for unicode and it wasn't available.
+ format = nsClipboard::GetFormat(aDataFlavor);
+ SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1,
+ TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI);
+ if (mDataObject->QueryGetData(&fe) == S_OK) {
+ *_retval = true; // found it!
+ return NS_OK;
+ }
+
+ // We haven't found the exact flavor the client asked for, but
+ // maybe we can still find it from something else that's in the
+ // data object.
+ if (strcmp(aDataFlavor, kTextMime) == 0) {
+ // If unicode wasn't there, it might exist as CF_TEXT, client asked
+ // for unicode and it wasn't present, check if we
+ // have CF_TEXT. We'll handle the actual data substitution in
+ // the data object.
+ SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1,
+ TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI);
+ if (mDataObject->QueryGetData(&fe) == S_OK) {
+ *_retval = true; // found it!
+ }
+ return NS_OK;
+ }
+
+ if (strcmp(aDataFlavor, kURLMime) == 0) {
+ // client asked for a url and it wasn't present, but if we
+ // have a file, then we have a URL to give them (the path, or
+ // the internal URL if an InternetShortcut).
+ format = nsClipboard::GetFormat(kFileMime);
+ SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1,
+ TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI);
+ if (mDataObject->QueryGetData(&fe) == S_OK) {
+ *_retval = true; // found it!
+ }
+ return NS_OK;
+ }
+
+ if (format == CF_HDROP) {
+ // Dragging a link from browsers creates both a URL and a FILE which is a
+ // *.url shortcut in the data object. The file is useful when dropping in
+ // Windows Explorer to create a internet shortcut. But when dropping in the
+ // browser, users do not expect to have this file. So do not try to look up
+ // virtal file if there is a URL in the data object.
+ format = nsClipboard::GetFormat(kURLMime);
+ SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (mDataObject->QueryGetData(&fe) == S_OK) {
+ return NS_OK;
+ }
+
+ // If the client wants a file, maybe we find a virtual file.
+ format = nsClipboard::GetClipboardFileDescriptorFormatW();
+ SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (mDataObject->QueryGetData(&fe) == S_OK) {
+ *_retval = true; // found it!
+ }
+
+ // XXX should we fall back to CFSTR_FILEDESCRIPTORA?
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+//
+// IsCollectionObject
+//
+// Determine if this is a single |IDataObject| or one of our private
+// collection objects. We know the difference because our collection
+// object will respond to supporting the private |MULTI_MIME| format.
+//
+bool nsDragService::IsCollectionObject(IDataObject* inDataObj) {
+ bool isCollection = false;
+
+ // setup the format object to ask for the MULTI_MIME format. We only
+ // need to do this once
+ static UINT sFormat = 0;
+ static FORMATETC sFE;
+ if (!sFormat) {
+ sFormat = nsClipboard::GetFormat(MULTI_MIME);
+ SET_FORMATETC(sFE, sFormat, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ }
+
+ // ask the object if it supports it. If yes, we have a collection
+ // object
+ if (inDataObj->QueryGetData(&sFE) == S_OK) isCollection = true;
+
+ return isCollection;
+
+} // IsCollectionObject
+
+//
+// EndDragSession
+//
+// Override the default to make sure that we release the data object
+// when the drag ends. It seems that OLE doesn't like to let apps quit
+// w/out crashing when we're still holding onto their data
+//
+NS_IMETHODIMP
+nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
+ // Bug 100180: If we've got mouse events captured, make sure we release it -
+ // that way, if we happen to call EndDragSession before diving into a nested
+ // event loop, we can still respond to mouse events.
+ if (::GetCapture()) {
+ ::ReleaseCapture();
+ }
+
+ nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers);
+ NS_IF_RELEASE(mDataObject);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDragService::UpdateDragImage(nsINode* aImage, int32_t aImageX,
+ int32_t aImageY) {
+ if (!mDataObject) {
+ return NS_OK;
+ }
+
+ nsBaseDragService::UpdateDragImage(aImage, aImageX, aImageY);
+
+ IDragSourceHelper* pdsh;
+ if (SUCCEEDED(CoCreateInstance(CLSID_DragDropHelper, nullptr,
+ CLSCTX_INPROC_SERVER, IID_IDragSourceHelper,
+ (void**)&pdsh))) {
+ SHDRAGIMAGE sdi;
+ if (CreateDragImage(mSourceNode, Nothing(), &sdi)) {
+ nsNativeDragTarget::DragImageChanged();
+ if (FAILED(pdsh->InitializeFromBitmap(&sdi, mDataObject)))
+ DeleteObject(sdi.hbmpDragImage);
+ }
+ pdsh->Release();
+ }
+
+ return NS_OK;
+}
diff --git a/widget/windows/nsDragService.h b/widget/windows/nsDragService.h
new file mode 100644
index 0000000000..5a86a74f76
--- /dev/null
+++ b/widget/windows/nsDragService.h
@@ -0,0 +1,67 @@
+/* -*- 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/. */
+
+#ifndef nsDragService_h__
+#define nsDragService_h__
+
+#include "nsBaseDragService.h"
+#include <windows.h>
+#include <shlobj.h>
+
+struct IDataObject;
+class nsDataObjCollection;
+
+/**
+ * Native Win32 DragService wrapper
+ */
+
+class nsDragService : public nsBaseDragService {
+ public:
+ nsDragService();
+ virtual ~nsDragService();
+
+ // nsBaseDragService
+ MOZ_CAN_RUN_SCRIPT virtual nsresult InvokeDragSessionImpl(
+ nsIArray* anArrayTransferables,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ uint32_t aActionType);
+
+ // nsIDragSession
+ NS_IMETHOD GetData(nsITransferable* aTransferable, uint32_t anItem) override;
+ NS_IMETHOD GetNumDropItems(uint32_t* aNumItems) override;
+ NS_IMETHOD IsDataFlavorSupported(const char* aDataFlavor,
+ bool* _retval) override;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndDragSession(bool aDoneDrag,
+ uint32_t aKeyModifiers) override;
+ NS_IMETHOD UpdateDragImage(nsINode* aImage, int32_t aImageX,
+ int32_t aImageY) override;
+
+ // native impl.
+ NS_IMETHOD SetIDataObject(IDataObject* aDataObj);
+ MOZ_CAN_RUN_SCRIPT nsresult StartInvokingDragSession(IDataObject* aDataObj,
+ uint32_t aActionType);
+
+ // A drop occurred within the application vs. outside of it.
+ void SetDroppedLocal();
+
+ IDataObject* GetDataObject() { return mDataObject; }
+
+ protected:
+ nsDataObjCollection* GetDataObjCollection(IDataObject* aDataObj);
+
+ // determine if we have a single data object or one of our private
+ // collections
+ bool IsCollectionObject(IDataObject* inDataObj);
+
+ // Create a bitmap for drag operations
+ bool CreateDragImage(nsINode* aDOMNode,
+ const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
+ SHDRAGIMAGE* psdi);
+
+ IDataObject* mDataObject;
+ bool mSentLocalDropEvent;
+};
+
+#endif // nsDragService_h__
diff --git a/widget/windows/nsFilePicker.cpp b/widget/windows/nsFilePicker.cpp
new file mode 100644
index 0000000000..310c54bb40
--- /dev/null
+++ b/widget/windows/nsFilePicker.cpp
@@ -0,0 +1,1078 @@
+/* -*- 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 "nsFilePicker.h"
+
+#include <cderr.h>
+#include <shlobj.h>
+#include <shlwapi.h>
+#include <sysinfoapi.h>
+#include <winerror.h>
+#include <winuser.h>
+#include <utility>
+
+#include "ContentAnalysis.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ipc/UtilityProcessManager.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "nsCRT.h"
+#include "nsEnumeratorUtils.h"
+#include "nsIContentAnalysis.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsToolkit.h"
+#include "nsWindow.h"
+#include "WinUtils.h"
+
+#include "mozilla/glean/GleanMetrics.h"
+
+#include "mozilla/widget/filedialog/WinFileDialogCommands.h"
+#include "mozilla/widget/filedialog/WinFileDialogParent.h"
+
+using mozilla::UniquePtr;
+
+using namespace mozilla::widget;
+
+UniquePtr<char16_t[], nsFilePicker::FreeDeleter>
+ nsFilePicker::sLastUsedUnicodeDirectory;
+
+using mozilla::LogLevel;
+
+#define MAX_EXTENSION_LENGTH 10
+
+///////////////////////////////////////////////////////////////////////////////
+// Helper classes
+
+// Manages matching PickerOpen/PickerClosed calls on the parent widget.
+class AutoWidgetPickerState {
+ static RefPtr<nsWindow> GetWindowForWidget(nsIWidget* aWidget) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!aWidget) {
+ return nullptr;
+ }
+ HWND hwnd = (HWND)aWidget->GetNativeData(NS_NATIVE_WINDOW);
+ return RefPtr(WinUtils::GetNSWindowPtr(hwnd));
+ }
+
+ public:
+ explicit AutoWidgetPickerState(nsIWidget* aWidget)
+ : mWindow(GetWindowForWidget(aWidget)) {
+ MOZ_ASSERT(mWindow);
+ if (mWindow) mWindow->PickerOpen();
+ }
+ ~AutoWidgetPickerState() {
+ // may be null if moved-from
+ if (mWindow) mWindow->PickerClosed();
+ }
+
+ AutoWidgetPickerState(AutoWidgetPickerState const&) = delete;
+ AutoWidgetPickerState(AutoWidgetPickerState&& that) noexcept = default;
+
+ private:
+ RefPtr<nsWindow> mWindow;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIFilePicker
+
+nsFilePicker::nsFilePicker() : mSelectedType(1) {}
+
+NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
+
+NS_IMETHODIMP nsFilePicker::Init(
+ mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ nsIFilePicker::Mode aMode,
+ mozilla::dom::BrowsingContext* aBrowsingContext) {
+ // Don't attempt to open a real file-picker in headless mode.
+ if (gfxPlatform::IsHeadless()) {
+ return nsresult::NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aParent);
+ nsIDocShell* docShell = window ? window->GetDocShell() : nullptr;
+ mLoadContext = do_QueryInterface(docShell);
+
+ return nsBaseFilePicker::Init(aParent, aTitle, aMode, aBrowsingContext);
+}
+
+namespace mozilla::detail {
+// Boilerplate for remotely showing a file dialog.
+template <typename ActionType,
+ typename ReturnType = typename decltype(std::declval<ActionType>()(
+ nullptr))::element_type::ResolveValueType>
+static auto ShowRemote(ActionType&& action)
+ -> RefPtr<MozPromise<ReturnType, HRESULT, true>> {
+ using RetPromise = MozPromise<ReturnType, HRESULT, true>;
+
+ constexpr static const auto fail = []() {
+ return RetPromise::CreateAndReject(E_FAIL, __PRETTY_FUNCTION__);
+ };
+
+ auto mgr = mozilla::ipc::UtilityProcessManager::GetSingleton();
+ if (!mgr) {
+ MOZ_ASSERT(false);
+ return fail();
+ }
+
+ auto wfda = mgr->CreateWinFileDialogActor();
+ if (!wfda) {
+ return fail();
+ }
+
+ using mozilla::widget::filedialog::sLogFileDialog;
+
+ return wfda->Then(
+ mozilla::GetMainThreadSerialEventTarget(),
+ "nsFilePicker ShowRemote acquire",
+ [action = std::forward<ActionType>(action)](
+ filedialog::ProcessProxy const& p) -> RefPtr<RetPromise> {
+ MOZ_LOG(sLogFileDialog, LogLevel::Info,
+ ("nsFilePicker ShowRemote first callback: p = [%p]", p.get()));
+
+ // false positive: not actually redundant
+ // NOLINTNEXTLINE(readability-redundant-smartptr-get)
+ return action(p.get())->Then(
+ mozilla::GetMainThreadSerialEventTarget(),
+ "nsFilePicker ShowRemote call",
+ [p](ReturnType ret) {
+ return RetPromise::CreateAndResolve(std::move(ret),
+ __PRETTY_FUNCTION__);
+ },
+ [](mozilla::ipc::ResponseRejectReason error) {
+ MOZ_LOG(sLogFileDialog, LogLevel::Error,
+ ("IPC call rejected: %zu", size_t(error)));
+ return fail();
+ });
+ },
+ [](nsresult error) -> RefPtr<RetPromise> {
+ MOZ_LOG(sLogFileDialog, LogLevel::Error,
+ ("could not acquire WinFileDialog: %zu", size_t(error)));
+ return fail();
+ });
+}
+
+// fd_async
+//
+// Wrapper-namespace for the AsyncExecute() and AsyncAll() functions.
+namespace fd_async {
+
+// Implementation details of, specifically, the AsyncExecute() and AsyncAll()
+// functions.
+namespace details {
+// Helper for generically copying ordinary types and nsTArray (which lacks a
+// copy constructor) in the same breath.
+template <typename T>
+static T Copy(T const& val) {
+ return val;
+}
+template <typename T>
+static nsTArray<T> Copy(nsTArray<T> const& arr) {
+ return arr.Clone();
+}
+
+// The possible execution strategies of AsyncExecute.
+enum Strategy { Local, Remote, RemoteWithFallback };
+
+// Decode the relevant preference to determine the desired execution-
+// strategy.
+static Strategy GetStrategy() {
+ int32_t const pref =
+ mozilla::StaticPrefs::widget_windows_utility_process_file_picker();
+ switch (pref) {
+ case -1:
+ return Local;
+ case 2:
+ return Remote;
+ case 1:
+ return RemoteWithFallback;
+
+ default:
+#ifdef NIGHTLY_BUILD
+ // on Nightly builds, fall back to local on failure
+ return RemoteWithFallback;
+#else
+ // on release and beta, remain local-only for now
+ return Local;
+#endif
+ }
+};
+
+template <typename T>
+class AsyncAllIterator final {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(AsyncAllIterator)
+ AsyncAllIterator(
+ nsTArray<T> aItems,
+ std::function<
+ RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)>
+ aPredicate,
+ RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> aPromise)
+ : mItems(std::move(aItems)),
+ mNextIndex(0),
+ mPredicate(std::move(aPredicate)),
+ mPromise(std::move(aPromise)) {}
+
+ void StartIterating() { ContinueIterating(); }
+
+ private:
+ ~AsyncAllIterator() = default;
+ void ContinueIterating() {
+ if (mNextIndex >= mItems.Length()) {
+ mPromise->Resolve(true, __func__);
+ return;
+ }
+ mPredicate(mItems.ElementAt(mNextIndex))
+ ->Then(
+ mozilla::GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr{this}](bool aResult) {
+ if (!aResult) {
+ self->mPromise->Resolve(false, __func__);
+ return;
+ }
+ ++self->mNextIndex;
+ self->ContinueIterating();
+ },
+ [self = RefPtr{this}](nsresult aError) {
+ self->mPromise->Reject(aError, __func__);
+ });
+ }
+ nsTArray<T> mItems;
+ uint32_t mNextIndex;
+ std::function<RefPtr<mozilla::MozPromise<bool, nsresult, true>>(
+ const T& item)>
+ mPredicate;
+ RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> mPromise;
+};
+
+namespace telemetry {
+static uint32_t Delta(uint64_t tb, uint64_t ta) {
+ // FILETIMEs are 100ns intervals; we reduce that to 1ms.
+ // (`u32::max()` milliseconds is roughly 47.91 days.)
+ return uint32_t((tb - ta) / 10'000);
+};
+static nsCString HexString(HRESULT val) {
+ return nsPrintfCString("%08lX", val);
+};
+
+static void RecordSuccess(uint64_t (&&time)[2]) {
+ auto [t0, t1] = time;
+
+ namespace glean_fd = mozilla::glean::file_dialog;
+ glean_fd::FallbackExtra extra{
+ .hresultLocal = Nothing(),
+ .hresultRemote = Nothing(),
+ .succeeded = Some(true),
+ .timeLocal = Nothing(),
+ .timeRemote = Some(Delta(t1, t0)),
+ };
+ glean_fd::fallback.Record(Some(extra));
+}
+
+static void RecordFailure(uint64_t (&&time)[3], HRESULT hrRemote,
+ HRESULT hrLocal) {
+ auto [t0, t1, t2] = time;
+
+ {
+ namespace glean_fd = mozilla::glean::file_dialog;
+ glean_fd::FallbackExtra extra{
+ .hresultLocal = Some(HexString(hrLocal)),
+ .hresultRemote = Some(HexString(hrRemote)),
+ .succeeded = Some(false),
+ .timeLocal = Some(Delta(t2, t1)),
+ .timeRemote = Some(Delta(t1, t0)),
+ };
+ glean_fd::fallback.Record(Some(extra));
+ }
+}
+
+} // namespace telemetry
+} // namespace details
+
+// Invoke either or both of a "do locally" and "do remotely" function with the
+// provided arguments, depending on the relevant preference-value and whether
+// or not the remote version fails.
+//
+// Both functions must be asynchronous, returning a `RefPtr<MozPromise<...>>`.
+// "Failure" is defined as the promise being rejected.
+template <typename Fn1, typename Fn2, typename... Args>
+static auto AsyncExecute(Fn1 local, Fn2 remote, Args const&... args)
+ -> std::invoke_result_t<Fn1, Args...> {
+ using namespace details;
+
+ static_assert(std::is_same_v<std::invoke_result_t<Fn1, Args...>,
+ std::invoke_result_t<Fn2, Args...>>);
+ using PromiseT = typename std::invoke_result_t<Fn1, Args...>::element_type;
+
+ constexpr static char kFunctionName[] = "LocalAndOrRemote::AsyncExecute";
+
+ switch (GetStrategy()) {
+ case Local:
+ return local(args...);
+
+ case Remote:
+ return remote(args...);
+
+ case RemoteWithFallback:
+ // more complicated; continue below
+ break;
+ }
+
+ // capture time for telemetry
+ constexpr static const auto GetTime = []() -> uint64_t {
+ FILETIME t;
+ ::GetSystemTimeAsFileTime(&t);
+ return (uint64_t(t.dwHighDateTime) << 32) | t.dwLowDateTime;
+ };
+ uint64_t const t0 = GetTime();
+
+ return remote(args...)->Then(
+ NS_GetCurrentThread(), kFunctionName,
+ [t0](typename PromiseT::ResolveValueType result) -> RefPtr<PromiseT> {
+ // success; stop here
+ auto const t1 = GetTime();
+ // record success
+ telemetry::RecordSuccess({t0, t1});
+ return PromiseT::CreateAndResolve(result, kFunctionName);
+ },
+ // initialized lambda pack captures are C++20 (clang 9, gcc 9);
+ // `make_tuple` is just a C++17 workaround
+ [=, tuple = std::make_tuple(Copy(args)...)](
+ typename PromiseT::RejectValueType err) mutable -> RefPtr<PromiseT> {
+ // failure; record time
+ auto const t1 = GetTime();
+ HRESULT const hrRemote = err;
+
+ // retry locally...
+ auto p0 = std::apply(local, std::move(tuple));
+ // ...then record the telemetry event
+ return p0->Then(
+ NS_GetCurrentThread(), kFunctionName,
+ [t0, t1,
+ hrRemote](typename PromiseT::ResolveOrRejectValue const& val)
+ -> RefPtr<PromiseT> {
+ auto const t2 = GetTime();
+ HRESULT const hrLocal = val.IsReject() ? val.RejectValue() : S_OK;
+ telemetry::RecordFailure({t0, t1, t2}, hrRemote, hrLocal);
+
+ return PromiseT::CreateAndResolveOrReject(val, kFunctionName);
+ });
+ });
+}
+
+// Asynchronously invokes `aPredicate` on each member of `aItems`.
+// Yields `false` (and stops immediately) if any invocation of
+// `predicate` yielded `false`; otherwise yields `true`.
+template <typename T>
+static RefPtr<mozilla::MozPromise<bool, nsresult, true>> AsyncAll(
+ nsTArray<T> aItems,
+ std::function<
+ RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)>
+ aPredicate) {
+ auto promise =
+ mozilla::MakeRefPtr<mozilla::MozPromise<bool, nsresult, true>::Private>(
+ __func__);
+ auto iterator = mozilla::MakeRefPtr<details::AsyncAllIterator<T>>(
+ std::move(aItems), aPredicate, promise);
+ iterator->StartIterating();
+ return promise;
+}
+} // namespace fd_async
+
+using fd_async::AsyncAll;
+using fd_async::AsyncExecute;
+
+} // namespace mozilla::detail
+
+/* static */
+nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerRemote(
+ HWND parent, filedialog::FileDialogType type,
+ nsTArray<filedialog::Command> const& commands) {
+ using mozilla::widget::filedialog::sLogFileDialog;
+ return mozilla::detail::ShowRemote(
+ [parent, type,
+ commands = commands.Clone()](filedialog::WinFileDialogParent* p) {
+ MOZ_LOG(sLogFileDialog, LogLevel::Info,
+ ("%s: p = [%p]", __PRETTY_FUNCTION__, p));
+ return p->SendShowFileDialog((uintptr_t)parent, type, commands);
+ });
+}
+
+/* static */
+nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerRemote(
+ HWND parent, nsTArray<filedialog::Command> const& commands) {
+ using mozilla::widget::filedialog::sLogFileDialog;
+ return mozilla::detail::ShowRemote([parent, commands = commands.Clone()](
+ filedialog::WinFileDialogParent* p) {
+ MOZ_LOG(sLogFileDialog, LogLevel::Info,
+ ("%s: p = [%p]", __PRETTY_FUNCTION__, p));
+ return p->SendShowFolderDialog((uintptr_t)parent, commands);
+ });
+}
+
+/* static */
+nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerLocal(
+ HWND parent, filedialog::FileDialogType type,
+ nsTArray<filedialog::Command> const& commands) {
+ return filedialog::SpawnFilePicker(parent, type, commands.Clone());
+}
+
+/* static */
+nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerLocal(
+ HWND parent, nsTArray<filedialog::Command> const& commands) {
+ return filedialog::SpawnFolderPicker(parent, commands.Clone());
+}
+
+/*
+ * Folder picker invocation
+ */
+
+/*
+ * Show a folder picker.
+ *
+ * @param aInitialDir The initial directory. The last-used directory will be
+ * used if left blank.
+ * @return A promise which:
+ * - resolves to true if a file was selected successfully (in which
+ * case mUnicodeFile will be updated);
+ * - resolves to false if the dialog was cancelled by the user;
+ * - is rejected with the associated HRESULT if some error occurred.
+ */
+RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFolderPicker(
+ const nsString& aInitialDir) {
+ using Promise = mozilla::MozPromise<bool, HRESULT, true>;
+ constexpr static auto Ok = [](bool val) {
+ return Promise::CreateAndResolve(val, "nsFilePicker::ShowFolderPicker");
+ };
+ constexpr static auto NotOk = [](HRESULT val = E_FAIL) {
+ return Promise::CreateAndReject(val, "nsFilePicker::ShowFolderPicker");
+ };
+
+ namespace fd = ::mozilla::widget::filedialog;
+ nsTArray<fd::Command> commands = {
+ fd::SetOptions(FOS_PICKFOLDERS),
+ fd::SetTitle(mTitle),
+ };
+
+ if (!mOkButtonLabel.IsEmpty()) {
+ commands.AppendElement(fd::SetOkButtonLabel(mOkButtonLabel));
+ }
+
+ if (!aInitialDir.IsEmpty()) {
+ commands.AppendElement(fd::SetFolder(aInitialDir));
+ }
+
+ ScopedRtlShimWindow shim(mParentWidget.get());
+ AutoWidgetPickerState awps(mParentWidget);
+
+ return mozilla::detail::AsyncExecute(&ShowFolderPickerLocal,
+ &ShowFolderPickerRemote, shim.get(),
+ commands)
+ ->Then(
+ NS_GetCurrentThread(), __PRETTY_FUNCTION__,
+ [self = RefPtr(this), shim = std::move(shim),
+ awps = std::move(awps)](Maybe<nsString> val) {
+ if (val) {
+ self->mUnicodeFile = val.extract();
+ return Ok(true);
+ }
+ return Ok(false);
+ },
+ [](HRESULT err) {
+ NS_WARNING("ShowFolderPicker failed");
+ return NotOk(err);
+ });
+}
+
+/*
+ * File open and save picker invocation
+ */
+
+/*
+ * Show a file picker.
+ *
+ * @param aInitialDir The initial directory. The last-used directory will be
+ * used if left blank.
+ * @return A promise which:
+ * - resolves to true if one or more files were selected successfully
+ * (in which case mUnicodeFile and/or mFiles will be updated);
+ * - resolves to false if the dialog was cancelled by the user;
+ * - is rejected with the associated HRESULT if some error occurred.
+ */
+RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFilePicker(
+ const nsString& aInitialDir) {
+ AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER);
+
+ using Promise = mozilla::MozPromise<bool, HRESULT, true>;
+ constexpr static auto Ok = [](bool val) {
+ return Promise::CreateAndResolve(val, "nsFilePicker::ShowFilePicker");
+ };
+ constexpr static auto NotOk = [](HRESULT val = E_FAIL) {
+ return Promise::CreateAndReject(val, "nsFilePicker::ShowFilePicker");
+ };
+
+ namespace fd = ::mozilla::widget::filedialog;
+ nsTArray<fd::Command> commands;
+ // options
+ {
+ FILEOPENDIALOGOPTIONS fos = 0;
+ fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT | FOS_FORCEFILESYSTEM;
+
+ // Handle add to recent docs settings
+ if (IsPrivacyModeEnabled() || !mAddToRecentDocs) {
+ fos |= FOS_DONTADDTORECENT;
+ }
+
+ // mode specific
+ switch (mMode) {
+ case modeOpen:
+ fos |= FOS_FILEMUSTEXIST;
+ break;
+
+ case modeOpenMultiple:
+ fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT;
+ break;
+
+ case modeSave:
+ fos |= FOS_NOREADONLYRETURN;
+ // Don't follow shortcuts when saving a shortcut, this can be used
+ // to trick users (bug 271732)
+ if (IsDefaultPathLink()) fos |= FOS_NODEREFERENCELINKS;
+ break;
+
+ case modeGetFolder:
+ MOZ_ASSERT(false, "file-picker opened in directory-picker mode");
+ return NotOk(E_FAIL);
+ }
+
+ commands.AppendElement(fd::SetOptions(fos));
+ }
+
+ // initial strings
+
+ // title
+ commands.AppendElement(fd::SetTitle(mTitle));
+
+ // default filename
+ if (!mDefaultFilename.IsEmpty()) {
+ // Prevent the shell from expanding environment variables by removing
+ // the % characters that are used to delimit them.
+ nsAutoString sanitizedFilename(mDefaultFilename);
+ sanitizedFilename.ReplaceChar('%', '_');
+
+ commands.AppendElement(fd::SetFileName(sanitizedFilename));
+ }
+
+ // default extension to append to new files
+ if (!mDefaultExtension.IsEmpty()) {
+ // We don't want environment variables expanded in the extension either.
+ nsAutoString sanitizedExtension(mDefaultExtension);
+ sanitizedExtension.ReplaceChar('%', '_');
+
+ commands.AppendElement(fd::SetDefaultExtension(sanitizedExtension));
+ } else if (IsDefaultPathHtml()) {
+ commands.AppendElement(fd::SetDefaultExtension(u"html"_ns));
+ }
+
+ // initial location
+ if (!aInitialDir.IsEmpty()) {
+ commands.AppendElement(fd::SetFolder(aInitialDir));
+ }
+
+ // filter types and the default index
+ if (!mFilterList.IsEmpty()) {
+ nsTArray<fd::ComDlgFilterSpec> fileTypes;
+ for (auto const& filter : mFilterList) {
+ fileTypes.EmplaceBack(filter.title, filter.filter);
+ }
+ commands.AppendElement(fd::SetFileTypes(std::move(fileTypes)));
+ commands.AppendElement(fd::SetFileTypeIndex(mSelectedType));
+ }
+
+ ScopedRtlShimWindow shim(mParentWidget.get());
+ AutoWidgetPickerState awps(mParentWidget);
+
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ auto type = mMode == modeSave ? FileDialogType::Save : FileDialogType::Open;
+
+ auto promise = mozilla::detail::AsyncExecute(
+ &ShowFilePickerLocal, &ShowFilePickerRemote, shim.get(), type, commands);
+
+ return promise->Then(
+ mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
+ [self = RefPtr(this), mode = mMode, shim = std::move(shim),
+ awps = std::move(awps)](Maybe<Results> res_opt) {
+ if (!res_opt) {
+ return Ok(false);
+ }
+ auto result = res_opt.extract();
+
+ // Remember what filter type the user selected
+ self->mSelectedType = int32_t(result.selectedFileTypeIndex());
+
+ auto const& paths = result.paths();
+
+ // single selection
+ if (mode != modeOpenMultiple) {
+ if (!paths.IsEmpty()) {
+ MOZ_ASSERT(paths.Length() == 1);
+ self->mUnicodeFile = paths[0];
+ return Ok(true);
+ }
+ return Ok(false);
+ }
+
+ // multiple selection
+ for (auto const& str : paths) {
+ nsCOMPtr<nsIFile> file;
+ if (NS_SUCCEEDED(NS_NewLocalFile(str, false, getter_AddRefs(file)))) {
+ self->mFiles.AppendObject(file);
+ }
+ }
+
+ return Ok(true);
+ },
+ [](HRESULT err) {
+ NS_WARNING("ShowFilePicker failed");
+ return NotOk(err);
+ });
+}
+
+void nsFilePicker::ClearFiles() {
+ mUnicodeFile.Truncate();
+ mFiles.Clear();
+}
+
+namespace {
+class GetFilesInDirectoryCallback final
+ : public mozilla::dom::GetFilesCallback {
+ public:
+ explicit GetFilesInDirectoryCallback(
+ RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult,
+ true>::Private>
+ aPromise)
+ : mPromise(std::move(aPromise)) {}
+ void Callback(
+ nsresult aStatus,
+ const FallibleTArray<RefPtr<mozilla::dom::BlobImpl>>& aBlobImpls) {
+ if (NS_FAILED(aStatus)) {
+ mPromise->Reject(aStatus, __func__);
+ return;
+ }
+ nsTArray<mozilla::PathString> filePaths;
+ filePaths.SetCapacity(aBlobImpls.Length());
+ for (const auto& blob : aBlobImpls) {
+ if (blob->IsFile()) {
+ mozilla::PathString pathString;
+ mozilla::ErrorResult error;
+ blob->GetMozFullPathInternal(pathString, error);
+ nsresult rv = error.StealNSResult();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mPromise->Reject(rv, __func__);
+ return;
+ }
+ filePaths.AppendElement(pathString);
+ } else {
+ NS_WARNING("Got a non-file blob, can't do content analysis on it");
+ }
+ }
+ mPromise->Resolve(std::move(filePaths), __func__);
+ }
+
+ private:
+ RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult,
+ true>::Private>
+ mPromise;
+};
+} // anonymous namespace
+
+RefPtr<nsFilePicker::ContentAnalysisResponse>
+nsFilePicker::CheckContentAnalysisService() {
+ nsresult rv;
+ nsCOMPtr<nsIContentAnalysis> contentAnalysis =
+ mozilla::components::nsIContentAnalysis::Service(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__);
+ }
+ bool contentAnalysisIsActive = false;
+ rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__);
+ }
+ if (!contentAnalysisIsActive) {
+ return nsFilePicker::ContentAnalysisResponse::CreateAndResolve(true,
+ __func__);
+ }
+
+ nsCOMPtr<nsIURI> uri = mBrowsingContext->Canonical()->GetCurrentURI();
+
+ auto processOneItem = [self = RefPtr{this},
+ contentAnalysis = std::move(contentAnalysis),
+ uri =
+ std::move(uri)](const mozilla::PathString& aItem) {
+ nsCString emptyDigestString;
+ auto* windowGlobal =
+ self->mBrowsingContext->Canonical()->GetCurrentWindowGlobal();
+ nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest(
+ new mozilla::contentanalysis::ContentAnalysisRequest(
+ nsIContentAnalysisRequest::AnalysisType::eFileAttached, aItem, true,
+ std::move(emptyDigestString), uri,
+ nsIContentAnalysisRequest::OperationType::eCustomDisplayString,
+ windowGlobal));
+
+ auto promise =
+ mozilla::MakeRefPtr<nsFilePicker::ContentAnalysisResponse::Private>(
+ __func__);
+ auto contentAnalysisCallback =
+ mozilla::MakeRefPtr<mozilla::contentanalysis::ContentAnalysisCallback>(
+ [promise](nsIContentAnalysisResponse* aResponse) {
+ promise->Resolve(aResponse->GetShouldAllowContent(), __func__);
+ },
+ [promise](nsresult aError) { promise->Reject(aError, __func__); });
+
+ nsresult rv = contentAnalysis->AnalyzeContentRequestCallback(
+ contentAnalysisRequest, /* aAutoAcknowledge */ true,
+ contentAnalysisCallback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->Reject(rv, __func__);
+ }
+ return promise;
+ };
+
+ // Since getting the files to analyze might be asynchronous, use a MozPromise
+ // to unify the logic below.
+ auto getFilesToAnalyzePromise = mozilla::MakeRefPtr<mozilla::MozPromise<
+ nsTArray<mozilla::PathString>, nsresult, true>::Private>(__func__);
+ if (mMode == modeGetFolder) {
+ nsCOMPtr<nsISupports> tmp;
+ nsresult rv = GetDomFileOrDirectory(getter_AddRefs(tmp));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ getFilesToAnalyzePromise->Reject(rv, __func__);
+ return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv,
+ __func__);
+ }
+ auto* directory = static_cast<mozilla::dom::Directory*>(tmp.get());
+ mozilla::dom::OwningFileOrDirectory owningDirectory;
+ owningDirectory.SetAsDirectory() = directory;
+ nsTArray<mozilla::dom::OwningFileOrDirectory> directoryArray{
+ std::move(owningDirectory)};
+
+ mozilla::ErrorResult error;
+ RefPtr<mozilla::dom::GetFilesHelper> helper =
+ mozilla::dom::GetFilesHelper::Create(directoryArray, true, error);
+ rv = error.StealNSResult();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ getFilesToAnalyzePromise->Reject(rv, __func__);
+ return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv,
+ __func__);
+ }
+ auto getFilesCallback = mozilla::MakeRefPtr<GetFilesInDirectoryCallback>(
+ getFilesToAnalyzePromise);
+ helper->AddCallback(getFilesCallback);
+ } else {
+ nsCOMArray<nsIFile> files;
+ if (!mUnicodeFile.IsEmpty()) {
+ nsCOMPtr<nsIFile> file;
+ rv = GetFile(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ getFilesToAnalyzePromise->Reject(rv, __func__);
+ return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv,
+ __func__);
+ }
+ files.AppendElement(file);
+ } else {
+ files.AppendElements(mFiles);
+ }
+ nsTArray<mozilla::PathString> paths(files.Length());
+ std::transform(files.begin(), files.end(), MakeBackInserter(paths),
+ [](auto* entry) { return entry->NativePath(); });
+ getFilesToAnalyzePromise->Resolve(std::move(paths), __func__);
+ }
+
+ return getFilesToAnalyzePromise->Then(
+ mozilla::GetMainThreadSerialEventTarget(), __func__,
+ [processOneItem](nsTArray<mozilla::PathString> aPaths) mutable {
+ return mozilla::detail::AsyncAll<mozilla::PathString>(std::move(aPaths),
+ processOneItem);
+ },
+ [](nsresult aError) {
+ return nsFilePicker::ContentAnalysisResponse::CreateAndReject(aError,
+ __func__);
+ });
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIFilePicker impl.
+
+nsresult nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
+ NS_ENSURE_ARG_POINTER(aCallback);
+
+ if (MaybeBlockFilePicker(aCallback)) {
+ return NS_OK;
+ }
+
+ // Don't attempt to open a real file-picker in headless mode.
+ if (gfxPlatform::IsHeadless()) {
+ return nsresult::NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsAutoString initialDir;
+ if (mDisplayDirectory) mDisplayDirectory->GetPath(initialDir);
+
+ // If no display directory, re-use the last one.
+ if (initialDir.IsEmpty()) {
+ // Allocate copy of last used dir.
+ initialDir = sLastUsedUnicodeDirectory.get();
+ }
+
+ // Clear previous file selections
+ ClearFiles();
+
+ auto promise = mMode == modeGetFolder ? ShowFolderPicker(initialDir)
+ : ShowFilePicker(initialDir);
+
+ auto p2 = promise->Then(
+ mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
+ [self = RefPtr(this),
+ callback = RefPtr(aCallback)](bool selectionMade) -> void {
+ if (!selectionMade) {
+ callback->Done(ResultCode::returnCancel);
+ return;
+ }
+
+ self->RememberLastUsedDirectory();
+
+ nsIFilePicker::ResultCode retValue = ResultCode::returnOK;
+
+ if (self->mMode == modeSave) {
+ // Windows does not return resultReplace; we must check whether the
+ // file already exists.
+ nsCOMPtr<nsIFile> file;
+ nsresult rv =
+ NS_NewLocalFile(self->mUnicodeFile, false, getter_AddRefs(file));
+
+ bool flag = false;
+ if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(file->Exists(&flag)) && flag) {
+ retValue = ResultCode::returnReplace;
+ }
+ }
+
+ if (self->mBrowsingContext && !self->mBrowsingContext->IsChrome() &&
+ self->mMode != modeSave && retValue != ResultCode::returnCancel) {
+ self->CheckContentAnalysisService()->Then(
+ mozilla::GetMainThreadSerialEventTarget(), __func__,
+ [retValue, callback, self = RefPtr{self}](bool aAllowContent) {
+ if (aAllowContent) {
+ callback->Done(retValue);
+ } else {
+ self->ClearFiles();
+ callback->Done(ResultCode::returnCancel);
+ }
+ },
+ [callback, self = RefPtr{self}](nsresult aError) {
+ self->ClearFiles();
+ callback->Done(ResultCode::returnCancel);
+ });
+ return;
+ }
+
+ callback->Done(retValue);
+ },
+ [callback = RefPtr(aCallback)](HRESULT err) {
+ using mozilla::widget::filedialog::sLogFileDialog;
+ MOZ_LOG(sLogFileDialog, LogLevel::Error,
+ ("nsFilePicker: Show failed with hr=0x%08lX", err));
+ callback->Done(ResultCode::returnCancel);
+ });
+
+ return NS_OK;
+}
+
+nsresult nsFilePicker::Show(nsIFilePicker::ResultCode* aReturnVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ *aFile = nullptr;
+
+ if (mUnicodeFile.IsEmpty()) return NS_OK;
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ file.forget(aFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetFileURL(nsIURI** aFileURL) {
+ *aFileURL = nullptr;
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = GetFile(getter_AddRefs(file));
+ if (!file) return rv;
+
+ return NS_NewFileURI(aFileURL, file);
+}
+
+NS_IMETHODIMP
+nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
+ NS_ENSURE_ARG_POINTER(aFiles);
+ return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
+}
+
+// Get the file + path
+NS_IMETHODIMP
+nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) {
+ mDefaultFilePath = aString;
+
+ // First, make sure the file name is not too long.
+ int32_t nameLength;
+ int32_t nameIndex = mDefaultFilePath.RFind(u"\\");
+ if (nameIndex == kNotFound)
+ nameIndex = 0;
+ else
+ nameIndex++;
+ nameLength = mDefaultFilePath.Length() - nameIndex;
+ mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex));
+
+ if (nameLength > MAX_PATH) {
+ int32_t extIndex = mDefaultFilePath.RFind(u".");
+ if (extIndex == kNotFound) extIndex = mDefaultFilePath.Length();
+
+ // Let's try to shave the needed characters from the name part.
+ int32_t charsToRemove = nameLength - MAX_PATH;
+ if (extIndex - nameIndex >= charsToRemove) {
+ mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove);
+ }
+ }
+
+ // Then, we need to replace illegal characters. At this stage, we cannot
+ // replace the backslash as the string might represent a file path.
+ mDefaultFilePath.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-');
+ mDefaultFilename.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-');
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseWinFilePicker::GetDefaultString(nsAString& aString) {
+ return NS_ERROR_FAILURE;
+}
+
+// The default extension to use for files
+NS_IMETHODIMP
+nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) {
+ aExtension = mDefaultExtension;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) {
+ mDefaultExtension = aExtension;
+ return NS_OK;
+}
+
+// Set the filter index
+NS_IMETHODIMP
+nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
+ // Windows' filter index is 1-based, we use a 0-based system.
+ *aFilterIndex = mSelectedType - 1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
+ // Windows' filter index is 1-based, we use a 0-based system.
+ mSelectedType = aFilterIndex + 1;
+ return NS_OK;
+}
+
+void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
+ mParentWidget = aParent;
+ mTitle.Assign(aTitle);
+}
+
+NS_IMETHODIMP
+nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
+ nsString sanitizedFilter(aFilter);
+ sanitizedFilter.ReplaceChar('%', '_');
+
+ if (sanitizedFilter == u"..apps"_ns) {
+ sanitizedFilter = u"*.exe;*.com"_ns;
+ } else {
+ sanitizedFilter.StripWhitespace();
+ if (sanitizedFilter == u"*"_ns) {
+ sanitizedFilter = u"*.*"_ns;
+ }
+ }
+ mFilterList.AppendElement(
+ Filter{.title = nsString(aTitle), .filter = std::move(sanitizedFilter)});
+ return NS_OK;
+}
+
+void nsFilePicker::RememberLastUsedDirectory() {
+ if (IsPrivacyModeEnabled()) {
+ // Don't remember the directory if private browsing was in effect
+ return;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)))) {
+ NS_WARNING("RememberLastUsedDirectory failed to init file path.");
+ return;
+ }
+
+ nsCOMPtr<nsIFile> dir;
+ nsAutoString newDir;
+ if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) ||
+ !(mDisplayDirectory = dir) ||
+ NS_FAILED(mDisplayDirectory->GetPath(newDir)) || newDir.IsEmpty()) {
+ NS_WARNING("RememberLastUsedDirectory failed to get parent directory.");
+ return;
+ }
+
+ sLastUsedUnicodeDirectory.reset(ToNewUnicode(newDir));
+}
+
+bool nsFilePicker::IsPrivacyModeEnabled() {
+ return mLoadContext && mLoadContext->UsePrivateBrowsing();
+}
+
+bool nsFilePicker::IsDefaultPathLink() {
+ NS_ConvertUTF16toUTF8 ext(mDefaultFilePath);
+ ext.Trim(" .", false, true); // watch out for trailing space and dots
+ ToLowerCase(ext);
+ return StringEndsWith(ext, ".lnk"_ns) || StringEndsWith(ext, ".pif"_ns) ||
+ StringEndsWith(ext, ".url"_ns);
+}
+
+bool nsFilePicker::IsDefaultPathHtml() {
+ int32_t extIndex = mDefaultFilePath.RFind(u".");
+ if (extIndex >= 0) {
+ nsAutoString ext;
+ mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex);
+ if (ext.LowerCaseEqualsLiteral(".htm") ||
+ ext.LowerCaseEqualsLiteral(".html") ||
+ ext.LowerCaseEqualsLiteral(".shtml"))
+ return true;
+ }
+ return false;
+}
diff --git a/widget/windows/nsFilePicker.h b/widget/windows/nsFilePicker.h
new file mode 100644
index 0000000000..1938b8bcb6
--- /dev/null
+++ b/widget/windows/nsFilePicker.h
@@ -0,0 +1,140 @@
+/* -*- 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/. */
+
+#ifndef nsFilePicker_h__
+#define nsFilePicker_h__
+
+#include <windows.h>
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/GetFilesHelper.h"
+#include "nsIContentAnalysis.h"
+#include "nsIFile.h"
+#include "nsISimpleEnumerator.h"
+#include "nsCOMArray.h"
+#include "nsBaseFilePicker.h"
+#include "nsString.h"
+#include "nsdefs.h"
+#include <commdlg.h>
+#include <shobjidl.h>
+#undef LogSeverity // SetupAPI.h #defines this as DWORD
+
+class nsILoadContext;
+
+namespace mozilla::widget::filedialog {
+class Command;
+class Results;
+enum class FileDialogType : uint8_t;
+} // namespace mozilla::widget::filedialog
+
+class nsBaseWinFilePicker : public nsBaseFilePicker {
+ public:
+ NS_IMETHOD GetDefaultString(nsAString& aDefaultString) override;
+ NS_IMETHOD SetDefaultString(const nsAString& aDefaultString) override;
+ NS_IMETHOD GetDefaultExtension(nsAString& aDefaultExtension) override;
+ NS_IMETHOD SetDefaultExtension(const nsAString& aDefaultExtension) override;
+
+ protected:
+ nsString mDefaultFilePath;
+ nsString mDefaultFilename;
+ nsString mDefaultExtension;
+};
+
+/**
+ * Native Windows FileSelector wrapper
+ */
+
+class nsFilePicker final : public nsBaseWinFilePicker {
+ virtual ~nsFilePicker() = default;
+
+ template <typename T>
+ using Maybe = mozilla::Maybe<T>;
+ template <typename T>
+ using Result = mozilla::Result<T, HRESULT>;
+ template <typename Res>
+ using FPPromise = RefPtr<mozilla::MozPromise<Maybe<Res>, HRESULT, true>>;
+
+ using Command = mozilla::widget::filedialog::Command;
+ using Results = mozilla::widget::filedialog::Results;
+ using FileDialogType = mozilla::widget::filedialog::FileDialogType;
+
+ public:
+ nsFilePicker();
+
+ NS_IMETHOD Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle,
+ nsIFilePicker::Mode aMode,
+ mozilla::dom::BrowsingContext* aBrowsingContext) override;
+
+ NS_DECL_ISUPPORTS
+
+ // nsIFilePicker (less what's in nsBaseFilePicker and nsBaseWinFilePicker)
+ NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override;
+ NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override;
+ NS_IMETHOD GetFile(nsIFile** aFile) override;
+ NS_IMETHOD GetFileURL(nsIURI** aFileURL) override;
+ NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override;
+ NS_IMETHOD AppendFilter(const nsAString& aTitle,
+ const nsAString& aFilter) override;
+
+ protected:
+ /* method from nsBaseFilePicker */
+ virtual void InitNative(nsIWidget* aParent, const nsAString& aTitle) override;
+ nsresult Show(nsIFilePicker::ResultCode* aReturnVal) override;
+ void GetFilterListArray(nsString& aFilterList);
+
+ NS_IMETHOD Open(nsIFilePickerShownCallback* aCallback) override;
+
+ private:
+ RefPtr<mozilla::MozPromise<bool, HRESULT, true>> ShowFolderPicker(
+ const nsString& aInitialDir);
+ RefPtr<mozilla::MozPromise<bool, HRESULT, true>> ShowFilePicker(
+ const nsString& aInitialDir);
+
+ // Show the dialog out-of-process.
+ static FPPromise<Results> ShowFilePickerRemote(
+ HWND aParent, FileDialogType type, nsTArray<Command> const& commands);
+ static FPPromise<nsString> ShowFolderPickerRemote(
+ HWND aParent, nsTArray<Command> const& commands);
+
+ // Show the dialog in-process.
+ static FPPromise<Results> ShowFilePickerLocal(
+ HWND aParent, FileDialogType type, nsTArray<Command> const& commands);
+ static FPPromise<nsString> ShowFolderPickerLocal(
+ HWND aParent, nsTArray<Command> const& commands);
+
+ void ClearFiles();
+ using ContentAnalysisResponse = mozilla::MozPromise<bool, nsresult, true>;
+ RefPtr<ContentAnalysisResponse> CheckContentAnalysisService();
+
+ protected:
+ void RememberLastUsedDirectory();
+ bool IsPrivacyModeEnabled();
+ bool IsDefaultPathLink();
+ bool IsDefaultPathHtml();
+
+ nsCOMPtr<nsILoadContext> mLoadContext;
+ nsCOMPtr<nsIWidget> mParentWidget;
+ nsString mTitle;
+ nsCString mFile;
+ int32_t mSelectedType;
+ nsCOMArray<nsIFile> mFiles;
+ nsString mUnicodeFile;
+
+ struct FreeDeleter {
+ void operator()(void* aPtr) { ::free(aPtr); }
+ };
+ static mozilla::UniquePtr<char16_t[], FreeDeleter> sLastUsedUnicodeDirectory;
+
+ struct Filter {
+ nsString title;
+ nsString filter;
+ };
+ AutoTArray<Filter, 1> mFilterList;
+};
+
+#endif // nsFilePicker_h__
diff --git a/widget/windows/nsLookAndFeel.cpp b/widget/windows/nsLookAndFeel.cpp
new file mode 100644
index 0000000000..01b126cd42
--- /dev/null
+++ b/widget/windows/nsLookAndFeel.cpp
@@ -0,0 +1,916 @@
+/* -*- 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 "nsLookAndFeel.h"
+#include <stdint.h>
+#include <windows.h>
+#include <shellapi.h>
+#include "nsStyleConsts.h"
+#include "nsUXThemeData.h"
+#include "nsUXThemeConstants.h"
+#include "nsWindowsHelpers.h"
+#include "WinUtils.h"
+#include "WindowsUIUtils.h"
+#include "mozilla/FontPropertyTypes.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/widget/WinRegistry.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+static Maybe<nscolor> GetColorFromTheme(nsUXThemeClass cls, int32_t aPart,
+ int32_t aState, int32_t aPropId) {
+ COLORREF color;
+ HRESULT hr = GetThemeColor(nsUXThemeData::GetTheme(cls), aPart, aState,
+ aPropId, &color);
+ if (hr == S_OK) {
+ return Some(COLOREF_2_NSRGB(color));
+ }
+ return Nothing();
+}
+
+static int32_t GetSystemParam(long flag, int32_t def) {
+ DWORD value;
+ return ::SystemParametersInfo(flag, 0, &value, 0) ? value : def;
+}
+
+static bool SystemWantsDarkTheme() {
+ if (nsUXThemeData::IsHighContrastOn()) {
+ return LookAndFeel::IsDarkColor(
+ LookAndFeel::Color(StyleSystemColor::Window, ColorScheme::Light,
+ LookAndFeel::UseStandins::No));
+ }
+
+ WinRegistry::Key key(
+ HKEY_CURRENT_USER,
+ u"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"_ns,
+ WinRegistry::KeyMode::QueryValue);
+ if (NS_WARN_IF(!key)) {
+ return false;
+ }
+ uint32_t light = key.GetValueAsDword(u"AppsUseLightTheme"_ns).valueOr(1);
+ return !light;
+}
+
+uint32_t nsLookAndFeel::SystemColorFilter() {
+ if (NS_WARN_IF(!mColorFilterWatcher)) {
+ return 0;
+ }
+
+ const auto& key = mColorFilterWatcher->GetKey();
+ if (!key.GetValueAsDword(u"Active"_ns).valueOr(0)) {
+ return 0;
+ }
+ return key.GetValueAsDword(u"FilterType"_ns).valueOr(0);
+}
+
+nsLookAndFeel::nsLookAndFeel() {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::TOUCH_ENABLED_DEVICE,
+ WinUtils::IsTouchDeviceSupportPresent());
+}
+
+nsLookAndFeel::~nsLookAndFeel() = default;
+
+void nsLookAndFeel::NativeInit() { EnsureInit(); }
+
+/* virtual */
+void nsLookAndFeel::RefreshImpl() {
+ mInitialized = false; // Fetch system colors next time they're used.
+ nsXPLookAndFeel::RefreshImpl();
+}
+
+static bool UseNonNativeMenuColors(ColorScheme aScheme) {
+ return !LookAndFeel::GetInt(LookAndFeel::IntID::UseAccessibilityTheme) ||
+ aScheme == ColorScheme::Dark;
+}
+
+nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
+ nscolor& aColor) {
+ EnsureInit();
+
+ auto IsHighlightColor = [&] {
+ switch (aID) {
+ case ColorID::MozMenuhover:
+ return !UseNonNativeMenuColors(aScheme);
+ case ColorID::Highlight:
+ case ColorID::Selecteditem:
+ // We prefer the generic dark selection color if we don't have an
+ // explicit one.
+ return aScheme != ColorScheme::Dark || mDarkHighlight;
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ return true;
+ default:
+ return false;
+ }
+ };
+
+ auto IsHighlightTextColor = [&] {
+ switch (aID) {
+ case ColorID::MozMenubarhovertext:
+ if (UseNonNativeMenuColors(aScheme)) {
+ return false;
+ }
+ [[fallthrough]];
+ case ColorID::MozMenuhovertext:
+ if (UseNonNativeMenuColors(aScheme)) {
+ return false;
+ }
+ return !mColorMenuHoverText;
+ case ColorID::Highlighttext:
+ case ColorID::Selecteditemtext:
+ // We prefer the generic dark selection color if we don't have an
+ // explicit one.
+ return aScheme != ColorScheme::Dark || mDarkHighlightText;
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ return true;
+ default:
+ return false;
+ }
+ };
+
+ if (IsHighlightColor()) {
+ if (aScheme == ColorScheme::Dark && mDarkHighlight) {
+ aColor = *mDarkHighlight;
+ } else {
+ aColor = GetColorForSysColorIndex(COLOR_HIGHLIGHT);
+ }
+ return NS_OK;
+ }
+
+ if (IsHighlightTextColor()) {
+ if (aScheme == ColorScheme::Dark && mDarkHighlightText) {
+ aColor = *mDarkHighlightText;
+ } else {
+ aColor = GetColorForSysColorIndex(COLOR_HIGHLIGHTTEXT);
+ }
+ return NS_OK;
+ }
+
+ // Titlebar colors are color-scheme aware.
+ switch (aID) {
+ case ColorID::Activecaption:
+ aColor = mTitlebarColors.Get(aScheme, true).mBg;
+ return NS_OK;
+ case ColorID::Captiontext:
+ aColor = mTitlebarColors.Get(aScheme, true).mFg;
+ return NS_OK;
+ case ColorID::Activeborder:
+ aColor = mTitlebarColors.Get(aScheme, true).mBorder;
+ return NS_OK;
+ case ColorID::Inactivecaption:
+ aColor = mTitlebarColors.Get(aScheme, false).mBg;
+ return NS_OK;
+ case ColorID::Inactivecaptiontext:
+ aColor = mTitlebarColors.Get(aScheme, false).mFg;
+ return NS_OK;
+ case ColorID::Inactiveborder:
+ aColor = mTitlebarColors.Get(aScheme, false).mBorder;
+ return NS_OK;
+ default:
+ break;
+ }
+
+ if (aScheme == ColorScheme::Dark) {
+ if (auto color = GenericDarkColor(aID)) {
+ aColor = *color;
+ return NS_OK;
+ }
+ }
+
+ static constexpr auto kNonNativeMenuText = NS_RGB(0x15, 0x14, 0x1a);
+ nsresult res = NS_OK;
+ int idx;
+ switch (aID) {
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ aColor = NS_TRANSPARENT;
+ return NS_OK;
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ return NS_OK;
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ return NS_OK;
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ aColor = NS_TRANSPARENT;
+ return NS_OK;
+
+ // New CSS 2 Color definitions
+ case ColorID::Appworkspace:
+ idx = COLOR_APPWORKSPACE;
+ break;
+ case ColorID::Background:
+ idx = COLOR_BACKGROUND;
+ break;
+ case ColorID::Buttonface:
+ case ColorID::MozButtonhoverface:
+ case ColorID::MozButtonactiveface:
+ case ColorID::MozButtondisabledface:
+ case ColorID::MozColheader:
+ case ColorID::MozColheaderhover:
+ case ColorID::MozColheaderactive:
+ idx = COLOR_BTNFACE;
+ break;
+ case ColorID::Buttonhighlight:
+ idx = COLOR_BTNHIGHLIGHT;
+ break;
+ case ColorID::Buttonshadow:
+ idx = COLOR_BTNSHADOW;
+ break;
+ case ColorID::Buttontext:
+ case ColorID::MozButtonhovertext:
+ case ColorID::MozButtonactivetext:
+ idx = COLOR_BTNTEXT;
+ break;
+ case ColorID::MozCellhighlighttext:
+ aColor = NS_RGB(0, 0, 0);
+ return NS_OK;
+ case ColorID::MozCellhighlight:
+ aColor = NS_RGB(206, 206, 206);
+ return NS_OK;
+ case ColorID::Graytext:
+ idx = COLOR_GRAYTEXT;
+ break;
+ case ColorID::MozMenubarhovertext:
+ if (UseNonNativeMenuColors(aScheme)) {
+ aColor = kNonNativeMenuText;
+ return NS_OK;
+ }
+ [[fallthrough]];
+ case ColorID::MozMenuhovertext:
+ if (UseNonNativeMenuColors(aScheme)) {
+ aColor = kNonNativeMenuText;
+ return NS_OK;
+ }
+ if (mColorMenuHoverText) {
+ aColor = *mColorMenuHoverText;
+ return NS_OK;
+ }
+ idx = COLOR_HIGHLIGHTTEXT;
+ break;
+ case ColorID::MozMenuhover:
+ MOZ_ASSERT(UseNonNativeMenuColors(aScheme));
+ aColor = NS_RGB(0xe0, 0xe0, 0xe6);
+ return NS_OK;
+ case ColorID::MozMenuhoverdisabled:
+ if (UseNonNativeMenuColors(aScheme)) {
+ aColor = NS_RGB(0xf0, 0xf0, 0xf3);
+ return NS_OK;
+ }
+ aColor = NS_TRANSPARENT;
+ return NS_OK;
+ case ColorID::Infobackground:
+ idx = COLOR_INFOBK;
+ break;
+ case ColorID::Infotext:
+ idx = COLOR_INFOTEXT;
+ break;
+ case ColorID::Menu:
+ if (UseNonNativeMenuColors(aScheme)) {
+ aColor = NS_RGB(0xf9, 0xf9, 0xfb);
+ return NS_OK;
+ }
+ idx = COLOR_MENU;
+ break;
+ case ColorID::Menutext:
+ if (UseNonNativeMenuColors(aScheme)) {
+ aColor = kNonNativeMenuText;
+ return NS_OK;
+ }
+ idx = COLOR_MENUTEXT;
+ break;
+ case ColorID::Scrollbar:
+ idx = COLOR_SCROLLBAR;
+ break;
+ case ColorID::Threeddarkshadow:
+ idx = COLOR_3DDKSHADOW;
+ break;
+ case ColorID::Threedface:
+ idx = COLOR_3DFACE;
+ break;
+ case ColorID::Threedhighlight:
+ idx = COLOR_3DHIGHLIGHT;
+ break;
+ case ColorID::Threedlightshadow:
+ case ColorID::Buttonborder:
+ case ColorID::MozDisabledfield:
+ case ColorID::MozSidebarborder:
+ idx = COLOR_3DLIGHT;
+ break;
+ case ColorID::Threedshadow:
+ idx = COLOR_3DSHADOW;
+ break;
+ case ColorID::Window:
+ idx = COLOR_WINDOW;
+ break;
+ case ColorID::Windowframe:
+ idx = COLOR_WINDOWFRAME;
+ break;
+ case ColorID::Windowtext:
+ idx = COLOR_WINDOWTEXT;
+ break;
+ case ColorID::MozEventreerow:
+ case ColorID::MozOddtreerow:
+ case ColorID::Field:
+ case ColorID::MozSidebar:
+ case ColorID::MozCombobox:
+ idx = COLOR_WINDOW;
+ break;
+ case ColorID::Fieldtext:
+ case ColorID::MozSidebartext:
+ case ColorID::MozComboboxtext:
+ idx = COLOR_WINDOWTEXT;
+ break;
+ case ColorID::MozHeaderbar:
+ case ColorID::MozHeaderbarinactive:
+ case ColorID::MozDialog:
+ idx = COLOR_3DFACE;
+ break;
+ case ColorID::Accentcolor:
+ aColor = mColorAccent;
+ return NS_OK;
+ case ColorID::Accentcolortext:
+ aColor = mColorAccentText;
+ return NS_OK;
+ case ColorID::MozHeaderbartext:
+ case ColorID::MozHeaderbarinactivetext:
+ case ColorID::MozDialogtext:
+ case ColorID::MozColheadertext:
+ case ColorID::MozColheaderhovertext:
+ case ColorID::MozColheaderactivetext:
+ idx = COLOR_WINDOWTEXT;
+ break;
+ case ColorID::MozNativehyperlinktext:
+ idx = COLOR_HOTLIGHT;
+ break;
+ case ColorID::Marktext:
+ case ColorID::Mark:
+ case ColorID::SpellCheckerUnderline:
+ aColor = GetStandinForNativeColor(aID, aScheme);
+ return NS_OK;
+ default:
+ idx = COLOR_WINDOW;
+ res = NS_ERROR_FAILURE;
+ break;
+ }
+
+ aColor = GetColorForSysColorIndex(idx);
+
+ return res;
+}
+
+nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ EnsureInit();
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ break;
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 3;
+ break;
+ case IntID::CaretBlinkTime:
+ aResult = static_cast<int32_t>(::GetCaretBlinkTime());
+ break;
+ case IntID::CaretBlinkCount: {
+ int32_t timeout = GetSystemParam(SPI_GETCARETTIMEOUT, 5000);
+ auto blinkTime = ::GetCaretBlinkTime();
+ if (timeout <= 0 || blinkTime <= 0) {
+ aResult = -1;
+ break;
+ }
+ // 2 * blinkTime because this integer is a full blink cycle.
+ aResult = std::ceil(float(timeout) / (2.0f * float(blinkTime)));
+ break;
+ }
+
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+ case IntID::SelectTextfieldsOnKeyFocus:
+ // Select textfield content when focused by kbd
+ // used by EventStateManager::sTextfieldSelectModel
+ aResult = 1;
+ break;
+ case IntID::SubmenuDelay:
+ // This will default to the Windows' default
+ // (400ms) on error.
+ aResult = GetSystemParam(SPI_GETMENUSHOWDELAY, 400);
+ break;
+ case IntID::TooltipDelay:
+ aResult = 500;
+ break;
+ case IntID::MenusCanOverlapOSBar:
+ // we want XUL popups to be able to overlap the task bar.
+ aResult = 1;
+ break;
+ case IntID::DragThresholdX:
+ // The system metric is the number of pixels at which a drag should
+ // start. Our look and feel metric is the number of pixels you can
+ // move before starting a drag, so subtract 1.
+ aResult = ::GetSystemMetrics(SM_CXDRAG) - 1;
+ break;
+ case IntID::DragThresholdY:
+ aResult = ::GetSystemMetrics(SM_CYDRAG) - 1;
+ break;
+ case IntID::UseAccessibilityTheme:
+ // High contrast is a misnomer under Win32 -- any theme can be used with
+ // it, e.g. normal contrast with large fonts, low contrast, etc. The high
+ // contrast flag really means -- use this theme and don't override it.
+ aResult = nsUXThemeData::IsHighContrastOn();
+ break;
+ case IntID::ScrollArrowStyle:
+ aResult = eScrollArrowStyle_Single;
+ break;
+ case IntID::TreeOpenDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeCloseDelay:
+ aResult = 0;
+ break;
+ case IntID::TreeLazyScrollDelay:
+ aResult = 150;
+ break;
+ case IntID::TreeScrollDelay:
+ aResult = 100;
+ break;
+ case IntID::TreeScrollLinesMax:
+ aResult = 3;
+ break;
+ case IntID::WindowsAccentColorInTitlebar: {
+ aResult = mTitlebarColors.mUseAccent;
+ } break;
+ case IntID::AlertNotificationOrigin:
+ aResult = 0;
+ {
+ // Get task bar window handle
+ HWND shellWindow = FindWindowW(L"Shell_TrayWnd", nullptr);
+
+ if (shellWindow != nullptr) {
+ // Determine position
+ APPBARDATA appBarData;
+ appBarData.hWnd = shellWindow;
+ appBarData.cbSize = sizeof(appBarData);
+ if (SHAppBarMessage(ABM_GETTASKBARPOS, &appBarData)) {
+ // Set alert origin as a bit field - see LookAndFeel.h
+ // 0 represents bottom right, sliding vertically.
+ switch (appBarData.uEdge) {
+ case ABE_LEFT:
+ aResult = NS_ALERT_HORIZONTAL | NS_ALERT_LEFT;
+ break;
+ case ABE_RIGHT:
+ aResult = NS_ALERT_HORIZONTAL;
+ break;
+ case ABE_TOP:
+ aResult = NS_ALERT_TOP;
+ [[fallthrough]];
+ case ABE_BOTTOM:
+ // If the task bar is right-to-left,
+ // move the origin to the left
+ if (::GetWindowLong(shellWindow, GWL_EXSTYLE) & WS_EX_LAYOUTRTL)
+ aResult |= NS_ALERT_LEFT;
+ break;
+ }
+ }
+ }
+ }
+ break;
+ case IntID::IMERawInputUnderlineStyle:
+ case IntID::IMEConvertedTextUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dashed);
+ break;
+ case IntID::IMESelectedRawTextUnderlineStyle:
+ case IntID::IMESelectedConvertedTextUnderline:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::None);
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Wavy);
+ break;
+ case IntID::ScrollbarButtonAutoRepeatBehavior:
+ aResult = 0;
+ break;
+ case IntID::SwipeAnimationEnabled:
+ // Forcibly enable the swipe animation on Windows. It doesn't matter on
+ // platforms where "Drag two fingers to scroll" isn't supported since on
+ // the platforms we will never generate any swipe gesture events.
+ aResult = 1;
+ break;
+ case IntID::UseOverlayScrollbars:
+ aResult = WindowsUIUtils::ComputeOverlayScrollbars();
+ break;
+ case IntID::AllowOverlayScrollbarsOverlap:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarDisplayOnMouseMove:
+ aResult = 1;
+ break;
+ case IntID::ScrollbarFadeBeginDelay:
+ aResult = 2500;
+ break;
+ case IntID::ScrollbarFadeDuration:
+ aResult = 350;
+ break;
+ case IntID::ContextMenuOffsetVertical:
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 2;
+ break;
+ case IntID::SystemUsesDarkTheme:
+ aResult = SystemWantsDarkTheme();
+ break;
+ case IntID::SystemScrollbarSize:
+ aResult = std::max(WinUtils::GetSystemMetricsForDpi(SM_CXVSCROLL, 96),
+ WinUtils::GetSystemMetricsForDpi(SM_CXHSCROLL, 96));
+ break;
+ case IntID::PrefersReducedMotion: {
+ BOOL enable = TRUE;
+ ::SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &enable, 0);
+ aResult = !enable;
+ break;
+ }
+ case IntID::PrefersReducedTransparency: {
+ // Prefers reduced transparency if the option for "Transparency Effects"
+ // is disabled
+ aResult = !WindowsUIUtils::ComputeTransparencyEffects();
+ break;
+ }
+ case IntID::InvertedColors: {
+ // Color filter values
+ // 1: Inverted
+ // 2: Grayscale inverted
+ aResult = mCurrentColorFilter == 1 || mCurrentColorFilter == 2;
+ break;
+ }
+ case IntID::PrimaryPointerCapabilities: {
+ aResult = static_cast<int32_t>(
+ widget::WinUtils::GetPrimaryPointerCapabilities());
+ break;
+ }
+ case IntID::AllPointerCapabilities: {
+ aResult =
+ static_cast<int32_t>(widget::WinUtils::GetAllPointerCapabilities());
+ break;
+ }
+ case IntID::TouchDeviceSupportPresent:
+ aResult = !!WinUtils::IsTouchDeviceSupportPresent();
+ break;
+ case IntID::PanelAnimations:
+ aResult = 1;
+ break;
+ case IntID::HideCursorWhileTyping: {
+ BOOL enable = TRUE;
+ ::SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &enable, 0);
+ aResult = enable;
+ break;
+ }
+ default:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ }
+ return res;
+}
+
+nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::TextScaleFactor:
+ aResult = WindowsUIUtils::ComputeTextScaleFactor();
+ break;
+ default:
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ }
+ return res;
+}
+
+LookAndFeelFont nsLookAndFeel::GetLookAndFeelFontInternal(
+ const LOGFONTW& aLogFont, bool aUseShellDlg) {
+ LookAndFeelFont result{};
+
+ result.haveFont() = false;
+
+ // Get scaling factor from physical to logical pixels
+ double pixelScale =
+ 1.0 / WinUtils::SystemScaleFactor() / LookAndFeel::GetTextScaleFactor();
+
+ // The lfHeight is in pixels, and it needs to be adjusted for the
+ // device it will be displayed on.
+ // Screens and Printers will differ in DPI
+ //
+ // So this accounts for the difference in the DeviceContexts
+ // The pixelScale will typically be 1.0 for the screen
+ // (though larger for hi-dpi screens where the Windows resolution
+ // scale factor is 125% or 150% or even more), and could be
+ // any value when going to a printer, for example pixelScale is
+ // 6.25 when going to a 600dpi printer.
+ float pixelHeight = -aLogFont.lfHeight;
+ if (pixelHeight < 0) {
+ nsAutoFont hFont(::CreateFontIndirectW(&aLogFont));
+ if (!hFont) {
+ return result;
+ }
+
+ nsAutoHDC dc(::GetDC(nullptr));
+ HGDIOBJ hObject = ::SelectObject(dc, hFont);
+ TEXTMETRIC tm;
+ ::GetTextMetrics(dc, &tm);
+ ::SelectObject(dc, hObject);
+
+ pixelHeight = tm.tmAscent;
+ }
+
+ pixelHeight *= pixelScale;
+
+ // we have problem on Simplified Chinese system because the system
+ // report the default font size is 8 points. but if we use 8, the text
+ // display very ugly. force it to be at 9 points (12 pixels) on that
+ // system (cp936), but leave other sizes alone.
+ if (pixelHeight < 12 && ::GetACP() == 936) {
+ pixelHeight = 12;
+ }
+
+ result.haveFont() = true;
+
+ if (aUseShellDlg) {
+ result.name() = u"MS Shell Dlg 2"_ns;
+ } else {
+ result.name() = aLogFont.lfFaceName;
+ }
+
+ result.size() = pixelHeight;
+ result.italic() = !!aLogFont.lfItalic;
+ // FIXME: Other weights?
+ result.weight() =
+ ((aLogFont.lfWeight == FW_BOLD) ? FontWeight::BOLD : FontWeight::NORMAL)
+ .ToFloat();
+
+ return result;
+}
+
+LookAndFeelFont nsLookAndFeel::GetLookAndFeelFont(LookAndFeel::FontID anID) {
+ LookAndFeelFont result{};
+
+ result.haveFont() = false;
+
+ // FontID::Icon is handled differently than the others
+ if (anID == LookAndFeel::FontID::Icon) {
+ LOGFONTW logFont;
+ if (::SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(logFont),
+ (PVOID)&logFont, 0)) {
+ result = GetLookAndFeelFontInternal(logFont, false);
+ }
+ return result;
+ }
+
+ NONCLIENTMETRICSW ncm;
+ ncm.cbSize = sizeof(NONCLIENTMETRICSW);
+ if (!::SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm),
+ (PVOID)&ncm, 0)) {
+ return result;
+ }
+
+ switch (anID) {
+ case LookAndFeel::FontID::Menu:
+ case LookAndFeel::FontID::MozPullDownMenu:
+ result = GetLookAndFeelFontInternal(ncm.lfMenuFont, false);
+ break;
+ case LookAndFeel::FontID::Caption:
+ result = GetLookAndFeelFontInternal(ncm.lfCaptionFont, false);
+ break;
+ case LookAndFeel::FontID::SmallCaption:
+ result = GetLookAndFeelFontInternal(ncm.lfSmCaptionFont, false);
+ break;
+ case LookAndFeel::FontID::StatusBar:
+ result = GetLookAndFeelFontInternal(ncm.lfStatusFont, false);
+ break;
+ case LookAndFeel::FontID::MozButton:
+ case LookAndFeel::FontID::MozField:
+ case LookAndFeel::FontID::MozList:
+ // XXX It's not clear to me whether this is exactly the right
+ // set of LookAndFeel values to map to the dialog font; we may
+ // want to add or remove cases here after reviewing the visual
+ // results under various Windows versions.
+ result = GetLookAndFeelFontInternal(ncm.lfMessageFont, true);
+ break;
+ default:
+ result = GetLookAndFeelFontInternal(ncm.lfMessageFont, false);
+ break;
+ }
+
+ return result;
+}
+
+bool nsLookAndFeel::NativeGetFont(LookAndFeel::FontID anID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ LookAndFeelFont font = GetLookAndFeelFont(anID);
+ return LookAndFeelFontToStyle(font, aFontName, aFontStyle);
+}
+
+/* virtual */
+char16_t nsLookAndFeel::GetPasswordCharacterImpl() {
+#define UNICODE_BLACK_CIRCLE_CHAR 0x25cf
+ return UNICODE_BLACK_CIRCLE_CHAR;
+}
+
+static nscolor GetAccentColorText(const nscolor aAccentColor) {
+ // We want the color that we return for text that will be drawn over
+ // a background that has the accent color to have good contrast with
+ // the accent color. Windows itself uses either white or black text
+ // depending on how light or dark the accent color is. We do the same
+ // here based on the luminance of the accent color with a threshhold
+ // value. This algorithm should match what Windows does. It comes from:
+ //
+ // https://docs.microsoft.com/en-us/windows/uwp/style/color
+ float luminance = (NS_GET_R(aAccentColor) * 2 + NS_GET_G(aAccentColor) * 5 +
+ NS_GET_B(aAccentColor)) /
+ 8;
+ return luminance <= 128 ? NS_RGB(255, 255, 255) : NS_RGB(0, 0, 0);
+}
+
+static Maybe<nscolor> GetAccentColorText(const Maybe<nscolor>& aAccentColor) {
+ if (!aAccentColor) {
+ return Nothing();
+ }
+ return Some(GetAccentColorText(*aAccentColor));
+}
+
+nscolor nsLookAndFeel::GetColorForSysColorIndex(int index) {
+ MOZ_ASSERT(index >= SYS_COLOR_MIN && index <= SYS_COLOR_MAX);
+ return mSysColorTable[index - SYS_COLOR_MIN];
+}
+
+auto nsLookAndFeel::ComputeTitlebarColors() -> TitlebarColors {
+ TitlebarColors result;
+
+ // Start with the native / non-accent-in-titlebar colors.
+ result.mActiveLight = {GetColorForSysColorIndex(COLOR_ACTIVECAPTION),
+ GetColorForSysColorIndex(COLOR_CAPTIONTEXT),
+ GetColorForSysColorIndex(COLOR_ACTIVEBORDER)};
+
+ result.mInactiveLight = {GetColorForSysColorIndex(COLOR_INACTIVECAPTION),
+ GetColorForSysColorIndex(COLOR_INACTIVECAPTIONTEXT),
+ GetColorForSysColorIndex(COLOR_INACTIVEBORDER)};
+
+ if (!nsUXThemeData::IsHighContrastOn()) {
+ // Use our non-native colors.
+ result.mActiveLight = {
+ GetStandinForNativeColor(ColorID::Activecaption, ColorScheme::Light),
+ GetStandinForNativeColor(ColorID::Captiontext, ColorScheme::Light),
+ GetStandinForNativeColor(ColorID::Activeborder, ColorScheme::Light)};
+ result.mInactiveLight = {
+ GetStandinForNativeColor(ColorID::Inactivecaption, ColorScheme::Light),
+ GetStandinForNativeColor(ColorID::Inactivecaptiontext,
+ ColorScheme::Light),
+ GetStandinForNativeColor(ColorID::Inactiveborder, ColorScheme::Light)};
+ }
+
+ // Our dark colors are always non-native.
+ result.mActiveDark = {*GenericDarkColor(ColorID::Activecaption),
+ *GenericDarkColor(ColorID::Captiontext),
+ *GenericDarkColor(ColorID::Activeborder)};
+ result.mInactiveDark = {*GenericDarkColor(ColorID::Inactivecaption),
+ *GenericDarkColor(ColorID::Inactivecaptiontext),
+ *GenericDarkColor(ColorID::Inactiveborder)};
+
+ // TODO(bug 1825241): Somehow get notified when this changes? Hopefully the
+ // sys color notification is enough.
+ WinRegistry::Key dwmKey(HKEY_CURRENT_USER,
+ u"SOFTWARE\\Microsoft\\Windows\\DWM"_ns,
+ WinRegistry::KeyMode::QueryValue);
+ if (NS_WARN_IF(!dwmKey)) {
+ return result;
+ }
+
+ // The order of the color components in the DWORD stored in the registry
+ // happens to be the same order as we store the components in nscolor
+ // so we can just assign directly here.
+ result.mAccent = dwmKey.GetValueAsDword(u"AccentColor"_ns);
+ result.mAccentText = GetAccentColorText(result.mAccent);
+
+ if (!result.mAccent) {
+ return result;
+ }
+
+ result.mAccentInactive = dwmKey.GetValueAsDword(u"AccentColorInactive"_ns);
+ result.mAccentInactiveText = GetAccentColorText(result.mAccentInactive);
+
+ // The ColorPrevalence value is set to 1 when the "Show color on title bar"
+ // setting in the Color section of Window's Personalization settings is
+ // turned on.
+ result.mUseAccent =
+ dwmKey.GetValueAsDword(u"ColorPrevalence"_ns).valueOr(0) == 1;
+ if (!result.mUseAccent) {
+ return result;
+ }
+
+ // TODO(emilio): Consider reading ColorizationColorBalance to compute a
+ // more correct border color, see [1]. Though for opaque accent colors this
+ // isn't needed.
+ //
+ // [1]:
+ // https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:ui/color/win/accent_color_observer.cc;l=42;drc=9d4eb7ed25296abba8fd525a6bdd0fdbf4bcdd9f
+ result.mActiveDark.mBorder = result.mActiveLight.mBorder = *result.mAccent;
+ result.mInactiveDark.mBorder = result.mInactiveLight.mBorder =
+ result.mAccentInactive.valueOr(NS_RGB(57, 57, 57));
+ result.mActiveLight.mBg = result.mActiveDark.mBg = *result.mAccent;
+ result.mActiveLight.mFg = result.mActiveDark.mFg = *result.mAccentText;
+ if (result.mAccentInactive) {
+ result.mInactiveLight.mBg = result.mInactiveDark.mBg =
+ *result.mAccentInactive;
+ result.mInactiveLight.mFg = result.mInactiveDark.mFg =
+ *result.mAccentInactiveText;
+ } else {
+ // This is hand-picked to .8 to change the accent color a bit but not too
+ // much.
+ constexpr uint8_t kBgAlpha = 208;
+ const auto BlendWithAlpha = [](nscolor aBg, nscolor aFg,
+ uint8_t aAlpha) -> nscolor {
+ return NS_ComposeColors(
+ aBg, NS_RGBA(NS_GET_R(aFg), NS_GET_G(aFg), NS_GET_B(aFg), aAlpha));
+ };
+ result.mInactiveLight.mBg =
+ BlendWithAlpha(NS_RGB(255, 255, 255), *result.mAccent, kBgAlpha);
+ result.mInactiveDark.mBg =
+ BlendWithAlpha(NS_RGB(0, 0, 0), *result.mAccent, kBgAlpha);
+ result.mInactiveLight.mFg = result.mInactiveDark.mFg = *result.mAccentText;
+ }
+ return result;
+}
+
+void nsLookAndFeel::EnsureInit() {
+ if (mInitialized) {
+ return;
+ }
+ mInitialized = true;
+
+ mColorMenuHoverText =
+ ::GetColorFromTheme(eUXMenu, MENU_POPUPITEM, MPI_HOT, TMT_TEXTCOLOR);
+
+ // Fill out the sys color table.
+ for (int i = SYS_COLOR_MIN; i <= SYS_COLOR_MAX; ++i) {
+ mSysColorTable[i - SYS_COLOR_MIN] = [&] {
+ if (auto c = WindowsUIUtils::GetSystemColor(ColorScheme::Light, i)) {
+ return *c;
+ }
+ DWORD color = ::GetSysColor(i);
+ return COLOREF_2_NSRGB(color);
+ }();
+ }
+
+ mDarkHighlight =
+ WindowsUIUtils::GetSystemColor(ColorScheme::Dark, COLOR_HIGHLIGHT);
+ mDarkHighlightText =
+ WindowsUIUtils::GetSystemColor(ColorScheme::Dark, COLOR_HIGHLIGHTTEXT);
+
+ mTitlebarColors = ComputeTitlebarColors();
+
+ mColorAccent = [&] {
+ if (auto accent = WindowsUIUtils::GetAccentColor()) {
+ return *accent;
+ }
+ // Try the titlebar accent as a fallback.
+ if (mTitlebarColors.mAccent) {
+ return *mTitlebarColors.mAccent;
+ }
+ // Seems to be the default color (hardcoded because of bug 1065998)
+ return NS_RGB(0, 120, 215);
+ }();
+ mColorAccentText = GetAccentColorText(mColorAccent);
+
+ if (!mColorFilterWatcher) {
+ WinRegistry::Key key(
+ HKEY_CURRENT_USER, u"Software\\Microsoft\\ColorFiltering"_ns,
+ WinRegistry::KeyMode::QueryValue | WinRegistry::KeyMode::Notify);
+ if (key) {
+ mColorFilterWatcher = MakeUnique<WinRegistry::KeyWatcher>(
+ std::move(key), GetCurrentSerialEventTarget(), [this] {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ if (mCurrentColorFilter != SystemColorFilter()) {
+ LookAndFeel::NotifyChangedAllWindows(
+ widget::ThemeChangeKind::MediaQueriesOnly);
+ }
+ });
+ }
+ }
+ mCurrentColorFilter = SystemColorFilter();
+
+ RecordTelemetry();
+}
diff --git a/widget/windows/nsLookAndFeel.h b/widget/windows/nsLookAndFeel.h
new file mode 100644
index 0000000000..d19aa91329
--- /dev/null
+++ b/widget/windows/nsLookAndFeel.h
@@ -0,0 +1,124 @@
+/* -*- 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/. */
+
+#ifndef __nsLookAndFeel
+#define __nsLookAndFeel
+
+#include <windows.h>
+
+#include "nsXPLookAndFeel.h"
+#include "gfxFont.h"
+
+/*
+ * Gesture System Metrics
+ */
+#ifndef SM_DIGITIZER
+# define SM_DIGITIZER 94
+# define TABLET_CONFIG_NONE 0x00000000
+# define NID_INTEGRATED_TOUCH 0x00000001
+# define NID_EXTERNAL_TOUCH 0x00000002
+# define NID_INTEGRATED_PEN 0x00000004
+# define NID_EXTERNAL_PEN 0x00000008
+# define NID_MULTI_INPUT 0x00000040
+# define NID_READY 0x00000080
+#endif
+
+/*
+ * Tablet mode detection
+ */
+#ifndef SM_SYSTEMDOCKED
+# define SM_CONVERTIBLESLATEMODE 0x00002003
+# define SM_SYSTEMDOCKED 0x00002004
+#endif
+
+/*
+ * Color constant inclusive bounds for GetSysColor
+ */
+#define SYS_COLOR_MIN 0
+#define SYS_COLOR_MAX 30
+#define SYS_COLOR_COUNT (SYS_COLOR_MAX - SYS_COLOR_MIN + 1)
+
+namespace mozilla::widget::WinRegistry {
+class KeyWatcher;
+}
+
+class nsLookAndFeel final : public nsXPLookAndFeel {
+ public:
+ nsLookAndFeel();
+ virtual ~nsLookAndFeel();
+
+ void NativeInit() final;
+ void RefreshImpl() override;
+ nsresult NativeGetInt(IntID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID, float& aResult) override;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override;
+ bool NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) override;
+ char16_t GetPasswordCharacterImpl() override;
+
+ private:
+ struct TitlebarColors {
+ // NOTE: These are the DWM accent colors, which might not match the
+ // UISettings/UWP accent color in some cases, see bug 1796730.
+ mozilla::Maybe<nscolor> mAccent;
+ mozilla::Maybe<nscolor> mAccentText;
+ mozilla::Maybe<nscolor> mAccentInactive;
+ mozilla::Maybe<nscolor> mAccentInactiveText;
+
+ bool mUseAccent = false;
+
+ struct Set {
+ nscolor mBg = 0;
+ nscolor mFg = 0;
+ nscolor mBorder = 0;
+ };
+
+ Set mActiveLight;
+ Set mActiveDark;
+
+ Set mInactiveLight;
+ Set mInactiveDark;
+
+ const Set& Get(mozilla::ColorScheme aScheme, bool aActive) const {
+ if (aScheme == mozilla::ColorScheme::Dark) {
+ return aActive ? mActiveDark : mInactiveDark;
+ }
+ return aActive ? mActiveLight : mInactiveLight;
+ }
+ };
+
+ TitlebarColors ComputeTitlebarColors();
+
+ nscolor GetColorForSysColorIndex(int index);
+
+ LookAndFeelFont GetLookAndFeelFontInternal(const LOGFONTW& aLogFont,
+ bool aUseShellDlg);
+
+ uint32_t SystemColorFilter();
+
+ LookAndFeelFont GetLookAndFeelFont(LookAndFeel::FontID anID);
+
+ // Cached colors and flags indicating success in their retrieval.
+ mozilla::Maybe<nscolor> mColorMenuHoverText;
+
+ mozilla::Maybe<nscolor> mDarkHighlight;
+ mozilla::Maybe<nscolor> mDarkHighlightText;
+
+ TitlebarColors mTitlebarColors;
+
+ nscolor mColorAccent = 0;
+ nscolor mColorAccentText = 0;
+
+ nscolor mSysColorTable[SYS_COLOR_COUNT];
+
+ mozilla::UniquePtr<mozilla::widget::WinRegistry::KeyWatcher>
+ mColorFilterWatcher;
+ uint32_t mCurrentColorFilter = 0;
+
+ bool mInitialized = false;
+ void EnsureInit();
+};
+
+#endif
diff --git a/widget/windows/nsNativeDragSource.cpp b/widget/windows/nsNativeDragSource.cpp
new file mode 100644
index 0000000000..ca5459e9df
--- /dev/null
+++ b/widget/windows/nsNativeDragSource.cpp
@@ -0,0 +1,98 @@
+/* -*- 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 "nsNativeDragSource.h"
+#include <stdio.h>
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+#include "nsToolkit.h"
+#include "nsWidgetsCID.h"
+#include "nsIDragService.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/dom/DataTransfer.h"
+
+/*
+ * class nsNativeDragSource
+ */
+nsNativeDragSource::nsNativeDragSource(
+ mozilla::dom::DataTransfer* aDataTransfer)
+ : m_cRef(0), m_hCursor(nullptr), mUserCancelled(false) {
+ mDataTransfer = aDataTransfer;
+}
+
+nsNativeDragSource::~nsNativeDragSource() {}
+
+STDMETHODIMP
+nsNativeDragSource::QueryInterface(REFIID riid, void** ppv) {
+ *ppv = nullptr;
+
+ if (IID_IUnknown == riid || IID_IDropSource == riid) *ppv = this;
+
+ if (nullptr != *ppv) {
+ ((LPUNKNOWN)*ppv)->AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP_(ULONG)
+nsNativeDragSource::AddRef(void) {
+ ++m_cRef;
+ NS_LOG_ADDREF(this, m_cRef, "nsNativeDragSource", sizeof(*this));
+ return m_cRef;
+}
+
+STDMETHODIMP_(ULONG)
+nsNativeDragSource::Release(void) {
+ --m_cRef;
+ NS_LOG_RELEASE(this, m_cRef, "nsNativeDragSource");
+ if (0 != m_cRef) return m_cRef;
+
+ delete this;
+ return 0;
+}
+
+STDMETHODIMP
+nsNativeDragSource::QueryContinueDrag(BOOL fEsc, DWORD grfKeyState) {
+ nsCOMPtr<nsIDragService> dragService =
+ do_GetService("@mozilla.org/widget/dragservice;1");
+ if (dragService) {
+ DWORD pos = ::GetMessagePos();
+ dragService->DragMoved(GET_X_LPARAM(pos), GET_Y_LPARAM(pos));
+ }
+
+ if (fEsc) {
+ mUserCancelled = true;
+ return DRAGDROP_S_CANCEL;
+ }
+
+ if (!(grfKeyState & MK_LBUTTON) || (grfKeyState & MK_RBUTTON))
+ return DRAGDROP_S_DROP;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+nsNativeDragSource::GiveFeedback(DWORD dwEffect) {
+ // For drags involving tabs, we do some custom work with cursors.
+ if (mDataTransfer) {
+ nsAutoString cursor;
+ mDataTransfer->GetMozCursor(cursor);
+ if (cursor.EqualsLiteral("default")) {
+ m_hCursor = ::LoadCursor(0, IDC_ARROW);
+ } else {
+ m_hCursor = nullptr;
+ }
+ }
+
+ if (m_hCursor) {
+ ::SetCursor(m_hCursor);
+ return S_OK;
+ }
+
+ // Let the system choose which cursor to apply.
+ return DRAGDROP_S_USEDEFAULTCURSORS;
+}
diff --git a/widget/windows/nsNativeDragSource.h b/widget/windows/nsNativeDragSource.h
new file mode 100644
index 0000000000..a8e37b90a8
--- /dev/null
+++ b/widget/windows/nsNativeDragSource.h
@@ -0,0 +1,68 @@
+/* -*- 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/. */
+#ifndef _nsNativeDragSource_h_
+#define _nsNativeDragSource_h_
+
+#include "nscore.h"
+#include <ole2.h>
+#include <oleidl.h>
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+namespace dom {
+class DataTransfer;
+} // namespace dom
+} // namespace mozilla
+
+// class nsIDragSource;
+
+/*
+ * nsNativeDragSource implements the IDropSource interface and gets
+ * most of its behavior from the associated adapter (m_dragDrop).
+ */
+class nsNativeDragSource final : public IDropSource {
+ public:
+ // construct an nsNativeDragSource referencing adapter
+ // nsNativeDragSource(nsIDragSource * adapter);
+ explicit nsNativeDragSource(mozilla::dom::DataTransfer* aDataTransfer);
+ ~nsNativeDragSource();
+
+ // IUnknown methods - see iunknown.h for documentation
+
+ STDMETHODIMP QueryInterface(REFIID, void**);
+ STDMETHODIMP_(ULONG) AddRef();
+ STDMETHODIMP_(ULONG) Release();
+
+ // IDropSource methods - see idropsrc.h for documentation
+
+ // Return DRAGDROP_S_USEDEFAULTCURSORS if this object lets OLE provide
+ // default cursors, otherwise return NOERROR. This method gets called in
+ // response to changes that the target makes to dEffect (DragEnter,
+ // DragOver).
+ STDMETHODIMP GiveFeedback(DWORD dEffect);
+
+ // This method gets called if there is any change in the mouse or key
+ // state. Return DRAGDROP_S_CANCEL to stop the drag, DRAGDROP_S_DROP
+ // to execute the drop, otherwise NOERROR.
+ STDMETHODIMP QueryContinueDrag(BOOL fESC, DWORD grfKeyState);
+
+ bool UserCancelled() { return mUserCancelled; }
+
+ protected:
+ // Reference count
+ ULONG m_cRef;
+
+ // Data object, hold information about cursor state
+ RefPtr<mozilla::dom::DataTransfer> mDataTransfer;
+
+ // Custom drag cursor
+ HCURSOR m_hCursor;
+
+ // true if the user cancelled the drag by pressing escape
+ bool mUserCancelled;
+};
+
+#endif // _nsNativeDragSource_h_
diff --git a/widget/windows/nsNativeDragTarget.cpp b/widget/windows/nsNativeDragTarget.cpp
new file mode 100644
index 0000000000..b615a3e4bc
--- /dev/null
+++ b/widget/windows/nsNativeDragTarget.cpp
@@ -0,0 +1,472 @@
+/* -*- 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 <stdio.h>
+#include "nsIDragService.h"
+#include "nsWidgetsCID.h"
+#include "nsNativeDragTarget.h"
+#include "nsDragService.h"
+#include "nsINode.h"
+#include "nsCOMPtr.h"
+
+#include "nsIWidget.h"
+#include "nsWindow.h"
+#include "nsClipboard.h"
+#include "KeyboardLayout.h"
+
+#include "mozilla/MouseEvents.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+// This is cached for Leave notification
+static POINTL gDragLastPoint;
+
+bool nsNativeDragTarget::gDragImageChanged = false;
+
+/*
+ * class nsNativeDragTarget
+ */
+nsNativeDragTarget::nsNativeDragTarget(nsIWidget* aWidget)
+ : m_cRef(0),
+ mEffectsAllowed(DROPEFFECT_MOVE | DROPEFFECT_COPY | DROPEFFECT_LINK),
+ mEffectsPreferred(DROPEFFECT_NONE),
+ mTookOwnRef(false),
+ mWidget(aWidget),
+ mDropTargetHelper(nullptr) {
+ mHWnd = (HWND)mWidget->GetNativeData(NS_NATIVE_WINDOW);
+
+ mDragService = do_GetService("@mozilla.org/widget/dragservice;1");
+}
+
+nsNativeDragTarget::~nsNativeDragTarget() {
+ if (mDropTargetHelper) {
+ mDropTargetHelper->Release();
+ mDropTargetHelper = nullptr;
+ }
+}
+
+// IUnknown methods - see iunknown.h for documentation
+STDMETHODIMP
+nsNativeDragTarget::QueryInterface(REFIID riid, void** ppv) {
+ *ppv = nullptr;
+
+ if (IID_IUnknown == riid || IID_IDropTarget == riid) *ppv = this;
+
+ if (nullptr != *ppv) {
+ ((LPUNKNOWN)*ppv)->AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP_(ULONG)
+nsNativeDragTarget::AddRef(void) {
+ ++m_cRef;
+ NS_LOG_ADDREF(this, m_cRef, "nsNativeDragTarget", sizeof(*this));
+ return m_cRef;
+}
+
+STDMETHODIMP_(ULONG) nsNativeDragTarget::Release(void) {
+ --m_cRef;
+ NS_LOG_RELEASE(this, m_cRef, "nsNativeDragTarget");
+ if (0 != m_cRef) return m_cRef;
+
+ delete this;
+ return 0;
+}
+
+void nsNativeDragTarget::GetGeckoDragAction(DWORD grfKeyState,
+ LPDWORD pdwEffect,
+ uint32_t* aGeckoAction) {
+ // If a window is disabled or a modal window is on top of it
+ // (which implies it is disabled), then we should not allow dropping.
+ if (!mWidget->IsEnabled()) {
+ *pdwEffect = DROPEFFECT_NONE;
+ *aGeckoAction = nsIDragService::DRAGDROP_ACTION_NONE;
+ return;
+ }
+
+ // If the user explicitly uses a modifier key, they want the associated action
+ // Shift + Control -> LINK, Shift -> MOVE, Ctrl -> COPY
+ DWORD desiredEffect = DROPEFFECT_NONE;
+ if ((grfKeyState & MK_CONTROL) && (grfKeyState & MK_SHIFT)) {
+ desiredEffect = DROPEFFECT_LINK;
+ } else if (grfKeyState & MK_SHIFT) {
+ desiredEffect = DROPEFFECT_MOVE;
+ } else if (grfKeyState & MK_CONTROL) {
+ desiredEffect = DROPEFFECT_COPY;
+ }
+
+ // Determine the desired effect from what is allowed and preferred.
+ if (!(desiredEffect &= mEffectsAllowed)) {
+ // No modifier key effect is set which is also allowed, check
+ // the preference of the data.
+ desiredEffect = mEffectsPreferred & mEffectsAllowed;
+ if (!desiredEffect) {
+ // No preference is set, so just fall back to the allowed effect itself
+ desiredEffect = mEffectsAllowed;
+ }
+ }
+
+ // Otherwise we should specify the first available effect
+ // from MOVE, COPY, or LINK.
+ if (desiredEffect & DROPEFFECT_MOVE) {
+ *pdwEffect = DROPEFFECT_MOVE;
+ *aGeckoAction = nsIDragService::DRAGDROP_ACTION_MOVE;
+ } else if (desiredEffect & DROPEFFECT_COPY) {
+ *pdwEffect = DROPEFFECT_COPY;
+ *aGeckoAction = nsIDragService::DRAGDROP_ACTION_COPY;
+ } else if (desiredEffect & DROPEFFECT_LINK) {
+ *pdwEffect = DROPEFFECT_LINK;
+ *aGeckoAction = nsIDragService::DRAGDROP_ACTION_LINK;
+ } else {
+ *pdwEffect = DROPEFFECT_NONE;
+ *aGeckoAction = nsIDragService::DRAGDROP_ACTION_NONE;
+ }
+}
+
+inline bool IsKeyDown(char key) { return GetKeyState(key) < 0; }
+
+void nsNativeDragTarget::DispatchDragDropEvent(EventMessage aEventMessage,
+ const POINTL& aPT) {
+ WidgetDragEvent event(true, aEventMessage, mWidget);
+
+ nsWindow* win = static_cast<nsWindow*>(mWidget);
+ win->InitEvent(event);
+ POINT cpos;
+
+ cpos.x = aPT.x;
+ cpos.y = aPT.y;
+
+ if (mHWnd != nullptr) {
+ ::ScreenToClient(mHWnd, &cpos);
+ event.mRefPoint = LayoutDeviceIntPoint(cpos.x, cpos.y);
+ } else {
+ event.mRefPoint = LayoutDeviceIntPoint(0, 0);
+ }
+
+ ModifierKeyState modifierKeyState;
+ modifierKeyState.InitInputEvent(event);
+
+ event.mInputSource =
+ static_cast<nsBaseDragService*>(mDragService.get())->GetInputSource();
+
+ mWidget->DispatchInputEvent(&event);
+}
+
+void nsNativeDragTarget::ProcessDrag(EventMessage aEventMessage,
+ DWORD grfKeyState, POINTL ptl,
+ DWORD* pdwEffect) {
+ // Before dispatching the event make sure we have the correct drop action set
+ uint32_t geckoAction;
+ GetGeckoDragAction(grfKeyState, pdwEffect, &geckoAction);
+
+ // Set the current action into the Gecko specific type
+ nsCOMPtr<nsIDragSession> currSession;
+ mDragService->GetCurrentSession(getter_AddRefs(currSession));
+ if (!currSession) {
+ return;
+ }
+
+ currSession->SetDragAction(geckoAction);
+
+ // Dispatch the event into Gecko
+ DispatchDragDropEvent(aEventMessage, ptl);
+
+ // If TakeChildProcessDragAction returns something other than
+ // DRAGDROP_ACTION_UNINITIALIZED, it means that the last event was sent
+ // to the child process and this event is also being sent to the child
+ // process. In this case, use the last event's action instead.
+ nsDragService* dragService = static_cast<nsDragService*>(mDragService.get());
+ currSession->GetDragAction(&geckoAction);
+
+ int32_t childDragAction = dragService->TakeChildProcessDragAction();
+ if (childDragAction != nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) {
+ geckoAction = childDragAction;
+ }
+
+ if (nsIDragService::DRAGDROP_ACTION_LINK & geckoAction) {
+ *pdwEffect = DROPEFFECT_LINK;
+ } else if (nsIDragService::DRAGDROP_ACTION_COPY & geckoAction) {
+ *pdwEffect = DROPEFFECT_COPY;
+ } else if (nsIDragService::DRAGDROP_ACTION_MOVE & geckoAction) {
+ *pdwEffect = DROPEFFECT_MOVE;
+ } else {
+ *pdwEffect = DROPEFFECT_NONE;
+ }
+
+ if (aEventMessage != eDrop) {
+ // Get the cached drag effect from the drag service, the data member should
+ // have been set by whoever handled the WidgetGUIEvent or nsIDOMEvent on
+ // drags.
+ bool canDrop;
+ currSession->GetCanDrop(&canDrop);
+ if (!canDrop) {
+ *pdwEffect = DROPEFFECT_NONE;
+ }
+ }
+
+ // Clear the cached value
+ currSession->SetCanDrop(false);
+}
+
+// IDropTarget methods
+STDMETHODIMP
+nsNativeDragTarget::DragEnter(LPDATAOBJECT pIDataSource, DWORD grfKeyState,
+ POINTL ptl, DWORD* pdwEffect) {
+ if (!mDragService) {
+ return E_FAIL;
+ }
+
+ mEffectsAllowed = *pdwEffect;
+ AddLinkSupportIfCanBeGenerated(pIDataSource);
+
+ // Drag and drop image helper
+ if (GetDropTargetHelper()) {
+ // We get a lot of crashes (often uncaught by our handler) later on during
+ // DragOver calls, see bug 1465513. It looks like this might be because
+ // we're not cleaning up previous drags fully and now released resources get
+ // used. Calling IDropTargetHelper::DragLeave before DragEnter seems to fix
+ // this for at least one reproduction of this crash.
+ GetDropTargetHelper()->DragLeave();
+ POINT pt = {ptl.x, ptl.y};
+ GetDropTargetHelper()->DragEnter(mHWnd, pIDataSource, &pt, *pdwEffect);
+ }
+
+ // save a ref to this, in case the window is destroyed underneath us
+ NS_ASSERTION(!mTookOwnRef, "own ref already taken!");
+ this->AddRef();
+ mTookOwnRef = true;
+
+ // tell the drag service about this drag (it may have come from an
+ // outside app).
+ mDragService->StartDragSession();
+
+ void* tempOutData = nullptr;
+ uint32_t tempDataLen = 0;
+ nsresult loadResult = nsClipboard::GetNativeDataOffClipboard(
+ pIDataSource, 0, ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT),
+ nullptr, &tempOutData, &tempDataLen);
+ if (NS_SUCCEEDED(loadResult) && tempOutData) {
+ mEffectsPreferred = *((DWORD*)tempOutData);
+ free(tempOutData);
+ } else {
+ // We have no preference if we can't obtain it
+ mEffectsPreferred = DROPEFFECT_NONE;
+ }
+
+ // Set the native data object into drag service
+ //
+ // This cast is ok because in the constructor we created a
+ // the actual implementation we wanted, so we know this is
+ // a nsDragService. It should be a private interface, though.
+ nsDragService* winDragService =
+ static_cast<nsDragService*>(mDragService.get());
+ winDragService->SetIDataObject(pIDataSource);
+
+ // Now process the native drag state and then dispatch the event
+ ProcessDrag(eDragEnter, grfKeyState, ptl, pdwEffect);
+
+ return S_OK;
+}
+
+void nsNativeDragTarget::AddLinkSupportIfCanBeGenerated(
+ LPDATAOBJECT aIDataSource) {
+ // If we don't have a link effect, but we can generate one, fix the
+ // drop effect to include it.
+ if (!(mEffectsAllowed & DROPEFFECT_LINK) && aIDataSource) {
+ if (S_OK == ::OleQueryLinkFromData(aIDataSource)) {
+ mEffectsAllowed |= DROPEFFECT_LINK;
+ }
+ }
+}
+
+STDMETHODIMP
+nsNativeDragTarget::DragOver(DWORD grfKeyState, POINTL ptl, LPDWORD pdwEffect) {
+ if (!mDragService) {
+ return E_FAIL;
+ }
+
+ bool dragImageChanged = gDragImageChanged;
+ gDragImageChanged = false;
+
+ // If a LINK effect could be generated previously from a DragEnter(),
+ // then we should include it as an allowed effect.
+ mEffectsAllowed = (*pdwEffect) | (mEffectsAllowed & DROPEFFECT_LINK);
+
+ nsCOMPtr<nsIDragSession> currentDragSession;
+ mDragService->GetCurrentSession(getter_AddRefs(currentDragSession));
+ if (!currentDragSession) {
+ return S_OK; // Drag was canceled.
+ }
+
+ // without the AddRef() |this| can get destroyed in an event handler
+ this->AddRef();
+
+ // Drag and drop image helper
+ if (GetDropTargetHelper()) {
+ if (dragImageChanged) {
+ // See comment in nsNativeDragTarget::DragEnter.
+ GetDropTargetHelper()->DragLeave();
+ // The drop helper only updates the image during DragEnter, so emulate
+ // a DragEnter if the image was changed.
+ POINT pt = {ptl.x, ptl.y};
+ nsDragService* dragService =
+ static_cast<nsDragService*>(mDragService.get());
+ GetDropTargetHelper()->DragEnter(mHWnd, dragService->GetDataObject(), &pt,
+ *pdwEffect);
+ }
+ POINT pt = {ptl.x, ptl.y};
+ GetDropTargetHelper()->DragOver(&pt, *pdwEffect);
+ }
+
+ ModifierKeyState modifierKeyState;
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->FireDragEventAtSource(eDrag, modifierKeyState.GetModifiers());
+ // Now process the native drag state and then dispatch the event
+ ProcessDrag(eDragOver, grfKeyState, ptl, pdwEffect);
+
+ this->Release();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+nsNativeDragTarget::DragLeave() {
+ if (!mDragService) {
+ return E_FAIL;
+ }
+
+ // Drag and drop image helper
+ if (GetDropTargetHelper()) {
+ GetDropTargetHelper()->DragLeave();
+ }
+
+ // dispatch the event into Gecko
+ DispatchDragDropEvent(eDragExit, gDragLastPoint);
+
+ nsCOMPtr<nsIDragSession> currentDragSession;
+ mDragService->GetCurrentSession(getter_AddRefs(currentDragSession));
+
+ if (currentDragSession) {
+ nsCOMPtr<nsINode> sourceNode;
+ currentDragSession->GetSourceNode(getter_AddRefs(sourceNode));
+
+ if (!sourceNode) {
+ // We're leaving a window while doing a drag that was
+ // initiated in a different app. End the drag session, since
+ // we're done with it for now (until the user drags back into
+ // mozilla).
+ ModifierKeyState modifierKeyState;
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->EndDragSession(false, modifierKeyState.GetModifiers());
+ }
+ }
+
+ // release the ref that was taken in DragEnter
+ NS_ASSERTION(mTookOwnRef, "want to release own ref, but not taken!");
+ if (mTookOwnRef) {
+ this->Release();
+ mTookOwnRef = false;
+ }
+
+ return S_OK;
+}
+
+void nsNativeDragTarget::DragCancel() {
+ // Cancel the drag session if we did DragEnter.
+ if (mTookOwnRef) {
+ if (GetDropTargetHelper()) {
+ GetDropTargetHelper()->DragLeave();
+ }
+ if (mDragService) {
+ ModifierKeyState modifierKeyState;
+ nsCOMPtr<nsIDragService> dragService = mDragService;
+ dragService->EndDragSession(false, modifierKeyState.GetModifiers());
+ }
+ this->Release(); // matching the AddRef in DragEnter
+ mTookOwnRef = false;
+ }
+}
+
+STDMETHODIMP
+nsNativeDragTarget::Drop(LPDATAOBJECT pData, DWORD grfKeyState, POINTL aPT,
+ LPDWORD pdwEffect) {
+ if (!mDragService) {
+ return E_FAIL;
+ }
+
+ mEffectsAllowed = *pdwEffect;
+ AddLinkSupportIfCanBeGenerated(pData);
+
+ // Drag and drop image helper
+ if (GetDropTargetHelper()) {
+ POINT pt = {aPT.x, aPT.y};
+ GetDropTargetHelper()->Drop(pData, &pt, *pdwEffect);
+ }
+
+ // Set the native data object into the drag service
+ //
+ // This cast is ok because in the constructor we created a
+ // the actual implementation we wanted, so we know this is
+ // a nsDragService (but it should still be a private interface)
+ nsDragService* winDragService =
+ static_cast<nsDragService*>(mDragService.get());
+ winDragService->SetIDataObject(pData);
+
+ // NOTE: ProcessDrag spins the event loop which may destroy arbitrary objects.
+ // We use strong refs to prevent it from destroying these:
+ RefPtr<nsNativeDragTarget> kungFuDeathGrip = this;
+ nsCOMPtr<nsIDragService> serv = mDragService;
+
+ // Now process the native drag state and then dispatch the event
+ ProcessDrag(eDrop, grfKeyState, aPT, pdwEffect);
+
+ nsCOMPtr<nsIDragSession> currentDragSession;
+ serv->GetCurrentSession(getter_AddRefs(currentDragSession));
+ if (!currentDragSession) {
+ return S_OK; // DragCancel() was called.
+ }
+
+ // Let the win drag service know whether this session experienced
+ // a drop event within the application. Drop will not oocur if the
+ // drop landed outside the app. (used in tab tear off, bug 455884)
+ winDragService->SetDroppedLocal();
+
+ // tell the drag service we're done with the session
+ // Use GetMessagePos to get the position of the mouse at the last message
+ // seen by the event loop. (Bug 489729)
+ DWORD pos = ::GetMessagePos();
+ POINT cpos;
+ cpos.x = GET_X_LPARAM(pos);
+ cpos.y = GET_Y_LPARAM(pos);
+ winDragService->SetDragEndPoint(nsIntPoint(cpos.x, cpos.y));
+ ModifierKeyState modifierKeyState;
+ serv->EndDragSession(true, modifierKeyState.GetModifiers());
+
+ // release the ref that was taken in DragEnter
+ NS_ASSERTION(mTookOwnRef, "want to release own ref, but not taken!");
+ if (mTookOwnRef) {
+ this->Release();
+ mTookOwnRef = false;
+ }
+
+ return S_OK;
+}
+
+/**
+ * By lazy loading mDropTargetHelper we save 50-70ms of startup time
+ * which is ~5% of startup time.
+ */
+IDropTargetHelper* nsNativeDragTarget::GetDropTargetHelper() {
+ if (!mDropTargetHelper) {
+ CoCreateInstance(CLSID_DragDropHelper, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IDropTargetHelper, (LPVOID*)&mDropTargetHelper);
+ }
+
+ return mDropTargetHelper;
+}
diff --git a/widget/windows/nsNativeDragTarget.h b/widget/windows/nsNativeDragTarget.h
new file mode 100644
index 0000000000..82edc7aae7
--- /dev/null
+++ b/widget/windows/nsNativeDragTarget.h
@@ -0,0 +1,103 @@
+/* -*- 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/. */
+#ifndef _nsNativeDragTarget_h_
+#define _nsNativeDragTarget_h_
+
+#include "nsCOMPtr.h"
+#include <ole2.h>
+#include <shlobj.h>
+
+#ifndef IDropTargetHelper
+# include <shobjidl.h> // Vista drag image interfaces
+# undef LogSeverity // SetupAPI.h #defines this as DWORD
+#endif
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+
+class nsIDragService;
+class nsIWidget;
+
+/*
+ * nsNativeDragTarget implements the IDropTarget interface and gets most of its
+ * behavior from the associated adapter (m_dragDrop).
+ */
+
+class nsNativeDragTarget final : public IDropTarget {
+ public:
+ explicit nsNativeDragTarget(nsIWidget* aWidget);
+ ~nsNativeDragTarget();
+
+ // IUnknown members - see iunknown.h for documentation
+ STDMETHODIMP QueryInterface(REFIID, void**);
+ STDMETHODIMP_(ULONG) AddRef();
+ STDMETHODIMP_(ULONG) Release();
+
+ // IDataTarget members
+
+ // Set pEffect based on whether this object can support a drop based on
+ // the data available from pSource, the key and mouse states specified
+ // in grfKeyState, and the coordinates specified by point. This is
+ // called by OLE when a drag enters this object's window (as registered
+ // by Initialize).
+ STDMETHODIMP DragEnter(LPDATAOBJECT pSource, DWORD grfKeyState, POINTL point,
+ DWORD* pEffect);
+
+ // Similar to DragEnter except it is called frequently while the drag
+ // is over this object's window.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP DragOver(DWORD grfKeyState,
+ POINTL point,
+ DWORD* pEffect);
+
+ // Release the drag-drop source and put internal state back to the point
+ // before the call to DragEnter. This is called when the drag leaves
+ // without a drop occurring.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP DragLeave();
+
+ // If point is within our region of interest and pSource's data supports
+ // one of our formats, get the data and set pEffect according to
+ // grfKeyState (DROPEFFECT_MOVE if the control key was not pressed,
+ // DROPEFFECT_COPY if the control key was pressed). Otherwise return
+ // E_FAIL.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP Drop(LPDATAOBJECT pSource,
+ DWORD grfKeyState, POINTL point,
+ DWORD* pEffect);
+ /**
+ * Cancel the current drag session, if any.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void DragCancel();
+
+ static void DragImageChanged() { gDragImageChanged = true; }
+
+ protected:
+ void GetGeckoDragAction(DWORD grfKeyState, LPDWORD pdwEffect,
+ uint32_t* aGeckoAction);
+ void ProcessDrag(mozilla::EventMessage aEventMessage, DWORD grfKeyState,
+ POINTL pt, DWORD* pdwEffect);
+ void DispatchDragDropEvent(mozilla::EventMessage aEventMessage,
+ const POINTL& aPT);
+ void AddLinkSupportIfCanBeGenerated(LPDATAOBJECT aIDataSource);
+
+ // Native Stuff
+ ULONG m_cRef; // reference count
+ HWND mHWnd;
+ DWORD mEffectsAllowed;
+ DWORD mEffectsPreferred;
+ bool mTookOwnRef;
+
+ // Gecko Stuff
+ nsIWidget* mWidget;
+ nsCOMPtr<nsIDragService> mDragService;
+ // Drag target helper
+ IDropTargetHelper* GetDropTargetHelper();
+
+ private:
+ // Drag target helper
+ IDropTargetHelper* mDropTargetHelper;
+
+ static bool gDragImageChanged;
+};
+
+#endif // _nsNativeDragTarget_h_
diff --git a/widget/windows/nsNativeThemeWin.cpp b/widget/windows/nsNativeThemeWin.cpp
new file mode 100644
index 0000000000..04883a833f
--- /dev/null
+++ b/widget/windows/nsNativeThemeWin.cpp
@@ -0,0 +1,1975 @@
+/* -*- Mode: C++; tab-width: 40; 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 "nsNativeThemeWin.h"
+
+#include <algorithm>
+#include <malloc.h>
+
+#include "gfxContext.h"
+#include "gfxPlatform.h"
+#include "gfxWindowsNativeDrawing.h"
+#include "gfxWindowsPlatform.h"
+#include "gfxWindowsSurface.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/gfx/Types.h" // for Color::FromABGR
+#include "mozilla/Logging.h"
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/dom/XULButtonElement.h"
+#include "nsColor.h"
+#include "nsComboboxControlFrame.h"
+#include "nsDeviceContext.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIContentInlines.h"
+#include "nsIFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsLookAndFeel.h"
+#include "nsNameSpaceManager.h"
+#include "Theme.h"
+#include "nsPresContext.h"
+#include "nsRect.h"
+#include "nsSize.h"
+#include "nsStyleConsts.h"
+#include "nsTransform2D.h"
+#include "nsWindow.h"
+#include "prinrval.h"
+#include "WinUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+
+using ElementState = dom::ElementState;
+
+extern mozilla::LazyLogModule gWindowsLog;
+
+namespace mozilla::widget {
+
+nsNativeThemeWin::nsNativeThemeWin()
+ : Theme(ScrollbarStyle()),
+ mProgressDeterminateTimeStamp(TimeStamp::Now()),
+ mProgressIndeterminateTimeStamp(TimeStamp::Now()),
+ mBorderCacheValid(),
+ mMinimumWidgetSizeCacheValid(),
+ mGutterSizeCacheValid(false) {}
+
+nsNativeThemeWin::~nsNativeThemeWin() { nsUXThemeData::Invalidate(); }
+
+bool nsNativeThemeWin::IsWidgetAlwaysNonNative(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ return Theme::IsWidgetAlwaysNonNative(aFrame, aAppearance) ||
+ aAppearance == StyleAppearance::Checkbox ||
+ aAppearance == StyleAppearance::Radio ||
+ aAppearance == StyleAppearance::MozMenulistArrowButton ||
+ aAppearance == StyleAppearance::SpinnerUpbutton ||
+ aAppearance == StyleAppearance::SpinnerDownbutton;
+}
+
+auto nsNativeThemeWin::IsWidgetNonNative(nsIFrame* aFrame,
+ StyleAppearance aAppearance)
+ -> NonNative {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return NonNative::Always;
+ }
+
+ // We only know how to draw light widgets, so we defer to the non-native
+ // theme when appropriate.
+ if (Theme::ThemeSupportsWidget(aFrame->PresContext(), aFrame, aAppearance) &&
+ LookAndFeel::ColorSchemeForFrame(aFrame) ==
+ LookAndFeel::ColorScheme::Dark) {
+ return NonNative::BecauseColorMismatch;
+ }
+ return NonNative::No;
+}
+
+static MARGINS GetCheckboxMargins(HANDLE theme, HDC hdc) {
+ MARGINS checkboxContent = {0};
+ GetThemeMargins(theme, hdc, MENU_POPUPCHECK, MCB_NORMAL, TMT_CONTENTMARGINS,
+ nullptr, &checkboxContent);
+ return checkboxContent;
+}
+
+static SIZE GetCheckboxBGSize(HANDLE theme, HDC hdc) {
+ SIZE checkboxSize;
+ GetThemePartSize(theme, hdc, MENU_POPUPCHECK, MC_CHECKMARKNORMAL, nullptr,
+ TS_TRUE, &checkboxSize);
+
+ MARGINS checkboxMargins = GetCheckboxMargins(theme, hdc);
+
+ int leftMargin = checkboxMargins.cxLeftWidth;
+ int rightMargin = checkboxMargins.cxRightWidth;
+ int topMargin = checkboxMargins.cyTopHeight;
+ int bottomMargin = checkboxMargins.cyBottomHeight;
+
+ int width = leftMargin + checkboxSize.cx + rightMargin;
+ int height = topMargin + checkboxSize.cy + bottomMargin;
+ SIZE ret;
+ ret.cx = width;
+ ret.cy = height;
+ return ret;
+}
+
+static SIZE GetCheckboxBGBounds(HANDLE theme, HDC hdc) {
+ MARGINS checkboxBGSizing = {0};
+ MARGINS checkboxBGContent = {0};
+ GetThemeMargins(theme, hdc, MENU_POPUPCHECKBACKGROUND, MCB_NORMAL,
+ TMT_SIZINGMARGINS, nullptr, &checkboxBGSizing);
+ GetThemeMargins(theme, hdc, MENU_POPUPCHECKBACKGROUND, MCB_NORMAL,
+ TMT_CONTENTMARGINS, nullptr, &checkboxBGContent);
+
+#define posdx(d) ((d) > 0 ? d : 0)
+
+ int dx =
+ posdx(checkboxBGContent.cxRightWidth - checkboxBGSizing.cxRightWidth) +
+ posdx(checkboxBGContent.cxLeftWidth - checkboxBGSizing.cxLeftWidth);
+ int dy =
+ posdx(checkboxBGContent.cyTopHeight - checkboxBGSizing.cyTopHeight) +
+ posdx(checkboxBGContent.cyBottomHeight - checkboxBGSizing.cyBottomHeight);
+
+#undef posdx
+
+ SIZE ret(GetCheckboxBGSize(theme, hdc));
+ ret.cx += dx;
+ ret.cy += dy;
+ return ret;
+}
+
+static SIZE GetGutterSize(HANDLE theme, HDC hdc) {
+ SIZE gutterSize;
+ GetThemePartSize(theme, hdc, MENU_POPUPGUTTER, 0, nullptr, TS_TRUE,
+ &gutterSize);
+
+ SIZE checkboxBGSize(GetCheckboxBGBounds(theme, hdc));
+
+ SIZE itemSize;
+ GetThemePartSize(theme, hdc, MENU_POPUPITEM, MPI_NORMAL, nullptr, TS_TRUE,
+ &itemSize);
+
+ // Figure out how big the menuitem's icon will be (if present) at current DPI
+ // Needs the system scale for consistency with Windows Theme API.
+ double scaleFactor = WinUtils::SystemScaleFactor();
+ int iconDevicePixels = NSToIntRound(16 * scaleFactor);
+ SIZE iconSize = {iconDevicePixels, iconDevicePixels};
+ // Not really sure what margins should be used here, but this seems to work in
+ // practice...
+ MARGINS margins = {0};
+ GetThemeMargins(theme, hdc, MENU_POPUPCHECKBACKGROUND, MCB_NORMAL,
+ TMT_CONTENTMARGINS, nullptr, &margins);
+ iconSize.cx += margins.cxLeftWidth + margins.cxRightWidth;
+ iconSize.cy += margins.cyTopHeight + margins.cyBottomHeight;
+
+ int width = std::max(
+ itemSize.cx, std::max(iconSize.cx, checkboxBGSize.cx) + gutterSize.cx);
+ int height = std::max(itemSize.cy, std::max(iconSize.cy, checkboxBGSize.cy));
+
+ SIZE ret;
+ ret.cx = width;
+ ret.cy = height;
+ return ret;
+}
+
+SIZE nsNativeThemeWin::GetCachedGutterSize(HANDLE theme) {
+ if (mGutterSizeCacheValid) {
+ return mGutterSizeCache;
+ }
+
+ mGutterSizeCache = GetGutterSize(theme, nullptr);
+ mGutterSizeCacheValid = true;
+
+ return mGutterSizeCache;
+}
+
+/*
+ * Notes on progress track and meter part constants:
+ * xp and up:
+ * PP_BAR(_VERT) - base progress track
+ * PP_TRANSPARENTBAR(_VERT) - transparent progress track. this only works if
+ * the underlying surface supports alpha. otherwise
+ * theme lib's DrawThemeBackground falls back on
+ * opaque PP_BAR. we currently don't use this.
+ * PP_CHUNK(_VERT) - xp progress meter. this does not draw an xp style
+ * progress w/chunks, it draws fill using the chunk
+ * graphic.
+ * vista and up:
+ * PP_FILL(_VERT) - progress meter. these have four states/colors.
+ * PP_PULSEOVERLAY(_VERT) - white reflection - an overlay, not sure what this
+ * is used for.
+ * PP_MOVEOVERLAY(_VERT) - green pulse - the pulse effect overlay on
+ * determined progress bars. we also use this for
+ * indeterminate chunk.
+ *
+ * Notes on state constants:
+ * PBBS_NORMAL - green progress
+ * PBBVS_PARTIAL/PBFVS_ERROR - red error progress
+ * PBFS_PAUSED - yellow paused progress
+ *
+ * There is no common controls style indeterminate part on vista and up.
+ */
+
+/*
+ * Progress bar related constants. These values are found by experimenting and
+ * comparing against native widgets used by the system. They are very unlikely
+ * exact but try to not be too wrong.
+ */
+// The amount of time we animate progress meters parts across the frame.
+static const double kProgressDeterminateTimeSpan = 3.0;
+static const double kProgressIndeterminateTimeSpan = 5.0;
+// The width of the overlay used to animate the horizontal progress bar (Vista
+// and later).
+static const int32_t kProgressHorizontalOverlaySize = 120;
+// The height of the overlay used to animate the vertical progress bar (Vista
+// and later).
+static const int32_t kProgressVerticalOverlaySize = 45;
+// The height of the overlay used for the vertical indeterminate progress bar
+// (Vista and later).
+static const int32_t kProgressVerticalIndeterminateOverlaySize = 60;
+// The width of the overlay used to animate the indeterminate progress bar
+// (Windows Classic).
+static const int32_t kProgressClassicOverlaySize = 40;
+
+/*
+ * GetProgressOverlayStyle - returns the proper overlay part for themed
+ * progress bars based on os and orientation.
+ */
+static int32_t GetProgressOverlayStyle(bool aIsVertical) {
+ return aIsVertical ? PP_MOVEOVERLAYVERT : PP_MOVEOVERLAY;
+}
+
+/*
+ * GetProgressOverlaySize - returns the minimum width or height for themed
+ * progress bar overlays. This includes the width of indeterminate chunks
+ * and vista pulse overlays.
+ */
+static int32_t GetProgressOverlaySize(bool aIsVertical, bool aIsIndeterminate) {
+ if (aIsVertical) {
+ return aIsIndeterminate ? kProgressVerticalIndeterminateOverlaySize
+ : kProgressVerticalOverlaySize;
+ }
+ return kProgressHorizontalOverlaySize;
+}
+
+/*
+ * IsProgressMeterFilled - Determines if a progress meter is at 100% fill based
+ * on a comparison of the current value and maximum.
+ */
+static bool IsProgressMeterFilled(nsIFrame* aFrame) {
+ NS_ENSURE_TRUE(aFrame, false);
+ nsIFrame* parentFrame = aFrame->GetParent();
+ NS_ENSURE_TRUE(parentFrame, false);
+ return nsNativeTheme::GetProgressValue(parentFrame) ==
+ nsNativeTheme::GetProgressMaxValue(parentFrame);
+}
+
+/*
+ * CalculateProgressOverlayRect - returns the padded overlay animation rect
+ * used in rendering progress bars. Resulting rects are used in rendering
+ * vista+ pulse overlays and indeterminate progress meters. Graphics should
+ * be rendered at the origin.
+ */
+RECT nsNativeThemeWin::CalculateProgressOverlayRect(nsIFrame* aFrame,
+ RECT* aWidgetRect,
+ bool aIsVertical,
+ bool aIsIndeterminate,
+ bool aIsClassic) {
+ NS_ASSERTION(aFrame, "bad frame pointer");
+ NS_ASSERTION(aWidgetRect, "bad rect pointer");
+
+ int32_t frameSize = aIsVertical ? aWidgetRect->bottom - aWidgetRect->top
+ : aWidgetRect->right - aWidgetRect->left;
+
+ // Recycle a set of progress pulse timers - these timers control the position
+ // of all progress overlays and indeterminate chunks that get rendered.
+ double span = aIsIndeterminate ? kProgressIndeterminateTimeSpan
+ : kProgressDeterminateTimeSpan;
+ TimeDuration period;
+ if (!aIsIndeterminate) {
+ if (TimeStamp::Now() >
+ (mProgressDeterminateTimeStamp + TimeDuration::FromSeconds(span))) {
+ mProgressDeterminateTimeStamp = TimeStamp::Now();
+ }
+ period = TimeStamp::Now() - mProgressDeterminateTimeStamp;
+ } else {
+ if (TimeStamp::Now() >
+ (mProgressIndeterminateTimeStamp + TimeDuration::FromSeconds(span))) {
+ mProgressIndeterminateTimeStamp = TimeStamp::Now();
+ }
+ period = TimeStamp::Now() - mProgressIndeterminateTimeStamp;
+ }
+
+ double percent = period / TimeDuration::FromSeconds(span);
+
+ if (!aIsVertical && IsFrameRTL(aFrame)) percent = 1 - percent;
+
+ RECT overlayRect = *aWidgetRect;
+ int32_t overlaySize;
+ if (!aIsClassic) {
+ overlaySize = GetProgressOverlaySize(aIsVertical, aIsIndeterminate);
+ } else {
+ overlaySize = kProgressClassicOverlaySize;
+ }
+
+ // Calculate a bounds that is larger than the meters frame such that the
+ // overlay starts and ends completely off the edge of the frame:
+ // [overlay][frame][overlay]
+ // This also yields a nice delay on rotation. Use overlaySize as the minimum
+ // size for [overlay] based on the graphics dims. If [frame] is larger, use
+ // the frame size instead.
+ int trackWidth = frameSize > overlaySize ? frameSize : overlaySize;
+ if (!aIsVertical) {
+ int xPos = aWidgetRect->left - trackWidth;
+ xPos += (int)ceil(((double)(trackWidth * 2) * percent));
+ overlayRect.left = xPos;
+ overlayRect.right = xPos + overlaySize;
+ } else {
+ int yPos = aWidgetRect->bottom + trackWidth;
+ yPos -= (int)ceil(((double)(trackWidth * 2) * percent));
+ overlayRect.bottom = yPos;
+ overlayRect.top = yPos - overlaySize;
+ }
+ return overlayRect;
+}
+
+/*
+ * DrawProgressMeter - render an appropriate progress meter based on progress
+ * meter style, orientation, and os. Note, this does not render the underlying
+ * progress track.
+ *
+ * @param aFrame the widget frame
+ * @param aAppearance type of widget
+ * @param aTheme progress theme handle
+ * @param aHdc hdc returned by gfxWindowsNativeDrawing
+ * @param aPart the PP_X progress part
+ * @param aState the theme state
+ * @param aWidgetRect bounding rect for the widget
+ * @param aClipRect dirty rect that needs drawing.
+ * @param aAppUnits app units per device pixel
+ */
+void nsNativeThemeWin::DrawThemedProgressMeter(
+ nsIFrame* aFrame, StyleAppearance aAppearance, HANDLE aTheme, HDC aHdc,
+ int aPart, int aState, RECT* aWidgetRect, RECT* aClipRect) {
+ if (!aFrame || !aTheme || !aHdc) return;
+
+ NS_ASSERTION(aWidgetRect, "bad rect pointer");
+ NS_ASSERTION(aClipRect, "bad clip rect pointer");
+
+ RECT adjWidgetRect, adjClipRect;
+ adjWidgetRect = *aWidgetRect;
+ adjClipRect = *aClipRect;
+
+ nsIFrame* parentFrame = aFrame->GetParent();
+ if (!parentFrame) {
+ // We have no parent to work with, just bail.
+ NS_WARNING("No parent frame for progress rendering. Can't paint.");
+ return;
+ }
+
+ ElementState elementState = GetContentState(parentFrame, aAppearance);
+ bool vertical = IsVerticalProgress(parentFrame);
+ bool indeterminate = elementState.HasState(ElementState::INDETERMINATE);
+ bool animate = indeterminate;
+
+ // Vista and up progress meter is fill style, rendered here. We render
+ // the pulse overlay in the follow up section below.
+ DrawThemeBackground(aTheme, aHdc, aPart, aState, &adjWidgetRect,
+ &adjClipRect);
+ if (!IsProgressMeterFilled(aFrame)) {
+ animate = true;
+ }
+
+ if (animate) {
+ // Indeterminate rendering
+ int32_t overlayPart = GetProgressOverlayStyle(vertical);
+ RECT overlayRect = CalculateProgressOverlayRect(
+ aFrame, &adjWidgetRect, vertical, indeterminate, false);
+ DrawThemeBackground(aTheme, aHdc, overlayPart, aState, &overlayRect,
+ &adjClipRect);
+
+ if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 60)) {
+ NS_WARNING("unable to animate progress widget!");
+ }
+ }
+}
+
+LayoutDeviceIntMargin nsNativeThemeWin::GetCachedWidgetBorder(
+ HTHEME aTheme, nsUXThemeClass aThemeClass, StyleAppearance aAppearance,
+ int32_t aPart, int32_t aState) {
+ int32_t cacheIndex = aThemeClass * THEME_PART_DISTINCT_VALUE_COUNT + aPart;
+ int32_t cacheBitIndex = cacheIndex / 8;
+ uint8_t cacheBit = 1u << (cacheIndex % 8);
+
+ if (mBorderCacheValid[cacheBitIndex] & cacheBit) {
+ return mBorderCache[cacheIndex];
+ }
+
+ // Get our info.
+ RECT outerRect; // Create a fake outer rect.
+ outerRect.top = outerRect.left = 100;
+ outerRect.right = outerRect.bottom = 200;
+ RECT contentRect(outerRect);
+ HRESULT res = GetThemeBackgroundContentRect(aTheme, nullptr, aPart, aState,
+ &outerRect, &contentRect);
+
+ if (FAILED(res)) {
+ return LayoutDeviceIntMargin();
+ }
+
+ // Now compute the delta in each direction and place it in our
+ // nsIntMargin struct.
+ LayoutDeviceIntMargin result;
+ result.top = contentRect.top - outerRect.top;
+ result.bottom = outerRect.bottom - contentRect.bottom;
+ result.left = contentRect.left - outerRect.left;
+ result.right = outerRect.right - contentRect.right;
+
+ mBorderCacheValid[cacheBitIndex] |= cacheBit;
+ mBorderCache[cacheIndex] = result;
+
+ return result;
+}
+
+nsresult nsNativeThemeWin::GetCachedMinimumWidgetSize(
+ nsIFrame* aFrame, HANDLE aTheme, nsUXThemeClass aThemeClass,
+ StyleAppearance aAppearance, int32_t aPart, int32_t aState,
+ THEMESIZE aSizeReq, mozilla::LayoutDeviceIntSize* aResult) {
+ int32_t cachePart = aPart;
+
+ if (aAppearance == StyleAppearance::Button && aSizeReq == TS_MIN) {
+ // In practice, StyleAppearance::Button is the only widget type which has an
+ // aSizeReq that varies for us, and it can only be TS_MIN or TS_TRUE. Just
+ // stuff that extra bit into the aPart part of the cache, since BP_Count is
+ // well below THEME_PART_DISTINCT_VALUE_COUNT anyway.
+ cachePart = BP_Count;
+ }
+
+ MOZ_ASSERT(aPart < THEME_PART_DISTINCT_VALUE_COUNT);
+ int32_t cacheIndex =
+ aThemeClass * THEME_PART_DISTINCT_VALUE_COUNT + cachePart;
+ int32_t cacheBitIndex = cacheIndex / 8;
+ uint8_t cacheBit = 1u << (cacheIndex % 8);
+
+ if (mMinimumWidgetSizeCacheValid[cacheBitIndex] & cacheBit) {
+ *aResult = mMinimumWidgetSizeCache[cacheIndex];
+ return NS_OK;
+ }
+
+ HDC hdc = ::GetDC(NULL);
+ if (!hdc) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SIZE sz;
+ GetThemePartSize(aTheme, hdc, aPart, aState, nullptr, aSizeReq, &sz);
+ aResult->width = sz.cx;
+ aResult->height = sz.cy;
+
+ ::ReleaseDC(nullptr, hdc);
+
+ mMinimumWidgetSizeCacheValid[cacheBitIndex] |= cacheBit;
+ mMinimumWidgetSizeCache[cacheIndex] = *aResult;
+
+ return NS_OK;
+}
+
+mozilla::Maybe<nsUXThemeClass> nsNativeThemeWin::GetThemeClass(
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ return Some(eUXButton);
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ return Some(eUXEdit);
+ case StyleAppearance::Toolbox:
+ return Some(eUXRebar);
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Separator:
+ return Some(eUXToolbar);
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ return Some(eUXProgress);
+ case StyleAppearance::Tab:
+ case StyleAppearance::Tabpanel:
+ case StyleAppearance::Tabpanels:
+ return Some(eUXTab);
+ case StyleAppearance::Range:
+ case StyleAppearance::RangeThumb:
+ return Some(eUXTrackbar);
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ return Some(eUXCombobox);
+ case StyleAppearance::Treeheadercell:
+ return Some(eUXHeader);
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::Treeitem:
+ return Some(eUXListview);
+ default:
+ return Nothing();
+ }
+}
+
+HANDLE
+nsNativeThemeWin::GetTheme(StyleAppearance aAppearance) {
+ mozilla::Maybe<nsUXThemeClass> themeClass = GetThemeClass(aAppearance);
+ if (themeClass.isNothing()) {
+ return nullptr;
+ }
+ return nsUXThemeData::GetTheme(themeClass.value());
+}
+
+int32_t nsNativeThemeWin::StandardGetState(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ bool wantFocused) {
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ if (elementState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE)) {
+ return TS_ACTIVE;
+ }
+ if (elementState.HasState(ElementState::HOVER)) {
+ return TS_HOVER;
+ }
+ if (wantFocused) {
+ if (elementState.HasState(ElementState::FOCUSRING)) {
+ return TS_FOCUSED;
+ }
+ // On Windows, focused buttons are always drawn as such by the native
+ // theme, that's why we check ElementState::FOCUS instead of
+ // ElementState::FOCUSRING.
+ if (aAppearance == StyleAppearance::Button &&
+ elementState.HasState(ElementState::FOCUS)) {
+ return TS_FOCUSED;
+ }
+ }
+
+ return TS_NORMAL;
+}
+
+bool nsNativeThemeWin::IsMenuActive(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ nsIContent* content = aFrame->GetContent();
+ if (content->IsXULElement() &&
+ content->NodeInfo()->Equals(nsGkAtoms::richlistitem))
+ return CheckBooleanAttr(aFrame, nsGkAtoms::selected);
+
+ return CheckBooleanAttr(aFrame, nsGkAtoms::menuactive);
+}
+
+/**
+ * aPart is filled in with the UXTheme part code. On return, values > 0
+ * are the actual UXTheme part code; -1 means the widget will be drawn by
+ * us; 0 means that we should use part code 0, which isn't a real part code
+ * but elicits some kind of default behaviour from UXTheme when drawing
+ * (but isThemeBackgroundPartiallyTransparent may not work).
+ */
+nsresult nsNativeThemeWin::GetThemePartAndState(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ int32_t& aPart,
+ int32_t& aState) {
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ aPart = BP_BUTTON;
+ if (!aFrame) {
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ if (elementState.HasState(ElementState::DISABLED)) {
+ aState = TS_DISABLED;
+ return NS_OK;
+ }
+ if (IsOpenButton(aFrame) || IsCheckedButton(aFrame)) {
+ aState = TS_ACTIVE;
+ return NS_OK;
+ }
+
+ aState = StandardGetState(aFrame, aAppearance, true);
+
+ // Check for default dialog buttons. These buttons should always look
+ // focused.
+ if (aState == TS_NORMAL && IsDefaultButton(aFrame)) aState = TS_FOCUSED;
+ return NS_OK;
+ }
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea: {
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+
+ /* Note: the NOSCROLL type has a rounded corner in each corner. The more
+ * specific HSCROLL, VSCROLL, HVSCROLL types have side and/or top/bottom
+ * edges rendered as straight horizontal lines with sharp corners to
+ * accommodate a scrollbar. However, the scrollbar gets rendered on top
+ * of this for us, so we don't care, and can just use NOSCROLL here.
+ */
+ aPart = TFP_EDITBORDER_NOSCROLL;
+
+ if (!aFrame) {
+ aState = TFS_EDITBORDER_NORMAL;
+ } else if (elementState.HasState(ElementState::DISABLED)) {
+ aState = TFS_EDITBORDER_DISABLED;
+ } else if (IsReadOnly(aFrame)) {
+ /* no special read-only state */
+ aState = TFS_EDITBORDER_NORMAL;
+ } else if (elementState.HasAtLeastOneOfStates(ElementState::ACTIVE |
+ ElementState::FOCUSRING)) {
+ aState = TFS_EDITBORDER_FOCUSED;
+ } else if (elementState.HasState(ElementState::HOVER)) {
+ aState = TFS_EDITBORDER_HOVER;
+ } else {
+ aState = TFS_EDITBORDER_NORMAL;
+ }
+
+ return NS_OK;
+ }
+ case StyleAppearance::ProgressBar: {
+ bool vertical = IsVerticalProgress(aFrame);
+ aPart = vertical ? PP_BARVERT : PP_BAR;
+ aState = PBBS_NORMAL;
+ return NS_OK;
+ }
+ case StyleAppearance::Progresschunk: {
+ nsIFrame* parentFrame = aFrame->GetParent();
+ if (IsVerticalProgress(parentFrame)) {
+ aPart = PP_FILLVERT;
+ } else {
+ aPart = PP_FILL;
+ }
+
+ aState = PBBVS_NORMAL;
+ return NS_OK;
+ }
+ case StyleAppearance::Toolbarbutton: {
+ aPart = BP_BUTTON;
+ if (!aFrame) {
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ if (elementState.HasState(ElementState::DISABLED)) {
+ aState = TS_DISABLED;
+ return NS_OK;
+ }
+ if (IsOpenButton(aFrame)) {
+ aState = TS_ACTIVE;
+ return NS_OK;
+ }
+
+ if (elementState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE))
+ aState = TS_ACTIVE;
+ else if (elementState.HasState(ElementState::HOVER)) {
+ if (IsCheckedButton(aFrame))
+ aState = TB_HOVER_CHECKED;
+ else
+ aState = TS_HOVER;
+ } else {
+ if (IsCheckedButton(aFrame))
+ aState = TB_CHECKED;
+ else
+ aState = TS_NORMAL;
+ }
+
+ return NS_OK;
+ }
+ case StyleAppearance::Separator: {
+ aPart = TP_SEPARATOR;
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+ case StyleAppearance::Range: {
+ if (IsRangeHorizontal(aFrame)) {
+ aPart = TKP_TRACK;
+ aState = TRS_NORMAL;
+ } else {
+ aPart = TKP_TRACKVERT;
+ aState = TRVS_NORMAL;
+ }
+ return NS_OK;
+ }
+ case StyleAppearance::RangeThumb: {
+ if (IsRangeHorizontal(aFrame)) {
+ aPart = TKP_THUMBBOTTOM;
+ } else {
+ aPart = IsFrameRTL(aFrame) ? TKP_THUMBLEFT : TKP_THUMBRIGHT;
+ }
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ if (!aFrame) {
+ aState = TS_NORMAL;
+ } else if (elementState.HasState(ElementState::DISABLED)) {
+ aState = TKP_DISABLED;
+ } else {
+ if (elementState.HasState(
+ ElementState::ACTIVE)) // Hover is not also a requirement for
+ // the thumb, since the drag is not
+ // canceled when you move outside the
+ // thumb.
+ aState = TS_ACTIVE;
+ else if (elementState.HasState(ElementState::FOCUSRING))
+ aState = TKP_FOCUSED;
+ else if (elementState.HasState(ElementState::HOVER))
+ aState = TS_HOVER;
+ else
+ aState = TS_NORMAL;
+ }
+ return NS_OK;
+ }
+ case StyleAppearance::Toolbox: {
+ aState = 0;
+ aPart = RP_BACKGROUND;
+ return NS_OK;
+ }
+ case StyleAppearance::Toolbar: {
+ // Use -1 to indicate we don't wish to have the theme background drawn
+ // for this item. We will pass any nessessary information via aState,
+ // and will render the item using separate code.
+ aPart = -1;
+ aState = 0;
+ if (aFrame) {
+ nsIContent* content = aFrame->GetContent();
+ nsIContent* parent = content->GetParent();
+ // XXXzeniko hiding the first toolbar will result in an unwanted margin
+ if (parent && parent->GetFirstChild() == content) {
+ aState = 1;
+ }
+ }
+ return NS_OK;
+ }
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Listbox: {
+ aPart = TREEVIEW_BODY;
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+ case StyleAppearance::Tabpanels: {
+ aPart = TABP_PANELS;
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+ case StyleAppearance::Tabpanel: {
+ aPart = TABP_PANEL;
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+ case StyleAppearance::Tab: {
+ aPart = TABP_TAB;
+ if (!aFrame) {
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ if (elementState.HasState(ElementState::DISABLED)) {
+ aState = TS_DISABLED;
+ return NS_OK;
+ }
+
+ if (IsSelectedTab(aFrame)) {
+ aPart = TABP_TAB_SELECTED;
+ aState = TS_ACTIVE; // The selected tab is always "pressed".
+ } else
+ aState = StandardGetState(aFrame, aAppearance, true);
+
+ return NS_OK;
+ }
+ case StyleAppearance::Treeheadercell: {
+ aPart = 1;
+ if (!aFrame) {
+ aState = TS_NORMAL;
+ return NS_OK;
+ }
+
+ aState = StandardGetState(aFrame, aAppearance, true);
+
+ return NS_OK;
+ }
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist: {
+ nsIContent* content = aFrame->GetContent();
+ bool useDropBorder = content && content->IsHTMLElement();
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+
+ /* On Vista/Win7, we use CBP_DROPBORDER instead of DROPFRAME for HTML
+ * content or for editable menulists; this gives us the thin outline,
+ * instead of the gradient-filled background */
+ if (useDropBorder)
+ aPart = CBP_DROPBORDER;
+ else
+ aPart = CBP_DROPFRAME;
+
+ if (elementState.HasState(ElementState::DISABLED)) {
+ aState = TS_DISABLED;
+ } else if (IsReadOnly(aFrame)) {
+ aState = TS_NORMAL;
+ } else if (IsOpenButton(aFrame)) {
+ aState = TS_ACTIVE;
+ } else if (useDropBorder &&
+ elementState.HasState(ElementState::FOCUSRING)) {
+ aState = TS_ACTIVE;
+ } else if (elementState.HasAllStates(ElementState::HOVER |
+ ElementState::ACTIVE)) {
+ aState = TS_ACTIVE;
+ } else if (elementState.HasState(ElementState::HOVER)) {
+ aState = TS_HOVER;
+ } else {
+ aState = TS_NORMAL;
+ }
+
+ return NS_OK;
+ }
+ default:
+ aPart = 0;
+ aState = 0;
+ return NS_ERROR_FAILURE;
+ }
+}
+
+static bool AssumeThemePartAndStateAreTransparent(int32_t aPart,
+ int32_t aState) {
+ if (!nsUXThemeData::IsHighContrastOn() && aPart == MENU_POPUPITEM &&
+ aState == MBI_NORMAL) {
+ return true;
+ }
+ return false;
+}
+
+// When running with per-monitor DPI (on Win8.1+), and rendering on a display
+// with a different DPI setting from the system's default scaling, we need to
+// apply scaling to native-themed elements as the Windows theme APIs assume
+// the system default resolution.
+static inline double GetThemeDpiScaleFactor(nsPresContext* aPresContext) {
+ if (WinUtils::IsPerMonitorDPIAware() ||
+ StaticPrefs::layout_css_devPixelsPerPx() > 0.0) {
+ nsCOMPtr<nsIWidget> rootWidget = aPresContext->GetRootWidget();
+ if (rootWidget) {
+ double systemScale = WinUtils::SystemScaleFactor();
+ return rootWidget->GetDefaultScale().scale / systemScale;
+ }
+ }
+ return 1.0;
+}
+
+static inline double GetThemeDpiScaleFactor(nsIFrame* aFrame) {
+ return GetThemeDpiScaleFactor(aFrame->PresContext());
+}
+
+NS_IMETHODIMP
+nsNativeThemeWin::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect,
+ DrawOverflow aDrawOverflow) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect,
+ aDirtyRect, aDrawOverflow);
+ }
+
+ HANDLE theme = GetTheme(aAppearance);
+ if (!theme)
+ return ClassicDrawWidgetBackground(aContext, aFrame, aAppearance, aRect,
+ aDirtyRect);
+
+ // ^^ without the right sdk, assume xp theming and fall through.
+ int32_t part, state;
+ nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state);
+ if (NS_FAILED(rv)) return rv;
+
+ if (AssumeThemePartAndStateAreTransparent(part, state)) {
+ return NS_OK;
+ }
+
+ gfxContextMatrixAutoSaveRestore save(aContext);
+
+ double themeScale = GetThemeDpiScaleFactor(aFrame);
+ if (themeScale != 1.0) {
+ aContext->SetMatrix(
+ aContext->CurrentMatrix().PreScale(themeScale, themeScale));
+ }
+
+ gfxFloat p2a = gfxFloat(aFrame->PresContext()->AppUnitsPerDevPixel());
+ RECT widgetRect;
+ RECT clipRect;
+ gfxRect tr(aRect.X(), aRect.Y(), aRect.Width(), aRect.Height()),
+ dr(aDirtyRect.X(), aDirtyRect.Y(), aDirtyRect.Width(),
+ aDirtyRect.Height());
+
+ tr.Scale(1.0 / (p2a * themeScale));
+ dr.Scale(1.0 / (p2a * themeScale));
+
+ gfxWindowsNativeDrawing nativeDrawing(
+ aContext, dr, GetWidgetNativeDrawingFlags(aAppearance));
+
+RENDER_AGAIN:
+
+ HDC hdc = nativeDrawing.BeginNativeDrawing();
+ if (!hdc) return NS_ERROR_FAILURE;
+
+ nativeDrawing.TransformToNativeRect(tr, widgetRect);
+ nativeDrawing.TransformToNativeRect(dr, clipRect);
+
+#if 0
+ {
+ MOZ_LOG(gWindowsLog, LogLevel::Error,
+ (stderr, "xform: %f %f %f %f [%f %f]\n", m._11, m._21, m._12, m._22,
+ m._31, m._32));
+ MOZ_LOG(gWindowsLog, LogLevel::Error,
+ (stderr, "tr: [%d %d %d %d]\ndr: [%d %d %d %d]\noff: [%f %f]\n",
+ tr.x, tr.y, tr.width, tr.height, dr.x, dr.y, dr.width, dr.height,
+ offset.x, offset.y));
+ }
+#endif
+
+ if (aAppearance == StyleAppearance::Tab) {
+ // For left edge and right edge tabs, we need to adjust the widget
+ // rects and clip rects so that the edges don't get drawn.
+ bool isLeft = IsLeftToSelectedTab(aFrame);
+ bool isRight = !isLeft && IsRightToSelectedTab(aFrame);
+
+ if (isLeft || isRight) {
+ // HACK ALERT: There appears to be no way to really obtain this value, so
+ // we're forced to just use the default value for Luna (which also happens
+ // to be correct for all the other skins I've tried).
+ int32_t edgeSize = 2;
+
+ // Armed with the size of the edge, we now need to either shift to the
+ // left or to the right. The clip rect won't include this extra area, so
+ // we know that we're effectively shifting the edge out of view (such that
+ // it won't be painted).
+ if (isLeft)
+ // The right edge should not be drawn. Extend our rect by the edge
+ // size.
+ widgetRect.right += edgeSize;
+ else
+ // The left edge should not be drawn. Move the widget rect's left coord
+ // back.
+ widgetRect.left -= edgeSize;
+ }
+ }
+
+ // widgetRect is the bounding box for a widget, yet the scale track is only
+ // a small portion of this size, so the edges of the scale need to be
+ // adjusted to the real size of the track.
+ if (aAppearance == StyleAppearance::Range) {
+ RECT contentRect;
+ GetThemeBackgroundContentRect(theme, hdc, part, state, &widgetRect,
+ &contentRect);
+
+ SIZE siz;
+ GetThemePartSize(theme, hdc, part, state, &widgetRect, TS_TRUE, &siz);
+
+ // When rounding is necessary, we round the position of the track
+ // away from the chevron of the thumb to make it look better.
+ if (IsRangeHorizontal(aFrame)) {
+ contentRect.top += (contentRect.bottom - contentRect.top - siz.cy) / 2;
+ contentRect.bottom = contentRect.top + siz.cy;
+ } else {
+ if (!IsFrameRTL(aFrame)) {
+ contentRect.left += (contentRect.right - contentRect.left - siz.cx) / 2;
+ contentRect.right = contentRect.left + siz.cx;
+ } else {
+ contentRect.right -=
+ (contentRect.right - contentRect.left - siz.cx) / 2;
+ contentRect.left = contentRect.right - siz.cx;
+ }
+ }
+
+ DrawThemeBackground(theme, hdc, part, state, &contentRect, &clipRect);
+ } else if (aAppearance == StyleAppearance::NumberInput ||
+ aAppearance == StyleAppearance::Textfield ||
+ aAppearance == StyleAppearance::Textarea) {
+ DrawThemeBackground(theme, hdc, part, state, &widgetRect, &clipRect);
+
+ if (state == TFS_EDITBORDER_DISABLED) {
+ InflateRect(&widgetRect, -1, -1);
+ ::FillRect(hdc, &widgetRect, reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1));
+ }
+ } else if (aAppearance == StyleAppearance::ProgressBar) {
+ // DrawThemeBackground renders each corner with a solid white pixel.
+ // Restore these pixels to the underlying color. Tracks are rendered
+ // using alpha recovery, so this makes the corners transparent.
+ COLORREF color;
+ color = GetPixel(hdc, widgetRect.left, widgetRect.top);
+ DrawThemeBackground(theme, hdc, part, state, &widgetRect, &clipRect);
+ SetPixel(hdc, widgetRect.left, widgetRect.top, color);
+ SetPixel(hdc, widgetRect.right - 1, widgetRect.top, color);
+ SetPixel(hdc, widgetRect.right - 1, widgetRect.bottom - 1, color);
+ SetPixel(hdc, widgetRect.left, widgetRect.bottom - 1, color);
+ } else if (aAppearance == StyleAppearance::Progresschunk) {
+ DrawThemedProgressMeter(aFrame, aAppearance, theme, hdc, part, state,
+ &widgetRect, &clipRect);
+ }
+ // If part is negative, the element wishes us to not render a themed
+ // background, instead opting to be drawn specially below.
+ else if (part >= 0) {
+ DrawThemeBackground(theme, hdc, part, state, &widgetRect, &clipRect);
+ }
+
+ // Draw focus rectangles for range elements
+ // XXX it'd be nice to draw these outside of the frame
+ if (aAppearance == StyleAppearance::Range) {
+ ElementState contentState = GetContentState(aFrame, aAppearance);
+
+ if (contentState.HasState(ElementState::FOCUSRING)) {
+ POINT vpOrg;
+ HPEN hPen = nullptr;
+
+ uint8_t id = SaveDC(hdc);
+
+ ::SelectClipRgn(hdc, nullptr);
+ ::GetViewportOrgEx(hdc, &vpOrg);
+ ::SetBrushOrgEx(hdc, vpOrg.x + widgetRect.left, vpOrg.y + widgetRect.top,
+ nullptr);
+ ::SetTextColor(hdc, 0);
+ ::DrawFocusRect(hdc, &widgetRect);
+ ::RestoreDC(hdc, id);
+ if (hPen) {
+ ::DeleteObject(hPen);
+ }
+ }
+ } else if (aAppearance == StyleAppearance::Toolbar && state == 0) {
+ // Draw toolbar separator lines above all toolbars except the first one.
+ // The lines are part of the Rebar theme, which is loaded for
+ // StyleAppearance::Toolbox.
+ theme = GetTheme(StyleAppearance::Toolbox);
+ if (!theme) return NS_ERROR_FAILURE;
+
+ widgetRect.bottom = widgetRect.top + TB_SEPARATOR_HEIGHT;
+ DrawThemeEdge(theme, hdc, RP_BAND, 0, &widgetRect, EDGE_ETCHED, BF_TOP,
+ nullptr);
+ }
+
+ nativeDrawing.EndNativeDrawing();
+
+ if (nativeDrawing.ShouldRenderAgain()) goto RENDER_AGAIN;
+
+ nativeDrawing.PaintToContext();
+
+ return NS_OK;
+}
+
+bool nsNativeThemeWin::CreateWebRenderCommandsForWidget(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ const layers::StackingContextHelper& aSc,
+ layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::CreateWebRenderCommandsForWidget(
+ aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect);
+ }
+ return false;
+}
+
+static void ScaleForFrameDPI(LayoutDeviceIntMargin* aMargin, nsIFrame* aFrame) {
+ double themeScale = GetThemeDpiScaleFactor(aFrame);
+ if (themeScale != 1.0) {
+ aMargin->top = NSToIntRound(aMargin->top * themeScale);
+ aMargin->left = NSToIntRound(aMargin->left * themeScale);
+ aMargin->bottom = NSToIntRound(aMargin->bottom * themeScale);
+ aMargin->right = NSToIntRound(aMargin->right * themeScale);
+ }
+}
+
+static void ScaleForFrameDPI(LayoutDeviceIntSize* aSize, nsIFrame* aFrame) {
+ double themeScale = GetThemeDpiScaleFactor(aFrame);
+ if (themeScale != 1.0) {
+ aSize->width = NSToIntRound(aSize->width * themeScale);
+ aSize->height = NSToIntRound(aSize->height * themeScale);
+ }
+}
+
+LayoutDeviceIntMargin nsNativeThemeWin::GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetWidgetBorder(aContext, aFrame, aAppearance);
+ }
+
+ LayoutDeviceIntMargin result;
+ mozilla::Maybe<nsUXThemeClass> themeClass = GetThemeClass(aAppearance);
+ HTHEME theme = NULL;
+ if (!themeClass.isNothing()) {
+ theme = nsUXThemeData::GetTheme(themeClass.value());
+ }
+ if (!theme) {
+ result = ClassicGetWidgetBorder(aContext, aFrame, aAppearance);
+ ScaleForFrameDPI(&result, aFrame);
+ return result;
+ }
+
+ if (!WidgetIsContainer(aAppearance) ||
+ aAppearance == StyleAppearance::Toolbox ||
+ aAppearance == StyleAppearance::Tabpanel)
+ return result; // Don't worry about it.
+
+ int32_t part, state;
+ nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state);
+ if (NS_FAILED(rv)) return result;
+
+ if (aAppearance == StyleAppearance::Toolbar) {
+ // make space for the separator line above all toolbars but the first
+ if (state == 0) result.top = TB_SEPARATOR_HEIGHT;
+ return result;
+ }
+
+ result = GetCachedWidgetBorder(theme, themeClass.value(), aAppearance, part,
+ state);
+
+ // Remove the edges for tabs that are before or after the selected tab,
+ if (aAppearance == StyleAppearance::Tab) {
+ if (IsLeftToSelectedTab(aFrame))
+ // Remove the right edge, since we won't be drawing it.
+ result.right = 0;
+ else if (IsRightToSelectedTab(aFrame))
+ // Remove the left edge, since we won't be drawing it.
+ result.left = 0;
+ }
+
+ if (aFrame && (aAppearance == StyleAppearance::NumberInput ||
+ aAppearance == StyleAppearance::Textfield ||
+ aAppearance == StyleAppearance::Textarea)) {
+ nsIContent* content = aFrame->GetContent();
+ if (content && content->IsHTMLElement()) {
+ // We need to pad textfields by 1 pixel, since the caret will draw
+ // flush against the edge by default if we don't.
+ result.top.value++;
+ result.left.value++;
+ result.bottom.value++;
+ result.right.value++;
+ }
+ }
+
+ ScaleForFrameDPI(&result, aFrame);
+ return result;
+}
+
+bool nsNativeThemeWin::GetWidgetPadding(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetWidgetPadding(aContext, aFrame, aAppearance, aResult);
+ }
+
+ bool ok = true;
+ HANDLE theme = GetTheme(aAppearance);
+ if (!theme) {
+ ok = ClassicGetWidgetPadding(aContext, aFrame, aAppearance, aResult);
+ ScaleForFrameDPI(aResult, aFrame);
+ return ok;
+ }
+
+ /* textfields need extra pixels on all sides, otherwise they wrap their
+ * content too tightly. The actual border is drawn 1px inside the specified
+ * rectangle, so Gecko will end up making the contents look too small.
+ * Instead, we add 2px padding for the contents and fix this. (Used to be 1px
+ * added, see bug 430212)
+ */
+ if (aAppearance == StyleAppearance::NumberInput ||
+ aAppearance == StyleAppearance::Textfield ||
+ aAppearance == StyleAppearance::Textarea) {
+ aResult->top = aResult->bottom = 2;
+ aResult->left = aResult->right = 2;
+ ScaleForFrameDPI(aResult, aFrame);
+ return ok;
+ } else if (IsHTMLContent(aFrame) &&
+ (aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton)) {
+ /* For content menulist controls, we need an extra pixel so that we have
+ * room to draw our focus rectangle stuff. Otherwise, the focus rect might
+ * overlap the control's border.
+ */
+ aResult->top = aResult->bottom = 1;
+ aResult->left = aResult->right = 1;
+ ScaleForFrameDPI(aResult, aFrame);
+ return ok;
+ }
+
+ int32_t right, left, top, bottom;
+ right = left = top = bottom = 0;
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ if (aFrame->GetContent()->IsXULElement()) {
+ top = 2;
+ bottom = 3;
+ }
+ left = right = 5;
+ break;
+ default:
+ return false;
+ }
+
+ if (IsFrameRTL(aFrame)) {
+ aResult->right = left;
+ aResult->left = right;
+ } else {
+ aResult->right = right;
+ aResult->left = left;
+ }
+ aResult->top = top;
+ aResult->bottom = bottom;
+
+ ScaleForFrameDPI(aResult, aFrame);
+ return ok;
+}
+
+bool nsNativeThemeWin::GetWidgetOverflow(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::GetWidgetOverflow(aContext, aFrame, aAppearance,
+ aOverflowRect);
+ }
+
+ /* This is disabled for now, because it causes invalidation problems --
+ * see bug 420381. The effect of not updating the overflow area is that
+ * for dropdown buttons in content areas, there is a 1px border on 3 sides
+ * where, if invalidated, the dropdown control probably won't be repainted.
+ * This is fairly minor, as by default there is nothing in that area, and
+ * a border only shows up if the widget is being hovered.
+ *
+ * TODO(jwatt): Figure out what do to about
+ * StyleAppearance::MozMenulistArrowButton too.
+ */
+#if 0
+ /* We explicitly draw dropdown buttons in HTML content 1px bigger up, right,
+ * and bottom so that they overlap the dropdown's border like they're
+ * supposed to.
+ */
+ if (aAppearance == StyleAppearance::MenulistButton &&
+ IsHTMLContent(aFrame) &&
+ !IsWidgetStyled(aFrame->GetParent()->PresContext(),
+ aFrame->GetParent(),
+ StyleAppearance::Menulist))
+ {
+ int32_t p2a = aContext->AppUnitsPerDevPixel();
+ /* Note: no overflow on the left */
+ nsMargin m(p2a, p2a, p2a, 0);
+ aOverflowRect->Inflate (m);
+ return true;
+ }
+#endif
+
+ return false;
+}
+
+LayoutDeviceIntSize nsNativeThemeWin::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance);
+ }
+
+ mozilla::Maybe<nsUXThemeClass> themeClass = GetThemeClass(aAppearance);
+ HTHEME theme = NULL;
+ if (!themeClass.isNothing()) {
+ theme = nsUXThemeData::GetTheme(themeClass.value());
+ }
+ if (!theme) {
+ auto result = ClassicGetMinimumWidgetSize(aFrame, aAppearance);
+ ScaleForFrameDPI(&result, aFrame);
+ return result;
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::Tabpanel:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ return {}; // Don't worry about it.
+ default:
+ break;
+ }
+
+ // Call GetSystemMetrics to determine size for WinXP scrollbars
+ // (GetThemeSysSize API returns the optimal size for the theme, but
+ // Windows appears to always use metrics when drawing standard scrollbars)
+ THEMESIZE sizeReq = TS_TRUE; // Best-fit size
+ switch (aAppearance) {
+ case StyleAppearance::ProgressBar:
+ // Best-fit size for progress meters is too large for most
+ // themes. We want these widgets to be able to really shrink
+ // down, so use the min-size request value (of 0).
+ sizeReq = TS_MIN;
+ break;
+
+ case StyleAppearance::RangeThumb: {
+ LayoutDeviceIntSize result(12, 20);
+ if (!IsRangeHorizontal(aFrame)) {
+ std::swap(result.width, result.height);
+ }
+ ScaleForFrameDPI(&result, aFrame);
+ return result;
+ }
+
+ case StyleAppearance::Separator: {
+ // that's 2px left margin, 2px right margin and 2px separator
+ // (the margin is drawn as part of the separator, though)
+ LayoutDeviceIntSize result(6, 0);
+ ScaleForFrameDPI(&result, aFrame);
+ return result;
+ }
+
+ case StyleAppearance::Button:
+ // We should let HTML buttons shrink to their min size.
+ // FIXME bug 403934: We should probably really separate
+ // GetPreferredWidgetSize from GetMinimumWidgetSize, so callers can
+ // use the one they want.
+ if (aFrame->GetContent()->IsHTMLElement()) {
+ sizeReq = TS_MIN;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ int32_t part, state;
+ nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state);
+ if (NS_FAILED(rv)) {
+ return {};
+ }
+
+ LayoutDeviceIntSize result;
+ rv = GetCachedMinimumWidgetSize(aFrame, theme, themeClass.value(),
+ aAppearance, part, state, sizeReq, &result);
+ ScaleForFrameDPI(&result, aFrame);
+ return result;
+}
+
+NS_IMETHODIMP
+nsNativeThemeWin::WidgetStateChanged(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) {
+ // Some widget types just never change state.
+ if (aAppearance == StyleAppearance::Toolbox ||
+ aAppearance == StyleAppearance::Toolbar ||
+ aAppearance == StyleAppearance::Progresschunk ||
+ aAppearance == StyleAppearance::ProgressBar ||
+ aAppearance == StyleAppearance::Tabpanels ||
+ aAppearance == StyleAppearance::Tabpanel ||
+ aAppearance == StyleAppearance::Separator) {
+ *aShouldRepaint = false;
+ return NS_OK;
+ }
+
+ // We need to repaint the dropdown arrow in vista HTML combobox controls when
+ // the control is closed to get rid of the hover effect.
+ if ((aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton) &&
+ nsNativeTheme::IsHTMLContent(aFrame)) {
+ *aShouldRepaint = true;
+ return NS_OK;
+ }
+
+ // XXXdwh Not sure what can really be done here. Can at least guess for
+ // specific widgets that they're highly unlikely to have certain states.
+ // For example, a toolbar doesn't care about any states.
+ if (!aAttribute) {
+ // Hover/focus/active changed. Always repaint.
+ *aShouldRepaint = true;
+ } else {
+ // Check the attribute to see if it's relevant.
+ // disabled, checked, dlgtype, default, etc.
+ *aShouldRepaint = false;
+ if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
+ aAttribute == nsGkAtoms::selected ||
+ aAttribute == nsGkAtoms::visuallyselected ||
+ aAttribute == nsGkAtoms::readonly || aAttribute == nsGkAtoms::open ||
+ aAttribute == nsGkAtoms::menuactive || aAttribute == nsGkAtoms::focused)
+ *aShouldRepaint = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNativeThemeWin::ThemeChanged() {
+ nsUXThemeData::Invalidate();
+ memset(mBorderCacheValid, 0, sizeof(mBorderCacheValid));
+ memset(mMinimumWidgetSizeCacheValid, 0, sizeof(mMinimumWidgetSizeCacheValid));
+ mGutterSizeCacheValid = false;
+ return NS_OK;
+}
+
+bool nsNativeThemeWin::ThemeSupportsWidget(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ // XXXdwh We can go even further and call the API to ask if support exists for
+ // specific widgets.
+
+ if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) {
+ return Theme::ThemeSupportsWidget(aPresContext, aFrame, aAppearance);
+ }
+
+ HANDLE theme = GetTheme(aAppearance);
+ if (theme || ClassicThemeSupportsWidget(aFrame, aAppearance))
+ // turn off theming for some HTML widgets styled by the page
+ return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+
+ return false;
+}
+
+bool nsNativeThemeWin::ThemeDrawsFocusForWidget(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::ThemeDrawsFocusForWidget(aFrame, aAppearance);
+ }
+ switch (aAppearance) {
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::NumberInput:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool nsNativeThemeWin::ThemeNeedsComboboxDropmarker() { return true; }
+
+nsITheme::Transparency nsNativeThemeWin::GetWidgetTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::GetWidgetTransparency(aFrame, aAppearance);
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Range:
+ return eTransparent;
+ default:
+ break;
+ }
+
+ HANDLE theme = GetTheme(aAppearance);
+ // For the classic theme we don't really have a way of knowing
+ if (!theme) {
+ return eUnknownTransparency;
+ }
+
+ int32_t part, state;
+ nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state);
+ // Fail conservatively
+ NS_ENSURE_SUCCESS(rv, eUnknownTransparency);
+
+ if (part <= 0) {
+ // Not a real part code, so IsThemeBackgroundPartiallyTransparent may
+ // not work, so don't call it.
+ return eUnknownTransparency;
+ }
+
+ if (IsThemeBackgroundPartiallyTransparent(theme, part, state))
+ return eTransparent;
+ return eOpaque;
+}
+
+/* Windows 9x/NT/2000/Classic XP Theme Support */
+
+bool nsNativeThemeWin::ClassicThemeSupportsWidget(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Range:
+ case StyleAppearance::RangeThumb:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Tab:
+ case StyleAppearance::Tabpanel:
+ case StyleAppearance::Tabpanels:
+ return true;
+ default:
+ return false;
+ }
+}
+
+LayoutDeviceIntMargin nsNativeThemeWin::ClassicGetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) {
+ LayoutDeviceIntMargin result;
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ result.top = result.left = result.bottom = result.right = 2;
+ break;
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Tab:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ result.top = result.left = result.bottom = result.right = 2;
+ break;
+ case StyleAppearance::ProgressBar:
+ result.top = result.left = result.bottom = result.right = 1;
+ break;
+ default:
+ result.top = result.bottom = result.left = result.right = 0;
+ break;
+ }
+ return result;
+}
+
+bool nsNativeThemeWin::ClassicGetWidgetPadding(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ switch (aAppearance) {
+ case StyleAppearance::ProgressBar:
+ (*aResult).top = (*aResult).left = (*aResult).bottom = (*aResult).right =
+ 1;
+ return true;
+ default:
+ return false;
+ }
+}
+
+LayoutDeviceIntSize nsNativeThemeWin::ClassicGetMinimumWidgetSize(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ LayoutDeviceIntSize result;
+ switch (aAppearance) {
+ case StyleAppearance::RangeThumb: {
+ if (IsRangeHorizontal(aFrame)) {
+ result.width = 12;
+ result.height = 20;
+ } else {
+ result.width = 20;
+ result.height = 12;
+ }
+ break;
+ }
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Button:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Tab:
+ case StyleAppearance::Tabpanel:
+ case StyleAppearance::Tabpanels:
+ // no minimum widget size
+ break;
+
+ default:
+ break;
+ }
+ return result;
+}
+
+nsresult nsNativeThemeWin::ClassicGetThemePartAndState(
+ nsIFrame* aFrame, StyleAppearance aAppearance, int32_t& aPart,
+ int32_t& aState, bool& aFocused) {
+ aFocused = false;
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ aPart = DFC_BUTTON;
+ aState = DFCS_BUTTONPUSH;
+ aFocused = false;
+
+ ElementState contentState = GetContentState(aFrame, aAppearance);
+ if (contentState.HasState(ElementState::DISABLED)) {
+ aState |= DFCS_INACTIVE;
+ } else if (IsOpenButton(aFrame)) {
+ aState |= DFCS_PUSHED;
+ } else if (IsCheckedButton(aFrame)) {
+ aState |= DFCS_CHECKED;
+ } else {
+ if (contentState.HasAllStates(ElementState::ACTIVE |
+ ElementState::HOVER)) {
+ aState |= DFCS_PUSHED;
+ // The down state is flat if the button is focusable
+ if (aFrame->StyleUI()->UserFocus() == StyleUserFocus::Normal) {
+ if (!aFrame->GetContent()->IsHTMLElement()) aState |= DFCS_FLAT;
+
+ aFocused = true;
+ }
+ }
+ // On Windows, focused buttons are always drawn as such by the native
+ // theme, that's why we check ElementState::FOCUS instead of
+ // ElementState::FOCUSRING.
+ if (contentState.HasState(ElementState::FOCUS) ||
+ (aState == DFCS_BUTTONPUSH && IsDefaultButton(aFrame))) {
+ aFocused = true;
+ }
+ }
+
+ return NS_OK;
+ }
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Range:
+ case StyleAppearance::RangeThumb:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Tab:
+ case StyleAppearance::Tabpanel:
+ case StyleAppearance::Tabpanels:
+ // these don't use DrawFrameControl
+ return NS_OK;
+ default:
+ return NS_ERROR_FAILURE;
+ }
+}
+
+// Draw classic Windows tab
+// (no system API for this, but DrawEdge can draw all the parts of a tab)
+static void DrawTab(HDC hdc, const RECT& R, int32_t aPosition, bool aSelected,
+ bool aDrawLeft, bool aDrawRight) {
+ int32_t leftFlag, topFlag, rightFlag, lightFlag, shadeFlag;
+ RECT topRect, sideRect, bottomRect, lightRect, shadeRect;
+ int32_t selectedOffset, lOffset, rOffset;
+
+ selectedOffset = aSelected ? 1 : 0;
+ lOffset = aDrawLeft ? 2 : 0;
+ rOffset = aDrawRight ? 2 : 0;
+
+ // Get info for tab orientation/position (Left, Top, Right, Bottom)
+ switch (aPosition) {
+ case BF_LEFT:
+ leftFlag = BF_TOP;
+ topFlag = BF_LEFT;
+ rightFlag = BF_BOTTOM;
+ lightFlag = BF_DIAGONAL_ENDTOPRIGHT;
+ shadeFlag = BF_DIAGONAL_ENDBOTTOMRIGHT;
+
+ ::SetRect(&topRect, R.left, R.top + lOffset, R.right, R.bottom - rOffset);
+ ::SetRect(&sideRect, R.left + 2, R.top, R.right - 2 + selectedOffset,
+ R.bottom);
+ ::SetRect(&bottomRect, R.right - 2, R.top, R.right, R.bottom);
+ ::SetRect(&lightRect, R.left, R.top, R.left + 3, R.top + 3);
+ ::SetRect(&shadeRect, R.left + 1, R.bottom - 2, R.left + 2, R.bottom - 1);
+ break;
+ case BF_TOP:
+ leftFlag = BF_LEFT;
+ topFlag = BF_TOP;
+ rightFlag = BF_RIGHT;
+ lightFlag = BF_DIAGONAL_ENDTOPRIGHT;
+ shadeFlag = BF_DIAGONAL_ENDBOTTOMRIGHT;
+
+ ::SetRect(&topRect, R.left + lOffset, R.top, R.right - rOffset, R.bottom);
+ ::SetRect(&sideRect, R.left, R.top + 2, R.right,
+ R.bottom - 1 + selectedOffset);
+ ::SetRect(&bottomRect, R.left, R.bottom - 1, R.right, R.bottom);
+ ::SetRect(&lightRect, R.left, R.top, R.left + 3, R.top + 3);
+ ::SetRect(&shadeRect, R.right - 2, R.top + 1, R.right - 1, R.top + 2);
+ break;
+ case BF_RIGHT:
+ leftFlag = BF_TOP;
+ topFlag = BF_RIGHT;
+ rightFlag = BF_BOTTOM;
+ lightFlag = BF_DIAGONAL_ENDTOPLEFT;
+ shadeFlag = BF_DIAGONAL_ENDBOTTOMLEFT;
+
+ ::SetRect(&topRect, R.left, R.top + lOffset, R.right, R.bottom - rOffset);
+ ::SetRect(&sideRect, R.left + 2 - selectedOffset, R.top, R.right - 2,
+ R.bottom);
+ ::SetRect(&bottomRect, R.left, R.top, R.left + 2, R.bottom);
+ ::SetRect(&lightRect, R.right - 3, R.top, R.right - 1, R.top + 2);
+ ::SetRect(&shadeRect, R.right - 2, R.bottom - 3, R.right, R.bottom - 1);
+ break;
+ case BF_BOTTOM:
+ leftFlag = BF_LEFT;
+ topFlag = BF_BOTTOM;
+ rightFlag = BF_RIGHT;
+ lightFlag = BF_DIAGONAL_ENDTOPLEFT;
+ shadeFlag = BF_DIAGONAL_ENDBOTTOMLEFT;
+
+ ::SetRect(&topRect, R.left + lOffset, R.top, R.right - rOffset, R.bottom);
+ ::SetRect(&sideRect, R.left, R.top + 2 - selectedOffset, R.right,
+ R.bottom - 2);
+ ::SetRect(&bottomRect, R.left, R.top, R.right, R.top + 2);
+ ::SetRect(&lightRect, R.left, R.bottom - 3, R.left + 2, R.bottom - 1);
+ ::SetRect(&shadeRect, R.right - 2, R.bottom - 3, R.right, R.bottom - 1);
+ break;
+ default:
+ MOZ_CRASH();
+ }
+
+ // Background
+ ::FillRect(hdc, &R, (HBRUSH)(COLOR_3DFACE + 1));
+
+ // Tab "Top"
+ ::DrawEdge(hdc, &topRect, EDGE_RAISED, BF_SOFT | topFlag);
+
+ // Tab "Bottom"
+ if (!aSelected) ::DrawEdge(hdc, &bottomRect, EDGE_RAISED, BF_SOFT | topFlag);
+
+ // Tab "Sides"
+ if (!aDrawLeft) leftFlag = 0;
+ if (!aDrawRight) rightFlag = 0;
+ ::DrawEdge(hdc, &sideRect, EDGE_RAISED, BF_SOFT | leftFlag | rightFlag);
+
+ // Tab Diagonal Corners
+ if (aDrawLeft) ::DrawEdge(hdc, &lightRect, EDGE_RAISED, BF_SOFT | lightFlag);
+
+ if (aDrawRight) ::DrawEdge(hdc, &shadeRect, EDGE_RAISED, BF_SOFT | shadeFlag);
+}
+
+void nsNativeThemeWin::DrawCheckedRect(HDC hdc, const RECT& rc, int32_t fore,
+ int32_t back, HBRUSH defaultBack) {
+ static WORD patBits[8] = {0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55};
+
+ HBITMAP patBmp = ::CreateBitmap(8, 8, 1, 1, patBits);
+ if (patBmp) {
+ HBRUSH brush = (HBRUSH)::CreatePatternBrush(patBmp);
+ if (brush) {
+ COLORREF oldForeColor = ::SetTextColor(hdc, ::GetSysColor(fore));
+ COLORREF oldBackColor = ::SetBkColor(hdc, ::GetSysColor(back));
+ POINT vpOrg;
+
+ ::UnrealizeObject(brush);
+ ::GetViewportOrgEx(hdc, &vpOrg);
+ ::SetBrushOrgEx(hdc, vpOrg.x + rc.left, vpOrg.y + rc.top, nullptr);
+ HBRUSH oldBrush = (HBRUSH)::SelectObject(hdc, brush);
+ ::FillRect(hdc, &rc, brush);
+ ::SetTextColor(hdc, oldForeColor);
+ ::SetBkColor(hdc, oldBackColor);
+ ::SelectObject(hdc, oldBrush);
+ ::DeleteObject(brush);
+ } else
+ ::FillRect(hdc, &rc, defaultBack);
+
+ ::DeleteObject(patBmp);
+ }
+}
+
+nsresult nsNativeThemeWin::ClassicDrawWidgetBackground(
+ gfxContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance,
+ const nsRect& aRect, const nsRect& aDirtyRect) {
+ int32_t part, state;
+ bool focused;
+ nsresult rv;
+ rv = ClassicGetThemePartAndState(aFrame, aAppearance, part, state, focused);
+ if (NS_FAILED(rv)) return rv;
+
+ if (AssumeThemePartAndStateAreTransparent(part, state)) {
+ return NS_OK;
+ }
+
+ gfxFloat p2a = gfxFloat(aFrame->PresContext()->AppUnitsPerDevPixel());
+ RECT widgetRect;
+ gfxRect tr(aRect.X(), aRect.Y(), aRect.Width(), aRect.Height()),
+ dr(aDirtyRect.X(), aDirtyRect.Y(), aDirtyRect.Width(),
+ aDirtyRect.Height());
+
+ tr.Scale(1.0 / p2a);
+ dr.Scale(1.0 / p2a);
+
+ gfxWindowsNativeDrawing nativeDrawing(
+ aContext, dr, GetWidgetNativeDrawingFlags(aAppearance));
+
+RENDER_AGAIN:
+
+ HDC hdc = nativeDrawing.BeginNativeDrawing();
+ if (!hdc) return NS_ERROR_FAILURE;
+
+ nativeDrawing.TransformToNativeRect(tr, widgetRect);
+
+ rv = NS_OK;
+ switch (aAppearance) {
+ // Draw button
+ case StyleAppearance::Button: {
+ if (focused) {
+ // draw dark button focus border first
+ if (HBRUSH brush = ::GetSysColorBrush(COLOR_3DDKSHADOW)) {
+ ::FrameRect(hdc, &widgetRect, brush);
+ }
+ InflateRect(&widgetRect, -1, -1);
+ }
+ // setup DC to make DrawFrameControl draw correctly
+ int32_t oldTA = ::SetTextAlign(hdc, TA_TOP | TA_LEFT | TA_NOUPDATECP);
+ ::DrawFrameControl(hdc, &widgetRect, part, state);
+ ::SetTextAlign(hdc, oldTA);
+ break;
+ }
+ // Draw controls with 2px 3D inset border
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton: {
+ // Draw inset edge
+ ::DrawEdge(hdc, &widgetRect, EDGE_SUNKEN, BF_RECT | BF_ADJUST);
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+
+ // Fill in background
+
+ if (elementState.HasState(ElementState::DISABLED) ||
+ (aFrame->GetContent()->IsXULElement() && IsReadOnly(aFrame)))
+ ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_BTNFACE + 1));
+ else
+ ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_WINDOW + 1));
+
+ break;
+ }
+ case StyleAppearance::Treeview: {
+ // Draw inset edge
+ ::DrawEdge(hdc, &widgetRect, EDGE_SUNKEN, BF_RECT | BF_ADJUST);
+
+ // Fill in window color background
+ ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_WINDOW + 1));
+
+ break;
+ }
+ // Draw 3D face background controls
+ case StyleAppearance::ProgressBar:
+ // Draw 3D border
+ ::DrawEdge(hdc, &widgetRect, BDR_SUNKENOUTER, BF_RECT | BF_MIDDLE);
+ InflateRect(&widgetRect, -1, -1);
+ [[fallthrough]];
+ case StyleAppearance::Tabpanel: {
+ ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_BTNFACE + 1));
+ break;
+ }
+ case StyleAppearance::RangeThumb: {
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+
+ ::DrawEdge(hdc, &widgetRect, EDGE_RAISED,
+ BF_RECT | BF_SOFT | BF_MIDDLE | BF_ADJUST);
+ if (elementState.HasState(ElementState::DISABLED)) {
+ DrawCheckedRect(hdc, widgetRect, COLOR_3DFACE, COLOR_3DHILIGHT,
+ (HBRUSH)COLOR_3DHILIGHT);
+ }
+
+ break;
+ }
+ // Draw scale track background
+ case StyleAppearance::Range: {
+ const int32_t trackWidth = 4;
+ // When rounding is necessary, we round the position of the track
+ // away from the chevron of the thumb to make it look better.
+ if (IsRangeHorizontal(aFrame)) {
+ widgetRect.top += (widgetRect.bottom - widgetRect.top - trackWidth) / 2;
+ widgetRect.bottom = widgetRect.top + trackWidth;
+ } else {
+ if (!IsFrameRTL(aFrame)) {
+ widgetRect.left +=
+ (widgetRect.right - widgetRect.left - trackWidth) / 2;
+ widgetRect.right = widgetRect.left + trackWidth;
+ } else {
+ widgetRect.right -=
+ (widgetRect.right - widgetRect.left - trackWidth) / 2;
+ widgetRect.left = widgetRect.right - trackWidth;
+ }
+ }
+
+ ::DrawEdge(hdc, &widgetRect, EDGE_SUNKEN, BF_RECT | BF_ADJUST);
+ ::FillRect(hdc, &widgetRect, (HBRUSH)GetStockObject(GRAY_BRUSH));
+
+ break;
+ }
+ case StyleAppearance::Progresschunk: {
+ nsIFrame* stateFrame = aFrame->GetParent();
+ ElementState elementState = GetContentState(stateFrame, aAppearance);
+
+ const bool indeterminate =
+ elementState.HasState(ElementState::INDETERMINATE);
+ bool vertical = IsVerticalProgress(stateFrame);
+
+ nsIContent* content = aFrame->GetContent();
+ if (!indeterminate || !content) {
+ ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_HIGHLIGHT + 1));
+ break;
+ }
+
+ RECT overlayRect = CalculateProgressOverlayRect(
+ aFrame, &widgetRect, vertical, indeterminate, true);
+
+ ::FillRect(hdc, &overlayRect, (HBRUSH)(COLOR_HIGHLIGHT + 1));
+
+ if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) {
+ NS_WARNING("unable to animate progress widget!");
+ }
+ break;
+ }
+
+ // Draw Tab
+ case StyleAppearance::Tab: {
+ DrawTab(hdc, widgetRect, IsBottomTab(aFrame) ? BF_BOTTOM : BF_TOP,
+ IsSelectedTab(aFrame), !IsRightToSelectedTab(aFrame),
+ !IsLeftToSelectedTab(aFrame));
+
+ break;
+ }
+ case StyleAppearance::Tabpanels:
+ ::DrawEdge(hdc, &widgetRect, EDGE_RAISED,
+ BF_SOFT | BF_MIDDLE | BF_LEFT | BF_RIGHT | BF_BOTTOM);
+
+ break;
+
+ default:
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+
+ nativeDrawing.EndNativeDrawing();
+
+ if (NS_FAILED(rv)) return rv;
+
+ if (nativeDrawing.ShouldRenderAgain()) goto RENDER_AGAIN;
+
+ nativeDrawing.PaintToContext();
+
+ return rv;
+}
+
+uint32_t nsNativeThemeWin::GetWidgetNativeDrawingFlags(
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ return gfxWindowsNativeDrawing::CANNOT_DRAW_TO_COLOR_ALPHA |
+ gfxWindowsNativeDrawing::CAN_AXIS_ALIGNED_SCALE |
+ gfxWindowsNativeDrawing::CANNOT_COMPLEX_TRANSFORM;
+
+ // need to check these others
+ default:
+ return gfxWindowsNativeDrawing::CANNOT_DRAW_TO_COLOR_ALPHA |
+ gfxWindowsNativeDrawing::CANNOT_AXIS_ALIGNED_SCALE |
+ gfxWindowsNativeDrawing::CANNOT_COMPLEX_TRANSFORM;
+ }
+}
+
+} // namespace mozilla::widget
+
+///////////////////////////////////////////
+// Creation Routine
+///////////////////////////////////////////
+
+already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() {
+ return do_AddRef(new nsNativeThemeWin());
+}
diff --git a/widget/windows/nsNativeThemeWin.h b/widget/windows/nsNativeThemeWin.h
new file mode 100644
index 0000000000..9937018197
--- /dev/null
+++ b/widget/windows/nsNativeThemeWin.h
@@ -0,0 +1,166 @@
+/* -*- 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/. */
+
+#ifndef nsNativeThemeWin_h
+#define nsNativeThemeWin_h
+
+#include <windows.h>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h"
+#include "Theme.h"
+#include "nsUXThemeConstants.h"
+#include "nsUXThemeData.h"
+
+namespace mozilla::widget {
+
+class nsNativeThemeWin : public Theme {
+ protected:
+ virtual ~nsNativeThemeWin();
+
+ public:
+ // Whether we draw a non-native widget.
+ //
+ // We always draw scrollbars as non-native so that all of Firefox has
+ // consistent scrollbar styles both in chrome and content (plus, the
+ // non-native scrollbars support scrollbar-width, auto-darkening...).
+ //
+ // We draw other widgets as non-native when their color-scheme is dark. In
+ // that case (`BecauseColorMismatch`) we don't call into the non-native theme
+ // for sizing information (GetWidgetPadding/Border and GetMinimumWidgetSize),
+ // to avoid subtle sizing changes. The non-native theme can basically draw at
+ // any size, so we prefer to have consistent sizing information.
+ enum class NonNative { No, Always, BecauseColorMismatch };
+ static bool IsWidgetAlwaysNonNative(nsIFrame*, StyleAppearance);
+ NonNative IsWidgetNonNative(nsIFrame*, StyleAppearance);
+
+ // The nsITheme interface.
+ NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect, const nsRect& aDirtyRect,
+ DrawOverflow) override;
+
+ bool CreateWebRenderCommandsForWidget(wr::DisplayListBuilder&,
+ wr::IpcResourceUpdateQueue&,
+ const layers::StackingContextHelper&,
+ layers::RenderRootStateManager*,
+ nsIFrame*, StyleAppearance,
+ const nsRect&) override;
+
+ [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) override;
+
+ virtual bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) override;
+
+ LayoutDeviceIntSize GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ virtual Transparency GetWidgetTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) override;
+
+ NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) override;
+
+ NS_IMETHOD ThemeChanged() override;
+
+ bool ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ bool ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) override;
+
+ bool ThemeWantsButtonInnerFocusRing() override { return true; }
+
+ bool ThemeNeedsComboboxDropmarker() override;
+
+ nsNativeThemeWin();
+
+ protected:
+ Maybe<nsUXThemeClass> GetThemeClass(StyleAppearance aAppearance);
+ HANDLE GetTheme(StyleAppearance aAppearance);
+ nsresult GetThemePartAndState(nsIFrame* aFrame, StyleAppearance aAppearance,
+ int32_t& aPart, int32_t& aState);
+ nsresult ClassicGetThemePartAndState(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ int32_t& aPart, int32_t& aState,
+ bool& aFocused);
+ nsresult ClassicDrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ const nsRect& aClipRect);
+ [[nodiscard]] LayoutDeviceIntMargin ClassicGetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance);
+ bool ClassicGetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult);
+ LayoutDeviceIntSize ClassicGetMinimumWidgetSize(nsIFrame* aFrame,
+ StyleAppearance aAppearance);
+ bool ClassicThemeSupportsWidget(nsIFrame* aFrame,
+ StyleAppearance aAppearance);
+ void DrawCheckedRect(HDC hdc, const RECT& rc, int32_t fore, int32_t back,
+ HBRUSH defaultBack);
+ uint32_t GetWidgetNativeDrawingFlags(StyleAppearance aAppearance);
+ int32_t StandardGetState(nsIFrame* aFrame, StyleAppearance aAppearance,
+ bool wantFocused);
+ bool IsMenuActive(nsIFrame* aFrame, StyleAppearance aAppearance);
+ RECT CalculateProgressOverlayRect(nsIFrame* aFrame, RECT* aWidgetRect,
+ bool aIsVertical, bool aIsIndeterminate,
+ bool aIsClassic);
+ void DrawThemedProgressMeter(nsIFrame* aFrame, StyleAppearance aAppearance,
+ HANDLE aTheme, HDC aHdc, int aPart, int aState,
+ RECT* aWidgetRect, RECT* aClipRect);
+
+ [[nodiscard]] LayoutDeviceIntMargin GetCachedWidgetBorder(
+ HANDLE aTheme, nsUXThemeClass aThemeClass, StyleAppearance aAppearance,
+ int32_t aPart, int32_t aState);
+
+ nsresult GetCachedMinimumWidgetSize(nsIFrame* aFrame, HANDLE aTheme,
+ nsUXThemeClass aThemeClass,
+ StyleAppearance aAppearance,
+ int32_t aPart, int32_t aState,
+ THEMESIZE aSizeReq,
+ LayoutDeviceIntSize* aResult);
+
+ SIZE GetCachedGutterSize(HANDLE theme);
+
+ private:
+ TimeStamp mProgressDeterminateTimeStamp;
+ TimeStamp mProgressIndeterminateTimeStamp;
+
+ // eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT is about 800 at the time of
+ // writing this, and nsIntMargin is 16 bytes wide, which makes this cache (1/8
+ // + 16) * 800 bytes, or about ~12KB. We could probably reduce this cache to
+ // 3KB by caching on the aAppearance value instead, but there would be some
+ // uncacheable values, since we derive some theme parts from other arguments.
+ uint8_t
+ mBorderCacheValid[(eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT + 7) /
+ 8];
+ LayoutDeviceIntMargin
+ mBorderCache[eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT];
+
+ // See the above not for mBorderCache and friends. However
+ // LayoutDeviceIntSize is half the size of nsIntMargin, making the
+ // cache roughly half as large. In total the caches should come to about 18KB.
+ uint8_t mMinimumWidgetSizeCacheValid
+ [(eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT + 7) / 8];
+ LayoutDeviceIntSize
+ mMinimumWidgetSizeCache[eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT];
+
+ bool mGutterSizeCacheValid;
+ SIZE mGutterSizeCache;
+};
+
+} // namespace mozilla::widget
+
+#endif
diff --git a/widget/windows/nsPrintDialogUtil.cpp b/widget/windows/nsPrintDialogUtil.cpp
new file mode 100644
index 0000000000..43f56e9706
--- /dev/null
+++ b/widget/windows/nsPrintDialogUtil.cpp
@@ -0,0 +1,360 @@
+/* -*- 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/. */
+
+/* -------------------------------------------------------------------
+To Build This:
+
+ You need to add this to the the makefile.win in mozilla/dom/base:
+
+ .\$(OBJDIR)\nsFlyOwnPrintDialog.obj \
+
+
+ And this to the makefile.win in mozilla/content/build:
+
+WIN_LIBS= \
+ winspool.lib \
+ comctl32.lib \
+ comdlg32.lib
+
+---------------------------------------------------------------------- */
+
+#include <windows.h>
+#include <tchar.h>
+
+#include <unknwn.h>
+#include <commdlg.h>
+
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Span.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsIPrintSettings.h"
+#include "nsIPrintSettingsWin.h"
+#include "nsIPrinterList.h"
+#include "nsServiceManagerUtils.h"
+
+#include "nsRect.h"
+
+#include "nsCRT.h"
+#include "prenv.h" /* for PR_GetEnv */
+
+#include <windows.h>
+#include <winspool.h>
+
+// For Localization
+
+// For NS_CopyUnicodeToNative
+#include "nsNativeCharsetUtils.h"
+
+// This is for extending the dialog
+#include <dlgs.h>
+
+#include "nsWindowsHelpers.h"
+#include "WinUtils.h"
+
+//-----------------------------------------------
+// Global Data
+//-----------------------------------------------
+
+static HWND gParentWnd = nullptr;
+
+//----------------------------------------------------------------------------------
+// Returns a Global Moveable Memory Handle to a DevMode
+// from the Printer by the name of aPrintName
+//
+// NOTE:
+// This function assumes that aPrintName has already been converted from
+// unicode
+//
+static nsReturnRef<nsHGLOBAL> CreateGlobalDevModeAndInit(
+ const nsString& aPrintName, nsIPrintSettings* aPS) {
+ nsHPRINTER hPrinter = nullptr;
+ // const cast kludge for silly Win32 api's
+ LPWSTR printName =
+ const_cast<wchar_t*>(static_cast<const wchar_t*>(aPrintName.get()));
+ BOOL status = ::OpenPrinterW(printName, &hPrinter, nullptr);
+ if (!status) {
+ return nsReturnRef<nsHGLOBAL>();
+ }
+
+ // Make sure hPrinter is closed on all paths
+ nsAutoPrinter autoPrinter(hPrinter);
+
+ // Get the buffer size
+ LONG needed = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, nullptr,
+ nullptr, 0);
+ if (needed < 0) {
+ return nsReturnRef<nsHGLOBAL>();
+ }
+
+ // Some drivers do not return the correct size for their DEVMODE, so we
+ // over-allocate to try and compensate.
+ // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5)
+ needed *= 2;
+ nsAutoDevMode newDevMode(
+ (LPDEVMODEW)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, needed));
+ if (!newDevMode) {
+ return nsReturnRef<nsHGLOBAL>();
+ }
+
+ nsHGLOBAL hDevMode = ::GlobalAlloc(GHND, needed);
+ nsAutoGlobalMem globalDevMode(hDevMode);
+ if (!hDevMode) {
+ return nsReturnRef<nsHGLOBAL>();
+ }
+
+ LONG ret = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, newDevMode,
+ nullptr, DM_OUT_BUFFER);
+ if (ret != IDOK) {
+ return nsReturnRef<nsHGLOBAL>();
+ }
+
+ // Lock memory and copy contents from DEVMODE (current printer)
+ // to Global Memory DEVMODE
+ LPDEVMODEW devMode = (DEVMODEW*)::GlobalLock(hDevMode);
+ if (!devMode) {
+ return nsReturnRef<nsHGLOBAL>();
+ }
+
+ memcpy(devMode, newDevMode.get(), needed);
+ // Initialize values from the PrintSettings
+ nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aPS);
+ MOZ_ASSERT(psWin);
+ psWin->CopyToNative(devMode);
+
+ // Sets back the changes we made to the DevMode into the Printer Driver
+ ret = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, devMode, devMode,
+ DM_IN_BUFFER | DM_OUT_BUFFER);
+ if (ret != IDOK) {
+ ::GlobalUnlock(hDevMode);
+ return nsReturnRef<nsHGLOBAL>();
+ }
+
+ ::GlobalUnlock(hDevMode);
+
+ return globalDevMode.out();
+}
+
+//------------------------------------------------------------------
+// helper
+static void GetDefaultPrinterNameFromGlobalPrinters(nsAString& aPrinterName) {
+ aPrinterName.Truncate();
+ nsCOMPtr<nsIPrinterList> printerList =
+ do_GetService("@mozilla.org/gfx/printerlist;1");
+ if (printerList) {
+ printerList->GetSystemDefaultPrinterName(aPrinterName);
+ }
+}
+
+//------------------------------------------------------------------
+// Displays the native Print Dialog
+nsresult NativeShowPrintDialog(HWND aHWnd, bool aHaveSelection,
+ nsIPrintSettings* aPrintSettings) {
+ // NS_ENSURE_ARG_POINTER(aHWnd);
+ NS_ENSURE_ARG_POINTER(aPrintSettings);
+
+ // Get the Print Name to be used
+ nsString printerName;
+ aPrintSettings->GetPrinterName(printerName);
+
+ // If there is no name then use the default printer
+ if (printerName.IsEmpty()) {
+ GetDefaultPrinterNameFromGlobalPrinters(printerName);
+ } else {
+ HANDLE hPrinter = nullptr;
+ if (!::OpenPrinterW(const_cast<wchar_t*>(
+ static_cast<const wchar_t*>(printerName.get())),
+ &hPrinter, nullptr)) {
+ // If the last used printer is not found, we should use default printer.
+ GetDefaultPrinterNameFromGlobalPrinters(printerName);
+ } else {
+ ::ClosePrinter(hPrinter);
+ }
+ }
+
+ // Now create a DEVNAMES struct so the the dialog is initialized correctly.
+
+ uint32_t len = printerName.Length();
+ nsHGLOBAL hDevNames =
+ ::GlobalAlloc(GHND, sizeof(wchar_t) * (len + 1) + sizeof(DEVNAMES));
+ nsAutoGlobalMem autoDevNames(hDevNames);
+ if (!hDevNames) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ DEVNAMES* pDevNames = (DEVNAMES*)::GlobalLock(hDevNames);
+ if (!pDevNames) {
+ return NS_ERROR_FAILURE;
+ }
+ pDevNames->wDriverOffset = sizeof(DEVNAMES) / sizeof(wchar_t);
+ pDevNames->wDeviceOffset = sizeof(DEVNAMES) / sizeof(wchar_t);
+ pDevNames->wOutputOffset = sizeof(DEVNAMES) / sizeof(wchar_t) + len;
+ pDevNames->wDefault = 0;
+
+ memcpy(pDevNames + 1, printerName.get(), (len + 1) * sizeof(wchar_t));
+ ::GlobalUnlock(hDevNames);
+
+ // Create a Moveable Memory Object that holds a new DevMode
+ // from the Printer Name
+ // The PRINTDLG.hDevMode requires that it be a moveable memory object
+ // NOTE: autoDevMode is automatically freed when any error occurred
+ nsAutoGlobalMem autoDevMode(
+ CreateGlobalDevModeAndInit(printerName, aPrintSettings));
+
+ // Prepare to Display the Print Dialog
+ // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms646942(v=vs.85)
+ // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-printdlgexw
+ PRINTDLGEXW prntdlg;
+ memset(&prntdlg, 0, sizeof(prntdlg));
+
+ prntdlg.lStructSize = sizeof(prntdlg);
+ prntdlg.hwndOwner = aHWnd;
+ prntdlg.hDevMode = autoDevMode.get();
+ prntdlg.hDevNames = hDevNames;
+ prntdlg.hDC = nullptr;
+ prntdlg.Flags = PD_ALLPAGES | PD_RETURNIC | PD_USEDEVMODECOPIESANDCOLLATE |
+ PD_COLLATE | PD_NOCURRENTPAGE;
+
+ // If there is a current selection then enable the "Selection" radio button
+ if (!aHaveSelection) {
+ prntdlg.Flags |= PD_NOSELECTION;
+ }
+
+ // 10 seems like a reasonable max number of ranges to support by default if
+ // the user doesn't choose a greater thing in the UI.
+ constexpr size_t kMinSupportedRanges = 10;
+
+ AutoTArray<PRINTPAGERANGE, kMinSupportedRanges> winPageRanges;
+ // Set up the page ranges.
+ {
+ AutoTArray<int32_t, kMinSupportedRanges * 2> pageRanges;
+ aPrintSettings->GetPageRanges(pageRanges);
+ // If there is a specified page range then enable the "Custom" radio button
+ if (!pageRanges.IsEmpty()) {
+ prntdlg.Flags |= PD_PAGENUMS;
+ }
+
+ const size_t specifiedRanges = pageRanges.Length() / 2;
+ const size_t maxRanges = std::max(kMinSupportedRanges, specifiedRanges);
+
+ prntdlg.nMaxPageRanges = maxRanges;
+ prntdlg.nPageRanges = specifiedRanges;
+
+ winPageRanges.SetCapacity(maxRanges);
+ for (size_t i = 0; i < pageRanges.Length(); i += 2) {
+ PRINTPAGERANGE* range = winPageRanges.AppendElement();
+ range->nFromPage = pageRanges[i];
+ range->nToPage = pageRanges[i + 1];
+ }
+ prntdlg.lpPageRanges = winPageRanges.Elements();
+
+ prntdlg.nMinPage = 1;
+ // TODO(emilio): Could probably get the right page number here from the
+ // new print UI.
+ prntdlg.nMaxPage = 0xFFFF;
+ }
+
+ // NOTE(emilio): This can always be 1 because we use the DEVMODE copies
+ // feature (see PD_USEDEVMODECOPIESANDCOLLATE).
+ prntdlg.nCopies = 1;
+
+ prntdlg.hInstance = nullptr;
+ prntdlg.lpPrintTemplateName = nullptr;
+
+ prntdlg.lpCallback = nullptr;
+ prntdlg.nPropertyPages = 0;
+ prntdlg.lphPropertyPages = nullptr;
+
+ prntdlg.nStartPage = START_PAGE_GENERAL;
+ prntdlg.dwResultAction = 0;
+
+ HRESULT result;
+ {
+ mozilla::widget::WinUtils::AutoSystemDpiAware dpiAwareness;
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ result = ::PrintDlgExW(&prntdlg);
+ }
+
+ auto cancelOnExit = mozilla::MakeScopeExit([&] { ::SetFocus(aHWnd); });
+
+ if (NS_WARN_IF(!SUCCEEDED(result))) {
+#ifdef DEBUG
+ printf_stderr("PrintDlgExW failed with %lx\n", result);
+#endif
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_WARN_IF(prntdlg.dwResultAction != PD_RESULT_PRINT)) {
+ return NS_ERROR_ABORT;
+ }
+ // check to make sure we don't have any nullptr pointers
+ NS_ENSURE_TRUE(prntdlg.hDevMode, NS_ERROR_ABORT);
+ NS_ENSURE_TRUE(prntdlg.hDevNames, NS_ERROR_ABORT);
+ // Lock the deviceNames and check for nullptr
+ DEVNAMES* devnames = (DEVNAMES*)::GlobalLock(prntdlg.hDevNames);
+ NS_ENSURE_TRUE(devnames, NS_ERROR_ABORT);
+
+ char16_t* device = &(((char16_t*)devnames)[devnames->wDeviceOffset]);
+ char16_t* driver = &(((char16_t*)devnames)[devnames->wDriverOffset]);
+
+ // Check to see if the "Print To File" control is checked
+ // then take the name from devNames and set it in the PrintSettings
+ //
+ // NOTE:
+ // As per Microsoft SDK documentation the returned value offset from
+ // devnames->wOutputOffset is either "FILE:" or nullptr
+ // if the "Print To File" checkbox is checked it MUST be "FILE:"
+ // We assert as an extra safety check.
+ if (prntdlg.Flags & PD_PRINTTOFILE) {
+ char16ptr_t fileName = &(((wchar_t*)devnames)[devnames->wOutputOffset]);
+ NS_ASSERTION(wcscmp(fileName, L"FILE:") == 0, "FileName must be `FILE:`");
+ aPrintSettings->SetOutputDestination(
+ nsIPrintSettings::kOutputDestinationFile);
+ aPrintSettings->SetToFileName(nsDependentString(fileName));
+ } else {
+ // clear "print to file" info
+ aPrintSettings->SetOutputDestination(
+ nsIPrintSettings::kOutputDestinationPrinter);
+ aPrintSettings->SetToFileName(u""_ns);
+ }
+
+ nsCOMPtr<nsIPrintSettingsWin> psWin(do_QueryInterface(aPrintSettings));
+ MOZ_RELEASE_ASSERT(psWin);
+
+ // Setup local Data members
+ psWin->SetDeviceName(nsDependentString(device));
+ psWin->SetDriverName(nsDependentString(driver));
+
+ // Fill the print options with the info from the dialog
+ aPrintSettings->SetPrinterName(nsDependentString(device));
+ aPrintSettings->SetPrintSelectionOnly(prntdlg.Flags & PD_SELECTION);
+
+ AutoTArray<int32_t, kMinSupportedRanges * 2> pageRanges;
+ if (prntdlg.Flags & PD_PAGENUMS) {
+ pageRanges.SetCapacity(prntdlg.nPageRanges * 2);
+ for (const auto& range :
+ mozilla::Span(prntdlg.lpPageRanges, prntdlg.nPageRanges)) {
+ pageRanges.AppendElement(range.nFromPage);
+ pageRanges.AppendElement(range.nToPage);
+ }
+ }
+ aPrintSettings->SetPageRanges(pageRanges);
+
+ // Unlock DeviceNames
+ ::GlobalUnlock(prntdlg.hDevNames);
+
+ // Transfer the settings from the native data to the PrintSettings
+ LPDEVMODEW devMode = (LPDEVMODEW)::GlobalLock(prntdlg.hDevMode);
+ if (!devMode || !prntdlg.hDC) {
+ return NS_ERROR_FAILURE;
+ }
+ psWin->SetDevMode(devMode); // copies DevMode
+ psWin->CopyFromNative(prntdlg.hDC, devMode);
+ ::GlobalUnlock(prntdlg.hDevMode);
+ ::DeleteDC(prntdlg.hDC);
+
+ cancelOnExit.release();
+ return NS_OK;
+}
diff --git a/widget/windows/nsPrintDialogUtil.h b/widget/windows/nsPrintDialogUtil.h
new file mode 100644
index 0000000000..3ec16e1b1d
--- /dev/null
+++ b/widget/windows/nsPrintDialogUtil.h
@@ -0,0 +1,11 @@
+/* -*- 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/. */
+#ifndef nsFlyOwnDialog_h___
+#define nsFlyOwnDialog_h___
+
+nsresult NativeShowPrintDialog(HWND aHWnd, bool aHaveSelection,
+ nsIPrintSettings* aPrintSettings);
+
+#endif /* nsFlyOwnDialog_h___ */
diff --git a/widget/windows/nsPrintDialogWin.cpp b/widget/windows/nsPrintDialogWin.cpp
new file mode 100644
index 0000000000..35ea52b17b
--- /dev/null
+++ b/widget/windows/nsPrintDialogWin.cpp
@@ -0,0 +1,147 @@
+/* -*- 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 "nsPrintDialogWin.h"
+
+#include "nsArray.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIBaseWindow.h"
+#include "nsIBrowserChild.h"
+#include "nsIDialogParamBlock.h"
+#include "nsIDocShell.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIPrintSettings.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsIWidget.h"
+#include "nsPrintDialogUtil.h"
+#include "nsIPrintSettings.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsServiceManagerUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsQueryObject.h"
+#include "WidgetUtils.h"
+#include "WinUtils.h"
+
+static const char* kPageSetupDialogURL =
+ "chrome://global/content/printPageSetup.xhtml";
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+/**
+ * ParamBlock
+ */
+
+class ParamBlock {
+ public:
+ ParamBlock() { mBlock = 0; }
+ ~ParamBlock() { NS_IF_RELEASE(mBlock); }
+ nsresult Init() {
+ return CallCreateInstance(NS_DIALOGPARAMBLOCK_CONTRACTID, &mBlock);
+ }
+ nsIDialogParamBlock* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN {
+ return mBlock;
+ }
+ operator nsIDialogParamBlock* const() { return mBlock; }
+
+ private:
+ nsIDialogParamBlock* mBlock;
+};
+
+NS_IMPL_ISUPPORTS(nsPrintDialogServiceWin, nsIPrintDialogService)
+
+nsPrintDialogServiceWin::nsPrintDialogServiceWin() {}
+
+nsPrintDialogServiceWin::~nsPrintDialogServiceWin() {}
+
+NS_IMETHODIMP
+nsPrintDialogServiceWin::Init() {
+ nsresult rv;
+ mWatcher = do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsPrintDialogServiceWin::ShowPrintDialog(mozIDOMWindowProxy* aParent,
+ bool aHaveSelection,
+ nsIPrintSettings* aSettings) {
+ NS_ENSURE_ARG(aParent);
+ RefPtr<nsIWidget> parentWidget =
+ WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(aParent));
+
+ ScopedRtlShimWindow shim(parentWidget.get());
+ NS_ASSERTION(shim.get(), "Couldn't get native window for PRint Dialog!");
+
+ return NativeShowPrintDialog(shim.get(), aHaveSelection, aSettings);
+}
+
+NS_IMETHODIMP
+nsPrintDialogServiceWin::ShowPageSetupDialog(mozIDOMWindowProxy* aParent,
+ nsIPrintSettings* aNSSettings) {
+ NS_ENSURE_ARG(aParent);
+ NS_ENSURE_ARG(aNSSettings);
+
+ ParamBlock block;
+ nsresult rv = block.Init();
+ if (NS_FAILED(rv)) return rv;
+
+ block->SetInt(0, 0);
+ rv = DoDialog(aParent, block, aNSSettings, kPageSetupDialogURL);
+
+ // if aWebBrowserPrint is not null then we are printing
+ // so we want to pass back NS_ERROR_ABORT on cancel
+ if (NS_SUCCEEDED(rv)) {
+ int32_t status;
+ block->GetInt(0, &status);
+ return status == 0 ? NS_ERROR_ABORT : NS_OK;
+ }
+
+ // We don't call nsPrintSettingsService::MaybeSavePrintSettingsToPrefs here
+ // since it's called for us in printPageSetup.js. Maybe we should move that
+ // call here for consistency with the other platforms though?
+
+ return rv;
+}
+
+nsresult nsPrintDialogServiceWin::DoDialog(mozIDOMWindowProxy* aParent,
+ nsIDialogParamBlock* aParamBlock,
+ nsIPrintSettings* aPS,
+ const char* aChromeURL) {
+ NS_ENSURE_ARG(aParamBlock);
+ NS_ENSURE_ARG(aPS);
+ NS_ENSURE_ARG(aChromeURL);
+
+ if (!mWatcher) return NS_ERROR_FAILURE;
+
+ // get a parent, if at all possible
+ // (though we'd rather this didn't fail, it's OK if it does. so there's
+ // no failure or null check.)
+ // retain ownership for method lifetime
+ nsCOMPtr<mozIDOMWindowProxy> activeParent;
+ if (!aParent) {
+ mWatcher->GetActiveWindow(getter_AddRefs(activeParent));
+ aParent = activeParent;
+ }
+
+ // create a nsIMutableArray of the parameters
+ // being passed to the window
+ nsCOMPtr<nsIMutableArray> array = nsArray::Create();
+
+ nsCOMPtr<nsISupports> psSupports(do_QueryInterface(aPS));
+ NS_ASSERTION(psSupports, "PrintSettings must be a supports");
+ array->AppendElement(psSupports);
+
+ nsCOMPtr<nsISupports> blkSupps(do_QueryInterface(aParamBlock));
+ NS_ASSERTION(blkSupps, "IOBlk must be a supports");
+ array->AppendElement(blkSupps);
+
+ nsCOMPtr<mozIDOMWindowProxy> dialog;
+ nsresult rv = mWatcher->OpenWindow(
+ aParent, nsDependentCString(aChromeURL), "_blank"_ns,
+ "centerscreen,chrome,modal,titlebar"_ns, array, getter_AddRefs(dialog));
+
+ return rv;
+}
diff --git a/widget/windows/nsPrintDialogWin.h b/widget/windows/nsPrintDialogWin.h
new file mode 100644
index 0000000000..bb8f212eb5
--- /dev/null
+++ b/widget/windows/nsPrintDialogWin.h
@@ -0,0 +1,39 @@
+/* -*- 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/. */
+
+#ifndef nsPrintDialog_h__
+#define nsPrintDialog_h__
+
+#include "nsIPrintDialogService.h"
+
+#include "nsCOMPtr.h"
+#include "nsIWindowWatcher.h"
+
+#include <windef.h>
+
+class nsIPrintSettings;
+class nsIDialogParamBlock;
+
+class nsPrintDialogServiceWin final : public nsIPrintDialogService {
+ public:
+ nsPrintDialogServiceWin();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPRINTDIALOGSERVICE
+
+ private:
+ virtual ~nsPrintDialogServiceWin();
+
+ nsresult DoDialog(mozIDOMWindowProxy* aParent,
+ nsIDialogParamBlock* aParamBlock, nsIPrintSettings* aPS,
+ const char* aChromeURL);
+
+ nsCOMPtr<nsIWindowWatcher> mWatcher;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintDialogServiceWin,
+ NS_IPRINTDIALOGSERVICE_IID)
+
+#endif
diff --git a/widget/windows/nsPrintSettingsServiceWin.cpp b/widget/windows/nsPrintSettingsServiceWin.cpp
new file mode 100644
index 0000000000..cbc99441ab
--- /dev/null
+++ b/widget/windows/nsPrintSettingsServiceWin.cpp
@@ -0,0 +1,127 @@
+/* -*- 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 "nsPrintSettingsServiceWin.h"
+
+#include "nsCOMPtr.h"
+#include "nsPrintSettingsWin.h"
+#include "nsPrintDialogUtil.h"
+
+#include "nsGfxCIID.h"
+#include "nsIServiceManager.h"
+#include "nsWindowsHelpers.h"
+#include "ipc/IPCMessageUtils.h"
+#include "chrome/common/ipc_channel.h"
+#include "mozilla/embedding/PPrintingTypes.h"
+
+using namespace mozilla::embedding;
+
+NS_IMETHODIMP
+nsPrintSettingsServiceWin::SerializeToPrintData(nsIPrintSettings* aSettings,
+ PrintData* data) {
+ nsresult rv = nsPrintSettingsService::SerializeToPrintData(aSettings, data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aSettings);
+ if (!psWin) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString deviceName;
+ nsAutoString driverName;
+
+ psWin->GetDeviceName(deviceName);
+ psWin->GetDriverName(driverName);
+
+ data->deviceName().Assign(deviceName);
+ data->driverName().Assign(driverName);
+
+ // When creating the print dialog on Windows, we only need to send certain
+ // print settings information from the parent to the child not vice versa.
+ if (XRE_IsParentProcess()) {
+ // A DEVMODE can actually be of arbitrary size. If it turns out that it'll
+ // make our IPC message larger than the limit, then we'll error out.
+ LPDEVMODEW devModeRaw;
+ psWin->GetDevMode(&devModeRaw); // This actually allocates a copy of the
+ // the nsIPrintSettingsWin DEVMODE, so
+ // we're now responsible for deallocating
+ // it. We'll use an nsAutoDevMode helper
+ // to do this.
+ if (devModeRaw) {
+ nsAutoDevMode devMode(devModeRaw);
+ devModeRaw = nullptr;
+
+ size_t devModeTotalSize = devMode->dmSize + devMode->dmDriverExtra;
+ size_t msgTotalSize = sizeof(PrintData) + devModeTotalSize;
+
+ if (msgTotalSize > IPC::Channel::kMaximumMessageSize / 2) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Instead of reaching in and manually reading each member, we'll just
+ // copy the bits over.
+ const char* devModeData = reinterpret_cast<const char*>(devMode.get());
+ nsTArray<uint8_t> arrayBuf;
+ arrayBuf.AppendElements(devModeData, devModeTotalSize);
+ data->devModeData() = std::move(arrayBuf);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrintSettingsServiceWin::DeserializeToPrintSettings(
+ const PrintData& data, nsIPrintSettings* settings) {
+ nsresult rv =
+ nsPrintSettingsService::DeserializeToPrintSettings(data, settings);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(settings);
+ if (!settings) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (XRE_IsContentProcess()) {
+ psWin->SetDeviceName(data.deviceName());
+ psWin->SetDriverName(data.driverName());
+
+ if (data.devModeData().IsEmpty()) {
+ psWin->SetDevMode(nullptr);
+ } else {
+ // Check minimum length of DEVMODE data.
+ auto devModeDataLength = data.devModeData().Length();
+ if (devModeDataLength < sizeof(DEVMODEW)) {
+ NS_WARNING("DEVMODE data is too short.");
+ return NS_ERROR_FAILURE;
+ }
+
+ DEVMODEW* devMode = reinterpret_cast<DEVMODEW*>(
+ const_cast<uint8_t*>(data.devModeData().Elements()));
+
+ // Check actual length of DEVMODE data.
+ if ((devMode->dmSize + devMode->dmDriverExtra) != devModeDataLength) {
+ NS_WARNING("DEVMODE length is incorrect.");
+ return NS_ERROR_FAILURE;
+ }
+
+ psWin->SetDevMode(devMode); // Copies
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsPrintSettingsServiceWin::_CreatePrintSettings(
+ nsIPrintSettings** _retval) {
+ *_retval = nullptr;
+ nsPrintSettingsWin* printSettings =
+ new nsPrintSettingsWin(); // does not initially ref count
+ NS_ENSURE_TRUE(printSettings, NS_ERROR_OUT_OF_MEMORY);
+
+ NS_ADDREF(*_retval = printSettings); // ref count
+
+ return NS_OK;
+}
diff --git a/widget/windows/nsPrintSettingsServiceWin.h b/widget/windows/nsPrintSettingsServiceWin.h
new file mode 100644
index 0000000000..e11d307a8b
--- /dev/null
+++ b/widget/windows/nsPrintSettingsServiceWin.h
@@ -0,0 +1,29 @@
+/* -*- Mode: IDL; tab-width: 4; 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/. */
+
+#ifndef nsPrintSettingsServiceWin_h
+#define nsPrintSettingsServiceWin_h
+
+#include "nsPrintSettingsService.h"
+
+class nsIPrintSettings;
+
+class nsPrintSettingsServiceWin final : public nsPrintSettingsService {
+ public:
+ nsPrintSettingsServiceWin() {}
+
+ NS_IMETHODIMP SerializeToPrintData(
+ nsIPrintSettings* aSettings,
+ mozilla::embedding::PrintData* data) override;
+
+ NS_IMETHODIMP DeserializeToPrintSettings(
+ const mozilla::embedding::PrintData& data,
+ nsIPrintSettings* settings) override;
+
+ nsresult _CreatePrintSettings(nsIPrintSettings** _retval) override;
+};
+
+#endif // nsPrintSettingsServiceWin_h
diff --git a/widget/windows/nsPrintSettingsWin.cpp b/widget/windows/nsPrintSettingsWin.cpp
new file mode 100644
index 0000000000..788c4ab6da
--- /dev/null
+++ b/widget/windows/nsPrintSettingsWin.cpp
@@ -0,0 +1,477 @@
+/* -*- 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 "nsPrintSettingsWin.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "nsCRT.h"
+#include "nsDeviceContextSpecWin.h"
+#include "nsPrintSettingsImpl.h"
+#include "WinUtils.h"
+
+using namespace mozilla;
+
+// Using paper sizes from wingdi.h and the units given there, plus a little
+// extra research for the ones it doesn't give. Looks like the list hasn't
+// changed since Windows 2000, so should be fairly stable now.
+const short kPaperSizeUnits[] = {
+ nsIPrintSettings::kPaperSizeMillimeters, // Not Used default to mm as
+ // DEVMODE uses tenths of mm, just
+ // in case
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTERSMALL
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_TABLOID
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LEDGER
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LEGAL
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_STATEMENT
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_EXECUTIVE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4SMALL
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B4
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_FOLIO
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_QUARTO
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_10X14
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_11X17
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_NOTE
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_9
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_10
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_11
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_12
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_14
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_CSHEET
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_DSHEET
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ESHEET
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_DL
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C5
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C3
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C4
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C6
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C65
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_B4
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_B5
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_B6
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_ITALY
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_MONARCH
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_PERSONAL
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_FANFOLD_US
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_FANFOLD_STD_GERMAN
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_FANFOLD_LGL_GERMAN
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ISO_B4
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JAPANESE_POSTCARD
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_9X11
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_10X11
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_15X11
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_INVITE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_RESERVED_48
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_RESERVED_49
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_EXTRA
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LEGAL_EXTRA
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_TABLOID_EXTRA
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_A4_EXTRA
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_TRANSVERSE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4_TRANSVERSE
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_EXTRA_TRANSVERSE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A_PLUS
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B_PLUS
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_PLUS
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4_PLUS
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5_TRANSVERSE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5_TRANSVERSE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_EXTRA
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5_EXTRA
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5_EXTRA
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A2
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_TRANSVERSE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_EXTRA_TRANSVERSE
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_DBL_JAPANESE_POSTCARD
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A6
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU2
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU3
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU3
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU4
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B4_JIS_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5_JIS_ROTATED
+ nsIPrintSettings::
+ kPaperSizeMillimeters, // DMPAPER_JAPANESE_POSTCARD_ROTATED
+ nsIPrintSettings::
+ kPaperSizeMillimeters, // DMPAPER_DBL_JAPANESE_POSTCARD_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A6_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU2_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU3_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU3_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU4_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B6_JIS
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B6_JIS_ROTATED
+ nsIPrintSettings::kPaperSizeInches, // DMPAPER_12X11
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_YOU4
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_YOU4_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P16K
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32K
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32KBIG
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_1
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_2
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_3
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_4
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_5
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_6
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_7
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_8
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_9
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_10
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P16K_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32K_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32KBIG_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_1_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_2_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_3_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_4_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_5_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_6_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_7_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_8_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_9_ROTATED
+ nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_10_ROTATED
+};
+
+NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsWin, nsPrintSettings,
+ nsIPrintSettingsWin)
+
+/** ---------------------------------------------------
+ * See documentation in nsPrintSettingsWin.h
+ * @update
+ */
+nsPrintSettingsWin::nsPrintSettingsWin()
+ : nsPrintSettings(),
+ mDeviceName(nullptr),
+ mDriverName(nullptr),
+ mDevMode(nullptr) {}
+
+/** ---------------------------------------------------
+ * See documentation in nsPrintSettingsWin.h
+ * @update
+ */
+nsPrintSettingsWin::nsPrintSettingsWin(const nsPrintSettingsWin& aPS)
+ : mDevMode(nullptr) {
+ *this = aPS;
+}
+
+/* static */
+void nsPrintSettingsWin::PaperSizeUnitFromDmPaperSize(short aPaperSize,
+ int16_t& aPaperSizeUnit) {
+ if (aPaperSize > 0 && aPaperSize < int32_t(ArrayLength(kPaperSizeUnits))) {
+ aPaperSizeUnit = kPaperSizeUnits[aPaperSize];
+ }
+}
+
+void nsPrintSettingsWin::InitWithInitializer(
+ const PrintSettingsInitializer& aSettings) {
+ nsPrintSettings::InitWithInitializer(aSettings);
+
+ if (aSettings.mDevmodeWStorage.Length() < sizeof(DEVMODEW)) {
+ return;
+ }
+
+ auto* devmode =
+ reinterpret_cast<const DEVMODEW*>(aSettings.mDevmodeWStorage.Elements());
+ if (devmode->dmSize != sizeof(DEVMODEW) ||
+ devmode->dmSize + devmode->dmDriverExtra >
+ aSettings.mDevmodeWStorage.Length()) {
+ return;
+ }
+
+ // SetDevMode copies the DEVMODE.
+ SetDevMode(const_cast<DEVMODEW*>(devmode));
+
+ if (mDevMode->dmFields & DM_SCALE) {
+ // Since we do the scaling, grab the DEVMODE value and reset it back to 100.
+ double scale = double(mDevMode->dmScale) / 100.0f;
+ if (mScaling == 1.0 || scale != 1.0) {
+ SetScaling(scale);
+ }
+ mDevMode->dmScale = 100;
+ }
+}
+
+already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings(
+ const PrintSettingsInitializer& aSettings) {
+ RefPtr<nsPrintSettings> settings = aSettings.mPrintSettings.get();
+ if (!settings) {
+ settings = MakeRefPtr<nsPrintSettingsWin>();
+ }
+ settings->InitWithInitializer(aSettings);
+ return settings.forget();
+}
+
+/** ---------------------------------------------------
+ * See documentation in nsPrintSettingsWin.h
+ * @update
+ */
+nsPrintSettingsWin::~nsPrintSettingsWin() {
+ if (mDevMode) ::HeapFree(::GetProcessHeap(), 0, mDevMode);
+}
+
+NS_IMETHODIMP nsPrintSettingsWin::SetDeviceName(const nsAString& aDeviceName) {
+ mDeviceName = aDeviceName;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettingsWin::GetDeviceName(nsAString& aDeviceName) {
+ aDeviceName = mDeviceName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettingsWin::SetDriverName(const nsAString& aDriverName) {
+ mDriverName = aDriverName;
+ return NS_OK;
+}
+NS_IMETHODIMP nsPrintSettingsWin::GetDriverName(nsAString& aDriverName) {
+ aDriverName = mDriverName;
+ return NS_OK;
+}
+
+void nsPrintSettingsWin::CopyDevMode(DEVMODEW* aInDevMode,
+ DEVMODEW*& aOutDevMode) {
+ aOutDevMode = nullptr;
+ size_t size = aInDevMode->dmSize + aInDevMode->dmDriverExtra;
+ aOutDevMode =
+ (LPDEVMODEW)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, size);
+ if (aOutDevMode) {
+ memcpy(aOutDevMode, aInDevMode, size);
+ }
+}
+
+NS_IMETHODIMP nsPrintSettingsWin::GetDevMode(DEVMODEW** aDevMode) {
+ NS_ENSURE_ARG_POINTER(aDevMode);
+
+ if (mDevMode) {
+ CopyDevMode(mDevMode, *aDevMode);
+ } else {
+ *aDevMode = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsPrintSettingsWin::SetDevMode(DEVMODEW* aDevMode) {
+ if (mDevMode) {
+ ::HeapFree(::GetProcessHeap(), 0, mDevMode);
+ mDevMode = nullptr;
+ }
+
+ if (aDevMode) {
+ CopyDevMode(aDevMode, mDevMode);
+ }
+ return NS_OK;
+}
+
+void nsPrintSettingsWin::InitUnwriteableMargin(HDC aHdc) {
+ mozilla::gfx::MarginDouble margin =
+ mozilla::widget::WinUtils::GetUnwriteableMarginsForDeviceInInches(aHdc);
+
+ mUnwriteableMargin.SizeTo(NS_INCHES_TO_INT_TWIPS(margin.top),
+ NS_INCHES_TO_INT_TWIPS(margin.right),
+ NS_INCHES_TO_INT_TWIPS(margin.bottom),
+ NS_INCHES_TO_INT_TWIPS(margin.left));
+}
+
+void nsPrintSettingsWin::CopyFromNative(HDC aHdc, DEVMODEW* aDevMode) {
+ MOZ_ASSERT(aHdc);
+ MOZ_ASSERT(aDevMode);
+
+ mIsInitedFromPrinter = true;
+ if (aDevMode->dmFields & DM_ORIENTATION) {
+ const bool areSheetsOfPaperPortraitMode =
+ (aDevMode->dmOrientation == DMORIENT_PORTRAIT);
+
+ // If our Windows print settings say that we're producing portrait-mode
+ // sheets of paper, then our page format must also be portrait-mode; unless
+ // we've got a pages-per-sheet value with orthogonal pages/sheets, in which
+ // case it's reversed.
+ const bool arePagesPortraitMode =
+ (areSheetsOfPaperPortraitMode != HasOrthogonalPagesPerSheet());
+
+ // Record the orientation of the pages (determined above) in mOrientation:
+ mOrientation = int32_t(arePagesPortraitMode ? kPortraitOrientation
+ : kLandscapeOrientation);
+ }
+
+ if (aDevMode->dmFields & DM_COPIES) {
+ mNumCopies = aDevMode->dmCopies;
+ }
+
+ if (aDevMode->dmFields & DM_DUPLEX) {
+ switch (aDevMode->dmDuplex) {
+ default:
+ MOZ_FALLTHROUGH_ASSERT("bad value for dmDuplex field");
+ case DMDUP_SIMPLEX:
+ mDuplex = kDuplexNone;
+ break;
+ case DMDUP_VERTICAL:
+ mDuplex = kDuplexFlipOnLongEdge;
+ break;
+ case DMDUP_HORIZONTAL:
+ mDuplex = kDuplexFlipOnShortEdge;
+ break;
+ }
+ }
+
+ // Since we do the scaling, grab their value and reset back to 100.
+ if (aDevMode->dmFields & DM_SCALE) {
+ double scale = double(aDevMode->dmScale) / 100.0f;
+ if (mScaling == 1.0 || scale != 1.0) {
+ mScaling = scale;
+ }
+ aDevMode->dmScale = 100;
+ }
+
+ if (aDevMode->dmFields & DM_PAPERSIZE) {
+ mPaperId.Truncate(0);
+ mPaperId.AppendInt(aDevMode->dmPaperSize);
+ // If it is not a paper size we know about, the unit will remain unchanged.
+ PaperSizeUnitFromDmPaperSize(aDevMode->dmPaperSize, mPaperSizeUnit);
+ }
+
+ if (aDevMode->dmFields & DM_COLOR) {
+ mPrintInColor = aDevMode->dmColor == DMCOLOR_COLOR;
+ }
+
+ InitUnwriteableMargin(aHdc);
+
+ int pixelsPerInchY = ::GetDeviceCaps(aHdc, LOGPIXELSY);
+ int physicalHeight = ::GetDeviceCaps(aHdc, PHYSICALHEIGHT);
+ double physicalHeightInch = double(physicalHeight) / pixelsPerInchY;
+ int pixelsPerInchX = ::GetDeviceCaps(aHdc, LOGPIXELSX);
+ int physicalWidth = ::GetDeviceCaps(aHdc, PHYSICALWIDTH);
+ double physicalWidthInch = double(physicalWidth) / pixelsPerInchX;
+
+ // Get the paper size from the device context rather than the DEVMODE, because
+ // it is always available.
+ double paperHeightInch = mOrientation == kPortraitOrientation
+ ? physicalHeightInch
+ : physicalWidthInch;
+ mPaperHeight = mPaperSizeUnit == kPaperSizeInches
+ ? paperHeightInch
+ : paperHeightInch * MM_PER_INCH_FLOAT;
+
+ double paperWidthInch = mOrientation == kPortraitOrientation
+ ? physicalWidthInch
+ : physicalHeightInch;
+ mPaperWidth = mPaperSizeUnit == kPaperSizeInches
+ ? paperWidthInch
+ : paperWidthInch * MM_PER_INCH_FLOAT;
+
+ // Using LOGPIXELSY to match existing code for print scaling calculations.
+ mResolution = pixelsPerInchY;
+}
+
+void nsPrintSettingsWin::CopyToNative(DEVMODEW* aDevMode) {
+ MOZ_ASSERT(aDevMode);
+
+ if (!mPaperId.IsEmpty()) {
+ aDevMode->dmPaperSize = _wtoi((const wchar_t*)mPaperId.BeginReading());
+ aDevMode->dmFields |= DM_PAPERSIZE;
+ } else {
+ aDevMode->dmPaperSize = 0;
+ aDevMode->dmFields &= ~DM_PAPERSIZE;
+ }
+
+ aDevMode->dmFields |= DM_COLOR;
+ aDevMode->dmColor = mPrintInColor ? DMCOLOR_COLOR : DMCOLOR_MONOCHROME;
+
+ // The length and width in DEVMODE are always in tenths of a millimeter.
+ double tenthsOfAmmPerSizeUnit =
+ mPaperSizeUnit == kPaperSizeInches ? MM_PER_INCH_FLOAT * 10.0 : 10.0;
+
+ // Note: small page sizes can be required here for sticker, label and slide
+ // printers etc. see bug 1271900.
+ if (mPaperHeight > 0) {
+ aDevMode->dmPaperLength = std::round(mPaperHeight * tenthsOfAmmPerSizeUnit);
+ aDevMode->dmFields |= DM_PAPERLENGTH;
+ } else {
+ aDevMode->dmPaperLength = 0;
+ aDevMode->dmFields &= ~DM_PAPERLENGTH;
+ }
+
+ if (mPaperWidth > 0) {
+ aDevMode->dmPaperWidth = std::round(mPaperWidth * tenthsOfAmmPerSizeUnit);
+ aDevMode->dmFields |= DM_PAPERWIDTH;
+ } else {
+ aDevMode->dmPaperWidth = 0;
+ aDevMode->dmFields &= ~DM_PAPERWIDTH;
+ }
+
+ // Setup Orientation
+ aDevMode->dmOrientation = GetSheetOrientation() == kPortraitOrientation
+ ? DMORIENT_PORTRAIT
+ : DMORIENT_LANDSCAPE;
+ aDevMode->dmFields |= DM_ORIENTATION;
+
+ // Setup Number of Copies
+ aDevMode->dmCopies = mNumCopies;
+ aDevMode->dmFields |= DM_COPIES;
+
+ // Setup Simplex/Duplex mode
+ switch (mDuplex) {
+ case kDuplexNone:
+ aDevMode->dmDuplex = DMDUP_SIMPLEX;
+ aDevMode->dmFields |= DM_DUPLEX;
+ break;
+ case kDuplexFlipOnLongEdge:
+ aDevMode->dmDuplex = DMDUP_VERTICAL;
+ aDevMode->dmFields |= DM_DUPLEX;
+ break;
+ case kDuplexFlipOnShortEdge:
+ aDevMode->dmDuplex = DMDUP_HORIZONTAL;
+ aDevMode->dmFields |= DM_DUPLEX;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("bad value for duplex option");
+ break;
+ }
+}
+
+//-------------------------------------------
+nsresult nsPrintSettingsWin::_Clone(nsIPrintSettings** _retval) {
+ RefPtr<nsPrintSettingsWin> printSettings = new nsPrintSettingsWin(*this);
+ printSettings.forget(_retval);
+ return NS_OK;
+}
+
+//-------------------------------------------
+nsPrintSettingsWin& nsPrintSettingsWin::operator=(
+ const nsPrintSettingsWin& rhs) {
+ if (this == &rhs) {
+ return *this;
+ }
+
+ ((nsPrintSettings&)*this) = rhs;
+
+ // Use free because we used the native malloc to create the memory
+ if (mDevMode) {
+ ::HeapFree(::GetProcessHeap(), 0, mDevMode);
+ }
+
+ mDeviceName = rhs.mDeviceName;
+ mDriverName = rhs.mDriverName;
+
+ if (rhs.mDevMode) {
+ CopyDevMode(rhs.mDevMode, mDevMode);
+ } else {
+ mDevMode = nullptr;
+ }
+
+ return *this;
+}
+
+//-------------------------------------------
+nsresult nsPrintSettingsWin::_Assign(nsIPrintSettings* aPS) {
+ nsPrintSettingsWin* psWin = static_cast<nsPrintSettingsWin*>(aPS);
+ *this = *psWin;
+ return NS_OK;
+}
diff --git a/widget/windows/nsPrintSettingsWin.h b/widget/windows/nsPrintSettingsWin.h
new file mode 100644
index 0000000000..c127efbeb6
--- /dev/null
+++ b/widget/windows/nsPrintSettingsWin.h
@@ -0,0 +1,62 @@
+/* -*- Mode: IDL; tab-width: 4; 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/. */
+
+#ifndef nsPrintSettingsWin_h__
+#define nsPrintSettingsWin_h__
+
+#include "nsPrintSettingsImpl.h"
+#include "nsIPrintSettingsWin.h"
+#include <windows.h>
+
+//*****************************************************************************
+//*** nsPrintSettingsWin
+//*****************************************************************************
+class nsPrintSettingsWin : public nsPrintSettings, public nsIPrintSettingsWin {
+ virtual ~nsPrintSettingsWin();
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIPRINTSETTINGSWIN
+
+ nsPrintSettingsWin();
+ nsPrintSettingsWin(const nsPrintSettingsWin& aPS);
+
+ /**
+ * @param aPaperSize the Windows dmPaperSize
+ * @param aPaperSizeUnit will be set to the nsIPrintSettings paper size unit
+ * associated with aPaperSize or left unchanged if
+ * aPaperSize is not recognized
+ */
+ static void PaperSizeUnitFromDmPaperSize(short aPaperSize,
+ int16_t& aPaperSizeUnit);
+
+ void InitWithInitializer(const PrintSettingsInitializer& aSettings) final;
+
+ /**
+ * Makes a new copy
+ */
+ virtual nsresult _Clone(nsIPrintSettings** _retval);
+
+ /**
+ * Assigns values
+ */
+ virtual nsresult _Assign(nsIPrintSettings* aPS);
+
+ /**
+ * Assignment
+ */
+ nsPrintSettingsWin& operator=(const nsPrintSettingsWin& rhs);
+
+ protected:
+ void CopyDevMode(DEVMODEW* aInDevMode, DEVMODEW*& aOutDevMode);
+ void InitUnwriteableMargin(HDC aHdc);
+
+ nsString mDeviceName;
+ nsString mDriverName;
+ LPDEVMODEW mDevMode;
+};
+
+#endif /* nsPrintSettingsWin_h__ */
diff --git a/widget/windows/nsPrinterWin.cpp b/widget/windows/nsPrinterWin.cpp
new file mode 100644
index 0000000000..75676cd613
--- /dev/null
+++ b/widget/windows/nsPrinterWin.cpp
@@ -0,0 +1,521 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsPrinterWin.h"
+
+#include <algorithm>
+#include <windows.h>
+#include <winspool.h>
+
+#include "mozilla/Array.h"
+#include "mozilla/dom/Promise.h"
+#include "nsPaper.h"
+#include "nsPrintSettingsImpl.h"
+#include "nsPrintSettingsWin.h"
+#include "nsWindowsHelpers.h"
+#include "PrintBackgroundTask.h"
+#include "WinUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+using mozilla::PrintSettingsInitializer;
+using mozilla::dom::Promise;
+
+static const double kPointsPerTenthMM = 72.0 / 254.0;
+static const double kPointsPerInch = 72.0;
+
+nsPrinterWin::nsPrinterWin(const CommonPaperInfoArray* aArray,
+ const nsAString& aName)
+ : nsPrinterBase(aArray),
+ mName(aName),
+ mDefaultDevmodeWStorage("nsPrinterWin::mDefaultDevmodeWStorage") {}
+
+// static
+already_AddRefed<nsPrinterWin> nsPrinterWin::Create(
+ const CommonPaperInfoArray* aArray, const nsAString& aName) {
+ return do_AddRef(new nsPrinterWin(aArray, aName));
+}
+
+template <class T>
+static nsTArray<T> GetDeviceCapabilityArray(const LPWSTR aPrinterName,
+ WORD aCapabilityID,
+ mozilla::Mutex& aDriverMutex,
+ int& aCount) {
+ MOZ_ASSERT(aCount >= 0, "Possibly passed aCount from previous error case.");
+
+ nsTArray<T> caps;
+
+ // We only want to access printer drivers in the parent process.
+ if (!XRE_IsParentProcess()) {
+ return caps;
+ }
+
+ // Both the call to get the size and the call to actually populate the array
+ // are relatively expensive, so as sometimes the lengths of the arrays that we
+ // retrieve depend on each other we allow a count to be passed in to save the
+ // first call. As we allocate double the count anyway this should allay any
+ // safety worries.
+ if (!aCount) {
+ // Passing nullptr as the port here seems to work. Given that we would have
+ // to OpenPrinter with just the name anyway to get the port that makes
+ // sense. Also, the printer set-up seems to stop you from having two
+ // printers with the same name. Note: this (and the call below) are blocking
+ // calls, which could be slow.
+ MutexAutoLock autoLock(aDriverMutex);
+ aCount = ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID,
+ nullptr, nullptr);
+ if (aCount <= 0) {
+ return caps;
+ }
+ }
+
+ // As DeviceCapabilitiesW doesn't take a size, there is a greater risk of the
+ // buffer being overflowed, so we over-allocate for safety.
+ caps.SetLength(aCount * 2);
+ MutexAutoLock autoLock(aDriverMutex);
+ int count =
+ ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID,
+ reinterpret_cast<LPWSTR>(caps.Elements()), nullptr);
+ if (count <= 0) {
+ caps.Clear();
+ return caps;
+ }
+
+ // We know from bug 1673708 that sometimes the final array returned is smaller
+ // than the required array count. Assert here to see if this is reproduced on
+ // test servers.
+ MOZ_ASSERT(count == aCount, "Different array count returned than expected.");
+
+ // Note that TruncateLength will crash if count > caps.Length().
+ caps.TruncateLength(count);
+ return caps;
+}
+
+static void DevmodeToSettingsInitializer(
+ const nsString& aPrinterName, const DEVMODEW* aDevmode,
+ mozilla::Mutex& aDriverMutex,
+ PrintSettingsInitializer& aSettingsInitializer) {
+ aSettingsInitializer.mPrinter.Assign(aPrinterName);
+
+ HDC dc;
+ {
+ MutexAutoLock autoLock(aDriverMutex);
+ dc = ::CreateICW(nullptr, aPrinterName.get(), nullptr, aDevmode);
+ }
+ nsAutoHDC printerDc(dc);
+ MOZ_ASSERT(printerDc, "CreateICW failed");
+ if (!printerDc) {
+ return;
+ }
+
+ if (aDevmode->dmFields & DM_PAPERSIZE) {
+ aSettingsInitializer.mPaperInfo.mId.Truncate();
+ aSettingsInitializer.mPaperInfo.mId.AppendInt(aDevmode->dmPaperSize);
+ // If it is not a paper size we know about, the unit will remain unchanged.
+ nsPrintSettingsWin::PaperSizeUnitFromDmPaperSize(
+ aDevmode->dmPaperSize, aSettingsInitializer.mPaperSizeUnit);
+ }
+
+ int pixelsPerInchY = ::GetDeviceCaps(printerDc, LOGPIXELSY);
+ int physicalHeight = ::GetDeviceCaps(printerDc, PHYSICALHEIGHT);
+ double heightInInches = double(physicalHeight) / pixelsPerInchY;
+ int pixelsPerInchX = ::GetDeviceCaps(printerDc, LOGPIXELSX);
+ int physicalWidth = ::GetDeviceCaps(printerDc, PHYSICALWIDTH);
+ double widthInches = double(physicalWidth) / pixelsPerInchX;
+ if (aDevmode->dmFields & DM_ORIENTATION &&
+ aDevmode->dmOrientation == DMORIENT_LANDSCAPE) {
+ std::swap(widthInches, heightInInches);
+ }
+ aSettingsInitializer.mPaperInfo.mSize.SizeTo(widthInches * kPointsPerInch,
+ heightInInches * kPointsPerInch);
+
+ gfx::MarginDouble margin =
+ WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc);
+ aSettingsInitializer.mPaperInfo.mUnwriteableMargin = Some(MarginDouble{
+ margin.top * kPointsPerInch, margin.right * kPointsPerInch,
+ margin.bottom * kPointsPerInch, margin.left * kPointsPerInch});
+
+ // Using Y to match existing code for print scaling calculations.
+ aSettingsInitializer.mResolution = pixelsPerInchY;
+
+ if (aDevmode->dmFields & DM_COLOR) {
+ // See comment for PrintSettingsInitializer.mPrintInColor
+ aSettingsInitializer.mPrintInColor =
+ aDevmode->dmColor != DMCOLOR_MONOCHROME;
+ }
+
+ if (aDevmode->dmFields & DM_ORIENTATION) {
+ aSettingsInitializer.mSheetOrientation =
+ int32_t(aDevmode->dmOrientation == DMORIENT_PORTRAIT
+ ? nsPrintSettings::kPortraitOrientation
+ : nsPrintSettings::kLandscapeOrientation);
+ }
+
+ if (aDevmode->dmFields & DM_COPIES) {
+ aSettingsInitializer.mNumCopies = aDevmode->dmCopies;
+ }
+
+ if (aDevmode->dmFields & DM_DUPLEX) {
+ switch (aDevmode->dmDuplex) {
+ default:
+ MOZ_FALLTHROUGH_ASSERT("bad value for dmDuplex field");
+ case DMDUP_SIMPLEX:
+ aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexNone;
+ break;
+ case DMDUP_VERTICAL:
+ aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnLongEdge;
+ break;
+ case DMDUP_HORIZONTAL:
+ aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnShortEdge;
+ break;
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsPrinterWin::GetName(nsAString& aName) {
+ aName.Assign(mName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrinterWin::GetSystemName(nsAString& aName) {
+ aName.Assign(mName);
+ return NS_OK;
+}
+
+namespace mozilla {
+template <>
+void ResolveOrReject(Promise& aPromise, nsPrinterWin& aPrinter,
+ const PrintSettingsInitializer& aResult) {
+ aPromise.MaybeResolve(
+ RefPtr<nsIPrintSettings>(CreatePlatformPrintSettings(aResult)));
+}
+} // namespace mozilla
+
+NS_IMETHODIMP nsPrinterWin::CopyFromWithValidation(
+ nsIPrintSettings* aSettingsToCopyFrom, JSContext* aCx,
+ Promise** aResultPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aResultPromise);
+
+ PrintSettingsInitializer settingsInitializer =
+ aSettingsToCopyFrom->GetSettingsInitializer();
+ return PrintBackgroundTaskPromise(
+ *this, aCx, aResultPromise, "CopyFromWithValidation"_ns,
+ &nsPrinterWin::GetValidatedSettings, settingsInitializer);
+}
+
+bool nsPrinterWin::SupportsDuplex() const {
+ MutexAutoLock autoLock(mDriverMutex);
+ return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_DUPLEX, nullptr,
+ nullptr) == 1;
+}
+
+bool nsPrinterWin::SupportsColor() const {
+ MutexAutoLock autoLock(mDriverMutex);
+ return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLORDEVICE, nullptr,
+ nullptr) == 1;
+}
+
+bool nsPrinterWin::SupportsMonochrome() const {
+ if (!SupportsColor()) {
+ return true;
+ }
+
+ nsHPRINTER hPrinter = nullptr;
+ if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) {
+ return false;
+ }
+ nsAutoPrinter autoPrinter(hPrinter);
+
+ nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
+ if (devmodeWStorage.IsEmpty()) {
+ return false;
+ }
+
+ auto* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements());
+
+ devmode->dmFields |= DM_COLOR;
+ devmode->dmColor = DMCOLOR_MONOCHROME;
+ // Try to modify the devmode settings and see if the setting sticks.
+ //
+ // This has been the only reliable way to detect it that we've found.
+ MutexAutoLock autoLock(mDriverMutex);
+ LONG ret =
+ ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), devmode,
+ devmode, DM_IN_BUFFER | DM_OUT_BUFFER);
+ if (ret != IDOK) {
+ return false;
+ }
+ return !(devmode->dmFields & DM_COLOR) ||
+ devmode->dmColor == DMCOLOR_MONOCHROME;
+}
+
+bool nsPrinterWin::SupportsCollation() const {
+ MutexAutoLock autoLock(mDriverMutex);
+ return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLLATE, nullptr,
+ nullptr) == 1;
+}
+
+nsPrinterBase::PrinterInfo nsPrinterWin::CreatePrinterInfo() const {
+ return PrinterInfo{PaperList(), DefaultSettings()};
+}
+
+mozilla::gfx::MarginDouble nsPrinterWin::GetMarginsForPaper(
+ nsString aPaperId) const {
+ gfx::MarginDouble margin;
+
+ nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
+ if (devmodeWStorage.IsEmpty()) {
+ return margin;
+ }
+
+ auto* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements());
+
+ devmode->dmFields = DM_PAPERSIZE;
+ devmode->dmPaperSize = _wtoi((const wchar_t*)aPaperId.BeginReading());
+ HDC dc;
+ {
+ MutexAutoLock autoLock(mDriverMutex);
+ dc = ::CreateICW(nullptr, mName.get(), nullptr, devmode);
+ }
+ nsAutoHDC printerDc(dc);
+ MOZ_ASSERT(printerDc, "CreateICW failed");
+ if (!printerDc) {
+ return margin;
+ }
+ margin = WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc);
+ margin.top *= kPointsPerInch;
+ margin.right *= kPointsPerInch;
+ margin.bottom *= kPointsPerInch;
+ margin.left *= kPointsPerInch;
+
+ return margin;
+}
+
+nsTArray<uint8_t> nsPrinterWin::CopyDefaultDevmodeW() const {
+ nsTArray<uint8_t> devmodeStorageW;
+
+ auto devmodeStorageWLock = mDefaultDevmodeWStorage.Lock();
+ if (devmodeStorageWLock->IsEmpty()) {
+ nsHPRINTER hPrinter = nullptr;
+ // OpenPrinter could fail if, for example, the printer has been removed
+ // or otherwise become inaccessible since it was selected.
+ if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) {
+ return devmodeStorageW;
+ }
+ nsAutoPrinter autoPrinter(hPrinter);
+ // Allocate devmode storage of the correct size.
+ MutexAutoLock autoLock(mDriverMutex);
+ LONG bytesNeeded = ::DocumentPropertiesW(nullptr, autoPrinter.get(),
+ mName.get(), nullptr, nullptr, 0);
+ // Note that we must cast the sizeof() to a signed type so that comparison
+ // with the signed, potentially-negative bytesNeeded will work!
+ MOZ_ASSERT(bytesNeeded >= LONG(sizeof(DEVMODEW)),
+ "DocumentPropertiesW failed to get valid size");
+ if (bytesNeeded < LONG(sizeof(DEVMODEW))) {
+ return devmodeStorageW;
+ }
+
+ // Allocate extra space in case of bad drivers that return a too-small
+ // result from DocumentProperties.
+ // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5)
+ if (!devmodeStorageWLock->SetLength(bytesNeeded * 2, fallible)) {
+ return devmodeStorageW;
+ }
+
+ memset(devmodeStorageWLock->Elements(), 0, devmodeStorageWLock->Length());
+ auto* devmode =
+ reinterpret_cast<DEVMODEW*>(devmodeStorageWLock->Elements());
+ LONG ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(),
+ devmode, nullptr, DM_OUT_BUFFER);
+ MOZ_ASSERT(ret == IDOK, "DocumentPropertiesW failed");
+ // Make sure that the lengths in the DEVMODEW make sense.
+ if (ret != IDOK || devmode->dmSize != sizeof(DEVMODEW) ||
+ devmode->dmSize + devmode->dmDriverExtra >
+ devmodeStorageWLock->Length()) {
+ // Clear mDefaultDevmodeWStorage to make sure we try again next time.
+ devmodeStorageWLock->Clear();
+ return devmodeStorageW;
+ }
+ }
+
+ devmodeStorageW.Assign(devmodeStorageWLock.ref());
+ return devmodeStorageW;
+}
+
+nsTArray<mozilla::PaperInfo> nsPrinterWin::PaperList() const {
+ // Paper IDs are returned as WORDs.
+ int requiredArrayCount = 0;
+ auto paperIds = GetDeviceCapabilityArray<WORD>(
+ mName.get(), DC_PAPERS, mDriverMutex, requiredArrayCount);
+ if (!paperIds.Length()) {
+ return {};
+ }
+
+ // Paper names are returned in 64 long character buffers.
+ auto paperNames = GetDeviceCapabilityArray<Array<wchar_t, 64>>(
+ mName.get(), DC_PAPERNAMES, mDriverMutex, requiredArrayCount);
+ // Check that we have the same number of names as IDs.
+ if (paperNames.Length() != paperIds.Length()) {
+ return {};
+ }
+
+ // Paper sizes are returned as POINT structs with a tenth of a millimeter as
+ // the unit.
+ auto paperSizes = GetDeviceCapabilityArray<POINT>(
+ mName.get(), DC_PAPERSIZE, mDriverMutex, requiredArrayCount);
+ // Check that we have the same number of sizes as IDs.
+ if (paperSizes.Length() != paperIds.Length()) {
+ return {};
+ }
+
+ nsTArray<mozilla::PaperInfo> paperList;
+ paperList.SetCapacity(paperNames.Length());
+ for (size_t i = 0; i < paperNames.Length(); ++i) {
+ // Paper names are null terminated unless they are 64 characters long.
+ auto firstNull =
+ std::find(paperNames[i].cbegin(), paperNames[i].cend(), L'\0');
+ auto nameLength = firstNull - paperNames[i].cbegin();
+ double width = paperSizes[i].x * kPointsPerTenthMM;
+ double height = paperSizes[i].y * kPointsPerTenthMM;
+
+ // Skip if no name or invalid size.
+ if (!nameLength || width <= 0 || height <= 0) {
+ continue;
+ }
+
+ // Windows paper IDs are 16-bit integers; we stringify them to store in the
+ // PaperInfo.mId field.
+ nsString paperIdString;
+ paperIdString.AppendInt(paperIds[i]);
+
+ // We don't resolve the margins eagerly because they're really expensive (on
+ // the order of seconds for some drivers).
+ nsDependentSubstring name(paperNames[i].cbegin(), nameLength);
+ paperList.AppendElement(mozilla::PaperInfo(paperIdString, nsString(name),
+ {width, height}, Nothing()));
+ }
+
+ return paperList;
+}
+
+PrintSettingsInitializer nsPrinterWin::DefaultSettings() const {
+ nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
+ if (devmodeWStorage.IsEmpty()) {
+ return {};
+ }
+
+ const auto* devmode =
+ reinterpret_cast<const DEVMODEW*>(devmodeWStorage.Elements());
+
+ PrintSettingsInitializer settingsInitializer;
+ DevmodeToSettingsInitializer(mName, devmode, mDriverMutex,
+ settingsInitializer);
+ settingsInitializer.mDevmodeWStorage = std::move(devmodeWStorage);
+ return settingsInitializer;
+}
+
+PrintSettingsInitializer nsPrinterWin::GetValidatedSettings(
+ PrintSettingsInitializer aSettingsToValidate) const {
+ // This function validates the settings by relying on the printer driver
+ // rejecting any invalid settings and resetting them to valid values.
+
+ // Create a copy of the default DEVMODE for this printer.
+ nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW();
+ if (devmodeWStorage.IsEmpty()) {
+ return aSettingsToValidate;
+ }
+
+ nsHPRINTER hPrinter = nullptr;
+ if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) {
+ return aSettingsToValidate;
+ }
+ nsAutoPrinter autoPrinter(hPrinter);
+
+ // Copy the settings from aSettingsToValidate into our DEVMODE.
+ DEVMODEW* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements());
+ if (!aSettingsToValidate.mPaperInfo.mId.IsEmpty()) {
+ devmode->dmPaperSize = _wtoi(
+ (const wchar_t*)aSettingsToValidate.mPaperInfo.mId.BeginReading());
+ devmode->dmFields |= DM_PAPERSIZE;
+ } else {
+ devmode->dmPaperSize = 0;
+ devmode->dmFields &= ~DM_PAPERSIZE;
+ }
+
+ devmode->dmFields |= DM_COLOR;
+ devmode->dmColor =
+ aSettingsToValidate.mPrintInColor ? DMCOLOR_COLOR : DMCOLOR_MONOCHROME;
+
+ // Note: small page sizes can be required here for sticker, label and slide
+ // printers etc. see bug 1271900.
+ if (aSettingsToValidate.mPaperInfo.mSize.height > 0) {
+ devmode->dmPaperLength = std::round(
+ aSettingsToValidate.mPaperInfo.mSize.height / kPointsPerTenthMM);
+ devmode->dmFields |= DM_PAPERLENGTH;
+ } else {
+ devmode->dmPaperLength = 0;
+ devmode->dmFields &= ~DM_PAPERLENGTH;
+ }
+
+ if (aSettingsToValidate.mPaperInfo.mSize.width > 0) {
+ devmode->dmPaperWidth = std::round(
+ aSettingsToValidate.mPaperInfo.mSize.width / kPointsPerTenthMM);
+ devmode->dmFields |= DM_PAPERWIDTH;
+ } else {
+ devmode->dmPaperWidth = 0;
+ devmode->dmFields &= ~DM_PAPERWIDTH;
+ }
+
+ // Setup Orientation
+ devmode->dmOrientation = aSettingsToValidate.mSheetOrientation ==
+ nsPrintSettings::kPortraitOrientation
+ ? DMORIENT_PORTRAIT
+ : DMORIENT_LANDSCAPE;
+ devmode->dmFields |= DM_ORIENTATION;
+
+ // Setup Number of Copies
+ devmode->dmCopies = aSettingsToValidate.mNumCopies;
+ devmode->dmFields |= DM_COPIES;
+
+ // Setup Simplex/Duplex mode
+ devmode->dmFields |= DM_DUPLEX;
+ switch (aSettingsToValidate.mDuplex) {
+ case nsPrintSettings::kDuplexNone:
+ devmode->dmDuplex = DMDUP_SIMPLEX;
+ break;
+ case nsPrintSettings::kDuplexFlipOnLongEdge:
+ devmode->dmDuplex = DMDUP_VERTICAL;
+ break;
+ case nsPrintSettings::kDuplexFlipOnShortEdge:
+ devmode->dmDuplex = DMDUP_HORIZONTAL;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("bad value for duplex option");
+ break;
+ }
+
+ // Apply the settings in the DEVMODE to the printer and retrieve the updated
+ // DEVMODE back into the same structure.
+ LONG ret;
+ {
+ MutexAutoLock autoLock(mDriverMutex);
+ ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(),
+ devmode, devmode, DM_IN_BUFFER | DM_OUT_BUFFER);
+ }
+ if (ret != IDOK) {
+ return aSettingsToValidate;
+ }
+
+ // Copy the settings back from the DEVMODE into aSettingsToValidate and
+ // return.
+ DevmodeToSettingsInitializer(mName, devmode, mDriverMutex,
+ aSettingsToValidate);
+ aSettingsToValidate.mDevmodeWStorage = std::move(devmodeWStorage);
+ return aSettingsToValidate;
+}
diff --git a/widget/windows/nsPrinterWin.h b/widget/windows/nsPrinterWin.h
new file mode 100644
index 0000000000..8afcf14d0e
--- /dev/null
+++ b/widget/windows/nsPrinterWin.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 4; 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/. */
+
+#ifndef nsPrinterWin_h_
+#define nsPrinterWin_h_
+
+#include "nsPrinterBase.h"
+#include "mozilla/DataMutex.h"
+#include "nsTArrayForwardDeclare.h"
+
+class nsPrinterWin final : public nsPrinterBase {
+ public:
+ NS_IMETHOD GetName(nsAString& aName) override;
+ NS_IMETHOD GetSystemName(nsAString& aName) override;
+ NS_IMETHOD CopyFromWithValidation(nsIPrintSettings*, JSContext*,
+ Promise**) final;
+ bool SupportsDuplex() const final;
+ bool SupportsColor() const final;
+ bool SupportsMonochrome() const final;
+ bool SupportsCollation() const final;
+ PrinterInfo CreatePrinterInfo() const final;
+ MarginDouble GetMarginsForPaper(nsString aPaperId) const final;
+
+ nsPrinterWin() = delete;
+ static already_AddRefed<nsPrinterWin> Create(
+ const mozilla::CommonPaperInfoArray* aPaperInfoArray,
+ const nsAString& aName);
+
+ private:
+ nsPrinterWin(const mozilla::CommonPaperInfoArray* aPaperInfoArray,
+ const nsAString& aName);
+ ~nsPrinterWin() = default;
+
+ PrintSettingsInitializer GetValidatedSettings(
+ PrintSettingsInitializer aSettingsToValidate) const;
+
+ nsTArray<uint8_t> CopyDefaultDevmodeW() const;
+ nsTArray<mozilla::PaperInfo> PaperList() const;
+ PrintSettingsInitializer DefaultSettings() const;
+
+ const nsString mName;
+ mutable mozilla::DataMutex<nsTArray<uint8_t>> mDefaultDevmodeWStorage;
+ // Even though some documentation seems to suggest that you should be able to
+ // use printer drivers on separate threads if you have separate handles, we
+ // see threading issues with multiple drivers. This Mutex is used to lock
+ // around all calls to DeviceCapabilitiesW, DocumentPropertiesW and
+ // CreateICW/DCW, to hopefully prevent these issues.
+ mutable mozilla::Mutex mDriverMutex MOZ_UNANNOTATED{"nsPrinterWin::Driver"};
+};
+
+#endif // nsPrinterWin_h_
diff --git a/widget/windows/nsSharePicker.cpp b/widget/windows/nsSharePicker.cpp
new file mode 100644
index 0000000000..9cdc792718
--- /dev/null
+++ b/widget/windows/nsSharePicker.cpp
@@ -0,0 +1,81 @@
+/* -*- 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 "nsSharePicker.h"
+
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "WindowsUIUtils.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Unused.h"
+
+using mozilla::dom::Promise;
+
+///////////////////////////////////////////////////////////////////////////////
+// nsISharePicker
+
+NS_IMPL_ISUPPORTS(nsSharePicker, nsISharePicker)
+
+namespace {
+inline NS_ConvertUTF8toUTF16 NS_ConvertUTF8toUTF16_MaybeVoid(
+ const nsACString& aStr) {
+ auto str = NS_ConvertUTF8toUTF16(aStr);
+ str.SetIsVoid(aStr.IsVoid());
+ return str;
+}
+inline nsIGlobalObject* GetGlobalObject() {
+ return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+}
+} // namespace
+
+NS_IMETHODIMP
+nsSharePicker::Init(mozIDOMWindowProxy* aOpenerWindow) {
+ if (mInited) {
+ return NS_ERROR_FAILURE;
+ }
+ mOpenerWindow = aOpenerWindow;
+ mInited = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSharePicker::GetOpenerWindow(mozIDOMWindowProxy** aOpenerWindow) {
+ *aOpenerWindow = mOpenerWindow;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSharePicker::Share(const nsACString& aTitle, const nsACString& aText,
+ nsIURI* aUrl, Promise** aPromise) {
+ mozilla::ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(GetGlobalObject(), result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+ nsAutoCString urlString;
+ if (aUrl) {
+ nsresult rv = aUrl->GetSpec(urlString);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ mozilla::Unused << rv;
+ } else {
+ urlString.SetIsVoid(true);
+ }
+
+ auto mozPromise =
+ WindowsUIUtils::Share(NS_ConvertUTF8toUTF16_MaybeVoid(aTitle),
+ NS_ConvertUTF8toUTF16_MaybeVoid(aText),
+ NS_ConvertUTF8toUTF16_MaybeVoid(urlString));
+ mozPromise->Then(
+ mozilla::GetCurrentSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise]() { promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); });
+
+ promise.forget(aPromise);
+
+ return NS_OK;
+}
diff --git a/widget/windows/nsSharePicker.h b/widget/windows/nsSharePicker.h
new file mode 100644
index 0000000000..d9ba9d2a6d
--- /dev/null
+++ b/widget/windows/nsSharePicker.h
@@ -0,0 +1,29 @@
+/* -*- 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/. */
+
+#ifndef nsSharePicker_h__
+#define nsSharePicker_h__
+
+#include "nsCOMPtr.h"
+#include "nsISharePicker.h"
+#include "nsPIDOMWindow.h"
+#include "nsThreadUtils.h"
+
+class nsSharePicker : public nsISharePicker {
+ virtual ~nsSharePicker() = default;
+
+ public:
+ nsSharePicker() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHAREPICKER
+
+ private:
+ bool mInited = false;
+ mozIDOMWindowProxy* mOpenerWindow;
+};
+
+#endif // nsSharePicker_h__
diff --git a/widget/windows/nsSound.cpp b/widget/windows/nsSound.cpp
new file mode 100644
index 0000000000..1fecf09c3a
--- /dev/null
+++ b/widget/windows/nsSound.cpp
@@ -0,0 +1,331 @@
+/* -*- 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 "nscore.h"
+#include <stdio.h>
+#include "nsString.h"
+#include <windows.h>
+
+// mmsystem.h is needed to build with WIN32_LEAN_AND_MEAN
+#include <mmsystem.h>
+
+#include "HeadlessSound.h"
+#include "nsSound.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "nsIChannel.h"
+#include "nsContentUtils.h"
+#include "nsCRT.h"
+#include "nsIObserverService.h"
+
+#include "mozilla/Logging.h"
+#include "prtime.h"
+
+#include "nsNativeCharsetUtils.h"
+#include "nsThreadUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "gfxPlatform.h"
+
+using mozilla::LogLevel;
+
+#ifdef DEBUG
+static mozilla::LazyLogModule gWin32SoundLog("nsSound");
+#endif
+
+// Hackaround for bug 1644240
+// When we call PlaySound for the first time in the process, winmm.dll creates
+// a new thread and starts a message loop in winmm!mciwindow. After that,
+// every call of PlaySound communicates with that thread via Window messages.
+// It seems that Warsaw application hooks USER32!GetMessageA, and there is
+// a timing window where they free their trampoline region without reverting
+// the hook on USER32!GetMessageA, resulting in crash when winmm!mciwindow
+// receives a message because it tries to jump to a freed buffer.
+// Based on the crash reports, it happened on all versions of Windows x64, and
+// the possible condition was wslbdhm64.dll was loaded but wslbscrwh64.dll was
+// unloaded. Therefore we suppress playing a sound under such a condition.
+static bool ShouldSuppressPlaySound() {
+#if defined(_M_AMD64)
+ if (::GetModuleHandle(L"wslbdhm64.dll") &&
+ !::GetModuleHandle(L"wslbscrwh64.dll")) {
+ return true;
+ }
+#endif // defined(_M_AMD64)
+ return false;
+}
+
+class nsSoundPlayer : public mozilla::Runnable {
+ public:
+ explicit nsSoundPlayer(const nsAString& aSoundName)
+ : mozilla::Runnable("nsSoundPlayer"),
+ mSoundName(aSoundName),
+ mSoundData(nullptr) {}
+
+ nsSoundPlayer(const uint8_t* aData, size_t aSize)
+ : mozilla::Runnable("nsSoundPlayer"), mSoundName(u""_ns) {
+ MOZ_ASSERT(aSize > 0, "Size should not be zero");
+ MOZ_ASSERT(aData, "Data shoud not be null");
+
+ // We will disptach nsSoundPlayer to playerthread, so keep a data copy
+ mSoundData = new uint8_t[aSize];
+ memcpy(mSoundData, aData, aSize);
+ }
+
+ NS_DECL_NSIRUNNABLE
+
+ protected:
+ ~nsSoundPlayer();
+
+ nsString mSoundName;
+ uint8_t* mSoundData;
+};
+
+NS_IMETHODIMP
+nsSoundPlayer::Run() {
+ if (ShouldSuppressPlaySound()) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(!mSoundName.IsEmpty() || mSoundData,
+ "Sound name or sound data should be specified");
+ DWORD flags = SND_NODEFAULT | SND_ASYNC;
+
+ if (mSoundData) {
+ flags |= SND_MEMORY;
+ ::PlaySoundW(reinterpret_cast<LPCWSTR>(mSoundData), nullptr, flags);
+ } else {
+ flags |= SND_ALIAS;
+ ::PlaySoundW(mSoundName.get(), nullptr, flags);
+ }
+ return NS_OK;
+}
+
+nsSoundPlayer::~nsSoundPlayer() { delete[] mSoundData; }
+
+mozilla::StaticRefPtr<nsISound> nsSound::sInstance;
+
+/* static */
+already_AddRefed<nsISound> nsSound::GetInstance() {
+ if (!sInstance) {
+ if (gfxPlatform::IsHeadless()) {
+ sInstance = new mozilla::widget::HeadlessSound();
+ } else {
+ RefPtr<nsSound> sound = new nsSound();
+ nsresult rv = sound->CreatePlayerThread();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ sInstance = sound.forget();
+ }
+ ClearOnShutdown(&sInstance);
+ }
+
+ RefPtr<nsISound> service = sInstance;
+ return service.forget();
+}
+
+#ifndef SND_PURGE
+// Not available on Windows CE, and according to MSDN
+// doesn't do anything on recent windows either.
+# define SND_PURGE 0
+#endif
+
+NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver, nsIObserver)
+
+nsSound::nsSound() : mInited(false) {}
+
+nsSound::~nsSound() {}
+
+void nsSound::PurgeLastSound() {
+ // Halt any currently playing sound.
+ if (mSoundPlayer) {
+ if (mPlayerThread) {
+ mPlayerThread->Dispatch(
+ NS_NewRunnableFunction("nsSound::PurgeLastSound",
+ [player = std::move(mSoundPlayer)]() {
+ // Capture move mSoundPlayer to lambda then
+ // PlaySoundW(nullptr, nullptr, SND_PURGE)
+ // will be called before freeing the
+ // nsSoundPlayer.
+ if (ShouldSuppressPlaySound()) {
+ return;
+ }
+ ::PlaySoundW(nullptr, nullptr, SND_PURGE);
+ }),
+ NS_DISPATCH_NORMAL);
+ }
+ }
+}
+
+NS_IMETHODIMP nsSound::Beep() {
+ ::MessageBeep(0);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader* aLoader,
+ nsISupports* context, nsresult aStatus,
+ uint32_t dataLen, const uint8_t* data) {
+ MOZ_ASSERT(mPlayerThread, "player thread should not be null ");
+ // print a load error on bad status
+ if (NS_FAILED(aStatus)) {
+#ifdef DEBUG
+ if (aLoader) {
+ nsCOMPtr<nsIRequest> request;
+ nsCOMPtr<nsIChannel> channel;
+ aLoader->GetRequest(getter_AddRefs(request));
+ if (request) channel = do_QueryInterface(request);
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsAutoCString uriSpec;
+ uri->GetSpec(uriSpec);
+ MOZ_LOG(gWin32SoundLog, LogLevel::Info,
+ ("Failed to load %s\n", uriSpec.get()));
+ }
+ }
+ }
+#endif
+ return aStatus;
+ }
+
+ PurgeLastSound();
+
+ if (data && dataLen > 0) {
+ MOZ_ASSERT(!mSoundPlayer, "mSoundPlayer should be null");
+ mSoundPlayer = new nsSoundPlayer(data, dataLen);
+ MOZ_ASSERT(mSoundPlayer, "Could not create player");
+
+ nsresult rv = mPlayerThread->Dispatch(mSoundPlayer, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSound::Play(nsIURL* aURL) {
+ nsresult rv;
+
+#ifdef DEBUG_SOUND
+ char* url;
+ aURL->GetSpec(&url);
+ MOZ_LOG(gWin32SoundLog, LogLevel::Info, ("%s\n", url));
+#endif
+
+ nsCOMPtr<nsIStreamLoader> loader;
+ rv = NS_NewStreamLoader(
+ getter_AddRefs(loader), aURL,
+ this, // aObserver
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ return rv;
+}
+
+nsresult nsSound::CreatePlayerThread() {
+ if (mPlayerThread) {
+ return NS_OK;
+ }
+ if (NS_WARN_IF(NS_FAILED(NS_NewNamedThread("PlayEventSound",
+ getter_AddRefs(mPlayerThread))))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add an observer for shutdown event to release the thread at that time
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) {
+ return NS_ERROR_FAILURE;
+ }
+
+ observerService->AddObserver(this, "xpcom-shutdown-threads", false);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSound::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "xpcom-shutdown-threads")) {
+ PurgeLastSound();
+
+ if (mPlayerThread) {
+ mPlayerThread->Shutdown();
+ mPlayerThread = nullptr;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSound::Init() {
+ if (mInited) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mPlayerThread, "player thread should not be null ");
+ // This call halts a sound if it was still playing.
+ // We have to use the sound library for something to make sure
+ // it is initialized.
+ // If we wait until the first sound is played, there will
+ // be a time lag as the library gets loaded.
+ // This should be done in player thread otherwise it will block main thread
+ // at the first time loading sound library.
+ mPlayerThread->Dispatch(
+ NS_NewRunnableFunction("nsSound::Init",
+ []() {
+ if (ShouldSuppressPlaySound()) {
+ return;
+ }
+ ::PlaySoundW(nullptr, nullptr, SND_PURGE);
+ }),
+ NS_DISPATCH_NORMAL);
+
+ mInited = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSound::PlayEventSound(uint32_t aEventId) {
+ MOZ_ASSERT(mPlayerThread, "player thread should not be null ");
+ PurgeLastSound();
+
+ const wchar_t* sound = nullptr;
+ switch (aEventId) {
+ case EVENT_NEW_MAIL_RECEIVED:
+ sound = L"MailBeep";
+ break;
+ case EVENT_ALERT_DIALOG_OPEN:
+ sound = L"SystemExclamation";
+ break;
+ case EVENT_CONFIRM_DIALOG_OPEN:
+ sound = L"SystemQuestion";
+ break;
+ case EVENT_MENU_EXECUTE:
+ sound = L"MenuCommand";
+ break;
+ case EVENT_MENU_POPUP:
+ sound = L"MenuPopup";
+ break;
+ case EVENT_EDITOR_MAX_LEN:
+ sound = L".Default";
+ break;
+ default:
+ // Win32 plays no sounds at NS_SYSSOUND_PROMPT_DIALOG and
+ // NS_SYSSOUND_SELECT_DIALOG.
+ return NS_OK;
+ }
+ NS_ASSERTION(sound, "sound is null");
+ MOZ_ASSERT(!mSoundPlayer, "mSoundPlayer should be null");
+ mSoundPlayer = new nsSoundPlayer(nsDependentString(sound));
+ MOZ_ASSERT(mSoundPlayer, "Could not create player");
+ nsresult rv = mPlayerThread->Dispatch(mSoundPlayer, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+}
diff --git a/widget/windows/nsSound.h b/widget/windows/nsSound.h
new file mode 100644
index 0000000000..d600b0873a
--- /dev/null
+++ b/widget/windows/nsSound.h
@@ -0,0 +1,47 @@
+/* -*- 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/. */
+
+#ifndef __nsSound_h__
+#define __nsSound_h__
+
+#include "nsISound.h"
+#include "nsIObserver.h"
+#include "nsIStreamLoader.h"
+#include "nsCOMPtr.h"
+#include "mozilla/StaticPtr.h"
+
+class nsIThread;
+class nsIRunnable;
+
+class nsSound : public nsISound,
+ public nsIStreamLoaderObserver,
+ public nsIObserver
+
+{
+ public:
+ nsSound();
+ static already_AddRefed<nsISound> GetInstance();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOUND
+ NS_DECL_NSISTREAMLOADEROBSERVER
+ NS_DECL_NSIOBSERVER
+
+ private:
+ virtual ~nsSound();
+ void PurgeLastSound();
+
+ private:
+ nsresult CreatePlayerThread();
+
+ nsCOMPtr<nsIThread> mPlayerThread;
+ nsCOMPtr<nsIRunnable> mSoundPlayer;
+ bool mInited;
+
+ static mozilla::StaticRefPtr<nsISound> sInstance;
+};
+
+#endif /* __nsSound_h__ */
diff --git a/widget/windows/nsToolkit.cpp b/widget/windows/nsToolkit.cpp
new file mode 100644
index 0000000000..6eea9c958f
--- /dev/null
+++ b/widget/windows/nsToolkit.cpp
@@ -0,0 +1,69 @@
+/* -*- 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 "nsToolkit.h"
+#include "nsAppShell.h"
+#include "nsWindow.h"
+#include "nsWidgetsCID.h"
+#include "prmon.h"
+#include "prtime.h"
+#include "nsComponentManagerUtils.h"
+#include <objbase.h>
+#include "WinUtils.h"
+
+#include "nsUXThemeData.h"
+
+// unknwn.h is needed to build with WIN32_LEAN_AND_MEAN
+#include <unknwn.h>
+
+using namespace mozilla::widget;
+
+nsToolkit* nsToolkit::gToolkit = nullptr;
+HINSTANCE nsToolkit::mDllInstance = 0;
+
+//-------------------------------------------------------------------------
+//
+// constructor
+//
+//-------------------------------------------------------------------------
+nsToolkit::nsToolkit() {
+ MOZ_COUNT_CTOR(nsToolkit);
+
+#if defined(MOZ_STATIC_COMPONENT_LIBS)
+ nsToolkit::Startup(GetModuleHandle(nullptr));
+#endif
+}
+
+//-------------------------------------------------------------------------
+//
+// destructor
+//
+//-------------------------------------------------------------------------
+nsToolkit::~nsToolkit() { MOZ_COUNT_DTOR(nsToolkit); }
+
+void nsToolkit::Startup(HMODULE hModule) {
+ nsToolkit::mDllInstance = hModule;
+ WinUtils::Initialize();
+}
+
+void nsToolkit::Shutdown() {
+ delete gToolkit;
+ gToolkit = nullptr;
+}
+
+//-------------------------------------------------------------------------
+//
+// Return the nsToolkit for the current thread. If a toolkit does not
+// yet exist, then one will be created...
+//
+//-------------------------------------------------------------------------
+// static
+nsToolkit* nsToolkit::GetToolkit() {
+ if (!gToolkit) {
+ gToolkit = new nsToolkit();
+ }
+
+ return gToolkit;
+}
diff --git a/widget/windows/nsToolkit.h b/widget/windows/nsToolkit.h
new file mode 100644
index 0000000000..4be7bdb80a
--- /dev/null
+++ b/widget/windows/nsToolkit.h
@@ -0,0 +1,47 @@
+/* -*- 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/. */
+
+#ifndef nsToolkit_h__
+#define nsToolkit_h__
+
+#include "nsdefs.h"
+
+#include "nsCOMPtr.h"
+#include <windows.h>
+
+// Avoid including windowsx.h to prevent macro pollution
+#ifndef GET_X_LPARAM
+# define GET_X_LPARAM(pt) (short(LOWORD(pt)))
+#endif
+#ifndef GET_Y_LPARAM
+# define GET_Y_LPARAM(pt) (short(HIWORD(pt)))
+#endif
+
+/**
+ * Wrapper around the thread running the message pump.
+ * The toolkit abstraction is necessary because the message pump must
+ * execute within the same thread that created the widget under Win32.
+ */
+
+class nsToolkit {
+ public:
+ nsToolkit();
+
+ private:
+ ~nsToolkit();
+
+ public:
+ static nsToolkit* GetToolkit();
+
+ static HINSTANCE mDllInstance;
+
+ static void Startup(HMODULE hModule);
+ static void Shutdown();
+
+ protected:
+ static nsToolkit* gToolkit;
+};
+
+#endif // TOOLKIT_H
diff --git a/widget/windows/nsUXThemeConstants.h b/widget/windows/nsUXThemeConstants.h
new file mode 100644
index 0000000000..44ce9ab34e
--- /dev/null
+++ b/widget/windows/nsUXThemeConstants.h
@@ -0,0 +1,256 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+
+#ifndef nsUXThemeConstants_h
+#define nsUXThemeConstants_h
+
+/*
+ * The following constants are used to determine how a widget is drawn using
+ * Windows' Theme API. For more information on theme parts and states see
+ * http://msdn.microsoft.com/en-us/library/bb773210(VS.85).aspx
+ */
+
+#include <vssym32.h>
+#include <vsstyle.h>
+
+#define THEME_COLOR 204
+#define THEME_FONT 210
+
+// Generic state constants
+#define TS_NORMAL 1
+#define TS_HOVER 2
+#define TS_ACTIVE 3
+#define TS_DISABLED 4
+#define TS_FOCUSED 5
+
+// These constants are reversed for the trackbar (scale) thumb
+#define TKP_FOCUSED 4
+#define TKP_DISABLED 5
+
+// Toolbarbutton constants
+#define TB_CHECKED 5
+#define TB_HOVER_CHECKED 6
+
+// Button constants
+#define BP_BUTTON 1
+#define BP_RADIO 2
+#define BP_CHECKBOX 3
+#define BP_GROUPBOX 4
+#define BP_Count 5
+
+// Textfield constants
+/* This is the EP_EDITTEXT part */
+#define TFP_TEXTFIELD 1
+#define TFP_EDITBORDER_NOSCROLL 6
+#define TFS_READONLY 6
+
+/* These are the state constants for the EDITBORDER parts */
+#define TFS_EDITBORDER_NORMAL 1
+#define TFS_EDITBORDER_HOVER 2
+#define TFS_EDITBORDER_FOCUSED 3
+#define TFS_EDITBORDER_DISABLED 4
+
+// Treeview/listbox constants
+#define TREEVIEW_BODY 1
+
+// Scrollbar constants
+#define SP_BUTTON 1
+#define SP_THUMBHOR 2
+#define SP_THUMBVERT 3
+#define SP_TRACKSTARTHOR 4
+#define SP_TRACKENDHOR 5
+#define SP_TRACKSTARTVERT 6
+#define SP_TRACKENDVERT 7
+#define SP_GRIPPERHOR 8
+#define SP_GRIPPERVERT 9
+
+// Implicit hover state.
+// BASE + 0 = UP, + 1 = DOWN, etc.
+#define SP_BUTTON_IMPLICIT_HOVER_BASE 17
+
+// Scale constants
+#define TKP_TRACK 1
+#define TKP_TRACKVERT 2
+#define TKP_THUMB 3
+#define TKP_THUMBBOTTOM 4
+#define TKP_THUMBTOP 5
+#define TKP_THUMBVERT 6
+#define TKP_THUMBLEFT 7
+#define TKP_THUMBRIGHT 8
+
+// Track state contstants
+#define TRS_NORMAL 1
+
+// Track vertical state constants
+#define TRVS_NORMAL 1
+
+// Spin constants
+#define SPNP_UP 1
+#define SPNP_DOWN 2
+
+// Tab constants
+#define TABP_TAB 4
+#define TABP_TAB_SELECTED 5
+#define TABP_PANELS 9
+#define TABP_PANEL 10
+
+// Tooltip constants
+#define TTP_STANDARD 1
+
+// Dropdown constants
+#define CBP_DROPMARKER 1
+#define CBP_DROPBORDER 4
+/* This is actually the 'READONLY' style */
+#define CBP_DROPFRAME 5
+#define CBP_DROPMARKER_VISTA 6
+
+// Menu Constants
+#define MENU_BARBACKGROUND 7
+#define MENU_BARITEM 8
+#define MENU_POPUPBACKGROUND 9
+#define MENU_POPUPBORDERS 10
+#define MENU_POPUPCHECK 11
+#define MENU_POPUPCHECKBACKGROUND 12
+#define MENU_POPUPGUTTER 13
+#define MENU_POPUPITEM 14
+#define MENU_POPUPSEPARATOR 15
+#define MENU_POPUPSUBMENU 16
+#define MENU_SYSTEMCLOSE 17
+#define MENU_SYSTEMMAXIMIZE 18
+#define MENU_SYSTEMMINIMIZE 19
+#define MENU_SYSTEMRESTORE 20
+
+#define MB_ACTIVE 1
+#define MB_INACTIVE 2
+
+#define MS_NORMAL 1
+#define MS_SELECTED 2
+#define MS_DEMOTED 3
+
+#define MBI_NORMAL 1
+#define MBI_HOT 2
+#define MBI_PUSHED 3
+#define MBI_DISABLED 4
+#define MBI_DISABLEDHOT 5
+#define MBI_DISABLEDPUSHED 6
+
+#define MC_CHECKMARKNORMAL 1
+#define MC_CHECKMARKDISABLED 2
+#define MC_BULLETNORMAL 3
+#define MC_BULLETDISABLED 4
+
+#define MCB_DISABLED 1
+#define MCB_NORMAL 2
+#define MCB_BITMAP 3
+
+#define MPI_NORMAL 1
+#define MPI_HOT 2
+#define MPI_DISABLED 3
+#define MPI_DISABLEDHOT 4
+
+#define MSM_NORMAL 1
+#define MSM_DISABLED 2
+
+// Rebar constants
+#define RP_BAND 3
+#define RP_BACKGROUND 6
+
+// Constants only found in new (98+, 2K+, XP+, etc.) Windows.
+#ifdef DFCS_HOT
+# undef DFCS_HOT
+#endif
+#define DFCS_HOT 0x00001000
+
+#ifdef COLOR_MENUHILIGHT
+# undef COLOR_MENUHILIGHT
+#endif
+#define COLOR_MENUHILIGHT 29
+
+#ifdef SPI_GETFLATMENU
+# undef SPI_GETFLATMENU
+#endif
+#define SPI_GETFLATMENU 0x1022
+#ifndef SPI_GETMENUSHOWDELAY
+# define SPI_GETMENUSHOWDELAY 106
+#endif // SPI_GETMENUSHOWDELAY
+#ifndef SPI_GETCARETTIMEOUT
+# define SPI_GETCARETTIMEOUT 0x2022
+#endif // SPI_GETCARETTIMEOUT
+#ifndef WS_EX_LAYOUTRTL
+# define WS_EX_LAYOUTRTL 0x00400000L // Right to left mirroring
+#endif
+
+// Our extra constants for passing a little bit more info to the renderer.
+#define DFCS_RTL 0x00010000
+
+// Toolbar separator dimension which can't be gotten from Windows
+#define TB_SEPARATOR_HEIGHT 2
+
+namespace mozilla {
+namespace widget {
+namespace themeconst {
+
+// Pulled from sdk/include/vsstyle.h
+enum {
+ WP_CAPTION = 1,
+ WP_SMALLCAPTION = 2,
+ WP_MINCAPTION = 3,
+ WP_SMALLMINCAPTION = 4,
+ WP_MAXCAPTION = 5,
+ WP_SMALLMAXCAPTION = 6,
+ WP_FRAMELEFT = 7,
+ WP_FRAMERIGHT = 8,
+ WP_FRAMEBOTTOM = 9,
+ WP_SMALLFRAMELEFT = 10,
+ WP_SMALLFRAMERIGHT = 11,
+ WP_SMALLFRAMEBOTTOM = 12,
+ WP_SYSBUTTON = 13,
+ WP_MDISYSBUTTON = 14,
+ WP_MINBUTTON = 15,
+ WP_MDIMINBUTTON = 16,
+ WP_MAXBUTTON = 17,
+ WP_CLOSEBUTTON = 18,
+ WP_SMALLCLOSEBUTTON = 19,
+ WP_MDICLOSEBUTTON = 20,
+ WP_RESTOREBUTTON = 21,
+ WP_MDIRESTOREBUTTON = 22,
+ WP_HELPBUTTON = 23,
+ WP_MDIHELPBUTTON = 24,
+ WP_HORZSCROLL = 25,
+ WP_HORZTHUMB = 26,
+ WP_VERTSCROLL = 27,
+ WP_VERTTHUMB = 28,
+ WP_DIALOG = 29,
+ WP_CAPTIONSIZINGTEMPLATE = 30,
+ WP_SMALLCAPTIONSIZINGTEMPLATE = 31,
+ WP_FRAMELEFTSIZINGTEMPLATE = 32,
+ WP_SMALLFRAMELEFTSIZINGTEMPLATE = 33,
+ WP_FRAMERIGHTSIZINGTEMPLATE = 34,
+ WP_SMALLFRAMERIGHTSIZINGTEMPLATE = 35,
+ WP_FRAMEBOTTOMSIZINGTEMPLATE = 36,
+ WP_SMALLFRAMEBOTTOMSIZINGTEMPLATE = 37,
+ WP_FRAME = 38,
+ WP_Count
+};
+
+enum {
+ BS_NORMAL = 1,
+ BS_HOT = 2,
+ BS_PUSHED = 3,
+ BS_DISABLED = 4,
+ BS_INACTIVE = 5 /* undocumented, inactive caption button */
+};
+
+} // namespace themeconst
+} // namespace widget
+} // namespace mozilla
+
+// If any theme part ends up having a value higher than WP_Count, this will
+// need to change.
+#define THEME_PART_DISTINCT_VALUE_COUNT mozilla::widget::themeconst::WP_Count
+
+#endif
diff --git a/widget/windows/nsUXThemeData.cpp b/widget/windows/nsUXThemeData.cpp
new file mode 100644
index 0000000000..b3f9e6fce9
--- /dev/null
+++ b/widget/windows/nsUXThemeData.cpp
@@ -0,0 +1,98 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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 "mozilla/ArrayUtils.h"
+#include "mozilla/WindowsVersion.h"
+
+#include "nsUXThemeData.h"
+#include "nsDebug.h"
+#include "nsToolkit.h"
+#include "nsUXThemeConstants.h"
+#include "gfxWindowsPlatform.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+nsUXThemeData::ThemeHandle nsUXThemeData::sThemes[eUXNumClasses];
+
+nsUXThemeData::ThemeHandle::~ThemeHandle() { Close(); }
+
+void nsUXThemeData::ThemeHandle::OpenOnce(HWND aWindow, LPCWSTR aClassList) {
+ if (mHandle.isSome()) {
+ return;
+ }
+
+ mHandle = Some(OpenThemeData(aWindow, aClassList));
+}
+
+void nsUXThemeData::ThemeHandle::Close() {
+ if (mHandle.isNothing()) {
+ return;
+ }
+
+ if (HANDLE rawHandle = mHandle.extract()) {
+ CloseThemeData(rawHandle);
+ }
+}
+
+nsUXThemeData::ThemeHandle::operator HANDLE() {
+ return mHandle.valueOr(nullptr);
+}
+
+void nsUXThemeData::Invalidate() {
+ for (auto& theme : sThemes) {
+ theme.Close();
+ }
+}
+
+HANDLE
+nsUXThemeData::GetTheme(nsUXThemeClass cls) {
+ NS_ASSERTION(cls < eUXNumClasses, "Invalid theme class!");
+ sThemes[cls].OpenOnce(nullptr, GetClassName(cls));
+ return sThemes[cls];
+}
+
+const wchar_t* nsUXThemeData::GetClassName(nsUXThemeClass cls) {
+ switch (cls) {
+ case eUXButton:
+ return L"Button";
+ case eUXEdit:
+ return L"Edit";
+ case eUXRebar:
+ return L"Rebar";
+ case eUXToolbar:
+ return L"Toolbar";
+ case eUXProgress:
+ return L"Progress";
+ case eUXTab:
+ return L"Tab";
+ case eUXTrackbar:
+ return L"Trackbar";
+ case eUXCombobox:
+ return L"Combobox";
+ case eUXHeader:
+ return L"Header";
+ case eUXListview:
+ return L"Listview";
+ case eUXMenu:
+ return L"Menu";
+ default:
+ MOZ_ASSERT_UNREACHABLE("unknown uxtheme class");
+ return L"";
+ }
+}
+
+bool nsUXThemeData::sIsHighContrastOn = false;
+
+// static
+void nsUXThemeData::UpdateNativeThemeInfo() {
+ HIGHCONTRAST highContrastInfo;
+ highContrastInfo.cbSize = sizeof(HIGHCONTRAST);
+ sIsHighContrastOn =
+ SystemParametersInfo(SPI_GETHIGHCONTRAST, 0, &highContrastInfo, 0) &&
+ highContrastInfo.dwFlags & HCF_HIGHCONTRASTON;
+}
diff --git a/widget/windows/nsUXThemeData.h b/widget/windows/nsUXThemeData.h
new file mode 100644
index 0000000000..38be8b4484
--- /dev/null
+++ b/widget/windows/nsUXThemeData.h
@@ -0,0 +1,69 @@
+/* vim: se cin sw=2 ts=2 et : */
+/* -*- 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/. */
+#ifndef __UXThemeData_h__
+#define __UXThemeData_h__
+#include <windows.h>
+#include <uxtheme.h>
+
+#include "nscore.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/Maybe.h"
+#include "WinUtils.h"
+
+#include "nsWindowDefs.h"
+
+enum nsUXThemeClass {
+ eUXButton = 0,
+ eUXEdit,
+ eUXRebar,
+ eUXToolbar,
+ eUXProgress,
+ eUXTab,
+ eUXTrackbar,
+ eUXCombobox,
+ eUXHeader,
+ eUXListview,
+ eUXMenu,
+ eUXNumClasses
+};
+
+class nsUXThemeData {
+ // This class makes sure we don't attempt to open a theme if the previous
+ // loading attempt has failed because OpenThemeData is a heavy task and
+ // it's less likely that the API returns a different result.
+ class ThemeHandle final {
+ mozilla::Maybe<HANDLE> mHandle;
+
+ public:
+ ThemeHandle() = default;
+ ~ThemeHandle();
+
+ // Disallow copy and move
+ ThemeHandle(const ThemeHandle&) = delete;
+ ThemeHandle(ThemeHandle&&) = delete;
+ ThemeHandle& operator=(const ThemeHandle&) = delete;
+ ThemeHandle& operator=(ThemeHandle&&) = delete;
+
+ operator HANDLE();
+ void OpenOnce(HWND aWindow, LPCWSTR aClassList);
+ void Close();
+ };
+
+ static ThemeHandle sThemes[eUXNumClasses];
+ static const wchar_t* GetClassName(nsUXThemeClass);
+
+ public:
+ static bool sIsHighContrastOn;
+
+ static void Invalidate();
+ static HANDLE GetTheme(nsUXThemeClass cls);
+ static HMODULE GetThemeDLL();
+
+ static void UpdateNativeThemeInfo();
+ static bool IsHighContrastOn() { return sIsHighContrastOn; }
+};
+#endif // __UXThemeData_h__
diff --git a/widget/windows/nsUserIdleServiceWin.cpp b/widget/windows/nsUserIdleServiceWin.cpp
new file mode 100644
index 0000000000..7cf957dffd
--- /dev/null
+++ b/widget/windows/nsUserIdleServiceWin.cpp
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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 "nsUserIdleServiceWin.h"
+#include <windows.h>
+
+bool nsUserIdleServiceWin::PollIdleTime(uint32_t* aIdleTime) {
+ LASTINPUTINFO inputInfo;
+ inputInfo.cbSize = sizeof(inputInfo);
+ if (!::GetLastInputInfo(&inputInfo)) return false;
+
+ *aIdleTime =
+ SAFE_COMPARE_EVEN_WITH_WRAPPING(GetTickCount(), inputInfo.dwTime);
+
+ return true;
+}
diff --git a/widget/windows/nsUserIdleServiceWin.h b/widget/windows/nsUserIdleServiceWin.h
new file mode 100644
index 0000000000..f9a47c8df5
--- /dev/null
+++ b/widget/windows/nsUserIdleServiceWin.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef nsUserIdleServiceWin_h__
+#define nsUserIdleServiceWin_h__
+
+#include "nsUserIdleService.h"
+#include "mozilla/AppShutdown.h"
+
+/* NOTE: Compare of GetTickCount() could overflow. This corrects for
+ * overflow situations.
+ ***/
+#ifndef SAFE_COMPARE_EVEN_WITH_WRAPPING
+# define SAFE_COMPARE_EVEN_WITH_WRAPPING(A, B) \
+ (((int)((long)A - (long)B) & 0xFFFFFFFF))
+#endif
+
+class nsUserIdleServiceWin : public nsUserIdleService {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsUserIdleServiceWin, nsUserIdleService)
+
+ bool PollIdleTime(uint32_t* aIdleTime) override;
+
+ static already_AddRefed<nsUserIdleServiceWin> GetInstance() {
+ RefPtr<nsUserIdleServiceWin> idleService =
+ nsUserIdleService::GetInstance().downcast<nsUserIdleServiceWin>();
+ if (!idleService) {
+ // Avoid late instantiation or resurrection during shutdown.
+ if (mozilla::AppShutdown::IsInOrBeyond(
+ mozilla::ShutdownPhase::AppShutdownConfirmed)) {
+ return nullptr;
+ }
+ idleService = new nsUserIdleServiceWin();
+ }
+
+ return idleService.forget();
+ }
+
+ protected:
+ nsUserIdleServiceWin() {}
+ virtual ~nsUserIdleServiceWin() {}
+};
+
+#endif // nsUserIdleServiceWin_h__
diff --git a/widget/windows/nsWidgetFactory.cpp b/widget/windows/nsWidgetFactory.cpp
new file mode 100644
index 0000000000..47c7d021ed
--- /dev/null
+++ b/widget/windows/nsWidgetFactory.cpp
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsWidgetFactory.h"
+
+#include "mozilla/Components.h"
+#include "nsISupports.h"
+#include "nsdefs.h"
+#include "nsWidgetsCID.h"
+#include "nsAppShell.h"
+#include "nsAppShellSingleton.h"
+#include "mozilla/WidgetUtils.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "nsLookAndFeel.h"
+#include "WinMouseScrollHandler.h"
+#include "KeyboardLayout.h"
+#include "nsToolkit.h"
+
+// Modules that switch out based on the environment
+#include "nsXULAppAPI.h"
+// Desktop
+#include "nsFilePicker.h" // needs to be included before other shobjidl.h includes
+#include "nsColorPicker.h"
+// Content processes
+#include "nsFilePickerProxy.h"
+
+// Clipboard
+#include "nsClipboardHelper.h"
+#include "nsClipboard.h"
+#include "HeadlessClipboard.h"
+
+#include "WindowsUIUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+NS_IMPL_COMPONENT_FACTORY(nsIClipboard) {
+ nsCOMPtr<nsIClipboard> inst;
+ if (gfxPlatform::IsHeadless()) {
+ inst = new HeadlessClipboard();
+ } else {
+ inst = new nsClipboard();
+ }
+ return inst.forget().downcast<nsISupports>();
+}
+
+nsresult nsWidgetWindowsModuleCtor() { return nsAppShellInit(); }
+
+void nsWidgetWindowsModuleDtor() {
+ // Shutdown all XP level widget classes.
+ WidgetUtils::Shutdown();
+
+ KeyboardLayout::Shutdown();
+ MouseScrollHandler::Shutdown();
+ nsLookAndFeel::Shutdown();
+ nsToolkit::Shutdown();
+ nsAppShellShutdown();
+}
diff --git a/widget/windows/nsWidgetFactory.h b/widget/windows/nsWidgetFactory.h
new file mode 100644
index 0000000000..41a39220e3
--- /dev/null
+++ b/widget/windows/nsWidgetFactory.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef widget_windows_nsWidgetFactory_h
+#define widget_windows_nsWidgetFactory_h
+
+#include "nscore.h"
+#include "nsID.h"
+
+class nsISupports;
+
+nsresult nsAppShellConstructor(const nsIID& iid, void** result);
+
+nsresult nsWidgetWindowsModuleCtor();
+void nsWidgetWindowsModuleDtor();
+
+#endif // defined widget_windows_nsWidgetFactory_h
diff --git a/widget/windows/nsWinGesture.cpp b/widget/windows/nsWinGesture.cpp
new file mode 100644
index 0000000000..8fd00b3ff0
--- /dev/null
+++ b/widget/windows/nsWinGesture.cpp
@@ -0,0 +1,388 @@
+/* -*- 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/. */
+
+/*
+ * nsWinGesture - Touch input handling for tablet displays.
+ */
+
+#include "nscore.h"
+#include "nsWinGesture.h"
+#include "nsUXThemeData.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/dom/SimpleGestureEventBinding.h"
+#include "mozilla/dom/WheelEventBinding.h"
+
+#include <cmath>
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+extern mozilla::LazyLogModule gWindowsLog;
+
+static bool gEnableSingleFingerPanEvents = false;
+
+nsWinGesture::nsWinGesture()
+ : mPanActive(false),
+ mFeedbackActive(false),
+ mXAxisFeedback(false),
+ mYAxisFeedback(false),
+ mPanInertiaActive(false) {
+ (void)InitLibrary();
+ mPixelScrollOverflow = 0;
+}
+
+/* Load and shutdown */
+
+bool nsWinGesture::InitLibrary() {
+ // Check to see if we want single finger gesture input. Only do this once
+ // for the app so we don't have to look it up on every window create.
+ gEnableSingleFingerPanEvents =
+ Preferences::GetBool("gestures.enable_single_finger_input", false);
+
+ return true;
+}
+
+#define GCOUNT 5
+
+bool nsWinGesture::SetWinGestureSupport(
+ HWND hWnd, WidgetGestureNotifyEvent::PanDirection aDirection) {
+ GESTURECONFIG config[GCOUNT];
+
+ memset(&config, 0, sizeof(config));
+
+ config[0].dwID = GID_ZOOM;
+ config[0].dwWant = GC_ZOOM;
+ config[0].dwBlock = 0;
+
+ config[1].dwID = GID_ROTATE;
+ config[1].dwWant = GC_ROTATE;
+ config[1].dwBlock = 0;
+
+ config[2].dwID = GID_PAN;
+ config[2].dwWant = GC_PAN | GC_PAN_WITH_INERTIA | GC_PAN_WITH_GUTTER;
+ config[2].dwBlock = GC_PAN_WITH_SINGLE_FINGER_VERTICALLY |
+ GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY;
+
+ if (gEnableSingleFingerPanEvents) {
+ if (aDirection == WidgetGestureNotifyEvent::ePanVertical ||
+ aDirection == WidgetGestureNotifyEvent::ePanBoth) {
+ config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY;
+ config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY;
+ }
+
+ if (aDirection == WidgetGestureNotifyEvent::ePanHorizontal ||
+ aDirection == WidgetGestureNotifyEvent::ePanBoth) {
+ config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY;
+ config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY;
+ }
+ }
+
+ config[3].dwWant = GC_TWOFINGERTAP;
+ config[3].dwID = GID_TWOFINGERTAP;
+ config[3].dwBlock = 0;
+
+ config[4].dwWant = GC_PRESSANDTAP;
+ config[4].dwID = GID_PRESSANDTAP;
+ config[4].dwBlock = 0;
+
+ return SetGestureConfig(hWnd, 0, GCOUNT, (PGESTURECONFIG)&config,
+ sizeof(GESTURECONFIG));
+}
+
+/* Helpers */
+
+bool nsWinGesture::IsPanEvent(LPARAM lParam) {
+ GESTUREINFO gi;
+
+ ZeroMemory(&gi, sizeof(GESTUREINFO));
+ gi.cbSize = sizeof(GESTUREINFO);
+
+ BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi);
+ if (!result) return false;
+
+ if (gi.dwID == GID_PAN) return true;
+
+ return false;
+}
+
+/* Gesture event processing */
+
+bool nsWinGesture::ProcessGestureMessage(HWND hWnd, WPARAM wParam,
+ LPARAM lParam,
+ WidgetSimpleGestureEvent& evt) {
+ GESTUREINFO gi;
+
+ ZeroMemory(&gi, sizeof(GESTUREINFO));
+ gi.cbSize = sizeof(GESTUREINFO);
+
+ BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi);
+ if (!result) return false;
+
+ // The coordinates of this event
+ nsPointWin coord;
+ coord = gi.ptsLocation;
+ coord.ScreenToClient(hWnd);
+
+ evt.mRefPoint = LayoutDeviceIntPoint(coord.x, coord.y);
+
+ // Multiple gesture can occur at the same time so gesture state
+ // info can't be shared.
+ switch (gi.dwID) {
+ case GID_BEGIN:
+ case GID_END:
+ // These should always fall through to DefWndProc
+ return false;
+ break;
+
+ case GID_ZOOM: {
+ if (gi.dwFlags & GF_BEGIN) {
+ // Send a zoom start event
+
+ // The low 32 bits are the distance in pixels.
+ mZoomIntermediate = (float)gi.ullArguments;
+
+ evt.mMessage = eMagnifyGestureStart;
+ evt.mDelta = 0.0;
+ } else if (gi.dwFlags & GF_END) {
+ // Send a zoom end event, the delta is the change
+ // in touch points.
+ evt.mMessage = eMagnifyGesture;
+ // (positive for a "zoom in")
+ evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments);
+ mZoomIntermediate = (float)gi.ullArguments;
+ } else {
+ // Send a zoom intermediate event, the delta is the change
+ // in touch points.
+ evt.mMessage = eMagnifyGestureUpdate;
+ // (positive for a "zoom in")
+ evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments);
+ mZoomIntermediate = (float)gi.ullArguments;
+ }
+ } break;
+
+ case GID_ROTATE: {
+ // Send a rotate start event
+ double radians = 0.0;
+
+ // On GF_BEGIN, ullArguments contains the absolute rotation at the
+ // start of the gesture. In later events it contains the offset from
+ // the start angle.
+ if (gi.ullArguments != 0)
+ radians = GID_ROTATE_ANGLE_FROM_ARGUMENT(gi.ullArguments);
+
+ double degrees = -1 * radians * (180 / M_PI);
+
+ if (gi.dwFlags & GF_BEGIN) {
+ // At some point we should pass the initial angle in
+ // along with delta. It's useful.
+ degrees = mRotateIntermediate = 0.0;
+ }
+
+ evt.mDirection = 0;
+ evt.mDelta = degrees - mRotateIntermediate;
+ mRotateIntermediate = degrees;
+
+ if (evt.mDelta > 0) {
+ evt.mDirection =
+ dom::SimpleGestureEvent_Binding::ROTATION_COUNTERCLOCKWISE;
+ } else if (evt.mDelta < 0) {
+ evt.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_CLOCKWISE;
+ }
+
+ if (gi.dwFlags & GF_BEGIN) {
+ evt.mMessage = eRotateGestureStart;
+ } else if (gi.dwFlags & GF_END) {
+ evt.mMessage = eRotateGesture;
+ } else {
+ evt.mMessage = eRotateGestureUpdate;
+ }
+ } break;
+
+ case GID_TWOFINGERTAP:
+ // Normally maps to "restore" from whatever you may have recently changed.
+ // A simple double click.
+ evt.mMessage = eTapGesture;
+ evt.mClickCount = 1;
+ break;
+
+ case GID_PRESSANDTAP:
+ // Two finger right click. Defaults to right click if it falls through.
+ evt.mMessage = ePressTapGesture;
+ evt.mClickCount = 1;
+ break;
+ }
+
+ return true;
+}
+
+bool nsWinGesture::ProcessPanMessage(HWND hWnd, WPARAM wParam, LPARAM lParam) {
+ GESTUREINFO gi;
+
+ ZeroMemory(&gi, sizeof(GESTUREINFO));
+ gi.cbSize = sizeof(GESTUREINFO);
+
+ BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi);
+ if (!result) return false;
+
+ // The coordinates of this event
+ nsPointWin coord;
+ coord = mPanRefPoint = gi.ptsLocation;
+ // We want screen coordinates in our local offsets as client coordinates will
+ // change when feedback is taking place. Gui events though require client
+ // coordinates.
+ mPanRefPoint.ScreenToClient(hWnd);
+
+ switch (gi.dwID) {
+ case GID_BEGIN:
+ case GID_END:
+ // These should always fall through to DefWndProc
+ return false;
+ break;
+
+ // Setup pixel scroll events for both axis
+ case GID_PAN: {
+ if (gi.dwFlags & GF_BEGIN) {
+ mPanIntermediate = coord;
+ mPixelScrollDelta = 0;
+ mPanActive = true;
+ mPanInertiaActive = false;
+ } else {
+#ifdef DBG_jimm
+ int32_t deltaX = mPanIntermediate.x - coord.x;
+ int32_t deltaY = mPanIntermediate.y - coord.y;
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("coordX=%d coordY=%d deltaX=%d deltaY=%d x:%d y:%d\n", coord.x,
+ coord.y, deltaX, deltaY, mXAxisFeedback, mYAxisFeedback));
+#endif
+
+ mPixelScrollDelta.x = mPanIntermediate.x - coord.x;
+ mPixelScrollDelta.y = mPanIntermediate.y - coord.y;
+ mPanIntermediate = coord;
+
+ if (gi.dwFlags & GF_INERTIA) mPanInertiaActive = true;
+
+ if (gi.dwFlags & GF_END) {
+ mPanActive = false;
+ mPanInertiaActive = false;
+ PanFeedbackFinalize(hWnd, true);
+ }
+ }
+ } break;
+ }
+ return true;
+}
+
+inline bool TestTransition(int32_t a, int32_t b) {
+ // If a is zero, overflow is zero, implying the cursor has moved back to the
+ // start position. If b is zero, cached overscroll is zero, implying feedback
+ // just begun.
+ if (a == 0 || b == 0) return true;
+ // Test for different signs.
+ return (a < 0) == (b < 0);
+}
+
+void nsWinGesture::UpdatePanFeedbackX(HWND hWnd, int32_t scrollOverflow,
+ bool& endFeedback) {
+ // If scroll overflow was returned indicating we panned past the bounds of
+ // the scrollable view port, start feeback.
+ if (scrollOverflow != 0) {
+ if (!mFeedbackActive) {
+ BeginPanningFeedback(hWnd);
+ mFeedbackActive = true;
+ }
+ endFeedback = false;
+ mXAxisFeedback = true;
+ return;
+ }
+
+ if (mXAxisFeedback) {
+ int32_t newOverflow = mPixelScrollOverflow.x - mPixelScrollDelta.x;
+
+ // Detect a reverse transition past the starting drag point. This tells us
+ // the user has panned all the way back so we can stop providing feedback
+ // for this axis.
+ if (!TestTransition(newOverflow, mPixelScrollOverflow.x) ||
+ newOverflow == 0)
+ return;
+
+ // Cache the total over scroll in pixels.
+ mPixelScrollOverflow.x = newOverflow;
+ endFeedback = false;
+ }
+}
+
+void nsWinGesture::UpdatePanFeedbackY(HWND hWnd, int32_t scrollOverflow,
+ bool& endFeedback) {
+ // If scroll overflow was returned indicating we panned past the bounds of
+ // the scrollable view port, start feeback.
+ if (scrollOverflow != 0) {
+ if (!mFeedbackActive) {
+ BeginPanningFeedback(hWnd);
+ mFeedbackActive = true;
+ }
+ endFeedback = false;
+ mYAxisFeedback = true;
+ return;
+ }
+
+ if (mYAxisFeedback) {
+ int32_t newOverflow = mPixelScrollOverflow.y - mPixelScrollDelta.y;
+
+ // Detect a reverse transition past the starting drag point. This tells us
+ // the user has panned all the way back so we can stop providing feedback
+ // for this axis.
+ if (!TestTransition(newOverflow, mPixelScrollOverflow.y) ||
+ newOverflow == 0)
+ return;
+
+ // Cache the total over scroll in pixels.
+ mPixelScrollOverflow.y = newOverflow;
+ endFeedback = false;
+ }
+}
+
+void nsWinGesture::PanFeedbackFinalize(HWND hWnd, bool endFeedback) {
+ if (!mFeedbackActive) return;
+
+ if (endFeedback) {
+ mFeedbackActive = false;
+ mXAxisFeedback = false;
+ mYAxisFeedback = false;
+ mPixelScrollOverflow = 0;
+ EndPanningFeedback(hWnd, TRUE);
+ return;
+ }
+
+ UpdatePanningFeedback(hWnd, mPixelScrollOverflow.x, mPixelScrollOverflow.y,
+ mPanInertiaActive);
+}
+
+bool nsWinGesture::PanDeltaToPixelScroll(WidgetWheelEvent& aWheelEvent) {
+ aWheelEvent.mDeltaX = aWheelEvent.mDeltaY = aWheelEvent.mDeltaZ = 0.0;
+ aWheelEvent.mLineOrPageDeltaX = aWheelEvent.mLineOrPageDeltaY = 0;
+
+ aWheelEvent.mRefPoint = LayoutDeviceIntPoint(mPanRefPoint.x, mPanRefPoint.y);
+ aWheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_PIXEL;
+ aWheelEvent.mScrollType = WidgetWheelEvent::SCROLL_SYNCHRONOUSLY;
+ aWheelEvent.mIsNoLineOrPageDelta = true;
+
+ aWheelEvent.mOverflowDeltaX = 0.0;
+ aWheelEvent.mOverflowDeltaY = 0.0;
+
+ // Don't scroll the view if we are currently at a bounds, or, if we are
+ // panning back from a max feedback position. This keeps the original drag
+ // point constant.
+ if (!mXAxisFeedback) {
+ aWheelEvent.mDeltaX = mPixelScrollDelta.x;
+ }
+ if (!mYAxisFeedback) {
+ aWheelEvent.mDeltaY = mPixelScrollDelta.y;
+ }
+
+ return (aWheelEvent.mDeltaX != 0 || aWheelEvent.mDeltaY != 0);
+}
diff --git a/widget/windows/nsWinGesture.h b/widget/windows/nsWinGesture.h
new file mode 100644
index 0000000000..040b460da9
--- /dev/null
+++ b/widget/windows/nsWinGesture.h
@@ -0,0 +1,91 @@
+/* -*- 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/. */
+
+#ifndef WinGesture_h__
+#define WinGesture_h__
+
+/*
+ * nsWinGesture - Touch input handling for tablet displays.
+ */
+
+#include "nsdefs.h"
+#include <winuser.h>
+#include <tpcshrd.h>
+#include "nsPoint.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/TouchEvents.h"
+
+// WM_TABLET_QUERYSYSTEMGESTURESTATUS return values
+#define TABLET_ROTATE_GESTURE_ENABLE 0x02000000
+
+class nsPointWin : public nsIntPoint {
+ public:
+ nsPointWin& operator=(const POINTS& aPoint) {
+ x = aPoint.x;
+ y = aPoint.y;
+ return *this;
+ }
+ nsPointWin& operator=(const POINT& aPoint) {
+ x = aPoint.x;
+ y = aPoint.y;
+ return *this;
+ }
+ nsPointWin& operator=(int val) {
+ x = y = val;
+ return *this;
+ }
+ void ScreenToClient(HWND hWnd) {
+ POINT tmp;
+ tmp.x = x;
+ tmp.y = y;
+ ::ScreenToClient(hWnd, &tmp);
+ *this = tmp;
+ }
+};
+
+class nsWinGesture {
+ public:
+ nsWinGesture();
+
+ public:
+ bool SetWinGestureSupport(
+ HWND hWnd, mozilla::WidgetGestureNotifyEvent::PanDirection aDirection);
+ bool ShutdownWinGestureSupport();
+
+ // Simple gesture process
+ bool ProcessGestureMessage(HWND hWnd, WPARAM wParam, LPARAM lParam,
+ mozilla::WidgetSimpleGestureEvent& evt);
+
+ // Pan processing
+ bool IsPanEvent(LPARAM lParam);
+ bool ProcessPanMessage(HWND hWnd, WPARAM wParam, LPARAM lParam);
+ bool PanDeltaToPixelScroll(mozilla::WidgetWheelEvent& aWheelEvent);
+ void UpdatePanFeedbackX(HWND hWnd, int32_t scrollOverflow, bool& endFeedback);
+ void UpdatePanFeedbackY(HWND hWnd, int32_t scrollOverflow, bool& endFeedback);
+ void PanFeedbackFinalize(HWND hWnd, bool endFeedback);
+
+ private:
+ // Delay load info
+ bool InitLibrary();
+
+ // Pan and feedback state
+ nsPointWin mPanIntermediate;
+ nsPointWin mPanRefPoint;
+ nsPointWin mPixelScrollDelta;
+ bool mPanActive;
+ bool mFeedbackActive;
+ bool mXAxisFeedback;
+ bool mYAxisFeedback;
+ bool mPanInertiaActive;
+ nsPointWin mPixelScrollOverflow;
+
+ // Zoom state
+ double mZoomIntermediate;
+
+ // Rotate state
+ double mRotateIntermediate;
+};
+
+#endif /* WinGesture_h__ */
diff --git a/widget/windows/nsWindow.cpp b/widget/windows/nsWindow.cpp
new file mode 100644
index 0000000000..1a8646d620
--- /dev/null
+++ b/widget/windows/nsWindow.cpp
@@ -0,0 +1,9000 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sts=2 sw=2 et cin: */
+/* 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/. */
+
+/*
+ * nsWindow - Native window management and event handling.
+ *
+ * nsWindow is organized into a set of major blocks and
+ * block subsections. The layout is as follows:
+ *
+ * Includes
+ * Variables
+ * nsIWidget impl.
+ * nsIWidget methods and utilities
+ * nsSwitchToUIThread impl.
+ * nsSwitchToUIThread methods and utilities
+ * Moz events
+ * Event initialization
+ * Event dispatching
+ * Native events
+ * Wndproc(s)
+ * Event processing
+ * OnEvent event handlers
+ * IME management and accessibility
+ * Transparency
+ * Popup hook handling
+ * Misc. utilities
+ * Child window impl.
+ *
+ * Search for "BLOCK:" to find major blocks.
+ * Search for "SECTION:" to find specific sections.
+ *
+ * Blocks should be split out into separate files if they
+ * become unmanageable.
+ *
+ * Notable related sources:
+ *
+ * nsWindowDefs.h - Definitions, macros, structs, enums
+ * and general setup.
+ * nsWindowDbg.h/.cpp - Debug related code and directives.
+ * nsWindowGfx.h/.cpp - Graphics and painting.
+ *
+ */
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Includes
+ **
+ ** Include headers.
+ **
+ **************************************************************
+ **************************************************************/
+
+#include "gfx2DGlue.h"
+#include "gfxEnv.h"
+#include "gfxPlatform.h"
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/Likely.h"
+#include "mozilla/PreXULSkeletonUI.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/SwipeTracker.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/TimeStamp.h"
+
+#include "mozilla/ipc/MessageChannel.h"
+#include <algorithm>
+#include <limits>
+
+#include "mozilla/widget/WinMessages.h"
+#include "nsWindow.h"
+#include "nsWindowTaskbarConcealer.h"
+#include "nsAppRunner.h"
+
+#include <shellapi.h>
+#include <windows.h>
+#include <wtsapi32.h>
+#include <process.h>
+#include <commctrl.h>
+#include <dbt.h>
+#include <unknwn.h>
+#include <psapi.h>
+#include <rpc.h>
+#include <propvarutil.h>
+#include <propkey.h>
+
+#include "mozilla/Logging.h"
+#include "prtime.h"
+#include "prenv.h"
+
+#include "mozilla/WidgetTraceEvent.h"
+#include "nsContentUtils.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITheme.h"
+#include "nsIObserverService.h"
+#include "nsIScreenManager.h"
+#include "imgIContainer.h"
+#include "nsIFile.h"
+#include "nsIRollupListener.h"
+#include "nsIClipboard.h"
+#include "WinMouseScrollHandler.h"
+#include "nsFontMetrics.h"
+#include "nsIFontEnumerator.h"
+#include "nsFont.h"
+#include "nsRect.h"
+#include "nsThreadUtils.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsGkAtoms.h"
+#include "nsCRT.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsWidgetsCID.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsString.h"
+#include "mozilla/Components.h"
+#include "nsNativeThemeWin.h"
+#include "nsXULPopupManager.h"
+#include "nsWindowsDllInterceptor.h"
+#include "nsLayoutUtils.h"
+#include "nsView.h"
+#include "nsWindowGfx.h"
+#include "gfxWindowsPlatform.h"
+#include "gfxDWriteFonts.h"
+#include "nsPrintfCString.h"
+#include "mozilla/Preferences.h"
+#include "SystemTimeConverter.h"
+#include "WinTaskbar.h"
+#include "WidgetUtils.h"
+#include "WinWindowOcclusionTracker.h"
+#include "nsIWidgetListener.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/Touch.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/TextEvents.h" // For WidgetKeyboardEvent
+#include "mozilla/TextEventDispatcherListener.h"
+#include "mozilla/widget/nsAutoRollup.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "mozilla/widget/Screen.h"
+#include "nsStyleConsts.h"
+#include "nsBidiKeyboard.h"
+#include "nsStyleConsts.h"
+#include "gfxConfig.h"
+#include "InProcessWinCompositorWidget.h"
+#include "InputDeviceUtils.h"
+#include "ScreenHelperWin.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsNativeAppSupportWin.h"
+#include "mozilla/browser/NimbusFeatures.h"
+
+#include "nsIGfxInfo.h"
+#include "nsUXThemeConstants.h"
+#include "KeyboardLayout.h"
+#include "nsNativeDragTarget.h"
+#include <mmsystem.h> // needed for WIN32_LEAN_AND_MEAN
+#include <zmouse.h>
+#include <richedit.h>
+
+#if defined(ACCESSIBILITY)
+
+# ifdef DEBUG
+# include "mozilla/a11y/Logging.h"
+# endif
+
+# include "oleidl.h"
+# include <winuser.h>
+# include "nsAccessibilityService.h"
+# include "mozilla/a11y/DocAccessible.h"
+# include "mozilla/a11y/LazyInstantiator.h"
+# include "mozilla/a11y/Platform.h"
+# if !defined(WINABLEAPI)
+# include <winable.h>
+# endif // !defined(WINABLEAPI)
+#endif // defined(ACCESSIBILITY)
+
+#include "WindowsUIUtils.h"
+
+#include "nsWindowDefs.h"
+
+#include "nsCrashOnException.h"
+
+#include "nsIContent.h"
+
+#include "mozilla/BackgroundHangMonitor.h"
+#include "WinIMEHandler.h"
+
+#include "npapi.h"
+
+#include <d3d11.h>
+
+// ERROR from wingdi.h (below) gets undefined by some code.
+// #define ERROR 0
+// #define RGN_ERROR ERROR
+#define ERROR 0
+
+#if !defined(SM_CONVERTIBLESLATEMODE)
+# define SM_CONVERTIBLESLATEMODE 0x2003
+#endif
+
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#include "InputData.h"
+
+#include "mozilla/TaskController.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+
+#include "DirectManipulationOwner.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+using namespace mozilla::plugins;
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Variables
+ **
+ ** nsWindow Class static initializations and global variables.
+ **
+ **************************************************************
+ **************************************************************/
+
+/**************************************************************
+ *
+ * SECTION: nsWindow statics
+ *
+ **************************************************************/
+static const wchar_t kUser32LibName[] = L"user32.dll";
+
+uint32_t nsWindow::sInstanceCount = 0;
+bool nsWindow::sIsOleInitialized = false;
+nsIWidget::Cursor nsWindow::sCurrentCursor = {};
+nsWindow* nsWindow::sCurrentWindow = nullptr;
+bool nsWindow::sJustGotDeactivate = false;
+bool nsWindow::sJustGotActivate = false;
+bool nsWindow::sIsInMouseCapture = false;
+
+// Urgent-message reentrancy depth for the static `WindowProc` callback.
+//
+// Three unfortunate facts collide:
+//
+// 𝛼) Some messages must be processed promptly. If not, Windows will leave the
+// receiving window in an intermediate, and potentially unusable, state until
+// the WindowProc invocation that is handling it returns.
+//
+// 𝛽) Some messages have indefinitely long processing time. These are mostly
+// messages which may cause us to enter a nested modal loop (via
+// `SpinEventLoopUntil` or similar).
+//
+// 𝛾) Sometimes, messages skip the queue entirely. Our `WindowProc` may be
+// reentrantly reinvoked from the kernel while we're blocking _on_ the
+// kernel, even briefly, during processing of other messages. (Relevant
+// search term: `KeUserModeCallback`.)
+//
+// The nightmare scenario, then, is that during processing of an 𝛼-message, we
+// briefly become blocked (e.g., by calling `::SendMessageW()`), and the kernel
+// takes that opportunity to use 𝛾 to hand us a 𝛽-message. (Concretely, see
+// bug 1842170.)
+//
+// There is little we can do to prevent the first half of this scenario. 𝛼) and
+// 𝛾) are effectively immutable facts of Windows, and we sometimes legitimately
+// need to make blocking calls to process 𝛼-messages. (We may not even be aware
+// that we're making such calls, if they're undocumented implementation details
+// of another API.)
+//
+// In an ideal world, WindowProc would always return promptly (or at least in
+// bounded time), and 𝛽-messages would not _per se_ exist; long-running modal
+// states would instead be implemented in async fashion. In practice, that's far
+// easier said than done -- replacing existing uses of `SpinEventLoopUntil` _et
+// al._ with asynchronous mechanisms is a collection of mostly-unrelated cross-
+// cutting architectural tasks, each of potentially unbounded scope. For now,
+// and for the foreseeable future, we're stuck with them.
+//
+// We therefore simply punt. More specifically: if a known 𝛽-message jumps the
+// queue to come in while we're in the middle of processing a known 𝛼-message,
+// we:
+// * properly queue the message for processing later;
+// * respond to the 𝛽-message as though we actually had processed it; and
+// * just hope that it can wait until we get around to it.
+//
+// The word "known" requires a bit of justification. There is no canonical set
+// of 𝛼-messages, nor is the set of 𝛽-messages fixed (or even demarcable). We
+// can't safely assume that all messages are 𝛼-messages, as that could cause
+// 𝛽-messages to be arbitrarily and surprisingly delayed whenever any nested
+// event loop is active. We also can't assume all messages are 𝛽-messages,
+// since one 𝛼-message jumping the queue while processing another 𝛼-message is
+// part of normal and required operation for windowed Windows applications.
+//
+// So we simply add messages to those sets as we identify them. (Or, preferably,
+// rework the 𝛽-message's handling to make it no longer 𝛽. But see above.)
+//
+// ---
+//
+// The actual value of `sDepth` is the number of active invocations of
+// `WindowProc` that are processing known 𝛼-messages.
+size_t nsWindow::WndProcUrgentInvocation::sDepth = 0;
+
+// Hook Data Members for Dropdowns. sProcessHook Tells the
+// hook methods whether they should be processing the hook
+// messages.
+HHOOK nsWindow::sMsgFilterHook = nullptr;
+HHOOK nsWindow::sCallProcHook = nullptr;
+HHOOK nsWindow::sCallMouseHook = nullptr;
+bool nsWindow::sProcessHook = false;
+UINT nsWindow::sRollupMsgId = 0;
+HWND nsWindow::sRollupMsgWnd = nullptr;
+UINT nsWindow::sHookTimerId = 0;
+
+// Used to prevent dispatching mouse events that do not originate from user
+// input.
+POINT nsWindow::sLastMouseMovePoint = {0};
+
+bool nsWindow::sIsRestoringSession = false;
+
+bool nsWindow::sTouchInjectInitialized = false;
+InjectTouchInputPtr nsWindow::sInjectTouchFuncPtr;
+
+static SystemTimeConverter<DWORD>& TimeConverter() {
+ static SystemTimeConverter<DWORD> timeConverterSingleton;
+ return timeConverterSingleton;
+}
+
+// Global event hook for window cloaking. Never deregistered.
+// - `Nothing` if not yet set.
+// - `Some(nullptr)` if no attempt should be made to set it.
+static mozilla::Maybe<HWINEVENTHOOK> sWinCloakEventHook = Nothing();
+static mozilla::LazyLogModule sCloakingLog("DWMCloaking");
+
+namespace mozilla {
+
+class CurrentWindowsTimeGetter {
+ public:
+ explicit CurrentWindowsTimeGetter(HWND aWnd) : mWnd(aWnd) {}
+
+ DWORD GetCurrentTime() const { return ::GetTickCount(); }
+
+ void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) {
+ DWORD currentTime = GetCurrentTime();
+ if (sBackwardsSkewStamp && currentTime == sLastPostTime) {
+ // There's already one inflight with this timestamp. Don't
+ // send a duplicate.
+ return;
+ }
+ sBackwardsSkewStamp = Some(aNow);
+ sLastPostTime = currentTime;
+ static_assert(sizeof(WPARAM) >= sizeof(DWORD),
+ "Can't fit a DWORD in a WPARAM");
+ ::PostMessage(mWnd, MOZ_WM_SKEWFIX, sLastPostTime, 0);
+ }
+
+ static bool GetAndClearBackwardsSkewStamp(DWORD aPostTime,
+ TimeStamp* aOutSkewStamp) {
+ if (aPostTime != sLastPostTime) {
+ // The SKEWFIX message is stale; we've sent a new one since then.
+ // Ignore this one.
+ return false;
+ }
+ MOZ_ASSERT(sBackwardsSkewStamp);
+ *aOutSkewStamp = sBackwardsSkewStamp.value();
+ sBackwardsSkewStamp = Nothing();
+ return true;
+ }
+
+ private:
+ static Maybe<TimeStamp> sBackwardsSkewStamp;
+ static DWORD sLastPostTime;
+ HWND mWnd;
+};
+
+Maybe<TimeStamp> CurrentWindowsTimeGetter::sBackwardsSkewStamp;
+DWORD CurrentWindowsTimeGetter::sLastPostTime = 0;
+
+} // namespace mozilla
+
+/**************************************************************
+ *
+ * SECTION: globals variables
+ *
+ **************************************************************/
+
+static const char* sScreenManagerContractID =
+ "@mozilla.org/gfx/screenmanager;1";
+
+extern mozilla::LazyLogModule gWindowsLog;
+
+static NS_DEFINE_CID(kCClipboardCID, NS_CLIPBOARD_CID);
+
+// General purpose user32.dll hook object
+static WindowsDllInterceptor sUser32Intercept;
+
+// When the client area is extended out into the default window frame area,
+// this is the minimum amount of space along the edge of resizable windows
+// we will always display a resize cursor in, regardless of the underlying
+// content.
+static const int32_t kResizableBorderMinSize = 3;
+
+// Getting this object from the window server can be expensive. Keep it
+// around, also get it off the main thread. (See bug 1640852)
+StaticRefPtr<IVirtualDesktopManager> gVirtualDesktopManager;
+static bool gInitializedVirtualDesktopManager = false;
+
+// We should never really try to accelerate windows bigger than this. In some
+// cases this might lead to no D3D9 acceleration where we could have had it
+// but D3D9 does not reliably report when it supports bigger windows. 8192
+// is as safe as we can get, we know at least D3D10 hardware always supports
+// this, other hardware we expect to report correctly in D3D9.
+#define MAX_ACCELERATED_DIMENSION 8192
+
+// On window open (as well as after), Windows has an unfortunate habit of
+// sending rather a lot of WM_NCHITTEST messages. Because we have to do point
+// to DOM target conversions for these, we cache responses for a given
+// coordinate this many milliseconds:
+#define HITTEST_CACHE_LIFETIME_MS 50
+
+#if defined(ACCESSIBILITY)
+
+namespace mozilla {
+
+/**
+ * Windows touchscreen code works by setting a global WH_GETMESSAGE hook and
+ * injecting tiptsf.dll. The touchscreen process then posts registered messages
+ * to our main thread. The tiptsf hook picks up those registered messages and
+ * uses them as commands, some of which call into UIA, which then calls into
+ * MSAA, which then sends WM_GETOBJECT to us.
+ *
+ * We can get ahead of this by installing our own thread-local WH_GETMESSAGE
+ * hook. Since thread-local hooks are called ahead of global hooks, we will
+ * see these registered messages before tiptsf does. At this point we can then
+ * raise a flag that blocks a11y before invoking CallNextHookEx which will then
+ * invoke the global tiptsf hook. Then when we see WM_GETOBJECT, we check the
+ * flag by calling TIPMessageHandler::IsA11yBlocked().
+ *
+ * For Windows 8, we also hook tiptsf!ProcessCaretEvents, which is an a11y hook
+ * function that also calls into UIA.
+ */
+class TIPMessageHandler {
+ public:
+ ~TIPMessageHandler() {
+ if (mHook) {
+ ::UnhookWindowsHookEx(mHook);
+ }
+ }
+
+ static void Initialize() {
+ if (sInstance) {
+ return;
+ }
+
+ sInstance = new TIPMessageHandler();
+ ClearOnShutdown(&sInstance);
+ }
+
+ static bool IsA11yBlocked() {
+ if (!sInstance) {
+ return false;
+ }
+
+ return sInstance->mA11yBlockCount > 0;
+ }
+
+ private:
+ TIPMessageHandler() : mHook(nullptr), mA11yBlockCount(0) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Registered messages used by tiptsf
+ mMessages[0] = ::RegisterWindowMessage(L"ImmersiveFocusNotification");
+ mMessages[1] = ::RegisterWindowMessage(L"TipCloseMenus");
+ mMessages[2] = ::RegisterWindowMessage(L"TabletInputPanelOpening");
+ mMessages[3] = ::RegisterWindowMessage(L"IHM Pen or Touch Event noticed");
+ mMessages[4] = ::RegisterWindowMessage(L"ProgrammabilityCaretVisibility");
+ mMessages[5] = ::RegisterWindowMessage(L"CaretTrackingUpdateIPHidden");
+ mMessages[6] = ::RegisterWindowMessage(L"CaretTrackingUpdateIPInfo");
+
+ mHook = ::SetWindowsHookEx(WH_GETMESSAGE, &TIPHook, nullptr,
+ ::GetCurrentThreadId());
+ MOZ_ASSERT(mHook);
+
+ if (!sSendMessageTimeoutWStub) {
+ sUser32Intercept.Init("user32.dll");
+ DebugOnly<bool> hooked = sSendMessageTimeoutWStub.Set(
+ sUser32Intercept, "SendMessageTimeoutW", &SendMessageTimeoutWHook);
+ MOZ_ASSERT(hooked);
+ }
+ }
+
+ class MOZ_RAII A11yInstantiationBlocker {
+ public:
+ A11yInstantiationBlocker() {
+ if (!TIPMessageHandler::sInstance) {
+ return;
+ }
+ ++TIPMessageHandler::sInstance->mA11yBlockCount;
+ }
+
+ ~A11yInstantiationBlocker() {
+ if (!TIPMessageHandler::sInstance) {
+ return;
+ }
+ MOZ_ASSERT(TIPMessageHandler::sInstance->mA11yBlockCount > 0);
+ --TIPMessageHandler::sInstance->mA11yBlockCount;
+ }
+ };
+
+ friend class A11yInstantiationBlocker;
+
+ static LRESULT CALLBACK TIPHook(int aCode, WPARAM aWParam, LPARAM aLParam) {
+ if (aCode < 0 || !sInstance) {
+ return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam);
+ }
+
+ MSG* msg = reinterpret_cast<MSG*>(aLParam);
+ UINT& msgCode = msg->message;
+
+ for (uint32_t i = 0; i < ArrayLength(sInstance->mMessages); ++i) {
+ if (msgCode == sInstance->mMessages[i]) {
+ A11yInstantiationBlocker block;
+ return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam);
+ }
+ }
+
+ return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam);
+ }
+
+ static LRESULT WINAPI SendMessageTimeoutWHook(HWND aHwnd, UINT aMsgCode,
+ WPARAM aWParam, LPARAM aLParam,
+ UINT aFlags, UINT aTimeout,
+ PDWORD_PTR aMsgResult) {
+ // We don't want to handle this unless the message is a WM_GETOBJECT that we
+ // want to block, and the aHwnd is a nsWindow that belongs to the current
+ // (i.e., main) thread.
+ if (!aMsgResult || aMsgCode != WM_GETOBJECT ||
+ static_cast<LONG>(aLParam) != OBJID_CLIENT || !::NS_IsMainThread() ||
+ !WinUtils::GetNSWindowPtr(aHwnd) || !IsA11yBlocked()) {
+ return sSendMessageTimeoutWStub(aHwnd, aMsgCode, aWParam, aLParam, aFlags,
+ aTimeout, aMsgResult);
+ }
+
+ // In this case we want to fake the result that would happen if we had
+ // decided not to handle WM_GETOBJECT in our WndProc. We hand the message
+ // off to DefWindowProc to accomplish this.
+ *aMsgResult = static_cast<DWORD_PTR>(
+ ::DefWindowProcW(aHwnd, aMsgCode, aWParam, aLParam));
+
+ return static_cast<LRESULT>(TRUE);
+ }
+
+ static WindowsDllInterceptor::FuncHookType<decltype(&SendMessageTimeoutW)>
+ sSendMessageTimeoutWStub;
+ static StaticAutoPtr<TIPMessageHandler> sInstance;
+
+ HHOOK mHook;
+ UINT mMessages[7];
+ uint32_t mA11yBlockCount;
+};
+
+WindowsDllInterceptor::FuncHookType<decltype(&SendMessageTimeoutW)>
+ TIPMessageHandler::sSendMessageTimeoutWStub;
+StaticAutoPtr<TIPMessageHandler> TIPMessageHandler::sInstance;
+
+} // namespace mozilla
+
+#endif // defined(ACCESSIBILITY)
+
+namespace mozilla {
+
+// This task will get the VirtualDesktopManager from the generic thread pool
+// since doing this on the main thread on startup causes performance issues.
+//
+// See bug 1640852.
+//
+// This should be fine and should not require any locking, as when the main
+// thread will access it, if it races with this function it will either find
+// it to be null or to have a valid value.
+class InitializeVirtualDesktopManagerTask : public Task {
+ public:
+ InitializeVirtualDesktopManagerTask()
+ : Task(Kind::OffMainThreadOnly, kDefaultPriorityValue) {}
+
+#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
+ bool GetName(nsACString& aName) override {
+ aName.AssignLiteral("InitializeVirtualDesktopManagerTask");
+ return true;
+ }
+#endif
+
+ virtual TaskResult Run() override {
+ RefPtr<IVirtualDesktopManager> desktopManager;
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_VirtualDesktopManager, NULL, CLSCTX_INPROC_SERVER,
+ __uuidof(IVirtualDesktopManager), getter_AddRefs(desktopManager));
+ if (FAILED(hr)) {
+ return TaskResult::Complete;
+ }
+
+ gVirtualDesktopManager = desktopManager;
+ return TaskResult::Complete;
+ }
+};
+
+// Ground-truth query: does Windows claim the window is cloaked right now?
+static bool IsCloaked(HWND hwnd) {
+ DWORD cloakedState;
+ HRESULT hr = ::DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloakedState,
+ sizeof(cloakedState));
+
+ if (FAILED(hr)) {
+ MOZ_LOG(sCloakingLog, LogLevel::Warning,
+ ("failed (%08lX) to query cloaking state for HWND %p", hr, hwnd));
+ return false;
+ }
+
+ return cloakedState != 0;
+}
+
+} // namespace mozilla
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: nsIWidget impl.
+ **
+ ** nsIWidget interface implementation, broken down into
+ ** sections.
+ **
+ **************************************************************
+ **************************************************************/
+
+/**************************************************************
+ *
+ * SECTION: nsWindow construction and destruction
+ *
+ **************************************************************/
+
+nsWindow::nsWindow(bool aIsChildWindow)
+ : nsBaseWidget(BorderStyle::Default),
+ mBrush(::CreateSolidBrush(NSRGB_2_COLOREF(::GetSysColor(COLOR_BTNFACE)))),
+ mFrameState(std::in_place, this),
+ mIsChildWindow(aIsChildWindow),
+ mLastPaintEndTime(TimeStamp::Now()),
+ mCachedHitTestTime(TimeStamp::Now()),
+ mSizeConstraintsScale(GetDefaultScale().scale),
+ mDesktopId("DesktopIdMutex") {
+ MOZ_ASSERT(mWindowType == WindowType::Child);
+
+ if (!gInitializedVirtualDesktopManager) {
+ TaskController::Get()->AddTask(
+ MakeAndAddRef<InitializeVirtualDesktopManagerTask>());
+ gInitializedVirtualDesktopManager = true;
+ }
+
+ // Global initialization
+ if (!sInstanceCount) {
+ // Global app registration id for Win7 and up. See
+ // WinTaskbar.cpp for details.
+ // MSIX packages explicitly do not support setting the appid from within
+ // the app, as it is set in the package manifest instead.
+ if (!WinUtils::HasPackageIdentity()) {
+ mozilla::widget::WinTaskbar::RegisterAppUserModelID();
+ }
+ if (!StaticPrefs::ui_key_layout_load_when_first_needed()) {
+ KeyboardLayout::GetInstance()->OnLayoutChange(::GetKeyboardLayout(0));
+ }
+#if defined(ACCESSIBILITY)
+ mozilla::TIPMessageHandler::Initialize();
+#endif // defined(ACCESSIBILITY)
+ if (SUCCEEDED(::OleInitialize(nullptr))) {
+ sIsOleInitialized = true;
+ }
+ NS_ASSERTION(sIsOleInitialized, "***** OLE is not initialized!\n");
+ MouseScrollHandler::Initialize();
+ // Init theme data
+ nsUXThemeData::UpdateNativeThemeInfo();
+ RedirectedKeyDownMessageManager::Forget();
+ } // !sInstanceCount
+
+ sInstanceCount++;
+}
+
+nsWindow::~nsWindow() {
+ mInDtor = true;
+
+ // If the widget was released without calling Destroy() then the native window
+ // still exists, and we need to destroy it. Destroy() will early-return if it
+ // was already called. In any case it is important to call it before
+ // destroying mPresentLock (cf. 1156182).
+ Destroy();
+
+ // Free app icon resources. This must happen after `OnDestroy` (see bug
+ // 708033).
+ if (mIconSmall) ::DestroyIcon(mIconSmall);
+
+ if (mIconBig) ::DestroyIcon(mIconBig);
+
+ sInstanceCount--;
+
+ // Global shutdown
+ if (sInstanceCount == 0) {
+ IMEHandler::Terminate();
+ sCurrentCursor = {};
+ if (sIsOleInitialized) {
+ ::OleFlushClipboard();
+ ::OleUninitialize();
+ sIsOleInitialized = false;
+ }
+ }
+
+ NS_IF_RELEASE(mNativeDragTarget);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::Create, nsIWidget::Destroy
+ *
+ * Creating and destroying windows for this widget.
+ *
+ **************************************************************/
+
+// Allow Derived classes to modify the height that is passed
+// when the window is created or resized.
+int32_t nsWindow::GetHeight(int32_t aProposedHeight) { return aProposedHeight; }
+
+void nsWindow::SendAnAPZEvent(InputData& aEvent) {
+ LRESULT popupHandlingResult;
+ if (DealWithPopups(mWnd, MOZ_WM_DMANIP, 0, 0, &popupHandlingResult)) {
+ // We need to consume the event after using it to roll up the popup(s).
+ return;
+ }
+
+ if (mSwipeTracker && aEvent.mInputType == PANGESTURE_INPUT) {
+ // Give the swipe tracker a first pass at the event. If a new pan gesture
+ // has been started since the beginning of the swipe, the swipe tracker
+ // will know to ignore the event.
+ nsEventStatus status =
+ mSwipeTracker->ProcessEvent(aEvent.AsPanGestureInput());
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ }
+
+ APZEventResult result;
+ if (mAPZC) {
+ result = mAPZC->InputBridge()->ReceiveInputEvent(aEvent);
+ }
+ if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+
+ MOZ_ASSERT(aEvent.mInputType == PANGESTURE_INPUT ||
+ aEvent.mInputType == PINCHGESTURE_INPUT);
+
+ if (aEvent.mInputType == PANGESTURE_INPUT) {
+ PanGestureInput& panInput = aEvent.AsPanGestureInput();
+ WidgetWheelEvent event = panInput.ToWidgetEvent(this);
+ if (!mAPZC) {
+ if (MayStartSwipeForNonAPZ(panInput)) {
+ return;
+ }
+ } else {
+ event = MayStartSwipeForAPZ(panInput, result);
+ }
+
+ ProcessUntransformedAPZEvent(&event, result);
+
+ return;
+ }
+
+ PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput();
+ WidgetWheelEvent event = pinchInput.ToWidgetEvent(this);
+ ProcessUntransformedAPZEvent(&event, result);
+}
+
+void nsWindow::RecreateDirectManipulationIfNeeded() {
+ DestroyDirectManipulation();
+
+ if (mWindowType != WindowType::TopLevel && mWindowType != WindowType::Popup) {
+ return;
+ }
+
+ if (!(StaticPrefs::apz_allow_zooming() ||
+ StaticPrefs::apz_windows_use_direct_manipulation()) ||
+ StaticPrefs::apz_windows_force_disable_direct_manipulation()) {
+ return;
+ }
+
+ mDmOwner = MakeUnique<DirectManipulationOwner>(this);
+
+ LayoutDeviceIntRect bounds(mBounds.X(), mBounds.Y(), mBounds.Width(),
+ GetHeight(mBounds.Height()));
+ mDmOwner->Init(bounds);
+}
+
+void nsWindow::ResizeDirectManipulationViewport() {
+ if (mDmOwner) {
+ LayoutDeviceIntRect bounds(mBounds.X(), mBounds.Y(), mBounds.Width(),
+ GetHeight(mBounds.Height()));
+ mDmOwner->ResizeViewport(bounds);
+ }
+}
+
+void nsWindow::DestroyDirectManipulation() {
+ if (mDmOwner) {
+ mDmOwner->Destroy();
+ mDmOwner.reset();
+ }
+}
+
+// Create the proper widget
+nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ widget::InitData* aInitData) {
+ // Historical note: there was once some belief and/or intent that nsWindows
+ // could be created on arbitrary threads, and this may still be reflected in
+ // some comments.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ widget::InitData defaultInitData;
+ if (!aInitData) aInitData = &defaultInitData;
+
+ nsIWidget* baseParent =
+ aInitData->mWindowType == WindowType::Dialog ||
+ aInitData->mWindowType == WindowType::TopLevel ||
+ aInitData->mWindowType == WindowType::Invisible
+ ? nullptr
+ : aParent;
+
+ mIsTopWidgetWindow = (nullptr == baseParent);
+ mBounds = aRect;
+
+ // Ensure that the toolkit is created.
+ nsToolkit::GetToolkit();
+
+ BaseCreate(baseParent, aInitData);
+
+ HWND parent;
+ if (aParent) { // has a nsIWidget parent
+ parent = aParent ? (HWND)aParent->GetNativeData(NS_NATIVE_WINDOW) : nullptr;
+ mParent = aParent;
+ } else { // has a nsNative parent
+ parent = (HWND)aNativeParent;
+ mParent =
+ aNativeParent ? WinUtils::GetNSWindowPtr((HWND)aNativeParent) : nullptr;
+ }
+
+ mIsRTL = aInitData->mRTL;
+ mOpeningAnimationSuppressed = aInitData->mIsAnimationSuppressed;
+ mAlwaysOnTop = aInitData->mAlwaysOnTop;
+ mIsAlert = aInitData->mIsAlert;
+ mResizable = aInitData->mResizable;
+
+ DWORD style = WindowStyle();
+ DWORD extendedStyle = WindowExStyle();
+
+ if (mWindowType == WindowType::Popup) {
+ if (!aParent) {
+ parent = nullptr;
+ }
+ } else if (mWindowType == WindowType::Invisible) {
+ // Make sure CreateWindowEx succeeds at creating a toplevel window
+ style &= ~0x40000000; // WS_CHILDWINDOW
+ } else {
+ // See if the caller wants to explictly set clip children and clip siblings
+ if (aInitData->mClipChildren) {
+ style |= WS_CLIPCHILDREN;
+ } else {
+ style &= ~WS_CLIPCHILDREN;
+ }
+ if (aInitData->mClipSiblings) {
+ style |= WS_CLIPSIBLINGS;
+ }
+ }
+
+ const wchar_t* className = ChooseWindowClass(mWindowType);
+
+ // Take specific actions when creating the first top-level window
+ static bool sFirstTopLevelWindowCreated = false;
+ if (aInitData->mWindowType == WindowType::TopLevel && !aParent &&
+ !sFirstTopLevelWindowCreated) {
+ sFirstTopLevelWindowCreated = true;
+ mWnd = ConsumePreXULSkeletonUIHandle();
+ auto skeletonUIError = GetPreXULSkeletonUIErrorReason();
+ if (skeletonUIError) {
+ nsAutoString errorString(
+ GetPreXULSkeletonUIErrorString(skeletonUIError.value()));
+ Telemetry::ScalarSet(
+ Telemetry::ScalarID::STARTUP_SKELETON_UI_DISABLED_REASON,
+ errorString);
+ }
+ if (mWnd) {
+ MOZ_ASSERT(style == kPreXULSkeletonUIWindowStyle,
+ "The skeleton UI window style should match the expected "
+ "style for the first window created");
+ MOZ_ASSERT(extendedStyle == kPreXULSkeletonUIWindowStyleEx,
+ "The skeleton UI window extended style should match the "
+ "expected extended style for the first window created");
+ MOZ_ASSERT(
+ ::GetWindowThreadProcessId(mWnd, nullptr) == ::GetCurrentThreadId(),
+ "The skeleton UI window should be created on the same thread as "
+ "other windows");
+ mIsShowingPreXULSkeletonUI = true;
+
+ // If we successfully consumed the pre-XUL skeleton UI, just update
+ // our internal state to match what is currently being displayed.
+ mIsVisible = true;
+ mIsCloaked = mozilla::IsCloaked(mWnd);
+ mFrameState->ConsumePreXULSkeletonState(WasPreXULSkeletonUIMaximized());
+
+ // These match the margins set in browser-tabsintitlebar.js with
+ // default prefs on Windows. Bug 1673092 tracks lining this up with
+ // that more correctly instead of hard-coding it.
+ SetNonClientMargins(LayoutDeviceIntMargin(0, 2, 2, 2));
+
+ // Reset the WNDPROC for this window and its whole class, as we had
+ // to use our own WNDPROC when creating the the skeleton UI window.
+ ::SetWindowLongPtrW(mWnd, GWLP_WNDPROC,
+ reinterpret_cast<LONG_PTR>(
+ WinUtils::NonClientDpiScalingDefWindowProcW));
+ ::SetClassLongPtrW(mWnd, GCLP_WNDPROC,
+ reinterpret_cast<LONG_PTR>(
+ WinUtils::NonClientDpiScalingDefWindowProcW));
+ }
+ }
+
+ if (!mWnd) {
+ mWnd =
+ ::CreateWindowExW(extendedStyle, className, L"", style, aRect.X(),
+ aRect.Y(), aRect.Width(), GetHeight(aRect.Height()),
+ parent, nullptr, nsToolkit::mDllInstance, nullptr);
+ }
+
+ if (!mWnd) {
+ NS_WARNING("nsWindow CreateWindowEx failed.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!sWinCloakEventHook) {
+ MOZ_LOG(sCloakingLog, LogLevel::Info, ("Registering cloaking event hook"));
+
+ // C++03 lambda approximation until P2173R1 is available (-std=c++2b)
+ struct StdcallLambda {
+ static void CALLBACK OnCloakUncloakHook(HWINEVENTHOOK hWinEventHook,
+ DWORD event, HWND hwnd,
+ LONG idObject, LONG idChild,
+ DWORD idEventThread,
+ DWORD dwmsEventTime) {
+ const bool isCloaked = event == EVENT_OBJECT_CLOAKED ? true : false;
+ nsWindow::OnCloakEvent(hwnd, isCloaked);
+ }
+ };
+
+ const HWINEVENTHOOK hook = ::SetWinEventHook(
+ EVENT_OBJECT_CLOAKED, EVENT_OBJECT_UNCLOAKED, HMODULE(nullptr),
+ &StdcallLambda::OnCloakUncloakHook, ::GetCurrentProcessId(),
+ ::GetCurrentThreadId(), WINEVENT_OUTOFCONTEXT);
+ sWinCloakEventHook = Some(hook);
+
+ if (!hook) {
+ const DWORD err = ::GetLastError();
+ MOZ_LOG(sCloakingLog, LogLevel::Error,
+ ("Failed to register cloaking event hook! GLE = %lu (0x%lX)", err,
+ err));
+ }
+ }
+
+ if (aInitData->mIsPrivate) {
+ if (NimbusFeatures::GetBool("majorRelease2022"_ns,
+ "feltPrivacyWindowSeparation"_ns, true) &&
+ // Although permanent Private Browsing mode is indeed Private Browsing,
+ // we choose to make it look like regular Firefox in terms of the icon
+ // it uses (which also means we shouldn't use the Private Browsing
+ // AUMID).
+ !StaticPrefs::browser_privatebrowsing_autostart()) {
+ RefPtr<IPropertyStore> pPropStore;
+ if (!FAILED(SHGetPropertyStoreForWindow(mWnd, IID_IPropertyStore,
+ getter_AddRefs(pPropStore)))) {
+ PROPVARIANT pv;
+ nsAutoString aumid;
+ // make sure we're using the private browsing AUMID so that taskbar
+ // grouping works properly
+ Unused << NS_WARN_IF(
+ !mozilla::widget::WinTaskbar::GenerateAppUserModelID(aumid, true));
+ if (!FAILED(InitPropVariantFromString(aumid.get(), &pv))) {
+ if (!FAILED(pPropStore->SetValue(PKEY_AppUserModel_ID, pv))) {
+ pPropStore->Commit();
+ }
+
+ PropVariantClear(&pv);
+ }
+ }
+ HICON icon = ::LoadIconW(::GetModuleHandleW(nullptr),
+ MAKEINTRESOURCEW(IDI_PBMODE));
+ SetBigIcon(icon);
+ SetSmallIcon(icon);
+ }
+ }
+
+ mDeviceNotifyHandle = InputDeviceUtils::RegisterNotification(mWnd);
+
+ // If mDefaultScale is set before mWnd has been set, it will have the scale of
+ // the primary monitor, rather than the monitor that the window is actually
+ // on. For non-popup windows this gets corrected by the WM_DPICHANGED message
+ // which resets mDefaultScale, but for popup windows we don't reset
+ // mDefaultScale on that message. In order to ensure that popup windows
+ // spawned on a non-primary monitor end up with the correct scale, we reset
+ // mDefaultScale here so that it gets recomputed using the correct monitor now
+ // that we have a mWnd.
+ mDefaultScale = -1.0;
+
+ if (mIsRTL) {
+ DWORD dwAttribute = TRUE;
+ DwmSetWindowAttribute(mWnd, DWMWA_NONCLIENT_RTL_LAYOUT, &dwAttribute,
+ sizeof dwAttribute);
+ }
+
+ UpdateDarkModeToolbar();
+
+ if (mOpeningAnimationSuppressed) {
+ SuppressAnimation(true);
+ }
+
+ if (mAlwaysOnTop) {
+ ::SetWindowPos(mWnd, HWND_TOPMOST, 0, 0, 0, 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ }
+
+ if (mWindowType != WindowType::Invisible &&
+ MouseScrollHandler::Device::IsFakeScrollableWindowNeeded()) {
+ // Ugly Thinkpad Driver Hack (Bugs 507222 and 594977)
+ //
+ // We create two zero-sized windows as descendants of the top-level window,
+ // like so:
+ //
+ // Top-level window (MozillaWindowClass)
+ // FAKETRACKPOINTSCROLLCONTAINER (MozillaWindowClass)
+ // FAKETRACKPOINTSCROLLABLE (MozillaWindowClass)
+ //
+ // We need to have the middle window, otherwise the Trackpoint driver
+ // will fail to deliver scroll messages. WM_MOUSEWHEEL messages are
+ // sent to the FAKETRACKPOINTSCROLLABLE, which then propagate up the
+ // window hierarchy until they are handled by nsWindow::WindowProc.
+ // WM_HSCROLL messages are also sent to the FAKETRACKPOINTSCROLLABLE,
+ // but these do not propagate automatically, so we have the window
+ // procedure pretend that they were dispatched to the top-level window
+ // instead.
+ //
+ // The FAKETRACKPOINTSCROLLABLE needs to have the specific window styles it
+ // is given below so that it catches the Trackpoint driver's heuristics.
+ HWND scrollContainerWnd = ::CreateWindowW(
+ className, L"FAKETRACKPOINTSCROLLCONTAINER", WS_CHILD | WS_VISIBLE, 0,
+ 0, 0, 0, mWnd, nullptr, nsToolkit::mDllInstance, nullptr);
+ HWND scrollableWnd = ::CreateWindowW(
+ className, L"FAKETRACKPOINTSCROLLABLE",
+ WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | 0x30, 0, 0, 0, 0,
+ scrollContainerWnd, nullptr, nsToolkit::mDllInstance, nullptr);
+
+ // Give the FAKETRACKPOINTSCROLLABLE window a specific ID so that
+ // WindowProcInternal can distinguish it from the top-level window
+ // easily.
+ ::SetWindowLongPtrW(scrollableWnd, GWLP_ID, eFakeTrackPointScrollableID);
+
+ // Make FAKETRACKPOINTSCROLLABLE use nsWindow::WindowProc, and store the
+ // old window procedure in its "user data".
+ WNDPROC oldWndProc = (WNDPROC)::SetWindowLongPtrW(
+ scrollableWnd, GWLP_WNDPROC, (LONG_PTR)nsWindow::WindowProc);
+ ::SetWindowLongPtrW(scrollableWnd, GWLP_USERDATA, (LONG_PTR)oldWndProc);
+ }
+
+ // We will start receiving native events after associating with our native
+ // window. We will also become the output of WinUtils::GetNSWindowPtr for that
+ // window.
+ if (!AssociateWithNativeWindow()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Starting with Windows XP, a process always runs within a terminal services
+ // session. In order to play nicely with RDP, fast user switching, and the
+ // lock screen, we should be handling WM_WTSSESSION_CHANGE. We must register
+ // our HWND in order to receive this message.
+ DebugOnly<BOOL> wtsRegistered =
+ ::WTSRegisterSessionNotification(mWnd, NOTIFY_FOR_THIS_SESSION);
+ NS_ASSERTION(wtsRegistered, "WTSRegisterSessionNotification failed!\n");
+
+ mDefaultIMC.Init(this);
+ IMEHandler::InitInputContext(this, mInputContext);
+
+ static bool a11yPrimed = false;
+ if (!a11yPrimed && mWindowType == WindowType::TopLevel) {
+ a11yPrimed = true;
+ if (Preferences::GetInt("accessibility.force_disabled", 0) == -1) {
+ ::PostMessage(mWnd, MOZ_WM_STARTA11Y, 0, 0);
+ }
+ }
+
+ RecreateDirectManipulationIfNeeded();
+
+ return NS_OK;
+}
+
+void nsWindow::LocalesChanged() {
+ bool isRTL = intl::LocaleService::GetInstance()->IsAppLocaleRTL();
+ if (mIsRTL != isRTL) {
+ DWORD dwAttribute = isRTL;
+ DwmSetWindowAttribute(mWnd, DWMWA_NONCLIENT_RTL_LAYOUT, &dwAttribute,
+ sizeof dwAttribute);
+ mIsRTL = isRTL;
+ }
+}
+
+// Close this nsWindow
+void nsWindow::Destroy() {
+ // WM_DESTROY has already fired, avoid calling it twice
+ if (mOnDestroyCalled) return;
+
+ // Don't destroy windows that have file pickers open, we'll tear these down
+ // later once the picker is closed.
+ mDestroyCalled = true;
+ if (mPickerDisplayCount) return;
+
+ // During the destruction of all of our children, make sure we don't get
+ // deleted.
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ DestroyDirectManipulation();
+
+ /**
+ * On windows the LayerManagerOGL destructor wants the widget to be around for
+ * cleanup. It also would like to have the HWND intact, so we nullptr it here.
+ */
+ DestroyLayerManager();
+
+ InputDeviceUtils::UnregisterNotification(mDeviceNotifyHandle);
+ mDeviceNotifyHandle = nullptr;
+
+ // The DestroyWindow function destroys the specified window. The function
+ // sends WM_DESTROY and WM_NCDESTROY messages to the window to deactivate it
+ // and remove the keyboard focus from it. The function also destroys the
+ // window's menu, flushes the thread message queue, destroys timers, removes
+ // clipboard ownership, and breaks the clipboard viewer chain (if the window
+ // is at the top of the viewer chain).
+ //
+ // If the specified window is a parent or owner window, DestroyWindow
+ // automatically destroys the associated child or owned windows when it
+ // destroys the parent or owner window. The function first destroys child or
+ // owned windows, and then it destroys the parent or owner window.
+ VERIFY(::DestroyWindow(mWnd));
+
+ // Our windows can be subclassed which may prevent us receiving WM_DESTROY. If
+ // OnDestroy() didn't get called, call it now.
+ if (false == mOnDestroyCalled) {
+ MSGResult msgResult;
+ mWindowHook.Notify(mWnd, WM_DESTROY, 0, 0, msgResult);
+ OnDestroy();
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: Window class utilities
+ *
+ * Utilities for calculating the proper window class name for
+ * Create window.
+ *
+ **************************************************************/
+
+/* static */
+const wchar_t* nsWindow::RegisterWindowClass(const wchar_t* aClassName,
+ UINT aExtraStyle, LPWSTR aIconID) {
+ WNDCLASSW wc;
+ if (::GetClassInfoW(nsToolkit::mDllInstance, aClassName, &wc)) {
+ // already registered
+ return aClassName;
+ }
+
+ wc.style = CS_DBLCLKS | aExtraStyle;
+ wc.lpfnWndProc = WinUtils::NonClientDpiScalingDefWindowProcW;
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hInstance = nsToolkit::mDllInstance;
+ wc.hIcon =
+ aIconID ? ::LoadIconW(::GetModuleHandleW(nullptr), aIconID) : nullptr;
+ wc.hCursor = nullptr;
+ wc.hbrBackground = nullptr;
+ wc.lpszMenuName = nullptr;
+ wc.lpszClassName = aClassName;
+
+ if (!::RegisterClassW(&wc)) {
+ // For older versions of Win32 (i.e., not XP), the registration may
+ // fail with aExtraStyle, so we have to re-register without it.
+ wc.style = CS_DBLCLKS;
+ ::RegisterClassW(&wc);
+ }
+ return aClassName;
+}
+
+static LPWSTR const gStockApplicationIcon = MAKEINTRESOURCEW(32512);
+
+/* static */
+const wchar_t* nsWindow::ChooseWindowClass(WindowType aWindowType) {
+ switch (aWindowType) {
+ case WindowType::Invisible:
+ return RegisterWindowClass(kClassNameHidden, 0, gStockApplicationIcon);
+ case WindowType::Dialog:
+ return RegisterWindowClass(kClassNameDialog, 0, nullptr);
+ case WindowType::Popup:
+ return RegisterWindowClass(kClassNameDropShadow, 0,
+ gStockApplicationIcon);
+ default:
+ return RegisterWindowClass(GetMainWindowClass(), 0,
+ gStockApplicationIcon);
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: Window styles utilities
+ *
+ * Return the proper windows styles and extended styles.
+ *
+ **************************************************************/
+
+// Return nsWindow styles
+DWORD nsWindow::WindowStyle() {
+ DWORD style;
+
+ switch (mWindowType) {
+ case WindowType::Child:
+ style = WS_OVERLAPPED;
+ break;
+
+ case WindowType::Dialog:
+ style = WS_OVERLAPPED | WS_BORDER | WS_DLGFRAME | WS_SYSMENU | DS_3DLOOK |
+ DS_MODALFRAME | WS_CLIPCHILDREN;
+ if (mBorderStyle != BorderStyle::Default)
+ style |= WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
+ break;
+
+ case WindowType::Popup:
+ style = WS_POPUP | WS_OVERLAPPED;
+ break;
+
+ default:
+ NS_ERROR("unknown border style");
+ [[fallthrough]];
+
+ case WindowType::TopLevel:
+ case WindowType::Invisible:
+ style = WS_OVERLAPPED | WS_BORDER | WS_DLGFRAME | WS_SYSMENU |
+ WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN;
+ break;
+ }
+
+ if (mBorderStyle != BorderStyle::Default &&
+ mBorderStyle != BorderStyle::All) {
+ if (mBorderStyle == BorderStyle::None ||
+ !(mBorderStyle & BorderStyle::Border))
+ style &= ~WS_BORDER;
+
+ if (mBorderStyle == BorderStyle::None ||
+ !(mBorderStyle & BorderStyle::Title)) {
+ style &= ~WS_DLGFRAME;
+ }
+
+ if (mBorderStyle == BorderStyle::None ||
+ !(mBorderStyle & BorderStyle::Close))
+ style &= ~0;
+ // XXX The close box can only be removed by changing the window class,
+ // as far as I know --- roc+moz@cs.cmu.edu
+
+ if (mBorderStyle == BorderStyle::None ||
+ !(mBorderStyle & (BorderStyle::Menu | BorderStyle::Close)))
+ style &= ~WS_SYSMENU;
+ // Looks like getting rid of the system menu also does away with the
+ // close box. So, we only get rid of the system menu if you want neither it
+ // nor the close box. How does the Windows "Dialog" window class get just
+ // closebox and no sysmenu? Who knows.
+
+ if (mBorderStyle == BorderStyle::None ||
+ !(mBorderStyle & BorderStyle::ResizeH))
+ style &= ~WS_THICKFRAME;
+
+ if (mBorderStyle == BorderStyle::None ||
+ !(mBorderStyle & BorderStyle::Minimize))
+ style &= ~WS_MINIMIZEBOX;
+
+ if (mBorderStyle == BorderStyle::None ||
+ !(mBorderStyle & BorderStyle::Maximize))
+ style &= ~WS_MAXIMIZEBOX;
+
+ if (IsPopupWithTitleBar()) {
+ style |= WS_CAPTION;
+ if (mBorderStyle & BorderStyle::Close) {
+ style |= WS_SYSMENU;
+ }
+ }
+ }
+
+ if (mIsChildWindow) {
+ style |= WS_CLIPCHILDREN;
+ if (!(style & WS_POPUP)) {
+ style |= WS_CHILD; // WS_POPUP and WS_CHILD are mutually exclusive.
+ }
+ }
+
+ VERIFY_WINDOW_STYLE(style);
+ return style;
+}
+
+// Return nsWindow extended styles
+DWORD nsWindow::WindowExStyle() {
+ MOZ_ASSERT_IF(mIsAlert, mWindowType == WindowType::Dialog);
+ switch (mWindowType) {
+ case WindowType::Child:
+ return 0;
+ case WindowType::Popup: {
+ DWORD extendedStyle = WS_EX_TOOLWINDOW;
+ if (mPopupLevel == PopupLevel::Top) {
+ extendedStyle |= WS_EX_TOPMOST;
+ }
+ return extendedStyle;
+ }
+ case WindowType::Dialog: {
+ if (mIsAlert) {
+ return WS_EX_TOOLWINDOW | WS_EX_TOPMOST;
+ }
+ return WS_EX_WINDOWEDGE | WS_EX_DLGMODALFRAME;
+ }
+ case WindowType::Sheet:
+ MOZ_FALLTHROUGH_ASSERT("Sheets are macOS specific");
+ case WindowType::TopLevel:
+ case WindowType::Invisible:
+ break;
+ }
+ return WS_EX_WINDOWEDGE;
+}
+
+/**************************************************************
+ *
+ * SECTION: Native window association utilities
+ *
+ * Used in Create and Destroy. A nsWindow can associate with its
+ * underlying native window mWnd. Once a native window is
+ * associated with a nsWindow, its native events will be handled
+ * by the static member function nsWindow::WindowProc. Moreover,
+ * the association will be registered in the WinUtils association
+ * list, that is, calling WinUtils::GetNSWindowPtr on the native
+ * window will return the associated nsWindow. This is used in
+ * nsWindow::WindowProc to correctly dispatch native events to
+ * the handler methods defined in nsWindow, even though it is a
+ * static member function.
+ *
+ * After dissociation, the native events of the native window will
+ * no longer be handled by nsWindow::WindowProc, and will thus not
+ * be dispatched to the nsWindow native event handler methods.
+ * Moreover, the association will no longer be registered in the
+ * WinUtils association list, so calling WinUtils::GetNSWindowPtr
+ * on the native window will return nullptr.
+ *
+ **************************************************************/
+
+bool nsWindow::AssociateWithNativeWindow() {
+ if (!mWnd || !IsWindow(mWnd)) {
+ NS_ERROR("Invalid window handle");
+ return false;
+ }
+
+ // Connect the this pointer to the native window handle.
+ // This should be done before SetWindowLongPtrW, because nsWindow::WindowProc
+ // uses WinUtils::GetNSWindowPtr internally.
+ WinUtils::SetNSWindowPtr(mWnd, this);
+
+ ::SetLastError(ERROR_SUCCESS);
+ const auto prevWndProc = reinterpret_cast<WNDPROC>(::SetWindowLongPtrW(
+ mWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(nsWindow::WindowProc)));
+ if (!prevWndProc && GetLastError() != ERROR_SUCCESS) {
+ NS_ERROR("Failure in SetWindowLongPtrW");
+ WinUtils::SetNSWindowPtr(mWnd, nullptr);
+ return false;
+ }
+
+ mPrevWndProc.emplace(prevWndProc);
+ return true;
+}
+
+void nsWindow::DissociateFromNativeWindow() {
+ if (!mWnd || !IsWindow(mWnd) || mPrevWndProc.isNothing()) {
+ return;
+ }
+
+ DebugOnly<WNDPROC> wndProcBeforeDissociate =
+ reinterpret_cast<WNDPROC>(::SetWindowLongPtrW(
+ mWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(*mPrevWndProc)));
+ NS_ASSERTION(wndProcBeforeDissociate == nsWindow::WindowProc,
+ "Unstacked an unexpected native window procedure");
+
+ WinUtils::SetNSWindowPtr(mWnd, nullptr);
+ mPrevWndProc.reset();
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::SetParent, nsIWidget::GetParent
+ *
+ * Set or clear the parent widgets using window properties, and
+ * handles calculating native parent handles.
+ *
+ **************************************************************/
+
+// Get and set parent widgets
+void nsWindow::SetParent(nsIWidget* aNewParent) {
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+ nsIWidget* parent = GetParent();
+ if (parent) {
+ parent->RemoveChild(this);
+ }
+
+ mParent = aNewParent;
+
+ if (aNewParent) {
+ ReparentNativeWidget(aNewParent);
+ aNewParent->AddChild(this);
+ return;
+ }
+ if (mWnd) {
+ // If we have no parent, SetParent should return the desktop.
+ VERIFY(::SetParent(mWnd, nullptr));
+ RecreateDirectManipulationIfNeeded();
+ }
+}
+
+void nsWindow::ReparentNativeWidget(nsIWidget* aNewParent) {
+ MOZ_ASSERT(aNewParent, "null widget");
+
+ mParent = aNewParent;
+ if (mWindowType == WindowType::Popup) {
+ return;
+ }
+ HWND newParent = (HWND)aNewParent->GetNativeData(NS_NATIVE_WINDOW);
+ NS_ASSERTION(newParent, "Parent widget has a null native window handle");
+ if (newParent && mWnd) {
+ ::SetParent(mWnd, newParent);
+ RecreateDirectManipulationIfNeeded();
+ }
+}
+
+nsIWidget* nsWindow::GetParent(void) {
+ if (mIsTopWidgetWindow) {
+ return nullptr;
+ }
+ if (mInDtor || mOnDestroyCalled) {
+ return nullptr;
+ }
+ return mParent;
+}
+
+static int32_t RoundDown(double aDouble) {
+ return aDouble > 0 ? static_cast<int32_t>(floor(aDouble))
+ : static_cast<int32_t>(ceil(aDouble));
+}
+
+float nsWindow::GetDPI() {
+ float dpi = 96.0f;
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ screen->GetDpi(&dpi);
+ }
+ return dpi;
+}
+
+double nsWindow::GetDefaultScaleInternal() {
+ if (mDefaultScale <= 0.0) {
+ mDefaultScale = WinUtils::LogToPhysFactor(mWnd);
+ }
+ return mDefaultScale;
+}
+
+int32_t nsWindow::LogToPhys(double aValue) {
+ return WinUtils::LogToPhys(
+ ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTOPRIMARY), aValue);
+}
+
+nsWindow* nsWindow::GetParentWindow(bool aIncludeOwner) {
+ return static_cast<nsWindow*>(GetParentWindowBase(aIncludeOwner));
+}
+
+nsWindow* nsWindow::GetParentWindowBase(bool aIncludeOwner) {
+ if (mIsTopWidgetWindow) {
+ // Must use a flag instead of mWindowType to tell if the window is the
+ // owned by the topmost widget, because a child window can be embedded
+ // inside a HWND which is not associated with a nsIWidget.
+ return nullptr;
+ }
+
+ // If this widget has already been destroyed, pretend we have no parent.
+ // This corresponds to code in Destroy which removes the destroyed
+ // widget from its parent's child list.
+ if (mInDtor || mOnDestroyCalled) return nullptr;
+
+ // aIncludeOwner set to true implies walking the parent chain to retrieve the
+ // root owner. aIncludeOwner set to false implies the search will stop at the
+ // true parent (default).
+ nsWindow* widget = nullptr;
+ if (mWnd) {
+ HWND parent = nullptr;
+ if (aIncludeOwner)
+ parent = ::GetParent(mWnd);
+ else
+ parent = ::GetAncestor(mWnd, GA_PARENT);
+
+ if (parent) {
+ widget = WinUtils::GetNSWindowPtr(parent);
+ if (widget) {
+ // If the widget is in the process of being destroyed then
+ // do NOT return it
+ if (widget->mInDtor) {
+ widget = nullptr;
+ }
+ }
+ }
+ }
+
+ return widget;
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::Show
+ *
+ * Hide or show this component.
+ *
+ **************************************************************/
+
+void nsWindow::Show(bool bState) {
+ if (bState && mIsShowingPreXULSkeletonUI) {
+ // The first time we decide to actually show the window is when we decide
+ // that we've taken over the window from the skeleton UI, and we should
+ // no longer treat resizes / moves specially.
+ mIsShowingPreXULSkeletonUI = false;
+#if defined(ACCESSIBILITY)
+ // If our HWND has focus and the a11y engine hasn't started yet, fire a
+ // focus win event. Windows already did this when the skeleton UI appeared,
+ // but a11y wouldn't have been able to start at that point even if a client
+ // responded. Firing this now gives clients the chance to respond with
+ // WM_GETOBJECT, which will trigger the a11y engine. We don't want to do
+ // this if the a11y engine has already started because it has probably
+ // already fired focus on a descendant.
+ if (::GetFocus() == mWnd && !GetAccService()) {
+ ::NotifyWinEvent(EVENT_OBJECT_FOCUS, mWnd, OBJID_CLIENT, CHILDID_SELF);
+ }
+#endif // defined(ACCESSIBILITY)
+ }
+
+ if (mWindowType == WindowType::Popup) {
+ MOZ_ASSERT(ChooseWindowClass(mWindowType) == kClassNameDropShadow);
+ // WS_EX_COMPOSITED conflicts with the WS_EX_LAYERED style and causes
+ // some popup menus to become invisible.
+ LONG_PTR exStyle = ::GetWindowLongPtrW(mWnd, GWL_EXSTYLE);
+ if (exStyle & WS_EX_LAYERED) {
+ ::SetWindowLongPtrW(mWnd, GWL_EXSTYLE, exStyle & ~WS_EX_COMPOSITED);
+ }
+ }
+
+ bool syncInvalidate = false;
+
+ bool wasVisible = mIsVisible;
+ // Set the status now so that anyone asking during ShowWindow or
+ // SetWindowPos would get the correct answer.
+ mIsVisible = bState;
+
+ // We may have cached an out of date visible state. This can happen
+ // when session restore sets the full screen mode.
+ if (mIsVisible)
+ mOldStyle |= WS_VISIBLE;
+ else
+ mOldStyle &= ~WS_VISIBLE;
+
+ if (mWnd) {
+ if (bState) {
+ if (!wasVisible && mWindowType == WindowType::TopLevel) {
+ // speed up the initial paint after show for
+ // top level windows:
+ syncInvalidate = true;
+
+ // Set the cursor before showing the window to avoid the default wait
+ // cursor.
+ SetCursor(Cursor{eCursor_standard});
+
+ switch (mFrameState->GetSizeMode()) {
+ case nsSizeMode_Fullscreen:
+ ::ShowWindow(mWnd, SW_SHOW);
+ break;
+ case nsSizeMode_Maximized:
+ ::ShowWindow(mWnd, SW_SHOWMAXIMIZED);
+ break;
+ case nsSizeMode_Minimized:
+ ::ShowWindow(mWnd, SW_SHOWMINIMIZED);
+ break;
+ default:
+ if (CanTakeFocus() && !mAlwaysOnTop) {
+ ::ShowWindow(mWnd, SW_SHOWNORMAL);
+ } else {
+ ::ShowWindow(mWnd, SW_SHOWNOACTIVATE);
+ // Don't flicker the window if we're restoring session
+ if (!sIsRestoringSession) {
+ Unused << GetAttention(2);
+ }
+ }
+ break;
+ }
+ } else {
+ DWORD flags = SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW;
+ if (wasVisible) {
+ flags |= SWP_NOZORDER;
+ }
+ if (mAlwaysOnTop || mIsAlert) {
+ flags |= SWP_NOACTIVATE;
+ }
+
+ if (mWindowType == WindowType::Popup) {
+ // ensure popups are the topmost of the TOPMOST
+ // layer. Remember not to set the SWP_NOZORDER
+ // flag as that might allow the taskbar to overlap
+ // the popup.
+ flags |= SWP_NOACTIVATE;
+ HWND owner = ::GetWindow(mWnd, GW_OWNER);
+ if (owner) {
+ // PopupLevel::Top popups should be above all else. All other
+ // types should be placed in front of their owner, without
+ // changing the owner's z-level relative to other windows.
+ if (mPopupLevel != PopupLevel::Top) {
+ ::SetWindowPos(mWnd, owner, 0, 0, 0, 0, flags);
+ ::SetWindowPos(owner, mWnd, 0, 0, 0, 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ } else {
+ ::SetWindowPos(mWnd, HWND_TOP, 0, 0, 0, 0, flags);
+ }
+ } else {
+ ::SetWindowPos(mWnd, HWND_TOPMOST, 0, 0, 0, 0, flags);
+ }
+ } else {
+ if (mWindowType == WindowType::Dialog && !CanTakeFocus())
+ flags |= SWP_NOACTIVATE;
+
+ ::SetWindowPos(mWnd, HWND_TOP, 0, 0, 0, 0, flags);
+ }
+ }
+ } else {
+ // Clear contents to avoid ghosting of old content if we display
+ // this window again.
+ if (wasVisible && mTransparencyMode == TransparencyMode::Transparent) {
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->ClearTransparentWindow();
+ }
+ }
+ if (mWindowType != WindowType::Dialog) {
+ ::ShowWindow(mWnd, SW_HIDE);
+ } else {
+ ::SetWindowPos(mWnd, 0, 0, 0, 0, 0,
+ SWP_HIDEWINDOW | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER |
+ SWP_NOACTIVATE);
+ }
+ }
+ }
+
+ if (!wasVisible && bState) {
+ Invalidate();
+ if (syncInvalidate && !mInDtor && !mOnDestroyCalled) {
+ ::UpdateWindow(mWnd);
+ }
+ }
+
+ if (mOpeningAnimationSuppressed) {
+ SuppressAnimation(false);
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::IsVisible
+ *
+ * Returns the visibility state.
+ *
+ **************************************************************/
+
+// Return true if the component is visible, false otherwise.
+//
+// This does not take cloaking into account.
+bool nsWindow::IsVisible() const { return mIsVisible; }
+
+/**************************************************************
+ *
+ * SECTION: Touch and APZ-related functions
+ *
+ **************************************************************/
+
+void nsWindow::RegisterTouchWindow() {
+ mTouchWindow = true;
+ ::RegisterTouchWindow(mWnd, TWF_WANTPALM);
+ ::EnumChildWindows(mWnd, nsWindow::RegisterTouchForDescendants, 0);
+}
+
+BOOL CALLBACK nsWindow::RegisterTouchForDescendants(HWND aWnd, LPARAM aMsg) {
+ nsWindow* win = WinUtils::GetNSWindowPtr(aWnd);
+ if (win) {
+ ::RegisterTouchWindow(aWnd, TWF_WANTPALM);
+ }
+ return TRUE;
+}
+
+void nsWindow::LockAspectRatio(bool aShouldLock) {
+ if (aShouldLock) {
+ mAspectRatio = (float)mBounds.Width() / (float)mBounds.Height();
+ } else {
+ mAspectRatio = 0.0;
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::SetInputRegion
+ *
+ * Sets whether the window should ignore mouse events.
+ *
+ **************************************************************/
+void nsWindow::SetInputRegion(const InputRegion& aInputRegion) {
+ mInputRegion = aInputRegion;
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::Move, nsIWidget::Resize, nsIWidget::Size
+ *
+ * Repositioning and sizing a window.
+ *
+ **************************************************************/
+
+void nsWindow::SetSizeConstraints(const SizeConstraints& aConstraints) {
+ SizeConstraints c = aConstraints;
+
+ if (mWindowType != WindowType::Popup && mResizable) {
+ c.mMinSize.width =
+ std::max(int32_t(::GetSystemMetrics(SM_CXMINTRACK)), c.mMinSize.width);
+ c.mMinSize.height =
+ std::max(int32_t(::GetSystemMetrics(SM_CYMINTRACK)), c.mMinSize.height);
+ }
+
+ if (mMaxTextureSize > 0) {
+ // We can't make ThebesLayers bigger than this anyway.. no point it letting
+ // a window grow bigger as we won't be able to draw content there in
+ // general.
+ c.mMaxSize.width = std::min(c.mMaxSize.width, mMaxTextureSize);
+ c.mMaxSize.height = std::min(c.mMaxSize.height, mMaxTextureSize);
+ }
+
+ mSizeConstraintsScale = GetDefaultScale().scale;
+
+ nsBaseWidget::SetSizeConstraints(c);
+}
+
+const SizeConstraints nsWindow::GetSizeConstraints() {
+ double scale = GetDefaultScale().scale;
+ if (mSizeConstraintsScale == scale || mSizeConstraintsScale == 0.0) {
+ return mSizeConstraints;
+ }
+ scale /= mSizeConstraintsScale;
+ SizeConstraints c = mSizeConstraints;
+ if (c.mMinSize.width != NS_MAXSIZE) {
+ c.mMinSize.width = NSToIntRound(c.mMinSize.width * scale);
+ }
+ if (c.mMinSize.height != NS_MAXSIZE) {
+ c.mMinSize.height = NSToIntRound(c.mMinSize.height * scale);
+ }
+ if (c.mMaxSize.width != NS_MAXSIZE) {
+ c.mMaxSize.width = NSToIntRound(c.mMaxSize.width * scale);
+ }
+ if (c.mMaxSize.height != NS_MAXSIZE) {
+ c.mMaxSize.height = NSToIntRound(c.mMaxSize.height * scale);
+ }
+ return c;
+}
+
+// Move this component
+void nsWindow::Move(double aX, double aY) {
+ if (mWindowType == WindowType::TopLevel ||
+ mWindowType == WindowType::Dialog) {
+ SetSizeMode(nsSizeMode_Normal);
+ }
+
+ // for top-level windows only, convert coordinates from desktop pixels
+ // (the "parent" coordinate space) to the window's device pixel space
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ int32_t x = NSToIntRound(aX * scale);
+ int32_t y = NSToIntRound(aY * scale);
+
+ // Check to see if window needs to be moved first
+ // to avoid a costly call to SetWindowPos. This check
+ // can not be moved to the calling code in nsView, because
+ // some platforms do not position child windows correctly
+
+ // Only perform this check for non-popup windows, since the positioning can
+ // in fact change even when the x/y do not. We always need to perform the
+ // check. See bug #97805 for details.
+ if (mWindowType != WindowType::Popup && mBounds.IsEqualXY(x, y)) {
+ // Nothing to do, since it is already positioned correctly.
+ return;
+ }
+
+ mBounds.MoveTo(x, y);
+
+ if (mWnd) {
+#ifdef DEBUG
+ // complain if a window is moved offscreen (legal, but potentially
+ // worrisome)
+ if (mIsTopWidgetWindow) { // only a problem for top-level windows
+ // Make sure this window is actually on the screen before we move it
+ // XXX: Needs multiple monitor support
+ HDC dc = ::GetDC(mWnd);
+ if (dc) {
+ if (::GetDeviceCaps(dc, TECHNOLOGY) == DT_RASDISPLAY) {
+ RECT workArea;
+ ::SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0);
+ // no annoying assertions. just mention the issue.
+ if (x < 0 || x >= workArea.right || y < 0 || y >= workArea.bottom) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("window moved to offscreen position\n"));
+ }
+ }
+ ::ReleaseDC(mWnd, dc);
+ }
+ }
+#endif
+
+ // Normally, when the skeleton UI is disabled, we resize+move the window
+ // before showing it in order to ensure that it restores to the correct
+ // position when the user un-maximizes it. However, when we are using the
+ // skeleton UI, this results in the skeleton UI window being moved around
+ // undesirably before being locked back into the maximized position. To
+ // avoid this, we simply set the placement to restore to via
+ // SetWindowPlacement. It's a little bit more of a dance, though, since we
+ // need to convert the workspace coords that SetWindowPlacement uses to the
+ // screen space coordinates we normally use with SetWindowPos.
+ if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) {
+ WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)};
+ VERIFY(::GetWindowPlacement(mWnd, &pl));
+
+ HMONITOR monitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONULL);
+ if (NS_WARN_IF(!monitor)) {
+ return;
+ }
+ MONITORINFO mi = {sizeof(MONITORINFO)};
+ VERIFY(::GetMonitorInfo(monitor, &mi));
+
+ int32_t deltaX =
+ x + mi.rcWork.left - mi.rcMonitor.left - pl.rcNormalPosition.left;
+ int32_t deltaY =
+ y + mi.rcWork.top - mi.rcMonitor.top - pl.rcNormalPosition.top;
+ pl.rcNormalPosition.left += deltaX;
+ pl.rcNormalPosition.right += deltaX;
+ pl.rcNormalPosition.top += deltaY;
+ pl.rcNormalPosition.bottom += deltaY;
+ VERIFY(::SetWindowPlacement(mWnd, &pl));
+ } else {
+ UINT flags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE;
+ double oldScale = mDefaultScale;
+ mResizeState = IN_SIZEMOVE;
+ VERIFY(::SetWindowPos(mWnd, nullptr, x, y, 0, 0, flags));
+ mResizeState = NOT_RESIZING;
+ if (WinUtils::LogToPhysFactor(mWnd) != oldScale) {
+ ChangedDPI();
+ }
+ }
+
+ ResizeDirectManipulationViewport();
+ }
+}
+
+// Resize this component
+void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
+ // for top-level windows only, convert coordinates from desktop pixels
+ // (the "parent" coordinate space) to the window's device pixel space
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ int32_t width = NSToIntRound(aWidth * scale);
+ int32_t height = NSToIntRound(aHeight * scale);
+
+ NS_ASSERTION((width >= 0), "Negative width passed to nsWindow::Resize");
+ NS_ASSERTION((height >= 0), "Negative height passed to nsWindow::Resize");
+ if (width < 0 || height < 0) {
+ gfxCriticalNoteOnce << "Negative passed to Resize(" << width << ", "
+ << height << ") repaint: " << aRepaint;
+ }
+
+ ConstrainSize(&width, &height);
+
+ // Avoid unnecessary resizing calls
+ if (mBounds.IsEqualSize(width, height)) {
+ if (aRepaint) {
+ Invalidate();
+ }
+ return;
+ }
+
+ // Set cached value for lightweight and printing
+ bool wasLocking = mAspectRatio != 0.0;
+ mBounds.SizeTo(width, height);
+ if (wasLocking) {
+ LockAspectRatio(true); // This causes us to refresh the mAspectRatio value
+ }
+
+ if (mWnd) {
+ // Refer to the comment above a similar check in nsWindow::Move
+ if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) {
+ WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)};
+ VERIFY(::GetWindowPlacement(mWnd, &pl));
+ pl.rcNormalPosition.right = pl.rcNormalPosition.left + width;
+ pl.rcNormalPosition.bottom = pl.rcNormalPosition.top + GetHeight(height);
+ mResizeState = RESIZING;
+ VERIFY(::SetWindowPlacement(mWnd, &pl));
+ mResizeState = NOT_RESIZING;
+ } else {
+ UINT flags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE;
+
+ if (!aRepaint) {
+ flags |= SWP_NOREDRAW;
+ }
+
+ double oldScale = mDefaultScale;
+ mResizeState = RESIZING;
+ VERIFY(
+ ::SetWindowPos(mWnd, nullptr, 0, 0, width, GetHeight(height), flags));
+
+ mResizeState = NOT_RESIZING;
+ if (WinUtils::LogToPhysFactor(mWnd) != oldScale) {
+ ChangedDPI();
+ }
+ }
+
+ ResizeDirectManipulationViewport();
+ }
+
+ if (aRepaint) Invalidate();
+}
+
+// Resize this component
+void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ // for top-level windows only, convert coordinates from desktop pixels
+ // (the "parent" coordinate space) to the window's device pixel space
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ int32_t x = NSToIntRound(aX * scale);
+ int32_t y = NSToIntRound(aY * scale);
+ int32_t width = NSToIntRound(aWidth * scale);
+ int32_t height = NSToIntRound(aHeight * scale);
+
+ NS_ASSERTION((width >= 0), "Negative width passed to nsWindow::Resize");
+ NS_ASSERTION((height >= 0), "Negative height passed to nsWindow::Resize");
+ if (width < 0 || height < 0) {
+ gfxCriticalNoteOnce << "Negative passed to Resize(" << x << " ," << y
+ << ", " << width << ", " << height
+ << ") repaint: " << aRepaint;
+ }
+
+ ConstrainSize(&width, &height);
+
+ // Avoid unnecessary resizing calls
+ if (mBounds.IsEqualRect(x, y, width, height)) {
+ if (aRepaint) {
+ Invalidate();
+ }
+ return;
+ }
+
+ // Set cached value for lightweight and printing
+ mBounds.SetRect(x, y, width, height);
+
+ if (mWnd) {
+ // Refer to the comment above a similar check in nsWindow::Move
+ if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) {
+ WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)};
+ VERIFY(::GetWindowPlacement(mWnd, &pl));
+
+ HMONITOR monitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONULL);
+ if (NS_WARN_IF(!monitor)) {
+ return;
+ }
+ MONITORINFO mi = {sizeof(MONITORINFO)};
+ VERIFY(::GetMonitorInfo(monitor, &mi));
+
+ int32_t deltaX =
+ x + mi.rcWork.left - mi.rcMonitor.left - pl.rcNormalPosition.left;
+ int32_t deltaY =
+ y + mi.rcWork.top - mi.rcMonitor.top - pl.rcNormalPosition.top;
+ pl.rcNormalPosition.left += deltaX;
+ pl.rcNormalPosition.right = pl.rcNormalPosition.left + width;
+ pl.rcNormalPosition.top += deltaY;
+ pl.rcNormalPosition.bottom = pl.rcNormalPosition.top + GetHeight(height);
+ VERIFY(::SetWindowPlacement(mWnd, &pl));
+ } else {
+ UINT flags = SWP_NOZORDER | SWP_NOACTIVATE;
+ if (!aRepaint) {
+ flags |= SWP_NOREDRAW;
+ }
+
+ double oldScale = mDefaultScale;
+ mResizeState = RESIZING;
+ VERIFY(
+ ::SetWindowPos(mWnd, nullptr, x, y, width, GetHeight(height), flags));
+ mResizeState = NOT_RESIZING;
+ if (WinUtils::LogToPhysFactor(mWnd) != oldScale) {
+ ChangedDPI();
+ }
+
+ if (mTransitionWnd) {
+ // If we have a fullscreen transition window, we need to make
+ // it topmost again, otherwise the taskbar may be raised by
+ // the system unexpectedly when we leave fullscreen state.
+ ::SetWindowPos(mTransitionWnd, HWND_TOPMOST, 0, 0, 0, 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ }
+ }
+
+ ResizeDirectManipulationViewport();
+ }
+
+ if (aRepaint) Invalidate();
+}
+
+mozilla::Maybe<bool> nsWindow::IsResizingNativeWidget() {
+ if (mResizeState == RESIZING) {
+ return Some(true);
+ }
+ return Some(false);
+}
+
+/**************************************************************
+ *
+ * SECTION: Window Z-order and state.
+ *
+ * nsIWidget::PlaceBehind, nsIWidget::SetSizeMode,
+ * nsIWidget::ConstrainPosition
+ *
+ * Z-order, positioning, restore, minimize, and maximize.
+ *
+ **************************************************************/
+
+// Position the window behind the given window
+void nsWindow::PlaceBehind(nsTopLevelWidgetZPlacement aPlacement,
+ nsIWidget* aWidget, bool aActivate) {
+ HWND behind = HWND_TOP;
+ if (aPlacement == eZPlacementBottom)
+ behind = HWND_BOTTOM;
+ else if (aPlacement == eZPlacementBelow && aWidget)
+ behind = (HWND)aWidget->GetNativeData(NS_NATIVE_WINDOW);
+ UINT flags = SWP_NOMOVE | SWP_NOREPOSITION | SWP_NOSIZE;
+ if (!aActivate) flags |= SWP_NOACTIVATE;
+
+ if (!CanTakeFocus() && behind == HWND_TOP) {
+ // Can't place the window to top so place it behind the foreground window
+ // (as long as it is not topmost)
+ HWND wndAfter = ::GetForegroundWindow();
+ if (!wndAfter)
+ behind = HWND_BOTTOM;
+ else if (!(GetWindowLongPtrW(wndAfter, GWL_EXSTYLE) & WS_EX_TOPMOST))
+ behind = wndAfter;
+ flags |= SWP_NOACTIVATE;
+ }
+
+ ::SetWindowPos(mWnd, behind, 0, 0, 0, 0, flags);
+}
+
+static UINT GetCurrentShowCmd(HWND aWnd) {
+ WINDOWPLACEMENT pl;
+ pl.length = sizeof(pl);
+ ::GetWindowPlacement(aWnd, &pl);
+ return pl.showCmd;
+}
+
+// Maximize, minimize or restore the window.
+void nsWindow::SetSizeMode(nsSizeMode aMode) {
+ // If we are still displaying a maximized pre-XUL skeleton UI, ignore the
+ // noise of sizemode changes. Once we have "shown" the window for the first
+ // time (called nsWindow::Show(true), even though the window is already
+ // technically displayed), we will again accept sizemode changes.
+ if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) {
+ return;
+ }
+
+ mFrameState->EnsureSizeMode(aMode);
+}
+
+nsSizeMode nsWindow::SizeMode() { return mFrameState->GetSizeMode(); }
+
+void DoGetWorkspaceID(HWND aWnd, nsAString* aWorkspaceID) {
+ RefPtr<IVirtualDesktopManager> desktopManager = gVirtualDesktopManager;
+ if (!desktopManager || !aWnd) {
+ return;
+ }
+
+ GUID desktop;
+ HRESULT hr = desktopManager->GetWindowDesktopId(aWnd, &desktop);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ RPC_WSTR workspaceIDStr = nullptr;
+ if (UuidToStringW(&desktop, &workspaceIDStr) == RPC_S_OK) {
+ aWorkspaceID->Assign((wchar_t*)workspaceIDStr);
+ RpcStringFreeW(&workspaceIDStr);
+ }
+}
+
+void nsWindow::GetWorkspaceID(nsAString& workspaceID) {
+ // If we have a value cached, use that, but also make sure it is
+ // scheduled to be updated. If we don't yet have a value, get
+ // one synchronously.
+ auto desktop = mDesktopId.Lock();
+ if (desktop->mID.IsEmpty()) {
+ DoGetWorkspaceID(mWnd, &desktop->mID);
+ desktop->mUpdateIsQueued = false;
+ } else {
+ AsyncUpdateWorkspaceID(*desktop);
+ }
+
+ workspaceID = desktop->mID;
+}
+
+void nsWindow::AsyncUpdateWorkspaceID(Desktop& aDesktop) {
+ struct UpdateWorkspaceIdTask : public Task {
+ explicit UpdateWorkspaceIdTask(nsWindow* aSelf)
+ : Task(Kind::OffMainThreadOnly, EventQueuePriority::Normal),
+ mSelf(aSelf) {}
+
+ TaskResult Run() override {
+ auto desktop = mSelf->mDesktopId.Lock();
+ if (desktop->mUpdateIsQueued) {
+ DoGetWorkspaceID(mSelf->mWnd, &desktop->mID);
+ desktop->mUpdateIsQueued = false;
+ }
+ return TaskResult::Complete;
+ }
+
+#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
+ bool GetName(nsACString& aName) override {
+ aName.AssignLiteral("UpdateWorkspaceIdTask");
+ return true;
+ }
+#endif
+
+ RefPtr<nsWindow> mSelf;
+ };
+
+ if (aDesktop.mUpdateIsQueued) {
+ return;
+ }
+
+ aDesktop.mUpdateIsQueued = true;
+ TaskController::Get()->AddTask(MakeAndAddRef<UpdateWorkspaceIdTask>(this));
+}
+
+void nsWindow::MoveToWorkspace(const nsAString& workspaceID) {
+ RefPtr<IVirtualDesktopManager> desktopManager = gVirtualDesktopManager;
+ if (!desktopManager) {
+ return;
+ }
+
+ GUID desktop;
+ const nsString flat = PromiseFlatString(workspaceID);
+ RPC_WSTR workspaceIDStr = reinterpret_cast<RPC_WSTR>((wchar_t*)flat.get());
+ if (UuidFromStringW(workspaceIDStr, &desktop) == RPC_S_OK) {
+ if (SUCCEEDED(desktopManager->MoveWindowToDesktop(mWnd, desktop))) {
+ auto desktop = mDesktopId.Lock();
+ desktop->mID = workspaceID;
+ }
+ }
+}
+
+void nsWindow::SuppressAnimation(bool aSuppress) {
+ DWORD dwAttribute = aSuppress ? TRUE : FALSE;
+ DwmSetWindowAttribute(mWnd, DWMWA_TRANSITIONS_FORCEDISABLED, &dwAttribute,
+ sizeof dwAttribute);
+}
+
+// Constrain a potential move to fit onscreen
+// Position (aX, aY) is specified in Windows screen (logical) pixels,
+// except when using per-monitor DPI, in which case it's device pixels.
+void nsWindow::ConstrainPosition(DesktopIntPoint& aPoint) {
+ if (!mIsTopWidgetWindow) // only a problem for top-level windows
+ return;
+
+ double dpiScale = GetDesktopToDeviceScale().scale;
+
+ // We need to use the window size in the kind of pixels used for window-
+ // manipulation APIs.
+ int32_t logWidth =
+ std::max<int32_t>(NSToIntRound(mBounds.Width() / dpiScale), 1);
+ int32_t logHeight =
+ std::max<int32_t>(NSToIntRound(mBounds.Height() / dpiScale), 1);
+
+ /* get our playing field. use the current screen, or failing that
+ for any reason, use device caps for the default screen. */
+ RECT screenRect;
+
+ nsCOMPtr<nsIScreenManager> screenmgr =
+ do_GetService(sScreenManagerContractID);
+ if (!screenmgr) {
+ return;
+ }
+ nsCOMPtr<nsIScreen> screen;
+ int32_t left, top, width, height;
+
+ screenmgr->ScreenForRect(aPoint.x, aPoint.y, logWidth, logHeight,
+ getter_AddRefs(screen));
+ if (mFrameState->GetSizeMode() != nsSizeMode_Fullscreen) {
+ // For normalized windows, use the desktop work area.
+ nsresult rv = screen->GetAvailRectDisplayPix(&left, &top, &width, &height);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ } else {
+ // For full screen windows, use the desktop.
+ nsresult rv = screen->GetRectDisplayPix(&left, &top, &width, &height);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+ screenRect.left = left;
+ screenRect.right = left + width;
+ screenRect.top = top;
+ screenRect.bottom = top + height;
+
+ if (aPoint.x < screenRect.left)
+ aPoint.x = screenRect.left;
+ else if (aPoint.x >= screenRect.right - logWidth)
+ aPoint.x = screenRect.right - logWidth;
+
+ if (aPoint.y < screenRect.top)
+ aPoint.y = screenRect.top;
+ else if (aPoint.y >= screenRect.bottom - logHeight)
+ aPoint.y = screenRect.bottom - logHeight;
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::Enable, nsIWidget::IsEnabled
+ *
+ * Enabling and disabling the widget.
+ *
+ **************************************************************/
+
+// Enable/disable this component
+void nsWindow::Enable(bool bState) {
+ if (mWnd) {
+ ::EnableWindow(mWnd, bState);
+ }
+}
+
+// Return the current enable state
+bool nsWindow::IsEnabled() const {
+ return !mWnd || (::IsWindowEnabled(mWnd) &&
+ ::IsWindowEnabled(::GetAncestor(mWnd, GA_ROOT)));
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::SetFocus
+ *
+ * Give the focus to this widget.
+ *
+ **************************************************************/
+
+void nsWindow::SetFocus(Raise aRaise, mozilla::dom::CallerType aCallerType) {
+ if (mWnd) {
+#ifdef WINSTATE_DEBUG_OUTPUT
+ if (mWnd == WinUtils::GetTopLevelHWND(mWnd)) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("*** SetFocus: [ top] raise=%d\n", aRaise == Raise::Yes));
+ } else {
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("*** SetFocus: [child] raise=%d\n", aRaise == Raise::Yes));
+ }
+#endif
+ // Uniconify, if necessary
+ HWND toplevelWnd = WinUtils::GetTopLevelHWND(mWnd);
+ if (aRaise == Raise::Yes && ::IsIconic(toplevelWnd)) {
+ ::ShowWindow(toplevelWnd, SW_RESTORE);
+ }
+ ::SetFocus(mWnd);
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: Bounds
+ *
+ * GetBounds, GetClientBounds, GetScreenBounds,
+ * GetRestoredBounds, GetClientOffset, SetNonClientMargins
+ *
+ * Bound calculations.
+ *
+ **************************************************************/
+
+// Return the window's full dimensions in screen coordinates.
+// If the window has a parent, converts the origin to an offset
+// of the parent's screen origin.
+LayoutDeviceIntRect nsWindow::GetBounds() {
+ if (!mWnd) {
+ return mBounds;
+ }
+
+ RECT r;
+ VERIFY(::GetWindowRect(mWnd, &r));
+
+ LayoutDeviceIntRect rect;
+
+ // assign size
+ rect.SizeTo(r.right - r.left, r.bottom - r.top);
+
+ // popup window bounds' are in screen coordinates, not relative to parent
+ // window
+ if (mWindowType == WindowType::Popup) {
+ rect.MoveTo(r.left, r.top);
+ return rect;
+ }
+
+ // chrome on parent:
+ // ___ 5,5 (chrome start)
+ // | ____ 10,10 (client start)
+ // | | ____ 20,20 (child start)
+ // | | |
+ // 20,20 - 5,5 = 15,15 (??)
+ // minus GetClientOffset:
+ // 15,15 - 5,5 = 10,10
+ //
+ // no chrome on parent:
+ // ______ 10,10 (win start)
+ // | ____ 20,20 (child start)
+ // | |
+ // 20,20 - 10,10 = 10,10
+ //
+ // walking the chain:
+ // ___ 5,5 (chrome start)
+ // | ___ 10,10 (client start)
+ // | | ___ 20,20 (child start)
+ // | | | __ 30,30 (child start)
+ // | | | |
+ // 30,30 - 20,20 = 10,10 (offset from second child to first)
+ // 20,20 - 5,5 = 15,15 + 10,10 = 25,25 (??)
+ // minus GetClientOffset:
+ // 25,25 - 5,5 = 20,20 (offset from second child to parent client)
+
+ // convert coordinates if parent exists
+ HWND parent = ::GetParent(mWnd);
+ if (parent) {
+ RECT pr;
+ VERIFY(::GetWindowRect(parent, &pr));
+ r.left -= pr.left;
+ r.top -= pr.top;
+ // adjust for chrome
+ nsWindow* pWidget = static_cast<nsWindow*>(GetParent());
+ if (pWidget && pWidget->IsTopLevelWidget()) {
+ LayoutDeviceIntPoint clientOffset = pWidget->GetClientOffset();
+ r.left -= clientOffset.x;
+ r.top -= clientOffset.y;
+ }
+ }
+ rect.MoveTo(r.left, r.top);
+ if (mCompositorSession &&
+ !wr::WindowSizeSanityCheck(rect.width, rect.height)) {
+ gfxCriticalNoteOnce << "Invalid size" << rect << " size mode "
+ << mFrameState->GetSizeMode();
+ }
+
+ return rect;
+}
+
+// Get this component dimension
+LayoutDeviceIntRect nsWindow::GetClientBounds() {
+ if (!mWnd) {
+ return LayoutDeviceIntRect(0, 0, 0, 0);
+ }
+
+ RECT r;
+ if (!::GetClientRect(mWnd, &r)) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ gfxCriticalNoteOnce << "GetClientRect failed " << ::GetLastError();
+ return mBounds;
+ }
+
+ LayoutDeviceIntRect bounds = GetBounds();
+ LayoutDeviceIntRect rect;
+ rect.MoveTo(bounds.TopLeft() + GetClientOffset());
+ rect.SizeTo(r.right - r.left, r.bottom - r.top);
+ return rect;
+}
+
+// Like GetBounds, but don't offset by the parent
+LayoutDeviceIntRect nsWindow::GetScreenBounds() {
+ if (!mWnd) {
+ return mBounds;
+ }
+
+ RECT r;
+ VERIFY(::GetWindowRect(mWnd, &r));
+
+ return LayoutDeviceIntRect(r.left, r.top, r.right - r.left, r.bottom - r.top);
+}
+
+nsresult nsWindow::GetRestoredBounds(LayoutDeviceIntRect& aRect) {
+ if (SizeMode() == nsSizeMode_Normal) {
+ aRect = GetScreenBounds();
+ return NS_OK;
+ }
+ if (!mWnd) {
+ return NS_ERROR_FAILURE;
+ }
+
+ WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)};
+ VERIFY(::GetWindowPlacement(mWnd, &pl));
+ const RECT& r = pl.rcNormalPosition;
+
+ HMONITOR monitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONULL);
+ if (!monitor) {
+ return NS_ERROR_FAILURE;
+ }
+ MONITORINFO mi = {sizeof(MONITORINFO)};
+ VERIFY(::GetMonitorInfo(monitor, &mi));
+
+ aRect.SetRect(r.left, r.top, r.right - r.left, r.bottom - r.top);
+ aRect.MoveBy(mi.rcWork.left - mi.rcMonitor.left,
+ mi.rcWork.top - mi.rcMonitor.top);
+ return NS_OK;
+}
+
+// Return the x,y offset of the client area from the origin of the window. If
+// the window is borderless returns (0,0).
+LayoutDeviceIntPoint nsWindow::GetClientOffset() {
+ if (!mWnd) {
+ return LayoutDeviceIntPoint(0, 0);
+ }
+
+ RECT r1;
+ GetWindowRect(mWnd, &r1);
+ LayoutDeviceIntPoint pt = WidgetToScreenOffset();
+ return LayoutDeviceIntPoint(pt.x - LayoutDeviceIntCoord(r1.left),
+ pt.y - LayoutDeviceIntCoord(r1.top));
+}
+
+void nsWindow::ResetLayout() {
+ // This will trigger a frame changed event, triggering
+ // nc calc size and a sizemode gecko event.
+ SetWindowPos(mWnd, 0, 0, 0, 0, 0,
+ SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE |
+ SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOZORDER);
+
+ // If hidden, just send the frame changed event for now.
+ if (!mIsVisible) {
+ return;
+ }
+
+ // Send a gecko size event to trigger reflow.
+ RECT clientRc = {0};
+ GetClientRect(mWnd, &clientRc);
+ OnResize(WinUtils::ToIntRect(clientRc).Size());
+
+ // Invalidate and update
+ Invalidate();
+}
+
+// Internally track the caption status via a window property. Required
+// due to our internal handling of WM_NCACTIVATE when custom client
+// margins are set.
+static const wchar_t kManageWindowInfoProperty[] = L"ManageWindowInfoProperty";
+typedef BOOL(WINAPI* GetWindowInfoPtr)(HWND hwnd, PWINDOWINFO pwi);
+static WindowsDllInterceptor::FuncHookType<GetWindowInfoPtr>
+ sGetWindowInfoPtrStub;
+
+BOOL WINAPI GetWindowInfoHook(HWND hWnd, PWINDOWINFO pwi) {
+ if (!sGetWindowInfoPtrStub) {
+ NS_ASSERTION(FALSE, "Something is horribly wrong in GetWindowInfoHook!");
+ return FALSE;
+ }
+ int windowStatus =
+ reinterpret_cast<LONG_PTR>(GetPropW(hWnd, kManageWindowInfoProperty));
+ // No property set, return the default data.
+ if (!windowStatus) return sGetWindowInfoPtrStub(hWnd, pwi);
+ // Call GetWindowInfo and update dwWindowStatus with our
+ // internally tracked value.
+ BOOL result = sGetWindowInfoPtrStub(hWnd, pwi);
+ if (result && pwi)
+ pwi->dwWindowStatus = (windowStatus == 1 ? 0 : WS_ACTIVECAPTION);
+ return result;
+}
+
+void nsWindow::UpdateGetWindowInfoCaptionStatus(bool aActiveCaption) {
+ if (!mWnd) return;
+
+ sUser32Intercept.Init("user32.dll");
+ sGetWindowInfoPtrStub.Set(sUser32Intercept, "GetWindowInfo",
+ &GetWindowInfoHook);
+ if (!sGetWindowInfoPtrStub) {
+ return;
+ }
+
+ // Update our internally tracked caption status
+ SetPropW(mWnd, kManageWindowInfoProperty,
+ reinterpret_cast<HANDLE>(static_cast<INT_PTR>(aActiveCaption) + 1));
+}
+
+#define DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 19
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+
+void nsWindow::UpdateDarkModeToolbar() {
+ PreferenceSheet::EnsureInitialized();
+ BOOL dark = PreferenceSheet::ColorSchemeForChrome() == ColorScheme::Dark;
+ DwmSetWindowAttribute(mWnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, &dark,
+ sizeof dark);
+ DwmSetWindowAttribute(mWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark,
+ sizeof dark);
+}
+
+LayoutDeviceIntMargin nsWindow::NormalWindowNonClientOffset() const {
+ LayoutDeviceIntMargin nonClientOffset;
+
+ // We're dealing with a "normal" window (not maximized, minimized, or
+ // fullscreen), so process `mNonClientMargins` and set `mNonClientOffset`
+ // accordingly.
+ //
+ // Setting `mNonClientOffset` to 0 has the effect of leaving the default
+ // frame intact. Setting it to a value greater than 0 reduces the frame
+ // size by that amount.
+
+ if (mNonClientMargins.top > 0) {
+ nonClientOffset.top = std::min(mCaptionHeight, mNonClientMargins.top);
+ } else if (mNonClientMargins.top == 0) {
+ nonClientOffset.top = mCaptionHeight;
+ } else {
+ nonClientOffset.top = 0;
+ }
+
+ if (mNonClientMargins.bottom > 0) {
+ nonClientOffset.bottom =
+ std::min(mVertResizeMargin, mNonClientMargins.bottom);
+ } else if (mNonClientMargins.bottom == 0) {
+ nonClientOffset.bottom = mVertResizeMargin;
+ } else {
+ nonClientOffset.bottom = 0;
+ }
+
+ if (mNonClientMargins.left > 0) {
+ nonClientOffset.left = std::min(mHorResizeMargin, mNonClientMargins.left);
+ } else if (mNonClientMargins.left == 0) {
+ nonClientOffset.left = mHorResizeMargin;
+ } else {
+ nonClientOffset.left = 0;
+ }
+
+ if (mNonClientMargins.right > 0) {
+ nonClientOffset.right = std::min(mHorResizeMargin, mNonClientMargins.right);
+ } else if (mNonClientMargins.right == 0) {
+ nonClientOffset.right = mHorResizeMargin;
+ } else {
+ nonClientOffset.right = 0;
+ }
+ return nonClientOffset;
+}
+
+/**
+ * Called when the window layout changes: full screen mode transitions,
+ * theme changes, and composition changes. Calculates the new non-client
+ * margins and fires off a frame changed event, which triggers an nc calc
+ * size windows event, kicking the changes in.
+ *
+ * The offsets calculated here are based on the value of `mNonClientMargins`
+ * which is specified in the "chromemargins" attribute of the window. For
+ * each margin, the value specified has the following meaning:
+ * -1 - leave the default frame in place
+ * 0 - remove the frame
+ * >0 - frame size equals min(0, (default frame size - margin value))
+ *
+ * This function calculates and populates `mNonClientOffset`.
+ * In our processing of `WM_NCCALCSIZE`, the frame size will be calculated
+ * as (default frame size - offset). For example, if the left frame should
+ * be 1 pixel narrower than the default frame size, `mNonClientOffset.left`
+ * will equal 1.
+ *
+ * For maximized, fullscreen, and minimized windows, the values stored in
+ * `mNonClientMargins` are ignored, and special processing takes place.
+ *
+ * For non-glass windows, we only allow frames to be their default size
+ * or removed entirely.
+ */
+bool nsWindow::UpdateNonClientMargins(bool aReflowWindow) {
+ if (!mCustomNonClient) {
+ return false;
+ }
+
+ const nsSizeMode sizeMode = mFrameState->GetSizeMode();
+
+ bool hasCaption =
+ bool(mBorderStyle & (BorderStyle::All | BorderStyle::Title |
+ BorderStyle::Menu | BorderStyle::Default));
+
+ float dpi = GetDPI();
+
+ // mCaptionHeight is the default size of the NC area at
+ // the top of the window. If the window has a caption,
+ // the size is calculated as the sum of:
+ // SM_CYFRAME - The thickness of the sizing border
+ // around a resizable window
+ // SM_CXPADDEDBORDER - The amount of border padding
+ // for captioned windows
+ // SM_CYCAPTION - The height of the caption area
+ //
+ // If the window does not have a caption, mCaptionHeight will be equal to
+ // `WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi)`
+ mCaptionHeight =
+ WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi) +
+ (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CYCAPTION, dpi) +
+ WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi)
+ : 0);
+ if (!mUseResizeMarginOverrides) {
+ // mHorResizeMargin is the size of the default NC areas on the
+ // left and right sides of our window. It is calculated as
+ // the sum of:
+ // SM_CXFRAME - The thickness of the sizing border
+ // SM_CXPADDEDBORDER - The amount of border padding
+ // for captioned windows
+ //
+ // If the window does not have a caption, mHorResizeMargin will be equal to
+ // `WinUtils::GetSystemMetricsForDpi(SM_CXFRAME, dpi)`
+ mHorResizeMargin =
+ WinUtils::GetSystemMetricsForDpi(SM_CXFRAME, dpi) +
+ (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi)
+ : 0);
+
+ // mVertResizeMargin is the size of the default NC area at the
+ // bottom of the window. It is calculated as the sum of:
+ // SM_CYFRAME - The thickness of the sizing border
+ // SM_CXPADDEDBORDER - The amount of border padding
+ // for captioned windows.
+ //
+ // If the window does not have a caption, mVertResizeMargin will be equal to
+ // `WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi)`
+ mVertResizeMargin =
+ WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi) +
+ (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi)
+ : 0);
+ }
+
+ if (sizeMode == nsSizeMode_Minimized) {
+ // Use default frame size for minimized windows
+ mNonClientOffset.top = 0;
+ mNonClientOffset.left = 0;
+ mNonClientOffset.right = 0;
+ mNonClientOffset.bottom = 0;
+ } else if (sizeMode == nsSizeMode_Fullscreen) {
+ // Remove the default frame from the top of our fullscreen window. This
+ // makes the whole caption part of our client area, allowing us to draw
+ // in the whole caption area. Additionally remove the default frame from
+ // the left, right, and bottom.
+ mNonClientOffset.top = mCaptionHeight;
+ mNonClientOffset.bottom = mVertResizeMargin;
+ mNonClientOffset.left = mHorResizeMargin;
+ mNonClientOffset.right = mHorResizeMargin;
+ } else if (sizeMode == nsSizeMode_Maximized) {
+ // We make the entire frame part of the client area. We leave the default
+ // frame sizes for left, right and bottom since Windows will automagically
+ // position the edges "offscreen" for maximized windows.
+ int verticalResize =
+ WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi) +
+ (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi)
+ : 0);
+
+ mNonClientOffset.top = mCaptionHeight - verticalResize;
+ mNonClientOffset.bottom = 0;
+ mNonClientOffset.left = 0;
+ mNonClientOffset.right = 0;
+
+ mozilla::Maybe<UINT> maybeEdge = GetHiddenTaskbarEdge();
+ if (maybeEdge) {
+ auto edge = maybeEdge.value();
+ if (ABE_LEFT == edge) {
+ mNonClientOffset.left -= kHiddenTaskbarSize;
+ } else if (ABE_RIGHT == edge) {
+ mNonClientOffset.right -= kHiddenTaskbarSize;
+ } else if (ABE_BOTTOM == edge || ABE_TOP == edge) {
+ mNonClientOffset.bottom -= kHiddenTaskbarSize;
+ }
+
+ // When we are drawing the non-client region, we need
+ // to clear the portion of the NC region that is exposed by the
+ // hidden taskbar. As above, we clear the bottom of the NC region
+ // when the taskbar is at the top of the screen.
+ UINT clearEdge = (edge == ABE_TOP) ? ABE_BOTTOM : edge;
+ mClearNCEdge = Some(clearEdge);
+ }
+ } else {
+ mNonClientOffset = NormalWindowNonClientOffset();
+ }
+
+ if (aReflowWindow) {
+ // Force a reflow of content based on the new client
+ // dimensions.
+ ResetLayout();
+ }
+
+ return true;
+}
+
+nsresult nsWindow::SetNonClientMargins(const LayoutDeviceIntMargin& margins) {
+ if (!mIsTopWidgetWindow || mBorderStyle == BorderStyle::None)
+ return NS_ERROR_INVALID_ARG;
+
+ if (mHideChrome) {
+ mFutureMarginsOnceChromeShows = margins;
+ mFutureMarginsToUse = true;
+ return NS_OK;
+ }
+ mFutureMarginsToUse = false;
+
+ // Request for a reset
+ if (margins.top == -1 && margins.left == -1 && margins.right == -1 &&
+ margins.bottom == -1) {
+ mCustomNonClient = false;
+ mNonClientMargins = margins;
+ // Force a reflow of content based on the new client
+ // dimensions.
+ ResetLayout();
+
+ int windowStatus =
+ reinterpret_cast<LONG_PTR>(GetPropW(mWnd, kManageWindowInfoProperty));
+ if (windowStatus) {
+ ::SendMessageW(mWnd, WM_NCACTIVATE, 1 != windowStatus, 0);
+ }
+
+ return NS_OK;
+ }
+
+ if (margins.top < -1 || margins.bottom < -1 || margins.left < -1 ||
+ margins.right < -1)
+ return NS_ERROR_INVALID_ARG;
+
+ mNonClientMargins = margins;
+ mCustomNonClient = true;
+ if (!UpdateNonClientMargins()) {
+ NS_WARNING("UpdateNonClientMargins failed!");
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+void nsWindow::SetResizeMargin(mozilla::LayoutDeviceIntCoord aResizeMargin) {
+ mUseResizeMarginOverrides = true;
+ mHorResizeMargin = aResizeMargin;
+ mVertResizeMargin = aResizeMargin;
+ UpdateNonClientMargins();
+}
+
+void nsWindow::InvalidateNonClientRegion() {
+ // +-+-----------------------+-+
+ // | | app non-client chrome | |
+ // | +-----------------------+ |
+ // | | app client chrome | | }
+ // | +-----------------------+ | }
+ // | | app content | | } area we don't want to invalidate
+ // | +-----------------------+ | }
+ // | | app client chrome | | }
+ // | +-----------------------+ |
+ // +---------------------------+ <
+ // ^ ^ windows non-client chrome
+ // client area = app *
+ RECT rect;
+ GetWindowRect(mWnd, &rect);
+ MapWindowPoints(nullptr, mWnd, (LPPOINT)&rect, 2);
+ HRGN winRgn = CreateRectRgnIndirect(&rect);
+
+ // Subtract app client chrome and app content leaving
+ // windows non-client chrome and app non-client chrome
+ // in winRgn.
+ GetWindowRect(mWnd, &rect);
+ rect.top += mCaptionHeight;
+ rect.right -= mHorResizeMargin;
+ rect.bottom -= mVertResizeMargin;
+ rect.left += mHorResizeMargin;
+ MapWindowPoints(nullptr, mWnd, (LPPOINT)&rect, 2);
+ HRGN clientRgn = CreateRectRgnIndirect(&rect);
+ CombineRgn(winRgn, winRgn, clientRgn, RGN_DIFF);
+ DeleteObject(clientRgn);
+
+ // triggers ncpaint and paint events for the two areas
+ RedrawWindow(mWnd, nullptr, winRgn, RDW_FRAME | RDW_INVALIDATE);
+ DeleteObject(winRgn);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::SetBackgroundColor
+ *
+ * Sets the window background paint color.
+ *
+ **************************************************************/
+
+void nsWindow::SetBackgroundColor(const nscolor& aColor) {
+ if (mBrush) ::DeleteObject(mBrush);
+
+ mBrush = ::CreateSolidBrush(NSRGB_2_COLOREF(aColor));
+ if (mWnd != nullptr) {
+ ::SetClassLongPtrW(mWnd, GCLP_HBRBACKGROUND, (LONG_PTR)mBrush);
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::SetCursor
+ *
+ * SetCursor and related utilities for manging cursor state.
+ *
+ **************************************************************/
+
+// Set this component cursor
+static HCURSOR CursorFor(nsCursor aCursor) {
+ switch (aCursor) {
+ case eCursor_select:
+ return ::LoadCursor(nullptr, IDC_IBEAM);
+ case eCursor_wait:
+ return ::LoadCursor(nullptr, IDC_WAIT);
+ case eCursor_hyperlink:
+ return ::LoadCursor(nullptr, IDC_HAND);
+ case eCursor_standard:
+ case eCursor_context_menu: // XXX See bug 258960.
+ return ::LoadCursor(nullptr, IDC_ARROW);
+
+ case eCursor_n_resize:
+ case eCursor_s_resize:
+ return ::LoadCursor(nullptr, IDC_SIZENS);
+
+ case eCursor_w_resize:
+ case eCursor_e_resize:
+ return ::LoadCursor(nullptr, IDC_SIZEWE);
+
+ case eCursor_nw_resize:
+ case eCursor_se_resize:
+ return ::LoadCursor(nullptr, IDC_SIZENWSE);
+
+ case eCursor_ne_resize:
+ case eCursor_sw_resize:
+ return ::LoadCursor(nullptr, IDC_SIZENESW);
+
+ case eCursor_crosshair:
+ return ::LoadCursor(nullptr, IDC_CROSS);
+
+ case eCursor_move:
+ return ::LoadCursor(nullptr, IDC_SIZEALL);
+
+ case eCursor_help:
+ return ::LoadCursor(nullptr, IDC_HELP);
+
+ case eCursor_copy: // CSS3
+ return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_COPY));
+
+ case eCursor_alias:
+ return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_ALIAS));
+
+ case eCursor_cell:
+ return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_CELL));
+ case eCursor_grab:
+ return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_GRAB));
+
+ case eCursor_grabbing:
+ return ::LoadCursor(nsToolkit::mDllInstance,
+ MAKEINTRESOURCE(IDC_GRABBING));
+
+ case eCursor_spinning:
+ return ::LoadCursor(nullptr, IDC_APPSTARTING);
+
+ case eCursor_zoom_in:
+ return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_ZOOMIN));
+
+ case eCursor_zoom_out:
+ return ::LoadCursor(nsToolkit::mDllInstance,
+ MAKEINTRESOURCE(IDC_ZOOMOUT));
+
+ case eCursor_not_allowed:
+ case eCursor_no_drop:
+ return ::LoadCursor(nullptr, IDC_NO);
+
+ case eCursor_col_resize:
+ return ::LoadCursor(nsToolkit::mDllInstance,
+ MAKEINTRESOURCE(IDC_COLRESIZE));
+
+ case eCursor_row_resize:
+ return ::LoadCursor(nsToolkit::mDllInstance,
+ MAKEINTRESOURCE(IDC_ROWRESIZE));
+
+ case eCursor_vertical_text:
+ return ::LoadCursor(nsToolkit::mDllInstance,
+ MAKEINTRESOURCE(IDC_VERTICALTEXT));
+
+ case eCursor_all_scroll:
+ // XXX not 100% appropriate perhaps
+ return ::LoadCursor(nullptr, IDC_SIZEALL);
+
+ case eCursor_nesw_resize:
+ return ::LoadCursor(nullptr, IDC_SIZENESW);
+
+ case eCursor_nwse_resize:
+ return ::LoadCursor(nullptr, IDC_SIZENWSE);
+
+ case eCursor_ns_resize:
+ return ::LoadCursor(nullptr, IDC_SIZENS);
+
+ case eCursor_ew_resize:
+ return ::LoadCursor(nullptr, IDC_SIZEWE);
+
+ case eCursor_none:
+ return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_NONE));
+
+ default:
+ NS_ERROR("Invalid cursor type");
+ return nullptr;
+ }
+}
+
+static HCURSOR CursorForImage(const nsIWidget::Cursor& aCursor,
+ CSSToLayoutDeviceScale aScale) {
+ if (!aCursor.IsCustom()) {
+ return nullptr;
+ }
+
+ nsIntSize size = nsIWidget::CustomCursorSize(aCursor);
+
+ // Reject cursors greater than 128 pixels in either direction, to prevent
+ // spoofing.
+ // XXX ideally we should rescale. Also, we could modify the API to
+ // allow trusted content to set larger cursors.
+ if (size.width > 128 || size.height > 128) {
+ return nullptr;
+ }
+
+ LayoutDeviceIntSize layoutSize =
+ RoundedToInt(CSSIntSize(size.width, size.height) * aScale);
+ LayoutDeviceIntPoint hotspot =
+ RoundedToInt(CSSIntPoint(aCursor.mHotspotX, aCursor.mHotspotY) * aScale);
+ HCURSOR cursor;
+ nsresult rv = nsWindowGfx::CreateIcon(aCursor.mContainer, true, hotspot,
+ layoutSize, &cursor);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ return cursor;
+}
+
+void nsWindow::SetCursor(const Cursor& aCursor) {
+ static HCURSOR sCurrentHCursor = nullptr;
+ static bool sCurrentHCursorIsCustom = false;
+
+ mCursor = aCursor;
+
+ if (sCurrentCursor == aCursor && sCurrentHCursor && !mUpdateCursor) {
+ // Cursors in windows are global, so even if our mUpdateCursor flag is
+ // false we always need to make sure the Windows cursor is up-to-date,
+ // since stuff like native drag and drop / resizers code can mutate it
+ // outside of this method.
+ ::SetCursor(sCurrentHCursor);
+ return;
+ }
+
+ mUpdateCursor = false;
+
+ if (sCurrentHCursorIsCustom) {
+ ::DestroyIcon(sCurrentHCursor);
+ }
+ sCurrentHCursor = nullptr;
+ sCurrentHCursorIsCustom = false;
+ sCurrentCursor = aCursor;
+
+ HCURSOR cursor = nullptr;
+ if (mCustomCursorAllowed) {
+ cursor = CursorForImage(aCursor, GetDefaultScale());
+ }
+ bool custom = false;
+ if (cursor) {
+ custom = true;
+ } else {
+ cursor = CursorFor(aCursor.mDefaultCursor);
+ }
+
+ if (!cursor) {
+ return;
+ }
+
+ sCurrentHCursor = cursor;
+ sCurrentHCursorIsCustom = custom;
+ ::SetCursor(cursor);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::Get/SetTransparencyMode
+ *
+ * Manage the transparency mode of the window containing this
+ * widget. Only works for popup and dialog windows when the
+ * Desktop Window Manager compositor is not enabled.
+ *
+ **************************************************************/
+
+TransparencyMode nsWindow::GetTransparencyMode() {
+ return GetTopLevelWindow(true)->GetWindowTranslucencyInner();
+}
+
+void nsWindow::SetTransparencyMode(TransparencyMode aMode) {
+ nsWindow* window = GetTopLevelWindow(true);
+ MOZ_ASSERT(window);
+
+ if (!window || window->DestroyCalled()) {
+ return;
+ }
+
+ window->SetWindowTranslucencyInner(aMode);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::UpdateWindowDraggingRegion
+ *
+ * For setting the draggable titlebar region from CSS
+ * with -moz-window-dragging: drag.
+ *
+ **************************************************************/
+
+void nsWindow::UpdateWindowDraggingRegion(
+ const LayoutDeviceIntRegion& aRegion) {
+ if (mDraggableRegion != aRegion) {
+ mDraggableRegion = aRegion;
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::HideWindowChrome
+ *
+ * Show or hide window chrome.
+ *
+ **************************************************************/
+
+void nsWindow::HideWindowChrome(bool aShouldHide) {
+ HWND hwnd = WinUtils::GetTopLevelHWND(mWnd, true);
+ if (!WinUtils::GetNSWindowPtr(hwnd)) {
+ NS_WARNING("Trying to hide window decorations in an embedded context");
+ return;
+ }
+
+ if (mHideChrome == aShouldHide) return;
+
+ DWORD_PTR style, exStyle;
+ mHideChrome = aShouldHide;
+ if (aShouldHide) {
+ DWORD_PTR tempStyle = ::GetWindowLongPtrW(hwnd, GWL_STYLE);
+ DWORD_PTR tempExStyle = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
+
+ style = tempStyle & ~(WS_CAPTION | WS_THICKFRAME);
+ exStyle = tempExStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE |
+ WS_EX_CLIENTEDGE | WS_EX_STATICEDGE);
+
+ mOldStyle = tempStyle;
+ mOldExStyle = tempExStyle;
+ } else {
+ if (!mOldStyle || !mOldExStyle) {
+ mOldStyle = ::GetWindowLongPtrW(hwnd, GWL_STYLE);
+ mOldExStyle = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
+ }
+
+ style = mOldStyle;
+ exStyle = mOldExStyle;
+ if (mFutureMarginsToUse) {
+ SetNonClientMargins(mFutureMarginsOnceChromeShows);
+ }
+ }
+
+ VERIFY_WINDOW_STYLE(style);
+ ::SetWindowLongPtrW(hwnd, GWL_STYLE, style);
+ ::SetWindowLongPtrW(hwnd, GWL_EXSTYLE, exStyle);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsWindow::Invalidate
+ *
+ * Invalidate an area of the client for painting.
+ *
+ **************************************************************/
+
+// Invalidate this component visible area
+void nsWindow::Invalidate(bool aEraseBackground, bool aUpdateNCArea,
+ bool aIncludeChildren) {
+ if (!mWnd) {
+ return;
+ }
+
+#ifdef WIDGET_DEBUG_OUTPUT
+ debug_DumpInvalidate(stdout, this, nullptr, "noname", (int32_t)mWnd);
+#endif // WIDGET_DEBUG_OUTPUT
+
+ DWORD flags = RDW_INVALIDATE;
+ if (aEraseBackground) {
+ flags |= RDW_ERASE;
+ }
+ if (aUpdateNCArea) {
+ flags |= RDW_FRAME;
+ }
+ if (aIncludeChildren) {
+ flags |= RDW_ALLCHILDREN;
+ }
+
+ VERIFY(::RedrawWindow(mWnd, nullptr, nullptr, flags));
+}
+
+// Invalidate this component visible area
+void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) {
+ if (mWnd) {
+#ifdef WIDGET_DEBUG_OUTPUT
+ debug_DumpInvalidate(stdout, this, &aRect, "noname", (int32_t)mWnd);
+#endif // WIDGET_DEBUG_OUTPUT
+
+ RECT rect;
+
+ rect.left = aRect.X();
+ rect.top = aRect.Y();
+ rect.right = aRect.XMost();
+ rect.bottom = aRect.YMost();
+
+ VERIFY(::InvalidateRect(mWnd, &rect, FALSE));
+ }
+}
+
+static LRESULT CALLBACK FullscreenTransitionWindowProc(HWND hWnd, UINT uMsg,
+ WPARAM wParam,
+ LPARAM lParam) {
+ switch (uMsg) {
+ case WM_FULLSCREEN_TRANSITION_BEFORE:
+ case WM_FULLSCREEN_TRANSITION_AFTER: {
+ DWORD duration = (DWORD)lParam;
+ DWORD flags = AW_BLEND;
+ if (uMsg == WM_FULLSCREEN_TRANSITION_AFTER) {
+ flags |= AW_HIDE;
+ }
+ ::AnimateWindow(hWnd, duration, flags);
+ // The message sender should have added ref for us.
+ NS_DispatchToMainThread(
+ already_AddRefed<nsIRunnable>((nsIRunnable*)wParam));
+ break;
+ }
+ case WM_DESTROY:
+ ::PostQuitMessage(0);
+ break;
+ default:
+ return ::DefWindowProcW(hWnd, uMsg, wParam, lParam);
+ }
+ return 0;
+}
+
+struct FullscreenTransitionInitData {
+ LayoutDeviceIntRect mBounds;
+ HANDLE mSemaphore;
+ HANDLE mThread;
+ HWND mWnd;
+
+ FullscreenTransitionInitData()
+ : mSemaphore(nullptr), mThread(nullptr), mWnd(nullptr) {}
+
+ ~FullscreenTransitionInitData() {
+ if (mSemaphore) {
+ ::CloseHandle(mSemaphore);
+ }
+ if (mThread) {
+ ::CloseHandle(mThread);
+ }
+ }
+};
+
+static DWORD WINAPI FullscreenTransitionThreadProc(LPVOID lpParam) {
+ // Initialize window class
+ static bool sInitialized = false;
+ if (!sInitialized) {
+ WNDCLASSW wc = {};
+ wc.lpfnWndProc = ::FullscreenTransitionWindowProc;
+ wc.hInstance = nsToolkit::mDllInstance;
+ wc.hbrBackground = ::CreateSolidBrush(RGB(0, 0, 0));
+ wc.lpszClassName = kClassNameTransition;
+ ::RegisterClassW(&wc);
+ sInitialized = true;
+ }
+
+ auto data = static_cast<FullscreenTransitionInitData*>(lpParam);
+ HWND wnd = ::CreateWindowW(kClassNameTransition, L"", 0, 0, 0, 0, 0, nullptr,
+ nullptr, nsToolkit::mDllInstance, nullptr);
+ if (!wnd) {
+ ::ReleaseSemaphore(data->mSemaphore, 1, nullptr);
+ return 0;
+ }
+
+ // Since AnimateWindow blocks the thread of the transition window,
+ // we need to hide the cursor for that window, otherwise the system
+ // would show the busy pointer to the user.
+ ::ShowCursor(false);
+ ::SetWindowLongW(wnd, GWL_STYLE, 0);
+ ::SetWindowLongW(
+ wnd, GWL_EXSTYLE,
+ WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE);
+ ::SetWindowPos(wnd, HWND_TOPMOST, data->mBounds.X(), data->mBounds.Y(),
+ data->mBounds.Width(), data->mBounds.Height(), 0);
+ data->mWnd = wnd;
+ ::ReleaseSemaphore(data->mSemaphore, 1, nullptr);
+ // The initialization data may no longer be valid
+ // after we release the semaphore.
+ data = nullptr;
+
+ MSG msg;
+ while (::GetMessageW(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+ ::ShowCursor(true);
+ ::DestroyWindow(wnd);
+ return 0;
+}
+
+class FullscreenTransitionData final : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit FullscreenTransitionData(HWND aWnd) : mWnd(aWnd) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "FullscreenTransitionData "
+ "should be constructed in the main thread");
+ }
+
+ const HWND mWnd;
+
+ private:
+ ~FullscreenTransitionData() {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "FullscreenTransitionData "
+ "should be deconstructed in the main thread");
+ ::PostMessageW(mWnd, WM_DESTROY, 0, 0);
+ }
+};
+
+NS_IMPL_ISUPPORTS0(FullscreenTransitionData)
+
+/* virtual */
+bool nsWindow::PrepareForFullscreenTransition(nsISupports** aData) {
+ FullscreenTransitionInitData initData;
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ const DesktopIntRect rect = screen->GetRectDisplayPix();
+ MOZ_ASSERT(BoundsUseDesktopPixels(),
+ "Should only be called on top-level window");
+ initData.mBounds =
+ LayoutDeviceIntRect::Round(rect * GetDesktopToDeviceScale());
+
+ // Create a semaphore for synchronizing the window handle which will
+ // be created by the transition thread and used by the main thread for
+ // posting the transition messages.
+ initData.mSemaphore = ::CreateSemaphore(nullptr, 0, 1, nullptr);
+ if (initData.mSemaphore) {
+ initData.mThread = ::CreateThread(
+ nullptr, 0, FullscreenTransitionThreadProc, &initData, 0, nullptr);
+ if (initData.mThread) {
+ ::WaitForSingleObject(initData.mSemaphore, INFINITE);
+ }
+ }
+ if (!initData.mWnd) {
+ return false;
+ }
+
+ mTransitionWnd = initData.mWnd;
+
+ auto data = new FullscreenTransitionData(initData.mWnd);
+ *aData = data;
+ NS_ADDREF(data);
+ return true;
+}
+
+/* virtual */
+void nsWindow::PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration,
+ nsISupports* aData,
+ nsIRunnable* aCallback) {
+ auto data = static_cast<FullscreenTransitionData*>(aData);
+ nsCOMPtr<nsIRunnable> callback = aCallback;
+ UINT msg = aStage == eBeforeFullscreenToggle ? WM_FULLSCREEN_TRANSITION_BEFORE
+ : WM_FULLSCREEN_TRANSITION_AFTER;
+ WPARAM wparam = (WPARAM)callback.forget().take();
+ ::PostMessage(data->mWnd, msg, wparam, (LPARAM)aDuration);
+}
+
+/* virtual */
+void nsWindow::CleanupFullscreenTransition() {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "CleanupFullscreenTransition "
+ "should only run on the main thread");
+
+ mTransitionWnd = nullptr;
+}
+
+void nsWindow::TryDwmResizeHack() {
+ // The "DWM resize hack", aka the "fullscreen resize hack", is a workaround
+ // for DWM's occasional and not-entirely-predictable failure to update its
+ // internal state when the client area of a window changes without changing
+ // the window size. The effect of this is that DWM will clip the content of
+ // the window to its former client area.
+ //
+ // It is not known under what circumstances the bug will trigger. Windows 11
+ // is known to be required, but many Windows 11 machines do not exhibit the
+ // issue. Even machines that _do_ exhibit it will sometimes not do so when
+ // apparently-irrelevant changes are made to the configuration. (See bug
+ // 1763981.)
+ //
+ // The bug is triggered by Firefox when a maximized window (which has window
+ // decorations) becomes fullscreen (which doesn't). To work around this, if we
+ // think it may occur, we "flicker-resize" the relevant window -- that is, we
+ // reduce its height by 1px, then restore it. This causes DWM to acquire the
+ // new client-area metrics.
+ //
+ // Note that, in particular, this bug will not occur when using a separate
+ // compositor window, as our compositor windows never have any nonclient area.
+ //
+ // This is admittedly a sledgehammer where a screwdriver should suffice.
+
+ // ---------------------------------------------------------------------------
+
+ // Regardless of preferences or heuristics, only apply the hack if this is the
+ // first time we've entered fullscreen across the entire Firefox session.
+ // (Subsequent transitions to fullscreen, even with different windows, don't
+ // appear to induce the bug.)
+ {
+ // (main thread only; `atomic` not needed)
+ static bool sIsFirstFullscreenEntry = true;
+ bool isFirstFullscreenEntry = sIsFirstFullscreenEntry;
+ sIsFirstFullscreenEntry = false;
+ if (MOZ_LIKELY(!isFirstFullscreenEntry)) {
+ return;
+ }
+ MOZ_LOG(gWindowsLog, LogLevel::Verbose,
+ ("%s: first fullscreen entry", __PRETTY_FUNCTION__));
+ }
+
+ // Check whether to try to apply the DWM resize hack, based on the override
+ // pref and/or some internal heuristics.
+ {
+ const auto hackApplicationHeuristics = [&]() -> bool {
+ // The bug has only been seen under Windows 11. (At time of writing, this
+ // is the latest version of Windows.)
+ if (!IsWin11OrLater()) {
+ return false;
+ }
+
+ KnowsCompositor const* const kc = mWindowRenderer->AsKnowsCompositor();
+ // This should never happen...
+ MOZ_ASSERT(kc);
+ // ... so if it does, we are in uncharted territory: don't apply the hack.
+ if (!kc) {
+ return false;
+ }
+
+ // The bug doesn't occur when we're using a separate compositor window
+ // (since the compositor window always comprises exactly its client area,
+ // with no non-client border).
+ if (kc->GetUseCompositorWnd()) {
+ return false;
+ }
+
+ // Otherwise, apply the hack.
+ return true;
+ };
+
+ // Figure out whether or not we should perform the hack, and -- arguably
+ // more importantly -- log that decision.
+ bool const shouldApplyHack = [&]() {
+ enum Reason : bool { Pref, Heuristics };
+ auto const msg = [&](bool decision, Reason reason) -> bool {
+ MOZ_LOG(gWindowsLog, LogLevel::Verbose,
+ ("%s %s per %s", decision ? "applying" : "skipping",
+ "DWM resize hack", reason == Pref ? "pref" : "heuristics"));
+ return decision;
+ };
+ switch (StaticPrefs::widget_windows_apply_dwm_resize_hack()) {
+ case 0:
+ return msg(false, Pref);
+ case 1:
+ return msg(true, Pref);
+ default: // treat all other values as `auto`
+ return msg(hackApplicationHeuristics(), Heuristics);
+ }
+ }();
+
+ if (!shouldApplyHack) {
+ return;
+ }
+ }
+
+ // The DWM bug is believed to involve a race condition: some users have
+ // reported that setting a custom theme or adding unused command-line
+ // parameters sometimes causes the bug to vanish.
+ //
+ // Out of an abundance of caution, we therefore apply the hack in a later
+ // event, rather than inline.
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "nsWindow::TryFullscreenResizeHack", [self = RefPtr(this)]() {
+ HWND const hwnd = self->GetWindowHandle();
+
+ if (self->mFrameState->GetSizeMode() != nsSizeMode_Fullscreen) {
+ MOZ_LOG(gWindowsLog, mozilla::LogLevel::Info,
+ ("DWM resize hack: window no longer fullscreen; aborting"));
+ return;
+ }
+
+ RECT origRect;
+ if (!::GetWindowRect(hwnd, &origRect)) {
+ MOZ_LOG(gWindowsLog, mozilla::LogLevel::Error,
+ ("DWM resize hack: could not get window size?!"));
+ return;
+ }
+ LONG const x = origRect.left;
+ LONG const y = origRect.top;
+ LONG const width = origRect.right - origRect.left;
+ LONG const height = origRect.bottom - origRect.top;
+
+ MOZ_DIAGNOSTIC_ASSERT(!self->mIsPerformingDwmFlushHack);
+ auto const onExit =
+ MakeScopeExit([&, oldVal = self->mIsPerformingDwmFlushHack]() {
+ self->mIsPerformingDwmFlushHack = oldVal;
+ });
+ self->mIsPerformingDwmFlushHack = true;
+
+ MOZ_LOG(gWindowsLog, LogLevel::Debug,
+ ("beginning DWM resize hack for HWND %08" PRIXPTR,
+ uintptr_t(hwnd)));
+ ::MoveWindow(hwnd, x, y, width, height - 1, FALSE);
+ ::MoveWindow(hwnd, x, y, width, height, TRUE);
+ MOZ_LOG(gWindowsLog, LogLevel::Debug,
+ ("concluded DWM resize hack for HWND %08" PRIXPTR,
+ uintptr_t(hwnd)));
+ }));
+}
+
+void nsWindow::OnFullscreenChanged(nsSizeMode aOldSizeMode, bool aFullScreen) {
+ MOZ_ASSERT((aOldSizeMode != nsSizeMode_Fullscreen) == aFullScreen);
+
+ // HACK: Potentially flicker-resize the window, to force DWM to get the right
+ // client-area information.
+ if (aFullScreen) {
+ TryDwmResizeHack();
+ }
+
+ // Hide chrome and reposition window. Note this will also cache dimensions for
+ // restoration, so it should only be called once per fullscreen request.
+ //
+ // Don't do this when minimized, since our bounds make no sense then, nor when
+ // coming back from that state.
+ const bool toOrFromMinimized =
+ mFrameState->GetSizeMode() == nsSizeMode_Minimized ||
+ aOldSizeMode == nsSizeMode_Minimized;
+ if (!toOrFromMinimized) {
+ InfallibleMakeFullScreen(aFullScreen);
+ }
+
+ // Possibly notify the taskbar that we have changed our fullscreen mode.
+ TaskbarConcealer::OnFullscreenChanged(this, aFullScreen);
+}
+
+nsresult nsWindow::MakeFullScreen(bool aFullScreen) {
+ mFrameState->EnsureFullscreenMode(aFullScreen);
+ return NS_OK;
+}
+
+/**************************************************************
+ *
+ * SECTION: Native data storage
+ *
+ * nsIWidget::GetNativeData
+ * nsIWidget::FreeNativeData
+ *
+ * Set or clear native data based on a constant.
+ *
+ **************************************************************/
+
+// Return some native data according to aDataType
+void* nsWindow::GetNativeData(uint32_t aDataType) {
+ switch (aDataType) {
+ case NS_NATIVE_WIDGET:
+ case NS_NATIVE_WINDOW:
+ case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID:
+ return (void*)mWnd;
+ case NS_NATIVE_GRAPHIC:
+ MOZ_ASSERT_UNREACHABLE("Not supported on Windows:");
+ return nullptr;
+ case NS_RAW_NATIVE_IME_CONTEXT: {
+ void* pseudoIMEContext = GetPseudoIMEContext();
+ if (pseudoIMEContext) {
+ return pseudoIMEContext;
+ }
+ [[fallthrough]];
+ }
+ case NS_NATIVE_TSF_THREAD_MGR:
+ case NS_NATIVE_TSF_CATEGORY_MGR:
+ case NS_NATIVE_TSF_DISPLAY_ATTR_MGR:
+ return IMEHandler::GetNativeData(this, aDataType);
+
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+// Free some native data according to aDataType
+void nsWindow::FreeNativeData(void* data, uint32_t aDataType) {
+ switch (aDataType) {
+ case NS_NATIVE_GRAPHIC:
+ case NS_NATIVE_WIDGET:
+ case NS_NATIVE_WINDOW:
+ break;
+ default:
+ break;
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::SetTitle
+ *
+ * Set the main windows title text.
+ *
+ **************************************************************/
+
+nsresult nsWindow::SetTitle(const nsAString& aTitle) {
+ const nsString& strTitle = PromiseFlatString(aTitle);
+ AutoRestore<bool> sendingText(mSendingSetText);
+ mSendingSetText = true;
+ ::SendMessageW(mWnd, WM_SETTEXT, (WPARAM)0, (LPARAM)(LPCWSTR)strTitle.get());
+ return NS_OK;
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::SetIcon
+ *
+ * Set the main windows icon.
+ *
+ **************************************************************/
+
+void nsWindow::SetBigIcon(HICON aIcon) {
+ HICON icon =
+ (HICON)::SendMessageW(mWnd, WM_SETICON, (WPARAM)ICON_BIG, (LPARAM)aIcon);
+ if (icon) {
+ ::DestroyIcon(icon);
+ }
+
+ mIconBig = aIcon;
+}
+
+void nsWindow::SetSmallIcon(HICON aIcon) {
+ HICON icon = (HICON)::SendMessageW(mWnd, WM_SETICON, (WPARAM)ICON_SMALL,
+ (LPARAM)aIcon);
+ if (icon) {
+ ::DestroyIcon(icon);
+ }
+
+ mIconSmall = aIcon;
+}
+
+void nsWindow::SetIcon(const nsAString& aIconSpec) {
+ // Assume the given string is a local identifier for an icon file.
+
+ nsCOMPtr<nsIFile> iconFile;
+ ResolveIconName(aIconSpec, u".ico"_ns, getter_AddRefs(iconFile));
+ if (!iconFile) return;
+
+ nsAutoString iconPath;
+ iconFile->GetPath(iconPath);
+
+ // XXX this should use MZLU (see bug 239279)
+
+ ::SetLastError(0);
+
+ HICON bigIcon =
+ (HICON)::LoadImageW(nullptr, (LPCWSTR)iconPath.get(), IMAGE_ICON,
+ ::GetSystemMetrics(SM_CXICON),
+ ::GetSystemMetrics(SM_CYICON), LR_LOADFROMFILE);
+ HICON smallIcon =
+ (HICON)::LoadImageW(nullptr, (LPCWSTR)iconPath.get(), IMAGE_ICON,
+ ::GetSystemMetrics(SM_CXSMICON),
+ ::GetSystemMetrics(SM_CYSMICON), LR_LOADFROMFILE);
+
+ if (bigIcon) {
+ SetBigIcon(bigIcon);
+ }
+#ifdef DEBUG_SetIcon
+ else {
+ NS_LossyConvertUTF16toASCII cPath(iconPath);
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("\nIcon load error; icon=%s, rc=0x%08X\n\n", cPath.get(),
+ ::GetLastError()));
+ }
+#endif
+ if (smallIcon) {
+ SetSmallIcon(smallIcon);
+ }
+#ifdef DEBUG_SetIcon
+ else {
+ NS_LossyConvertUTF16toASCII cPath(iconPath);
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("\nSmall icon load error; icon=%s, rc=0x%08X\n\n", cPath.get(),
+ ::GetLastError()));
+ }
+#endif
+}
+
+void nsWindow::SetBigIconNoData() {
+ HICON bigIcon =
+ ::LoadIconW(::GetModuleHandleW(nullptr), gStockApplicationIcon);
+ SetBigIcon(bigIcon);
+}
+
+void nsWindow::SetSmallIconNoData() {
+ HICON smallIcon =
+ ::LoadIconW(::GetModuleHandleW(nullptr), gStockApplicationIcon);
+ SetSmallIcon(smallIcon);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::WidgetToScreenOffset
+ *
+ * Return this widget's origin in screen coordinates.
+ *
+ **************************************************************/
+
+LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() {
+ POINT point;
+ point.x = 0;
+ point.y = 0;
+ ::ClientToScreen(mWnd, &point);
+ return LayoutDeviceIntPoint(point.x, point.y);
+}
+
+LayoutDeviceIntMargin nsWindow::ClientToWindowMargin() {
+ if (mWindowType == WindowType::Popup && !IsPopupWithTitleBar()) {
+ return {};
+ }
+
+ if (mCustomNonClient) {
+ return NonClientSizeMargin(NormalWindowNonClientOffset());
+ }
+
+ // Just use a dummy 200x200 at (200, 200) client rect as the rect.
+ RECT clientRect;
+ clientRect.left = 200;
+ clientRect.top = 200;
+ clientRect.right = 400;
+ clientRect.bottom = 400;
+
+ auto ToRect = [](const RECT& aRect) -> LayoutDeviceIntRect {
+ return {aRect.left, aRect.top, aRect.right - aRect.left,
+ aRect.bottom - aRect.top};
+ };
+
+ RECT windowRect = clientRect;
+ ::AdjustWindowRectEx(&windowRect, WindowStyle(), false, WindowExStyle());
+
+ return ToRect(windowRect) - ToRect(clientRect);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::EnableDragDrop
+ *
+ * Enables/Disables drag and drop of files on this widget.
+ *
+ **************************************************************/
+
+void nsWindow::EnableDragDrop(bool aEnable) {
+ if (!mWnd) {
+ // Return early if the window already closed
+ return;
+ }
+
+ if (aEnable) {
+ if (!mNativeDragTarget) {
+ mNativeDragTarget = new nsNativeDragTarget(this);
+ mNativeDragTarget->AddRef();
+ ::RegisterDragDrop(mWnd, (LPDROPTARGET)mNativeDragTarget);
+ }
+ } else {
+ if (mWnd && mNativeDragTarget) {
+ ::RevokeDragDrop(mWnd);
+ mNativeDragTarget->DragCancel();
+ NS_RELEASE(mNativeDragTarget);
+ }
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::CaptureMouse
+ *
+ * Enables/Disables system mouse capture.
+ *
+ **************************************************************/
+
+void nsWindow::CaptureMouse(bool aCapture) {
+ TRACKMOUSEEVENT mTrack;
+ mTrack.cbSize = sizeof(TRACKMOUSEEVENT);
+ mTrack.dwHoverTime = 0;
+ mTrack.hwndTrack = mWnd;
+ if (aCapture) {
+ mTrack.dwFlags = TME_CANCEL | TME_LEAVE;
+ ::SetCapture(mWnd);
+ } else {
+ mTrack.dwFlags = TME_LEAVE;
+ ::ReleaseCapture();
+ }
+ sIsInMouseCapture = aCapture;
+ TrackMouseEvent(&mTrack);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::CaptureRollupEvents
+ *
+ * Dealing with event rollup on destroy for popups. Enables &
+ * Disables system capture of any and all events that would
+ * cause a dropdown to be rolled up.
+ *
+ **************************************************************/
+
+void nsWindow::CaptureRollupEvents(bool aDoCapture) {
+ if (aDoCapture) {
+ if (!sMsgFilterHook && !sCallProcHook && !sCallMouseHook) {
+ RegisterSpecialDropdownHooks();
+ }
+ sProcessHook = true;
+ } else {
+ sProcessHook = false;
+ UnregisterSpecialDropdownHooks();
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::GetAttention
+ *
+ * Bring this window to the user's attention.
+ *
+ **************************************************************/
+
+// Draw user's attention to this window until it comes to foreground.
+nsresult nsWindow::GetAttention(int32_t aCycleCount) {
+ // Got window?
+ if (!mWnd) return NS_ERROR_NOT_INITIALIZED;
+
+ HWND flashWnd = WinUtils::GetTopLevelHWND(mWnd, false, false);
+ HWND fgWnd = ::GetForegroundWindow();
+ // Don't flash if the flash count is 0 or if the foreground window is our
+ // window handle or that of our owned-most window.
+ if (aCycleCount == 0 || flashWnd == fgWnd ||
+ flashWnd == WinUtils::GetTopLevelHWND(fgWnd, false, false)) {
+ return NS_OK;
+ }
+
+ DWORD defaultCycleCount = 0;
+ ::SystemParametersInfo(SPI_GETFOREGROUNDFLASHCOUNT, 0, &defaultCycleCount, 0);
+
+ FLASHWINFO flashInfo = {sizeof(FLASHWINFO), flashWnd, FLASHW_ALL,
+ aCycleCount > 0 ? aCycleCount : defaultCycleCount, 0};
+ ::FlashWindowEx(&flashInfo);
+
+ return NS_OK;
+}
+
+void nsWindow::StopFlashing() {
+ HWND flashWnd = mWnd;
+ while (HWND ownerWnd = ::GetWindow(flashWnd, GW_OWNER)) {
+ flashWnd = ownerWnd;
+ }
+
+ FLASHWINFO flashInfo = {sizeof(FLASHWINFO), flashWnd, FLASHW_STOP, 0, 0};
+ ::FlashWindowEx(&flashInfo);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::HasPendingInputEvent
+ *
+ * Ask whether there user input events pending. All input events are
+ * included, including those not targeted at this nsIwidget instance.
+ *
+ **************************************************************/
+
+bool nsWindow::HasPendingInputEvent() {
+ // If there is pending input or the user is currently
+ // moving the window then return true.
+ // Note: When the user is moving the window WIN32 spins
+ // a separate event loop and input events are not
+ // reported to the application.
+ if (HIWORD(GetQueueStatus(QS_INPUT))) return true;
+ GUITHREADINFO guiInfo;
+ guiInfo.cbSize = sizeof(GUITHREADINFO);
+ if (!GetGUIThreadInfo(GetCurrentThreadId(), &guiInfo)) return false;
+ return GUI_INMOVESIZE == (guiInfo.flags & GUI_INMOVESIZE);
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::GetWindowRenderer
+ *
+ * Get the window renderer associated with this widget.
+ *
+ **************************************************************/
+
+WindowRenderer* nsWindow::GetWindowRenderer() {
+ if (mWindowRenderer) {
+ return mWindowRenderer;
+ }
+
+ if (!mLocalesChangedObserver) {
+ mLocalesChangedObserver = new LocalesChangedObserver(this);
+ }
+
+ // Try OMTC first.
+ if (!mWindowRenderer && ShouldUseOffMainThreadCompositing()) {
+ gfxWindowsPlatform::GetPlatform()->UpdateRenderMode();
+ CreateCompositor();
+ }
+
+ if (!mWindowRenderer) {
+ MOZ_ASSERT(!mCompositorSession && !mCompositorBridgeChild);
+ MOZ_ASSERT(!mCompositorWidgetDelegate);
+
+ // Ensure we have a widget proxy even if we're not using the compositor,
+ // since all our transparent window handling lives there.
+ WinCompositorWidgetInitData initData(
+ reinterpret_cast<uintptr_t>(mWnd),
+ reinterpret_cast<uintptr_t>(static_cast<nsIWidget*>(this)),
+ mTransparencyMode, mFrameState->GetSizeMode());
+ // If we're not using the compositor, the options don't actually matter.
+ CompositorOptions options(false, false);
+ mBasicLayersSurface =
+ new InProcessWinCompositorWidget(initData, options, this);
+ mCompositorWidgetDelegate = mBasicLayersSurface;
+ mWindowRenderer = CreateFallbackRenderer();
+ }
+
+ NS_ASSERTION(mWindowRenderer, "Couldn't provide a valid window renderer.");
+
+ if (mWindowRenderer) {
+ // Update the size constraints now that the layer manager has been
+ // created.
+ KnowsCompositor* knowsCompositor = mWindowRenderer->AsKnowsCompositor();
+ if (knowsCompositor) {
+ SizeConstraints c = mSizeConstraints;
+ mMaxTextureSize = knowsCompositor->GetMaxTextureSize();
+ c.mMaxSize.width = std::min(c.mMaxSize.width, mMaxTextureSize);
+ c.mMaxSize.height = std::min(c.mMaxSize.height, mMaxTextureSize);
+ nsBaseWidget::SetSizeConstraints(c);
+ }
+ }
+
+ return mWindowRenderer;
+}
+
+/**************************************************************
+ *
+ * SECTION: nsBaseWidget::SetCompositorWidgetDelegate
+ *
+ * Called to connect the nsWindow to the delegate providing
+ * platform compositing API access.
+ *
+ **************************************************************/
+
+void nsWindow::SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) {
+ if (delegate) {
+ mCompositorWidgetDelegate = delegate->AsPlatformSpecificDelegate();
+ MOZ_ASSERT(mCompositorWidgetDelegate,
+ "nsWindow::SetCompositorWidgetDelegate called with a "
+ "non-PlatformCompositorWidgetDelegate");
+ } else {
+ mCompositorWidgetDelegate = nullptr;
+ }
+}
+
+/**************************************************************
+ *
+ * SECTION: nsIWidget::OnDefaultButtonLoaded
+ *
+ * Called after the dialog is loaded and it has a default button.
+ *
+ **************************************************************/
+
+nsresult nsWindow::OnDefaultButtonLoaded(
+ const LayoutDeviceIntRect& aButtonRect) {
+ if (aButtonRect.IsEmpty()) return NS_OK;
+
+ // Don't snap when we are not active.
+ HWND activeWnd = ::GetActiveWindow();
+ if (activeWnd != ::GetForegroundWindow() ||
+ WinUtils::GetTopLevelHWND(mWnd, true) !=
+ WinUtils::GetTopLevelHWND(activeWnd, true)) {
+ return NS_OK;
+ }
+
+ bool isAlwaysSnapCursor =
+ Preferences::GetBool("ui.cursor_snapping.always_enabled", false);
+
+ if (!isAlwaysSnapCursor) {
+ BOOL snapDefaultButton;
+ if (!::SystemParametersInfo(SPI_GETSNAPTODEFBUTTON, 0, &snapDefaultButton,
+ 0) ||
+ !snapDefaultButton)
+ return NS_OK;
+ }
+
+ LayoutDeviceIntRect widgetRect = GetScreenBounds();
+ LayoutDeviceIntRect buttonRect(aButtonRect + widgetRect.TopLeft());
+
+ LayoutDeviceIntPoint centerOfButton(buttonRect.X() + buttonRect.Width() / 2,
+ buttonRect.Y() + buttonRect.Height() / 2);
+ // The center of the button can be outside of the widget.
+ // E.g., it could be hidden by scrolling.
+ if (!widgetRect.Contains(centerOfButton)) {
+ return NS_OK;
+ }
+
+ if (!::SetCursorPos(centerOfButton.x, centerOfButton.y)) {
+ NS_ERROR("SetCursorPos failed");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+uint32_t nsWindow::GetMaxTouchPoints() const {
+ return WinUtils::GetMaxTouchPoints();
+}
+
+void nsWindow::SetWindowClass(const nsAString& xulWinType,
+ const nsAString& xulWinClass,
+ const nsAString& xulWinName) {
+ mIsEarlyBlankWindow = xulWinType.EqualsLiteral("navigator:blank");
+}
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Moz Events
+ **
+ ** Moz GUI event management.
+ **
+ **************************************************************
+ **************************************************************/
+
+/**************************************************************
+ *
+ * SECTION: Mozilla event initialization
+ *
+ * Helpers for initializing moz events.
+ *
+ **************************************************************/
+
+// Event initialization
+void nsWindow::InitEvent(WidgetGUIEvent& event, LayoutDeviceIntPoint* aPoint) {
+ if (nullptr == aPoint) { // use the point from the event
+ // get the message position in client coordinates
+ if (mWnd != nullptr) {
+ DWORD pos = ::GetMessagePos();
+ POINT cpos;
+
+ cpos.x = GET_X_LPARAM(pos);
+ cpos.y = GET_Y_LPARAM(pos);
+
+ ::ScreenToClient(mWnd, &cpos);
+ event.mRefPoint = LayoutDeviceIntPoint(cpos.x, cpos.y);
+ } else {
+ event.mRefPoint = LayoutDeviceIntPoint(0, 0);
+ }
+ } else {
+ // use the point override if provided
+ event.mRefPoint = *aPoint;
+ }
+
+ event.AssignEventTime(CurrentMessageWidgetEventTime());
+}
+
+WidgetEventTime nsWindow::CurrentMessageWidgetEventTime() const {
+ LONG messageTime = ::GetMessageTime();
+ return WidgetEventTime(GetMessageTimeStamp(messageTime));
+}
+
+/**************************************************************
+ *
+ * SECTION: Moz event dispatch helpers
+ *
+ * Helpers for dispatching different types of moz events.
+ *
+ **************************************************************/
+
+// Main event dispatch. Invokes callback and ProcessEvent method on
+// Event Listener object. Part of nsIWidget.
+nsresult nsWindow::DispatchEvent(WidgetGUIEvent* event,
+ nsEventStatus& aStatus) {
+#ifdef WIDGET_DEBUG_OUTPUT
+ debug_DumpEvent(stdout, event->mWidget, event, "something", (int32_t)mWnd);
+#endif // WIDGET_DEBUG_OUTPUT
+
+ aStatus = nsEventStatus_eIgnore;
+
+ // Top level windows can have a view attached which requires events be sent
+ // to the underlying base window and the view. Added when we combined the
+ // base chrome window with the main content child for nc client area (title
+ // bar) rendering.
+ if (mAttachedWidgetListener) {
+ aStatus = mAttachedWidgetListener->HandleEvent(event, mUseAttachedEvents);
+ } else if (mWidgetListener) {
+ aStatus = mWidgetListener->HandleEvent(event, mUseAttachedEvents);
+ }
+
+ // the window can be destroyed during processing of seemingly innocuous events
+ // like, say, mousedowns due to the magic of scripting. mousedowns will return
+ // nsEventStatus_eIgnore, which causes problems with the deleted window.
+ // therefore:
+ if (mOnDestroyCalled) aStatus = nsEventStatus_eConsumeNoDefault;
+ return NS_OK;
+}
+
+bool nsWindow::DispatchStandardEvent(EventMessage aMsg) {
+ WidgetGUIEvent event(true, aMsg, this);
+ InitEvent(event);
+
+ bool result = DispatchWindowEvent(event);
+ return result;
+}
+
+bool nsWindow::DispatchKeyboardEvent(WidgetKeyboardEvent* event) {
+ nsEventStatus status = DispatchInputEvent(event).mContentStatus;
+ return ConvertStatus(status);
+}
+
+bool nsWindow::DispatchContentCommandEvent(WidgetContentCommandEvent* aEvent) {
+ nsEventStatus status;
+ DispatchEvent(aEvent, status);
+ return ConvertStatus(status);
+}
+
+bool nsWindow::DispatchWheelEvent(WidgetWheelEvent* aEvent) {
+ nsEventStatus status =
+ DispatchInputEvent(aEvent->AsInputEvent()).mContentStatus;
+ return ConvertStatus(status);
+}
+
+// Recursively dispatch synchronous paints for nsIWidget
+// descendants with invalidated rectangles.
+BOOL CALLBACK nsWindow::DispatchStarvedPaints(HWND aWnd, LPARAM aMsg) {
+ LONG_PTR proc = ::GetWindowLongPtrW(aWnd, GWLP_WNDPROC);
+ if (proc == (LONG_PTR)&nsWindow::WindowProc) {
+ // its one of our windows so check to see if it has a
+ // invalidated rect. If it does. Dispatch a synchronous
+ // paint.
+ if (GetUpdateRect(aWnd, nullptr, FALSE)) VERIFY(::UpdateWindow(aWnd));
+ }
+ return TRUE;
+}
+
+// Check for pending paints and dispatch any pending paint
+// messages for any nsIWidget which is a descendant of the
+// top-level window that *this* window is embedded within.
+//
+// Note: We do not dispatch pending paint messages for non
+// nsIWidget managed windows.
+void nsWindow::DispatchPendingEvents() {
+ // We need to ensure that reflow events do not get starved.
+ // At the same time, we don't want to recurse through here
+ // as that would prevent us from dispatching starved paints.
+ static int recursionBlocker = 0;
+ if (recursionBlocker++ == 0) {
+ NS_ProcessPendingEvents(nullptr, PR_MillisecondsToInterval(100));
+ --recursionBlocker;
+ }
+
+ // Quickly check to see if there are any paint events pending,
+ // but only dispatch them if it has been long enough since the
+ // last paint completed.
+ if (::GetQueueStatus(QS_PAINT) &&
+ ((TimeStamp::Now() - mLastPaintEndTime).ToMilliseconds() >= 50)) {
+ // Find the top level window.
+ HWND topWnd = WinUtils::GetTopLevelHWND(mWnd);
+
+ // Dispatch pending paints for topWnd and all its descendant windows.
+ // Note: EnumChildWindows enumerates all descendant windows not just
+ // the children (but not the window itself).
+ nsWindow::DispatchStarvedPaints(topWnd, 0);
+ ::EnumChildWindows(topWnd, nsWindow::DispatchStarvedPaints, 0);
+ }
+}
+
+void nsWindow::DispatchCustomEvent(const nsString& eventName) {
+ if (Document* doc = GetDocument()) {
+ if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
+ win->DispatchCustomEvent(eventName, ChromeOnlyDispatch::eYes);
+ }
+ }
+}
+
+bool nsWindow::TouchEventShouldStartDrag(EventMessage aEventMessage,
+ LayoutDeviceIntPoint aEventPoint) {
+ // Allow users to start dragging by double-tapping.
+ if (aEventMessage == eMouseDoubleClick) {
+ return true;
+ }
+
+ // In chrome UI, allow touchdownstartsdrag attributes
+ // to cause any touchdown event to trigger a drag.
+ if (aEventMessage == eMouseDown) {
+ WidgetMouseEvent hittest(true, eMouseHitTest, this,
+ WidgetMouseEvent::eReal);
+ hittest.mRefPoint = aEventPoint;
+ hittest.mIgnoreRootScrollFrame = true;
+ hittest.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+ DispatchInputEvent(&hittest);
+
+ if (EventTarget* target = hittest.GetDOMEventTarget()) {
+ if (nsIContent* content = nsIContent::FromEventTarget(target)) {
+ // Check if the element or any parent element has the
+ // attribute we're looking for.
+ for (Element* element = content->GetAsElementOrParentElement(); element;
+ element = element->GetParentElement()) {
+ nsAutoString startDrag;
+ element->GetAttribute(u"touchdownstartsdrag"_ns, startDrag);
+ if (!startDrag.IsEmpty()) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+// Deal with all sort of mouse event
+bool nsWindow::DispatchMouseEvent(EventMessage aEventMessage, WPARAM wParam,
+ LPARAM lParam, bool aIsContextMenuKey,
+ int16_t aButton, uint16_t aInputSource,
+ WinPointerInfo* aPointerInfo,
+ bool aIgnoreAPZ) {
+ ContextMenuPreventer contextMenuPreventer(this);
+ bool result = false;
+
+ UserActivity();
+
+ if (!mWidgetListener) {
+ return result;
+ }
+
+ LayoutDeviceIntPoint eventPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+ LayoutDeviceIntPoint mpScreen = eventPoint + WidgetToScreenOffset();
+
+ // Suppress mouse moves caused by widget creation. Make sure to do this early
+ // so that we update sLastMouseMovePoint even for touch-induced mousemove
+ // events.
+ if (aEventMessage == eMouseMove) {
+ if ((sLastMouseMovePoint.x == mpScreen.x.value) &&
+ (sLastMouseMovePoint.y == mpScreen.y.value)) {
+ return result;
+ }
+ sLastMouseMovePoint.x = mpScreen.x;
+ sLastMouseMovePoint.y = mpScreen.y;
+ }
+
+ if (!aIgnoreAPZ && WinUtils::GetIsMouseFromTouch(aEventMessage)) {
+ if (mTouchWindow) {
+ // If mTouchWindow is true, then we must have APZ enabled and be
+ // feeding it raw touch events. In that case we only want to
+ // send touch-generated mouse events to content if they should
+ // start a touch-based drag-and-drop gesture, such as on
+ // double-tapping or when tapping elements marked with the
+ // touchdownstartsdrag attribute in chrome UI.
+ MOZ_ASSERT(mAPZC);
+ if (TouchEventShouldStartDrag(aEventMessage, eventPoint)) {
+ aEventMessage = eMouseTouchDrag;
+ } else {
+ return result;
+ }
+ }
+ }
+
+ uint32_t pointerId =
+ aPointerInfo ? aPointerInfo->pointerId : MOUSE_POINTERID();
+
+ switch (aEventMessage) {
+ case eMouseDown:
+ CaptureMouse(true);
+ break;
+
+ // eMouseMove and eMouseExitFromWidget are here because we need to make
+ // sure capture flag isn't left on after a drag where we wouldn't see a
+ // button up message (see bug 324131).
+ case eMouseUp:
+ case eMouseMove:
+ case eMouseExitFromWidget:
+ if (!(wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON)) &&
+ sIsInMouseCapture)
+ CaptureMouse(false);
+ break;
+
+ default:
+ break;
+
+ } // switch
+
+ WidgetMouseEvent event(true, aEventMessage, this, WidgetMouseEvent::eReal,
+ aIsContextMenuKey ? WidgetMouseEvent::eContextMenuKey
+ : WidgetMouseEvent::eNormal);
+ if (aEventMessage == eContextMenu && aIsContextMenuKey) {
+ LayoutDeviceIntPoint zero(0, 0);
+ InitEvent(event, &zero);
+ } else {
+ InitEvent(event, &eventPoint);
+ }
+
+ ModifierKeyState modifierKeyState;
+ modifierKeyState.InitInputEvent(event);
+
+ // eContextMenu with Shift state is special. It won't fire "contextmenu"
+ // event in the web content for blocking web content to prevent its default.
+ // However, Shift+F10 is a standard shortcut key on Windows. Therefore,
+ // this should not block web page to prevent its default. I.e., it should
+ // behave same as ContextMenu key without Shift key.
+ // XXX Should we allow to block web page to prevent its default with
+ // Ctrl+Shift+F10 or Alt+Shift+F10 instead?
+ if (aEventMessage == eContextMenu && aIsContextMenuKey && event.IsShift() &&
+ NativeKey::LastKeyOrCharMSG().message == WM_SYSKEYDOWN &&
+ NativeKey::LastKeyOrCharMSG().wParam == VK_F10) {
+ event.mModifiers &= ~MODIFIER_SHIFT;
+ }
+
+ event.mButton = aButton;
+ event.mInputSource = aInputSource;
+ if (aPointerInfo) {
+ // Mouse events from Windows WM_POINTER*. Fill more information in
+ // WidgetMouseEvent.
+ event.AssignPointerHelperData(*aPointerInfo);
+ event.mPressure = aPointerInfo->mPressure;
+ event.mButtons = aPointerInfo->mButtons;
+ } else {
+ // If we get here the mouse events must be from non-touch sources, so
+ // convert it to pointer events as well
+ event.convertToPointer = true;
+ event.pointerId = pointerId;
+ }
+
+ // Static variables used to distinguish simple-, double- and triple-clicks.
+ static POINT sLastMousePoint = {0};
+ static LONG sLastMouseDownTime = 0L;
+ static LONG sLastClickCount = 0L;
+ static BYTE sLastMouseButton = 0;
+
+ bool insideMovementThreshold =
+ (DeprecatedAbs(sLastMousePoint.x - eventPoint.x.value) <
+ (short)::GetSystemMetrics(SM_CXDOUBLECLK)) &&
+ (DeprecatedAbs(sLastMousePoint.y - eventPoint.y.value) <
+ (short)::GetSystemMetrics(SM_CYDOUBLECLK));
+
+ BYTE eventButton;
+ switch (aButton) {
+ case MouseButton::ePrimary:
+ eventButton = VK_LBUTTON;
+ break;
+ case MouseButton::eMiddle:
+ eventButton = VK_MBUTTON;
+ break;
+ case MouseButton::eSecondary:
+ eventButton = VK_RBUTTON;
+ break;
+ default:
+ eventButton = 0;
+ break;
+ }
+
+ // Doubleclicks are used to set the click count, then changed to mousedowns
+ // We're going to time double-clicks from mouse *up* to next mouse *down*
+ LONG curMsgTime = ::GetMessageTime();
+
+ switch (aEventMessage) {
+ case eMouseDoubleClick:
+ event.mMessage = eMouseDown;
+ event.mButton = aButton;
+ sLastClickCount = 2;
+ sLastMouseDownTime = curMsgTime;
+ break;
+ case eMouseUp:
+ // remember when this happened for the next mouse down
+ sLastMousePoint.x = eventPoint.x;
+ sLastMousePoint.y = eventPoint.y;
+ sLastMouseButton = eventButton;
+ break;
+ case eMouseDown:
+ // now look to see if we want to convert this to a double- or triple-click
+ if (((curMsgTime - sLastMouseDownTime) < (LONG)::GetDoubleClickTime()) &&
+ insideMovementThreshold && eventButton == sLastMouseButton) {
+ sLastClickCount++;
+ } else {
+ // reset the click count, to count *this* click
+ sLastClickCount = 1;
+ }
+ // Set last Click time on MouseDown only
+ sLastMouseDownTime = curMsgTime;
+ break;
+ case eMouseMove:
+ if (!insideMovementThreshold) {
+ sLastClickCount = 0;
+ }
+ break;
+ case eMouseExitFromWidget:
+ event.mExitFrom =
+ Some(IsTopLevelMouseExit(mWnd) ? WidgetMouseEvent::ePlatformTopLevel
+ : WidgetMouseEvent::ePlatformChild);
+ break;
+ default:
+ break;
+ }
+ event.mClickCount = sLastClickCount;
+
+#ifdef NS_DEBUG_XX
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("Msg Time: %d Click Count: %d\n", curMsgTime, event.mClickCount));
+#endif
+
+ // call the event callback
+ if (mWidgetListener) {
+ if (aEventMessage == eMouseMove) {
+ LayoutDeviceIntRect rect = GetBounds();
+ rect.MoveTo(0, 0);
+
+ if (rect.Contains(event.mRefPoint)) {
+ if (sCurrentWindow == nullptr || sCurrentWindow != this) {
+ if ((nullptr != sCurrentWindow) && (!sCurrentWindow->mInDtor)) {
+ LPARAM pos = sCurrentWindow->lParamToClient(lParamToScreen(lParam));
+ sCurrentWindow->DispatchMouseEvent(
+ eMouseExitFromWidget, wParam, pos, false, MouseButton::ePrimary,
+ aInputSource, aPointerInfo);
+ }
+ sCurrentWindow = this;
+ if (!mInDtor) {
+ LPARAM pos = sCurrentWindow->lParamToClient(lParamToScreen(lParam));
+ sCurrentWindow->DispatchMouseEvent(
+ eMouseEnterIntoWidget, wParam, pos, false,
+ MouseButton::ePrimary, aInputSource, aPointerInfo);
+ }
+ }
+ }
+ } else if (aEventMessage == eMouseExitFromWidget) {
+ if (sCurrentWindow == this) {
+ sCurrentWindow = nullptr;
+ }
+ }
+
+ nsIWidget::ContentAndAPZEventStatus eventStatus =
+ DispatchInputEvent(&event);
+ contextMenuPreventer.Update(event, eventStatus);
+ return ConvertStatus(eventStatus.mContentStatus);
+ }
+
+ return result;
+}
+
+HWND nsWindow::GetTopLevelForFocus(HWND aCurWnd) {
+ // retrieve the toplevel window or dialogue
+ HWND toplevelWnd = nullptr;
+ while (aCurWnd) {
+ toplevelWnd = aCurWnd;
+ nsWindow* win = WinUtils::GetNSWindowPtr(aCurWnd);
+ if (win) {
+ if (win->mWindowType == WindowType::TopLevel ||
+ win->mWindowType == WindowType::Dialog) {
+ break;
+ }
+ }
+
+ aCurWnd = ::GetParent(aCurWnd); // Parent or owner (if has no parent)
+ }
+ return toplevelWnd;
+}
+
+void nsWindow::DispatchFocusToTopLevelWindow(bool aIsActivate) {
+ if (aIsActivate) {
+ sJustGotActivate = false;
+ }
+ sJustGotDeactivate = false;
+ mLastKillFocusWindow = nullptr;
+
+ HWND toplevelWnd = GetTopLevelForFocus(mWnd);
+
+ if (toplevelWnd) {
+ nsWindow* win = WinUtils::GetNSWindowPtr(toplevelWnd);
+ if (win && win->mWidgetListener) {
+ if (aIsActivate) {
+ win->mWidgetListener->WindowActivated();
+ } else {
+ win->mWidgetListener->WindowDeactivated();
+ }
+ }
+ }
+}
+
+HWND nsWindow::WindowAtMouse() {
+ DWORD pos = ::GetMessagePos();
+ POINT mp;
+ mp.x = GET_X_LPARAM(pos);
+ mp.y = GET_Y_LPARAM(pos);
+ return ::WindowFromPoint(mp);
+}
+
+bool nsWindow::IsTopLevelMouseExit(HWND aWnd) {
+ HWND mouseWnd = WindowAtMouse();
+
+ // WinUtils::GetTopLevelHWND() will return a HWND for the window frame
+ // (which includes the non-client area). If the mouse has moved into
+ // the non-client area, we should treat it as a top-level exit.
+ HWND mouseTopLevel = WinUtils::GetTopLevelHWND(mouseWnd);
+ if (mouseWnd == mouseTopLevel) return true;
+
+ return WinUtils::GetTopLevelHWND(aWnd) != mouseTopLevel;
+}
+
+/**************************************************************
+ *
+ * SECTION: IPC
+ *
+ * IPC related helpers.
+ *
+ **************************************************************/
+
+// static
+bool nsWindow::IsAsyncResponseEvent(UINT aMsg, LRESULT& aResult) {
+ switch (aMsg) {
+ case WM_SETFOCUS:
+ case WM_KILLFOCUS:
+ case WM_ENABLE:
+ case WM_WINDOWPOSCHANGING:
+ case WM_WINDOWPOSCHANGED:
+ case WM_PARENTNOTIFY:
+ case WM_ACTIVATEAPP:
+ case WM_NCACTIVATE:
+ case WM_ACTIVATE:
+ case WM_CHILDACTIVATE:
+ case WM_IME_SETCONTEXT:
+ case WM_IME_NOTIFY:
+ case WM_SHOWWINDOW:
+ case WM_CANCELMODE:
+ case WM_MOUSEACTIVATE:
+ case WM_CONTEXTMENU:
+ aResult = 0;
+ return true;
+
+ case WM_SETTINGCHANGE:
+ case WM_SETCURSOR:
+ return false;
+ }
+
+#ifdef DEBUG
+ char szBuf[200];
+ sprintf(szBuf,
+ "An unhandled ISMEX_SEND message was received during spin loop! (%X)",
+ aMsg);
+ NS_WARNING(szBuf);
+#endif
+
+ return false;
+}
+
+void nsWindow::IPCWindowProcHandler(UINT& msg, WPARAM& wParam, LPARAM& lParam) {
+ MOZ_ASSERT_IF(
+ msg != WM_GETOBJECT,
+ !mozilla::ipc::MessageChannel::IsPumpingMessages() ||
+ mozilla::ipc::SuppressedNeuteringRegion::IsNeuteringSuppressed());
+
+ // Modal UI being displayed in windowless plugins.
+ if (mozilla::ipc::MessageChannel::IsSpinLoopActive() &&
+ (InSendMessageEx(nullptr) & (ISMEX_REPLIED | ISMEX_SEND)) == ISMEX_SEND) {
+ LRESULT res;
+ if (IsAsyncResponseEvent(msg, res)) {
+ ReplyMessage(res);
+ }
+ return;
+ }
+
+ // Handle certain sync plugin events sent to the parent which
+ // trigger ipc calls that result in deadlocks.
+
+ DWORD dwResult = 0;
+ bool handled = false;
+
+ switch (msg) {
+ // Windowless flash sending WM_ACTIVATE events to the main window
+ // via calls to ShowWindow.
+ case WM_ACTIVATE:
+ if (lParam != 0 && LOWORD(wParam) == WA_ACTIVE &&
+ IsWindow((HWND)lParam)) {
+ // Check for Adobe Reader X sync activate message from their
+ // helper window and ignore. Fixes an annoying focus problem.
+ if ((InSendMessageEx(nullptr) & (ISMEX_REPLIED | ISMEX_SEND)) ==
+ ISMEX_SEND) {
+ wchar_t szClass[10];
+ HWND focusWnd = (HWND)lParam;
+ if (IsWindowVisible(focusWnd) &&
+ GetClassNameW(focusWnd, szClass,
+ sizeof(szClass) / sizeof(char16_t)) &&
+ !wcscmp(szClass, L"Edit") &&
+ !WinUtils::IsOurProcessWindow(focusWnd)) {
+ break;
+ }
+ }
+ handled = true;
+ }
+ break;
+ // Plugins taking or losing focus triggering focus app messages.
+ case WM_SETFOCUS:
+ case WM_KILLFOCUS:
+ // Windowed plugins that pass sys key events to defwndproc generate
+ // WM_SYSCOMMAND events to the main window.
+ case WM_SYSCOMMAND:
+ // Windowed plugins that fire context menu selection events to parent
+ // windows.
+ case WM_CONTEXTMENU:
+ // IME events fired as a result of synchronous focus changes
+ case WM_IME_SETCONTEXT:
+ handled = true;
+ break;
+ }
+
+ if (handled &&
+ (InSendMessageEx(nullptr) & (ISMEX_REPLIED | ISMEX_SEND)) == ISMEX_SEND) {
+ ReplyMessage(dwResult);
+ }
+}
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Native events
+ **
+ ** Main Windows message handlers and OnXXX handlers for
+ ** Windows event handling.
+ **
+ **************************************************************
+ **************************************************************/
+
+/**************************************************************
+ *
+ * SECTION: Wind proc.
+ *
+ * The main Windows event procedures and associated
+ * message processing methods.
+ *
+ **************************************************************/
+
+static bool DisplaySystemMenu(HWND hWnd, nsSizeMode sizeMode, bool isRtl,
+ int32_t x, int32_t y) {
+ HMENU hMenu = GetSystemMenu(hWnd, FALSE);
+ if (hMenu) {
+ MENUITEMINFO mii;
+ mii.cbSize = sizeof(MENUITEMINFO);
+ mii.fMask = MIIM_STATE;
+ mii.fType = 0;
+
+ // update the options
+ mii.fState = MF_ENABLED;
+ SetMenuItemInfo(hMenu, SC_RESTORE, FALSE, &mii);
+ SetMenuItemInfo(hMenu, SC_SIZE, FALSE, &mii);
+ SetMenuItemInfo(hMenu, SC_MOVE, FALSE, &mii);
+ SetMenuItemInfo(hMenu, SC_MAXIMIZE, FALSE, &mii);
+ SetMenuItemInfo(hMenu, SC_MINIMIZE, FALSE, &mii);
+
+ mii.fState = MF_GRAYED;
+ switch (sizeMode) {
+ case nsSizeMode_Fullscreen:
+ // intentional fall through
+ case nsSizeMode_Maximized:
+ SetMenuItemInfo(hMenu, SC_SIZE, FALSE, &mii);
+ SetMenuItemInfo(hMenu, SC_MOVE, FALSE, &mii);
+ SetMenuItemInfo(hMenu, SC_MAXIMIZE, FALSE, &mii);
+ break;
+ case nsSizeMode_Minimized:
+ SetMenuItemInfo(hMenu, SC_MINIMIZE, FALSE, &mii);
+ break;
+ case nsSizeMode_Normal:
+ SetMenuItemInfo(hMenu, SC_RESTORE, FALSE, &mii);
+ break;
+ case nsSizeMode_Invalid:
+ NS_ASSERTION(false, "Did the argument come from invalid IPC?");
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhnalded nsSizeMode value detected");
+ break;
+ }
+ LPARAM cmd = TrackPopupMenu(
+ hMenu,
+ (TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_TOPALIGN |
+ (isRtl ? TPM_RIGHTALIGN : TPM_LEFTALIGN)),
+ x, y, 0, hWnd, nullptr);
+ if (cmd) {
+ PostMessage(hWnd, WM_SYSCOMMAND, cmd, 0);
+ return true;
+ }
+ }
+ return false;
+}
+
+// The WndProc procedure for all nsWindows in this toolkit. This merely catches
+// SEH exceptions and passes the real work to WindowProcInternal. See bug 587406
+// and http://msdn.microsoft.com/en-us/library/ms633573%28VS.85%29.aspx
+LRESULT CALLBACK nsWindow::WindowProc(HWND hWnd, UINT msg, WPARAM wParam,
+ LPARAM lParam) {
+ mozilla::ipc::CancelCPOWs();
+
+ BackgroundHangMonitor().NotifyActivity();
+
+ return mozilla::CallWindowProcCrashProtected(WindowProcInternal, hWnd, msg,
+ wParam, lParam);
+}
+
+LRESULT CALLBACK nsWindow::WindowProcInternal(HWND hWnd, UINT msg,
+ WPARAM wParam, LPARAM lParam) {
+ if (::GetWindowLongPtrW(hWnd, GWLP_ID) == eFakeTrackPointScrollableID) {
+ // This message was sent to the FAKETRACKPOINTSCROLLABLE.
+ if (msg == WM_HSCROLL) {
+ // Route WM_HSCROLL messages to the main window.
+ hWnd = ::GetParent(::GetParent(hWnd));
+ } else {
+ // Handle all other messages with its original window procedure.
+ WNDPROC prevWindowProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_USERDATA);
+ return ::CallWindowProcW(prevWindowProc, hWnd, msg, wParam, lParam);
+ }
+ }
+
+ if (msg == MOZ_WM_TRACE) {
+ // This is a tracer event for measuring event loop latency.
+ // See WidgetTraceEvent.cpp for more details.
+ mozilla::SignalTracerThread();
+ return 0;
+ }
+
+ // Get the window which caused the event and ask it to process the message
+ nsWindow* targetWindow = WinUtils::GetNSWindowPtr(hWnd);
+ NS_ASSERTION(targetWindow, "nsWindow* is null!");
+ if (!targetWindow) return ::DefWindowProcW(hWnd, msg, wParam, lParam);
+
+ // Hold the window for the life of this method, in case it gets
+ // destroyed during processing, unless we're in the dtor already.
+ nsCOMPtr<nsIWidget> kungFuDeathGrip;
+ if (!targetWindow->mInDtor) kungFuDeathGrip = targetWindow;
+
+ targetWindow->IPCWindowProcHandler(msg, wParam, lParam);
+
+ // Create this here so that we store the last rolled up popup until after
+ // the event has been processed.
+ nsAutoRollup autoRollup;
+
+ LRESULT popupHandlingResult;
+ if (DealWithPopups(hWnd, msg, wParam, lParam, &popupHandlingResult))
+ return popupHandlingResult;
+
+ // Call ProcessMessage
+ LRESULT retValue;
+ if (targetWindow->ProcessMessage(msg, wParam, lParam, &retValue)) {
+ return retValue;
+ }
+
+ LRESULT res = ::CallWindowProcW(targetWindow->GetPrevWindowProc(), hWnd, msg,
+ wParam, lParam);
+
+ return res;
+}
+
+const char16_t* GetQuitType() {
+ if (Preferences::GetBool(PREF_WIN_REGISTER_APPLICATION_RESTART, false)) {
+ DWORD cchCmdLine = 0;
+ HRESULT rc = ::GetApplicationRestartSettings(::GetCurrentProcess(), nullptr,
+ &cchCmdLine, nullptr);
+ if (rc == S_OK) {
+ return u"os-restart";
+ }
+ }
+ return nullptr;
+}
+
+bool nsWindow::ExternalHandlerProcessMessage(UINT aMessage, WPARAM& aWParam,
+ LPARAM& aLParam,
+ MSGResult& aResult) {
+ if (mWindowHook.Notify(mWnd, aMessage, aWParam, aLParam, aResult)) {
+ return true;
+ }
+
+ if (IMEHandler::ProcessMessage(this, aMessage, aWParam, aLParam, aResult)) {
+ return true;
+ }
+
+ if (MouseScrollHandler::ProcessMessage(this, aMessage, aWParam, aLParam,
+ aResult)) {
+ return true;
+ }
+
+ return false;
+}
+
+// The main windows message processing method. Wraps ProcessMessageInternal so
+// we can log aRetValue.
+bool nsWindow::ProcessMessage(UINT msg, WPARAM& wParam, LPARAM& lParam,
+ LRESULT* aRetValue) {
+ // For some events we might change the parameter values, so log
+ // before and after we process them.
+ NativeEventLogger eventLogger("nsWindow", mWnd, msg, wParam, lParam);
+ bool result = ProcessMessageInternal(msg, wParam, lParam, aRetValue);
+ eventLogger.SetResult(*aRetValue, result);
+
+ return result;
+}
+
+// The main windows message processing method. Called by ProcessMessage.
+bool nsWindow::ProcessMessageInternal(UINT msg, WPARAM& wParam, LPARAM& lParam,
+ LRESULT* aRetValue) {
+ MSGResult msgResult(aRetValue);
+ if (ExternalHandlerProcessMessage(msg, wParam, lParam, msgResult)) {
+ return (msgResult.mConsumed || !mWnd);
+ }
+
+ bool result = false; // call the default nsWindow proc
+ *aRetValue = 0;
+
+ // The DWM resize hack (see bug 1763981) causes us to process a number of
+ // messages, notably including some WM_WINDOWPOSCHANG{ING,ED} messages which
+ // would ordinarily result in a whole lot of internal state being updated.
+ //
+ // Since we're supposed to end in the same state we started in (and since the
+ // content shouldn't know about any of this nonsense), just discard any
+ // messages synchronously dispatched from within the hack.
+ if (MOZ_UNLIKELY(mIsPerformingDwmFlushHack)) {
+ return true;
+ }
+
+ // Glass hit testing w/custom transparent margins.
+ //
+ // FIXME(emilio): is this needed? We deal with titlebar buttons non-natively
+ // now.
+ LRESULT dwmHitResult;
+ if (mCustomNonClient &&
+ DwmDefWindowProc(mWnd, msg, wParam, lParam, &dwmHitResult)) {
+ *aRetValue = dwmHitResult;
+ return true;
+ }
+
+ // The preference whether to use a different keyboard layout for each
+ // window is cached, and updating it will not take effect until the
+ // next restart. We read the preference here and not upon WM_ACTIVATE to make
+ // sure that this behavior is consistent. Otherwise, if the user changed the
+ // preference before having ever lowered the window, the preference would take
+ // effect immediately.
+ static const bool sSwitchKeyboardLayout =
+ Preferences::GetBool("intl.keyboard.per_window_layout", false);
+ AppShutdownReason shutdownReason = AppShutdownReason::Unknown;
+
+ // (Large blocks of code should be broken out into OnEvent handlers.)
+ switch (msg) {
+ // WM_QUERYENDSESSION must be handled by all windows.
+ // Otherwise Windows thinks the window can just be killed at will.
+ case WM_QUERYENDSESSION: {
+ // Ask around if it's ok to quit.
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ nsCOMPtr<nsISupportsPRBool> cancelQuitWrapper =
+ do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID);
+ cancelQuitWrapper->SetData(false);
+
+ const char16_t* quitType = GetQuitType();
+ obsServ->NotifyObservers(cancelQuitWrapper, "quit-application-requested",
+ quitType);
+
+ bool shouldCancelQuit;
+ cancelQuitWrapper->GetData(&shouldCancelQuit);
+ *aRetValue = !shouldCancelQuit;
+ result = true;
+ } break;
+
+ case MOZ_WM_STARTA11Y:
+#if defined(ACCESSIBILITY)
+ Unused << GetAccessible();
+ result = true;
+#else
+ result = false;
+#endif
+ break;
+
+ case WM_ENDSESSION: {
+ // For WM_ENDSESSION, wParam indicates whether we need to shutdown
+ // (TRUE) or not (FALSE).
+ if (!wParam) {
+ result = true;
+ break;
+ }
+ // According to WM_ENDSESSION lParam documentation:
+ // 0 -> OS shutdown or restart (no way to distinguish)
+ // ENDSESSION_LOGOFF -> User is logging off
+ // ENDSESSION_CLOSEAPP -> Application must shutdown
+ // ENDSESSION_CRITICAL -> Application is forced to shutdown
+ // The difference of the last two is not very clear.
+ if (lParam == 0) {
+ shutdownReason = AppShutdownReason::OSShutdown;
+ } else if (lParam & ENDSESSION_LOGOFF) {
+ shutdownReason = AppShutdownReason::OSSessionEnd;
+ } else if (lParam & (ENDSESSION_CLOSEAPP | ENDSESSION_CRITICAL)) {
+ shutdownReason = AppShutdownReason::OSForceClose;
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false,
+ "Received WM_ENDSESSION with unknown flags.");
+ shutdownReason = AppShutdownReason::OSForceClose;
+ }
+ }
+ [[fallthrough]];
+ case MOZ_WM_APP_QUIT: {
+ if (shutdownReason == AppShutdownReason::Unknown) {
+ // TODO: We do not expect that these days anybody sends us
+ // MOZ_WM_APP_QUIT, see bug 1827807.
+ shutdownReason = AppShutdownReason::WinUnexpectedMozQuit;
+ }
+ // Let's fake a shutdown sequence without actually closing windows etc.
+ // to avoid Windows killing us in the middle. A proper shutdown would
+ // require having a chance to pump some messages. Unfortunately
+ // Windows won't let us do that. Bug 212316.
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ const char16_t* syncShutdown = u"syncShutdown";
+ const char16_t* quitType = GetQuitType();
+
+ AppShutdown::Init(AppShutdownMode::Normal, 0, shutdownReason);
+
+ obsServ->NotifyObservers(nullptr, "quit-application-granted",
+ syncShutdown);
+ obsServ->NotifyObservers(nullptr, "quit-application-forced", nullptr);
+
+ AppShutdown::OnShutdownConfirmed();
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownConfirmed,
+ quitType);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownNetTeardown,
+ nullptr);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTeardown,
+ nullptr);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdown, nullptr);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownQM, nullptr);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTelemetry,
+ nullptr);
+
+ AppShutdown::DoImmediateExit();
+ MOZ_ASSERT_UNREACHABLE("Our process was supposed to exit.");
+ } break;
+
+ case WM_SYSCOLORCHANGE:
+ // No need to invalidate layout for system color changes, but we need to
+ // invalidate style.
+ NotifyThemeChanged(widget::ThemeChangeKind::Style);
+ break;
+
+ case WM_THEMECHANGED: {
+ // Update non-client margin offsets
+ UpdateNonClientMargins();
+ nsUXThemeData::UpdateNativeThemeInfo();
+
+ // We assume pretty much everything could've changed here.
+ NotifyThemeChanged(widget::ThemeChangeKind::StyleAndLayout);
+
+ UpdateDarkModeToolbar();
+
+ // Invalidate the window so that the repaint will
+ // pick up the new theme.
+ Invalidate(true, true, true);
+ } break;
+
+ case WM_WTSSESSION_CHANGE: {
+ switch (wParam) {
+ case WTS_CONSOLE_CONNECT:
+ case WTS_REMOTE_CONNECT:
+ case WTS_SESSION_UNLOCK:
+ // When a session becomes visible, we should invalidate.
+ Invalidate(true, true, true);
+ break;
+ default:
+ break;
+ }
+ } break;
+
+ case WM_FONTCHANGE: {
+ // We only handle this message for the hidden window,
+ // as we only need to update the (global) font list once
+ // for any given change, not once per window!
+ if (mWindowType != WindowType::Invisible) {
+ break;
+ }
+
+ // update the global font list
+ gfxPlatform::GetPlatform()->UpdateFontList();
+ } break;
+
+ case WM_SETTINGCHANGE: {
+ if (wParam == SPI_SETCLIENTAREAANIMATION ||
+ wParam == SPI_SETKEYBOARDDELAY || wParam == SPI_SETMOUSEVANISH) {
+ // These need to update LookAndFeel cached values.
+ // They affect reduced motion settings / caret blink count / show
+ // pointer while typing, so no need to invalidate style / layout.
+ NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly);
+ break;
+ }
+ if (wParam == SPI_SETFONTSMOOTHING ||
+ wParam == SPI_SETFONTSMOOTHINGTYPE) {
+ gfxDWriteFont::UpdateSystemTextVars();
+ break;
+ }
+ if (wParam == SPI_SETWORKAREA) {
+ // NB: We also refresh screens on WM_DISPLAYCHANGE but the rcWork
+ // values are sometimes wrong at that point. This message then
+ // arrives soon afterward, when we can get the right rcWork values.
+ ScreenHelperWin::RefreshScreens();
+ break;
+ }
+ if (auto lParamString = reinterpret_cast<const wchar_t*>(lParam)) {
+ if (!wcscmp(lParamString, L"ImmersiveColorSet")) {
+ // This affects system colors (-moz-win-accentcolor), so gotta pass
+ // the style flag.
+ NotifyThemeChanged(widget::ThemeChangeKind::Style);
+ break;
+ }
+
+ // UserInteractionMode, ConvertibleSlateMode, SystemDockMode may cause
+ // @media(pointer) queries to change, which layout needs to know about
+ //
+ // (WM_SETTINGCHANGE will be sent to all top-level windows, so we
+ // only respond to the hidden top-level window to avoid hammering
+ // layout with a bunch of NotifyThemeChanged() calls)
+ //
+ if (mWindowType == WindowType::Invisible) {
+ if (!wcscmp(lParamString, L"UserInteractionMode") ||
+ !wcscmp(lParamString, L"ConvertibleSlateMode") ||
+ !wcscmp(lParamString, L"SystemDockMode")) {
+ NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly);
+ WindowsUIUtils::UpdateInTabletMode();
+ }
+ }
+ }
+ } break;
+
+ case WM_DEVICECHANGE: {
+ if (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE) {
+ DEV_BROADCAST_HDR* hdr = reinterpret_cast<DEV_BROADCAST_HDR*>(lParam);
+ // Check dbch_devicetype explicitly since we will get other device types
+ // (e.g. DBT_DEVTYP_VOLUME) for some reasons even if we specify
+ // DBT_DEVTYP_DEVICEINTERFACE in the filter for
+ // RegisterDeviceNotification.
+ if (hdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) {
+ // This can only change media queries (any-hover/any-pointer).
+ NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly);
+ }
+ }
+ } break;
+
+ case WM_NCCALCSIZE: {
+ // NOTE: the following block is mirrored in PreXULSkeletonUI.cpp, and
+ // will need to be kept in sync.
+ if (mCustomNonClient) {
+ // If `wParam` is `FALSE`, `lParam` points to a `RECT` that contains
+ // the proposed window rectangle for our window. During our
+ // processing of the `WM_NCCALCSIZE` message, we are expected to
+ // modify the `RECT` that `lParam` points to, so that its value upon
+ // our return is the new client area. We must return 0 if `wParam`
+ // is `FALSE`.
+ //
+ // If `wParam` is `TRUE`, `lParam` points to a `NCCALCSIZE_PARAMS`
+ // struct. This struct contains an array of 3 `RECT`s, the first of
+ // which has the exact same meaning as the `RECT` that is pointed to
+ // by `lParam` when `wParam` is `FALSE`. The remaining `RECT`s, in
+ // conjunction with our return value, can
+ // be used to specify portions of the source and destination window
+ // rectangles that are valid and should be preserved. We opt not to
+ // implement an elaborate client-area preservation technique, and
+ // simply return 0, which means "preserve the entire old client area
+ // and align it with the upper-left corner of our new client area".
+ RECT* clientRect =
+ wParam ? &(reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam))->rgrc[0]
+ : (reinterpret_cast<RECT*>(lParam));
+ auto margin = NonClientSizeMargin();
+ clientRect->top += margin.top;
+ clientRect->left += margin.left;
+ clientRect->right -= margin.right;
+ clientRect->bottom -= margin.bottom;
+ // Make client rect's width and height more than 0 to
+ // avoid problems of webrender and angle.
+ clientRect->right = std::max(clientRect->right, clientRect->left + 1);
+ clientRect->bottom = std::max(clientRect->bottom, clientRect->top + 1);
+
+ result = true;
+ *aRetValue = 0;
+ }
+ break;
+ }
+
+ case WM_NCHITTEST: {
+ if (mInputRegion.mFullyTransparent) {
+ // Treat this window as transparent.
+ *aRetValue = HTTRANSPARENT;
+ result = true;
+ break;
+ }
+
+ if (mInputRegion.mMargin) {
+ const LayoutDeviceIntPoint screenPoint(GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam));
+ LayoutDeviceIntRect screenRect = GetScreenBounds();
+ screenRect.Deflate(mInputRegion.mMargin);
+ if (!screenRect.Contains(screenPoint)) {
+ *aRetValue = HTTRANSPARENT;
+ result = true;
+ break;
+ }
+ }
+
+ /*
+ * If an nc client area margin has been moved, we are responsible
+ * for calculating where the resize margins are and returning the
+ * appropriate set of hit test constants. DwmDefWindowProc (above)
+ * will handle hit testing on it's command buttons if we are on a
+ * composited desktop.
+ */
+
+ if (!mCustomNonClient) {
+ break;
+ }
+
+ *aRetValue =
+ ClientMarginHitTestPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+ result = true;
+ break;
+ }
+
+ case WM_SETTEXT:
+ /*
+ * WM_SETTEXT paints the titlebar area. Avoid this if we have a
+ * custom titlebar we paint ourselves, or if we're the ones
+ * sending the message with an updated title
+ */
+
+ if (mSendingSetText || !mCustomNonClient || mNonClientMargins.top == -1)
+ break;
+
+ {
+ // From msdn, the way around this is to disable the visible state
+ // temporarily. We need the text to be set but we don't want the
+ // redraw to occur. However, we need to make sure that we don't
+ // do this at the same time that a Present is happening.
+ //
+ // To do this we take mPresentLock in nsWindow::PreRender and
+ // if that lock is taken we wait before doing WM_SETTEXT
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->EnterPresentLock();
+ }
+ DWORD style = GetWindowLong(mWnd, GWL_STYLE);
+ SetWindowLong(mWnd, GWL_STYLE, style & ~WS_VISIBLE);
+ *aRetValue =
+ CallWindowProcW(GetPrevWindowProc(), mWnd, msg, wParam, lParam);
+ SetWindowLong(mWnd, GWL_STYLE, style);
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->LeavePresentLock();
+ }
+
+ return true;
+ }
+
+ case WM_NCACTIVATE: {
+ /*
+ * WM_NCACTIVATE paints nc areas. Avoid this and re-route painting
+ * through WM_NCPAINT via InvalidateNonClientRegion.
+ */
+ UpdateGetWindowInfoCaptionStatus(FALSE != wParam);
+
+ if (!mCustomNonClient) {
+ break;
+ }
+
+ // There is a case that rendered result is not kept. Bug 1237617
+ if (wParam == TRUE && !gfxEnv::MOZ_DISABLE_FORCE_PRESENT()) {
+ NS_DispatchToMainThread(NewRunnableMethod(
+ "nsWindow::ForcePresent", this, &nsWindow::ForcePresent));
+ }
+
+ // let the dwm handle nc painting on glass
+ // Never allow native painting if we are on fullscreen
+ if (mFrameState->GetSizeMode() != nsSizeMode_Fullscreen) break;
+
+ if (wParam == TRUE) {
+ // going active
+ *aRetValue = FALSE; // ignored
+ result = true;
+ // invalidate to trigger a paint
+ InvalidateNonClientRegion();
+ break;
+ } else {
+ // going inactive
+ *aRetValue = TRUE; // go ahead and deactive
+ result = true;
+ // invalidate to trigger a paint
+ InvalidateNonClientRegion();
+ break;
+ }
+ }
+
+ case WM_NCPAINT: {
+ /*
+ * ClearType changes often don't send a WM_SETTINGCHANGE message. But they
+ * do seem to always send a WM_NCPAINT message, so let's update on that.
+ */
+ gfxDWriteFont::UpdateSystemTextVars();
+ } break;
+
+ case WM_POWERBROADCAST:
+ switch (wParam) {
+ case PBT_APMSUSPEND:
+ PostSleepWakeNotification(true);
+ break;
+ case PBT_APMRESUMEAUTOMATIC:
+ case PBT_APMRESUMECRITICAL:
+ case PBT_APMRESUMESUSPEND:
+ PostSleepWakeNotification(false);
+ break;
+ }
+ break;
+
+ case WM_CLOSE: // close request
+ if (mWidgetListener) mWidgetListener->RequestWindowClose(this);
+ result = true; // abort window closure
+ break;
+
+ case WM_DESTROY:
+ // clean up.
+ DestroyLayerManager();
+ OnDestroy();
+ result = true;
+ break;
+
+ case WM_PAINT:
+ *aRetValue = (int)OnPaint(0);
+ result = true;
+ break;
+
+ case WM_HOTKEY:
+ result = OnHotKey(wParam, lParam);
+ break;
+
+ case WM_SYSCHAR:
+ case WM_CHAR: {
+ MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd);
+ result = ProcessCharMessage(nativeMsg, nullptr);
+ DispatchPendingEvents();
+ } break;
+
+ case WM_SYSKEYUP:
+ case WM_KEYUP: {
+ MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd);
+ nativeMsg.time = ::GetMessageTime();
+ result = ProcessKeyUpMessage(nativeMsg, nullptr);
+ DispatchPendingEvents();
+ } break;
+
+ case WM_SYSKEYDOWN:
+ case WM_KEYDOWN: {
+ MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd);
+ result = ProcessKeyDownMessage(nativeMsg, nullptr);
+ DispatchPendingEvents();
+ } break;
+
+ // Say we've dealt with erasing the background. (This is actually handled in
+ // WM_PAINT, where necessary.)
+ case WM_ERASEBKGND: {
+ *aRetValue = 1;
+ result = true;
+ } break;
+
+ case WM_MOUSEMOVE: {
+ LPARAM lParamScreen = lParamToScreen(lParam);
+ mSimulatedClientArea = IsSimulatedClientArea(GET_X_LPARAM(lParamScreen),
+ GET_Y_LPARAM(lParamScreen));
+
+ if (!mMousePresent && !sIsInMouseCapture) {
+ // First MOUSEMOVE over the client area. Ask for MOUSELEAVE
+ TRACKMOUSEEVENT mTrack;
+ mTrack.cbSize = sizeof(TRACKMOUSEEVENT);
+ mTrack.dwFlags = TME_LEAVE;
+ mTrack.dwHoverTime = 0;
+ mTrack.hwndTrack = mWnd;
+ TrackMouseEvent(&mTrack);
+ }
+ mMousePresent = true;
+
+ // Suppress dispatch of pending events
+ // when mouse moves are generated by widget
+ // creation instead of user input.
+ POINT mp;
+ mp.x = GET_X_LPARAM(lParamScreen);
+ mp.y = GET_Y_LPARAM(lParamScreen);
+ bool userMovedMouse = false;
+ if ((sLastMouseMovePoint.x != mp.x) || (sLastMouseMovePoint.y != mp.y)) {
+ userMovedMouse = true;
+ }
+
+ if (userMovedMouse) {
+ result = DispatchMouseEvent(
+ eMouseMove, wParam, lParam, false, MouseButton::ePrimary,
+ MOUSE_INPUT_SOURCE(),
+ mPointerEvents.GetCachedPointerInfo(msg, wParam));
+ DispatchPendingEvents();
+ }
+ } break;
+
+ case WM_NCMOUSEMOVE: {
+ LPARAM lParamClient = lParamToClient(lParam);
+ if (IsSimulatedClientArea(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))) {
+ if (!sIsInMouseCapture) {
+ TRACKMOUSEEVENT mTrack;
+ mTrack.cbSize = sizeof(TRACKMOUSEEVENT);
+ mTrack.dwFlags = TME_LEAVE | TME_NONCLIENT;
+ mTrack.dwHoverTime = 0;
+ mTrack.hwndTrack = mWnd;
+ TrackMouseEvent(&mTrack);
+ }
+ // If we noticed the mouse moving in our draggable region, forward the
+ // message as a normal WM_MOUSEMOVE.
+ SendMessage(mWnd, WM_MOUSEMOVE, 0, lParamClient);
+ } else {
+ // We've transitioned from a draggable area to somewhere else within
+ // the non-client area - perhaps one of the edges of the window for
+ // resizing.
+ mSimulatedClientArea = false;
+ }
+
+ if (mMousePresent && !sIsInMouseCapture && !mSimulatedClientArea) {
+ SendMessage(mWnd, WM_MOUSELEAVE, 0, 0);
+ }
+ } break;
+
+ case WM_LBUTTONDOWN: {
+ result =
+ DispatchMouseEvent(eMouseDown, wParam, lParam, false,
+ MouseButton::ePrimary, MOUSE_INPUT_SOURCE(),
+ mPointerEvents.GetCachedPointerInfo(msg, wParam));
+ DispatchPendingEvents();
+ } break;
+
+ case WM_LBUTTONUP: {
+ result =
+ DispatchMouseEvent(eMouseUp, wParam, lParam, false,
+ MouseButton::ePrimary, MOUSE_INPUT_SOURCE(),
+ mPointerEvents.GetCachedPointerInfo(msg, wParam));
+ DispatchPendingEvents();
+ } break;
+
+ case WM_NCMOUSELEAVE: {
+ mSimulatedClientArea = false;
+
+ if (EventIsInsideWindow(this)) {
+ // If we're handling WM_NCMOUSELEAVE and the mouse is still over the
+ // window, then by process of elimination, the mouse has moved from the
+ // non-client to client area, so no need to fall-through to the
+ // WM_MOUSELEAVE handler. We also need to re-register for the
+ // WM_MOUSELEAVE message, since according to the documentation at [1],
+ // all tracking requested via TrackMouseEvent is cleared once
+ // WM_NCMOUSELEAVE or WM_MOUSELEAVE fires.
+ // [1]:
+ // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-trackmouseevent
+ TRACKMOUSEEVENT mTrack;
+ mTrack.cbSize = sizeof(TRACKMOUSEEVENT);
+ mTrack.dwFlags = TME_LEAVE;
+ mTrack.dwHoverTime = 0;
+ mTrack.hwndTrack = mWnd;
+ TrackMouseEvent(&mTrack);
+ break;
+ }
+ // We've transitioned from non-client to outside of the window, so
+ // fall-through to the WM_MOUSELEAVE handler.
+ [[fallthrough]];
+ }
+ case WM_MOUSELEAVE: {
+ if (!mMousePresent) break;
+ if (mSimulatedClientArea) break;
+ mMousePresent = false;
+
+ // Check if the mouse is over the fullscreen transition window, if so
+ // clear sLastMouseMovePoint. This way the WM_MOUSEMOVE we get after the
+ // transition window disappears will not be ignored, even if the mouse
+ // hasn't moved.
+ if (mTransitionWnd && WindowAtMouse() == mTransitionWnd) {
+ sLastMouseMovePoint = {0};
+ }
+
+ // We need to check mouse button states and put them in for
+ // wParam.
+ WPARAM mouseState = (GetKeyState(VK_LBUTTON) ? MK_LBUTTON : 0) |
+ (GetKeyState(VK_MBUTTON) ? MK_MBUTTON : 0) |
+ (GetKeyState(VK_RBUTTON) ? MK_RBUTTON : 0);
+ // Synthesize an event position because we don't get one from
+ // WM_MOUSELEAVE.
+ LPARAM pos = lParamToClient(::GetMessagePos());
+ DispatchMouseEvent(eMouseExitFromWidget, mouseState, pos, false,
+ MouseButton::ePrimary, MOUSE_INPUT_SOURCE());
+ } break;
+
+ case WM_CONTEXTMENU: {
+ // If the context menu is brought up by a touch long-press, then
+ // the APZ code is responsible for dealing with this, so we don't
+ // need to do anything.
+ if (mTouchWindow &&
+ MOUSE_INPUT_SOURCE() == MouseEvent_Binding::MOZ_SOURCE_TOUCH) {
+ MOZ_ASSERT(mAPZC); // since mTouchWindow is true, APZ must be enabled
+ result = true;
+ break;
+ }
+
+ // If this WM_CONTEXTMENU is triggered by a mouse's secondary button up
+ // event in overscroll gutter, we shouldn't open context menu.
+ if (MOUSE_INPUT_SOURCE() == MouseEvent_Binding::MOZ_SOURCE_MOUSE &&
+ mNeedsToPreventContextMenu) {
+ result = true;
+ break;
+ }
+
+ // if the context menu is brought up from the keyboard, |lParam|
+ // will be -1.
+ LPARAM pos;
+ bool contextMenukey = false;
+ if (lParam == -1) {
+ contextMenukey = true;
+ pos = lParamToClient(GetMessagePos());
+ } else {
+ pos = lParamToClient(lParam);
+ }
+
+ result = DispatchMouseEvent(
+ eContextMenu, wParam, pos, contextMenukey,
+ contextMenukey ? MouseButton::ePrimary : MouseButton::eSecondary,
+ MOUSE_INPUT_SOURCE());
+ if (lParam != -1 && !result && mCustomNonClient &&
+ mDraggableRegion.Contains(GET_X_LPARAM(pos), GET_Y_LPARAM(pos))) {
+ // Blank area hit, throw up the system menu.
+ DisplaySystemMenu(mWnd, mFrameState->GetSizeMode(), mIsRTL,
+ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+ result = true;
+ }
+ } break;
+
+ case WM_POINTERLEAVE:
+ case WM_POINTERDOWN:
+ case WM_POINTERUP:
+ case WM_POINTERUPDATE:
+ result = OnPointerEvents(msg, wParam, lParam);
+ if (result) {
+ DispatchPendingEvents();
+ }
+ break;
+
+ case DM_POINTERHITTEST:
+ if (mDmOwner) {
+ UINT contactId = GET_POINTERID_WPARAM(wParam);
+ POINTER_INPUT_TYPE pointerType;
+ if (mPointerEvents.GetPointerType(contactId, &pointerType) &&
+ pointerType == PT_TOUCHPAD) {
+ mDmOwner->SetContact(contactId);
+ }
+ }
+ break;
+
+ case WM_LBUTTONDBLCLK:
+ result = DispatchMouseEvent(eMouseDoubleClick, wParam, lParam, false,
+ MouseButton::ePrimary, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_MBUTTONDOWN:
+ result = DispatchMouseEvent(eMouseDown, wParam, lParam, false,
+ MouseButton::eMiddle, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_MBUTTONUP:
+ result = DispatchMouseEvent(eMouseUp, wParam, lParam, false,
+ MouseButton::eMiddle, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_MBUTTONDBLCLK:
+ result = DispatchMouseEvent(eMouseDoubleClick, wParam, lParam, false,
+ MouseButton::eMiddle, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_NCMBUTTONDOWN:
+ result = DispatchMouseEvent(eMouseDown, 0, lParamToClient(lParam), false,
+ MouseButton::eMiddle, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_NCMBUTTONUP:
+ result = DispatchMouseEvent(eMouseUp, 0, lParamToClient(lParam), false,
+ MouseButton::eMiddle, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_NCMBUTTONDBLCLK:
+ result =
+ DispatchMouseEvent(eMouseDoubleClick, 0, lParamToClient(lParam),
+ false, MouseButton::eMiddle, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_RBUTTONDOWN:
+ result =
+ DispatchMouseEvent(eMouseDown, wParam, lParam, false,
+ MouseButton::eSecondary, MOUSE_INPUT_SOURCE(),
+ mPointerEvents.GetCachedPointerInfo(msg, wParam));
+ DispatchPendingEvents();
+ break;
+
+ case WM_RBUTTONUP:
+ result =
+ DispatchMouseEvent(eMouseUp, wParam, lParam, false,
+ MouseButton::eSecondary, MOUSE_INPUT_SOURCE(),
+ mPointerEvents.GetCachedPointerInfo(msg, wParam));
+ DispatchPendingEvents();
+ break;
+
+ case WM_RBUTTONDBLCLK:
+ result =
+ DispatchMouseEvent(eMouseDoubleClick, wParam, lParam, false,
+ MouseButton::eSecondary, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_NCRBUTTONDOWN:
+ result =
+ DispatchMouseEvent(eMouseDown, 0, lParamToClient(lParam), false,
+ MouseButton::eSecondary, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_NCRBUTTONUP:
+ result =
+ DispatchMouseEvent(eMouseUp, 0, lParamToClient(lParam), false,
+ MouseButton::eSecondary, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_NCRBUTTONDBLCLK:
+ result = DispatchMouseEvent(eMouseDoubleClick, 0, lParamToClient(lParam),
+ false, MouseButton::eSecondary,
+ MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ // Windows doesn't provide to customize the behavior of 4th nor 5th button
+ // of mouse. If 5-button mouse works with standard mouse deriver of
+ // Windows, users cannot disable 4th button (browser back) nor 5th button
+ // (browser forward). We should allow to do it with our prefs since we can
+ // prevent Windows to generate WM_APPCOMMAND message if WM_XBUTTONUP
+ // messages are not sent to DefWindowProc.
+ case WM_XBUTTONDOWN:
+ case WM_XBUTTONUP:
+ case WM_NCXBUTTONDOWN:
+ case WM_NCXBUTTONUP:
+ *aRetValue = TRUE;
+ switch (GET_XBUTTON_WPARAM(wParam)) {
+ case XBUTTON1:
+ result = !Preferences::GetBool("mousebutton.4th.enabled", true);
+ break;
+ case XBUTTON2:
+ result = !Preferences::GetBool("mousebutton.5th.enabled", true);
+ break;
+ default:
+ break;
+ }
+ break;
+
+ case WM_SIZING: {
+ if (mAspectRatio > 0) {
+ LPRECT rect = (LPRECT)lParam;
+ int32_t newWidth, newHeight;
+
+ // The following conditions and switch statement borrow heavily from the
+ // Chromium source code from
+ // https://chromium.googlesource.com/chromium/src/+/456d6e533cfb4531995e0ef52c279d4b5aa8a352/ui/views/window/window_resize_utils.cc#45
+ if (wParam == WMSZ_LEFT || wParam == WMSZ_RIGHT ||
+ wParam == WMSZ_TOPLEFT || wParam == WMSZ_BOTTOMLEFT) {
+ newWidth = rect->right - rect->left;
+ newHeight = newWidth / mAspectRatio;
+ if (newHeight < mSizeConstraints.mMinSize.height) {
+ newHeight = mSizeConstraints.mMinSize.height;
+ newWidth = newHeight * mAspectRatio;
+ } else if (newHeight > mSizeConstraints.mMaxSize.height) {
+ newHeight = mSizeConstraints.mMaxSize.height;
+ newWidth = newHeight * mAspectRatio;
+ }
+ } else {
+ newHeight = rect->bottom - rect->top;
+ newWidth = newHeight * mAspectRatio;
+ if (newWidth < mSizeConstraints.mMinSize.width) {
+ newWidth = mSizeConstraints.mMinSize.width;
+ newHeight = newWidth / mAspectRatio;
+ } else if (newWidth > mSizeConstraints.mMaxSize.width) {
+ newWidth = mSizeConstraints.mMaxSize.width;
+ newHeight = newWidth / mAspectRatio;
+ }
+ }
+
+ switch (wParam) {
+ case WMSZ_RIGHT:
+ case WMSZ_BOTTOM:
+ rect->right = newWidth + rect->left;
+ rect->bottom = rect->top + newHeight;
+ break;
+ case WMSZ_TOP:
+ rect->right = newWidth + rect->left;
+ rect->top = rect->bottom - newHeight;
+ break;
+ case WMSZ_LEFT:
+ case WMSZ_TOPLEFT:
+ rect->left = rect->right - newWidth;
+ rect->top = rect->bottom - newHeight;
+ break;
+ case WMSZ_TOPRIGHT:
+ rect->right = rect->left + newWidth;
+ rect->top = rect->bottom - newHeight;
+ break;
+ case WMSZ_BOTTOMLEFT:
+ rect->left = rect->right - newWidth;
+ rect->bottom = rect->top + newHeight;
+ break;
+ case WMSZ_BOTTOMRIGHT:
+ rect->right = rect->left + newWidth;
+ rect->bottom = rect->top + newHeight;
+ break;
+ }
+ }
+
+ // When we get WM_ENTERSIZEMOVE we don't know yet if we're in a live
+ // resize or move event. Instead we wait for first VM_SIZING message
+ // within a ENTERSIZEMOVE to consider this a live resize event.
+ if (mResizeState == IN_SIZEMOVE) {
+ mResizeState = RESIZING;
+ NotifyLiveResizeStarted();
+ }
+ break;
+ }
+
+ case WM_MOVING:
+ FinishLiveResizing(MOVING);
+ if (WinUtils::IsPerMonitorDPIAware()) {
+ // Sometimes, we appear to miss a WM_DPICHANGED message while moving
+ // a window around. Therefore, call ChangedDPI and ResetLayout here
+ // if it appears that the window's scaling is not what we expect.
+ // This causes the prescontext and appshell window management code to
+ // check the appUnitsPerDevPixel value and current widget size, and
+ // refresh them if necessary. If nothing has changed, these calls will
+ // return without actually triggering any extra reflow or painting.
+ if (WinUtils::LogToPhysFactor(mWnd) != mDefaultScale) {
+ ChangedDPI();
+ ResetLayout();
+ if (mWidgetListener) {
+ mWidgetListener->UIResolutionChanged();
+ }
+ }
+ }
+ break;
+
+ case WM_ENTERSIZEMOVE: {
+ if (mResizeState == NOT_RESIZING) {
+ mResizeState = IN_SIZEMOVE;
+ }
+ break;
+ }
+
+ case WM_EXITSIZEMOVE: {
+ FinishLiveResizing(NOT_RESIZING);
+
+ if (!sIsInMouseCapture) {
+ NotifySizeMoveDone();
+ }
+
+ // Windows spins a separate hidden event loop when moving a window so we
+ // don't hear mouse events during this time and WM_EXITSIZEMOVE is fired
+ // when the hidden event loop exits. We set mDraggingWindowWithMouse to
+ // true in WM_NCLBUTTONDOWN when we started moving the window with the
+ // mouse so we know that if mDraggingWindowWithMouse is true, we can send
+ // a mouse up event.
+ if (mDraggingWindowWithMouse) {
+ mDraggingWindowWithMouse = false;
+ result = DispatchMouseEvent(
+ eMouseUp, wParam, lParam, false, MouseButton::ePrimary,
+ MOUSE_INPUT_SOURCE(),
+ mPointerEvents.GetCachedPointerInfo(msg, wParam));
+ }
+
+ break;
+ }
+
+ case WM_DISPLAYCHANGE: {
+ ScreenHelperWin::RefreshScreens();
+ if (mWidgetListener) {
+ mWidgetListener->UIResolutionChanged();
+ }
+ break;
+ }
+
+ case WM_NCLBUTTONDBLCLK:
+ DispatchMouseEvent(eMouseDoubleClick, 0, lParamToClient(lParam), false,
+ MouseButton::ePrimary, MOUSE_INPUT_SOURCE());
+ result = DispatchMouseEvent(eMouseUp, 0, lParamToClient(lParam), false,
+ MouseButton::ePrimary, MOUSE_INPUT_SOURCE());
+ DispatchPendingEvents();
+ break;
+
+ case WM_NCLBUTTONDOWN: {
+ // Dispatch a custom event when this happens in the draggable region, so
+ // that non-popup-based panels can react to it. This doesn't send an
+ // actual mousedown event because that would break dragging or interfere
+ // with other mousedown handling in the caption area.
+ if (ClientMarginHitTestPoint(GET_X_LPARAM(lParam),
+ GET_Y_LPARAM(lParam)) == HTCAPTION) {
+ DispatchCustomEvent(u"draggableregionleftmousedown"_ns);
+ mDraggingWindowWithMouse = true;
+ }
+
+ if (IsWindowButton(wParam) && mCustomNonClient) {
+ DispatchMouseEvent(eMouseDown, wParamFromGlobalMouseState(),
+ lParamToClient(lParam), false, MouseButton::ePrimary,
+ MOUSE_INPUT_SOURCE(), nullptr, true);
+ DispatchPendingEvents();
+ result = true;
+ }
+ break;
+ }
+
+ case WM_APPCOMMAND: {
+ MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd);
+ result = HandleAppCommandMsg(nativeMsg, aRetValue);
+ break;
+ }
+
+ // The WM_ACTIVATE event is fired when a window is raised or lowered,
+ // and the loword of wParam specifies which. But we don't want to tell
+ // the focus system about this until the WM_SETFOCUS or WM_KILLFOCUS
+ // events are fired. Instead, set either the sJustGotActivate or
+ // gJustGotDeactivate flags and activate/deactivate once the focus
+ // events arrive.
+ case WM_ACTIVATE: {
+ int32_t fActive = LOWORD(wParam);
+ if (mWidgetListener) {
+ if (WA_INACTIVE == fActive) {
+ // when minimizing a window, the deactivation and focus events will
+ // be fired in the reverse order. Instead, just deactivate right away.
+ // This can also happen when a modal system dialog is opened, so check
+ // if the last window to receive the WM_KILLFOCUS message was this one
+ // or a child of this one.
+ if (HIWORD(wParam) ||
+ (mLastKillFocusWindow &&
+ (GetTopLevelForFocus(mLastKillFocusWindow) == mWnd))) {
+ DispatchFocusToTopLevelWindow(false);
+ } else {
+ sJustGotDeactivate = true;
+ }
+ if (mIsTopWidgetWindow) {
+ mLastKeyboardLayout = KeyboardLayout::GetLayout();
+ }
+ } else {
+ StopFlashing();
+
+ sJustGotActivate = true;
+ WidgetMouseEvent event(true, eMouseActivate, this,
+ WidgetMouseEvent::eReal);
+ InitEvent(event);
+ ModifierKeyState modifierKeyState;
+ modifierKeyState.InitInputEvent(event);
+ DispatchInputEvent(&event);
+ if (sSwitchKeyboardLayout && mLastKeyboardLayout)
+ ActivateKeyboardLayout(mLastKeyboardLayout, 0);
+
+#ifdef ACCESSIBILITY
+ a11y::LazyInstantiator::ResetUiaDetectionCache();
+#endif
+ }
+ }
+ } break;
+
+ case WM_ACTIVATEAPP: {
+ // Bug 1851991: Sometimes this can be called before gfxPlatform::Init
+ // when a window is created very early. In that case we just forego
+ // setting this and accept the GPU process might briefly run at a lower
+ // priority.
+ if (GPUProcessManager::Get()) {
+ GPUProcessManager::Get()->SetAppInForeground(wParam);
+ }
+ } break;
+
+ case WM_MOUSEACTIVATE:
+ // A popup with a parent owner should not be activated when clicked but
+ // should still allow the mouse event to be fired, so the return value
+ // is set to MA_NOACTIVATE. But if the owner isn't the frontmost window,
+ // just use default processing so that the window is activated.
+ if (IsPopup() && IsOwnerForegroundWindow()) {
+ *aRetValue = MA_NOACTIVATE;
+ result = true;
+ }
+ break;
+
+ case WM_WINDOWPOSCHANGING: {
+ LPWINDOWPOS info = (LPWINDOWPOS)lParam;
+ OnWindowPosChanging(info);
+ result = true;
+ } break;
+
+ // Workaround for race condition in explorer.exe.
+ case MOZ_WM_FULLSCREEN_STATE_UPDATE: {
+ TaskbarConcealer::OnAsyncStateUpdateRequest(mWnd);
+ result = true;
+ } break;
+
+ case WM_GETMINMAXINFO: {
+ MINMAXINFO* mmi = (MINMAXINFO*)lParam;
+ // Set the constraints. The minimum size should also be constrained to the
+ // default window maximum size so that it fits on screen.
+ mmi->ptMinTrackSize.x =
+ std::min((int32_t)mmi->ptMaxTrackSize.x,
+ std::max((int32_t)mmi->ptMinTrackSize.x,
+ mSizeConstraints.mMinSize.width));
+ mmi->ptMinTrackSize.y =
+ std::min((int32_t)mmi->ptMaxTrackSize.y,
+ std::max((int32_t)mmi->ptMinTrackSize.y,
+ mSizeConstraints.mMinSize.height));
+ mmi->ptMaxTrackSize.x = std::min((int32_t)mmi->ptMaxTrackSize.x,
+ mSizeConstraints.mMaxSize.width);
+ mmi->ptMaxTrackSize.y = std::min((int32_t)mmi->ptMaxTrackSize.y,
+ mSizeConstraints.mMaxSize.height);
+ } break;
+
+ case WM_SETFOCUS: {
+ WndProcUrgentInvocation::Marker _marker;
+
+ // If previous focused window isn't ours, it must have received the
+ // redirected message. So, we should forget it.
+ if (!WinUtils::IsOurProcessWindow(HWND(wParam))) {
+ RedirectedKeyDownMessageManager::Forget();
+ }
+ if (sJustGotActivate) {
+ DispatchFocusToTopLevelWindow(true);
+ }
+ TaskbarConcealer::OnFocusAcquired(this);
+ } break;
+
+ case WM_KILLFOCUS:
+ if (sJustGotDeactivate) {
+ DispatchFocusToTopLevelWindow(false);
+ } else {
+ mLastKillFocusWindow = mWnd;
+ }
+ break;
+
+ case WM_WINDOWPOSCHANGED: {
+ WINDOWPOS* wp = (LPWINDOWPOS)lParam;
+ OnWindowPosChanged(wp);
+ TaskbarConcealer::OnWindowPosChanged(this);
+ result = true;
+ } break;
+
+ case WM_INPUTLANGCHANGEREQUEST:
+ *aRetValue = TRUE;
+ result = false;
+ break;
+
+ case WM_INPUTLANGCHANGE:
+ KeyboardLayout::GetInstance()->OnLayoutChange(
+ reinterpret_cast<HKL>(lParam));
+ nsBidiKeyboard::OnLayoutChange();
+ result = false; // always pass to child window
+ break;
+
+ case WM_DESTROYCLIPBOARD: {
+ nsIClipboard* clipboard;
+ nsresult rv = CallGetService(kCClipboardCID, &clipboard);
+ if (NS_SUCCEEDED(rv)) {
+ clipboard->EmptyClipboard(nsIClipboard::kGlobalClipboard);
+ NS_RELEASE(clipboard);
+ }
+ } break;
+
+#ifdef ACCESSIBILITY
+ case WM_GETOBJECT: {
+ *aRetValue = 0;
+ // Do explicit casting to make it working on 64bit systems (see bug 649236
+ // for details).
+ int32_t objId = static_cast<DWORD>(lParam);
+ if (objId == OBJID_CLIENT) { // oleacc.dll will be loaded dynamically
+ RefPtr<IAccessible> root(
+ a11y::LazyInstantiator::GetRootAccessible(mWnd));
+ if (root) {
+ *aRetValue = LresultFromObject(IID_IAccessible, wParam, root);
+ a11y::LazyInstantiator::EnableBlindAggregation(mWnd);
+ result = true;
+ }
+ }
+ } break;
+#endif
+
+ case WM_SYSCOMMAND: {
+ WPARAM const filteredWParam = (wParam & 0xFFF0);
+
+ // SC_CLOSE may trigger a synchronous confirmation prompt. If we're in the
+ // middle of something important, put off responding to it.
+ if (filteredWParam == SC_CLOSE && WndProcUrgentInvocation::IsActive()) {
+ ::PostMessageW(mWnd, msg, wParam, lParam);
+ result = true;
+ break;
+ }
+
+ if (mFrameState->GetSizeMode() == nsSizeMode_Fullscreen &&
+ filteredWParam == SC_RESTORE &&
+ GetCurrentShowCmd(mWnd) != SW_SHOWMINIMIZED) {
+ mFrameState->EnsureFullscreenMode(false);
+ result = true;
+ }
+
+ // Handle the system menu manually when we're in full screen mode
+ // so we can set the appropriate options.
+ if (filteredWParam == SC_KEYMENU && lParam == VK_SPACE &&
+ mFrameState->GetSizeMode() == nsSizeMode_Fullscreen) {
+ DisplaySystemMenu(mWnd, mFrameState->GetSizeMode(), mIsRTL,
+ MOZ_SYSCONTEXT_X_POS, MOZ_SYSCONTEXT_Y_POS);
+ result = true;
+ }
+ } break;
+
+ case WM_DPICHANGED: {
+ LPRECT rect = (LPRECT)lParam;
+ OnDPIChanged(rect->left, rect->top, rect->right - rect->left,
+ rect->bottom - rect->top);
+ break;
+ }
+
+ /* Gesture support events */
+ case WM_TABLET_QUERYSYSTEMGESTURESTATUS:
+ // According to MS samples, this must be handled to enable
+ // rotational support in multi-touch drivers.
+ result = true;
+ *aRetValue = TABLET_ROTATE_GESTURE_ENABLE;
+ break;
+
+ case WM_TOUCH:
+ result = OnTouch(wParam, lParam);
+ if (result) {
+ *aRetValue = 0;
+ }
+ break;
+
+ case WM_GESTURE:
+ result = OnGesture(wParam, lParam);
+ break;
+
+ case WM_GESTURENOTIFY: {
+ if (mWindowType != WindowType::Invisible) {
+ // A GestureNotify event is dispatched to decide which single-finger
+ // panning direction should be active (including none) and if pan
+ // feedback should be displayed. Java and plugin windows can make their
+ // own calls.
+
+ GESTURENOTIFYSTRUCT* gestureinfo = (GESTURENOTIFYSTRUCT*)lParam;
+ nsPointWin touchPoint;
+ touchPoint = gestureinfo->ptsLocation;
+ touchPoint.ScreenToClient(mWnd);
+ WidgetGestureNotifyEvent gestureNotifyEvent(true, eGestureNotify, this);
+ gestureNotifyEvent.mRefPoint =
+ LayoutDeviceIntPoint::FromUnknownPoint(touchPoint);
+ nsEventStatus status;
+ DispatchEvent(&gestureNotifyEvent, status);
+ mDisplayPanFeedback = gestureNotifyEvent.mDisplayPanFeedback;
+ if (!mTouchWindow)
+ mGesture.SetWinGestureSupport(mWnd, gestureNotifyEvent.mPanDirection);
+ }
+ result = false; // should always bubble to DefWindowProc
+ } break;
+
+ case WM_CLEAR: {
+ WidgetContentCommandEvent command(true, eContentCommandDelete, this);
+ DispatchWindowEvent(command);
+ result = true;
+ } break;
+
+ case WM_CUT: {
+ WidgetContentCommandEvent command(true, eContentCommandCut, this);
+ DispatchWindowEvent(command);
+ result = true;
+ } break;
+
+ case WM_COPY: {
+ WidgetContentCommandEvent command(true, eContentCommandCopy, this);
+ DispatchWindowEvent(command);
+ result = true;
+ } break;
+
+ case WM_PASTE: {
+ WidgetContentCommandEvent command(true, eContentCommandPaste, this);
+ DispatchWindowEvent(command);
+ result = true;
+ } break;
+
+ case EM_UNDO: {
+ WidgetContentCommandEvent command(true, eContentCommandUndo, this);
+ DispatchWindowEvent(command);
+ *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled);
+ result = true;
+ } break;
+
+ case EM_REDO: {
+ WidgetContentCommandEvent command(true, eContentCommandRedo, this);
+ DispatchWindowEvent(command);
+ *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled);
+ result = true;
+ } break;
+
+ case EM_CANPASTE: {
+ // Support EM_CANPASTE message only when wParam isn't specified or
+ // is plain text format.
+ if (wParam == 0 || wParam == CF_TEXT || wParam == CF_UNICODETEXT) {
+ WidgetContentCommandEvent command(true, eContentCommandPaste, this,
+ true);
+ DispatchWindowEvent(command);
+ *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled);
+ result = true;
+ }
+ } break;
+
+ case EM_CANUNDO: {
+ WidgetContentCommandEvent command(true, eContentCommandUndo, this, true);
+ DispatchWindowEvent(command);
+ *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled);
+ result = true;
+ } break;
+
+ case EM_CANREDO: {
+ WidgetContentCommandEvent command(true, eContentCommandRedo, this, true);
+ DispatchWindowEvent(command);
+ *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled);
+ result = true;
+ } break;
+
+ case MOZ_WM_SKEWFIX: {
+ TimeStamp skewStamp;
+ if (CurrentWindowsTimeGetter::GetAndClearBackwardsSkewStamp(wParam,
+ &skewStamp)) {
+ TimeConverter().CompensateForBackwardsSkew(::GetMessageTime(),
+ skewStamp);
+ }
+ } break;
+
+ default: {
+ if (msg == nsAppShell::GetTaskbarButtonCreatedMessage()) {
+ SetHasTaskbarIconBeenCreated();
+ }
+ } break;
+ }
+
+ //*aRetValue = result;
+ if (mWnd) {
+ return result;
+ } else {
+ // Events which caused mWnd destruction and aren't consumed
+ // will crash during the Windows default processing.
+ return true;
+ }
+}
+
+void nsWindow::FinishLiveResizing(ResizeState aNewState) {
+ if (mResizeState == RESIZING) {
+ NotifyLiveResizeStopped();
+ }
+ mResizeState = aNewState;
+ ForcePresent();
+}
+
+/**************************************************************
+ *
+ * SECTION: Event processing helpers
+ *
+ * Special processing for certain event types and
+ * synthesized events.
+ *
+ **************************************************************/
+
+LayoutDeviceIntMargin nsWindow::NonClientSizeMargin(
+ const LayoutDeviceIntMargin& aNonClientOffset) const {
+ return LayoutDeviceIntMargin(mCaptionHeight - aNonClientOffset.top,
+ mHorResizeMargin - aNonClientOffset.right,
+ mVertResizeMargin - aNonClientOffset.bottom,
+ mHorResizeMargin - aNonClientOffset.left);
+}
+
+int32_t nsWindow::ClientMarginHitTestPoint(int32_t aX, int32_t aY) {
+ const nsSizeMode sizeMode = mFrameState->GetSizeMode();
+ if (sizeMode == nsSizeMode_Minimized || sizeMode == nsSizeMode_Fullscreen) {
+ return HTCLIENT;
+ }
+
+ // Calculations are done in screen coords
+ const LayoutDeviceIntRect winRect = GetScreenBounds();
+ const LayoutDeviceIntPoint point(aX, aY);
+
+ // hit return constants:
+ // HTBORDER - non-resizable border
+ // HTBOTTOM, HTLEFT, HTRIGHT, HTTOP - resizable border
+ // HTBOTTOMLEFT, HTBOTTOMRIGHT - resizable corner
+ // HTTOPLEFT, HTTOPRIGHT - resizable corner
+ // HTCAPTION - general title bar area
+ // HTCLIENT - area considered the client
+ // HTCLOSE - hovering over the close button
+ // HTMAXBUTTON - maximize button
+ // HTMINBUTTON - minimize button
+
+ int32_t testResult = HTCLIENT;
+ const bool isResizable =
+ sizeMode != nsSizeMode_Maximized &&
+ (mBorderStyle &
+ (BorderStyle::All | BorderStyle::ResizeH | BorderStyle::Default));
+
+ LayoutDeviceIntMargin nonClientSizeMargin = NonClientSizeMargin();
+
+ // Ensure being accessible to borders of window. Even if contents are in
+ // this area, the area must behave as border.
+ nonClientSizeMargin.EnsureAtLeast(
+ LayoutDeviceIntMargin(kResizableBorderMinSize, kResizableBorderMinSize,
+ kResizableBorderMinSize, kResizableBorderMinSize));
+
+ LayoutDeviceIntRect clientRect = winRect;
+ clientRect.Deflate(nonClientSizeMargin);
+
+ const bool allowContentOverride =
+ sizeMode == nsSizeMode_Maximized || clientRect.Contains(point);
+
+ // The border size. If there is no content under mouse cursor, the border
+ // size should be larger than the values in system settings. Otherwise,
+ // contents under the mouse cursor should be able to override the behavior.
+ // E.g., user must expect that Firefox button always opens the popup menu
+ // even when the user clicks on the above edge of it.
+ LayoutDeviceIntMargin borderSize = nonClientSizeMargin;
+ borderSize.EnsureAtLeast(
+ LayoutDeviceIntMargin(mVertResizeMargin, mHorResizeMargin,
+ mVertResizeMargin, mHorResizeMargin));
+
+ bool top = false;
+ bool bottom = false;
+ bool left = false;
+ bool right = false;
+
+ if (point.y >= winRect.y && point.y < winRect.y + borderSize.top) {
+ top = true;
+ } else if (point.y <= winRect.YMost() &&
+ point.y > winRect.YMost() - borderSize.bottom) {
+ bottom = true;
+ }
+
+ // (the 2x case here doubles the resize area for corners)
+ int multiplier = (top || bottom) ? 2 : 1;
+ if (point.x >= winRect.x &&
+ point.x < winRect.x + (multiplier * borderSize.left)) {
+ left = true;
+ } else if (point.x <= winRect.XMost() &&
+ point.x > winRect.XMost() - (multiplier * borderSize.right)) {
+ right = true;
+ }
+
+ bool inResizeRegion = false;
+ if (isResizable) {
+ if (top) {
+ testResult = HTTOP;
+ if (left) {
+ testResult = HTTOPLEFT;
+ } else if (right) {
+ testResult = HTTOPRIGHT;
+ }
+ } else if (bottom) {
+ testResult = HTBOTTOM;
+ if (left) {
+ testResult = HTBOTTOMLEFT;
+ } else if (right) {
+ testResult = HTBOTTOMRIGHT;
+ }
+ } else {
+ if (left) {
+ testResult = HTLEFT;
+ }
+ if (right) {
+ testResult = HTRIGHT;
+ }
+ }
+ inResizeRegion = (testResult != HTCLIENT);
+ } else {
+ if (top) {
+ testResult = HTCAPTION;
+ } else if (bottom || left || right) {
+ testResult = HTBORDER;
+ }
+ }
+
+ if (!sIsInMouseCapture && allowContentOverride) {
+ {
+ POINT pt = {aX, aY};
+ ::ScreenToClient(mWnd, &pt);
+
+ if (pt.x == mCachedHitTestPoint.x.value &&
+ pt.y == mCachedHitTestPoint.y.value &&
+ TimeStamp::Now() - mCachedHitTestTime <
+ TimeDuration::FromMilliseconds(HITTEST_CACHE_LIFETIME_MS)) {
+ return mCachedHitTestResult;
+ }
+
+ mCachedHitTestPoint = {pt.x, pt.y};
+ mCachedHitTestTime = TimeStamp::Now();
+ }
+
+ auto pt = mCachedHitTestPoint;
+
+ if (mWindowBtnRect[WindowButtonType::Minimize].Contains(pt)) {
+ testResult = HTMINBUTTON;
+ } else if (mWindowBtnRect[WindowButtonType::Maximize].Contains(pt)) {
+ testResult = HTMAXBUTTON;
+ } else if (mWindowBtnRect[WindowButtonType::Close].Contains(pt)) {
+ testResult = HTCLOSE;
+ } else if (!inResizeRegion) {
+ // If we're in the resize region, avoid overriding that with either a
+ // drag or a client result; resize takes priority over either (but not
+ // over the window controls, which is why we check this after those).
+ if (mDraggableRegion.Contains(pt)) {
+ testResult = HTCAPTION;
+ } else {
+ testResult = HTCLIENT;
+ }
+ }
+
+ mCachedHitTestResult = testResult;
+ }
+
+ return testResult;
+}
+
+bool nsWindow::IsSimulatedClientArea(int32_t screenX, int32_t screenY) {
+ int32_t testResult = ClientMarginHitTestPoint(screenX, screenY);
+ return testResult == HTCAPTION || IsWindowButton(testResult);
+}
+
+bool nsWindow::IsWindowButton(int32_t hitTestResult) {
+ return hitTestResult == HTMINBUTTON || hitTestResult == HTMAXBUTTON ||
+ hitTestResult == HTCLOSE;
+}
+
+TimeStamp nsWindow::GetMessageTimeStamp(LONG aEventTime) const {
+ CurrentWindowsTimeGetter getCurrentTime(mWnd);
+ return TimeConverter().GetTimeStampFromSystemTime(aEventTime, getCurrentTime);
+}
+
+void nsWindow::PostSleepWakeNotification(const bool aIsSleepMode) {
+ // Retain the previous mode that was notified to observers
+ static bool sWasSleepMode = false;
+
+ // Only notify observers if mode changed
+ if (aIsSleepMode == sWasSleepMode) return;
+
+ sWasSleepMode = aIsSleepMode;
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService)
+ observerService->NotifyObservers(nullptr,
+ aIsSleepMode
+ ? NS_WIDGET_SLEEP_OBSERVER_TOPIC
+ : NS_WIDGET_WAKE_OBSERVER_TOPIC,
+ nullptr);
+}
+
+LRESULT nsWindow::ProcessCharMessage(const MSG& aMsg, bool* aEventDispatched) {
+ if (IMEHandler::IsComposingOn(this)) {
+ IMEHandler::NotifyIME(this, REQUEST_TO_COMMIT_COMPOSITION);
+ }
+ // These must be checked here too as a lone WM_CHAR could be received
+ // if a child window didn't handle it (for example Alt+Space in a content
+ // window)
+ ModifierKeyState modKeyState;
+ NativeKey nativeKey(this, aMsg, modKeyState);
+ return static_cast<LRESULT>(nativeKey.HandleCharMessage(aEventDispatched));
+}
+
+LRESULT nsWindow::ProcessKeyUpMessage(const MSG& aMsg, bool* aEventDispatched) {
+ ModifierKeyState modKeyState;
+ NativeKey nativeKey(this, aMsg, modKeyState);
+ bool result = nativeKey.HandleKeyUpMessage(aEventDispatched);
+ if (aMsg.wParam == VK_F10) {
+ // Bug 1382199: Windows default behavior will trigger the System menu bar
+ // when F10 is released. Among other things, this causes the System menu bar
+ // to appear when a web page overrides the contextmenu event. We *never*
+ // want this default behavior, so eat this key (never pass it to Windows).
+ return true;
+ }
+ return result;
+}
+
+LRESULT nsWindow::ProcessKeyDownMessage(const MSG& aMsg,
+ bool* aEventDispatched) {
+ // If this method doesn't call NativeKey::HandleKeyDownMessage(), this method
+ // must clean up the redirected message information itself. For more
+ // information, see above comment of
+ // RedirectedKeyDownMessageManager::AutoFlusher class definition in
+ // KeyboardLayout.h.
+ RedirectedKeyDownMessageManager::AutoFlusher redirectedMsgFlusher(this, aMsg);
+
+ ModifierKeyState modKeyState;
+
+ NativeKey nativeKey(this, aMsg, modKeyState);
+ LRESULT result =
+ static_cast<LRESULT>(nativeKey.HandleKeyDownMessage(aEventDispatched));
+ // HandleKeyDownMessage cleaned up the redirected message information
+ // itself, so, we should do nothing.
+ redirectedMsgFlusher.Cancel();
+
+ if (aMsg.wParam == VK_MENU ||
+ (aMsg.wParam == VK_F10 && !modKeyState.IsShift())) {
+ // We need to let Windows handle this keypress,
+ // by returning false, if there's a native menu
+ // bar somewhere in our containing window hierarchy.
+ // Otherwise we handle the keypress and don't pass
+ // it on to Windows, by returning true.
+ bool hasNativeMenu = false;
+ HWND hWnd = mWnd;
+ while (hWnd) {
+ if (::GetMenu(hWnd)) {
+ hasNativeMenu = true;
+ break;
+ }
+ hWnd = ::GetParent(hWnd);
+ }
+ result = !hasNativeMenu;
+ }
+
+ return result;
+}
+
+nsresult nsWindow::SynthesizeNativeKeyEvent(
+ int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
+ uint32_t aModifierFlags, const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "keyevent");
+
+ KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance();
+ return keyboardLayout->SynthesizeNativeKeyEvent(
+ this, aNativeKeyboardLayout, aNativeKeyCode, aModifierFlags, aCharacters,
+ aUnmodifiedCharacters);
+}
+
+nsresult nsWindow::SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+
+ INPUT input;
+ memset(&input, 0, sizeof(input));
+
+ // TODO (bug 1693240):
+ // Now, we synthesize native mouse events asynchronously since we want to
+ // synthesize the event on the front window at the point. However, Windows
+ // does not provide a way to set modifier only while a mouse message is
+ // being handled, and MOUSEEVENTF_MOVE may be coalesced by Windows. So, we
+ // need a trick for handling it.
+
+ switch (aNativeMessage) {
+ case NativeMouseMessage::Move:
+ input.mi.dwFlags = MOUSEEVENTF_MOVE;
+ // Reset sLastMouseMovePoint so that even if we're moving the mouse
+ // to the position it's already at, we still dispatch a mousemove
+ // event, because the callers of this function expect that.
+ sLastMouseMovePoint = {0};
+ break;
+ case NativeMouseMessage::ButtonDown:
+ case NativeMouseMessage::ButtonUp: {
+ const bool isDown = aNativeMessage == NativeMouseMessage::ButtonDown;
+ switch (aButton) {
+ case MouseButton::ePrimary:
+ input.mi.dwFlags = isDown ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP;
+ break;
+ case MouseButton::eMiddle:
+ input.mi.dwFlags =
+ isDown ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP;
+ break;
+ case MouseButton::eSecondary:
+ input.mi.dwFlags =
+ isDown ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP;
+ break;
+ case MouseButton::eX1:
+ input.mi.dwFlags = isDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP;
+ input.mi.mouseData = XBUTTON1;
+ break;
+ case MouseButton::eX2:
+ input.mi.dwFlags = isDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP;
+ input.mi.mouseData = XBUTTON2;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+ break;
+ }
+ case NativeMouseMessage::EnterWindow:
+ case NativeMouseMessage::LeaveWindow:
+ MOZ_ASSERT_UNREACHABLE("Non supported mouse event on Windows");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ input.type = INPUT_MOUSE;
+ ::SetCursorPos(aPoint.x, aPoint.y);
+ ::SendInput(1, &input, sizeof(INPUT));
+
+ return NS_OK;
+}
+
+nsresult nsWindow::SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+ return MouseScrollHandler::SynthesizeNativeMouseScrollEvent(
+ this, aPoint, aNativeMessage,
+ (aNativeMessage == WM_MOUSEWHEEL || aNativeMessage == WM_VSCROLL)
+ ? static_cast<int32_t>(aDeltaY)
+ : static_cast<int32_t>(aDeltaX),
+ aModifierFlags, aAdditionalFlags);
+}
+
+nsresult nsWindow::SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpadpanevent");
+ DirectManipulationOwner::SynthesizeNativeTouchpadPan(
+ this, aEventPhase, aPoint, aDeltaX, aDeltaY, aModifierFlags);
+ return NS_OK;
+}
+
+static void MaybeLogPosChanged(HWND aWnd, WINDOWPOS* wp) {
+#ifdef WINSTATE_DEBUG_OUTPUT
+ if (aWnd == WinUtils::GetTopLevelHWND(aWnd)) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("*** OnWindowPosChanged: [ top] "));
+ } else {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("*** OnWindowPosChanged: [child] "));
+ }
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("WINDOWPOS flags:"));
+ if (wp->flags & SWP_FRAMECHANGED) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_FRAMECHANGED "));
+ }
+ if (wp->flags & SWP_SHOWWINDOW) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_SHOWWINDOW "));
+ }
+ if (wp->flags & SWP_NOSIZE) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_NOSIZE "));
+ }
+ if (wp->flags & SWP_HIDEWINDOW) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_HIDEWINDOW "));
+ }
+ if (wp->flags & SWP_NOZORDER) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_NOZORDER "));
+ }
+ if (wp->flags & SWP_NOACTIVATE) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_NOACTIVATE "));
+ }
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("\n"));
+#endif
+}
+
+/**************************************************************
+ *
+ * SECTION: OnXXX message handlers
+ *
+ * For message handlers that need to be broken out or
+ * implemented in specific platform code.
+ *
+ **************************************************************/
+
+void nsWindow::OnWindowPosChanged(WINDOWPOS* wp) {
+ if (!wp) {
+ return;
+ }
+
+ MaybeLogPosChanged(mWnd, wp);
+
+ // Handle window size mode changes
+ if (wp->flags & SWP_FRAMECHANGED) {
+ // Bug 566135 - Windows theme code calls show window on SW_SHOWMINIMIZED
+ // windows when fullscreen games disable desktop composition. If we're
+ // minimized and not being activated, ignore the event and let windows
+ // handle it.
+ if (mFrameState->GetSizeMode() == nsSizeMode_Minimized &&
+ (wp->flags & SWP_NOACTIVATE)) {
+ return;
+ }
+
+ mFrameState->OnFrameChanged();
+
+ if (mFrameState->GetSizeMode() == nsSizeMode_Minimized) {
+ // Skip window size change events below on minimization.
+ return;
+ }
+ }
+
+ // Notify visibility change when window is activated.
+ if (!(wp->flags & SWP_NOACTIVATE) && NeedsToTrackWindowOcclusionState()) {
+ WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
+ this, mFrameState->GetSizeMode() != nsSizeMode_Minimized);
+ }
+
+ // Handle window position changes
+ if (!(wp->flags & SWP_NOMOVE)) {
+ mBounds.MoveTo(wp->x, wp->y);
+ NotifyWindowMoved(wp->x, wp->y);
+ }
+
+ // Handle window size changes
+ if (!(wp->flags & SWP_NOSIZE)) {
+ RECT r;
+ int32_t newWidth, newHeight;
+
+ ::GetWindowRect(mWnd, &r);
+
+ newWidth = r.right - r.left;
+ newHeight = r.bottom - r.top;
+
+ if (newWidth > mLastSize.width) {
+ RECT drect;
+
+ // getting wider
+ drect.left = wp->x + mLastSize.width;
+ drect.top = wp->y;
+ drect.right = drect.left + (newWidth - mLastSize.width);
+ drect.bottom = drect.top + newHeight;
+
+ ::RedrawWindow(mWnd, &drect, nullptr,
+ RDW_INVALIDATE | RDW_NOERASE | RDW_NOINTERNALPAINT |
+ RDW_ERASENOW | RDW_ALLCHILDREN);
+ }
+ if (newHeight > mLastSize.height) {
+ RECT drect;
+
+ // getting taller
+ drect.left = wp->x;
+ drect.top = wp->y + mLastSize.height;
+ drect.right = drect.left + newWidth;
+ drect.bottom = drect.top + (newHeight - mLastSize.height);
+
+ ::RedrawWindow(mWnd, &drect, nullptr,
+ RDW_INVALIDATE | RDW_NOERASE | RDW_NOINTERNALPAINT |
+ RDW_ERASENOW | RDW_ALLCHILDREN);
+ }
+
+ mBounds.SizeTo(newWidth, newHeight);
+ mLastSize.width = newWidth;
+ mLastSize.height = newHeight;
+
+#ifdef WINSTATE_DEBUG_OUTPUT
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("*** Resize window: %d x %d x %d x %d\n", wp->x, wp->y, newWidth,
+ newHeight));
+#endif
+
+ if (mAspectRatio > 0) {
+ // It's possible (via Windows Aero Snap) that the size of the window
+ // has changed such that it violates the aspect ratio constraint. If so,
+ // queue up an event to enforce the aspect ratio constraint and repaint.
+ // When resized with Windows Aero Snap, we are in the NOT_RESIZING state.
+ float newAspectRatio = (float)newWidth / newHeight;
+ if (mResizeState == NOT_RESIZING && mAspectRatio != newAspectRatio) {
+ // Hold a reference to self alive and pass it into the lambda to make
+ // sure this nsIWidget stays alive long enough to run this function.
+ nsCOMPtr<nsIWidget> self(this);
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "EnforceAspectRatio", [self, this, newWidth]() -> void {
+ if (mWnd) {
+ Resize(newWidth, newWidth / mAspectRatio, true);
+ }
+ }));
+ }
+ }
+
+ // If a maximized window is resized, recalculate the non-client margins.
+ if (mFrameState->GetSizeMode() == nsSizeMode_Maximized) {
+ if (UpdateNonClientMargins(true)) {
+ // gecko resize event already sent by UpdateNonClientMargins.
+ return;
+ }
+ }
+ }
+
+ // Notify the widget listener for size change of client area for gecko
+ // events. This needs to be done when either window size is changed,
+ // or window frame is changed. They may not happen together.
+ // However, we don't invoke that for popup when window frame changes,
+ // because popups may trigger frame change before size change via
+ // {Set,Clear}ThemeRegion they invoke in Resize. That would make the
+ // code below call OnResize with a wrong client size first, which can
+ // lead to flickerling for some popups.
+ if (!(wp->flags & SWP_NOSIZE) ||
+ ((wp->flags & SWP_FRAMECHANGED) && !IsPopup())) {
+ RECT r;
+ LayoutDeviceIntSize clientSize;
+ if (::GetClientRect(mWnd, &r)) {
+ clientSize = WinUtils::ToIntRect(r).Size();
+ } else {
+ clientSize = mBounds.Size();
+ }
+ // Send a gecko resize event
+ OnResize(clientSize);
+ }
+}
+
+void nsWindow::OnWindowPosChanging(WINDOWPOS* info) {
+ // Update non-client margins if the frame size is changing, and let the
+ // browser know we are changing size modes, so alternative css can kick in.
+ // If we're going into fullscreen mode, ignore this, since it'll reset
+ // margins to normal mode.
+ if (info->flags & SWP_FRAMECHANGED && !(info->flags & SWP_NOSIZE)) {
+ mFrameState->OnFrameChanging();
+ }
+
+ // Force fullscreen. This works around a bug in Windows 10 1809 where
+ // using fullscreen when a window is "snapped" causes a spurious resize
+ // smaller than the full screen, see bug 1482920.
+ if (mFrameState->GetSizeMode() == nsSizeMode_Fullscreen &&
+ !(info->flags & SWP_NOMOVE) && !(info->flags & SWP_NOSIZE)) {
+ nsCOMPtr<nsIScreenManager> screenmgr =
+ do_GetService(sScreenManagerContractID);
+ if (screenmgr) {
+ LayoutDeviceIntRect bounds(info->x, info->y, info->cx, info->cy);
+ DesktopIntRect deskBounds =
+ RoundedToInt(bounds / GetDesktopToDeviceScale());
+ nsCOMPtr<nsIScreen> screen;
+ screenmgr->ScreenForRect(deskBounds.X(), deskBounds.Y(),
+ deskBounds.Width(), deskBounds.Height(),
+ getter_AddRefs(screen));
+
+ if (screen) {
+ auto rect = screen->GetRect();
+ info->x = rect.x;
+ info->y = rect.y;
+ info->cx = rect.width;
+ info->cy = rect.height;
+ }
+ }
+ }
+
+ // enforce local z-order rules
+ if (!(info->flags & SWP_NOZORDER)) {
+ HWND hwndAfter = info->hwndInsertAfter;
+
+ nsWindow* aboveWindow = 0;
+ nsWindowZ placement;
+
+ if (hwndAfter == HWND_BOTTOM)
+ placement = nsWindowZBottom;
+ else if (hwndAfter == HWND_TOP || hwndAfter == HWND_TOPMOST ||
+ hwndAfter == HWND_NOTOPMOST)
+ placement = nsWindowZTop;
+ else {
+ placement = nsWindowZRelative;
+ aboveWindow = WinUtils::GetNSWindowPtr(hwndAfter);
+ }
+
+ if (mWidgetListener) {
+ nsCOMPtr<nsIWidget> actualBelow = nullptr;
+ if (mWidgetListener->ZLevelChanged(false, &placement, aboveWindow,
+ getter_AddRefs(actualBelow))) {
+ if (placement == nsWindowZBottom)
+ info->hwndInsertAfter = HWND_BOTTOM;
+ else if (placement == nsWindowZTop)
+ info->hwndInsertAfter = HWND_TOP;
+ else {
+ info->hwndInsertAfter =
+ (HWND)actualBelow->GetNativeData(NS_NATIVE_WINDOW);
+ }
+ }
+ }
+ }
+ // prevent rude external programs from making hidden window visible
+ if (mWindowType == WindowType::Invisible) info->flags &= ~SWP_SHOWWINDOW;
+
+ // When waking from sleep or switching out of tablet mode, Windows 10
+ // Version 1809 will reopen popup windows that should be hidden. Detect
+ // this case and refuse to show the window.
+ static bool sDWMUnhidesPopups = IsWin10Sep2018UpdateOrLater();
+ if (sDWMUnhidesPopups && (info->flags & SWP_SHOWWINDOW) &&
+ mWindowType == WindowType::Popup && mWidgetListener &&
+ mWidgetListener->ShouldNotBeVisible()) {
+ info->flags &= ~SWP_SHOWWINDOW;
+ }
+}
+
+void nsWindow::UserActivity() {
+ // Check if we have the idle service, if not we try to get it.
+ if (!mIdleService) {
+ mIdleService = do_GetService("@mozilla.org/widget/useridleservice;1");
+ }
+
+ // Check that we now have the idle service.
+ if (mIdleService) {
+ mIdleService->ResetIdleTimeOut(0);
+ }
+}
+
+// Helper function for TouchDeviceNeedsPanGestureConversion(PTOUCHINPUT,
+// uint32_t).
+static bool TouchDeviceNeedsPanGestureConversion(HANDLE aSource) {
+ std::string deviceName;
+ UINT dataSize = 0;
+ // The first call just queries how long the name string will be.
+ GetRawInputDeviceInfoA(aSource, RIDI_DEVICENAME, nullptr, &dataSize);
+ if (!dataSize || dataSize > 0x10000) {
+ return false;
+ }
+ deviceName.resize(dataSize);
+ // The second call actually populates the string.
+ UINT result = GetRawInputDeviceInfoA(aSource, RIDI_DEVICENAME, &deviceName[0],
+ &dataSize);
+ if (result == UINT_MAX) {
+ return false;
+ }
+ // The affected device name is "\\?\VIRTUAL_DIGITIZER", but each backslash
+ // needs to be escaped with another one.
+ std::string expectedDeviceName = "\\\\?\\VIRTUAL_DIGITIZER";
+ // For some reason, the dataSize returned by the first call is double the
+ // actual length of the device name (as if it were returning the size of a
+ // wide-character string in bytes) even though we are using the narrow
+ // version of the API. For the comparison against the expected device name
+ // to pass, we truncate the buffer to be no longer tha the expected device
+ // name.
+ if (deviceName.substr(0, expectedDeviceName.length()) != expectedDeviceName) {
+ return false;
+ }
+
+ RID_DEVICE_INFO deviceInfo;
+ deviceInfo.cbSize = sizeof(deviceInfo);
+ dataSize = sizeof(deviceInfo);
+ result =
+ GetRawInputDeviceInfoA(aSource, RIDI_DEVICEINFO, &deviceInfo, &dataSize);
+ if (result == UINT_MAX) {
+ return false;
+ }
+ // The device identifiers that we check for here come from bug 1355162
+ // comment 1 (see also bug 1511901 comment 35).
+ return deviceInfo.dwType == RIM_TYPEHID && deviceInfo.hid.dwVendorId == 0 &&
+ deviceInfo.hid.dwProductId == 0 &&
+ deviceInfo.hid.dwVersionNumber == 1 &&
+ deviceInfo.hid.usUsagePage == 13 && deviceInfo.hid.usUsage == 4;
+}
+
+// Determine if the touch device that originated |aOSEvent| needs to have
+// touch events representing a two-finger gesture converted to pan
+// gesture events.
+// We only do this for touch devices with a specific name and identifiers.
+static bool TouchDeviceNeedsPanGestureConversion(PTOUCHINPUT aOSEvent,
+ uint32_t aTouchCount) {
+ if (!StaticPrefs::apz_windows_check_for_pan_gesture_conversion()) {
+ return false;
+ }
+ if (aTouchCount == 0) {
+ return false;
+ }
+ HANDLE source = aOSEvent[0].hSource;
+
+ // Cache the result of this computation for each touch device.
+ // Touch devices are identified by the HANDLE stored in the hSource
+ // field of TOUCHINPUT.
+ static std::map<HANDLE, bool> sResultCache;
+ auto [iter, inserted] = sResultCache.emplace(source, false);
+ if (inserted) {
+ iter->second = TouchDeviceNeedsPanGestureConversion(source);
+ }
+ return iter->second;
+}
+
+Maybe<PanGestureInput> nsWindow::ConvertTouchToPanGesture(
+ const MultiTouchInput& aTouchInput, PTOUCHINPUT aOSEvent) {
+ // Checks if the touch device that originated the touch event is one
+ // for which we want to convert the touch events to pang gesture events.
+ bool shouldConvert = TouchDeviceNeedsPanGestureConversion(
+ aOSEvent, aTouchInput.mTouches.Length());
+ if (!shouldConvert) {
+ return Nothing();
+ }
+
+ // Only two-finger gestures need conversion.
+ if (aTouchInput.mTouches.Length() != 2) {
+ return Nothing();
+ }
+
+ PanGestureInput::PanGestureType eventType = PanGestureInput::PANGESTURE_PAN;
+ if (aTouchInput.mType == MultiTouchInput::MULTITOUCH_START) {
+ eventType = PanGestureInput::PANGESTURE_START;
+ } else if (aTouchInput.mType == MultiTouchInput::MULTITOUCH_END) {
+ eventType = PanGestureInput::PANGESTURE_END;
+ } else if (aTouchInput.mType == MultiTouchInput::MULTITOUCH_CANCEL) {
+ eventType = PanGestureInput::PANGESTURE_CANCELLED;
+ }
+
+ // Use the midpoint of the two touches as the start point of the pan gesture.
+ ScreenPoint focusPoint = (aTouchInput.mTouches[0].mScreenPoint +
+ aTouchInput.mTouches[1].mScreenPoint) /
+ 2;
+ // To compute the displacement of the pan gesture, we keep track of the
+ // location of the previous event.
+ ScreenPoint displacement = (eventType == PanGestureInput::PANGESTURE_START)
+ ? ScreenPoint(0, 0)
+ : (focusPoint - mLastPanGestureFocus);
+ mLastPanGestureFocus = focusPoint;
+
+ // We need to negate the displacement because for a touch event, moving the
+ // fingers down results in scrolling up, but for a touchpad gesture, we want
+ // moving the fingers down to result in scrolling down.
+ PanGestureInput result(eventType, aTouchInput.mTimeStamp, focusPoint,
+ -displacement, aTouchInput.modifiers);
+ result.mSimulateMomentum = true;
+
+ return Some(result);
+}
+
+// Dispatch an event that originated as an OS touch event.
+// Usually, we want to dispatch it as a touch event, but some touchpads
+// produce touch events for two-finger scrolling, which need to be converted
+// to pan gesture events for correct behaviour.
+void nsWindow::DispatchTouchOrPanGestureInput(MultiTouchInput& aTouchInput,
+ PTOUCHINPUT aOSEvent) {
+ if (Maybe<PanGestureInput> panInput =
+ ConvertTouchToPanGesture(aTouchInput, aOSEvent)) {
+ DispatchPanGestureInput(*panInput);
+ return;
+ }
+
+ DispatchTouchInput(aTouchInput);
+}
+
+bool nsWindow::OnTouch(WPARAM wParam, LPARAM lParam) {
+ uint32_t cInputs = LOWORD(wParam);
+ PTOUCHINPUT pInputs = new TOUCHINPUT[cInputs];
+
+ if (GetTouchInputInfo((HTOUCHINPUT)lParam, cInputs, pInputs,
+ sizeof(TOUCHINPUT))) {
+ MultiTouchInput touchInput, touchEndInput;
+
+ // Walk across the touch point array processing each contact point.
+ for (uint32_t i = 0; i < cInputs; i++) {
+ bool addToEvent = false, addToEndEvent = false;
+
+ // N.B.: According with MS documentation
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317334(v=vs.85).aspx
+ // TOUCHEVENTF_DOWN cannot be combined with TOUCHEVENTF_MOVE or
+ // TOUCHEVENTF_UP. Possibly, it means that TOUCHEVENTF_MOVE and
+ // TOUCHEVENTF_UP can be combined together.
+
+ if (pInputs[i].dwFlags & (TOUCHEVENTF_DOWN | TOUCHEVENTF_MOVE)) {
+ if (touchInput.mTimeStamp.IsNull()) {
+ // Initialize a touch event to send.
+ touchInput.mType = MultiTouchInput::MULTITOUCH_MOVE;
+ touchInput.mTimeStamp = GetMessageTimeStamp(::GetMessageTime());
+ ModifierKeyState modifierKeyState;
+ touchInput.modifiers = modifierKeyState.GetModifiers();
+ }
+ // Pres shell expects this event to be a eTouchStart
+ // if any new contact points have been added since the last event sent.
+ if (pInputs[i].dwFlags & TOUCHEVENTF_DOWN) {
+ touchInput.mType = MultiTouchInput::MULTITOUCH_START;
+ }
+ addToEvent = true;
+ }
+ if (pInputs[i].dwFlags & TOUCHEVENTF_UP) {
+ // Pres shell expects removed contacts points to be delivered in a
+ // separate eTouchEnd event containing only the contact points that were
+ // removed.
+ if (touchEndInput.mTimeStamp.IsNull()) {
+ // Initialize a touch event to send.
+ touchEndInput.mType = MultiTouchInput::MULTITOUCH_END;
+ touchEndInput.mTimeStamp = GetMessageTimeStamp(::GetMessageTime());
+ ModifierKeyState modifierKeyState;
+ touchEndInput.modifiers = modifierKeyState.GetModifiers();
+ }
+ addToEndEvent = true;
+ }
+ if (!addToEvent && !addToEndEvent) {
+ // Filter out spurious Windows events we don't understand, like palm
+ // contact.
+ continue;
+ }
+
+ // Setup the touch point we'll append to the touch event array.
+ nsPointWin touchPoint;
+ touchPoint.x = TOUCH_COORD_TO_PIXEL(pInputs[i].x);
+ touchPoint.y = TOUCH_COORD_TO_PIXEL(pInputs[i].y);
+ touchPoint.ScreenToClient(mWnd);
+
+ // Initialize the touch data.
+ SingleTouchData touchData(
+ pInputs[i].dwID, // aIdentifier
+ ScreenIntPoint::FromUnknownPoint(touchPoint), // aScreenPoint
+ // The contact area info cannot be trusted even when
+ // TOUCHINPUTMASKF_CONTACTAREA is set when the input source is pen,
+ // which somehow violates the API docs. (bug 1710509) Ultimately the
+ // dwFlags check will become redundant since we want to migrate to
+ // WM_POINTER for pens. (bug 1707075)
+ (pInputs[i].dwMask & TOUCHINPUTMASKF_CONTACTAREA) &&
+ !(pInputs[i].dwFlags & TOUCHEVENTF_PEN)
+ ? ScreenSize(TOUCH_COORD_TO_PIXEL(pInputs[i].cxContact) / 2,
+ TOUCH_COORD_TO_PIXEL(pInputs[i].cyContact) / 2)
+ : ScreenSize(1, 1), // aRadius
+ 0.0f, // aRotationAngle
+ 0.0f); // aForce
+
+ // Append touch data to the appropriate event.
+ if (addToEvent) {
+ touchInput.mTouches.AppendElement(touchData);
+ }
+ if (addToEndEvent) {
+ touchEndInput.mTouches.AppendElement(touchData);
+ }
+ }
+
+ // Dispatch touch start and touch move event if we have one.
+ if (!touchInput.mTimeStamp.IsNull()) {
+ DispatchTouchOrPanGestureInput(touchInput, pInputs);
+ }
+ // Dispatch touch end event if we have one.
+ if (!touchEndInput.mTimeStamp.IsNull()) {
+ DispatchTouchOrPanGestureInput(touchEndInput, pInputs);
+ }
+ }
+
+ delete[] pInputs;
+ CloseTouchInputHandle((HTOUCHINPUT)lParam);
+ return true;
+}
+
+// Gesture event processing. Handles WM_GESTURE events.
+bool nsWindow::OnGesture(WPARAM wParam, LPARAM lParam) {
+ // Treatment for pan events which translate into scroll events:
+ if (mGesture.IsPanEvent(lParam)) {
+ if (!mGesture.ProcessPanMessage(mWnd, wParam, lParam))
+ return false; // ignore
+
+ nsEventStatus status;
+
+ WidgetWheelEvent wheelEvent(true, eWheel, this);
+
+ ModifierKeyState modifierKeyState;
+ modifierKeyState.InitInputEvent(wheelEvent);
+
+ wheelEvent.mButton = 0;
+ wheelEvent.mTimeStamp = GetMessageTimeStamp(::GetMessageTime());
+ wheelEvent.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+
+ bool endFeedback = true;
+
+ if (mGesture.PanDeltaToPixelScroll(wheelEvent)) {
+ DispatchEvent(&wheelEvent, status);
+ }
+
+ if (mDisplayPanFeedback) {
+ mGesture.UpdatePanFeedbackX(
+ mWnd, DeprecatedAbs(RoundDown(wheelEvent.mOverflowDeltaX)),
+ endFeedback);
+ mGesture.UpdatePanFeedbackY(
+ mWnd, DeprecatedAbs(RoundDown(wheelEvent.mOverflowDeltaY)),
+ endFeedback);
+ mGesture.PanFeedbackFinalize(mWnd, endFeedback);
+ }
+
+ CloseGestureInfoHandle((HGESTUREINFO)lParam);
+
+ return true;
+ }
+
+ // Other gestures translate into simple gesture events:
+ WidgetSimpleGestureEvent event(true, eVoidEvent, this);
+ if (!mGesture.ProcessGestureMessage(mWnd, wParam, lParam, event)) {
+ return false; // fall through to DefWndProc
+ }
+
+ // Polish up and send off the new event
+ ModifierKeyState modifierKeyState;
+ modifierKeyState.InitInputEvent(event);
+ event.mButton = 0;
+ event.mTimeStamp = GetMessageTimeStamp(::GetMessageTime());
+ event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+
+ nsEventStatus status;
+ DispatchEvent(&event, status);
+ if (status == nsEventStatus_eIgnore) {
+ return false; // Ignored, fall through
+ }
+
+ // Only close this if we process and return true.
+ CloseGestureInfoHandle((HGESTUREINFO)lParam);
+
+ return true; // Handled
+}
+
+// WM_DESTROY event handler
+void nsWindow::OnDestroy() {
+ mOnDestroyCalled = true;
+
+ // If this is a toplevel window, notify the taskbar concealer to clean up any
+ // relevant state.
+ if (!mParent) {
+ TaskbarConcealer::OnWindowDestroyed(mWnd);
+ }
+
+ // Make sure we don't get destroyed in the process of tearing down.
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
+ // Dispatch the destroy notification.
+ if (!mInDtor) NotifyWindowDestroyed();
+
+ // Prevent the widget from sending additional events.
+ mWidgetListener = nullptr;
+ mAttachedWidgetListener = nullptr;
+
+ DestroyDirectManipulation();
+
+ if (mWnd == mLastKillFocusWindow) {
+ mLastKillFocusWindow = nullptr;
+ }
+ // Unregister notifications from terminal services
+ ::WTSUnRegisterSessionNotification(mWnd);
+
+ // We will stop receiving native events after dissociating from our native
+ // window. We will also disappear from the output of WinUtils::GetNSWindowPtr
+ // for that window.
+ DissociateFromNativeWindow();
+
+ // Once mWidgetListener is cleared and the subclass is reset, sCurrentWindow
+ // can be cleared. (It's used in tracking windows for mouse events.)
+ if (sCurrentWindow == this) sCurrentWindow = nullptr;
+
+ // Disconnects us from our parent, will call our GetParent().
+ nsBaseWidget::Destroy();
+
+ // Release references to children, device context, toolkit, and app shell.
+ nsBaseWidget::OnDestroy();
+
+ // Clear our native parent handle.
+ // XXX Windows will take care of this in the proper order, and
+ // SetParent(nullptr)'s remove child on the parent already took place in
+ // nsBaseWidget's Destroy call above.
+ // SetParent(nullptr);
+ mParent = nullptr;
+
+ // We have to destroy the native drag target before we null out our window
+ // pointer.
+ EnableDragDrop(false);
+
+ // If we're going away and for some reason we're still the rollup widget,
+ // rollup and turn off capture.
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ nsCOMPtr<nsIWidget> rollupWidget;
+ if (rollupListener) {
+ rollupWidget = rollupListener->GetRollupWidget();
+ }
+ if (this == rollupWidget) {
+ rollupListener->Rollup({});
+ CaptureRollupEvents(false);
+ }
+
+ IMEHandler::OnDestroyWindow(this);
+
+ // Free GDI window class objects
+ if (mBrush) {
+ VERIFY(::DeleteObject(mBrush));
+ mBrush = nullptr;
+ }
+
+ // Destroy any custom cursor resources.
+ if (mCursor.IsCustom()) {
+ SetCursor(Cursor{eCursor_standard});
+ }
+
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->OnDestroyWindow();
+ }
+ mBasicLayersSurface = nullptr;
+
+ // Finalize panning feedback to possibly restore window displacement
+ mGesture.PanFeedbackFinalize(mWnd, true);
+
+ // Clear the main HWND.
+ mWnd = nullptr;
+}
+
+// Send a resize message to the listener
+bool nsWindow::OnResize(const LayoutDeviceIntSize& aSize) {
+ if (mCompositorWidgetDelegate &&
+ !mCompositorWidgetDelegate->OnWindowResize(aSize)) {
+ return false;
+ }
+
+ bool result = false;
+ if (mWidgetListener) {
+ result = mWidgetListener->WindowResized(this, aSize.width, aSize.height);
+ }
+
+ // If there is an attached view, inform it as well as the normal widget
+ // listener.
+ if (mAttachedWidgetListener) {
+ return mAttachedWidgetListener->WindowResized(this, aSize.width,
+ aSize.height);
+ }
+
+ return result;
+}
+
+void nsWindow::OnSizeModeChange() {
+ const nsSizeMode mode = mFrameState->GetSizeMode();
+
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("nsWindow::OnSizeModeChange() sizeMode %d", mode));
+
+ if (NeedsToTrackWindowOcclusionState()) {
+ WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
+ this, mode != nsSizeMode_Minimized);
+
+ wr::DebugFlags flags{0};
+ flags._0 = gfx::gfxVars::WebRenderDebugFlags();
+ bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG);
+ if (debugEnabled && mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyVisibilityUpdated(mode,
+ mIsFullyOccluded);
+ }
+ }
+
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->OnWindowModeChange(mode);
+ }
+
+ if (mWidgetListener) {
+ mWidgetListener->SizeModeChanged(mode);
+ }
+}
+
+bool nsWindow::OnHotKey(WPARAM wParam, LPARAM lParam) { return true; }
+
+bool nsWindow::IsPopup() { return mWindowType == WindowType::Popup; }
+
+bool nsWindow::ShouldUseOffMainThreadCompositing() {
+ if (mWindowType == WindowType::Popup && mPopupType == PopupType::Tooltip) {
+ return false;
+ }
+
+ // Content rendering of popup is always done by child window.
+ // See nsDocumentViewer::ShouldAttachToTopLevel().
+ if (mWindowType == WindowType::Popup && !mIsChildWindow) {
+ MOZ_ASSERT(!mParent);
+ return false;
+ }
+
+ return nsBaseWidget::ShouldUseOffMainThreadCompositing();
+}
+
+void nsWindow::WindowUsesOMTC() {
+ ULONG_PTR style = ::GetClassLongPtr(mWnd, GCL_STYLE);
+ if (!style) {
+ NS_WARNING("Could not get window class style");
+ return;
+ }
+ style |= CS_HREDRAW | CS_VREDRAW;
+ DebugOnly<ULONG_PTR> result = ::SetClassLongPtr(mWnd, GCL_STYLE, style);
+ NS_WARNING_ASSERTION(result, "Could not reset window class style");
+}
+
+void nsWindow::OnDPIChanged(int32_t x, int32_t y, int32_t width,
+ int32_t height) {
+ // Don't try to handle WM_DPICHANGED for popup windows (see bug 1239353);
+ // they remain tied to their original parent's resolution.
+ if (mWindowType == WindowType::Popup) {
+ return;
+ }
+ if (StaticPrefs::layout_css_devPixelsPerPx() > 0.0) {
+ return;
+ }
+ mDefaultScale = -1.0; // force recomputation of scale factor
+
+ if (mResizeState != RESIZING &&
+ mFrameState->GetSizeMode() == nsSizeMode_Normal) {
+ // Limit the position (if not in the middle of a drag-move) & size,
+ // if it would overflow the destination screen
+ nsCOMPtr<nsIScreenManager> sm = do_GetService(sScreenManagerContractID);
+ if (sm) {
+ nsCOMPtr<nsIScreen> screen;
+ sm->ScreenForRect(x, y, width, height, getter_AddRefs(screen));
+ if (screen) {
+ int32_t availLeft, availTop, availWidth, availHeight;
+ screen->GetAvailRect(&availLeft, &availTop, &availWidth, &availHeight);
+ if (mResizeState != MOVING) {
+ x = std::max(x, availLeft);
+ y = std::max(y, availTop);
+ }
+ width = std::min(width, availWidth);
+ height = std::min(height, availHeight);
+ }
+ }
+
+ Resize(x, y, width, height, true);
+ }
+ UpdateNonClientMargins();
+ ChangedDPI();
+ ResetLayout();
+}
+
+// Callback to generate OnCloakChanged pseudo-events.
+/* static */
+void nsWindow::OnCloakEvent(HWND aWnd, bool aCloaked) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ const char* const kEventName = aCloaked ? "CLOAKED" : "UNCLOAKED";
+ nsWindow* pWin = WinUtils::GetNSWindowPtr(aWnd);
+ if (!pWin) {
+ MOZ_LOG(
+ sCloakingLog, LogLevel::Debug,
+ ("Received %s event for HWND %p (not an nsWindow)", kEventName, aWnd));
+ return;
+ }
+
+ const char* const kWasCloakedStr = pWin->mIsCloaked ? "cloaked" : "uncloaked";
+ if (mozilla::IsCloaked(aWnd) == pWin->mIsCloaked) {
+ MOZ_LOG(sCloakingLog, LogLevel::Debug,
+ ("Received redundant %s event for %s HWND %p; discarding",
+ kEventName, kWasCloakedStr, aWnd));
+ return;
+ }
+
+ MOZ_LOG(
+ sCloakingLog, LogLevel::Info,
+ ("Received %s event for %s HWND %p", kEventName, kWasCloakedStr, aWnd));
+
+ // Cloaking events like the one we've just received are sent asynchronously.
+ // Rather than process them one-by-one, we jump the gun a bit and perform
+ // updates on all newly cloaked/uncloaked nsWindows at once. This also lets us
+ // batch operations that consider more than one window's state.
+ struct Item {
+ nsWindow* win;
+ bool nowCloaked;
+ };
+ nsTArray<Item> changedWindows;
+
+ mozilla::EnumerateThreadWindows([&](HWND hwnd) {
+ nsWindow* pWin = WinUtils::GetNSWindowPtr(hwnd);
+ if (!pWin) {
+ return;
+ }
+
+ const bool isCloaked = mozilla::IsCloaked(hwnd);
+ if (isCloaked != pWin->mIsCloaked) {
+ changedWindows.AppendElement(Item{pWin, isCloaked});
+ }
+ });
+
+ if (changedWindows.IsEmpty()) {
+ return;
+ }
+
+ for (const Item& item : changedWindows) {
+ item.win->OnCloakChanged(item.nowCloaked);
+ }
+
+ nsWindow::TaskbarConcealer::OnCloakChanged();
+}
+
+void nsWindow::OnCloakChanged(bool aCloaked) {
+ MOZ_LOG(sCloakingLog, LogLevel::Info,
+ ("Calling OnCloakChanged(): HWND %p, aCloaked %s", mWnd,
+ aCloaked ? "true" : "false"));
+ mIsCloaked = aCloaked;
+}
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: IME management and accessibility
+ **
+ ** Handles managing IME input and accessibility.
+ **
+ **************************************************************
+ **************************************************************/
+
+void nsWindow::SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) {
+ InputContext newInputContext = aContext;
+ IMEHandler::SetInputContext(this, newInputContext, aAction);
+ mInputContext = newInputContext;
+}
+
+InputContext nsWindow::GetInputContext() {
+ mInputContext.mIMEState.mOpen = IMEState::CLOSED;
+ if (WinUtils::IsIMEEnabled(mInputContext) && IMEHandler::GetOpenState(this)) {
+ mInputContext.mIMEState.mOpen = IMEState::OPEN;
+ } else {
+ mInputContext.mIMEState.mOpen = IMEState::CLOSED;
+ }
+ return mInputContext;
+}
+
+TextEventDispatcherListener* nsWindow::GetNativeTextEventDispatcherListener() {
+ return IMEHandler::GetNativeTextEventDispatcherListener();
+}
+
+#ifdef ACCESSIBILITY
+# ifdef DEBUG
+# define NS_LOG_WMGETOBJECT(aWnd, aHwnd, aAcc) \
+ if (a11y::logging::IsEnabled(a11y::logging::ePlatforms)) { \
+ printf( \
+ "Get the window:\n {\n HWND: %p, parent HWND: %p, wndobj: " \
+ "%p,\n", \
+ aHwnd, ::GetParent(aHwnd), aWnd); \
+ printf(" acc: %p", aAcc); \
+ if (aAcc) { \
+ nsAutoString name; \
+ aAcc->Name(name); \
+ printf(", accname: %s", NS_ConvertUTF16toUTF8(name).get()); \
+ } \
+ printf("\n }\n"); \
+ }
+
+# else
+# define NS_LOG_WMGETOBJECT(aWnd, aHwnd, aAcc)
+# endif
+
+a11y::LocalAccessible* nsWindow::GetAccessible() {
+ // If the pref was ePlatformIsDisabled, return null here, disabling a11y.
+ if (a11y::PlatformDisabledState() == a11y::ePlatformIsDisabled)
+ return nullptr;
+
+ if (mInDtor || mOnDestroyCalled || mWindowType == WindowType::Invisible) {
+ return nullptr;
+ }
+
+ // In case of popup window return a popup accessible.
+ nsView* view = nsView::GetViewFor(this);
+ if (view) {
+ nsIFrame* frame = view->GetFrame();
+ if (frame && nsLayoutUtils::IsPopup(frame)) {
+ nsAccessibilityService* accService = GetOrCreateAccService();
+ if (accService) {
+ a11y::DocAccessible* docAcc =
+ GetAccService()->GetDocAccessible(frame->PresShell());
+ if (docAcc) {
+ NS_LOG_WMGETOBJECT(
+ this, mWnd,
+ docAcc->GetAccessibleOrDescendant(frame->GetContent()));
+ return docAcc->GetAccessibleOrDescendant(frame->GetContent());
+ }
+ }
+ }
+ }
+
+ // otherwise root document accessible.
+ NS_LOG_WMGETOBJECT(this, mWnd, GetRootAccessible());
+ return GetRootAccessible();
+}
+#endif
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Transparency
+ **
+ ** Window transparency helpers.
+ **
+ **************************************************************
+ **************************************************************/
+
+void nsWindow::SetWindowTranslucencyInner(TransparencyMode aMode) {
+ if (aMode == mTransparencyMode) return;
+
+ // stop on dialogs and popups!
+ HWND hWnd = WinUtils::GetTopLevelHWND(mWnd, true);
+ nsWindow* parent = WinUtils::GetNSWindowPtr(hWnd);
+
+ if (!parent) {
+ NS_WARNING("Trying to use transparent chrome in an embedded context");
+ return;
+ }
+
+ if (parent != this) {
+ NS_WARNING(
+ "Setting SetWindowTranslucencyInner on a parent this is not us!");
+ }
+
+ if (aMode == TransparencyMode::Transparent) {
+ // If we're switching to the use of a transparent window, hide the chrome
+ // on our parent.
+ HideWindowChrome(true);
+ } else if (mHideChrome &&
+ mTransparencyMode == TransparencyMode::Transparent) {
+ // if we're switching out of transparent, re-enable our parent's chrome.
+ HideWindowChrome(false);
+ }
+
+ LONG_PTR style = ::GetWindowLongPtrW(hWnd, GWL_STYLE),
+ exStyle = ::GetWindowLongPtr(hWnd, GWL_EXSTYLE);
+
+ if (parent->mIsVisible) {
+ style |= WS_VISIBLE;
+ if (parent->mFrameState->GetSizeMode() == nsSizeMode_Maximized) {
+ style |= WS_MAXIMIZE;
+ } else if (parent->mFrameState->GetSizeMode() == nsSizeMode_Minimized) {
+ style |= WS_MINIMIZE;
+ }
+ }
+
+ if (aMode == TransparencyMode::Transparent)
+ exStyle |= WS_EX_LAYERED;
+ else
+ exStyle &= ~WS_EX_LAYERED;
+
+ VERIFY_WINDOW_STYLE(style);
+ ::SetWindowLongPtrW(hWnd, GWL_STYLE, style);
+ ::SetWindowLongPtrW(hWnd, GWL_EXSTYLE, exStyle);
+
+ mTransparencyMode = aMode;
+
+ if (mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->UpdateTransparency(aMode);
+ }
+}
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Popup rollup hooks
+ **
+ ** Deals with CaptureRollup on popup windows.
+ **
+ **************************************************************
+ **************************************************************/
+
+// Schedules a timer for a window, so we can rollup after processing the hook
+// event
+void nsWindow::ScheduleHookTimer(HWND aWnd, UINT aMsgId) {
+ // In some cases multiple hooks may be scheduled
+ // so ignore any other requests once one timer is scheduled
+ if (sHookTimerId == 0) {
+ // Remember the window handle and the message ID to be used later
+ sRollupMsgId = aMsgId;
+ sRollupMsgWnd = aWnd;
+ // Schedule native timer for doing the rollup after
+ // this event is done being processed
+ sHookTimerId = ::SetTimer(nullptr, 0, 0, (TIMERPROC)HookTimerForPopups);
+ NS_ASSERTION(sHookTimerId, "Timer couldn't be created.");
+ }
+}
+
+#ifdef POPUP_ROLLUP_DEBUG_OUTPUT
+int gLastMsgCode = 0;
+extern MSGFEventMsgInfo gMSGFEvents[];
+#endif
+
+// Process Menu messages, rollup when popup is clicked.
+LRESULT CALLBACK nsWindow::MozSpecialMsgFilter(int code, WPARAM wParam,
+ LPARAM lParam) {
+#ifdef POPUP_ROLLUP_DEBUG_OUTPUT
+ if (sProcessHook) {
+ MSG* pMsg = (MSG*)lParam;
+
+ int inx = 0;
+ while (gMSGFEvents[inx].mId != code && gMSGFEvents[inx].mStr != nullptr) {
+ inx++;
+ }
+ if (code != gLastMsgCode) {
+ if (gMSGFEvents[inx].mId == code) {
+# ifdef DEBUG
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("MozSpecialMessageProc - code: 0x%X - %s hw: %p\n", code,
+ gMSGFEvents[inx].mStr, pMsg->hwnd));
+# endif
+ } else {
+# ifdef DEBUG
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("MozSpecialMessageProc - code: 0x%X - %d hw: %p\n", code,
+ gMSGFEvents[inx].mId, pMsg->hwnd));
+# endif
+ }
+ gLastMsgCode = code;
+ }
+ PrintEvent(pMsg->message, FALSE, FALSE);
+ }
+#endif // #ifdef POPUP_ROLLUP_DEBUG_OUTPUT
+
+ if (sProcessHook && code == MSGF_MENU) {
+ MSG* pMsg = (MSG*)lParam;
+ ScheduleHookTimer(pMsg->hwnd, pMsg->message);
+ }
+
+ return ::CallNextHookEx(sMsgFilterHook, code, wParam, lParam);
+}
+
+// Process all mouse messages. Roll up when a click is in a native window
+// that doesn't have an nsIWidget.
+LRESULT CALLBACK nsWindow::MozSpecialMouseProc(int code, WPARAM wParam,
+ LPARAM lParam) {
+ if (sProcessHook) {
+ switch (WinUtils::GetNativeMessage(wParam)) {
+ case WM_LBUTTONDOWN:
+ case WM_RBUTTONDOWN:
+ case WM_MBUTTONDOWN:
+ case WM_MOUSEWHEEL:
+ case WM_MOUSEHWHEEL: {
+ MOUSEHOOKSTRUCT* ms = (MOUSEHOOKSTRUCT*)lParam;
+ nsIWidget* mozWin = WinUtils::GetNSWindowPtr(ms->hwnd);
+ if (!mozWin) {
+ ScheduleHookTimer(ms->hwnd, (UINT)wParam);
+ }
+ break;
+ }
+ }
+ }
+ return ::CallNextHookEx(sCallMouseHook, code, wParam, lParam);
+}
+
+// Process all messages. Roll up when the window is moving, or
+// is resizing or when maximized or mininized.
+LRESULT CALLBACK nsWindow::MozSpecialWndProc(int code, WPARAM wParam,
+ LPARAM lParam) {
+#ifdef POPUP_ROLLUP_DEBUG_OUTPUT
+ if (sProcessHook) {
+ CWPSTRUCT* cwpt = (CWPSTRUCT*)lParam;
+ PrintEvent(cwpt->message, FALSE, FALSE);
+ }
+#endif
+
+ if (sProcessHook) {
+ CWPSTRUCT* cwpt = (CWPSTRUCT*)lParam;
+ if (cwpt->message == WM_MOVING || cwpt->message == WM_SIZING ||
+ cwpt->message == WM_GETMINMAXINFO) {
+ ScheduleHookTimer(cwpt->hwnd, (UINT)cwpt->message);
+ }
+ }
+
+ return ::CallNextHookEx(sCallProcHook, code, wParam, lParam);
+}
+
+// Register the special "hooks" for dropdown processing.
+void nsWindow::RegisterSpecialDropdownHooks() {
+ NS_ASSERTION(!sMsgFilterHook, "sMsgFilterHook must be NULL!");
+ NS_ASSERTION(!sCallProcHook, "sCallProcHook must be NULL!");
+
+ DISPLAY_NMM_PRT("***************** Installing Msg Hooks ***************\n");
+
+ // Install msg hook for moving the window and resizing
+ if (!sMsgFilterHook) {
+ DISPLAY_NMM_PRT("***** Hooking sMsgFilterHook!\n");
+ sMsgFilterHook = SetWindowsHookEx(WH_MSGFILTER, MozSpecialMsgFilter,
+ nullptr, GetCurrentThreadId());
+#ifdef POPUP_ROLLUP_DEBUG_OUTPUT
+ if (!sMsgFilterHook) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("***** SetWindowsHookEx is NOT installed for WH_MSGFILTER!\n"));
+ }
+#endif
+ }
+
+ // Install msg hook for menus
+ if (!sCallProcHook) {
+ DISPLAY_NMM_PRT("***** Hooking sCallProcHook!\n");
+ sCallProcHook = SetWindowsHookEx(WH_CALLWNDPROC, MozSpecialWndProc, nullptr,
+ GetCurrentThreadId());
+#ifdef POPUP_ROLLUP_DEBUG_OUTPUT
+ if (!sCallProcHook) {
+ MOZ_LOG(
+ gWindowsLog, LogLevel::Info,
+ ("***** SetWindowsHookEx is NOT installed for WH_CALLWNDPROC!\n"));
+ }
+#endif
+ }
+
+ // Install msg hook for the mouse
+ if (!sCallMouseHook) {
+ DISPLAY_NMM_PRT("***** Hooking sCallMouseHook!\n");
+ sCallMouseHook = SetWindowsHookEx(WH_MOUSE, MozSpecialMouseProc, nullptr,
+ GetCurrentThreadId());
+#ifdef POPUP_ROLLUP_DEBUG_OUTPUT
+ if (!sCallMouseHook) {
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("***** SetWindowsHookEx is NOT installed for WH_MOUSE!\n"));
+ }
+#endif
+ }
+}
+
+// Unhook special message hooks for dropdowns.
+void nsWindow::UnregisterSpecialDropdownHooks() {
+ DISPLAY_NMM_PRT(
+ "***************** De-installing Msg Hooks ***************\n");
+
+ if (sCallProcHook) {
+ DISPLAY_NMM_PRT("***** Unhooking sCallProcHook!\n");
+ if (!::UnhookWindowsHookEx(sCallProcHook)) {
+ DISPLAY_NMM_PRT("***** UnhookWindowsHookEx failed for sCallProcHook!\n");
+ }
+ sCallProcHook = nullptr;
+ }
+
+ if (sMsgFilterHook) {
+ DISPLAY_NMM_PRT("***** Unhooking sMsgFilterHook!\n");
+ if (!::UnhookWindowsHookEx(sMsgFilterHook)) {
+ DISPLAY_NMM_PRT("***** UnhookWindowsHookEx failed for sMsgFilterHook!\n");
+ }
+ sMsgFilterHook = nullptr;
+ }
+
+ if (sCallMouseHook) {
+ DISPLAY_NMM_PRT("***** Unhooking sCallMouseHook!\n");
+ if (!::UnhookWindowsHookEx(sCallMouseHook)) {
+ DISPLAY_NMM_PRT("***** UnhookWindowsHookEx failed for sCallMouseHook!\n");
+ }
+ sCallMouseHook = nullptr;
+ }
+}
+
+// This timer is designed to only fire one time at most each time a "hook"
+// function is used to rollup the dropdown. In some cases, the timer may be
+// scheduled from the hook, but that hook event or a subsequent event may roll
+// up the dropdown before this timer function is executed.
+//
+// For example, if an MFC control takes focus, the combobox will lose focus and
+// rollup before this function fires.
+VOID CALLBACK nsWindow::HookTimerForPopups(HWND hwnd, UINT uMsg, UINT idEvent,
+ DWORD dwTime) {
+ if (sHookTimerId != 0) {
+ // if the window is nullptr then we need to use the ID to kill the timer
+ DebugOnly<BOOL> status = ::KillTimer(nullptr, sHookTimerId);
+ NS_ASSERTION(status, "Hook Timer was not killed.");
+ sHookTimerId = 0;
+ }
+
+ if (sRollupMsgId != 0) {
+ // Note: DealWithPopups does the check to make sure that the rollup widget
+ // is set.
+ LRESULT popupHandlingResult;
+ nsAutoRollup autoRollup;
+ DealWithPopups(sRollupMsgWnd, sRollupMsgId, 0, 0, &popupHandlingResult);
+ sRollupMsgId = 0;
+ sRollupMsgWnd = nullptr;
+ }
+}
+
+static bool IsDifferentThreadWindow(HWND aWnd) {
+ return ::GetCurrentThreadId() != ::GetWindowThreadProcessId(aWnd, nullptr);
+}
+
+// static
+bool nsWindow::EventIsInsideWindow(nsWindow* aWindow,
+ Maybe<POINT> aEventPoint) {
+ RECT r;
+ ::GetWindowRect(aWindow->mWnd, &r);
+ POINT mp;
+ if (aEventPoint) {
+ mp = *aEventPoint;
+ } else {
+ DWORD pos = ::GetMessagePos();
+ mp.x = GET_X_LPARAM(pos);
+ mp.y = GET_Y_LPARAM(pos);
+ }
+
+ auto margin = aWindow->mInputRegion.mMargin;
+ if (margin > 0) {
+ r.top += margin;
+ r.bottom -= margin;
+ r.left += margin;
+ r.right -= margin;
+ }
+
+ // was the event inside this window?
+ return static_cast<bool>(::PtInRect(&r, mp));
+}
+
+// static
+bool nsWindow::GetPopupsToRollup(nsIRollupListener* aRollupListener,
+ uint32_t* aPopupsToRollup,
+ Maybe<POINT> aEventPoint) {
+ // If we're dealing with menus, we probably have submenus and we don't want
+ // to rollup some of them if the click is in a parent menu of the current
+ // submenu.
+ *aPopupsToRollup = UINT32_MAX;
+ AutoTArray<nsIWidget*, 5> widgetChain;
+ uint32_t sameTypeCount = aRollupListener->GetSubmenuWidgetChain(&widgetChain);
+ for (uint32_t i = 0; i < widgetChain.Length(); ++i) {
+ nsIWidget* widget = widgetChain[i];
+ if (EventIsInsideWindow(static_cast<nsWindow*>(widget), aEventPoint)) {
+ // Don't roll up if the mouse event occurred within a menu of the
+ // same type. If the mouse event occurred in a menu higher than that,
+ // roll up, but pass the number of popups to Rollup so that only those
+ // of the same type close up.
+ if (i < sameTypeCount) {
+ return false;
+ }
+
+ *aPopupsToRollup = sameTypeCount;
+ break;
+ }
+ }
+ return true;
+}
+
+// static
+bool nsWindow::NeedsToHandleNCActivateDelayed(HWND aWnd) {
+ // While popup is open, popup window might be activated by other application.
+ // At this time, we need to take back focus to the previous window but it
+ // causes flickering its nonclient area because WM_NCACTIVATE comes before
+ // WM_ACTIVATE and we cannot know which window will take focus at receiving
+ // WM_NCACTIVATE. Therefore, we need a hack for preventing the flickerling.
+ //
+ // If non-popup window receives WM_NCACTIVATE at deactivating, default
+ // wndproc shouldn't handle it as deactivating. Instead, at receiving
+ // WM_ACTIVIATE after that, WM_NCACTIVATE should be sent again manually.
+ // This returns true if the window needs to handle WM_NCACTIVATE later.
+
+ nsWindow* window = WinUtils::GetNSWindowPtr(aWnd);
+ return window && !window->IsPopup();
+}
+
+static bool IsTouchSupportEnabled(HWND aWnd) {
+ nsWindow* topWindow =
+ WinUtils::GetNSWindowPtr(WinUtils::GetTopLevelHWND(aWnd, true));
+ return topWindow ? topWindow->IsTouchWindow() : false;
+}
+
+static Maybe<POINT> GetSingleTouch(WPARAM wParam, LPARAM lParam) {
+ Maybe<POINT> ret;
+ uint32_t cInputs = LOWORD(wParam);
+ if (cInputs != 1) {
+ return ret;
+ }
+ TOUCHINPUT input;
+ if (GetTouchInputInfo((HTOUCHINPUT)lParam, cInputs, &input,
+ sizeof(TOUCHINPUT))) {
+ ret.emplace();
+ ret->x = TOUCH_COORD_TO_PIXEL(input.x);
+ ret->y = TOUCH_COORD_TO_PIXEL(input.y);
+ }
+ // Note that we don't call CloseTouchInputHandle here because we need
+ // to read the touch input info again in OnTouch later.
+ return ret;
+}
+
+// static
+bool nsWindow::DealWithPopups(HWND aWnd, UINT aMessage, WPARAM aWParam,
+ LPARAM aLParam, LRESULT* aResult) {
+ NS_ASSERTION(aResult, "Bad outResult");
+
+ // XXX Why do we use the return value of WM_MOUSEACTIVATE for all messages?
+ *aResult = MA_NOACTIVATE;
+
+ if (!::IsWindowVisible(aWnd)) {
+ return false;
+ }
+
+ if (MOZ_UNLIKELY(aMessage == WM_KILLFOCUS)) {
+ // NOTE: We deal with this here rather than on the switch below because we
+ // want to do this even if there are no menus to rollup (tooltips don't set
+ // the rollup listener etc).
+ if (RefPtr pm = nsXULPopupManager::GetInstance()) {
+ pm->RollupTooltips();
+ }
+ }
+
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+ NS_ENSURE_TRUE(rollupListener, false);
+
+ nsCOMPtr<nsIWidget> popup = rollupListener->GetRollupWidget();
+ if (!popup) {
+ return false;
+ }
+
+ static bool sSendingNCACTIVATE = false;
+ static bool sPendingNCACTIVATE = false;
+ uint32_t popupsToRollup = UINT32_MAX;
+
+ bool consumeRollupEvent = false;
+ Maybe<POINT> touchPoint; // In screen coords.
+
+ // If we rollup with animations but get occluded right away, we might not
+ // advance the refresh driver enough for the animation to finish.
+ auto allowAnimations = nsIRollupListener::AllowAnimations::Yes;
+ nsWindow* popupWindow = static_cast<nsWindow*>(popup.get());
+ UINT nativeMessage = WinUtils::GetNativeMessage(aMessage);
+ switch (nativeMessage) {
+ case WM_TOUCH:
+ if (!IsTouchSupportEnabled(aWnd)) {
+ // If APZ is disabled, don't allow touch inputs to dismiss popups. The
+ // compatibility mouse events will do it instead.
+ return false;
+ }
+ touchPoint = GetSingleTouch(aWParam, aLParam);
+ if (!touchPoint) {
+ return false;
+ }
+ [[fallthrough]];
+ case WM_LBUTTONDOWN:
+ case WM_RBUTTONDOWN:
+ case WM_MBUTTONDOWN:
+ case WM_NCLBUTTONDOWN:
+ case WM_NCRBUTTONDOWN:
+ case WM_NCMBUTTONDOWN:
+ if (nativeMessage != WM_TOUCH && IsTouchSupportEnabled(aWnd) &&
+ MOUSE_INPUT_SOURCE() == MouseEvent_Binding::MOZ_SOURCE_TOUCH) {
+ // If any of these mouse events are really compatibility events that
+ // Windows is sending for touch inputs, then don't allow them to dismiss
+ // popups when APZ is enabled (instead we do the dismissing as part of
+ // WM_TOUCH handling which is more correct).
+ // If we don't do this, then when the user lifts their finger after a
+ // long-press, the WM_RBUTTONDOWN compatibility event that Windows sends
+ // us will dismiss the contextmenu popup that we displayed as part of
+ // handling the long-tap-up.
+ return false;
+ }
+ if (!EventIsInsideWindow(popupWindow, touchPoint) &&
+ GetPopupsToRollup(rollupListener, &popupsToRollup, touchPoint)) {
+ break;
+ }
+ return false;
+ case WM_POINTERDOWN: {
+ WinPointerEvents pointerEvents;
+ if (!pointerEvents.ShouldRollupOnPointerEvent(nativeMessage, aWParam)) {
+ return false;
+ }
+ POINT pt;
+ pt.x = GET_X_LPARAM(aLParam);
+ pt.y = GET_Y_LPARAM(aLParam);
+ if (!GetPopupsToRollup(rollupListener, &popupsToRollup, Some(pt))) {
+ return false;
+ }
+ if (EventIsInsideWindow(popupWindow, Some(pt))) {
+ // Don't roll up if the event is inside the popup window.
+ return false;
+ }
+ } break;
+ case MOZ_WM_DMANIP: {
+ POINT pt;
+ ::GetCursorPos(&pt);
+ if (!GetPopupsToRollup(rollupListener, &popupsToRollup, Some(pt))) {
+ return false;
+ }
+ if (EventIsInsideWindow(popupWindow, Some(pt))) {
+ // Don't roll up if the event is inside the popup window
+ return false;
+ }
+ } break;
+ case WM_MOUSEWHEEL:
+ case WM_MOUSEHWHEEL:
+ // We need to check if the popup thinks that it should cause closing
+ // itself when mouse wheel events are fired outside the rollup widget.
+ if (!EventIsInsideWindow(popupWindow)) {
+ // Check if we should consume this event even if we don't roll-up:
+ consumeRollupEvent = rollupListener->ShouldConsumeOnMouseWheelEvent();
+ *aResult = MA_ACTIVATE;
+ if (rollupListener->ShouldRollupOnMouseWheelEvent() &&
+ GetPopupsToRollup(rollupListener, &popupsToRollup)) {
+ break;
+ }
+ }
+ return consumeRollupEvent;
+
+ case WM_ACTIVATEAPP:
+ allowAnimations = nsIRollupListener::AllowAnimations::No;
+ break;
+
+ case WM_ACTIVATE: {
+ WndProcUrgentInvocation::Marker _marker;
+
+ // NOTE: Don't handle WA_INACTIVE for preventing popup taking focus
+ // because we cannot distinguish it's caused by mouse or not.
+ if (LOWORD(aWParam) == WA_ACTIVE && aLParam) {
+ nsWindow* window = WinUtils::GetNSWindowPtr(aWnd);
+ if (window && (window->IsPopup() || window->mIsAlert)) {
+ // Cancel notifying widget listeners of deactivating the previous
+ // active window (see WM_KILLFOCUS case in ProcessMessage()).
+ sJustGotDeactivate = false;
+ // Reactivate the window later.
+ ::PostMessageW(aWnd, MOZ_WM_REACTIVATE, aWParam, aLParam);
+ return true;
+ }
+ // Don't rollup the popup when focus moves back to the parent window
+ // from a popup because such case is caused by strange mouse drivers.
+ nsWindow* prevWindow =
+ WinUtils::GetNSWindowPtr(reinterpret_cast<HWND>(aLParam));
+ if (prevWindow && prevWindow->IsPopup()) {
+ // Consume this message here since previous window must not have
+ // been inactivated since we've already stopped accepting the
+ // inactivation below.
+ return true;
+ }
+ } else if (LOWORD(aWParam) == WA_INACTIVE) {
+ nsWindow* activeWindow =
+ WinUtils::GetNSWindowPtr(reinterpret_cast<HWND>(aLParam));
+ if (sPendingNCACTIVATE && NeedsToHandleNCActivateDelayed(aWnd)) {
+ // If focus moves to non-popup widget or focusable popup, the window
+ // needs to update its nonclient area.
+ if (!activeWindow || !activeWindow->IsPopup()) {
+ sSendingNCACTIVATE = true;
+ ::SendMessageW(aWnd, WM_NCACTIVATE, false, 0);
+ sSendingNCACTIVATE = false;
+ }
+ sPendingNCACTIVATE = false;
+ }
+ // If focus moves from/to popup, we don't need to rollup the popup
+ // because such case is caused by strange mouse drivers. And in
+ // such case, we should consume the message here since we need to
+ // hide this odd focus move from our content. (If we didn't consume
+ // the message here, ProcessMessage() will notify widget listener of
+ // inactivation and that causes unnecessary reflow for supporting
+ // -moz-window-inactive pseudo class.
+ if (activeWindow) {
+ if (activeWindow->IsPopup()) {
+ return true;
+ }
+ nsWindow* deactiveWindow = WinUtils::GetNSWindowPtr(aWnd);
+ if (deactiveWindow && deactiveWindow->IsPopup()) {
+ return true;
+ }
+ }
+ } else if (LOWORD(aWParam) == WA_CLICKACTIVE) {
+ // If the WM_ACTIVATE message is caused by a click in a popup,
+ // we should not rollup any popups.
+ nsWindow* window = WinUtils::GetNSWindowPtr(aWnd);
+ if ((window && window->IsPopup()) ||
+ !GetPopupsToRollup(rollupListener, &popupsToRollup)) {
+ return false;
+ }
+ }
+ allowAnimations = nsIRollupListener::AllowAnimations::No;
+ } break;
+
+ case MOZ_WM_REACTIVATE:
+ // The previous active window should take back focus.
+ if (::IsWindow(reinterpret_cast<HWND>(aLParam))) {
+ // FYI: Even without this API call, you see expected result (e.g., the
+ // owner window of the popup keeps active without flickering
+ // the non-client area). And also this causes initializing
+ // TSF and it causes using CPU time a lot. However, even if we
+ // consume WM_ACTIVE messages, native focus change has already
+ // been occurred. I.e., a popup window is active now. Therefore,
+ // you'll see some odd behavior if we don't reactivate the owner
+ // window here. For example, if you do:
+ // 1. Turn wheel on a bookmark panel.
+ // 2. Turn wheel on another window.
+ // then, you'll see that the another window becomes active but the
+ // owner window of the bookmark panel looks still active and the
+ // bookmark panel keeps open. The reason is that the first wheel
+ // operation gives focus to the bookmark panel. Therefore, when
+ // the next operation gives focus to the another window, previous
+ // focus window is the bookmark panel (i.e., a popup window).
+ // So, in this case, our hack around here prevents to inactivate
+ // the owner window and roll up the bookmark panel.
+ ::SetForegroundWindow(reinterpret_cast<HWND>(aLParam));
+ }
+ return true;
+
+ case WM_NCACTIVATE:
+ if (!aWParam && !sSendingNCACTIVATE &&
+ NeedsToHandleNCActivateDelayed(aWnd)) {
+ // Don't just consume WM_NCACTIVATE. It doesn't handle only the
+ // nonclient area state change.
+ ::DefWindowProcW(aWnd, aMessage, TRUE, aLParam);
+ // Accept the deactivating because it's necessary to receive following
+ // WM_ACTIVATE.
+ *aResult = TRUE;
+ sPendingNCACTIVATE = true;
+ return true;
+ }
+ return false;
+
+ case WM_MOUSEACTIVATE:
+ if (!EventIsInsideWindow(popupWindow) &&
+ GetPopupsToRollup(rollupListener, &popupsToRollup)) {
+ // WM_MOUSEACTIVATE may be caused by moving the mouse (e.g., X-mouse
+ // of TweakUI is enabled. Then, check if the popup should be rolled up
+ // with rollup listener. If not, just consume the message.
+ if (HIWORD(aLParam) == WM_MOUSEMOVE &&
+ !rollupListener->ShouldRollupOnMouseActivate()) {
+ return true;
+ }
+ // Otherwise, it should be handled by wndproc.
+ return false;
+ }
+
+ // Prevent the click inside the popup from causing a change in window
+ // activation. Since the popup is shown non-activated, we need to eat any
+ // requests to activate the window while it is displayed. Windows will
+ // automatically activate the popup on the mousedown otherwise.
+ return true;
+
+ case WM_SHOWWINDOW:
+ // If the window is being minimized, close popups.
+ if (aLParam == SW_PARENTCLOSING) {
+ allowAnimations = nsIRollupListener::AllowAnimations::No;
+ break;
+ }
+ return false;
+
+ case WM_KILLFOCUS:
+ // If focus moves to other window created in different process/thread,
+ // e.g., a plugin window, popups should be rolled up.
+ if (IsDifferentThreadWindow(reinterpret_cast<HWND>(aWParam))) {
+ allowAnimations = nsIRollupListener::AllowAnimations::No;
+ break;
+ }
+ return false;
+
+ case WM_MOVING:
+ case WM_MENUSELECT:
+ break;
+
+ default:
+ return false;
+ }
+
+ // Only need to deal with the last rollup for left mouse down events.
+ NS_ASSERTION(!nsAutoRollup::GetLastRollup(), "last rollup is null");
+
+ nsIRollupListener::RollupOptions rollupOptions{
+ popupsToRollup,
+ nsIRollupListener::FlushViews::Yes,
+ /* mPoint = */ nullptr,
+ allowAnimations,
+ };
+
+ if (nativeMessage == WM_TOUCH || nativeMessage == WM_LBUTTONDOWN ||
+ nativeMessage == WM_POINTERDOWN) {
+ LayoutDeviceIntPoint pos;
+ if (nativeMessage == WM_TOUCH) {
+ pos.x = touchPoint->x;
+ pos.y = touchPoint->y;
+ } else {
+ POINT pt;
+ pt.x = GET_X_LPARAM(aLParam);
+ pt.y = GET_Y_LPARAM(aLParam);
+ // POINTERDOWN is already in screen coords.
+ if (nativeMessage == WM_LBUTTONDOWN) {
+ ::ClientToScreen(aWnd, &pt);
+ }
+ pos = LayoutDeviceIntPoint(pt.x, pt.y);
+ }
+
+ rollupOptions.mPoint = &pos;
+ nsIContent* lastRollup = nullptr;
+ consumeRollupEvent = rollupListener->Rollup(rollupOptions, &lastRollup);
+ nsAutoRollup::SetLastRollup(lastRollup);
+ } else {
+ consumeRollupEvent = rollupListener->Rollup(rollupOptions);
+ }
+
+ // Tell hook to stop processing messages
+ sProcessHook = false;
+ sRollupMsgId = 0;
+ sRollupMsgWnd = nullptr;
+
+ // If we are NOT supposed to be consuming events, let it go through
+ if (consumeRollupEvent && nativeMessage != WM_RBUTTONDOWN) {
+ *aResult = MA_ACTIVATE;
+ return true;
+ }
+
+ return false;
+}
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Misc. utility methods and functions.
+ **
+ ** General use.
+ **
+ **************************************************************
+ **************************************************************/
+
+// Note that the result of GetTopLevelWindow method can be different from the
+// result of WinUtils::GetTopLevelHWND(). The result can be non-floating
+// window. Because our top level window may be contained in another window
+// which is not managed by us.
+nsWindow* nsWindow::GetTopLevelWindow(bool aStopOnDialogOrPopup) {
+ nsWindow* curWindow = this;
+
+ while (true) {
+ if (aStopOnDialogOrPopup) {
+ switch (curWindow->mWindowType) {
+ case WindowType::Dialog:
+ case WindowType::Popup:
+ return curWindow;
+ default:
+ break;
+ }
+ }
+
+ // Retrieve the top level parent or owner window
+ nsWindow* parentWindow = curWindow->GetParentWindow(true);
+
+ if (!parentWindow) return curWindow;
+
+ curWindow = parentWindow;
+ }
+}
+
+// Set a flag if hwnd is a (non-popup) visible window from this process,
+// and bail out of the enumeration. Otherwise leave the flag unmodified
+// and continue the enumeration.
+// lParam must be a bool* pointing at the flag to be set.
+static BOOL CALLBACK EnumVisibleWindowsProc(HWND hwnd, LPARAM lParam) {
+ DWORD pid;
+ ::GetWindowThreadProcessId(hwnd, &pid);
+ if (pid == ::GetCurrentProcessId() && ::IsWindowVisible(hwnd)) {
+ // Don't count popups as visible windows, since they don't take focus,
+ // in case we only have a popup visible (see bug 1554490 where the gfx
+ // test window is an offscreen popup).
+ nsWindow* window = WinUtils::GetNSWindowPtr(hwnd);
+ if (!window || !window->IsPopup()) {
+ bool* windowsVisible = reinterpret_cast<bool*>(lParam);
+ *windowsVisible = true;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+// Determine if it would be ok to activate a window, taking focus.
+// We want to avoid stealing focus from another app (bug 225305).
+bool nsWindow::CanTakeFocus() {
+ HWND fgWnd = ::GetForegroundWindow();
+ if (!fgWnd) {
+ // There is no foreground window, so don't worry about stealing focus.
+ return true;
+ }
+ // We can take focus if the current foreground window is already from
+ // this process.
+ DWORD pid;
+ ::GetWindowThreadProcessId(fgWnd, &pid);
+ if (pid == ::GetCurrentProcessId()) {
+ return true;
+ }
+
+ bool windowsVisible = false;
+ ::EnumWindows(EnumVisibleWindowsProc,
+ reinterpret_cast<LPARAM>(&windowsVisible));
+
+ if (!windowsVisible) {
+ // We're probably creating our first visible window, allow that to
+ // take focus.
+ return true;
+ }
+ return false;
+}
+
+/* static */ const wchar_t* nsWindow::GetMainWindowClass() {
+ static const wchar_t* sMainWindowClass = nullptr;
+ if (!sMainWindowClass) {
+ nsAutoString className;
+ Preferences::GetString("ui.window_class_override", className);
+ if (!className.IsEmpty()) {
+ sMainWindowClass = wcsdup(className.get());
+ } else {
+ sMainWindowClass = kClassNameGeneral;
+ }
+ }
+ return sMainWindowClass;
+}
+
+LPARAM nsWindow::lParamToScreen(LPARAM lParam) {
+ POINT pt;
+ pt.x = GET_X_LPARAM(lParam);
+ pt.y = GET_Y_LPARAM(lParam);
+ ::ClientToScreen(mWnd, &pt);
+ return MAKELPARAM(pt.x, pt.y);
+}
+
+LPARAM nsWindow::lParamToClient(LPARAM lParam) {
+ POINT pt;
+ pt.x = GET_X_LPARAM(lParam);
+ pt.y = GET_Y_LPARAM(lParam);
+ ::ScreenToClient(mWnd, &pt);
+ return MAKELPARAM(pt.x, pt.y);
+}
+
+WPARAM nsWindow::wParamFromGlobalMouseState() {
+ WPARAM result = 0;
+
+ if (!!::GetKeyState(VK_CONTROL)) {
+ result |= MK_CONTROL;
+ }
+
+ if (!!::GetKeyState(VK_SHIFT)) {
+ result |= MK_SHIFT;
+ }
+
+ if (!!::GetKeyState(VK_LBUTTON)) {
+ result |= MK_LBUTTON;
+ }
+
+ if (!!::GetKeyState(VK_MBUTTON)) {
+ result |= MK_MBUTTON;
+ }
+
+ if (!!::GetKeyState(VK_RBUTTON)) {
+ result |= MK_RBUTTON;
+ }
+
+ if (!!::GetKeyState(VK_XBUTTON1)) {
+ result |= MK_XBUTTON1;
+ }
+
+ if (!!::GetKeyState(VK_XBUTTON2)) {
+ result |= MK_XBUTTON2;
+ }
+
+ return result;
+}
+
+void nsWindow::PickerOpen() {
+ AssertIsOnMainThread();
+ mPickerDisplayCount++;
+}
+
+void nsWindow::PickerClosed() {
+ AssertIsOnMainThread();
+ NS_ASSERTION(mPickerDisplayCount > 0, "mPickerDisplayCount out of sync!");
+ if (!mPickerDisplayCount) return;
+ mPickerDisplayCount--;
+
+ // WORKAROUND FOR UNDOCUMENTED BEHAVIOR: `IFileDialog::Show` disables the
+ // top-level ancestor of its provided owner-window. If the modal window's
+ // container process crashes, it will never get a chance to undo that, so we
+ // do it manually here.
+ //
+ // Note that this may cause problems in the embedded case if you reparent a
+ // subtree of the native window hierarchy containing a Gecko window while that
+ // Gecko window has a file-dialog open.
+ if (!mPickerDisplayCount) {
+ ::EnableWindow(::GetAncestor(GetWindowHandle(), GA_ROOT), TRUE);
+ }
+
+ if (!mPickerDisplayCount && mDestroyCalled) {
+ Destroy();
+ }
+}
+
+bool nsWindow::WidgetTypeSupportsAcceleration() {
+ // We don't currently support using an accelerated layer manager with
+ // transparent windows so don't even try. I'm also not sure if we even
+ // want to support this case. See bug 593471.
+ //
+ // Windows' support for transparent accelerated surfaces isn't great.
+ // Some possible approaches:
+ // - Readback the data and update it using
+ // UpdateLayeredWindow/UpdateLayeredWindowIndirect
+ // This is what WPF does. See
+ // CD3DDeviceLevel1::PresentWithGDI/CD3DSwapChainWithSwDC in WpfGfx. The
+ // rationale for not using IDirect3DSurface9::GetDC is explained here:
+ // https://web.archive.org/web/20160521191104/https://blogs.msdn.microsoft.com/dwayneneed/2008/09/08/transparent-windows-in-wpf/
+ // - Use D3D11_RESOURCE_MISC_GDI_COMPATIBLE, IDXGISurface1::GetDC(),
+ // and UpdateLayeredWindowIndirect.
+ // This is suggested here:
+ // https://docs.microsoft.com/en-us/archive/msdn-magazine/2009/december/windows-with-c-layered-windows-with-direct2d
+ // but might have the same problem that IDirect3DSurface9::GetDC has.
+ // - Creating the window with the WS_EX_NOREDIRECTIONBITMAP flag and use
+ // DirectComposition.
+ // Not supported on Win7.
+ // - Using DwmExtendFrameIntoClientArea with negative margins and something
+ // to turn off the glass effect.
+ // This doesn't work when the DWM is not running (Win7)
+ //
+ // Also see bug 1150376, D3D11 composition can cause issues on some devices
+ // on Windows 7 where presentation fails randomly for windows with drop
+ // shadows.
+ return mTransparencyMode != TransparencyMode::Transparent &&
+ !(IsPopup() && DeviceManagerDx::Get()->IsWARP());
+}
+
+bool nsWindow::DispatchTouchEventFromWMPointer(
+ UINT msg, LPARAM aLParam, const WinPointerInfo& aPointerInfo,
+ mozilla::MouseButton aButton) {
+ MultiTouchInput::MultiTouchType touchType;
+ switch (msg) {
+ case WM_POINTERDOWN:
+ touchType = MultiTouchInput::MULTITOUCH_START;
+ break;
+ case WM_POINTERUPDATE:
+ if (aPointerInfo.mPressure == 0) {
+ return false; // hover
+ }
+ touchType = MultiTouchInput::MULTITOUCH_MOVE;
+ break;
+ case WM_POINTERUP:
+ touchType = MultiTouchInput::MULTITOUCH_END;
+ break;
+ default:
+ return false;
+ }
+
+ nsPointWin touchPoint;
+ touchPoint.x = GET_X_LPARAM(aLParam);
+ touchPoint.y = GET_Y_LPARAM(aLParam);
+ touchPoint.ScreenToClient(mWnd);
+
+ SingleTouchData touchData(static_cast<int32_t>(aPointerInfo.pointerId),
+ ScreenIntPoint::FromUnknownPoint(touchPoint),
+ ScreenSize(1, 1), // pixel size radius for pen
+ 0.0f, // no radius rotation
+ aPointerInfo.mPressure);
+ touchData.mTiltX = aPointerInfo.tiltX;
+ touchData.mTiltY = aPointerInfo.tiltY;
+ touchData.mTwist = aPointerInfo.twist;
+
+ MultiTouchInput touchInput;
+ touchInput.mType = touchType;
+ touchInput.mTimeStamp = GetMessageTimeStamp(::GetMessageTime());
+ touchInput.mTouches.AppendElement(touchData);
+ touchInput.mButton = aButton;
+ touchInput.mButtons = aPointerInfo.mButtons;
+
+ // POINTER_INFO.dwKeyStates can't be used as it only supports Shift and Ctrl
+ ModifierKeyState modifierKeyState;
+ touchInput.modifiers = modifierKeyState.GetModifiers();
+
+ DispatchTouchInput(touchInput, MouseEvent_Binding::MOZ_SOURCE_PEN);
+ return true;
+}
+
+static MouseButton PenFlagsToMouseButton(PEN_FLAGS aPenFlags) {
+ // Theoretically flags can be set together but they do not
+ if (aPenFlags & PEN_FLAG_BARREL) {
+ return MouseButton::eSecondary;
+ }
+ if (aPenFlags & PEN_FLAG_ERASER) {
+ return MouseButton::eEraser;
+ }
+ return MouseButton::ePrimary;
+}
+
+bool nsWindow::OnPointerEvents(UINT msg, WPARAM aWParam, LPARAM aLParam) {
+ if (!mAPZC) {
+ // APZ is not available on context menu. Follow the behavior of touch input
+ // which fallbacks to WM_LBUTTON* and WM_GESTURE, to keep consistency.
+ return false;
+ }
+ if (!mPointerEvents.ShouldHandleWinPointerMessages(msg, aWParam)) {
+ return false;
+ }
+ if (!mPointerEvents.ShouldFirePointerEventByWinPointerMessages()) {
+ // We have to handle WM_POINTER* to fetch and cache pen related information
+ // and fire WidgetMouseEvent with the cached information the WM_*BUTTONDOWN
+ // handler. This is because Windows doesn't support ::DoDragDrop in the
+ // touch or pen message handlers.
+ mPointerEvents.ConvertAndCachePointerInfo(msg, aWParam);
+ // Don't consume the Windows WM_POINTER* messages
+ return false;
+ }
+
+ uint32_t pointerId = mPointerEvents.GetPointerId(aWParam);
+ POINTER_PEN_INFO penInfo{};
+ if (!mPointerEvents.GetPointerPenInfo(pointerId, &penInfo)) {
+ return false;
+ }
+
+ // When dispatching mouse events with pen, there may be some
+ // WM_POINTERUPDATE messages between WM_POINTERDOWN and WM_POINTERUP with
+ // small movements. Those events will reset sLastMousePoint and reset
+ // sLastClickCount. To prevent that, we keep the last pen down position
+ // and compare it with the subsequent WM_POINTERUPDATE. If the movement is
+ // smaller than GetSystemMetrics(SM_CXDRAG), then we suppress firing
+ // eMouseMove for WM_POINTERUPDATE.
+ static POINT sLastPointerDownPoint = {0};
+
+ // We don't support chorded buttons for pen. Keep the button at
+ // WM_POINTERDOWN.
+ static mozilla::MouseButton sLastPenDownButton = MouseButton::ePrimary;
+ static bool sPointerDown = false;
+
+ EventMessage message;
+ mozilla::MouseButton button = MouseButton::ePrimary;
+ switch (msg) {
+ case WM_POINTERDOWN: {
+ LayoutDeviceIntPoint eventPoint(GET_X_LPARAM(aLParam),
+ GET_Y_LPARAM(aLParam));
+ sLastPointerDownPoint.x = eventPoint.x;
+ sLastPointerDownPoint.y = eventPoint.y;
+ message = eMouseDown;
+ button = PenFlagsToMouseButton(penInfo.penFlags);
+ sLastPenDownButton = button;
+ sPointerDown = true;
+ } break;
+ case WM_POINTERUP:
+ message = eMouseUp;
+ MOZ_ASSERT(sPointerDown, "receive WM_POINTERUP w/o WM_POINTERDOWN");
+ button = sPointerDown ? sLastPenDownButton : MouseButton::ePrimary;
+ sPointerDown = false;
+ break;
+ case WM_POINTERUPDATE:
+ message = eMouseMove;
+ if (sPointerDown) {
+ LayoutDeviceIntPoint eventPoint(GET_X_LPARAM(aLParam),
+ GET_Y_LPARAM(aLParam));
+ int32_t movementX = sLastPointerDownPoint.x > eventPoint.x
+ ? sLastPointerDownPoint.x - eventPoint.x.value
+ : eventPoint.x.value - sLastPointerDownPoint.x;
+ int32_t movementY = sLastPointerDownPoint.y > eventPoint.y
+ ? sLastPointerDownPoint.y - eventPoint.y.value
+ : eventPoint.y.value - sLastPointerDownPoint.y;
+ bool insideMovementThreshold =
+ movementX < (int32_t)::GetSystemMetrics(SM_CXDRAG) &&
+ movementY < (int32_t)::GetSystemMetrics(SM_CYDRAG);
+
+ if (insideMovementThreshold) {
+ // Suppress firing eMouseMove for WM_POINTERUPDATE if the movement
+ // from last WM_POINTERDOWN is smaller than SM_CXDRAG / SM_CYDRAG
+ return false;
+ }
+ button = sLastPenDownButton;
+ }
+ break;
+ case WM_POINTERLEAVE:
+ message = eMouseExitFromWidget;
+ break;
+ default:
+ return false;
+ }
+
+ // Windows defines the pen pressure is normalized to a range between 0 and
+ // 1024. Convert it to float.
+ float pressure = penInfo.pressure ? (float)penInfo.pressure / 1024 : 0;
+ int16_t buttons = sPointerDown
+ ? nsContentUtils::GetButtonsFlagForButton(button)
+ : MouseButtonsFlag::eNoButtons;
+ WinPointerInfo pointerInfo(pointerId, penInfo.tiltX, penInfo.tiltY, pressure,
+ buttons);
+ // Per
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-pointer_pen_info,
+ // the rotation is normalized in a range of 0 to 359.
+ MOZ_ASSERT(penInfo.rotation <= 359);
+ pointerInfo.twist = (int32_t)penInfo.rotation;
+
+ // Fire touch events but not when the barrel button is pressed.
+ if (button != MouseButton::eSecondary &&
+ StaticPrefs::dom_w3c_pointer_events_scroll_by_pen_enabled() &&
+ DispatchTouchEventFromWMPointer(msg, aLParam, pointerInfo, button)) {
+ return true;
+ }
+
+ // The aLParam of WM_POINTER* is the screen location. Convert it to client
+ // location
+ LPARAM newLParam = lParamToClient(aLParam);
+ DispatchMouseEvent(message, aWParam, newLParam, false, button,
+ MouseEvent_Binding::MOZ_SOURCE_PEN, &pointerInfo);
+
+ if (button == MouseButton::eSecondary && message == eMouseUp) {
+ // Fire eContextMenu manually since consuming WM_POINTER* blocks
+ // WM_CONTEXTMENU
+ DispatchMouseEvent(eContextMenu, aWParam, newLParam, false, button,
+ MouseEvent_Binding::MOZ_SOURCE_PEN, &pointerInfo);
+ }
+ // Consume WM_POINTER* to stop Windows fires WM_*BUTTONDOWN / WM_*BUTTONUP
+ // WM_MOUSEMOVE.
+ return true;
+}
+
+void nsWindow::GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) {
+ *aInitData = WinCompositorWidgetInitData(
+ reinterpret_cast<uintptr_t>(mWnd),
+ reinterpret_cast<uintptr_t>(static_cast<nsIWidget*>(this)),
+ mTransparencyMode, mFrameState->GetSizeMode());
+}
+
+bool nsWindow::SynchronouslyRepaintOnResize() { return false; }
+
+void nsWindow::MaybeDispatchInitialFocusEvent() {
+ if (mIsShowingPreXULSkeletonUI && ::GetActiveWindow() == mWnd) {
+ DispatchFocusToTopLevelWindow(true);
+ }
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow();
+ return window.forget();
+}
+
+already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
+ nsCOMPtr<nsIWidget> window = new nsWindow(true);
+ return window.forget();
+}
+
+// static
+bool nsWindow::InitTouchInjection() {
+ if (!sTouchInjectInitialized) {
+ // Initialize touch injection on the first call
+ HMODULE hMod = LoadLibraryW(kUser32LibName);
+ if (!hMod) {
+ return false;
+ }
+
+ InitializeTouchInjectionPtr func =
+ (InitializeTouchInjectionPtr)GetProcAddress(hMod,
+ "InitializeTouchInjection");
+ if (!func) {
+ WinUtils::Log("InitializeTouchInjection not available.");
+ return false;
+ }
+
+ if (!func(TOUCH_INJECT_MAX_POINTS, TOUCH_FEEDBACK_DEFAULT)) {
+ WinUtils::Log("InitializeTouchInjection failure. GetLastError=%d",
+ GetLastError());
+ return false;
+ }
+
+ sInjectTouchFuncPtr =
+ (InjectTouchInputPtr)GetProcAddress(hMod, "InjectTouchInput");
+ if (!sInjectTouchFuncPtr) {
+ WinUtils::Log("InjectTouchInput not available.");
+ return false;
+ }
+ sTouchInjectInitialized = true;
+ }
+ return true;
+}
+
+bool nsWindow::InjectTouchPoint(uint32_t aId, LayoutDeviceIntPoint& aPoint,
+ POINTER_FLAGS aFlags, uint32_t aPressure,
+ uint32_t aOrientation) {
+ if (aId > TOUCH_INJECT_MAX_POINTS) {
+ WinUtils::Log("Pointer ID exceeds maximum. See TOUCH_INJECT_MAX_POINTS.");
+ return false;
+ }
+
+ POINTER_TOUCH_INFO info{};
+
+ info.touchFlags = TOUCH_FLAG_NONE;
+ info.touchMask =
+ TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
+ info.pressure = aPressure;
+ info.orientation = aOrientation;
+
+ info.pointerInfo.pointerFlags = aFlags;
+ info.pointerInfo.pointerType = PT_TOUCH;
+ info.pointerInfo.pointerId = aId;
+ info.pointerInfo.ptPixelLocation.x = aPoint.x;
+ info.pointerInfo.ptPixelLocation.y = aPoint.y;
+
+ info.rcContact.top = info.pointerInfo.ptPixelLocation.y - 2;
+ info.rcContact.bottom = info.pointerInfo.ptPixelLocation.y + 2;
+ info.rcContact.left = info.pointerInfo.ptPixelLocation.x - 2;
+ info.rcContact.right = info.pointerInfo.ptPixelLocation.x + 2;
+
+ for (int i = 0; i < 3; i++) {
+ if (sInjectTouchFuncPtr(1, &info)) {
+ break;
+ }
+ DWORD error = GetLastError();
+ if (error == ERROR_NOT_READY && i < 2) {
+ // We sent it too quickly after the previous injection (see bug 1535140
+ // comment 10). On the first loop iteration we just yield (via Sleep(0))
+ // and try again. If it happens again on the second loop iteration we
+ // explicitly Sleep(1) and try again. If that doesn't work either we just
+ // error out.
+ ::Sleep(i);
+ continue;
+ }
+ WinUtils::Log("InjectTouchInput failure. GetLastError=%d", error);
+ return false;
+ }
+ return true;
+}
+
+void nsWindow::ChangedDPI() {
+ if (mWidgetListener) {
+ if (PresShell* presShell = mWidgetListener->GetPresShell()) {
+ presShell->BackingScaleFactorChanged();
+ }
+ }
+}
+
+static Result<POINTER_FLAGS, nsresult> PointerStateToFlag(
+ nsWindow::TouchPointerState aPointerState, bool isUpdate) {
+ bool hover = aPointerState & nsWindow::TOUCH_HOVER;
+ bool contact = aPointerState & nsWindow::TOUCH_CONTACT;
+ bool remove = aPointerState & nsWindow::TOUCH_REMOVE;
+ bool cancel = aPointerState & nsWindow::TOUCH_CANCEL;
+
+ POINTER_FLAGS flags;
+ if (isUpdate) {
+ // We know about this pointer, send an update
+ flags = POINTER_FLAG_UPDATE;
+ if (hover) {
+ flags |= POINTER_FLAG_INRANGE;
+ } else if (contact) {
+ flags |= POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE;
+ } else if (remove) {
+ flags = POINTER_FLAG_UP;
+ }
+
+ if (cancel) {
+ flags |= POINTER_FLAG_CANCELED;
+ }
+ } else {
+ // Missing init state, error out
+ if (remove || cancel) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // Create a new pointer
+ flags = POINTER_FLAG_INRANGE;
+ if (contact) {
+ flags |= POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;
+ }
+ }
+ return flags;
+}
+
+nsresult nsWindow::SynthesizeNativeTouchPoint(
+ uint32_t aPointerId, nsIWidget::TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPointerPressure,
+ uint32_t aPointerOrientation, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpoint");
+
+ if (StaticPrefs::apz_test_fails_with_native_injection() ||
+ !InitTouchInjection()) {
+ // If we don't have touch injection from the OS, or if we are running a test
+ // that cannot properly inject events to satisfy the OS requirements (see
+ // bug 1313170) we can just fake it and synthesize the events from here.
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aPointerState == TOUCH_HOVER) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!mSynthesizedTouchInput) {
+ mSynthesizedTouchInput = MakeUnique<MultiTouchInput>();
+ }
+
+ WidgetEventTime time = CurrentMessageWidgetEventTime();
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState(
+ mSynthesizedTouchInput.get(), time.mTimeStamp, aPointerId,
+ aPointerState, pointInWindow, aPointerPressure, aPointerOrientation);
+ DispatchTouchInput(inputToDispatch);
+ return NS_OK;
+ }
+
+ // win api expects a value from 0 to 1024. aPointerPressure is a value
+ // from 0.0 to 1.0.
+ uint32_t pressure = (uint32_t)ceil(aPointerPressure * 1024);
+
+ // If we already know about this pointer id get it's record
+ return mActivePointers.WithEntryHandle(aPointerId, [&](auto&& entry) {
+ POINTER_FLAGS flags;
+ // Can't use MOZ_TRY_VAR because it confuses WithEntryHandle
+ auto result = PointerStateToFlag(aPointerState, !!entry);
+ if (result.isOk()) {
+ flags = result.unwrap();
+ } else {
+ return result.unwrapErr();
+ }
+
+ if (!entry) {
+ entry.Insert(MakeUnique<PointerInfo>(aPointerId, aPoint,
+ PointerInfo::PointerType::TOUCH));
+ } else {
+ if (entry.Data()->mType != PointerInfo::PointerType::TOUCH) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (aPointerState & TOUCH_REMOVE) {
+ // Remove the pointer from our tracking list. This is UniquePtr wrapped,
+ // so shouldn't leak.
+ entry.Remove();
+ }
+ }
+
+ return !InjectTouchPoint(aPointerId, aPoint, flags, pressure,
+ aPointerOrientation)
+ ? NS_ERROR_UNEXPECTED
+ : NS_OK;
+ });
+}
+
+nsresult nsWindow::ClearNativeTouchSequence(nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "cleartouch");
+ if (!sTouchInjectInitialized) {
+ return NS_OK;
+ }
+
+ // cancel all input points
+ for (auto iter = mActivePointers.Iter(); !iter.Done(); iter.Next()) {
+ auto* info = iter.UserData();
+ if (info->mType != PointerInfo::PointerType::TOUCH) {
+ continue;
+ }
+ InjectTouchPoint(info->mPointerId, info->mPosition, POINTER_FLAG_CANCELED);
+ iter.Remove();
+ }
+
+ nsBaseWidget::ClearNativeTouchSequence(nullptr);
+
+ return NS_OK;
+}
+
+#if !defined(NTDDI_WIN10_RS5) || (NTDDI_VERSION < NTDDI_WIN10_RS5)
+static CreateSyntheticPointerDevicePtr CreateSyntheticPointerDevice;
+static DestroySyntheticPointerDevicePtr DestroySyntheticPointerDevice;
+static InjectSyntheticPointerInputPtr InjectSyntheticPointerInput;
+#endif
+static HSYNTHETICPOINTERDEVICE sSyntheticPenDevice;
+
+static bool InitPenInjection() {
+ if (sSyntheticPenDevice) {
+ return true;
+ }
+#if !defined(NTDDI_WIN10_RS5) || (NTDDI_VERSION < NTDDI_WIN10_RS5)
+ HMODULE hMod = LoadLibraryW(kUser32LibName);
+ if (!hMod) {
+ return false;
+ }
+ CreateSyntheticPointerDevice =
+ (CreateSyntheticPointerDevicePtr)GetProcAddress(
+ hMod, "CreateSyntheticPointerDevice");
+ if (!CreateSyntheticPointerDevice) {
+ WinUtils::Log("CreateSyntheticPointerDevice not available.");
+ return false;
+ }
+ DestroySyntheticPointerDevice =
+ (DestroySyntheticPointerDevicePtr)GetProcAddress(
+ hMod, "DestroySyntheticPointerDevice");
+ if (!DestroySyntheticPointerDevice) {
+ WinUtils::Log("DestroySyntheticPointerDevice not available.");
+ return false;
+ }
+ InjectSyntheticPointerInput = (InjectSyntheticPointerInputPtr)GetProcAddress(
+ hMod, "InjectSyntheticPointerInput");
+ if (!InjectSyntheticPointerInput) {
+ WinUtils::Log("InjectSyntheticPointerInput not available.");
+ return false;
+ }
+#endif
+ sSyntheticPenDevice =
+ CreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT);
+ return !!sSyntheticPenDevice;
+}
+
+nsresult nsWindow::SynthesizeNativePenInput(
+ uint32_t aPointerId, nsIWidget::TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPressure, uint32_t aRotation,
+ int32_t aTiltX, int32_t aTiltY, int32_t aButton, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "peninput");
+ if (!InitPenInjection()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // win api expects a value from 0 to 1024. aPointerPressure is a value
+ // from 0.0 to 1.0.
+ uint32_t pressure = (uint32_t)ceil(aPressure * 1024);
+
+ // If we already know about this pointer id get it's record
+ return mActivePointers.WithEntryHandle(aPointerId, [&](auto&& entry) {
+ POINTER_FLAGS flags;
+ // Can't use MOZ_TRY_VAR because it confuses WithEntryHandle
+ auto result = PointerStateToFlag(aPointerState, !!entry);
+ if (result.isOk()) {
+ flags = result.unwrap();
+ } else {
+ return result.unwrapErr();
+ }
+
+ if (!entry) {
+ entry.Insert(MakeUnique<PointerInfo>(aPointerId, aPoint,
+ PointerInfo::PointerType::PEN));
+ } else {
+ if (entry.Data()->mType != PointerInfo::PointerType::PEN) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (aPointerState & TOUCH_REMOVE) {
+ // Remove the pointer from our tracking list. This is UniquePtr wrapped,
+ // so shouldn't leak.
+ entry.Remove();
+ }
+ }
+
+ POINTER_TYPE_INFO info{};
+
+ info.type = PT_PEN;
+ info.penInfo.pointerInfo.pointerType = PT_PEN;
+ info.penInfo.pointerInfo.pointerFlags = flags;
+ info.penInfo.pointerInfo.pointerId = aPointerId;
+ info.penInfo.pointerInfo.ptPixelLocation.x = aPoint.x;
+ info.penInfo.pointerInfo.ptPixelLocation.y = aPoint.y;
+
+ info.penInfo.penFlags = PEN_FLAG_NONE;
+ // PEN_FLAG_ERASER is not supported this way, unfortunately.
+ if (aButton == 2) {
+ info.penInfo.penFlags |= PEN_FLAG_BARREL;
+ }
+ info.penInfo.penMask = PEN_MASK_PRESSURE | PEN_MASK_ROTATION |
+ PEN_MASK_TILT_X | PEN_MASK_TILT_Y;
+ info.penInfo.pressure = pressure;
+ info.penInfo.rotation = aRotation;
+ info.penInfo.tiltX = aTiltX;
+ info.penInfo.tiltY = aTiltY;
+
+ return InjectSyntheticPointerInput(sSyntheticPenDevice, &info, 1)
+ ? NS_OK
+ : NS_ERROR_UNEXPECTED;
+ });
+};
+
+bool nsWindow::HandleAppCommandMsg(const MSG& aAppCommandMsg,
+ LRESULT* aRetValue) {
+ ModifierKeyState modKeyState;
+ NativeKey nativeKey(this, aAppCommandMsg, modKeyState);
+ bool consumed = nativeKey.HandleAppCommandMessage();
+ *aRetValue = consumed ? 1 : 0;
+ return consumed;
+}
+
+#ifdef DEBUG
+nsresult nsWindow::SetHiDPIMode(bool aHiDPI) {
+ return WinUtils::SetHiDPIMode(aHiDPI);
+}
+
+nsresult nsWindow::RestoreHiDPIMode() { return WinUtils::RestoreHiDPIMode(); }
+#endif
+
+mozilla::Maybe<UINT> nsWindow::GetHiddenTaskbarEdge() {
+ HMONITOR windowMonitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONEAREST);
+
+ // Check all four sides of our monitor for an appbar. Skip any that aren't
+ // the system taskbar.
+ MONITORINFO mi;
+ mi.cbSize = sizeof(MONITORINFO);
+ ::GetMonitorInfo(windowMonitor, &mi);
+
+ APPBARDATA appBarData;
+ appBarData.cbSize = sizeof(appBarData);
+ appBarData.rc = mi.rcMonitor;
+ const auto kEdges = {ABE_BOTTOM, ABE_TOP, ABE_LEFT, ABE_RIGHT};
+ for (auto edge : kEdges) {
+ appBarData.uEdge = edge;
+ HWND appBarHwnd = (HWND)SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &appBarData);
+ if (appBarHwnd) {
+ nsAutoString className;
+ if (WinUtils::GetClassName(appBarHwnd, className)) {
+ if (className.Equals(L"Shell_TrayWnd") ||
+ className.Equals(L"Shell_SecondaryTrayWnd")) {
+ return Some(edge);
+ }
+ }
+ }
+ }
+
+ return Nothing();
+}
+
+static nsSizeMode GetSizeModeForWindowFrame(HWND aWnd, bool aFullscreenMode) {
+ WINDOWPLACEMENT pl;
+ pl.length = sizeof(pl);
+ ::GetWindowPlacement(aWnd, &pl);
+
+ if (pl.showCmd == SW_SHOWMINIMIZED) {
+ return nsSizeMode_Minimized;
+ } else if (aFullscreenMode) {
+ return nsSizeMode_Fullscreen;
+ } else if (pl.showCmd == SW_SHOWMAXIMIZED) {
+ return nsSizeMode_Maximized;
+ } else {
+ return nsSizeMode_Normal;
+ }
+}
+
+static void ShowWindowWithMode(HWND aWnd, nsSizeMode aMode) {
+ // This will likely cause a callback to
+ // nsWindow::FrameState::{OnFrameChanging() and OnFrameChanged()}
+ switch (aMode) {
+ case nsSizeMode_Fullscreen:
+ ::ShowWindow(aWnd, SW_SHOW);
+ break;
+
+ case nsSizeMode_Maximized:
+ ::ShowWindow(aWnd, SW_MAXIMIZE);
+ break;
+
+ case nsSizeMode_Minimized:
+ ::ShowWindow(aWnd, SW_MINIMIZE);
+ break;
+
+ default:
+ // Don't call ::ShowWindow if we're trying to "restore" a window that is
+ // already in a normal state. Prevents a bug where snapping to one side
+ // of the screen and then minimizing would cause Windows to forget our
+ // window's correct restored position/size.
+ if (GetCurrentShowCmd(aWnd) != SW_SHOWNORMAL) {
+ ::ShowWindow(aWnd, SW_RESTORE);
+ }
+ }
+}
+
+nsWindow::FrameState::FrameState(nsWindow* aWindow) : mWindow(aWindow) {}
+
+nsSizeMode nsWindow::FrameState::GetSizeMode() const { return mSizeMode; }
+
+void nsWindow::FrameState::CheckInvariant() const {
+ MOZ_ASSERT(mSizeMode >= 0 && mSizeMode < nsSizeMode_Invalid);
+ MOZ_ASSERT(mLastSizeMode >= 0 && mLastSizeMode < nsSizeMode_Invalid);
+ MOZ_ASSERT(mPreFullscreenSizeMode >= 0 &&
+ mPreFullscreenSizeMode < nsSizeMode_Invalid);
+ MOZ_ASSERT(mWindow);
+
+ // We should never observe fullscreen sizemode unless fullscreen is enabled
+ MOZ_ASSERT_IF(mSizeMode == nsSizeMode_Fullscreen, mFullscreenMode);
+ MOZ_ASSERT_IF(!mFullscreenMode, mSizeMode != nsSizeMode_Fullscreen);
+
+ // Something went wrong if we somehow saved fullscreen mode when we are
+ // changing into fullscreen mode
+ MOZ_ASSERT(mPreFullscreenSizeMode != nsSizeMode_Fullscreen);
+}
+
+void nsWindow::FrameState::ConsumePreXULSkeletonState(bool aWasMaximized) {
+ mSizeMode = aWasMaximized ? nsSizeMode_Maximized : nsSizeMode_Normal;
+}
+
+void nsWindow::FrameState::EnsureSizeMode(nsSizeMode aMode,
+ DoShowWindow aDoShowWindow) {
+ if (mSizeMode == aMode) {
+ return;
+ }
+
+ if (StaticPrefs::widget_windows_fullscreen_remind_taskbar()) {
+ // If we're unminimizing a window, asynchronously notify the taskbar after
+ // the message has been processed. This redundant notification works around
+ // a race condition in explorer.exe. (See bug 1835851, or comments in
+ // TaskbarConcealer.)
+ //
+ // Note that we notify regardless of `aMode`: unminimizing a non-fullscreen
+ // window can also affect the correct taskbar state, yet fail to affect the
+ // current taskbar state.
+ if (mSizeMode == nsSizeMode_Minimized) {
+ ::PostMessage(mWindow->mWnd, MOZ_WM_FULLSCREEN_STATE_UPDATE, 0, 0);
+ }
+ }
+
+ if (aMode == nsSizeMode_Fullscreen) {
+ EnsureFullscreenMode(true, aDoShowWindow);
+ MOZ_ASSERT(mSizeMode == nsSizeMode_Fullscreen);
+ } else if (mSizeMode == nsSizeMode_Fullscreen && aMode == nsSizeMode_Normal) {
+ // If we are in fullscreen mode, minimize should work like normal and
+ // return us to fullscreen mode when unminimized. Maximize isn't really
+ // available and won't do anything. "Restore" should do the same thing as
+ // requesting to end fullscreen.
+ EnsureFullscreenMode(false, aDoShowWindow);
+ } else {
+ SetSizeModeInternal(aMode, aDoShowWindow);
+ }
+}
+
+void nsWindow::FrameState::EnsureFullscreenMode(bool aFullScreen,
+ DoShowWindow aDoShowWindow) {
+ const bool changed = aFullScreen != mFullscreenMode;
+ if (changed && aFullScreen) {
+ // Save the size mode from before fullscreen.
+ mPreFullscreenSizeMode = mSizeMode;
+ }
+ mFullscreenMode = aFullScreen;
+ if (changed || aFullScreen) {
+ // NOTE(emilio): When minimizing a fullscreen window we remain with
+ // mFullscreenMode = true, but mSizeMode = nsSizeMode_Minimized. We need to
+ // make sure to call SetSizeModeInternal even if mFullscreenMode didn't
+ // change, to ensure we actually end up with a fullscreen sizemode when
+ // restoring a window from that state.
+ SetSizeModeInternal(
+ aFullScreen ? nsSizeMode_Fullscreen : mPreFullscreenSizeMode,
+ aDoShowWindow);
+ }
+}
+
+void nsWindow::FrameState::OnFrameChanging() {
+ const nsSizeMode newSizeMode =
+ GetSizeModeForWindowFrame(mWindow->mWnd, mFullscreenMode);
+ EnsureSizeMode(newSizeMode);
+ mWindow->UpdateNonClientMargins(false);
+}
+
+void nsWindow::FrameState::OnFrameChanged() {
+ // We don't want to perform the ShowWindow ourselves if we're on the frame
+ // changed message. Windows has done the frame change for us, and we take care
+ // of activating as needed. We also don't want to potentially trigger
+ // more focus / restore. Among other things, this addresses a bug on Win7
+ // related to window docking. (bug 489258)
+ const auto newSizeMode =
+ GetSizeModeForWindowFrame(mWindow->mWnd, mFullscreenMode);
+ EnsureSizeMode(newSizeMode, DoShowWindow::No);
+
+ // If window was restored, activate the window now to get correct attributes.
+ if (mWindow->mIsVisible && mWindow->IsForegroundWindow() &&
+ mLastSizeMode == nsSizeMode_Minimized &&
+ mSizeMode != nsSizeMode_Minimized) {
+ mWindow->DispatchFocusToTopLevelWindow(true);
+ }
+ mLastSizeMode = mSizeMode;
+}
+
+static void MaybeLogSizeMode(nsSizeMode aMode) {
+#ifdef WINSTATE_DEBUG_OUTPUT
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("*** SizeMode: %d\n", int(aMode)));
+#endif
+}
+
+void nsWindow::FrameState::SetSizeModeInternal(nsSizeMode aMode,
+ DoShowWindow aDoShowWindow) {
+ if (mSizeMode == aMode) {
+ return;
+ }
+
+ const auto oldSizeMode = mSizeMode;
+ const bool fullscreenChange =
+ mSizeMode == nsSizeMode_Fullscreen || aMode == nsSizeMode_Fullscreen;
+ const bool fullscreen = aMode == nsSizeMode_Fullscreen;
+
+ mLastSizeMode = mSizeMode;
+ mSizeMode = aMode;
+
+ MaybeLogSizeMode(mSizeMode);
+
+ if (bool(aDoShowWindow) && mWindow->mIsVisible) {
+ ShowWindowWithMode(mWindow->mWnd, aMode);
+ }
+
+ mWindow->UpdateNonClientMargins(false);
+
+ if (fullscreenChange) {
+ mWindow->OnFullscreenChanged(oldSizeMode, fullscreen);
+ }
+
+ mWindow->OnSizeModeChange();
+}
+
+void nsWindow::ContextMenuPreventer::Update(
+ const WidgetMouseEvent& aEvent,
+ const nsIWidget::ContentAndAPZEventStatus& aEventStatus) {
+ mNeedsToPreventContextMenu =
+ aEvent.mMessage == eMouseUp &&
+ aEvent.mButton == MouseButton::eSecondary &&
+ aEvent.mInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE &&
+ aEventStatus.mApzStatus == nsEventStatus_eConsumeNoDefault;
+}
diff --git a/widget/windows/nsWindow.h b/widget/windows/nsWindow.h
new file mode 100644
index 0000000000..c75c9d174d
--- /dev/null
+++ b/widget/windows/nsWindow.h
@@ -0,0 +1,905 @@
+/* -*- 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/. */
+
+#ifndef WIDGET_WINDOWS_NSWINDOW_H_
+#define WIDGET_WINDOWS_NSWINDOW_H_
+
+/*
+ * nsWindow - Native window management and event handling.
+ */
+
+#include "mozilla/RefPtr.h"
+#include "nsBaseWidget.h"
+#include "CompositorWidget.h"
+#include "mozilla/EventForwards.h"
+#include "nsClassHashtable.h"
+#include <windows.h>
+#include "touchinjection_sdk80.h"
+#include "nsdefs.h"
+#include "nsUserIdleService.h"
+#include "nsToolkit.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "gfxWindowsPlatform.h"
+#include "gfxWindowsSurface.h"
+#include "nsWindowDbg.h"
+#include "cairo.h"
+#include "nsRegion.h"
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/UniquePtr.h"
+#include "nsMargin.h"
+#include "nsRegionFwd.h"
+
+#include "nsWinGesture.h"
+#include "WinPointerEvents.h"
+#include "WinUtils.h"
+#include "WindowHook.h"
+#include "TaskbarWindowPreview.h"
+
+#ifdef ACCESSIBILITY
+# include "oleacc.h"
+# include "mozilla/a11y/LocalAccessible.h"
+#endif
+
+#include "nsUXThemeData.h"
+#include "nsIUserIdleServiceInternal.h"
+
+#include "IMMHandler.h"
+#include "CheckInvariantWrapper.h"
+
+/**
+ * Forward class definitions
+ */
+
+class nsNativeDragTarget;
+class nsIRollupListener;
+class imgIContainer;
+
+namespace mozilla {
+class WidgetMouseEvent;
+namespace widget {
+class NativeKey;
+class InProcessWinCompositorWidget;
+struct MSGResult;
+class DirectManipulationOwner;
+} // namespace widget
+} // namespace mozilla
+
+/**
+ * Forward Windows-internal definitions of otherwise incomplete ones provided by
+ * the SDK.
+ */
+const CLSID CLSID_ImmersiveShell = {
+ 0xC2F03A33,
+ 0x21F5,
+ 0x47FA,
+ {0xB4, 0xBB, 0x15, 0x63, 0x62, 0xA2, 0xF2, 0x39}};
+
+/**
+ * Native WIN32 window wrapper.
+ */
+
+class nsWindow final : public nsBaseWidget {
+ public:
+ using WindowHook = mozilla::widget::WindowHook;
+ using IMEContext = mozilla::widget::IMEContext;
+ using WidgetEventTime = mozilla::WidgetEventTime;
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsWindow, nsBaseWidget)
+
+ explicit nsWindow(bool aIsChildWindow = false);
+
+ void SendAnAPZEvent(mozilla::InputData& aEvent);
+
+ /*
+ * Init a standard gecko event for this widget.
+ * @param aEvent the event to initialize.
+ * @param aPoint message position in physical coordinates.
+ */
+ void InitEvent(mozilla::WidgetGUIEvent& aEvent,
+ LayoutDeviceIntPoint* aPoint = nullptr);
+
+ /*
+ * Returns WidgetEventTime instance which is initialized with current message
+ * time.
+ */
+ WidgetEventTime CurrentMessageWidgetEventTime() const;
+
+ /*
+ * Dispatch a gecko keyboard event for this widget. This
+ * is called by KeyboardLayout to dispatch gecko events.
+ * Returns true if it's consumed. Otherwise, false.
+ */
+ bool DispatchKeyboardEvent(mozilla::WidgetKeyboardEvent* aEvent);
+
+ /*
+ * Dispatch a gecko wheel event for this widget. This
+ * is called by ScrollHandler to dispatch gecko events.
+ * Returns true if it's consumed. Otherwise, false.
+ */
+ bool DispatchWheelEvent(mozilla::WidgetWheelEvent* aEvent);
+
+ /*
+ * Dispatch a gecko content command event for this widget. This
+ * is called by ScrollHandler to dispatch gecko events.
+ * Returns true if it's consumed. Otherwise, false.
+ */
+ bool DispatchContentCommandEvent(mozilla::WidgetContentCommandEvent* aEvent);
+
+ /*
+ * Return the parent window, if it exists.
+ */
+ nsWindow* GetParentWindowBase(bool aIncludeOwner);
+
+ /*
+ * Return true if this is a top level widget.
+ */
+ bool IsTopLevelWidget() { return mIsTopWidgetWindow; }
+
+ // nsIWidget interface
+ using nsBaseWidget::Create; // for Create signature not overridden here
+ [[nodiscard]] nsresult Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* aInitData = nullptr) override;
+ void Destroy() override;
+ void SetParent(nsIWidget* aNewParent) override;
+ nsIWidget* GetParent(void) override;
+ float GetDPI() override;
+ double GetDefaultScaleInternal() override;
+ int32_t LogToPhys(double aValue);
+ mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() override {
+ if (mozilla::widget::WinUtils::IsPerMonitorDPIAware()) {
+ return mozilla::DesktopToLayoutDeviceScale(1.0);
+ } else {
+ return mozilla::DesktopToLayoutDeviceScale(GetDefaultScaleInternal());
+ }
+ }
+
+ void Show(bool aState) override;
+ bool IsVisible() const override;
+ void ConstrainPosition(DesktopIntPoint&) override;
+ void SetSizeConstraints(const SizeConstraints& aConstraints) override;
+ void LockAspectRatio(bool aShouldLock) override;
+ const SizeConstraints GetSizeConstraints() override;
+ void SetInputRegion(const InputRegion&) override;
+ void Move(double aX, double aY) override;
+ void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+ mozilla::Maybe<bool> IsResizingNativeWidget() override;
+ void PlaceBehind(nsTopLevelWidgetZPlacement aPlacement, nsIWidget* aWidget,
+ bool aActivate) override;
+ void SetSizeMode(nsSizeMode aMode) override;
+ nsSizeMode SizeMode() override;
+ void GetWorkspaceID(nsAString& workspaceID) override;
+ void MoveToWorkspace(const nsAString& workspaceID) override;
+ void SuppressAnimation(bool aSuppress) override;
+ void Enable(bool aState) override;
+ bool IsEnabled() const override;
+ void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ LayoutDeviceIntRect GetBounds() override;
+ LayoutDeviceIntRect GetScreenBounds() override;
+ [[nodiscard]] nsresult GetRestoredBounds(LayoutDeviceIntRect& aRect) override;
+ LayoutDeviceIntRect GetClientBounds() override;
+ LayoutDeviceIntPoint GetClientOffset() override;
+ void SetBackgroundColor(const nscolor& aColor) override;
+ void SetCursor(const Cursor&) override;
+ bool PrepareForFullscreenTransition(nsISupports** aData) override;
+ void PerformFullscreenTransition(FullscreenTransitionStage aStage,
+ uint16_t aDuration, nsISupports* aData,
+ nsIRunnable* aCallback) override;
+ void CleanupFullscreenTransition() override;
+ nsresult MakeFullScreen(bool aFullScreen) override;
+ void HideWindowChrome(bool aShouldHide) override;
+ void Invalidate(bool aEraseBackground = false, bool aUpdateNCArea = false,
+ bool aIncludeChildren = false);
+ void Invalidate(const LayoutDeviceIntRect& aRect) override;
+ void* GetNativeData(uint32_t aDataType) override;
+ void FreeNativeData(void* data, uint32_t aDataType) override;
+ nsresult SetTitle(const nsAString& aTitle) override;
+ void SetIcon(const nsAString& aIconSpec) override;
+ LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ LayoutDeviceIntMargin ClientToWindowMargin() override;
+ nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+ void EnableDragDrop(bool aEnable) override;
+ void CaptureMouse(bool aCapture);
+ void CaptureRollupEvents(bool aDoCapture) override;
+ [[nodiscard]] nsresult GetAttention(int32_t aCycleCount) override;
+ bool HasPendingInputEvent() override;
+ WindowRenderer* GetWindowRenderer() override;
+ void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override;
+ [[nodiscard]] nsresult OnDefaultButtonLoaded(
+ const LayoutDeviceIntRect& aButtonRect) override;
+ nsresult SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout,
+ int32_t aNativeKeyCode,
+ uint32_t aModifierFlags,
+ const nsAString& aCharacters,
+ const nsAString& aUnmodifiedCharacters,
+ nsIObserver* aObserver) override;
+ nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
+ NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton,
+ nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ return SynthesizeNativeMouseEvent(
+ aPoint, NativeMouseMessage::Move, mozilla::MouseButton::eNotPressed,
+ nsIWidget::Modifiers::NO_MODIFIERS, aObserver);
+ }
+
+ nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override;
+
+ nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlagsn,
+ nsIObserver* aObserver) override;
+
+ void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override;
+ InputContext GetInputContext() override;
+ TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override;
+ void SetTransparencyMode(TransparencyMode aMode) override;
+ TransparencyMode GetTransparencyMode() override;
+ nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) override;
+ void SetResizeMargin(mozilla::LayoutDeviceIntCoord aResizeMargin) override;
+ void UpdateWindowDraggingRegion(
+ const LayoutDeviceIntRegion& aRegion) override;
+
+ uint32_t GetMaxTouchPoints() const override;
+ void SetWindowClass(const nsAString& xulWinType, const nsAString& xulWinClass,
+ const nsAString& xulWinName) override;
+
+ /**
+ * Event helpers
+ */
+ bool DispatchMouseEvent(mozilla::EventMessage aEventMessage, WPARAM wParam,
+ LPARAM lParam, bool aIsContextMenuKey,
+ int16_t aButton, uint16_t aInputSource,
+ WinPointerInfo* aPointerInfo = nullptr,
+ bool aIgnoreAPZ = false);
+ void DispatchPendingEvents();
+ void DispatchCustomEvent(const nsString& eventName);
+
+#ifdef ACCESSIBILITY
+ /**
+ * Return an accessible associated with the window.
+ */
+ mozilla::a11y::LocalAccessible* GetAccessible();
+#endif // ACCESSIBILITY
+
+ /**
+ * Window utilities
+ */
+ nsWindow* GetTopLevelWindow(bool aStopOnDialogOrPopup);
+ WNDPROC GetPrevWindowProc() { return mPrevWndProc.valueOr(nullptr); }
+ WindowHook& GetWindowHook() { return mWindowHook; }
+ nsWindow* GetParentWindow(bool aIncludeOwner);
+
+ /**
+ * Misc.
+ */
+ bool WidgetTypeSupportsAcceleration() override;
+
+ void ForcePresent();
+ bool TouchEventShouldStartDrag(mozilla::EventMessage aEventMessage,
+ LayoutDeviceIntPoint aEventPoint);
+
+ void SetSmallIcon(HICON aIcon);
+ void SetBigIcon(HICON aIcon);
+ void SetSmallIconNoData();
+ void SetBigIconNoData();
+
+ static void SetIsRestoringSession(const bool aIsRestoringSession) {
+ sIsRestoringSession = aIsRestoringSession;
+ }
+
+ bool IsRTL() const { return mIsRTL; }
+
+ /**
+ * AssociateDefaultIMC() associates or disassociates the default IMC for
+ * the window.
+ *
+ * @param aAssociate TRUE, associates the default IMC with the window.
+ * Otherwise, disassociates the default IMC from the
+ * window.
+ * @return TRUE if this method associated the default IMC with
+ * disassociated window or disassociated the default IMC
+ * from associated window.
+ * Otherwise, i.e., if this method did nothing actually,
+ * FALSE.
+ */
+ bool AssociateDefaultIMC(bool aAssociate);
+
+ bool HasTaskbarIconBeenCreated() { return mHasTaskbarIconBeenCreated; }
+ // Called when either the nsWindow or an nsITaskbarTabPreview receives the
+ // noticiation that this window has its icon placed on the taskbar.
+ void SetHasTaskbarIconBeenCreated(bool created = true) {
+ mHasTaskbarIconBeenCreated = created;
+ }
+
+ // Getter/setter for the nsITaskbarWindowPreview for this nsWindow
+ already_AddRefed<nsITaskbarWindowPreview> GetTaskbarPreview() {
+ nsCOMPtr<nsITaskbarWindowPreview> preview(
+ do_QueryReferent(mTaskbarPreview));
+ return preview.forget();
+ }
+ void SetTaskbarPreview(nsITaskbarWindowPreview* preview) {
+ mTaskbarPreview = do_GetWeakReference(preview);
+ }
+
+ void ReparentNativeWidget(nsIWidget* aNewParent) override;
+
+ // Open file picker tracking
+ void PickerOpen();
+ void PickerClosed();
+
+ bool DestroyCalled() { return mDestroyCalled; }
+
+ bool IsPopup();
+ bool ShouldUseOffMainThreadCompositing() override;
+
+ const IMEContext& DefaultIMC() const { return mDefaultIMC; }
+
+ void GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) override;
+ bool IsTouchWindow() const { return mTouchWindow; }
+ bool SynchronouslyRepaintOnResize() override;
+ void MaybeDispatchInitialFocusEvent() override;
+
+ void LocalesChanged() override;
+
+ void NotifyOcclusionState(mozilla::widget::OcclusionState aState) override;
+ void MaybeEnableWindowOcclusion(bool aEnable);
+
+ /*
+ * Return the HWND or null for this widget.
+ */
+ HWND GetWindowHandle() {
+ return static_cast<HWND>(GetNativeData(NS_NATIVE_WINDOW));
+ }
+
+ /*
+ * Touch input injection apis
+ */
+ nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+ nsresult ClearNativeTouchSequence(nsIObserver* aObserver) override;
+
+ nsresult SynthesizeNativePenInput(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPressure, uint32_t aRotation,
+ int32_t aTiltX, int32_t aTiltY,
+ int32_t aButton,
+ nsIObserver* aObserver) override;
+
+ /*
+ * WM_APPCOMMAND common handler.
+ * Sends events via NativeKey::HandleAppCommandMessage().
+ */
+ bool HandleAppCommandMsg(const MSG& aAppCommandMsg, LRESULT* aRetValue);
+
+ const InputContext& InputContextRef() const { return mInputContext; }
+
+ private:
+ using TimeStamp = mozilla::TimeStamp;
+ using TimeDuration = mozilla::TimeDuration;
+ using TaskbarWindowPreview = mozilla::widget::TaskbarWindowPreview;
+ using NativeKey = mozilla::widget::NativeKey;
+ using MSGResult = mozilla::widget::MSGResult;
+ using PlatformCompositorWidgetDelegate =
+ mozilla::widget::PlatformCompositorWidgetDelegate;
+
+ struct Desktop {
+ // Cached GUID of the virtual desktop this window should be on.
+ // This value may be stale.
+ nsString mID;
+ bool mUpdateIsQueued = false;
+ };
+
+ class PointerInfo {
+ public:
+ enum class PointerType : uint8_t {
+ TOUCH,
+ PEN,
+ };
+
+ PointerInfo(int32_t aPointerId, LayoutDeviceIntPoint& aPoint,
+ PointerType aType)
+ : mPointerId(aPointerId), mPosition(aPoint), mType(aType) {}
+
+ int32_t mPointerId;
+ LayoutDeviceIntPoint mPosition;
+ PointerType mType;
+ };
+
+ class FrameState {
+ public:
+ explicit FrameState(nsWindow* aWindow);
+
+ void ConsumePreXULSkeletonState(bool aWasMaximized);
+
+ // Whether we should call ShowWindow with the relevant size mode if needed.
+ // We want to avoid that when Windows is already performing the change for
+ // us (via the SWP_FRAMECHANGED messages).
+ enum class DoShowWindow : bool { No, Yes };
+
+ void EnsureSizeMode(nsSizeMode, DoShowWindow = DoShowWindow::Yes);
+ void EnsureFullscreenMode(bool, DoShowWindow = DoShowWindow::Yes);
+ void OnFrameChanging();
+ void OnFrameChanged();
+
+ nsSizeMode GetSizeMode() const;
+
+ void CheckInvariant() const;
+
+ private:
+ void SetSizeModeInternal(nsSizeMode, DoShowWindow);
+
+ nsSizeMode mSizeMode = nsSizeMode_Normal;
+ // XXX mLastSizeMode is rather bizarre and needs some documentation.
+ nsSizeMode mLastSizeMode = nsSizeMode_Normal;
+ // The old size mode before going into fullscreen mode. This should never
+ // be nsSizeMode_Fullscreen.
+ nsSizeMode mPreFullscreenSizeMode = nsSizeMode_Normal;
+ // Whether we're in fullscreen. We need to keep this state out of band,
+ // rather than just using mSizeMode, because a window can be minimized
+ // while fullscreen, and we don't store the fullscreen state anywhere else.
+ bool mFullscreenMode = false;
+ nsWindow* mWindow;
+ };
+
+ // Manager for taskbar-hiding. No persistent state.
+ class TaskbarConcealer;
+
+ // A magic number to identify the FAKETRACKPOINTSCROLLABLE window created
+ // when the trackpoint hack is enabled.
+ enum { eFakeTrackPointScrollableID = 0x46545053 };
+
+ // Used for displayport suppression during window resize
+ enum ResizeState { NOT_RESIZING, IN_SIZEMOVE, RESIZING, MOVING };
+
+ ~nsWindow() override;
+
+ void WindowUsesOMTC() override;
+ void RegisterTouchWindow() override;
+
+ /**
+ * Callbacks
+ */
+ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam,
+ LPARAM lParam);
+ static LRESULT CALLBACK WindowProcInternal(HWND hWnd, UINT msg, WPARAM wParam,
+ LPARAM lParam);
+
+ static BOOL CALLBACK DispatchStarvedPaints(HWND aTopWindow, LPARAM aMsg);
+ static BOOL CALLBACK RegisterTouchForDescendants(HWND aTopWindow,
+ LPARAM aMsg);
+ static BOOL CALLBACK UnregisterTouchForDescendants(HWND aTopWindow,
+ LPARAM aMsg);
+ static LRESULT CALLBACK MozSpecialMsgFilter(int code, WPARAM wParam,
+ LPARAM lParam);
+ static LRESULT CALLBACK MozSpecialWndProc(int code, WPARAM wParam,
+ LPARAM lParam);
+ static LRESULT CALLBACK MozSpecialMouseProc(int code, WPARAM wParam,
+ LPARAM lParam);
+ static VOID CALLBACK HookTimerForPopups(HWND hwnd, UINT uMsg, UINT idEvent,
+ DWORD dwTime);
+
+ /**
+ * Window utilities
+ */
+ LPARAM lParamToScreen(LPARAM lParam);
+ LPARAM lParamToClient(LPARAM lParam);
+
+ WPARAM wParamFromGlobalMouseState();
+
+ bool AssociateWithNativeWindow();
+ void DissociateFromNativeWindow();
+ bool CanTakeFocus();
+ bool UpdateNonClientMargins(bool aReflowWindow = true);
+ void UpdateDarkModeToolbar();
+ void UpdateGetWindowInfoCaptionStatus(bool aActiveCaption);
+ void ResetLayout();
+ void InvalidateNonClientRegion();
+ static const wchar_t* GetMainWindowClass();
+ HWND GetOwnerWnd() const { return ::GetWindow(mWnd, GW_OWNER); }
+ bool IsOwnerForegroundWindow() const {
+ HWND owner = GetOwnerWnd();
+ return owner && owner == ::GetForegroundWindow();
+ }
+ bool IsForegroundWindow() const { return mWnd == ::GetForegroundWindow(); }
+ bool IsPopup() const { return mWindowType == WindowType::Popup; }
+ bool IsCloaked() const { return mIsCloaked; }
+
+ /**
+ * Event processing helpers
+ */
+ HWND GetTopLevelForFocus(HWND aCurWnd);
+ void DispatchFocusToTopLevelWindow(bool aIsActivate);
+ bool DispatchStandardEvent(mozilla::EventMessage aMsg);
+ void RelayMouseEvent(UINT aMsg, WPARAM wParam, LPARAM lParam);
+ bool ProcessMessage(UINT msg, WPARAM& wParam, LPARAM& lParam,
+ LRESULT* aRetValue);
+ // We wrap this in ProcessMessage so we can log the return value
+ bool ProcessMessageInternal(UINT msg, WPARAM& wParam, LPARAM& lParam,
+ LRESULT* aRetValue);
+ bool ExternalHandlerProcessMessage(UINT aMessage, WPARAM& aWParam,
+ LPARAM& aLParam, MSGResult& aResult);
+ LRESULT ProcessCharMessage(const MSG& aMsg, bool* aEventDispatched);
+ LRESULT ProcessKeyUpMessage(const MSG& aMsg, bool* aEventDispatched);
+ LRESULT ProcessKeyDownMessage(const MSG& aMsg, bool* aEventDispatched);
+ static bool EventIsInsideWindow(
+ nsWindow* aWindow,
+ mozilla::Maybe<POINT> aEventPoint = mozilla::Nothing());
+ static void PostSleepWakeNotification(const bool aIsSleepMode);
+ int32_t ClientMarginHitTestPoint(int32_t mx, int32_t my);
+ void SetWindowButtonRect(WindowButtonType aButtonType,
+ const LayoutDeviceIntRect& aClientRect) override {
+ mWindowBtnRect[aButtonType] = aClientRect;
+ }
+ TimeStamp GetMessageTimeStamp(LONG aEventTime) const;
+ static void UpdateFirstEventTime(DWORD aEventTime);
+ void FinishLiveResizing(ResizeState aNewState);
+ mozilla::Maybe<mozilla::PanGestureInput> ConvertTouchToPanGesture(
+ const mozilla::MultiTouchInput& aTouchInput, PTOUCHINPUT aOriginalEvent);
+ void DispatchTouchOrPanGestureInput(mozilla::MultiTouchInput& aTouchInput,
+ PTOUCHINPUT aOSEvent);
+
+ /**
+ * Event handlers
+ */
+ void OnDestroy() override;
+ bool OnResize(const LayoutDeviceIntSize& aSize);
+ void OnSizeModeChange();
+ bool OnGesture(WPARAM wParam, LPARAM lParam);
+ bool OnTouch(WPARAM wParam, LPARAM lParam);
+ bool OnHotKey(WPARAM wParam, LPARAM lParam);
+ bool OnPaint(uint32_t aNestingLevel);
+ void OnWindowPosChanging(WINDOWPOS* info);
+ void OnWindowPosChanged(WINDOWPOS* wp);
+ void OnSysColorChanged();
+ void OnDPIChanged(int32_t x, int32_t y, int32_t width, int32_t height);
+ bool OnPointerEvents(UINT msg, WPARAM wParam, LPARAM lParam);
+
+ /**
+ * Function that registers when the user has been active (used for detecting
+ * when the user is idle).
+ */
+ void UserActivity();
+
+ int32_t GetHeight(int32_t aProposedHeight);
+
+ DWORD WindowStyle();
+ DWORD WindowExStyle();
+
+ static const wchar_t* ChooseWindowClass(WindowType);
+ // This method registers the given window class, and returns the class name.
+ static const wchar_t* RegisterWindowClass(const wchar_t* aClassName,
+ UINT aExtraStyle, LPWSTR aIconID);
+
+ /**
+ * Popup hooks
+ */
+ static void ScheduleHookTimer(HWND aWnd, UINT aMsgId);
+ static void RegisterSpecialDropdownHooks();
+ static void UnregisterSpecialDropdownHooks();
+ static bool GetPopupsToRollup(
+ nsIRollupListener* aRollupListener, uint32_t* aPopupsToRollup,
+ mozilla::Maybe<POINT> aEventPoint = mozilla::Nothing());
+ static bool NeedsToHandleNCActivateDelayed(HWND aWnd);
+ static bool DealWithPopups(HWND inWnd, UINT inMsg, WPARAM inWParam,
+ LPARAM inLParam, LRESULT* outResult);
+
+ /**
+ * Window transparency helpers
+ */
+ void SetWindowTranslucencyInner(TransparencyMode aMode);
+ TransparencyMode GetWindowTranslucencyInner() const {
+ return mTransparencyMode;
+ }
+ bool IsSimulatedClientArea(int32_t clientX, int32_t clientY);
+ bool IsWindowButton(int32_t hitTestResult);
+
+ bool DispatchTouchEventFromWMPointer(UINT msg, LPARAM aLParam,
+ const WinPointerInfo& aPointerInfo,
+ mozilla::MouseButton aButton);
+
+ static bool IsAsyncResponseEvent(UINT aMsg, LRESULT& aResult);
+ void IPCWindowProcHandler(UINT& msg, WPARAM& wParam, LPARAM& lParam);
+
+ /**
+ * Misc.
+ */
+ void StopFlashing();
+ static HWND WindowAtMouse();
+ static bool IsTopLevelMouseExit(HWND aWnd);
+ LayoutDeviceIntRegion GetRegionToPaint(bool aForceFullRepaint, PAINTSTRUCT ps,
+ HDC aDC);
+ nsIWidgetListener* GetPaintListener();
+
+ void CreateCompositor() override;
+ void DestroyCompositor() override;
+ void RequestFxrOutput() override;
+
+ void RecreateDirectManipulationIfNeeded();
+ void ResizeDirectManipulationViewport();
+ void DestroyDirectManipulation();
+
+ bool NeedsToTrackWindowOcclusionState();
+
+ void AsyncUpdateWorkspaceID(Desktop& aDesktop);
+
+ // See bug 603793
+ static bool HasBogusPopupsDropShadowOnMultiMonitor();
+
+ static void InitMouseWheelScrollData();
+
+ void ChangedDPI();
+
+ static bool InitTouchInjection();
+
+ bool InjectTouchPoint(uint32_t aId, LayoutDeviceIntPoint& aPoint,
+ POINTER_FLAGS aFlags, uint32_t aPressure = 1024,
+ uint32_t aOrientation = 90);
+
+ void OnFullscreenChanged(nsSizeMode aOldSizeMode, bool aFullScreen);
+ void TryDwmResizeHack();
+
+ static void OnCloakEvent(HWND aWnd, bool aCloaked);
+ void OnCloakChanged(bool aCloaked);
+
+#ifdef DEBUG
+ virtual nsresult SetHiDPIMode(bool aHiDPI) override;
+ virtual nsresult RestoreHiDPIMode() override;
+#endif
+
+ // Get the orientation of the hidden taskbar, on the screen that this window
+ // is on, or Nothing if taskbar isn't hidden.
+ mozilla::Maybe<UINT> GetHiddenTaskbarEdge();
+
+ static bool sTouchInjectInitialized;
+ static InjectTouchInputPtr sInjectTouchFuncPtr;
+ static uint32_t sInstanceCount;
+ static nsWindow* sCurrentWindow;
+ static bool sIsOleInitialized;
+ static Cursor sCurrentCursor;
+ static bool sJustGotDeactivate;
+ static bool sJustGotActivate;
+ static bool sIsInMouseCapture;
+ static bool sIsRestoringSession;
+
+ // Message postponement hack. See the definition-site of
+ // WndProcUrgentInvocation::sDepth for details.
+ struct MOZ_STACK_CLASS WndProcUrgentInvocation {
+ struct Marker {
+ Marker() { ++sDepth; }
+ ~Marker() { --sDepth; }
+ };
+ inline static bool IsActive() { return sDepth > 0; }
+ static size_t sDepth;
+ };
+
+ // Hook Data Members for Dropdowns. sProcessHook Tells the
+ // hook methods whether they should be processing the hook
+ // messages.
+ static HHOOK sMsgFilterHook;
+ static HHOOK sCallProcHook;
+ static HHOOK sCallMouseHook;
+ static bool sProcessHook;
+ static UINT sRollupMsgId;
+ static HWND sRollupMsgWnd;
+ static UINT sHookTimerId;
+
+ // Used to prevent dispatching mouse events that do not originate from user
+ // input.
+ static POINT sLastMouseMovePoint;
+
+ nsClassHashtable<nsUint32HashKey, PointerInfo> mActivePointers;
+
+ // This is used by SynthesizeNativeTouchPoint to maintain state between
+ // multiple synthesized points, in the case where we can't call InjectTouch
+ // directly.
+ mozilla::UniquePtr<mozilla::MultiTouchInput> mSynthesizedTouchInput;
+
+ InputContext mInputContext;
+
+ nsCOMPtr<nsIWidget> mParent;
+ nsIntSize mLastSize = nsIntSize(0, 0);
+ nsIntPoint mLastPoint;
+ HWND mWnd = nullptr;
+ HWND mTransitionWnd = nullptr;
+ mozilla::Maybe<WNDPROC> mPrevWndProc;
+ HBRUSH mBrush;
+ IMEContext mDefaultIMC;
+ HDEVNOTIFY mDeviceNotifyHandle = nullptr;
+ bool mIsTopWidgetWindow = false;
+ bool mInDtor = false;
+ bool mIsVisible = false;
+ bool mIsCloaked = false;
+ bool mTouchWindow = false;
+ bool mDisplayPanFeedback = false;
+ bool mHideChrome = false;
+ bool mIsRTL;
+ bool mMousePresent = false;
+ bool mSimulatedClientArea = false;
+ bool mDestroyCalled = false;
+ bool mOpeningAnimationSuppressed;
+ bool mAlwaysOnTop;
+ bool mIsEarlyBlankWindow = false;
+ bool mIsShowingPreXULSkeletonUI = false;
+ bool mResizable = false;
+ // Whether we're an alert window. Alert windows don't have taskbar icons and
+ // don't steal focus from other windows when opened. They're also expected to
+ // be of type WindowType::Dialog.
+ bool mIsAlert = false;
+ bool mIsPerformingDwmFlushHack = false;
+ bool mDraggingWindowWithMouse = false;
+ DWORD_PTR mOldStyle = 0;
+ DWORD_PTR mOldExStyle = 0;
+ nsNativeDragTarget* mNativeDragTarget = nullptr;
+ HKL mLastKeyboardLayout = 0;
+ mozilla::CheckInvariantWrapper<FrameState> mFrameState;
+ WindowHook mWindowHook;
+ uint32_t mPickerDisplayCount = 0;
+ HICON mIconSmall = nullptr;
+ HICON mIconBig = nullptr;
+ HWND mLastKillFocusWindow = nullptr;
+ PlatformCompositorWidgetDelegate* mCompositorWidgetDelegate = nullptr;
+
+ LayoutDeviceIntMargin NonClientSizeMargin() const {
+ return NonClientSizeMargin(mNonClientOffset);
+ }
+ LayoutDeviceIntMargin NonClientSizeMargin(
+ const LayoutDeviceIntMargin& aNonClientOffset) const;
+ LayoutDeviceIntMargin NormalWindowNonClientOffset() const;
+
+ // Non-client margin settings
+ // Pre-calculated outward offset applied to default frames
+ LayoutDeviceIntMargin mNonClientOffset;
+ // Margins set by the owner
+ LayoutDeviceIntMargin mNonClientMargins;
+ // Margins we'd like to set once chrome is reshown:
+ LayoutDeviceIntMargin mFutureMarginsOnceChromeShows;
+ // Indicates we need to apply margins once toggling chrome into showing:
+ bool mFutureMarginsToUse = false;
+
+ // Indicates custom frames are enabled
+ bool mCustomNonClient = false;
+ // Indicates custom resize margins are in effect
+ bool mUseResizeMarginOverrides = false;
+ // Width of the left and right portions of the resize region
+ mozilla::LayoutDeviceIntCoord mHorResizeMargin;
+ // Height of the top and bottom portions of the resize region
+ mozilla::LayoutDeviceIntCoord mVertResizeMargin;
+ // Height of the caption plus border
+ mozilla::LayoutDeviceIntCoord mCaptionHeight;
+
+ // not yet set, will be calculated on first use
+ double mDefaultScale = -1.0;
+
+ // not yet set, will be calculated on first use
+ float mAspectRatio = 0.0;
+
+ nsCOMPtr<nsIUserIdleServiceInternal> mIdleService;
+
+ // Draggable titlebar region maintained by UpdateWindowDraggingRegion
+ LayoutDeviceIntRegion mDraggableRegion;
+
+ // Graphics
+ LayoutDeviceIntRect mLastPaintBounds;
+
+ ResizeState mResizeState = NOT_RESIZING;
+
+ // Transparency
+ TransparencyMode mTransparencyMode = TransparencyMode::Opaque;
+ nsIntRegion mPossiblyTransparentRegion;
+
+ // Win7 Gesture processing and management
+ nsWinGesture mGesture;
+
+ // Weak ref to the nsITaskbarWindowPreview associated with this window
+ nsWeakPtr mTaskbarPreview = nullptr;
+
+ // The input region that determines whether mouse events should be ignored
+ // and pass through to the window below. This is currently only used for
+ // popups.
+ InputRegion mInputRegion;
+
+ // True if the taskbar (possibly through the tab preview) tells us that the
+ // icon has been created on the taskbar.
+ bool mHasTaskbarIconBeenCreated = false;
+
+ // Whether we're in the process of sending a WM_SETTEXT ourselves
+ bool mSendingSetText = false;
+
+ // Whether we were created as a child window (aka ChildWindow) or not.
+ bool mIsChildWindow : 1;
+
+ int32_t mCachedHitTestResult = 0;
+
+ // The point in time at which the last paint completed. We use this to avoid
+ // painting too rapidly in response to frequent input events.
+ TimeStamp mLastPaintEndTime;
+
+ // Caching for hit test results (in client coordinates)
+ LayoutDeviceIntPoint mCachedHitTestPoint;
+ TimeStamp mCachedHitTestTime;
+
+ RefPtr<mozilla::widget::InProcessWinCompositorWidget> mBasicLayersSurface;
+
+ double mSizeConstraintsScale; // scale in effect when setting constraints
+
+ // Will be calculated when layer manager is created.
+ int32_t mMaxTextureSize = -1;
+
+ // Pointer events processing and management
+ WinPointerEvents mPointerEvents;
+
+ ScreenPoint mLastPanGestureFocus;
+
+ // When true, used to indicate an async call to RequestFxrOutput to the GPU
+ // process after the Compositor is created
+ bool mRequestFxrOutputPending = false;
+
+ // A stack based class used in DispatchMouseEvent() to tell whether we should
+ // NOT open context menu when we receives WM_CONTEXTMENU after the
+ // DispatchMouseEvent calls.
+ // This class now works only in the case where a mouse up event happened in
+ // the overscroll gutter.
+ class MOZ_STACK_CLASS ContextMenuPreventer final {
+ public:
+ explicit ContextMenuPreventer(nsWindow* aWindow)
+ : mWindow(aWindow), mNeedsToPreventContextMenu(false){};
+ ~ContextMenuPreventer() {
+ mWindow->mNeedsToPreventContextMenu = mNeedsToPreventContextMenu;
+ }
+ void Update(const mozilla::WidgetMouseEvent& aEvent,
+ const nsIWidget::ContentAndAPZEventStatus& aEventStatus);
+
+ private:
+ nsWindow* mWindow;
+ bool mNeedsToPreventContextMenu = false;
+ };
+ friend class ContextMenuPreventer;
+ bool mNeedsToPreventContextMenu = false;
+
+ mozilla::UniquePtr<mozilla::widget::DirectManipulationOwner> mDmOwner;
+
+ // Client rect for minimize, maximize and close buttons.
+ mozilla::EnumeratedArray<WindowButtonType, WindowButtonType::Count,
+ LayoutDeviceIntRect>
+ mWindowBtnRect;
+
+ mozilla::DataMutex<Desktop> mDesktopId;
+
+ // If set, indicates the edge of the NC region we should clear to black
+ // on next paint. One of: ABE_TOP, ABE_BOTTOM, ABE_LEFT or ABE_RIGHT.
+ mozilla::Maybe<UINT> mClearNCEdge;
+
+ friend class nsWindowGfx;
+
+ static constexpr int kHiddenTaskbarSize = 2;
+};
+
+#endif // WIDGET_WINDOWS_NSWINDOW_H_
diff --git a/widget/windows/nsWindowDbg.cpp b/widget/windows/nsWindowDbg.cpp
new file mode 100644
index 0000000000..8125a8532b
--- /dev/null
+++ b/widget/windows/nsWindowDbg.cpp
@@ -0,0 +1,1612 @@
+/* -*- 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/. */
+
+/*
+ * nsWindowDbg - Debug related utilities for nsWindow.
+ */
+
+#include "nsWindowDbg.h"
+#include "nsToolkit.h"
+#include "WinPointerEvents.h"
+#include "nsWindowLoggedMessages.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "nsWindow.h"
+#include "GeckoProfiler.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Document.h"
+
+#include <winuser.h>
+#include <dbt.h>
+#include <imm.h>
+#include <tpcshrd.h>
+
+#include <unordered_set>
+
+using namespace mozilla;
+using namespace mozilla::widget;
+extern mozilla::LazyLogModule gWindowsLog;
+static mozilla::LazyLogModule gWindowsEventLog("WindowsEvent");
+
+// currently defined in widget/windows/nsAppShell.cpp
+extern UINT sAppShellGeckoMsgId;
+
+#if defined(POPUP_ROLLUP_DEBUG_OUTPUT)
+MSGFEventMsgInfo gMSGFEvents[] = {
+ "MSGF_DIALOGBOX", 0, "MSGF_MESSAGEBOX", 1, "MSGF_MENU", 2,
+ "MSGF_SCROLLBAR", 5, "MSGF_NEXTWINDOW", 6, "MSGF_MAX", 8,
+ "MSGF_USER", 4096, nullptr, 0};
+#endif
+
+static long gEventCounter = 0;
+static UINT gLastEventMsg = 0;
+
+namespace geckoprofiler::markers {
+
+struct WindowProcMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("WindowProc");
+ }
+ static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter,
+ const ProfilerString8View& aMsgLoopName,
+ UINT aMsg, WPARAM aWParam, LPARAM aLParam) {
+ aWriter.StringProperty("messageLoop", aMsgLoopName);
+ aWriter.IntProperty("uMsg", aMsg);
+ const char* name;
+ if (aMsg < WM_USER) {
+ const auto eventMsgInfo = mozilla::widget::gAllEvents.find(aMsg);
+ if (eventMsgInfo != mozilla::widget::gAllEvents.end()) {
+ name = eventMsgInfo->second.mStr;
+ } else {
+ name = "ui message";
+ }
+ } else if (aMsg >= WM_USER && aMsg < WM_APP) {
+ name = "WM_USER message";
+ } else if (aMsg >= WM_APP && aMsg < 0xC000) {
+ name = "WM_APP message";
+ } else if (aMsg >= 0xC000 && aMsg < 0x10000) {
+ if (aMsg == sAppShellGeckoMsgId) {
+ name = "nsAppShell:EventID";
+ } else {
+ name = "registered Windows message";
+ }
+ } else {
+ name = "system message";
+ }
+ aWriter.StringProperty("name", MakeStringSpan(name));
+
+ if (aWParam) {
+ aWriter.IntProperty("wParam", aWParam);
+ }
+ if (aLParam) {
+ aWriter.IntProperty("lParam", aLParam);
+ }
+ }
+
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ schema.AddKeyFormat("uMsg", MS::Format::Integer);
+ schema.SetChartLabel(
+ "{marker.data.messageLoop} | {marker.data.name} ({marker.data.uMsg})");
+ schema.SetTableLabel(
+ "{marker.name} - {marker.data.messageLoop} - {marker.data.name} "
+ "({marker.data.uMsg})");
+ schema.SetTooltipLabel(
+ "{marker.data.messageLoop} - {marker.name} - {marker.data.name}");
+ schema.AddKeyFormat("wParam", MS::Format::Integer);
+ schema.AddKeyFormat("lParam", MS::Format::Integer);
+ return schema;
+ }
+};
+
+} // namespace geckoprofiler::markers
+
+namespace mozilla::widget {
+
+AutoProfilerMessageMarker::AutoProfilerMessageMarker(
+ Span<const char> aMsgLoopName, HWND hWnd, UINT msg, WPARAM wParam,
+ LPARAM lParam)
+ : mMsgLoopName(aMsgLoopName), mMsg(msg), mWParam(wParam), mLParam(lParam) {
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ mOptions.emplace(MarkerOptions(MarkerTiming::IntervalStart()));
+ nsWindow* win = WinUtils::GetNSWindowPtr(hWnd);
+ if (win) {
+ nsIWidgetListener* wl = win->GetWidgetListener();
+ if (wl) {
+ PresShell* presShell = wl->GetPresShell();
+ if (presShell) {
+ dom::Document* doc = presShell->GetDocument();
+ if (doc) {
+ mOptions->Set(MarkerInnerWindowId(doc->InnerWindowID()));
+ }
+ }
+ }
+ }
+ }
+}
+
+AutoProfilerMessageMarker::~AutoProfilerMessageMarker() {
+ if (!profiler_thread_is_being_profiled_for_markers()) {
+ return;
+ }
+
+ if (mOptions) {
+ mOptions->TimingRef().SetIntervalEnd();
+ } else {
+ mOptions.emplace(MarkerOptions(MarkerTiming::IntervalEnd()));
+ }
+ profiler_add_marker(
+ "WindowProc", ::mozilla::baseprofiler::category::OTHER,
+ std::move(*mOptions), geckoprofiler::markers::WindowProcMarker{},
+ ProfilerString8View::WrapNullTerminatedString(mMsgLoopName.data()), mMsg,
+ mWParam, mLParam);
+}
+
+// Using an unordered_set so we can initialize this with nice syntax instead of
+// having to add them one at a time to a mozilla::HashSet.
+std::unordered_set<UINT> gEventsToLogOriginalParams = {
+ WM_WINDOWPOSCHANGING, // (dummy comments for clang-format)
+ WM_SIZING, //
+ WM_STYLECHANGING,
+ WM_GETTEXT,
+ WM_GETMINMAXINFO,
+ WM_MEASUREITEM,
+ WM_NCCALCSIZE,
+};
+
+// If you add an event here, you must add cases for these to
+// MakeMessageSpecificData() and AppendFriendlyMessageSpecificData()
+// in nsWindowLoggedMessages.cpp.
+std::unordered_set<UINT> gEventsToRecordInAboutPage = {
+ WM_WINDOWPOSCHANGING, // (dummy comments for clang-format)
+ WM_WINDOWPOSCHANGED, //
+ WM_SIZING,
+ WM_SIZE,
+ WM_DPICHANGED,
+ WM_SETTINGCHANGE,
+ WM_NCCALCSIZE,
+ WM_MOVE,
+ WM_MOVING,
+ WM_GETMINMAXINFO,
+};
+
+NativeEventLogger::NativeEventLogger(Span<const char> aMsgLoopName, HWND hwnd,
+ UINT msg, WPARAM wParam, LPARAM lParam)
+ : mProfilerMarker(aMsgLoopName, hwnd, msg, wParam, lParam),
+ mMsgLoopName(aMsgLoopName.data()),
+ mHwnd(hwnd),
+ mMsg(msg),
+ mWParam(wParam),
+ mLParam(lParam),
+ mResult(mozilla::Nothing()),
+ mShouldLogPostCall(false) {
+ if (NativeEventLoggerInternal()) {
+ // this event was logged, so reserve this counter number for the post-call
+ mEventCounter = mozilla::Some(gEventCounter);
+ ++gEventCounter;
+ }
+}
+
+NativeEventLogger::~NativeEventLogger() {
+ // If mResult is Nothing, perhaps an exception was thrown or something
+ // before SetResult() was supposed to be called.
+ if (mResult.isSome()) {
+ if (NativeEventLoggerInternal() && mEventCounter.isNothing()) {
+ // We didn't reserve a counter in the pre-call, so reserve it here.
+ ++gEventCounter;
+ }
+ }
+ if (mMsg == WM_DESTROY) {
+ // Remove any logged messages for this window.
+ WindowClosed(mHwnd);
+ }
+}
+
+void EventMsgInfo::LogParameters(nsCString& str, WPARAM wParam, LPARAM lParam,
+ bool isPreCall) {
+ if (mParamInfoFn) {
+ str = mParamInfoFn(wParam, lParam, isPreCall);
+ } else {
+ if (mWParamInfoFn) {
+ mWParamInfoFn(str, wParam, mWParamName, isPreCall);
+ }
+ if (mLParamInfoFn) {
+ if (mWParamInfoFn) {
+ str.AppendASCII(" ");
+ }
+ mLParamInfoFn(str, lParam, mLParamName, isPreCall);
+ }
+ }
+}
+
+nsAutoCString DefaultParamInfo(uint64_t wParam, uint64_t lParam,
+ bool /* isPreCall */) {
+ nsAutoCString result;
+ result.AppendPrintf("wParam=0x%08llX lParam=0x%08llX", wParam, lParam);
+ return result;
+}
+
+void AppendEnumValueInfo(
+ nsCString& str, uint64_t value,
+ const std::unordered_map<uint64_t, const char*>& valuesAndNames,
+ const char* name) {
+ if (name != nullptr) {
+ str.AppendPrintf("%s=", name);
+ }
+ auto entry = valuesAndNames.find(value);
+ if (entry == valuesAndNames.end()) {
+ str.AppendPrintf("Unknown (0x%08llX)", value);
+ } else {
+ str.AppendASCII(entry->second);
+ }
+}
+
+bool AppendFlagsInfo(nsCString& str, uint64_t flags,
+ const nsTArray<EnumValueAndName>& flagsAndNames,
+ const char* name) {
+ if (name != nullptr) {
+ str.AppendPrintf("%s=", name);
+ }
+ bool firstAppend = true;
+ for (const EnumValueAndName& flagAndName : flagsAndNames) {
+ if (MOZ_UNLIKELY(flagAndName.mFlag == 0)) {
+ // Special case - only want to write this if nothing else was set.
+ // For this to make sense, 0 values should come at the end of
+ // flagsAndNames.
+ if (flags == 0 && firstAppend) {
+ firstAppend = false;
+ str.AppendASCII(flagAndName.mName);
+ }
+ } else if ((flags & flagAndName.mFlag) == flagAndName.mFlag) {
+ if (MOZ_LIKELY(!firstAppend)) {
+ str.Append('|');
+ }
+ firstAppend = false;
+ str.AppendASCII(flagAndName.mName);
+ flags = flags & ~flagAndName.mFlag;
+ }
+ }
+ if (flags != 0) {
+ if (MOZ_LIKELY(!firstAppend)) {
+ str.Append('|');
+ }
+ firstAppend = false;
+ str.AppendPrintf("Unknown (0x%08llX)", flags);
+ }
+ return !firstAppend;
+}
+
+// if mResult is not set, this is used to log the parameters passed in the
+// message, otherwise we are logging the parameters after we have handled the
+// message. This is useful for events where we might change the parameters while
+// handling the message (for example WM_GETTEXT and WM_NCCALCSIZE)
+// Returns whether this message was logged, so we need to reserve a
+// counter number for it.
+bool NativeEventLogger::NativeEventLoggerInternal() {
+ mozilla::LogLevel const targetLogLevel = [&] {
+ // These messages often take up more than 90% of logs if not filtered out.
+ if (mMsg == WM_SETCURSOR || mMsg == WM_MOUSEMOVE || mMsg == WM_NCHITTEST) {
+ return LogLevel::Verbose;
+ }
+ if (gLastEventMsg == mMsg) {
+ return LogLevel::Debug;
+ }
+ return LogLevel::Info;
+ }();
+
+ bool isPreCall = mResult.isNothing();
+ if (isPreCall || mShouldLogPostCall) {
+ bool recordInAboutPage = gEventsToRecordInAboutPage.find(mMsg) !=
+ gEventsToRecordInAboutPage.end();
+ bool writeToWindowsLog;
+ if (isPreCall) {
+ writeToWindowsLog = MOZ_LOG_TEST(gWindowsEventLog, targetLogLevel);
+ bool shouldLogAtAll = recordInAboutPage || writeToWindowsLog;
+ // Since calling mParamInfoFn() allocates a string, only go down this code
+ // path if we're going to log this message to reduce allocations.
+ if (!shouldLogAtAll) {
+ return false;
+ }
+ mShouldLogPostCall = true;
+ bool shouldLogPreCall = gEventsToLogOriginalParams.find(mMsg) !=
+ gEventsToLogOriginalParams.end();
+ if (!shouldLogPreCall) {
+ // Pre-call and we don't want to log both, so skip this one.
+ return false;
+ }
+ } else {
+ writeToWindowsLog = true;
+ }
+ if (recordInAboutPage) {
+ LogWindowMessage(mHwnd, mMsg, isPreCall,
+ mEventCounter.valueOr(gEventCounter), mWParam, mLParam,
+ mResult, mRetValue);
+ }
+ gLastEventMsg = mMsg;
+ if (writeToWindowsLog) {
+ const auto& eventMsgInfo = gAllEvents.find(mMsg);
+ const char* msgText = eventMsgInfo != gAllEvents.end()
+ ? eventMsgInfo->second.mStr
+ : nullptr;
+ nsAutoCString paramInfo;
+ if (eventMsgInfo != gAllEvents.end()) {
+ eventMsgInfo->second.LogParameters(paramInfo, mWParam, mLParam,
+ isPreCall);
+ } else {
+ paramInfo = DefaultParamInfo(mWParam, mLParam, isPreCall);
+ }
+ const char* resultMsg = mResult.isSome()
+ ? (mResult.value() ? "true" : "false")
+ : "initial call";
+ nsAutoCString logMessage;
+ logMessage.AppendPrintf(
+ "%s | %6ld %08" PRIX64 " - 0x%04X %s%s%s: 0x%08" PRIX64 " (%s)\n",
+ mMsgLoopName, mEventCounter.valueOr(gEventCounter),
+ reinterpret_cast<uint64_t>(mHwnd), mMsg,
+ msgText ? msgText : "Unknown", paramInfo.IsEmpty() ? "" : " ",
+ paramInfo.get(),
+ mResult.isSome() ? static_cast<uint64_t>(mRetValue) : 0, resultMsg);
+ const char* logMessageData = logMessage.Data();
+ MOZ_LOG(gWindowsEventLog, targetLogLevel, ("%s", logMessageData));
+ }
+ return true;
+ }
+ return false;
+}
+
+void TrueFalseParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ result.AppendPrintf("%s=%s", name, value == TRUE ? "TRUE" : "FALSE");
+}
+
+void TrueFalseLowOrderWordParamInfo(nsCString& result, uint64_t value,
+ const char* name, bool /* isPreCall */) {
+ result.AppendPrintf("%s=%s", name, LOWORD(value) == TRUE ? "TRUE" : "FALSE");
+}
+
+void HexParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ result.AppendPrintf("%s=0x%08llX", name, value);
+}
+
+void IntParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ result.AppendPrintf("%s=%lld", name, value);
+}
+
+void RectParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ LPRECT rect = reinterpret_cast<LPRECT>(value);
+ if (rect == nullptr) {
+ str.AppendPrintf("NULL rect?");
+ return;
+ }
+ if (name != nullptr) {
+ str.AppendPrintf("%s ", name);
+ }
+ str.AppendPrintf("left=%ld top=%ld right=%ld bottom=%ld", rect->left,
+ rect->top, rect->right, rect->bottom);
+}
+
+#define VALANDNAME_ENTRY(_msg) \
+ { _msg, #_msg }
+
+void CreateStructParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ CREATESTRUCT* createStruct = reinterpret_cast<CREATESTRUCT*>(value);
+ if (createStruct == nullptr) {
+ str.AppendASCII("NULL createStruct?");
+ return;
+ }
+ str.AppendPrintf(
+ "%s: hInstance=%p hMenu=%p hwndParent=%p lpszName=%S lpszClass=%S x=%d "
+ "y=%d cx=%d cy=%d",
+ name, createStruct->hInstance, createStruct->hMenu,
+ createStruct->hwndParent, createStruct->lpszName, createStruct->lpszClass,
+ createStruct->x, createStruct->y, createStruct->cx, createStruct->cy);
+ str.AppendASCII(" ");
+ const static nsTArray<EnumValueAndName> windowStyles = {
+ // these combinations of other flags need to come first
+ VALANDNAME_ENTRY(WS_OVERLAPPEDWINDOW), VALANDNAME_ENTRY(WS_POPUPWINDOW),
+ VALANDNAME_ENTRY(WS_CAPTION),
+ // regular flags
+ VALANDNAME_ENTRY(WS_POPUP), VALANDNAME_ENTRY(WS_CHILD),
+ VALANDNAME_ENTRY(WS_MINIMIZE), VALANDNAME_ENTRY(WS_VISIBLE),
+ VALANDNAME_ENTRY(WS_DISABLED), VALANDNAME_ENTRY(WS_CLIPSIBLINGS),
+ VALANDNAME_ENTRY(WS_CLIPCHILDREN), VALANDNAME_ENTRY(WS_MAXIMIZE),
+ VALANDNAME_ENTRY(WS_BORDER), VALANDNAME_ENTRY(WS_DLGFRAME),
+ VALANDNAME_ENTRY(WS_VSCROLL), VALANDNAME_ENTRY(WS_HSCROLL),
+ VALANDNAME_ENTRY(WS_SYSMENU), VALANDNAME_ENTRY(WS_THICKFRAME),
+ VALANDNAME_ENTRY(WS_GROUP), VALANDNAME_ENTRY(WS_TABSTOP),
+ // zero value needs to come last
+ VALANDNAME_ENTRY(WS_OVERLAPPED)};
+ AppendFlagsInfo(str, createStruct->style, windowStyles, "style");
+ str.AppendASCII(" ");
+ const nsTArray<EnumValueAndName> extendedWindowStyles = {
+ // these combinations of other flags need to come first
+ VALANDNAME_ENTRY(WS_EX_OVERLAPPEDWINDOW),
+ VALANDNAME_ENTRY(WS_EX_PALETTEWINDOW),
+ // regular flags
+ VALANDNAME_ENTRY(WS_EX_DLGMODALFRAME),
+ VALANDNAME_ENTRY(WS_EX_NOPARENTNOTIFY),
+ VALANDNAME_ENTRY(WS_EX_TOPMOST),
+ VALANDNAME_ENTRY(WS_EX_ACCEPTFILES),
+ VALANDNAME_ENTRY(WS_EX_TRANSPARENT),
+ VALANDNAME_ENTRY(WS_EX_MDICHILD),
+ VALANDNAME_ENTRY(WS_EX_TOOLWINDOW),
+ VALANDNAME_ENTRY(WS_EX_WINDOWEDGE),
+ VALANDNAME_ENTRY(WS_EX_CLIENTEDGE),
+ VALANDNAME_ENTRY(WS_EX_CONTEXTHELP),
+ VALANDNAME_ENTRY(WS_EX_RIGHT),
+ VALANDNAME_ENTRY(WS_EX_LEFT),
+ VALANDNAME_ENTRY(WS_EX_RTLREADING),
+ VALANDNAME_ENTRY(WS_EX_LTRREADING),
+ VALANDNAME_ENTRY(WS_EX_LEFTSCROLLBAR),
+ VALANDNAME_ENTRY(WS_EX_RIGHTSCROLLBAR),
+ VALANDNAME_ENTRY(WS_EX_CONTROLPARENT),
+ VALANDNAME_ENTRY(WS_EX_STATICEDGE),
+ VALANDNAME_ENTRY(WS_EX_APPWINDOW),
+ VALANDNAME_ENTRY(WS_EX_LAYERED),
+ VALANDNAME_ENTRY(WS_EX_NOINHERITLAYOUT),
+ VALANDNAME_ENTRY(WS_EX_LAYOUTRTL),
+ VALANDNAME_ENTRY(WS_EX_NOACTIVATE),
+ VALANDNAME_ENTRY(WS_EX_COMPOSITED),
+ VALANDNAME_ENTRY(WS_EX_NOREDIRECTIONBITMAP),
+ };
+ AppendFlagsInfo(str, createStruct->dwExStyle, extendedWindowStyles,
+ "dwExStyle");
+}
+
+void XLowWordYHighWordParamInfo(nsCString& str, uint64_t value,
+ const char* name, bool /* isPreCall */) {
+ str.AppendPrintf("%s: x=%d y=%d", name, static_cast<int>(LOWORD(value)),
+ static_cast<int>(HIWORD(value)));
+}
+
+void PointParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ str.AppendPrintf("%s: x=%d y=%d", name, static_cast<int>(GET_X_LPARAM(value)),
+ static_cast<int>(GET_Y_LPARAM(value)));
+}
+
+void PointExplicitParamInfo(nsCString& str, POINT point, const char* name) {
+ str.AppendPrintf("%s: x=%ld y=%ld", name, point.x, point.y);
+}
+
+void PointsParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ PPOINTS points = reinterpret_cast<PPOINTS>(&value);
+ str.AppendPrintf("%s: x=%d y=%d", name, points->x, points->y);
+}
+
+void VirtualKeyParamInfo(nsCString& result, uint64_t param, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> virtualKeys{
+ VALANDNAME_ENTRY(VK_LBUTTON),
+ VALANDNAME_ENTRY(VK_RBUTTON),
+ VALANDNAME_ENTRY(VK_CANCEL),
+ VALANDNAME_ENTRY(VK_MBUTTON),
+ VALANDNAME_ENTRY(VK_XBUTTON1),
+ VALANDNAME_ENTRY(VK_XBUTTON2),
+ VALANDNAME_ENTRY(VK_BACK),
+ VALANDNAME_ENTRY(VK_TAB),
+ VALANDNAME_ENTRY(VK_CLEAR),
+ VALANDNAME_ENTRY(VK_RETURN),
+ VALANDNAME_ENTRY(VK_SHIFT),
+ VALANDNAME_ENTRY(VK_CONTROL),
+ VALANDNAME_ENTRY(VK_MENU),
+ VALANDNAME_ENTRY(VK_PAUSE),
+ VALANDNAME_ENTRY(VK_CAPITAL),
+ VALANDNAME_ENTRY(VK_KANA),
+ VALANDNAME_ENTRY(VK_HANGUL),
+#ifdef VK_IME_ON
+ VALANDNAME_ENTRY(VK_IME_ON),
+#endif
+ VALANDNAME_ENTRY(VK_JUNJA),
+ VALANDNAME_ENTRY(VK_FINAL),
+ VALANDNAME_ENTRY(VK_HANJA),
+ VALANDNAME_ENTRY(VK_KANJI),
+#ifdef VK_IME_OFF
+ VALANDNAME_ENTRY(VK_IME_OFF),
+#endif
+ VALANDNAME_ENTRY(VK_ESCAPE),
+ VALANDNAME_ENTRY(VK_CONVERT),
+ VALANDNAME_ENTRY(VK_NONCONVERT),
+ VALANDNAME_ENTRY(VK_ACCEPT),
+ VALANDNAME_ENTRY(VK_MODECHANGE),
+ VALANDNAME_ENTRY(VK_SPACE),
+ VALANDNAME_ENTRY(VK_PRIOR),
+ VALANDNAME_ENTRY(VK_NEXT),
+ VALANDNAME_ENTRY(VK_END),
+ VALANDNAME_ENTRY(VK_HOME),
+ VALANDNAME_ENTRY(VK_LEFT),
+ VALANDNAME_ENTRY(VK_UP),
+ VALANDNAME_ENTRY(VK_RIGHT),
+ VALANDNAME_ENTRY(VK_DOWN),
+ VALANDNAME_ENTRY(VK_SELECT),
+ VALANDNAME_ENTRY(VK_PRINT),
+ VALANDNAME_ENTRY(VK_EXECUTE),
+ VALANDNAME_ENTRY(VK_SNAPSHOT),
+ VALANDNAME_ENTRY(VK_INSERT),
+ VALANDNAME_ENTRY(VK_DELETE),
+ VALANDNAME_ENTRY(VK_HELP),
+ VALANDNAME_ENTRY(VK_LWIN),
+ VALANDNAME_ENTRY(VK_RWIN),
+ VALANDNAME_ENTRY(VK_APPS),
+ VALANDNAME_ENTRY(VK_SLEEP),
+ VALANDNAME_ENTRY(VK_NUMPAD0),
+ VALANDNAME_ENTRY(VK_NUMPAD1),
+ VALANDNAME_ENTRY(VK_NUMPAD2),
+ VALANDNAME_ENTRY(VK_NUMPAD3),
+ VALANDNAME_ENTRY(VK_NUMPAD4),
+ VALANDNAME_ENTRY(VK_NUMPAD5),
+ VALANDNAME_ENTRY(VK_NUMPAD6),
+ VALANDNAME_ENTRY(VK_NUMPAD7),
+ VALANDNAME_ENTRY(VK_NUMPAD8),
+ VALANDNAME_ENTRY(VK_NUMPAD9),
+ VALANDNAME_ENTRY(VK_MULTIPLY),
+ VALANDNAME_ENTRY(VK_ADD),
+ VALANDNAME_ENTRY(VK_SEPARATOR),
+ VALANDNAME_ENTRY(VK_SUBTRACT),
+ VALANDNAME_ENTRY(VK_DECIMAL),
+ VALANDNAME_ENTRY(VK_DIVIDE),
+ VALANDNAME_ENTRY(VK_F1),
+ VALANDNAME_ENTRY(VK_F2),
+ VALANDNAME_ENTRY(VK_F3),
+ VALANDNAME_ENTRY(VK_F4),
+ VALANDNAME_ENTRY(VK_F5),
+ VALANDNAME_ENTRY(VK_F6),
+ VALANDNAME_ENTRY(VK_F7),
+ VALANDNAME_ENTRY(VK_F8),
+ VALANDNAME_ENTRY(VK_F9),
+ VALANDNAME_ENTRY(VK_F10),
+ VALANDNAME_ENTRY(VK_F11),
+ VALANDNAME_ENTRY(VK_F12),
+ VALANDNAME_ENTRY(VK_F13),
+ VALANDNAME_ENTRY(VK_F14),
+ VALANDNAME_ENTRY(VK_F15),
+ VALANDNAME_ENTRY(VK_F16),
+ VALANDNAME_ENTRY(VK_F17),
+ VALANDNAME_ENTRY(VK_F18),
+ VALANDNAME_ENTRY(VK_F19),
+ VALANDNAME_ENTRY(VK_F20),
+ VALANDNAME_ENTRY(VK_F21),
+ VALANDNAME_ENTRY(VK_F22),
+ VALANDNAME_ENTRY(VK_F23),
+ VALANDNAME_ENTRY(VK_F24),
+ VALANDNAME_ENTRY(VK_NUMLOCK),
+ VALANDNAME_ENTRY(VK_SCROLL),
+ VALANDNAME_ENTRY(VK_LSHIFT),
+ VALANDNAME_ENTRY(VK_RSHIFT),
+ VALANDNAME_ENTRY(VK_LCONTROL),
+ VALANDNAME_ENTRY(VK_RCONTROL),
+ VALANDNAME_ENTRY(VK_LMENU),
+ VALANDNAME_ENTRY(VK_RMENU),
+ VALANDNAME_ENTRY(VK_BROWSER_BACK),
+ VALANDNAME_ENTRY(VK_BROWSER_FORWARD),
+ VALANDNAME_ENTRY(VK_BROWSER_REFRESH),
+ VALANDNAME_ENTRY(VK_BROWSER_STOP),
+ VALANDNAME_ENTRY(VK_BROWSER_SEARCH),
+ VALANDNAME_ENTRY(VK_BROWSER_FAVORITES),
+ VALANDNAME_ENTRY(VK_BROWSER_HOME),
+ VALANDNAME_ENTRY(VK_VOLUME_MUTE),
+ VALANDNAME_ENTRY(VK_VOLUME_DOWN),
+ VALANDNAME_ENTRY(VK_VOLUME_UP),
+ VALANDNAME_ENTRY(VK_MEDIA_NEXT_TRACK),
+ VALANDNAME_ENTRY(VK_MEDIA_PREV_TRACK),
+ VALANDNAME_ENTRY(VK_MEDIA_STOP),
+ VALANDNAME_ENTRY(VK_MEDIA_PLAY_PAUSE),
+ VALANDNAME_ENTRY(VK_LAUNCH_MAIL),
+ VALANDNAME_ENTRY(VK_LAUNCH_MEDIA_SELECT),
+ VALANDNAME_ENTRY(VK_LAUNCH_APP1),
+ VALANDNAME_ENTRY(VK_LAUNCH_APP2),
+ VALANDNAME_ENTRY(VK_OEM_1),
+ VALANDNAME_ENTRY(VK_OEM_PLUS),
+ VALANDNAME_ENTRY(VK_OEM_COMMA),
+ VALANDNAME_ENTRY(VK_OEM_MINUS),
+ VALANDNAME_ENTRY(VK_OEM_PERIOD),
+ VALANDNAME_ENTRY(VK_OEM_2),
+ VALANDNAME_ENTRY(VK_OEM_3),
+ VALANDNAME_ENTRY(VK_OEM_4),
+ VALANDNAME_ENTRY(VK_OEM_5),
+ VALANDNAME_ENTRY(VK_OEM_6),
+ VALANDNAME_ENTRY(VK_OEM_7),
+ VALANDNAME_ENTRY(VK_OEM_8),
+ VALANDNAME_ENTRY(VK_OEM_102),
+ VALANDNAME_ENTRY(VK_PROCESSKEY),
+ VALANDNAME_ENTRY(VK_PACKET),
+ VALANDNAME_ENTRY(VK_ATTN),
+ VALANDNAME_ENTRY(VK_CRSEL),
+ VALANDNAME_ENTRY(VK_EXSEL),
+ VALANDNAME_ENTRY(VK_EREOF),
+ VALANDNAME_ENTRY(VK_PLAY),
+ VALANDNAME_ENTRY(VK_ZOOM),
+ VALANDNAME_ENTRY(VK_NONAME),
+ VALANDNAME_ENTRY(VK_PA1),
+ VALANDNAME_ENTRY(VK_OEM_CLEAR),
+ {0x30, "0"},
+ {0x31, "1"},
+ {0x32, "2"},
+ {0x33, "3"},
+ {0x34, "4"},
+ {0x35, "5"},
+ {0x36, "6"},
+ {0x37, "7"},
+ {0x38, "8"},
+ {0x39, "9"},
+ {0x41, "A"},
+ {0x42, "B"},
+ {0x43, "C"},
+ {0x44, "D"},
+ {0x45, "E"},
+ {0x46, "F"},
+ {0x47, "G"},
+ {0x48, "H"},
+ {0x49, "I"},
+ {0x4A, "J"},
+ {0x4B, "K"},
+ {0x4C, "L"},
+ {0x4D, "M"},
+ {0x4E, "N"},
+ {0x4F, "O"},
+ {0x50, "P"},
+ {0x51, "Q"},
+ {0x52, "S"},
+ {0x53, "T"},
+ {0x54, "U"},
+ {0x55, "V"},
+ {0x56, "W"},
+ {0x57, "X"},
+ {0x58, "Y"},
+ {0x59, "Z"},
+ };
+ AppendEnumValueInfo(result, param, virtualKeys, name);
+}
+
+void VirtualModifierKeysParamInfo(nsCString& result, uint64_t param,
+ const char* name, bool /* isPreCall */) {
+ const static nsTArray<EnumValueAndName> virtualKeys{
+ VALANDNAME_ENTRY(MK_CONTROL), VALANDNAME_ENTRY(MK_LBUTTON),
+ VALANDNAME_ENTRY(MK_MBUTTON), VALANDNAME_ENTRY(MK_RBUTTON),
+ VALANDNAME_ENTRY(MK_SHIFT), VALANDNAME_ENTRY(MK_XBUTTON1),
+ VALANDNAME_ENTRY(MK_XBUTTON2), {0, "(none)"}};
+ AppendFlagsInfo(result, param, virtualKeys, name);
+}
+
+void ParentNotifyEventParamInfo(nsCString& str, uint64_t param,
+ const char* /* name */, bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> eventValues{
+ VALANDNAME_ENTRY(WM_CREATE), VALANDNAME_ENTRY(WM_DESTROY),
+ VALANDNAME_ENTRY(WM_LBUTTONDOWN), VALANDNAME_ENTRY(WM_MBUTTONDOWN),
+ VALANDNAME_ENTRY(WM_RBUTTONDOWN), VALANDNAME_ENTRY(WM_XBUTTONDOWN),
+ VALANDNAME_ENTRY(WM_POINTERDOWN)};
+ AppendEnumValueInfo(str, LOWORD(param), eventValues, "event");
+ str.AppendASCII(" ");
+ HexParamInfo(str, HIWORD(param), "hiWord", false);
+}
+
+void KeystrokeFlagsParamInfo(nsCString& str, uint64_t param,
+ const char* /* name */, bool /* isPreCall */) {
+ WORD repeatCount = LOWORD(param);
+ WORD keyFlags = HIWORD(param);
+ WORD scanCode = LOBYTE(keyFlags);
+ bool isExtendedKey = (keyFlags & KF_EXTENDED) == KF_EXTENDED;
+ if (isExtendedKey) {
+ scanCode = MAKEWORD(scanCode, 0xE0);
+ }
+ bool contextCode = (keyFlags & KF_ALTDOWN) == KF_ALTDOWN;
+ bool wasKeyDown = (keyFlags & KF_REPEAT) == KF_REPEAT;
+ bool transitionState = (keyFlags & KF_UP) == KF_UP;
+
+ str.AppendPrintf(
+ "repeatCount: %d scanCode: %d isExtended: %d, contextCode: %d "
+ "previousKeyState: %d transitionState: %d",
+ repeatCount, scanCode, isExtendedKey ? 1 : 0, contextCode ? 1 : 0,
+ wasKeyDown ? 1 : 0, transitionState ? 1 : 0);
+};
+
+void VirtualKeysLowWordDistanceHighWordParamInfo(nsCString& str, uint64_t value,
+ const char* /* name */,
+ bool isPreCall) {
+ VirtualModifierKeysParamInfo(str, LOWORD(value), "virtualKeys", isPreCall);
+ str.AppendASCII(" ");
+ IntParamInfo(str, HIWORD(value), "distance", isPreCall);
+}
+
+void ShowWindowReasonParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> showWindowReasonValues{
+ VALANDNAME_ENTRY(SW_OTHERUNZOOM),
+ VALANDNAME_ENTRY(SW_OTHERZOOM),
+ VALANDNAME_ENTRY(SW_PARENTCLOSING),
+ VALANDNAME_ENTRY(SW_PARENTOPENING),
+ {0, "Call to ShowWindow()"}};
+ AppendEnumValueInfo(str, value, showWindowReasonValues, name);
+}
+
+void WindowEdgeParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> windowEdgeValues{
+ VALANDNAME_ENTRY(WMSZ_BOTTOM), VALANDNAME_ENTRY(WMSZ_BOTTOMLEFT),
+ VALANDNAME_ENTRY(WMSZ_BOTTOMRIGHT), VALANDNAME_ENTRY(WMSZ_LEFT),
+ VALANDNAME_ENTRY(WMSZ_RIGHT), VALANDNAME_ENTRY(WMSZ_TOP),
+ VALANDNAME_ENTRY(WMSZ_TOPLEFT), VALANDNAME_ENTRY(WMSZ_TOPRIGHT)};
+ AppendEnumValueInfo(str, value, windowEdgeValues, name);
+}
+
+void UiActionParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> uiActionValues{
+ VALANDNAME_ENTRY(SPI_GETACCESSTIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETAUDIODESCRIPTION),
+ VALANDNAME_ENTRY(SPI_GETCLIENTAREAANIMATION),
+ VALANDNAME_ENTRY(SPI_GETDISABLEOVERLAPPEDCONTENT),
+ VALANDNAME_ENTRY(SPI_GETFILTERKEYS),
+ VALANDNAME_ENTRY(SPI_GETFOCUSBORDERHEIGHT),
+ VALANDNAME_ENTRY(SPI_GETFOCUSBORDERWIDTH),
+ VALANDNAME_ENTRY(SPI_GETHIGHCONTRAST),
+ VALANDNAME_ENTRY(SPI_GETLOGICALDPIOVERRIDE),
+ VALANDNAME_ENTRY(SPI_SETLOGICALDPIOVERRIDE),
+ VALANDNAME_ENTRY(SPI_GETMESSAGEDURATION),
+ VALANDNAME_ENTRY(SPI_GETMOUSECLICKLOCK),
+ VALANDNAME_ENTRY(SPI_GETMOUSECLICKLOCKTIME),
+ VALANDNAME_ENTRY(SPI_GETMOUSEKEYS),
+ VALANDNAME_ENTRY(SPI_GETMOUSESONAR),
+ VALANDNAME_ENTRY(SPI_GETMOUSEVANISH),
+ VALANDNAME_ENTRY(SPI_GETSCREENREADER),
+ VALANDNAME_ENTRY(SPI_GETSERIALKEYS),
+ VALANDNAME_ENTRY(SPI_GETSHOWSOUNDS),
+ VALANDNAME_ENTRY(SPI_GETSOUNDSENTRY),
+ VALANDNAME_ENTRY(SPI_GETSTICKYKEYS),
+ VALANDNAME_ENTRY(SPI_GETTOGGLEKEYS),
+ VALANDNAME_ENTRY(SPI_SETACCESSTIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETAUDIODESCRIPTION),
+ VALANDNAME_ENTRY(SPI_SETCLIENTAREAANIMATION),
+ VALANDNAME_ENTRY(SPI_SETDISABLEOVERLAPPEDCONTENT),
+ VALANDNAME_ENTRY(SPI_SETFILTERKEYS),
+ VALANDNAME_ENTRY(SPI_SETFOCUSBORDERHEIGHT),
+ VALANDNAME_ENTRY(SPI_SETFOCUSBORDERWIDTH),
+ VALANDNAME_ENTRY(SPI_SETHIGHCONTRAST),
+ VALANDNAME_ENTRY(SPI_SETMESSAGEDURATION),
+ VALANDNAME_ENTRY(SPI_SETMOUSECLICKLOCK),
+ VALANDNAME_ENTRY(SPI_SETMOUSECLICKLOCKTIME),
+ VALANDNAME_ENTRY(SPI_SETMOUSEKEYS),
+ VALANDNAME_ENTRY(SPI_SETMOUSESONAR),
+ VALANDNAME_ENTRY(SPI_SETMOUSEVANISH),
+ VALANDNAME_ENTRY(SPI_SETSCREENREADER),
+ VALANDNAME_ENTRY(SPI_SETSERIALKEYS),
+ VALANDNAME_ENTRY(SPI_SETSHOWSOUNDS),
+ VALANDNAME_ENTRY(SPI_SETSOUNDSENTRY),
+ VALANDNAME_ENTRY(SPI_SETSTICKYKEYS),
+ VALANDNAME_ENTRY(SPI_SETTOGGLEKEYS),
+ VALANDNAME_ENTRY(SPI_GETCLEARTYPE),
+ VALANDNAME_ENTRY(SPI_GETDESKWALLPAPER),
+ VALANDNAME_ENTRY(SPI_GETDROPSHADOW),
+ VALANDNAME_ENTRY(SPI_GETFLATMENU),
+ VALANDNAME_ENTRY(SPI_GETFONTSMOOTHING),
+ VALANDNAME_ENTRY(SPI_GETFONTSMOOTHINGCONTRAST),
+ VALANDNAME_ENTRY(SPI_GETFONTSMOOTHINGORIENTATION),
+ VALANDNAME_ENTRY(SPI_GETFONTSMOOTHINGTYPE),
+ VALANDNAME_ENTRY(SPI_GETWORKAREA),
+ VALANDNAME_ENTRY(SPI_SETCLEARTYPE),
+ VALANDNAME_ENTRY(SPI_SETCURSORS),
+ VALANDNAME_ENTRY(SPI_SETDESKPATTERN),
+ VALANDNAME_ENTRY(SPI_SETDESKWALLPAPER),
+ VALANDNAME_ENTRY(SPI_SETDROPSHADOW),
+ VALANDNAME_ENTRY(SPI_SETFLATMENU),
+ VALANDNAME_ENTRY(SPI_SETFONTSMOOTHING),
+ VALANDNAME_ENTRY(SPI_SETFONTSMOOTHINGCONTRAST),
+ VALANDNAME_ENTRY(SPI_SETFONTSMOOTHINGORIENTATION),
+ VALANDNAME_ENTRY(SPI_SETFONTSMOOTHINGTYPE),
+ VALANDNAME_ENTRY(SPI_SETWORKAREA),
+ VALANDNAME_ENTRY(SPI_GETICONMETRICS),
+ VALANDNAME_ENTRY(SPI_GETICONTITLELOGFONT),
+ VALANDNAME_ENTRY(SPI_GETICONTITLEWRAP),
+ VALANDNAME_ENTRY(SPI_ICONHORIZONTALSPACING),
+ VALANDNAME_ENTRY(SPI_ICONVERTICALSPACING),
+ VALANDNAME_ENTRY(SPI_SETICONMETRICS),
+ VALANDNAME_ENTRY(SPI_SETICONS),
+ VALANDNAME_ENTRY(SPI_SETICONTITLELOGFONT),
+ VALANDNAME_ENTRY(SPI_SETICONTITLEWRAP),
+ VALANDNAME_ENTRY(SPI_GETBEEP),
+ VALANDNAME_ENTRY(SPI_GETBLOCKSENDINPUTRESETS),
+ VALANDNAME_ENTRY(SPI_GETCONTACTVISUALIZATION),
+ VALANDNAME_ENTRY(SPI_SETCONTACTVISUALIZATION),
+ VALANDNAME_ENTRY(SPI_GETDEFAULTINPUTLANG),
+ VALANDNAME_ENTRY(SPI_GETGESTUREVISUALIZATION),
+ VALANDNAME_ENTRY(SPI_SETGESTUREVISUALIZATION),
+ VALANDNAME_ENTRY(SPI_GETKEYBOARDCUES),
+ VALANDNAME_ENTRY(SPI_GETKEYBOARDDELAY),
+ VALANDNAME_ENTRY(SPI_GETKEYBOARDPREF),
+ VALANDNAME_ENTRY(SPI_GETKEYBOARDSPEED),
+ VALANDNAME_ENTRY(SPI_GETMOUSE),
+ VALANDNAME_ENTRY(SPI_GETMOUSEHOVERHEIGHT),
+ VALANDNAME_ENTRY(SPI_GETMOUSEHOVERTIME),
+ VALANDNAME_ENTRY(SPI_GETMOUSEHOVERWIDTH),
+ VALANDNAME_ENTRY(SPI_GETMOUSESPEED),
+ VALANDNAME_ENTRY(SPI_GETMOUSETRAILS),
+ VALANDNAME_ENTRY(SPI_GETMOUSEWHEELROUTING),
+ VALANDNAME_ENTRY(SPI_SETMOUSEWHEELROUTING),
+ VALANDNAME_ENTRY(SPI_GETPENVISUALIZATION),
+ VALANDNAME_ENTRY(SPI_SETPENVISUALIZATION),
+ VALANDNAME_ENTRY(SPI_GETSNAPTODEFBUTTON),
+ VALANDNAME_ENTRY(SPI_GETSYSTEMLANGUAGEBAR),
+ VALANDNAME_ENTRY(SPI_SETSYSTEMLANGUAGEBAR),
+ VALANDNAME_ENTRY(SPI_GETTHREADLOCALINPUTSETTINGS),
+ VALANDNAME_ENTRY(SPI_SETTHREADLOCALINPUTSETTINGS),
+ VALANDNAME_ENTRY(SPI_GETWHEELSCROLLCHARS),
+ VALANDNAME_ENTRY(SPI_GETWHEELSCROLLLINES),
+ VALANDNAME_ENTRY(SPI_SETBEEP),
+ VALANDNAME_ENTRY(SPI_SETBLOCKSENDINPUTRESETS),
+ VALANDNAME_ENTRY(SPI_SETDEFAULTINPUTLANG),
+ VALANDNAME_ENTRY(SPI_SETDOUBLECLICKTIME),
+ VALANDNAME_ENTRY(SPI_SETDOUBLECLKHEIGHT),
+ VALANDNAME_ENTRY(SPI_SETDOUBLECLKWIDTH),
+ VALANDNAME_ENTRY(SPI_SETKEYBOARDCUES),
+ VALANDNAME_ENTRY(SPI_SETKEYBOARDDELAY),
+ VALANDNAME_ENTRY(SPI_SETKEYBOARDPREF),
+ VALANDNAME_ENTRY(SPI_SETKEYBOARDSPEED),
+ VALANDNAME_ENTRY(SPI_SETLANGTOGGLE),
+ VALANDNAME_ENTRY(SPI_SETMOUSE),
+ VALANDNAME_ENTRY(SPI_SETMOUSEBUTTONSWAP),
+ VALANDNAME_ENTRY(SPI_SETMOUSEHOVERHEIGHT),
+ VALANDNAME_ENTRY(SPI_SETMOUSEHOVERTIME),
+ VALANDNAME_ENTRY(SPI_SETMOUSEHOVERWIDTH),
+ VALANDNAME_ENTRY(SPI_SETMOUSESPEED),
+ VALANDNAME_ENTRY(SPI_SETMOUSETRAILS),
+ VALANDNAME_ENTRY(SPI_SETSNAPTODEFBUTTON),
+ VALANDNAME_ENTRY(SPI_SETWHEELSCROLLCHARS),
+ VALANDNAME_ENTRY(SPI_SETWHEELSCROLLLINES),
+ VALANDNAME_ENTRY(SPI_GETMENUDROPALIGNMENT),
+ VALANDNAME_ENTRY(SPI_GETMENUFADE),
+ VALANDNAME_ENTRY(SPI_GETMENUSHOWDELAY),
+ VALANDNAME_ENTRY(SPI_SETMENUDROPALIGNMENT),
+ VALANDNAME_ENTRY(SPI_SETMENUFADE),
+ VALANDNAME_ENTRY(SPI_SETMENUSHOWDELAY),
+ VALANDNAME_ENTRY(SPI_GETLOWPOWERACTIVE),
+ VALANDNAME_ENTRY(SPI_GETLOWPOWERTIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETPOWEROFFACTIVE),
+ VALANDNAME_ENTRY(SPI_GETPOWEROFFTIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETLOWPOWERACTIVE),
+ VALANDNAME_ENTRY(SPI_SETLOWPOWERTIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETPOWEROFFACTIVE),
+ VALANDNAME_ENTRY(SPI_SETPOWEROFFTIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETSCREENSAVEACTIVE),
+ VALANDNAME_ENTRY(SPI_GETSCREENSAVERRUNNING),
+ VALANDNAME_ENTRY(SPI_GETSCREENSAVESECURE),
+ VALANDNAME_ENTRY(SPI_GETSCREENSAVETIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETSCREENSAVEACTIVE),
+ VALANDNAME_ENTRY(SPI_SETSCREENSAVERRUNNING),
+ VALANDNAME_ENTRY(SPI_SETSCREENSAVESECURE),
+ VALANDNAME_ENTRY(SPI_SETSCREENSAVETIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETHUNGAPPTIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETWAITTOKILLTIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETWAITTOKILLSERVICETIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETHUNGAPPTIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETWAITTOKILLTIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETWAITTOKILLSERVICETIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETCOMBOBOXANIMATION),
+ VALANDNAME_ENTRY(SPI_GETCURSORSHADOW),
+ VALANDNAME_ENTRY(SPI_GETGRADIENTCAPTIONS),
+ VALANDNAME_ENTRY(SPI_GETHOTTRACKING),
+ VALANDNAME_ENTRY(SPI_GETLISTBOXSMOOTHSCROLLING),
+ VALANDNAME_ENTRY(SPI_GETMENUANIMATION),
+ VALANDNAME_ENTRY(SPI_GETMENUUNDERLINES),
+ VALANDNAME_ENTRY(SPI_GETSELECTIONFADE),
+ VALANDNAME_ENTRY(SPI_GETTOOLTIPANIMATION),
+ VALANDNAME_ENTRY(SPI_GETTOOLTIPFADE),
+ VALANDNAME_ENTRY(SPI_GETUIEFFECTS),
+ VALANDNAME_ENTRY(SPI_SETCOMBOBOXANIMATION),
+ VALANDNAME_ENTRY(SPI_SETCURSORSHADOW),
+ VALANDNAME_ENTRY(SPI_SETGRADIENTCAPTIONS),
+ VALANDNAME_ENTRY(SPI_SETHOTTRACKING),
+ VALANDNAME_ENTRY(SPI_SETLISTBOXSMOOTHSCROLLING),
+ VALANDNAME_ENTRY(SPI_SETMENUANIMATION),
+ VALANDNAME_ENTRY(SPI_SETMENUUNDERLINES),
+ VALANDNAME_ENTRY(SPI_SETSELECTIONFADE),
+ VALANDNAME_ENTRY(SPI_SETTOOLTIPANIMATION),
+ VALANDNAME_ENTRY(SPI_SETTOOLTIPFADE),
+ VALANDNAME_ENTRY(SPI_SETUIEFFECTS),
+ VALANDNAME_ENTRY(SPI_GETACTIVEWINDOWTRACKING),
+ VALANDNAME_ENTRY(SPI_GETACTIVEWNDTRKZORDER),
+ VALANDNAME_ENTRY(SPI_GETACTIVEWNDTRKTIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETANIMATION),
+ VALANDNAME_ENTRY(SPI_GETBORDER),
+ VALANDNAME_ENTRY(SPI_GETCARETWIDTH),
+ VALANDNAME_ENTRY(SPI_GETDOCKMOVING),
+ VALANDNAME_ENTRY(SPI_GETDRAGFROMMAXIMIZE),
+ VALANDNAME_ENTRY(SPI_GETDRAGFULLWINDOWS),
+ VALANDNAME_ENTRY(SPI_GETFOREGROUNDFLASHCOUNT),
+ VALANDNAME_ENTRY(SPI_GETFOREGROUNDLOCKTIMEOUT),
+ VALANDNAME_ENTRY(SPI_GETMINIMIZEDMETRICS),
+ VALANDNAME_ENTRY(SPI_GETMOUSEDOCKTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_GETMOUSEDRAGOUTTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_GETMOUSESIDEMOVETHRESHOLD),
+ VALANDNAME_ENTRY(SPI_GETNONCLIENTMETRICS),
+ VALANDNAME_ENTRY(SPI_GETPENDOCKTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_GETPENDRAGOUTTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_GETPENSIDEMOVETHRESHOLD),
+ VALANDNAME_ENTRY(SPI_GETSHOWIMEUI),
+ VALANDNAME_ENTRY(SPI_GETSNAPSIZING),
+ VALANDNAME_ENTRY(SPI_GETWINARRANGING),
+ VALANDNAME_ENTRY(SPI_SETACTIVEWINDOWTRACKING),
+ VALANDNAME_ENTRY(SPI_SETACTIVEWNDTRKZORDER),
+ VALANDNAME_ENTRY(SPI_SETACTIVEWNDTRKTIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETANIMATION),
+ VALANDNAME_ENTRY(SPI_SETBORDER),
+ VALANDNAME_ENTRY(SPI_SETCARETWIDTH),
+ VALANDNAME_ENTRY(SPI_SETDOCKMOVING),
+ VALANDNAME_ENTRY(SPI_SETDRAGFROMMAXIMIZE),
+ VALANDNAME_ENTRY(SPI_SETDRAGFULLWINDOWS),
+ VALANDNAME_ENTRY(SPI_SETFOREGROUNDFLASHCOUNT),
+ VALANDNAME_ENTRY(SPI_SETFOREGROUNDLOCKTIMEOUT),
+ VALANDNAME_ENTRY(SPI_SETMINIMIZEDMETRICS),
+ VALANDNAME_ENTRY(SPI_SETMOUSEDOCKTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_SETMOUSEDRAGOUTTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_SETMOUSESIDEMOVETHRESHOLD),
+ VALANDNAME_ENTRY(SPI_SETNONCLIENTMETRICS),
+ VALANDNAME_ENTRY(SPI_SETPENDOCKTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_SETPENDRAGOUTTHRESHOLD),
+ VALANDNAME_ENTRY(SPI_SETPENSIDEMOVETHRESHOLD),
+ VALANDNAME_ENTRY(SPI_SETSHOWIMEUI),
+ VALANDNAME_ENTRY(SPI_SETSNAPSIZING),
+ VALANDNAME_ENTRY(SPI_SETWINARRANGING),
+ };
+ AppendEnumValueInfo(str, value, uiActionValues, name);
+}
+
+nsAutoCString WmSizeParamInfo(uint64_t wParam, uint64_t lParam,
+ bool /* isPreCall */) {
+ nsAutoCString result;
+ const static std::unordered_map<uint64_t, const char*> sizeValues{
+ VALANDNAME_ENTRY(SIZE_RESTORED), VALANDNAME_ENTRY(SIZE_MINIMIZED),
+ VALANDNAME_ENTRY(SIZE_MAXIMIZED), VALANDNAME_ENTRY(SIZE_MAXSHOW),
+ VALANDNAME_ENTRY(SIZE_MAXHIDE)};
+ AppendEnumValueInfo(result, wParam, sizeValues, "size");
+ result.AppendPrintf(" width=%d height=%d", static_cast<int>(LOWORD(lParam)),
+ static_cast<int>(HIWORD(lParam)));
+ return result;
+}
+
+const nsTArray<EnumValueAndName> windowPositionFlags = {
+ VALANDNAME_ENTRY(SWP_DRAWFRAME), VALANDNAME_ENTRY(SWP_HIDEWINDOW),
+ VALANDNAME_ENTRY(SWP_NOACTIVATE), VALANDNAME_ENTRY(SWP_NOCOPYBITS),
+ VALANDNAME_ENTRY(SWP_NOMOVE), VALANDNAME_ENTRY(SWP_NOOWNERZORDER),
+ VALANDNAME_ENTRY(SWP_NOREDRAW), VALANDNAME_ENTRY(SWP_NOSENDCHANGING),
+ VALANDNAME_ENTRY(SWP_NOSIZE), VALANDNAME_ENTRY(SWP_NOZORDER),
+ VALANDNAME_ENTRY(SWP_SHOWWINDOW),
+};
+
+void WindowPosParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ LPWINDOWPOS windowPos = reinterpret_cast<LPWINDOWPOS>(value);
+ if (windowPos == nullptr) {
+ str.AppendASCII("null windowPos?");
+ return;
+ }
+ HexParamInfo(str, reinterpret_cast<uint64_t>(windowPos->hwnd), "hwnd", false);
+ str.AppendASCII(" ");
+ HexParamInfo(str, reinterpret_cast<uint64_t>(windowPos->hwndInsertAfter),
+ "hwndInsertAfter", false);
+ str.AppendASCII(" ");
+ IntParamInfo(str, windowPos->x, "x", false);
+ str.AppendASCII(" ");
+ IntParamInfo(str, windowPos->y, "y", false);
+ str.AppendASCII(" ");
+ IntParamInfo(str, windowPos->cx, "cx", false);
+ str.AppendASCII(" ");
+ IntParamInfo(str, windowPos->cy, "cy", false);
+ str.AppendASCII(" ");
+ AppendFlagsInfo(str, windowPos->flags, windowPositionFlags, "flags");
+}
+
+void StyleOrExtendedParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> styleOrExtended{
+ VALANDNAME_ENTRY(GWL_EXSTYLE), VALANDNAME_ENTRY(GWL_STYLE)};
+ AppendEnumValueInfo(str, value, styleOrExtended, name);
+}
+
+void StyleStructParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ LPSTYLESTRUCT styleStruct = reinterpret_cast<LPSTYLESTRUCT>(value);
+ if (styleStruct == nullptr) {
+ str.AppendASCII("null STYLESTRUCT?");
+ return;
+ }
+ HexParamInfo(str, styleStruct->styleOld, "styleOld", false);
+ str.AppendASCII(" ");
+ HexParamInfo(str, styleStruct->styleNew, "styleNew", false);
+}
+
+void NcCalcSizeParamsParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ LPNCCALCSIZE_PARAMS params = reinterpret_cast<LPNCCALCSIZE_PARAMS>(value);
+ if (params == nullptr) {
+ str.AppendASCII("null NCCALCSIZE_PARAMS?");
+ return;
+ }
+ str.AppendPrintf("%s[0]: ", name);
+ RectParamInfo(str, reinterpret_cast<uintptr_t>(&params->rgrc[0]), nullptr,
+ false);
+ str.AppendPrintf(" %s[1]: ", name);
+ RectParamInfo(str, reinterpret_cast<uintptr_t>(&params->rgrc[1]), nullptr,
+ false);
+ str.AppendPrintf(" %s[2]: ", name);
+ RectParamInfo(str, reinterpret_cast<uintptr_t>(&params->rgrc[2]), nullptr,
+ false);
+ str.AppendASCII(" ");
+ WindowPosParamInfo(str, reinterpret_cast<uintptr_t>(params->lppos), nullptr,
+ false);
+}
+
+nsAutoCString WmNcCalcSizeParamInfo(uint64_t wParam, uint64_t lParam,
+ bool /* isPreCall */) {
+ nsAutoCString result;
+ TrueFalseParamInfo(result, wParam, "shouldIndicateValidArea", false);
+ result.AppendASCII(" ");
+ if (wParam == TRUE) {
+ NcCalcSizeParamsParamInfo(result, lParam, "ncCalcSizeParams", false);
+ } else {
+ RectParamInfo(result, lParam, "rect", false);
+ }
+ return result;
+}
+
+void ActivateWParamInfo(nsCString& result, uint64_t wParam, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> activateValues{
+ VALANDNAME_ENTRY(WA_ACTIVE), VALANDNAME_ENTRY(WA_CLICKACTIVE),
+ VALANDNAME_ENTRY(WA_INACTIVE)};
+ AppendEnumValueInfo(result, wParam, activateValues, name);
+}
+
+void HitTestParamInfo(nsCString& result, uint64_t param, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> hitTestResults{
+ VALANDNAME_ENTRY(HTBORDER), VALANDNAME_ENTRY(HTBOTTOM),
+ VALANDNAME_ENTRY(HTBOTTOMLEFT), VALANDNAME_ENTRY(HTBOTTOMRIGHT),
+ VALANDNAME_ENTRY(HTCAPTION), VALANDNAME_ENTRY(HTCLIENT),
+ VALANDNAME_ENTRY(HTCLOSE), VALANDNAME_ENTRY(HTERROR),
+ VALANDNAME_ENTRY(HTGROWBOX), VALANDNAME_ENTRY(HTHELP),
+ VALANDNAME_ENTRY(HTHSCROLL), VALANDNAME_ENTRY(HTLEFT),
+ VALANDNAME_ENTRY(HTMENU), VALANDNAME_ENTRY(HTMAXBUTTON),
+ VALANDNAME_ENTRY(HTMINBUTTON), VALANDNAME_ENTRY(HTNOWHERE),
+ VALANDNAME_ENTRY(HTREDUCE), VALANDNAME_ENTRY(HTRIGHT),
+ VALANDNAME_ENTRY(HTSIZE), VALANDNAME_ENTRY(HTSYSMENU),
+ VALANDNAME_ENTRY(HTTOP), VALANDNAME_ENTRY(HTTOPLEFT),
+ VALANDNAME_ENTRY(HTTOPRIGHT), VALANDNAME_ENTRY(HTTRANSPARENT),
+ VALANDNAME_ENTRY(HTVSCROLL), VALANDNAME_ENTRY(HTZOOM),
+ };
+ AppendEnumValueInfo(result, param, hitTestResults, name);
+}
+
+void SetCursorLParamInfo(nsCString& result, uint64_t lParam,
+ const char* /* name */, bool /* isPreCall */) {
+ HitTestParamInfo(result, LOWORD(lParam), "hitTestResult", false);
+ result.AppendASCII(" ");
+ HexParamInfo(result, HIWORD(lParam), "message", false);
+}
+
+void MinMaxInfoParamInfo(nsCString& result, uint64_t value,
+ const char* /* name */, bool /* isPreCall */) {
+ PMINMAXINFO minMaxInfo = reinterpret_cast<PMINMAXINFO>(value);
+ if (minMaxInfo == nullptr) {
+ result.AppendPrintf("NULL minMaxInfo?");
+ return;
+ }
+ PointExplicitParamInfo(result, minMaxInfo->ptMaxSize, "maxSize");
+ result.AppendASCII(" ");
+ PointExplicitParamInfo(result, minMaxInfo->ptMaxPosition, "maxPosition");
+ result.AppendASCII(" ");
+ PointExplicitParamInfo(result, minMaxInfo->ptMinTrackSize, "minTrackSize");
+ result.AppendASCII(" ");
+ PointExplicitParamInfo(result, minMaxInfo->ptMaxTrackSize, "maxTrackSize");
+}
+
+void WideStringParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ result.AppendPrintf("%s=%S", name, reinterpret_cast<LPCWSTR>(value));
+}
+
+void DeviceEventParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ const static std::unordered_map<uint64_t, const char*> deviceEventValues{
+ VALANDNAME_ENTRY(DBT_DEVNODES_CHANGED),
+ VALANDNAME_ENTRY(DBT_QUERYCHANGECONFIG),
+ VALANDNAME_ENTRY(DBT_CONFIGCHANGED),
+ VALANDNAME_ENTRY(DBT_CONFIGCHANGECANCELED),
+ VALANDNAME_ENTRY(DBT_DEVICEARRIVAL),
+ VALANDNAME_ENTRY(DBT_DEVICEQUERYREMOVE),
+ VALANDNAME_ENTRY(DBT_DEVICEQUERYREMOVEFAILED),
+ VALANDNAME_ENTRY(DBT_DEVICEREMOVEPENDING),
+ VALANDNAME_ENTRY(DBT_DEVICEREMOVECOMPLETE),
+ VALANDNAME_ENTRY(DBT_DEVICETYPESPECIFIC),
+ VALANDNAME_ENTRY(DBT_CUSTOMEVENT),
+ VALANDNAME_ENTRY(DBT_USERDEFINED)};
+ AppendEnumValueInfo(result, value, deviceEventValues, name);
+}
+
+void ResolutionParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool /* isPreCall */) {
+ result.AppendPrintf("horizontalRes=%d verticalRes=%d", LOWORD(value),
+ HIWORD(value));
+}
+
+// Window message with default wParam/lParam logging
+#define ENTRY(_msg) \
+ { \
+ _msg, { #_msg, _msg, DefaultParamInfo } \
+ }
+// Window message with no parameters
+#define ENTRY_WITH_NO_PARAM_INFO(_msg) \
+ { \
+ _msg, { #_msg, _msg, nullptr } \
+ }
+// Window message with custom parameter logging functions
+#define ENTRY_WITH_CUSTOM_PARAM_INFO(_msg, paramInfoFn) \
+ { \
+ _msg, { #_msg, _msg, paramInfoFn } \
+ }
+// Window message with separate custom wParam and lParam logging functions
+#define ENTRY_WITH_SPLIT_PARAM_INFOS(_msg, wParamInfoFn, wParamName, \
+ lParamInfoFn, lParamName) \
+ { \
+ _msg, { \
+ #_msg, _msg, nullptr, wParamInfoFn, wParamName, lParamInfoFn, lParamName \
+ } \
+ }
+std::unordered_map<UINT, EventMsgInfo> gAllEvents = {
+ ENTRY_WITH_NO_PARAM_INFO(WM_NULL),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_CREATE, nullptr, nullptr,
+ CreateStructParamInfo, "createStruct"),
+ ENTRY_WITH_NO_PARAM_INFO(WM_DESTROY),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOVE, nullptr, nullptr,
+ XLowWordYHighWordParamInfo, "upperLeft"),
+ ENTRY_WITH_CUSTOM_PARAM_INFO(WM_SIZE, WmSizeParamInfo),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ACTIVATE, ActivateWParamInfo, "wParam",
+ HexParamInfo, "handle"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETFOCUS, HexParamInfo, "handle", nullptr,
+ nullptr),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_KILLFOCUS, HexParamInfo, "handle", nullptr,
+ nullptr),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ENABLE, TrueFalseParamInfo, "enabled",
+ nullptr, nullptr),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETREDRAW, TrueFalseParamInfo,
+ "redrawState", nullptr, nullptr),
+ ENTRY(WM_SETTEXT),
+ ENTRY(WM_GETTEXT),
+ ENTRY(WM_GETTEXTLENGTH),
+ ENTRY_WITH_NO_PARAM_INFO(WM_PAINT),
+ ENTRY_WITH_NO_PARAM_INFO(WM_CLOSE),
+ ENTRY(WM_QUERYENDSESSION),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_QUIT, HexParamInfo, "exitCode", nullptr,
+ nullptr),
+ ENTRY(WM_QUERYOPEN),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ERASEBKGND, HexParamInfo, "deviceContext",
+ nullptr, nullptr),
+ ENTRY(WM_SYSCOLORCHANGE),
+ ENTRY(WM_ENDSESSION),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SHOWWINDOW, TrueFalseParamInfo,
+ "windowBeingShown", ShowWindowReasonParamInfo,
+ "status"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETTINGCHANGE, UiActionParamInfo,
+ "uiAction", WideStringParamInfo,
+ "paramChanged"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DEVMODECHANGE, nullptr, nullptr,
+ WideStringParamInfo, "deviceName"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ACTIVATEAPP, TrueFalseParamInfo,
+ "activated", HexParamInfo, "threadId"),
+ ENTRY(WM_FONTCHANGE),
+ ENTRY(WM_TIMECHANGE),
+ ENTRY(WM_CANCELMODE),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETCURSOR, HexParamInfo, "windowHandle",
+ SetCursorLParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEACTIVATE, HexParamInfo, "windowHandle",
+ SetCursorLParamInfo, ""),
+ ENTRY(WM_CHILDACTIVATE),
+ ENTRY(WM_QUEUESYNC),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_GETMINMAXINFO, nullptr, nullptr,
+ MinMaxInfoParamInfo, ""),
+ ENTRY(WM_PAINTICON),
+ ENTRY(WM_ICONERASEBKGND),
+ ENTRY(WM_NEXTDLGCTL),
+ ENTRY(WM_SPOOLERSTATUS),
+ ENTRY(WM_DRAWITEM),
+ ENTRY(WM_MEASUREITEM),
+ ENTRY(WM_DELETEITEM),
+ ENTRY(WM_VKEYTOITEM),
+ ENTRY(WM_CHARTOITEM),
+ ENTRY(WM_SETFONT),
+ ENTRY(WM_GETFONT),
+ ENTRY(WM_SETHOTKEY),
+ ENTRY(WM_GETHOTKEY),
+ ENTRY(WM_QUERYDRAGICON),
+ ENTRY(WM_COMPAREITEM),
+ ENTRY(WM_GETOBJECT),
+ ENTRY(WM_COMPACTING),
+ ENTRY(WM_COMMNOTIFY),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_WINDOWPOSCHANGING, nullptr, nullptr,
+ WindowPosParamInfo, "newSizeAndPos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_WINDOWPOSCHANGED, nullptr, nullptr,
+ WindowPosParamInfo, "newSizeAndPos"),
+ ENTRY(WM_POWER),
+ ENTRY(WM_COPYDATA),
+ ENTRY(WM_CANCELJOURNAL),
+ ENTRY(WM_NOTIFY),
+ ENTRY(WM_INPUTLANGCHANGEREQUEST),
+ ENTRY(WM_INPUTLANGCHANGE),
+ ENTRY(WM_TCARD),
+ ENTRY(WM_HELP),
+ ENTRY(WM_USERCHANGED),
+ ENTRY(WM_NOTIFYFORMAT),
+ ENTRY(WM_CONTEXTMENU),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_STYLECHANGING, StyleOrExtendedParamInfo,
+ "styleOrExtended", StyleStructParamInfo,
+ "newStyles"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_STYLECHANGED, StyleOrExtendedParamInfo,
+ "styleOrExtended", StyleStructParamInfo,
+ "newStyles"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DISPLAYCHANGE, IntParamInfo, "bitsPerPixel",
+ ResolutionParamInfo, ""),
+ ENTRY(WM_GETICON),
+ ENTRY(WM_SETICON),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCCREATE, nullptr, nullptr,
+ CreateStructParamInfo, "createStruct"),
+ ENTRY_WITH_NO_PARAM_INFO(WM_NCDESTROY),
+ ENTRY_WITH_CUSTOM_PARAM_INFO(WM_NCCALCSIZE, WmNcCalcSizeParamInfo),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCHITTEST, nullptr, nullptr,
+ XLowWordYHighWordParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCPAINT, HexParamInfo, "updateRegionHandle",
+ nullptr, nullptr),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCACTIVATE, TrueFalseParamInfo,
+ "isTitleBarOrIconActive", HexParamInfo,
+ "updateRegion"),
+ ENTRY(WM_GETDLGCODE),
+ ENTRY(WM_SYNCPAINT),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMOUSEMOVE, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCLBUTTONDOWN, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCLBUTTONUP, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCLBUTTONDBLCLK, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCRBUTTONDOWN, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCRBUTTONUP, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCRBUTTONDBLCLK, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMBUTTONDOWN, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMBUTTONUP, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMBUTTONDBLCLK, HitTestParamInfo,
+ "hitTestValue", PointsParamInfo, "mousePos"),
+ ENTRY(EM_GETSEL),
+ ENTRY(EM_SETSEL),
+ ENTRY(EM_GETRECT),
+ ENTRY(EM_SETRECT),
+ ENTRY(EM_SETRECTNP),
+ ENTRY(EM_SCROLL),
+ ENTRY(EM_LINESCROLL),
+ ENTRY(EM_SCROLLCARET),
+ ENTRY(EM_GETMODIFY),
+ ENTRY(EM_SETMODIFY),
+ ENTRY(EM_GETLINECOUNT),
+ ENTRY(EM_LINEINDEX),
+ ENTRY(EM_SETHANDLE),
+ ENTRY(EM_GETHANDLE),
+ ENTRY(EM_GETTHUMB),
+ ENTRY(EM_LINELENGTH),
+ ENTRY(EM_REPLACESEL),
+ ENTRY(EM_GETLINE),
+ ENTRY(EM_LIMITTEXT),
+ ENTRY(EM_CANUNDO),
+ ENTRY(EM_UNDO),
+ ENTRY(EM_FMTLINES),
+ ENTRY(EM_LINEFROMCHAR),
+ ENTRY(EM_SETTABSTOPS),
+ ENTRY(EM_SETPASSWORDCHAR),
+ ENTRY(EM_EMPTYUNDOBUFFER),
+ ENTRY(EM_GETFIRSTVISIBLELINE),
+ ENTRY(EM_SETREADONLY),
+ ENTRY(EM_SETWORDBREAKPROC),
+ ENTRY(EM_GETWORDBREAKPROC),
+ ENTRY(EM_GETPASSWORDCHAR),
+ ENTRY(EM_SETMARGINS),
+ ENTRY(EM_GETMARGINS),
+ ENTRY(EM_GETLIMITTEXT),
+ ENTRY(EM_POSFROMCHAR),
+ ENTRY(EM_CHARFROMPOS),
+ ENTRY(EM_SETIMESTATUS),
+ ENTRY(EM_GETIMESTATUS),
+ ENTRY(SBM_SETPOS),
+ ENTRY(SBM_GETPOS),
+ ENTRY(SBM_SETRANGE),
+ ENTRY(SBM_SETRANGEREDRAW),
+ ENTRY(SBM_GETRANGE),
+ ENTRY(SBM_ENABLE_ARROWS),
+ ENTRY(SBM_SETSCROLLINFO),
+ ENTRY(SBM_GETSCROLLINFO),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_KEYDOWN, VirtualKeyParamInfo, "vKey",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_KEYUP, VirtualKeyParamInfo, "vKey",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_CHAR, IntParamInfo, "charCode",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DEADCHAR, IntParamInfo, "charCode",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSKEYDOWN, VirtualKeyParamInfo, "vKey",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSKEYUP, VirtualKeyParamInfo, "vKey",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSCHAR, IntParamInfo, "charCode",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSDEADCHAR, IntParamInfo, "charCode",
+ KeystrokeFlagsParamInfo, ""),
+ ENTRY(WM_KEYLAST),
+ ENTRY(WM_IME_STARTCOMPOSITION),
+ ENTRY(WM_IME_ENDCOMPOSITION),
+ ENTRY(WM_IME_COMPOSITION),
+ ENTRY(WM_INITDIALOG),
+ ENTRY(WM_COMMAND),
+ ENTRY(WM_SYSCOMMAND),
+ ENTRY(WM_TIMER),
+ ENTRY(WM_HSCROLL),
+ ENTRY(WM_VSCROLL),
+ ENTRY(WM_INITMENU),
+ ENTRY(WM_INITMENUPOPUP),
+ ENTRY(WM_MENUSELECT),
+ ENTRY(WM_MENUCHAR),
+ ENTRY(WM_ENTERIDLE),
+ ENTRY(WM_MENURBUTTONUP),
+ ENTRY(WM_MENUDRAG),
+ ENTRY(WM_MENUGETOBJECT),
+ ENTRY(WM_UNINITMENUPOPUP),
+ ENTRY(WM_MENUCOMMAND),
+ ENTRY(WM_CHANGEUISTATE),
+ ENTRY(WM_QUERYUISTATE),
+ ENTRY(WM_UPDATEUISTATE),
+ ENTRY(WM_CTLCOLORMSGBOX),
+ ENTRY(WM_CTLCOLOREDIT),
+ ENTRY(WM_CTLCOLORLISTBOX),
+ ENTRY(WM_CTLCOLORBTN),
+ ENTRY(WM_CTLCOLORDLG),
+ ENTRY(WM_CTLCOLORSCROLLBAR),
+ ENTRY(WM_CTLCOLORSTATIC),
+ ENTRY(CB_GETEDITSEL),
+ ENTRY(CB_LIMITTEXT),
+ ENTRY(CB_SETEDITSEL),
+ ENTRY(CB_ADDSTRING),
+ ENTRY(CB_DELETESTRING),
+ ENTRY(CB_DIR),
+ ENTRY(CB_GETCOUNT),
+ ENTRY(CB_GETCURSEL),
+ ENTRY(CB_GETLBTEXT),
+ ENTRY(CB_GETLBTEXTLEN),
+ ENTRY(CB_INSERTSTRING),
+ ENTRY(CB_RESETCONTENT),
+ ENTRY(CB_FINDSTRING),
+ ENTRY(CB_SELECTSTRING),
+ ENTRY(CB_SETCURSEL),
+ ENTRY(CB_SHOWDROPDOWN),
+ ENTRY(CB_GETITEMDATA),
+ ENTRY(CB_SETITEMDATA),
+ ENTRY(CB_GETDROPPEDCONTROLRECT),
+ ENTRY(CB_SETITEMHEIGHT),
+ ENTRY(CB_GETITEMHEIGHT),
+ ENTRY(CB_SETEXTENDEDUI),
+ ENTRY(CB_GETEXTENDEDUI),
+ ENTRY(CB_GETDROPPEDSTATE),
+ ENTRY(CB_FINDSTRINGEXACT),
+ ENTRY(CB_SETLOCALE),
+ ENTRY(CB_GETLOCALE),
+ ENTRY(CB_GETTOPINDEX),
+ ENTRY(CB_SETTOPINDEX),
+ ENTRY(CB_GETHORIZONTALEXTENT),
+ ENTRY(CB_SETHORIZONTALEXTENT),
+ ENTRY(CB_GETDROPPEDWIDTH),
+ ENTRY(CB_SETDROPPEDWIDTH),
+ ENTRY(CB_INITSTORAGE),
+ ENTRY(CB_MSGMAX),
+ ENTRY(LB_ADDSTRING),
+ ENTRY(LB_INSERTSTRING),
+ ENTRY(LB_DELETESTRING),
+ ENTRY(LB_SELITEMRANGEEX),
+ ENTRY(LB_RESETCONTENT),
+ ENTRY(LB_SETSEL),
+ ENTRY(LB_SETCURSEL),
+ ENTRY(LB_GETSEL),
+ ENTRY(LB_GETCURSEL),
+ ENTRY(LB_GETTEXT),
+ ENTRY(LB_GETTEXTLEN),
+ ENTRY(LB_GETCOUNT),
+ ENTRY(LB_SELECTSTRING),
+ ENTRY(LB_DIR),
+ ENTRY(LB_GETTOPINDEX),
+ ENTRY(LB_FINDSTRING),
+ ENTRY(LB_GETSELCOUNT),
+ ENTRY(LB_GETSELITEMS),
+ ENTRY(LB_SETTABSTOPS),
+ ENTRY(LB_GETHORIZONTALEXTENT),
+ ENTRY(LB_SETHORIZONTALEXTENT),
+ ENTRY(LB_SETCOLUMNWIDTH),
+ ENTRY(LB_ADDFILE),
+ ENTRY(LB_SETTOPINDEX),
+ ENTRY(LB_GETITEMRECT),
+ ENTRY(LB_GETITEMDATA),
+ ENTRY(LB_SETITEMDATA),
+ ENTRY(LB_SELITEMRANGE),
+ ENTRY(LB_SETANCHORINDEX),
+ ENTRY(LB_GETANCHORINDEX),
+ ENTRY(LB_SETCARETINDEX),
+ ENTRY(LB_GETCARETINDEX),
+ ENTRY(LB_SETITEMHEIGHT),
+ ENTRY(LB_GETITEMHEIGHT),
+ ENTRY(LB_FINDSTRINGEXACT),
+ ENTRY(LB_SETLOCALE),
+ ENTRY(LB_GETLOCALE),
+ ENTRY(LB_SETCOUNT),
+ ENTRY(LB_INITSTORAGE),
+ ENTRY(LB_ITEMFROMPOINT),
+ ENTRY(LB_MSGMAX),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEMOVE, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_LBUTTONDOWN, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_LBUTTONUP, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_LBUTTONDBLCLK, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_RBUTTONDOWN, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_RBUTTONUP, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_RBUTTONDBLCLK, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MBUTTONDOWN, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MBUTTONUP, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MBUTTONDBLCLK, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEWHEEL,
+ VirtualKeysLowWordDistanceHighWordParamInfo,
+ "", XLowWordYHighWordParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEHWHEEL,
+ VirtualKeysLowWordDistanceHighWordParamInfo,
+ "", XLowWordYHighWordParamInfo, "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_PARENTNOTIFY, ParentNotifyEventParamInfo,
+ "", PointParamInfo, "pointerLocation"),
+ ENTRY(WM_ENTERMENULOOP),
+ ENTRY(WM_EXITMENULOOP),
+ ENTRY(WM_NEXTMENU),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SIZING, WindowEdgeParamInfo, "edge",
+ RectParamInfo, "rect"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_CAPTURECHANGED, nullptr, nullptr,
+ HexParamInfo, "windowHandle"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOVING, nullptr, nullptr, RectParamInfo,
+ "rect"),
+ ENTRY(WM_POWERBROADCAST),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DEVICECHANGE, DeviceEventParamInfo, "event",
+ HexParamInfo, "data"),
+ ENTRY(WM_MDICREATE),
+ ENTRY(WM_MDIDESTROY),
+ ENTRY(WM_MDIACTIVATE),
+ ENTRY(WM_MDIRESTORE),
+ ENTRY(WM_MDINEXT),
+ ENTRY(WM_MDIMAXIMIZE),
+ ENTRY(WM_MDITILE),
+ ENTRY(WM_MDICASCADE),
+ ENTRY(WM_MDIICONARRANGE),
+ ENTRY(WM_MDIGETACTIVE),
+ ENTRY(WM_MDISETMENU),
+ ENTRY(WM_ENTERSIZEMOVE),
+ ENTRY(WM_EXITSIZEMOVE),
+ ENTRY(WM_DROPFILES),
+ ENTRY(WM_MDIREFRESHMENU),
+ ENTRY(WM_IME_SETCONTEXT),
+ ENTRY(WM_IME_NOTIFY),
+ ENTRY(WM_IME_CONTROL),
+ ENTRY(WM_IME_COMPOSITIONFULL),
+ ENTRY(WM_IME_SELECT),
+ ENTRY(WM_IME_CHAR),
+ ENTRY(WM_IME_REQUEST),
+ ENTRY(WM_IME_KEYDOWN),
+ ENTRY(WM_IME_KEYUP),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMOUSEHOVER, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEHOVER, VirtualModifierKeysParamInfo,
+ "virtualKeys", XLowWordYHighWordParamInfo,
+ "mousePos"),
+ ENTRY_WITH_NO_PARAM_INFO(WM_NCMOUSELEAVE),
+ ENTRY_WITH_NO_PARAM_INFO(WM_MOUSELEAVE),
+ ENTRY(WM_CUT),
+ ENTRY(WM_COPY),
+ ENTRY(WM_PASTE),
+ ENTRY(WM_CLEAR),
+ ENTRY(WM_UNDO),
+ ENTRY(WM_RENDERFORMAT),
+ ENTRY(WM_RENDERALLFORMATS),
+ ENTRY(WM_DESTROYCLIPBOARD),
+ ENTRY(WM_DRAWCLIPBOARD),
+ ENTRY(WM_PAINTCLIPBOARD),
+ ENTRY(WM_VSCROLLCLIPBOARD),
+ ENTRY(WM_SIZECLIPBOARD),
+ ENTRY(WM_ASKCBFORMATNAME),
+ ENTRY(WM_CHANGECBCHAIN),
+ ENTRY(WM_HSCROLLCLIPBOARD),
+ ENTRY(WM_QUERYNEWPALETTE),
+ ENTRY(WM_PALETTEISCHANGING),
+ ENTRY(WM_PALETTECHANGED),
+ ENTRY(WM_HOTKEY),
+ ENTRY(WM_PRINT),
+ ENTRY(WM_PRINTCLIENT),
+ ENTRY(WM_THEMECHANGED),
+ ENTRY(WM_HANDHELDFIRST),
+ ENTRY(WM_HANDHELDLAST),
+ ENTRY(WM_AFXFIRST),
+ ENTRY(WM_AFXLAST),
+ ENTRY(WM_PENWINFIRST),
+ ENTRY(WM_PENWINLAST),
+ ENTRY(WM_APP),
+ ENTRY_WITH_NO_PARAM_INFO(WM_DWMCOMPOSITIONCHANGED),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DWMNCRENDERINGCHANGED, TrueFalseParamInfo,
+ "DwmNcRendering", nullptr, nullptr),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DWMCOLORIZATIONCOLORCHANGED, HexParamInfo,
+ "color:AARRGGBB", TrueFalseParamInfo,
+ "isOpaque"),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DWMWINDOWMAXIMIZEDCHANGE,
+ TrueFalseParamInfo, "maximized", nullptr,
+ nullptr),
+ ENTRY(WM_DWMSENDICONICTHUMBNAIL), // lParam: HIWORD is x, LOWORD is y
+ ENTRY_WITH_NO_PARAM_INFO(WM_DWMSENDICONICLIVEPREVIEWBITMAP),
+ ENTRY(WM_TABLET_QUERYSYSTEMGESTURESTATUS),
+ ENTRY(WM_GESTURE),
+ ENTRY(WM_GESTURENOTIFY),
+ ENTRY(WM_GETTITLEBARINFOEX),
+ ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DPICHANGED, XLowWordYHighWordParamInfo,
+ "newDPI", RectParamInfo,
+ "suggestedSizeAndPos"),
+};
+#undef ENTRY
+#undef ENTRY_WITH_NO_PARAM_INFO
+#undef ENTRY_WITH_CUSTOM_PARAM_INFO
+#undef ENTRY_WITH_SPLIT_PARAM_INFO
+
+} // namespace mozilla::widget
+
+#ifdef DEBUG
+void DDError(const char* msg, HRESULT hr) {
+ /*XXX make nicer */
+ MOZ_LOG(gWindowsLog, LogLevel::Error,
+ ("DirectDraw error %s: 0x%08lx\n", msg, hr));
+}
+#endif
+
+#ifdef DEBUG_VK
+bool is_vk_down(int vk) {
+ SHORT st = GetKeyState(vk);
+# ifdef DEBUG
+ MOZ_LOG(gWindowsLog, LogLevel::Info, ("is_vk_down vk=%x st=%x\n", vk, st));
+# endif
+ return (st < 0);
+}
+#endif
diff --git a/widget/windows/nsWindowDbg.h b/widget/windows/nsWindowDbg.h
new file mode 100644
index 0000000000..c739966fd5
--- /dev/null
+++ b/widget/windows/nsWindowDbg.h
@@ -0,0 +1,153 @@
+/* -*- 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/. */
+
+#ifndef WindowDbg_h__
+#define WindowDbg_h__
+
+/*
+ * nsWindowDbg - Debug related utilities for nsWindow.
+ */
+
+#include "nsWindowDefs.h"
+#include "mozilla/BaseProfilerMarkersPrerequisites.h"
+
+// Enables debug output for popup rollup hooks
+// #define POPUP_ROLLUP_DEBUG_OUTPUT
+
+// Enable window size and state debug output
+// #define WINSTATE_DEBUG_OUTPUT
+
+// nsIWidget defines a set of debug output statements
+// that are called in various places within the code.
+// #define WIDGET_DEBUG_OUTPUT
+
+// Enable IS_VK_DOWN debug output
+// #define DEBUG_VK
+
+namespace mozilla::widget {
+
+class MOZ_RAII AutoProfilerMessageMarker {
+ public:
+ explicit AutoProfilerMessageMarker(Span<const char> aMsgLoopName, HWND hWnd,
+ UINT msg, WPARAM wParam, LPARAM lParam);
+
+ ~AutoProfilerMessageMarker();
+
+ protected:
+ Maybe<MarkerOptions> mOptions;
+ Span<const char> mMsgLoopName;
+ UINT mMsg;
+ WPARAM mWParam;
+ LPARAM mLParam;
+};
+
+// Windows message debugging data
+struct EventMsgInfo {
+ const char* mStr;
+ UINT mId;
+ std::function<nsAutoCString(WPARAM, LPARAM, bool)> mParamInfoFn;
+ std::function<void(nsCString&, WPARAM, const char*, bool)> mWParamInfoFn;
+ const char* mWParamName;
+ std::function<void(nsCString&, LPARAM, const char*, bool)> mLParamInfoFn;
+ const char* mLParamName;
+ void LogParameters(nsCString& str, WPARAM wParam, LPARAM lParam,
+ bool isPreCall);
+};
+extern std::unordered_map<UINT, EventMsgInfo> gAllEvents;
+
+// RAII-style class to log before and after an event is handled.
+class NativeEventLogger final {
+ public:
+ template <size_t N>
+ NativeEventLogger(const char (&aMsgLoopName)[N], HWND hwnd, UINT msg,
+ WPARAM wParam, LPARAM lParam)
+ : NativeEventLogger(Span(aMsgLoopName), hwnd, msg, wParam, lParam) {}
+
+ void SetResult(LRESULT lresult, bool result) {
+ mRetValue = lresult;
+ mResult = mozilla::Some(result);
+ }
+ ~NativeEventLogger();
+
+ private:
+ NativeEventLogger(Span<const char> aMsgLoopName, HWND hwnd, UINT msg,
+ WPARAM wParam, LPARAM lParam);
+ bool NativeEventLoggerInternal();
+
+ AutoProfilerMessageMarker mProfilerMarker;
+ const char* mMsgLoopName;
+ const HWND mHwnd;
+ const UINT mMsg;
+ const WPARAM mWParam;
+ const LPARAM mLParam;
+ mozilla::Maybe<long> mEventCounter;
+ // not const because these will be set after the event is handled
+ mozilla::Maybe<bool> mResult;
+ LRESULT mRetValue = 0;
+
+ bool mShouldLogPostCall;
+};
+
+struct EnumValueAndName {
+ uint64_t mFlag;
+ const char* mName;
+};
+
+// Appends to str a description of the flags passed in.
+// flagsAndNames is a list of flag values with a string description
+// for each one. These are processed in order, so if there are
+// flag values that are combination of individual values (for example
+// something like WS_OVERLAPPEDWINDOW) they need to come first
+// in the flagsAndNames array.
+// A 0 flag value will only be written if the flags input is exactly
+// 0, and it must come last in the flagsAndNames array.
+// Returns whether any info was appended to str.
+bool AppendFlagsInfo(nsCString& str, uint64_t flags,
+ const nsTArray<EnumValueAndName>& flagsAndNames,
+ const char* name);
+
+nsAutoCString WmSizeParamInfo(uint64_t wParam, uint64_t lParam, bool isPreCall);
+void XLowWordYHighWordParamInfo(nsCString& str, uint64_t value,
+ const char* name, bool isPreCall);
+void WindowPosParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool isPreCall);
+void WindowEdgeParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool isPreCall);
+void RectParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool isPreCall);
+void UiActionParamInfo(nsCString& str, uint64_t value, const char* name,
+ bool isPreCall);
+void WideStringParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool isPreCall);
+void MinMaxInfoParamInfo(nsCString& result, uint64_t value, const char* name,
+ bool isPreCall);
+nsAutoCString WmNcCalcSizeParamInfo(uint64_t wParam, uint64_t lParam,
+ bool isPreCall);
+} // namespace mozilla::widget
+
+#if defined(POPUP_ROLLUP_DEBUG_OUTPUT)
+typedef struct {
+ char* mStr;
+ int mId;
+} MSGFEventMsgInfo;
+
+# define DISPLAY_NMM_PRT(_arg) \
+ MOZ_LOG(gWindowsLog, mozilla::LogLevel::Info, ((_arg)));
+#else
+# define DISPLAY_NMM_PRT(_arg)
+#endif // defined(POPUP_ROLLUP_DEBUG_OUTPUT)
+
+#if defined(DEBUG)
+void DDError(const char* msg, HRESULT hr);
+#endif // defined(DEBUG)
+
+#if defined(DEBUG_VK)
+bool is_vk_down(int vk);
+# define IS_VK_DOWN is_vk_down
+#else
+# define IS_VK_DOWN(a) (GetKeyState(a) < 0)
+#endif // defined(DEBUG_VK)
+
+#endif /* WindowDbg_h__ */
diff --git a/widget/windows/nsWindowDefs.h b/widget/windows/nsWindowDefs.h
new file mode 100644
index 0000000000..320d6ef07b
--- /dev/null
+++ b/widget/windows/nsWindowDefs.h
@@ -0,0 +1,119 @@
+/* -*- 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/. */
+
+#ifndef WindowDefs_h__
+#define WindowDefs_h__
+
+/*
+ * nsWindowDefs - nsWindow related definitions, consts, and macros.
+ */
+
+#include "mozilla/widget/WinMessages.h"
+#include "nsBaseWidget.h"
+#include "nsdefs.h"
+#include "resource.h"
+
+/**************************************************************
+ *
+ * SECTION: defines
+ *
+ **************************************************************/
+
+// ConstrainPosition window positioning slop value
+#define kWindowPositionSlop 20
+
+// Origin of the system context menu when displayed in full screen mode
+#define MOZ_SYSCONTEXT_X_POS 20
+#define MOZ_SYSCONTEXT_Y_POS 20
+
+// Don't put more than this many rects in the dirty region, just fluff
+// out to the bounding-box if there are more
+#define MAX_RECTS_IN_REGION 100
+
+// Tablet PC Mouse Input Source
+#define TABLET_INK_SIGNATURE 0xFFFFFF00
+#define TABLET_INK_CHECK 0xFF515700
+#define TABLET_INK_TOUCH 0x00000080
+#define TABLET_INK_ID_MASK 0x0000007F
+#define MOUSE_INPUT_SOURCE() WinUtils::GetMouseInputSource()
+#define MOUSE_POINTERID() WinUtils::GetMousePointerID()
+
+/**************************************************************
+ *
+ * SECTION: constants
+ *
+ **************************************************************/
+
+/*
+ * Native windows class names
+ *
+ * ::: IMPORTANT :::
+ *
+ * External apps and drivers depend on window class names.
+ * For example, changing the window classes could break
+ * touchpad scrolling or screen readers.
+ *
+ * See bug 1776498.
+ */
+const wchar_t kClassNameHidden[] = L"MozillaHiddenWindowClass";
+const wchar_t kClassNameGeneral[] = L"MozillaWindowClass";
+const wchar_t kClassNameDialog[] = L"MozillaDialogClass";
+const wchar_t kClassNameDropShadow[] = L"MozillaDropShadowWindowClass";
+const wchar_t kClassNameTransition[] = L"MozillaTransitionWindowClass";
+
+/**************************************************************
+ *
+ * SECTION: structs
+ *
+ **************************************************************/
+
+// Used for synthesizing events
+struct KeyPair {
+ uint8_t mGeneral;
+ uint8_t mSpecific;
+ uint16_t mScanCode;
+ KeyPair(uint32_t aGeneral, uint32_t aSpecific)
+ : mGeneral(aGeneral & 0xFF),
+ mSpecific(aSpecific & 0xFF),
+ mScanCode((aGeneral & 0xFFFF0000) >> 16) {}
+ KeyPair(uint8_t aGeneral, uint8_t aSpecific, uint16_t aScanCode)
+ : mGeneral(aGeneral), mSpecific(aSpecific), mScanCode(aScanCode) {}
+};
+
+namespace mozilla {
+namespace widget {
+
+struct MSGResult {
+ // Result for the message.
+ LRESULT& mResult;
+ // If mConsumed is true, the caller shouldn't call next wndproc.
+ bool mConsumed;
+
+ explicit MSGResult(LRESULT* aResult = nullptr)
+ : mResult(aResult ? *aResult : mDefaultResult), mConsumed(false) {}
+
+ private:
+ LRESULT mDefaultResult;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+/**************************************************************
+ *
+ * SECTION: macros
+ *
+ **************************************************************/
+
+#define NSRGB_2_COLOREF(color) \
+ RGB(NS_GET_R(color), NS_GET_G(color), NS_GET_B(color))
+#define COLOREF_2_NSRGB(color) \
+ NS_RGB(GetRValue(color), GetGValue(color), GetBValue(color))
+
+#define VERIFY_WINDOW_STYLE(s) \
+ NS_ASSERTION(((s) & (WS_CHILD | WS_POPUP)) != (WS_CHILD | WS_POPUP), \
+ "WS_POPUP and WS_CHILD are mutually exclusive")
+
+#endif /* WindowDefs_h__ */
diff --git a/widget/windows/nsWindowGfx.cpp b/widget/windows/nsWindowGfx.cpp
new file mode 100644
index 0000000000..c2a91dcf6b
--- /dev/null
+++ b/widget/windows/nsWindowGfx.cpp
@@ -0,0 +1,718 @@
+/* -*- 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/. */
+
+/*
+ * nsWindowGfx - Painting and aceleration.
+ */
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Includes
+ **
+ ** Include headers.
+ **
+ **************************************************************
+ **************************************************************/
+
+#include "mozilla/dom/ContentParent.h"
+
+#include "nsWindowGfx.h"
+#include "nsAppRunner.h"
+#include <windows.h>
+#include <shellapi.h>
+#include "gfxEnv.h"
+#include "gfxImageSurface.h"
+#include "gfxUtils.h"
+#include "gfxConfig.h"
+#include "gfxWindowsSurface.h"
+#include "gfxWindowsPlatform.h"
+#include "gfxDWriteFonts.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/Tools.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "nsGfxCIID.h"
+#include "gfxContext.h"
+#include "WinUtils.h"
+#include "WinWindowOcclusionTracker.h"
+#include "nsIWidgetListener.h"
+#include "mozilla/Unused.h"
+#include "nsDebug.h"
+#include "WindowRenderer.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "InProcessWinCompositorWidget.h"
+
+#include "nsUXThemeData.h"
+#include "nsUXThemeConstants.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+using namespace mozilla::plugins;
+extern mozilla::LazyLogModule gWindowsLog;
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: Variables
+ **
+ ** nsWindow Class static initializations and global variables.
+ **
+ **************************************************************
+ **************************************************************/
+
+/**************************************************************
+ *
+ * SECTION: nsWindow statics
+ *
+ **************************************************************/
+
+struct IconMetrics {
+ int32_t xMetric;
+ int32_t yMetric;
+ int32_t defaultSize;
+};
+
+// Corresponds 1:1 to the IconSizeType enum
+static IconMetrics sIconMetrics[] = {
+ {SM_CXSMICON, SM_CYSMICON, 16}, // small icon
+ {SM_CXICON, SM_CYICON, 32} // regular icon
+};
+
+/**************************************************************
+ **************************************************************
+ **
+ ** BLOCK: nsWindow impl.
+ **
+ ** Paint related nsWindow methods.
+ **
+ **************************************************************
+ **************************************************************/
+
+// GetRegionToPaint returns the invalidated region that needs to be painted
+LayoutDeviceIntRegion nsWindow::GetRegionToPaint(bool aForceFullRepaint,
+ PAINTSTRUCT ps, HDC aDC) {
+ if (aForceFullRepaint) {
+ RECT paintRect;
+ ::GetClientRect(mWnd, &paintRect);
+ return LayoutDeviceIntRegion(WinUtils::ToIntRect(paintRect));
+ }
+
+ HRGN paintRgn = ::CreateRectRgn(0, 0, 0, 0);
+ if (paintRgn != nullptr) {
+ int result = GetRandomRgn(aDC, paintRgn, SYSRGN);
+ if (result == 1) {
+ POINT pt = {0, 0};
+ ::MapWindowPoints(nullptr, mWnd, &pt, 1);
+ ::OffsetRgn(paintRgn, pt.x, pt.y);
+ }
+ LayoutDeviceIntRegion rgn(WinUtils::ConvertHRGNToRegion(paintRgn));
+ ::DeleteObject(paintRgn);
+ return rgn;
+ }
+ return LayoutDeviceIntRegion(WinUtils::ToIntRect(ps.rcPaint));
+}
+
+nsIWidgetListener* nsWindow::GetPaintListener() {
+ if (mDestroyCalled) return nullptr;
+ return mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener;
+}
+
+void nsWindow::ForcePresent() {
+ if (mResizeState != RESIZING) {
+ if (CompositorBridgeChild* remoteRenderer = GetRemoteRenderer()) {
+ remoteRenderer->SendForcePresent(wr::RenderReasons::WIDGET);
+ }
+ }
+}
+
+bool nsWindow::OnPaint(uint32_t aNestingLevel) {
+ DeviceResetReason resetReason = DeviceResetReason::OK;
+ if (gfxWindowsPlatform::GetPlatform()->DidRenderingDeviceReset(
+ &resetReason)) {
+ gfxCriticalNote << "(nsWindow) Detected device reset: " << (int)resetReason;
+
+ gfxWindowsPlatform::GetPlatform()->UpdateRenderMode();
+
+ bool guilty;
+ switch (resetReason) {
+ case DeviceResetReason::HUNG:
+ case DeviceResetReason::RESET:
+ case DeviceResetReason::INVALID_CALL:
+ guilty = true;
+ break;
+ default:
+ guilty = false;
+ break;
+ }
+
+ GPUProcessManager::Get()->OnInProcessDeviceReset(guilty);
+
+ gfxCriticalNote << "(nsWindow) Finished device reset.";
+ return false;
+ }
+
+ PAINTSTRUCT ps;
+
+ // Avoid starting the GPU process for the initial navigator:blank window.
+ if (mIsEarlyBlankWindow) {
+ // Call BeginPaint/EndPaint or Windows will keep sending us messages.
+ ::BeginPaint(mWnd, &ps);
+ ::EndPaint(mWnd, &ps);
+ return true;
+ }
+
+ WindowRenderer* renderer = GetWindowRenderer();
+ KnowsCompositor* knowsCompositor = renderer->AsKnowsCompositor();
+ WebRenderLayerManager* layerManager = renderer->AsWebRender();
+
+ if (mClearNCEdge) {
+ // We need to clear this edge of the non-client region to black (once).
+ HDC hdc;
+ RECT rect;
+ hdc = ::GetWindowDC(mWnd);
+ ::GetWindowRect(mWnd, &rect);
+ ::MapWindowPoints(nullptr, mWnd, (LPPOINT)&rect, 2);
+ switch (mClearNCEdge.value()) {
+ case ABE_TOP:
+ rect.bottom = rect.top + kHiddenTaskbarSize;
+ break;
+ case ABE_LEFT:
+ rect.right = rect.left + kHiddenTaskbarSize;
+ break;
+ case ABE_BOTTOM:
+ rect.top = rect.bottom - kHiddenTaskbarSize;
+ break;
+ case ABE_RIGHT:
+ rect.left = rect.right - kHiddenTaskbarSize;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid edge value");
+ break;
+ }
+ ::FillRect(hdc, &rect,
+ reinterpret_cast<HBRUSH>(::GetStockObject(BLACK_BRUSH)));
+ ::ReleaseDC(mWnd, hdc);
+
+ mClearNCEdge.reset();
+ }
+
+ if (knowsCompositor && layerManager &&
+ !mBounds.IsEqualEdges(mLastPaintBounds)) {
+ // Do an early async composite so that we at least have something on the
+ // screen in the right place, even if the content is out of date.
+ layerManager->ScheduleComposite(wr::RenderReasons::WIDGET);
+ }
+ mLastPaintBounds = mBounds;
+
+ // For layered translucent windows all drawing should go to memory DC and no
+ // WM_PAINT messages are normally generated. To support asynchronous painting
+ // we force generation of WM_PAINT messages by invalidating window areas with
+ // RedrawWindow, InvalidateRect or InvalidateRgn function calls.
+ const bool usingMemoryDC =
+ renderer->GetBackendType() == LayersBackend::LAYERS_NONE &&
+ mTransparencyMode == TransparencyMode::Transparent;
+
+ HDC hDC = nullptr;
+ if (usingMemoryDC) {
+ // BeginPaint/EndPaint must be called to make Windows think that invalid
+ // area is painted. Otherwise it will continue sending the same message
+ // endlessly.
+ ::BeginPaint(mWnd, &ps);
+ ::EndPaint(mWnd, &ps);
+
+ // We're guaranteed to have a widget proxy since we called
+ // GetLayerManager().
+ hDC = mBasicLayersSurface->GetTransparentDC();
+ } else {
+ hDC = ::BeginPaint(mWnd, &ps);
+ }
+
+ const bool forceRepaint = mTransparencyMode == TransparencyMode::Transparent;
+ const LayoutDeviceIntRegion region = GetRegionToPaint(forceRepaint, ps, hDC);
+
+ if (knowsCompositor && layerManager) {
+ // We need to paint to the screen even if nothing changed, since if we
+ // don't have a compositing window manager, our pixels could be stale.
+ layerManager->SetNeedsComposite(true);
+ layerManager->SendInvalidRegion(region.ToUnknownRegion());
+ }
+
+ RefPtr<nsWindow> strongThis(this);
+
+ nsIWidgetListener* listener = GetPaintListener();
+ if (listener) {
+ listener->WillPaintWindow(this);
+ }
+ // Re-get the listener since the will paint notification may have killed it.
+ listener = GetPaintListener();
+ if (!listener) {
+ return false;
+ }
+
+ if (knowsCompositor && layerManager && layerManager->NeedsComposite()) {
+ layerManager->ScheduleComposite(wr::RenderReasons::WIDGET);
+ layerManager->SetNeedsComposite(false);
+ }
+
+ bool result = true;
+ if (!region.IsEmpty() && listener) {
+ // Should probably pass in a real region here, using GetRandomRgn
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/clipping_4q0e.asp
+
+#ifdef WIDGET_DEBUG_OUTPUT
+ debug_DumpPaintEvent(stdout, this, region.ToUnknownRegion(), "noname",
+ (int32_t)mWnd);
+#endif // WIDGET_DEBUG_OUTPUT
+
+ switch (renderer->GetBackendType()) {
+ case LayersBackend::LAYERS_NONE: {
+ RefPtr<gfxASurface> targetSurface;
+
+ // don't support transparency for non-GDI rendering, for now
+ if (TransparencyMode::Transparent == mTransparencyMode) {
+ // This mutex needs to be held when EnsureTransparentSurface is
+ // called.
+ MutexAutoLock lock(mBasicLayersSurface->GetTransparentSurfaceLock());
+ targetSurface = mBasicLayersSurface->EnsureTransparentSurface();
+ }
+
+ RefPtr<gfxWindowsSurface> targetSurfaceWin;
+ if (!targetSurface) {
+ uint32_t flags = (mTransparencyMode == TransparencyMode::Opaque)
+ ? 0
+ : gfxWindowsSurface::FLAG_IS_TRANSPARENT;
+ targetSurfaceWin = new gfxWindowsSurface(hDC, flags);
+ targetSurface = targetSurfaceWin;
+ }
+
+ if (!targetSurface) {
+ NS_ERROR("Invalid RenderMode!");
+ return false;
+ }
+
+ RECT paintRect;
+ ::GetClientRect(mWnd, &paintRect);
+ RefPtr<DrawTarget> dt = gfxPlatform::CreateDrawTargetForSurface(
+ targetSurface, IntSize(paintRect.right - paintRect.left,
+ paintRect.bottom - paintRect.top));
+ if (!dt || !dt->IsValid()) {
+ gfxWarning()
+ << "nsWindow::OnPaint failed in CreateDrawTargetForSurface";
+ return false;
+ }
+
+ // don't need to double buffer with anything but GDI
+ BufferMode doubleBuffering = mozilla::layers::BufferMode::BUFFER_NONE;
+ switch (mTransparencyMode) {
+ case TransparencyMode::Transparent:
+ // If we're rendering with translucency, we're going to be
+ // rendering the whole window; make sure we clear it first
+ dt->ClearRect(
+ Rect(0.f, 0.f, dt->GetSize().width, dt->GetSize().height));
+ break;
+ default:
+ // If we're not doing translucency, then double buffer
+ doubleBuffering = mozilla::layers::BufferMode::BUFFERED;
+ break;
+ }
+
+ gfxContext thebesContext(dt);
+
+ {
+ AutoLayerManagerSetup setupLayerManager(this, &thebesContext,
+ doubleBuffering);
+ result = listener->PaintWindow(this, region);
+ }
+
+ if (TransparencyMode::Transparent == mTransparencyMode) {
+ // Data from offscreen drawing surface was copied to memory bitmap of
+ // transparent bitmap. Now it can be read from memory bitmap to apply
+ // alpha channel and after that displayed on the screen.
+ mBasicLayersSurface->RedrawTransparentWindow();
+ }
+ } break;
+ case LayersBackend::LAYERS_WR: {
+ result = listener->PaintWindow(this, region);
+ if (!gfxEnv::MOZ_DISABLE_FORCE_PRESENT()) {
+ nsCOMPtr<nsIRunnable> event = NewRunnableMethod(
+ "nsWindow::ForcePresent", this, &nsWindow::ForcePresent);
+ NS_DispatchToMainThread(event);
+ }
+ } break;
+ default:
+ NS_ERROR("Unknown layers backend used!");
+ break;
+ }
+ }
+
+ if (!usingMemoryDC) {
+ ::EndPaint(mWnd, &ps);
+ }
+
+ mLastPaintEndTime = TimeStamp::Now();
+
+ // Re-get the listener since painting may have killed it.
+ listener = GetPaintListener();
+ if (listener) listener->DidPaintWindow();
+
+ if (aNestingLevel == 0 && ::GetUpdateRect(mWnd, nullptr, false)) {
+ OnPaint(1);
+ }
+
+ return result;
+}
+
+bool nsWindow::NeedsToTrackWindowOcclusionState() {
+ if (!WinWindowOcclusionTracker::Get()) {
+ return false;
+ }
+
+ if (mCompositorSession && mWindowType == WindowType::TopLevel) {
+ return true;
+ }
+
+ return false;
+}
+
+void nsWindow::NotifyOcclusionState(mozilla::widget::OcclusionState aState) {
+ MOZ_ASSERT(NeedsToTrackWindowOcclusionState());
+
+ bool isFullyOccluded = aState == mozilla::widget::OcclusionState::OCCLUDED;
+ // When window is minimized, it is not set as fully occluded.
+ if (mFrameState->GetSizeMode() == nsSizeMode_Minimized) {
+ isFullyOccluded = false;
+ }
+
+ // Don't dispatch if the new occlustion state is the same as the current
+ // state.
+ if (mIsFullyOccluded == isFullyOccluded) {
+ return;
+ }
+
+ mIsFullyOccluded = isFullyOccluded;
+
+ MOZ_LOG(gWindowsLog, LogLevel::Info,
+ ("nsWindow::NotifyOcclusionState() mIsFullyOccluded %d "
+ "mFrameState->GetSizeMode() %d",
+ mIsFullyOccluded, mFrameState->GetSizeMode()));
+
+ wr::DebugFlags flags{0};
+ flags._0 = gfx::gfxVars::WebRenderDebugFlags();
+ bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG);
+ if (debugEnabled && mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyVisibilityUpdated(
+ mFrameState->GetSizeMode(), mIsFullyOccluded);
+ }
+
+ if (mWidgetListener) {
+ mWidgetListener->OcclusionStateChanged(mIsFullyOccluded);
+ }
+}
+
+void nsWindow::MaybeEnableWindowOcclusion(bool aEnable) {
+ // WindowOcclusion is enabled/disabled only when compositor session exists.
+ // See nsWindow::NeedsToTrackWindowOcclusionState().
+ if (!mCompositorSession) {
+ return;
+ }
+
+ bool enabled = gfxConfig::IsEnabled(gfx::Feature::WINDOW_OCCLUSION);
+
+ if (aEnable) {
+ // Enable window occlusion.
+ if (enabled && NeedsToTrackWindowOcclusionState()) {
+ WinWindowOcclusionTracker::Get()->Enable(this, mWnd);
+
+ wr::DebugFlags flags{0};
+ flags._0 = gfx::gfxVars::WebRenderDebugFlags();
+ bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG);
+ if (debugEnabled && mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyVisibilityUpdated(
+ mFrameState->GetSizeMode(), mIsFullyOccluded);
+ }
+ }
+ return;
+ }
+
+ // Disable window occlusion.
+ MOZ_ASSERT(!aEnable);
+
+ if (!NeedsToTrackWindowOcclusionState()) {
+ return;
+ }
+
+ WinWindowOcclusionTracker::Get()->Disable(this, mWnd);
+ NotifyOcclusionState(OcclusionState::VISIBLE);
+
+ wr::DebugFlags flags{0};
+ flags._0 = gfx::gfxVars::WebRenderDebugFlags();
+ bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG);
+ if (debugEnabled && mCompositorWidgetDelegate) {
+ mCompositorWidgetDelegate->NotifyVisibilityUpdated(
+ mFrameState->GetSizeMode(), mIsFullyOccluded);
+ }
+}
+
+// This override of CreateCompositor is to add support for sending the IPC
+// call for RequesetFxrOutput as soon as the compositor for this widget is
+// available.
+void nsWindow::CreateCompositor() {
+ nsBaseWidget::CreateCompositor();
+
+ MaybeEnableWindowOcclusion(/* aEnable */ true);
+
+ if (mRequestFxrOutputPending) {
+ GetRemoteRenderer()->SendRequestFxrOutput();
+ }
+}
+
+void nsWindow::DestroyCompositor() {
+ MaybeEnableWindowOcclusion(/* aEnable */ false);
+
+ nsBaseWidget::DestroyCompositor();
+}
+
+void nsWindow::RequestFxrOutput() {
+ if (GetRemoteRenderer() != nullptr) {
+ MOZ_CRASH("RequestFxrOutput should happen before Compositor is created.");
+ } else {
+ // The compositor isn't ready, so indicate to make the IPC call when
+ // it is available.
+ mRequestFxrOutputPending = true;
+ }
+}
+
+LayoutDeviceIntSize nsWindowGfx::GetIconMetrics(IconSizeType aSizeType) {
+ int32_t width = ::GetSystemMetrics(sIconMetrics[aSizeType].xMetric);
+ int32_t height = ::GetSystemMetrics(sIconMetrics[aSizeType].yMetric);
+
+ if (width == 0 || height == 0) {
+ width = height = sIconMetrics[aSizeType].defaultSize;
+ }
+
+ return LayoutDeviceIntSize(width, height);
+}
+
+nsresult nsWindowGfx::CreateIcon(imgIContainer* aContainer, bool aIsCursor,
+ LayoutDeviceIntPoint aHotspot,
+ LayoutDeviceIntSize aScaledSize,
+ HICON* aIcon) {
+ MOZ_ASSERT(aHotspot.x >= 0 && aHotspot.y >= 0);
+ MOZ_ASSERT((aScaledSize.width > 0 && aScaledSize.height > 0) ||
+ (aScaledSize.width == 0 && aScaledSize.height == 0));
+
+ // Get the image data
+ RefPtr<SourceSurface> surface = aContainer->GetFrame(
+ imgIContainer::FRAME_CURRENT,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+ NS_ENSURE_TRUE(surface, NS_ERROR_NOT_AVAILABLE);
+
+ IntSize frameSize = surface->GetSize();
+ if (frameSize.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ IntSize iconSize(aScaledSize.width, aScaledSize.height);
+ if (iconSize == IntSize(0, 0)) { // use frame's intrinsic size
+ iconSize = frameSize;
+ }
+
+ RefPtr<DataSourceSurface> dataSurface;
+ bool mappedOK;
+ DataSourceSurface::MappedSurface map;
+
+ if (iconSize != frameSize) {
+ // Scale the surface
+ dataSurface =
+ Factory::CreateDataSourceSurface(iconSize, SurfaceFormat::B8G8R8A8);
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+ mappedOK = dataSurface->Map(DataSourceSurface::MapType::READ_WRITE, &map);
+ NS_ENSURE_TRUE(mappedOK, NS_ERROR_FAILURE);
+
+ RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
+ BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride,
+ SurfaceFormat::B8G8R8A8);
+ if (!dt) {
+ gfxWarning()
+ << "nsWindowGfx::CreatesIcon failed in CreateDrawTargetForData";
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ dt->DrawSurface(surface, Rect(0, 0, iconSize.width, iconSize.height),
+ Rect(0, 0, frameSize.width, frameSize.height),
+ DrawSurfaceOptions(),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+ } else if (surface->GetFormat() != SurfaceFormat::B8G8R8A8) {
+ // Convert format to SurfaceFormat::B8G8R8A8
+ dataSurface = gfxUtils::CopySurfaceToDataSourceSurfaceWithFormat(
+ surface, SurfaceFormat::B8G8R8A8);
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+ mappedOK = dataSurface->Map(DataSourceSurface::MapType::READ, &map);
+ } else {
+ dataSurface = surface->GetDataSurface();
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+ mappedOK = dataSurface->Map(DataSourceSurface::MapType::READ, &map);
+ }
+ NS_ENSURE_TRUE(dataSurface && mappedOK, NS_ERROR_FAILURE);
+ MOZ_ASSERT(dataSurface->GetFormat() == SurfaceFormat::B8G8R8A8);
+
+ uint8_t* data = nullptr;
+ UniquePtr<uint8_t[]> autoDeleteArray;
+ if (map.mStride == BytesPerPixel(dataSurface->GetFormat()) * iconSize.width) {
+ // Mapped data is already packed
+ data = map.mData;
+ } else {
+ // We can't use map.mData since the pixels are not packed (as required by
+ // CreateDIBitmap, which is called under the DataToBitmap call below).
+ //
+ // We must unmap before calling SurfaceToPackedBGRA because it needs access
+ // to the pixel data.
+ dataSurface->Unmap();
+ map.mData = nullptr;
+
+ autoDeleteArray = SurfaceToPackedBGRA(dataSurface);
+ data = autoDeleteArray.get();
+ NS_ENSURE_TRUE(data, NS_ERROR_FAILURE);
+ }
+
+ HBITMAP bmp = DataToBitmap(data, iconSize.width, -iconSize.height, 32);
+ uint8_t* a1data = Data32BitTo1Bit(data, iconSize.width, iconSize.height);
+ if (map.mData) {
+ dataSurface->Unmap();
+ }
+ if (!a1data) {
+ return NS_ERROR_FAILURE;
+ }
+
+ HBITMAP mbmp = DataToBitmap(a1data, iconSize.width, -iconSize.height, 1);
+ free(a1data);
+
+ ICONINFO info = {0};
+ info.fIcon = !aIsCursor;
+ info.xHotspot = aHotspot.x;
+ info.yHotspot = aHotspot.y;
+ info.hbmMask = mbmp;
+ info.hbmColor = bmp;
+
+ HCURSOR icon = ::CreateIconIndirect(&info);
+ ::DeleteObject(mbmp);
+ ::DeleteObject(bmp);
+ if (!icon) return NS_ERROR_FAILURE;
+ *aIcon = icon;
+ return NS_OK;
+}
+
+// Adjust cursor image data
+uint8_t* nsWindowGfx::Data32BitTo1Bit(uint8_t* aImageData, uint32_t aWidth,
+ uint32_t aHeight) {
+ // We need (aWidth + 7) / 8 bytes plus zero-padding up to a multiple of
+ // 4 bytes for each row (HBITMAP requirement). Bug 353553.
+ uint32_t outBpr = ((aWidth + 31) / 8) & ~3;
+
+ // Allocate and clear mask buffer
+ uint8_t* outData = (uint8_t*)calloc(outBpr, aHeight);
+ if (!outData) return nullptr;
+
+ int32_t* imageRow = (int32_t*)aImageData;
+ for (uint32_t curRow = 0; curRow < aHeight; curRow++) {
+ uint8_t* outRow = outData + curRow * outBpr;
+ uint8_t mask = 0x80;
+ for (uint32_t curCol = 0; curCol < aWidth; curCol++) {
+ // Use sign bit to test for transparency, as alpha byte is highest byte
+ if (*imageRow++ < 0) *outRow |= mask;
+
+ mask >>= 1;
+ if (!mask) {
+ outRow++;
+ mask = 0x80;
+ }
+ }
+ }
+
+ return outData;
+}
+
+/**
+ * Convert the given image data to a HBITMAP. If the requested depth is
+ * 32 bit, a bitmap with an alpha channel will be returned.
+ *
+ * @param aImageData The image data to convert. Must use the format accepted
+ * by CreateDIBitmap.
+ * @param aWidth With of the bitmap, in pixels.
+ * @param aHeight Height of the image, in pixels.
+ * @param aDepth Image depth, in bits. Should be one of 1, 24 and 32.
+ *
+ * @return The HBITMAP representing the image. Caller should call
+ * DeleteObject when done with the bitmap.
+ * On failure, nullptr will be returned.
+ */
+HBITMAP nsWindowGfx::DataToBitmap(uint8_t* aImageData, uint32_t aWidth,
+ uint32_t aHeight, uint32_t aDepth) {
+ HDC dc = ::GetDC(nullptr);
+
+ if (aDepth == 32) {
+ // Alpha channel. We need the new header.
+ BITMAPV4HEADER head = {0};
+ head.bV4Size = sizeof(head);
+ head.bV4Width = aWidth;
+ head.bV4Height = aHeight;
+ head.bV4Planes = 1;
+ head.bV4BitCount = aDepth;
+ head.bV4V4Compression = BI_BITFIELDS;
+ head.bV4SizeImage = 0; // Uncompressed
+ head.bV4XPelsPerMeter = 0;
+ head.bV4YPelsPerMeter = 0;
+ head.bV4ClrUsed = 0;
+ head.bV4ClrImportant = 0;
+
+ head.bV4RedMask = 0x00FF0000;
+ head.bV4GreenMask = 0x0000FF00;
+ head.bV4BlueMask = 0x000000FF;
+ head.bV4AlphaMask = 0xFF000000;
+
+ HBITMAP bmp = ::CreateDIBitmap(
+ dc, reinterpret_cast<CONST BITMAPINFOHEADER*>(&head), CBM_INIT,
+ aImageData, reinterpret_cast<CONST BITMAPINFO*>(&head), DIB_RGB_COLORS);
+ ::ReleaseDC(nullptr, dc);
+ return bmp;
+ }
+
+ char reserved_space[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 2];
+ BITMAPINFOHEADER& head = *(BITMAPINFOHEADER*)reserved_space;
+
+ head.biSize = sizeof(BITMAPINFOHEADER);
+ head.biWidth = aWidth;
+ head.biHeight = aHeight;
+ head.biPlanes = 1;
+ head.biBitCount = (WORD)aDepth;
+ head.biCompression = BI_RGB;
+ head.biSizeImage = 0; // Uncompressed
+ head.biXPelsPerMeter = 0;
+ head.biYPelsPerMeter = 0;
+ head.biClrUsed = 0;
+ head.biClrImportant = 0;
+
+ BITMAPINFO& bi = *(BITMAPINFO*)reserved_space;
+
+ if (aDepth == 1) {
+ RGBQUAD black = {0, 0, 0, 0};
+ RGBQUAD white = {255, 255, 255, 0};
+
+ bi.bmiColors[0] = white;
+ bi.bmiColors[1] = black;
+ }
+
+ HBITMAP bmp =
+ ::CreateDIBitmap(dc, &head, CBM_INIT, aImageData, &bi, DIB_RGB_COLORS);
+ ::ReleaseDC(nullptr, dc);
+ return bmp;
+}
diff --git a/widget/windows/nsWindowGfx.h b/widget/windows/nsWindowGfx.h
new file mode 100644
index 0000000000..0d5fd9e01a
--- /dev/null
+++ b/widget/windows/nsWindowGfx.h
@@ -0,0 +1,35 @@
+/* -*- 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/. */
+
+#ifndef WindowGfx_h__
+#define WindowGfx_h__
+
+/*
+ * nsWindowGfx - Painting and aceleration.
+ */
+
+#include "nsWindow.h"
+#include <imgIContainer.h>
+
+class nsWindowGfx {
+ public:
+ enum IconSizeType { kSmallIcon, kRegularIcon };
+ static mozilla::LayoutDeviceIntSize GetIconMetrics(IconSizeType aSizeType);
+ static nsresult CreateIcon(imgIContainer* aContainer, bool aIsCursor,
+ mozilla::LayoutDeviceIntPoint aHotspot,
+ mozilla::LayoutDeviceIntSize aScaledSize,
+ HICON* aIcon);
+
+ private:
+ /**
+ * Cursor helpers
+ */
+ static uint8_t* Data32BitTo1Bit(uint8_t* aImageData, uint32_t aWidth,
+ uint32_t aHeight);
+ static HBITMAP DataToBitmap(uint8_t* aImageData, uint32_t aWidth,
+ uint32_t aHeight, uint32_t aDepth);
+};
+
+#endif // WindowGfx_h__
diff --git a/widget/windows/nsWindowLoggedMessages.cpp b/widget/windows/nsWindowLoggedMessages.cpp
new file mode 100644
index 0000000000..ac0f05a875
--- /dev/null
+++ b/widget/windows/nsWindowLoggedMessages.cpp
@@ -0,0 +1,307 @@
+/* -*- 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 <windef.h>
+#include <winuser.h>
+#include "mozilla/StaticPrefs_storage.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsWindowLoggedMessages.h"
+#include "nsWindow.h"
+#include "WinUtils.h"
+#include <map>
+#include <algorithm>
+
+namespace mozilla::widget {
+
+// NCCALCSIZE_PARAMS and WINDOWPOS are relatively large structures, so store
+// them as a pointer to save memory
+using NcCalcSizeVariantData =
+ Variant<UniquePtr<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>>, RECT>;
+// to save memory, hold the raw data and only convert to string
+// when requested
+using MessageSpecificData =
+ Variant<std::pair<WPARAM, LPARAM>, // WM_SIZE, WM_MOVE
+ WINDOWPOS, // WM_WINDOWPOSCHANGING, WM_WINDOWPOSCHANGED
+ std::pair<WPARAM, RECT>, // WM_SIZING, WM_DPICHANGED, WM_MOVING
+ std::pair<WPARAM, nsString>, // WM_SETTINGCHANGE
+ std::pair<bool, NcCalcSizeVariantData>, // WM_NCCALCSIZE
+ MINMAXINFO // WM_GETMINMAXINFO
+ >;
+
+struct WindowMessageData {
+ long mEventCounter;
+ bool mIsPreEvent;
+ MessageSpecificData mSpecificData;
+ mozilla::Maybe<bool> mResult;
+ LRESULT mRetValue;
+ WindowMessageData(long eventCounter, bool isPreEvent,
+ MessageSpecificData&& specificData,
+ mozilla::Maybe<bool> result, LRESULT retValue)
+ : mEventCounter(eventCounter),
+ mIsPreEvent(isPreEvent),
+ mSpecificData(std::move(specificData)),
+ mResult(result),
+ mRetValue(retValue) {}
+ // Disallow copy constructor/operator since MessageSpecificData has a
+ // UniquePtr
+ WindowMessageData(const WindowMessageData&) = delete;
+ WindowMessageData& operator=(const WindowMessageData&) = delete;
+ WindowMessageData(WindowMessageData&&) = default;
+ WindowMessageData& operator=(WindowMessageData&&) = default;
+};
+
+struct WindowMessageDataSortKey {
+ long mEventCounter;
+ bool mIsPreEvent;
+ explicit WindowMessageDataSortKey(const WindowMessageData& data)
+ : mEventCounter(data.mEventCounter), mIsPreEvent(data.mIsPreEvent) {}
+ bool operator<(const WindowMessageDataSortKey& other) const {
+ if (mEventCounter < other.mEventCounter) {
+ return true;
+ }
+ if (other.mEventCounter < mEventCounter) {
+ return false;
+ }
+ if (mIsPreEvent && !other.mIsPreEvent) {
+ return true;
+ }
+ if (other.mIsPreEvent && !mIsPreEvent) {
+ return false;
+ }
+ // they're equal
+ return false;
+ }
+};
+
+struct CircularMessageBuffer {
+ // Only used when the vector is at its maximum size
+ size_t mNextFreeIndex = 0;
+ std::vector<WindowMessageData> mMessages;
+};
+static std::map<HWND, std::map<UINT, CircularMessageBuffer>> gWindowMessages;
+
+static HWND GetHwndFromWidget(nsIWidget* windowWidget) {
+ nsWindow* window = static_cast<nsWindow*>(windowWidget);
+ return window->GetWindowHandle();
+}
+
+MessageSpecificData MakeMessageSpecificData(UINT event, WPARAM wParam,
+ LPARAM lParam) {
+ // Since we store this data for every message we log, make sure it's of a
+ // reasonable size. Keep in mind we're storing up to 10 (number of message
+ // types)
+ // * 6 (default number of messages per type to keep) of these messages per
+ // window.
+ static_assert(sizeof(MessageSpecificData) <= 48);
+ switch (event) {
+ case WM_SIZE:
+ case WM_MOVE:
+ return MessageSpecificData(std::make_pair(wParam, lParam));
+ case WM_WINDOWPOSCHANGING:
+ case WM_WINDOWPOSCHANGED: {
+ LPWINDOWPOS windowPosPtr = reinterpret_cast<LPWINDOWPOS>(lParam);
+ WINDOWPOS windowPos = *windowPosPtr;
+ return MessageSpecificData(std::move(windowPos));
+ }
+ case WM_SIZING:
+ case WM_DPICHANGED:
+ case WM_MOVING: {
+ LPRECT rectPtr = reinterpret_cast<LPRECT>(lParam);
+ RECT rect = *rectPtr;
+ return MessageSpecificData(std::make_pair(wParam, std::move(rect)));
+ }
+ case WM_SETTINGCHANGE: {
+ LPCWSTR wideStrPtr = reinterpret_cast<LPCWSTR>(lParam);
+ nsString str(wideStrPtr);
+ return MessageSpecificData(std::make_pair(wParam, std::move(str)));
+ }
+ case WM_NCCALCSIZE: {
+ bool shouldIndicateValidArea = wParam == TRUE;
+ if (shouldIndicateValidArea) {
+ LPNCCALCSIZE_PARAMS ncCalcSizeParamsPtr =
+ reinterpret_cast<LPNCCALCSIZE_PARAMS>(lParam);
+ NCCALCSIZE_PARAMS ncCalcSizeParams = *ncCalcSizeParamsPtr;
+ WINDOWPOS windowPos = *ncCalcSizeParams.lppos;
+ UniquePtr<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>> ncCalcSizeData =
+ MakeUnique<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>>(
+ std::pair(std::move(ncCalcSizeParams), std::move(windowPos)));
+ return MessageSpecificData(
+ std::make_pair(shouldIndicateValidArea,
+ NcCalcSizeVariantData(std::move(ncCalcSizeData))));
+ } else {
+ LPRECT rectPtr = reinterpret_cast<LPRECT>(lParam);
+ RECT rect = *rectPtr;
+ return MessageSpecificData(std::make_pair(
+ shouldIndicateValidArea, NcCalcSizeVariantData(std::move(rect))));
+ }
+ }
+ case WM_GETMINMAXINFO: {
+ PMINMAXINFO minMaxInfoPtr = reinterpret_cast<PMINMAXINFO>(lParam);
+ MINMAXINFO minMaxInfo = *minMaxInfoPtr;
+ return MessageSpecificData(std::move(minMaxInfo));
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Unhandled message type in MakeMessageSpecificData");
+ return MessageSpecificData(std::make_pair(wParam, lParam));
+ }
+}
+
+void AppendFriendlyMessageSpecificData(nsCString& str, UINT event,
+ bool isPreEvent,
+ const MessageSpecificData& data) {
+ switch (event) {
+ case WM_SIZE: {
+ const auto& params = data.as<std::pair<WPARAM, LPARAM>>();
+ nsAutoCString tempStr =
+ WmSizeParamInfo(params.first, params.second, isPreEvent);
+ str.AppendASCII(tempStr);
+ break;
+ }
+ case WM_MOVE: {
+ const auto& params = data.as<std::pair<WPARAM, LPARAM>>();
+ XLowWordYHighWordParamInfo(str, params.second, "upperLeft", isPreEvent);
+ break;
+ }
+ case WM_WINDOWPOSCHANGING:
+ case WM_WINDOWPOSCHANGED: {
+ const auto& params = data.as<WINDOWPOS>();
+ WindowPosParamInfo(str, reinterpret_cast<uint64_t>(&params),
+ "newSizeAndPos", isPreEvent);
+ break;
+ }
+ case WM_SIZING: {
+ const auto& params = data.as<std::pair<WPARAM, RECT>>();
+ WindowEdgeParamInfo(str, params.first, "edge", isPreEvent);
+ str.AppendASCII(" ");
+ RectParamInfo(str, reinterpret_cast<uint64_t>(&params.second), "rect",
+ isPreEvent);
+ break;
+ }
+ case WM_DPICHANGED: {
+ const auto& params = data.as<std::pair<WPARAM, RECT>>();
+ XLowWordYHighWordParamInfo(str, params.first, "newDPI", isPreEvent);
+ str.AppendASCII(" ");
+ RectParamInfo(str, reinterpret_cast<uint64_t>(&params.second),
+ "suggestedSizeAndPos", isPreEvent);
+ break;
+ }
+ case WM_MOVING: {
+ const auto& params = data.as<std::pair<WPARAM, RECT>>();
+ RectParamInfo(str, reinterpret_cast<uint64_t>(&params.second), "rect",
+ isPreEvent);
+ break;
+ }
+ case WM_SETTINGCHANGE: {
+ const auto& params = data.as<std::pair<WPARAM, nsString>>();
+ UiActionParamInfo(str, params.first, "uiAction", isPreEvent);
+ str.AppendASCII(" ");
+ WideStringParamInfo(
+ str,
+ reinterpret_cast<uint64_t>((const wchar_t*)(params.second.Data())),
+ "paramChanged", isPreEvent);
+ break;
+ }
+ case WM_NCCALCSIZE: {
+ const auto& params = data.as<std::pair<bool, NcCalcSizeVariantData>>();
+ bool shouldIndicateValidArea = params.first;
+ if (shouldIndicateValidArea) {
+ const auto& validAreaParams =
+ params.second
+ .as<UniquePtr<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>>>();
+ // Make pointer point to the cached data
+ validAreaParams->first.lppos = &validAreaParams->second;
+ nsAutoCString tempStr = WmNcCalcSizeParamInfo(
+ TRUE, reinterpret_cast<uint64_t>(&validAreaParams->first),
+ isPreEvent);
+ str.AppendASCII(tempStr);
+ } else {
+ RECT rect = params.second.as<RECT>();
+ nsAutoCString tempStr = WmNcCalcSizeParamInfo(
+ FALSE, reinterpret_cast<uint64_t>(&rect), isPreEvent);
+ str.AppendASCII(tempStr);
+ }
+ break;
+ }
+ case WM_GETMINMAXINFO: {
+ const auto& params = data.as<MINMAXINFO>();
+ MinMaxInfoParamInfo(str, reinterpret_cast<uint64_t>(&params), "",
+ isPreEvent);
+ break;
+ }
+ default:
+ MOZ_ASSERT(false,
+ "Unhandled message type in AppendFriendlyMessageSpecificData");
+ str.AppendASCII("???");
+ }
+}
+
+nsCString MakeFriendlyMessage(UINT event, bool isPreEvent, long eventCounter,
+ const MessageSpecificData& data,
+ mozilla::Maybe<bool> result, LRESULT retValue) {
+ nsCString str;
+ const char* eventName = mozilla::widget::WinUtils::WinEventToEventName(event);
+ MOZ_ASSERT(eventName, "Unknown event name in MakeFriendlyMessage");
+ eventName = eventName ? eventName : "(unknown)";
+ str.AppendPrintf("%6ld %04x (%s) - ", eventCounter, event, eventName);
+ AppendFriendlyMessageSpecificData(str, event, isPreEvent, data);
+ const char* resultMsg =
+ result.isSome() ? (result.value() ? "true" : "false") : "initial call";
+ str.AppendPrintf(" 0x%08llX (%s)",
+ result.isSome() ? static_cast<uint64_t>(retValue) : 0,
+ resultMsg);
+ return str;
+}
+
+void WindowClosed(HWND hwnd) { gWindowMessages.erase(hwnd); }
+
+void LogWindowMessage(HWND hwnd, UINT event, bool isPreEvent, long eventCounter,
+ WPARAM wParam, LPARAM lParam, mozilla::Maybe<bool> result,
+ LRESULT retValue) {
+ auto& hwndMessages = gWindowMessages[hwnd];
+ auto& hwndWindowMessages = hwndMessages[event];
+ WindowMessageData messageData = {
+ eventCounter, isPreEvent, MakeMessageSpecificData(event, wParam, lParam),
+ result, retValue};
+ uint32_t numberOfMessagesToKeep =
+ StaticPrefs::widget_windows_messages_to_log();
+ if (hwndWindowMessages.mMessages.size() < numberOfMessagesToKeep) {
+ // haven't reached limit yet
+ hwndWindowMessages.mMessages.push_back(std::move(messageData));
+ } else {
+ hwndWindowMessages.mMessages[hwndWindowMessages.mNextFreeIndex] =
+ std::move(messageData);
+ }
+ hwndWindowMessages.mNextFreeIndex =
+ (hwndWindowMessages.mNextFreeIndex + 1) % numberOfMessagesToKeep;
+}
+
+void GetLatestWindowMessages(RefPtr<nsIWidget> windowWidget,
+ nsTArray<nsCString>& messages) {
+ HWND hwnd = GetHwndFromWidget(windowWidget);
+ const auto& rawMessages = gWindowMessages[hwnd];
+ nsTArray<std::pair<WindowMessageDataSortKey, nsCString>>
+ sortKeyAndMessageArray;
+ sortKeyAndMessageArray.SetCapacity(
+ rawMessages.size() * StaticPrefs::widget_windows_messages_to_log());
+ for (const auto& eventAndMessage : rawMessages) {
+ for (const auto& messageData : eventAndMessage.second.mMessages) {
+ nsCString message = MakeFriendlyMessage(
+ eventAndMessage.first, messageData.mIsPreEvent,
+ messageData.mEventCounter, messageData.mSpecificData,
+ messageData.mResult, messageData.mRetValue);
+ WindowMessageDataSortKey sortKey(messageData);
+ sortKeyAndMessageArray.AppendElement(
+ std::make_pair(sortKey, std::move(message)));
+ }
+ }
+ std::sort(sortKeyAndMessageArray.begin(), sortKeyAndMessageArray.end());
+ messages.SetCapacity(sortKeyAndMessageArray.Length());
+ for (const std::pair<WindowMessageDataSortKey, nsCString>& entry :
+ sortKeyAndMessageArray) {
+ messages.AppendElement(std::move(entry.second));
+ }
+}
+} // namespace mozilla::widget
diff --git a/widget/windows/nsWindowLoggedMessages.h b/widget/windows/nsWindowLoggedMessages.h
new file mode 100644
index 0000000000..c0ea632bb0
--- /dev/null
+++ b/widget/windows/nsWindowLoggedMessages.h
@@ -0,0 +1,26 @@
+/* -*- 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/. */
+
+#ifndef WindowLoggedMessages_h__
+#define WindowLoggedMessages_h__
+
+#include "minwindef.h"
+#include "wtypes.h"
+
+#include "nsIWidget.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::widget {
+
+void LogWindowMessage(HWND hwnd, UINT event, bool isPreEvent, long eventCounter,
+ WPARAM wParam, LPARAM lParam, mozilla::Maybe<bool> result,
+ LRESULT retValue);
+void WindowClosed(HWND hwnd);
+void GetLatestWindowMessages(RefPtr<nsIWidget> windowWidget,
+ nsTArray<nsCString>& messages);
+
+} // namespace mozilla::widget
+
+#endif /* WindowLoggedMessages */
diff --git a/widget/windows/nsWindowTaskbarConcealer.cpp b/widget/windows/nsWindowTaskbarConcealer.cpp
new file mode 100644
index 0000000000..b2b6b13a1d
--- /dev/null
+++ b/widget/windows/nsWindowTaskbarConcealer.cpp
@@ -0,0 +1,384 @@
+/* -*- 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 "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;
+
+// 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) {
+ 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, uint32_t(hr)));
+ }
+};
+
+/**************************************************************
+ *
+ * SECTION: TaskbarConcealer event callbacks
+ *
+ **************************************************************/
+
+void nsWindow::TaskbarConcealer::OnWindowDestroyed(HWND aWnd) {
+ MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info,
+ ("==> OnWindowDestroyed() for HWND %p", aWnd));
+
+ UpdateAllState(aWnd);
+}
+
+void nsWindow::TaskbarConcealer::OnFocusAcquired(nsWindow* aWin) {
+ // 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) {
+ 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) {
+ // 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::OnAsyncStateUpdateRequest(HWND hwnd) {
+ MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info,
+ ("==> OnAsyncStateUpdateRequest()"));
+
+ // Work around a race condition in explorer.exe.
+ //
+ // When a window is unminimized (and on several other events), the taskbar
+ // receives a notification that it needs to recalculate the current
+ // is-a-fullscreen-window-active-here-state ("rudeness") of each monitor.
+ // Unfortunately, this notification is sent concurrently with the
+ // WM_WINDOWPOSCHANGING message that performs the unminimization.
+ //
+ // Until that message is resolved, the window's position is still "minimized".
+ // If the taskbar processes its notification faster than the window handles
+ // its WM_WINDOWPOSCHANGING message, then the window will appear to the
+ // taskbar to still be minimized, and won't be taken into account for
+ // computing rudeness. This usually presents as a just-unminimized Firefox
+ // fullscreen-window occasionally having the taskbar stuck above it.
+ //
+ // Unfortunately, it's a bit difficult to improve Firefox's speed-of-response
+ // to WM_WINDOWPOSCHANGING messages (we can, and do, execute JavaScript during
+ // these), and even if we could that wouldn't always fix it. We instead adopt
+ // a variant of a strategy by Etienne Duchamps, who has investigated and
+ // documented this issue extensively[0]: we simply send another signal to the
+ // shell to notify it to recalculate the current rudeness state of all
+ // monitors.
+ //
+ // [0]
+ // https://github.com/dechamps/RudeWindowFixer#a-race-condition-activating-a-minimized-window
+ //
+ static UINT const shellHookMsg = ::RegisterWindowMessageW(L"SHELLHOOK");
+ if (shellHookMsg != 0) {
+ // Identifying the particular thread of the particular instance of the
+ // shell associated with our current desktop is probably possible, but
+ // also probably not worth the effort. Just broadcast the message
+ // globally.
+ DWORD info = BSM_APPLICATIONS;
+ ::BroadcastSystemMessage(BSF_POSTMESSAGE | BSF_IGNORECURRENTTASK, &info,
+ shellHookMsg, HSHELL_WINDOWACTIVATED,
+ (LPARAM)hwnd);
+ }
+}
+
+void nsWindow::TaskbarConcealer::OnCloakChanged() {
+ MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("==> OnCloakChanged()"));
+
+ UpdateAllState();
+}
diff --git a/widget/windows/nsWindowTaskbarConcealer.h b/widget/windows/nsWindowTaskbarConcealer.h
new file mode 100644
index 0000000000..84567fc857
--- /dev/null
+++ b/widget/windows/nsWindowTaskbarConcealer.h
@@ -0,0 +1,53 @@
+/* -*- 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/. */
+
+#ifndef WIDGET_WINDOWS_NSWINDOWTASKBARCONCEALER_H_
+#define WIDGET_WINDOWS_NSWINDOWTASKBARCONCEALER_H_
+
+#include "nsWindow.h"
+#include "mozilla/Maybe.h"
+
+/**
+ * nsWindow::TaskbarConcealer
+ *
+ * Fullscreen-state (and, thus, taskbar-occlusion) manager.
+ */
+class nsWindow::TaskbarConcealer {
+ public:
+ // To be called when a window acquires focus. (Note that no action need be
+ // taken when focus is lost.)
+ static void OnFocusAcquired(nsWindow* aWin);
+
+ // To be called during or after a window's destruction. The corresponding
+ // nsWindow pointer is not needed, and will not be acquired or accessed.
+ static void OnWindowDestroyed(HWND aWnd);
+
+ // To be called when the Gecko-fullscreen state of a window changes.
+ static void OnFullscreenChanged(nsWindow* aWin, bool enteredFullscreen);
+
+ // To be called when the position of a window changes. (Performs its own
+ // batching; irrelevant movements will be cheap.)
+ static void OnWindowPosChanged(nsWindow* aWin);
+
+ // To be called when the cloaking state of any window changes. (Expects that
+ // all windows' internal cloaking-state mirror variables are up-to-date.)
+ static void OnCloakChanged();
+
+ // To be called upon receipt of MOZ_WM_FULLSCREEN_STATE_UPDATE.
+ static void OnAsyncStateUpdateRequest(HWND);
+
+ private:
+ static void UpdateAllState(HWND destroyedHwnd = nullptr);
+
+ struct WindowState {
+ HMONITOR monitor;
+ bool isGkFullscreen;
+ };
+ static mozilla::Maybe<WindowState> GetWindowState(HWND);
+
+ static nsTHashMap<HWND, HMONITOR> sKnownWindows;
+};
+
+#endif // WIDGET_WINDOWS_NSWINDOWTASKBARCONCEALER_H_
diff --git a/widget/windows/nsdefs.h b/widget/windows/nsdefs.h
new file mode 100644
index 0000000000..ebf7892fda
--- /dev/null
+++ b/widget/windows/nsdefs.h
@@ -0,0 +1,58 @@
+/* -*- 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/. */
+
+#ifndef NSDEFS_H
+#define NSDEFS_H
+
+#include <windows.h>
+
+#ifdef _DEBUG
+# define BREAK_TO_DEBUGGER DebugBreak()
+#else
+# define BREAK_TO_DEBUGGER
+#endif
+
+#ifdef _DEBUG
+# define VERIFY(exp) \
+ if (!(exp)) { \
+ GetLastError(); \
+ BREAK_TO_DEBUGGER; \
+ }
+#else // !_DEBUG
+# define VERIFY(exp) (exp)
+#endif // !_DEBUG
+
+// inttypes.h-like macro for LONG_PTR/LPARAM formatting.
+#ifdef HAVE_64BIT_BUILD
+# define PRIdLPTR "lld"
+# define PRIxLPTR "llx"
+# define PRIXLPTR "llX"
+#else
+# define PRIdLPTR "ld"
+# define PRIxLPTR "lx"
+# define PRIXLPTR "lX"
+#endif
+
+// Win32 logging modules:
+// nsWindow, nsSound, and nsClipboard
+//
+// Logging can be changed at runtime without recompiling in the General
+// property page of Visual Studio under the "Environment" property.
+//
+// Two variables are of importance to be set:
+// MOZ_LOG and MOZ_LOG_FILE
+//
+// MOZ_LOG:
+// MOZ_LOG=all:5 (To log everything completely)
+// MOZ_LOG=nsWindow:5,nsSound:5,nsClipboard:5
+// (To log windows widget stuff)
+// MOZ_LOG= (To turn off logging)
+//
+// MOZ_LOG_FILE:
+// MOZ_LOG_FILE=C:\log.txt (To a file on disk)
+// MOZ_LOG_FILE=WinDebug (To the debug window)
+// MOZ_LOG_FILE= (To stdout/stderr)
+
+#endif // NSDEFS_H
diff --git a/widget/windows/res/aliasb.cur b/widget/windows/res/aliasb.cur
new file mode 100644
index 0000000000..8d9ac9478c
--- /dev/null
+++ b/widget/windows/res/aliasb.cur
Binary files differ
diff --git a/widget/windows/res/cell.cur b/widget/windows/res/cell.cur
new file mode 100644
index 0000000000..decfbdcac5
--- /dev/null
+++ b/widget/windows/res/cell.cur
Binary files differ
diff --git a/widget/windows/res/col_resize.cur b/widget/windows/res/col_resize.cur
new file mode 100644
index 0000000000..8f7f675122
--- /dev/null
+++ b/widget/windows/res/col_resize.cur
Binary files differ
diff --git a/widget/windows/res/copy.cur b/widget/windows/res/copy.cur
new file mode 100644
index 0000000000..87f1519cd1
--- /dev/null
+++ b/widget/windows/res/copy.cur
Binary files differ
diff --git a/widget/windows/res/grab.cur b/widget/windows/res/grab.cur
new file mode 100644
index 0000000000..db7ad5aed3
--- /dev/null
+++ b/widget/windows/res/grab.cur
Binary files differ
diff --git a/widget/windows/res/grabbing.cur b/widget/windows/res/grabbing.cur
new file mode 100644
index 0000000000..e0dfd04e4d
--- /dev/null
+++ b/widget/windows/res/grabbing.cur
Binary files differ
diff --git a/widget/windows/res/none.cur b/widget/windows/res/none.cur
new file mode 100644
index 0000000000..2114dfaee3
--- /dev/null
+++ b/widget/windows/res/none.cur
Binary files differ
diff --git a/widget/windows/res/row_resize.cur b/widget/windows/res/row_resize.cur
new file mode 100644
index 0000000000..a7369d32d1
--- /dev/null
+++ b/widget/windows/res/row_resize.cur
Binary files differ
diff --git a/widget/windows/res/select.cur b/widget/windows/res/select.cur
new file mode 100644
index 0000000000..5a88b3707e
--- /dev/null
+++ b/widget/windows/res/select.cur
Binary files differ
diff --git a/widget/windows/res/vertical_text.cur b/widget/windows/res/vertical_text.cur
new file mode 100644
index 0000000000..3de04ebec3
--- /dev/null
+++ b/widget/windows/res/vertical_text.cur
Binary files differ
diff --git a/widget/windows/res/zoom_in.cur b/widget/windows/res/zoom_in.cur
new file mode 100644
index 0000000000..b594d79271
--- /dev/null
+++ b/widget/windows/res/zoom_in.cur
Binary files differ
diff --git a/widget/windows/res/zoom_out.cur b/widget/windows/res/zoom_out.cur
new file mode 100644
index 0000000000..7e495fbaa4
--- /dev/null
+++ b/widget/windows/res/zoom_out.cur
Binary files differ
diff --git a/widget/windows/resource.h b/widget/windows/resource.h
new file mode 100644
index 0000000000..a367e9f3e9
--- /dev/null
+++ b/widget/windows/resource.h
@@ -0,0 +1,16 @@
+/* -*- 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/. */
+#define IDC_GRAB 4101
+#define IDC_GRABBING 4102
+#define IDC_CELL 4103
+#define IDC_COPY 4104
+#define IDC_ALIAS 4105
+#define IDC_ZOOMIN 4106
+#define IDC_ZOOMOUT 4107
+#define IDC_COLRESIZE 4108
+#define IDC_ROWRESIZE 4109
+#define IDC_VERTICALTEXT 4110
+#define IDC_DUMMY_CE_MENUBAR 4111
+#define IDC_NONE 4112
diff --git a/widget/windows/tests/TestUriValidation.cpp b/widget/windows/tests/TestUriValidation.cpp
new file mode 100644
index 0000000000..d8a0ca09ce
--- /dev/null
+++ b/widget/windows/tests/TestUriValidation.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#define MOZ_USE_LAUNCHER_ERROR
+
+#define UNICODE
+#include "mozilla/UrlmonHeaderOnlyUtils.h"
+#include "TestUrisToValidate.h"
+
+#include <urlmon.h>
+
+using namespace mozilla;
+
+static LauncherResult<_bstr_t> ShellValidateUri(const wchar_t* aUri) {
+ LauncherResult<UniqueAbsolutePidl> pidlResult = ShellParseDisplayName(aUri);
+ if (pidlResult.isErr()) {
+ return pidlResult.propagateErr();
+ }
+ UniqueAbsolutePidl pidl = pidlResult.unwrap();
+
+ // |pidl| is an absolute path. IShellFolder::GetDisplayNameOf requires a
+ // valid child ID, so the first thing we need to resolve is the IShellFolder
+ // for |pidl|'s parent, as well as the childId that represents |pidl|.
+ // Fortunately SHBindToParent does exactly that!
+ PCUITEMID_CHILD childId = nullptr;
+ RefPtr<IShellFolder> parentFolder;
+ HRESULT hr = SHBindToParent(pidl.get(), IID_IShellFolder,
+ getter_AddRefs(parentFolder), &childId);
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // Now we retrieve the display name of |childId|, telling the shell that we
+ // plan to have the string parsed.
+ STRRET strret;
+ hr = parentFolder->GetDisplayNameOf(childId, SHGDN_FORPARSING, &strret);
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // StrRetToBSTR automatically takes care of freeing any dynamically
+ // allocated memory in |strret|.
+ _bstr_t bstrUri;
+ hr = StrRetToBSTR(&strret, nullptr, bstrUri.GetAddress());
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ return bstrUri;
+}
+
+static LauncherResult<_bstr_t> GetFragment(const wchar_t* aUri) {
+ constexpr DWORD flags =
+ Uri_CREATE_NO_DECODE_EXTRA_INFO | Uri_CREATE_CANONICALIZE |
+ Uri_CREATE_CRACK_UNKNOWN_SCHEMES | Uri_CREATE_PRE_PROCESS_HTML_URI |
+ Uri_CREATE_IE_SETTINGS;
+ RefPtr<IUri> uri;
+ HRESULT hr = CreateUri(aUri, flags, 0, getter_AddRefs(uri));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ _bstr_t bstrFragment;
+ hr = uri->GetFragment(bstrFragment.GetAddress());
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+ return bstrFragment;
+}
+
+static bool RunSingleTest(const wchar_t* aUri) {
+ LauncherResult<_bstr_t> uriOld = ShellValidateUri(aUri),
+ uriNew = UrlmonValidateUri(aUri);
+ if (uriOld.isErr() != uriNew.isErr()) {
+ printf("TEST-FAILED | UriValidation | Validation result mismatch on %S\n",
+ aUri);
+ return false;
+ }
+
+ if (uriOld.isErr()) {
+ if (uriOld.unwrapErr().mError != uriNew.unwrapErr().mError) {
+ printf("TEST-FAILED | UriValidation | Error code mismatch on %S\n", aUri);
+ return false;
+ }
+ return true;
+ }
+
+ LauncherResult<_bstr_t> bstrFragment = GetFragment(aUri);
+ if (bstrFragment.isErr()) {
+ printf("TEST-FAILED | UriValidation | Failed to get a fragment from %S\n",
+ aUri);
+ return false;
+ }
+
+ // We validate a uri with two logics: the current one UrlmonValidateUri and
+ // the older one ShellValidateUri, to make sure the same validation result.
+ // We introduced UrlmonValidateUri because ShellValidateUri drops a fragment
+ // in a uri due to the design of Windows. To bypass the fragment issue, we
+ // extract a fragment and appends it into the validated string, and compare.
+ _bstr_t bstrUriOldCorrected = uriOld.unwrap() + bstrFragment.unwrap();
+ const _bstr_t& bstrUriNew = uriNew.unwrap();
+ if (bstrUriOldCorrected != bstrUriNew) {
+ printf("TEST-FAILED | UriValidation | %S %S %S\n", aUri,
+ static_cast<const wchar_t*>(bstrUriOldCorrected),
+ static_cast<const wchar_t*>(bstrUriNew));
+ return false;
+ }
+
+ return true;
+}
+
+int wmain(int argc, wchar_t* argv[]) {
+ HRESULT hr = CoInitialize(nullptr);
+ if (FAILED(hr)) {
+ return 1;
+ }
+
+ bool isOk = true;
+
+ if (argc == 2) {
+ isOk = RunSingleTest(argv[1]);
+ } else {
+ for (const wchar_t*& testUri : kTestUris) {
+ if (!RunSingleTest(testUri)) {
+ isOk = false;
+ }
+ }
+ }
+
+ CoUninitialize();
+ return isOk ? 0 : 1;
+}
diff --git a/widget/windows/tests/TestUrisToValidate.h b/widget/windows/tests/TestUrisToValidate.h
new file mode 100644
index 0000000000..cb00366d1e
--- /dev/null
+++ b/widget/windows/tests/TestUrisToValidate.h
@@ -0,0 +1,471 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TestUrisToValidate_h
+#define mozilla_TestUrisToValidate_h
+
+const wchar_t* kTestUris[] = {
+ L"callto:%.txt",
+ L"callto:%00.txt",
+ L"callto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"fdaction:%.txt",
+ L"fdaction:%00.txt",
+ L"fdaction:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"feed:%.txt",
+ L"feed:%00.txt",
+ L"feed:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"feeds:%.txt",
+ L"feeds:%00.txt",
+ L"feeds:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"file:///%.txt",
+ L"file:///%00.txt",
+ L"file:///%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"firefox.url:%.txt",
+ L"firefox.url:%00.txt",
+ L"firefox.url:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"firefoxurl:%.txt",
+ L"firefoxurl:%00.txt",
+ L"firefoxurl:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ftp:%.txt",
+ L"ftp:%00.txt",
+ L"ftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gopher:%.txt",
+ L"gopher:%00.txt",
+ L"gopher:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gtalk:%.txt",
+ L"gtalk:%00.txt",
+ L"gtalk:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"HTTP:%.txt",
+ L"HTTP:%00.txt",
+ L"HTTP:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"http:%.txt",
+ L"http:%00.txt",
+ L"http:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"https://bug389580.bmoattachments.org/%.txt",
+ L"https://bug389580.bmoattachments.org/%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"ie.ftp:%.txt",
+ L"ie.ftp:%00.txt",
+ L"ie.ftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ie.http:%.txt",
+ L"ie.http:%00.txt",
+ L"ie.http:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ie.https:%.txt",
+ L"ie.https:%00.txt",
+ L"ie.https:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"irc:%.txt",
+ L"irc:%00.txt",
+ L"irc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ircs:%.txt",
+ L"ircs:%00.txt",
+ L"ircs:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itms:%.txt",
+ L"itms:%00.txt",
+ L"itms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itmss:%.txt",
+ L"itmss:%00.txt",
+ L"itmss:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itpc:%.txt",
+ L"itpc:%00.txt",
+ L"itpc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itms:%.txt",
+ L"itunes.assocprotocol.itms:%00.txt",
+ L"itunes.assocprotocol.itms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itmss:%.txt",
+ L"itunes.assocprotocol.itmss:%00.txt",
+ L"itunes.assocprotocol.itmss:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itpc:%.txt",
+ L"itunes.assocprotocol.itpc:%00.txt",
+ L"itunes.assocprotocol.itpc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ldap:%.txt",
+ L"ldap:%00.txt",
+ L"ldap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mailto:%.txt",
+ L"mailto:%00.txt",
+ L"mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mms:%.txt",
+ L"mms:%00.txt",
+ L"mms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"navigatorurl:%.txt",
+ L"navigatorurl:%00.txt",
+ L"navigatorurl:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"news:%.txt",
+ L"news:%00.txt",
+ L"news:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"nntp:%.txt",
+ L"nntp:%00.txt",
+ L"nntp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"oms:%.txt",
+ L"oms:%00.txt",
+ L"oms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook:%.txt",
+ L"outlook:%00.txt",
+ L"outlook:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.feed:%.txt",
+ L"outlook.url.feed:%00.txt",
+ L"outlook.url.feed:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.mailto:%.txt",
+ L"outlook.url.mailto:%00.txt",
+ L"outlook.url.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.webcal:%.txt",
+ L"outlook.url.webcal:%00.txt",
+ L"outlook.url.webcal:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlookfeed:%.txt",
+ L"outlookfeed:%00.txt",
+ L"outlookfeed:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlookfeeds:%.txt",
+ L"outlookfeeds:%00.txt",
+ L"outlookfeeds:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"pnm:%.txt",
+ L"pnm:%00.txt",
+ L"pnm:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.ftp:%.txt",
+ L"prls.intappfile.ftp:%00.txt",
+ L"prls.intappfile.ftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.http:%.txt",
+ L"prls.intappfile.http:%00.txt",
+ L"prls.intappfile.http:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.https:%.txt",
+ L"prls.intappfile.https:%00.txt",
+ L"prls.intappfile.https:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.mailto:%.txt",
+ L"prls.intappfile.mailto:%00.txt",
+ L"prls.intappfile.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"rlogin:%.txt",
+ L"rlogin:%00.txt",
+ L"rlogin:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"rtsp:%.txt",
+ L"rtsp:%00.txt",
+ L"rtsp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"scp:%.txt",
+ L"scp:%00.txt",
+ L"scp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sftp:%.txt",
+ L"sftp:%00.txt",
+ L"sftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sip:%.txt",
+ L"sip:%00.txt",
+ L"sip:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"skype:%.txt",
+ L"skype:%00.txt",
+ L"skype:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"snews:%.txt",
+ L"snews:%00.txt",
+ L"snews:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"telnet:%.txt",
+ L"telnet:%00.txt",
+ L"telnet:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"thunderbird.url.mailto:%.txt",
+ L"thunderbird.url.mailto:%00.txt",
+ L"thunderbird.url.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"thunderbird.url.news:%.txt",
+ L"thunderbird.url.news:%00.txt",
+ L"thunderbird.url.news:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tn3270:%.txt",
+ L"tn3270:%00.txt",
+ L"tn3270:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tscrec4:%.txt",
+ L"tscrec4:%00.txt",
+ L"tscrec4:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"webcals:%.txt",
+ L"webcals:%00.txt",
+ L"webcals:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowscalendar.urlwebcal.1:%.txt",
+ L"windowscalendar.urlwebcal.1:%00.txt",
+ L"windowscalendar.urlwebcal.1:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.mailto:%.txt",
+ L"windowsmail.url.mailto:%00.txt",
+ L"windowsmail.url.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.news:%.txt",
+ L"windowsmail.url.news:%00.txt",
+ L"windowsmail.url.news:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.nntp:%.txt",
+ L"windowsmail.url.nntp:%00.txt",
+ L"windowsmail.url.nntp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.snews:%.txt",
+ L"windowsmail.url.snews:%00.txt",
+ L"windowsmail.url.snews:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"wmp11.assocprotocol.mms:%.txt",
+ L"wmp11.assocprotocol.mms:%00.txt",
+ L"wmp11.assocprotocol.mms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"wpc:%.txt",
+ L"wpc:%00.txt",
+ L"wpc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ymsgr:%.txt",
+ L"ymsgr:%00.txt",
+ L"ymsgr:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"acrobat:%.txt",
+ L"acrobat:%00.txt",
+ L"acrobat:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"acsui:%.txt",
+ L"acsui:%00.txt",
+ L"acsui:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"allc8.commands.2:%.txt",
+ L"allc8.commands.2:%00.txt",
+ L"allc8.commands.2:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"allholdem.commands.2:%.txt",
+ L"allholdem.commands.2:%00.txt",
+ L"allholdem.commands.2:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"allpoker.commands.2:%.txt",
+ L"allpoker.commands.2:%00.txt",
+ L"allpoker.commands.2:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aolautofix:%.txt",
+ L"aolautofix:%00.txt",
+ L"aolautofix:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aolds:%.txt",
+ L"aolds:%00.txt",
+ L"aolds:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"bc:%.txt",
+ L"bc:%00.txt",
+ L"bc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"bctp:%.txt",
+ L"bctp:%00.txt",
+ L"bctp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"bittorrent:%.txt",
+ L"bittorrent:%00.txt",
+ L"bittorrent:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"camfrog:%.txt",
+ L"camfrog:%00.txt",
+ L"camfrog:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"csi:%.txt",
+ L"csi:%00.txt",
+ L"csi:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"cvs:%.txt",
+ L"cvs:%00.txt",
+ L"cvs:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"daap:%.txt",
+ L"daap:%00.txt",
+ L"daap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ed2k:%.txt",
+ L"ed2k:%00.txt",
+ L"ed2k:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"explorer.assocprotocol.search-ms:%.txt",
+ L"explorer.assocprotocol.search-ms:%00.txt",
+ L"explorer.assocprotocol.search-ms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gizmoproject:%.txt",
+ L"gizmoproject:%00.txt",
+ L"gizmoproject:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gnet:%.txt",
+ L"gnet:%00.txt",
+ L"gnet:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gnutella:%.txt",
+ L"gnutella:%00.txt",
+ L"gnutella:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gsarcade:%.txt",
+ L"gsarcade:%00.txt",
+ L"gsarcade:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"hcp:%.txt",
+ L"hcp:%00.txt",
+ L"hcp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"icquser:%.txt",
+ L"icquser:%00.txt",
+ L"icquser:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"icy:%.txt",
+ L"icy:%00.txt",
+ L"icy:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"imesync:%.txt",
+ L"imesync:%00.txt",
+ L"imesync:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.daap:%.txt",
+ L"itunes.assocprotocol.daap:%00.txt",
+ L"itunes.assocprotocol.daap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.pcast:%.txt",
+ L"itunes.assocprotocol.pcast:%00.txt",
+ L"itunes.assocprotocol.pcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"joost:%.txt",
+ L"joost:%00.txt",
+ L"joost:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"m4macdrive:%.txt",
+ L"m4macdrive:%00.txt",
+ L"m4macdrive:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"magnet:%.txt",
+ L"magnet:%00.txt",
+ L"magnet:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mapi:%.txt",
+ L"mapi:%00.txt",
+ L"mapi:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mc12:%.txt",
+ L"mc12:%00.txt",
+ L"mc12:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mediajukebox:%.txt",
+ L"mediajukebox:%00.txt",
+ L"mediajukebox:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"morpheus:%.txt",
+ L"morpheus:%00.txt",
+ L"morpheus:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mp2p:%.txt",
+ L"mp2p:%00.txt",
+ L"mp2p:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mpodcast:%.txt",
+ L"mpodcast:%00.txt",
+ L"mpodcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"msdigitallocker:%.txt",
+ L"msdigitallocker:%00.txt",
+ L"msdigitallocker:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.stssync:%.txt",
+ L"outlook.url.stssync:%00.txt",
+ L"outlook.url.stssync:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"p2p:%.txt",
+ L"p2p:%00.txt",
+ L"p2p:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"pando:%.txt",
+ L"pando:%00.txt",
+ L"pando:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"pcast:%.txt",
+ L"pcast:%00.txt",
+ L"pcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"picasa:%.txt",
+ L"picasa:%00.txt",
+ L"picasa:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"plaxo:%.txt",
+ L"plaxo:%00.txt",
+ L"plaxo:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"play:%.txt",
+ L"play:%00.txt",
+ L"play:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"podcast:%.txt",
+ L"podcast:%00.txt",
+ L"podcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ppmate:%.txt",
+ L"ppmate:%00.txt",
+ L"ppmate:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ppmates:%.txt",
+ L"ppmates:%00.txt",
+ L"ppmates:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ppstream:%.txt",
+ L"ppstream:%00.txt",
+ L"ppstream:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"quicktime:%.txt",
+ L"quicktime:%00.txt",
+ L"quicktime:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"realplayer.autoplay.6:%.txt",
+ L"realplayer.autoplay.6:%00.txt",
+ L"realplayer.autoplay.6:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"realplayer.cdburn.6:%.txt",
+ L"realplayer.cdburn.6:%00.txt",
+ L"realplayer.cdburn.6:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"rhap:%.txt",
+ L"rhap:%00.txt",
+ L"rhap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sc:%.txt",
+ L"sc:%00.txt",
+ L"sc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"search-ms:%.txt",
+ L"search-ms:%00.txt",
+ L"search-ms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"shareaza:%.txt",
+ L"shareaza:%00.txt",
+ L"shareaza:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"shell:%.txt",
+ L"shell:%00.txt",
+ L"shell:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"shout:%.txt",
+ L"shout:%00.txt",
+ L"shout:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sig2dat:%.txt",
+ L"sig2dat:%00.txt",
+ L"sig2dat:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sop:%.txt",
+ L"sop:%00.txt",
+ L"sop:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"steam:%.txt",
+ L"steam:%00.txt",
+ L"steam:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"stssync:%.txt",
+ L"stssync:%00.txt",
+ L"stssync:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"svn:%.txt",
+ L"svn:%00.txt",
+ L"svn:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"svn+ssh:%.txt",
+ L"svn+ssh:%00.txt",
+ L"svn+ssh:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"synacast:%.txt",
+ L"synacast:%00.txt",
+ L"synacast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"torrent:%.txt",
+ L"torrent:%00.txt",
+ L"torrent:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tsvn:%.txt",
+ L"tsvn:%00.txt",
+ L"tsvn:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tvants:%.txt",
+ L"tvants:%00.txt",
+ L"tvants:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tvu:%.txt",
+ L"tvu:%00.txt",
+ L"tvu:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"unsv:%.txt",
+ L"unsv:%00.txt",
+ L"unsv:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"uvox:%.txt",
+ L"uvox:%00.txt",
+ L"uvox:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ventrilo:%.txt",
+ L"ventrilo:%00.txt",
+ L"ventrilo:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"vs:%.txt",
+ L"vs:%00.txt",
+ L"vs:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"zune:%.txt",
+ L"zune:%00.txt",
+ L"zune:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"https://example.com/?a=123&b=456",
+ L"https://example.com/#123?a=123&b=456",
+ L"https://example.com/?#123a=123&b=456",
+ L"https://example.com/?a=123&b=456#123",
+ L"mailto:%41%42%23%31",
+ L"mailto:%41%42%23%31#fragment",
+ L"news:%41%42%23%31",
+ L"news:%41%42%23%31#fragment",
+ L"microsoft-edge:%41%42%23%31",
+ L"microsoft-edge:%41%42%23%31#fragment",
+ L"microsoft-edge:%41%42%23%31#fragment#",
+ L"microsoft-edge:%41%42%23%31####",
+ L"something-unknown:",
+ L"something-unknown:x=123",
+ L"something-unknown:?=123",
+ L"something-unknown:#code=0123456789%200123456789&x=01234567890123456789",
+};
+
+#endif // mozilla_TestUrisToValidate_h
diff --git a/widget/windows/tests/gtest/TestJumpListBuilder.cpp b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
new file mode 100644
index 0000000000..5494c42d37
--- /dev/null
+++ b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
@@ -0,0 +1,823 @@
+/* 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 <objectarray.h>
+#include <shobjidl.h>
+#include <windows.h>
+#include <string.h>
+#include <propvarutil.h>
+#include <propkey.h>
+
+#ifdef __MINGW32__
+// MinGW-w64 headers are missing PropVariantToString.
+# include <propsys.h>
+PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch);
+#endif
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "JumpListBuilder.h"
+
+using namespace mozilla;
+using namespace testing;
+using mozilla::dom::AutoJSAPI;
+using mozilla::dom::Promise;
+using mozilla::dom::PromiseNativeHandler;
+using mozilla::dom::ToJSValue;
+using mozilla::dom::WindowsJumpListShortcutDescription;
+using mozilla::widget::JumpListBackend;
+using mozilla::widget::JumpListBuilder;
+
+/**
+ * GMock matcher that ensures that two LPCWSTRs match.
+ */
+MATCHER_P(LPCWSTREq, value, "The equivalent of StrEq for LPCWSTRs") {
+ return (wcscmp(arg, value)) == 0;
+}
+
+/**
+ * GMock matcher that ensures that a IObjectArray* contains nsIShellLinkW's
+ * that match an equivalent set of nsTArray<WindowsJumpListShortcutDescriptions>
+ */
+MATCHER_P(ShellLinksEq, descs,
+ "Comparing generated IShellLinkW with "
+ "WindowsJumpListShortcutDescription definitions") {
+ uint32_t count = 0;
+ HRESULT hr = arg->GetCount(&count);
+ if (FAILED(hr) || count != descs->Length()) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < descs->Length(); ++i) {
+ RefPtr<IShellLinkW> link;
+ if (FAILED(arg->GetAt(i, IID_IShellLinkW,
+ static_cast<void**>(getter_AddRefs(link))))) {
+ return false;
+ }
+
+ if (!link) {
+ return false;
+ }
+
+ const WindowsJumpListShortcutDescription& desc = descs->ElementAt(i);
+
+ // We'll now compare each member of the WindowsJumpListShortcutDescription
+ // with what is stored in the IShellLink.
+
+ // WindowsJumpListShortcutDescription.title
+ IPropertyStore* propStore = nullptr;
+ hr = link->QueryInterface(IID_IPropertyStore, (LPVOID*)&propStore);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ PROPVARIANT pv;
+ hr = propStore->GetValue(PKEY_Title, &pv);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ wchar_t title[PKEYSTR_MAX];
+ hr = PropVariantToString(pv, title, PKEYSTR_MAX);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ if (!desc.mTitle.Equals(title)) {
+ return false;
+ }
+
+ // WindowsJumpListShortcutDescription.path
+ wchar_t pathBuf[MAX_PATH];
+ hr = link->GetPath(pathBuf, MAX_PATH, nullptr, SLGP_SHORTPATH);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ if (!desc.mPath.Equals(pathBuf)) {
+ return false;
+ }
+
+ // WindowsJumpListShortcutDescription.arguments (optional)
+ wchar_t argsBuf[MAX_PATH];
+ hr = link->GetArguments(argsBuf, MAX_PATH);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ if (desc.mArguments.WasPassed()) {
+ if (!desc.mArguments.Value().Equals(argsBuf)) {
+ return false;
+ }
+ } else {
+ // Otherwise, the arguments should be empty.
+ if (wcsnlen(argsBuf, MAX_PATH) != 0) {
+ return false;
+ }
+ }
+
+ // WindowsJumpListShortcutDescription.description
+ wchar_t descBuf[MAX_PATH];
+ hr = link->GetDescription(descBuf, MAX_PATH);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ if (!desc.mDescription.Equals(descBuf)) {
+ return false;
+ }
+
+ // WindowsJumpListShortcutDescription.iconPath and
+ // WindowsJumpListShortcutDescription.fallbackIconIndex
+ int iconIdx = 0;
+ wchar_t iconPathBuf[MAX_PATH];
+ hr = link->GetIconLocation(iconPathBuf, MAX_PATH, &iconIdx);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ if (desc.mIconPath.WasPassed() && !desc.mIconPath.Value().IsEmpty()) {
+ // If the WindowsJumpListShortcutDescription supplied an iconPath,
+ // then it should match iconPathBuf and have an icon index of 0.
+ if (!desc.mIconPath.Value().Equals(iconPathBuf) || iconIdx != 0) {
+ return false;
+ }
+ } else {
+ // Otherwise, the iconPathBuf should equal the
+ // WindowsJumpListShortcutDescription path, and the iconIdx should match
+ // the fallbackIconIndex.
+ if (!desc.mPath.Equals(iconPathBuf) ||
+ desc.mFallbackIconIndex != iconIdx) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+/**
+ * This is a helper class that allows our tests to wait for a native DOM Promise
+ * to resolve, and get the JS::Value that the Promise resolves with. This is
+ * expected to run on the main thread.
+ */
+class WaitForResolver : public PromiseNativeHandler {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaitForResolver, override)
+
+ NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override {
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ NS_INTERFACE_TABLE0(WaitForResolver)
+
+ return rv;
+ }
+
+ WaitForResolver() : mIsDone(false) {}
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aError) override {
+ mResult = aValue;
+ mIsDone = true;
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aError) override {
+ ASSERT_TRUE(false); // Should never reach here.
+ }
+
+ /**
+ * Spins a nested event loop and blocks until the Promise has resolved.
+ */
+ void SpinUntilResolved() {
+ SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns,
+ [&]() { return mIsDone; });
+ }
+
+ /**
+ * Spins a nested event loop and blocks until the Promise has resolved,
+ * after which the JS::Value that the Promise resolves with is returned via
+ * the aRetval outparam.
+ *
+ * @param {JS::MutableHandle<JS::Value>} aRetval
+ * The outparam for the JS::Value that the Promise resolves with.
+ */
+ void SpinUntilResolvedWithResult(JS::MutableHandle<JS::Value> aRetval) {
+ SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns,
+ [&]() { return mIsDone; });
+ aRetval.set(mResult);
+ }
+
+ private:
+ virtual ~WaitForResolver() = default;
+
+ JS::Heap<JS::Value> mResult;
+ bool mIsDone;
+};
+
+/**
+ * An implementation of JumpListBackend that is instrumented using the GMock
+ * framework to record calls. Unlike the NativeJumpListBackend, this backend
+ * is expected to be instantiated on the main thread and passed as an argument
+ * to the JumpListBuilder's worker thread. Testers should wait for the methods
+ * that call these functions to resolve their Promises before checking the
+ * recorded values.
+ */
+class TestingJumpListBackend : public JumpListBackend {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JumpListBackend, override)
+
+ TestingJumpListBackend() : mMonitor("TestingJumpListBackend::mMonitor") {}
+
+ virtual bool IsAvailable() override { return true; }
+
+ MOCK_METHOD(HRESULT, SetAppID, (LPCWSTR));
+ MOCK_METHOD(HRESULT, BeginList, (UINT*, REFIID, void**));
+ MOCK_METHOD(HRESULT, AddUserTasks, (IObjectArray*));
+ MOCK_METHOD(HRESULT, AppendCategory, (LPCWSTR, IObjectArray*));
+ MOCK_METHOD(HRESULT, CommitList, ());
+ MOCK_METHOD(HRESULT, AbortList, ());
+ MOCK_METHOD(HRESULT, DeleteList, (LPCWSTR));
+
+ virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override {
+ return 0;
+ }
+
+ // In one case (construction), an operation occurs off of the main thread that
+ // we must wait for without an associated Promise.
+ Monitor& GetMonitor() { return mMonitor; }
+
+ protected:
+ virtual ~TestingJumpListBackend() override{};
+
+ private:
+ Monitor mMonitor;
+};
+
+/**
+ * A helper function that creates some fake WindowsJumpListShortcutDescription
+ * objects as well as JS::Value representations of those objects. These are
+ * returned to the caller through outparams.
+ *
+ * @param {JSContext*} aCx
+ * The current JSContext in the execution environment.
+ * @param {uint32_t} howMany
+ * The number of WindowsJumpListShortcutDescriptions to generate.
+ * @param {boolean} longDescription
+ * True if the description should be greater than MAX_PATH (260 characters).
+ * @param {nsTArray<WindowsJumpListShortcutDescription>&} aArray
+ * The outparam for the array of generated
+ * WindowsJumpListShortcutDescriptions.
+ * @param {nsTArray<JS::Value>&} aJSValArray
+ * The outparam for the array of JS::Value's representing the generated
+ * WindowsJumpListShortcutDescriptions.
+ */
+void GenerateWindowsJumpListShortcutDescriptions(
+ JSContext* aCx, uint32_t howMany, bool longDescription,
+ nsTArray<WindowsJumpListShortcutDescription>& aArray,
+ nsTArray<JS::Value>& aJSValArray) {
+ for (uint32_t i = 0; i < howMany; ++i) {
+ WindowsJumpListShortcutDescription desc;
+ nsAutoString title(u"Test Task #");
+ title.AppendInt(i);
+ desc.mTitle = title;
+
+ nsAutoString path(u"C:\\Some\\Test\\Path.exe");
+ desc.mPath = path;
+ nsAutoString description;
+
+ if (longDescription) {
+ description.AppendPrintf(
+ "For item #%i, this is a very very very very VERY VERY very very "
+ "very very very very very very very very very very VERY VERY very "
+ "very very very very very very very very very very very VERY VERY "
+ "very very very very very very very very very very very very VERY "
+ "VERY very very very very very very very very very very very very "
+ "VERY VERY very very very very very very very very very very very "
+ "very VERY VERY very very very very very very very very long test "
+ "description for an item",
+ i);
+ } else {
+ description.AppendPrintf("This is a test description for an item #%i", i);
+ }
+
+ desc.mDescription = description;
+ desc.mFallbackIconIndex = 0;
+
+ if (!(i % 2)) {
+ nsAutoString arguments(u"-arg1 -arg2 -arg3");
+ desc.mArguments.Construct(arguments);
+ nsAutoString iconPath(u"C:\\Some\\icon.png");
+ desc.mIconPath.Construct(iconPath);
+ }
+
+ aArray.AppendElement(desc);
+ JS::Rooted<JS::Value> descJSValue(aCx);
+ ASSERT_TRUE(ToJSValue(aCx, desc, &descJSValue));
+ aJSValArray.AppendElement(std::move(descJSValue));
+ }
+}
+
+/**
+ * Tests construction and that the application ID is properly passed to the
+ * backend.
+ */
+TEST(JumpListBuilder, Construction)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ ASSERT_TRUE(testBackend);
+
+ nsAutoString aumid(u"TestApplicationID");
+ LPCWSTR passedID = aumid.get();
+ // Construction of our class (or any class of that matter) does not return a
+ // Promise that we can wait on to ensure that the background thread got the
+ // right information. We therefore use a monitor on the testing backend as
+ // well as an EXPECT_CALL to block execution of the test until the background
+ // work has completed.
+ Monitor& mon = testBackend->GetMonitor();
+ MonitorAutoLock lock(mon);
+ EXPECT_CALL(*testBackend, SetAppID(LPCWSTREq(passedID))).WillOnce([&mon] {
+ MonitorAutoLock lock(mon);
+ mon.Notify();
+ return S_OK;
+ });
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ // This is the amount of time that we will wait for the background thread to
+ // respond before considering it a timeout failure.
+ const int kWaitTimeoutMs = 5000;
+
+ ASSERT_TRUE(mon.Wait(TimeDuration::FromMilliseconds(kWaitTimeoutMs)) !=
+ CVStatus::Timeout);
+}
+
+/**
+ * Tests calling CheckForRemovals and receiving a series of removed jump list
+ * entries. Calling CheckForRemovals should call the following methods on the
+ * backend, in order:
+ *
+ * - SetAppID
+ * - AbortList
+ * - BeginList
+ * - AbortList
+ */
+TEST(JumpListBuilder, CheckForRemovals)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ nsAutoString aumid(u"TestApplicationID");
+ // We set up this expectation here because SetAppID will be called soon
+ // after construction of the JumpListBuilder via the background thread.
+ EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ EXPECT_CALL(*testBackend, AbortList()).Times(2);
+
+ // Let's prepare BeginList to return a two entry collection of IShellLinks.
+ // The first IShellLink will have the URL be https://example.com, the second
+ // will have the URL be https://mozilla.org.
+ EXPECT_CALL(*testBackend, BeginList)
+ .WillOnce([](UINT* pcMinSlots, REFIID riid, void** ppv) {
+ RefPtr<IObjectCollection> collection;
+ DebugOnly<HRESULT> hr = CoCreateInstance(
+ CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IObjectCollection, getter_AddRefs(collection));
+ MOZ_ASSERT(SUCCEEDED(hr));
+
+ RefPtr<IShellLinkW> link;
+ hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, getter_AddRefs(link));
+ MOZ_ASSERT(SUCCEEDED(hr));
+
+ nsAutoString firstLinkHref(u"https://example.com"_ns);
+ link->SetArguments(firstLinkHref.get());
+
+ nsAutoString appPath(u"C:\\Tmp\\firefox.exe"_ns);
+ link->SetIconLocation(appPath.get(), 0);
+
+ collection->AddObject(link);
+
+ // Let's re-use the same IShellLink, but change the URL to add our
+ // second entry. The values of the IShellLink are ultimately copied
+ // over to the items being added to the collection.
+ nsAutoString secondLinkHref(u"https://mozilla.org"_ns);
+ link->SetArguments(secondLinkHref.get());
+ collection->AddObject(link);
+
+ RefPtr<IObjectArray> pArray;
+ hr = collection->QueryInterface(IID_IObjectArray,
+ getter_AddRefs(pArray));
+ MOZ_ASSERT(SUCCEEDED(hr));
+
+ *ppv = static_cast<IObjectArray*>(pArray);
+ (static_cast<IUnknown*>(*ppv))->AddRef();
+
+ // This is the default value to return, according to
+ // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
+ *pcMinSlots = 10;
+
+ return S_OK;
+ });
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+ nsresult rv = builder->CheckForRemovals(cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolvedWithResult(&result);
+
+ ASSERT_TRUE(result.isObject());
+ JS::Rooted<JSObject*> obj(cx, result.toObjectOrNull());
+
+ bool isArray;
+ ASSERT_TRUE(JS::IsArrayObject(cx, obj, &isArray));
+ ASSERT_TRUE(isArray);
+
+ // We should expect to see 2 URL strings returned in the array.
+ uint32_t length = 0;
+ ASSERT_TRUE(JS::GetArrayLength(cx, obj, &length));
+ ASSERT_EQ(length, 2U);
+
+ // The first one should be https://example.com
+ JS::Rooted<JS::Value> firstURLValue(cx);
+ ASSERT_TRUE(JS_GetElement(cx, obj, 0, &firstURLValue));
+ JS::Rooted<JSString*> firstURLJSString(cx, firstURLValue.toString());
+ nsAutoJSString firstURLAutoString;
+ ASSERT_TRUE(firstURLAutoString.init(cx, firstURLJSString));
+
+ ASSERT_TRUE(firstURLAutoString.EqualsLiteral("https://example.com"));
+
+ // The second one should be https://mozilla.org
+ JS::Rooted<JS::Value> secondURLValue(cx);
+ ASSERT_TRUE(JS_GetElement(cx, obj, 1, &secondURLValue));
+ JS::Rooted<JSString*> secondURLJSString(cx, secondURLValue.toString());
+ nsAutoJSString secondURLAutoString;
+ ASSERT_TRUE(secondURLAutoString.init(cx, secondURLJSString));
+
+ ASSERT_TRUE(secondURLAutoString.EqualsLiteral("https://mozilla.org"));
+}
+
+/**
+ * Tests calling PopulateJumpList with empty arguments, which should call the
+ * following methods on the backend, in order:
+ *
+ * - SetAppID
+ * - AbortList
+ * - BeginList
+ * - CommitList
+ *
+ * This should result in an empty jump list for the user.
+ */
+TEST(JumpListBuilder, PopulateJumpListEmpty)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ nsAutoString aumid(u"TestApplicationID");
+ // We set up this expectation here because SetAppID will be called soon
+ // after construction of the JumpListBuilder via the background thread.
+ EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<JS::Value> taskDescJSVals;
+ nsAutoString customTitle(u"");
+ nsTArray<JS::Value> customDescJSVals;
+
+ EXPECT_CALL(*testBackend, AbortList()).Times(1);
+ EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
+ EXPECT_CALL(*testBackend, CommitList()).Times(1);
+ EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);
+
+ nsresult rv =
+ builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals,
+ cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolved();
+}
+
+/**
+ * Tests calling PopulateJumpList with only tasks, and no custom items.
+ * This should call the following methods on the backend, in order:
+ *
+ * - SetAppID
+ * - AbortList
+ * - BeginList
+ * - AddUserTasks
+ * - CommitList
+ *
+ * This should result in a jump list with just tasks shown to the user, and
+ * no custom section.
+ */
+TEST(JumpListBuilder, PopulateJumpListOnlyTasks)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ nsAutoString aumid(u"TestApplicationID");
+ // We set up this expectation here because SetAppID will be called soon
+ // after construction of the JumpListBuilder via the background thread.
+ EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<JS::Value> taskDescJSVals;
+ nsTArray<WindowsJumpListShortcutDescription> taskDescs;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs,
+ taskDescJSVals);
+
+ nsAutoString customTitle(u"");
+ nsTArray<JS::Value> customDescJSVals;
+
+ EXPECT_CALL(*testBackend, AbortList()).Times(1);
+ EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
+ EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1);
+
+ EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0);
+ EXPECT_CALL(*testBackend, CommitList()).Times(1);
+ EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);
+
+ nsresult rv =
+ builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals,
+ cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolved();
+}
+
+/**
+ * Tests calling PopulateJumpList with only custom items, and no tasks.
+ * This should call the following methods on the backend, in order:
+ *
+ * - SetAppID
+ * - AbortList
+ * - BeginList
+ * - AppendCategory
+ * - CommitList
+ *
+ * This should result in a jump list with just custom items shown to the user,
+ * and no tasks.
+ */
+TEST(JumpListBuilder, PopulateJumpListOnlyCustomItems)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ nsAutoString aumid(u"TestApplicationID");
+ // We set up this expectation here because SetAppID will be called soon
+ // after construction of the JumpListBuilder via the background thread.
+ EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<WindowsJumpListShortcutDescription> descs;
+ nsTArray<JS::Value> customDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, descs,
+ customDescJSVals);
+
+ nsAutoString customTitle(u"My custom title");
+ nsTArray<JS::Value> taskDescJSVals;
+
+ EXPECT_CALL(*testBackend, AbortList()).Times(1);
+ EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
+ EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0);
+
+ EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()),
+ ShellLinksEq(&descs)))
+ .Times(1);
+ EXPECT_CALL(*testBackend, CommitList()).Times(1);
+ EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);
+
+ nsresult rv =
+ builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals,
+ cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolved();
+}
+
+/**
+ * Tests calling PopulateJumpList with tasks and custom items.
+ * This should call the following methods on the backend, in order:
+ *
+ * - SetAppID
+ * - AbortList
+ * - BeginList
+ * - AddUserTasks
+ * - AppendCategory
+ * - CommitList
+ *
+ * This should result in a jump list with both built-in tasks as well as
+ * custom tasks with a custom label.
+ */
+TEST(JumpListBuilder, PopulateJumpList)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ nsAutoString aumid(u"TestApplicationID");
+ // We set up this expectation here because SetAppID will be called soon
+ // after construction of the JumpListBuilder via the background thread.
+ EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<WindowsJumpListShortcutDescription> taskDescs;
+ nsTArray<JS::Value> taskDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs,
+ taskDescJSVals);
+
+ nsTArray<WindowsJumpListShortcutDescription> customDescs;
+ nsTArray<JS::Value> customDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, customDescs,
+ customDescJSVals);
+
+ nsAutoString customTitle(u"My custom title");
+
+ EXPECT_CALL(*testBackend, AbortList()).Times(1);
+ EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
+ EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1);
+
+ EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()),
+ ShellLinksEq(&customDescs)))
+ .Times(1);
+ EXPECT_CALL(*testBackend, CommitList()).Times(1);
+ EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);
+
+ nsresult rv =
+ builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals,
+ cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolved();
+}
+
+/**
+ * Tests calling ClearJumpList calls the following:
+ *
+ * - SetAppID
+ * - DeleteList (passing the aumid)
+ *
+ * This results in an empty jump list for the user.
+ */
+TEST(JumpListBuilder, ClearJumpList)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ nsAutoString aumid(u"TestApplicationID");
+ // We set up this expectation here because SetAppID will be called soon
+ // after construction of the JumpListBuilder via the background thread.
+ EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ EXPECT_CALL(*testBackend, AbortList()).Times(0);
+ EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(0);
+ EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0);
+
+ EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0);
+ EXPECT_CALL(*testBackend, CommitList()).Times(0);
+ EXPECT_CALL(*testBackend, DeleteList(LPCWSTREq(aumid.get()))).Times(1);
+
+ nsresult rv = builder->ClearJumpList(cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolved();
+}
+
+/**
+ * Test that a WindowsJumpListShortcutDescription with a description
+ * longer than MAX_PATH gets truncated to MAX_PATH. This is because a
+ * description longer than MAX_PATH will cause CommitList to fail.
+ */
+TEST(JumpListBuilder, TruncateDescription)
+{
+ RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ nsAutoString aumid(u"TestApplicationID");
+ // We set up this expectation here because SetAppID will be called soon
+ // after construction of the JumpListBuilder via the background thread.
+ EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);
+
+ nsCOMPtr<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<WindowsJumpListShortcutDescription> taskDescs;
+ nsTArray<JS::Value> taskDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, taskDescs,
+ taskDescJSVals);
+
+ nsTArray<WindowsJumpListShortcutDescription> customDescs;
+ nsTArray<JS::Value> customDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, customDescs,
+ customDescJSVals);
+ // We expect the long descriptions to be truncated to 260 characters, so
+ // we'll truncate the descriptions here ourselves.
+ for (auto& taskDesc : taskDescs) {
+ taskDesc.mDescription.SetLength(MAX_PATH - 1);
+ }
+ for (auto& customDesc : customDescs) {
+ customDesc.mDescription.SetLength(MAX_PATH - 1);
+ }
+
+ nsAutoString customTitle(u"My custom title");
+
+ EXPECT_CALL(*testBackend, AbortList()).Times(1);
+ EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
+ EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1);
+
+ EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()),
+ ShellLinksEq(&customDescs)))
+ .Times(1);
+ EXPECT_CALL(*testBackend, CommitList()).Times(1);
+ EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);
+
+ nsresult rv =
+ builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals,
+ cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolved();
+}
diff --git a/widget/windows/tests/gtest/TestWinDND.cpp b/widget/windows/tests/gtest/TestWinDND.cpp
new file mode 100644
index 0000000000..fb7849fd79
--- /dev/null
+++ b/widget/windows/tests/gtest/TestWinDND.cpp
@@ -0,0 +1,728 @@
+/* -*- 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 <windows.h>
+#include <ole2.h>
+#include <shlobj.h>
+
+#include "nsArray.h"
+#include "nsArrayUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+
+#include "nsClipboard.h"
+#include "nsDataObjCollection.h"
+
+#include "gtest/gtest.h"
+
+// shims for conversion from cppunittest to gtest
+template <size_t N>
+void fail(const char (&msg)[N]) {
+ ADD_FAILURE() << "TEST-UNEXPECTED-FAIL | " << msg;
+}
+template <size_t N>
+void passed(const char (&msg)[N]) {
+ GTEST_SUCCEED() << "TEST-PASS | " << msg;
+}
+
+nsIFile* xferFile;
+
+nsresult CheckValidHDROP(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ DROPFILES* pDropFiles;
+ pDropFiles = (DROPFILES*)GlobalLock(hGlobal);
+ if (!pDropFiles) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (pDropFiles->pFiles != sizeof(DROPFILES)) {
+ fail("DROPFILES struct has wrong size");
+ }
+
+ if (!pDropFiles->fWide) {
+ fail("Received data is not Unicode");
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsString s;
+ unsigned long offset = 0;
+ while (true) {
+ s = (char16_t*)((char*)pDropFiles + pDropFiles->pFiles + offset);
+ if (s.IsEmpty()) break;
+ nsresult rv;
+ nsCOMPtr<nsIFile> localFile(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ rv = localFile->InitWithPath(s);
+ if (NS_FAILED(rv)) {
+ fail("File could not be opened");
+ return NS_ERROR_UNEXPECTED;
+ }
+ offset += sizeof(char16_t) * (s.Length() + 1);
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidTEXT(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char* pText;
+ pText = (char*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidTEXTTwo(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char* pText;
+ pText = (char*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidUNICODE(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char16_t* pText;
+ pText = (char16_t*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidUNICODETwo(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char16_t* pText;
+ pText = (char16_t*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult GetTransferableFile(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferFile);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("application/x-moz-file", genericWrapper);
+ return rv;
+}
+
+nsresult GetTransferableText(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+ constexpr auto mozString = u"Mozilla can drag and drop"_ns;
+ nsCOMPtr<nsISupportsString> xferString =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ rv = xferString->SetData(mozString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("text/plain", genericWrapper);
+ return rv;
+}
+
+nsresult GetTransferableTextTwo(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+ constexpr auto mozString = u" twice over"_ns;
+ nsCOMPtr<nsISupportsString> xferString =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ rv = xferString->SetData(mozString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("text/plain", genericWrapper);
+ return rv;
+}
+
+nsresult GetTransferableURI(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> xferURI;
+
+ rv = NS_NewURI(getter_AddRefs(xferURI), "http://www.mozilla.org");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferURI);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("text/x-moz-url", genericWrapper);
+ return rv;
+}
+
+nsresult MakeDataObject(nsIArray* transferableArray,
+ RefPtr<IDataObject>& itemToDrag) {
+ nsresult rv;
+ uint32_t itemCount = 0;
+
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), "http://www.mozilla.org");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transferableArray->GetLength(&itemCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Copied more or less exactly from nsDragService::InvokeDragSession
+ // This is what lets us play fake Drag Service for the test
+ if (itemCount > 1) {
+ nsDataObjCollection* dataObjCollection = new nsDataObjCollection();
+ if (!dataObjCollection) return NS_ERROR_OUT_OF_MEMORY;
+ itemToDrag = dataObjCollection;
+ for (uint32_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, i);
+ if (trans) {
+ RefPtr<IDataObject> dataObj;
+ rv = nsClipboard::CreateNativeDataObject(trans, getter_AddRefs(dataObj),
+ uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Add the flavors to the collection object too
+ rv = nsClipboard::SetupNativeDataObject(trans, dataObjCollection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dataObjCollection->AddDataObject(dataObj);
+ }
+ }
+ } // if dragging multiple items
+ else {
+ nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, 0);
+ if (trans) {
+ rv = nsClipboard::CreateNativeDataObject(trans,
+ getter_AddRefs(itemToDrag), uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } // else dragging a single object
+ return rv;
+}
+
+nsresult Do_CheckOneFile() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableFile(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("File data object does not support the file data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("File data object did not provide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidHDROP(stg);
+ if (NS_FAILED(rv)) {
+ fail("HDROP was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ return NS_OK;
+}
+
+nsresult Do_CheckTwoFiles() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableFile(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = GetTransferableFile(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("File data object does not support the file data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("File data object did not provide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidHDROP(stg);
+ if (NS_FAILED(rv)) {
+ fail("HDROP was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ return NS_OK;
+}
+
+nsresult Do_CheckOneString() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the ASCII text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide ASCII data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidTEXT(stg);
+ if (NS_FAILED(rv)) {
+ fail("TEXT was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the wide text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide wide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidUNICODE(stg);
+ if (NS_FAILED(rv)) {
+ fail("UNICODE was invalid");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult Do_CheckTwoStrings() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = GetTransferableTextTwo(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the ASCII text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide ASCII data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidTEXTTwo(stg);
+ if (NS_FAILED(rv)) {
+ fail("TEXT was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the wide text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide wide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidUNICODETwo(stg);
+ if (NS_FAILED(rv)) {
+ fail("UNICODE was invalid");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult Do_CheckSetArbitraryData(bool aMultiple) {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ if (aMultiple) {
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ static CLIPFORMAT mozArbitraryFormat =
+ ::RegisterClipboardFormatW(L"MozillaTestFormat");
+ FORMATETC fe;
+ STGMEDIUM stg;
+ SET_FORMATETC(fe, mozArbitraryFormat, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+
+ HGLOBAL hg = GlobalAlloc(GPTR, 1024);
+ stg.tymed = TYMED_HGLOBAL;
+ stg.hGlobal = hg;
+ stg.pUnkForRelease = nullptr;
+
+ if (dataObj->SetData(&fe, &stg, true) != S_OK) {
+ if (aMultiple) {
+ fail("Unable to set arbitrary data type on data object collection!");
+ } else {
+ fail("Unable to set arbitrary data type on data object!");
+ }
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("Arbitrary data set on data object is not advertised!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg2;
+ stg2 = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg2) != S_OK) {
+ fail("Data object did not provide arbitrary data upon request!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (stg2->hGlobal != hg) {
+ fail("Arbitrary data was not returned properly!");
+ return rv;
+ }
+ ReleaseStgMedium(stg2);
+
+ return NS_OK;
+}
+
+// This function performs basic drop tests, testing a data object consisting
+// of one transferable
+nsresult Do_Test1() {
+ nsresult rv = NS_OK;
+ nsresult workingrv;
+
+ workingrv = Do_CheckOneFile();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on a single file");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working file drag object!");
+ }
+
+ workingrv = Do_CheckOneString();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on a single string");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working string drag object!");
+ }
+
+ workingrv = Do_CheckSetArbitraryData(false);
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on setting arbitrary data");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully set arbitrary data on a drag object");
+ }
+
+ return rv;
+}
+
+// This function performs basic drop tests, testing a data object consisting of
+// two transferables.
+nsresult Do_Test2() {
+ nsresult rv = NS_OK;
+ nsresult workingrv;
+
+ workingrv = Do_CheckTwoFiles();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on multiple files");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working multiple file drag object!");
+ }
+
+ workingrv = Do_CheckTwoStrings();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on multiple strings");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working multiple string drag object!");
+ }
+
+ workingrv = Do_CheckSetArbitraryData(true);
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on setting arbitrary data");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully set arbitrary data on a drag object");
+ }
+
+ return rv;
+}
+
+// This function performs advanced drag and drop tests, testing a data object
+// consisting of multiple transferables that have different data types
+nsresult Do_Test3() {
+ nsresult rv = NS_OK;
+ // nsresult workingrv;
+
+ // XXX TODO Write more advanced tests in Bug 535860
+ return rv;
+}
+
+nsCOMPtr<nsIFile> GetTemporaryDirectory() {
+ nsCOMPtr<nsIFile> tmpdir;
+
+#define ENSURE(expr) NS_ENSURE_SUCCESS(expr, nullptr);
+
+ ENSURE(NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpdir)));
+ MOZ_ASSERT(tmpdir);
+
+ ENSURE(tmpdir->AppendNative("TestWinDND"_ns));
+ ENSURE(tmpdir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0777));
+
+#undef ENSURE
+
+ return tmpdir;
+}
+
+TEST(TestWinDND, All)
+{
+ nsCOMPtr<nsIFile> file = GetTemporaryDirectory();
+ if (!file) {
+ fail("could not create temporary directory!");
+ return;
+ }
+ xferFile = file;
+
+ if (NS_SUCCEEDED(Do_Test1())) {
+ passed(
+ "Basic Drag and Drop data type tests (single transferable) succeeded!");
+ }
+
+ if (NS_SUCCEEDED(Do_Test2())) {
+ passed(
+ "Basic Drag and Drop data type tests (multiple transferables) "
+ "succeeded!");
+ }
+
+ // if (NS_SUCCEEDED(Do_Test3()))
+ // passed("Advanced Drag and Drop data type tests succeeded!");
+}
diff --git a/widget/windows/tests/gtest/moz.build b/widget/windows/tests/gtest/moz.build
new file mode 100644
index 0000000000..4057b7ae57
--- /dev/null
+++ b/widget/windows/tests/gtest/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "TestJumpListBuilder.cpp",
+ "TestWinDND.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/widget",
+ "/widget/windows",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/widget/windows/tests/moz.build b/widget/windows/tests/moz.build
new file mode 100644
index 0000000000..2c7d200571
--- /dev/null
+++ b/widget/windows/tests/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+GeckoCppUnitTests(
+ [
+ "TestUriValidation",
+ ],
+ linkage=None,
+)
+
+DIRS = ["gtest"]
+
+LOCAL_INCLUDES += []
+
+OS_LIBS += [
+ "oleaut32",
+ "ole32",
+ "shell32",
+ "shlwapi",
+ "urlmon",
+ "uuid",
+]
+
+if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"):
+ # This allows us to use wmain as the entry point on mingw
+ LDFLAGS += [
+ "-municode",
+ ]
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"]
diff --git a/widget/windows/tests/unit/test_windows_alert_service.js b/widget/windows/tests/unit/test_windows_alert_service.js
new file mode 100644
index 0000000000..0ba0d2a4d4
--- /dev/null
+++ b/widget/windows/tests/unit/test_windows_alert_service.js
@@ -0,0 +1,667 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that Windows alert notifications generate expected XML.
+ */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+let gProfD = do_get_profile();
+
+// Setup that allows to use the profile service in xpcshell tests,
+// lifted from `toolkit/profile/xpcshell/head.js`.
+function setupProfileService() {
+ let gDataHome = gProfD.clone();
+ gDataHome.append("data");
+ gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let gDataHomeLocal = gProfD.clone();
+ gDataHomeLocal.append("local");
+ gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
+ Ci.nsIXREDirProvider
+ );
+ xreDirProvider.setUserDataDirectory(gDataHome, false);
+ xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
+}
+
+add_setup(setupProfileService);
+
+function makeAlert(options) {
+ var alert = Cc["@mozilla.org/alert-notification;1"].createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(
+ options.name,
+ options.imageURL,
+ options.title,
+ options.text,
+ options.textClickable,
+ options.cookie,
+ options.dir,
+ options.lang,
+ options.data,
+ options.principal,
+ options.inPrivateBrowsing,
+ options.requireInteraction,
+ options.silent,
+ options.vibrate || []
+ );
+ if (options.actions) {
+ alert.actions = options.actions;
+ }
+ if (options.opaqueRelaunchData) {
+ alert.opaqueRelaunchData = options.opaqueRelaunchData;
+ }
+ return alert;
+}
+
+/**
+ * Take a `key1\nvalue1\n...` string encoding as used by the Windows native
+ * notification server DLL, and split it into an object, keeping `action\n...`
+ * intact.
+ *
+ * @param {string} t string encoding.
+ * @returns {object} an object with keys and values.
+ */
+function parseOneEncoded(t) {
+ var launch = {};
+
+ var lines = t.split("\n");
+ while (lines.length) {
+ var key = lines.shift();
+ var value;
+ if (key === "action") {
+ value = lines.join("\n");
+ lines = [];
+ } else {
+ value = lines.shift();
+ }
+ launch[key] = value;
+ }
+
+ return launch;
+}
+
+/**
+ * This complicated-looking function takes a (XML) string representation of a
+ * Windows alert (toast notification), parses it into XML, extracts and further
+ * parses internal data, and returns a simplified XML representation together
+ * with the parsed internals.
+ *
+ * Doing this lets us compare JSON objects rather than stringified-JSON further
+ * encoded as XML strings, which have lots of slashes and `&quot;` characters to
+ * contend with.
+ *
+ * @param {string} s XML string for Windows alert.
+
+ * @returns {Array} a pair of a simplified XML string and an object with
+ * `launch` and `actions` keys.
+ */
+function parseLaunchAndActions(s) {
+ var document = new DOMParser().parseFromString(s, "text/xml");
+ var root = document.documentElement;
+
+ var launchString = root.getAttribute("launch");
+ root.setAttribute("launch", "launch");
+ var launch = parseOneEncoded(launchString);
+
+ // `actions` is keyed by "content" attribute.
+ let actions = {};
+ for (var actionElement of root.querySelectorAll("action")) {
+ // `activationType="system"` is special. Leave them alone.
+ let systemActivationType =
+ actionElement.getAttribute("activationType") === "system";
+
+ let action = {};
+ let names = [...actionElement.attributes].map(attribute => attribute.name);
+
+ for (var name of names) {
+ let value = actionElement.getAttribute(name);
+
+ // Here is where we parse stringified-JSON to simplify comparisons.
+ if (value.startsWith("{")) {
+ value = JSON.parse(value);
+ if ("opaqueRelaunchData" in value) {
+ value.opaqueRelaunchData = JSON.parse(value.opaqueRelaunchData);
+ }
+ }
+
+ if (name == "arguments" && !systemActivationType) {
+ action[name] = parseOneEncoded(value);
+ } else {
+ action[name] = value;
+ }
+
+ if (name != "content" && !systemActivationType) {
+ actionElement.removeAttribute(name);
+ }
+ }
+
+ let actionName = actionElement.getAttribute("content");
+ actions[actionName] = action;
+ }
+
+ return [new XMLSerializer().serializeToString(document), { launch, actions }];
+}
+
+function escape(s) {
+ return s
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/\n/g, "&#xA;");
+}
+
+function unescape(s) {
+ return s
+ .replace(/&amp;/g, "&")
+ .replace(/&quot;/g, '"')
+ .replace(/&lt;/g, "<")
+ .replace(/&gt;/g, ">")
+ .replace(/&#xA;/g, "\n");
+}
+
+function testAlert(when, { serverEnabled, profD, isBackgroundTaskMode } = {}) {
+ let argumentString = action => {
+ // &#xA; is "\n".
+ let s = ``;
+ if (serverEnabled) {
+ s += `program&#xA;${AppConstants.MOZ_APP_NAME}`;
+ } else {
+ s += `invalid key&#xA;invalid value`;
+ }
+ if (serverEnabled && profD) {
+ s += `&#xA;profile&#xA;${profD.path}`;
+ }
+ if (serverEnabled) {
+ s += "&#xA;windowsTag&#xA;";
+ }
+ if (action) {
+ s += `&#xA;action&#xA;${escape(JSON.stringify(action))}`;
+ }
+
+ return s;
+ };
+
+ let parsedArgumentString = action =>
+ parseOneEncoded(unescape(argumentString(action)));
+
+ let settingsAction = isBackgroundTaskMode
+ ? ""
+ : `<action content="Notification settings"/>`;
+
+ let parsedSettingsAction = hostport => {
+ if (isBackgroundTaskMode) {
+ return [];
+ }
+ let content = "Notification settings";
+ return [
+ content,
+ {
+ content,
+ arguments: parsedArgumentString(
+ Object.assign(
+ {
+ action: "settings",
+ },
+ hostport && {
+ launchUrl: hostport,
+ }
+ )
+ ),
+ placement: "contextmenu",
+ },
+ ];
+ };
+
+ let parsedSnoozeAction = hostport => {
+ let content = `Disable notifications from ${hostport}`;
+ return [
+ content,
+ {
+ content,
+ arguments: parsedArgumentString(
+ Object.assign(
+ {
+ action: "snooze",
+ },
+ hostport && {
+ launchUrl: hostport,
+ }
+ )
+ ),
+ placement: "contextmenu",
+ },
+ ];
+ };
+
+ let alertsService = Cc["@mozilla.org/system-alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIWindowsAlertsService);
+
+ let name = "name";
+ let title = "title";
+ let text = "text";
+ let imageURL = "file:///image.png";
+ let actions = [
+ { action: "action1", title: "title1", iconURL: "file:///iconURL1.png" },
+ { action: "action2", title: "title2", iconURL: "file:///iconURL2.png" },
+ ];
+ let opaqueRelaunchData = { foo: 1, bar: "two" };
+
+ let alert = makeAlert({ name, title, text });
+ let expected = `<toast launch="launch"><visual><binding template="ToastText03"><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [parsedSettingsAction()].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ alert = makeAlert({ name, title, text, imageURL });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [parsedSettingsAction()].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ alert = makeAlert({ name, title, text, imageURL, requireInteraction: true });
+ expected = `<toast scenario="reminder" launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="Dismiss" arguments="dismiss" activationType="system"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsAction(),
+ [
+ "Dismiss",
+ {
+ content: "Dismiss",
+ arguments: "dismiss",
+ activationType: "system",
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ alert = makeAlert({ name, title, text, imageURL, actions });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsAction(),
+ [
+ "title1",
+ {
+ content: "title1",
+ arguments: parsedArgumentString({ action: "action1" }),
+ },
+ ],
+ [
+ "title2",
+ {
+ content: "title2",
+ arguments: parsedArgumentString({ action: "action2" }),
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // Chrome privileged alerts can use `windowsSystemActivationType`.
+ let systemActions = [
+ {
+ action: "dismiss",
+ title: "dismissTitle",
+ windowsSystemActivationType: true,
+ },
+ {
+ action: "snooze",
+ title: "snoozeTitle",
+ windowsSystemActivationType: true,
+ },
+ ];
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal: systemPrincipal,
+ actions: systemActions,
+ });
+ let parsedSettingsActionWithPrivilegedName = isBackgroundTaskMode
+ ? []
+ : [
+ "Notification settings",
+ {
+ content: "Notification settings",
+ arguments: parsedArgumentString({
+ action: "settings",
+ privilegedName: name,
+ }),
+ placement: "contextmenu",
+ },
+ ];
+
+ expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="dismissTitle" arguments="dismiss" activationType="system"/><action content="snoozeTitle" arguments="snooze" activationType="system"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "", privilegedName: name }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsActionWithPrivilegedName,
+ [
+ "dismissTitle",
+ {
+ content: "dismissTitle",
+ arguments: "dismiss",
+ activationType: "system",
+ },
+ ],
+ [
+ "snoozeTitle",
+ {
+ content: "snoozeTitle",
+ arguments: "snooze",
+ activationType: "system",
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // But content unprivileged alerts can't use `windowsSystemActivationType`.
+ let launchUrl = "https://example.com/foo/bar.html";
+ const principaluri = Services.io.newURI(launchUrl);
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ principaluri,
+ {}
+ );
+
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ actions: systemActions,
+ principal,
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}<action content="dismissTitle"/><action content="snoozeTitle"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({
+ action: "",
+ launchUrl: principaluri.hostPort,
+ }),
+ actions: Object.fromEntries(
+ [
+ parsedSnoozeAction(principaluri.hostPort),
+ parsedSettingsAction(principaluri.hostPort),
+ [
+ "dismissTitle",
+ {
+ content: "dismissTitle",
+ arguments: parsedArgumentString({
+ action: "dismiss",
+ launchUrl: principaluri.hostPort,
+ }),
+ },
+ ],
+ [
+ "snoozeTitle",
+ {
+ content: "snoozeTitle",
+ arguments: parsedArgumentString({
+ action: "snooze",
+ launchUrl: principaluri.hostPort,
+ }),
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // Chrome privileged alerts can set `opaqueRelaunchData`.
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal: systemPrincipal,
+ opaqueRelaunchData: JSON.stringify(opaqueRelaunchData),
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({
+ action: "",
+ opaqueRelaunchData: JSON.stringify(opaqueRelaunchData),
+ privilegedName: name,
+ }),
+ actions: Object.fromEntries(
+ [parsedSettingsActionWithPrivilegedName].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // But content unprivileged alerts can't set `opaqueRelaunchData`.
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal,
+ opaqueRelaunchData: JSON.stringify(opaqueRelaunchData),
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({
+ action: "",
+ launchUrl: principaluri.hostPort,
+ }),
+ actions: Object.fromEntries(
+ [
+ parsedSnoozeAction(principaluri.hostPort),
+ parsedSettingsAction(principaluri.hostPort),
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // Chrome privileged alerts can set action-specific relaunch parameters.
+ let systemRelaunchActions = [
+ {
+ action: "action1",
+ title: "title1",
+ opaqueRelaunchData: JSON.stringify({ json: "data1" }),
+ },
+ {
+ action: "action2",
+ title: "title2",
+ opaqueRelaunchData: JSON.stringify({ json: "data2" }),
+ },
+ ];
+ systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal: systemPrincipal,
+ actions: systemRelaunchActions,
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "", privilegedName: name }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsActionWithPrivilegedName,
+ [
+ "title1",
+ {
+ content: "title1",
+ arguments: parsedArgumentString(
+ {
+ action: "action1",
+ opaqueRelaunchData: JSON.stringify({ json: "data1" }),
+ privilegedName: name,
+ },
+ null,
+ name
+ ),
+ },
+ ],
+
+ [
+ "title2",
+ {
+ content: "title2",
+ arguments: parsedArgumentString(
+ {
+ action: "action2",
+ opaqueRelaunchData: JSON.stringify({ json: "data2" }),
+ privilegedName: name,
+ },
+ null,
+ name
+ ),
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+}
+
+add_task(async () => {
+ Services.prefs.deleteBranch(
+ "alerts.useSystemBackend.windows.notificationserver.enabled"
+ );
+ testAlert("when notification server pref is unset", {
+ profD: gProfD,
+ });
+
+ Services.prefs.setBoolPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ false
+ );
+ testAlert("when notification server pref is false", { profD: gProfD });
+
+ Services.prefs.setBoolPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ true
+ );
+ testAlert("when notification server pref is true", {
+ serverEnabled: true,
+ profD: gProfD,
+ });
+});
+
+let condition = {
+ skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS,
+};
+
+add_task(condition, async () => {
+ const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
+ Ci.nsIBackgroundTasks
+ );
+
+ // Pretend that this is a background task.
+ bts.overrideBackgroundTaskNameForTesting("taskname");
+
+ Services.prefs.setBoolPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ true
+ );
+ testAlert(
+ "when notification server pref is true in background task, no default profile",
+ { serverEnabled: true, isBackgroundTaskMode: true }
+ );
+
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+
+ let profilePath = do_get_profile();
+ profilePath.append(`test_windows_alert_service`);
+ let profile = profileService.createUniqueProfile(
+ profilePath,
+ "test_windows_alert_service"
+ );
+
+ profileService.defaultProfile = profile;
+
+ testAlert(
+ "when notification server pref is true in background task, default profile",
+ { serverEnabled: true, isBackgroundTaskMode: true, profD: profilePath }
+ );
+
+ // No longer a background task,
+ bts.overrideBackgroundTaskNameForTesting("");
+});
diff --git a/widget/windows/tests/unit/xpcshell.toml b/widget/windows/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..9943d5510e
--- /dev/null
+++ b/widget/windows/tests/unit/xpcshell.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["test_windows_alert_service.js"]
diff --git a/widget/windows/touchinjection_sdk80.h b/widget/windows/touchinjection_sdk80.h
new file mode 100644
index 0000000000..7e9f5410d2
--- /dev/null
+++ b/widget/windows/touchinjection_sdk80.h
@@ -0,0 +1,171 @@
+/* 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/. */
+
+#ifndef touchinjection_sdk80_h
+#define touchinjection_sdk80_h
+
+#include <windows.h>
+
+// Note, this isn't inclusive of all touch injection header info.
+// You may need to add more to expand on current apis.
+
+#ifndef TOUCH_FEEDBACK_DEFAULT
+
+# define TOUCH_FEEDBACK_DEFAULT 0x1
+# define TOUCH_FEEDBACK_INDIRECT 0x2
+# define TOUCH_FEEDBACK_NONE 0x3
+
+enum POINTER_FEEDBACK_MODE {
+ POINTER_FEEDBACK_DEFAULT =
+ 1, // The injected pointer input feedback may get suppressed by the
+ // end-user settings in the Pen and Touch control panel.
+ POINTER_FEEDBACK_INDIRECT =
+ 2, // The injected pointer input feedback overrides the end-user settings
+ // in the Pen and Touch control panel.
+ POINTER_FEEDBACK_NONE = 3, // No touch visualizations.
+};
+
+enum {
+ PT_POINTER = 0x00000001, // Generic pointer
+ PT_TOUCH = 0x00000002, // Touch
+ PT_PEN = 0x00000003, // Pen
+ PT_MOUSE = 0x00000004, // Mouse
+ PT_TOUCHPAD = 0x00000005, // Touch pad
+};
+
+using POINTER_INPUT_TYPE = DWORD;
+using POINTER_FLAGS = UINT32;
+
+enum POINTER_BUTTON_CHANGE_TYPE {
+ POINTER_CHANGE_NONE,
+ POINTER_CHANGE_FIRSTBUTTON_DOWN,
+ POINTER_CHANGE_FIRSTBUTTON_UP,
+ POINTER_CHANGE_SECONDBUTTON_DOWN,
+ POINTER_CHANGE_SECONDBUTTON_UP,
+ POINTER_CHANGE_THIRDBUTTON_DOWN,
+ POINTER_CHANGE_THIRDBUTTON_UP,
+ POINTER_CHANGE_FOURTHBUTTON_DOWN,
+ POINTER_CHANGE_FOURTHBUTTON_UP,
+ POINTER_CHANGE_FIFTHBUTTON_DOWN,
+ POINTER_CHANGE_FIFTHBUTTON_UP,
+};
+
+struct POINTER_INFO {
+ POINTER_INPUT_TYPE pointerType;
+ UINT32 pointerId;
+ UINT32 frameId;
+ POINTER_FLAGS pointerFlags;
+ HANDLE sourceDevice;
+ HWND hwndTarget;
+ POINT ptPixelLocation;
+ POINT ptHimetricLocation;
+ POINT ptPixelLocationRaw;
+ POINT ptHimetricLocationRaw;
+ DWORD dwTime;
+ UINT32 historyCount;
+ INT32 InputData;
+ DWORD dwKeyStates;
+ UINT64 PerformanceCount;
+ POINTER_BUTTON_CHANGE_TYPE ButtonChangeType;
+};
+
+using TOUCH_FLAGS = UINT32;
+using TOUCH_MASK = UINT32;
+
+struct POINTER_TOUCH_INFO {
+ POINTER_INFO pointerInfo;
+ TOUCH_FLAGS touchFlags;
+ TOUCH_MASK touchMask;
+ RECT rcContact;
+ RECT rcContactRaw;
+ UINT32 orientation;
+ UINT32 pressure;
+};
+
+# define PEN_FLAG_NONE 0x00000000 // Default
+# define PEN_FLAG_BARREL 0x00000001 // The barrel button is pressed
+# define PEN_FLAG_INVERTED 0x00000002 // The pen is inverted
+# define PEN_FLAG_ERASER 0x00000004 // The eraser button is pressed
+
+# define PEN_MASK_NONE \
+ 0x00000000 // Default - none of the optional fields are valid
+# define PEN_MASK_PRESSURE 0x00000001 // The pressure field is valid
+# define PEN_MASK_ROTATION 0x00000002 // The rotation field is valid
+# define PEN_MASK_TILT_X 0x00000004 // The tiltX field is valid
+# define PEN_MASK_TILT_Y 0x00000008 // The tiltY field is valid
+
+using PEN_FLAGS = UINT32;
+using PEN_MASK = UINT32;
+
+struct POINTER_PEN_INFO {
+ POINTER_INFO pointerInfo;
+ PEN_FLAGS penFlags;
+ PEN_MASK penMask;
+ UINT32 pressure;
+ UINT32 rotation;
+ INT32 tiltX;
+ INT32 tiltY;
+};
+
+struct POINTER_TYPE_INFO {
+ POINTER_INPUT_TYPE type;
+ union {
+ POINTER_TOUCH_INFO touchInfo;
+ POINTER_PEN_INFO penInfo;
+ };
+};
+
+# define TOUCH_FLAG_NONE 0x00000000 // Default
+
+# define TOUCH_MASK_NONE \
+ 0x00000000 // Default - none of the optional fields are valid
+# define TOUCH_MASK_CONTACTAREA 0x00000001 // The rcContact field is valid
+# define TOUCH_MASK_ORIENTATION 0x00000002 // The orientation field is valid
+# define TOUCH_MASK_PRESSURE 0x00000004 // The pressure field is valid
+
+# define POINTER_FLAG_NONE 0x00000000 // Default
+# define POINTER_FLAG_NEW 0x00000001 // New pointer
+# define POINTER_FLAG_INRANGE 0x00000002 // Pointer has not departed
+# define POINTER_FLAG_INCONTACT 0x00000004 // Pointer is in contact
+# define POINTER_FLAG_FIRSTBUTTON 0x00000010 // Primary action
+# define POINTER_FLAG_SECONDBUTTON 0x00000020 // Secondary action
+# define POINTER_FLAG_THIRDBUTTON 0x00000040 // Third button
+# define POINTER_FLAG_FOURTHBUTTON 0x00000080 // Fourth button
+# define POINTER_FLAG_FIFTHBUTTON 0x00000100 // Fifth button
+# define POINTER_FLAG_PRIMARY 0x00002000 // Pointer is primary
+# define POINTER_FLAG_CONFIDENCE \
+ 0x00004000 // Pointer is considered unlikely to be accidental
+# define POINTER_FLAG_CANCELED \
+ 0x00008000 // Pointer is departing in an abnormal manner
+# define POINTER_FLAG_DOWN \
+ 0x00010000 // Pointer transitioned to down state (made contact)
+# define POINTER_FLAG_UPDATE 0x00020000 // Pointer update
+# define POINTER_FLAG_UP \
+ 0x00040000 // Pointer transitioned from down state (broke contact)
+# define POINTER_FLAG_WHEEL 0x00080000 // Vertical wheel
+# define POINTER_FLAG_HWHEEL 0x00100000 // Horizontal wheel
+# define POINTER_FLAG_CAPTURECHANGED 0x00200000 // Lost capture
+
+#endif // TOUCH_FEEDBACK_DEFAULT
+
+#define TOUCH_FLAGS_CONTACTUPDATE \
+ (POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT)
+#define TOUCH_FLAGS_CONTACTDOWN \
+ (POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT)
+
+using InitializeTouchInjectionPtr = BOOL(WINAPI*)(UINT32, DWORD);
+using InjectTouchInputPtr = BOOL(WINAPI*)(UINT32, const POINTER_TOUCH_INFO*);
+
+#if !defined(NTDDI_WIN10_RS5) || (NTDDI_VERSION < NTDDI_WIN10_RS5)
+# define HSYNTHETICPOINTERDEVICE intptr_t
+#endif // NTDDI_VERSION < NTDDI_WIN10_RS5
+
+using CreateSyntheticPointerDevicePtr = HSYNTHETICPOINTERDEVICE(WINAPI*)(
+ POINTER_INPUT_TYPE, ULONG, POINTER_FEEDBACK_MODE);
+using DestroySyntheticPointerDevicePtr = void(WINAPI*)(HSYNTHETICPOINTERDEVICE);
+using InjectSyntheticPointerInputPtr = BOOL(WINAPI*)(HSYNTHETICPOINTERDEVICE,
+ const POINTER_TYPE_INFO*,
+ UINT32);
+
+#endif // touchinjection_sdk80_h
diff --git a/widget/windows/widget.rc b/widget/windows/widget.rc
new file mode 100644
index 0000000000..9361f9e48c
--- /dev/null
+++ b/widget/windows/widget.rc
@@ -0,0 +1,30 @@
+/* -*- 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 "resource.h"
+#include <winresrc.h>
+#include <dlgs.h>
+
+IDC_GRAB CURSOR DISCARDABLE "res/grab.cur"
+IDC_GRABBING CURSOR DISCARDABLE "res/grabbing.cur"
+IDC_CELL CURSOR DISCARDABLE "res/cell.cur"
+IDC_COPY CURSOR DISCARDABLE "res/copy.cur"
+IDC_ALIAS CURSOR DISCARDABLE "res/aliasb.cur"
+IDC_ZOOMIN CURSOR DISCARDABLE "res/zoom_in.cur"
+IDC_ZOOMOUT CURSOR DISCARDABLE "res/zoom_out.cur"
+IDC_COLRESIZE CURSOR DISCARDABLE "res/col_resize.cur"
+IDC_ROWRESIZE CURSOR DISCARDABLE "res/row_resize.cur"
+IDC_VERTICALTEXT CURSOR DISCARDABLE "res/vertical_text.cur"
+IDC_NONE CURSOR DISCARDABLE "res/none.cur"
+
+OPTPROPSHEET DIALOG DISCARDABLE 32, 32, 288, 226
+STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE |
+ WS_CAPTION | WS_SYSMENU
+CAPTION "Options"
+FONT 8, "MS Sans Serif"
+BEGIN
+
+END
diff --git a/widget/x11/keysym2ucs.c b/widget/x11/keysym2ucs.c
new file mode 100644
index 0000000000..b0c2229e6d
--- /dev/null
+++ b/widget/x11/keysym2ucs.c
@@ -0,0 +1,866 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/*
+ *
+ * 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/. */
+/*
+ * This module converts keysym values into the corresponding ISO 10646-1
+ * (UCS, Unicode) values.
+ *
+ * The array keysymtab[] contains pairs of X11 keysym values for graphical
+ * characters and the corresponding Unicode value. The function
+ * keysym2ucs() maps a keysym onto a Unicode value using a binary search,
+ * therefore keysymtab[] must remain SORTED by keysym value.
+ *
+ * The keysym -> UTF-8 conversion will hopefully one day be provided
+ * by Xlib via XmbLookupString() and should ideally not have to be
+ * done in X applications. But we are not there yet.
+ *
+ * We allow to represent any UCS character in the range U+00000000 to
+ * U+00FFFFFF by a keysym value in the range 0x01000000 to 0x01ffffff.
+ * This admittedly does not cover the entire 31-bit space of UCS, but
+ * it does cover all of the characters up to U+10FFFF, which can be
+ * represented by UTF-16, and more, and it is very unlikely that higher
+ * UCS codes will ever be assigned by ISO. So to get Unicode character
+ * U+ABCD you can directly use keysym 0x1000abcd.
+ *
+ * NOTE: The comments in the table below contain the actual character
+ * encoded in UTF-8, so for viewing and editing best use an editor in
+ * UTF-8 mode.
+ *
+ * Author: Markus G. Kuhn <mkuhn@acm.org>, University of Cambridge, June 1999
+ *
+ * Special thanks to Richard Verhoeven <river@win.tue.nl> for preparing
+ * an initial draft of the mapping table.
+ *
+ * This software is in the public domain. Share and enjoy!
+ */
+
+#include "keysym2ucs.h"
+
+static const struct codepair {
+ unsigned short keysym;
+ unsigned short ucs;
+} keysymtab[] = {
+ // clang-format off
+ { 0x01a1, 0x0104 }, /* Aogonek Ą LATIN CAPITAL LETTER A WITH OGONEK */
+ { 0x01a2, 0x02d8 }, /* breve ˘ BREVE */
+ { 0x01a3, 0x0141 }, /* Lstroke Ł LATIN CAPITAL LETTER L WITH STROKE */
+ { 0x01a5, 0x013d }, /* Lcaron Ľ LATIN CAPITAL LETTER L WITH CARON */
+ { 0x01a6, 0x015a }, /* Sacute Ś LATIN CAPITAL LETTER S WITH ACUTE */
+ { 0x01a9, 0x0160 }, /* Scaron Š LATIN CAPITAL LETTER S WITH CARON */
+ { 0x01aa, 0x015e }, /* Scedilla Ş LATIN CAPITAL LETTER S WITH CEDILLA */
+ { 0x01ab, 0x0164 }, /* Tcaron Ť LATIN CAPITAL LETTER T WITH CARON */
+ { 0x01ac, 0x0179 }, /* Zacute Ź LATIN CAPITAL LETTER Z WITH ACUTE */
+ { 0x01ae, 0x017d }, /* Zcaron Ž LATIN CAPITAL LETTER Z WITH CARON */
+ { 0x01af, 0x017b }, /* Zabovedot Ż LATIN CAPITAL LETTER Z WITH DOT ABOVE */
+ { 0x01b1, 0x0105 }, /* aogonek ą LATIN SMALL LETTER A WITH OGONEK */
+ { 0x01b2, 0x02db }, /* ogonek ˛ OGONEK */
+ { 0x01b3, 0x0142 }, /* lstroke ł LATIN SMALL LETTER L WITH STROKE */
+ { 0x01b5, 0x013e }, /* lcaron ľ LATIN SMALL LETTER L WITH CARON */
+ { 0x01b6, 0x015b }, /* sacute ś LATIN SMALL LETTER S WITH ACUTE */
+ { 0x01b7, 0x02c7 }, /* caron ˇ CARON */
+ { 0x01b9, 0x0161 }, /* scaron š LATIN SMALL LETTER S WITH CARON */
+ { 0x01ba, 0x015f }, /* scedilla ş LATIN SMALL LETTER S WITH CEDILLA */
+ { 0x01bb, 0x0165 }, /* tcaron ť LATIN SMALL LETTER T WITH CARON */
+ { 0x01bc, 0x017a }, /* zacute ź LATIN SMALL LETTER Z WITH ACUTE */
+ { 0x01bd, 0x02dd }, /* doubleacute ˝ DOUBLE ACUTE ACCENT */
+ { 0x01be, 0x017e }, /* zcaron ž LATIN SMALL LETTER Z WITH CARON */
+ { 0x01bf, 0x017c }, /* zabovedot ż LATIN SMALL LETTER Z WITH DOT ABOVE */
+ { 0x01c0, 0x0154 }, /* Racute Ŕ LATIN CAPITAL LETTER R WITH ACUTE */
+ { 0x01c3, 0x0102 }, /* Abreve Ă LATIN CAPITAL LETTER A WITH BREVE */
+ { 0x01c5, 0x0139 }, /* Lacute Ĺ LATIN CAPITAL LETTER L WITH ACUTE */
+ { 0x01c6, 0x0106 }, /* Cacute Ć LATIN CAPITAL LETTER C WITH ACUTE */
+ { 0x01c8, 0x010c }, /* Ccaron Č LATIN CAPITAL LETTER C WITH CARON */
+ { 0x01ca, 0x0118 }, /* Eogonek Ę LATIN CAPITAL LETTER E WITH OGONEK */
+ { 0x01cc, 0x011a }, /* Ecaron Ě LATIN CAPITAL LETTER E WITH CARON */
+ { 0x01cf, 0x010e }, /* Dcaron Ď LATIN CAPITAL LETTER D WITH CARON */
+ { 0x01d0, 0x0110 }, /* Dstroke Đ LATIN CAPITAL LETTER D WITH STROKE */
+ { 0x01d1, 0x0143 }, /* Nacute Ń LATIN CAPITAL LETTER N WITH ACUTE */
+ { 0x01d2, 0x0147 }, /* Ncaron Ň LATIN CAPITAL LETTER N WITH CARON */
+ { 0x01d5, 0x0150 }, /* Odoubleacute Ő LATIN CAPITAL LETTER O WITH DOUBLE ACUTE */
+ { 0x01d8, 0x0158 }, /* Rcaron Ř LATIN CAPITAL LETTER R WITH CARON */
+ { 0x01d9, 0x016e }, /* Uring Ů LATIN CAPITAL LETTER U WITH RING ABOVE */
+ { 0x01db, 0x0170 }, /* Udoubleacute Ű LATIN CAPITAL LETTER U WITH DOUBLE ACUTE */
+ { 0x01de, 0x0162 }, /* Tcedilla Ţ LATIN CAPITAL LETTER T WITH CEDILLA */
+ { 0x01e0, 0x0155 }, /* racute ŕ LATIN SMALL LETTER R WITH ACUTE */
+ { 0x01e3, 0x0103 }, /* abreve ă LATIN SMALL LETTER A WITH BREVE */
+ { 0x01e5, 0x013a }, /* lacute ĺ LATIN SMALL LETTER L WITH ACUTE */
+ { 0x01e6, 0x0107 }, /* cacute ć LATIN SMALL LETTER C WITH ACUTE */
+ { 0x01e8, 0x010d }, /* ccaron č LATIN SMALL LETTER C WITH CARON */
+ { 0x01ea, 0x0119 }, /* eogonek ę LATIN SMALL LETTER E WITH OGONEK */
+ { 0x01ec, 0x011b }, /* ecaron ě LATIN SMALL LETTER E WITH CARON */
+ { 0x01ef, 0x010f }, /* dcaron ď LATIN SMALL LETTER D WITH CARON */
+ { 0x01f0, 0x0111 }, /* dstroke đ LATIN SMALL LETTER D WITH STROKE */
+ { 0x01f1, 0x0144 }, /* nacute ń LATIN SMALL LETTER N WITH ACUTE */
+ { 0x01f2, 0x0148 }, /* ncaron ň LATIN SMALL LETTER N WITH CARON */
+ { 0x01f5, 0x0151 }, /* odoubleacute ő LATIN SMALL LETTER O WITH DOUBLE ACUTE */
+ { 0x01f8, 0x0159 }, /* rcaron ř LATIN SMALL LETTER R WITH CARON */
+ { 0x01f9, 0x016f }, /* uring ů LATIN SMALL LETTER U WITH RING ABOVE */
+ { 0x01fb, 0x0171 }, /* udoubleacute ű LATIN SMALL LETTER U WITH DOUBLE ACUTE */
+ { 0x01fe, 0x0163 }, /* tcedilla ţ LATIN SMALL LETTER T WITH CEDILLA */
+ { 0x01ff, 0x02d9 }, /* abovedot ˙ DOT ABOVE */
+ { 0x02a1, 0x0126 }, /* Hstroke Ħ LATIN CAPITAL LETTER H WITH STROKE */
+ { 0x02a6, 0x0124 }, /* Hcircumflex Ĥ LATIN CAPITAL LETTER H WITH CIRCUMFLEX */
+ { 0x02a9, 0x0130 }, /* Iabovedot İ LATIN CAPITAL LETTER I WITH DOT ABOVE */
+ { 0x02ab, 0x011e }, /* Gbreve Ğ LATIN CAPITAL LETTER G WITH BREVE */
+ { 0x02ac, 0x0134 }, /* Jcircumflex Ĵ LATIN CAPITAL LETTER J WITH CIRCUMFLEX */
+ { 0x02b1, 0x0127 }, /* hstroke ħ LATIN SMALL LETTER H WITH STROKE */
+ { 0x02b6, 0x0125 }, /* hcircumflex ĥ LATIN SMALL LETTER H WITH CIRCUMFLEX */
+ { 0x02b9, 0x0131 }, /* idotless ı LATIN SMALL LETTER DOTLESS I */
+ { 0x02bb, 0x011f }, /* gbreve ğ LATIN SMALL LETTER G WITH BREVE */
+ { 0x02bc, 0x0135 }, /* jcircumflex ĵ LATIN SMALL LETTER J WITH CIRCUMFLEX */
+ { 0x02c5, 0x010a }, /* Cabovedot Ċ LATIN CAPITAL LETTER C WITH DOT ABOVE */
+ { 0x02c6, 0x0108 }, /* Ccircumflex Ĉ LATIN CAPITAL LETTER C WITH CIRCUMFLEX */
+ { 0x02d5, 0x0120 }, /* Gabovedot Ġ LATIN CAPITAL LETTER G WITH DOT ABOVE */
+ { 0x02d8, 0x011c }, /* Gcircumflex Ĝ LATIN CAPITAL LETTER G WITH CIRCUMFLEX */
+ { 0x02dd, 0x016c }, /* Ubreve Ŭ LATIN CAPITAL LETTER U WITH BREVE */
+ { 0x02de, 0x015c }, /* Scircumflex Ŝ LATIN CAPITAL LETTER S WITH CIRCUMFLEX */
+ { 0x02e5, 0x010b }, /* cabovedot ċ LATIN SMALL LETTER C WITH DOT ABOVE */
+ { 0x02e6, 0x0109 }, /* ccircumflex ĉ LATIN SMALL LETTER C WITH CIRCUMFLEX */
+ { 0x02f5, 0x0121 }, /* gabovedot ġ LATIN SMALL LETTER G WITH DOT ABOVE */
+ { 0x02f8, 0x011d }, /* gcircumflex ĝ LATIN SMALL LETTER G WITH CIRCUMFLEX */
+ { 0x02fd, 0x016d }, /* ubreve ŭ LATIN SMALL LETTER U WITH BREVE */
+ { 0x02fe, 0x015d }, /* scircumflex ŝ LATIN SMALL LETTER S WITH CIRCUMFLEX */
+ { 0x03a2, 0x0138 }, /* kra ĸ LATIN SMALL LETTER KRA */
+ { 0x03a3, 0x0156 }, /* Rcedilla Ŗ LATIN CAPITAL LETTER R WITH CEDILLA */
+ { 0x03a5, 0x0128 }, /* Itilde Ĩ LATIN CAPITAL LETTER I WITH TILDE */
+ { 0x03a6, 0x013b }, /* Lcedilla Ļ LATIN CAPITAL LETTER L WITH CEDILLA */
+ { 0x03aa, 0x0112 }, /* Emacron Ē LATIN CAPITAL LETTER E WITH MACRON */
+ { 0x03ab, 0x0122 }, /* Gcedilla Ģ LATIN CAPITAL LETTER G WITH CEDILLA */
+ { 0x03ac, 0x0166 }, /* Tslash Ŧ LATIN CAPITAL LETTER T WITH STROKE */
+ { 0x03b3, 0x0157 }, /* rcedilla ŗ LATIN SMALL LETTER R WITH CEDILLA */
+ { 0x03b5, 0x0129 }, /* itilde ĩ LATIN SMALL LETTER I WITH TILDE */
+ { 0x03b6, 0x013c }, /* lcedilla ļ LATIN SMALL LETTER L WITH CEDILLA */
+ { 0x03ba, 0x0113 }, /* emacron ē LATIN SMALL LETTER E WITH MACRON */
+ { 0x03bb, 0x0123 }, /* gcedilla ģ LATIN SMALL LETTER G WITH CEDILLA */
+ { 0x03bc, 0x0167 }, /* tslash ŧ LATIN SMALL LETTER T WITH STROKE */
+ { 0x03bd, 0x014a }, /* ENG Ŋ LATIN CAPITAL LETTER ENG */
+ { 0x03bf, 0x014b }, /* eng ŋ LATIN SMALL LETTER ENG */
+ { 0x03c0, 0x0100 }, /* Amacron Ā LATIN CAPITAL LETTER A WITH MACRON */
+ { 0x03c7, 0x012e }, /* Iogonek Į LATIN CAPITAL LETTER I WITH OGONEK */
+ { 0x03cc, 0x0116 }, /* Eabovedot Ė LATIN CAPITAL LETTER E WITH DOT ABOVE */
+ { 0x03cf, 0x012a }, /* Imacron Ī LATIN CAPITAL LETTER I WITH MACRON */
+ { 0x03d1, 0x0145 }, /* Ncedilla Ņ LATIN CAPITAL LETTER N WITH CEDILLA */
+ { 0x03d2, 0x014c }, /* Omacron Ō LATIN CAPITAL LETTER O WITH MACRON */
+ { 0x03d3, 0x0136 }, /* Kcedilla Ķ LATIN CAPITAL LETTER K WITH CEDILLA */
+ { 0x03d9, 0x0172 }, /* Uogonek Ų LATIN CAPITAL LETTER U WITH OGONEK */
+ { 0x03dd, 0x0168 }, /* Utilde Ũ LATIN CAPITAL LETTER U WITH TILDE */
+ { 0x03de, 0x016a }, /* Umacron Ū LATIN CAPITAL LETTER U WITH MACRON */
+ { 0x03e0, 0x0101 }, /* amacron ā LATIN SMALL LETTER A WITH MACRON */
+ { 0x03e7, 0x012f }, /* iogonek į LATIN SMALL LETTER I WITH OGONEK */
+ { 0x03ec, 0x0117 }, /* eabovedot ė LATIN SMALL LETTER E WITH DOT ABOVE */
+ { 0x03ef, 0x012b }, /* imacron ī LATIN SMALL LETTER I WITH MACRON */
+ { 0x03f1, 0x0146 }, /* ncedilla ņ LATIN SMALL LETTER N WITH CEDILLA */
+ { 0x03f2, 0x014d }, /* omacron ō LATIN SMALL LETTER O WITH MACRON */
+ { 0x03f3, 0x0137 }, /* kcedilla ķ LATIN SMALL LETTER K WITH CEDILLA */
+ { 0x03f9, 0x0173 }, /* uogonek ų LATIN SMALL LETTER U WITH OGONEK */
+ { 0x03fd, 0x0169 }, /* utilde ũ LATIN SMALL LETTER U WITH TILDE */
+ { 0x03fe, 0x016b }, /* umacron ū LATIN SMALL LETTER U WITH MACRON */
+ { 0x047e, 0x203e }, /* overline ‾ OVERLINE */
+ { 0x04a1, 0x3002 }, /* kana_fullstop 。 IDEOGRAPHIC FULL STOP */
+ { 0x04a2, 0x300c }, /* kana_openingbracket 「 LEFT CORNER BRACKET */
+ { 0x04a3, 0x300d }, /* kana_closingbracket 」 RIGHT CORNER BRACKET */
+ { 0x04a4, 0x3001 }, /* kana_comma 、 IDEOGRAPHIC COMMA */
+ { 0x04a5, 0x30fb }, /* kana_conjunctive ・ KATAKANA MIDDLE DOT */
+ { 0x04a6, 0x30f2 }, /* kana_WO ヲ KATAKANA LETTER WO */
+ { 0x04a7, 0x30a1 }, /* kana_a ァ KATAKANA LETTER SMALL A */
+ { 0x04a8, 0x30a3 }, /* kana_i ィ KATAKANA LETTER SMALL I */
+ { 0x04a9, 0x30a5 }, /* kana_u ゥ KATAKANA LETTER SMALL U */
+ { 0x04aa, 0x30a7 }, /* kana_e ェ KATAKANA LETTER SMALL E */
+ { 0x04ab, 0x30a9 }, /* kana_o ォ KATAKANA LETTER SMALL O */
+ { 0x04ac, 0x30e3 }, /* kana_ya ャ KATAKANA LETTER SMALL YA */
+ { 0x04ad, 0x30e5 }, /* kana_yu ュ KATAKANA LETTER SMALL YU */
+ { 0x04ae, 0x30e7 }, /* kana_yo ョ KATAKANA LETTER SMALL YO */
+ { 0x04af, 0x30c3 }, /* kana_tsu ッ KATAKANA LETTER SMALL TU */
+ { 0x04b0, 0x30fc }, /* prolongedsound ー KATAKANA-HIRAGANA PROLONGED SOUND MARK */
+ { 0x04b1, 0x30a2 }, /* kana_A ア KATAKANA LETTER A */
+ { 0x04b2, 0x30a4 }, /* kana_I イ KATAKANA LETTER I */
+ { 0x04b3, 0x30a6 }, /* kana_U ウ KATAKANA LETTER U */
+ { 0x04b4, 0x30a8 }, /* kana_E エ KATAKANA LETTER E */
+ { 0x04b5, 0x30aa }, /* kana_O オ KATAKANA LETTER O */
+ { 0x04b6, 0x30ab }, /* kana_KA カ KATAKANA LETTER KA */
+ { 0x04b7, 0x30ad }, /* kana_KI キ KATAKANA LETTER KI */
+ { 0x04b8, 0x30af }, /* kana_KU ク KATAKANA LETTER KU */
+ { 0x04b9, 0x30b1 }, /* kana_KE ケ KATAKANA LETTER KE */
+ { 0x04ba, 0x30b3 }, /* kana_KO コ KATAKANA LETTER KO */
+ { 0x04bb, 0x30b5 }, /* kana_SA サ KATAKANA LETTER SA */
+ { 0x04bc, 0x30b7 }, /* kana_SHI シ KATAKANA LETTER SI */
+ { 0x04bd, 0x30b9 }, /* kana_SU ス KATAKANA LETTER SU */
+ { 0x04be, 0x30bb }, /* kana_SE セ KATAKANA LETTER SE */
+ { 0x04bf, 0x30bd }, /* kana_SO ソ KATAKANA LETTER SO */
+ { 0x04c0, 0x30bf }, /* kana_TA タ KATAKANA LETTER TA */
+ { 0x04c1, 0x30c1 }, /* kana_CHI チ KATAKANA LETTER TI */
+ { 0x04c2, 0x30c4 }, /* kana_TSU ツ KATAKANA LETTER TU */
+ { 0x04c3, 0x30c6 }, /* kana_TE テ KATAKANA LETTER TE */
+ { 0x04c4, 0x30c8 }, /* kana_TO ト KATAKANA LETTER TO */
+ { 0x04c5, 0x30ca }, /* kana_NA ナ KATAKANA LETTER NA */
+ { 0x04c6, 0x30cb }, /* kana_NI ニ KATAKANA LETTER NI */
+ { 0x04c7, 0x30cc }, /* kana_NU ヌ KATAKANA LETTER NU */
+ { 0x04c8, 0x30cd }, /* kana_NE ネ KATAKANA LETTER NE */
+ { 0x04c9, 0x30ce }, /* kana_NO ノ KATAKANA LETTER NO */
+ { 0x04ca, 0x30cf }, /* kana_HA ハ KATAKANA LETTER HA */
+ { 0x04cb, 0x30d2 }, /* kana_HI ヒ KATAKANA LETTER HI */
+ { 0x04cc, 0x30d5 }, /* kana_FU フ KATAKANA LETTER HU */
+ { 0x04cd, 0x30d8 }, /* kana_HE ヘ KATAKANA LETTER HE */
+ { 0x04ce, 0x30db }, /* kana_HO ホ KATAKANA LETTER HO */
+ { 0x04cf, 0x30de }, /* kana_MA マ KATAKANA LETTER MA */
+ { 0x04d0, 0x30df }, /* kana_MI ミ KATAKANA LETTER MI */
+ { 0x04d1, 0x30e0 }, /* kana_MU ム KATAKANA LETTER MU */
+ { 0x04d2, 0x30e1 }, /* kana_ME メ KATAKANA LETTER ME */
+ { 0x04d3, 0x30e2 }, /* kana_MO モ KATAKANA LETTER MO */
+ { 0x04d4, 0x30e4 }, /* kana_YA ヤ KATAKANA LETTER YA */
+ { 0x04d5, 0x30e6 }, /* kana_YU ユ KATAKANA LETTER YU */
+ { 0x04d6, 0x30e8 }, /* kana_YO ヨ KATAKANA LETTER YO */
+ { 0x04d7, 0x30e9 }, /* kana_RA ラ KATAKANA LETTER RA */
+ { 0x04d8, 0x30ea }, /* kana_RI リ KATAKANA LETTER RI */
+ { 0x04d9, 0x30eb }, /* kana_RU ル KATAKANA LETTER RU */
+ { 0x04da, 0x30ec }, /* kana_RE レ KATAKANA LETTER RE */
+ { 0x04db, 0x30ed }, /* kana_RO ロ KATAKANA LETTER RO */
+ { 0x04dc, 0x30ef }, /* kana_WA ワ KATAKANA LETTER WA */
+ { 0x04dd, 0x30f3 }, /* kana_N ン KATAKANA LETTER N */
+ { 0x04de, 0x309b }, /* voicedsound ゛ KATAKANA-HIRAGANA VOICED SOUND MARK */
+ { 0x04df, 0x309c }, /* semivoicedsound ゜ KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK */
+ { 0x05ac, 0x060c }, /* Arabic_comma ، ARABIC COMMA */
+ { 0x05bb, 0x061b }, /* Arabic_semicolon ؛ ARABIC SEMICOLON */
+ { 0x05bf, 0x061f }, /* Arabic_question_mark ؟ ARABIC QUESTION MARK */
+ { 0x05c1, 0x0621 }, /* Arabic_hamza ء ARABIC LETTER HAMZA */
+ { 0x05c2, 0x0622 }, /* Arabic_maddaonalef آ ARABIC LETTER ALEF WITH MADDA ABOVE */
+ { 0x05c3, 0x0623 }, /* Arabic_hamzaonalef أ ARABIC LETTER ALEF WITH HAMZA ABOVE */
+ { 0x05c4, 0x0624 }, /* Arabic_hamzaonwaw ؤ ARABIC LETTER WAW WITH HAMZA ABOVE */
+ { 0x05c5, 0x0625 }, /* Arabic_hamzaunderalef إ ARABIC LETTER ALEF WITH HAMZA BELOW */
+ { 0x05c6, 0x0626 }, /* Arabic_hamzaonyeh ئ ARABIC LETTER YEH WITH HAMZA ABOVE */
+ { 0x05c7, 0x0627 }, /* Arabic_alef ا ARABIC LETTER ALEF */
+ { 0x05c8, 0x0628 }, /* Arabic_beh ب ARABIC LETTER BEH */
+ { 0x05c9, 0x0629 }, /* Arabic_tehmarbuta ة ARABIC LETTER TEH MARBUTA */
+ { 0x05ca, 0x062a }, /* Arabic_teh ت ARABIC LETTER TEH */
+ { 0x05cb, 0x062b }, /* Arabic_theh ث ARABIC LETTER THEH */
+ { 0x05cc, 0x062c }, /* Arabic_jeem ج ARABIC LETTER JEEM */
+ { 0x05cd, 0x062d }, /* Arabic_hah ح ARABIC LETTER HAH */
+ { 0x05ce, 0x062e }, /* Arabic_khah خ ARABIC LETTER KHAH */
+ { 0x05cf, 0x062f }, /* Arabic_dal د ARABIC LETTER DAL */
+ { 0x05d0, 0x0630 }, /* Arabic_thal ذ ARABIC LETTER THAL */
+ { 0x05d1, 0x0631 }, /* Arabic_ra ر ARABIC LETTER REH */
+ { 0x05d2, 0x0632 }, /* Arabic_zain ز ARABIC LETTER ZAIN */
+ { 0x05d3, 0x0633 }, /* Arabic_seen س ARABIC LETTER SEEN */
+ { 0x05d4, 0x0634 }, /* Arabic_sheen ش ARABIC LETTER SHEEN */
+ { 0x05d5, 0x0635 }, /* Arabic_sad ص ARABIC LETTER SAD */
+ { 0x05d6, 0x0636 }, /* Arabic_dad ض ARABIC LETTER DAD */
+ { 0x05d7, 0x0637 }, /* Arabic_tah ط ARABIC LETTER TAH */
+ { 0x05d8, 0x0638 }, /* Arabic_zah ظ ARABIC LETTER ZAH */
+ { 0x05d9, 0x0639 }, /* Arabic_ain ع ARABIC LETTER AIN */
+ { 0x05da, 0x063a }, /* Arabic_ghain غ ARABIC LETTER GHAIN */
+ { 0x05e0, 0x0640 }, /* Arabic_tatweel ـ ARABIC TATWEEL */
+ { 0x05e1, 0x0641 }, /* Arabic_feh ف ARABIC LETTER FEH */
+ { 0x05e2, 0x0642 }, /* Arabic_qaf ق ARABIC LETTER QAF */
+ { 0x05e3, 0x0643 }, /* Arabic_kaf ك ARABIC LETTER KAF */
+ { 0x05e4, 0x0644 }, /* Arabic_lam ل ARABIC LETTER LAM */
+ { 0x05e5, 0x0645 }, /* Arabic_meem م ARABIC LETTER MEEM */
+ { 0x05e6, 0x0646 }, /* Arabic_noon ن ARABIC LETTER NOON */
+ { 0x05e7, 0x0647 }, /* Arabic_ha ه ARABIC LETTER HEH */
+ { 0x05e8, 0x0648 }, /* Arabic_waw و ARABIC LETTER WAW */
+ { 0x05e9, 0x0649 }, /* Arabic_alefmaksura ى ARABIC LETTER ALEF MAKSURA */
+ { 0x05ea, 0x064a }, /* Arabic_yeh ي ARABIC LETTER YEH */
+ { 0x05eb, 0x064b }, /* Arabic_fathatan ً ARABIC FATHATAN */
+ { 0x05ec, 0x064c }, /* Arabic_dammatan ٌ ARABIC DAMMATAN */
+ { 0x05ed, 0x064d }, /* Arabic_kasratan ٍ ARABIC KASRATAN */
+ { 0x05ee, 0x064e }, /* Arabic_fatha َ ARABIC FATHA */
+ { 0x05ef, 0x064f }, /* Arabic_damma ُ ARABIC DAMMA */
+ { 0x05f0, 0x0650 }, /* Arabic_kasra ِ ARABIC KASRA */
+ { 0x05f1, 0x0651 }, /* Arabic_shadda ّ ARABIC SHADDA */
+ { 0x05f2, 0x0652 }, /* Arabic_sukun ْ ARABIC SUKUN */
+ { 0x06a1, 0x0452 }, /* Serbian_dje ђ CYRILLIC SMALL LETTER DJE */
+ { 0x06a2, 0x0453 }, /* Macedonia_gje ѓ CYRILLIC SMALL LETTER GJE */
+ { 0x06a3, 0x0451 }, /* Cyrillic_io ё CYRILLIC SMALL LETTER IO */
+ { 0x06a4, 0x0454 }, /* Ukrainian_ie є CYRILLIC SMALL LETTER UKRAINIAN IE */
+ { 0x06a5, 0x0455 }, /* Macedonia_dse ѕ CYRILLIC SMALL LETTER DZE */
+ { 0x06a6, 0x0456 }, /* Ukrainian_i і CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I */
+ { 0x06a7, 0x0457 }, /* Ukrainian_yi ї CYRILLIC SMALL LETTER YI */
+ { 0x06a8, 0x0458 }, /* Cyrillic_je ј CYRILLIC SMALL LETTER JE */
+ { 0x06a9, 0x0459 }, /* Cyrillic_lje љ CYRILLIC SMALL LETTER LJE */
+ { 0x06aa, 0x045a }, /* Cyrillic_nje њ CYRILLIC SMALL LETTER NJE */
+ { 0x06ab, 0x045b }, /* Serbian_tshe ћ CYRILLIC SMALL LETTER TSHE */
+ { 0x06ac, 0x045c }, /* Macedonia_kje ќ CYRILLIC SMALL LETTER KJE */
+ { 0x06ad, 0x0491 }, /* Ukrainian_ghe_with_upturn ґ CYRILLIC SMALL LETTER GHE WITH UPTURN */
+ { 0x06ae, 0x045e }, /* Byelorussian_shortu ў CYRILLIC SMALL LETTER SHORT U */
+ { 0x06af, 0x045f }, /* Cyrillic_dzhe џ CYRILLIC SMALL LETTER DZHE */
+ { 0x06b0, 0x2116 }, /* numerosign № NUMERO SIGN */
+ { 0x06b1, 0x0402 }, /* Serbian_DJE Ђ CYRILLIC CAPITAL LETTER DJE */
+ { 0x06b2, 0x0403 }, /* Macedonia_GJE Ѓ CYRILLIC CAPITAL LETTER GJE */
+ { 0x06b3, 0x0401 }, /* Cyrillic_IO Ё CYRILLIC CAPITAL LETTER IO */
+ { 0x06b4, 0x0404 }, /* Ukrainian_IE Є CYRILLIC CAPITAL LETTER UKRAINIAN IE */
+ { 0x06b5, 0x0405 }, /* Macedonia_DSE Ѕ CYRILLIC CAPITAL LETTER DZE */
+ { 0x06b6, 0x0406 }, /* Ukrainian_I І CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I */
+ { 0x06b7, 0x0407 }, /* Ukrainian_YI Ї CYRILLIC CAPITAL LETTER YI */
+ { 0x06b8, 0x0408 }, /* Cyrillic_JE Ј CYRILLIC CAPITAL LETTER JE */
+ { 0x06b9, 0x0409 }, /* Cyrillic_LJE Љ CYRILLIC CAPITAL LETTER LJE */
+ { 0x06ba, 0x040a }, /* Cyrillic_NJE Њ CYRILLIC CAPITAL LETTER NJE */
+ { 0x06bb, 0x040b }, /* Serbian_TSHE Ћ CYRILLIC CAPITAL LETTER TSHE */
+ { 0x06bc, 0x040c }, /* Macedonia_KJE Ќ CYRILLIC CAPITAL LETTER KJE */
+ { 0x06bd, 0x0490 }, /* Ukrainian_GHE_WITH_UPTURN Ґ CYRILLIC CAPITAL LETTER GHE WITH UPTURN */
+ { 0x06be, 0x040e }, /* Byelorussian_SHORTU Ў CYRILLIC CAPITAL LETTER SHORT U */
+ { 0x06bf, 0x040f }, /* Cyrillic_DZHE Џ CYRILLIC CAPITAL LETTER DZHE */
+ { 0x06c0, 0x044e }, /* Cyrillic_yu ю CYRILLIC SMALL LETTER YU */
+ { 0x06c1, 0x0430 }, /* Cyrillic_a а CYRILLIC SMALL LETTER A */
+ { 0x06c2, 0x0431 }, /* Cyrillic_be б CYRILLIC SMALL LETTER BE */
+ { 0x06c3, 0x0446 }, /* Cyrillic_tse ц CYRILLIC SMALL LETTER TSE */
+ { 0x06c4, 0x0434 }, /* Cyrillic_de д CYRILLIC SMALL LETTER DE */
+ { 0x06c5, 0x0435 }, /* Cyrillic_ie е CYRILLIC SMALL LETTER IE */
+ { 0x06c6, 0x0444 }, /* Cyrillic_ef ф CYRILLIC SMALL LETTER EF */
+ { 0x06c7, 0x0433 }, /* Cyrillic_ghe г CYRILLIC SMALL LETTER GHE */
+ { 0x06c8, 0x0445 }, /* Cyrillic_ha х CYRILLIC SMALL LETTER HA */
+ { 0x06c9, 0x0438 }, /* Cyrillic_i и CYRILLIC SMALL LETTER I */
+ { 0x06ca, 0x0439 }, /* Cyrillic_shorti й CYRILLIC SMALL LETTER SHORT I */
+ { 0x06cb, 0x043a }, /* Cyrillic_ka к CYRILLIC SMALL LETTER KA */
+ { 0x06cc, 0x043b }, /* Cyrillic_el л CYRILLIC SMALL LETTER EL */
+ { 0x06cd, 0x043c }, /* Cyrillic_em м CYRILLIC SMALL LETTER EM */
+ { 0x06ce, 0x043d }, /* Cyrillic_en н CYRILLIC SMALL LETTER EN */
+ { 0x06cf, 0x043e }, /* Cyrillic_o о CYRILLIC SMALL LETTER O */
+ { 0x06d0, 0x043f }, /* Cyrillic_pe п CYRILLIC SMALL LETTER PE */
+ { 0x06d1, 0x044f }, /* Cyrillic_ya я CYRILLIC SMALL LETTER YA */
+ { 0x06d2, 0x0440 }, /* Cyrillic_er р CYRILLIC SMALL LETTER ER */
+ { 0x06d3, 0x0441 }, /* Cyrillic_es с CYRILLIC SMALL LETTER ES */
+ { 0x06d4, 0x0442 }, /* Cyrillic_te т CYRILLIC SMALL LETTER TE */
+ { 0x06d5, 0x0443 }, /* Cyrillic_u у CYRILLIC SMALL LETTER U */
+ { 0x06d6, 0x0436 }, /* Cyrillic_zhe ж CYRILLIC SMALL LETTER ZHE */
+ { 0x06d7, 0x0432 }, /* Cyrillic_ve в CYRILLIC SMALL LETTER VE */
+ { 0x06d8, 0x044c }, /* Cyrillic_softsign ь CYRILLIC SMALL LETTER SOFT SIGN */
+ { 0x06d9, 0x044b }, /* Cyrillic_yeru ы CYRILLIC SMALL LETTER YERU */
+ { 0x06da, 0x0437 }, /* Cyrillic_ze з CYRILLIC SMALL LETTER ZE */
+ { 0x06db, 0x0448 }, /* Cyrillic_sha ш CYRILLIC SMALL LETTER SHA */
+ { 0x06dc, 0x044d }, /* Cyrillic_e э CYRILLIC SMALL LETTER E */
+ { 0x06dd, 0x0449 }, /* Cyrillic_shcha щ CYRILLIC SMALL LETTER SHCHA */
+ { 0x06de, 0x0447 }, /* Cyrillic_che ч CYRILLIC SMALL LETTER CHE */
+ { 0x06df, 0x044a }, /* Cyrillic_hardsign ъ CYRILLIC SMALL LETTER HARD SIGN */
+ { 0x06e0, 0x042e }, /* Cyrillic_YU Ю CYRILLIC CAPITAL LETTER YU */
+ { 0x06e1, 0x0410 }, /* Cyrillic_A А CYRILLIC CAPITAL LETTER A */
+ { 0x06e2, 0x0411 }, /* Cyrillic_BE Б CYRILLIC CAPITAL LETTER BE */
+ { 0x06e3, 0x0426 }, /* Cyrillic_TSE Ц CYRILLIC CAPITAL LETTER TSE */
+ { 0x06e4, 0x0414 }, /* Cyrillic_DE Д CYRILLIC CAPITAL LETTER DE */
+ { 0x06e5, 0x0415 }, /* Cyrillic_IE Е CYRILLIC CAPITAL LETTER IE */
+ { 0x06e6, 0x0424 }, /* Cyrillic_EF Ф CYRILLIC CAPITAL LETTER EF */
+ { 0x06e7, 0x0413 }, /* Cyrillic_GHE Г CYRILLIC CAPITAL LETTER GHE */
+ { 0x06e8, 0x0425 }, /* Cyrillic_HA Х CYRILLIC CAPITAL LETTER HA */
+ { 0x06e9, 0x0418 }, /* Cyrillic_I И CYRILLIC CAPITAL LETTER I */
+ { 0x06ea, 0x0419 }, /* Cyrillic_SHORTI Й CYRILLIC CAPITAL LETTER SHORT I */
+ { 0x06eb, 0x041a }, /* Cyrillic_KA К CYRILLIC CAPITAL LETTER KA */
+ { 0x06ec, 0x041b }, /* Cyrillic_EL Л CYRILLIC CAPITAL LETTER EL */
+ { 0x06ed, 0x041c }, /* Cyrillic_EM М CYRILLIC CAPITAL LETTER EM */
+ { 0x06ee, 0x041d }, /* Cyrillic_EN Н CYRILLIC CAPITAL LETTER EN */
+ { 0x06ef, 0x041e }, /* Cyrillic_O О CYRILLIC CAPITAL LETTER O */
+ { 0x06f0, 0x041f }, /* Cyrillic_PE П CYRILLIC CAPITAL LETTER PE */
+ { 0x06f1, 0x042f }, /* Cyrillic_YA Я CYRILLIC CAPITAL LETTER YA */
+ { 0x06f2, 0x0420 }, /* Cyrillic_ER Р CYRILLIC CAPITAL LETTER ER */
+ { 0x06f3, 0x0421 }, /* Cyrillic_ES С CYRILLIC CAPITAL LETTER ES */
+ { 0x06f4, 0x0422 }, /* Cyrillic_TE Т CYRILLIC CAPITAL LETTER TE */
+ { 0x06f5, 0x0423 }, /* Cyrillic_U У CYRILLIC CAPITAL LETTER U */
+ { 0x06f6, 0x0416 }, /* Cyrillic_ZHE Ж CYRILLIC CAPITAL LETTER ZHE */
+ { 0x06f7, 0x0412 }, /* Cyrillic_VE В CYRILLIC CAPITAL LETTER VE */
+ { 0x06f8, 0x042c }, /* Cyrillic_SOFTSIGN Ь CYRILLIC CAPITAL LETTER SOFT SIGN */
+ { 0x06f9, 0x042b }, /* Cyrillic_YERU Ы CYRILLIC CAPITAL LETTER YERU */
+ { 0x06fa, 0x0417 }, /* Cyrillic_ZE З CYRILLIC CAPITAL LETTER ZE */
+ { 0x06fb, 0x0428 }, /* Cyrillic_SHA Ш CYRILLIC CAPITAL LETTER SHA */
+ { 0x06fc, 0x042d }, /* Cyrillic_E Э CYRILLIC CAPITAL LETTER E */
+ { 0x06fd, 0x0429 }, /* Cyrillic_SHCHA Щ CYRILLIC CAPITAL LETTER SHCHA */
+ { 0x06fe, 0x0427 }, /* Cyrillic_CHE Ч CYRILLIC CAPITAL LETTER CHE */
+ { 0x06ff, 0x042a }, /* Cyrillic_HARDSIGN Ъ CYRILLIC CAPITAL LETTER HARD SIGN */
+ { 0x07a1, 0x0386 }, /* Greek_ALPHAaccent Ά GREEK CAPITAL LETTER ALPHA WITH TONOS */
+ { 0x07a2, 0x0388 }, /* Greek_EPSILONaccent Έ GREEK CAPITAL LETTER EPSILON WITH TONOS */
+ { 0x07a3, 0x0389 }, /* Greek_ETAaccent Ή GREEK CAPITAL LETTER ETA WITH TONOS */
+ { 0x07a4, 0x038a }, /* Greek_IOTAaccent Ί GREEK CAPITAL LETTER IOTA WITH TONOS */
+ { 0x07a5, 0x03aa }, /* Greek_IOTAdiaeresis Ϊ GREEK CAPITAL LETTER IOTA WITH DIALYTIKA */
+ { 0x07a7, 0x038c }, /* Greek_OMICRONaccent Ό GREEK CAPITAL LETTER OMICRON WITH TONOS */
+ { 0x07a8, 0x038e }, /* Greek_UPSILONaccent Ύ GREEK CAPITAL LETTER UPSILON WITH TONOS */
+ { 0x07a9, 0x03ab }, /* Greek_UPSILONdieresis Ϋ GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA */
+ { 0x07ab, 0x038f }, /* Greek_OMEGAaccent Ώ GREEK CAPITAL LETTER OMEGA WITH TONOS */
+ { 0x07ae, 0x0385 }, /* Greek_accentdieresis ΅ GREEK DIALYTIKA TONOS */
+ { 0x07af, 0x2015 }, /* Greek_horizbar ― HORIZONTAL BAR */
+ { 0x07b1, 0x03ac }, /* Greek_alphaaccent ά GREEK SMALL LETTER ALPHA WITH TONOS */
+ { 0x07b2, 0x03ad }, /* Greek_epsilonaccent έ GREEK SMALL LETTER EPSILON WITH TONOS */
+ { 0x07b3, 0x03ae }, /* Greek_etaaccent ή GREEK SMALL LETTER ETA WITH TONOS */
+ { 0x07b4, 0x03af }, /* Greek_iotaaccent ί GREEK SMALL LETTER IOTA WITH TONOS */
+ { 0x07b5, 0x03ca }, /* Greek_iotadieresis ϊ GREEK SMALL LETTER IOTA WITH DIALYTIKA */
+ { 0x07b6, 0x0390 }, /* Greek_iotaaccentdieresis ΐ GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS */
+ { 0x07b7, 0x03cc }, /* Greek_omicronaccent ό GREEK SMALL LETTER OMICRON WITH TONOS */
+ { 0x07b8, 0x03cd }, /* Greek_upsilonaccent ύ GREEK SMALL LETTER UPSILON WITH TONOS */
+ { 0x07b9, 0x03cb }, /* Greek_upsilondieresis ϋ GREEK SMALL LETTER UPSILON WITH DIALYTIKA */
+ { 0x07ba, 0x03b0 }, /* Greek_upsilonaccentdieresis ΰ GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS */
+ { 0x07bb, 0x03ce }, /* Greek_omegaaccent ώ GREEK SMALL LETTER OMEGA WITH TONOS */
+ { 0x07c1, 0x0391 }, /* Greek_ALPHA Α GREEK CAPITAL LETTER ALPHA */
+ { 0x07c2, 0x0392 }, /* Greek_BETA Β GREEK CAPITAL LETTER BETA */
+ { 0x07c3, 0x0393 }, /* Greek_GAMMA Γ GREEK CAPITAL LETTER GAMMA */
+ { 0x07c4, 0x0394 }, /* Greek_DELTA Δ GREEK CAPITAL LETTER DELTA */
+ { 0x07c5, 0x0395 }, /* Greek_EPSILON Ε GREEK CAPITAL LETTER EPSILON */
+ { 0x07c6, 0x0396 }, /* Greek_ZETA Ζ GREEK CAPITAL LETTER ZETA */
+ { 0x07c7, 0x0397 }, /* Greek_ETA Η GREEK CAPITAL LETTER ETA */
+ { 0x07c8, 0x0398 }, /* Greek_THETA Θ GREEK CAPITAL LETTER THETA */
+ { 0x07c9, 0x0399 }, /* Greek_IOTA Ι GREEK CAPITAL LETTER IOTA */
+ { 0x07ca, 0x039a }, /* Greek_KAPPA Κ GREEK CAPITAL LETTER KAPPA */
+ { 0x07cb, 0x039b }, /* Greek_LAMBDA Λ GREEK CAPITAL LETTER LAMDA */
+ { 0x07cc, 0x039c }, /* Greek_MU Μ GREEK CAPITAL LETTER MU */
+ { 0x07cd, 0x039d }, /* Greek_NU Ν GREEK CAPITAL LETTER NU */
+ { 0x07ce, 0x039e }, /* Greek_XI Ξ GREEK CAPITAL LETTER XI */
+ { 0x07cf, 0x039f }, /* Greek_OMICRON Ο GREEK CAPITAL LETTER OMICRON */
+ { 0x07d0, 0x03a0 }, /* Greek_PI Π GREEK CAPITAL LETTER PI */
+ { 0x07d1, 0x03a1 }, /* Greek_RHO Ρ GREEK CAPITAL LETTER RHO */
+ { 0x07d2, 0x03a3 }, /* Greek_SIGMA Σ GREEK CAPITAL LETTER SIGMA */
+ { 0x07d4, 0x03a4 }, /* Greek_TAU Τ GREEK CAPITAL LETTER TAU */
+ { 0x07d5, 0x03a5 }, /* Greek_UPSILON Υ GREEK CAPITAL LETTER UPSILON */
+ { 0x07d6, 0x03a6 }, /* Greek_PHI Φ GREEK CAPITAL LETTER PHI */
+ { 0x07d7, 0x03a7 }, /* Greek_CHI Χ GREEK CAPITAL LETTER CHI */
+ { 0x07d8, 0x03a8 }, /* Greek_PSI Ψ GREEK CAPITAL LETTER PSI */
+ { 0x07d9, 0x03a9 }, /* Greek_OMEGA Ω GREEK CAPITAL LETTER OMEGA */
+ { 0x07e1, 0x03b1 }, /* Greek_alpha α GREEK SMALL LETTER ALPHA */
+ { 0x07e2, 0x03b2 }, /* Greek_beta β GREEK SMALL LETTER BETA */
+ { 0x07e3, 0x03b3 }, /* Greek_gamma γ GREEK SMALL LETTER GAMMA */
+ { 0x07e4, 0x03b4 }, /* Greek_delta δ GREEK SMALL LETTER DELTA */
+ { 0x07e5, 0x03b5 }, /* Greek_epsilon ε GREEK SMALL LETTER EPSILON */
+ { 0x07e6, 0x03b6 }, /* Greek_zeta ζ GREEK SMALL LETTER ZETA */
+ { 0x07e7, 0x03b7 }, /* Greek_eta η GREEK SMALL LETTER ETA */
+ { 0x07e8, 0x03b8 }, /* Greek_theta θ GREEK SMALL LETTER THETA */
+ { 0x07e9, 0x03b9 }, /* Greek_iota ι GREEK SMALL LETTER IOTA */
+ { 0x07ea, 0x03ba }, /* Greek_kappa κ GREEK SMALL LETTER KAPPA */
+ { 0x07eb, 0x03bb }, /* Greek_lambda λ GREEK SMALL LETTER LAMDA */
+ { 0x07ec, 0x03bc }, /* Greek_mu μ GREEK SMALL LETTER MU */
+ { 0x07ed, 0x03bd }, /* Greek_nu ν GREEK SMALL LETTER NU */
+ { 0x07ee, 0x03be }, /* Greek_xi ξ GREEK SMALL LETTER XI */
+ { 0x07ef, 0x03bf }, /* Greek_omicron ο GREEK SMALL LETTER OMICRON */
+ { 0x07f0, 0x03c0 }, /* Greek_pi π GREEK SMALL LETTER PI */
+ { 0x07f1, 0x03c1 }, /* Greek_rho ρ GREEK SMALL LETTER RHO */
+ { 0x07f2, 0x03c3 }, /* Greek_sigma σ GREEK SMALL LETTER SIGMA */
+ { 0x07f3, 0x03c2 }, /* Greek_finalsmallsigma ς GREEK SMALL LETTER FINAL SIGMA */
+ { 0x07f4, 0x03c4 }, /* Greek_tau τ GREEK SMALL LETTER TAU */
+ { 0x07f5, 0x03c5 }, /* Greek_upsilon υ GREEK SMALL LETTER UPSILON */
+ { 0x07f6, 0x03c6 }, /* Greek_phi φ GREEK SMALL LETTER PHI */
+ { 0x07f7, 0x03c7 }, /* Greek_chi χ GREEK SMALL LETTER CHI */
+ { 0x07f8, 0x03c8 }, /* Greek_psi ψ GREEK SMALL LETTER PSI */
+ { 0x07f9, 0x03c9 }, /* Greek_omega ω GREEK SMALL LETTER OMEGA */
+/* 0x08a1 leftradical ? ??? */
+/* 0x08a2 topleftradical ? ??? */
+/* 0x08a3 horizconnector ? ??? */
+ { 0x08a4, 0x2320 }, /* topintegral ⌠ TOP HALF INTEGRAL */
+ { 0x08a5, 0x2321 }, /* botintegral ⌡ BOTTOM HALF INTEGRAL */
+ { 0x08a6, 0x2502 }, /* vertconnector │ BOX DRAWINGS LIGHT VERTICAL */
+/* 0x08a7 topleftsqbracket ? ??? */
+/* 0x08a8 botleftsqbracket ? ??? */
+/* 0x08a9 toprightsqbracket ? ??? */
+/* 0x08aa botrightsqbracket ? ??? */
+/* 0x08ab topleftparens ? ??? */
+/* 0x08ac botleftparens ? ??? */
+/* 0x08ad toprightparens ? ??? */
+/* 0x08ae botrightparens ? ??? */
+/* 0x08af leftmiddlecurlybrace ? ??? */
+/* 0x08b0 rightmiddlecurlybrace ? ??? */
+/* 0x08b1 topleftsummation ? ??? */
+/* 0x08b2 botleftsummation ? ??? */
+/* 0x08b3 topvertsummationconnector ? ??? */
+/* 0x08b4 botvertsummationconnector ? ??? */
+/* 0x08b5 toprightsummation ? ??? */
+/* 0x08b6 botrightsummation ? ??? */
+/* 0x08b7 rightmiddlesummation ? ??? */
+ { 0x08bc, 0x2264 }, /* lessthanequal ≤ LESS-THAN OR EQUAL TO */
+ { 0x08bd, 0x2260 }, /* notequal ≠ NOT EQUAL TO */
+ { 0x08be, 0x2265 }, /* greaterthanequal ≥ GREATER-THAN OR EQUAL TO */
+ { 0x08bf, 0x222b }, /* integral ∫ INTEGRAL */
+ { 0x08c0, 0x2234 }, /* therefore ∴ THEREFORE */
+ { 0x08c1, 0x221d }, /* variation ∝ PROPORTIONAL TO */
+ { 0x08c2, 0x221e }, /* infinity ∞ INFINITY */
+ { 0x08c5, 0x2207 }, /* nabla ∇ NABLA */
+ { 0x08c8, 0x2245 }, /* approximate ≅ APPROXIMATELY EQUAL TO */
+/* 0x08c9 similarequal ? ??? */
+ { 0x08cd, 0x21d4 }, /* ifonlyif ⇔ LEFT RIGHT DOUBLE ARROW */
+ { 0x08ce, 0x21d2 }, /* implies ⇒ RIGHTWARDS DOUBLE ARROW */
+ { 0x08cf, 0x2261 }, /* identical ≡ IDENTICAL TO */
+ { 0x08d6, 0x221a }, /* radical √ SQUARE ROOT */
+ { 0x08da, 0x2282 }, /* includedin ⊂ SUBSET OF */
+ { 0x08db, 0x2283 }, /* includes ⊃ SUPERSET OF */
+ { 0x08dc, 0x2229 }, /* intersection ∩ INTERSECTION */
+ { 0x08dd, 0x222a }, /* union ∪ UNION */
+ { 0x08de, 0x2227 }, /* logicaland ∧ LOGICAL AND */
+ { 0x08df, 0x2228 }, /* logicalor ∨ LOGICAL OR */
+ { 0x08ef, 0x2202 }, /* partialderivative ∂ PARTIAL DIFFERENTIAL */
+ { 0x08f6, 0x0192 }, /* function ƒ LATIN SMALL LETTER F WITH HOOK */
+ { 0x08fb, 0x2190 }, /* leftarrow ← LEFTWARDS ARROW */
+ { 0x08fc, 0x2191 }, /* uparrow ↑ UPWARDS ARROW */
+ { 0x08fd, 0x2192 }, /* rightarrow → RIGHTWARDS ARROW */
+ { 0x08fe, 0x2193 }, /* downarrow ↓ DOWNWARDS ARROW */
+ { 0x09df, 0x2422 }, /* blank ␢ BLANK SYMBOL */
+ { 0x09e0, 0x25c6 }, /* soliddiamond ◆ BLACK DIAMOND */
+ { 0x09e1, 0x2592 }, /* checkerboard ▒ MEDIUM SHADE */
+ { 0x09e2, 0x2409 }, /* ht ␉ SYMBOL FOR HORIZONTAL TABULATION */
+ { 0x09e3, 0x240c }, /* ff ␌ SYMBOL FOR FORM FEED */
+ { 0x09e4, 0x240d }, /* cr ␍ SYMBOL FOR CARRIAGE RETURN */
+ { 0x09e5, 0x240a }, /* lf ␊ SYMBOL FOR LINE FEED */
+ { 0x09e8, 0x2424 }, /* nl ␤ SYMBOL FOR NEWLINE */
+ { 0x09e9, 0x240b }, /* vt ␋ SYMBOL FOR VERTICAL TABULATION */
+ { 0x09ea, 0x2518 }, /* lowrightcorner ┘ BOX DRAWINGS LIGHT UP AND LEFT */
+ { 0x09eb, 0x2510 }, /* uprightcorner ┐ BOX DRAWINGS LIGHT DOWN AND LEFT */
+ { 0x09ec, 0x250c }, /* upleftcorner ┌ BOX DRAWINGS LIGHT DOWN AND RIGHT */
+ { 0x09ed, 0x2514 }, /* lowleftcorner └ BOX DRAWINGS LIGHT UP AND RIGHT */
+ { 0x09ee, 0x253c }, /* crossinglines ┼ BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL */
+/* 0x09ef horizlinescan1 ? ??? */
+/* 0x09f0 horizlinescan3 ? ??? */
+ { 0x09f1, 0x2500 }, /* horizlinescan5 ─ BOX DRAWINGS LIGHT HORIZONTAL */
+/* 0x09f2 horizlinescan7 ? ??? */
+/* 0x09f3 horizlinescan9 ? ??? */
+ { 0x09f4, 0x251c }, /* leftt ├ BOX DRAWINGS LIGHT VERTICAL AND RIGHT */
+ { 0x09f5, 0x2524 }, /* rightt ┤ BOX DRAWINGS LIGHT VERTICAL AND LEFT */
+ { 0x09f6, 0x2534 }, /* bott ┴ BOX DRAWINGS LIGHT UP AND HORIZONTAL */
+ { 0x09f7, 0x252c }, /* topt ┬ BOX DRAWINGS LIGHT DOWN AND HORIZONTAL */
+ { 0x09f8, 0x2502 }, /* vertbar │ BOX DRAWINGS LIGHT VERTICAL */
+ { 0x0aa1, 0x2003 }, /* emspace   EM SPACE */
+ { 0x0aa2, 0x2002 }, /* enspace   EN SPACE */
+ { 0x0aa3, 0x2004 }, /* em3space   THREE-PER-EM SPACE */
+ { 0x0aa4, 0x2005 }, /* em4space   FOUR-PER-EM SPACE */
+ { 0x0aa5, 0x2007 }, /* digitspace   FIGURE SPACE */
+ { 0x0aa6, 0x2008 }, /* punctspace   PUNCTUATION SPACE */
+ { 0x0aa7, 0x2009 }, /* thinspace   THIN SPACE */
+ { 0x0aa8, 0x200a }, /* hairspace   HAIR SPACE */
+ { 0x0aa9, 0x2014 }, /* emdash — EM DASH */
+ { 0x0aaa, 0x2013 }, /* endash – EN DASH */
+/* 0x0aac signifblank ? ??? */
+ { 0x0aae, 0x2026 }, /* ellipsis … HORIZONTAL ELLIPSIS */
+/* 0x0aaf doubbaselinedot ? ??? */
+ { 0x0ab0, 0x2153 }, /* onethird ⅓ VULGAR FRACTION ONE THIRD */
+ { 0x0ab1, 0x2154 }, /* twothirds ⅔ VULGAR FRACTION TWO THIRDS */
+ { 0x0ab2, 0x2155 }, /* onefifth ⅕ VULGAR FRACTION ONE FIFTH */
+ { 0x0ab3, 0x2156 }, /* twofifths ⅖ VULGAR FRACTION TWO FIFTHS */
+ { 0x0ab4, 0x2157 }, /* threefifths ⅗ VULGAR FRACTION THREE FIFTHS */
+ { 0x0ab5, 0x2158 }, /* fourfifths ⅘ VULGAR FRACTION FOUR FIFTHS */
+ { 0x0ab6, 0x2159 }, /* onesixth ⅙ VULGAR FRACTION ONE SIXTH */
+ { 0x0ab7, 0x215a }, /* fivesixths ⅚ VULGAR FRACTION FIVE SIXTHS */
+ { 0x0ab8, 0x2105 }, /* careof ℅ CARE OF */
+ { 0x0abb, 0x2012 }, /* figdash ‒ FIGURE DASH */
+ { 0x0abc, 0x2329 }, /* leftanglebracket 〈 LEFT-POINTING ANGLE BRACKET */
+ { 0x0abd, 0x002e }, /* decimalpoint . FULL STOP */
+ { 0x0abe, 0x232a }, /* rightanglebracket 〉 RIGHT-POINTING ANGLE BRACKET */
+/* 0x0abf marker ? ??? */
+ { 0x0ac3, 0x215b }, /* oneeighth ⅛ VULGAR FRACTION ONE EIGHTH */
+ { 0x0ac4, 0x215c }, /* threeeighths ⅜ VULGAR FRACTION THREE EIGHTHS */
+ { 0x0ac5, 0x215d }, /* fiveeighths ⅝ VULGAR FRACTION FIVE EIGHTHS */
+ { 0x0ac6, 0x215e }, /* seveneighths ⅞ VULGAR FRACTION SEVEN EIGHTHS */
+ { 0x0ac9, 0x2122 }, /* trademark ™ TRADE MARK SIGN */
+ { 0x0aca, 0x2613 }, /* signaturemark ☓ SALTIRE */
+/* 0x0acb trademarkincircle ? ??? */
+ { 0x0acc, 0x25c1 }, /* leftopentriangle ◁ WHITE LEFT-POINTING TRIANGLE */
+ { 0x0acd, 0x25b7 }, /* rightopentriangle ▷ WHITE RIGHT-POINTING TRIANGLE */
+ { 0x0ace, 0x25cb }, /* emopencircle ○ WHITE CIRCLE */
+ { 0x0acf, 0x25a1 }, /* emopenrectangle □ WHITE SQUARE */
+ { 0x0ad0, 0x2018 }, /* leftsinglequotemark ‘ LEFT SINGLE QUOTATION MARK */
+ { 0x0ad1, 0x2019 }, /* rightsinglequotemark ’ RIGHT SINGLE QUOTATION MARK */
+ { 0x0ad2, 0x201c }, /* leftdoublequotemark “ LEFT DOUBLE QUOTATION MARK */
+ { 0x0ad3, 0x201d }, /* rightdoublequotemark ” RIGHT DOUBLE QUOTATION MARK */
+ { 0x0ad4, 0x211e }, /* prescription ℞ PRESCRIPTION TAKE */
+ { 0x0ad6, 0x2032 }, /* minutes ′ PRIME */
+ { 0x0ad7, 0x2033 }, /* seconds ″ DOUBLE PRIME */
+ { 0x0ad9, 0x271d }, /* latincross ✝ LATIN CROSS */
+/* 0x0ada hexagram ? ??? */
+ { 0x0adb, 0x25ac }, /* filledrectbullet ▬ BLACK RECTANGLE */
+ { 0x0adc, 0x25c0 }, /* filledlefttribullet ◀ BLACK LEFT-POINTING TRIANGLE */
+ { 0x0add, 0x25b6 }, /* filledrighttribullet ▶ BLACK RIGHT-POINTING TRIANGLE */
+ { 0x0ade, 0x25cf }, /* emfilledcircle ● BLACK CIRCLE */
+ { 0x0adf, 0x25a0 }, /* emfilledrect ■ BLACK SQUARE */
+ { 0x0ae0, 0x25e6 }, /* enopencircbullet ◦ WHITE BULLET */
+ { 0x0ae1, 0x25ab }, /* enopensquarebullet ▫ WHITE SMALL SQUARE */
+ { 0x0ae2, 0x25ad }, /* openrectbullet ▭ WHITE RECTANGLE */
+ { 0x0ae3, 0x25b3 }, /* opentribulletup △ WHITE UP-POINTING TRIANGLE */
+ { 0x0ae4, 0x25bd }, /* opentribulletdown ▽ WHITE DOWN-POINTING TRIANGLE */
+ { 0x0ae5, 0x2606 }, /* openstar ☆ WHITE STAR */
+ { 0x0ae6, 0x2022 }, /* enfilledcircbullet • BULLET */
+ { 0x0ae7, 0x25aa }, /* enfilledsqbullet ▪ BLACK SMALL SQUARE */
+ { 0x0ae8, 0x25b2 }, /* filledtribulletup ▲ BLACK UP-POINTING TRIANGLE */
+ { 0x0ae9, 0x25bc }, /* filledtribulletdown ▼ BLACK DOWN-POINTING TRIANGLE */
+ { 0x0aea, 0x261c }, /* leftpointer ☜ WHITE LEFT POINTING INDEX */
+ { 0x0aeb, 0x261e }, /* rightpointer ☞ WHITE RIGHT POINTING INDEX */
+ { 0x0aec, 0x2663 }, /* club ♣ BLACK CLUB SUIT */
+ { 0x0aed, 0x2666 }, /* diamond ♦ BLACK DIAMOND SUIT */
+ { 0x0aee, 0x2665 }, /* heart ♥ BLACK HEART SUIT */
+ { 0x0af0, 0x2720 }, /* maltesecross ✠ MALTESE CROSS */
+ { 0x0af1, 0x2020 }, /* dagger † DAGGER */
+ { 0x0af2, 0x2021 }, /* doubledagger ‡ DOUBLE DAGGER */
+ { 0x0af3, 0x2713 }, /* checkmark ✓ CHECK MARK */
+ { 0x0af4, 0x2717 }, /* ballotcross ✗ BALLOT X */
+ { 0x0af5, 0x266f }, /* musicalsharp ♯ MUSIC SHARP SIGN */
+ { 0x0af6, 0x266d }, /* musicalflat ♭ MUSIC FLAT SIGN */
+ { 0x0af7, 0x2642 }, /* malesymbol ♂ MALE SIGN */
+ { 0x0af8, 0x2640 }, /* femalesymbol ♀ FEMALE SIGN */
+ { 0x0af9, 0x260e }, /* telephone ☎ BLACK TELEPHONE */
+ { 0x0afa, 0x2315 }, /* telephonerecorder ⌕ TELEPHONE RECORDER */
+ { 0x0afb, 0x2117 }, /* phonographcopyright ℗ SOUND RECORDING COPYRIGHT */
+ { 0x0afc, 0x2038 }, /* caret ‸ CARET */
+ { 0x0afd, 0x201a }, /* singlelowquotemark ‚ SINGLE LOW-9 QUOTATION MARK */
+ { 0x0afe, 0x201e }, /* doublelowquotemark „ DOUBLE LOW-9 QUOTATION MARK */
+/* 0x0aff cursor ? ??? */
+ { 0x0ba3, 0x003c }, /* leftcaret < LESS-THAN SIGN */
+ { 0x0ba6, 0x003e }, /* rightcaret > GREATER-THAN SIGN */
+ { 0x0ba8, 0x2228 }, /* downcaret ∨ LOGICAL OR */
+ { 0x0ba9, 0x2227 }, /* upcaret ∧ LOGICAL AND */
+ { 0x0bc0, 0x00af }, /* overbar ¯ MACRON */
+ { 0x0bc2, 0x22a4 }, /* downtack ⊤ DOWN TACK */
+ { 0x0bc3, 0x2229 }, /* upshoe ∩ INTERSECTION */
+ { 0x0bc4, 0x230a }, /* downstile ⌊ LEFT FLOOR */
+ { 0x0bc6, 0x005f }, /* underbar _ LOW LINE */
+ { 0x0bca, 0x2218 }, /* jot ∘ RING OPERATOR */
+ { 0x0bcc, 0x2395 }, /* quad ⎕ APL FUNCTIONAL SYMBOL QUAD (Unicode 3.0) */
+ { 0x0bce, 0x22a5 }, /* uptack ⊥ UP TACK */
+ { 0x0bcf, 0x25cb }, /* circle ○ WHITE CIRCLE */
+ { 0x0bd3, 0x2308 }, /* upstile ⌈ LEFT CEILING */
+ { 0x0bd6, 0x222a }, /* downshoe ∪ UNION */
+ { 0x0bd8, 0x2283 }, /* rightshoe ⊃ SUPERSET OF */
+ { 0x0bda, 0x2282 }, /* leftshoe ⊂ SUBSET OF */
+ { 0x0bdc, 0x22a3 }, /* lefttack ⊣ LEFT TACK */
+ { 0x0bfc, 0x22a2 }, /* righttack ⊢ RIGHT TACK */
+ { 0x0cdf, 0x2017 }, /* hebrew_doublelowline ‗ DOUBLE LOW LINE */
+ { 0x0ce0, 0x05d0 }, /* hebrew_aleph א HEBREW LETTER ALEF */
+ { 0x0ce1, 0x05d1 }, /* hebrew_bet ב HEBREW LETTER BET */
+ { 0x0ce2, 0x05d2 }, /* hebrew_gimel ג HEBREW LETTER GIMEL */
+ { 0x0ce3, 0x05d3 }, /* hebrew_dalet ד HEBREW LETTER DALET */
+ { 0x0ce4, 0x05d4 }, /* hebrew_he ה HEBREW LETTER HE */
+ { 0x0ce5, 0x05d5 }, /* hebrew_waw ו HEBREW LETTER VAV */
+ { 0x0ce6, 0x05d6 }, /* hebrew_zain ז HEBREW LETTER ZAYIN */
+ { 0x0ce7, 0x05d7 }, /* hebrew_chet ח HEBREW LETTER HET */
+ { 0x0ce8, 0x05d8 }, /* hebrew_tet ט HEBREW LETTER TET */
+ { 0x0ce9, 0x05d9 }, /* hebrew_yod י HEBREW LETTER YOD */
+ { 0x0cea, 0x05da }, /* hebrew_finalkaph ך HEBREW LETTER FINAL KAF */
+ { 0x0ceb, 0x05db }, /* hebrew_kaph כ HEBREW LETTER KAF */
+ { 0x0cec, 0x05dc }, /* hebrew_lamed ל HEBREW LETTER LAMED */
+ { 0x0ced, 0x05dd }, /* hebrew_finalmem ם HEBREW LETTER FINAL MEM */
+ { 0x0cee, 0x05de }, /* hebrew_mem מ HEBREW LETTER MEM */
+ { 0x0cef, 0x05df }, /* hebrew_finalnun ן HEBREW LETTER FINAL NUN */
+ { 0x0cf0, 0x05e0 }, /* hebrew_nun נ HEBREW LETTER NUN */
+ { 0x0cf1, 0x05e1 }, /* hebrew_samech ס HEBREW LETTER SAMEKH */
+ { 0x0cf2, 0x05e2 }, /* hebrew_ayin ע HEBREW LETTER AYIN */
+ { 0x0cf3, 0x05e3 }, /* hebrew_finalpe ף HEBREW LETTER FINAL PE */
+ { 0x0cf4, 0x05e4 }, /* hebrew_pe פ HEBREW LETTER PE */
+ { 0x0cf5, 0x05e5 }, /* hebrew_finalzade ץ HEBREW LETTER FINAL TSADI */
+ { 0x0cf6, 0x05e6 }, /* hebrew_zade צ HEBREW LETTER TSADI */
+ { 0x0cf7, 0x05e7 }, /* hebrew_qoph ק HEBREW LETTER QOF */
+ { 0x0cf8, 0x05e8 }, /* hebrew_resh ר HEBREW LETTER RESH */
+ { 0x0cf9, 0x05e9 }, /* hebrew_shin ש HEBREW LETTER SHIN */
+ { 0x0cfa, 0x05ea }, /* hebrew_taw ת HEBREW LETTER TAV */
+ { 0x0da1, 0x0e01 }, /* Thai_kokai ก THAI CHARACTER KO KAI */
+ { 0x0da2, 0x0e02 }, /* Thai_khokhai ข THAI CHARACTER KHO KHAI */
+ { 0x0da3, 0x0e03 }, /* Thai_khokhuat ฃ THAI CHARACTER KHO KHUAT */
+ { 0x0da4, 0x0e04 }, /* Thai_khokhwai ค THAI CHARACTER KHO KHWAI */
+ { 0x0da5, 0x0e05 }, /* Thai_khokhon ฅ THAI CHARACTER KHO KHON */
+ { 0x0da6, 0x0e06 }, /* Thai_khorakhang ฆ THAI CHARACTER KHO RAKHANG */
+ { 0x0da7, 0x0e07 }, /* Thai_ngongu ง THAI CHARACTER NGO NGU */
+ { 0x0da8, 0x0e08 }, /* Thai_chochan จ THAI CHARACTER CHO CHAN */
+ { 0x0da9, 0x0e09 }, /* Thai_choching ฉ THAI CHARACTER CHO CHING */
+ { 0x0daa, 0x0e0a }, /* Thai_chochang ช THAI CHARACTER CHO CHANG */
+ { 0x0dab, 0x0e0b }, /* Thai_soso ซ THAI CHARACTER SO SO */
+ { 0x0dac, 0x0e0c }, /* Thai_chochoe ฌ THAI CHARACTER CHO CHOE */
+ { 0x0dad, 0x0e0d }, /* Thai_yoying ญ THAI CHARACTER YO YING */
+ { 0x0dae, 0x0e0e }, /* Thai_dochada ฎ THAI CHARACTER DO CHADA */
+ { 0x0daf, 0x0e0f }, /* Thai_topatak ฏ THAI CHARACTER TO PATAK */
+ { 0x0db0, 0x0e10 }, /* Thai_thothan ฐ THAI CHARACTER THO THAN */
+ { 0x0db1, 0x0e11 }, /* Thai_thonangmontho ฑ THAI CHARACTER THO NANGMONTHO */
+ { 0x0db2, 0x0e12 }, /* Thai_thophuthao ฒ THAI CHARACTER THO PHUTHAO */
+ { 0x0db3, 0x0e13 }, /* Thai_nonen ณ THAI CHARACTER NO NEN */
+ { 0x0db4, 0x0e14 }, /* Thai_dodek ด THAI CHARACTER DO DEK */
+ { 0x0db5, 0x0e15 }, /* Thai_totao ต THAI CHARACTER TO TAO */
+ { 0x0db6, 0x0e16 }, /* Thai_thothung ถ THAI CHARACTER THO THUNG */
+ { 0x0db7, 0x0e17 }, /* Thai_thothahan ท THAI CHARACTER THO THAHAN */
+ { 0x0db8, 0x0e18 }, /* Thai_thothong ธ THAI CHARACTER THO THONG */
+ { 0x0db9, 0x0e19 }, /* Thai_nonu น THAI CHARACTER NO NU */
+ { 0x0dba, 0x0e1a }, /* Thai_bobaimai บ THAI CHARACTER BO BAIMAI */
+ { 0x0dbb, 0x0e1b }, /* Thai_popla ป THAI CHARACTER PO PLA */
+ { 0x0dbc, 0x0e1c }, /* Thai_phophung ผ THAI CHARACTER PHO PHUNG */
+ { 0x0dbd, 0x0e1d }, /* Thai_fofa ฝ THAI CHARACTER FO FA */
+ { 0x0dbe, 0x0e1e }, /* Thai_phophan พ THAI CHARACTER PHO PHAN */
+ { 0x0dbf, 0x0e1f }, /* Thai_fofan ฟ THAI CHARACTER FO FAN */
+ { 0x0dc0, 0x0e20 }, /* Thai_phosamphao ภ THAI CHARACTER PHO SAMPHAO */
+ { 0x0dc1, 0x0e21 }, /* Thai_moma ม THAI CHARACTER MO MA */
+ { 0x0dc2, 0x0e22 }, /* Thai_yoyak ย THAI CHARACTER YO YAK */
+ { 0x0dc3, 0x0e23 }, /* Thai_rorua ร THAI CHARACTER RO RUA */
+ { 0x0dc4, 0x0e24 }, /* Thai_ru ฤ THAI CHARACTER RU */
+ { 0x0dc5, 0x0e25 }, /* Thai_loling ล THAI CHARACTER LO LING */
+ { 0x0dc6, 0x0e26 }, /* Thai_lu ฦ THAI CHARACTER LU */
+ { 0x0dc7, 0x0e27 }, /* Thai_wowaen ว THAI CHARACTER WO WAEN */
+ { 0x0dc8, 0x0e28 }, /* Thai_sosala ศ THAI CHARACTER SO SALA */
+ { 0x0dc9, 0x0e29 }, /* Thai_sorusi ษ THAI CHARACTER SO RUSI */
+ { 0x0dca, 0x0e2a }, /* Thai_sosua ส THAI CHARACTER SO SUA */
+ { 0x0dcb, 0x0e2b }, /* Thai_hohip ห THAI CHARACTER HO HIP */
+ { 0x0dcc, 0x0e2c }, /* Thai_lochula ฬ THAI CHARACTER LO CHULA */
+ { 0x0dcd, 0x0e2d }, /* Thai_oang อ THAI CHARACTER O ANG */
+ { 0x0dce, 0x0e2e }, /* Thai_honokhuk ฮ THAI CHARACTER HO NOKHUK */
+ { 0x0dcf, 0x0e2f }, /* Thai_paiyannoi ฯ THAI CHARACTER PAIYANNOI */
+ { 0x0dd0, 0x0e30 }, /* Thai_saraa ะ THAI CHARACTER SARA A */
+ { 0x0dd1, 0x0e31 }, /* Thai_maihanakat ั THAI CHARACTER MAI HAN-AKAT */
+ { 0x0dd2, 0x0e32 }, /* Thai_saraaa า THAI CHARACTER SARA AA */
+ { 0x0dd3, 0x0e33 }, /* Thai_saraam ำ THAI CHARACTER SARA AM */
+ { 0x0dd4, 0x0e34 }, /* Thai_sarai ิ THAI CHARACTER SARA I */
+ { 0x0dd5, 0x0e35 }, /* Thai_saraii ี THAI CHARACTER SARA II */
+ { 0x0dd6, 0x0e36 }, /* Thai_saraue ึ THAI CHARACTER SARA UE */
+ { 0x0dd7, 0x0e37 }, /* Thai_sarauee ื THAI CHARACTER SARA UEE */
+ { 0x0dd8, 0x0e38 }, /* Thai_sarau ุ THAI CHARACTER SARA U */
+ { 0x0dd9, 0x0e39 }, /* Thai_sarauu ู THAI CHARACTER SARA UU */
+ { 0x0dda, 0x0e3a }, /* Thai_phinthu ฺ THAI CHARACTER PHINTHU */
+ { 0x0dde, 0x0e3e }, /* Thai_maihanakat_maitho ฾ ??? */
+ { 0x0ddf, 0x0e3f }, /* Thai_baht ฿ THAI CURRENCY SYMBOL BAHT */
+ { 0x0de0, 0x0e40 }, /* Thai_sarae เ THAI CHARACTER SARA E */
+ { 0x0de1, 0x0e41 }, /* Thai_saraae แ THAI CHARACTER SARA AE */
+ { 0x0de2, 0x0e42 }, /* Thai_sarao โ THAI CHARACTER SARA O */
+ { 0x0de3, 0x0e43 }, /* Thai_saraaimaimuan ใ THAI CHARACTER SARA AI MAIMUAN */
+ { 0x0de4, 0x0e44 }, /* Thai_saraaimaimalai ไ THAI CHARACTER SARA AI MAIMALAI */
+ { 0x0de5, 0x0e45 }, /* Thai_lakkhangyao ๅ THAI CHARACTER LAKKHANGYAO */
+ { 0x0de6, 0x0e46 }, /* Thai_maiyamok ๆ THAI CHARACTER MAIYAMOK */
+ { 0x0de7, 0x0e47 }, /* Thai_maitaikhu ็ THAI CHARACTER MAITAIKHU */
+ { 0x0de8, 0x0e48 }, /* Thai_maiek ่ THAI CHARACTER MAI EK */
+ { 0x0de9, 0x0e49 }, /* Thai_maitho ้ THAI CHARACTER MAI THO */
+ { 0x0dea, 0x0e4a }, /* Thai_maitri ๊ THAI CHARACTER MAI TRI */
+ { 0x0deb, 0x0e4b }, /* Thai_maichattawa ๋ THAI CHARACTER MAI CHATTAWA */
+ { 0x0dec, 0x0e4c }, /* Thai_thanthakhat ์ THAI CHARACTER THANTHAKHAT */
+ { 0x0ded, 0x0e4d }, /* Thai_nikhahit ํ THAI CHARACTER NIKHAHIT */
+ { 0x0df0, 0x0e50 }, /* Thai_leksun ๐ THAI DIGIT ZERO */
+ { 0x0df1, 0x0e51 }, /* Thai_leknung ๑ THAI DIGIT ONE */
+ { 0x0df2, 0x0e52 }, /* Thai_leksong ๒ THAI DIGIT TWO */
+ { 0x0df3, 0x0e53 }, /* Thai_leksam ๓ THAI DIGIT THREE */
+ { 0x0df4, 0x0e54 }, /* Thai_leksi ๔ THAI DIGIT FOUR */
+ { 0x0df5, 0x0e55 }, /* Thai_lekha ๕ THAI DIGIT FIVE */
+ { 0x0df6, 0x0e56 }, /* Thai_lekhok ๖ THAI DIGIT SIX */
+ { 0x0df7, 0x0e57 }, /* Thai_lekchet ๗ THAI DIGIT SEVEN */
+ { 0x0df8, 0x0e58 }, /* Thai_lekpaet ๘ THAI DIGIT EIGHT */
+ { 0x0df9, 0x0e59 }, /* Thai_lekkao ๙ THAI DIGIT NINE */
+ { 0x0ea1, 0x3131 }, /* Hangul_Kiyeog ㄱ HANGUL LETTER KIYEOK */
+ { 0x0ea2, 0x3132 }, /* Hangul_SsangKiyeog ㄲ HANGUL LETTER SSANGKIYEOK */
+ { 0x0ea3, 0x3133 }, /* Hangul_KiyeogSios ㄳ HANGUL LETTER KIYEOK-SIOS */
+ { 0x0ea4, 0x3134 }, /* Hangul_Nieun ㄴ HANGUL LETTER NIEUN */
+ { 0x0ea5, 0x3135 }, /* Hangul_NieunJieuj ㄵ HANGUL LETTER NIEUN-CIEUC */
+ { 0x0ea6, 0x3136 }, /* Hangul_NieunHieuh ㄶ HANGUL LETTER NIEUN-HIEUH */
+ { 0x0ea7, 0x3137 }, /* Hangul_Dikeud ㄷ HANGUL LETTER TIKEUT */
+ { 0x0ea8, 0x3138 }, /* Hangul_SsangDikeud ㄸ HANGUL LETTER SSANGTIKEUT */
+ { 0x0ea9, 0x3139 }, /* Hangul_Rieul ㄹ HANGUL LETTER RIEUL */
+ { 0x0eaa, 0x313a }, /* Hangul_RieulKiyeog ㄺ HANGUL LETTER RIEUL-KIYEOK */
+ { 0x0eab, 0x313b }, /* Hangul_RieulMieum ㄻ HANGUL LETTER RIEUL-MIEUM */
+ { 0x0eac, 0x313c }, /* Hangul_RieulPieub ㄼ HANGUL LETTER RIEUL-PIEUP */
+ { 0x0ead, 0x313d }, /* Hangul_RieulSios ㄽ HANGUL LETTER RIEUL-SIOS */
+ { 0x0eae, 0x313e }, /* Hangul_RieulTieut ㄾ HANGUL LETTER RIEUL-THIEUTH */
+ { 0x0eaf, 0x313f }, /* Hangul_RieulPhieuf ㄿ HANGUL LETTER RIEUL-PHIEUPH */
+ { 0x0eb0, 0x3140 }, /* Hangul_RieulHieuh ㅀ HANGUL LETTER RIEUL-HIEUH */
+ { 0x0eb1, 0x3141 }, /* Hangul_Mieum ㅁ HANGUL LETTER MIEUM */
+ { 0x0eb2, 0x3142 }, /* Hangul_Pieub ㅂ HANGUL LETTER PIEUP */
+ { 0x0eb3, 0x3143 }, /* Hangul_SsangPieub ㅃ HANGUL LETTER SSANGPIEUP */
+ { 0x0eb4, 0x3144 }, /* Hangul_PieubSios ㅄ HANGUL LETTER PIEUP-SIOS */
+ { 0x0eb5, 0x3145 }, /* Hangul_Sios ㅅ HANGUL LETTER SIOS */
+ { 0x0eb6, 0x3146 }, /* Hangul_SsangSios ㅆ HANGUL LETTER SSANGSIOS */
+ { 0x0eb7, 0x3147 }, /* Hangul_Ieung ㅇ HANGUL LETTER IEUNG */
+ { 0x0eb8, 0x3148 }, /* Hangul_Jieuj ㅈ HANGUL LETTER CIEUC */
+ { 0x0eb9, 0x3149 }, /* Hangul_SsangJieuj ㅉ HANGUL LETTER SSANGCIEUC */
+ { 0x0eba, 0x314a }, /* Hangul_Cieuc ㅊ HANGUL LETTER CHIEUCH */
+ { 0x0ebb, 0x314b }, /* Hangul_Khieuq ㅋ HANGUL LETTER KHIEUKH */
+ { 0x0ebc, 0x314c }, /* Hangul_Tieut ㅌ HANGUL LETTER THIEUTH */
+ { 0x0ebd, 0x314d }, /* Hangul_Phieuf ㅍ HANGUL LETTER PHIEUPH */
+ { 0x0ebe, 0x314e }, /* Hangul_Hieuh ㅎ HANGUL LETTER HIEUH */
+ { 0x0ebf, 0x314f }, /* Hangul_A ㅏ HANGUL LETTER A */
+ { 0x0ec0, 0x3150 }, /* Hangul_AE ㅐ HANGUL LETTER AE */
+ { 0x0ec1, 0x3151 }, /* Hangul_YA ㅑ HANGUL LETTER YA */
+ { 0x0ec2, 0x3152 }, /* Hangul_YAE ㅒ HANGUL LETTER YAE */
+ { 0x0ec3, 0x3153 }, /* Hangul_EO ㅓ HANGUL LETTER EO */
+ { 0x0ec4, 0x3154 }, /* Hangul_E ㅔ HANGUL LETTER E */
+ { 0x0ec5, 0x3155 }, /* Hangul_YEO ㅕ HANGUL LETTER YEO */
+ { 0x0ec6, 0x3156 }, /* Hangul_YE ㅖ HANGUL LETTER YE */
+ { 0x0ec7, 0x3157 }, /* Hangul_O ㅗ HANGUL LETTER O */
+ { 0x0ec8, 0x3158 }, /* Hangul_WA ㅘ HANGUL LETTER WA */
+ { 0x0ec9, 0x3159 }, /* Hangul_WAE ㅙ HANGUL LETTER WAE */
+ { 0x0eca, 0x315a }, /* Hangul_OE ㅚ HANGUL LETTER OE */
+ { 0x0ecb, 0x315b }, /* Hangul_YO ㅛ HANGUL LETTER YO */
+ { 0x0ecc, 0x315c }, /* Hangul_U ㅜ HANGUL LETTER U */
+ { 0x0ecd, 0x315d }, /* Hangul_WEO ㅝ HANGUL LETTER WEO */
+ { 0x0ece, 0x315e }, /* Hangul_WE ㅞ HANGUL LETTER WE */
+ { 0x0ecf, 0x315f }, /* Hangul_WI ㅟ HANGUL LETTER WI */
+ { 0x0ed0, 0x3160 }, /* Hangul_YU ㅠ HANGUL LETTER YU */
+ { 0x0ed1, 0x3161 }, /* Hangul_EU ㅡ HANGUL LETTER EU */
+ { 0x0ed2, 0x3162 }, /* Hangul_YI ㅢ HANGUL LETTER YI */
+ { 0x0ed3, 0x3163 }, /* Hangul_I ㅣ HANGUL LETTER I */
+ { 0x0ed4, 0x11a8 }, /* Hangul_J_Kiyeog ᆨ HANGUL JONGSEONG KIYEOK */
+ { 0x0ed5, 0x11a9 }, /* Hangul_J_SsangKiyeog ᆩ HANGUL JONGSEONG SSANGKIYEOK */
+ { 0x0ed6, 0x11aa }, /* Hangul_J_KiyeogSios ᆪ HANGUL JONGSEONG KIYEOK-SIOS */
+ { 0x0ed7, 0x11ab }, /* Hangul_J_Nieun ᆫ HANGUL JONGSEONG NIEUN */
+ { 0x0ed8, 0x11ac }, /* Hangul_J_NieunJieuj ᆬ HANGUL JONGSEONG NIEUN-CIEUC */
+ { 0x0ed9, 0x11ad }, /* Hangul_J_NieunHieuh ᆭ HANGUL JONGSEONG NIEUN-HIEUH */
+ { 0x0eda, 0x11ae }, /* Hangul_J_Dikeud ᆮ HANGUL JONGSEONG TIKEUT */
+ { 0x0edb, 0x11af }, /* Hangul_J_Rieul ᆯ HANGUL JONGSEONG RIEUL */
+ { 0x0edc, 0x11b0 }, /* Hangul_J_RieulKiyeog ᆰ HANGUL JONGSEONG RIEUL-KIYEOK */
+ { 0x0edd, 0x11b1 }, /* Hangul_J_RieulMieum ᆱ HANGUL JONGSEONG RIEUL-MIEUM */
+ { 0x0ede, 0x11b2 }, /* Hangul_J_RieulPieub ᆲ HANGUL JONGSEONG RIEUL-PIEUP */
+ { 0x0edf, 0x11b3 }, /* Hangul_J_RieulSios ᆳ HANGUL JONGSEONG RIEUL-SIOS */
+ { 0x0ee0, 0x11b4 }, /* Hangul_J_RieulTieut ᆴ HANGUL JONGSEONG RIEUL-THIEUTH */
+ { 0x0ee1, 0x11b5 }, /* Hangul_J_RieulPhieuf ᆵ HANGUL JONGSEONG RIEUL-PHIEUPH */
+ { 0x0ee2, 0x11b6 }, /* Hangul_J_RieulHieuh ᆶ HANGUL JONGSEONG RIEUL-HIEUH */
+ { 0x0ee3, 0x11b7 }, /* Hangul_J_Mieum ᆷ HANGUL JONGSEONG MIEUM */
+ { 0x0ee4, 0x11b8 }, /* Hangul_J_Pieub ᆸ HANGUL JONGSEONG PIEUP */
+ { 0x0ee5, 0x11b9 }, /* Hangul_J_PieubSios ᆹ HANGUL JONGSEONG PIEUP-SIOS */
+ { 0x0ee6, 0x11ba }, /* Hangul_J_Sios ᆺ HANGUL JONGSEONG SIOS */
+ { 0x0ee7, 0x11bb }, /* Hangul_J_SsangSios ᆻ HANGUL JONGSEONG SSANGSIOS */
+ { 0x0ee8, 0x11bc }, /* Hangul_J_Ieung ᆼ HANGUL JONGSEONG IEUNG */
+ { 0x0ee9, 0x11bd }, /* Hangul_J_Jieuj ᆽ HANGUL JONGSEONG CIEUC */
+ { 0x0eea, 0x11be }, /* Hangul_J_Cieuc ᆾ HANGUL JONGSEONG CHIEUCH */
+ { 0x0eeb, 0x11bf }, /* Hangul_J_Khieuq ᆿ HANGUL JONGSEONG KHIEUKH */
+ { 0x0eec, 0x11c0 }, /* Hangul_J_Tieut ᇀ HANGUL JONGSEONG THIEUTH */
+ { 0x0eed, 0x11c1 }, /* Hangul_J_Phieuf ᇁ HANGUL JONGSEONG PHIEUPH */
+ { 0x0eee, 0x11c2 }, /* Hangul_J_Hieuh ᇂ HANGUL JONGSEONG HIEUH */
+ { 0x0eef, 0x316d }, /* Hangul_RieulYeorinHieuh ㅭ HANGUL LETTER RIEUL-YEORINHIEUH */
+ { 0x0ef0, 0x3171 }, /* Hangul_SunkyeongeumMieum ㅱ HANGUL LETTER KAPYEOUNMIEUM */
+ { 0x0ef1, 0x3178 }, /* Hangul_SunkyeongeumPieub ㅸ HANGUL LETTER KAPYEOUNPIEUP */
+ { 0x0ef2, 0x317f }, /* Hangul_PanSios ㅿ HANGUL LETTER PANSIOS */
+/* 0x0ef3 Hangul_KkogjiDalrinIeung ? ??? */
+ { 0x0ef4, 0x3184 }, /* Hangul_SunkyeongeumPhieuf ㆄ HANGUL LETTER KAPYEOUNPHIEUPH */
+ { 0x0ef5, 0x3186 }, /* Hangul_YeorinHieuh ㆆ HANGUL LETTER YEORINHIEUH */
+ { 0x0ef6, 0x318d }, /* Hangul_AraeA ㆍ HANGUL LETTER ARAEA */
+ { 0x0ef7, 0x318e }, /* Hangul_AraeAE ㆎ HANGUL LETTER ARAEAE */
+ { 0x0ef8, 0x11eb }, /* Hangul_J_PanSios ᇫ HANGUL JONGSEONG PANSIOS */
+/* 0x0ef9 Hangul_J_KkogjiDalrinIeung ? ??? */
+ { 0x0efa, 0x11f9 }, /* Hangul_J_YeorinHieuh ᇹ HANGUL JONGSEONG YEORINHIEUH */
+ { 0x0eff, 0x20a9 }, /* Korean_Won ₩ WON SIGN */
+ { 0x13bc, 0x0152 }, /* OE Œ LATIN CAPITAL LIGATURE OE */
+ { 0x13bd, 0x0153 }, /* oe œ LATIN SMALL LIGATURE OE */
+ { 0x13be, 0x0178 }, /* Ydiaeresis Ÿ LATIN CAPITAL LETTER Y WITH DIAERESIS */
+ { 0x20a0, 0x20a0 }, /* EcuSign ₠ EURO-CURRENCY SIGN */
+ { 0x20a1, 0x20a1 }, /* ColonSign ₡ COLON SIGN */
+ { 0x20a2, 0x20a2 }, /* CruzeiroSign ₢ CRUZEIRO SIGN */
+ { 0x20a3, 0x20a3 }, /* FFrancSign ₣ FRENCH FRANC SIGN */
+ { 0x20a4, 0x20a4 }, /* LiraSign ₤ LIRA SIGN */
+ { 0x20a5, 0x20a5 }, /* MillSign ₥ MILL SIGN */
+ { 0x20a6, 0x20a6 }, /* NairaSign ₦ NAIRA SIGN */
+ { 0x20a7, 0x20a7 }, /* PesetaSign ₧ PESETA SIGN */
+ { 0x20a8, 0x20a8 }, /* RupeeSign ₨ RUPEE SIGN */
+ { 0x20a9, 0x20a9 }, /* WonSign ₩ WON SIGN */
+ { 0x20aa, 0x20aa }, /* NewSheqelSign ₪ NEW SHEQEL SIGN */
+ { 0x20ab, 0x20ab }, /* DongSign ₫ DONG SIGN */
+ { 0x20ac, 0x20ac }, /* EuroSign € EURO SIGN */
+ // clang-format on
+};
+
+long keysym2ucs(KeySym keysym) {
+ int min = 0;
+ int max = sizeof(keysymtab) / sizeof(struct codepair) - 1;
+ int mid;
+
+ /* first check for Latin-1 characters (1:1 mapping) */
+ if ((keysym >= 0x0020 && keysym <= 0x007e) ||
+ (keysym >= 0x00a0 && keysym <= 0x00ff))
+ return keysym;
+
+ /* also check for directly encoded 24-bit UCS characters */
+ if ((keysym & 0xff000000) == 0x01000000) return keysym & 0x00ffffff;
+
+ /* binary search in table */
+ while (max >= min) {
+ mid = (min + max) / 2;
+ if (keysymtab[mid].keysym < keysym)
+ min = mid + 1;
+ else if (keysymtab[mid].keysym > keysym)
+ max = mid - 1;
+ else {
+ /* found it */
+ return keysymtab[mid].ucs;
+ }
+ }
+
+ /* no matching Unicode value found */
+ return -1;
+}
diff --git a/widget/x11/keysym2ucs.h b/widget/x11/keysym2ucs.h
new file mode 100644
index 0000000000..b081e27605
--- /dev/null
+++ b/widget/x11/keysym2ucs.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/*
+ *
+ * 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/. */
+/*
+ * This module converts keysym values into the corresponding ISO 10646-1
+ * (UCS, Unicode) values.
+ */
+
+#ifdef MOZ_X11
+# include <X11/X.h>
+#else
+# define KeySym unsigned int
+#endif /* MOZ_X11 */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+long keysym2ucs(KeySym keysym);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
diff --git a/widget/x11/moz.build b/widget/x11/moz.build
new file mode 100644
index 0000000000..81d345c0af
--- /dev/null
+++ b/widget/x11/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Gtk")
+
+SOURCES += [
+ "keysym2ucs.c",
+]
+
+FINAL_LIBRARY = "xul"
+
+CFLAGS += CONFIG["MOZ_X11_CFLAGS"]